Browse Source

Merge branch 'master' into shaderpipeline

rdb 10 months ago
parent
commit
1dde82e789
100 changed files with 2597 additions and 839 deletions
  1. 10 10
      .github/workflows/ci.yml
  2. 2 2
      .github/workflows/mypy.yml
  3. 8 9
      README.md
  4. 6 2
      cmake/macros/Interrogate.cmake
  5. 20 1
      direct/src/dist/FreezeTool.py
  6. 6 3
      direct/src/dist/_android.py
  7. 66 26
      direct/src/dist/commands.py
  8. 2 2
      direct/src/distributed/DistributedObjectAI.py
  9. 1 1
      direct/src/distributed/DistributedObjectUD.py
  10. 12 5
      direct/src/showbase/Loader.py
  11. 27 23
      direct/src/showbase/ShowBase.py
  12. 2 1
      direct/src/showbase/ShowBaseGlobal.py
  13. 1 1
      direct/src/task/Task.py
  14. 1 1
      dtool/Config.cmake
  15. 2 0
      dtool/src/dtoolutil/CMakeLists.txt
  16. 50 0
      dtool/src/dtoolutil/console_preamble.js
  17. 12 13
      dtool/src/dtoolutil/executionEnvironment.cxx
  18. 4 4
      dtool/src/dtoolutil/executionEnvironment.h
  19. 1 0
      dtool/src/dtoolutil/p3dtoolutil_ext_composite.cxx
  20. 80 0
      dtool/src/dtoolutil/pyenv_init.cxx
  21. 14 0
      dtool/src/dtoolutil/pyenv_init.h
  22. 3 3
      dtool/src/prc/notify.cxx
  23. 1 1
      makepanda/makepackage.py
  24. 33 10
      makepanda/makepanda.py
  25. 17 14
      makepanda/makepandacore.py
  26. 6 0
      makepanda/makewheel.py
  27. 3 0
      mypy.ini
  28. 1 1
      panda/CMakeLists.txt
  29. 11 7
      panda/src/android/android_main.cxx
  30. 1 1
      panda/src/android/pview_manifest.xml
  31. 11 12
      panda/src/androiddisplay/androidGraphicsWindow.cxx
  32. 2 1
      panda/src/cocoagldisplay/cocoaGLGraphicsBuffer.mm
  33. 2 2
      panda/src/cocoagldisplay/cocoaGLGraphicsPipe.mm
  34. 2 1
      panda/src/cocoagldisplay/cocoaGLGraphicsWindow.mm
  35. 2 0
      panda/src/collide/CMakeLists.txt
  36. 21 0
      panda/src/collide/collisionNode.I
  37. 22 2
      panda/src/collide/collisionNode.cxx
  38. 19 0
      panda/src/collide/collisionNode.h
  39. 62 0
      panda/src/collide/collisionNode_ext.cxx
  40. 40 0
      panda/src/collide/collisionNode_ext.h
  41. 1 0
      panda/src/collide/p3collide_ext_composite.cxx
  42. 1 1
      panda/src/cull/cullBinBackToFront.cxx
  43. 1 1
      panda/src/cull/cullBinFrontToBack.cxx
  44. 3 1
      panda/src/display/displayRegion.cxx
  45. 50 0
      panda/src/display/graphicsEngine.I
  46. 68 142
      panda/src/display/graphicsEngine.cxx
  47. 32 12
      panda/src/display/graphicsEngine.h
  48. 41 1
      panda/src/display/graphicsStateGuardian.I
  49. 36 0
      panda/src/display/graphicsStateGuardian.cxx
  50. 17 0
      panda/src/display/graphicsStateGuardian.h
  51. 2 1
      panda/src/egldisplay/eglGraphicsBuffer.cxx
  52. 3 0
      panda/src/egldisplay/eglGraphicsPipe.cxx
  53. 2 1
      panda/src/egldisplay/eglGraphicsWindow.cxx
  54. 11 0
      panda/src/event/asyncFuture.cxx
  55. 32 1
      panda/src/event/asyncFuture.h
  56. 3 0
      panda/src/event/asyncTaskChain.cxx
  57. 17 18
      panda/src/express/trueClock.cxx
  58. 2 0
      panda/src/framework/pandaFramework.cxx
  59. 9 0
      panda/src/gles2gsg/gles2gsg.h
  60. 4 0
      panda/src/glstuff/glBufferContext_src.h
  61. 181 22
      panda/src/glstuff/glGraphicsBuffer_src.cxx
  62. 4 0
      panda/src/glstuff/glGraphicsBuffer_src.h
  63. 515 219
      panda/src/glstuff/glGraphicsStateGuardian_src.cxx
  64. 72 22
      panda/src/glstuff/glGraphicsStateGuardian_src.h
  65. 44 12
      panda/src/glstuff/glShaderContext_src.cxx
  66. 6 0
      panda/src/glstuff/glShaderContext_src.h
  67. 40 0
      panda/src/glstuff/glTextureContext_src.I
  68. 110 24
      panda/src/glstuff/glTextureContext_src.cxx
  69. 31 2
      panda/src/glstuff/glTextureContext_src.h
  70. 18 0
      panda/src/glstuff/glmisc_src.cxx
  71. 4 0
      panda/src/glstuff/glmisc_src.h
  72. 2 1
      panda/src/glxdisplay/glxGraphicsBuffer.cxx
  73. 3 0
      panda/src/glxdisplay/glxGraphicsPipe.cxx
  74. 2 1
      panda/src/glxdisplay/glxGraphicsWindow.cxx
  75. 4 1
      panda/src/gobj/bufferContext.cxx
  76. 1 1
      panda/src/gobj/bufferContext.h
  77. 7 0
      panda/src/gobj/bufferContextChain.cxx
  78. 4 0
      panda/src/gobj/bufferContextChain.h
  79. 1 0
      panda/src/gobj/bufferResidencyTracker.cxx
  80. 18 3
      panda/src/gobj/preparedGraphicsObjects.cxx
  81. 9 1
      panda/src/gobj/texture.I
  82. 38 2
      panda/src/gobj/texture.cxx
  83. 6 0
      panda/src/gobj/texture.h
  84. 2 0
      panda/src/gsgbase/graphicsStateGuardianBase.h
  85. 133 127
      panda/src/linmath/compose_matrix_src.cxx
  86. 11 7
      panda/src/pgraph/loader.cxx
  87. 2 2
      panda/src/pgraph/modelPool.I
  88. 6 29
      panda/src/pgraph/modelPool.cxx
  89. 6 6
      panda/src/pgraph/modelPool.h
  90. 2 0
      panda/src/pgraph/nodePath.h
  91. 59 0
      panda/src/pgraph/nodePath_ext.cxx
  92. 6 0
      panda/src/pgraph/nodePath_ext.h
  93. 8 3
      panda/src/pgraphnodes/lightLensNode.cxx
  94. 1 1
      panda/src/pipeline/threadPosixImpl.I
  95. 10 15
      panda/src/pstatclient/pStatClientImpl.cxx
  96. 4 0
      panda/src/putil/CMakeLists.txt
  97. 75 0
      panda/src/putil/completable.I
  98. 82 0
      panda/src/putil/completable.h
  99. 97 0
      panda/src/putil/completionCounter.I
  100. 52 0
      panda/src/putil/completionCounter.cxx

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

@@ -508,7 +508,7 @@ jobs:
     - name: Setup emsdk
     - name: Setup emsdk
       uses: mymindstorm/setup-emsdk@v14
       uses: mymindstorm/setup-emsdk@v14
       with:
       with:
-        version: 3.1.70
+        version: 4.0.2
         actions-cache-folder: 'emsdk-cache'
         actions-cache-folder: 'emsdk-cache'
 
 
     - name: Restore Python build cache
     - name: Restore Python build cache
@@ -516,19 +516,19 @@ jobs:
       uses: actions/cache/restore@v4
       uses: actions/cache/restore@v4
       with:
       with:
         path: ~/python
         path: ~/python
-        key: cache-emscripten-python-3.12.7
+        key: cache-emscripten-python-3.12.8
 
 
     - name: Build Python 3.12
     - name: Build Python 3.12
       if: steps.cache-emscripten-python-restore.outputs.cache-hit != 'true'
       if: steps.cache-emscripten-python-restore.outputs.cache-hit != 'true'
       run: |
       run: |
-        wget https://www.python.org/ftp/python/3.12.7/Python-3.12.7.tar.xz
-        tar -xJf Python-3.12.7.tar.xz
-        (cd Python-3.12.7 && EM_CONFIG=$EMSDK/.emscripten python3 Tools/wasm/wasm_build.py emscripten-browser)
-        (cd Python-3.12.7/builddir/emscripten-browser && make install DESTDIR=~/python)
-        cp Python-3.12.7/builddir/emscripten-browser/Modules/_hacl/libHacl_Hash_SHA2.a ~/python/usr/local/lib
-        cp Python-3.12.7/builddir/emscripten-browser/Modules/_decimal/libmpdec/libmpdec.a ~/python/usr/local/lib
-        cp Python-3.12.7/builddir/emscripten-browser/Modules/expat/libexpat.a ~/python/usr/local/lib
-        rm -rf Python-3.12.7
+        wget https://www.python.org/ftp/python/3.12.8/Python-3.12.8.tar.xz
+        tar -xJf Python-3.12.8.tar.xz
+        (cd Python-3.12.8 && EM_CONFIG=$EMSDK/.emscripten python3 Tools/wasm/wasm_build.py emscripten-browser)
+        (cd Python-3.12.8/builddir/emscripten-browser && make install DESTDIR=~/python)
+        cp Python-3.12.8/builddir/emscripten-browser/Modules/_hacl/libHacl_Hash_SHA2.a ~/python/usr/local/lib
+        cp Python-3.12.8/builddir/emscripten-browser/Modules/_decimal/libmpdec/libmpdec.a ~/python/usr/local/lib
+        cp Python-3.12.8/builddir/emscripten-browser/Modules/expat/libexpat.a ~/python/usr/local/lib
+        rm -rf Python-3.12.8
 
 
     - name: Save Python build cache
     - name: Save Python build cache
       id: cache-emscripten-python-save
       id: cache-emscripten-python-save

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

@@ -7,7 +7,7 @@ jobs:
     strategy:
     strategy:
       matrix:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: ['3.8', '3.11']
+        python-version: ['3.9', '3.13']
       fail-fast: false
       fail-fast: false
     steps:
     steps:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
@@ -18,6 +18,6 @@ jobs:
       - name: Install dependencies
       - name: Install dependencies
         run: |
         run: |
           python -m pip install --upgrade pip
           python -m pip install --upgrade pip
-          pip install mypy==1.4.0
+          pip install mypy==1.14.1
       - name: Run mypy on direct
       - name: Run mypy on direct
         run: python tests/run_mypy.py
         run: python tests/run_mypy.py

+ 8 - 9
README.md

@@ -181,22 +181,21 @@ Although it's possible to build Panda3D on an Android device using the
 [termux](https://termux.com/) shell, the recommended route is to cross-compile
 [termux](https://termux.com/) shell, the recommended route is to cross-compile
 .whl files using the SDK and NDK, which can then be used by the `build_apps`
 .whl files using the SDK and NDK, which can then be used by the `build_apps`
 command to build a Python application into an .apk or .aab bundle.  You will
 command to build a Python application into an .apk or .aab bundle.  You will
-need to get the latest thirdparty packages, which can be obtained from the
-artifacts page of the last successful run here:
+need to get the latest thirdparty packages, which can be obtained from here:
 
 
-https://github.com/rdb/panda3d-thirdparty/actions?query=branch%3Amain+is%3Asuccess+event%3Apush
+https://rdb.name/thirdparty-android.tar.gz
 
 
-This does not include Python at the moment, which can be extracted from
-[this archive](https://rdb.name/thirdparty-android.tar.gz) instead.
+This includes a copy of Python 3.13 compiled for Android.  You will need to
+use Python 3.13 on the host as well.
 
 
 These commands show how to compile wheels for the supported Android ABIs:
 These commands show how to compile wheels for the supported Android ABIs:
 
 
 ```bash
 ```bash
 export ANDROID_SDK_ROOT=/home/rdb/local/android
 export ANDROID_SDK_ROOT=/home/rdb/local/android
-python3.8 makepanda/makepanda.py --everything --outputdir built-droid-arm64 --arch arm64 --target android-21 --threads 6 --wheel
-python3.8 makepanda/makepanda.py --everything --outputdir built-droid-armv7a --arch armv7a --target android-19 --threads 6 --wheel
-python3.8 makepanda/makepanda.py --everything --outputdir built-droid-x86_64 --arch x86_64 --target android-21 --threads 6 --wheel
-python3.8 makepanda/makepanda.py --everything --outputdir built-droid-x86 --arch x86 --target android-19 --threads 6 --wheel
+python3.13 makepanda/makepanda.py --everything --outputdir built-droid-arm64 --arch arm64 --target android-21 --threads 6 --wheel
+python3.13 makepanda/makepanda.py --everything --outputdir built-droid-armv7a --arch arm --target android-21 --threads 6 --wheel
+python3.13 makepanda/makepanda.py --everything --outputdir built-droid-x86_64 --arch x86_64 --target android-21 --threads 6 --wheel
+python3.13 makepanda/makepanda.py --everything --outputdir built-droid-x86 --arch x86 --target android-21 --threads 6 --wheel
 ```
 ```
 
 
 It is now possible to use the generated wheels with `build_apps`, as explained
 It is now possible to use the generated wheels with `build_apps`, as explained

+ 6 - 2
cmake/macros/Interrogate.cmake

@@ -283,7 +283,7 @@ endfunction(interrogate_sources)
 
 
 #
 #
 # Function: add_python_module(module [lib1 [lib2 ...]] [LINK lib1 ...]
 # Function: add_python_module(module [lib1 [lib2 ...]] [LINK lib1 ...]
-#    [IMPORT mod1 ...])
+#    [IMPORT mod1 ...] [INIT func1 ...])
 # Uses interrogate to create a Python module. If the LINK keyword is specified,
 # Uses interrogate to create a Python module. If the LINK keyword is specified,
 # the Python module is linked against the specified libraries instead of those
 # the Python module is linked against the specified libraries instead of those
 # listed before. The IMPORT keyword makes the output module import another
 # listed before. The IMPORT keyword makes the output module import another
@@ -305,7 +305,7 @@ function(add_python_module module)
 
 
   set(keyword)
   set(keyword)
   foreach(arg ${ARGN})
   foreach(arg ${ARGN})
-    if(arg STREQUAL "LINK" OR arg STREQUAL "IMPORT" OR arg STREQUAL "COMPONENT")
+    if(arg STREQUAL "LINK" OR arg STREQUAL "IMPORT" OR arg STREQUAL "INIT" OR arg STREQUAL "COMPONENT")
       set(keyword "${arg}")
       set(keyword "${arg}")
 
 
     elseif(keyword STREQUAL "LINK")
     elseif(keyword STREQUAL "LINK")
@@ -316,6 +316,10 @@ function(add_python_module module)
       list(APPEND import_flags "-import" "${arg}")
       list(APPEND import_flags "-import" "${arg}")
       set(keyword)
       set(keyword)
 
 
+    elseif(keyword STREQUAL "INIT")
+      list(APPEND import_flags "-init" "${arg}")
+      set(keyword)
+
     elseif(keyword STREQUAL "COMPONENT")
     elseif(keyword STREQUAL "COMPONENT")
       set(component "${arg}")
       set(component "${arg}")
       set(keyword)
       set(keyword)

+ 20 - 1
direct/src/dist/FreezeTool.py

@@ -88,6 +88,7 @@ defaultHiddenImports = {
         'numpy.core._dtype_ctypes',
         'numpy.core._dtype_ctypes',
         'numpy.core._methods',
         'numpy.core._methods',
     ],
     ],
+    'panda3d.core': ['enum'],
     'pandas.compat': ['lzma', 'cmath'],
     'pandas.compat': ['lzma', 'cmath'],
     'pandas._libs.tslibs.conversion': ['pandas._libs.tslibs.base'],
     'pandas._libs.tslibs.conversion': ['pandas._libs.tslibs.base'],
     'plyer': ['plyer.platforms'],
     'plyer': ['plyer.platforms'],
@@ -925,7 +926,22 @@ class Freezer:
             if sys.version_info < (3, 8):
             if sys.version_info < (3, 8):
                 abi_flags += 'm'
                 abi_flags += 'm'
 
 
-            if 'linux' in self.platform:
+            if 'android' in self.platform:
+                arch = self.platform.split('_', 1)[1]
+                if arch in ('arm64', 'aarch64'):
+                    suffixes.append(('.cpython-{0}{1}-aarch64-linux-android.so'.format(abi_version, abi_flags), 'rb', 3))
+                elif arch in ('arm', 'armv7l'):
+                    suffixes.append(('.cpython-{0}{1}-arm-linux-androideabi.so'.format(abi_version, abi_flags), 'rb', 3))
+                elif arch in ('x86_64', 'amd64'):
+                    suffixes.append(('.cpython-{0}{1}-x86_64-linux-android.so'.format(abi_version, abi_flags), 'rb', 3))
+                elif arch in ('i386', 'i686'):
+                    suffixes.append(('.cpython-{0}{1}-i686-linux-android.so'.format(abi_version, abi_flags), 'rb', 3))
+
+                suffixes += [
+                    ('.abi{0}.so'.format(sys.version_info[0]), 'rb', 3),
+                    ('.so', 'rb', 3),
+                ]
+            elif 'linux' in self.platform:
                 suffixes += [
                 suffixes += [
                     ('.cpython-{0}{1}-x86_64-linux-gnu.so'.format(abi_version, abi_flags), 'rb', 3),
                     ('.cpython-{0}{1}-x86_64-linux-gnu.so'.format(abi_version, abi_flags), 'rb', 3),
                     ('.cpython-{0}{1}-i686-linux-gnu.so'.format(abi_version, abi_flags), 'rb', 3),
                     ('.cpython-{0}{1}-i686-linux-gnu.so'.format(abi_version, abi_flags), 'rb', 3),
@@ -1150,6 +1166,9 @@ class Freezer:
             self.modules['_frozen_importlib'] = self.ModuleDef('importlib._bootstrap', implicit = True)
             self.modules['_frozen_importlib'] = self.ModuleDef('importlib._bootstrap', implicit = True)
             self.modules['_frozen_importlib_external'] = self.ModuleDef('importlib._bootstrap_external', implicit = True)
             self.modules['_frozen_importlib_external'] = self.ModuleDef('importlib._bootstrap_external', implicit = True)
 
 
+            if self.platform.startswith('android'):
+                self.modules['_android_support'] = self.ModuleDef('_android_support', implicit = True)
+
             for moduleName in startupModules:
             for moduleName in startupModules:
                 if moduleName not in self.modules:
                 if moduleName not in self.modules:
                     self.addModule(moduleName, implicit = True)
                     self.addModule(moduleName, implicit = True)

+ 6 - 3
direct/src/dist/_android.py

@@ -50,7 +50,7 @@ def flag_resource(id, **values):
         bitmask = 0
         bitmask = 0
         flags = attrib.value.split('|')
         flags = attrib.value.split('|')
         for flag in flags:
         for flag in flags:
-            bitmask = values[flag]
+            bitmask |= values[flag]
         attrib.compiled_item.prim.int_hexadecimal_value = bitmask
         attrib.compiled_item.prim.int_hexadecimal_value = bitmask
     return compile
     return compile
 
 
@@ -168,10 +168,11 @@ ANDROID_ATTRIBUTES = {
     'allowSingleTap': bool_resource(0x1010259),
     'allowSingleTap': bool_resource(0x1010259),
     'allowTaskReparenting': bool_resource(0x1010204),
     'allowTaskReparenting': bool_resource(0x1010204),
     'alwaysRetainTaskState': bool_resource(0x1010203),
     'alwaysRetainTaskState': bool_resource(0x1010203),
+    'appCategory': enum_resource(0x01010545, "game", "audio", "video", "image", "social", "news", "maps", "productivity", "accessibility"),
     'clearTaskOnLaunch': bool_resource(0x1010015),
     'clearTaskOnLaunch': bool_resource(0x1010015),
+    'configChanges': flag_resource(0x0101001f, mcc=0x0001, mnc=0x0002, locale=0x0004, touchscreen=0x0008, keyboard=0x0010, keyboardHidden=0x0020, navigation=0x0040, orientation=0x0080, screenLayout=0x0100, uiMode=0x0200, screenSize=0x0400, smallestScreenSize=0x0800, layoutDirection=0x2000, colorMode=0x4000, grammaticalGender=0x8000, fontScale=0x40000000, fontWeightAdjustment=0x10000000),
     'debuggable': bool_resource(0x0101000f),
     'debuggable': bool_resource(0x0101000f),
     'documentLaunchMode': enum_resource(0x1010445, "none", "intoExisting", "always", "never"),
     'documentLaunchMode': enum_resource(0x1010445, "none", "intoExisting", "always", "never"),
-    'configChanges': flag_resource(0x0101001f, mcc=0x0001, mnc=0x0002, locale=0x0004, touchscreen=0x0008, keyboard=0x0010, keyboardHidden=0x0020, navigation=0x0040, orientation=0x0080, screenLayout=0x0100, uiMode=0x0200, screenSize=0x0400, smallestScreenSize=0x0800, layoutDirection=0x2000, fontScale=0x40000000),
     'enabled': bool_resource(0x101000e),
     'enabled': bool_resource(0x101000e),
     'excludeFromRecents': bool_resource(0x1010017),
     'excludeFromRecents': bool_resource(0x1010017),
     'exported': bool_resource(0x1010010),
     'exported': bool_resource(0x1010010),
@@ -179,6 +180,7 @@ ANDROID_ATTRIBUTES = {
     'finishOnTaskLaunch': bool_resource(0x1010014),
     'finishOnTaskLaunch': bool_resource(0x1010014),
     'fullBackupContent': bool_resource(0x10104eb),
     'fullBackupContent': bool_resource(0x10104eb),
     'glEsVersion': int_resource(0x1010281),
     'glEsVersion': int_resource(0x1010281),
+    'hardwareAccelerated': bool_resource(0x10102d3),
     'hasCode': bool_resource(0x101000c),
     'hasCode': bool_resource(0x101000c),
     'host': str_resource(0x1010028),
     'host': str_resource(0x1010028),
     'icon': ref_resource(0x1010002),
     'icon': ref_resource(0x1010002),
@@ -194,8 +196,9 @@ ANDROID_ATTRIBUTES = {
     'name': str_resource(0x1010003),
     'name': str_resource(0x1010003),
     'noHistory': bool_resource(0x101022d),
     'noHistory': bool_resource(0x101022d),
     'pathPattern': str_resource(0x101002c),
     'pathPattern': str_resource(0x101002c),
-    'resizeableActivity': bool_resource(0x10104f6),
+    'preferMinimalPostProcessing': bool_resource(0x101060c),
     'required': bool_resource(0x101028e),
     'required': bool_resource(0x101028e),
+    'resizeableActivity': bool_resource(0x10104f6),
     'scheme': str_resource(0x1010027),
     'scheme': str_resource(0x1010027),
     'screenOrientation': enum_resource(0x101001e, 'landscape', 'portrait', 'user', 'behind', 'sensor', 'nosensor', 'sensorLandscape', 'sensorPortrait', 'reverseLandscape', 'reversePortrait', 'fullSensor', 'userLandscape', 'userPortrait', 'fullUser', 'locked'),
     'screenOrientation': enum_resource(0x101001e, 'landscape', 'portrait', 'user', 'behind', 'sensor', 'nosensor', 'sensorLandscape', 'sensorPortrait', 'reverseLandscape', 'reversePortrait', 'fullSensor', 'userLandscape', 'userPortrait', 'fullUser', 'locked'),
     'stateNotNeeded': bool_resource(0x1010016),
     'stateNotNeeded': bool_resource(0x1010016),

+ 66 - 26
direct/src/dist/commands.py

@@ -188,11 +188,26 @@ FrozenImporter.get_data = get_data
 """
 """
 
 
 SITE_PY_ANDROID = """
 SITE_PY_ANDROID = """
+# Define this first, before we import anything that might import an extension
+# module.
 import sys, os
 import sys, os
+from importlib import _bootstrap, _bootstrap_external
+
+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):
+            loader = _bootstrap_external.ExtensionFileLoader(fullname, path)
+            return _bootstrap.ModuleSpec(fullname, loader, origin=path)
+
+
+sys.meta_path.append(AndroidExtensionFinder)
+
+
 from _frozen_importlib import _imp, FrozenImporter
 from _frozen_importlib import _imp, FrozenImporter
-from importlib import _bootstrap_external
-from importlib.abc import Loader, MetaPathFinder
-from importlib.machinery import ModuleSpec
 from io import RawIOBase, TextIOWrapper
 from io import RawIOBase, TextIOWrapper
 
 
 from android_log import write as android_log_write
 from android_log import write as android_log_write
@@ -242,8 +257,9 @@ class AndroidLogStream:
     def writable(self):
     def writable(self):
         return True
         return True
 
 
-sys.stdout = AndroidLogStream(2, 'Python')
-sys.stderr = AndroidLogStream(3, 'Python')
+if sys.version_info < (3, 13):
+    sys.stdout = AndroidLogStream(4, 'python.stdout')
+    sys.stderr = AndroidLogStream(5, 'python.stderr')
 
 
 
 
 # Alter FrozenImporter to give a __file__ property to frozen modules.
 # Alter FrozenImporter to give a __file__ property to frozen modules.
@@ -262,20 +278,6 @@ def get_data(path):
 
 
 FrozenImporter.find_spec = find_spec
 FrozenImporter.find_spec = find_spec
 FrozenImporter.get_data = get_data
 FrozenImporter.get_data = get_data
-
-
-class AndroidExtensionFinder(MetaPathFinder):
-    @classmethod
-    def find_spec(cls, fullname, path=None, target=None):
-        soname = 'libpy.' + fullname + '.so'
-        path = os.path.join(os.path.dirname(sys.executable), soname)
-
-        if os.path.exists(path):
-            loader = _bootstrap_external.ExtensionFileLoader(fullname, path)
-            return ModuleSpec(fullname, loader, origin=path)
-
-
-sys.meta_path.append(AndroidExtensionFinder)
 """
 """
 
 
 
 
@@ -294,6 +296,7 @@ class build_apps(setuptools.Command):
         self.application_id = None
         self.application_id = None
         self.android_abis = None
         self.android_abis = None
         self.android_debuggable = False
         self.android_debuggable = False
+        self.android_app_category = None
         self.android_version_code = 1
         self.android_version_code = 1
         self.android_min_sdk_version = 21
         self.android_min_sdk_version = 21
         self.android_max_sdk_version = None
         self.android_max_sdk_version = None
@@ -516,6 +519,18 @@ class build_apps(setuptools.Command):
         tmp.update(self.package_data_dirs)
         tmp.update(self.package_data_dirs)
         self.package_data_dirs = tmp
         self.package_data_dirs = tmp
 
 
+        if 'android' in self.platforms:
+            assert self.application_id, \
+                'Must have a valid application_id when targeting Android!'
+
+            parts = self.application_id.split('.')
+            assert len(parts) >= 2, \
+                'application_id must contain at least one \'.\' separator!'
+
+            for part in parts:
+                assert part.isidentifier(), \
+                    'Each part of application_id must be a valid identifier!'
+
         # Default to all supported ABIs (for the given Android version).
         # Default to all supported ABIs (for the given Android version).
         if self.android_max_sdk_version and self.android_max_sdk_version < 21:
         if self.android_max_sdk_version and self.android_max_sdk_version < 21:
             assert self.android_max_sdk_version >= 19, \
             assert self.android_max_sdk_version >= 19, \
@@ -782,10 +797,29 @@ class build_apps(setuptools.Command):
         version = self.distribution.get_version()
         version = self.distribution.get_version()
         classifiers = self.distribution.get_classifiers()
         classifiers = self.distribution.get_classifiers()
 
 
-        is_game = False
-        for classifier in classifiers:
-            if classifier == 'Topic :: Games/Entertainment' or classifier.startswith('Topic :: Games/Entertainment ::'):
-                is_game = True
+        # If we have no app category, determine it based on the classifiers.
+        category = self.android_app_category
+        if not category:
+            for classifier in classifiers:
+                classifier = tuple(classifier.split(' :: '))
+                if len(classifier) < 2 or classifier[0] != 'Topic':
+                    continue
+
+                if classifier[:2] == ('Topic', 'Games/Entertainment'):
+                    category = 'game'
+                    break
+                elif classifier[:3] == ('Topic', 'Multimedia', 'Audio'):
+                    category = 'audio'
+                elif classifier[:4] == ('Topic', 'Multimedia', 'Graphics', 'Editors'):
+                    category = 'image'
+                elif classifier[:2] == ('Topic', 'Communications', 'Usenet News'):
+                    category = 'news'
+                elif classifier[:2] == ('Topic', 'Office/Business'):
+                    category = 'productivity'
+                elif classifier[:3] == ('Topic', 'Communications', 'Chat'):
+                    category = 'social'
+                elif classifier[:3] == ('Topic', 'Multimedia', 'Video'):
+                    category = 'video'
 
 
         manifest = ET.Element('manifest')
         manifest = ET.Element('manifest')
         manifest.set('xmlns:android', 'http://schemas.android.com/apk/res/android')
         manifest.set('xmlns:android', 'http://schemas.android.com/apk/res/android')
@@ -816,9 +850,13 @@ class build_apps(setuptools.Command):
 
 
         application = ET.SubElement(manifest, 'application')
         application = ET.SubElement(manifest, 'application')
         application.set('android:label', name)
         application.set('android:label', name)
-        application.set('android:isGame', ('false', 'true')[is_game])
+        if category == 'game':
+            application.set('android:isGame', 'true')
+        if category:
+            application.set('android:appCategory', category)
         application.set('android:debuggable', ('false', 'true')[self.android_debuggable])
         application.set('android:debuggable', ('false', 'true')[self.android_debuggable])
         application.set('android:extractNativeLibs', 'true')
         application.set('android:extractNativeLibs', 'true')
+        application.set('android:hardwareAccelerated', 'true')
 
 
         app_icon = self.icon_objects.get('*', self.icon_objects.get(self.macos_main_app))
         app_icon = self.icon_objects.get('*', self.icon_objects.get(self.macos_main_app))
         if app_icon:
         if app_icon:
@@ -828,9 +866,11 @@ class build_apps(setuptools.Command):
             activity = ET.SubElement(application, 'activity')
             activity = ET.SubElement(application, 'activity')
             activity.set('android:name', 'org.panda3d.android.PythonActivity')
             activity.set('android:name', 'org.panda3d.android.PythonActivity')
             activity.set('android:label', appname)
             activity.set('android:label', appname)
-            activity.set('android:theme', '@android:style/Theme.NoTitleBar')
-            activity.set('android:configChanges', 'orientation|keyboardHidden')
+            activity.set('android:theme', '@android:style/Theme.NoTitleBar.Fullscreen')
+            activity.set('android:alwaysRetainTaskState', 'true')
+            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:launchMode', 'singleInstance')
+            activity.set('android:preferMinimalPostProcessing', 'true')
 
 
             act_icon = self.icon_objects.get(appname)
             act_icon = self.icon_objects.get(appname)
             if act_icon and act_icon is not app_icon:
             if act_icon and act_icon is not app_icon:

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

@@ -299,7 +299,7 @@ class DistributedObjectAI(DistributedObjectBase):
         # setLocation destroys self._zoneData if we move away to
         # setLocation destroys self._zoneData if we move away to
         # a different zone
         # a different zone
         if self._zoneData is None:
         if self._zoneData is None:
-            from otp.ai.AIZoneData import AIZoneData  # type: ignore[import]
+            from otp.ai.AIZoneData import AIZoneData  # type: ignore[import-not-found]
             self._zoneData = AIZoneData(self.air, self.parentId, self.zoneId)
             self._zoneData = AIZoneData(self.air, self.parentId, self.zoneId)
         return self._zoneData
         return self._zoneData
 
 
@@ -489,7 +489,7 @@ class DistributedObjectAI(DistributedObjectBase):
         # simultaneously on different lists of avatars, although they
         # simultaneously on different lists of avatars, although they
         # should have different names.
         # should have different names.
 
 
-        from otp.ai import Barrier  # type: ignore[import]
+        from otp.ai import Barrier  # type: ignore[import-not-found]
         context = self.__nextBarrierContext
         context = self.__nextBarrierContext
         # We assume the context number is passed as a uint16.
         # We assume the context number is passed as a uint16.
         self.__nextBarrierContext = (self.__nextBarrierContext + 1) & 0xffff
         self.__nextBarrierContext = (self.__nextBarrierContext + 1) & 0xffff

+ 1 - 1
direct/src/distributed/DistributedObjectUD.py

@@ -424,7 +424,7 @@ class DistributedObjectUD(DistributedObjectBase):
         # simultaneously on different lists of avatars, although they
         # simultaneously on different lists of avatars, although they
         # should have different names.
         # should have different names.
 
 
-        from otp.ai import Barrier  # type: ignore[import]
+        from otp.ai import Barrier  # type: ignore[import-not-found]
         context = self.__nextBarrierContext
         context = self.__nextBarrierContext
         # We assume the context number is passed as a uint16.
         # We assume the context number is passed as a uint16.
         self.__nextBarrierContext = (self.__nextBarrierContext + 1) & 0xffff
         self.__nextBarrierContext = (self.__nextBarrierContext + 1) & 0xffff

+ 12 - 5
direct/src/showbase/Loader.py

@@ -126,7 +126,7 @@ class Loader(DirectObject):
                 yield await req
                 yield await req
 
 
     # special methods
     # special methods
-    def __init__(self, base):
+    def __init__(self, base=None):
         self.base = base
         self.base = base
         self.loader = PandaLoader.getGlobalPtr()
         self.loader = PandaLoader.getGlobalPtr()
 
 
@@ -134,15 +134,15 @@ class Loader(DirectObject):
 
 
         self.hook = "async_loader_%s" % (Loader.loaderIndex)
         self.hook = "async_loader_%s" % (Loader.loaderIndex)
         Loader.loaderIndex += 1
         Loader.loaderIndex += 1
-        self.accept(self.hook, self.__gotAsyncObject)
-
-        self._loadPythonFileTypes()
 
 
     def destroy(self):
     def destroy(self):
         self.ignore(self.hook)
         self.ignore(self.hook)
         self.loader.stopThreads()
         self.loader.stopThreads()
         del self.base
         del self.base
-        del self.loader
+
+    def _init_base(self, base):
+        self.base = base
+        self.accept(self.hook, self.__gotAsyncObject)
 
 
     @classmethod
     @classmethod
     def _loadPythonFileTypes(cls):
     def _loadPythonFileTypes(cls):
@@ -229,6 +229,10 @@ class Loader(DirectObject):
         """
         """
 
 
         assert Loader.notify.debug("Loading model: %s" % (modelPath,))
         assert Loader.notify.debug("Loading model: %s" % (modelPath,))
+
+        if not self._loadedPythonFileTypes:
+            self._loadPythonFileTypes()
+
         if loaderOptions is None:
         if loaderOptions is None:
             loaderOptions = LoaderOptions()
             loaderOptions = LoaderOptions()
         else:
         else:
@@ -416,6 +420,9 @@ class Loader(DirectObject):
         a callback is used, the model is saved asynchronously, and the
         a callback is used, the model is saved asynchronously, and the
         true/false status is passed to the callback function. """
         true/false status is passed to the callback function. """
 
 
+        if not self._loadedPythonFileTypes:
+            self._loadPythonFileTypes()
+
         if loaderOptions is None:
         if loaderOptions is None:
             loaderOptions = LoaderOptions()
             loaderOptions = LoaderOptions()
         else:
         else:

+ 27 - 23
direct/src/showbase/ShowBase.py

@@ -177,6 +177,8 @@ class ShowBase(DirectObject.DirectObject):
     aspect2d: NodePath
     aspect2d: NodePath
     pixel2d: NodePath
     pixel2d: NodePath
 
 
+    cluster: Any | None
+
     def __init__(self, fStartDirect: bool = True, windowType: str | None = None) -> None:
     def __init__(self, fStartDirect: bool = True, windowType: str | None = None) -> None:
         """Opens a window, sets up a 3-D and several 2-D scene graphs, and
         """Opens a window, sets up a 3-D and several 2-D scene graphs, and
         everything else needed to render the scene graph to the window.
         everything else needed to render the scene graph to the window.
@@ -436,9 +438,9 @@ class ShowBase(DirectObject.DirectObject):
         self.useTrackball()
         self.useTrackball()
 
 
         #: `.Loader.Loader` object.
         #: `.Loader.Loader` object.
-        self.loader = Loader.Loader(self)
+        self.loader = ShowBaseGlobal.loader
+        self.loader._init_base(self)
         self.graphicsEngine.setDefaultLoader(self.loader.loader)
         self.graphicsEngine.setDefaultLoader(self.loader.loader)
-        ShowBaseGlobal.loader = self.loader
 
 
         #: The global event manager, as imported from `.EventManagerGlobal`.
         #: The global event manager, as imported from `.EventManagerGlobal`.
         self.eventMgr = eventMgr
         self.eventMgr = eventMgr
@@ -679,7 +681,7 @@ class ShowBase(DirectObject.DirectObject):
         complete.
         complete.
         """
         """
 
 
-        if Thread.getCurrentThread() != Thread.getMainThread():
+        if sys.platform != "android" and Thread.getCurrentThread() != Thread.getMainThread():
             task = taskMgr.add(self.destroy, extraArgs=[])
             task = taskMgr.add(self.destroy, extraArgs=[])
             task.wait()
             task.wait()
             return
             return
@@ -1208,7 +1210,7 @@ class ShowBase(DirectObject.DirectObject):
             self.taskMgr.remove('clientSleep')
             self.taskMgr.remove('clientSleep')
             self.taskMgr.add(self.__sleepCycleTask, 'clientSleep', sort = 55)
             self.taskMgr.add(self.__sleepCycleTask, 'clientSleep', sort = 55)
 
 
-    def __sleepCycleTask(self, task):
+    def __sleepCycleTask(self, task: object) -> int:
         Thread.sleep(self.clientSleep)
         Thread.sleep(self.clientSleep)
         #time.sleep(self.clientSleep)
         #time.sleep(self.clientSleep)
         return Task.cont
         return Task.cont
@@ -1444,7 +1446,7 @@ class ShowBase(DirectObject.DirectObject):
         self.__configAspectRatio = aspectRatio
         self.__configAspectRatio = aspectRatio
         self.adjustWindowAspectRatio(self.getAspectRatio())
         self.adjustWindowAspectRatio(self.getAspectRatio())
 
 
-    def getAspectRatio(self, win = None):
+    def getAspectRatio(self, win: GraphicsOutput | None = None) -> float:
         # Returns the actual aspect ratio of the indicated (or main
         # Returns the actual aspect ratio of the indicated (or main
         # window), or the default aspect ratio if there is not yet a
         # window), or the default aspect ratio if there is not yet a
         # main window.
         # main window.
@@ -1453,7 +1455,7 @@ class ShowBase(DirectObject.DirectObject):
         if self.__configAspectRatio:
         if self.__configAspectRatio:
             return self.__configAspectRatio
             return self.__configAspectRatio
 
 
-        aspectRatio = 1
+        aspectRatio: float = 1
 
 
         if win is None:
         if win is None:
             win = self.win
             win = self.win
@@ -1476,7 +1478,7 @@ class ShowBase(DirectObject.DirectObject):
 
 
         return aspectRatio
         return aspectRatio
 
 
-    def getSize(self, win = None):
+    def getSize(self, win: GraphicsOutput | None = None) -> tuple[int, int]:
         """
         """
         Returns the actual size of the indicated (or main window), or the
         Returns the actual size of the indicated (or main window), or the
         default size if there is not yet a main window.
         default size if there is not yet a main window.
@@ -2176,7 +2178,7 @@ class ShowBase(DirectObject.DirectObject):
                 music.setLoop(looping)
                 music.setLoop(looping)
                 music.play()
                 music.play()
 
 
-    def __resetPrevTransform(self, state):
+    def __resetPrevTransform(self, state: object) -> int:
         # Clear out the previous velocity deltas now, after we have
         # Clear out the previous velocity deltas now, after we have
         # rendered (the previous frame).  We do this after the render,
         # rendered (the previous frame).  We do this after the render,
         # so that we have a chance to draw a representation of spheres
         # so that we have a chance to draw a representation of spheres
@@ -2187,7 +2189,7 @@ class ShowBase(DirectObject.DirectObject):
         PandaNode.resetAllPrevTransform()
         PandaNode.resetAllPrevTransform()
         return Task.cont
         return Task.cont
 
 
-    def __dataLoop(self, state):
+    def __dataLoop(self, state: object) -> int:
         # Check if there were newly connected devices.
         # Check if there were newly connected devices.
         self.devices.update()
         self.devices.update()
 
 
@@ -2197,7 +2199,7 @@ class ShowBase(DirectObject.DirectObject):
         self.dgTrav.traverse(self.dataRootNode)
         self.dgTrav.traverse(self.dataRootNode)
         return Task.cont
         return Task.cont
 
 
-    def __ivalLoop(self, state):
+    def __ivalLoop(self, state: object) -> int:
         # Execute all intervals in the global ivalMgr.
         # Execute all intervals in the global ivalMgr.
         IntervalManager.ivalMgr.step()
         IntervalManager.ivalMgr.step()
         return Task.cont
         return Task.cont
@@ -2215,7 +2217,7 @@ class ShowBase(DirectObject.DirectObject):
             self.shadowTrav.traverse(self.render)
             self.shadowTrav.traverse(self.render)
         return Task.cont
         return Task.cont
 
 
-    def __collisionLoop(self, state):
+    def __collisionLoop(self, state: object) -> int:
         # run the collision traversal if we have a
         # run the collision traversal if we have a
         # CollisionTraverser set.
         # CollisionTraverser set.
         if self.cTrav:
         if self.cTrav:
@@ -2227,14 +2229,14 @@ class ShowBase(DirectObject.DirectObject):
         messenger.send("collisionLoopFinished")
         messenger.send("collisionLoopFinished")
         return Task.cont
         return Task.cont
 
 
-    def __audioLoop(self, state):
+    def __audioLoop(self, state: object) -> int:
         if self.musicManager is not None:
         if self.musicManager is not None:
             self.musicManager.update()
             self.musicManager.update()
         for x in self.sfxManagerList:
         for x in self.sfxManagerList:
             x.update()
             x.update()
         return Task.cont
         return Task.cont
 
 
-    def __garbageCollectStates(self, state):
+    def __garbageCollectStates(self, state: object) -> int:
         """ This task is started only when we have
         """ This task is started only when we have
         garbage-collect-states set in the Config.prc file, in which
         garbage-collect-states set in the Config.prc file, in which
         case we're responsible for taking out Panda's garbage from
         case we're responsible for taking out Panda's garbage from
@@ -2245,7 +2247,7 @@ class ShowBase(DirectObject.DirectObject):
         RenderState.garbageCollect()
         RenderState.garbageCollect()
         return Task.cont
         return Task.cont
 
 
-    def __igLoop(self, state):
+    def __igLoop(self, state: object) -> int:
         if __debug__:
         if __debug__:
             # We render the watch variables for the onScreenDebug as soon
             # We render the watch variables for the onScreenDebug as soon
             # as we reasonably can before the renderFrame().
             # as we reasonably can before the renderFrame().
@@ -2285,7 +2287,7 @@ class ShowBase(DirectObject.DirectObject):
         throw_new_frame()
         throw_new_frame()
         return Task.cont
         return Task.cont
 
 
-    def __igLoopSync(self, state):
+    def __igLoopSync(self, state: object) -> int:
         if __debug__:
         if __debug__:
             # We render the watch variables for the onScreenDebug as soon
             # We render the watch variables for the onScreenDebug as soon
             # as we reasonably can before the renderFrame().
             # as we reasonably can before the renderFrame().
@@ -2294,6 +2296,7 @@ class ShowBase(DirectObject.DirectObject):
         if self.recorder:
         if self.recorder:
             self.recorder.recordFrame()
             self.recorder.recordFrame()
 
 
+        assert self.cluster is not None
         self.cluster.collectData()
         self.cluster.collectData()
 
 
         # Finally, render the frame.
         # Finally, render the frame.
@@ -2323,6 +2326,7 @@ class ShowBase(DirectObject.DirectObject):
             time.sleep(0.1)
             time.sleep(0.1)
 
 
         self.graphicsEngine.readyFlip()
         self.graphicsEngine.readyFlip()
+        assert self.cluster is not None
         self.cluster.waitForFlipCommand()
         self.cluster.waitForFlipCommand()
         self.graphicsEngine.flipFrame()
         self.graphicsEngine.flipFrame()
 
 
@@ -2753,7 +2757,7 @@ class ShowBase(DirectObject.DirectObject):
                 self.oobeVis.reparentTo(self.camera)
                 self.oobeVis.reparentTo(self.camera)
             self.oobeMode = 1
             self.oobeMode = 1
 
 
-    def __oobeButton(self, suffix, button):
+    def __oobeButton(self, suffix: str, button: str) -> None:
         if button.startswith('mouse'):
         if button.startswith('mouse'):
             # Eat mouse buttons.
             # Eat mouse buttons.
             return
             return
@@ -3073,7 +3077,7 @@ class ShowBase(DirectObject.DirectObject):
         else:
         else:
             return Task.cont
             return Task.cont
 
 
-    def windowEvent(self, win):
+    def windowEvent(self, win: GraphicsOutput) -> None:
         if win != self.win:
         if win != self.win:
             # This event isn't about our window.
             # This event isn't about our window.
             return
             return
@@ -3092,9 +3096,9 @@ class ShowBase(DirectObject.DirectObject):
                 self.userExit()
                 self.userExit()
 
 
             if properties.getForeground() and not self.mainWinForeground:
             if properties.getForeground() and not self.mainWinForeground:
-                self.mainWinForeground = 1
+                self.mainWinForeground = True
             elif not properties.getForeground() and self.mainWinForeground:
             elif not properties.getForeground() and self.mainWinForeground:
-                self.mainWinForeground = 0
+                self.mainWinForeground = False
                 if __debug__:
                 if __debug__:
                     if self.__autoGarbageLogging:
                     if self.__autoGarbageLogging:
                         GarbageReport.b_checkForGarbageLeaks()
                         GarbageReport.b_checkForGarbageLeaks()
@@ -3102,12 +3106,12 @@ class ShowBase(DirectObject.DirectObject):
             if properties.getMinimized() and not self.mainWinMinimized:
             if properties.getMinimized() and not self.mainWinMinimized:
                 # If the main window is minimized, throw an event to
                 # If the main window is minimized, throw an event to
                 # stop the music.
                 # stop the music.
-                self.mainWinMinimized = 1
+                self.mainWinMinimized = True
                 messenger.send('PandaPaused')
                 messenger.send('PandaPaused')
             elif not properties.getMinimized() and self.mainWinMinimized:
             elif not properties.getMinimized() and self.mainWinMinimized:
                 # If the main window is restored, throw an event to
                 # If the main window is restored, throw an event to
                 # restart the music.
                 # restart the music.
-                self.mainWinMinimized = 0
+                self.mainWinMinimized = False
                 messenger.send('PandaRestarted')
                 messenger.send('PandaRestarted')
 
 
             # If we have not forced the aspect ratio, let's see if it has
             # If we have not forced the aspect ratio, let's see if it has
@@ -3125,7 +3129,7 @@ class ShowBase(DirectObject.DirectObject):
                     if self.wantRender2dp:
                     if self.wantRender2dp:
                         self.pixel2dp.setScale(2.0 / xsize, 1.0, 2.0 / ysize)
                         self.pixel2dp.setScale(2.0 / xsize, 1.0, 2.0 / ysize)
 
 
-    def adjustWindowAspectRatio(self, aspectRatio):
+    def adjustWindowAspectRatio(self, aspectRatio: float) -> None:
         """ This function is normally called internally by
         """ This function is normally called internally by
         `windowEvent()`, but it may also be called to explicitly adjust
         `windowEvent()`, but it may also be called to explicitly adjust
         the aspect ratio of the render/render2d DisplayRegion, by a
         the aspect ratio of the render/render2d DisplayRegion, by a
@@ -3439,7 +3443,7 @@ class ShowBase(DirectObject.DirectObject):
         This method must be called from the main thread, otherwise an error is
         This method must be called from the main thread, otherwise an error is
         thrown.
         thrown.
         """
         """
-        if Thread.getCurrentThread() != Thread.getMainThread():
+        if Thread.getCurrentThread() != Thread.getMainThread() and sys.platform != "android":
             self.notify.error("run() must be called from the main thread.")
             self.notify.error("run() must be called from the main thread.")
             return
             return
 
 

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

@@ -62,7 +62,8 @@ aspect2d = render2d.attachNewNode(PGTop("aspect2d"))
 #: A dummy scene graph that is not being rendered by anything.
 #: A dummy scene graph that is not being rendered by anything.
 hidden = NodePath("hidden")
 hidden = NodePath("hidden")
 
 
-loader: Loader
+#: The global Loader instance for models, textures, etc.
+loader = Loader()
 
 
 # Set direct notify categories now that we have config
 # Set direct notify categories now that we have config
 directNotify.setDconfigLevels()
 directNotify.setDconfigLevels()

+ 1 - 1
direct/src/task/Task.py

@@ -28,7 +28,7 @@ if hasattr(sys, 'getandroidapilevel'):
     signal = None
     signal = None
 else:
 else:
     try:
     try:
-        import _signal as signal  # type: ignore[import, no-redef]
+        import _signal as signal  # type: ignore[import-not-found, no-redef]
     except ImportError:
     except ImportError:
         signal = None
         signal = None
 
 

+ 1 - 1
dtool/Config.cmake

@@ -265,7 +265,7 @@ if(BUILD_INTERROGATE)
     panda3d-interrogate
     panda3d-interrogate
 
 
     GIT_REPOSITORY https://github.com/panda3d/interrogate.git
     GIT_REPOSITORY https://github.com/panda3d/interrogate.git
-    GIT_TAG 03418d6d7ddda7fb99abf27230aa42d1d8bd607e
+    GIT_TAG d2844d994fcc465a4e22b10001d3ac5c4012b814
 
 
     PREFIX ${_interrogate_dir}
     PREFIX ${_interrogate_dir}
     CMAKE_ARGS
     CMAKE_ARGS

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

@@ -69,6 +69,8 @@ set(P3DTOOLUTIL_IGATEEXT
   globPattern_ext.h
   globPattern_ext.h
   iostream_ext.cxx
   iostream_ext.cxx
   iostream_ext.h
   iostream_ext.h
+  pyenv_init.cxx
+  pyenv_init.h
   textEncoder_ext.cxx
   textEncoder_ext.cxx
   textEncoder_ext.h
   textEncoder_ext.h
 )
 )

+ 50 - 0
dtool/src/dtoolutil/console_preamble.js

@@ -0,0 +1,50 @@
+/**
+ * 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 console_preamble.js
+ * @author rdb
+ * @date 2025-02-03
+ */
+
+if (ENVIRONMENT_IS_NODE) {
+  Module["preInit"] = Module["preInit"] || [];
+  Module["preInit"].push(function() {
+    if (typeof process === "object" && typeof process.env === "object") {
+      // These are made up by emscripten if we don't set them to undefined
+      ENV['USER'] = undefined;
+      ENV['LOGNAME'] = undefined;
+      ENV['PATH'] = undefined;
+      ENV['PWD'] = undefined;
+      ENV['HOME'] = undefined;
+      ENV['LANG'] = undefined;
+      ENV['_'] = undefined;
+      for (var variable in process.env) {
+        ENV[variable] = process.env[variable];
+      }
+    }
+
+    addOnPreMain(function preloadNodeEnv() {
+      var sp = stackSave();
+      var set_binary_name = wasmExports["_set_binary_name"];
+      if (set_binary_name && typeof __filename === "string") {
+        set_binary_name(stringToUTF8OnStack(__filename));
+      }
+
+      var set_env_var = wasmExports["_set_env_var"];
+      if (set_env_var) {
+        for (var variable in ENV) {
+          var value = ENV[variable];
+          if (value !== undefined) {
+            set_env_var(stringToUTF8OnStack(variable), stringToUTF8OnStack(value));
+          }
+        }
+      }
+      stackRestore(sp);
+    });
+  });
+}

+ 12 - 13
dtool/src/dtoolutil/executionEnvironment.cxx

@@ -125,12 +125,18 @@ static const char *const libp3dtool_filenames[] = {
 
 
 #if defined(__EMSCRIPTEN__) && !defined(CPPPARSER)
 #if defined(__EMSCRIPTEN__) && !defined(CPPPARSER)
 extern "C" void EMSCRIPTEN_KEEPALIVE
 extern "C" void EMSCRIPTEN_KEEPALIVE
-_set_env_var(ExecutionEnvironment *ptr, const char *var, const char *value) {
+_set_env_var(const char *var, const char *value) {
+  ExecutionEnvironment *ptr = ExecutionEnvironment::get_ptr();
   ptr->_variables[std::string(var)] = std::string(value);
   ptr->_variables[std::string(var)] = std::string(value);
 }
 }
+
+extern "C" void EMSCRIPTEN_KEEPALIVE
+_set_binary_name(const char *path) {
+  ExecutionEnvironment::set_binary_name(std::string(path));
+}
 #endif
 #endif
 
 
-// Linux with GNU libc does have global argvargc variables, but we can't
+// Linux with GNU libc does have global argv/argc variables, but we can't
 // safely access them at stat init time--at least, not in libc5. (It does seem
 // safely access them at stat init time--at least, not in libc5. (It does seem
 // to work with glibc2, however.)
 // to work with glibc2, however.)
 
 
@@ -584,17 +590,10 @@ read_environment_variables() {
     }
     }
   }
   }
 #elif defined(__EMSCRIPTEN__)
 #elif defined(__EMSCRIPTEN__)
-  // We only have environment variables if we're running in node.js.
-#ifndef CPPPARSER
-  EM_ASM({
-    if (typeof process === 'object' && typeof process.env === 'object') {
-      for (var variable in process.env) {
-        __set_env_var($0, stringToUTF8OnStack(variable),
-                          stringToUTF8OnStack(process.env[variable]));
-      }
-    }
-  }, this);
-#endif
+  // The environment variables get loaded in by the .js file before main()
+  // using the _set_env_var exported function, defined above.  Trying to load
+  // env vars at static init time otherwise makes some optimizations more
+  // difficult, notably wasm-ctor-eval/wizer.
 
 
 #elif defined(HAVE_PROC_SELF_ENVIRON)
 #elif defined(HAVE_PROC_SELF_ENVIRON)
   // In some cases, we may have a file called procselfenviron that may be read
   // In some cases, we may have a file called procselfenviron that may be read

+ 4 - 4
dtool/src/dtoolutil/executionEnvironment.h

@@ -22,10 +22,10 @@
 #include <map>
 #include <map>
 
 
 #if defined(__EMSCRIPTEN__) && !defined(CPPPARSER)
 #if defined(__EMSCRIPTEN__) && !defined(CPPPARSER)
-class ExecutionEnvironment;
-
 extern "C" void EMSCRIPTEN_KEEPALIVE
 extern "C" void EMSCRIPTEN_KEEPALIVE
-_set_env_var(ExecutionEnvironment *ptr, const char *var, const char *value);
+_set_env_var(const char *var, const char *value);
+extern "C" void EMSCRIPTEN_KEEPALIVE
+_set_binary_name(const char *path);
 #endif
 #endif
 
 
 /**
 /**
@@ -98,7 +98,7 @@ private:
   static ExecutionEnvironment *_global_ptr;
   static ExecutionEnvironment *_global_ptr;
 
 
 #ifdef __EMSCRIPTEN__
 #ifdef __EMSCRIPTEN__
-  friend void ::_set_env_var(ExecutionEnvironment *ptr, const char *var, const char *value);
+  friend void ::_set_env_var(const char *var, const char *value);
 #endif
 #endif
 };
 };
 
 

+ 1 - 0
dtool/src/dtoolutil/p3dtoolutil_ext_composite.cxx

@@ -1,4 +1,5 @@
 #include "filename_ext.cxx"
 #include "filename_ext.cxx"
 #include "globPattern_ext.cxx"
 #include "globPattern_ext.cxx"
 #include "iostream_ext.cxx"
 #include "iostream_ext.cxx"
+#include "pyenv_init.cxx"
 #include "textEncoder_ext.cxx"
 #include "textEncoder_ext.cxx"

+ 80 - 0
dtool/src/dtoolutil/pyenv_init.cxx

@@ -0,0 +1,80 @@
+/**
+ * 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 pyenv_init.cxx
+ * @author rdb
+ * @date 2025-02-02
+ */
+
+#include "pyenv_init.h"
+#include "py_panda.h"
+#include "executionEnvironment.h"
+
+/**
+ * Called when panda3d.core is initialized, does some initialization specific
+ * to the Python environment.
+ */
+void
+pyenv_init() {
+  // MAIN_DIR needs to be set very early; this seems like a convenient place
+  // to do that.  Perhaps we'll find a better place for this in the future.
+  static bool initialized_main_dir = false;
+  if (!initialized_main_dir) {
+    /*if (interrogatedb_cat.is_debug()) {
+      // Good opportunity to print this out once, at startup.
+      interrogatedb_cat.debug()
+        << "Python " << version << "\n";
+    }*/
+
+    if (!ExecutionEnvironment::has_environment_variable("MAIN_DIR")) {
+      // Grab the __main__ module and extract its __file__ attribute.
+      Filename main_dir;
+      PyObject *main_module = PyImport_ImportModule("__main__");
+      PyObject *file_attr = nullptr;
+      if (main_module != nullptr) {
+        file_attr = PyObject_GetAttrString(main_module, "__file__");
+      } else {
+        std::cerr << "Warning: unable to import __main__\n";
+      }
+      if (file_attr == nullptr) {
+        // Must be running in the interactive interpreter.  Use the CWD.
+        main_dir = ExecutionEnvironment::get_cwd();
+      } else {
+#if PY_MAJOR_VERSION >= 3
+        Py_ssize_t length;
+        wchar_t *buffer = PyUnicode_AsWideCharString(file_attr, &length);
+        if (buffer != nullptr) {
+          main_dir = Filename::from_os_specific_w(std::wstring(buffer, length));
+          main_dir.make_absolute();
+          main_dir = main_dir.get_dirname();
+          PyMem_Free(buffer);
+        }
+#else
+        char *buffer;
+        Py_ssize_t length;
+        if (PyString_AsStringAndSize(file_attr, &buffer, &length) != -1) {
+          main_dir = Filename::from_os_specific(std::string(buffer, length));
+          main_dir.make_absolute();
+          main_dir = main_dir.get_dirname();
+        }
+#endif
+        else {
+          std::cerr << "Invalid string for __main__.__file__\n";
+        }
+      }
+      ExecutionEnvironment::shadow_environment_variable("MAIN_DIR", main_dir.to_os_specific());
+      PyErr_Clear();
+    }
+    initialized_main_dir = true;
+  }
+
+  // Also, while we are at it, initialize the thread swap hook.
+#if defined(HAVE_THREADS) && defined(SIMPLE_THREADS)
+  global_thread_state_swap = PyThreadState_Swap;
+#endif
+}

+ 14 - 0
dtool/src/dtoolutil/pyenv_init.h

@@ -0,0 +1,14 @@
+/**
+ * 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 pyenv_init.h
+ * @author rdb
+ * @date 2025-02-02
+ */
+
+extern "C" void pyenv_init();

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

@@ -467,12 +467,12 @@ config_initialized() {
 
 
   Notify *ptr = Notify::ptr();
   Notify *ptr = Notify::ptr();
 
 
-  for (int i = 0; i <= NS_fatal; ++i) {
+  for (int severity = 0; severity <= NS_fatal; ++severity) {
     int priority = ANDROID_LOG_UNKNOWN;
     int priority = ANDROID_LOG_UNKNOWN;
     if (severity != NS_unspecified) {
     if (severity != NS_unspecified) {
-      priority = i + 1;
+      priority = severity + 1;
     }
     }
-    ptr->_log_streams[i] = new AndroidLogStream(priority);
+    ptr->_log_streams[severity] = new AndroidLogStream(priority);
   }
   }
 
 
 #elif defined(__EMSCRIPTEN__)
 #elif defined(__EMSCRIPTEN__)

+ 1 - 1
makepanda/makepackage.py

@@ -863,7 +863,7 @@ def MakeInstallerAndroid(version, **kwargs):
         shutil.copy(source, target)
         shutil.copy(source, target)
 
 
         # Walk through the library dependencies.
         # Walk through the library dependencies.
-        handle = subprocess.Popen(['readelf', '--dynamic', target], stdout=subprocess.PIPE)
+        handle = subprocess.Popen(['llvm-readelf', '--dynamic', target], stdout=subprocess.PIPE)
         for line in handle.communicate()[0].splitlines():
         for line in handle.communicate()[0].splitlines():
             # The line will look something like:
             # The line will look something like:
             # 0x0000000000000001 (NEEDED)             Shared library: [libpanda.so]
             # 0x0000000000000001 (NEEDED)             Shared library: [libpanda.so]

+ 33 - 10
makepanda/makepanda.py

@@ -498,6 +498,8 @@ elif not CrossCompiling():
 else:
 else:
     if target_arch == 'amd64':
     if target_arch == 'amd64':
         target_arch = 'x86_64'
         target_arch = 'x86_64'
+    if target_arch == 'arm' and target == 'android':
+        target_arch = 'armv7a'
     PLATFORM = '{0}-{1}'.format(target, target_arch)
     PLATFORM = '{0}-{1}'.format(target, target_arch)
 
 
 
 
@@ -1420,10 +1422,10 @@ def CompileCxx(obj,src,opts):
                 cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/')
                 cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/')
             cmd += ' -ffunction-sections -funwind-tables'
             cmd += ' -ffunction-sections -funwind-tables'
             cmd += ' -target ' + SDK["ANDROID_TRIPLE"]
             cmd += ' -target ' + SDK["ANDROID_TRIPLE"]
-            if arch == 'armv7a':
+            if arch in ('armv7a', 'arm'):
                 cmd += ' -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16'
                 cmd += ' -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16'
-            elif arch == 'arm':
-                cmd += ' -march=armv5te -mtune=xscale -msoft-float'
+            #elif arch == 'arm':
+            #    cmd += ' -march=armv5te -mtune=xscale -msoft-float'
             elif arch == 'mips':
             elif arch == 'mips':
                 cmd += ' -mips32'
                 cmd += ' -mips32'
             elif arch == 'mips64':
             elif arch == 'mips64':
@@ -1698,6 +1700,9 @@ def CompileImod(wobj, wsrc, opts):
     importmod = GetValueOption(opts, "IMPORT:")
     importmod = GetValueOption(opts, "IMPORT:")
     if importmod:
     if importmod:
         cmd += ' -import ' + importmod
         cmd += ' -import ' + importmod
+    initfunc = GetValueOption(opts, "INIT:")
+    if initfunc:
+        cmd += ' -init ' + initfunc
     for x in wsrc: cmd += ' ' + BracketNameWithQuotes(x)
     for x in wsrc: cmd += ' ' + BracketNameWithQuotes(x)
     oscmd(cmd)
     oscmd(cmd)
     CompileCxx(wobj,woutc,opts)
     CompileCxx(wobj,woutc,opts)
@@ -1946,17 +1951,21 @@ def CompileLink(dll, obj, opts):
                 cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/')
                 cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/')
             cmd += " -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now"
             cmd += " -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now"
             cmd += ' -target ' + SDK["ANDROID_TRIPLE"]
             cmd += ' -target ' + SDK["ANDROID_TRIPLE"]
-            if arch == 'armv7a':
+            if arch in ('armv7a', 'arm'):
                 cmd += " -march=armv7-a -Wl,--fix-cortex-a8"
                 cmd += " -march=armv7-a -Wl,--fix-cortex-a8"
             elif arch == 'mips':
             elif arch == 'mips':
                 cmd += ' -mips32'
                 cmd += ' -mips32'
             cmd += ' -lc -lm'
             cmd += ' -lc -lm'
 
 
         elif GetTarget() == 'emscripten':
         elif GetTarget() == 'emscripten':
-            cmd += " -s WARN_ON_UNDEFINED_SYMBOLS=1"
+            cmd += " -s WARN_ON_UNDEFINED_SYMBOLS=1 -mbulk-memory"
+
             if GetOrigExt(dll) == ".exe":
             if GetOrigExt(dll) == ".exe":
                 cmd += " -s EXIT_RUNTIME=1"
                 cmd += " -s EXIT_RUNTIME=1"
 
 
+                if dll.endswith(".js") and "SUBSYSTEM:WINDOWS" not in opts:
+                    cmd += " --pre-js dtool/src/dtoolutil/console_preamble.js"
+
         else:
         else:
             cmd += " -pthread"
             cmd += " -pthread"
             if "SYSROOT" in SDK:
             if "SYSROOT" in SDK:
@@ -2121,13 +2130,23 @@ def CompileJava(target, src, opts):
     if GetHost() == 'android':
     if GetHost() == 'android':
         cmd = "ecj "
         cmd = "ecj "
     else:
     else:
-        cmd = "javac -bootclasspath " + BracketNameWithQuotes(SDK["ANDROID_JAR"]) + " "
+        cmd = "javac "
+        home = os.environ.get('JAVA_HOME')
+        if home:
+            javac_path = os.path.join(home, 'bin', 'javac')
+            if GetHost() == 'windows':
+                javac_path += '.exe'
+            if os.path.isfile(javac_path):
+                cmd = BracketNameWithQuotes(javac_path) + " "
+
+        cmd += "-Xlint:deprecation "
 
 
     optlevel = GetOptimizeOption(opts)
     optlevel = GetOptimizeOption(opts)
     if optlevel >= 4:
     if optlevel >= 4:
         cmd += "-debug:none "
         cmd += "-debug:none "
 
 
-    cmd += "-cp " + GetOutputDir() + "/classes "
+    classpath = BracketNameWithQuotes(SDK["ANDROID_JAR"] + ":" + GetOutputDir() + "/classes")
+    cmd += "-cp " + classpath + " "
     cmd += "-d " + GetOutputDir() + "/classes "
     cmd += "-d " + GetOutputDir() + "/classes "
     cmd += BracketNameWithQuotes(src)
     cmd += BracketNameWithQuotes(src)
     oscmd(cmd)
     oscmd(cmd)
@@ -4065,7 +4084,9 @@ TargetAdd('libp3pgui.in', opts=['IMOD:panda3d.core', 'ILIB:libp3pgui', 'SRCDIR:p
 # DIRECTORY: panda/src/pnmimagetypes/
 # DIRECTORY: panda/src/pnmimagetypes/
 #
 #
 
 
-OPTS=['DIR:panda/src/pnmimagetypes', 'DIR:panda/src/pnmimage', 'BUILDING:PANDA', 'PNG', 'ZLIB', 'JPEG', 'TIFF', 'OPENEXR', 'EXCEPTIONS']
+OPTS=['DIR:panda/src/pnmimagetypes', 'DIR:panda/src/pnmimage', 'BUILDING:PANDA', 'PNG', 'ZLIB', 'JPEG', 'TIFF', 'OPENEXR']
+if not PkgSkip('OPENEXR') and GetTarget() != 'emscripten':
+    OPTS.append('EXCEPTIONS')
 TargetAdd('p3pnmimagetypes_composite1.obj', opts=OPTS, input='p3pnmimagetypes_composite1.cxx')
 TargetAdd('p3pnmimagetypes_composite1.obj', opts=OPTS, input='p3pnmimagetypes_composite1.cxx')
 TargetAdd('p3pnmimagetypes_composite2.obj', opts=OPTS, input='p3pnmimagetypes_composite2.cxx')
 TargetAdd('p3pnmimagetypes_composite2.obj', opts=OPTS, input='p3pnmimagetypes_composite2.cxx')
 
 
@@ -4213,7 +4234,7 @@ if GetTarget() != "emscripten":
 if PkgSkip("FREETYPE")==0:
 if PkgSkip("FREETYPE")==0:
     PyTargetAdd('core_module.obj', input='libp3pnmtext.in')
     PyTargetAdd('core_module.obj', input='libp3pnmtext.in')
 
 
-PyTargetAdd('core_module.obj', opts=['IMOD:panda3d.core', 'ILIB:core'])
+PyTargetAdd('core_module.obj', opts=['IMOD:panda3d.core', 'ILIB:core', 'INIT:pyenv_init'])
 
 
 PyTargetAdd('core.pyd', input='libp3dtoolbase_igate.obj')
 PyTargetAdd('core.pyd', input='libp3dtoolbase_igate.obj')
 PyTargetAdd('core.pyd', input='p3dtoolbase_typeHandle_ext.obj')
 PyTargetAdd('core.pyd', input='p3dtoolbase_typeHandle_ext.obj')
@@ -4974,11 +4995,13 @@ if GetTarget() == 'android':
     TargetAdd('org/panda3d/android/NativeIStream.class', opts=OPTS, input='NativeIStream.java')
     TargetAdd('org/panda3d/android/NativeIStream.class', opts=OPTS, input='NativeIStream.java')
     TargetAdd('org/panda3d/android/NativeOStream.class', opts=OPTS, input='NativeOStream.java')
     TargetAdd('org/panda3d/android/NativeOStream.class', opts=OPTS, input='NativeOStream.java')
     TargetAdd('org/panda3d/android/PandaActivity.class', opts=OPTS, input='PandaActivity.java')
     TargetAdd('org/panda3d/android/PandaActivity.class', opts=OPTS, input='PandaActivity.java')
+    TargetAdd('org/panda3d/android/PandaActivity$1.class', opts=OPTS+['DEPENDENCYONLY'], input='PandaActivity.java')
     TargetAdd('org/panda3d/android/PythonActivity.class', opts=OPTS, input='PythonActivity.java')
     TargetAdd('org/panda3d/android/PythonActivity.class', opts=OPTS, input='PythonActivity.java')
 
 
     TargetAdd('classes.dex', input='org/panda3d/android/NativeIStream.class')
     TargetAdd('classes.dex', input='org/panda3d/android/NativeIStream.class')
     TargetAdd('classes.dex', input='org/panda3d/android/NativeOStream.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.class')
+    TargetAdd('classes.dex', input='org/panda3d/android/PandaActivity$1.class')
     TargetAdd('classes.dex', input='org/panda3d/android/PythonActivity.class')
     TargetAdd('classes.dex', input='org/panda3d/android/PythonActivity.class')
 
 
     TargetAdd('p3android_composite1.obj', opts=OPTS, input='p3android_composite1.cxx')
     TargetAdd('p3android_composite1.obj', opts=OPTS, input='p3android_composite1.cxx')
@@ -6023,7 +6046,7 @@ if GetLinkAllStatic():
     if not PkgSkip('BULLET'):
     if not PkgSkip('BULLET'):
         DefSymbol('RUN_TESTS_FLAGS', 'HAVE_BULLET')
         DefSymbol('RUN_TESTS_FLAGS', 'HAVE_BULLET')
 
 
-    OPTS=['DIR:tests', 'PYTHON', 'RUN_TESTS_FLAGS']
+    OPTS=['DIR:tests', 'PYTHON', 'RUN_TESTS_FLAGS', 'SUBSYSTEM:CONSOLE']
     PyTargetAdd('run_tests-main.obj', opts=OPTS, input='main.c')
     PyTargetAdd('run_tests-main.obj', opts=OPTS, input='main.c')
     PyTargetAdd('run_tests.exe', input='run_tests-main.obj')
     PyTargetAdd('run_tests.exe', input='run_tests-main.obj')
     PyTargetAdd('run_tests.exe', input='core.pyd')
     PyTargetAdd('run_tests.exe', input='core.pyd')

+ 17 - 14
makepanda/makepandacore.py

@@ -357,11 +357,11 @@ def SetTarget(target, arch=None):
 
 
     elif target == 'android' or target.startswith('android-'):
     elif target == 'android' or target.startswith('android-'):
         if arch is None:
         if arch is None:
-            # If compiling on Android, default to same architecture.  Otherwise, arm.
+            # If compiling on Android, default to same architecture.
             if host == 'android':
             if host == 'android':
                 arch = host_arch
                 arch = host_arch
             else:
             else:
-                arch = 'armv7a'
+                exit('Specify an Android architecture using --arch')
 
 
         if arch == 'aarch64':
         if arch == 'aarch64':
             arch = 'arm64'
             arch = 'arm64'
@@ -371,12 +371,9 @@ def SetTarget(target, arch=None):
         target, _, api = target.partition('-')
         target, _, api = target.partition('-')
         if api:
         if api:
             ANDROID_API = int(api)
             ANDROID_API = int(api)
-        elif arch in ('mips64', 'arm64', 'x86_64'):
-            # 64-bit platforms were introduced in Android 21.
-            ANDROID_API = 21
         else:
         else:
             # Default to the lowest API level still supported by Google.
             # Default to the lowest API level still supported by Google.
-            ANDROID_API = 19
+            ANDROID_API = 21
 
 
         # Determine the prefix for our gcc tools, eg. arm-linux-androideabi-gcc
         # Determine the prefix for our gcc tools, eg. arm-linux-androideabi-gcc
         global ANDROID_ABI, ANDROID_TRIPLE
         global ANDROID_ABI, ANDROID_TRIPLE
@@ -592,8 +589,8 @@ def GetInterrogateDir():
             return INTERROGATE_DIR
             return INTERROGATE_DIR
 
 
         dir = os.path.join(GetOutputDir(), "tmp", "interrogate")
         dir = os.path.join(GetOutputDir(), "tmp", "interrogate")
-        if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.4.0.dist-info")):
-            oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.4.0" % (sys.executable, dir))
+        if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.5.0.dist-info")):
+            oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.5.0" % (sys.executable, dir))
 
 
         INTERROGATE_DIR = dir
         INTERROGATE_DIR = dir
 
 
@@ -669,11 +666,12 @@ def oscmd(cmd, ignoreError = False, cwd=None):
         print(GetColor("blue") + cmd.split(" ", 1)[0] + " " + GetColor("magenta") + cmd.split(" ", 1)[1] + GetColor())
         print(GetColor("blue") + cmd.split(" ", 1)[0] + " " + GetColor("magenta") + cmd.split(" ", 1)[1] + GetColor())
     sys.stdout.flush()
     sys.stdout.flush()
 
 
+    if cmd[0] == '"':
+        exe = cmd[1 : cmd.index('"', 1)]
+    else:
+        exe = cmd.split()[0]
+
     if sys.platform == "win32":
     if sys.platform == "win32":
-        if cmd[0] == '"':
-            exe = cmd[1 : cmd.index('"', 1)]
-        else:
-            exe = cmd.split()[0]
         exe_path = LocateBinary(exe)
         exe_path = LocateBinary(exe)
         if exe_path is None:
         if exe_path is None:
             exit("Cannot find "+exe+" on search path")
             exit("Cannot find "+exe+" on search path")
@@ -709,7 +707,7 @@ def oscmd(cmd, ignoreError = False, cwd=None):
             exit("")
             exit("")
 
 
     if res != 0 and not ignoreError:
     if res != 0 and not ignoreError:
-        if "interrogate" in cmd.split(" ", 1)[0] and GetVerbose():
+        if "interrogate" in exe and "interrogate_module" not in exe and GetVerbose():
             print(ColorText("red", "Interrogate failed, retrieving debug output..."))
             print(ColorText("red", "Interrogate failed, retrieving debug output..."))
             sys.stdout.flush()
             sys.stdout.flush()
             verbose_cmd = cmd.split(" ", 1)[0] + " -vv " + cmd.split(" ", 1)[1]
             verbose_cmd = cmd.split(" ", 1)[0] + " -vv " + cmd.split(" ", 1)[1]
@@ -1422,6 +1420,9 @@ def GetThirdpartyDir():
     elif (target == 'android'):
     elif (target == 'android'):
         THIRDPARTYDIR = base + "/android-libs-%s/" % (target_arch)
         THIRDPARTYDIR = base + "/android-libs-%s/" % (target_arch)
 
 
+        if target_arch == 'armv7a' and not os.path.isdir(THIRDPARTYDIR):
+            THIRDPARTYDIR = base + "/android-libs-arm/"
+
     elif (target == 'emscripten'):
     elif (target == 'emscripten'):
         THIRDPARTYDIR = base + "/emscripten-libs/"
         THIRDPARTYDIR = base + "/emscripten-libs/"
 
 
@@ -1858,7 +1859,7 @@ def SmartPkgEnable(pkg, pkgconfig = None, libs = None, incs = None, defs = None,
             DefSymbol(target_pkg, d, v)
             DefSymbol(target_pkg, d, v)
         return
         return
 
 
-    elif not custom_loc and GetHost() == "darwin" and framework is not None:
+    elif not custom_loc and GetHost() == "darwin" and GetTarget() == "darwin" and framework is not None:
         prefix = SDK["MACOSX"]
         prefix = SDK["MACOSX"]
         if (os.path.isdir(prefix + "/Library/Frameworks/%s.framework" % framework) or
         if (os.path.isdir(prefix + "/Library/Frameworks/%s.framework" % framework) or
             os.path.isdir(prefix + "/System/Library/Frameworks/%s.framework" % framework) or
             os.path.isdir(prefix + "/System/Library/Frameworks/%s.framework" % framework) or
@@ -2639,6 +2640,8 @@ def SdkLocateAndroid():
     # We need to redistribute the C++ standard library.
     # We need to redistribute the C++ standard library.
     stdlibc = os.path.join(ndk_root, 'sources', 'cxx-stl', 'llvm-libc++')
     stdlibc = os.path.join(ndk_root, 'sources', 'cxx-stl', 'llvm-libc++')
     stl_lib = os.path.join(stdlibc, 'libs', abi, 'libc++_shared.so')
     stl_lib = os.path.join(stdlibc, 'libs', abi, 'libc++_shared.so')
+    if not os.path.isfile(stl_lib):
+        stl_lib = os.path.join(prebuilt_dir, 'sysroot', 'usr', 'lib', ANDROID_TRIPLE.rstrip('0123456789'), 'libc++_shared.so')
     CopyFile(os.path.join(GetOutputDir(), 'lib', 'libc++_shared.so'), stl_lib)
     CopyFile(os.path.join(GetOutputDir(), 'lib', 'libc++_shared.so'), stl_lib)
 
 
     # The Android support library polyfills C++ features not available in the
     # The Android support library polyfills C++ features not available in the

+ 6 - 0
makepanda/makewheel.py

@@ -88,7 +88,9 @@ MANYLINUX_LIBS = [
     # These are not mentioned in manylinux1 spec but should nonetheless always
     # These are not mentioned in manylinux1 spec but should nonetheless always
     # be excluded.
     # be excluded.
     "linux-vdso.so.1", "linux-gate.so.1", "ld-linux.so.2", "libdrm.so.2",
     "linux-vdso.so.1", "linux-gate.so.1", "ld-linux.so.2", "libdrm.so.2",
+    "ld-linux-x86-64.so.2", "ld-linux-aarch64.so.1",
     "libEGL.so.1", "libOpenGL.so.0", "libGLX.so.0", "libGLdispatch.so.0",
     "libEGL.so.1", "libOpenGL.so.0", "libGLX.so.0", "libGLdispatch.so.0",
+    "libGLESv2.so.2",
 ]
 ]
 
 
 # Binaries to never scan for dependencies on non-Windows systems.
 # Binaries to never scan for dependencies on non-Windows systems.
@@ -677,6 +679,7 @@ def makewheel(version, output_dir, platform=None):
         or platform.startswith('win_') \
         or platform.startswith('win_') \
         or platform.startswith('cygwin_')
         or platform.startswith('cygwin_')
     is_macosx = platform.startswith('macosx_')
     is_macosx = platform.startswith('macosx_')
+    is_android = platform.startswith('android_')
 
 
     # Global filepaths
     # Global filepaths
     panda3d_dir = join(output_dir, "panda3d")
     panda3d_dir = join(output_dir, "panda3d")
@@ -747,6 +750,9 @@ def makewheel(version, output_dir, platform=None):
     elif is_macosx:
     elif is_macosx:
         pylib_name = 'libpython{0}.{1}{2}.dylib'.format(sys.version_info[0], sys.version_info[1], suffix)
         pylib_name = 'libpython{0}.{1}{2}.dylib'.format(sys.version_info[0], sys.version_info[1], suffix)
         pylib_path = os.path.join(get_config_var('LIBDIR'), pylib_name)
         pylib_path = os.path.join(get_config_var('LIBDIR'), pylib_name)
+    elif is_android and CrossCompiling():
+        pylib_name = 'libpython{0}.{1}{2}.so'.format(sys.version_info[0], sys.version_info[1], suffix)
+        pylib_path = os.path.join(GetThirdpartyDir(), 'python', 'lib', pylib_name)
     else:
     else:
         pylib_name = get_config_var('LDLIBRARY')
         pylib_name = get_config_var('LDLIBRARY')
         pylib_arch = get_config_var('MULTIARCH')
         pylib_arch = get_config_var('MULTIARCH')

+ 3 - 0
mypy.ini

@@ -11,3 +11,6 @@ ignore_missing_imports = True
 
 
 [mypy-Pmw.*]
 [mypy-Pmw.*]
 ignore_missing_imports = True
 ignore_missing_imports = True
+
+[mypy-imp]
+ignore_missing_imports = True

+ 1 - 1
panda/CMakeLists.txt

@@ -108,7 +108,7 @@ if(HAVE_FREETYPE)
 endif()
 endif()
 
 
 if(INTERROGATE_PYTHON_INTERFACE)
 if(INTERROGATE_PYTHON_INTERFACE)
-  add_python_module(panda3d.core ${CORE_MODULE_COMPONENTS} LINK panda)
+  add_python_module(panda3d.core ${CORE_MODULE_COMPONENTS} LINK panda INIT pyenv_init)
 
 
   # Generate our __init__.py
   # Generate our __init__.py
   if(WIN32)
   if(WIN32)

+ 11 - 7
panda/src/android/android_main.cxx

@@ -75,6 +75,7 @@ void android_main(struct android_app* app) {
     << "New native activity started on " << *current_thread << "\n";
     << "New native activity started on " << *current_thread << "\n";
 
 
   // Were we given an optional location to write the stdout/stderr streams?
   // Were we given an optional location to write the stdout/stderr streams?
+  bool owns_stdout = false;
   methodID = env->GetMethodID(activity_class, "getIntentOutputUri", "()Ljava/lang/String;");
   methodID = env->GetMethodID(activity_class, "getIntentOutputUri", "()Ljava/lang/String;");
   jstring joutput_uri = (jstring) env->CallObjectMethod(activity->clazz, methodID);
   jstring joutput_uri = (jstring) env->CallObjectMethod(activity->clazz, methodID);
   if (joutput_uri != nullptr) {
   if (joutput_uri != nullptr) {
@@ -92,6 +93,7 @@ void android_main(struct android_app* app) {
 
 
           dup2(fd, 1);
           dup2(fd, 1);
           dup2(fd, 2);
           dup2(fd, 2);
+          owns_stdout = true;
         } else {
         } else {
           android_cat.error()
           android_cat.error()
             << "Failed to open output path " << path << "\n";
             << "Failed to open output path " << path << "\n";
@@ -109,6 +111,7 @@ void android_main(struct android_app* app) {
             << spec.get_server_and_port() << "\n";
             << spec.get_server_and_port() << "\n";
           dup2(fd, 1);
           dup2(fd, 1);
           dup2(fd, 2);
           dup2(fd, 2);
+          owns_stdout = true;
         } else {
         } else {
           android_cat.error()
           android_cat.error()
             << "Failed to open output socket "
             << "Failed to open output socket "
@@ -267,11 +270,10 @@ void android_main(struct android_app* app) {
 
 
     // We still need to keep an event loop going until Android gives us leave
     // We still need to keep an event loop going until Android gives us leave
     // to end the process.
     // to end the process.
-    int looper_id;
-    int events;
-    struct android_poll_source *source;
-    while ((looper_id = ALooper_pollAll(-1, nullptr, &events, (void**)&source)) >= 0) {
-      // Process this event, but intercept application command events.
+    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) {
       if (looper_id == LOOPER_ID_MAIN) {
         int8_t cmd = android_app_read_cmd(app);
         int8_t cmd = android_app_read_cmd(app);
         android_app_pre_exec_cmd(app, cmd);
         android_app_pre_exec_cmd(app, cmd);
@@ -300,8 +302,10 @@ void android_main(struct android_app* app) {
     env->ReleaseStringUTFChars(filename, filename_str);
     env->ReleaseStringUTFChars(filename, filename_str);
   }
   }
 
 
-  close(1);
-  close(2);
+  if (owns_stdout) {
+    close(1);
+    close(2);
+  }
 
 
   // Detach the thread before exiting.
   // Detach the thread before exiting.
   activity->vm->DetachCurrentThread();
   activity->vm->DetachCurrentThread();

+ 1 - 1
panda/src/android/pview_manifest.xml

@@ -8,7 +8,7 @@
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.INTERNET" />
-    <uses-sdk android:minSdkVersion="21" />
+    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
     <uses-feature android:glEsVersion="0x00020000" android:required="true" />
     <uses-feature android:glEsVersion="0x00020000" android:required="true" />
 
 
     <application android:label="Panda Viewer" android:hasCode="true" android:debuggable="true">
     <application android:label="Panda Viewer" android:hasCode="true" android:debuggable="true">

+ 11 - 12
panda/src/androiddisplay/androidGraphicsWindow.cxx

@@ -181,16 +181,14 @@ process_events() {
   GraphicsWindow::process_events();
   GraphicsWindow::process_events();
 
 
   // Read all pending events.
   // Read all pending events.
-  int looper_id;
-  int events;
-  struct android_poll_source* source;
-
-  // Loop until all events are read.
-  while ((looper_id = ALooper_pollAll(0, nullptr, &events, (void**)&source)) >= 0) {
-    // Process this event.
-    if (source != nullptr) {
-      source->process(_app, source);
-    }
+  struct android_poll_source *source;
+
+  auto result = ALooper_pollOnce(0, nullptr, nullptr, (void **)&source);
+  nassertv(result != ALOOPER_POLL_ERROR);
+
+  // Process this event.
+  if (source != nullptr) {
+    source->process(_app, source);
   }
   }
 }
 }
 
 
@@ -437,14 +435,15 @@ ns_handle_command(int32_t command) {
     case APP_CMD_WINDOW_RESIZED:
     case APP_CMD_WINDOW_RESIZED:
       properties.set_size(ANativeWindow_getWidth(_app->window),
       properties.set_size(ANativeWindow_getWidth(_app->window),
                           ANativeWindow_getHeight(_app->window));
                           ANativeWindow_getHeight(_app->window));
+      system_changed_properties(properties);
       break;
       break;
     case APP_CMD_WINDOW_REDRAW_NEEDED:
     case APP_CMD_WINDOW_REDRAW_NEEDED:
       break;
       break;
     case APP_CMD_CONTENT_RECT_CHANGED:
     case APP_CMD_CONTENT_RECT_CHANGED:
-      properties.set_origin(_app->contentRect.left, _app->contentRect.top);
+      /*properties.set_origin(_app->contentRect.left, _app->contentRect.top);
       properties.set_size(_app->contentRect.right - _app->contentRect.left,
       properties.set_size(_app->contentRect.right - _app->contentRect.left,
                           _app->contentRect.bottom - _app->contentRect.top);
                           _app->contentRect.bottom - _app->contentRect.top);
-      system_changed_properties(properties);
+      system_changed_properties(properties);*/
       break;
       break;
     case APP_CMD_GAINED_FOCUS:
     case APP_CMD_GAINED_FOCUS:
       properties.set_foreground(true);
       properties.set_foreground(true);

+ 2 - 1
panda/src/cocoagldisplay/cocoaGLGraphicsBuffer.mm

@@ -98,7 +98,8 @@ open_buffer() {
     // If the old gsg has the wrong pixel format, create a new one that shares
     // If the old gsg has the wrong pixel format, create a new one that shares
     // with the old gsg.
     // with the old gsg.
     DCAST_INTO_R(cocoagsg, _gsg, false);
     DCAST_INTO_R(cocoagsg, _gsg, false);
-    if (!cocoagsg->get_fb_properties().subsumes(_fb_properties)) {
+    if (cocoagsg->get_engine() != _engine ||
+        !cocoagsg->get_fb_properties().subsumes(_fb_properties)) {
       cocoagsg = new CocoaGLGraphicsStateGuardian(_engine, _pipe, cocoagsg);
       cocoagsg = new CocoaGLGraphicsStateGuardian(_engine, _pipe, cocoagsg);
       cocoagsg->choose_pixel_format(_fb_properties, cocoa_pipe->get_display_id(), false);
       cocoagsg->choose_pixel_format(_fb_properties, cocoa_pipe->get_display_id(), false);
       _gsg = cocoagsg;
       _gsg = cocoagsg;

+ 2 - 2
panda/src/cocoagldisplay/cocoaGLGraphicsPipe.mm

@@ -125,12 +125,12 @@ make_output(const std::string &name,
         precertify = true;
         precertify = true;
       }
       }
     }
     }
-    if (host != nullptr) {
+    if (host != nullptr && host->get_engine() == engine) {
       return new GLGraphicsBuffer(engine, this, name, fb_prop, win_prop,
       return new GLGraphicsBuffer(engine, this, name, fb_prop, win_prop,
                                   flags, gsg, host);
                                   flags, gsg, host);
     } else {
     } else {
       return new CocoaGLGraphicsBuffer(engine, this, name, fb_prop, win_prop,
       return new CocoaGLGraphicsBuffer(engine, this, name, fb_prop, win_prop,
-                                       flags, gsg, host);
+                                       flags, gsg, nullptr);
     }
     }
   }
   }
 
 

+ 2 - 1
panda/src/cocoagldisplay/cocoaGLGraphicsWindow.mm

@@ -200,7 +200,8 @@ open_window() {
     // If the old gsg has the wrong pixel format, create a new one that shares
     // If the old gsg has the wrong pixel format, create a new one that shares
     // with the old gsg.
     // with the old gsg.
     DCAST_INTO_R(cocoagsg, _gsg, false);
     DCAST_INTO_R(cocoagsg, _gsg, false);
-    if (!cocoagsg->get_fb_properties().subsumes(_fb_properties)) {
+    if (cocoagsg->get_engine() != _engine ||
+        !cocoagsg->get_fb_properties().subsumes(_fb_properties)) {
       cocoagsg = new CocoaGLGraphicsStateGuardian(_engine, _pipe, cocoagsg);
       cocoagsg = new CocoaGLGraphicsStateGuardian(_engine, _pipe, cocoagsg);
       cocoagsg->choose_pixel_format(_fb_properties, _display, false);
       cocoagsg->choose_pixel_format(_fb_properties, _display, false);
       _gsg = cocoagsg;
       _gsg = cocoagsg;

+ 2 - 0
panda/src/collide/CMakeLists.txt

@@ -74,6 +74,8 @@ set(P3COLLIDE_IGATEEXT
   collisionHandlerPhysical_ext.h
   collisionHandlerPhysical_ext.h
   collisionHandlerQueue_ext.cxx
   collisionHandlerQueue_ext.cxx
   collisionHandlerQueue_ext.h
   collisionHandlerQueue_ext.h
+  collisionNode_ext.cxx
+  collisionNode_ext.h
   collisionPolygon_ext.cxx
   collisionPolygon_ext.cxx
   collisionPolygon_ext.h
   collisionPolygon_ext.h
   collisionTraverser_ext.cxx
   collisionTraverser_ext.cxx

+ 21 - 0
panda/src/collide/collisionNode.I

@@ -166,3 +166,24 @@ INLINE CollideMask CollisionNode::
 get_default_collide_mask() {
 get_default_collide_mask() {
   return default_collision_node_collide_mask;
   return default_collision_node_collide_mask;
 }
 }
+
+/**
+ * Returns the custom pointer set via set_owner().
+ */
+INLINE void *CollisionNode::
+get_owner() const {
+  return _owner;
+}
+
+/**
+ * Sets a custom pointer, together with an optional callback that will be
+ * called when the node is deleted.
+ *
+ * The owner or the callback will not be copied along with the CollisionNode.
+ */
+INLINE void CollisionNode::
+set_owner(void *owner, OwnerCallback *callback) {
+  clear_owner();
+  _owner = owner;
+  _owner_callback = callback;
+}

+ 22 - 2
panda/src/collide/collisionNode.cxx

@@ -40,7 +40,9 @@ CollisionNode::
 CollisionNode(const std::string &name) :
 CollisionNode(const std::string &name) :
   PandaNode(name),
   PandaNode(name),
   _from_collide_mask(get_default_collide_mask()),
   _from_collide_mask(get_default_collide_mask()),
-  _collider_sort(0)
+  _collider_sort(0),
+  _owner(nullptr),
+  _owner_callback(nullptr)
 {
 {
   set_cull_callback();
   set_cull_callback();
   set_renderable();
   set_renderable();
@@ -60,7 +62,9 @@ CollisionNode(const CollisionNode &copy) :
   PandaNode(copy),
   PandaNode(copy),
   _from_collide_mask(copy._from_collide_mask),
   _from_collide_mask(copy._from_collide_mask),
   _collider_sort(copy._collider_sort),
   _collider_sort(copy._collider_sort),
-  _solids(copy._solids)
+  _solids(copy._solids),
+  _owner(nullptr),
+  _owner_callback(nullptr)
 {
 {
 }
 }
 
 
@@ -69,6 +73,10 @@ CollisionNode(const CollisionNode &copy) :
  */
  */
 CollisionNode::
 CollisionNode::
 ~CollisionNode() {
 ~CollisionNode() {
+  if (_owner_callback != nullptr) {
+    _owner_callback(_owner);
+    _owner_callback = nullptr;
+  }
 }
 }
 
 
 /**
 /**
@@ -251,6 +259,18 @@ set_from_collide_mask(CollideMask mask) {
   _from_collide_mask = mask;
   _from_collide_mask = mask;
 }
 }
 
 
+/**
+ * Removes the owner that was previously set using set_owner().
+ */
+void CollisionNode::
+clear_owner() {
+  if (_owner_callback != nullptr) {
+    _owner_callback(_owner);
+  }
+  _owner = nullptr;
+  _owner_callback = nullptr;
+}
+
 /**
 /**
  * Called when needed to recompute the node's _internal_bound object.  Nodes
  * Called when needed to recompute the node's _internal_bound object.  Nodes
  * that contain anything of substance should redefine this to do the right
  * that contain anything of substance should redefine this to do the right

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

@@ -77,6 +77,22 @@ PUBLISHED:
   INLINE static CollideMask get_default_collide_mask();
   INLINE static CollideMask get_default_collide_mask();
   MAKE_PROPERTY(default_collide_mask, get_default_collide_mask);
   MAKE_PROPERTY(default_collide_mask, get_default_collide_mask);
 
 
+public:
+  typedef void (OwnerCallback)(void *);
+
+  INLINE void *get_owner() const;
+
+#ifndef CPPPARSER
+  INLINE void set_owner(void *owner, OwnerCallback *callback = nullptr);
+  void clear_owner();
+#endif
+
+  EXTENSION(PyObject *get_owner() const);
+  EXTENSION(void set_owner(PyObject *owner));
+
+PUBLISHED:
+  MAKE_PROPERTY(owner, get_owner, set_owner);
+
 protected:
 protected:
   virtual void compute_internal_bounds(CPT(BoundingVolume) &internal_bounds,
   virtual void compute_internal_bounds(CPT(BoundingVolume) &internal_bounds,
                                        int &internal_vertices,
                                        int &internal_vertices,
@@ -94,6 +110,9 @@ private:
   typedef pvector< COWPT(CollisionSolid) > Solids;
   typedef pvector< COWPT(CollisionSolid) > Solids;
   Solids _solids;
   Solids _solids;
 
 
+  void *_owner = nullptr;
+  OwnerCallback *_owner_callback = nullptr;
+
   friend class CollisionTraverser;
   friend class CollisionTraverser;
 
 
 public:
 public:

+ 62 - 0
panda/src/collide/collisionNode_ext.cxx

@@ -0,0 +1,62 @@
+/**
+ * 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 collisionNode_ext.cxx
+ * @author rdb
+ * @date 2024-12-12
+ */
+
+#include "collisionNode_ext.h"
+
+#ifdef HAVE_PYTHON
+
+#include "collisionNode.h"
+
+/**
+ * Returns the object previously set via set_owner().  If the object has been
+ * destroyed, returns None.
+ */
+PyObject *Extension<CollisionNode>::
+get_owner() const {
+  PyObject *owner = (PyObject *)_this->get_owner();
+
+#if PY_VERSION_HEX >= 0x030D0000 // 3.13
+  PyObject *strong_ref;
+  int result = 0;
+  if (owner != nullptr) {
+    result = PyWeakref_GetRef(owner, &strong_ref);
+  }
+  if (result > 0) {
+    return strong_ref;
+  }
+  else if (result == 0) {
+    return Py_NewRef(Py_None);
+  }
+  else {
+    return nullptr;
+  }
+#else
+  return Py_NewRef(owner != nullptr ? PyWeakref_GetObject(owner) : Py_None);
+#endif
+}
+
+/**
+ * Stores a weak reference to the given object on the CollisionNode, for later
+ * use in collision events and handlers.
+ */
+void Extension<CollisionNode>::
+set_owner(PyObject *owner) {
+  if (owner != Py_None) {
+    PyObject *ref = PyWeakref_NewRef(owner, nullptr);
+    _this->set_owner(ref, [](void *obj) { Py_DECREF((PyObject *)obj); });
+  } else {
+    _this->clear_owner();
+  }
+}
+
+#endif

+ 40 - 0
panda/src/collide/collisionNode_ext.h

@@ -0,0 +1,40 @@
+/**
+ * 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 collisionNode_ext.h
+ * @author rdb
+ * @date 2024-12-12
+ */
+
+#ifndef COLLISIONNODE_EXT_H
+#define COLLISIONNODE_EXT_H
+
+#include "pandabase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "collisionNode.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for CollisionNode, which are called
+ * instead of any C++ methods with the same prototype.
+ *
+ * @since 1.11.0
+ */
+template<>
+class Extension<CollisionNode> : public ExtensionBase<CollisionNode> {
+public:
+  PyObject *get_owner() const;
+  void set_owner(PyObject *owner);
+};
+
+#endif  // HAVE_PYTHON
+
+#endif

+ 1 - 0
panda/src/collide/p3collide_ext_composite.cxx

@@ -1,5 +1,6 @@
 #include "collisionHandlerEvent_ext.cxx"
 #include "collisionHandlerEvent_ext.cxx"
 #include "collisionHandlerPhysical_ext.cxx"
 #include "collisionHandlerPhysical_ext.cxx"
 #include "collisionHandlerQueue_ext.cxx"
 #include "collisionHandlerQueue_ext.cxx"
+#include "collisionNode_ext.cxx"
 #include "collisionPolygon_ext.cxx"
 #include "collisionPolygon_ext.cxx"
 #include "collisionTraverser_ext.cxx"
 #include "collisionTraverser_ext.cxx"

+ 1 - 1
panda/src/cull/cullBinBackToFront.cxx

@@ -40,7 +40,7 @@ add_object(CullableObject *object, Thread *current_thread) {
   // Determine the center of the bounding volume.
   // Determine the center of the bounding volume.
   CPT(BoundingVolume) volume = object->_geom->get_bounds(current_thread);
   CPT(BoundingVolume) volume = object->_geom->get_bounds(current_thread);
   if (volume->is_empty()) {
   if (volume->is_empty()) {
-    delete object;
+    // No point in culling objects with no volume.
     return;
     return;
   }
   }
 
 

+ 1 - 1
panda/src/cull/cullBinFrontToBack.cxx

@@ -40,7 +40,7 @@ add_object(CullableObject *object, Thread *current_thread) {
   // Determine the center of the bounding volume.
   // Determine the center of the bounding volume.
   CPT(BoundingVolume) volume = object->_geom->get_bounds();
   CPT(BoundingVolume) volume = object->_geom->get_bounds();
   if (volume->is_empty()) {
   if (volume->is_empty()) {
-    delete object;
+    // No point in culling objects with no volume.
     return;
     return;
   }
   }
 
 

+ 3 - 1
panda/src/display/displayRegion.cxx

@@ -487,7 +487,9 @@ get_screenshot() {
   if (gsg->get_threading_model().get_draw_stage() != current_thread->get_pipeline_stage()) {
   if (gsg->get_threading_model().get_draw_stage() != current_thread->get_pipeline_stage()) {
     // Ask the engine to do on the draw thread.
     // Ask the engine to do on the draw thread.
     GraphicsEngine *engine = window->get_engine();
     GraphicsEngine *engine = window->get_engine();
-    return engine->do_get_screenshot(this, gsg);
+    return engine->run_on_draw_thread([this] {
+      return get_screenshot();
+    });
   }
   }
 
 
   // We are on the draw thread.
   // We are on the draw thread.

+ 50 - 0
panda/src/display/graphicsEngine.I

@@ -171,3 +171,53 @@ INLINE void GraphicsEngine::
 dispatch_compute(const LVecBase3i &work_groups, const ShaderAttrib *sattr, GraphicsStateGuardian *gsg) {
 dispatch_compute(const LVecBase3i &work_groups, const ShaderAttrib *sattr, GraphicsStateGuardian *gsg) {
   dispatch_compute(work_groups, RenderState::make(sattr), gsg);
   dispatch_compute(work_groups, RenderState::make(sattr), gsg);
 }
 }
+
+#ifndef CPPPARSER
+/**
+ * Waits for the draw thread to become idle, then runs the given function on it.
+ */
+template<class Callable>
+INLINE auto GraphicsEngine::
+run_on_draw_thread(Callable &&callable) -> decltype(callable()) {
+  ReMutexHolder holder(_lock);
+  std::string draw_name = _threading_model.get_draw_name();
+  if (draw_name.empty()) {
+    return std::move(callable)();
+  } else {
+    WindowRenderer *wr = get_window_renderer(draw_name, 0);
+    RenderThread *thread = (RenderThread *)wr;
+    return thread->run_on_thread(std::move(callable));
+  }
+}
+
+/**
+ * Waits for this thread to become idle, then runs the given function on it.
+ */
+template<class Callable>
+INLINE auto GraphicsEngine::RenderThread::
+run_on_thread(Callable &&callable) ->
+  typename std::enable_if<!std::is_void<decltype(callable())>::value, decltype(callable())>::type {
+
+  using ReturnType = decltype(callable());
+  alignas(ReturnType) unsigned char storage[sizeof(ReturnType)];
+
+  run_on_thread([] (RenderThread *data) {
+    new (data->_return_data) ReturnType(std::move(*(Callable *)data->_callback_data)());
+  }, &callable, storage);
+
+  return *(ReturnType *)storage;
+}
+
+/**
+ * Waits for this thread to become idle, then runs the given function on it.
+ */
+template<class Callable>
+INLINE auto GraphicsEngine::RenderThread::
+run_on_thread(Callable &&callable) ->
+  typename std::enable_if<std::is_void<decltype(callable())>::value, decltype(callable())>::type {
+
+  run_on_thread([] (RenderThread *data) {
+    std::move(*(Callable *)data->_callback_data)();
+  }, &callable, nullptr);
+}
+#endif  // CPPPARSER

+ 68 - 142
panda/src/display/graphicsEngine.cxx

@@ -152,8 +152,9 @@ INLINE static bool operator < (const CullKey &a, const CullKey &b) {
  * any Pipeline you choose.
  * any Pipeline you choose.
  */
  */
 GraphicsEngine::
 GraphicsEngine::
-GraphicsEngine(Pipeline *pipeline) :
+GraphicsEngine(ClockObject *clock, Pipeline *pipeline) :
   _pipeline(pipeline),
   _pipeline(pipeline),
+  _clock(clock),
   _app("app"),
   _app("app"),
   _lock("GraphicsEngine::_lock"),
   _lock("GraphicsEngine::_lock"),
   _loaded_textures_lock("GraphicsEngine::_loaded_textures_lock")
   _loaded_textures_lock("GraphicsEngine::_loaded_textures_lock")
@@ -339,7 +340,7 @@ make_output(GraphicsPipe *pipe,
   nassertr(pipe != nullptr, nullptr);
   nassertr(pipe != nullptr, nullptr);
   if (gsg != nullptr) {
   if (gsg != nullptr) {
     nassertr(pipe == gsg->get_pipe(), nullptr);
     nassertr(pipe == gsg->get_pipe(), nullptr);
-    nassertr(this == gsg->get_engine(), nullptr);
+    //nassertr(this == gsg->get_engine(), nullptr);
   }
   }
 
 
   // Are we really asking for a callback window?
   // Are we really asking for a callback window?
@@ -729,11 +730,9 @@ render_frame() {
   // been rendered).
   // been rendered).
   open_windows();
   open_windows();
 
 
-  ClockObject *global_clock = ClockObject::get_global_clock();
-
   if (display_cat.is_spam()) {
   if (display_cat.is_spam()) {
     display_cat.spam()
     display_cat.spam()
-      << "render_frame() - frame " << global_clock->get_frame_count() << "\n";
+      << "render_frame() - frame " << _clock->get_frame_count() << "\n";
   }
   }
 
 
   {
   {
@@ -846,8 +845,8 @@ render_frame() {
     }
     }
 #endif  // THREADED_PIPELINE
 #endif  // THREADED_PIPELINE
 
 
-    global_clock->tick(current_thread);
-    if (global_clock->check_errors(current_thread)) {
+    _clock->tick(current_thread);
+    if (_clock->check_errors(current_thread)) {
       throw_event("clock_error");
       throw_event("clock_error");
     }
     }
 
 
@@ -1135,47 +1134,30 @@ flip_frame() {
  */
  */
 bool GraphicsEngine::
 bool GraphicsEngine::
 extract_texture_data(Texture *tex, GraphicsStateGuardian *gsg) {
 extract_texture_data(Texture *tex, GraphicsStateGuardian *gsg) {
-  ReMutexHolder holder(_lock);
-
-  string draw_name = gsg->get_threading_model().get_draw_name();
-  if (draw_name.empty()) {
-    // A single-threaded environment.  No problem.
+  return run_on_draw_thread([=] () {
     return gsg->extract_texture_data(tex);
     return gsg->extract_texture_data(tex);
+  });
+}
 
 
-  } else {
-    // A multi-threaded environment.  We have to wait until the draw thread
-    // has finished its current task.
-    WindowRenderer *wr = get_window_renderer(draw_name, 0);
-    RenderThread *thread = (RenderThread *)wr;
-    MutexHolder cv_holder(thread->_cv_mutex);
-
-    while (thread->_thread_state != TS_wait) {
-      thread->_cv_done.wait();
-    }
-
-    // Temporarily set this so that it accesses data from the current thread.
-    int pipeline_stage = Thread::get_current_pipeline_stage();
-    int draw_pipeline_stage = thread->get_pipeline_stage();
-    thread->set_pipeline_stage(pipeline_stage);
-
-    // Now that the draw thread is idle, signal it to do the extraction task.
-    thread->_gsg = gsg;
-    thread->_texture = tex;
-    thread->_thread_state = TS_do_extract;
-    thread->_cv_mutex.release();
-    thread->_cv_start.notify();
-    thread->_cv_mutex.acquire();
-
-    // Wait for it to finish the extraction.
-    while (thread->_thread_state != TS_wait) {
-      thread->_cv_done.wait();
+/**
+ * Asks the indicated GraphicsStateGuardian to retrieve the buffer memory
+ * image of the indicated ShaderBuffer and return it.
+ *
+ * This is mainly useful for debugging.  It is a very slow call because it
+ * introduces a pipeline stall both of Panda's pipeline and the graphics
+ * pipeline.
+ *
+ * The return value is empty if some kind of error occurred.
+ */
+vector_uchar GraphicsEngine::
+extract_shader_buffer_data(ShaderBuffer *buffer, GraphicsStateGuardian *gsg) {
+  return run_on_draw_thread([=] () {
+    vector_uchar data;
+    if (!gsg->extract_shader_buffer_data(buffer, data)) {
+      data.clear();
     }
     }
-
-    thread->set_pipeline_stage(draw_pipeline_stage);
-    thread->_gsg = nullptr;
-    thread->_texture = nullptr;
-    return thread->_result;
-  }
+    return data;
+  });
 }
 }
 
 
 /**
 /**
@@ -1200,50 +1182,12 @@ dispatch_compute(const LVecBase3i &work_groups, const RenderState *state, Graphi
   nassertv(shader != nullptr);
   nassertv(shader != nullptr);
   nassertv(gsg != nullptr);
   nassertv(gsg != nullptr);
 
 
-  ReMutexHolder holder(_lock);
-
-  string draw_name = gsg->get_threading_model().get_draw_name();
-  if (draw_name.empty()) {
-    // A single-threaded environment.  No problem.
+  run_on_draw_thread([=] () {
     gsg->push_group_marker(std::string("Compute ") + shader->get_filename(Shader::ST_compute).get_basename());
     gsg->push_group_marker(std::string("Compute ") + shader->get_filename(Shader::ST_compute).get_basename());
     gsg->set_state_and_transform(state, TransformState::make_identity());
     gsg->set_state_and_transform(state, TransformState::make_identity());
     gsg->dispatch_compute(work_groups[0], work_groups[1], work_groups[2]);
     gsg->dispatch_compute(work_groups[0], work_groups[1], work_groups[2]);
     gsg->pop_group_marker();
     gsg->pop_group_marker();
-
-  } else {
-    // A multi-threaded environment.  We have to wait until the draw thread
-    // has finished its current task.
-    WindowRenderer *wr = get_window_renderer(draw_name, 0);
-    RenderThread *thread = (RenderThread *)wr;
-    MutexHolder cv_holder(thread->_cv_mutex);
-
-    while (thread->_thread_state != TS_wait) {
-      thread->_cv_done.wait();
-    }
-
-    // Temporarily set this so that it accesses data from the current thread.
-    int pipeline_stage = Thread::get_current_pipeline_stage();
-    int draw_pipeline_stage = thread->get_pipeline_stage();
-    thread->set_pipeline_stage(pipeline_stage);
-
-    // Now that the draw thread is idle, signal it to do the compute task.
-    thread->_gsg = gsg;
-    thread->_state = state;
-    thread->_work_groups = work_groups;
-    thread->_thread_state = TS_do_compute;
-    thread->_cv_mutex.release();
-    thread->_cv_start.notify();
-    thread->_cv_mutex.acquire();
-
-    // Wait for it to finish the compute task.
-    while (thread->_thread_state != TS_wait) {
-      thread->_cv_done.wait();
-    }
-
-    thread->set_pipeline_stage(draw_pipeline_stage);
-    thread->_gsg = nullptr;
-    thread->_state = nullptr;
-  }
+  });
 }
 }
 
 
 /**
 /**
@@ -1279,43 +1223,6 @@ texture_uploaded(Texture *tex) {
 // Usually only called by DisplayRegion::do_cull.
 // Usually only called by DisplayRegion::do_cull.
 }
 }
 
 
-/**
- * Called by DisplayRegion::do_get_screenshot
- */
-PT(Texture) GraphicsEngine::
-do_get_screenshot(DisplayRegion *region, GraphicsStateGuardian *gsg) {
-  // A multi-threaded environment.  We have to wait until the draw thread
-  // has finished its current task.
-
-  ReMutexHolder holder(_lock);
-
-  const std::string &draw_name = gsg->get_threading_model().get_draw_name();
-  WindowRenderer *wr = get_window_renderer(draw_name, 0);
-  RenderThread *thread = (RenderThread *)wr;
-  MutexHolder cv_holder(thread->_cv_mutex);
-
-  while (thread->_thread_state != TS_wait) {
-    thread->_cv_done.wait();
-  }
-
-  // Now that the draw thread is idle, signal it to do the extraction task.
-  thread->_region = region;
-  thread->_thread_state = TS_do_screenshot;
-  thread->_cv_mutex.release();
-  thread->_cv_start.notify();
-  thread->_cv_mutex.acquire();
-
-  // Wait for it to finish the extraction.
-  while (thread->_thread_state != TS_wait) {
-    thread->_cv_done.wait();
-  }
-
-  PT(Texture) tex = std::move(thread->_texture);
-  thread->_region = nullptr;
-  thread->_texture = nullptr;
-  return tex;
-}
-
 /**
 /**
  * Fires off a cull traversal using the indicated camera.
  * Fires off a cull traversal using the indicated camera.
  */
  */
@@ -2804,26 +2711,9 @@ thread_main() {
       do_pending(_engine, current_thread);
       do_pending(_engine, current_thread);
       break;
       break;
 
 
-    case TS_do_compute:
-      nassertd(_gsg != nullptr && _state != nullptr) break;
-      {
-        const ShaderAttrib *sattr;
-        _state->get_attrib(sattr);
-        _gsg->push_group_marker(std::string("Compute ") + sattr->get_shader()->get_filename(Shader::ST_compute).get_basename());
-        _gsg->set_state_and_transform(_state, TransformState::make_identity());
-        _gsg->dispatch_compute(_work_groups[0], _work_groups[1], _work_groups[2]);
-        _gsg->pop_group_marker();
-      }
-      break;
-
-    case TS_do_extract:
-      nassertd(_gsg != nullptr && _texture != nullptr) break;
-      _result = _gsg->extract_texture_data(_texture);
-      break;
-
-    case TS_do_screenshot:
-      nassertd(_region != nullptr) break;
-      _texture = _region->get_screenshot();
+    case TS_callback:
+      nassertd(_callback != nullptr) break;
+      _callback(this);
       break;
       break;
 
 
     case TS_terminate:
     case TS_terminate:
@@ -2848,3 +2738,39 @@ thread_main() {
     }
     }
   }
   }
 }
 }
+
+/**
+ * Waits for this thread to become idle, then runs the given function on it.
+ */
+void GraphicsEngine::RenderThread::
+run_on_thread(Callback *callback, void *callback_data, void *return_data) {
+  MutexHolder cv_holder(_cv_mutex);
+
+  while (_thread_state != TS_wait) {
+    _cv_done.wait();
+  }
+
+  // Temporarily set this so that it accesses data from the current thread.
+  int pipeline_stage = Thread::get_current_pipeline_stage();
+  int thread_pipeline_stage = get_pipeline_stage();
+  set_pipeline_stage(pipeline_stage);
+
+  // Now that the draw thread is idle, signal it to run the callback.
+  _callback = callback;
+  _callback_data = callback_data;
+  _return_data = return_data;
+  _thread_state = TS_callback;
+  _cv_mutex.release();
+  _cv_start.notify();
+  _cv_mutex.acquire();
+
+  // Wait for it to finish the job.
+  while (_thread_state != TS_wait) {
+    _cv_done.wait();
+  }
+
+  set_pipeline_stage(thread_pipeline_stage);
+  _callback = nullptr;
+  _callback_data = nullptr;
+  _return_data = nullptr;
+}

+ 32 - 12
panda/src/display/graphicsEngine.h

@@ -33,6 +33,9 @@
 #include "loader.h"
 #include "loader.h"
 #include "referenceCount.h"
 #include "referenceCount.h"
 #include "renderState.h"
 #include "renderState.h"
+#include "clockObject.h"
+
+#include <type_traits>
 
 
 class Pipeline;
 class Pipeline;
 class DisplayRegion;
 class DisplayRegion;
@@ -53,7 +56,8 @@ class Texture;
  */
  */
 class EXPCL_PANDA_DISPLAY GraphicsEngine : public ReferenceCount {
 class EXPCL_PANDA_DISPLAY GraphicsEngine : public ReferenceCount {
 PUBLISHED:
 PUBLISHED:
-  explicit GraphicsEngine(Pipeline *pipeline = nullptr);
+  explicit GraphicsEngine(ClockObject *clock = ClockObject::get_global_clock(),
+                          Pipeline *pipeline = nullptr);
   BLOCKING ~GraphicsEngine();
   BLOCKING ~GraphicsEngine();
 
 
   void set_threading_model(const GraphicsThreadingModel &threading_model);
   void set_threading_model(const GraphicsThreadingModel &threading_model);
@@ -111,6 +115,7 @@ PUBLISHED:
   BLOCKING void flip_frame();
   BLOCKING void flip_frame();
 
 
   bool extract_texture_data(Texture *tex, GraphicsStateGuardian *gsg);
   bool extract_texture_data(Texture *tex, GraphicsStateGuardian *gsg);
+  vector_uchar extract_shader_buffer_data(ShaderBuffer *buffer, GraphicsStateGuardian *gsg);
   void dispatch_compute(const LVecBase3i &work_groups,
   void dispatch_compute(const LVecBase3i &work_groups,
                         const RenderState *state,
                         const RenderState *state,
                         GraphicsStateGuardian *gsg);
                         GraphicsStateGuardian *gsg);
@@ -127,15 +132,17 @@ public:
     TS_do_flip,
     TS_do_flip,
     TS_do_release,
     TS_do_release,
     TS_do_windows,
     TS_do_windows,
-    TS_do_compute,
-    TS_do_extract,
-    TS_do_screenshot,
+    TS_callback,
     TS_terminate,
     TS_terminate,
     TS_done
     TS_done
   };
   };
 
 
   void texture_uploaded(Texture *tex);
   void texture_uploaded(Texture *tex);
-  PT(Texture) do_get_screenshot(DisplayRegion *region, GraphicsStateGuardian *gsg);
+
+#ifndef CPPPARSER
+  template<class Callable>
+  INLINE auto run_on_draw_thread(Callable &&callable) -> decltype(callable());
+#endif
 
 
 public:
 public:
   static void do_cull(CullHandler *cull_handler, SceneSetup *scene_setup,
   static void do_cull(CullHandler *cull_handler, SceneSetup *scene_setup,
@@ -302,24 +309,37 @@ private:
     RenderThread(const std::string &name, GraphicsEngine *engine);
     RenderThread(const std::string &name, GraphicsEngine *engine);
     virtual void thread_main();
     virtual void thread_main();
 
 
+    typedef void Callback(RenderThread *thread);
+    void run_on_thread(Callback *callback,
+                       void *callback_data = nullptr,
+                       void *return_data = nullptr);
+
+#ifndef CPPPARSER
+    template<class Callable>
+    INLINE auto run_on_thread(Callable &&callable) ->
+      typename std::enable_if<!std::is_void<decltype(callable())>::value, decltype(callable())>::type;
+
+    template<class Callable>
+    INLINE auto run_on_thread(Callable &&callable) ->
+      typename std::enable_if<std::is_void<decltype(callable())>::value, decltype(callable())>::type;
+#endif
+
     GraphicsEngine *_engine;
     GraphicsEngine *_engine;
     Mutex _cv_mutex;
     Mutex _cv_mutex;
     ConditionVar _cv_start;
     ConditionVar _cv_start;
     ConditionVar _cv_done;
     ConditionVar _cv_done;
     ThreadState _thread_state;
     ThreadState _thread_state;
 
 
-    // These are stored for extract_texture_data and dispatch_compute.
-    GraphicsStateGuardian *_gsg;
-    PT(Texture) _texture;
-    const RenderState *_state;
-    DisplayRegion *_region;
-    LVecBase3i _work_groups;
-    bool _result;
+    // Used for TS_callback.
+    Callback *_callback;
+    void *_callback_data;
+    void *_return_data;
   };
   };
 
 
   WindowRenderer *get_window_renderer(const std::string &name, int pipeline_stage);
   WindowRenderer *get_window_renderer(const std::string &name, int pipeline_stage);
 
 
   Pipeline *_pipeline;
   Pipeline *_pipeline;
+  ClockObject *const _clock;
   Windows _windows;
   Windows _windows;
   bool _windows_sorted;
   bool _windows_sorted;
 
 

+ 41 - 1
panda/src/display/graphicsStateGuardian.I

@@ -596,7 +596,8 @@ get_supports_tessellation_shaders() const {
  */
  */
 INLINE bool GraphicsStateGuardian::
 INLINE bool GraphicsStateGuardian::
 get_supports_compute_shaders() const {
 get_supports_compute_shaders() const {
-  return (_supported_shader_caps & ShaderModule::C_compute_shader) != 0;
+  return (_supported_shader_caps & ShaderModule::C_compute_shader) != 0
+      && _max_compute_work_group_invocations > 0;
 }
 }
 
 
 /**
 /**
@@ -711,6 +712,45 @@ get_supports_dual_source_blending() const {
   return _supports_dual_source_blending;
   return _supports_dual_source_blending;
 }
 }
 
 
+/**
+ * Returns the maximum number of work groups that can be submitted in a single
+ * compute dispatch.
+ *
+ * If compute shaders are supported, this will be at least 65535x65535x65535.
+ * Otherwise, it will be zero.
+ */
+INLINE LVecBase3i GraphicsStateGuardian::
+get_max_compute_work_group_count() const {
+  return _max_compute_work_group_count;
+}
+
+/**
+ * Returns the maximum number of invocations in each work group split out
+ * separately to every x, y, z dimension.  This limit applies in addition to
+ * the overall number of invocations, which is specified by
+ * get_max_compute_work_group_invocations().
+ *
+ * If compute shaders are supported, this will be at least 128x128x64.
+ * Otherwise, it will be zero.
+ */
+INLINE LVecBase3i GraphicsStateGuardian::
+get_max_compute_work_group_size() const {
+  return _max_compute_work_group_size;
+}
+
+/**
+ * Returns the maximum number of invocations in each work group as a product
+ * of the x, y, z dimensions.  This limit applies in addition to the
+ * per-dimension limits specified by get_max_compute_work_group_size().
+ *
+ * If compute shaders are supported, this will be at least 128.  Otherwise, it
+ * will be zero.
+ */
+INLINE int GraphicsStateGuardian::
+get_max_compute_work_group_invocations() const {
+  return _max_compute_work_group_invocations;
+}
+
 /**
 /**
  * Deprecated.  Use get_max_color_targets() instead, which returns the exact
  * Deprecated.  Use get_max_color_targets() instead, which returns the exact
  * same value.
  * same value.

+ 36 - 0
panda/src/display/graphicsStateGuardian.cxx

@@ -100,6 +100,7 @@ PStatCollector GraphicsStateGuardian::_draw_primitive_pcollector("Draw:Primitive
 PStatCollector GraphicsStateGuardian::_draw_set_state_pcollector("Draw:Set State");
 PStatCollector GraphicsStateGuardian::_draw_set_state_pcollector("Draw:Set State");
 PStatCollector GraphicsStateGuardian::_flush_pcollector("Draw:Flush");
 PStatCollector GraphicsStateGuardian::_flush_pcollector("Draw:Flush");
 PStatCollector GraphicsStateGuardian::_compute_dispatch_pcollector("Draw:Compute dispatch");
 PStatCollector GraphicsStateGuardian::_compute_dispatch_pcollector("Draw:Compute dispatch");
+PStatCollector GraphicsStateGuardian::_compute_work_groups_pcollector("Compute work groups");
 
 
 PStatCollector GraphicsStateGuardian::_wait_occlusion_pcollector("Wait:Occlusion");
 PStatCollector GraphicsStateGuardian::_wait_occlusion_pcollector("Wait:Occlusion");
 PStatCollector GraphicsStateGuardian::_wait_timer_pcollector("Wait:Timer Queries");
 PStatCollector GraphicsStateGuardian::_wait_timer_pcollector("Wait:Timer Queries");
@@ -244,6 +245,10 @@ GraphicsStateGuardian(CoordinateSystem internal_coordinate_system,
   _supports_hlsl = false;
   _supports_hlsl = false;
   _supports_spir_v = false;
   _supports_spir_v = false;
 
 
+  _max_compute_work_group_count = LVecBase3i(0, 0, 0);
+  _max_compute_work_group_size = LVecBase3i(0, 0, 0);
+  _max_compute_work_group_invocations = 0;
+
   _supports_stencil = false;
   _supports_stencil = false;
   _supports_stencil_wrap = false;
   _supports_stencil_wrap = false;
   _supports_two_sided_stencil = false;
   _supports_two_sided_stencil = false;
@@ -568,6 +573,23 @@ update_texture(TextureContext *, bool) {
   return true;
   return true;
 }
 }
 
 
+/**
+ * Ensures that the current Texture data is refreshed onto the GSG.  This
+ * means updating the texture properties and/or re-uploading the texture
+ * image, if necessary.  This should only be called within the draw thread.
+ *
+ * If force is true, this function will not return until the texture has been
+ * fully uploaded.  If force is false, the function may choose to upload a
+ * simple version of the texture instead, if the texture is not fully resident
+ * (and if get_incomplete_render() is true).
+ */
+bool GraphicsStateGuardian::
+update_texture(TextureContext *tc, bool force, CompletionToken token) {
+  bool result = update_texture(tc, force);
+  token.complete(result);
+  return result;
+}
+
 /**
 /**
  * Frees the resources previously allocated via a call to prepare_texture(),
  * Frees the resources previously allocated via a call to prepare_texture(),
  * including deleting the TextureContext itself, if it is non-NULL.
  * including deleting the TextureContext itself, if it is non-NULL.
@@ -741,6 +763,18 @@ release_shader_buffers(const pvector<BufferContext *> &contexts) {
   }
   }
 }
 }
 
 
+/**
+ * This method should only be called by the GraphicsEngine.  Do not call it
+ * directly; call GraphicsEngine::extract_texture_data() instead.
+ *
+ * This method will be called in the draw thread to download the buffer's
+ * current contents synchronously.
+ */
+bool GraphicsStateGuardian::
+extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data) {
+  return false;
+}
+
 /**
 /**
  * Begins a new occlusion query.  After this call, you may call
  * Begins a new occlusion query.  After this call, you may call
  * begin_draw_primitives() and draw_triangles()/draw_whatever() repeatedly.
  * begin_draw_primitives() and draw_triangles()/draw_whatever() repeatedly.
@@ -1366,6 +1400,7 @@ end_frame(Thread *current_thread) {
   _vertices_tri_pcollector.flush_level();
   _vertices_tri_pcollector.flush_level();
   _vertices_patch_pcollector.flush_level();
   _vertices_patch_pcollector.flush_level();
   _vertices_other_pcollector.flush_level();
   _vertices_other_pcollector.flush_level();
+  _compute_work_groups_pcollector.flush_level();
 
 
   _state_pcollector.flush_level();
   _state_pcollector.flush_level();
   _texture_state_pcollector.flush_level();
   _texture_state_pcollector.flush_level();
@@ -2016,6 +2051,7 @@ init_frame_pstats() {
     _vertices_tri_pcollector.clear_level();
     _vertices_tri_pcollector.clear_level();
     _vertices_patch_pcollector.clear_level();
     _vertices_patch_pcollector.clear_level();
     _vertices_other_pcollector.clear_level();
     _vertices_other_pcollector.clear_level();
+    _compute_work_groups_pcollector.clear_level();
 
 
     _state_pcollector.clear_level();
     _state_pcollector.clear_level();
     _transform_state_pcollector.clear_level();
     _transform_state_pcollector.clear_level();

+ 17 - 0
panda/src/display/graphicsStateGuardian.h

@@ -114,6 +114,7 @@ PUBLISHED:
   GraphicsEngine *get_engine() const;
   GraphicsEngine *get_engine() const;
   INLINE const GraphicsThreadingModel &get_threading_model() const;
   INLINE const GraphicsThreadingModel &get_threading_model() const;
   MAKE_PROPERTY(pipe, get_pipe);
   MAKE_PROPERTY(pipe, get_pipe);
+  MAKE_PROPERTY(engine, get_engine);
 
 
   virtual bool make_current() const;
   virtual bool make_current() const;
 
 
@@ -177,6 +178,12 @@ PUBLISHED:
   INLINE int get_maximum_simultaneous_render_targets() const;
   INLINE int get_maximum_simultaneous_render_targets() const;
   INLINE bool get_supports_dual_source_blending() const;
   INLINE bool get_supports_dual_source_blending() const;
 
 
+public:
+  INLINE LVecBase3i get_max_compute_work_group_count() const;
+  INLINE LVecBase3i get_max_compute_work_group_size() const;
+  INLINE int get_max_compute_work_group_invocations() const;
+
+PUBLISHED:
   MAKE_PROPERTY(max_vertices_per_array, get_max_vertices_per_array);
   MAKE_PROPERTY(max_vertices_per_array, get_max_vertices_per_array);
   MAKE_PROPERTY(max_vertices_per_primitive, get_max_vertices_per_primitive);
   MAKE_PROPERTY(max_vertices_per_primitive, get_max_vertices_per_primitive);
   MAKE_PROPERTY(max_texture_stages, get_max_texture_stages);
   MAKE_PROPERTY(max_texture_stages, get_max_texture_stages);
@@ -223,6 +230,9 @@ PUBLISHED:
   MAKE_PROPERTY(timer_queries_active, get_timer_queries_active);
   MAKE_PROPERTY(timer_queries_active, get_timer_queries_active);
   MAKE_PROPERTY(max_color_targets, get_max_color_targets);
   MAKE_PROPERTY(max_color_targets, get_max_color_targets);
   MAKE_PROPERTY(supports_dual_source_blending, get_supports_dual_source_blending);
   MAKE_PROPERTY(supports_dual_source_blending, get_supports_dual_source_blending);
+  MAKE_PROPERTY(max_compute_work_group_count, get_max_compute_work_group_count);
+  MAKE_PROPERTY(max_compute_work_group_size, get_max_compute_work_group_size);
+  MAKE_PROPERTY(max_compute_work_group_invocations, get_max_compute_work_group_invocations);
 
 
   INLINE ShaderModel get_shader_model() const;
   INLINE ShaderModel get_shader_model() const;
   INLINE void set_shader_model(ShaderModel shader_model);
   INLINE void set_shader_model(ShaderModel shader_model);
@@ -294,6 +304,7 @@ PUBLISHED:
 public:
 public:
   virtual TextureContext *prepare_texture(Texture *tex);
   virtual TextureContext *prepare_texture(Texture *tex);
   virtual bool update_texture(TextureContext *tc, bool force);
   virtual bool update_texture(TextureContext *tc, bool force);
+  virtual bool update_texture(TextureContext *tc, bool force, CompletionToken token);
   virtual void release_texture(TextureContext *tc);
   virtual void release_texture(TextureContext *tc);
   virtual void release_textures(const pvector<TextureContext *> &contexts);
   virtual void release_textures(const pvector<TextureContext *> &contexts);
   virtual bool extract_texture_data(Texture *tex);
   virtual bool extract_texture_data(Texture *tex);
@@ -318,6 +329,7 @@ public:
   virtual BufferContext *prepare_shader_buffer(ShaderBuffer *data);
   virtual BufferContext *prepare_shader_buffer(ShaderBuffer *data);
   virtual void release_shader_buffer(BufferContext *ibc);
   virtual void release_shader_buffer(BufferContext *ibc);
   virtual void release_shader_buffers(const pvector<BufferContext *> &contexts);
   virtual void release_shader_buffers(const pvector<BufferContext *> &contexts);
+  virtual bool extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data);
 
 
   virtual void begin_occlusion_query();
   virtual void begin_occlusion_query();
   virtual PT(OcclusionQueryContext) end_occlusion_query();
   virtual PT(OcclusionQueryContext) end_occlusion_query();
@@ -617,6 +629,10 @@ protected:
   bool _supports_framebuffer_multisample;
   bool _supports_framebuffer_multisample;
   bool _supports_framebuffer_blit;
   bool _supports_framebuffer_blit;
 
 
+  LVecBase3i _max_compute_work_group_count;
+  LVecBase3i _max_compute_work_group_size;
+  int _max_compute_work_group_invocations;
+
   bool _supports_stencil;
   bool _supports_stencil;
   bool _supports_stencil_wrap;
   bool _supports_stencil_wrap;
   bool _supports_two_sided_stencil;
   bool _supports_two_sided_stencil;
@@ -684,6 +700,7 @@ public:
   static PStatCollector _draw_set_state_pcollector;
   static PStatCollector _draw_set_state_pcollector;
   static PStatCollector _flush_pcollector;
   static PStatCollector _flush_pcollector;
   static PStatCollector _compute_dispatch_pcollector;
   static PStatCollector _compute_dispatch_pcollector;
+  static PStatCollector _compute_work_groups_pcollector;
   static PStatCollector _wait_occlusion_pcollector;
   static PStatCollector _wait_occlusion_pcollector;
   static PStatCollector _wait_timer_pcollector;
   static PStatCollector _wait_timer_pcollector;
   static PStatCollector _timer_queries_pcollector;
   static PStatCollector _timer_queries_pcollector;

+ 2 - 1
panda/src/egldisplay/eglGraphicsBuffer.cxx

@@ -190,7 +190,8 @@ open_buffer() {
     // If the old gsg has the wrong pixel format, create a new one that shares
     // If the old gsg has the wrong pixel format, create a new one that shares
     // with the old gsg.
     // with the old gsg.
     DCAST_INTO_R(eglgsg, _gsg, false);
     DCAST_INTO_R(eglgsg, _gsg, false);
-    if (!eglgsg->get_fb_properties().subsumes(_fb_properties)) {
+    if (eglgsg->get_engine() != _engine ||
+        !eglgsg->get_fb_properties().subsumes(_fb_properties)) {
       eglgsg = new eglGraphicsStateGuardian(_engine, _pipe, eglgsg);
       eglgsg = new eglGraphicsStateGuardian(_engine, _pipe, eglgsg);
       eglgsg->choose_pixel_format(_fb_properties, egl_pipe, false, true, false);
       eglgsg->choose_pixel_format(_fb_properties, egl_pipe, false, true, false);
       _gsg = eglgsg;
       _gsg = eglgsg;

+ 3 - 0
panda/src/egldisplay/eglGraphicsPipe.cxx

@@ -288,6 +288,9 @@ make_output(const std::string &name,
         ((flags&BF_require_window)!=0)) {
         ((flags&BF_require_window)!=0)) {
       return nullptr;
       return nullptr;
     }
     }
+    if (host->get_engine() != engine) {
+      return nullptr;
+    }
     // Early failure - if we are sure that this buffer WONT meet specs, we can
     // Early failure - if we are sure that this buffer WONT meet specs, we can
     // bail out early.
     // bail out early.
     if ((flags & BF_fb_props_optional) == 0) {
     if ((flags & BF_fb_props_optional) == 0) {

+ 2 - 1
panda/src/egldisplay/eglGraphicsWindow.cxx

@@ -223,7 +223,8 @@ open_window() {
     // If the old gsg has the wrong pixel format, create a new one that shares
     // If the old gsg has the wrong pixel format, create a new one that shares
     // with the old gsg.
     // with the old gsg.
     DCAST_INTO_R(eglgsg, _gsg, false);
     DCAST_INTO_R(eglgsg, _gsg, false);
-    if (!eglgsg->get_fb_properties().subsumes(_fb_properties)) {
+    if (eglgsg->get_engine() != _engine ||
+        !eglgsg->get_fb_properties().subsumes(_fb_properties)) {
       eglgsg = new eglGraphicsStateGuardian(_engine, _pipe, eglgsg);
       eglgsg = new eglGraphicsStateGuardian(_engine, _pipe, eglgsg);
       eglgsg->choose_pixel_format(_fb_properties, egl_pipe, true, false, false);
       eglgsg->choose_pixel_format(_fb_properties, egl_pipe, true, false, false);
       _gsg = eglgsg;
       _gsg = eglgsg;

+ 11 - 0
panda/src/event/asyncFuture.cxx

@@ -389,6 +389,17 @@ wake_task(AsyncTask *task) {
   }
   }
 }
 }
 
 
+/**
+ * Internal callback called when a CompletionToken created from this future
+ * completes.
+ */
+void AsyncFuture::
+token_callback(Completable::Data *data, bool success) {
+  AsyncFuture *future = (AsyncFuture *)data;
+  future->set_result(EventParameter(success));
+  unref_delete(future);
+}
+
 /**
 /**
  * @see AsyncFuture::gather
  * @see AsyncFuture::gather
  */
  */

+ 32 - 1
panda/src/event/asyncFuture.h

@@ -20,6 +20,7 @@
 #include "eventParameter.h"
 #include "eventParameter.h"
 #include "patomic.h"
 #include "patomic.h"
 #include "small_vector.h"
 #include "small_vector.h"
+#include "completionToken.h"
 
 
 class AsyncTaskManager;
 class AsyncTaskManager;
 class AsyncTask;
 class AsyncTask;
@@ -58,7 +59,7 @@ class AsyncTask;
  *
  *
  * @since 1.10.0
  * @since 1.10.0
  */
  */
-class EXPCL_PANDA_EVENT AsyncFuture : public TypedReferenceCount {
+class EXPCL_PANDA_EVENT AsyncFuture : public TypedReferenceCount, protected Completable::Data {
 PUBLISHED:
 PUBLISHED:
   INLINE AsyncFuture();
   INLINE AsyncFuture();
   virtual ~AsyncFuture();
   virtual ~AsyncFuture();
@@ -109,6 +110,8 @@ public:
 private:
 private:
   void wake_task(AsyncTask *task);
   void wake_task(AsyncTask *task);
 
 
+  static void token_callback(Completable::Data *, bool success);
+
 protected:
 protected:
   enum FutureState : patomic_unsigned_lock_free::value_type {
   enum FutureState : patomic_unsigned_lock_free::value_type {
     // Pending states
     // Pending states
@@ -136,6 +139,7 @@ protected:
 
 
   friend class AsyncGatheringFuture;
   friend class AsyncGatheringFuture;
   friend class AsyncTaskChain;
   friend class AsyncTaskChain;
+  friend class CompletionToken;
   friend class PythonTask;
   friend class PythonTask;
 
 
 public:
 public:
@@ -199,6 +203,33 @@ private:
   static TypeHandle _type_handle;
   static TypeHandle _type_handle;
 };
 };
 
 
+#ifndef CPPPARSER
+// Allow passing a future into a method accepting a CompletionToken.
+template<>
+INLINE CompletionToken::
+CompletionToken(AsyncFuture *future) {
+  if (future != nullptr) {
+    future->ref();
+    _callback._data = future;
+    if (_callback._data->_function == nullptr) {
+      _callback._data->_function = &AsyncFuture::token_callback;
+    }
+  }
+}
+
+template<>
+INLINE CompletionToken::
+CompletionToken(PT(AsyncFuture) future) {
+  if (future != nullptr) {
+    _callback._data = future;
+    if (_callback._data->_function == nullptr) {
+      _callback._data->_function = &AsyncFuture::token_callback;
+    }
+    future.cheat() = nullptr;
+  }
+}
+#endif
+
 #include "asyncFuture.I"
 #include "asyncFuture.I"
 
 
 #endif // !ASYNCFUTURE_H
 #endif // !ASYNCFUTURE_H

+ 3 - 0
panda/src/event/asyncTaskChain.cxx

@@ -1438,6 +1438,9 @@ AsyncTaskChainThread(const string &name, AsyncTaskChain *chain) :
 void AsyncTaskChain::AsyncTaskChainThread::
 void AsyncTaskChain::AsyncTaskChainThread::
 thread_main() {
 thread_main() {
 #ifdef HAVE_THREADS
 #ifdef HAVE_THREADS
+  // Let PStats know this thread exists.
+  PStatClient::thread_tick();
+
   MutexHolder holder(_chain->_manager->_lock);
   MutexHolder holder(_chain->_manager->_lock);
   while (_chain->_state != S_shutdown && _chain->_state != S_interrupted) {
   while (_chain->_state != S_shutdown && _chain->_state != S_interrupted) {
     thread_consider_yield();
     thread_consider_yield();

+ 17 - 18
panda/src/express/trueClock.cxx

@@ -521,6 +521,7 @@ TrueClock() {
 #include <stdio.h>  // for perror
 #include <stdio.h>  // for perror
 
 
 static long _init_sec;
 static long _init_sec;
+static time_t _init_sec_monotonic = 0;
 
 
 /**
 /**
  *
  *
@@ -553,25 +554,13 @@ get_long_time() {
  */
  */
 double TrueClock::
 double TrueClock::
 get_short_raw_time() {
 get_short_raw_time() {
-  struct timeval tv;
-
-  int result;
-
-#ifdef GETTIMEOFDAY_ONE_PARAM
-  result = gettimeofday(&tv);
-#else
-  result = gettimeofday(&tv, nullptr);
-#endif
-
-  if (result < 0) {
-    // Error in gettimeofday().
-    return 0.0;
+#if defined(CLOCK_MONOTONIC) && !defined(__APPLE__)
+  struct timespec spec;
+  if (clock_gettime(CLOCK_MONOTONIC, &spec) == 0) {
+    return (double)(spec.tv_sec - _init_sec_monotonic) + (double)spec.tv_nsec / 1000000000.0;
   }
   }
-
-  // We subtract out the time at which the clock was initialized, because we
-  // don't care about the number of seconds all the way back to 1970, and we
-  // want to leave the double with as much precision as it can get.
-  return (double)(tv.tv_sec - _init_sec) + (double)tv.tv_usec / 1000000.0;
+#endif
+  return get_long_time();
 }
 }
 
 
 /**
 /**
@@ -603,6 +592,16 @@ TrueClock() {
   } else {
   } else {
     _init_sec = tv.tv_sec;
     _init_sec = tv.tv_sec;
   }
   }
+
+#if defined(CLOCK_MONOTONIC) && !defined(__APPLE__)
+  struct timespec spec;
+  if (clock_gettime(CLOCK_MONOTONIC, &spec) == 0) {
+    _init_sec_monotonic = spec.tv_sec;
+  } else {
+    perror("clock_gettime(CLOCK_MONOTONIC)");
+    _init_sec_monotonic = 0;
+  }
+#endif
 }
 }
 
 
 #endif
 #endif

+ 2 - 0
panda/src/framework/pandaFramework.cxx

@@ -204,6 +204,8 @@ close_framework() {
 
 
   _event_handler.remove_all_hooks();
   _event_handler.remove_all_hooks();
 
 
+  _task_mgr.cleanup();
+
   _is_open = false;
   _is_open = false;
   _made_default_pipe = false;
   _made_default_pipe = false;
   _default_pipe.clear();
   _default_pipe.clear();

+ 9 - 0
panda/src/gles2gsg/gles2gsg.h

@@ -147,6 +147,11 @@ typedef char GLchar;
 #define GL_FRAMEBUFFER_BARRIER_BIT 0x400
 #define GL_FRAMEBUFFER_BARRIER_BIT 0x400
 #define GL_TRANSFORM_FEEDBACK_BARRIER_BIT 0x800
 #define GL_TRANSFORM_FEEDBACK_BARRIER_BIT 0x800
 #define GL_ATOMIC_COUNTER_BARRIER_BIT 0x1000
 #define GL_ATOMIC_COUNTER_BARRIER_BIT 0x1000
+#define GL_SHADER_STORAGE_BARRIER_BIT 0x2000
+#define GL_MAP_INVALIDATE_RANGE_BIT 0x0004
+#define GL_MAP_INVALIDATE_BUFFER_BIT 0x0008
+#define GL_MAP_FLUSH_EXPLICIT_BIT 0x0010
+#define GL_MAP_UNSYNCHRONIZED_BIT 0x0020
 #define GL_HALF_FLOAT 0x140B
 #define GL_HALF_FLOAT 0x140B
 #define GL_COLOR 0x1800
 #define GL_COLOR 0x1800
 #define GL_DEPTH 0x1801
 #define GL_DEPTH 0x1801
@@ -181,6 +186,7 @@ typedef char GLchar;
 #define GL_WRITE_ONLY 0x88B9
 #define GL_WRITE_ONLY 0x88B9
 #define GL_READ_WRITE 0x88BA
 #define GL_READ_WRITE 0x88BA
 #define GL_PIXEL_PACK_BUFFER 0x88EB
 #define GL_PIXEL_PACK_BUFFER 0x88EB
+#define GL_PIXEL_UNPACK_BUFFER 0x88EC
 #define GL_MAX_ARRAY_TEXTURE_LAYERS 0x88FF
 #define GL_MAX_ARRAY_TEXTURE_LAYERS 0x88FF
 #define GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH 0x8A35
 #define GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH 0x8A35
 #define GL_ACTIVE_UNIFORM_BLOCKS 0x8A36
 #define GL_ACTIVE_UNIFORM_BLOCKS 0x8A36
@@ -276,6 +282,7 @@ typedef char GLchar;
 #define GL_SHADER_STORAGE_BUFFER_BINDING 0x90D3
 #define GL_SHADER_STORAGE_BUFFER_BINDING 0x90D3
 #define GL_SHADER_STORAGE_BUFFER_START 0x90D4
 #define GL_SHADER_STORAGE_BUFFER_START 0x90D4
 #define GL_SHADER_STORAGE_BUFFER_SIZE 0x90D5
 #define GL_SHADER_STORAGE_BUFFER_SIZE 0x90D5
+#define GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS 0x90EB
 #define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
 #define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
 #define GL_UNSIGNALED 0x9118
 #define GL_UNSIGNALED 0x9118
 #define GL_SIGNALED 0x9119
 #define GL_SIGNALED 0x9119
@@ -283,6 +290,8 @@ typedef char GLchar;
 #define GL_TIMEOUT_EXPIRED 0x911B
 #define GL_TIMEOUT_EXPIRED 0x911B
 #define GL_CONDITION_SATISFIED 0x911C
 #define GL_CONDITION_SATISFIED 0x911C
 #define GL_COMPUTE_SHADER 0x91B9
 #define GL_COMPUTE_SHADER 0x91B9
+#define GL_MAX_COMPUTE_WORK_GROUP_COUNT 0x91BE
+#define GL_MAX_COMPUTE_WORK_GROUP_SIZE 0x91BF
 #define GL_FRAMEBUFFER_DEFAULT_WIDTH 0x9310
 #define GL_FRAMEBUFFER_DEFAULT_WIDTH 0x9310
 #define GL_FRAMEBUFFER_DEFAULT_HEIGHT 0x9311
 #define GL_FRAMEBUFFER_DEFAULT_HEIGHT 0x9311
 #define GL_FRAMEBUFFER_DEFAULT_SAMPLES 0x9313
 #define GL_FRAMEBUFFER_DEFAULT_SAMPLES 0x9313

+ 4 - 0
panda/src/glstuff/glBufferContext_src.h

@@ -32,6 +32,10 @@ public:
   // This is the GL "name" of the data object.
   // This is the GL "name" of the data object.
   GLuint _index;
   GLuint _index;
 
 
+  // This is set to glgsg->_shader_storage_barrier_counter if a write was
+  // performed, in which case a barrier is issued before the next use.
+  int _shader_storage_barrier_counter = -1;
+
 public:
 public:
   static TypeHandle get_class_type() {
   static TypeHandle get_class_type() {
     return _type_handle;
     return _type_handle;

+ 181 - 22
panda/src/glstuff/glGraphicsBuffer_src.cxx

@@ -281,7 +281,7 @@ begin_frame(FrameMode mode, Thread *current_thread) {
       CLP(GraphicsStateGuardian) *glgsg = (CLP(GraphicsStateGuardian) *)_gsg.p();
       CLP(GraphicsStateGuardian) *glgsg = (CLP(GraphicsStateGuardian) *)_gsg.p();
 
 
       for (CLP(TextureContext) *gtc : _texture_contexts) {
       for (CLP(TextureContext) *gtc : _texture_contexts) {
-        if (gtc->needs_barrier(GL_FRAMEBUFFER_BARRIER_BIT)) {
+        if (gtc->needs_barrier(GL_FRAMEBUFFER_BARRIER_BIT, true)) {
           glgsg->issue_memory_barrier(GL_FRAMEBUFFER_BARRIER_BIT);
           glgsg->issue_memory_barrier(GL_FRAMEBUFFER_BARRIER_BIT);
           // If we've done it for one, we've done it for all.
           // If we've done it for one, we've done it for all.
           break;
           break;
@@ -419,6 +419,7 @@ rebuild_bitplanes() {
   Texture *attach[RTP_COUNT];
   Texture *attach[RTP_COUNT];
   memset(attach, 0, sizeof(Texture *) * RTP_COUNT);
   memset(attach, 0, sizeof(Texture *) * RTP_COUNT);
   _texture_contexts.clear();
   _texture_contexts.clear();
+  _textures.clear();
 
 
   // Sort the textures list into appropriate slots.
   // Sort the textures list into appropriate slots.
   {
   {
@@ -458,15 +459,17 @@ rebuild_bitplanes() {
       }
       }
 
 
       // If we can't bind this type of texture, punt it.
       // If we can't bind this type of texture, punt it.
-      if ((tex->get_texture_type() != Texture::TT_2d_texture) &&
-          (tex->get_texture_type() != Texture::TT_3d_texture) &&
-          (tex->get_texture_type() != Texture::TT_2d_texture_array) &&
-          (tex->get_texture_type() != Texture::TT_cube_map)) {
+      Texture::TextureType texture_type = tex->get_texture_type();
+      if (texture_type != Texture::TT_2d_texture &&
+          texture_type != Texture::TT_3d_texture &&
+          texture_type != Texture::TT_2d_texture_array &&
+          texture_type != Texture::TT_cube_map &&
+          texture_type != Texture::TT_cube_map_array) {
         ((CData *)cdata.p())->_textures[i]._rtm_mode = RTM_copy_texture;
         ((CData *)cdata.p())->_textures[i]._rtm_mode = RTM_copy_texture;
         continue;
         continue;
       }
       }
 
 
-      if (_rb_size_z > 1 && tex->get_texture_type() == Texture::TT_2d_texture) {
+      if (_rb_size_z > 1 && texture_type == Texture::TT_2d_texture) {
         // We can't bind a 2D texture to a layered FBO.  If the user happened
         // We can't bind a 2D texture to a layered FBO.  If the user happened
         // to request RTM_bind_layered for a 2D texture, that's just silly,
         // to request RTM_bind_layered for a 2D texture, that's just silly,
         // and we can't render to anything but the first layer anyway.
         // and we can't render to anything but the first layer anyway.
@@ -520,6 +523,18 @@ rebuild_bitplanes() {
       // but it's a waste.  Let's not do it unless the user requested stencil.
       // but it's a waste.  Let's not do it unless the user requested stencil.
       _use_depth_stencil = false;
       _use_depth_stencil = false;
 
 
+#ifdef __APPLE__
+    } else if (_fb_properties.get_depth_bits() > 0 && _requested_multisamples) {
+      // Apple's OpenGL driver doesn't like blitting depth-stencil targets.
+      // See GitHub issue #1719
+      _use_depth_stencil = false;
+      if (_fb_properties.get_depth_bits() < 24) {
+        // Make sure we do get at least as many depth bits as we would have
+        // gotten if we did get a depth-stencil buffer.
+        _fb_properties.set_depth_bits(24);
+      }
+#endif
+
     } else if (_fb_properties.get_depth_bits() > 0) {
     } else if (_fb_properties.get_depth_bits() > 0) {
       // Let's use a depth stencil buffer by default, if a depth buffer was
       // Let's use a depth stencil buffer by default, if a depth buffer was
       // requested.
       // requested.
@@ -625,10 +640,23 @@ rebuild_bitplanes() {
 
 
     if (_have_any_color || have_any_depth) {
     if (_have_any_color || have_any_depth) {
       // Clear if the fbo was just created, regardless of the clear settings per
       // Clear if the fbo was just created, regardless of the clear settings per
-      // frame.
+      // frame.  However, we don't do this for textures, which may have useful
+      // contents that need to be preserved.
       if (_initial_clear) {
       if (_initial_clear) {
-        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
-        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+        GLbitfield mask = 0;
+        if (_rb[RTP_color]) {
+          glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+          mask |= GL_COLOR_BUFFER_BIT;
+        }
+        if (_rb[RTP_depth]) {
+          mask |= GL_DEPTH_BUFFER_BIT;
+        }
+        if (_rb[RTP_depth_stencil]) {
+          mask |= GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
+        }
+        if (mask != 0) {
+          glClear(mask);
+        }
       }
       }
 #ifndef OPENGLES_1
 #ifndef OPENGLES_1
     } else if (glgsg->_supports_empty_framebuffer) {
     } else if (glgsg->_supports_empty_framebuffer) {
@@ -793,6 +821,12 @@ bind_slot(int layer, bool rb_resize, Texture **attach, RenderTexturePlane slot,
       _fb_properties.setup_color_texture(tex);
       _fb_properties.setup_color_texture(tex);
     }
     }
 
 
+    if (slot == RTP_color && !tex->has_clear_color()) {
+      tex->set_clear_color(LColor(0, 0, 0, 1));
+    }
+
+    _textures.push_back(tex);
+
     TextureContext *tc = tex->prepare_now(glgsg->get_prepared_objects(), glgsg);
     TextureContext *tc = tex->prepare_now(glgsg->get_prepared_objects(), glgsg);
     nassertv(tc != nullptr);
     nassertv(tc != nullptr);
     CLP(TextureContext) *gtc = DCAST(CLP(TextureContext), tc);
     CLP(TextureContext) *gtc = DCAST(CLP(TextureContext), tc);
@@ -1132,7 +1166,9 @@ bind_slot(int layer, bool rb_resize, Texture **attach, RenderTexturePlane slot,
 
 
     if (slot == RTP_depth_stencil) {
     if (slot == RTP_depth_stencil) {
       if (GLCAT.is_debug()) {
       if (GLCAT.is_debug()) {
-        GLCAT.debug() << "Creating depth stencil renderbuffer.\n";
+        GLCAT.debug()
+          << "Creating depth stencil renderbuffer with format 0x" << std::hex
+          << gl_format << std::dec << ".\n";
       }
       }
       // Allocate renderbuffer storage for depth stencil.
       // Allocate renderbuffer storage for depth stencil.
       GLint depth_size = 0, stencil_size = 0;
       GLint depth_size = 0, stencil_size = 0;
@@ -1160,7 +1196,9 @@ bind_slot(int layer, bool rb_resize, Texture **attach, RenderTexturePlane slot,
 
 
     } else if (slot == RTP_depth) {
     } else if (slot == RTP_depth) {
       if (GLCAT.is_debug()) {
       if (GLCAT.is_debug()) {
-        GLCAT.debug() << "Creating depth renderbuffer.\n";
+        GLCAT.debug()
+          << "Creating depth renderbuffer with format 0x" << std::hex
+          << gl_format << std::dec << ".\n";
       }
       }
       // Allocate renderbuffer storage for regular depth.
       // Allocate renderbuffer storage for regular depth.
       GLint depth_size = 0;
       GLint depth_size = 0;
@@ -1176,6 +1214,11 @@ bind_slot(int layer, bool rb_resize, Texture **attach, RenderTexturePlane slot,
         } else {
         } else {
           gl_format = GL_DEPTH_COMPONENT32F_NV;
           gl_format = GL_DEPTH_COMPONENT32F_NV;
         }
         }
+        if (GLCAT.is_debug()) {
+          GLCAT.debug()
+            << "GL_DEPTH_COMPONENT32 not supported, switching to format 0x"
+            << std::hex << gl_format << std::dec << " instead.\n";
+        }
         glgsg->_glRenderbufferStorage(GL_RENDERBUFFER_EXT, gl_format, _rb_size_x, _rb_size_y);
         glgsg->_glRenderbufferStorage(GL_RENDERBUFFER_EXT, gl_format, _rb_size_x, _rb_size_y);
         glgsg->_glGetRenderbufferParameteriv(GL_RENDERBUFFER_EXT, GL_RENDERBUFFER_DEPTH_SIZE_EXT, &depth_size);
         glgsg->_glGetRenderbufferParameteriv(GL_RENDERBUFFER_EXT, GL_RENDERBUFFER_DEPTH_SIZE_EXT, &depth_size);
 
 
@@ -1200,7 +1243,9 @@ bind_slot(int layer, bool rb_resize, Texture **attach, RenderTexturePlane slot,
 
 
     } else {
     } else {
       if (GLCAT.is_debug()) {
       if (GLCAT.is_debug()) {
-        GLCAT.debug() << "Creating color renderbuffer.\n";
+        GLCAT.debug()
+          << "Creating color renderbuffer with format 0x" << std::hex
+          << gl_format << std::dec << ".\n";
       }
       }
       glgsg->_glRenderbufferStorage(GL_RENDERBUFFER_EXT, gl_format, _rb_size_x, _rb_size_y);
       glgsg->_glRenderbufferStorage(GL_RENDERBUFFER_EXT, gl_format, _rb_size_x, _rb_size_y);
 
 
@@ -1248,12 +1293,33 @@ bind_slot_multisample(bool rb_resize, Texture **attach, RenderTexturePlane slot,
 #ifndef OPENGLES_2
 #ifndef OPENGLES_2
     if (_use_depth_stencil) {
     if (_use_depth_stencil) {
       glgsg->_glBindRenderbuffer(GL_RENDERBUFFER_EXT, _rbm[slot]);
       glgsg->_glBindRenderbuffer(GL_RENDERBUFFER_EXT, _rbm[slot]);
+      GLuint format;
+#ifdef OPENGLES_1
+      format = GL_DEPTH24_STENCIL8_OES;
+#else
+      if (_fb_properties.get_depth_bits() > 24 ||
+          _fb_properties.get_float_depth()) {
+        if (!glgsg->_use_remapped_depth_range) {
+          format = GL_DEPTH32F_STENCIL8;
+        } else {
+          format = GL_DEPTH32F_STENCIL8_NV;
+        }
+      } else {
+        format = GL_DEPTH24_STENCIL8;
+      }
+#endif
+      if (GLCAT.is_debug()) {
+        GLCAT.debug()
+          << "Creating depth stencil renderbuffer with format 0x" << std::hex
+          << format << std::dec << " and " << _requested_multisamples
+          << " multisamples.\n";
+      }
       if (_requested_coverage_samples) {
       if (_requested_coverage_samples) {
         glgsg->_glRenderbufferStorageMultisampleCoverage(GL_RENDERBUFFER_EXT, _requested_coverage_samples,
         glgsg->_glRenderbufferStorageMultisampleCoverage(GL_RENDERBUFFER_EXT, _requested_coverage_samples,
-                                                         _requested_multisamples, GL_DEPTH_STENCIL_EXT,
+                                                         _requested_multisamples, format,
                                                          _rb_size_x, _rb_size_y);
                                                          _rb_size_x, _rb_size_y);
       } else {
       } else {
-        glgsg->_glRenderbufferStorageMultisample(GL_RENDERBUFFER_EXT, _requested_multisamples, GL_DEPTH_STENCIL_EXT,
+        glgsg->_glRenderbufferStorageMultisample(GL_RENDERBUFFER_EXT, _requested_multisamples, format,
                                                  _rb_size_x, _rb_size_y);
                                                  _rb_size_x, _rb_size_y);
       }
       }
 #ifndef OPENGLES
 #ifndef OPENGLES
@@ -1292,6 +1358,22 @@ bind_slot_multisample(bool rb_resize, Texture **attach, RenderTexturePlane slot,
           default:
           default:
             break;
             break;
         }
         }
+#ifndef OPENGLES
+      } else if (_fb_properties.get_depth_bits() > 24) {
+        format = GL_DEPTH_COMPONENT32;
+      } else if (_fb_properties.get_depth_bits() > 16) {
+        format = GL_DEPTH_COMPONENT24;
+      } else if (_fb_properties.get_depth_bits() > 1) {
+        format = GL_DEPTH_COMPONENT16;
+      } else {
+        format = GL_DEPTH_COMPONENT;
+#endif
+      }
+      if (GLCAT.is_debug()) {
+        GLCAT.debug()
+          << "Creating depth renderbuffer with format 0x" << std::hex
+          << format << std::dec << " and " << _requested_multisamples
+          << " multisamples.\n";
       }
       }
       if (_requested_coverage_samples) {
       if (_requested_coverage_samples) {
         glgsg->_glRenderbufferStorageMultisampleCoverage(GL_RENDERBUFFER_EXT, _requested_coverage_samples,
         glgsg->_glRenderbufferStorageMultisampleCoverage(GL_RENDERBUFFER_EXT, _requested_coverage_samples,
@@ -1331,21 +1413,97 @@ bind_slot_multisample(bool rb_resize, Texture **attach, RenderTexturePlane slot,
       case RTP_aux_rgba_1:
       case RTP_aux_rgba_1:
       case RTP_aux_rgba_2:
       case RTP_aux_rgba_2:
       case RTP_aux_rgba_3:
       case RTP_aux_rgba_3:
+        gl_format = GL_RGBA;
+        break;
       default:
       default:
-        if (_fb_properties.get_srgb_color()) {
-          gl_format = GL_SRGB8_ALPHA8;
-        } else if (_fb_properties.get_float_color()) {
-          if (_fb_properties.get_color_bits() > 16 * 3) {
-            gl_format = GL_RGBA32F_ARB;
+        if (_fb_properties.get_alpha_bits() == 0) {
+          if (_fb_properties.get_srgb_color()) {
+            gl_format = GL_SRGB8;
+          } else if (_fb_properties.get_color_bits() > 16 * 3 ||
+                     _fb_properties.get_red_bits() > 16 ||
+                     _fb_properties.get_green_bits() > 16 ||
+                     _fb_properties.get_blue_bits() > 16) {
+            // 32-bit, which is always floating-point.
+            if (_fb_properties.get_blue_bits() > 0 ||
+                _fb_properties.get_color_bits() == 1 ||
+                _fb_properties.get_color_bits() > 32 * 2) {
+              gl_format = GL_RGB32F;
+            } else if (_fb_properties.get_green_bits() > 0 ||
+                       _fb_properties.get_color_bits() > 32) {
+              gl_format = GL_RG32F;
+            } else {
+              gl_format = GL_R32F;
+            }
+          } else if (_fb_properties.get_float_color()) {
+            // 16-bit floating-point.
+            if (_fb_properties.get_blue_bits() > 10 ||
+                _fb_properties.get_color_bits() == 1 ||
+                _fb_properties.get_color_bits() > 32) {
+              gl_format = GL_RGB16F;
+            } else if (_fb_properties.get_blue_bits() > 0) {
+              if (_fb_properties.get_red_bits() > 11 ||
+                  _fb_properties.get_green_bits() > 11) {
+                gl_format = GL_RGB16F;
+              } else {
+                gl_format = GL_R11F_G11F_B10F;
+              }
+            } else if (_fb_properties.get_green_bits() > 0 ||
+                       _fb_properties.get_color_bits() > 16) {
+              gl_format = GL_RG16F;
+            } else {
+              gl_format = GL_R16F;
+            }
+          } else if (_fb_properties.get_color_bits() > 10 * 3 ||
+                     _fb_properties.get_red_bits() > 10 ||
+                     _fb_properties.get_green_bits() > 10 ||
+                     _fb_properties.get_blue_bits() > 10) {
+            // 16-bit normalized.
+            if (_fb_properties.get_blue_bits() > 0 ||
+                _fb_properties.get_color_bits() == 1 ||
+                _fb_properties.get_color_bits() > 16 * 2) {
+              gl_format = GL_RGBA16;
+            } else if (_fb_properties.get_green_bits() > 0 ||
+                       _fb_properties.get_color_bits() > 16) {
+              gl_format = GL_RG16;
+            } else {
+              gl_format = GL_R16;
+            }
+          } else if (_fb_properties.get_color_bits() > 8 * 3 ||
+                     _fb_properties.get_red_bits() > 8 ||
+                     _fb_properties.get_green_bits() > 8 ||
+                     _fb_properties.get_blue_bits() > 8) {
+            gl_format = GL_RGB10_A2;
           } else {
           } else {
-            gl_format = GL_RGBA16F_ARB;
+            gl_format = GL_RGB;
           }
           }
         } else {
         } else {
-          gl_format = GL_RGBA;
+          if (_fb_properties.get_srgb_color()) {
+            gl_format = GL_SRGB8_ALPHA8;
+          } else if (_fb_properties.get_float_color()) {
+            if (_fb_properties.get_color_bits() > 16 * 3) {
+              gl_format = GL_RGBA32F_ARB;
+            } else {
+              gl_format = GL_RGBA16F_ARB;
+            }
+          } else {
+            if (_fb_properties.get_color_bits() > 16 * 3) {
+              gl_format = GL_RGBA32F_ARB;
+            } else if (_fb_properties.get_color_bits() > 8 * 3) {
+              gl_format = GL_RGBA16;
+            } else {
+              gl_format = GL_RGBA;
+            }
+          }
         }
         }
         break;
         break;
     }
     }
 #endif
 #endif
+    if (GLCAT.is_debug()) {
+      GLCAT.debug()
+        << "Creating color renderbuffer with format 0x" << std::hex
+        << gl_format << std::dec << " and " << _requested_multisamples
+        << " multisamples.\n";
+    }
     glgsg->_glBindRenderbuffer(GL_RENDERBUFFER_EXT, _rbm[slot]);
     glgsg->_glBindRenderbuffer(GL_RENDERBUFFER_EXT, _rbm[slot]);
     if (_requested_coverage_samples) {
     if (_requested_coverage_samples) {
       glgsg->_glRenderbufferStorageMultisampleCoverage(GL_RENDERBUFFER_EXT, _requested_coverage_samples,
       glgsg->_glRenderbufferStorageMultisampleCoverage(GL_RENDERBUFFER_EXT, _requested_coverage_samples,
@@ -1405,6 +1563,7 @@ attach_tex(GLenum attachpoint, CLP(TextureContext) *gtc, int view, int layer) {
                                    target, index, 0, layer);
                                    target, index, 0, layer);
     break;
     break;
   case GL_TEXTURE_2D_ARRAY:
   case GL_TEXTURE_2D_ARRAY:
+  case GL_TEXTURE_CUBE_MAP_ARRAY:
     glgsg->_glFramebufferTextureLayer(GL_FRAMEBUFFER_EXT, attachpoint,
     glgsg->_glFramebufferTextureLayer(GL_FRAMEBUFFER_EXT, attachpoint,
                                       index, 0, layer);
                                       index, 0, layer);
     break;
     break;
@@ -1967,7 +2126,7 @@ resolve_multisamples() {
     // Issue memory barriers as necessary to make sure that the texture memory
     // Issue memory barriers as necessary to make sure that the texture memory
     // is synchronized before we blit to it.
     // is synchronized before we blit to it.
     for (CLP(TextureContext) *gtc : _texture_contexts) {
     for (CLP(TextureContext) *gtc : _texture_contexts) {
-      if (gtc->needs_barrier(GL_FRAMEBUFFER_BARRIER_BIT)) {
+      if (gtc->needs_barrier(GL_FRAMEBUFFER_BARRIER_BIT, true)) {
         glgsg->issue_memory_barrier(GL_FRAMEBUFFER_BARRIER_BIT);
         glgsg->issue_memory_barrier(GL_FRAMEBUFFER_BARRIER_BIT);
         // If we've done it for one, we've done it for all.
         // If we've done it for one, we've done it for all.
         break;
         break;

+ 4 - 0
panda/src/glstuff/glGraphicsBuffer_src.h

@@ -128,6 +128,10 @@ protected:
   typedef pvector<CLP(TextureContext)*> TextureContexts;
   typedef pvector<CLP(TextureContext)*> TextureContexts;
   TextureContexts _texture_contexts;
   TextureContexts _texture_contexts;
 
 
+  // List of textures we need to keep a reference to.
+  typedef pvector<PT(Texture)> Textures;
+  Textures _textures;
+
   // The cube map face we are currently drawing to or have just finished
   // The cube map face we are currently drawing to or have just finished
   // drawing to, or -1 if we are not drawing to a cube map.
   // drawing to, or -1 if we are not drawing to a cube map.
   int _bound_tex_page;
   int _bound_tex_page;

File diff suppressed because it is too large
+ 515 - 219
panda/src/glstuff/glGraphicsStateGuardian_src.cxx


+ 72 - 22
panda/src/glstuff/glGraphicsStateGuardian_src.h

@@ -39,6 +39,8 @@
 #include "geomVertexArrayData.h"
 #include "geomVertexArrayData.h"
 #include "lightMutex.h"
 #include "lightMutex.h"
 #include "pStatGPUTimer.h"
 #include "pStatGPUTimer.h"
+#include "completionToken.h"
+#include "asyncTaskChain.h"
 
 
 class PlaneNode;
 class PlaneNode;
 class Light;
 class Light;
@@ -166,6 +168,7 @@ typedef void (APIENTRYP PFNGLGETPROGRAMINFOLOGPROC) (GLuint program, GLsizei buf
 typedef void (APIENTRYP PFNGLGETSHADERIVPROC) (GLuint shader, GLenum pname, GLint *params);
 typedef void (APIENTRYP PFNGLGETSHADERIVPROC) (GLuint shader, GLenum pname, GLint *params);
 typedef void (APIENTRYP PFNGLGETSHADERINFOLOGPROC) (GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog);
 typedef void (APIENTRYP PFNGLGETSHADERINFOLOGPROC) (GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog);
 typedef GLint (APIENTRYP PFNGLGETUNIFORMLOCATIONPROC) (GLuint program, const GLchar *name);
 typedef GLint (APIENTRYP PFNGLGETUNIFORMLOCATIONPROC) (GLuint program, const GLchar *name);
+typedef void (APIENTRYP PFNGLGETINTEGERI_VPROC) (GLenum target, GLuint index, GLint *data);
 typedef void (APIENTRYP PFNGLLINKPROGRAMPROC) (GLuint program);
 typedef void (APIENTRYP PFNGLLINKPROGRAMPROC) (GLuint program);
 typedef void (APIENTRYP PFNGLSHADERSOURCEPROC_P) (GLuint shader, GLsizei count, const GLchar* const *string, const GLint *length);
 typedef void (APIENTRYP PFNGLSHADERSOURCEPROC_P) (GLuint shader, GLsizei count, const GLchar* const *string, const GLint *length);
 typedef void (APIENTRYP PFNGLSPECIALIZESHADERARBPROC) (GLuint shader, const GLchar *, GLuint, const GLuint *, const GLuint *);
 typedef void (APIENTRYP PFNGLSPECIALIZESHADERARBPROC) (GLuint shader, const GLchar *, GLuint, const GLuint *, const GLuint *);
@@ -250,6 +253,7 @@ typedef void (APIENTRYP PFNGLGETPROGRAMBINARYPROC) (GLuint program, GLsizei bufS
 typedef void (APIENTRYP PFNGLPROGRAMBINARYPROC) (GLuint program, GLenum binaryFormat, const void *binary, GLsizei length);
 typedef void (APIENTRYP PFNGLPROGRAMBINARYPROC) (GLuint program, GLenum binaryFormat, const void *binary, GLsizei length);
 typedef void (APIENTRYP PFNGLGETINTERNALFORMATIVPROC) (GLenum target, GLenum internalformat, GLenum pname, GLsizei bufSize, GLint *params);
 typedef void (APIENTRYP PFNGLGETINTERNALFORMATIVPROC) (GLenum target, GLenum internalformat, GLenum pname, GLsizei bufSize, GLint *params);
 typedef void (APIENTRYP PFNGLBUFFERSTORAGEPROC) (GLenum target, GLsizeiptr size, const void *data, GLbitfield flags);
 typedef void (APIENTRYP PFNGLBUFFERSTORAGEPROC) (GLenum target, GLsizeiptr size, const void *data, GLbitfield flags);
+typedef void (APIENTRYP PFNGLCOPYBUFFERSUBDATAPROC) (GLenum readTarget, GLenum writeTarget, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size);
 typedef void (APIENTRYP PFNGLBINDIMAGETEXTUREPROC) (GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format);
 typedef void (APIENTRYP PFNGLBINDIMAGETEXTUREPROC) (GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format);
 typedef void (APIENTRYP PFNGLCLEARTEXIMAGEPROC) (GLuint texture, GLint level, GLenum format, GLenum type, const void *data);
 typedef void (APIENTRYP PFNGLCLEARTEXIMAGEPROC) (GLuint texture, GLint level, GLenum format, GLenum type, const void *data);
 typedef void (APIENTRYP PFNGLCLEARTEXSUBIMAGEPROC) (GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void *data);
 typedef void (APIENTRYP PFNGLCLEARTEXSUBIMAGEPROC) (GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void *data);
@@ -365,7 +369,8 @@ public:
 #endif
 #endif
 
 
   virtual TextureContext *prepare_texture(Texture *tex);
   virtual TextureContext *prepare_texture(Texture *tex);
-  virtual bool update_texture(TextureContext *tc, bool force);
+  virtual bool update_texture(TextureContext *tc, bool force,
+                              CompletionToken token = CompletionToken());
   virtual void release_texture(TextureContext *tc);
   virtual void release_texture(TextureContext *tc);
   virtual void release_textures(const pvector<TextureContext *> &contexts);
   virtual void release_textures(const pvector<TextureContext *> &contexts);
   virtual bool extract_texture_data(Texture *tex);
   virtual bool extract_texture_data(Texture *tex);
@@ -411,9 +416,10 @@ public:
 
 
 #ifndef OPENGLES_1
 #ifndef OPENGLES_1
   virtual BufferContext *prepare_shader_buffer(ShaderBuffer *data);
   virtual BufferContext *prepare_shader_buffer(ShaderBuffer *data);
-  void apply_shader_buffer(GLuint base, ShaderBuffer *buffer);
+  CLP(BufferContext) *apply_shader_buffer(GLuint base, ShaderBuffer *buffer);
   virtual void release_shader_buffer(BufferContext *bc);
   virtual void release_shader_buffer(BufferContext *bc);
   virtual void release_shader_buffers(const pvector<BufferContext *> &contexts);
   virtual void release_shader_buffers(const pvector<BufferContext *> &contexts);
+  virtual bool extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data);
 #endif
 #endif
 
 
 #ifndef OPENGLES
 #ifndef OPENGLES
@@ -438,7 +444,6 @@ public:
   virtual bool framebuffer_copy_to_ram
   virtual bool framebuffer_copy_to_ram
     (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb,
     (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb,
      ScreenshotRequest *request);
      ScreenshotRequest *request);
-  void finish_async_framebuffer_ram_copies(bool force = false);
 
 
 #ifdef SUPPORT_FIXED_FUNCTION
 #ifdef SUPPORT_FIXED_FUNCTION
   void apply_fog(Fog *fog);
   void apply_fog(Fog *fog);
@@ -649,12 +654,21 @@ protected:
   bool apply_texture(CLP(TextureContext) *gtc, int view);
   bool apply_texture(CLP(TextureContext) *gtc, int view);
   bool apply_sampler(GLuint unit, const SamplerState &sampler,
   bool apply_sampler(GLuint unit, const SamplerState &sampler,
                      CLP(TextureContext) *gtc, int view);
                      CLP(TextureContext) *gtc, int view);
-  bool upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps);
-  bool upload_texture_image(CLP(TextureContext) *gtc, int view,
-                            bool needs_reload, int mipmap_bias, int num_levels,
+  bool upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps,
+                      CompletionToken token = CompletionToken());
+  bool upload_texture_view(CLP(TextureContext) *gtc, int view,
+                           bool needs_reload, int mipmap_bias, int num_levels,
+                           GLint internal_format, GLint external_format,
+                           GLenum component_type, bool compressed,
+                           int async_buffers, CompletionToken token);
+  bool upload_texture_level(bool full_reload, bool compressed,
+                            GLenum target, int level,
+                            int width, int height, int depth,
                             GLint internal_format, GLint external_format,
                             GLint internal_format, GLint external_format,
                             GLenum component_type,
                             GLenum component_type,
-                            Texture::CompressionMode image_compression);
+                            const unsigned char *image_ptr,
+                            size_t page_size, SparseArray pages,
+                            GLenum usage_hint);
   void generate_mipmaps(CLP(TextureContext) *gtc);
   void generate_mipmaps(CLP(TextureContext) *gtc);
   bool upload_simple_texture(CLP(TextureContext) *gtc);
   bool upload_simple_texture(CLP(TextureContext) *gtc);
 
 
@@ -670,6 +684,20 @@ protected:
   void do_point_size();
   void do_point_size();
 #endif
 #endif
 
 
+#ifndef OPENGLES_1
+  void *map_read_buffer(GLenum target, GLuint buffer, size_t size);
+  void *map_write_discard_buffer(GLenum target, GLuint buffer, size_t size,
+                                 bool create_storage);
+#endif
+
+#ifndef OPENGLES_1
+  void insert_fence(CompletionToken &&callback);
+  void process_fences(bool force);
+#endif
+
+  void call_later(Completable &&job);
+  void process_pending_jobs(bool wait);
+
   enum AutoAntialiasMode {
   enum AutoAntialiasMode {
     AA_poly,
     AA_poly,
     AA_line,
     AA_line,
@@ -802,6 +830,10 @@ protected:
 #endif
 #endif
 
 
 public:
 public:
+#ifndef OPENGLES_1
+  PFNGLGETINTEGERI_VPROC _glGetIntegeri_v;
+#endif
+
 #ifndef OPENGLES_1
 #ifndef OPENGLES_1
   bool _use_depth_zero_to_one;
   bool _use_depth_zero_to_one;
   bool _use_remapped_depth_range;
   bool _use_remapped_depth_range;
@@ -909,6 +941,10 @@ public:
   PFNGLGETBUFFERSUBDATAPROC _glGetBufferSubData;
   PFNGLGETBUFFERSUBDATAPROC _glGetBufferSubData;
 #endif
 #endif
 
 
+#ifndef OPENGLES_1
+  PFNGLCOPYBUFFERSUBDATAPROC _glCopyBufferSubData;
+#endif
+
 #ifdef OPENGLES
 #ifdef OPENGLES
   PFNGLMAPBUFFERRANGEEXTPROC _glMapBufferRange;
   PFNGLMAPBUFFERRANGEEXTPROC _glMapBufferRange;
   PFNGLUNMAPBUFFEROESPROC _glUnmapBuffer;
   PFNGLUNMAPBUFFEROESPROC _glUnmapBuffer;
@@ -916,6 +952,10 @@ public:
   PFNGLMAPBUFFERRANGEPROC _glMapBufferRange;
   PFNGLMAPBUFFERRANGEPROC _glMapBufferRange;
 #endif
 #endif
 
 
+#ifndef OPENGLES_1
+  bool _supports_pixel_buffers;
+#endif
+
 #ifndef OPENGLES_1
 #ifndef OPENGLES_1
   bool _supports_uniform_buffers;
   bool _supports_uniform_buffers;
   bool _supports_shader_buffers;
   bool _supports_shader_buffers;
@@ -983,6 +1023,7 @@ public:
   PFNGLTEXTUREPARAMETERIPROC _glTextureParameteri;
   PFNGLTEXTUREPARAMETERIPROC _glTextureParameteri;
   PFNGLGENERATETEXTUREMIPMAPPROC _glGenerateTextureMipmap;
   PFNGLGENERATETEXTUREMIPMAPPROC _glGenerateTextureMipmap;
   PFNGLBINDTEXTUREUNITPROC _glBindTextureUnit;
   PFNGLBINDTEXTUREUNITPROC _glBindTextureUnit;
+  PFNGLMAPNAMEDBUFFERRANGEPROC _glMapNamedBufferRange;
 #endif
 #endif
 
 
 #ifndef OPENGLES_1
 #ifndef OPENGLES_1
@@ -1188,12 +1229,15 @@ public:
 #endif
 #endif
 
 
 #ifndef OPENGLES_1
 #ifndef OPENGLES_1
-  // Stores textures for which memory bariers should be issued.
-  typedef pset<TextureContext*> TextureSet;
-  TextureSet _textures_needing_fetch_barrier;
-  TextureSet _textures_needing_image_access_barrier;
-  TextureSet _textures_needing_update_barrier;
-  TextureSet _textures_needing_framebuffer_barrier;
+  // This count increments every time the corresponding barrier is issued.
+  // GLTextureContext et al store copies of this counter, when a write is
+  // performed on a texture, it will set its counter to match the value on the
+  // GSG to indicate that it is out of sync and the barrier needs to be issued.
+  int _texture_fetch_barrier_counter = 0;
+  int _shader_image_access_barrier_counter = 0;
+  int _texture_update_barrier_counter = 0;
+  int _framebuffer_barrier_counter = 0;
+  int _shader_storage_barrier_counter = 0;
 #endif
 #endif
 
 
   // RenderState::SlotMask _inv_state_mask;
   // RenderState::SlotMask _inv_state_mask;
@@ -1243,16 +1287,21 @@ public:
   FrameTiming *_current_frame_timing = nullptr;
   FrameTiming *_current_frame_timing = nullptr;
 #endif
 #endif
 
 
-  struct AsyncRamCopy {
-    PT(ScreenshotRequest) _request;
-    GLuint _pbo;
-    GLsync _fence;
-    GLuint _external_format;
-    int _view;
-    void *_mapped_pointer;
-    size_t _size;
+  struct Fence {
+    GLsync _object;
+    CompletionToken _token;
   };
   };
-  pdeque<AsyncRamCopy> _async_ram_copies;
+  pdeque<Fence> _fences;
+
+#ifdef HAVE_THREADS
+  AsyncTaskChain *_async_chain;
+#endif
+
+  // Min job system pending a real job system
+  typedef pvector<Completable> JobQueue;
+  Mutex _job_queue_mutex;
+  ConditionVar _job_queue_cvar;
+  JobQueue _job_queue;
 
 
   BufferResidencyTracker _renderbuffer_residency;
   BufferResidencyTracker _renderbuffer_residency;
 
 
@@ -1296,6 +1345,7 @@ private:
   friend class CLP(IndexBufferContext);
   friend class CLP(IndexBufferContext);
   friend class CLP(BufferContext);
   friend class CLP(BufferContext);
   friend class CLP(ShaderContext);
   friend class CLP(ShaderContext);
+  friend class CLP(TextureContext);
   friend class CLP(GraphicsBuffer);
   friend class CLP(GraphicsBuffer);
   friend class CLP(OcclusionQueryContext);
   friend class CLP(OcclusionQueryContext);
 };
 };

+ 44 - 12
panda/src/glstuff/glShaderContext_src.cxx

@@ -199,6 +199,10 @@ CLP(ShaderContext)(CLP(GraphicsStateGuardian) *glgsg, Shader *s) : ShaderContext
   if (!valid) {
   if (!valid) {
     _shader->_error_flag = true;
     _shader->_error_flag = true;
   }
   }
+
+#ifdef DO_PSTATS
+  _compute_dispatch_pcollector = PStatCollector(_glgsg->_compute_dispatch_pcollector, s->get_debug_name());
+#endif
 }
 }
 
 
 /**
 /**
@@ -2173,6 +2177,10 @@ update_shader_vertex_arrays(ShaderContext *prev, bool force) {
     issue_parameters(Shader::D_vertex_data);
     issue_parameters(Shader::D_vertex_data);
   }
   }
 
 
+  // This ought to be moved elsewhere, but it's convenient to do this here for
+  // now since it's called before every Geom is drawn.
+  issue_memory_barriers();
+
   _glgsg->report_my_gl_errors();
   _glgsg->report_my_gl_errors();
 
 
   return true;
   return true;
@@ -2256,9 +2264,7 @@ update_shader_texture_bindings(ShaderContext *prev) {
   //  return;
   //  return;
   //}
   //}
 
 
-#ifndef OPENGLES
   GLbitfield barriers = 0;
   GLbitfield barriers = 0;
-#endif
 
 
   ShaderInputBinding::State state;
   ShaderInputBinding::State state;
   state.gsg = _glgsg;
   state.gsg = _glgsg;
@@ -2293,12 +2299,6 @@ update_shader_texture_bindings(ShaderContext *prev) {
 
 
           int view = _glgsg->get_current_tex_view_offset();
           int view = _glgsg->get_current_tex_view_offset();
           gl_tex = gtc->get_view_index(view);
           gl_tex = gtc->get_view_index(view);
-
-#ifndef OPENGLES
-          if (gtc->needs_barrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT)) {
-            barriers |= GL_SHADER_IMAGE_ACCESS_BARRIER_BIT;
-          }
-#endif
         }
         }
       }
       }
 
 
@@ -2355,6 +2355,10 @@ update_shader_texture_bindings(ShaderContext *prev) {
           break;
           break;
         }
         }
 
 
+        if (gtc->needs_barrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT, unit._written)) {
+          barriers |= GL_SHADER_IMAGE_ACCESS_BARRIER_BIT;
+        }
+
         _glgsg->_glBindImageTexture(i, gl_tex, bind_level, layered,
         _glgsg->_glBindImageTexture(i, gl_tex, bind_level, layered,
                                     bind_layer, gl_access, gtc->_internal_format);
                                     bind_layer, gl_access, gtc->_internal_format);
 
 
@@ -2420,7 +2424,7 @@ update_shader_texture_bindings(ShaderContext *prev) {
 #ifndef OPENGLES
 #ifndef OPENGLES
     // If it was recently written to, we will have to issue a memory barrier
     // If it was recently written to, we will have to issue a memory barrier
     // soon.
     // soon.
-    if (gtc->needs_barrier(GL_TEXTURE_FETCH_BARRIER_BIT)) {
+    if (gtc->needs_barrier(GL_TEXTURE_FETCH_BARRIER_BIT, false)) {
       barriers |= GL_TEXTURE_FETCH_BARRIER_BIT;
       barriers |= GL_TEXTURE_FETCH_BARRIER_BIT;
     }
     }
 #endif
 #endif
@@ -2471,12 +2475,12 @@ update_shader_texture_bindings(ShaderContext *prev) {
     _glgsg->_glBindTextures(0, num_textures, textures);
     _glgsg->_glBindTextures(0, num_textures, textures);
     _glgsg->_glBindSamplers(0, num_textures, samplers);
     _glgsg->_glBindSamplers(0, num_textures, samplers);
   }
   }
+#endif
 
 
   if (barriers != 0) {
   if (barriers != 0) {
     // Issue a memory barrier prior to this shader's execution.
     // Issue a memory barrier prior to this shader's execution.
     _glgsg->issue_memory_barrier(barriers);
     _glgsg->issue_memory_barrier(barriers);
   }
   }
-#endif
 
 
   _glgsg->report_my_gl_errors();
   _glgsg->report_my_gl_errors();
 }
 }
@@ -2491,10 +2495,38 @@ update_shader_buffer_bindings(ShaderContext *prev) {
   state.gsg = _glgsg;
   state.gsg = _glgsg;
   state.matrix_cache = &_matrix_cache[0];
   state.matrix_cache = &_matrix_cache[0];
 
 
-  for (const StorageBlock &block : _storage_blocks) {
+  for (StorageBlock &block : _storage_blocks) {
     PT(ShaderBuffer) buffer = block._binding->fetch_shader_buffer(state, block._resource_id);
     PT(ShaderBuffer) buffer = block._binding->fetch_shader_buffer(state, block._resource_id);
-    _glgsg->apply_shader_buffer(block._binding_index, buffer);
+    block._gbc = _glgsg->apply_shader_buffer(block._binding_index, buffer);
+  }
+}
+
+/**
+ * Issues memory barriers for shader buffers, should be called before a draw.
+ */
+void CLP(ShaderContext)::
+issue_memory_barriers() {
+#ifndef OPENGLES
+  bool barrier_needed = false;
+  for (StorageBlock &block : _storage_blocks) {
+    if (block._gbc != nullptr &&
+        block._gbc->_shader_storage_barrier_counter == _glgsg->_shader_storage_barrier_counter) {
+      barrier_needed = true;
+      break;
+    }
+  }
+
+  if (barrier_needed) {
+    _glgsg->issue_memory_barrier(GL_SHADER_STORAGE_BARRIER_BIT);
   }
   }
+
+  // We assume that all SSBOs will be written to, for now.
+  for (StorageBlock &block : _storage_blocks) {
+    if (block._gbc != nullptr) {
+      block._gbc->_shader_storage_barrier_counter = _glgsg->_shader_storage_barrier_counter;
+    }
+  }
+#endif
 }
 }
 
 
 /**
 /**

+ 6 - 0
panda/src/glstuff/glShaderContext_src.h

@@ -92,6 +92,7 @@ private:
   void disable_shader_texture_bindings();
   void disable_shader_texture_bindings();
   void update_shader_texture_bindings(ShaderContext *prev);
   void update_shader_texture_bindings(ShaderContext *prev);
   void update_shader_buffer_bindings(ShaderContext *prev);
   void update_shader_buffer_bindings(ShaderContext *prev);
+  void issue_memory_barriers();
 
 
   bool uses_standard_vertex_arrays(void) {
   bool uses_standard_vertex_arrays(void) {
     return _uses_standard_vertex_arrays;
     return _uses_standard_vertex_arrays;
@@ -172,6 +173,7 @@ private:
 
 
   struct StorageBlock {
   struct StorageBlock {
     PT(ShaderInputBinding) _binding;
     PT(ShaderInputBinding) _binding;
+    CLP(BufferContext) *_gbc = nullptr;
     ShaderInputBinding::ResourceId _resource_id;
     ShaderInputBinding::ResourceId _resource_id;
     GLint _binding_index;
     GLint _binding_index;
   };
   };
@@ -188,6 +190,10 @@ private:
 
 
   bool _uses_standard_vertex_arrays;
   bool _uses_standard_vertex_arrays;
 
 
+#ifdef DO_PSTATS
+  PStatCollector _compute_dispatch_pcollector;
+#endif
+
   void report_shader_errors(GLuint handle, Shader::Stage stage, bool fatal);
   void report_shader_errors(GLuint handle, Shader::Stage stage, bool fatal);
   void report_program_errors(GLuint program, bool fatal);
   void report_program_errors(GLuint program, bool fatal);
   GLuint create_shader(GLuint program, const ShaderModule *module, size_t mi,
   GLuint create_shader(GLuint program, const ShaderModule *module, size_t mi,

+ 40 - 0
panda/src/glstuff/glTextureContext_src.I

@@ -59,3 +59,43 @@ get_view_buffer(int view) const {
     return 0;
     return 0;
   }
   }
 }
 }
+
+/**
+ * Returns true if an async upload is pending.
+ */
+INLINE bool CLP(TextureContext)::
+is_upload_pending() const {
+  // We can't simply compare _uploads_started to _uploads_finished, since
+  // they also get set to the same by cancel_pending_uploads()
+  return _uploads_pending > 0;
+}
+
+/**
+ * Waits for all uploads to be finished.
+ */
+INLINE void CLP(TextureContext)::
+wait_pending_uploads() const {
+  if (is_upload_pending()) {
+    do_wait_pending_uploads();
+  }
+}
+
+/**
+ * Cancels all asynchronous uploads.  Not guaranteed to be cancelled by the
+ * time this returns, consider following this up with a call to
+ * wait_pending_uploads().
+ */
+INLINE void CLP(TextureContext)::
+cancel_pending_uploads() {
+  _uploads_finished = _uploads_started;
+}
+
+/**
+ * Waits for an unused PBO unless we're not at the given limit of PBOs yet.
+ */
+INLINE void CLP(TextureContext)::
+wait_for_unused_pbo(int limit) const {
+  if (_unused_pbos.empty() && _num_pbos >= limit) {
+    do_wait_for_unused_pbo(limit);
+  }
+}

+ 110 - 24
panda/src/glstuff/glTextureContext_src.cxx

@@ -13,6 +13,8 @@
 
 
 #include "pnotify.h"
 #include "pnotify.h"
 
 
+static PStatCollector _wait_async_texture_uploads_pcollector("Wait:Async Texture Uploads");
+
 TypeHandle CLP(TextureContext)::_type_handle;
 TypeHandle CLP(TextureContext)::_type_handle;
 
 
 /**
 /**
@@ -48,6 +50,8 @@ evict_lru() {
  */
  */
 void CLP(TextureContext)::
 void CLP(TextureContext)::
 reset_data(GLenum target, int num_views) {
 reset_data(GLenum target, int num_views) {
+  cancel_pending_uploads();
+
   // Free the texture resources.
   // Free the texture resources.
   set_num_views(0);
   set_num_views(0);
 
 
@@ -63,12 +67,13 @@ reset_data(GLenum target, int num_views) {
 
 
 #ifndef OPENGLES_1
 #ifndef OPENGLES_1
   // Mark the texture as coherent.
   // Mark the texture as coherent.
-  if (gl_enable_memory_barriers) {
-    _glgsg->_textures_needing_fetch_barrier.erase(this);
-    _glgsg->_textures_needing_image_access_barrier.erase(this);
-    _glgsg->_textures_needing_update_barrier.erase(this);
-    _glgsg->_textures_needing_framebuffer_barrier.erase(this);
-  }
+  _texture_fetch_barrier_counter = _glgsg->_texture_fetch_barrier_counter - 1;
+  _shader_image_read_barrier_counter = _glgsg->_shader_image_access_barrier_counter - 1;
+  _shader_image_write_barrier_counter = _glgsg->_shader_image_access_barrier_counter - 1;
+  _texture_read_barrier_counter = _glgsg->_texture_update_barrier_counter - 1;
+  _texture_write_barrier_counter = _glgsg->_shader_image_access_barrier_counter - 1;
+  _framebuffer_read_barrier_counter = _glgsg->_framebuffer_barrier_counter - 1;
+  _framebuffer_write_barrier_counter = _glgsg->_framebuffer_barrier_counter - 1;
 #endif
 #endif
 }
 }
 
 
@@ -168,26 +173,50 @@ set_num_views(int num_views) {
 
 
 #ifndef OPENGLES_1
 #ifndef OPENGLES_1
 /**
 /**
- *
+ * Returns true if the texture needs a barrier before a read or write of the
+ * given kind.  If writing is false, only writes are synced, otherwise both
+ * reads and writes are synced.
  */
  */
 bool CLP(TextureContext)::
 bool CLP(TextureContext)::
-needs_barrier(GLbitfield barrier) {
+needs_barrier(GLbitfield barrier, bool writing) {
   if (!gl_enable_memory_barriers) {
   if (!gl_enable_memory_barriers) {
     return false;
     return false;
   }
   }
 
 
-  return (((barrier & GL_TEXTURE_FETCH_BARRIER_BIT) &&
-           _glgsg->_textures_needing_fetch_barrier.count(this)))
-      || (((barrier & GL_SHADER_IMAGE_ACCESS_BARRIER_BIT) &&
-           _glgsg->_textures_needing_image_access_barrier.count(this)))
-      || (((barrier & GL_TEXTURE_UPDATE_BARRIER_BIT) &&
-           _glgsg->_textures_needing_update_barrier.count(this)))
-      || (((barrier & GL_FRAMEBUFFER_BARRIER_BIT) &&
-           _glgsg->_textures_needing_framebuffer_barrier.count(this)));
+  if (barrier & GL_TEXTURE_FETCH_BARRIER_BIT) {
+    // This is always a read, so only sync RAW.
+    if (_glgsg->_texture_fetch_barrier_counter == _texture_fetch_barrier_counter) {
+      return true;
+    }
+  }
+
+  if (barrier & GL_SHADER_IMAGE_ACCESS_BARRIER_BIT) {
+    // Sync WAR, WAW and RAW, but not RAR.
+    if ((writing && _glgsg->_shader_image_access_barrier_counter == _shader_image_read_barrier_counter) ||
+        (_glgsg->_shader_image_access_barrier_counter == _shader_image_write_barrier_counter)) {
+      return true;
+    }
+  }
+
+  if (barrier & GL_TEXTURE_UPDATE_BARRIER_BIT) {
+    if ((writing && _glgsg->_texture_update_barrier_counter == _texture_read_barrier_counter) ||
+        (_glgsg->_texture_update_barrier_counter == _texture_write_barrier_counter)) {
+      return true;
+    }
+  }
+
+  if (barrier & GL_FRAMEBUFFER_BARRIER_BIT) {
+    if ((writing && _glgsg->_framebuffer_barrier_counter == _framebuffer_read_barrier_counter) ||
+        (_glgsg->_framebuffer_barrier_counter == _framebuffer_write_barrier_counter)) {
+      return true;
+    }
+  }
+
+  return false;
 }
 }
 
 
 /**
 /**
- * Mark a texture as needing a memory barrier, since a non-coherent read or
+ * Mark a texture as needing a memory barrier, since an unsynchronized read or
  * write just happened to it.  If 'wrote' is true, it was written to.
  * write just happened to it.  If 'wrote' is true, it was written to.
  */
  */
 void CLP(TextureContext)::
 void CLP(TextureContext)::
@@ -199,16 +228,73 @@ mark_incoherent(bool wrote) {
   // If we only read from it, the next read operation won't need another
   // If we only read from it, the next read operation won't need another
   // barrier, since it'll be reading the same data.
   // barrier, since it'll be reading the same data.
   if (wrote) {
   if (wrote) {
-    _glgsg->_textures_needing_fetch_barrier.insert(this);
+    _texture_fetch_barrier_counter = _glgsg->_texture_fetch_barrier_counter;
+    _shader_image_write_barrier_counter = _glgsg->_shader_image_access_barrier_counter;
+    _texture_write_barrier_counter = _glgsg->_shader_image_access_barrier_counter;
+    _framebuffer_write_barrier_counter = _glgsg->_framebuffer_barrier_counter;
   }
   }
 
 
   // We could still write to it before we read from it, so we have to always
   // We could still write to it before we read from it, so we have to always
-  // insert these barriers.  This could be slightly optimized so that we don't
-  // issue a barrier between consecutive image reads, but that may not be
-  // worth the trouble.
-  _glgsg->_textures_needing_image_access_barrier.insert(this);
-  _glgsg->_textures_needing_update_barrier.insert(this);
-  _glgsg->_textures_needing_framebuffer_barrier.insert(this);
+  // insert these barriers.
+  _shader_image_read_barrier_counter = _glgsg->_shader_image_access_barrier_counter;
+  _texture_read_barrier_counter = _glgsg->_texture_update_barrier_counter;
+  _framebuffer_read_barrier_counter = _glgsg->_framebuffer_barrier_counter;
 }
 }
 
 
 #endif  // !OPENGLES_1
 #endif  // !OPENGLES_1
+
+/**
+ * Returns a PBO with the given size to the pool of unused PBOs.
+ */
+void CLP(TextureContext)::
+return_pbo(GLuint pbo, size_t size) {
+  // Also triggers when the number of buffers is -1 (which effectively means
+  // to always delete the buffers after use).
+  if (_num_pbos > get_texture()->get_num_async_transfer_buffers() ||
+      size < _pbo_size) {
+    // We have too many PBOs, or this PBO is no longer of the proper
+    // size, so delete it rather than returning it to the pool.
+    _num_pbos--;
+    _glgsg->_glDeleteBuffers(1, &pbo);
+  } else {
+    _unused_pbos.push_front(pbo);
+  }
+}
+
+/**
+ * Deletes all unused PBOs.
+ */
+void CLP(TextureContext)::
+delete_unused_pbos() {
+  if (!_unused_pbos.empty()) {
+    for (GLuint pbo : _unused_pbos) {
+      _glgsg->_glDeleteBuffers(1, &pbo);
+    }
+    _num_pbos -= (int)_unused_pbos.size();
+    _unused_pbos.clear();
+  }
+}
+
+/**
+ * Waits for all uploads to be finished.
+ */
+void CLP(TextureContext)::
+do_wait_pending_uploads() const {
+  PStatTimer timer(_wait_async_texture_uploads_pcollector);
+  do {
+    _glgsg->process_pending_jobs(true);
+  }
+  while (is_upload_pending());
+}
+
+/**
+ *
+ */
+void CLP(TextureContext)::
+do_wait_for_unused_pbo(int limit) const {
+  PStatTimer timer(_wait_async_texture_uploads_pcollector);
+  do {
+    _glgsg->process_pending_jobs(true);
+  }
+  while (_unused_pbos.empty() && _num_pbos >= limit);
+}

+ 31 - 2
panda/src/glstuff/glTextureContext_src.h

@@ -41,12 +41,24 @@ public:
   INLINE GLuint get_view_buffer(int view) const;
   INLINE GLuint get_view_buffer(int view) const;
 
 
 #ifdef OPENGLES_1
 #ifdef OPENGLES_1
-  static constexpr bool needs_barrier(GLbitfield barrier) { return false; };
+  static constexpr bool needs_barrier(GLbitfield barrier, bool writing) { return false; };
 #else
 #else
-  bool needs_barrier(GLbitfield barrier);
+  bool needs_barrier(GLbitfield barrier, bool writing);
   void mark_incoherent(bool wrote);
   void mark_incoherent(bool wrote);
 #endif
 #endif
 
 
+  INLINE bool is_upload_pending() const;
+  INLINE void wait_pending_uploads() const;
+  INLINE void cancel_pending_uploads();
+
+  void return_pbo(GLuint pbo, size_t size);
+  void delete_unused_pbos();
+  INLINE void wait_for_unused_pbo(int limit) const;
+
+private:
+  void do_wait_pending_uploads() const;
+  void do_wait_for_unused_pbo(int limit) const;
+
 private:
 private:
   // This is the GL "name" of the texture object.
   // This is the GL "name" of the texture object.
   GLuint _index;
   GLuint _index;
@@ -76,8 +88,25 @@ public:
   GLenum _target;
   GLenum _target;
   SamplerState _active_sampler;
   SamplerState _active_sampler;
 
 
+  // These counters are used to prevent out-of-order updates.
+  int _uploads_started = 0;
+  int _uploads_finished = 0;
+  int _uploads_pending = 0;
+  pdeque<GLuint> _unused_pbos;
+  int _num_pbos = 0;
+  size_t _pbo_size = 0;
+
   CLP(GraphicsStateGuardian) *_glgsg;
   CLP(GraphicsStateGuardian) *_glgsg;
 
 
+  // These are set to the equivalent counter in glgsg when a write is performed.
+  int _texture_fetch_barrier_counter = -1;
+  int _shader_image_read_barrier_counter = -1;
+  int _shader_image_write_barrier_counter = -1;
+  int _texture_read_barrier_counter = -1;
+  int _texture_write_barrier_counter = -1;
+  int _framebuffer_read_barrier_counter = -1;
+  int _framebuffer_write_barrier_counter = -1;
+
 public:
 public:
   static TypeHandle get_class_type() {
   static TypeHandle get_class_type() {
     return _type_handle;
     return _type_handle;

+ 18 - 0
panda/src/glstuff/glmisc_src.cxx

@@ -22,6 +22,11 @@ ConfigVariableBool gl_forward_compatible
    PRC_DESC("Setting this to true will request a forward-compatible OpenGL "
    PRC_DESC("Setting this to true will request a forward-compatible OpenGL "
             "context, which will not support the fixed-function pipeline."));
             "context, which will not support the fixed-function pipeline."));
 
 
+ConfigVariableBool gl_support_dsa
+  ("gl-support-dsa", true,
+   PRC_DESC("Configure this false if you suspect your GL's implementation of "
+            "Direct State Access is broken."));
+
 ConfigVariableBool gl_support_fbo
 ConfigVariableBool gl_support_fbo
   ("gl-support-fbo", true,
   ("gl-support-fbo", true,
    PRC_DESC("Configure this false if your GL's implementation of "
    PRC_DESC("Configure this false if your GL's implementation of "
@@ -335,6 +340,19 @@ ConfigVariableBool gl_depth_zero_to_one
             "range from 0 to 1, matching other graphics APIs.  This setting "
             "range from 0 to 1, matching other graphics APIs.  This setting "
             "requires OpenGL 4.5, or NVIDIA GeForce 8+ hardware."));
             "requires OpenGL 4.5, or NVIDIA GeForce 8+ hardware."));
 
 
+ConfigVariableInt gl_texture_transfer_num_threads
+ ("gl-texture-transfer-num-threads", 2,
+  PRC_DESC("The number of threads that will be started to upload and download "
+           "texture data asynchronously, either via the setup_async_transfer "
+           "interface on the the Texture class or via the async screenshot "
+           "interface."));
+
+ConfigVariableEnum<ThreadPriority> gl_texture_transfer_thread_priority
+ ("gl-texture-transfer-thread-priority", TP_normal,
+  PRC_DESC("The default thread priority to assign to the threads created for "
+           "asynchronous texture transfers.  The default is 'normal'; you may "
+           "also specify 'low', 'high', or 'urgent'."));
+
 extern ConfigVariableBool gl_parallel_arrays;
 extern ConfigVariableBool gl_parallel_arrays;
 
 
 void CLP(init_classes)() {
 void CLP(init_classes)() {

+ 4 - 0
panda/src/glstuff/glmisc_src.h

@@ -17,6 +17,7 @@
 #include "configVariableEnum.h"
 #include "configVariableEnum.h"
 #include "geomEnums.h"
 #include "geomEnums.h"
 #include "coordinateSystem.h"
 #include "coordinateSystem.h"
+#include "threadPriority.h"
 
 
 // Define some macros to transparently map to the double or float versions of
 // Define some macros to transparently map to the double or float versions of
 // the OpenGL function names.
 // the OpenGL function names.
@@ -34,6 +35,7 @@
 
 
 extern EXPCL_GL ConfigVariableInt gl_version;
 extern EXPCL_GL ConfigVariableInt gl_version;
 extern EXPCL_GL ConfigVariableBool gl_forward_compatible;
 extern EXPCL_GL ConfigVariableBool gl_forward_compatible;
+extern ConfigVariableBool gl_support_dsa;
 extern EXPCL_GL ConfigVariableBool gl_support_fbo;
 extern EXPCL_GL ConfigVariableBool gl_support_fbo;
 extern ConfigVariableBool gl_support_spirv;
 extern ConfigVariableBool gl_support_spirv;
 extern ConfigVariableInt gl_force_glsl_version;
 extern ConfigVariableInt gl_force_glsl_version;
@@ -77,6 +79,8 @@ extern ConfigVariableBool gl_support_shadow_filter;
 extern ConfigVariableBool gl_support_vertex_array_bgra;
 extern ConfigVariableBool gl_support_vertex_array_bgra;
 extern ConfigVariableBool gl_force_image_bindings_writeonly;
 extern ConfigVariableBool gl_force_image_bindings_writeonly;
 extern ConfigVariableEnum<CoordinateSystem> gl_coordinate_system;
 extern ConfigVariableEnum<CoordinateSystem> gl_coordinate_system;
+extern ConfigVariableInt gl_texture_transfer_num_threads;
+extern ConfigVariableEnum<ThreadPriority> gl_texture_transfer_thread_priority;
 
 
 extern EXPCL_GL void CLP(init_classes)();
 extern EXPCL_GL void CLP(init_classes)();
 
 

+ 2 - 1
panda/src/glxdisplay/glxGraphicsBuffer.cxx

@@ -166,7 +166,8 @@ open_buffer() {
     // with the old gsg.
     // with the old gsg.
     DCAST_INTO_R(glxgsg, _gsg, false);
     DCAST_INTO_R(glxgsg, _gsg, false);
 
 
-    if (!glxgsg->_context_has_pbuffer ||
+    if (glxgsg->get_engine() != _engine ||
+        !glxgsg->_context_has_pbuffer ||
         !glxgsg->get_fb_properties().subsumes(_fb_properties)) {
         !glxgsg->get_fb_properties().subsumes(_fb_properties)) {
       // We need a new pixel format, and hence a new GSG.
       // We need a new pixel format, and hence a new GSG.
       glxgsg = new glxGraphicsStateGuardian(_engine, _pipe, glxgsg);
       glxgsg = new glxGraphicsStateGuardian(_engine, _pipe, glxgsg);

+ 3 - 0
panda/src/glxdisplay/glxGraphicsPipe.cxx

@@ -130,6 +130,9 @@ make_output(const string &name,
         (flags & (BF_require_parasite | BF_require_window)) != 0) {
         (flags & (BF_require_parasite | BF_require_window)) != 0) {
       return nullptr;
       return nullptr;
     }
     }
+    if (host->get_engine() != engine) {
+      return nullptr;
+    }
     // Early failure - if we are sure that this buffer WONT meet specs, we can
     // Early failure - if we are sure that this buffer WONT meet specs, we can
     // bail out early.
     // bail out early.
     if ((flags & BF_fb_props_optional) == 0) {
     if ((flags & BF_fb_props_optional) == 0) {

+ 2 - 1
panda/src/glxdisplay/glxGraphicsWindow.cxx

@@ -184,7 +184,8 @@ open_window() {
     // If the old gsg has the wrong pixel format, create a new one that shares
     // If the old gsg has the wrong pixel format, create a new one that shares
     // with the old gsg.
     // with the old gsg.
     DCAST_INTO_R(glxgsg, _gsg, false);
     DCAST_INTO_R(glxgsg, _gsg, false);
-    if (!glxgsg->get_fb_properties().subsumes(_fb_properties)) {
+    if (glxgsg->get_engine() != _engine ||
+        !glxgsg->get_fb_properties().subsumes(_fb_properties)) {
       glxgsg = new glxGraphicsStateGuardian(_engine, _pipe, glxgsg);
       glxgsg = new glxGraphicsStateGuardian(_engine, _pipe, glxgsg);
       glxgsg->choose_pixel_format(_fb_properties, glx_pipe->get_display(), glx_pipe->get_screen(), false, false);
       glxgsg->choose_pixel_format(_fb_properties, glx_pipe->get_display(), glx_pipe->get_screen(), false, false);
       _gsg = glxgsg;
       _gsg = glxgsg;

+ 4 - 1
panda/src/gobj/bufferContext.cxx

@@ -12,6 +12,7 @@
  */
  */
 
 
 #include "bufferContext.h"
 #include "bufferContext.h"
+#include "lightMutexHolder.h"
 
 
 TypeHandle BufferContext::_type_handle;
 TypeHandle BufferContext::_type_handle;
 
 
@@ -43,7 +44,8 @@ BufferContext::
 void BufferContext::
 void BufferContext::
 set_owning_chain(BufferContextChain *chain) {
 set_owning_chain(BufferContextChain *chain) {
   if (chain != _owning_chain) {
   if (chain != _owning_chain) {
-    if (_owning_chain != nullptr){
+    if (_owning_chain != nullptr) {
+      LightMutexHolder holder(_owning_chain->_lock);
       --(_owning_chain->_count);
       --(_owning_chain->_count);
       _owning_chain->adjust_bytes(-(int)_data_size_bytes);
       _owning_chain->adjust_bytes(-(int)_data_size_bytes);
       remove_from_list();
       remove_from_list();
@@ -52,6 +54,7 @@ set_owning_chain(BufferContextChain *chain) {
     _owning_chain = chain;
     _owning_chain = chain;
 
 
     if (_owning_chain != nullptr) {
     if (_owning_chain != nullptr) {
+      LightMutexHolder holder(_owning_chain->_lock);
       ++(_owning_chain->_count);
       ++(_owning_chain->_count);
       _owning_chain->adjust_bytes((int)_data_size_bytes);
       _owning_chain->adjust_bytes((int)_data_size_bytes);
       insert_before(_owning_chain);
       insert_before(_owning_chain);

+ 1 - 1
panda/src/gobj/bufferContext.h

@@ -73,7 +73,7 @@ protected:
   TypedWritableReferenceCount *_object;
   TypedWritableReferenceCount *_object;
 
 
 private:
 private:
-  BufferResidencyTracker *_residency;
+  BufferResidencyTracker *const _residency;
   int _residency_state;
   int _residency_state;
 
 
   size_t _data_size_bytes;
   size_t _data_size_bytes;

+ 7 - 0
panda/src/gobj/bufferContextChain.cxx

@@ -14,11 +14,15 @@
 #include "bufferContextChain.h"
 #include "bufferContextChain.h"
 #include "bufferContext.h"
 #include "bufferContext.h"
 #include "indent.h"
 #include "indent.h"
+#include "lightMutexHolder.h"
 
 
 /**
 /**
  * Returns the first BufferContext object stored in the tracker.  You can walk
  * Returns the first BufferContext object stored in the tracker.  You can walk
  * through the entire list of objects stored on the tracker by calling
  * through the entire list of objects stored on the tracker by calling
  * get_next() on each returned object, until the return value is NULL.
  * get_next() on each returned object, until the return value is NULL.
+ *
+ * This does not grab the lock; make sure you are holding the lock while
+ * iterating over the chain.
  */
  */
 BufferContext *BufferContextChain::
 BufferContext *BufferContextChain::
 get_first() {
 get_first() {
@@ -32,9 +36,11 @@ get_first() {
 
 
 /**
 /**
  * Moves all of the BufferContexts from the other tracker onto this one.
  * Moves all of the BufferContexts from the other tracker onto this one.
+ * The other chain must be locked.
  */
  */
 void BufferContextChain::
 void BufferContextChain::
 take_from(BufferContextChain &other) {
 take_from(BufferContextChain &other) {
+  LightMutexHolder holder(_lock);
   _total_size += other._total_size;
   _total_size += other._total_size;
   _count += other._count;
   _count += other._count;
   other._total_size = 0;
   other._total_size = 0;
@@ -55,6 +61,7 @@ take_from(BufferContextChain &other) {
  */
  */
 void BufferContextChain::
 void BufferContextChain::
 write(std::ostream &out, int indent_level) const {
 write(std::ostream &out, int indent_level) const {
+  LightMutexHolder holder(_lock);
   indent(out, indent_level)
   indent(out, indent_level)
     << _count << " objects, consuming " << _total_size << " bytes:\n";
     << _count << " objects, consuming " << _total_size << " bytes:\n";
 
 

+ 4 - 0
panda/src/gobj/bufferContextChain.h

@@ -16,6 +16,7 @@
 
 
 #include "pandabase.h"
 #include "pandabase.h"
 #include "linkedListNode.h"
 #include "linkedListNode.h"
+#include "lightMutex.h"
 
 
 class BufferContext;
 class BufferContext;
 
 
@@ -47,6 +48,9 @@ private:
   size_t _total_size;
   size_t _total_size;
   int _count;
   int _count;
 
 
+public:
+  LightMutex _lock;
+
   friend class BufferContext;
   friend class BufferContext;
 };
 };
 
 

+ 1 - 0
panda/src/gobj/bufferResidencyTracker.cxx

@@ -117,6 +117,7 @@ write(std::ostream &out, int indent_level) const {
  */
  */
 void BufferResidencyTracker::
 void BufferResidencyTracker::
 move_inactive(BufferContextChain &inactive, BufferContextChain &active) {
 move_inactive(BufferContextChain &inactive, BufferContextChain &active) {
+  LightMutexHolder active_holder(active._lock);
   BufferContext *node = active.get_first();
   BufferContext *node = active.get_first();
   while (node != nullptr) {
   while (node != nullptr) {
     nassertv((node->_residency_state & S_active) != 0);
     nassertv((node->_residency_state & S_active) != 0);

+ 18 - 3
panda/src/gobj/preparedGraphicsObjects.cxx

@@ -1515,9 +1515,24 @@ begin_frame(GraphicsStateGuardianBase *gsg, Thread *current_thread) {
     Texture *tex = qti->first;
     Texture *tex = qti->first;
     TextureContext *tc = tex->prepare_now(this, gsg);
     TextureContext *tc = tex->prepare_now(this, gsg);
     if (tc != nullptr) {
     if (tc != nullptr) {
-      gsg->update_texture(tc, true);
-      if (qti->second != nullptr) {
-        qti->second->set_result(tc);
+      if (tex->get_num_async_transfer_buffers() == 0) {
+        gsg->update_texture(tc, true);
+        if (qti->second != nullptr) {
+          qti->second->set_result(tc);
+        }
+      } else {
+        // Async update
+        CompletionToken token;
+        if (qti->second != nullptr) {
+          token = [tc, fut = std::move(qti->second)] (bool success) {
+            if (success) {
+              fut->set_result(tc);
+            } else {
+              fut->notify_removed();
+            }
+          };
+        }
+        gsg->update_texture(tc, false, std::move(token));
       }
       }
     }
     }
   }
   }

+ 9 - 1
panda/src/gobj/texture.I

@@ -278,7 +278,7 @@ set_clear_color(const LColor &color) {
 INLINE void Texture::
 INLINE void Texture::
 clear_clear_color() {
 clear_clear_color() {
   CDWriter cdata(_cycler, true);
   CDWriter cdata(_cycler, true);
-  cdata->_has_clear_color = true;
+  cdata->_has_clear_color = false;
 }
 }
 
 
 /**
 /**
@@ -2139,6 +2139,14 @@ rescale_texture() {
   return do_rescale_texture(cdata);
   return do_rescale_texture(cdata);
 }
 }
 
 
+/**
+ * Returns the number previously passed to setup_async_transfer().
+ */
+INLINE int Texture::
+get_num_async_transfer_buffers() const {
+  return _num_async_transfer_buffers.load(std::memory_order_relaxed);
+}
+
 /**
 /**
  * Works like adjust_size, but also considers the texture class.  Movie
  * Works like adjust_size, but also considers the texture class.  Movie
  * textures, for instance, always pad outwards, regardless of textures-
  * textures, for instance, always pad outwards, regardless of textures-

+ 38 - 2
panda/src/gobj/texture.cxx

@@ -1570,6 +1570,27 @@ get_view_modified_pages(UpdateSeq since, int view, int n) const {
   return result;
   return result;
 }
 }
 
 
+/**
+ * Sets the number of buffers for asynchronous upload of texture data.  If this
+ * number is higher than 0, future texture uploads will occur in the background,
+ * up to the provided amount at a time.  The asynchronous upload will be
+ * triggered by calls to prepare() or when the texture comes into view and
+ * allow-incomplete-render is true.
+ *
+ * Each buffer is only large enough to contain a single view, so you may wish
+ * to create twice as many buffers if you want to update twice as many views.
+ *
+ * You can also pass the special value -1, which means to create as many
+ * buffers as is necessary for all asynchronous uploads to take place, and they
+ * will be deleted afterwards automatically.
+ *
+ * This setting will take effect immediately.
+ */
+void Texture::
+setup_async_transfer(int num_buffers) {
+  _num_async_transfer_buffers.store(num_buffers);
+}
+
 /**
 /**
  * Indicates that the texture should be enqueued to be prepared in the
  * Indicates that the texture should be enqueued to be prepared in the
  * indicated prepared_objects at the beginning of the next frame.  This will
  * indicated prepared_objects at the beginning of the next frame.  This will
@@ -5704,7 +5725,14 @@ do_modify_ram_image(CData *cdata) {
   } else {
   } else {
     do_clear_ram_mipmap_images(cdata);
     do_clear_ram_mipmap_images(cdata);
   }
   }
-  return cdata->_ram_images[0]._image;
+  PTA_uchar data = cdata->_ram_images[0]._image;
+  if (data.get_node_ref_count() > 0) {
+    // Copy on write, if an upload thread is reading this now.
+    PTA_uchar new_data = PTA_uchar::empty_array(0);
+    new_data.v() = data.v();
+    data.swap(new_data);
+  }
+  return data;
 }
 }
 
 
 /**
 /**
@@ -5779,7 +5807,15 @@ do_modify_ram_mipmap_image(CData *cdata, int n) {
       cdata->_ram_images[n]._image.empty()) {
       cdata->_ram_images[n]._image.empty()) {
     do_make_ram_mipmap_image(cdata, n);
     do_make_ram_mipmap_image(cdata, n);
   }
   }
-  return cdata->_ram_images[n]._image;
+
+  PTA_uchar data = cdata->_ram_images[n]._image;
+  if (data.get_node_ref_count() > 0) {
+    // Copy on write, if an upload thread is reading this now.
+    PTA_uchar new_data = PTA_uchar::empty_array(0);
+    new_data.v() = data.v();
+    data.swap(new_data);
+  }
+  return data;
 }
 }
 
 
 /**
 /**

+ 6 - 0
panda/src/gobj/texture.h

@@ -47,6 +47,7 @@
 #include "pfmFile.h"
 #include "pfmFile.h"
 #include "asyncTask.h"
 #include "asyncTask.h"
 #include "extension.h"
 #include "extension.h"
+#include "patomic.h"
 
 
 class TextureContext;
 class TextureContext;
 class FactoryParams;
 class FactoryParams;
@@ -536,6 +537,8 @@ PUBLISHED:
   MAKE_PROPERTY(auto_texture_scale, get_auto_texture_scale,
   MAKE_PROPERTY(auto_texture_scale, get_auto_texture_scale,
                                     set_auto_texture_scale);
                                     set_auto_texture_scale);
 
 
+  void setup_async_transfer(int num_buffers);
+
   PT(AsyncFuture) prepare(PreparedGraphicsObjects *prepared_objects);
   PT(AsyncFuture) prepare(PreparedGraphicsObjects *prepared_objects);
   bool is_prepared(PreparedGraphicsObjects *prepared_objects) const;
   bool is_prepared(PreparedGraphicsObjects *prepared_objects) const;
   bool was_image_modified(PreparedGraphicsObjects *prepared_objects) const;
   bool was_image_modified(PreparedGraphicsObjects *prepared_objects) const;
@@ -628,6 +631,7 @@ PUBLISHED:
 
 
 public:
 public:
   void texture_uploaded();
   void texture_uploaded();
+  INLINE int get_num_async_transfer_buffers() const;
 
 
   virtual bool has_cull_callback() const;
   virtual bool has_cull_callback() const;
   virtual bool cull_callback(CullTraverser *trav, const CullTraverserData &data) const;
   virtual bool cull_callback(CullTraverser *trav, const CullTraverserData &data) const;
@@ -1072,6 +1076,8 @@ protected:
   typedef pmap<PreparedGraphicsObjects *, TextureContext *> Contexts;
   typedef pmap<PreparedGraphicsObjects *, TextureContext *> Contexts;
   Contexts _contexts;
   Contexts _contexts;
 
 
+  patomic_signed_lock_free _num_async_transfer_buffers { 0 };
+
   // It is common, when using normal maps, specular maps, gloss maps, and
   // It is common, when using normal maps, specular maps, gloss maps, and
   // such, to use a file naming convention where the filenames of the special
   // such, to use a file naming convention where the filenames of the special
   // maps are derived by concatenating a suffix to the name of the diffuse
   // maps are derived by concatenating a suffix to the name of the diffuse

+ 2 - 0
panda/src/gsgbase/graphicsStateGuardianBase.h

@@ -22,6 +22,7 @@
 #include "lightMutex.h"
 #include "lightMutex.h"
 #include "patomic.h"
 #include "patomic.h"
 #include "small_vector.h"
 #include "small_vector.h"
+#include "completionToken.h"
 
 
 // A handful of forward references.
 // A handful of forward references.
 
 
@@ -150,6 +151,7 @@ public:
 
 
   virtual TextureContext *prepare_texture(Texture *tex)=0;
   virtual TextureContext *prepare_texture(Texture *tex)=0;
   virtual bool update_texture(TextureContext *tc, bool force)=0;
   virtual bool update_texture(TextureContext *tc, bool force)=0;
+  virtual bool update_texture(TextureContext *tc, bool force, CompletionToken token)=0;
   virtual void release_texture(TextureContext *tc)=0;
   virtual void release_texture(TextureContext *tc)=0;
   virtual void release_textures(const pvector<TextureContext *> &contexts)=0;
   virtual void release_textures(const pvector<TextureContext *> &contexts)=0;
   virtual bool extract_texture_data(Texture *tex)=0;
   virtual bool extract_texture_data(Texture *tex)=0;

+ 133 - 127
panda/src/linmath/compose_matrix_src.cxx

@@ -323,79 +323,82 @@ unwind_yup_rotation(FLOATNAME(LMatrix3) &mat, FLOATNAME(LVecBase3) &hpr) {
   mat.get_row(z,2);
   mat.get_row(z,2);
 
 
   // Project Z into the XZ plane.
   // Project Z into the XZ plane.
+  FLOATTYPE heading = 0;
   FLOATNAME(LVector2) xz(z[0], z[2]);
   FLOATNAME(LVector2) xz(z[0], z[2]);
-  xz = normalize(xz);
-
-  // Compute the rotation about the +Y (up) axis.  This is yaw, or "heading".
-  FLOATTYPE heading = catan2(xz[0], xz[1]);
-
-  // Unwind the heading, and continue.
-  FLOATNAME(LMatrix3) rot_y;
-  rot_y._m(0, 0) = xz[1];
-  rot_y._m(0, 1) = 0;
-  rot_y._m(0, 2) = xz[0];
-
-  rot_y._m(1, 0) = 0;
-  rot_y._m(1, 1) = 1;
-  rot_y._m(1, 2) = 0;
-
-  rot_y._m(2, 0) = -xz[0];
-  rot_y._m(2, 1) = 0;
-  rot_y._m(2, 2) = xz[1];
-
-  x = x * rot_y;
-  y = y * rot_y;
-  z = z * rot_y;
+  if (xz.normalize()) {
+    // Compute the rotation about the +Y (up) axis.  This is yaw, or "heading".
+    heading = catan2(xz[0], xz[1]);
+
+    // Unwind the heading, and continue.
+    FLOATNAME(LMatrix3) rot_y;
+    rot_y._m(0, 0) = xz[1];
+    rot_y._m(0, 1) = 0;
+    rot_y._m(0, 2) = xz[0];
+
+    rot_y._m(1, 0) = 0;
+    rot_y._m(1, 1) = 1;
+    rot_y._m(1, 2) = 0;
+
+    rot_y._m(2, 0) = -xz[0];
+    rot_y._m(2, 1) = 0;
+    rot_y._m(2, 2) = xz[1];
+
+    x = x * rot_y;
+    y = y * rot_y;
+    z = z * rot_y;
+  }
 
 
   // Project the rotated Z into the YZ plane.
   // Project the rotated Z into the YZ plane.
+  FLOATTYPE pitch = 0;
   FLOATNAME(LVector2) yz(z[1], z[2]);
   FLOATNAME(LVector2) yz(z[1], z[2]);
-  yz = normalize(yz);
-
-  // Compute the rotation about the +X (right) axis.  This is pitch.
-  FLOATTYPE pitch = -catan2(yz[0], yz[1]);
-
-  // Unwind the pitch.
-  FLOATNAME(LMatrix3) rot_x;
-  rot_x._m(0, 0) = 1;
-  rot_x._m(0, 1) = 0;
-  rot_x._m(0, 2) = 0;
-
-  rot_x._m(1, 0) = 0;
-  rot_x._m(1, 1) = yz[1];
-  rot_x._m(1, 2) = yz[0];
-
-  rot_x._m(2, 0) = 0;
-  rot_x._m(2, 1) = -yz[0];
-  rot_x._m(2, 2) = yz[1];
-
-  x = x * rot_x;
-  y = y * rot_x;
-  z = z * rot_x;
+  if (yz.normalize()) {
+    // Compute the rotation about the +X (right) axis.  This is pitch.
+    pitch = -catan2(yz[0], yz[1]);
+
+    // Unwind the pitch.
+    FLOATNAME(LMatrix3) rot_x;
+    rot_x._m(0, 0) = 1;
+    rot_x._m(0, 1) = 0;
+    rot_x._m(0, 2) = 0;
+
+    rot_x._m(1, 0) = 0;
+    rot_x._m(1, 1) = yz[1];
+    rot_x._m(1, 2) = yz[0];
+
+    rot_x._m(2, 0) = 0;
+    rot_x._m(2, 1) = -yz[0];
+    rot_x._m(2, 2) = yz[1];
+
+    x = x * rot_x;
+    y = y * rot_x;
+    z = z * rot_x;
+  }
 
 
   // Project the rotated X onto the XY plane.
   // Project the rotated X onto the XY plane.
+  FLOATTYPE roll = 0;
   FLOATNAME(LVector2) xy(x[0], x[1]);
   FLOATNAME(LVector2) xy(x[0], x[1]);
-  xy = normalize(xy);
-
-  // Compute the rotation about the +Z (back) axis.  This is roll.
-  FLOATTYPE roll = -catan2(xy[1], xy[0]);
-
-  // Unwind the roll from the axes, and continue.
-  FLOATNAME(LMatrix3) rot_z;
-  rot_z._m(0, 0) = xy[0];
-  rot_z._m(0, 1) = -xy[1];
-  rot_z._m(0, 2) = 0;
-
-  rot_z._m(1, 0) = xy[1];
-  rot_z._m(1, 1) = xy[0];
-  rot_z._m(1, 2) = 0;
-
-  rot_z._m(2, 0) = 0;
-  rot_z._m(2, 1) = 0;
-  rot_z._m(2, 2) = 1;
-
-  x = x * rot_z;
-  y = y * rot_z;
-  z = z * rot_z;
+  if (xy.normalize()) {
+    // Compute the rotation about the +Z (back) axis.  This is roll.
+    roll = -catan2(xy[1], xy[0]);
+
+    // Unwind the roll from the axes, and continue.
+    FLOATNAME(LMatrix3) rot_z;
+    rot_z._m(0, 0) = xy[0];
+    rot_z._m(0, 1) = -xy[1];
+    rot_z._m(0, 2) = 0;
+
+    rot_z._m(1, 0) = xy[1];
+    rot_z._m(1, 1) = xy[0];
+    rot_z._m(1, 2) = 0;
+
+    rot_z._m(2, 0) = 0;
+    rot_z._m(2, 1) = 0;
+    rot_z._m(2, 2) = 1;
+
+    x = x * rot_z;
+    y = y * rot_z;
+    z = z * rot_z;
+  }
 
 
   // Reset the matrix to reflect the unwinding.
   // Reset the matrix to reflect the unwinding.
   mat.set_row(0, x);
   mat.set_row(0, x);
@@ -425,79 +428,82 @@ unwind_zup_rotation(FLOATNAME(LMatrix3) &mat, FLOATNAME(LVecBase3) &hpr) {
   mat.get_row(z,2);
   mat.get_row(z,2);
 
 
   // Project Y into the XY plane.
   // Project Y into the XY plane.
+  FLOATTYPE heading = 0;
   FLOATNAME(LVector2) xy(y[0], y[1]);
   FLOATNAME(LVector2) xy(y[0], y[1]);
-  xy = normalize(xy);
-
-  // Compute the rotation about the +Z (up) axis.  This is yaw, or "heading".
-  FLOATTYPE heading = -catan2(xy[0], xy[1]);
-
-  // Unwind the heading, and continue.
-  FLOATNAME(LMatrix3) rot_z;
-  rot_z._m(0, 0) = xy[1];
-  rot_z._m(0, 1) = xy[0];
-  rot_z._m(0, 2) = 0;
-
-  rot_z._m(1, 0) = -xy[0];
-  rot_z._m(1, 1) = xy[1];
-  rot_z._m(1, 2) = 0;
-
-  rot_z._m(2, 0) = 0;
-  rot_z._m(2, 1) = 0;
-  rot_z._m(2, 2) = 1;
-
-  x = x * rot_z;
-  y = y * rot_z;
-  z = z * rot_z;
+  if (xy.normalize()) {
+    // Compute the rotation about the +Z (up) axis.  This is yaw, or "heading".
+    heading = -catan2(xy[0], xy[1]);
+
+    // Unwind the heading, and continue.
+    FLOATNAME(LMatrix3) rot_z;
+    rot_z._m(0, 0) = xy[1];
+    rot_z._m(0, 1) = xy[0];
+    rot_z._m(0, 2) = 0;
+
+    rot_z._m(1, 0) = -xy[0];
+    rot_z._m(1, 1) = xy[1];
+    rot_z._m(1, 2) = 0;
+
+    rot_z._m(2, 0) = 0;
+    rot_z._m(2, 1) = 0;
+    rot_z._m(2, 2) = 1;
+
+    x = x * rot_z;
+    y = y * rot_z;
+    z = z * rot_z;
+  }
 
 
   // Project the rotated Y into the YZ plane.
   // Project the rotated Y into the YZ plane.
+  FLOATTYPE pitch = 0;
   FLOATNAME(LVector2) yz(y[1], y[2]);
   FLOATNAME(LVector2) yz(y[1], y[2]);
-  yz = normalize(yz);
-
-  // Compute the rotation about the +X (right) axis.  This is pitch.
-  FLOATTYPE pitch = catan2(yz[1], yz[0]);
-
-  // Unwind the pitch.
-  FLOATNAME(LMatrix3) rot_x;
-  rot_x._m(0, 0) = 1;
-  rot_x._m(0, 1) = 0;
-  rot_x._m(0, 2) = 0;
-
-  rot_x._m(1, 0) = 0;
-  rot_x._m(1, 1) = yz[0];
-  rot_x._m(1, 2) = -yz[1];
-
-  rot_x._m(2, 0) = 0;
-  rot_x._m(2, 1) = yz[1];
-  rot_x._m(2, 2) = yz[0];
-
-  x = x * rot_x;
-  y = y * rot_x;
-  z = z * rot_x;
+  if (yz.normalize()) {
+    // Compute the rotation about the +X (right) axis.  This is pitch.
+    pitch = catan2(yz[1], yz[0]);
+
+    // Unwind the pitch.
+    FLOATNAME(LMatrix3) rot_x;
+    rot_x._m(0, 0) = 1;
+    rot_x._m(0, 1) = 0;
+    rot_x._m(0, 2) = 0;
+
+    rot_x._m(1, 0) = 0;
+    rot_x._m(1, 1) = yz[0];
+    rot_x._m(1, 2) = -yz[1];
+
+    rot_x._m(2, 0) = 0;
+    rot_x._m(2, 1) = yz[1];
+    rot_x._m(2, 2) = yz[0];
+
+    x = x * rot_x;
+    y = y * rot_x;
+    z = z * rot_x;
+  }
 
 
   // Project X into the XZ plane.
   // Project X into the XZ plane.
+  FLOATTYPE roll = 0;
   FLOATNAME(LVector2) xz(x[0], x[2]);
   FLOATNAME(LVector2) xz(x[0], x[2]);
-  xz = normalize(xz);
-
+  if (xz.normalize()) {
   // Compute the rotation about the -Y (back) axis.  This is roll.
   // Compute the rotation about the -Y (back) axis.  This is roll.
-  FLOATTYPE roll = -catan2(xz[1], xz[0]);
+    roll = -catan2(xz[1], xz[0]);
 
 
-  // Unwind the roll from the axes, and continue.
-  FLOATNAME(LMatrix3) rot_y;
-  rot_y._m(0, 0) = xz[0];
-  rot_y._m(0, 1) = 0;
-  rot_y._m(0, 2) = -xz[1];
+    // Unwind the roll from the axes, and continue.
+    FLOATNAME(LMatrix3) rot_y;
+    rot_y._m(0, 0) = xz[0];
+    rot_y._m(0, 1) = 0;
+    rot_y._m(0, 2) = -xz[1];
 
 
-  rot_y._m(1, 0) = 0;
-  rot_y._m(1, 1) = 1;
-  rot_y._m(1, 2) = 0;
+    rot_y._m(1, 0) = 0;
+    rot_y._m(1, 1) = 1;
+    rot_y._m(1, 2) = 0;
 
 
-  rot_y._m(2, 0) = xz[1];
-  rot_y._m(2, 1) = 0;
-  rot_y._m(2, 2) = xz[0];
+    rot_y._m(2, 0) = xz[1];
+    rot_y._m(2, 1) = 0;
+    rot_y._m(2, 2) = xz[0];
 
 
-  x = x * rot_y;
-  y = y * rot_y;
-  z = z * rot_y;
+    x = x * rot_y;
+    y = y * rot_y;
+    z = z * rot_y;
+  }
 
 
   // Reset the matrix to reflect the unwinding.
   // Reset the matrix to reflect the unwinding.
   mat.set_row(0, x);
   mat.set_row(0, x);

+ 11 - 7
panda/src/pgraph/loader.cxx

@@ -392,13 +392,17 @@ try_load_file(const Filename &pathname, const LoaderOptions &options,
     sgr.premunge(result, RenderState::make_empty());
     sgr.premunge(result, RenderState::make_empty());
   }
   }
 
 
-  if (allow_ram_cache && result->is_of_type(ModelRoot::get_class_type())) {
-    // Store the loaded model in the RAM cache, and make sure we return a
-    // copy so that this node can be modified independently from the RAM
-    // cached version.
-    ModelPool::add_model(pathname, DCAST(ModelRoot, result.p()));
-    if ((options.get_flags() & LoaderOptions::LF_allow_instance) == 0) {
-      result = NodePath(result).copy_to(NodePath()).node();
+  if (result->is_of_type(ModelRoot::get_class_type())) {
+    ((ModelRoot *)result.p())->set_fullpath(pathname);
+
+    if (allow_ram_cache) {
+      // Store the loaded model in the RAM cache, and make sure we return a
+      // copy so that this node can be modified independently from the RAM
+      // cached version.
+      ModelPool::add_model(pathname, DCAST(ModelRoot, result.p()));
+      if ((options.get_flags() & LoaderOptions::LF_allow_instance) == 0) {
+        result = NodePath(result).copy_to(NodePath()).node();
+      }
     }
     }
   }
   }
 
 

+ 2 - 2
panda/src/pgraph/modelPool.I

@@ -42,7 +42,7 @@ verify_model(const Filename &filename) {
  * date (and hasn't been modified in the meantime), and if not, will still
  * date (and hasn't been modified in the meantime), and if not, will still
  * return NULL.
  * return NULL.
  */
  */
-INLINE ModelRoot *ModelPool::
+INLINE PT(ModelRoot) ModelPool::
 get_model(const Filename &filename, bool verify) {
 get_model(const Filename &filename, bool verify) {
   return get_ptr()->ns_get_model(filename, verify);
   return get_ptr()->ns_get_model(filename, verify);
 }
 }
@@ -54,7 +54,7 @@ get_model(const Filename &filename, bool verify) {
  * is true and the file has recently changed).  If the model file cannot be
  * is true and the file has recently changed).  If the model file cannot be
  * found, or cannot be loaded for some reason, returns NULL.
  * found, or cannot be loaded for some reason, returns NULL.
  */
  */
-INLINE ModelRoot *ModelPool::
+INLINE PT(ModelRoot) ModelPool::
 load_model(const Filename &filename, const LoaderOptions &options) {
 load_model(const Filename &filename, const LoaderOptions &options) {
   return get_ptr()->ns_load_model(filename, options);
   return get_ptr()->ns_load_model(filename, options);
 }
 }

+ 6 - 29
panda/src/pgraph/modelPool.cxx

@@ -48,7 +48,7 @@ ns_has_model(const Filename &filename) {
 /**
 /**
  * The nonstatic implementation of get_model().
  * The nonstatic implementation of get_model().
  */
  */
-ModelRoot *ModelPool::
+PT(ModelRoot) ModelPool::
 ns_get_model(const Filename &filename, bool verify) {
 ns_get_model(const Filename &filename, bool verify) {
 
 
   PT(ModelRoot) cached_model;
   PT(ModelRoot) cached_model;
@@ -116,54 +116,31 @@ ns_get_model(const Filename &filename, bool verify) {
 /**
 /**
  * The nonstatic implementation of load_model().
  * The nonstatic implementation of load_model().
  */
  */
-ModelRoot *ModelPool::
+PT(ModelRoot) ModelPool::
 ns_load_model(const Filename &filename, const LoaderOptions &options) {
 ns_load_model(const Filename &filename, const LoaderOptions &options) {
-
-  // First check if it has already been loaded and is still current.
+  // First check if it's been cached under the given filename (for backward
+  // compatibility reasons)
   PT(ModelRoot) cached_model = ns_get_model(filename, true);
   PT(ModelRoot) cached_model = ns_get_model(filename, true);
   if (cached_model != nullptr) {
   if (cached_model != nullptr) {
     return cached_model;
     return cached_model;
   }
   }
 
 
-  // Look on disk for the current file.
   LoaderOptions new_options(options);
   LoaderOptions new_options(options);
-  new_options.set_flags((new_options.get_flags() | LoaderOptions::LF_no_ram_cache) &
-                        ~LoaderOptions::LF_search);
+  new_options.set_flags(new_options.get_flags() & ~LoaderOptions::LF_no_ram_cache);
 
 
   Loader *model_loader = Loader::get_global_ptr();
   Loader *model_loader = Loader::get_global_ptr();
   PT(PandaNode) panda_node = model_loader->load_sync(filename, new_options);
   PT(PandaNode) panda_node = model_loader->load_sync(filename, new_options);
   PT(ModelRoot) node;
   PT(ModelRoot) node;
 
 
-  if (panda_node.is_null()) {
-    // This model was not found.
-
-  } else {
+  if (!panda_node.is_null()) {
     if (panda_node->is_of_type(ModelRoot::get_class_type())) {
     if (panda_node->is_of_type(ModelRoot::get_class_type())) {
       node = DCAST(ModelRoot, panda_node);
       node = DCAST(ModelRoot, panda_node);
-
     } else {
     } else {
       // We have to construct a ModelRoot node to put it under.
       // We have to construct a ModelRoot node to put it under.
       node = new ModelRoot(filename);
       node = new ModelRoot(filename);
       node->add_child(panda_node);
       node->add_child(panda_node);
     }
     }
-    node->set_fullpath(filename);
-  }
-
-  {
-    LightMutexHolder holder(_lock);
-
-    // Look again, in case someone has just loaded the model in another
-    // thread.
-    Models::const_iterator ti;
-    ti = _models.find(filename);
-    if (ti != _models.end() && (*ti).second != cached_model) {
-      // This model was previously loaded.
-      return (*ti).second;
-    }
-
-    _models[filename] = node;
   }
   }
-
   return node;
   return node;
 }
 }
 
 

+ 6 - 6
panda/src/pgraph/modelPool.h

@@ -43,9 +43,9 @@ class EXPCL_PANDA_PGRAPH ModelPool {
 PUBLISHED:
 PUBLISHED:
   INLINE static bool has_model(const Filename &filename);
   INLINE static bool has_model(const Filename &filename);
   INLINE static bool verify_model(const Filename &filename);
   INLINE static bool verify_model(const Filename &filename);
-  INLINE static ModelRoot *get_model(const Filename &filename, bool verify);
-  BLOCKING INLINE static ModelRoot *load_model(const Filename &filename,
-                                               const LoaderOptions &options = LoaderOptions());
+  INLINE static PT(ModelRoot) get_model(const Filename &filename, bool verify);
+  BLOCKING INLINE static PT(ModelRoot) load_model(const Filename &filename,
+                                                  const LoaderOptions &options = LoaderOptions());
 
 
   INLINE static void add_model(const Filename &filename, ModelRoot *model);
   INLINE static void add_model(const Filename &filename, ModelRoot *model);
   INLINE static void release_model(const Filename &filename);
   INLINE static void release_model(const Filename &filename);
@@ -65,9 +65,9 @@ private:
   INLINE ModelPool();
   INLINE ModelPool();
 
 
   bool ns_has_model(const Filename &filename);
   bool ns_has_model(const Filename &filename);
-  ModelRoot *ns_get_model(const Filename &filename, bool verify);
-  ModelRoot *ns_load_model(const Filename &filename,
-                           const LoaderOptions &options);
+  PT(ModelRoot) ns_get_model(const Filename &filename, bool verify);
+  PT(ModelRoot) ns_load_model(const Filename &filename,
+                              const LoaderOptions &options);
   void ns_add_model(const Filename &filename, ModelRoot *model);
   void ns_add_model(const Filename &filename, ModelRoot *model);
   void ns_release_model(const Filename &filename);
   void ns_release_model(const Filename &filename);
 
 

+ 2 - 0
panda/src/pgraph/nodePath.h

@@ -884,6 +884,8 @@ PUBLISHED:
   INLINE void set_collide_mask(CollideMask new_mask, CollideMask bits_to_change = CollideMask::all_on(),
   INLINE void set_collide_mask(CollideMask new_mask, CollideMask bits_to_change = CollideMask::all_on(),
                                TypeHandle node_type = TypeHandle::none());
                                TypeHandle node_type = TypeHandle::none());
 
 
+  EXTENSION(void set_collide_owner(PyObject *owner));
+
   // Comparison methods
   // Comparison methods
   INLINE bool operator == (const NodePath &other) const;
   INLINE bool operator == (const NodePath &other) const;
   INLINE bool operator != (const NodePath &other) const;
   INLINE bool operator != (const NodePath &other) const;

+ 59 - 0
panda/src/pgraph/nodePath_ext.cxx

@@ -15,6 +15,7 @@
 #include "typedWritable_ext.h"
 #include "typedWritable_ext.h"
 #include "shaderInput_ext.h"
 #include "shaderInput_ext.h"
 #include "shaderAttrib.h"
 #include "shaderAttrib.h"
+#include "collisionNode.h"
 
 
 #ifdef HAVE_PYTHON
 #ifdef HAVE_PYTHON
 
 
@@ -327,4 +328,62 @@ get_tight_bounds(const NodePath &other) const {
   }
   }
 }
 }
 
 
+/**
+ * Recursively assigns a weak reference to the given owner object to all
+ * collision nodes at this level and below.
+ *
+ * You may pass in None to clear all owners below this level.
+ *
+ * Note that there is no corresponding get_collide_owner(), since there may be
+ * multiple nodes below this level with different owners.
+ */
+void Extension<NodePath>::
+set_collide_owner(PyObject *owner) {
+  if (owner != Py_None) {
+    PyObject *ref = PyWeakref_NewRef(owner, nullptr);
+    if (ref != nullptr) {
+      r_set_collide_owner(_this->node(), ref);
+      Py_DECREF(ref);
+    }
+  } else {
+    r_clear_collide_owner(_this->node());
+  }
+}
+
+/**
+ * Recursive implementation of set_collide_owner.  weakref must be a weak ref
+ * object.
+ */
+void Extension<NodePath>::
+r_set_collide_owner(PandaNode *node, PyObject *weakref) {
+  if (node->is_collision_node()) {
+    CollisionNode *cnode = (CollisionNode *)node;
+    cnode->set_owner(Py_NewRef(weakref),
+                     [](void *obj) { Py_DECREF((PyObject *)obj); });
+  }
+
+  PandaNode::Children cr = node->get_children();
+  int num_children = cr.get_num_children();
+  for (int i = 0; i < num_children; i++) {
+    r_set_collide_owner(cr.get_child(i), weakref);
+  }
+}
+
+/**
+ * Recursive implementation of set_collide_owner(None).
+ */
+void Extension<NodePath>::
+r_clear_collide_owner(PandaNode *node) {
+  if (node->is_collision_node()) {
+    CollisionNode *cnode = (CollisionNode *)node;
+    cnode->clear_owner();
+  }
+
+  PandaNode::Children cr = node->get_children();
+  int num_children = cr.get_num_children();
+  for (int i = 0; i < num_children; i++) {
+    r_clear_collide_owner(cr.get_child(i));
+  }
+}
+
 #endif  // HAVE_PYTHON
 #endif  // HAVE_PYTHON

+ 6 - 0
panda/src/pgraph/nodePath_ext.h

@@ -54,6 +54,12 @@ public:
   void set_shader_inputs(PyObject *args, PyObject *kwargs);
   void set_shader_inputs(PyObject *args, PyObject *kwargs);
 
 
   PyObject *get_tight_bounds(const NodePath &other = NodePath()) const;
   PyObject *get_tight_bounds(const NodePath &other = NodePath()) const;
+
+  void set_collide_owner(PyObject *owner);
+
+private:
+  static void r_set_collide_owner(PandaNode *node, PyObject *weakref);
+  static void r_clear_collide_owner(PandaNode *node);
 };
 };
 
 
 BEGIN_PUBLISH
 BEGIN_PUBLISH

+ 8 - 3
panda/src/pgraphnodes/lightLensNode.cxx

@@ -19,6 +19,7 @@
 #include "renderState.h"
 #include "renderState.h"
 #include "cullFaceAttrib.h"
 #include "cullFaceAttrib.h"
 #include "colorWriteAttrib.h"
 #include "colorWriteAttrib.h"
+#include "lightAttrib.h"
 #include "graphicsStateGuardianBase.h"
 #include "graphicsStateGuardianBase.h"
 
 
 TypeHandle LightLensNode::_type_handle;
 TypeHandle LightLensNode::_type_handle;
@@ -37,10 +38,14 @@ LightLensNode(const std::string &name, Lens *lens) :
   _shadow_caster = false;
   _shadow_caster = false;
   _sb_size.set(512, 512);
   _sb_size.set(512, 512);
   _sb_sort = -10;
   _sb_sort = -10;
-  // set_initial_state(RenderState::make(ShaderAttrib::make_off(), 1000));
+
   // Backface culling helps eliminating artifacts.
   // Backface culling helps eliminating artifacts.
-  set_initial_state(RenderState::make(CullFaceAttrib::make_reverse(),
-                    ColorWriteAttrib::make(ColorWriteAttrib::C_off)));
+  static CPT(RenderState) default_initial_state =
+    RenderState::make(
+      CullFaceAttrib::make_reverse(),
+      ColorWriteAttrib::make(ColorWriteAttrib::C_off)
+    )->set_attrib(LightAttrib::make_all_off(), RenderState::get_max_priority());
+  set_initial_state(default_initial_state);
 }
 }
 
 
 /**
 /**

+ 1 - 1
panda/src/pipeline/threadPosixImpl.I

@@ -19,7 +19,7 @@ ThreadPosixImpl(Thread *parent_obj) :
   _parent_obj(parent_obj)
   _parent_obj(parent_obj)
 {
 {
   _joinable = false;
   _joinable = false;
-  _detached = false;
+  _detached = true;
   _status = S_new;
   _status = S_new;
 #ifdef ANDROID
 #ifdef ANDROID
   _jni_env = nullptr;
   _jni_env = nullptr;

+ 10 - 15
panda/src/pstatclient/pStatClientImpl.cxx

@@ -155,10 +155,9 @@ client_connect(std::string hostname, int port) {
       int thread_index = current_thread->get_pstats_index();
       int thread_index = current_thread->get_pstats_index();
       if (thread_index >= 0) {
       if (thread_index >= 0) {
         PStatClient *client = PStatClient::get_global_pstats();
         PStatClient *client = PStatClient::get_global_pstats();
-        double start = client->get_real_time();
+        client->start(_wait_sleep_pcollector.get_index(), thread_index);
         ThreadImpl::sleep(seconds);
         ThreadImpl::sleep(seconds);
-        double stop = client->get_real_time();
-        client->start_stop(_wait_sleep_pcollector.get_index(), thread_index, start, stop);
+        client->stop(_wait_sleep_pcollector.get_index(), thread_index);
         client->add_level(_cswitch_sleep_pcollector.get_index(), thread_index, 1);
         client->add_level(_cswitch_sleep_pcollector.get_index(), thread_index, 1);
       }
       }
       else {
       else {
@@ -171,10 +170,9 @@ client_connect(std::string hostname, int port) {
       int thread_index = current_thread->get_pstats_index();
       int thread_index = current_thread->get_pstats_index();
       if (thread_index >= 0) {
       if (thread_index >= 0) {
         PStatClient *client = PStatClient::get_global_pstats();
         PStatClient *client = PStatClient::get_global_pstats();
-        double start = client->get_real_time();
+        client->start(_wait_yield_pcollector.get_index(), thread_index);
         ThreadImpl::yield();
         ThreadImpl::yield();
-        double stop = client->get_real_time();
-        client->start_stop(_wait_yield_pcollector.get_index(), thread_index, start, stop);
+        client->stop(_wait_yield_pcollector.get_index(), thread_index);
         client->add_level(_cswitch_yield_pcollector.get_index(), thread_index, 1);
         client->add_level(_cswitch_yield_pcollector.get_index(), thread_index, 1);
       }
       }
       else {
       else {
@@ -190,10 +188,9 @@ client_connect(std::string hostname, int port) {
         BOOL result;
         BOOL result;
         if (thread_index >= 0) {
         if (thread_index >= 0) {
           PStatClient *client = PStatClient::get_global_pstats();
           PStatClient *client = PStatClient::get_global_pstats();
-          double start = client->get_real_time();
+          client->start(_wait_cvar_pcollector.get_index(), thread_index);
           result = SleepConditionVariableSRW(cvar, lock, time, flags);
           result = SleepConditionVariableSRW(cvar, lock, time, flags);
-          double stop = client->get_real_time();
-          client->start_stop(_wait_cvar_pcollector.get_index(), thread_index, start, stop);
+          client->stop(_wait_cvar_pcollector.get_index(), thread_index);
           client->add_level(_cswitch_cvar_pcollector.get_index(), thread_index, 1);
           client->add_level(_cswitch_cvar_pcollector.get_index(), thread_index, 1);
         }
         }
         else {
         else {
@@ -211,10 +208,9 @@ client_connect(std::string hostname, int port) {
         int result;
         int result;
         if (thread_index >= 0) {
         if (thread_index >= 0) {
           PStatClient *client = PStatClient::get_global_pstats();
           PStatClient *client = PStatClient::get_global_pstats();
-          double start = client->get_real_time();
+          client->start(_wait_cvar_pcollector.get_index(), thread_index);
           result = pthread_cond_wait(cvar, lock);
           result = pthread_cond_wait(cvar, lock);
-          double stop = client->get_real_time();
-          client->start_stop(_wait_cvar_pcollector.get_index(), thread_index, start, stop);
+          client->stop(_wait_cvar_pcollector.get_index(), thread_index);
           client->add_level(_cswitch_cvar_pcollector.get_index(), thread_index, 1);
           client->add_level(_cswitch_cvar_pcollector.get_index(), thread_index, 1);
         }
         }
         else {
         else {
@@ -231,10 +227,9 @@ client_connect(std::string hostname, int port) {
         int result;
         int result;
         if (thread_index >= 0) {
         if (thread_index >= 0) {
           PStatClient *client = PStatClient::get_global_pstats();
           PStatClient *client = PStatClient::get_global_pstats();
-          double start = client->get_real_time();
+          client->start(_wait_cvar_pcollector.get_index(), thread_index);
           result = pthread_cond_timedwait(cvar, lock, ts);
           result = pthread_cond_timedwait(cvar, lock, ts);
-          double stop = client->get_real_time();
-          client->start_stop(_wait_cvar_pcollector.get_index(), thread_index, start, stop);
+          client->stop(_wait_cvar_pcollector.get_index(), thread_index);
           client->add_level(_cswitch_cvar_pcollector.get_index(), thread_index, 1);
           client->add_level(_cswitch_cvar_pcollector.get_index(), thread_index, 1);
         }
         }
         else {
         else {

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

@@ -20,6 +20,9 @@ set(P3PUTIL_HEADERS
   clockObject.h clockObject.I
   clockObject.h clockObject.I
   collideMask.h
   collideMask.h
   colorSpace.h
   colorSpace.h
+  completable.I completable.h
+  completionCounter.I completionCounter.h
+  completionToken.I completionToken.h
   copyOnWriteObject.h copyOnWriteObject.I
   copyOnWriteObject.h copyOnWriteObject.I
   copyOnWritePointer.h copyOnWritePointer.I
   copyOnWritePointer.h copyOnWritePointer.I
   compareTo.I compareTo.h
   compareTo.I compareTo.h
@@ -86,6 +89,7 @@ set(P3PUTIL_SOURCES
   callbackObject.cxx
   callbackObject.cxx
   clockObject.cxx
   clockObject.cxx
   colorSpace.cxx
   colorSpace.cxx
+  completionCounter.cxx
   copyOnWriteObject.cxx
   copyOnWriteObject.cxx
   copyOnWritePointer.cxx
   copyOnWritePointer.cxx
   config_putil.cxx configurable.cxx
   config_putil.cxx configurable.cxx

+ 75 - 0
panda/src/putil/completable.I

@@ -0,0 +1,75 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file completable.I
+ * @author rdb
+ * @date 2025-01-22
+ */
+
+#ifndef CPPPARSER
+/**
+ *
+ */
+template<class Callable>
+INLINE Completable::
+Completable(Callable callback) :
+  _data(new LambdaData<Callable>(std::move(callback), [](Data *data, bool do_run) {
+    LambdaData<Callable> *self = (LambdaData<Callable> *)data;
+    if (do_run) {
+      std::move(self->_lambda)();
+    }
+    delete self;
+  })) {
+}
+#endif
+
+/**
+ *
+ */
+INLINE Completable::
+Completable(Completable &&from) noexcept :
+  _data(from._data) {
+  from._data = nullptr;
+}
+
+/**
+ *
+ */
+INLINE Completable &Completable::
+operator =(Completable &&from) {
+  Data *data = _data;
+  _data = from._data;
+  from._data = nullptr;
+  if (data != nullptr) {
+    data->_function.load(std::memory_order_relaxed)(data, false);
+  }
+  return *this;
+}
+
+/**
+ *
+ */
+INLINE Completable::
+~Completable() {
+  Data *data = _data;
+  if (data != nullptr) {
+    data->_function.load(std::memory_order_relaxed)(data, false);
+  }
+}
+
+/**
+ *
+ */
+INLINE void Completable::
+operator ()() {
+  Data *data = _data;
+  _data = nullptr;
+  if (data != nullptr) {
+    data->_function.load(std::memory_order_relaxed)(data, true);
+  }
+}

+ 82 - 0
panda/src/putil/completable.h

@@ -0,0 +1,82 @@
+/**
+ * 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 completable.h
+ * @author rdb
+ * @date 2025-01-22
+ */
+
+#ifndef COMPLETABLE_H
+#define COMPLETABLE_H
+
+#include "pandabase.h"
+#include "patomic.h"
+
+/**
+ * Stores a type-erased callable that is move-only.  May only be called once.
+ */
+class EXPCL_PANDA_PUTIL Completable {
+public:
+  constexpr Completable() = default;
+
+#ifndef CPPPARSER
+  template<class Callable>
+  INLINE Completable(Callable callback);
+#endif
+
+  INLINE Completable(const Completable &copy) = delete;
+  INLINE Completable(Completable &&from) noexcept;
+
+  INLINE Completable &operator =(const Completable &copy) = delete;
+  INLINE Completable &operator =(Completable &&from);
+
+  INLINE void operator ()();
+
+  INLINE ~Completable();
+
+protected:
+  // There are several design approaches here:
+  // 1. Optimize for no data block: do not require dynamic allocation of a data
+  //    block in the simple case where the callback data is only the size of a
+  //    single pointer.  Store two pointers, one function pointer and a data
+  //    pointer(-sized storage), directly on the class here.
+  // 2. Optimize for a data block: store the function pointer on the data block,
+  //    always requiring dynamic allocation.
+  //
+  // Right now I have opted for 2 because it allows the function pointer to be
+  // dynamically swapped (used in CompletionCounter), but this decision may
+  // change in the future.
+
+  struct Data;
+  typedef void CallbackFunction(Data *, bool);
+
+  struct Data {
+    patomic<CallbackFunction *> _function { nullptr };
+  };
+
+  template<typename Lambda>
+  struct LambdaData : public Data {
+    // Must unfortunately be defined inline, since this struct is protected.
+    LambdaData(Lambda lambda, CallbackFunction *function) :
+      _lambda(std::move(lambda)) {
+      _function = function;
+    }
+
+    Lambda _lambda;
+  };
+
+  Data *_data = nullptr;
+
+  friend class AsyncFuture;
+  friend class CompletionCounter;
+  friend class CompletionToken;
+};
+
+#include "completable.I"
+
+#endif

+ 97 - 0
panda/src/putil/completionCounter.I

@@ -0,0 +1,97 @@
+/**
+ * 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 completionCounter.I
+ * @author rdb
+ * @date 2025-01-22
+ */
+
+/**
+ *
+ */
+INLINE CompletionCounter::
+~CompletionCounter() {
+  CounterData *data = _data;
+  if (data != nullptr) {
+    // then() is not called; we still need something that destructs the data
+    // when done.
+    auto prev_function = data->_function.exchange(&abandon_callback, std::memory_order_relaxed);
+    if (prev_function == nullptr) {
+      // Was already done.
+      delete data;
+    }
+  }
+}
+
+/**
+ * Returns a new token.  May not be called after then().
+ */
+INLINE CompletionToken CompletionCounter::
+make_token() {
+  CompletionToken token;
+  if (_data == nullptr) {
+    _data = new CounterData;
+    _data->_function = &initial_callback;
+  }
+  auto old_value = _data->_counter.fetch_add(1);
+  nassertr(old_value >= 0, token);
+  token._callback._data = _data;
+  return token;
+}
+
+/**
+ * Runs the given callback immediately upon completion.  If the counter is
+ * already done, runs it immediately.  This requires an rvalue because it
+ * consumes the counter, use std::move() if you don't have an rvalue.
+ *
+ * The callback will either be called immediately or directly when the last
+ * token calls complete(), however, it may also be called if a token is
+ * destroyed.  This may happen at unexpected times, such as when the lambda
+ * holding the token is destroyed prematurely.  In this case, however, the
+ * passed success argument will always be false.
+ */
+template<class Callable>
+INLINE void CompletionCounter::
+then(Callable callable) && {
+  // Replace the callback pointer with something that calls the given callable
+  // once the count reaches 0.
+  CounterData *data = _data;
+  nassertv(data != nullptr);
+  _data = nullptr;
+  if (data->_function.load(std::memory_order_acquire) == nullptr) {
+    // Already done.
+    callable((data->_counter.load(std::memory_order_relaxed) & ~0xffff) == 0);
+    delete data;
+    return;
+  }
+
+  static_assert(sizeof(Callable) <= sizeof(data->_storage),
+    "raise storage size in completionCounter.h or reduce lambda captures");
+
+  new (data->_storage) Callable(std::move(callable));
+
+  Completable::CallbackFunction *new_function =
+    [] (Completable::Data *data_ptr, bool success) {
+      CounterData *data = (CounterData *)data_ptr;
+      auto prev_count = data->_counter.fetch_add((success ? 0 : 0x10000) - 1, std::memory_order_release);
+      if ((short)(prev_count & 0xffff) > 1) {
+        return;
+      }
+
+      Callable *callable = (Callable *)data->_storage;
+      std::move(*callable)(success && (prev_count & ~0xffff) == 0);
+      callable->~Callable();
+      delete data;
+    };
+
+  auto prev_function = data->_function.exchange(new_function, std::memory_order_acq_rel);
+  if (UNLIKELY(prev_function == nullptr)) {
+    // Last token finished in the meantime.
+    new_function(data, (data->_counter.load(std::memory_order_relaxed) & ~0xffff) == 0);
+  }
+}

+ 52 - 0
panda/src/putil/completionCounter.cxx

@@ -0,0 +1,52 @@
+/**
+ * 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 completionCounter.cxx
+ * @author rdb
+ * @date 2025-01-24
+ */
+
+#include "completionCounter.h"
+
+/**
+ * Called when a token is completed before then() is called.
+ */
+void CompletionCounter::
+initial_callback(Completable::Data *data_ptr, bool success) {
+  CounterData &data = *(CounterData *)data_ptr;
+  auto prev_count = data._counter.fetch_add((success ? 0 : 0x10000) - 1, std::memory_order_release);
+  if ((prev_count & 0xffff) == 1) {
+    // We're done early.
+    auto prev_callback = data._function.exchange(nullptr, std::memory_order_acq_rel);
+    nassertv(prev_callback != nullptr);
+
+    // Someone called then() in the meantime.  Call the new callback.  The
+    // refcount will drop below 0 when that's called but they are designed to
+    // handle that.
+    if (prev_callback != &initial_callback) {
+      prev_callback(data_ptr, success && (prev_count & ~0xffff) == 0);
+    }
+  }
+}
+
+/**
+ * Called when a token is completed after this object is destroyed without
+ * then() being called.
+ */
+void CompletionCounter::
+abandon_callback(Completable::Data *data_ptr, bool success) {
+  CounterData &data = *(CounterData *)data_ptr;
+  auto prev_count = data._counter.fetch_sub(1, std::memory_order_relaxed);
+  if ((prev_count & 0xffff) <= 1) {
+    // Done.
+    auto prev_callback = data._function.exchange(nullptr, std::memory_order_relaxed);
+    nassertv(prev_callback != nullptr);
+    nassertv(prev_callback == &abandon_callback);
+    delete &data;
+  }
+}

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