Browse Source

Merge branch 'shaderpipeline' into vulkan

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

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

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

+ 1 - 0
BACKERS.md

@@ -34,6 +34,7 @@ This is a list of all the people who are contributing financially to Panda3D.  I
 * Kyle Roach
 * Brian Lach
 * C0MPU73R
+* Maxwell Dreytser
 
 ## Backers
 

+ 1 - 1
CONTRIBUTING.md

@@ -18,7 +18,7 @@ It is important for acceptance that the change is implemented in a way that fits
 the general design principles of the Panda3D API, and fits well with the general
 priorities of the team.  Therefore, prior discussion with other developers is
 critical.  Issues can be used to facilitate this, but we also invite you to
-visit the #development channel on Discord (or #panda3d-devel on FreeNode IRC).
+visit the #development channel on Discord (or #panda3d-devel on Libera Chat).
 
 We also recommend that you familiarize yourself with the established coding
 style and design patterns of Panda3D, to reduce the amount of changes that have

+ 11 - 18
README.md

@@ -24,7 +24,7 @@ Installing Panda3D
 ==================
 
 The latest Panda3D SDK can be downloaded from
-[this page](https://www.panda3d.org/download/sdk-1-10-9/).
+[this page](https://www.panda3d.org/download/sdk-1-10-10/).
 If you are familiar with installing Python packages, you can use
 the following command:
 
@@ -52,9 +52,9 @@ Building Panda3D
 Windows
 -------
 
-You can build Panda3D with the Microsoft Visual C++ 2015, 2017 or 2019 compiler,
-which can be downloaded for free from the [Visual Studio site](https://visualstudio.microsoft.com/downloads/).
-You will also need to install the [Windows 10 SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk),
+You can build Panda3D with the Microsoft Visual C++ 2015, 2017, 2019 or 2022
+compiler, which can be downloaded for free from the [Visual Studio site](https://visualstudio.microsoft.com/downloads/).
+You will also need to install the [Windows SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-sdk),
 and if you intend to target Windows Vista, you will also need the
 [Windows 8.1 SDK](https://go.microsoft.com/fwlink/p/?LinkId=323507).
 
@@ -64,17 +64,17 @@ depending on whether you are on a 32-bit or 64-bit system, or you can
 [click here](https://github.com/rdb/panda3d-thirdparty) for instructions on
 building them from source.
 
-- https://www.panda3d.org/download/panda3d-1.10.9/panda3d-1.10.9-tools-win64.zip
-- https://www.panda3d.org/download/panda3d-1.10.9/panda3d-1.10.9-tools-win32.zip
+- https://www.panda3d.org/download/panda3d-1.10.10/panda3d-1.10.10-tools-win64.zip
+- https://www.panda3d.org/download/panda3d-1.10.10/panda3d-1.10.10-tools-win32.zip
 
 After acquiring these dependencies, you can build Panda3D from the command
 prompt using the following command.  Change the `--msvc-version` option based
-on your version of Visual C++; 2019 is 14.2, 2017 is 14.1, and 2015 is 14.
-Remove the `--windows-sdk=10` option if you need to support Windows Vista,
-which requires the Windows 8.1 SDK.
+on your version of Visual C++; 2022 is 14.3, 2019 is 14.2, 2017 is 14.1, and
+2015 is 14.  Remove the `--windows-sdk=10` option if you need to support
+Windows Vista, which requires the Windows 8.1 SDK.
 
 ```bash
-makepanda\makepanda.bat --everything --installer --msvc-version=14.2 --windows-sdk=10 --no-eigen --threads=2
+makepanda\makepanda.bat --everything --installer --msvc-version=14.3 --windows-sdk=10 --no-eigen --threads=2
 ```
 
 When the build succeeds, it will produce an .exe file that you can use to
@@ -136,7 +136,7 @@ macOS
 -----
 
 On macOS, you will need to download a set of precompiled thirdparty packages in order to
-compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.9/panda3d-1.10.9-tools-mac.tar.gz).
+compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.10/panda3d-1.10.10-tools-mac.tar.gz).
 
 After placing the thirdparty directory inside the panda3d source directory,
 you may build Panda3D using a command like the following:
@@ -249,10 +249,3 @@ to everyone who has donated!
 <a href="https://opencollective.com/panda3d" target="_blank">
   <img src="https://opencollective.com/panda3d/contribute/[email protected]?color=blue" width=300 />
 </a>
-
-### Gold Sponsors
-[![](https://opencollective.com/panda3d/tiers/gold-sponsor/0/avatar.svg?avatarHeight=128)](https://opencollective.com/panda3d/tiers/gold-sponsor/0/website)
-[![](https://opencollective.com/panda3d/tiers/gold-sponsor/1/avatar.svg?avatarHeight=128)](https://opencollective.com/panda3d/tiers/gold-sponsor/1/website)
-[![](https://opencollective.com/panda3d/tiers/gold-sponsor/2/avatar.svg?avatarHeight=128)](https://opencollective.com/panda3d/tiers/gold-sponsor/2/website)
-[![](https://opencollective.com/panda3d/tiers/gold-sponsor/3/avatar.svg?avatarHeight=128)](https://opencollective.com/panda3d/tiers/gold-sponsor/3/website)
-[![](https://opencollective.com/panda3d/tiers/gold-sponsor/4/avatar.svg?avatarHeight=128)](https://opencollective.com/panda3d/tiers/gold-sponsor/4/website)

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

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

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

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

+ 10 - 2
contrib/src/rplight/pssmCameraRig.cxx

@@ -369,8 +369,16 @@ void PSSMCameraRig::update(NodePath cam_node, const LVecBase3 &light_vector) {
   LMatrix4 transform = cam_node.get_transform()->get_mat();
 
   // Get Camera and Lens pointers
-  Camera* cam = DCAST(Camera, cam_node.get_child(0).node());
-  nassertv(cam != nullptr);
+  Camera *cam;
+  PandaNode *node = cam_node.node();
+  if (node->is_of_type(Camera::get_class_type())) {
+    cam = (Camera *)node;
+  }
+  else {
+    // Perhaps we passed in something like base.camera ?
+    cam = DCAST(Camera, cam_node.get_child(0).node());
+    nassertv(cam != nullptr);
+  }
   Lens* lens = cam->get_lens();
 
   // Extract near and far points:

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

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

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

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

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

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

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

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

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

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

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

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

+ 4 - 0
direct/src/dcparser/dcPacker_ext.cxx

@@ -226,6 +226,10 @@ unpack_object() {
       std::string str;
       _this->unpack_string(str);
       object = PyUnicode_FromStringAndSize(str.data(), str.size());
+      if (object == nullptr) {
+        nassert_raise("Unable to decode UTF-8 string; use blob type for binary data");
+        return nullptr;
+      }
     }
     break;
 

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

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

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

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

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

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

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

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

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

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

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

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

+ 12 - 11
direct/src/dist/FreezeTool.py

@@ -90,6 +90,7 @@ ignoreImports = {
     'direct.showbase.PythonUtil': ['pstats', 'profile'],
 
     'toml.encoder': ['numpy'],
+    'py._builtin': ['__builtin__'],
 }
 
 if sys.version_info >= (3, 8):
@@ -106,7 +107,7 @@ overrideModules = {
     # Used by the warnings module, among others, to get line numbers.  Since
     # we set __file__, this would cause it to try and extract Python code
     # lines from the main executable, which we don't want.
-    'linecache': """__all__ = ["getline", "clearcache", "checkcache"]
+    'linecache': """__all__ = ["getline", "clearcache", "checkcache", "lazycache"]
 
 cache = {}
 
@@ -662,7 +663,7 @@ okMissing = [
     'email.Iterators', '_subprocess', 'gestalt', 'java.lang',
     'direct.extensions_native.extensions_darwin', '_manylinux',
     'collections.Iterable', 'collections.Mapping', 'collections.MutableMapping',
-    'collections.Sequence', 'numpy_distutils',
+    'collections.Sequence', 'numpy_distutils', '_winapi',
     ]
 
 # Since around macOS 10.15, Apple's codesigning process has become more strict.
@@ -1408,7 +1409,7 @@ class Freezer:
                 else:
                     filename += '.pyo'
                 if multifile.findSubfile(filename) < 0:
-                    code = compile('', moduleName, 'exec')
+                    code = compile('', moduleName, 'exec', optimize=2)
                     self.__addPyc(multifile, filename, code, compressionLevel)
 
             moduleDirs[str] = True
@@ -1488,7 +1489,7 @@ class Freezer:
                 source = open(sourceFilename.toOsSpecific(), 'r').read()
                 if source and source[-1] != '\n':
                     source = source + '\n'
-                code = compile(source, str(sourceFilename), 'exec')
+                code = compile(source, str(sourceFilename), 'exec', optimize=2)
 
         self.__addPyc(multifile, filename, code, compressionLevel)
 
@@ -1567,7 +1568,7 @@ class Freezer:
             # trouble importing it as a builtin module.  Synthesize a frozen
             # module that loads it as builtin.
             if '.' in moduleName and self.linkExtensionModules:
-                code = compile('import sys;del sys.modules["%s"];import imp;imp.init_builtin("%s")' % (moduleName, moduleName), moduleName, 'exec')
+                code = compile('import sys;del sys.modules["%s"];import imp;imp.init_builtin("%s")' % (moduleName, moduleName), moduleName, 'exec', optimize=2)
                 code = marshal.dumps(code)
                 mangledName = self.mangleName(moduleName)
                 moduleDefs.append(self.makeModuleDef(mangledName, code))
@@ -1834,7 +1835,7 @@ class Freezer:
             # If it is a submodule of a frozen module, Python will have
             # trouble importing it as a builtin module.  Synthesize a frozen
             # module that loads it dynamically.
-            if '.' in moduleName:
+            if '.' in moduleName and not self.platform.startswith('android'):
                 if self.platform.startswith("macosx") and not use_console:
                     # We write the Frameworks directory to sys.path[0].
                     code = 'import sys;del sys.modules["%s"];import sys,os,imp;imp.load_dynamic("%s",os.path.join(sys.path[0], "%s%s"))' % (moduleName, moduleName, moduleName, modext)
@@ -2471,7 +2472,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
         if type is _PKG_NAMESPACE_DIRECTORY:
             m = self.add_module(fqname)
-            m.__code__ = compile('', '', 'exec')
+            m.__code__ = compile('', '', 'exec', optimize=2)
             m.__path__ = pathname
             return m
 
@@ -2483,7 +2484,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
                 code = fp.read()
 
             code += b'\n' if isinstance(code, bytes) else '\n'
-            co = compile(code, pathname, 'exec')
+            co = compile(code, pathname, 'exec', optimize=2)
         elif type == imp.PY_COMPILED:
             if sys.version_info >= (3, 7):
                 try:
@@ -2625,9 +2626,9 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
         (or self.path if None).  Returns a tuple like (fp, path, stuff), where
         stuff is a tuple like (suffix, mode, type). """
 
-        if imp.is_frozen(name):
-            # Don't pick up modules that are frozen into p3dpython.
-            raise ImportError("'%s' is a frozen module" % (name))
+        #if imp.is_frozen(name):
+        #    # Don't pick up modules that are frozen into p3dpython.
+        #    raise ImportError("'%s' is a frozen module" % (name))
 
         if parent is not None:
             fullname = parent.__name__+'.'+name

+ 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


+ 316 - 39
direct/src/dist/commands.py

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 2 - 0
direct/src/filter/FilterManager.py

@@ -367,6 +367,8 @@ class FilterManager(DirectObject):
         self.camstate = self.caminit
         self.camera.node().setInitialState(self.caminit)
         self.region.setCamera(self.camera)
+        if hasattr(self.region, 'clearCullResult'):
+            self.region.clearCullResult()
         self.nextsort = self.win.getSort() - 9
         self.basex = 0
         self.basey = 0

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

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

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

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

+ 8 - 0
direct/src/interval/MetaInterval.py

@@ -348,10 +348,14 @@ class MetaInterval(CMetaInterval):
     def getManager(self):
         return self.__manager
 
+    manager = property(getManager, setManager)
+
     def setT(self, t):
         self.__updateIvals()
         CMetaInterval.setT(self, t)
 
+    t = property(CMetaInterval.getT, setT)
+
     def start(self, startT = 0.0, endT = -1.0, playRate = 1.0):
         self.__updateIvals()
         self.setupPlay(startT, endT, playRate, 0)
@@ -475,6 +479,8 @@ class MetaInterval(CMetaInterval):
         else:
             CMetaInterval.setPlayRate(self, playRate)
 
+    play_rate = property(CMetaInterval.getPlayRate, setPlayRate)
+
     def __doPythonCallbacks(self):
         # This function invokes any Python-level Intervals that need
         # to be invoked at this point in time.  It must be called
@@ -549,6 +555,8 @@ class MetaInterval(CMetaInterval):
         self.__updateIvals()
         return CMetaInterval.getDuration(self)
 
+    duration = property(getDuration)
+
     def __repr__(self, *args, **kw):
         # This function overrides from the parent level to force it to
         # update the interval list first, if necessary.

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

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

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

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

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

@@ -43,7 +43,7 @@ import functools
 
 __report_indent = 3
 
-from panda3d.core import ConfigVariableBool
+from panda3d.core import ConfigVariableBool, ClockObject
 
 
 ## with one integer positional arg, this uses about 4/5 of the memory of the Functor class below
@@ -1127,8 +1127,6 @@ def normalDistrib(a, b, gauss=random.gauss):
     uniformly onto the curve inside [a, b]
 
     ------------------------------------------------------------------------
-    https://statweb.stanford.edu/~naras/jsm/NormalDensity/NormalDensity.html
-
     The 68-95-99.7% Rule
     ====================
     All normal density curves satisfy the following property which is often
@@ -2055,6 +2053,7 @@ def report(types = [], prefix = '', xform = None, notifyFunc = None, dConfigPara
             if prefixes:
                 outStr = '%%s %s' % (outStr,)
 
+            globalClock = ClockObject.getGlobalClock()
 
             if 'module' in types:
                 outStr = '%s {M:%s}' % (outStr, f.__module__.split('.')[-1])
@@ -2266,9 +2265,10 @@ if __debug__:
                 # at the time that PythonUtil is loaded
                 if not ConfigVariableBool("profile-debug", False):
                     #dumb timings
-                    st=globalClock.getRealTime()
-                    f(*args,**kArgs)
-                    s=globalClock.getRealTime()-st
+                    clock = ClockObject.getGlobalClock()
+                    st = clock.getRealTime()
+                    f(*args, **kArgs)
+                    s = clock.getRealTime() - st
                     print("Function %s.%s took %s seconds"%(f.__module__, f.__name__,s))
                 else:
                     import profile as prof, pstats

+ 15 - 11
direct/src/showbase/ShowBase.py

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 33 - 0
doc/ReleaseNotes

@@ -1,3 +1,36 @@
+-----------------------  RELEASE 1.10.10  -----------------------
+
+This release fixes assorted, mostly very minor bugs.
+
+* Round refresh rates when choosing display mode on macOS (#1144)
+* Vectors now support floor division
+* It is now possible to use round(), ceil() and floor() with vector types
+* Add RenderState::get_unused_states()
+* Fix assertion error in RenderState::get_num_unused_states()
+* Fix Assimp loader not importing normal vectors correctly (#1163)
+* Fix error when trying to render DirectGUI in offscreen mode (#1174)
+* Fix crash when resizing window in multi-window DirectX 9 application (#1167)
+* Fixes to enable compilation with recent OpenEXR and FFMpeg versions
+* Fix draw callback being called twice if cull callback calls upcall()
+* Improve error message when display module fails to load
+* Fix writing/reading BitArray to/from bam files on 64-bit systems (#1181)
+* evdev input devices (such as gamepads) are now supported in FreeBSD as well
+* Panda no longer tries to compress buffer textures when compression is enabled
+* Fix Geom::make_lines_in_place() (& points, patches) leaving invalid state
+* Fix memory leak when cleaning up FilterManager (#1166)
+* Fix memory leak deleting multisample OpenGL FBOs (#1166)
+* Fix auto-binding of SSBOs sometimes causing overlapping bindings (#1176)
+* PSSMCameraRig::update() now accepts a camera node directly
+* Support copying depth buffer for 32-bit depth with 8-bit stencil (#1142)
+* Prevent trying to copy depth from non-depth buffer in OpenGL renderer (#1142)
+* Fixes to format selection for OpenGL renderbuffers (#1137, #1141)
+* gl-depth-zero-to-one is now supported in OpenGL ES 2+ (if driver supports)
+* Maya models can contain more than three eggObjectTypes (#1134)
+* Fix black screen on Linux when switching fullscreen without a WM active
+* Fix Linux crash when trying to load a directory instead of a file (#1140)
+* Fix crash when loading an invalid font
+* Fix a very obscure unintended DirectGUI behavior change in 1.10.9
+
 ------------------------  RELEASE 1.10.9  -----------------------
 
 This is a bugfix release which addresses some severe issues on macOS, as well as

+ 1 - 1
doc/man/bam-info.1

@@ -15,7 +15,7 @@ List the scene graph hierarchy in the bam file.
 List explicitly each transition in the hierarchy.
 .TP
 .B \-g
-Output verbose information about the each Geom in the Bam file.
+Output verbose information about each Geom in the Bam file.
 .TP
 .B \-h
 Display this help page.

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

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

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

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

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

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

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

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

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

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

+ 108 - 42
dtool/src/interrogate/interfaceMakerPythonNative.cxx

@@ -109,6 +109,7 @@ RenameSet methodRenameDictionary[] = {
   { "__nonzero__"   , "__nonzero__",            0 },
   { "__int__"       , "__int__",                0 },
   { "__reduce__"    , "__reduce__",             0 },
+  { "__reduce_ex__" , "__reduce_ex__",          0 },
   { "__reduce_persist__", "__reduce_persist__", 0 },
   { "__copy__"      , "__copy__",               0 },
   { "__deepcopy__"  , "__deepcopy__",           0 },
@@ -307,7 +308,9 @@ get_slotted_function_def(Object *obj, Function *func, FunctionRemap *remap,
   string method_name = func->_ifunc.get_name();
   bool is_unary_op = func->_ifunc.is_unary_op();
 
-  if (method_name == "operator +") {
+  if (method_name == "operator +" ||
+      method_name == "__add__" ||
+      method_name == "__radd__") {
     def._answer_location = "nb_add";
     def._wrapper_type = WT_binary_operator;
     return true;
@@ -319,13 +322,17 @@ get_slotted_function_def(Object *obj, Function *func, FunctionRemap *remap,
     return true;
   }
 
-  if (method_name == "operator -") {
+  if (method_name == "operator -" ||
+      method_name == "__sub__" ||
+      method_name == "__rsub__") {
     def._answer_location = "nb_subtract";
     def._wrapper_type = WT_binary_operator;
     return true;
   }
 
-  if (method_name == "operator *") {
+  if (method_name == "operator *" ||
+      method_name == "__mul__" ||
+      method_name == "__rmul__") {
     def._answer_location = "nb_multiply";
     def._wrapper_type = WT_binary_operator;
     return true;
@@ -337,37 +344,47 @@ get_slotted_function_def(Object *obj, Function *func, FunctionRemap *remap,
     return true;
   }
 
-  if (method_name == "__truediv__") {
+  if (method_name == "__truediv__" ||
+      method_name == "__rtruediv__") {
     def._answer_location = "nb_true_divide";
     def._wrapper_type = WT_binary_operator;
     return true;
   }
 
-  if (method_name == "__floordiv__") {
+  if (method_name == "__floordiv__" ||
+      method_name == "__rfloordiv__") {
     def._answer_location = "nb_floor_divide";
     def._wrapper_type = WT_binary_operator;
     return true;
   }
 
-  if (method_name == "operator %") {
+  if (method_name == "operator %" ||
+      method_name == "__mod__" ||
+      method_name == "__rmod__") {
     def._answer_location = "nb_remainder";
     def._wrapper_type = WT_binary_operator;
     return true;
   }
 
-  if (method_name == "operator <<") {
+  if (method_name == "operator <<" ||
+      method_name == "__lshift__" ||
+      method_name == "__rlshift__") {
     def._answer_location = "nb_lshift";
     def._wrapper_type = WT_binary_operator;
     return true;
   }
 
-  if (method_name == "operator >>") {
+  if (method_name == "operator >>" ||
+      method_name == "__rshift__" ||
+      method_name == "__rrshift__") {
     def._answer_location = "nb_rshift";
     def._wrapper_type = WT_binary_operator;
     return true;
   }
 
-  if (method_name == "operator ^") {
+  if (method_name == "operator ^" ||
+      method_name == "__xor__" ||
+      method_name == "__rxor__") {
     def._answer_location = "nb_xor";
     def._wrapper_type = WT_binary_operator;
     return true;
@@ -379,13 +396,17 @@ get_slotted_function_def(Object *obj, Function *func, FunctionRemap *remap,
     return true;
   }
 
-  if (method_name == "operator &") {
+  if (method_name == "operator &" ||
+      method_name == "__and__" ||
+      method_name == "__rand__") {
     def._answer_location = "nb_and";
     def._wrapper_type = WT_binary_operator;
     return true;
   }
 
-  if (method_name == "operator |") {
+  if (method_name == "operator |" ||
+      method_name == "__or__" ||
+      method_name == "__ror__") {
     def._answer_location = "nb_or";
     def._wrapper_type = WT_binary_operator;
     return true;
@@ -1896,16 +1917,8 @@ write_module_class(ostream &out, Object *obj) {
         break;
 
       case WT_one_param:
-      case WT_binary_operator:
-      case WT_inplace_binary_operator:
         // PyObject *func(PyObject *self, PyObject *one)
         {
-          int return_flags = RF_err_null;
-          if (rfi->second._wrapper_type == WT_inplace_binary_operator) {
-            return_flags |= RF_self;
-          } else {
-            return_flags |= RF_pyobject;
-          }
           bool all_nonconst = true;
           for (FunctionRemap *remap : def._remaps) {
             if (remap->_const_method) {
@@ -1918,20 +1931,7 @@ write_module_class(ostream &out, Object *obj) {
           out << "//////////////////\n";
           out << "static PyObject *" << def._wrapper_name << "(PyObject *self, PyObject *arg) {\n";
           out << "  " << cClassName << " *local_this = nullptr;\n";
-          if (rfi->second._wrapper_type != WT_one_param) {
-            // WT_binary_operator means we must return NotImplemented, instead
-            // of raising an exception, if the this pointer doesn't match.
-            // This is for things like __sub__, which Python likes to call on
-            // the wrong-type objects.
-            out << "  DTOOL_Call_ExtractThisPointerForType(self, &Dtool_" << ClassName << ", (void **)&local_this);\n";
-            if (all_nonconst) {
-              out << "  if (local_this == nullptr || DtoolInstance_IS_CONST(self)) {\n";
-            } else {
-              out << "  if (local_this == nullptr) {\n";
-            }
-            out << "    Py_INCREF(Py_NotImplemented);\n";
-            out << "    return Py_NotImplemented;\n";
-          } else if (all_nonconst) {
+          if (all_nonconst) {
             out << "  if (!Dtool_Call_ExtractThisPointer_NonConst(self, Dtool_"
                 << ClassName << ", (void **)&local_this, \"" << ClassName
                 << "." << methodNameFromCppName(fname, "", false) << "\")) {\n";
@@ -1944,19 +1944,85 @@ write_module_class(ostream &out, Object *obj) {
 
           string expected_params;
           write_function_forset(out, def._remaps, 1, 1, expected_params, 2, true, true,
-                                AT_single_arg, return_flags, false, !all_nonconst);
+                                AT_single_arg, RF_err_null | RF_pyobject, false, !all_nonconst);
+
+          out << "  if (!_PyErr_OCCURRED()) {\n";
+          out << "    return Dtool_Raise_BadArgumentsError(\n";
+          output_quoted(out, 6, expected_params);
+          out << ");\n";
+          out << "  }\n";
+          out << "  return nullptr;\n";
+          out << "}\n\n";
+        }
+        break;
 
-          if (rfi->second._wrapper_type != WT_one_param) {
-            out << "  Py_INCREF(Py_NotImplemented);\n";
-            out << "  return Py_NotImplemented;\n";
+      case WT_binary_operator:
+      case WT_inplace_binary_operator:
+        // PyObject *func(PyObject *self, PyObject *one)
+        {
+          int return_flags = RF_err_null;
+          if (rfi->second._wrapper_type == WT_inplace_binary_operator) {
+            return_flags |= RF_self;
           } else {
-            out << "  if (!_PyErr_OCCURRED()) {\n";
-            out << "    return Dtool_Raise_BadArgumentsError(\n";
-            output_quoted(out, 6, expected_params);
-            out << ");\n";
+            return_flags |= RF_pyobject;
+          }
+          bool forward_all_nonconst = true;
+          bool reverse_all_nonconst = true;
+          set<FunctionRemap *> forward_remaps;
+          set<FunctionRemap *> reverse_remaps;
+          for (FunctionRemap *remap : def._remaps) {
+            std::string fname = remap->_cppfunc->get_simple_name();
+            if (fname.compare(0, 3, "__r") == 0 && fname != "__rshift__") {
+              reverse_remaps.insert(remap);
+              if (remap->_const_method) {
+                reverse_all_nonconst = false;
+              }
+            } else {
+              forward_remaps.insert(remap);
+              if (remap->_const_method) {
+                forward_all_nonconst = false;
+              }
+            }
+          }
+          out << "//////////////////\n";
+          out << "// A wrapper function to satisfy Python's internal calling conventions.\n";
+          out << "// " << ClassName << " slot " << rfi->second._answer_location << " -> " << fname << "\n";
+          out << "//////////////////\n";
+          out << "static PyObject *" << def._wrapper_name << "(PyObject *self, PyObject *arg) {\n";
+          out << "  " << cClassName << " *local_this = nullptr;\n";
+          // WT_binary_operator means we must return NotImplemented, instead
+          // of raising an exception, if the this pointer doesn't match.
+          // This is for things like __sub__, which Python likes to call on
+          // the wrong-type objects.
+          if (!forward_remaps.empty()) {
+            out << "  DTOOL_Call_ExtractThisPointerForType(self, &Dtool_" << ClassName << ", (void **)&local_this);\n";
+            if (forward_all_nonconst) {
+              out << "  if (local_this != nullptr && !DtoolInstance_IS_CONST(self)) {\n";
+            } else {
+              out << "  if (local_this != nullptr) {\n";
+            }
+            string expected_params;
+            write_function_forset(out, forward_remaps, 1, 1, expected_params, 4, true, true,
+                                  AT_single_arg, return_flags, false, !forward_all_nonconst);
             out << "  }\n";
-            out << "  return nullptr;\n";
           }
+
+          if (!reverse_remaps.empty()) {
+            out << "  std::swap(self, arg);\n";
+            out << "  DTOOL_Call_ExtractThisPointerForType(self, &Dtool_" << ClassName << ", (void **)&local_this);\n";
+            if (reverse_all_nonconst) {
+              out << "  if (local_this != nullptr && !DtoolInstance_IS_CONST(self)) {\n";
+            } else {
+              out << "  if (local_this != nullptr) {\n";
+            }
+            string expected_params;
+            write_function_forset(out, reverse_remaps, 1, 1, expected_params, 4, true, true,
+                                  AT_single_arg, return_flags, false, !reverse_all_nonconst);
+            out << "  }\n";
+          }
+
+          out << "  Py_INCREF(Py_NotImplemented);\n";
+          out << "  return Py_NotImplemented;\n";
           out << "}\n\n";
         }
         break;

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

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

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

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

+ 1 - 1
dtool/src/interrogatedb/py_wrappers.cxx

@@ -371,7 +371,7 @@ static PyObject *Dtool_MutableSequenceWrapper_insert(PyObject *self, PyObject *a
       return PyErr_Format(PyExc_TypeError, "%s.insert() does not support negative indices", wrap->_base._name);
     }
   }
-  return wrap->_insert_func(wrap->_base._self, (ssize_t)std::max(index, (Py_ssize_t)0), PyTuple_GET_ITEM(args, 1));
+  return wrap->_insert_func(wrap->_base._self, (size_t)std::max(index, (Py_ssize_t)0), PyTuple_GET_ITEM(args, 1));
 }
 
 /**

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -31,6 +31,8 @@
 
 #ifdef ANDROID
 #include <android/log.h>
+
+#include "androidLogStream.h"
 #endif
 
 using std::cerr;
@@ -345,8 +347,9 @@ assert_failure(const char *expression, int line,
 
 #ifdef ANDROID
   __android_log_assert("assert", "Panda3D", "Assertion failed: %s", message.c_str());
-#endif
+#else
   nout << "Assertion failed: " << message << "\n";
+#endif
 
   // This is redefined here, shadowing the defining in config_prc.h, so we can
   // guarantee it has already been constructed.
@@ -477,6 +480,12 @@ config_initialized() {
         }
 #endif  // BUILD_IPHONE
       }
+#ifdef ANDROID
+    } else {
+      // By default, we always redirect the notify stream to the Android log.
+      Notify *ptr = Notify::ptr();
+      ptr->set_ostream_ptr(new AndroidLogStream(ANDROID_LOG_INFO), true);
+#endif
     }
   }
 }

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

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

+ 1 - 0
makepanda/config.in

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

+ 12 - 0
makepanda/installer.nsi

@@ -374,6 +374,7 @@ SectionGroup "Python modules" SecGroupPython
         !insertmacro PyBindingSection 3.8-32 .cp38-win32.pyd
         !insertmacro PyBindingSection 3.9-32 .cp39-win32.pyd
         !insertmacro PyBindingSection 3.10-32 .cp310-win32.pyd
+        !insertmacro PyBindingSection 3.11-32 .cp311-win32.pyd
     !else
         !insertmacro PyBindingSection 3.5 .cp35-win_amd64.pyd
         !insertmacro PyBindingSection 3.6 .cp36-win_amd64.pyd
@@ -381,6 +382,7 @@ SectionGroup "Python modules" SecGroupPython
         !insertmacro PyBindingSection 3.8 .cp38-win_amd64.pyd
         !insertmacro PyBindingSection 3.9 .cp39-win_amd64.pyd
         !insertmacro PyBindingSection 3.10 .cp310-win_amd64.pyd
+        !insertmacro PyBindingSection 3.11 .cp311-win_amd64.pyd
     !endif
 SectionGroupEnd
 
@@ -489,6 +491,7 @@ Function .onInit
         ${If} ${AtLeastWin8}
         !insertmacro MaybeEnablePyBindingSection 3.9-32
         !insertmacro MaybeEnablePyBindingSection 3.10-32
+        !insertmacro MaybeEnablePyBindingSection 3.11-32
         ${EndIf}
     !else
         !insertmacro MaybeEnablePyBindingSection 3.5
@@ -498,6 +501,7 @@ Function .onInit
         ${If} ${AtLeastWin8}
         !insertmacro MaybeEnablePyBindingSection 3.9
         !insertmacro MaybeEnablePyBindingSection 3.10
+        !insertmacro MaybeEnablePyBindingSection 3.11
         ${EndIf}
     !endif
 
@@ -511,6 +515,10 @@ Function .onInit
         SectionSetFlags ${SecPyBindings3.10} ${SF_RO}
         SectionSetInstTypes ${SecPyBindings3.10} 0
     !endif
+    !ifdef SecPyBindings3.11
+        SectionSetFlags ${SecPyBindings3.11} ${SF_RO}
+        SectionSetInstTypes ${SecPyBindings3.11} 0
+    !endif
     ${EndUnless}
 FunctionEnd
 
@@ -822,6 +830,7 @@ Section Uninstall
         !insertmacro RemovePythonPath 3.8-32
         !insertmacro RemovePythonPath 3.9-32
         !insertmacro RemovePythonPath 3.10-32
+        !insertmacro RemovePythonPath 3.11-32
     !else
         !insertmacro RemovePythonPath 3.5
         !insertmacro RemovePythonPath 3.6
@@ -829,6 +838,7 @@ Section Uninstall
         !insertmacro RemovePythonPath 3.8
         !insertmacro RemovePythonPath 3.9
         !insertmacro RemovePythonPath 3.10
+        !insertmacro RemovePythonPath 3.11
     !endif
 
     SetDetailsPrint both
@@ -897,6 +907,7 @@ SectionEnd
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.8-32} $(DESC_SecPyBindings3.8-32)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.9-32} $(DESC_SecPyBindings3.9-32)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.10-32} $(DESC_SecPyBindings3.10-32)
+    !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.11-32} $(DESC_SecPyBindings3.11-32)
   !else
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.5} $(DESC_SecPyBindings3.5)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.6} $(DESC_SecPyBindings3.6)
@@ -904,6 +915,7 @@ SectionEnd
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.8} $(DESC_SecPyBindings3.8)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.9} $(DESC_SecPyBindings3.9)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.10} $(DESC_SecPyBindings3.10)
+    !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.11} $(DESC_SecPyBindings3.11)
   !endif
   !ifdef INCLUDE_PYVER
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPython} $(DESC_SecPython)

+ 2 - 2
makepanda/installpanda.py

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

+ 23 - 17
makepanda/makepackage.py

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

+ 123 - 62
makepanda/makepanda.py

@@ -21,6 +21,7 @@ try:
     import threading
     import signal
     import shutil
+    import sysconfig
     import plistlib
     import queue
 except KeyboardInterrupt:
@@ -30,7 +31,10 @@ except:
     print("Please install the development package of Python and try again.")
     exit(1)
 
-from distutils.util import get_platform
+if sys.version_info >= (3, 10):
+    from sysconfig import get_platform
+else:
+    from distutils.util import get_platform
 from makepandacore import *
 
 try:
@@ -63,6 +67,7 @@ DISTRIBUTOR=""
 VERSION=None
 DEBVERSION=None
 WHLVERSION=None
+RPMVERSION=None
 RPMRELEASE="1"
 GIT_COMMIT=None
 MAJOR_VERSION=None
@@ -152,8 +157,8 @@ def usage(problem):
     print("  --nothing         (disable every third-party lib)")
     print("  --everything      (enable every third-party lib)")
     print("  --directx-sdk=X   (specify version of DirectX SDK to use: jun2010, aug2009)")
-    print("  --windows-sdk=X   (specify Windows SDK version, eg. 7.1, 8.1 or 10.  Default is 8.1)")
-    print("  --msvc-version=X  (specify Visual C++ version, eg. 10, 11, 12, 14, 14.1, 14.2.  Default is 14)")
+    print("  --windows-sdk=X   (specify Windows SDK version, eg. 7.1, 8.1, 10 or 11.  Default is 8.1)")
+    print("  --msvc-version=X  (specify Visual C++ version, eg. 10, 11, 12, 14, 14.1, 14.2, 14.3.  Default is 14)")
     print("  --use-icl         (experimental setting to use an intel compiler instead of MSVC on Windows)")
     print("")
     print("The simplest way to compile panda is to just type:")
@@ -165,7 +170,7 @@ def usage(problem):
 def parseopts(args):
     global INSTALLER,WHEEL,RUNTESTS,GENMAN,DISTRIBUTOR,VERSION
     global COMPRESSOR,THREADCOUNT,OSX_ARCHS
-    global DEBVERSION,WHLVERSION,RPMRELEASE,GIT_COMMIT
+    global DEBVERSION,WHLVERSION,RPMVERSION,RPMRELEASE,GIT_COMMIT
     global STRDXSDKVERSION, WINDOWS_SDK, MSVC_VERSION, BOOUSEINTELCOMPILER
     global COPY_PYTHON
 
@@ -181,7 +186,7 @@ def parseopts(args):
         "help","distributor=","verbose","tests",
         "optimize=","everything","nothing","installer","wheel","rtdist","nocolor",
         "version=","lzma","no-python","threads=","outputdir=","override=",
-        "static","debversion=","rpmrelease=","p3dsuffix=","rtdist-version=",
+        "static","debversion=","rpmversion=","rpmrelease=","p3dsuffix=","rtdist-version=",
         "directx-sdk=", "windows-sdk=", "msvc-version=", "clean", "use-icl",
         "universal", "target=", "arch=", "git-commit=", "no-copy-python",
         "glslang-incdir=", "glslang-libdir=",
@@ -230,6 +235,7 @@ def parseopts(args):
             elif (option=="--override"): AddOverride(value.strip())
             elif (option=="--static"): SetLinkAllStatic(True)
             elif (option=="--debversion"): DEBVERSION=value
+            elif (option=="--rpmversion"): RPMVERSION=value
             elif (option=="--rpmrelease"): RPMRELEASE=value
             elif (option=="--git-commit"): GIT_COMMIT=value
             # Backward compatibility, OPENGL was renamed to GL
@@ -380,6 +386,9 @@ print("Version: %s" % VERSION)
 if DEBVERSION is None:
     DEBVERSION = VERSION
 
+if RPMVERSION is None:
+    RPMVERSION = VERSION
+
 MAJOR_VERSION = '.'.join(VERSION.split('.')[:2])
 
 # Now determine the distutils-style platform tag for the target system.
@@ -437,6 +446,13 @@ elif target == 'linux' and (os.path.isfile("/lib/libc-2.17.so") or os.path.isfil
     else:
         PLATFORM = 'manylinux2014-i686'
 
+elif target == 'linux' and (os.path.isfile("/lib/i386-linux-gnu/libc-2.24.so") or os.path.isfile("/lib/x86_64/libc-2.24.so")) and os.path.isdir("/opt/python"):
+    # Same sloppy check for manylinux_2_24.
+    if GetTargetArch() in ('x86_64', 'amd64'):
+        PLATFORM = 'manylinux_2_24-x86_64'
+    else:
+        PLATFORM = 'manylinux_2_24-i686'
+
 elif not CrossCompiling():
     if HasTargetArch():
         # Replace the architecture in the platform string.
@@ -663,6 +679,7 @@ if (COMPILER == "MSVC"):
         else:
             LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Half" + suffix + ".lib")
         IncDirectory("OPENEXR", GetThirdpartyDir() + "openexr/include/OpenEXR")
+        IncDirectory("OPENEXR", GetThirdpartyDir() + "openexr/include/Imath")
     if (PkgSkip("JPEG")==0):     LibName("JPEG",     GetThirdpartyDir() + "jpeg/lib/jpeg-static.lib")
     if (PkgSkip("ZLIB")==0):     LibName("ZLIB",     GetThirdpartyDir() + "zlib/lib/zlibstatic.lib")
     if (PkgSkip("VRPN")==0):     LibName("VRPN",     GetThirdpartyDir() + "vrpn/lib/vrpn.lib")
@@ -855,7 +872,7 @@ if (COMPILER=="GCC"):
     SmartPkgEnable("OPENAL",    "openal",    ("openal"), "AL/al.h", framework = "OpenAL")
     SmartPkgEnable("SQUISH",    "",          ("squish"), "squish.h")
     SmartPkgEnable("TIFF",      "libtiff-4", ("tiff"), "tiff.h")
-    SmartPkgEnable("OPENEXR",   "OpenEXR",   ("IlmImf", "Imath", "Half", "Iex", "IexMath", "IlmThread"), ("OpenEXR", "OpenEXR/ImfOutputFile.h"))
+    SmartPkgEnable("OPENEXR",   "OpenEXR",   ("IlmImf", "Imath", "Half", "Iex", "IexMath", "IlmThread"), ("OpenEXR", "Imath", "OpenEXR/ImfOutputFile.h"))
     SmartPkgEnable("VRPN",      "",          ("vrpn", "quat"), ("vrpn", "quat.h", "vrpn/vrpn_Types.h"))
     SmartPkgEnable("BULLET", "bullet", ("BulletSoftBody", "BulletDynamics", "BulletCollision", "LinearMath"), ("bullet", "bullet/btBulletDynamicsCommon.h"))
     SmartPkgEnable("VORBIS",    "vorbisfile",("vorbisfile", "vorbis", "ogg"), ("ogg/ogg.h", "vorbis/vorbisfile.h"))
@@ -996,8 +1013,11 @@ if (COMPILER=="GCC"):
         LibName("OPENSSL", "-Wl,--exclude-libs,libssl.a")
         LibName("OPENSSL", "-Wl,--exclude-libs,libcrypto.a")
 
-    if GetTarget() not in ('darwin', 'android'):
-        SmartPkgEnable("X11", "x11", "X11", ("X11", "X11/Xlib.h", "X11/XKBlib.h"))
+    if GetTarget() != 'darwin':
+        if GetTarget() != "android":
+            SmartPkgEnable("X11", "x11", "X11", ("X11", "X11/Xlib.h", "X11/XKBlib.h"))
+        else:
+            PkgDisable("X11")
 
     if GetHost() != "darwin":
         # Workaround for an issue where pkg-config does not include this path
@@ -1061,6 +1081,7 @@ if (COMPILER=="GCC"):
         LibName("ALWAYS", '-llog')
         LibName("ANDROID", '-landroid')
         LibName("JNIGRAPHICS", '-ljnigraphics')
+        LibName("OPENSLES", '-lOpenSLES')
 
     for pkg in MAYAVERSIONS:
         if (PkgSkip(pkg)==0 and (pkg in SDK)):
@@ -1089,7 +1110,7 @@ if (COMPILER=="GCC"):
             LibName(pkg, "-lDependEngine")
             LibName(pkg, "-lCommandEngine")
             LibName(pkg, "-lFoundation")
-            if pkg != "MAYA2020":
+            if pkg not in ("MAYA2020", "MAYA2022"):
                 LibName(pkg, "-lIMFbase")
             if GetTarget() != 'darwin':
                 LibName(pkg, "-lOpenMayalib")
@@ -1357,10 +1378,6 @@ def CompileCxx(obj,src,opts):
         if "SYSROOT" in SDK:
             if GetTarget() != "android":
                 cmd += ' --sysroot=%s' % (SDK["SYSROOT"])
-            else:
-                ndk_dir = SDK["ANDROID_NDK"].replace('\\', '/')
-                cmd += ' -isystem %s/sysroot/usr/include' % (ndk_dir)
-                cmd += ' -isystem %s/sysroot/usr/include/%s' % (ndk_dir, SDK["ANDROID_TRIPLE"])
             cmd += ' -no-canonical-prefixes'
 
         # Android-specific flags.
@@ -1369,46 +1386,35 @@ def CompileCxx(obj,src,opts):
         if GetTarget() == "android":
             # Most of the specific optimization flags here were
             # just copied from the default Android Makefiles.
-            if "ANDROID_API" in SDK:
-                cmd += ' -D__ANDROID_API__=' + str(SDK["ANDROID_API"])
             if "ANDROID_GCC_TOOLCHAIN" in SDK:
                 cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/')
             cmd += ' -ffunction-sections -funwind-tables'
+            cmd += ' -target ' + SDK["ANDROID_TRIPLE"]
             if arch == 'armv7a':
-                cmd += ' -target armv7-none-linux-androideabi'
                 cmd += ' -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16'
-                cmd += ' -fno-integrated-as'
             elif arch == 'arm':
-                cmd += ' -target armv5te-none-linux-androideabi'
                 cmd += ' -march=armv5te -mtune=xscale -msoft-float'
-                cmd += ' -fno-integrated-as'
-            elif arch == 'aarch64':
-                cmd += ' -target aarch64-none-linux-android'
             elif arch == 'mips':
-                cmd += ' -target mipsel-none-linux-android'
                 cmd += ' -mips32'
             elif arch == 'mips64':
-                cmd += ' -target mips64el-none-linux-android'
                 cmd += ' -fintegrated-as'
             elif arch == 'x86':
-                cmd += ' -target i686-none-linux-android'
-                cmd += ' -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32'
+                cmd += ' -march=i686 -mssse3 -mfpmath=sse -m32'
                 cmd += ' -mstackrealign'
             elif arch == 'x86_64':
-                cmd += ' -target x86_64-none-linux-android'
-                cmd += ' -march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel'
+                cmd += ' -march=x86-64 -msse4.2 -mpopcnt -m64'
 
             cmd += " -Wa,--noexecstack"
 
             # Do we want thumb or arm instructions?
-            if arch.startswith('arm'):
+            if arch != 'arm64' and arch.startswith('arm'):
                 if optlevel >= 3:
                     cmd += ' -mthumb'
                 else:
                     cmd += ' -marm'
 
             # Enable SIMD instructions if requested
-            if arch.startswith('arm') and PkgSkip("NEON") == 0:
+            if arch != 'arm64' and arch.startswith('arm') and PkgSkip("NEON") == 0:
                 cmd += ' -mfpu=neon'
 
         else:
@@ -1437,14 +1443,17 @@ def CompileCxx(obj,src,opts):
         # Needed by both Python, Panda, Eigen, all of which break aliasing rules.
         cmd += " -fno-strict-aliasing"
 
-        if optlevel >= 3:
-            cmd += " -ffast-math -fno-stack-protector"
-        if optlevel == 3:
-            # Fast math is nice, but we'd like to see NaN in dev builds.
-            cmd += " -fno-finite-math-only"
+        # Certain clang versions crash when passing these math flags while
+        # compiling Objective-C++ code
+        if not src.endswith(".m") and not src.endswith(".mm"):
+            if optlevel >= 3:
+                cmd += " -ffast-math -fno-stack-protector"
+            if optlevel == 3:
+                # Fast math is nice, but we'd like to see NaN in dev builds.
+                cmd += " -fno-finite-math-only"
 
-        # Make sure this is off to avoid GCC/Eigen bug (see GitHub #228)
-        cmd += " -fno-unsafe-math-optimizations"
+            # Make sure this is off to avoid GCC/Eigen bug (see GitHub #228)
+            cmd += " -fno-unsafe-math-optimizations"
 
         if (optlevel==1): cmd += " -ggdb -D_DEBUG"
         if (optlevel==2): cmd += " -O1 -D_DEBUG"
@@ -1897,28 +1906,16 @@ def CompileLink(dll, obj, opts):
             if "ANDROID_GCC_TOOLCHAIN" in SDK:
                 cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/')
             cmd += " -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now"
+            cmd += ' -target ' + SDK["ANDROID_TRIPLE"]
             if arch == 'armv7a':
-                cmd += ' -target armv7-none-linux-androideabi'
                 cmd += " -march=armv7-a -Wl,--fix-cortex-a8"
-            elif arch == 'arm':
-                cmd += ' -target armv5te-none-linux-androideabi'
-            elif arch == 'aarch64':
-                cmd += ' -target aarch64-none-linux-android'
             elif arch == 'mips':
-                cmd += ' -target mipsel-none-linux-android'
                 cmd += ' -mips32'
-            elif arch == 'mips64':
-                cmd += ' -target mips64el-none-linux-android'
-            elif arch == 'x86':
-                cmd += ' -target i686-none-linux-android'
-            elif arch == 'x86_64':
-                cmd += ' -target x86_64-none-linux-android'
             cmd += ' -lc -lm'
         else:
             cmd += " -pthread"
-
-        if "SYSROOT" in SDK:
-            cmd += " --sysroot=%s -no-canonical-prefixes" % (SDK["SYSROOT"])
+            if "SYSROOT" in SDK:
+                cmd += " --sysroot=%s -no-canonical-prefixes" % (SDK["SYSROOT"])
 
         if LDFLAGS != "":
             cmd += " " + LDFLAGS
@@ -2182,6 +2179,31 @@ def CompileMIDL(target, src, opts):
 
         oscmd(cmd)
 
+##########################################################################################
+#
+# CompileDalvik
+#
+##########################################################################################
+
+def CompileDalvik(target, inputs, opts):
+    cmd = "d8 --output " + os.path.dirname(target)
+
+    if GetOptimize() <= 2:
+        cmd += " --debug"
+    else:
+        cmd += " --release"
+
+    if "ANDROID_API" in SDK:
+        cmd += " --min-api %d" % (SDK["ANDROID_API"])
+
+    if "ANDROID_JAR" in SDK:
+        cmd += " --lib %s" % (SDK["ANDROID_JAR"])
+
+    for i in inputs:
+        cmd += " " + BracketNameWithQuotes(i)
+
+    oscmd(cmd)
+
 ##########################################################################################
 #
 # CompileAnything
@@ -2293,6 +2315,9 @@ def CompileAnything(target, inputs, opts, progress = None):
         elif infile.endswith(".r"):
             ProgressOutput(progress, "Building resource object", target)
             return CompileRsrc(target, infile, opts)
+    elif origsuffix == ".dex":
+        ProgressOutput(progress, "Building Dalvik object", target)
+        return CompileDalvik(target, inputs, opts)
     exit("Don't know how to compile: %s from %s" % (target, inputs))
 
 ##########################################################################################
@@ -2887,7 +2912,12 @@ if PkgSkip("GL") or GetLinkAllStatic():
     configprc = configprc.replace("\nload-display pandagl", "\n#load-display pandagl")
 
 if PkgSkip("GLES") or GetLinkAllStatic():
-    configprc = configprc.replace("\n#load-display pandagles", "")
+    configprc = configprc.replace("\n#load-display pandagles\n", "\n")
+
+if PkgSkip("GL") and not PkgSkip("GLES2") and not GetLinkAllStatic():
+    configprc = configprc.replace("\n#load-display pandagles2", "\nload-display pandagles2")
+elif PkgSkip("GLES2") or GetLinkAllStatic():
+    configprc = configprc.replace("\n#load-display pandagles2", "")
 
 if PkgSkip("DX9") or GetLinkAllStatic():
     configprc = configprc.replace("\n#load-display pandadx9", "")
@@ -3105,8 +3135,10 @@ else:
 if not PkgSkip("PANDATOOL"):
     CopyAllFiles(GetOutputDir()+"/plugins/",  "pandatool/src/scripts/", ".mel")
     CopyAllFiles(GetOutputDir()+"/plugins/",  "pandatool/src/scripts/", ".ms")
-if not PkgSkip("PYTHON") and os.path.isdir(GetThirdpartyBase()+"/Pmw"):
-    CopyTree(GetOutputDir()+'/Pmw',         GetThirdpartyBase()+'/Pmw')
+
+if not PkgSkip("PYTHON") and os.path.isdir(GetThirdpartyBase() + "/Pmw"):
+    CopyTree(GetOutputDir() + "/Pmw", GetThirdpartyBase() + "/Pmw", exclude=["Pmw_1_3", "Pmw_1_3_3"])
+
 ConditionalWriteFile(GetOutputDir()+'/include/ctl3d.h', '/* dummy file to make MAX happy */')
 
 # Since Eigen is included by all sorts of core headers, as a convenience
@@ -4400,7 +4432,7 @@ if PkgSkip("OPENAL") == 0:
     TargetAdd('openal_audio_openal_audio_composite1.obj', opts=OPTS, input='openal_audio_composite1.cxx')
     TargetAdd('libp3openal_audio.dll', input='openal_audio_openal_audio_composite1.obj')
     TargetAdd('libp3openal_audio.dll', input=COMMON_PANDA_LIBS)
-    TargetAdd('libp3openal_audio.dll', opts=['MODULE', 'ADVAPI', 'WINUSER', 'WINMM', 'WINSHELL', 'WINOLE', 'OPENAL'])
+    TargetAdd('libp3openal_audio.dll', opts=['MODULE', 'ADVAPI', 'WINUSER', 'WINMM', 'WINSHELL', 'WINOLE', 'OPENAL', 'OPENSLES'])
 
 #
 # DIRECTORY: panda/src/downloadertools/
@@ -4685,7 +4717,7 @@ elif not PkgSkip("EGL") and not PkgSkip("GL") and GetTarget() not in ('windows',
 # DIRECTORY: panda/src/egldisplay/
 #
 
-if not PkgSkip("EGL") and not PkgSkip("GLES"):
+if GetTarget() != 'android' and not PkgSkip("EGL") and not PkgSkip("GLES"):
     DefSymbol('GLES', 'OPENGLES_1', '')
     OPTS=['DIR:panda/src/egldisplay', 'DIR:panda/src/glstuff', 'BUILDING:PANDAGLES', 'GLES', 'EGL', 'X11']
     TargetAdd('pandagles_egldisplay_composite1.obj', opts=OPTS, input='p3egldisplay_composite1.cxx')
@@ -4704,7 +4736,7 @@ if not PkgSkip("EGL") and not PkgSkip("GLES"):
 # DIRECTORY: panda/src/egldisplay/
 #
 
-if not PkgSkip("EGL") and not PkgSkip("GLES2"):
+if GetTarget() != 'android' and not PkgSkip("EGL") and not PkgSkip("GLES2"):
     DefSymbol('GLES2', 'OPENGLES_2', '')
     OPTS=['DIR:panda/src/egldisplay', 'DIR:panda/src/glstuff', 'BUILDING:PANDAGLES2', 'GLES2', 'EGL', 'X11']
     TargetAdd('pandagles2_egldisplay_composite1.obj', opts=OPTS, input='p3egldisplay_composite1.cxx')
@@ -4926,12 +4958,17 @@ if not PkgSkip("PVIEW"):
 #
 
 if GetTarget() == 'android':
-    OPTS=['DIR:panda/src/android']
+    OPTS=['DIR:panda/src/android', 'PNG']
     TargetAdd('org/panda3d/android/NativeIStream.class', opts=OPTS, input='NativeIStream.java')
     TargetAdd('org/panda3d/android/NativeOStream.class', opts=OPTS, input='NativeOStream.java')
     TargetAdd('org/panda3d/android/PandaActivity.class', opts=OPTS, input='PandaActivity.java')
     TargetAdd('org/panda3d/android/PythonActivity.class', opts=OPTS, input='PythonActivity.java')
 
+    TargetAdd('classes.dex', input='org/panda3d/android/NativeIStream.class')
+    TargetAdd('classes.dex', input='org/panda3d/android/NativeOStream.class')
+    TargetAdd('classes.dex', input='org/panda3d/android/PandaActivity.class')
+    TargetAdd('classes.dex', input='org/panda3d/android/PythonActivity.class')
+
     TargetAdd('p3android_composite1.obj', opts=OPTS, input='p3android_composite1.cxx')
     TargetAdd('libp3android.dll', input='p3android_composite1.obj')
     TargetAdd('libp3android.dll', input=COMMON_PANDA_LIBS)
@@ -4981,6 +5018,20 @@ if GetTarget() == 'android' and not PkgSkip("EGL") and not PkgSkip("GLES"):
     TargetAdd('libpandagles.dll', input=COMMON_PANDA_LIBS)
     TargetAdd('libpandagles.dll', opts=['MODULE', 'GLES', 'EGL'])
 
+if GetTarget() == 'android' and not PkgSkip("EGL") and not PkgSkip("GLES2"):
+    DefSymbol('GLES2', 'OPENGLES_2', '')
+    OPTS=['DIR:panda/src/androiddisplay', 'DIR:panda/src/glstuff', 'BUILDING:PANDAGLES2', 'GLES2', 'EGL']
+    TargetAdd('pandagles2_androiddisplay_composite1.obj', opts=OPTS, input='p3androiddisplay_composite1.cxx')
+    OPTS=['DIR:panda/metalibs/pandagles2', 'BUILDING:PANDAGLES2', 'GLES2', 'EGL']
+    TargetAdd('pandagles2_pandagles2.obj', opts=OPTS, input='pandagles2.cxx')
+    TargetAdd('libpandagles2.dll', input='pandagles2_pandagles2.obj')
+    TargetAdd('libpandagles2.dll', input='p3gles2gsg_config_gles2gsg.obj')
+    TargetAdd('libpandagles2.dll', input='p3gles2gsg_gles2gsg.obj')
+    TargetAdd('libpandagles2.dll', input='pandagles2_androiddisplay_composite1.obj')
+    TargetAdd('libpandagles2.dll', input='libp3android.dll')
+    TargetAdd('libpandagles2.dll', input=COMMON_PANDA_LIBS)
+    TargetAdd('libpandagles2.dll', opts=['MODULE', 'GLES2', 'EGL'])
+
 #
 # DIRECTORY: panda/src/tinydisplay/
 #
@@ -6077,10 +6128,11 @@ if PkgSkip("PYTHON") == 0:
         LibName('DEPLOYSTUB', "-Wl,-rpath,\\$ORIGIN")
         LibName('DEPLOYSTUB', "-Wl,-z,origin")
         LibName('DEPLOYSTUB', "-rdynamic")
+
     PyTargetAdd('deploy-stub.exe', input='deploy-stub.obj')
     if GetTarget() == 'windows':
         PyTargetAdd('deploy-stub.exe', input='frozen_dllmain.obj')
-    PyTargetAdd('deploy-stub.exe', opts=['WINSHELL', 'DEPLOYSTUB', 'NOICON'])
+    PyTargetAdd('deploy-stub.exe', opts=['WINSHELL', 'DEPLOYSTUB', 'NOICON', 'ANDROID'])
 
     if GetTarget() == 'windows':
         PyTargetAdd('deploy-stubw.exe', input='deploy-stub.obj')
@@ -6092,6 +6144,15 @@ if PkgSkip("PYTHON") == 0:
         PyTargetAdd('deploy-stubw.obj', opts=OPTS, input='deploy-stub.c')
         PyTargetAdd('deploy-stubw.exe', input='deploy-stubw.obj')
         PyTargetAdd('deploy-stubw.exe', opts=['MACOS_APP_BUNDLE', 'DEPLOYSTUB', 'NOICON'])
+    elif GetTarget() == 'android':
+        PyTargetAdd('deploy-stubw_android_main.obj', opts=OPTS, input='android_main.cxx')
+        PyTargetAdd('deploy-stubw_android_log.obj', opts=OPTS, input='android_log.c')
+        PyTargetAdd('libdeploy-stubw.dll', input='android_native_app_glue.obj')
+        PyTargetAdd('libdeploy-stubw.dll', input='deploy-stubw_android_main.obj')
+        PyTargetAdd('libdeploy-stubw.dll', input='deploy-stubw_android_log.obj')
+        PyTargetAdd('libdeploy-stubw.dll', input=COMMON_PANDA_LIBS)
+        PyTargetAdd('libdeploy-stubw.dll', input='libp3android.dll')
+        PyTargetAdd('libdeploy-stubw.dll', opts=['DEPLOYSTUB', 'ANDROID'])
 
 #
 # Generate the models directory and samples directory
@@ -6192,7 +6253,7 @@ def ParallelMake(tasklist):
     # Create the workers
     for slave in range(THREADCOUNT):
         th = threading.Thread(target=BuildWorker, args=[taskqueue, donequeue])
-        th.setDaemon(1)
+        th.daemon = True
         th.start()
     # Feed tasks to the workers.
     tasksqueued = 0
@@ -6283,8 +6344,8 @@ if INSTALLER:
 
     MakeInstaller(version=VERSION, outputdir=GetOutputDir(),
                   optimize=GetOptimize(), compressor=COMPRESSOR,
-                  debversion=DEBVERSION, rpmrelease=RPMRELEASE,
-                  python_versions=python_versions)
+                  debversion=DEBVERSION, rpmversion=RPMVERSION,
+                  rpmrelease=RPMRELEASE, python_versions=python_versions)
 
 if WHEEL:
     ProgressOutput(100.0, "Building wheel")

+ 85 - 62
makepanda/makepandacore.py

@@ -6,7 +6,6 @@
 ########################################################################
 
 import configparser
-from distutils import sysconfig
 import fnmatch
 import getpass
 import glob
@@ -18,6 +17,7 @@ import shutil
 import signal
 import subprocess
 import sys
+import sysconfig
 import threading
 import _thread as thread
 import time
@@ -28,7 +28,7 @@ SUFFIX_LIB = [".lib",".ilb"]
 VCS_DIRS = set(["CVS", "CVSROOT", ".git", ".hg", "__pycache__"])
 VCS_FILES = set([".cvsignore", ".gitignore", ".gitmodules", ".hgignore"])
 STARTTIME = time.time()
-MAINTHREAD = threading.currentThread()
+MAINTHREAD = threading.current_thread()
 OUTPUTDIR = "built"
 CUSTOM_OUTPUTDIR = False
 THIRDPARTYBASE = None
@@ -80,6 +80,7 @@ MSVCVERSIONINFO = {
     (14,0): {"vsversion":(14,0), "vsname":"Visual Studio 2015"},
     (14,1): {"vsversion":(15,0), "vsname":"Visual Studio 2017"},
     (14,2): {"vsversion":(16,0), "vsname":"Visual Studio 2019"},
+    (14,3): {"vsversion":(17,0), "vsname":"Visual Studio 2022"},
 }
 
 ########################################################################
@@ -108,6 +109,7 @@ MAYAVERSIONINFO = [("MAYA6",   "6.0"),
                    ("MAYA2018","2018"),
                    ("MAYA2019","2019"),
                    ("MAYA2020","2020"),
+                   ("MAYA2022","2022"),
 ]
 
 MAXVERSIONINFO = [("MAX6", "SOFTWARE\\Autodesk\\3DSMAX\\6.0", "installdir", "maxsdk\\cssdk\\include"),
@@ -242,7 +244,7 @@ def ProgressOutput(progress, msg, target = None):
     sys.stdout.flush()
     sys.stderr.flush()
     prefix = ""
-    thisthread = threading.currentThread()
+    thisthread = threading.current_thread()
     if thisthread is MAINTHREAD:
         if progress is None:
             prefix = ""
@@ -272,7 +274,7 @@ def ProgressOutput(progress, msg, target = None):
 def exit(msg = ""):
     sys.stdout.flush()
     sys.stderr.flush()
-    if threading.currentThread() == MAINTHREAD:
+    if threading.current_thread() == MAINTHREAD:
         SaveDependencyCache()
         print("Elapsed Time: " + PrettyTime(time.time() - STARTTIME))
         print(msg)
@@ -392,30 +394,30 @@ def SetTarget(target, arch=None):
             else:
                 arch = 'armv7a'
 
-        if arch == 'arm64':
-            arch = 'aarch64'
+        if arch == 'aarch64':
+            arch = 'arm64'
 
         # Did we specify an API level?
         global ANDROID_API
         target, _, api = target.partition('-')
         if api:
             ANDROID_API = int(api)
-        elif arch in ('mips64', 'aarch64', 'x86_64'):
+        elif arch in ('mips64', 'arm64', 'x86_64'):
             # 64-bit platforms were introduced in Android 21.
             ANDROID_API = 21
         else:
-            # Default to the lowest API level supported by NDK r16.
-            ANDROID_API = 14
+            # Default to the lowest API level still supported by Google.
+            ANDROID_API = 19
 
         # Determine the prefix for our gcc tools, eg. arm-linux-androideabi-gcc
         global ANDROID_ABI, ANDROID_TRIPLE
         if arch == 'armv7a':
             ANDROID_ABI = 'armeabi-v7a'
-            ANDROID_TRIPLE = 'arm-linux-androideabi'
+            ANDROID_TRIPLE = 'armv7a-linux-androideabi'
         elif arch == 'arm':
             ANDROID_ABI = 'armeabi'
             ANDROID_TRIPLE = 'arm-linux-androideabi'
-        elif arch == 'aarch64':
+        elif arch == 'arm64':
             ANDROID_ABI = 'arm64-v8a'
             ANDROID_TRIPLE = 'aarch64-linux-android'
         elif arch == 'mips':
@@ -431,8 +433,9 @@ def SetTarget(target, arch=None):
             ANDROID_ABI = 'x86_64'
             ANDROID_TRIPLE = 'x86_64-linux-android'
         else:
-            exit('Android architecture must be arm, armv7a, aarch64, mips, mips64, x86 or x86_64')
+            exit('Android architecture must be arm, armv7a, arm64, mips, mips64, x86 or x86_64')
 
+        ANDROID_TRIPLE += str(ANDROID_API)
         TOOLCHAIN_PREFIX = ANDROID_TRIPLE + '-'
 
     elif target == 'linux':
@@ -497,7 +500,7 @@ def GetCXX():
 def GetStrip():
     # Hack
     if TARGET == 'android':
-        return TOOLCHAIN_PREFIX + 'strip'
+        return 'llvm-strip'
     else:
         return 'strip'
 
@@ -636,6 +639,7 @@ def oscmd(cmd, ignoreError = False, cwd=None):
             os.chdir(pwd)
     else:
         cmd = cmd.replace(';', '\\;')
+        cmd = cmd.replace('$', '\\$')
         res = subprocess.call(cmd, cwd=cwd, shell=True)
         sig = res & 0x7F
         if (GetVerbose() and res != 0):
@@ -1361,7 +1365,7 @@ def GetThirdpartyDir():
             THIRDPARTYDIR = base + "/freebsd-libs-a/"
 
     elif (target == 'android'):
-        THIRDPARTYDIR = GetThirdpartyBase()+"/android-libs-%s/" % (GetTargetArch())
+        THIRDPARTYDIR = base + "/android-libs-%s/" % (target_arch)
 
     else:
         Warn("Unsupported platform:", target)
@@ -1556,8 +1560,8 @@ def PkgConfigGetIncDirs(pkgname, tool = "pkg-config"):
     else:
         handle = os.popen(LocateBinary(tool) + " --cflags")
     result = handle.read().strip()
-    if len(result) == 0: return []
     handle.close()
+    if len(result) == 0: return []
     dirs = []
     for opt in result.split(" "):
         if (opt.startswith("-I")):
@@ -2174,12 +2178,12 @@ def SdkLocatePython(prefer_thirdparty_python=False):
         LibDirectory("PYTHON", py_fwx + "/lib")
 
     #elif GetTarget() == 'windows':
-    #    SDK["PYTHON"] = os.path.dirname(sysconfig.get_python_inc())
+    #    SDK["PYTHON"] = os.path.dirname(sysconfig.get_path("include"))
     #    SDK["PYTHONVERSION"] = "python" + sysconfig.get_python_version()
     #    SDK["PYTHONEXEC"] = sys.executable
 
     else:
-        SDK["PYTHON"] = sysconfig.get_python_inc()
+        SDK["PYTHON"] = sysconfig.get_path("include")
         SDK["PYTHONVERSION"] = "python" + sysconfig.get_python_version() + abiflags
         SDK["PYTHONEXEC"] = os.path.realpath(sys.executable)
 
@@ -2286,7 +2290,7 @@ def SdkLocateWindows(version=None):
     if version == '10':
         version = '10.0'
 
-    if version and version.startswith('10.') and version.count('.') == 1:
+    if (version and version.startswith('10.') and version.count('.') == 1) or version == '11':
         # Choose the latest version of the Windows 10 SDK.
         platsdk = GetRegistryKey("SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots", "KitsRoot10")
 
@@ -2295,7 +2299,13 @@ def SdkLocateWindows(version=None):
             platsdk = "C:\\Program Files (x86)\\Windows Kits\\10\\"
 
         if platsdk and os.path.isdir(platsdk):
+            min_version = (10, 0, 0)
+            if version == '11':
+                version = '10.0'
+                min_version = (10, 0, 22000)
+
             incdirs = glob.glob(os.path.join(platsdk, 'Include', version + '.*.*'))
+
             max_version = ()
             for dir in incdirs:
                 verstring = os.path.basename(dir)
@@ -2313,7 +2323,7 @@ def SdkLocateWindows(version=None):
                     continue
 
                 vertuple = tuple(map(int, verstring.split('.')))
-                if vertuple > max_version:
+                if vertuple > max_version and vertuple > min_version:
                     version = verstring
                     max_version = vertuple
 
@@ -2391,7 +2401,7 @@ def SdkLocateMacOSX(archs = []):
         # Prefer pre-10.14 for now so that we can keep building FMOD.
         sdk_versions += ["10.13", "10.12", "10.11", "10.10", "10.9"]
 
-    sdk_versions += ["11.1", "11.0"]
+    sdk_versions += ["11.3", "11.1", "11.0"]
 
     if 'arm64' not in archs:
         sdk_versions += ["10.15", "10.14"]
@@ -2531,19 +2541,15 @@ def SdkLocateAndroid():
     SDK["SYSROOT"] = os.path.join(ndk_root, 'platforms', 'android-%s' % (api), arch_dir).replace('\\', '/')
     #IncDirectory("ALWAYS", os.path.join(SDK["SYSROOT"], 'usr', 'include'))
 
-    # Starting with NDK r16, libc++ is the recommended STL to use.
+    # We need to redistribute the C++ standard library.
     stdlibc = os.path.join(ndk_root, 'sources', 'cxx-stl', 'llvm-libc++')
-    IncDirectory("ALWAYS", os.path.join(stdlibc, 'include').replace('\\', '/'))
-    LibDirectory("ALWAYS", os.path.join(stdlibc, 'libs', abi).replace('\\', '/'))
-
     stl_lib = os.path.join(stdlibc, 'libs', abi, 'libc++_shared.so')
-    LibName("ALWAYS", stl_lib.replace('\\', '/'))
     CopyFile(os.path.join(GetOutputDir(), 'lib', 'libc++_shared.so'), stl_lib)
 
     # The Android support library polyfills C++ features not available in the
     # STL that ships with Android.
-    support = os.path.join(ndk_root, 'sources', 'android', 'support', 'include')
-    IncDirectory("ALWAYS", support.replace('\\', '/'))
+    #support = os.path.join(ndk_root, 'sources', 'android', 'support', 'include')
+    #IncDirectory("ALWAYS", support.replace('\\', '/'))
     if api < 21:
         LibName("ALWAYS", "-landroid_support")
 
@@ -2781,7 +2787,7 @@ def SetupVisualStudioEnviron():
         elif not win_kit.endswith('\\'):
             win_kit += '\\'
 
-        for vnum in 10150, 10240, 10586, 14393, 15063, 16299, 17134, 17763, 18362:
+        for vnum in 10150, 10240, 10586, 14393, 15063, 16299, 17134, 17763, 18362, 19041, 20348, 22000:
             version = "10.0.{0}.0".format(vnum)
             if os.path.isfile(win_kit + "Include\\" + version + "\\ucrt\\assert.h"):
                 print("Using Universal CRT %s" % (version))
@@ -2898,8 +2904,10 @@ def SetupBuildEnvironment(compiler):
         if SDK.get("MACOSX"):
             # The default compiler in Leopard does not respect --sysroot correctly.
             sysroot_flag = " -isysroot " + SDK["MACOSX"]
-        if SDK.get("SYSROOT"):
-            sysroot_flag = ' --sysroot=%s -no-canonical-prefixes' % (SDK["SYSROOT"])
+        #if SDK.get("SYSROOT"):
+        #    sysroot_flag = ' --sysroot=%s -no-canonical-prefixes' % (SDK["SYSROOT"])
+        if GetTarget() == "android":
+            sysroot_flag = " -target " + ANDROID_TRIPLE
 
         # Extract the dirs from the line that starts with 'libraries: ='.
         cmd = GetCXX() + " -print-search-dirs" + sysroot_flag
@@ -2935,12 +2943,7 @@ def SetupBuildEnvironment(compiler):
 
         # Now extract the preprocessor's include directories.
         cmd = GetCXX() + " -x c++ -v -E " + os.devnull
-        if "ANDROID_NDK" in SDK:
-            ndk_dir = SDK["ANDROID_NDK"].replace('\\', '/')
-            cmd += ' -isystem %s/sysroot/usr/include' % (ndk_dir)
-            cmd += ' -isystem %s/sysroot/usr/include/%s' % (ndk_dir, SDK["ANDROID_TRIPLE"])
-        else:
-            cmd += sysroot_flag
+        cmd += sysroot_flag
 
         null = open(os.devnull, 'w')
         handle = subprocess.Popen(cmd, stdout=null, stderr=subprocess.PIPE, shell=True)
@@ -3082,13 +3085,16 @@ def CopyAllHeaders(dir, skip=[]):
             WriteBinaryFile(dstfile, ReadBinaryFile(srcfile))
             JustBuilt([dstfile], [srcfile])
 
-def CopyTree(dstdir, srcdir, omitVCS=True):
+def CopyTree(dstdir, srcdir, omitVCS=True, exclude=()):
     if os.path.isdir(dstdir):
         source_entries = os.listdir(srcdir)
         for entry in source_entries:
             srcpth = os.path.join(srcdir, entry)
             dstpth = os.path.join(dstdir, entry)
 
+            if entry in exclude:
+                continue
+
             if os.path.islink(srcpth) or os.path.isfile(srcpth):
                 if not omitVCS or entry not in VCS_FILES:
                     CopyFile(dstpth, srcpth)
@@ -3098,7 +3104,7 @@ def CopyTree(dstdir, srcdir, omitVCS=True):
 
         # Delete files in dstdir that are not in srcdir.
         for entry in os.listdir(dstdir):
-            if entry not in source_entries:
+            if entry not in source_entries or entry in exclude:
                 path = os.path.join(dstdir, entry)
                 if os.path.islink(path) or os.path.isfile(path):
                     os.remove(path)
@@ -3114,6 +3120,13 @@ def CopyTree(dstdir, srcdir, omitVCS=True):
             if subprocess.call(['cp', '-R', '-f', srcdir, dstdir]) != 0:
                 exit("Copy failed.")
 
+        for entry in exclude:
+            path = os.path.join(dstdir, entry)
+            if os.path.islink(path) or os.path.isfile(path):
+                os.remove(path)
+            elif os.path.isdir(path):
+                shutil.rmtree(path)
+
         if omitVCS:
             DeleteVCS(dstdir)
 
@@ -3244,6 +3257,22 @@ def WriteResourceFile(basename, **kwargs):
     return basename
 
 
+def GenerateEmbeddedStringFile(string_name, data):
+    yield 'extern const char %s[] = {\n' % (string_name)
+    i = 0
+    for byte in data:
+        if i == 0:
+            yield ' '
+
+        yield ' 0x%02x,' % (byte)
+        i += 1
+        if i >= 12:
+            yield '\n'
+            i = 0
+
+    yield '\n};\n'
+
+
 def WriteEmbeddedStringFile(basename, inputs, string_name=None):
     if os.path.splitext(basename)[1] not in SUFFIX_INC:
         basename += '.cxx'
@@ -3268,20 +3297,7 @@ def WriteEmbeddedStringFile(basename, inputs, string_name=None):
 
     data.append(0)
 
-    output = 'extern const char %s[] = {\n' % (string_name)
-
-    i = 0
-    for byte in data:
-        if i == 0:
-            output += ' '
-
-        output += ' 0x%02x,' % (byte)
-        i += 1
-        if i >= 12:
-            output += '\n'
-            i = 0
-
-    output += '\n};\n'
+    output = ''.join(GenerateEmbeddedStringFile(string_name, data))
     ConditionalWriteFile(target, output)
     return target
 
@@ -3302,13 +3318,17 @@ def SetOrigExt(x, v):
     ORIG_EXT[x] = v
 
 def GetExtensionSuffix():
-    import _imp
-    return _imp.extension_suffixes()[0]
+    if CrossCompiling():
+        return '.{0}.so'.format(GetPythonABI())
+    else:
+        import _imp
+        return _imp.extension_suffixes()[0]
 
 def GetPythonABI():
-    soabi = sysconfig.get_config_var('SOABI')
-    if soabi:
-        return soabi
+    if not CrossCompiling():
+        soabi = sysconfig.get_config_var('SOABI')
+        if soabi:
+            return soabi
 
     soabi = 'cpython-%d%d' % (sys.version_info[:2])
 
@@ -3336,6 +3356,7 @@ def CalcLocation(fn, ipath):
     if (GetOptimize() <= 2 and target == 'windows'): dllext = "_d"
 
     if (fn == "AndroidManifest.xml"): return OUTPUTDIR+"/"+fn
+    if (fn == "classes.dex"): return OUTPUTDIR+"/"+fn
     if (fn.endswith(".cxx")): return CxxFindSource(fn, ipath)
     if (fn.endswith(".I")):   return CxxFindSource(fn, ipath)
     if (fn.endswith(".h")):   return CxxFindSource(fn, ipath)
@@ -3434,14 +3455,13 @@ def GetCurrentPythonVersionInfo():
     if PkgSkip("PYTHON"):
         return
 
-    from distutils.sysconfig import get_python_lib
     return {
         "version": SDK["PYTHONVERSION"][6:].rstrip('dmu'),
         "soabi": GetPythonABI(),
         "ext_suffix": GetExtensionSuffix(),
         "executable": sys.executable,
-        "purelib": get_python_lib(False),
-        "platlib": get_python_lib(True),
+        "purelib": sysconfig.get_path("purelib"),
+        "platlib": sysconfig.get_path("platlib"),
     }
 
 
@@ -3452,7 +3472,8 @@ def UpdatePythonVersionInfoFile(new_info):
     json_data = []
     if os.path.isfile(json_file) and not PkgSkip("PYTHON"):
         try:
-            json_data = json.load(open(json_file, 'r'))
+            with open(json_file, 'r') as fh:
+                json_data = json.load(fh)
         except:
             json_data = []
 
@@ -3472,7 +3493,9 @@ def UpdatePythonVersionInfoFile(new_info):
 
     if VERBOSE:
         print("Writing %s" % (json_file))
-    json.dump(json_data, open(json_file, 'w'), indent=4)
+
+    with open(json_file, 'w') as fh:
+        json.dump(json_data, fh, indent=4)
 
 
 def ReadPythonVersionInfoFile():

+ 131 - 27
makepanda/makewheel.py

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

+ 6 - 2
makepanda/test_wheel.py

@@ -38,8 +38,12 @@ def test_wheel(wheel, verbose=False):
 
     # Install pytest into the environment, as well as our wheel.
     packages = ["pytest", wheel]
-    if sys.version_info[0:2] == (3, 4) and sys.platform == "win32":
-        packages += ["colorama==0.4.1"]
+    if sys.version_info[0:2] == (3, 4):
+        if sys.platform == "win32":
+            packages += ["colorama==0.4.1"]
+
+        # See https://github.com/python-attrs/attrs/pull/807
+        packages += ["attrs<21"]
 
     if subprocess.call([python, "-m", "pip", "install"] + packages) != 0:
         shutil.rmtree(envdir)

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

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

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

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

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

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

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

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

+ 6 - 5
panda/src/bullet/bulletDebugNode.I

@@ -20,7 +20,7 @@ INLINE BulletDebugNode::
 }
 
 /**
- *
+ * If true, displays collision shapes in wireframe mode.
  */
 INLINE void BulletDebugNode::
 show_wireframe(bool show) {
@@ -39,7 +39,8 @@ get_show_wireframe() const {
 }
 
 /**
- *
+ * If true, display limits defined for constraints, e.g. a pivot axis or maximum
+ * amplitude.
  */
 INLINE void BulletDebugNode::
 show_constraints(bool show) {
@@ -58,7 +59,7 @@ get_show_constraints() const {
 }
 
 /**
- *
+ * If true, displays axis aligned bounding boxes for objects.
  */
 INLINE void BulletDebugNode::
 show_bounding_boxes(bool show) {
@@ -77,7 +78,7 @@ get_show_bounding_boxes() const {
 }
 
 /**
- *
+ * If true, displays normal vectors for triangle mesh and heightfield faces.
  */
 INLINE void BulletDebugNode::
 show_normals(bool show) {
@@ -92,4 +93,4 @@ INLINE bool BulletDebugNode::
 get_show_normals() const {
 
   return _drawer._normals;
-}
+}

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

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

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

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

+ 4 - 4
panda/src/bullet/bulletHeightfieldShape.cxx

@@ -44,7 +44,7 @@ BulletHeightfieldShape(const PNMImage &image, PN_stdfloat max_height, BulletUpAx
 
   _shape = new btHeightfieldTerrainShape(_num_rows,
                                          _num_cols,
-                                         _data,
+                                         (const void *)_data,
                                          max_height,
                                          up,
                                          true, false);
@@ -102,7 +102,7 @@ BulletHeightfieldShape(Texture *tex, PN_stdfloat max_height, BulletUpAxis up) :
 
   _shape = new btHeightfieldTerrainShape(_num_rows,
                                          _num_cols,
-                                         _data,
+                                         (const void *)_data,
                                          max_height,
                                          up,
                                          true, false);
@@ -127,7 +127,7 @@ BulletHeightfieldShape(const BulletHeightfieldShape &copy) {
 
   _shape = new btHeightfieldTerrainShape(_num_rows,
                                          _num_cols,
-                                         _data,
+                                         (const void *)_data,
                                          _max_height,
                                          _up,
                                          true, false);
@@ -208,7 +208,7 @@ fillin(DatagramIterator &scan, BamReader *manager) {
 
   _shape = new btHeightfieldTerrainShape(_num_rows,
                                          _num_cols,
-                                         _data,
+                                         (const void *)_data,
                                          _max_height,
                                          _up,
                                          true, false);

+ 10 - 5
panda/src/bullet/bulletPersistentManifold.cxx

@@ -49,6 +49,8 @@ get_contact_processing_threshold() const {
  */
 void BulletPersistentManifold::
 clear_manifold() {
+  nassertv_always(_manifold != nullptr);
+
   LightMutexHolder holder(BulletWorld::get_global_lock());
 
   _manifold->clearManifold();
@@ -59,6 +61,8 @@ clear_manifold() {
  */
 PandaNode *BulletPersistentManifold::
 get_node0() {
+  nassertr_always(_manifold != nullptr, nullptr);
+
   LightMutexHolder holder(BulletWorld::get_global_lock());
 
 #if BT_BULLET_VERSION >= 281
@@ -75,6 +79,8 @@ get_node0() {
  */
 PandaNode *BulletPersistentManifold::
 get_node1() {
+  nassertr_always(_manifold != nullptr, nullptr);
+
   LightMutexHolder holder(BulletWorld::get_global_lock());
 
 #if BT_BULLET_VERSION >= 281
@@ -91,6 +97,8 @@ get_node1() {
  */
 int BulletPersistentManifold::
 get_num_manifold_points() const {
+  nassertr_always(_manifold != nullptr, 0);
+
   LightMutexHolder holder(BulletWorld::get_global_lock());
 
   return _manifold->getNumContacts();
@@ -99,11 +107,8 @@ get_num_manifold_points() const {
 /**
  *
  */
-BulletManifoldPoint *BulletPersistentManifold::
+BulletManifoldPoint BulletPersistentManifold::
 get_manifold_point(int idx) const {
   LightMutexHolder holder(BulletWorld::get_global_lock());
-
-  nassertr(idx < _manifold->getNumContacts(), nullptr)
-
-  return new BulletManifoldPoint(_manifold->getContactPoint(idx));
+  return BulletManifoldPoint(_manifold->getContactPoint(idx));
 }

+ 1 - 1
panda/src/bullet/bulletPersistentManifold.h

@@ -34,7 +34,7 @@ PUBLISHED:
   PandaNode *get_node1();
 
   int get_num_manifold_points() const;
-  BulletManifoldPoint *get_manifold_point(int idx) const;
+  BulletManifoldPoint get_manifold_point(int idx) const;
   MAKE_SEQ(get_manifold_points, get_num_manifold_points, get_manifold_point);
 
   PN_stdfloat get_contact_breaking_threshold() const;

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

@@ -125,7 +125,7 @@ BulletWorld() {
   // Some prefered settings
   _world->getDispatchInfo().m_enableSPU = true;      // default: true
   _world->getDispatchInfo().m_useContinuous = true;  // default: true
-  _world->getSolverInfo().m_splitImpulse = false;    // default: false
+  _world->getSolverInfo().m_splitImpulse = bullet_split_impulse;
   _world->getSolverInfo().m_numIterations = bullet_solver_iterations;
 }
 
@@ -1064,14 +1064,14 @@ contact_test_pair(PandaNode *node0, PandaNode *node1) const {
 /**
  *
  */
-BulletPersistentManifold *BulletWorld::
+BulletPersistentManifold BulletWorld::
 get_manifold(int idx) const {
   LightMutexHolder holder(get_global_lock());
 
-  nassertr(idx < _dispatcher->getNumManifolds(), nullptr);
+  nassertr(idx < _dispatcher->getNumManifolds(), BulletPersistentManifold(nullptr));
 
   btPersistentManifold *ptr = _dispatcher->getManifoldByIndexInternal(idx);
-  return (ptr) ? new BulletPersistentManifold(ptr) : nullptr;
+  return BulletPersistentManifold(ptr);
 }
 
 /**

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

@@ -127,7 +127,7 @@ PUBLISHED:
 
   // Manifolds
   int get_num_manifolds() const;
-  BulletPersistentManifold *get_manifold(int idx) const;
+  BulletPersistentManifold get_manifold(int idx) const;
   MAKE_SEQ(get_manifolds, get_num_manifolds, get_manifold);
 
   // Collision filtering

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

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

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

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

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

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

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