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
         - ubuntu-bionic-coverage-ninja
         - macos-eigen-coverage-unity-xcode
         - macos-eigen-coverage-unity-xcode
         - macos-nometa-standard-makefile
         - 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
         - windows-nopython-nometa-standard-msvc
 
 
         include:
         include:
@@ -40,7 +40,7 @@ jobs:
           eigen: NO
           eigen: NO
 
 
         - profile: macos-eigen-coverage-unity-xcode
         - profile: macos-eigen-coverage-unity-xcode
-          os: macOS-latest
+          os: macOS-10.15
           config: Coverage
           config: Coverage
           unity: YES
           unity: YES
           generator: Xcode
           generator: Xcode
@@ -50,7 +50,7 @@ jobs:
           eigen: YES
           eigen: YES
 
 
         - profile: macos-nometa-standard-makefile
         - profile: macos-nometa-standard-makefile
-          os: macOS-latest
+          os: macOS-10.15
           config: Standard
           config: Standard
           unity: NO
           unity: NO
           generator: Unix Makefiles
           generator: Unix Makefiles
@@ -60,20 +60,20 @@ jobs:
           eigen: NO
           eigen: NO
 
 
         - profile: windows-standard-unity-msvc
         - profile: windows-standard-unity-msvc
-          os: windows-2019
+          os: windows-2022
           config: Standard
           config: Standard
           unity: YES
           unity: YES
-          generator: Visual Studio 16 2019
+          generator: Visual Studio 17 2022
           compiler: Default
           compiler: Default
           metalibs: YES
           metalibs: YES
           python: YES
           python: YES
           eigen: NO
           eigen: NO
 
 
         - profile: windows-nopython-nometa-standard-msvc
         - profile: windows-nopython-nometa-standard-msvc
-          os: windows-2019
+          os: windows-2022
           config: Standard
           config: Standard
           unity: NO
           unity: NO
-          generator: Visual Studio 16 2019
+          generator: Visual Studio 17 2022
           compiler: Default
           compiler: Default
           metalibs: NO
           metalibs: NO
           python: NO
           python: NO
@@ -92,10 +92,10 @@ jobs:
     - name: Install dependencies (macOS)
     - name: Install dependencies (macOS)
       if: runner.os == 'macOS'
       if: runner.os == 'macOS'
       run: |
       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.
         # 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
         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
       uses: actions/cache@v1
       with:
       with:
         path: thirdparty
         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)
     - name: Install dependencies (Windows)
       if: runner.os == 'Windows'
       if: runner.os == 'Windows'
       shell: powershell
       shell: powershell
       run: |
       run: |
         if (!(Test-Path thirdparty/win-libs-vc14-x64)) {
         if (!(Test-Path thirdparty/win-libs-vc14-x64)) {
           $wc = New-Object System.Net.WebClient
           $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
           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)
     - name: ccache (non-Windows)
@@ -153,7 +153,7 @@ jobs:
         cd build
         cd build
 
 
         if ${{ matrix.compiler == 'Clang' }}; then
         if ${{ matrix.compiler == 'Clang' }}; then
-          if [[ "$CMAKE_GENERATOR" == *Studio*2019* ]]; then
+          if [[ "$CMAKE_GENERATOR" =~ Studio.+20(19|22) ]]; then
             export CMAKE_GENERATOR_TOOLSET=ClangCL
             export CMAKE_GENERATOR_TOOLSET=ClangCL
           elif [[ "$CMAKE_GENERATOR" == *Studio* ]]; then
           elif [[ "$CMAKE_GENERATOR" == *Studio* ]]; then
             export CMAKE_GENERATOR_TOOLSET=LLVM
             export CMAKE_GENERATOR_TOOLSET=LLVM
@@ -186,7 +186,7 @@ jobs:
 
 
     - name: Setup Python (Python 3.6)
     - name: Setup Python (Python 3.6)
       if: contains(matrix.python, 'YES')
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
       with:
         python-version: 3.6
         python-version: 3.6
     - name: Configure (Python 3.6)
     - name: Configure (Python 3.6)
@@ -218,7 +218,7 @@ jobs:
 
 
     - name: Setup Python (Python 3.7)
     - name: Setup Python (Python 3.7)
       if: contains(matrix.python, 'YES')
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
       with:
         python-version: 3.7
         python-version: 3.7
     - name: Configure (Python 3.7)
     - name: Configure (Python 3.7)
@@ -250,7 +250,7 @@ jobs:
 
 
     - name: Setup Python (Python 3.8)
     - name: Setup Python (Python 3.8)
       if: contains(matrix.python, 'YES')
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
       with:
         python-version: 3.8
         python-version: 3.8
     - name: Configure (Python 3.8)
     - name: Configure (Python 3.8)
@@ -282,7 +282,7 @@ jobs:
 
 
     - name: Setup Python (Python 3.9)
     - name: Setup Python (Python 3.9)
       if: contains(matrix.python, 'YES')
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
       with:
         python-version: 3.9
         python-version: 3.9
     - name: Configure (Python 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]')"
     if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
     strategy:
     strategy:
       matrix:
       matrix:
-        os: [ubuntu-18.04, windows-2016, macOS-latest]
+        os: [ubuntu-18.04, windows-2016, macOS-11]
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     steps:
     steps:
     - uses: actions/checkout@v1
     - uses: actions/checkout@v1
@@ -347,19 +347,19 @@ jobs:
       shell: powershell
       shell: powershell
       run: |
       run: |
         $wc = New-Object System.Net.WebClient
         $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
         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)
     - name: Get thirdparty packages (macOS)
       if: runner.os == 'macOS'
       if: runner.os == 'macOS'
       run: |
       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)
         (cd thirdparty/darwin-libs-a && rm -rf rocket)
     - name: Set up Python 3.9
     - name: Set up Python 3.9
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
       with:
         python-version: 3.9
         python-version: 3.9
     - name: Build Python 3.9
     - name: Build Python 3.9
@@ -372,7 +372,7 @@ jobs:
         python -m pip install pytest
         python -m pip install pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
     - name: Set up Python 3.8
     - name: Set up Python 3.8
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
       with:
         python-version: 3.8
         python-version: 3.8
     - name: Build Python 3.8
     - name: Build Python 3.8
@@ -385,7 +385,7 @@ jobs:
         python -m pip install pytest
         python -m pip install pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
     - name: Set up Python 3.7
     - name: Set up Python 3.7
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
       with:
         python-version: 3.7
         python-version: 3.7
     - name: Build Python 3.7
     - name: Build Python 3.7

+ 7 - 7
README.md

@@ -52,9 +52,9 @@ Building Panda3D
 Windows
 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
 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).
 [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
 After acquiring these dependencies, you can build Panda3D from the command
 prompt using the following command.  Change the `--msvc-version` option based
 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
 ```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
 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;
   return _window_render;
 }
 }
 
 
+std::string AICharacter::get_name() {
+  return _name;
+}
+
 void AICharacter::set_pf_guide(bool pf_guide) {
 void AICharacter::set_pf_guide(bool pf_guide) {
   _pf_guide = 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);
   void set_char_render(NodePath render);
   NodePath get_char_render();
   NodePath get_char_render();
 
 
+  std::string get_name();
+
 PUBLISHED:
 PUBLISHED:
     double get_mass();
     double get_mass();
     void set_mass(double m);
     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);
     explicit AICharacter(std::string model_name, NodePath model_np, double mass, double movt_force, double max_force);
     ~AICharacter();
     ~AICharacter();
+
+  MAKE_PROPERTY(name, get_name);
 };
 };
 
 
 #endif
 #endif

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

@@ -1002,17 +1002,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   return true;
   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
  * 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
  * 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::
 void SpeedTreeNode::
 init_node() {
 init_node() {
   PandaNode::set_cull_callback();
   PandaNode::set_cull_callback();
+  PandaNode::set_renderable();
 
 
   _is_valid = false;
   _is_valid = false;
   _needs_repopulate = false;
   _needs_repopulate = false;

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

@@ -157,7 +157,6 @@ public:
                                          GeomTransformer &transformer);
                                          GeomTransformer &transformer);
 
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
   virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);
   virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);
 
 
   void prepare_scene(GraphicsStateGuardianBase *gsgbase, const RenderState *net_state);
   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.
 """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']
 __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
 It loads and controls an animated character and manages the animations
 playing on it.
 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)
         taskMgr.add(self.synchronizeTimeTask, "synchronizeTimeTask", -40)
 
 
     def synchronizeTimeTask(self, task):
     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:
         for server in self.serverList:
             server.sendTimeData(frameCount, frameTime, dt)
             server.sendTimeData(frameCount, frameTime, dt)
         return Task.cont
         return Task.cont

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

@@ -52,7 +52,7 @@ class ClusterServer(DirectObject.DirectObject):
             self.startSwapCoordinator()
             self.startSwapCoordinator()
             base.graphicsEngine.setAutoFlip(0)
             base.graphicsEngine.setAutoFlip(0)
         # Set global clock mode to slave mode
         # Set global clock mode to slave mode
-        globalClock.setMode(ClockObject.MSlave)
+        ClockObject.getGlobalClock().setMode(ClockObject.MSlave)
         # Send verification of startup to client
         # Send verification of startup to client
         self.daemon = DirectD()
         self.daemon = DirectD()
 
 
@@ -335,9 +335,10 @@ class ClusterServer(DirectObject.DirectObject):
         """ Update cameraJig position to reflect latest position """
         """ Update cameraJig position to reflect latest position """
         (frameCount, frameTime, dt) = self.msgHandler.parseTimeDataDatagram(dgi)
         (frameCount, frameTime, dt) = self.msgHandler.parseTimeDataDatagram(dgi)
         # Use frame time from client for both real and frame time
         # 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):
     def handleCommandString(self, dgi):
         """ Handle arbitrary command string from client """
         """ 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.setFov(52.0)
 base.camLens.setNearFar(1.0, 10000.0)
 base.camLens.setNearFar(1.0, 10000.0)
 
 
-globalClock.setMaxDt(0.2)
+base.clock.setMaxDt(0.2)
 base.enableParticles()
 base.enableParticles()
 
 
 # Force the screen to update:
 # 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.setFov(52.0)
 base.camLens.setNearFar(1.0, 10000.0)
 base.camLens.setNearFar(1.0, 10000.0)
 
 
-globalClock.setMaxDt(0.2)
+base.clock.setMaxDt(0.2)
 base.enableParticles()
 base.enableParticles()
 base.addAngularIntegrator()
 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.gui import OnscreenText
 from direct.task import Task
 from direct.task import Task
 from direct.task.TaskManagerGlobal import taskMgr
 from direct.task.TaskManagerGlobal import taskMgr
+from panda3d.core import ClockObject
+
 import math
 import math
 
 
 #TODO: Handle interaction between widget, followSelectedTask and updateTask
 #TODO: Handle interaction between widget, followSelectedTask and updateTask
@@ -58,7 +60,7 @@ class DirectJoybox(DirectObject):
                         R_TWIST, L_TWIST, NULL_AXIS]
                         R_TWIST, L_TWIST, NULL_AXIS]
         self.modifier = [1, 1, 1, -1, -1, 0]
         self.modifier = [1, 1, 1, -1, -1, 0]
         # Initialize time
         # Initialize time
-        self.lastTime = globalClock.getFrameTime()
+        self.lastTime = ClockObject.getGlobalClock().getFrameTime()
         # Record node path
         # Record node path
         self.nodePath = nodePath
         self.nodePath = nodePath
         self.headingNP = headingNP
         self.headingNP = headingNP
@@ -148,7 +150,7 @@ class DirectJoybox(DirectObject):
 
 
     def updateVals(self):
     def updateVals(self):
         # Update delta time
         # Update delta time
-        cTime = globalClock.getFrameTime()
+        cTime = ClockObject.getGlobalClock().getFrameTime()
         self.deltaTime = cTime - self.lastTime
         self.deltaTime = cTime - self.lastTime
         self.lastTime = cTime
         self.lastTime = cTime
         # Update analogs
         # Update analogs
@@ -164,7 +166,7 @@ class DirectJoybox(DirectObject):
 
 
     def updateValsUnrolled(self):
     def updateValsUnrolled(self):
         # Update delta time
         # Update delta time
-        cTime = globalClock.getFrameTime()
+        cTime = ClockObject.getGlobalClock().getFrameTime()
         self.deltaTime = cTime - self.lastTime
         self.deltaTime = cTime - self.lastTime
         self.lastTime = cTime
         self.lastTime = cTime
         # Update analogs
         # Update analogs

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

@@ -129,8 +129,8 @@ class DirectCameraControl(DirectObject):
             # Hide the marker for this kind of motion
             # Hide the marker for this kind of motion
             self.coaMarker.hide()
             self.coaMarker.hide()
             # Record time of start of mouse interaction
             # 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 the cam is orthogonal, spawn differentTask
             if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView and\
             if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView and\
                base.direct.camera.getName() != 'persp':
                base.direct.camera.getName() != 'persp':
@@ -169,8 +169,8 @@ class DirectCameraControl(DirectObject):
             # Hide the marker for this kind of motion
             # Hide the marker for this kind of motion
             self.coaMarker.hide()
             self.coaMarker.hide()
             # Record time of start of mouse interaction
             # 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
             # Start manipulation
             # If the cam is orthogonal, spawn differentTask
             # If the cam is orthogonal, spawn differentTask
             if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView and\
             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
                 # Hide the marker for this kind of motion
                 self.coaMarker.hide()
                 self.coaMarker.hide()
                 # Record time of start of mouse interaction
                 # 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
                 # Start manipulation
                 self.spawnXZTranslateOrHPanYZoom()
                 self.spawnXZTranslateOrHPanYZoom()
                 # END MOUSE IN CENTRAL REGION
                 # END MOUSE IN CENTRAL REGION
@@ -204,9 +204,9 @@ class DirectCameraControl(DirectObject):
 
 
     def mouseFlyStop(self):
     def mouseFlyStop(self):
         self.__stopManipulateCamera()
         self.__stopManipulateCamera()
-        stopT = globalClock.getFrameTime()
+        stopT = base.clock.getFrameTime()
         deltaT = stopT - self.startT
         deltaT = stopT - self.startT
-        stopF = globalClock.getFrameCount()
+        stopF = base.clock.getFrameCount()
         deltaF = stopF - self.startF
         deltaF = stopF - self.startF
         ## No reason this shouldn't work with Maya cam on
         ## No reason this shouldn't work with Maya cam on
         # if not self.useMayaCamControls and (deltaT <= 0.25) or (deltaF <= 1):
         # 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
     Function to lerp background color to a new value
     """
     """
     def lerpColor(state):
     def lerpColor(state):
-        dt = globalClock.getDt()
+        dt = base.clock.getDt()
         state.time += dt
         state.time += dt
         sf = state.time / state.duration
         sf = state.time / state.duration
         if sf >= 1.0:
         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.showbase.MessengerGlobal import messenger
 from direct.directtools.DirectGeometry import *
 from direct.directtools.DirectGeometry import *
 
 
-from panda3d.core import NodePath, LineSegs
+from panda3d.core import NodePath, LineSegs, ClockObject
+
 
 
 class Mopath(DirectObject):
 class Mopath(DirectObject):
 
 
@@ -152,13 +153,13 @@ class Mopath(DirectObject):
         self.stop()
         self.stop()
         t = taskMgr.add(self.__playTask, self.name + '-play')
         t = taskMgr.add(self.__playTask, self.name + '-play')
         t.currentTime = time
         t.currentTime = time
-        t.lastTime = globalClock.getFrameTime()
+        t.lastTime = ClockObject.getGlobalClock().getFrameTime()
 
 
     def stop(self):
     def stop(self):
         taskMgr.remove(self.name + '-play')
         taskMgr.remove(self.name + '-play')
 
 
     def __playTask(self, task):
     def __playTask(self, task):
-        time = globalClock.getFrameTime()
+        time = ClockObject.getGlobalClock().getFrameTime()
         dTime = time - task.lastTime
         dTime = time - task.lastTime
         task.lastTime = time
         task.lastTime = time
         if self.loop:
         if self.loop:

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

@@ -1409,7 +1409,7 @@ class Freezer:
                 else:
                 else:
                     filename += '.pyo'
                     filename += '.pyo'
                 if multifile.findSubfile(filename) < 0:
                 if multifile.findSubfile(filename) < 0:
-                    code = compile('', moduleName, 'exec')
+                    code = compile('', moduleName, 'exec', optimize=2)
                     self.__addPyc(multifile, filename, code, compressionLevel)
                     self.__addPyc(multifile, filename, code, compressionLevel)
 
 
             moduleDirs[str] = True
             moduleDirs[str] = True
@@ -1489,7 +1489,7 @@ class Freezer:
                 source = open(sourceFilename.toOsSpecific(), 'r').read()
                 source = open(sourceFilename.toOsSpecific(), 'r').read()
                 if source and source[-1] != '\n':
                 if source and source[-1] != '\n':
                     source = source + '\n'
                     source = source + '\n'
-                code = compile(source, str(sourceFilename), 'exec')
+                code = compile(source, str(sourceFilename), 'exec', optimize=2)
 
 
         self.__addPyc(multifile, filename, code, compressionLevel)
         self.__addPyc(multifile, filename, code, compressionLevel)
 
 
@@ -1568,7 +1568,7 @@ class Freezer:
             # trouble importing it as a builtin module.  Synthesize a frozen
             # trouble importing it as a builtin module.  Synthesize a frozen
             # module that loads it as builtin.
             # module that loads it as builtin.
             if '.' in moduleName and self.linkExtensionModules:
             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)
                 code = marshal.dumps(code)
                 mangledName = self.mangleName(moduleName)
                 mangledName = self.mangleName(moduleName)
                 moduleDefs.append(self.makeModuleDef(mangledName, code))
                 moduleDefs.append(self.makeModuleDef(mangledName, code))
@@ -1835,7 +1835,7 @@ class Freezer:
             # If it is a submodule of a frozen module, Python will have
             # If it is a submodule of a frozen module, Python will have
             # trouble importing it as a builtin module.  Synthesize a frozen
             # trouble importing it as a builtin module.  Synthesize a frozen
             # module that loads it dynamically.
             # 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:
                 if self.platform.startswith("macosx") and not use_console:
                     # We write the Frameworks directory to sys.path[0].
                     # 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)
                     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:
         if type is _PKG_NAMESPACE_DIRECTORY:
             m = self.add_module(fqname)
             m = self.add_module(fqname)
-            m.__code__ = compile('', '', 'exec')
+            m.__code__ = compile('', '', 'exec', optimize=2)
             m.__path__ = pathname
             m.__path__ = pathname
             return m
             return m
 
 
@@ -2484,7 +2484,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
                 code = fp.read()
                 code = fp.read()
 
 
             code += b'\n' if isinstance(code, bytes) else '\n'
             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:
         elif type == imp.PY_COMPILED:
             if sys.version_info >= (3, 7):
             if sys.version_info >= (3, 7):
                 try:
                 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
 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):
 class build_apps(setuptools.Command):
     description = 'build Panda3D applications'
     description = 'build Panda3D applications'
@@ -171,6 +260,13 @@ class build_apps(setuptools.Command):
 
 
     def initialize_options(self):
     def initialize_options(self):
         self.build_base = os.path.join(os.getcwd(), 'build')
         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.gui_apps = {}
         self.console_apps = {}
         self.console_apps = {}
         self.macos_main_app = None
         self.macos_main_app = None
@@ -266,6 +362,11 @@ class build_apps(setuptools.Command):
             '/usr/lib/libxar.1.dylib',
             '/usr/lib/libxar.1.dylib',
             '/usr/lib/libmenu.5.4.dylib',
             '/usr/lib/libmenu.5.4.dylib',
             '/System/Library/**',
             '/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 = {}
         self.package_data_dirs = {}
@@ -363,6 +464,21 @@ class build_apps(setuptools.Command):
         tmp.update(self.package_data_dirs)
         tmp.update(self.package_data_dirs)
         self.package_data_dirs = tmp
         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 = {}
         self.icon_objects = {}
         for app, iconpaths in self.icons.items():
         for app, iconpaths in self.icons.items():
             if not isinstance(iconpaths, list) and not isinstance(iconpaths, tuple):
             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)
         self.announce('Building platforms: {0}'.format(','.join(self.platforms)), distutils.log.INFO)
 
 
         for platform in self.platforms:
         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):
     def download_wheels(self, platform):
         """ Downloads wheels for the given platform using pip. This includes panda3d
         """ 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:
         with open(os.path.join(contentsdir, 'Info.plist'), 'wb') as f:
             plistlib.dump(plist, 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[:]
         path = sys.path[:]
         p3dwhl = None
         p3dwhl = None
         wheelpaths = []
         wheelpaths = []
@@ -607,6 +853,9 @@ class build_apps(setuptools.Command):
                     value = value[:c].rstrip()
                     value = value[:c].rstrip()
 
 
                 if var == 'model-cache-dir' and value:
                 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()))
                     value = value.replace('/panda3d', '/{}'.format(self.distribution.get_name()))
 
 
                 if var == 'audio-library-name':
                 if var == 'audio-library-name':
@@ -642,15 +891,14 @@ class build_apps(setuptools.Command):
         prcexport = '\n'.join(prcexport)
         prcexport = '\n'.join(prcexport)
         if not self.embed_prc_data:
         if not self.embed_prc_data:
             prcdir = self.default_prc_dir.replace('<auto>', '')
             prcdir = self.default_prc_dir.replace('<auto>', '')
-            prcdir = os.path.join(builddir, prcdir)
+            prcdir = os.path.join(binary_dir, prcdir)
             os.makedirs(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)
                 f.write(prcexport)
 
 
         # Create runtimes
         # Create runtimes
         freezer_extras = set()
         freezer_extras = set()
         freezer_modules = set()
         freezer_modules = set()
-        freezer_modpaths = set()
         ext_suffixes = set()
         ext_suffixes = set()
 
 
         def get_search_path_for(source_path):
         def get_search_path_for(source_path):
@@ -683,33 +931,42 @@ class build_apps(setuptools.Command):
 
 
             return search_path
             return search_path
 
 
-        def create_runtime(appname, mainscript, use_console):
+        def create_runtime(platform, appname, mainscript, use_console):
             freezer = FreezeTool.Freezer(
             freezer = FreezeTool.Freezer(
                 platform=platform,
                 platform=platform,
                 path=path,
                 path=path,
                 hiddenImports=self.hidden_imports
                 hiddenImports=self.hidden_imports
             )
             )
             freezer.addModule('__main__', filename=mainscript)
             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('*', []):
             for incmod in self.include_modules.get(appname, []) + self.include_modules.get('*', []):
                 freezer.addModule(incmod)
                 freezer.addModule(incmod)
             for exmod in self.exclude_modules.get(appname, []) + self.exclude_modules.get('*', []):
             for exmod in self.exclude_modules.get(appname, []) + self.exclude_modules.get('*', []):
                 freezer.excludeModule(exmod)
                 freezer.excludeModule(exmod)
             freezer.done(addStartupModules=True)
             freezer.done(addStartupModules=True)
 
 
-            target_path = os.path.join(builddir, appname)
-
             stub_name = 'deploy-stub'
             stub_name = 'deploy-stub'
+            target_name = appname
             if platform.startswith('win') or 'macosx' in platform:
             if platform.startswith('win') or 'macosx' in platform:
                 if not use_console:
                 if not use_console:
                     stub_name = 'deploy-stubw'
                     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'):
             if platform.startswith('win'):
                 stub_name += '.exe'
                 stub_name += '.exe'
-                target_path += '.exe'
+                target_name += '.exe'
 
 
             if use_wheels:
             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:
             else:
                 dtool_path = p3d.Filename(p3d.ExecutionEnvironment.get_dtool_name()).to_os_specific()
                 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)
                 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:
             if not self.log_filename or '%' not in self.log_filename:
                 use_strftime = False
                 use_strftime = False
 
 
+            target_path = os.path.join(binary_dir, target_name)
             freezer.generateRuntimeFromStub(target_path, stub_file, use_console, {
             freezer.generateRuntimeFromStub(target_path, stub_file, use_console, {
                 'prc_data': prcexport if self.embed_prc_data else None,
                 'prc_data': prcexport if self.embed_prc_data else None,
                 'default_prc_dir': self.default_prc_dir,
                 'default_prc_dir': self.default_prc_dir,
@@ -750,26 +1008,23 @@ class build_apps(setuptools.Command):
                 os.unlink(temp_file.name)
                 os.unlink(temp_file.name)
 
 
             # Copy the dependencies.
             # Copy the dependencies.
-            search_path = [builddir]
+            search_path = [binary_dir]
             if use_wheels:
             if use_wheels:
+                search_path.append(os.path.join(p3dwhlfn, 'panda3d'))
                 search_path.append(os.path.join(p3dwhlfn, 'deploy_libs'))
                 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_extras.update(freezer.extras)
             freezer_modules.update(freezer.getAllModuleNames())
             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:
             for suffix in freezer.moduleSuffixes:
                 if suffix[2] == imp.C_EXTENSION:
                 if suffix[2] == imp.C_EXTENSION:
                     ext_suffixes.add(suffix[0])
                     ext_suffixes.add(suffix[0])
 
 
         for appname, scriptname in self.gui_apps.items():
         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():
         for appname, scriptname in self.console_apps.items():
-            create_runtime(appname, scriptname, True)
+            create_runtime(platform, appname, scriptname, True)
 
 
         # Copy extension modules
         # Copy extension modules
         whl_modules = []
         whl_modules = []
@@ -805,7 +1060,7 @@ class build_apps(setuptools.Command):
             plugname = lib.split('.', 1)[0]
             plugname = lib.split('.', 1)[0]
             if plugname in plugin_list:
             if plugname in plugin_list:
                 source_path = os.path.join(p3dwhlfn, lib)
                 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)]
                 search_path = [os.path.dirname(source_path)]
                 self.copy_with_dependencies(source_path, target_path, search_path)
                 self.copy_with_dependencies(source_path, target_path, search_path)
 
 
@@ -846,8 +1101,13 @@ class build_apps(setuptools.Command):
                 else:
                 else:
                     continue
                     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.
             # 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)
             search_path = get_search_path_for(source_path)
             self.copy_with_dependencies(source_path, target_path, search_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:
             if os.path.isdir(tcl_dir) and 'tkinter' in freezer_modules:
                 self.announce('Copying Tcl files', distutils.log.INFO)
                 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):
                 for dir in os.listdir(tcl_dir):
                     sub_dir = os.path.join(tcl_dir, dir)
                     sub_dir = os.path.join(tcl_dir, dir)
                     if os.path.isdir(sub_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))
                         self.announce('copying {0} -> {1}'.format(sub_dir, target_dir))
                         shutil.copytree(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.
         # Extract any other data files from dependency packages.
         for module, datadesc in self.package_data_dirs.items():
         for module, datadesc in self.package_data_dirs.items():
             if module not in freezer_modules:
             if module not in freezer_modules:
@@ -883,7 +1148,7 @@ class build_apps(setuptools.Command):
                     source_dir = os.path.dirname(source_pattern)
                     source_dir = os.path.dirname(source_pattern)
                     # Relocate the target dir to the build directory.
                     # Relocate the target dir to the build directory.
                     target_dir = target_dir.replace('/', os.sep)
                     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:
                     for wf in filenames:
                         if wf.lower().startswith(source_dir.lower() + '/'):
                         if wf.lower().startswith(source_dir.lower() + '/'):
@@ -903,15 +1168,18 @@ class build_apps(setuptools.Command):
                             else:
                             else:
                                 self.copy(source_path, target_path)
                                 self.copy(source_path, target_path)
 
 
+    def build_data(self, data_dir, platform):
+        """ Builds the data files for the given platform. """
+
         # Copy Game Files
         # Copy Game Files
         self.announce('Copying game files for platform: {}'.format(platform), distutils.log.INFO)
         self.announce('Copying game files for platform: {}'.format(platform), distutils.log.INFO)
         ignore_copy_list = [
         ignore_copy_list = [
             '**/__pycache__/**',
             '**/__pycache__/**',
             '**/*.pyc',
             '**/*.pyc',
+            '**/*.py',
             '{}/**'.format(self.build_base),
             '{}/**'.format(self.build_base),
         ]
         ]
         ignore_copy_list += self.exclude_patterns
         ignore_copy_list += self.exclude_patterns
-        ignore_copy_list += freezer_modpaths
         ignore_copy_list += self.extra_prc_files
         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]
         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:
             for fname in filelist:
                 src = os.path.join(dirpath, fname)
                 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)
                 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):
     def add_dependency(self, name, target_dir, search_path, referenced_by):
         """ Searches for the given DLL on the search path.  If it exists,
         """ Searches for the given DLL on the search path.  If it exists,
         copies it to the target_dir. """
         copies it to the target_dir. """
@@ -1333,6 +1597,7 @@ class bdist_apps(setuptools.Command):
         'manylinux1_i686': ['gztar'],
         'manylinux1_i686': ['gztar'],
         'manylinux2010_x86_64': ['gztar'],
         'manylinux2010_x86_64': ['gztar'],
         'manylinux2010_i686': ['gztar'],
         'manylinux2010_i686': ['gztar'],
+        'android': ['aab'],
         # Everything else defaults to ['zip']
         # Everything else defaults to ['zip']
     }
     }
 
 
@@ -1342,6 +1607,7 @@ class bdist_apps(setuptools.Command):
         'bztar': installers.create_bztar,
         'bztar': installers.create_bztar,
         'xztar': installers.create_xztar,
         'xztar': installers.create_xztar,
         'nsis': installers.create_nsis,
         'nsis': installers.create_nsis,
+        'aab': installers.create_aab,
     }
     }
 
 
     description = 'bundle built Panda3D applications into distributable forms'
     description = 'bundle built Panda3D applications into distributable forms'
@@ -1357,6 +1623,9 @@ class bdist_apps(setuptools.Command):
         self.installers = {}
         self.installers = {}
         self.dist_dir = os.path.join(os.getcwd(), 'dist')
         self.dist_dir = os.path.join(os.getcwd(), 'dist')
         self.skip_build = False
         self.skip_build = False
+        self.signing_certificate = None
+        self.signing_private_key = None
+        self.signing_passphrase = None
         self.installer_functions = {}
         self.installer_functions = {}
         self._current_platform = None
         self._current_platform = None
         for opt in self._build_apps_options():
         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()
             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 = self.DEFAULT_INSTALLER_FUNCS.copy()
         tmp.update(self.installer_functions)
         tmp.update(self.installer_functions)
         tmp.update({
         tmp.update({

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

@@ -32,6 +32,9 @@ class Icon:
 
 
         return True
         return True
 
 
+    def getLargestSize(self):
+        return max(self.images.keys())
+
     def generateMissingImages(self):
     def generateMissingImages(self):
         """ Generates image sizes that should be present but aren't by scaling
         """ Generates image sizes that should be present but aren't by scaling
         from the next higher size. """
         from the next higher size. """
@@ -52,10 +55,12 @@ class Icon:
             if from_size > required_size:
             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))
                 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)
                 image = PNMImage(required_size, required_size)
-                if self.images[from_size].hasAlpha():
+                image.setColorType(from_image.getColorType())
+                if from_image.hasAlpha():
                     image.addAlpha()
                     image.addAlpha()
-                image.quickFilterFrom(self.images[from_size])
+                image.quickFilterFrom(from_image)
                 self.images[required_size] = image
                 self.images[required_size] = image
             else:
             else:
                 Icon.notify.warning("Cannot generate %dx%d icon; no higher resolution image available" % (required_size, required_size))
                 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()
         icns.close()
 
 
         return True
         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())
     cmd.append(nsifile.to_os_specific())
     subprocess.check_call(cmd)
     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 .MsgTypesCMU import *
 from .PyDatagram import PyDatagram
 from .PyDatagram import PyDatagram
 from .PyDatagramIterator import PyDatagramIterator
 from .PyDatagramIterator import PyDatagramIterator
-from panda3d.core import UniqueIdAllocator, Notify
+from panda3d.core import UniqueIdAllocator, Notify, ClockObject
 
 
 
 
 class ClientRepository(ClientRepositoryBase):
 class ClientRepository(ClientRepositoryBase):
@@ -281,7 +281,7 @@ class ClientRepository(ClientRepositoryBase):
         datagram.addUint16(CLIENT_HEARTBEAT_CMU)
         datagram.addUint16(CLIENT_HEARTBEAT_CMU)
         # Send it!
         # Send it!
         self.send(datagram)
         self.send(datagram)
-        self.lastHeartbeat = globalClock.getRealTime()
+        self.lastHeartbeat = ClockObject.getGlobalClock().getRealTime()
         # This is important enough to consider flushing immediately
         # This is important enough to consider flushing immediately
         # (particularly if we haven't run readerPollTask recently).
         # (particularly if we haven't run readerPollTask recently).
         self.considerFlush()
         self.considerFlush()

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

@@ -186,7 +186,7 @@ class ClientRepositoryBase(ConnectionRepository):
                 self.doGenerate(*args)
                 self.doGenerate(*args)
 
 
                 if deferrable:
                 if deferrable:
-                    self.lastGenerate = globalClock.getFrameTime()
+                    self.lastGenerate = ClockObject.getGlobalClock().getFrameTime()
 
 
                 for dg, di in updates:
                 for dg, di in updates:
                     # non-DC updates that need to be played back in-order are
                     # 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
         """ This is the task that generates an object on the deferred
         queue. """
         queue. """
 
 
-        now = globalClock.getFrameTime()
+        now = ClockObject.getGlobalClock().getFrameTime()
         while self.deferredGenerates:
         while self.deferredGenerates:
             if now - self.lastGenerate < self.deferInterval:
             if now - self.lastGenerate < self.deferInterval:
                 # Come back later.
                 # Come back later.
@@ -539,7 +539,7 @@ class ClientRepositoryBase(ConnectionRepository):
             self.notify.debug("Heartbeats not started; not sending.")
             self.notify.debug("Heartbeats not started; not sending.")
             return
             return
 
 
-        elapsed = globalClock.getRealTime() - self.lastHeartbeat
+        elapsed = ClockObject.getGlobalClock().getRealTime() - self.lastHeartbeat
         if elapsed < 0 or elapsed > self.heartbeatInterval:
         if elapsed < 0 or elapsed > self.heartbeatInterval:
             # It's time to send the heartbeat again (or maybe someone
             # It's time to send the heartbeat again (or maybe someone
             # reset the clock back).
             # 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
         reflect the node's current position
         """
         """
         if self.stopped:
         if self.stopped:
-            currTime = globalClock.getFrameTime()
+            currTime = ClockObject.getGlobalClock().getFrameTime()
             now = currTime - self.smoother.getExpectedBroadcastPeriod()
             now = currTime - self.smoother.getExpectedBroadcastPeriod()
             last = self.smoother.getMostRecentTimestamp()
             last = self.smoother.getMostRecentTimestamp()
             if now > last:
             if now > last:
@@ -348,6 +348,7 @@ class DistributedSmoothNode(DistributedNode.DistributedNode,
                 self.smoother.setPhonyTimestamp()
                 self.smoother.setPhonyTimestamp()
             self.smoother.markPosition()
             self.smoother.markPosition()
         else:
         else:
+            globalClock = ClockObject.getGlobalClock()
             now = globalClock.getFrameTime()
             now = globalClock.getFrameTime()
             local = globalClockDelta.networkToLocalTime(timestamp, now)
             local = globalClockDelta.networkToLocalTime(timestamp, now)
             realTime = globalClock.getRealTime()
             realTime = globalClock.getRealTime()

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

@@ -157,7 +157,7 @@ class DoInterestManager(DirectObject.DirectObject):
         """
         """
         assert DoInterestManager.notify.debugCall()
         assert DoInterestManager.notify.debugCall()
         handle = self._getNextHandle()
         handle = self._getNextHandle()
-        # print 'base.cr.addInterest(',description,',',handle,'):',globalClock.getFrameCount()
+        # print 'base.cr.addInterest(',description,',',handle,'):',base.clock.getFrameCount()
         if self._noNewInterests:
         if self._noNewInterests:
             DoInterestManager.notify.warning(
             DoInterestManager.notify.warning(
                 "addInterest: addingInterests on delete: %s" % (handle))
                 "addInterest: addingInterests on delete: %s" % (handle))
@@ -229,7 +229,7 @@ class DoInterestManager(DirectObject.DirectObject):
         """
         """
         Stop looking in a (set of) zone(s)
         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 DoInterestManager.notify.debugCall()
         assert isinstance(handle, InterestHandle)
         assert isinstance(handle, InterestHandle)
@@ -567,7 +567,7 @@ class DoInterestManager(DirectObject.DirectObject):
         def checkMoreInterests():
         def checkMoreInterests():
             # if there are new interests, cancel this delayed callback, another
             # if there are new interests, cancel this delayed callback, another
             # will automatically be scheduled when all interests complete
             # 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
             return self._completeEventCount.num > 0
         def sendEvent():
         def sendEvent():
             messenger.send(self.getAllInterestsCompleteEvent())
             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.directnotify import DirectNotifyGlobal
 from direct.distributed.ClockDelta import globalClockDelta
 from direct.distributed.ClockDelta import globalClockDelta
 
 
+
 class TimeManager(DistributedObject.DistributedObject):
 class TimeManager(DistributedObject.DistributedObject):
     """
     """
     This DistributedObject lives on the AI and on the client side, and
     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
         The return value is true if the attempt is made, or false if
         it is too soon since the last attempt.
         it is too soon since the last attempt.
         """
         """
-        now = globalClock.getRealTime()
+        now = ClockObject.getGlobalClock().getRealTime()
 
 
         if now - self.lastAttempt < self.minWait:
         if now - self.lastAttempt < self.minWait:
             self.notify.debug("Not resyncing (too soon): %s" % (description))
             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
         determine the clock delta between the AI and the client
         machines.
         machines.
         """
         """
-        end = globalClock.getRealTime()
+        clock = ClockObject.getGlobalClock()
+        end = clock.getRealTime()
 
 
         if context != self.thisContext:
         if context != self.thisContext:
             self.notify.info("Ignoring TimeManager response for old context %d" % (context))
             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 globalClockDelta.getUncertainty() > self.maxUncertainty:
             if self.attemptCount < self.maxAttempts:
             if self.attemptCount < self.maxAttempts:
                 self.notify.info("Uncertainty is too high, trying again.")
                 self.notify.info("Uncertainty is too high, trying again.")
-                self.start = globalClock.getRealTime()
+                self.start = clock.getRealTime()
                 self.sendUpdate("requestServerTime", [self.thisContext])
                 self.sendUpdate("requestServerTime", [self.thisContext])
                 return
                 return
             self.notify.info("Giving up on uncertainty requirement.")
             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
 Class CommonFilters implements certain common image
 postprocessing filters.  See the :ref:`common-image-filters` page for
 postprocessing filters.  See the :ref:`common-image-filters` page for
 more information about how to use these filters.
 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 LVecBase4, LPoint2
 from panda3d.core import AuxBitplaneAttrib
 from panda3d.core import AuxBitplaneAttrib
 from panda3d.core import Texture, Shader, ATSNone
 from panda3d.core import Texture, Shader, ATSNone
@@ -459,6 +459,11 @@ class CommonFilters:
         return True
         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"):
     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":
         if size == 0 or size == "off":
             self.delBloom()
             self.delBloom()
             return
             return
@@ -548,7 +553,7 @@ class CommonFilters:
         return True
         return True
 
 
     def setBlurSharpen(self, amount=0.0):
     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."""
         A value of 0.0 means fully blurred, and a value higher than 1.0 sharpens the image."""
         fullrebuild = ("BlurSharpen" not in self.configuration)
         fullrebuild = ("BlurSharpen" not in self.configuration)
         self.configuration["BlurSharpen"] = amount
         self.configuration["BlurSharpen"] = amount

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

@@ -107,7 +107,9 @@ class OnscreenImage(DirectObject, NodePath):
                 tex = image
                 tex = image
             else:
             else:
                 # It's a Texture file name
                 # 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 = CardMaker('OnscreenImage')
             cm.setFrame(-1, 1, -1, 1)
             cm.setFrame(-1, 1, -1, 1)
             self.assign(parent.attachNewNode(cm.generate(), sort))
             self.assign(parent.attachNewNode(cm.generate(), sort))

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

@@ -137,22 +137,22 @@ if __name__ == "__main__":
     startTime = 0.0
     startTime = 0.0
     def printStart():
     def printStart():
         global startTime
         global startTime
-        startTime = globalClock.getFrameTime()
+        startTime = base.clock.getFrameTime()
         print('Start')
         print('Start')
 
 
     def printPreviousStart():
     def printPreviousStart():
         global startTime
         global startTime
-        currTime = globalClock.getFrameTime()
+        currTime = base.clock.getFrameTime()
         print('PREVIOUS_END %0.2f' % (currTime - startTime))
         print('PREVIOUS_END %0.2f' % (currTime - startTime))
 
 
     def printPreviousEnd():
     def printPreviousEnd():
         global startTime
         global startTime
-        currTime = globalClock.getFrameTime()
+        currTime = base.clock.getFrameTime()
         print('PREVIOUS_END %0.2f' % (currTime - startTime))
         print('PREVIOUS_END %0.2f' % (currTime - startTime))
 
 
     def printTrackStart():
     def printTrackStart():
         global startTime
         global startTime
-        currTime = globalClock.getFrameTime()
+        currTime = base.clock.getFrameTime()
         print('TRACK_START %0.2f' % (currTime - startTime))
         print('TRACK_START %0.2f' % (currTime - startTime))
 
 
     def printArguments(a, b, c):
     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.showbase.DirectObject import *
 from direct.directtools.DirectUtil import *
 from direct.directtools.DirectUtil import *
 from direct.gui.DirectGui import *
 from direct.gui.DirectGui import *
+from panda3d.core import ClockObject
 
 
 from .CurveEditor import *
 from .CurveEditor import *
 from .FileMgr import *
 from .FileMgr import *
@@ -385,7 +386,7 @@ class LevelEditorBase(DirectObject):
                     alreadyExists = True
                     alreadyExists = True
                     break
                     break
             if not alreadyExists:
             if not alreadyExists:
-                time = globalClock.getRealTime() + 15
+                time = ClockObject.getGlobalClock().getRealTime() + 15
                 self.statusLines.append([time,status,color])
                 self.statusLines.append([time,status,color])
 
 
         # update display of new status lines
         # update display of new status lines
@@ -407,7 +408,7 @@ class LevelEditorBase(DirectObject):
     def updateStatusReadoutTimeouts(self,task=None):
     def updateStatusReadoutTimeouts(self,task=None):
         removalList = []
         removalList = []
         for currLine in self.statusLines:
         for currLine in self.statusLines:
-            if globalClock.getRealTime() >= currLine[0]:
+            if ClockObject.getGlobalClock().getRealTime() >= currLine[0]:
                 removalList.append(currLine)
                 removalList.append(currLine)
         for currRemoval in removalList:
         for currRemoval in removalList:
             self.statusLines.remove(currRemoval)
             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
             self._useOverflowTime = ConfigVariableBool('job-use-overflow-time', 1).value
 
 
         if len(self._pri2jobId2job) > 0:
         if len(self._pri2jobId2job) > 0:
+            clock = ClockObject.getGlobalClock()
             #assert self.notify.debugCall()
             #assert self.notify.debugCall()
             # figure out how long we can run
             # figure out how long we can run
-            endT = globalClock.getRealTime() + (self.getTimeslice() * .9)
+            endT = clock.getRealTime() + (self.getTimeslice() * .9)
             while True:
             while True:
                 if self._jobIdGenerator is None:
                 if self._jobIdGenerator is None:
                     # round-robin the jobs, giving high-priority jobs more timeslices
                     # 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
                 # check if there's overflow time that we need to make up for
                 if self._useOverflowTime:
                 if self._useOverflowTime:
                     overflowTime = self._jobId2overflowTime[jobId]
                     overflowTime = self._jobId2overflowTime[jobId]
-                    timeLeft = endT - globalClock.getRealTime()
+                    timeLeft = endT - clock.getRealTime()
                     if overflowTime >= timeLeft:
                     if overflowTime >= timeLeft:
                         self._jobId2overflowTime[jobId] = max(0., overflowTime-timeLeft)
                         self._jobId2overflowTime[jobId] = max(0., overflowTime-timeLeft)
                         # don't run any more jobs this frame, this makes up
                         # don't run any more jobs this frame, this makes up
@@ -184,7 +185,7 @@ class JobManager:
                 if __debug__:
                 if __debug__:
                     job._pstats.start()
                     job._pstats.start()
                 job.resume()
                 job.resume()
-                while globalClock.getRealTime() < endT:
+                while clock.getRealTime() < endT:
                     try:
                     try:
                         result = next(gen)
                         result = next(gen)
                     except StopIteration:
                     except StopIteration:
@@ -210,9 +211,9 @@ class JobManager:
                         break
                         break
                 else:
                 else:
                     # we've run out of time
                     # 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()
                     job.suspend()
-                    overflowTime = globalClock.getRealTime() - endT
+                    overflowTime = clock.getRealTime() - endT
                     if overflowTime > self.getTimeslice():
                     if overflowTime > self.getTimeslice():
                         self._jobId2overflowTime[jobId] += overflowTime
                         self._jobId2overflowTime[jobId] += overflowTime
                     if __debug__:
                     if __debug__:

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

@@ -43,7 +43,7 @@ import functools
 
 
 __report_indent = 3
 __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
 ## 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:
             if prefixes:
                 outStr = '%%s %s' % (outStr,)
                 outStr = '%%s %s' % (outStr,)
 
 
+            globalClock = ClockObject.getGlobalClock()
 
 
             if 'module' in types:
             if 'module' in types:
                 outStr = '%s {M:%s}' % (outStr, f.__module__.split('.')[-1])
                 outStr = '%s {M:%s}' % (outStr, f.__module__.split('.')[-1])
@@ -2264,9 +2265,10 @@ if __debug__:
                 # at the time that PythonUtil is loaded
                 # at the time that PythonUtil is loaded
                 if not ConfigVariableBool("profile-debug", False):
                 if not ConfigVariableBool("profile-debug", False):
                     #dumb timings
                     #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))
                     print("Function %s.%s took %s seconds"%(f.__module__, f.__name__,s))
                 else:
                 else:
                     import profile as prof, pstats
                     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
         # Get a pointer to Panda's global ClockObject, used for
         # synchronizing events between Python and C.
         # 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
         # Since we have already started up a TaskManager, and probably
         # a number of tasks; and since the TaskManager had to use the
         # a number of tasks; and since the TaskManager had to use the
         # TrueClock to tell time until this moment, make sure the
         # TrueClock to tell time until this moment, make sure the
         # globalClock object is exactly in sync with the TrueClock.
         # globalClock object is exactly in sync with the TrueClock.
         trueClock = TrueClock.getGlobalPtr()
         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 is determined by, in order:
         # - client-cpu-affinity-mask config
         # - client-cpu-affinity-mask config
@@ -444,7 +447,7 @@ class ShowBase(DirectObject.DirectObject):
         builtins.ostream = Notify.out()
         builtins.ostream = Notify.out()
         builtins.directNotify = directNotify
         builtins.directNotify = directNotify
         builtins.giveNotify = giveNotify
         builtins.giveNotify = giveNotify
-        builtins.globalClock = globalClock
+        builtins.globalClock = clock
         builtins.vfs = vfs
         builtins.vfs = vfs
         builtins.cpMgr = ConfigPageManager.getGlobalPtr()
         builtins.cpMgr = ConfigPageManager.getGlobalPtr()
         builtins.cvMgr = ConfigVariableManager.getGlobalPtr()
         builtins.cvMgr = ConfigVariableManager.getGlobalPtr()
@@ -1899,7 +1902,7 @@ class ShowBase(DirectObject.DirectObject):
         return self.physicsMgrEnabled
         return self.physicsMgrEnabled
 
 
     def updateManagers(self, state):
     def updateManagers(self, state):
-        dt = globalClock.getDt()
+        dt = self.clock.dt
         if self.particleMgrEnabled:
         if self.particleMgrEnabled:
             self.particleMgr.doParticles(dt)
             self.particleMgr.doParticles(dt)
         if self.physicsMgrEnabled:
         if self.physicsMgrEnabled:
@@ -2927,14 +2930,15 @@ class ShowBase(DirectObject.DirectObject):
         Returns:
         Returns:
             A `~direct.task.Task` that can be awaited.
             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 = self.taskMgr.add(self._movieTask, namePrefix + '_task')
         t.frameIndex = 0  # Frame 0 is not captured.
         t.frameIndex = 0  # Frame 0 is not captured.
         t.numFrames = int(duration * fps)
         t.numFrames = int(duration * fps)
         t.source = source
         t.source = source
         t.outputString = namePrefix + '_%0' + repr(sd) + 'd.' + format
         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
         return t
 
 
     def _movieTask(self, state):
     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
 #: The clock object used by default for rendering and animation, obtained using
 #: :meth:`panda3d.core.ClockObject.getGlobalClock()`.
 #: :meth:`panda3d.core.ClockObject.getGlobalClock()`.
+#: @deprecated Use `base.clock` instead.
 globalClock = ClockObject.getGlobalClock()
 globalClock = ClockObject.getGlobalClock()
 
 
 #: See :meth:`panda3d.core.ConfigPageManager.getGlobalPtr()`.
 #: 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.directnotify.DirectNotifyGlobal import directNotify
 from direct.task import Task
 from direct.task import Task
 from direct.task.TaskManagerGlobal import taskMgr
 from direct.task.TaskManagerGlobal import taskMgr
+from panda3d.core import ClockObject
 
 
 from .PythonUtil import SerialNumGen, Functor
 from .PythonUtil import SerialNumGen, Functor
 
 
@@ -89,14 +90,14 @@ class TaskThreaded:
     def _doCallback(self, callback, taskName, task):
     def _doCallback(self, callback, taskName, task):
         assert self.notify.debugCall()
         assert self.notify.debugCall()
         self.__taskNames.remove(taskName)
         self.__taskNames.remove(taskName)
-        self._taskStartTime = globalClock.getRealTime()
+        self._taskStartTime = ClockObject.getGlobalClock().getRealTime()
         callback()
         callback()
         self._taskStartTime = None
         self._taskStartTime = None
         return Task.done
         return Task.done
 
 
     def _doThreadCallback(self, thread, taskName, task):
     def _doThreadCallback(self, thread, taskName, task):
         assert self.notify.debugCall()
         assert self.notify.debugCall()
-        self._taskStartTime = globalClock.getRealTime()
+        self._taskStartTime = ClockObject.getGlobalClock().getRealTime()
         thread.run()
         thread.run()
         self._taskStartTime = None
         self._taskStartTime = None
         if thread.isFinished():
         if thread.isFinished():
@@ -114,7 +115,7 @@ class TaskThreaded:
             # we must not be in a task callback, we must be running in non-threaded
             # we must not be in a task callback, we must be running in non-threaded
             # mode
             # mode
             return True
             return True
-        return (globalClock.getRealTime() - self._taskStartTime) < self.__timeslice
+        return (ClockObject.getGlobalClock().getRealTime() - self._taskStartTime) < self.__timeslice
 
 
 class TaskThread:
 class TaskThread:
     # derive and override these four funcs
     # 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.directnotify.DirectNotifyGlobal import directNotify
 from direct.fsm.StatePush import FunctionCall
 from direct.fsm.StatePush import FunctionCall
 from direct.showbase.PythonUtil import formatTimeExact, normalDistrib, serialNum
 from direct.showbase.PythonUtil import formatTimeExact, normalDistrib, serialNum
@@ -59,7 +59,7 @@ class FrameProfiler:
     def _setEnabled(self, enabled):
     def _setEnabled(self, enabled):
         if enabled:
         if enabled:
             self.notify.info('frame profiler started')
             self.notify.info('frame profiler started')
-            self._startTime = globalClock.getFrameTime()
+            self._startTime = ClockObject.getGlobalClock().getFrameTime()
             self._profileCounter = 0
             self._profileCounter = 0
             self._jitter = None
             self._jitter = None
             self._period2aggregateProfile = {}
             self._period2aggregateProfile = {}
@@ -110,7 +110,7 @@ class FrameProfiler:
             self._analyzeResults, sessionId))
             self._analyzeResults, sessionId))
 
 
         # schedule the next profile
         # schedule the next profile
-        delay = max(time - globalClock.getFrameTime(), 0.)
+        delay = max(time - ClockObject.getGlobalClock().getFrameTime(), 0.)
         self._task = taskMgr.doMethodLater(delay, self._scheduleNextProfileDoLater,
         self._task = taskMgr.doMethodLater(delay, self._scheduleNextProfileDoLater,
                                            'FrameProfiler-%s' % serialNum())
                                            'FrameProfiler-%s' % serialNum())
 
 

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

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

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

@@ -2,6 +2,8 @@
 
 
 __all__ = ['Timer']
 __all__ = ['Timer']
 
 
+from panda3d.core import ClockObject
+
 from . import Task
 from . import Task
 from .TaskManagerGlobal import taskMgr
 from .TaskManagerGlobal import taskMgr
 
 
@@ -25,7 +27,7 @@ class Timer:
         self.callback = None
         self.callback = None
         self.finalT = t
         self.finalT = t
         self.name = name
         self.name = name
-        self.startT = globalClock.getFrameTime()
+        self.startT = ClockObject.getGlobalClock().getFrameTime()
         self.currT = 0.0
         self.currT = 0.0
         taskMgr.add(self.__timerTask, self.name + '-run')
         taskMgr.add(self.__timerTask, self.name + '-run')
         self.started = 1
         self.started = 1
@@ -35,7 +37,7 @@ class Timer:
             self.stop()
             self.stop()
         self.callback = callback
         self.callback = callback
         self.finalT = t
         self.finalT = t
-        self.startT = globalClock.getFrameTime()
+        self.startT = ClockObject.getGlobalClock().getFrameTime()
         self.currT = 0.0
         self.currT = 0.0
         taskMgr.add(self.__timerTask, self.name + '-run')
         taskMgr.add(self.__timerTask, self.name + '-run')
         self.started = 1
         self.started = 1
@@ -71,7 +73,7 @@ class Timer:
         return self.finalT - self.currT
         return self.finalT - self.currT
 
 
     def __timerTask(self, task):
     def __timerTask(self, task):
-        t = globalClock.getFrameTime()
+        t = ClockObject.getGlobalClock().getFrameTime()
         te = t - self.startT
         te = t - self.startT
         self.currT = te
         self.currT = te
         if te >= self.finalT:
         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 ###
 ### SEE END OF FILE FOR EXAMPLE USEAGE ###
 
 
 # Import Tkinter, Pmw, and the floater code from this directory tree.
 # 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.tkwidgets.AppShell import *
 from direct.showbase.TkGlobal import *
 from direct.showbase.TkGlobal import *
 from direct.task import Task
 from direct.task import Task
@@ -294,7 +294,7 @@ class AnimPanel(AppShell):
 
 
     def playActorControls(self):
     def playActorControls(self):
         self.stopActorControls()
         self.stopActorControls()
-        self.lastT = globalClock.getFrameTime()
+        self.lastT = ClockObject.getGlobalClock().getFrameTime()
         self.playList = self.actorControlList[:]
         self.playList = self.actorControlList[:]
         taskMgr.add(self.play, self.id + '_UpdateTask')
         taskMgr.add(self.play, self.id + '_UpdateTask')
 
 
@@ -302,7 +302,7 @@ class AnimPanel(AppShell):
         if not self.playList:
         if not self.playList:
             return Task.done
             return Task.done
         fLoop = self.loopVar.get()
         fLoop = self.loopVar.get()
-        currT = globalClock.getFrameTime()
+        currT = ClockObject.getGlobalClock().getFrameTime()
         deltaT = currT - self.lastT
         deltaT = currT - self.lastT
         self.lastT = currT
         self.lastT = currT
         for actorControl in self.playList:
         for actorControl in self.playList:

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

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

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

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

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

@@ -8,6 +8,7 @@ __all__ = ['Floater', 'FloaterWidget', 'FloaterGroup']
 from direct.showbase.TkGlobal import *
 from direct.showbase.TkGlobal import *
 from .Valuator import Valuator, VALUATOR_MINI, VALUATOR_FULL
 from .Valuator import Valuator, VALUATOR_MINI, VALUATOR_FULL
 from direct.task import Task
 from direct.task import Task
+from panda3d.core import ClockObject
 import math
 import math
 import Pmw
 import Pmw
 
 
@@ -149,14 +150,14 @@ class FloaterWidget(Pmw.MegaWidget):
         self.velocitySF = 0.0
         self.velocitySF = 0.0
         self.updateTask = taskMgr.add(self.updateFloaterTask,
         self.updateTask = taskMgr.add(self.updateFloaterTask,
                                         'updateFloater')
                                         'updateFloater')
-        self.updateTask.lastTime = globalClock.getFrameTime()
+        self.updateTask.lastTime = ClockObject.getGlobalClock().getFrameTime()
 
 
     def updateFloaterTask(self, state):
     def updateFloaterTask(self, state):
         """
         """
         Update floaterWidget value based on current scaleFactor
         Update floaterWidget value based on current scaleFactor
         Adjust for time to compensate for fluctuating frame rates
         Adjust for time to compensate for fluctuating frame rates
         """
         """
-        currT = globalClock.getFrameTime()
+        currT = ClockObject.getGlobalClock().getFrameTime()
         dt = currT - state.lastTime
         dt = currT - state.lastTime
         self.set(self.value + self.velocitySF * dt)
         self.set(self.value + self.velocitySF * dt)
         state.lastTime = currT
         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 [edx]
       fstp DWORD ptr [eax]
       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);
   *sin_result = sinf(v);
   *cos_result = cosf(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 [edx]
       fstp QWORD ptr [eax]
       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_
 #else //!_X86_
   *sin_result = sin(v);
   *sin_result = sin(v);
   *cos_result = cos(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(parent_classes, get_num_parent_classes, get_parent_class);
   MAKE_SEQ_PROPERTY(child_classes, get_num_child_classes, get_child_class);
   MAKE_SEQ_PROPERTY(child_classes, get_num_child_classes, get_child_class);
 
 
+  EXTENSION(PyObject *__reduce__() const);
+  EXTENSION(void __setstate__(PyObject *));
+
 public:
 public:
 #ifdef HAVE_PYTHON
 #ifdef HAVE_PYTHON
   PyObject *get_python_type() const;
   PyObject *get_python_type() const;

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

@@ -32,4 +32,55 @@ make(PyTypeObject *tp) {
   return dtool_tp->_type;
   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
 #endif

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

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

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

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

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

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

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

@@ -43,22 +43,4 @@ size_t PyLongOrInt_AsSize_t(PyObject *vv) {
 }
 }
 #endif
 #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
 #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 */
 /* Python 3.9 */
 
 
 #if PY_VERSION_HEX < 0x03090000
 #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) {
 INLINE PyObject *PyObject_CallOneArg(PyObject *callable, PyObject *arg) {
 #if PY_VERSION_HEX >= 0x03060000
 #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 time_t;
 typedef long clock_t;
 typedef long clock_t;
 
 
-typedef unsigned int uint;
-typedef unsigned long ulong;
-typedef unsigned short ushort;
-typedef unsigned char uchar;
-
 #ifdef _WIN64
 #ifdef _WIN64
 #define __SIZE_TYPE__ unsigned long long
 #define __SIZE_TYPE__ unsigned long long
 #define __PTRDIFF_TYPE__ long long
 #define __PTRDIFF_TYPE__ long long

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

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

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

@@ -92,7 +92,7 @@ overflow(int ch) {
  */
  */
 void AndroidLogStream::AndroidLogStreamBuf::
 void AndroidLogStream::AndroidLogStreamBuf::
 write_char(char c) {
 write_char(char c) {
-  nout.put(c);
+  //nout.put(c);
   if (c == '\n') {
   if (c == '\n') {
     // Write a line to the log file.
     // Write a line to the log file.
     __android_log_write(_priority, _tag.c_str(), _data.c_str());
     __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;
     std::string _data;
   };
   };
 
 
-  AndroidLogStream(int priority);
-
 public:
 public:
+  AndroidLogStream(int priority);
   virtual ~AndroidLogStream();
   virtual ~AndroidLogStream();
+
   static std::ostream &out(NotifySeverity severity);
   static std::ostream &out(NotifySeverity severity);
 };
 };
 
 

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

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

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

@@ -44,6 +44,8 @@ PUBLISHED:
 
 
   INLINE size_t get_num_words() const;
   INLINE size_t get_num_words() const;
 
 
+  EXTENSION(PyObject *__reduce__(PyObject *self) const);
+
 protected:
 protected:
   INLINE const ConfigDeclaration *get_default_value() const;
   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
 #ifdef ANDROID
 #include <android/log.h>
 #include <android/log.h>
+
+#include "androidLogStream.h"
 #endif
 #endif
 
 
 using std::cerr;
 using std::cerr;
@@ -345,8 +347,9 @@ assert_failure(const char *expression, int line,
 
 
 #ifdef ANDROID
 #ifdef ANDROID
   __android_log_assert("assert", "Panda3D", "Assertion failed: %s", message.c_str());
   __android_log_assert("assert", "Panda3D", "Assertion failed: %s", message.c_str());
-#endif
+#else
   nout << "Assertion failed: " << message << "\n";
   nout << "Assertion failed: " << message << "\n";
+#endif
 
 
   // This is redefined here, shadowing the defining in config_prc.h, so we can
   // This is redefined here, shadowing the defining in config_prc.h, so we can
   // guarantee it has already been constructed.
   // guarantee it has already been constructed.
@@ -477,6 +480,12 @@ config_initialized() {
         }
         }
 #endif  // BUILD_IPHONE
 #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 "streamReader_ext.cxx"
 #include "streamWriter_ext.cxx"
 #include "streamWriter_ext.cxx"

+ 1 - 0
makepanda/config.in

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

+ 2 - 2
makepanda/installpanda.py

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

+ 23 - 17
makepanda/makepackage.py

@@ -6,6 +6,7 @@ import shutil
 import glob
 import glob
 import re
 import re
 import subprocess
 import subprocess
+import sysconfig
 from makepandacore import *
 from makepandacore import *
 from installpanda import *
 from installpanda import *
 
 
@@ -214,7 +215,7 @@ def MakeDebugSymbolArchive(zipname, dirname):
     zip.close()
     zip.close()
 
 
 
 
-def MakeInstallerLinux(version, debversion=None, rpmrelease=1,
+def MakeInstallerLinux(version, debversion=None, rpmversion=None, rpmrelease=1,
                        python_versions=[], **kwargs):
                        python_versions=[], **kwargs):
     outputdir = GetOutputDir()
     outputdir = GetOutputDir()
 
 
@@ -235,6 +236,8 @@ def MakeInstallerLinux(version, debversion=None, rpmrelease=1,
     major_version = '.'.join(version.split('.')[:2])
     major_version = '.'.join(version.split('.')[:2])
     if not debversion:
     if not debversion:
         debversion = version
         debversion = version
+    if not rpmversion:
+        rpmversion = version
 
 
     # Clean and set up a directory to install Panda3D into
     # Clean and set up a directory to install Panda3D into
     oscmd("rm -rf targetroot data.tar.gz control.tar.gz panda3d.spec")
     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)
                 txt += "/usr/bin/%s\n" % (base)
 
 
         # Write out the spec file.
         # Write out the spec file.
-        txt = txt.replace("VERSION", version)
+        txt = txt.replace("VERSION", rpmversion)
         txt = txt.replace("RPMRELEASE", str(rpmrelease))
         txt = txt.replace("RPMRELEASE", str(rpmrelease))
         txt = txt.replace("PANDASOURCE", pandasource)
         txt = txt.replace("PANDASOURCE", pandasource)
         WriteFile("panda3d.spec", txt)
         WriteFile("panda3d.spec", txt)
 
 
         oscmd("fakeroot rpmbuild --define '_rpmdir "+pandasource+"' --buildroot '"+os.path.abspath("targetroot")+"' -bb panda3d.spec")
         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)
         oscmd("rm -rf "+arch, True)
 
 
     else:
     else:
@@ -791,16 +794,8 @@ def MakeInstallerAndroid(version, **kwargs):
     if os.path.exists(apk_unsigned):
     if os.path.exists(apk_unsigned):
         os.unlink(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
     # 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.
     # 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))
                 copy_library(source, "libpy.panda3d.{}.so".format(modname))
 
 
         # Same for standard Python modules.
         # 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):
         for base in os.listdir(source_dir):
             if not base.endswith('.so'):
             if not base.endswith('.so'):
                 continue
                 continue
@@ -911,8 +910,7 @@ def MakeInstallerAndroid(version, **kwargs):
                     shutil.copy(os.path.join(source_dir, base), target)
                     shutil.copy(os.path.join(source_dir, base), target)
 
 
     # Copy the Python standard library to the .apk as well.
     # 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))
     stdlib_target = os.path.join("apkroot", "lib", "python{0}.{1}".format(*sys.version_info))
     copy_python_tree(stdlib_source, stdlib_target)
     copy_python_tree(stdlib_source, stdlib_target)
 
 
@@ -1025,6 +1023,13 @@ if __name__ == "__main__":
         help='Version number for .deb file',
         help='Version number for .deb file',
         default=None,
         default=None,
     )
     )
+    parser.add_option(
+        '',
+        '--rpmversion',
+        dest='rpmversion',
+        help='Version number for .rpm file',
+        default=None,
+    )
     parser.add_option(
     parser.add_option(
         '',
         '',
         '--rpmrelease',
         '--rpmrelease',
@@ -1094,6 +1099,7 @@ if __name__ == "__main__":
         optimize=GetOptimize(),
         optimize=GetOptimize(),
         compressor=options.compressor,
         compressor=options.compressor,
         debversion=options.debversion,
         debversion=options.debversion,
+        rpmversion=options.rpmversion,
         rpmrelease=options.rpmrelease,
         rpmrelease=options.rpmrelease,
         python_versions=ReadPythonVersionInfoFile(),
         python_versions=ReadPythonVersionInfoFile(),
         installdir=options.installdir,
         installdir=options.installdir,

+ 121 - 61
makepanda/makepanda.py

@@ -21,6 +21,7 @@ try:
     import threading
     import threading
     import signal
     import signal
     import shutil
     import shutil
+    import sysconfig
     import plistlib
     import plistlib
     import queue
     import queue
 except KeyboardInterrupt:
 except KeyboardInterrupt:
@@ -30,7 +31,10 @@ except:
     print("Please install the development package of Python and try again.")
     print("Please install the development package of Python and try again.")
     exit(1)
     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 *
 from makepandacore import *
 
 
 try:
 try:
@@ -63,6 +67,7 @@ DISTRIBUTOR=""
 VERSION=None
 VERSION=None
 DEBVERSION=None
 DEBVERSION=None
 WHLVERSION=None
 WHLVERSION=None
+RPMVERSION=None
 RPMRELEASE="1"
 RPMRELEASE="1"
 GIT_COMMIT=None
 GIT_COMMIT=None
 MAJOR_VERSION=None
 MAJOR_VERSION=None
@@ -152,8 +157,8 @@ def usage(problem):
     print("  --nothing         (disable every third-party lib)")
     print("  --nothing         (disable every third-party lib)")
     print("  --everything      (enable 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("  --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("  --use-icl         (experimental setting to use an intel compiler instead of MSVC on Windows)")
     print("")
     print("")
     print("The simplest way to compile panda is to just type:")
     print("The simplest way to compile panda is to just type:")
@@ -165,7 +170,7 @@ def usage(problem):
 def parseopts(args):
 def parseopts(args):
     global INSTALLER,WHEEL,RUNTESTS,GENMAN,DISTRIBUTOR,VERSION
     global INSTALLER,WHEEL,RUNTESTS,GENMAN,DISTRIBUTOR,VERSION
     global COMPRESSOR,THREADCOUNT,OSX_ARCHS
     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 STRDXSDKVERSION, WINDOWS_SDK, MSVC_VERSION, BOOUSEINTELCOMPILER
     global COPY_PYTHON
     global COPY_PYTHON
 
 
@@ -181,7 +186,7 @@ def parseopts(args):
         "help","distributor=","verbose","tests",
         "help","distributor=","verbose","tests",
         "optimize=","everything","nothing","installer","wheel","rtdist","nocolor",
         "optimize=","everything","nothing","installer","wheel","rtdist","nocolor",
         "version=","lzma","no-python","threads=","outputdir=","override=",
         "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",
         "directx-sdk=", "windows-sdk=", "msvc-version=", "clean", "use-icl",
         "universal", "target=", "arch=", "git-commit=", "no-copy-python",
         "universal", "target=", "arch=", "git-commit=", "no-copy-python",
         "glslang-incdir=", "glslang-libdir=",
         "glslang-incdir=", "glslang-libdir=",
@@ -230,6 +235,7 @@ def parseopts(args):
             elif (option=="--override"): AddOverride(value.strip())
             elif (option=="--override"): AddOverride(value.strip())
             elif (option=="--static"): SetLinkAllStatic(True)
             elif (option=="--static"): SetLinkAllStatic(True)
             elif (option=="--debversion"): DEBVERSION=value
             elif (option=="--debversion"): DEBVERSION=value
+            elif (option=="--rpmversion"): RPMVERSION=value
             elif (option=="--rpmrelease"): RPMRELEASE=value
             elif (option=="--rpmrelease"): RPMRELEASE=value
             elif (option=="--git-commit"): GIT_COMMIT=value
             elif (option=="--git-commit"): GIT_COMMIT=value
             # Backward compatibility, OPENGL was renamed to GL
             # Backward compatibility, OPENGL was renamed to GL
@@ -380,6 +386,9 @@ print("Version: %s" % VERSION)
 if DEBVERSION is None:
 if DEBVERSION is None:
     DEBVERSION = VERSION
     DEBVERSION = VERSION
 
 
+if RPMVERSION is None:
+    RPMVERSION = VERSION
+
 MAJOR_VERSION = '.'.join(VERSION.split('.')[:2])
 MAJOR_VERSION = '.'.join(VERSION.split('.')[:2])
 
 
 # Now determine the distutils-style platform tag for the target system.
 # 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:
     else:
         PLATFORM = 'manylinux2014-i686'
         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():
 elif not CrossCompiling():
     if HasTargetArch():
     if HasTargetArch():
         # Replace the architecture in the platform string.
         # 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,libssl.a")
         LibName("OPENSSL", "-Wl,--exclude-libs,libcrypto.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":
     if GetHost() != "darwin":
         # Workaround for an issue where pkg-config does not include this path
         # Workaround for an issue where pkg-config does not include this path
@@ -1056,6 +1075,7 @@ if (COMPILER=="GCC"):
         LibName("ALWAYS", '-llog')
         LibName("ALWAYS", '-llog')
         LibName("ANDROID", '-landroid')
         LibName("ANDROID", '-landroid')
         LibName("JNIGRAPHICS", '-ljnigraphics')
         LibName("JNIGRAPHICS", '-ljnigraphics')
+        LibName("OPENSLES", '-lOpenSLES')
 
 
     for pkg in MAYAVERSIONS:
     for pkg in MAYAVERSIONS:
         if (PkgSkip(pkg)==0 and (pkg in SDK)):
         if (PkgSkip(pkg)==0 and (pkg in SDK)):
@@ -1084,7 +1104,7 @@ if (COMPILER=="GCC"):
             LibName(pkg, "-lDependEngine")
             LibName(pkg, "-lDependEngine")
             LibName(pkg, "-lCommandEngine")
             LibName(pkg, "-lCommandEngine")
             LibName(pkg, "-lFoundation")
             LibName(pkg, "-lFoundation")
-            if pkg != "MAYA2020":
+            if pkg not in ("MAYA2020", "MAYA2022"):
                 LibName(pkg, "-lIMFbase")
                 LibName(pkg, "-lIMFbase")
             if GetTarget() != 'darwin':
             if GetTarget() != 'darwin':
                 LibName(pkg, "-lOpenMayalib")
                 LibName(pkg, "-lOpenMayalib")
@@ -1352,10 +1372,6 @@ def CompileCxx(obj,src,opts):
         if "SYSROOT" in SDK:
         if "SYSROOT" in SDK:
             if GetTarget() != "android":
             if GetTarget() != "android":
                 cmd += ' --sysroot=%s' % (SDK["SYSROOT"])
                 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'
             cmd += ' -no-canonical-prefixes'
 
 
         # Android-specific flags.
         # Android-specific flags.
@@ -1364,46 +1380,35 @@ def CompileCxx(obj,src,opts):
         if GetTarget() == "android":
         if GetTarget() == "android":
             # Most of the specific optimization flags here were
             # Most of the specific optimization flags here were
             # just copied from the default Android Makefiles.
             # 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:
             if "ANDROID_GCC_TOOLCHAIN" in SDK:
                 cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/')
                 cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/')
             cmd += ' -ffunction-sections -funwind-tables'
             cmd += ' -ffunction-sections -funwind-tables'
+            cmd += ' -target ' + SDK["ANDROID_TRIPLE"]
             if arch == 'armv7a':
             if arch == 'armv7a':
-                cmd += ' -target armv7-none-linux-androideabi'
                 cmd += ' -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16'
                 cmd += ' -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16'
-                cmd += ' -fno-integrated-as'
             elif arch == 'arm':
             elif arch == 'arm':
-                cmd += ' -target armv5te-none-linux-androideabi'
                 cmd += ' -march=armv5te -mtune=xscale -msoft-float'
                 cmd += ' -march=armv5te -mtune=xscale -msoft-float'
-                cmd += ' -fno-integrated-as'
-            elif arch == 'aarch64':
-                cmd += ' -target aarch64-none-linux-android'
             elif arch == 'mips':
             elif arch == 'mips':
-                cmd += ' -target mipsel-none-linux-android'
                 cmd += ' -mips32'
                 cmd += ' -mips32'
             elif arch == 'mips64':
             elif arch == 'mips64':
-                cmd += ' -target mips64el-none-linux-android'
                 cmd += ' -fintegrated-as'
                 cmd += ' -fintegrated-as'
             elif arch == 'x86':
             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'
                 cmd += ' -mstackrealign'
             elif arch == 'x86_64':
             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"
             cmd += " -Wa,--noexecstack"
 
 
             # Do we want thumb or arm instructions?
             # Do we want thumb or arm instructions?
-            if arch.startswith('arm'):
+            if arch != 'arm64' and arch.startswith('arm'):
                 if optlevel >= 3:
                 if optlevel >= 3:
                     cmd += ' -mthumb'
                     cmd += ' -mthumb'
                 else:
                 else:
                     cmd += ' -marm'
                     cmd += ' -marm'
 
 
             # Enable SIMD instructions if requested
             # 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'
                 cmd += ' -mfpu=neon'
 
 
         else:
         else:
@@ -1432,14 +1437,17 @@ def CompileCxx(obj,src,opts):
         # Needed by both Python, Panda, Eigen, all of which break aliasing rules.
         # Needed by both Python, Panda, Eigen, all of which break aliasing rules.
         cmd += " -fno-strict-aliasing"
         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==1): cmd += " -ggdb -D_DEBUG"
         if (optlevel==2): cmd += " -O1 -D_DEBUG"
         if (optlevel==2): cmd += " -O1 -D_DEBUG"
@@ -1892,28 +1900,16 @@ def CompileLink(dll, obj, opts):
             if "ANDROID_GCC_TOOLCHAIN" in SDK:
             if "ANDROID_GCC_TOOLCHAIN" in SDK:
                 cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/')
                 cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/')
             cmd += " -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now"
             cmd += " -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now"
+            cmd += ' -target ' + SDK["ANDROID_TRIPLE"]
             if arch == 'armv7a':
             if arch == 'armv7a':
-                cmd += ' -target armv7-none-linux-androideabi'
                 cmd += " -march=armv7-a -Wl,--fix-cortex-a8"
                 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':
             elif arch == 'mips':
-                cmd += ' -target mipsel-none-linux-android'
                 cmd += ' -mips32'
                 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'
             cmd += ' -lc -lm'
         else:
         else:
             cmd += " -pthread"
             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 != "":
         if LDFLAGS != "":
             cmd += " " + LDFLAGS
             cmd += " " + LDFLAGS
@@ -2177,6 +2173,31 @@ def CompileMIDL(target, src, opts):
 
 
         oscmd(cmd)
         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
 # CompileAnything
@@ -2288,6 +2309,9 @@ def CompileAnything(target, inputs, opts, progress = None):
         elif infile.endswith(".r"):
         elif infile.endswith(".r"):
             ProgressOutput(progress, "Building resource object", target)
             ProgressOutput(progress, "Building resource object", target)
             return CompileRsrc(target, infile, opts)
             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))
     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")
     configprc = configprc.replace("\nload-display pandagl", "\n#load-display pandagl")
 
 
 if PkgSkip("GLES") or GetLinkAllStatic():
 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():
 if PkgSkip("DX9") or GetLinkAllStatic():
     configprc = configprc.replace("\n#load-display pandadx9", "")
     configprc = configprc.replace("\n#load-display pandadx9", "")
@@ -3100,8 +3129,10 @@ else:
 if not PkgSkip("PANDATOOL"):
 if not PkgSkip("PANDATOOL"):
     CopyAllFiles(GetOutputDir()+"/plugins/",  "pandatool/src/scripts/", ".mel")
     CopyAllFiles(GetOutputDir()+"/plugins/",  "pandatool/src/scripts/", ".mel")
     CopyAllFiles(GetOutputDir()+"/plugins/",  "pandatool/src/scripts/", ".ms")
     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 */')
 ConditionalWriteFile(GetOutputDir()+'/include/ctl3d.h', '/* dummy file to make MAX happy */')
 
 
 # Since Eigen is included by all sorts of core headers, as a convenience
 # 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('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='openal_audio_openal_audio_composite1.obj')
     TargetAdd('libp3openal_audio.dll', input=COMMON_PANDA_LIBS)
     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/
 # 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/
 # 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', '')
     DefSymbol('GLES', 'OPENGLES_1', '')
     OPTS=['DIR:panda/src/egldisplay', 'DIR:panda/src/glstuff', 'BUILDING:PANDAGLES', 'GLES', 'EGL', 'X11']
     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')
     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/
 # 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', '')
     DefSymbol('GLES2', 'OPENGLES_2', '')
     OPTS=['DIR:panda/src/egldisplay', 'DIR:panda/src/glstuff', 'BUILDING:PANDAGLES2', 'GLES2', 'EGL', 'X11']
     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')
     TargetAdd('pandagles2_egldisplay_composite1.obj', opts=OPTS, input='p3egldisplay_composite1.cxx')
@@ -4904,12 +4935,17 @@ if not PkgSkip("PVIEW"):
 #
 #
 
 
 if GetTarget() == 'android':
 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/NativeIStream.class', opts=OPTS, input='NativeIStream.java')
     TargetAdd('org/panda3d/android/NativeOStream.class', opts=OPTS, input='NativeOStream.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/PandaActivity.class', opts=OPTS, input='PandaActivity.java')
     TargetAdd('org/panda3d/android/PythonActivity.class', opts=OPTS, input='PythonActivity.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('p3android_composite1.obj', opts=OPTS, input='p3android_composite1.cxx')
     TargetAdd('libp3android.dll', input='p3android_composite1.obj')
     TargetAdd('libp3android.dll', input='p3android_composite1.obj')
     TargetAdd('libp3android.dll', input=COMMON_PANDA_LIBS)
     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', input=COMMON_PANDA_LIBS)
     TargetAdd('libpandagles.dll', opts=['MODULE', 'GLES', 'EGL'])
     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/
 # DIRECTORY: panda/src/tinydisplay/
 #
 #
@@ -6055,10 +6105,11 @@ if PkgSkip("PYTHON") == 0:
         LibName('DEPLOYSTUB', "-Wl,-rpath,\\$ORIGIN")
         LibName('DEPLOYSTUB', "-Wl,-rpath,\\$ORIGIN")
         LibName('DEPLOYSTUB', "-Wl,-z,origin")
         LibName('DEPLOYSTUB', "-Wl,-z,origin")
         LibName('DEPLOYSTUB', "-rdynamic")
         LibName('DEPLOYSTUB', "-rdynamic")
+
     PyTargetAdd('deploy-stub.exe', input='deploy-stub.obj')
     PyTargetAdd('deploy-stub.exe', input='deploy-stub.obj')
     if GetTarget() == 'windows':
     if GetTarget() == 'windows':
         PyTargetAdd('deploy-stub.exe', input='frozen_dllmain.obj')
         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':
     if GetTarget() == 'windows':
         PyTargetAdd('deploy-stubw.exe', input='deploy-stub.obj')
         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.obj', opts=OPTS, input='deploy-stub.c')
         PyTargetAdd('deploy-stubw.exe', input='deploy-stubw.obj')
         PyTargetAdd('deploy-stubw.exe', input='deploy-stubw.obj')
         PyTargetAdd('deploy-stubw.exe', opts=['MACOS_APP_BUNDLE', 'DEPLOYSTUB', 'NOICON'])
         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
 # Generate the models directory and samples directory
@@ -6170,7 +6230,7 @@ def ParallelMake(tasklist):
     # Create the workers
     # Create the workers
     for slave in range(THREADCOUNT):
     for slave in range(THREADCOUNT):
         th = threading.Thread(target=BuildWorker, args=[taskqueue, donequeue])
         th = threading.Thread(target=BuildWorker, args=[taskqueue, donequeue])
-        th.setDaemon(1)
+        th.daemon = True
         th.start()
         th.start()
     # Feed tasks to the workers.
     # Feed tasks to the workers.
     tasksqueued = 0
     tasksqueued = 0
@@ -6261,8 +6321,8 @@ if INSTALLER:
 
 
     MakeInstaller(version=VERSION, outputdir=GetOutputDir(),
     MakeInstaller(version=VERSION, outputdir=GetOutputDir(),
                   optimize=GetOptimize(), compressor=COMPRESSOR,
                   optimize=GetOptimize(), compressor=COMPRESSOR,
-                  debversion=DEBVERSION, rpmrelease=RPMRELEASE,
-                  python_versions=python_versions)
+                  debversion=DEBVERSION, rpmversion=RPMVERSION,
+                  rpmrelease=RPMRELEASE, python_versions=python_versions)
 
 
 if WHEEL:
 if WHEEL:
     ProgressOutput(100.0, "Building wheel")
     ProgressOutput(100.0, "Building wheel")

+ 85 - 62
makepanda/makepandacore.py

@@ -6,7 +6,6 @@
 ########################################################################
 ########################################################################
 
 
 import configparser
 import configparser
-from distutils import sysconfig
 import fnmatch
 import fnmatch
 import getpass
 import getpass
 import glob
 import glob
@@ -18,6 +17,7 @@ import shutil
 import signal
 import signal
 import subprocess
 import subprocess
 import sys
 import sys
+import sysconfig
 import threading
 import threading
 import _thread as thread
 import _thread as thread
 import time
 import time
@@ -28,7 +28,7 @@ SUFFIX_LIB = [".lib",".ilb"]
 VCS_DIRS = set(["CVS", "CVSROOT", ".git", ".hg", "__pycache__"])
 VCS_DIRS = set(["CVS", "CVSROOT", ".git", ".hg", "__pycache__"])
 VCS_FILES = set([".cvsignore", ".gitignore", ".gitmodules", ".hgignore"])
 VCS_FILES = set([".cvsignore", ".gitignore", ".gitmodules", ".hgignore"])
 STARTTIME = time.time()
 STARTTIME = time.time()
-MAINTHREAD = threading.currentThread()
+MAINTHREAD = threading.current_thread()
 OUTPUTDIR = "built"
 OUTPUTDIR = "built"
 CUSTOM_OUTPUTDIR = False
 CUSTOM_OUTPUTDIR = False
 THIRDPARTYBASE = None
 THIRDPARTYBASE = None
@@ -80,6 +80,7 @@ MSVCVERSIONINFO = {
     (14,0): {"vsversion":(14,0), "vsname":"Visual Studio 2015"},
     (14,0): {"vsversion":(14,0), "vsname":"Visual Studio 2015"},
     (14,1): {"vsversion":(15,0), "vsname":"Visual Studio 2017"},
     (14,1): {"vsversion":(15,0), "vsname":"Visual Studio 2017"},
     (14,2): {"vsversion":(16,0), "vsname":"Visual Studio 2019"},
     (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"),
                    ("MAYA2018","2018"),
                    ("MAYA2019","2019"),
                    ("MAYA2019","2019"),
                    ("MAYA2020","2020"),
                    ("MAYA2020","2020"),
+                   ("MAYA2022","2022"),
 ]
 ]
 
 
 MAXVERSIONINFO = [("MAX6", "SOFTWARE\\Autodesk\\3DSMAX\\6.0", "installdir", "maxsdk\\cssdk\\include"),
 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.stdout.flush()
     sys.stderr.flush()
     sys.stderr.flush()
     prefix = ""
     prefix = ""
-    thisthread = threading.currentThread()
+    thisthread = threading.current_thread()
     if thisthread is MAINTHREAD:
     if thisthread is MAINTHREAD:
         if progress is None:
         if progress is None:
             prefix = ""
             prefix = ""
@@ -272,7 +274,7 @@ def ProgressOutput(progress, msg, target = None):
 def exit(msg = ""):
 def exit(msg = ""):
     sys.stdout.flush()
     sys.stdout.flush()
     sys.stderr.flush()
     sys.stderr.flush()
-    if threading.currentThread() == MAINTHREAD:
+    if threading.current_thread() == MAINTHREAD:
         SaveDependencyCache()
         SaveDependencyCache()
         print("Elapsed Time: " + PrettyTime(time.time() - STARTTIME))
         print("Elapsed Time: " + PrettyTime(time.time() - STARTTIME))
         print(msg)
         print(msg)
@@ -392,30 +394,30 @@ def SetTarget(target, arch=None):
             else:
             else:
                 arch = 'armv7a'
                 arch = 'armv7a'
 
 
-        if arch == 'arm64':
-            arch = 'aarch64'
+        if arch == 'aarch64':
+            arch = 'arm64'
 
 
         # Did we specify an API level?
         # Did we specify an API level?
         global ANDROID_API
         global ANDROID_API
         target, _, api = target.partition('-')
         target, _, api = target.partition('-')
         if api:
         if api:
             ANDROID_API = int(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.
             # 64-bit platforms were introduced in Android 21.
             ANDROID_API = 21
             ANDROID_API = 21
         else:
         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
         # Determine the prefix for our gcc tools, eg. arm-linux-androideabi-gcc
         global ANDROID_ABI, ANDROID_TRIPLE
         global ANDROID_ABI, ANDROID_TRIPLE
         if arch == 'armv7a':
         if arch == 'armv7a':
             ANDROID_ABI = 'armeabi-v7a'
             ANDROID_ABI = 'armeabi-v7a'
-            ANDROID_TRIPLE = 'arm-linux-androideabi'
+            ANDROID_TRIPLE = 'armv7a-linux-androideabi'
         elif arch == 'arm':
         elif arch == 'arm':
             ANDROID_ABI = 'armeabi'
             ANDROID_ABI = 'armeabi'
             ANDROID_TRIPLE = 'arm-linux-androideabi'
             ANDROID_TRIPLE = 'arm-linux-androideabi'
-        elif arch == 'aarch64':
+        elif arch == 'arm64':
             ANDROID_ABI = 'arm64-v8a'
             ANDROID_ABI = 'arm64-v8a'
             ANDROID_TRIPLE = 'aarch64-linux-android'
             ANDROID_TRIPLE = 'aarch64-linux-android'
         elif arch == 'mips':
         elif arch == 'mips':
@@ -431,8 +433,9 @@ def SetTarget(target, arch=None):
             ANDROID_ABI = 'x86_64'
             ANDROID_ABI = 'x86_64'
             ANDROID_TRIPLE = 'x86_64-linux-android'
             ANDROID_TRIPLE = 'x86_64-linux-android'
         else:
         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 + '-'
         TOOLCHAIN_PREFIX = ANDROID_TRIPLE + '-'
 
 
     elif target == 'linux':
     elif target == 'linux':
@@ -497,7 +500,7 @@ def GetCXX():
 def GetStrip():
 def GetStrip():
     # Hack
     # Hack
     if TARGET == 'android':
     if TARGET == 'android':
-        return TOOLCHAIN_PREFIX + 'strip'
+        return 'llvm-strip'
     else:
     else:
         return 'strip'
         return 'strip'
 
 
@@ -636,6 +639,7 @@ def oscmd(cmd, ignoreError = False, cwd=None):
             os.chdir(pwd)
             os.chdir(pwd)
     else:
     else:
         cmd = cmd.replace(';', '\\;')
         cmd = cmd.replace(';', '\\;')
+        cmd = cmd.replace('$', '\\$')
         res = subprocess.call(cmd, cwd=cwd, shell=True)
         res = subprocess.call(cmd, cwd=cwd, shell=True)
         sig = res & 0x7F
         sig = res & 0x7F
         if (GetVerbose() and res != 0):
         if (GetVerbose() and res != 0):
@@ -1361,7 +1365,7 @@ def GetThirdpartyDir():
             THIRDPARTYDIR = base + "/freebsd-libs-a/"
             THIRDPARTYDIR = base + "/freebsd-libs-a/"
 
 
     elif (target == 'android'):
     elif (target == 'android'):
-        THIRDPARTYDIR = GetThirdpartyBase()+"/android-libs-%s/" % (GetTargetArch())
+        THIRDPARTYDIR = base + "/android-libs-%s/" % (target_arch)
 
 
     else:
     else:
         Warn("Unsupported platform:", target)
         Warn("Unsupported platform:", target)
@@ -1556,8 +1560,8 @@ def PkgConfigGetIncDirs(pkgname, tool = "pkg-config"):
     else:
     else:
         handle = os.popen(LocateBinary(tool) + " --cflags")
         handle = os.popen(LocateBinary(tool) + " --cflags")
     result = handle.read().strip()
     result = handle.read().strip()
-    if len(result) == 0: return []
     handle.close()
     handle.close()
+    if len(result) == 0: return []
     dirs = []
     dirs = []
     for opt in result.split(" "):
     for opt in result.split(" "):
         if (opt.startswith("-I")):
         if (opt.startswith("-I")):
@@ -2174,12 +2178,12 @@ def SdkLocatePython(prefer_thirdparty_python=False):
         LibDirectory("PYTHON", py_fwx + "/lib")
         LibDirectory("PYTHON", py_fwx + "/lib")
 
 
     #elif GetTarget() == 'windows':
     #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["PYTHONVERSION"] = "python" + sysconfig.get_python_version()
     #    SDK["PYTHONEXEC"] = sys.executable
     #    SDK["PYTHONEXEC"] = sys.executable
 
 
     else:
     else:
-        SDK["PYTHON"] = sysconfig.get_python_inc()
+        SDK["PYTHON"] = sysconfig.get_path("include")
         SDK["PYTHONVERSION"] = "python" + sysconfig.get_python_version() + abiflags
         SDK["PYTHONVERSION"] = "python" + sysconfig.get_python_version() + abiflags
         SDK["PYTHONEXEC"] = os.path.realpath(sys.executable)
         SDK["PYTHONEXEC"] = os.path.realpath(sys.executable)
 
 
@@ -2286,7 +2290,7 @@ def SdkLocateWindows(version=None):
     if version == '10':
     if version == '10':
         version = '10.0'
         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.
         # Choose the latest version of the Windows 10 SDK.
         platsdk = GetRegistryKey("SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots", "KitsRoot10")
         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\\"
             platsdk = "C:\\Program Files (x86)\\Windows Kits\\10\\"
 
 
         if platsdk and os.path.isdir(platsdk):
         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 + '.*.*'))
             incdirs = glob.glob(os.path.join(platsdk, 'Include', version + '.*.*'))
+
             max_version = ()
             max_version = ()
             for dir in incdirs:
             for dir in incdirs:
                 verstring = os.path.basename(dir)
                 verstring = os.path.basename(dir)
@@ -2313,7 +2323,7 @@ def SdkLocateWindows(version=None):
                     continue
                     continue
 
 
                 vertuple = tuple(map(int, verstring.split('.')))
                 vertuple = tuple(map(int, verstring.split('.')))
-                if vertuple > max_version:
+                if vertuple > max_version and vertuple > min_version:
                     version = verstring
                     version = verstring
                     max_version = vertuple
                     max_version = vertuple
 
 
@@ -2391,7 +2401,7 @@ def SdkLocateMacOSX(archs = []):
         # Prefer pre-10.14 for now so that we can keep building FMOD.
         # 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 += ["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:
     if 'arm64' not in archs:
         sdk_versions += ["10.15", "10.14"]
         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('\\', '/')
     SDK["SYSROOT"] = os.path.join(ndk_root, 'platforms', 'android-%s' % (api), arch_dir).replace('\\', '/')
     #IncDirectory("ALWAYS", os.path.join(SDK["SYSROOT"], 'usr', 'include'))
     #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++')
     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')
     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)
     CopyFile(os.path.join(GetOutputDir(), 'lib', 'libc++_shared.so'), stl_lib)
 
 
     # The Android support library polyfills C++ features not available in the
     # The Android support library polyfills C++ features not available in the
     # STL that ships with Android.
     # 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:
     if api < 21:
         LibName("ALWAYS", "-landroid_support")
         LibName("ALWAYS", "-landroid_support")
 
 
@@ -2760,7 +2766,7 @@ def SetupVisualStudioEnviron():
         elif not win_kit.endswith('\\'):
         elif not win_kit.endswith('\\'):
             win_kit += '\\'
             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)
             version = "10.0.{0}.0".format(vnum)
             if os.path.isfile(win_kit + "Include\\" + version + "\\ucrt\\assert.h"):
             if os.path.isfile(win_kit + "Include\\" + version + "\\ucrt\\assert.h"):
                 print("Using Universal CRT %s" % (version))
                 print("Using Universal CRT %s" % (version))
@@ -2877,8 +2883,10 @@ def SetupBuildEnvironment(compiler):
         if SDK.get("MACOSX"):
         if SDK.get("MACOSX"):
             # The default compiler in Leopard does not respect --sysroot correctly.
             # The default compiler in Leopard does not respect --sysroot correctly.
             sysroot_flag = " -isysroot " + SDK["MACOSX"]
             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: ='.
         # Extract the dirs from the line that starts with 'libraries: ='.
         cmd = GetCXX() + " -print-search-dirs" + sysroot_flag
         cmd = GetCXX() + " -print-search-dirs" + sysroot_flag
@@ -2914,12 +2922,7 @@ def SetupBuildEnvironment(compiler):
 
 
         # Now extract the preprocessor's include directories.
         # Now extract the preprocessor's include directories.
         cmd = GetCXX() + " -x c++ -v -E " + os.devnull
         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')
         null = open(os.devnull, 'w')
         handle = subprocess.Popen(cmd, stdout=null, stderr=subprocess.PIPE, shell=True)
         handle = subprocess.Popen(cmd, stdout=null, stderr=subprocess.PIPE, shell=True)
@@ -3061,13 +3064,16 @@ def CopyAllHeaders(dir, skip=[]):
             WriteBinaryFile(dstfile, ReadBinaryFile(srcfile))
             WriteBinaryFile(dstfile, ReadBinaryFile(srcfile))
             JustBuilt([dstfile], [srcfile])
             JustBuilt([dstfile], [srcfile])
 
 
-def CopyTree(dstdir, srcdir, omitVCS=True):
+def CopyTree(dstdir, srcdir, omitVCS=True, exclude=()):
     if os.path.isdir(dstdir):
     if os.path.isdir(dstdir):
         source_entries = os.listdir(srcdir)
         source_entries = os.listdir(srcdir)
         for entry in source_entries:
         for entry in source_entries:
             srcpth = os.path.join(srcdir, entry)
             srcpth = os.path.join(srcdir, entry)
             dstpth = os.path.join(dstdir, entry)
             dstpth = os.path.join(dstdir, entry)
 
 
+            if entry in exclude:
+                continue
+
             if os.path.islink(srcpth) or os.path.isfile(srcpth):
             if os.path.islink(srcpth) or os.path.isfile(srcpth):
                 if not omitVCS or entry not in VCS_FILES:
                 if not omitVCS or entry not in VCS_FILES:
                     CopyFile(dstpth, srcpth)
                     CopyFile(dstpth, srcpth)
@@ -3077,7 +3083,7 @@ def CopyTree(dstdir, srcdir, omitVCS=True):
 
 
         # Delete files in dstdir that are not in srcdir.
         # Delete files in dstdir that are not in srcdir.
         for entry in os.listdir(dstdir):
         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)
                 path = os.path.join(dstdir, entry)
                 if os.path.islink(path) or os.path.isfile(path):
                 if os.path.islink(path) or os.path.isfile(path):
                     os.remove(path)
                     os.remove(path)
@@ -3093,6 +3099,13 @@ def CopyTree(dstdir, srcdir, omitVCS=True):
             if subprocess.call(['cp', '-R', '-f', srcdir, dstdir]) != 0:
             if subprocess.call(['cp', '-R', '-f', srcdir, dstdir]) != 0:
                 exit("Copy failed.")
                 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:
         if omitVCS:
             DeleteVCS(dstdir)
             DeleteVCS(dstdir)
 
 
@@ -3223,6 +3236,22 @@ def WriteResourceFile(basename, **kwargs):
     return basename
     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):
 def WriteEmbeddedStringFile(basename, inputs, string_name=None):
     if os.path.splitext(basename)[1] not in SUFFIX_INC:
     if os.path.splitext(basename)[1] not in SUFFIX_INC:
         basename += '.cxx'
         basename += '.cxx'
@@ -3247,20 +3276,7 @@ def WriteEmbeddedStringFile(basename, inputs, string_name=None):
 
 
     data.append(0)
     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)
     ConditionalWriteFile(target, output)
     return target
     return target
 
 
@@ -3281,13 +3297,17 @@ def SetOrigExt(x, v):
     ORIG_EXT[x] = v
     ORIG_EXT[x] = v
 
 
 def GetExtensionSuffix():
 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():
 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])
     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 (GetOptimize() <= 2 and target == 'windows'): dllext = "_d"
 
 
     if (fn == "AndroidManifest.xml"): return OUTPUTDIR+"/"+fn
     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(".cxx")): return CxxFindSource(fn, ipath)
     if (fn.endswith(".I")):   return CxxFindSource(fn, ipath)
     if (fn.endswith(".I")):   return CxxFindSource(fn, ipath)
     if (fn.endswith(".h")):   return CxxFindSource(fn, ipath)
     if (fn.endswith(".h")):   return CxxFindSource(fn, ipath)
@@ -3413,14 +3434,13 @@ def GetCurrentPythonVersionInfo():
     if PkgSkip("PYTHON"):
     if PkgSkip("PYTHON"):
         return
         return
 
 
-    from distutils.sysconfig import get_python_lib
     return {
     return {
         "version": SDK["PYTHONVERSION"][6:].rstrip('dmu'),
         "version": SDK["PYTHONVERSION"][6:].rstrip('dmu'),
         "soabi": GetPythonABI(),
         "soabi": GetPythonABI(),
         "ext_suffix": GetExtensionSuffix(),
         "ext_suffix": GetExtensionSuffix(),
         "executable": sys.executable,
         "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 = []
     json_data = []
     if os.path.isfile(json_file) and not PkgSkip("PYTHON"):
     if os.path.isfile(json_file) and not PkgSkip("PYTHON"):
         try:
         try:
-            json_data = json.load(open(json_file, 'r'))
+            with open(json_file, 'r') as fh:
+                json_data = json.load(fh)
         except:
         except:
             json_data = []
             json_data = []
 
 
@@ -3451,7 +3472,9 @@ def UpdatePythonVersionInfoFile(new_info):
 
 
     if VERBOSE:
     if VERBOSE:
         print("Writing %s" % (json_file))
         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():
 def ReadPythonVersionInfoFile():

+ 131 - 27
makepanda/makewheel.py

@@ -10,11 +10,11 @@ import hashlib
 import tempfile
 import tempfile
 import subprocess
 import subprocess
 import time
 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 optparse import OptionParser
 from base64 import urlsafe_b64encode
 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():
 def get_abi_tag():
@@ -65,8 +65,11 @@ def is_fat_file(path):
 
 
 
 
 def get_python_ext_module_dir():
 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'):
 if sys.platform in ('win32', 'cygwin'):
@@ -251,16 +254,72 @@ def parse_dependencies_unix(data):
     return filenames
     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):
 def scan_dependencies(pathname):
     """ Checks the named file for DLL dependencies, and adds any appropriate
     """ Checks the named file for DLL dependencies, and adds any appropriate
     dependencies found into pluginDependencies and dependentFiles. """
     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":
     if sys.platform == "darwin":
         command = ['otool', '-XL', pathname]
         command = ['otool', '-XL', pathname]
     elif sys.platform in ("win32", "cygwin"):
     elif sys.platform in ("win32", "cygwin"):
         command = ['dumpbin', '/dependents', pathname]
         command = ['dumpbin', '/dependents', pathname]
     else:
     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)
     process = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True)
     output, unused_err = process.communicate()
     output, unused_err = process.communicate()
@@ -322,18 +381,24 @@ class WheelFile(object):
 
 
         self.dep_paths[dep] = None
         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():
             if GetVerbose():
                 print("Ignoring {0} (explicitly ignored)".format(dep))
                 print("Ignoring {0} (explicitly ignored)".format(dep))
             return
             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('/'):
         if dep.startswith('/'):
             source_path = dep
             source_path = dep
@@ -386,7 +451,7 @@ class WheelFile(object):
             temp = tempfile.NamedTemporaryFile(suffix=suffix, prefix='whl', delete=False)
             temp = tempfile.NamedTemporaryFile(suffix=suffix, prefix='whl', delete=False)
 
 
             # On macOS, if no fat wheel was requested, extract the right architecture.
             # 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 not self.platform.endswith("_intel") \
                 and "_fat" not in self.platform \
                 and "_fat" not in self.platform \
                 and "_universal" 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)
             os.chmod(temp.name, os.stat(temp.name).st_mode | 0o711)
 
 
             # Now add dependencies.  On macOS, fix @loader_path references.
             # Now add dependencies.  On macOS, fix @loader_path references.
-            if sys.platform == "darwin":
+            if self.platform.startswith("macosx"):
                 if source_path.endswith('deploy-stubw'):
                 if source_path.endswith('deploy-stubw'):
                     deps_path = '@executable_path/../Frameworks'
                     deps_path = '@executable_path/../Frameworks'
                 else:
                 else:
@@ -457,12 +522,32 @@ class WheelFile(object):
                 # On other unixes, we just add dependencies normally.
                 # On other unixes, we just add dependencies normally.
                 for dep in deps:
                 for dep in deps:
                     # Only include dependencies with relative path, for now.
                     # 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
                         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
             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.")
             raise Exception("patchelf is required when building a Linux wheel.")
 
 
     if sys.version_info < (3, 6):
     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:
     if platform is None:
         # Determine the platform from the build.
         # Determine the platform from the build.
@@ -568,9 +653,16 @@ def makewheel(version, output_dir, platform=None):
                     platform = platform.replace("linux", "manylinux2010")
                     platform = platform.replace("linux", "manylinux2010")
                 elif os.path.isfile("/lib/libc-2.17.so") or os.path.isfile("/lib64/libc-2.17.so"):
                 elif os.path.isfile("/lib/libc-2.17.so") or os.path.isfile("/lib64/libc-2.17.so"):
                     platform = platform.replace("linux", "manylinux2014")
                     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('.', '_')
     platform = platform.replace('-', '_').replace('.', '_')
 
 
+    is_windows = platform == 'win32' \
+        or platform.startswith('win_') \
+        or platform.startswith('cygwin_')
+    is_macosx = platform.startswith('macosx_')
+
     # Global filepaths
     # Global filepaths
     panda3d_dir = join(output_dir, "panda3d")
     panda3d_dir = join(output_dir, "panda3d")
     pandac_dir = join(output_dir, "pandac")
     pandac_dir = join(output_dir, "pandac")
@@ -578,7 +670,7 @@ def makewheel(version, output_dir, platform=None):
     models_dir = join(output_dir, "models")
     models_dir = join(output_dir, "models")
     etc_dir = join(output_dir, "etc")
     etc_dir = join(output_dir, "etc")
     bin_dir = join(output_dir, "bin")
     bin_dir = join(output_dir, "bin")
-    if sys.platform == "win32":
+    if is_windows:
         libs_dir = join(output_dir, "bin")
         libs_dir = join(output_dir, "bin")
     else:
     else:
         libs_dir = join(output_dir, "lib")
         libs_dir = join(output_dir, "lib")
@@ -613,7 +705,7 @@ def makewheel(version, output_dir, platform=None):
     whl = WheelFile('panda3d', version, platform)
     whl = WheelFile('panda3d', version, platform)
     whl.lib_path = [libs_dir]
     whl.lib_path = [libs_dir]
 
 
-    if sys.platform == "win32":
+    if is_windows:
         whl.lib_path.append(ext_mod_dir)
         whl.lib_path.append(ext_mod_dir)
 
 
     if platform.startswith("manylinux"):
     if platform.startswith("manylinux"):
@@ -629,10 +721,10 @@ def makewheel(version, output_dir, platform=None):
         whl.ignore_deps.update(MANYLINUX_LIBS)
         whl.ignore_deps.update(MANYLINUX_LIBS)
 
 
     # Add libpython for deployment.
     # Add libpython for deployment.
-    if sys.platform in ('win32', 'cygwin'):
+    if is_windows:
         pylib_name = 'python{0}{1}.dll'.format(*sys.version_info)
         pylib_name = 'python{0}{1}.dll'.format(*sys.version_info)
         pylib_path = os.path.join(get_config_var('BINDIR'), pylib_name)
         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_name = 'libpython{0}.{1}.dylib'.format(*sys.version_info)
         pylib_path = os.path.join(get_config_var('LIBDIR'), pylib_name)
         pylib_path = os.path.join(get_config_var('LIBDIR'), pylib_name)
     else:
     else:
@@ -679,6 +771,9 @@ if __debug__:
             if file.endswith('.pyd') and platform.startswith('cygwin'):
             if file.endswith('.pyd') and platform.startswith('cygwin'):
                 # Rename it to .dll for cygwin Python to be able to load it.
                 # Rename it to .dll for cygwin Python to be able to load it.
                 target_path = 'panda3d/' + os.path.splitext(file)[0] + '.dll'
                 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:
             else:
                 target_path = 'panda3d/' + file
                 target_path = 'panda3d/' + file
 
 
@@ -686,7 +781,7 @@ if __debug__:
 
 
     # And copy the extension modules from the Python installation into the
     # And copy the extension modules from the Python installation into the
     # deploy_libs directory, for use by deploy-ng.
     # 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)):
     for file in sorted(os.listdir(ext_mod_dir)):
         if file.endswith(ext_suffix):
         if file.endswith(ext_suffix):
@@ -703,9 +798,9 @@ if __debug__:
     # Add plug-ins.
     # Add plug-ins.
     for lib in PLUGIN_LIBS:
     for lib in PLUGIN_LIBS:
         plugin_name = 'lib' + lib
         plugin_name = 'lib' + lib
-        if sys.platform in ('win32', 'cygwin'):
+        if is_windows:
             plugin_name += '.dll'
             plugin_name += '.dll'
-        elif sys.platform == 'darwin':
+        elif is_macosx:
             plugin_name += '.dylib'
             plugin_name += '.dylib'
         else:
         else:
             plugin_name += '.so'
             plugin_name += '.so'
@@ -713,6 +808,15 @@ if __debug__:
         if os.path.isfile(plugin_path):
         if os.path.isfile(plugin_path):
             whl.write_file('panda3d/' + plugin_name, 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.
     # Add the .data directory, containing additional files.
     data_dir = 'panda3d-{0}.data'.format(version)
     data_dir = 'panda3d-{0}.data'.format(version)
     #whl.write_directory(data_dir + '/data/etc', etc_dir)
     #whl.write_directory(data_dir + '/data/etc', etc_dir)

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

@@ -9,8 +9,13 @@
 #define OPENGLES_2
 #define OPENGLES_2
 #include "config_gles2gsg.h"
 #include "config_gles2gsg.h"
 
 
+#if defined(ANDROID)
+#include "config_androiddisplay.h"
+#include "androidGraphicsPipe.h"
+#else
 #include "config_egldisplay.h"
 #include "config_egldisplay.h"
 #include "eglGraphicsPipe.h"
 #include "eglGraphicsPipe.h"
+#endif
 
 
 /**
 /**
  * Initializes the library.  This must be called at least once before any of
  * Initializes the library.  This must be called at least once before any of
@@ -21,7 +26,12 @@
 void
 void
 init_libpandagles2() {
 init_libpandagles2() {
   init_libgles2gsg();
   init_libgles2gsg();
+
+#if defined(ANDROID)
+  init_libandroiddisplay();
+#else
   init_libegldisplay();
   init_libegldisplay();
+#endif
 }
 }
 
 
 /**
 /**
@@ -30,5 +40,9 @@ init_libpandagles2() {
  */
  */
 int
 int
 get_pipe_type_pandagles2() {
 get_pipe_type_pandagles2() {
+#if defined(ANDROID)
+  return AndroidGraphicsPipe::get_class_type().get_index();
+#else
   return eglGraphicsPipe::get_class_type().get_index();
   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.app.NativeActivity;
 import android.content.Intent;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.net.Uri;
 import android.widget.Toast;
 import android.widget.Toast;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.BitmapFactory;
+import dalvik.system.BaseDexClassLoader;
 import org.panda3d.android.NativeIStream;
 import org.panda3d.android.NativeIStream;
 import org.panda3d.android.NativeOStream;
 import org.panda3d.android.NativeOStream;
 
 
@@ -74,6 +77,26 @@ public class PandaActivity extends NativeActivity {
         return Thread.currentThread().getName();
         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() {
     public String getIntentDataPath() {
         Intent intent = getIntent();
         Intent intent = getIntent();
         Uri data = intent.getData();
         Uri data = intent.getData();
@@ -96,6 +119,9 @@ public class PandaActivity extends NativeActivity {
         return getCacheDir().toString();
         return getCacheDir().toString();
     }
     }
 
 
+    /**
+     * Shows a pop-up notification.
+     */
     public void showToast(final String text, final int duration) {
     public void showToast(final String text, final int duration) {
         final PandaActivity activity = this;
         final PandaActivity activity = this;
         runOnUiThread(new Runnable() {
         runOnUiThread(new Runnable() {
@@ -107,14 +133,10 @@ public class PandaActivity extends NativeActivity {
     }
     }
 
 
     static {
     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.
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * See the License for the specific language governing permissions and
  * limitations under the License.
  * limitations under the License.
- *
  */
  */
 
 
+#include "android_native_app_glue.h"
+
 #include <jni.h>
 #include <jni.h>
 
 
 #include <errno.h>
 #include <errno.h>
 #include <stdlib.h>
 #include <stdlib.h>
 #include <string.h>
 #include <string.h>
 #include <unistd.h>
 #include <unistd.h>
-#include <sys/resource.h>
 
 
-#include "android_native_app_glue.h"
 #include <android/log.h>
 #include <android/log.h>
 
 
 #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "threaded_app", __VA_ARGS__))
 #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 android_app_read_cmd(struct android_app* android_app) {
     int8_t cmd;
     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!");
         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) {
 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) {
 void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
     switch (cmd) {
     switch (cmd) {
         case APP_CMD_INPUT_CHANGED:
         case APP_CMD_INPUT_CHANGED:
-            LOGV("APP_CMD_INPUT_CHANGED\n");
+            LOGV("APP_CMD_INPUT_CHANGED");
             pthread_mutex_lock(&android_app->mutex);
             pthread_mutex_lock(&android_app->mutex);
             if (android_app->inputQueue != NULL) {
             if (android_app->inputQueue != NULL) {
                 AInputQueue_detachLooper(android_app->inputQueue);
                 AInputQueue_detachLooper(android_app->inputQueue);
@@ -106,7 +100,7 @@ void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
             break;
             break;
 
 
         case APP_CMD_INIT_WINDOW:
         case APP_CMD_INIT_WINDOW:
-            LOGV("APP_CMD_INIT_WINDOW\n");
+            LOGV("APP_CMD_INIT_WINDOW");
             pthread_mutex_lock(&android_app->mutex);
             pthread_mutex_lock(&android_app->mutex);
             android_app->window = android_app->pendingWindow;
             android_app->window = android_app->pendingWindow;
             pthread_cond_broadcast(&android_app->cond);
             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;
             break;
 
 
         case APP_CMD_TERM_WINDOW:
         case APP_CMD_TERM_WINDOW:
-            LOGV("APP_CMD_TERM_WINDOW\n");
+            LOGV("APP_CMD_TERM_WINDOW");
             pthread_cond_broadcast(&android_app->cond);
             pthread_cond_broadcast(&android_app->cond);
             break;
             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_START:
         case APP_CMD_PAUSE:
         case APP_CMD_PAUSE:
         case APP_CMD_STOP:
         case APP_CMD_STOP:
-            LOGV("activityState=%d\n", cmd);
+            LOGV("activityState=%d", cmd);
             pthread_mutex_lock(&android_app->mutex);
             pthread_mutex_lock(&android_app->mutex);
             android_app->activityState = cmd;
             android_app->activityState = cmd;
             pthread_cond_broadcast(&android_app->cond);
             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;
             break;
 
 
         case APP_CMD_CONFIG_CHANGED:
         case APP_CMD_CONFIG_CHANGED:
-            LOGV("APP_CMD_CONFIG_CHANGED\n");
+            LOGV("APP_CMD_CONFIG_CHANGED");
             AConfiguration_fromAssetManager(android_app->config,
             AConfiguration_fromAssetManager(android_app->config,
                     android_app->activity->assetManager);
                     android_app->activity->assetManager);
             print_cur_config(android_app);
             print_cur_config(android_app);
             break;
             break;
 
 
         case APP_CMD_DESTROY:
         case APP_CMD_DESTROY:
-            LOGV("APP_CMD_DESTROY\n");
+            LOGV("APP_CMD_DESTROY");
             android_app->destroyRequested = 1;
             android_app->destroyRequested = 1;
             break;
             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) {
 void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) {
     switch (cmd) {
     switch (cmd) {
         case APP_CMD_TERM_WINDOW:
         case APP_CMD_TERM_WINDOW:
-            LOGV("APP_CMD_TERM_WINDOW\n");
+            LOGV("APP_CMD_TERM_WINDOW");
             pthread_mutex_lock(&android_app->mutex);
             pthread_mutex_lock(&android_app->mutex);
             android_app->window = NULL;
             android_app->window = NULL;
             pthread_cond_broadcast(&android_app->cond);
             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;
             break;
 
 
         case APP_CMD_SAVE_STATE:
         case APP_CMD_SAVE_STATE:
-            LOGV("APP_CMD_SAVE_STATE\n");
+            LOGV("APP_CMD_SAVE_STATE");
             pthread_mutex_lock(&android_app->mutex);
             pthread_mutex_lock(&android_app->mutex);
             android_app->stateSaved = 1;
             android_app->stateSaved = 1;
             pthread_cond_broadcast(&android_app->cond);
             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() {
 void app_dummy() {
-
 }
 }
 
 
 static void android_app_destroy(struct android_app* android_app) {
 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) {
 static void process_input(struct android_app* app, struct android_poll_source* source) {
     AInputEvent* event = NULL;
     AInputEvent* event = NULL;
     while (AInputQueue_getEvent(app->inputQueue, &event) >= 0) {
     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)) {
         if (AInputQueue_preDispatchEvent(app->inputQueue, event)) {
             continue;
             continue;
         }
         }
@@ -241,9 +234,8 @@ static void* android_app_entry(void* param) {
 // --------------------------------------------------------------------
 // --------------------------------------------------------------------
 
 
 static struct android_app* android_app_create(ANativeActivity* activity,
 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;
     android_app->activity = activity;
 
 
     pthread_mutex_init(&android_app->mutex, NULL);
     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->msgread = msgpipe[0];
     android_app->msgwrite = msgpipe[1];
     android_app->msgwrite = msgpipe[1];
 
 
-    pthread_attr_t attr; 
+    pthread_attr_t attr;
     pthread_attr_init(&attr);
     pthread_attr_init(&attr);
     pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
     pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
     pthread_create(&android_app->thread, &attr, android_app_entry, android_app);
     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) {
 static void android_app_write_cmd(struct android_app* android_app, int8_t cmd) {
     if (write(android_app->msgwrite, &cmd, sizeof(cmd)) != sizeof(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);
     free(android_app);
 }
 }
 
 
+static struct android_app* ToApp(ANativeActivity* activity) {
+    return (struct android_app*) activity->instance;
+}
+
 static void onDestroy(ANativeActivity* activity) {
 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) {
 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) {
 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) {
 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);
     pthread_mutex_lock(&android_app->mutex);
     android_app->stateSaved = 0;
     android_app->stateSaved = 0;
     android_app_write_cmd(android_app, APP_CMD_SAVE_STATE);
     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) {
 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) {
 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) {
 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) {
 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) {
 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) {
 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) {
 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) {
 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) {
 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
 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->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->onResume = onResume;
     activity->callbacks->onSaveInstanceState = onSaveInstanceState;
     activity->callbacks->onSaveInstanceState = onSaveInstanceState;
-    activity->callbacks->onPause = onPause;
+    activity->callbacks->onStart = onStart;
     activity->callbacks->onStop = onStop;
     activity->callbacks->onStop = onStop;
-    activity->callbacks->onConfigurationChanged = onConfigurationChanged;
-    activity->callbacks->onLowMemory = onLowMemory;
     activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
     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);
     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.
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * See the License for the specific language governing permissions and
  * limitations under the License.
  * limitations under the License.
- *
  */
  */
 
 
-#ifndef _ANDROID_NATIVE_APP_GLUE_H
-#define _ANDROID_NATIVE_APP_GLUE_H
+#pragma once
 
 
 #include <poll.h>
 #include <poll.h>
 #include <pthread.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);
 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")))
  * glue code. No longer necessary, since __attribute__((visibility("default")))
  * does this for us.
  * does this for us.
  */
  */
@@ -350,5 +348,3 @@ extern void android_main(struct android_app* app);
 #ifdef __cplusplus
 #ifdef __cplusplus
 }
 }
 #endif
 #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_bounds(bounds);
   set_final(true);
   set_final(true);
   set_overall_hidden(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
  * 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
  * 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_combine_children() const;
   virtual bool safe_to_flatten_below() const;
   virtual bool safe_to_flatten_below() const;
 
 
-  virtual bool is_renderable() const;
   virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);
   virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);
 
 
 private:
 private:

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

@@ -125,7 +125,7 @@ BulletWorld() {
   // Some prefered settings
   // Some prefered settings
   _world->getDispatchInfo().m_enableSPU = true;      // default: true
   _world->getDispatchInfo().m_enableSPU = true;      // default: true
   _world->getDispatchInfo().m_useContinuous = 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;
   _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 "
          "contact events might create more load on the event queue "
          "then you might want! Default value is FALSE."));
          "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
 ConfigVariableInt bullet_solver_iterations
 ("bullet-solver-iterations", 10,
 ("bullet-solver-iterations", 10,
 PRC_DESC("Specifies the number of iterations for the Bullet contact "
 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 ConfigVariableEnum<BulletWorld::FilterAlgorithm> bullet_filter_algorithm;
 extern ConfigVariableDouble bullet_sap_extents;
 extern ConfigVariableDouble bullet_sap_extents;
 extern ConfigVariableBool bullet_enable_contact_events;
 extern ConfigVariableBool bullet_enable_contact_events;
+extern ConfigVariableBool bullet_split_impulse;
 extern ConfigVariableInt bullet_solver_iterations;
 extern ConfigVariableInt bullet_solver_iterations;
 extern ConfigVariableBool bullet_additional_damping;
 extern ConfigVariableBool bullet_additional_damping;
 extern ConfigVariableDouble bullet_additional_damping_linear_factor;
 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;
     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;
   plist<int> holes;
   int channel_index = 0;
   int channel_index = 0;
   pick_channel_index(holes, channel_index);
   pick_channel_index(holes, channel_index);
@@ -616,7 +620,6 @@ do_bind_anim(AnimControl *control, AnimBundle *anim,
                  subset.is_include_empty(), bound_joints, subset);
                  subset.is_include_empty(), bound_joints, subset);
   control->setup_anim(this, anim, channel_index, bound_joints);
   control->setup_anim(this, anim, channel_index, bound_joints);
 
 
-  CDReader cdata(_cycler);
   determine_effective_channels(cdata);
   determine_effective_channels(cdata);
 
 
   return true;
   return true;

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

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

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

@@ -30,9 +30,9 @@ CollisionLevelState(const NodePath &node_path) :
  */
  */
 template<class MaskType>
 template<class MaskType>
 INLINE CollisionLevelState<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),
   CollisionLevelStateBase(parent, child),
-  _current(parent._current)
+  _current(mask)
 {
 {
 }
 }
 #endif  // CPPPARSER
 #endif  // CPPPARSER
@@ -101,21 +101,21 @@ prepare_collider(const ColliderDef &def, const NodePath &root) {
 template<class MaskType>
 template<class MaskType>
 bool CollisionLevelState<MaskType>::
 bool CollisionLevelState<MaskType>::
 any_in_bounds() {
 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;
   int indent_level = 0;
-  if (collide_cat.is_spam()) {
+  if (is_spam) {
     indent_level = _node_path.get_num_nodes() * 2;
     indent_level = _node_path.get_num_nodes() * 2;
     collide_cat.spam();
     collide_cat.spam();
     indent(collide_cat.spam(false), indent_level)
     indent(collide_cat.spam(false), indent_level)
       << "Considering " << _node_path.get_node_path() << "\n";
       << "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();
     CollideMask this_mask = pnode->get_net_collide_mask();
 
 
     int num_colliders = get_num_colliders();
     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
           // Also don't test a node with itself, or with any of its
           // descendants.
           // descendants.
           if (pnode == cnode) {
           if (pnode == cnode) {
-#ifndef NDEBUG
-            if (collide_cat.is_spam()) {
+            if (is_spam) {
               indent(collide_cat.spam(false), indent_level)
               indent(collide_cat.spam(false), indent_level)
                 << "Not comparing " << c << " to " << _node_path
                 << "Not comparing " << c << " to " << _node_path
                 << " (same node)\n";
                 << " (same node)\n";
             }
             }
-#endif  // NDEBUG
 
 
           } else {
           } else {
             // There are bits in common, and it's not the same instance, so go
             // 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.
             is_in = true;  // If there's no bounding volume, we're implicitly in.
 
 
             if (col_gbv != nullptr) {
             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);
               _node_volume_pcollector.add_level(1);
 
 
-#ifndef NDEBUG
-              if (collide_cat.is_spam()) {
+              if (is_spam) {
                 indent(collide_cat.spam(false), indent_level)
                 indent(collide_cat.spam(false), indent_level)
                   << "Comparing " << c << ": " << *col_gbv
                   << "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_active_colliders = 0;
     int num_colliders = get_num_colliders();
     int num_colliders = get_num_colliders();
     for (int c = 0; c < num_colliders; c++) {
     for (int c = 0; c < num_colliders; c++) {
@@ -201,11 +196,121 @@ any_in_bounds() {
     collide_cat.spam(false)
     collide_cat.spam(false)
       << "\n";
       << "\n";
   }
   }
-#endif  // NDEBUG
   return has_any_collider();
   return has_any_collider();
 }
 }
 #endif  // CPPPARSER
 #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
 #ifndef CPPPARSER
 /**
 /**
  * Applies the inverse transform from the current node, if any, onto all the
  * 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
 #ifndef CPPPARSER
   INLINE CollisionLevelState(const NodePath &node_path);
   INLINE CollisionLevelState(const NodePath &node_path);
   INLINE CollisionLevelState(const CollisionLevelState<MaskType> &parent,
   INLINE CollisionLevelState(const CollisionLevelState<MaskType> &parent,
-                             PandaNode *child);
+                             const PandaNode::DownConnection &child,
+                             MaskType mask);
   INLINE CollisionLevelState(const CollisionLevelState<MaskType> &copy);
   INLINE CollisionLevelState(const CollisionLevelState<MaskType> &copy);
   INLINE void operator = (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);
   INLINE void prepare_collider(const ColliderDef &def, const NodePath &root);
 
 
   bool any_in_bounds();
   bool any_in_bounds();
+  MaskType get_child_mask(const PandaNode::DownConnection &child) const;
   bool apply_transform();
   bool apply_transform();
 
 
   INLINE static bool has_max_colliders();
   INLINE static bool has_max_colliders();

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

@@ -18,7 +18,8 @@ INLINE CollisionLevelStateBase::
 CollisionLevelStateBase(const NodePath &node_path) :
 CollisionLevelStateBase(const NodePath &node_path) :
   _node_path(node_path),
   _node_path(node_path),
   _colliders(get_class_type()),
   _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),
   _node_path(parent._node_path, child),
   _colliders(parent._colliders),
   _colliders(parent._colliders),
   _include_mask(parent._include_mask),
   _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),
   _colliders(copy._colliders),
   _include_mask(copy._include_mask),
   _include_mask(copy._include_mask),
   _local_bounds(copy._local_bounds),
   _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;
   _include_mask = copy._include_mask;
   _local_bounds = copy._local_bounds;
   _local_bounds = copy._local_bounds;
   _parent_bounds = copy._parent_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;
   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
  * Returns the bounding volume of the indicated collider, transformed into the
  * current node's transform space.
  * 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 NodePath &node_path);
   INLINE CollisionLevelStateBase(const CollisionLevelStateBase &parent,
   INLINE CollisionLevelStateBase(const CollisionLevelStateBase &parent,
                                  PandaNode *child);
                                  PandaNode *child);
+  INLINE CollisionLevelStateBase(const CollisionLevelStateBase &parent,
+                                 const PandaNode::DownConnection &child);
   INLINE CollisionLevelStateBase(const CollisionLevelStateBase &copy);
   INLINE CollisionLevelStateBase(const CollisionLevelStateBase &copy);
   INLINE void operator = (const CollisionLevelStateBase &copy);
   INLINE void operator = (const CollisionLevelStateBase &copy);
 
 
@@ -67,6 +69,7 @@ public:
   INLINE const CollisionSolid *get_collider(int n) const;
   INLINE const CollisionSolid *get_collider(int n) const;
   INLINE CollisionNode *get_collider_node(int n) const;
   INLINE CollisionNode *get_collider_node(int n) const;
   INLINE NodePath get_collider_node_path(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_local_bound(int n) const;
   INLINE const GeometricBoundingVolume *get_parent_bound(int n) const;
   INLINE const GeometricBoundingVolume *get_parent_bound(int n) const;
 
 
@@ -80,6 +83,8 @@ protected:
   Colliders _colliders;
   Colliders _colliders;
   CollideMask _include_mask;
   CollideMask _include_mask;
 
 
+  const GeometricBoundingVolume *_node_gbv = nullptr;
+
   typedef PTA(CPT(GeometricBoundingVolume)) BoundingVolumes;
   typedef PTA(CPT(GeometricBoundingVolume)) BoundingVolumes;
   BoundingVolumes _local_bounds;
   BoundingVolumes _local_bounds;
   BoundingVolumes _parent_bounds;
   BoundingVolumes _parent_bounds;

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

@@ -43,6 +43,7 @@ CollisionNode(const std::string &name) :
   _collider_sort(0)
   _collider_sort(0)
 {
 {
   set_cull_callback();
   set_cull_callback();
+  set_renderable();
 
 
   // CollisionNodes are hidden by default.
   // CollisionNodes are hidden by default.
   set_overall_hidden(true);
   set_overall_hidden(true);
@@ -186,11 +187,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
     CPT(CollisionSolid) solid = (*si).get_read_pointer();
     CPT(CollisionSolid) solid = (*si).get_read_pointer();
     PT(PandaNode) node = solid->get_viz(trav, data, false);
     PT(PandaNode) node = solid->get_viz(trav, data, false);
     if (node != nullptr) {
     if (node != nullptr) {
-      CullTraverserData next_data(data, node);
-
       // We don't want to inherit the render state from above for these guys.
       // 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();
         CPT(CollisionSolid) solid = (*si).get_read_pointer();
         PT(PandaNode) node = solid->get_viz(trav, data, false);
         PT(PandaNode) node = solid->get_viz(trav, data, false);
         if (node != nullptr) {
         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;
   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
  * A simple downcast check.  Returns true if this kind of node happens to
  * inherit from CollisionNode, false otherwise.
  * 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 CollideMask get_legal_collide_mask() const;
 
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
   virtual bool is_collision_node() const;
   virtual bool is_collision_node() const;
 
 
   virtual void output(std::ostream &out) 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
 #ifdef DO_PSTATS
         PStatTimer pass_timer(get_pass_collector(pass));
         PStatTimer pass_timer(get_pass_collector(pass));
 #endif
 #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::
 void CollisionTraverser::
 r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
 r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
-  if (!level_state.any_in_bounds()) {
-    return;
-  }
   if (!level_state.apply_transform()) {
   if (!level_state.apply_transform()) {
     return;
     return;
   }
   }
 
 
   PandaNode *node = level_state.node();
   PandaNode *node = level_state.node();
   if (node->is_collision_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;
     CollisionEntry entry;
     entry._into_node = cnode;
     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();
     int num_colliders = level_state.get_num_colliders();
     for (int c = 0; c < num_colliders; ++c) {
     for (int c = 0; c < num_colliders; ++c) {
       if (level_state.has_collider(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) {
              cnode->get_into_collide_mask()) != 0) {
           #ifdef DO_PSTATS
           #ifdef DO_PSTATS
           // PStatTimer collide_timer(_solid_collide_collectors[pass]);
           // PStatTimer collide_timer(_solid_collide_collectors[pass]);
           #endif
           #endif
+          entry._from_node = from_node;
           entry._from_node_path = level_state.get_collider_node_path(c);
           entry._from_node_path = level_state.get_collider_node_path(c);
           entry._from = level_state.get_collider(c);
           entry._from = level_state.get_collider(c);
 
 
@@ -603,7 +597,7 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
               entry,
               entry,
               level_state.get_parent_bound(c),
               level_state.get_parent_bound(c),
               level_state.get_local_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
     #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;
     CollisionEntry entry;
     entry._into_node = gnode;
     entry._into_node = gnode;
@@ -648,7 +636,7 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
               entry,
               entry,
               level_state.get_parent_bound(c),
               level_state.get_parent_bound(c),
               level_state.get_local_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
     // If it's a switch node or sequence node, visit just the one visible
     // child.
     // child.
     int index = node->get_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()) {
   } 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();
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
     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 {
   } else {
@@ -686,8 +683,12 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
     PandaNode::Children children = node->get_children();
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
     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::
 void CollisionTraverser::
 r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
 r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
-  if (!level_state.any_in_bounds()) {
-    return;
-  }
   if (!level_state.apply_transform()) {
   if (!level_state.apply_transform()) {
     return;
     return;
   }
   }
 
 
   PandaNode *node = level_state.node();
   PandaNode *node = level_state.node();
   if (node->is_collision_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;
     CollisionEntry entry;
     entry._into_node = cnode;
     entry._into_node = cnode;
@@ -814,7 +806,7 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
               entry,
               entry,
               level_state.get_parent_bound(c),
               level_state.get_parent_bound(c),
               level_state.get_local_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
     #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;
     CollisionEntry entry;
     entry._into_node = gnode;
     entry._into_node = gnode;
@@ -859,7 +845,7 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
               entry,
               entry,
               level_state.get_parent_bound(c),
               level_state.get_parent_bound(c),
               level_state.get_local_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
     // If it's a switch node or sequence node, visit just the one visible
     // child.
     // child.
     int index = node->get_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()) {
   } 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(),
     // visit all other levels without GeomNode::get_default_collide_mask(),
     // allowing only collision with CollisionNodes and special geometry under
     // allowing only collision with CollisionNodes and special geometry under
     // higher levels of detail.
     // 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();
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
     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 {
   } else {
@@ -897,8 +892,12 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
     PandaNode::Children children = node->get_children();
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
     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::
 void CollisionTraverser::
 r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
 r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
-  if (!level_state.any_in_bounds()) {
-    return;
-  }
   if (!level_state.apply_transform()) {
   if (!level_state.apply_transform()) {
     return;
     return;
   }
   }
 
 
   PandaNode *node = level_state.node();
   PandaNode *node = level_state.node();
   if (node->is_collision_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;
     CollisionEntry entry;
     entry._into_node = cnode;
     entry._into_node = cnode;
@@ -1025,7 +1015,7 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
               entry,
               entry,
               level_state.get_parent_bound(c),
               level_state.get_parent_bound(c),
               level_state.get_local_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
     #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;
     CollisionEntry entry;
     entry._into_node = gnode;
     entry._into_node = gnode;
@@ -1070,7 +1054,7 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
               entry,
               entry,
               level_state.get_parent_bound(c),
               level_state.get_parent_bound(c),
               level_state.get_local_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
     // If it's a switch node or sequence node, visit just the one visible
     // child.
     // child.
     int index = node->get_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()) {
   } 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(),
     // visit all other levels without GeomNode::get_default_collide_mask(),
     // allowing only collision with CollisionNodes and special geometry under
     // allowing only collision with CollisionNodes and special geometry under
     // higher levels of detail.
     // 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();
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
     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 {
   } else {
@@ -1108,8 +1101,12 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
     PandaNode::Children children = node->get_children();
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
     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
         // CollisionNodes.  We are already filtering out tests for a
         // CollisionNode into itself.
         // CollisionNode into itself.
         CPT(BoundingVolume) solid_bv = entry._into->get_bounds();
         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);
         compare_collider_to_solid(entry, from_node_gbv, solid_gbv);
       }
       }
@@ -1198,14 +1192,13 @@ compare_collider_to_geom_node(CollisionEntry &entry,
       if (geom != nullptr) {
       if (geom != nullptr) {
         CPT(BoundingVolume) geom_bv = geom->get_bounds();
         CPT(BoundingVolume) geom_bv = geom->get_bounds();
         const GeometricBoundingVolume *geom_gbv = nullptr;
         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
           // Only bother to test against each geom's bounding volume if we
           // have more than one geom in the node, as a slight optimization.
           // have more than one geom in the node, as a slight optimization.
           // (If the node contains just one geom, then the node's bounding
           // (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, which we just tested, is the same as the geom's bounding
           // volume.)
           // 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);
         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::
 CollisionVisualizer(const std::string &name) : PandaNode(name), _lock("CollisionVisualizer") {
 CollisionVisualizer(const std::string &name) : PandaNode(name), _lock("CollisionVisualizer") {
   set_cull_callback();
   set_cull_callback();
+  set_renderable();
 
 
   // We always want to render the CollisionVisualizer node itself (even if it
   // We always want to render the CollisionVisualizer node itself (even if it
   // doesn't appear to have any geometry within it).
   // doesn't appear to have any geometry within it).
@@ -62,6 +63,7 @@ CollisionVisualizer(const CollisionVisualizer &copy) :
   _normal_scale(copy._normal_scale) {
   _normal_scale(copy._normal_scale) {
 
 
   set_cull_callback();
   set_cull_callback();
+  set_renderable();
 
 
   // We always want to render the CollisionVisualizer node itself (even if it
   // We always want to render the CollisionVisualizer node itself (even if it
   // doesn't appear to have any geometry within 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);
       bool was_detected = (solid_info._detected_count > 0);
       PT(PandaNode) node = solid->get_viz(trav, xform_data, !was_detected);
       PT(PandaNode) node = solid->get_viz(trav, xform_data, !was_detected);
       if (node != nullptr) {
       if (node != nullptr) {
-        CullTraverserData next_data(xform_data, node);
-
         // We don't want to inherit the render state from above for these
         // We don't want to inherit the render state from above for these
         // guys.
         // 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;
   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.
  * Writes a brief description of the node to the indicated output stream.
  * This is invoked by the << operator.  It may be overridden in derived
  * 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.
   // from parent class PandaNode.
   virtual PandaNode *make_copy() const;
   virtual PandaNode *make_copy() const;
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
   virtual void output(std::ostream &out) const;
   virtual void output(std::ostream &out) const;
 
 
   // from parent class CollisionRecorder.
   // 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},
   {0x046d, 0xc629, InputDevice::DeviceClass::spatial_mouse, 0},
   // 3Dconnexion Space Mouse Pro
   // 3Dconnexion Space Mouse Pro
   {0x046d, 0xc62b, InputDevice::DeviceClass::spatial_mouse, 0},
   {0x046d, 0xc62b, InputDevice::DeviceClass::spatial_mouse, 0},
+  // FrSky Simulator
+  {0x0483, 0x5720, InputDevice::DeviceClass::flight_stick, 0},
   {0},
   {0},
 };
 };
 
 

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

@@ -40,7 +40,7 @@ make_global_ptr() {
   _global_ptr = new WinInputDeviceManager;
   _global_ptr = new WinInputDeviceManager;
 #elif defined(__APPLE__)
 #elif defined(__APPLE__)
   _global_ptr = new IOKitInputDeviceManager;
   _global_ptr = new IOKitInputDeviceManager;
-#elif defined(PHAVE_LINUX_INPUT_H)
+#elif defined(PHAVE_LINUX_INPUT_H) && !defined(ANDROID)
   _global_ptr = new LinuxInputDeviceManager;
   _global_ptr = new LinuxInputDeviceManager;
 #else
 #else
   _global_ptr = new InputDeviceManager;
   _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,
   {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"}
     {"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},
   {0},
 };
 };
 
 
@@ -401,7 +405,8 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
           << ", UsagePage=0x" << hex << cap.UsagePage
           << ", UsagePage=0x" << hex << cap.UsagePage
           << ", Usage=0x" << cap.Range.UsageMin << "..0x" << cap.Range.UsageMax
           << ", Usage=0x" << cap.Range.UsageMin << "..0x" << cap.Range.UsageMax
           << dec << ", LogicalMin=" << cap.LogicalMin
           << dec << ", LogicalMin=" << cap.LogicalMin
-          << ", LogicalMax=" << cap.LogicalMax << "\n";
+          << ", LogicalMax=" << cap.LogicalMax
+          << ", BitSize=" << cap.BitSize << "\n";
       }
       }
     } else {
     } else {
       if (device_cat.is_debug()) {
       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
           << ", UsagePage=0x" << hex << cap.UsagePage
           << ", Usage=0x" << cap.NotRange.Usage
           << ", Usage=0x" << cap.NotRange.Usage
           << dec << ", LogicalMin=" << cap.LogicalMin
           << 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.
       // My gamepads give this odd invalid range.
       if (cap.LogicalMin == 0 && cap.LogicalMax == -1) {
       if (cap.LogicalMin == 0 && cap.LogicalMax == -1) {
-        cap.LogicalMax = 65535;
+        cap.LogicalMax = (1 << cap.BitSize) - 1;
         is_signed = false;
         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;
       int axis_index;
       if (!is_signed) {
       if (!is_signed) {
         // All axes on the weird XInput-style mappings go from -1 to 1
         // 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 {
       } else {
         axis_index = add_axis(axis, cap.LogicalMin, cap.LogicalMax);
         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) {
   if (status == HIDP_STATUS_SUCCESS) {
     for (ULONG di = 0; di < count; ++di) {
     for (ULONG di = 0; di < count; ++di) {
       if (data[di].DataIndex != _hat_data_index) {
       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];
         const Index &idx = _indices[data[di].DataIndex];
         if (idx._axis >= 0) {
         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 {
           } else {
             axis_changed(idx._axis, data[di].RawValue);
             axis_changed(idx._axis, data[di].RawValue);
           }
           }

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

@@ -59,16 +59,16 @@ private:
       idx._button = index;
       idx._button = index;
       return idx;
       return idx;
     }
     }
-    static Index axis(int index, bool is_signed=true) {
+    static Index axis(int index, int sign_bit = 0) {
       Index idx;
       Index idx;
       idx._axis = index;
       idx._axis = index;
-      idx._signed = is_signed;
+      idx._sign_bit = sign_bit;
       return idx;
       return idx;
     }
     }
 
 
     int _button;
     int _button;
     int _axis;
     int _axis;
-    bool _signed;
+    int _sign_bit;
   };
   };
 
 
   // Maps a "data index" to either button index or axis index.
   // 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
 set(P3DISPLAY_IGATEEXT
+  frameBufferProperties_ext.cxx
+  frameBufferProperties_ext.h
+  graphicsPipeSelection_ext.cxx
+  graphicsPipeSelection_ext.h
   graphicsStateGuardian_ext.cxx
   graphicsStateGuardian_ext.cxx
   graphicsStateGuardian_ext.h
   graphicsStateGuardian_ext.h
   graphicsWindow_ext.cxx
   graphicsWindow_ext.cxx

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

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

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