Преглед изворни кода

Merge branch 'master' into shaderpipeline

rdb пре 2 година
родитељ
комит
685f5421bb
100 измењених фајлова са 1956 додато и 1122 уклоњено
  1. 112 48
      .github/workflows/ci.yml
  2. 3 11
      BACKERS.md
  3. 5 5
      README.md
  4. 10 8
      contrib/src/rplight/pssmCameraRig.cxx
  5. 2 2
      direct/src/dcparser/dcClass.cxx
  6. 17 8
      direct/src/dist/FreezeTool.py
  7. 66 19
      direct/src/dist/commands.py
  8. 30 0
      direct/src/dist/pefile.py
  9. 1 1
      direct/src/distributed/ConnectionRepository.py
  10. 3 2
      direct/src/distributed/cConnectionRepository.cxx
  11. 7 0
      direct/src/filter/CommonFilters.py
  12. 1 1
      direct/src/interval/Interval.py
  13. 1 1
      direct/src/interval/MetaInterval.py
  14. 1 1
      direct/src/interval/cInterval.cxx
  15. 170 72
      direct/src/motiontrail/MotionTrail.py
  16. 285 434
      direct/src/motiontrail/cMotionTrail.cxx
  17. 37 39
      direct/src/motiontrail/cMotionTrail.h
  18. 26 6
      direct/src/showbase/ContainerLeakDetector.py
  19. 28 14
      direct/src/showbase/ContainerReport.py
  20. 5 2
      direct/src/showbase/EventManager.py
  21. 6 7
      direct/src/showbase/GarbageReport.py
  22. 1 1
      direct/src/showbase/Job.py
  23. 46 35
      direct/src/showbase/ShowBase.py
  24. 0 1
      direct/src/showutil/Effects.py
  25. 96 0
      doc/ReleaseNotes
  26. 1 0
      dtool/src/dtoolbase/deletedBufferChain.cxx
  27. 2 2
      dtool/src/dtoolbase/memoryHook.I
  28. 18 18
      dtool/src/dtoolbase/memoryHook.cxx
  29. 5 5
      dtool/src/dtoolbase/memoryHook.h
  30. 1 0
      dtool/src/dtoolbase/patomic.I
  31. 0 19
      dtool/src/dtoolbase/typeHandle.cxx
  32. 5 3
      dtool/src/dtoolbase/typeHandle.h
  33. 0 18
      dtool/src/dtoolbase/typeRegistry.cxx
  34. 0 3
      dtool/src/dtoolbase/typeRegistry.h
  35. 0 8
      dtool/src/dtoolbase/typedObject.I
  36. 0 2
      dtool/src/dtoolbase/typedObject.h
  37. 4 0
      dtool/src/dtoolutil/executionEnvironment.cxx
  38. 32 21
      dtool/src/dtoolutil/filename.cxx
  39. 11 8
      dtool/src/dtoolutil/filename.h
  40. 10 10
      dtool/src/dtoolutil/textEncoder.h
  41. 7 2
      dtool/src/interrogate/functionRemap.cxx
  42. 25 10
      dtool/src/interrogate/interfaceMakerPythonNative.cxx
  43. 1 1
      dtool/src/interrogate/interrogate.cxx
  44. 3 3
      dtool/src/interrogatedb/dtool_super_base.cxx
  45. 4 0
      dtool/src/interrogatedb/py_compat.h
  46. 12 18
      dtool/src/interrogatedb/py_panda.cxx
  47. 3 2
      dtool/src/interrogatedb/py_panda.h
  48. 3 0
      dtool/src/parser-inc/Python.h
  49. 0 71
      dtool/src/parser-inc/tinyxml.h
  50. 2 0
      dtool/src/prc/CMakeLists.txt
  51. 3 1
      dtool/src/prc/configVariable.h
  52. 11 0
      dtool/src/prc/configVariableFilename.I
  53. 1 0
      dtool/src/prc/configVariableFilename.h
  54. 73 2
      dtool/src/prc/encryptStreamBuf.cxx
  55. 48 0
      dtool/src/prc/notify_ext.cxx
  56. 37 0
      dtool/src/prc/notify_ext.h
  57. 1 0
      dtool/src/prc/p3prc_ext_composite.cxx
  58. 4 0
      dtool/src/prc/pnotify.h
  59. 3 2
      dtool/src/prc/streamReader.h
  60. 3 1
      dtool/src/prc/streamWriter.h
  61. 31 8
      makepanda/installer.nsi
  62. 52 24
      makepanda/installpanda.py
  63. 31 37
      makepanda/makepanda.py
  64. 6 2
      makepanda/makepandacore.py
  65. 4 0
      makepanda/makewheel.py
  66. 0 11
      makepanda/panda3d.desktop
  67. 12 0
      makepanda/pstats.desktop
  68. 2 2
      makepanda/pview.desktop
  69. 4 2
      panda/CMakeLists.txt
  70. 1 1
      panda/metalibs/panda/CMakeLists.txt
  71. 8 0
      panda/src/audio/audioSound.I
  72. 1 1
      panda/src/audio/audioSound.cxx
  73. 6 1
      panda/src/audio/audioSound.h
  74. 11 16
      panda/src/audio/nullAudioManager.cxx
  75. 1 1
      panda/src/audio/nullAudioSound.cxx
  76. 1 2
      panda/src/audiotraits/fmodAudioSound.cxx
  77. 1 1
      panda/src/audiotraits/openalAudioSound.cxx
  78. 0 1
      panda/src/audiotraits/openalAudioSound.h
  79. 54 21
      panda/src/cocoadisplay/cocoaGraphicsWindow.mm
  80. 9 1
      panda/src/cocoadisplay/cocoaPandaAppDelegate.h
  81. 48 0
      panda/src/cocoadisplay/cocoaPandaAppDelegate.mm
  82. 1 0
      panda/src/cocoadisplay/cocoaPandaWindowDelegate.h
  83. 13 4
      panda/src/cocoadisplay/cocoaPandaWindowDelegate.mm
  84. 0 8
      panda/src/collide/collisionHandler.cxx
  85. 5 6
      panda/src/collide/collisionHandler.h
  86. 2 0
      panda/src/display/CMakeLists.txt
  87. 2 0
      panda/src/display/config_display.cxx
  88. 3 1
      panda/src/display/frameBufferProperties.h
  89. 5 0
      panda/src/display/graphicsEngine.cxx
  90. 89 1
      panda/src/display/graphicsOutput.cxx
  91. 7 0
      panda/src/display/graphicsOutput.h
  92. 3 1
      panda/src/display/graphicsPipeSelection.h
  93. 5 1
      panda/src/display/graphicsStateGuardian.cxx
  94. 10 7
      panda/src/display/graphicsStateGuardian.h
  95. 4 2
      panda/src/display/graphicsWindow.h
  96. 1 0
      panda/src/display/p3display_composite2.cxx
  97. 39 0
      panda/src/display/screenshotRequest.I
  98. 104 0
      panda/src/display/screenshotRequest.cxx
  99. 73 0
      panda/src/display/screenshotRequest.h
  100. 15 11
      panda/src/display/windowProperties.h

+ 112 - 48
.github/workflows/ci.yml

@@ -92,10 +92,10 @@ jobs:
     - name: Install dependencies (macOS)
       if: runner.os == 'macOS'
       run: |
-        curl -O https://www.panda3d.org/download/panda3d-1.10.10/panda3d-1.10.10-tools-mac.tar.gz
-        tar -xf panda3d-1.10.10-tools-mac.tar.gz
-        mv panda3d-1.10.10/thirdparty thirdparty
-        rmdir panda3d-1.10.10
+        curl -O https://www.panda3d.org/download/panda3d-1.10.13/panda3d-1.10.13-tools-mac.tar.gz
+        tar -xf panda3d-1.10.13-tools-mac.tar.gz
+        mv panda3d-1.10.13/thirdparty thirdparty
+        rmdir panda3d-1.10.13
 
         # Temporary hack so that pzip can run, since we are about to remove Cg anyway.
         install_name_tool -id "$(pwd)/thirdparty/darwin-libs-a/nvidiacg/lib/libCg.dylib" thirdparty/darwin-libs-a/nvidiacg/lib/libCg.dylib
@@ -124,16 +124,16 @@ jobs:
       uses: actions/cache@v1
       with:
         path: thirdparty
-        key: ci-cmake-${{ runner.OS }}-thirdparty-v1.10.10-r1
+        key: ci-cmake-${{ runner.OS }}-thirdparty-v1.10.13-r1
     - name: Install dependencies (Windows)
       if: runner.os == 'Windows'
       shell: powershell
       run: |
         if (!(Test-Path thirdparty/win-libs-vc14-x64)) {
           $wc = New-Object System.Net.WebClient
-          $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.10/panda3d-1.10.10-tools-win64.zip", "thirdparty-tools.zip")
+          $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.13/panda3d-1.10.13-tools-win64.zip", "thirdparty-tools.zip")
           Expand-Archive -Path thirdparty-tools.zip
-          Move-Item -Path thirdparty-tools/panda3d-1.10.10/thirdparty -Destination .
+          Move-Item -Path thirdparty-tools/panda3d-1.10.13/thirdparty -Destination .
         }
 
     - name: ccache (non-Windows)
@@ -173,7 +173,7 @@ jobs:
         -D CMAKE_UNITY_BUILD=${{ matrix.unity }}
         -D CMAKE_BUILD_TYPE="${{ matrix.config }}"
         -D BUILD_METALIBS=${{ matrix.metalibs }}
-        -D HAVE_PYTHON=${{ matrix.python }}
+        -D HAVE_PYTHON=${{ runner.os != 'Windows' && matrix.python || 'NO' }}
         -D HAVE_EIGEN=${{ matrix.eigen }}
         ..
 
@@ -184,25 +184,25 @@ jobs:
       run: cmake --build . --config ${{ matrix.config }} --parallel 4
       # END A
 
-    - name: Setup Python (Python 3.6)
+    - name: Setup Python (Python 3.7)
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
-        python-version: 3.6
-    - name: Configure (Python 3.6)
+        python-version: '3.7'
+    - name: Configure (Python 3.7)
       if: contains(matrix.python, 'YES')
       working-directory: build
       shell: bash
       run: >
-        cmake -DWANT_PYTHON_VERSION=3.6
+        cmake -DWANT_PYTHON_VERSION=3.7 -DHAVE_PYTHON=YES
         -DPython_FIND_REGISTRY=NEVER -DPython_ROOT="$pythonLocation" .
-    - name: Build (Python 3.6)
+    - name: Build (Python 3.7)
       if: contains(matrix.python, 'YES')
       # BEGIN A
       working-directory: build
       run: cmake --build . --config ${{ matrix.config }} --parallel 4
       # END A
-    - name: Test (Python 3.6)
+    - name: Test (Python 3.7)
       # BEGIN B
       if: contains(matrix.python, 'YES')
       working-directory: build
@@ -216,25 +216,25 @@ jobs:
         $PYTHON_EXECUTABLE -m pytest ../tests --cov=.
       # END B
 
-    - name: Setup Python (Python 3.7)
+    - name: Setup Python (Python 3.8)
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
-        python-version: 3.7
-    - name: Configure (Python 3.7)
+        python-version: '3.8'
+    - name: Configure (Python 3.8)
       if: contains(matrix.python, 'YES')
       working-directory: build
       shell: bash
       run: >
-        cmake -DWANT_PYTHON_VERSION=3.7
+        cmake -DWANT_PYTHON_VERSION=3.8 -DHAVE_PYTHON=YES
         -DPython_FIND_REGISTRY=NEVER -DPython_ROOT="$pythonLocation" .
-    - name: Build (Python 3.7)
+    - name: Build (Python 3.8)
       if: contains(matrix.python, 'YES')
       # BEGIN A
       working-directory: build
       run: cmake --build . --config ${{ matrix.config }} --parallel 4
       # END A
-    - name: Test (Python 3.7)
+    - name: Test (Python 3.8)
       # BEGIN B
       if: contains(matrix.python, 'YES')
       working-directory: build
@@ -248,25 +248,25 @@ jobs:
         $PYTHON_EXECUTABLE -m pytest ../tests --cov=.
       # END B
 
-    - name: Setup Python (Python 3.8)
+    - name: Setup Python (Python 3.9)
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
-        python-version: 3.8
-    - name: Configure (Python 3.8)
+        python-version: '3.9'
+    - name: Configure (Python 3.9)
       if: contains(matrix.python, 'YES')
       working-directory: build
       shell: bash
       run: >
-        cmake -DWANT_PYTHON_VERSION=3.8
+        cmake -DWANT_PYTHON_VERSION=3.9 -DHAVE_PYTHON=YES
         -DPython_FIND_REGISTRY=NEVER -DPython_ROOT="$pythonLocation" .
-    - name: Build (Python 3.8)
+    - name: Build (Python 3.9)
       if: contains(matrix.python, 'YES')
       # BEGIN A
       working-directory: build
       run: cmake --build . --config ${{ matrix.config }} --parallel 4
       # END A
-    - name: Test (Python 3.8)
+    - name: Test (Python 3.9)
       # BEGIN B
       if: contains(matrix.python, 'YES')
       working-directory: build
@@ -280,25 +280,57 @@ jobs:
         $PYTHON_EXECUTABLE -m pytest ../tests --cov=.
       # END B
 
-    - name: Setup Python (Python 3.9)
+    - name: Setup Python (Python 3.10)
       if: contains(matrix.python, 'YES')
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
-        python-version: 3.9
-    - name: Configure (Python 3.9)
+        python-version: '3.10'
+    - name: Configure (Python 3.10)
       if: contains(matrix.python, 'YES')
       working-directory: build
       shell: bash
       run: >
-        cmake -DWANT_PYTHON_VERSION=3.9
+        cmake -DWANT_PYTHON_VERSION=3.10 -DHAVE_PYTHON=YES
         -DPython_FIND_REGISTRY=NEVER -DPython_ROOT="$pythonLocation" .
-    - name: Build (Python 3.9)
+    - name: Build (Python 3.10)
       if: contains(matrix.python, 'YES')
       # BEGIN A
       working-directory: build
       run: cmake --build . --config ${{ matrix.config }} --parallel 4
       # END A
-    - name: Test (Python 3.9)
+    - name: Test (Python 3.10)
+      # BEGIN B
+      if: contains(matrix.python, 'YES')
+      working-directory: build
+      shell: bash
+      env:
+        PYTHONPATH: ${{ matrix.config }}
+      run: |
+        PYTHON_EXECUTABLE=$(grep 'Python_EXECUTABLE:' CMakeCache.txt | sed 's/.*=//')
+        $PYTHON_EXECUTABLE -m pip install pytest pytest-cov
+        export COVERAGE_FILE=.coverage.$RANDOM LLVM_PROFILE_FILE=$PWD/pid-%p.profraw
+        $PYTHON_EXECUTABLE -m pytest ../tests --cov=.
+      # END B
+
+    - name: Setup Python (Python 3.11)
+      if: contains(matrix.python, 'YES')
+      uses: actions/setup-python@v4
+      with:
+        python-version: '3.11'
+    - name: Configure (Python 3.11)
+      if: contains(matrix.python, 'YES')
+      working-directory: build
+      shell: bash
+      run: >
+        cmake -DWANT_PYTHON_VERSION=3.11 -DHAVE_PYTHON=YES
+        -DPython_FIND_REGISTRY=NEVER -DPython_ROOT="$pythonLocation" .
+    - name: Build (Python 3.11)
+      if: contains(matrix.python, 'YES')
+      # BEGIN A
+      working-directory: build
+      run: cmake --build . --config ${{ matrix.config }} --parallel 4
+      # END A
+    - name: Test (Python 3.11)
       # BEGIN B
       if: contains(matrix.python, 'YES')
       working-directory: build
@@ -347,21 +379,50 @@ jobs:
       shell: powershell
       run: |
         $wc = New-Object System.Net.WebClient
-        $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.11/panda3d-1.10.11-tools-win64.zip", "thirdparty-tools.zip")
+        $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.13/panda3d-1.10.13-tools-win64.zip", "thirdparty-tools.zip")
         Expand-Archive -Path thirdparty-tools.zip
-        Move-Item -Path thirdparty-tools/panda3d-1.10.11/thirdparty -Destination .
+        Move-Item -Path thirdparty-tools/panda3d-1.10.13/thirdparty -Destination .
     - name: Get thirdparty packages (macOS)
       if: runner.os == 'macOS'
       run: |
-        curl -O https://www.panda3d.org/download/panda3d-1.10.11/panda3d-1.10.11-tools-mac.tar.gz
-        tar -xf panda3d-1.10.11-tools-mac.tar.gz
-        mv panda3d-1.10.11/thirdparty thirdparty
-        rmdir panda3d-1.10.11
+        curl -O https://www.panda3d.org/download/panda3d-1.10.13/panda3d-1.10.13-tools-mac.tar.gz
+        tar -xf panda3d-1.10.13-tools-mac.tar.gz
+        mv panda3d-1.10.13/thirdparty thirdparty
+        rmdir panda3d-1.10.13
         (cd thirdparty/darwin-libs-a && rm -rf rocket)
+
+    - name: Set up Python 3.11
+      uses: actions/setup-python@v4
+      with:
+        python-version: '3.11'
+    - name: Build Python 3.11
+      shell: bash
+      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
+    - name: Test Python 3.11
+      shell: bash
+      run: |
+        python -m pip install pytest
+        PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+
+    - name: Set up Python 3.10
+      uses: actions/setup-python@v4
+      with:
+        python-version: '3.10'
+    - name: Build Python 3.10
+      shell: bash
+      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
+    - name: Test Python 3.10
+      shell: bash
+      run: |
+        python -m pip install pytest
+        PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+
     - name: Set up Python 3.9
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
-        python-version: 3.9
+        python-version: '3.9'
     - name: Build Python 3.9
       shell: bash
       run: |
@@ -371,10 +432,11 @@ jobs:
       run: |
         python -m pip install pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+
     - name: Set up Python 3.8
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
-        python-version: 3.8
+        python-version: '3.8'
     - name: Build Python 3.8
       shell: bash
       run: |
@@ -384,10 +446,11 @@ jobs:
       run: |
         python -m pip install pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+
     - name: Set up Python 3.7
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
-        python-version: 3.7
+        python-version: '3.7'
     - name: Build Python 3.7
       shell: bash
       run: |
@@ -397,6 +460,7 @@ jobs:
       run: |
         python -m pip install pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+
     - name: Make installer
       run: |
         python makepanda/makepackage.py --verbose --lzma

+ 3 - 11
BACKERS.md

@@ -2,20 +2,12 @@
 
 This is a list of all the people who are contributing financially to Panda3D.  If you'd like to join them, visit [our campaign on OpenCollective](https://opencollective.com/panda3d)!
 
-## Gold Sponsors
-
-![Gold Sponsors](https://opencollective.com/panda3d/tiers/gold-sponsor.svg?avatarHeight=48&width=600)
-
-* [tcdude](https://opencollective.com/tizilogic)
-
 ## Bronze Sponsors
 
 [<img src="https://www.panda3d.org/wp-content/uploads/2021/02/changecrab_logo.png" alt="ChangeCrab" height="48">](https://changecrab.com/) ![Bronze Sponsors](https://opencollective.com/panda3d/tiers/bronze-sponsor.svg?avatarHeight=48&width=600)
 
-* [Mitchell Stokes](https://opencollective.com/mitchell-stokes)
 * [Daniel Stokes](https://opencollective.com/daniel-stokes)
 * [David Rose](https://opencollective.com/david-rose)
-* [ChangeCrab](https://changecrab.com)
 
 ## Benefactors
 
@@ -24,17 +16,17 @@ This is a list of all the people who are contributing financially to Panda3D.  I
 * Sam Edwards
 * Max Voss
 * Hawkheart
-* Dan Mlodecki
+* Veronica
 
 ## Enthusiasts
 
 ![Enthusiasts](https://opencollective.com/panda3d/tiers/enthusiast.svg?avatarHeight=48&width=600)
 
 * Eric Thomson
-* Kyle Roach
 * Brian Lach
-* C0MPU73R
 * Maxwell Dreytser
+* SureBet
+* Gyedo Jeon
 
 ## Backers
 

+ 5 - 5
README.md

@@ -64,8 +64,8 @@ depending on whether you are on a 32-bit or 64-bit system, or you can
 [click here](https://github.com/rdb/panda3d-thirdparty) for instructions on
 building them from source.
 
-- https://www.panda3d.org/download/panda3d-1.10.12/panda3d-1.10.12-tools-win64.zip
-- https://www.panda3d.org/download/panda3d-1.10.12/panda3d-1.10.12-tools-win32.zip
+- https://www.panda3d.org/download/panda3d-1.10.13/panda3d-1.10.13-tools-win64.zip
+- https://www.panda3d.org/download/panda3d-1.10.13/panda3d-1.10.13-tools-win32.zip
 
 After acquiring these dependencies, you can build Panda3D from the command
 prompt using the following command.  Change the `--msvc-version` option based
@@ -80,10 +80,10 @@ makepanda\makepanda.bat --everything --installer --msvc-version=14.3 --windows-s
 When the build succeeds, it will produce an .exe file that you can use to
 install Panda3D on your system.
 
-Note: you may choose to remove --no-eigen and build with Eigen support in
+**Note:** you may choose to remove `--no-eigen` and build with Eigen support in
 order to improve runtime performance.  However, this will cause the build to
 take hours to complete, as Eigen is a heavily template-based library, and the
-the MSVC compiler does not perform well under these circumstances.
+MSVC compiler does not perform well under those circumstances.
 
 Linux
 -----
@@ -136,7 +136,7 @@ macOS
 -----
 
 On macOS, you will need to download a set of precompiled thirdparty packages in order to
-compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.12/panda3d-1.10.12-tools-mac.tar.gz).
+compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.13/panda3d-1.10.13-tools-mac.tar.gz).
 
 After placing the thirdparty directory inside the panda3d source directory,
 you may build Panda3D using a command like the following:

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

@@ -296,9 +296,10 @@ void PSSMCameraRig::compute_pssm_splits(const LMatrix4& transform, float max_dis
 
     // Reset the film size, offset and far-plane
     Camera* cam = DCAST(Camera, _cam_nodes[i].node());
-    cam->get_lens()->set_film_size(1, 1);
-    cam->get_lens()->set_film_offset(0, 0);
-    cam->get_lens()->set_near_far(1, 100);
+    Lens *lens = cam->get_lens();
+    lens->set_film_size(1, 1);
+    lens->set_film_offset(0, 0);
+    lens->set_near_far(1, 100);
 
     // Find a good initial position
     _cam_nodes[i].set_pos(cam_start);
@@ -320,16 +321,16 @@ void PSSMCameraRig::compute_pssm_splits(const LMatrix4& transform, float max_dis
       if (_max_film_sizes[i].get_x() < film_size.get_x()) _max_film_sizes[i].set_x(film_size.get_x());
       if (_max_film_sizes[i].get_y() < film_size.get_y()) _max_film_sizes[i].set_y(film_size.get_y());
 
-      cam->get_lens()->set_film_size(_max_film_sizes[i] * filmsize_bias);
+      lens->set_film_size(_max_film_sizes[i] * filmsize_bias);
     } else {
       // If we don't use a fixed film size, we can just set the film size
       // on the lens.
-      cam->get_lens()->set_film_size(film_size * filmsize_bias);
+      lens->set_film_size(film_size * filmsize_bias);
     }
 
     // Compute new film offset
-    cam->get_lens()->set_film_offset(film_offset);
-    cam->get_lens()->set_near_far(10, best_max_extent.get_z());
+    lens->set_film_offset(film_offset);
+    lens->set_near_far(10, best_max_extent.get_z());
     _camera_nearfar[i] = LVecBase2(10, best_max_extent.get_z());
 
     // Compute the camera MVP
@@ -399,7 +400,8 @@ void PSSMCameraRig::update(NodePath cam_node, const LVecBase3 &light_vector) {
   }
 
   // Do the actual PSSM
-  compute_pssm_splits( transform, _pssm_distance / lens->get_far(), light_vector );
+  double far_recip = std::max(1.0 / (double)lens->get_far(), (double)lens_far_limit);
+  compute_pssm_splits( transform, _pssm_distance * far_recip, light_vector );
 
   _update_collector.stop();
 }

+ 2 - 2
direct/src/dcparser/dcClass.cxx

@@ -24,8 +24,8 @@ using std::string;
 
 #ifdef WITHIN_PANDA
 #ifndef CPPPARSER
-PStatCollector DCClass::_update_pcollector("App:Show code:readerPollTask:Update");
-PStatCollector DCClass::_generate_pcollector("App:Show code:readerPollTask:Generate");
+PStatCollector DCClass::_update_pcollector("App:Tasks:readerPollTask:Update");
+PStatCollector DCClass::_generate_pcollector("App:Tasks:readerPollTask:Generate");
 #endif  // CPPPARSER
 
 ConfigVariableBool dc_multiple_inheritance

+ 17 - 8
direct/src/dist/FreezeTool.py

@@ -787,7 +787,7 @@ class Freezer:
             return 'ModuleDef(%s)' % (', '.join(args))
 
     def __init__(self, previous = None, debugLevel = 0,
-                 platform = None, path=None, hiddenImports=None):
+                 platform = None, path=None, hiddenImports=None, optimize=None):
         # Normally, we are freezing for our own platform.  Change this
         # if untrue.
         self.platform = platform or PandaSystem.getPlatform()
@@ -917,7 +917,13 @@ class Freezer:
                     ('.so', 'rb', 3),
                 ]
 
-        self.mf = PandaModuleFinder(excludes=['doctest'], suffixes=suffixes, path=path)
+        if optimize is None or optimize < 0:
+            self.optimize = sys.flags.optimize
+        else:
+            self.optimize = optimize
+
+        self.mf = PandaModuleFinder(excludes=['doctest'], suffixes=suffixes,
+                                    path=path, optimize=self.optimize)
 
     def excludeFrom(self, freezer):
         """ Excludes all modules that have already been processed by
@@ -1413,7 +1419,7 @@ class Freezer:
                 else:
                     filename += '.pyo'
                 if multifile.findSubfile(filename) < 0:
-                    code = compile('', moduleName, 'exec', optimize=2)
+                    code = compile('', moduleName, 'exec', optimize=self.optimize)
                     self.__addPyc(multifile, filename, code, compressionLevel)
 
             moduleDirs[str] = True
@@ -1493,7 +1499,7 @@ class Freezer:
                 source = open(sourceFilename.toOsSpecific(), 'r').read()
                 if source and source[-1] != '\n':
                     source = source + '\n'
-                code = compile(source, str(sourceFilename), 'exec', optimize=2)
+                code = compile(source, str(sourceFilename), 'exec', optimize=self.optimize)
 
         self.__addPyc(multifile, filename, code, compressionLevel)
 
@@ -1572,7 +1578,7 @@ class Freezer:
             # trouble importing it as a builtin module.  Synthesize a frozen
             # module that loads it as builtin.
             if '.' in moduleName and self.linkExtensionModules:
-                code = compile('import sys;del sys.modules["%s"];import imp;imp.init_builtin("%s")' % (moduleName, moduleName), moduleName, 'exec', optimize=2)
+                code = compile('import sys;del sys.modules["%s"];import imp;imp.init_builtin("%s")' % (moduleName, moduleName), moduleName, 'exec', optimize=self.optimize)
                 code = marshal.dumps(code)
                 mangledName = self.mangleName(moduleName)
                 moduleDefs.append(self.makeModuleDef(mangledName, code))
@@ -1845,7 +1851,7 @@ class Freezer:
                     code = 'import sys;del sys.modules["%s"];import sys,os,imp;imp.load_dynamic("%s",os.path.join(sys.path[0], "%s%s"))' % (moduleName, moduleName, moduleName, modext)
                 else:
                     code = 'import sys;del sys.modules["%s"];import sys,os,imp;imp.load_dynamic("%s",os.path.join(os.path.dirname(sys.executable), "%s%s"))' % (moduleName, moduleName, moduleName, modext)
-                code = compile(code, moduleName, 'exec', optimize=2)
+                code = compile(code, moduleName, 'exec', optimize=self.optimize)
                 code = marshal.dumps(code)
                 moduleList.append((moduleName, len(pool), len(code)))
                 pool += code
@@ -1946,6 +1952,8 @@ class Freezer:
                 flags |= 1
             if log_filename_strftime:
                 flags |= 2
+            if self.optimize < 2:
+                flags |= 4 # keep_docstrings
 
             # Compose the header we will be writing to the stub, to tell it
             # where to find the module data blob, as well as other variables.
@@ -2377,6 +2385,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
         """
 
         self.suffixes = kw.pop('suffixes', imp.get_suffixes())
+        self.optimize = kw.pop('optimize', -1)
 
         modulefinder.ModuleFinder.__init__(self, *args, **kw)
 
@@ -2530,7 +2539,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
         if type is _PKG_NAMESPACE_DIRECTORY:
             m = self.add_module(fqname)
-            m.__code__ = compile('', '', 'exec', optimize=2)
+            m.__code__ = compile('', '', 'exec', optimize=self.optimize)
             m.__path__ = pathname
             return m
 
@@ -2542,7 +2551,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
                 code = fp.read()
 
             code += b'\n' if isinstance(code, bytes) else '\n'
-            co = compile(code, pathname, 'exec', optimize=2)
+            co = compile(code, pathname, 'exec', optimize=self.optimize)
         elif type == imp.PY_COMPILED:
             if sys.version_info >= (3, 7):
                 try:

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

@@ -49,24 +49,39 @@ def _parse_dict(input):
     return d
 
 
+def _register_python_loaders():
+    # We need this method so that we don't depend on direct.showbase.Loader.
+    if getattr(_register_python_loaders, 'done', None):
+        return
 
-def egg2bam(_build_cmd, srcpath, dstpath):
+    _register_python_loaders.done = True
+
+    registry = p3d.LoaderFileTypeRegistry.getGlobalPtr()
+
+    for entry_point in pkg_resources.iter_entry_points('panda3d.loaders'):
+        registry.register_deferred_type(entry_point)
+
+
+def _model_to_bam(_build_cmd, srcpath, dstpath):
     if dstpath.endswith('.gz') or dstpath.endswith('.pz'):
         dstpath = dstpath[:-3]
     dstpath = dstpath + '.bam'
-    try:
-        subprocess.check_call([
-            'egg2bam',
-            '-o', dstpath,
-            '-pd', os.path.dirname(os.path.abspath(srcpath)),
-            '-ps', 'rel',
-            srcpath
-        ])
-    except FileNotFoundError:
-        raise RuntimeError('egg2bam failed: egg2bam was not found in the PATH')
-    except (subprocess.CalledProcessError, OSError) as err:
-        raise RuntimeError('egg2bam failed: {}'.format(err))
-    return dstpath
+
+    src_fn = p3d.Filename.from_os_specific(srcpath)
+    dst_fn = p3d.Filename.from_os_specific(dstpath)
+
+    _register_python_loaders()
+
+    loader = p3d.Loader.get_global_ptr()
+    options = p3d.LoaderOptions(p3d.LoaderOptions.LF_report_errors |
+                                p3d.LoaderOptions.LF_no_ram_cache)
+    node = loader.load_sync(src_fn, options)
+    if not node:
+        raise IOError('Failed to load model: %s' % (srcpath))
+
+    if not p3d.NodePath(node).write_bam_file(dst_fn):
+        raise IOError('Failed to write .bam file: %s' % (dstpath))
+
 
 macosx_binary_magics = (
     b'\xFE\xED\xFA\xCE', b'\xCE\xFA\xED\xFE',
@@ -243,7 +258,6 @@ class build_apps(setuptools.Command):
         ('platforms=', 'p', 'a list of platforms to build for'),
     ]
     default_file_handlers = {
-        '.egg': egg2bam,
     }
 
     def initialize_options(self):
@@ -277,13 +291,16 @@ class build_apps(setuptools.Command):
         self.log_filename = None
         self.log_filename_strftime = True
         self.log_append = False
+        self.prefer_discrete_gpu = False
         self.requirements_path = os.path.join(os.getcwd(), 'requirements.txt')
+        self.strip_docstrings = True
         self.use_optimized_wheels = True
         self.optimized_wheel_index = ''
         self.pypi_extra_indexes = [
             'https://archive.panda3d.org/thirdparty',
         ]
         self.file_handlers = {}
+        self.bam_model_extensions = ['.egg', '.gltf', '.glb']
         self.exclude_dependencies = [
             # Windows
             'kernel32.dll', 'user32.dll', 'wsock32.dll', 'ws2_32.dll',
@@ -444,6 +461,15 @@ class build_apps(setuptools.Command):
         for glob in self.exclude_dependencies:
             glob.case_sensitive = False
 
+        # bam_model_extensions registers a 2bam handler for each given extension.
+        # They can override a default handler, but not a custom handler.
+        if self.bam_model_extensions:
+            for ext in self.bam_model_extensions:
+                ext = '.' + ext.lstrip('.')
+                assert ext not in self.file_handlers, \
+                    'Extension {} occurs in both file_handlers and bam_model_extensions!'.format(ext)
+                self.file_handlers[ext] = _model_to_bam
+
         tmp = self.default_file_handlers.copy()
         tmp.update(self.file_handlers)
         self.file_handlers = tmp
@@ -625,11 +651,16 @@ class build_apps(setuptools.Command):
             self.icon_objects.get('*', None),
         )
 
-        if icon is not None:
+        if icon is not None or self.prefer_discrete_gpu:
             pef = pefile.PEFile()
             pef.open(runtime, 'r+')
-            pef.add_icon(icon)
-            pef.add_resource_section()
+            if icon is not None:
+                pef.add_icon(icon)
+                pef.add_resource_section()
+            if self.prefer_discrete_gpu:
+                if not pef.rename_export("SymbolPlaceholder___________________", "AmdPowerXpressRequestHighPerformance") or \
+                   not pef.rename_export("SymbolPlaceholder__", "NvOptimusEnablement"):
+                    self.warn("Failed to apply prefer_discrete_gpu, newer target Panda3D version may be required")
             pef.write_changes()
             pef.close()
 
@@ -944,7 +975,8 @@ class build_apps(setuptools.Command):
             freezer = FreezeTool.Freezer(
                 platform=platform,
                 path=path,
-                hiddenImports=self.hidden_imports
+                hiddenImports=self.hidden_imports,
+                optimize=2 if self.strip_docstrings else 1
             )
             freezer.addModule('__main__', filename=mainscript)
             if platform.startswith('android'):
@@ -1617,6 +1649,10 @@ class bdist_apps(setuptools.Command):
         'manylinux_2_24_ppc64': ['gztar'],
         'manylinux_2_24_ppc64le': ['gztar'],
         'manylinux_2_24_s390x': ['gztar'],
+        'manylinux_2_28_x86_64': ['gztar'],
+        'manylinux_2_28_aarch64': ['gztar'],
+        'manylinux_2_28_ppc64le': ['gztar'],
+        'manylinux_2_28_s390x': ['gztar'],
         'android': ['aab'],
         # Everything else defaults to ['zip']
     }
@@ -1710,3 +1746,14 @@ class bdist_apps(setuptools.Command):
                     continue
 
                 self.installer_functions[installer](self, basename, build_dir)
+
+
+def finalize_distribution_options(dist):
+    """Entry point for compatibility with setuptools>=61, see #1394."""
+
+    options = dist.get_option_dict('build_apps')
+    if options.get('gui_apps') or options.get('console_apps'):
+        # Make sure this is set to avoid auto-discovery taking place.
+        if getattr(dist.metadata, 'py_modules', None) is None and \
+           getattr(dist.metadata, 'packages', None) is None:
+            dist.py_modules = []

+ 30 - 0
direct/src/dist/pefile.py

@@ -600,6 +600,36 @@ class PEFile(object):
         if self.res_rva.addr and self.res_rva.size:
             self.resources.unpack_from(self.vmem, self.res_rva.addr)
 
+    def _mark_address_modified(self, rva):
+        for section in self.sections:
+            if rva >= section.vaddr and rva - section.vaddr <= section.size:
+                section.modified = True
+
+    def rename_export(self, old_name, new_name):
+        """ Renames a symbol in the export table. """
+
+        assert len(new_name) <= len(old_name)
+
+        new_name = new_name.ljust(len(old_name) + 1, '\0').encode('ascii')
+
+        start = self.exp_rva.addr
+        expdir = expdirtab(*unpack('<IIHHIIIIIII', self.vmem[start:start+40]))
+        if expdir.nnames == 0 or expdir.ordinals == 0 or expdir.names == 0:
+            return False
+
+        nptr = expdir.names
+        for i in range(expdir.nnames):
+            name_rva, = unpack('<I', self.vmem[nptr:nptr+4])
+            if name_rva != 0:
+                name = _unpack_zstring(self.vmem, name_rva)
+                if name == old_name:
+                    self.vmem[name_rva:name_rva+len(new_name)] = new_name
+                    self._mark_address_modified(name_rva)
+                    return True
+            nptr += 4
+
+        return False
+
     def get_export_address(self, symbol_name):
         """ Finds the virtual address for a named export symbol. """
 

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

@@ -120,7 +120,7 @@ class ConnectionRepository(
 
         self._serverAddress = ''
 
-        if self.config.GetBool('gc-save-all', 1):
+        if self.config.GetBool('gc-save-all', 0):
             # set gc to preserve every object involved in a cycle, even ones that
             # would normally be freed automatically during garbage collect
             # allows us to find and fix these cycles, reducing or eliminating the

+ 3 - 2
direct/src/distributed/cConnectionRepository.cxx

@@ -35,7 +35,7 @@ using std::string;
 const string CConnectionRepository::_overflow_event_name = "CRDatagramOverflow";
 
 #ifndef CPPPARSER
-PStatCollector CConnectionRepository::_update_pcollector("App:Show code:readerPollTask:Update");
+PStatCollector CConnectionRepository::_update_pcollector("App:Tasks:readerPollTask:Update");
 #endif  // CPPPARSER
 
 /**
@@ -580,7 +580,8 @@ flush() {
 
   #ifdef HAVE_OPENSSL
   if (_http_conn) {
-    return _http_conn->flush();
+    _http_conn->flush();
+    return !_http_conn->is_closed();
   }
   #endif  // HAVE_OPENSSL
 

+ 7 - 0
direct/src/filter/CommonFilters.py

@@ -466,6 +466,13 @@ class CommonFilters:
             return task.cont
 
     def setMSAA(self, samples):
+        """Enables multisample anti-aliasing on the render-to-texture buffer.
+        If you enable this, it is recommended to leave any multisample request
+        on the main framebuffer OFF (ie. don't set framebuffer-multisample true
+        in Config.prc), since it would be a waste of resources otherwise.
+
+        .. versionadded:: 1.10.13
+        """
         fullrebuild = "MSAA" not in self.configuration or self.configuration["MSAA"].samples != samples
         newconfig = FilterConfig()
         newconfig.samples = samples

+ 1 - 1
direct/src/interval/Interval.py

@@ -41,7 +41,7 @@ class Interval(DirectObject):
         self.pstats = None
         if __debug__ and TaskManager.taskTimerVerbose:
             self.pname = name.split('-', 1)[0]
-            self.pstats = PStatCollector("App:Show code:ivalLoop:%s" % (self.pname))
+            self.pstats = PStatCollector("App:Tasks:ivalLoop:%s" % (self.pname))
 
         # Set true if the interval should be invoked if it was
         # completely skipped over during initialize or finalize, false

+ 1 - 1
direct/src/interval/MetaInterval.py

@@ -106,7 +106,7 @@ class MetaInterval(CMetaInterval):
         self.pstats = None
         if __debug__ and TaskManager.taskTimerVerbose:
             self.pname = name.split('-', 1)[0]
-            self.pstats = PStatCollector("App:Show code:ivalLoop:%s" % (self.pname))
+            self.pstats = PStatCollector("App:Tasks:ivalLoop:%s" % (self.pname))
 
         self.pythonIvals = []
 

+ 1 - 1
direct/src/interval/cInterval.cxx

@@ -22,7 +22,7 @@
 using std::ostream;
 using std::string;
 
-PStatCollector CInterval::_root_pcollector("App:Show code:ivalLoop");
+PStatCollector CInterval::_root_pcollector("App:Tasks:ivalLoop");
 TypeHandle CInterval::_type_handle;
 
 static inline string

+ 170 - 72
direct/src/motiontrail/MotionTrail.py

@@ -47,6 +47,30 @@ class MotionTrailFrame:
 
 
 class MotionTrail(NodePath, DirectObject):
+    """Generates smooth geometry-based motion trails behind a moving object.
+
+    To use this class, first define the shape of the cross-section polygon that
+    is to be extruded along the motion trail by calling `add_vertex()` and
+    `set_vertex_color()`.  When this is done, call `update_vertices()`.
+
+    To generate the motion trail, either call `register_motion_trail()`
+    to have Panda update it automatically, or periodically call the method
+    `update_motion_trail()` with the current time and the new transform.
+
+    The duration of the sample history is specified by `time_window`.  A larger
+    time window creates longer motion trails (given constant speed).  Samples
+    that are no longer within the time window are automatically discarded.
+
+    The `use_nurbs` option can be used to create smooth interpolated curves
+    from the samples.  This option is useful for animations that lack sampling
+    to begin with, animations that move very quickly, or low frame rates, or if
+    `sampling_time` is used to artificially slow down the update frequency.
+
+    By default, the optimized C++ implementation (provided by `.CMotionTrail`)
+    is used to generate the motion trails.  If for some reason you want to use
+    the pure-Python implementation instead, set `want-python-motion-trails` to
+    true in Config.prc.
+    """
 
     notify = directNotify.newCategory("MotionTrail")
 
@@ -58,9 +82,16 @@ class MotionTrail(NodePath, DirectObject):
 
     @classmethod
     def setGlobalEnable(cls, enable):
+        """Set this to False to have the task stop updating all motion trails.
+        This does not prevent updating them manually using the
+        `update_motion_trail()` method.
+        """
         cls.global_enable = enable
 
     def __init__(self, name, parent_node_path):
+        """Creates the motion trail with the given name and parents it to the
+        given root node.
+        """
         NodePath.__init__(self, name)
 
         # required initialization
@@ -91,8 +122,17 @@ class MotionTrail(NodePath, DirectObject):
         # default options
         self.continuous_motion_trail = True
         self.color_scale = 1.0
+
+        #: How long the time window is for which the trail is computed.  Can be
+        #: increased to obtain a longer trail, decreased for a shorter trail.
         self.time_window = 1.0
+
+        #: How often the trail updates, in seconds.  The default is 0.0, which
+        #: has the trail updated every frame for the smoothest result.  Higher
+        #: values will generate a choppier trail.  The `use_nurbs` option can
+        #: compensate partially for this choppiness, however.
         self.sampling_time = 0.0
+
         self.square_t = True
 
 #        self.task_transform = False
@@ -100,7 +140,12 @@ class MotionTrail(NodePath, DirectObject):
 
         # node path states
         self.reparentTo(parent_node_path)
+
+        #: A `.GeomNode` object containing the generated geometry.  By default
+        #: parented to the MotionTrail itself, but can be reparented elsewhere
+        #: if necessary.
         self.geom_node = GeomNode("motion_trail")
+        self.geom_node.setBoundsType(BoundingVolume.BT_box)
         self.geom_node_path = self.attachNewNode(self.geom_node)
         node_path = self.geom_node_path
 
@@ -127,10 +172,13 @@ class MotionTrail(NodePath, DirectObject):
 
             MotionTrail.task_added = True
 
-
         self.relative_to_render = False
 
+        #: Set this to True to use a NURBS curve to generate a smooth trail,
+        #: even if the underlying animation or movement is janky.
         self.use_nurbs = False
+
+        #: This can be changed to fine-tune the resolution of the NURBS curve.
         self.resolution_distance = 0.5
 
         self.cmotion_trail = CMotionTrail()
@@ -142,14 +190,13 @@ class MotionTrail(NodePath, DirectObject):
         else:
             self.use_python_version = False
 
-        return
-
     def delete(self):
+        """Completely cleans up the motion trail object.
+        """
         self.reset_motion_trail()
         self.reset_motion_trail_geometry()
         self.cmotion_trail.resetVertexList()
         self.removeNode()
-        return
 
     def print_matrix(self, matrix):
         separator = ' '
@@ -206,11 +253,32 @@ class MotionTrail(NodePath, DirectObject):
 
         return Task.cont
 
-    def add_vertex(self, vertex_id, vertex_function, context):
-        motion_trail_vertex = MotionTrailVertex(vertex_id, vertex_function, context)
+    def add_vertex(self, vertex_id, vertex_function=None, context=None, *,
+                   start_color=(1.0, 1.0, 1.0, 1.0), end_color=(0.0, 0.0, 0.0, 1.0)):
+        """This must be called initially to define the polygon that forms the
+        cross-section of the generated motion trail geometry.  The first
+        argument is a user-defined vertex identifier, the second is a function
+        that will be called with three parameters that should return the
+        position of the vertex as a `.Vec4` object, and the third is an
+        arbitrary context object that is passed as last argument to the
+        provided function.
+
+        After calling this, you must call `update_vertices()` before the
+        changes will fully take effect.
+
+        As of Panda3D 1.10.13, you may alternatively simply pass in a single
+        argument containing the vertex position as a `.Vec4` or `.Point3`.
+        """
+        if vertex_function is None:
+            motion_trail_vertex = MotionTrailVertex(None, None, context)
+            motion_trail_vertex.vertex = Vec4(vertex_id)
+        else:
+            motion_trail_vertex = MotionTrailVertex(vertex_id, vertex_function, context)
+        motion_trail_vertex.start_color = Vec4(start_color)
+        motion_trail_vertex.end_color = Vec4(end_color)
         total_vertices = len(self.vertex_list)
 
-        self.vertex_list [total_vertices : total_vertices] = [motion_trail_vertex]
+        self.vertex_list[total_vertices : total_vertices] = [motion_trail_vertex]
 
         self.total_vertices = len(self.vertex_list)
 
@@ -219,15 +287,24 @@ class MotionTrail(NodePath, DirectObject):
         return motion_trail_vertex
 
     def set_vertex_color(self, vertex_id, start_color, end_color):
+        """Sets the start and end color of the vertex with the given index,
+        which must have been previously added by `add_vertex()`.  The motion
+        trail will contain a smooth gradient between these colors.  By default,
+        the motion trail fades from white to black (which, with the default
+        additive blending mode, makes it show up as a purely white motion trail
+        that fades out towards the end).
+        """
         if vertex_id >= 0 and vertex_id < self.total_vertices:
-            motion_trail_vertex = self.vertex_list [vertex_id]
+            motion_trail_vertex = self.vertex_list[vertex_id]
             motion_trail_vertex.start_color = start_color
             motion_trail_vertex.end_color = end_color
 
         self.modified_vertices = True
-        return
 
     def set_texture(self, texture):
+        """Defines the texture that should be applied to the trail geometry.
+        This also enables generation of UV coordinates.
+        """
         self.texture = texture
         if texture:
             self.geom_node_path.setTexture(texture)
@@ -237,17 +314,21 @@ class MotionTrail(NodePath, DirectObject):
             self.geom_node_path.clearTexture()
 
         self.modified_vertices = True
-        return
 
     def update_vertices(self):
+        """This must be called after the list of vertices defining the
+        cross-section shape of the motion trail has been defined by
+        `add_vertex()` and `set_vertex_color()`.
+        """
         total_vertices = len(self.vertex_list)
 
         self.total_vertices = total_vertices
         if total_vertices >= 2:
             vertex_index = 0
             while vertex_index < total_vertices:
-                motion_trail_vertex = self.vertex_list [vertex_index]
-                motion_trail_vertex.vertex = motion_trail_vertex.vertex_function(motion_trail_vertex, motion_trail_vertex.vertex_id, motion_trail_vertex.context)
+                motion_trail_vertex = self.vertex_list[vertex_index]
+                if motion_trail_vertex.vertex_function is not None:
+                    motion_trail_vertex.vertex = motion_trail_vertex.vertex_function(motion_trail_vertex, motion_trail_vertex.vertex_id, motion_trail_vertex.context)
                 vertex_index += 1
 
             # calculate v coordinate
@@ -257,7 +338,7 @@ class MotionTrail(NodePath, DirectObject):
             float_total_vertices = 0.0
             float_total_vertices = total_vertices - 1.0
             while vertex_index < total_vertices:
-                motion_trail_vertex = self.vertex_list [vertex_index]
+                motion_trail_vertex = self.vertex_list[vertex_index]
                 motion_trail_vertex.v = float_vertex_index / float_total_vertices
                 vertex_index += 1
                 float_vertex_index += 1.0
@@ -265,7 +346,6 @@ class MotionTrail(NodePath, DirectObject):
 #                print "motion_trail_vertex.v", motion_trail_vertex.v
 
         self.modified_vertices = True
-        return
 
     def transferVertices(self):
 
@@ -278,22 +358,24 @@ class MotionTrail(NodePath, DirectObject):
             vertex_index = 0
             total_vertices = len(self.vertex_list)
             while vertex_index < total_vertices:
-                motion_trail_vertex = self.vertex_list [vertex_index]
+                motion_trail_vertex = self.vertex_list[vertex_index]
                 self.cmotion_trail.addVertex(motion_trail_vertex.vertex, motion_trail_vertex.start_color, motion_trail_vertex.end_color, motion_trail_vertex.v)
                 vertex_index += 1
 
             self.modified_vertices = False
 
-        return
-
     def register_motion_trail(self):
+        """Adds this motion trail to the list of trails that are updated
+        automatically every frame.  Be careful not to call this twice.
+        """
         MotionTrail.motion_trail_list = MotionTrail.motion_trail_list + [self]
-        return
 
     def unregister_motion_trail(self):
+        """Removes this motion trail from the list of trails that are updated
+        automatically every frame.  If it is not on that list, does nothing.
+        """
         if self in MotionTrail.motion_trail_list:
             MotionTrail.motion_trail_list.remove(self)
-        return
 
     def begin_geometry(self):
         self.vertex_index = 0
@@ -314,21 +396,21 @@ class MotionTrail(NodePath, DirectObject):
 
     def add_geometry_quad(self, v0, v1, v2, v3, c0, c1, c2, c3, t0, t1, t2, t3):
 
-        self.vertex_writer.addData3f(v0 [0], v0 [1], v0 [2])
-        self.vertex_writer.addData3f(v1 [0], v1 [1], v1 [2])
-        self.vertex_writer.addData3f(v2 [0], v2 [1], v2 [2])
-        self.vertex_writer.addData3f(v3 [0], v3 [1], v3 [2])
+        self.vertex_writer.addData3(v0[0], v0[1], v0[2])
+        self.vertex_writer.addData3(v1[0], v1[1], v1[2])
+        self.vertex_writer.addData3(v2[0], v2[1], v2[2])
+        self.vertex_writer.addData3(v3[0], v3[1], v3[2])
 
-        self.color_writer.addData4f(c0)
-        self.color_writer.addData4f(c1)
-        self.color_writer.addData4f(c2)
-        self.color_writer.addData4f(c3)
+        self.color_writer.addData4(c0)
+        self.color_writer.addData4(c1)
+        self.color_writer.addData4(c2)
+        self.color_writer.addData4(c3)
 
         if self.texture is not None:
-            self.texture_writer.addData2f(t0)
-            self.texture_writer.addData2f(t1)
-            self.texture_writer.addData2f(t2)
-            self.texture_writer.addData2f(t3)
+            self.texture_writer.addData2(t0)
+            self.texture_writer.addData2(t1)
+            self.texture_writer.addData2(t2)
+            self.texture_writer.addData2(t3)
 
         vertex_index = self.vertex_index
 
@@ -352,7 +434,10 @@ class MotionTrail(NodePath, DirectObject):
         self.geom_node.addGeom(self.geometry)
 
     def check_for_update(self, current_time):
-
+        """Returns true if the motion trail is overdue for an update based on
+        the configured `sampling_time` (by default 0.0 to update continuously),
+        and is not currently paused.
+        """
         state = False
         if (current_time - self.last_update_time) >= self.sampling_time:
             state = True
@@ -365,9 +450,12 @@ class MotionTrail(NodePath, DirectObject):
         return state
 
     def update_motion_trail(self, current_time, transform):
-
+        """If the trail is overdue for an update based on the given time in
+        seconds, updates it, extracting the new object position from the given
+        transform matrix.
+        """
         if len(self.frame_list) >= 1:
-            if transform == self.frame_list [0].transform:
+            if transform == self.frame_list[0].transform:
                 # ignore duplicate transform updates
                 return
 
@@ -378,7 +466,7 @@ class MotionTrail(NodePath, DirectObject):
                 elapsed_time = current_time - self.fade_start_time
 
                 if elapsed_time < 0.0:
-                    print("elapsed_time < 0: %f" %(elapsed_time))
+                    print("elapsed_time < 0: %f" % (elapsed_time))
                     elapsed_time = 0.0
 
                 if elapsed_time < self.fade_time:
@@ -397,13 +485,13 @@ class MotionTrail(NodePath, DirectObject):
             last_frame_index = len(self.frame_list) - 1
 
             while index <= last_frame_index:
-                motion_trail_frame = self.frame_list [last_frame_index - index]
+                motion_trail_frame = self.frame_list[last_frame_index - index]
                 if motion_trail_frame.time >= minimum_time:
                     break
                 index += 1
 
             if index > 0:
-                self.frame_list [last_frame_index - index: last_frame_index + 1] = []
+                self.frame_list[last_frame_index - index: last_frame_index + 1] = []
 
             # add new frame to beginning of list
             motion_trail_frame = MotionTrailFrame(current_time, transform)
@@ -416,15 +504,14 @@ class MotionTrail(NodePath, DirectObject):
             #
             #index = 0
             #while index < total_frames:
-            #    motion_trail_frame = self.frame_list [index]
+            #    motion_trail_frame = self.frame_list[index]
             #    print("frame time", index, motion_trail_frame.time)
             #    index += 1
 
-            if (total_frames >= 2) and(self.total_vertices >= 2):
-
+            if total_frames >= 2 and self.total_vertices >= 2:
                 self.begin_geometry()
                 total_segments = total_frames - 1
-                last_motion_trail_frame = self.frame_list [total_segments]
+                last_motion_trail_frame = self.frame_list[total_segments]
                 minimum_time = last_motion_trail_frame.time
                 delta_time = current_time - minimum_time
 
@@ -432,7 +519,7 @@ class MotionTrail(NodePath, DirectObject):
                     inverse_matrix = Mat4(transform)
                     inverse_matrix.invertInPlace()
 
-                if self.use_nurbs and(total_frames >= 5):
+                if self.use_nurbs and total_frames >= 5:
 
                     total_distance = 0.0
                     vector = Vec3()
@@ -452,10 +539,10 @@ class MotionTrail(NodePath, DirectObject):
                     # add vertices to each NurbsCurveEvaluator
                     segment_index = 0
                     while segment_index < total_segments:
-                        motion_trail_frame_start = self.frame_list [segment_index]
-                        motion_trail_frame_end = self.frame_list [segment_index + 1]
+                        motion_trail_frame_start = self.frame_list[segment_index]
+                        motion_trail_frame_end = self.frame_list[segment_index + 1]
 
-                        vertex_segement_index = 0
+                        vertex_segment_index = 0
 
                         if self.calculate_relative_matrix:
                             start_transform = Mat4()
@@ -468,34 +555,34 @@ class MotionTrail(NodePath, DirectObject):
                             start_transform = motion_trail_frame_start.transform
                             end_transform = motion_trail_frame_end.transform
 
-                        motion_trail_vertex_start = self.vertex_list [0]
+                        motion_trail_vertex_start = self.vertex_list[0]
 
                         v0 = start_transform.xform(motion_trail_vertex_start.vertex)
                         v2 = end_transform.xform(motion_trail_vertex_start.vertex)
 
-                        nurbs_curve_evaluator = nurbs_curve_evaluator_list [vertex_segement_index]
+                        nurbs_curve_evaluator = nurbs_curve_evaluator_list [vertex_segment_index]
 
                         nurbs_curve_evaluator.setVertex(segment_index, v0)
 
-                        while vertex_segement_index < total_vertex_segments:
+                        while vertex_segment_index < total_vertex_segments:
 
-                            motion_trail_vertex_start = self.vertex_list [vertex_segement_index]
-                            motion_trail_vertex_end = self.vertex_list [vertex_segement_index + 1]
+                            motion_trail_vertex_start = self.vertex_list[vertex_segment_index]
+                            motion_trail_vertex_end = self.vertex_list[vertex_segment_index + 1]
 
                             v1 = start_transform.xform(motion_trail_vertex_end.vertex)
                             v3 = end_transform.xform(motion_trail_vertex_end.vertex)
 
-                            nurbs_curve_evaluator = nurbs_curve_evaluator_list [vertex_segement_index + 1]
+                            nurbs_curve_evaluator = nurbs_curve_evaluator_list [vertex_segment_index + 1]
 
                             nurbs_curve_evaluator.setVertex(segment_index, v1)
 
-                            if vertex_segement_index == (total_vertex_segments - 1):
+                            if vertex_segment_index == (total_vertex_segments - 1):
                                 v = v1 - v3
                                 vector.set(v[0], v[1], v[2])
                                 distance = vector.length()
                                 total_distance += distance
 
-                            vertex_segement_index += 1
+                            vertex_segment_index += 1
 
                         segment_index += 1
 
@@ -531,7 +618,7 @@ class MotionTrail(NodePath, DirectObject):
                     curve_segment_index = 0.0
                     while curve_segment_index < total_curve_segments:
 
-                        vertex_segement_index = 0
+                        vertex_segment_index = 0
 
                         st = curve_segment_index / total_curve_segments
                         et = (curve_segment_index + 1.0) / total_curve_segments
@@ -545,7 +632,7 @@ class MotionTrail(NodePath, DirectObject):
                             start_t *= start_t
                             end_t *= end_t
 
-                        motion_trail_vertex_start = self.vertex_list [0]
+                        motion_trail_vertex_start = self.vertex_list[0]
 
                         vertex_start_color = motion_trail_vertex_start.end_color + (motion_trail_vertex_start.start_color - motion_trail_vertex_start.end_color)
                         color_start_t = color_scale * start_t
@@ -556,13 +643,13 @@ class MotionTrail(NodePath, DirectObject):
                         t0 = Vec2(one_minus_x(st), motion_trail_vertex_start.v)
                         t2 = Vec2(one_minus_x(et), motion_trail_vertex_start.v)
 
-                        while vertex_segement_index < total_vertex_segments:
+                        while vertex_segment_index < total_vertex_segments:
 
-                            motion_trail_vertex_start = self.vertex_list [vertex_segement_index]
-                            motion_trail_vertex_end = self.vertex_list [vertex_segement_index + 1]
+                            motion_trail_vertex_start = self.vertex_list[vertex_segment_index]
+                            motion_trail_vertex_end = self.vertex_list[vertex_segment_index + 1]
 
-                            start_nurbs_curve_result = nurbs_curve_result_list [vertex_segement_index]
-                            end_nurbs_curve_result = nurbs_curve_result_list [vertex_segement_index + 1]
+                            start_nurbs_curve_result = nurbs_curve_result_list [vertex_segment_index]
+                            end_nurbs_curve_result = nurbs_curve_result_list [vertex_segment_index + 1]
 
                             start_nurbs_start_t = start_nurbs_curve_result.getStartT()
                             start_nurbs_end_t = start_nurbs_curve_result.getEndT()
@@ -597,17 +684,15 @@ class MotionTrail(NodePath, DirectObject):
                             t0 = t1
                             t2 = t3
 
-                            vertex_segement_index += 1
+                            vertex_segment_index += 1
 
                         curve_segment_index += 1.0
 
-
                 else:
-
                     segment_index = 0
                     while segment_index < total_segments:
-                        motion_trail_frame_start = self.frame_list [segment_index]
-                        motion_trail_frame_end = self.frame_list [segment_index + 1]
+                        motion_trail_frame_start = self.frame_list[segment_index]
+                        motion_trail_frame_end = self.frame_list[segment_index + 1]
 
                         start_t = (motion_trail_frame_start.time - minimum_time) / delta_time
                         end_t = (motion_trail_frame_end.time - minimum_time) / delta_time
@@ -619,7 +704,7 @@ class MotionTrail(NodePath, DirectObject):
                             start_t *= start_t
                             end_t *= end_t
 
-                        vertex_segement_index = 0
+                        vertex_segment_index = 0
                         total_vertex_segments = self.total_vertices - 1
 
                         if self.calculate_relative_matrix:
@@ -631,7 +716,7 @@ class MotionTrail(NodePath, DirectObject):
                             start_transform = motion_trail_frame_start.transform
                             end_transform = motion_trail_frame_end.transform
 
-                        motion_trail_vertex_start = self.vertex_list [0]
+                        motion_trail_vertex_start = self.vertex_list[0]
 
                         v0 = start_transform.xform(motion_trail_vertex_start.vertex)
                         v2 = end_transform.xform(motion_trail_vertex_start.vertex)
@@ -645,10 +730,10 @@ class MotionTrail(NodePath, DirectObject):
                         t0 = Vec2(st, motion_trail_vertex_start.v)
                         t2 = Vec2(et, motion_trail_vertex_start.v)
 
-                        while vertex_segement_index < total_vertex_segments:
+                        while vertex_segment_index < total_vertex_segments:
 
-                            motion_trail_vertex_start = self.vertex_list [vertex_segement_index]
-                            motion_trail_vertex_end = self.vertex_list [vertex_segement_index + 1]
+                            motion_trail_vertex_start = self.vertex_list[vertex_segment_index]
+                            motion_trail_vertex_end = self.vertex_list[vertex_segment_index + 1]
 
                             v1 = start_transform.xform(motion_trail_vertex_end.vertex)
                             v3 = end_transform.xform(motion_trail_vertex_end.vertex)
@@ -675,24 +760,37 @@ class MotionTrail(NodePath, DirectObject):
                             t0 = t1
                             t2 = t3
 
-                            vertex_segement_index += 1
+                            vertex_segment_index += 1
 
                         segment_index += 1
 
                 self.end_geometry()
 
     def enable_motion_trail(self, enable):
+        """Sets whether the motion trail is currently enabled.  Every motion
+        trail starts off as being enabled, passing False to this method prevents
+        it from being updated.
+        """
         self.enable = enable
 
     def reset_motion_trail(self):
+        """Call this to have the motion trail restart from nothing on the next
+        update.
+        """
         self.frame_list = []
         self.cmotion_trail.reset()
 
     def reset_motion_trail_geometry(self):
+        """Destroys the currently generated motion trail geometry immediately.
+        However, it will be fully regenerated on the next call to update, see
+        `reset_motion_trail()` to prevent this.
+        """
         if self.geom_node is not None:
             self.geom_node.removeAllGeoms()
 
     def attach_motion_trail(self):
+        """Alias of `reset_motion_trail()`.
+        """
         self.reset_motion_trail()
 
     def begin_motion_trail(self):
@@ -733,7 +831,7 @@ class MotionTrail(NodePath, DirectObject):
             frame_index = 0
             total_frames = len(self.frame_list)
             while frame_index < total_frames:
-                motion_trail_frame = self.frame_list [frame_index]
+                motion_trail_frame = self.frame_list[frame_index]
                 motion_trail_frame.time += delta_time
                 frame_index += 1
 

Разлика између датотеке није приказан због своје велике величине
+ 285 - 434
direct/src/motiontrail/cMotionTrail.cxx


+ 37 - 39
direct/src/motiontrail/cMotionTrail.h

@@ -21,13 +21,11 @@
 #include "geomVertexWriter.h"
 #include "geomTriangles.h"
 #include "luse.h"
-#include "memoryBase.h"
 #include "nurbsCurveEvaluator.h"
 #include "plist.h"
 #include "epvector.h"
 
-class CMotionTrailVertex : public MemoryBase {
-public:
+struct CMotionTrailVertex : public MemoryBase {
   LPoint4 _vertex;
   LVecBase4 _start_color;
   LVecBase4 _end_color;
@@ -36,9 +34,8 @@ public:
   PT(NurbsCurveEvaluator) _nurbs_curve_evaluator;
 };
 
-class CMotionTrailFrame {
-public:
-  UnalignedLMatrix4 _transform;
+struct CMotionTrailFrame : public MemoryBase {
+  LMatrix4 _transform;
   PN_stdfloat _time;
 };
 
@@ -72,8 +69,8 @@ public:
  */
 class EXPCL_DIRECT_MOTIONTRAIL CMotionTrail : public TypedReferenceCount {
 PUBLISHED:
-  CMotionTrail();
-  ~CMotionTrail();
+  CMotionTrail() = default;
+  ~CMotionTrail() = default;
 
   void reset();
   void reset_vertex_list();
@@ -81,64 +78,66 @@ PUBLISHED:
   void enable(bool enable);
 
   void set_geom_node(GeomNode *geom_node);
-  void add_vertex(LVector4 *vertex, LVector4 *start_color, LVector4 *end_color, PN_stdfloat v);
+  void add_vertex(const LVector4 &vertex, const LVector4 &start_color,
+                  const LVector4 &end_color, PN_stdfloat v);
 
-  void set_parameters(PN_stdfloat sampling_time, PN_stdfloat time_window, bool use_texture, bool calculate_relative_matrix, bool use_nurbs, PN_stdfloat resolution_distance);
+  void set_parameters(PN_stdfloat sampling_time, PN_stdfloat time_window,
+                      bool use_texture, bool calculate_relative_matrix,
+                      bool use_nurbs, PN_stdfloat resolution_distance);
 
   int check_for_update(PN_stdfloat current_time);
-  void update_motion_trail(PN_stdfloat current_time, LMatrix4 *transform);
+  void update_motion_trail(PN_stdfloat current_time, const LMatrix4 &transform);
 
 public:
+  void begin_geometry(int num_quads);
+  void add_geometry_quad(
+    const LPoint3 &v0, const LPoint3 &v1, const LPoint3 &v2, const LPoint3 &v3,
+    const LVector4 &c0, const LVector4 &c1, const LVector4 &c2, const LVector4 &c3,
+    const LVector2 &t0, const LVector2 &t1, const LVector2 &t2, const LVector2 &t3);
+  void end_geometry(const LPoint3 &min_vertex, const LPoint3 &max_vertex);
 
-  void begin_geometry();
-  void add_geometry_quad(LVector3 &v0, LVector3 &v1, LVector3 &v2, LVector3 &v3, LVector4 &c0, LVector4 &c1, LVector4 &c2, LVector4 &c3, LVector2 &t0, LVector2 &t1, LVector2 &t2, LVector2 &t3);
-  void add_geometry_quad(LVector4 &v0, LVector4 &v1, LVector4 &v2, LVector4 &v3, LVector4 &c0, LVector4 &c1, LVector4 &c2, LVector4 &c3, LVector2 &t0, LVector2 &t1, LVector2 &t2, LVector2 &t3);
-  void end_geometry();
-
-  int _active;
-  int _enable;
+  bool _active = true;
+  bool _enable = true;
 
-  int _pause;
-  PN_stdfloat _pause_time;
+  bool _pause = false;
+  PN_stdfloat _pause_time = 0.0f;
 
-  int _fade;
-  int _fade_end;
-  PN_stdfloat _fade_time;
-  PN_stdfloat _fade_start_time;
-  PN_stdfloat _fade_color_scale;
+  bool _fade = false;
+  bool _fade_end = false;
+  PN_stdfloat _fade_time = 0.0f;
+  PN_stdfloat _fade_start_time = 0.0f;
+  PN_stdfloat _fade_color_scale = 1.0f;
 
-  PN_stdfloat _last_update_time;
+  PN_stdfloat _last_update_time = 0.0f;
 
   typedef epvector<CMotionTrailVertex> VertexList;
   VertexList _vertex_list;
-  typedef plist<CMotionTrailFrame> FrameList;
+  typedef pdeque<CMotionTrailFrame> FrameList;
   FrameList _frame_list;
+  PN_stdfloat _vertex_bounds_radius = 0.0f;
 
   // parameters
-  PN_stdfloat _color_scale;
-  PN_stdfloat _sampling_time;
-  PN_stdfloat _time_window;
-  bool _square_t;
-  bool _use_texture;
-  int _calculate_relative_matrix;
+  PN_stdfloat _color_scale = 1.0f;
+  PN_stdfloat _sampling_time = 0.0f;
+  PN_stdfloat _time_window = 1.0f;
+  bool _square_t = true;
+  bool _use_texture = false;
+  bool _calculate_relative_matrix = false;
 
   // nurbs parameters
-  bool _use_nurbs;
-  PN_stdfloat _resolution_distance;
+  bool _use_nurbs = false;
+  PN_stdfloat _resolution_distance = 0.5f;
 
   // geom
   PT(GeomNode) _geom_node;
 
   // real-time data
-  int _vertex_index;
   PT(GeomVertexData) _vertex_data;
   GeomVertexWriter _vertex_writer;
   GeomVertexWriter _color_writer;
   GeomVertexWriter _texture_writer;
   PT(GeomTriangles) _triangles;
 
-  CMotionTrailVertex *_vertex_array;
-
 public:
   static TypeHandle get_class_type() {
     return _type_handle;
@@ -155,7 +154,6 @@ public:
 
 private:
   static TypeHandle _type_handle;
-
 };
 
 #endif

+ 26 - 6
direct/src/showbase/ContainerLeakDetector.py

@@ -11,12 +11,15 @@ import weakref
 import random
 import builtins
 
-deadEndTypes = (bool, types.BuiltinFunctionType,
-                types.BuiltinMethodType, complex,
-                float, int,
-                type(None), type(NotImplemented),
-                type, types.CodeType, types.FunctionType,
-                bytes, str, tuple)
+deadEndTypes = frozenset((
+    types.BuiltinFunctionType, types.BuiltinMethodType,
+    types.CodeType, types.FunctionType,
+    types.GeneratorType, types.CoroutineType,
+    types.AsyncGeneratorType,
+    bool, complex, float, int, type,
+    bytes, str, list, tuple,
+    type(None), type(NotImplemented)
+))
 
 
 def _createContainerLeak():
@@ -547,6 +550,23 @@ class FindContainers(Job):
                 # if we hit a dead end, start over from another container
                 curObjRef = None
 
+                # types.CellType was added in Python 3.8
+                if sys.version_info >= (3, 8) and type(curObj) is types.CellType:
+                    child = curObj.cell_contents
+                    hasLength = self._hasLength(child)
+                    notDeadEnd = not self._isDeadEnd(child)
+                    if hasLength or notDeadEnd:
+                        objRef = ObjectRef(Indirection(evalStr='.cell_contents'),
+                                           id(child), parentObjRef)
+                        yield None
+                        if hasLength:
+                            for i in self._addContainerGen(child, objRef):
+                                yield None
+                        if notDeadEnd:
+                            self._addDiscoveredStartRef(child, objRef)
+                            curObjRef = objRef
+                    continue
+
                 if hasattr(curObj, '__dict__'):
                     child = curObj.__dict__
                     hasLength = self._hasLength(child)

+ 28 - 14
direct/src/showbase/ContainerReport.py

@@ -3,7 +3,10 @@ from direct.showbase.PythonUtil import Queue, invertDictLossless
 from direct.showbase.PythonUtil import safeRepr
 from direct.showbase.Job import Job
 from direct.showbase.JobManagerGlobal import jobMgr
+from direct.showbase.ContainerLeakDetector import deadEndTypes
 import types
+import sys
+import io
 
 
 class ContainerReport(Job):
@@ -89,14 +92,6 @@ class ContainerReport(Job):
             if isinstance(parentObj, (str, bytes)):
                 continue
 
-            if type(parentObj) in (types.ModuleType, types.InstanceType):
-                child = parentObj.__dict__
-                if self._examine(child):
-                    assert self._queue.back() is child
-                    self._instanceDictIds.add(id(child))
-                    self._id2pathStr[id(child)] = str(self._id2pathStr[id(parentObj)])
-                continue
-
             if isinstance(parentObj, dict):
                 key = None
                 attr = None
@@ -126,7 +121,25 @@ class ContainerReport(Job):
                 del attr
                 continue
 
-            if type(parentObj) is not types.FileType:
+            # types.CellType was added in Python 3.8
+            if sys.version_info >= (3, 8) and type(parentObj) is types.CellType:
+                child = parentObj.cell_contents
+                if self._examine(child):
+                    assert (self._queue.back() is child)
+                    self._instanceDictIds.add(id(child))
+                    self._id2pathStr[id(child)] = str(self._id2pathStr[id(parentObj)]) + '.cell_contents'
+                continue
+
+            if hasattr(parentObj, '__dict__'):
+                # Instance of a class
+                child = parentObj.__dict__
+                if self._examine(child):
+                    assert (self._queue.back() is child)
+                    self._instanceDictIds.add(id(child))
+                    self._id2pathStr[id(child)] = str(self._id2pathStr[id(parentObj)])
+                continue
+
+            if not isinstance(parentObj, io.TextIOWrapper):
                 try:
                     itr = iter(parentObj)
                 except:
@@ -161,7 +174,10 @@ class ContainerReport(Job):
                 childName = None
                 child = None
                 for childName in childNames:
-                    child = getattr(parentObj, childName)
+                    try:
+                        child = getattr(parentObj, childName)
+                    except:
+                        continue
                     if id(child) not in self._visitedIds:
                         self._visitedIds.add(id(child))
                         if self._examine(child):
@@ -198,9 +214,7 @@ class ContainerReport(Job):
             self._type2id2len[type(obj)][objId] = length
     def _examine(self, obj):
         # return False if it's an object that can't contain or lead to other objects
-        if type(obj) in (bool, types.BuiltinFunctionType, types.BuiltinMethodType,
-                         complex, float, int, type(None), type(NotImplemented),
-                         type, types.CodeType, types.FunctionType):
+        if type(obj) in deadEndTypes:
             return False
         # if it's an internal object, ignore it
         if id(obj) in ContainerReport.PrivateIds:
@@ -243,7 +257,7 @@ class ContainerReport(Job):
             for i in self._outputType(type, **kArgs):
                 yield None
         otherTypes = list(set(self._type2id2len.keys()).difference(set(initialTypes)))
-        otherTypes.sort()
+        otherTypes.sort(key=lambda obj: obj.__name__)
         for type in otherTypes:
             for i in self._outputType(type, **kArgs):
                 yield None

+ 5 - 2
direct/src/showbase/EventManager.py

@@ -137,11 +137,11 @@ class EventManager:
             hyphen = name.find('-')
             if hyphen >= 0:
                 name = name[0:hyphen]
-            pstatCollector = PStatCollector('App:Show code:eventManager:' + name)
+            pstatCollector = PStatCollector('App:Tasks:eventManager:' + name)
             pstatCollector.start()
             if self.eventHandler:
                 cppPstatCollector = PStatCollector(
-                    'App:Show code:eventManager:' + name + ':C++')
+                    'App:Tasks:eventManager:' + name + ':C++')
 
             messenger.send(eventName, paramList)
 
@@ -180,3 +180,6 @@ class EventManager:
         # since the task removal itself might also fire off an event.
         if self.eventQueue is not None:
             self.eventQueue.clear()
+
+    do_events = doEvents
+    process_event = processEvent

+ 6 - 7
direct/src/showbase/GarbageReport.py

@@ -8,9 +8,8 @@ from direct.showbase.PythonUtil import itype, deeptype, fastRepr
 from direct.showbase.Job import Job
 from direct.showbase.JobManagerGlobal import jobMgr
 from direct.showbase.MessengerGlobal import messenger
-import direct.showbase.DConfig as config
+from panda3d.core import ConfigVariableBool
 import gc
-import types
 
 GarbageCycleCountAnnounceEvent = 'announceGarbageCycleDesc2num'
 
@@ -213,7 +212,7 @@ class GarbageReport(Job):
                     startIndex = 0
                     # + 1 to include a reference back to the first object
                     endIndex = numObjs + 1
-                    if type(objs[-1]) is types.InstanceType and type(objs[0]) is dict:
+                    if type(objs[0]) is dict and hasattr(objs[-1], '__dict__'):
                         startIndex -= 1
                         endIndex -= 1
 
@@ -222,7 +221,7 @@ class GarbageReport(Job):
                             numToSkip -= 1
                             continue
                         obj = objs[index]
-                        if type(obj) is types.InstanceType:
+                        if hasattr(obj, '__dict__'):
                             if not objAlreadyRepresented:
                                 cycleBySyntax += '%s' % obj.__class__.__name__
                             cycleBySyntax += '.'
@@ -307,7 +306,7 @@ class GarbageReport(Job):
             while n > 0:
                 yield None
                 digits += 1
-                n /= 10
+                n = n // 10
             digits = digits
             format = '%0' + '%s' % digits + 'i:%s \t%s'
 
@@ -562,7 +561,7 @@ class _CFGLGlobals:
 def checkForGarbageLeaks():
     gc.collect()
     numGarbage = len(gc.garbage)
-    if numGarbage > 0 and config.GetBool('auto-garbage-logging', 0):
+    if numGarbage > 0 and ConfigVariableBool('auto-garbage-logging', False):
         if numGarbage != _CFGLGlobals.LastNumGarbage:
             print("")
             gr = GarbageReport('found garbage', threaded=False, collect=False)
@@ -572,7 +571,7 @@ def checkForGarbageLeaks():
             messenger.send(GarbageCycleCountAnnounceEvent, [gr.getDesc2numDict()])
             gr.destroy()
         notify = directNotify.newCategory("GarbageDetect")
-        if config.GetBool('allow-garbage-cycles', 1):
+        if ConfigVariableBool('allow-garbage-cycles', True):
             func = notify.warning
         else:
             func = notify.error

+ 1 - 1
direct/src/showbase/Job.py

@@ -35,7 +35,7 @@ class Job(DirectObject):
         self._priority = Job.Priorities.Normal
         self._finished = False
         if __debug__:
-            self._pstats = PStatCollector("App:Show code:jobManager:%s" % self._name)
+            self._pstats = PStatCollector("App:Tasks:jobManager:%s" % self._name)
 
     def destroy(self):
         del self._name

+ 46 - 35
direct/src/showbase/ShowBase.py

@@ -1151,7 +1151,7 @@ class ShowBase(DirectObject.DirectObject):
         Creates the render scene graph, the primary scene graph for
         rendering 3-d geometry.
         """
-        ## This is the root of the 3-D scene graph.
+        #: This is the root of the 3-D scene graph.
         self.render = NodePath('render')
         self.render.setAttrib(RescaleNormalAttrib.makeDefault())
 
@@ -1170,7 +1170,7 @@ class ShowBase(DirectObject.DirectObject):
         # for the benefit of creating DirectGui elements before ShowBase.
         from . import ShowBaseGlobal
 
-        ## This is the root of the 2-D scene graph.
+        #: This is the root of the 2-D scene graph.
         self.render2d = ShowBaseGlobal.render2d
 
         # Set up some overrides to turn off certain properties which
@@ -1191,12 +1191,12 @@ class ShowBase(DirectObject.DirectObject):
         self.render2d.setMaterialOff(1)
         self.render2d.setTwoSided(1)
 
-        ## The normal 2-d DisplayRegion has an aspect ratio that
-        ## matches the window, but its coordinate system is square.
-        ## This means anything we parent to render2d gets stretched.
-        ## For things where that makes a difference, we set up
-        ## aspect2d, which scales things back to the right aspect
-        ## ratio along the X axis (Z is still from -1 to 1)
+        #: The normal 2-d DisplayRegion has an aspect ratio that
+        #: matches the window, but its coordinate system is square.
+        #: This means anything we parent to render2d gets stretched.
+        #: For things where that makes a difference, we set up
+        #: aspect2d, which scales things back to the right aspect
+        #: ratio along the X axis (Z is still from -1 to 1)
         self.aspect2d = ShowBaseGlobal.aspect2d
 
         aspectRatio = self.getAspectRatio()
@@ -1204,13 +1204,13 @@ class ShowBase(DirectObject.DirectObject):
 
         self.a2dBackground = self.aspect2d.attachNewNode("a2dBackground")
 
-        ## The Z position of the top border of the aspect2d screen.
+        #: The Z position of the top border of the aspect2d screen.
         self.a2dTop = 1.0
-        ## The Z position of the bottom border of the aspect2d screen.
+        #: The Z position of the bottom border of the aspect2d screen.
         self.a2dBottom = -1.0
-        ## The X position of the left border of the aspect2d screen.
+        #: The X position of the left border of the aspect2d screen.
         self.a2dLeft = -aspectRatio
-        ## The X position of the right border of the aspect2d screen.
+        #: The X position of the right border of the aspect2d screen.
         self.a2dRight = aspectRatio
 
         self.a2dTopCenter = self.aspect2d.attachNewNode("a2dTopCenter")
@@ -1250,9 +1250,9 @@ class ShowBase(DirectObject.DirectObject):
         self.a2dBottomRight.setPos(self.a2dRight, 0, self.a2dBottom)
         self.a2dBottomRightNs.setPos(self.a2dRight, 0, self.a2dBottom)
 
-        ## This special root, pixel2d, uses units in pixels that are relative
-        ## to the window. The upperleft corner of the window is (0, 0),
-        ## the lowerleft corner is (xsize, -ysize), in this coordinate system.
+        #: This special root, pixel2d, uses units in pixels that are relative
+        #: to the window. The upperleft corner of the window is (0, 0),
+        #: the lowerleft corner is (xsize, -ysize), in this coordinate system.
         self.pixel2d = self.render2d.attachNewNode(PGTop("pixel2d"))
         self.pixel2d.setPos(-1, 0, 1)
         xsize, ysize = self.getSize()
@@ -1282,25 +1282,25 @@ class ShowBase(DirectObject.DirectObject):
         self.render2dp.setMaterialOff(1)
         self.render2dp.setTwoSided(1)
 
-        ## The normal 2-d DisplayRegion has an aspect ratio that
-        ## matches the window, but its coordinate system is square.
-        ## This means anything we parent to render2dp gets stretched.
-        ## For things where that makes a difference, we set up
-        ## aspect2dp, which scales things back to the right aspect
-        ## ratio along the X axis (Z is still from -1 to 1)
+        #: The normal 2-d DisplayRegion has an aspect ratio that
+        #: matches the window, but its coordinate system is square.
+        #: This means anything we parent to render2dp gets stretched.
+        #: For things where that makes a difference, we set up
+        #: aspect2dp, which scales things back to the right aspect
+        #: ratio along the X axis (Z is still from -1 to 1)
         self.aspect2dp = self.render2dp.attachNewNode(PGTop("aspect2dp"))
         self.aspect2dp.node().setStartSort(16384)
 
         aspectRatio = self.getAspectRatio()
         self.aspect2dp.setScale(1.0 / aspectRatio, 1.0, 1.0)
 
-        ## The Z position of the top border of the aspect2dp screen.
+        #: The Z position of the top border of the aspect2dp screen.
         self.a2dpTop = 1.0
-        ## The Z position of the bottom border of the aspect2dp screen.
+        #: The Z position of the bottom border of the aspect2dp screen.
         self.a2dpBottom = -1.0
-        ## The X position of the left border of the aspect2dp screen.
+        #: The X position of the left border of the aspect2dp screen.
         self.a2dpLeft = -aspectRatio
-        ## The X position of the right border of the aspect2dp screen.
+        #: The X position of the right border of the aspect2dp screen.
         self.a2dpRight = aspectRatio
 
         self.a2dpTopCenter = self.aspect2dp.attachNewNode("a2dpTopCenter")
@@ -1324,9 +1324,9 @@ class ShowBase(DirectObject.DirectObject):
         self.a2dpBottomLeft.setPos(self.a2dpLeft, 0, self.a2dpBottom)
         self.a2dpBottomRight.setPos(self.a2dpRight, 0, self.a2dpBottom)
 
-        ## This special root, pixel2d, uses units in pixels that are relative
-        ## to the window. The upperleft corner of the window is (0, 0),
-        ## the lowerleft corner is (xsize, -ysize), in this coordinate system.
+        #: This special root, pixel2dp, uses units in pixels that are relative
+        #: to the window. The upperleft corner of the window is (0, 0),
+        #: the lowerleft corner is (xsize, -ysize), in this coordinate system.
         self.pixel2dp = self.render2dp.attachNewNode(PGTop("pixel2dp"))
         self.pixel2dp.node().setStartSort(16384)
         self.pixel2dp.setPos(-1, 0, 1)
@@ -1647,11 +1647,11 @@ class ShowBase(DirectObject.DirectObject):
 
         mw = self.buttonThrowers[0].getParent()
 
-        ## A special ButtonThrower to generate keyboard events and
-        ## include the time from the OS.  This is separate only to
-        ## support legacy code that did not expect a time parameter; it
-        ## will eventually be folded into the normal ButtonThrower,
-        ## above.
+        #: A special ButtonThrower to generate keyboard events and
+        #: include the time from the OS.  This is separate only to
+        #: support legacy code that did not expect a time parameter; it
+        #: will eventually be folded into the normal ButtonThrower,
+        #: above.
         self.timeButtonThrower = mw.attachNewNode(ButtonThrower('timeButtons'))
         self.timeButtonThrower.node().setPrefix('time-')
         self.timeButtonThrower.node().setTimeFlag(1)
@@ -2713,7 +2713,7 @@ class ShowBase(DirectObject.DirectObject):
 
     def screenshot(self, namePrefix = 'screenshot',
                    defaultFilename = 1, source = None,
-                   imageComment=""):
+                   imageComment="", blocking=True):
         """ Captures a screenshot from the main window or from the
         specified window or Texture and writes it to a filename in the
         current directory (or to a specified directory).
@@ -2735,6 +2735,13 @@ class ShowBase(DirectObject.DirectObject):
         generated by makeCubeMap(), namePrefix should contain the hash
         mark ('#') character.
 
+        Normally, this call will block until the screenshot is fully
+        written.  To write the screenshot in a background thread
+        instead, pass blocking = False.  In this case, the return value
+        is a future that can be awaited.
+
+        A "screenshot" event will be sent once the screenshot is saved.
+
         :returns: The filename if successful, or None if there is a problem.
         """
 
@@ -2751,8 +2758,12 @@ class ShowBase(DirectObject.DirectObject):
                 saved = source.write(filename, 0, 0, 1, 0)
             else:
                 saved = source.write(filename)
-        else:
+        elif blocking:
             saved = source.saveScreenshot(filename, imageComment)
+        else:
+            request = source.saveAsyncScreenshot(filename, imageComment)
+            request.addDoneCallback(lambda fut, filename=filename: messenger.send('screenshot', [filename]))
+            return request
 
         if saved:
             # Announce to anybody that a screenshot has been taken

+ 0 - 1
direct/src/showutil/Effects.py

@@ -105,7 +105,6 @@ def createBounce(nodeObj, numBounces, startValues, totalTime, amplitude,
 
         newVec3 = Vec3(startValues)
         newVec3.setCell(index, currBounceVal)
-        print("### newVec3 = %s" % newVec3)
 
         # create the right type of lerp
         if ((bounceType == SX_BOUNCE) or (bounceType == SY_BOUNCE) or

+ 96 - 0
doc/ReleaseNotes

@@ -1,3 +1,99 @@
+-----------------------  RELEASE 1.10.13  -----------------------
+
+This is a significant release containing many important bug fixes and a couple
+of interesting new features.
+
+Rendering
+* Fix single texture stage limit when `gl-version 3 2` or higher is set (#1404)
+* Fix some render-to-texture bugs with multithreaded pipeline (incl. #1364)
+* Fix failure to unset divisor after rendering with hardware instancing
+* Add "MSAA" filter to CommonFilters class as convenience to enable MSAA
+* Fix multisample FBOs with MRT resolving aux target into color target
+* Fix shader generator not responding to fog color changes
+* Fix OpenGL error when downloading GL_LUMINANCE8 texture
+
+Windowing
+* Fix incorrect "without" event generation when mouse leaves window (#1400)
+* Windows: Fix lost "up" events when dragging cursor outside window while
+  multiple mouse buttons are pressed down (#1396)
+* macOS: Fix crash with threading-model on newer macOS versions (#1286)
+* macOS: Fix black screen when going fullscreen on Apple M1-based macs (#1316)
+* macOS: Fix window overlapping Dock when requesting very large height
+* macOS: Improve application termination handling, now sends proper exit events
+* X11: tinydisplay handles window resizes more efficiently
+* X11: Work around window not rendering at first on swaywm (#1087)
+
+Deployment
+* Not all code was being built with optimize level 2
+* Add `keep_docstrings` option to switch to optimize level 1 (#1341)
+* Add `prefer_discrete_gpu` option to force dedicated GPU on Windows (#680)
+* Add `bam_model_extensions` for converting non-egg models to .bam (#714)
+* Work around autodiscovery error when using `setuptools>=61.0.0` (#1394)
+* Default Linux target to `manylinux2014_x86_64` on Python 3.11+
+
+PStats
+* Add support for Python profiling with `pstats-python-profiler` Config.prc var
+* Fix PStats crash at launch from pip installs in newer Linux distros (#1391)
+* Performance improvements to time-based strip chart views
+* Time-based strip charts now can show start/stop count in corner of graphs
+* Optimize client performance when sending a large number of samples
+* Fix dropped frames by changing value for `pstats-max-queue-size` from 1 to 4
+* Server accept clients using PStats protocol version 2.3
+
+Assimp
+* Fix assertion when loading meshes with multiple primitive types
+* Add `assimp-collapse-dummy-root-node` option to remove root node (see #366)
+* Import custom object properties as tags
+* Add support for additional texture maps, including PBR textures
+* Support reading tangent and binormal vectors
+* Improve performance when loading geometry
+* Fix problems reading external files (see #366)
+* Support reading alpha mode when loading .glTF models
+* Add support for texture transforms
+* Add support for texture wrapping modes
+* Fix memory corruption bugs
+
+Build
+* Fixes to `pview.desktop` file on Linux
+* makepanda: Fix problems when building on arm64 on macOS without `--arch` flag
+* makepanda: Fix detection issues with newer macOS / XCode versions
+* makepanda: Fix motiontrail header files not being copied
+* Windows: Fix HTTPClient not working when nativenet module is disabled
+* macOS: Fix OpenCV library refusing to load in arm64 build (#1393)
+* Fix compiler error when compiling for e2k (MCST Elbrus 2000) (#1367)
+
+Miscellaneous
+* Add new motion trails sample program
+* `MotionTrail.add_vertex()` method now directly accepts a vertex position
+* Significant performance optimization of C++-based motion trail implementation
+* Fix race condition when destructing/constructing NodePaths in thread (#1366)
+* Add implementation of capsule-into-polygon collision test (#1369)
+* Fix texture transforms sometimes not being flattened (#1392)
+* Fix support for `#pragma include <file.glsl>` in GLSL shaders
+* Fix `ShaderBuffer.prepare()` not doing anything
+* Implement deepcopy for PointerToArray
+* Fix Texture deepcopy keeping a reference to the original RAM image
+* Fix bf-cbc encryption no longer working when building with OpenSSL 3.0
+* PandaNode bounds_type property was erroneously marked read-only
+* Fix warnings when copying OdeTriMeshGeom objects
+* Fix a crash when using `Notify.set_ostream_ptr()` from Python (#1371)
+* Fix GarbageReport not working with Python 3 (#1304)
+* Make `mat.cols[n]` and `mat.rows[n]` assignable
+* Fix `ExecutionEnvironment.args` being empty on Linux
+* Add various useful functions to interrogatedb module
+* Fix Python 3 issues unpacking uint types in Python 3 (#1380)
+* Fix interrogate syntax error with C++11-style attributes in declarators
+* Fix double-precision color values not being clamped by GeomVertexWriter
+* Fix regression with BufferViewer in double-precision build (#1365)
+* Fix `PandaNode.nested_vertices` not updating properly
+* Prevent Panda calculating bounding volume of Geom with custom bounding volume
+* Add `do_events()` and `process_event()` snake_case aliases in eventMgr
+* Support second arg of None in `replace_texture()` / `replace_material()`
+* Support `os.fspath()` for ConfigVariableFilename objects (#1406)
+* rplight: Fix PSSM calculation failing with infinite far distance (#1397)
+* Remove spurious print in `direct.showutil.Effects.createBounce()` (#1383)
+* Fix assorted compiler warnings
+
 -----------------------  RELEASE 1.10.12  -----------------------
 
 Recommended maintenance release containing primarily bug fixes.

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

@@ -52,6 +52,7 @@ allocate(size_t size, TypeHandle type_handle) {
 #ifdef USE_DELETED_CHAIN
   // TAU_PROFILE("void *DeletedBufferChain::allocate(size_t, TypeHandle)", "
   // ", TAU_USER);
+  // If this triggers, maybe you forgot ALLOC_DELETED_CHAIN in a subclass?
   assert(size <= _buffer_size);
 
   // Determine how much space to allocate.

+ 2 - 2
dtool/src/dtoolbase/memoryHook.I

@@ -18,7 +18,7 @@
 INLINE void MemoryHook::
 inc_heap(size_t size) {
 #ifdef DO_MEMORY_USAGE
-  AtomicAdjust::add(_requested_heap_size, (AtomicAdjust::Integer)size);
+  _requested_heap_size.fetch_add(size, std::memory_order_relaxed);
 #endif  // DO_MEMORY_USAGE
 }
 
@@ -30,7 +30,7 @@ INLINE void MemoryHook::
 dec_heap(size_t size) {
 #ifdef DO_MEMORY_USAGE
   // assert((int)size <= _requested_heap_size);
-  AtomicAdjust::add(_requested_heap_size, -(AtomicAdjust::Integer)size);
+  _requested_heap_size.fetch_sub(size, std::memory_order_relaxed);
 #endif  // DO_MEMORY_USAGE
 }
 

+ 18 - 18
dtool/src/dtoolbase/memoryHook.cxx

@@ -203,10 +203,10 @@ ptr_to_alloc(void *ptr, size_t &size) {
  */
 MemoryHook::
 MemoryHook(const MemoryHook &copy) :
-  _total_heap_single_size(copy._total_heap_single_size),
-  _total_heap_array_size(copy._total_heap_array_size),
-  _requested_heap_size(copy._requested_heap_size),
-  _total_mmap_size(copy._total_mmap_size),
+  _total_heap_single_size(copy._total_heap_single_size.load(std::memory_order_relaxed)),
+  _total_heap_array_size(copy._total_heap_array_size.load(std::memory_order_relaxed)),
+  _requested_heap_size(copy._requested_heap_size.load(std::memory_order_relaxed)),
+  _total_mmap_size(copy._total_mmap_size.load(std::memory_order_relaxed)),
   _max_heap_size(copy._max_heap_size),
   _page_size(copy._page_size) {
 }
@@ -250,9 +250,9 @@ heap_alloc_single(size_t size) {
   size = get_ptr_size(alloc);
   inflated_size = size;
 #endif
-  AtomicAdjust::add(_total_heap_single_size, (AtomicAdjust::Integer)size);
-  if ((size_t)AtomicAdjust::get(_total_heap_single_size) +
-      (size_t)AtomicAdjust::get(_total_heap_array_size) >
+  _total_heap_single_size.fetch_add(size, std::memory_order_relaxed);
+  if (_total_heap_single_size.load(std::memory_order_relaxed) +
+      _total_heap_array_size.load(std::memory_order_relaxed) >
       _max_heap_size) {
     overflow_heap_size();
   }
@@ -275,8 +275,8 @@ heap_free_single(void *ptr) {
   void *alloc = ptr_to_alloc(ptr, size);
 
 #ifdef DO_MEMORY_USAGE
-  assert((int)size <= _total_heap_single_size);
-  AtomicAdjust::add(_total_heap_single_size, -(AtomicAdjust::Integer)size);
+  assert((int)size <= _total_heap_single_size.load(std::memory_order_relaxed));
+  _total_heap_single_size.fetch_sub(size, std::memory_order_relaxed);
 #endif  // DO_MEMORY_USAGE
 
 #ifdef MEMORY_HOOK_MALLOC_LOCK
@@ -327,9 +327,9 @@ heap_alloc_array(size_t size) {
   size = get_ptr_size(alloc);
   inflated_size = size;
 #endif
-  AtomicAdjust::add(_total_heap_array_size, (AtomicAdjust::Integer)size);
-  if ((size_t)AtomicAdjust::get(_total_heap_single_size) +
-      (size_t)AtomicAdjust::get(_total_heap_array_size) >
+  _total_heap_array_size.fetch_add(size, std::memory_order_relaxed);
+  if (_total_heap_single_size.load(std::memory_order_relaxed) +
+      _total_heap_array_size.load(std::memory_order_relaxed) >
       _max_heap_size) {
     overflow_heap_size();
   }
@@ -383,8 +383,8 @@ heap_realloc_array(void *ptr, size_t size) {
   size = get_ptr_size(alloc1);
   inflated_size = size;
 #endif
-  assert((AtomicAdjust::Integer)orig_size <= _total_heap_array_size);
-  AtomicAdjust::add(_total_heap_array_size, (AtomicAdjust::Integer)size-(AtomicAdjust::Integer)orig_size);
+  assert(orig_size <= _total_heap_array_size.load(std::memory_order_relaxed));
+  _total_heap_array_size.fetch_add(size - orig_size, std::memory_order_relaxed);
 #endif  // DO_MEMORY_USAGE
 
   // Align this to the requested boundary.
@@ -424,7 +424,7 @@ heap_free_array(void *ptr) {
 
 #ifdef DO_MEMORY_USAGE
   assert((int)size <= _total_heap_array_size);
-  AtomicAdjust::add(_total_heap_array_size, -(AtomicAdjust::Integer)size);
+  _total_heap_array_size.fetch_sub(size, std::memory_order_relaxed);
 #endif  // DO_MEMORY_USAGE
 
 #ifdef MEMORY_HOOK_MALLOC_LOCK
@@ -489,7 +489,7 @@ mmap_alloc(size_t size, bool allow_exec) {
   assert((size % _page_size) == 0);
 
 #ifdef DO_MEMORY_USAGE
-  _total_mmap_size += size;
+  _total_mmap_size.fetch_add(size, std::memory_order_relaxed);
 #endif
 
 #ifdef _WIN32
@@ -544,8 +544,8 @@ mmap_free(void *ptr, size_t size) {
   assert((size % _page_size) == 0);
 
 #ifdef DO_MEMORY_USAGE
-  assert((int)size <= _total_mmap_size);
-  _total_mmap_size -= size;
+  assert((int)size <= _total_mmap_size.load(std::memory_order_relaxed));
+  _total_mmap_size.fetch_sub(size, std::memory_order_relaxed);
 #endif
 
 #ifdef _WIN32

+ 5 - 5
dtool/src/dtoolbase/memoryHook.h

@@ -16,7 +16,7 @@
 
 #include "dtoolbase.h"
 #include "numeric_types.h"
-#include "atomicAdjust.h"
+#include "patomic.h"
 #include "mutexImpl.h"
 #include <map>
 
@@ -66,10 +66,10 @@ public:
   INLINE static size_t get_ptr_size(void *ptr);
 
 protected:
-  TVOLATILE AtomicAdjust::Integer _total_heap_single_size = 0;
-  TVOLATILE AtomicAdjust::Integer _total_heap_array_size = 0;
-  TVOLATILE AtomicAdjust::Integer _requested_heap_size = 0;
-  TVOLATILE AtomicAdjust::Integer _total_mmap_size = 0;
+  patomic<size_t> _total_heap_single_size { 0u };
+  patomic<size_t> _total_heap_array_size { 0u };
+  patomic<size_t> _requested_heap_size { 0u };
+  patomic<size_t> _total_mmap_size { 0u };
 
   // If the allocated heap size crosses this threshold, we call
   // overflow_heap_size().

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

@@ -63,6 +63,7 @@ template<class T>
 ALWAYS_INLINE T patomic<T>::
 operator=(T desired) noexcept {
   _value = desired;
+  return desired;
 }
 
 /**

+ 0 - 19
dtool/src/dtoolbase/typeHandle.cxx

@@ -166,25 +166,6 @@ get_python_type() const {
 }
 #endif
 
-/**
- * Return the Index of the BEst fit Classs from a set
- */
-int TypeHandle::
-get_best_parent_from_Set(const std::set< int > &legal_vals) const {
-  if (legal_vals.find(_index) != legal_vals.end()) {
-    return _index;
-  }
-
-  for (int pi = 0; pi < get_num_parent_classes(); ++pi) {
-    TypeHandle ph = get_parent_class(pi);
-    int val = ph.get_best_parent_from_Set(legal_vals);
-    if (val > 0) {
-      return val;
-    }
-  }
-  return -1;
-}
-
 std::ostream &
 operator << (std::ostream &out, TypeHandle::MemoryClass mem_class) {
   switch (mem_class) {

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

@@ -97,7 +97,9 @@ PUBLISHED:
   // its value, it  might happen after the value had already been set
   // previously by another static initializer!
 
+#ifdef HAVE_PYTHON
   EXTENSION(static TypeHandle make(PyTypeObject *classobj));
+#endif
 
   INLINE bool operator == (const TypeHandle &other) const;
   INLINE bool operator != (const TypeHandle &other) const;
@@ -121,8 +123,6 @@ PUBLISHED:
   INLINE TypeHandle get_parent_towards(TypeHandle ancestor,
                                        TypedObject *object = nullptr) const;
 
-  int get_best_parent_from_Set(const std::set< int > &legal_vals) const;
-
   size_t get_memory_usage(MemoryClass memory_class) const;
   void inc_memory_usage(MemoryClass memory_class, size_t size);
   void dec_memory_usage(MemoryClass memory_class, size_t size);
@@ -137,13 +137,15 @@ PUBLISHED:
   MAKE_SEQ_PROPERTY(parent_classes, get_num_parent_classes, get_parent_class);
   MAKE_SEQ_PROPERTY(child_classes, get_num_child_classes, get_child_class);
 
+#ifdef HAVE_PYTHON
   EXTENSION(PyObject *__reduce__() const);
   EXTENSION(void __setstate__(PyObject *));
+#endif // HAVE_PYTHON
 
 public:
 #ifdef HAVE_PYTHON
   PyObject *get_python_type() const;
-#endif
+#endif // HAVE_PYTHON
 
   void *allocate_array(size_t size) RETURNS_ALIGNED(MEMORY_HOOK_ALIGNMENT);
   void *reallocate_array(void *ptr, size_t size) RETURNS_ALIGNED(MEMORY_HOOK_ALIGNMENT);

+ 0 - 18
dtool/src/dtoolbase/typeRegistry.cxx

@@ -695,21 +695,3 @@ look_up_invalid(TypeHandle handle, TypedObject *object) const {
 
   return _handle_registry[handle._index];
 }
-
-/**
-
- */
-extern "C" int
-get_best_parent_from_Set(int id, const std::set<int> &this_set) {
-  // most common case..
-  if (this_set.find(id) != this_set.end()) {
-    return id;
-  }
-
-  TypeHandle th = TypeRegistry::ptr()->find_type_by_id(id);
-  if (th == TypeHandle::none()) {
-    return -1;
-  }
-
-  return th.get_best_parent_from_Set(this_set);
-}

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

@@ -118,9 +118,6 @@ private:
   friend class TypeHandle;
 };
 
-// Helper function to allow for "C" interaction into the type system
-extern "C" EXPCL_DTOOL_DTOOLBASE  int get_best_parent_from_Set(int id, const std::set<int> &this_set);
-
 #include "typeHandle.h"
 
 #include "typeRegistry.I"

+ 0 - 8
dtool/src/dtoolbase/typedObject.I

@@ -43,14 +43,6 @@ is_exact_type(TypeHandle handle) const {
   return get_type() == handle;
 }
 
-/**
- *
- */
-INLINE int TypedObject::
-get_best_parent_from_Set(const std::set<int> &inset) const {
-  return get_type().get_best_parent_from_Set(inset);
-}
-
 /**
  * Returns the object, upcast (if necessary) to a TypedObject pointer.
  */

+ 0 - 2
dtool/src/dtoolbase/typedObject.h

@@ -106,8 +106,6 @@ PUBLISHED:
   INLINE bool is_exact_type(TypeHandle handle) const;
 
 public:
-  INLINE int get_best_parent_from_Set(const std::set<int> &) const;
-
   // Derived classes should override this function to call init_type().  It
   // will only be called in error situations when the type was for some reason
   // not properly initialized.

+ 4 - 0
dtool/src/dtoolutil/executionEnvironment.cxx

@@ -418,6 +418,10 @@ ns_get_environment_variable(const string &var) const {
   } else if (var == "XDG_DATA_HOME") {
     Filename home_dir = Filename::get_home_directory();
     return home_dir.get_fullpath() + "/.local/share";
+
+  } else if (var == "XDG_STATE_HOME") {
+    Filename home_dir = Filename::get_home_directory();
+    return home_dir.get_fullpath() + "/.local/state";
   }
 #endif // _WIN32
 

+ 32 - 21
dtool/src/dtoolutil/filename.cxx

@@ -16,7 +16,6 @@
 #include "dSearchPath.h"
 #include "executionEnvironment.h"
 #include "vector_string.h"
-#include "atomicAdjust.h"
 
 #include <stdio.h>  // For rename() and tempnam()
 #include <time.h>   // for clock() and time()
@@ -60,10 +59,10 @@ using std::wstring;
 
 TextEncoder::Encoding Filename::_filesystem_encoding = TextEncoder::E_utf8;
 
-TVOLATILE AtomicAdjust::Pointer Filename::_home_directory;
-TVOLATILE AtomicAdjust::Pointer Filename::_temp_directory;
-TVOLATILE AtomicAdjust::Pointer Filename::_user_appdata_directory;
-TVOLATILE AtomicAdjust::Pointer Filename::_common_appdata_directory;
+patomic<Filename *> Filename::_home_directory(nullptr);
+patomic<Filename *> Filename::_temp_directory(nullptr);
+patomic<Filename *> Filename::_user_appdata_directory(nullptr);
+patomic<Filename *> Filename::_common_appdata_directory(nullptr);
 TypeHandle Filename::_type_handle;
 
 #ifdef ANDROID
@@ -486,7 +485,8 @@ temporary(const string &dirname, const string &prefix, const string &suffix,
  */
 const Filename &Filename::
 get_home_directory() {
-  if (AtomicAdjust::get_ptr(_home_directory) == nullptr) {
+  Filename *curdir = _home_directory.load(std::memory_order_consume);
+  if (curdir == nullptr) {
     Filename home_directory;
 
     // In all environments except Windows, check $HOME first.
@@ -538,14 +538,16 @@ get_home_directory() {
     }
 
     Filename *newdir = new Filename(home_directory);
-    if (AtomicAdjust::compare_and_exchange_ptr(_home_directory, nullptr, newdir) != nullptr) {
+    if (_home_directory.compare_exchange_strong(curdir, newdir, std::memory_order_release, std::memory_order_consume)) {
+      return *newdir;
+    } else {
       // Didn't store it.  Must have been stored by someone else.
-      assert(_home_directory != nullptr);
+      assert(curdir != nullptr);
       delete newdir;
     }
   }
 
-  return (*(Filename *)_home_directory);
+  return *curdir;
 }
 
 /**
@@ -553,7 +555,8 @@ get_home_directory() {
  */
 const Filename &Filename::
 get_temp_directory() {
-  if (AtomicAdjust::get_ptr(_temp_directory) == nullptr) {
+  Filename *curdir = _temp_directory.load(std::memory_order_consume);
+  if (curdir == nullptr) {
     Filename temp_directory;
 
 #ifdef _WIN32
@@ -586,14 +589,16 @@ get_temp_directory() {
     }
 
     Filename *newdir = new Filename(temp_directory);
-    if (AtomicAdjust::compare_and_exchange_ptr(_temp_directory, nullptr, newdir) != nullptr) {
+    if (_temp_directory.compare_exchange_strong(curdir, newdir, std::memory_order_release, std::memory_order_consume)) {
+      return *newdir;
+    } else {
       // Didn't store it.  Must have been stored by someone else.
-      assert(_temp_directory != nullptr);
+      assert(curdir != nullptr);
       delete newdir;
     }
   }
 
-  return (*(Filename *)_temp_directory);
+  return *curdir;
 }
 
 /**
@@ -603,7 +608,8 @@ get_temp_directory() {
  */
 const Filename &Filename::
 get_user_appdata_directory() {
-  if (AtomicAdjust::get_ptr(_user_appdata_directory) == nullptr) {
+  Filename *curdir = _user_appdata_directory.load(std::memory_order_consume);
+  if (curdir == nullptr) {
     Filename user_appdata_directory;
 
 #ifdef _WIN32
@@ -643,14 +649,16 @@ get_user_appdata_directory() {
     }
 
     Filename *newdir = new Filename(user_appdata_directory);
-    if (AtomicAdjust::compare_and_exchange_ptr(_user_appdata_directory, nullptr, newdir) != nullptr) {
+    if (_user_appdata_directory.compare_exchange_strong(curdir, newdir, std::memory_order_release, std::memory_order_consume)) {
+      return *newdir;
+    } else {
       // Didn't store it.  Must have been stored by someone else.
-      assert(_user_appdata_directory != nullptr);
+      assert(curdir != nullptr);
       delete newdir;
     }
   }
 
-  return (*(Filename *)_user_appdata_directory);
+  return *curdir;
 }
 
 /**
@@ -659,7 +667,8 @@ get_user_appdata_directory() {
  */
 const Filename &Filename::
 get_common_appdata_directory() {
-  if (AtomicAdjust::get_ptr(_common_appdata_directory) == nullptr) {
+  Filename *curdir = _common_appdata_directory.load(std::memory_order_consume);
+  if (curdir == nullptr) {
     Filename common_appdata_directory;
 
 #ifdef _WIN32
@@ -693,14 +702,16 @@ get_common_appdata_directory() {
     }
 
     Filename *newdir = new Filename(common_appdata_directory);
-    if (AtomicAdjust::compare_and_exchange_ptr(_common_appdata_directory, nullptr, newdir) != nullptr) {
+    if (_common_appdata_directory.compare_exchange_strong(curdir, newdir, std::memory_order_release, std::memory_order_consume)) {
+      return *newdir;
+    } else {
       // Didn't store it.  Must have been stored by someone else.
-      assert(_common_appdata_directory != nullptr);
+      assert(curdir != nullptr);
       delete newdir;
     }
   }
 
-  return (*(Filename *)_common_appdata_directory);
+  return *curdir;
 }
 
 /**

+ 11 - 8
dtool/src/dtoolutil/filename.h

@@ -20,6 +20,7 @@
 #include "register_type.h"
 #include "vector_string.h"
 #include "textEncoder.h"
+#include "patomic.h"
 
 #include <assert.h>
 
@@ -69,7 +70,7 @@ PUBLISHED:
   EXTENSION(Filename(PyObject *path));
 
   EXTENSION(PyObject *__reduce__(PyObject *self) const);
-#endif
+#endif // HAVE_PYTHON
 
   // Static constructors to explicitly create a filename that refers to a text
   // or binary file.  This is in lieu of calling set_text() or set_binary() or
@@ -113,8 +114,10 @@ PUBLISHED:
   INLINE size_t length() const;
   INLINE char operator [] (size_t n) const;
 
+#ifdef HAVE_PYTHON
   EXTENSION(PyObject *__repr__() const);
   EXTENSION(PyObject *__fspath__() const);
+#endif // HAVE_PYTHON
 
   INLINE std::string substr(size_t begin) const;
   INLINE std::string substr(size_t begin, size_t end) const;
@@ -201,7 +204,7 @@ PUBLISHED:
   bool scan_directory(vector_string &contents) const;
 #ifdef HAVE_PYTHON
   EXTENSION(PyObject *scan_directory() const);
-#endif
+#endif // HAVE_PYTHON
 
   bool open_read(std::ifstream &stream) const;
   bool open_write(std::ofstream &stream, bool truncate = true) const;
@@ -265,15 +268,15 @@ protected:
   int _flags;
 
   static TextEncoder::Encoding _filesystem_encoding;
-  static TVOLATILE AtomicAdjust::Pointer _home_directory;
-  static TVOLATILE AtomicAdjust::Pointer _temp_directory;
-  static TVOLATILE AtomicAdjust::Pointer _user_appdata_directory;
-  static TVOLATILE AtomicAdjust::Pointer _common_appdata_directory;
+  static patomic<Filename *> _home_directory;
+  static patomic<Filename *> _temp_directory;
+  static patomic<Filename *> _user_appdata_directory;
+  static patomic<Filename *> _common_appdata_directory;
 
 #ifdef ANDROID
 public:
   static std::string _internal_data_dir;
-#endif
+#endif // ANDROID
 
 public:
   static TypeHandle get_class_type() {
@@ -294,4 +297,4 @@ INLINE std::ostream &operator << (std::ostream &out, const Filename &n) {
 
 #include "filename.I"
 
-#endif
+#endif // !FILENAME_H

+ 10 - 10
dtool/src/dtoolutil/textEncoder.h

@@ -54,28 +54,28 @@ PUBLISHED:
   INLINE static Encoding get_default_encoding();
   MAKE_PROPERTY(default_encoding, get_default_encoding, set_default_encoding);
 
-#ifdef CPPPARSER
+#if defined(CPPPARSER) && defined(HAVE_PYTHON)
   EXTEND void set_text(PyObject *text);
   EXTEND void set_text(PyObject *text, Encoding encoding);
-#else
+#else // CPPPARSER && HAVE_PYTHON
   INLINE void set_text(const std::string &text);
   INLINE void set_text(const std::string &text, Encoding encoding);
-#endif
+#endif // CPPPARSER && HAVE_PYTHON
   INLINE void clear_text();
   INLINE bool has_text() const;
 
   void make_upper();
   void make_lower();
 
-#ifdef CPPPARSER
+#if defined(CPPPARSER) && defined(HAVE_PYTHON)
   EXTEND PyObject *get_text() const;
   EXTEND PyObject *get_text(Encoding encoding) const;
   EXTEND void append_text(PyObject *text);
-#else
+#else // CPPPARSER && HAVE_PYTHON
   INLINE std::string get_text() const;
   INLINE std::string get_text(Encoding encoding) const;
   INLINE void append_text(const std::string &text);
-#endif
+#endif // CPPPARSER && HAVE_PYTHON
   INLINE void append_unicode_char(char32_t character);
   INLINE size_t get_num_chars() const;
   INLINE int get_unicode_char(size_t index) const;
@@ -108,19 +108,19 @@ PUBLISHED:
   std::wstring get_wtext_as_ascii() const;
   bool is_wtext() const;
 
-#ifdef CPPPARSER
+#if defined(CPPPARSER) && defined(HAVE_PYTHON)
   EXTEND static PyObject *encode_wchar(char32_t ch, Encoding encoding);
   EXTEND INLINE PyObject *encode_wtext(const std::wstring &wtext) const;
   EXTEND static PyObject *encode_wtext(const std::wstring &wtext, Encoding encoding);
   EXTEND INLINE PyObject *decode_text(PyObject *text) const;
   EXTEND static PyObject *decode_text(PyObject *text, Encoding encoding);
-#else
+#else // CPPPARSER && HAVE_PYTHON
   static std::string encode_wchar(char32_t ch, Encoding encoding);
   INLINE std::string encode_wtext(const std::wstring &wtext) const;
   static std::string encode_wtext(const std::wstring &wtext, Encoding encoding);
   INLINE std::wstring decode_text(const std::string &text) const;
   static std::wstring decode_text(const std::string &text, Encoding encoding);
-#endif
+#endif // CPPPARSER && HAVE_PYTHON
 
   MAKE_PROPERTY(text, get_text, set_text);
 
@@ -156,4 +156,4 @@ INLINE std::ostream & operator << (std::ostream &out, const std::wstring &str);
 
 #include "textEncoder.I"
 
-#endif
+#endif // !TEXTENCODER_H

+ 7 - 2
dtool/src/interrogate/functionRemap.cxx

@@ -872,8 +872,13 @@ setup_properties(const InterrogateFunction &ifunc, InterfaceMaker *interface_mak
     } else if (fname == "make") {
       if (!_has_this && _parameters.size() >= 1 &&
           TypeManager::is_pointer(_return_type->get_new_type())) {
-        // We can use this for coercion.
-        _flags |= F_coerce_constructor;
+        // We can use this for coercion, except if this is a kwargs param that
+        // does not have a default value.
+        if ((_flags & F_explicit_args) == 0 ||
+            _parameters.size() != first_param + 2 ||
+            _parameters[first_param + 1]._remap->has_default_value()) {
+          _flags |= F_coerce_constructor;
+        }
       }
 
       if (_args_type == InterfaceMaker::AT_varargs) {

+ 25 - 10
dtool/src/interrogate/interfaceMakerPythonNative.cxx

@@ -1221,22 +1221,33 @@ write_class_details(ostream &out, Object *obj) {
     out << "  return nullptr;\n";
     out << "}\n\n";
 
-    out << "static void *Dtool_DowncastInterface_" << ClassName << "(void *from_this, Dtool_PyTypedObject *from_type) {\n";
+    out << "static Dtool_PyInstDef *Dtool_Wrap_" << ClassName << "(void *from_this, Dtool_PyTypedObject *from_type) {\n";
     out << "  if (from_this == nullptr || from_type == nullptr) {\n";
     out << "    return nullptr;\n";
     out << "  }\n";
-    out << "  if (from_type == Dtool_Ptr_" << ClassName << ") {\n";
-    out << "    return from_this;\n";
+    out << "  " << cClassName << " *to_this;\n";
+    out << "  if (from_type == &Dtool_" << ClassName << ") {\n";
+    out << "    to_this = (" << cClassName << "*)from_this;\n";
     out << "  }\n";
     for (di = details.begin(); di != details.end(); di++) {
       if (di->second._can_downcast && di->second._is_legal_py_class) {
-        out << "  if (from_type == Dtool_Ptr_" << make_safe_name(di->second._to_class_name) << ") {\n";
+        out << "  else if (from_type == Dtool_Ptr_" << make_safe_name(di->second._to_class_name) << ") {\n";
         out << "    " << di->second._to_class_name << "* other_this = (" << di->second._to_class_name << "*)from_this;\n" ;
-        out << "    return (" << cClassName << "*)other_this;\n";
+        out << "    to_this = (" << cClassName << "*)other_this;\n";
         out << "  }\n";
       }
     }
-    out << "  return nullptr;\n";
+    out << "  else {\n";
+    out << "    return nullptr;\n";
+    out << "  }\n";
+    out << "  // Allocate a new Python instance\n";
+    out << "  Dtool_PyInstDef *self = (Dtool_PyInstDef *)PyType_GenericAlloc(&Dtool_" << ClassName << "._PyType, 0);\n";
+    out << "  self->_signature = PY_PANDA_SIGNATURE;\n";
+    out << "  self->_My_Type = &Dtool_" << ClassName << ";\n";
+    out << "  self->_ptr_to_object = to_this;\n";
+    out << "  self->_memory_rules = false;\n";
+    out << "  self->_is_const = false;\n";
+    out << "  return self;\n";
     out << "}\n\n";
   }
 }
@@ -3251,7 +3262,7 @@ write_module_class(ostream &out, Object *obj) {
   out << "  TypeHandle::none(),\n";
   out << "  Dtool_PyModuleClassInit_" << ClassName << ",\n";
   out << "  Dtool_UpcastInterface_" << ClassName << ",\n";
-  out << "  Dtool_DowncastInterface_" << ClassName << ",\n";
+  out << "  Dtool_Wrap_" << ClassName << ",\n";
 
   int has_coerce = has_coerce_constructor(obj->_itype._cpptype->as_struct_type());
   if (has_coerce > 0) {
@@ -4819,9 +4830,13 @@ write_function_instance(ostream &out, FunctionRemap *remap,
     // The function handles the arguments by itself.
     expected_params += "*args";
     pexprs.push_back("args");
-    if (args_type == AT_keyword_args) {
-      expected_params += ", **kwargs";
-      pexprs.push_back("kwds");
+    if (remap->_args_type == AT_keyword_args) {
+      if (args_type == AT_keyword_args) {
+        expected_params += ", **kwargs";
+        pexprs.push_back("kwds");
+      } else {
+        pexprs.push_back("nullptr");
+      }
     }
     num_params = 0;
   }

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

@@ -513,7 +513,7 @@ main(int argc, char **argv) {
   for (i = 1; i < argc; ++i) {
     Filename filename = Filename::from_os_specific(argv[i]);
     if (!parser.parse_file(filename)) {
-      cerr << "Error parsing file: '" << argv[i] << "'\n";
+      cerr << "interrogate failed to parse file: '" << argv[i] << "'\n";
       exit(1);
     }
     builder.add_source_file(filename.to_os_generic());

+ 3 - 3
dtool/src/interrogatedb/dtool_super_base.cxx

@@ -39,11 +39,11 @@ static void Dtool_PyModuleClassInit_DTOOL_SUPER_BASE(PyObject *module) {
   }
 }
 
-static void *Dtool_DowncastInterface_DTOOL_SUPER_BASE(void *from_this, Dtool_PyTypedObject *from_type) {
+static void *Dtool_UpcastInterface_DTOOL_SUPER_BASE(PyObject *self, Dtool_PyTypedObject *requested_type) {
   return nullptr;
 }
 
-static void *Dtool_UpcastInterface_DTOOL_SUPER_BASE(PyObject *self, Dtool_PyTypedObject *requested_type) {
+static Dtool_PyInstDef *Dtool_Wrap_DTOOL_SUPER_BASE(void *from_this, Dtool_PyTypedObject *from_type) {
   return nullptr;
 }
 
@@ -140,7 +140,7 @@ Dtool_PyTypedObject *Dtool_GetSuperBase() {
     TypeHandle::none(),
     Dtool_PyModuleClassInit_DTOOL_SUPER_BASE,
     Dtool_UpcastInterface_DTOOL_SUPER_BASE,
-    Dtool_DowncastInterface_DTOOL_SUPER_BASE,
+    Dtool_Wrap_DTOOL_SUPER_BASE,
     nullptr,
     nullptr,
   };

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

@@ -211,6 +211,10 @@ INLINE PyObject *_PyLong_Lshift(PyObject *a, size_t shiftby) {
 
 /* Python 3.9 */
 
+#ifndef PyCFunction_CheckExact
+#  define PyCFunction_CheckExact(op) (Py_TYPE(op) == &PyCFunction_Type)
+#endif
+
 #if PY_VERSION_HEX < 0x03090000
 INLINE PyObject *PyObject_CallNoArgs(PyObject *func) {
 #if PY_VERSION_HEX >= 0x03080000

+ 12 - 18
dtool/src/interrogatedb/py_panda.cxx

@@ -458,31 +458,24 @@ PyObject *DTool_CreatePyInstanceTyped(void *local_this_in, Dtool_PyTypedObject &
     Dtool_PyTypedObject *target_class = (Dtool_PyTypedObject *)TypeHandle::from_index(type_index).get_python_type();
     if (target_class != nullptr) {
       // cast to the type...
-      void *new_local_this = target_class->_Dtool_DowncastInterface(local_this_in, &known_class_type);
-      if (new_local_this != nullptr) {
-        // ask class to allocate an instance..
-        Dtool_PyInstDef *self = (Dtool_PyInstDef *) target_class->_PyType.tp_new(&target_class->_PyType, nullptr, nullptr);
-        if (self != nullptr) {
-          self->_ptr_to_object = new_local_this;
-          self->_memory_rules = memory_rules;
-          self->_is_const = is_const;
-          // self->_signature = PY_PANDA_SIGNATURE;
-          self->_My_Type = target_class;
-          return (PyObject *)self;
-        }
+      Dtool_PyInstDef *self = target_class->_Dtool_WrapInterface(local_this_in, &known_class_type);
+      if (self != nullptr) {
+        self->_memory_rules = memory_rules;
+        self->_is_const = is_const;
+        return (PyObject *)self;
       }
     }
   }
 
   // if we get this far .. just wrap the thing in the known type ?? better
   // than aborting...I guess....
-  Dtool_PyInstDef *self = (Dtool_PyInstDef *) known_class_type._PyType.tp_new(&known_class_type._PyType, nullptr, nullptr);
+  Dtool_PyInstDef *self = (Dtool_PyInstDef *)PyType_GenericAlloc(&known_class_type._PyType, 0);
   if (self != nullptr) {
+    self->_signature = PY_PANDA_SIGNATURE;
+    self->_My_Type = &known_class_type;
     self->_ptr_to_object = local_this_in;
     self->_memory_rules = memory_rules;
     self->_is_const = is_const;
-    // self->_signature = PY_PANDA_SIGNATURE;
-    self->_My_Type = &known_class_type;
   }
   return (PyObject *)self;
 }
@@ -497,13 +490,14 @@ PyObject *DTool_CreatePyInstance(void *local_this, Dtool_PyTypedObject &in_class
     return Py_None;
   }
 
-  Dtool_PyTypedObject *classdef = &in_classdef;
-  Dtool_PyInstDef *self = (Dtool_PyInstDef *) classdef->_PyType.tp_new(&classdef->_PyType, nullptr, nullptr);
+  Dtool_PyInstDef *self = (Dtool_PyInstDef *)PyType_GenericAlloc(&in_classdef._PyType, 0);
   if (self != nullptr) {
+    self->_signature = PY_PANDA_SIGNATURE;
+    self->_My_Type = &in_classdef;
     self->_ptr_to_object = local_this;
     self->_memory_rules = memory_rules;
     self->_is_const = is_const;
-    self->_My_Type = classdef;
+    self->_My_Type = &in_classdef;
   }
   return (PyObject *)self;
 }

+ 3 - 2
dtool/src/interrogatedb/py_panda.h

@@ -35,12 +35,13 @@ using namespace std;
 #define IMPORT_THIS extern
 #endif
 
+struct Dtool_PyInstDef;
 struct Dtool_PyTypedObject;
 
 // used to stamp dtool instance..
 #define PY_PANDA_SIGNATURE 0xbeaf
 typedef void *(*UpcastFunction)(PyObject *,Dtool_PyTypedObject *);
-typedef void *(*DowncastFunction)(void *, Dtool_PyTypedObject *);
+typedef Dtool_PyInstDef *(*WrapFunction)(void *, Dtool_PyTypedObject *);
 typedef void *(*CoerceFunction)(PyObject *, void *);
 typedef void (*ModuleClassInitFunction)(PyObject *module);
 
@@ -78,7 +79,7 @@ struct Dtool_PyTypedObject {
   ModuleClassInitFunction _Dtool_ModuleClassInit;
 
   UpcastFunction _Dtool_UpcastInterface;    // The Upcast Function By Slot
-  DowncastFunction _Dtool_DowncastInterface; // The Downcast Function By Slot
+  WrapFunction _Dtool_WrapInterface; // The Downcast Function By Slot
 
   CoerceFunction _Dtool_ConstCoerce;
   CoerceFunction _Dtool_Coerce;

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

@@ -25,6 +25,9 @@ typedef _object PyObject;
 struct _typeobject;
 typedef _typeobject PyTypeObject;
 
+struct _frame;
+typedef _frame PyFrameObject;
+
 typedef struct {} PyStringObject;
 typedef struct {} PyUnicodeObject;
 

+ 0 - 71
dtool/src/parser-inc/tinyxml.h

@@ -1,71 +0,0 @@
-#ifndef TINYXML_H
-#define TINYXML_H
-
-// A simple header to mirror the subset of the tinyxml interface we
-// wish to expose to interrogate.  This is intended to protect us from
-// having to run interrogate directly on the tinyxml.h header file.
-
-class TiXmlBase;
-class TiXmlNode;
-class TiXmlElement;
-class TiXmlDocument;
-
-class TiXmlBase {
-};
-
-
-class TiXmlNode : public TiXmlBase {
-public:
-  const char *Value() const;
-  void SetValue(const char *_value);
-
-  TiXmlNode *InsertEndChild(const TiXmlNode &addThis);
-  bool RemoveChild( TiXmlNode* removeThis );
-  
-  const TiXmlElement *NextSiblingElement() const;
-  TiXmlElement *NextSiblingElement();
-
-  const TiXmlElement* NextSiblingElement(const char *) const;
-  TiXmlElement* NextSiblingElement(const char *_next);
-
-  const TiXmlElement* FirstChildElement() const;
-  TiXmlElement* FirstChildElement();
-
-  const TiXmlElement* FirstChildElement( const char * _value ) const;
-  TiXmlElement* FirstChildElement( const char * _value );
-
-  virtual TiXmlNode* Clone() const;
-};
-
-
-class TiXmlElement : public TiXmlNode {
-public:
-  TiXmlElement(const char * in_value);
-  TiXmlElement( const TiXmlElement& );
-
-  const char* Attribute( const char* name ) const;
-  void SetAttribute( const char* name, const char * _value );
-  void RemoveAttribute( const char * name );
-};
-
-class TiXmlDeclaration : public TiXmlNode {
-public:
-  TiXmlDeclaration(const char* _version,
-                   const char* _encoding,
-                   const char* _standalone);
-};
-
-
-class TiXmlDocument : public TiXmlNode {
-public:
-  TiXmlDocument();
-  TiXmlDocument(const char * documentName);
-
-  bool LoadFile();
-  bool SaveFile() const;
-  bool LoadFile(const char * filename);
-  bool SaveFile(const char * filename) const;
-};
-
-
-#endif

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

@@ -76,6 +76,8 @@ endif()
 set(P3PRC_IGATEEXT
   configVariable_ext.cxx
   configVariable_ext.h
+  notify_ext.cxx
+  notify_ext.h
   streamReader_ext.cxx
   streamReader_ext.h
   streamWriter_ext.cxx

+ 3 - 1
dtool/src/prc/configVariable.h

@@ -44,7 +44,9 @@ PUBLISHED:
 
   INLINE size_t get_num_words() const;
 
+#ifdef HAVE_PYTHON
   EXTENSION(PyObject *__reduce__(PyObject *self) const);
+#endif // HAVE_PYTHON
 
 protected:
   INLINE const ConfigDeclaration *get_default_value() const;
@@ -74,4 +76,4 @@ protected:
 
 #include "configVariable.I"
 
-#endif
+#endif // !CONFIGVARIABLE_H

+ 11 - 0
dtool/src/prc/configVariableFilename.I

@@ -226,6 +226,17 @@ __bool__() const {
   return !get_value().empty();
 }
 
+/**
+ * Allows a ConfigVariableFilename object to be passed to any Python function
+ * that accepts an os.PathLike object.
+ *
+ * @since 1.10.13
+ */
+INLINE std::wstring ConfigVariableFilename::
+__fspath__() const {
+  return get_ref_value().to_os_specific_w();
+}
+
 /**
  * Returns the variable's value, as a reference into the config variable
  * itself.  This is the internal method that implements get_value(), which

+ 1 - 0
dtool/src/prc/configVariableFilename.h

@@ -61,6 +61,7 @@ PUBLISHED:
   INLINE void set_word(size_t n, const Filename &value);
 
   INLINE bool __bool__() const;
+  INLINE std::wstring __fspath__() const;
 
 private:
   void reload_cache();

+ 73 - 2
dtool/src/prc/encryptStreamBuf.cxx

@@ -23,6 +23,32 @@
 #include <openssl/rand.h>
 #include <openssl/evp.h>
 
+#if OPENSSL_VERSION_MAJOR >= 3
+#include <openssl/provider.h>
+
+/**
+ * Tries to load the legacy provider in OpenSSL.  Returns true if the provider
+ * was just loaded, false if it was already loaded or couldn't be loaded.
+ */
+static bool load_legacy_provider() {
+  static bool tried = false;
+  if (!tried) {
+    tried = true;
+    if (OSSL_PROVIDER_try_load(nullptr, "legacy", 1) != nullptr) {
+      if (prc_cat.is_debug()) {
+        prc_cat.debug()
+          << "Loaded legacy OpenSSL provider.\n";
+      }
+      return true;
+    } else {
+      prc_cat.warning()
+        << "Failed to load legacy OpenSSL provider.\n";
+    }
+  }
+  return false;
+}
+#endif  // OPENSSL_VERSION_MAJOR
+
 // The iteration count is scaled by this factor for writing to the stream.
 static const int iteration_count_factor = 1000;
 
@@ -37,10 +63,10 @@ EncryptStreamBuf() {
   _owns_dest = false;
 
   ConfigVariableString encryption_algorithm
-    ("encryption-algorithm", "bf-cbc",
+    ("encryption-algorithm", "aes-256-cbc",
      PRC_DESC("This defines the OpenSSL encryption algorithm which is used to "
               "encrypt any streams created by the current runtime.  The default is "
-              "Blowfish; the complete set of available algorithms is defined by "
+              "AES-256; the complete set of available algorithms is defined by "
               "the current version of OpenSSL.  This value is used only to control "
               "encryption; the correct algorithm will automatically be selected on "
               "decryption."));
@@ -122,7 +148,31 @@ open_read(std::istream *source, bool owns_source, const std::string &password) {
   int key_length = sr.get_uint16();
   int count = sr.get_uint16();
 
+#if OPENSSL_VERSION_MAJOR >= 3
+  // First, convert the cipher's nid to its full name.
+  const char *cipher_name = OBJ_nid2ln(nid);
+
+  const EVP_CIPHER *cipher = nullptr;
+  if (cipher_name != nullptr) {
+    // Now, fetch the cipher known by this name.
+    cipher = EVP_CIPHER_fetch(nullptr, cipher_name, nullptr);
+
+    if (cipher == nullptr && EVP_get_cipherbynid(nid) != nullptr) {
+      if (load_legacy_provider()) {
+        cipher = EVP_CIPHER_fetch(nullptr, cipher_name, nullptr);
+      }
+
+      if (cipher == nullptr) {
+        prc_cat.error()
+          << "No implementation available for encryption algorithm in stream: "
+          << cipher_name << "\n";
+        return;
+      }
+    }
+  }
+#else
   const EVP_CIPHER *cipher = EVP_get_cipherbynid(nid);
+#endif
 
   if (cipher == nullptr) {
     prc_cat.error()
@@ -220,8 +270,29 @@ open_write(std::ostream *dest, bool owns_dest, const std::string &password) {
   _dest = dest;
   _owns_dest = owns_dest;
 
+#if OPENSSL_VERSION_MAJOR >= 3
+  // This checks that there is actually an implementation available.
+  const EVP_CIPHER *cipher =
+    EVP_CIPHER_fetch(nullptr, _algorithm.c_str(), nullptr);
+
+  if (cipher == nullptr &&
+      EVP_get_cipherbyname(_algorithm.c_str()) != nullptr) {
+    // The cipher does exist, though, do we need to load the legacy provider?
+    if (load_legacy_provider()) {
+      cipher = EVP_CIPHER_fetch(nullptr, _algorithm.c_str(), nullptr);
+    }
+
+    if (cipher == nullptr) {
+      prc_cat.error()
+        << "No implementation available for encryption algorithm: "
+        << _algorithm << "\n";
+      return;
+    }
+  }
+#else
   const EVP_CIPHER *cipher =
     EVP_get_cipherbyname(_algorithm.c_str());
+#endif
 
   if (cipher == nullptr) {
     prc_cat.error()

+ 48 - 0
dtool/src/prc/notify_ext.cxx

@@ -0,0 +1,48 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file notify_ext.cxx
+ * @author rdb
+ * @date 2022-12-09
+ */
+
+#include "notify_ext.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * Changes the ostream that all subsequent Notify messages will be written to.
+ * If the previous ostream was set with delete_later = true, this will delete
+ * the previous ostream.  If ostream_ptr is None, this resets the default to
+ * cerr.
+ */
+void Extension<Notify>::
+set_ostream_ptr(PyObject *ostream_ptr, bool delete_later) {
+  extern struct Dtool_PyTypedObject Dtool_std_ostream;
+
+  if (ostream_ptr == Py_None) {
+    _this->set_ostream_ptr(nullptr, false);
+    return;
+  }
+
+  std::ostream *ptr = (std::ostream *)DTOOL_Call_GetPointerThisClass(ostream_ptr, &Dtool_std_ostream, 1, "Notify.set_ostream_ptr", false, true);
+  if (ptr == nullptr) {
+    return;
+  }
+
+  // Since we now have a reference to this class on the C++ end, make sure
+  // that the ostream isn't being destructed when its Python wrapper expires.
+  // Note that this may cause a memory leak if delete_later is not set, but
+  // since these pointers are usually set once for the rest of time, this is
+  // considered less of a problem than having the Python object destroy the
+  // object while C++ is still using it.  See GitHub #1371.
+  ((Dtool_PyInstDef *)ostream_ptr)->_memory_rules = false;
+  _this->set_ostream_ptr(ptr, delete_later);
+}
+
+#endif  // HAVE_PYTHON

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

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

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

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

+ 4 - 0
dtool/src/prc/pnotify.h

@@ -35,7 +35,11 @@ PUBLISHED:
   Notify();
   ~Notify();
 
+#if defined(CPPPARSER) && defined(HAVE_PYTHON)
+  EXTEND void set_ostream_ptr(PyObject *ostream_ptr, bool delete_later);
+#else
   void set_ostream_ptr(std::ostream *ostream_ptr, bool delete_later);
+#endif
   std::ostream *get_ostream_ptr() const;
 
   typedef bool AssertHandler(const char *expression, int line,

+ 3 - 2
dtool/src/prc/streamReader.h

@@ -68,11 +68,12 @@ PUBLISHED:
 
   BLOCKING void skip_bytes(size_t size);
   BLOCKING size_t extract_bytes(unsigned char *into, size_t size);
+#ifdef HAVE_PYTHON
   EXTENSION(PyObject *extract_bytes(size_t size));
 
   EXTENSION(PyObject *readline());
   EXTENSION(PyObject *readlines());
-
+#endif // HAVE_PYTHON
 public:
   BLOCKING vector_uchar extract_bytes(size_t size);
   BLOCKING std::string readline();
@@ -84,4 +85,4 @@ private:
 
 #include "streamReader.I"
 
-#endif
+#endif // !STREAMREADER_H

+ 3 - 1
dtool/src/prc/streamWriter.h

@@ -70,7 +70,9 @@ PUBLISHED:
   BLOCKING INLINE void add_fixed_string(const std::string &str, size_t size);
 
   BLOCKING void pad_bytes(size_t size);
+#ifdef HAVE_PYTHON
   EXTENSION(void append_data(PyObject *data));
+#endif // HAVE_PYTHON
 
   BLOCKING INLINE void flush();
 
@@ -87,4 +89,4 @@ private:
 
 #include "streamWriter.I"
 
-#endif
+#endif // !STREAMWRITER_H

+ 31 - 8
makepanda/installer.nsi

@@ -320,7 +320,6 @@ Section "Tools and utilities" SecTools
 
     SetOutPath "$INSTDIR\bin"
     File /r /x deploy-stub.exe /x deploy-stubw.exe "${BUILT}\bin\*.exe"
-    File /nonfatal /r "${BUILT}\bin\*.p3d"
     SetOutPath "$INSTDIR\NSIS"
     File /r /x CVS "${NSISDIR}\*"
 
@@ -343,6 +342,13 @@ Section "Tools and utilities" SecTools
     WriteRegStr HKCU "Software\Classes\Panda3D.Multifile\shell" "" "open"
     WriteRegStr HKCU "Software\Classes\Panda3D.Multifile\shell\extract" "" "Extract here"
     WriteRegStr HKCU "Software\Classes\Panda3D.Multifile\shell\extract\command" "" '"$INSTDIR\bin\multify.exe" -xf "%1"'
+
+    IfFileExists "$INSTDIR\bin\pstats.exe" 0 SkipPStatsFileAssociation
+    WriteRegStr HKCU "Software\Classes\Panda3D.PStatsSession" "" "PStats Session"
+    WriteRegStr HKCU "Software\Classes\Panda3D.PStatsSession\DefaultIcon" "" "%SystemRoot%\system32\imageres.dll,144"
+    WriteRegStr HKCU "Software\Classes\Panda3D.PStatsSession\shell" "" "open"
+    WriteRegStr HKCU "Software\Classes\Panda3D.PStatsSession\shell\open\command" "" '"$INSTDIR\bin\pstats.exe" "%1"'
+    SkipPStatsFileAssociation:
 SectionEnd
 
 SectionGroup "Python modules" SecGroupPython
@@ -375,6 +381,7 @@ SectionGroup "Python modules" SecGroupPython
         !insertmacro PyBindingSection 3.9-32 .cp39-win32.pyd
         !insertmacro PyBindingSection 3.10-32 .cp310-win32.pyd
         !insertmacro PyBindingSection 3.11-32 .cp311-win32.pyd
+        !insertmacro PyBindingSection 3.12-32 .cp312-win32.pyd
     !else
         !insertmacro PyBindingSection 3.5 .cp35-win_amd64.pyd
         !insertmacro PyBindingSection 3.6 .cp36-win_amd64.pyd
@@ -383,6 +390,7 @@ SectionGroup "Python modules" SecGroupPython
         !insertmacro PyBindingSection 3.9 .cp39-win_amd64.pyd
         !insertmacro PyBindingSection 3.10 .cp310-win_amd64.pyd
         !insertmacro PyBindingSection 3.11 .cp311-win_amd64.pyd
+        !insertmacro PyBindingSection 3.12 .cp312-win_amd64.pyd
     !endif
 SectionGroupEnd
 
@@ -492,6 +500,7 @@ Function .onInit
         !insertmacro MaybeEnablePyBindingSection 3.9-32
         !insertmacro MaybeEnablePyBindingSection 3.10-32
         !insertmacro MaybeEnablePyBindingSection 3.11-32
+        !insertmacro MaybeEnablePyBindingSection 3.12-32
         ${EndIf}
     !else
         !insertmacro MaybeEnablePyBindingSection 3.5
@@ -502,6 +511,7 @@ Function .onInit
         !insertmacro MaybeEnablePyBindingSection 3.9
         !insertmacro MaybeEnablePyBindingSection 3.10
         !insertmacro MaybeEnablePyBindingSection 3.11
+        !insertmacro MaybeEnablePyBindingSection 3.12
         ${EndIf}
     !endif
 
@@ -519,6 +529,10 @@ Function .onInit
         SectionSetFlags ${SecPyBindings3.11} ${SF_RO}
         SectionSetInstTypes ${SecPyBindings3.11} 0
     !endif
+    !ifdef SecPyBindings3.12
+        SectionSetFlags ${SecPyBindings3.12} ${SF_RO}
+        SectionSetInstTypes ${SecPyBindings3.12} 0
+    !endif
     ${EndUnless}
 FunctionEnd
 
@@ -632,6 +646,9 @@ Section "Sample programs" SecSamples
     WriteINIStr $INSTDIR\Manual.url "InternetShortcut" "URL" "https://docs.panda3d.org/${MAJOR_VER}"
     WriteINIStr $INSTDIR\Samples.url "InternetShortcut" "URL" "https://docs.panda3d.org/${MAJOR_VER}/python/more-resources/samples/index"
     SetOutPath $INSTDIR
+    IfFileExists "$INSTDIR\bin\pstats.exe" 0 SkipPStatsShortcut
+    CreateShortCut "$SMPROGRAMS\${TITLE}\Panda3D Stats Monitor.lnk" "$INSTDIR\bin\pstats.exe" "" "%SystemRoot%\system32\imageres.dll" 144 "" "" "Panda3D Stats Monitor"
+    SkipPStatsShortcut:
     CreateShortCut "$SMPROGRAMS\${TITLE}\Panda3D Manual.lnk" "$INSTDIR\Manual.url" "" "$INSTDIR\pandaIcon.ico" 0 "" "" "Panda3D Manual"
     CreateShortCut "$SMPROGRAMS\${TITLE}\Panda3D Website.lnk" "$INSTDIR\Website.url" "" "$INSTDIR\pandaIcon.ico" 0 "" "" "Panda3D Website"
     CreateShortCut "$SMPROGRAMS\${TITLE}\Sample Program Manual.lnk" "$INSTDIR\Samples.url" "" "$INSTDIR\pandaIcon.ico" 0 "" "" "Sample Program Manual"
@@ -732,13 +749,7 @@ Section -post
     DetailPrint "Registering file type associations..."
     SetDetailsPrint listonly
 
-    ; Even though we need the runtime to run these, we might as well tell
-    ; Windows what this kind of file is.
-    WriteRegStr HKCU "Software\Classes\.p3d" "" "Panda3D applet"
-    WriteRegStr HKCU "Software\Classes\.p3d" "Content Type" "application/x-panda3d"
-    WriteRegStr HKCU "Software\Classes\.p3d" "PerceivedType" "application"
-
-    ; Register various model files
+    ; Register various file extensions
     WriteRegStr HKCU "Software\Classes\.egg" "" "Panda3D.Model"
     WriteRegStr HKCU "Software\Classes\.egg" "Content Type" "application/x-egg"
     WriteRegStr HKCU "Software\Classes\.egg" "PerceivedType" "gamemedia"
@@ -752,6 +763,9 @@ Section -post
     WriteRegStr HKCU "Software\Classes\.prc" "" "inifile"
     WriteRegStr HKCU "Software\Classes\.prc" "Content Type" "text/plain"
     WriteRegStr HKCU "Software\Classes\.prc" "PerceivedType" "text"
+    WriteRegStr HKCU "Software\Classes\.pstats" "" "Panda3D.PStatsSession"
+    WriteRegStr HKCU "Software\Classes\.pstats" "Content Type" "application/vnd.panda3d.pstats"
+    WriteRegStr HKCU "Software\Classes\.pstats" "PerceivedType" "application"
 
     ; For convenience, if nobody registered .pyd, we will.
     ReadRegStr $0 HKCR "Software\Classes\.pyd" ""
@@ -812,6 +826,11 @@ Section Uninstall
     DeleteRegKey HKCU "Software\Classes\Panda3D.Multifile\DefaultIcon"
     DeleteRegKey HKCU "Software\Classes\Panda3D.Multifile\shell"
 
+    ReadRegStr $0 HKCU "Software\Classes\Panda3D.PStatsSession\DefaultIcon" ""
+    StrCmp $0 "$INSTDIR\bin\pstats.exe" 0 +3
+    DeleteRegKey HKCU "Software\Classes\Panda3D.PStatsSession\DefaultIcon"
+    DeleteRegKey HKCU "Software\Classes\Panda3D.PStatsSession\shell"
+
     !ifdef INCLUDE_PYVER
         ReadRegStr $0 HKLM "Software\Python\PythonCore\${INCLUDE_PYVER}\InstallPath" ""
         StrCmp $0 "$INSTDIR\python" 0 +2
@@ -831,6 +850,7 @@ Section Uninstall
         !insertmacro RemovePythonPath 3.9-32
         !insertmacro RemovePythonPath 3.10-32
         !insertmacro RemovePythonPath 3.11-32
+        !insertmacro RemovePythonPath 3.12-32
     !else
         !insertmacro RemovePythonPath 3.5
         !insertmacro RemovePythonPath 3.6
@@ -839,6 +859,7 @@ Section Uninstall
         !insertmacro RemovePythonPath 3.9
         !insertmacro RemovePythonPath 3.10
         !insertmacro RemovePythonPath 3.11
+        !insertmacro RemovePythonPath 3.12
     !endif
 
     SetDetailsPrint both
@@ -908,6 +929,7 @@ SectionEnd
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.9-32} $(DESC_SecPyBindings3.9-32)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.10-32} $(DESC_SecPyBindings3.10-32)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.11-32} $(DESC_SecPyBindings3.11-32)
+    !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.12-32} $(DESC_SecPyBindings3.12-32)
   !else
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.5} $(DESC_SecPyBindings3.5)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.6} $(DESC_SecPyBindings3.6)
@@ -916,6 +938,7 @@ SectionEnd
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.9} $(DESC_SecPyBindings3.9)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.10} $(DESC_SecPyBindings3.10)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.11} $(DESC_SecPyBindings3.11)
+    !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.12} $(DESC_SecPyBindings3.12)
   !endif
   !ifdef INCLUDE_PYVER
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPython} $(DESC_SecPython)

+ 52 - 24
makepanda/installpanda.py

@@ -18,29 +18,36 @@ from distutils.sysconfig import get_python_lib
 
 
 MIME_INFO = (
-    ("egg", "model/x-egg", "EGG model file", "pview"),
-    ("bam", "model/x-bam", "Panda3D binary model file", "pview"),
-    ("egg.pz", "model/x-compressed-egg", "Compressed EGG model file", "pview"),
-    ("bam.pz", "model/x-compressed-bam", "Compressed Panda3D binary model file", "pview"),
+    # ext, mime, desc, app, magic
+    ("egg", "model/x-egg", "EGG model file", "pview", None),
+    ("bam", "model/x-bam", "Panda3D binary model file", "pview", None),
+    ("egg.pz", "model/x-compressed-egg", "Compressed EGG model file", "pview", None),
+    ("bam.pz", "model/x-compressed-bam", "Compressed Panda3D binary model file", "pview", None),
+    ("pstats", "application/vnd.panda3d.pstats", "PStats session file", "pstats", b"pstat\0\n\r"),
 )
 
 APP_INFO = (
-  ("pview", "Panda3D Model Viewer", ("egg", "bam", "egg.pz", "bam.pz")),
+  ("pview", "Panda3D Model Viewer", ("egg", "bam", "egg.pz", "bam.pz"), True),
+  ("pstats", "Panda3D Profiling Tool", ("pstats",), False),
 )
 
-def WriteApplicationsFile(fname, appinfo, mimeinfo):
+def WriteApplicationsFile(fname, appinfo, mimeinfo, bindir):
     fhandle = open(fname, "w")
-    for app, desc, exts in appinfo:
+    for app, desc, exts, multiple in appinfo:
+        if not os.path.isfile(os.path.join(bindir, app)):
+            continue
+
         fhandle.write("%s\n" % (app))
         fhandle.write("\tcommand=%s\n" % (app))
         fhandle.write("\tname=%s\n" % (desc))
-        fhandle.write("\tcan_open_multiple_files=true\n")
+        fhandle.write("\tcan_open_multiple_files=%s\n" % ('true' if multiple else 'false'))
+        fhandle.write("\tstartup_notify=true\n")
         fhandle.write("\texpects_uris=false\n")
         fhandle.write("\trequires_terminal=false\n")
         fhandle.write("\tmime_types=")
         first = True
-        for ext, mime, desc2, app2 in mimeinfo:
-            if ext in exts:
+        for ext, mime, desc2, app2, magic in mimeinfo:
+            if app == app2 and ext in exts:
                 if first:
                     fhandle.write(mime)
                     first = False
@@ -50,37 +57,53 @@ def WriteApplicationsFile(fname, appinfo, mimeinfo):
     fhandle.close()
 
 
-def WriteMimeXMLFile(fname, info):
+def WriteMimeXMLFile(fname, info, bindir):
     fhandle = open(fname, "w")
     fhandle.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
     fhandle.write("<mime-info xmlns=\"http://www.freedesktop.org/standards/shared-mime-info\">\n")
-    for ext, mime, desc, app in info:
+    for ext, mime, desc, app, magic in info:
+        if not os.path.isfile(os.path.join(bindir, app)):
+            continue
+
         fhandle.write("\t<mime-type type=\"%s\">\n" % (mime))
         fhandle.write("\t\t<comment xml:lang=\"en\">%s</comment>\n" % (desc))
+        if app == "pstats":
+            fhandle.write("\t\t<generic-icon name=\"x-office-spreadsheet\"/>\n")
+        if magic:
+            magic = magic.decode('latin-1').encode('unicode-escape').decode('latin-1')
+            fhandle.write("\t\t<magic>\n\t\t\t<match type=\"string\" offset=\"0\" value=\"%s\"/>\n\t\t</magic>\n" % (magic))
         fhandle.write("\t\t<glob pattern=\"*.%s\"/>\n" % (ext))
         fhandle.write("\t</mime-type>\n")
     fhandle.write("</mime-info>\n")
     fhandle.close()
 
 
-def WriteMimeFile(fname, info):
+def WriteMimeFile(fname, info, bindir):
     fhandle = open(fname, "w")
-    for ext, mime, desc, app in info:
-        fhandle.write("%s:\n" % (mime))
+    for ext, mime, desc, app, magic in info:
+        if not os.path.isfile(os.path.join(bindir, app)):
+            continue
+
+        fhandle.write("%s\n" % (mime))
         if "." in ext:
-            fhandle.write("\tregex,2: %s$\n" % (ext.replace(".", "\\.")))
+            fhandle.write("\tregex,2: \\.%s$\n" % (ext.replace(".", "\\.")))
         fhandle.write("\text: %s\n" % (ext))
         fhandle.write("\n")
     fhandle.close()
 
 
-def WriteKeysFile(fname, info):
+def WriteKeysFile(fname, info, bindir):
     fhandle = open(fname, "w")
-    for ext, mime, desc, app in info:
-        fhandle.write("%s:\n" % (mime))
+    for ext, mime, desc, app, magic in info:
+        if not os.path.isfile(os.path.join(bindir, app)):
+            continue
+
+        fhandle.write("%s\n" % (mime))
         fhandle.write("\tdescription=%s\n" % (desc))
         fhandle.write("\tdefault_action_type=application\n")
         fhandle.write("\tshort_list_application_ids_for_novice_user_level=%s\n" % (app))
+        fhandle.write("\tshort_list_application_ids_for_intermediate_user_level=%s\n" % (app))
+        fhandle.write("\tshort_list_application_ids_for_advanced_user_level=%s\n" % (app))
         fhandle.write("\topen=%s %%f\n" % (app))
         fhandle.write("\tview=%s %%f\n" % (app))
         fhandle.write("\n")
@@ -215,12 +238,15 @@ def InstallPanda(destdir="", prefix="/usr", outputdir="built", libdir=GetLibDir(
             if base.endswith(".py") or (base.endswith(suffix) and '.' not in base[:-len(suffix)]):
                 oscmd(f"cp {outputdir}/panda3d/{base} {destdir}{platlib}/panda3d/{base}")
 
-    WriteMimeFile(dest_prefix + "/share/mime-info/panda3d.mime", MIME_INFO)
-    WriteKeysFile(dest_prefix + "/share/mime-info/panda3d.keys", MIME_INFO)
-    WriteMimeXMLFile(dest_prefix + "/share/mime/packages/panda3d.xml", MIME_INFO)
-    WriteApplicationsFile(dest_prefix + "/share/application-registry/panda3d.applications", APP_INFO, MIME_INFO)
+    bindir = outputdir + "/bin"
+    WriteMimeFile(dest_prefix + "/share/mime-info/panda3d.mime", MIME_INFO, bindir)
+    WriteKeysFile(dest_prefix + "/share/mime-info/panda3d.keys", MIME_INFO, bindir)
+    WriteMimeXMLFile(dest_prefix + "/share/mime/packages/panda3d.xml", MIME_INFO, bindir)
+    WriteApplicationsFile(dest_prefix + "/share/application-registry/panda3d.applications", APP_INFO, MIME_INFO, bindir)
     if os.path.isfile(outputdir + "/bin/pview"):
         oscmd(f"cp makepanda/pview.desktop {dest_prefix}/share/applications/pview.desktop")
+    if os.path.isfile(outputdir + "/bin/pstats"):
+        oscmd(f"cp makepanda/pstats.desktop {dest_prefix}/share/applications/pstats.desktop")
 
     oscmd(f"cp doc/ReleaseNotes {dest_prefix}/share/panda3d/ReleaseNotes")
 
@@ -326,5 +352,7 @@ if __name__ == "__main__":
 
     if not destdir:
         warn_prefix = "%sNote:%s " % (GetColor("red"), GetColor())
-        print(warn_prefix + "You may need to call this command to update the library cache:")
+        print(warn_prefix + "You may need to call these commands to update system caches:")
         print("  sudo ldconfig")
+        print("  sudo update-desktop-database")
+        print("  sudo update-mime-database -n %s/share/mime" % (options.prefix))

+ 31 - 37
makepanda/makepanda.py

@@ -303,6 +303,8 @@ def parseopts(args):
         OSX_ARCHS.append("arm64")
     elif target_archs:
         OSX_ARCHS = target_archs
+    elif GetTarget() == 'darwin':
+        OSX_ARCHS = (GetTargetArch(),)
 
     try:
         SetOptimize(int(optimize))
@@ -393,8 +395,9 @@ MAJOR_VERSION = '.'.join(VERSION.split('.')[:2])
 
 # Now determine the distutils-style platform tag for the target system.
 target = GetTarget()
+target_arch = GetTargetArch()
 if target == 'windows':
-    if GetTargetArch() == 'x64':
+    if target_arch == 'x64':
         PLATFORM = 'win-amd64'
     else:
         PLATFORM = 'win32'
@@ -402,7 +405,7 @@ if target == 'windows':
 elif target == 'darwin':
     arch_tag = None
     if not OSX_ARCHS:
-        arch_tag = GetTargetArch()
+        arch_tag = target_arch
     elif len(OSX_ARCHS) == 1:
         arch_tag = OSX_ARCHS[0]
     elif frozenset(OSX_ARCHS) == frozenset(('i386', 'ppc')):
@@ -427,45 +430,53 @@ elif target == 'darwin':
 
 elif target == 'linux' and (os.path.isfile("/lib/libc-2.5.so") or os.path.isfile("/lib64/libc-2.5.so")) and os.path.isdir("/opt/python"):
     # This is manylinux1.  A bit of a sloppy check, though.
-    if GetTargetArch() in ('x86_64', 'amd64'):
+    if target_arch in ('x86_64', 'amd64'):
         PLATFORM = 'manylinux1-x86_64'
-    elif GetTargetArch() in ('arm64', 'aarch64'):
+    elif target_arch in ('arm64', 'aarch64'):
         PLATFORM = 'manylinux1-aarch64'
     else:
         PLATFORM = 'manylinux1-i686'
 
 elif target == 'linux' and (os.path.isfile("/lib/libc-2.12.so") or os.path.isfile("/lib64/libc-2.12.so")) and os.path.isdir("/opt/python"):
     # Same sloppy check for manylinux2010.
-    if GetTargetArch() in ('x86_64', 'amd64'):
+    if target_arch in ('x86_64', 'amd64'):
         PLATFORM = 'manylinux2010-x86_64'
-    elif GetTargetArch() in ('arm64', 'aarch64'):
+    elif target_arch in ('arm64', 'aarch64'):
         PLATFORM = 'manylinux2010-aarch64'
     else:
         PLATFORM = 'manylinux2010-i686'
 
 elif target == 'linux' and (os.path.isfile("/lib/libc-2.17.so") or os.path.isfile("/lib64/libc-2.17.so")) and os.path.isdir("/opt/python"):
     # Same sloppy check for manylinux2014.
-    if GetTargetArch() in ('x86_64', 'amd64'):
+    if target_arch in ('x86_64', 'amd64'):
         PLATFORM = 'manylinux2014-x86_64'
-    elif GetTargetArch() in ('arm64', 'aarch64'):
+    elif target_arch in ('arm64', 'aarch64'):
         PLATFORM = 'manylinux2014-aarch64'
     else:
         PLATFORM = 'manylinux2014-i686'
 
 elif target == 'linux' and (os.path.isfile("/lib/i386-linux-gnu/libc-2.24.so") or os.path.isfile("/lib/x86_64-linux-gnu/libc-2.24.so")) and os.path.isdir("/opt/python"):
     # Same sloppy check for manylinux_2_24.
-    if GetTargetArch() in ('x86_64', 'amd64'):
+    if target_arch in ('x86_64', 'amd64'):
         PLATFORM = 'manylinux_2_24-x86_64'
-    elif GetTargetArch() in ('arm64', 'aarch64'):
+    elif target_arch in ('arm64', 'aarch64'):
         PLATFORM = 'manylinux_2_24-aarch64'
     else:
         PLATFORM = 'manylinux_2_24-i686'
 
+elif target == 'linux' and os.path.isfile("/lib64/libc-2.28.so") and os.path.isfile('/etc/almalinux-release') and os.path.isdir("/opt/python"):
+    # Same sloppy check for manylinux_2_28.
+    if target_arch in ('x86_64', 'amd64'):
+        PLATFORM = 'manylinux_2_28-x86_64'
+    elif target_arch in ('arm64', 'aarch64'):
+        PLATFORM = 'manylinux_2_28-aarch64'
+    else:
+        raise RuntimeError('Unhandled arch %s, please file a bug report!' % (target_arch))
+
 elif not CrossCompiling():
     if HasTargetArch():
         # Replace the architecture in the platform string.
         platform_parts = get_platform().rsplit('-', 1)
-        target_arch = GetTargetArch()
         if target_arch == 'amd64':
             target_arch = 'x86_64'
         PLATFORM = platform_parts[0] + '-' + target_arch
@@ -474,7 +485,6 @@ elif not CrossCompiling():
         PLATFORM = get_platform()
 
 else:
-    target_arch = GetTargetArch()
     if target_arch == 'amd64':
         target_arch = 'x86_64'
     PLATFORM = '{0}-{1}'.format(target, target_arch)
@@ -998,6 +1008,9 @@ if (COMPILER=="GCC"):
             LibName("ARTOOLKIT", "-Wl,--exclude-libs,libAR.a")
             LibName("ARTOOLKIT", "-Wl,--exclude-libs,libARMulti.a")
 
+        if not PkgSkip("HARFBUZZ"):
+            LibName("HARFBUZZ", "-Wl,--exclude-libs,libharfbuzz.a")
+
         if not PkgSkip("MIMALLOC"):
             LibName("MIMALLOC", "-Wl,--exclude-libs,libmimalloc.a")
 
@@ -2910,6 +2923,9 @@ Author-email: [email protected]
 ENTRY_POINTS = """[distutils.commands]
 build_apps = direct.dist.commands:build_apps
 bdist_apps = direct.dist.commands:bdist_apps
+
+[setuptools.finalize_distribution_options]
+build_apps = direct.dist.commands:finalize_distribution_options
 """
 
 if not PkgSkip("DIRECT"):
@@ -3280,7 +3296,6 @@ if not PkgSkip("PANDAPHYSICS"):
     CopyAllHeaders('panda/src/physics')
     if not PkgSkip("PANDAPARTICLESYSTEM"):
         CopyAllHeaders('panda/src/particlesystem')
-CopyAllHeaders('panda/src/dxml')
 CopyAllHeaders('panda/metalibs/panda')
 CopyAllHeaders('panda/src/audiotraits')
 CopyAllHeaders('panda/src/audiotraits')
@@ -3839,6 +3854,7 @@ IGATEFILES=GetDirectoryContents('panda/src/pstatclient', ["*.h", "*_composite*.c
 IGATEFILES.remove("config_pstats.h")
 TargetAdd('libp3pstatclient.in', opts=OPTS, input=IGATEFILES)
 TargetAdd('libp3pstatclient.in', opts=['IMOD:panda3d.core', 'ILIB:libp3pstatclient', 'SRCDIR:panda/src/pstatclient'])
+PyTargetAdd('p3pstatclient_pStatClient_ext.obj', opts=OPTS, input='pStatClient_ext.cxx')
 
 #
 # DIRECTORY: panda/src/gobj/
@@ -4113,24 +4129,6 @@ IGATEFILES=GetDirectoryContents('panda/src/recorder', ["*.h", "*_composite*.cxx"
 TargetAdd('libp3recorder.in', opts=OPTS, input=IGATEFILES)
 TargetAdd('libp3recorder.in', opts=['IMOD:panda3d.core', 'ILIB:libp3recorder', 'SRCDIR:panda/src/recorder'])
 
-#
-# DIRECTORY: panda/src/dxml/
-#
-
-DefSymbol("TINYXML", "TIXML_USE_STL", "")
-
-OPTS=['DIR:panda/src/dxml', 'TINYXML']
-TargetAdd('tinyxml_composite1.obj', opts=OPTS, input='tinyxml_composite1.cxx')
-TargetAdd('libp3tinyxml.ilb', input='tinyxml_composite1.obj')
-
-OPTS=['DIR:panda/src/dxml', 'BUILDING:PANDA', 'TINYXML']
-TargetAdd('p3dxml_composite1.obj', opts=OPTS, input='p3dxml_composite1.cxx')
-
-OPTS=['DIR:panda/src/dxml', 'TINYXML']
-IGATEFILES=GetDirectoryContents('panda/src/dxml', ["*.h", "p3dxml_composite1.cxx"])
-TargetAdd('libp3dxml.in', opts=OPTS, input=IGATEFILES)
-TargetAdd('libp3dxml.in', opts=['IMOD:panda3d.core', 'ILIB:libp3dxml', 'SRCDIR:panda/src/dxml'])
-
 #
 # DIRECTORY: panda/metalibs/panda/
 #
@@ -4208,7 +4206,6 @@ TargetAdd('libpanda.dll', input='p3net_composite2.obj')
 TargetAdd('libpanda.dll', input='p3nativenet_composite1.obj')
 TargetAdd('libpanda.dll', input='p3pandabase_pandabase.obj')
 TargetAdd('libpanda.dll', input='libpandaexpress.dll')
-TargetAdd('libpanda.dll', input='p3dxml_composite1.obj')
 TargetAdd('libpanda.dll', input='libp3dtoolconfig.dll')
 TargetAdd('libpanda.dll', input='libp3dtool.dll')
 
@@ -4254,7 +4251,6 @@ PyTargetAdd('core_module.obj', input='libp3nativenet.in')
 PyTargetAdd('core_module.obj', input='libp3net.in')
 PyTargetAdd('core_module.obj', input='libp3pgui.in')
 PyTargetAdd('core_module.obj', input='libp3movies.in')
-PyTargetAdd('core_module.obj', input='libp3dxml.in')
 
 if PkgSkip("FREETYPE")==0:
     PyTargetAdd('core_module.obj', input='libp3pnmtext.in')
@@ -4300,7 +4296,6 @@ PyTargetAdd('core.pyd', input='libp3audio_igate.obj')
 PyTargetAdd('core.pyd', input='libp3pgui_igate.obj')
 PyTargetAdd('core.pyd', input='libp3net_igate.obj')
 PyTargetAdd('core.pyd', input='libp3nativenet_igate.obj')
-PyTargetAdd('core.pyd', input='libp3dxml_igate.obj')
 
 if PkgSkip("FREETYPE")==0:
     PyTargetAdd('core.pyd', input="libp3pnmtext_igate.obj")
@@ -4310,14 +4305,13 @@ PyTargetAdd('core.pyd', input='p3putil_ext_composite.obj')
 PyTargetAdd('core.pyd', input='p3pnmimage_pfmFile_ext.obj')
 PyTargetAdd('core.pyd', input='p3event_asyncFuture_ext.obj')
 PyTargetAdd('core.pyd', input='p3event_pythonTask.obj')
+PyTargetAdd('core.pyd', input='p3pstatclient_pStatClient_ext.obj')
 PyTargetAdd('core.pyd', input='p3gobj_ext_composite.obj')
 PyTargetAdd('core.pyd', input='p3pgraph_ext_composite.obj')
 PyTargetAdd('core.pyd', input='p3display_ext_composite.obj')
 PyTargetAdd('core.pyd', input='p3collide_ext_composite.obj')
 
 PyTargetAdd('core.pyd', input='core_module.obj')
-if not GetLinkAllStatic() and GetTarget() != 'emscripten':
-    PyTargetAdd('core.pyd', input='libp3tinyxml.ilb')
 PyTargetAdd('core.pyd', input='libp3interrogatedb.dll')
 PyTargetAdd('core.pyd', input=COMMON_PANDA_LIBS)
 PyTargetAdd('core.pyd', opts=['WINSOCK2'])
@@ -5871,7 +5865,7 @@ if not PkgSkip("PANDATOOL") and not PkgSkip("EGG"):
     TargetAdd('libp3ptloader.dll', input='libp3lwo.lib')
     TargetAdd('libp3ptloader.dll', input='libp3dxfegg.lib')
     TargetAdd('libp3ptloader.dll', input='libp3dxf.lib')
-    TargetAdd('libp3ptloader.dll', input='libp3objegg.lib')
+    #TargetAdd('libp3ptloader.dll', input='libp3objegg.lib')
     TargetAdd('libp3ptloader.dll', input='libp3vrmlegg.lib')
     TargetAdd('libp3ptloader.dll', input='libp3vrml.lib')
     TargetAdd('libp3ptloader.dll', input='libp3xfileegg.lib')

+ 6 - 2
makepanda/makepandacore.py

@@ -433,7 +433,7 @@ def SetTarget(target, arch=None):
             ANDROID_ABI = 'x86_64'
             ANDROID_TRIPLE = 'x86_64-linux-android'
         else:
-            exit('Android architecture must be arm, armv7a, arm64, mips, mips64, x86 or x86_64')
+            exit('Android architecture must be arm, armv7a, arm64, mips, mips64, x86 or x86_64, use --arch to specify')
 
         ANDROID_TRIPLE += str(ANDROID_API)
         TOOLCHAIN_PREFIX = ANDROID_TRIPLE + '-'
@@ -2194,6 +2194,10 @@ def SdkLocatePython(prefer_thirdparty_python=False):
             # Fall back to looking on the system.
             py_fwx = "/Library/Frameworks/Python.framework/Versions/" + version
 
+        if not os.path.exists(py_fwx):
+            # Newer macOS versions use this scheme.
+            py_fwx = "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/" + version
+
         if not os.path.exists(py_fwx):
             exit("Could not locate Python installation at %s" % (py_fwx))
 
@@ -2430,7 +2434,7 @@ def SdkLocateMacOSX(archs = []):
         # Prefer pre-10.14 for now so that we can keep building FMOD.
         sdk_versions += ["10.13", "10.12", "10.11", "10.10", "10.9"]
 
-    sdk_versions += ["11.3", "11.1", "11.0"]
+    sdk_versions += ["13.0", "12.3", "11.3", "11.1", "11.0"]
 
     if 'arm64' not in archs:
         sdk_versions += ["10.15", "10.14"]

+ 4 - 0
makepanda/makewheel.py

@@ -656,6 +656,8 @@ def makewheel(version, output_dir, platform=None):
                     platform = platform.replace("linux", "manylinux2014")
                 elif os.path.isfile("/lib/i386-linux-gnu/libc-2.24.so") or os.path.isfile("/lib/x86_64-linux-gnu/libc-2.24.so"):
                     platform = platform.replace("linux", "manylinux_2_24")
+                elif os.path.isfile("/lib64/libc-2.28.so") and os.path.isfile('/etc/almalinux-release'):
+                    platform = platform.replace("linux", "manylinux_2_28")
 
     platform = platform.replace('-', '_').replace('.', '_')
 
@@ -887,6 +889,8 @@ if __debug__:
     entry_points += '[distutils.commands]\n'
     entry_points += 'build_apps = direct.dist.commands:build_apps\n'
     entry_points += 'bdist_apps = direct.dist.commands:bdist_apps\n'
+    entry_points += '[setuptools.finalize_distribution_options]\n'
+    entry_points += 'build_apps = direct.dist.commands:finalize_distribution_options\n'
 
     whl.write_file_data('panda3d_tools/__init__.py', PANDA3D_TOOLS_INIT.format(tools_init))
 

+ 0 - 11
makepanda/panda3d.desktop

@@ -1,11 +0,0 @@
-[Desktop Entry]
-Name=Panda3D
-Comment=Runs 3-D games and interactive applets
-TryExec=panda3d
-Exec=panda3d %U
-StartupNotify=true
-NoDisplay=true
-Terminal=false
-Type=Application
-Categories=Game;3DGraphics;Viewer;
-MimeType=application/x-panda3d;

+ 12 - 0
makepanda/pstats.desktop

@@ -0,0 +1,12 @@
+[Desktop Entry]
+Name=Panda3D Stats Monitor
+GenericName=Profiling Tool
+Comment=Analyze performance of Panda3D applications
+TryExec=pstats
+Exec=pstats %f
+StartupNotify=true
+NoDisplay=false
+Terminal=false
+Type=Application
+Categories=Utility;Development;
+MimeType=application/vnd.panda3d.pstats;

+ 2 - 2
makepanda/pview.desktop

@@ -3,10 +3,10 @@ Name=Panda3D Model Viewer
 GenericName=Model Viewer
 Comment=View Panda3D model files
 TryExec=pview
-Exec=pview %U
+Exec=pview %F
 StartupNotify=true
 NoDisplay=true
-Terminal=true
+Terminal=false
 Type=Application
 Categories=Graphics;Utility;3DGraphics;Viewer;
 MimeType=model/x-egg;model/x-compressed-egg;model/x-bam;model/x-compressed-bam;

+ 4 - 2
panda/CMakeLists.txt

@@ -18,7 +18,6 @@ add_subdirectory(src/distort)
 add_subdirectory(src/downloader)
 add_subdirectory(src/downloadertools)
 add_subdirectory(src/dxgsg9)
-add_subdirectory(src/dxml)
 add_subdirectory(src/egg)
 add_subdirectory(src/egg2pg)
 add_subdirectory(src/egldisplay)
@@ -82,7 +81,7 @@ add_subdirectory(metalibs/pandaphysics)
 # Now add the Python modules:
 set(CORE_MODULE_COMPONENTS
   p3chan p3char p3collide p3cull p3device p3dgraph p3display
-  p3downloader p3dxml p3event p3express p3gobj p3grutil p3gsgbase p3linmath
+  p3downloader p3event p3express p3gobj p3grutil p3gsgbase p3linmath
   p3mathutil p3movies p3parametrics p3pgraph p3pgraphnodes p3pgui
   p3pipeline p3pnmimage p3pstatclient p3putil p3recorder p3text p3tform
   p3prc p3dtoolutil p3dtoolbase
@@ -149,6 +148,9 @@ from .interrogatedb import *
   set(entry_points_file "[distutils.commands]
 build_apps = direct.dist.commands:build_apps
 bdist_apps = direct.dist.commands:bdist_apps
+
+[setuptools.finalize_distribution_options]
+build_apps = direct.dist.commands:finalize_distribution_options
 ")
 
   configure_file("${PROJECT_SOURCE_DIR}/cmake/templates/METADATA.in"

+ 1 - 1
panda/metalibs/panda/CMakeLists.txt

@@ -1,5 +1,5 @@
 set(PANDA_LINK_TARGETS
-  p3chan p3char p3collide p3cull p3device p3dgraph p3display p3dxml
+  p3chan p3char p3collide p3cull p3device p3dgraph p3display
   p3event p3gobj p3grutil p3gsgbase p3linmath p3mathutil
   p3movies p3parametrics p3pgraph p3pgraphnodes p3pgui p3pipeline
   p3pnmimage p3pnmimagetypes p3pstatclient p3putil p3recorder p3text p3tform

+ 8 - 0
panda/src/audio/audioSound.I

@@ -10,3 +10,11 @@
  * @author jyelon
  * @date 2007-08-01
  */
+
+/**
+ * Returns true if this was created as a positional sound.
+ */
+INLINE bool AudioSound::
+is_positional() const {
+  return _positional;
+}

+ 1 - 1
panda/src/audio/audioSound.cxx

@@ -29,7 +29,7 @@ AudioSound::
  *
  */
 AudioSound::
-AudioSound() {
+AudioSound(bool positional) : _positional(positional) {
   // Intentionally blank.
 }
 

+ 6 - 1
panda/src/audio/audioSound.h

@@ -88,6 +88,8 @@ PUBLISHED:
   // There is no set_name(), this is intentional.
   virtual const std::string& get_name() const = 0;
 
+  INLINE bool is_positional() const;
+
   // return: playing time in seconds.
   virtual PN_stdfloat length() const = 0;
 
@@ -134,9 +136,12 @@ PUBLISHED:
   MAKE_PROPERTY(play_rate, get_play_rate, set_play_rate);
   MAKE_PROPERTY(active, get_active, set_active);
   MAKE_PROPERTY(name, get_name);
+  MAKE_PROPERTY(positional, is_positional);
 
 protected:
-  AudioSound();
+  AudioSound(bool positional);
+
+  const bool _positional = false;
 
   friend class AudioManager;
 

+ 11 - 16
panda/src/audio/nullAudioManager.cxx

@@ -16,11 +16,6 @@
 
 TypeHandle NullAudioManager::_type_handle;
 
-// namespace { static const string blank=""; static PN_stdfloat
-// no_listener_attributes [] = {0.0f,0.0f,0.0f, 0.0f,0.0f,0.0f,
-// 0.0f,0.0f,0.0f, 0.0f,0.0f,0.0f}; }
-
-
 /**
  *
  */
@@ -179,7 +174,7 @@ stop_all_sounds() {
  */
 void NullAudioManager::
 audio_3d_set_listener_attributes(PN_stdfloat px, PN_stdfloat py, PN_stdfloat pz, PN_stdfloat vx, PN_stdfloat vy, PN_stdfloat vz, PN_stdfloat fx, PN_stdfloat fy, PN_stdfloat fz, PN_stdfloat ux, PN_stdfloat uy, PN_stdfloat uz) {
-    // intentionally blank.
+  // intentionally blank.
 }
 
 /**
@@ -187,7 +182,7 @@ audio_3d_set_listener_attributes(PN_stdfloat px, PN_stdfloat py, PN_stdfloat pz,
  */
 void NullAudioManager::
 audio_3d_get_listener_attributes(PN_stdfloat *px, PN_stdfloat *py, PN_stdfloat *pz, PN_stdfloat *vx, PN_stdfloat *vy, PN_stdfloat *vz, PN_stdfloat *fx, PN_stdfloat *fy, PN_stdfloat *fz, PN_stdfloat *ux, PN_stdfloat *uy, PN_stdfloat *uz) {
-    // intentionally blank.
+  // intentionally blank.
 }
 
 /**
@@ -195,7 +190,7 @@ audio_3d_get_listener_attributes(PN_stdfloat *px, PN_stdfloat *py, PN_stdfloat *
  */
 void NullAudioManager::
 audio_3d_set_distance_factor(PN_stdfloat factor) {
-    // intentionally blank.
+  // intentionally blank.
 }
 
 /**
@@ -203,8 +198,8 @@ audio_3d_set_distance_factor(PN_stdfloat factor) {
  */
 PN_stdfloat NullAudioManager::
 audio_3d_get_distance_factor() const {
-    // intentionally blank.
-    return 0.0f;
+  // intentionally blank.
+  return 0.0f;
 }
 
 /**
@@ -212,7 +207,7 @@ audio_3d_get_distance_factor() const {
  */
 void NullAudioManager::
 audio_3d_set_doppler_factor(PN_stdfloat factor) {
-    // intentionally blank.
+  // intentionally blank.
 }
 
 /**
@@ -220,8 +215,8 @@ audio_3d_set_doppler_factor(PN_stdfloat factor) {
  */
 PN_stdfloat NullAudioManager::
 audio_3d_get_doppler_factor() const {
-    // intentionally blank.
-    return 0.0f;
+  // intentionally blank.
+  return 0.0f;
 }
 
 /**
@@ -229,7 +224,7 @@ audio_3d_get_doppler_factor() const {
  */
 void NullAudioManager::
 audio_3d_set_drop_off_factor(PN_stdfloat factor) {
-    // intentionally blank.
+  // intentionally blank.
 }
 
 /**
@@ -237,6 +232,6 @@ audio_3d_set_drop_off_factor(PN_stdfloat factor) {
  */
 PN_stdfloat NullAudioManager::
 audio_3d_get_drop_off_factor() const {
-    // intentionally blank.
-    return 0.0f;
+  // intentionally blank.
+  return 0.0f;
 }

+ 1 - 1
panda/src/audio/nullAudioSound.cxx

@@ -26,7 +26,7 @@ namespace {
 /**
  * All of these functions are just stubs.
  */
-NullAudioSound::NullAudioSound() {
+NullAudioSound::NullAudioSound() : AudioSound(false) {
   // Intentionally blank.
 }
 

+ 1 - 2
panda/src/audiotraits/fmodAudioSound.cxx

@@ -37,9 +37,8 @@ TypeHandle FmodAudioSound::_type_handle;
  * Constructor All sound will DEFAULT load as a 2D sound unless otherwise
  * specified.
  */
-
 FmodAudioSound::
-FmodAudioSound(AudioManager *manager, VirtualFile *file, bool positional) {
+FmodAudioSound(AudioManager *manager, VirtualFile *file, bool positional) : AudioSound(positional) {
   ReMutexHolder holder(FmodAudioManager::_lock);
   audio_debug("FmodAudioSound::FmodAudioSound() Creating new sound, filename: "
               << file->get_original_filename());

+ 1 - 1
panda/src/audiotraits/openalAudioSound.cxx

@@ -37,6 +37,7 @@ OpenALAudioSound(OpenALAudioManager* manager,
                  MovieAudio *movie,
                  bool positional,
                  int mode) :
+  AudioSound(positional),
   _movie(movie),
   _sd(nullptr),
   _playing_loops(0),
@@ -47,7 +48,6 @@ OpenALAudioSound(OpenALAudioManager* manager,
   _volume(1.0f),
   _balance(0),
   _play_rate(1.0),
-  _positional(positional),
   _min_dist(1.0f),
   _max_dist(1000000000.0f),
   _drop_off_factor(1.0f),

+ 0 - 1
panda/src/audiotraits/openalAudioSound.h

@@ -152,7 +152,6 @@ private:
   PN_stdfloat _balance; // -1..1
   PN_stdfloat _play_rate; // 0..1.0
 
-  bool _positional;
   ALfloat _location[3];
   ALfloat _velocity[3];
 

+ 54 - 21
panda/src/cocoadisplay/cocoaGraphicsWindow.mm

@@ -76,7 +76,7 @@ CocoaGraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe,
   if (NSApp == nil) {
     [CocoaPandaApp sharedApplication];
 
-    CocoaPandaAppDelegate *delegate = [[CocoaPandaAppDelegate alloc] init];
+    CocoaPandaAppDelegate *delegate = [[CocoaPandaAppDelegate alloc] initWithEngine:engine];
     [NSApp setDelegate:delegate];
 
     [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
@@ -186,29 +186,29 @@ begin_frame(FrameMode mode, Thread *current_thread) {
   cocoagsg->lock_context();
 
   // Set the drawable.
-  if (_properties.get_fullscreen()) {
-    // Fullscreen.
-    CGLSetFullScreenOnDisplay((CGLContextObj) [cocoagsg->_context CGLContextObj], CGDisplayIDToOpenGLDisplayMask(_display));
-  } else {
-    // Although not recommended, it is technically possible to use the same
-    // context with multiple different-sized windows.  If that happens, the
-    // context needs to be updated accordingly.
-    if ([cocoagsg->_context view] != _view) {
-      // XXX I'm not 100% sure that changing the view requires it to update.
-      _context_needs_update = true;
-      [cocoagsg->_context setView:_view];
+  // Although not recommended, it is technically possible to use the same
+  // context with multiple different-sized windows.  If that happens, the
+  // context needs to be updated accordingly.
+  if ([cocoagsg->_context view] != _view) {
+    // XXX I'm not 100% sure that changing the view requires it to update.
+    _context_needs_update = true;
+    [cocoagsg->_context setView:_view];
 
-      if (cocoadisplay_cat.is_spam()) {
-        cocoadisplay_cat.spam()
-          << "Switching context to view " << _view << "\n";
-      }
+    if (cocoadisplay_cat.is_spam()) {
+      cocoadisplay_cat.spam()
+        << "Switching context to view " << _view << "\n";
     }
   }
 
   // Update the context if necessary, to make it reallocate buffers etc.
   if (_context_needs_update) {
-    [cocoagsg->_context update];
-    _context_needs_update = false;
+    if ([NSThread isMainThread]) {
+      [cocoagsg->_context update];
+      _context_needs_update = false;
+    } else {
+      cocoagsg->unlock_context();
+      return false;
+    }
   }
 
   // Lock the view for drawing.
@@ -349,6 +349,18 @@ process_events() {
   }
 
   [pool release];
+
+  if (_context_needs_update && _gsg != nullptr) {
+    CocoaGraphicsStateGuardian *cocoagsg;
+    DCAST_INTO_V(cocoagsg, _gsg);
+
+    if (cocoagsg != nullptr && cocoagsg->_context != nil) {
+      cocoagsg->lock_context();
+      _context_needs_update = false;
+      [cocoagsg->_context update];
+      cocoagsg->unlock_context();
+    }
+  }
 }
 
 /**
@@ -609,7 +621,7 @@ open_window() {
     }
 
     if (_properties.get_fullscreen()) {
-      [_window setLevel: NSMainMenuWindowLevel + 1];
+      [_window setLevel: CGShieldingWindowLevel()];
     } else {
       switch (_properties.get_z_order()) {
       case WindowProperties::Z_bottom:
@@ -807,7 +819,7 @@ set_properties_now(WindowProperties &properties) {
                 [_window setStyleMask:NSBorderlessWindowMask];
               }
               [_window makeFirstResponder:_view];
-              [_window setLevel:NSMainMenuWindowLevel+1];
+              [_window setLevel:CGShieldingWindowLevel()];
               [_window makeKeyAndOrderFront:nil];
             }
 
@@ -912,10 +924,19 @@ set_properties_now(WindowProperties &properties) {
     NSRect frame;
     NSRect container;
     if (_window != nil) {
-      frame = [_window contentRectForFrameRect:[_window frame]];
+      NSRect window_frame = [_window frame];
+      frame = [_window contentRectForFrameRect:window_frame];
       NSScreen *screen = [_window screen];
       nassertv(screen != nil);
       container = [screen frame];
+
+      // Prevent the centering from overlapping the Dock
+      if (y < 0) {
+        NSRect visible_frame = [screen visibleFrame];
+        if (window_frame.size.height == visible_frame.size.height) {
+          y = 0;
+        }
+      }
     } else {
       frame = [_view frame];
       container = [[_view superview] frame];
@@ -1116,6 +1137,18 @@ set_properties_now(WindowProperties &properties) {
       break;
     }
   }
+
+  if (_context_needs_update && _gsg != nullptr) {
+    CocoaGraphicsStateGuardian *cocoagsg;
+    DCAST_INTO_V(cocoagsg, _gsg);
+
+    if (cocoagsg != nullptr && cocoagsg->_context != nil) {
+      cocoagsg->lock_context();
+      _context_needs_update = false;
+      [cocoagsg->_context update];
+      cocoagsg->unlock_context();
+    }
+  }
 }
 
 /**

+ 9 - 1
panda/src/cocoadisplay/cocoaPandaAppDelegate.h

@@ -14,9 +14,17 @@
 #import <Foundation/Foundation.h>
 #import <AppKit/AppKit.h>
 
+class GraphicsEngine;
+
 // Cocoa is picky about where and when certain methods are called in the initialization process.
-@interface CocoaPandaAppDelegate : NSObject<NSApplicationDelegate>
+@interface CocoaPandaAppDelegate : NSObject<NSApplicationDelegate> {
+  @private
+    GraphicsEngine *_engine;
+}
 
+- (id) initWithEngine:(GraphicsEngine *)engine;
 - (void)applicationDidFinishLaunching:(NSNotification *)notification;
+- (BOOL)applicationShouldTerminate:(NSApplication *)app;
+- (void)applicationWillTerminate:(NSNotification *)notification;
 
 @end

+ 48 - 0
panda/src/cocoadisplay/cocoaPandaAppDelegate.mm

@@ -12,12 +12,60 @@
 */
 
 #import "cocoaPandaAppDelegate.h"
+#include "graphicsEngine.h"
+#include "config_cocoadisplay.h"
 
 @implementation CocoaPandaAppDelegate
 
+- (id) initWithEngine:(GraphicsEngine *)engine {
+
+  if (self = [super init]) {
+    _engine = engine;
+  }
+
+  return self;
+}
+
 - (void)applicationDidFinishLaunching:(NSNotification *)notification {
   // This only seems to work when called here.
   [NSApp activateIgnoringOtherApps:YES];
 }
 
+- (BOOL)applicationShouldTerminate:(NSApplication *)app {
+  if (cocoadisplay_cat.is_debug()) {
+    cocoadisplay_cat.debug()
+      << "Received applicationShouldTerminate, requesting to close all Cocoa windows\n";
+  }
+  // Ask all the windows whether they are OK to be closed.
+  bool should_close = true;
+  for (NSWindow *window in [app windows]) {
+    id<NSWindowDelegate> delegate = [window delegate];
+    if (delegate != nil && ![delegate windowShouldClose:window]) {
+      should_close = false;
+    }
+  }
+  if (should_close) {
+    if (cocoadisplay_cat.is_debug()) {
+      cocoadisplay_cat.debug()
+        << "No window objected to close request, closing all windows\n";
+    }
+    // If so (none of them fired a close request event), close them now.
+    for (NSWindow *window in [app windows]) {
+      [window close];
+    }
+  }
+  // Give the application a chance to run its own cleanup functions.
+  return FALSE;
+}
+
+- (void)applicationWillTerminate:(NSNotification *)notification {
+  // The application is about to be closed, tell the graphics engine to close
+  // all the windows.
+  if (cocoadisplay_cat.is_debug()) {
+    cocoadisplay_cat.debug()
+      << "Received applicationWillTerminate, removing all windows\n";
+  }
+  _engine->remove_all_windows();
+}
+
 @end

+ 1 - 0
panda/src/cocoadisplay/cocoaPandaWindowDelegate.h

@@ -30,6 +30,7 @@ class CocoaGraphicsWindow;
 - (void)windowDidBecomeKey:(NSNotification *)notification;
 - (void)windowDidResignKey:(NSNotification *)notification;
 - (BOOL)windowShouldClose:(id)sender;
+- (void)windowWillClose:(id)sender;
 
 // TODO: handle fullscreen on Lion.
 

+ 13 - 4
panda/src/cocoadisplay/cocoaPandaWindowDelegate.mm

@@ -12,6 +12,7 @@
  */
 
 #import "cocoaPandaWindowDelegate.h"
+#include "config_cocoadisplay.h"
 
 @implementation CocoaPandaWindowDelegate
 - (id) initWithGraphicsWindow:(CocoaGraphicsWindow*)window {
@@ -51,11 +52,19 @@
 }
 
 - (BOOL) windowShouldClose:(id)sender {
-  bool should_close = _graphicsWindow->handle_close_request();
-  if (should_close) {
-    _graphicsWindow->handle_close_event();
+  if (cocoadisplay_cat.is_debug()) {
+    cocoadisplay_cat.debug()
+      << "Received windowShouldClose for window " << _graphicsWindow << "\n";
   }
-  return should_close;
+  return _graphicsWindow->handle_close_request();
+}
+
+- (void) windowWillClose:(id)sender {
+  if (cocoadisplay_cat.is_debug()) {
+    cocoadisplay_cat.debug()
+      << "Received windowWillClose for window " << _graphicsWindow << "\n";
+  }
+  _graphicsWindow->handle_close_event();
 }
 
 @end

+ 0 - 8
panda/src/collide/collisionHandler.cxx

@@ -15,14 +15,6 @@
 
 TypeHandle CollisionHandler::_type_handle;
 
-/**
- *
- */
-CollisionHandler::
-CollisionHandler() {
-  _wants_all_potential_collidees = false;
-}
-
 /**
  * Will be called by the CollisionTraverser before a new traversal is begun.
  * It instructs the handler to reset itself in preparation for a number of

+ 5 - 6
panda/src/collide/collisionHandler.h

@@ -28,9 +28,10 @@ class CollisionEntry;
  * dispatch detected collisions.
  */
 class EXPCL_PANDA_COLLIDE CollisionHandler : public TypedReferenceCount {
-public:
-  CollisionHandler();
+PUBLISHED:
+  CollisionHandler() = default;
 
+public:
   virtual void begin_group();
   virtual void add_entry(CollisionEntry *entry);
   virtual bool end_group();
@@ -38,12 +39,10 @@ public:
   INLINE bool wants_all_potential_collidees() const;
   INLINE void set_root(const NodePath &root);
 
-PUBLISHED:
+public:
   static TypeHandle get_class_type() {
     return _type_handle;
   }
-
-public:
   static void init_type() {
     TypedReferenceCount::init_type();
     register_type(_type_handle, "CollisionHandler",
@@ -55,7 +54,7 @@ public:
   virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
 
 protected:
-  bool _wants_all_potential_collidees;
+  bool _wants_all_potential_collidees = false;
   const NodePath *_root;
 
 private:

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

@@ -27,6 +27,7 @@ set(P3DISPLAY_HEADERS
   windowHandle.I windowHandle.h
   windowProperties.I windowProperties.h
   renderBuffer.h
+  screenshotRequest.I screenshotRequest.h
   stereoDisplayRegion.I stereoDisplayRegion.h
   displaySearchParameters.h
   displayInformation.h
@@ -61,6 +62,7 @@ set(P3DISPLAY_SOURCES
   parasiteBuffer.cxx
   windowHandle.cxx
   windowProperties.cxx
+  screenshotRequest.cxx
   stereoDisplayRegion.cxx
   subprocessWindow.cxx
   touchInfo.cxx

+ 2 - 0
panda/src/display/config_display.cxx

@@ -29,6 +29,7 @@
 #include "nativeWindowHandle.h"
 #include "parasiteBuffer.h"
 #include "pandaSystem.h"
+#include "screenshotRequest.h"
 #include "stereoDisplayRegion.h"
 #include "subprocessWindow.h"
 #include "windowHandle.h"
@@ -532,6 +533,7 @@ init_libdisplay() {
   MouseAndKeyboard::init_type();
   NativeWindowHandle::init_type();
   ParasiteBuffer::init_type();
+  ScreenshotRequest::init_type();
   StandardMunger::init_type();
   StereoDisplayRegion::init_type();
 #ifdef SUPPORT_SUBPROCESS_WINDOW

+ 3 - 1
panda/src/display/frameBufferProperties.h

@@ -144,8 +144,10 @@ PUBLISHED:
   MAKE_PROPERTY(float_color, get_float_color, set_float_color);
   MAKE_PROPERTY(float_depth, get_float_depth, set_float_depth);
 
+#ifdef HAVE_PYTHON
   EXTENSION(PyObject *__getstate__() const);
   EXTENSION(void __setstate__(PyObject *self, PyObject *state));
+#endif // HAVE_PYTHON
 
   // Other.
 
@@ -181,4 +183,4 @@ INLINE std::ostream &operator << (std::ostream &out, const FrameBufferProperties
 
 #include "frameBufferProperties.I"
 
-#endif
+#endif // !FRAMEBUFFERPROPERTIES_H

+ 5 - 0
panda/src/display/graphicsEngine.cxx

@@ -1419,6 +1419,8 @@ cull_and_draw_together(GraphicsEngine::Windows wlist,
       }
 
       if (win->begin_frame(GraphicsOutput::FM_render, current_thread)) {
+        win->copy_async_screenshot();
+
         if (win->is_any_clear_active()) {
           GraphicsStateGuardian *gsg = win->get_gsg();
           PStatGPUTimer timer(gsg, win->get_clear_window_pcollector(), current_thread);
@@ -1720,6 +1722,9 @@ draw_bins(const GraphicsEngine::Windows &wlist, Thread *current_thread) {
         // a current context for PStatGPUTimer to work.
         {
           PStatGPUTimer timer(gsg, win->get_draw_window_pcollector(), current_thread);
+
+          win->copy_async_screenshot();
+
           if (win->is_any_clear_active()) {
             PStatGPUTimer timer(gsg, win->get_clear_window_pcollector(), current_thread);
             win->get_gsg()->push_group_marker("Clear");

+ 89 - 1
panda/src/display/graphicsOutput.cxx

@@ -976,6 +976,43 @@ make_cube_map(const string &name, int size, NodePath &camera_rig,
   return buffer;
 }
 
+/**
+ * Like save_screenshot, but performs both the texture transfer and the saving
+ * to disk in the background.  Returns a future that can be awaited.
+ *
+ * This captures the frame that was last submitted by the App stage to the
+ * render_frame() call.  This may not be the latest frame shown on the screen
+ * if the multi-threaded pipeline is used, in which case the request may take
+ * several frames extra to complete.
+ */
+PT(ScreenshotRequest) GraphicsOutput::
+save_async_screenshot(const Filename &filename, const std::string &image_comment) {
+  PT(ScreenshotRequest) request = get_async_screenshot();
+  request->add_output_file(filename, image_comment);
+  return request;
+}
+
+/**
+ * Used to obtain a new Texture object containing the previously rendered frame.
+ * Unlike get_screenshot, this works asynchronously, meaning that the contents
+ * are transferred in the background.  Returns a future that can be awaited.
+ *
+ * This captures the frame that was last submitted by the App stage to the
+ * render_frame() call.  This may not be the latest frame shown on the screen
+ * if the multi-threaded pipeline is used, in which case the request may take
+ * several frames extra to complete.
+ */
+PT(ScreenshotRequest) GraphicsOutput::
+get_async_screenshot() {
+  Thread *current_thread = Thread::get_current_thread();
+  CDWriter cdata(_cycler, current_thread);
+  if (cdata->_screenshot_request == nullptr) {
+    PT(Texture) texture = new Texture("screenshot of " + get_name());
+    cdata->_screenshot_request = new ScreenshotRequest(texture);
+  }
+  return cdata->_screenshot_request;
+}
+
 /**
  * Returns a PandaNode containing a square polygon.  The dimensions are
  * (-1,0,-1) to (1,0,1). The texture coordinates are such that the texture of
@@ -1468,6 +1505,56 @@ copy_to_textures() {
   return okflag;
 }
 
+/**
+ * Do the necessary copies for the get_async_screenshot request.
+ */
+void GraphicsOutput::
+copy_async_screenshot() {
+  Thread *current_thread = Thread::get_current_thread();
+  PT(ScreenshotRequest) request;
+  {
+    CDWriter cdata(_cycler, current_thread);
+    if (cdata->_screenshot_request == nullptr) {
+      return;
+    }
+    request = std::move(cdata->_screenshot_request);
+    cdata->_screenshot_request.clear();
+  }
+
+  // Make sure it is cleared from upstream stages as well.
+  OPEN_ITERATE_UPSTREAM_ONLY(_cycler, current_thread) {
+    CDStageWriter cdata(_cycler, pipeline_stage, current_thread);
+    if (cdata->_screenshot_request == request) {
+      cdata->_screenshot_request.clear();
+    }
+  }
+  CLOSE_ITERATE_UPSTREAM_ONLY(_cycler);
+
+  PStatTimer timer(_copy_texture_pcollector);
+
+  RenderBuffer buffer = _gsg->get_render_buffer(get_draw_buffer_type(),
+                                                get_fb_properties());
+  DisplayRegion *dr = _overlay_display_region;
+
+  Texture *texture = request->get_result();
+
+  if (_fb_properties.is_stereo()) {
+    // We've got two texture views to copy.
+    texture->set_num_views(2);
+
+    RenderBuffer left(_gsg, buffer._buffer_type & ~RenderBuffer::T_right);
+    RenderBuffer right(_gsg, buffer._buffer_type & ~RenderBuffer::T_left);
+
+    _gsg->framebuffer_copy_to_ram(texture, 0, _target_tex_page,
+                                  dr, left, request);
+    _gsg->framebuffer_copy_to_ram(texture, 1, _target_tex_page,
+                                  dr, right, request);
+  } else {
+    _gsg->framebuffer_copy_to_ram(texture, 0, _target_tex_page,
+                                  dr, buffer, request);
+  }
+}
+
 /**
  * Generates a GeomVertexData for a texture card.
  */
@@ -1653,7 +1740,8 @@ CData(const GraphicsOutput::CData &copy) :
   _active(copy._active),
   _one_shot_frame(copy._one_shot_frame),
   _active_display_regions(copy._active_display_regions),
-  _active_display_regions_stale(copy._active_display_regions_stale)
+  _active_display_regions_stale(copy._active_display_regions_stale),
+  _screenshot_request(copy._screenshot_request)
 {
 }
 

+ 7 - 0
panda/src/display/graphicsOutput.h

@@ -41,6 +41,7 @@
 #include "pipelineCycler.h"
 #include "updateSeq.h"
 #include "asyncFuture.h"
+#include "screenshotRequest.h"
 
 class PNMImage;
 class GraphicsEngine;
@@ -239,6 +240,9 @@ PUBLISHED:
       const Filename &filename, const std::string &image_comment = "");
   INLINE bool get_screenshot(PNMImage &image);
   INLINE PT(Texture) get_screenshot();
+  PT(ScreenshotRequest) save_async_screenshot(const Filename &filename,
+                                              const std::string &image_comment = "");
+  PT(ScreenshotRequest) get_async_screenshot();
 
   NodePath get_texture_card();
 
@@ -298,6 +302,7 @@ protected:
   void prepare_for_deletion();
   void promote_to_copy_texture();
   bool copy_to_textures();
+  void copy_async_screenshot();
 
   INLINE void begin_frame_spam(FrameMode mode);
   INLINE void end_frame_spam(FrameMode mode);
@@ -392,6 +397,8 @@ protected:
     int _one_shot_frame;
     ActiveDisplayRegions _active_display_regions;
     bool _active_display_regions_stale;
+
+    PT(ScreenshotRequest) _screenshot_request;
   };
   PipelineCycler<CData> _cycler;
   typedef CycleDataLockedReader<CData> CDLockedReader;

+ 3 - 1
panda/src/display/graphicsPipeSelection.h

@@ -52,7 +52,9 @@ PUBLISHED:
 
   INLINE static GraphicsPipeSelection *get_global_ptr();
 
+#ifdef HAVE_PYTHON
   EXTENSION(PyObject *__reduce__() const);
+#endif // HAVE_PYTHON
 
 public:
   typedef PT(GraphicsPipe) PipeConstructorFunc();
@@ -94,4 +96,4 @@ private:
 
 #include "graphicsPipeSelection.I"
 
-#endif
+#endif // !GRAPHICSPIPESELECTION_H

+ 5 - 1
panda/src/display/graphicsStateGuardian.cxx

@@ -3027,11 +3027,15 @@ framebuffer_copy_to_texture(Texture *, int, int, const DisplayRegion *,
  * into system memory, not texture memory.  Returns true on success, false on
  * failure.
  *
+ * If a future is given, the operation may be scheduled to occur in the
+ * background, in which case the texture will be passed as the result of the
+ * future when the operation is complete.
+ *
  * This completely redefines the ram image of the indicated texture.
  */
 bool GraphicsStateGuardian::
 framebuffer_copy_to_ram(Texture *, int, int, const DisplayRegion *,
-                        const RenderBuffer &) {
+                        const RenderBuffer &, ScreenshotRequest *) {
   return false;
 }
 

+ 10 - 7
panda/src/display/graphicsStateGuardian.h

@@ -256,7 +256,9 @@ PUBLISHED:
   MAKE_PROPERTY(texture_quality_override, get_texture_quality_override,
                                           set_texture_quality_override);
 
+#ifdef HAVE_PYTHON
   EXTENSION(PyObject *get_prepared_textures() const);
+#endif // HAVE_PYTHON
   typedef bool TextureCallback(TextureContext *tc, void *callback_arg);
   void traverse_prepared_textures(TextureCallback *func, void *callback_arg);
 
@@ -265,7 +267,7 @@ PUBLISHED:
   void clear_flash_texture();
   Texture *get_flash_texture() const;
   MAKE_PROPERTY(flash_texture, get_flash_texture, set_flash_texture);
-#endif
+#endif // !NDEBUG || !CPPPARSER
 
 PUBLISHED:
   virtual bool has_extension(const std::string &extension) const;
@@ -426,7 +428,8 @@ public:
   virtual bool framebuffer_copy_to_texture
   (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb);
   virtual bool framebuffer_copy_to_ram
-  (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb);
+  (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb,
+   ScreenshotRequest *request = nullptr);
 
   virtual void bind_light(PointLight *light_obj, const NodePath &light,
                           int light_id);
@@ -446,7 +449,7 @@ public:
 #ifdef DO_PSTATS
   static void init_frame_pstats();
   PStatThread get_pstats_thread();
-#endif
+#endif // DO_PSTATS
 
 protected:
   virtual void reissue_transforms();
@@ -603,7 +606,7 @@ protected:
 #ifdef DO_PSTATS
   int _pstats_gpu_thread;
   bool _timer_queries_active;
-#endif
+#endif // DO_PSTATS
 
   bool _copy_texture_inverted;
   bool _supports_multisample;
@@ -648,9 +651,9 @@ protected:
 
 #ifndef NDEBUG
   PT(Texture) _flash_texture;
-#else
+#else // !NDEBUG
   PT(Texture) _flash_texture_unused;
-#endif
+#endif // !NDEBUG
 
 public:
   // Statistics
@@ -761,4 +764,4 @@ EXPCL_PANDA_DISPLAY std::ostream &operator << (std::ostream &out, GraphicsStateG
 
 #include "graphicsStateGuardian.I"
 
-#endif
+#endif // !GRAPHICSSTATEGUARDIAN_H

+ 4 - 2
panda/src/display/graphicsWindow.h

@@ -56,7 +56,9 @@ PUBLISHED:
   void clear_rejected_properties();
   WindowProperties get_rejected_properties() const;
 
+#ifdef HAVE_PYTHON
   EXTENSION(void request_properties(PyObject *args, PyObject *kwds));
+#endif // HAVE_PYTHON
 
   INLINE bool is_closed() const;
   virtual bool is_active() const;
@@ -169,7 +171,7 @@ private:
 #ifdef HAVE_PYTHON
   typedef pset<GraphicsWindowProc*> PythonWinProcClasses;
   PythonWinProcClasses _python_window_proc_classes;
-#endif
+#endif // HAVE_PYTHON
 
 public:
   static TypeHandle get_class_type() {
@@ -194,4 +196,4 @@ private:
 
 #include "graphicsWindow.I"
 
-#endif /* GRAPHICSWINDOW_H */
+#endif // !GRAPHICSWINDOW_H

+ 1 - 0
panda/src/display/p3display_composite2.cxx

@@ -9,6 +9,7 @@
 #include "parasiteBuffer.cxx"
 #include "standardMunger.cxx"
 #include "touchInfo.cxx"
+#include "screenshotRequest.cxx"
 #include "stereoDisplayRegion.cxx"
 #include "subprocessWindow.cxx"
 #ifdef IS_OSX

+ 39 - 0
panda/src/display/screenshotRequest.I

@@ -0,0 +1,39 @@
+/**
+ * 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 screenshotRequest.I
+ * @author rdb
+ * @date 2022-12-26
+ */
+
+/**
+ *
+ */
+INLINE ScreenshotRequest::
+ScreenshotRequest(Texture *tex) :
+  _frame_number(ClockObject::get_global_clock()->get_frame_count()) {
+  _result = tex;
+  _result_ref = tex;
+}
+
+
+/**
+ * Returns the frame number in which the request originated.
+ */
+INLINE int ScreenshotRequest::
+get_frame_number() const {
+  return _frame_number;
+}
+
+/**
+ * Returns the resulting texture.  Can always be called.
+ */
+INLINE Texture *ScreenshotRequest::
+get_result() const {
+  return (Texture *)_result;
+}

+ 104 - 0
panda/src/display/screenshotRequest.cxx

@@ -0,0 +1,104 @@
+/**
+ * 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 screenshotRequest.cxx
+ * @author rdb
+ * @date 2022-12-26
+ */
+
+#include "screenshotRequest.h"
+#include "lightMutexHolder.h"
+#include "pnmImage.h"
+#include "texture.h"
+
+TypeHandle ScreenshotRequest::_type_handle;
+
+/**
+ *
+ */
+void ScreenshotRequest::
+set_view_data(int view, const void *ptr) {
+  const int z = 0;
+
+  Texture *tex = get_result();
+  PTA_uchar new_image = tex->modify_ram_image();
+  unsigned char *image_ptr = new_image.p();
+  size_t image_size = tex->get_ram_image_size();
+  if (z >= 0 || view > 0) {
+    image_size = tex->get_expected_ram_page_size();
+    if (z >= 0) {
+      image_ptr += z * image_size;
+    }
+    if (view > 0) {
+      image_ptr += (view * tex->get_z_size()) * image_size;
+      nassertd(view < tex->get_num_views()) {
+        if (set_future_state(FS_cancelled)) {
+          notify_done(false);
+        }
+        return;
+      }
+    }
+  }
+  memcpy(image_ptr, ptr, image_size);
+}
+
+/**
+ *
+ */
+void ScreenshotRequest::
+finish() {
+  Texture *tex = get_result();
+
+  ++_got_num_views;
+  if (_got_num_views < tex->get_num_views()) {
+    return;
+  }
+
+  {
+    LightMutexHolder holder(_lock);
+    if (!_output_files.empty()) {
+      PNMImage image;
+      tex->store(image);
+
+      for (const auto &item : _output_files) {
+        image.set_comment(item.second);
+        image.write(item.first);
+      }
+    }
+
+    AsyncFuture::set_result(tex);
+    _output_files.clear();
+
+    if (!set_future_state(FS_finished)) {
+      return;
+    }
+  }
+
+  notify_done(true);
+}
+
+/**
+ * Adds a filename to write the screenshot to when it is available.  If the
+ * request is already done, performs the write synchronously.
+ */
+void ScreenshotRequest::
+add_output_file(const Filename &filename, const std::string &image_comment) {
+  if (!done()) {
+    LightMutexHolder holder(_lock);
+    if (!done()) {
+      _output_files[filename] = image_comment;
+      return;
+    }
+  }
+  // Was already done, write it right away.
+  Texture *tex = get_result();
+  PNMImage image;
+  tex->store(image);
+  image.set_comment(image_comment);
+  image.write(filename);
+}

+ 73 - 0
panda/src/display/screenshotRequest.h

@@ -0,0 +1,73 @@
+/**
+ * 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 screenshotRequest.h
+ * @author rdb
+ * @date 2022-12-26
+ */
+
+#ifndef SCREENSHOTREQUEST_H
+#define SCREENSHOTREQUEST_H
+
+#include "pandabase.h"
+
+#include "asyncFuture.h"
+#include "clockObject.h"
+#include "filename.h"
+#include "lightMutex.h"
+#include "pmap.h"
+#include "texture.h"
+
+/**
+ * A class representing an asynchronous request to save a screenshot.
+ */
+class EXPCL_PANDA_DISPLAY ScreenshotRequest : public AsyncFuture {
+public:
+  INLINE ScreenshotRequest(Texture *tex);
+
+  INLINE int get_frame_number() const;
+  INLINE Texture *get_result() const;
+
+  void set_view_data(int view, const void *ptr);
+  void finish();
+
+PUBLISHED:
+  void add_output_file(const Filename &filename,
+                       const std::string &image_comment = "");
+
+private:
+  // It's possible to call save_screenshot multiple times in the same frame, so
+  // rather than have to store a vector of request objects, we just allow
+  // storing multiple filenames to handle this corner case.
+  LightMutex _lock;
+  pmap<Filename, std::string> _output_files;
+  int _got_num_views = 0;
+
+  int _frame_number = 0;
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    AsyncFuture::init_type();
+    register_type(_type_handle, "ScreenshotRequest",
+                  AsyncFuture::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 "screenshotRequest.I"
+
+#endif

+ 15 - 11
panda/src/display/windowProperties.h

@@ -44,7 +44,9 @@ PUBLISHED:
     M_confined,
   };
 
+#ifdef HAVE_PYTHON
   EXTENSION(WindowProperties(PyObject *self, PyObject *args, PyObject *kwds));
+#endif // HAVE_PYTHON
 
 PUBLISHED:
   void operator = (const WindowProperties &copy);
@@ -70,9 +72,9 @@ PUBLISHED:
   INLINE void set_origin(int x_origin, int y_origin);
 #ifdef CPPPARSER
   INLINE LPoint2i get_origin() const;
-#else
+#else // CPPPARSER
   INLINE const LPoint2i &get_origin() const;
-#endif
+#endif // CPPPARSER
   INLINE int get_x_origin() const;
   INLINE int get_y_origin() const;
   INLINE bool has_origin() const;
@@ -83,9 +85,9 @@ PUBLISHED:
   INLINE void set_size(int x_size, int y_size);
 #ifdef CPPPARSER
   INLINE LVector2i get_size() const;
-#else
+#else // CPPPARSER
   INLINE const LVector2i &get_size() const;
-#endif
+#endif // CPPPARSER
   INLINE int get_x_size() const;
   INLINE int get_y_size() const;
   INLINE bool has_size() const;
@@ -102,9 +104,9 @@ PUBLISHED:
   INLINE void set_title(const std::string &title);
 #ifdef CPPPARSER
   INLINE std::string get_title() const;
-#else
+#else // CPPPARSER
   INLINE const std::string &get_title() const;
-#endif
+#endif // CPPPARSER
   INLINE bool has_title() const;
   INLINE void clear_title();
   MAKE_PROPERTY2(title, has_title, get_title, set_title, clear_title);
@@ -172,9 +174,9 @@ PUBLISHED:
   INLINE void set_icon_filename(const Filename &icon_filename);
 #ifdef CPPPARSER
   INLINE Filename get_icon_filename() const;
-#else
+#else // CPPPARSER
   INLINE const Filename &get_icon_filename() const;
-#endif
+#endif // CPPPARSER
   INLINE bool has_icon_filename() const;
   INLINE void clear_icon_filename();
   MAKE_PROPERTY2(icon_filename, has_icon_filename, get_icon_filename,
@@ -183,9 +185,9 @@ PUBLISHED:
   INLINE void set_cursor_filename(const Filename &cursor_filename);
 #ifdef CPPPARSER
   INLINE Filename get_cursor_filename() const;
-#else
+#else // CPPPARSER
   INLINE const Filename &get_cursor_filename() const;
-#endif
+#endif // CPPPARSER
   INLINE bool has_cursor_filename() const;
   INLINE void clear_cursor_filename();
   MAKE_PROPERTY2(cursor_filename, has_cursor_filename, get_cursor_filename,
@@ -205,8 +207,10 @@ PUBLISHED:
   MAKE_PROPERTY2(parent_window, has_parent_window, get_parent_window,
                                 set_parent_window, clear_parent_window);
 
+#ifdef HAVE_PYTHON
   EXTENSION(PyObject *__getstate__(PyObject *self) const);
   EXTENSION(void __setstate__(PyObject *self, PyObject *state));
+#endif // HAVE_PYTHON
 
   void add_properties(const WindowProperties &other);
 
@@ -278,4 +282,4 @@ INLINE std::ostream &operator << (std::ostream &out, const WindowProperties &pro
 
 #include "windowProperties.I"
 
-#endif
+#endif // !WINDOWPROPERTIES_H

Неке датотеке нису приказане због велике количине промена