Browse Source

Merge branch 'master' into shaderpipeline

rdb 19 hours ago
parent
commit
ecea4d460b
92 changed files with 1883 additions and 684 deletions
  1. 167 14
      .github/workflows/ci.yml
  2. 1 1
      .github/workflows/mypy.yml
  3. 2 0
      BACKERS.md
  4. 8 5
      README.md
  5. 23 14
      direct/src/directnotify/DirectNotify.py
  6. 61 28
      direct/src/dist/FreezeTool.py
  7. 14 1
      direct/src/dist/_android.py
  8. 13 8
      direct/src/dist/_proto/Configuration_pb2.py
  9. 13 8
      direct/src/dist/_proto/Resources_pb2.py
  10. 13 8
      direct/src/dist/_proto/config_pb2.py
  11. 33 292
      direct/src/dist/_proto/files_pb2.py
  12. 13 8
      direct/src/dist/_proto/targeting_pb2.py
  13. 111 27
      direct/src/dist/commands.py
  14. 41 11
      direct/src/dist/installers.py
  15. 2 1
      direct/src/distributed/ConnectionRepository.py
  16. 2 2
      direct/src/distributed/direct.dc
  17. 21 6
      direct/src/gui/DirectButton.py
  18. 1 0
      direct/src/gui/DirectCheckBox.py
  19. 9 1
      direct/src/gui/DirectGuiGlobals.py
  20. 1 0
      direct/src/showbase/ShowBase.py
  21. 8 0
      direct/src/showbase/showBase.cxx
  22. 3 0
      direct/src/showbase/showBase.h
  23. 28 0
      doc/ReleaseNotes
  24. 3 0
      dtool/src/dtoolbase/dtool_platform.h
  25. 3 0
      dtool/src/dtoolbase/memoryHook.cxx
  26. 12 0
      dtool/src/dtoolbase/patomic.I
  27. 1 1
      dtool/src/dtoolbase/patomic.cxx
  28. 9 4
      dtool/src/dtoolbase/patomic.h
  29. 3 1
      dtool/src/dtoolbase/pvector.h
  30. 10 0
      dtool/src/dtoolutil/load_dso.cxx
  31. 3 3
      dtool/src/prc/configPageManager.cxx
  32. 31 18
      dtool/src/prc/notify.cxx
  33. 1 1
      dtool/src/prc/pnotify.h
  34. 12 0
      makepanda/installer.nsi
  35. 38 16
      makepanda/makepanda.py
  36. 13 5
      makepanda/makepandacore.py
  37. 65 0
      panda/src/android/PandaActivity.java
  38. 41 0
      panda/src/android/PythonActivity.java
  39. 3 4
      panda/src/android/android_main.cxx
  40. 9 0
      panda/src/android/android_native_app_glue.c
  41. 1 1
      panda/src/android/android_native_app_glue.h
  42. 52 0
      panda/src/android/config_android.cxx
  43. 8 0
      panda/src/android/config_android.h
  44. 48 0
      panda/src/android/jni_PandaActivity.cxx
  45. 1 0
      panda/src/android/p3android_composite1.cxx
  46. 5 0
      panda/src/androiddisplay/androidGraphicsPipe.cxx
  47. 6 0
      panda/src/androiddisplay/androidGraphicsWindow.cxx
  48. 159 32
      panda/src/audiotraits/openalAudioManager.cxx
  49. 48 7
      panda/src/audiotraits/openalAudioSound.cxx
  50. 11 3
      panda/src/cocoadisplay/cocoaGraphicsWindow.mm
  51. 8 2
      panda/src/display/shaderInputBinding_impls.cxx
  52. 1 1
      panda/src/display/subprocessWindowBuffer.cxx
  53. 4 1
      panda/src/doc/eggSyntax.txt
  54. 20 7
      panda/src/egg/eggTexture.cxx
  55. 4 1
      panda/src/egg/eggTexture.h
  56. 10 2
      panda/src/egg2pg/eggLoader.cxx
  57. 8 2
      panda/src/egg2pg/eggSaver.cxx
  58. 1 1
      panda/src/event/asyncFuture_ext.cxx
  59. 6 8
      panda/src/event/pythonTask.cxx
  60. 0 1
      panda/src/express/virtualFileMountAndroidAsset.h
  61. 32 15
      panda/src/express/zipArchive.cxx
  62. 7 5
      panda/src/express/zipArchive.h
  63. 12 0
      panda/src/glstuff/glGraphicsStateGuardian_src.cxx
  64. 8 2
      panda/src/gobj/textureStage.cxx
  65. 5 1
      panda/src/gobj/textureStage.h
  66. 3 1
      panda/src/grutil/multitexReducer.cxx
  67. 10 0
      panda/src/ode/odeRayGeom.I
  68. 4 2
      panda/src/ode/odeRayGeom.h
  69. 16 2
      panda/src/pgraphnodes/shaderGenerator.cxx
  70. 1 0
      panda/src/pgraphnodes/shaderGenerator.h
  71. 6 1
      panda/src/pgui/pgSliderBar.cxx
  72. 6 0
      panda/src/pipeline/threadPosixImpl.I
  73. 37 10
      panda/src/pipeline/threadPosixImpl.cxx
  74. 10 2
      panda/src/pipeline/threadPosixImpl.h
  75. 1 0
      panda/src/putil/bamCache.I
  76. 83 0
      panda/src/putil/bamCache.cxx
  77. 3 1
      panda/src/putil/bamCache.h
  78. 1 1
      panda/src/x11display/config_x11display.cxx
  79. 1 1
      panda/src/x11display/x11GraphicsWindow.cxx
  80. 33 14
      pandatool/src/assimp/assimpLoader.cxx
  81. 3 3
      pandatool/src/assimp/assimpLoader.h
  82. 4 0
      pandatool/src/converter/CMakeLists.txt
  83. 148 0
      pandatool/src/converter/txoConverter.cxx
  84. 43 0
      pandatool/src/converter/txoConverter.h
  85. 85 25
      pandatool/src/deploy-stub/android_main.cxx
  86. 29 8
      pandatool/src/deploy-stub/android_support.cxx
  87. 6 0
      pandatool/src/deploy-stub/deploy-stub.c
  88. 2 2
      pandatool/src/mac-stats/macStatsStripChart.mm
  89. 0 24
      pandatool/src/xfile/windowsGuid.I
  90. 5 7
      pandatool/src/xfile/windowsGuid.h
  91. 28 0
      tests/display/test_color_buffer.py
  92. 5 0
      tests/main.c

+ 167 - 14
.github/workflows/ci.yml

@@ -42,7 +42,7 @@ jobs:
           double: NO
 
         #- profile: macos-coverage-unity-xcode
-        #  os: macOS-13
+        #  os: macOS-15
         #  config: Coverage
         #  unity: YES
         #  generator: Xcode
@@ -53,7 +53,7 @@ jobs:
         #  double: NO
 
         - profile: macos-nometa-standard-makefile
-          os: macOS-13
+          os: macOS-15
           config: Standard
           unity: NO
           generator: Unix Makefiles
@@ -88,7 +88,7 @@ jobs:
     runs-on: ${{ matrix.os }}
 
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
       with:
         fetch-depth: 10
 
@@ -110,7 +110,7 @@ jobs:
 
     - name: Set up XCode (macOS)
       if: runner.os == 'macOS'
-      run: sudo xcode-select -s /Applications/Xcode_14.3.1.app/Contents/Developer
+      run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer
 
     - name: Install dependencies (Ubuntu)
       if: startsWith(matrix.os, 'ubuntu')
@@ -127,7 +127,7 @@ jobs:
 
     - name: Cache dependencies (Windows)
       if: runner.os == 'Windows'
-      uses: actions/cache@v4
+      uses: actions/cache@v5
       with:
         path: thirdparty
         key: ci-cmake-${{ runner.OS }}-thirdparty-v1.10.15-r1
@@ -144,7 +144,7 @@ jobs:
 
     - name: ccache (non-Windows)
       if: runner.os != 'Windows'
-      uses: actions/cache@v4
+      uses: actions/cache@v5
       with:
         path: ccache
         key: ci-cmake-ccache-${{ matrix.profile }}
@@ -372,10 +372,10 @@ jobs:
     if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
     strategy:
       matrix:
-        os: [ubuntu-22.04, windows-2022, macOS-13]
+        os: [ubuntu-22.04, windows-2022, macOS-15]
     runs-on: ${{ matrix.os }}
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
     - name: Install dependencies (Ubuntu)
       if: matrix.os == 'ubuntu-22.04'
       run: |
@@ -386,9 +386,9 @@ jobs:
       shell: powershell
       run: |
         $wc = New-Object System.Net.WebClient
-        $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.15/panda3d-1.10.15-tools-win64.zip", "thirdparty-tools.zip")
+        $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.16/panda3d-1.10.16-tools-win64.zip", "thirdparty-tools.zip")
         Expand-Archive -Path thirdparty-tools.zip
-        Move-Item -Path thirdparty-tools/panda3d-1.10.15/thirdparty -Destination .
+        Move-Item -Path thirdparty-tools/panda3d-1.10.16/thirdparty -Destination .
     - name: Get thirdparty packages (macOS)
       if: runner.os == 'macOS'
       run: |
@@ -400,7 +400,7 @@ jobs:
 
     - name: Set up XCode (macOS)
       if: runner.os == 'macOS'
-      run: sudo xcode-select -s /Applications/Xcode_14.3.1.app/Contents/Developer
+      run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer
 
     - name: Set up Python 3.13
       uses: actions/setup-python@v6
@@ -498,7 +498,7 @@ jobs:
     if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
     runs-on: ubuntu-24.04
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
 
     - name: Install dependencies
       run: |
@@ -513,7 +513,7 @@ jobs:
 
     - name: Restore Python build cache
       id: cache-emscripten-python-restore
-      uses: actions/cache/restore@v4
+      uses: actions/cache/restore@v5
       with:
         path: ~/python
         key: cache-emscripten-python-3.12.8
@@ -533,7 +533,7 @@ jobs:
     - name: Save Python build cache
       id: cache-emscripten-python-save
       if: steps.cache-emscripten-python-restore.outputs.cache-hit != 'true'
-      uses: actions/cache/save@v4
+      uses: actions/cache/save@v5
       with:
         path: ~/python
         key: ${{ steps.cache-emscripten-python-restore.outputs.cache-primary-key }}
@@ -552,3 +552,156 @@ jobs:
       shell: bash
       run: |
         PYTHONHOME=~/python/usr/local PYTHONPATH=built node built/bin/run_tests.js tests
+
+  android:
+    if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
+
+    strategy:
+      fail-fast: false
+
+      matrix:
+        include:
+        - android-abi: arm64-v8a
+          android-arch: arm64
+          android-triple: aarch64-linux-android
+          ndk-version: 29.0.14206865
+          runner: ubuntu-latest
+
+        - android-abi: armeabi-v7a
+          android-arch: arm
+          android-triple: arm-linux-androideabi
+          ndk-version: 29.0.14206865
+          runner: ubuntu-latest
+
+        - android-abi: x86_64
+          android-arch: x86_64
+          android-triple: x86_64-linux-android
+          ndk-version: 29.0.14206865
+          runner: ubuntu-latest
+
+        - android-abi: x86
+          android-arch: x86
+          android-triple: i686-linux-android
+          ndk-version: 29.0.14206865
+          runner: ubuntu-latest
+
+    runs-on: ${{ matrix.runner }}
+    steps:
+    - uses: actions/checkout@v6
+
+    - name: Setup Android SDK
+      uses: android-actions/setup-android@v3
+      with:
+        packages: 'tools platform-tools platforms;android-21 ndk;${{ matrix.ndk-version }}'
+
+    - name: Set up Android NDK
+      run: |
+        echo "ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/${{ matrix.ndk-version }}" >> $GITHUB_ENV
+
+    - uses: actions/setup-python@v6
+      with:
+        python-version: '3.13'
+
+    - name: Restore Python build cache
+      id: cache-android-python-restore
+      uses: actions/cache/restore@v5
+      with:
+        path: ~/python-prefix
+        key: cache-android-python-3.13.9-${{ matrix.android-abi }}-ndk${{ matrix.ndk-version }}
+
+    - name: Build Python 3.13
+      if: steps.cache-android-python-restore.outputs.cache-hit != 'true'
+      run: |
+        wget https://www.python.org/ftp/python/3.13.9/Python-3.13.9.tar.xz
+        tar -xJf Python-3.13.9.tar.xz
+        sed -i.bak "s/aarch64-linux-android/${{ matrix.android-triple }}/" Python-3.13.9/Android/android.py
+        sed -i.bak "s/ndk_version=[0-9.]\{1,\}/ndk_version=${{ matrix.ndk-version }}/" Python-3.13.9/Android/android-env.sh
+        (cd Python-3.13.9/Android && python3 android.py build ${{ matrix.android-triple }})
+        cp -R Python-3.13.9/cross-build/${{ matrix.android-triple }}/prefix ~/python-prefix
+        rm -rf Python-3.13.9
+
+    - name: Save Python build cache
+      id: cache-android-python-save
+      if: steps.cache-android-python-restore.outputs.cache-hit != 'true'
+      uses: actions/cache/save@v5
+      with:
+        path: ~/python-prefix
+        key: ${{ steps.cache-android-python-restore.outputs.cache-primary-key }}
+
+    - name: Restore thirdparty build cache
+      id: cache-android-thirdparty-restore
+      uses: actions/cache/restore@v5
+      with:
+        path: ./thirdparty
+        key: cache-android-thirdparty-935c80380ca171e08587c570e9dad678d29db3c8-${{ matrix.android-abi }}-ndk${{ matrix.ndk-version }}
+
+    - name: Install yasm
+      if: steps.cache-android-thirdparty-restore.outputs.cache-hit != 'true'
+      run: ${{runner.os == 'macOS' && 'brew' || 'sudo apt-get'}} install yasm
+
+    - name: Build thirdparty packages
+      if: steps.cache-android-thirdparty-restore.outputs.cache-hit != 'true'
+      run: |
+        git clone --revision=935c80380ca171e08587c570e9dad678d29db3c8 --depth=1 https://github.com/rdb/panda3d-thirdparty.git thirdparty
+        cd thirdparty
+        cmake -B build \
+          -DCMAKE_BUILD_TYPE=Release \
+          -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_ROOT/build/cmake/android.toolchain.cmake \
+          -DCMAKE_ANDROID_ARCH_ABI=${{ matrix.android-abi }} \
+          -DCMAKE_ANDROID_ARCH=${{ matrix.android-arch }} \
+          -DCMAKE_SYSTEM_VERSION=21 \
+          -DANDROID_ABI=${{ matrix.android-abi }} \
+          -DANDROID_PLATFORM=android-21 \
+          -DBUILD_FCOLLADA=OFF \
+          -DBUILD_OPENSSL=OFF \
+          -DBUILD_VRPN=OFF \
+          -DBUILD_ARTOOLKIT=OFF
+        cmake --build build --config Release -j4
+        rm -rf build
+
+    - name: Save thirdparty build cache
+      id: cache-android-thirdparty-save
+      if: steps.cache-android-thirdparty-restore.outputs.cache-hit != 'true'
+      uses: actions/cache/save@v5
+      with:
+        path: ./thirdparty
+        key: ${{ steps.cache-android-thirdparty-restore.outputs.cache-primary-key }}
+
+    - name: Build for Android
+      shell: bash
+      run: |
+        python makepanda/makepanda.py --git-commit=${{github.sha}} --target android-21 --python-incdir=~/python-prefix/include --python-libdir=~/python-prefix/lib --arch ${{ matrix.android-arch }} --everything --no-openssl --no-tinydisplay --no-pandatool --verbose --threads=4
+
+    - name: Install test dependencies
+      shell: bash
+      run: |
+        python -m pip install -t ~/python-prefix/lib/python3.13/site-packages -r requirements-test.txt
+
+    - name: Enable KVM group perms
+      if: startsWith(matrix.runner, 'ubuntu')
+      run: |
+          echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+          sudo udevadm control --reload-rules
+          sudo udevadm trigger --name-match=kvm
+
+    - name: Setup Android Emulator and Run Tests
+      if: startsWith(matrix.android-abi, 'x86')
+      uses: reactivecircus/android-emulator-runner@v2
+      with:
+        api-level: 27
+        arch: ${{ matrix.android-abi }}
+        target: default
+        profile: pixel_6
+        script: |
+          adb shell "rm -rf /data/local/tmp/panda3d-tests"
+          adb push built/bin/run_tests /data/local/tmp/panda3d-tests/run_tests
+          adb shell "mkdir /data/local/tmp/panda3d-tests/lib"
+          adb push built/lib/*.so /data/local/tmp/panda3d-tests/lib
+          adb push ~/python-prefix/lib/*.so /data/local/tmp/panda3d-tests/lib
+          adb push ~/python-prefix/lib/python3.13 /data/local/tmp/panda3d-tests/lib/python3.13
+          adb push built/panda3d /data/local/tmp/panda3d-tests/lib/python3.13/site-packages/
+          adb push built/direct /data/local/tmp/panda3d-tests/lib/python3.13/site-packages/
+          adb push built/models /data/local/tmp/panda3d-tests/models
+          adb push built/etc /data/local/tmp/panda3d-tests/etc
+          adb push tests /data/local/tmp/panda3d-tests/tests
+          adb shell "cd /data/local/tmp/panda3d-tests && LD_LIBRARY_PATH=/data/local/tmp/panda3d-tests/lib PYTHONPATH=/data/local/tmp/panda3d-tests/lib/python3.13/site-packages ./run_tests -v tests"

+ 1 - 1
.github/workflows/mypy.yml

@@ -10,7 +10,7 @@ jobs:
         python-version: ['3.9', '3.13']
       fail-fast: false
     steps:
-      - uses: actions/checkout@v5
+      - uses: actions/checkout@v6
       - name: Set up Python ${{ matrix.python-version }}
         uses: actions/setup-python@v6
         with:

+ 2 - 0
BACKERS.md

@@ -5,10 +5,12 @@ This is a list of all the people who are contributing financially to Panda3D.  I
 ## Bronze Sponsors
 
 [<img src="https://www.panda3d.org/wp-content/uploads/2024/08/Route4MeLogo1185x300-2-1-1024x259.png" alt="Route4Me" height="48">](https://route4me.com/)
+[<img src="https://www.lambdatest.com/blue-logo.png" alt="LambdaTest" height="48">](https://www.lambdatest.com/?utm_source=panda3d&utm_medium=sponsor)
 
 * [Daniel Stokes](https://opencollective.com/daniel-stokes)
 * [David Rose](https://opencollective.com/david-rose)
 * [Route4Me](https://route4me.com/)
+* [LambdaTest](https://www.lambdatest.com/?utm_source=panda3d&utm_medium=sponsor)
 
 ## Benefactors
 

+ 8 - 5
README.md

@@ -64,8 +64,8 @@ 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.15/panda3d-1.10.15-tools-win64.zip
-- https://www.panda3d.org/download/panda3d-1.10.15/panda3d-1.10.15-tools-win32.zip
+- https://www.panda3d.org/download/panda3d-1.10.16/panda3d-1.10.16-tools-win64.zip
+- https://www.panda3d.org/download/panda3d-1.10.16/panda3d-1.10.16-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
@@ -238,9 +238,12 @@ If you would like to support the project financially, visit
 [our campaign on OpenCollective](https://opencollective.com/panda3d).  Your
 contributions help us accelerate the development of Panda3D.
 
-For the list of backers, see the [BACKERS.md](BACKERS.md) file or visit the
-[Sponsors page](https://www.panda3d.org/sponsors) on our web site.  Thank you
-to everyone who has donated!
+For the complete list of backers, see the [BACKERS.md](BACKERS.md) file or
+visit the [Sponsors page](https://www.panda3d.org/sponsors) on our web site.
+Thank you to everyone who has donated!
+
+[<img src="https://www.panda3d.org/wp-content/uploads/2024/08/Route4MeLogo1185x300-2-1-1024x259.png" alt="Route4Me" height="48">](https://route4me.com/)
+[<img src="https://www.lambdatest.com/blue-logo.png" alt="LambdaTest" height="48">](https://www.lambdatest.com/?utm_source=panda3d&utm_medium=sponsor)
 
 <a href="https://opencollective.com/panda3d" target="_blank">
   <img src="https://opencollective.com/panda3d/contribute/[email protected]?color=blue" width=300 />

+ 23 - 14
direct/src/directnotify/DirectNotify.py

@@ -42,25 +42,25 @@ class DirectNotify:
         """
         return list(self.__categories.keys())
 
-    def getCategory(self, categoryName: str) -> Notifier.Notifier | None:
+    def getCategory(self, name: str) -> Notifier.Notifier | None:
         """getCategory(self, string)
         Return the category with given name if present, None otherwise
         """
-        return self.__categories.get(categoryName, None)
+        return self.__categories.get(name, None)
 
-    def newCategory(self, categoryName: str, logger: Logger.Logger | None = None) -> Notifier.Notifier:
+    def newCategory(self, name: str, logger: Logger.Logger | None = None) -> Notifier.Notifier:
         """newCategory(self, string)
-        Make a new notify category named categoryName. Return new category
+        Make a new notify category named name. Return new category
         if no such category exists, else return existing category
         """
-        if categoryName not in self.__categories:
-            self.__categories[categoryName] = Notifier.Notifier(categoryName, logger)
-            self.setDconfigLevel(categoryName)
-        notifier = self.getCategory(categoryName)
+        if name not in self.__categories:
+            self.__categories[name] = Notifier.Notifier(name, logger)
+            self.setDconfigLevel(name)
+        notifier = self.getCategory(name)
         assert notifier is not None
         return notifier
 
-    def setDconfigLevel(self, categoryName: str) -> None:
+    def setDconfigLevel(self, name: str) -> None:
         """
         Check to see if this category has a dconfig variable
         to set the notify severity and then set that level. You cannot
@@ -71,7 +71,7 @@ class DirectNotify:
         # we're running before ShowBase has finished initializing
         from panda3d.core import ConfigVariableString
 
-        dconfigParam = ("notify-level-" + categoryName)
+        dconfigParam = ("notify-level-" + name)
         cvar = ConfigVariableString(dconfigParam, "")
         level = cvar.getValue()
 
@@ -82,11 +82,11 @@ class DirectNotify:
         if not level:
             level = 'error'
 
-        category = self.getCategory(categoryName)
-        assert category is not None, f'failed to find category: {categoryName!r}'
+        category = self.getCategory(name)
+        assert category is not None, f'failed to find category: {name!r}'
         # Note - this print statement is making it difficult to
         # achieve "no output unless there's an error" operation - Josh
-        # print ("Setting DirectNotify category: " + categoryName +
+        # print ("Setting DirectNotify category: " + name +
         #        " to severity: " + level)
         if level == "error":
             category.setWarning(False)
@@ -106,7 +106,7 @@ class DirectNotify:
             category.setDebug(True)
         else:
             print("DirectNotify: unknown notify level: " + str(level)
-                   + " for category: " + str(categoryName))
+                   + " for category: " + str(name))
 
     def setDconfigLevels(self) -> None:
         for categoryName in self.getCategories():
@@ -129,3 +129,12 @@ class DirectNotify:
 
     def giveNotify(self, cls) -> None:
         cls.notify = self.newCategory(cls.__name__)
+
+    get_categories = getCategories
+    get_category = getCategory
+    new_category = newCategory
+    set_dconfig_level = setDconfigLevel
+    set_dconfig_levels = setDconfigLevels
+    set_verbose = setVerbose
+    popup_controls = popupControls
+    give_notify = giveNotify

+ 61 - 28
direct/src/dist/FreezeTool.py

@@ -102,6 +102,7 @@ defaultHiddenImports = {
     'scipy.stats._stats': ['scipy.special.cython_special'],
     'setuptools.monkey': ['setuptools.msvc'],
     'shapely._geometry_helpers': ['shapely._geos'],
+    'jnius': ['jnius_config'],
 }
 
 
@@ -114,7 +115,7 @@ ignoreImports = {
     'toml.encoder': ['numpy'],
     'py._builtin': ['__builtin__'],
 
-    'site': ['android_log'],
+    'site': ['android_support'],
 }
 
 if sys.version_info >= (3, 8):
@@ -139,12 +140,23 @@ def getline(filename, lineno, module_globals=None):
     return ''
 
 def clearcache():
-    global cache
-    cache = {}
+    cache.clear()
 
 def getlines(filename, module_globals=None):
     return []
 
+def _getline_from_code(filename, lineno):
+    return ''
+
+def _make_key(code):
+    return (code.co_filename, code.co_qualname, code.co_firstlineno)
+
+def _getlines_from_code(code):
+    return []
+
+def _source_unavailable(filename):
+    return True
+
 def checkcache(filename=None):
     pass
 
@@ -1237,8 +1249,12 @@ class Freezer:
                 pass
 
         # Check if any new modules we found have "hidden" imports
-        for origName in list(self.mf.modules.keys()):
+        checkHiddenImports = set(self.mf.modules.keys())
+        while checkHiddenImports:
+            origName = next(iter(checkHiddenImports))
+            checkHiddenImports.discard(origName)
             hidden = self.hiddenImports.get(origName, [])
+            preModules = frozenset(self.mf.modules.keys())
             for modname in hidden:
                 if modname.endswith('.*'):
                     mdefs = self._gatherSubmodules(modname, implicit = True)
@@ -1252,6 +1268,9 @@ class Freezer:
                         self.__loadModule(self.ModuleDef(modname, implicit = True))
                     except ImportError:
                         pass
+            addedModules = set(self.mf.modules.keys())
+            addedModules -= preModules
+            checkHiddenImports |= addedModules
 
         # Special case for sysconfig, which depends on a platform-specific
         # sysconfigdata module on POSIX systems.
@@ -1835,7 +1854,8 @@ class Freezer:
         return target
 
     def generateRuntimeFromStub(self, target, stub_file, use_console, fields={},
-                                log_append=False, log_filename_strftime=False):
+                                log_append=False, log_filename_strftime=False,
+                                blob_path=None):
         self.__replacePaths()
 
         # We must have a __main__ module to make an exe file.
@@ -1970,6 +1990,9 @@ class Freezer:
         elif self.platform.endswith('_aarch64') or self.platform.endswith('_arm64'):
             # Most arm64 operating systems are configured with 16 KiB pages.
             blob_align = 16384
+        elif self.platform.replace('-', '_') == 'android_x86_64':
+            # Android nowadays requires 16 KiB pages on 64-bit Intel as well.
+            blob_align = 16384
         else:
             # Align to page size, so that it can be mmapped.
             blob_align = 4096
@@ -1980,29 +2003,33 @@ class Freezer:
             pad = (blob_align - (blob_size & (blob_align - 1)))
             blob_size += pad
 
-        # TODO: Support creating custom sections in universal binaries.
-        append_blob = True
-        if self.platform.startswith('macosx') and len(bitnesses) == 1:
-            # If our deploy-stub has a __PANDA segment, we know we're meant to
-            # put our blob there rather than attach it to the end.
-            load_commands = self._parse_macho_load_commands(stub_data)
-            if b'__PANDA' in load_commands.keys():
-                append_blob = False
-
-        if self.platform.startswith("macosx") and not append_blob:
-            # Take this time to shift any Mach-O structures around to fit our
-            # blob. We don't need to worry about aligning the offset since the
-            # compiler already took care of that when creating the segment.
-            blob_offset = self._shift_macho_structures(stub_data, load_commands, blob_size)
+        if blob_path is not None:
+            # We'll be writing the blob to a separate location.
+            blob_offset = 0
         else:
-            # Add padding before the blob if necessary.
-            blob_offset = len(stub_data)
-            if (blob_offset & (blob_align - 1)) != 0:
-                pad = (blob_align - (blob_offset & (blob_align - 1)))
-                stub_data += (b'\0' * pad)
-                blob_offset += pad
-            assert (blob_offset % blob_align) == 0
-            assert blob_offset == len(stub_data)
+            # TODO: Support creating custom sections in universal binaries.
+            append_blob = True
+            if self.platform.startswith('macosx') and len(bitnesses) == 1:
+                # If our deploy-stub has a __PANDA segment, we know we're meant to
+                # put our blob there rather than attach it to the end.
+                load_commands = self._parse_macho_load_commands(stub_data)
+                if b'__PANDA' in load_commands.keys():
+                    append_blob = False
+
+            if self.platform.startswith("macosx") and not append_blob:
+                # Take this time to shift any Mach-O structures around to fit our
+                # blob. We don't need to worry about aligning the offset since the
+                # compiler already took care of that when creating the segment.
+                blob_offset = self._shift_macho_structures(stub_data, load_commands, blob_size)
+            else:
+                # Add padding before the blob if necessary.
+                blob_offset = len(stub_data)
+                if (blob_offset & (blob_align - 1)) != 0:
+                    pad = (blob_align - (blob_offset & (blob_align - 1)))
+                    stub_data += (b'\0' * pad)
+                    blob_offset += pad
+                assert (blob_offset % blob_align) == 0
+                assert blob_offset == len(stub_data)
 
         # Calculate the offsets for the variables.  These are pointers,
         # relative to the beginning of the blob.
@@ -2088,7 +2115,9 @@ class Freezer:
             blob += struct.pack('<Q', blob_offset)
 
         with open(target, 'wb') as f:
-            if append_blob:
+            if blob_path is not None:
+                f.write(stub_data)
+            elif append_blob:
                 f.write(stub_data)
                 assert f.tell() == blob_offset
                 f.write(blob)
@@ -2096,6 +2125,10 @@ class Freezer:
                 stub_data[blob_offset:blob_offset + blob_size] = blob
                 f.write(stub_data)
 
+        if blob_path is not None:
+            with open(blob_path, 'wb') as f:
+                f.write(blob)
+
         os.chmod(target, 0o755)
         return target
 

+ 14 - 1
direct/src/dist/_android.py

@@ -3,7 +3,7 @@
 import xml.etree.ElementTree as ET
 
 from ._proto.targeting_pb2 import Abi
-from ._proto.config_pb2 import BundleConfig # pylint: disable=unused-import
+from ._proto.config_pb2 import BundleConfig, UncompressNativeLibraries # pylint: disable=unused-import
 from ._proto.files_pb2 import NativeLibraries # pylint: disable=unused-import
 from ._proto.Resources_pb2 import ResourceTable # pylint: disable=unused-import
 from ._proto.Resources_pb2 import XmlNode
@@ -174,6 +174,7 @@ ANDROID_ATTRIBUTES = {
     'debuggable': bool_resource(0x0101000f),
     'documentLaunchMode': enum_resource(0x1010445, "none", "intoExisting", "always", "never"),
     'enabled': bool_resource(0x101000e),
+    'enableOnBackInvokedCallback': bool_resource(0x0101066c),
     'excludeFromRecents': bool_resource(0x1010017),
     'exported': bool_resource(0x1010010),
     'extractNativeLibs': bool_resource(0x10104ea),
@@ -195,8 +196,10 @@ ANDROID_ATTRIBUTES = {
     'multiprocess': bool_resource(0x1010013),
     'name': str_resource(0x1010003),
     'noHistory': bool_resource(0x101022d),
+    'pageSizeCompat': bool_resource(0x010106ab),
     'pathPattern': str_resource(0x101002c),
     'preferMinimalPostProcessing': bool_resource(0x101060c),
+    'resource': ref_resource(0x01010025),
     'required': bool_resource(0x101028e),
     'resizeableActivity': bool_resource(0x10104f6),
     'scheme': str_resource(0x1010027),
@@ -220,6 +223,7 @@ class AndroidManifest:
         self.root = XmlNode()
         self.resource_types = []
         self.resources = {}
+        self.extract_native_libs = None
 
     def parse_xml(self, data):
         parser = ET.XMLParser(target=self)
@@ -240,6 +244,15 @@ class AndroidManifest:
         element = node.element
         element.name = tag
 
+        if tag == 'application':
+            value = attribs.get('{http://schemas.android.com/apk/res/android}extractNativeLibs')
+            if value == 'false':
+                self.extract_native_libs = False
+            elif value == 'true':
+                self.extract_native_libs = True
+            else:
+                print(f'Warning: invalid value for android:extractNativeLibs: {value}')
+
         self._stack.append(element)
 
         for key, value in attribs.items():

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


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


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


+ 33 - 292
direct/src/dist/_proto/files_pb2.py

@@ -1,11 +1,22 @@
 # -*- coding: utf-8 -*-
 # Generated by the protocol buffer compiler.  DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
 # source: files.proto
+# Protobuf Python Version: 6.33.0
 """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 descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
 from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+    _runtime_version.Domain.PUBLIC,
+    6,
+    33,
+    0,
+    '',
+    'files.proto'
+)
 # @@protoc_insertion_point(imports)
 
 _sym_db = _symbol_database.Default()
@@ -14,294 +25,24 @@ _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
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(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')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'files_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+  _globals['DESCRIPTOR']._loaded_options = None
+  _globals['DESCRIPTOR']._serialized_options = b'\n\022com.android.bundle'
+  _globals['_ASSETS']._serialized_start=48
+  _globals['_ASSETS']._serialized_end=116
+  _globals['_NATIVELIBRARIES']._serialized_start=118
+  _globals['_NATIVELIBRARIES']._serialized_end=195
+  _globals['_APEXIMAGES']._serialized_start=197
+  _globals['_APEXIMAGES']._serialized_end=265
+  _globals['_TARGETEDASSETSDIRECTORY']._serialized_start=267
+  _globals['_TARGETEDASSETSDIRECTORY']._serialized_end=367
+  _globals['_TARGETEDNATIVEDIRECTORY']._serialized_start=369
+  _globals['_TARGETEDNATIVEDIRECTORY']._serialized_end=469
+  _globals['_TARGETEDAPEXIMAGE']._serialized_start=471
+  _globals['_TARGETEDAPEXIMAGE']._serialized_end=584
 # @@protoc_insertion_point(module_scope)

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


+ 111 - 27
direct/src/dist/commands.py

@@ -79,6 +79,10 @@ def _model_to_bam(_build_cmd, srcpath, dstpath):
 
     _register_python_loaders()
 
+    if src_fn.get_extension() == 'egg' and zipfile.is_zipfile(src_fn.to_os_specific()):
+        _build_cmd.warn('Skipping %s as it appears to be a Python .egg archive, was this included by mistake?' % (src_fn.to_os_specific()))
+        return
+
     loader = p3d.Loader.get_global_ptr()
     options = p3d.LoaderOptions(p3d.LoaderOptions.LF_report_errors |
                                 p3d.LoaderOptions.LF_no_ram_cache)
@@ -192,14 +196,14 @@ SITE_PY_ANDROID = """
 # module.
 import sys, os
 from importlib import _bootstrap, _bootstrap_external
+from android_support import log_write as android_log_write
+from android_support import find_library
 
 class AndroidExtensionFinder:
     @classmethod
     def find_spec(cls, fullname, path=None, target=None):
-        soname = 'libpy.' + fullname + '.so'
-        path = os.path.join(sys.platlibdir, soname)
-
-        if os.path.exists(path):
+        path = find_library('py.' + fullname)
+        if path:
             loader = _bootstrap_external.ExtensionFileLoader(fullname, path)
             return _bootstrap.ModuleSpec(fullname, loader, origin=path)
 
@@ -210,8 +214,6 @@ sys.meta_path.append(AndroidExtensionFinder)
 from _frozen_importlib import _imp, FrozenImporter
 from io import RawIOBase, TextIOWrapper
 
-from android_log import write as android_log_write
-
 
 sys.frozen = True
 
@@ -300,7 +302,8 @@ class build_apps(setuptools.Command):
         self.android_version_code = 1
         self.android_min_sdk_version = 21
         self.android_max_sdk_version = None
-        self.android_target_sdk_version = 30
+        self.android_target_sdk_version = 35
+        self.android_manifest_file = None
         self.gui_apps = {}
         self.console_apps = {}
         self.macos_main_app = None
@@ -316,7 +319,10 @@ class build_apps(setuptools.Command):
             'win_amd64',
         ]
 
-        if sys.version_info >= (3, 13):
+        if sys.version_info >= (3, 14):
+            # This version of Python is only available for 10.15+.
+            self.platforms[1] = 'macosx_10_15_x86_64'
+        elif sys.version_info >= (3, 13):
             # This version of Python is only available for 10.13+.
             self.platforms[1] = 'macosx_10_13_x86_64'
 
@@ -586,6 +592,10 @@ class build_apps(setuptools.Command):
                 data_dir = os.path.join(build_dir, 'assets')
                 os.makedirs(data_dir, exist_ok=True)
 
+                res_dir = os.path.join(build_dir, 'res')
+                res_raw_dir = os.path.join(res_dir, 'raw')
+                os.makedirs(res_raw_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)
@@ -602,7 +612,7 @@ class build_apps(setuptools.Command):
 
                     # We end up copying the data multiple times to the same
                     # directory, but that's probably fine for now.
-                    self.build_binaries(platform + suffix, lib_dir, data_dir)
+                    self.build_binaries(platform + suffix, lib_dir, data_dir, res_raw_dir)
 
                 # Write out the icons to the res directory.
                 for appname, icon in self.icon_objects.items():
@@ -610,9 +620,9 @@ class build_apps(setuptools.Command):
                         # Conventional name for icon on Android.
                         basename = 'ic_launcher.png'
                     else:
-                        basename = f'ic_{appname}.png'
+                        appname_sane = appname.replace(' ', '_')
+                        basename = f'ic_{appname_sane}.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))
@@ -623,8 +633,17 @@ class build_apps(setuptools.Command):
 
                 self.build_assets(platform, data_dir)
 
-                # Generate an AndroidManifest.xml
-                self.generate_android_manifest(os.path.join(build_dir, 'AndroidManifest.xml'))
+                # Generate an AndroidManifest.xml if none was provided
+                manifest_path = os.path.join(build_dir, 'AndroidManifest.xml')
+                if self.android_manifest_file:
+                    try:
+                        self.check_android_manifest(self.android_manifest_file)
+                    except Exception as e:
+                        self.announce(f"Failed to use provided manifest file from {self.android_manifest_file}", distutils.log.FATAL)
+                        raise
+                    self.copy(self.android_manifest_file, manifest_path)
+                else:
+                    self.generate_android_manifest(manifest_path)
             else:
                 self.build_binaries(platform, build_dir, build_dir)
                 self.build_assets(platform, build_dir)
@@ -691,8 +710,14 @@ class build_apps(setuptools.Command):
             subprocess.check_call([sys.executable, '-m', 'pip'] + pip_args)
         except:
             # Display a more helpful message for these common issues.
-            if platform.startswith('macosx_10_9_') and sys.version_info >= (3, 13):
-                new_platform = platform.replace('macosx_10_9_', 'macosx_10_13_')
+            if platform.startswith('macosx_10_13_') and sys.version_info >= (3, 14):
+                new_platform = platform.replace('macosx_10_13_', 'macosx_10_15_')
+                self.announce('This error likely occurs because {} is not a supported target as of Python 3.14.\nChange the target platform to {} instead.'.format(platform, new_platform), distutils.log.ERROR)
+            elif platform.startswith('macosx_10_9_') and sys.version_info >= (3, 13):
+                if sys.version_info >= (3, 14):
+                    new_platform = platform.replace('macosx_10_9_', 'macosx_10_15_')
+                else:
+                    new_platform = platform.replace('macosx_10_9_', 'macosx_10_13_')
                 self.announce('This error likely occurs because {} is not a supported target as of Python 3.13.\nChange the target platform to {} instead.'.format(platform, new_platform), distutils.log.ERROR)
             elif platform.startswith('manylinux2010_') and sys.version_info >= (3, 11):
                 new_platform = platform.replace('manylinux2010_', 'manylinux2014_')
@@ -701,7 +726,9 @@ class build_apps(setuptools.Command):
                 new_platform = platform.replace('manylinux1_', 'manylinux2014_')
                 self.announce('This error likely occurs because {} is not a supported target as of Python 3.10.\nChange the target platform to {} instead.'.format(platform, new_platform), distutils.log.ERROR)
             elif platform.startswith('macosx_10_6_') and sys.version_info >= (3, 8):
-                if sys.version_info >= (3, 13):
+                if sys.version_info >= (3, 14):
+                    new_platform = platform.replace('macosx_10_6_', 'macosx_10_15_')
+                elif sys.version_info >= (3, 13):
                     new_platform = platform.replace('macosx_10_6_', 'macosx_10_13_')
                 else:
                     new_platform = platform.replace('macosx_10_6_', 'macosx_10_9_')
@@ -855,7 +882,7 @@ class build_apps(setuptools.Command):
         if category:
             application.set('android:appCategory', category)
         application.set('android:debuggable', ('false', 'true')[self.android_debuggable])
-        application.set('android:extractNativeLibs', 'true')
+        application.set('android:extractNativeLibs', 'false')
         application.set('android:hardwareAccelerated', 'true')
 
         app_icon = self.icon_objects.get('*', self.icon_objects.get(self.macos_main_app))
@@ -863,6 +890,8 @@ class build_apps(setuptools.Command):
             application.set('android:icon', '@mipmap/ic_launcher')
 
         for appname in self.gui_apps:
+            appname_sane = appname.replace(' ', '_')
+
             activity = ET.SubElement(application, 'activity')
             activity.set('android:name', 'org.panda3d.android.PythonActivity')
             activity.set('android:label', appname)
@@ -871,14 +900,19 @@ class build_apps(setuptools.Command):
             activity.set('android:configChanges', 'layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation')
             activity.set('android:launchMode', 'singleInstance')
             activity.set('android:preferMinimalPostProcessing', 'true')
+            activity.set('android:exported', 'true')
 
             act_icon = self.icon_objects.get(appname)
             if act_icon and act_icon is not app_icon:
-                activity.set('android:icon', '@mipmap/ic_' + appname)
+                activity.set('android:icon', '@mipmap/ic_' + appname_sane)
 
             meta_data = ET.SubElement(activity, 'meta-data')
             meta_data.set('android:name', 'android.app.lib_name')
-            meta_data.set('android:value', appname)
+            meta_data.set('android:value', appname_sane)
+
+            meta_data = ET.SubElement(activity, 'meta-data')
+            meta_data.set('android:name', 'org.panda3d.android.BLOB_RESOURCE')
+            meta_data.set('android:resource', '@raw/' + appname_sane + '.so')
 
             intent_filter = ET.SubElement(activity, 'intent-filter')
             ET.SubElement(intent_filter, 'action').set('android:name', 'android.intent.action.MAIN')
@@ -886,10 +920,44 @@ class build_apps(setuptools.Command):
             ET.SubElement(intent_filter, 'category').set('android:name', 'android.intent.category.LEANBACK_LAUNCHER')
 
         tree = ET.ElementTree(manifest)
+        if sys.version_info >= (3, 9):
+            ET.indent(tree)
         with open(path, 'wb') as fh:
             tree.write(fh, encoding='utf-8', xml_declaration=True)
 
-    def build_binaries(self, platform, binary_dir, data_dir=None):
+    def check_android_manifest(self, path):
+        """ Checks that the user-provided manifest file seems OK. """
+
+        # This function doesn't aim to check everything as it's the user's
+        # responsibility, just a basic sanity check, but if we change anything
+        # in our own generation logic then it would be good to check those
+        # things here and warn if anything needs to be updated.
+
+        import xml.etree.ElementTree as ET
+
+        android = '{http://schemas.android.com/apk/res/android}'
+
+        tree = ET.parse(path)
+        root = tree.getroot()
+        if root.tag != 'manifest':
+            raise RuntimeError(f"Expected <manifest> in {path}")
+
+        if root.attrib['package'] != self.application_id:
+            raise RuntimeError(f"<manifest> package attribute does not match given application_id {self.application_id}")
+
+        apps = root.findall('application')
+        if len(apps) != 1:
+            raise RuntimeError("<manifest> must contain exactly one <application>")
+
+        application = apps[0]
+        for activity in application.iter('activity'):
+            if f'{android}name' not in activity.attrib:
+                raise RuntimeError("<activity> element must have android:name attribute")
+
+            if self.android_target_sdk_version >= 31 and f'{android}exported' not in activity.attrib:
+                raise RuntimeError("<activity> element must have android:exported attribute when targeting Android API 31+")
+
+    def build_binaries(self, platform, binary_dir, data_dir=None, blob_dir=None):
         """ Builds the binary data for the given platform. """
 
         use_wheels = True
@@ -1085,13 +1153,26 @@ class build_apps(setuptools.Command):
 
             stub_name = 'deploy-stub'
             target_name = appname
+            appname_sane = 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'
+                    appname_sane = appname.replace(' ', '_')
+                    target_name = 'lib' + appname_sane + '.so'
+
+                if use_wheels:
+                    dexfile = os.path.join(binary_dir, '..', '..', 'classes.dex')
+                    self.copy(os.path.join(p3dwhlfn, 'deploy_libs', 'classes.dex'), dexfile)
+
+                    # Can this wheel load the blob as a raw resource?
+                    with open(dexfile, 'rb') as fh:
+                        supports_blob_resource = b'org.panda3d.android.BLOB_RESOURCE' in fh.read()
+
+                    assert supports_blob_resource, \
+                        "Please use a newer Panda3D wheel to build for Android using this version of build_apps"
 
             if platform.startswith('win'):
                 stub_name += '.exe'
@@ -1123,6 +1204,14 @@ class build_apps(setuptools.Command):
             if not self.log_filename or '%' not in self.log_filename:
                 use_strftime = False
 
+            blob_path = None
+            if blob_dir is not None:
+                if platform.startswith('android'):
+                    # Not really a .so file, but it forces bundletool to align it
+                    blob_path = os.path.join(blob_dir, appname_sane + '.so')
+                else:
+                    blob_path = os.path.join(blob_dir, appname_sane)
+
             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,
@@ -1136,7 +1225,7 @@ class build_apps(setuptools.Command):
                 'prc_executable_args_envvar': None,
                 'main_dir': None,
                 'log_filename': self.expand_path(self.log_filename, platform),
-            }, self.log_append, use_strftime)
+            }, self.log_append, use_strftime, blob_path)
             stub_file.close()
 
             if temp_file:
@@ -1251,11 +1340,6 @@ class build_apps(setuptools.Command):
             search_path = get_search_path_for(source_path)
             self.copy_with_dependencies(source_path, target_path, search_path)
 
-        # 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.
         if data_dir is None:
             return

+ 41 - 11
direct/src/dist/installers.py

@@ -207,7 +207,7 @@ def create_aab(command, basename, build_dir):
     and use it to convert an .aab into an .apk.
     """
 
-    from ._android import AndroidManifest, AbiAlias, BundleConfig, NativeLibraries, ResourceTable
+    from ._android import AndroidManifest, AbiAlias, BundleConfig, NativeLibraries, ResourceTable, UncompressNativeLibraries
 
     bundle_fn = p3d.Filename.from_os_specific(command.dist_dir) / (basename + '.aab')
     build_dir_fn = p3d.Filename.from_os_specific(build_dir)
@@ -230,7 +230,13 @@ def create_aab(command, basename, build_dir):
     config = BundleConfig()
     config.bundletool.version = '1.1.0'
     config.optimizations.splits_config.Clear()
-    config.optimizations.uncompress_native_libraries.enabled = False
+    if axml.extract_native_libs:
+        config.optimizations.uncompress_native_libraries.enabled = False
+    else:
+        config.optimizations.uncompress_native_libraries.enabled = True
+        config.optimizations.uncompress_native_libraries.alignment = \
+            UncompressNativeLibraries.PageAlignment.PAGE_ALIGNMENT_16K
+    config.compression.uncompressed_glob.append('res/raw/**')
     bundle.add_subfile('BundleConfig.pb', p3d.StringStream(config.SerializeToString()), 9)
 
     resources = ResourceTable()
@@ -251,13 +257,25 @@ def create_aab(command, basename, build_dir):
             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
+            if type_name == 'raw':
+                path = f'res/raw/{res_name}'
+                if not (build_dir_fn / path).exists():
+                    command.announce(
+                        f'\tRaw resource {path} was not found on disk', distutils.log.ERROR)
+                    return
+
+                # These are aligned to page size for mmap.
+                bundle.add_subfile('base/' + path, build_dir_fn / path, 0, 16384)
+                config_value = entry.config_value.add()
+                config_value.value.item.file.path = path
+            else:
+                for density, tag in (120, 'ldpi'), (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)
 
@@ -273,13 +291,25 @@ def create_aab(command, basename, build_dir):
     # Add the classes.dex.
     bundle.add_subfile('base/dex/classes.dex', build_dir_fn / 'classes.dex', 9)
 
-    # Add libraries, compressed.
+    # Add libraries, compressed, unless extractNativeLibs is false, in which
+    # case they have to be aligned to page boundaries (16 KiB on 64-bit).
+    lib_compress = 9 if axml.extract_native_libs else 0
+
     for abi in os.listdir(os.path.join(build_dir, 'lib')):
         abi_dir = os.path.join(build_dir, 'lib', abi)
 
+        if axml.extract_native_libs:
+            lib_align = 0
+        elif '64' in abi:
+            lib_align = 16384
+        else:
+            lib_align = 4096
+
         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)
+                bundle.add_subfile(f'base/lib/{abi}/{lib}',
+                                   build_dir_fn / 'lib' / abi / lib,
+                                   lib_compress, lib_align)
 
     # Add assets, compressed.
     assets_dir = os.path.join(build_dir, 'assets')

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

@@ -1,4 +1,4 @@
-from panda3d.core import DocumentSpec, Filename, HTTPClient, VirtualFileSystem, getModelPath
+from panda3d.core import DocumentSpec, Filename, VirtualFileSystem, getModelPath
 from panda3d.direct import CConnectionRepository, DCPacker
 from direct.task import Task
 from direct.task.TaskManagerGlobal import taskMgr
@@ -590,6 +590,7 @@ class ConnectionRepository(
 
         if self.http is None:
             try:
+                from panda3d.core import HTTPClient
                 self.http = HTTPClient()
             except Exception:
                 pass

+ 2 - 2
direct/src/distributed/direct.dc

@@ -84,8 +84,8 @@ dclass DistributedSmoothNode: DistributedNode {
 
   suggestResync(uint32 avId, int16 timestampA, int16 timestampB,
                 int32 serverTimeSec, uint16 serverTimeUSec,
-                uint16 / 100 uncertainty);
+                int16 / 100 uncertainty);
   returnResync(uint32 avId, int16 timestampB,
                int32 serverTimeSec, uint16 serverTimeUSec,
-               uint16 / 100 uncertainty);
+               int16 / 100 uncertainty);
 }; 

+ 21 - 6
direct/src/gui/DirectButton.py

@@ -44,6 +44,7 @@ class DirectButton(DirectFrame):
             # Sounds to be used for button events
             ('rolloverSound', DGG.getDefaultRolloverSound(), self.setRolloverSound),
             ('clickSound',    DGG.getDefaultClickSound(),    self.setClickSound),
+            ('releaseSound',  DGG.getDefaultReleaseSound(),  self.setReleaseSound),
             # Can only be specified at time of widget contruction
             # Do the text/graphics appear to move when the button is clicked
             ('pressEffect',     1,         DGG.INITOPT),
@@ -108,6 +109,13 @@ class DirectButton(DirectFrame):
             # Pass any extra args to command
             self['command'](*self['extraArgs'])
 
+    def setRolloverSound(self):
+        rolloverSound = self['rolloverSound']
+        if rolloverSound:
+            self.guiItem.setSound(DGG.ENTER + self.guiId, rolloverSound)
+        else:
+            self.guiItem.clearSound(DGG.ENTER + self.guiId)
+
     def setClickSound(self):
         clickSound = self['clickSound']
         # Clear out sounds
@@ -122,9 +130,16 @@ class DirectButton(DirectFrame):
             if DGG.RMB in self['commandButtons']:
                 self.guiItem.setSound(DGG.B3PRESS + self.guiId, clickSound)
 
-    def setRolloverSound(self):
-        rolloverSound = self['rolloverSound']
-        if rolloverSound:
-            self.guiItem.setSound(DGG.ENTER + self.guiId, rolloverSound)
-        else:
-            self.guiItem.clearSound(DGG.ENTER + self.guiId)
+    def setReleaseSound(self):
+        releaseSound = self['releaseSound']
+        # Clear out sounds
+        self.guiItem.clearSound(DGG.B1RELEASE + self.guiId)
+        self.guiItem.clearSound(DGG.B2RELEASE + self.guiId)
+        self.guiItem.clearSound(DGG.B3RELEASE + self.guiId)
+        if releaseSound:
+            if DGG.LMB in self['commandButtons']:
+                self.guiItem.setSound(DGG.B1RELEASE + self.guiId, releaseSound)
+            if DGG.MMB in self['commandButtons']:
+                self.guiItem.setSound(DGG.B2RELEASE + self.guiId, releaseSound)
+            if DGG.RMB in self['commandButtons']:
+                self.guiItem.setSound(DGG.B3RELEASE + self.guiId, releaseSound)

+ 1 - 0
direct/src/gui/DirectCheckBox.py

@@ -28,6 +28,7 @@ class DirectCheckBox(DirectButton):
             # Sounds to be used for button events
             ('rolloverSound', DGG.getDefaultRolloverSound(), self.setRolloverSound),
             ('clickSound',    DGG.getDefaultClickSound(),    self.setClickSound),
+            ('releaseSound',  DGG.getDefaultReleaseSound(),  self.setReleaseSound),
             # Can only be specified at time of widget contruction
             # Do the text/graphics appear to move when the button is clicked
             ('pressEffect',     1,         DGG.INITOPT),

+ 9 - 1
direct/src/gui/DirectGuiGlobals.py

@@ -17,8 +17,9 @@ from panda3d.core import (
 
 defaultFont = None
 defaultFontFunc = TextNode.getDefaultFont
-defaultClickSound = None
 defaultRolloverSound = None
+defaultClickSound = None
+defaultReleaseSound = None
 defaultDialogGeom = None
 defaultDialogRelief = PGFrameStyle.TBevelOut
 drawOrder = 100
@@ -129,6 +130,13 @@ def setDefaultClickSound(newSound):
     global defaultClickSound
     defaultClickSound = newSound
 
+def getDefaultReleaseSound():
+    return defaultReleaseSound
+
+def setDefaultReleaseSound(newSound):
+    global defaultReleaseSound
+    defaultReleaseSound = newSound
+
 def getDefaultFont():
     global defaultFont
     if defaultFont is None:

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

@@ -3517,6 +3517,7 @@ class ShowBase(DirectObject.DirectObject):
     remove_camera_frustum = removeCameraFrustum
     save_cube_map = saveCubeMap
     save_sphere_map = saveSphereMap
+    user_exit = userExit
     start_wx = startWx
     start_tk = startTk
     start_direct = startDirect

+ 8 - 0
direct/src/showbase/showBase.cxx

@@ -21,6 +21,7 @@ extern "C" { void CPSEnableForegroundOperation(ProcessSerialNumber* psn); }
 
 #include "showBase.h"
 
+#include "throw_event.h"
 #include "graphicsWindow.h"
 #include "renderBuffer.h"
 #include "camera.h"
@@ -44,6 +45,13 @@ ConfigureDef(config_showbase);
 ConfigureFn(config_showbase) {
 }
 
+// Throw the "NewFrame" event in the C++ world.  Some of the lerp code depends
+// on receiving this.
+void
+throw_new_frame() {
+  throw_event("NewFrame");
+}
+
 // Initialize the application for making a Gui-based app, such as wx.  At the
 // moment, this is a no-op except on Mac.
 void

+ 3 - 0
direct/src/showbase/showBase.h

@@ -16,6 +16,7 @@
 
 #include "directbase.h"
 
+#include "eventHandler.h"
 #include "graphicsWindow.h"
 #include "graphicsPipe.h"
 #include "animControl.h"
@@ -31,6 +32,8 @@ class GraphicsEngine;
 
 BEGIN_PUBLISH
 
+EXPCL_DIRECT_SHOWBASE void throw_new_frame();
+
 EXPCL_DIRECT_SHOWBASE void init_app_for_gui();
 
 // to handle windows stickykeys

+ 28 - 0
doc/ReleaseNotes

@@ -1,3 +1,31 @@
+-----------------------  RELEASE 1.10.16  -----------------------
+
+This maintenance release fixes some minor defects and stability issues.
+
+* OpenAL: Support non-default coordinate systems when playing 3D audio
+* Tasks: Coroutine detect now also handles coroutine subclass to support Nuitka
+* Tasks: Now properly handles generators without send()
+* Windows: Fixes a hang when adjusting Z-order in some situations
+* Fix SparseArray methods get_lowest_off_bit() and get_lowest_on_bit()
+* Fix linecache error when distributing for newer Python versions
+* Fix `await AsyncFuture.gather()` returning first item instead of tuple (#1738)
+* Fix use-after-free in collision system with transform cache disabled (#1733)
+* Egg: Add limited forward compatibility for metallic-roughness textures
+* OpenGL: fix error blitting depth texture on macOS (#1719)
+* OpenGL: fix SSBO support not being detected in certain situations
+* GLSL: Add p3d_MetallicRoughnessTexture input mapped to M_metallic_roughness
+* X11: Add config variable to enable detection of autorepeat key events (#1735)
+* GUI: Fix bug with PGSliderBar dragging (#1722)
+* Minor thread safety things for free-threaded Python builds
+* Add forward compatibility for bam version 6.46
+* Fixes a harmless buffer overflow in pdtoa
+* Fix compilation issues with SDL version of tinydisplay (#1708)
+* bam2egg: Fix issue when having more than two tags (#1725)
+* Fix "Detected leak for ... which interrogate cannot delete." error (#1743)
+* Fix PythonCallbackObject crash upon destruction in some cases
+* PStats: Fix crash when receiving frames out of order
+* PandaFramework::close_framework() now clears task manager of tasks
+
 -----------------------  RELEASE 1.10.15  -----------------------
 
 This release adds support for Python 3.13, and fixes some significant bugs.

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

@@ -96,6 +96,9 @@
 #elif defined(__arm__)
 #define DTOOL_PLATFORM "linux_arm"
 
+#elif defined(__riscv)
+#define DTOOL_PLATFORM "linux_riscv"
+
 #elif defined(__ppc__)
 #define DTOOL_PLATFORM "linux_ppc"
 

+ 3 - 0
dtool/src/dtoolbase/memoryHook.cxx

@@ -618,6 +618,9 @@ determine_page_size() const {
 
   _page_size = (size_t)sysinfo.dwPageSize;
 
+#elif defined(ANDROID)
+  _page_size = getpagesize();
+
 #else
   // Posix case.
   _page_size = sysconf(_SC_PAGESIZE);

+ 12 - 0
dtool/src/dtoolbase/patomic.I

@@ -454,6 +454,12 @@ patomic_notify_one(volatile uint32_t *value) {
 #elif defined(_WIN32)
   _patomic_wake_one_func((void *)value);
 #elif defined(__APPLE__)
+#ifndef __arm64__
+  if (UNLIKELY(__ulock_wake == nullptr || __ulock_wait == nullptr)) {
+    _patomic_notify_all(value);
+    return;
+  }
+#endif
   __ulock_wake(UL_COMPARE_AND_WAIT, (void *)value, 0);
 #elif defined(HAVE_POSIX_THREADS)
   _patomic_notify_all(value);
@@ -472,6 +478,12 @@ patomic_notify_all(volatile uint32_t *value) {
 #elif defined(_WIN32)
   _patomic_wake_all_func((void *)value);
 #elif defined(__APPLE__)
+#ifndef __arm64__
+  if (UNLIKELY(__ulock_wake == nullptr || __ulock_wait == nullptr)) {
+    _patomic_notify_all(value);
+    return;
+  }
+#endif
   __ulock_wake(UL_COMPARE_AND_WAIT | ULF_WAKE_ALL, (void *)value, 0);
 #elif defined(HAVE_POSIX_THREADS)
   _patomic_notify_all(value);

+ 1 - 1
dtool/src/dtoolbase/patomic.cxx

@@ -125,7 +125,7 @@ initialize_wait(volatile VOID *addr, PVOID cmp, SIZE_T size, DWORD timeout) {
   return emulated_wait(addr, cmp, size, timeout);
 }
 
-#elif !defined(CPPPARSER) && !defined(__linux__) && !defined(__APPLE__) && defined(HAVE_POSIX_THREADS)
+#elif !defined(CPPPARSER) && !defined(__linux__) && (!defined(__APPLE__) || !defined(__arm64__)) && defined(HAVE_POSIX_THREADS)
 
 // Same as above, but using pthreads.
 struct alignas(64) WaitTableEntry {

+ 9 - 4
dtool/src/dtoolbase/patomic.h

@@ -36,9 +36,6 @@
 // Undocumented API, see https://outerproduct.net/futex-dictionary.html
 #define UL_COMPARE_AND_WAIT 1
 #define ULF_WAKE_ALL 0x00000100
-
-extern "C" int __ulock_wait(uint32_t op, void *addr, uint64_t value, uint32_t timeout);
-extern "C" int __ulock_wake(uint32_t op, void *addr, uint64_t wake_value);
 #endif
 
 #if defined(THREAD_DUMMY_IMPL) || defined(THREAD_SIMPLE_IMPL)
@@ -173,9 +170,17 @@ ALWAYS_INLINE void patomic_notify_all(volatile uint32_t *value);
 EXPCL_DTOOL_DTOOLBASE extern BOOL (__stdcall *_patomic_wait_func)(volatile VOID *, PVOID, SIZE_T, DWORD);
 EXPCL_DTOOL_DTOOLBASE extern void (__stdcall *_patomic_wake_one_func)(PVOID);
 EXPCL_DTOOL_DTOOLBASE extern void (__stdcall *_patomic_wake_all_func)(PVOID);
-#elif !defined(__linux__) && !defined(__APPLE__) && defined(HAVE_POSIX_THREADS)
+#elif defined(__APPLE__) && defined(__arm64__)
+extern "C" int __ulock_wait(uint32_t op, void *addr, uint64_t value, uint32_t timeout);
+extern "C" int __ulock_wake(uint32_t op, void *addr, uint64_t wake_value);
+#elif !defined(__linux__) && defined(HAVE_POSIX_THREADS)
 EXPCL_DTOOL_DTOOLBASE void _patomic_wait(const volatile uint32_t *value, uint32_t old);
 EXPCL_DTOOL_DTOOLBASE void _patomic_notify_all(volatile uint32_t *value);
+#ifdef __APPLE__
+// Use conditionally since we can't count on support before 10.12.
+extern "C" int __ulock_wait(uint32_t op, void *addr, uint64_t value, uint32_t timeout) __attribute__((weak_import));
+extern "C" int __ulock_wake(uint32_t op, void *addr, uint64_t wake_value) __attribute__((weak_import));
+#endif
 #endif
 
 #include "patomic.I"

+ 3 - 1
dtool/src/dtoolbase/pvector.h

@@ -51,7 +51,9 @@ public:
   pvector(pvector<Type> &&from) noexcept : base_class(std::move(from)) {};
   explicit pvector(size_type n, TypeHandle type_handle = pvector_type_handle) : base_class(n, Type(), allocator(type_handle)) { }
   explicit pvector(size_type n, const Type &value, TypeHandle type_handle = pvector_type_handle) : base_class(n, value, allocator(type_handle)) { }
-  pvector(const Type *begin, const Type *end, TypeHandle type_handle = pvector_type_handle) : base_class(begin, end, allocator(type_handle)) { }
+  pvector(const Type *begin, const Type *end, TypeHandle type_handle = pvector_type_handle) : base_class(allocator(type_handle)) {
+    this->insert(this->end(), begin, end);
+  }
   pvector(std::initializer_list<Type> init, TypeHandle type_handle = pvector_type_handle) : base_class(std::move(init), allocator(type_handle)) { }
 
   pvector<Type> &operator =(const pvector<Type> &copy) {

+ 10 - 0
dtool/src/dtoolutil/load_dso.cxx

@@ -19,11 +19,15 @@ using std::string;
 static Filename resolve_dso(const DSearchPath &path, const Filename &filename) {
   if (filename.is_local()) {
     if ((path.get_num_directories()==1)&&(path.get_directory(0)=="<auto>")) {
+#ifdef ANDROID
+      return filename;
+#else
       // This is a special case, meaning to search in the same directory in
       // which libp3dtool.dll, or the exe, was started from.
       Filename dtoolpath = ExecutionEnvironment::get_dtool_name();
       DSearchPath spath(dtoolpath.get_dirname());
       return spath.find_file(filename);
+#endif
     } else {
       return path.find_file(filename);
     }
@@ -119,7 +123,13 @@ get_dso_symbol(void *handle, const string &name) {
 void *
 load_dso(const DSearchPath &path, const Filename &filename) {
   Filename abspath = resolve_dso(path, filename);
+#ifdef ANDROID
+  // We just try to load it on Android, because we can't verify right now
+  // whether it might just be an unextracted library.
+  if (abspath.empty()) {
+#else
   if (!abspath.is_regular_file()) {
+#endif
     // Make sure the error flag is cleared, to prevent a subsequent call to
     // load_dso_error() from returning a previously stored error.
     dlerror();

+ 3 - 3
dtool/src/prc/configPageManager.cxx

@@ -97,7 +97,6 @@ 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 {
@@ -129,11 +128,13 @@ reload_implicit_pages() {
 //  const BlobInfo *blobinfo = (const BlobInfo *)dlsym(RTLD_SELF, "blobinfo");
 #elif defined(__EMSCRIPTEN__)
   const BlobInfo *blobinfo = nullptr;
+#elif defined(ANDROID)
+  const BlobInfo *blobinfo = nullptr;
 #else
   const BlobInfo *blobinfo = (const BlobInfo *)dlsym(dlopen(nullptr, RTLD_NOW), "blobinfo");
 #endif
   if (blobinfo == nullptr) {
-#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
+#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && !defined(ANDROID)
     // Clear the error flag.
     dlerror();
 #endif
@@ -482,7 +483,6 @@ reload_implicit_pages() {
       }
     }
   }
-#endif  // ANDROID
 
   if (!_loaded_implicit) {
     config_initialized();

+ 31 - 18
dtool/src/prc/notify.cxx

@@ -27,6 +27,7 @@
 #endif
 
 #ifdef ANDROID
+#include <sys/stat.h>
 #include <android/log.h>
 #include "androidLogStream.h"
 #endif
@@ -635,22 +636,7 @@ config_initialized() {
   // notify-output even after the initial import of Panda3D modules.  However,
   // it cannot be changed after the first time it is set.
 
-#if defined(ANDROID)
-  // Android redirects stdio and stderr to /dev/null,
-  // but does provide its own logging system.  We use a special
-  // type of stream that redirects it to Android's log system.
-
-  Notify *ptr = Notify::ptr();
-
-  for (int severity = 0; severity <= NS_fatal; ++severity) {
-    int priority = ANDROID_LOG_UNKNOWN;
-    if (severity != NS_unspecified) {
-      priority = severity + 1;
-    }
-    ptr->_log_streams[severity] = new AndroidLogStream(priority);
-  }
-
-#elif defined(__EMSCRIPTEN__)
+#if defined(__EMSCRIPTEN__)
   // We have no writable filesystem in JavaScript.  Instead, we set up a
   // special stream that logs straight into the Javascript console.
 
@@ -715,11 +701,38 @@ config_initialized() {
         }
 #endif  // BUILD_IPHONE
       }
+
 #ifdef ANDROID
+      for (int severity = 0; severity <= NS_fatal; ++severity) {
+        ptr->_log_streams[severity] = ptr->_ostream_ptr;
+      }
+
     } else {
-      // By default, we always redirect the notify stream to the Android log.
+      // By default, we always redirect the notify stream to the Android log,
+      // except if we are running from the adb shell.  We decide this based
+      // on whether stderr is redirected to /dev/null.
       Notify *ptr = Notify::ptr();
-      ptr->set_ostream_ptr(new AndroidLogStream(ANDROID_LOG_INFO), true);
+      struct stat a, b;
+      if (fstat(STDERR_FILENO, &a) == 0 && stat("/dev/null", &b) == 0 &&
+          a.st_dev == b.st_dev && a.st_ino == b.st_ino) {
+        // Android redirects stdio and stderr to /dev/null,
+        // but does provide its own logging system.  We use a special
+        // type of stream that redirects it to Android's log system.
+        for (int severity = 0; severity <= NS_fatal; ++severity) {
+          int priority = ANDROID_LOG_UNKNOWN;
+          if (severity != NS_unspecified) {
+            priority = severity + 1;
+          }
+          ptr->_log_streams[severity] = new AndroidLogStream(priority);
+        }
+        ptr->set_ostream_ptr(new AndroidLogStream(ANDROID_LOG_INFO), true);
+      } else {
+        // Running from the terminal, set all the log streams to point to the
+        // same output.
+        for (int severity = 0; severity <= NS_fatal; ++severity) {
+          ptr->_log_streams[severity] = &cerr;
+        }
+      }
 #endif
     }
   }

+ 1 - 1
dtool/src/prc/pnotify.h

@@ -102,7 +102,7 @@ private:
   Categories _categories;
 
 #if defined(ANDROID)
-  AndroidLogStream *_log_streams[NS_fatal + 1];
+  std::ostream *_log_streams[NS_fatal + 1];
 #elif defined(__EMSCRIPTEN__)
   EmscriptenLogStream *_log_streams[NS_fatal + 1];
 #endif

+ 12 - 0
makepanda/installer.nsi

@@ -381,6 +381,7 @@ SectionGroup "Python modules" SecGroupPython
         !insertmacro PyBindingSection 3.12-32 .cp312-win32.pyd
         !insertmacro PyBindingSection 3.13-32 .cp313-win32.pyd
         !insertmacro PyBindingSection 3.14-32 .cp314-win32.pyd
+        !insertmacro PyBindingSection 3.15-32 .cp315-win32.pyd
     !else
         !insertmacro PyBindingSection 3.5 .cp35-win_amd64.pyd
         !insertmacro PyBindingSection 3.6 .cp36-win_amd64.pyd
@@ -392,6 +393,7 @@ SectionGroup "Python modules" SecGroupPython
         !insertmacro PyBindingSection 3.12 .cp312-win_amd64.pyd
         !insertmacro PyBindingSection 3.13 .cp313-win_amd64.pyd
         !insertmacro PyBindingSection 3.14 .cp314-win_amd64.pyd
+        !insertmacro PyBindingSection 3.15 .cp315-win_amd64.pyd
     !endif
 SectionGroupEnd
 
@@ -504,6 +506,7 @@ Function .onInit
         !insertmacro MaybeEnablePyBindingSection 3.12-32
         !insertmacro MaybeEnablePyBindingSection 3.13-32
         !insertmacro MaybeEnablePyBindingSection 3.14-32
+        !insertmacro MaybeEnablePyBindingSection 3.15-32
         ${EndIf}
     !else
         !insertmacro MaybeEnablePyBindingSection 3.5
@@ -517,6 +520,7 @@ Function .onInit
         !insertmacro MaybeEnablePyBindingSection 3.12
         !insertmacro MaybeEnablePyBindingSection 3.13
         !insertmacro MaybeEnablePyBindingSection 3.14
+        !insertmacro MaybeEnablePyBindingSection 3.15
         ${EndIf}
     !endif
 
@@ -546,6 +550,10 @@ Function .onInit
         SectionSetFlags ${SecPyBindings3.14} ${SF_RO}
         SectionSetInstTypes ${SecPyBindings3.14} 0
     !endif
+    !ifdef SecPyBindings3.15
+        SectionSetFlags ${SecPyBindings3.15} ${SF_RO}
+        SectionSetInstTypes ${SecPyBindings3.15} 0
+    !endif
     ${EndUnless}
 FunctionEnd
 
@@ -851,6 +859,7 @@ Section Uninstall
         !insertmacro RemovePythonPath 3.12-32
         !insertmacro RemovePythonPath 3.13-32
         !insertmacro RemovePythonPath 3.14-32
+        !insertmacro RemovePythonPath 3.15-32
     !else
         !insertmacro RemovePythonPath 3.5
         !insertmacro RemovePythonPath 3.6
@@ -862,6 +871,7 @@ Section Uninstall
         !insertmacro RemovePythonPath 3.12
         !insertmacro RemovePythonPath 3.13
         !insertmacro RemovePythonPath 3.14
+        !insertmacro RemovePythonPath 3.15
     !endif
 
     SetDetailsPrint both
@@ -934,6 +944,7 @@ SectionEnd
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.12-32} $(DESC_SecPyBindings3.12-32)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.13-32} $(DESC_SecPyBindings3.13-32)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.14-32} $(DESC_SecPyBindings3.14-32)
+    !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.15-32} $(DESC_SecPyBindings3.15-32)
   !else
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.5} $(DESC_SecPyBindings3.5)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.6} $(DESC_SecPyBindings3.6)
@@ -945,6 +956,7 @@ SectionEnd
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.12} $(DESC_SecPyBindings3.12)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.13} $(DESC_SecPyBindings3.13)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.14} $(DESC_SecPyBindings3.14)
+    !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.15} $(DESC_SecPyBindings3.15)
   !endif
   !ifdef INCLUDE_PYVER
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPython} $(DESC_SecPython)

+ 38 - 16
makepanda/makepanda.py

@@ -1120,7 +1120,6 @@ if (COMPILER=="GCC"):
         LibName("ALWAYS", "-framework AppKit")
         LibName("IOKIT", "-framework IOKit")
         LibName("QUARTZ", "-framework Quartz")
-        LibName("AGL", "-framework AGL")
         LibName("CARBON", "-framework Carbon")
         LibName("COCOA", "-framework Cocoa")
         # Fix for a bug in OSX Leopard:
@@ -1431,10 +1430,10 @@ def CompileCxx(obj,src,opts):
             elif arch == 'mips64':
                 cmd += ' -fintegrated-as'
             elif arch == 'x86':
-                cmd += ' -march=i686 -mssse3 -mfpmath=sse -m32'
+                cmd += ' -march=i686 -mssse3 -mfpmath=sse'
                 cmd += ' -mstackrealign'
             elif arch == 'x86_64':
-                cmd += ' -march=x86-64 -msse4.2 -mpopcnt -m64'
+                cmd += ' -march=x86-64 -msse4.2 -mpopcnt'
 
             cmd += " -Wa,--noexecstack"
 
@@ -1477,7 +1476,7 @@ def CompileCxx(obj,src,opts):
                 if optlevel >= 4 or target == "android":
                     cmd += " -fno-rtti"
 
-        if ('SSE2' in opts or not PkgSkip("SSE2")) and not arch.startswith("arm") and arch != 'aarch64':
+        if ('SSE2' in opts or not PkgSkip("SSE2")) and arch.find('86') > 0:
             if GetTarget() != "emscripten":
                 cmd += " -msse2"
 
@@ -1955,6 +1954,11 @@ def CompileLink(dll, obj, opts):
                 cmd += " -march=armv7-a -Wl,--fix-cortex-a8"
             elif arch == 'mips':
                 cmd += ' -mips32'
+
+            if arch.endswith('64'):
+                # See https://developer.android.com/guide/practices/page-sizes
+                cmd += ' -Wl,-z,max-page-size=16384'
+
             cmd += ' -lc -lm'
 
         elif GetTarget() == 'emscripten':
@@ -4999,13 +5003,17 @@ if GetTarget() == 'android':
     TargetAdd('org/panda3d/android/NativeOStream.class', opts=OPTS, input='NativeOStream.java')
     TargetAdd('org/panda3d/android/PandaActivity.class', opts=OPTS, input='PandaActivity.java')
     TargetAdd('org/panda3d/android/PandaActivity$1.class', opts=OPTS+['DEPENDENCYONLY'], input='PandaActivity.java')
+    TargetAdd('org/panda3d/android/PandaActivity$2.class', opts=OPTS+['DEPENDENCYONLY'], input='PandaActivity.java')
     TargetAdd('org/panda3d/android/PythonActivity.class', opts=OPTS, input='PythonActivity.java')
+    TargetAdd('org/panda3d/android/PythonActivity$ActivityResultListener.class', opts=OPTS+['DEPENDENCYONLY'], 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/PandaActivity$1.class')
+    TargetAdd('classes.dex', input='org/panda3d/android/PandaActivity$2.class')
     TargetAdd('classes.dex', input='org/panda3d/android/PythonActivity.class')
+    TargetAdd('classes.dex', input='org/panda3d/android/PythonActivity$ActivityResultListener.class')
 
     TargetAdd('p3android_composite1.obj', opts=OPTS, input='p3android_composite1.cxx')
     TargetAdd('libp3android.dll', input='p3android_composite1.obj')
@@ -5337,6 +5345,19 @@ if not PkgSkip("PANDATOOL"):
         TargetAdd('egg2bam.exe', input=COMMON_EGG2X_LIBS)
         TargetAdd('egg2bam.exe', opts=['ADVAPI', 'FFTW'])
 
+#
+# DIRECTORY: pandatool/src/converter/
+#
+
+if not PkgSkip("PANDATOOL"):
+    OPTS=['DIR:pandatool/src/converter']
+    TargetAdd('txo-converter_txoConverter.obj', opts=OPTS, input='txoConverter.cxx')
+    TargetAdd('txo-converter.exe', input='txo-converter_txoConverter.obj')
+    TargetAdd('txo-converter.exe', input='libp3progbase.lib')
+    TargetAdd('txo-converter.exe', input='libp3pandatoolbase.lib')
+    TargetAdd('txo-converter.exe', input=COMMON_PANDA_LIBS)
+    TargetAdd('txo-converter.exe', opts=['ADVAPI', 'FFTW'])
+
 #
 # DIRECTORY: pandatool/src/daeegg/
 #
@@ -6020,10 +6041,10 @@ if PkgSkip("PYTHON") == 0:
         TargetAdd('classes.dex', input='org/jnius/NativeInvocationHandler.class')
 
         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('deploy-stubw_android_support.obj', opts=OPTS, input='android_support.cxx')
         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='deploy-stubw_android_support.obj')
         PyTargetAdd('libdeploy-stubw.dll', input=COMMON_PANDA_LIBS)
         PyTargetAdd('libdeploy-stubw.dll', input='libp3android.dll')
         PyTargetAdd('libdeploy-stubw.dll', opts=['DEPLOYSTUB', 'ANDROID'])
@@ -6031,7 +6052,7 @@ if PkgSkip("PYTHON") == 0:
 #
 # Build the test runner for static builds
 #
-if GetLinkAllStatic():
+if GetLinkAllStatic() or GetTarget() == 'android':
     if GetTarget() == 'emscripten':
         LinkFlag('RUN_TESTS_FLAGS', '-s NODERAWFS')
         LinkFlag('RUN_TESTS_FLAGS', '-s ASSERTIONS=2')
@@ -6052,15 +6073,16 @@ if GetLinkAllStatic():
     OPTS=['DIR:tests', 'PYTHON', 'RUN_TESTS_FLAGS', 'SUBSYSTEM:CONSOLE']
     PyTargetAdd('run_tests-main.obj', opts=OPTS, input='main.c')
     PyTargetAdd('run_tests.exe', input='run_tests-main.obj')
-    PyTargetAdd('run_tests.exe', input='core.pyd')
-    if not PkgSkip('DIRECT'):
-        PyTargetAdd('run_tests.exe', input='direct.pyd')
-    if not PkgSkip('PANDAPHYSICS'):
-        PyTargetAdd('run_tests.exe', input='physics.pyd')
-    if not PkgSkip('EGG'):
-        PyTargetAdd('run_tests.exe', input='egg.pyd')
-    if not PkgSkip('BULLET'):
-        PyTargetAdd('run_tests.exe', input='bullet.pyd')
+    if GetLinkAllStatic():
+        PyTargetAdd('run_tests.exe', input='core.pyd')
+        if not PkgSkip('DIRECT'):
+            PyTargetAdd('run_tests.exe', input='direct.pyd')
+        if not PkgSkip('PANDAPHYSICS'):
+            PyTargetAdd('run_tests.exe', input='physics.pyd')
+        if not PkgSkip('EGG'):
+            PyTargetAdd('run_tests.exe', input='egg.pyd')
+        if not PkgSkip('BULLET'):
+            PyTargetAdd('run_tests.exe', input='bullet.pyd')
     PyTargetAdd('run_tests.exe', input=COMMON_PANDA_LIBS)
     PyTargetAdd('run_tests.exe', opts=['PYTHON', 'BULLET', 'RUN_TESTS_FLAGS'])
 

+ 13 - 5
makepanda/makepandacore.py

@@ -2497,7 +2497,7 @@ def SdkLocateMacOSX(archs = []):
         # Prefer pre-10.14 for now so that we can keep building FMOD.
         sdk_versions += ["10.13", "10.12"]
 
-    sdk_versions += ["14.0", "13.3", "13.1", "13.0", "12.3", "11.3", "11.1", "11.0"]
+    sdk_versions += ["15.5", "15.4", "15.2", "15.1", "15.0", "14.5", "14.4", "14.2", "14.0", "13.3", "13.1", "13.0", "12.3", "11.3", "11.1", "11.0"]
 
     if 'arm64' not in archs:
         sdk_versions += ["10.15", "10.14"]
@@ -2506,16 +2506,20 @@ def SdkLocateMacOSX(archs = []):
         sdkname = "MacOSX" + version
         if os.path.exists("/Library/Developer/CommandLineTools/SDKs/%s.sdk" % sdkname):
             SDK["MACOSX"] = "/Library/Developer/CommandLineTools/SDKs/%s.sdk" % sdkname
-            return
         elif os.path.exists("/Developer/SDKs/%s.sdk" % sdkname):
             SDK["MACOSX"] = "/Developer/SDKs/%s.sdk" % sdkname
-            return
         elif os.path.exists("/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/%s.sdk" % sdkname):
             SDK["MACOSX"] = "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/%s.sdk" % sdkname
-            return
         elif xcode_dir and os.path.exists("%s/Platforms/MacOSX.platform/Developer/SDKs/%s.sdk" % (xcode_dir, sdkname)):
             SDK["MACOSX"] = "%s/Platforms/MacOSX.platform/Developer/SDKs/%s.sdk" % (xcode_dir, sdkname)
-            return
+        else:
+            continue
+
+        if GetVerbose():
+            print("Using macOS %s SDK located at %s" % (version, SDK["MACOSX"]))
+        else:
+            print("Using macOS %s SDK" % version)
+        return
 
     exit("Couldn't find any suitable MacOSX SDK!")
 
@@ -3446,6 +3450,10 @@ def GetExtensionSuffix():
         abi = GetPythonABI()
         arch = GetTargetArch()
         return '.{0}-{1}-emscripten.so'.format(abi, arch)
+    elif target == 'android':
+        abi = GetPythonABI()
+        triple = ANDROID_TRIPLE.rstrip('0123456789')
+        return '.{0}-{1}.so'.format(abi, triple)
     elif CrossCompiling():
         return '.{0}.so'.format(GetPythonABI())
     else:

+ 65 - 0
panda/src/android/PandaActivity.java

@@ -17,7 +17,9 @@ import android.app.NativeActivity;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
+import android.content.res.AssetFileDescriptor;
 import android.net.Uri;
+import android.os.ParcelFileDescriptor;
 import android.widget.Toast;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -25,6 +27,8 @@ import dalvik.system.BaseDexClassLoader;
 import org.panda3d.android.NativeIStream;
 import org.panda3d.android.NativeOStream;
 
+import android.util.Log;
+
 /**
  * The entry point for a Panda-based activity.  Loads the Panda libraries and
  * also provides some utility functions.
@@ -77,6 +81,44 @@ public class PandaActivity extends NativeActivity {
         return Thread.currentThread().getName();
     }
 
+    /**
+     * Called by android_native_app_glue to spawn the application thread.
+     * Gets passed a function pointer and a data pointer to pass to it.
+     */
+    protected void spawnAppThread(long ptr, long data) {
+        new Thread(() -> {
+            nativeThreadEntry(ptr, data);
+        }).start();
+    }
+
+    /**
+     * Maps the blob to memory and returns the pointer.
+     */
+    public long mapBlobFromResource(long offset) {
+        int resourceId = 0;
+        try {
+            ActivityInfo ai = getPackageManager().getActivityInfo(
+                    getIntent().getComponent(), PackageManager.GET_META_DATA);
+            if (ai.metaData == null) {
+                Log.e("Panda3D", "Failed to get activity metadata");
+                return 0;
+            }
+            resourceId = ai.metaData.getInt("org.panda3d.android.BLOB_RESOURCE");
+            if (resourceId == 0) {
+                return 0;
+            }
+
+            AssetFileDescriptor afd = getResources().openRawResourceFd(resourceId);
+            ParcelFileDescriptor pfd = afd.getParcelFileDescriptor();
+            long off = afd.getStartOffset() + offset;
+            long len = afd.getLength();
+            return nativeMmap(pfd.getFd(), off, len);
+        } catch (Exception e) {
+            Log.e("Panda3D", "Received exception while trying to map blob: " + e);
+            return 0;
+        }
+    }
+
     /**
      * Returns the path to the main native library.
      */
@@ -97,6 +139,14 @@ public class PandaActivity extends NativeActivity {
         return classLoader.findLibrary(libname);
     }
 
+    /**
+     * Returns the path to some other native library.
+     */
+    public String findLibrary(String libname) {
+        BaseDexClassLoader classLoader = (BaseDexClassLoader)getClassLoader();
+        return classLoader.findLibrary(libname);
+    }
+
     public String getIntentDataPath() {
         Intent intent = getIntent();
         Uri data = intent.getData();
@@ -119,6 +169,18 @@ public class PandaActivity extends NativeActivity {
         return getCacheDir().toString();
     }
 
+    /**
+     * Sets the window title.
+     */
+    public void setWindowTitle(final CharSequence title) {
+        final PandaActivity activity = this;
+        runOnUiThread(new Runnable() {
+            public void run() {
+                activity.setTitle(title);
+            }
+        });
+    }
+
     /**
      * Shows a pop-up notification.
      */
@@ -139,4 +201,7 @@ public class PandaActivity extends NativeActivity {
         // Contains our JNI calls.
         System.loadLibrary("p3android");
     }
+
+    private static native long nativeMmap(int fd, long off, long len);
+    private static native void nativeThreadEntry(long ptr, long data);
 }

+ 41 - 0
panda/src/android/PythonActivity.java

@@ -15,6 +15,12 @@ package org.panda3d.android;
 
 import org.panda3d.android.PandaActivity;
 
+import android.content.Intent;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
 /**
  * Extends PandaActivity with some things that are useful in a Python
  * application.
@@ -26,4 +32,39 @@ public class PythonActivity extends PandaActivity {
     public PythonActivity() {
         mActivity = this;
     }
+
+    // Helper code to further support plyer.
+    public interface ActivityResultListener {
+        void onActivityResult(int requestCode, int resultCode, Intent data);
+    }
+
+    private List<ActivityResultListener> activityResultListeners = null;
+
+    public void registerActivityResultListener(ActivityResultListener listener) {
+        if (this.activityResultListeners == null) {
+            this.activityResultListeners = Collections.synchronizedList(new ArrayList<ActivityResultListener>());
+        }
+        this.activityResultListeners.add(listener);
+    }
+
+    public void unregisterActivityResultListener(ActivityResultListener listener) {
+        if (this.activityResultListeners == null) {
+            return;
+        }
+        this.activityResultListeners.remove(listener);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+        if (this.activityResultListeners == null) {
+            return;
+        }
+        this.onResume();
+        synchronized (this.activityResultListeners) {
+            Iterator<ActivityResultListener> iterator = this.activityResultListeners.iterator();
+            while (iterator.hasNext()) {
+                iterator.next().onActivityResult(requestCode, resultCode, intent);
+            }
+        }
+    }
 }

+ 3 - 4
panda/src/android/android_main.cxx

@@ -271,10 +271,9 @@ void android_main(struct android_app* app) {
     // We still need to keep an event loop going until Android gives us leave
     // to end the process.
     while (!app->destroyRequested) {
-      int looper_id;
-      struct android_poll_source *source;
-      auto result = ALooper_pollOnce(-1, &looper_id, nullptr, (void **)&source);
-      if (looper_id == LOOPER_ID_MAIN) {
+      struct android_poll_source *source = nullptr;
+      int ident = ALooper_pollOnce(-1, nullptr, nullptr, (void **)&source);
+      if (ident == LOOPER_ID_MAIN) {
         int8_t cmd = android_app_read_cmd(app);
         android_app_pre_exec_cmd(app, cmd);
         android_app_post_exec_cmd(app, cmd);

+ 9 - 0
panda/src/android/android_native_app_glue.c

@@ -255,10 +255,19 @@ static struct android_app* android_app_create(ANativeActivity* activity,
     android_app->msgread = msgpipe[0];
     android_app->msgwrite = msgpipe[1];
 
+    // Spawn the app thread in Java so it gets a valid class loader.
+    JNIEnv *env = activity->env;
+    jclass activity_class = (*env)->GetObjectClass(env, activity->clazz);
+    jmethodID method = (*env)->GetMethodID(env, activity_class, "spawnAppThread", "(JJ)V");
+    LOGE("got function pointer: %ld, data: %ld, env: %ld", (jlong)(void *)android_app_entry, (jlong)(void *)android_app, (jlong)(void *)env);
+    (*env)->CallVoidMethod(env, activity->clazz, method, (void *)android_app_entry, (void *)android_app);
+
+    /*
     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);
+    */
 
     // Wait for thread to start.
     pthread_mutex_lock(&android_app->mutex);

+ 1 - 1
panda/src/android/android_native_app_glue.h

@@ -168,7 +168,7 @@ struct android_app {
     int msgread;
     int msgwrite;
 
-    pthread_t thread;
+    //pthread_t thread;
 
     struct android_poll_source cmdPollSource;
     struct android_poll_source inputPollSource;

+ 52 - 0
panda/src/android/config_android.cxx

@@ -27,6 +27,8 @@ jmethodID jni_PandaActivity_readBitmap;
 jmethodID jni_PandaActivity_createBitmap;
 jmethodID jni_PandaActivity_compressBitmap;
 jmethodID jni_PandaActivity_showToast;
+jmethodID jni_PandaActivity_setWindowTitle;
+jmethodID jni_PandaActivity_findLibrary;
 
 jclass   jni_BitmapFactory_Options;
 jfieldID jni_BitmapFactory_Options_outWidth;
@@ -77,9 +79,15 @@ jint JNI_OnLoad(JavaVM *jvm, void *reserved) {
   jni_PandaActivity_compressBitmap = env->GetStaticMethodID(jni_PandaActivity,
                    "compressBitmap", "(Landroid/graphics/Bitmap;IIJ)Z");
 
+  jni_PandaActivity_setWindowTitle = env->GetMethodID(jni_PandaActivity,
+                   "setWindowTitle", "(Ljava/lang/CharSequence;)V");
+
   jni_PandaActivity_showToast = env->GetMethodID(jni_PandaActivity,
                    "showToast", "(Ljava/lang/String;I)V");
 
+  jni_PandaActivity_findLibrary = env->GetMethodID(jni_PandaActivity,
+                   "findLibrary", "(Ljava/lang/String;)Ljava/lang/String;");
+
   jni_BitmapFactory_Options = env->FindClass("android/graphics/BitmapFactory$Options");
   jni_BitmapFactory_Options = (jclass) env->NewGlobalRef(jni_BitmapFactory_Options);
 
@@ -135,6 +143,43 @@ void JNI_OnUnload(JavaVM *jvm, void *reserved) {
   }
 }
 
+/**
+ *
+ */
+Filename android_find_library(ANativeActivity *activity, const std::string &lib) {
+  Thread *thread = Thread::get_current_thread();
+  JNIEnv *env = thread->get_jni_env();
+  nassertr(env != nullptr, Filename());
+
+  jstring jlib = env->NewStringUTF(lib.c_str());
+  jstring jresult = (jstring)env->CallObjectMethod(activity->clazz, jni_PandaActivity_findLibrary, jlib);
+  env->DeleteLocalRef(jlib);
+
+  Filename result;
+  if (jresult != nullptr) {
+    const char *c_str = env->GetStringUTFChars(jresult, nullptr);
+    result = c_str;
+    env->ReleaseStringUTFChars(jresult, c_str);
+  }
+
+  return result;
+}
+
+/**
+ * Sets the window title of the activity.
+ */
+void android_set_title(ANativeActivity *activity, const std::string &title) {
+  nassertv(jni_PandaActivity_setWindowTitle);
+
+  Thread *thread = Thread::get_current_thread();
+  JNIEnv *env = thread->get_jni_env();
+  nassertv(env != nullptr);
+
+  jstring jmsg = env->NewStringUTF(title.c_str());
+  env->CallVoidMethod(activity->clazz, jni_PandaActivity_setWindowTitle, jmsg);
+  env->DeleteLocalRef(jmsg);
+}
+
 /**
  * Shows a toast notification at the bottom of the activity.  The duration
  * should be 0 for short and 1 for long.
@@ -148,3 +193,10 @@ void android_show_toast(ANativeActivity *activity, const std::string &message, i
   env->CallVoidMethod(activity->clazz, jni_PandaActivity_showToast, jmsg, (jint)duration);
   env->DeleteLocalRef(jmsg);
 }
+
+/**
+ * Returns the JNIEnv pointer corresponding to the current thread.
+ */
+void *SDL_AndroidGetJNIEnv() {
+  return Thread::get_current_thread()->get_jni_env();
+}

+ 8 - 0
panda/src/android/config_android.h

@@ -33,12 +33,20 @@ extern jmethodID jni_PandaActivity_readBitmapHeader;
 extern jmethodID jni_PandaActivity_readBitmap;
 extern jmethodID jni_PandaActivity_createBitmap;
 extern jmethodID jni_PandaActivity_compressBitmap;
+extern jmethodID jni_PandaActivity_setWindowTitle;
 extern jmethodID jni_PandaActivity_showToast;
 
 extern jclass   jni_BitmapFactory_Options;
 extern jfieldID jni_BitmapFactory_Options_outWidth;
 extern jfieldID jni_BitmapFactory_Options_outHeight;
 
+EXPORT_CLASS Filename android_find_library(ANativeActivity *activity, const std::string &lib);
+EXPORT_CLASS void android_set_title(ANativeActivity *activity, const std::string &title);
 EXPORT_CLASS void android_show_toast(ANativeActivity *activity, const std::string &message, int duration);
 
+// Used to support pyjnius
+extern "C" {
+  EXPORT_CLASS void *SDL_AndroidGetJNIEnv(void);
+};
+
 #endif

+ 48 - 0
panda/src/android/jni_PandaActivity.cxx

@@ -0,0 +1,48 @@
+/**
+ * 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 jni_PandaActivity.cxx
+ * @author rdb
+ * @date 2025-11-09
+ */
+
+#include <jni.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#if __GNUC__ >= 4
+#define EXPORT_JNI extern "C" __attribute__((visibility("default")))
+#else
+#define EXPORT_JNI extern "C"
+#endif
+
+/**
+ *
+ */
+EXPORT_JNI jlong
+Java_org_panda3d_android_PandaActivity_nativeMmap(JNIEnv* env, jclass, jint fd, jlong off, jlong len) {
+  // Align the offset down to the page size boundary.
+  size_t page_size = getpagesize();
+  off_t aligned = off & ~((off_t)page_size - 1);
+  size_t delta = (size_t)(off - aligned);
+
+  void *ptr = mmap(nullptr, (size_t)len + delta, PROT_READ, MAP_PRIVATE, fd, aligned);
+  if (ptr != MAP_FAILED && ptr != nullptr) {
+    return (jlong)((uintptr_t)ptr + delta);
+  } else {
+    return (jlong)0;
+  }
+}
+
+/**
+ * Calls the given function pointer, passing the given data pointer.
+ */
+EXPORT_JNI void
+Java_org_panda3d_android_PandaActivity_nativeThreadEntry(JNIEnv* env, jobject self, jlong func, jlong data) {
+  ((void (*)(void *))(void *)func)((void *)data);
+}

+ 1 - 0
panda/src/android/p3android_composite1.cxx

@@ -1,6 +1,7 @@
 #include "config_android.cxx"
 #include "jni_NativeIStream.cxx"
 #include "jni_NativeOStream.cxx"
+#include "jni_PandaActivity.cxx"
 #include "pnmFileTypeAndroid.cxx"
 #include "pnmFileTypeAndroidReader.cxx"
 #include "pnmFileTypeAndroidWriter.cxx"

+ 5 - 0
panda/src/androiddisplay/androidGraphicsPipe.cxx

@@ -19,6 +19,8 @@
 #include "config_androiddisplay.h"
 #include "frameBufferProperties.h"
 
+extern IMPORT_CLASS struct android_app *panda_android_app;
+
 TypeHandle AndroidGraphicsPipe::_type_handle;
 
 /**
@@ -121,6 +123,9 @@ make_output(const std::string &name,
   // First thing to try: an eglGraphicsWindow
 
   if (retry == 0) {
+    if (panda_android_app == nullptr) {
+      return nullptr;
+    }
     if (((flags&BF_require_parasite)!=0)||
         ((flags&BF_refuse_window)!=0)||
         ((flags&BF_resizeable)!=0)||

+ 6 - 0
panda/src/androiddisplay/androidGraphicsWindow.cxx

@@ -15,6 +15,7 @@
 #include "androidGraphicsStateGuardian.h"
 #include "config_androiddisplay.h"
 #include "androidGraphicsPipe.h"
+#include "config_android.h"
 
 #include "graphicsPipe.h"
 #include "keyboardButton.h"
@@ -232,6 +233,11 @@ set_properties_now(WindowProperties &properties) {
     _properties.set_fullscreen(properties.get_fullscreen());
     properties.clear_fullscreen();
   }
+
+  if (properties.has_title()) {
+    android_set_title(_app->activity, properties.get_title());
+    properties.clear_title();
+  }
 }
 
 /**

+ 159 - 32
panda/src/audiotraits/openalAudioManager.cxx

@@ -156,11 +156,10 @@ OpenALAudioManager() {
       // We managed to get a device open.
       alcGetError(_device); // clear errors
 
-      ALCboolean is_hrtf_present = alcIsExtensionPresent(_device, "ALC_SOFT_HRTF");
-
       ALCint attrs[3] = {0};
 
 #ifndef HAVE_OPENAL_FRAMEWORK
+      ALCboolean is_hrtf_present = alcIsExtensionPresent(_device, "ALC_SOFT_HRTF");
       if (is_hrtf_present) {
         attrs[0] = ALC_HRTF_SOFT;
         attrs[1] = audio_want_hrtf.get_value() ? ALC_TRUE : ALC_FALSE;
@@ -716,21 +715,85 @@ get_active() const {
 void OpenALAudioManager::
 audio_3d_set_listener_attributes(PN_stdfloat px, PN_stdfloat py, PN_stdfloat pz, PN_stdfloat vx, PN_stdfloat vy, PN_stdfloat vz, PN_stdfloat fx, PN_stdfloat fy, PN_stdfloat fz, PN_stdfloat ux, PN_stdfloat uy, PN_stdfloat uz) {
   ReMutexHolder holder(_lock);
-  _position[0] = px;
-  _position[1] = pz;
-  _position[2] = -py;
-
-  _velocity[0] = vx;
-  _velocity[1] = vz;
-  _velocity[2] = -vy;
-
-  _forward_up[0] = fx;
-  _forward_up[1] = fz;
-  _forward_up[2] = -fy;
-
-  _forward_up[3] = ux;
-  _forward_up[4] = uz;
-  _forward_up[5] = -uy;
+  CoordinateSystem cs = get_default_coordinate_system();
+
+  switch (cs) {
+  case CS_yup_right:
+    _position[0] = px;
+    _position[1] = py;
+    _position[2] = pz;
+
+    _velocity[0] = vx;
+    _velocity[1] = vy;
+    _velocity[2] = vz;
+
+    _forward_up[0] = fx;
+    _forward_up[1] = fy;
+    _forward_up[2] = fz;
+
+    _forward_up[3] = ux;
+    _forward_up[4] = uy;
+    _forward_up[5] = uz;
+    break;
+
+  case CS_zup_right:
+    _position[0] = px;
+    _position[1] = pz;
+    _position[2] = -py;
+
+    _velocity[0] = vx;
+    _velocity[1] = vz;
+    _velocity[2] = -vy;
+
+    _forward_up[0] = fx;
+    _forward_up[1] = fz;
+    _forward_up[2] = -fy;
+
+    _forward_up[3] = ux;
+    _forward_up[4] = uz;
+    _forward_up[5] = -uy;
+    break;
+
+  case CS_yup_left:
+    _position[0] = px;
+    _position[1] = py;
+    _position[2] = -pz;
+
+    _velocity[0] = vx;
+    _velocity[1] = vy;
+    _velocity[2] = -vz;
+
+    _forward_up[0] = fx;
+    _forward_up[1] = fy;
+    _forward_up[2] = -fz;
+
+    _forward_up[3] = ux;
+    _forward_up[4] = uy;
+    _forward_up[5] = -uz;
+    break;
+
+  case CS_zup_left:
+    _position[0] = px;
+    _position[1] = pz;
+    _position[2] = py;
+
+    _velocity[0] = vx;
+    _velocity[1] = vz;
+    _velocity[2] = vy;
+
+    _forward_up[0] = fx;
+    _forward_up[1] = fz;
+    _forward_up[2] = fy;
+
+    _forward_up[3] = ux;
+    _forward_up[4] = uz;
+    _forward_up[5] = uy;
+    break;
+
+  default:
+    nassert_raise("invalid coordinate system");
+    return;
+  }
 
 
   make_current();
@@ -750,21 +813,85 @@ audio_3d_set_listener_attributes(PN_stdfloat px, PN_stdfloat py, PN_stdfloat pz,
 void OpenALAudioManager::
 audio_3d_get_listener_attributes(PN_stdfloat *px, PN_stdfloat *py, PN_stdfloat *pz, PN_stdfloat *vx, PN_stdfloat *vy, PN_stdfloat *vz, PN_stdfloat *fx, PN_stdfloat *fy, PN_stdfloat *fz, PN_stdfloat *ux, PN_stdfloat *uy, PN_stdfloat *uz) {
   ReMutexHolder holder(_lock);
-  *px = _position[0];
-  *py = -_position[2];
-  *pz = _position[1];
-
-  *vx = _velocity[0];
-  *vy = -_velocity[2];
-  *vz = _velocity[1];
-
-  *fx = _forward_up[0];
-  *fy = -_forward_up[2];
-  *fz = _forward_up[1];
-
-  *ux = _forward_up[3];
-  *uy = -_forward_up[5];
-  *uz = _forward_up[4];
+    CoordinateSystem cs = get_default_coordinate_system();
+
+  switch (cs) {
+  case CS_yup_right:
+    *px = _position[0];
+    *py = _position[1];
+    *pz = _position[2];
+
+    *vx = _velocity[0];
+    *vy = _velocity[1];
+    *vz = _velocity[2];
+
+    *fx = _forward_up[0];
+    *fy = _forward_up[1];
+    *fz = _forward_up[2];
+
+    *ux = _forward_up[3];
+    *uy = _forward_up[4];
+    *uz = _forward_up[5];
+    break;
+
+  case CS_zup_right:
+    *px = _position[0];
+    *py = -_position[2];
+    *pz = _position[1];
+
+    *vx = _velocity[0];
+    *vy = -_velocity[2];
+    *vz = _velocity[1];
+
+    *fx = _forward_up[0];
+    *fy = -_forward_up[2];
+    *fz = _forward_up[1];
+
+    *ux = _forward_up[3];
+    *uy = -_forward_up[5];
+    *uz = _forward_up[4];
+    break;
+
+  case CS_yup_left:
+    *px = _position[0];
+    *py = _position[1];
+    *pz = -_position[2];
+
+    *vx = _velocity[0];
+    *vy = _velocity[1];
+    *vz = -_velocity[2];
+
+    *fx = _forward_up[0];
+    *fy = _forward_up[1];
+    *fz = -_forward_up[2];
+
+    *ux = _forward_up[3];
+    *uy = _forward_up[4];
+    *uz = -_forward_up[5];
+    break;
+
+  case CS_zup_left:
+    *px = _position[0];
+    *py = _position[2];
+    *pz = _position[1];
+
+    *vx = _velocity[0];
+    *vy = _velocity[2];
+    *vz = _velocity[1];
+
+    *fx = _forward_up[0];
+    *fy = _forward_up[2];
+    *fz = _forward_up[1];
+
+    *ux = _forward_up[3];
+    *uy = _forward_up[5];
+    *uz = _forward_up[4];
+    break;
+
+  default:
+    nassert_raise("invalid coordinate system");
+    return;
+  }
 }
 
 /**

+ 48 - 7
panda/src/audiotraits/openalAudioSound.cxx

@@ -14,6 +14,7 @@
 
 // Panda Headers
 #include "throw_event.h"
+#include "coordinateSystem.h"
 #include "openalAudioSound.h"
 #include "openalAudioManager.h"
 
@@ -745,13 +746,53 @@ length() const {
 void OpenALAudioSound::
 set_3d_attributes(PN_stdfloat px, PN_stdfloat py, PN_stdfloat pz, PN_stdfloat vx, PN_stdfloat vy, PN_stdfloat vz) {
   ReMutexHolder holder(OpenALAudioManager::_lock);
-  _location[0] = px;
-  _location[1] = pz;
-  _location[2] = -py;
-
-  _velocity[0] = vx;
-  _velocity[1] = vz;
-  _velocity[2] = -vy;
+  CoordinateSystem cs = get_default_coordinate_system();
+
+  switch (cs) {
+  case CS_yup_right:
+    _location[0] = px;
+    _location[1] = py;
+    _location[2] = pz;
+
+    _velocity[0] = vx;
+    _velocity[1] = vy;
+    _velocity[2] = vz;
+    break;
+
+  case CS_zup_right:
+    _location[0] = px;
+    _location[1] = pz;
+    _location[2] = -py;
+
+    _velocity[0] = vx;
+    _velocity[1] = vz;
+    _velocity[2] = -vy;
+    break;
+
+  case CS_yup_left:
+    _location[0] = px;
+    _location[1] = py;
+    _location[2] = -pz;
+
+    _velocity[0] = vx;
+    _velocity[1] = vy;
+    _velocity[2] = -vz;
+    break;
+
+  case CS_zup_left:
+    _location[0] = px;
+    _location[1] = pz;
+    _location[2] = py;
+
+    _velocity[0] = vx;
+    _velocity[1] = vz;
+    _velocity[2] = vy;
+    break;
+
+  default:
+    nassert_raise("invalid coordinate system");
+    return;
+  }
 
   if (is_playing()) {
     _manager->make_current();

+ 11 - 3
panda/src/cocoadisplay/cocoaGraphicsWindow.mm

@@ -774,7 +774,10 @@ set_properties_now(WindowProperties &properties) {
     if (!_properties.get_fullscreen()) {
       // We use the view, not the window, to convert the frame size, expressed
       // in pixels, into points as the "dpi awareness" is managed by the view.
-      NSSize size = [_view convertSizeFromBacking:NSMakeSize(width, height)];
+      NSSize size = NSMakeSize(width, height);
+      if (dpi_aware) {
+        size = [_view convertSizeFromBacking:NSMakeSize(width, height)];
+      }
       if (_window != nil) {
         [_window setContentSize:size];
       }
@@ -782,7 +785,8 @@ set_properties_now(WindowProperties &properties) {
 
       if (cocoadisplay_cat.is_debug()) {
         cocoadisplay_cat.debug()
-          << "Setting size to " << width << ", " << height << "\n";
+          << "Setting size to " << width << " x " << height
+          << " (scaled to " << size.width << " x " << size.height << ")\n";
       }
 
       // Cocoa doesn't send an event, and the other resize-window handlers
@@ -1379,7 +1383,11 @@ handle_resize_event() {
     [_view setFrameSize:contentRect.size];
   }
 
-  NSRect frame = [_view convertRectToBacking:[_view bounds]];
+  NSRect frame = [_view bounds];
+
+  if (dpi_aware) {
+    frame = [_view convertRectToBacking:frame];
+  }
 
   WindowProperties properties;
   bool changed = false;

+ 8 - 2
panda/src/display/shaderInputBinding_impls.cxx

@@ -2451,8 +2451,10 @@ make_binding_glsl(const InternalName *name, const ShaderType *type) {
           mode_mask = (1 << TextureStage::M_height)
                     | (1 << TextureStage::M_normal_height);
         }
-        else if (pieces[1].compare(7, string::npos, "Selector") == 0) {
-          mode_mask = (1 << TextureStage::M_selector);
+        else if (pieces[1].compare(7, string::npos, "MetallicRoughness") == 0 ||
+                 pieces[1].compare(7, string::npos, "Selector") == 0) {
+          mode_mask = (1 << TextureStage::M_metallic_roughness)
+                    | (1 << TextureStage::M_occlusion_metallic_roughness);
         }
         else if (pieces[1].compare(7, string::npos, "Gloss") == 0) {
           mode_mask = (1 << TextureStage::M_gloss)
@@ -2462,6 +2464,10 @@ make_binding_glsl(const InternalName *name, const ShaderType *type) {
         else if (pieces[1].compare(7, string::npos, "Emission") == 0) {
           mode_mask = (1 << TextureStage::M_emission);
         }
+        else if (pieces[1].compare(7, string::npos, "Occlusion") == 0) {
+          mode_mask = (1 << TextureStage::M_occlusion)
+                    | (1 << TextureStage::M_occlusion_metallic_roughness);
+        }
         else {
           return report_parameter_error(name, type, "unrecognized parameter name");
         }

+ 1 - 1
panda/src/display/subprocessWindowBuffer.cxx

@@ -110,7 +110,7 @@ new_buffer(int &fd, size_t &mmap_size, string &filename,
   mmap_size = temp._mmap_size;
 
   // Ensure the disk file is large enough.
-  size_t zero_size = 1024;
+  const size_t zero_size = 1024;
   char zero[zero_size];
   memset(zero, 0, zero_size);
   for (size_t bi = 0; bi < mmap_size; bi += zero_size) {

+ 4 - 1
panda/src/doc/eggSyntax.txt

@@ -339,7 +339,10 @@ appear before they are referenced.
      *GLOW
      *GLOSS
      *HEIGHT
-     *SELECTOR
+     *EMISSION
+     *METALLIC_ROUGHNESS
+     *OCCLUSION
+     *OCCLUSION_METALLIC_ROUGHNESS
 
     The default environment type is MODULATE, which means the texture
     color is multiplied with the base polygon (or vertex) color.  This

+ 20 - 7
panda/src/egg/eggTexture.cxx

@@ -561,11 +561,11 @@ affects_polygon_alpha() const {
   case ET_height:
   case ET_normal_gloss:
   case ET_emission:
+  case ET_occlusion:
+  case ET_metallic_roughness:
+  case ET_occlusion_metallic_roughness:
     return false;
 
-  case ET_selector:
-    return true;
-
   case ET_unspecified:
     break;
   }
@@ -883,8 +883,9 @@ string_env_type(const string &string) {
   } else if (cmp_nocase_uh(string, "height") == 0) {
     return ET_height;
 
-  } else if (cmp_nocase_uh(string, "selector") == 0) {
-    return ET_selector;
+  } else if (cmp_nocase_uh(string, "metallic_roughness") == 0 ||
+             cmp_nocase_uh(string, "selector") == 0) {
+    return ET_metallic_roughness;
 
   } else if (cmp_nocase_uh(string, "normal_gloss") == 0) {
     return ET_normal_gloss;
@@ -892,6 +893,12 @@ string_env_type(const string &string) {
   } else if (cmp_nocase_uh(string, "emission") == 0) {
     return ET_emission;
 
+  } else if (cmp_nocase_uh(string, "occlusion") == 0) {
+    return ET_occlusion;
+
+  } else if (cmp_nocase_uh(string, "occlusion_metallic_roughness") == 0) {
+    return ET_occlusion_metallic_roughness;
+
   } else {
     return ET_unspecified;
   }
@@ -1321,14 +1328,20 @@ ostream &operator << (ostream &out, EggTexture::EnvType type) {
   case EggTexture::ET_height:
     return out << "height";
 
-  case EggTexture::ET_selector:
-    return out << "selector";
+  case EggTexture::ET_metallic_roughness:
+    return out << "metallic_roughness";
 
   case EggTexture::ET_normal_gloss:
     return out << "normal_gloss";
 
   case EggTexture::ET_emission:
     return out << "emission";
+
+  case EggTexture::ET_occlusion:
+    return out << "occlusion";
+
+  case EggTexture::ET_occlusion_metallic_roughness:
+    return out << "occlusion_metallic_roughness";
   }
 
   nassertr(false, out);

+ 4 - 1
panda/src/egg/eggTexture.h

@@ -106,9 +106,12 @@ PUBLISHED:
     ET_glow,
     ET_gloss,
     ET_height,
-    ET_selector,
+    ET_metallic_roughness,
     ET_normal_gloss,
     ET_emission,
+    ET_occlusion,
+    ET_occlusion_metallic_roughness,
+    ET_selector = ET_metallic_roughness,
   };
   enum CombineMode {
     CM_unspecified,

+ 10 - 2
panda/src/egg2pg/eggLoader.cxx

@@ -1579,8 +1579,8 @@ make_texture_stage(const EggTexture *egg_tex) {
     stage->set_mode(TextureStage::M_height);
     break;
 
-  case EggTexture::ET_selector:
-    stage->set_mode(TextureStage::M_selector);
+  case EggTexture::ET_metallic_roughness:
+    stage->set_mode(TextureStage::M_metallic_roughness);
     break;
 
   case EggTexture::ET_normal_gloss:
@@ -1591,6 +1591,14 @@ make_texture_stage(const EggTexture *egg_tex) {
     stage->set_mode(TextureStage::M_emission);
     break;
 
+  case EggTexture::ET_occlusion:
+    stage->set_mode(TextureStage::M_occlusion);
+    break;
+
+  case EggTexture::ET_occlusion_metallic_roughness:
+    stage->set_mode(TextureStage::M_occlusion_metallic_roughness);
+    break;
+
   case EggTexture::ET_unspecified:
     break;
   }

+ 8 - 2
panda/src/egg2pg/eggSaver.cxx

@@ -852,8 +852,8 @@ convert_primitive(const GeomVertexData *vertex_data,
         case TextureStage::M_height:
           egg_tex->set_env_type(EggTexture::ET_height);
           break;
-        case TextureStage::M_selector:
-          egg_tex->set_env_type(EggTexture::ET_selector);
+        case TextureStage::M_metallic_roughness:
+          egg_tex->set_env_type(EggTexture::ET_metallic_roughness);
           break;
         case TextureStage::M_normal_gloss:
           egg_tex->set_env_type(EggTexture::ET_normal_gloss);
@@ -861,6 +861,12 @@ convert_primitive(const GeomVertexData *vertex_data,
         case TextureStage::M_emission:
           egg_tex->set_env_type(EggTexture::ET_emission);
           break;
+        case TextureStage::M_occlusion:
+          egg_tex->set_env_type(EggTexture::ET_occlusion);
+          break;
+        case TextureStage::M_occlusion_metallic_roughness:
+          egg_tex->set_env_type(EggTexture::ET_occlusion_metallic_roughness);
+          break;
         default:
           break;
         }

+ 1 - 1
panda/src/event/asyncFuture_ext.cxx

@@ -373,7 +373,7 @@ gather(PyObject *args) {
         futures.push_back(fut);
         continue;
       }
-    } else if (PyCoro_CheckExact(item)) {
+    } else if (PyObject_TypeCheck(item, &PyCoro_Type)) {
       // We allow passing in a coroutine instead of a future.  This causes it
       // to be scheduled as a task on the current task manager.
       PT(AsyncTask) task = new PythonTask(item);

+ 6 - 8
panda/src/event/pythonTask.cxx

@@ -53,12 +53,10 @@ PythonTask(PyObject *func_or_coro, const std::string &name) :
   if (func_or_coro == Py_None || PyCallable_Check(func_or_coro)) {
     _function = Py_NewRef(func_or_coro);
   }
-  else if (PyCoro_CheckExact(func_or_coro)) {
-    // We also allow passing in a coroutine, because why not.
-    _generator = Py_NewRef(func_or_coro);
-  }
-  else if (PyGen_CheckExact(func_or_coro)) {
-    // Something emulating a coroutine.
+  else if (PyCoro_CheckExact(func_or_coro) ||
+           PyGen_CheckExact(func_or_coro) ||
+           PyType_IsSubtype(Py_TYPE(func_or_coro), &PyCoro_Type)) {
+    // We also allow passing in a coroutine or something emulating it.
     _generator = Py_NewRef(func_or_coro);
   }
   else {
@@ -620,7 +618,7 @@ do_python_task() {
         Py_DECREF(str);
         Py_DECREF(str2);
       }
-      if (PyCoro_CheckExact(result)) {
+      if (PyObject_TypeCheck(result, &PyCoro_Type)) {
         // If a coroutine, am_await is possible but senseless, since we can
         // just call send(None) on the coroutine itself.
         _generator = result;
@@ -747,7 +745,7 @@ do_python_task() {
       }
 
     } else if (result == Py_None) {
-      // Bare yield means to continue next frame.
+      // Bare yield from a coroutine means to continue next frame.
       Py_DECREF(result);
       return DS_cont;
 

+ 0 - 1
panda/src/express/virtualFileMountAndroidAsset.h

@@ -74,7 +74,6 @@ private:
 
   private:
     AAsset *_asset;
-    off_t _offset;
 
     friend class VirtualFileMountAndroidAsset;
   };

+ 32 - 15
panda/src/express/zipArchive.cxx

@@ -317,7 +317,7 @@ close() {
  */
 std::string ZipArchive::
 add_subfile(const std::string &subfile_name, const Filename &filename,
-            int compression_level) {
+            int compression_level, size_t data_alignment) {
   nassertr(is_write_valid(), std::string());
 
 #ifndef HAVE_ZLIB
@@ -337,7 +337,7 @@ add_subfile(const std::string &subfile_name, const Filename &filename,
     return std::string();
   }
 
-  std::string name = add_subfile(subfile_name, in, compression_level);
+  std::string name = add_subfile(subfile_name, in, compression_level, data_alignment);
   vfs->close_read_file(in);
   return name;
 }
@@ -356,7 +356,7 @@ add_subfile(const std::string &subfile_name, const Filename &filename,
  */
 std::string ZipArchive::
 add_subfile(const std::string &subfile_name, std::istream *subfile_data,
-            int compression_level) {
+            int compression_level, size_t data_alignment) {
   nassertr(is_write_valid(), string());
 
 #ifndef HAVE_ZLIB
@@ -367,14 +367,14 @@ add_subfile(const std::string &subfile_name, std::istream *subfile_data,
 
   std::string name = standardize_subfile_name(subfile_name);
   if (!name.empty()) {
-    Subfile *subfile = new Subfile(subfile_name, compression_level);
+    Subfile *subfile = new Subfile(subfile_name, compression_level, data_alignment);
 
     // Write it straight away, overwriting the index at the end of the file.
     // This index will be rewritten at the next call to flush() or close().
     std::streampos fpos = _index_start;
     _write->seekp(fpos);
 
-    if (!subfile->write_header(*_write, fpos)) {
+    if (!subfile->write_header(*_write, fpos, data_alignment)) {
       delete subfile;
       return "";
     }
@@ -406,7 +406,7 @@ add_subfile(const std::string &subfile_name, std::istream *subfile_data,
  */
 string ZipArchive::
 update_subfile(const std::string &subfile_name, const Filename &filename,
-               int compression_level) {
+               int compression_level, size_t data_alignment) {
   nassertr(is_write_valid(), string());
 
 #ifndef HAVE_ZLIB
@@ -431,7 +431,7 @@ update_subfile(const std::string &subfile_name, const Filename &filename,
 
     // The subfile does not already exist or it is different from the source
     // file.  Add the new source file.
-    Subfile *subfile = new Subfile(name, compression_level);
+    Subfile *subfile = new Subfile(name, compression_level, data_alignment);
     add_new_subfile(subfile, compression_level);
   }
 
@@ -764,7 +764,7 @@ repack() {
     // the checksum and sizes.
     subfile->_flags &= ~SF_data_descriptor;
 
-    if (!subfile->write_header(temp, fpos)) {
+    if (!subfile->write_header(temp, fpos, subfile->_data_alignment)) {
       success = false;
       continue;
     }
@@ -1669,10 +1669,11 @@ write_index(std::ostream &write, std::streampos &fpos) {
  * Creates a new subfile record.
  */
 ZipArchive::Subfile::
-Subfile(const std::string &name, int compression_level) :
+Subfile(const std::string &name, int compression_level, size_t data_alignment) :
   _name(name),
   _timestamp(dos_epoch),
-  _compression_method((compression_level > 0) ? CM_deflate : CM_store)
+  _compression_method((compression_level > 0) ? CM_deflate : CM_store),
+  _data_alignment(data_alignment)
 {
   // If the name contains any non-ASCII characters, we set the UTF-8 flag.
   for (char c : name) {
@@ -2096,7 +2097,7 @@ write_index(std::ostream &write, streampos &fpos) {
  * than the actual size of the subfile).
  */
 bool ZipArchive::Subfile::
-write_header(std::ostream &write, std::streampos &fpos) {
+write_header(std::ostream &write, std::streampos &fpos, size_t data_alignment) {
   nassertr(write.tellp() == fpos, false);
 
   std::string encoded_name;
@@ -2109,13 +2110,29 @@ write_header(std::ostream &write, std::streampos &fpos) {
   std::streamoff header_size = 30 + encoded_name.size();
 
   StreamWriter writer(write);
-  int modulo = (fpos + header_size) % 4;
-  if (!is_compressed() && modulo != 0) {
+  if (!is_compressed()) {
     // Align uncompressed files to 4-byte boundary.  We don't really need to do
     // this, but it's needed when producing .apk files, and it doesn't really
     // cause harm to do it in other cases as well.
-    writer.pad_bytes(4 - modulo);
-    fpos += (4 - modulo);
+    if (data_alignment < 4) {
+      data_alignment = 4;
+    }
+    else if ((data_alignment % 4) != 0) {
+      data_alignment *= 2;
+      if ((data_alignment % 4) != 0) {
+        data_alignment *= 2;
+      }
+    }
+  }
+
+  if (data_alignment > 0) {
+    // The data follows the header directly, so the actual padding has to be
+    // inserted before the header.
+    int modulo = (fpos + header_size) % data_alignment;
+    if (modulo != 0) {
+      writer.pad_bytes(data_alignment - modulo);
+      fpos += (data_alignment - modulo);
+    }
   }
 
   _header_start = fpos;

+ 7 - 5
panda/src/express/zipArchive.h

@@ -66,11 +66,11 @@ PUBLISHED:
   INLINE bool get_record_timestamp() const;
 
   std::string add_subfile(const std::string &subfile_name, const Filename &filename,
-                          int compression_level);
+                          int compression_level, size_t data_alignment=0);
   std::string add_subfile(const std::string &subfile_name, std::istream *subfile_data,
-                          int compression_level);
+                          int compression_level, size_t data_alignment=0);
   std::string update_subfile(const std::string &subfile_name, const Filename &filename,
-                             int compression_level);
+                             int compression_level, size_t data_alignment=0);
 
 #ifdef HAVE_OPENSSL
   bool add_jar_signature(const Filename &certificate, const Filename &pkey,
@@ -152,7 +152,8 @@ private:
   class Subfile {
   public:
     Subfile() = default;
-    Subfile(const std::string &name, int compression_level);
+    Subfile(const std::string &name, int compression_level,
+            size_t data_alignment=0);
 
     INLINE bool operator < (const Subfile &other) const;
 
@@ -160,7 +161,7 @@ private:
     bool read_header(std::istream &read);
     bool verify_data(std::istream &read);
     bool write_index(std::ostream &write, std::streampos &fpos);
-    bool write_header(std::ostream &write, std::streampos &fpos);
+    bool write_header(std::ostream &write, std::streampos &fpos, size_t data_alignment=0);
     bool write_data(std::ostream &write, std::istream *read,
                     std::streampos &fpos, int compression_level);
     INLINE bool is_compressed() const;
@@ -180,6 +181,7 @@ private:
     std::string _comment;
     int _flags = SF_data_descriptor;
     CompressionMethod _compression_method = CM_store;
+    size_t _data_alignment = 0;
   };
 
   void add_new_subfile(Subfile *subfile, int compression_level);

+ 12 - 0
panda/src/glstuff/glGraphicsStateGuardian_src.cxx

@@ -7906,6 +7906,12 @@ release_shader_buffer(BufferContext *bc) {
     _current_sbuffer_index = 0;
   }
 
+  for (GLuint &index : _current_sbuffer_base) {
+    if (index == gbc->_index) {
+      index = 0;
+    }
+  }
+
   _glDeleteBuffers(1, &gbc->_index);
   report_my_gl_errors();
 
@@ -7945,6 +7951,12 @@ release_shader_buffers(const pvector<BufferContext *> &contexts) {
       _current_sbuffer_index = 0;
     }
 
+    for (GLuint &index : _current_sbuffer_base) {
+      if (index == gbc->_index) {
+        index = 0;
+      }
+    }
+
     if (debug) {
       GLCAT.debug()
         << "deleting shader buffer " << gbc->_index << "\n";

+ 8 - 2
panda/src/gobj/textureStage.cxx

@@ -519,14 +519,20 @@ operator << (ostream &out, TextureStage::Mode mode) {
   case TextureStage::M_height:
     return out << "height";
 
-  case TextureStage::M_selector:
-    return out << "selector";
+  case TextureStage::M_metallic_roughness:
+    return out << "metallic_roughness";
 
   case TextureStage::M_normal_gloss:
     return out << "normal_gloss";
 
   case TextureStage::M_emission:
     return out << "emission";
+
+  case TextureStage::M_occlusion:
+    return out << "occlusion";
+
+  case TextureStage::M_occlusion_metallic_roughness:
+    return out << "occlusion_metallic_roughness";
   }
 
   return out << "**invalid Mode(" << (int)mode << ")**";

+ 5 - 1
panda/src/gobj/textureStage.h

@@ -61,10 +61,14 @@ PUBLISHED:
     M_glow,         // Rarely used: modulate_glow  is more efficient.
     M_gloss,        // Rarely used: modulate_gloss is more efficient.
     M_height,       // Rarely used: normal_height  is more efficient.
-    M_selector,
+    M_metallic_roughness, // metalness in B, roughness in G
     M_normal_gloss,
 
     M_emission,
+    M_occlusion, // In red channel
+    M_occlusion_metallic_roughness,
+
+    M_selector = M_metallic_roughness,
   };
 
   enum CombineMode {

+ 3 - 1
panda/src/grutil/multitexReducer.cxx

@@ -712,9 +712,11 @@ make_texture_layer(const NodePath &render,
   case TextureStage::M_glow:
   case TextureStage::M_gloss:
   case TextureStage::M_height:
-  case TextureStage::M_selector:
+  case TextureStage::M_metallic_roughness:
   case TextureStage::M_normal_gloss:
   case TextureStage::M_emission:
+  case TextureStage::M_occlusion:
+  case TextureStage::M_occlusion_metallic_roughness:
     // Don't know what to do with these funny modes.  We should probably raise
     // an exception or something.  Fall through for now.
 

+ 10 - 0
panda/src/ode/odeRayGeom.I

@@ -63,6 +63,11 @@ get_params(int &first_contact, int &backface_cull) const {
   dGeomRayGetParams(_id, &first_contact, &backface_cull);
 }
 
+INLINE void OdeRayGeom::
+set_first_contact(int first_contact) {
+  dGeomRaySetFirstContact(_id, first_contact);
+}
+
 INLINE int OdeRayGeom::
 get_first_contact() const {
   int fc, bc;
@@ -70,6 +75,11 @@ get_first_contact() const {
   return fc;
 }
 
+INLINE void OdeRayGeom::
+set_backface_cull(int backface_cull) {
+  dGeomRaySetBackfaceCull(_id, backface_cull);
+}
+
 INLINE int OdeRayGeom::
 get_backface_cull() const {
   int fc, bc;

+ 4 - 2
panda/src/ode/odeRayGeom.h

@@ -41,9 +41,11 @@ PUBLISHED:
   INLINE void get(LVecBase3f &start, LVecBase3f &dir) const;
   INLINE LVecBase3f get_start() const;
   INLINE LVecBase3f get_direction() const;
-  INLINE void set_params(int first_contact, int backface_cull);
-  INLINE void get_params(int &first_contact, int &backface_cull) const;
+  [[deprecated]] INLINE void set_params(int first_contact, int backface_cull);
+  [[deprecated]] INLINE void get_params(int &first_contact, int &backface_cull) const;
+  INLINE void set_first_contact(int first_contact);
   INLINE int get_first_contact() const;
+  INLINE void set_backface_cull(int backface_cull);
   INLINE int get_backface_cull() const;
   INLINE void set_closest_hit(int closest_hit);
   INLINE int get_closest_hit();

+ 16 - 2
panda/src/pgraphnodes/shaderGenerator.cxx

@@ -732,6 +732,10 @@ analyze_renderstate(ShaderKey &key, const RenderState *rs) {
     case TextureStage::M_emission:
       info._flags = ShaderKey::TF_map_emission;
       break;
+    case TextureStage::M_occlusion:
+    case TextureStage::M_occlusion_metallic_roughness:
+      info._flags = ShaderKey::TF_map_occlusion;
+      break;
     default:
       break;
     }
@@ -798,9 +802,13 @@ analyze_renderstate(ShaderKey &key, const RenderState *rs) {
 
   // Decide whether to separate ambient and diffuse calculations.
   if (have_ambient) {
-    if (key._flags & Material::F_ambient) {
+    if (key._texture_flags & ShaderKey::TF_map_occlusion) {
       key._flags |= ShaderKey::F_have_separate_ambient;
-    } else {
+    }
+    else if (key._flags & Material::F_ambient) {
+      key._flags |= ShaderKey::F_have_separate_ambient;
+    }
+    else {
       if (key._flags & Material::F_diffuse) {
         key._flags |= ShaderKey::F_have_separate_ambient;
       }
@@ -1841,6 +1849,12 @@ synthesize_shader(const RenderState *rs, const GeomVertexAnimationSpec &anim) {
       text << "\t result *= saturate(2 * (tex" << map_index_glow << ".a - 0.5));\n";
     }
     if (key._flags & ShaderKey::F_have_separate_ambient) {
+      for (size_t i = 0; i < key._textures.size(); ++i) {
+        ShaderKey::TextureInfo &tex = key._textures[i];
+        if (tex._flags & ShaderKey::TF_map_occlusion) {
+          text << "\t tot_ambient *= tex" << i << ".r;\n";
+        }
+      }
       if (key._flags & Material::F_ambient) {
         text << "\t result += tot_ambient * attr_material[0];\n";
       } else if (key._flags & ShaderKey::F_vertex_color) {

+ 1 - 0
panda/src/pgraphnodes/shaderGenerator.h

@@ -136,6 +136,7 @@ protected:
       TF_map_glow     = 0x080,
       TF_map_gloss    = 0x100,
       TF_map_emission = 0x001000000,
+      TF_map_occlusion = 0x002000000,
       TF_uses_color   = 0x200,
       TF_uses_primary_color = 0x400,
       TF_uses_last_saved_result = 0x800,

+ 6 - 1
panda/src/pgui/pgSliderBar.cxx

@@ -693,6 +693,11 @@ advance_scroll() {
  */
 void PGSliderBar::
 advance_page() {
+  // Do not try to advance the page while dragging
+  if (_dragging) {
+    return;
+  }
+
   // Is the mouse position left or right of the current thumb position?
   LPoint3 mouse = mouse_to_local(_mouse_pos) - _thumb_start;
   PN_stdfloat target_ratio = mouse.dot(_axis) / _range_x;
@@ -705,7 +710,7 @@ advance_page() {
     t = min(_ratio + _page_ratio - _scroll_ratio, target_ratio);
   }
   internal_set_ratio(t);
-  if (t == target_ratio) {
+  if (_ratio == target_ratio) {
     // We made it; begin dragging from now on until the user releases the
     // mouse.
     begin_drag();

+ 6 - 0
panda/src/pipeline/threadPosixImpl.I

@@ -46,8 +46,14 @@ prepare_for_exit() {
 INLINE Thread *ThreadPosixImpl::
 get_current_thread() {
   TAU_PROFILE("Thread *ThreadPosixImpl::get_current_thread()", " ", TAU_USER);
+#ifdef ANDROID
+  // Android doesn't correctly share TLS variables across multiple DSOs, so
+  // we can't inline this.
+  return do_get_current_thread();
+#else
   Thread *thread = _current_thread;
   return (thread != nullptr) ? thread : init_current_thread();
+#endif
 }
 
 /**

+ 37 - 10
panda/src/pipeline/threadPosixImpl.cxx

@@ -33,7 +33,12 @@
 static JavaVM *java_vm = nullptr;
 #endif
 
+// See comment in header file.
+#ifdef ANDROID
+static __thread Thread *_current_thread = nullptr;
+#else
 __thread Thread *ThreadPosixImpl::_current_thread = nullptr;
+#endif
 static patomic_flag _main_thread_known = ATOMIC_FLAG_INIT;
 
 /**
@@ -53,6 +58,10 @@ ThreadPosixImpl::
     _detached = true;
   }
 
+  if (_current_thread == _parent_obj) {
+    _current_thread = nullptr;
+  }
+
   _mutex.unlock();
 }
 
@@ -214,12 +223,15 @@ bind_thread(Thread *thread) {
 
 #ifdef ANDROID
 /**
- * Attaches the thread to the Java virtual machine.  If this returns true, a
- * JNIEnv pointer can be acquired using get_jni_env().
+ * Attaches the thread to the Java virtual machine.  On success, returns a
+ * JNIEnv pointer; returns nullptr otherwise, in which case the application
+ * might not be running inside a Java VM.
  */
-bool ThreadPosixImpl::
+JNIEnv *ThreadPosixImpl::
 attach_java_vm() {
-  assert(java_vm != nullptr);
+  if (java_vm == nullptr) {
+    return nullptr;
+  }
 
   JNIEnv *env;
   std::string thread_name = _parent_obj->get_name();
@@ -232,10 +244,10 @@ attach_java_vm() {
       << "Failed to attach Java VM to thread "
       << _parent_obj->get_name() << "!\n";
     _jni_env = nullptr;
-    return false;
+    return nullptr;
   }
   _jni_env = env;
-  return true;
+  return env;
 }
 
 /**
@@ -308,7 +320,7 @@ root_func(void *data) {
 
 #ifdef ANDROID
     // Attach the Java VM to allow calling Java functions in this thread.
-    self->attach_java_vm();
+    JNIEnv *jni_env = self->attach_java_vm();
 #endif
 
     self->_parent_obj->thread_main();
@@ -331,9 +343,11 @@ root_func(void *data) {
 
 #ifdef ANDROID
     // We cannot let the thread end without detaching it.
-    if (self->_jni_env != nullptr) {
-      java_vm->DetachCurrentThread();
-      self->_jni_env = nullptr;
+    if (jni_env != nullptr) {
+      if (java_vm != nullptr) {
+        java_vm->DetachCurrentThread();
+      }
+      jni_env = nullptr;
     }
 #endif
 
@@ -341,6 +355,8 @@ root_func(void *data) {
     // might delete the parent object, and in turn, delete the ThreadPosixImpl
     // object.
     unref_delete(self->_parent_obj);
+
+    _current_thread = nullptr;
   }
 
   return nullptr;
@@ -360,6 +376,17 @@ init_current_thread() {
   return thread;
 }
 
+#ifdef ANDROID
+/**
+ *
+ */
+Thread *ThreadPosixImpl::
+do_get_current_thread() {
+  Thread *thread = _current_thread;
+  return (thread != nullptr) ? thread : init_current_thread();
+}
+#endif  // ANDROID
+
 #ifdef ANDROID
 /**
  * Called by Java when loading this library from the Java virtual machine.

+ 10 - 2
panda/src/pipeline/threadPosixImpl.h

@@ -48,7 +48,7 @@ public:
 
   INLINE static void prepare_for_exit();
 
-  INLINE static Thread *get_current_thread();
+  static Thread *get_current_thread();
   static Thread *bind_thread(Thread *thread);
   INLINE static bool is_threading_supported();
   INLINE static bool is_true_threads();
@@ -59,7 +59,7 @@ public:
 
 #ifdef ANDROID
   INLINE JNIEnv *get_jni_env() const;
-  bool attach_java_vm();
+  JNIEnv *attach_java_vm();
   static void bind_java_thread();
 #endif
 
@@ -69,6 +69,10 @@ private:
   static void *root_func(void *data);
   static Thread *init_current_thread();
 
+#ifdef ANDROID
+  static Thread *do_get_current_thread();
+#endif
+
   // There appears to be a name collision with the word "Status".
   enum PStatus {
     S_new,
@@ -88,7 +92,11 @@ private:
   JNIEnv *_jni_env;
 #endif
 
+  // Android doesn't do TLS correctly across .so boundaries, so we hide the
+  // current thread in the .cxx file.
+#ifndef ANDROID
   static __thread Thread *_current_thread;
+#endif
 };
 
 #include "threadPosixImpl.I"

+ 1 - 0
panda/src/putil/bamCache.I

@@ -257,3 +257,4 @@ mark_index_stale() {
     _index_stale_since = time(nullptr);
   }
 }
+

+ 83 - 0
panda/src/putil/bamCache.cxx

@@ -959,6 +959,89 @@ do_read_record(const Filename &cache_pathname, bool read_data) {
   return record;
 }
 
+/**
+ * Clear the model cache.
+ *
+ * Acquires the internal _lock for thread-safety (so callers do NOT need to
+ * hold the lock). If no cache root is configured (_root.empty()), returns
+ * immediately. If the cache is marked _read_only, resets only the in-memory
+ * index and emits a warning message.
+ * If the VirtualFileSystem global pointer is not available, logs an error,
+ * resets only the in-memory index, and returns immediately. Otherwise, scans
+ * the cache root directory and deletes cache files matching the known cache
+ * extensions (.bam, .txo, .sho). Removes the on-disk index file (if known)
+ * or the index reference file and clears the in-memory index reference
+ * contents. Resets the in-memory index to an empty BamCacheIndex. Marks the
+ * index stale and attempts to flush a new, empty on-disk index so other
+ * processes will observe the cleared state.
+ */
+void BamCache::
+clear() {
+  ReMutexHolder holder(_lock);
+
+  VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr();
+
+  if (_root.empty()) {
+    return;
+  }
+
+  // If the cache is read-only, only reset the in-memory index and log.
+  if (_read_only) {
+    util_cat.info() << "BamCache::clear(): cache is read-only; clearing in-memory index only.\n";
+    reset_in_memory_index();
+    return;
+  }
+
+  if (vfs == nullptr) {
+    util_cat.error() << "BamCache::clear(): VFS is not available\n";
+    reset_in_memory_index();
+    return;
+  }
+
+  // Delete cache files (.bam, .txo, .sho) in the cache root directory.
+  PT(VirtualFileList) contents = vfs->scan_directory(_root);
+  if (contents != nullptr) {
+    int num_files = contents->get_num_files();
+    for (int ci = 0; ci < num_files; ++ci) {
+      VirtualFile *file = contents->get_file(ci);
+      Filename filename = file->get_filename();
+      const std::string ext = filename.get_extension();
+
+      if (ext == "bam" || ext == "txo" || ext == "sho") {
+        Filename pathname(_root, filename);
+        if (!vfs->delete_file(pathname)) {
+          util_cat.debug()
+            << "Could not delete cache file " << pathname << "\n";
+        }
+      }
+    }
+  }
+
+  // Remove index files: the index file itself (if known) and the index ref.
+  Filename index_ref_pathname(_root, Filename("index_name.txt"));
+  if (!_index_pathname.empty()) {
+    vfs->delete_file(_index_pathname);
+    _index_pathname = Filename();
+    _index_ref_contents.clear();
+  } else {
+    vfs->delete_file(index_ref_pathname);
+  }
+
+  reset_in_memory_index();
+
+  // Try to write an empty index back to disk: mark stale then flush.
+  mark_index_stale();
+  flush_index();
+}
+
+/* Reset the in-memory cache index to an empty state (caller must hold _lock). */
+void BamCache::
+reset_in_memory_index() {
+  delete _index;
+  _index = new BamCacheIndex;
+  _index_stale_since = 0;
+}
+
 /**
  * Returns the appropriate filename to use for a cache file, given the
  * fullpath string to the source filename.

+ 3 - 1
panda/src/putil/bamCache.h

@@ -80,6 +80,8 @@ PUBLISHED:
 
   void list_index(std::ostream &out, int indent_level = 0) const;
 
+  void clear();
+
   INLINE static BamCache *get_global_ptr();
   INLINE static void consider_flush_global_index();
   INLINE static void flush_global_index();
@@ -104,7 +106,7 @@ private:
   void merge_index(BamCacheIndex *new_index);
   void rebuild_index();
   INLINE void mark_index_stale();
-
+  void reset_in_memory_index(); 
   void add_to_index(const BamCacheRecord *record);
   void remove_from_index(const Filename &source_filename);
 

+ 1 - 1
panda/src/x11display/config_x11display.cxx

@@ -113,7 +113,7 @@ ConfigVariableBool x_send_startup_notification
           "that it no longer needs to display a spinning mouse cursor."));
 
 ConfigVariableBool x_detectable_auto_repeat
-("x-detectable-auto-repeat", false,
+("x-detectable-auto-repeat", true,
  PRC_DESC("Set this true to enable detectable auto-repeat for keyboard input."));
 
 /**

+ 1 - 1
panda/src/x11display/x11GraphicsWindow.cxx

@@ -2755,7 +2755,7 @@ enable_detectable_auto_repeat() {
   if (!x_detectable_auto_repeat) {
     return;
   }
-  
+
   Bool supported;
   if (XkbSetDetectableAutoRepeat(_display, True, &supported)) {
     x11display_cat.info() << "Detectable auto-repeat enabled.\n";

+ 33 - 14
pandatool/src/assimp/assimpLoader.cxx

@@ -58,6 +58,10 @@
 #define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_ROUGHNESS_FACTOR "$mat.gltf.pbrMetallicRoughness.roughnessFactor", 0, 0
 #endif
 
+#ifndef AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE
+#define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE aiTextureType_UNKNOWN, 0
+#endif
+
 #ifndef AI_MATKEY_GLTF_ALPHAMODE
 #define AI_MATKEY_GLTF_ALPHAMODE "$mat.gltf.alphaMode", 0, 0
 #endif
@@ -325,11 +329,13 @@ load_texture(size_t index) {
 
 /**
  * Converts an aiMaterial into a RenderState.
+ * The dummy argument exists so we can pass a MATKEY macro into this function
+ * instead of just a texture type.
  */
 void AssimpLoader::
-load_texture_stage(const aiMaterial &mat, const aiTextureType &ttype,
-                   TextureStage::Mode mode, CPT(TextureAttrib) &tattr,
-                   CPT(TexMatrixAttrib) &tmattr) {
+load_texture_stage(const aiMaterial &mat, TextureStage::Mode mode,
+                   CPT(TextureAttrib) &tattr, CPT(TexMatrixAttrib) &tmattr,
+                   const aiTextureType &ttype, unsigned int dummy) {
   aiString path;
   aiTextureMapping mapping;
   unsigned int uvindex;
@@ -596,20 +602,33 @@ load_material(size_t index) {
   // And let's not forget the textures!
   CPT(TextureAttrib) tattr = DCAST(TextureAttrib, TextureAttrib::make());
   CPT(TexMatrixAttrib) tmattr;
-  load_texture_stage(mat, aiTextureType_DIFFUSE, TextureStage::M_modulate, tattr, tmattr);
-
-  // Check for an ORM map, from the glTF/OBJ importer.  glTF also puts it in the
-  // LIGHTMAP slot, despite only having the lightmap in the red channel, so we
-  // have to ignore it.
-  if (mat.GetTextureCount(aiTextureType_UNKNOWN) > 0) {
-    load_texture_stage(mat, aiTextureType_UNKNOWN, TextureStage::M_selector, tattr, tmattr);
+  load_texture_stage(mat, TextureStage::M_modulate, tattr, tmattr, aiTextureType_DIFFUSE);
+
+  // Check for an ORM map, from the glTF/OBJ importer.  glTF also erroneously
+  // puts an occlusion texture in the LIGHTMAP slot.
+  aiString roughness_path, occlusion_path;
+  aiTextureMapping roughness_mapping, occlusion_mapping;
+  unsigned int roughness_uv, occlusion_uv;
+  if (AI_SUCCESS == mat.GetTexture(AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE, &roughness_path, &roughness_mapping, &roughness_uv)) {
+    if (AI_SUCCESS == mat.GetTexture(aiTextureType_LIGHTMAP, 0, &occlusion_path, &occlusion_mapping, &occlusion_uv)) {
+      if (roughness_path == occlusion_path &&
+          roughness_mapping == occlusion_mapping &&
+          roughness_uv == occlusion_uv) {
+        load_texture_stage(mat, TextureStage::M_occlusion_metallic_roughness, tattr, tmattr, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE);
+      } else {
+        load_texture_stage(mat, TextureStage::M_metallic_roughness, tattr, tmattr, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE);
+        load_texture_stage(mat, TextureStage::M_occlusion, tattr, tmattr, aiTextureType_LIGHTMAP);
+      }
+    } else {
+      load_texture_stage(mat, TextureStage::M_metallic_roughness, tattr, tmattr, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE);
+    }
   } else {
-    load_texture_stage(mat, aiTextureType_LIGHTMAP, TextureStage::M_modulate, tattr, tmattr);
+    load_texture_stage(mat, TextureStage::M_modulate, tattr, tmattr, aiTextureType_LIGHTMAP);
   }
 
-  load_texture_stage(mat, aiTextureType_NORMALS, TextureStage::M_normal, tattr, tmattr);
-  load_texture_stage(mat, aiTextureType_EMISSIVE, TextureStage::M_emission, tattr, tmattr);
-  load_texture_stage(mat, aiTextureType_HEIGHT, TextureStage::M_height, tattr, tmattr);
+  load_texture_stage(mat, TextureStage::M_normal, tattr, tmattr, aiTextureType_NORMALS);
+  load_texture_stage(mat, TextureStage::M_emission, tattr, tmattr, aiTextureType_EMISSIVE);
+  load_texture_stage(mat, TextureStage::M_height, tattr, tmattr, aiTextureType_HEIGHT);
   if (tattr->get_num_on_stages() > 0) {
     state = state->add_attrib(tattr);
   }

+ 3 - 3
pandatool/src/assimp/assimpLoader.h

@@ -78,9 +78,9 @@ private:
   const aiNode *find_node(const aiNode &root, const aiString &name);
 
   void load_texture(size_t index);
-  void load_texture_stage(const aiMaterial &mat, const aiTextureType &ttype,
-                          TextureStage::Mode mode, CPT(TextureAttrib) &tattr,
-                          CPT(TexMatrixAttrib) &tmattr);
+  void load_texture_stage(const aiMaterial &mat, TextureStage::Mode mode,
+                          CPT(TextureAttrib) &tattr, CPT(TexMatrixAttrib) &tmattr,
+                          const aiTextureType &ttype, unsigned int dummy=0);
   void load_material(size_t index);
   void create_joint(Character *character, CharacterJointBundle *bundle, PartGroup *parent, const aiNode &node);
   void create_anim_channel(const aiAnimation &anim, AnimBundle *bundle, AnimGroup *parent, const aiNode &node);

+ 4 - 0
pandatool/src/converter/CMakeLists.txt

@@ -21,3 +21,7 @@ install(TARGETS p3converter
   INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d
   ARCHIVE COMPONENT ToolsDevel)
 install(FILES ${P3CONVERTER_HEADERS} COMPONENT ToolsDevel DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)
+
+add_executable(txo-converter txoConverter.cxx txoConverter.h)
+target_link_libraries(txo-converter p3progbase panda)
+install(TARGETS txo-converter EXPORT Tools COMPONENT Tools DESTINATION ${CMAKE_INSTALL_BINDIR})

+ 148 - 0
pandatool/src/converter/txoConverter.cxx

@@ -0,0 +1,148 @@
+/**
+ * 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 txoConverter.cxx
+ * @author RegDogg
+ * @date 2025-11-10
+ */
+
+#include "txoConverter.h"
+#include "pnmFileTypeRegistry.h"
+#include "pnmFileType.h"
+
+/**
+ *
+ */
+TxoConverter::
+TxoConverter() : WithOutputFile(true, false, true) {
+  set_program_brief("convert various image formats to .txo file format");
+  set_program_description
+    ("This program reads the image date from the input file and "
+     "outputs a txo files, suitable for viewing in Panda.");
+
+  clear_runlines();
+  add_runline("[opts] input output");
+  add_runline("[opts] -o output input");
+
+  add_option
+    ("o", "filename", 0,
+     "Specify the filename to which the resulting .bam file will be written.  "
+     "If this option is omitted, the last parameter name is taken to be the "
+     "name of the output file.",
+     &TxoConverter::dispatch_filename, &_got_output_filename, &_output_filename);
+
+  add_option
+    ("alpha", "filename", 0,
+     "Apply an RGB alpha file for image types such as JPEG.",
+     &TxoConverter::dispatch_filename, &_got_rgb_filename, &_rgb_filename);
+}
+
+/**
+ *
+ */
+void TxoConverter::
+run() {
+  nassertv(has_output_filename());
+  Filename fullpath = Filename(_image_filename.get_fullpath());
+
+  nout << "Reading " << fullpath << "...\n";
+
+  PNMFileType *type = PNMFileTypeRegistry::get_global_ptr()->get_type_from_extension(fullpath);
+  if (type == nullptr) {
+    nout << "Cannot determine type of image file " << fullpath << ".\n";
+    nout << "Currently supported image types:\n";
+    PNMFileTypeRegistry::get_global_ptr()->write(nout, 2);
+    nout << "\n";
+    return;
+  }
+
+  PT(Texture) tex = new Texture("original image");
+
+  if (_got_rgb_filename) {
+    PNMFileType *type = PNMFileTypeRegistry::get_global_ptr()->get_type_from_extension(_rgb_filename);
+    if (type == nullptr) {
+      nout << "Image file type '" << _rgb_filename << "' is unknown.\n";
+      return;
+    }
+    tex->read(_image_filename, _rgb_filename.get_fullpath(), 0, 0, LoaderOptions());
+  }
+  else {
+    tex->read(_image_filename, LoaderOptions());
+  }
+  tex->get_ram_image();
+
+  convert_txo(tex);
+
+}
+
+
+/**
+ * If the indicated Texture was not already loaded from a txo file, writes it
+ * to a txo file and updates the Texture object to reference the new file.
+ */
+void TxoConverter::
+convert_txo(Texture *tex) {
+  if (!tex->get_loaded_from_txo()) {
+
+    Filename output = get_output_filename();
+    output.make_dir();
+
+    if (tex->write(output)) {
+      nout << "Writing " << output << "...\n";
+      tex->set_loaded_from_txo();
+      tex->set_fullpath(output);
+      tex->clear_alpha_fullpath();
+
+      tex->set_filename(output);
+      tex->clear_alpha_filename();
+    }
+  }
+}
+
+/**
+ *
+ */
+bool TxoConverter::
+handle_args(ProgramBase::Args &args) {
+  if (!_got_output_filename && args.size() > 1) {
+    _got_output_filename = true;
+    _output_filename = Filename::from_os_specific(args.back());
+    args.pop_back();
+  }
+
+  if (!_got_output_filename) {
+    nout << "You must specify an output path.";
+    return false;
+  }
+
+  if ((_output_filename.get_extension() != "txo")) {
+    nout << "Output filename " << _output_filename << " must end in .txo\n";
+    return false;
+  }
+
+  if (args.empty()) {
+    nout << "You must specify the image file to read on the command line.\n";
+    return false;
+  }
+
+  if (args.size() != 1) {
+    nout << "Specify only one image on the command line.\n";
+    return false;
+  }
+
+  _image_filename = Filename::from_os_specific(args[0]);
+
+  return true;
+}
+
+int main(int argc, char *argv[]) {
+  TxoConverter prog;
+  prog.parse_command_line(argc, argv);
+  prog.run();
+  return 0;
+}

+ 43 - 0
pandatool/src/converter/txoConverter.h

@@ -0,0 +1,43 @@
+/**
+ * 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 txoConverter.h
+ * @author RegDogg
+ * @date 2025-11-10
+ */
+
+#ifndef TXOCONVERTER_H
+#define TXOCONVERTER_H
+
+#include "pandatoolbase.h"
+#include "programBase.h"
+#include "filename.h"
+#include "withOutputFile.h"
+#include "textureAttrib.h"
+
+/**
+ *
+ */
+class TxoConverter : public ProgramBase, public WithOutputFile {
+public:
+  TxoConverter();
+    
+  void run();
+
+  protected:
+  virtual bool handle_args(Args &args);
+
+  private:
+  void convert_txo(Texture *tex);
+
+  Filename _image_filename;
+  bool _got_rgb_filename;
+  Filename _rgb_filename;
+};
+
+#endif

+ 85 - 25
pandatool/src/deploy-stub/android_main.cxx

@@ -27,11 +27,17 @@
 #include <sys/mman.h>
 #include <android/log.h>
 
-#include <thread>
-
 // Leave room for future expansion.
 #define MAX_NUM_POINTERS 24
 
+/* Stored in the flags field of the blobinfo structure below. */
+enum Flags {
+  F_log_append = 1,
+  F_log_filename_strftime = 2,
+  F_keep_docstrings = 4,
+  F_python_verbose = 8,
+};
+
 // Define an exposed symbol where we store the offset to the module data.
 extern "C" {
   __attribute__((__visibility__("default"), used))
@@ -50,8 +56,8 @@ extern "C" {
   } blobinfo = {(uint64_t)-1};
 }
 
-// Defined in android_log.c
-extern "C" PyObject *PyInit_android_log();
+// Defined in android_support.cxx
+extern "C" PyObject *PyInit_android_support();
 
 /**
  * Maps the binary blob at the given memory address to memory, and returns the
@@ -61,11 +67,16 @@ static void *map_blob(const char *path, off_t offset, size_t size) {
   FILE *runtime = fopen(path, "rb");
   assert(runtime != NULL);
 
-  void *blob = (void *)mmap(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fileno(runtime), offset);
+  // Align the offset down to the page size boundary.
+  size_t page_size = getpagesize();
+  off_t aligned = offset & ~((off_t)page_size - 1);
+  size_t delta = (size_t)(offset - aligned);
+
+  void *blob = (void *)mmap(0, size + delta, PROT_READ, MAP_PRIVATE, fileno(runtime), aligned);
   assert(blob != MAP_FAILED);
 
   fclose(runtime);
-  return blob;
+  return (void *)((uintptr_t)blob + delta);
 }
 
 /**
@@ -118,7 +129,8 @@ void android_main(struct android_app *app) {
   PT(Thread) current_thread = Thread::bind_thread(thread_name, "android_app");
 
   android_cat.info()
-    << "New native activity started on " << *current_thread << "\n";
+    << "New native activity started on " << *current_thread
+    << " (" << current_thread << ")\n";
 
   // Fetch the data directory.
   jmethodID get_appinfo = env->GetMethodID(activity_class, "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;");
@@ -180,10 +192,29 @@ void android_main(struct android_app *app) {
   android_cat.info() << "Path to native library: " << lib_path << "\n";
   ExecutionEnvironment::set_binary_name(lib_path);
 
-  // Map the blob to memory
-  void *blob = map_blob(lib_path, (off_t)blobinfo.blob_offset, (size_t)blobinfo.blob_size);
+  // Nowadays we store the blob in a raw resource.
+  methodID = env->GetMethodID(activity_class, "mapBlobFromResource", "(J)J");
+  assert(methodID != nullptr);
+  void *blob = (void *)env->CallLongMethod(activity->clazz, methodID, blobinfo.blob_offset);
+
+  if (blob == nullptr) {
+    // Try the old method otherwise.
+    blob = map_blob(lib_path, (off_t)blobinfo.blob_offset, (size_t)blobinfo.blob_size);
+  }
   env->ReleaseStringUTFChars(lib_path_jstr, lib_path);
-  assert(blob != NULL);
+  assert(blob != nullptr);
+
+  // This should always be aligned, but just in case...
+  void *blob_copy = nullptr;
+  if ((((uintptr_t)blob) & (sizeof(void *) - 1)) != 0) {
+    android_cat.warning()
+      << "Blob with offset " << blobinfo.blob_offset
+      << " and size " << blobinfo.blob_size << " is not aligned!\n";
+
+    blob_copy = malloc(blobinfo.blob_size);
+    memcpy(blob_copy, blob, blobinfo.blob_size);
+    blob = blob_copy;
+  }
 
   assert(blobinfo.num_pointers <= MAX_NUM_POINTERS);
   for (uint32_t i = 0; i < blobinfo.num_pointers; ++i) {
@@ -224,17 +255,35 @@ void android_main(struct android_app *app) {
   get_model_path().append_directory(asset_dir);
 
   // Offset the pointers in the module table using the base mmap address.
-  struct _frozen *moddef = (struct _frozen *)blobinfo.pointers[0];
-  while (moddef->name) {
-    moddef->name = (char *)((uintptr_t)moddef->name + (uintptr_t)blob);
-    if (moddef->code != nullptr) {
-      moddef->code = (unsigned char *)((uintptr_t)moddef->code + (uintptr_t)blob);
+  // We did a read-only mmap, so we have to create a copy of this table.
+  // First count how many modules there are.
+  struct _frozen *src_moddef = (struct _frozen *)blobinfo.pointers[0];
+  struct _frozen *dst_moddef;
+  struct _frozen *new_modules = nullptr;
+  if (blob_copy != nullptr) {
+    // We already made a copy, so we can just write to this.
+    dst_moddef = src_moddef;
+  } else {
+    size_t num_modules = 0;
+    while (src_moddef->name) {
+      num_modules++;
+      src_moddef++;
     }
-    //__android_log_print(ANDROID_LOG_DEBUG, "Panda3D", "MOD: %s %p %d\n", moddef->name, (void*)moddef->code, moddef->size);
-    moddef++;
+
+    new_modules = (struct _frozen *)malloc(sizeof(struct _frozen) * (num_modules + 1));
+    memcpy(new_modules, blobinfo.pointers[0], sizeof(struct _frozen) * (num_modules + 1));
+    dst_moddef = new_modules;
   }
+  PyImport_FrozenModules = dst_moddef;
 
-  PyImport_FrozenModules = (struct _frozen *)blobinfo.pointers[0];
+  while (dst_moddef->name) {
+    dst_moddef->name = (char *)((uintptr_t)dst_moddef->name + (uintptr_t)blob);
+    if (dst_moddef->code != nullptr) {
+      dst_moddef->code = (unsigned char *)((uintptr_t)dst_moddef->code + (uintptr_t)blob);
+    }
+    //__android_log_print(ANDROID_LOG_DEBUG, "Panda3D", "MOD: %s %p %d\n", dst_moddef->name, (void*)dst_moddef->code, dst_moddef->size);
+    dst_moddef++;
+  }
 
   PyPreConfig preconfig;
   PyPreConfig_InitIsolatedConfig(&preconfig);
@@ -246,10 +295,10 @@ void android_main(struct android_app *app) {
     return;
   }
 
-  // Register the android_log module.
-  if (PyImport_AppendInittab("android_log", &PyInit_android_log) < 0) {
+  // Register the android_support module.
+  if (PyImport_AppendInittab("android_support", &PyInit_android_support) < 0) {
     android_cat.error()
-      << "Failed to register android_log module.\n";
+      << "Failed to register android_support module.\n";
     env->ReleaseStringUTFChars(libdir_jstr, libdir);
     return;
   }
@@ -260,9 +309,14 @@ void android_main(struct android_app *app) {
   config.buffered_stdio = 0;
   config.configure_c_stdio = 0;
   config.write_bytecode = 0;
+  config.module_search_paths_set = 1; // Leave sys.path empty
   PyConfig_SetBytesString(&config, &config.platlibdir, libdir);
   env->ReleaseStringUTFChars(libdir_jstr, libdir);
 
+  if (blobinfo.flags & F_python_verbose) {
+    config.verbose = 1;
+  }
+
   status = Py_InitializeFromConfig(&config);
   PyConfig_Clear(&config);
   if (PyStatus_Exception(status)) {
@@ -299,10 +353,9 @@ void android_main(struct android_app *app) {
     // We still need to keep an event loop going until Android gives us leave
     // to end the process.
     while (!app->destroyRequested) {
-      int looper_id;
-      struct android_poll_source *source;
-      auto result = ALooper_pollOnce(-1, &looper_id, nullptr, (void **)&source);
-      if (looper_id == LOOPER_ID_MAIN) {
+      struct android_poll_source *source = nullptr;
+      int ident = ALooper_pollOnce(-1, nullptr, nullptr, (void **)&source);
+      if (ident == LOOPER_ID_MAIN) {
         int8_t cmd = android_app_read_cmd(app);
         android_app_pre_exec_cmd(app, cmd);
         android_app_post_exec_cmd(app, cmd);
@@ -328,7 +381,14 @@ void android_main(struct android_app *app) {
     cp_mgr->delete_explicit_page(page);
   }
 
+  PyImport_FrozenModules = nullptr;
+  if (new_modules != nullptr) {
+    free(new_modules);
+  }
   unmap_blob(blob);
+  if (blob_copy != nullptr) {
+    free(blob_copy);
+  }
 
   // Detach the thread before exiting.
   activity->vm->DetachCurrentThread();

+ 29 - 8
pandatool/src/deploy-stub/android_log.c → pandatool/src/deploy-stub/android_support.cxx

@@ -6,23 +6,26 @@
  * license.  You should have received a copy of this license along
  * with this source code in a file named "LICENSE."
  *
- * @file android_log.c
+ * @file android_support.cxx
  * @author rdb
  * @date 2021-12-10
  */
 
+#include <android/log.h>
+#include "android_native_app_glue.h"
+#include "config_android.h"
+
 #undef _POSIX_C_SOURCE
 #undef _XOPEN_SOURCE
 #define PY_SSIZE_T_CLEAN 1
 
 #include "Python.h"
-#include <android/log.h>
 
 /**
  * Writes a message to the Android log.
  */
 static PyObject *
-_py_write(PyObject *self, PyObject *args) {
+_py_log_write(PyObject *self, PyObject *args) {
   int prio;
   char *tag;
   char *text;
@@ -33,14 +36,32 @@ _py_write(PyObject *self, PyObject *args) {
   return NULL;
 }
 
+/**
+ * Returns the path to a library, if it can be found.
+ */
+static PyObject *
+_py_find_library(PyObject *self, PyObject *args) {
+  char *lib;
+  if (PyArg_ParseTuple(args, "s", &lib)) {
+    Filename result = android_find_library(panda_android_app->activity, lib);
+    if (!result.empty()) {
+      return PyUnicode_FromStringAndSize(result.c_str(), (Py_ssize_t)result.length());
+    } else {
+      Py_RETURN_NONE;
+    }
+  }
+  return NULL;
+}
+
 static PyMethodDef python_simple_funcs[] = {
-  { "write", &_py_write, METH_VARARGS },
+  { "log_write", &_py_log_write, METH_VARARGS },
+  { "find_library", &_py_find_library, METH_VARARGS },
   { NULL, NULL }
 };
 
-static struct PyModuleDef android_log_module = {
+static struct PyModuleDef android_support_module = {
   PyModuleDef_HEAD_INIT,
-  "android_log",
+  "android_support",
   NULL,
   -1,
   python_simple_funcs,
@@ -48,6 +69,6 @@ static struct PyModuleDef android_log_module = {
 };
 
 __attribute__((visibility("default")))
-PyObject *PyInit_android_log() {
-  return PyModule_Create(&android_log_module);
+extern "C" PyObject *PyInit_android_support() {
+  return PyModule_Create(&android_support_module);
 }

+ 6 - 0
pandatool/src/deploy-stub/deploy-stub.c

@@ -21,6 +21,7 @@
 #include <stdio.h>
 #include <stdint.h>
 #include <fcntl.h>
+#include <time.h>
 
 #include <locale.h>
 
@@ -35,6 +36,7 @@ enum Flags {
   F_log_append = 1,
   F_log_filename_strftime = 2,
   F_keep_docstrings = 4,
+  F_python_verbose = 8,
 };
 
 /* Define an exposed symbol where we store the offset to the module data. */
@@ -416,6 +418,10 @@ int Py_FrozenMain(int argc, char **argv)
     }
 #endif
 
+    if (blobinfo.flags & F_python_verbose) {
+      Py_VerboseFlag = 1;
+    }
+
 #ifndef NDEBUG
     if ((p = Py_GETENV("PYTHONINSPECT")) && *p != '\0')
         inspect = 1;

+ 2 - 2
pandatool/src/mac-stats/macStatsStripChart.mm

@@ -23,8 +23,8 @@
 static const int default_strip_chart_width = 400;
 static const int default_strip_chart_height = 200;
 
-static const int minimum_strip_chart_sidebar_width = 116;
-static const int default_strip_chart_sidebar_width = 116;
+static const int minimum_strip_chart_sidebar_width = 128;
+static const int default_strip_chart_sidebar_width = 128;
 
 /**
  *

+ 0 - 24
pandatool/src/xfile/windowsGuid.I

@@ -11,14 +11,6 @@
  * @date 2004-10-03
  */
 
-/**
- *
- */
-INLINE WindowsGuid::
-WindowsGuid() {
-  memset(this, 0, sizeof(WindowsGuid));
-}
-
 /**
  *
  */
@@ -42,22 +34,6 @@ WindowsGuid(unsigned long data1,
 {
 }
 
-/**
- *
- */
-INLINE WindowsGuid::
-WindowsGuid(const WindowsGuid &copy) {
-  (*this) = copy;
-}
-
-/**
- *
- */
-INLINE void WindowsGuid::
-operator = (const WindowsGuid &copy) {
-  memcpy(this, &copy, sizeof(WindowsGuid));
-}
-
 /**
  *
  */

+ 5 - 7
pandatool/src/xfile/windowsGuid.h

@@ -25,14 +25,12 @@
  */
 class WindowsGuid {
 public:
-  INLINE WindowsGuid();
+  constexpr WindowsGuid() = default;
   INLINE WindowsGuid(unsigned long data1,
                      unsigned short data2, unsigned short data3,
                      unsigned char b1, unsigned char b2, unsigned char b3,
                      unsigned char b4, unsigned char b5, unsigned char b6,
                      unsigned char b7, unsigned char b8);
-  INLINE WindowsGuid(const WindowsGuid &copy);
-  INLINE void operator = (const WindowsGuid &copy);
 
   INLINE bool operator == (const WindowsGuid &other) const;
   INLINE bool operator != (const WindowsGuid &other) const;
@@ -45,10 +43,10 @@ public:
   void output(std::ostream &out) const;
 
 private:
-  unsigned long _data1;
-  unsigned short _data2;
-  unsigned short _data3;
-  unsigned char _b1, _b2, _b3, _b4, _b5, _b6, _b7, _b8;
+  unsigned long _data1 = 0;
+  unsigned short _data2 = 0;
+  unsigned short _data3 = 0;
+  unsigned char _b1 = 0, _b2 = 0, _b3 = 0, _b4 = 0, _b5 = 0, _b6 = 0, _b7 = 0, _b8 = 0;
 };
 
 INLINE std::ostream &operator << (std::ostream &out, const WindowsGuid &guid);

+ 28 - 0
tests/display/test_color_buffer.py

@@ -367,3 +367,31 @@ def test_color_transparency_no_light(color_region, shader_attrib):
     )
     result = render_color_pixel(color_region, state)
     assert result.x == pytest.approx(1.0, 0.1)
+
+
+def test_texture_occlusion(color_region):
+    shader_attrib = core.ShaderAttrib.make_default().set_shader_auto(True)
+
+    tex = core.Texture("occlusion")
+    tex.set_clear_color((0.5, 1.0, 1.0, 1.0))
+    stage = core.TextureStage("occlusion")
+    stage.set_mode(core.TextureStage.M_occlusion)
+    texture_attrib = core.TextureAttrib.make().add_on_stage(stage, tex)
+
+    mat = core.Material()
+    mat.diffuse = (0, 1, 0, 1)
+    mat.ambient = (0, 0, 1, 1)
+    material_attrib = core.MaterialAttrib.make(mat)
+
+    alight = core.AmbientLight("ambient")
+    alight.set_color((0, 0, 1, 1))
+    light_attrib = core.LightAttrib.make().add_on_light(core.NodePath(alight))
+
+    state = core.RenderState.make(
+        shader_attrib,
+        material_attrib,
+        texture_attrib,
+        light_attrib
+    )
+    result = render_color_pixel(color_region, state)
+    assert result.z == pytest.approx(0.5, 0.05)

+ 5 - 0
tests/main.c

@@ -111,6 +111,11 @@ int main(int argc, char **argv) {
   PyRun_SimpleString("import sys; sys.argv.insert(1, '--capture=sys')");
 #endif
 
+#ifdef ANDROID
+  // No caching on Android
+  PyRun_SimpleString("import sys; sys.argv.insert(1, '-o cache_dir=/dev/null')");
+#endif
+
   return Py_RunMain();
 
 exception:

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