Browse Source

Merge branch 'master' into shaderpipeline

rdb 4 years ago
parent
commit
29712f9662
100 changed files with 2337 additions and 686 deletions
  1. 29 29
      .github/workflows/ci.yml
  2. 7 7
      README.md
  3. 4 0
      contrib/src/ai/aiCharacter.cxx
  4. 4 0
      contrib/src/ai/aiCharacter.h
  5. 1 11
      contrib/src/speedtree/speedTreeNode.cxx
  6. 0 1
      contrib/src/speedtree/speedTreeNode.h
  7. 2 2
      direct/src/actor/Actor.py
  8. 2 2
      direct/src/actor/__init__.py
  9. 4 3
      direct/src/cluster/ClusterClient.py
  10. 5 4
      direct/src/cluster/ClusterServer.py
  11. 1 1
      direct/src/directbase/TestStart.py
  12. 1 1
      direct/src/directbase/ThreeUpStart.py
  13. 5 3
      direct/src/directdevices/DirectJoybox.py
  14. 8 8
      direct/src/directtools/DirectCameraControl.py
  15. 1 1
      direct/src/directtools/DirectUtil.py
  16. 4 3
      direct/src/directutil/Mopath.py
  17. 6 6
      direct/src/dist/FreezeTool.py
  18. 275 0
      direct/src/dist/_android.py
  19. 21 0
      direct/src/dist/_proto/Configuration_pb2.py
  20. 4 0
      direct/src/dist/_proto/README
  21. 22 0
      direct/src/dist/_proto/Resources_pb2.py
  22. 0 0
      direct/src/dist/_proto/__init__.py
  23. 21 0
      direct/src/dist/_proto/config_pb2.py
  24. 307 0
      direct/src/dist/_proto/files_pb2.py
  25. 22 0
      direct/src/dist/_proto/targeting_pb2.py
  26. 312 38
      direct/src/dist/commands.py
  27. 39 2
      direct/src/dist/icon.py
  28. 116 0
      direct/src/dist/installers.py
  29. 2 2
      direct/src/distributed/ClientRepository.py
  30. 3 3
      direct/src/distributed/ClientRepositoryBase.py
  31. 2 1
      direct/src/distributed/DistributedSmoothNode.py
  32. 3 3
      direct/src/distributed/DoInterestManager.py
  33. 5 3
      direct/src/distributed/TimeManager.py
  34. 17 12
      direct/src/filter/CommonFilters.py
  35. 3 1
      direct/src/gui/OnscreenImage.py
  36. 4 4
      direct/src/interval/IntervalTest.py
  37. 3 2
      direct/src/leveleditor/LevelEditorBase.py
  38. 6 5
      direct/src/showbase/JobManager.py
  39. 6 4
      direct/src/showbase/PythonUtil.py
  40. 14 10
      direct/src/showbase/ShowBase.py
  41. 1 0
      direct/src/showbase/ShowBaseGlobal.py
  42. 4 3
      direct/src/showbase/TaskThreaded.py
  43. 3 3
      direct/src/task/FrameProfiler.py
  44. 5 11
      direct/src/task/Task.py
  45. 5 3
      direct/src/task/Timer.py
  46. 3 3
      direct/src/tkpanels/AnimPanel.py
  47. 4 4
      direct/src/tkpanels/MopathRecorder.py
  48. 3 2
      direct/src/tkwidgets/Dial.py
  49. 3 2
      direct/src/tkwidgets/Floater.py
  50. 10 2
      dtool/src/dtoolbase/cmath.I
  51. 3 0
      dtool/src/dtoolbase/typeHandle.h
  52. 51 0
      dtool/src/dtoolbase/typeHandle_ext.cxx
  53. 3 0
      dtool/src/dtoolbase/typeHandle_ext.h
  54. 10 10
      dtool/src/dtoolutil/filename.cxx
  55. 1 0
      dtool/src/interrogate/interfaceMakerPythonNative.cxx
  56. 0 18
      dtool/src/interrogatedb/py_compat.cxx
  57. 11 1
      dtool/src/interrogatedb/py_compat.h
  58. 10 0
      dtool/src/parser-inc/android/log.h
  59. 0 5
      dtool/src/parser-inc/stdtypedefs.h
  60. 2 0
      dtool/src/prc/CMakeLists.txt
  61. 1 1
      dtool/src/prc/androidLogStream.cxx
  62. 2 2
      dtool/src/prc/androidLogStream.h
  63. 2 1
      dtool/src/prc/configPageManager.cxx
  64. 2 0
      dtool/src/prc/configVariable.h
  65. 39 0
      dtool/src/prc/configVariable_ext.cxx
  66. 37 0
      dtool/src/prc/configVariable_ext.h
  67. 10 1
      dtool/src/prc/notify.cxx
  68. 1 0
      dtool/src/prc/p3prc_ext_composite.cxx
  69. 1 0
      makepanda/config.in
  70. 2 2
      makepanda/installpanda.py
  71. 23 17
      makepanda/makepackage.py
  72. 121 61
      makepanda/makepanda.py
  73. 85 62
      makepanda/makepandacore.py
  74. 131 27
      makepanda/makewheel.py
  75. 14 0
      panda/metalibs/pandagles2/pandagles2.cxx
  76. 31 9
      panda/src/android/PandaActivity.java
  77. 83 68
      panda/src/android/android_native_app_glue.c
  78. 2 6
      panda/src/android/android_native_app_glue.h
  79. 1 11
      panda/src/bullet/bulletDebugNode.cxx
  80. 0 1
      panda/src/bullet/bulletDebugNode.h
  81. 1 1
      panda/src/bullet/bulletWorld.cxx
  82. 5 0
      panda/src/bullet/config_bullet.cxx
  83. 1 0
      panda/src/bullet/config_bullet.h
  84. 4 1
      panda/src/chan/partBundle.cxx
  85. 0 1
      panda/src/chan/partGroup.h
  86. 126 21
      panda/src/collide/collisionLevelState.I
  87. 3 1
      panda/src/collide/collisionLevelState.h
  88. 28 3
      panda/src/collide/collisionLevelStateBase.I
  89. 5 0
      panda/src/collide/collisionLevelStateBase.h
  90. 4 21
      panda/src/collide/collisionNode.cxx
  91. 0 1
      panda/src/collide/collisionNode.h
  92. 92 99
      panda/src/collide/collisionTraverser.cxx
  93. 3 16
      panda/src/collide/collisionVisualizer.cxx
  94. 0 1
      panda/src/collide/collisionVisualizer.h
  95. 2 0
      panda/src/device/evdevInputDevice.cxx
  96. 1 1
      panda/src/device/inputDeviceManager.cxx
  97. 44 7
      panda/src/device/winRawInputDevice.cxx
  98. 3 3
      panda/src/device/winRawInputDevice.h
  99. 4 0
      panda/src/display/CMakeLists.txt
  100. 0 1
      panda/src/display/displayInformation.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

+ 7 - 7
README.md

@@ -52,9 +52,9 @@ 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 will also need to install the [Windows 10 SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk),
+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 SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-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

+ 4 - 0
contrib/src/ai/aiCharacter.cxx

@@ -117,6 +117,10 @@ NodePath AICharacter::get_char_render() {
   return _window_render;
 }
 
+std::string AICharacter::get_name() {
+  return _name;
+}
+
 void AICharacter::set_pf_guide(bool pf_guide) {
   _pf_guide = pf_guide;
 }

+ 4 - 0
contrib/src/ai/aiCharacter.h

@@ -46,6 +46,8 @@ class EXPCL_PANDAAI AICharacter : public ReferenceCount {
   void set_char_render(NodePath render);
   NodePath get_char_render();
 
+  std::string get_name();
+
 PUBLISHED:
     double get_mass();
     void set_mass(double m);
@@ -65,6 +67,8 @@ PUBLISHED:
 
     explicit AICharacter(std::string model_name, NodePath model_np, double mass, double movt_force, double max_force);
     ~AICharacter();
+
+  MAKE_PROPERTY(name, get_name);
 };
 
 #endif

+ 1 - 11
contrib/src/speedtree/speedTreeNode.cxx

@@ -1002,17 +1002,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   return true;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool SpeedTreeNode::
-is_renderable() const {
-  return true;
-}
-
 /**
  * Adds the node's contents to the CullResult we are building up during the
  * cull traversal, so that it will be drawn at render time.  For most nodes
@@ -1162,6 +1151,7 @@ set_transparent_texture_mode(SpeedTree::ETextureAlphaRenderMode eMode) const {
 void SpeedTreeNode::
 init_node() {
   PandaNode::set_cull_callback();
+  PandaNode::set_renderable();
 
   _is_valid = false;
   _needs_repopulate = false;

+ 0 - 1
contrib/src/speedtree/speedTreeNode.h

@@ -157,7 +157,6 @@ public:
                                          GeomTransformer &transformer);
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
   virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);
 
   void prepare_scene(GraphicsStateGuardianBase *gsgbase, const RenderState *net_state);

+ 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.")

+ 17 - 12
direct/src/filter/CommonFilters.py

@@ -1,21 +1,21 @@
 """
-
 Class CommonFilters implements certain common image
 postprocessing filters.  See the :ref:`common-image-filters` page for
 more information about how to use these filters.
 
-It is not ideal that these filters are all included in a single
-monolithic module.  Unfortunately, when you want to apply two filters
-at the same time, you have to compose them into a single shader, and
-the composition process isn't simply a question of concatenating them:
-you have to somehow make them work together.  I suspect that there
-exists some fairly simple framework that would make this automatable.
-However, until I write some more filters myself, I won't know what
-that framework is.  Until then, I'll settle for this
-clunky approach.  - Josh
-
+These filters are written in the Cg shading language.
 """
 
+# It is not ideal that these filters are all included in a single
+# monolithic module.  Unfortunately, when you want to apply two filters
+# at the same time, you have to compose them into a single shader, and
+# the composition process isn't simply a question of concatenating them:
+# you have to somehow make them work together.  I suspect that there
+# exists some fairly simple framework that would make this automatable.
+# However, until I write some more filters myself, I won't know what
+# that framework is.  Until then, I'll settle for this
+# clunky approach.  - Josh
+
 from panda3d.core import LVecBase4, LPoint2
 from panda3d.core import AuxBitplaneAttrib
 from panda3d.core import Texture, Shader, ATSNone
@@ -459,6 +459,11 @@ class CommonFilters:
         return True
 
     def setBloom(self, blend=(0.3,0.4,0.3,0.0), mintrigger=0.6, maxtrigger=1.0, desat=0.6, intensity=1.0, size="medium"):
+        """
+        Applies the Bloom filter to the output.
+        size can either be "off", "small", "medium", or "large".
+        Setting size to "off" will remove the Bloom filter.
+        """
         if size == 0 or size == "off":
             self.delBloom()
             return
@@ -548,7 +553,7 @@ class CommonFilters:
         return True
 
     def setBlurSharpen(self, amount=0.0):
-        """Enables the blur/sharpen filter. If the 'amount' parameter is 1.0, it will not have effect.
+        """Enables the blur/sharpen filter. If the 'amount' parameter is 1.0, it will not have any effect.
         A value of 0.0 means fully blurred, and a value higher than 1.0 sharpens the image."""
         fullrebuild = ("BlurSharpen" not in self.configuration)
         self.configuration["BlurSharpen"] = amount

+ 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

+ 10 - 10
dtool/src/dtoolutil/filename.cxx

@@ -1876,13 +1876,13 @@ open_read(std::ifstream &stream) const {
 #endif
 
   stream.clear();
-#ifdef _WIN32
+#ifdef _MSC_VER
   wstring os_specific = to_os_specific_w();
   stream.open(os_specific.c_str(), open_mode);
 #else
   string os_specific = to_os_specific();
   stream.open(os_specific.c_str(), open_mode);
-#endif  // _WIN32
+#endif  // _MSC_VER
 
   return (!stream.fail());
 }
@@ -1926,11 +1926,11 @@ open_write(std::ofstream &stream, bool truncate) const {
 #endif
 
   stream.clear();
-#ifdef _WIN32
+#ifdef _MSC_VER
   wstring os_specific = to_os_specific_w();
 #else
   string os_specific = to_os_specific();
-#endif  // _WIN32
+#endif  // _MSC_VER
   stream.open(os_specific.c_str(), open_mode);
 
   return (!stream.fail());
@@ -1958,11 +1958,11 @@ open_append(std::ofstream &stream) const {
 #endif
 
   stream.clear();
-#ifdef _WIN32
+#ifdef _MSC_VER
   wstring os_specific = to_os_specific_w();
 #else
   string os_specific = to_os_specific();
-#endif  // _WIN32
+#endif  // _MSC_VER
   stream.open(os_specific.c_str(), open_mode);
 
   return (!stream.fail());
@@ -2000,11 +2000,11 @@ open_read_write(std::fstream &stream, bool truncate) const {
 #endif
 
   stream.clear();
-#ifdef _WIN32
+#ifdef _MSC_VER
   wstring os_specific = to_os_specific_w();
 #else
   string os_specific = to_os_specific();
-#endif  // _WIN32
+#endif  // _MSC_VER
   stream.open(os_specific.c_str(), open_mode);
 
   return (!stream.fail());
@@ -2032,11 +2032,11 @@ open_read_append(std::fstream &stream) const {
 #endif
 
   stream.clear();
-#ifdef _WIN32
+#ifdef _MSC_VER
   wstring os_specific = to_os_specific_w();
 #else
   string os_specific = to_os_specific();
-#endif  // _WIN32
+#endif  // _MSC_VER
   stream.open(os_specific.c_str(), open_mode);
 
   return (!stream.fail());

+ 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

@@ -74,6 +74,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);
 };
 

+ 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 {
@@ -459,6 +460,7 @@ reload_implicit_pages() {
       }
     }
   }
+#endif  // ANDROID
 
   if (!_loaded_implicit) {
     config_initialized();
@@ -498,7 +500,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

+ 10 - 1
dtool/src/prc/notify.cxx

@@ -31,6 +31,8 @@
 
 #ifdef ANDROID
 #include <android/log.h>
+
+#include "androidLogStream.h"
 #endif
 
 using std::cerr;
@@ -345,8 +347,9 @@ assert_failure(const char *expression, int line,
 
 #ifdef ANDROID
   __android_log_assert("assert", "Panda3D", "Assertion failed: %s", message.c_str());
-#endif
+#else
   nout << "Assertion failed: " << message << "\n";
+#endif
 
   // This is redefined here, shadowing the defining in config_prc.h, so we can
   // guarantee it has already been constructed.
@@ -477,6 +480,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
     }
   }
 }

+ 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")
@@ -368,13 +371,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:
@@ -791,16 +794,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.
@@ -884,8 +879,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
@@ -911,8 +910,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)
 
@@ -1025,6 +1023,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',
@@ -1094,6 +1099,7 @@ if __name__ == "__main__":
         optimize=GetOptimize(),
         compressor=options.compressor,
         debversion=options.debversion,
+        rpmversion=options.rpmversion,
         rpmrelease=options.rpmrelease,
         python_versions=ReadPythonVersionInfoFile(),
         installdir=options.installdir,

+ 121 - 61
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
@@ -152,8 +157,8 @@ def usage(problem):
     print("  --nothing         (disable every third-party lib)")
     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("  --windows-sdk=X   (specify Windows SDK version, eg. 7.1, 8.1, 10 or 11.  Default is 8.1)")
+    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
 
@@ -181,7 +186,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",
         "glslang-incdir=", "glslang-libdir=",
@@ -230,6 +235,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
@@ -380,6 +386,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.
@@ -437,6 +446,13 @@ elif target == 'linux' and (os.path.isfile("/lib/libc-2.17.so") or os.path.isfil
     else:
         PLATFORM = 'manylinux2014-i686'
 
+elif target == 'linux' and (os.path.isfile("/lib/i386-linux-gnu/libc-2.24.so") or os.path.isfile("/lib/x86_64/libc-2.24.so")) and os.path.isdir("/opt/python"):
+    # Same sloppy check for manylinux_2_24.
+    if GetTargetArch() in ('x86_64', 'amd64'):
+        PLATFORM = 'manylinux_2_24-x86_64'
+    else:
+        PLATFORM = 'manylinux_2_24-i686'
+
 elif not CrossCompiling():
     if HasTargetArch():
         # Replace the architecture in the platform string.
@@ -991,8 +1007,11 @@ if (COMPILER=="GCC"):
         LibName("OPENSSL", "-Wl,--exclude-libs,libssl.a")
         LibName("OPENSSL", "-Wl,--exclude-libs,libcrypto.a")
 
-    if GetTarget() not in ('darwin', 'android'):
-        SmartPkgEnable("X11", "x11", "X11", ("X11", "X11/Xlib.h", "X11/XKBlib.h"))
+    if GetTarget() != 'darwin':
+        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
@@ -1056,6 +1075,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)):
@@ -1084,7 +1104,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")
@@ -1352,10 +1372,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.
@@ -1364,46 +1380,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'
 
         else:
@@ -1432,14 +1437,17 @@ 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)
-        cmd += " -fno-unsafe-math-optimizations"
+            # Make sure this is off to avoid GCC/Eigen bug (see GitHub #228)
+            cmd += " -fno-unsafe-math-optimizations"
 
         if (optlevel==1): cmd += " -ggdb -D_DEBUG"
         if (optlevel==2): cmd += " -O1 -D_DEBUG"
@@ -1892,28 +1900,16 @@ 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'
         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
@@ -2177,6 +2173,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
@@ -2288,6 +2309,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))
 
 ##########################################################################################
@@ -2882,7 +2906,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", "")
@@ -3100,8 +3129,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
@@ -4394,7 +4425,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/
@@ -4679,7 +4710,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')
@@ -4698,7 +4729,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')
@@ -4904,12 +4935,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)
@@ -4959,6 +4995,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/
 #
@@ -6055,10 +6105,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() == 'windows':
         PyTargetAdd('deploy-stubw.exe', input='deploy-stub.obj')
@@ -6070,6 +6121,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
@@ -6170,7 +6230,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
@@ -6261,8 +6321,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")

+ 85 - 62
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
@@ -80,6 +80,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"},
 }
 
 ########################################################################
@@ -108,6 +109,7 @@ MAYAVERSIONINFO = [("MAYA6",   "6.0"),
                    ("MAYA2018","2018"),
                    ("MAYA2019","2019"),
                    ("MAYA2020","2020"),
+                   ("MAYA2022","2022"),
 ]
 
 MAXVERSIONINFO = [("MAX6", "SOFTWARE\\Autodesk\\3DSMAX\\6.0", "installdir", "maxsdk\\cssdk\\include"),
@@ -242,7 +244,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 = ""
@@ -272,7 +274,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)
@@ -392,30 +394,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':
@@ -431,8 +433,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 + '-'
 
     elif target == 'linux':
@@ -497,7 +500,7 @@ def GetCXX():
 def GetStrip():
     # Hack
     if TARGET == 'android':
-        return TOOLCHAIN_PREFIX + 'strip'
+        return 'llvm-strip'
     else:
         return 'strip'
 
@@ -636,6 +639,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):
@@ -1361,7 +1365,7 @@ def GetThirdpartyDir():
             THIRDPARTYDIR = base + "/freebsd-libs-a/"
 
     elif (target == 'android'):
-        THIRDPARTYDIR = GetThirdpartyBase()+"/android-libs-%s/" % (GetTargetArch())
+        THIRDPARTYDIR = base + "/android-libs-%s/" % (target_arch)
 
     else:
         Warn("Unsupported platform:", target)
@@ -1556,8 +1560,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")):
@@ -2174,12 +2178,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)
 
@@ -2286,7 +2290,7 @@ def SdkLocateWindows(version=None):
     if version == '10':
         version = '10.0'
 
-    if version and version.startswith('10.') and version.count('.') == 1:
+    if (version and version.startswith('10.') and version.count('.') == 1) or version == '11':
         # Choose the latest version of the Windows 10 SDK.
         platsdk = GetRegistryKey("SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots", "KitsRoot10")
 
@@ -2295,7 +2299,13 @@ def SdkLocateWindows(version=None):
             platsdk = "C:\\Program Files (x86)\\Windows Kits\\10\\"
 
         if platsdk and os.path.isdir(platsdk):
+            min_version = (10, 0, 0)
+            if version == '11':
+                version = '10.0'
+                min_version = (10, 0, 22000)
+
             incdirs = glob.glob(os.path.join(platsdk, 'Include', version + '.*.*'))
+
             max_version = ()
             for dir in incdirs:
                 verstring = os.path.basename(dir)
@@ -2313,7 +2323,7 @@ def SdkLocateWindows(version=None):
                     continue
 
                 vertuple = tuple(map(int, verstring.split('.')))
-                if vertuple > max_version:
+                if vertuple > max_version and vertuple > min_version:
                     version = verstring
                     max_version = vertuple
 
@@ -2391,7 +2401,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"]
@@ -2531,19 +2541,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")
 
@@ -2760,7 +2766,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, 20348, 22000:
             version = "10.0.{0}.0".format(vnum)
             if os.path.isfile(win_kit + "Include\\" + version + "\\ucrt\\assert.h"):
                 print("Using Universal CRT %s" % (version))
@@ -2877,8 +2883,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: ='.
         cmd = GetCXX() + " -print-search-dirs" + sysroot_flag
@@ -2914,12 +2922,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)
@@ -3061,13 +3064,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)
@@ -3077,7 +3083,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)
@@ -3093,6 +3099,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)
 
@@ -3223,6 +3236,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'
@@ -3247,20 +3276,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
 
@@ -3281,13 +3297,17 @@ def SetOrigExt(x, v):
     ORIG_EXT[x] = v
 
 def GetExtensionSuffix():
-    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])
 
@@ -3315,6 +3335,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)
@@ -3413,14 +3434,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"),
     }
 
 
@@ -3431,7 +3451,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 = []
 
@@ -3451,7 +3472,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():

+ 131 - 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.
@@ -568,9 +653,16 @@ def makewheel(version, output_dir, platform=None):
                     platform = platform.replace("linux", "manylinux2010")
                 elif os.path.isfile("/lib/libc-2.17.so") or os.path.isfile("/lib64/libc-2.17.so"):
                     platform = platform.replace("linux", "manylinux2014")
+                elif os.path.isfile("/lib/i386-linux-gnu/libc-2.24.so") or os.path.isfile("/lib/x86_64/libc-2.24.so"):
+                    platform = platform.replace("linux", "manylinux_2_24")
 
     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 +670,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 +705,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 +721,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 +771,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 +781,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 +798,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 +808,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 */

+ 1 - 11
panda/src/bullet/bulletDebugNode.cxx

@@ -48,6 +48,7 @@ BulletDebugNode(const char *name) : PandaNode(name) {
   set_bounds(bounds);
   set_final(true);
   set_overall_hidden(true);
+  set_renderable();
 }
 
 /**
@@ -151,17 +152,6 @@ draw_mask_changed() {
   }
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool BulletDebugNode::
-is_renderable() const {
-  return true;
-}
-
 /**
  * Adds the node's contents to the CullResult we are building up during the
  * cull traversal, so that it will be drawn at render time.  For most nodes

+ 0 - 1
panda/src/bullet/bulletDebugNode.h

@@ -55,7 +55,6 @@ public:
   virtual bool safe_to_combine_children() const;
   virtual bool safe_to_flatten_below() const;
 
-  virtual bool is_renderable() const;
   virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);
 
 private:

+ 1 - 1
panda/src/bullet/bulletWorld.cxx

@@ -125,7 +125,7 @@ BulletWorld() {
   // Some prefered settings
   _world->getDispatchInfo().m_enableSPU = true;      // default: true
   _world->getDispatchInfo().m_useContinuous = true;  // default: true
-  _world->getSolverInfo().m_splitImpulse = false;    // default: false
+  _world->getSolverInfo().m_splitImpulse = bullet_split_impulse;
   _world->getSolverInfo().m_numIterations = bullet_solver_iterations;
 }
 

+ 5 - 0
panda/src/bullet/config_bullet.cxx

@@ -100,6 +100,11 @@ PRC_DESC("Specifies if events should be send when new contacts are "
          "contact events might create more load on the event queue "
          "then you might want! Default value is FALSE."));
 
+ConfigVariableBool bullet_split_impulse
+("bullet-split-impulse", false,
+PRC_DESC("Penetrating recovery won't add momentum. "
+         "btContactSolverInfo::m_splitImpulse. Default value is false."));
+
 ConfigVariableInt bullet_solver_iterations
 ("bullet-solver-iterations", 10,
 PRC_DESC("Specifies the number of iterations for the Bullet contact "

+ 1 - 0
panda/src/bullet/config_bullet.h

@@ -33,6 +33,7 @@ extern ConfigVariableEnum<BulletWorld::BroadphaseAlgorithm> bullet_broadphase_al
 extern ConfigVariableEnum<BulletWorld::FilterAlgorithm> bullet_filter_algorithm;
 extern ConfigVariableDouble bullet_sap_extents;
 extern ConfigVariableBool bullet_enable_contact_events;
+extern ConfigVariableBool bullet_split_impulse;
 extern ConfigVariableInt bullet_solver_iterations;
 extern ConfigVariableBool bullet_additional_damping;
 extern ConfigVariableDouble bullet_additional_damping_linear_factor;

+ 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"

+ 126 - 21
panda/src/collide/collisionLevelState.I

@@ -30,9 +30,9 @@ CollisionLevelState(const NodePath &node_path) :
  */
 template<class MaskType>
 INLINE CollisionLevelState<MaskType>::
-CollisionLevelState(const CollisionLevelState<MaskType> &parent, PandaNode *child) :
+CollisionLevelState(const CollisionLevelState<MaskType> &parent, const PandaNode::DownConnection &child, MaskType mask) :
   CollisionLevelStateBase(parent, child),
-  _current(parent._current)
+  _current(mask)
 {
 }
 #endif  // CPPPARSER
@@ -101,21 +101,21 @@ prepare_collider(const ColliderDef &def, const NodePath &root) {
 template<class MaskType>
 bool CollisionLevelState<MaskType>::
 any_in_bounds() {
-#ifndef NDEBUG
+#ifdef NDEBUG
+  const bool is_spam = false;
+#else
+  const bool is_spam = collide_cat.is_spam();
+#endif
   int indent_level = 0;
-  if (collide_cat.is_spam()) {
+  if (is_spam) {
     indent_level = _node_path.get_num_nodes() * 2;
     collide_cat.spam();
     indent(collide_cat.spam(false), indent_level)
       << "Considering " << _node_path.get_node_path() << "\n";
   }
-#endif  // NDEBUG
 
-  PandaNode *pnode = node();
-
-  CPT(BoundingVolume) node_bv = pnode->get_bounds();
-  if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-    const GeometricBoundingVolume *node_gbv = (const GeometricBoundingVolume *)node_bv.p();
+  if (_node_gbv != nullptr) {
+    PandaNode *pnode = node();
     CollideMask this_mask = pnode->get_net_collide_mask();
 
     int num_colliders = get_num_colliders();
@@ -131,13 +131,11 @@ any_in_bounds() {
           // Also don't test a node with itself, or with any of its
           // descendants.
           if (pnode == cnode) {
-#ifndef NDEBUG
-            if (collide_cat.is_spam()) {
+            if (is_spam) {
               indent(collide_cat.spam(false), indent_level)
                 << "Not comparing " << c << " to " << _node_path
                 << " (same node)\n";
             }
-#endif  // NDEBUG
 
           } else {
             // There are bits in common, and it's not the same instance, so go
@@ -148,16 +146,14 @@ any_in_bounds() {
             is_in = true;  // If there's no bounding volume, we're implicitly in.
 
             if (col_gbv != nullptr) {
-              is_in = (node_gbv->contains(col_gbv) != 0);
+              is_in = (_node_gbv->contains(col_gbv) != 0);
               _node_volume_pcollector.add_level(1);
 
-#ifndef NDEBUG
-              if (collide_cat.is_spam()) {
+              if (is_spam) {
                 indent(collide_cat.spam(false), indent_level)
                   << "Comparing " << c << ": " << *col_gbv
-                  << " to " << *node_gbv << ", is_in = " << is_in << "\n";
+                  << " to " << *_node_gbv << ", is_in = " << is_in << "\n";
               }
-#endif  // NDEBUG
             }
           }
         }
@@ -171,8 +167,7 @@ any_in_bounds() {
     }
   }
 
-#ifndef NDEBUG
-  if (collide_cat.is_spam()) {
+  if (is_spam) {
     int num_active_colliders = 0;
     int num_colliders = get_num_colliders();
     for (int c = 0; c < num_colliders; c++) {
@@ -201,11 +196,121 @@ any_in_bounds() {
     collide_cat.spam(false)
       << "\n";
   }
-#endif  // NDEBUG
   return has_any_collider();
 }
 #endif  // CPPPARSER
 
+#ifndef CPPPARSER
+/**
+ * Checks the bounding volume of the given child of the current node against
+ * each of our colliders.  Returns a mask indicating which colliders are inside
+ * of the bounding volume.
+ */
+template<class MaskType>
+MaskType CollisionLevelState<MaskType>::
+get_child_mask(const PandaNode::DownConnection &child) const {
+  PandaNode *pnode = child.get_child();
+#ifdef NDEBUG
+  const bool is_spam = false;
+#else
+  const bool is_spam = collide_cat.is_spam();
+#endif
+  int indent_level = 0;
+  if (is_spam) {
+    indent_level = (_node_path.get_num_nodes() + 1) * 2;
+    collide_cat.spam();
+    indent(collide_cat.spam(false), indent_level)
+      << "Considering " << _node_path << "/" << pnode->get_name() << "\n";
+  }
+
+  MaskType mask = _current;
+
+  const GeometricBoundingVolume *node_gbv = child.get_bounds();
+  if (node_gbv != nullptr) {
+    CollideMask node_mask = child.get_net_collide_mask();
+
+    int num_colliders = get_num_colliders();
+    for (int c = 0; c < num_colliders; c++) {
+      if (mask.get_bit(c)) {
+        CollisionNode *cnode = get_collider_node(c);
+        bool is_in = false;
+
+        // Don't even bother testing the bounding volume if there are no
+        // collide bits in common between our collider and this node.
+        CollideMask from_mask = cnode->get_from_collide_mask() & _include_mask;
+        if (!(from_mask & node_mask).is_zero()) {
+          // Also don't test a node with itself, or with any of its
+          // descendants.
+          if (pnode == cnode) {
+            if (is_spam) {
+              indent(collide_cat.spam(false), indent_level)
+                << "Not comparing " << c << " to " << _node_path << "/"
+                << pnode->get_name() << " (same node)\n";
+            }
+
+          } else {
+            // There are bits in common, and it's not the same instance, so go
+            // ahead and try the bounding volume.
+            const GeometricBoundingVolume *col_gbv =
+              get_local_bound(c);
+
+            is_in = true;  // If there's no bounding volume, we're implicitly in.
+
+            if (col_gbv != nullptr) {
+              is_in = (node_gbv->contains(col_gbv) != 0);
+              _node_volume_pcollector.add_level(1);
+
+              if (is_spam) {
+                indent(collide_cat.spam(false), indent_level)
+                  << "Comparing " << c << ": " << *col_gbv
+                  << " to " << *node_gbv << ", is_in = " << is_in << "\n";
+              }
+            }
+          }
+        }
+
+        if (!is_in) {
+          // This collider cannot intersect with any geometry at this node or
+          // below.
+          mask.clear_bit(c);
+        }
+      }
+    }
+  }
+
+  if (is_spam) {
+    int num_active_colliders = 0;
+    int num_colliders = get_num_colliders();
+    for (int c = 0; c < num_colliders; c++) {
+      if (mask.get_bit(c)) {
+        num_active_colliders++;
+      }
+    }
+
+    collide_cat.spam();
+    indent(collide_cat.spam(false), indent_level)
+      << _node_path.get_node_path() << "/" << pnode->get_name() << " has "
+      << num_active_colliders << " interested colliders";
+    if (num_colliders != 0) {
+      collide_cat.spam(false)
+        << " (";
+      for (int c = 0; c < num_colliders; c++) {
+        if (mask.get_bit(c)) {
+          CollisionNode *cnode = get_collider_node(c);
+          collide_cat.spam(false)
+            << " " << c << ". " << cnode->get_name();
+        }
+      }
+      collide_cat.spam(false)
+        << " )";
+    }
+    collide_cat.spam(false)
+      << "\n";
+  }
+  return mask;
+}
+#endif  // CPPPARSER
+
 #ifndef CPPPARSER
 /**
  * Applies the inverse transform from the current node, if any, onto all the

+ 3 - 1
panda/src/collide/collisionLevelState.h

@@ -37,7 +37,8 @@ public:
 #ifndef CPPPARSER
   INLINE CollisionLevelState(const NodePath &node_path);
   INLINE CollisionLevelState(const CollisionLevelState<MaskType> &parent,
-                             PandaNode *child);
+                             const PandaNode::DownConnection &child,
+                             MaskType mask);
   INLINE CollisionLevelState(const CollisionLevelState<MaskType> &copy);
   INLINE void operator = (const CollisionLevelState<MaskType> &copy);
 
@@ -45,6 +46,7 @@ public:
   INLINE void prepare_collider(const ColliderDef &def, const NodePath &root);
 
   bool any_in_bounds();
+  MaskType get_child_mask(const PandaNode::DownConnection &child) const;
   bool apply_transform();
 
   INLINE static bool has_max_colliders();

+ 28 - 3
panda/src/collide/collisionLevelStateBase.I

@@ -18,7 +18,8 @@ INLINE CollisionLevelStateBase::
 CollisionLevelStateBase(const NodePath &node_path) :
   _node_path(node_path),
   _colliders(get_class_type()),
-  _include_mask(CollideMask::all_on())
+  _include_mask(CollideMask::all_on()),
+  _node_gbv(node_path.node()->get_bounds()->as_geometric_bounding_volume())
 {
 }
 
@@ -30,7 +31,21 @@ CollisionLevelStateBase(const CollisionLevelStateBase &parent, PandaNode *child)
   _node_path(parent._node_path, child),
   _colliders(parent._colliders),
   _include_mask(parent._include_mask),
-  _local_bounds(parent._local_bounds)
+  _local_bounds(parent._local_bounds),
+  _node_gbv(child->get_bounds()->as_geometric_bounding_volume())
+{
+}
+
+/**
+ * This constructor goes to the next child node in the traversal.
+ */
+INLINE CollisionLevelStateBase::
+CollisionLevelStateBase(const CollisionLevelStateBase &parent, const PandaNode::DownConnection &child) :
+  _node_path(parent._node_path, child.get_child()),
+  _colliders(parent._colliders),
+  _include_mask(parent._include_mask),
+  _local_bounds(parent._local_bounds),
+  _node_gbv(child.get_bounds())
 {
 }
 
@@ -43,7 +58,8 @@ CollisionLevelStateBase(const CollisionLevelStateBase &copy) :
   _colliders(copy._colliders),
   _include_mask(copy._include_mask),
   _local_bounds(copy._local_bounds),
-  _parent_bounds(copy._parent_bounds)
+  _parent_bounds(copy._parent_bounds),
+  _node_gbv(copy._node_gbv)
 {
 }
 
@@ -57,6 +73,7 @@ operator = (const CollisionLevelStateBase &copy) {
   _include_mask = copy._include_mask;
   _local_bounds = copy._local_bounds;
   _parent_bounds = copy._parent_bounds;
+  _node_gbv = copy._node_gbv;
 }
 
 /**
@@ -113,6 +130,14 @@ get_collider_node_path(int n) const {
   return _colliders[n]._node_path;
 }
 
+/**
+ * Returns the bounding volume of the current node.
+ */
+INLINE const GeometricBoundingVolume *CollisionLevelStateBase::
+get_node_bound() const {
+  return _node_gbv;
+}
+
 /**
  * Returns the bounding volume of the indicated collider, transformed into the
  * current node's transform space.

+ 5 - 0
panda/src/collide/collisionLevelStateBase.h

@@ -52,6 +52,8 @@ public:
   INLINE CollisionLevelStateBase(const NodePath &node_path);
   INLINE CollisionLevelStateBase(const CollisionLevelStateBase &parent,
                                  PandaNode *child);
+  INLINE CollisionLevelStateBase(const CollisionLevelStateBase &parent,
+                                 const PandaNode::DownConnection &child);
   INLINE CollisionLevelStateBase(const CollisionLevelStateBase &copy);
   INLINE void operator = (const CollisionLevelStateBase &copy);
 
@@ -67,6 +69,7 @@ public:
   INLINE const CollisionSolid *get_collider(int n) const;
   INLINE CollisionNode *get_collider_node(int n) const;
   INLINE NodePath get_collider_node_path(int n) const;
+  INLINE const GeometricBoundingVolume *get_node_bound() const;
   INLINE const GeometricBoundingVolume *get_local_bound(int n) const;
   INLINE const GeometricBoundingVolume *get_parent_bound(int n) const;
 
@@ -80,6 +83,8 @@ protected:
   Colliders _colliders;
   CollideMask _include_mask;
 
+  const GeometricBoundingVolume *_node_gbv = nullptr;
+
   typedef PTA(CPT(GeometricBoundingVolume)) BoundingVolumes;
   BoundingVolumes _local_bounds;
   BoundingVolumes _parent_bounds;

+ 4 - 21
panda/src/collide/collisionNode.cxx

@@ -43,6 +43,7 @@ CollisionNode(const std::string &name) :
   _collider_sort(0)
 {
   set_cull_callback();
+  set_renderable();
 
   // CollisionNodes are hidden by default.
   set_overall_hidden(true);
@@ -186,11 +187,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
     CPT(CollisionSolid) solid = (*si).get_read_pointer();
     PT(PandaNode) node = solid->get_viz(trav, data, false);
     if (node != nullptr) {
-      CullTraverserData next_data(data, node);
-
       // We don't want to inherit the render state from above for these guys.
-      next_data._state = RenderState::make_empty();
-      trav->traverse(next_data);
+      trav->traverse_down(data, node, data._net_transform, RenderState::make_empty());
     }
   }
 
@@ -208,12 +206,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
         CPT(CollisionSolid) solid = (*si).get_read_pointer();
         PT(PandaNode) node = solid->get_viz(trav, data, false);
         if (node != nullptr) {
-          CullTraverserData next_data(data, node);
-
-          next_data._net_transform =
-            next_data._net_transform->compose(transform);
-          next_data._state = get_last_pos_state();
-          trav->traverse(next_data);
+          trav->traverse_down(data, node,
+            data._net_transform->compose(transform), get_last_pos_state());
         }
       }
     }
@@ -223,17 +217,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   return true;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool CollisionNode::
-is_renderable() const {
-  return true;
-}
-
 /**
  * A simple downcast check.  Returns true if this kind of node happens to
  * inherit from CollisionNode, false otherwise.

+ 0 - 1
panda/src/collide/collisionNode.h

@@ -43,7 +43,6 @@ public:
   virtual CollideMask get_legal_collide_mask() const;
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
   virtual bool is_collision_node() const;
 
   virtual void output(std::ostream &out) const;

+ 92 - 99
panda/src/collide/collisionTraverser.cxx

@@ -288,7 +288,9 @@ traverse(const NodePath &root) {
 #ifdef DO_PSTATS
         PStatTimer pass_timer(get_pass_collector(pass));
 #endif
-        r_traverse_single(level_states[pass], pass);
+        if (level_states[pass].any_in_bounds()) {
+          r_traverse_single(level_states[pass], pass);
+        }
       }
     }
   }
@@ -562,22 +564,13 @@ prepare_colliders_single(CollisionTraverser::LevelStatesSingle &level_states,
  */
 void CollisionTraverser::
 r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
-  if (!level_state.any_in_bounds()) {
-    return;
-  }
   if (!level_state.apply_transform()) {
     return;
   }
 
   PandaNode *node = level_state.node();
   if (node->is_collision_node()) {
-    CollisionNode *cnode;
-    DCAST_INTO_V(cnode, node);
-    CPT(BoundingVolume) node_bv = cnode->get_bounds();
-    const GeometricBoundingVolume *node_gbv = nullptr;
-    if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-      DCAST_INTO_V(node_gbv, node_bv);
-    }
+    CollisionNode *cnode = (CollisionNode *)node;
 
     CollisionEntry entry;
     entry._into_node = cnode;
@@ -589,13 +582,14 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
     int num_colliders = level_state.get_num_colliders();
     for (int c = 0; c < num_colliders; ++c) {
       if (level_state.has_collider(c)) {
-        entry._from_node = level_state.get_collider_node(c);
+        CollisionNode *from_node = level_state.get_collider_node(c);
 
-        if ((entry._from_node->get_from_collide_mask() &
+        if ((from_node->get_from_collide_mask() &
              cnode->get_into_collide_mask()) != 0) {
           #ifdef DO_PSTATS
           // PStatTimer collide_timer(_solid_collide_collectors[pass]);
           #endif
+          entry._from_node = from_node;
           entry._from_node_path = level_state.get_collider_node_path(c);
           entry._from = level_state.get_collider(c);
 
@@ -603,7 +597,7 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
               entry,
               level_state.get_parent_bound(c),
               level_state.get_local_bound(c),
-              node_gbv);
+              level_state.get_node_bound());
         }
       }
     }
@@ -616,13 +610,7 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
     }
     #endif
 
-    GeomNode *gnode;
-    DCAST_INTO_V(gnode, node);
-    CPT(BoundingVolume) node_bv = gnode->get_bounds();
-    const GeometricBoundingVolume *node_gbv = nullptr;
-    if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-      DCAST_INTO_V(node_gbv, node_bv);
-    }
+    GeomNode *gnode = (GeomNode *)node;
 
     CollisionEntry entry;
     entry._into_node = gnode;
@@ -648,7 +636,7 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
               entry,
               level_state.get_parent_bound(c),
               level_state.get_local_bound(c),
-              node_gbv);
+              level_state.get_node_bound());
         }
       }
     }
@@ -658,9 +646,14 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
     // If it's a switch node or sequence node, visit just the one visible
     // child.
     int index = node->get_visible_child();
-    if (index >= 0 && index < node->get_num_children()) {
-      CollisionLevelStateSingle next_state(level_state, node->get_child(index));
-      r_traverse_single(next_state, pass);
+    PandaNode::Children children = node->get_children();
+    if (index >= 0 && index < children.get_num_children()) {
+      const PandaNode::DownConnection &child = children.get_child_connection(index);
+      CollisionLevelStateSingle::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateSingle next_state(level_state, child, mask);
+        r_traverse_single(next_state, pass);
+      }
     }
 
   } else if (node->is_lod_node()) {
@@ -673,12 +666,16 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
-      CollisionLevelStateSingle next_state(level_state, children.get_child(i));
-      if (i != index) {
-        next_state.set_include_mask(next_state.get_include_mask() &
-          ~GeomNode::get_default_collide_mask());
+      const PandaNode::DownConnection &child = children.get_child_connection(i);
+      CollisionLevelStateSingle::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateSingle next_state(level_state, child, mask);
+        if (i != index) {
+          next_state.set_include_mask(next_state.get_include_mask() &
+            ~GeomNode::get_default_collide_mask());
+        }
+        r_traverse_single(next_state, pass);
       }
-      r_traverse_single(next_state, pass);
     }
 
   } else {
@@ -686,8 +683,12 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
-      CollisionLevelStateSingle next_state(level_state, children.get_child(i));
-      r_traverse_single(next_state, pass);
+      const PandaNode::DownConnection &child = children.get_child_connection(i);
+      CollisionLevelStateSingle::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateSingle next_state(level_state, child, mask);
+        r_traverse_single(next_state, pass);
+      }
     }
   }
 }
@@ -773,22 +774,13 @@ prepare_colliders_double(CollisionTraverser::LevelStatesDouble &level_states,
  */
 void CollisionTraverser::
 r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
-  if (!level_state.any_in_bounds()) {
-    return;
-  }
   if (!level_state.apply_transform()) {
     return;
   }
 
   PandaNode *node = level_state.node();
   if (node->is_collision_node()) {
-    CollisionNode *cnode;
-    DCAST_INTO_V(cnode, node);
-    CPT(BoundingVolume) node_bv = cnode->get_bounds();
-    const GeometricBoundingVolume *node_gbv = nullptr;
-    if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-      DCAST_INTO_V(node_gbv, node_bv);
-    }
+    CollisionNode *cnode = (CollisionNode *)node;
 
     CollisionEntry entry;
     entry._into_node = cnode;
@@ -814,7 +806,7 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
               entry,
               level_state.get_parent_bound(c),
               level_state.get_local_bound(c),
-              node_gbv);
+              level_state.get_node_bound());
         }
       }
     }
@@ -827,13 +819,7 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
     }
     #endif
 
-    GeomNode *gnode;
-    DCAST_INTO_V(gnode, node);
-    CPT(BoundingVolume) node_bv = gnode->get_bounds();
-    const GeometricBoundingVolume *node_gbv = nullptr;
-    if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-      DCAST_INTO_V(node_gbv, node_bv);
-    }
+    GeomNode *gnode = (GeomNode *)node;
 
     CollisionEntry entry;
     entry._into_node = gnode;
@@ -859,7 +845,7 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
               entry,
               level_state.get_parent_bound(c),
               level_state.get_local_bound(c),
-              node_gbv);
+              level_state.get_node_bound());
         }
       }
     }
@@ -869,9 +855,14 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
     // If it's a switch node or sequence node, visit just the one visible
     // child.
     int index = node->get_visible_child();
-    if (index >= 0 && index < node->get_num_children()) {
-      CollisionLevelStateDouble next_state(level_state, node->get_child(index));
-      r_traverse_double(next_state, pass);
+    PandaNode::Children children = node->get_children();
+    if (index >= 0 && index < children.get_num_children()) {
+      const PandaNode::DownConnection &child = children.get_child_connection(index);
+      CollisionLevelStateDouble::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateDouble next_state(level_state, child, mask);
+        r_traverse_double(next_state, pass);
+      }
     }
 
   } else if (node->is_lod_node()) {
@@ -880,16 +871,20 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
     // visit all other levels without GeomNode::get_default_collide_mask(),
     // allowing only collision with CollisionNodes and special geometry under
     // higher levels of detail.
-    int index = DCAST(LODNode, node)->get_lowest_switch();
+    int index = ((LODNode *)node)->get_lowest_switch();
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
-      CollisionLevelStateDouble next_state(level_state, children.get_child(i));
-      if (i != index) {
-        next_state.set_include_mask(next_state.get_include_mask() &
-          ~GeomNode::get_default_collide_mask());
+      const PandaNode::DownConnection &child = children.get_child_connection(i);
+      CollisionLevelStateDouble::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateDouble next_state(level_state, child, mask);
+        if (i != index) {
+          next_state.set_include_mask(next_state.get_include_mask() &
+            ~GeomNode::get_default_collide_mask());
+        }
+        r_traverse_double(next_state, pass);
       }
-      r_traverse_double(next_state, pass);
     }
 
   } else {
@@ -897,8 +892,12 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
-      CollisionLevelStateDouble next_state(level_state, children.get_child(i));
-      r_traverse_double(next_state, pass);
+      const PandaNode::DownConnection &child = children.get_child_connection(i);
+      CollisionLevelStateDouble::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateDouble next_state(level_state, child, mask);
+        r_traverse_double(next_state, pass);
+      }
     }
   }
 }
@@ -984,22 +983,13 @@ prepare_colliders_quad(CollisionTraverser::LevelStatesQuad &level_states,
  */
 void CollisionTraverser::
 r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
-  if (!level_state.any_in_bounds()) {
-    return;
-  }
   if (!level_state.apply_transform()) {
     return;
   }
 
   PandaNode *node = level_state.node();
   if (node->is_collision_node()) {
-    CollisionNode *cnode;
-    DCAST_INTO_V(cnode, node);
-    CPT(BoundingVolume) node_bv = cnode->get_bounds();
-    const GeometricBoundingVolume *node_gbv = nullptr;
-    if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-      DCAST_INTO_V(node_gbv, node_bv);
-    }
+    CollisionNode *cnode = (CollisionNode *)node;
 
     CollisionEntry entry;
     entry._into_node = cnode;
@@ -1025,7 +1015,7 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
               entry,
               level_state.get_parent_bound(c),
               level_state.get_local_bound(c),
-              node_gbv);
+              level_state.get_node_bound());
         }
       }
     }
@@ -1038,13 +1028,7 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
     }
     #endif
 
-    GeomNode *gnode;
-    DCAST_INTO_V(gnode, node);
-    CPT(BoundingVolume) node_bv = gnode->get_bounds();
-    const GeometricBoundingVolume *node_gbv = nullptr;
-    if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-      DCAST_INTO_V(node_gbv, node_bv);
-    }
+    GeomNode *gnode = (GeomNode *)node;
 
     CollisionEntry entry;
     entry._into_node = gnode;
@@ -1070,7 +1054,7 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
               entry,
               level_state.get_parent_bound(c),
               level_state.get_local_bound(c),
-              node_gbv);
+              level_state.get_node_bound());
         }
       }
     }
@@ -1080,9 +1064,14 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
     // If it's a switch node or sequence node, visit just the one visible
     // child.
     int index = node->get_visible_child();
-    if (index >= 0 && index < node->get_num_children()) {
-      CollisionLevelStateQuad next_state(level_state, node->get_child(index));
-      r_traverse_quad(next_state, pass);
+    PandaNode::Children children = node->get_children();
+    if (index >= 0 && index < children.get_num_children()) {
+      const PandaNode::DownConnection &child = children.get_child_connection(index);
+      CollisionLevelStateQuad::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateQuad next_state(level_state, child, mask);
+        r_traverse_quad(next_state, pass);
+      }
     }
 
   } else if (node->is_lod_node()) {
@@ -1091,16 +1080,20 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
     // visit all other levels without GeomNode::get_default_collide_mask(),
     // allowing only collision with CollisionNodes and special geometry under
     // higher levels of detail.
-    int index = DCAST(LODNode, node)->get_lowest_switch();
+    int index = ((LODNode *)node)->get_lowest_switch();
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
-      CollisionLevelStateQuad next_state(level_state, children.get_child(i));
-      if (i != index) {
-        next_state.set_include_mask(next_state.get_include_mask() &
-          ~GeomNode::get_default_collide_mask());
+      const PandaNode::DownConnection &child = children.get_child_connection(i);
+      CollisionLevelStateQuad::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateQuad next_state(level_state, child, mask);
+        if (i != index) {
+          next_state.set_include_mask(next_state.get_include_mask() &
+            ~GeomNode::get_default_collide_mask());
+        }
+        r_traverse_quad(next_state, pass);
       }
-      r_traverse_quad(next_state, pass);
     }
 
   } else {
@@ -1108,8 +1101,12 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
-      CollisionLevelStateQuad next_state(level_state, children.get_child(i));
-      r_traverse_quad(next_state, pass);
+      const PandaNode::DownConnection &child = children.get_child_connection(i);
+      CollisionLevelStateQuad::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateQuad next_state(level_state, child, mask);
+        r_traverse_quad(next_state, pass);
+      }
     }
   }
 }
@@ -1162,10 +1159,7 @@ compare_collider_to_node(CollisionEntry &entry,
         // CollisionNodes.  We are already filtering out tests for a
         // CollisionNode into itself.
         CPT(BoundingVolume) solid_bv = entry._into->get_bounds();
-        const GeometricBoundingVolume *solid_gbv = nullptr;
-        if (solid_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-          solid_gbv = (const GeometricBoundingVolume *)solid_bv.p();
-        }
+        const GeometricBoundingVolume *solid_gbv = solid_bv->as_geometric_bounding_volume();
 
         compare_collider_to_solid(entry, from_node_gbv, solid_gbv);
       }
@@ -1198,14 +1192,13 @@ compare_collider_to_geom_node(CollisionEntry &entry,
       if (geom != nullptr) {
         CPT(BoundingVolume) geom_bv = geom->get_bounds();
         const GeometricBoundingVolume *geom_gbv = nullptr;
-        if (num_geoms > 1 &&
-            geom_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
+        if (num_geoms > 1) {
           // Only bother to test against each geom's bounding volume if we
           // have more than one geom in the node, as a slight optimization.
           // (If the node contains just one geom, then the node's bounding
           // volume, which we just tested, is the same as the geom's bounding
           // volume.)
-          DCAST_INTO_V(geom_gbv, geom_bv);
+          geom_gbv = geom_bv->as_geometric_bounding_volume();
         }
 
         compare_collider_to_geom(entry, geom, from_node_gbv, geom_gbv);

+ 3 - 16
panda/src/collide/collisionVisualizer.cxx

@@ -43,6 +43,7 @@ TypeHandle CollisionVisualizer::_type_handle;
 CollisionVisualizer::
 CollisionVisualizer(const std::string &name) : PandaNode(name), _lock("CollisionVisualizer") {
   set_cull_callback();
+  set_renderable();
 
   // We always want to render the CollisionVisualizer node itself (even if it
   // doesn't appear to have any geometry within it).
@@ -62,6 +63,7 @@ CollisionVisualizer(const CollisionVisualizer &copy) :
   _normal_scale(copy._normal_scale) {
 
   set_cull_callback();
+  set_renderable();
 
   // We always want to render the CollisionVisualizer node itself (even if it
   // doesn't appear to have any geometry within it).
@@ -145,12 +147,9 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
       bool was_detected = (solid_info._detected_count > 0);
       PT(PandaNode) node = solid->get_viz(trav, xform_data, !was_detected);
       if (node != nullptr) {
-        CullTraverserData next_data(xform_data, node);
-
         // We don't want to inherit the render state from above for these
         // guys.
-        next_data._state = get_viz_state();
-        trav->traverse(next_data);
+        trav->traverse_down(xform_data, node, xform_data._net_transform, get_viz_state());
       }
     }
 
@@ -251,18 +250,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   return true;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool CollisionVisualizer::
-is_renderable() const {
-  return true;
-}
-
-
 /**
  * Writes a brief description of the node to the indicated output stream.
  * This is invoked by the << operator.  It may be overridden in derived

+ 0 - 1
panda/src/collide/collisionVisualizer.h

@@ -50,7 +50,6 @@ public:
   // from parent class PandaNode.
   virtual PandaNode *make_copy() const;
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
   virtual void output(std::ostream &out) const;
 
   // from parent class CollisionRecorder.

+ 2 - 0
panda/src/device/evdevInputDevice.cxx

@@ -113,6 +113,8 @@ static const struct DeviceMapping {
   {0x046d, 0xc629, InputDevice::DeviceClass::spatial_mouse, 0},
   // 3Dconnexion Space Mouse Pro
   {0x046d, 0xc62b, InputDevice::DeviceClass::spatial_mouse, 0},
+  // FrSky Simulator
+  {0x0483, 0x5720, InputDevice::DeviceClass::flight_stick, 0},
   {0},
 };
 

+ 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;

+ 44 - 7
panda/src/device/winRawInputDevice.cxx

@@ -85,6 +85,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::flight_stick, 0,
+    {0}
+  },
   {0},
 };
 
@@ -401,7 +405,8 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
           << ", UsagePage=0x" << hex << cap.UsagePage
           << ", Usage=0x" << cap.Range.UsageMin << "..0x" << cap.Range.UsageMax
           << dec << ", LogicalMin=" << cap.LogicalMin
-          << ", LogicalMax=" << cap.LogicalMax << "\n";
+          << ", LogicalMax=" << cap.LogicalMax
+          << ", BitSize=" << cap.BitSize << "\n";
       }
     } else {
       if (device_cat.is_debug()) {
@@ -411,7 +416,8 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
           << ", UsagePage=0x" << hex << cap.UsagePage
           << ", Usage=0x" << cap.NotRange.Usage
           << dec << ", LogicalMin=" << cap.LogicalMin
-          << ", LogicalMax=" << cap.LogicalMax << "\n";
+          << ", LogicalMax=" << cap.LogicalMax
+          << ", BitSize=" << cap.BitSize << "\n";
       }
     }
 
@@ -424,7 +430,7 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
 
       // My gamepads give this odd invalid range.
       if (cap.LogicalMin == 0 && cap.LogicalMax == -1) {
-        cap.LogicalMax = 65535;
+        cap.LogicalMax = (1 << cap.BitSize) - 1;
         is_signed = false;
       }
 
@@ -558,6 +564,17 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
         }
       }
 
+      int sign_bit = 0;
+      if (cap.BitSize < 32) {
+        if (cap.LogicalMin < 0) {
+          sign_bit = 1 << (cap.BitSize - 1);
+        }
+        else if (is_signed) {
+          //XXX is this still necessary?
+          sign_bit = (1 << 15);
+        }
+      }
+
       int axis_index;
       if (!is_signed) {
         // All axes on the weird XInput-style mappings go from -1 to 1
@@ -565,7 +582,7 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
       } else {
         axis_index = add_axis(axis, cap.LogicalMin, cap.LogicalMax);
       }
-      _indices[data_index] = Index::axis(axis_index, is_signed);
+      _indices[data_index] = Index::axis(axis_index, sign_bit);
     }
   }
 
@@ -663,11 +680,31 @@ 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) {
-            axis_changed(idx._axis, (SHORT)data[di].RawValue);
+          if (idx._sign_bit != 0) {
+            // Sign extend.
+            int value = data[di].RawValue;
+            if (value & idx._sign_bit) {
+              value -= (idx._sign_bit << 1);
+            }
+            axis_changed(idx._axis, value);
           } else {
             axis_changed(idx._axis, data[di].RawValue);
           }

+ 3 - 3
panda/src/device/winRawInputDevice.h

@@ -59,16 +59,16 @@ private:
       idx._button = index;
       return idx;
     }
-    static Index axis(int index, bool is_signed=true) {
+    static Index axis(int index, int sign_bit = 0) {
       Index idx;
       idx._axis = index;
-      idx._signed = is_signed;
+      idx._sign_bit = sign_bit;
       return idx;
     }
 
     int _button;
     int _axis;
-    bool _signed;
+    int _sign_bit;
   };
 
   // Maps a "data index" to either button index or axis index.

+ 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 {

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