Browse Source

Merge branch 'shaderpipeline' into vulkan

rdb 1 month ago
parent
commit
2063306add
100 changed files with 2074 additions and 1325 deletions
  1. 24 24
      .github/workflows/ci.yml
  2. 2 2
      .github/workflows/mypy.yml
  3. 2 2
      README.md
  4. 7 2
      contrib/src/sceneeditor/seParticlePanel.py
  5. 1 1
      direct/src/dcparser/dcClass_ext.cxx
  6. 2 2
      direct/src/directtools/DirectUtil.py
  7. 1 1
      direct/src/distributed/ClockDelta.py
  8. 7 3
      direct/src/extensions_native/extension_native_helpers.py
  9. 10 7
      direct/src/interval/IntervalManager.py
  10. 27 15
      direct/src/showbase/BufferViewer.py
  11. 14 5
      direct/src/showbase/DirectObject.py
  12. 29 12
      direct/src/showbase/EventManager.py
  13. 4 4
      direct/src/showbase/ExceptionVarDump.py
  14. 29 12
      direct/src/showbase/GarbageReport.py
  15. 13 9
      direct/src/showbase/JobManager.py
  16. 51 21
      direct/src/showbase/Loader.py
  17. 54 20
      direct/src/showbase/Messenger.py
  18. 13 7
      direct/src/showbase/OnScreenDebug.py
  19. 43 25
      direct/src/showbase/PythonUtil.py
  20. 11 10
      direct/src/showbase/ShowBase.py
  21. 191 447
      direct/src/showbase/VFSImporter.py
  22. 0 17
      direct/src/showbase/showBase.cxx
  23. 0 7
      direct/src/showbase/showBase.h
  24. 21 11
      direct/src/stdpy/thread.py
  25. 32 14
      direct/src/stdpy/threading.py
  26. 56 44
      direct/src/stdpy/threading2.py
  27. 1 1
      direct/src/tkpanels/Inspector.py
  28. 23 5
      direct/src/tkpanels/ParticlePanel.py
  29. 1 1
      dtool/Config.cmake
  30. 1 0
      dtool/LocalSetup.cmake
  31. 1 1
      dtool/src/dtoolbase/atomicAdjust.h
  32. 13 1
      dtool/src/dtoolbase/deletedBufferChain.cxx
  33. 44 0
      dtool/src/dtoolbase/deletedChain.h
  34. 1 1
      dtool/src/dtoolbase/pdtoa.cxx
  35. 3 0
      dtool/src/dtoolutil/iostream_ext.h
  36. 1 1
      dtool/src/dtoolutil/textEncoder_ext.cxx
  37. 0 1
      dtool/src/interrogatedb/CMakeLists.txt
  38. 2 2
      dtool/src/interrogatedb/interrogate_request.h
  39. 6 0
      dtool/src/interrogatedb/py_compat.h
  40. 2 75
      dtool/src/interrogatedb/py_panda.I
  41. 9 234
      dtool/src/interrogatedb/py_panda.h
  42. 0 61
      dtool/src/interrogatedb/py_wrappers.h
  43. 0 0
      dtool/src/parser-inc/DbgHelp.h
  44. 5 0
      dtool/src/parser-inc/Python.h
  45. 1 0
      dtool/src/parser-inc/emmintrin.h
  46. 29 0
      dtool/src/parser-inc/random
  47. 15 0
      dtool/src/prc/configPageManager.cxx
  48. 180 5
      dtool/src/prc/notify.cxx
  49. 5 35
      dtool/src/prc/notifyCategory.cxx
  50. 4 2
      dtool/src/prc/notifyCategory.h
  51. 10 8
      dtool/src/prc/pnotify.h
  52. 51 0
      dtool/src/prckeys/makePrcKey.cxx
  53. 2 2
      makepanda/makepackage.py
  54. 3 0
      makepanda/makepanda.py
  55. 2 2
      makepanda/makepandacore.py
  56. 10 5
      panda/src/collide/collisionBox.cxx
  57. 10 5
      panda/src/collide/collisionCapsule.cxx
  58. 3 3
      panda/src/collide/collisionEntry.I
  59. 3 3
      panda/src/collide/collisionEntry.h
  60. 6 2
      panda/src/collide/collisionFloorMesh.cxx
  61. 12 6
      panda/src/collide/collisionInvSphere.cxx
  62. 11 1
      panda/src/collide/collisionNode_ext.cxx
  63. 14 7
      panda/src/collide/collisionPlane.cxx
  64. 8 4
      panda/src/collide/collisionPolygon.cxx
  65. 16 9
      panda/src/collide/collisionSphere.cxx
  66. 3 3
      panda/src/display/shaderInputBinding_impls.cxx
  67. 12 5
      panda/src/downloader/httpChannel_emscripten.cxx
  68. 11 2
      panda/src/downloader/virtualFileHTTP.cxx
  69. 4 0
      panda/src/downloader/virtualFileHTTP.h
  70. 5 23
      panda/src/egg2pg/eggSaver.cxx
  71. 4 1
      panda/src/egldisplay/eglGraphicsBuffer.cxx
  72. 25 4
      panda/src/event/asyncFuture_ext.cxx
  73. 23 9
      panda/src/event/pythonTask.cxx
  74. 10 5
      panda/src/express/memoryUsage.cxx
  75. 5 2
      panda/src/express/multifile_ext.I
  76. 17 14
      panda/src/express/zipArchive.cxx
  77. 31 3
      panda/src/glstuff/glGraphicsStateGuardian_src.cxx
  78. 1 1
      panda/src/glstuff/glGraphicsStateGuardian_src.h
  79. 5 5
      panda/src/glstuff/glShaderContext_src.cxx
  80. 2 0
      panda/src/gobj/shaderContext.h
  81. 7 0
      panda/src/grutil/config_grutil.cxx
  82. 12 0
      panda/src/grutil/htmlVideoTexture.I
  83. 489 0
      panda/src/grutil/htmlVideoTexture.cxx
  84. 115 0
      panda/src/grutil/htmlVideoTexture.h
  85. 1 0
      panda/src/grutil/p3grutil_composite1.cxx
  86. 4 2
      panda/src/nativenet/socket_address.cxx
  87. 5 1
      panda/src/pgraph/cullResult.I
  88. 17 1
      panda/src/pgraph/cullResult.cxx
  89. 0 16
      panda/src/pgraph/cullableObject.I
  90. 1 1
      panda/src/pgraph/cullableObject.h
  91. 35 9
      panda/src/pgraph/loader.cxx
  92. 23 3
      panda/src/pgraph/modelRoot.I
  93. 10 0
      panda/src/pgraph/modelRoot.cxx
  94. 8 0
      panda/src/pgraph/modelRoot.h
  95. 2 3
      panda/src/pgraph/nodePath_ext.cxx
  96. 2 3
      panda/src/pgraph/pandaNode_ext.cxx
  97. 8 0
      panda/src/pgraphnodes/lightLensNode.I
  98. 2 0
      panda/src/pgraphnodes/lightLensNode.h
  99. 4 1
      panda/src/physics/linearDistanceForce.h
  100. 19 1
      panda/src/physics/linearSinkForce.cxx

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

@@ -20,7 +20,7 @@ jobs:
 
 
         include:
         include:
         - profile: ubuntu-bionic-double-standard-unity-makefile
         - profile: ubuntu-bionic-double-standard-unity-makefile
-          os: ubuntu-20.04
+          os: ubuntu-22.04
           config: Standard
           config: Standard
           unity: YES
           unity: YES
           generator: Unix Makefiles
           generator: Unix Makefiles
@@ -31,7 +31,7 @@ jobs:
           double: YES
           double: YES
 
 
         - profile: ubuntu-bionic-coverage-ninja
         - profile: ubuntu-bionic-coverage-ninja
-          os: ubuntu-20.04
+          os: ubuntu-22.04
           config: Coverage
           config: Coverage
           unity: NO
           unity: NO
           generator: Ninja
           generator: Ninja
@@ -88,7 +88,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
 
 
     steps:
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v5
       with:
       with:
         fetch-depth: 10
         fetch-depth: 10
 
 
@@ -193,7 +193,7 @@ jobs:
 
 
     - name: Setup Python (Python 3.8)
     - name: Setup Python (Python 3.8)
       if: contains(matrix.python, 'YES')
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
       with:
         python-version: '3.8'
         python-version: '3.8'
     - name: Configure (Python 3.8)
     - name: Configure (Python 3.8)
@@ -225,7 +225,7 @@ jobs:
 
 
     - name: Setup Python (Python 3.9)
     - name: Setup Python (Python 3.9)
       if: contains(matrix.python, 'YES')
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
       with:
         python-version: '3.9'
         python-version: '3.9'
     - name: Configure (Python 3.9)
     - name: Configure (Python 3.9)
@@ -257,7 +257,7 @@ jobs:
 
 
     - name: Setup Python (Python 3.10)
     - name: Setup Python (Python 3.10)
       if: contains(matrix.python, 'YES')
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
       with:
         python-version: '3.10'
         python-version: '3.10'
     - name: Configure (Python 3.10)
     - name: Configure (Python 3.10)
@@ -289,7 +289,7 @@ jobs:
 
 
     - name: Setup Python (Python 3.11)
     - name: Setup Python (Python 3.11)
       if: contains(matrix.python, 'YES')
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
       with:
         python-version: '3.11'
         python-version: '3.11'
     - name: Configure (Python 3.11)
     - name: Configure (Python 3.11)
@@ -321,7 +321,7 @@ jobs:
 
 
     - name: Setup Python (Python 3.12)
     - name: Setup Python (Python 3.12)
       if: contains(matrix.python, 'YES')
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
       with:
         python-version: '3.12'
         python-version: '3.12'
     - name: Configure (Python 3.12)
     - name: Configure (Python 3.12)
@@ -372,12 +372,12 @@ jobs:
     if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
     if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
     strategy:
     strategy:
       matrix:
       matrix:
-        os: [ubuntu-20.04, windows-2019, macOS-13]
+        os: [ubuntu-22.04, windows-2022, macOS-13]
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     steps:
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v5
     - name: Install dependencies (Ubuntu)
     - name: Install dependencies (Ubuntu)
-      if: matrix.os == 'ubuntu-20.04'
+      if: matrix.os == 'ubuntu-22.04'
       run: |
       run: |
         sudo apt-get update
         sudo apt-get update
         sudo apt-get install build-essential bison flex libfreetype6-dev libgl1-mesa-dev libjpeg-dev libode-dev libopenal-dev libpng-dev libssl-dev libvorbis-dev libx11-dev libxcursor-dev libxrandr-dev nvidia-cg-toolkit zlib1g-dev
         sudo apt-get install build-essential bison flex libfreetype6-dev libgl1-mesa-dev libjpeg-dev libode-dev libopenal-dev libpng-dev libssl-dev libvorbis-dev libx11-dev libxcursor-dev libxrandr-dev nvidia-cg-toolkit zlib1g-dev
@@ -403,13 +403,13 @@ jobs:
       run: sudo xcode-select -s /Applications/Xcode_14.3.1.app/Contents/Developer
       run: sudo xcode-select -s /Applications/Xcode_14.3.1.app/Contents/Developer
 
 
     - name: Set up Python 3.13
     - name: Set up Python 3.13
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
       with:
         python-version: '3.13'
         python-version: '3.13'
     - name: Build Python 3.13
     - name: Build Python 3.13
       shell: bash
       shell: bash
       run: |
       run: |
-        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2
+        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3
     - name: Test Python 3.13
     - name: Test Python 3.13
       shell: bash
       shell: bash
       run: |
       run: |
@@ -417,13 +417,13 @@ jobs:
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
 
 
     - name: Set up Python 3.12
     - name: Set up Python 3.12
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
       with:
         python-version: '3.12'
         python-version: '3.12'
     - name: Build Python 3.12 (double)
     - name: Build Python 3.12 (double)
       shell: bash
       shell: bash
       run: |
       run: |
-        python makepanda/makepanda.py --override STDFLOAT_DOUBLE=1 --git-commit=${{github.sha}} --outputdir=built_dbl --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2
+        python makepanda/makepanda.py --override STDFLOAT_DOUBLE=1 --git-commit=${{github.sha}} --outputdir=built_dbl --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3
     - name: Test Python 3.12 (double)
     - name: Test Python 3.12 (double)
       shell: bash
       shell: bash
       run: |
       run: |
@@ -435,13 +435,13 @@ jobs:
         python makepanda/makepackage.py --verbose --lzma --outputdir=built_dbl
         python makepanda/makepackage.py --verbose --lzma --outputdir=built_dbl
 
 
     - name: Set up Python 3.11
     - name: Set up Python 3.11
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
       with:
         python-version: '3.11'
         python-version: '3.11'
     - name: Build Python 3.11
     - name: Build Python 3.11
       shell: bash
       shell: bash
       run: |
       run: |
-        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2
+        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3
     - name: Test Python 3.11
     - name: Test Python 3.11
       shell: bash
       shell: bash
       run: |
       run: |
@@ -449,13 +449,13 @@ jobs:
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
 
 
     - name: Set up Python 3.10
     - name: Set up Python 3.10
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
       with:
         python-version: '3.10'
         python-version: '3.10'
     - name: Build Python 3.10
     - name: Build Python 3.10
       shell: bash
       shell: bash
       run: |
       run: |
-        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2
+        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3
     - name: Test Python 3.10
     - name: Test Python 3.10
       shell: bash
       shell: bash
       run: |
       run: |
@@ -463,13 +463,13 @@ jobs:
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
 
 
     - name: Set up Python 3.9
     - name: Set up Python 3.9
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
       with:
         python-version: '3.9'
         python-version: '3.9'
     - name: Build Python 3.9
     - name: Build Python 3.9
       shell: bash
       shell: bash
       run: |
       run: |
-        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2
+        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3
     - name: Test Python 3.9
     - name: Test Python 3.9
       shell: bash
       shell: bash
       run: |
       run: |
@@ -477,13 +477,13 @@ jobs:
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
 
 
     - name: Set up Python 3.8
     - name: Set up Python 3.8
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
       with:
         python-version: '3.8'
         python-version: '3.8'
     - name: Build Python 3.8
     - name: Build Python 3.8
       shell: bash
       shell: bash
       run: |
       run: |
-        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2
+        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3
     - name: Test Python 3.8
     - name: Test Python 3.8
       shell: bash
       shell: bash
       run: |
       run: |
@@ -498,7 +498,7 @@ jobs:
     if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
     if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
     runs-on: ubuntu-24.04
     runs-on: ubuntu-24.04
     steps:
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v5
 
 
     - name: Install dependencies
     - name: Install dependencies
       run: |
       run: |

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

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

+ 2 - 2
README.md

@@ -228,8 +228,8 @@ information as possible to help the developers track down the issue, such as
 your version of Panda3D, operating system, architecture, and any code and
 your version of Panda3D, operating system, architecture, and any code and
 models that are necessary for the developers to reproduce the issue.
 models that are necessary for the developers to reproduce the issue.
 
 
-If you're not sure whether you've encountered a bug, feel free to ask about
-it in the forums or the IRC channel first.
+If you're unsure whether you've encountered a bug, feel free to ask in the [forums](https://discourse.panda3d.org) or the [IRC channel](https://web.libera.chat/#panda3d) before opening an issue.
+
 
 
 Supporting the Project
 Supporting the Project
 ======================
 ======================

+ 7 - 2
contrib/src/sceneeditor/seParticlePanel.py

@@ -1,6 +1,7 @@
 """PANDA3D Particle Panel"""
 """PANDA3D Particle Panel"""
 
 
 # Import Tkinter, Pmw, and the floater code from this directory tree.
 # Import Tkinter, Pmw, and the floater code from this directory tree.
+from panda3d.core import ConfigVariableSearchPath, Point2, Point3, Vec3, Vec4
 from direct.tkwidgets.AppShell import AppShell
 from direct.tkwidgets.AppShell import AppShell
 import os, Pmw
 import os, Pmw
 from direct.tkwidgets.Dial import AngleDial
 from direct.tkwidgets.Dial import AngleDial
@@ -17,6 +18,10 @@ from tkinter.filedialog import *
 from tkinter.simpledialog import askstring
 from tkinter.simpledialog import askstring
 
 
 
 
+particlePath = ConfigVariableSearchPath("particle-path",
+    "The directories to search for particle files to be loaded.")
+
+
 class ParticlePanel(AppShell):
 class ParticlePanel(AppShell):
     # Override class variables
     # Override class variables
     appname = 'Particle Panel'
     appname = 'Particle Panel'
@@ -1041,7 +1046,7 @@ class ParticlePanel(AppShell):
 
 
     def loadParticleEffectFromFile(self):
     def loadParticleEffectFromFile(self):
         # Find path to particle directory
         # Find path to particle directory
-        pPath = getParticlePath()
+        pPath = particlePath
         if pPath.getNumDirectories() > 0:
         if pPath.getNumDirectories() > 0:
             if repr(pPath.getDirectory(0)) == '.':
             if repr(pPath.getDirectory(0)) == '.':
                 path = '.'
                 path = '.'
@@ -1070,7 +1075,7 @@ class ParticlePanel(AppShell):
 
 
     def saveParticleEffectToFile(self):
     def saveParticleEffectToFile(self):
         # Find path to particle directory
         # Find path to particle directory
-        pPath = getParticlePath()
+        pPath = particlePath
         if pPath.getNumDirectories() > 0:
         if pPath.getNumDirectories() > 0:
             if repr(pPath.getDirectory(0)) == '.':
             if repr(pPath.getDirectory(0)) == '.':
                 path = '.'
                 path = '.'

+ 1 - 1
direct/src/dcparser/dcClass_ext.cxx

@@ -108,7 +108,7 @@ receive_update(PyObject *distobj, DatagramIterator &di) const {
   int field_id = packer.raw_unpack_uint16();
   int field_id = packer.raw_unpack_uint16();
   DCField *field = _this->get_field_by_index(field_id);
   DCField *field = _this->get_field_by_index(field_id);
   if (field == nullptr) {
   if (field == nullptr) {
-    ostringstream strm;
+    std::ostringstream strm;
     strm
     strm
         << "Received update for field " << field_id << ", not in class "
         << "Received update for field " << field_id << ", not in class "
         << _this->get_name();
         << _this->get_name();

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

@@ -1,4 +1,4 @@
-from panda3d.core import VBase4
+from panda3d.core import NodePath, VBase4
 from direct.task.Task import Task
 from direct.task.Task import Task
 from direct.task.TaskManagerGlobal import taskMgr
 from direct.task.TaskManagerGlobal import taskMgr
 
 
@@ -55,7 +55,7 @@ def lerpBackgroundColor(r, g, b, duration):
 
 
 # Set direct drawing style for an object
 # Set direct drawing style for an object
 # Never light object or draw in wireframe
 # Never light object or draw in wireframe
-def useDirectRenderStyle(nodePath, priority = 0):
+def useDirectRenderStyle(nodePath: NodePath, priority: int = 0) -> None:
     """
     """
     Function to force a node path to use direct render style:
     Function to force a node path to use direct render style:
     no lighting, and no wireframe
     no lighting, and no wireframe

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

@@ -48,7 +48,7 @@ class ClockDelta(DirectObject.DirectObject):
 
 
     notify = DirectNotifyGlobal.directNotify.newCategory('ClockDelta')
     notify = DirectNotifyGlobal.directNotify.newCategory('ClockDelta')
 
 
-    def __init__(self):
+    def __init__(self) -> None:
         self.globalClock = ClockObject.getGlobalClock()
         self.globalClock = ClockObject.getGlobalClock()
 
 
         # self.delta is the relative delta from our clock to the
         # self.delta is the relative delta from our clock to the

+ 7 - 3
direct/src/extensions_native/extension_native_helpers.py

@@ -1,15 +1,19 @@
+from __future__ import annotations
+
 __all__ = ["Dtool_ObjectToDict", "Dtool_funcToMethod"]
 __all__ = ["Dtool_ObjectToDict", "Dtool_funcToMethod"]
 
 
+from collections.abc import Callable
+
 
 
 def Dtool_ObjectToDict(cls, name, obj):
 def Dtool_ObjectToDict(cls, name, obj):
     cls.DtoolClassDict[name] = obj
     cls.DtoolClassDict[name] = obj
 
 
 
 
-def Dtool_funcToMethod(func, cls, method_name=None):
+def Dtool_funcToMethod(func: Callable, cls, method_name: str | None = None) -> None:
     """Adds func to class so it is an accessible method; use method_name to specify the name to be used for calling the method.
     """Adds func to class so it is an accessible method; use method_name to specify the name to be used for calling the method.
     The new method is accessible to any instance immediately."""
     The new method is accessible to any instance immediately."""
-    func.__func__ = func
-    func.__self__ = None
+    func.__func__ = func  # type: ignore[attr-defined]
+    func.__self__ = None  # type: ignore[attr-defined]
     if not method_name:
     if not method_name:
         method_name = func.__name__
         method_name = func.__name__
     cls.DtoolClassDict[method_name] = func
     cls.DtoolClassDict[method_name] = func

+ 10 - 7
direct/src/interval/IntervalManager.py

@@ -1,11 +1,14 @@
 """Defines the IntervalManager class as well as the global instance of
 """Defines the IntervalManager class as well as the global instance of
 this class, ivalMgr."""
 this class, ivalMgr."""
 
 
+from __future__ import annotations
+
 __all__ = ['IntervalManager', 'ivalMgr']
 __all__ = ['IntervalManager', 'ivalMgr']
 
 
 from panda3d.core import EventQueue
 from panda3d.core import EventQueue
-from panda3d.direct import CIntervalManager, Dtool_BorrowThisReference
+from panda3d.direct import CInterval, CIntervalManager, Dtool_BorrowThisReference
 from direct.showbase import EventManager
 from direct.showbase import EventManager
+from . import Interval
 import fnmatch
 import fnmatch
 
 
 class IntervalManager(CIntervalManager):
 class IntervalManager(CIntervalManager):
@@ -15,7 +18,7 @@ class IntervalManager(CIntervalManager):
     # the Python extensions is to add support for Python-based
     # the Python extensions is to add support for Python-based
     # intervals (like MetaIntervals).
     # intervals (like MetaIntervals).
 
 
-    def __init__(self, globalPtr = 0):
+    def __init__(self, globalPtr: bool = False) -> None:
         # Pass globalPtr == 1 to the constructor to trick it into
         # Pass globalPtr == 1 to the constructor to trick it into
         # "constructing" a Python wrapper around the global
         # "constructing" a Python wrapper around the global
         # CIntervalManager object.
         # CIntervalManager object.
@@ -28,8 +31,8 @@ class IntervalManager(CIntervalManager):
         self.eventQueue = EventQueue()
         self.eventQueue = EventQueue()
         self.MyEventmanager = EventManager.EventManager(self.eventQueue)
         self.MyEventmanager = EventManager.EventManager(self.eventQueue)
         self.setEventQueue(self.eventQueue)
         self.setEventQueue(self.eventQueue)
-        self.ivals = []
-        self.removedIvals = {}
+        self.ivals: list[Interval.Interval | CInterval | None] = []
+        self.removedIvals: dict = {}
 
 
     def addInterval(self, interval):
     def addInterval(self, interval):
         index = self.addCInterval(interval, 1)
         index = self.addCInterval(interval, 1)
@@ -86,7 +89,7 @@ class IntervalManager(CIntervalManager):
             ival.pause()
             ival.pause()
         return len(ivals)
         return len(ivals)
 
 
-    def step(self):
+    def step(self) -> None:
         # This method should be called once per frame to perform all
         # This method should be called once per frame to perform all
         # of the per-frame processing on the active intervals.
         # of the per-frame processing on the active intervals.
         # Call C++ step, then do the Python stuff.
         # Call C++ step, then do the Python stuff.
@@ -101,7 +104,7 @@ class IntervalManager(CIntervalManager):
         CIntervalManager.interrupt(self)
         CIntervalManager.interrupt(self)
         self.__doPythonCallbacks()
         self.__doPythonCallbacks()
 
 
-    def __doPythonCallbacks(self):
+    def __doPythonCallbacks(self) -> None:
         # This method does all of the required Python post-processing
         # This method does all of the required Python post-processing
         # after performing some C++-level action.
         # after performing some C++-level action.
         # It is important to call all of the python callbacks on the
         # It is important to call all of the python callbacks on the
@@ -137,4 +140,4 @@ class IntervalManager(CIntervalManager):
         self.ivals[index] = interval
         self.ivals[index] = interval
 
 
 #: The global IntervalManager object.
 #: The global IntervalManager object.
-ivalMgr = IntervalManager(1)
+ivalMgr = IntervalManager(True)

+ 27 - 15
direct/src/showbase/BufferViewer.py

@@ -12,6 +12,8 @@ Or, you can enable the following variable in your Config.prc::
     show-buffers true
     show-buffers true
 """
 """
 
 
+from __future__ import annotations
+
 __all__ = ['BufferViewer']
 __all__ = ['BufferViewer']
 
 
 from panda3d.core import (
 from panda3d.core import (
@@ -39,14 +41,19 @@ from direct.task.TaskManagerGlobal import taskMgr
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.showbase.DirectObject import DirectObject
 from direct.showbase.DirectObject import DirectObject
 import math
 import math
+from typing import Literal, Union
+
+# The following variables are typing constructs used in annotations
+# to succinctly express complex type structures.
+_Texture = Union[Texture, GraphicsOutput, Literal['all']]
 
 
 
 
 class BufferViewer(DirectObject):
 class BufferViewer(DirectObject):
     notify = directNotify.newCategory('BufferViewer')
     notify = directNotify.newCategory('BufferViewer')
 
 
-    def __init__(self, win, parent):
+    def __init__(self, win: GraphicsOutput | None, parent: NodePath) -> None:
         """Access: private.  Constructor."""
         """Access: private.  Constructor."""
-        self.enabled = 0
+        self.enabled = False
         size = ConfigVariableDouble('buffer-viewer-size', '0 0')
         size = ConfigVariableDouble('buffer-viewer-size', '0 0')
         self.sizex = size[0]
         self.sizex = size[0]
         self.sizey = size[1]
         self.sizey = size[1]
@@ -59,23 +66,23 @@ class BufferViewer(DirectObject):
         self.win = win
         self.win = win
         self.engine = GraphicsEngine.getGlobalPtr()
         self.engine = GraphicsEngine.getGlobalPtr()
         self.renderParent = parent
         self.renderParent = parent
-        self.cards = []
+        self.cards: list[NodePath] = []
         self.cardindex = 0
         self.cardindex = 0
         self.cardmaker = CardMaker("cubemaker")
         self.cardmaker = CardMaker("cubemaker")
         self.cardmaker.setFrame(-1,1,-1,1)
         self.cardmaker.setFrame(-1,1,-1,1)
         self.task = 0
         self.task = 0
-        self.dirty = 1
+        self.dirty = True
         self.accept("render-texture-targets-changed", self.refreshReadout)
         self.accept("render-texture-targets-changed", self.refreshReadout)
         if ConfigVariableBool("show-buffers", 0):
         if ConfigVariableBool("show-buffers", 0):
-            self.enable(1)
+            self.enable(True)
 
 
-    def refreshReadout(self):
+    def refreshReadout(self) -> None:
         """Force the readout to be refreshed.  This is usually invoked
         """Force the readout to be refreshed.  This is usually invoked
         by GraphicsOutput::add_render_texture (via an event handler).
         by GraphicsOutput::add_render_texture (via an event handler).
         However, it is also possible to invoke it manually.  Currently,
         However, it is also possible to invoke it manually.  Currently,
         the only time I know of that this is necessary is after a
         the only time I know of that this is necessary is after a
         window resize (and I ought to fix that)."""
         window resize (and I ought to fix that)."""
-        self.dirty = 1
+        self.dirty = True
 
 
         # Call enabled again, mainly to ensure that the task has been
         # Call enabled again, mainly to ensure that the task has been
         # started.
         # started.
@@ -95,14 +102,14 @@ class BufferViewer(DirectObject):
         """Returns true if the buffer viewer is currently enabled."""
         """Returns true if the buffer viewer is currently enabled."""
         return self.enabled
         return self.enabled
 
 
-    def enable(self, x):
+    def enable(self, x: bool) -> None:
         """Turn the buffer viewer on or off.  The initial state of the
         """Turn the buffer viewer on or off.  The initial state of the
         buffer viewer depends on the Config variable 'show-buffers'."""
         buffer viewer depends on the Config variable 'show-buffers'."""
         if x != 0 and x != 1:
         if x != 0 and x != 1:
             BufferViewer.notify.error('invalid parameter to BufferViewer.enable')
             BufferViewer.notify.error('invalid parameter to BufferViewer.enable')
             return
             return
         self.enabled = x
         self.enabled = x
-        self.dirty = 1
+        self.dirty = True
         if (x and self.task == 0):
         if (x and self.task == 0):
             self.task = taskMgr.add(self.maintainReadout, "buffer-viewer-maintain-readout",
             self.task = taskMgr.add(self.maintainReadout, "buffer-viewer-maintain-readout",
                                     priority=1)
                                     priority=1)
@@ -218,7 +225,11 @@ class BufferViewer(DirectObject):
         self.renderParent = renderParent
         self.renderParent = renderParent
         self.dirty = 1
         self.dirty = 1
 
 
-    def analyzeTextureSet(self, x, set):
+    def analyzeTextureSet(
+        self,
+        x: _Texture | GraphicsEngine | list[_Texture | GraphicsEngine],
+        set: dict[Texture, int],
+    ) -> None:
         """Access: private.  Converts a list of GraphicsObject,
         """Access: private.  Converts a list of GraphicsObject,
         GraphicsEngine, and Texture into a table of Textures."""
         GraphicsEngine, and Texture into a table of Textures."""
 
 
@@ -240,7 +251,7 @@ class BufferViewer(DirectObject):
         else:
         else:
             return
             return
 
 
-    def makeFrame(self, sizex, sizey):
+    def makeFrame(self, sizex: int, sizey: int) -> NodePath:
         """Access: private.  Each texture card is displayed with
         """Access: private.  Each texture card is displayed with
         a two-pixel wide frame (a ring of black and a ring of white).
         a two-pixel wide frame (a ring of black and a ring of white).
         This routine builds the frame geometry.  It is necessary to
         This routine builds the frame geometry.  It is necessary to
@@ -287,7 +298,7 @@ class BufferViewer(DirectObject):
         geomnode.addGeom(geom)
         geomnode.addGeom(geom)
         return NodePath(geomnode)
         return NodePath(geomnode)
 
 
-    def maintainReadout(self, task):
+    def maintainReadout(self, task: object) -> int:
         """Access: private.  Whenever necessary, rebuilds the entire
         """Access: private.  Whenever necessary, rebuilds the entire
         display from scratch.  This is only done when the configuration
         display from scratch.  This is only done when the configuration
         parameters have changed."""
         parameters have changed."""
@@ -295,7 +306,7 @@ class BufferViewer(DirectObject):
         # If nothing has changed, don't update.
         # If nothing has changed, don't update.
         if not self.dirty:
         if not self.dirty:
             return Task.cont
             return Task.cont
-        self.dirty = 0
+        self.dirty = False
 
 
         # Delete the old set of cards.
         # Delete the old set of cards.
         for card in self.cards:
         for card in self.cards:
@@ -308,8 +319,8 @@ class BufferViewer(DirectObject):
             return Task.done
             return Task.done
 
 
         # Generate the include and exclude sets.
         # Generate the include and exclude sets.
-        exclude = {}
-        include = {}
+        exclude: dict[Texture, int] = {}
+        include: dict[Texture, int] = {}
         self.analyzeTextureSet(self.exclude, exclude)
         self.analyzeTextureSet(self.exclude, exclude)
         self.analyzeTextureSet(self.include, include)
         self.analyzeTextureSet(self.include, include)
 
 
@@ -407,6 +418,7 @@ class BufferViewer(DirectObject):
 
 
         bordersize = 4.0
         bordersize = 4.0
 
 
+        assert self.win is not None
         if float(self.sizex) == 0.0 and float(self.sizey) == 0.0:
         if float(self.sizex) == 0.0 and float(self.sizey) == 0.0:
             sizey = int(0.4266666667 * self.win.getYSize())
             sizey = int(0.4266666667 * self.win.getYSize())
             sizex = (sizey * aspectx) // aspecty
             sizex = (sizey * aspectx) // aspecty

+ 14 - 5
direct/src/showbase/DirectObject.py

@@ -1,8 +1,14 @@
 """Defines the DirectObject class, a convenient class to inherit from if the
 """Defines the DirectObject class, a convenient class to inherit from if the
 object needs to be able to respond to events."""
 object needs to be able to respond to events."""
 
 
+from __future__ import annotations
+
 __all__ = ['DirectObject']
 __all__ = ['DirectObject']
 
 
+from typing import Callable
+
+from panda3d.core import AsyncTask
+
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.task.TaskManagerGlobal import taskMgr
 from direct.task.TaskManagerGlobal import taskMgr
 from .MessengerGlobal import messenger
 from .MessengerGlobal import messenger
@@ -12,6 +18,9 @@ class DirectObject:
     """
     """
     This is the class that all Direct/SAL classes should inherit from
     This is the class that all Direct/SAL classes should inherit from
     """
     """
+    _MSGRmessengerId: tuple[str, int]
+    _taskList: dict[int, AsyncTask]
+
     #def __del__(self):
     #def __del__(self):
         # This next line is useful for debugging leaks
         # This next line is useful for debugging leaks
         #print "Destructing: ", self.__class__.__name__
         #print "Destructing: ", self.__class__.__name__
@@ -19,16 +28,16 @@ class DirectObject:
     # Wrapper functions to have a cleaner, more object oriented approach to
     # Wrapper functions to have a cleaner, more object oriented approach to
     # the messenger functionality.
     # the messenger functionality.
 
 
-    def accept(self, event, method, extraArgs=[]):
-        return messenger.accept(event, self, method, extraArgs, 1)
+    def accept(self, event: str, method: Callable, extraArgs: list = []) -> None:
+        return messenger.accept(event, self, method, extraArgs, True)
 
 
     def acceptOnce(self, event, method, extraArgs=[]):
     def acceptOnce(self, event, method, extraArgs=[]):
         return messenger.accept(event, self, method, extraArgs, 0)
         return messenger.accept(event, self, method, extraArgs, 0)
 
 
-    def ignore(self, event):
+    def ignore(self, event: str) -> None:
         return messenger.ignore(event, self)
         return messenger.ignore(event, self)
 
 
-    def ignoreAll(self):
+    def ignoreAll(self) -> None:
         return messenger.ignoreAll(self)
         return messenger.ignoreAll(self)
 
 
     def isAccepting(self, event):
     def isAccepting(self, event):
@@ -41,7 +50,7 @@ class DirectObject:
         return messenger.isIgnoring(event, self)
         return messenger.isIgnoring(event, self)
 
 
     #This function must be used if you want a managed task
     #This function must be used if you want a managed task
-    def addTask(self, *args, **kwargs):
+    def addTask(self, *args, **kwargs) -> AsyncTask:
         if not hasattr(self, "_taskList"):
         if not hasattr(self, "_taskList"):
             self._taskList = {}
             self._taskList = {}
         kwargs['owner'] = self
         kwargs['owner'] = self

+ 29 - 12
direct/src/showbase/EventManager.py

@@ -1,21 +1,33 @@
 """Contains the EventManager class.  See :mod:`.EventManagerGlobal` for the
 """Contains the EventManager class.  See :mod:`.EventManagerGlobal` for the
 global eventMgr instance."""
 global eventMgr instance."""
 
 
+from __future__ import annotations
+
 __all__ = ['EventManager']
 __all__ = ['EventManager']
 
 
 
 
+from typing import Any
+
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.directnotify.DirectNotifyGlobal import directNotify
+from direct.directnotify.Notifier import Notifier
 from direct.task.TaskManagerGlobal import taskMgr
 from direct.task.TaskManagerGlobal import taskMgr
 from direct.showbase.MessengerGlobal import messenger
 from direct.showbase.MessengerGlobal import messenger
-from panda3d.core import PStatCollector, EventQueue, EventHandler
-from panda3d.core import ConfigVariableBool
+from panda3d.core import (
+    ConfigVariableBool,
+    Event,
+    EventHandler,
+    EventParameter,
+    EventQueue,
+    PStatCollector,
+    PythonTask,
+)
 
 
 
 
 class EventManager:
 class EventManager:
 
 
-    notify = None
+    notify: Notifier | None = None
 
 
-    def __init__(self, eventQueue = None):
+    def __init__(self, eventQueue: EventQueue | None = None) -> None:
         """
         """
         Create a C++ event queue and handler
         Create a C++ event queue and handler
         """
         """
@@ -24,11 +36,11 @@ class EventManager:
             EventManager.notify = directNotify.newCategory("EventManager")
             EventManager.notify = directNotify.newCategory("EventManager")
 
 
         self.eventQueue = eventQueue
         self.eventQueue = eventQueue
-        self.eventHandler = None
+        self.eventHandler: EventHandler | None = None
 
 
         self._wantPstats = ConfigVariableBool('pstats-eventmanager', False)
         self._wantPstats = ConfigVariableBool('pstats-eventmanager', False)
 
 
-    def doEvents(self):
+    def doEvents(self) -> None:
         """
         """
         Process all the events on the C++ event queue
         Process all the events on the C++ event queue
         """
         """
@@ -38,12 +50,13 @@ class EventManager:
             processFunc = self.processEventPstats
             processFunc = self.processEventPstats
         else:
         else:
             processFunc = self.processEvent
             processFunc = self.processEvent
+        assert self.eventQueue is not None
         isEmptyFunc = self.eventQueue.isQueueEmpty
         isEmptyFunc = self.eventQueue.isQueueEmpty
         dequeueFunc = self.eventQueue.dequeueEvent
         dequeueFunc = self.eventQueue.dequeueEvent
         while not isEmptyFunc():
         while not isEmptyFunc():
             processFunc(dequeueFunc())
             processFunc(dequeueFunc())
 
 
-    def eventLoopTask(self, task):
+    def eventLoopTask(self, task: PythonTask) -> int:
         """
         """
         Process all the events on the C++ event queue
         Process all the events on the C++ event queue
         """
         """
@@ -51,7 +64,7 @@ class EventManager:
         messenger.send("event-loop-done")
         messenger.send("event-loop-done")
         return task.cont
         return task.cont
 
 
-    def parseEventParameter(self, eventParameter):
+    def parseEventParameter(self, eventParameter: EventParameter) -> Any:
         """
         """
         Extract the actual data from the eventParameter
         Extract the actual data from the eventParameter
         """
         """
@@ -72,7 +85,7 @@ class EventManager:
             # which will be downcast to that type.
             # which will be downcast to that type.
             return eventParameter.getPtr()
             return eventParameter.getPtr()
 
 
-    def processEvent(self, event):
+    def processEvent(self, event: Event) -> None:
         """
         """
         Process a C++ event
         Process a C++ event
         Duplicate any changes in processEventPstats
         Duplicate any changes in processEventPstats
@@ -89,6 +102,7 @@ class EventManager:
                 paramList.append(eventParameterData)
                 paramList.append(eventParameterData)
 
 
             # Do not print the new frame debug, it is too noisy!
             # Do not print the new frame debug, it is too noisy!
+            assert EventManager.notify is not None
             if EventManager.notify.getDebug() and eventName != 'NewFrame':
             if EventManager.notify.getDebug() and eventName != 'NewFrame':
                 EventManager.notify.debug('received C++ event named: ' + eventName +
                 EventManager.notify.debug('received C++ event named: ' + eventName +
                                           ' parameters: ' + repr(paramList))
                                           ' parameters: ' + repr(paramList))
@@ -106,9 +120,10 @@ class EventManager:
 
 
         else:
         else:
             # An unnamed event from C++ is probably a bad thing
             # An unnamed event from C++ is probably a bad thing
+            assert EventManager.notify is not None
             EventManager.notify.warning('unnamed event in processEvent')
             EventManager.notify.warning('unnamed event in processEvent')
 
 
-    def processEventPstats(self, event):
+    def processEventPstats(self, event: Event) -> None:
         """
         """
         Process a C++ event with pstats tracking
         Process a C++ event with pstats tracking
         Duplicate any changes in processEvent
         Duplicate any changes in processEvent
@@ -125,6 +140,7 @@ class EventManager:
                 paramList.append(eventParameterData)
                 paramList.append(eventParameterData)
 
 
             # Do not print the new frame debug, it is too noisy!
             # Do not print the new frame debug, it is too noisy!
+            assert EventManager.notify is not None
             if EventManager.notify.getDebug() and eventName != 'NewFrame':
             if EventManager.notify.getDebug() and eventName != 'NewFrame':
                 EventManager.notify.debug('received C++ event named: ' + eventName +
                 EventManager.notify.debug('received C++ event named: ' + eventName +
                                           ' parameters: ' + repr(paramList))
                                           ' parameters: ' + repr(paramList))
@@ -156,9 +172,10 @@ class EventManager:
 
 
         else:
         else:
             # An unnamed event from C++ is probably a bad thing
             # An unnamed event from C++ is probably a bad thing
+            assert EventManager.notify is not None
             EventManager.notify.warning('unnamed event in processEvent')
             EventManager.notify.warning('unnamed event in processEvent')
 
 
-    def restart(self):
+    def restart(self) -> None:
         if self.eventQueue is None:
         if self.eventQueue is None:
             self.eventQueue = EventQueue.getGlobalEventQueue()
             self.eventQueue = EventQueue.getGlobalEventQueue()
 
 
@@ -173,7 +190,7 @@ class EventManager:
 
 
         taskMgr.add(self.eventLoopTask, 'eventManager')
         taskMgr.add(self.eventLoopTask, 'eventManager')
 
 
-    def shutdown(self):
+    def shutdown(self) -> None:
         taskMgr.remove('eventManager')
         taskMgr.remove('eventManager')
 
 
         # Flush the event queue.  We do this after removing the task
         # Flush the event queue.  We do this after removing the task

+ 4 - 4
direct/src/showbase/ExceptionVarDump.py

@@ -35,7 +35,7 @@ def _varDump__init__(self, *args, **kArgs):
 sReentry = 0
 sReentry = 0
 
 
 
 
-def _varDump__print(exc):
+def _varDump__print(exc) -> None:
     global sReentry
     global sReentry
     global notify
     global notify
     if sReentry > 0:
     if sReentry > 0:
@@ -176,7 +176,7 @@ def _excepthookDumpVars(eType, eValue, tb):
     oldExcepthook(eType, eValue, origTb)
     oldExcepthook(eType, eValue, origTb)
 
 
 
 
-def install(log, upload):
+def install(log: bool, upload: bool) -> None:
     """Installs the exception hook."""
     """Installs the exception hook."""
     global oldExcepthook
     global oldExcepthook
     global wantStackDumpLog
     global wantStackDumpLog
@@ -192,8 +192,8 @@ def install(log, upload):
         # thrown by the interpreter don't get created until the
         # thrown by the interpreter don't get created until the
         # stack has been unwound and an except block has been reached
         # stack has been unwound and an except block has been reached
         if not hasattr(Exception, '_moved__init__'):
         if not hasattr(Exception, '_moved__init__'):
-            Exception._moved__init__ = Exception.__init__
-            Exception.__init__ = _varDump__init__
+            Exception._moved__init__ = Exception.__init__  # type: ignore[attr-defined]
+            Exception.__init__ = _varDump__init__  # type: ignore[method-assign]
     else:
     else:
         if sys.excepthook is not _excepthookDumpVars:
         if sys.excepthook is not _excepthookDumpVars:
             oldExcepthook = sys.excepthook
             oldExcepthook = sys.excepthook

+ 29 - 12
direct/src/showbase/GarbageReport.py

@@ -1,5 +1,7 @@
 """Contains utility classes for debugging memory leaks."""
 """Contains utility classes for debugging memory leaks."""
 
 
+from __future__ import annotations
+
 __all__ = ['FakeObject', '_createGarbage', 'GarbageReport', 'GarbageLogger']
 __all__ = ['FakeObject', '_createGarbage', 'GarbageReport', 'GarbageLogger']
 
 
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.directnotify.DirectNotifyGlobal import directNotify
@@ -10,6 +12,7 @@ from direct.showbase.JobManagerGlobal import jobMgr
 from direct.showbase.MessengerGlobal import messenger
 from direct.showbase.MessengerGlobal import messenger
 from panda3d.core import ConfigVariableBool
 from panda3d.core import ConfigVariableBool
 import gc
 import gc
+from collections.abc import Callable
 
 
 GarbageCycleCountAnnounceEvent = 'announceGarbageCycleDesc2num'
 GarbageCycleCountAnnounceEvent = 'announceGarbageCycleDesc2num'
 
 
@@ -41,9 +44,21 @@ class GarbageReport(Job):
     If you just want to dump the report to the log, use GarbageLogger."""
     If you just want to dump the report to the log, use GarbageLogger."""
     notify = directNotify.newCategory("GarbageReport")
     notify = directNotify.newCategory("GarbageReport")
 
 
-    def __init__(self, name, log=True, verbose=False, fullReport=False, findCycles=True,
-                 threaded=False, doneCallback=None, autoDestroy=False, priority=None,
-                 safeMode=False, delOnly=False, collect=True):
+    def __init__(
+        self,
+        name: str,
+        log: bool = True,
+        verbose: bool = False,
+        fullReport: bool = False,
+        findCycles: bool = True,
+        threaded: bool = False,
+        doneCallback: Callable[[GarbageReport], object] | None = None,
+        autoDestroy: bool = False,
+        priority: int | None = None,
+        safeMode: bool = False,
+        delOnly: bool = False,
+        collect: bool = True
+    ) -> None:
         # if autoDestroy is True, GarbageReport will self-destroy after logging
         # if autoDestroy is True, GarbageReport will self-destroy after logging
         # if false, caller is responsible for calling destroy()
         # if false, caller is responsible for calling destroy()
         # if threaded is True, processing will be performed over multiple frames
         # if threaded is True, processing will be performed over multiple frames
@@ -399,7 +414,7 @@ class GarbageReport(Job):
         if self._args.autoDestroy:
         if self._args.autoDestroy:
             self.destroy()
             self.destroy()
 
 
-    def destroy(self):
+    def destroy(self) -> None:
         #print 'GarbageReport.destroy'
         #print 'GarbageReport.destroy'
         del self._args
         del self._args
         del self.garbage
         del self.garbage
@@ -417,13 +432,13 @@ class GarbageReport(Job):
             del self._reportStr
             del self._reportStr
         Job.destroy(self)
         Job.destroy(self)
 
 
-    def getNumCycles(self):
+    def getNumCycles(self) -> int:
         # if the job hasn't run yet, we don't have a numCycles yet
         # if the job hasn't run yet, we don't have a numCycles yet
         return self.numCycles
         return self.numCycles
 
 
-    def getDesc2numDict(self):
+    def getDesc2numDict(self) -> dict[str, int]:
         # dict of python-syntax leak -> number of that type of leak
         # dict of python-syntax leak -> number of that type of leak
-        desc2num = {}
+        desc2num: dict[str, int] = {}
         for cycleBySyntax in self.cyclesBySyntax:
         for cycleBySyntax in self.cyclesBySyntax:
             desc2num.setdefault(cycleBySyntax, 0)
             desc2num.setdefault(cycleBySyntax, 0)
             desc2num[cycleBySyntax] += 1
             desc2num[cycleBySyntax] += 1
@@ -563,7 +578,7 @@ class _CFGLGlobals:
     LastNumCycles = 0
     LastNumCycles = 0
 
 
 
 
-def checkForGarbageLeaks():
+def checkForGarbageLeaks() -> int:
     gc.collect()
     gc.collect()
     numGarbage = len(gc.garbage)
     numGarbage = len(gc.garbage)
     if numGarbage > 0 and ConfigVariableBool('auto-garbage-logging', False):
     if numGarbage > 0 and ConfigVariableBool('auto-garbage-logging', False):
@@ -576,6 +591,7 @@ def checkForGarbageLeaks():
             messenger.send(GarbageCycleCountAnnounceEvent, [gr.getDesc2numDict()])
             messenger.send(GarbageCycleCountAnnounceEvent, [gr.getDesc2numDict()])
             gr.destroy()
             gr.destroy()
         notify = directNotify.newCategory("GarbageDetect")
         notify = directNotify.newCategory("GarbageDetect")
+        func: Callable[[str], object]
         if ConfigVariableBool('allow-garbage-cycles', True):
         if ConfigVariableBool('allow-garbage-cycles', True):
             func = notify.warning
             func = notify.warning
         else:
         else:
@@ -584,7 +600,8 @@ def checkForGarbageLeaks():
     return numGarbage
     return numGarbage
 
 
 
 
-def b_checkForGarbageLeaks(wantReply=False):
+def b_checkForGarbageLeaks(wantReply: bool = False) -> int:
+    from direct.showbase.ShowBaseGlobal import base, __dev__
     if not __dev__:
     if not __dev__:
         return 0
         return 0
     # does a garbage collect on the client and the AI
     # does a garbage collect on the client and the AI
@@ -592,10 +609,10 @@ def b_checkForGarbageLeaks(wantReply=False):
     # logs leak info and terminates (if configured to do so)
     # logs leak info and terminates (if configured to do so)
     try:
     try:
         # if this is the client, tell the AI to check for leaks too
         # if this is the client, tell the AI to check for leaks too
-        base.cr.timeManager
+        base.cr.timeManager  # type: ignore[attr-defined]
     except Exception:
     except Exception:
         pass
         pass
     else:
     else:
-        if base.cr.timeManager:
-            base.cr.timeManager.d_checkForGarbageLeaks(wantReply=wantReply)
+        if base.cr.timeManager:  # type: ignore[attr-defined]
+            base.cr.timeManager.d_checkForGarbageLeaks(wantReply=wantReply)  # type: ignore[attr-defined]
     return checkForGarbageLeaks()
     return checkForGarbageLeaks()

+ 13 - 9
direct/src/showbase/JobManager.py

@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from collections.abc import Iterator
+
 from panda3d.core import ConfigVariableBool, ConfigVariableDouble, ClockObject
 from panda3d.core import ConfigVariableBool, ConfigVariableDouble, ClockObject
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.task.TaskManagerGlobal import taskMgr
 from direct.task.TaskManagerGlobal import taskMgr
@@ -17,28 +21,28 @@ class JobManager:
     # there's one task for the JobManager, all jobs run in this task
     # there's one task for the JobManager, all jobs run in this task
     TaskName = 'jobManager'
     TaskName = 'jobManager'
 
 
-    def __init__(self, timeslice=None):
+    def __init__(self, timeslice: float | None = None) -> None:
         # how long do we run per frame
         # how long do we run per frame
         self._timeslice = timeslice
         self._timeslice = timeslice
         # store the jobs in these structures to allow fast lookup by various keys
         # store the jobs in these structures to allow fast lookup by various keys
         # priority -> jobId -> job
         # priority -> jobId -> job
-        self._pri2jobId2job = {}
+        self._pri2jobId2job: dict[int, dict[int, Job]] = {}
         # priority -> chronological list of jobIds
         # priority -> chronological list of jobIds
-        self._pri2jobIds = {}
+        self._pri2jobIds: dict[int, list[int]] = {}
         # jobId -> priority
         # jobId -> priority
-        self._jobId2pri = {}
+        self._jobId2pri: dict[int, int] = {}
         # how many timeslices to give each job; this is used to efficiently implement
         # how many timeslices to give each job; this is used to efficiently implement
         # the relative job priorities
         # the relative job priorities
-        self._jobId2timeslices = {}
+        self._jobId2timeslices: dict[int, int] = {}
         # how much time did the job use beyond the allotted timeslice, used to balance
         # how much time did the job use beyond the allotted timeslice, used to balance
         # out CPU usage
         # out CPU usage
-        self._jobId2overflowTime = {}
-        self._useOverflowTime = None
+        self._jobId2overflowTime: dict[int, float] = {}
+        self._useOverflowTime: bool | None = None
         # this is a generator that we use to give high-priority jobs more timeslices,
         # this is a generator that we use to give high-priority jobs more timeslices,
         # it yields jobIds in a sequence that includes high-priority jobIds more often
         # it yields jobIds in a sequence that includes high-priority jobIds more often
         # than low-priority
         # than low-priority
-        self._jobIdGenerator = None
-        self._highestPriority = Job.Priorities.Normal
+        self._jobIdGenerator: Iterator[int] | None = None
+        self._highestPriority: int = Job.Priorities.Normal  # type: ignore[attr-defined]
 
 
     def destroy(self):
     def destroy(self):
         taskMgr.remove(JobManager.TaskName)
         taskMgr.remove(JobManager.TaskName)

+ 51 - 21
direct/src/showbase/Loader.py

@@ -2,9 +2,13 @@
 sound, music, shaders and fonts from disk.
 sound, music, shaders and fonts from disk.
 """
 """
 
 
+from __future__ import annotations
+
 __all__ = ['Loader']
 __all__ = ['Loader']
 
 
 from panda3d.core import (
 from panda3d.core import (
+    AsyncTask,
+    AudioManager,
     ConfigVariableBool,
     ConfigVariableBool,
     Filename,
     Filename,
     FontPool,
     FontPool,
@@ -24,13 +28,15 @@ from panda3d.core import (
 from panda3d.core import Loader as PandaLoader
 from panda3d.core import Loader as PandaLoader
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.showbase.DirectObject import DirectObject
 from direct.showbase.DirectObject import DirectObject
+from . import ShowBase
 import warnings
 import warnings
 import sys
 import sys
+from typing import Any, Callable, Iterable
 
 
 # You can specify a phaseChecker callback to check
 # You can specify a phaseChecker callback to check
 # a modelPath to see if it is being loaded in the correct
 # a modelPath to see if it is being loaded in the correct
 # phase
 # phase
-phaseChecker = None
+phaseChecker: Callable[[str, LoaderOptions], object] | None = None
 
 
 
 
 class Loader(DirectObject):
 class Loader(DirectObject):
@@ -49,16 +55,23 @@ class Loader(DirectObject):
         # This indicates that this class behaves like a Future.
         # This indicates that this class behaves like a Future.
         _asyncio_future_blocking = False
         _asyncio_future_blocking = False
 
 
-        def __init__(self, loader, numObjects, gotList, callback, extraArgs):
+        def __init__(
+            self,
+            loader: Loader | None,
+            numObjects: int,
+            gotList: bool,
+            callback: Callable[..., object] | None,
+            extraArgs: list,
+        ) -> None:
             self._loader = loader
             self._loader = loader
-            self.objects = [None] * numObjects
+            self.objects: list[Any | None] = [None] * numObjects
             self.gotList = gotList
             self.gotList = gotList
             self.callback = callback
             self.callback = callback
             self.extraArgs = extraArgs
             self.extraArgs = extraArgs
-            self.requests = set()
-            self.requestList = []
+            self.requests: set[AsyncTask] = set()
+            self.requestList: list[AsyncTask] = []
 
 
-        def gotObject(self, index, object):
+        def gotObject(self, index: int, object) -> None:
             self.objects[index] = object
             self.objects[index] = object
 
 
             if not self.requests:
             if not self.requests:
@@ -79,7 +92,7 @@ class Loader(DirectObject):
                 self.requests = None
                 self.requests = None
                 self.requestList = None
                 self.requestList = None
 
 
-        def cancelled(self):
+        def cancelled(self) -> bool:
             "Returns true if the request was cancelled."
             "Returns true if the request was cancelled."
             return self.requestList is None
             return self.requestList is None
 
 
@@ -87,7 +100,7 @@ class Loader(DirectObject):
             "Returns true if all the requests were finished or cancelled."
             "Returns true if all the requests were finished or cancelled."
             return not self.requests
             return not self.requests
 
 
-        def result(self):
+        def result(self) -> Any:
             "Returns the results, suspending the thread to wait if necessary."
             "Returns the results, suspending the thread to wait if necessary."
             for r in list(self.requests):
             for r in list(self.requests):
                 r.wait()
                 r.wait()
@@ -126,26 +139,26 @@ class Loader(DirectObject):
                 yield await req
                 yield await req
 
 
     # special methods
     # special methods
-    def __init__(self, base=None):
+    def __init__(self, base: ShowBase.ShowBase | None = None) -> None:
         self.base = base
         self.base = base
         self.loader = PandaLoader.getGlobalPtr()
         self.loader = PandaLoader.getGlobalPtr()
 
 
-        self._requests = {}
+        self._requests: dict[AsyncTask, tuple[Loader._Callback, int]] = {}
 
 
         self.hook = "async_loader_%s" % (Loader.loaderIndex)
         self.hook = "async_loader_%s" % (Loader.loaderIndex)
         Loader.loaderIndex += 1
         Loader.loaderIndex += 1
 
 
-    def destroy(self):
+    def destroy(self) -> None:
         self.ignore(self.hook)
         self.ignore(self.hook)
         self.loader.stopThreads()
         self.loader.stopThreads()
         del self.base
         del self.base
 
 
-    def _init_base(self, base):
+    def _init_base(self, base: ShowBase.ShowBase) -> None:
         self.base = base
         self.base = base
         self.accept(self.hook, self.__gotAsyncObject)
         self.accept(self.hook, self.__gotAsyncObject)
 
 
     @classmethod
     @classmethod
-    def _loadPythonFileTypes(cls):
+    def _loadPythonFileTypes(cls) -> None:
         if cls._loadedPythonFileTypes:
         if cls._loadedPythonFileTypes:
             return
             return
 
 
@@ -168,10 +181,18 @@ class Loader(DirectObject):
             cls._loadedPythonFileTypes = True
             cls._loadedPythonFileTypes = True
 
 
     # model loading funcs
     # model loading funcs
-    def loadModel(self, modelPath, loaderOptions = None, noCache = None,
-                  allowInstance = False, okMissing = None,
-                  callback = None, extraArgs = [], priority = None,
-                  blocking = None):
+    def loadModel(
+        self,
+        modelPath: str | list[str] | tuple[str, ...] | set[str],
+        loaderOptions: LoaderOptions | None = None,
+        noCache: bool | None = None,
+        allowInstance: bool = False,
+        okMissing: bool | None = None,
+        callback: Callable[..., object] | None = None,
+        extraArgs: list = [],
+        priority: int | None = None,
+        blocking: bool | None = None,
+    ) -> Any:
         """
         """
         Attempts to load a model or models from one or more relative
         Attempts to load a model or models from one or more relative
         pathnames.  If the input modelPath is a string (a single model
         pathnames.  If the input modelPath is a string (a single model
@@ -255,6 +276,7 @@ class Loader(DirectObject):
         if allowInstance:
         if allowInstance:
             loaderOptions.setFlags(loaderOptions.getFlags() | LoaderOptions.LFAllowInstance)
             loaderOptions.setFlags(loaderOptions.getFlags() | LoaderOptions.LFAllowInstance)
 
 
+        modelList: Iterable[str]
         if not isinstance(modelPath, (tuple, list, set)):
         if not isinstance(modelPath, (tuple, list, set)):
             # We were given a single model pathname.
             # We were given a single model pathname.
             modelList = [modelPath]
             modelList = [modelPath]
@@ -949,7 +971,7 @@ class Loader(DirectObject):
         TexturePool.releaseTexture(texture)
         TexturePool.releaseTexture(texture)
 
 
     # sound loading funcs
     # sound loading funcs
-    def loadSfx(self, *args, **kw):
+    def loadSfx(self, *args, **kw) -> Any:
         """Loads one or more sound files, specifically designated as a
         """Loads one or more sound files, specifically designated as a
         "sound effect" file (that is, uses the sfxManager to load the
         "sound effect" file (that is, uses the sfxManager to load the
         sound).  There is no distinction between sound effect files
         sound).  There is no distinction between sound effect files
@@ -959,6 +981,7 @@ class Loader(DirectObject):
         independently of the other group."""
         independently of the other group."""
 
 
         # showbase-created sfxManager should always be at front of list
         # showbase-created sfxManager should always be at front of list
+        assert self.base is not None
         if self.base.sfxManagerList:
         if self.base.sfxManagerList:
             return self.loadSound(self.base.sfxManagerList[0], *args, **kw)
             return self.loadSound(self.base.sfxManagerList[0], *args, **kw)
         return None
         return None
@@ -976,8 +999,14 @@ class Loader(DirectObject):
         else:
         else:
             return None
             return None
 
 
-    def loadSound(self, manager, soundPath, positional = False,
-                  callback = None, extraArgs = []):
+    def loadSound(
+        self,
+        manager: AudioManager,
+        soundPath: str | tuple[str, ...] | list[str] | set[str],
+        positional: bool = False,
+        callback: Callable[..., object] | None = None,
+        extraArgs: list = [],
+    ) -> Any:
         """Loads one or more sound files, specifying the particular
         """Loads one or more sound files, specifying the particular
         AudioManager that should be used to load them.  The soundPath
         AudioManager that should be used to load them.  The soundPath
         may be either a single filename, or a list of filenames.  If a
         may be either a single filename, or a list of filenames.  If a
@@ -987,6 +1016,7 @@ class Loader(DirectObject):
 
 
         from panda3d.core import AudioLoadRequest
         from panda3d.core import AudioLoadRequest
 
 
+        soundList: Iterable[str]
         if not isinstance(soundPath, (tuple, list, set)):
         if not isinstance(soundPath, (tuple, list, set)):
             # We were given a single sound pathname or a MovieAudio instance.
             # We were given a single sound pathname or a MovieAudio instance.
             soundList = [soundPath]
             soundList = [soundPath]
@@ -1112,7 +1142,7 @@ class Loader(DirectObject):
             else:
             else:
                 callback(*(origModelList + extraArgs))
                 callback(*(origModelList + extraArgs))
 
 
-    def __gotAsyncObject(self, request):
+    def __gotAsyncObject(self, request: AsyncTask) -> None:
         """A model or sound file or some such thing has just been
         """A model or sound file or some such thing has just been
         loaded asynchronously by the sub-thread.  Add it to the list
         loaded asynchronously by the sub-thread.  Add it to the list
         of loaded objects, and call the appropriate callback when it's
         of loaded objects, and call the appropriate callback when it's

+ 54 - 20
direct/src/showbase/Messenger.py

@@ -2,19 +2,40 @@
 :ref:`event handling <event-handlers>` that happens on the Python side.
 :ref:`event handling <event-handlers>` that happens on the Python side.
 """
 """
 
 
+from __future__ import annotations
+
 __all__ = ['Messenger']
 __all__ = ['Messenger']
 
 
+import types
+from collections.abc import Callable
+from typing import Protocol
+# These can be replaced with their builtin counterparts once support for Python 3.8 is dropped.
+from typing import Dict, Tuple
+
+from panda3d.core import AsyncTask
+
 from direct.stdpy.threading import Lock
 from direct.stdpy.threading import Lock
 from direct.directnotify import DirectNotifyGlobal
 from direct.directnotify import DirectNotifyGlobal
 from .PythonUtil import safeRepr
 from .PythonUtil import safeRepr
-import types
+
+# The following variables are typing constructs used in annotations
+# to succinctly express complex type structures.
+_ObjMsgrId = Tuple[str, int]
+_CallbackInfo = list  # [Callable, list, bool]
+_ListenerObject = list  # [int, DirectObject]
+_AcceptorDict = Dict[_ObjMsgrId, _CallbackInfo]
+_EventTuple = Tuple[_AcceptorDict, str, list, bool]
+
+
+class _HasMessengerID(Protocol):
+    _MSGRmessengerId: _ObjMsgrId
 
 
 
 
 class Messenger:
 class Messenger:
 
 
     notify = DirectNotifyGlobal.directNotify.newCategory("Messenger")
     notify = DirectNotifyGlobal.directNotify.newCategory("Messenger")
 
 
-    def __init__(self):
+    def __init__(self) -> None:
         """
         """
         One is keyed off the event name. It has the following structure::
         One is keyed off the event name. It has the following structure::
 
 
@@ -34,16 +55,16 @@ class Messenger:
             {'mouseDown': {avatar: [avatar.jump, [2.0], 1]}}
             {'mouseDown': {avatar: [avatar.jump, [2.0], 1]}}
         """
         """
         # eventName->objMsgrId->callbackInfo
         # eventName->objMsgrId->callbackInfo
-        self.__callbacks = {}
+        self.__callbacks: dict[str, _AcceptorDict] = {}
         # objMsgrId->set(eventName)
         # objMsgrId->set(eventName)
-        self.__objectEvents = {}
+        self.__objectEvents: dict[_ObjMsgrId, dict[str, None]] = {}
         self._messengerIdGen = 0
         self._messengerIdGen = 0
         # objMsgrId->listenerObject
         # objMsgrId->listenerObject
-        self._id2object = {}
+        self._id2object: dict[_ObjMsgrId, _ListenerObject] = {}
 
 
         # A mapping of taskChain -> eventList, used for sending events
         # A mapping of taskChain -> eventList, used for sending events
         # across task chains (and therefore across threads).
         # across task chains (and therefore across threads).
-        self._eventQueuesByTaskChain = {}
+        self._eventQueuesByTaskChain: dict[str, list[_EventTuple]] = {}
 
 
         # This protects the data structures within this object from
         # This protects the data structures within this object from
         # multithreaded access.
         # multithreaded access.
@@ -51,7 +72,7 @@ class Messenger:
 
 
         if __debug__:
         if __debug__:
             self.__isWatching=0
             self.__isWatching=0
-            self.__watching={}
+            self.__watching: dict[str, bool] = {}
         # I'd like this to be in the __debug__, but I fear that someone will
         # I'd like this to be in the __debug__, but I fear that someone will
         # want this in a release build.  If you're sure that that will not be
         # want this in a release build.  If you're sure that that will not be
         # then please remove this comment and put the quiet/verbose stuff
         # then please remove this comment and put the quiet/verbose stuff
@@ -62,7 +83,7 @@ class Messenger:
                        'collisionLoopFinished':1,
                        'collisionLoopFinished':1,
                        } # see def quiet()
                        } # see def quiet()
 
 
-    def _getMessengerId(self, object):
+    def _getMessengerId(self, object: _HasMessengerID) -> _ObjMsgrId:
         # TODO: allocate this id in DirectObject.__init__ and get derived
         # TODO: allocate this id in DirectObject.__init__ and get derived
         # classes to call down (speed optimization, assuming objects
         # classes to call down (speed optimization, assuming objects
         # accept/ignore more than once over their lifetime)
         # accept/ignore more than once over their lifetime)
@@ -73,7 +94,7 @@ class Messenger:
             self._messengerIdGen += 1
             self._messengerIdGen += 1
         return object._MSGRmessengerId
         return object._MSGRmessengerId
 
 
-    def _storeObject(self, object):
+    def _storeObject(self, object: _HasMessengerID) -> None:
         # store reference-counted reference to object in case we need to
         # store reference-counted reference to object in case we need to
         # retrieve it later.  assumes lock is held.
         # retrieve it later.  assumes lock is held.
         id = self._getMessengerId(object)
         id = self._getMessengerId(object)
@@ -82,7 +103,7 @@ class Messenger:
         else:
         else:
             self._id2object[id][0] += 1
             self._id2object[id][0] += 1
 
 
-    def _getObject(self, id):
+    def _getObject(self, id: _ObjMsgrId) -> _HasMessengerID:
         return self._id2object[id][1]
         return self._id2object[id][1]
 
 
     def _getObjects(self):
     def _getObjects(self):
@@ -101,7 +122,7 @@ class Messenger:
     def _getEvents(self):
     def _getEvents(self):
         return list(self.__callbacks.keys())
         return list(self.__callbacks.keys())
 
 
-    def _releaseObject(self, object):
+    def _releaseObject(self, object: _HasMessengerID) -> None:
         # assumes lock is held.
         # assumes lock is held.
         id = self._getMessengerId(object)
         id = self._getMessengerId(object)
         if id in self._id2object:
         if id in self._id2object:
@@ -117,7 +138,14 @@ class Messenger:
         from .EventManagerGlobal import eventMgr
         from .EventManagerGlobal import eventMgr
         return eventMgr.eventHandler.get_future(event)
         return eventMgr.eventHandler.get_future(event)
 
 
-    def accept(self, event, object, method, extraArgs=[], persistent=1):
+    def accept(
+        self,
+        event: str,
+        object: _HasMessengerID,
+        method: Callable,
+        extraArgs: list = [],
+        persistent: bool = True,
+    ) -> None:
         """ accept(self, string, DirectObject, Function, List, Boolean)
         """ accept(self, string, DirectObject, Function, List, Boolean)
 
 
         Make this object accept this event. When the event is
         Make this object accept this event. When the event is
@@ -174,7 +202,7 @@ class Messenger:
         finally:
         finally:
             self.lock.release()
             self.lock.release()
 
 
-    def ignore(self, event, object):
+    def ignore(self, event: str, object: _HasMessengerID) -> None:
         """ ignore(self, string, DirectObject)
         """ ignore(self, string, DirectObject)
         Make this object no longer respond to this event.
         Make this object no longer respond to this event.
         It is safe to call even if it was not already accepting
         It is safe to call even if it was not already accepting
@@ -208,7 +236,7 @@ class Messenger:
         finally:
         finally:
             self.lock.release()
             self.lock.release()
 
 
-    def ignoreAll(self, object):
+    def ignoreAll(self, object: _HasMessengerID) -> None:
         """
         """
         Make this object no longer respond to any events it was accepting
         Make this object no longer respond to any events it was accepting
         Useful for cleanup
         Useful for cleanup
@@ -283,7 +311,7 @@ class Messenger:
         """
         """
         return not self.isAccepting(event, object)
         return not self.isAccepting(event, object)
 
 
-    def send(self, event, sentArgs=[], taskChain=None):
+    def send(self, event: str, sentArgs: list = [], taskChain: str | None = None) -> None:
         """
         """
         Send this event, optionally passing in arguments.
         Send this event, optionally passing in arguments.
 
 
@@ -305,12 +333,12 @@ class Messenger:
 
 
         self.lock.acquire()
         self.lock.acquire()
         try:
         try:
-            foundWatch = 0
+            foundWatch = False
             if __debug__:
             if __debug__:
                 if self.__isWatching:
                 if self.__isWatching:
                     for i in self.__watching:
                     for i in self.__watching:
                         if str(event).find(i) >= 0:
                         if str(event).find(i) >= 0:
-                            foundWatch = 1
+                            foundWatch = True
                             break
                             break
             acceptorDict = self.__callbacks.get(event)
             acceptorDict = self.__callbacks.get(event)
             if not acceptorDict:
             if not acceptorDict:
@@ -336,7 +364,7 @@ class Messenger:
         finally:
         finally:
             self.lock.release()
             self.lock.release()
 
 
-    def __taskChainDispatch(self, taskChain, task):
+    def __taskChainDispatch(self, taskChain: str, task: AsyncTask) -> int:
         """ This task is spawned each time an event is sent across
         """ This task is spawned each time an event is sent across
         task chains.  Its job is to empty the task events on the queue
         task chains.  Its job is to empty the task events on the queue
         for this particular task chain.  This guarantees that events
         for this particular task chain.  This guarantees that events
@@ -365,7 +393,13 @@ class Messenger:
 
 
         return task.done
         return task.done
 
 
-    def __dispatch(self, acceptorDict, event, sentArgs, foundWatch):
+    def __dispatch(
+        self,
+        acceptorDict: _AcceptorDict,
+        event: str,
+        sentArgs: list,
+        foundWatch: bool,
+    ) -> None:
         for id in list(acceptorDict.keys()):
         for id in list(acceptorDict.keys()):
             # We have to make this apparently redundant check, because
             # We have to make this apparently redundant check, because
             # it is possible that one object removes its own hooks
             # it is possible that one object removes its own hooks
@@ -562,7 +596,7 @@ class Messenger:
                         break
                         break
         return matches
         return matches
 
 
-    def __methodRepr(self, method):
+    def __methodRepr(self, method: object) -> str:
         """
         """
         return string version of class.method or method.
         return string version of class.method or method.
         """
         """

+ 13 - 7
direct/src/showbase/OnScreenDebug.py

@@ -1,7 +1,11 @@
 """Contains the OnScreenDebug class."""
 """Contains the OnScreenDebug class."""
 
 
+from __future__ import annotations
+
 __all__ = ['OnScreenDebug']
 __all__ = ['OnScreenDebug']
 
 
+from typing import Any
+
 from panda3d.core import (
 from panda3d.core import (
     ConfigVariableBool,
     ConfigVariableBool,
     ConfigVariableDouble,
     ConfigVariableDouble,
@@ -18,13 +22,13 @@ class OnScreenDebug:
 
 
     enabled = ConfigVariableBool("on-screen-debug-enabled", False)
     enabled = ConfigVariableBool("on-screen-debug-enabled", False)
 
 
-    def __init__(self):
-        self.onScreenText = None
+    def __init__(self) -> None:
+        self.onScreenText: OnscreenText.OnscreenText | None = None
         self.frame = 0
         self.frame = 0
         self.text = ""
         self.text = ""
-        self.data = {}
+        self.data: dict[str, tuple[int, Any]] = {}
 
 
-    def load(self):
+    def load(self) -> None:
         if self.onScreenText:
         if self.onScreenText:
             return
             return
 
 
@@ -40,6 +44,7 @@ class OnScreenDebug:
         fgColor.setW(ConfigVariableDouble("on-screen-debug-fg-alpha", 0.85).value)
         fgColor.setW(ConfigVariableDouble("on-screen-debug-fg-alpha", 0.85).value)
         bgColor.setW(ConfigVariableDouble("on-screen-debug-bg-alpha", 0.85).value)
         bgColor.setW(ConfigVariableDouble("on-screen-debug-bg-alpha", 0.85).value)
 
 
+        from direct.showbase.ShowBaseGlobal import base
         font = base.loader.loadFont(fontPath)
         font = base.loader.loadFont(fontPath)
         if not font.isValid():
         if not font.isValid():
             print("failed to load OnScreenDebug font %s" % fontPath)
             print("failed to load OnScreenDebug font %s" % fontPath)
@@ -47,15 +52,16 @@ class OnScreenDebug:
         self.onScreenText = OnscreenText.OnscreenText(
         self.onScreenText = OnscreenText.OnscreenText(
                 parent = base.a2dTopLeft, pos = (0.0, -0.1),
                 parent = base.a2dTopLeft, pos = (0.0, -0.1),
                 fg=fgColor, bg=bgColor, scale = (fontScale, fontScale, 0.0),
                 fg=fgColor, bg=bgColor, scale = (fontScale, fontScale, 0.0),
-                align = TextNode.ALeft, mayChange = 1, font = font)
+                align = TextNode.ALeft, mayChange = True, font = font)
         # Make sure readout is never lit or drawn in wireframe
         # Make sure readout is never lit or drawn in wireframe
         DirectUtil.useDirectRenderStyle(self.onScreenText)
         DirectUtil.useDirectRenderStyle(self.onScreenText)
 
 
-    def render(self):
+    def render(self) -> None:
         if not self.enabled:
         if not self.enabled:
             return
             return
         if not self.onScreenText:
         if not self.onScreenText:
             self.load()
             self.load()
+        assert self.onScreenText is not None
         self.onScreenText.clearText()
         self.onScreenText.clearText()
         for k, v in sorted(self.data.items()):
         for k, v in sorted(self.data.items()):
             if v[0] == self.frame:
             if v[0] == self.frame:
@@ -75,7 +81,7 @@ class OnScreenDebug:
         self.onScreenText.appendText(self.text)
         self.onScreenText.appendText(self.text)
         self.frame += 1
         self.frame += 1
 
 
-    def clear(self):
+    def clear(self) -> None:
         self.text = ""
         self.text = ""
         if self.onScreenText:
         if self.onScreenText:
             self.onScreenText.clearText()
             self.onScreenText.clearText()

+ 43 - 25
direct/src/showbase/PythonUtil.py

@@ -41,13 +41,17 @@ import time
 import builtins
 import builtins
 import importlib
 import importlib
 import functools
 import functools
-from typing import Callable
+from collections.abc import Callable, Container, Iterable, Mapping
+from typing import Any, Generic, TypeVar
 
 
 __report_indent = 3
 __report_indent = 3
 
 
 from panda3d.core import ConfigVariableBool, ConfigVariableString, ConfigFlags
 from panda3d.core import ConfigVariableBool, ConfigVariableString, ConfigFlags
 from panda3d.core import ClockObject
 from panda3d.core import ClockObject
 
 
+_T = TypeVar('_T')
+_KT = TypeVar('_KT')
+_VT = TypeVar('_VT')
 
 
 ## with one integer positional arg, this uses about 4/5 of the memory of the Functor class below
 ## with one integer positional arg, this uses about 4/5 of the memory of the Functor class below
 #def Functor(function, *args, **kArgs):
 #def Functor(function, *args, **kArgs):
@@ -100,9 +104,9 @@ class Functor:
         return s
         return s
 
 
 
 
-class Stack:
-    def __init__(self):
-        self.__list = []
+class Stack(Generic[_T]):
+    def __init__(self) -> None:
+        self.__list: list[_T] = []
 
 
     def push(self, item):
     def push(self, item):
         self.__list.append(item)
         self.__list.append(item)
@@ -229,7 +233,7 @@ if __debug__:
 
 
     #-----------------------------------------------------------------------------
     #-----------------------------------------------------------------------------
 
 
-    def traceFunctionCall(frame):
+    def traceFunctionCall(frame: types.FrameType) -> str:
         """
         """
         return a string that shows the call frame with calling arguments.
         return a string that shows the call frame with calling arguments.
         e.g.
         e.g.
@@ -270,7 +274,7 @@ if __debug__:
                 r+="*** undefined ***"
                 r+="*** undefined ***"
         return r+')'
         return r+')'
 
 
-    def traceParentCall():
+    def traceParentCall() -> str:
         return traceFunctionCall(sys._getframe(2))
         return traceFunctionCall(sys._getframe(2))
 
 
     def printThisCall():
     def printThisCall():
@@ -411,7 +415,7 @@ def list2dict(L, value=None):
     return dict([(k, value) for k in L])
     return dict([(k, value) for k in L])
 
 
 
 
-def listToIndex2item(L):
+def listToIndex2item(L: Iterable[_VT]) -> dict[int, _VT]:
     """converts list to dict of list index->list item"""
     """converts list to dict of list index->list item"""
     d = {}
     d = {}
     for i, item in enumerate(L):
     for i, item in enumerate(L):
@@ -422,7 +426,7 @@ def listToIndex2item(L):
 assert listToIndex2item(['a','b']) == {0: 'a', 1: 'b',}
 assert listToIndex2item(['a','b']) == {0: 'a', 1: 'b',}
 
 
 
 
-def listToItem2index(L):
+def listToItem2index(L: Iterable[_KT]) -> dict[_KT, int]:
     """converts list to dict of list item->list index
     """converts list to dict of list item->list index
     This is lossy if there are duplicate list items"""
     This is lossy if there are duplicate list items"""
     d = {}
     d = {}
@@ -451,7 +455,7 @@ def invertDict(D, lossy=False):
     return n
     return n
 
 
 
 
-def invertDictLossless(D):
+def invertDictLossless(D: Mapping[_KT, _VT]) -> dict[_VT, list[_KT]]:
     """similar to invertDict, but values of new dict are lists of keys from
     """similar to invertDict, but values of new dict are lists of keys from
     old dict. No information is lost.
     old dict. No information is lost.
 
 
@@ -459,7 +463,7 @@ def invertDictLossless(D):
     >>> invertDictLossless(old)
     >>> invertDictLossless(old)
     {1: ['key1'], 2: ['key2', 'keyA']}
     {1: ['key1'], 2: ['key2', 'keyA']}
     """
     """
-    n = {}
+    n: dict[_VT, list[_KT]] = {}
     for key, value in D.items():
     for key, value in D.items():
         n.setdefault(value, [])
         n.setdefault(value, [])
         n[value].append(key)
         n[value].append(key)
@@ -706,7 +710,7 @@ if __debug__:
     movedDumpFuncs: list[Callable] = []
     movedDumpFuncs: list[Callable] = []
     movedLoadFuncs: list[Callable] = []
     movedLoadFuncs: list[Callable] = []
     profileFilenames = set()
     profileFilenames = set()
-    profileFilenameList = Stack()
+    profileFilenameList = Stack[str]()
     profileFilename2file = {}
     profileFilename2file = {}
     profileFilename2marshalData = {}
     profileFilename2marshalData = {}
 
 
@@ -998,7 +1002,7 @@ def lineupPos(i, num, spacing):
     return pos - ((float(spacing) * (num-1))/2.)
     return pos - ((float(spacing) * (num-1))/2.)
 
 
 
 
-def formatElapsedSeconds(seconds):
+def formatElapsedSeconds(seconds: float) -> str:
     """
     """
     Returns a string of the form "mm:ss" or "hh:mm:ss" or "n days",
     Returns a string of the form "mm:ss" or "hh:mm:ss" or "n days",
     representing the indicated elapsed time in seconds.
     representing the indicated elapsed time in seconds.
@@ -1278,7 +1282,7 @@ def randInt32(rng=random.random):
 class SerialNumGen:
 class SerialNumGen:
     """generates serial numbers"""
     """generates serial numbers"""
 
 
-    def __init__(self, start=None):
+    def __init__(self, start: int | None = None) -> None:
         if start is None:
         if start is None:
             start = 0
             start = 0
         self.__counter = start-1
         self.__counter = start-1
@@ -1305,7 +1309,7 @@ class SerialMaskedGen(SerialNumGen):
 _serialGen = SerialNumGen()
 _serialGen = SerialNumGen()
 
 
 
 
-def serialNum():
+def serialNum() -> int:
     return _serialGen.next()
     return _serialGen.next()
 
 
 
 
@@ -1404,14 +1408,16 @@ def _getSafeReprNotify():
     return safeReprNotify
     return safeReprNotify
 
 
 
 
-def safeRepr(obj):
+def safeRepr(obj: object) -> str:
     global dtoolSuperBase
     global dtoolSuperBase
     if dtoolSuperBase is None:
     if dtoolSuperBase is None:
         _getDtoolSuperBase()
         _getDtoolSuperBase()
+    assert dtoolSuperBase is not None
 
 
     global safeReprNotify
     global safeReprNotify
     if safeReprNotify is None:
     if safeReprNotify is None:
         _getSafeReprNotify()
         _getSafeReprNotify()
+    assert safeReprNotify is not None
 
 
     if isinstance(obj, dtoolSuperBase):
     if isinstance(obj, dtoolSuperBase):
         # repr of C++ object could crash, particularly if the object has been deleted
         # repr of C++ object could crash, particularly if the object has been deleted
@@ -1443,7 +1449,12 @@ def safeReprTypeOnFail(obj):
         return '<** FAILED REPR OF %s instance at %s **>' % (obj.__class__.__name__, hex(id(obj)))
         return '<** FAILED REPR OF %s instance at %s **>' % (obj.__class__.__name__, hex(id(obj)))
 
 
 
 
-def fastRepr(obj, maxLen=200, strFactor=10, _visitedIds=None):
+def fastRepr(
+    obj: object,
+    maxLen: int = 200,
+    strFactor: int = 10,
+    _visitedIds: set[int] | None = None,
+) -> str:
     """ caps the length of iterable types, so very large objects will print faster.
     """ caps the length of iterable types, so very large objects will print faster.
     also prevents infinite recursion """
     also prevents infinite recursion """
     try:
     try:
@@ -1452,9 +1463,9 @@ def fastRepr(obj, maxLen=200, strFactor=10, _visitedIds=None):
         if id(obj) in _visitedIds:
         if id(obj) in _visitedIds:
             return '<ALREADY-VISITED %s>' % itype(obj)
             return '<ALREADY-VISITED %s>' % itype(obj)
         if type(obj) in (tuple, list):
         if type(obj) in (tuple, list):
+            assert isinstance(obj, (tuple, list))
             s = ''
             s = ''
-            s += {tuple: '(',
-                  list:  '[',}[type(obj)]
+            s += '(' if type(obj) == tuple else '['
             if maxLen is not None and len(obj) > maxLen:
             if maxLen is not None and len(obj) > maxLen:
                 o = obj[:maxLen]
                 o = obj[:maxLen]
                 ellips = '...'
                 ellips = '...'
@@ -1467,8 +1478,7 @@ def fastRepr(obj, maxLen=200, strFactor=10, _visitedIds=None):
                 s += ', '
                 s += ', '
             _visitedIds.remove(id(obj))
             _visitedIds.remove(id(obj))
             s += ellips
             s += ellips
-            s += {tuple: ')',
-                  list:  ']',}[type(obj)]
+            s += ')' if type(obj) == tuple else ']'
             return s
             return s
         elif type(obj) is dict:
         elif type(obj) is dict:
             s = '{'
             s = '{'
@@ -1588,7 +1598,7 @@ class ScratchPad:
 class Sync:
 class Sync:
     _SeriesGen = SerialNumGen()
     _SeriesGen = SerialNumGen()
 
 
-    def __init__(self, name, other=None):
+    def __init__(self, name: str, other: Sync | None = None) -> None:
         self._name = name
         self._name = name
         if other is None:
         if other is None:
             self._series = self._SeriesGen.next()
             self._series = self._SeriesGen.next()
@@ -1620,7 +1630,7 @@ class Sync:
                               self._name, self._series, self._value)
                               self._name, self._series, self._value)
 
 
 
 
-def itype(obj):
+def itype(obj: object) -> type | str:
     # version of type that gives more complete information about instance types
     # version of type that gives more complete information about instance types
     global dtoolSuperBase
     global dtoolSuperBase
     t = type(obj)
     t = type(obj)
@@ -1628,6 +1638,7 @@ def itype(obj):
     # check if this is a C++ object
     # check if this is a C++ object
     if dtoolSuperBase is None:
     if dtoolSuperBase is None:
         _getDtoolSuperBase()
         _getDtoolSuperBase()
+    assert dtoolSuperBase is not None
     if isinstance(obj, dtoolSuperBase):
     if isinstance(obj, dtoolSuperBase):
         return "<type 'instance' of %s>" % (obj.__class__)
         return "<type 'instance' of %s>" % (obj.__class__)
     return t
     return t
@@ -1972,7 +1983,13 @@ def pstatcollect(scope, level = None):
 __report_indent = 0
 __report_indent = 0
 
 
 
 
-def report(types = [], prefix = '', xform = None, notifyFunc = None, dConfigParam = []):
+def report(
+    types: Container[str] = [],
+    prefix: str = '',
+    xform: Callable[[Any], object] | None = None,
+    notifyFunc: Callable[[str], object] | None = None,
+    dConfigParam: str | list[str] | tuple[str, ...] = [],
+) -> Callable[[_T], _T]:
     """
     """
     This is a decorator generating function.  Use is similar to
     This is a decorator generating function.  Use is similar to
     a @decorator, except you must be sure to call it as a function.
     a @decorator, except you must be sure to call it as a function.
@@ -2031,7 +2048,7 @@ def report(types = [], prefix = '', xform = None, notifyFunc = None, dConfigPara
         return f
         return f
 
 
     try:
     try:
-        if not __dev__ and not ConfigVariableBool('force-reports', False):
+        if not __dev__ and not ConfigVariableBool('force-reports', False):  # type: ignore[name-defined]
             return decorator
             return decorator
 
 
         # determine whether we should use the decorator
         # determine whether we should use the decorator
@@ -2041,6 +2058,7 @@ def report(types = [], prefix = '', xform = None, notifyFunc = None, dConfigPara
         if not dConfigParam:
         if not dConfigParam:
             doPrint = True
             doPrint = True
         else:
         else:
+            dConfigParams: list[str] | tuple[str, ...]
             if not isinstance(dConfigParam, (list,tuple)):
             if not isinstance(dConfigParam, (list,tuple)):
                 dConfigParams = (dConfigParam,)
                 dConfigParams = (dConfigParam,)
             else:
             else:
@@ -2070,7 +2088,7 @@ def report(types = [], prefix = '', xform = None, notifyFunc = None, dConfigPara
 
 
     globalClockDelta = importlib.import_module("direct.distributed.ClockDelta").globalClockDelta
     globalClockDelta = importlib.import_module("direct.distributed.ClockDelta").globalClockDelta
 
 
-    def decorator(f):
+    def decorator(f):  # type: ignore[no-redef]
         def wrap(*args, **kwargs):
         def wrap(*args, **kwargs):
             if args:
             if args:
                 rArgs = [args[0].__class__.__name__ + ', ']
                 rArgs = [args[0].__class__.__name__ + ', ']

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

@@ -62,6 +62,7 @@ from panda3d.core import (
     DepthTestAttrib,
     DepthTestAttrib,
     DepthWriteAttrib,
     DepthWriteAttrib,
     DriveInterface,
     DriveInterface,
+    EventQueue,
     ExecutionEnvironment,
     ExecutionEnvironment,
     Filename,
     Filename,
     FisheyeMaker,
     FisheyeMaker,
@@ -114,7 +115,7 @@ from panda3d.core import (
     WindowProperties,
     WindowProperties,
     getModelPath,
     getModelPath,
 )
 )
-from panda3d.direct import throw_new_frame, init_app_for_gui
+from panda3d.direct import init_app_for_gui
 from panda3d.direct import storeAccessibilityShortcutKeys, allowAccessibilityShortcutKeys
 from panda3d.direct import storeAccessibilityShortcutKeys, allowAccessibilityShortcutKeys
 from . import DConfig
 from . import DConfig
 
 
@@ -177,6 +178,8 @@ class ShowBase(DirectObject.DirectObject):
     aspect2d: NodePath
     aspect2d: NodePath
     pixel2d: NodePath
     pixel2d: NodePath
 
 
+    a2dTopLeft: NodePath
+
     cluster: Any | None
     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:
@@ -382,7 +385,7 @@ class ShowBase(DirectObject.DirectObject):
         #: yourself every frame.
         #: yourself every frame.
         self.cTrav: CollisionTraverser | Literal[0] = 0
         self.cTrav: CollisionTraverser | Literal[0] = 0
         self.shadowTrav: CollisionTraverser | Literal[0] = 0
         self.shadowTrav: CollisionTraverser | Literal[0] = 0
-        self.cTravStack = Stack()
+        self.cTravStack = Stack[CollisionTraverser]()
         # Ditto for an AppTraverser.
         # Ditto for an AppTraverser.
         self.appTrav: Any | Literal[0] = 0
         self.appTrav: Any | Literal[0] = 0
 
 
@@ -438,7 +441,7 @@ class ShowBase(DirectObject.DirectObject):
         self.useTrackball()
         self.useTrackball()
 
 
         #: `.Loader.Loader` object.
         #: `.Loader.Loader` object.
-        self.loader = ShowBaseGlobal.loader
+        self.loader: Loader.Loader = ShowBaseGlobal.loader
         self.loader._init_base(self)
         self.loader._init_base(self)
         self.graphicsEngine.setDefaultLoader(self.loader.loader)
         self.graphicsEngine.setDefaultLoader(self.loader.loader)
 
 
@@ -654,7 +657,7 @@ class ShowBase(DirectObject.DirectObject):
     def getExitErrorCode(self):
     def getExitErrorCode(self):
         return 0
         return 0
 
 
-    def printEnvDebugInfo(self):
+    def printEnvDebugInfo(self) -> None:
         """Print some information about the environment that we are running
         """Print some information about the environment that we are running
         in.  Stuff like the model paths and other paths.  Feel free to
         in.  Stuff like the model paths and other paths.  Feel free to
         add stuff to this.
         add stuff to this.
@@ -2282,9 +2285,8 @@ class ShowBase(DirectObject.DirectObject):
             # now until someone complains.
             # now until someone complains.
             time.sleep(0.1)
             time.sleep(0.1)
 
 
-        # Lerp stuff needs this event, and it must be generated in
-        # C++, not in Python.
-        throw_new_frame()
+        # Lerp stuff needs this event, thrown directly on the C++ queue.
+        EventQueue.getGlobalEventQueue().queueEvent("NewFrame")
         return Task.cont
         return Task.cont
 
 
     def __igLoopSync(self, state: object) -> int:
     def __igLoopSync(self, state: object) -> int:
@@ -2330,9 +2332,8 @@ class ShowBase(DirectObject.DirectObject):
         self.cluster.waitForFlipCommand()
         self.cluster.waitForFlipCommand()
         self.graphicsEngine.flipFrame()
         self.graphicsEngine.flipFrame()
 
 
-        # Lerp stuff needs this event, and it must be generated in
-        # C++, not in Python.
-        throw_new_frame()
+        # Lerp stuff needs this event, thrown directly on the C++ queue.
+        EventQueue.getGlobalEventQueue().queueEvent("NewFrame")
         return Task.cont
         return Task.cont
 
 
     def restart(self, clusterSync: bool = False, cluster=None) -> None:
     def restart(self, clusterSync: bool = False, cluster=None) -> None:

+ 191 - 447
direct/src/showbase/VFSImporter.py

@@ -5,477 +5,267 @@ Calling the :func:`register()` function to register the import hooks should be
 sufficient to enable this functionality.
 sufficient to enable this functionality.
 """
 """
 
 
-__all__ = ['register', 'sharedPackages',
-           'reloadSharedPackage', 'reloadSharedPackages']
+from __future__ import annotations
 
 
-from panda3d.core import Filename, VirtualFileSystem, VirtualFileMountSystem, OFileStream, copyStream
-from direct.stdpy.file import open
+__all__ = ['register']
+
+from panda3d.core import Filename, VirtualFile, VirtualFileSystem, VirtualFileMountSystem
+from panda3d.core import OFileStream, copy_stream
 import sys
 import sys
 import marshal
 import marshal
-import imp
-import types
-
-#: The sharedPackages dictionary lists all of the "shared packages",
-#: special Python packages that automatically span multiple directories
-#: via magic in the VFSImporter.  You can make a package "shared"
-#: simply by adding its name into this dictionary (and then calling
-#: reloadSharedPackages() if it's already been imported).
-#:
-#: When a package name is in this dictionary at import time, *all*
-#: instances of the package are located along sys.path, and merged into
-#: a single Python module with a __path__ setting that represents the
-#: union.  Thus, you can have a direct.showbase.foo in your own
-#: application, and loading it won't shadow the system
-#: direct.showbase.ShowBase which is in a different directory on disk.
-sharedPackages = {}
-
-vfs = VirtualFileSystem.getGlobalPtr()
-
-compiledExtensions = ['pyc', 'pyo']
-if not __debug__:
-    # In optimized mode, we prefer loading .pyo files over .pyc files.
-    # We implement that by reversing the extension names.
-    compiledExtensions = ['pyo', 'pyc']
-
-
-class VFSImporter:
+import _imp
+import atexit
+from importlib.abc import Loader, SourceLoader
+from importlib.util import MAGIC_NUMBER, decode_source
+from importlib.machinery import ModuleSpec, EXTENSION_SUFFIXES, BYTECODE_SUFFIXES
+from types import ModuleType
+from typing import Any
+
+vfs = VirtualFileSystem.get_global_ptr()
+
+
+def _make_spec(fullname: str, loader: VFSLoader, *, is_package: bool) -> ModuleSpec:
+    filename = loader._vfile.get_filename()
+    spec = ModuleSpec(fullname, loader, origin=filename.to_os_specific(), is_package=is_package)
+    if is_package:
+        assert spec.submodule_search_locations is not None
+        spec.submodule_search_locations.append(Filename(filename.get_dirname()).to_os_specific())
+    spec.has_location = True
+    return spec
+
+
+class VFSFinder:
     """ This class serves as a Python importer to support loading
     """ This class serves as a Python importer to support loading
     Python .py and .pyc/.pyo files from Panda's Virtual File System,
     Python .py and .pyc/.pyo files from Panda's Virtual File System,
     which allows loading Python source files from mounted .mf files
     which allows loading Python source files from mounted .mf files
     (among other places). """
     (among other places). """
 
 
-    def __init__(self, path):
-        if isinstance(path, Filename):
-            self.dir_path = Filename(path)
-        else:
-            self.dir_path = Filename.fromOsSpecific(path)
+    def __init__(self, path: str) -> None:
+        self.path = path
 
 
-    def find_module(self, fullname, path = None):
-        if path is None:
-            dir_path = self.dir_path
-        else:
-            dir_path = path
-        #print >>sys.stderr, "find_module(%s), dir_path = %s" % (fullname, dir_path)
+    def find_spec(self, fullname: str, target: ModuleType | None = None) -> ModuleSpec | None:
+        #print(f"find_spec({fullname}), dir_path = {dir_path}", file=sys.stderr)
         basename = fullname.split('.')[-1]
         basename = fullname.split('.')[-1]
-        path = Filename(dir_path, basename)
+        filename = Filename(Filename.from_os_specific(self.path), basename)
+
+        loader: VFSLoader
 
 
         # First, look for Python files.
         # First, look for Python files.
-        filename = Filename(path)
-        filename.setExtension('py')
-        vfile = vfs.getFile(filename, True)
+        vfile = vfs.get_file(filename + '.py', True)
         if vfile:
         if vfile:
-            return VFSLoader(dir_path, vfile, filename,
-                             desc=('.py', 'r', imp.PY_SOURCE))
+            loader = VFSSourceLoader(fullname, vfile)
+            return _make_spec(fullname, loader, is_package=False)
 
 
         # If there's no .py file, but there's a .pyc file, load that
         # If there's no .py file, but there's a .pyc file, load that
         # anyway.
         # anyway.
-        for ext in compiledExtensions:
-            filename = Filename(path)
-            filename.setExtension(ext)
-            vfile = vfs.getFile(filename, True)
+        for suffix in BYTECODE_SUFFIXES:
+            vfile = vfs.get_file(filename + suffix, True)
             if vfile:
             if vfile:
-                return VFSLoader(dir_path, vfile, filename,
-                                 desc=('.'+ext, 'rb', imp.PY_COMPILED))
+                loader = VFSCompiledLoader(fullname, vfile)
+                return _make_spec(fullname, loader, is_package=False)
 
 
         # Look for a C/C++ extension module.
         # Look for a C/C++ extension module.
-        for desc in imp.get_suffixes():
-            if desc[2] != imp.C_EXTENSION:
-                continue
-
-            filename = Filename(path + desc[0])
-            vfile = vfs.getFile(filename, True)
+        for suffix in EXTENSION_SUFFIXES:
+            vfile = vfs.get_file(filename + suffix, True)
             if vfile:
             if vfile:
-                return VFSLoader(dir_path, vfile, filename, desc=desc)
+                loader = VFSExtensionLoader(fullname, vfile)
+                return _make_spec(fullname, loader, is_package=False)
 
 
-        # Finally, consider a package, i.e. a directory containing
-        # __init__.py.
-        filename = Filename(path, '__init__.py')
-        vfile = vfs.getFile(filename, True)
+        # Consider a package, i.e. a directory containing __init__.py.
+        init_filename = Filename(filename, '__init__.py')
+        vfile = vfs.get_file(init_filename, True)
         if vfile:
         if vfile:
-            return VFSLoader(dir_path, vfile, filename, packagePath=path,
-                             desc=('.py', 'r', imp.PY_SOURCE))
-        for ext in compiledExtensions:
-            filename = Filename(path, '__init__.' + ext)
-            vfile = vfs.getFile(filename, True)
+            loader = VFSSourceLoader(fullname, vfile)
+            return _make_spec(fullname, loader, is_package=True)
+
+        for suffix in BYTECODE_SUFFIXES:
+            init_filename = Filename(filename, '__init__' + suffix)
+            vfile = vfs.get_file(init_filename, True)
             if vfile:
             if vfile:
-                return VFSLoader(dir_path, vfile, filename, packagePath=path,
-                                 desc=('.'+ext, 'rb', imp.PY_COMPILED))
+                loader = VFSCompiledLoader(fullname, vfile)
+                return _make_spec(fullname, loader, is_package=True)
+
+        # Consider a namespace package.
+        if vfs.is_directory(filename):
+            spec = ModuleSpec(fullname, VFSNamespaceLoader(), is_package=True)
+            assert spec.submodule_search_locations is not None
+            spec.submodule_search_locations.append(filename.to_os_specific())
+            return spec
 
 
-        #print >>sys.stderr, "not found."
+        #print("not found.", file=sys.stderr)
         return None
         return None
 
 
 
 
-class VFSLoader:
-    """ The second part of VFSImporter, this is created for a
-    particular .py file or directory. """
+class VFSLoader(Loader):
+    def __init__(self, fullname: str, vfile: VirtualFile) -> None:
+        self.name = fullname
+        self._vfile = vfile
+
+    def is_package(self, fullname):
+        if fullname is not None and self.name != fullname:
+            raise ImportError
+
+        filename = self._vfile.get_filename().get_basename()
+        filename_base = filename.rsplit('.', 1)[0]
+        tail_name = fullname.rpartition('.')[2]
+        return filename_base == '__init__' and tail_name != '__init__'
+
+    def create_module(self, spec: ModuleSpec) -> ModuleType | None:
+        """Use default semantics for module creation."""
 
 
-    def __init__(self, dir_path, vfile, filename, desc, packagePath=None):
-        self.dir_path = dir_path
-        self.timestamp = None
+    def exec_module(self, module: ModuleType) -> None:
+        """Execute the module."""
+        code = self.get_code(module.__name__)  # type: ignore[attr-defined]
+        exec(code, module.__dict__)
+
+    def get_filename(self, fullname: str) -> str:
+        if fullname is not None and self.name != fullname:
+            raise ImportError
+
+        return self._vfile.get_filename().to_os_specific()
+
+    @staticmethod
+    def get_data(path: str) -> bytes:
+        vfile = vfs.get_file(Filename.from_os_specific(path))
         if vfile:
         if vfile:
-            self.timestamp = vfile.getTimestamp()
-        self.filename = filename
-        self.desc = desc
-        self.packagePath = packagePath
-
-    def load_module(self, fullname, loadingShared = False):
-        #print >>sys.stderr, "load_module(%s), dir_path = %s, filename = %s" % (fullname, self.dir_path, self.filename)
-        if self.desc[2] == imp.PY_FROZEN:
-            return self._import_frozen_module(fullname)
-        if self.desc[2] == imp.C_EXTENSION:
-            return self._import_extension_module(fullname)
-
-        # Check if this is a child of a shared package.
-        if not loadingShared and self.packagePath and '.' in fullname:
-            parentname = fullname.rsplit('.', 1)[0]
-            if parentname in sharedPackages:
-                # It is.  That means it's a shared package too.
-                parent = sys.modules[parentname]
-                path = getattr(parent, '__path__', None)
-                importer = VFSSharedImporter()
-                sharedPackages[fullname] = True
-                loader = importer.find_module(fullname, path = path)
-                assert loader
-                return loader.load_module(fullname)
-
-        code = self._read_code()
-        if not code:
-            raise ImportError('No Python code in %s' % (fullname))
-
-        mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
-        mod.__file__ = self.filename.toOsSpecific()
-        mod.__loader__ = self
-        if self.packagePath:
-            mod.__path__ = [self.packagePath.toOsSpecific()]
-            #print >> sys.stderr, "loaded %s, path = %s" % (fullname, mod.__path__)
-
-        exec(code, mod.__dict__)
-        return sys.modules[fullname]
-
-    def getdata(self, path):
-        path = Filename(self.dir_path, Filename.fromOsSpecific(path))
-        vfile = vfs.getFile(path)
-        if not vfile:
-            raise IOError("Could not find '%s'" % (path))
-        return vfile.readFile(True)
+            return vfile.read_file(True)
+        else:
+            raise OSError
 
 
-    def is_package(self, fullname):
-        return bool(self.packagePath)
+    @staticmethod
+    def path_stats(path: str) -> dict[str, Any]:
+        vfile = vfs.get_file(Filename.from_os_specific(path))
+        if vfile:
+            return {'mtime': vfile.get_timestamp(), 'size': vfile.get_file_size()}
+        else:
+            raise OSError
 
 
-    def get_code(self, fullname):
-        return self._read_code()
+    @staticmethod
+    def path_mtime(path):
+        vfile = vfs.get_file(Filename.from_os_specific(path))
+        if vfile:
+            return vfile.get_timestamp()
+        else:
+            raise OSError
 
 
+
+class VFSSourceLoader(VFSLoader, SourceLoader): # type: ignore[misc]
     def get_source(self, fullname):
     def get_source(self, fullname):
-        return self._read_source()
+        if fullname is not None and self.name != fullname:
+            raise ImportError
 
 
-    def get_filename(self, fullname):
-        return self.filename.toOsSpecific()
+        return decode_source(self._vfile.read_file(True))
 
 
-    def _read_source(self):
-        """ Returns the Python source for this file, if it is
-        available, or None if it is not.  May raise IOError. """
 
 
-        if self.desc[2] == imp.PY_COMPILED or \
-           self.desc[2] == imp.C_EXTENSION:
-            return None
+class VFSCompiledLoader(VFSLoader):
+    def get_code(self, fullname):
+        if fullname is not None and self.name != fullname:
+            raise ImportError
 
 
-        filename = Filename(self.filename)
-        filename.setExtension('py')
-        filename.setText()
+        vfile = self._vfile
+        data = vfile.read_file(True)
+        if data[:4] != MAGIC_NUMBER:
+            raise ImportError("Bad magic number in %s" % (vfile))
 
 
-        # Use the tokenize module to detect the encoding.
-        import tokenize
-        fh = open(self.filename, 'rb')
-        encoding, lines = tokenize.detect_encoding(fh.readline)
-        return (b''.join(lines) + fh.read()).decode(encoding)
+        return marshal.loads(data[16:])
+
+    def get_source(self, fullname):
+        return None
 
 
-    def _import_extension_module(self, fullname):
-        """ Loads the binary shared object as a Python module, and
-        returns it. """
 
 
-        vfile = vfs.getFile(self.filename, False)
+class VFSExtensionLoader(VFSLoader):
+    def create_module(self, spec):
+        vfile = self._vfile
+        filename = vfile.get_filename()
 
 
         # We can only import an extension module if it already exists on
         # We can only import an extension module if it already exists on
         # disk.  This means if it's a truly virtual file that has no
         # disk.  This means if it's a truly virtual file that has no
         # on-disk equivalent, we have to write it to a temporary file
         # on-disk equivalent, we have to write it to a temporary file
         # first.
         # first.
-        if hasattr(vfile, 'getMount') and \
-           isinstance(vfile.getMount(), VirtualFileMountSystem):
+        if isinstance(vfile.get_mount(), VirtualFileMountSystem):
             # It's a real file.
             # It's a real file.
-            filename = self.filename
-        elif self.filename.exists():
+            pass
+        elif filename.exists():
             # It's a virtual file, but it's shadowing a real file in
             # It's a virtual file, but it's shadowing a real file in
             # the same directory.  Assume they're the same, and load
             # the same directory.  Assume they're the same, and load
             # the real one.
             # the real one.
-            filename = self.filename
+            pass
         else:
         else:
             # It's a virtual file with no real-world existence.  Dump
             # It's a virtual file with no real-world existence.  Dump
-            # it to disk.  TODO: clean up this filename.
-            filename = Filename.temporary('', self.filename.getBasenameWoExtension(),
-                                          '.' + self.filename.getExtension(),
-                                          type = Filename.TDso)
-            filename.setExtension(self.filename.getExtension())
-            filename.setBinary()
-            sin = vfile.openReadFile(True)
-            sout = OFileStream()
-            if not filename.openWrite(sout):
-                raise IOError
-            if not copyStream(sin, sout):
-                raise IOError
-            vfile.closeReadFile(sin)
-            del sout
-
-        module = imp.load_module(fullname, None, filename.toOsSpecific(),
-                                 self.desc)
-        module.__file__ = self.filename.toOsSpecific()
-        return module
-
-    def _import_frozen_module(self, fullname):
-        """ Imports the frozen module without messing around with
-        searching any more. """
-        #print >>sys.stderr, "importing frozen %s" % (fullname)
-        module = imp.load_module(fullname, None, fullname,
-                                 ('', '', imp.PY_FROZEN))
-        module.__path__ = []
-        return module
-
-    def _read_code(self):
-        """ Returns the Python compiled code object for this file, if
-        it is available, or None if it is not.  May raise IOError,
-        ValueError, SyntaxError, or a number of other errors generated
-        by the low-level system. """
-
-        if self.desc[2] == imp.PY_COMPILED:
-            # It's a pyc file; just read it directly.
-            pycVfile = vfs.getFile(self.filename, False)
-            if pycVfile:
-                return self._loadPyc(pycVfile, None)
-            raise IOError('Could not read %s' % (self.filename))
-
-        elif self.desc[2] == imp.C_EXTENSION:
-            return None
-
-        # It's a .py file (or an __init__.py file; same thing).  Read
-        # the .pyc file if it is available and current; otherwise read
-        # the .py file and compile it.
-        t_pyc = None
-        for ext in compiledExtensions:
-            pycFilename = Filename(self.filename)
-            pycFilename.setExtension(ext)
-            pycVfile = vfs.getFile(pycFilename, False)
-            if pycVfile:
-                t_pyc = pycVfile.getTimestamp()
-                break
-
-        code = None
-        if t_pyc and t_pyc >= self.timestamp:
+            # it to disk.
+            ext = filename.get_extension()
+            tmp_filename = Filename.temporary('', filename.get_basename_wo_extension(),
+                                              '.' + ext,
+                                              type = Filename.T_dso)
+            tmp_filename.set_extension(ext)
+            tmp_filename.set_binary()
+            sin = vfile.open_read_file(True)
             try:
             try:
-                code = self._loadPyc(pycVfile, self.timestamp)
-            except ValueError:
-                code = None
+                sout = OFileStream()
+                if not tmp_filename.open_write(sout):
+                    raise IOError
+                if not copy_stream(sin, sout):
+                    raise IOError
+            finally:
+                vfile.close_read_file(sin)
+            del sout
 
 
-        if not code:
-            source = self._read_source()
-            filename = Filename(self.filename)
-            filename.setExtension('py')
-            code = self._compile(filename, source)
+            # Delete when the process ends.
+            atexit.register(tmp_filename.unlink)
 
 
-        return code
+            # Make a dummy spec to pass to create_dynamic with the path to
+            # our temporary file.
+            spec = ModuleSpec(spec.name, spec.loader,
+                              origin=tmp_filename.to_os_specific(),
+                              is_package=False)
 
 
-    def _loadPyc(self, vfile, timestamp):
-        """ Reads and returns the marshal data from a .pyc file.
-        Raises ValueError if there is a problem. """
+        module = _imp.create_dynamic(spec)
+        module.__file__ = filename.to_os_specific()
+        return module
 
 
-        code = None
-        data = vfile.readFile(True)
-        if data[:4] != imp.get_magic():
-            raise ValueError("Bad magic number in %s" % (vfile))
+    def exec_module(self, module):
+        _imp.exec_dynamic(module)
 
 
-        t = int.from_bytes(data[4:8], 'little')
-        data = data[12:]
+    def is_package(self, fullname):
+        return False
 
 
-        if not timestamp or t == timestamp:
-            return marshal.loads(data)
-        else:
-            raise ValueError("Timestamp wrong on %s" % (vfile))
-
-    def _compile(self, filename, source):
-        """ Compiles the Python source code to a code object and
-        attempts to write it to an appropriate .pyc file.  May raise
-        SyntaxError or other errors generated by the compiler. """
-
-        if source and source[-1] != '\n':
-            source = source + '\n'
-        code = compile(source, filename.toOsSpecific(), 'exec')
-
-        # try to cache the compiled code
-        pycFilename = Filename(filename)
-        pycFilename.setExtension(compiledExtensions[0])
-        try:
-            f = open(pycFilename.toOsSpecific(), 'wb')
-        except IOError:
-            pass
-        else:
-            f.write(imp.get_magic())
-            f.write((self.timestamp & 0xffffffff).to_bytes(4, 'little'))
-            f.write(b'\0\0\0\0')
-            f.write(marshal.dumps(code))
-            f.close()
+    def get_code(self, fullname):
+        return None
 
 
-        return code
+    def get_source(self, fullname):
+        return None
 
 
 
 
-class VFSSharedImporter:
-    """ This is a special importer that is added onto the meta_path
-    list, so that it is called before sys.path is traversed.  It uses
-    special logic to load one of the "shared" packages, by searching
-    the entire sys.path for all instances of this shared package, and
-    merging them. """
+class VFSNamespaceLoader(Loader):
+    def create_module(self, spec: ModuleSpec) -> ModuleType | None:
+        """Use default semantics for module creation."""
 
 
-    def __init__(self):
+    def exec_module(self, module: ModuleType) -> None:
         pass
         pass
 
 
-    def find_module(self, fullname, path = None, reload = False):
-        #print >>sys.stderr, "shared find_module(%s), path = %s" % (fullname, path)
-
-        if fullname not in sharedPackages:
-            # Not a shared package; fall back to normal import.
-            return None
-
-        if path is None:
-            path = sys.path
-
-        excludePaths = []
-        if reload:
-            # If reload is true, we are simply reloading the module,
-            # looking for new paths to add.
-            mod = sys.modules[fullname]
-            excludePaths = getattr(mod, '_vfs_shared_path', None)
-            if excludePaths is None:
-                # If there isn't a _vfs_shared_path symbol already,
-                # the module must have been loaded through
-                # conventional means.  Try to guess which path it was
-                # found on.
-                d = self.getLoadedDirname(mod)
-                excludePaths = [d]
-
-        loaders = []
-        for dir in path:
-            if dir in excludePaths:
-                continue
-
-            importer = sys.path_importer_cache.get(dir, None)
-            if importer is None:
-                try:
-                    importer = VFSImporter(dir)
-                except ImportError:
-                    continue
-
-                sys.path_importer_cache[dir] = importer
-
-            try:
-                loader = importer.find_module(fullname)
-                if not loader:
-                    continue
-            except ImportError:
-                continue
-
-            loaders.append(loader)
-
-        if not loaders:
-            return None
-        return VFSSharedLoader(loaders, reload = reload)
-
-    def getLoadedDirname(self, mod):
-        """ Returns the directory name that the indicated
-        conventionally-loaded module must have been loaded from. """
-
-        if not getattr(mod, '__file__', None):
-            return None
-
-        fullname = mod.__name__
-        dirname = Filename.fromOsSpecific(mod.__file__).getDirname()
-
-        parentname = None
-        basename = fullname
-        if '.' in fullname:
-            parentname, basename = fullname.rsplit('.', 1)
-
-        path = None
-        if parentname:
-            parent = sys.modules[parentname]
-            path = parent.__path__
-        if path is None:
-            path = sys.path
-
-        for dir in path:
-            pdir = str(Filename.fromOsSpecific(dir))
-            if pdir + '/' + basename == dirname:
-                # We found it!
-                return dir
-
-        # Couldn't figure it out.
-        return None
-
-
-class VFSSharedLoader:
-    """ The second part of VFSSharedImporter, this imports a list of
-    packages and combines them. """
+    def is_package(self, fullname):
+        return True
 
 
-    def __init__(self, loaders, reload):
-        self.loaders = loaders
-        self.reload = reload
+    def get_source(self, fullname):
+        return ''
 
 
-    def load_module(self, fullname):
-        #print >>sys.stderr, "shared load_module(%s), loaders = %s" % (fullname, map(lambda l: l.dir_path, self.loaders))
+    def get_code(self, fullname):
+        return compile('', '<string>', 'exec', dont_inherit=True)
 
 
-        mod = None
-        message = None
-        path = []
-        vfs_shared_path = []
-        if self.reload:
-            mod = sys.modules[fullname]
-            path = mod.__path__ or []
-            if path == fullname:
-                # Work around Python bug setting __path__ of frozen modules.
-                path = []
-            vfs_shared_path = getattr(mod, '_vfs_shared_path', [])
 
 
-        for loader in self.loaders:
-            try:
-                mod = loader.load_module(fullname, loadingShared = True)
-            except ImportError:
-                etype, evalue, etraceback = sys.exc_info()
-                print("%s on %s: %s" % (etype.__name__, fullname, evalue))
-                if not message:
-                    message = '%s: %s' % (fullname, evalue)
-                continue
-            for dir in getattr(mod, '__path__', []):
-                if dir not in path:
-                    path.append(dir)
-
-        if mod is None:
-            # If all of them failed to load, raise ImportError.
-            raise ImportError(message)
-
-        # If at least one of them loaded successfully, return the
-        # union of loaded modules.
-        mod.__path__ = path
-        mod.__package__ = fullname
-
-        # Also set this special symbol, which records that this is a
-        # shared package, and also lists the paths we have already
-        # loaded.
-        mod._vfs_shared_path = vfs_shared_path + [l.dir_path for l in self.loaders]
-
-        return mod
+def _path_hook(entry: str) -> VFSFinder:
+    # If this is a directory in the VFS, create a VFSFinder for this entry.
+    vfile = vfs.get_file(Filename.from_os_specific(entry), False)
+    if vfile and vfile.is_directory() and not isinstance(vfile.get_mount(), VirtualFileMountSystem):
+        return VFSFinder(entry)
+    else:
+        raise ImportError
 
 
 
 
 _registered = False
 _registered = False
 
 
-
-def register():
-    """ Register the VFSImporter on the path_hooks, if it has not
+def register() -> None:
+    """ Register the VFSFinder on the path_hooks, if it has not
     already been registered, so that future Python import statements
     already been registered, so that future Python import statements
     will vector through here (and therefore will take advantage of
     will vector through here (and therefore will take advantage of
     Panda's virtual file system). """
     Panda's virtual file system). """
@@ -483,55 +273,9 @@ def register():
     global _registered
     global _registered
     if not _registered:
     if not _registered:
         _registered = True
         _registered = True
-        sys.path_hooks.insert(0, VFSImporter)
-        sys.meta_path.insert(0, VFSSharedImporter())
+        sys.path_hooks.insert(0, _path_hook)
 
 
         # Blow away the importer cache, so we'll come back through the
         # Blow away the importer cache, so we'll come back through the
-        # VFSImporter for every folder in the future, even those
+        # VFSFinder for every folder in the future, even those
         # folders that previously were loaded directly.
         # folders that previously were loaded directly.
         sys.path_importer_cache = {}
         sys.path_importer_cache = {}
-
-
-def reloadSharedPackage(mod):
-    """ Reloads the specific module as a shared package, adding any
-    new directories that might have appeared on the search path. """
-
-    fullname = mod.__name__
-    path = None
-    if '.' in fullname:
-        parentname = fullname.rsplit('.', 1)[0]
-        parent = sys.modules[parentname]
-        path = parent.__path__
-
-    importer = VFSSharedImporter()
-    loader = importer.find_module(fullname, path = path, reload = True)
-    if loader:
-        loader.load_module(fullname)
-
-    # Also force any child packages to become shared packages, if
-    # they aren't already.
-    for basename, child in list(mod.__dict__.items()):
-        if isinstance(child, types.ModuleType):
-            childname = child.__name__
-            if childname == fullname + '.' + basename and \
-               hasattr(child, '__path__') and \
-               childname not in sharedPackages:
-                sharedPackages[childname] = True
-                reloadSharedPackage(child)
-
-
-def reloadSharedPackages():
-    """ Walks through the sharedPackages list, and forces a reload of
-    any modules on that list that have already been loaded.  This
-    allows new directories to be added to the search path. """
-
-    #print >> sys.stderr, "reloadSharedPackages, path = %s, sharedPackages = %s" % (sys.path, sharedPackages.keys())
-
-    # Sort the list, just to make sure parent packages are reloaded
-    # before child packages are.
-    for fullname in sorted(sharedPackages.keys()):
-        mod = sys.modules.get(fullname, None)
-        if not mod:
-            continue
-
-        reloadSharedPackage(mod)

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

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

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

@@ -16,14 +16,11 @@
 
 
 #include "directbase.h"
 #include "directbase.h"
 
 
-#include "eventHandler.h"
 #include "graphicsWindow.h"
 #include "graphicsWindow.h"
 #include "graphicsPipe.h"
 #include "graphicsPipe.h"
 #include "animControl.h"
 #include "animControl.h"
 #include "pointerTo.h"
 #include "pointerTo.h"
 #include "dconfig.h"
 #include "dconfig.h"
-#include "dSearchPath.h"
-#include "configVariableSearchPath.h"
 #include "nodePath.h"
 #include "nodePath.h"
 
 
 ConfigureDecl(config_showbase, EXPCL_DIRECT_SHOWBASE, EXPTP_DIRECT_SHOWBASE);
 ConfigureDecl(config_showbase, EXPCL_DIRECT_SHOWBASE, EXPTP_DIRECT_SHOWBASE);
@@ -34,10 +31,6 @@ class GraphicsEngine;
 
 
 BEGIN_PUBLISH
 BEGIN_PUBLISH
 
 
-EXPCL_DIRECT_SHOWBASE ConfigVariableSearchPath &get_particle_path();
-
-EXPCL_DIRECT_SHOWBASE void throw_new_frame();
-
 EXPCL_DIRECT_SHOWBASE void init_app_for_gui();
 EXPCL_DIRECT_SHOWBASE void init_app_for_gui();
 
 
 // to handle windows stickykeys
 // to handle windows stickykeys

+ 21 - 11
direct/src/stdpy/thread.py

@@ -5,6 +5,8 @@ in some compilation models, Panda's threading constructs are
 incompatible with the OS-provided threads used by Python's thread
 incompatible with the OS-provided threads used by Python's thread
 module. """
 module. """
 
 
+from __future__ import annotations
+
 __all__ = [
 __all__ = [
     'error', 'LockType',
     'error', 'LockType',
     'start_new_thread',
     'start_new_thread',
@@ -19,6 +21,9 @@ __all__ = [
 from panda3d import core
 from panda3d import core
 import sys
 import sys
 
 
+from collections.abc import Callable, Iterable, Mapping
+from typing import Any
+
 if sys.platform == "win32":
 if sys.platform == "win32":
     TIMEOUT_MAX = float(0xffffffff // 1000)
     TIMEOUT_MAX = float(0xffffffff // 1000)
 else:
 else:
@@ -41,12 +46,12 @@ class LockType:
     allows a different thread to release the lock than the one that
     allows a different thread to release the lock than the one that
     acquired it. """
     acquired it. """
 
 
-    def __init__(self):
+    def __init__(self) -> None:
         self.__lock = core.Mutex('PythonLock')
         self.__lock = core.Mutex('PythonLock')
         self.__cvar = core.ConditionVar(self.__lock)
         self.__cvar = core.ConditionVar(self.__lock)
         self.__locked = False
         self.__locked = False
 
 
-    def acquire(self, waitflag = 1, timeout = -1):
+    def acquire(self, waitflag: bool = True, timeout: float = -1) -> bool:
         self.__lock.acquire()
         self.__lock.acquire()
         try:
         try:
             if self.__locked and not waitflag:
             if self.__locked and not waitflag:
@@ -65,7 +70,7 @@ class LockType:
         finally:
         finally:
             self.__lock.release()
             self.__lock.release()
 
 
-    def release(self):
+    def release(self) -> None:
         self.__lock.acquire()
         self.__lock.acquire()
         try:
         try:
             if not self.__locked:
             if not self.__locked:
@@ -90,18 +95,23 @@ class LockType:
 _counter = 0
 _counter = 0
 
 
 
 
-def _newname(template="Thread-%d"):
+def _newname(template: str = "Thread-%d") -> str:
     global _counter
     global _counter
     _counter = _counter + 1
     _counter = _counter + 1
     return template % _counter
     return template % _counter
 
 
 
 
-_threads = {}
+_threads: dict[int, tuple[core.Thread, dict[int, dict[str, Any]], Any | None]] = {}
 _nextThreadId = 0
 _nextThreadId = 0
 _threadsLock = core.Mutex('thread._threadsLock')
 _threadsLock = core.Mutex('thread._threadsLock')
 
 
 
 
-def start_new_thread(function, args, kwargs = {}, name = None):
+def start_new_thread(
+    function: Callable[..., object],
+    args: Iterable[Any],
+    kwargs: Mapping[str, Any] = {},
+    name: str | None = None,
+) -> int:
     def threadFunc(threadId, function = function, args = args, kwargs = kwargs):
     def threadFunc(threadId, function = function, args = args, kwargs = kwargs):
         try:
         try:
             try:
             try:
@@ -132,7 +142,7 @@ def start_new_thread(function, args, kwargs = {}, name = None):
         _threadsLock.release()
         _threadsLock.release()
 
 
 
 
-def _add_thread(thread, wrapper):
+def _add_thread(thread: core.Thread, wrapper: Any) -> int:
     """ Adds the indicated core.Thread object, with the indicated Python
     """ Adds the indicated core.Thread object, with the indicated Python
     wrapper, to the thread list.  Returns the new thread ID. """
     wrapper, to the thread list.  Returns the new thread ID. """
 
 
@@ -150,7 +160,7 @@ def _add_thread(thread, wrapper):
         _threadsLock.release()
         _threadsLock.release()
 
 
 
 
-def _get_thread_wrapper(thread, wrapperClass):
+def _get_thread_wrapper(thread: core.Thread, wrapperClass: Callable[[core.Thread, int], Any]) -> Any:
     """ Returns the thread wrapper for the indicated thread.  If there
     """ Returns the thread wrapper for the indicated thread.  If there
     is not one, creates an instance of the indicated wrapperClass
     is not one, creates an instance of the indicated wrapperClass
     instead. """
     instead. """
@@ -222,7 +232,7 @@ def _get_thread_locals(thread, i):
             _threadsLock.release()
             _threadsLock.release()
 
 
 
 
-def _remove_thread_id(threadId):
+def _remove_thread_id(threadId: int) -> None:
     """ Removes the thread with the indicated ID from the thread list. """
     """ Removes the thread with the indicated ID from the thread list. """
 
 
     # On interpreter shutdown, Python may set module globals to None.
     # On interpreter shutdown, Python may set module globals to None.
@@ -250,11 +260,11 @@ def exit():
     raise SystemExit
     raise SystemExit
 
 
 
 
-def allocate_lock():
+def allocate_lock() -> LockType:
     return LockType()
     return LockType()
 
 
 
 
-def get_ident():
+def get_ident() -> int:
     return core.Thread.getCurrentThread().this
     return core.Thread.getCurrentThread().this
 
 
 
 

+ 32 - 14
direct/src/stdpy/threading.py

@@ -21,12 +21,17 @@ easier to use and understand.
 It is permissible to mix-and-match both threading and threading2
 It is permissible to mix-and-match both threading and threading2
 within the same application. """
 within the same application. """
 
 
+from __future__ import annotations
+
 from panda3d import core
 from panda3d import core
 from direct.stdpy import thread as _thread
 from direct.stdpy import thread as _thread
 import sys as _sys
 import sys as _sys
 
 
 import weakref
 import weakref
 
 
+from collections.abc import Callable, Iterable, Mapping
+from typing import Any, NoReturn
+
 __all__ = [
 __all__ = [
     'Thread',
     'Thread',
     'Lock', 'RLock',
     'Lock', 'RLock',
@@ -54,10 +59,14 @@ class ThreadBase:
     """ A base class for both Thread and ExternalThread in this
     """ A base class for both Thread and ExternalThread in this
     module. """
     module. """
 
 
-    def __init__(self):
+    name: str
+    ident: int
+    daemon: bool
+
+    def __init__(self) -> None:
         pass
         pass
 
 
-    def getName(self):
+    def getName(self) -> str:
         return self.name
         return self.name
 
 
     def isDaemon(self):
     def isDaemon(self):
@@ -92,7 +101,15 @@ class Thread(ThreadBase):
     object.  The wrapper is designed to emulate Python's own
     object.  The wrapper is designed to emulate Python's own
     threading.Thread object. """
     threading.Thread object. """
 
 
-    def __init__(self, group=None, target=None, name=None, args=(), kwargs={}, daemon=None):
+    def __init__(
+        self,
+        group: None = None,
+        target: Callable[..., object] | None = None,
+        name: str | None = None,
+        args: Iterable[Any] = (),
+        kwargs: Mapping[str, Any] = {},
+        daemon: bool | None = None,
+    ) -> None:
         ThreadBase.__init__(self)
         ThreadBase.__init__(self)
 
 
         assert group is None
         assert group is None
@@ -131,7 +148,7 @@ class Thread(ThreadBase):
 
 
     isAlive = is_alive
     isAlive = is_alive
 
 
-    def start(self):
+    def start(self) -> None:
         thread = self.__thread
         thread = self.__thread
         if thread is None or thread.is_started():
         if thread is None or thread.is_started():
             raise RuntimeError
             raise RuntimeError
@@ -147,7 +164,7 @@ class Thread(ThreadBase):
 
 
         self.__target(*self.__args, **self.__kwargs)
         self.__target(*self.__args, **self.__kwargs)
 
 
-    def join(self, timeout = None):
+    def join(self, timeout: float | None = None) -> None:
         # We don't support a timed join here, sorry.
         # We don't support a timed join here, sorry.
         assert timeout is None
         assert timeout is None
         thread = self.__thread
         thread = self.__thread
@@ -157,7 +174,7 @@ class Thread(ThreadBase):
             self.__thread = None
             self.__thread = None
             _thread._remove_thread_id(self.ident)
             _thread._remove_thread_id(self.ident)
 
 
-    def setName(self, name):
+    def setName(self, name: str) -> None:
         self.__dict__['name'] = name
         self.__dict__['name'] = name
         self.__thread.setName(name)
         self.__thread.setName(name)
 
 
@@ -166,7 +183,7 @@ class ExternalThread(ThreadBase):
     """ Returned for a Thread object that wasn't created by this
     """ Returned for a Thread object that wasn't created by this
     interface. """
     interface. """
 
 
-    def __init__(self, extThread, threadId):
+    def __init__(self, extThread: core.Thread, threadId: int) -> None:
         ThreadBase.__init__(self)
         ThreadBase.__init__(self)
 
 
         self.__thread = extThread
         self.__thread = extThread
@@ -196,7 +213,7 @@ class ExternalThread(ThreadBase):
 class MainThread(ExternalThread):
 class MainThread(ExternalThread):
     """ Returned for the MainThread object. """
     """ Returned for the MainThread object. """
 
 
-    def __init__(self, extThread, threadId):
+    def __init__(self, extThread: core.Thread, threadId: int) -> None:
         ExternalThread.__init__(self, extThread, threadId)
         ExternalThread.__init__(self, extThread, threadId)
         self.__dict__['daemon'] = False
         self.__dict__['daemon'] = False
 
 
@@ -206,7 +223,7 @@ class Lock(core.Mutex):
     The wrapper is designed to emulate Python's own threading.Lock
     The wrapper is designed to emulate Python's own threading.Lock
     object. """
     object. """
 
 
-    def __init__(self, name = "PythonLock"):
+    def __init__(self, name: str = "PythonLock") -> None:
         core.Mutex.__init__(self, name)
         core.Mutex.__init__(self, name)
 
 
 
 
@@ -224,7 +241,7 @@ class Condition(core.ConditionVar):
     object.  The wrapper is designed to emulate Python's own
     object.  The wrapper is designed to emulate Python's own
     threading.Condition object. """
     threading.Condition object. """
 
 
-    def __init__(self, lock = None):
+    def __init__(self, lock: Lock | RLock | None = None) -> None:
         if not lock:
         if not lock:
             lock = Lock()
             lock = Lock()
 
 
@@ -241,7 +258,7 @@ class Condition(core.ConditionVar):
     def release(self):
     def release(self):
         self.__lock.release()
         self.__lock.release()
 
 
-    def wait(self, timeout = None):
+    def wait(self, timeout: float | None = None) -> None:
         if timeout is None:
         if timeout is None:
             core.ConditionVar.wait(self)
             core.ConditionVar.wait(self)
         else:
         else:
@@ -373,8 +390,9 @@ class Timer(Thread):
         self.finished.set()
         self.finished.set()
 
 
 
 
-def _create_thread_wrapper(t, threadId):
+def _create_thread_wrapper(t: core.Thread, threadId: int) -> ExternalThread:
     """ Creates a thread wrapper for the indicated external thread. """
     """ Creates a thread wrapper for the indicated external thread. """
+    pyt: ExternalThread
     if isinstance(t, core.MainThread):
     if isinstance(t, core.MainThread):
         pyt = MainThread(t, threadId)
         pyt = MainThread(t, threadId)
     else:
     else:
@@ -383,7 +401,7 @@ def _create_thread_wrapper(t, threadId):
     return pyt
     return pyt
 
 
 
 
-def current_thread():
+def current_thread() -> ThreadBase:
     t = core.Thread.getCurrentThread()
     t = core.Thread.getCurrentThread()
     return _thread._get_thread_wrapper(t, _create_thread_wrapper)
     return _thread._get_thread_wrapper(t, _create_thread_wrapper)
 
 
@@ -430,5 +448,5 @@ def setprofile(func):
     _setprofile_func = func
     _setprofile_func = func
 
 
 
 
-def stack_size(size = None):
+def stack_size(size: object = None) -> NoReturn:
     raise ThreadError
     raise ThreadError

+ 56 - 44
direct/src/stdpy/threading2.py

@@ -13,6 +13,8 @@ to import Panda's thread reimplementation instead of the system thread
 module, and so it is therefore layered on top of Panda's thread
 module, and so it is therefore layered on top of Panda's thread
 implementation. """
 implementation. """
 
 
+from __future__ import annotations
+
 import sys as _sys
 import sys as _sys
 import atexit as _atexit
 import atexit as _atexit
 
 
@@ -21,8 +23,10 @@ from direct.stdpy.thread import stack_size, _newname, _local as local
 from panda3d import core
 from panda3d import core
 _sleep = core.Thread.sleep
 _sleep = core.Thread.sleep
 
 
+from collections.abc import Callable, Iterable, Mapping
 from time import time as _time
 from time import time as _time
 from traceback import format_exc as _format_exc
 from traceback import format_exc as _format_exc
+from typing import Any
 
 
 __all__ = ['get_ident', 'active_count', 'Condition', 'current_thread',
 __all__ = ['get_ident', 'active_count', 'Condition', 'current_thread',
            'enumerate', 'main_thread', 'TIMEOUT_MAX',
            'enumerate', 'main_thread', 'TIMEOUT_MAX',
@@ -51,12 +55,12 @@ if __debug__:
 
 
     class _Verbose(object):
     class _Verbose(object):
 
 
-        def __init__(self, verbose=None):
+        def __init__(self, verbose: bool | None = None) -> None:
             if verbose is None:
             if verbose is None:
                 verbose = _VERBOSE
                 verbose = _VERBOSE
             self.__verbose = verbose
             self.__verbose = verbose
 
 
-        def _note(self, format, *args):
+        def _note(self, format: str, *args: Any) -> None:
             if self.__verbose:
             if self.__verbose:
                 format = format % args
                 format = format % args
                 format = "%s: %s\n" % (
                 format = "%s: %s\n" % (
@@ -66,9 +70,9 @@ if __debug__:
 else:
 else:
     # Disable this when using "python -O"
     # Disable this when using "python -O"
     class _Verbose(object):  # type: ignore[no-redef]
     class _Verbose(object):  # type: ignore[no-redef]
-        def __init__(self, verbose=None):
+        def __init__(self, verbose: bool | None = None) -> None:
             pass
             pass
-        def _note(self, *args):
+        def _note(self, *args) -> None:
             pass
             pass
 
 
 # Support for profile and trace hooks
 # Support for profile and trace hooks
@@ -88,15 +92,15 @@ def settrace(func):
 
 
 Lock = _allocate_lock
 Lock = _allocate_lock
 
 
-def RLock(*args, **kwargs):
-    return _RLock(*args, **kwargs)
+def RLock(verbose: bool | None = None) -> _RLock:
+    return _RLock(verbose)
 
 
 class _RLock(_Verbose):
 class _RLock(_Verbose):
 
 
-    def __init__(self, verbose=None):
+    def __init__(self, verbose: bool | None = None) -> None:
         _Verbose.__init__(self, verbose)
         _Verbose.__init__(self, verbose)
         self.__block = _allocate_lock()
         self.__block = _allocate_lock()
-        self.__owner = None
+        self.__owner: Thread | None = None
         self.__count = 0
         self.__count = 0
 
 
     def __repr__(self):
     def __repr__(self):
@@ -105,13 +109,13 @@ class _RLock(_Verbose):
                 self.__owner and self.__owner.getName(),
                 self.__owner and self.__owner.getName(),
                 self.__count)
                 self.__count)
 
 
-    def acquire(self, blocking=1):
+    def acquire(self, blocking: bool = True) -> bool:
         me = currentThread()
         me = currentThread()
         if self.__owner is me:
         if self.__owner is me:
             self.__count = self.__count + 1
             self.__count = self.__count + 1
             if __debug__:
             if __debug__:
                 self._note("%s.acquire(%s): recursive success", self, blocking)
                 self._note("%s.acquire(%s): recursive success", self, blocking)
-            return 1
+            return True
         rc = self.__block.acquire(blocking)
         rc = self.__block.acquire(blocking)
         if rc:
         if rc:
             self.__owner = me
             self.__owner = me
@@ -125,7 +129,7 @@ class _RLock(_Verbose):
 
 
     __enter__ = acquire
     __enter__ = acquire
 
 
-    def release(self):
+    def release(self) -> None:
         me = currentThread()
         me = currentThread()
         assert self.__owner is me, "release() of un-acquire()d lock"
         assert self.__owner is me, "release() of un-acquire()d lock"
         self.__count = count = self.__count - 1
         self.__count = count = self.__count - 1
@@ -163,12 +167,12 @@ class _RLock(_Verbose):
         return self.__owner is currentThread()
         return self.__owner is currentThread()
 
 
 
 
-def Condition(*args, **kwargs):
-    return _Condition(*args, **kwargs)
+def Condition(lock: _thread.LockType | _RLock | None = None, verbose: bool | None = None) -> _Condition:
+    return _Condition(lock, verbose)
 
 
 class _Condition(_Verbose):
 class _Condition(_Verbose):
 
 
-    def __init__(self, lock=None, verbose=None):
+    def __init__(self, lock: _thread.LockType | _RLock | None = None, verbose: bool | None = None) -> None:
         _Verbose.__init__(self, verbose)
         _Verbose.__init__(self, verbose)
         if lock is None:
         if lock is None:
             lock = RLock()
             lock = RLock()
@@ -180,18 +184,18 @@ class _Condition(_Verbose):
         # these override the default implementations (which just call
         # these override the default implementations (which just call
         # release() and acquire() on the lock).  Ditto for _is_owned().
         # release() and acquire() on the lock).  Ditto for _is_owned().
         try:
         try:
-            self._release_save = lock._release_save
+            self._release_save = lock._release_save  # type: ignore[method-assign, union-attr]
         except AttributeError:
         except AttributeError:
             pass
             pass
         try:
         try:
-            self._acquire_restore = lock._acquire_restore
+            self._acquire_restore = lock._acquire_restore  # type: ignore[method-assign, union-attr]
         except AttributeError:
         except AttributeError:
             pass
             pass
         try:
         try:
-            self._is_owned = lock._is_owned
+            self._is_owned = lock._is_owned  # type: ignore[method-assign, union-attr]
         except AttributeError:
         except AttributeError:
             pass
             pass
-        self.__waiters = []
+        self.__waiters: list[_thread.LockType] = []
 
 
     def __enter__(self):
     def __enter__(self):
         return self.__lock.__enter__()
         return self.__lock.__enter__()
@@ -202,22 +206,22 @@ class _Condition(_Verbose):
     def __repr__(self):
     def __repr__(self):
         return "<Condition(%s, %d)>" % (self.__lock, len(self.__waiters))
         return "<Condition(%s, %d)>" % (self.__lock, len(self.__waiters))
 
 
-    def _release_save(self): # pylint: disable=method-hidden
+    def _release_save(self) -> Any: # pylint: disable=method-hidden
         self.__lock.release()           # No state to save
         self.__lock.release()           # No state to save
 
 
-    def _acquire_restore(self, x): # pylint: disable=method-hidden
+    def _acquire_restore(self, x) -> None: # pylint: disable=method-hidden
         self.__lock.acquire()           # Ignore saved state
         self.__lock.acquire()           # Ignore saved state
 
 
-    def _is_owned(self): # pylint: disable=method-hidden
+    def _is_owned(self) -> bool: # pylint: disable=method-hidden
         # Return True if lock is owned by currentThread.
         # Return True if lock is owned by currentThread.
         # This method is called only if __lock doesn't have _is_owned().
         # This method is called only if __lock doesn't have _is_owned().
-        if self.__lock.acquire(0):
+        if self.__lock.acquire(False):
             self.__lock.release()
             self.__lock.release()
             return False
             return False
         else:
         else:
             return True
             return True
 
 
-    def wait(self, timeout=None):
+    def wait(self, timeout: float | None = None) -> None:
         assert self._is_owned(), "wait() of un-acquire()d lock"
         assert self._is_owned(), "wait() of un-acquire()d lock"
         waiter = _allocate_lock()
         waiter = _allocate_lock()
         waiter.acquire()
         waiter.acquire()
@@ -237,7 +241,7 @@ class _Condition(_Verbose):
                 endtime = _time() + timeout
                 endtime = _time() + timeout
                 delay = 0.0005 # 500 us -> initial delay of 1 ms
                 delay = 0.0005 # 500 us -> initial delay of 1 ms
                 while True:
                 while True:
-                    gotit = waiter.acquire(0)
+                    gotit = waiter.acquire(False)
                     if gotit:
                     if gotit:
                         break
                         break
                     remaining = endtime - _time()
                     remaining = endtime - _time()
@@ -258,7 +262,7 @@ class _Condition(_Verbose):
         finally:
         finally:
             self._acquire_restore(saved_state)
             self._acquire_restore(saved_state)
 
 
-    def notify(self, n=1):
+    def notify(self, n: int = 1) -> None:
         assert self._is_owned(), "notify() of un-acquire()d lock"
         assert self._is_owned(), "notify() of un-acquire()d lock"
         __waiters = self.__waiters
         __waiters = self.__waiters
         waiters = __waiters[:n]
         waiters = __waiters[:n]
@@ -275,7 +279,7 @@ class _Condition(_Verbose):
             except ValueError:
             except ValueError:
                 pass
                 pass
 
 
-    def notifyAll(self):
+    def notifyAll(self) -> None:
         self.notify(len(self.__waiters))
         self.notify(len(self.__waiters))
 
 
 
 
@@ -381,7 +385,7 @@ class _Event(_Verbose):
 
 
 # Active thread administration
 # Active thread administration
 _active_limbo_lock = _allocate_lock()
 _active_limbo_lock = _allocate_lock()
-_active = {}    # maps thread id to Thread object
+_active: dict[int, Thread] = {}    # maps thread id to Thread object
 _limbo = {}
 _limbo = {}
 
 
 
 
@@ -400,8 +404,16 @@ class Thread(_Verbose):
     # Protected by _active_limbo_lock.
     # Protected by _active_limbo_lock.
     __registered_atexit = False
     __registered_atexit = False
 
 
-    def __init__(self, group=None, target=None, name=None,
-                 args=(), kwargs=None, verbose=None, daemon=None):
+    def __init__(
+        self,
+        group: None = None,
+        target: Callable[..., object] | None = None,
+        name: object = None,
+        args: Iterable[Any] = (),
+        kwargs: Mapping[str, Any] | None = None,
+        verbose: bool | None = None,
+        daemon: bool | None = None,
+    ) -> None:
         assert group is None, "group argument must be None for now"
         assert group is None, "group argument must be None for now"
         _Verbose.__init__(self, verbose)
         _Verbose.__init__(self, verbose)
         if kwargs is None:
         if kwargs is None:
@@ -422,7 +434,7 @@ class Thread(_Verbose):
         # sys.exc_info since it can be changed between instances
         # sys.exc_info since it can be changed between instances
         self.__stderr = _sys.stderr
         self.__stderr = _sys.stderr
 
 
-    def _set_daemon(self):
+    def _set_daemon(self) -> bool:
         # Overridden in _MainThread and _DummyThread
         # Overridden in _MainThread and _DummyThread
         return currentThread().isDaemon()
         return currentThread().isDaemon()
 
 
@@ -437,7 +449,7 @@ class Thread(_Verbose):
             status = status + " daemon"
             status = status + " daemon"
         return "<%s(%s, %s)>" % (self.__class__.__name__, self.__name, status)
         return "<%s(%s, %s)>" % (self.__class__.__name__, self.__name, status)
 
 
-    def start(self):
+    def start(self) -> None:
         assert self.__initialized, "Thread.__init__() not called"
         assert self.__initialized, "Thread.__init__() not called"
         assert not self.__started, "thread already started"
         assert not self.__started, "thread already started"
         if __debug__:
         if __debug__:
@@ -457,11 +469,11 @@ class Thread(_Verbose):
         self.__started = True
         self.__started = True
         _sleep(0.000001)    # 1 usec, to let the thread run (Solaris hack)
         _sleep(0.000001)    # 1 usec, to let the thread run (Solaris hack)
 
 
-    def run(self):
+    def run(self) -> None:
         if self.__target:
         if self.__target:
             self.__target(*self.__args, **self.__kwargs)
             self.__target(*self.__args, **self.__kwargs)
 
 
-    def __bootstrap(self):
+    def __bootstrap(self) -> None:
         try:
         try:
             self.__started = True
             self.__started = True
             _active_limbo_lock.acquire()
             _active_limbo_lock.acquire()
@@ -497,7 +509,7 @@ class Thread(_Verbose):
                     # Do the best job possible w/o a huge amt. of code to
                     # Do the best job possible w/o a huge amt. of code to
                     # approximate a traceback (code ideas from
                     # approximate a traceback (code ideas from
                     # Lib/traceback.py)
                     # Lib/traceback.py)
-                    exc_type, exc_value, exc_tb = self.__exc_info()
+                    exc_type, exc_value, exc_tb = self.__exc_info()  # type: ignore[misc]
                     try:
                     try:
                         self.__stderr.write("Exception in thread " + self.getName() +
                         self.__stderr.write("Exception in thread " + self.getName() +
                             " (most likely raised during interpreter shutdown):\n")
                             " (most likely raised during interpreter shutdown):\n")
@@ -523,13 +535,13 @@ class Thread(_Verbose):
             except:
             except:
                 pass
                 pass
 
 
-    def __stop(self):
+    def __stop(self) -> None:
         self.__block.acquire()
         self.__block.acquire()
         self.__stopped = True
         self.__stopped = True
         self.__block.notifyAll()
         self.__block.notifyAll()
         self.__block.release()
         self.__block.release()
 
 
-    def __delete(self):
+    def __delete(self) -> None:
         "Remove current thread from the dict of currently running threads."
         "Remove current thread from the dict of currently running threads."
 
 
         # Notes about running with dummy_thread:
         # Notes about running with dummy_thread:
@@ -563,7 +575,7 @@ class Thread(_Verbose):
         finally:
         finally:
             _active_limbo_lock.release()
             _active_limbo_lock.release()
 
 
-    def join(self, timeout=None):
+    def join(self, timeout: float | None = None) -> None:
         assert self.__initialized, "Thread.__init__() not called"
         assert self.__initialized, "Thread.__init__() not called"
         assert self.__started, "cannot join thread before it is started"
         assert self.__started, "cannot join thread before it is started"
         assert self is not currentThread(), "cannot join current thread"
         assert self is not currentThread(), "cannot join current thread"
@@ -592,11 +604,11 @@ class Thread(_Verbose):
         finally:
         finally:
             self.__block.release()
             self.__block.release()
 
 
-    def getName(self):
+    def getName(self) -> str:
         assert self.__initialized, "Thread.__init__() not called"
         assert self.__initialized, "Thread.__init__() not called"
         return self.__name
         return self.__name
 
 
-    def setName(self, name):
+    def setName(self, name: object) -> None:
         assert self.__initialized, "Thread.__init__() not called"
         assert self.__initialized, "Thread.__init__() not called"
         self.__name = str(name)
         self.__name = str(name)
 
 
@@ -606,7 +618,7 @@ class Thread(_Verbose):
 
 
     isAlive = is_alive
     isAlive = is_alive
 
 
-    def isDaemon(self):
+    def isDaemon(self) -> bool:
         assert self.__initialized, "Thread.__init__() not called"
         assert self.__initialized, "Thread.__init__() not called"
         return self.__daemonic
         return self.__daemonic
 
 
@@ -654,7 +666,7 @@ class _Timer(Thread):
 
 
 class _MainThread(Thread):
 class _MainThread(Thread):
 
 
-    def __init__(self):
+    def __init__(self) -> None:
         Thread.__init__(self, name="MainThread")
         Thread.__init__(self, name="MainThread")
         self._Thread__started = True
         self._Thread__started = True
         _active_limbo_lock.acquire()
         _active_limbo_lock.acquire()
@@ -688,13 +700,13 @@ class _MainThread(Thread):
 
 
 class _DummyThread(Thread):
 class _DummyThread(Thread):
 
 
-    def __init__(self):
+    def __init__(self) -> None:
         Thread.__init__(self, name=_newname("Dummy-%d"), daemon=True)
         Thread.__init__(self, name=_newname("Dummy-%d"), daemon=True)
 
 
         # Thread.__block consumes an OS-level locking primitive, which
         # Thread.__block consumes an OS-level locking primitive, which
         # can never be used by a _DummyThread.  Since a _DummyThread
         # can never be used by a _DummyThread.  Since a _DummyThread
         # instance is immortal, that's bad, so release this resource.
         # instance is immortal, that's bad, so release this resource.
-        del self._Thread__block
+        del self._Thread__block  # type: ignore[attr-defined]
 
 
         self._Thread__started = True
         self._Thread__started = True
         _active_limbo_lock.acquire()
         _active_limbo_lock.acquire()
@@ -710,7 +722,7 @@ class _DummyThread(Thread):
 
 
 # Global API functions
 # Global API functions
 
 
-def current_thread():
+def current_thread() -> Thread:
     try:
     try:
         return _active[get_ident()]
         return _active[get_ident()]
     except KeyError:
     except KeyError:

+ 1 - 1
direct/src/tkpanels/Inspector.py

@@ -48,7 +48,7 @@ def inspectorFor(anObject):
 
 
 ### initializing
 ### initializing
 
 
-def initializeInspectorMap():
+def initializeInspectorMap() -> None:
     global _InspectorMap
     global _InspectorMap
     notFinishedTypes = ['BufferType',  'EllipsisType',  'FrameType', 'TracebackType', 'XRangeType']
     notFinishedTypes = ['BufferType',  'EllipsisType',  'FrameType', 'TracebackType', 'XRangeType']
 
 

+ 23 - 5
direct/src/tkpanels/ParticlePanel.py

@@ -3,7 +3,7 @@
 __all__ = ['ParticlePanel']
 __all__ = ['ParticlePanel']
 
 
 # Import Tkinter, Pmw, and the floater code from this directory tree.
 # Import Tkinter, Pmw, and the floater code from this directory tree.
-from panda3d.core import ColorBlendAttrib, Filename, Point2, Point3, Vec3, Vec4, getModelPath
+from panda3d.core import ColorBlendAttrib, ConfigVariableSearchPath, Filename, Point2, Point3, Vec3, Vec4, getModelPath
 from panda3d.physics import (
 from panda3d.physics import (
     BaseParticleEmitter,
     BaseParticleEmitter,
     BaseParticleRenderer,
     BaseParticleRenderer,
@@ -36,7 +36,6 @@ from panda3d.physics import (
     SpriteParticleRenderer,
     SpriteParticleRenderer,
     TangentRingEmitter,
     TangentRingEmitter,
 )
 )
-from panda3d.direct import getParticlePath
 from direct.tkwidgets.AppShell import AppShell
 from direct.tkwidgets.AppShell import AppShell
 from direct.tkwidgets import Dial
 from direct.tkwidgets import Dial
 from direct.tkwidgets import Floater
 from direct.tkwidgets import Floater
@@ -53,6 +52,10 @@ import os
 import tkinter as tk
 import tkinter as tk
 
 
 
 
+particlePath = ConfigVariableSearchPath("particle-path",
+    "The directories to search for particle files to be loaded.")
+
+
 class ParticlePanel(AppShell):
 class ParticlePanel(AppShell):
     # Override class variables
     # Override class variables
     appname = 'Particle Panel'
     appname = 'Particle Panel'
@@ -1275,7 +1278,7 @@ class ParticlePanel(AppShell):
 
 
     def loadParticleEffectFromFile(self):
     def loadParticleEffectFromFile(self):
         # Find path to particle directory
         # Find path to particle directory
-        pPath = getParticlePath()
+        pPath = particlePath
         if pPath.getNumDirectories() > 0:
         if pPath.getNumDirectories() > 0:
             if repr(pPath.getDirectory(0)) == '.':
             if repr(pPath.getDirectory(0)) == '.':
                 path = '.'
                 path = '.'
@@ -1303,7 +1306,7 @@ class ParticlePanel(AppShell):
 
 
     def saveParticleEffectToFile(self):
     def saveParticleEffectToFile(self):
         # Find path to particle directory
         # Find path to particle directory
-        pPath = getParticlePath()
+        pPath = particlePath
         if pPath.getNumDirectories() > 0:
         if pPath.getNumDirectories() > 0:
             if repr(pPath.getDirectory(0)) == '.':
             if repr(pPath.getDirectory(0)) == '.':
                 path = '.'
                 path = '.'
@@ -2872,6 +2875,12 @@ class ParticlePanel(AppShell):
             if type == 'FT_ONE_OVER_R_CUBED':
             if type == 'FT_ONE_OVER_R_CUBED':
                 #f.setFalloffType(LinearDistanceForce.FTONEOVERRCUBED)
                 #f.setFalloffType(LinearDistanceForce.FTONEOVERRCUBED)
                 f.setFalloffType(2)
                 f.setFalloffType(2)
+            if type == 'FT_ONE_OVER_R_OVER_DISTANCE':
+                f.setFalloffType(3)
+            if type == 'FT_ONE_OVER_R_OVER_DISTANCE_SQUARED':
+                f.setFalloffType(4)
+            if type == 'FT_ONE_OVER_R_OVER_DISTANCE_CUBED':
+                f.setFalloffType(5)
 
 
         def setForceCenter(vec, f = force):
         def setForceCenter(vec, f = force):
             f.setForceCenter(Point3(vec[0], vec[1], vec[2]))
             f.setForceCenter(Point3(vec[0], vec[1], vec[2]))
@@ -2886,7 +2895,10 @@ class ParticlePanel(AppShell):
             'Set force falloff type',
             'Set force falloff type',
             ('FT_ONE_OVER_R',
             ('FT_ONE_OVER_R',
              'FT_ONE_OVER_R_SQUARED',
              'FT_ONE_OVER_R_SQUARED',
-             'FT_ONE_OVER_R_CUBED'),
+             'FT_ONE_OVER_R_CUBED',
+             'FT_ONE_OVER_R_OVER_DISTANCE',
+             'FT_ONE_OVER_R_OVER_DISTANCE_SQUARED',
+             'FT_ONE_OVER_R_OVER_DISTANCE_CUBED'),
             command = setFalloffType)
             command = setFalloffType)
         self.getWidget(pageName, forceName + ' Falloff').configure(
         self.getWidget(pageName, forceName + ' Falloff').configure(
             label_width = 16)
             label_width = 16)
@@ -2897,6 +2909,12 @@ class ParticlePanel(AppShell):
             var.set('FT_ONE_OVER_R_SQUARED')
             var.set('FT_ONE_OVER_R_SQUARED')
         elif falloff == LinearDistanceForce.FTONEOVERRCUBED:
         elif falloff == LinearDistanceForce.FTONEOVERRCUBED:
             var.set('FT_ONE_OVER_R_CUBED')
             var.set('FT_ONE_OVER_R_CUBED')
+        elif falloff == LinearDistanceForce.FTONEOVERROVERDISTANCE:
+            var.set('FT_ONE_OVER_R_OVER_DISTANCE')
+        elif falloff == LinearDistanceForce.FTONEOVERROVERDISTANCESQUARED:
+            var.set('FT_ONE_OVER_R_OVER_DISTANCE_SQUARED')
+        elif falloff == LinearDistanceForce.FTONEOVERROVERDISTANCECUBED:
+            var.set('FT_ONE_OVER_R_OVER_DISTANCE_CUBED')
         vec = force.getForceCenter()
         vec = force.getForceCenter()
         self.createVector3Entry(frame, pageName, forceName + ' Center',
         self.createVector3Entry(frame, pageName, forceName + ' Center',
                                 'Set center of force',
                                 'Set center of force',

+ 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 d2844d994fcc465a4e22b10001d3ac5c4012b814
+    GIT_TAG ccbd5f9d79037a9d53d6dfb1a5e9fdf30f2510c1
 
 
     PREFIX ${_interrogate_dir}
     PREFIX ${_interrogate_dir}
     CMAKE_ARGS
     CMAKE_ARGS

+ 1 - 0
dtool/LocalSetup.cmake

@@ -133,6 +133,7 @@ check_include_file_cxx(dirent.h PHAVE_DIRENT_H)
 check_include_file_cxx(ucontext.h PHAVE_UCONTEXT_H) #TODO doesn't work on OSX, use sys/ucontext.h
 check_include_file_cxx(ucontext.h PHAVE_UCONTEXT_H) #TODO doesn't work on OSX, use sys/ucontext.h
 check_include_file_cxx(linux/input.h PHAVE_LINUX_INPUT_H)
 check_include_file_cxx(linux/input.h PHAVE_LINUX_INPUT_H)
 check_include_file_cxx(stdint.h PHAVE_STDINT_H)
 check_include_file_cxx(stdint.h PHAVE_STDINT_H)
+check_include_file_cxx(execinfo.h PHAVE_EXECINFO_H)
 
 
 # Do we have Posix threads?
 # Do we have Posix threads?
 #set(HAVE_POSIX_THREADS ${CMAKE_USE_PTHREADS_INIT})
 #set(HAVE_POSIX_THREADS ${CMAKE_USE_PTHREADS_INIT})

+ 1 - 1
dtool/src/dtoolbase/atomicAdjust.h

@@ -30,7 +30,7 @@ struct AtomicAdjust {
 #include "atomicAdjustDummyImpl.h"
 #include "atomicAdjustDummyImpl.h"
 typedef AtomicAdjustDummyImpl AtomicAdjust;
 typedef AtomicAdjustDummyImpl AtomicAdjust;
 
 
-#elif (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7))) || (defined(__clang__) && (__clang_major__ >= 3))
+#elif (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7))) || (defined(__clang__) && (__clang_major__ >= 3) && !defined(WIN32_VC) )
 // GCC 4.7 and above has built-in __atomic functions for atomic operations.
 // GCC 4.7 and above has built-in __atomic functions for atomic operations.
 // Clang 3.0 and above also supports them.
 // Clang 3.0 and above also supports them.
 
 

+ 13 - 1
dtool/src/dtoolbase/deletedBufferChain.cxx

@@ -88,7 +88,14 @@ allocate(size_t size, TypeHandle type_handle) {
   return ptr;
   return ptr;
 
 
 #else  // USE_DELETED_CHAIN
 #else  // USE_DELETED_CHAIN
-  return PANDA_MALLOC_SINGLE(_buffer_size);
+  void *ptr = PANDA_MALLOC_SINGLE(_buffer_size);
+
+#ifdef DO_MEMORY_USAGE
+  type_handle.inc_memory_usage(TypeHandle::MC_singleton, _buffer_size);
+#endif  // DO_MEMORY_USAGE
+
+  return ptr;
+
 #endif  // USE_DELETED_CHAIN
 #endif  // USE_DELETED_CHAIN
 }
 }
 
 
@@ -133,6 +140,11 @@ deallocate(void *ptr, TypeHandle type_handle) {
   _lock.unlock();
   _lock.unlock();
 
 
 #else  // USE_DELETED_CHAIN
 #else  // USE_DELETED_CHAIN
+
+#ifdef DO_MEMORY_USAGE
+  type_handle.dec_memory_usage(TypeHandle::MC_singleton, _buffer_size);
+#endif  // DO_MEMORY_USAGE
+
   PANDA_FREE_SINGLE(ptr);
   PANDA_FREE_SINGLE(ptr);
 #endif  // USE_DELETED_CHAIN
 #endif  // USE_DELETED_CHAIN
 }
 }

+ 44 - 0
dtool/src/dtoolbase/deletedChain.h

@@ -122,6 +122,50 @@ public:
 #define ALLOC_DELETED_CHAIN_DEF(Type)                        \
 #define ALLOC_DELETED_CHAIN_DEF(Type)                        \
   DeletedChain< Type > Type::_deleted_chain;
   DeletedChain< Type > Type::_deleted_chain;
 
 
+#elif defined(DO_MEMORY_USAGE)
+
+#define ALLOC_DELETED_CHAIN(Type)                            \
+  inline void *operator new(size_t size) RETURNS_ALIGNED(MEMORY_HOOK_ALIGNMENT) { \
+    void *ptr = PANDA_MALLOC_SINGLE(size);                   \
+    get_type_handle(Type).inc_memory_usage(TypeHandle::MC_singleton, sizeof(Type)); \
+    return ptr;                                              \
+  }                                                          \
+  inline void *operator new(size_t size, void *ptr) {        \
+    (void) size;                                             \
+    return ptr;                                              \
+  }                                                          \
+  inline void operator delete(void *ptr) {                   \
+    if (ptr != nullptr) {                                    \
+      get_type_handle(Type).dec_memory_usage(TypeHandle::MC_singleton, sizeof(Type)); \
+      PANDA_FREE_SINGLE(ptr);                                \
+    }                                                        \
+  }                                                          \
+  inline void operator delete(void *, void *) {              \
+  }                                                          \
+  inline void *operator new[](size_t size) RETURNS_ALIGNED(MEMORY_HOOK_ALIGNMENT) { \
+    void *ptr = PANDA_MALLOC_SINGLE(size);                   \
+    get_type_handle(Type).inc_memory_usage(TypeHandle::MC_array, sizeof(Type)); \
+    return ptr;                                              \
+  }                                                          \
+  inline void *operator new[](size_t size, void *ptr) {      \
+    (void) size;                                             \
+    return ptr;                                              \
+  }                                                          \
+  inline void operator delete[](void *ptr) {                 \
+    if (ptr != nullptr) {                                    \
+      get_type_handle(Type).dec_memory_usage(TypeHandle::MC_array, sizeof(Type)); \
+      PANDA_FREE_SINGLE(ptr);                                \
+    }                                                        \
+  }                                                          \
+  inline void operator delete[](void *, void *) {            \
+  }                                                          \
+  inline static bool validate_ptr(const void *ptr) {         \
+    return (ptr != nullptr);                                 \
+  }
+
+#define ALLOC_DELETED_CHAIN_DECL(Type) ALLOC_DELETED_CHAIN(Type)
+#define ALLOC_DELETED_CHAIN_DEF(Type)
+
 #else  // USE_DELETED_CHAIN
 #else  // USE_DELETED_CHAIN
 
 
 #define ALLOC_DELETED_CHAIN(Type)                            \
 #define ALLOC_DELETED_CHAIN(Type)                            \

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

@@ -252,7 +252,7 @@ inline static unsigned CountDecimalDigit32(uint32_t n) {
 }
 }
 
 
 inline static void DigitGen(const DiyFp& W, const DiyFp& Mp, uint64_t delta, char* buffer, int* len, int* K) {
 inline static void DigitGen(const DiyFp& W, const DiyFp& Mp, uint64_t delta, char* buffer, int* len, int* K) {
-  static const uint32_t kPow10[] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 0, 0, 0, 0, 0 };
+  static const uint32_t kPow10[] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 0, 0, 0, 0, 0, 0 };
   const DiyFp one(uint64_t(1) << -Mp.e, Mp.e);
   const DiyFp one(uint64_t(1) << -Mp.e, Mp.e);
   const DiyFp wp_w = Mp - W;
   const DiyFp wp_w = Mp - W;
   uint32_t p1 = static_cast<uint32_t>(Mp.f >> -one.e);
   uint32_t p1 = static_cast<uint32_t>(Mp.f >> -one.e);

+ 3 - 0
dtool/src/dtoolutil/iostream_ext.h

@@ -22,6 +22,9 @@
 #include <iostream>
 #include <iostream>
 #include "py_panda.h"
 #include "py_panda.h"
 
 
+using std::ostream;
+using std::istream;
+
 /**
 /**
  * These classes define the extension methods for istream and ostream, which
  * These classes define the extension methods for istream and ostream, which
  * are called instead of any C++ methods with the same prototype.
  * are called instead of any C++ methods with the same prototype.

+ 1 - 1
dtool/src/dtoolutil/textEncoder_ext.cxx

@@ -94,7 +94,7 @@ encode_wchar(char32_t ch, TextEncoder::Encoding encoding) {
  * given encoding.
  * given encoding.
  */
  */
 PyObject *Extension<TextEncoder>::
 PyObject *Extension<TextEncoder>::
-encode_wtext(const wstring &wtext, TextEncoder::Encoding encoding) {
+encode_wtext(const std::wstring &wtext, TextEncoder::Encoding encoding) {
   std::string value = TextEncoder::encode_wtext(wtext, encoding);
   std::string value = TextEncoder::encode_wtext(wtext, encoding);
   return PyBytes_FromStringAndSize((char *)value.data(), (Py_ssize_t)value.size());
   return PyBytes_FromStringAndSize((char *)value.data(), (Py_ssize_t)value.size());
 }
 }

+ 0 - 1
dtool/src/interrogatedb/CMakeLists.txt

@@ -2,7 +2,6 @@ set(P3IGATERUNTIME_HEADERS
   interrogate_request.h
   interrogate_request.h
   py_compat.h
   py_compat.h
   py_panda.h py_panda.I
   py_panda.h py_panda.I
-  py_wrappers.h
 )
 )
 
 
 install(FILES ${P3IGATERUNTIME_HEADERS} COMPONENT CoreDevel DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)
 install(FILES ${P3IGATERUNTIME_HEADERS} COMPONENT CoreDevel DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)

+ 2 - 2
dtool/src/interrogatedb/interrogate_request.h

@@ -44,12 +44,12 @@ EXPCL_INTERROGATEDB void interrogate_request_database(const char *database_filen
 
 
 /* The more sophisticated interface uses these structures. */
 /* The more sophisticated interface uses these structures. */
 
 
-typedef struct {
+typedef struct InterrogateUniqueNameDef {
   const char *name;
   const char *name;
   int index_offset;
   int index_offset;
 } InterrogateUniqueNameDef;
 } InterrogateUniqueNameDef;
 
 
-typedef struct {
+typedef struct InterrogateModuleDef {
   int file_identifier;
   int file_identifier;
 
 
   const char *library_name;
   const char *library_name;

+ 6 - 0
dtool/src/interrogatedb/py_compat.h

@@ -368,6 +368,12 @@ PyDict_GetItemStringRef(PyObject *mp, const char *key, PyObject **result) {
 #  define Py_END_CRITICAL_SECTION2() }
 #  define Py_END_CRITICAL_SECTION2() }
 #endif
 #endif
 
 
+/* Python 3.14 */
+
+#if PY_VERSION_HEX < 0x030E00A8
+#  define PyUnstable_Object_IsUniquelyReferenced(op) (Py_REFCNT((op)) == 1)
+#endif
+
 /* Other Python implementations */
 /* Other Python implementations */
 
 
 #endif  // HAVE_PYTHON
 #endif  // HAVE_PYTHON

+ 2 - 75
dtool/src/interrogatedb/py_panda.I

@@ -59,63 +59,6 @@ DtoolInstance_GetPointer(PyObject *self, T *&into, Dtool_PyTypedObject &target_c
   return false;
   return false;
 }
 }
 
 
-/**
- * Function to create a hash from a wrapped Python object.
- */
-INLINE Py_hash_t DtoolInstance_HashPointer(PyObject *self) {
-  if (self != nullptr && DtoolInstance_Check(self)) {
-    return (Py_hash_t)(intptr_t)DtoolInstance_VOID_PTR(self);
-  }
-  return -1;
-}
-
-/**
- * Python 2-style comparison function that compares objects by pointer.
- */
-INLINE int DtoolInstance_ComparePointers(PyObject *v1, PyObject *v2) {
-  void *v1_this = DtoolInstance_Check(v1) ? DtoolInstance_VOID_PTR(v1) : nullptr;
-  void *v2_this = DtoolInstance_Check(v2) ? DtoolInstance_VOID_PTR(v2) : nullptr;
-  if (v1_this != nullptr && v2_this != nullptr) {
-    return (v1_this > v2_this) - (v1_this < v2_this);
-  } else {
-    return (v1 > v2) - (v1 < v2);
-  }
-}
-
-/**
- * Rich comparison function that compares objects by pointer.
- */
-INLINE PyObject *DtoolInstance_RichComparePointers(PyObject *v1, PyObject *v2, int op) {
-  int cmpval = DtoolInstance_ComparePointers(v1, v2);
-  Py_RETURN_RICHCOMPARE(cmpval, 0, op);
-}
-
-/**
- * Utility function for assigning a PyObject pointer while managing refcounts.
- */
-ALWAYS_INLINE void
-Dtool_Assign_PyObject(PyObject *&ptr, PyObject *value) {
-  PyObject *prev_value = ptr;
-  if (prev_value != value) {
-    ptr = Py_XNewRef(value);
-    Py_XDECREF(prev_value);
-  }
-}
-
-/**
- * Converts the enum value to a C long.
- */
-INLINE long Dtool_EnumValue_AsLong(PyObject *value) {
-  PyObject *val = PyObject_GetAttrString(value, "value");
-  if (val != nullptr) {
-    long as_long = PyLongOrInt_AS_LONG(val);
-    Py_DECREF(val);
-    return as_long;
-  } else {
-    return -1;
-  }
-}
-
 /**
 /**
  * These functions wrap a pointer for a class that defines get_type_handle().
  * These functions wrap a pointer for a class that defines get_type_handle().
  */
  */
@@ -167,23 +110,6 @@ DTool_PyInit_Finalize(PyObject *self, void *local_this, Dtool_PyTypedObject *typ
   return 0;
   return 0;
 }
 }
 
 
-/**
- * Checks that the tuple is empty.
- */
-ALWAYS_INLINE bool
-Dtool_CheckNoArgs(PyObject *args) {
-  return PyTuple_GET_SIZE(args) == 0;
-}
-
-/**
- * Checks that the tuple is empty, and that the dict is empty or NULL.
- */
-ALWAYS_INLINE bool
-Dtool_CheckNoArgs(PyObject *args, PyObject *kwds) {
-  return PyTuple_GET_SIZE(args) == 0 &&
-    (kwds == nullptr || PyDict_GET_SIZE(kwds) == 0);
-}
-
 /**
 /**
  * The following functions wrap an arbitrary C++ value into a PyObject.
  * The following functions wrap an arbitrary C++ value into a PyObject.
  */
  */
@@ -324,7 +250,8 @@ ALWAYS_INLINE PyObject *Dtool_WrapValue(PyObject *value) {
   return value;
   return value;
 }
 }
 
 
-ALWAYS_INLINE PyObject *Dtool_WrapValue(const vector_uchar &value) {
+template<class Allocator>
+ALWAYS_INLINE PyObject *Dtool_WrapValue(const std::vector<unsigned char, Allocator> &value) {
 #if PY_MAJOR_VERSION >= 3
 #if PY_MAJOR_VERSION >= 3
   return PyBytes_FromStringAndSize((char *)value.data(), (Py_ssize_t)value.size());
   return PyBytes_FromStringAndSize((char *)value.data(), (Py_ssize_t)value.size());
 #else
 #else

+ 9 - 234
dtool/src/interrogatedb/py_panda.h

@@ -8,15 +8,14 @@
 #include <set>
 #include <set>
 #include <map>
 #include <map>
 #include <string>
 #include <string>
+#include <vector>
 
 
 #ifdef USE_DEBUG_PYTHON
 #ifdef USE_DEBUG_PYTHON
 #define  Py_DEBUG
 #define  Py_DEBUG
 #endif
 #endif
 
 
 #include "pnotify.h"
 #include "pnotify.h"
-#include "vector_uchar.h"
 #include "register_type.h"
 #include "register_type.h"
-#include "interrogate_request.h"
 
 
 #if defined(HAVE_PYTHON) && !defined(CPPPARSER)
 #if defined(HAVE_PYTHON) && !defined(CPPPARSER)
 
 
@@ -24,8 +23,6 @@
 #include "py_compat.h"
 #include "py_compat.h"
 #include <structmember.h>
 #include <structmember.h>
 
 
-using namespace std;
-
 // this is tempory .. untill this is glued better into the panda build system
 // this is tempory .. untill this is glued better into the panda build system
 
 
 #if defined(_WIN32) && !defined(LINK_ALL_STATIC)
 #if defined(_WIN32) && !defined(LINK_ALL_STATIC)
@@ -86,83 +83,6 @@ struct Dtool_PyTypedObject {
   CoerceFunction _Dtool_Coerce;
   CoerceFunction _Dtool_Coerce;
 };
 };
 
 
-// This is now simply a forward declaration.  The actual definition is created
-// by the code generator.
-#define Define_Dtool_Class(MODULE_NAME, CLASS_NAME, PUBLIC_NAME) \
-  extern Dtool_PyTypedObject Dtool_##CLASS_NAME;
-
-// More Macro(s) to Implement class functions.. Usually used if C++ needs type
-// information
-#define Define_Dtool_new(CLASS_NAME,CNAME)\
-static PyObject *Dtool_new_##CLASS_NAME(PyTypeObject *type, PyObject *args, PyObject *kwds) {\
-  (void) args; (void) kwds;\
-  PyObject *self = type->tp_alloc(type, 0);\
-  ((Dtool_PyInstDef *)self)->_signature = PY_PANDA_SIGNATURE;\
-  ((Dtool_PyInstDef *)self)->_My_Type = &Dtool_##CLASS_NAME;\
-  return self;\
-}
-
-// The following used to be in the above macro, but it doesn't seem to be
-// necessary as tp_alloc memsets the object to 0.
-//  ((Dtool_PyInstDef *)self)->_ptr_to_object = NULL;
-//  ((Dtool_PyInstDef *)self)->_memory_rules = false;
-//  ((Dtool_PyInstDef *)self)->_is_const = false;
-
-// Delete functions..
-#ifdef NDEBUG
-#define Define_Dtool_FreeInstance_Private(CLASS_NAME,CNAME)\
-static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\
-  Py_TYPE(self)->tp_free(self);\
-}
-#else // NDEBUG
-#define Define_Dtool_FreeInstance_Private(CLASS_NAME,CNAME)\
-static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\
-  if (DtoolInstance_VOID_PTR(self) != nullptr) {\
-    if (((Dtool_PyInstDef *)self)->_memory_rules) {\
-      std::cerr << "Detected leak for " << #CLASS_NAME \
-           << " which interrogate cannot delete.\n"; \
-    }\
-  }\
-  Py_TYPE(self)->tp_free(self);\
-}
-#endif  // NDEBUG
-
-#define Define_Dtool_FreeInstance(CLASS_NAME,CNAME)\
-static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\
-  if (DtoolInstance_VOID_PTR(self) != nullptr) {\
-    if (((Dtool_PyInstDef *)self)->_memory_rules) {\
-      delete (CNAME *)DtoolInstance_VOID_PTR(self);\
-    }\
-  }\
-  Py_TYPE(self)->tp_free(self);\
-}
-
-#define Define_Dtool_FreeInstanceRef(CLASS_NAME,CNAME)\
-static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\
-  if (DtoolInstance_VOID_PTR(self) != nullptr) {\
-    if (((Dtool_PyInstDef *)self)->_memory_rules) {\
-      unref_delete((CNAME *)DtoolInstance_VOID_PTR(self));\
-    }\
-  }\
-  Py_TYPE(self)->tp_free(self);\
-}
-
-#define Define_Dtool_FreeInstanceRef_Private(CLASS_NAME,CNAME)\
-static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\
-  if (DtoolInstance_VOID_PTR(self) != nullptr) {\
-    if (((Dtool_PyInstDef *)self)->_memory_rules) {\
-      unref_delete((ReferenceCount *)(CNAME *)DtoolInstance_VOID_PTR(self));\
-    }\
-  }\
-  Py_TYPE(self)->tp_free(self);\
-}
-
-#define Define_Dtool_Simple_FreeInstance(CLASS_NAME, CNAME)\
-static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\
-  ((Dtool_InstDef_##CLASS_NAME *)self)->_value.~##CLASS_NAME();\
-  Py_TYPE(self)->tp_free(self);\
-}
-
 // Extract the PyTypeObject pointer corresponding to a Dtool_PyTypedObject.
 // Extract the PyTypeObject pointer corresponding to a Dtool_PyTypedObject.
 #define Dtool_GetPyTypeObject(type) (&(type)->_PyType)
 #define Dtool_GetPyTypeObject(type) (&(type)->_PyType)
 
 
@@ -178,31 +98,12 @@ static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\
 #define DtoolInstance_INIT_PTR(obj, ptr) { ((Dtool_PyInstDef *)obj)->_ptr_to_object = (void*)(ptr); }
 #define DtoolInstance_INIT_PTR(obj, ptr) { ((Dtool_PyInstDef *)obj)->_ptr_to_object = (void*)(ptr); }
 #define DtoolInstance_UPCAST(obj, type) (((Dtool_PyInstDef *)(obj))->_My_Type->_Dtool_UpcastInterface((obj), &(type)))
 #define DtoolInstance_UPCAST(obj, type) (((Dtool_PyInstDef *)(obj))->_My_Type->_Dtool_UpcastInterface((obj), &(type)))
 
 
-// ** HACK ** allert.. Need to keep a runtime type dictionary ... that is
-// forward declared of typed object.  We rely on the fact that typed objects
-// are uniquly defined by an integer.
-
-#if PY_VERSION_HEX >= 0x030d0000
-class Dtool_TypeMap : public std::map<std::string, Dtool_PyTypedObject *> {
-public:
-  PyMutex _lock { 0 };
-};
-#else
-typedef std::map<std::string, Dtool_PyTypedObject *> Dtool_TypeMap;
-#endif
-
-EXPCL_PYPANDA Dtool_TypeMap *Dtool_GetGlobalTypeMap();
-
 class DtoolProxy {
 class DtoolProxy {
 public:
 public:
   mutable PyObject *_self;
   mutable PyObject *_self;
   TypeHandle _type;
   TypeHandle _type;
 };
 };
 
 
-EXPCL_PYPANDA void DtoolProxy_Init(DtoolProxy *proxy, PyObject *self,
-                                   Dtool_PyTypedObject &classdef,
-                                   TypeRegistry::PythonWrapFunc *wrap_func);
-
 /**
 /**
 
 
  */
  */
@@ -218,23 +119,11 @@ EXPCL_PYPANDA bool Dtool_Call_ExtractThisPointer_NonConst(PyObject *self, Dtool_
 template<class T> INLINE bool DtoolInstance_GetPointer(PyObject *self, T *&into);
 template<class T> INLINE bool DtoolInstance_GetPointer(PyObject *self, T *&into);
 template<class T> INLINE bool DtoolInstance_GetPointer(PyObject *self, T *&into, Dtool_PyTypedObject &classdef);
 template<class T> INLINE bool DtoolInstance_GetPointer(PyObject *self, T *&into, Dtool_PyTypedObject &classdef);
 
 
-INLINE Py_hash_t DtoolInstance_HashPointer(PyObject *self);
-INLINE int DtoolInstance_ComparePointers(PyObject *v1, PyObject *v2);
-INLINE PyObject *DtoolInstance_RichComparePointers(PyObject *v1, PyObject *v2, int op);
-
-// Functions related to error reporting.
-EXPCL_PYPANDA bool _Dtool_CheckErrorOccurred();
-
-#ifdef NDEBUG
-#define Dtool_CheckErrorOccurred() (UNLIKELY(PyErr_Occurred() != nullptr))
-#else
-#define Dtool_CheckErrorOccurred() (UNLIKELY(_Dtool_CheckErrorOccurred()))
-#endif
-
 EXPCL_PYPANDA PyObject *Dtool_Raise_AssertionError();
 EXPCL_PYPANDA PyObject *Dtool_Raise_AssertionError();
 EXPCL_PYPANDA PyObject *Dtool_Raise_TypeError(const char *message);
 EXPCL_PYPANDA PyObject *Dtool_Raise_TypeError(const char *message);
 EXPCL_PYPANDA PyObject *Dtool_Raise_ArgTypeError(PyObject *obj, int param, const char *function_name, const char *type_name);
 EXPCL_PYPANDA PyObject *Dtool_Raise_ArgTypeError(PyObject *obj, int param, const char *function_name, const char *type_name);
 EXPCL_PYPANDA PyObject *Dtool_Raise_AttributeError(PyObject *obj, const char *attribute);
 EXPCL_PYPANDA PyObject *Dtool_Raise_AttributeError(PyObject *obj, const char *attribute);
+EXPCL_PYPANDA int Dtool_Raise_CantDeleteAttributeError(const char *attribute);
 
 
 EXPCL_PYPANDA PyObject *_Dtool_Raise_BadArgumentsError();
 EXPCL_PYPANDA PyObject *_Dtool_Raise_BadArgumentsError();
 EXPCL_PYPANDA PyObject *_Dtool_Raise_BadArgumentsError(const char *message);
 EXPCL_PYPANDA PyObject *_Dtool_Raise_BadArgumentsError(const char *message);
@@ -250,31 +139,6 @@ EXPCL_PYPANDA int _Dtool_Raise_BadArgumentsError_Int(const char *message);
 #define Dtool_Raise_BadArgumentsError_Int(x) _Dtool_Raise_BadArgumentsError_Int(x)
 #define Dtool_Raise_BadArgumentsError_Int(x) _Dtool_Raise_BadArgumentsError_Int(x)
 #endif
 #endif
 
 
-// These functions are similar to Dtool_WrapValue, except that they also
-// contain code for checking assertions and exceptions when compiling with
-// NDEBUG mode on.
-EXPCL_PYPANDA PyObject *_Dtool_Return_None();
-EXPCL_PYPANDA PyObject *Dtool_Return_Bool(bool value);
-EXPCL_PYPANDA PyObject *_Dtool_Return(PyObject *value);
-
-#ifdef NDEBUG
-#define Dtool_Return_None() (LIKELY(PyErr_Occurred() == nullptr) ? (Py_NewRef(Py_None)) : nullptr)
-#define Dtool_Return(value) (LIKELY(PyErr_Occurred() == nullptr) ? value : nullptr)
-#else
-#define Dtool_Return_None() _Dtool_Return_None()
-#define Dtool_Return(value) _Dtool_Return(value)
-#endif
-
-ALWAYS_INLINE void Dtool_Assign_PyObject(PyObject *&ptr, PyObject *value);
-
-/**
- * Wrapper around Python 3.4's enum library, which does not have a C API.
- */
-EXPCL_PYPANDA PyTypeObject *Dtool_EnumType_Create(const char *name, PyObject *names,
-                                                  const char *module = nullptr);
-INLINE long Dtool_EnumValue_AsLong(PyObject *value);
-
-
 /**
 /**
 
 
  */
  */
@@ -291,102 +155,9 @@ template<class T> INLINE PyObject *DTool_CreatePyInstance(T *obj, bool memory_ru
 template<class T> INLINE PyObject *DTool_CreatePyInstanceTyped(const T *obj, bool memory_rules);
 template<class T> INLINE PyObject *DTool_CreatePyInstanceTyped(const T *obj, bool memory_rules);
 template<class T> INLINE PyObject *DTool_CreatePyInstanceTyped(T *obj, bool memory_rules);
 template<class T> INLINE PyObject *DTool_CreatePyInstanceTyped(T *obj, bool memory_rules);
 
 
-// Macro(s) class definition .. Used to allocate storage and init some values
-// for a Dtool Py Type object.
-
-// struct Dtool_PyTypedObject Dtool_##CLASS_NAME;
-
-#define Define_Module_Class_Internal(MODULE_NAME,CLASS_NAME,CNAME)\
-extern struct Dtool_PyTypedObject Dtool_##CLASS_NAME;\
-static int Dtool_Init_##CLASS_NAME(PyObject *self, PyObject *args, PyObject *kwds);\
-static PyObject *Dtool_new_##CLASS_NAME(PyTypeObject *type, PyObject *args, PyObject *kwds);
-
-#define Define_Module_Class(MODULE_NAME,CLASS_NAME,CNAME,PUBLIC_NAME)\
-Define_Module_Class_Internal(MODULE_NAME,CLASS_NAME,CNAME)\
-Define_Dtool_new(CLASS_NAME,CNAME)\
-Define_Dtool_FreeInstance(CLASS_NAME,CNAME)\
-Define_Dtool_Class(MODULE_NAME,CLASS_NAME,PUBLIC_NAME)
-
-#define Define_Module_Class_Private(MODULE_NAME,CLASS_NAME,CNAME,PUBLIC_NAME)\
-Define_Module_Class_Internal(MODULE_NAME,CLASS_NAME,CNAME)\
-Define_Dtool_new(CLASS_NAME,CNAME)\
-Define_Dtool_FreeInstance_Private(CLASS_NAME,CNAME)\
-Define_Dtool_Class(MODULE_NAME,CLASS_NAME,PUBLIC_NAME)
-
-#define Define_Module_ClassRef_Private(MODULE_NAME,CLASS_NAME,CNAME,PUBLIC_NAME)\
-Define_Module_Class_Internal(MODULE_NAME,CLASS_NAME,CNAME)\
-Define_Dtool_new(CLASS_NAME,CNAME)\
-Define_Dtool_FreeInstanceRef_Private(CLASS_NAME,CNAME)\
-Define_Dtool_Class(MODULE_NAME,CLASS_NAME,PUBLIC_NAME)
-
-#define Define_Module_ClassRef(MODULE_NAME,CLASS_NAME,CNAME,PUBLIC_NAME)\
-Define_Module_Class_Internal(MODULE_NAME,CLASS_NAME,CNAME)\
-Define_Dtool_new(CLASS_NAME,CNAME)\
-Define_Dtool_FreeInstanceRef(CLASS_NAME,CNAME)\
-Define_Dtool_Class(MODULE_NAME,CLASS_NAME,PUBLIC_NAME)
-
 // The finalizer for simple instances.
 // The finalizer for simple instances.
 INLINE int DTool_PyInit_Finalize(PyObject *self, void *This, Dtool_PyTypedObject *type, bool memory_rules, bool is_const);
 INLINE int DTool_PyInit_Finalize(PyObject *self, void *This, Dtool_PyTypedObject *type, bool memory_rules, bool is_const);
 
 
-// A heler function to glu methed definition together .. that can not be done
-// at code generation time becouse of multiple generation passes in
-// interigate..
-typedef std::map<std::string, PyMethodDef *> MethodDefmap;
-
-// We need a way to runtime merge compile units into a python "Module" .. this
-// is done with the fallowing structors and code.. along with the support of
-// interigate_module
-
-struct Dtool_TypeDef {
-  const char *const name;
-  Dtool_PyTypedObject *type;
-};
-
-struct LibraryDef {
-  PyMethodDef *const _methods;
-  const Dtool_TypeDef *const _types;
-  Dtool_TypeDef *const _external_types;
-  const InterrogateModuleDef *const _module_def;
-};
-
-#if PY_MAJOR_VERSION >= 3
-EXPCL_PYPANDA PyObject *Dtool_PyModuleInitHelper(const LibraryDef *defs[], PyModuleDef *module_def);
-#else
-EXPCL_PYPANDA PyObject *Dtool_PyModuleInitHelper(const LibraryDef *defs[], const char *modulename);
-#endif
-
-// HACK.... Be carefull Dtool_BorrowThisReference This function can be used to
-// grab the "THIS" pointer from an object and use it Required to support fom
-// historical inharatence in the for of "is this instance of"..
-EXPCL_PYPANDA PyObject *Dtool_BorrowThisReference(PyObject *self, PyObject *args);
-
-#define DTOOL_PyObject_HashPointer DtoolInstance_HashPointer
-#define DTOOL_PyObject_ComparePointers DtoolInstance_ComparePointers
-
-EXPCL_PYPANDA PyObject *
-copy_from_make_copy(PyObject *self, PyObject *noargs);
-
-EXPCL_PYPANDA PyObject *
-copy_from_copy_constructor(PyObject *self, PyObject *noargs);
-
-EXPCL_PYPANDA PyObject *
-map_deepcopy_to_copy(PyObject *self, PyObject *args);
-
-/**
- * These functions check whether the arguments passed to a function conform to
- * certain expectations.
- */
-ALWAYS_INLINE bool Dtool_CheckNoArgs(PyObject *args);
-ALWAYS_INLINE bool Dtool_CheckNoArgs(PyObject *args, PyObject *kwds);
-EXPCL_PYPANDA bool Dtool_ExtractArg(PyObject **result, PyObject *args,
-                                    PyObject *kwds, const char *keyword);
-EXPCL_PYPANDA bool Dtool_ExtractArg(PyObject **result, PyObject *args,
-                                    PyObject *kwds);
-EXPCL_PYPANDA bool Dtool_ExtractOptionalArg(PyObject **result, PyObject *args,
-                                            PyObject *kwds, const char *keyword);
-EXPCL_PYPANDA  bool Dtool_ExtractOptionalArg(PyObject **result, PyObject *args,
-                                             PyObject *kwds);
-
 /**
 /**
  * These functions convert a C++ value into the corresponding Python object.
  * These functions convert a C++ value into the corresponding Python object.
  * This used to be generated by the code generator, but it seems more reliable
  * This used to be generated by the code generator, but it seems more reliable
@@ -411,7 +182,8 @@ ALWAYS_INLINE PyObject *Dtool_WrapValue(char value);
 ALWAYS_INLINE PyObject *Dtool_WrapValue(wchar_t value);
 ALWAYS_INLINE PyObject *Dtool_WrapValue(wchar_t value);
 ALWAYS_INLINE PyObject *Dtool_WrapValue(std::nullptr_t);
 ALWAYS_INLINE PyObject *Dtool_WrapValue(std::nullptr_t);
 ALWAYS_INLINE PyObject *Dtool_WrapValue(PyObject *value);
 ALWAYS_INLINE PyObject *Dtool_WrapValue(PyObject *value);
-ALWAYS_INLINE PyObject *Dtool_WrapValue(const vector_uchar &value);
+template<class Allocator>
+ALWAYS_INLINE PyObject *Dtool_WrapValue(const std::vector<unsigned char, Allocator> &value);
 
 
 #if PY_MAJOR_VERSION >= 0x02060000
 #if PY_MAJOR_VERSION >= 0x02060000
 ALWAYS_INLINE PyObject *Dtool_WrapValue(Py_buffer *value);
 ALWAYS_INLINE PyObject *Dtool_WrapValue(Py_buffer *value);
@@ -422,9 +194,12 @@ ALWAYS_INLINE PyObject *Dtool_WrapValue(const std::pair<T1, T2> &value);
 
 
 EXPCL_PYPANDA Dtool_PyTypedObject *Dtool_GetSuperBase();
 EXPCL_PYPANDA Dtool_PyTypedObject *Dtool_GetSuperBase();
 
 
-#include "py_panda.I"
+/**
+ * Creates a Python generator object from a next() function.
+ */
+EXPCL_PYPANDA PyObject *Dtool_NewGenerator(PyObject *self, iternextfunc func);
 
 
-#include "py_wrappers.h"
+#include "py_panda.I"
 
 
 #endif  // HAVE_PYTHON && !CPPPARSER
 #endif  // HAVE_PYTHON && !CPPPARSER
 
 

+ 0 - 61
dtool/src/interrogatedb/py_wrappers.h

@@ -1,61 +0,0 @@
-/**
- * @file py_wrappers.h
- * @author rdb
- * @date 2017-11-26
- */
-
-#ifndef PY_WRAPPERS_H
-#define PY_WRAPPERS_H
-
-#include "py_panda.h"
-
-#ifdef HAVE_PYTHON
-
-/**
- * These classes are returned from properties that require a subscript
- * interface, ie. something.children[i] = 3.
- */
-struct Dtool_WrapperBase {
-  PyObject_HEAD;
-  PyObject *_self;
-  const char *_name;
-};
-
-struct Dtool_SequenceWrapper {
-  Dtool_WrapperBase _base;
-  lenfunc _len_func;
-  ssizeargfunc _getitem_func;
-};
-
-struct Dtool_MutableSequenceWrapper {
-  Dtool_WrapperBase _base;
-  lenfunc _len_func;
-  ssizeargfunc _getitem_func;
-  ssizeobjargproc _setitem_func;
-  PyObject *(*_insert_func)(PyObject *, size_t, PyObject *);
-};
-
-struct Dtool_MappingWrapper {
-  union {
-    Dtool_WrapperBase _base;
-    Dtool_SequenceWrapper _keys;
-  };
-  binaryfunc _getitem_func;
-  objobjargproc _setitem_func;
-};
-
-struct Dtool_GeneratorWrapper {
-  Dtool_WrapperBase _base;
-  iternextfunc _iternext_func;
-};
-
-EXPCL_PYPANDA Dtool_SequenceWrapper *Dtool_NewSequenceWrapper(PyObject *self, const char *name);
-EXPCL_PYPANDA Dtool_MutableSequenceWrapper *Dtool_NewMutableSequenceWrapper(PyObject *self, const char *name);
-EXPCL_PYPANDA Dtool_MappingWrapper *Dtool_NewMappingWrapper(PyObject *self, const char *name);
-EXPCL_PYPANDA Dtool_MappingWrapper *Dtool_NewMutableMappingWrapper(PyObject *self, const char *name);
-EXPCL_PYPANDA PyObject *Dtool_NewGenerator(PyObject *self, iternextfunc func);
-EXPCL_PYPANDA PyObject *Dtool_NewStaticProperty(PyTypeObject *obj, const PyGetSetDef *getset);
-
-#endif  // HAVE_PYTHON
-
-#endif  // PY_WRAPPERS_H

+ 0 - 0
dtool/src/parser-inc/DbgHelp.h


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

@@ -52,4 +52,9 @@ PyObject _Py_FalseStruct;
 
 
 typedef void *visitproc;
 typedef void *visitproc;
 
 
+typedef enum {
+  PyRefTracer_CREATE = 0,
+  PyRefTracer_DESTROY = 1,
+} PyRefTracerEvent;
+
 #endif  // PYTHON_H
 #endif  // PYTHON_H

+ 1 - 0
dtool/src/parser-inc/emmintrin.h

@@ -0,0 +1 @@
+#include <xmmintrin.h>

+ 29 - 0
dtool/src/parser-inc/random

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <initializer_list>
+
+namespace std {
+  class random_device;
+  class seed_seq;
+
+  template<class IntType = int> class uniform_int_distribution;
+  template<class RealType = double> class uniform_real_distribution;
+  class bernoulli_distribution;
+  template<class IntType = int> class binomial_distribution;
+  template<class IntType = int> class geometric_distribution;
+  template<class IntType = int> class negative_binomial_distribution;
+  template<class IntType = int> class poisson_distribution;
+  template<class RealType = double> class exponential_distribution;
+  template<class RealType = double> class gamma_distribution;
+  template<class RealType = double> class weibull_distribution;
+  template<class RealType = double> class extreme_value_distribution;
+  template<class RealType = double> class normal_distribution;
+  template<class RealType = double> class lognormal_distribution;
+  template<class RealType = double> class chi_squared_distribution;
+  template<class RealType = double> class cauchy_distribution;
+  template<class RealType = double> class fisher_f_distribution;
+  template<class RealType = double> class student_t_distribution;
+  template<class IntType = int> class discrete_distribution;
+  template<class RealType = double> class piecewise_constant_distribution;
+  template<class RealType = double> class piecewise_linear_distribution;
+}

+ 15 - 0
dtool/src/prc/configPageManager.cxx

@@ -492,6 +492,21 @@ reload_implicit_pages() {
   _currently_loading = false;
   _currently_loading = false;
   invalidate_cache();
   invalidate_cache();
 
 
+  // These important variables are updated here to avoid recursion, since
+  // they may be accessed by the PRC system.
+  ConfigVariableBool notify_timestamp
+    ("notify-timestamp", false,
+     PRC_DESC("Set true to output the date & time with each notify message."));
+  NotifyCategory::_notify_timestamp = notify_timestamp;
+
+#ifndef NDEBUG
+  ConfigVariableBool check_debug_notify_protect
+    ("check-debug-notify-protect", false,
+     PRC_DESC("Set true to issue a warning message if a debug or spam "
+              "notify output is not protected within an if statement."));
+  NotifyCategory::_check_debug_notify_protect = check_debug_notify_protect;
+#endif
+
 #ifdef USE_PANDAFILESTREAM
 #ifdef USE_PANDAFILESTREAM
   // Update this very low-level config variable here, for lack of any better
   // Update this very low-level config variable here, for lack of any better
   // place.
   // place.

+ 180 - 5
dtool/src/prc/notify.cxx

@@ -35,6 +35,18 @@
 #include "emscriptenLogStream.h"
 #include "emscriptenLogStream.h"
 #endif
 #endif
 
 
+#ifndef NDEBUG
+#ifdef PHAVE_EXECINFO_H
+#include <execinfo.h>  // for backtrace()
+#include <dlfcn.h>
+#include <cxxabi.h>
+#endif
+
+#ifdef _WIN32
+#include <dbghelp.h>
+#endif
+#endif  // NDEBUG
+
 using std::cerr;
 using std::cerr;
 using std::cout;
 using std::cout;
 using std::ostream;
 using std::ostream;
@@ -348,15 +360,16 @@ assert_failure(const char *expression, int line,
     << expression << " at line " << line << " of " << source_file;
     << expression << " at line " << line << " of " << source_file;
   string message = message_str.str();
   string message = message_str.str();
 
 
-  if (!_assert_failed) {
+  Notify *self = ptr();
+  if (!self->_assert_failed) {
     // We only save the first assertion failure message, as this is usually
     // We only save the first assertion failure message, as this is usually
     // the most meaningful when several occur in a row.
     // the most meaningful when several occur in a row.
-    _assert_failed = true;
-    _assert_error_message = message;
+    self->_assert_failed = true;
+    self->_assert_error_message = message;
   }
   }
 
 
-  if (has_assert_handler()) {
-    return (*_assert_handler)(expression, line, source_file);
+  if (self->has_assert_handler()) {
+    return (*self->_assert_handler)(expression, line, source_file);
   }
   }
 
 
 #ifdef ANDROID
 #ifdef ANDROID
@@ -380,6 +393,33 @@ assert_failure(const char *expression, int line,
     // Make sure the error message has been flushed to the output.
     // Make sure the error message has been flushed to the output.
     nout.flush();
     nout.flush();
 
 
+    // Capture and list a stack trace.
+#ifdef NDEBUG
+#elif defined(PHAVE_EXECINFO_H)
+    void *trace[64];
+    int size = backtrace(trace, 64);
+    if (size > 0) {
+      // Remove the frame(s) corresponding to the current function.
+      void **tracep = trace;
+      void *return_addr = __builtin_return_address(0);
+      for (int i = 0; i < size; ++i) {
+        if (trace[i] == return_addr) {
+          tracep = trace + (i + 1);
+          size -= i + 1;
+          break;
+        }
+      }
+      write_backtrace(tracep, size);
+    }
+#elif defined(_WIN32)
+    const ULONG max_size = 62;
+    void *trace[max_size];
+    int size = CaptureStackBackTrace(1, max_size, trace, nullptr);
+    if (size > 0) {
+      write_backtrace(trace, size);
+    }
+#endif
+
 #ifdef _MSC_VER
 #ifdef _MSC_VER
     // How to trigger an exception in VC++ that offers to take us into the
     // How to trigger an exception in VC++ that offers to take us into the
     // debugger?  abort() doesn't do it.  We used to be able to assert(false),
     // debugger?  abort() doesn't do it.  We used to be able to assert(false),
@@ -411,6 +451,141 @@ assert_failure(const char *expression, int line,
   return true;
   return true;
 }
 }
 
 
+/**
+ *
+ */
+void Notify::
+write_backtrace(void **trace, int size) {
+  std::ostream &out = nout;
+
+#ifdef NDEBUG
+#elif defined(PHAVE_EXECINFO_H)
+  char namebuf[128];
+
+  for (int i = 0; i < size; ++i) {
+    void *addr = trace[i];
+    Dl_info info;
+    if (dladdr((char *)addr - 1, &info) != 0) {
+      const char *name = nullptr;
+
+      if (info.dli_sname != nullptr) {
+        int status = 0;
+        size_t size = 0;
+        char *demangled = nullptr;
+        if (info.dli_sname[0] == '_') {
+          size = sizeof(namebuf) - 1;
+          demangled = abi::__cxa_demangle(info.dli_sname, namebuf, &size, &status);
+        }
+        if (status == 0 && demangled != nullptr) {
+          namebuf[size] = 0;
+          name = demangled;
+          //if (strncmp(name, "Notify::assert_failure", 22) == 0) {
+            //continue;
+          //}
+        } else {
+          name = info.dli_sname;
+        }
+      }
+
+      out << "[" << addr << "] ";
+
+      if (info.dli_fname != nullptr) {
+        const char *slash = strrchr(info.dli_fname, '/');
+        out << std::left << std::setw(30) << (slash ? slash + 1 : info.dli_fname) << " ";
+      }
+
+      if (name != nullptr) {
+        out << name;
+      } else {
+        out << info.dli_saddr;
+      }
+
+      int offset = reinterpret_cast<uintptr_t>(addr) - reinterpret_cast<uintptr_t>(info.dli_saddr);
+      if (offset > 0) {
+        out << " + " << offset;
+      }
+      out << "\n";
+    } else {
+      out << "[" << addr << "]\n";
+    }
+  }
+#elif defined(_WIN32)
+  HMODULE handle = LoadLibraryA("dbghelp.dll");
+  if (!handle) {
+    return;
+  }
+
+  auto pSymInitialize = (BOOL (WINAPI *)(HANDLE, PCSTR, BOOL))GetProcAddress(handle, "SymInitialize");
+  auto pSymCleanup = (BOOL (WINAPI *)(HANDLE))GetProcAddress(handle, "SymCleanup");
+  auto pSymFromAddr = (BOOL (WINAPI *)(HANDLE, DWORD64, DWORD64 *, PSYMBOL_INFO))GetProcAddress(handle, "SymFromAddr");
+  auto pSymGetModuleInfo64 = (BOOL (WINAPI *)(HANDLE, DWORD64, PIMAGEHLP_MODULE64))GetProcAddress(handle, "SymGetModuleInfo64");
+  auto pSymGetLineFromAddr64 = (BOOL (WINAPI *)(HANDLE, DWORD64, PDWORD, PIMAGEHLP_LINE64))GetProcAddress(handle, "SymGetLineFromAddr64");
+  if (!pSymInitialize || !pSymCleanup || !pSymFromAddr) {
+    FreeLibrary(handle);
+    return;
+  }
+
+  HANDLE process = GetCurrentProcess();
+  pSymInitialize(process, nullptr, TRUE);
+
+  for (int i = 0; i < size; ++i) {
+    DWORD64 addr = (DWORD64)trace[i];
+
+    alignas(SYMBOL_INFO) char buffer[sizeof(SYMBOL_INFO) + 256] = {0};
+    SYMBOL_INFO *symbol = (SYMBOL_INFO *)buffer;
+    symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
+    symbol->MaxNameLen = 255;
+
+    out << "[" << (void *)addr << "]";
+
+    // Show the name of the library.
+    IMAGEHLP_MODULE64 module;
+    module.SizeOfStruct = sizeof(module);
+    const char *basename = "???";
+    if (pSymGetModuleInfo64 && pSymGetModuleInfo64(process, addr, &module)) {
+      const char *slash = strrchr(module.ImageName, '\\');
+      basename = (slash ? slash + 1 : module.ImageName);
+    }
+
+    // Look up the symbol name.  If the reported name comes from the export
+    // table and the address is way off, ignore it, it's probably not right.
+    if (pSymFromAddr(process, addr, nullptr, symbol) &&
+        ((symbol->Flags & SYMFLAG_EXPORT) == 0 || addr - (DWORD64)symbol->Address < 0x4000)) {
+
+      out << " " << std::left << std::setw(30) << basename << " " << symbol->Name;
+
+      DWORD64 offset = addr - (DWORD64)(symbol->Address);
+      if (offset > 0) {
+        out << " + " << offset;
+      }
+
+      // Look up filename + line number if available.
+      IMAGEHLP_LINE64 line;
+      DWORD displacement = 0;
+      line.SizeOfStruct = sizeof(line);
+      if (pSymGetLineFromAddr64(process, addr, &displacement, &line)) {
+        const char *slash = strrchr(line.FileName, '\\');
+        const char *basename = (slash ? slash + 1 : line.FileName);
+
+        out << " (" << basename << ":" << line.LineNumber;
+        //if (displacement > 0) {
+        //  out << " + " << displacement;
+        //}
+        out << ")";
+      }
+    } else {
+      out << " " << basename;
+    }
+    out << "\n";
+  }
+
+  pSymCleanup(process);
+  FreeLibrary(handle);
+#endif
+
+  out.flush();
+}
+
 /**
 /**
  * Given a string, one of "debug", "info", "warning", etc., return the
  * Given a string, one of "debug", "info", "warning", etc., return the
  * corresponding Severity level, or NS_unspecified if none of the strings
  * corresponding Severity level, or NS_unspecified if none of the strings

+ 5 - 35
dtool/src/prc/notifyCategory.cxx

@@ -21,6 +21,9 @@
 #include <time.h>  // for strftime().
 #include <time.h>  // for strftime().
 #include <assert.h>
 #include <assert.h>
 
 
+bool NotifyCategory::_notify_timestamp = false;
+bool NotifyCategory::_check_debug_notify_protect = false;
+
 long NotifyCategory::_server_delta = 0;
 long NotifyCategory::_server_delta = 0;
 
 
 /**
 /**
@@ -55,7 +58,7 @@ std::ostream &NotifyCategory::
 out(NotifySeverity severity, bool prefix) const {
 out(NotifySeverity severity, bool prefix) const {
   if (is_on(severity)) {
   if (is_on(severity)) {
     if (prefix) {
     if (prefix) {
-      if (get_notify_timestamp()) {
+      if (_notify_timestamp) {
         // Format a timestamp to include as a prefix as well.
         // Format a timestamp to include as a prefix as well.
         time_t now = time(nullptr) + _server_delta;
         time_t now = time(nullptr) + _server_delta;
         struct tm atm;
         struct tm atm;
@@ -79,7 +82,7 @@ out(NotifySeverity severity, bool prefix) const {
       return Notify::out(severity);
       return Notify::out(severity);
     }
     }
 
 
-  } else if (severity <= NS_debug && get_check_debug_notify_protect()) {
+  } else if (severity <= NS_debug && _check_debug_notify_protect) {
     // Someone issued a debug Notify output statement without protecting it
     // Someone issued a debug Notify output statement without protecting it
     // within an if statement.  This can cause a significant runtime
     // within an if statement.  This can cause a significant runtime
     // performance hit, since it forces the iostream library to fully format
     // performance hit, since it forces the iostream library to fully format
@@ -172,36 +175,3 @@ update_severity_cache() {
 
 
   mark_cache_valid(_local_modified);
   mark_cache_valid(_local_modified);
 }
 }
-
-/**
- * Returns the value of the notify-timestamp ConfigVariable.  This is defined
- * using a method accessor rather than a static ConfigVariableBool, to protect
- * against the variable needing to be accessed at static init time.
- */
-bool NotifyCategory::
-get_notify_timestamp() {
-  static ConfigVariableBool *notify_timestamp = nullptr;
-  if (notify_timestamp == nullptr) {
-    notify_timestamp = new ConfigVariableBool
-      ("notify-timestamp", false,
-       "Set true to output the date & time with each notify message.");
-  }
-  return *notify_timestamp;
-}
-
-/**
- * Returns the value of the check-debug-notify-protect ConfigVariable.  This
- * is defined using a method accessor rather than a static ConfigVariableBool,
- * to protect against the variable needing to be accessed at static init time.
- */
-bool NotifyCategory::
-get_check_debug_notify_protect() {
-  static ConfigVariableBool *check_debug_notify_protect = nullptr;
-  if (check_debug_notify_protect == nullptr) {
-    check_debug_notify_protect = new ConfigVariableBool
-      ("check-debug-notify-protect", false,
-       "Set true to issue a warning message if a debug or spam "
-       "notify output is not protected within an if statement.");
-  }
-  return *check_debug_notify_protect;
-}

+ 4 - 2
dtool/src/prc/notifyCategory.h

@@ -76,8 +76,6 @@ PUBLISHED:
 private:
 private:
   std::string get_config_name() const;
   std::string get_config_name() const;
   void update_severity_cache();
   void update_severity_cache();
-  static bool get_notify_timestamp();
-  static bool get_check_debug_notify_protect();
 
 
   std::string _fullname;
   std::string _fullname;
   std::string _basename;
   std::string _basename;
@@ -86,12 +84,16 @@ private:
   typedef std::vector<NotifyCategory *> Children;
   typedef std::vector<NotifyCategory *> Children;
   Children _children;
   Children _children;
 
 
+  static bool _notify_timestamp;
+  static bool _check_debug_notify_protect;
+
   static long _server_delta; // not a time_t because server delta may be signed.
   static long _server_delta; // not a time_t because server delta may be signed.
 
 
   AtomicAdjust::Integer _local_modified;
   AtomicAdjust::Integer _local_modified;
   NotifySeverity _severity_cache;
   NotifySeverity _severity_cache;
 
 
   friend class Notify;
   friend class Notify;
+  friend class ConfigPageManager;
 };
 };
 
 
 INLINE std::ostream &operator << (std::ostream &out, const NotifyCategory &cat);
 INLINE std::ostream &operator << (std::ostream &out, const NotifyCategory &cat);

+ 10 - 8
dtool/src/prc/pnotify.h

@@ -76,10 +76,12 @@ PUBLISHED:
 public:
 public:
   static ios_fmtflags get_literal_flag();
   static ios_fmtflags get_literal_flag();
 
 
-  bool assert_failure(const std::string &expression, int line,
-                      const char *source_file);
-  bool assert_failure(const char *expression, int line,
-                      const char *source_file);
+  static bool assert_failure(const std::string &expression, int line,
+                             const char *source_file);
+  static bool assert_failure(const char *expression, int line,
+                             const char *source_file);
+
+  static void write_backtrace(void **trace, int size);
 
 
   static NotifySeverity string_severity(const std::string &string);
   static NotifySeverity string_severity(const std::string &string);
 
 
@@ -204,7 +206,7 @@ private:
 #define nassertr(condition, return_value) \
 #define nassertr(condition, return_value) \
   { \
   { \
     if (_nassert_check(condition)) { \
     if (_nassert_check(condition)) { \
-      if (Notify::ptr()->assert_failure(#condition, __LINE__, __FILE__)) { \
+      if (Notify::assert_failure(#condition, __LINE__, __FILE__)) { \
         return return_value; \
         return return_value; \
       } \
       } \
     } \
     } \
@@ -213,7 +215,7 @@ private:
 #define nassertv(condition) \
 #define nassertv(condition) \
   { \
   { \
     if (_nassert_check(condition)) { \
     if (_nassert_check(condition)) { \
-      if (Notify::ptr()->assert_failure(#condition, __LINE__, __FILE__)) { \
+      if (Notify::assert_failure(#condition, __LINE__, __FILE__)) { \
         return; \
         return; \
       } \
       } \
     } \
     } \
@@ -221,12 +223,12 @@ private:
 
 
 #define nassertd(condition) \
 #define nassertd(condition) \
   if (_nassert_check(condition) && \
   if (_nassert_check(condition) && \
-      Notify::ptr()->assert_failure(#condition, __LINE__, __FILE__))
+      Notify::assert_failure(#condition, __LINE__, __FILE__))
 
 
 #define nassertr_always(condition, return_value) nassertr(condition, return_value)
 #define nassertr_always(condition, return_value) nassertr(condition, return_value)
 #define nassertv_always(condition) nassertv(condition)
 #define nassertv_always(condition) nassertv(condition)
 
 
-#define nassert_raise(message) Notify::ptr()->assert_failure(message, __LINE__, __FILE__)
+#define nassert_raise(message) Notify::assert_failure(message, __LINE__, __FILE__)
 
 
 #endif  // NDEBUG
 #endif  // NDEBUG
 
 

+ 51 - 0
dtool/src/prckeys/makePrcKey.cxx

@@ -114,6 +114,56 @@ output_c_string(std::ostream &out, const string &string_name,
  */
  */
 EVP_PKEY *
 EVP_PKEY *
 generate_key() {
 generate_key() {
+#if OPENSSL_VERSION_MAJOR >= 3
+  EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
+  if (ctx == nullptr) {
+    EVP_PKEY_CTX_free(ctx);
+    output_ssl_errors();
+    exit(1);
+  }
+
+  if (EVP_PKEY_keygen_init(ctx) <= 0 ||
+      EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 1024) <= 0) {
+    EVP_PKEY_CTX_free(ctx);
+    output_ssl_errors();
+    exit(1);
+  }
+
+  BIGNUM *e = BN_new();
+  if (e == nullptr || !BN_set_word(e, 7)) {
+    BN_free(e);
+    EVP_PKEY_CTX_free(ctx);
+    output_ssl_errors();
+    exit(1);
+  }
+
+  unsigned char *e_buf = nullptr;
+  int e_len = BN_num_bytes(e);
+  e_buf = (unsigned char *)OPENSSL_malloc(e_len);
+  BN_bn2bin(e, e_buf);
+
+  size_t bits = 1024;
+  OSSL_PARAM params[] = {
+    OSSL_PARAM_construct_size_t("bits", &bits),
+    OSSL_PARAM_construct_BN("e", e_buf, e_len),
+    OSSL_PARAM_END
+  };
+
+  EVP_PKEY *pkey = nullptr;
+  if (EVP_PKEY_CTX_set_params(ctx, params) <= 0 ||
+      EVP_PKEY_generate(ctx, &pkey) <= 0) {
+    OPENSSL_free(e_buf);
+    BN_free(e);
+    EVP_PKEY_CTX_free(ctx);
+    output_ssl_errors();
+    exit(1);
+  }
+
+  OPENSSL_free(e_buf);
+  BN_free(e);
+  EVP_PKEY_CTX_free(ctx);
+  return pkey;
+#else
   RSA *rsa = RSA_new();
   RSA *rsa = RSA_new();
   BIGNUM *e = BN_new();
   BIGNUM *e = BN_new();
   if (rsa == nullptr || e == nullptr) {
   if (rsa == nullptr || e == nullptr) {
@@ -134,6 +184,7 @@ generate_key() {
   EVP_PKEY *pkey = EVP_PKEY_new();
   EVP_PKEY *pkey = EVP_PKEY_new();
   EVP_PKEY_assign_RSA(pkey, rsa);
   EVP_PKEY_assign_RSA(pkey, rsa);
   return pkey;
   return pkey;
+#endif
 }
 }
 
 
 /**
 /**

+ 2 - 2
makepanda/makepackage.py

@@ -335,8 +335,8 @@ def MakeInstallerLinux(version, debversion=None, rpmversion=None, rpmrelease=1,
 
 
         # Library dependencies are required, binary dependencies are recommended.
         # Library dependencies are required, binary dependencies are recommended.
         # We explicitly exclude libphysx-extras since we don't want to depend on PhysX.
         # We explicitly exclude libphysx-extras since we don't want to depend on PhysX.
-        oscmd(f"cd targetroot && LD_LIBRARY_PATH=usr/{lib_dir}/panda3d {dpkg_shlibdeps} -Tdebian/substvars_dep --ignore-missing-info -x{pkg_name} -xlibphysx-extras {lib_pattern}")
-        oscmd(f"cd targetroot && LD_LIBRARY_PATH=usr/{lib_dir}/panda3d {dpkg_shlibdeps} -Tdebian/substvars_rec --ignore-missing-info -x{pkg_name} {bin_pattern}")
+        oscmd(f"cd targetroot && {dpkg_shlibdeps} -lusr/{lib_dir}/panda3d -Tdebian/substvars_dep --ignore-missing-info -x{pkg_name} -xlibphysx-extras {lib_pattern}")
+        oscmd(f"cd targetroot && {dpkg_shlibdeps} -lusr/{lib_dir}/panda3d -Tdebian/substvars_rec --ignore-missing-info -x{pkg_name} {bin_pattern}")
 
 
         # Parse the substvars files generated by dpkg-shlibdeps.
         # Parse the substvars files generated by dpkg-shlibdeps.
         depends = ReadFile("targetroot/debian/substvars_dep").replace("shlibs:Depends=", "").strip()
         depends = ReadFile("targetroot/debian/substvars_dep").replace("shlibs:Depends=", "").strip()

+ 3 - 0
makepanda/makepanda.py

@@ -2489,6 +2489,7 @@ DTOOL_CONFIG=[
     ("PHAVE_DIRENT_H",                 'UNDEF',                  '1'),
     ("PHAVE_DIRENT_H",                 'UNDEF',                  '1'),
     ("PHAVE_UCONTEXT_H",               'UNDEF',                  '1'),
     ("PHAVE_UCONTEXT_H",               'UNDEF',                  '1'),
     ("PHAVE_STDINT_H",                 '1',                      '1'),
     ("PHAVE_STDINT_H",                 '1',                      '1'),
+    ("PHAVE_EXECINFO_H",               'UNDEF',                  '1'),
     ("HAVE_RTTI",                      '1',                      '1'),
     ("HAVE_RTTI",                      '1',                      '1'),
     ("HAVE_X11",                       'UNDEF',                  '1'),
     ("HAVE_X11",                       'UNDEF',                  '1'),
     ("IS_LINUX",                       'UNDEF',                  '1'),
     ("IS_LINUX",                       'UNDEF',                  '1'),
@@ -2617,6 +2618,7 @@ def WriteConfigSettings():
         dtool_config["PHAVE_GLOB_H"] = 'UNDEF'
         dtool_config["PHAVE_GLOB_H"] = 'UNDEF'
         dtool_config["PHAVE_LOCKF"] = 'UNDEF'
         dtool_config["PHAVE_LOCKF"] = 'UNDEF'
         dtool_config["HAVE_VIDEO4LINUX"] = 'UNDEF'
         dtool_config["HAVE_VIDEO4LINUX"] = 'UNDEF'
+        dtool_config["PHAVE_EXECINFO_H"] = 'UNDEF'
 
 
     if (GetTarget() == "emscripten"):
     if (GetTarget() == "emscripten"):
         # There are no threads in JavaScript, so don't bother using them.
         # There are no threads in JavaScript, so don't bother using them.
@@ -2629,6 +2631,7 @@ def WriteConfigSettings():
         dtool_config["PHAVE_LINUX_INPUT_H"] = 'UNDEF'
         dtool_config["PHAVE_LINUX_INPUT_H"] = 'UNDEF'
         dtool_config["HAVE_X11"] = 'UNDEF'
         dtool_config["HAVE_X11"] = 'UNDEF'
         dtool_config["HAVE_GLX"] = 'UNDEF'
         dtool_config["HAVE_GLX"] = 'UNDEF'
+        dtool_config["PHAVE_EXECINFO_H"] = 'UNDEF'
 
 
         # There are no environment vars either, or default prc files.
         # There are no environment vars either, or default prc files.
         prc_parameters["DEFAULT_PRC_DIR"] = 'UNDEF'
         prc_parameters["DEFAULT_PRC_DIR"] = 'UNDEF'

+ 2 - 2
makepanda/makepandacore.py

@@ -589,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.5.0.dist-info")):
-            oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.5.0" % (sys.executable, dir))
+        if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.7.1.dist-info")):
+            oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.7.1" % (sys.executable, dir))
 
 
         INTERROGATE_DIR = dir
         INTERROGATE_DIR = dir
 
 

+ 10 - 5
panda/src/collide/collisionBox.cxx

@@ -343,7 +343,8 @@ test_intersection_from_line(const CollisionEntry &entry) const {
   const CollisionLine *line;
   const CollisionLine *line;
   DCAST_INTO_R(line, entry.get_from(), nullptr);
   DCAST_INTO_R(line, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_origin = line->get_origin() * wrt_mat;
   LPoint3 from_origin = line->get_origin() * wrt_mat;
   LVector3 from_direction = line->get_direction() * wrt_mat;
   LVector3 from_direction = line->get_direction() * wrt_mat;
@@ -386,7 +387,8 @@ PT(CollisionEntry) CollisionBox::
 test_intersection_from_ray(const CollisionEntry &entry) const {
 test_intersection_from_ray(const CollisionEntry &entry) const {
   const CollisionRay *ray;
   const CollisionRay *ray;
   DCAST_INTO_R(ray, entry.get_from(), nullptr);
   DCAST_INTO_R(ray, entry.get_from(), nullptr);
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_origin = ray->get_origin() * wrt_mat;
   LPoint3 from_origin = ray->get_origin() * wrt_mat;
   LVector3 from_direction = ray->get_direction() * wrt_mat;
   LVector3 from_direction = ray->get_direction() * wrt_mat;
@@ -511,7 +513,8 @@ PT(CollisionEntry) CollisionBox::
 test_intersection_from_segment(const CollisionEntry &entry) const {
 test_intersection_from_segment(const CollisionEntry &entry) const {
   const CollisionSegment *seg;
   const CollisionSegment *seg;
   DCAST_INTO_R(seg, entry.get_from(), nullptr);
   DCAST_INTO_R(seg, entry.get_from(), nullptr);
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_origin = seg->get_point_a() * wrt_mat;
   LPoint3 from_origin = seg->get_point_a() * wrt_mat;
   LPoint3 from_extent = seg->get_point_b() * wrt_mat;
   LPoint3 from_extent = seg->get_point_b() * wrt_mat;
@@ -566,7 +569,8 @@ test_intersection_from_capsule(const CollisionEntry &entry) const {
   const CollisionCapsule *capsule;
   const CollisionCapsule *capsule;
   DCAST_INTO_R(capsule, entry.get_from(), nullptr);
   DCAST_INTO_R(capsule, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_a = capsule->get_point_a() * wrt_mat;
   LPoint3 from_a = capsule->get_point_a() * wrt_mat;
   LPoint3 from_b = capsule->get_point_b() * wrt_mat;
   LPoint3 from_b = capsule->get_point_b() * wrt_mat;
@@ -711,7 +715,8 @@ test_intersection_from_box(const CollisionEntry &entry) const {
   const CollisionBox *box;
   const CollisionBox *box;
   DCAST_INTO_R(box, entry.get_from(), nullptr);
   DCAST_INTO_R(box, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 diff = wrt_mat.xform_point_general(box->get_center()) - _center;
   LPoint3 diff = wrt_mat.xform_point_general(box->get_center()) - _center;
   LVector3 from_extents = box->get_dimensions() * 0.5f;
   LVector3 from_extents = box->get_dimensions() * 0.5f;

+ 10 - 5
panda/src/collide/collisionCapsule.cxx

@@ -308,7 +308,8 @@ test_intersection_from_line(const CollisionEntry &entry) const {
   const CollisionLine *line;
   const CollisionLine *line;
   DCAST_INTO_R(line, entry.get_from(), nullptr);
   DCAST_INTO_R(line, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_origin = line->get_origin() * wrt_mat;
   LPoint3 from_origin = line->get_origin() * wrt_mat;
   LVector3 from_direction = line->get_direction() * wrt_mat;
   LVector3 from_direction = line->get_direction() * wrt_mat;
@@ -356,7 +357,8 @@ test_intersection_from_ray(const CollisionEntry &entry) const {
   const CollisionRay *ray;
   const CollisionRay *ray;
   DCAST_INTO_R(ray, entry.get_from(), nullptr);
   DCAST_INTO_R(ray, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_origin = ray->get_origin() * wrt_mat;
   LPoint3 from_origin = ray->get_origin() * wrt_mat;
   LVector3 from_direction = ray->get_direction() * wrt_mat;
   LVector3 from_direction = ray->get_direction() * wrt_mat;
@@ -417,7 +419,8 @@ test_intersection_from_segment(const CollisionEntry &entry) const {
   const CollisionSegment *segment;
   const CollisionSegment *segment;
   DCAST_INTO_R(segment, entry.get_from(), nullptr);
   DCAST_INTO_R(segment, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_a = segment->get_point_a() * wrt_mat;
   LPoint3 from_a = segment->get_point_a() * wrt_mat;
   LPoint3 from_b = segment->get_point_b() * wrt_mat;
   LPoint3 from_b = segment->get_point_b() * wrt_mat;
@@ -484,7 +487,8 @@ test_intersection_from_capsule(const CollisionEntry &entry) const {
   LPoint3 into_a = _a;
   LPoint3 into_a = _a;
   LVector3 into_direction = _b - into_a;
   LVector3 into_direction = _b - into_a;
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_a = capsule->get_point_a() * wrt_mat;
   LPoint3 from_a = capsule->get_point_a() * wrt_mat;
   LPoint3 from_b = capsule->get_point_b() * wrt_mat;
   LPoint3 from_b = capsule->get_point_b() * wrt_mat;
@@ -545,7 +549,8 @@ test_intersection_from_parabola(const CollisionEntry &entry) const {
   const CollisionParabola *parabola;
   const CollisionParabola *parabola;
   DCAST_INTO_R(parabola, entry.get_from(), nullptr);
   DCAST_INTO_R(parabola, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   // Convert the parabola into local coordinate space.
   // Convert the parabola into local coordinate space.
   LParabola local_p(parabola->get_parabola());
   LParabola local_p(parabola->get_parabola());

+ 3 - 3
panda/src/collide/collisionEntry.I

@@ -291,7 +291,7 @@ get_wrt_prev_space() const {
 /**
 /**
  * Returns the relative transform of the from node as seen from the into node.
  * Returns the relative transform of the from node as seen from the into node.
  */
  */
-INLINE const LMatrix4 &CollisionEntry::
+INLINE LMatrix4 CollisionEntry::
 get_wrt_mat() const {
 get_wrt_mat() const {
   return get_wrt_space()->get_mat();
   return get_wrt_space()->get_mat();
 }
 }
@@ -299,7 +299,7 @@ get_wrt_mat() const {
 /**
 /**
  * Returns the relative transform of the into node as seen from the from node.
  * Returns the relative transform of the into node as seen from the from node.
  */
  */
-INLINE const LMatrix4 &CollisionEntry::
+INLINE LMatrix4 CollisionEntry::
 get_inv_wrt_mat() const {
 get_inv_wrt_mat() const {
   return get_inv_wrt_space()->get_mat();
   return get_inv_wrt_space()->get_mat();
 }
 }
@@ -309,7 +309,7 @@ get_inv_wrt_mat() const {
  * as of the previous frame (according to set_prev_transform(),
  * as of the previous frame (according to set_prev_transform(),
  * set_fluid_pos(), etc.)
  * set_fluid_pos(), etc.)
  */
  */
-INLINE const LMatrix4 &CollisionEntry::
+INLINE LMatrix4 CollisionEntry::
 get_wrt_prev_mat() const {
 get_wrt_prev_mat() const {
   return get_wrt_prev_space()->get_mat();
   return get_wrt_prev_space()->get_mat();
 }
 }

+ 3 - 3
panda/src/collide/collisionEntry.h

@@ -109,9 +109,9 @@ public:
   INLINE CPT(TransformState) get_inv_wrt_space() const;
   INLINE CPT(TransformState) get_inv_wrt_space() const;
   INLINE CPT(TransformState) get_wrt_prev_space() const;
   INLINE CPT(TransformState) get_wrt_prev_space() const;
 
 
-  INLINE const LMatrix4 &get_wrt_mat() const;
-  INLINE const LMatrix4 &get_inv_wrt_mat() const;
-  INLINE const LMatrix4 &get_wrt_prev_mat() const;
+  INLINE LMatrix4 get_wrt_mat() const;
+  INLINE LMatrix4 get_inv_wrt_mat() const;
+  INLINE LMatrix4 get_wrt_prev_mat() const;
 
 
   INLINE const ClipPlaneAttrib *get_into_clip_planes() const;
   INLINE const ClipPlaneAttrib *get_into_clip_planes() const;
 
 

+ 6 - 2
panda/src/collide/collisionFloorMesh.cxx

@@ -130,7 +130,9 @@ PT(CollisionEntry) CollisionFloorMesh::
 test_intersection_from_ray(const CollisionEntry &entry) const {
 test_intersection_from_ray(const CollisionEntry &entry) const {
   const CollisionRay *ray;
   const CollisionRay *ray;
   DCAST_INTO_R(ray, entry.get_from(), nullptr);
   DCAST_INTO_R(ray, entry.get_from(), nullptr);
-  LPoint3 from_origin = ray->get_origin() * entry.get_wrt_mat();
+
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  LPoint3 from_origin = ray->get_origin() * wrt_space->get_mat();
 
 
   double fx = from_origin[0];
   double fx = from_origin[0];
   double fy = from_origin[1];
   double fy = from_origin[1];
@@ -195,7 +197,9 @@ PT(CollisionEntry) CollisionFloorMesh::
 test_intersection_from_sphere(const CollisionEntry &entry) const {
 test_intersection_from_sphere(const CollisionEntry &entry) const {
   const CollisionSphere *sphere;
   const CollisionSphere *sphere;
   DCAST_INTO_R(sphere, entry.get_from(), nullptr);
   DCAST_INTO_R(sphere, entry.get_from(), nullptr);
-  LPoint3 from_origin = sphere->get_center() * entry.get_wrt_mat();
+
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  LPoint3 from_origin = sphere->get_center() * wrt_space->get_mat();
 
 
   double fx = from_origin[0];
   double fx = from_origin[0];
   double fy = from_origin[1];
   double fy = from_origin[1];

+ 12 - 6
panda/src/collide/collisionInvSphere.cxx

@@ -97,7 +97,8 @@ test_intersection_from_sphere(const CollisionEntry &entry) const {
   const CollisionSphere *sphere;
   const CollisionSphere *sphere;
   DCAST_INTO_R(sphere, entry.get_from(), nullptr);
   DCAST_INTO_R(sphere, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_center = sphere->get_center() * wrt_mat;
   LPoint3 from_center = sphere->get_center() * wrt_mat;
   LVector3 from_radius_v =
   LVector3 from_radius_v =
@@ -149,7 +150,8 @@ test_intersection_from_line(const CollisionEntry &entry) const {
   const CollisionLine *line;
   const CollisionLine *line;
   DCAST_INTO_R(line, entry.get_from(), nullptr);
   DCAST_INTO_R(line, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_origin = line->get_origin() * wrt_mat;
   LPoint3 from_origin = line->get_origin() * wrt_mat;
   LVector3 from_direction = line->get_direction() * wrt_mat;
   LVector3 from_direction = line->get_direction() * wrt_mat;
@@ -190,7 +192,8 @@ test_intersection_from_ray(const CollisionEntry &entry) const {
   const CollisionRay *ray;
   const CollisionRay *ray;
   DCAST_INTO_R(ray, entry.get_from(), nullptr);
   DCAST_INTO_R(ray, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_origin = ray->get_origin() * wrt_mat;
   LPoint3 from_origin = ray->get_origin() * wrt_mat;
   LVector3 from_direction = ray->get_direction() * wrt_mat;
   LVector3 from_direction = ray->get_direction() * wrt_mat;
@@ -233,7 +236,8 @@ test_intersection_from_segment(const CollisionEntry &entry) const {
   const CollisionSegment *segment;
   const CollisionSegment *segment;
   DCAST_INTO_R(segment, entry.get_from(), nullptr);
   DCAST_INTO_R(segment, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_a = segment->get_point_a() * wrt_mat;
   LPoint3 from_a = segment->get_point_a() * wrt_mat;
   LPoint3 from_b = segment->get_point_b() * wrt_mat;
   LPoint3 from_b = segment->get_point_b() * wrt_mat;
@@ -411,7 +415,8 @@ test_intersection_from_capsule(const CollisionEntry &entry) const {
   const CollisionCapsule *capsule;
   const CollisionCapsule *capsule;
   DCAST_INTO_R(capsule, entry.get_from(), nullptr);
   DCAST_INTO_R(capsule, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_a = capsule->get_point_a() * wrt_mat;
   LPoint3 from_a = capsule->get_point_a() * wrt_mat;
   LPoint3 from_b = capsule->get_point_b() * wrt_mat;
   LPoint3 from_b = capsule->get_point_b() * wrt_mat;
@@ -466,7 +471,8 @@ test_intersection_from_box(const CollisionEntry &entry) const {
   const CollisionBox *box;
   const CollisionBox *box;
   DCAST_INTO_R(box, entry.get_from(), nullptr);
   DCAST_INTO_R(box, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 center = get_center();
   LPoint3 center = get_center();
   PN_stdfloat radius_sq = get_radius();
   PN_stdfloat radius_sq = get_radius();

+ 11 - 1
panda/src/collide/collisionNode_ext.cxx

@@ -53,7 +53,17 @@ void Extension<CollisionNode>::
 set_owner(PyObject *owner) {
 set_owner(PyObject *owner) {
   if (owner != Py_None) {
   if (owner != Py_None) {
     PyObject *ref = PyWeakref_NewRef(owner, nullptr);
     PyObject *ref = PyWeakref_NewRef(owner, nullptr);
-    _this->set_owner(ref, [](void *obj) { Py_DECREF((PyObject *)obj); });
+    _this->set_owner(ref, [](void *obj) {
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+      // Use PyGILState to protect this asynchronous call.
+      PyGILState_STATE gstate;
+      gstate = PyGILState_Ensure();
+#endif
+      Py_DECREF((PyObject *)obj);
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+      PyGILState_Release(gstate);
+#endif
+    });
   } else {
   } else {
     _this->clear_owner();
     _this->clear_owner();
   }
   }

+ 14 - 7
panda/src/collide/collisionPlane.cxx

@@ -109,7 +109,8 @@ test_intersection_from_sphere(const CollisionEntry &entry) const {
   const CollisionSphere *sphere;
   const CollisionSphere *sphere;
   DCAST_INTO_R(sphere, entry.get_from(), nullptr);
   DCAST_INTO_R(sphere, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_center = sphere->get_center() * wrt_mat;
   LPoint3 from_center = sphere->get_center() * wrt_mat;
   LVector3 from_radius_v =
   LVector3 from_radius_v =
@@ -148,7 +149,8 @@ test_intersection_from_line(const CollisionEntry &entry) const {
   const CollisionLine *line;
   const CollisionLine *line;
   DCAST_INTO_R(line, entry.get_from(), nullptr);
   DCAST_INTO_R(line, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_origin = line->get_origin() * wrt_mat;
   LPoint3 from_origin = line->get_origin() * wrt_mat;
   LVector3 from_direction = line->get_direction() * wrt_mat;
   LVector3 from_direction = line->get_direction() * wrt_mat;
@@ -193,7 +195,8 @@ test_intersection_from_ray(const CollisionEntry &entry) const {
   const CollisionRay *ray;
   const CollisionRay *ray;
   DCAST_INTO_R(ray, entry.get_from(), nullptr);
   DCAST_INTO_R(ray, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_origin = ray->get_origin() * wrt_mat;
   LPoint3 from_origin = ray->get_origin() * wrt_mat;
   LVector3 from_direction = ray->get_direction() * wrt_mat;
   LVector3 from_direction = ray->get_direction() * wrt_mat;
@@ -245,7 +248,8 @@ test_intersection_from_segment(const CollisionEntry &entry) const {
   const CollisionSegment *segment;
   const CollisionSegment *segment;
   DCAST_INTO_R(segment, entry.get_from(), nullptr);
   DCAST_INTO_R(segment, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_a = segment->get_point_a() * wrt_mat;
   LPoint3 from_a = segment->get_point_a() * wrt_mat;
   LPoint3 from_b = segment->get_point_b() * wrt_mat;
   LPoint3 from_b = segment->get_point_b() * wrt_mat;
@@ -302,7 +306,8 @@ test_intersection_from_capsule(const CollisionEntry &entry) const {
   const CollisionCapsule *capsule;
   const CollisionCapsule *capsule;
   DCAST_INTO_R(capsule, entry.get_from(), nullptr);
   DCAST_INTO_R(capsule, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_a = capsule->get_point_a() * wrt_mat;
   LPoint3 from_a = capsule->get_point_a() * wrt_mat;
   LPoint3 from_b = capsule->get_point_b() * wrt_mat;
   LPoint3 from_b = capsule->get_point_b() * wrt_mat;
@@ -373,7 +378,8 @@ test_intersection_from_parabola(const CollisionEntry &entry) const {
   const CollisionParabola *parabola;
   const CollisionParabola *parabola;
   DCAST_INTO_R(parabola, entry.get_from(), nullptr);
   DCAST_INTO_R(parabola, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   // Convert the parabola into local coordinate space.
   // Convert the parabola into local coordinate space.
   LParabola local_p(parabola->get_parabola());
   LParabola local_p(parabola->get_parabola());
@@ -439,7 +445,8 @@ test_intersection_from_box(const CollisionEntry &entry) const {
   const CollisionBox *box;
   const CollisionBox *box;
   DCAST_INTO_R(box, entry.get_from(), nullptr);
   DCAST_INTO_R(box, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_center = box->get_center() * wrt_mat;
   LPoint3 from_center = box->get_center() * wrt_mat;
   LVector3 from_extents = box->get_dimensions() * 0.5f;
   LVector3 from_extents = box->get_dimensions() * 0.5f;

+ 8 - 4
panda/src/collide/collisionPolygon.cxx

@@ -584,7 +584,8 @@ test_intersection_from_line(const CollisionEntry &entry) const {
   const CollisionLine *line;
   const CollisionLine *line;
   DCAST_INTO_R(line, entry.get_from(), nullptr);
   DCAST_INTO_R(line, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_origin = line->get_origin() * wrt_mat;
   LPoint3 from_origin = line->get_origin() * wrt_mat;
   LVector3 from_direction = line->get_direction() * wrt_mat;
   LVector3 from_direction = line->get_direction() * wrt_mat;
@@ -652,7 +653,8 @@ test_intersection_from_ray(const CollisionEntry &entry) const {
   const CollisionRay *ray;
   const CollisionRay *ray;
   DCAST_INTO_R(ray, entry.get_from(), nullptr);
   DCAST_INTO_R(ray, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_origin = ray->get_origin() * wrt_mat;
   LPoint3 from_origin = ray->get_origin() * wrt_mat;
   LVector3 from_direction = ray->get_direction() * wrt_mat;
   LVector3 from_direction = ray->get_direction() * wrt_mat;
@@ -725,7 +727,8 @@ test_intersection_from_segment(const CollisionEntry &entry) const {
   const CollisionSegment *segment;
   const CollisionSegment *segment;
   DCAST_INTO_R(segment, entry.get_from(), nullptr);
   DCAST_INTO_R(segment, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_a = segment->get_point_a() * wrt_mat;
   LPoint3 from_a = segment->get_point_a() * wrt_mat;
   LPoint3 from_b = segment->get_point_b() * wrt_mat;
   LPoint3 from_b = segment->get_point_b() * wrt_mat;
@@ -1047,7 +1050,8 @@ test_intersection_from_parabola(const CollisionEntry &entry) const {
   const CollisionParabola *parabola;
   const CollisionParabola *parabola;
   DCAST_INTO_R(parabola, entry.get_from(), nullptr);
   DCAST_INTO_R(parabola, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   // Convert the parabola into local coordinate space.
   // Convert the parabola into local coordinate space.
   LParabola local_p(parabola->get_parabola());
   LParabola local_p(parabola->get_parabola());

+ 16 - 9
panda/src/collide/collisionSphere.cxx

@@ -242,7 +242,8 @@ test_intersection_from_line(const CollisionEntry &entry) const {
   const CollisionLine *line;
   const CollisionLine *line;
   DCAST_INTO_R(line, entry.get_from(), nullptr);
   DCAST_INTO_R(line, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_origin = line->get_origin() * wrt_mat;
   LPoint3 from_origin = line->get_origin() * wrt_mat;
   LVector3 from_direction = line->get_direction() * wrt_mat;
   LVector3 from_direction = line->get_direction() * wrt_mat;
@@ -284,10 +285,11 @@ test_intersection_from_box(const CollisionEntry &entry) const {
 
 
   // Instead of transforming the box into the sphere's coordinate space, we do
   // Instead of transforming the box into the sphere's coordinate space, we do
   // it the other way around.  It's easier that way.
   // it the other way around.  It's easier that way.
-  const LMatrix4 &wrt_mat = entry.get_inv_wrt_mat();
+  CPT(TransformState) inv_wrt_space = entry.get_inv_wrt_space();
+  const LMatrix4 &inv_wrt_mat = inv_wrt_space->get_mat();
 
 
-  LPoint3 center = wrt_mat.xform_point(_center);
-  PN_stdfloat radius_sq = wrt_mat.xform_vec(LVector3(0, 0, _radius)).length_squared();
+  LPoint3 center = inv_wrt_mat.xform_point(_center);
+  PN_stdfloat radius_sq = inv_wrt_mat.xform_vec(LVector3(0, 0, _radius)).length_squared();
 
 
   LPoint3 box_min = box->get_min();
   LPoint3 box_min = box->get_min();
   LPoint3 box_max = box->get_max();
   LPoint3 box_max = box->get_max();
@@ -336,7 +338,8 @@ test_intersection_from_box(const CollisionEntry &entry) const {
   PT(CollisionEntry) new_entry = new CollisionEntry(entry);
   PT(CollisionEntry) new_entry = new CollisionEntry(entry);
 
 
   // To get the interior point, clamp the sphere center to the AABB.
   // To get the interior point, clamp the sphere center to the AABB.
-  LPoint3 interior = entry.get_wrt_mat().xform_point(center.fmax(box_min).fmin(box_max));
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  LPoint3 interior = wrt_space->get_mat().xform_point(center.fmax(box_min).fmin(box_max));
   new_entry->set_interior_point(interior);
   new_entry->set_interior_point(interior);
 
 
   // Now extrapolate the surface point and normal from that.
   // Now extrapolate the surface point and normal from that.
@@ -358,7 +361,8 @@ test_intersection_from_ray(const CollisionEntry &entry) const {
   const CollisionRay *ray;
   const CollisionRay *ray;
   DCAST_INTO_R(ray, entry.get_from(), nullptr);
   DCAST_INTO_R(ray, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_origin = ray->get_origin() * wrt_mat;
   LPoint3 from_origin = ray->get_origin() * wrt_mat;
   LVector3 from_direction = ray->get_direction() * wrt_mat;
   LVector3 from_direction = ray->get_direction() * wrt_mat;
@@ -405,7 +409,8 @@ test_intersection_from_segment(const CollisionEntry &entry) const {
   const CollisionSegment *segment;
   const CollisionSegment *segment;
   DCAST_INTO_R(segment, entry.get_from(), nullptr);
   DCAST_INTO_R(segment, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_a = segment->get_point_a() * wrt_mat;
   LPoint3 from_a = segment->get_point_a() * wrt_mat;
   LPoint3 from_b = segment->get_point_b() * wrt_mat;
   LPoint3 from_b = segment->get_point_b() * wrt_mat;
@@ -454,7 +459,8 @@ test_intersection_from_capsule(const CollisionEntry &entry) const {
   const CollisionCapsule *capsule;
   const CollisionCapsule *capsule;
   DCAST_INTO_R(capsule, entry.get_from(), nullptr);
   DCAST_INTO_R(capsule, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   LPoint3 from_a = capsule->get_point_a() * wrt_mat;
   LPoint3 from_a = capsule->get_point_a() * wrt_mat;
   LPoint3 from_b = capsule->get_point_b() * wrt_mat;
   LPoint3 from_b = capsule->get_point_b() * wrt_mat;
@@ -510,7 +516,8 @@ test_intersection_from_parabola(const CollisionEntry &entry) const {
   const CollisionParabola *parabola;
   const CollisionParabola *parabola;
   DCAST_INTO_R(parabola, entry.get_from(), nullptr);
   DCAST_INTO_R(parabola, entry.get_from(), nullptr);
 
 
-  const LMatrix4 &wrt_mat = entry.get_wrt_mat();
+  CPT(TransformState) wrt_space = entry.get_wrt_space();
+  const LMatrix4 &wrt_mat = wrt_space->get_mat();
 
 
   // Convert the parabola into local coordinate space.
   // Convert the parabola into local coordinate space.
   LParabola local_p(parabola->get_parabola());
   LParabola local_p(parabola->get_parabola());

+ 3 - 3
panda/src/display/shaderInputBinding_impls.cxx

@@ -1355,7 +1355,6 @@ ShaderLightStructBinding(const ShaderType *type, const InternalName *input) {
       continue;
       continue;
     }
     }
 
 
-    PT(InternalName) fqname = InternalName::make("light")->append(member.name);
     if (member.name == "color") {
     if (member.name == "color") {
       _color_offset = member.offset;
       _color_offset = member.offset;
     }
     }
@@ -2295,7 +2294,8 @@ r_collect_members(const InternalName *name, const ShaderType *type, size_t offse
     for (uint32_t ai = 0; ai < array_type->get_num_elements(); ++ai) {
     for (uint32_t ai = 0; ai < array_type->get_num_elements(); ++ai) {
       sprintf(buffer + basename_size, "[%d]", (int)ai);
       sprintf(buffer + basename_size, "[%d]", (int)ai);
 
 
-      r_collect_members(name->get_parent()->append(buffer), element_type, offset);
+      PT(InternalName) fqname = name->get_parent()->append(buffer);
+      r_collect_members(fqname, element_type, offset);
       offset += stride;
       offset += stride;
     }
     }
   }
   }
@@ -3491,7 +3491,7 @@ make_binding_cg(const InternalName *name, const ShaderType *type) {
   if (pieces[0] == "k") {
   if (pieces[0] == "k") {
     //k_prefix = true;
     //k_prefix = true;
     name_str = name_str.substr(2);
     name_str = name_str.substr(2);
-    name = InternalName::make(name_str);
+    return make_shader_input(type, InternalName::make(name_str));
   }
   }
 
 
   // If we get here, it's not a specially recognized input, but just a regular
   // If we get here, it's not a specially recognized input, but just a regular

+ 12 - 5
panda/src/downloader/httpChannel_emscripten.cxx

@@ -373,9 +373,11 @@ run_headers_received() {
       var xhr = window._httpChannels[$0];
       var xhr = window._httpChannels[$0];
       var loaded = 0;
       var loaded = 0;
       xhr.onprogress = function (ev) {
       xhr.onprogress = function (ev) {
-        var chunk = this.responseText.slice(loaded, ev.loaded);
-        var ptr = __extend_string($1, chunk.length);
-        writeAsciiToMemory(chunk, ptr, true);
+        var body = this.responseText;
+        var ptr = __extend_string($1, ev.loaded - loaded);
+        for (var i = loaded; i < ev.loaded; ++i) {
+          HEAPU8[ptr + i] = body.charCodeAt(i);
+        }
         loaded = ev.loaded;
         loaded = ev.loaded;
       };
       };
     }, this, dest);
     }, this, dest);
@@ -388,8 +390,11 @@ run_headers_received() {
       var loaded = 0;
       var loaded = 0;
       xhr.onprogress = function (ev) {
       xhr.onprogress = function (ev) {
         while (loaded < ev.loaded) {
         while (loaded < ev.loaded) {
+          var body = this.responseText;
           var size = Math.min(ev.loaded - read, 4096);
           var size = Math.min(ev.loaded - read, 4096);
-          writeAsciiToMemory(this.responseText.substr(read, size), $2, true);
+          for (var i = read; i < read + size; ++i) {
+            HEAPU8[$2 + i] = body.charCodeAt(i);
+          }
           __write_stream($1, $2, size);
           __write_stream($1, $2, size);
           loaded += size;
           loaded += size;
         }
         }
@@ -700,7 +705,9 @@ download_to_ram(Ramfile *ramfile, bool subdocument_resumes) {
     var state = xhr.readyState;
     var state = xhr.readyState;
     var body = xhr.responseText;
     var body = xhr.responseText;
     var ptr = __extend_string($1, body.length);
     var ptr = __extend_string($1, body.length);
-    writeAsciiToMemory(body, ptr, true);
+    for (var i = 0; i < body.length; ++i) {
+      HEAPU8[ptr + i] = body.charCodeAt(i);
+    }
     return state;
     return state;
   }, this, &ramfile->_data);
   }, this, &ramfile->_data);
 
 

+ 11 - 2
panda/src/downloader/virtualFileHTTP.cxx

@@ -39,8 +39,7 @@ VirtualFileHTTP(VirtualFileMountHTTP *mount, const Filename &local_filename,
   _implicit_pz_file(implicit_pz_file),
   _implicit_pz_file(implicit_pz_file),
   _status_only(open_flags != 0)
   _status_only(open_flags != 0)
 {
 {
-  URLSpec url(_mount->get_root());
-  url.set_path(_mount->get_root().get_path() + _local_filename.c_str());
+  URLSpec url = get_url();
   _channel = _mount->get_channel();
   _channel = _mount->get_channel();
   if (_status_only) {
   if (_status_only) {
     _channel->get_header(url);
     _channel->get_header(url);
@@ -89,6 +88,16 @@ get_filename() const {
   }
   }
 }
 }
 
 
+/**
+ * Returns the full URL of this file.
+ */
+URLSpec VirtualFileHTTP::
+get_url() const {
+  URLSpec url(_mount->get_root());
+  url.set_path(_mount->get_root().get_path() + _local_filename.c_str());
+  return url;
+}
+
 /**
 /**
  * Returns true if this file exists, false otherwise.
  * Returns true if this file exists, false otherwise.
  */
  */

+ 4 - 0
panda/src/downloader/virtualFileHTTP.h

@@ -39,6 +39,7 @@ public:
 
 
   virtual VirtualFileSystem *get_file_system() const;
   virtual VirtualFileSystem *get_file_system() const;
   virtual Filename get_filename() const;
   virtual Filename get_filename() const;
+  URLSpec get_url() const;
 
 
   virtual bool has_file() const;
   virtual bool has_file() const;
   virtual bool is_directory() const;
   virtual bool is_directory() const;
@@ -54,6 +55,9 @@ public:
   virtual bool read_file(std::string &result, bool auto_unwrap) const;
   virtual bool read_file(std::string &result, bool auto_unwrap) const;
   virtual bool read_file(vector_uchar &result, bool auto_unwrap) const;
   virtual bool read_file(vector_uchar &result, bool auto_unwrap) const;
 
 
+PUBLISHED:
+  MAKE_PROPERTY(url, get_url);
+
 private:
 private:
   bool fetch_file(std::ostream *buffer_stream) const;
   bool fetch_file(std::ostream *buffer_stream) const;
   std::istream *return_file(std::istream *buffer_stream, bool auto_unwrap) const;
   std::istream *return_file(std::istream *buffer_stream, bool auto_unwrap) const;

+ 5 - 23
panda/src/egg2pg/eggSaver.cxx

@@ -1229,34 +1229,16 @@ apply_state_properties(EggRenderMode *egg_render_mode, const RenderState *state)
  */
  */
 bool EggSaver::
 bool EggSaver::
 apply_tags(EggGroup *egg_group, PandaNode *node) {
 apply_tags(EggGroup *egg_group, PandaNode *node) {
-  std::ostringstream strm;
-  char delimiter = '\n';
-  string delimiter_str(1, delimiter);
-  node->list_tags(strm, delimiter_str);
-
-  string data = strm.str();
-  if (data.empty()) {
-    return false;
-  }
-
   bool any_applied = false;
   bool any_applied = false;
 
 
-  size_t p = 0;
-  size_t q = data.find(delimiter);
-  while (q != string::npos) {
-    string tag = data.substr(p, q);
-    if (apply_tag(egg_group, node, tag)) {
+  vector_string keys;
+  node->get_tag_keys(keys);
+
+  for (size_t i = 0; i < keys.size(); ++i) {
+    if (apply_tag(egg_group, node, keys[i])) {
       any_applied = true;
       any_applied = true;
     }
     }
-    p = q + 1;
-    q = data.find(delimiter, p);
   }
   }
-
-  string tag = data.substr(p);
-  if (apply_tag(egg_group, node, tag)) {
-    any_applied = true;
-  }
-
   return any_applied;
   return any_applied;
 }
 }
 
 

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

@@ -157,7 +157,10 @@ close_buffer() {
   if (_gsg != nullptr) {
   if (_gsg != nullptr) {
     eglGraphicsStateGuardian *eglgsg;
     eglGraphicsStateGuardian *eglgsg;
     DCAST_INTO_V(eglgsg, _gsg);
     DCAST_INTO_V(eglgsg, _gsg);
-    if (!eglMakeCurrent(_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
+
+    // Try to keep the GSG bound surfaceless if possible, otherwise unbind.
+    if (!eglgsg->make_current() &&
+        !eglMakeCurrent(_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
       egldisplay_cat.error() << "Failed to call eglMakeCurrent: "
       egldisplay_cat.error() << "Failed to call eglMakeCurrent: "
         << get_egl_error_string(eglGetError()) << "\n";
         << get_egl_error_string(eglGetError()) << "\n";
     }
     }

+ 25 - 4
panda/src/event/asyncFuture_ext.cxx

@@ -63,13 +63,13 @@ static PyObject *get_done_result(const AsyncFuture *future) {
       // If it's an AsyncGatheringFuture, get the result for each future.
       // If it's an AsyncGatheringFuture, get the result for each future.
       const AsyncGatheringFuture *gather = (const AsyncGatheringFuture *)future;
       const AsyncGatheringFuture *gather = (const AsyncGatheringFuture *)future;
       Py_ssize_t num_futures = (Py_ssize_t)gather->get_num_futures();
       Py_ssize_t num_futures = (Py_ssize_t)gather->get_num_futures();
-      PyObject *results = PyTuple_New(num_futures);
+      PyObject *results = PyList_New(num_futures);
 
 
       for (Py_ssize_t i = 0; i < num_futures; ++i) {
       for (Py_ssize_t i = 0; i < num_futures; ++i) {
         PyObject *result = get_done_result(gather->get_future((size_t)i));
         PyObject *result = get_done_result(gather->get_future((size_t)i));
         if (result != nullptr) {
         if (result != nullptr) {
           // This steals a reference.
           // This steals a reference.
-          PyTuple_SET_ITEM(results, i, result);
+          PyList_SET_ITEM(results, i, result);
         } else {
         } else {
           Py_DECREF(results);
           Py_DECREF(results);
           return nullptr;
           return nullptr;
@@ -145,9 +145,30 @@ static PyObject *gen_next_asyncfuture(PyObject *self) {
   else {
   else {
     PyObject *result = get_done_result(future);
     PyObject *result = get_done_result(future);
     if (result != nullptr) {
     if (result != nullptr) {
-      PyErr_SetObject(PyExc_StopIteration, result);
-      // PyErr_SetObject increased the reference count, so we no longer need our reference.
+      // See python/cpython#101578 - PyErr_SetObject has a special case where
+      // it interprets a tuple specially, so we bypass that by creating the
+      // exception directly.
+#if PY_VERSION_HEX >= 0x030C0000 // 3.12
+      PyObject *exc = PyObject_CallOneArg(PyExc_StopIteration, result);
       Py_DECREF(result);
       Py_DECREF(result);
+      if (LIKELY(exc != nullptr)) {
+        // This function steals a reference to exc.
+        PyErr_SetRaisedException(exc);
+      }
+#else
+      if (PyTuple_Check(result)) {
+        PyObject *exc = PyObject_CallOneArg(PyExc_StopIteration, result);
+        Py_DECREF(result);
+        if (LIKELY(exc != nullptr)) {
+          PyErr_SetObject(PyExc_StopIteration, exc);
+          Py_DECREF(exc);
+        }
+      } else {
+        PyErr_SetObject(PyExc_StopIteration, result);
+        // PyErr_SetObject increased the reference count, so we no longer need our reference.
+        Py_DECREF(result);
+      }
+#endif
     }
     }
     return nullptr;
     return nullptr;
   }
   }

+ 23 - 9
panda/src/event/pythonTask.cxx

@@ -364,7 +364,7 @@ __getattribute__(PyObject *self, PyObject *attr) const {
   // We consult the instance dict first, since the user may have overridden a
   // We consult the instance dict first, since the user may have overridden a
   // method or something.
   // method or something.
   PyObject *item;
   PyObject *item;
-  if (PyDict_GetItemRef(__dict__, attr, &item) > 0) {
+  if (PyDict_GetItemRef(__dict__, attr, &item) != 0) {
     return item;
     return item;
   }
   }
 
 
@@ -503,7 +503,7 @@ cancel() {
       --_chain->_num_awaiting_tasks;
       --_chain->_num_awaiting_tasks;
       return true;
       return true;
     }
     }
-    else if (must_cancel || _fut_waiter != nullptr) {
+    else if (_generator != nullptr && (must_cancel || _fut_waiter != nullptr)) {
       // We may be polling an external future, so we still need to throw a
       // We may be polling an external future, so we still need to throw a
       // CancelledException and allow it to be caught.
       // CancelledException and allow it to be caught.
       if (must_cancel) {
       if (must_cancel) {
@@ -638,17 +638,31 @@ do_python_task() {
       // We are calling a generator.  Use "send" rather than PyIter_Next since
       // We are calling a generator.  Use "send" rather than PyIter_Next since
       // we need to be able to read the value from a StopIteration exception.
       // we need to be able to read the value from a StopIteration exception.
       PyObject *func = PyObject_GetAttrString(_generator, "send");
       PyObject *func = PyObject_GetAttrString(_generator, "send");
-      nassertr(func != nullptr, DS_interrupt);
-      result = PyObject_CallOneArg(func, Py_None);
-      Py_DECREF(func);
+      if (func != nullptr) {
+        result = PyObject_CallOneArg(func, Py_None);
+        Py_DECREF(func);
+      } else {
+        // It has no send(), just call next() directly.
+        nassertr(Py_TYPE(_generator)->tp_iternext != nullptr, DS_interrupt);
+        PyErr_Clear();
+        result = Py_TYPE(_generator)->tp_iternext(_generator);
+      }
     } else {
     } else {
       // Throw a CancelledError into the generator.
       // Throw a CancelledError into the generator.
       _must_cancel = false;
       _must_cancel = false;
-      PyObject *exc = PyObject_CallNoArgs(Extension<AsyncFuture>::get_cancelled_error_type());
+      PyObject *exc_type = Extension<AsyncFuture>::get_cancelled_error_type();
       PyObject *func = PyObject_GetAttrString(_generator, "throw");
       PyObject *func = PyObject_GetAttrString(_generator, "throw");
-      result = PyObject_CallFunctionObjArgs(func, exc, nullptr);
-      Py_DECREF(func);
-      Py_DECREF(exc);
+      if (func != nullptr) {
+        PyObject *exc = PyObject_CallNoArgs(exc_type);
+        result = PyObject_CallFunctionObjArgs(func, exc, nullptr);
+        Py_DECREF(exc);
+        Py_DECREF(func);
+      } else {
+        // If it has no throw(), assume it propagated the CancelledException.
+        PyErr_Clear();
+        PyErr_SetNone(exc_type);
+        result = nullptr;
+      }
     }
     }
 
 
     if (result == nullptr) {
     if (result == nullptr) {

+ 10 - 5
panda/src/express/memoryUsage.cxx

@@ -826,7 +826,8 @@ ns_record_void_pointer(void *ptr, size_t size) {
   if (_track_memory_usage) {
   if (_track_memory_usage) {
     if (express_cat.is_spam()) {
     if (express_cat.is_spam()) {
       express_cat.spam()
       express_cat.spam()
-        << "Recording void pointer " << (void *)ptr << "\n";
+        << "Recording void pointer " << (void *)ptr
+        << " with size " << size << "\n";
     }
     }
 
 
     // We have to protect modifications to the table from recursive calls by
     // We have to protect modifications to the table from recursive calls by
@@ -846,11 +847,15 @@ ns_record_void_pointer(void *ptr, size_t size) {
 
 
     MemoryInfo *info = (*insert_result.first).second;
     MemoryInfo *info = (*insert_result.first).second;
 
 
-    // We shouldn't already have a void pointer.
+    // If we already have a void pointer, that's okay, as long as the size
+    // matches.
     if (info->_void_ptr != nullptr) {
     if (info->_void_ptr != nullptr) {
-      express_cat.error()
-        << "Void pointer " << (void *)ptr << " recorded twice!\n";
-      nassertv(false);
+      if (info->_size != size) {
+        express_cat.error()
+          << "Void pointer " << (void *)ptr
+          << " was already recorded with size " << size << "!\n";
+        nassertv(false);
+      }
     }
     }
 
 
     if (info->_freeze_index == _freeze_index) {
     if (info->_freeze_index == _freeze_index) {

+ 5 - 2
panda/src/express/multifile_ext.I

@@ -24,8 +24,11 @@ set_encryption_password(PyObject *encryption_password) const {
   // Have we been passed a string?
   // Have we been passed a string?
   if (PyUnicode_Check(encryption_password)) {
   if (PyUnicode_Check(encryption_password)) {
     const char *pass_str = PyUnicode_AsUTF8AndSize(encryption_password, &pass_len);
     const char *pass_str = PyUnicode_AsUTF8AndSize(encryption_password, &pass_len);
+    if (pass_str == NULL) {
+      return NULL;
+    }
     _this->set_encryption_password(std::string(pass_str, pass_len));
     _this->set_encryption_password(std::string(pass_str, pass_len));
-    return Dtool_Return_None();
+    Py_RETURN_NONE;
   }
   }
 
 
   // Have we been passed a bytes object?
   // Have we been passed a bytes object?
@@ -46,7 +49,7 @@ set_encryption_password(PyObject *encryption_password) const {
     }
     }
 
 
     _this->set_encryption_password(std::string(pass_str, pass_len));
     _this->set_encryption_password(std::string(pass_str, pass_len));
-    return Dtool_Return_None();
+    Py_RETURN_NONE;
   }
   }
 
 
   return Dtool_Raise_BadArgumentsError(
   return Dtool_Raise_BadArgumentsError(

+ 17 - 14
panda/src/express/zipArchive.cxx

@@ -28,6 +28,11 @@
 
 
 #include "openSSLWrapper.h"
 #include "openSSLWrapper.h"
 
 
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+#define EVP_MD_CTX_new() EVP_MD_CTX_create()
+#define EVP_MD_CTX_free(ctx) EVP_MD_CTX_destroy(ctx)
+#endif
+
 using std::streamoff;
 using std::streamoff;
 using std::streampos;
 using std::streampos;
 using std::streamsize;
 using std::streamsize;
@@ -571,11 +576,11 @@ add_jar_signature(X509 *cert, EVP_PKEY *pkey, const std::string &alias) {
   const std::string header_digest = "VmrRqAIgAm0FCZViZFzpaP8OfDbN4iY0MyYFuzTMPv8=";
   const std::string header_digest = "VmrRqAIgAm0FCZViZFzpaP8OfDbN4iY0MyYFuzTMPv8=";
 
 
   std::stringstream manifest;
   std::stringstream manifest;
-  SHA256_CTX manifest_ctx;
-  SHA256_Init(&manifest_ctx);
+  EVP_MD_CTX *manifest_ctx = EVP_MD_CTX_new();
+  EVP_DigestInit_ex(manifest_ctx, EVP_sha256(), nullptr);
 
 
   manifest << header;
   manifest << header;
-  SHA256_Update(&manifest_ctx, header.data(), header.size());
+  EVP_DigestUpdate(manifest_ctx, header.data(), header.size());
 
 
   std::ostringstream sigfile_body;
   std::ostringstream sigfile_body;
 
 
@@ -594,20 +599,21 @@ add_jar_signature(X509 *cert, EVP_PKEY *pkey, const std::string &alias) {
     {
     {
       std::istream *stream = open_read_subfile(subfile);
       std::istream *stream = open_read_subfile(subfile);
 
 
-      SHA256_CTX subfile_ctx;
-      SHA256_Init(&subfile_ctx);
+      EVP_MD_CTX *subfile_ctx = EVP_MD_CTX_new();
+      EVP_DigestInit_ex(subfile_ctx, EVP_sha256(), nullptr);
 
 
       char buffer[4096];
       char buffer[4096];
       stream->read(buffer, sizeof(buffer));
       stream->read(buffer, sizeof(buffer));
       size_t count = stream->gcount();
       size_t count = stream->gcount();
       while (count > 0) {
       while (count > 0) {
-        SHA256_Update(&subfile_ctx, buffer, count);
+        EVP_DigestUpdate(subfile_ctx, buffer, count);
         stream->read(buffer, sizeof(buffer));
         stream->read(buffer, sizeof(buffer));
         count = stream->gcount();
         count = stream->gcount();
       }
       }
       delete stream;
       delete stream;
 
 
-      SHA256_Final(digest, &subfile_ctx);
+      EVP_DigestFinal_ex(subfile_ctx, digest, nullptr);
+      EVP_MD_CTX_free(subfile_ctx);
     }
     }
 
 
     // Encode to base64.
     // Encode to base64.
@@ -616,24 +622,21 @@ add_jar_signature(X509 *cert, EVP_PKEY *pkey, const std::string &alias) {
     // Encode what we just wrote to the manifest file as well.
     // Encode what we just wrote to the manifest file as well.
     {
     {
       unsigned char digest[SHA256_DIGEST_LENGTH];
       unsigned char digest[SHA256_DIGEST_LENGTH];
-
-      SHA256_CTX section_ctx;
-      SHA256_Init(&section_ctx);
-      SHA256_Update(&section_ctx, section.data(), section.size());
-      SHA256_Final(digest, &section_ctx);
+      EVP_Digest(section.data(), section.size(), digest, nullptr, EVP_sha256(), nullptr);
 
 
       sigfile_body << "SHA-256-Digest: " << base64_encode(digest, SHA256_DIGEST_LENGTH) << "\r\n\r\n";
       sigfile_body << "SHA-256-Digest: " << base64_encode(digest, SHA256_DIGEST_LENGTH) << "\r\n\r\n";
     }
     }
 
 
     manifest << section;
     manifest << section;
-    SHA256_Update(&manifest_ctx, section.data(), section.size());
+    EVP_DigestUpdate(manifest_ctx, section.data(), section.size());
   }
   }
 
 
   // The hash for the whole manifest file goes at the beginning of the .SF file.
   // The hash for the whole manifest file goes at the beginning of the .SF file.
   std::stringstream sigfile;
   std::stringstream sigfile;
   {
   {
     unsigned char digest[SHA256_DIGEST_LENGTH];
     unsigned char digest[SHA256_DIGEST_LENGTH];
-    SHA256_Final(digest, &manifest_ctx);
+    EVP_DigestFinal_ex(manifest_ctx, digest, nullptr);
+    EVP_MD_CTX_free(manifest_ctx);
     sigfile << "Signature-Version: 1.0\r\n";
     sigfile << "Signature-Version: 1.0\r\n";
     sigfile << "SHA-256-Digest-Manifest-Main-Attributes: " << header_digest << "\r\n";
     sigfile << "SHA-256-Digest-Manifest-Main-Attributes: " << header_digest << "\r\n";
     sigfile << "SHA-256-Digest-Manifest: " << base64_encode(digest, SHA256_DIGEST_LENGTH) << "\r\n\r\n";
     sigfile << "SHA-256-Digest-Manifest: " << base64_encode(digest, SHA256_DIGEST_LENGTH) << "\r\n\r\n";

+ 31 - 3
panda/src/glstuff/glGraphicsStateGuardian_src.cxx

@@ -70,6 +70,10 @@
 #include "displayInformation.h"
 #include "displayInformation.h"
 #include "completionCounter.h"
 #include "completionCounter.h"
 
 
+#ifdef __EMSCRIPTEN__
+#include "htmlVideoTexture.h"
+#endif
+
 #include <algorithm>
 #include <algorithm>
 
 
 using std::dec;
 using std::dec;
@@ -2659,7 +2663,7 @@ reset() {
 
 
 #ifndef OPENGLES
 #ifndef OPENGLES
   // Check for SSBOs.
   // Check for SSBOs.
-  if (is_at_least_gl_version(4, 3) || has_extension("ARB_shader_storage_buffer_object")) {
+  if (is_at_least_gl_version(4, 3) || has_extension("GL_ARB_shader_storage_buffer_object")) {
     _supports_shader_buffers = _supports_program_interface_query;
     _supports_shader_buffers = _supports_program_interface_query;
 
 
     _glShaderStorageBlockBinding = (PFNGLSHADERSTORAGEBLOCKBINDINGPROC)
     _glShaderStorageBlockBinding = (PFNGLSHADERSTORAGEBLOCKBINDINGPROC)
@@ -14680,8 +14684,8 @@ upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps,
 }
 }
 
 
 /**
 /**
- * Loads a texture image, or one page of a cube map image, from system RAM to
- * texture memory.
+ * Loads a texture image, or one view of a multiview texture, from system RAM
+ * to texture memory.
  */
  */
 bool CLP(GraphicsStateGuardian)::
 bool CLP(GraphicsStateGuardian)::
 upload_texture_view(CLP(TextureContext) *gtc, int view, bool needs_reload,
 upload_texture_view(CLP(TextureContext) *gtc, int view, bool needs_reload,
@@ -14980,6 +14984,30 @@ upload_texture_view(CLP(TextureContext) *gtc, int view, bool needs_reload,
       image_ptr = ptimage;
       image_ptr = ptimage;
     }
     }
     if (image_ptr == nullptr) {
     if (image_ptr == nullptr) {
+#ifdef __EMSCRIPTEN__
+      if (tex->is_of_type(HTMLVideoTexture::get_class_type())) {
+        // Load directly from the video element into WebGL.
+        int result = EM_ASM_INT({
+          var video = window._htmlVideoData[$7].video;
+          if (video.readyState < video.HAVE_CURRENT_DATA) {
+            return 0;
+          }
+          GLctx.pixelStorei(GLctx.UNPACK_FLIP_Y_WEBGL, true);
+          if ($8) {
+            GLctx.texImage2D($0, $1, $2, $3, $4, 0, $5, $6, video);
+          } else {
+            GLctx.texSubImage2D($0, $1, 0, 0, $3, $4, $5, $6, video);
+          }
+          GLctx.pixelStorei(GLctx.UNPACK_FLIP_Y_WEBGL, false);
+          return 1;
+        }, target, 0, internal_format, width, height, external_format, component_type, tex, needs_reload);
+
+        if (result) {
+          continue;
+        }
+      }
+#endif
+
       if (level < num_ram_mipmap_levels) {
       if (level < num_ram_mipmap_levels) {
         // We were told we'd have this many RAM mipmap images, but we
         // We were told we'd have this many RAM mipmap images, but we
         // don't.  Raise a warning.
         // don't.  Raise a warning.

+ 1 - 1
panda/src/glstuff/glGraphicsStateGuardian_src.h

@@ -1294,7 +1294,7 @@ public:
   pdeque<Fence> _fences;
   pdeque<Fence> _fences;
 
 
 #ifdef HAVE_THREADS
 #ifdef HAVE_THREADS
-  AsyncTaskChain *_async_chain;
+  PT(AsyncTaskChain) _async_chain;
 #endif
 #endif
 
 
   // Min job system pending a real job system
   // Min job system pending a real job system

+ 5 - 5
panda/src/glstuff/glShaderContext_src.cxx

@@ -228,7 +228,7 @@ bind(CLP(GraphicsStateGuardian) *glgsg,
      RenderAttrib::PandaCompareFunc alpha_test_mode) {
      RenderAttrib::PandaCompareFunc alpha_test_mode) {
   _glgsg = glgsg;
   _glgsg = glgsg;
   /*if (!_validated) {
   /*if (!_validated) {
-    _glgsg->_glValidateProgram(_glsl_program);
+    glgsg->_glValidateProgram(_glsl_program);
     report_program_errors(_glsl_program, false);
     report_program_errors(_glsl_program, false);
     _validated = true;
     _validated = true;
   }*/
   }*/
@@ -240,7 +240,7 @@ bind(CLP(GraphicsStateGuardian) *glgsg,
   GLuint program = _linked_programs[alpha_test_mode];
   GLuint program = _linked_programs[alpha_test_mode];
   if (program != 0) {
   if (program != 0) {
     if (!_shader->get_error_flag()) {
     if (!_shader->get_error_flag()) {
-      _glgsg->_glUseProgram(program);
+      glgsg->_glUseProgram(program);
     } else {
     } else {
       return false;
       return false;
     }
     }
@@ -258,7 +258,7 @@ bind(CLP(GraphicsStateGuardian) *glgsg,
                  << alpha_test_mode << "\n";
                  << alpha_test_mode << "\n";
   }
   }
 
 
-  _glgsg->report_my_gl_errors();
+  glgsg->report_my_gl_errors();
   return true;
   return true;
 }
 }
 
 
@@ -938,7 +938,7 @@ reflect_program(GLuint program, SparseArray &active_locations) {
       if (p != nullptr) {
       if (p != nullptr) {
         // It's an array, this is a bit annoying.
         // It's an array, this is a bit annoying.
         CPT_InternalName name = InternalName::make(std::string(block_name_cstr, p - block_name_cstr));
         CPT_InternalName name = InternalName::make(std::string(block_name_cstr, p - block_name_cstr));
-        SSBO &ssbo = ssbos[name];
+        SSBO &ssbo = ssbos[std::move(name)];
         size_t i = 0;
         size_t i = 0;
         do {
         do {
           ++p;
           ++p;
@@ -957,7 +957,7 @@ reflect_program(GLuint program, SparseArray &active_locations) {
       } else {
       } else {
         // Simple case.  We can already jot down the binding, so we don't have
         // Simple case.  We can already jot down the binding, so we don't have
         // to query it later.
         // to query it later.
-        CPT(InternalName) name = InternalName::make(std::string(block_name_cstr));
+        CPT_InternalName name = InternalName::make(std::string(block_name_cstr));
         _bindings[name] = values[0];
         _bindings[name] = values[0];
         ssbos[std::move(name)];
         ssbos[std::move(name)];
       }
       }

+ 2 - 0
panda/src/gobj/shaderContext.h

@@ -19,6 +19,8 @@
 #include "savedContext.h"
 #include "savedContext.h"
 #include "shader.h"
 #include "shader.h"
 
 
+class GraphicsStateGuardian;
+
 /**
 /**
  * The ShaderContext is meant to contain the compiled version of a shader
  * The ShaderContext is meant to contain the compiled version of a shader
  * string.  ShaderContext is an abstract base class, there will be a subclass
  * string.  ShaderContext is an abstract base class, there will be a subclass

+ 7 - 0
panda/src/grutil/config_grutil.cxx

@@ -17,6 +17,7 @@
 #include "meshDrawer.h"
 #include "meshDrawer.h"
 #include "meshDrawer2D.h"
 #include "meshDrawer2D.h"
 #include "geoMipTerrain.h"
 #include "geoMipTerrain.h"
+#include "htmlVideoTexture.h"
 #include "movieTexture.h"
 #include "movieTexture.h"
 #include "pandaSystem.h"
 #include "pandaSystem.h"
 #include "texturePool.h"
 #include "texturePool.h"
@@ -137,4 +138,10 @@ init_libgrutil() {
   TexturePool *ts = TexturePool::get_global_ptr();
   TexturePool *ts = TexturePool::get_global_ptr();
   ts->register_texture_type(MovieTexture::make_texture, "avi m2v mov mpg mpeg mp4 wmv asf flv nut ogm mkv ogv webm");
   ts->register_texture_type(MovieTexture::make_texture, "avi m2v mov mpg mpeg mp4 wmv asf flv nut ogm mkv ogv webm");
 #endif
 #endif
+
+#ifdef __EMSCRIPTEN__
+  HTMLVideoTexture::init_type();
+
+  ts->register_texture_type(HTMLVideoTexture::make_texture, "avi m2v mov mpg mpeg mp4 wmv asf flv nut ogm mkv ogv webm");
+#endif
 }
 }

+ 12 - 0
panda/src/grutil/htmlVideoTexture.I

@@ -0,0 +1,12 @@
+/**
+ * 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 htmlVideoTexture.I
+ * @author rdb
+ * @date 2025-02-24
+ */

+ 489 - 0
panda/src/grutil/htmlVideoTexture.cxx

@@ -0,0 +1,489 @@
+/**
+ * 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 htmlVideoTexture.cxx
+ * @author rdb
+ * @date 2025-02-24
+ */
+
+#include "htmlVideoTexture.h"
+
+#ifdef __EMSCRIPTEN__
+
+#include "config_grutil.h"
+#include "virtualFileSystem.h"
+#include "virtualFileHTTP.h"
+
+#include <emscripten/emscripten.h>
+
+#ifndef CPPPARSER
+// If the browser supports video frame callback, we use that instead of relying
+// on cull callbacks.
+extern "C" void EMSCRIPTEN_KEEPALIVE
+_video_frame_callback(HTMLVideoTexture *texture, int width, int height, double time) {
+  texture->video_frame_callback(width, height, time);
+}
+
+static bool get_supports_video_frame_callback() {
+  static bool supported = EM_ASM_INT({
+    return 'requestVideoFrameCallback' in HTMLVideoElement.prototype;
+  });
+  return supported;
+}
+#endif
+
+TypeHandle HTMLVideoTexture::_type_handle;
+
+/**
+ * Creates a blank movie texture.  Movies must be added using do_read_one.
+ */
+HTMLVideoTexture::
+HTMLVideoTexture(const std::string &name) :
+  Texture(name)
+{
+  EM_ASM_INT({
+    if (!window._htmlVideoData) {
+      window._htmlVideoData = {};
+    }
+    var video = document.createElement("video");
+    video.style.display = 'none';
+    video.defaultMuted = true;
+    document.body.appendChild(video);
+    window._htmlVideoData[$0] = {video: video};
+  }, this);
+}
+
+/**
+ * xxx
+ */
+HTMLVideoTexture::
+~HTMLVideoTexture() {
+  clear();
+
+  EM_ASM_INT({
+    var data = window._htmlVideoData[$0];
+    if (data) {
+      if (data.video) {
+        if (data.callback) {
+          video.cancelVideoFrameCallback(data.callback);
+          delete data.callback;
+        }
+        data.video.remove();
+        delete data.video;
+      }
+      if (data.objectURL) {
+        URL.revokeObjectURL(data.objectURL);
+        delete data.objectURL;
+      }
+      delete window._htmlVideoData[$0];
+    }
+  }, this);
+}
+
+/**
+ * Returns the length of the video.
+ */
+double HTMLVideoTexture::
+get_video_length() const {
+  return EM_ASM_DOUBLE({
+    return window._htmlVideoData[$0].video.duration;
+  }, this);
+}
+
+/**
+ * Returns the width in texels of the source video stream.  This is not
+ * necessarily the width of the actual texture, since the texture may have
+ * been expanded to raise it to a power of 2.
+ */
+int HTMLVideoTexture::
+get_video_width() const {
+  return EM_ASM_DOUBLE({
+    return window._htmlVideoData[$0].video.videoWidth;
+  }, this);
+}
+
+/**
+ * Returns the height in texels of the source video stream.  This is not
+ * necessarily the height of the actual texture, since the texture may have
+ * been expanded to raise it to a power of 2.
+ */
+int HTMLVideoTexture::
+get_video_height() const {
+  return EM_ASM_DOUBLE({
+    return window._htmlVideoData[$0].video.videoHeight;
+  }, this);
+}
+
+/**
+ * Start playing the video from where it was last paused.  Has no effect if
+ * the video is not paused, or if the video's cursor is already at the end.
+ */
+void HTMLVideoTexture::
+restart() {
+  EM_ASM({
+    window._htmlVideoData[$0].video.play();
+  }, this);
+}
+
+/**
+ * Stops a currently playing or looping movie right where it is.  the video's
+ * cursor remains frozen at the point where it was stopped.
+ */
+void HTMLVideoTexture::
+stop() {
+  EM_ASM({
+    window._htmlVideoData[$0].video.pause();
+  }, this);
+}
+
+/**
+ * Plays the video from the beginning.
+ */
+void HTMLVideoTexture::
+play() {
+  EM_ASM({
+    var video = window._htmlVideoData[$0].video;
+    if (video.playing) {
+      video.pause();
+    }
+    video.currentTime = 0.0;
+    video.play();
+  }, this);
+}
+
+/**
+ * Sets the video's cursor.
+ */
+void HTMLVideoTexture::
+set_time(double t) {
+  EM_ASM({
+    window._htmlVideoData[$0].video.currentTime = $1;
+  }, this, t);
+}
+
+/**
+ * Returns the current value of the video's cursor.
+ */
+double HTMLVideoTexture::
+get_time() const {
+  return EM_ASM_DOUBLE({
+    return window._htmlVideoData[$0].video.currentTime;
+  }, this);
+}
+
+/**
+ * If true, sets the video's loop count to 1 billion.  If false, sets the
+ * movie's loop count to one.
+ */
+void HTMLVideoTexture::
+set_loop(bool loop) {
+  EM_ASM({
+    window._htmlVideoData[$0].video.loop = $1;
+  }, this, loop);
+}
+
+/**
+ * Returns true if the video's is looping.
+ */
+bool HTMLVideoTexture::
+get_loop() const {
+  return EM_ASM_INT({
+    return window._htmlVideoData[$0].video.loop;
+  }, this);
+}
+
+/**
+ * Sets the video's play-rate.  This is the speed at which the video's cursor
+ * advances.  The default is to advance 1.0 movie-seconds per real-time
+ * second.
+ */
+void HTMLVideoTexture::
+set_play_rate(double rate) {
+  EM_ASM({
+    window._htmlVideoData[$0].video.playbackRate = $1;
+  }, this, rate);
+}
+
+/**
+ * Gets the video's play-rate.
+ */
+double HTMLVideoTexture::
+get_play_rate() const {
+  return EM_ASM_DOUBLE({
+    return window._htmlVideoData[$0].video.playbackRate;
+  }, this);
+}
+
+/**
+ * Returns true if the video's cursor is advancing.
+ */
+bool HTMLVideoTexture::
+is_playing() const {
+  return EM_ASM_INT({
+    return !window._htmlVideoData[$0].video.paused;
+  }, this);
+}
+
+/**
+ * A factory function to make a new HTMLVideoTexture, used to pass to the
+ * TexturePool.
+ */
+PT(Texture) HTMLVideoTexture::
+make_texture() {
+  return new HTMLVideoTexture("");
+}
+
+/**
+ * Should be overridden by derived classes to return true if cull_callback()
+ * has been defined.  Otherwise, returns false to indicate cull_callback()
+ * does not need to be called for this node during the cull traversal.
+ */
+bool HTMLVideoTexture::
+has_cull_callback() const {
+  return !get_supports_video_frame_callback();
+}
+
+/**
+ * This function will be called during the cull traversal to update the
+ * HTMLVideoTexture.  This will just check whether the video time changed since
+ * the last time it was checked.
+ */
+bool HTMLVideoTexture::
+cull_callback(CullTraverser *, const CullTraverserData &) const {
+  ((HTMLVideoTexture *)this)->check_update();
+  return true;
+}
+
+/**
+ *
+ */
+void HTMLVideoTexture::
+video_frame_callback(int width, int height, double time) {
+  CDWriter cdata(_cycler, true);
+  if (width != cdata->_x_size || height != cdata->_y_size) {
+    cdata->_x_size = width;
+    cdata->_y_size = height;
+    cdata->inc_properties_modified();
+  }
+  cdata->inc_image_modified();
+  _last_time = time;
+}
+
+/**
+ *
+ */
+bool HTMLVideoTexture::
+check_update() {
+  double current_time = get_time();
+  if (current_time != _last_time) {
+    int width = get_video_width();
+    int height = get_video_height();
+    if (width == 0 && height == 0) {
+      return false;
+    }
+
+    video_frame_callback(width, height, current_time);
+    return true;
+  }
+  return false;
+}
+
+/**
+ * The protected implementation of clear().  Assumes the lock is already held.
+ */
+void HTMLVideoTexture::
+do_clear(Texture::CData *cdata) {
+  EM_ASM_INT({
+    var data = window._htmlVideoData[$0];
+    data.video.pause();
+    data.video.src = "";
+    if (data.callback) {
+      data.video.cancelVideoFrameCallback(data.callback);
+      delete data.callback;
+    }
+    if (data.objectURL) {
+      URL.revokeObjectURL(data.objectURL);
+      delete data.objectURL;
+    }
+  }, this);
+
+  Texture::do_clear(cdata);
+}
+
+/**
+ * Returns true if we can safely call do_unlock_and_reload_ram_image() in
+ * order to make the image available, or false if we shouldn't do this
+ * (because we know from a priori knowledge that it wouldn't work anyway).
+ */
+bool HTMLVideoTexture::
+do_can_reload(const Texture::CData *cdata) const {
+  return false;
+}
+
+/**
+ * Works like adjust_size, but also considers the texture class.  Movie
+ * textures, for instance, always pad outwards, never scale down.
+ */
+bool HTMLVideoTexture::
+do_adjust_this_size(const Texture::CData *cdata_tex,
+                    int &x_size, int &y_size, const std::string &name,
+                    bool for_padding) const {
+  // We always scale, for now.  May change in the future.
+  AutoTextureScale ats = do_get_auto_texture_scale(cdata_tex);
+  if (ats == ATS_pad) {
+    ats = ATS_up;
+  }
+
+  return adjust_size(x_size, y_size, name, for_padding, ats);
+}
+
+/**
+ * Combines a color and alpha video image from the two indicated filenames.
+ * Both must be the same kind of video with similar properties.
+ */
+bool HTMLVideoTexture::
+do_read_one(Texture::CData *cdata_tex,
+            const Filename &fullpath, const Filename &alpha_fullpath,
+            int z, int n, int primary_file_num_channels, int alpha_file_channel,
+            const LoaderOptions &options,
+            bool header_only, BamCacheRecord *record) {
+  nassertr(n == 0, false);
+  if (!do_reconsider_z_size(cdata_tex, z, options)) {
+    return false;
+  }
+  nassertr(z >= 0 && z < cdata_tex->_z_size * cdata_tex->_num_views, false);
+
+  if (!alpha_fullpath.empty()) {
+    grutil_cat.error()
+      << "HTMLVideoTexture does not support loading separate alpha video.\n";
+    return false;
+  }
+
+  VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr();
+  PT(VirtualFile) vfile = vfs->get_file(fullpath, true);
+  if (vfile == nullptr) {
+    return false;
+  }
+
+  clear();
+
+  if (record != nullptr) {
+    record->add_dependent_file(vfile);
+  }
+
+  // Pass in the URL directly if it's an HTTP file.
+  if (vfile->is_of_type(VirtualFileHTTP::get_class_type())) {
+    URLSpec url = ((VirtualFileHTTP *)vfile.p())->get_url();
+
+    _last_time = EM_ASM_DOUBLE({
+      var data = window._htmlVideoData[$0];
+      var video = data.video;
+      video.src = UTF8ToString($1);
+      video.loop = true;
+      video.muted = true;
+
+      video.load();
+      return video.currentTime;
+    }, this, url.c_str());
+  }
+  else {
+    vector_uchar data;
+    if (!vfile->read_file(data, true)) {
+      return false;
+    }
+
+    _last_time = EM_ASM_DOUBLE({
+      var data = window._htmlVideoData[$0];
+
+      var blob = new Blob([HEAPU8.subarray($1, $1 + $2)]);
+      data.objectURL = URL.createObjectURL(blob);
+
+      var video = data.video;
+      video.src = data.objectURL;
+      video.loop = true;
+      video.muted = true;
+
+      video.load();
+      return video.currentTime;
+    }, this, (void *)data.data(), data.size());
+  }
+
+  // If the browser supports requestVideoframeCallback, we set up a callback to
+  // know when the next video frame is available instead of having to poll in
+  // cull_callback().
+  if (get_supports_video_frame_callback()) {
+    EM_ASM({
+      var data = window._htmlVideoData[$0];
+
+      function callback(now, metadata) {
+        __video_frame_callback($0, metadata.width, metadata.height, metadata.mediaTime);
+        data.callback = data.video.requestVideoFrameCallback(callback);
+      }
+      data.callback = data.video.requestVideoFrameCallback(callback);
+    }, this);
+  }
+
+  int width = get_video_width();
+  int height = get_video_height();
+  if (width == 0 || height == 0) {
+    // Not yet known.  Don't set the size to 0 since that will cause
+    // CardMaker::set_uv_range() to generate invalid coordinates.
+    width = 1;
+    height = 1;
+  }
+
+  if (!do_reconsider_image_properties(cdata_tex, width, height, 3, T_unsigned_byte, z, options)) {
+    return false;
+  }
+
+  if (z == 0) {
+    if (!has_name()) {
+      set_name(fullpath.get_basename_wo_extension());
+    }
+    // Don't use has_filename() here, it will cause a deadlock
+    if (cdata_tex->_filename.empty()) {
+      cdata_tex->_filename = fullpath;
+      cdata_tex->_alpha_filename = alpha_fullpath;
+    }
+
+    cdata_tex->_fullpath = fullpath;
+    cdata_tex->_alpha_fullpath = alpha_fullpath;
+  }
+
+  cdata_tex->_primary_file_num_channels = primary_file_num_channels;
+  cdata_tex->_alpha_file_channel = alpha_file_channel;
+
+  cdata_tex->_loaded_from_image = true;
+  play();
+  check_update();
+  return true;
+}
+
+/**
+ * Loading a static image into an HTMLVideoTexture is an error.
+ */
+bool HTMLVideoTexture::
+do_load_one(Texture::CData *cdata_tex,
+            const PNMImage &pnmimage, const std::string &name, int z, int n,
+            const LoaderOptions &options) {
+  grutil_cat.error() << "You cannot load a static image into an HTMLVideoTexture\n";
+  return false;
+}
+
+/**
+ * Loading a static image into an HTMLVideoTexture is an error.
+ */
+bool HTMLVideoTexture::
+do_load_one(Texture::CData *cdata_tex,
+            const PfmFile &pfm, const std::string &name, int z, int n,
+            const LoaderOptions &options) {
+  grutil_cat.error() << "You cannot load a static image into an HTMLVideoTexture\n";
+  return false;
+}
+
+#endif  // __EMSCRIPTEN__

+ 115 - 0
panda/src/grutil/htmlVideoTexture.h

@@ -0,0 +1,115 @@
+/**
+ * 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 htmlVideoTexture.h
+ * @author rdb
+ * @date 2025-02-24
+ */
+
+#ifndef HTMLVIDEOTEXTURE_H
+#define HTMLVIDEOTEXTURE_H
+
+#include "pandabase.h"
+
+#ifdef __EMSCRIPTEN__
+
+/**
+ * A texture that renders video frames from an HTMLVideoElement.
+ * Interface aims to be (nearly) identical to MovieTexture, with some
+ * limitations:
+ * - No RAM access of video data is possible
+ * - No explicit synchronization with audio
+ * - No loop count
+ */
+class EXPCL_PANDA_GRUTIL HTMLVideoTexture : public Texture {
+PUBLISHED:
+  explicit HTMLVideoTexture(const std::string &name);
+  HTMLVideoTexture(const HTMLVideoTexture &copy) = delete;
+  virtual ~HTMLVideoTexture();
+
+  double get_video_length() const;
+  int get_video_width() const;
+  int get_video_height() const;
+
+  void restart();
+  void stop();
+  void play();
+  void set_time(double t);
+  double get_time() const;
+  void set_loop(bool enable);
+  bool get_loop() const;
+  void set_play_rate(double play_rate);
+  double get_play_rate() const;
+  bool is_playing() const;
+
+PUBLISHED:
+  MAKE_PROPERTY(video_length, get_video_length);
+  MAKE_PROPERTY(video_width, get_video_width);
+  MAKE_PROPERTY(video_height, get_video_height);
+
+  MAKE_PROPERTY(time, get_time, set_time);
+  MAKE_PROPERTY(loop, get_loop, set_loop);
+  MAKE_PROPERTY(play_rate, get_play_rate, set_play_rate);
+  MAKE_PROPERTY(playing, is_playing);
+
+public:
+  static PT(Texture) make_texture();
+
+  virtual bool has_cull_callback() const;
+  virtual bool cull_callback(CullTraverser *trav, const CullTraverserData &data) const;
+
+  void video_frame_callback(int width, int height, double time);
+
+protected:
+  bool check_update();
+
+  virtual void do_clear(Texture::CData *cdata);
+  virtual bool do_can_reload(const Texture::CData *cdata) const;
+
+  virtual bool do_adjust_this_size(const Texture::CData *cdata,
+                                   int &x_size, int &y_size, const std::string &name,
+                                   bool for_padding) const;
+
+  virtual bool do_read_one(Texture::CData *cdata,
+                           const Filename &fullpath, const Filename &alpha_fullpath,
+                           int z, int n, int primary_file_num_channels, int alpha_file_channel,
+                           const LoaderOptions &options,
+                           bool header_only, BamCacheRecord *record);
+  virtual bool do_load_one(Texture::CData *cdata,
+                           const PNMImage &pnmimage, const std::string &name,
+                           int z, int n, const LoaderOptions &options);
+  virtual bool do_load_one(Texture::CData *cdata,
+                           const PfmFile &pfm, const std::string &name,
+                           int z, int n, const LoaderOptions &options);
+
+private:
+  double _last_time = 0.0;
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    Texture::init_type();
+    register_type(_type_handle, "HTMLVideoTexture",
+                  Texture::get_class_type());
+  }
+  virtual TypeHandle get_type() const {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
+
+private:
+  static TypeHandle _type_handle;
+};
+
+#include "htmlVideoTexture.I"
+
+#endif  // __EMSCRIPTEN__
+
+#endif

+ 1 - 0
panda/src/grutil/p3grutil_composite1.cxx

@@ -1,5 +1,6 @@
 #include "cardMaker.cxx"
 #include "cardMaker.cxx"
 #include "heightfieldTesselator.cxx"
 #include "heightfieldTesselator.cxx"
+#include "htmlVideoTexture.cxx"
 #include "geoMipTerrain.cxx"
 #include "geoMipTerrain.cxx"
 #include "shaderTerrainMesh.cxx"
 #include "shaderTerrainMesh.cxx"
 #include "config_grutil.cxx"
 #include "config_grutil.cxx"

+ 4 - 2
panda/src/nativenet/socket_address.cxx

@@ -125,13 +125,15 @@ get_ip_port() const {
 
 
   if (_storage.ss_family == AF_INET) {
   if (_storage.ss_family == AF_INET) {
     getnameinfo(&_addr, sizeof(sockaddr_in), buf, sizeof(buf), nullptr, 0, NI_NUMERICHOST);
     getnameinfo(&_addr, sizeof(sockaddr_in), buf, sizeof(buf), nullptr, 0, NI_NUMERICHOST);
-    sprintf(buf + strlen(buf), ":%hu", get_port());
+    size_t len = strlen(buf);
+    snprintf(buf + len, sizeof(buf) - len, ":%hu", get_port());
 
 
   } else if (_storage.ss_family == AF_INET6) {
   } else if (_storage.ss_family == AF_INET6) {
     // Protect the IPv6 address within square brackets.
     // Protect the IPv6 address within square brackets.
     buf[0] = '[';
     buf[0] = '[';
     getnameinfo(&_addr, sizeof(sockaddr_in6), buf + 1, sizeof(buf) - 1, nullptr, 0, NI_NUMERICHOST);
     getnameinfo(&_addr, sizeof(sockaddr_in6), buf + 1, sizeof(buf) - 1, nullptr, 0, NI_NUMERICHOST);
-    sprintf(buf + strlen(buf), "]:%hu", get_port());
+    size_t len = strlen(buf);
+    snprintf(buf + len, sizeof(buf) - len, "]:%hu", get_port());
 
 
   } else {
   } else {
     nassert_raise("unsupported address family");
     nassert_raise("unsupported address family");

+ 5 - 1
panda/src/pgraph/cullResult.I

@@ -43,7 +43,11 @@ alloc_object(CullableObject &&object) {
   if (page->_size >= page->_capacity) {
   if (page->_size >= page->_capacity) {
     page = new_page();
     page = new_page();
   }
   }
-  return new (page->_memory + sizeof(CullableObject) * (page->_size++)) CullableObject(std::move(object));
+  void *ptr = page->_memory + sizeof(CullableObject) * (page->_size++);
+#ifdef DO_MEMORY_USAGE
+  //memory_hook->mark_pointer(ptr, sizeof(CullableObject), nullptr);
+#endif
+  return new (ptr) CullableObject(std::move(object));
 }
 }
 
 
 /**
 /**

+ 17 - 1
panda/src/pgraph/cullResult.cxx

@@ -65,7 +65,10 @@ CullResult(GraphicsStateGuardianBase *gsg,
 
 
 #ifdef DO_MEMORY_USAGE
 #ifdef DO_MEMORY_USAGE
   MemoryUsage::update_type(this, get_class_type());
   MemoryUsage::update_type(this, get_class_type());
-#endif
+
+  CullableObject::get_class_type().inc_memory_usage(
+    TypeHandle::MC_array, sizeof(CullableObject) * AllocationPage::_capacity);
+#endif  // DO_MEMORY_USAGE
 
 
 #ifndef NDEBUG
 #ifndef NDEBUG
   _show_transparency = show_transparency.get_value();
   _show_transparency = show_transparency.get_value();
@@ -372,6 +375,10 @@ make_new_bin(int bin_index) {
 CullResult::AllocationPage *CullResult::
 CullResult::AllocationPage *CullResult::
 new_page() {
 new_page() {
   AllocationPage *page = new AllocationPage;
   AllocationPage *page = new AllocationPage;
+#ifdef DO_MEMORY_USAGE
+  CullableObject::get_class_type().inc_memory_usage(
+    TypeHandle::MC_array, sizeof(CullableObject) * AllocationPage::_capacity);
+#endif  // DO_MEMORY_USAGE
   page->_next = _page;
   page->_next = _page;
   _page = page;
   _page = page;
   return page;
   return page;
@@ -385,7 +392,16 @@ delete_page(AllocationPage *page) {
   size_t size = std::exchange(page->_size, 0);
   size_t size = std::exchange(page->_size, 0);
   for (size_t i = 0; i < size; ++i) {
   for (size_t i = 0; i < size; ++i) {
     ((CullableObject *)page->_memory)[i].~CullableObject();
     ((CullableObject *)page->_memory)[i].~CullableObject();
+#ifdef DO_MEMORY_USAGE
+    //MemoryUsage::remove_void_pointer(&((CullableObject *)page->_memory)[i]);
+#endif
   }
   }
+
+#ifdef DO_MEMORY_USAGE
+  CullableObject::get_class_type().dec_memory_usage(
+    TypeHandle::MC_array, sizeof(CullableObject) * AllocationPage::_capacity);
+#endif  // DO_MEMORY_USAGE
+
   AllocationPage *next = page->_next;
   AllocationPage *next = page->_next;
   if (next != nullptr) {
   if (next != nullptr) {
     delete_page(next);
     delete_page(next);

+ 0 - 16
panda/src/pgraph/cullableObject.I

@@ -11,16 +11,6 @@
  * @date 2002-03-04
  * @date 2002-03-04
  */
  */
 
 
-/**
- * Creates an empty CullableObject whose pointers can be filled in later.
- */
-INLINE CullableObject::
-CullableObject() {
-#ifdef DO_MEMORY_USAGE
-  MemoryUsage::record_pointer(this, get_class_type());
-#endif
-}
-
 /**
 /**
  * Creates a CullableObject based the indicated geom, with the indicated
  * Creates a CullableObject based the indicated geom, with the indicated
  * render state and transform.
  * render state and transform.
@@ -32,9 +22,6 @@ CullableObject(CPT(Geom) geom, CPT(RenderState) state,
   _state(std::move(state)),
   _state(std::move(state)),
   _internal_transform(std::move(internal_transform))
   _internal_transform(std::move(internal_transform))
 {
 {
-#ifdef DO_MEMORY_USAGE
-  MemoryUsage::record_pointer(this, get_class_type());
-#endif
 }
 }
 
 
 /**
 /**
@@ -47,9 +34,6 @@ CullableObject(const CullableObject &copy) :
   _state(copy._state),
   _state(copy._state),
   _internal_transform(copy._internal_transform)
   _internal_transform(copy._internal_transform)
 {
 {
-#ifdef DO_MEMORY_USAGE
-  MemoryUsage::record_pointer(this, get_class_type());
-#endif
 }
 }
 
 
 /**
 /**

+ 1 - 1
panda/src/pgraph/cullableObject.h

@@ -40,7 +40,7 @@ class GeomMunger;
  */
  */
 class EXPCL_PANDA_PGRAPH CullableObject {
 class EXPCL_PANDA_PGRAPH CullableObject {
 public:
 public:
-  INLINE CullableObject();
+  INLINE CullableObject() = default;
   INLINE CullableObject(CPT(Geom) geom, CPT(RenderState) state,
   INLINE CullableObject(CPT(Geom) geom, CPT(RenderState) state,
                         CPT(TransformState) internal_transform);
                         CPT(TransformState) internal_transform);
 
 

+ 35 - 9
panda/src/pgraph/loader.cxx

@@ -305,21 +305,44 @@ try_load_file(const Filename &pathname, const LoaderOptions &options,
       (options.get_flags() & LoaderOptions::LF_no_disk_cache) == 0) {
       (options.get_flags() & LoaderOptions::LF_no_disk_cache) == 0) {
     // See if the model can be found in the on-disk cache, if it is active.
     // See if the model can be found in the on-disk cache, if it is active.
     record = cache->lookup(pathname, "bam");
     record = cache->lookup(pathname, "bam");
-    if (record != nullptr) {
-      if (record->has_data()) {
+    if (record != nullptr && record->has_data()) {
+      PT(PandaNode) result = DCAST(PandaNode, record->get_data());
+
+      ModelRoot *model_root = nullptr;
+      if (result != nullptr && result->is_of_type(ModelRoot::get_class_type())) {
+        model_root = DCAST(ModelRoot, result.p());
+
+        if (requested_type != nullptr &&
+            model_root->get_loader_type() != requested_type->get_type()) {
+          result.clear();
+          if (report_errors) {
+            loader_cat.info()
+              << "Model " << pathname << " found in disk cache, but using "
+              << "unexpected loader " << model_root->get_loader_type()
+              << " (expected " << requested_type->get_type() << ").\n";
+          }
+        }
+      }
+
+      if (result != nullptr) {
         if (report_errors) {
         if (report_errors) {
-          loader_cat.info()
-            << "Model " << pathname << " found in disk cache.\n";
+          TypeHandle loaded_type = model_root->get_loader_type();
+          if (loaded_type != TypeHandle::none()) {
+            loader_cat.info()
+              << "Model " << pathname << " found in disk cache (loaded using "
+              << loaded_type << ").\n";
+          } else {
+            loader_cat.info()
+              << "Model " << pathname << " found in disk cache.\n";
+          }
         }
         }
-        PT(PandaNode) result = DCAST(PandaNode, record->get_data());
 
 
         if (premunge_data) {
         if (premunge_data) {
           SceneGraphReducer sgr;
           SceneGraphReducer sgr;
           sgr.premunge(result, RenderState::make_empty());
           sgr.premunge(result, RenderState::make_empty());
         }
         }
 
 
-        if (result->is_of_type(ModelRoot::get_class_type())) {
-          ModelRoot *model_root = DCAST(ModelRoot, result.p());
+        if (model_root != nullptr) {
           model_root->set_fullpath(pathname);
           model_root->set_fullpath(pathname);
           model_root->set_timestamp(record->get_source_timestamp());
           model_root->set_timestamp(record->get_source_timestamp());
 
 
@@ -336,8 +359,7 @@ try_load_file(const Filename &pathname, const LoaderOptions &options,
         return result;
         return result;
       }
       }
     }
     }
-
-    if (loader_cat.is_debug()) {
+    else if (loader_cat.is_debug()) {
       loader_cat.debug()
       loader_cat.debug()
         << "Model " << pathname << " not found in cache.\n";
         << "Model " << pathname << " not found in cache.\n";
     }
     }
@@ -354,6 +376,10 @@ try_load_file(const Filename &pathname, const LoaderOptions &options,
     result = requested_type->load_file(pathname, options, record);
     result = requested_type->load_file(pathname, options, record);
   }
   }
   if (result != nullptr) {
   if (result != nullptr) {
+    if (result->is_of_type(ModelRoot::get_class_type())) {
+      ((ModelRoot *)result.p())->set_loader_type(requested_type->get_type());
+    }
+
     if (record != nullptr) {
     if (record != nullptr) {
       // Store the loaded model in the model cache.
       // Store the loaded model in the model cache.
       record->set_data(result);
       record->set_data(result);

+ 23 - 3
panda/src/pgraph/modelRoot.I

@@ -19,7 +19,8 @@ ModelRoot(const std::string &name) :
   ModelNode(name),
   ModelNode(name),
   _fullpath(name),
   _fullpath(name),
   _timestamp(0),
   _timestamp(0),
-  _reference(new ModelRoot::ModelReference)
+  _reference(new ModelRoot::ModelReference),
+  _loader_type(TypeHandle::none())
 {
 {
 }
 }
 
 
@@ -31,7 +32,8 @@ ModelRoot(const Filename &fullpath, time_t timestamp) :
   ModelNode(fullpath.get_basename()),
   ModelNode(fullpath.get_basename()),
   _fullpath(fullpath),
   _fullpath(fullpath),
   _timestamp(timestamp),
   _timestamp(timestamp),
-  _reference(new ModelRoot::ModelReference)
+  _reference(new ModelRoot::ModelReference),
+  _loader_type(TypeHandle::none())
 {
 {
 }
 }
 
 
@@ -114,6 +116,23 @@ set_reference(ModelRoot::ModelReference *ref) {
   _reference = ref;
   _reference = ref;
 }
 }
 
 
+/**
+ * Returns the type of the loader object that was used to load this model.
+ */
+INLINE TypeHandle ModelRoot::
+get_loader_type() const {
+  return _loader_type;
+}
+
+/**
+ * Sets the type of the loader object used to load this model.  Normally this
+ * is only called by Loader.
+ */
+INLINE void ModelRoot::
+set_loader_type(TypeHandle type) {
+  _loader_type = type;
+}
+
 /**
 /**
  *
  *
  */
  */
@@ -122,7 +141,8 @@ ModelRoot(const ModelRoot &copy) :
   ModelNode(copy),
   ModelNode(copy),
   _fullpath(copy._fullpath),
   _fullpath(copy._fullpath),
   _timestamp(copy._timestamp),
   _timestamp(copy._timestamp),
-  _reference(copy._reference)
+  _reference(copy._reference),
+  _loader_type(copy._loader_type)
 {
 {
 }
 }
 
 

+ 10 - 0
panda/src/pgraph/modelRoot.cxx

@@ -41,6 +41,10 @@ register_with_read_factory() {
 void ModelRoot::
 void ModelRoot::
 write_datagram(BamWriter *manager, Datagram &dg) {
 write_datagram(BamWriter *manager, Datagram &dg) {
   ModelNode::write_datagram(manager, dg);
   ModelNode::write_datagram(manager, dg);
+
+  if (manager->get_file_minor_ver() >= 46) {
+    manager->write_handle(dg, _loader_type);
+  }
 }
 }
 
 
 /**
 /**
@@ -67,4 +71,10 @@ make_from_bam(const FactoryParams &params) {
 void ModelRoot::
 void ModelRoot::
 fillin(DatagramIterator &scan, BamReader *manager) {
 fillin(DatagramIterator &scan, BamReader *manager) {
   ModelNode::fillin(scan, manager);
   ModelNode::fillin(scan, manager);
+
+  if (manager->get_file_minor_ver() >= 46) {
+    _loader_type = manager->read_handle(scan);
+  } else {
+    _loader_type = TypeHandle::none();
+  }
 }
 }

+ 8 - 0
panda/src/pgraph/modelRoot.h

@@ -50,6 +50,13 @@ PUBLISHED:
   void set_reference(ModelReference *ref);
   void set_reference(ModelReference *ref);
   MAKE_PROPERTY(reference, get_reference, set_reference);
   MAKE_PROPERTY(reference, get_reference, set_reference);
 
 
+public:
+  INLINE TypeHandle get_loader_type() const;
+  INLINE void set_loader_type(TypeHandle loader_type);
+
+PUBLISHED:
+  MAKE_PROPERTY(loader_type, get_loader_type);
+
 protected:
 protected:
   INLINE ModelRoot(const ModelRoot &copy);
   INLINE ModelRoot(const ModelRoot &copy);
 
 
@@ -60,6 +67,7 @@ private:
   Filename _fullpath;
   Filename _fullpath;
   time_t _timestamp;
   time_t _timestamp;
   PT(ModelReference) _reference;
   PT(ModelReference) _reference;
+  TypeHandle _loader_type;
 
 
 public:
 public:
   static void register_with_read_factory();
   static void register_with_read_factory();

+ 2 - 3
panda/src/pgraph/nodePath_ext.cxx

@@ -56,10 +56,9 @@ PyObject *Extension<NodePath>::
 __deepcopy__(PyObject *self, PyObject *memo) const {
 __deepcopy__(PyObject *self, PyObject *memo) const {
   extern struct Dtool_PyTypedObject Dtool_NodePath;
   extern struct Dtool_PyTypedObject Dtool_NodePath;
 
 
-  // Borrowed reference.
   PyObject *dupe;
   PyObject *dupe;
-  if (PyDict_GetItemRef(memo, self, &dupe) > 0) {
-    // Already in the memo dictionary.
+  if (PyDict_GetItemRef(memo, self, &dupe) != 0) {
+    // Already in the memo dictionary (or an error happened).
     return dupe;
     return dupe;
   }
   }
 
 

+ 2 - 3
panda/src/pgraph/pandaNode_ext.cxx

@@ -46,10 +46,9 @@ PyObject *Extension<PandaNode>::
 __deepcopy__(PyObject *self, PyObject *memo) const {
 __deepcopy__(PyObject *self, PyObject *memo) const {
   extern struct Dtool_PyTypedObject Dtool_PandaNode;
   extern struct Dtool_PyTypedObject Dtool_PandaNode;
 
 
-  // Borrowed reference.
   PyObject *dupe;
   PyObject *dupe;
-  if (PyDict_GetItemRef(memo, self, &dupe) > 0) {
-    // Already in the memo dictionary.
+  if (PyDict_GetItemRef(memo, self, &dupe) != 0) {
+    // Already in the memo dictionary (or an error happened).
     return dupe;
     return dupe;
   }
   }
 
 

+ 8 - 0
panda/src/pgraphnodes/lightLensNode.I

@@ -36,6 +36,14 @@ get_shadow_buffer_sort() const {
   return _sb_sort;
   return _sb_sort;
 }
 }
 
 
+/**
+ * Sets the sort of the shadow buffer to be created for this light source.
+ */
+INLINE void LightLensNode::
+set_shadow_buffer_sort(int sort) {
+  _sb_sort = sort;
+}
+
 /**
 /**
  * Returns the size of the shadow buffer to be created for this light source.
  * Returns the size of the shadow buffer to be created for this light source.
  */
  */

+ 2 - 0
panda/src/pgraphnodes/lightLensNode.h

@@ -42,6 +42,7 @@ PUBLISHED:
   void set_shadow_caster(bool caster, int buffer_xsize, int buffer_ysize, int sort = -10);
   void set_shadow_caster(bool caster, int buffer_xsize, int buffer_ysize, int sort = -10);
 
 
   INLINE int get_shadow_buffer_sort() const;
   INLINE int get_shadow_buffer_sort() const;
+  INLINE void set_shadow_buffer_sort(int sort);
 
 
   INLINE LVecBase2i get_shadow_buffer_size() const;
   INLINE LVecBase2i get_shadow_buffer_size() const;
   INLINE void set_shadow_buffer_size(const LVecBase2i &size);
   INLINE void set_shadow_buffer_size(const LVecBase2i &size);
@@ -50,6 +51,7 @@ PUBLISHED:
 
 
 PUBLISHED:
 PUBLISHED:
   MAKE_PROPERTY(shadow_caster, is_shadow_caster);
   MAKE_PROPERTY(shadow_caster, is_shadow_caster);
+  MAKE_PROPERTY(shadow_buffer_sort, get_shadow_buffer_sort, set_shadow_buffer_sort);
   MAKE_PROPERTY(shadow_buffer_size, get_shadow_buffer_size, set_shadow_buffer_size);
   MAKE_PROPERTY(shadow_buffer_size, get_shadow_buffer_size, set_shadow_buffer_size);
 
 
 public:
 public:

+ 4 - 1
panda/src/physics/linearDistanceForce.h

@@ -26,7 +26,10 @@ PUBLISHED:
   enum FalloffType {
   enum FalloffType {
     FT_ONE_OVER_R,
     FT_ONE_OVER_R,
     FT_ONE_OVER_R_SQUARED,
     FT_ONE_OVER_R_SQUARED,
-    FT_ONE_OVER_R_CUBED
+    FT_ONE_OVER_R_CUBED,
+    FT_ONE_OVER_R_OVER_DISTANCE,
+    FT_ONE_OVER_R_OVER_DISTANCE_SQUARED,
+    FT_ONE_OVER_R_OVER_DISTANCE_CUBED
   };
   };
 
 
   INLINE void set_radius(PN_stdfloat r);
   INLINE void set_radius(PN_stdfloat r);

+ 19 - 1
panda/src/physics/linearSinkForce.cxx

@@ -61,7 +61,25 @@ make_copy() {
  */
  */
 LVector3 LinearSinkForce::
 LVector3 LinearSinkForce::
 get_child_vector(const PhysicsObject *po) {
 get_child_vector(const PhysicsObject *po) {
-  return (get_force_center() - po->get_position()) * get_scalar_term();
+  LVector3 distance_vector = get_force_center() - po->get_position();
+  PN_stdfloat distance_squared = distance_vector.length_squared();
+
+  if (distance_squared == 0) {
+    return distance_vector;
+  }
+
+  PN_stdfloat scalar = get_scalar_term();
+
+  switch (get_falloff_type()) {
+  case FT_ONE_OVER_R_OVER_DISTANCE:
+    return (distance_vector / sqrt(distance_squared)) * scalar;
+  case FT_ONE_OVER_R_OVER_DISTANCE_SQUARED:
+    return (distance_vector / distance_squared) * scalar;
+  case FT_ONE_OVER_R_OVER_DISTANCE_CUBED:
+    return (distance_vector / (distance_squared * sqrt(distance_squared))) * scalar;
+  default:
+    return distance_vector * scalar;
+  }
 }
 }
 
 
 /**
 /**

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