Browse Source

Merge branch 'master' into webgl-port

rdb 4 years ago
parent
commit
db5b9c6f0f
100 changed files with 2375 additions and 594 deletions
  1. 29 29
      .github/workflows/ci.yml
  2. 6 6
      README.md
  3. 2 2
      direct/src/actor/Actor.py
  4. 2 2
      direct/src/actor/__init__.py
  5. 4 3
      direct/src/cluster/ClusterClient.py
  6. 5 4
      direct/src/cluster/ClusterServer.py
  7. 1 1
      direct/src/directbase/TestStart.py
  8. 1 1
      direct/src/directbase/ThreeUpStart.py
  9. 5 3
      direct/src/directdevices/DirectJoybox.py
  10. 8 8
      direct/src/directtools/DirectCameraControl.py
  11. 1 1
      direct/src/directtools/DirectUtil.py
  12. 4 3
      direct/src/directutil/Mopath.py
  13. 6 6
      direct/src/dist/FreezeTool.py
  14. 275 0
      direct/src/dist/_android.py
  15. 21 0
      direct/src/dist/_proto/Configuration_pb2.py
  16. 4 0
      direct/src/dist/_proto/README
  17. 22 0
      direct/src/dist/_proto/Resources_pb2.py
  18. 0 0
      direct/src/dist/_proto/__init__.py
  19. 21 0
      direct/src/dist/_proto/config_pb2.py
  20. 307 0
      direct/src/dist/_proto/files_pb2.py
  21. 22 0
      direct/src/dist/_proto/targeting_pb2.py
  22. 312 38
      direct/src/dist/commands.py
  23. 39 2
      direct/src/dist/icon.py
  24. 116 0
      direct/src/dist/installers.py
  25. 2 2
      direct/src/distributed/ClientRepository.py
  26. 3 3
      direct/src/distributed/ClientRepositoryBase.py
  27. 2 1
      direct/src/distributed/DistributedSmoothNode.py
  28. 3 3
      direct/src/distributed/DoInterestManager.py
  29. 5 3
      direct/src/distributed/TimeManager.py
  30. 3 1
      direct/src/gui/OnscreenImage.py
  31. 4 4
      direct/src/interval/IntervalTest.py
  32. 3 2
      direct/src/leveleditor/LevelEditorBase.py
  33. 6 5
      direct/src/showbase/JobManager.py
  34. 6 4
      direct/src/showbase/PythonUtil.py
  35. 14 10
      direct/src/showbase/ShowBase.py
  36. 1 0
      direct/src/showbase/ShowBaseGlobal.py
  37. 4 3
      direct/src/showbase/TaskThreaded.py
  38. 3 3
      direct/src/task/FrameProfiler.py
  39. 5 11
      direct/src/task/Task.py
  40. 5 3
      direct/src/task/Timer.py
  41. 3 3
      direct/src/tkpanels/AnimPanel.py
  42. 4 4
      direct/src/tkpanels/MopathRecorder.py
  43. 3 2
      direct/src/tkwidgets/Dial.py
  44. 3 2
      direct/src/tkwidgets/Floater.py
  45. 10 2
      dtool/src/dtoolbase/cmath.I
  46. 3 0
      dtool/src/dtoolbase/typeHandle.h
  47. 51 0
      dtool/src/dtoolbase/typeHandle_ext.cxx
  48. 3 0
      dtool/src/dtoolbase/typeHandle_ext.h
  49. 1 0
      dtool/src/interrogate/interfaceMakerPythonNative.cxx
  50. 0 18
      dtool/src/interrogatedb/py_compat.cxx
  51. 11 1
      dtool/src/interrogatedb/py_compat.h
  52. 10 0
      dtool/src/parser-inc/android/log.h
  53. 0 5
      dtool/src/parser-inc/stdtypedefs.h
  54. 2 0
      dtool/src/prc/CMakeLists.txt
  55. 1 1
      dtool/src/prc/androidLogStream.cxx
  56. 2 2
      dtool/src/prc/androidLogStream.h
  57. 2 1
      dtool/src/prc/configPageManager.cxx
  58. 2 0
      dtool/src/prc/configVariable.h
  59. 39 0
      dtool/src/prc/configVariable_ext.cxx
  60. 37 0
      dtool/src/prc/configVariable_ext.h
  61. 6 0
      dtool/src/prc/notify.cxx
  62. 1 0
      dtool/src/prc/p3prc_ext_composite.cxx
  63. 1 0
      makepanda/config.in
  64. 2 2
      makepanda/installpanda.py
  65. 23 17
      makepanda/makepackage.py
  66. 108 56
      makepanda/makepanda.py
  67. 77 60
      makepanda/makepandacore.py
  68. 129 27
      makepanda/makewheel.py
  69. 14 0
      panda/metalibs/pandagles2/pandagles2.cxx
  70. 31 9
      panda/src/android/PandaActivity.java
  71. 83 68
      panda/src/android/android_native_app_glue.c
  72. 2 6
      panda/src/android/android_native_app_glue.h
  73. 4 1
      panda/src/chan/partBundle.cxx
  74. 0 1
      panda/src/chan/partGroup.h
  75. 1 1
      panda/src/device/inputDeviceManager.cxx
  76. 38 4
      panda/src/device/winRawInputDevice.cxx
  77. 4 0
      panda/src/display/CMakeLists.txt
  78. 0 1
      panda/src/display/displayInformation.h
  79. 2 0
      panda/src/display/displayRegion.I
  80. 6 0
      panda/src/display/frameBufferProperties.h
  81. 75 0
      panda/src/display/frameBufferProperties_ext.cxx
  82. 38 0
      panda/src/display/frameBufferProperties_ext.h
  83. 2 0
      panda/src/display/graphicsOutput.h
  84. 2 0
      panda/src/display/graphicsPipeSelection.h
  85. 31 0
      panda/src/display/graphicsPipeSelection_ext.cxx
  86. 37 0
      panda/src/display/graphicsPipeSelection_ext.h
  87. 2 25
      panda/src/display/graphicsStateGuardian.cxx
  88. 5 1
      panda/src/display/graphicsWindow.h
  89. 17 0
      panda/src/display/graphicsWindow_ext.cxx
  90. 2 0
      panda/src/display/graphicsWindow_ext.h
  91. 2 0
      panda/src/display/p3display_ext_composite.cxx
  92. 1 1
      panda/src/display/pythonGraphicsWindowProc.h
  93. 4 1
      panda/src/display/windowProperties.h
  94. 55 19
      panda/src/display/windowProperties_ext.cxx
  95. 3 0
      panda/src/display/windowProperties_ext.h
  96. 82 67
      panda/src/doc/eggSyntax.txt
  97. 10 10
      panda/src/doc/howto.MultiGenModelFlags
  98. 2 5
      panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx
  99. 2 2
      panda/src/dxgsg9/dxTextureContext9.cxx
  100. 2 2
      panda/src/dxgsg9/dxgsg9base.h

+ 29 - 29
.github/workflows/ci.yml

@@ -15,7 +15,7 @@ jobs:
         - ubuntu-bionic-coverage-ninja
         - macos-eigen-coverage-unity-xcode
         - macos-nometa-standard-makefile
-#       - windows-standard-unity-msvc # FIXME when GH Actions runners upgrade CMake to >=3.16.1
+        - windows-standard-unity-msvc
         - windows-nopython-nometa-standard-msvc
 
         include:
@@ -40,7 +40,7 @@ jobs:
           eigen: NO
 
         - profile: macos-eigen-coverage-unity-xcode
-          os: macOS-latest
+          os: macOS-10.15
           config: Coverage
           unity: YES
           generator: Xcode
@@ -50,7 +50,7 @@ jobs:
           eigen: YES
 
         - profile: macos-nometa-standard-makefile
-          os: macOS-latest
+          os: macOS-10.15
           config: Standard
           unity: NO
           generator: Unix Makefiles
@@ -60,20 +60,20 @@ jobs:
           eigen: NO
 
         - profile: windows-standard-unity-msvc
-          os: windows-2019
+          os: windows-2022
           config: Standard
           unity: YES
-          generator: Visual Studio 16 2019
+          generator: Visual Studio 17 2022
           compiler: Default
           metalibs: YES
           python: YES
           eigen: NO
 
         - profile: windows-nopython-nometa-standard-msvc
-          os: windows-2019
+          os: windows-2022
           config: Standard
           unity: NO
-          generator: Visual Studio 16 2019
+          generator: Visual Studio 17 2022
           compiler: Default
           metalibs: NO
           python: NO
@@ -92,10 +92,10 @@ jobs:
     - name: Install dependencies (macOS)
       if: runner.os == 'macOS'
       run: |
-        curl -O https://www.panda3d.org/download/panda3d-1.10.9/panda3d-1.10.9-tools-mac.tar.gz
-        tar -xf panda3d-1.10.9-tools-mac.tar.gz
-        mv panda3d-1.10.9/thirdparty thirdparty
-        rmdir panda3d-1.10.9
+        curl -O https://www.panda3d.org/download/panda3d-1.10.10/panda3d-1.10.10-tools-mac.tar.gz
+        tar -xf panda3d-1.10.10-tools-mac.tar.gz
+        mv panda3d-1.10.10/thirdparty thirdparty
+        rmdir panda3d-1.10.10
 
         # Temporary hack so that pzip can run, since we are about to remove Cg anyway.
         install_name_tool -id "$(pwd)/thirdparty/darwin-libs-a/nvidiacg/lib/libCg.dylib" thirdparty/darwin-libs-a/nvidiacg/lib/libCg.dylib
@@ -124,16 +124,16 @@ jobs:
       uses: actions/cache@v1
       with:
         path: thirdparty
-        key: ci-cmake-${{ runner.OS }}-thirdparty-v1.10.9-r1
+        key: ci-cmake-${{ runner.OS }}-thirdparty-v1.10.10-r1
     - name: Install dependencies (Windows)
       if: runner.os == 'Windows'
       shell: powershell
       run: |
         if (!(Test-Path thirdparty/win-libs-vc14-x64)) {
           $wc = New-Object System.Net.WebClient
-          $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.9/panda3d-1.10.9-tools-win64.zip", "thirdparty-tools.zip")
+          $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.10/panda3d-1.10.10-tools-win64.zip", "thirdparty-tools.zip")
           Expand-Archive -Path thirdparty-tools.zip
-          Move-Item -Path thirdparty-tools/panda3d-1.10.9/thirdparty -Destination .
+          Move-Item -Path thirdparty-tools/panda3d-1.10.10/thirdparty -Destination .
         }
 
     - name: ccache (non-Windows)
@@ -153,7 +153,7 @@ jobs:
         cd build
 
         if ${{ matrix.compiler == 'Clang' }}; then
-          if [[ "$CMAKE_GENERATOR" == *Studio*2019* ]]; then
+          if [[ "$CMAKE_GENERATOR" =~ Studio.+20(19|22) ]]; then
             export CMAKE_GENERATOR_TOOLSET=ClangCL
           elif [[ "$CMAKE_GENERATOR" == *Studio* ]]; then
             export CMAKE_GENERATOR_TOOLSET=LLVM
@@ -186,7 +186,7 @@ jobs:
 
     - name: Setup Python (Python 3.6)
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
         python-version: 3.6
     - name: Configure (Python 3.6)
@@ -218,7 +218,7 @@ jobs:
 
     - name: Setup Python (Python 3.7)
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
         python-version: 3.7
     - name: Configure (Python 3.7)
@@ -250,7 +250,7 @@ jobs:
 
     - name: Setup Python (Python 3.8)
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
         python-version: 3.8
     - name: Configure (Python 3.8)
@@ -282,7 +282,7 @@ jobs:
 
     - name: Setup Python (Python 3.9)
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
         python-version: 3.9
     - name: Configure (Python 3.9)
@@ -333,7 +333,7 @@ jobs:
     if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
     strategy:
       matrix:
-        os: [ubuntu-18.04, windows-2016, macOS-latest]
+        os: [ubuntu-18.04, windows-2016, macOS-11]
     runs-on: ${{ matrix.os }}
     steps:
     - uses: actions/checkout@v1
@@ -347,19 +347,19 @@ jobs:
       shell: powershell
       run: |
         $wc = New-Object System.Net.WebClient
-        $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.9/panda3d-1.10.9-tools-win64.zip", "thirdparty-tools.zip")
+        $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.10/panda3d-1.10.10-tools-win64.zip", "thirdparty-tools.zip")
         Expand-Archive -Path thirdparty-tools.zip
-        Move-Item -Path thirdparty-tools/panda3d-1.10.9/thirdparty -Destination .
+        Move-Item -Path thirdparty-tools/panda3d-1.10.10/thirdparty -Destination .
     - name: Get thirdparty packages (macOS)
       if: runner.os == 'macOS'
       run: |
-        curl -O https://www.panda3d.org/download/panda3d-1.10.9/panda3d-1.10.9-tools-mac.tar.gz
-        tar -xf panda3d-1.10.9-tools-mac.tar.gz
-        mv panda3d-1.10.9/thirdparty thirdparty
-        rmdir panda3d-1.10.9
+        curl -O https://www.panda3d.org/download/panda3d-1.10.10/panda3d-1.10.10-tools-mac.tar.gz
+        tar -xf panda3d-1.10.10-tools-mac.tar.gz
+        mv panda3d-1.10.10/thirdparty thirdparty
+        rmdir panda3d-1.10.10
         (cd thirdparty/darwin-libs-a && rm -rf rocket)
     - name: Set up Python 3.9
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
         python-version: 3.9
     - name: Build Python 3.9
@@ -372,7 +372,7 @@ jobs:
         python -m pip install pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
     - name: Set up Python 3.8
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
         python-version: 3.8
     - name: Build Python 3.8
@@ -385,7 +385,7 @@ jobs:
         python -m pip install pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
     - name: Set up Python 3.7
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
         python-version: 3.7
     - name: Build Python 3.7

+ 6 - 6
README.md

@@ -52,8 +52,8 @@ Building Panda3D
 Windows
 -------
 
-You can build Panda3D with the Microsoft Visual C++ 2015, 2017 or 2019 compiler,
-which can be downloaded for free from the [Visual Studio site](https://visualstudio.microsoft.com/downloads/).
+You can build Panda3D with the Microsoft Visual C++ 2015, 2017, 2019 or 2022
+compiler, which can be downloaded for free from the [Visual Studio site](https://visualstudio.microsoft.com/downloads/).
 You will also need to install the [Windows 10 SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk),
 and if you intend to target Windows Vista, you will also need the
 [Windows 8.1 SDK](https://go.microsoft.com/fwlink/p/?LinkId=323507).
@@ -69,12 +69,12 @@ building them from source.
 
 After acquiring these dependencies, you can build Panda3D from the command
 prompt using the following command.  Change the `--msvc-version` option based
-on your version of Visual C++; 2019 is 14.2, 2017 is 14.1, and 2015 is 14.
-Remove the `--windows-sdk=10` option if you need to support Windows Vista,
-which requires the Windows 8.1 SDK.
+on your version of Visual C++; 2022 is 14.3, 2019 is 14.2, 2017 is 14.1, and
+2015 is 14.  Remove the `--windows-sdk=10` option if you need to support
+Windows Vista, which requires the Windows 8.1 SDK.
 
 ```bash
-makepanda\makepanda.bat --everything --installer --msvc-version=14.2 --windows-sdk=10 --no-eigen --threads=2
+makepanda\makepanda.bat --everything --installer --msvc-version=14.3 --windows-sdk=10 --no-eigen --threads=2
 ```
 
 When the build succeeds, it will produce an .exe file that you can use to

+ 2 - 2
direct/src/actor/Actor.py

@@ -1,7 +1,7 @@
 """Actor module: contains the Actor class.
 
-See the :ref:`models-and-actors` page in the Programming Guide to learn
-more about loading models and animated actors.
+See the :ref:`loading-actors-and-animations` page in the Programming Guide
+to learn more about loading animated models.
 """
 
 __all__ = ['Actor']

+ 2 - 2
direct/src/actor/__init__.py

@@ -5,6 +5,6 @@ the lower-level :class:`panda3d.core.Character` implementation.
 It loads and controls an animated character and manages the animations
 playing on it.
 
-See the :ref:`models-and-actors` page in the Programming Guide to learn
-more about loading models and animated actors.
+See the :ref:`loading-actors-and-animations` page in the Programming Guide
+to learn more about loading animated models.
 """

+ 4 - 3
direct/src/cluster/ClusterClient.py

@@ -122,9 +122,10 @@ class ClusterClient(DirectObject.DirectObject):
         taskMgr.add(self.synchronizeTimeTask, "synchronizeTimeTask", -40)
 
     def synchronizeTimeTask(self, task):
-        frameCount = globalClock.getFrameCount()
-        frameTime = globalClock.getFrameTime()
-        dt = globalClock.getDt()
+        clock = ClockObject.getGlobalClock()
+        frameCount = clock.getFrameCount()
+        frameTime = clock.getFrameTime()
+        dt = clock.dt
         for server in self.serverList:
             server.sendTimeData(frameCount, frameTime, dt)
         return Task.cont

+ 5 - 4
direct/src/cluster/ClusterServer.py

@@ -52,7 +52,7 @@ class ClusterServer(DirectObject.DirectObject):
             self.startSwapCoordinator()
             base.graphicsEngine.setAutoFlip(0)
         # Set global clock mode to slave mode
-        globalClock.setMode(ClockObject.MSlave)
+        ClockObject.getGlobalClock().setMode(ClockObject.MSlave)
         # Send verification of startup to client
         self.daemon = DirectD()
 
@@ -335,9 +335,10 @@ class ClusterServer(DirectObject.DirectObject):
         """ Update cameraJig position to reflect latest position """
         (frameCount, frameTime, dt) = self.msgHandler.parseTimeDataDatagram(dgi)
         # Use frame time from client for both real and frame time
-        globalClock.setFrameCount(frameCount)
-        globalClock.setFrameTime(frameTime)
-        globalClock.setDt(dt)
+        clock = ClockObject.getGlobalClock()
+        clock.setFrameCount(frameCount)
+        clock.setFrameTime(frameTime)
+        clock.dt = dt
 
     def handleCommandString(self, dgi):
         """ Handle arbitrary command string from client """

+ 1 - 1
direct/src/directbase/TestStart.py

@@ -13,7 +13,7 @@ base.camera.setPosHpr(0, -10.0, 0, 0, 0, 0)
 base.camLens.setFov(52.0)
 base.camLens.setNearFar(1.0, 10000.0)
 
-globalClock.setMaxDt(0.2)
+base.clock.setMaxDt(0.2)
 base.enableParticles()
 
 # Force the screen to update:

+ 1 - 1
direct/src/directbase/ThreeUpStart.py

@@ -15,7 +15,7 @@ base.camera.setPosHpr(0, -10.0, 0, 0, 0, 0)
 base.camLens.setFov(52.0)
 base.camLens.setNearFar(1.0, 10000.0)
 
-globalClock.setMaxDt(0.2)
+base.clock.setMaxDt(0.2)
 base.enableParticles()
 base.addAngularIntegrator()
 

+ 5 - 3
direct/src/directdevices/DirectJoybox.py

@@ -5,6 +5,8 @@ from direct.directtools.DirectUtil import *
 from direct.gui import OnscreenText
 from direct.task import Task
 from direct.task.TaskManagerGlobal import taskMgr
+from panda3d.core import ClockObject
+
 import math
 
 #TODO: Handle interaction between widget, followSelectedTask and updateTask
@@ -58,7 +60,7 @@ class DirectJoybox(DirectObject):
                         R_TWIST, L_TWIST, NULL_AXIS]
         self.modifier = [1, 1, 1, -1, -1, 0]
         # Initialize time
-        self.lastTime = globalClock.getFrameTime()
+        self.lastTime = ClockObject.getGlobalClock().getFrameTime()
         # Record node path
         self.nodePath = nodePath
         self.headingNP = headingNP
@@ -148,7 +150,7 @@ class DirectJoybox(DirectObject):
 
     def updateVals(self):
         # Update delta time
-        cTime = globalClock.getFrameTime()
+        cTime = ClockObject.getGlobalClock().getFrameTime()
         self.deltaTime = cTime - self.lastTime
         self.lastTime = cTime
         # Update analogs
@@ -164,7 +166,7 @@ class DirectJoybox(DirectObject):
 
     def updateValsUnrolled(self):
         # Update delta time
-        cTime = globalClock.getFrameTime()
+        cTime = ClockObject.getGlobalClock().getFrameTime()
         self.deltaTime = cTime - self.lastTime
         self.lastTime = cTime
         # Update analogs

+ 8 - 8
direct/src/directtools/DirectCameraControl.py

@@ -129,8 +129,8 @@ class DirectCameraControl(DirectObject):
             # Hide the marker for this kind of motion
             self.coaMarker.hide()
             # Record time of start of mouse interaction
-            self.startT= globalClock.getFrameTime()
-            self.startF = globalClock.getFrameCount()
+            self.startT = base.clock.getFrameTime()
+            self.startF = base.clock.getFrameCount()
             # If the cam is orthogonal, spawn differentTask
             if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView and\
                base.direct.camera.getName() != 'persp':
@@ -169,8 +169,8 @@ class DirectCameraControl(DirectObject):
             # Hide the marker for this kind of motion
             self.coaMarker.hide()
             # Record time of start of mouse interaction
-            self.startT= globalClock.getFrameTime()
-            self.startF = globalClock.getFrameCount()
+            self.startT = base.clock.getFrameTime()
+            self.startF = base.clock.getFrameCount()
             # Start manipulation
             # If the cam is orthogonal, spawn differentTask
             if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView and\
@@ -186,8 +186,8 @@ class DirectCameraControl(DirectObject):
                 # Hide the marker for this kind of motion
                 self.coaMarker.hide()
                 # Record time of start of mouse interaction
-                self.startT= globalClock.getFrameTime()
-                self.startF = globalClock.getFrameCount()
+                self.startT = base.clock.getFrameTime()
+                self.startF = base.clock.getFrameCount()
                 # Start manipulation
                 self.spawnXZTranslateOrHPanYZoom()
                 # END MOUSE IN CENTRAL REGION
@@ -204,9 +204,9 @@ class DirectCameraControl(DirectObject):
 
     def mouseFlyStop(self):
         self.__stopManipulateCamera()
-        stopT = globalClock.getFrameTime()
+        stopT = base.clock.getFrameTime()
         deltaT = stopT - self.startT
-        stopF = globalClock.getFrameCount()
+        stopF = base.clock.getFrameCount()
         deltaF = stopF - self.startF
         ## No reason this shouldn't work with Maya cam on
         # if not self.useMayaCamControls and (deltaT <= 0.25) or (deltaF <= 1):

+ 1 - 1
direct/src/directtools/DirectUtil.py

@@ -36,7 +36,7 @@ def lerpBackgroundColor(r, g, b, duration):
     Function to lerp background color to a new value
     """
     def lerpColor(state):
-        dt = globalClock.getDt()
+        dt = base.clock.getDt()
         state.time += dt
         sf = state.time / state.duration
         if sf >= 1.0:

+ 4 - 3
direct/src/directutil/Mopath.py

@@ -2,7 +2,8 @@ from direct.showbase.DirectObject import DirectObject
 from direct.showbase.MessengerGlobal import messenger
 from direct.directtools.DirectGeometry import *
 
-from panda3d.core import NodePath, LineSegs
+from panda3d.core import NodePath, LineSegs, ClockObject
+
 
 class Mopath(DirectObject):
 
@@ -152,13 +153,13 @@ class Mopath(DirectObject):
         self.stop()
         t = taskMgr.add(self.__playTask, self.name + '-play')
         t.currentTime = time
-        t.lastTime = globalClock.getFrameTime()
+        t.lastTime = ClockObject.getGlobalClock().getFrameTime()
 
     def stop(self):
         taskMgr.remove(self.name + '-play')
 
     def __playTask(self, task):
-        time = globalClock.getFrameTime()
+        time = ClockObject.getGlobalClock().getFrameTime()
         dTime = time - task.lastTime
         task.lastTime = time
         if self.loop:

+ 6 - 6
direct/src/dist/FreezeTool.py

@@ -1409,7 +1409,7 @@ class Freezer:
                 else:
                     filename += '.pyo'
                 if multifile.findSubfile(filename) < 0:
-                    code = compile('', moduleName, 'exec')
+                    code = compile('', moduleName, 'exec', optimize=2)
                     self.__addPyc(multifile, filename, code, compressionLevel)
 
             moduleDirs[str] = True
@@ -1489,7 +1489,7 @@ class Freezer:
                 source = open(sourceFilename.toOsSpecific(), 'r').read()
                 if source and source[-1] != '\n':
                     source = source + '\n'
-                code = compile(source, str(sourceFilename), 'exec')
+                code = compile(source, str(sourceFilename), 'exec', optimize=2)
 
         self.__addPyc(multifile, filename, code, compressionLevel)
 
@@ -1568,7 +1568,7 @@ class Freezer:
             # trouble importing it as a builtin module.  Synthesize a frozen
             # module that loads it as builtin.
             if '.' in moduleName and self.linkExtensionModules:
-                code = compile('import sys;del sys.modules["%s"];import imp;imp.init_builtin("%s")' % (moduleName, moduleName), moduleName, 'exec')
+                code = compile('import sys;del sys.modules["%s"];import imp;imp.init_builtin("%s")' % (moduleName, moduleName), moduleName, 'exec', optimize=2)
                 code = marshal.dumps(code)
                 mangledName = self.mangleName(moduleName)
                 moduleDefs.append(self.makeModuleDef(mangledName, code))
@@ -1835,7 +1835,7 @@ class Freezer:
             # If it is a submodule of a frozen module, Python will have
             # trouble importing it as a builtin module.  Synthesize a frozen
             # module that loads it dynamically.
-            if '.' in moduleName:
+            if '.' in moduleName and not self.platform.startswith('android'):
                 if self.platform.startswith("macosx") and not use_console:
                     # We write the Frameworks directory to sys.path[0].
                     code = 'import sys;del sys.modules["%s"];import sys,os,imp;imp.load_dynamic("%s",os.path.join(sys.path[0], "%s%s"))' % (moduleName, moduleName, moduleName, modext)
@@ -2472,7 +2472,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
         if type is _PKG_NAMESPACE_DIRECTORY:
             m = self.add_module(fqname)
-            m.__code__ = compile('', '', 'exec')
+            m.__code__ = compile('', '', 'exec', optimize=2)
             m.__path__ = pathname
             return m
 
@@ -2484,7 +2484,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
                 code = fp.read()
 
             code += b'\n' if isinstance(code, bytes) else '\n'
-            co = compile(code, pathname, 'exec')
+            co = compile(code, pathname, 'exec', optimize=2)
         elif type == imp.PY_COMPILED:
             if sys.version_info >= (3, 7):
                 try:

+ 275 - 0
direct/src/dist/_android.py

@@ -0,0 +1,275 @@
+"""Internal support module for Android builds."""
+
+import xml.etree.ElementTree as ET
+
+from ._proto.targeting_pb2 import Abi
+from ._proto.config_pb2 import BundleConfig
+from ._proto.files_pb2 import NativeLibraries
+from ._proto.Resources_pb2 import XmlNode, ResourceTable
+
+
+AbiAlias = Abi.AbiAlias
+
+
+def str_resource(id):
+    def compile(attrib, manifest):
+        attrib.resource_id = id
+    return compile
+
+
+def int_resource(id):
+    def compile(attrib, manifest):
+        attrib.resource_id = id
+        if attrib.value.startswith('0x') or attrib.value.startswith('0X'):
+            attrib.compiled_item.prim.int_hexadecimal_value = int(attrib.value, 16)
+        else:
+            attrib.compiled_item.prim.int_decimal_value = int(attrib.value)
+    return compile
+
+
+def bool_resource(id):
+    def compile(attrib, manifest):
+        attrib.resource_id = id
+        attrib.compiled_item.prim.boolean_value = {
+            'true': True, '1': True, 'false': False, '0': False
+        }[attrib.value]
+    return compile
+
+
+def enum_resource(id, *values):
+    def compile(attrib, manifest):
+        attrib.resource_id = id
+        attrib.compiled_item.prim.int_decimal_value = values.index(attrib.value)
+    return compile
+
+
+def flag_resource(id, **values):
+    def compile(attrib, manifest):
+        attrib.resource_id = id
+        bitmask = 0
+        flags = attrib.value.split('|')
+        for flag in flags:
+            bitmask = values[flag]
+        attrib.compiled_item.prim.int_hexadecimal_value = bitmask
+    return compile
+
+
+def ref_resource(id):
+    def compile(attrib, manifest):
+        assert attrib.value[0] == '@'
+        ref_type, ref_name = attrib.value[1:].split('/')
+        attrib.resource_id = id
+        attrib.compiled_item.ref.name = ref_type + '/' + ref_name
+
+        if ref_type == 'android:style':
+            attrib.compiled_item.ref.id = ANDROID_STYLES[ref_name]
+        elif ':' not in ref_type:
+            attrib.compiled_item.ref.id = manifest.register_resource(ref_type, ref_name)
+        else:
+            print(f'Warning: unhandled AndroidManifest.xml reference "{attrib.value}"')
+    return compile
+
+
+# See data/res/values/public.xml
+ANDROID_STYLES = {
+    'Animation': 0x01030000,
+    'Animation.Activity': 0x01030001,
+    'Animation.Dialog': 0x01030002,
+    'Animation.Translucent': 0x01030003,
+    'Animation.Toast': 0x01030004,
+    'Theme': 0x01030005,
+    'Theme.NoTitleBar': 0x01030006,
+    'Theme.NoTitleBar.Fullscreen': 0x01030007,
+    'Theme.Black': 0x01030008,
+    'Theme.Black.NoTitleBar': 0x01030009,
+    'Theme.Black.NoTitleBar.Fullscreen': 0x0103000a,
+    'Theme.Dialog': 0x0103000b,
+    'Theme.Light': 0x0103000c,
+    'Theme.Light.NoTitleBar': 0x0103000d,
+    'Theme.Light.NoTitleBar.Fullscreen': 0x0103000e,
+    'Theme.Translucent': 0x0103000f,
+    'Theme.Translucent.NoTitleBar': 0x01030010,
+    'Theme.Translucent.NoTitleBar.Fullscreen': 0x01030011,
+    'Widget': 0x01030012,
+    'Widget.AbsListView': 0x01030013,
+    'Widget.Button': 0x01030014,
+    'Widget.Button.Inset': 0x01030015,
+    'Widget.Button.Small': 0x01030016,
+    'Widget.Button.Toggle': 0x01030017,
+    'Widget.CompoundButton': 0x01030018,
+    'Widget.CompoundButton.CheckBox': 0x01030019,
+    'Widget.CompoundButton.RadioButton': 0x0103001a,
+    'Widget.CompoundButton.Star': 0x0103001b,
+    'Widget.ProgressBar': 0x0103001c,
+    'Widget.ProgressBar.Large': 0x0103001d,
+    'Widget.ProgressBar.Small': 0x0103001e,
+    'Widget.ProgressBar.Horizontal': 0x0103001f,
+    'Widget.SeekBar': 0x01030020,
+    'Widget.RatingBar': 0x01030021,
+    'Widget.TextView': 0x01030022,
+    'Widget.EditText': 0x01030023,
+    'Widget.ExpandableListView': 0x01030024,
+    'Widget.ImageWell': 0x01030025,
+    'Widget.ImageButton': 0x01030026,
+    'Widget.AutoCompleteTextView': 0x01030027,
+    'Widget.Spinner': 0x01030028,
+    'Widget.TextView.PopupMenu': 0x01030029,
+    'Widget.TextView.SpinnerItem': 0x0103002a,
+    'Widget.DropDownItem': 0x0103002b,
+    'Widget.DropDownItem.Spinner': 0x0103002c,
+    'Widget.ScrollView': 0x0103002d,
+    'Widget.ListView': 0x0103002e,
+    'Widget.ListView.White': 0x0103002f,
+    'Widget.ListView.DropDown': 0x01030030,
+    'Widget.ListView.Menu': 0x01030031,
+    'Widget.GridView': 0x01030032,
+    'Widget.WebView': 0x01030033,
+    'Widget.TabWidget': 0x01030034,
+    'Widget.Gallery': 0x01030035,
+    'Widget.PopupWindow': 0x01030036,
+    'MediaButton': 0x01030037,
+    'MediaButton.Previous': 0x01030038,
+    'MediaButton.Next': 0x01030039,
+    'MediaButton.Play': 0x0103003a,
+    'MediaButton.Ffwd': 0x0103003b,
+    'MediaButton.Rew': 0x0103003c,
+    'MediaButton.Pause': 0x0103003d,
+    'TextAppearance': 0x0103003e,
+    'TextAppearance.Inverse': 0x0103003f,
+    'TextAppearance.Theme': 0x01030040,
+    'TextAppearance.DialogWindowTitle': 0x01030041,
+    'TextAppearance.Large': 0x01030042,
+    'TextAppearance.Large.Inverse': 0x01030043,
+    'TextAppearance.Medium': 0x01030044,
+    'TextAppearance.Medium.Inverse': 0x01030045,
+    'TextAppearance.Small': 0x01030046,
+    'TextAppearance.Small.Inverse': 0x01030047,
+    'TextAppearance.Theme.Dialog': 0x01030048,
+    'TextAppearance.Widget': 0x01030049,
+    'TextAppearance.Widget.Button': 0x0103004a,
+    'TextAppearance.Widget.IconMenu.Item': 0x0103004b,
+    'TextAppearance.Widget.EditText': 0x0103004c,
+    'TextAppearance.Widget.TabWidget': 0x0103004d,
+    'TextAppearance.Widget.TextView': 0x0103004e,
+    'TextAppearance.Widget.TextView.PopupMenu': 0x0103004f,
+    'TextAppearance.Widget.DropDownHint': 0x01030050,
+    'TextAppearance.Widget.DropDownItem': 0x01030051,
+    'TextAppearance.Widget.TextView.SpinnerItem': 0x01030052,
+    'TextAppearance.WindowTitle': 0x01030053,
+}
+
+
+# See data/res/values/public.xml, attrs.xml and especially attrs_manifest.xml
+ANDROID_ATTRIBUTES = {
+    'allowBackup': bool_resource(0x1010280),
+    'allowClearUserData': bool_resource(0x1010005),
+    'allowParallelSyncs': bool_resource(0x1010332),
+    'allowSingleTap': bool_resource(0x1010259),
+    'allowTaskReparenting': bool_resource(0x1010204),
+    'alwaysRetainTaskState': bool_resource(0x1010203),
+    'clearTaskOnLaunch': bool_resource(0x1010015),
+    'debuggable': bool_resource(0x0101000f),
+    'configChanges': flag_resource(0x0101001f, mcc=0x0001, mnc=0x0002, locale=0x0004, touchscreen=0x0008, keyboard=0x0010, keyboardHidden=0x0020, navigation=0x0040, orientation=0x0080, screenLayout=0x0100, uiMode=0x0200, screenSize=0x0400, smallestScreenSize=0x0800, layoutDirection=0x2000, fontScale=0x40000000),
+    'enabled': bool_resource(0x101000e),
+    'excludeFromRecents': bool_resource(0x1010017),
+    'extractNativeLibs': bool_resource(0x10104ea),
+    'finishOnTaskLaunch': bool_resource(0x1010014),
+    'fullBackupContent': bool_resource(0x10104eb),
+    'glEsVersion': int_resource(0x1010281),
+    'hasCode': bool_resource(0x101000c),
+    'host': str_resource(0x1010028),
+    'icon': ref_resource(0x1010002),
+    'immersive': bool_resource(0x10102c0),
+    'installLocation': enum_resource(0x10102b7, "auto", "internalOnly", "preferExternal"),
+    'isGame': bool_resource(0x010103f4),
+    'label': str_resource(0x01010001),
+    'launchMode': enum_resource(0x101001d, "standard", "singleTop", "singleTask", "singleInstance"),
+    'maxSdkVersion': int_resource(0x1010271),
+    'mimeType': str_resource(0x1010026),
+    'minSdkVersion': int_resource(0x101020c),
+    'multiprocess': bool_resource(0x1010013),
+    'name': str_resource(0x1010003),
+    'pathPattern': str_resource(0x101002c),
+    'required': bool_resource(0x101028e),
+    'scheme': str_resource(0x1010027),
+    'stateNotNeeded': bool_resource(0x1010016),
+    'supportsRtl': bool_resource(0x010103af),
+    'supportsUploading': bool_resource(0x101029b),
+    'targetSandboxVersion': int_resource(0x101054c),
+    'targetSdkVersion': int_resource(0x1010270),
+    'theme': ref_resource(0x01010000),
+    'value': str_resource(0x1010024),
+    'versionCode': int_resource(0x101021b),
+    'versionName': str_resource(0x101021c),
+}
+
+
+class AndroidManifest:
+    def __init__(self):
+        super().__init__()
+        self._stack = []
+        self.root = XmlNode()
+        self.resource_types = []
+        self.resources = {}
+
+    def parse_xml(self, data):
+        parser = ET.XMLParser(target=self)
+        parser.feed(data)
+        parser.close()
+
+    def start_ns(self, prefix, uri):
+        decl = self.root.element.namespace_declaration.add()
+        decl.prefix = prefix
+        decl.uri = uri
+
+    def start(self, tag, attribs):
+        if not self._stack:
+            node = self.root
+        else:
+            node = self._stack[-1].child.add()
+
+        element = node.element
+        element.name = tag
+
+        self._stack.append(element)
+
+        for key, value in attribs.items():
+            attrib = element.attribute.add()
+            attrib.value = value
+
+            if key.startswith('{'):
+                attrib.namespace_uri, key = key[1:].split('}', 1)
+                res_compile = ANDROID_ATTRIBUTES.get(key, None)
+                if not res_compile:
+                    print(f'Warning: unhandled AndroidManifest.xml attribute "{key}"')
+            else:
+                res_compile = None
+
+            attrib.name = key
+
+            if res_compile:
+                res_compile(attrib, self)
+
+    def end(self, tag):
+        self._stack.pop()
+
+    def register_resource(self, type, name):
+        if type not in self.resource_types:
+            self.resource_types.append(type)
+            type_id = len(self.resource_types)
+            self.resources[type] = []
+        else:
+            type_id = self.resource_types.index(type) + 1
+
+        resources = self.resources[type]
+        if name in resources:
+            entry_id = resources.index(name)
+        else:
+            entry_id = len(resources)
+            resources.append(name)
+
+        id = (0x7f << 24) | (type_id << 16) | (entry_id)
+        return id
+
+    def dumps(self):
+        return self.root.SerializeToString()

File diff suppressed because it is too large
+ 21 - 0
direct/src/dist/_proto/Configuration_pb2.py


+ 4 - 0
direct/src/dist/_proto/README

@@ -0,0 +1,4 @@
+The files in this directory were generated from the .proto files in the
+bundletool and aapt2 repositories.
+
+They are used by installer.py when generating an Android App Bundle.

File diff suppressed because it is too large
+ 22 - 0
direct/src/dist/_proto/Resources_pb2.py


+ 0 - 0
direct/src/dist/_proto/__init__.py


File diff suppressed because it is too large
+ 21 - 0
direct/src/dist/_proto/config_pb2.py


+ 307 - 0
direct/src/dist/_proto/files_pb2.py

@@ -0,0 +1,307 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: files.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from . import targeting_pb2 as targeting__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='files.proto',
+  package='android.bundle',
+  syntax='proto3',
+  serialized_options=b'\n\022com.android.bundle',
+  create_key=_descriptor._internal_create_key,
+  serialized_pb=b'\n\x0b\x66iles.proto\x12\x0e\x61ndroid.bundle\x1a\x0ftargeting.proto\"D\n\x06\x41ssets\x12:\n\tdirectory\x18\x01 \x03(\x0b\x32\'.android.bundle.TargetedAssetsDirectory\"M\n\x0fNativeLibraries\x12:\n\tdirectory\x18\x01 \x03(\x0b\x32\'.android.bundle.TargetedNativeDirectory\"D\n\nApexImages\x12\x30\n\x05image\x18\x01 \x03(\x0b\x32!.android.bundle.TargetedApexImageJ\x04\x08\x02\x10\x03\"d\n\x17TargetedAssetsDirectory\x12\x0c\n\x04path\x18\x01 \x01(\t\x12;\n\ttargeting\x18\x02 \x01(\x0b\x32(.android.bundle.AssetsDirectoryTargeting\"d\n\x17TargetedNativeDirectory\x12\x0c\n\x04path\x18\x01 \x01(\t\x12;\n\ttargeting\x18\x02 \x01(\x0b\x32(.android.bundle.NativeDirectoryTargeting\"q\n\x11TargetedApexImage\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x17\n\x0f\x62uild_info_path\x18\x03 \x01(\t\x12\x35\n\ttargeting\x18\x02 \x01(\x0b\x32\".android.bundle.ApexImageTargetingB\x14\n\x12\x63om.android.bundleb\x06proto3'
+  ,
+  dependencies=[targeting__pb2.DESCRIPTOR,])
+
+
+
+
+_ASSETS = _descriptor.Descriptor(
+  name='Assets',
+  full_name='android.bundle.Assets',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='directory', full_name='android.bundle.Assets.directory', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=48,
+  serialized_end=116,
+)
+
+
+_NATIVELIBRARIES = _descriptor.Descriptor(
+  name='NativeLibraries',
+  full_name='android.bundle.NativeLibraries',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='directory', full_name='android.bundle.NativeLibraries.directory', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=118,
+  serialized_end=195,
+)
+
+
+_APEXIMAGES = _descriptor.Descriptor(
+  name='ApexImages',
+  full_name='android.bundle.ApexImages',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='image', full_name='android.bundle.ApexImages.image', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=197,
+  serialized_end=265,
+)
+
+
+_TARGETEDASSETSDIRECTORY = _descriptor.Descriptor(
+  name='TargetedAssetsDirectory',
+  full_name='android.bundle.TargetedAssetsDirectory',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='path', full_name='android.bundle.TargetedAssetsDirectory.path', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='targeting', full_name='android.bundle.TargetedAssetsDirectory.targeting', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=267,
+  serialized_end=367,
+)
+
+
+_TARGETEDNATIVEDIRECTORY = _descriptor.Descriptor(
+  name='TargetedNativeDirectory',
+  full_name='android.bundle.TargetedNativeDirectory',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='path', full_name='android.bundle.TargetedNativeDirectory.path', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='targeting', full_name='android.bundle.TargetedNativeDirectory.targeting', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=369,
+  serialized_end=469,
+)
+
+
+_TARGETEDAPEXIMAGE = _descriptor.Descriptor(
+  name='TargetedApexImage',
+  full_name='android.bundle.TargetedApexImage',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='path', full_name='android.bundle.TargetedApexImage.path', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='build_info_path', full_name='android.bundle.TargetedApexImage.build_info_path', index=1,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='targeting', full_name='android.bundle.TargetedApexImage.targeting', index=2,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=471,
+  serialized_end=584,
+)
+
+_ASSETS.fields_by_name['directory'].message_type = _TARGETEDASSETSDIRECTORY
+_NATIVELIBRARIES.fields_by_name['directory'].message_type = _TARGETEDNATIVEDIRECTORY
+_APEXIMAGES.fields_by_name['image'].message_type = _TARGETEDAPEXIMAGE
+_TARGETEDASSETSDIRECTORY.fields_by_name['targeting'].message_type = targeting__pb2._ASSETSDIRECTORYTARGETING
+_TARGETEDNATIVEDIRECTORY.fields_by_name['targeting'].message_type = targeting__pb2._NATIVEDIRECTORYTARGETING
+_TARGETEDAPEXIMAGE.fields_by_name['targeting'].message_type = targeting__pb2._APEXIMAGETARGETING
+DESCRIPTOR.message_types_by_name['Assets'] = _ASSETS
+DESCRIPTOR.message_types_by_name['NativeLibraries'] = _NATIVELIBRARIES
+DESCRIPTOR.message_types_by_name['ApexImages'] = _APEXIMAGES
+DESCRIPTOR.message_types_by_name['TargetedAssetsDirectory'] = _TARGETEDASSETSDIRECTORY
+DESCRIPTOR.message_types_by_name['TargetedNativeDirectory'] = _TARGETEDNATIVEDIRECTORY
+DESCRIPTOR.message_types_by_name['TargetedApexImage'] = _TARGETEDAPEXIMAGE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+Assets = _reflection.GeneratedProtocolMessageType('Assets', (_message.Message,), {
+  'DESCRIPTOR' : _ASSETS,
+  '__module__' : 'files_pb2'
+  # @@protoc_insertion_point(class_scope:android.bundle.Assets)
+  })
+_sym_db.RegisterMessage(Assets)
+
+NativeLibraries = _reflection.GeneratedProtocolMessageType('NativeLibraries', (_message.Message,), {
+  'DESCRIPTOR' : _NATIVELIBRARIES,
+  '__module__' : 'files_pb2'
+  # @@protoc_insertion_point(class_scope:android.bundle.NativeLibraries)
+  })
+_sym_db.RegisterMessage(NativeLibraries)
+
+ApexImages = _reflection.GeneratedProtocolMessageType('ApexImages', (_message.Message,), {
+  'DESCRIPTOR' : _APEXIMAGES,
+  '__module__' : 'files_pb2'
+  # @@protoc_insertion_point(class_scope:android.bundle.ApexImages)
+  })
+_sym_db.RegisterMessage(ApexImages)
+
+TargetedAssetsDirectory = _reflection.GeneratedProtocolMessageType('TargetedAssetsDirectory', (_message.Message,), {
+  'DESCRIPTOR' : _TARGETEDASSETSDIRECTORY,
+  '__module__' : 'files_pb2'
+  # @@protoc_insertion_point(class_scope:android.bundle.TargetedAssetsDirectory)
+  })
+_sym_db.RegisterMessage(TargetedAssetsDirectory)
+
+TargetedNativeDirectory = _reflection.GeneratedProtocolMessageType('TargetedNativeDirectory', (_message.Message,), {
+  'DESCRIPTOR' : _TARGETEDNATIVEDIRECTORY,
+  '__module__' : 'files_pb2'
+  # @@protoc_insertion_point(class_scope:android.bundle.TargetedNativeDirectory)
+  })
+_sym_db.RegisterMessage(TargetedNativeDirectory)
+
+TargetedApexImage = _reflection.GeneratedProtocolMessageType('TargetedApexImage', (_message.Message,), {
+  'DESCRIPTOR' : _TARGETEDAPEXIMAGE,
+  '__module__' : 'files_pb2'
+  # @@protoc_insertion_point(class_scope:android.bundle.TargetedApexImage)
+  })
+_sym_db.RegisterMessage(TargetedApexImage)
+
+
+DESCRIPTOR._options = None
+# @@protoc_insertion_point(module_scope)

File diff suppressed because it is too large
+ 22 - 0
direct/src/dist/_proto/targeting_pb2.py


+ 312 - 38
direct/src/dist/commands.py

@@ -157,6 +157,95 @@ if os.path.isdir(tcl_dir):
 del os
 """
 
+SITE_PY_ANDROID = """
+import sys, os
+from _frozen_importlib import _imp, FrozenImporter
+from importlib import _bootstrap_external
+from importlib.abc import Loader, MetaPathFinder
+from importlib.machinery import ModuleSpec
+from io import RawIOBase, TextIOWrapper
+
+from android_log import write as android_log_write
+
+
+sys.frozen = True
+sys.platform = "android"
+
+
+# Replace stdout/stderr with something that writes to the Android log.
+class AndroidLogStream:
+    closed = False
+    encoding = 'utf-8'
+
+    def __init__(self, prio, tag):
+        self.prio = prio
+        self.tag = tag
+        self.buffer = ''
+
+    def isatty(self):
+        return False
+
+    def write(self, text):
+        self.writelines(text.split('\\n'))
+
+    def writelines(self, lines):
+        num_lines = len(lines)
+        if num_lines == 1:
+            self.buffer += lines[0]
+        elif num_lines > 1:
+            android_log_write(self.prio, self.tag, self.buffer + lines[0])
+            for line in lines[1:-1]:
+                android_log_write(self.prio, self.tag, line)
+            self.buffer = lines[-1]
+
+    def flush(self):
+        pass
+
+    def seekable(self):
+        return False
+
+    def readable(self):
+        return False
+
+    def writable(self):
+        return True
+
+sys.stdout = AndroidLogStream(2, 'Python')
+sys.stderr = AndroidLogStream(3, 'Python')
+
+
+# Alter FrozenImporter to give a __file__ property to frozen modules.
+_find_spec = FrozenImporter.find_spec
+
+def find_spec(fullname, path=None, target=None):
+    spec = _find_spec(fullname, path=path, target=target)
+    if spec:
+        spec.has_location = True
+        spec.origin = sys.executable
+    return spec
+
+def get_data(path):
+    with open(path, 'rb') as fp:
+        return fp.read()
+
+FrozenImporter.find_spec = find_spec
+FrozenImporter.get_data = get_data
+
+
+class AndroidExtensionFinder(MetaPathFinder):
+    @classmethod
+    def find_spec(cls, fullname, path=None, target=None):
+        soname = 'libpy.' + fullname + '.so'
+        path = os.path.join(os.path.dirname(sys.executable), soname)
+
+        if os.path.exists(path):
+            loader = _bootstrap_external.ExtensionFileLoader(fullname, path)
+            return ModuleSpec(fullname, loader, origin=path)
+
+
+sys.meta_path.append(AndroidExtensionFinder)
+"""
+
 
 class build_apps(setuptools.Command):
     description = 'build Panda3D applications'
@@ -171,6 +260,13 @@ class build_apps(setuptools.Command):
 
     def initialize_options(self):
         self.build_base = os.path.join(os.getcwd(), 'build')
+        self.application_id = None
+        self.android_abis = None
+        self.android_debuggable = False
+        self.android_version_code = 1
+        self.android_min_sdk_version = 21
+        self.android_max_sdk_version = None
+        self.android_target_sdk_version = 30
         self.gui_apps = {}
         self.console_apps = {}
         self.macos_main_app = None
@@ -266,6 +362,11 @@ class build_apps(setuptools.Command):
             '/usr/lib/libxar.1.dylib',
             '/usr/lib/libmenu.5.4.dylib',
             '/System/Library/**',
+
+            # Android
+            'libc.so', 'libm.so', 'liblog.so', 'libdl.so', 'libandroid.so',
+            'libGLESv1_CM.so', 'libGLESv2.so', 'libjnigraphics.so', 'libEGL.so',
+            'libOpenSLES.so', 'libandroid.so', 'libOpenMAXAL.so', 'libz.so',
         ]
 
         self.package_data_dirs = {}
@@ -363,6 +464,21 @@ class build_apps(setuptools.Command):
         tmp.update(self.package_data_dirs)
         self.package_data_dirs = tmp
 
+        # Default to all supported ABIs (for the given Android version).
+        if self.android_max_sdk_version and self.android_max_sdk_version < 21:
+            assert self.android_max_sdk_version >= 19, \
+                'Panda3D requires at least Android API level 19!'
+
+            if self.android_abis:
+                for abi in self.android_abis:
+                    assert abi not in ('mips64', 'x86_64', 'arm64-v8a'), \
+                        f'{abi} was not a valid Android ABI before Android 21!'
+            else:
+                self.android_abis = ['armeabi-v7a', 'x86']
+
+        elif not self.android_abis:
+            self.android_abis = ['arm64-v8a', 'armeabi-v7a', 'x86_64', 'x86']
+
         self.icon_objects = {}
         for app, iconpaths in self.icons.items():
             if not isinstance(iconpaths, list) and not isinstance(iconpaths, tuple):
@@ -379,7 +495,68 @@ class build_apps(setuptools.Command):
         self.announce('Building platforms: {0}'.format(','.join(self.platforms)), distutils.log.INFO)
 
         for platform in self.platforms:
-            self.build_runtimes(platform, True)
+            # Create the build directory, or ensure it is empty.
+            build_dir = os.path.join(self.build_base, platform)
+
+            if os.path.exists(build_dir):
+                for entry in os.listdir(build_dir):
+                    path = os.path.join(build_dir, entry)
+                    if os.path.islink(path) or os.path.isfile(path):
+                        os.unlink(path)
+                    else:
+                        shutil.rmtree(path)
+            else:
+                os.makedirs(build_dir)
+
+            if platform == 'android':
+                # Make a multi-arch build for Android.
+                data_dir = os.path.join(build_dir, 'assets')
+                os.makedirs(data_dir, exist_ok=True)
+
+                for abi in self.android_abis:
+                    lib_dir = os.path.join(build_dir, 'lib', abi)
+                    os.makedirs(lib_dir, exist_ok=True)
+
+                    suffix = None
+                    if abi == 'arm64-v8a':
+                        suffix = '_arm64'
+                    elif abi == 'armeabi-v7a':
+                        suffix = '_armv7a'
+                    elif abi == 'armeabi':
+                        suffix = '_arm'
+                    else: # e.g. x86, x86_64, mips, mips64
+                        suffix = '_' + abi.replace('-', '_')
+
+                    self.build_binaries(lib_dir, platform + suffix)
+
+                # Write out the icons to the res directory.
+                for appname, icon in self.icon_objects.items():
+                    if appname == '*' or (appname == self.macos_main_app and '*' not in self.icon_objects):
+                        # Conventional name for icon on Android.
+                        basename = 'ic_launcher.png'
+                    else:
+                        basename = f'ic_{appname}.png'
+
+                    res_dir = os.path.join(build_dir, 'res')
+                    icon.writeSize(48, os.path.join(res_dir, 'mipmap-mdpi-v4', basename))
+                    icon.writeSize(72, os.path.join(res_dir, 'mipmap-hdpi-v4', basename))
+                    icon.writeSize(96, os.path.join(res_dir, 'mipmap-xhdpi-v4', basename))
+                    icon.writeSize(144, os.path.join(res_dir, 'mipmap-xxhdpi-v4', basename))
+
+                    if icon.getLargestSize() >= 192:
+                        icon.writeSize(192, os.path.join(res_dir, 'mipmap-xxxhdpi-v4', basename))
+
+                self.build_data(data_dir, platform)
+
+                # Generate an AndroidManifest.xml
+                self.generate_android_manifest(os.path.join(build_dir, 'AndroidManifest.xml'))
+            else:
+                self.build_binaries(build_dir, platform)
+                self.build_data(build_dir, platform)
+
+            # Bundle into an .app on macOS
+            if self.macos_main_app and 'macosx' in platform:
+                self.bundle_macos_app(build_dir)
 
     def download_wheels(self, platform):
         """ Downloads wheels for the given platform using pip. This includes panda3d
@@ -508,15 +685,84 @@ class build_apps(setuptools.Command):
         with open(os.path.join(contentsdir, 'Info.plist'), 'wb') as f:
             plistlib.dump(plist, f)
 
-    def build_runtimes(self, platform, use_wheels):
-        """ Builds the distributions for the given platform. """
-
-        builddir = os.path.join(self.build_base, platform)
-
-        if os.path.exists(builddir):
-            shutil.rmtree(builddir)
-        os.makedirs(builddir)
-
+    def generate_android_manifest(self, path):
+        import xml.etree.ElementTree as ET
+
+        name = self.distribution.get_name()
+        version = self.distribution.get_version()
+        classifiers = self.distribution.get_classifiers()
+
+        is_game = False
+        for classifier in classifiers:
+            if classifier == 'Topic :: Games/Entertainment' or classifier.startswith('Topic :: Games/Entertainment ::'):
+                is_game = True
+
+        manifest = ET.Element('manifest')
+        manifest.set('xmlns:android', 'http://schemas.android.com/apk/res/android')
+        manifest.set('package', self.application_id)
+        manifest.set('android:versionCode', str(int(self.android_version_code)))
+        manifest.set('android:versionName', version)
+        manifest.set('android:installLocation', 'auto')
+
+        uses_sdk = ET.SubElement(manifest, 'uses-sdk')
+        uses_sdk.set('android:minSdkVersion', str(int(self.android_min_sdk_version)))
+        uses_sdk.set('android:targetSdkVersion', str(int(self.android_target_sdk_version)))
+        if self.android_max_sdk_version:
+            uses_sdk.set('android:maxSdkVersion', str(int(self.android_max_sdk_version)))
+
+        if 'pandagles2' in self.plugins:
+            uses_feature = ET.SubElement(manifest, 'uses-feature')
+            uses_feature.set('android:glEsVersion', '0x00020000')
+            uses_feature.set('android:required', 'false' if 'pandagles' in self.plugins else 'true')
+
+        if 'p3openal_audio' in self.plugins:
+            uses_feature = ET.SubElement(manifest, 'uses-feature')
+            uses_feature.set('android:name', 'android.hardware.audio.output')
+            uses_feature.set('android:required', 'false')
+
+        uses_feature = ET.SubElement(manifest, 'uses-feature')
+        uses_feature.set('android:name', 'android.hardware.gamepad')
+        uses_feature.set('android:required', 'false')
+
+        application = ET.SubElement(manifest, 'application')
+        application.set('android:label', name)
+        application.set('android:isGame', ('false', 'true')[is_game])
+        application.set('android:debuggable', ('false', 'true')[self.android_debuggable])
+        application.set('android:extractNativeLibs', 'true')
+
+        app_icon = self.icon_objects.get('*', self.icon_objects.get(self.macos_main_app))
+        if app_icon:
+            application.set('android:icon', '@mipmap/ic_launcher')
+
+        for appname in self.gui_apps:
+            activity = ET.SubElement(application, 'activity')
+            activity.set('android:name', 'org.panda3d.android.PandaActivity')
+            activity.set('android:label', appname)
+            activity.set('android:theme', '@android:style/Theme.NoTitleBar')
+            activity.set('android:configChanges', 'orientation|keyboardHidden')
+            activity.set('android:launchMode', 'singleInstance')
+
+            act_icon = self.icon_objects.get(appname)
+            if act_icon and act_icon is not app_icon:
+                activity.set('android:icon', '@mipmap/ic_' + appname)
+
+            meta_data = ET.SubElement(activity, 'meta-data')
+            meta_data.set('android:name', 'android.app.lib_name')
+            meta_data.set('android:value', appname)
+
+            intent_filter = ET.SubElement(activity, 'intent-filter')
+            ET.SubElement(intent_filter, 'action').set('android:name', 'android.intent.action.MAIN')
+            ET.SubElement(intent_filter, 'category').set('android:name', 'android.intent.category.LAUNCHER')
+            ET.SubElement(intent_filter, 'category').set('android:name', 'android.intent.category.LEANBACK_LAUNCHER')
+
+        tree = ET.ElementTree(manifest)
+        with open(path, 'wb') as fh:
+            tree.write(fh, encoding='utf-8', xml_declaration=True)
+
+    def build_binaries(self, binary_dir, platform):
+        """ Builds the binary data for the given platform. """
+
+        use_wheels = True
         path = sys.path[:]
         p3dwhl = None
         wheelpaths = []
@@ -607,6 +853,9 @@ class build_apps(setuptools.Command):
                     value = value[:c].rstrip()
 
                 if var == 'model-cache-dir' and value:
+                    if platform.startswith('android'):
+                        # Ignore on Android, where the cache dir is fixed.
+                        continue
                     value = value.replace('/panda3d', '/{}'.format(self.distribution.get_name()))
 
                 if var == 'audio-library-name':
@@ -642,15 +891,14 @@ class build_apps(setuptools.Command):
         prcexport = '\n'.join(prcexport)
         if not self.embed_prc_data:
             prcdir = self.default_prc_dir.replace('<auto>', '')
-            prcdir = os.path.join(builddir, prcdir)
+            prcdir = os.path.join(binary_dir, prcdir)
             os.makedirs(prcdir)
-            with open (os.path.join(prcdir, '00-panda3d.prc'), 'w') as f:
+            with open(os.path.join(prcdir, '00-panda3d.prc'), 'w') as f:
                 f.write(prcexport)
 
         # Create runtimes
         freezer_extras = set()
         freezer_modules = set()
-        freezer_modpaths = set()
         ext_suffixes = set()
 
         def get_search_path_for(source_path):
@@ -683,33 +931,42 @@ class build_apps(setuptools.Command):
 
             return search_path
 
-        def create_runtime(appname, mainscript, use_console):
+        def create_runtime(platform, appname, mainscript, use_console):
             freezer = FreezeTool.Freezer(
                 platform=platform,
                 path=path,
                 hiddenImports=self.hidden_imports
             )
             freezer.addModule('__main__', filename=mainscript)
-            freezer.addModule('site', filename='site.py', text=SITE_PY)
+            if platform.startswith('android'):
+                freezer.addModule('site', filename='site.py', text=SITE_PY_ANDROID)
+            else:
+                freezer.addModule('site', filename='site.py', text=SITE_PY)
             for incmod in self.include_modules.get(appname, []) + self.include_modules.get('*', []):
                 freezer.addModule(incmod)
             for exmod in self.exclude_modules.get(appname, []) + self.exclude_modules.get('*', []):
                 freezer.excludeModule(exmod)
             freezer.done(addStartupModules=True)
 
-            target_path = os.path.join(builddir, appname)
-
             stub_name = 'deploy-stub'
+            target_name = appname
             if platform.startswith('win') or 'macosx' in platform:
                 if not use_console:
                     stub_name = 'deploy-stubw'
+            elif platform.startswith('android'):
+                if not use_console:
+                    stub_name = 'libdeploy-stubw.so'
+                    target_name = 'lib' + target_name + '.so'
 
             if platform.startswith('win'):
                 stub_name += '.exe'
-                target_path += '.exe'
+                target_name += '.exe'
 
             if use_wheels:
-                stub_file = p3dwhl.open('panda3d_tools/{0}'.format(stub_name))
+                if stub_name.endswith('.so'):
+                    stub_file = p3dwhl.open('deploy_libs/{0}'.format(stub_name))
+                else:
+                    stub_file = p3dwhl.open('panda3d_tools/{0}'.format(stub_name))
             else:
                 dtool_path = p3d.Filename(p3d.ExecutionEnvironment.get_dtool_name()).to_os_specific()
                 stub_path = os.path.join(os.path.dirname(dtool_path), '..', 'bin', stub_name)
@@ -731,6 +988,7 @@ class build_apps(setuptools.Command):
             if not self.log_filename or '%' not in self.log_filename:
                 use_strftime = False
 
+            target_path = os.path.join(binary_dir, target_name)
             freezer.generateRuntimeFromStub(target_path, stub_file, use_console, {
                 'prc_data': prcexport if self.embed_prc_data else None,
                 'default_prc_dir': self.default_prc_dir,
@@ -750,26 +1008,23 @@ class build_apps(setuptools.Command):
                 os.unlink(temp_file.name)
 
             # Copy the dependencies.
-            search_path = [builddir]
+            search_path = [binary_dir]
             if use_wheels:
+                search_path.append(os.path.join(p3dwhlfn, 'panda3d'))
                 search_path.append(os.path.join(p3dwhlfn, 'deploy_libs'))
-            self.copy_dependencies(target_path, builddir, search_path, stub_name)
+            self.copy_dependencies(target_path, binary_dir, search_path, stub_name)
 
             freezer_extras.update(freezer.extras)
             freezer_modules.update(freezer.getAllModuleNames())
-            freezer_modpaths.update({
-                mod[1].filename.to_os_specific()
-                for mod in freezer.getModuleDefs() if mod[1].filename
-            })
             for suffix in freezer.moduleSuffixes:
                 if suffix[2] == imp.C_EXTENSION:
                     ext_suffixes.add(suffix[0])
 
         for appname, scriptname in self.gui_apps.items():
-            create_runtime(appname, scriptname, False)
+            create_runtime(platform, appname, scriptname, False)
 
         for appname, scriptname in self.console_apps.items():
-            create_runtime(appname, scriptname, True)
+            create_runtime(platform, appname, scriptname, True)
 
         # Copy extension modules
         whl_modules = []
@@ -805,7 +1060,7 @@ class build_apps(setuptools.Command):
             plugname = lib.split('.', 1)[0]
             if plugname in plugin_list:
                 source_path = os.path.join(p3dwhlfn, lib)
-                target_path = os.path.join(builddir, os.path.basename(lib))
+                target_path = os.path.join(binary_dir, os.path.basename(lib))
                 search_path = [os.path.dirname(source_path)]
                 self.copy_with_dependencies(source_path, target_path, search_path)
 
@@ -846,8 +1101,13 @@ class build_apps(setuptools.Command):
                 else:
                     continue
 
+            if platform.startswith('android'):
+                # Python modules on Android need a special prefix to be loadable
+                # as a library.
+                basename = 'libpy.' + basename
+
             # If this is a dynamic library, search for dependencies.
-            target_path = os.path.join(builddir, basename)
+            target_path = os.path.join(binary_dir, basename)
             search_path = get_search_path_for(source_path)
             self.copy_with_dependencies(source_path, target_path, search_path)
 
@@ -858,15 +1118,20 @@ class build_apps(setuptools.Command):
 
             if os.path.isdir(tcl_dir) and 'tkinter' in freezer_modules:
                 self.announce('Copying Tcl files', distutils.log.INFO)
-                os.makedirs(os.path.join(builddir, 'tcl'))
+                os.makedirs(os.path.join(binary_dir, 'tcl'))
 
                 for dir in os.listdir(tcl_dir):
                     sub_dir = os.path.join(tcl_dir, dir)
                     if os.path.isdir(sub_dir):
-                        target_dir = os.path.join(builddir, 'tcl', dir)
+                        target_dir = os.path.join(binary_dir, 'tcl', dir)
                         self.announce('copying {0} -> {1}'.format(sub_dir, target_dir))
                         shutil.copytree(sub_dir, target_dir)
 
+        # Copy classes.dex on Android
+        if use_wheels and platform.startswith('android'):
+            self.copy(os.path.join(p3dwhlfn, 'deploy_libs', 'classes.dex'),
+                      os.path.join(binary_dir, '..', '..', 'classes.dex'))
+
         # Extract any other data files from dependency packages.
         for module, datadesc in self.package_data_dirs.items():
             if module not in freezer_modules:
@@ -883,7 +1148,7 @@ class build_apps(setuptools.Command):
                     source_dir = os.path.dirname(source_pattern)
                     # Relocate the target dir to the build directory.
                     target_dir = target_dir.replace('/', os.sep)
-                    target_dir = os.path.join(builddir, target_dir)
+                    target_dir = os.path.join(data_dir, target_dir)
 
                     for wf in filenames:
                         if wf.lower().startswith(source_dir.lower() + '/'):
@@ -903,15 +1168,18 @@ class build_apps(setuptools.Command):
                             else:
                                 self.copy(source_path, target_path)
 
+    def build_data(self, data_dir, platform):
+        """ Builds the data files for the given platform. """
+
         # Copy Game Files
         self.announce('Copying game files for platform: {}'.format(platform), distutils.log.INFO)
         ignore_copy_list = [
             '**/__pycache__/**',
             '**/*.pyc',
+            '**/*.py',
             '{}/**'.format(self.build_base),
         ]
         ignore_copy_list += self.exclude_patterns
-        ignore_copy_list += freezer_modpaths
         ignore_copy_list += self.extra_prc_files
         ignore_copy_list = [p3d.GlobPattern(p3d.Filename.from_os_specific(i).get_fullpath()) for i in ignore_copy_list]
 
@@ -1008,14 +1276,10 @@ class build_apps(setuptools.Command):
 
             for fname in filelist:
                 src = os.path.join(dirpath, fname)
-                dst = os.path.join(builddir, update_path(src))
+                dst = os.path.join(data_dir, update_path(src))
 
                 copy_file(src, dst)
 
-        # Bundle into an .app on macOS
-        if self.macos_main_app and 'macosx' in platform:
-            self.bundle_macos_app(builddir)
-
     def add_dependency(self, name, target_dir, search_path, referenced_by):
         """ Searches for the given DLL on the search path.  If it exists,
         copies it to the target_dir. """
@@ -1333,6 +1597,7 @@ class bdist_apps(setuptools.Command):
         'manylinux1_i686': ['gztar'],
         'manylinux2010_x86_64': ['gztar'],
         'manylinux2010_i686': ['gztar'],
+        'android': ['aab'],
         # Everything else defaults to ['zip']
     }
 
@@ -1342,6 +1607,7 @@ class bdist_apps(setuptools.Command):
         'bztar': installers.create_bztar,
         'xztar': installers.create_xztar,
         'nsis': installers.create_nsis,
+        'aab': installers.create_aab,
     }
 
     description = 'bundle built Panda3D applications into distributable forms'
@@ -1357,6 +1623,9 @@ class bdist_apps(setuptools.Command):
         self.installers = {}
         self.dist_dir = os.path.join(os.getcwd(), 'dist')
         self.skip_build = False
+        self.signing_certificate = None
+        self.signing_private_key = None
+        self.signing_passphrase = None
         self.installer_functions = {}
         self._current_platform = None
         for opt in self._build_apps_options():
@@ -1370,6 +1639,11 @@ class bdist_apps(setuptools.Command):
             for key, value in _parse_dict(self.installers).items()
         }
 
+        if self.signing_certificate:
+            assert self.signing_private_key, 'Missing signing_private_key'
+            self.signing_certificate = os.path.abspath(self.signing_certificate)
+            self.signing_private_key = os.path.abspath(self.signing_private_key)
+
         tmp = self.DEFAULT_INSTALLER_FUNCS.copy()
         tmp.update(self.installer_functions)
         tmp.update({

+ 39 - 2
direct/src/dist/icon.py

@@ -32,6 +32,9 @@ class Icon:
 
         return True
 
+    def getLargestSize(self):
+        return max(self.images.keys())
+
     def generateMissingImages(self):
         """ Generates image sizes that should be present but aren't by scaling
         from the next higher size. """
@@ -52,10 +55,12 @@ class Icon:
             if from_size > required_size:
                 Icon.notify.warning("Generating %dx%d icon by scaling down %dx%d image" % (required_size, required_size, from_size, from_size))
 
+                from_image = self.images[from_size]
                 image = PNMImage(required_size, required_size)
-                if self.images[from_size].hasAlpha():
+                image.setColorType(from_image.getColorType())
+                if from_image.hasAlpha():
                     image.addAlpha()
-                image.quickFilterFrom(self.images[from_size])
+                image.quickFilterFrom(from_image)
                 self.images[required_size] = image
             else:
                 Icon.notify.warning("Cannot generate %dx%d icon; no higher resolution image available" % (required_size, required_size))
@@ -267,3 +272,35 @@ class Icon:
         icns.close()
 
         return True
+
+    def writeSize(self, required_size, fn):
+        if not isinstance(fn, Filename):
+            fn = Filename.fromOsSpecific(fn)
+        fn.setBinary()
+        fn.makeDir()
+
+        if required_size in self.images:
+            image = self.images[required_size]
+        else:
+            # Find the next size up.
+            sizes = sorted(self.images.keys())
+            if required_size * 2 in sizes:
+                from_size = required_size * 2
+            else:
+                from_size = 0
+                for from_size in sizes:
+                    if from_size > required_size:
+                        break
+
+            if from_size > required_size:
+                Icon.notify.warning("Generating %dx%d icon by scaling down %dx%d image" % (required_size, required_size, from_size, from_size))
+            else:
+                Icon.notify.warning("Generating %dx%d icon by scaling up %dx%d image" % (required_size, required_size, from_size, from_size))
+
+            from_image = self.images[from_size]
+            image = PNMImage(required_size, required_size)
+            image.setColorType(from_image.getColorType())
+            image.quickFilterFrom(from_image)
+
+        if not image.write(fn):
+            Icon.notify.error("Failed to write %dx%d to %s" % (required_size, required_size, fn))

+ 116 - 0
direct/src/dist/installers.py

@@ -196,3 +196,119 @@ def create_nsis(command, basename, build_dir):
         )
     cmd.append(nsifile.to_os_specific())
     subprocess.check_call(cmd)
+
+
+def create_aab(command, basename, build_dir):
+    """Create an Android App Bundle.  This is a newer format that replaces
+    Android's .apk format for uploads to the Play Store.  Unlike .apk files, it
+    does not rely on a proprietary signing scheme or an undocumented binary XML
+    format (protobuf is used instead), so it is easier to create without
+    requiring external tools.  If desired, it is possible to install bundletool
+    and use it to convert an .aab into an .apk.
+    """
+
+    from ._android import AndroidManifest, AbiAlias, BundleConfig, NativeLibraries, ResourceTable
+
+    bundle_fn = p3d.Filename.from_os_specific(command.dist_dir) / (basename + '.aab')
+    build_dir_fn = p3d.Filename.from_os_specific(build_dir)
+
+    # Convert the AndroidManifest.xml file to a protobuf-encoded version of it.
+    axml = AndroidManifest()
+    with open(os.path.join(build_dir, 'AndroidManifest.xml'), 'rb') as fh:
+        axml.parse_xml(fh.read())
+
+    # We use our own zip implementation, which can create the correct
+    # alignment and signature needed by Android automatically.
+    bundle_fn.unlink()
+
+    bundle = p3d.ZipArchive()
+    if not bundle.open_read_write(bundle_fn):
+        command.announce(
+            f'\tUnable to open {bundle_fn} for writing', distutils.log.ERROR)
+        return
+
+    config = BundleConfig()
+    config.bundletool.version = '1.1.0'
+    config.optimizations.splits_config.Clear()
+    config.optimizations.uncompress_native_libraries.enabled = False
+    bundle.add_subfile('BundleConfig.pb', p3d.StringStream(config.SerializeToString()), 9)
+
+    resources = ResourceTable()
+    package = resources.package.add()
+    package.package_id.id = 0x7f
+    for attrib in axml.root.element.attribute:
+        if attrib.name == 'package':
+            package.package_name = attrib.value
+
+    # Were there any icons referenced in the AndroidManifest.xml?
+    for type_i, type_name in enumerate(axml.resource_types):
+        res_type = package.type.add()
+        res_type.name = type_name
+        res_type.type_id.id = type_i + 1
+
+        for entry_id, res_name in enumerate(axml.resources[type_name]):
+            entry = res_type.entry.add()
+            entry.entry_id.id = entry_id
+            entry.name = res_name
+
+            for density, tag in (160, 'mdpi'), (240, 'hdpi'), (320, 'xhdpi'), (480, 'xxhdpi'), (640, 'xxxhdpi'):
+                path = f'res/mipmap-{tag}-v4/{res_name}.png'
+                if (build_dir_fn / path).exists():
+                    bundle.add_subfile('base/' + path, build_dir_fn / path, 0)
+                    config_value = entry.config_value.add()
+                    config_value.config.density = density
+                    config_value.value.item.file.path = path
+
+    bundle.add_subfile('base/resources.pb', p3d.StringStream(resources.SerializeToString()), 9)
+
+    native = NativeLibraries()
+    for abi in os.listdir(os.path.join(build_dir, 'lib')):
+        native_dir = native.directory.add()
+        native_dir.path = 'lib/' + abi
+        native_dir.targeting.abi.alias = getattr(AbiAlias, abi.upper().replace('-', '_'))
+    bundle.add_subfile('base/native.pb', p3d.StringStream(native.SerializeToString()), 9)
+
+    bundle.add_subfile('base/manifest/AndroidManifest.xml', p3d.StringStream(axml.dumps()), 9)
+
+    # Add the classes.dex.
+    bundle.add_subfile(f'base/dex/classes.dex', build_dir_fn / 'classes.dex', 9)
+
+    # Add libraries, compressed.
+    for abi in os.listdir(os.path.join(build_dir, 'lib')):
+        abi_dir = os.path.join(build_dir, 'lib', abi)
+
+        for lib in os.listdir(abi_dir):
+            if lib.startswith('lib') and lib.endswith('.so'):
+                bundle.add_subfile(f'base/lib/{abi}/{lib}', build_dir_fn / 'lib' / abi / lib, 9)
+
+    # Add assets, compressed.
+    assets_dir = os.path.join(build_dir, 'assets')
+    for dirpath, dirnames, filenames in os.walk(assets_dir):
+        rel_dirpath = os.path.relpath(dirpath, build_dir).replace('\\', '/')
+        dirnames.sort()
+        filenames.sort()
+
+        for name in filenames:
+            fn = p3d.Filename.from_os_specific(dirpath) / name
+            if fn.is_regular_file():
+                bundle.add_subfile(f'base/{rel_dirpath}/{name}', fn, 9)
+
+    # Finally, generate the manifest file / signature, if a signing certificate
+    # has been specified.
+    if command.signing_certificate:
+        password = command.signing_passphrase or ''
+
+        if not password and 'ENCRYPTED' in open(command.signing_private_key).read():
+            # It appears to be encrypted, and we don't have a passphrase, so we
+            # must request it on the command-line.
+            from getpass import getpass
+            password = getpass(f'Enter pass phrase for private key: ')
+
+        if not bundle.add_jar_signature(
+                p3d.Filename.from_os_specific(command.signing_certificate),
+                p3d.Filename.from_os_specific(command.signing_private_key),
+                password):
+            command.announce(
+                f'\tFailed to sign {bundle_fn}.', distutils.log.ERROR)
+
+    bundle.close()

+ 2 - 2
direct/src/distributed/ClientRepository.py

@@ -6,7 +6,7 @@ from direct.showbase.MessengerGlobal import messenger
 from .MsgTypesCMU import *
 from .PyDatagram import PyDatagram
 from .PyDatagramIterator import PyDatagramIterator
-from panda3d.core import UniqueIdAllocator, Notify
+from panda3d.core import UniqueIdAllocator, Notify, ClockObject
 
 
 class ClientRepository(ClientRepositoryBase):
@@ -281,7 +281,7 @@ class ClientRepository(ClientRepositoryBase):
         datagram.addUint16(CLIENT_HEARTBEAT_CMU)
         # Send it!
         self.send(datagram)
-        self.lastHeartbeat = globalClock.getRealTime()
+        self.lastHeartbeat = ClockObject.getGlobalClock().getRealTime()
         # This is important enough to consider flushing immediately
         # (particularly if we haven't run readerPollTask recently).
         self.considerFlush()

+ 3 - 3
direct/src/distributed/ClientRepositoryBase.py

@@ -186,7 +186,7 @@ class ClientRepositoryBase(ConnectionRepository):
                 self.doGenerate(*args)
 
                 if deferrable:
-                    self.lastGenerate = globalClock.getFrameTime()
+                    self.lastGenerate = ClockObject.getGlobalClock().getFrameTime()
 
                 for dg, di in updates:
                     # non-DC updates that need to be played back in-order are
@@ -207,7 +207,7 @@ class ClientRepositoryBase(ConnectionRepository):
         """ This is the task that generates an object on the deferred
         queue. """
 
-        now = globalClock.getFrameTime()
+        now = ClockObject.getGlobalClock().getFrameTime()
         while self.deferredGenerates:
             if now - self.lastGenerate < self.deferInterval:
                 # Come back later.
@@ -539,7 +539,7 @@ class ClientRepositoryBase(ConnectionRepository):
             self.notify.debug("Heartbeats not started; not sending.")
             return
 
-        elapsed = globalClock.getRealTime() - self.lastHeartbeat
+        elapsed = ClockObject.getGlobalClock().getRealTime() - self.lastHeartbeat
         if elapsed < 0 or elapsed > self.heartbeatInterval:
             # It's time to send the heartbeat again (or maybe someone
             # reset the clock back).

+ 2 - 1
direct/src/distributed/DistributedSmoothNode.py

@@ -187,7 +187,7 @@ class DistributedSmoothNode(DistributedNode.DistributedNode,
         reflect the node's current position
         """
         if self.stopped:
-            currTime = globalClock.getFrameTime()
+            currTime = ClockObject.getGlobalClock().getFrameTime()
             now = currTime - self.smoother.getExpectedBroadcastPeriod()
             last = self.smoother.getMostRecentTimestamp()
             if now > last:
@@ -348,6 +348,7 @@ class DistributedSmoothNode(DistributedNode.DistributedNode,
                 self.smoother.setPhonyTimestamp()
             self.smoother.markPosition()
         else:
+            globalClock = ClockObject.getGlobalClock()
             now = globalClock.getFrameTime()
             local = globalClockDelta.networkToLocalTime(timestamp, now)
             realTime = globalClock.getRealTime()

+ 3 - 3
direct/src/distributed/DoInterestManager.py

@@ -157,7 +157,7 @@ class DoInterestManager(DirectObject.DirectObject):
         """
         assert DoInterestManager.notify.debugCall()
         handle = self._getNextHandle()
-        # print 'base.cr.addInterest(',description,',',handle,'):',globalClock.getFrameCount()
+        # print 'base.cr.addInterest(',description,',',handle,'):',base.clock.getFrameCount()
         if self._noNewInterests:
             DoInterestManager.notify.warning(
                 "addInterest: addingInterests on delete: %s" % (handle))
@@ -229,7 +229,7 @@ class DoInterestManager(DirectObject.DirectObject):
         """
         Stop looking in a (set of) zone(s)
         """
-        # print 'base.cr.removeInterest(',handle,'):',globalClock.getFrameCount()
+        # print 'base.cr.removeInterest(',handle,'):',base.clock.getFrameCount()
 
         assert DoInterestManager.notify.debugCall()
         assert isinstance(handle, InterestHandle)
@@ -567,7 +567,7 @@ class DoInterestManager(DirectObject.DirectObject):
         def checkMoreInterests():
             # if there are new interests, cancel this delayed callback, another
             # will automatically be scheduled when all interests complete
-            # print 'checkMoreInterests(',self._completeEventCount.num,'):',globalClock.getFrameCount()
+            # print 'checkMoreInterests(',self._completeEventCount.num,'):',base.clock.getFrameCount()
             return self._completeEventCount.num > 0
         def sendEvent():
             messenger.send(self.getAllInterestsCompleteEvent())

+ 5 - 3
direct/src/distributed/TimeManager.py

@@ -6,6 +6,7 @@ from direct.distributed import DistributedObject
 from direct.directnotify import DirectNotifyGlobal
 from direct.distributed.ClockDelta import globalClockDelta
 
+
 class TimeManager(DistributedObject.DistributedObject):
     """
     This DistributedObject lives on the AI and on the client side, and
@@ -128,7 +129,7 @@ class TimeManager(DistributedObject.DistributedObject):
         The return value is true if the attempt is made, or false if
         it is too soon since the last attempt.
         """
-        now = globalClock.getRealTime()
+        now = ClockObject.getGlobalClock().getRealTime()
 
         if now - self.lastAttempt < self.minWait:
             self.notify.debug("Not resyncing (too soon): %s" % (description))
@@ -157,7 +158,8 @@ class TimeManager(DistributedObject.DistributedObject):
         determine the clock delta between the AI and the client
         machines.
         """
-        end = globalClock.getRealTime()
+        clock = ClockObject.getGlobalClock()
+        end = clock.getRealTime()
 
         if context != self.thisContext:
             self.notify.info("Ignoring TimeManager response for old context %d" % (context))
@@ -177,7 +179,7 @@ class TimeManager(DistributedObject.DistributedObject):
         if globalClockDelta.getUncertainty() > self.maxUncertainty:
             if self.attemptCount < self.maxAttempts:
                 self.notify.info("Uncertainty is too high, trying again.")
-                self.start = globalClock.getRealTime()
+                self.start = clock.getRealTime()
                 self.sendUpdate("requestServerTime", [self.thisContext])
                 return
             self.notify.info("Giving up on uncertainty requirement.")

+ 3 - 1
direct/src/gui/OnscreenImage.py

@@ -107,7 +107,9 @@ class OnscreenImage(DirectObject, NodePath):
                 tex = image
             else:
                 # It's a Texture file name
-                tex = base.loader.loadTexture(image)
+                tex = TexturePool.loadTexture(image)
+                if not tex:
+                    raise IOError('Could not load texture: %s' % (image))
             cm = CardMaker('OnscreenImage')
             cm.setFrame(-1, 1, -1, 1)
             self.assign(parent.attachNewNode(cm.generate(), sort))

+ 4 - 4
direct/src/interval/IntervalTest.py

@@ -137,22 +137,22 @@ if __name__ == "__main__":
     startTime = 0.0
     def printStart():
         global startTime
-        startTime = globalClock.getFrameTime()
+        startTime = base.clock.getFrameTime()
         print('Start')
 
     def printPreviousStart():
         global startTime
-        currTime = globalClock.getFrameTime()
+        currTime = base.clock.getFrameTime()
         print('PREVIOUS_END %0.2f' % (currTime - startTime))
 
     def printPreviousEnd():
         global startTime
-        currTime = globalClock.getFrameTime()
+        currTime = base.clock.getFrameTime()
         print('PREVIOUS_END %0.2f' % (currTime - startTime))
 
     def printTrackStart():
         global startTime
-        currTime = globalClock.getFrameTime()
+        currTime = base.clock.getFrameTime()
         print('TRACK_START %0.2f' % (currTime - startTime))
 
     def printArguments(a, b, c):

+ 3 - 2
direct/src/leveleditor/LevelEditorBase.py

@@ -8,6 +8,7 @@ Refer LevelEditor.py for example.
 from direct.showbase.DirectObject import *
 from direct.directtools.DirectUtil import *
 from direct.gui.DirectGui import *
+from panda3d.core import ClockObject
 
 from .CurveEditor import *
 from .FileMgr import *
@@ -385,7 +386,7 @@ class LevelEditorBase(DirectObject):
                     alreadyExists = True
                     break
             if not alreadyExists:
-                time = globalClock.getRealTime() + 15
+                time = ClockObject.getGlobalClock().getRealTime() + 15
                 self.statusLines.append([time,status,color])
 
         # update display of new status lines
@@ -407,7 +408,7 @@ class LevelEditorBase(DirectObject):
     def updateStatusReadoutTimeouts(self,task=None):
         removalList = []
         for currLine in self.statusLines:
-            if globalClock.getRealTime() >= currLine[0]:
+            if ClockObject.getGlobalClock().getRealTime() >= currLine[0]:
                 removalList.append(currLine)
         for currRemoval in removalList:
             self.statusLines.remove(currRemoval)

+ 6 - 5
direct/src/showbase/JobManager.py

@@ -150,9 +150,10 @@ class JobManager:
             self._useOverflowTime = ConfigVariableBool('job-use-overflow-time', 1).value
 
         if len(self._pri2jobId2job) > 0:
+            clock = ClockObject.getGlobalClock()
             #assert self.notify.debugCall()
             # figure out how long we can run
-            endT = globalClock.getRealTime() + (self.getTimeslice() * .9)
+            endT = clock.getRealTime() + (self.getTimeslice() * .9)
             while True:
                 if self._jobIdGenerator is None:
                     # round-robin the jobs, giving high-priority jobs more timeslices
@@ -173,7 +174,7 @@ class JobManager:
                 # check if there's overflow time that we need to make up for
                 if self._useOverflowTime:
                     overflowTime = self._jobId2overflowTime[jobId]
-                    timeLeft = endT - globalClock.getRealTime()
+                    timeLeft = endT - clock.getRealTime()
                     if overflowTime >= timeLeft:
                         self._jobId2overflowTime[jobId] = max(0., overflowTime-timeLeft)
                         # don't run any more jobs this frame, this makes up
@@ -184,7 +185,7 @@ class JobManager:
                 if __debug__:
                     job._pstats.start()
                 job.resume()
-                while globalClock.getRealTime() < endT:
+                while clock.getRealTime() < endT:
                     try:
                         result = next(gen)
                     except StopIteration:
@@ -210,9 +211,9 @@ class JobManager:
                         break
                 else:
                     # we've run out of time
-                    #assert self.notify.debug('timeslice end: %s, %s' % (endT, globalClock.getRealTime()))
+                    #assert self.notify.debug('timeslice end: %s, %s' % (endT, clock.getRealTime()))
                     job.suspend()
-                    overflowTime = globalClock.getRealTime() - endT
+                    overflowTime = clock.getRealTime() - endT
                     if overflowTime > self.getTimeslice():
                         self._jobId2overflowTime[jobId] += overflowTime
                     if __debug__:

+ 6 - 4
direct/src/showbase/PythonUtil.py

@@ -43,7 +43,7 @@ import functools
 
 __report_indent = 3
 
-from panda3d.core import ConfigVariableBool
+from panda3d.core import ConfigVariableBool, ClockObject
 
 
 ## with one integer positional arg, this uses about 4/5 of the memory of the Functor class below
@@ -2053,6 +2053,7 @@ def report(types = [], prefix = '', xform = None, notifyFunc = None, dConfigPara
             if prefixes:
                 outStr = '%%s %s' % (outStr,)
 
+            globalClock = ClockObject.getGlobalClock()
 
             if 'module' in types:
                 outStr = '%s {M:%s}' % (outStr, f.__module__.split('.')[-1])
@@ -2264,9 +2265,10 @@ if __debug__:
                 # at the time that PythonUtil is loaded
                 if not ConfigVariableBool("profile-debug", False):
                     #dumb timings
-                    st=globalClock.getRealTime()
-                    f(*args,**kArgs)
-                    s=globalClock.getRealTime()-st
+                    clock = ClockObject.getGlobalClock()
+                    st = clock.getRealTime()
+                    f(*args, **kArgs)
+                    s = clock.getRealTime() - st
                     print("Function %s.%s took %s seconds"%(f.__module__, f.__name__,s))
                 else:
                     import profile as prof, pstats

+ 14 - 10
direct/src/showbase/ShowBase.py

@@ -384,18 +384,21 @@ class ShowBase(DirectObject.DirectObject):
 
         # Get a pointer to Panda's global ClockObject, used for
         # synchronizing events between Python and C.
-        globalClock = ClockObject.getGlobalClock()
+        clock = ClockObject.getGlobalClock()
+
+        #: This is the global :class:`~panda3d.core.ClockObject`.
+        self.clock = clock
 
         # Since we have already started up a TaskManager, and probably
         # a number of tasks; and since the TaskManager had to use the
         # TrueClock to tell time until this moment, make sure the
         # globalClock object is exactly in sync with the TrueClock.
         trueClock = TrueClock.getGlobalPtr()
-        globalClock.setRealTime(trueClock.getShortTime())
-        globalClock.tick()
+        clock.setRealTime(trueClock.getShortTime())
+        clock.tick()
 
-        # Now we can make the TaskManager start using the new globalClock.
-        taskMgr.globalClock = globalClock
+        # Now we can make the TaskManager start using the new clock.
+        taskMgr.globalClock = clock
 
         # client CPU affinity is determined by, in order:
         # - client-cpu-affinity-mask config
@@ -444,7 +447,7 @@ class ShowBase(DirectObject.DirectObject):
         builtins.ostream = Notify.out()
         builtins.directNotify = directNotify
         builtins.giveNotify = giveNotify
-        builtins.globalClock = globalClock
+        builtins.globalClock = clock
         builtins.vfs = vfs
         builtins.cpMgr = ConfigPageManager.getGlobalPtr()
         builtins.cvMgr = ConfigVariableManager.getGlobalPtr()
@@ -1899,7 +1902,7 @@ class ShowBase(DirectObject.DirectObject):
         return self.physicsMgrEnabled
 
     def updateManagers(self, state):
-        dt = globalClock.getDt()
+        dt = self.clock.dt
         if self.particleMgrEnabled:
             self.particleMgr.doParticles(dt)
         if self.physicsMgrEnabled:
@@ -2927,14 +2930,15 @@ class ShowBase(DirectObject.DirectObject):
         Returns:
             A `~direct.task.Task` that can be awaited.
         """
-        globalClock.setMode(ClockObject.MNonRealTime)
-        globalClock.setDt(1.0/float(fps))
+        clock = self.clock
+        clock.mode = ClockObject.MNonRealTime
+        clock.dt = 1.0 / fps
         t = self.taskMgr.add(self._movieTask, namePrefix + '_task')
         t.frameIndex = 0  # Frame 0 is not captured.
         t.numFrames = int(duration * fps)
         t.source = source
         t.outputString = namePrefix + '_%0' + repr(sd) + 'd.' + format
-        t.setUponDeath(lambda state: globalClock.setMode(ClockObject.MNormal))
+        t.setUponDeath(lambda state: clock.setMode(ClockObject.MNormal))
         return t
 
     def _movieTask(self, state):

+ 1 - 0
direct/src/showbase/ShowBaseGlobal.py

@@ -33,6 +33,7 @@ ostream = Notify.out()
 
 #: The clock object used by default for rendering and animation, obtained using
 #: :meth:`panda3d.core.ClockObject.getGlobalClock()`.
+#: @deprecated Use `base.clock` instead.
 globalClock = ClockObject.getGlobalClock()
 
 #: See :meth:`panda3d.core.ConfigPageManager.getGlobalPtr()`.

+ 4 - 3
direct/src/showbase/TaskThreaded.py

@@ -5,6 +5,7 @@ __all__ = ['TaskThreaded', 'TaskThread']
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.task import Task
 from direct.task.TaskManagerGlobal import taskMgr
+from panda3d.core import ClockObject
 
 from .PythonUtil import SerialNumGen, Functor
 
@@ -89,14 +90,14 @@ class TaskThreaded:
     def _doCallback(self, callback, taskName, task):
         assert self.notify.debugCall()
         self.__taskNames.remove(taskName)
-        self._taskStartTime = globalClock.getRealTime()
+        self._taskStartTime = ClockObject.getGlobalClock().getRealTime()
         callback()
         self._taskStartTime = None
         return Task.done
 
     def _doThreadCallback(self, thread, taskName, task):
         assert self.notify.debugCall()
-        self._taskStartTime = globalClock.getRealTime()
+        self._taskStartTime = ClockObject.getGlobalClock().getRealTime()
         thread.run()
         self._taskStartTime = None
         if thread.isFinished():
@@ -114,7 +115,7 @@ class TaskThreaded:
             # we must not be in a task callback, we must be running in non-threaded
             # mode
             return True
-        return (globalClock.getRealTime() - self._taskStartTime) < self.__timeslice
+        return (ClockObject.getGlobalClock().getRealTime() - self._taskStartTime) < self.__timeslice
 
 class TaskThread:
     # derive and override these four funcs

+ 3 - 3
direct/src/task/FrameProfiler.py

@@ -1,4 +1,4 @@
-from panda3d.core import ConfigVariableBool
+from panda3d.core import ConfigVariableBool, ClockObject
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.fsm.StatePush import FunctionCall
 from direct.showbase.PythonUtil import formatTimeExact, normalDistrib, serialNum
@@ -59,7 +59,7 @@ class FrameProfiler:
     def _setEnabled(self, enabled):
         if enabled:
             self.notify.info('frame profiler started')
-            self._startTime = globalClock.getFrameTime()
+            self._startTime = ClockObject.getGlobalClock().getFrameTime()
             self._profileCounter = 0
             self._jitter = None
             self._period2aggregateProfile = {}
@@ -110,7 +110,7 @@ class FrameProfiler:
             self._analyzeResults, sessionId))
 
         # schedule the next profile
-        delay = max(time - globalClock.getFrameTime(), 0.)
+        delay = max(time - ClockObject.getGlobalClock().getFrameTime(), 0.)
         self._task = taskMgr.doMethodLater(delay, self._scheduleNextProfileDoLater,
                                            'FrameProfiler-%s' % serialNum())
 

+ 5 - 11
direct/src/task/Task.py

@@ -274,33 +274,27 @@ class TaskManager:
     def getTasksNamed(self, taskName):
         """Returns a list of all tasks, active or sleeping, with the
         indicated name. """
-        return self.__makeTaskList(self.mgr.findTasks(taskName))
+        return list(self.mgr.findTasks(taskName))
 
     def getTasksMatching(self, taskPattern):
         """Returns a list of all tasks, active or sleeping, with a
         name that matches the pattern, which can include standard
         shell globbing characters like \\*, ?, and []. """
 
-        return self.__makeTaskList(self.mgr.findTasksMatching(GlobPattern(taskPattern)))
+        return list(self.mgr.findTasksMatching(GlobPattern(taskPattern)))
 
     def getAllTasks(self):
         """Returns list of all tasks, active and sleeping, in
         arbitrary order. """
-        return self.__makeTaskList(self.mgr.getTasks())
+        return list(self.mgr.getTasks())
 
     def getTasks(self):
         """Returns list of all active tasks in arbitrary order. """
-        return self.__makeTaskList(self.mgr.getActiveTasks())
+        return list(self.mgr.getActiveTasks())
 
     def getDoLaters(self):
         """Returns list of all sleeping tasks in arbitrary order. """
-        return self.__makeTaskList(self.mgr.getSleepingTasks())
-
-    def __makeTaskList(self, taskCollection):
-        l = []
-        for i in range(taskCollection.getNumTasks()):
-            l.append(taskCollection.getTask(i))
-        return l
+        return list(self.mgr.getSleepingTasks())
 
     def doMethodLater(self, delayTime, funcOrTask, name, extraArgs = None,
                       sort = None, priority = None, taskChain = None,

+ 5 - 3
direct/src/task/Timer.py

@@ -2,6 +2,8 @@
 
 __all__ = ['Timer']
 
+from panda3d.core import ClockObject
+
 from . import Task
 from .TaskManagerGlobal import taskMgr
 
@@ -25,7 +27,7 @@ class Timer:
         self.callback = None
         self.finalT = t
         self.name = name
-        self.startT = globalClock.getFrameTime()
+        self.startT = ClockObject.getGlobalClock().getFrameTime()
         self.currT = 0.0
         taskMgr.add(self.__timerTask, self.name + '-run')
         self.started = 1
@@ -35,7 +37,7 @@ class Timer:
             self.stop()
         self.callback = callback
         self.finalT = t
-        self.startT = globalClock.getFrameTime()
+        self.startT = ClockObject.getGlobalClock().getFrameTime()
         self.currT = 0.0
         taskMgr.add(self.__timerTask, self.name + '-run')
         self.started = 1
@@ -71,7 +73,7 @@ class Timer:
         return self.finalT - self.currT
 
     def __timerTask(self, task):
-        t = globalClock.getFrameTime()
+        t = ClockObject.getGlobalClock().getFrameTime()
         te = t - self.startT
         self.currT = te
         if te >= self.finalT:

+ 3 - 3
direct/src/tkpanels/AnimPanel.py

@@ -5,7 +5,7 @@ __all__ = ['AnimPanel', 'ActorControl']
 ### SEE END OF FILE FOR EXAMPLE USEAGE ###
 
 # Import Tkinter, Pmw, and the floater code from this directory tree.
-from panda3d.core import Filename, getModelPath
+from panda3d.core import Filename, getModelPath, ClockObject
 from direct.tkwidgets.AppShell import *
 from direct.showbase.TkGlobal import *
 from direct.task import Task
@@ -294,7 +294,7 @@ class AnimPanel(AppShell):
 
     def playActorControls(self):
         self.stopActorControls()
-        self.lastT = globalClock.getFrameTime()
+        self.lastT = ClockObject.getGlobalClock().getFrameTime()
         self.playList = self.actorControlList[:]
         taskMgr.add(self.play, self.id + '_UpdateTask')
 
@@ -302,7 +302,7 @@ class AnimPanel(AppShell):
         if not self.playList:
             return Task.done
         fLoop = self.loopVar.get()
-        currT = globalClock.getFrameTime()
+        currT = ClockObject.getGlobalClock().getFrameTime()
         deltaT = currT - self.lastT
         self.lastT = currT
         for actorControl in self.playList:

+ 4 - 4
direct/src/tkpanels/MopathRecorder.py

@@ -983,7 +983,7 @@ class MopathRecorder(AppShell, DirectObject):
                 # Start new task
                 t = taskMgr.add(
                     self.recordTask, self.name + '-recordTask')
-                t.startTime = globalClock.getFrameTime()
+                t.startTime = ClockObject.getGlobalClock().getFrameTime()
         else:
             if self.samplingMode == 'Continuous':
                 # Kill old task
@@ -1016,7 +1016,7 @@ class MopathRecorder(AppShell, DirectObject):
     def recordTask(self, state):
         # Record raw data point
         time = self.recordStart + (
-            globalClock.getFrameTime() - state.startTime)
+            ClockObject.getGlobalClock().getFrameTime() - state.startTime)
         self.recordPoint(time)
         return Task.cont
 
@@ -1281,7 +1281,7 @@ class MopathRecorder(AppShell, DirectObject):
         t = taskMgr.add(
             self.playbackTask, self.name + '-playbackTask')
         t.currentTime = self.playbackTime
-        t.lastTime = globalClock.getFrameTime()
+        t.lastTime = ClockObject.getGlobalClock().getFrameTime()
 
     def setSpeedScale(self, value):
         self.speedScale.set(math.log10(value))
@@ -1291,7 +1291,7 @@ class MopathRecorder(AppShell, DirectObject):
         self.speedVar.set('%0.2f' % self.playbackSF)
 
     def playbackTask(self, state):
-        time = globalClock.getFrameTime()
+        time = ClockObject.getGlobalClock().getFrameTime()
         dTime = self.playbackSF * (time - state.lastTime)
         state.lastTime = time
         if self.loopPlayback:

+ 3 - 2
direct/src/tkwidgets/Dial.py

@@ -8,6 +8,7 @@ __all__ = ['Dial', 'AngleDial', 'DialWidget']
 from direct.showbase.TkGlobal import *
 from .Valuator import Valuator, VALUATOR_MINI, VALUATOR_FULL
 from direct.task import Task
+from panda3d.core import ClockObject
 import math
 import operator
 import Pmw
@@ -335,11 +336,11 @@ class DialWidget(Pmw.MegaWidget):
         self._onButtonPress()
         self.knobSF = 0.0
         self.updateTask = taskMgr.add(self.updateDialTask, 'updateDial')
-        self.updateTask.lastTime = globalClock.getFrameTime()
+        self.updateTask.lastTime = ClockObject.getGlobalClock().getFrameTime()
 
     def updateDialTask(self, state):
         # Update value
-        currT = globalClock.getFrameTime()
+        currT = ClockObject.getGlobalClock().getFrameTime()
         dt = currT - state.lastTime
         self.set(self.value + self.knobSF * dt)
         state.lastTime = currT

+ 3 - 2
direct/src/tkwidgets/Floater.py

@@ -8,6 +8,7 @@ __all__ = ['Floater', 'FloaterWidget', 'FloaterGroup']
 from direct.showbase.TkGlobal import *
 from .Valuator import Valuator, VALUATOR_MINI, VALUATOR_FULL
 from direct.task import Task
+from panda3d.core import ClockObject
 import math
 import Pmw
 
@@ -149,14 +150,14 @@ class FloaterWidget(Pmw.MegaWidget):
         self.velocitySF = 0.0
         self.updateTask = taskMgr.add(self.updateFloaterTask,
                                         'updateFloater')
-        self.updateTask.lastTime = globalClock.getFrameTime()
+        self.updateTask.lastTime = ClockObject.getGlobalClock().getFrameTime()
 
     def updateFloaterTask(self, state):
         """
         Update floaterWidget value based on current scaleFactor
         Adjust for time to compensate for fluctuating frame rates
         """
-        currT = globalClock.getFrameTime()
+        currT = ClockObject.getGlobalClock().getFrameTime()
         dt = currT - state.lastTime
         self.set(self.value + self.velocitySF * dt)
         state.lastTime = currT

+ 10 - 2
dtool/src/dtoolbase/cmath.I

@@ -64,10 +64,14 @@ csincos(float v, float *sin_result, float *cos_result) {
       fstp DWORD ptr [edx]
       fstp DWORD ptr [eax]
       }
-#else //!_X86_
+#elif defined(__APPLE__)
+  __sincosf(v, sin_result, cos_result);
+#elif defined(_GNU_SOURCE)
+  sincosf(v, sin_result, cos_result);
+#else
   *sin_result = sinf(v);
   *cos_result = cosf(v);
-#endif //!_X86_
+#endif
 }
 
 /**
@@ -231,6 +235,10 @@ csincos(double v, double *sin_result, double *cos_result) {
       fstp QWORD ptr [edx]
       fstp QWORD ptr [eax]
       }
+#elif defined(__APPLE__)
+  __sincos(v, sin_result, cos_result);
+#elif defined(_GNU_SOURCE)
+  sincos(v, sin_result, cos_result);
 #else //!_X86_
   *sin_result = sin(v);
   *cos_result = cos(v);

+ 3 - 0
dtool/src/dtoolbase/typeHandle.h

@@ -137,6 +137,9 @@ PUBLISHED:
   MAKE_SEQ_PROPERTY(parent_classes, get_num_parent_classes, get_parent_class);
   MAKE_SEQ_PROPERTY(child_classes, get_num_child_classes, get_child_class);
 
+  EXTENSION(PyObject *__reduce__() const);
+  EXTENSION(void __setstate__(PyObject *));
+
 public:
 #ifdef HAVE_PYTHON
   PyObject *get_python_type() const;

+ 51 - 0
dtool/src/dtoolbase/typeHandle_ext.cxx

@@ -32,4 +32,55 @@ make(PyTypeObject *tp) {
   return dtool_tp->_type;
 }
 
+/**
+ * Implements pickle support.
+ */
+PyObject *Extension<TypeHandle>::
+__reduce__() const {
+  extern struct Dtool_PyTypedObject Dtool_TypeHandle;
+  extern struct Dtool_PyTypedObject Dtool_TypeRegistry;
+
+  if (!*_this) {
+    PyObject *func = PyObject_GetAttrString((PyObject *)&Dtool_TypeHandle, "none");
+    return Py_BuildValue("N()", func);
+  }
+
+  // If we have a Python binding registered for it, that's the preferred method,
+  // since it ensures that the appropriate module gets loaded by pickle.
+  PyObject *py_type = _this->get_python_type();
+  if (py_type != nullptr && *_this == ((Dtool_PyTypedObject *)py_type)->_type) {
+    PyObject *func = PyObject_GetAttrString((PyObject *)&Dtool_TypeHandle, "make");
+    return Py_BuildValue("N(O)", func, py_type);
+  }
+
+  // Fall back to the __setstate__ mechanism.
+  std::string name = _this->get_name();
+  Py_ssize_t num_parents = _this->get_num_parent_classes();
+  PyObject *parents = PyTuple_New(num_parents);
+  for (Py_ssize_t i = 0; i < num_parents; ++i) {
+    PyObject *parent = DTool_CreatePyInstance(new TypeHandle(_this->get_parent_class(i)), Dtool_TypeHandle, true, false);
+    PyTuple_SET_ITEM(parents, i, parent);
+  }
+  return Py_BuildValue("O()(s#N)", (PyObject *)&Dtool_TypeHandle, name.c_str(), name.size(), parents);
+}
+
+/**
+ * Implements pickle support.
+ */
+void Extension<TypeHandle>::
+__setstate__(PyObject *state) {
+  Py_ssize_t len;
+  const char *name_str = PyUnicode_AsUTF8AndSize(PyTuple_GET_ITEM(state, 0), &len);
+  PyObject *parents = PyTuple_GET_ITEM(state, 1);
+
+  TypeRegistry *type_registry = TypeRegistry::ptr();
+  *_this = type_registry->register_dynamic_type(std::string(name_str, len));
+
+  Py_ssize_t num_parents = PyTuple_GET_SIZE(parents);
+  for (Py_ssize_t i = 0; i < num_parents; ++i) {
+    TypeHandle *parent = (TypeHandle *)DtoolInstance_VOID_PTR(PyTuple_GET_ITEM(parents, i));
+    type_registry->record_derivation(*_this, *parent);
+  }
+}
+
 #endif

+ 3 - 0
dtool/src/dtoolbase/typeHandle_ext.h

@@ -30,6 +30,9 @@ template<>
 class Extension<TypeHandle> : public ExtensionBase<TypeHandle> {
 public:
   static TypeHandle make(PyTypeObject *tp);
+
+  PyObject *__reduce__() const;
+  void __setstate__(PyObject *);
 };
 
 #endif  // HAVE_PYTHON

+ 1 - 0
dtool/src/interrogate/interfaceMakerPythonNative.cxx

@@ -109,6 +109,7 @@ RenameSet methodRenameDictionary[] = {
   { "__nonzero__"   , "__nonzero__",            0 },
   { "__int__"       , "__int__",                0 },
   { "__reduce__"    , "__reduce__",             0 },
+  { "__reduce_ex__" , "__reduce_ex__",          0 },
   { "__reduce_persist__", "__reduce_persist__", 0 },
   { "__copy__"      , "__copy__",               0 },
   { "__deepcopy__"  , "__deepcopy__",           0 },

+ 0 - 18
dtool/src/interrogatedb/py_compat.cxx

@@ -43,22 +43,4 @@ size_t PyLongOrInt_AsSize_t(PyObject *vv) {
 }
 #endif
 
-#if PY_VERSION_HEX < 0x03090000
-/**
- * Most efficient way to call a function without any arguments.
- */
-PyObject *PyObject_CallNoArgs(PyObject *func) {
-#if PY_VERSION_HEX >= 0x03080000
-  return _PyObject_Vectorcall(func, nullptr, 0, nullptr);
-#elif PY_VERSION_HEX >= 0x03070000
-  return _PyObject_FastCallDict(func, nullptr, 0, nullptr);
-#elif PY_VERSION_HEX >= 0x03060000
-  return _PyObject_FastCall(func, nullptr, 0);
-#else
-  static PyObject *empty_tuple = PyTuple_New(0);
-  return PyObject_Call(func, empty_tuple, nullptr);
-#endif
-}
-#endif
-
 #endif  // HAVE_PYTHON

+ 11 - 1
dtool/src/interrogatedb/py_compat.h

@@ -212,7 +212,17 @@ INLINE PyObject *_PyLong_Lshift(PyObject *a, size_t shiftby) {
 /* Python 3.9 */
 
 #if PY_VERSION_HEX < 0x03090000
-EXPCL_PYPANDA PyObject *PyObject_CallNoArgs(PyObject *func);
+INLINE PyObject *PyObject_CallNoArgs(PyObject *func) {
+#if PY_VERSION_HEX >= 0x03080000
+  return _PyObject_Vectorcall(func, nullptr, 0, nullptr);
+#elif PY_VERSION_HEX >= 0x03070000
+  return _PyObject_FastCallDict(func, nullptr, 0, nullptr);
+#elif PY_VERSION_HEX >= 0x03060000
+  return _PyObject_FastCall(func, nullptr, 0);
+#else
+  return PyObject_CallObject(func, nullptr);
+#endif
+}
 
 INLINE PyObject *PyObject_CallOneArg(PyObject *callable, PyObject *arg) {
 #if PY_VERSION_HEX >= 0x03060000

+ 10 - 0
dtool/src/parser-inc/android/log.h

@@ -0,0 +1,10 @@
+#pragma once
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <sys/cdefs.h>
+
+typedef enum android_LogPriority android_LogPriority;
+typedef enum log_id log_id_t;
+struct __android_log_message;

+ 0 - 5
dtool/src/parser-inc/stdtypedefs.h

@@ -22,11 +22,6 @@
 typedef long time_t;
 typedef long clock_t;
 
-typedef unsigned int uint;
-typedef unsigned long ulong;
-typedef unsigned short ushort;
-typedef unsigned char uchar;
-
 #ifdef _WIN64
 #define __SIZE_TYPE__ unsigned long long
 #define __PTRDIFF_TYPE__ long long

+ 2 - 0
dtool/src/prc/CMakeLists.txt

@@ -79,6 +79,8 @@ if(HAVE_OPENSSL)
 endif()
 
 set(P3PRC_IGATEEXT
+  configVariable_ext.cxx
+  configVariable_ext.h
   streamReader_ext.cxx
   streamReader_ext.h
   streamWriter_ext.cxx

+ 1 - 1
dtool/src/prc/androidLogStream.cxx

@@ -92,7 +92,7 @@ overflow(int ch) {
  */
 void AndroidLogStream::AndroidLogStreamBuf::
 write_char(char c) {
-  nout.put(c);
+  //nout.put(c);
   if (c == '\n') {
     // Write a line to the log file.
     __android_log_write(_priority, _tag.c_str(), _data.c_str());

+ 2 - 2
dtool/src/prc/androidLogStream.h

@@ -44,10 +44,10 @@ private:
     std::string _data;
   };
 
-  AndroidLogStream(int priority);
-
 public:
+  AndroidLogStream(int priority);
   virtual ~AndroidLogStream();
+
   static std::ostream &out(NotifySeverity severity);
 
   friend class Notify;

+ 2 - 1
dtool/src/prc/configPageManager.cxx

@@ -97,6 +97,7 @@ reload_implicit_pages() {
   }
   _implicit_pages.clear();
 
+#ifndef ANDROID
   // If we are running inside a deployed application, see if it exposes
   // information about how the PRC data should be initialized.
   struct BlobInfo {
@@ -481,6 +482,7 @@ reload_implicit_pages() {
       }
     }
   }
+#endif  // ANDROID
 
   if (!_loaded_implicit) {
     config_initialized();
@@ -520,7 +522,6 @@ reload_implicit_pages() {
     SetErrorMode(SEM_FAILCRITICALERRORS);
   }
 #endif
-
 }
 
 /**

+ 2 - 0
dtool/src/prc/configVariable.h

@@ -44,6 +44,8 @@ PUBLISHED:
 
   INLINE size_t get_num_words() const;
 
+  EXTENSION(PyObject *__reduce__(PyObject *self) const);
+
 protected:
   INLINE const ConfigDeclaration *get_default_value() const;
 

+ 39 - 0
dtool/src/prc/configVariable_ext.cxx

@@ -0,0 +1,39 @@
+/**
+ * PANDA 3D SOFTWAREitiueuiitgyrsrtfu
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file configVariable_ext.cxx
+ * @author rdb
+ * @date 2021-01-01
+ */
+
+#include "configVariable_ext.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * Implements pickle support.
+ */
+PyObject *Extension<ConfigVariable>::
+__reduce__(PyObject *self) const {
+  const std::string &name = _this->get_name();
+  const std::string &descr = _this->get_description();
+  int flags = _this->get_flags();
+
+  // If the subclass defines a get_default_value method, we assume it takes a
+  // default value in the constructor.
+  PyObject *get_default_value = PyObject_GetAttrString((PyObject *)Py_TYPE(self), "get_default_value");
+  if (get_default_value != nullptr) {
+    PyObject *default_value = PyObject_CallOneArg(get_default_value, self);
+    return Py_BuildValue("O(s#Ns#i)", Py_TYPE(self), name.data(), (Py_ssize_t)name.length(), default_value, descr.data(), (Py_ssize_t)descr.length(), flags);
+  }
+  else {
+    return Py_BuildValue("O(s#s#i)", Py_TYPE(self), name.data(), (Py_ssize_t)name.length(), descr.data(), (Py_ssize_t)descr.length(), flags);
+  }
+}
+
+#endif

+ 37 - 0
dtool/src/prc/configVariable_ext.h

@@ -0,0 +1,37 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file configVariable_ext.h
+ * @author rdb
+ * @date 2021-12-10
+ */
+
+#ifndef CONFIGVARIABLE_EXT_H
+#define CONFIGVARIABLE_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "configVariable.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for ConfigVariable, which are called
+ * instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<ConfigVariable> : public ExtensionBase<ConfigVariable> {
+public:
+  PyObject *__reduce__(PyObject *self) const;
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // CONFIGVARIABLE_EXT_H

+ 6 - 0
dtool/src/prc/notify.cxx

@@ -544,6 +544,12 @@ config_initialized() {
         }
 #endif  // BUILD_IPHONE
       }
+#ifdef ANDROID
+    } else {
+      // By default, we always redirect the notify stream to the Android log.
+      Notify *ptr = Notify::ptr();
+      ptr->set_ostream_ptr(new AndroidLogStream(ANDROID_LOG_INFO), true);
+#endif
     }
   }
 #endif

+ 1 - 0
dtool/src/prc/p3prc_ext_composite.cxx

@@ -1,2 +1,3 @@
+#include "configVariable_ext.cxx"
 #include "streamReader_ext.cxx"
 #include "streamWriter_ext.cxx"

+ 1 - 0
makepanda/config.in

@@ -15,6 +15,7 @@
 load-display pandagl
 #load-display pandadx9
 #load-display pandagles
+#load-display pandagles2
 #load-display p3tinydisplay
 
 # These control the placement and size of the default rendering window.

+ 2 - 2
makepanda/installpanda.py

@@ -10,7 +10,7 @@
 
 import os
 import sys
-from distutils.sysconfig import get_python_lib
+import sysconfig
 from optparse import OptionParser
 from makepandacore import *
 
@@ -141,7 +141,7 @@ def GetLibDir():
 
     # If Python is installed into /usr/lib64, it's probably safe
     # to assume that we should install there as well.
-    python_lib = get_python_lib(1)
+    python_lib = sysconfig.get_path("platlib")
     if python_lib.startswith('/usr/lib64/') or \
        python_lib.startswith('/usr/local/lib64/'):
         return "lib64"

+ 23 - 17
makepanda/makepackage.py

@@ -6,6 +6,7 @@ import shutil
 import glob
 import re
 import subprocess
+import sysconfig
 from makepandacore import *
 from installpanda import *
 
@@ -214,7 +215,7 @@ def MakeDebugSymbolArchive(zipname, dirname):
     zip.close()
 
 
-def MakeInstallerLinux(version, debversion=None, rpmrelease=1,
+def MakeInstallerLinux(version, debversion=None, rpmversion=None, rpmrelease=1,
                        python_versions=[], **kwargs):
     outputdir = GetOutputDir()
 
@@ -235,6 +236,8 @@ def MakeInstallerLinux(version, debversion=None, rpmrelease=1,
     major_version = '.'.join(version.split('.')[:2])
     if not debversion:
         debversion = version
+    if not rpmversion:
+        rpmversion = version
 
     # Clean and set up a directory to install Panda3D into
     oscmd("rm -rf targetroot data.tar.gz control.tar.gz panda3d.spec")
@@ -371,13 +374,13 @@ def MakeInstallerLinux(version, debversion=None, rpmrelease=1,
                 txt += "/usr/bin/%s\n" % (base)
 
         # Write out the spec file.
-        txt = txt.replace("VERSION", version)
+        txt = txt.replace("VERSION", rpmversion)
         txt = txt.replace("RPMRELEASE", str(rpmrelease))
         txt = txt.replace("PANDASOURCE", pandasource)
         WriteFile("panda3d.spec", txt)
 
         oscmd("fakeroot rpmbuild --define '_rpmdir "+pandasource+"' --buildroot '"+os.path.abspath("targetroot")+"' -bb panda3d.spec")
-        oscmd("mv "+arch+"/panda3d-"+version+"-"+rpmrelease+"."+arch+".rpm .")
+        oscmd("mv "+arch+"/panda3d-"+rpmversion+"-"+rpmrelease+"."+arch+".rpm .")
         oscmd("rm -rf "+arch, True)
 
     else:
@@ -794,16 +797,8 @@ def MakeInstallerAndroid(version, **kwargs):
     if os.path.exists(apk_unsigned):
         os.unlink(apk_unsigned)
 
-    # Compile the Java classes into a Dalvik executable.
-    dx_cmd = "dx --dex --output=apkroot/classes.dex "
-    if GetOptimize() <= 2:
-        dx_cmd += "--debug "
-    if GetVerbose():
-        dx_cmd += "--verbose "
-    if "ANDROID_API" in SDK:
-        dx_cmd += "--min-sdk-version=%d " % (SDK["ANDROID_API"])
-    dx_cmd += os.path.join(outputdir, "classes")
-    oscmd(dx_cmd)
+    # Copy the compiled Java classes.
+    oscmd("cp %s apkroot/classes.dex" % (os.path.join(outputdir, "classes.dex")))
 
     # Copy the libraries one by one.  In case of library dependencies, strip
     # off any suffix (eg. libfile.so.1.0), as Android does not support them.
@@ -887,8 +882,12 @@ def MakeInstallerAndroid(version, **kwargs):
                 copy_library(source, "libpy.panda3d.{}.so".format(modname))
 
         # Same for standard Python modules.
-        import _ctypes
-        source_dir = os.path.dirname(_ctypes.__file__)
+        if CrossCompiling():
+            source_dir = os.path.join(GetThirdpartyDir(), "python", "lib", SDK["PYTHONVERSION"], "lib-dynload")
+        else:
+            import _ctypes
+            source_dir = os.path.dirname(_ctypes.__file__)
+
         for base in os.listdir(source_dir):
             if not base.endswith('.so'):
                 continue
@@ -914,8 +913,7 @@ def MakeInstallerAndroid(version, **kwargs):
                     shutil.copy(os.path.join(source_dir, base), target)
 
     # Copy the Python standard library to the .apk as well.
-    from distutils.sysconfig import get_python_lib
-    stdlib_source = get_python_lib(False, True)
+    stdlib_source = sysconfig.get_path("stdlib")
     stdlib_target = os.path.join("apkroot", "lib", "python{0}.{1}".format(*sys.version_info))
     copy_python_tree(stdlib_source, stdlib_target)
 
@@ -1028,6 +1026,13 @@ if __name__ == "__main__":
         help='Version number for .deb file',
         default=None,
     )
+    parser.add_option(
+        '',
+        '--rpmversion',
+        dest='rpmversion',
+        help='Version number for .rpm file',
+        default=None,
+    )
     parser.add_option(
         '',
         '--rpmrelease',
@@ -1097,6 +1102,7 @@ if __name__ == "__main__":
         optimize=GetOptimize(),
         compressor=options.compressor,
         debversion=options.debversion,
+        rpmversion=options.rpmversion,
         rpmrelease=options.rpmrelease,
         python_versions=ReadPythonVersionInfoFile(),
         installdir=options.installdir,

+ 108 - 56
makepanda/makepanda.py

@@ -21,6 +21,7 @@ try:
     import threading
     import signal
     import shutil
+    import sysconfig
     import plistlib
     import queue
 except KeyboardInterrupt:
@@ -30,7 +31,10 @@ except:
     print("Please install the development package of Python and try again.")
     exit(1)
 
-from distutils.util import get_platform
+if sys.version_info >= (3, 10):
+    from sysconfig import get_platform
+else:
+    from distutils.util import get_platform
 from makepandacore import *
 
 try:
@@ -63,6 +67,7 @@ DISTRIBUTOR=""
 VERSION=None
 DEBVERSION=None
 WHLVERSION=None
+RPMVERSION=None
 RPMRELEASE="1"
 GIT_COMMIT=None
 MAJOR_VERSION=None
@@ -153,7 +158,7 @@ def usage(problem):
     print("  --everything      (enable every third-party lib)")
     print("  --directx-sdk=X   (specify version of DirectX SDK to use: jun2010, aug2009)")
     print("  --windows-sdk=X   (specify Windows SDK version, eg. 7.1, 8.1 or 10.  Default is 8.1)")
-    print("  --msvc-version=X  (specify Visual C++ version, eg. 10, 11, 12, 14, 14.1, 14.2.  Default is 14)")
+    print("  --msvc-version=X  (specify Visual C++ version, eg. 10, 11, 12, 14, 14.1, 14.2, 14.3.  Default is 14)")
     print("  --use-icl         (experimental setting to use an intel compiler instead of MSVC on Windows)")
     print("")
     print("The simplest way to compile panda is to just type:")
@@ -165,7 +170,7 @@ def usage(problem):
 def parseopts(args):
     global INSTALLER,WHEEL,RUNTESTS,GENMAN,DISTRIBUTOR,VERSION
     global COMPRESSOR,THREADCOUNT,OSX_ARCHS
-    global DEBVERSION,WHLVERSION,RPMRELEASE,GIT_COMMIT
+    global DEBVERSION,WHLVERSION,RPMVERSION,RPMRELEASE,GIT_COMMIT
     global STRDXSDKVERSION, WINDOWS_SDK, MSVC_VERSION, BOOUSEINTELCOMPILER
     global COPY_PYTHON
 
@@ -180,7 +185,7 @@ def parseopts(args):
         "help","distributor=","verbose","tests",
         "optimize=","everything","nothing","installer","wheel","rtdist","nocolor",
         "version=","lzma","no-python","threads=","outputdir=","override=",
-        "static","debversion=","rpmrelease=","p3dsuffix=","rtdist-version=",
+        "static","debversion=","rpmversion=","rpmrelease=","p3dsuffix=","rtdist-version=",
         "directx-sdk=", "windows-sdk=", "msvc-version=", "clean", "use-icl",
         "universal", "target=", "arch=", "git-commit=", "no-copy-python",
         "cggl-incdir=", "cggl-libdir=",
@@ -227,6 +232,7 @@ def parseopts(args):
             elif (option=="--override"): AddOverride(value.strip())
             elif (option=="--static"): SetLinkAllStatic(True)
             elif (option=="--debversion"): DEBVERSION=value
+            elif (option=="--rpmversion"): RPMVERSION=value
             elif (option=="--rpmrelease"): RPMRELEASE=value
             elif (option=="--git-commit"): GIT_COMMIT=value
             # Backward compatibility, OPENGL was renamed to GL
@@ -367,6 +373,9 @@ print("Version: %s" % VERSION)
 if DEBVERSION is None:
     DEBVERSION = VERSION
 
+if RPMVERSION is None:
+    RPMVERSION = VERSION
+
 MAJOR_VERSION = '.'.join(VERSION.split('.')[:2])
 
 # Now determine the distutils-style platform tag for the target system.
@@ -971,6 +980,8 @@ if (COMPILER=="GCC"):
             SmartPkgEnable("CGGL", "", ("CgGL"), "Cg/cgGL.h", thirdparty_dir = "nvidiacg")
         if GetTarget() != "android":
             SmartPkgEnable("X11", "x11", "X11", ("X11", "X11/Xlib.h", "X11/XKBlib.h"))
+        else:
+            PkgDisable("X11")
 
     if GetHost() != "darwin":
         # Workaround for an issue where pkg-config does not include this path
@@ -1034,6 +1045,7 @@ if (COMPILER=="GCC"):
         LibName("ALWAYS", '-llog')
         LibName("ANDROID", '-landroid')
         LibName("JNIGRAPHICS", '-ljnigraphics')
+        LibName("OPENSLES", '-lOpenSLES')
 
     for pkg in MAYAVERSIONS:
         if (PkgSkip(pkg)==0 and (pkg in SDK)):
@@ -1062,7 +1074,7 @@ if (COMPILER=="GCC"):
             LibName(pkg, "-lDependEngine")
             LibName(pkg, "-lCommandEngine")
             LibName(pkg, "-lFoundation")
-            if pkg != "MAYA2020":
+            if pkg not in ("MAYA2020", "MAYA2022"):
                 LibName(pkg, "-lIMFbase")
             if GetTarget() != 'darwin':
                 LibName(pkg, "-lOpenMayalib")
@@ -1332,10 +1344,6 @@ def CompileCxx(obj,src,opts):
         if "SYSROOT" in SDK:
             if GetTarget() != "android":
                 cmd += ' --sysroot=%s' % (SDK["SYSROOT"])
-            else:
-                ndk_dir = SDK["ANDROID_NDK"].replace('\\', '/')
-                cmd += ' -isystem %s/sysroot/usr/include' % (ndk_dir)
-                cmd += ' -isystem %s/sysroot/usr/include/%s' % (ndk_dir, SDK["ANDROID_TRIPLE"])
             cmd += ' -no-canonical-prefixes'
 
         # Android-specific flags.
@@ -1344,46 +1352,35 @@ def CompileCxx(obj,src,opts):
         if GetTarget() == "android":
             # Most of the specific optimization flags here were
             # just copied from the default Android Makefiles.
-            if "ANDROID_API" in SDK:
-                cmd += ' -D__ANDROID_API__=' + str(SDK["ANDROID_API"])
             if "ANDROID_GCC_TOOLCHAIN" in SDK:
                 cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/')
             cmd += ' -ffunction-sections -funwind-tables'
+            cmd += ' -target ' + SDK["ANDROID_TRIPLE"]
             if arch == 'armv7a':
-                cmd += ' -target armv7-none-linux-androideabi'
                 cmd += ' -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16'
-                cmd += ' -fno-integrated-as'
             elif arch == 'arm':
-                cmd += ' -target armv5te-none-linux-androideabi'
                 cmd += ' -march=armv5te -mtune=xscale -msoft-float'
-                cmd += ' -fno-integrated-as'
-            elif arch == 'aarch64':
-                cmd += ' -target aarch64-none-linux-android'
             elif arch == 'mips':
-                cmd += ' -target mipsel-none-linux-android'
                 cmd += ' -mips32'
             elif arch == 'mips64':
-                cmd += ' -target mips64el-none-linux-android'
                 cmd += ' -fintegrated-as'
             elif arch == 'x86':
-                cmd += ' -target i686-none-linux-android'
-                cmd += ' -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32'
+                cmd += ' -march=i686 -mssse3 -mfpmath=sse -m32'
                 cmd += ' -mstackrealign'
             elif arch == 'x86_64':
-                cmd += ' -target x86_64-none-linux-android'
-                cmd += ' -march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel'
+                cmd += ' -march=x86-64 -msse4.2 -mpopcnt -m64'
 
             cmd += " -Wa,--noexecstack"
 
             # Do we want thumb or arm instructions?
-            if arch.startswith('arm'):
+            if arch != 'arm64' and arch.startswith('arm'):
                 if optlevel >= 3:
                     cmd += ' -mthumb'
                 else:
                     cmd += ' -marm'
 
             # Enable SIMD instructions if requested
-            if arch.startswith('arm') and PkgSkip("NEON") == 0:
+            if arch != 'arm64' and arch.startswith('arm') and PkgSkip("NEON") == 0:
                 cmd += ' -mfpu=neon'
 
         elif GetTarget() == 'emscripten':
@@ -1421,11 +1418,14 @@ def CompileCxx(obj,src,opts):
         # Needed by both Python, Panda, Eigen, all of which break aliasing rules.
         cmd += " -fno-strict-aliasing"
 
-        if optlevel >= 3:
-            cmd += " -ffast-math -fno-stack-protector"
-        if optlevel == 3:
-            # Fast math is nice, but we'd like to see NaN in dev builds.
-            cmd += " -fno-finite-math-only"
+        # Certain clang versions crash when passing these math flags while
+        # compiling Objective-C++ code
+        if not src.endswith(".m") and not src.endswith(".mm"):
+            if optlevel >= 3:
+                cmd += " -ffast-math -fno-stack-protector"
+            if optlevel == 3:
+                # Fast math is nice, but we'd like to see NaN in dev builds.
+                cmd += " -fno-finite-math-only"
 
         # Make sure this is off to avoid GCC/Eigen bug (see GitHub #228)
         if GetTarget() != "emscripten":
@@ -1892,22 +1892,11 @@ def CompileLink(dll, obj, opts):
             if "ANDROID_GCC_TOOLCHAIN" in SDK:
                 cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/')
             cmd += " -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now"
+            cmd += ' -target ' + SDK["ANDROID_TRIPLE"]
             if arch == 'armv7a':
-                cmd += ' -target armv7-none-linux-androideabi'
                 cmd += " -march=armv7-a -Wl,--fix-cortex-a8"
-            elif arch == 'arm':
-                cmd += ' -target armv5te-none-linux-androideabi'
-            elif arch == 'aarch64':
-                cmd += ' -target aarch64-none-linux-android'
             elif arch == 'mips':
-                cmd += ' -target mipsel-none-linux-android'
                 cmd += ' -mips32'
-            elif arch == 'mips64':
-                cmd += ' -target mips64el-none-linux-android'
-            elif arch == 'x86':
-                cmd += ' -target i686-none-linux-android'
-            elif arch == 'x86_64':
-                cmd += ' -target x86_64-none-linux-android'
             cmd += ' -lc -lm'
 
         elif GetTarget() == 'emscripten':
@@ -1918,9 +1907,8 @@ def CompileLink(dll, obj, opts):
 
         else:
             cmd += " -pthread"
-
-        if "SYSROOT" in SDK:
-            cmd += " --sysroot=%s -no-canonical-prefixes" % (SDK["SYSROOT"])
+            if "SYSROOT" in SDK:
+                cmd += " --sysroot=%s -no-canonical-prefixes" % (SDK["SYSROOT"])
 
         if LDFLAGS != "":
             cmd += " " + LDFLAGS
@@ -2194,6 +2182,31 @@ def CompileMIDL(target, src, opts):
 
         oscmd(cmd)
 
+##########################################################################################
+#
+# CompileDalvik
+#
+##########################################################################################
+
+def CompileDalvik(target, inputs, opts):
+    cmd = "d8 --output " + os.path.dirname(target)
+
+    if GetOptimize() <= 2:
+        cmd += " --debug"
+    else:
+        cmd += " --release"
+
+    if "ANDROID_API" in SDK:
+        cmd += " --min-api %d" % (SDK["ANDROID_API"])
+
+    if "ANDROID_JAR" in SDK:
+        cmd += " --lib %s" % (SDK["ANDROID_JAR"])
+
+    for i in inputs:
+        cmd += " " + BracketNameWithQuotes(i)
+
+    oscmd(cmd)
+
 ##########################################################################################
 #
 # CompileAnything
@@ -2305,6 +2318,9 @@ def CompileAnything(target, inputs, opts, progress = None):
         elif infile.endswith(".r"):
             ProgressOutput(progress, "Building resource object", target)
             return CompileRsrc(target, infile, opts)
+    elif origsuffix == ".dex":
+        ProgressOutput(progress, "Building Dalvik object", target)
+        return CompileDalvik(target, inputs, opts)
     exit("Don't know how to compile: %s from %s" % (target, inputs))
 
 ##########################################################################################
@@ -2934,7 +2950,12 @@ if PkgSkip("GL") or GetLinkAllStatic():
     configprc = configprc.replace("\nload-display pandagl", "\n#load-display pandagl")
 
 if PkgSkip("GLES") or GetLinkAllStatic():
-    configprc = configprc.replace("\n#load-display pandagles", "")
+    configprc = configprc.replace("\n#load-display pandagles\n", "\n")
+
+if PkgSkip("GL") and not PkgSkip("GLES2") and not GetLinkAllStatic():
+    configprc = configprc.replace("\n#load-display pandagles2", "\nload-display pandagles2")
+elif PkgSkip("GLES2") or GetLinkAllStatic():
+    configprc = configprc.replace("\n#load-display pandagles2", "")
 
 if PkgSkip("DX9") or GetLinkAllStatic():
     configprc = configprc.replace("\n#load-display pandadx9", "")
@@ -3152,8 +3173,10 @@ else:
 if not PkgSkip("PANDATOOL"):
     CopyAllFiles(GetOutputDir()+"/plugins/",  "pandatool/src/scripts/", ".mel")
     CopyAllFiles(GetOutputDir()+"/plugins/",  "pandatool/src/scripts/", ".ms")
-if not PkgSkip("PYTHON") and os.path.isdir(GetThirdpartyBase()+"/Pmw"):
-    CopyTree(GetOutputDir()+'/Pmw',         GetThirdpartyBase()+'/Pmw')
+
+if not PkgSkip("PYTHON") and os.path.isdir(GetThirdpartyBase() + "/Pmw"):
+    CopyTree(GetOutputDir() + "/Pmw", GetThirdpartyBase() + "/Pmw", exclude=["Pmw_1_3", "Pmw_1_3_3"])
+
 ConditionalWriteFile(GetOutputDir()+'/include/ctl3d.h', '/* dummy file to make MAX happy */')
 
 # Since Eigen is included by all sorts of core headers, as a convenience
@@ -4434,7 +4457,7 @@ if PkgSkip("OPENAL") == 0:
     TargetAdd('openal_audio_openal_audio_composite1.obj', opts=OPTS, input='openal_audio_composite1.cxx')
     TargetAdd('libp3openal_audio.dll', input='openal_audio_openal_audio_composite1.obj')
     TargetAdd('libp3openal_audio.dll', input=COMMON_PANDA_LIBS)
-    TargetAdd('libp3openal_audio.dll', opts=['MODULE', 'ADVAPI', 'WINUSER', 'WINMM', 'WINSHELL', 'WINOLE', 'OPENAL'])
+    TargetAdd('libp3openal_audio.dll', opts=['MODULE', 'ADVAPI', 'WINUSER', 'WINMM', 'WINSHELL', 'WINOLE', 'OPENAL', 'OPENSLES'])
 
 #
 # DIRECTORY: panda/src/downloadertools/
@@ -4719,7 +4742,7 @@ elif not PkgSkip("EGL") and not PkgSkip("GL") and GetTarget() not in ('windows',
 # DIRECTORY: panda/src/egldisplay/
 #
 
-if not PkgSkip("EGL") and not PkgSkip("GLES"):
+if GetTarget() != 'android' and not PkgSkip("EGL") and not PkgSkip("GLES"):
     DefSymbol('GLES', 'OPENGLES_1', '')
     OPTS=['DIR:panda/src/egldisplay', 'DIR:panda/src/glstuff', 'BUILDING:PANDAGLES', 'GLES', 'EGL', 'X11']
     TargetAdd('pandagles_egldisplay_composite1.obj', opts=OPTS, input='p3egldisplay_composite1.cxx')
@@ -4738,7 +4761,7 @@ if not PkgSkip("EGL") and not PkgSkip("GLES"):
 # DIRECTORY: panda/src/egldisplay/
 #
 
-if not PkgSkip("EGL") and not PkgSkip("GLES2"):
+if GetTarget() != 'android' and not PkgSkip("EGL") and not PkgSkip("GLES2"):
     DefSymbol('GLES2', 'OPENGLES_2', '')
     OPTS=['DIR:panda/src/egldisplay', 'DIR:panda/src/glstuff', 'BUILDING:PANDAGLES2', 'GLES2', 'EGL', 'X11']
     TargetAdd('pandagles2_egldisplay_composite1.obj', opts=OPTS, input='p3egldisplay_composite1.cxx')
@@ -4960,12 +4983,17 @@ if not PkgSkip("PVIEW"):
 #
 
 if GetTarget() == 'android':
-    OPTS=['DIR:panda/src/android']
+    OPTS=['DIR:panda/src/android', 'PNG']
     TargetAdd('org/panda3d/android/NativeIStream.class', opts=OPTS, input='NativeIStream.java')
     TargetAdd('org/panda3d/android/NativeOStream.class', opts=OPTS, input='NativeOStream.java')
     TargetAdd('org/panda3d/android/PandaActivity.class', opts=OPTS, input='PandaActivity.java')
     TargetAdd('org/panda3d/android/PythonActivity.class', opts=OPTS, input='PythonActivity.java')
 
+    TargetAdd('classes.dex', input='org/panda3d/android/NativeIStream.class')
+    TargetAdd('classes.dex', input='org/panda3d/android/NativeOStream.class')
+    TargetAdd('classes.dex', input='org/panda3d/android/PandaActivity.class')
+    TargetAdd('classes.dex', input='org/panda3d/android/PythonActivity.class')
+
     TargetAdd('p3android_composite1.obj', opts=OPTS, input='p3android_composite1.cxx')
     TargetAdd('libp3android.dll', input='p3android_composite1.obj')
     TargetAdd('libp3android.dll', input=COMMON_PANDA_LIBS)
@@ -5015,6 +5043,20 @@ if GetTarget() == 'android' and not PkgSkip("EGL") and not PkgSkip("GLES"):
     TargetAdd('libpandagles.dll', input=COMMON_PANDA_LIBS)
     TargetAdd('libpandagles.dll', opts=['MODULE', 'GLES', 'EGL'])
 
+if GetTarget() == 'android' and not PkgSkip("EGL") and not PkgSkip("GLES2"):
+    DefSymbol('GLES2', 'OPENGLES_2', '')
+    OPTS=['DIR:panda/src/androiddisplay', 'DIR:panda/src/glstuff', 'BUILDING:PANDAGLES2', 'GLES2', 'EGL']
+    TargetAdd('pandagles2_androiddisplay_composite1.obj', opts=OPTS, input='p3androiddisplay_composite1.cxx')
+    OPTS=['DIR:panda/metalibs/pandagles2', 'BUILDING:PANDAGLES2', 'GLES2', 'EGL']
+    TargetAdd('pandagles2_pandagles2.obj', opts=OPTS, input='pandagles2.cxx')
+    TargetAdd('libpandagles2.dll', input='pandagles2_pandagles2.obj')
+    TargetAdd('libpandagles2.dll', input='p3gles2gsg_config_gles2gsg.obj')
+    TargetAdd('libpandagles2.dll', input='p3gles2gsg_gles2gsg.obj')
+    TargetAdd('libpandagles2.dll', input='pandagles2_androiddisplay_composite1.obj')
+    TargetAdd('libpandagles2.dll', input='libp3android.dll')
+    TargetAdd('libpandagles2.dll', input=COMMON_PANDA_LIBS)
+    TargetAdd('libpandagles2.dll', opts=['MODULE', 'GLES2', 'EGL'])
+
 #
 # DIRECTORY: panda/src/tinydisplay/
 #
@@ -6115,10 +6157,11 @@ if PkgSkip("PYTHON") == 0:
         LibName('DEPLOYSTUB', "-Wl,-rpath,\\$ORIGIN")
         LibName('DEPLOYSTUB', "-Wl,-z,origin")
         LibName('DEPLOYSTUB', "-rdynamic")
+
     PyTargetAdd('deploy-stub.exe', input='deploy-stub.obj')
     if GetTarget() == 'windows':
         PyTargetAdd('deploy-stub.exe', input='frozen_dllmain.obj')
-    PyTargetAdd('deploy-stub.exe', opts=['WINSHELL', 'DEPLOYSTUB', 'NOICON'])
+    PyTargetAdd('deploy-stub.exe', opts=['WINSHELL', 'DEPLOYSTUB', 'NOICON', 'ANDROID'])
 
     if GetTarget() == 'emscripten':
         PyTargetAdd('deploy-stub.exe', opts=['ZLIB'])
@@ -6133,6 +6176,15 @@ if PkgSkip("PYTHON") == 0:
         PyTargetAdd('deploy-stubw.obj', opts=OPTS, input='deploy-stub.c')
         PyTargetAdd('deploy-stubw.exe', input='deploy-stubw.obj')
         PyTargetAdd('deploy-stubw.exe', opts=['MACOS_APP_BUNDLE', 'DEPLOYSTUB', 'NOICON'])
+    elif GetTarget() == 'android':
+        PyTargetAdd('deploy-stubw_android_main.obj', opts=OPTS, input='android_main.cxx')
+        PyTargetAdd('deploy-stubw_android_log.obj', opts=OPTS, input='android_log.c')
+        PyTargetAdd('libdeploy-stubw.dll', input='android_native_app_glue.obj')
+        PyTargetAdd('libdeploy-stubw.dll', input='deploy-stubw_android_main.obj')
+        PyTargetAdd('libdeploy-stubw.dll', input='deploy-stubw_android_log.obj')
+        PyTargetAdd('libdeploy-stubw.dll', input=COMMON_PANDA_LIBS)
+        PyTargetAdd('libdeploy-stubw.dll', input='libp3android.dll')
+        PyTargetAdd('libdeploy-stubw.dll', opts=['DEPLOYSTUB', 'ANDROID'])
 
 #
 # Generate the models directory and samples directory
@@ -6233,7 +6285,7 @@ def ParallelMake(tasklist):
     # Create the workers
     for slave in range(THREADCOUNT):
         th = threading.Thread(target=BuildWorker, args=[taskqueue, donequeue])
-        th.setDaemon(1)
+        th.daemon = True
         th.start()
     # Feed tasks to the workers.
     tasksqueued = 0
@@ -6324,8 +6376,8 @@ if INSTALLER:
 
     MakeInstaller(version=VERSION, outputdir=GetOutputDir(),
                   optimize=GetOptimize(), compressor=COMPRESSOR,
-                  debversion=DEBVERSION, rpmrelease=RPMRELEASE,
-                  python_versions=python_versions)
+                  debversion=DEBVERSION, rpmversion=RPMVERSION,
+                  rpmrelease=RPMRELEASE, python_versions=python_versions)
 
 if WHEEL:
     ProgressOutput(100.0, "Building wheel")

+ 77 - 60
makepanda/makepandacore.py

@@ -6,7 +6,6 @@
 ########################################################################
 
 import configparser
-from distutils import sysconfig
 import fnmatch
 import getpass
 import glob
@@ -18,6 +17,7 @@ import shutil
 import signal
 import subprocess
 import sys
+import sysconfig
 import threading
 import _thread as thread
 import time
@@ -28,7 +28,7 @@ SUFFIX_LIB = [".lib",".ilb"]
 VCS_DIRS = set(["CVS", "CVSROOT", ".git", ".hg", "__pycache__"])
 VCS_FILES = set([".cvsignore", ".gitignore", ".gitmodules", ".hgignore"])
 STARTTIME = time.time()
-MAINTHREAD = threading.currentThread()
+MAINTHREAD = threading.current_thread()
 OUTPUTDIR = "built"
 CUSTOM_OUTPUTDIR = False
 THIRDPARTYBASE = None
@@ -84,6 +84,7 @@ MSVCVERSIONINFO = {
     (14,0): {"vsversion":(14,0), "vsname":"Visual Studio 2015"},
     (14,1): {"vsversion":(15,0), "vsname":"Visual Studio 2017"},
     (14,2): {"vsversion":(16,0), "vsname":"Visual Studio 2019"},
+    (14,3): {"vsversion":(17,0), "vsname":"Visual Studio 2022"},
 }
 
 ########################################################################
@@ -112,6 +113,7 @@ MAYAVERSIONINFO = [("MAYA6",   "6.0"),
                    ("MAYA2018","2018"),
                    ("MAYA2019","2019"),
                    ("MAYA2020","2020"),
+                   ("MAYA2022","2022"),
 ]
 
 MAXVERSIONINFO = [("MAX6", "SOFTWARE\\Autodesk\\3DSMAX\\6.0", "installdir", "maxsdk\\cssdk\\include"),
@@ -246,7 +248,7 @@ def ProgressOutput(progress, msg, target = None):
     sys.stdout.flush()
     sys.stderr.flush()
     prefix = ""
-    thisthread = threading.currentThread()
+    thisthread = threading.current_thread()
     if thisthread is MAINTHREAD:
         if progress is None:
             prefix = ""
@@ -276,7 +278,7 @@ def ProgressOutput(progress, msg, target = None):
 def exit(msg = ""):
     sys.stdout.flush()
     sys.stderr.flush()
-    if threading.currentThread() == MAINTHREAD:
+    if threading.current_thread() == MAINTHREAD:
         SaveDependencyCache()
         print("Elapsed Time: " + PrettyTime(time.time() - STARTTIME))
         print(msg)
@@ -400,30 +402,30 @@ def SetTarget(target, arch=None):
             else:
                 arch = 'armv7a'
 
-        if arch == 'arm64':
-            arch = 'aarch64'
+        if arch == 'aarch64':
+            arch = 'arm64'
 
         # Did we specify an API level?
         global ANDROID_API
         target, _, api = target.partition('-')
         if api:
             ANDROID_API = int(api)
-        elif arch in ('mips64', 'aarch64', 'x86_64'):
+        elif arch in ('mips64', 'arm64', 'x86_64'):
             # 64-bit platforms were introduced in Android 21.
             ANDROID_API = 21
         else:
-            # Default to the lowest API level supported by NDK r16.
-            ANDROID_API = 14
+            # Default to the lowest API level still supported by Google.
+            ANDROID_API = 19
 
         # Determine the prefix for our gcc tools, eg. arm-linux-androideabi-gcc
         global ANDROID_ABI, ANDROID_TRIPLE
         if arch == 'armv7a':
             ANDROID_ABI = 'armeabi-v7a'
-            ANDROID_TRIPLE = 'arm-linux-androideabi'
+            ANDROID_TRIPLE = 'armv7a-linux-androideabi'
         elif arch == 'arm':
             ANDROID_ABI = 'armeabi'
             ANDROID_TRIPLE = 'arm-linux-androideabi'
-        elif arch == 'aarch64':
+        elif arch == 'arm64':
             ANDROID_ABI = 'arm64-v8a'
             ANDROID_TRIPLE = 'aarch64-linux-android'
         elif arch == 'mips':
@@ -439,8 +441,9 @@ def SetTarget(target, arch=None):
             ANDROID_ABI = 'x86_64'
             ANDROID_TRIPLE = 'x86_64-linux-android'
         else:
-            exit('Android architecture must be arm, armv7a, aarch64, mips, mips64, x86 or x86_64')
+            exit('Android architecture must be arm, armv7a, arm64, mips, mips64, x86 or x86_64')
 
+        ANDROID_TRIPLE += str(ANDROID_API)
         TOOLCHAIN_PREFIX = ANDROID_TRIPLE + '-'
         DEFAULT_CC = "clang"
         DEFAULT_CXX = "clang++"
@@ -513,7 +516,7 @@ def GetCXX():
 def GetStrip():
     # Hack
     if TARGET == 'android':
-        return TOOLCHAIN_PREFIX + 'strip'
+        return 'llvm-strip'
     else:
         return 'strip'
 
@@ -652,6 +655,7 @@ def oscmd(cmd, ignoreError = False, cwd=None):
             os.chdir(pwd)
     else:
         cmd = cmd.replace(';', '\\;')
+        cmd = cmd.replace('$', '\\$')
         res = subprocess.call(cmd, cwd=cwd, shell=True)
         sig = res & 0x7F
         if (GetVerbose() and res != 0):
@@ -1377,7 +1381,7 @@ def GetThirdpartyDir():
             THIRDPARTYDIR = base + "/freebsd-libs-a/"
 
     elif (target == 'android'):
-        THIRDPARTYDIR = GetThirdpartyBase()+"/android-libs-%s/" % (GetTargetArch())
+        THIRDPARTYDIR = base + "/android-libs-%s/" % (target_arch)
 
     elif (target == 'emscripten'):
         THIRDPARTYDIR = base + "/emscripten-libs/"
@@ -1575,8 +1579,8 @@ def PkgConfigGetIncDirs(pkgname, tool = "pkg-config"):
     else:
         handle = os.popen(LocateBinary(tool) + " --cflags")
     result = handle.read().strip()
-    if len(result) == 0: return []
     handle.close()
+    if len(result) == 0: return []
     dirs = []
     for opt in result.split(" "):
         if (opt.startswith("-I")):
@@ -2202,12 +2206,12 @@ def SdkLocatePython(prefer_thirdparty_python=False):
         LibDirectory("PYTHON", py_fwx + "/lib")
 
     #elif GetTarget() == 'windows':
-    #    SDK["PYTHON"] = os.path.dirname(sysconfig.get_python_inc())
+    #    SDK["PYTHON"] = os.path.dirname(sysconfig.get_path("include"))
     #    SDK["PYTHONVERSION"] = "python" + sysconfig.get_python_version()
     #    SDK["PYTHONEXEC"] = sys.executable
 
     else:
-        SDK["PYTHON"] = sysconfig.get_python_inc()
+        SDK["PYTHON"] = sysconfig.get_path("include")
         SDK["PYTHONVERSION"] = "python" + sysconfig.get_python_version() + abiflags
         SDK["PYTHONEXEC"] = os.path.realpath(sys.executable)
 
@@ -2419,7 +2423,7 @@ def SdkLocateMacOSX(archs = []):
         # Prefer pre-10.14 for now so that we can keep building FMOD.
         sdk_versions += ["10.13", "10.12", "10.11", "10.10", "10.9"]
 
-    sdk_versions += ["11.1", "11.0"]
+    sdk_versions += ["11.3", "11.1", "11.0"]
 
     if 'arm64' not in archs:
         sdk_versions += ["10.15", "10.14"]
@@ -2559,19 +2563,15 @@ def SdkLocateAndroid():
     SDK["SYSROOT"] = os.path.join(ndk_root, 'platforms', 'android-%s' % (api), arch_dir).replace('\\', '/')
     #IncDirectory("ALWAYS", os.path.join(SDK["SYSROOT"], 'usr', 'include'))
 
-    # Starting with NDK r16, libc++ is the recommended STL to use.
+    # We need to redistribute the C++ standard library.
     stdlibc = os.path.join(ndk_root, 'sources', 'cxx-stl', 'llvm-libc++')
-    IncDirectory("ALWAYS", os.path.join(stdlibc, 'include').replace('\\', '/'))
-    LibDirectory("ALWAYS", os.path.join(stdlibc, 'libs', abi).replace('\\', '/'))
-
     stl_lib = os.path.join(stdlibc, 'libs', abi, 'libc++_shared.so')
-    LibName("ALWAYS", stl_lib.replace('\\', '/'))
     CopyFile(os.path.join(GetOutputDir(), 'lib', 'libc++_shared.so'), stl_lib)
 
     # The Android support library polyfills C++ features not available in the
     # STL that ships with Android.
-    support = os.path.join(ndk_root, 'sources', 'android', 'support', 'include')
-    IncDirectory("ALWAYS", support.replace('\\', '/'))
+    #support = os.path.join(ndk_root, 'sources', 'android', 'support', 'include')
+    #IncDirectory("ALWAYS", support.replace('\\', '/'))
     if api < 21:
         LibName("ALWAYS", "-landroid_support")
 
@@ -2788,7 +2788,7 @@ def SetupVisualStudioEnviron():
         elif not win_kit.endswith('\\'):
             win_kit += '\\'
 
-        for vnum in 10150, 10240, 10586, 14393, 15063, 16299, 17134, 17763, 18362:
+        for vnum in 10150, 10240, 10586, 14393, 15063, 16299, 17134, 17763, 18362, 19041:
             version = "10.0.{0}.0".format(vnum)
             if os.path.isfile(win_kit + "Include\\" + version + "\\ucrt\\assert.h"):
                 print("Using Universal CRT %s" % (version))
@@ -2947,8 +2947,10 @@ def SetupBuildEnvironment(compiler):
         if SDK.get("MACOSX"):
             # The default compiler in Leopard does not respect --sysroot correctly.
             sysroot_flag = " -isysroot " + SDK["MACOSX"]
-        if SDK.get("SYSROOT"):
-            sysroot_flag = ' --sysroot=%s -no-canonical-prefixes' % (SDK["SYSROOT"])
+        #if SDK.get("SYSROOT"):
+        #    sysroot_flag = ' --sysroot=%s -no-canonical-prefixes' % (SDK["SYSROOT"])
+        if GetTarget() == "android":
+            sysroot_flag = " -target " + ANDROID_TRIPLE
 
         # Extract the dirs from the line that starts with 'libraries: ='.
         # The -E is mostly to keep emscripten happy by preventing it from
@@ -2986,12 +2988,7 @@ def SetupBuildEnvironment(compiler):
 
         # Now extract the preprocessor's include directories.
         cmd = GetCXX() + " -x c++ -v -E " + os.devnull
-        if "ANDROID_NDK" in SDK:
-            ndk_dir = SDK["ANDROID_NDK"].replace('\\', '/')
-            cmd += ' -isystem %s/sysroot/usr/include' % (ndk_dir)
-            cmd += ' -isystem %s/sysroot/usr/include/%s' % (ndk_dir, SDK["ANDROID_TRIPLE"])
-        else:
-            cmd += sysroot_flag
+        cmd += sysroot_flag
 
         null = open(os.devnull, 'w')
         handle = subprocess.Popen(cmd, stdout=null, stderr=subprocess.PIPE, shell=True)
@@ -3133,13 +3130,16 @@ def CopyAllHeaders(dir, skip=[]):
             WriteBinaryFile(dstfile, ReadBinaryFile(srcfile))
             JustBuilt([dstfile], [srcfile])
 
-def CopyTree(dstdir, srcdir, omitVCS=True):
+def CopyTree(dstdir, srcdir, omitVCS=True, exclude=()):
     if os.path.isdir(dstdir):
         source_entries = os.listdir(srcdir)
         for entry in source_entries:
             srcpth = os.path.join(srcdir, entry)
             dstpth = os.path.join(dstdir, entry)
 
+            if entry in exclude:
+                continue
+
             if os.path.islink(srcpth) or os.path.isfile(srcpth):
                 if not omitVCS or entry not in VCS_FILES:
                     CopyFile(dstpth, srcpth)
@@ -3149,7 +3149,7 @@ def CopyTree(dstdir, srcdir, omitVCS=True):
 
         # Delete files in dstdir that are not in srcdir.
         for entry in os.listdir(dstdir):
-            if entry not in source_entries:
+            if entry not in source_entries or entry in exclude:
                 path = os.path.join(dstdir, entry)
                 if os.path.islink(path) or os.path.isfile(path):
                     os.remove(path)
@@ -3165,6 +3165,13 @@ def CopyTree(dstdir, srcdir, omitVCS=True):
             if subprocess.call(['cp', '-R', '-f', srcdir, dstdir]) != 0:
                 exit("Copy failed.")
 
+        for entry in exclude:
+            path = os.path.join(dstdir, entry)
+            if os.path.islink(path) or os.path.isfile(path):
+                os.remove(path)
+            elif os.path.isdir(path):
+                shutil.rmtree(path)
+
         if omitVCS:
             DeleteVCS(dstdir)
 
@@ -3295,6 +3302,22 @@ def WriteResourceFile(basename, **kwargs):
     return basename
 
 
+def GenerateEmbeddedStringFile(string_name, data):
+    yield 'extern const char %s[] = {\n' % (string_name)
+    i = 0
+    for byte in data:
+        if i == 0:
+            yield ' '
+
+        yield ' 0x%02x,' % (byte)
+        i += 1
+        if i >= 12:
+            yield '\n'
+            i = 0
+
+    yield '\n};\n'
+
+
 def WriteEmbeddedStringFile(basename, inputs, string_name=None):
     if os.path.splitext(basename)[1] not in SUFFIX_INC:
         basename += '.cxx'
@@ -3319,20 +3342,7 @@ def WriteEmbeddedStringFile(basename, inputs, string_name=None):
 
     data.append(0)
 
-    output = 'extern const char %s[] = {\n' % (string_name)
-
-    i = 0
-    for byte in data:
-        if i == 0:
-            output += ' '
-
-        output += ' 0x%02x,' % (byte)
-        i += 1
-        if i >= 12:
-            output += '\n'
-            i = 0
-
-    output += '\n};\n'
+    output = ''.join(GenerateEmbeddedStringFile(string_name, data))
     ConditionalWriteFile(target, output)
     return target
 
@@ -3356,13 +3366,17 @@ def GetExtensionSuffix():
     if GetTarget() == 'emscripten':
         return '.so'
 
-    import _imp
-    return _imp.extension_suffixes()[0]
+    if CrossCompiling():
+        return '.{0}.so'.format(GetPythonABI())
+    else:
+        import _imp
+        return _imp.extension_suffixes()[0]
 
 def GetPythonABI():
-    soabi = sysconfig.get_config_var('SOABI')
-    if soabi:
-        return soabi
+    if not CrossCompiling():
+        soabi = sysconfig.get_config_var('SOABI')
+        if soabi:
+            return soabi
 
     soabi = 'cpython-%d%d' % (sys.version_info[:2])
 
@@ -3390,6 +3404,7 @@ def CalcLocation(fn, ipath):
     if (GetOptimize() <= 2 and target == 'windows'): dllext = "_d"
 
     if (fn == "AndroidManifest.xml"): return OUTPUTDIR+"/"+fn
+    if (fn == "classes.dex"): return OUTPUTDIR+"/"+fn
     if (fn.endswith(".cxx")): return CxxFindSource(fn, ipath)
     if (fn.endswith(".I")):   return CxxFindSource(fn, ipath)
     if (fn.endswith(".h")):   return CxxFindSource(fn, ipath)
@@ -3497,14 +3512,13 @@ def GetCurrentPythonVersionInfo():
     if PkgSkip("PYTHON"):
         return
 
-    from distutils.sysconfig import get_python_lib
     return {
         "version": SDK["PYTHONVERSION"][6:].rstrip('dmu'),
         "soabi": GetPythonABI(),
         "ext_suffix": GetExtensionSuffix(),
         "executable": sys.executable,
-        "purelib": get_python_lib(False),
-        "platlib": get_python_lib(True),
+        "purelib": sysconfig.get_path("purelib"),
+        "platlib": sysconfig.get_path("platlib"),
     }
 
 
@@ -3515,7 +3529,8 @@ def UpdatePythonVersionInfoFile(new_info):
     json_data = []
     if os.path.isfile(json_file) and not PkgSkip("PYTHON"):
         try:
-            json_data = json.load(open(json_file, 'r'))
+            with open(json_file, 'r') as fh:
+                json_data = json.load(fh)
         except:
             json_data = []
 
@@ -3535,7 +3550,9 @@ def UpdatePythonVersionInfoFile(new_info):
 
     if VERBOSE:
         print("Writing %s" % (json_file))
-    json.dump(json_data, open(json_file, 'w'), indent=4)
+
+    with open(json_file, 'w') as fh:
+        json.dump(json_data, fh, indent=4)
 
 
 def ReadPythonVersionInfoFile():

+ 129 - 27
makepanda/makewheel.py

@@ -10,11 +10,11 @@ import hashlib
 import tempfile
 import subprocess
 import time
-from distutils.util import get_platform
-from distutils.sysconfig import get_config_var
+import struct
+from sysconfig import get_platform, get_config_var
 from optparse import OptionParser
 from base64 import urlsafe_b64encode
-from makepandacore import LocateBinary, GetExtensionSuffix, SetVerbose, GetVerbose, GetMetadataValue
+from makepandacore import LocateBinary, GetExtensionSuffix, SetVerbose, GetVerbose, GetMetadataValue, CrossCompiling, GetThirdpartyDir, SDK, GetStrip
 
 
 def get_abi_tag():
@@ -65,8 +65,11 @@ def is_fat_file(path):
 
 
 def get_python_ext_module_dir():
-    import _ctypes
-    return os.path.dirname(_ctypes.__file__)
+    if CrossCompiling():
+        return os.path.join(GetThirdpartyDir(), "python", "lib", SDK["PYTHONVERSION"], "lib-dynload")
+    else:
+        import _ctypes
+        return os.path.dirname(_ctypes.__file__)
 
 
 if sys.platform in ('win32', 'cygwin'):
@@ -251,16 +254,72 @@ def parse_dependencies_unix(data):
     return filenames
 
 
+def _scan_dependencies_elf(elf):
+    deps = []
+    ident = elf.read(12)
+
+    # Make sure we read in the correct endianness and integer size
+    byte_order = "<>"[ord(ident[1:2]) - 1]
+    elf_class = ord(ident[0:1]) - 1 # 0 = 32-bits, 1 = 64-bits
+    header_struct = byte_order + ("HHIIIIIHHHHHH", "HHIQQQIHHHHHH")[elf_class]
+    section_struct = byte_order + ("4xI8xIII8xI", "4xI16xQQI12xQ")[elf_class]
+    dynamic_struct = byte_order + ("iI", "qQ")[elf_class]
+
+    type, machine, version, entry, phoff, shoff, flags, ehsize, phentsize, phnum, shentsize, shnum, shstrndx \
+      = struct.unpack(header_struct, elf.read(struct.calcsize(header_struct)))
+    dynamic_sections = []
+    string_tables = {}
+
+    # Seek to the section header table and find the .dynamic section.
+    elf.seek(shoff)
+    for i in range(shnum):
+        type, offset, size, link, entsize = struct.unpack_from(section_struct, elf.read(shentsize))
+        if type == 6 and link != 0: # DYNAMIC type, links to string table
+            dynamic_sections.append((offset, size, link, entsize))
+            string_tables[link] = None
+
+    # Read the relevant string tables.
+    for idx in string_tables.keys():
+        elf.seek(shoff + idx * shentsize)
+        type, offset, size, link, entsize = struct.unpack_from(section_struct, elf.read(shentsize))
+        if type != 3: continue
+        elf.seek(offset)
+        string_tables[idx] = elf.read(size)
+
+    # Loop through the dynamic sections to get the NEEDED entries.
+    needed = []
+    for offset, size, link, entsize in dynamic_sections:
+        elf.seek(offset)
+        data = elf.read(entsize)
+        tag, val = struct.unpack_from(dynamic_struct, data)
+
+        # Read tags until we find a NULL tag.
+        while tag != 0:
+            if tag == 1: # A NEEDED entry.  Read it from the string table.
+                string = string_tables[link][val : string_tables[link].find(b'\0', val)]
+                needed.append(string.decode('utf-8'))
+
+            data = elf.read(entsize)
+            tag, val = struct.unpack_from(dynamic_struct, data)
+
+    elf.close()
+    return needed
+
+
 def scan_dependencies(pathname):
     """ Checks the named file for DLL dependencies, and adds any appropriate
     dependencies found into pluginDependencies and dependentFiles. """
 
+    with open(pathname, 'rb') as fh:
+        if fh.read(4) == b'\x7FELF':
+            return _scan_dependencies_elf(fh)
+
     if sys.platform == "darwin":
         command = ['otool', '-XL', pathname]
     elif sys.platform in ("win32", "cygwin"):
         command = ['dumpbin', '/dependents', pathname]
     else:
-        command = ['ldd', pathname]
+        sys.exit("Don't know how to determine dependencies from %s" % (pathname))
 
     process = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True)
     output, unused_err = process.communicate()
@@ -322,18 +381,24 @@ class WheelFile(object):
 
         self.dep_paths[dep] = None
 
-        if dep in self.ignore_deps or dep.lower().startswith("python") or os.path.basename(dep).startswith("libpython"):
-            # Don't include the Python library, or any other explicit ignore.
+        if dep in self.ignore_deps:
             if GetVerbose():
                 print("Ignoring {0} (explicitly ignored)".format(dep))
             return
 
-        if sys.platform == "darwin" and dep.endswith(".so"):
-            # Temporary hack for 1.9, which had link deps on modules.
-            return
+        if not self.platform.startswith("android"):
+            if dep.lower().startswith("python") or os.path.basename(dep).startswith("libpython"):
+                if GetVerbose():
+                    print("Ignoring {0} (explicitly ignored)".format(dep))
+                return
 
-        if sys.platform == "darwin" and dep.startswith("/System/"):
-            return
+        if self.platform.startswith("macosx"):
+            if dep.endswith(".so"):
+                # Temporary hack for 1.9, which had link deps on modules.
+                return
+
+            if dep.startswith("/System/"):
+                return
 
         if dep.startswith('/'):
             source_path = dep
@@ -386,7 +451,7 @@ class WheelFile(object):
             temp = tempfile.NamedTemporaryFile(suffix=suffix, prefix='whl', delete=False)
 
             # On macOS, if no fat wheel was requested, extract the right architecture.
-            if sys.platform == "darwin" and is_fat_file(source_path) \
+            if self.platform.startswith("macosx") and is_fat_file(source_path) \
                 and not self.platform.endswith("_intel") \
                 and "_fat" not in self.platform \
                 and "_universal" not in self.platform:
@@ -404,7 +469,7 @@ class WheelFile(object):
             os.chmod(temp.name, os.stat(temp.name).st_mode | 0o711)
 
             # Now add dependencies.  On macOS, fix @loader_path references.
-            if sys.platform == "darwin":
+            if self.platform.startswith("macosx"):
                 if source_path.endswith('deploy-stubw'):
                     deps_path = '@executable_path/../Frameworks'
                 else:
@@ -457,12 +522,32 @@ class WheelFile(object):
                 # On other unixes, we just add dependencies normally.
                 for dep in deps:
                     # Only include dependencies with relative path, for now.
-                    if '/' not in dep:
+                    if '/' in dep:
+                        continue
+
+                    if self.platform.startswith('android') and '.so.' in dep:
+                        # Change .so.1.2 suffix to .so, to allow loading in .apk
+                        new_dep = dep.rpartition('.so.')[0] + '.so'
+                        subprocess.call(["patchelf", "--replace-needed", dep, new_dep, temp.name])
+                        target_dep = os.path.dirname(target_path) + '/' + new_dep
+                    else:
                         target_dep = os.path.dirname(target_path) + '/' + dep
-                        self.consider_add_dependency(target_dep, dep)
 
-                subprocess.call(["strip", "-s", temp.name])
-                subprocess.call(["patchelf", "--force-rpath", "--set-rpath", "$ORIGIN", temp.name])
+                    self.consider_add_dependency(target_dep, dep)
+
+                subprocess.call([GetStrip(), "-s", temp.name])
+
+                if self.platform.startswith('android'):
+                    # We must link explicitly with Python, because the usual
+                    # -rdynamic trick doesn't work from a shared library loaded
+                    # through ANativeActivity.
+                    if suffix == '.so' and not os.path.basename(source_path).startswith('lib'):
+                        pylib_name = "libpython" + get_config_var('LDVERSION') + ".so"
+                        subprocess.call(["patchelf", "--add-needed", pylib_name, temp.name])
+                else:
+                    # On other systems, we use the rpath to force it to locate
+                    # dependencies in the same directory.
+                    subprocess.call(["patchelf", "--force-rpath", "--set-rpath", "$ORIGIN", temp.name])
 
             source_path = temp.name
 
@@ -550,7 +635,7 @@ def makewheel(version, output_dir, platform=None):
             raise Exception("patchelf is required when building a Linux wheel.")
 
     if sys.version_info < (3, 6):
-        raise Exception("Python 3.6 is required to produce a wheel.")
+        raise Exception("Python 3.6 or higher is required to produce a wheel.")
 
     if platform is None:
         # Determine the platform from the build.
@@ -571,6 +656,11 @@ def makewheel(version, output_dir, platform=None):
 
     platform = platform.replace('-', '_').replace('.', '_')
 
+    is_windows = platform == 'win32' \
+        or platform.startswith('win_') \
+        or platform.startswith('cygwin_')
+    is_macosx = platform.startswith('macosx_')
+
     # Global filepaths
     panda3d_dir = join(output_dir, "panda3d")
     pandac_dir = join(output_dir, "pandac")
@@ -578,7 +668,7 @@ def makewheel(version, output_dir, platform=None):
     models_dir = join(output_dir, "models")
     etc_dir = join(output_dir, "etc")
     bin_dir = join(output_dir, "bin")
-    if sys.platform == "win32":
+    if is_windows:
         libs_dir = join(output_dir, "bin")
     else:
         libs_dir = join(output_dir, "lib")
@@ -613,7 +703,7 @@ def makewheel(version, output_dir, platform=None):
     whl = WheelFile('panda3d', version, platform)
     whl.lib_path = [libs_dir]
 
-    if sys.platform == "win32":
+    if is_windows:
         whl.lib_path.append(ext_mod_dir)
 
     if platform.startswith("manylinux"):
@@ -629,10 +719,10 @@ def makewheel(version, output_dir, platform=None):
         whl.ignore_deps.update(MANYLINUX_LIBS)
 
     # Add libpython for deployment.
-    if sys.platform in ('win32', 'cygwin'):
+    if is_windows:
         pylib_name = 'python{0}{1}.dll'.format(*sys.version_info)
         pylib_path = os.path.join(get_config_var('BINDIR'), pylib_name)
-    elif sys.platform == 'darwin':
+    elif is_macosx:
         pylib_name = 'libpython{0}.{1}.dylib'.format(*sys.version_info)
         pylib_path = os.path.join(get_config_var('LIBDIR'), pylib_name)
     else:
@@ -679,6 +769,9 @@ if __debug__:
             if file.endswith('.pyd') and platform.startswith('cygwin'):
                 # Rename it to .dll for cygwin Python to be able to load it.
                 target_path = 'panda3d/' + os.path.splitext(file)[0] + '.dll'
+            elif file.endswith(ext_suffix) and platform.startswith('android'):
+                # Strip the extension suffix on Android.
+                target_path = 'panda3d/' + file[:-len(ext_suffix)] + '.so'
             else:
                 target_path = 'panda3d/' + file
 
@@ -686,7 +779,7 @@ if __debug__:
 
     # And copy the extension modules from the Python installation into the
     # deploy_libs directory, for use by deploy-ng.
-    ext_suffix = '.pyd' if sys.platform in ('win32', 'cygwin') else '.so'
+    ext_suffix = '.pyd' if is_windows else '.so'
 
     for file in sorted(os.listdir(ext_mod_dir)):
         if file.endswith(ext_suffix):
@@ -703,9 +796,9 @@ if __debug__:
     # Add plug-ins.
     for lib in PLUGIN_LIBS:
         plugin_name = 'lib' + lib
-        if sys.platform in ('win32', 'cygwin'):
+        if is_windows:
             plugin_name += '.dll'
-        elif sys.platform == 'darwin':
+        elif is_macosx:
             plugin_name += '.dylib'
         else:
             plugin_name += '.so'
@@ -713,6 +806,15 @@ if __debug__:
         if os.path.isfile(plugin_path):
             whl.write_file('panda3d/' + plugin_name, plugin_path)
 
+    if platform.startswith('android'):
+        deploy_stub_path = os.path.join(libs_dir, 'libdeploy-stubw.so')
+        if os.path.isfile(deploy_stub_path):
+            whl.write_file('deploy_libs/libdeploy-stubw.so', deploy_stub_path)
+
+        classes_dex_path = os.path.join(output_dir, 'classes.dex')
+        if os.path.isfile(classes_dex_path):
+            whl.write_file('deploy_libs/classes.dex', classes_dex_path)
+
     # Add the .data directory, containing additional files.
     data_dir = 'panda3d-{0}.data'.format(version)
     #whl.write_directory(data_dir + '/data/etc', etc_dir)

+ 14 - 0
panda/metalibs/pandagles2/pandagles2.cxx

@@ -9,8 +9,13 @@
 #define OPENGLES_2
 #include "config_gles2gsg.h"
 
+#if defined(ANDROID)
+#include "config_androiddisplay.h"
+#include "androidGraphicsPipe.h"
+#else
 #include "config_egldisplay.h"
 #include "eglGraphicsPipe.h"
+#endif
 
 /**
  * Initializes the library.  This must be called at least once before any of
@@ -21,7 +26,12 @@
 void
 init_libpandagles2() {
   init_libgles2gsg();
+
+#if defined(ANDROID)
+  init_libandroiddisplay();
+#else
   init_libegldisplay();
+#endif
 }
 
 /**
@@ -30,5 +40,9 @@ init_libpandagles2() {
  */
 int
 get_pipe_type_pandagles2() {
+#if defined(ANDROID)
+  return AndroidGraphicsPipe::get_class_type().get_index();
+#else
   return eglGraphicsPipe::get_class_type().get_index();
+#endif
 }

+ 31 - 9
panda/src/android/PandaActivity.java

@@ -15,10 +15,13 @@ package org.panda3d.android;
 
 import android.app.NativeActivity;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.widget.Toast;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import dalvik.system.BaseDexClassLoader;
 import org.panda3d.android.NativeIStream;
 import org.panda3d.android.NativeOStream;
 
@@ -74,6 +77,26 @@ public class PandaActivity extends NativeActivity {
         return Thread.currentThread().getName();
     }
 
+    /**
+     * Returns the path to the main native library.
+     */
+    public String getNativeLibraryPath() {
+        String libname = "main";
+        try {
+            ActivityInfo ai = getPackageManager().getActivityInfo(
+                    getIntent().getComponent(), PackageManager.GET_META_DATA);
+            if (ai.metaData != null) {
+                String ln = ai.metaData.getString(META_DATA_LIB_NAME);
+                if (ln != null) libname = ln;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new RuntimeException("Error getting activity info", e);
+        }
+
+        BaseDexClassLoader classLoader = (BaseDexClassLoader) getClassLoader();
+        return classLoader.findLibrary(libname);
+    }
+
     public String getIntentDataPath() {
         Intent intent = getIntent();
         Uri data = intent.getData();
@@ -96,6 +119,9 @@ public class PandaActivity extends NativeActivity {
         return getCacheDir().toString();
     }
 
+    /**
+     * Shows a pop-up notification.
+     */
     public void showToast(final String text, final int duration) {
         final PandaActivity activity = this;
         runOnUiThread(new Runnable() {
@@ -107,14 +133,10 @@ public class PandaActivity extends NativeActivity {
     }
 
     static {
-        //System.loadLibrary("gnustl_shared");
-        //System.loadLibrary("p3dtool");
-        //System.loadLibrary("p3dtoolconfig");
-        //System.loadLibrary("pandaexpress");
-        //System.loadLibrary("panda");
-        //System.loadLibrary("p3android");
-        //System.loadLibrary("p3framework");
-        System.loadLibrary("pandaegg");
-        System.loadLibrary("pandagles");
+        // Load this explicitly to initialize the JVM with the thread system.
+        System.loadLibrary("panda");
+
+        // Contains our JNI calls.
+        System.loadLibrary("p3android");
     }
 }

+ 83 - 68
panda/src/android/android_native_app_glue.c

@@ -12,18 +12,17 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- *
  */
 
+#include "android_native_app_glue.h"
+
 #include <jni.h>
 
 #include <errno.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
-#include <sys/resource.h>
 
-#include "android_native_app_glue.h"
 #include <android/log.h>
 
 #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "threaded_app", __VA_ARGS__))
@@ -48,17 +47,12 @@ static void free_saved_state(struct android_app* android_app) {
 
 int8_t android_app_read_cmd(struct android_app* android_app) {
     int8_t cmd;
-    if (read(android_app->msgread, &cmd, sizeof(cmd)) == sizeof(cmd)) {
-        switch (cmd) {
-            case APP_CMD_SAVE_STATE:
-                free_saved_state(android_app);
-                break;
-        }
-        return cmd;
-    } else {
+    if (read(android_app->msgread, &cmd, sizeof(cmd)) != sizeof(cmd)) {
         LOGE("No data on command pipe!");
+        return -1;
     }
-    return -1;
+    if (cmd == APP_CMD_SAVE_STATE) free_saved_state(android_app);
+    return cmd;
 }
 
 static void print_cur_config(struct android_app* android_app) {
@@ -89,7 +83,7 @@ static void print_cur_config(struct android_app* android_app) {
 void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
     switch (cmd) {
         case APP_CMD_INPUT_CHANGED:
-            LOGV("APP_CMD_INPUT_CHANGED\n");
+            LOGV("APP_CMD_INPUT_CHANGED");
             pthread_mutex_lock(&android_app->mutex);
             if (android_app->inputQueue != NULL) {
                 AInputQueue_detachLooper(android_app->inputQueue);
@@ -106,7 +100,7 @@ void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
             break;
 
         case APP_CMD_INIT_WINDOW:
-            LOGV("APP_CMD_INIT_WINDOW\n");
+            LOGV("APP_CMD_INIT_WINDOW");
             pthread_mutex_lock(&android_app->mutex);
             android_app->window = android_app->pendingWindow;
             pthread_cond_broadcast(&android_app->cond);
@@ -114,7 +108,7 @@ void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
             break;
 
         case APP_CMD_TERM_WINDOW:
-            LOGV("APP_CMD_TERM_WINDOW\n");
+            LOGV("APP_CMD_TERM_WINDOW");
             pthread_cond_broadcast(&android_app->cond);
             break;
 
@@ -122,7 +116,7 @@ void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
         case APP_CMD_START:
         case APP_CMD_PAUSE:
         case APP_CMD_STOP:
-            LOGV("activityState=%d\n", cmd);
+            LOGV("activityState=%d", cmd);
             pthread_mutex_lock(&android_app->mutex);
             android_app->activityState = cmd;
             pthread_cond_broadcast(&android_app->cond);
@@ -130,14 +124,14 @@ void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
             break;
 
         case APP_CMD_CONFIG_CHANGED:
-            LOGV("APP_CMD_CONFIG_CHANGED\n");
+            LOGV("APP_CMD_CONFIG_CHANGED");
             AConfiguration_fromAssetManager(android_app->config,
                     android_app->activity->assetManager);
             print_cur_config(android_app);
             break;
 
         case APP_CMD_DESTROY:
-            LOGV("APP_CMD_DESTROY\n");
+            LOGV("APP_CMD_DESTROY");
             android_app->destroyRequested = 1;
             break;
     }
@@ -146,7 +140,7 @@ void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
 void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) {
     switch (cmd) {
         case APP_CMD_TERM_WINDOW:
-            LOGV("APP_CMD_TERM_WINDOW\n");
+            LOGV("APP_CMD_TERM_WINDOW");
             pthread_mutex_lock(&android_app->mutex);
             android_app->window = NULL;
             pthread_cond_broadcast(&android_app->cond);
@@ -154,7 +148,7 @@ void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) {
             break;
 
         case APP_CMD_SAVE_STATE:
-            LOGV("APP_CMD_SAVE_STATE\n");
+            LOGV("APP_CMD_SAVE_STATE");
             pthread_mutex_lock(&android_app->mutex);
             android_app->stateSaved = 1;
             pthread_cond_broadcast(&android_app->cond);
@@ -168,7 +162,6 @@ void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) {
 }
 
 void app_dummy() {
-
 }
 
 static void android_app_destroy(struct android_app* android_app) {
@@ -188,7 +181,7 @@ static void android_app_destroy(struct android_app* android_app) {
 static void process_input(struct android_app* app, struct android_poll_source* source) {
     AInputEvent* event = NULL;
     while (AInputQueue_getEvent(app->inputQueue, &event) >= 0) {
-        LOGV("New input event: type=%d\n", AInputEvent_getType(event));
+        LOGV("New input event: type=%d", AInputEvent_getType(event));
         if (AInputQueue_preDispatchEvent(app->inputQueue, event)) {
             continue;
         }
@@ -241,9 +234,8 @@ static void* android_app_entry(void* param) {
 // --------------------------------------------------------------------
 
 static struct android_app* android_app_create(ANativeActivity* activity,
-        void* savedState, size_t savedStateSize) {
-    struct android_app* android_app = (struct android_app*)malloc(sizeof(struct android_app));
-    memset(android_app, 0, sizeof(struct android_app));
+                                              void* savedState, size_t savedStateSize) {
+    struct android_app* android_app = calloc(1, sizeof(struct android_app));
     android_app->activity = activity;
 
     pthread_mutex_init(&android_app->mutex, NULL);
@@ -263,7 +255,7 @@ static struct android_app* android_app_create(ANativeActivity* activity,
     android_app->msgread = msgpipe[0];
     android_app->msgwrite = msgpipe[1];
 
-    pthread_attr_t attr; 
+    pthread_attr_t attr;
     pthread_attr_init(&attr);
     pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
     pthread_create(&android_app->thread, &attr, android_app_entry, android_app);
@@ -280,7 +272,7 @@ static struct android_app* android_app_create(ANativeActivity* activity,
 
 static void android_app_write_cmd(struct android_app* android_app, int8_t cmd) {
     if (write(android_app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) {
-        LOGE("Failure writing android_app cmd: %s\n", strerror(errno));
+        LOGE("Failure writing android_app cmd: %s", strerror(errno));
     }
 }
 
@@ -333,26 +325,30 @@ static void android_app_free(struct android_app* android_app) {
     free(android_app);
 }
 
+static struct android_app* ToApp(ANativeActivity* activity) {
+    return (struct android_app*) activity->instance;
+}
+
 static void onDestroy(ANativeActivity* activity) {
-    LOGV("Destroy: %p\n", activity);
-    android_app_free((struct android_app*)activity->instance);
+    LOGV("Destroy: %p", activity);
+    android_app_free(ToApp(activity));
 }
 
 static void onStart(ANativeActivity* activity) {
-    LOGV("Start: %p\n", activity);
-    android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_START);
+    LOGV("Start: %p", activity);
+    android_app_set_activity_state(ToApp(activity), APP_CMD_START);
 }
 
 static void onResume(ANativeActivity* activity) {
-    LOGV("Resume: %p\n", activity);
-    android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_RESUME);
+    LOGV("Resume: %p", activity);
+    android_app_set_activity_state(ToApp(activity), APP_CMD_RESUME);
 }
 
 static void* onSaveInstanceState(ANativeActivity* activity, size_t* outLen) {
-    struct android_app* android_app = (struct android_app*)activity->instance;
-    void* savedState = NULL;
+    LOGV("SaveInstanceState: %p", activity);
 
-    LOGV("SaveInstanceState: %p\n", activity);
+    struct android_app* android_app = ToApp(activity);
+    void* savedState = NULL;
     pthread_mutex_lock(&android_app->mutex);
     android_app->stateSaved = 0;
     android_app_write_cmd(android_app, APP_CMD_SAVE_STATE);
@@ -373,70 +369,89 @@ static void* onSaveInstanceState(ANativeActivity* activity, size_t* outLen) {
 }
 
 static void onPause(ANativeActivity* activity) {
-    LOGV("Pause: %p\n", activity);
-    android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_PAUSE);
+    LOGV("Pause: %p", activity);
+    android_app_set_activity_state(ToApp(activity), APP_CMD_PAUSE);
 }
 
 static void onStop(ANativeActivity* activity) {
-    LOGV("Stop: %p\n", activity);
-    android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_STOP);
+    LOGV("Stop: %p", activity);
+    android_app_set_activity_state(ToApp(activity), APP_CMD_STOP);
 }
 
 static void onConfigurationChanged(ANativeActivity* activity) {
-    struct android_app* android_app = (struct android_app*)activity->instance;
-    LOGV("ConfigurationChanged: %p\n", activity);
-    android_app_write_cmd(android_app, APP_CMD_CONFIG_CHANGED);
+    LOGV("ConfigurationChanged: %p", activity);
+    android_app_write_cmd(ToApp(activity), APP_CMD_CONFIG_CHANGED);
+}
+
+static void onContentRectChanged(ANativeActivity* activity, const ARect* r) {
+    LOGV("ContentRectChanged: l=%d,t=%d,r=%d,b=%d", r->left, r->top, r->right, r->bottom);
+    struct android_app* android_app = ToApp(activity);
+    pthread_mutex_lock(&android_app->mutex);
+    android_app->contentRect = *r;
+    pthread_mutex_unlock(&android_app->mutex);
+    android_app_write_cmd(ToApp(activity), APP_CMD_CONTENT_RECT_CHANGED);
 }
 
 static void onLowMemory(ANativeActivity* activity) {
-    struct android_app* android_app = (struct android_app*)activity->instance;
-    LOGV("LowMemory: %p\n", activity);
-    android_app_write_cmd(android_app, APP_CMD_LOW_MEMORY);
+    LOGV("LowMemory: %p", activity);
+    android_app_write_cmd(ToApp(activity), APP_CMD_LOW_MEMORY);
 }
 
 static void onWindowFocusChanged(ANativeActivity* activity, int focused) {
-    LOGV("WindowFocusChanged: %p -- %d\n", activity, focused);
-    android_app_write_cmd((struct android_app*)activity->instance,
-            focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS);
+    LOGV("WindowFocusChanged: %p -- %d", activity, focused);
+    android_app_write_cmd(ToApp(activity), focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS);
 }
 
 static void onNativeWindowCreated(ANativeActivity* activity, ANativeWindow* window) {
-    LOGV("NativeWindowCreated: %p -- %p\n", activity, window);
-    android_app_set_window((struct android_app*)activity->instance, window);
+    LOGV("NativeWindowCreated: %p -- %p", activity, window);
+    android_app_set_window(ToApp(activity), window);
 }
 
 static void onNativeWindowDestroyed(ANativeActivity* activity, ANativeWindow* window) {
-    LOGV("NativeWindowDestroyed: %p -- %p\n", activity, window);
-    android_app_set_window((struct android_app*)activity->instance, NULL);
+    LOGV("NativeWindowDestroyed: %p -- %p", activity, window);
+    android_app_set_window(ToApp(activity), NULL);
+}
+
+static void onNativeWindowRedrawNeeded(ANativeActivity* activity, ANativeWindow* window) {
+    LOGV("NativeWindowRedrawNeeded: %p -- %p", activity, window);
+    android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_REDRAW_NEEDED);
+}
+
+static void onNativeWindowResized(ANativeActivity* activity, ANativeWindow* window) {
+    LOGV("NativeWindowResized: %p -- %p", activity, window);
+    android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_RESIZED);
 }
 
 static void onInputQueueCreated(ANativeActivity* activity, AInputQueue* queue) {
-    LOGV("InputQueueCreated: %p -- %p\n", activity, queue);
-    android_app_set_input((struct android_app*)activity->instance, queue);
+    LOGV("InputQueueCreated: %p -- %p", activity, queue);
+    android_app_set_input(ToApp(activity), queue);
 }
 
 static void onInputQueueDestroyed(ANativeActivity* activity, AInputQueue* queue) {
-    LOGV("InputQueueDestroyed: %p -- %p\n", activity, queue);
-    android_app_set_input((struct android_app*)activity->instance, NULL);
+    LOGV("InputQueueDestroyed: %p -- %p", activity, queue);
+    android_app_set_input(ToApp(activity), NULL);
 }
 
 JNIEXPORT
-void ANativeActivity_onCreate(ANativeActivity* activity, void* savedState,
-                              size_t savedStateSize) {
-    LOGV("Creating: %p\n", activity);
+void ANativeActivity_onCreate(ANativeActivity* activity, void* savedState, size_t savedStateSize) {
+    LOGV("Creating: %p", activity);
+
+    activity->callbacks->onConfigurationChanged = onConfigurationChanged;
+    activity->callbacks->onContentRectChanged = onContentRectChanged;
     activity->callbacks->onDestroy = onDestroy;
-    activity->callbacks->onStart = onStart;
+    activity->callbacks->onInputQueueCreated = onInputQueueCreated;
+    activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;
+    activity->callbacks->onLowMemory = onLowMemory;
+    activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
+    activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
+    activity->callbacks->onNativeWindowRedrawNeeded = onNativeWindowRedrawNeeded;
+    activity->callbacks->onNativeWindowResized = onNativeWindowResized;
+    activity->callbacks->onPause = onPause;
     activity->callbacks->onResume = onResume;
     activity->callbacks->onSaveInstanceState = onSaveInstanceState;
-    activity->callbacks->onPause = onPause;
+    activity->callbacks->onStart = onStart;
     activity->callbacks->onStop = onStop;
-    activity->callbacks->onConfigurationChanged = onConfigurationChanged;
-    activity->callbacks->onLowMemory = onLowMemory;
     activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
-    activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
-    activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
-    activity->callbacks->onInputQueueCreated = onInputQueueCreated;
-    activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;
 
     activity->instance = android_app_create(activity, savedState, savedStateSize);
 }

+ 2 - 6
panda/src/android/android_native_app_glue.h

@@ -12,11 +12,9 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- *
  */
 
-#ifndef _ANDROID_NATIVE_APP_GLUE_H
-#define _ANDROID_NATIVE_APP_GLUE_H
+#pragma once
 
 #include <poll.h>
 #include <pthread.h>
@@ -332,7 +330,7 @@ void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd);
 void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd);
 
 /**
- * Dummy function that used to be used to prevent the linker from stripping app
+ * No-op function that used to be used to prevent the linker from stripping app
  * glue code. No longer necessary, since __attribute__((visibility("default")))
  * does this for us.
  */
@@ -350,5 +348,3 @@ extern void android_main(struct android_app* app);
 #ifdef __cplusplus
 }
 #endif
-
-#endif /* _ANDROID_NATIVE_APP_GLUE_H */

+ 4 - 1
panda/src/chan/partBundle.cxx

@@ -599,6 +599,10 @@ do_bind_anim(AnimControl *control, AnimBundle *anim,
     return false;
   }
 
+  // Grabbing the lock early prevents any other thread in stage 0 from also
+  // trying to modify the channel list at the same time.
+  CDLockedReader cdata(_cycler);
+
   plist<int> holes;
   int channel_index = 0;
   pick_channel_index(holes, channel_index);
@@ -616,7 +620,6 @@ do_bind_anim(AnimControl *control, AnimBundle *anim,
                  subset.is_include_empty(), bound_joints, subset);
   control->setup_anim(this, anim, channel_index, bound_joints);
 
-  CDReader cdata(_cycler);
   determine_effective_channels(cdata);
 
   return true;

+ 0 - 1
panda/src/chan/partGroup.h

@@ -19,7 +19,6 @@
 #include "typedWritableReferenceCount.h"
 #include "pointerTo.h"
 #include "namable.h"
-#include "typedef.h"
 #include "thread.h"
 #include "plist.h"
 #include "luse.h"

+ 1 - 1
panda/src/device/inputDeviceManager.cxx

@@ -40,7 +40,7 @@ make_global_ptr() {
   _global_ptr = new WinInputDeviceManager;
 #elif defined(__APPLE__)
   _global_ptr = new IOKitInputDeviceManager;
-#elif defined(PHAVE_LINUX_INPUT_H)
+#elif defined(PHAVE_LINUX_INPUT_H) && !defined(ANDROID)
   _global_ptr = new LinuxInputDeviceManager;
 #else
   _global_ptr = new InputDeviceManager;

+ 38 - 4
panda/src/device/winRawInputDevice.cxx

@@ -38,6 +38,9 @@ enum QuirkBits : int {
 
   // Axes on the right stick are swapped, using x for y and vice versa.
   QB_right_axes_swapped = 64,
+
+  // Using an RC (drone) controller as a gamepad instead of a flight stick
+  QB_rc_controller = 128,
 };
 
 // Some nonstandard gamepads have different button mappings.
@@ -85,6 +88,10 @@ static const struct DeviceMapping {
   {0x2563, 0x0523, InputDevice::DeviceClass::gamepad, QB_rstick_from_z | QB_no_analog_triggers,
     {"face_y", "face_b", "face_a", "face_x", "lshoulder", "rshoulder", "ltrigger", "rtrigger", "back", "start", "lstick", "rstick"}
   },
+  // FrSky Simulator
+  {0x0483, 0x5720, InputDevice::DeviceClass::gamepad, QB_rc_controller,
+    {0}
+  },
   {0},
 };
 
@@ -434,7 +441,11 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
         switch (usage) {
           case HID_USAGE_GENERIC_X:
           if (_device_class == DeviceClass::gamepad) {
-            axis = Axis::left_x;
+            if (quirks & QB_rc_controller) {
+              axis = Axis::right_x;
+            } else {
+              axis = Axis::left_x;
+            }
           } else if (_device_class == DeviceClass::flight_stick) {
             axis = Axis::roll;
           } else {
@@ -443,8 +454,12 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
           break;
         case HID_USAGE_GENERIC_Y:
           if (_device_class == DeviceClass::gamepad) {
-            axis = Axis::left_y;
-            swap(cap.LogicalMin, cap.LogicalMax);
+            if (quirks & QB_rc_controller) {
+              axis = Axis::right_y;
+            } else {
+              axis = Axis::left_y;
+              swap(cap.LogicalMin, cap.LogicalMax);
+            }
           } else if (_device_class == DeviceClass::flight_stick) {
             axis = Axis::pitch;
           } else {
@@ -461,6 +476,8 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
               } else {
                 axis = InputDevice::Axis::right_x;
               }
+            } else if (quirks & QB_rc_controller) {
+              axis = InputDevice::Axis::left_y;
             } else if ((quirks & QB_no_analog_triggers) == 0) {
               axis = Axis::left_trigger;
             }
@@ -483,6 +500,8 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
               if ((quirks & QB_no_analog_triggers) == 0) {
                 axis = Axis::left_trigger;
               }
+            } else if (quirks & QB_rc_controller) {
+              axis = Axis::left_x;
             } else {
               axis = Axis::right_x;
             }
@@ -663,7 +682,22 @@ process_report(PCHAR ptr, size_t size) {
   if (status == HIDP_STATUS_SUCCESS) {
     for (ULONG di = 0; di < count; ++di) {
       if (data[di].DataIndex != _hat_data_index) {
-        nassertd(data[di].DataIndex < _indices.size()) continue;
+        if (device_cat.is_spam()) {
+          device_cat.spam()
+            << "Read RawValue " << data[di].RawValue
+            << " for DataIndex " << data[di].DataIndex
+            << " from raw device " << _path << "\n";
+        }
+
+        if (data[di].DataIndex >= _indices.size()) {
+          if (device_cat.is_debug()) {
+            device_cat.debug()
+              << "Ignoring out of range DataIndex " << data[di].DataIndex
+              << "from raw device " << _path << "\n";
+          }
+          continue;
+        }
+
         const Index &idx = _indices[data[di].DataIndex];
         if (idx._axis >= 0) {
           if (idx._signed) {

+ 4 - 0
panda/src/display/CMakeLists.txt

@@ -67,6 +67,10 @@ set(P3DISPLAY_SOURCES
 )
 
 set(P3DISPLAY_IGATEEXT
+  frameBufferProperties_ext.cxx
+  frameBufferProperties_ext.h
+  graphicsPipeSelection_ext.cxx
+  graphicsPipeSelection_ext.h
   graphicsStateGuardian_ext.cxx
   graphicsStateGuardian_ext.h
   graphicsWindow_ext.cxx

+ 0 - 1
panda/src/display/displayInformation.h

@@ -14,7 +14,6 @@
 #ifndef DISPLAYINFORMATION_H
 #define DISPLAYINFORMATION_H
 
-#include "typedef.h"
 #include "graphicsStateGuardian.h"
 
 struct EXPCL_PANDA_DISPLAY DisplayMode {

+ 2 - 0
panda/src/display/displayRegion.I

@@ -160,6 +160,8 @@ set_dimensions(const LVecBase4 &dimensions) {
  * Changes the range of the depth buffer this DisplayRegion writes to.
  * The parameters range from 0 to 1.  It is legal for the near value to be
  * larger than the far value.
+ *
+ * @since 1.11.0
  */
 INLINE void DisplayRegion::
 set_depth_range(PN_stdfloat near_depth, PN_stdfloat far_depth) {

+ 6 - 0
panda/src/display/frameBufferProperties.h

@@ -16,6 +16,7 @@
 
 #include "pandabase.h"
 #include "pnotify.h"
+#include "extension.h"
 
 class Texture;
 
@@ -143,6 +144,9 @@ PUBLISHED:
   MAKE_PROPERTY(float_color, get_float_color, set_float_color);
   MAKE_PROPERTY(float_depth, get_float_depth, set_float_depth);
 
+  EXTENSION(PyObject *__getstate__() const);
+  EXTENSION(void __setstate__(PyObject *self, PyObject *state));
+
   // Other.
 
   constexpr FrameBufferProperties() = default;
@@ -169,6 +173,8 @@ PUBLISHED:
 
   bool setup_color_texture(Texture *tex) const;
   bool setup_depth_texture(Texture *tex) const;
+
+  friend class Extension<FrameBufferProperties>;
 };
 
 INLINE std::ostream &operator << (std::ostream &out, const FrameBufferProperties &properties);

+ 75 - 0
panda/src/display/frameBufferProperties_ext.cxx

@@ -0,0 +1,75 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file frameBufferProperties_ext.cxx
+ * @author rdb
+ * @date 2021-12-13
+ */
+
+#include "frameBufferProperties_ext.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * Returns the properties as a dictionary.
+ */
+PyObject *Extension<FrameBufferProperties>::
+__getstate__() const {
+  static const char *props[FrameBufferProperties::FBP_COUNT] = {"depth_bits", "color_bits", "red_bits", "green_bits", "blue_bits", "alpha_bits", "stencil_bits", "accum_bits", "aux_rgba", "aux_hrgba", "aux_float", "multisamples", "coverage_samples", "back_buffers"};
+  static const char *flags[] = {"indexed_color", "rgb_color", "stereo", "force_hardware", "force_software", "srgb_color", "float_color", "float_depth", nullptr};
+
+  PyObject *state = PyDict_New();
+
+  for (size_t i = 0; i < FrameBufferProperties::FBP_COUNT; ++i) {
+    if (_this->_specified & (1 << i)) {
+      PyObject *value = PyLong_FromLong(_this->_property[i]);
+      PyDict_SetItemString(state, props[i], value);
+      Py_DECREF(value);
+    }
+  }
+
+  for (size_t i = 0; flags[i] != nullptr; ++i) {
+    if (_this->_flags_specified & (1 << i)) {
+      PyObject *value = (_this->_flags & (1 << i)) ? Py_True : Py_False;
+      PyDict_SetItemString(state, flags[i], value);
+    }
+  }
+
+  return state;
+}
+
+/**
+ *
+ */
+void Extension<FrameBufferProperties>::
+__setstate__(PyObject *self, PyObject *props) {
+  PyTypeObject *type = Py_TYPE(self);
+  PyObject *key, *value;
+  Py_ssize_t pos = 0;
+
+  while (PyDict_Next(props, &pos, &key, &value)) {
+    // Look for a writable property on the type by this name.
+    PyObject *descr = _PyType_Lookup(type, key);
+
+    if (descr != nullptr && Py_TYPE(descr)->tp_descr_set != nullptr) {
+      if (Py_TYPE(descr)->tp_descr_set(descr, self, value) < 0) {
+        return;
+      }
+    } else {
+      PyObject *key_repr = PyObject_Repr(key);
+      PyErr_Format(PyExc_TypeError,
+                   "%.100s is an invalid framebuffer property",
+                   PyUnicode_AsUTF8(key_repr)
+                  );
+      Py_DECREF(key_repr);
+      return;
+    }
+  }
+}
+
+#endif  // HAVE_PYTHON

+ 38 - 0
panda/src/display/frameBufferProperties_ext.h

@@ -0,0 +1,38 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file frameBufferProperties_ext.h
+ * @author rdb
+ * @date 2021-12-13
+ */
+
+#ifndef FRAMEBUFFERPROPERTIES_EXT_H
+#define FRAMEBUFFERPROPERTIES_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "frameBufferProperties.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for FrameBufferProperties, which are
+ * called instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<FrameBufferProperties> : public ExtensionBase<FrameBufferProperties> {
+public:
+  PyObject *__getstate__() const;
+  void __setstate__(PyObject *self, PyObject *state);
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // FRAMEBUFFERPROPERTIES_EXT_H

+ 2 - 0
panda/src/display/graphicsOutput.h

@@ -255,6 +255,8 @@ PUBLISHED:
   virtual bool flip_ready() const;
   virtual GraphicsOutput *get_host();
 
+  MAKE_PROPERTY(host, get_host);
+
 public:
   INLINE bool operator < (const GraphicsOutput &other) const;
 

+ 2 - 0
panda/src/display/graphicsPipeSelection.h

@@ -52,6 +52,8 @@ PUBLISHED:
 
   INLINE static GraphicsPipeSelection *get_global_ptr();
 
+  EXTENSION(PyObject *__reduce__() const);
+
 public:
   typedef PT(GraphicsPipe) PipeConstructorFunc();
   bool add_pipe_type(TypeHandle type, PipeConstructorFunc *func);

+ 31 - 0
panda/src/display/graphicsPipeSelection_ext.cxx

@@ -0,0 +1,31 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file graphicsPipeSelection_ext.cxx
+ * @author rdb
+ * @date 2021-12-10
+ */
+
+#include "graphicsPipeSelection_ext.h"
+
+#ifdef HAVE_PYTHON
+
+#include "pythonLoaderFileType.h"
+
+extern struct Dtool_PyTypedObject Dtool_GraphicsPipeSelection;
+
+/**
+ * Implements pickle support.
+ */
+PyObject *Extension<GraphicsPipeSelection>::
+__reduce__() const {
+  PyObject *func = PyObject_GetAttrString((PyObject *)&Dtool_GraphicsPipeSelection, "get_global_ptr");
+  return Py_BuildValue("N()", func);
+}
+
+#endif

+ 37 - 0
panda/src/display/graphicsPipeSelection_ext.h

@@ -0,0 +1,37 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file graphicsPipeSelection_ext.h
+ * @author rdb
+ * @date 2021-12-10
+ */
+
+#ifndef GRAPHICSPIPESELECTION_EXT_H
+#define GRAPHICSPIPESELECTION_EXT_H
+
+#include "pandabase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "graphicsPipeSelection.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for GraphicsPipeSelection, which are called
+ * instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<GraphicsPipeSelection> : public ExtensionBase<GraphicsPipeSelection> {
+public:
+  PyObject *__reduce__() const;
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // GRAPHICSPIPESELECTION_EXT_H

+ 2 - 25
panda/src/display/graphicsStateGuardian.cxx

@@ -3581,31 +3581,8 @@ async_reload_texture(TextureContext *tc) {
     priority = _current_display_region->get_texture_reload_priority();
   }
 
-  string task_name = string("reload:") + tc->get_texture()->get_name();
-  PT(AsyncTaskManager) task_mgr = _loader->get_task_manager();
-
-  // See if we are already loading this task.
-  AsyncTaskCollection orig_tasks = task_mgr->find_tasks(task_name);
-  size_t num_tasks = orig_tasks.get_num_tasks();
-  for (size_t ti = 0; ti < num_tasks; ++ti) {
-    AsyncTask *task = orig_tasks.get_task(ti);
-    if (task->is_exact_type(TextureReloadRequest::get_class_type()) &&
-        ((TextureReloadRequest *)task)->get_texture() == tc->get_texture()) {
-      // This texture is already queued to be reloaded.  Don't queue it again,
-      // just make sure the priority is updated, and return.
-      task->set_priority(std::max(task->get_priority(), priority));
-      return (AsyncFuture *)task;
-    }
-  }
-
-  // This texture has not yet been queued to be reloaded.  Queue it up now.
-  PT(AsyncTask) request =
-    new TextureReloadRequest(task_name,
-                             _prepared_objects, tc->get_texture(),
-                             _supports_compressed_texture);
-  request->set_priority(priority);
-  _loader->load_async(request);
-  return (AsyncFuture *)request.p();
+  Texture *tex = tc->get_texture();
+  return tex->async_ensure_ram_image(_supports_compressed_texture, priority);
 }
 
 /**

+ 5 - 1
panda/src/display/graphicsWindow.h

@@ -55,7 +55,9 @@ PUBLISHED:
   const WindowProperties get_requested_properties() const;
   void clear_rejected_properties();
   WindowProperties get_rejected_properties() const;
-  void request_properties(const WindowProperties &requested_properties);
+
+  EXTENSION(void request_properties(PyObject *args, PyObject *kwds));
+
   INLINE bool is_closed() const;
   virtual bool is_active() const;
   INLINE bool is_fullscreen() const;
@@ -100,6 +102,8 @@ PUBLISHED:
   virtual void close_ime();
 
 public:
+  void request_properties(const WindowProperties &requested_properties);
+
   virtual void add_window_proc( const GraphicsWindowProc* wnd_proc_object ){};
   virtual void remove_window_proc( const GraphicsWindowProc* wnd_proc_object ){};
   virtual void clear_window_procs(){};

+ 17 - 0
panda/src/display/graphicsWindow_ext.cxx

@@ -12,9 +12,26 @@
  */
 
 #include "graphicsWindow_ext.h"
+#include "windowProperties_ext.h"
 
 #ifdef HAVE_PYTHON
 
+/**
+ * Convenient shorthand for requesting properties.
+ */
+void Extension<GraphicsWindow>::
+request_properties(PyObject *args, PyObject *kwds) {
+  extern struct Dtool_PyTypedObject Dtool_WindowProperties;
+
+  WindowProperties props;
+  PyObject *py_props = DTool_CreatePyInstance((void *)&props, Dtool_WindowProperties, false, false);
+
+  invoke_extension(&props).__init__(py_props, args, kwds);
+
+  _this->request_properties(props);
+  Py_DECREF(py_props);
+}
+
 /**
  * Adds a python event handler to be called when a window event occurs.
  */

+ 2 - 0
panda/src/display/graphicsWindow_ext.h

@@ -30,6 +30,8 @@
 template<>
 class Extension<GraphicsWindow> : public ExtensionBase<GraphicsWindow> {
 public:
+  void request_properties(PyObject *args, PyObject *kwds);
+
   void add_python_event_handler(PyObject* handler, PyObject* name);
   void remove_python_event_handler(PyObject* name);
 };

+ 2 - 0
panda/src/display/p3display_ext_composite.cxx

@@ -1,3 +1,5 @@
+#include "frameBufferProperties_ext.cxx"
+#include "graphicsPipeSelection_ext.cxx"
 #include "graphicsStateGuardian_ext.cxx"
 #include "graphicsWindow_ext.cxx"
 #include "pythonGraphicsWindowProc.cxx"

+ 1 - 1
panda/src/display/pythonGraphicsWindowProc.h

@@ -21,7 +21,7 @@
 #ifdef HAVE_PYTHON
 
 /**
- * Extends GraphicsWindowProc to provde callback functionality to a python
+ * Extends GraphicsWindowProc to provide callback functionality to a Python
  * program.
  */
 class PythonGraphicsWindowProc: public GraphicsWindowProc,

+ 4 - 1
panda/src/display/windowProperties.h

@@ -205,6 +205,9 @@ PUBLISHED:
   MAKE_PROPERTY2(parent_window, has_parent_window, get_parent_window,
                                 set_parent_window, clear_parent_window);
 
+  EXTENSION(PyObject *__getstate__(PyObject *self) const);
+  EXTENSION(void __setstate__(PyObject *self, PyObject *state));
+
   void add_properties(const WindowProperties &other);
 
   void output(std::ostream &out) const;
@@ -232,7 +235,7 @@ private:
     S_maximized            = 0x10000,
   };
 
-  // This bitmask represents the truefalse settings for various boolean flags
+  // This bitmask represents the true/false settings for various boolean flags
   // (assuming the corresponding S_* bit has been set, above).
   enum Flags {
     F_undecorated    = S_undecorated,

+ 55 - 19
panda/src/display/windowProperties_ext.cxx

@@ -50,27 +50,63 @@ __init__(PyObject *self, PyObject *args, PyObject *kwds) {
   // Now iterate over the keyword arguments, which define the default values
   // for the different properties.
   if (kwds != nullptr) {
-    PyTypeObject *type = Py_TYPE(self);
-    PyObject *key, *value;
-    Py_ssize_t pos = 0;
-
-    while (PyDict_Next(kwds, &pos, &key, &value)) {
-      // Look for a writable property on the type by this name.
-      PyObject *descr = _PyType_Lookup(type, key);
-
-      if (descr != nullptr && Py_TYPE(descr)->tp_descr_set != nullptr) {
-        if (Py_TYPE(descr)->tp_descr_set(descr, self, value) < 0) {
-          return;
-        }
-      } else {
-        PyObject *key_repr = PyObject_Repr(key);
-        PyErr_Format(PyExc_TypeError,
-                     "%.100s is an invalid keyword argument for WindowProperties()",
-                     PyUnicode_AsUTF8(key_repr)
-                    );
-        Py_DECREF(key_repr);
+    __setstate__(self, kwds);
+  }
+}
+
+/**
+ * Returns the properties as a dictionary.
+ */
+PyObject *Extension<WindowProperties>::
+__getstate__(PyObject *self) const {
+  static const char *props[] = {"origin", "size", "title", "undecorated", "fixed_size", "fullscreen", "foreground", "minimized", "maximized", "raw_mice", "open", "cursor_hidden", "icon_filename", "cursor_filename", "z_order", "mouse_mode", "parent_window", nullptr};
+
+  PyTypeObject *type = Py_TYPE(self);
+  PyObject *state = PyDict_New();
+
+  for (size_t i = 0; props[i] != nullptr; ++i) {
+    PyObject *key = PyUnicode_FromString(props[i]);
+    PyObject *descr = _PyType_Lookup(type, key);
+
+    if (descr != nullptr && Py_TYPE(descr)->tp_descr_get != nullptr) {
+      PyObject *value = Py_TYPE(descr)->tp_descr_get(descr, self, (PyObject *)type);
+      nassertr(value != nullptr, nullptr);
+      if (value != Py_None) {
+        PyDict_SetItem(state, key, value);
+      }
+      Py_DECREF(value);
+    }
+    Py_DECREF(key);
+  }
+
+  return state;
+}
+
+/**
+ *
+ */
+void Extension<WindowProperties>::
+__setstate__(PyObject *self, PyObject *props) {
+  PyTypeObject *type = Py_TYPE(self);
+  PyObject *key, *value;
+  Py_ssize_t pos = 0;
+
+  while (PyDict_Next(props, &pos, &key, &value)) {
+    // Look for a writable property on the type by this name.
+    PyObject *descr = _PyType_Lookup(type, key);
+
+    if (descr != nullptr && Py_TYPE(descr)->tp_descr_set != nullptr) {
+      if (Py_TYPE(descr)->tp_descr_set(descr, self, value) < 0) {
         return;
       }
+    } else {
+      PyObject *key_repr = PyObject_Repr(key);
+      PyErr_Format(PyExc_TypeError,
+                   "%.100s is an invalid keyword argument for WindowProperties()",
+                   PyUnicode_AsUTF8(key_repr)
+                  );
+      Py_DECREF(key_repr);
+      return;
     }
   }
 }

+ 3 - 0
panda/src/display/windowProperties_ext.h

@@ -30,6 +30,9 @@ template<>
 class Extension<WindowProperties> : public ExtensionBase<WindowProperties> {
 public:
   void __init__(PyObject *self, PyObject *args, PyObject *kwds);
+
+  PyObject *__getstate__(PyObject *self) const;
+  void __setstate__(PyObject *self, PyObject *state);
 };
 
 #endif  // HAVE_PYTHON

+ 82 - 67
panda/src/doc/eggSyntax.txt

@@ -3,7 +3,7 @@ THE PHILOSOPHY OF EGG FILES (vs. bam files)
 Egg files are used by Panda3D to describe many properties of a scene:
 simple geometry, including special effects and collision surfaces,
 characters including skeletons, morphs, and multiple-joint
-assignments, and character animation tables.  
+assignments, and character animation tables.
 
 Egg files are designed to be the lingua franca of model manipulation
 for Panda tools.  A number of utilities are provided that read and
@@ -168,8 +168,9 @@ appear before they are referenced.
     The remaining formats are generic and specify only the semantic
     meaning of the channels.  The size of the texels is determined by
     the width of the components in the image file.  RGBA is the most
-    general; RGB is the same, but without any alpha channel.  RGBM is
-    like RGBA, except that it requests only one bit of alpha, if the
+    general; RGB is the same, but without any alpha channel.
+
+    RGBM is like RGBA, except that it requests only one bit of alpha, if the
     graphics card can provide that, to leave more room for the RGB
     components, which is especially important for older 16-bit
     graphics cards (the "M" stands for "mask", as in a cutout).
@@ -211,7 +212,7 @@ appear before they are referenced.
     Although less often used, for 3-d textures wrapw may also be
     specified, and it behaves similarly to wrapu and wrapv.
 
-    There are other legal values in addtional to REPEAT and CLAMP.
+    There are other legal values in additional to REPEAT and CLAMP.
     The full list is:
 
       CLAMP
@@ -278,7 +279,7 @@ appear before they are referenced.
     / (number of views).
 
   <Scalar> read-mipmaps { flag }
- 
+
    If this flag is nonzero, then pre-generated mipmap levels will be
     loaded along with the texture.  In this case, the filename should
     contain a sequence of one or more hash mark ("#") characters,
@@ -413,7 +414,7 @@ appear before they are referenced.
       <Scalar> combine-alpha-operand2 { src-alpha }
 
   <Scalar> saved-result { flag }
- 
+
     If flag is nonzero, then it indicates that this particular texture
     stage will be supplied as the "last_saved_result" source for any
     future texture stages.
@@ -704,7 +705,7 @@ appear before they are referenced.
     <UV> [name] { u v [w] [tangent] [binormal] [morph-list] }
 
     This gives the texture coordinates of the vertex.  This must be
-    specified if a texture is to be mapped onto this geometry.  
+    specified if a texture is to be mapped onto this geometry.
 
     The texture coordinates are usually two-dimensional, with two
     component values (u v), but they may also be three-dimensional,
@@ -739,7 +740,7 @@ appear before they are referenced.
     meaning to custom code or a custom shader.  Like named UV's, there
     may be multiple Aux entries for a given vertex, each with a
     different name.
-    
+
 
 
 <DynamicVertexPool> name { vertices }
@@ -758,18 +759,18 @@ appear before they are referenced.
 
   At the present time, the DynamicVertexPool is not implemented in
   Panda3D.
-  
+
 
 
 
 GEOMETRY ENTRIES
 
-<Polygon> name { 
-    [attributes] 
-    <VertexRef> { 
-        indices 
-        <Ref> { pool-name } 
-    } 
+<Polygon> name {
+    [attributes]
+    <VertexRef> {
+        indices
+        <Ref> { pool-name }
+    }
 }
 
   A polygon consists of a sequence of vertices from a single vertex
@@ -822,7 +823,7 @@ GEOMETRY ENTRIES
     used unless all vertices also have a normal.  If no normal is
     defined, none will be supplied.  The polygon normal, like the
     vertex normal, may be morphed by specifying a series of <DNormal>
-    entries.  
+    entries.
 
     The polygon normal is used only for lighting and environment
     mapping calculations, and is not related to the implicit normal
@@ -844,7 +845,7 @@ GEOMETRY ENTRIES
     disabled, and polygons are one-sided; specifying a nonzero value
     disables backface culling for this particular polygon and allows
     it to be viewed from either side.
-    
+
 
   <Scalar> bin { bin-name }
 
@@ -916,12 +917,12 @@ GEOMETRY ENTRIES
     or even within a texture.
 
 
-<Patch> name { 
-    [attributes] 
-    <VertexRef> { 
-        indices 
-        <Ref> { pool-name } 
-    } 
+<Patch> name {
+    [attributes]
+    <VertexRef> {
+        indices
+        <Ref> { pool-name }
+    }
 }
 
   A patch is similar to a polygon, but it is a special primitive that
@@ -936,12 +937,12 @@ GEOMETRY ENTRIES
   specified for Patch.
 
 
-<PointLight> name { 
-    [attributes] 
-    <VertexRef> { 
-        indices 
-        <Ref> { pool-name } 
-    } 
+<PointLight> name {
+    [attributes]
+    <VertexRef> {
+        indices
+        <Ref> { pool-name }
+    }
 }
 
   A PointLight is a set of single points.  One point is drawn for each
@@ -964,12 +965,12 @@ GEOMETRY ENTRIES
     viewer normally.
 
 
-<Line> name { 
-    [attributes] 
-    <VertexRef> { 
-        indices 
-        <Ref> { pool-name } 
-    } 
+<Line> name {
+    [attributes]
+    <VertexRef> {
+        indices
+        <Ref> { pool-name }
+    }
     [component attributes]
 }
 
@@ -985,11 +986,11 @@ GEOMETRY ENTRIES
   line segment, as in TriangleStrip, below.
 
 
-<TriangleStrip> name { 
-    [attributes] 
-    <VertexRef> { 
-        indices 
-        <Ref> { pool-name } 
+<TriangleStrip> name {
+    [attributes]
+    <VertexRef> {
+        indices
+        <Ref> { pool-name }
     }
     [component attributes]
 }
@@ -1013,7 +1014,7 @@ GEOMETRY ENTRIES
   It is possible for the individual triangles of a triangle strip to
   have a separate normal and/or color.  If so, a <Component> entry
   should be given for each so-modified triangle:
-  
+
   <Component> index {
     <RGBA> { r g b a [morph-list] }
     <Normal> { x y z [morph-list] }
@@ -1024,11 +1025,11 @@ GEOMETRY ENTRIES
   must always follow the vertex list.
 
 
-<TriangleFan> name { 
-    [attributes] 
-    <VertexRef> { 
-        indices 
-        <Ref> { pool-name } 
+<TriangleFan> name {
+    [attributes]
+    <VertexRef> {
+        indices
+        <Ref> { pool-name }
     }
     [component attributes]
 }
@@ -1057,7 +1058,7 @@ itself doesn't support them and will always create static curves and
 surfaces.  External tools like egg-qtess, however, may respect them.
 
 <NURBSCurve> {
-    [attributes] 
+    [attributes]
 
     <Order> { order }
     <Knots> { knot-list }
@@ -1083,7 +1084,7 @@ surfaces.  External tools like egg-qtess, however, may respect them.
 
   <Scalar> type { curve-type }
 
-    This defines the semanting meaning of this curve, either XYZ, HPR,
+    This defines the semantic meaning of this curve, either XYZ, HPR,
     or T.  If the type is XYZ, the curve will automatically be
     transformed between Y-up and Z-up if necessary; otherwise, it will
     be left alone.
@@ -1107,16 +1108,16 @@ surfaces.  External tools like egg-qtess, however, may respect them.
 
 
 <NURBSSurface> name {
-    [attributes] 
+    [attributes]
 
     <Order> { u-order v-order }
     <U-knots> { u-knot-list }
     <V-knots> { v-knot-list }
 
-    <VertexRef> { 
-        indices 
-        <Ref> { pool-name } 
-    } 
+    <VertexRef> {
+        indices
+        <Ref> { pool-name }
+    }
 }
 
   A NURBS surface is an extension of a NURBS curve into two parametric
@@ -1137,7 +1138,7 @@ surfaces.  External tools like egg-qtess, however, may respect them.
     These define the number of subdivisions to make in the U and V
     directions to represent the surface.  A uniform subdivision is
     always made, and trim curves are not respected (though they will
-    be drawn in if the trim curves themselves also have a subiv
+    be drawn in if the trim curves themselves also have a subdiv
     parameter).  This is only intended as a cheesy visualization.
 
 
@@ -1194,7 +1195,7 @@ surfaces.  External tools like egg-qtess, however, may respect them.
 
   Although the egg syntax supports trim curves, there are at present
   no egg processing tools that respect them.  For instance, egg-qtess
-  ignores trim curves and always tesselates the entire NURBS surface.
+  ignores trim curves and always tessellates the entire NURBS surface.
 
 
 MORPH DESCRIPTION ENTRIES
@@ -1267,7 +1268,7 @@ GROUPING ENTRIES
   attributes of the group:
 
   GROUP BINARY ATTRIBUTES
-  
+
   These attributes may be either on or off; they are off by default.
   They are turned on by specifying a non-zero "boolean-value".
 
@@ -1310,6 +1311,15 @@ GROUPING ENTRIES
     vertices; joints and morphs appearing outside of a hierarchy
     identified with a <Dart> flag are undefined.
 
+  <Dart> { structured }
+
+    This is an optional alternative for the <Dart> flag.
+    By default, Panda will collapse all of the geometry in a group (with the <Dart> { 1 } flag)
+    a single node. While this is optimal for conditions such as characters moving around
+    a scene, it may be suboptimal for larger or more complex characters.
+    This entry is typically generated by the egg-optchar program with the "-dart structured" flag.
+    <Dart> { structured } implies <Dart> { 1 }.
+
   <Switch> { boolean-value }
 
     This attribute indicates that the child nodes of this group
@@ -1446,7 +1456,7 @@ GROUPING ENTRIES
     of the billboard, not at the origin of the scene.
 
   <SwitchCondition> {
-     <Distance> { 
+     <Distance> {
         in out [fade] <Vertex> { x y z }
      }
   }
@@ -1483,7 +1493,7 @@ GROUPING ENTRIES
     Valid types so far are:
 
     Plane
-    
+
       The geometry represents an infinite plane.  The first polygon
       found in the group will define the plane.
 
@@ -1550,7 +1560,7 @@ GROUPING ENTRIES
       most compatibility.
 
     keep
- 
+
       Don't discard the visible geometry after using it to define a
       collision surface; create both an invisible collision surface
       and the visible geometry.
@@ -1619,7 +1629,8 @@ GROUPING ENTRIES
 
     There may also be additional predefined egg object types not
     listed here; see the *.pp files that are installed into the etc
-    directory for a complete list.
+    directory for a complete list. Additionally, you can reference
+    the file located in $PANDA/src/doc/howto.MultiGenModelFlags
 
   <Transform> { transform-definition }
 
@@ -1683,9 +1694,9 @@ GROUPING ENTRIES
     current group, regardless of the group in which the geometry is
     actually defined.  See the <Joint> description, below.
 
-  <AnimPreload> { 
+  <AnimPreload> {
     <Scalar> fps { float-value }
-    <Scalar> num-frames { integer-value } 
+    <Scalar> num-frames { integer-value }
   }
 
     One or more AnimPreload entries may appear within the <Group> that
@@ -1720,7 +1731,7 @@ GROUPING ENTRIES
   geometry in the scene graph.  The syntax is:
 
 <Instance> name {
-  <Ref> { group-name } 
+  <Ref> { group-name }
   [ <Ref> { group-name } ... ]
 }
 
@@ -1746,8 +1757,8 @@ GROUPING ENTRIES
   and it may contain other joints.
 
   A tree of <Joint> nodes only makes sense within a character
-  definition, which is created by applying the <DART> flag to a group.
-  See <DART>, above.
+  definition, which is created by applying the <Dart> flag to a group.
+  See <Dart>, above.
 
   The vertex assignment is crucial.  This is how the geometry of a
   character is made to move with the joints.  The character's geometry
@@ -1790,7 +1801,7 @@ GROUPING ENTRIES
   bundles, or any one of the following (<Scalar> entries are optional,
   and default as shown):
 
-  <S$Anim> name { 
+  <S$Anim> name {
       <Scalar> fps { 24 }
       <V> { values }
   }
@@ -1844,7 +1855,7 @@ GROUPING ENTRIES
     an animation sequence.
 
 
-  <VertexAnim> name { 
+  <VertexAnim> name {
       <Scalar> width { table-width }
       <Scalar> fps { 24 }
       <V> { values }
@@ -1913,6 +1924,10 @@ ANIMATION STRUCTURE
   animated model description.  Without the <Dart> flag, joints will be
   treated as ordinary groups, and morphs will be ignored.
 
+  It is important to note that utilizing <Dart> { 1 } will collapse all of the
+  model's geometry into a single node. To omit this, use <Dart> { structured } instead.
+  (See <Dart> above.)
+
   In the above, UPPERCASE NAMES represent an arbitrary name that you
   may choose.  The name of the enclosing group, CHARACTER_NAME, is
   taken as the name of the animated model.  It should generally match

+ 10 - 10
panda/src/doc/howto.MultiGenModelFlags

@@ -3,7 +3,7 @@
 
 This document describes the different kinds of model flags one can place in
 the comment field of MultiGen group beads.  The general format for a model
-flag is: 
+flag is:
                        <egg> { <FLAGNAME> {value} }
 
 The most up-to-date version of this document can be found in:
@@ -30,7 +30,7 @@ The most up-to-date version of this document can be found in:
 <egg> { <ObjectType> {camera-barrier} }      Invisible collision surface for camera and colliders
 <egg> { <ObjectType> {camera-barrier-sphere} }     Invisible sphere collision surface for camera and colliders
 <egg> { <ObjectType> {backstage} }  Modeling reference object
-<egg> { <Decal>      {1} }          Decal the node below to me 
+<egg> { <Decal>      {1} }          Decal the node below to me
                                     (like a window on a wall)
 <egg> { <Scalar> fps { # } }        Set rate of animation for a pfSequence
 
@@ -52,12 +52,12 @@ common flag/value pairs and describes what they are used for.
    which one places the flag (so names like red-hut are more useful than
    names like o34).
 
-<egg> { <Model> {1} }   
+<egg> { <Model> {1} }
 
-   Used to show/hide, change the color, or change the collision properties 
+   Used to show/hide, change the color, or change the collision properties
    of a chunk.
 
-<egg> { <DCS> {1} }     
+<egg> { <DCS> {1} }
 
    Used to move, rotate, or scale a chunk of the model.  Also can be used
    (like the <Model> flag) to show/hide, change the color, and change the
@@ -91,7 +91,7 @@ common flag/value pairs and describes what they are used for.
          a door, for example, so the player can tell when the avatar has
          moved through the door.
       -  BACKSTAGE objects are not translated over to the player.  Modelers
-         should use this flag on reference objects that they include to help 
+         should use this flag on reference objects that they include to help
          in the modeling task (such as scale references)
 
    IMPORTANT NOTE:
@@ -108,7 +108,7 @@ common flag/value pairs and describes what they are used for.
 
                      ********** PROPERTIES **********
 
-These are used to control properties of selected chunks. 
+These are used to control properties of selected chunks.
 
 <egg> { <Scalar> fps { frame-rate } }
 
@@ -124,7 +124,7 @@ These are used to control properties of selected chunks.
    Multiple Flag/value pairs can be combined within an single <egg> field.
    For example:
 
-   <egg> { <Model> {1}  
+   <egg> { <Model> {1}
            <ObjectType> {barrier} }
 
    Generally, the <Model> flag can be combined with most other flags
@@ -134,7 +134,7 @@ These are used to control properties of selected chunks.
    could also be written as:
 
    <egg>{<model>{1}<objecttype>{barrier}}
-         
+
 3) Where to place the flags
 
    All model flags except <Normal> flags are generally placed in the
@@ -154,6 +154,6 @@ These are used to control properties of selected chunks.
 4) Flags at different levels in the model
 
    Flags in lower level beads generally override flags in upper level
-   beads. 
+   beads.
 
 5) For more detailed information see $PANDA/src/doc/eggSyntax.txt.

+ 2 - 5
panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx

@@ -245,7 +245,7 @@ apply_texture(int i, TextureContext *tc, const SamplerState &sampler) {
 
   set_sampler_state(i, D3DSAMP_BORDERCOLOR, border_color);
 
-  uint aniso_degree = sampler.get_effective_anisotropic_degree();
+  unsigned int aniso_degree = sampler.get_effective_anisotropic_degree();
   SamplerState::FilterType ft = sampler.get_effective_magfilter();
 
   if (aniso_degree >= 1) {
@@ -357,10 +357,7 @@ upload_texture(DXTextureContext9 *dtc, bool force) {
       async_reload_texture(dtc);
       has_image = _supports_compressed_texture ? tex->has_ram_image() : tex->has_uncompressed_ram_image();
       if (!has_image) {
-        if (dtc->was_simple_image_modified()) {
-          return dtc->create_simple_texture(*_screen);
-        }
-        return true;
+        return dtc->create_simple_texture(*_screen);
       }
     }
   }

+ 2 - 2
panda/src/dxgsg9/dxTextureContext9.cxx

@@ -831,7 +831,7 @@ create_texture(DXScreenData &scrn) {
 
   tex->set_minfilter(ft);
 
-  uint aniso_degree;
+  unsigned int aniso_degree;
 
   aniso_degree = 1;
   if (scrn._d3dcaps.RasterCaps & D3DPRASTERCAPS_ANISOTROPY) {
@@ -1157,7 +1157,7 @@ create_simple_texture(DXScreenData &scrn) {
     goto error_exit;
   }
 
-  mark_simple_loaded();
+  mark_loaded();
   return true;
 
  error_exit:

+ 2 - 2
panda/src/dxgsg9/dxgsg9base.h

@@ -192,8 +192,8 @@ struct DXScreenData {
   HWND _window;
   HMONITOR _monitor;
   DWORD _max_available_video_memory;
-  ushort _card_id;  // adapter ID
-  ushort _depth_buffer_bitdepth;  //GetSurfaceDesc is not reliable so must store this explicitly
+  unsigned short _card_id;  // adapter ID
+  unsigned short _depth_buffer_bitdepth;  //GetSurfaceDesc is not reliable so must store this explicitly
   bool _can_direct_disable_color_writes;  // if true, don't need blending for this
   bool _is_low_memory_card;
   bool _is_tnl_device;

Some files were not shown because too many files changed in this diff