Browse Source

Merge branch 'release/1.10.x' into incoming

rdb 3 years ago
parent
commit
a5610f217e

+ 40 - 8
.github/workflows/ci.yml

@@ -347,9 +347,9 @@ jobs:
       shell: powershell
       shell: powershell
       run: |
       run: |
         $wc = New-Object System.Net.WebClient
         $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
         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)
     - name: Get thirdparty packages (macOS)
       if: runner.os == 'macOS'
       if: runner.os == 'macOS'
       run: |
       run: |
@@ -358,10 +358,39 @@ jobs:
         mv panda3d-1.10.13/thirdparty thirdparty
         mv panda3d-1.10.13/thirdparty thirdparty
         rmdir panda3d-1.10.13
         rmdir panda3d-1.10.13
         (cd thirdparty/darwin-libs-a && rm -rf rocket)
         (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
     - name: Set up Python 3.9
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
       with:
-        python-version: 3.9
+        python-version: '3.9'
     - name: Build Python 3.9
     - name: Build Python 3.9
       shell: bash
       shell: bash
       run: |
       run: |
@@ -371,10 +400,11 @@ jobs:
       run: |
       run: |
         python -m pip install pytest
         python -m pip install pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+
     - name: Set up Python 3.8
     - name: Set up Python 3.8
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
       with:
-        python-version: 3.8
+        python-version: '3.8'
     - name: Build Python 3.8
     - name: Build Python 3.8
       shell: bash
       shell: bash
       run: |
       run: |
@@ -384,10 +414,11 @@ jobs:
       run: |
       run: |
         python -m pip install pytest
         python -m pip install pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+
     - name: Set up Python 3.7
     - name: Set up Python 3.7
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
       with:
-        python-version: 3.7
+        python-version: '3.7'
     - name: Build Python 3.7
     - name: Build Python 3.7
       shell: bash
       shell: bash
       run: |
       run: |
@@ -397,6 +428,7 @@ jobs:
       run: |
       run: |
         python -m pip install pytest
         python -m pip install pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
         PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+
     - name: Make installer
     - name: Make installer
       run: |
       run: |
         python makepanda/makepackage.py --verbose --lzma
         python makepanda/makepackage.py --verbose --lzma

+ 2 - 2
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
 [click here](https://github.com/rdb/panda3d-thirdparty) for instructions on
 building them from source.
 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
 After acquiring these dependencies, you can build Panda3D from the command
 prompt using the following command.  Change the `--msvc-version` option based
 prompt using the following command.  Change the `--msvc-version` option based

+ 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
     // Reset the film size, offset and far-plane
     Camera* cam = DCAST(Camera, _cam_nodes[i].node());
     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
     // Find a good initial position
     _cam_nodes[i].set_pos(cam_start);
     _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_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());
       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 {
     } else {
       // If we don't use a fixed film size, we can just set the film size
       // If we don't use a fixed film size, we can just set the film size
       // on the lens.
       // 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
     // 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());
     _camera_nearfar[i] = LVecBase2(10, best_max_extent.get_z());
 
 
     // Compute the camera MVP
     // Compute the camera MVP
@@ -399,7 +400,8 @@ void PSSMCameraRig::update(NodePath cam_node, const LVecBase3 &light_vector) {
   }
   }
 
 
   // Do the actual PSSM
   // 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();
   _update_collector.stop();
 }
 }

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

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

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

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

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

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

+ 12 - 0
makepanda/installer.nsi

@@ -375,6 +375,7 @@ SectionGroup "Python modules" SecGroupPython
         !insertmacro PyBindingSection 3.9-32 .cp39-win32.pyd
         !insertmacro PyBindingSection 3.9-32 .cp39-win32.pyd
         !insertmacro PyBindingSection 3.10-32 .cp310-win32.pyd
         !insertmacro PyBindingSection 3.10-32 .cp310-win32.pyd
         !insertmacro PyBindingSection 3.11-32 .cp311-win32.pyd
         !insertmacro PyBindingSection 3.11-32 .cp311-win32.pyd
+        !insertmacro PyBindingSection 3.12-32 .cp312-win32.pyd
     !else
     !else
         !insertmacro PyBindingSection 3.5 .cp35-win_amd64.pyd
         !insertmacro PyBindingSection 3.5 .cp35-win_amd64.pyd
         !insertmacro PyBindingSection 3.6 .cp36-win_amd64.pyd
         !insertmacro PyBindingSection 3.6 .cp36-win_amd64.pyd
@@ -383,6 +384,7 @@ SectionGroup "Python modules" SecGroupPython
         !insertmacro PyBindingSection 3.9 .cp39-win_amd64.pyd
         !insertmacro PyBindingSection 3.9 .cp39-win_amd64.pyd
         !insertmacro PyBindingSection 3.10 .cp310-win_amd64.pyd
         !insertmacro PyBindingSection 3.10 .cp310-win_amd64.pyd
         !insertmacro PyBindingSection 3.11 .cp311-win_amd64.pyd
         !insertmacro PyBindingSection 3.11 .cp311-win_amd64.pyd
+        !insertmacro PyBindingSection 3.12 .cp312-win_amd64.pyd
     !endif
     !endif
 SectionGroupEnd
 SectionGroupEnd
 
 
@@ -492,6 +494,7 @@ Function .onInit
         !insertmacro MaybeEnablePyBindingSection 3.9-32
         !insertmacro MaybeEnablePyBindingSection 3.9-32
         !insertmacro MaybeEnablePyBindingSection 3.10-32
         !insertmacro MaybeEnablePyBindingSection 3.10-32
         !insertmacro MaybeEnablePyBindingSection 3.11-32
         !insertmacro MaybeEnablePyBindingSection 3.11-32
+        !insertmacro MaybeEnablePyBindingSection 3.12-32
         ${EndIf}
         ${EndIf}
     !else
     !else
         !insertmacro MaybeEnablePyBindingSection 3.5
         !insertmacro MaybeEnablePyBindingSection 3.5
@@ -502,6 +505,7 @@ Function .onInit
         !insertmacro MaybeEnablePyBindingSection 3.9
         !insertmacro MaybeEnablePyBindingSection 3.9
         !insertmacro MaybeEnablePyBindingSection 3.10
         !insertmacro MaybeEnablePyBindingSection 3.10
         !insertmacro MaybeEnablePyBindingSection 3.11
         !insertmacro MaybeEnablePyBindingSection 3.11
+        !insertmacro MaybeEnablePyBindingSection 3.12
         ${EndIf}
         ${EndIf}
     !endif
     !endif
 
 
@@ -519,6 +523,10 @@ Function .onInit
         SectionSetFlags ${SecPyBindings3.11} ${SF_RO}
         SectionSetFlags ${SecPyBindings3.11} ${SF_RO}
         SectionSetInstTypes ${SecPyBindings3.11} 0
         SectionSetInstTypes ${SecPyBindings3.11} 0
     !endif
     !endif
+    !ifdef SecPyBindings3.12
+        SectionSetFlags ${SecPyBindings3.12} ${SF_RO}
+        SectionSetInstTypes ${SecPyBindings3.12} 0
+    !endif
     ${EndUnless}
     ${EndUnless}
 FunctionEnd
 FunctionEnd
 
 
@@ -831,6 +839,7 @@ Section Uninstall
         !insertmacro RemovePythonPath 3.9-32
         !insertmacro RemovePythonPath 3.9-32
         !insertmacro RemovePythonPath 3.10-32
         !insertmacro RemovePythonPath 3.10-32
         !insertmacro RemovePythonPath 3.11-32
         !insertmacro RemovePythonPath 3.11-32
+        !insertmacro RemovePythonPath 3.12-32
     !else
     !else
         !insertmacro RemovePythonPath 3.5
         !insertmacro RemovePythonPath 3.5
         !insertmacro RemovePythonPath 3.6
         !insertmacro RemovePythonPath 3.6
@@ -839,6 +848,7 @@ Section Uninstall
         !insertmacro RemovePythonPath 3.9
         !insertmacro RemovePythonPath 3.9
         !insertmacro RemovePythonPath 3.10
         !insertmacro RemovePythonPath 3.10
         !insertmacro RemovePythonPath 3.11
         !insertmacro RemovePythonPath 3.11
+        !insertmacro RemovePythonPath 3.12
     !endif
     !endif
 
 
     SetDetailsPrint both
     SetDetailsPrint both
@@ -908,6 +918,7 @@ SectionEnd
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.9-32} $(DESC_SecPyBindings3.9-32)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.9-32} $(DESC_SecPyBindings3.9-32)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.10-32} $(DESC_SecPyBindings3.10-32)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.10-32} $(DESC_SecPyBindings3.10-32)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.11-32} $(DESC_SecPyBindings3.11-32)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.11-32} $(DESC_SecPyBindings3.11-32)
+    !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.12-32} $(DESC_SecPyBindings3.12-32)
   !else
   !else
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.5} $(DESC_SecPyBindings3.5)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.5} $(DESC_SecPyBindings3.5)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.6} $(DESC_SecPyBindings3.6)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.6} $(DESC_SecPyBindings3.6)
@@ -916,6 +927,7 @@ SectionEnd
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.9} $(DESC_SecPyBindings3.9)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.9} $(DESC_SecPyBindings3.9)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.10} $(DESC_SecPyBindings3.10)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.10} $(DESC_SecPyBindings3.10)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.11} $(DESC_SecPyBindings3.11)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.11} $(DESC_SecPyBindings3.11)
+    !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.12} $(DESC_SecPyBindings3.12)
   !endif
   !endif
   !ifdef INCLUDE_PYVER
   !ifdef INCLUDE_PYVER
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPython} $(DESC_SecPython)
     !insertmacro MUI_DESCRIPTION_TEXT ${SecPython} $(DESC_SecPython)

+ 2 - 0
makepanda/makepanda.py

@@ -3815,6 +3815,7 @@ IGATEFILES=GetDirectoryContents('panda/src/pstatclient', ["*.h", "*_composite*.c
 IGATEFILES.remove("config_pstats.h")
 IGATEFILES.remove("config_pstats.h")
 TargetAdd('libp3pstatclient.in', opts=OPTS, input=IGATEFILES)
 TargetAdd('libp3pstatclient.in', opts=OPTS, input=IGATEFILES)
 TargetAdd('libp3pstatclient.in', opts=['IMOD:panda3d.core', 'ILIB:libp3pstatclient', 'SRCDIR:panda/src/pstatclient'])
 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/
 # DIRECTORY: panda/src/gobj/
@@ -4244,6 +4245,7 @@ PyTargetAdd('core.pyd', input='p3putil_ext_composite.obj')
 PyTargetAdd('core.pyd', input='p3pnmimage_pfmFile_ext.obj')
 PyTargetAdd('core.pyd', input='p3pnmimage_pfmFile_ext.obj')
 PyTargetAdd('core.pyd', input='p3event_asyncFuture_ext.obj')
 PyTargetAdd('core.pyd', input='p3event_asyncFuture_ext.obj')
 PyTargetAdd('core.pyd', input='p3event_pythonTask.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='p3gobj_ext_composite.obj')
 PyTargetAdd('core.pyd', input='p3pgraph_ext_composite.obj')
 PyTargetAdd('core.pyd', input='p3pgraph_ext_composite.obj')
 PyTargetAdd('core.pyd', input='p3display_ext_composite.obj')
 PyTargetAdd('core.pyd', input='p3display_ext_composite.obj')

+ 1 - 1
makepanda/makepandacore.py

@@ -433,7 +433,7 @@ def SetTarget(target, arch=None):
             ANDROID_ABI = 'x86_64'
             ANDROID_ABI = 'x86_64'
             ANDROID_TRIPLE = 'x86_64-linux-android'
             ANDROID_TRIPLE = 'x86_64-linux-android'
         else:
         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)
         ANDROID_TRIPLE += str(ANDROID_API)
         TOOLCHAIN_PREFIX = ANDROID_TRIPLE + '-'
         TOOLCHAIN_PREFIX = ANDROID_TRIPLE + '-'

+ 5 - 7
panda/src/gobj/texture.cxx

@@ -381,8 +381,7 @@ Texture(const string &name) :
   _reloading = false;
   _reloading = false;
 
 
   CDWriter cdata(_cycler, true);
   CDWriter cdata(_cycler, true);
-  do_set_format(cdata, F_rgb);
-  do_set_component_type(cdata, T_unsigned_byte);
+  cdata->inc_properties_modified();
 }
 }
 
 
 /**
 /**
@@ -10782,11 +10781,10 @@ CData() {
   _y_size = 1;
   _y_size = 1;
   _z_size = 1;
   _z_size = 1;
   _num_views = 1;
   _num_views = 1;
-
-  // We will override the format in a moment (in the Texture constructor), but
-  // set it to something else first to avoid the check in do_set_format
-  // depending on an uninitialized value.
-  _format = F_rgba;
+  _num_components = 3;
+  _component_width = 1;
+  _format = F_rgb;
+  _component_type = T_unsigned_byte;
 
 
   // Only used for buffer textures.
   // Only used for buffer textures.
   _usage_hint = GeomEnums::UH_unspecified;
   _usage_hint = GeomEnums::UH_unspecified;

+ 7 - 1
panda/src/pstatclient/CMakeLists.txt

@@ -22,6 +22,12 @@ set(P3PSTATCLIENT_SOURCES
   pStatThread.cxx
   pStatThread.cxx
 )
 )
 
 
+set(P3PSTATCLIENT_IGATEEXT
+  pStatClient_ext.I
+  pStatClient_ext.cxx
+  pStatClient_ext.h
+)
+
 composite_sources(p3pstatclient P3PSTATCLIENT_SOURCES)
 composite_sources(p3pstatclient P3PSTATCLIENT_SOURCES)
 add_component_library(p3pstatclient SYMBOL BUILDING_PANDA_PSTATCLIENT
 add_component_library(p3pstatclient SYMBOL BUILDING_PANDA_PSTATCLIENT
   ${P3PSTATCLIENT_HEADERS} ${P3PSTATCLIENT_SOURCES})
   ${P3PSTATCLIENT_HEADERS} ${P3PSTATCLIENT_SOURCES})
@@ -31,7 +37,7 @@ if(HAVE_NET AND WANT_NATIVE_NET)
   target_link_libraries(p3pstatclient p3net)
   target_link_libraries(p3pstatclient p3net)
 endif()
 endif()
 
 
-target_interrogate(p3pstatclient ALL)
+target_interrogate(p3pstatclient ALL EXTENSIONS ${P3PSTATCLIENT_IGATEEXT})
 
 
 if(NOT BUILD_METALIBS)
 if(NOT BUILD_METALIBS)
   install(TARGETS p3pstatclient
   install(TARGETS p3pstatclient

+ 10 - 0
panda/src/pstatclient/config_pstatclient.cxx

@@ -87,6 +87,16 @@ ConfigVariableBool pstats_thread_profiling
  PRC_DESC("Set this true to query the system for thread statistics, such as "
  PRC_DESC("Set this true to query the system for thread statistics, such as "
           "the number of context switches and time spent waiting."));
           "the number of context switches and time spent waiting."));
 
 
+ConfigVariableBool pstats_python_profiler
+("pstats-python-profiler", false,
+ PRC_DESC("Set this true to integrate with the Python profiler to show "
+          "detailed information about individual Python functions in "
+          "PStats, similar to the information offered by Python's built-in "
+          "profiler.  This can be really useful to find bottlenecks in a "
+          "Python program, but enabling this will slow down the application "
+          "somewhat, and requires a recent version of the PStats server, so "
+          "it is not enabled by default."));
+
 // The rest are different in that they directly control the server, not the
 // The rest are different in that they directly control the server, not the
 // client.
 // client.
 ConfigVariableBool pstats_scroll_mode
 ConfigVariableBool pstats_scroll_mode

+ 1 - 0
panda/src/pstatclient/config_pstatclient.h

@@ -39,6 +39,7 @@ extern EXPCL_PANDA_PSTATCLIENT ConfigVariableInt pstats_port;
 extern EXPCL_PANDA_PSTATCLIENT ConfigVariableDouble pstats_target_frame_rate;
 extern EXPCL_PANDA_PSTATCLIENT ConfigVariableDouble pstats_target_frame_rate;
 extern EXPCL_PANDA_PSTATCLIENT ConfigVariableBool pstats_gpu_timing;
 extern EXPCL_PANDA_PSTATCLIENT ConfigVariableBool pstats_gpu_timing;
 extern EXPCL_PANDA_PSTATCLIENT ConfigVariableBool pstats_thread_profiling;
 extern EXPCL_PANDA_PSTATCLIENT ConfigVariableBool pstats_thread_profiling;
+extern EXPCL_PANDA_PSTATCLIENT ConfigVariableBool pstats_python_profiler;
 
 
 extern EXPCL_PANDA_PSTATCLIENT ConfigVariableBool pstats_scroll_mode;
 extern EXPCL_PANDA_PSTATCLIENT ConfigVariableBool pstats_scroll_mode;
 extern EXPCL_PANDA_PSTATCLIENT ConfigVariableDouble pstats_history;
 extern EXPCL_PANDA_PSTATCLIENT ConfigVariableDouble pstats_history;

+ 7 - 4
panda/src/pstatclient/pStatClient.h

@@ -29,6 +29,7 @@
 #include "patomic.h"
 #include "patomic.h"
 #include "numeric_types.h"
 #include "numeric_types.h"
 #include "bitArray.h"
 #include "bitArray.h"
+#include "extension.h"
 
 
 class PStatClientImpl;
 class PStatClientImpl;
 class PStatCollector;
 class PStatCollector;
@@ -88,8 +89,8 @@ PUBLISHED:
   MAKE_PROPERTY(current_thread, get_current_thread);
   MAKE_PROPERTY(current_thread, get_current_thread);
   MAKE_PROPERTY(real_time, get_real_time);
   MAKE_PROPERTY(real_time, get_real_time);
 
 
-  INLINE static bool connect(const std::string &hostname = std::string(), int port = -1);
-  INLINE static void disconnect();
+  EXTEND INLINE static bool connect(const std::string &hostname = std::string(), int port = -1);
+  EXTEND INLINE static void disconnect();
   INLINE static bool is_connected();
   INLINE static bool is_connected();
 
 
   INLINE static void resume_after_pause();
   INLINE static void resume_after_pause();
@@ -101,8 +102,8 @@ PUBLISHED:
   void client_main_tick();
   void client_main_tick();
   void client_thread_tick();
   void client_thread_tick();
   void client_thread_tick(const std::string &sync_name);
   void client_thread_tick(const std::string &sync_name);
-  bool client_connect(std::string hostname, int port);
-  void client_disconnect();
+  EXTEND bool client_connect(std::string hostname, int port);
+  EXTEND void client_disconnect();
   bool client_is_connected() const;
   bool client_is_connected() const;
 
 
   void client_resume_after_pause();
   void client_resume_after_pause();
@@ -258,6 +259,8 @@ private:
   friend class PStatThread;
   friend class PStatThread;
   friend class PStatClientImpl;
   friend class PStatClientImpl;
   friend class GraphicsStateGuardian;
   friend class GraphicsStateGuardian;
+
+  friend class Extension<PStatClient>;
 };
 };
 
 
 #include "pStatClient.I"
 #include "pStatClient.I"

+ 7 - 0
panda/src/pstatclient/pStatClientImpl.cxx

@@ -566,6 +566,13 @@ send_hello() {
   message._major_version = get_current_pstat_major_version();
   message._major_version = get_current_pstat_major_version();
   message._minor_version = get_current_pstat_minor_version();
   message._minor_version = get_current_pstat_minor_version();
 
 
+  // The Python profiling feature may send nested start/stop pairs, so requires
+  // a server version capable of dealing with this.
+  if (pstats_python_profiler && message._major_version <= 3) {
+    message._major_version = 3;
+    message._minor_version = std::max(message._minor_version, 1);
+  }
+
   Datagram datagram;
   Datagram datagram;
   message.encode(datagram);
   message.encode(datagram);
   _writer.send(datagram, _tcp_connection, true);
   _writer.send(datagram, _tcp_connection, true);

+ 31 - 0
panda/src/pstatclient/pStatClient_ext.I

@@ -0,0 +1,31 @@
+/**
+ * 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 pStatClient_ext.I
+ * @author rdb
+ * @date 2022-11-29
+ */
+
+/**
+ * Attempts to establish a connection to the indicated PStatServer.  Returns
+ * true if successful, false on failure.
+ */
+INLINE bool Extension<PStatClient>::
+connect(const std::string &hostname, int port) {
+  PStatClient *client = PStatClient::get_global_pstats();
+  return invoke_extension<PStatClient>(client).client_connect(hostname, port);
+}
+
+/**
+ * Closes the connection previously established.
+ */
+INLINE void Extension<PStatClient>::
+disconnect() {
+  PStatClient *client = PStatClient::get_global_pstats();
+  invoke_extension<PStatClient>(client).client_disconnect();
+}

+ 336 - 0
panda/src/pstatclient/pStatClient_ext.cxx

@@ -0,0 +1,336 @@
+/**
+ * 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 pStatClient_ext.cxx
+ * @author rdb
+ * @date 2022-11-23
+ */
+
+#include "pStatClient_ext.h"
+
+#if defined(HAVE_PYTHON) && defined(DO_PSTATS)
+
+#include "pStatCollector.h"
+#include "config_pstatclient.h"
+
+#ifndef CPPPARSER
+#include "frameobject.h"
+#endif
+
+static bool _python_profiler_enabled = false;
+
+// Used to cache stuff onto PyCodeObjects.
+static Py_ssize_t _extra_index = -1;
+
+// Stores a mapping between C method definitions and collector indices.
+static pmap<PyMethodDef *, int> _c_method_collectors;
+
+// Parent collector for all Python profiling collectors.
+static PStatCollector code_collector("App:Python");
+
+/**
+ * Walks up the type hierarchy to find the class where the method originates.
+ */
+static bool
+find_method(PyTypeObject *&cls, PyObject *name, PyCodeObject *code) {
+  PyObject *meth = _PyType_Lookup(cls, name);
+  if (meth == nullptr || !PyFunction_Check(meth) ||
+      PyFunction_GET_CODE(meth) != (PyObject *)code) {
+    return false;
+  }
+
+  if (cls->tp_bases != nullptr) {
+    Py_ssize_t size = PyTuple_GET_SIZE(cls->tp_bases);
+    for (Py_ssize_t i = 0; i < size; ++i) {
+      PyTypeObject *base = (PyTypeObject *)PyTuple_GET_ITEM(cls->tp_bases, i);
+
+      if (find_method(base, name, code)) {
+        cls = base;
+        return true;
+      }
+    }
+  }
+
+  // Didn't find it in any of the bases, it must be defined here.
+  return true;
+}
+
+/**
+ * Returns the collector for a Python frame.
+ */
+static int
+#ifdef __GNUC__
+__attribute__ ((noinline))
+#elif defined(_MSC_VER)
+__declspec(noinline)
+#endif
+make_python_frame_collector(PyFrameObject *frame, PyCodeObject *code) {
+#if PY_VERSION_HEX >= 0x030B0000 // 3.11
+  // Fetch the module name out of the frame's global scope.
+  PyObject *globals = PyFrame_GetGlobals(frame);
+  PyObject *py_mod_name = PyDict_GetItemString(globals, "__name__");
+  Py_DECREF(globals);
+
+  const char *mod_name = py_mod_name ? PyUnicode_AsUTF8(py_mod_name) : "<unknown>";
+  const char *meth_name = PyUnicode_AsUTF8(code->co_qualname);
+  char buffer[1024];
+  size_t len = snprintf(buffer, sizeof(buffer), "%s:%s", mod_name, meth_name);
+  for (size_t i = 0; i < len - 1; ++i) {
+    if (buffer[i] == '.') {
+      buffer[i] = ':';
+    }
+  }
+#else
+  // Try to figure out the type name.  There's no obvious way to do this.
+  // It's possible that the first argument passed to this function is the
+  // self instance or the current type (for a classmethod), but we have to
+  // double-check that to make sure.
+  PyTypeObject *cls = nullptr;
+  if (code->co_argcount >= 1) {
+    PyFrame_FastToLocals(frame);
+    PyObject *first_arg = PyDict_GetItem(frame->f_locals, PyTuple_GET_ITEM(code->co_varnames, 0));
+    cls = PyType_Check(first_arg) ? (PyTypeObject *)first_arg : Py_TYPE(first_arg);
+    if ((cls->tp_flags & Py_TPFLAGS_HEAPTYPE) != 0) {
+      // Mangling scheme for methods starting (but not ending) with "__"
+      PyObject *meth_name = code->co_name;
+      Py_ssize_t len = PyUnicode_GET_LENGTH(meth_name);
+      if (len >= 2 && PyUnicode_READ_CHAR(meth_name, 0) == '_' && PyUnicode_READ_CHAR(meth_name, 1) == '_' &&
+          (len < 4 || PyUnicode_READ_CHAR(meth_name, len - 1) != '_' || PyUnicode_READ_CHAR(meth_name, len - 2) != '_')) {
+        const char *cls_name = cls->tp_name;
+        while (cls_name[0] == '_') {
+          ++cls_name;
+        }
+        meth_name = PyUnicode_FromFormat("_%s%S", cls_name, meth_name);
+      } else {
+        Py_INCREF(meth_name);
+      }
+      if (!find_method(cls, meth_name, code)) {
+        // Not a matching method object, it's something else.  Forget it.
+        cls = nullptr;
+      }
+      Py_DECREF(meth_name);
+    } else {
+      cls = nullptr;
+    }
+  }
+
+  // Fetch the module name out of the frame's global scope.
+  PyObject *py_mod_name = PyDict_GetItemString(frame->f_globals, "__name__");
+  if (py_mod_name == nullptr && cls != nullptr) {
+    py_mod_name = PyDict_GetItemString(cls->tp_dict, "__module__");
+  }
+
+  const char *mod_name = py_mod_name ? PyUnicode_AsUTF8(py_mod_name) : "<unknown>";
+  char buffer[1024];
+  size_t len = snprintf(buffer, sizeof(buffer), "%s:", mod_name);
+  for (size_t i = 0; i < len - 1; ++i) {
+    if (buffer[i] == '.') {
+      buffer[i] = ':';
+    }
+  }
+
+  const char *meth_name = PyUnicode_AsUTF8(code->co_name);
+  if (cls != nullptr) {
+    len += snprintf(buffer + len, sizeof(buffer) - len, "%s:%s", cls->tp_name, meth_name);
+  } else {
+    len += snprintf(buffer + len, sizeof(buffer) - len, "%s", meth_name);
+  }
+#endif
+
+  // Add parentheses, unless it's something special like <listcomp>
+  if (len < sizeof(buffer) - 2 && buffer[len - 1] != '>') {
+    buffer[len++] = '(';
+    buffer[len++] = ')';
+    buffer[len] = '\0';
+  }
+
+  PStatCollector collector(code_collector, buffer);
+  intptr_t collector_index = collector.get_index();
+  if (_extra_index != -1) {
+    _PyCode_SetExtra((PyObject *)code, _extra_index, (void *)collector_index);
+  }
+  return collector_index;
+}
+
+/**
+ * Creates a collector for a C function.
+ */
+static int
+#ifdef __GNUC__
+__attribute__ ((noinline))
+#elif defined(_MSC_VER)
+__declspec(noinline)
+#endif
+make_c_function_collector(PyCFunctionObject *meth) {
+  char buffer[1024];
+  size_t len;
+  if (meth->m_self != nullptr && !PyModule_Check(meth->m_self)) {
+    PyTypeObject *cls = PyType_Check(meth->m_self) ? (PyTypeObject *)meth->m_self : Py_TYPE(meth->m_self);
+
+    const char *dot = strrchr(cls->tp_name, '.');
+    if (dot != nullptr) {
+      // The module name is included in the type name.
+      snprintf(buffer, sizeof(buffer), "%s:%s()", cls->tp_name, meth->m_ml->ml_name);
+      len = (dot - cls->tp_name) + 1;
+    } else {
+      // If there's no module name, we need to get it from __module__.
+      PyObject *py_mod_name = cls->tp_dict ? PyDict_GetItemString(cls->tp_dict, "__module__") : nullptr;
+      const char *mod_name;
+      if (py_mod_name != nullptr) {
+        mod_name = PyUnicode_AsUTF8(py_mod_name);
+      } else {
+        // Is it a built-in, like int or dict?
+        PyObject *builtins = PyEval_GetBuiltins();
+        if (PyDict_GetItemString(builtins, cls->tp_name) == (PyObject *)cls) {
+          mod_name = "builtins";
+        } else {
+          mod_name = "<unknown>";
+        }
+      }
+      len = snprintf(buffer, sizeof(buffer), "%s:%s:%s()", mod_name, cls->tp_name, meth->m_ml->ml_name) - 2;
+    }
+  }
+  else if (meth->m_self != nullptr) {
+    const char *mod_name = PyModule_GetName(meth->m_self);
+    len = snprintf(buffer, sizeof(buffer), "%s:%s()", mod_name, meth->m_ml->ml_name) - 2;
+  }
+  else {
+    snprintf(buffer, sizeof(buffer), "%s()", meth->m_ml->ml_name);
+    len = 0;
+  }
+  for (size_t i = 0; i < len; ++i) {
+    if (buffer[i] == '.') {
+      buffer[i] = ':';
+    }
+  }
+  PStatCollector collector(code_collector, buffer);
+  int collector_index = collector.get_index();
+  _c_method_collectors[meth->m_ml] = collector.get_index();
+  return collector_index;
+}
+
+/**
+ * Attempts to establish a connection to the indicated PStatServer.  Returns
+ * true if successful, false on failure.
+ */
+bool Extension<PStatClient>::
+client_connect(std::string hostname, int port) {
+  extern struct Dtool_PyTypedObject Dtool_PStatThread;
+
+  if (_this->client_connect(std::move(hostname), port)) {
+    // Pass a PStatThread as argument.
+    if (!_python_profiler_enabled && pstats_python_profiler) {
+      PStatThread *thread = new PStatThread(_this->get_current_thread());
+      PyObject *arg = DTool_CreatePyInstance((void *)thread, Dtool_PStatThread, true, false);
+      if (_extra_index == -1) {
+        _extra_index = _PyEval_RequestCodeExtraIndex(nullptr);
+      }
+      PyEval_SetProfile(&trace_callback, arg);
+      _python_profiler_enabled = false;
+    }
+    return true;
+  }
+  else if (_python_profiler_enabled) {
+    PyEval_SetProfile(nullptr, nullptr);
+    _python_profiler_enabled = false;
+  }
+  return false;
+}
+
+/**
+ * Closes the connection previously established.
+ */
+void Extension<PStatClient>::
+client_disconnect() {
+  _this->client_disconnect();
+  if (_python_profiler_enabled) {
+    PyEval_SetProfile(nullptr, nullptr);
+    _python_profiler_enabled = false;
+  }
+}
+
+/**
+ * Callback passed to PyEval_SetProfile.
+ */
+int Extension<PStatClient>::
+trace_callback(PyObject *py_thread, PyFrameObject *frame, int what, PyObject *arg) {
+  intptr_t collector_index;
+
+  if (what == PyTrace_CALL || what == PyTrace_RETURN || what == PyTrace_EXCEPTION) {
+    // Normal Python frame entry/exit.
+#if PY_VERSION_HEX >= 0x030B0000 // 3.11
+    PyCodeObject *code = PyFrame_GetCode(frame);
+#else
+    PyCodeObject *code = frame->f_code;
+#endif
+
+    // The index for this collector is cached on the code object.
+    if (_PyCode_GetExtra((PyObject *)code, _extra_index, (void **)&collector_index) != 0 || collector_index == 0) {
+      collector_index = make_python_frame_collector(frame, code);
+    }
+
+#if PY_VERSION_HEX >= 0x030B0000 // 3.11
+    Py_DECREF(code);
+#endif
+  } else if (what == PyTrace_C_CALL || what == PyTrace_C_RETURN || what == PyTrace_C_EXCEPTION) {
+    // Call to a C function or method, which has no frame of its own.
+    if (PyCFunction_CheckExact(arg)) {
+      PyCFunctionObject *meth = (PyCFunctionObject *)arg;
+      auto it = _c_method_collectors.find(meth->m_ml);
+      if (it != _c_method_collectors.end()) {
+        collector_index = it->second;
+      } else {
+        collector_index = make_c_function_collector(meth);
+      }
+    } else {
+      return 0;
+    }
+  } else {
+    return 0;
+  }
+
+  if (collector_index <= 0) {
+    return 0;
+  }
+
+  PStatThread &pthread = *(PStatThread *)DtoolInstance_VOID_PTR(py_thread);
+  PStatClient *client = pthread.get_client();
+  if (!client->client_is_connected()) {
+    // Client was disconnected, disable Python profiling.
+    PyEval_SetProfile(nullptr, nullptr);
+    _python_profiler_enabled = false;
+    return 0;
+  }
+
+  int thread_index = pthread.get_index();
+
+#ifdef _DEBUG
+  nassertr(collector_index >= 0 && collector_index < client->get_num_collectors(), -1);
+  nassertr(thread_index >= 0 && thread_index < client->get_num_threads(), -1);
+#endif
+
+  PStatClient::Collector *collector = client->get_collector_ptr(collector_index);
+  PStatClient::InternalThread *thread = client->get_thread_ptr(thread_index);
+
+  if (collector->is_active() && thread->_is_active) {
+    double as_of = client->get_real_time();
+
+    LightMutexHolder holder(thread->_thread_lock);
+    if (thread->_thread_active) {
+      if (what == PyTrace_CALL || what == PyTrace_C_CALL) {
+        thread->_frame_data.add_start(collector_index, as_of);
+      } else {
+        thread->_frame_data.add_stop(collector_index, as_of);
+      }
+    }
+  }
+
+  return 0;
+}
+
+#endif  // HAVE_PYTHON && DO_PSTATS

+ 49 - 0
panda/src/pstatclient/pStatClient_ext.h

@@ -0,0 +1,49 @@
+/**
+ * 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 pStatClient_ext.h
+ * @author rdb
+ * @date 2022-11-23
+ */
+
+#ifndef PSTATCLIENT_EXT_H
+#define PSTATCLIENT_EXT_H
+
+#include "dtoolbase.h"
+
+#if defined(HAVE_PYTHON) && defined(DO_PSTATS)
+
+#include "extension.h"
+#include "pStatClient.h"
+#include "py_panda.h"
+
+typedef struct _frame PyFrameObject;
+
+/**
+ * This class defines the extension methods for PStatClient, which are called
+ * instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<PStatClient> : public ExtensionBase<PStatClient> {
+public:
+  INLINE static bool connect(const std::string &hostname = std::string(), int port = -1);
+  INLINE static void disconnect();
+
+  bool client_connect(std::string hostname, int port);
+  void client_disconnect();
+
+private:
+  static int trace_callback(PyObject *py_thread, PyFrameObject *frame,
+                            int what, PyObject *arg);
+};
+
+#include "pStatClient_ext.I"
+
+#endif  // HAVE_PYTHON && DO_PSTATS
+
+#endif  // PSTATCLIENT_EXT_H

+ 1 - 3
panda/src/pstatclient/pStatCollector.h

@@ -43,11 +43,9 @@ class Thread;
 class EXPCL_PANDA_PSTATCLIENT PStatCollector {
 class EXPCL_PANDA_PSTATCLIENT PStatCollector {
 #ifdef DO_PSTATS
 #ifdef DO_PSTATS
 
 
-private:
-  INLINE PStatCollector(PStatClient *client, int index);
-
 public:
 public:
   PStatCollector() = default;
   PStatCollector() = default;
+  INLINE PStatCollector(PStatClient *client, int index);
 
 
 PUBLISHED:
 PUBLISHED:
   INLINE explicit PStatCollector(const std::string &name,
   INLINE explicit PStatCollector(const std::string &name,

+ 4 - 3
panda/src/pstatclient/pStatProperties.cxx

@@ -27,9 +27,10 @@ using std::string;
 
 
 static const int current_pstat_major_version = 3;
 static const int current_pstat_major_version = 3;
 static const int current_pstat_minor_version = 0;
 static const int current_pstat_minor_version = 0;
-// Initialized at 2.0 on 51801, when version numbers were first added.
-// Incremented to 2.1 on 52101 to add support for TCP frame data.  Incremented
-// to 3.0 on 42805 to bump TCP headers to 32 bits.
+// Initialized at 2.0 on 5/18/01, when version numbers were first added.
+// Incremented to 2.1 on 5/21/01 to add support for TCP frame data.
+// Incremented to 3.0 on 4/28/05 to bump TCP headers to 32 bits.
+// Incremented to 3.1 on 11/29/22 to support nested start/stop pairs.
 
 
 /**
 /**
  * Returns the current major version number of the PStats protocol.  This is
  * Returns the current major version number of the PStats protocol.  This is

+ 8 - 0
panda/src/pstatclient/pStatThread.I

@@ -82,3 +82,11 @@ INLINE int PStatThread::
 get_index() const {
 get_index() const {
   return _index;
   return _index;
 }
 }
+
+/**
+ *
+ */
+INLINE PStatClient *PStatThread::
+get_client() const {
+  return _client;
+}

+ 3 - 0
panda/src/pstatclient/pStatThread.h

@@ -45,6 +45,9 @@ PUBLISHED:
   MAKE_PROPERTY(thread, get_thread);
   MAKE_PROPERTY(thread, get_thread);
   MAKE_PROPERTY(index, get_index);
   MAKE_PROPERTY(index, get_index);
 
 
+public:
+  PStatClient *get_client() const;
+
 private:
 private:
   PStatClient *_client;
   PStatClient *_client;
   int _index;
   int _index;

+ 2 - 1
pandatool/src/gtk-stats/gtkStatsStripChart.cxx

@@ -15,6 +15,7 @@
 #include "gtkStatsMonitor.h"
 #include "gtkStatsMonitor.h"
 #include "pStatCollectorDef.h"
 #include "pStatCollectorDef.h"
 #include "numeric_types.h"
 #include "numeric_types.h"
+#include "string_utils.h"
 
 
 static const int default_strip_chart_width = 400;
 static const int default_strip_chart_width = 400;
 static const int default_strip_chart_height = 100;
 static const int default_strip_chart_height = 100;
@@ -119,7 +120,7 @@ new_data(int thread_index, int frame_number) {
   if (!_pause) {
   if (!_pause) {
     update();
     update();
 
 
-    std::string text = format_number(get_average_net_value(), get_guide_bar_units(), get_guide_bar_unit_name());
+    std::string text = get_total_text();
     if (_net_value_text != text) {
     if (_net_value_text != text) {
       _net_value_text = text;
       _net_value_text = text;
       gtk_label_set_text(GTK_LABEL(_total_label), _net_value_text.c_str());
       gtk_label_set_text(GTK_LABEL(_total_label), _net_value_text.c_str());

+ 2 - 1
pandatool/src/pstatserver/pStatReader.cxx

@@ -194,7 +194,8 @@ handle_client_control_message(const PStatClientControlMessage &message) {
 
 
       if (message._major_version != server_major_version ||
       if (message._major_version != server_major_version ||
           (message._major_version == server_major_version &&
           (message._major_version == server_major_version &&
-           message._minor_version > server_minor_version)) {
+           message._minor_version > server_minor_version &&
+           (message._major_version != 3 || message._minor_version != 1))) {
         _monitor->bad_version(message._client_hostname, message._client_progname,
         _monitor->bad_version(message._client_hostname, message._client_progname,
                               message._client_pid,
                               message._client_pid,
                               message._major_version, message._minor_version,
                               message._major_version, message._minor_version,

+ 15 - 0
pandatool/src/pstatserver/pStatStripChart.cxx

@@ -306,6 +306,21 @@ get_title_text() {
   return text;
   return text;
 }
 }
 
 
+/**
+ * Returns the text suitable for the total label above the graph.
+ */
+std::string PStatStripChart::
+get_total_text() {
+  std::string text = format_number(get_average_net_value(), get_guide_bar_units(), get_guide_bar_unit_name());
+  if (get_collector_index() != 0 && !_view.get_show_level()) {
+    const PStatViewLevel *level = _view.get_level(get_collector_index());
+    if (level != nullptr && level->get_count() > 0) {
+      text += " / " + format_string(level->get_count()) + "x";
+    }
+  }
+  return text;
+}
+
 /**
 /**
  * Called when the mouse hovers over a label, and should return the text that
  * Called when the mouse hovers over a label, and should return the text that
  * should appear on the tooltip.
  * should appear on the tooltip.

+ 2 - 1
pandatool/src/pstatserver/pStatStripChart.h

@@ -70,8 +70,9 @@ public:
   INLINE int height_to_pixel(double value) const;
   INLINE int height_to_pixel(double value) const;
   INLINE double pixel_to_height(int y) const;
   INLINE double pixel_to_height(int y) const;
 
 
-  bool is_title_unknown() const;
+  INLINE bool is_title_unknown() const;
   std::string get_title_text();
   std::string get_title_text();
+  std::string get_total_text();
   std::string get_label_tooltip(int collector_index) const;
   std::string get_label_tooltip(int collector_index) const;
 
 
   virtual void write_datagram(Datagram &dg) const final;
   virtual void write_datagram(Datagram &dg) const final;

+ 89 - 98
pandatool/src/pstatserver/pStatView.cxx

@@ -15,13 +15,9 @@
 
 
 #include "pStatFrameData.h"
 #include "pStatFrameData.h"
 #include "pStatCollectorDef.h"
 #include "pStatCollectorDef.h"
-#include "vector_int.h"
-#include "plist.h"
-#include "pset.h"
 
 
 #include <algorithm>
 #include <algorithm>
-
-
+#include <vector>
 
 
 /**
 /**
  * This class is used within this module only--in fact, within
  * This class is used within this module only--in fact, within
@@ -30,94 +26,89 @@
  */
  */
 class FrameSample {
 class FrameSample {
 public:
 public:
-  typedef plist<FrameSample *> Started;
+  void start(double time, FrameSample *started) {
+    // Keep track of nested start/stop pairs.  We only consider the outer one.
+    if (_started++ > 0) {
+      return;
+    }
 
 
-  FrameSample() {
-    _touched = false;
-    _is_started = false;
-    _pushed = false;
-    _net_time = 0.0;
+    nassertv(!_pushed);
+    _net_time -= time;
+    push_all(time, started);
+    nassertv(_next == nullptr && _prev == nullptr);
+    _prev = started->_prev;
+    _next = started;
+    _prev->_next = this;
+    started->_prev = this;
   }
   }
-  void data_point(double time, bool is_start, Started &started) {
-    _touched = true;
-
-    // We only consider events that change the startstop state.  With two
-    // consecutive 'start' events, for instance, we ignore the second one.
-
-/*
- * *** That's not quite the right thing to do.  We should keep track of the
- * nesting level and bracket things correctly, so that we ignore the second
- * start and the *first* stop, but respect the outer startstop.  For the short
- * term, this works, because the client is already doing this logic and won't
- * send us nested startstop pairs, but we'd like to generalize this in the
- * future so we can deal with these nested pairs properly.
- */
-    nassertv(is_start != _is_started);
 
 
-    _is_started = is_start;
+  void stop(double time, FrameSample *started) {
+    nassertv(_started > 0);
+    if (--_started > 0) {
+      return;
+    }
 
 
-    if (_pushed) {
-      nassertv(!_is_started);
-      Started::iterator si = find(started.begin(), started.end(), this);
-      nassertv(si != started.end());
-      started.erase(si);
+    nassertv(_next != nullptr && _prev != nullptr);
 
 
+    if (_pushed) {
+      _prev->_next = _next;
+      _next->_prev = _prev;
+      _next = _prev = nullptr;
     } else {
     } else {
-      if (_is_started) {
-        _net_time -= time;
-        push_all(time, started);
-        started.push_back(this);
-      } else {
-        _net_time += time;
-        Started::iterator si = find(started.begin(), started.end(), this);
-        nassertv(si != started.end());
-        started.erase(si);
-        pop_one(time, started);
-      }
+      _net_time += time;
+      _prev->_next = _next;
+      _next->_prev = _prev;
+      _next = _prev = nullptr;
+      pop_one(time, started);
     }
     }
   }
   }
+
+private:
   void push(double time) {
   void push(double time) {
     if (!_pushed) {
     if (!_pushed) {
       _pushed = true;
       _pushed = true;
-      if (_is_started) {
+      if (_started > 0) {
         _net_time += time;
         _net_time += time;
       }
       }
     }
     }
   }
   }
+
   void pop(double time) {
   void pop(double time) {
     if (_pushed) {
     if (_pushed) {
       _pushed = false;
       _pushed = false;
-      if (_is_started) {
+      if (_started > 0) {
         _net_time -= time;
         _net_time -= time;
       }
       }
     }
     }
   }
   }
 
 
-  void push_all(double time, Started &started) {
-    Started::iterator si;
-    for (si = started.begin(); si != started.end(); ++si) {
-      (*si)->push(time);
+  void push_all(double time, FrameSample *started) {
+    for (FrameSample *sample = started->_next;
+         sample != started; sample = sample->_next) {
+      sample->push(time);
     }
     }
   }
   }
 
 
-  void pop_one(double time, Started &started) {
-    Started::reverse_iterator si;
-    for (si = started.rbegin(); si != started.rend(); ++si) {
-      if ((*si)->_pushed) {
-        (*si)->pop(time);
+  void pop_one(double time, FrameSample *started) {
+    for (FrameSample *sample = started->_prev;
+         sample != started; sample = sample->_prev) {
+      if (sample->_pushed) {
+        sample->pop(time);
         return;
         return;
       }
       }
     }
     }
   }
   }
 
 
-  bool _touched;
-  bool _is_started;
-  bool _pushed;
-  double _net_time;
+public:
+  FrameSample *_next = nullptr;
+  FrameSample *_prev = nullptr;
+  double _net_time = 0.0;
+  int _started = 0;
+  int _count = 0;
+  bool _pushed = false;
+  bool _is_new = false;
 };
 };
 
 
-
-
 /**
 /**
  *
  *
  */
  */
@@ -279,19 +270,19 @@ get_level(int collector) {
 void PStatView::
 void PStatView::
 update_time_data(const PStatFrameData &frame_data) {
 update_time_data(const PStatFrameData &frame_data) {
   int num_events = frame_data.get_num_events();
   int num_events = frame_data.get_num_events();
+  int num_collectors = _client_data->get_num_collectors();
 
 
-  typedef pvector<FrameSample> Samples;
-  Samples samples(_client_data->get_num_collectors());
+  typedef std::vector<FrameSample> Samples;
+  Samples samples(num_collectors);
 
 
-  FrameSample::Started started;
+  // Keep a linked list of started samples.
+  FrameSample started;
+  started._next = &started;
+  started._prev = &started;
 
 
   _all_collectors_known = true;
   _all_collectors_known = true;
 
 
-
-  // This tracks the set of samples we actually care about.
-  typedef pset<int> GotSamples;
-  GotSamples got_samples;
-
+  int new_collectors = 0;
   int i;
   int i;
   for (i = 0; i < num_events; i++) {
   for (i = 0; i < num_events; i++) {
     int collector_index = frame_data.get_time_collector(i);
     int collector_index = frame_data.get_time_collector(i);
@@ -301,42 +292,40 @@ update_time_data(const PStatFrameData &frame_data) {
       _all_collectors_known = false;
       _all_collectors_known = false;
 
 
     } else {
     } else {
-      nassertv(collector_index >= 0 && collector_index < (int)samples.size());
+      nassertv(collector_index >= 0 && collector_index < num_collectors);
 
 
       if (_client_data->get_child_distance(_constraint, collector_index) >= 0) {
       if (_client_data->get_child_distance(_constraint, collector_index) >= 0) {
         // Here's a data point we care about: anything at constraint level or
         // Here's a data point we care about: anything at constraint level or
         // below.
         // below.
-        if (is_start == samples[collector_index]._is_started) {
-          if (!is_start) {
-            // A "stop" in the middle of a frame implies a "start" since time
-            // 0 (that is, since the first data point in the frame).
-            samples[collector_index].data_point(frame_data.get_time(0), true, started);
-            samples[collector_index].data_point(frame_data.get_time(i), is_start, started);
-          } else {
-            // An extra "start" for a collector that's already started is an
-            // error.
-            nout << "Unexpected data point for "
-                 << _client_data->get_collector_fullname(collector_index)
-                 << "\n";
-          }
+        if (is_start) {
+          samples[collector_index].start(frame_data.get_time(i), &started);
+          samples[collector_index]._count++;
         } else {
         } else {
-          samples[collector_index].data_point(frame_data.get_time(i), is_start, started);
-          got_samples.insert(collector_index);
+          // A "stop" in the middle of a frame implies a "start" since time
+          // 0 (that is, since the first data point in the frame).
+          if (samples[collector_index]._started == 0) {
+            samples[collector_index].start(frame_data.get_time(0), &started);
+          }
+          samples[collector_index].stop(frame_data.get_time(i), &started);
+        }
+
+        if (!samples[collector_index]._is_new) {
+          samples[collector_index]._is_new = true;
+          ++new_collectors;
         }
         }
       }
       }
     }
     }
   }
   }
 
 
   // Make sure everything is stopped.
   // Make sure everything is stopped.
-
   Samples::iterator si;
   Samples::iterator si;
   for (i = 0, si = samples.begin(); si != samples.end(); ++i, ++si) {
   for (i = 0, si = samples.begin(); si != samples.end(); ++i, ++si) {
-    if ((*si)._is_started) {
-      (*si).data_point(frame_data.get_end(), false, started);
+    if ((*si)._started > 0) {
+      (*si).stop(frame_data.get_end(), &started);
     }
     }
   }
   }
 
 
-  nassertv(started.empty());
+  nassertv(started._next == &started && started._prev == &started);
 
 
   bool any_new_levels = false;
   bool any_new_levels = false;
 
 
@@ -356,11 +345,11 @@ update_time_data(const PStatFrameData &frame_data) {
     }
     }
 
 
     int collector_index = level->_collector;
     int collector_index = level->_collector;
-    GotSamples::iterator gi;
-    gi = got_samples.find(collector_index);
-    if (gi != got_samples.end()) {
+    if (samples[collector_index]._is_new) {
       level->_value_alone = samples[collector_index]._net_time;
       level->_value_alone = samples[collector_index]._net_time;
-      got_samples.erase(gi);
+      level->_count = samples[collector_index]._count;
+      samples[collector_index]._is_new = false;
+      --new_collectors;
     }
     }
 
 
     li = lnext;
     li = lnext;
@@ -368,14 +357,15 @@ update_time_data(const PStatFrameData &frame_data) {
 
 
   // Finally, any samples left over in the got_samples set are new collectors
   // Finally, any samples left over in the got_samples set are new collectors
   // that we need to add to the Levels list.
   // that we need to add to the Levels list.
-  if (!got_samples.empty()) {
+  if (new_collectors > 0) {
     any_new_levels = true;
     any_new_levels = true;
 
 
-    GotSamples::const_iterator gi;
-    for (gi = got_samples.begin(); gi != got_samples.end(); ++gi) {
-      int collector_index = (*gi);
-      PStatViewLevel *level = get_level(collector_index);
-      level->_value_alone = samples[*gi]._net_time;
+    for (int collector_index = 0; collector_index < num_collectors; ++collector_index) {
+      if (samples[collector_index]._is_new) {
+        PStatViewLevel *level = get_level(collector_index);
+        level->_value_alone = samples[collector_index]._net_time;
+        level->_count = samples[collector_index]._count;
+      }
     }
     }
   }
   }
 
 
@@ -509,6 +499,7 @@ bool PStatView::
 reset_level(PStatViewLevel *level) {
 reset_level(PStatViewLevel *level) {
   bool any_changed = false;
   bool any_changed = false;
   level->_value_alone = 0.0;
   level->_value_alone = 0.0;
+  level->_count = 0;
 
 
   if (level->_collector == _constraint) {
   if (level->_collector == _constraint) {
     return false;
     return false;

+ 8 - 0
pandatool/src/pstatserver/pStatViewLevel.I

@@ -27,3 +27,11 @@ INLINE double PStatViewLevel::
 get_value_alone() const {
 get_value_alone() const {
   return _value_alone;
   return _value_alone;
 }
 }
+
+/**
+ * Returns the number of start/stop pairs for this collector.
+ */
+INLINE int PStatViewLevel::
+get_count() const {
+  return _count;
+}

+ 2 - 0
pandatool/src/pstatserver/pStatViewLevel.h

@@ -31,6 +31,7 @@ public:
   INLINE int get_collector() const;
   INLINE int get_collector() const;
   INLINE double get_value_alone() const;
   INLINE double get_value_alone() const;
   double get_net_value() const;
   double get_net_value() const;
+  INLINE int get_count() const;
 
 
   void sort_children(const PStatClientData *client_data);
   void sort_children(const PStatClientData *client_data);
 
 
@@ -39,6 +40,7 @@ public:
 
 
 private:
 private:
   int _collector;
   int _collector;
+  int _count = 0;
   double _value_alone;
   double _value_alone;
   PStatViewLevel *_parent;
   PStatViewLevel *_parent;
 
 

+ 1 - 1
pandatool/src/win-stats/winStatsStripChart.cxx

@@ -97,7 +97,7 @@ new_data(int thread_index, int frame_number) {
   if (!_pause) {
   if (!_pause) {
     update();
     update();
 
 
-    std::string text = format_number(get_average_net_value(), get_guide_bar_units(), get_guide_bar_unit_name());
+    std::string text = get_total_text();
     if (_net_value_text != text) {
     if (_net_value_text != text) {
       _net_value_text = text;
       _net_value_text = text;
       RECT rect;
       RECT rect;