Przeglądaj źródła

Merge branch 'master' into webgl-port

rdb 2 lat temu
rodzic
commit
d93942bf3e
100 zmienionych plików z 2205 dodań i 1463 usunięć
  1. 55 9
      .github/workflows/ci.yml
  2. 2 2
      README.md
  3. 6 1
      cmake/install/Panda3DConfig.cmake
  4. 3 3
      contrib/src/panda3dtoolsgui/Panda3DToolsGUI.py
  5. 1 1
      contrib/src/panda3dtoolsgui/setup.py
  6. 1 4
      contrib/src/sceneeditor/seCameraControl.py
  7. 2 8
      contrib/src/sceneeditor/seManipulation.py
  8. 35 27
      direct/src/directnotify/DirectNotify.py
  9. 14 10
      direct/src/directnotify/Logger.py
  10. 46 41
      direct/src/directnotify/Notifier.py
  11. 29 19
      direct/src/directnotify/RotatingLog.py
  12. 187 166
      direct/src/directtools/DirectCameraControl.py
  13. 4 3
      direct/src/directtools/DirectGrid.py
  14. 204 180
      direct/src/directtools/DirectManipulation.py
  15. 59 39
      direct/src/directtools/DirectSession.py
  16. 59 30
      direct/src/dist/FreezeTool.py
  17. 19 7
      direct/src/dist/commands.py
  18. 1 1
      direct/src/distributed/DistributedCartesianGrid.py
  19. 3 0
      direct/src/distributed/MsgTypes.py
  20. 12 4
      direct/src/extensions_native/NodePath_extensions.py
  21. 1 1
      direct/src/gui/DirectEntry.py
  22. 1 1
      direct/src/gui/OnscreenText.py
  23. 0 6
      direct/src/interval/FunctionInterval.py
  24. 0 222
      direct/src/interval/IntervalTest.py
  25. 28 27
      direct/src/leveleditor/LevelEditorUIBase.py
  26. 3 0
      direct/src/leveleditor/LevelLoader.py
  27. 1 1
      direct/src/particles/ParticleEffect.py
  28. 11 8
      direct/src/showbase/Loader.py
  29. 18 11
      direct/src/showbase/ShowBase.py
  30. 4 1
      direct/src/showbase/ShowBaseGlobal.py
  31. 2 1
      direct/src/showbase/TkGlobal.py
  32. 2 1
      direct/src/showbase/WxGlobal.py
  33. 6 4
      direct/src/showutil/TexMemWatcher.py
  34. 1 1
      direct/src/showutil/TexViewer.py
  35. 2 0
      direct/src/task/MiniTask.py
  36. 104 47
      direct/src/task/Task.py
  37. 90 89
      direct/src/tkpanels/DirectSessionPanel.py
  38. 5 1
      direct/src/tkpanels/Inspector.py
  39. 26 25
      direct/src/tkpanels/Placer.py
  40. 4 5
      direct/src/wxwidgets/WxAppShell.py
  41. 47 43
      direct/src/wxwidgets/WxPandaShell.py
  42. 17 12
      direct/src/wxwidgets/WxSlider.py
  43. 4 4
      dtool/Config.cmake
  44. 26 10
      dtool/Package.cmake
  45. 1 17
      dtool/src/dtoolbase/deletedBufferChain.I
  46. 18 27
      dtool/src/dtoolbase/deletedBufferChain.cxx
  47. 4 12
      dtool/src/dtoolbase/deletedBufferChain.h
  48. 6 0
      dtool/src/dtoolbase/patomic.I
  49. 1 1
      dtool/src/dtoolbase/patomic.cxx
  50. 10 1
      dtool/src/dtoolbase/patomic.h
  51. 1 1
      dtool/src/interrogatedb/dtool_super_base.cxx
  52. 12 0
      dtool/src/interrogatedb/py_compat.h
  53. 1 3
      makepanda/installpanda.py
  54. 32 0
      makepanda/locations.py
  55. 1 2
      makepanda/makepackage.py
  56. 20 9
      makepanda/makepanda.py
  57. 10 10
      makepanda/makepandacore.py
  58. 2 1
      makepanda/makewheel.py
  59. 20 2
      makepanda/test_wheel.py
  60. 3 0
      panda/CMakeLists.txt
  61. 15 2
      panda/src/cocoadisplay/cocoaGraphicsPipe.h
  62. 186 5
      panda/src/cocoadisplay/cocoaGraphicsPipe.mm
  63. 1 0
      panda/src/cocoadisplay/cocoaGraphicsWindow.h
  64. 209 105
      panda/src/cocoadisplay/cocoaGraphicsWindow.mm
  65. 1 0
      panda/src/cocoadisplay/cocoaPandaWindowDelegate.h
  66. 4 0
      panda/src/cocoadisplay/cocoaPandaWindowDelegate.mm
  67. 1 0
      panda/src/cocoadisplay/config_cocoadisplay.h
  68. 5 0
      panda/src/cocoadisplay/config_cocoadisplay.mm
  69. 0 7
      panda/src/cocoagldisplay/cocoaGLGraphicsStateGuardian.h
  70. 1 62
      panda/src/cocoagldisplay/cocoaGLGraphicsStateGuardian.mm
  71. 1 0
      panda/src/cocoagldisplay/cocoaGLGraphicsWindow.h
  72. 9 8
      panda/src/cocoagldisplay/cocoaGLGraphicsWindow.mm
  73. 17 9
      panda/src/display/displayInformation.cxx
  74. 5 3
      panda/src/display/displayInformation.h
  75. 31 10
      panda/src/display/graphicsStateGuardian.cxx
  76. 2 2
      panda/src/glstuff/glShaderContext_src.cxx
  77. 11 6
      panda/src/gobj/geomVertexArrayData.cxx
  78. 1 1
      panda/src/gobj/shader.cxx
  79. 11 6
      panda/src/gobj/shaderBuffer.cxx
  80. 1 13
      panda/src/pnmimagetypes/bmp.h
  81. 1 0
      panda/src/pnmimagetypes/pnmFileTypeBMP.h
  82. 80 34
      panda/src/pnmimagetypes/pnmFileTypeBMPReader.cxx
  83. 2 2
      panda/src/putil/bitArray_ext.cxx
  84. 1 1
      panda/src/putil/doubleBitMask_ext.I
  85. 127 0
      panda/src/tinydisplay/CMakeLists.txt
  86. 2 0
      panda/src/tinydisplay/srgb_tables.cxx
  87. 5 0
      panda/src/tinydisplay/srgb_tables.h
  88. 2 0
      panda/src/tinydisplay/tinyCocoaGraphicsWindow.h
  89. 15 0
      panda/src/tinydisplay/tinyCocoaGraphicsWindow.mm
  90. 2 0
      panda/src/tinydisplay/zmath.h
  91. 6 0
      panda/src/wgldisplay/wglGraphicsStateGuardian.cxx
  92. 12 3
      panda/src/windisplay/winDetectDx.h
  93. 16 0
      panda/src/windisplay/winGraphicsPipe.cxx
  94. 14 0
      panda/src/x11display/x11GraphicsPipe.cxx
  95. 1 0
      pandatool/CMakeLists.txt
  96. 34 7
      pandatool/src/assimp/loaderFileTypeAssimp.cxx
  97. 1 1
      pandatool/src/gtk-stats/CMakeLists.txt
  98. 53 0
      pandatool/src/mac-stats/CMakeLists.txt
  99. 30 13
      pandatool/src/mac-stats/macStatsTimeline.mm
  100. 2 1
      requirements-test.txt

+ 55 - 9
.github/workflows/ci.yml

@@ -120,16 +120,16 @@ jobs:
       uses: actions/cache@v1
       with:
         path: thirdparty
-        key: ci-cmake-${{ runner.OS }}-thirdparty-v1.10.13-r1
+        key: ci-cmake-${{ runner.OS }}-thirdparty-v1.10.14-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.13/panda3d-1.10.13-tools-win64.zip", "thirdparty-tools.zip")
+          $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.14/panda3d-1.10.14-tools-win64.zip", "thirdparty-tools.zip")
           Expand-Archive -Path thirdparty-tools.zip
-          Move-Item -Path thirdparty-tools/panda3d-1.10.13/thirdparty -Destination .
+          Move-Item -Path thirdparty-tools/panda3d-1.10.14/thirdparty -Destination .
         }
 
     - name: ccache (non-Windows)
@@ -308,6 +308,38 @@ jobs:
         $PYTHON_EXECUTABLE -m pytest ../tests --cov=.
       # END B
 
+    - name: Setup Python (Python 3.12)
+      if: contains(matrix.python, 'YES')
+      uses: actions/setup-python@v4
+      with:
+        python-version: '3.12'
+    - name: Configure (Python 3.12)
+      if: contains(matrix.python, 'YES')
+      working-directory: build
+      shell: bash
+      run: >
+        cmake -DWANT_PYTHON_VERSION=3.12 -DHAVE_PYTHON=YES
+        -DPython_FIND_REGISTRY=NEVER -DPython_ROOT="$pythonLocation" .
+    - name: Build (Python 3.12)
+      if: contains(matrix.python, 'YES')
+      # BEGIN A
+      working-directory: build
+      run: cmake --build . --config ${{ matrix.config }} --parallel 4
+      # END A
+    - name: Test (Python 3.12)
+      # 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 -r ../requirements-test.txt
+        export COVERAGE_FILE=.coverage.$RANDOM LLVM_PROFILE_FILE=$PWD/pid-%p.profraw
+        $PYTHON_EXECUTABLE -m pytest ../tests --cov=.
+      # END B
+
     - name: Upload coverage reports
       if: always() && matrix.config == 'Coverage'
       working-directory: build
@@ -343,9 +375,9 @@ jobs:
       shell: powershell
       run: |
         $wc = New-Object System.Net.WebClient
-        $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.13/panda3d-1.10.13-tools-win64.zip", "thirdparty-tools.zip")
+        $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.14/panda3d-1.10.14-tools-win64.zip", "thirdparty-tools.zip")
         Expand-Archive -Path thirdparty-tools.zip
-        Move-Item -Path thirdparty-tools/panda3d-1.10.13/thirdparty -Destination .
+        Move-Item -Path thirdparty-tools/panda3d-1.10.14/thirdparty -Destination .
     - name: Get thirdparty packages (macOS)
       if: runner.os == 'macOS'
       run: |
@@ -355,6 +387,20 @@ jobs:
         rmdir panda3d-1.10.14
         (cd thirdparty/darwin-libs-a && rm -rf rocket)
 
+    - name: Set up Python 3.12
+      uses: actions/setup-python@v4
+      with:
+        python-version: '3.12'
+    - name: Build Python 3.12
+      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.12
+      shell: bash
+      run: |
+        python -m pip install -r requirements-test.txt
+        PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+
     - name: Set up Python 3.11
       uses: actions/setup-python@v4
       with:
@@ -367,7 +413,7 @@ jobs:
       shell: bash
       run: |
         python -m pip install -r requirements-test.txt
-        PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+        PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
 
     - name: Set up Python 3.10
       uses: actions/setup-python@v4
@@ -381,7 +427,7 @@ jobs:
       shell: bash
       run: |
         python -m pip install -r requirements-test.txt
-        PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+        PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
 
     - name: Set up Python 3.9
       uses: actions/setup-python@v4
@@ -395,7 +441,7 @@ jobs:
       shell: bash
       run: |
         python -m pip install -r requirements-test.txt
-        PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+        PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
 
     - name: Set up Python 3.8
       uses: actions/setup-python@v4
@@ -409,7 +455,7 @@ jobs:
       shell: bash
       run: |
         python -m pip install -r requirements-test.txt
-        PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+        PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
 
     - name: Make installer
       run: |

+ 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
 building them from source.
 
-- 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
+- https://www.panda3d.org/download/panda3d-1.10.14/panda3d-1.10.14-tools-win64.zip
+- https://www.panda3d.org/download/panda3d-1.10.14/panda3d-1.10.14-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

+ 6 - 1
cmake/install/Panda3DConfig.cmake

@@ -103,6 +103,11 @@
 #               Panda3D::OpenGLES2::pandagles2
 #
 #
+#   TinyDisplay - Support for software rendering.
+#
+#               Panda3D::TinyDisplay::p3tinydisplay
+#
+#
 #   Vision    - Support for vision processing.
 #
 #               Panda3D::Vision::p3vision
@@ -126,7 +131,7 @@ set(_panda_components
   Bullet ODE
   FFmpeg
   OpenAL FMOD
-  OpenGL DX9 OpenGLES1 OpenGLES2
+  OpenGL DX9 OpenGLES1 OpenGLES2 TinyDisplay
   Vision VRPN
 )
 

+ 3 - 3
contrib/src/panda3dtoolsgui/Panda3DToolsGUI.py

@@ -2650,11 +2650,11 @@ class main(wx.Frame):
                                             for inputFile in inputs:
                                                 if (inputFile != ''):
                                                     inputFilename = inputFile.split('\\')[-1]
-                                                    print "Compare: ", inFile, filename, inputFile, inputFilename
+                                                    print("Compare: ", inFile, filename, inputFile, inputFilename)
                                                     if inputFilename == filename:
                                                         inputTime = os.path.getmtime(inputFile)
                                                         outputTime = os.path.getmtime(inFile)
-                                                        print "Matched: ", (inputTime > outputTime)
+                                                        print("Matched: ", (inputTime > outputTime))
                                                         inputChanged = (inputTime > outputTime)
                                                         break
                                             '''
@@ -2848,7 +2848,7 @@ class main(wx.Frame):
 
         except ValueError:
             return
-        #print self.batchList
+        #print(self.batchList)
 
     def OnBatchItemEdit(self, event):
         selectedItemId = self.batchTree.GetSelections()

+ 1 - 1
contrib/src/panda3dtoolsgui/setup.py

@@ -1,4 +1,4 @@
-from distutils.core import setup
+from setuptools import setup
 import py2exe
 
 setup(console=['Panda3DToolsGUI.py'])

+ 1 - 4
contrib/src/sceneeditor/seCameraControl.py

@@ -281,10 +281,7 @@ class DirectCameraControl(DirectObject):
         angle = getCrankAngle(state.coaCenter)
         deltaAngle = angle - state.lastAngle
         state.lastAngle = angle
-        if base.config.GetBool('temp-hpr-fix',0):
-            self.camManipRef.setHpr(self.camManipRef, 0, 0, deltaAngle)
-        else:
-            self.camManipRef.setHpr(self.camManipRef, 0, 0, -deltaAngle)
+        self.camManipRef.setHpr(self.camManipRef, 0, 0, deltaAngle)
         SEditor.camera.setTransform(self.camManipRef, wrt)
         return Task.cont
 

+ 2 - 8
contrib/src/sceneeditor/seManipulation.py

@@ -324,10 +324,7 @@ class DirectManipulationControl(DirectObject):
         if self.rotateAxis == 'x':
             SEditor.widget.setP(SEditor.widget, deltaAngle)
         elif self.rotateAxis == 'y':
-            if base.config.GetBool('temp-hpr-fix',0):
-                SEditor.widget.setR(SEditor.widget, deltaAngle)
-            else:
-                SEditor.widget.setR(SEditor.widget, -deltaAngle)
+            SEditor.widget.setR(SEditor.widget, deltaAngle)
         elif self.rotateAxis == 'z':
             SEditor.widget.setH(SEditor.widget, deltaAngle)
         # Record crank angle for next time around
@@ -456,10 +453,7 @@ class DirectManipulationControl(DirectObject):
         deltaAngle = angle - state.lastAngle
         state.lastAngle = angle
         # Mouse motion edge to edge of display region results in one full turn
-        if base.config.GetBool('temp-hpr-fix',0):
-            relHpr(SEditor.widget, SEditor.camera, 0, 0, -deltaAngle)
-        else:
-            relHpr(SEditor.widget, SEditor.camera, 0, 0, deltaAngle)
+        relHpr(SEditor.widget, SEditor.camera, 0, 0, -deltaAngle)
 
     def scale3D(self, state):
         # Scale the selected node based upon up down mouse motion

+ 35 - 27
direct/src/directnotify/DirectNotify.py

@@ -2,6 +2,10 @@
 DirectNotify module: this module contains the DirectNotify class
 """
 
+from __future__ import annotations
+
+from panda3d.core import StreamWriter
+
 from . import Notifier
 from . import Logger
 
@@ -12,39 +16,39 @@ class DirectNotify:
     mulitple notify categories via a dictionary of Notifiers.
     """
 
-    def __init__(self):
+    def __init__(self) -> None:
         """
         DirectNotify class keeps a dictionary of Notfiers
         """
-        self.__categories = {}
+        self.__categories: dict[str, Notifier.Notifier] = {}
         # create a default log file
         self.logger = Logger.Logger()
 
         # This will get filled in later by ShowBase.py with a
         # C++-level StreamWriter object for writing to standard
         # output.
-        self.streamWriter = None
+        self.streamWriter: StreamWriter | None = None
 
-    def __str__(self):
+    def __str__(self) -> str:
         """
         Print handling routine
         """
         return "DirectNotify categories: %s" % (self.__categories)
 
     #getters and setters
-    def getCategories(self):
+    def getCategories(self) -> list[str]:
         """
         Return list of category dictionary keys
         """
         return list(self.__categories.keys())
 
-    def getCategory(self, categoryName):
+    def getCategory(self, categoryName: str) -> Notifier.Notifier | None:
         """getCategory(self, string)
         Return the category with given name if present, None otherwise
         """
         return self.__categories.get(categoryName, None)
 
-    def newCategory(self, categoryName, logger=None):
+    def newCategory(self, categoryName: str, logger: Logger.Logger | None = None) -> Notifier.Notifier:
         """newCategory(self, string)
         Make a new notify category named categoryName. Return new category
         if no such category exists, else return existing category
@@ -52,9 +56,11 @@ class DirectNotify:
         if categoryName not in self.__categories:
             self.__categories[categoryName] = Notifier.Notifier(categoryName, logger)
             self.setDconfigLevel(categoryName)
-        return self.getCategory(categoryName)
+        notifier = self.getCategory(categoryName)
+        assert notifier is not None
+        return notifier
 
-    def setDconfigLevel(self, categoryName):
+    def setDconfigLevel(self, categoryName: str) -> None:
         """
         Check to see if this category has a dconfig variable
         to set the notify severity and then set that level. You cannot
@@ -77,40 +83,42 @@ class DirectNotify:
             level = 'error'
 
         category = self.getCategory(categoryName)
+        assert category is not None, f'failed to find category: {categoryName!r}'
         # Note - this print statement is making it difficult to
         # achieve "no output unless there's an error" operation - Josh
         # print ("Setting DirectNotify category: " + categoryName +
         #        " to severity: " + level)
         if level == "error":
-            category.setWarning(0)
-            category.setInfo(0)
-            category.setDebug(0)
+            category.setWarning(False)
+            category.setInfo(False)
+            category.setDebug(False)
         elif level == "warning":
-            category.setWarning(1)
-            category.setInfo(0)
-            category.setDebug(0)
+            category.setWarning(True)
+            category.setInfo(False)
+            category.setDebug(False)
         elif level == "info":
-            category.setWarning(1)
-            category.setInfo(1)
-            category.setDebug(0)
+            category.setWarning(True)
+            category.setInfo(True)
+            category.setDebug(False)
         elif level == "debug":
-            category.setWarning(1)
-            category.setInfo(1)
-            category.setDebug(1)
+            category.setWarning(True)
+            category.setInfo(True)
+            category.setDebug(True)
         else:
             print("DirectNotify: unknown notify level: " + str(level)
                    + " for category: " + str(categoryName))
 
-    def setDconfigLevels(self):
+    def setDconfigLevels(self) -> None:
         for categoryName in self.getCategories():
             self.setDconfigLevel(categoryName)
 
-    def setVerbose(self):
+    def setVerbose(self) -> None:
         for categoryName in self.getCategories():
             category = self.getCategory(categoryName)
-            category.setWarning(1)
-            category.setInfo(1)
-            category.setDebug(1)
+            assert category is not None
+            category.setWarning(True)
+            category.setInfo(True)
+            category.setDebug(True)
 
     def popupControls(self, tl = None):
         # Don't use a regular import, to prevent ModuleFinder from picking
@@ -119,5 +127,5 @@ class DirectNotify:
         NotifyPanel = importlib.import_module('direct.tkpanels.NotifyPanel')
         NotifyPanel.NotifyPanel(self, tl)
 
-    def giveNotify(self,cls):
+    def giveNotify(self, cls) -> None:
         cls.notify = self.newCategory(cls.__name__)

+ 14 - 10
direct/src/directnotify/Logger.py

@@ -1,27 +1,30 @@
 """Logger module: contains the logger class which creates and writes
    data to log files on disk"""
 
+from __future__ import annotations
+
+import io
 import time
 import math
 
 
 class Logger:
-    def __init__(self, fileName="log"):
+    def __init__(self, fileName: str = "log") -> None:
         """
         Logger constructor
         """
-        self.__timeStamp = 1
+        self.__timeStamp = True
         self.__startTime = 0.0
-        self.__logFile = None
+        self.__logFile: io.TextIOWrapper | None = None
         self.__logFileName = fileName
 
-    def setTimeStamp(self, enable):
+    def setTimeStamp(self, enable: bool) -> None:
         """
         Toggle time stamp printing with log entries on and off
         """
         self.__timeStamp = enable
 
-    def getTimeStamp(self):
+    def getTimeStamp(self) -> bool:
         """
         Return whether or not we are printing time stamps with log entries
         """
@@ -29,24 +32,25 @@ class Logger:
 
     # logging control
 
-    def resetStartTime(self):
+    def resetStartTime(self) -> None:
         """
         Reset the start time of the log file for time stamps
         """
         self.__startTime = time.time()
 
-    def log(self, entryString):
+    def log(self, entryString: str) -> None:
         """log(self, string)
         Print the given string to the log file"""
         if self.__logFile is None:
             self.__openLogFile()
+        assert self.__logFile is not None
         if self.__timeStamp:
             self.__logFile.write(self.__getTimeStamp())
         self.__logFile.write(entryString + '\n')
 
     # logging functions
 
-    def __openLogFile(self):
+    def __openLogFile(self) -> None:
         """
         Open a file for logging error/warning messages
         """
@@ -56,14 +60,14 @@ class Logger:
         logFileName = self.__logFileName + "." + st
         self.__logFile = open(logFileName, "w")
 
-    def __closeLogFile(self):
+    def __closeLogFile(self) -> None:
         """
         Close the error/warning output file
         """
         if self.__logFile is not None:
             self.__logFile.close()
 
-    def __getTimeStamp(self):
+    def __getTimeStamp(self) -> str:
         """
         Return the offset between current time and log file startTime
         """

+ 46 - 41
direct/src/directnotify/Notifier.py

@@ -2,11 +2,16 @@
 Notifier module: contains methods for handling information output
 for the programmer/user
 """
+
+from __future__ import annotations
+
+from .Logger import Logger
 from .LoggerGlobal import defaultLogger
 from direct.showbase import PythonUtil
 from panda3d.core import ConfigVariableBool, NotifyCategory, StreamWriter, Notify
 import time
 import sys
+from typing import NoReturn
 
 
 class NotifierException(Exception):
@@ -20,13 +25,13 @@ class Notifier:
     # messages instead of writing them to the console.  This is
     # particularly useful for integrating the Python notify system
     # with the C++ notify system.
-    streamWriter = None
+    streamWriter: StreamWriter | None = None
     if ConfigVariableBool('notify-integrate', True):
         streamWriter = StreamWriter(Notify.out(), False)
 
     showTime = ConfigVariableBool('notify-timestamp', False)
 
-    def __init__(self, name, logger=None):
+    def __init__(self, name: str, logger: Logger | None = None) -> None:
         """
         Parameters:
             name (str): a string name given to this Notifier instance.
@@ -42,12 +47,12 @@ class Notifier:
             self.__logger = logger
 
         # Global default levels are initialized here
-        self.__info = 1
-        self.__warning = 1
-        self.__debug = 0
-        self.__logging = 0
+        self.__info = True
+        self.__warning = True
+        self.__debug = False
+        self.__logging = False
 
-    def setServerDelta(self, delta, timezone):
+    def setServerDelta(self, delta: float, timezone: int) -> None:
         """
         Call this method on any Notify object to globally change the
         timestamp printed for each line of all Notify objects.
@@ -65,7 +70,7 @@ class Notifier:
 
         self.info("Notify clock adjusted by %s (and timezone adjusted by %s hours) to synchronize with server." % (PythonUtil.formatElapsedSeconds(delta), (time.timezone - timezone) / 3600))
 
-    def getTime(self):
+    def getTime(self) -> str:
         """
         Return the time as a string suitable for printing at the
         head of any notify message
@@ -74,14 +79,14 @@ class Notifier:
         # the task is out of focus on win32.  time.clock doesn't have this problem.
         return time.strftime(":%m-%d-%Y %H:%M:%S ", time.localtime(time.time() + self.serverDelta))
 
-    def getOnlyTime(self):
+    def getOnlyTime(self) -> str:
         """
         Return the time as a string.
         The Only in the name is referring to not showing the date.
         """
         return time.strftime("%H:%M:%S", time.localtime(time.time() + self.serverDelta))
 
-    def __str__(self):
+    def __str__(self) -> str:
         """
         Print handling routine
         """
@@ -89,26 +94,26 @@ class Notifier:
                (self.__name, self.__info, self.__warning, self.__debug, self.__logging)
 
     # Severity funcs
-    def setSeverity(self, severity):
+    def setSeverity(self, severity: int) -> None:
         from panda3d.core import NSDebug, NSInfo, NSWarning, NSError
         if severity >= NSError:
-            self.setWarning(0)
-            self.setInfo(0)
-            self.setDebug(0)
+            self.setWarning(False)
+            self.setInfo(False)
+            self.setDebug(False)
         elif severity == NSWarning:
-            self.setWarning(1)
-            self.setInfo(0)
-            self.setDebug(0)
+            self.setWarning(True)
+            self.setInfo(False)
+            self.setDebug(False)
         elif severity == NSInfo:
-            self.setWarning(1)
-            self.setInfo(1)
-            self.setDebug(0)
+            self.setWarning(True)
+            self.setInfo(True)
+            self.setDebug(False)
         elif severity <= NSDebug:
-            self.setWarning(1)
-            self.setInfo(1)
-            self.setDebug(1)
+            self.setWarning(True)
+            self.setInfo(True)
+            self.setDebug(True)
 
-    def getSeverity(self):
+    def getSeverity(self) -> int:
         from panda3d.core import NSDebug, NSInfo, NSWarning, NSError
         if self.getDebug():
             return NSDebug
@@ -120,7 +125,7 @@ class Notifier:
             return NSError
 
     # error funcs
-    def error(self, errorString, exception=NotifierException):
+    def error(self, errorString: object, exception: type[Exception] = NotifierException) -> NoReturn:
         """
         Raise an exception with given string and optional type:
         Exception: error
@@ -134,7 +139,7 @@ class Notifier:
         raise exception(errorString)
 
     # warning funcs
-    def warning(self, warningString):
+    def warning(self, warningString: object) -> int:
         """
         Issue the warning message if warn flag is on
         """
@@ -148,20 +153,20 @@ class Notifier:
             self.__print(string)
         return 1 # to allow assert myNotify.warning("blah")
 
-    def setWarning(self, enable):
+    def setWarning(self, enable: bool) -> None:
         """
         Enable/Disable the printing of warning messages
         """
         self.__warning = enable
 
-    def getWarning(self):
+    def getWarning(self) -> bool:
         """
         Return whether the printing of warning messages is on or off
         """
         return self.__warning
 
     # debug funcs
-    def debug(self, debugString):
+    def debug(self, debugString: object) -> int:
         """
         Issue the debug message if debug flag is on
         """
@@ -175,20 +180,20 @@ class Notifier:
             self.__print(string)
         return 1 # to allow assert myNotify.debug("blah")
 
-    def setDebug(self, enable):
+    def setDebug(self, enable: bool) -> None:
         """
         Enable/Disable the printing of debug messages
         """
         self.__debug = enable
 
-    def getDebug(self):
+    def getDebug(self) -> bool:
         """
         Return whether the printing of debug messages is on or off
         """
         return self.__debug
 
     # info funcs
-    def info(self, infoString):
+    def info(self, infoString: object) -> int:
         """
         Print the given informational string, if info flag is on
         """
@@ -202,39 +207,39 @@ class Notifier:
             self.__print(string)
         return 1 # to allow assert myNotify.info("blah")
 
-    def getInfo(self):
+    def getInfo(self) -> bool:
         """
         Return whether the printing of info messages is on or off
         """
         return self.__info
 
-    def setInfo(self, enable):
+    def setInfo(self, enable: bool) -> None:
         """
         Enable/Disable informational message  printing
         """
         self.__info = enable
 
     # log funcs
-    def __log(self, logEntry):
+    def __log(self, logEntry: str) -> None:
         """
         Determine whether to send informational message to the logger
         """
         if self.__logging:
             self.__logger.log(logEntry)
 
-    def getLogging(self):
+    def getLogging(self) -> bool:
         """
         Return 1 if logging enabled, 0 otherwise
         """
         return self.__logging
 
-    def setLogging(self, enable):
+    def setLogging(self, enable: bool) -> None:
         """
         Set the logging flag to int (1=on, 0=off)
         """
         self.__logging = enable
 
-    def __print(self, string):
+    def __print(self, string: str) -> None:
         """
         Prints the string to output followed by a newline.
         """
@@ -251,7 +256,7 @@ class Notifier:
         the function call (with parameters).
         """
         #f.f_locals['self'].__init__.im_class.__name__
-        if self.__debug:
+        if __debug__ and self.__debug:
             state = ''
             doId = ''
             if obj is not None:
@@ -285,13 +290,13 @@ class Notifier:
             self.__print(string)
         return 1 # to allow assert self.notify.debugStateCall(self)
 
-    def debugCall(self, debugString=''):
+    def debugCall(self, debugString: object = '') -> int:
         """
         If this notify is in debug mode, print the time of the
         call followed by the notifier category and
         the function call (with parameters).
         """
-        if self.__debug:
+        if __debug__ and self.__debug:
             message = str(debugString)
             string = ":%s:%s \"%s\" %s"%(
                 self.getOnlyTime(),

+ 29 - 19
direct/src/directnotify/RotatingLog.py

@@ -1,5 +1,8 @@
+from __future__ import annotations
+
 import os
 import time
+from typing import Iterable
 
 
 class RotatingLog:
@@ -8,7 +11,12 @@ class RotatingLog:
     to a new file if the prior file is too large or after a time interval.
     """
 
-    def __init__(self, path="./log_file", hourInterval=24, megabyteLimit=1024):
+    def __init__(
+        self,
+        path: str = "./log_file",
+        hourInterval: int | None = 24,
+        megabyteLimit: int | None = 1024,
+    ) -> None:
         """
         Args:
             path: a full or partial path with file name.
@@ -28,33 +36,33 @@ class RotatingLog:
         if megabyteLimit is not None:
             self.sizeLimit = megabyteLimit*1024*1024
 
-    def __del__(self):
+    def __del__(self) -> None:
         self.close()
 
-    def close(self):
+    def close(self) -> None:
         if hasattr(self, "file"):
             self.file.flush()
             self.file.close()
             self.closed = self.file.closed
             del self.file
         else:
-            self.closed = 1
+            self.closed = True
 
-    def shouldRotate(self):
+    def shouldRotate(self) -> bool:
         """
         Returns a bool about whether a new log file should
         be created and written to (while at the same time
         stopping output to the old log file and closing it).
         """
         if not hasattr(self, "file"):
-            return 1
+            return True
         if self.timeLimit is not None and time.time() > self.timeLimit:
-            return 1
+            return True
         if self.sizeLimit is not None and self.file.tell() > self.sizeLimit:
-            return 1
-        return 0
+            return True
+        return False
 
-    def filePath(self):
+    def filePath(self) -> str:
         dateString = time.strftime("%Y_%m_%d_%H", time.localtime())
         for i in range(26):
             limit = self.sizeLimit
@@ -65,7 +73,7 @@ class RotatingLog:
         # Maybe we should clear the self.sizeLimit here... maybe.
         return path
 
-    def rotate(self):
+    def rotate(self) -> None:
         """
         Rotate the log now.  You normally shouldn't need to call this.
         See write().
@@ -88,12 +96,13 @@ class RotatingLog:
             #self.newlines = self.file.newlines # Python 2.3, maybe
 
             if self.timeLimit is not None and time.time() > self.timeLimit:
+                assert self.timeInterval is not None
                 self.timeLimit=time.time()+self.timeInterval
         else:
             # We'll keep writing to the old file, if available.
             print("RotatingLog error: Unable to open new log file \"%s\"." % (path,))
 
-    def write(self, data):
+    def write(self, data: str) -> int | None:
         """
         Write the data to either the current log or a new one,
         depending on the return of shouldRotate() and whether
@@ -105,14 +114,15 @@ class RotatingLog:
             r = self.file.write(data)
             self.file.flush()
             return r
+        return None
 
-    def flush(self):
+    def flush(self) -> None:
         return self.file.flush()
 
-    def fileno(self):
+    def fileno(self) -> int:
         return self.file.fileno()
 
-    def isatty(self):
+    def isatty(self) -> bool:
         return self.file.isatty()
 
     def __next__(self):
@@ -131,14 +141,14 @@ class RotatingLog:
     def xreadlines(self):
         return self.file.xreadlines()
 
-    def seek(self, offset, whence=0):
+    def seek(self, offset: int, whence: int = 0) -> int:
         return self.file.seek(offset, whence)
 
-    def tell(self):
+    def tell(self) -> int:
         return self.file.tell()
 
-    def truncate(self, size):
+    def truncate(self, size: int | None) -> int:
         return self.file.truncate(size)
 
-    def writelines(self, sequence):
+    def writelines(self, sequence: Iterable[str]) -> None:
         return self.file.writelines(sequence)

+ 187 - 166
direct/src/directtools/DirectCameraControl.py

@@ -1,6 +1,7 @@
 import math
 from panda3d.core import BitMask32, Mat4, NodePath, Point3, VBase3, Vec3, Vec4, rad2Deg
 from direct.showbase.DirectObject import DirectObject
+from direct.showbase import ShowBaseGlobal
 from .DirectUtil import CLAMP, useDirectRenderStyle
 from .DirectGeometry import getCrankAngle, getScreenXY
 from . import DirectGlobals as DG
@@ -26,7 +27,7 @@ class DirectCameraControl(DirectObject):
         self.orthoViewRoll = 0.0
         self.lastView = 0
         self.coa = Point3(0, 100, 0)
-        self.coaMarker = base.loader.loadModel('models/misc/sphere')
+        self.coaMarker = ShowBaseGlobal.loader.loadModel('models/misc/sphere')
         self.coaMarker.setName('DirectCameraCOAMarker')
         self.coaMarker.setTransparency(1)
         self.coaMarker.setColor(1, 0, 0, 0)
@@ -37,8 +38,8 @@ class DirectCameraControl(DirectObject):
         self.fLockCOA = 0
         self.nullHitPointCount = 0
         self.cqEntries = []
-        self.coaMarkerRef = base.direct.group.attachNewNode('coaMarkerRef')
-        self.camManipRef = base.direct.group.attachNewNode('camManipRef')
+        self.coaMarkerRef = ShowBaseGlobal.direct.group.attachNewNode('coaMarkerRef')
+        self.camManipRef = ShowBaseGlobal.direct.group.attachNewNode('camManipRef')
         self.switchDirBelowZero = True
         self.manipulateCameraTask = None
         self.manipulateCameraInterval = None
@@ -112,11 +113,6 @@ class DirectCameraControl(DirectObject):
         self.perspCollPlane2 = None # [gjeon] used for new LE
 
     def toggleMarkerVis(self):
-##        if base.direct.cameraControl.coaMarker.isHidden():
-##            base.direct.cameraControl.coaMarker.show()
-##        else:
-##            base.direct.cameraControl.coaMarker.hide()
-
         if self.coaMarker.isHidden():
             self.coaMarker.show()
         else:
@@ -132,11 +128,14 @@ class DirectCameraControl(DirectObject):
             # Hide the marker for this kind of motion
             self.coaMarker.hide()
             # Record time of start of mouse interaction
+            base = ShowBaseGlobal.base
             self.startT = base.clock.getFrameTime()
             self.startF = base.clock.getFrameCount()
             # If the cam is orthogonal, spawn differentTask
-            if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView and\
-               base.direct.camera.getName() != 'persp':
+            direct = ShowBaseGlobal.direct
+            if hasattr(direct, "manipulationControl") and \
+               direct.manipulationControl.fMultiView and \
+               direct.camera.getName() != 'persp':
                 self.spawnOrthoZoom()
             else:
                 # Start manipulation
@@ -167,7 +166,9 @@ class DirectCameraControl(DirectObject):
 
     def mouseFlyStart(self, modifiers):
         # Record undo point
-        # base.direct.pushUndo([base.direct.camera])            # Wasteful use of undo
+        base = ShowBaseGlobal.base
+        direct = ShowBaseGlobal.direct
+        #direct.pushUndo([direct.camera])            # Wasteful use of undo
         if self.useMayaCamControls and modifiers == 4:          # alt is down, use maya controls
             # Hide the marker for this kind of motion
             self.coaMarker.hide()
@@ -176,15 +177,16 @@ class DirectCameraControl(DirectObject):
             self.startF = base.clock.getFrameCount()
             # Start manipulation
             # If the cam is orthogonal, spawn differentTask
-            if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView and\
-               base.direct.camera.getName() != 'persp':
+            if hasattr(direct, "manipulationControl") and \
+               direct.manipulationControl.fMultiView and \
+               direct.camera.getName() != 'persp':
                 self.spawnOrthoTranslate()
             else:
                 self.spawnXZTranslate()
             self.altDown = 1
         elif not self.useMayaCamControls:
             # Where are we in the display region?
-            if ((abs(base.direct.dr.mouseX) < 0.9) and (abs(base.direct.dr.mouseY) < 0.9)):
+            if abs(direct.dr.mouseX) < 0.9 and abs(direct.dr.mouseY) < 0.9:
                 # MOUSE IS IN CENTRAL REGION
                 # Hide the marker for this kind of motion
                 self.coaMarker.hide()
@@ -194,19 +196,18 @@ class DirectCameraControl(DirectObject):
                 # Start manipulation
                 self.spawnXZTranslateOrHPanYZoom()
                 # END MOUSE IN CENTRAL REGION
+            elif abs(direct.dr.mouseX) > 0.9 and abs(direct.dr.mouseY) > 0.9:
+                # Mouse is in corners, spawn roll task
+                self.spawnMouseRollTask()
             else:
-                if ((abs(base.direct.dr.mouseX) > 0.9) and
-                    (abs(base.direct.dr.mouseY) > 0.9)):
-                    # Mouse is in corners, spawn roll task
-                    self.spawnMouseRollTask()
-                else:
-                    # Mouse is in outer frame, spawn mouseRotateTask
-                    self.spawnMouseRotateTask()
+                # Mouse is in outer frame, spawn mouseRotateTask
+                self.spawnMouseRotateTask()
         if not modifiers == 4:
             self.altDown = 0
 
     def mouseFlyStop(self):
         self.__stopManipulateCamera()
+        base = ShowBaseGlobal.base
         stopT = base.clock.getFrameTime()
         deltaT = stopT - self.startT
         stopF = base.clock.getFrameCount()
@@ -215,7 +216,8 @@ class DirectCameraControl(DirectObject):
         # if not self.useMayaCamControls and (deltaT <= 0.25) or (deltaF <= 1):
 
         # Do this when not trying to manipulate camera
-        if not self.altDown and len(base.direct.selected.getSelectedAsList()) == 0:
+        direct = ShowBaseGlobal.direct
+        if not self.altDown and len(direct.selected.getSelectedAsList()) == 0:
             # Check for a hit point based on
             # current mouse position
             # Allow intersection with unpickable objects
@@ -224,13 +226,13 @@ class DirectCameraControl(DirectObject):
             skipFlags = DG.SKIP_HIDDEN | DG.SKIP_BACKFACE
             # Skip camera (and its children), unless control key is pressed
             skipFlags |= DG.SKIP_CAMERA * (1 - base.getControl())
-            self.computeCOA(base.direct.iRay.pickGeom(skipFlags = skipFlags))
+            self.computeCOA(direct.iRay.pickGeom(skipFlags = skipFlags))
             # Record reference point
             self.coaMarkerRef.setPosHprScale(base.cam, 0, 0, 0, 0, 0, 0, 1, 1, 1)
             # Record entries
             self.cqEntries = []
-            for i in range(base.direct.iRay.getNumEntries()):
-                self.cqEntries.append(base.direct.iRay.getEntry(i))
+            for i in range(direct.iRay.getNumEntries()):
+                self.cqEntries.append(direct.iRay.getEntry(i))
         # Show the marker
         self.coaMarker.show()
         # Resize it
@@ -251,7 +253,7 @@ class DirectCameraControl(DirectObject):
         # Spawn the new task
         t = Task.Task(self.XZTranslateOrHPanYZoomTask)
         # For HPanYZoom
-        t.zoomSF = Vec3(self.coaMarker.getPos(base.direct.camera)).length()
+        t.zoomSF = Vec3(self.coaMarker.getPos(ShowBaseGlobal.direct.camera)).length()
         self.__startManipulateCamera(task = t)
 
     def spawnXZTranslateOrHPPan(self):
@@ -277,7 +279,7 @@ class DirectCameraControl(DirectObject):
         self.__stopManipulateCamera()
         # Spawn new task
         t = Task.Task(self.HPanYZoomTask)
-        t.zoomSF = Vec3(self.coaMarker.getPos(base.direct.camera)).length()
+        t.zoomSF = Vec3(self.coaMarker.getPos(ShowBaseGlobal.direct.camera)).length()
         self.__startManipulateCamera(task = t)
 
     def spawnOrthoZoom(self):
@@ -294,13 +296,13 @@ class DirectCameraControl(DirectObject):
         self.__startManipulateCamera(func = self.HPPanTask)
 
     def XZTranslateOrHPanYZoomTask(self, state):
-        if base.direct.fShift:
+        if ShowBaseGlobal.direct.fShift:
             return self.XZTranslateTask(state)
         else:
             return self.HPanYZoomTask(state)
 
     def XZTranslateOrHPPanTask(self, state):
-        if base.direct.fShift:
+        if ShowBaseGlobal.direct.fShift:
             # Panning action
             return self.HPPanTask(state)
         else:
@@ -308,43 +310,46 @@ class DirectCameraControl(DirectObject):
             return self.XZTranslateTask(state)
 
     def XZTranslateTask(self, state):
-        coaDist = Vec3(self.coaMarker.getPos(base.direct.camera)).length()
-        xlateSF = coaDist / base.direct.dr.near
-        base.direct.camera.setPos(base.direct.camera,
-                             (-0.5 * base.direct.dr.mouseDeltaX *
-                              base.direct.dr.nearWidth *
+        direct = ShowBaseGlobal.direct
+        coaDist = Vec3(self.coaMarker.getPos(direct.camera)).length()
+        xlateSF = coaDist / direct.dr.near
+        direct.camera.setPos(direct.camera,
+                             (-0.5 * direct.dr.mouseDeltaX *
+                              direct.dr.nearWidth *
                               xlateSF),
                              0.0,
-                             (-0.5 * base.direct.dr.mouseDeltaY *
-                              base.direct.dr.nearHeight *
+                             (-0.5 * direct.dr.mouseDeltaY *
+                              direct.dr.nearHeight *
                               xlateSF))
         return Task.cont
 
     def OrthoTranslateTask(self, state):
         # create ray from the camera to detect 3d position
-        iRay = SelectionRay(base.direct.camera)
-        iRay.collider.setFromLens(base.direct.camNode, base.direct.dr.mouseX, base.direct.dr.mouseY)
+        direct = ShowBaseGlobal.direct
+        iRay = SelectionRay(direct.camera)
+        iRay.collider.setFromLens(direct.camNode, direct.dr.mouseX, direct.dr.mouseY)
         #iRay.collideWithBitMask(1)
         iRay.collideWithBitMask(BitMask32.bit(21))
-        iRay.ct.traverse(base.direct.grid)
+        iRay.ct.traverse(direct.grid)
 
         entry = iRay.getEntry(0)
         hitPt = entry.getSurfacePoint(entry.getFromNodePath())
         iRay.collisionNodePath.removeNode()
         del iRay
         if hasattr(state, 'prevPt'):
-            base.direct.camera.setPos(base.direct.camera, (state.prevPt - hitPt))
+            direct.camera.setPos(direct.camera, (state.prevPt - hitPt))
         state.prevPt = hitPt
         return Task.cont
 
     def HPanYZoomTask(self, state):
         # If the cam is orthogonal, don't rotate or zoom.
-        if (hasattr(base.direct.cam.node(), "getLens") and
-            base.direct.cam.node().getLens().__class__.__name__ == "OrthographicLens"):
+        direct = ShowBaseGlobal.direct
+        if (hasattr(direct.cam.node(), "getLens") and
+            direct.cam.node().getLens().__class__.__name__ == "OrthographicLens"):
             return
 
-        if base.direct.fControl:
-            moveDir = Vec3(self.coaMarker.getPos(base.direct.camera))
+        if direct.fControl:
+            moveDir = Vec3(self.coaMarker.getPos(direct.camera))
             # If marker is behind camera invert vector
             if moveDir[1] < 0.0:
                 moveDir.assign(moveDir * -1)
@@ -353,18 +358,18 @@ class DirectCameraControl(DirectObject):
             moveDir = Vec3(Y_AXIS)
 
         if self.useMayaCamControls: # use maya controls
-            moveDir.assign(moveDir * ((base.direct.dr.mouseDeltaX -1.0 * base.direct.dr.mouseDeltaY)
+            moveDir.assign(moveDir * ((direct.dr.mouseDeltaX -1.0 * direct.dr.mouseDeltaY)
                                     * state.zoomSF))
             hVal = 0.0
         else:
-            moveDir.assign(moveDir * (-1.0 * base.direct.dr.mouseDeltaY *
+            moveDir.assign(moveDir * (-1.0 * direct.dr.mouseDeltaY *
                                         state.zoomSF))
-            if base.direct.dr.mouseDeltaY > 0.0:
+            if direct.dr.mouseDeltaY > 0.0:
                 moveDir.setY(moveDir[1] * 1.0)
 
-            hVal = 0.5 * base.direct.dr.mouseDeltaX * base.direct.dr.fovH
+            hVal = 0.5 * direct.dr.mouseDeltaX * direct.dr.fovH
 
-        base.direct.camera.setPosHpr(base.direct.camera,
+        direct.camera.setPosHpr(direct.camera,
                                 moveDir[0],
                                 moveDir[1],
                                 moveDir[2],
@@ -372,39 +377,42 @@ class DirectCameraControl(DirectObject):
                                 0.0, 0.0)
         if self.lockRoll:
             # flatten roll
-            base.direct.camera.setR(0)
+            direct.camera.setR(0)
 
         return Task.cont
 
     def OrthoZoomTask(self, state):
-        filmSize = base.direct.camNode.getLens().getFilmSize()
-        factor = (base.direct.dr.mouseDeltaX -1.0 * base.direct.dr.mouseDeltaY) * 0.1
-        x = base.direct.dr.getWidth()
-        y = base.direct.dr.getHeight()
-        base.direct.dr.orthoFactor -= factor
-        if base.direct.dr.orthoFactor < 0:
-            base.direct.dr.orthoFactor = 0.0001
-        base.direct.dr.updateFilmSize(x, y)
+        direct = ShowBaseGlobal.direct
+        filmSize = direct.camNode.getLens().getFilmSize()
+        factor = (direct.dr.mouseDeltaX -1.0 * direct.dr.mouseDeltaY) * 0.1
+        x = direct.dr.getWidth()
+        y = direct.dr.getHeight()
+        direct.dr.orthoFactor -= factor
+        if direct.dr.orthoFactor < 0:
+            direct.dr.orthoFactor = 0.0001
+        direct.dr.updateFilmSize(x, y)
         return Task.cont
 
     def HPPanTask(self, state):
-        base.direct.camera.setHpr(base.direct.camera,
-                             (0.5 * base.direct.dr.mouseDeltaX *
-                              base.direct.dr.fovH),
-                             (-0.5 * base.direct.dr.mouseDeltaY *
-                              base.direct.dr.fovV),
+        direct = ShowBaseGlobal.direct
+        direct.camera.setHpr(direct.camera,
+                             (0.5 * direct.dr.mouseDeltaX *
+                              direct.dr.fovH),
+                             (-0.5 * direct.dr.mouseDeltaY *
+                              direct.dr.fovV),
                              0.0)
         return Task.cont
 
     def spawnMouseRotateTask(self):
         # Kill any existing tasks
         self.__stopManipulateCamera()
+        direct = ShowBaseGlobal.direct
         if self.perspCollPlane:
-            iRay = SelectionRay(base.direct.camera)
-            iRay.collider.setFromLens(base.direct.camNode, 0.0, 0.0)
+            iRay = SelectionRay(direct.camera)
+            iRay.collider.setFromLens(direct.camNode, 0.0, 0.0)
             iRay.collideWithBitMask(1)
 
-            if base.direct.camera.getPos().getZ() >=0:
+            if direct.camera.getPos().getZ() >=0:
                 iRay.ct.traverse(self.perspCollPlane)
             else:
                 iRay.ct.traverse(self.perspCollPlane2)
@@ -415,7 +423,7 @@ class DirectCameraControl(DirectObject):
 
                 # create a temp nodePath to get the position
                 np = NodePath('temp')
-                np.setPos(base.direct.camera, hitPt)
+                np.setPos(direct.camera, hitPt)
                 self.coaMarkerPos = np.getPos()
                 np.removeNode()
                 self.coaMarker.setPos(self.coaMarkerPos)
@@ -425,9 +433,9 @@ class DirectCameraControl(DirectObject):
 
         # Set at markers position in render coordinates
         self.camManipRef.setPos(self.coaMarkerPos)
-        self.camManipRef.setHpr(base.direct.camera, DG.ZERO_POINT)
+        self.camManipRef.setHpr(direct.camera, DG.ZERO_POINT)
         t = Task.Task(self.mouseRotateTask)
-        if abs(base.direct.dr.mouseX) > 0.9:
+        if abs(direct.dr.mouseX) > 0.9:
             t.constrainedDir = 'y'
         else:
             t.constrainedDir = 'x'
@@ -435,36 +443,37 @@ class DirectCameraControl(DirectObject):
 
     def mouseRotateTask(self, state):
         # If the cam is orthogonal, don't rotate.
-        if (hasattr(base.direct.cam.node(), "getLens") and
-            base.direct.cam.node().getLens().__class__.__name__ == "OrthographicLens"):
+        direct = ShowBaseGlobal.direct
+        if (hasattr(direct.cam.node(), "getLens") and
+            direct.cam.node().getLens().__class__.__name__ == "OrthographicLens"):
             return
         # If moving outside of center, ignore motion perpendicular to edge
-        if ((state.constrainedDir == 'y') and (abs(base.direct.dr.mouseX) > 0.9)):
+        if ((state.constrainedDir == 'y') and (abs(direct.dr.mouseX) > 0.9)):
             deltaX = 0
-            deltaY = base.direct.dr.mouseDeltaY
-        elif ((state.constrainedDir == 'x') and (abs(base.direct.dr.mouseY) > 0.9)):
-            deltaX = base.direct.dr.mouseDeltaX
+            deltaY = direct.dr.mouseDeltaY
+        elif ((state.constrainedDir == 'x') and (abs(direct.dr.mouseY) > 0.9)):
+            deltaX = direct.dr.mouseDeltaX
             deltaY = 0
         else:
-            deltaX = base.direct.dr.mouseDeltaX
-            deltaY = base.direct.dr.mouseDeltaY
-        if base.direct.fShift:
-            base.direct.camera.setHpr(base.direct.camera,
-                                 (deltaX * base.direct.dr.fovH),
-                                 (-deltaY * base.direct.dr.fovV),
+            deltaX = direct.dr.mouseDeltaX
+            deltaY = direct.dr.mouseDeltaY
+        if direct.fShift:
+            direct.camera.setHpr(direct.camera,
+                                 (deltaX * direct.dr.fovH),
+                                 (-deltaY * direct.dr.fovV),
                                  0.0)
             if self.lockRoll:
                 # flatten roll
-                base.direct.camera.setR(0)
+                direct.camera.setR(0)
             self.camManipRef.setPos(self.coaMarkerPos)
-            self.camManipRef.setHpr(base.direct.camera, DG.ZERO_POINT)
+            self.camManipRef.setHpr(direct.camera, DG.ZERO_POINT)
         else:
-            if base.direct.camera.getPos().getZ() >=0 or not self.switchDirBelowZero:
+            if direct.camera.getPos().getZ() >=0 or not self.switchDirBelowZero:
                 dirX = -1
             else:
                 dirX = 1
 
-            wrt = base.direct.camera.getTransform(self.camManipRef)
+            wrt = direct.camera.getTransform(self.camManipRef)
             self.camManipRef.setHpr(self.camManipRef,
                                     (dirX * deltaX * 180.0),
                                     (deltaY * 180.0),
@@ -473,20 +482,21 @@ class DirectCameraControl(DirectObject):
             if self.lockRoll:
                 # flatten roll
                 self.camManipRef.setR(0)
-            base.direct.camera.setTransform(self.camManipRef, wrt)
+            direct.camera.setTransform(self.camManipRef, wrt)
         return Task.cont
 
     def spawnMouseRollTask(self):
         # Kill any existing tasks
         self.__stopManipulateCamera()
         # Set at markers position in render coordinates
+        direct = ShowBaseGlobal.direct
         self.camManipRef.setPos(self.coaMarkerPos)
-        self.camManipRef.setHpr(base.direct.camera, DG.ZERO_POINT)
+        self.camManipRef.setHpr(direct.camera, DG.ZERO_POINT)
         t = Task.Task(self.mouseRollTask)
         t.coaCenter = getScreenXY(self.coaMarker)
         t.lastAngle = getCrankAngle(t.coaCenter)
         # Store the camera/manipRef offset transform
-        t.wrt = base.direct.camera.getTransform(self.camManipRef)
+        t.wrt = direct.camera.getTransform(self.camManipRef)
         self.__startManipulateCamera(task = t)
 
     def mouseRollTask(self, state):
@@ -498,23 +508,23 @@ class DirectCameraControl(DirectObject):
         if self.lockRoll:
             # flatten roll
             self.camManipRef.setR(0)
-        base.direct.camera.setTransform(self.camManipRef, wrt)
+        ShowBaseGlobal.direct.camera.setTransform(self.camManipRef, wrt)
         return Task.cont
 
     def lockCOA(self):
         self.fLockCOA = 1
-        base.direct.message('COA Lock On')
+        ShowBaseGlobal.direct.message('COA Lock On')
 
     def unlockCOA(self):
         self.fLockCOA = 0
-        base.direct.message('COA Lock Off')
+        ShowBaseGlobal.direct.message('COA Lock Off')
 
     def toggleCOALock(self):
         self.fLockCOA = 1 - self.fLockCOA
         if self.fLockCOA:
-            base.direct.message('COA Lock On')
+            ShowBaseGlobal.direct.message('COA Lock On')
         else:
-            base.direct.message('COA Lock Off')
+            ShowBaseGlobal.direct.message('COA Lock Off')
 
     def pickNextCOA(self):
         """ Cycle through collision handler entries """
@@ -524,7 +534,7 @@ class DirectCameraControl(DirectObject):
             self.cqEntries = self.cqEntries[1:] + self.cqEntries[:1]
             # Filter out object's under camera
             nodePath = entry.getIntoNodePath()
-            if base.direct.camera not in nodePath.getAncestors():
+            if ShowBaseGlobal.direct.camera not in nodePath.getAncestors():
                 # Compute new hit point
                 hitPt = entry.getSurfacePoint(entry.getFromNodePath())
                 # Move coa marker to new point
@@ -536,11 +546,11 @@ class DirectCameraControl(DirectObject):
 
     def computeCOA(self, entry):
         coa = Point3(0)
-        dr = base.direct.drList.getCurrentDr()
+        dr = ShowBaseGlobal.direct.drList.getCurrentDr()
         if self.fLockCOA:
             # COA is locked, use existing point
             # Use existing point
-            coa.assign(self.coaMarker.getPos(base.direct.camera))
+            coa.assign(self.coaMarker.getPos(ShowBaseGlobal.direct.camera))
             # Reset hit point count
             self.nullHitPointCount = 0
         elif entry:
@@ -553,7 +563,7 @@ class DirectCameraControl(DirectObject):
             if ((hitPtDist < (1.1 * dr.near)) or
                 (hitPtDist > dr.far)):
                 # Just use existing point
-                coa.assign(self.coaMarker.getPos(base.direct.camera))
+                coa.assign(self.coaMarker.getPos(ShowBaseGlobal.direct.camera))
             # Reset hit point count
             self.nullHitPointCount = 0
         else:
@@ -565,7 +575,7 @@ class DirectCameraControl(DirectObject):
             # MRM: Would be nice to be able to control this
             # At least display it
             dist = pow(10.0, self.nullHitPointCount)
-            base.direct.message('COA Distance: ' + repr(dist))
+            ShowBaseGlobal.direct.message('COA Distance: ' + repr(dist))
             coa.set(0, dist, 0)
         # Compute COA Dist
         coaDist = Vec3(coa - DG.ZERO_POINT).length()
@@ -583,7 +593,7 @@ class DirectCameraControl(DirectObject):
         if ref is None:
             # KEH: use the current display region
             # ref = base.cam
-            ref = base.direct.drList.getCurrentDr().cam
+            ref = ShowBaseGlobal.direct.drList.getCurrentDr().cam
         self.coaMarker.setPos(ref, self.coa)
         pos = self.coaMarker.getPos()
         self.coaMarker.setPosHprScale(pos, Vec3(0), Vec3(1))
@@ -598,10 +608,10 @@ class DirectCameraControl(DirectObject):
 
     def updateCoaMarkerSize(self, coaDist = None):
         if not coaDist:
-            coaDist = Vec3(self.coaMarker.getPos(base.direct.camera)).length()
+            coaDist = Vec3(self.coaMarker.getPos(ShowBaseGlobal.direct.camera)).length()
         # Nominal size based on default 30 degree vertical FOV
         # Need to adjust size based on distance and current FOV
-        sf = COA_MARKER_SF * coaDist * (base.direct.drList.getCurrentDr().fovV/30.0)
+        sf = COA_MARKER_SF * coaDist * (ShowBaseGlobal.direct.drList.getCurrentDr().fovV/30.0)
         if sf == 0.0:
             sf = 0.1
         self.coaMarker.setScale(sf)
@@ -619,32 +629,36 @@ class DirectCameraControl(DirectObject):
 
     def homeCam(self):
         # Record undo point
-        base.direct.pushUndo([base.direct.camera])
-        base.direct.camera.reparentTo(render)
-        base.direct.camera.clearMat()
+        direct = ShowBaseGlobal.direct
+        direct.pushUndo([direct.camera])
+        direct.camera.reparentTo(ShowBaseGlobal.base.render)
+        direct.camera.clearMat()
         # Resize coa marker
         self.updateCoaMarkerSize()
 
     def uprightCam(self):
         self.__stopManipulateCamera()
         # Record undo point
-        base.direct.pushUndo([base.direct.camera])
+        direct = ShowBaseGlobal.direct
+        direct.pushUndo([direct.camera])
         # Pitch camera till upright
-        currH = base.direct.camera.getH()
-        ival = base.direct.camera.hprInterval(CAM_MOVE_DURATION,
-                                              (currH, 0, 0),
-                                              other = render,
-                                              blendType = 'easeInOut',
-                                              name = 'manipulateCamera')
-        self.__startManipulateCamera(ival = ival)
+        currH = direct.camera.getH()
+        ival = direct.camera.hprInterval(CAM_MOVE_DURATION,
+                                         (currH, 0, 0),
+                                         other=ShowBaseGlobal.base.render,
+                                         blendType='easeInOut',
+                                         name='manipulateCamera')
+        self.__startManipulateCamera(ival=ival)
 
     def orbitUprightCam(self):
         self.__stopManipulateCamera()
         # Record undo point
-        base.direct.pushUndo([base.direct.camera])
+        direct = ShowBaseGlobal.direct
+        direct.pushUndo([direct.camera])
         # Transform camera z axis to render space
+        render = ShowBaseGlobal.base.render
         mCam2Render = Mat4(Mat4.identMat()) # [gjeon] fixed to give required argument
-        mCam2Render.assign(base.direct.camera.getMat(render))
+        mCam2Render.assign(direct.camera.getMat(render))
         zAxis = Vec3(mCam2Render.xformVec(DG.Z_AXIS))
         zAxis.normalize()
         # Compute rotation angle needed to upright cam
@@ -665,8 +679,8 @@ class DirectCameraControl(DirectObject):
         self.camManipRef.setPos(self.coaMarker, Vec3(0))
         self.camManipRef.setHpr(render, rotAngle, 0, 0)
         # Reparent Cam to ref Coordinate system
-        parent = base.direct.camera.getParent()
-        base.direct.camera.wrtReparentTo(self.camManipRef)
+        parent = direct.camera.getParent()
+        direct.camera.wrtReparentTo(self.camManipRef)
         # Rotate ref CS to final orientation
         ival = self.camManipRef.hprInterval(CAM_MOVE_DURATION,
                                             (rotAngle, orbitAngle, 0),
@@ -685,17 +699,18 @@ class DirectCameraControl(DirectObject):
     def centerCamIn(self, t):
         self.__stopManipulateCamera()
         # Record undo point
-        base.direct.pushUndo([base.direct.camera])
+        direct = ShowBaseGlobal.direct
+        direct.pushUndo([direct.camera])
         # Determine marker location
-        markerToCam = self.coaMarker.getPos(base.direct.camera)
+        markerToCam = self.coaMarker.getPos(direct.camera)
         dist = Vec3(markerToCam - DG.ZERO_POINT).length()
         scaledCenterVec = Y_AXIS * dist
         delta = markerToCam - scaledCenterVec
-        self.camManipRef.setPosHpr(base.direct.camera, Point3(0), Point3(0))
-        ival = base.direct.camera.posInterval(CAM_MOVE_DURATION,
-                                              Point3(delta),
-                                              other = self.camManipRef,
-                                              blendType = 'easeInOut')
+        self.camManipRef.setPosHpr(direct.camera, Point3(0), Point3(0))
+        ival = direct.camera.posInterval(CAM_MOVE_DURATION,
+                                         Point3(delta),
+                                         other=self.camManipRef,
+                                         blendType='easeInOut')
         ival = Sequence(ival, Func(self.updateCoaMarkerSizeOnDeath),
                         name = 'manipulateCamera')
         self.__startManipulateCamera(ival = ival)
@@ -703,17 +718,18 @@ class DirectCameraControl(DirectObject):
     def zoomCam(self, zoomFactor, t):
         self.__stopManipulateCamera()
         # Record undo point
-        base.direct.pushUndo([base.direct.camera])
+        direct = ShowBaseGlobal.direct
+        direct.pushUndo([direct.camera])
         # Find a point zoom factor times the current separation
         # of the widget and cam
-        zoomPtToCam = self.coaMarker.getPos(base.direct.camera) * zoomFactor
+        zoomPtToCam = self.coaMarker.getPos(direct.camera) * zoomFactor
         # Put a target nodePath there
-        self.camManipRef.setPos(base.direct.camera, zoomPtToCam)
+        self.camManipRef.setPos(direct.camera, zoomPtToCam)
         # Move to that point
-        ival = base.direct.camera.posInterval(CAM_MOVE_DURATION,
-                                              DG.ZERO_POINT,
-                                              other = self.camManipRef,
-                                              blendType = 'easeInOut')
+        ival = direct.camera.posInterval(CAM_MOVE_DURATION,
+                                         DG.ZERO_POINT,
+                                         other=self.camManipRef,
+                                         blendType='easeInOut')
         ival = Sequence(ival, Func(self.updateCoaMarkerSizeOnDeath),
                         name = 'manipulateCamera')
         self.__startManipulateCamera(ival = ival)
@@ -722,7 +738,8 @@ class DirectCameraControl(DirectObject):
         # Kill any existing tasks
         self.__stopManipulateCamera()
         # Record undo point
-        base.direct.pushUndo([base.direct.camera])
+        direct = ShowBaseGlobal.direct
+        direct.pushUndo([direct.camera])
         # Calc hprOffset
         hprOffset = VBase3()
         if view == 8:
@@ -751,7 +768,7 @@ class DirectCameraControl(DirectObject):
         self.camManipRef.setPosHpr(self.coaMarker, DG.ZERO_VEC,
                                    hprOffset)
         # Scale center vec by current distance to target
-        offsetDistance = Vec3(base.direct.camera.getPos(self.camManipRef) -
+        offsetDistance = Vec3(direct.camera.getPos(self.camManipRef) -
                               DG.ZERO_POINT).length()
         scaledCenterVec = Y_AXIS * (-1.0 * offsetDistance)
         # Now put the camManipRef at that point
@@ -760,11 +777,11 @@ class DirectCameraControl(DirectObject):
                                    DG.ZERO_VEC)
         # Record view for next time around
         self.lastView = view
-        ival = base.direct.camera.posHprInterval(CAM_MOVE_DURATION,
-                                                 pos = DG.ZERO_POINT,
-                                                 hpr = VBase3(0, 0, self.orthoViewRoll),
-                                                 other = self.camManipRef,
-                                                 blendType = 'easeInOut')
+        ival = direct.camera.posHprInterval(CAM_MOVE_DURATION,
+                                            pos=DG.ZERO_POINT,
+                                            hpr=VBase3(0, 0, self.orthoViewRoll),
+                                            other=self.camManipRef,
+                                            blendType='easeInOut')
         ival = Sequence(ival, Func(self.updateCoaMarkerSizeOnDeath),
                         name = 'manipulateCamera')
         self.__startManipulateCamera(ival = ival)
@@ -774,15 +791,16 @@ class DirectCameraControl(DirectObject):
         self.__stopManipulateCamera()
 
         # Record undo point
-        base.direct.pushUndo([base.direct.camera])
+        direct = ShowBaseGlobal.direct
+        direct.pushUndo([direct.camera])
 
         # Coincident with widget
         self.camManipRef.setPos(self.coaMarker, DG.ZERO_POINT)
         # But aligned with render space
         self.camManipRef.setHpr(DG.ZERO_POINT)
 
-        parent = base.direct.camera.getParent()
-        base.direct.camera.wrtReparentTo(self.camManipRef)
+        parent = direct.camera.getParent()
+        direct.camera.wrtReparentTo(self.camManipRef)
 
         ival = self.camManipRef.hprInterval(CAM_MOVE_DURATION,
                                             VBase3(degrees, 0, 0),
@@ -792,7 +810,7 @@ class DirectCameraControl(DirectObject):
         self.__startManipulateCamera(ival = ival)
 
     def reparentCam(self, parent):
-        base.direct.camera.wrtReparentTo(parent)
+        ShowBaseGlobal.direct.camera.wrtReparentTo(parent)
         self.updateCoaMarkerSize()
 
     def fitOnWidget(self, nodePath = 'None Given'):
@@ -800,75 +818,78 @@ class DirectCameraControl(DirectObject):
         # stop any ongoing tasks
         self.__stopManipulateCamera()
         # How big is the node?
-        nodeScale = base.direct.widget.scalingNode.getScale(render)
+        direct = ShowBaseGlobal.direct
+        nodeScale = direct.widget.scalingNode.getScale(ShowBaseGlobal.base.render)
         maxScale = max(nodeScale[0], nodeScale[1], nodeScale[2])
-        maxDim = min(base.direct.dr.nearWidth, base.direct.dr.nearHeight)
+        maxDim = min(direct.dr.nearWidth, direct.dr.nearHeight)
 
         # At what distance does the object fill 30% of the screen?
         # Assuming radius of 1 on widget
-        camY = base.direct.dr.near * (2.0 * maxScale)/(0.3 * maxDim)
+        camY = direct.dr.near * (2.0 * maxScale) / (0.3 * maxDim)
 
         # What is the vector through the center of the screen?
         centerVec = Y_AXIS * camY
 
         # Where is the node relative to the viewpoint
-        vWidget2Camera = base.direct.widget.getPos(base.direct.camera)
+        vWidget2Camera = direct.widget.getPos(direct.camera)
 
         # How far do you move the camera to be this distance from the node?
         deltaMove = vWidget2Camera - centerVec
 
         # Move a target there
         try:
-            self.camManipRef.setPos(base.direct.camera, deltaMove)
+            self.camManipRef.setPos(direct.camera, deltaMove)
         except Exception:
             #self.notify.debug
             pass
 
-        parent = base.direct.camera.getParent()
-        base.direct.camera.wrtReparentTo(self.camManipRef)
-        ival = base.direct.camera.posInterval(CAM_MOVE_DURATION,
-                                              Point3(0, 0, 0),
-                                              blendType = 'easeInOut')
+        parent = direct.camera.getParent()
+        direct.camera.wrtReparentTo(self.camManipRef)
+        ival = direct.camera.posInterval(CAM_MOVE_DURATION,
+                                         Point3(0, 0, 0),
+                                         blendType='easeInOut')
         ival = Sequence(ival, Func(self.reparentCam, parent),
-                        name = 'manipulateCamera')
-        self.__startManipulateCamera(ival = ival)
+                        name='manipulateCamera')
+        self.__startManipulateCamera(ival=ival)
 
     def moveToFit(self):
         # How big is the active widget?
-        widgetScale = base.direct.widget.scalingNode.getScale(render)
+        direct = ShowBaseGlobal.direct
+        widgetScale = direct.widget.scalingNode.getScale(ShowBaseGlobal.base.render)
         maxScale = max(widgetScale[0], widgetScale[1], widgetScale[2])
         # At what distance does the widget fill 50% of the screen?
-        camY = ((2 * base.direct.dr.near * (1.5 * maxScale)) /
-                min(base.direct.dr.nearWidth, base.direct.dr.nearHeight))
+        camY = ((2 * direct.dr.near * (1.5 * maxScale)) /
+                min(direct.dr.nearWidth, direct.dr.nearHeight))
         # Find a point this distance along the Y axis
         # MRM: This needs to be generalized to support non uniform frusta
         centerVec = Y_AXIS * camY
         # Before moving, record the relationship between the selected nodes
         # and the widget, so that this can be maintained
-        base.direct.selected.getWrtAll()
+        direct.selected.getWrtAll()
         # Push state onto undo stack
-        base.direct.pushUndo(base.direct.selected)
+        direct.pushUndo(direct.selected)
         # Remove the task to keep the widget attached to the object
         taskMgr.remove('followSelectedNodePath')
         # Spawn a task to keep the selected objects with the widget
         taskMgr.add(self.stickToWidgetTask, 'stickToWidget')
         # Spawn a task to move the widget
-        ival = base.direct.widget.posInterval(CAM_MOVE_DURATION,
-                                              Point3(centerVec),
-                                              other = base.direct.camera,
-                                              blendType = 'easeInOut')
+        ival = direct.widget.posInterval(CAM_MOVE_DURATION,
+                                         Point3(centerVec),
+                                         other=direct.camera,
+                                         blendType='easeInOut')
         ival = Sequence(ival, Func(lambda: taskMgr.remove('stickToWidget')),
                         name = 'moveToFit')
         ival.start()
 
     def stickToWidgetTask(self, state):
         # Move the objects with the widget
-        base.direct.selected.moveWrtWidgetAll()
+        ShowBaseGlobal.direct.selected.moveWrtWidgetAll()
         # Continue
         return Task.cont
 
     def enableMouseFly(self, fKeyEvents = 1):
         # disable C++ fly interface
+        base = ShowBaseGlobal.base
         base.disableMouse()
         # Enable events
         for event in self.actionEvents:
@@ -877,11 +898,11 @@ class DirectCameraControl(DirectObject):
             for event in self.keyEvents:
                 self.accept(event[0], event[1], extraArgs = event[2:])
         # Show marker
-        self.coaMarker.reparentTo(base.direct.group)
+        self.coaMarker.reparentTo(ShowBaseGlobal.direct.group)
 
     def disableMouseFly(self):
         # Hide the marker
-        self.coaMarker.reparentTo(hidden)
+        self.coaMarker.reparentTo(ShowBaseGlobal.hidden)
         # Ignore events
         for event in self.actionEvents:
             self.ignore(event[0])
@@ -890,7 +911,7 @@ class DirectCameraControl(DirectObject):
         # Kill tasks
         self.removeManipulateCameraTask()
         taskMgr.remove('stickToWidget')
-        base.enableMouse()
+        ShowBaseGlobal.base.enableMouse()
 
     def removeManipulateCameraTask(self):
         self.__stopManipulateCamera()

+ 4 - 3
direct/src/directtools/DirectGrid.py

@@ -1,6 +1,7 @@
 import math
 from panda3d.core import NodePath, Point3, VBase4
 from direct.showbase.DirectObject import DirectObject
+from direct.showbase import ShowBaseGlobal
 from .DirectUtil import ROUND_TO, useDirectRenderStyle
 from .DirectGeometry import LineNodePath
 
@@ -14,7 +15,7 @@ class DirectGrid(NodePath, DirectObject):
 
         # Load up grid parts to initialize grid object
         # Polygon used to mark grid plane
-        self.gridBack = base.loader.loadModel('models/misc/gridBack')
+        self.gridBack = ShowBaseGlobal.loader.loadModel('models/misc/gridBack')
         self.gridBack.reparentTo(self)
         self.gridBack.setColor(*planeColor)
 
@@ -36,7 +37,7 @@ class DirectGrid(NodePath, DirectObject):
         self.centerLines.setThickness(3)
 
         # Small marker to hilight snap-to-grid point
-        self.snapMarker = base.loader.loadModel('models/misc/sphere')
+        self.snapMarker = ShowBaseGlobal.loader.loadModel('models/misc/sphere')
         self.snapMarker.node().setName('gridSnapMarker')
         self.snapMarker.reparentTo(self)
         self.snapMarker.setColor(1, 0, 0, 1)
@@ -55,7 +56,7 @@ class DirectGrid(NodePath, DirectObject):
         if parent:
             self.reparentTo(parent)
         else:
-            self.reparentTo(base.direct.group)
+            self.reparentTo(ShowBaseGlobal.direct.group)
 
         self.updateGrid()
         self.fEnabled = 1

Plik diff jest za duży
+ 204 - 180
direct/src/directtools/DirectManipulation.py


+ 59 - 39
direct/src/directtools/DirectSession.py

@@ -5,6 +5,7 @@ from panda3d.core import (
     ConfigVariableBool,
     ConfigVariableString,
     CSDefault,
+    GraphicsWindow,
     NodePath,
     Point3,
     TextNode,
@@ -35,19 +36,33 @@ from direct.gui import OnscreenText
 from direct.interval.IntervalGlobal import Func, Sequence
 from direct.task.TaskManagerGlobal import taskMgr
 from direct.showbase.MessengerGlobal import messenger
+from direct.showbase import ShowBaseGlobal
+from direct.showbase.ShowBaseGlobal import ShowBase, hidden
 
+import builtins
+
+base: ShowBase
 
 class DirectSession(DirectObject):
 
     # post this to the bboard to make sure DIRECT doesn't turn on
     DIRECTdisablePost = 'disableDIRECT'
 
+    cam: NodePath
+    camera: NodePath
+    oobeCamera: NodePath
+
     def __init__(self):
         # Establish a global pointer to the direct object early on
         # so dependant classes can access it in their code
-        __builtins__["direct"] = base.direct = self
+        global direct, base
+        base = ShowBaseGlobal.base
+        base.direct = self
+        setattr(builtins, 'direct', self)
+        ShowBaseGlobal.direct = self
+
         # These come early since they are used later on
-        self.group = render.attachNewNode('DIRECT')
+        self.group = base.render.attachNewNode('DIRECT')
         self.font = TextNode.getDefaultFont()
         self.fEnabled = 0
         self.fEnabledLight = 0
@@ -57,7 +72,7 @@ class DirectSession(DirectObject):
         self.drList = DisplayRegionList()
         self.iRayList = [x.iRay for x in self.drList]
         self.dr = self.drList[0]
-        self.win = base.win
+        self.win: GraphicsWindow = base.win
         self.camera = base.camera
         self.cam = base.cam
         self.camNode = base.camNode
@@ -70,7 +85,7 @@ class DirectSession(DirectObject):
         self.useObjectHandles()
         self.grid = DirectGrid()
         self.grid.disable()
-        self.lights = DirectLights(base.direct.group)
+        self.lights = DirectLights(self.group)
         # Create some default lights
         self.lights.createDefaultLights()
         # But turn them off
@@ -308,13 +323,16 @@ class DirectSession(DirectObject):
         if base.wantTk:
             from direct.tkpanels import DirectSessionPanel
             self.panel = DirectSessionPanel.DirectSessionPanel(parent = base.tkRoot)
-        try:
+
+        clusterMode: str
+        if hasattr(builtins, 'clusterMode'):
             # Has the clusterMode been set externally (i.e. via the
             # bootstrap application?
-            self.clusterMode = clusterMode
-        except NameError:
+            clusterMode = builtins.clusterMode
+        else:
             # Has the clusterMode been set via a config variable?
-            self.clusterMode = ConfigVariableString("cluster-mode", '').value
+            clusterMode = ConfigVariableString("cluster-mode", '').value
+        self.clusterMode = clusterMode
 
         if self.clusterMode == 'client':
             from direct.cluster.ClusterClient import createClusterClient
@@ -325,7 +343,7 @@ class DirectSession(DirectObject):
         else:
             from direct.cluster.ClusterClient import DummyClusterClient
             self.cluster = DummyClusterClient()
-        __builtins__['cluster'] = self.cluster
+        setattr(builtins, 'cluster', self.cluster)
 
     def addPassThroughKey(self,key):
 
@@ -412,10 +430,10 @@ class DirectSession(DirectObject):
 
         if self.oobeMode:
             # Position a target point to lerp the oobe camera to
-            base.direct.cameraControl.camManipRef.setPosHpr(self.trueCamera, 0, 0, 0, 0, 0, 0)
+            self.cameraControl.camManipRef.setPosHpr(self.trueCamera, 0, 0, 0, 0, 0, 0)
             ival = self.oobeCamera.posHprInterval(
                 2.0, pos = Point3(0), hpr = Vec3(0),
-                other = base.direct.cameraControl.camManipRef,
+                other = self.cameraControl.camManipRef,
                 blendType = 'easeInOut')
             ival = Sequence(ival, Func(self.endOOBE), name = 'oobeTransition')
             ival.start()
@@ -432,20 +450,20 @@ class DirectSession(DirectObject):
             # Put camera under new oobe camera
             self.cam.reparentTo(self.oobeCamera)
             # Position a target point to lerp the oobe camera to
-            base.direct.cameraControl.camManipRef.setPos(
+            self.cameraControl.camManipRef.setPos(
                 self.trueCamera, Vec3(-2, -20, 5))
-            base.direct.cameraControl.camManipRef.lookAt(self.trueCamera)
+            self.cameraControl.camManipRef.lookAt(self.trueCamera)
             ival = self.oobeCamera.posHprInterval(
                 2.0, pos = Point3(0), hpr = Vec3(0),
-                other = base.direct.cameraControl.camManipRef,
+                other = self.cameraControl.camManipRef,
                 blendType = 'easeInOut')
             ival = Sequence(ival, Func(self.beginOOBE), name = 'oobeTransition')
             ival.start()
 
     def beginOOBE(self):
         # Make sure we've reached our final destination
-        self.oobeCamera.setPosHpr(base.direct.cameraControl.camManipRef, 0, 0, 0, 0, 0, 0)
-        base.direct.camera = self.oobeCamera
+        self.oobeCamera.setPosHpr(self.cameraControl.camManipRef, 0, 0, 0, 0, 0, 0)
+        self.camera = self.oobeCamera
         self.oobeMode = 1
 
     def endOOBE(self):
@@ -453,7 +471,7 @@ class DirectSession(DirectObject):
         self.oobeCamera.setPosHpr(self.trueCamera, 0, 0, 0, 0, 0, 0)
         # Disable OOBE mode.
         self.cam.reparentTo(self.trueCamera)
-        base.direct.camera = self.trueCamera
+        self.camera = self.trueCamera
         # Get rid of ancillary node paths
         self.oobeVis.reparentTo(hidden)
         self.oobeCamera.reparentTo(hidden)
@@ -501,7 +519,7 @@ class DirectSession(DirectObject):
     def inputHandler(self, input):
         if not hasattr(self, 'oobeMode') or self.oobeMode == 0:
             # [gjeon] change current camera dr, iRay, mouseWatcher accordingly to support multiple windows
-            if base.direct.manipulationControl.fMultiView:
+            if self.manipulationControl.fMultiView:
                 # handling orphan events
                 if self.fMouse1 and 'mouse1' not in input or\
                    self.fMouse2 and 'mouse2' not in input or\
@@ -518,7 +536,7 @@ class DirectSession(DirectObject):
                     return
 
                 if (self.fMouse1 or self.fMouse2 or self.fMouse3) and\
-                   input[4:7] != base.direct.camera.getName()[:3] and\
+                   input[4:7] != self.camera.getName()[:3] and\
                    input.endswith('-up'):
                     # to handle orphan events
                     return
@@ -551,14 +569,14 @@ class DirectSession(DirectObject):
                     self.cam = NodePath(winCtrl.camNode)
                     self.camNode = winCtrl.camNode
                     if hasattr(winCtrl, 'grid'):
-                        base.direct.grid = winCtrl.grid
-                    base.direct.dr = base.direct.drList[base.camList.index(NodePath(winCtrl.camNode))]
-                    base.direct.iRay = base.direct.dr.iRay
+                        self.grid = winCtrl.grid
+                    self.dr = self.drList[base.camList.index(NodePath(winCtrl.camNode))]
+                    self.iRay = self.dr.iRay
                     base.mouseWatcher = winCtrl.mouseWatcher
                     base.mouseWatcherNode = winCtrl.mouseWatcher.node()
-                    base.direct.dr.mouseUpdate()
+                    self.dr.mouseUpdate()
                     DG.LE_showInOneCam(self.selectedNPReadout, self.camera.getName())
-                    base.direct.widget = base.direct.manipulationControl.widgetList[base.camList.index(NodePath(winCtrl.camNode))]
+                    self.widget = self.manipulationControl.widgetList[base.camList.index(NodePath(winCtrl.camNode))]
 
                 input = input[8:] # get rid of camera prefix
                 if self.fAlt and 'alt' not in input and not input.endswith('-up'):
@@ -683,20 +701,18 @@ class DirectSession(DirectObject):
         if not taskMgr.hasTaskNamed('resizeObjectHandles'):
             dnp = self.selected.last
             if dnp:
-                direct = base.direct
-
                 if self.manipulationControl.fMultiView:
                     for i in range(3):
-                        sf = 30.0 * direct.drList[i].orthoFactor
+                        sf = 30.0 * self.drList[i].orthoFactor
                         self.manipulationControl.widgetList[i].setDirectScalingFactor(sf)
 
                     nodeCamDist = Vec3(dnp.getPos(base.camList[3])).length()
-                    sf = 0.075 * nodeCamDist * math.tan(deg2Rad(direct.drList[3].fovV))
+                    sf = 0.075 * nodeCamDist * math.tan(deg2Rad(self.drList[3].fovV))
                     self.manipulationControl.widgetList[3].setDirectScalingFactor(sf)
 
                 else:
-                    nodeCamDist = Vec3(dnp.getPos(direct.camera)).length()
-                    sf = 0.075 * nodeCamDist * math.tan(deg2Rad(direct.drList.getCurrentDr().fovV))
+                    nodeCamDist = Vec3(dnp.getPos(self.camera)).length()
+                    sf = 0.075 * nodeCamDist * math.tan(deg2Rad(self.drList.getCurrentDr().fovV))
                     self.widget.setDirectScalingFactor(sf)
         return Task.cont
 
@@ -755,7 +771,7 @@ class DirectSession(DirectObject):
             messenger.send('DIRECT_selectedNodePath_fMulti_fTag_fLEPane', [dnp, fMultiSelect, fSelectTag, fLEPane])
 
     def followSelectedNodePathTask(self, state):
-        mCoa2Render = state.dnp.mCoa2Dnp * state.dnp.getMat(render)
+        mCoa2Render = state.dnp.mCoa2Dnp * state.dnp.getMat(base.render)
         decomposeMatrix(mCoa2Render,
                         self.scale, self.hpr, self.pos,
                         CSDefault)
@@ -874,7 +890,7 @@ class DirectSession(DirectObject):
         if nodePath == 'None Given':
             # If nothing specified, try selected node path
             nodePath = self.selected.last
-        base.direct.select(nodePath)
+        self.select(nodePath)
 
         def fitTask(state, self = self):
             self.cameraControl.fitOnWidget()
@@ -1061,7 +1077,7 @@ class DirectSession(DirectObject):
 
     def useObjectHandles(self):
         self.widget = self.manipulationControl.objectHandles
-        self.widget.reparentTo(base.direct.group)
+        self.widget.reparentTo(self.group)
 
     def hideSelectedNPReadout(self):
         self.selectedNPReadout.reparentTo(hidden)
@@ -1167,14 +1183,14 @@ class DisplayRegionContext(DirectObject):
             self.camLens.setFov(hfov, vfov)
 
     def getWidth(self):
-        prop = base.direct.win.getProperties()
+        prop = ShowBaseGlobal.direct.win.getProperties()
         if prop.hasSize():
             return prop.getXSize()
         else:
             return 640
 
     def getHeight(self):
-        prop = base.direct.win.getProperties()
+        prop = ShowBaseGlobal.direct.win.getProperties()
         if prop.hasSize():
             return prop.getYSize()
         else:
@@ -1208,9 +1224,10 @@ class DisplayRegionContext(DirectObject):
 
         # Values for this frame
         # This ranges from -1 to 1
-        if base.mouseWatcherNode and base.mouseWatcherNode.hasMouse():
-            self.mouseX = base.mouseWatcherNode.getMouseX()
-            self.mouseY = base.mouseWatcherNode.getMouseY()
+        mouseWatcherNode = base.mouseWatcherNode
+        if mouseWatcherNode and mouseWatcherNode.hasMouse():
+            self.mouseX = mouseWatcherNode.getMouseX()
+            self.mouseY = mouseWatcherNode.getMouseY()
             self.mouseX = (self.mouseX-self.originX)*self.scaleX
             self.mouseY = (self.mouseY-self.originY)*self.scaleY
         # Delta percent of window the mouse moved
@@ -1262,6 +1279,9 @@ class DisplayRegionList(DirectObject):
     def __len__(self):
         return len(self.displayRegionList)
 
+    def __iter__(self):
+        return iter(self.displayRegionList)
+
     def updateContext(self):
         self.contextTask(None)
 
@@ -1296,7 +1316,7 @@ class DisplayRegionList(DirectObject):
 
     def getCurrentDr(self):
         if not self.tryToGetCurrentDr:
-            return base.direct.dr
+            return ShowBaseGlobal.direct.dr
         for dr in self.displayRegionList:
             if (dr.mouseX >= -1.0 and dr.mouseX <= 1.0 and
                 dr.mouseY >= -1.0 and dr.mouseY <= 1.0):

+ 59 - 30
direct/src/dist/FreezeTool.py

@@ -5,7 +5,6 @@ import modulefinder
 import sys
 import os
 import marshal
-import imp
 import platform
 import struct
 import io
@@ -13,6 +12,7 @@ import sysconfig
 import zipfile
 import importlib
 import warnings
+from importlib import machinery
 
 from . import pefile
 
@@ -24,6 +24,16 @@ except ImportError:
 
 from panda3d.core import Filename, Multifile, PandaSystem, StringStream
 
+# Old imp constants.
+_PY_SOURCE = 1
+_PY_COMPILED = 2
+_C_EXTENSION = 3
+_PKG_DIRECTORY = 5
+_C_BUILTIN = 6
+_PY_FROZEN = 7
+
+_PKG_NAMESPACE_DIRECTORY = object()
+
 # Check to see if we are running python_d, which implies we have a
 # debug build, and we have to build the module with debug options.
 # This is only relevant on Windows.
@@ -37,7 +47,7 @@ isDebugBuild = (python.lower().endswith('_d'))
 # NB. if encodings are removed, be sure to remove them from the shortcut in
 # deploy-stub.c.
 startupModules = [
-    'imp', 'encodings', 'encodings.*', 'io', 'marshal', 'importlib.machinery',
+    'encodings', 'encodings.*', 'io', 'marshal', 'importlib.machinery',
     'importlib.util',
 ]
 
@@ -262,10 +272,15 @@ class CompilationEnvironment:
                 self.arch = '-arch x86_64'
             elif proc in ('arm64', 'aarch64'):
                 self.arch = '-arch arm64'
-            self.compileObjExe = "gcc -c %(arch)s -o %(basename)s.o -O2 -I%(pythonIPath)s %(filename)s"
-            self.compileObjDll = "gcc -fPIC -c %(arch)s -o %(basename)s.o -O2 -I%(pythonIPath)s %(filename)s"
-            self.linkExe = "gcc %(arch)s -o %(basename)s %(basename)s.o -framework Python"
-            self.linkDll = "gcc %(arch)s -undefined dynamic_lookup -bundle -o %(basename)s.so %(basename)s.o"
+            self.compileObjExe = "clang -c %(arch)s -o %(basename)s.o -O2 -I%(pythonIPath)s %(filename)s"
+            self.compileObjDll = "clang -fPIC -c %(arch)s -o %(basename)s.o -O2 -I%(pythonIPath)s %(filename)s"
+            self.linkExe = "clang %(arch)s -o %(basename)s %(basename)s.o"
+            if '/Python.framework/' in self.PythonIPath:
+                framework_dir = self.PythonIPath.split("/Python.framework/", 1)[0]
+                if framework_dir != "/System/Library/Frameworks":
+                    self.linkExe += " -F " + framework_dir
+            self.linkExe += " -framework Python"
+            self.linkDll = "clang %(arch)s -undefined dynamic_lookup -bundle -o %(basename)s.so %(basename)s.o"
 
         else:
             # Unix
@@ -897,12 +912,11 @@ class Freezer:
 
         # Suffix/extension for Python C extension modules
         if self.platform == PandaSystem.getPlatform():
-            suffixes = imp.get_suffixes()
-
-            # Set extension for Python files to binary mode
-            for i, suffix in enumerate(suffixes):
-                if suffix[2] == imp.PY_SOURCE:
-                    suffixes[i] = (suffix[0], 'rb', imp.PY_SOURCE)
+            suffixes = (
+                [(s, 'rb', _C_EXTENSION) for s in machinery.EXTENSION_SUFFIXES] +
+                [(s, 'rb', _PY_SOURCE) for s in machinery.SOURCE_SUFFIXES] +
+                [(s, 'rb', _PY_COMPILED) for s in machinery.BYTECODE_SUFFIXES]
+            )
         else:
             suffixes = [('.py', 'rb', 1), ('.pyc', 'rb', 2)]
 
@@ -1316,11 +1330,11 @@ class Freezer:
             ext = mdef.filename.getExtension()
             if ext == 'pyc' or ext == 'pyo':
                 fp = open(pathname, 'rb')
-                stuff = ("", "rb", imp.PY_COMPILED)
+                stuff = ("", "rb", _PY_COMPILED)
                 self.mf.load_module(mdef.moduleName, fp, pathname, stuff)
             else:
-                stuff = ("", "rb", imp.PY_SOURCE)
-                if mdef.text:
+                stuff = ("", "rb", _PY_SOURCE)
+                if mdef.text is not None:
                     fp = io.StringIO(mdef.text)
                 else:
                     fp = open(pathname, 'rb')
@@ -1415,7 +1429,7 @@ class Freezer:
 
     def __addPyc(self, multifile, filename, code, compressionLevel):
         if code:
-            data = imp.get_magic() + b'\0\0\0\0\0\0\0\0'
+            data = importlib.util.MAGIC_NUMBER + b'\0\0\0\0\0\0\0\0'
             data += marshal.dumps(code)
 
             stream = StringStream(data)
@@ -1605,7 +1619,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=self.optimize)
+                code = compile('import sys;del sys.modules["%s"];from importlib._bootstrap import _builtin_from_name;_builtin_from_name("%s")' % (moduleName, moduleName), moduleName, 'exec', optimize=self.optimize)
                 code = marshal.dumps(code)
                 mangledName = self.mangleName(moduleName)
                 moduleDefs.append(self.makeModuleDef(mangledName, code))
@@ -1887,9 +1901,19 @@ class Freezer:
             if '.' in moduleName and not self.platform.startswith('android'):
                 if self.platform.startswith("macosx") and not use_console:
                     # We write the Frameworks directory to sys.path[0].
-                    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)
+                    direxpr = 'sys.path[0]'
                 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)
+                    direxpr = 'os.path.dirname(sys.executable)'
+
+                code = \
+                    f'import sys;' \
+                    f'del sys.modules["{moduleName}"];' \
+                    f'import sys,os;' \
+                    f'from importlib.machinery import ExtensionFileLoader,ModuleSpec;' \
+                    f'from importlib._bootstrap import _load;' \
+                    f'path=os.path.join({direxpr}, "{moduleName}{modext}");' \
+                    f'_load(ModuleSpec(name="{moduleName}", loader=ExtensionFileLoader("{moduleName}", path), origin=path))'
+
                 code = compile(code, moduleName, 'exec', optimize=self.optimize)
                 code = marshal.dumps(code)
                 moduleList.append((moduleName, len(pool), len(code)))
@@ -1924,6 +1948,9 @@ class Freezer:
         if self.platform.startswith('win'):
             # We don't use mmap on Windows.  Align just for good measure.
             blob_align = 32
+        elif self.platform.endswith('_aarch64') or self.platform.endswith('_arm64'):
+            # Most arm64 operating systems are configured with 16 KiB pages.
+            blob_align = 16384
         else:
             # Align to page size, so that it can be mmapped.
             blob_align = 4096
@@ -2400,9 +2427,6 @@ class Freezer:
         return True
 
 
-_PKG_NAMESPACE_DIRECTORY = object()
-
-
 class PandaModuleFinder(modulefinder.ModuleFinder):
 
     def __init__(self, *args, **kw):
@@ -2415,7 +2439,12 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
         self.builtin_module_names = kw.pop('builtin_module_names', sys.builtin_module_names)
 
-        self.suffixes = kw.pop('suffixes', imp.get_suffixes())
+        self.suffixes = kw.pop('suffixes', (
+            [(s, 'rb', _C_EXTENSION) for s in machinery.EXTENSION_SUFFIXES] +
+            [(s, 'r', _PY_SOURCE) for s in machinery.SOURCE_SUFFIXES] +
+            [(s, 'rb', _PY_COMPILED) for s in machinery.BYTECODE_SUFFIXES]
+        ))
+
         self.optimize = kw.pop('optimize', -1)
 
         modulefinder.ModuleFinder.__init__(self, *args, **kw)
@@ -2563,7 +2592,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
         suffix, mode, type = file_info
         self.msgin(2, "load_module", fqname, fp and "fp", pathname)
-        if type == imp.PKG_DIRECTORY:
+        if type == _PKG_DIRECTORY:
             m = self.load_package(fqname, pathname)
             self.msgout(2, "load_module ->", m)
             return m
@@ -2574,7 +2603,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
             m.__path__ = pathname
             return m
 
-        if type == imp.PY_SOURCE:
+        if type == _PY_SOURCE:
             if fqname in overrideModules:
                 # This module has a custom override.
                 code = overrideModules[fqname]
@@ -2598,7 +2627,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
             code += b'\n' if isinstance(code, bytes) else '\n'
             co = compile(code, pathname, 'exec', optimize=self.optimize)
-        elif type == imp.PY_COMPILED:
+        elif type == _PY_COMPILED:
             if sys.version_info >= (3, 7):
                 try:
                     data = fp.read()
@@ -2752,11 +2781,11 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
         # If we have a custom override for this module, we know we have it.
         if fullname in overrideModules:
-            return (None, '', ('.py', 'r', imp.PY_SOURCE))
+            return (None, '', ('.py', 'r', _PY_SOURCE))
 
         # It's built into the interpreter.
         if fullname in self.builtin_module_names:
-            return (None, None, ('', '', imp.C_BUILTIN))
+            return (None, None, ('', '', _C_BUILTIN))
 
         # If no search path is given, look for a built-in module.
         if path is None:
@@ -2806,7 +2835,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
             for suffix, mode, _ in self.suffixes:
                 init = os.path.join(basename, '__init__' + suffix)
                 if self._open_file(init, mode):
-                    return (None, basename, ('', '', imp.PKG_DIRECTORY))
+                    return (None, basename, ('', '', _PKG_DIRECTORY))
 
             # This may be a namespace package.
             if self._dir_exists(basename):
@@ -2818,7 +2847,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
             # Only if we're not looking on a particular path, though.
             if p3extend_frozen and p3extend_frozen.is_frozen_module(name):
                 # It's a frozen module.
-                return (None, name, ('', '', imp.PY_FROZEN))
+                return (None, name, ('', '', _PY_FROZEN))
 
         # If we found folders on the path with this module name without an
         # __init__.py file, we should consider this a namespace package.

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

@@ -13,7 +13,6 @@ import re
 import shutil
 import stat
 import struct
-import imp
 import string
 import tempfile
 
@@ -55,10 +54,16 @@ def _register_python_loaders():
 
     _register_python_loaders.done = True
 
-    registry = p3d.LoaderFileTypeRegistry.getGlobalPtr()
+    from importlib.metadata import entry_points
 
-    import pkg_resources
-    for entry_point in pkg_resources.iter_entry_points('panda3d.loaders'):
+    eps = entry_points()
+    if isinstance(eps, dict): # Python 3.8 and 3.9
+        loaders = eps.get('panda3d.loaders', ())
+    else:
+        loaders = eps.select(group='panda3d.loaders')
+
+    registry = p3d.LoaderFileTypeRegistry.get_global_ptr()
+    for entry_point in loaders:
         registry.register_deferred_type(entry_point)
 
 
@@ -717,6 +722,7 @@ class build_apps(setuptools.Command):
             'CFBundlePackageType': 'APPL',
             'CFBundleSignature': '', #TODO
             'CFBundleExecutable': self.macos_main_app,
+            'NSHighResolutionCapable': 'True',
         }
 
         icon = self.icon_objects.get(
@@ -1067,7 +1073,7 @@ class build_apps(setuptools.Command):
             freezer_extras.update(freezer.extras)
             freezer_modules.update(freezer.getAllModuleNames())
             for suffix in freezer.mf.suffixes:
-                if suffix[2] == imp.C_EXTENSION:
+                if suffix[2] == 3: # imp.C_EXTENSION:
                     ext_suffixes.add(suffix[0])
 
         for appname, scriptname in self.gui_apps.items():
@@ -1698,7 +1704,7 @@ class bdist_apps(setuptools.Command):
             setattr(self, opt, None)
 
     def finalize_options(self):
-        import pkg_resources
+        from importlib.metadata import entry_points
 
         # We need to massage the inputs a bit in case they came from a
         # setup.cfg file.
@@ -1712,11 +1718,17 @@ class bdist_apps(setuptools.Command):
             self.signing_certificate = os.path.abspath(self.signing_certificate)
             self.signing_private_key = os.path.abspath(self.signing_private_key)
 
+        eps = entry_points()
+        if isinstance(eps, dict): # Python 3.8 and 3.9
+            installer_eps = eps.get('panda3d.bdist_apps.installers', ())
+        else:
+            installer_eps = eps.select(group='panda3d.bdist_apps.installers')
+
         tmp = self.DEFAULT_INSTALLER_FUNCS.copy()
         tmp.update(self.installer_functions)
         tmp.update({
             entrypoint.name: entrypoint.load()
-            for entrypoint in pkg_resources.iter_entry_points('panda3d.bdist_apps.installers')
+            for entrypoint in installer_eps
         })
         self.installer_functions = tmp
 

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

@@ -23,7 +23,7 @@ GRID_Z_OFFSET = 0.0
 
 class DistributedCartesianGrid(DistributedNode, CartesianGridBase):
     notify = directNotify.newCategory("DistributedCartesianGrid")
-    notify.setDebug(0)
+    notify.setDebug(False)
 
     VisualizeGrid = ConfigVariableBool("visualize-cartesian-grid", False)
 

+ 3 - 0
direct/src/distributed/MsgTypes.py

@@ -42,6 +42,9 @@ CONTROL_ADD_RANGE =                               9002
 CONTROL_REMOVE_RANGE =                            9003
 CONTROL_ADD_POST_REMOVE =                         9010
 CONTROL_CLEAR_POST_REMOVES =                      9011
+CONTROL_SET_CON_NAME =                            9012
+CONTROL_SET_CON_URL =                             9013
+CONTROL_LOG_MESSAGE =                             9014
 
 # State Server control messages:
 STATESERVER_CREATE_OBJECT_WITH_REQUIRED =         2000

+ 12 - 4
direct/src/extensions_native/NodePath_extensions.py

@@ -435,7 +435,8 @@ Dtool_funcToMethod(iPosHprScale, NodePath)
 del iPosHprScale
 #####################################################################
 def place(self):
-    base.startDirect(fWantTk = 1)
+    from direct.showbase import ShowBaseGlobal
+    ShowBaseGlobal.base.startDirect(fWantTk = 1)
     # Don't use a regular import, to prevent ModuleFinder from picking
     # it up as a dependency when building a .p3d package.
     import importlib
@@ -446,7 +447,8 @@ Dtool_funcToMethod(place, NodePath)
 del place
 #####################################################################
 def explore(self):
-    base.startDirect(fWantTk = 1)
+    from direct.showbase import ShowBaseGlobal
+    ShowBaseGlobal.base.startDirect(fWantTk = 1)
     # Don't use a regular import, to prevent ModuleFinder from picking
     # it up as a dependency when building a .p3d package.
     import importlib
@@ -457,7 +459,8 @@ Dtool_funcToMethod(explore, NodePath)
 del explore
 #####################################################################
 def rgbPanel(self, cb = None):
-    base.startTk()
+    from direct.showbase import ShowBaseGlobal
+    ShowBaseGlobal.base.startTk()
     # Don't use a regular import, to prevent ModuleFinder from picking
     # it up as a dependency when building a .p3d package.
     import importlib
@@ -468,6 +471,8 @@ Dtool_funcToMethod(rgbPanel, NodePath)
 del rgbPanel
 #####################################################################
 def select(self):
+    from direct.showbase import ShowBaseGlobal
+    base = ShowBaseGlobal.base
     base.startDirect(fWantTk = 0)
     base.direct.select(self)
 
@@ -475,6 +480,8 @@ Dtool_funcToMethod(select, NodePath)
 del select
 #####################################################################
 def deselect(self):
+    from direct.showbase import ShowBaseGlobal
+    base = ShowBaseGlobal.base
     base.startDirect(fWantTk = 0)
     base.direct.deselect(self)
 
@@ -676,7 +683,8 @@ def flattenMultitex(self, stateFrom = None, target = None,
     mr.setAllowTexMat(allowTexMat)
 
     if win is None:
-        win = base.win
+        from direct.showbase import ShowBaseGlobal
+        win = ShowBaseGlobal.base.win
 
     if stateFrom is None:
         mr.scan(self)

+ 1 - 1
direct/src/gui/DirectEntry.py

@@ -28,7 +28,7 @@ class DirectEntry(DirectFrame):
     to keyboard buttons
     """
 
-    directWtext = ConfigVariableBool('direct-wtext', 1)
+    directWtext = ConfigVariableBool('direct-wtext', True)
 
     AllowCapNamePrefixes = ("Al", "Ap", "Ben", "De", "Del", "Della", "Delle", "Der", "Di", "Du",
                             "El", "Fitz", "La", "Las", "Le", "Les", "Lo", "Los",

+ 1 - 1
direct/src/gui/OnscreenText.py

@@ -173,7 +173,7 @@ class OnscreenText(NodePath):
         self.__wordwrap = wordwrap
 
         if decal:
-            textNode.setCardDecal(1)
+            textNode.setCardDecal(True)
 
         if font is None:
             font = DGG.getDefaultFont()

+ 0 - 6
direct/src/interval/FunctionInterval.py

@@ -10,12 +10,6 @@ from direct.directnotify.DirectNotifyGlobal import directNotify
 from . import Interval
 
 
-#############################################################
-###                                                       ###
-### See examples of function intervals in IntervalTest.py ###
-###                                                       ###
-#############################################################
-
 class FunctionInterval(Interval.Interval):
     # Name counter
     functionIntervalNum = 1

+ 0 - 222
direct/src/interval/IntervalTest.py

@@ -1,222 +0,0 @@
-"""Undocumented Module"""
-
-__all__ = ()
-
-
-if __name__ == "__main__":
-    from panda3d.core import Filename, Point3, Vec3
-    from direct.showbase.DirectObject import DirectObject
-    from direct.showbase.ShowBase import ShowBase
-    from direct.actor.Actor import Actor
-    from direct.directutil import Mopath
-    from direct.showbase.MessengerGlobal import messenger
-    from .ActorInterval import ActorInterval
-    from .FunctionInterval import (
-        AcceptInterval,
-        EventInterval,
-        FunctionInterval,
-        IgnoreInterval,
-        PosHprInterval,
-    )
-    from .LerpInterval import LerpPosInterval, LerpHprInterval, LerpPosHprInterval
-    from .MopathInterval import MopathInterval
-    from .SoundInterval import SoundInterval
-    from .MetaInterval import PREVIOUS_END, PREVIOUS_START, TRACK_START, Track
-
-    base = ShowBase()
-
-    boat = base.loader.loadModel('models/misc/smiley')
-    boat.reparentTo(base.render)
-
-    donald = Actor()
-    donald.loadModel("phase_6/models/char/donald-wheel-1000")
-    donald.loadAnims({"steer":"phase_6/models/char/donald-wheel-wheel"})
-    donald.reparentTo(boat)
-
-    dock = base.loader.loadModel('models/misc/smiley')
-    dock.reparentTo(base.render)
-
-    sound = base.loader.loadSfx('phase_6/audio/sfx/SZ_DD_waterlap.mp3')
-    foghorn = base.loader.loadSfx('phase_6/audio/sfx/SZ_DD_foghorn.mp3')
-
-    mp = Mopath.Mopath()
-    mp.loadFile(Filename('phase_6/paths/dd-e-w'))
-
-    # Set up the boat
-    boatMopath = MopathInterval(mp, boat, 'boatpath')
-    boatTrack = Track([boatMopath], 'boattrack')
-    BOAT_START = boatTrack.getIntervalStartTime('boatpath')
-    BOAT_END = boatTrack.getIntervalEndTime('boatpath')
-
-    # This will create an anim interval that is posed every frame
-    donaldSteerInterval = ActorInterval(donald, 'steer')
-    # This will create an anim interval that is started at t = 0 and then
-    # loops for 10 seconds
-    donaldLoopInterval = ActorInterval(donald, 'steer', loop=1, duration = 10.0)
-    donaldSteerTrack = Track([donaldSteerInterval, donaldLoopInterval],
-                             name = 'steerTrack')
-
-    # Make the dock lerp up so that it's up when the boat reaches the end of
-    # its mopath
-    dockLerp = LerpPosHprInterval(dock, 5.0,
-                                  pos=Point3(0, 0, -5),
-                                  hpr=Vec3(0, 0, 0),
-                                  name='dock-lerp')
-    # We need the dock's state to be defined before the lerp
-    dockPos = PosHprInterval(dock, dock.getPos(), dock.getHpr(), 1.0, 'dockpos')
-    dockUpTime = BOAT_END - dockLerp.getDuration()
-    hpr2 = Vec3(90.0, 90.0, 90.0)
-    dockLerp2 = LerpHprInterval(dock, 3.0, hpr2, name='hpr-lerp')
-    dockTrack = Track([dockLerp2, dockPos, dockLerp], 'docktrack')
-    dockTrack.setIntervalStartTime('dock-lerp', dockUpTime)
-    dockTrack.setIntervalStartTime('hpr-lerp', BOAT_START)
-
-    # Start the water sound 5 seconds after the boat starts moving
-    waterStartTime = BOAT_START + 5.0
-    waterSound = SoundInterval(sound, name='watersound')
-    soundTrack = Track([waterSound], 'soundtrack')
-    soundTrack.setIntervalStartTime('watersound', waterStartTime)
-
-    # Throw an event when the water track ends
-    eventTime = soundTrack.getIntervalEndTime('watersound')
-    waterDone = EventInterval('water-is-done')
-    waterEventTrack = Track([waterDone])
-    waterEventTrack.setIntervalStartTime('water-is-done', eventTime)
-
-    def handleWaterDone():
-        print('water is done')
-
-    # Interval can handle its own event
-    messenger.accept('water-is-done', waterDone, handleWaterDone)
-
-    foghornStartTime = BOAT_START + 4.0
-    foghornSound = SoundInterval(foghorn, name='foghorn')
-    soundTrack2 = Track([(foghornStartTime, foghornSound)], 'soundtrack2')
-
-    mtrack = MultiTrack([boatTrack, dockTrack, soundTrack, soundTrack2, waterEventTrack,  # type: ignore[name-defined]
-                         donaldSteerTrack])
-    # Print out MultiTrack parameters
-    print(mtrack)
-
-    ### Using lambdas and functions ###
-    # Using a lambda
-    i1 = FunctionInterval(lambda: base.transitions.fadeOut())
-    i2 = FunctionInterval(lambda: base.transitions.fadeIn())
-
-    def caughtIt():
-        print('Caught here-is-an-event')
-
-    class DummyAcceptor(DirectObject):
-        pass
-
-    da = DummyAcceptor()
-    i3 = AcceptInterval(da, 'here-is-an-event', caughtIt)
-
-    i4 = EventInterval('here-is-an-event')
-
-    i5 = IgnoreInterval(da, 'here-is-an-event')
-
-    # Using a function
-    def printDone():
-        print('done')
-
-    i6 = FunctionInterval(printDone)
-
-    # Create track
-    t1 = Track([
-        # Fade out
-        (0.0, i1),
-        # Fade in
-        (2.0, i2),
-        # Accept event
-        (4.0, i3),
-        # Throw it,
-        (5.0, i4),
-        # Ignore event
-        (6.0, i5),
-        # Throw event again and see if ignore worked
-        (7.0, i4),
-        # Print done
-        (8.0, i6)], name = 'demo')
-
-    print(t1)
-
-    ### Specifying interval start times during track construction ###
-    # Interval start time can be specified relative to three different points:
-    # PREVIOUS_END
-    # PREVIOUS_START
-    # TRACK_START
-
-    startTime = 0.0
-    def printStart():
-        global startTime
-        startTime = base.clock.getFrameTime()
-        print('Start')
-
-    def printPreviousStart():
-        global startTime
-        currTime = base.clock.getFrameTime()
-        print('PREVIOUS_END %0.2f' % (currTime - startTime))
-
-    def printPreviousEnd():
-        global startTime
-        currTime = base.clock.getFrameTime()
-        print('PREVIOUS_END %0.2f' % (currTime - startTime))
-
-    def printTrackStart():
-        global startTime
-        currTime = base.clock.getFrameTime()
-        print('TRACK_START %0.2f' % (currTime - startTime))
-
-    def printArguments(a, b, c):
-        print('My args were %d, %d, %d' % (a, b, c))
-
-    i1 = FunctionInterval(printStart)
-    # Just to take time
-    i2 = LerpPosInterval(base.camera, 2.0, Point3(0, 10, 5))
-    # This will be relative to end of camera move
-    i3 = FunctionInterval(printPreviousEnd)  # type: ignore[assignment]
-    # Just to take time
-    i4 = LerpPosInterval(base.camera, 2.0, Point3(0, 0, 5))
-    # This will be relative to the start of the camera move
-    i5 = FunctionInterval(printPreviousStart)  # type: ignore[assignment]
-    # This will be relative to track start
-    i6 = FunctionInterval(printTrackStart)
-    # This will print some arguments
-    # This will be relative to track start
-    i7 = FunctionInterval(printArguments, extraArgs = [1, 10, 100])
-    # Create the track, if you don't specify offset type in tuple it defaults to
-    # relative to TRACK_START (first entry below)
-    t2 = Track([(0.0, i1),                 # i1 start at t = 0, duration = 0.0
-                (1.0, i2, TRACK_START),    # i2 start at t = 1, duration = 2.0
-                (2.0, i3, PREVIOUS_END),   # i3 start at t = 5, duration = 0.0
-                (1.0, i4, PREVIOUS_END),   # i4 start at t = 6, duration = 2.0
-                (3.0, i5, PREVIOUS_START), # i5 start at t = 9, duration = 0.0
-                (10.0, i6, TRACK_START),   # i6 start at t = 10, duration = 0.0
-                (12.0, i7)],               # i7 start at t = 12, duration = 0.0
-               name = 'startTimeDemo')
-
-    print(t2)
-
-    # Play tracks
-    # mtrack.play()
-    # t1.play()
-    # t2.play()
-
-
-    def test(n):
-        lerps = []
-        for i in range(n):
-            lerps.append(LerpPosHprInterval(dock, 5.0,
-                                            pos=Point3(0, 0, -5),
-                                            hpr=Vec3(0, 0, 0),
-                                            startPos=dock.getPos(),
-                                            startHpr=dock.getHpr(),
-                                            name='dock-lerp'))
-            lerps.append(EventInterval("joe"))
-        t = Track(lerps)
-        mt = MultiTrack([t])
-        # return mt
-
-    test(5)
-    base.run()

+ 28 - 27
direct/src/leveleditor/LevelEditorUIBase.py

@@ -7,6 +7,7 @@ from direct.wxwidgets.WxPandaShell import WxPandaShell
 from direct.wxwidgets.WxSlider import WxSlider
 from direct.directtools.DirectSelection import SelectionRay
 from direct.showbase.MessengerGlobal import messenger
+from direct.showbase import ShowBaseGlobal
 
 #from ViewPort import *
 from . import ObjectGlobals as OG
@@ -88,7 +89,7 @@ class PandaTextDropTarget(wx.TextDropTarget):
             np = NodePath('temp')
             np.setPos(self.view.camera, hitPt)
 
-            if base.direct.manipulationControl.fGridSnap:
+            if ShowBaseGlobal.direct.manipulationControl.fGridSnap:
                 snappedPos = self.view.grid.computeSnapPoint(np.getPos())
                 np.setPos(snappedPos)
 
@@ -98,10 +99,10 @@ class PandaTextDropTarget(wx.TextDropTarget):
 
             # transform newobj to cursor position
             obj = self.editor.objectMgr.findObjectByNodePath(newobj)
-            action = ActionTransformObj(self.editor, obj[OG.OBJ_UID], Mat4(np.getMat()))
-            self.editor.actionMgr.push(action)
+            action2 = ActionTransformObj(self.editor, obj[OG.OBJ_UID], Mat4(np.getMat()))
+            self.editor.actionMgr.push(action2)
             np.remove()
-            action()
+            action2()
         iRay.collisionNodePath.removeNode()
         del iRay
 
@@ -250,13 +251,13 @@ class LevelEditorUIBase(WxPandaShell):
         WxPandaShell.createMenu(self)
 
     def onGraphEditor(self, e):
-        if base.direct.selected.last is None:
+        if ShowBaseGlobal.direct.selected.last is None:
             dlg = wx.MessageDialog(None, 'Please select a object first.', 'NOTICE', wx.OK)
             dlg.ShowModal()
             dlg.Destroy()
             self.graphEditorMenuItem.Check(False)
         else:
-            currentObj = self.editor.objectMgr.findObjectByNodePath(base.direct.selected.last)
+            currentObj = self.editor.objectMgr.findObjectByNodePath(ShowBaseGlobal.direct.selected.last)
             self.graphEditorUI = GraphEditorUI(self, self.editor, currentObj)
             self.graphEditorUI.Show()
             self.graphEditorMenuItem.Check(True)
@@ -298,7 +299,7 @@ class LevelEditorUIBase(WxPandaShell):
                     degreeUI = CurveDegreeUI(self, -1, 'Curve Degree')
                     degreeUI.ShowModal()
                     degreeUI.Destroy()
-                    base.direct.manipulationControl.disableManipulation()
+                    ShowBaseGlobal.direct.manipulationControl.disableManipulation()
                     self.editCurveMenuItem.Check(False)
 
     def onEditCurve(self, e):
@@ -313,15 +314,15 @@ class LevelEditorUIBase(WxPandaShell):
                 self.createCurveMenuItem.Check(False)
                 self.onEditCurve(None)
             else:
-                if base.direct.selected.last is None:
+                if ShowBaseGlobal.direct.selected.last is None:
                     dlg = wx.MessageDialog(None, 'Please select a curve first.', 'NOTICE', wx.OK)
                     dlg.ShowModal()
                     dlg.Destroy()
                     self.editCurveMenuItem.Check(False)
-                if base.direct.selected.last is not None:
-                    base.direct.manipulationControl.enableManipulation()
+                if ShowBaseGlobal.direct.selected.last is not None:
+                    ShowBaseGlobal.direct.manipulationControl.enableManipulation()
                     self.createCurveMenuItem.Check(False)
-                    self.curveObj = self.editor.objectMgr.findObjectByNodePath(base.direct.selected.last)
+                    self.curveObj = self.editor.objectMgr.findObjectByNodePath(ShowBaseGlobal.direct.selected.last)
                     if self.curveObj[OG.OBJ_DEF].name == '__Curve__':
                         self.editor.mode = self.editor.EDIT_CURVE_MODE
                         self.editor.updateStatusReadout('Please press ENTER to end the curve editing.')
@@ -339,8 +340,8 @@ class LevelEditorUIBase(WxPandaShell):
 
     def updateMenu(self):
         hotKeyDict = {}
-        for hotKey in base.direct.hotKeyMap.keys():
-            desc = base.direct.hotKeyMap[hotKey]
+        for hotKey in ShowBaseGlobal.direct.hotKeyMap.keys():
+            desc = ShowBaseGlobal.direct.hotKeyMap[hotKey]
             hotKeyDict[desc[1]] = hotKey
 
         for id in self.MENU_TEXTS.keys():
@@ -401,16 +402,16 @@ class LevelEditorUIBase(WxPandaShell):
         else:
             mpos = evt.GetPosition()
 
-        base.direct.fMouse3 = 0
+        ShowBaseGlobal.direct.fMouse3 = 0
         self.PopupMenu(self.contextMenu, mpos)
 
     def onKeyDownEvent(self, evt):
         if evt.GetKeyCode() == wx.WXK_ALT:
-            base.direct.fAlt = 1
+            ShowBaseGlobal.direct.fAlt = 1
         elif evt.GetKeyCode() == wx.WXK_CONTROL:
-            base.direct.fControl = 1
+            ShowBaseGlobal.direct.fControl = 1
         elif evt.GetKeyCode() == wx.WXK_SHIFT:
-            base.direct.fShift = 1
+            ShowBaseGlobal.direct.fShift = 1
         elif evt.GetKeyCode() == wx.WXK_UP:
             messenger.send('arrow_up')
         elif evt.GetKeyCode() == wx.WXK_DOWN:
@@ -428,11 +429,11 @@ class LevelEditorUIBase(WxPandaShell):
 
     def onKeyUpEvent(self, evt):
         if evt.GetKeyCode() == wx.WXK_ALT:
-            base.direct.fAlt = 0
+            ShowBaseGlobal.direct.fAlt = 0
         elif evt.GetKeyCode() == wx.WXK_CONTROL:
-            base.direct.fControl = 0
+            ShowBaseGlobal.direct.fControl = 0
         elif evt.GetKeyCode() == wx.WXK_SHIFT:
-            base.direct.fShift = 0
+            ShowBaseGlobal.direct.fShift = 0
         elif evt.GetKeyCode() == wx.WXK_UP:
             messenger.send('arrow_up-up')
         elif evt.GetKeyCode() == wx.WXK_DOWN:
@@ -473,8 +474,8 @@ class LevelEditorUIBase(WxPandaShell):
                 input = 'control-%s'%chr(evt.GetKeyCode())
             elif evt.GetKeyCode() < 256:
                 input = chr(evt.GetKeyCode())
-        if input in base.direct.hotKeyMap.keys():
-            keyDesc = base.direct.hotKeyMap[input]
+        if input in ShowBaseGlobal.direct.hotKeyMap.keys():
+            keyDesc = ShowBaseGlobal.direct.hotKeyMap[input]
             messenger.send(keyDesc[1])
 
     def reset(self):
@@ -533,12 +534,12 @@ class LevelEditorUIBase(WxPandaShell):
 
     def toggleGridSnap(self, evt):
         if self.gridSnapMenuItem.IsChecked():
-            base.direct.manipulationControl.fGridSnap = 1
+            ShowBaseGlobal.direct.manipulationControl.fGridSnap = 1
             for grid in [self.perspView.grid, self.topView.grid, self.frontView.grid, self.leftView.grid]:
                 grid.fXyzSnap = 1
 
         else:
-            base.direct.manipulationControl.fGridSnap = 0
+            ShowBaseGlobal.direct.manipulationControl.fGridSnap = 0
             for grid in [self.perspView.grid, self.topView.grid, self.frontView.grid, self.leftView.grid]:
                 grid.fXyzSnap = 0
 
@@ -589,7 +590,7 @@ class LevelEditorUIBase(WxPandaShell):
         self.contextMenu.AppendSeparator()
 
     def replaceObject(self, evt, all=False):
-        currObj = self.editor.objectMgr.findObjectByNodePath(base.direct.selected.last)
+        currObj = self.editor.objectMgr.findObjectByNodePath(ShowBaseGlobal.direct.selected.last)
         if currObj is None:
             print('No valid object is selected for replacement')
             return
@@ -636,13 +637,13 @@ class GridSizeUI(wx.Dialog):
         vbox.Add(okButton, 1, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 5)
 
         self.SetSizer(vbox)
-        base.le.ui.bindKeyEvents(False)
+        ShowBaseGlobal.base.le.ui.bindKeyEvents(False)
 
     def onApply(self, evt):
         newSize = self.gridSizeSlider.GetValue()
         newSpacing = self.gridSpacingSlider.GetValue()
         self.parent.updateGrids(newSize, newSpacing)
-        base.le.ui.bindKeyEvents(True)
+        ShowBaseGlobal.base.le.ui.bindKeyEvents(True)
         self.Destroy()
 
 

+ 3 - 0
direct/src/leveleditor/LevelLoader.py

@@ -25,6 +25,9 @@ class LevelLoader(LevelLoaderBase):
 
     def initLoader(self):
         self.defaultPath = os.path.dirname(__file__)
+
+        from direct.showbase import ShowBaseGlobal
+        base = ShowBaseGlobal.base
         base.objectPalette = ObjectPalette()
         base.protoPalette = ProtoPalette()
         base.objectHandler = ObjectHandler(None)

+ 1 - 1
direct/src/particles/ParticleEffect.py

@@ -97,7 +97,7 @@ class ParticleEffect(NodePath):
     def addForceGroup(self, forceGroup):
         forceGroup.nodePath.reparentTo(self)
         forceGroup.particleEffect = self
-        self.forceGroupDict[forceGroup.getName()] = forceGroup
+        self.forceGroupDict[forceGroup.name] = forceGroup
 
         # Associate the force group with all particles
         for force in forceGroup:

+ 11 - 8
direct/src/showbase/Loader.py

@@ -5,7 +5,6 @@ sound, music, shaders and fonts from disk.
 __all__ = ['Loader']
 
 from panda3d.core import (
-    AudioLoadRequest,
     ConfigVariableBool,
     Filename,
     FontPool,
@@ -26,6 +25,7 @@ from panda3d.core import Loader as PandaLoader
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.showbase.DirectObject import DirectObject
 import warnings
+import sys
 
 # You can specify a phaseChecker callback to check
 # a modelPath to see if it is being loaded in the correct
@@ -152,16 +152,17 @@ class Loader(DirectObject):
         if not ConfigVariableBool('loader-support-entry-points', True):
             return
 
-        import importlib
-        try:
-            pkg_resources = importlib.import_module('pkg_resources')
-        except ImportError:
-            pkg_resources = None
+        from importlib.metadata import entry_points
+        eps = entry_points()
+        if sys.version_info < (3, 10):
+            loaders = eps.get('panda3d.loaders', ())
+        else:
+            loaders = eps.select(group='panda3d.loaders')
 
-        if pkg_resources:
+        if loaders:
             registry = LoaderFileTypeRegistry.getGlobalPtr()
 
-            for entry_point in pkg_resources.iter_entry_points('panda3d.loaders'):
+            for entry_point in loaders:
                 registry.register_deferred_type(entry_point)
 
             cls._loadedPythonFileTypes = True
@@ -977,6 +978,8 @@ class Loader(DirectObject):
         just as in loadModel(); otherwise, the loading happens before
         loadSound() returns."""
 
+        from panda3d.core import AudioLoadRequest
+
         if not isinstance(soundPath, (tuple, list, set)):
             # We were given a single sound pathname or a MovieAudio instance.
             soundList = [soundPath]

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

@@ -123,6 +123,7 @@ import builtins
 builtins.config = DConfig  # type: ignore[attr-defined]
 
 from direct.directnotify.DirectNotifyGlobal import directNotify, giveNotify
+from direct.directnotify.Notifier import Notifier
 from .MessengerGlobal import messenger
 from .BulletinBoardGlobal import bulletinBoard
 from direct.task.TaskManagerGlobal import taskMgr
@@ -140,6 +141,7 @@ import importlib
 from direct.showbase import ExceptionVarDump
 from . import DirectObject
 from . import SfxPlayer
+from typing import ClassVar, Optional
 if __debug__:
     from direct.showbase import GarbageReport
     from direct.directutil import DeltaProfiler
@@ -160,8 +162,13 @@ def exitfunc():
 class ShowBase(DirectObject.DirectObject):
 
     #: The deprecated `.DConfig` interface for accessing config variables.
-    config = DConfig
-    notify = directNotify.newCategory("ShowBase")
+    config: ClassVar = DConfig
+    notify: ClassVar[Notifier] = directNotify.newCategory("ShowBase")
+    guiItems: ClassVar[dict]
+
+    render2d: NodePath
+    aspect2d: NodePath
+    pixel2d: NodePath
 
     def __init__(self, fStartDirect=True, windowType=None):
         """Opens a window, sets up a 3-D and several 2-D scene graphs, and
@@ -337,10 +344,10 @@ class ShowBase(DirectObject.DirectObject):
         self.tkRootCreated = False
 
         # This is used for syncing multiple PCs in a distributed cluster
-        try:
+        if hasattr(builtins, 'clusterSyncFlag'):
             # Has the cluster sync variable been set externally?
-            self.clusterSyncFlag = clusterSyncFlag
-        except NameError:
+            self.clusterSyncFlag = builtins.clusterSyncFlag
+        else:
             # Has the clusterSyncFlag been set via a config variable
             self.clusterSyncFlag = ConfigVariableBool('cluster-sync', False)
 
@@ -424,6 +431,7 @@ class ShowBase(DirectObject.DirectObject):
         #: `.Loader.Loader` object.
         self.loader = Loader.Loader(self)
         self.graphicsEngine.setDefaultLoader(self.loader.loader)
+        ShowBaseGlobal.loader = self.loader
 
         #: The global event manager, as imported from `.EventManagerGlobal`.
         self.eventMgr = eventMgr
@@ -712,10 +720,9 @@ class ShowBase(DirectObject.DirectObject):
         except Exception:
             pass
 
-        if hasattr(self, 'win'):
-            del self.win
-            del self.winList
-            del self.pipe
+        self.win = None
+        self.winList.clear()
+        self.pipe = None
 
     def makeDefaultPipe(self, printPipeTypes = None):
         """
@@ -728,7 +735,7 @@ class ShowBase(DirectObject.DirectObject):
             # When the user didn't specify an explicit setting, take the value
             # from the config variable. We could just omit the parameter, however
             # this way we can keep backward compatibility.
-            printPipeTypes = ConfigVariableBool("print-pipe-types", True)
+            printPipeTypes = ConfigVariableBool("print-pipe-types", True).value
 
         selection = GraphicsPipeSelection.getGlobalPtr()
         if printPipeTypes:
@@ -3414,7 +3421,7 @@ class ShowBase(DirectObject.DirectObject):
         # Set fWantTk to 0 to avoid starting Tk with this call
         self.startDirect(fWantDirect = fDirect, fWantTk = fTk, fWantWx = fWx)
 
-    def run(self): # pylint: disable=method-hidden
+    def run(self) -> None: # pylint: disable=method-hidden
         """This method runs the :class:`~direct.task.Task.TaskManager`
         when ``self.appRunner is None``, which is to say, when we are
         not running from within a p3d file.  When we *are* within a p3d

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

@@ -19,9 +19,10 @@ from panda3d.core import VirtualFileSystem, Notify, ClockObject, PandaSystem
 from panda3d.core import ConfigPageManager, ConfigVariableManager, ConfigVariableBool
 from panda3d.core import NodePath, PGTop
 from . import DConfig as config # pylint: disable=unused-import
+from .Loader import Loader
 import warnings
 
-__dev__ = ConfigVariableBool('want-dev', __debug__).value
+__dev__: bool = ConfigVariableBool('want-dev', __debug__).value
 
 base: ShowBase
 
@@ -61,6 +62,8 @@ aspect2d = render2d.attachNewNode(PGTop("aspect2d"))
 #: A dummy scene graph that is not being rendered by anything.
 hidden = NodePath("hidden")
 
+loader: Loader
+
 # Set direct notify categories now that we have config
 directNotify.setDconfigLevels()
 

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

@@ -37,4 +37,5 @@ del bordercolors
 
 def spawnTkLoop():
     """Alias for :meth:`base.spawnTkLoop() <.ShowBase.spawnTkLoop>`."""
-    base.spawnTkLoop()
+    from direct.showbase import ShowBaseGlobal
+    ShowBaseGlobal.base.spawnTkLoop()

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

@@ -3,4 +3,5 @@
 
 def spawnWxLoop():
     """Alias for :meth:`base.spawnWxLoop() <.ShowBase.spawnWxLoop>`."""
-    base.spawnWxLoop()
+    from direct.showbase import ShowBaseGlobal
+    ShowBaseGlobal.base.spawnWxLoop()

+ 6 - 4
direct/src/showutil/TexMemWatcher.py

@@ -22,6 +22,7 @@ from panda3d.core import (
     WindowProperties,
 )
 from direct.showbase.DirectObject import DirectObject
+from direct.showbase import ShowBaseGlobal
 from direct.task.TaskManagerGlobal import taskMgr
 import math
 import copy
@@ -109,7 +110,7 @@ class TexMemWatcher(DirectObject):
         # This is the maximum number of bitmask rows (within
         # self.limit) to allocate for packing.  This controls the
         # value assigned to self.quantize in repack().
-        self.maxHeight = base.config.GetInt('tex-mem-max-height', 300)
+        self.maxHeight = ConfigVariableInt('tex-mem-max-height', 300).value
 
         # The total number of texture bytes tracked, including overflow.
         self.totalSize = 0
@@ -122,6 +123,7 @@ class TexMemWatcher(DirectObject):
         self.placedQSize = 0
 
         # If no GSG is specified, use the main GSG.
+        base = ShowBaseGlobal.base
         if gsg is None:
             gsg = base.win.getGsg()
         elif isinstance(gsg, GraphicsOutput):
@@ -150,7 +152,7 @@ class TexMemWatcher(DirectObject):
         # Set this to tinydisplay if you're running on a machine with
         # limited texture memory.  That way you won't compete for
         # texture memory with the main scene.
-        moduleName = base.config.GetString('tex-mem-pipe', '')
+        moduleName = ConfigVariableString('tex-mem-pipe', '').value
         if moduleName:
             self.pipe = base.makeModulePipe(moduleName)
 
@@ -202,7 +204,7 @@ class TexMemWatcher(DirectObject):
 
         # How frequently should the texture memory window check for
         # state changes?
-        updateInterval = base.config.GetDouble("tex-mem-update-interval", 0.5)
+        updateInterval = ConfigVariableDouble("tex-mem-update-interval", 0.5).value
         self.task = taskMgr.doMethodLater(updateInterval, self.updateTextures, 'TexMemWatcher')
 
         self.setLimit(limit)
@@ -380,7 +382,7 @@ class TexMemWatcher(DirectObject):
             self.cleanedUp = True
 
             # Remove the window.
-            base.graphicsEngine.removeWindow(self.win)
+            self.win.engine.removeWindow(self.win)
             self.win = None
             self.gsg = None
             self.pipe = None

+ 1 - 1
direct/src/showutil/TexViewer.py

@@ -18,7 +18,7 @@ class TexViewer(DirectObject):
 
         # We'll put the full-resolution texture on the left.
         cm = CardMaker('left')
-        l, r, b, t = (-1, -0.1, 0, 0.9)
+        l, r, b, t = (-1.0, -0.1, 0.0, 0.9)
         cm.setFrame(l, r, b, t)
         left = cards.attachNewNode(cm.generate())
         left.setTexture(self.tex)

+ 2 - 0
direct/src/task/MiniTask.py

@@ -12,6 +12,8 @@ class MiniTask:
     done = 0
     cont = 1
 
+    name: str
+
     def __init__(self, callback):
         self.__call__ = callback
 

+ 104 - 47
direct/src/task/Task.py

@@ -6,6 +6,8 @@ For more information about the task system, consult the
 :ref:`tasks-and-event-handling` page in the programming manual.
 """
 
+from __future__ import annotations
+
 __all__ = ['Task', 'TaskManager',
            'cont', 'done', 'again', 'pickup', 'exit',
            'sequence', 'loop', 'pause']
@@ -13,6 +15,7 @@ __all__ = ['Task', 'TaskManager',
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.showbase.PythonUtil import Functor, ScratchPad
 from direct.showbase.MessengerGlobal import messenger
+from typing import Any, Callable, Coroutine, Final, Generator, Sequence, TypeVar, Union
 import types
 import random
 import importlib
@@ -20,6 +23,7 @@ import sys
 
 # On Android, there's no use handling SIGINT, and in fact we can't, since we
 # run the application in a separate thread from the main thread.
+signal: types.ModuleType | None
 if hasattr(sys, 'getandroidapilevel'):
     signal = None
 else:
@@ -41,8 +45,15 @@ from panda3d.core import (
 )
 from direct.extensions_native import HTTPChannel_extensions # pylint: disable=unused-import
 
+# The following variables are typing constructs used in annotations
+# to succinctly express all the types that can be converted into tasks.
+_T = TypeVar('_T', covariant=True)
+_TaskCoroutine = Union[Coroutine[Any, None, _T], Generator[Any, None, _T]]
+_TaskFunction = Callable[..., Union[int, _TaskCoroutine[Union[int, None]], None]]
+_FuncOrTask = Union[_TaskFunction, _TaskCoroutine[Any], AsyncTask]
+
 
-def print_exc_plus():
+def print_exc_plus() -> None:
     """
     Print the usual traceback information, followed by a listing of all the
     local variables in each frame.
@@ -50,12 +61,13 @@ def print_exc_plus():
     import traceback
 
     tb = sys.exc_info()[2]
+    assert tb is not None
     while 1:
         if not tb.tb_next:
             break
         tb = tb.tb_next
     stack = []
-    f = tb.tb_frame
+    f: types.FrameType | None = tb.tb_frame
     while f:
         stack.append(f)
         f = f.f_back
@@ -82,11 +94,11 @@ def print_exc_plus():
 # these Python names, and define them both at the module level, here,
 # and at the class level (below).  The preferred access is via the
 # class level.
-done = AsyncTask.DSDone
-cont = AsyncTask.DSCont
-again = AsyncTask.DSAgain
-pickup = AsyncTask.DSPickup
-exit = AsyncTask.DSExit
+done: Final = AsyncTask.DSDone
+cont: Final = AsyncTask.DSCont
+again: Final = AsyncTask.DSAgain
+pickup: Final = AsyncTask.DSPickup
+exit: Final = AsyncTask.DSExit
 
 #: Task aliases to :class:`panda3d.core.PythonTask` for historical purposes.
 Task = PythonTask
@@ -110,7 +122,7 @@ gather = Task.gather
 shield = Task.shield
 
 
-def sequence(*taskList):
+def sequence(*taskList: AsyncTask) -> AsyncTaskSequence:
     seq = AsyncTaskSequence('sequence')
     for task in taskList:
         seq.addTask(task)
@@ -120,7 +132,7 @@ def sequence(*taskList):
 Task.DtoolClassDict['sequence'] = staticmethod(sequence)
 
 
-def loop(*taskList):
+def loop(*taskList: AsyncTask) -> AsyncTaskSequence:
     seq = AsyncTaskSequence('loop')
     for task in taskList:
         seq.addTask(task)
@@ -140,10 +152,12 @@ class TaskManager:
 
     MaxEpochSpeed = 1.0/30.0
 
-    def __init__(self):
+    __prevHandler: Any
+
+    def __init__(self) -> None:
         self.mgr = AsyncTaskManager.getGlobalPtr()
 
-        self.resumeFunc = None
+        self.resumeFunc: Callable[[], object] | None = None
         self.globalClock = self.mgr.getClock()
         self.stepping = False
         self.running = False
@@ -153,12 +167,12 @@ class TaskManager:
         if signal:
             self.__prevHandler = signal.default_int_handler
 
-        self._frameProfileQueue = []
+        self._frameProfileQueue: list[tuple[int, Any, Callable[[], object] | None]] = []
 
         # this will be set when it's safe to import StateVar
-        self._profileFrames = None
+        self._profileFrames: Any = None
         self._frameProfiler = None
-        self._profileTasks = None
+        self._profileTasks: Any = None
         self._taskProfiler = None
         self._taskProfileInfo = ScratchPad(
             taskId = None,
@@ -166,7 +180,7 @@ class TaskManager:
             session = None,
         )
 
-    def finalInit(self):
+    def finalInit(self) -> None:
         # This function should be called once during startup, after
         # most things are imported.
         from direct.fsm.StatePush import StateVar
@@ -175,7 +189,7 @@ class TaskManager:
         self._profileFrames = StateVar(False)
         self.setProfileFrames(ConfigVariableBool('profile-frames', 0).getValue())
 
-    def destroy(self):
+    def destroy(self) -> None:
         # This should be safe to call multiple times.
         self.running = False
         self.notify.info("TaskManager.destroy()")
@@ -183,11 +197,14 @@ class TaskManager:
         self._frameProfileQueue.clear()
         self.mgr.cleanup()
 
-    def setClock(self, clockObject):
+    def __getClock(self) -> ClockObject:
+        return self.mgr.getClock()
+
+    def setClock(self, clockObject: ClockObject) -> None:
         self.mgr.setClock(clockObject)
         self.globalClock = clockObject
 
-    clock = property(lambda self: self.mgr.getClock(), setClock)
+    clock = property(__getClock, setClock)
 
     def invokeDefaultHandler(self, signalNumber, stackFrame):
         print('*** allowing mid-frame keyboard interrupt.')
@@ -208,13 +225,13 @@ class TaskManager:
             # Next time around invoke the default handler
             signal.signal(signal.SIGINT, self.invokeDefaultHandler)
 
-    def getCurrentTask(self):
+    def getCurrentTask(self) -> AsyncTask | None:
         """ Returns the task currently executing on this thread, or
         None if this is being called outside of the task manager. """
 
         return Thread.getCurrentThread().getCurrentTask()
 
-    def hasTaskChain(self, chainName):
+    def hasTaskChain(self, chainName: str) -> bool:
         """ Returns true if a task chain with the indicated name has
         already been defined, or false otherwise.  Note that
         setupTaskChain() will implicitly define a task chain if it has
@@ -224,9 +241,16 @@ class TaskManager:
 
         return self.mgr.findTaskChain(chainName) is not None
 
-    def setupTaskChain(self, chainName, numThreads = None, tickClock = None,
-                       threadPriority = None, frameBudget = None,
-                       frameSync = None, timeslicePriority = None):
+    def setupTaskChain(
+        self,
+        chainName: str,
+        numThreads: int | None = None,
+        tickClock: bool | None = None,
+        threadPriority: int | None = None,
+        frameBudget: float | None = None,
+        frameSync: bool | None = None,
+        timeslicePriority: bool | None = None,
+    ) -> None:
         """Defines a new task chain.  Each task chain executes tasks
         potentially in parallel with all of the other task chains (if
         numThreads is more than zero).  When a new task is created, it
@@ -290,40 +314,50 @@ class TaskManager:
         if timeslicePriority is not None:
             chain.setTimeslicePriority(timeslicePriority)
 
-    def hasTaskNamed(self, taskName):
+    def hasTaskNamed(self, taskName: str) -> bool:
         """Returns true if there is at least one task, active or
         sleeping, with the indicated name. """
 
         return bool(self.mgr.findTask(taskName))
 
-    def getTasksNamed(self, taskName):
+    def getTasksNamed(self, taskName: str) -> list[AsyncTask]:
         """Returns a list of all tasks, active or sleeping, with the
         indicated name. """
         return list(self.mgr.findTasks(taskName))
 
-    def getTasksMatching(self, taskPattern):
+    def getTasksMatching(self, taskPattern: GlobPattern | str) -> list[AsyncTask]:
         """Returns a list of all tasks, active or sleeping, with a
         name that matches the pattern, which can include standard
         shell globbing characters like \\*, ?, and []. """
 
         return list(self.mgr.findTasksMatching(GlobPattern(taskPattern)))
 
-    def getAllTasks(self):
+    def getAllTasks(self) -> list[AsyncTask]:
         """Returns list of all tasks, active and sleeping, in
         arbitrary order. """
         return list(self.mgr.getTasks())
 
-    def getTasks(self):
+    def getTasks(self) -> list[AsyncTask]:
         """Returns list of all active tasks in arbitrary order. """
         return list(self.mgr.getActiveTasks())
 
-    def getDoLaters(self):
+    def getDoLaters(self) -> list[AsyncTask]:
         """Returns list of all sleeping tasks in arbitrary order. """
         return list(self.mgr.getSleepingTasks())
 
-    def doMethodLater(self, delayTime, funcOrTask, name, extraArgs = None,
-                      sort = None, priority = None, taskChain = None,
-                      uponDeath = None, appendTask = False, owner = None):
+    def doMethodLater(
+        self,
+        delayTime: float,
+        funcOrTask: _FuncOrTask,
+        name: str | None,
+        extraArgs: Sequence | None = None,
+        sort: int | None = None,
+        priority: int | None = None,
+        taskChain: str | None = None,
+        uponDeath: Callable[[], object] | None = None,
+        appendTask: bool = False,
+        owner = None,
+    ) -> AsyncTask:
         """Adds a task to be performed at some time in the future.
         This is identical to `add()`, except that the specified
         delayTime is applied to the Task object first, which means
@@ -346,9 +380,19 @@ class TaskManager:
 
     do_method_later = doMethodLater
 
-    def add(self, funcOrTask, name = None, sort = None, extraArgs = None,
-            priority = None, uponDeath = None, appendTask = False,
-            taskChain = None, owner = None, delay = None):
+    def add(
+        self,
+        funcOrTask: _FuncOrTask,
+        name: str | None = None,
+        sort: int | None = None,
+        extraArgs: Sequence | None = None,
+        priority: int | None = None,
+        uponDeath: Callable[[], object] | None = None,
+        appendTask: bool = False,
+        taskChain: str | None = None,
+        owner = None,
+        delay: float | None = None,
+    ) -> AsyncTask:
         """
         Add a new task to the taskMgr.  The task will begin executing
         immediately, or next frame if its sort value has already
@@ -415,7 +459,18 @@ class TaskManager:
         self.mgr.add(task)
         return task
 
-    def __setupTask(self, funcOrTask, name, priority, sort, extraArgs, taskChain, appendTask, owner, uponDeath):
+    def __setupTask(
+        self,
+        funcOrTask: _FuncOrTask,
+        name: str | None,
+        priority: int | None,
+        sort: int | None,
+        extraArgs: Sequence | None,
+        taskChain: str | None,
+        appendTask: bool,
+        owner,
+        uponDeath: Callable[[], object] | None,
+    ) -> AsyncTask:
         wasTask = False
         if isinstance(funcOrTask, AsyncTask):
             task = funcOrTask
@@ -473,7 +528,7 @@ class TaskManager:
 
         return task
 
-    def remove(self, taskOrName):
+    def remove(self, taskOrName: AsyncTask | str | list[AsyncTask | str]) -> int:
         """Removes a task from the task manager.  The task is stopped,
         almost as if it had returned task.done.  (But if the task is
         currently executing, it will finish out its current frame
@@ -485,13 +540,15 @@ class TaskManager:
         if isinstance(taskOrName, AsyncTask):
             return self.mgr.remove(taskOrName)
         elif isinstance(taskOrName, list):
+            count = 0
             for task in taskOrName:
-                self.remove(task)
+                count += self.remove(task)
+            return count
         else:
             tasks = self.mgr.findTasks(taskOrName)
             return self.mgr.remove(tasks)
 
-    def removeTasksMatching(self, taskPattern):
+    def removeTasksMatching(self, taskPattern: GlobPattern | str) -> int:
         """Removes all tasks whose names match the pattern, which can
         include standard shell globbing characters like \\*, ?, and [].
         See also :meth:`remove()`.
@@ -501,7 +558,7 @@ class TaskManager:
         tasks = self.mgr.findTasksMatching(GlobPattern(taskPattern))
         return self.mgr.remove(tasks)
 
-    def step(self):
+    def step(self) -> None:
         """Invokes the task manager for one frame, and then returns.
         Normally, this executes each task exactly once, though task
         chains that are in sub-threads or that have frame budgets
@@ -512,7 +569,7 @@ class TaskManager:
         # Replace keyboard interrupt handler during task list processing
         # so we catch the keyboard interrupt but don't handle it until
         # after task list processing is complete.
-        self.fKeyboardInterrupt = 0
+        self.fKeyboardInterrupt = False
         self.interruptCount = 0
 
         if signal:
@@ -534,7 +591,7 @@ class TaskManager:
         if self.fKeyboardInterrupt:
             raise KeyboardInterrupt
 
-    def run(self):
+    def run(self) -> None:
         """Starts the task manager running.  Does not return until an
         exception is encountered (including KeyboardInterrupt). """
 
@@ -560,11 +617,11 @@ class TaskManager:
                     if len(self._frameProfileQueue) > 0:
                         numFrames, session, callback = self._frameProfileQueue.pop(0)
 
-                        def _profileFunc(numFrames=numFrames):
+                        def _profileFunc(numFrames: int = numFrames) -> None:
                             self._doProfiledFrames(numFrames)
                         session.setFunc(_profileFunc)
                         session.run()
-                        _profileFunc = None
+                        del _profileFunc
                         if callback:
                             callback()
                         session.release()
@@ -617,7 +674,7 @@ class TaskManager:
             message = ioError
         return code, message
 
-    def stop(self):
+    def stop(self) -> None:
         # Set a flag so we will stop before beginning next frame
         self.running = False
 
@@ -782,12 +839,12 @@ class TaskManager:
             task = tasks.getTask(i)
         return task
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return str(self.mgr)
 
     # In the event we want to do frame time managment, this is the
     # function to replace or overload.
-    def doYield(self, frameStartTime, nextScheduledTaskTime):
+    def doYield(self, frameStartTime: float, nextScheduledTaskTime: float) -> None:
         pass
 
     #def doYieldExample(self, frameStartTime, nextScheduledTaskTime):

+ 90 - 89
direct/src/tkpanels/DirectSessionPanel.py

@@ -20,6 +20,7 @@ from direct.tkwidgets import VectorWidgets
 from direct.tkwidgets import SceneGraphExplorer
 from direct.tkwidgets import MemoryExplorer
 from direct.task.TaskManagerGlobal import taskMgr
+from direct.showbase import ShowBaseGlobal
 from .TaskManagerPanel import TaskManagerWidget
 import Pmw
 import tkinter as tk
@@ -44,8 +45,8 @@ class DirectSessionPanel(AppShell):
         AppShell.__init__(self, parent)
 
         # Active light
-        if len(base.direct.lights) > 0:
-            name = base.direct.lights.getNameList()[0]
+        if len(ShowBaseGlobal.direct.lights) > 0:
+            name = ShowBaseGlobal.direct.lights.getNameList()[0]
             self.lightMenu.selectitem(name)
             self.selectLightNamed(name)
         else:
@@ -62,14 +63,14 @@ class DirectSessionPanel(AppShell):
         # Initialize state
         # Dictionary keeping track of all node paths selected so far
         self.nodePathDict = {}
-        self.nodePathDict['widget'] = base.direct.widget
+        self.nodePathDict['widget'] = ShowBaseGlobal.direct.widget
         self.nodePathNames = ['widget']
 
         # Dictionary keeping track of all jb node paths selected so far
         self.jbNodePathDict = {}
         self.jbNodePathDict['none'] = 'No Node Path'
-        self.jbNodePathDict['widget'] = base.direct.widget
-        self.jbNodePathDict['camera'] = base.direct.camera
+        self.jbNodePathDict['widget'] = ShowBaseGlobal.direct.widget
+        self.jbNodePathDict['camera'] = ShowBaseGlobal.direct.camera
         self.jbNodePathNames = ['camera', 'selected', 'none']
 
         # Set up event hooks
@@ -93,7 +94,7 @@ class DirectSessionPanel(AppShell):
         self.menuBar.addmenu('DIRECT', 'Direct Session Panel Operations')
 
         self.directEnabled = tk.BooleanVar()
-        self.directEnabled.set(1)
+        self.directEnabled.set(True)
         self.menuBar.addmenuitem('DIRECT', 'checkbutton',
                                  'DIRECT Enabled',
                                  label = 'Enable',
@@ -101,7 +102,7 @@ class DirectSessionPanel(AppShell):
                                  command = self.toggleDirect)
 
         self.directGridEnabled = tk.BooleanVar()
-        self.directGridEnabled.set(base.direct.grid.isEnabled())
+        self.directGridEnabled.set(ShowBaseGlobal.direct.grid.isEnabled())
         self.menuBar.addmenuitem('DIRECT', 'checkbutton',
                                  'DIRECT Grid Enabled',
                                  label = 'Enable Grid',
@@ -111,16 +112,16 @@ class DirectSessionPanel(AppShell):
         self.menuBar.addmenuitem('DIRECT', 'command',
                                  'Toggle Object Handles Visability',
                                  label = 'Toggle Widget Viz',
-                                 command = base.direct.toggleWidgetVis)
+                                 command = ShowBaseGlobal.direct.toggleWidgetVis)
 
         self.menuBar.addmenuitem(
             'DIRECT', 'command',
             'Toggle Widget Move/COA Mode',
             label = 'Toggle Widget Mode',
-            command = base.direct.manipulationControl.toggleObjectHandlesMode)
+            command = ShowBaseGlobal.direct.manipulationControl.toggleObjectHandlesMode)
 
         self.directWidgetOnTop = tk.BooleanVar()
-        self.directWidgetOnTop.set(0)
+        self.directWidgetOnTop.set(False)
         self.menuBar.addmenuitem('DIRECT', 'checkbutton',
                                  'DIRECT Widget On Top',
                                  label = 'Widget On Top',
@@ -130,7 +131,7 @@ class DirectSessionPanel(AppShell):
         self.menuBar.addmenuitem('DIRECT', 'command',
                                  'Deselect All',
                                  label = 'Deselect All',
-                                 command = base.direct.deselectAll)
+                                 command = ShowBaseGlobal.direct.deselectAll)
 
         # Get a handle to the menu frame
         menuFrame = self.menuFrame
@@ -150,8 +151,8 @@ class DirectSessionPanel(AppShell):
         self.bind(self.nodePathMenu, 'Select node path to manipulate')
 
         self.undoButton = tk.Button(menuFrame, text = 'Undo',
-                                    command = base.direct.undo)
-        if base.direct.undoList:
+                                    command = ShowBaseGlobal.direct.undo)
+        if ShowBaseGlobal.direct.undoList:
             self.undoButton['state'] = 'normal'
         else:
             self.undoButton['state'] = 'disabled'
@@ -159,8 +160,8 @@ class DirectSessionPanel(AppShell):
         self.bind(self.undoButton, 'Undo last operation')
 
         self.redoButton = tk.Button(menuFrame, text = 'Redo',
-                                    command = base.direct.redo)
-        if base.direct.redoList:
+                                    command = ShowBaseGlobal.direct.redo)
+        if ShowBaseGlobal.direct.redoList:
             self.redoButton['state'] = 'normal'
         else:
             self.redoButton['state'] = 'disabled'
@@ -177,7 +178,7 @@ class DirectSessionPanel(AppShell):
 
         # Scene Graph Explorer
         self.SGE = SceneGraphExplorer.SceneGraphExplorer(
-            sgeFrame, nodePath = render,
+            sgeFrame, nodePath = ShowBaseGlobal.base.render,
             scrolledCanvas_hull_width = 250,
             scrolledCanvas_hull_height = 300)
         self.SGE.pack(fill = tk.BOTH, expand = 1)
@@ -218,7 +219,7 @@ class DirectSessionPanel(AppShell):
         tk.Label(drFrame, text = 'Display Region',
                  font=('MSSansSerif', 14, 'bold')).pack(expand = 0)
 
-        nameList = ['Display Region ' + repr(x) for x in range(len(base.direct.drList))]
+        nameList = ['Display Region ' + repr(x) for x in range(len(ShowBaseGlobal.direct.drList))]
         self.drMenu = Pmw.ComboBox(
             drFrame, labelpos = tk.W, label_text = 'Display Region:',
             entry_width = 20,
@@ -264,7 +265,7 @@ class DirectSessionPanel(AppShell):
 
         frame = tk.Frame(fovFrame)
         self.lockedFov = tk.BooleanVar()
-        self.lockedFov.set(1)
+        self.lockedFov.set(True)
         self.lockedFovButton = tk.Checkbutton(
             frame,
             text = 'Locked',
@@ -289,25 +290,25 @@ class DirectSessionPanel(AppShell):
         self.toggleBackfaceButton = tk.Button(
             toggleFrame,
             text = 'Backface',
-            command = base.toggleBackface)
+            command = ShowBaseGlobal.base.toggleBackface)
         self.toggleBackfaceButton.pack(side = tk.LEFT, fill = tk.X, expand = 1)
 
         self.toggleLightsButton = tk.Button(
             toggleFrame,
             text = 'Lights',
-            command = base.direct.lights.toggle)
+            command = ShowBaseGlobal.direct.lights.toggle)
         self.toggleLightsButton.pack(side = tk.LEFT, fill = tk.X, expand = 1)
 
         self.toggleTextureButton = tk.Button(
             toggleFrame,
             text = 'Texture',
-            command = base.toggleTexture)
+            command = ShowBaseGlobal.base.toggleTexture)
         self.toggleTextureButton.pack(side = tk.LEFT, fill = tk.X, expand = 1)
 
         self.toggleWireframeButton = tk.Button(
             toggleFrame,
             text = 'Wireframe',
-            command = base.toggleWireframe)
+            command = ShowBaseGlobal.base.toggleWireframe)
         self.toggleWireframeButton.pack(fill = tk.X, expand = 1)
         toggleFrame.pack(side = tk.LEFT, fill = tk.X, expand = 1)
 
@@ -354,7 +355,7 @@ class DirectSessionPanel(AppShell):
         mainSwitchFrame.pack(fill = tk.X, expand = 0)
 
         # Widget to select a light to configure
-        nameList = base.direct.lights.getNameList()
+        nameList = ShowBaseGlobal.direct.lights.getNameList()
         lightMenuFrame = tk.Frame(lightFrame)
 
         self.lightMenu = Pmw.ComboBox(
@@ -510,36 +511,36 @@ class DirectSessionPanel(AppShell):
             gridPage,
             text = 'Grid Spacing',
             min = 0.1,
-            value = base.direct.grid.getGridSpacing())
-        self.gridSpacing['command'] = base.direct.grid.setGridSpacing
+            value = ShowBaseGlobal.direct.grid.getGridSpacing())
+        self.gridSpacing['command'] = ShowBaseGlobal.direct.grid.setGridSpacing
         self.gridSpacing.pack(fill = tk.X, expand = 0)
 
         self.gridSize = Floater.Floater(
             gridPage,
             text = 'Grid Size',
             min = 1.0,
-            value = base.direct.grid.getGridSize())
-        self.gridSize['command'] = base.direct.grid.setGridSize
+            value = ShowBaseGlobal.direct.grid.getGridSize())
+        self.gridSize['command'] = ShowBaseGlobal.direct.grid.setGridSize
         self.gridSize.pack(fill = tk.X, expand = 0)
 
         self.gridSnapAngle = Dial.AngleDial(
             gridPage,
             text = 'Snap Angle',
             style = 'mini',
-            value = base.direct.grid.getSnapAngle())
-        self.gridSnapAngle['command'] = base.direct.grid.setSnapAngle
+            value = ShowBaseGlobal.direct.grid.getSnapAngle())
+        self.gridSnapAngle['command'] = ShowBaseGlobal.direct.grid.setSnapAngle
         self.gridSnapAngle.pack(fill = tk.X, expand = 0)
 
     def createDevicePage(self, devicePage):
         tk.Label(devicePage, text = 'DEVICES',
               font=('MSSansSerif', 14, 'bold')).pack(expand = 0)
 
-        if base.direct.joybox is not None:
+        if ShowBaseGlobal.direct.joybox is not None:
             joyboxFrame = tk.Frame(devicePage, borderwidth = 2, relief = 'sunken')
             tk.Label(joyboxFrame, text = 'Joybox',
                      font=('MSSansSerif', 14, 'bold')).pack(expand = 0)
             self.enableJoybox = tk.BooleanVar()
-            self.enableJoybox.set(1)
+            self.enableJoybox.set(True)
             self.enableJoyboxButton = tk.Checkbutton(
                 joyboxFrame,
                 text = 'Enabled/Disabled',
@@ -581,7 +582,7 @@ class DirectSessionPanel(AppShell):
                 hull_relief = tk.RIDGE, hull_borderwidth = 2,
                 min = 1.0, max = 100.0)
             self.jbXyzSF['command'] = (
-                lambda v: base.direct.joybox.setXyzMultiplier(v))
+                lambda v: ShowBaseGlobal.direct.joybox.setXyzMultiplier(v))
             self.jbXyzSF.pack(fill = tk.X, expand = 0)
             self.bind(self.jbXyzSF, 'Set joybox XYZ speed multiplier')
 
@@ -592,7 +593,7 @@ class DirectSessionPanel(AppShell):
                 hull_relief = tk.RIDGE, hull_borderwidth = 2,
                 min = 1.0, max = 100.0)
             self.jbHprSF['command'] = (
-                lambda v: base.direct.joybox.setHprMultiplier(v))
+                lambda v: ShowBaseGlobal.direct.joybox.setHprMultiplier(v))
             self.jbHprSF.pack(fill = tk.X, expand = 0)
             self.bind(self.jbHprSF, 'Set joybox HPR speed multiplier')
 
@@ -604,30 +605,30 @@ class DirectSessionPanel(AppShell):
 
     def createMemPage(self, memPage):
         self.MemExp = MemoryExplorer.MemoryExplorer(
-            memPage, nodePath = render,
+            memPage, nodePath = ShowBaseGlobal.base.render,
             scrolledCanvas_hull_width = 250,
             scrolledCanvas_hull_height = 250)
         self.MemExp.pack(fill = tk.BOTH, expand = 1)
 
     def toggleDirect(self):
         if self.directEnabled.get():
-            base.direct.enable()
+            ShowBaseGlobal.direct.enable()
         else:
-            base.direct.disable()
+            ShowBaseGlobal.direct.disable()
 
     def toggleDirectGrid(self):
         if self.directGridEnabled.get():
-            base.direct.grid.enable()
+            ShowBaseGlobal.direct.grid.enable()
         else:
-            base.direct.grid.disable()
+            ShowBaseGlobal.direct.grid.disable()
 
     def toggleWidgetOnTop(self):
         if self.directWidgetOnTop.get():
-            base.direct.widget.setBin('gui-popup', 0)
-            base.direct.widget.setDepthTest(0)
+            ShowBaseGlobal.direct.widget.setBin('gui-popup', 0)
+            ShowBaseGlobal.direct.widget.setDepthTest(0)
         else:
-            base.direct.widget.clearBin()
-            base.direct.widget.setDepthTest(1)
+            ShowBaseGlobal.direct.widget.clearBin()
+            ShowBaseGlobal.direct.widget.setDepthTest(1)
 
     def selectedNodePathHook(self, nodePath):
         # Make sure node path is in nodePathDict
@@ -657,7 +658,7 @@ class DirectSessionPanel(AppShell):
         # Did we finally get something?
         if nodePath is not None:
             # Yes, select it!
-            base.direct.select(nodePath)
+            ShowBaseGlobal.direct.select(nodePath)
 
     def addNodePath(self, nodePath):
         self.addNodePathToDict(nodePath, self.nodePathNames,
@@ -665,25 +666,25 @@ class DirectSessionPanel(AppShell):
 
     def selectJBModeNamed(self, name):
         if name == 'Joe Mode':
-            base.direct.joybox.joeMode()
+            ShowBaseGlobal.direct.joybox.joeMode()
         elif name == 'Drive Mode':
-            base.direct.joybox.driveMode()
+            ShowBaseGlobal.direct.joybox.driveMode()
         elif name == 'Orbit Mode':
-            base.direct.joybox.orbitMode()
+            ShowBaseGlobal.direct.joybox.orbitMode()
         elif name == 'Look At Mode':
-            base.direct.joybox.lookAtMode()
+            ShowBaseGlobal.direct.joybox.lookAtMode()
         elif name == 'Look Around Mode':
-            base.direct.joybox.lookAroundMode()
+            ShowBaseGlobal.direct.joybox.lookAroundMode()
         elif name == 'Walkthru Mode':
-            base.direct.joybox.walkthruMode()
+            ShowBaseGlobal.direct.joybox.walkthruMode()
         elif name == 'Demo Mode':
-            base.direct.joybox.demoMode()
+            ShowBaseGlobal.direct.joybox.demoMode()
         elif name == 'HPRXYZ Mode':
-            base.direct.joybox.hprXyzMode()
+            ShowBaseGlobal.direct.joybox.hprXyzMode()
 
     def selectJBNodePathNamed(self, name):
         if name == 'selected':
-            nodePath = base.direct.selected.last
+            nodePath = ShowBaseGlobal.direct.selected.last
             # Add Combo box entry for this selected object
             self.addJBNodePath(nodePath)
         else:
@@ -708,9 +709,9 @@ class DirectSessionPanel(AppShell):
         if nodePath is not None:
             # Yes, select it!
             if nodePath == 'No Node Path':
-                base.direct.joybox.setNodePath(None)
+                ShowBaseGlobal.direct.joybox.setNodePath(None)
             else:
-                base.direct.joybox.setNodePath(nodePath)
+                ShowBaseGlobal.direct.joybox.setNodePath(nodePath)
 
     def addJBNodePath(self, nodePath):
         self.addNodePathToDict(nodePath, self.jbNodePathNames,
@@ -741,14 +742,13 @@ class DirectSessionPanel(AppShell):
         self.setBackgroundColorVec((r, g, b))
 
     def setBackgroundColorVec(self, color):
-        base.setBackgroundColor(color[0]/255.0,
-                                color[1]/255.0,
-                                color[2]/255.0)
+        ShowBaseGlobal.base.setBackgroundColor(
+            color[0] / 255.0, color[1] / 255.0, color[2] / 255.0)
 
     def selectDisplayRegionNamed(self, name):
         if name.find('Display Region ') >= 0:
             drIndex = int(name[-1:])
-            self.activeDisplayRegion = base.direct.drList[drIndex]
+            self.activeDisplayRegion = ShowBaseGlobal.direct.drList[drIndex]
         else:
             self.activeDisplayRegion = None
         # Make sure info is current
@@ -758,13 +758,13 @@ class DirectSessionPanel(AppShell):
         dr = self.activeDisplayRegion
         if dr:
             dr.camLens.setNear(near)
-            cluster('base.camLens.setNear(%f)' % near, 0)
+            ShowBaseGlobal.direct.cluster('base.camLens.setNear(%f)' % near, 0)
 
     def setFar(self, far):
         dr = self.activeDisplayRegion
         if dr:
             dr.camLens.setFar(far)
-            cluster('base.camLens.setFar(%f)' % far, 0)
+            ShowBaseGlobal.direct.cluster('base.camLens.setFar(%f)' % far, 0)
 
     def setHFov(self, hFov):
         dr = self.activeDisplayRegion
@@ -802,10 +802,10 @@ class DirectSessionPanel(AppShell):
     # Lights #
     def selectLightNamed(self, name):
         # See if light exists
-        self.activeLight = base.direct.lights[name]
+        self.activeLight = ShowBaseGlobal.direct.lights[name]
         # If not...create new one
         if self.activeLight is None:
-            self.activeLight = base.direct.lights.create(name)
+            self.activeLight = ShowBaseGlobal.direct.lights.create(name)
         # Do we have a valid light at this point?
         if self.activeLight:
             light = self.activeLight.getLight()
@@ -820,28 +820,28 @@ class DirectSessionPanel(AppShell):
         else:
             # Restore valid data
             listbox = self.lightMenu.component('scrolledlist')
-            listbox.setlist(base.direct.lights.getNameList())
-            if len(base.direct.lights) > 0:
-                self.lightMenu.selectitem(base.direct.lights.getNameList()[0])
+            listbox.setlist(ShowBaseGlobal.direct.lights.getNameList())
+            if len(ShowBaseGlobal.direct.lights) > 0:
+                self.lightMenu.selectitem(ShowBaseGlobal.direct.lights.getNameList()[0])
         # Make sure info is current
         self.updateLightInfo()
 
     def addAmbient(self):
-        return base.direct.lights.create('ambient')
+        return ShowBaseGlobal.direct.lights.create('ambient')
 
     def addDirectional(self):
-        return base.direct.lights.create('directional')
+        return ShowBaseGlobal.direct.lights.create('directional')
 
     def addPoint(self):
-        return base.direct.lights.create('point')
+        return ShowBaseGlobal.direct.lights.create('point')
 
     def addSpot(self):
-        return base.direct.lights.create('spot')
+        return ShowBaseGlobal.direct.lights.create('spot')
 
     def addLight(self, light):
         # Make list reflect current list of lights
         listbox = self.lightMenu.component('scrolledlist')
-        listbox.setlist(base.direct.lights.getNameList())
+        listbox.setlist(ShowBaseGlobal.direct.lights.getNameList())
         # Select the newly added light
         self.lightMenu.selectitem(light.getName())
         # And show corresponding page
@@ -849,16 +849,16 @@ class DirectSessionPanel(AppShell):
 
     def toggleLights(self):
         if self.enableLights.get():
-            base.direct.lights.allOn()
+            ShowBaseGlobal.direct.lights.allOn()
         else:
-            base.direct.lights.allOff()
+            ShowBaseGlobal.direct.lights.allOff()
 
     def toggleActiveLight(self):
         if self.activeLight:
             if self.lightActive.get():
-                base.direct.lights.setOn(self.activeLight)
+                ShowBaseGlobal.direct.lights.setOn(self.activeLight)
             else:
-                base.direct.lights.setOff(self.activeLight)
+                ShowBaseGlobal.direct.lights.setOff(self.activeLight)
 
     def setLightColor(self, color):
         if self.activeLight:
@@ -893,22 +893,22 @@ class DirectSessionPanel(AppShell):
     ## GRID CONTROLS ##
     def toggleGrid(self):
         if self.enableGrid.get():
-            base.direct.grid.enable()
+            ShowBaseGlobal.direct.grid.enable()
         else:
-            base.direct.grid.disable()
+            ShowBaseGlobal.direct.grid.disable()
 
     def toggleXyzSnap(self):
-        base.direct.grid.setXyzSnap(self.xyzSnap.get())
+        ShowBaseGlobal.direct.grid.setXyzSnap(self.xyzSnap.get())
 
     def toggleHprSnap(self):
-        base.direct.grid.setHprSnap(self.hprSnap.get())
+        ShowBaseGlobal.direct.grid.setHprSnap(self.hprSnap.get())
 
     ## DEVICE CONTROLS
     def toggleJoybox(self):
         if self.enableJoybox.get():
-            base.direct.joybox.enable()
+            ShowBaseGlobal.direct.joybox.enable()
         else:
-            base.direct.joybox.disable()
+            ShowBaseGlobal.direct.joybox.disable()
 
     ## UPDATE INFO ##
     def updateInfo(self, page = 'Environment'):
@@ -920,7 +920,7 @@ class DirectSessionPanel(AppShell):
             self.updateGridInfo()
 
     def updateEnvironmentInfo(self):
-        bkgrdColor = base.getBackgroundColor() * 255.0
+        bkgrdColor = ShowBaseGlobal.base.getBackgroundColor() * 255.0
         self.backgroundColor.set([bkgrdColor[0],
                                   bkgrdColor[1],
                                   bkgrdColor[2],
@@ -936,6 +936,7 @@ class DirectSessionPanel(AppShell):
 
     def updateLightInfo(self, page = None):
         # Set main lighting button
+        render = ShowBaseGlobal.base.render
         self.enableLights.set(
             render.node().hasAttrib(LightAttrib.getClassType()))
 
@@ -974,16 +975,16 @@ class DirectSessionPanel(AppShell):
                 self.pQuadraticAttenuation.set(att[2], 0)
 
     def updateGridInfo(self):
-        self.enableGrid.set(base.direct.grid.isEnabled())
-        self.xyzSnap.set(base.direct.grid.getXyzSnap())
-        self.hprSnap.set(base.direct.grid.getHprSnap())
-        self.gridSpacing.set(base.direct.grid.getGridSpacing(), 0)
-        self.gridSize.set(base.direct.grid.getGridSize(), 0)
-        self.gridSnapAngle.set(base.direct.grid.getSnapAngle(), 0)
+        self.enableGrid.set(ShowBaseGlobal.direct.grid.isEnabled())
+        self.xyzSnap.set(ShowBaseGlobal.direct.grid.getXyzSnap())
+        self.hprSnap.set(ShowBaseGlobal.direct.grid.getHprSnap())
+        self.gridSpacing.set(ShowBaseGlobal.direct.grid.getGridSpacing(), 0)
+        self.gridSize.set(ShowBaseGlobal.direct.grid.getGridSize(), 0)
+        self.gridSnapAngle.set(ShowBaseGlobal.direct.grid.getSnapAngle(), 0)
 
     # UNDO/REDO
     def pushUndo(self, fResetRedo = 1):
-        base.direct.pushUndo([self['nodePath']])
+        ShowBaseGlobal.direct.pushUndo([self['nodePath']])
 
     def undoHook(self, nodePathList = []):
         pass
@@ -997,7 +998,7 @@ class DirectSessionPanel(AppShell):
         self.undoButton.configure(state = 'disabled')
 
     def pushRedo(self):
-        base.direct.pushRedo([self['nodePath']])
+        ShowBaseGlobal.direct.pushRedo([self['nodePath']])
 
     def redoHook(self, nodePathList = []):
         pass

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

@@ -11,6 +11,7 @@ so that I can just type: ``inspect(anObject)`` any time.
 See :ref:`inspection-utilities` for more information.
 """
 
+from __future__ import annotations
 
 __all__ = ['inspect', 'inspectorFor', 'Inspector', 'ModuleInspector', 'ClassInspector', 'InstanceInspector', 'FunctionInspector', 'InstanceMethodInspector', 'CodeInspector', 'ComplexInspector', 'DictionaryInspector', 'SequenceInspector', 'SliceInspector', 'InspectorWindow']
 
@@ -31,6 +32,8 @@ def inspect(anObject):
 
 ### private
 
+_InspectorMap: dict[str, str]
+
 
 def inspectorFor(anObject):
     typeName = type(anObject).__name__.capitalize() + 'Type'
@@ -396,7 +399,8 @@ class InspectorWindow:
         self.listWidget.component('listbox').focus_set()
 
     def showHelp(self):
-        help = tk.Toplevel(base.tkRoot)
+        from direct.showbase import ShowBaseGlobal
+        help = tk.Toplevel(ShowBaseGlobal.base.tkRoot)
         help.title("Inspector Help")
         frame = tk.Frame(help)
         frame.pack()

+ 26 - 25
direct/src/tkpanels/Placer.py

@@ -9,6 +9,7 @@ from direct.tkwidgets import Dial
 from direct.tkwidgets import Floater
 from direct.directtools.DirectGlobals import ZERO_VEC, UNIT_VEC
 from direct.showbase.MessengerGlobal import messenger
+from direct.showbase import ShowBaseGlobal
 from direct.task.TaskManagerGlobal import taskMgr
 import Pmw
 import tkinter as tk
@@ -28,7 +29,7 @@ class Placer(AppShell):
         INITOPT = Pmw.INITOPT
         optiondefs = (
             ('title',       self.appname,       None),
-            ('nodePath',    base.direct.camera,      None),
+            ('nodePath',    ShowBaseGlobal.direct.camera,      None),
         )
         self.defineoptions(kw, optiondefs)
 
@@ -39,23 +40,23 @@ class Placer(AppShell):
 
     def appInit(self):
         # Initialize state
-        self.tempCS = base.direct.group.attachNewNode('placerTempCS')
-        self.orbitFromCS = base.direct.group.attachNewNode(
+        self.tempCS = ShowBaseGlobal.direct.group.attachNewNode('placerTempCS')
+        self.orbitFromCS = ShowBaseGlobal.direct.group.attachNewNode(
             'placerOrbitFromCS')
-        self.orbitToCS = base.direct.group.attachNewNode('placerOrbitToCS')
+        self.orbitToCS = ShowBaseGlobal.direct.group.attachNewNode('placerOrbitToCS')
         self.refCS = self.tempCS
 
         # Dictionary keeping track of all node paths manipulated so far
         self.nodePathDict = {}
-        self.nodePathDict['camera'] = base.direct.camera
-        self.nodePathDict['widget'] = base.direct.widget
+        self.nodePathDict['camera'] = ShowBaseGlobal.direct.camera
+        self.nodePathDict['widget'] = ShowBaseGlobal.direct.widget
         self.nodePathNames = ['camera', 'widget', 'selected']
 
         self.refNodePathDict = {}
         self.refNodePathDict['parent'] = self['nodePath'].getParent()
         self.refNodePathDict['render'] = render
-        self.refNodePathDict['camera'] = base.direct.camera
-        self.refNodePathDict['widget'] = base.direct.widget
+        self.refNodePathDict['camera'] = ShowBaseGlobal.direct.camera
+        self.refNodePathDict['widget'] = ShowBaseGlobal.direct.widget
         self.refNodePathNames = ['parent', 'self', 'render',
                                  'camera', 'widget', 'selected']
 
@@ -103,12 +104,12 @@ class Placer(AppShell):
             'Placer', 'command',
             'Toggle widget visability',
             label = 'Toggle Widget Vis',
-            command = base.direct.toggleWidgetVis)
+            command = ShowBaseGlobal.direct.toggleWidgetVis)
         self.menuBar.addmenuitem(
             'Placer', 'command',
             'Toggle widget manipulation mode',
             label = 'Toggle Widget Mode',
-            command = base.direct.manipulationControl.toggleObjectHandlesMode)
+            command = ShowBaseGlobal.direct.manipulationControl.toggleObjectHandlesMode)
 
         # Get a handle to the menu frame
         menuFrame = self.menuFrame
@@ -145,8 +146,8 @@ class Placer(AppShell):
         self.bind(self.refNodePathMenu, 'Select relative node path')
 
         self.undoButton = tk.Button(menuFrame, text = 'Undo',
-                                    command = base.direct.undo)
-        if base.direct.undoList:
+                                    command = ShowBaseGlobal.direct.undo)
+        if ShowBaseGlobal.direct.undoList:
             self.undoButton['state'] = 'normal'
         else:
             self.undoButton['state'] = 'disabled'
@@ -154,8 +155,8 @@ class Placer(AppShell):
         self.bind(self.undoButton, 'Undo last operation')
 
         self.redoButton = tk.Button(menuFrame, text = 'Redo',
-                                    command = base.direct.redo)
-        if base.direct.redoList:
+                                    command = ShowBaseGlobal.direct.redo)
+        if ShowBaseGlobal.direct.redoList:
             self.redoButton['state'] = 'normal'
         else:
             self.redoButton['state'] = 'disabled'
@@ -394,7 +395,7 @@ class Placer(AppShell):
             # Add Combo box entry for the initial node path
             self.addNodePath(nodePath)
         elif name == 'selected':
-            nodePath = base.direct.selected.last
+            nodePath = ShowBaseGlobal.direct.selected.last
             # Add Combo box entry for this selected object
             self.addNodePath(nodePath)
         else:
@@ -417,7 +418,7 @@ class Placer(AppShell):
             else:
                 if name == 'widget':
                     # Record relationship between selected nodes and widget
-                    base.direct.selected.getWrtAll()
+                    ShowBaseGlobal.direct.selected.getWrtAll()
         # Update active node path
         self.setActiveNodePath(nodePath)
 
@@ -449,7 +450,7 @@ class Placer(AppShell):
         if name == 'self':
             nodePath = self.tempCS
         elif name == 'selected':
-            nodePath = base.direct.selected.last
+            nodePath = ShowBaseGlobal.direct.selected.last
             # Add Combo box entry for this selected object
             self.addRefNodePath(nodePath)
         elif name == 'parent':
@@ -560,13 +561,13 @@ class Placer(AppShell):
         elif self.movementMode == 'Orbit:':
             self.xformOrbit(value, axis)
         if self.nodePathMenu.get() == 'widget':
-            if base.direct.manipulationControl.fSetCoa:
+            if ShowBaseGlobal.direct.manipulationControl.fSetCoa:
                 # Update coa based on current widget position
-                base.direct.selected.last.mCoa2Dnp.assign(
-                    base.direct.widget.getMat(base.direct.selected.last))
+                ShowBaseGlobal.direct.selected.last.mCoa2Dnp.assign(
+                    ShowBaseGlobal.direct.widget.getMat(ShowBaseGlobal.direct.selected.last))
             else:
                 # Move the objects with the widget
-                base.direct.selected.moveWrtWidgetAll()
+                ShowBaseGlobal.direct.selected.moveWrtWidgetAll()
 
     def xformStart(self, data):
         # Record undo point
@@ -575,7 +576,7 @@ class Placer(AppShell):
         if self.nodePathMenu.get() == 'widget':
             taskMgr.remove('followSelectedNodePath')
             # Record relationship between selected nodes and widget
-            base.direct.selected.getWrtAll()
+            ShowBaseGlobal.direct.selected.getWrtAll()
         # Record initial state
         self.deltaHpr = self['nodePath'].getHpr(self.refCS)
         # Update placer to reflect new state
@@ -590,7 +591,7 @@ class Placer(AppShell):
         # If moving widget restart follow task
         if self.nodePathMenu.get() == 'widget':
             # Restart followSelectedNodePath task
-            base.direct.manipulationControl.spawnFollowSelectedNodePathTask()
+            ShowBaseGlobal.direct.manipulationControl.spawnFollowSelectedNodePathTask()
 
     def xformRelative(self, value, axis):
         nodePath = self['nodePath']
@@ -729,7 +730,7 @@ class Placer(AppShell):
             self.xformStop(None)
 
     def pushUndo(self, fResetRedo = 1):
-        base.direct.pushUndo([self['nodePath']])
+        ShowBaseGlobal.direct.pushUndo([self['nodePath']])
 
     def undoHook(self, nodePathList = []):
         # Reflect new changes
@@ -744,7 +745,7 @@ class Placer(AppShell):
         self.undoButton.configure(state = 'disabled')
 
     def pushRedo(self):
-        base.direct.pushRedo([self['nodePath']])
+        ShowBaseGlobal.direct.pushRedo([self['nodePath']])
 
     def redoHook(self, nodePathList = []):
         # Reflect new changes

+ 4 - 5
direct/src/wxwidgets/WxAppShell.py

@@ -79,13 +79,12 @@ class WxAppShell(wx.Frame):
         self.onDestroy(event)
 
         # to close Panda
-        try:
-            base
-        except NameError:
+        from direct.showbase import ShowBaseGlobal
+        if hasattr(ShowBaseGlobal, 'base'):
+            ShowBaseGlobal.base.userExit()
+        else:
             sys.exit()
 
-        base.userExit()
-
     ### USER METHODS ###
     # To be overridden
     def appInit(self):

+ 47 - 43
direct/src/wxwidgets/WxPandaShell.py

@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import wx
 from wx.lib.agw import fourwaysplitter as FWS
 
@@ -25,7 +27,7 @@ class WxPandaShell(WxAppShell):
     copyright       = ('Copyright 2010 Disney Online Studios.' +
                        '\nAll Rights Reserved.')
 
-    MENU_TEXTS = {
+    MENU_TEXTS: dict[int, tuple[str, str | None]] = {
         ID_FOUR_VIEW: ("Four Views", None),
         ID_TOP_VIEW: ("Top View", None),
         ID_FRONT_VIEW: ("Front View", None),
@@ -114,6 +116,7 @@ class WxPandaShell(WxAppShell):
         self.wxStep()
         ViewportManager.initializeAll()
         # Position the camera
+        base = ShowBaseGlobal.base
         if base.trackball is not None:
             base.trackball.node().setPos(0, 30, 0)
             base.trackball.node().setHpr(0, 15, 0)
@@ -125,33 +128,34 @@ class WxPandaShell(WxAppShell):
         # initializing direct
         if self.fStartDirect:
             base.startDirect(fWantTk = 0, fWantWx = 0)
-
-            base.direct.disableMouseEvents()
-            newMouseEvents = ["_le_per_%s"%x for x in base.direct.mouseEvents] +\
-                             ["_le_fro_%s"%x for x in base.direct.mouseEvents] +\
-                             ["_le_lef_%s"%x for x in base.direct.mouseEvents] +\
-                             ["_le_top_%s"%x for x in base.direct.mouseEvents]
-            base.direct.mouseEvents = newMouseEvents
-            base.direct.enableMouseEvents()
-
-            base.direct.disableKeyEvents()
-            keyEvents = ["_le_per_%s"%x for x in base.direct.keyEvents] +\
-                             ["_le_fro_%s"%x for x in base.direct.keyEvents] +\
-                             ["_le_lef_%s"%x for x in base.direct.keyEvents] +\
-                             ["_le_top_%s"%x for x in base.direct.keyEvents]
-            base.direct.keyEvents = keyEvents
-            base.direct.enableKeyEvents()
-
-            base.direct.disableModifierEvents()
-            modifierEvents = ["_le_per_%s"%x for x in base.direct.modifierEvents] +\
-                             ["_le_fro_%s"%x for x in base.direct.modifierEvents] +\
-                             ["_le_lef_%s"%x for x in base.direct.modifierEvents] +\
-                             ["_le_top_%s"%x for x in base.direct.modifierEvents]
-            base.direct.modifierEvents = modifierEvents
-            base.direct.enableModifierEvents()
-
-            base.direct.cameraControl.lockRoll = True
-            base.direct.setFScaleWidgetByCam(1)
+            direct = ShowBaseGlobal.direct
+
+            direct.disableMouseEvents()
+            newMouseEvents = ["_le_per_%s"%x for x in direct.mouseEvents] +\
+                             ["_le_fro_%s"%x for x in direct.mouseEvents] +\
+                             ["_le_lef_%s"%x for x in direct.mouseEvents] +\
+                             ["_le_top_%s"%x for x in direct.mouseEvents]
+            direct.mouseEvents = newMouseEvents
+            direct.enableMouseEvents()
+
+            direct.disableKeyEvents()
+            keyEvents = ["_le_per_%s"%x for x in direct.keyEvents] +\
+                             ["_le_fro_%s"%x for x in direct.keyEvents] +\
+                             ["_le_lef_%s"%x for x in direct.keyEvents] +\
+                             ["_le_top_%s"%x for x in direct.keyEvents]
+            direct.keyEvents = keyEvents
+            direct.enableKeyEvents()
+
+            direct.disableModifierEvents()
+            modifierEvents = ["_le_per_%s"%x for x in direct.modifierEvents] +\
+                             ["_le_fro_%s"%x for x in direct.modifierEvents] +\
+                             ["_le_lef_%s"%x for x in direct.modifierEvents] +\
+                             ["_le_top_%s"%x for x in direct.modifierEvents]
+            direct.modifierEvents = modifierEvents
+            direct.enableModifierEvents()
+
+            direct.cameraControl.lockRoll = True
+            direct.setFScaleWidgetByCam(1)
 
             unpickables = [
                 "z-guide",
@@ -172,31 +176,31 @@ class WxPandaShell(WxAppShell):
                 "Sphere",]
 
             for unpickable in unpickables:
-                base.direct.addUnpickable(unpickable)
+                direct.addUnpickable(unpickable)
 
-            base.direct.manipulationControl.optionalSkipFlags |= SKIP_UNPICKABLE
-            base.direct.manipulationControl.fAllowMarquee = 1
-            base.direct.manipulationControl.supportMultiView()
-            base.direct.cameraControl.useMayaCamControls = 1
-            base.direct.cameraControl.perspCollPlane = self.perspView.collPlane
-            base.direct.cameraControl.perspCollPlane2 = self.perspView.collPlane2
+            direct.manipulationControl.optionalSkipFlags |= SKIP_UNPICKABLE
+            direct.manipulationControl.fAllowMarquee = 1
+            direct.manipulationControl.supportMultiView()
+            direct.cameraControl.useMayaCamControls = 1
+            direct.cameraControl.perspCollPlane = self.perspView.collPlane
+            direct.cameraControl.perspCollPlane2 = self.perspView.collPlane2
 
-            for widget in base.direct.manipulationControl.widgetList:
+            for widget in direct.manipulationControl.widgetList:
                 widget.setBin('gui-popup', 0)
                 widget.setDepthTest(0)
 
             # [gjeon] to intercept messages here
-            base.direct.ignore('DIRECT-delete')
-            base.direct.ignore('DIRECT-select')
-            base.direct.ignore('DIRECT-preDeselectAll')
-            base.direct.ignore('DIRECT-toggleWidgetVis')
-            base.direct.fIgnoreDirectOnlyKeyMap = 1
+            direct.ignore('DIRECT-delete')
+            direct.ignore('DIRECT-select')
+            direct.ignore('DIRECT-preDeselectAll')
+            direct.ignore('DIRECT-toggleWidgetVis')
+            direct.fIgnoreDirectOnlyKeyMap = 1
 
             # [gjeon] do not use the old way of finding current DR
-            base.direct.drList.tryToGetCurrentDr = False
+            direct.drList.tryToGetCurrentDr = False
 
         else:
-            base.direct=None
+            base.direct = None
         #base.closeWindow(base.win)
         base.win = base.winList[3]
 

+ 17 - 12
direct/src/wxwidgets/WxSlider.py

@@ -16,7 +16,7 @@ class WxSlider(wx.Slider):
 
         self.maxValue = maxValue
         self.minValue = minValue
-        intVal = 100.0 / (self.maxValue - self.minValue) * (value - self.minValue)
+        intVal = int(100.0 / (self.maxValue - self.minValue) * (value - self.minValue))
 
         intMin = 0
         intMax = 100
@@ -35,33 +35,37 @@ class WxSlider(wx.Slider):
                                              textSize, wx.TE_CENTER | wx.TE_PROCESS_ENTER)
 
                 self.textValue.Disable()
-                newPos = (pos[0], pos[1] + 20)
+                pos = (pos[0], pos[1] + 20)
         else:
             newStyle = wx.SL_VERTICAL
-            newPos = (pos[0], pos[1] + 40)
+            pos = (pos[0], pos[1] + 40)
 
         if style & wx.SL_AUTOTICKS:
             newStyle |= wx.SL_AUTOTICKS
 
-        wx.Slider.__init__(self, parent, id, intVal, intMin, intMax, newPos, size, style=newStyle)
+        wx.Slider.__init__(self, parent, id, intVal, intMin, intMax, pos, size, style=newStyle)
         self.Disable()
 
     def GetValue(self):
         # overriding wx.Slider.GetValue()
-        #return (wx.Slider.GetValue(self) * (self.maxValue - self.minValue) / 100.0 + self.minValue)
-        return float(self.textValue.GetValue()) # [gjeon] since the value from the slider is not as precise as the value entered by the user
+        if self.textValue is not None: # Horizontal with labels
+            return float(self.textValue.GetValue()) # [gjeon] since the value from the slider is not as precise as the value entered by the user
+        else:
+            return (wx.Slider.GetValue(self) * (self.maxValue - self.minValue) / 100.0 + self.minValue)
 
     def SetValue(self, value):
         # overriding wx.Slider.SetValue()
-        self.textValue.SetValue("%.2f" % value)
+        if self.textValue is not None:
+            self.textValue.SetValue("%.2f" % value)
         intVal = 100.0 / (self.maxValue - self.minValue) * (value - self.minValue)
         wx.Slider.SetValue(self, intVal)
 
     def onChange(self, event):
         # update textValue from slider
-        self.textValue.Clear()
-        floatVal = wx.Slider.GetValue(self) * (self.maxValue - self.minValue) / 100.0 + self.minValue
-        self.textValue.WriteText("%.2f" % floatVal)
+        if self.textValue is not None:
+            self.textValue.Clear()
+            floatVal = wx.Slider.GetValue(self) * (self.maxValue - self.minValue) / 100.0 + self.minValue
+            self.textValue.WriteText("%.2f" % floatVal)
         if self.updateCB: # callback function sould receive event as the argument
             self.updateCB(event)
         event.Skip()
@@ -82,13 +86,14 @@ class WxSlider(wx.Slider):
     def Disable(self):
         # overriding wx.Slider.Disable()
         wx.Slider.Disable(self)
-        self.textValue.Disable()
+        if self.textValue is not None:
+            self.textValue.Disable()
 
     def Enable(self):
         # overriding wx.Slider.Enable()
         wx.Slider.Enable(self)
         self.Bind(wx.EVT_SLIDER, self.onChange)
 
-        if not self.textValue is None:
+        if self.textValue is not None:
             self.textValue.Enable()
             self.textValue.Bind(wx.EVT_TEXT_ENTER, self.onEnter)

+ 4 - 4
dtool/Config.cmake

@@ -453,10 +453,10 @@ on DirectX rendering." OFF)
 mark_as_advanced(SUPPORT_FIXED_FUNCTION)
 
 # Should build tinydisplay?
-#option(HAVE_TINYDISPLAY
-#  "Builds TinyDisplay, a light software renderer based on TinyGL,
-#that is built into Panda. TinyDisplay is not as full-featured as Mesa
-#but is many times faster." ON)
+option(HAVE_TINYDISPLAY
+  "Builds TinyDisplay, a light software renderer based on TinyGL,
+that is built into Panda. TinyDisplay is not as full-featured as Mesa
+but is many times faster." ON)
 
 # Is SDL installed, and where?
 set(Threads_FIND_QUIETLY TRUE) # Fix for builtin FindSDL

+ 26 - 10
dtool/Package.cmake

@@ -257,16 +257,32 @@ if(HAVE_PYTHON)
     set(_ARCH_DIR ".")
 
   elseif(PYTHON_EXECUTABLE)
-    execute_process(
-      COMMAND ${PYTHON_EXECUTABLE}
-        -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(False))"
-      OUTPUT_VARIABLE _LIB_DIR
-      OUTPUT_STRIP_TRAILING_WHITESPACE)
-    execute_process(
-      COMMAND ${PYTHON_EXECUTABLE}
-        -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(True))"
-      OUTPUT_VARIABLE _ARCH_DIR
-      OUTPUT_STRIP_TRAILING_WHITESPACE)
+    # Python 3.12 drops the distutils module, so we have to use the newer
+    # sysconfig module instead.  Earlier versions of Python had the newer
+    # module too, but it was broken in Debian/Ubuntu, see #1230
+    if(PYTHON_VERSION_STRING VERSION_LESS "3.12")
+      execute_process(
+        COMMAND ${PYTHON_EXECUTABLE}
+          -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(False))"
+        OUTPUT_VARIABLE _LIB_DIR
+        OUTPUT_STRIP_TRAILING_WHITESPACE)
+      execute_process(
+        COMMAND ${PYTHON_EXECUTABLE}
+          -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(True))"
+        OUTPUT_VARIABLE _ARCH_DIR
+        OUTPUT_STRIP_TRAILING_WHITESPACE)
+    else()
+      execute_process(
+        COMMAND ${PYTHON_EXECUTABLE}
+          -c "import sysconfig; print(sysconfig.get_path('purelib'))"
+        OUTPUT_VARIABLE _LIB_DIR
+        OUTPUT_STRIP_TRAILING_WHITESPACE)
+      execute_process(
+        COMMAND ${PYTHON_EXECUTABLE}
+          -c "import sysconfig; print(sysconfig.get_path('platlib'))"
+        OUTPUT_VARIABLE _ARCH_DIR
+        OUTPUT_STRIP_TRAILING_WHITESPACE)
+    endif()
 
   else()
     set(_LIB_DIR "")

+ 1 - 17
dtool/src/dtoolbase/deletedBufferChain.I

@@ -16,7 +16,7 @@
  * size.
  */
 constexpr DeletedBufferChain::
-DeletedBufferChain(size_t buffer_size) : _buffer_size(buffer_size) {
+DeletedBufferChain(size_t buffer_size) noexcept : _buffer_size(buffer_size) {
 }
 
 /**
@@ -77,22 +77,6 @@ operator < (const DeletedBufferChain &other) const {
   return _buffer_size < other._buffer_size;
 }
 
-/**
- * Returns a deleted chain of the given size.
- */
-INLINE DeletedBufferChain *DeletedBufferChain::
-get_deleted_chain(size_t buffer_size) {
-  // We must allocate at least this much space for bookkeeping reasons.
-  buffer_size = (std::max)(buffer_size, sizeof(ObjectNode));
-
-  size_t index = ((buffer_size + sizeof(void *) - 1) / sizeof(void *)) - 1;
-  if (index < num_small_deleted_chains) {
-    return &_small_deleted_chains[index];
-  } else {
-    return get_large_deleted_chain((index + 1) * sizeof(void *));
-  }
-}
-
 /**
  * Casts an ObjectNode* to a void* buffer.
  */

+ 18 - 27
dtool/src/dtoolbase/deletedBufferChain.cxx

@@ -16,32 +16,10 @@
 
 #include <set>
 
-DeletedBufferChain DeletedBufferChain::_small_deleted_chains[DeletedBufferChain::num_small_deleted_chains] = {
-  DeletedBufferChain(sizeof(void *)),
-  DeletedBufferChain(sizeof(void *) * 2),
-  DeletedBufferChain(sizeof(void *) * 3),
-  DeletedBufferChain(sizeof(void *) * 4),
-  DeletedBufferChain(sizeof(void *) * 5),
-  DeletedBufferChain(sizeof(void *) * 6),
-  DeletedBufferChain(sizeof(void *) * 7),
-  DeletedBufferChain(sizeof(void *) * 8),
-  DeletedBufferChain(sizeof(void *) * 9),
-  DeletedBufferChain(sizeof(void *) * 10),
-  DeletedBufferChain(sizeof(void *) * 11),
-  DeletedBufferChain(sizeof(void *) * 12),
-  DeletedBufferChain(sizeof(void *) * 13),
-  DeletedBufferChain(sizeof(void *) * 14),
-  DeletedBufferChain(sizeof(void *) * 15),
-  DeletedBufferChain(sizeof(void *) * 16),
-  DeletedBufferChain(sizeof(void *) * 17),
-  DeletedBufferChain(sizeof(void *) * 18),
-  DeletedBufferChain(sizeof(void *) * 19),
-  DeletedBufferChain(sizeof(void *) * 20),
-  DeletedBufferChain(sizeof(void *) * 21),
-  DeletedBufferChain(sizeof(void *) * 22),
-  DeletedBufferChain(sizeof(void *) * 23),
-  DeletedBufferChain(sizeof(void *) * 24),
-};
+// This array stores the deleted chains for smaller sizes, starting with
+// sizeof(void *) and increasing in multiples thereof.
+static const size_t num_small_deleted_chains = 24;
+static DeletedBufferChain small_deleted_chains[num_small_deleted_chains] = {};
 
 /**
  * Allocates the memory for a new buffer of the indicated size (which must be
@@ -49,6 +27,8 @@ DeletedBufferChain DeletedBufferChain::_small_deleted_chains[DeletedBufferChain:
  */
 void *DeletedBufferChain::
 allocate(size_t size, TypeHandle type_handle) {
+  assert(_buffer_size > 0);
+
 #ifdef USE_DELETED_CHAIN
   // TAU_PROFILE("void *DeletedBufferChain::allocate(size_t, TypeHandle)", "
   // ", TAU_USER);
@@ -161,7 +141,18 @@ deallocate(void *ptr, TypeHandle type_handle) {
  * Returns a new DeletedBufferChain.
  */
 DeletedBufferChain *DeletedBufferChain::
-get_large_deleted_chain(size_t buffer_size) {
+get_deleted_chain(size_t buffer_size) {
+  // Common, smaller sized chains avoid the expensive locking and set
+  // manipulation code further down.
+  size_t index = ((buffer_size + sizeof(void *) - 1) / sizeof(void *));
+  buffer_size = index * sizeof(void *);
+  index--;
+  if (index < num_small_deleted_chains) {
+    DeletedBufferChain *chain = &small_deleted_chains[index];
+    chain->_buffer_size = buffer_size;
+    return chain;
+  }
+
   static MutexImpl lock;
   lock.lock();
   static std::set<DeletedBufferChain> deleted_chains;

+ 4 - 12
dtool/src/dtoolbase/deletedBufferChain.h

@@ -57,10 +57,9 @@ enum DeletedChainFlag : unsigned int {
  * Use MemoryHook to get a new DeletedBufferChain of a particular size.
  */
 class EXPCL_DTOOL_DTOOLBASE DeletedBufferChain {
-protected:
-  constexpr explicit DeletedBufferChain(size_t buffer_size);
-
 public:
+  constexpr DeletedBufferChain() = default;
+  constexpr explicit DeletedBufferChain(size_t buffer_size) noexcept;
   INLINE DeletedBufferChain(DeletedBufferChain &&from) noexcept;
   INLINE DeletedBufferChain(const DeletedBufferChain &copy);
 
@@ -72,11 +71,9 @@ public:
 
   INLINE bool operator < (const DeletedBufferChain &other) const;
 
-  static INLINE DeletedBufferChain *get_deleted_chain(size_t buffer_size);
+  static DeletedBufferChain *get_deleted_chain(size_t buffer_size);
 
 private:
-  static DeletedBufferChain *get_large_deleted_chain(size_t buffer_size);
-
   class ObjectNode {
   public:
 #ifdef USE_DELETEDCHAINFLAG
@@ -99,7 +96,7 @@ private:
   ObjectNode *_deleted_chain = nullptr;
 
   MutexImpl _lock;
-  const size_t _buffer_size;
+  size_t _buffer_size = 0;
 
 #ifndef USE_DELETEDCHAINFLAG
   // Without DELETEDCHAINFLAG, we don't even store the _flag member at all.
@@ -110,11 +107,6 @@ private:
   static const size_t flag_reserved_bytes = sizeof(AtomicAdjust::Integer);
 #endif  // USE_DELETEDCHAINFLAG
 
-  // This array stores the deleted chains for smaller sizes, starting with
-  // sizeof(void *) and increasing in multiples thereof.
-  static const size_t num_small_deleted_chains = 24;
-  static DeletedBufferChain _small_deleted_chains[num_small_deleted_chains];
-
   friend class MemoryHook;
 };
 

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

@@ -433,6 +433,8 @@ patomic_wait(const volatile uint32_t *value, uint32_t old) {
   while (*value == old) {
     _patomic_wait_func((volatile void *)value, &old, sizeof(uint32_t), INFINITE);
   }
+#elif defined(__APPLE__)
+  __ulock_wait(UL_COMPARE_AND_WAIT, (void *)value, old, 0);
 #elif defined(HAVE_POSIX_THREADS)
   _patomic_wait(value, old);
 #else
@@ -451,6 +453,8 @@ patomic_notify_one(volatile uint32_t *value) {
 //  WakeByAddressSingle((void *)value);
 #elif defined(_WIN32)
   _patomic_wake_one_func((void *)value);
+#elif defined(__APPLE__)
+  __ulock_wake(UL_COMPARE_AND_WAIT, (void *)value, 0);
 #elif defined(HAVE_POSIX_THREADS)
   _patomic_notify_all(value);
 #endif
@@ -467,6 +471,8 @@ patomic_notify_all(volatile uint32_t *value) {
 //  WakeByAddressAll((void *)value);
 #elif defined(_WIN32)
   _patomic_wake_all_func((void *)value);
+#elif defined(__APPLE__)
+  __ulock_wake(UL_COMPARE_AND_WAIT | ULF_WAKE_ALL, (void *)value, 0);
 #elif defined(HAVE_POSIX_THREADS)
   _patomic_notify_all(value);
 #endif

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

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

+ 10 - 1
dtool/src/dtoolbase/patomic.h

@@ -32,6 +32,15 @@
 #include <unistd.h>
 #endif
 
+#ifdef __APPLE__
+// Undocumented API, see https://outerproduct.net/futex-dictionary.html
+#define UL_COMPARE_AND_WAIT 1
+#define ULF_WAKE_ALL 0x00000100
+
+extern "C" int __ulock_wait(uint32_t op, void *addr, uint64_t value, uint32_t timeout);
+extern "C" int __ulock_wake(uint32_t op, void *addr, uint64_t wake_value);
+#endif
+
 #if defined(THREAD_DUMMY_IMPL) || defined(THREAD_SIMPLE_IMPL)
 
 /**
@@ -164,7 +173,7 @@ ALWAYS_INLINE void patomic_notify_all(volatile uint32_t *value);
 EXPCL_DTOOL_DTOOLBASE extern BOOL (__stdcall *_patomic_wait_func)(volatile VOID *, PVOID, SIZE_T, DWORD);
 EXPCL_DTOOL_DTOOLBASE extern void (__stdcall *_patomic_wake_one_func)(PVOID);
 EXPCL_DTOOL_DTOOLBASE extern void (__stdcall *_patomic_wake_all_func)(PVOID);
-#elif !defined(__linux__) && defined(HAVE_POSIX_THREADS)
+#elif !defined(__linux__) && !defined(__APPLE__) && defined(HAVE_POSIX_THREADS)
 EXPCL_DTOOL_DTOOLBASE void _patomic_wait(const volatile uint32_t *value, uint32_t old);
 EXPCL_DTOOL_DTOOLBASE void _patomic_notify_all(volatile uint32_t *value);
 #endif

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

@@ -21,7 +21,7 @@ static PyMemberDef standard_type_members[] = {
   {(char *)"this_const", T_BOOL, offsetof(Dtool_PyInstDef, _is_const), READONLY, (char *)"C++ 'this' const flag"},
 // {(char *)"this_signature", T_INT, offsetof(Dtool_PyInstDef, _signature),
 // READONLY, (char *)"A type check signature"},
-  {(char *)"this_metatype", T_OBJECT, offsetof(Dtool_PyInstDef, _My_Type), READONLY, (char *)"The dtool meta object"},
+  {(char *)"this_metatype", T_OBJECT_EX, offsetof(Dtool_PyInstDef, _My_Type), READONLY, (char *)"The dtool meta object"},
   {nullptr}  /* Sentinel */
 };
 

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

@@ -241,6 +241,18 @@ INLINE PyObject *PyObject_CallMethodOneArg(PyObject *obj, PyObject *name, PyObje
 }
 #endif
 
+/* Python 3.12 */
+
+#if PY_VERSION_HEX < 0x030C0000
+#  define PyLong_IsNonNegative(value) (Py_SIZE((value)) >= 0)
+#else
+INLINE bool PyLong_IsNonNegative(PyObject *value) {
+  int overflow = 0;
+  long longval = PyLong_AsLongAndOverflow(value, &overflow);
+  return overflow == 1 || longval >= 0;
+}
+#endif
+
 /* Other Python implementations */
 
 #endif  // HAVE_PYTHON

+ 1 - 3
makepanda/installpanda.py

@@ -12,9 +12,7 @@ import os
 import sys
 from optparse import OptionParser
 from makepandacore import *
-
-# DO NOT CHANGE TO sysconfig - see GitHub issue #1230
-from distutils.sysconfig import get_python_lib
+from locations import get_python_lib
 
 
 MIME_INFO = (

+ 32 - 0
makepanda/locations.py

@@ -0,0 +1,32 @@
+__all__ = [
+    'get_python_inc',
+    'get_config_var',
+    'get_python_version',
+    'PREFIX',
+    'get_python_lib',
+    'get_config_vars',
+]
+
+import sys
+
+if sys.version_info < (3, 12):
+    from distutils.sysconfig import *
+else:
+    from sysconfig import *
+
+    PREFIX = get_config_var('prefix')
+
+    def get_python_inc(plat_specific=False):
+        path_name = 'platinclude' if plat_specific else 'include'
+        return get_path(path_name)
+
+    def get_python_lib(plat_specific=False, standard_lib=False):
+        if standard_lib:
+            path_name = 'stdlib'
+            if plat_specific:
+                path_name = 'plat' + path_name
+        elif plat_specific:
+            path_name = 'platlib'
+        else:
+            path_name = 'purelib'
+        return get_path(path_name)

+ 1 - 2
makepanda/makepackage.py

@@ -942,8 +942,7 @@ def MakeInstallerAndroid(version, **kwargs):
                     shutil.copy(os.path.join(source_dir, base), target)
 
     # Copy the Python standard library to the .apk as well.
-    # DO NOT CHANGE TO sysconfig - see #1230
-    from distutils.sysconfig import get_python_lib
+    from locations import get_python_lib
     stdlib_source = get_python_lib(False, True)
     stdlib_target = os.path.join("apkroot", "lib", "python{0}.{1}".format(*sys.version_info))
     copy_python_tree(stdlib_source, stdlib_target)

+ 20 - 9
makepanda/makepanda.py

@@ -30,12 +30,10 @@ except:
     print("Please install the development package of Python and try again.")
     exit(1)
 
-if sys.version_info >= (3, 10):
-    from sysconfig import get_platform
-else:
-    from distutils.util import get_platform
 from makepandacore import *
 
+from sysconfig import get_platform
+
 try:
     import zlib
 except:
@@ -366,6 +364,12 @@ if GetHost() == "darwin":
 if VERSION is None:
     # Take the value from the setup.cfg file.
     VERSION = GetMetadataValue('version')
+    match = re.match(r'^\d+\.\d+(\.\d+)+', VERSION)
+    if not match:
+        exit("Invalid version %s in setup.cfg, three digits are required" % (VERSION))
+    if WHLVERSION is None:
+        WHLVERSION = VERSION
+    VERSION = match.group()
 
 if WHLVERSION is None:
     WHLVERSION = VERSION
@@ -2776,11 +2780,18 @@ template class CheckPandaVersion<void>;
 
 
 def CreatePandaVersionFiles():
-    version1=int(VERSION.split(".")[0])
-    version2=int(VERSION.split(".")[1])
-    version3=int(VERSION.split(".")[2])
-    nversion=version1*1000000+version2*1000+version3
-    if (DISTRIBUTOR != "cmu"):
+    parts = VERSION.split(".", 2)
+    version1 = int(parts[0])
+    version2 = int(parts[1])
+    version3 = 0
+    if len(parts) > 2:
+        for c in parts[2]:
+            if c.isdigit():
+                version3 = version3 * 10 + ord(c) - 48
+            else:
+                break
+    nversion = version1 * 1000000 + version2 * 1000 + version3
+    if DISTRIBUTOR != "cmu":
         # Subtract 1 if we are not an official version.
         nversion -= 1
 

+ 10 - 10
makepanda/makepandacore.py

@@ -6,7 +6,6 @@
 ########################################################################
 
 import configparser
-from distutils import sysconfig # DO NOT CHANGE to sysconfig - see #1230
 import fnmatch
 import getpass
 import glob
@@ -21,6 +20,7 @@ import sys
 import threading
 import _thread as thread
 import time
+import locations
 
 SUFFIX_INC = [".cxx",".cpp",".c",".h",".I",".yxx",".lxx",".mm",".rc",".r"]
 SUFFIX_DLL = [".dll",".dlo",".dle",".dli",".dlm",".mll",".exe",".pyd",".ocx"]
@@ -2218,7 +2218,7 @@ def SdkLocatePython(prefer_thirdparty_python=False):
         # On macOS, search for the Python framework directory matching the
         # version number of our current Python version.
         sysroot = SDK.get("MACOSX", "")
-        version = sysconfig.get_python_version()
+        version = locations.get_python_version()
 
         py_fwx = "{0}/System/Library/Frameworks/Python.framework/Versions/{1}".format(sysroot, version)
 
@@ -2243,19 +2243,19 @@ def SdkLocatePython(prefer_thirdparty_python=False):
         LibDirectory("PYTHON", py_fwx + "/lib")
 
     #elif GetTarget() == 'windows':
-    #    SDK["PYTHON"] = os.path.dirname(sysconfig.get_python_inc())
-    #    SDK["PYTHONVERSION"] = "python" + sysconfig.get_python_version()
+    #    SDK["PYTHON"] = os.path.dirname(locations.get_python_inc())
+    #    SDK["PYTHONVERSION"] = "python" + locations.get_python_version()
     #    SDK["PYTHONEXEC"] = sys.executable
 
     else:
-        SDK["PYTHON"] = sysconfig.get_python_inc()
-        SDK["PYTHONVERSION"] = "python" + sysconfig.get_python_version() + abiflags
+        SDK["PYTHON"] = locations.get_python_inc()
+        SDK["PYTHONVERSION"] = "python" + locations.get_python_version() + abiflags
         SDK["PYTHONEXEC"] = os.path.realpath(sys.executable)
 
     if CrossCompiling():
         # We need a version of Python we can run.
         SDK["PYTHONEXEC"] = sys.executable
-        host_version = "python" + sysconfig.get_python_version() + abiflags
+        host_version = "python" + locations.get_python_version() + abiflags
         if SDK["PYTHONVERSION"] != host_version:
             exit("Host Python version (%s) must be the same as target Python version (%s)!" % (host_version, SDK["PYTHONVERSION"]))
 
@@ -3422,7 +3422,7 @@ def GetExtensionSuffix():
 
 def GetPythonABI():
     if not CrossCompiling():
-        soabi = sysconfig.get_config_var('SOABI')
+        soabi = locations.get_config_var('SOABI')
         if soabi:
             return soabi
 
@@ -3552,8 +3552,8 @@ def GetCurrentPythonVersionInfo():
         "soabi": GetPythonABI(),
         "ext_suffix": GetExtensionSuffix(),
         "executable": sys.executable,
-        "purelib": sysconfig.get_python_lib(False),
-        "platlib": sysconfig.get_python_lib(True),
+        "purelib": locations.get_python_lib(False),
+        "platlib": locations.get_python_lib(True),
     }
 
 

+ 2 - 1
makepanda/makewheel.py

@@ -11,10 +11,11 @@ import tempfile
 import subprocess
 import time
 import struct
-from sysconfig import get_platform, get_config_var
 from optparse import OptionParser
 from base64 import urlsafe_b64encode
 from makepandacore import LocateBinary, GetExtensionSuffix, SetVerbose, GetVerbose, GetMetadataValue, CrossCompiling, GetThirdpartyDir, SDK, GetStrip
+from locations import get_config_var
+from sysconfig import get_platform
 
 
 def get_abi_tag():

+ 20 - 2
makepanda/test_wheel.py

@@ -41,7 +41,7 @@ def test_wheel(wheel, verbose=False):
     if sys.version_info >= (3, 10):
         packages += ["pytest>=6.2.4"]
     else:
-        packages += ["pytest"]
+        packages += ["pytest>=3.9.0"]
 
     if sys.version_info[0:2] == (3, 4):
         if sys.platform == "win32":
@@ -50,6 +50,9 @@ def test_wheel(wheel, verbose=False):
         # See https://github.com/python-attrs/attrs/pull/807
         packages += ["attrs<21"]
 
+    if sys.version_info >= (3, 12):
+        packages += ["setuptools"]
+
     if subprocess.call([python, "-m", "pip", "install"] + packages) != 0:
         shutil.rmtree(envdir)
         sys.exit(1)
@@ -59,7 +62,22 @@ def test_wheel(wheel, verbose=False):
     if verbose:
         test_cmd.append("--verbose")
 
-    exit_code = subprocess.call(test_cmd)
+    # Put the location of the python DLL on the path, for deploy-stub test
+    # This is needed because venv does not install a copy of the python DLL
+    env = None
+    if sys.platform == "win32":
+        deploy_libs = os.path.join(envdir, "Lib", "site-packages", "deploy_libs")
+        if os.path.isdir(deploy_libs):
+            # We have to do this dance because os.environ is case insensitive
+            env = dict(os.environ)
+            for key, value in env.items():
+                if key.upper() == "PATH":
+                    env[key] = deploy_libs + ";" + value
+                    break
+            else:
+                env["PATH"] = deploy_libs
+
+    exit_code = subprocess.call(test_cmd, env=env)
     shutil.rmtree(envdir)
 
     if exit_code != 0:

+ 3 - 0
panda/CMakeLists.txt

@@ -60,6 +60,9 @@ add_subdirectory(src/wgldisplay)
 add_subdirectory(src/windisplay)
 add_subdirectory(src/x11display)
 
+# Creates a metalib, so should go after
+add_subdirectory(src/tinydisplay)
+
 # For other components
 # bullet
 add_subdirectory(src/bullet)

+ 15 - 2
panda/src/cocoadisplay/cocoaGraphicsPipe.h

@@ -16,8 +16,10 @@
 
 #include "pandabase.h"
 #include "graphicsPipe.h"
+#include "patomic.h"
 
 #include <ApplicationServices/ApplicationServices.h>
+#include <CoreVideo/CoreVideo.h>
 
 /**
  * This graphics pipe represents the base class for pipes that create
@@ -28,17 +30,28 @@ public:
   CocoaGraphicsPipe(CGDirectDisplayID display = CGMainDisplayID());
   virtual ~CocoaGraphicsPipe();
 
+  virtual PreferredWindowThread get_preferred_window_thread() const;
+
   INLINE CGDirectDisplayID get_display_id() const;
 
-public:
-  virtual PreferredWindowThread get_preferred_window_thread() const;
+  bool init_vsync(uint32_t &counter);
+  void wait_vsync(uint32_t &counter, bool adaptive=false);
 
 private:
+  static CVReturn display_link_cb(CVDisplayLinkRef link, const CVTimeStamp *now,
+                                  const CVTimeStamp *output_time,
+                                  CVOptionFlags flags_in, CVOptionFlags *flags_out,
+                                  void *context);
+
   void load_display_information();
 
   // This is the Quartz display identifier.
   CGDirectDisplayID _display;
 
+  CVDisplayLinkRef _display_link = nullptr;
+  patomic<int> _last_wait_frame {0};
+  uint32_t _vsync_counter = 0;
+
 public:
   static TypeHandle get_class_type() {
     return _type_handle;

+ 186 - 5
panda/src/cocoadisplay/cocoaGraphicsPipe.mm

@@ -40,18 +40,49 @@ CocoaGraphicsPipe(CGDirectDisplayID display) : _display(display) {
   [thread start];
   [thread autorelease];
 
+  // If the application is dpi-aware, iterate over all the screens to find the
+  // one with our display ID and get the backing scale factor to configure the
+  // detected display zoom. Otherwise the detected display zoom keeps its
+  // default value of 1.0
+
+  if (dpi_aware) {
+    NSScreen *screen;
+    NSEnumerator *e = [[NSScreen screens] objectEnumerator];
+    while (screen = (NSScreen *) [e nextObject]) {
+      NSNumber *num = [[screen deviceDescription] objectForKey: @"NSScreenNumber"];
+      if (_display == (CGDirectDisplayID) [num longValue]) {
+        set_detected_display_zoom([screen backingScaleFactor]);
+        if (cocoadisplay_cat.is_debug()) {
+          cocoadisplay_cat.debug()
+            << "Display zoom is " << [screen backingScaleFactor] << "\n";
+        }
+        break;
+      }
+    }
+  }
+
   // We used to also obtain the corresponding NSScreen here, but this causes
   // the application icon to start bouncing, which may be undesirable for
   // apps that will never open a window.
 
-  _display_width = CGDisplayPixelsWide(_display);
-  _display_height = CGDisplayPixelsHigh(_display);
+  // Although the name of these functions mention pixels, they actually return
+  // display points, we use the detected display zoom to transform the values
+  // into pixels.
+  _display_width = CGDisplayPixelsWide(_display) * _detected_display_zoom;
+  _display_height = CGDisplayPixelsHigh(_display) * _detected_display_zoom;
   load_display_information();
 
   if (cocoadisplay_cat.is_debug()) {
     cocoadisplay_cat.debug()
       << "Creating CocoaGraphicsPipe for display ID " << _display << "\n";
   }
+
+  // It takes a while to fire up the display link, so let's fire it up now if
+  // we expect to need VSync.
+  if (sync_video) {
+    uint32_t counter;
+    init_vsync(counter);
+  }
 }
 
 /**
@@ -64,19 +95,44 @@ load_display_information() {
   // _display_information->_device_id = CGDisplaySerialNumber(_display);
 
   // Display modes
+  CFDictionaryRef options = NULL;
+  const CFStringRef dictkeys[] = {kCGDisplayShowDuplicateLowResolutionModes};
+  const CFBooleanRef dictvalues[] = {kCFBooleanTrue};
+  options = CFDictionaryCreate(NULL,
+                               (const void **)dictkeys,
+                               (const void **)dictvalues,
+                               1,
+                               &kCFCopyStringDictionaryKeyCallBacks,
+                               &kCFTypeDictionaryValueCallBacks);
   size_t num_modes = 0;
-  CFArrayRef modes = CGDisplayCopyAllDisplayModes(_display, NULL);
+  int32_t current_mode_id = -1;
+  CFArrayRef modes = CGDisplayCopyAllDisplayModes(_display, options);
   if (modes != NULL) {
     num_modes = CFArrayGetCount(modes);
     _display_information->_total_display_modes = num_modes;
     _display_information->_display_mode_array = new DisplayMode[num_modes];
+
+    // Get information about the current mode.
+    CGDisplayModeRef mode = CGDisplayCopyDisplayMode(_display);
+    if (mode) {
+      current_mode_id = CGDisplayModeGetIODisplayModeID(mode);
+      CGDisplayModeRelease(mode);
+    }
+  }
+  if (options != NULL) {
+    CFRelease(options);
   }
 
   for (size_t i = 0; i < num_modes; ++i) {
     CGDisplayModeRef mode = (CGDisplayModeRef) CFArrayGetValueAtIndex(modes, i);
 
-    _display_information->_display_mode_array[i].width = CGDisplayModeGetWidth(mode);
-    _display_information->_display_mode_array[i].height = CGDisplayModeGetHeight(mode);
+    if (dpi_aware) {
+      _display_information->_display_mode_array[i].width = CGDisplayModeGetPixelWidth(mode);
+      _display_information->_display_mode_array[i].height = CGDisplayModeGetPixelHeight(mode);
+    } else {
+      _display_information->_display_mode_array[i].width = CGDisplayModeGetWidth(mode);
+      _display_information->_display_mode_array[i].height = CGDisplayModeGetHeight(mode);
+    }
     _display_information->_display_mode_array[i].refresh_rate = CGDisplayModeGetRefreshRate(mode);
     _display_information->_display_mode_array[i].fullscreen_only = false;
 
@@ -105,6 +161,14 @@ load_display_information() {
       // pixel can be deduced from the string length.  Nifty!
       _display_information->_display_mode_array[i].bits_per_pixel = CFStringGetLength(encoding);
     }
+
+    if (current_mode_id >= 0 && current_mode_id == CGDisplayModeGetIODisplayModeID(mode)) {
+      _display_information->_current_display_mode_index = i;
+
+      // Stop checking
+      current_mode_id = -1;
+    }
+
     CFRelease(encoding);
   }
   if (modes != nullptr) {
@@ -130,6 +194,14 @@ load_display_information() {
  */
 CocoaGraphicsPipe::
 ~CocoaGraphicsPipe() {
+  if (_display_link != nil) {
+    CVDisplayLinkRelease(_display_link);
+    _display_link = nil;
+
+    // Unblock any threads that may be waiting on the VSync counter.
+    __atomic_fetch_add(&_vsync_counter, 1u, __ATOMIC_SEQ_CST);
+    patomic_notify_all(&_vsync_counter);
+  }
 }
 
 /**
@@ -143,3 +215,112 @@ CocoaGraphicsPipe::get_preferred_window_thread() const {
   // only be called from the main thread!
   return PWT_app;
 }
+
+/**
+ * Ensures a CVDisplayLink is created, which tells us when the display will
+ * want a frame, to avoid tearing (vertical blanking interval).
+ * Initializes the counter with the value that can be passed to wait_vsync
+ * to wait for the next interval.
+ */
+bool CocoaGraphicsPipe::
+init_vsync(uint32_t &counter) {
+  if (_display_link != nil) {
+    // Already set up.
+    __atomic_load(&_vsync_counter, &counter, __ATOMIC_SEQ_CST);
+    return true;
+  }
+
+  counter = 0;
+  _vsync_counter = 0;
+
+  CVDisplayLinkRef display_link;
+  CVReturn result = CVDisplayLinkCreateWithActiveCGDisplays(&display_link);
+  if (result != kCVReturnSuccess) {
+    cocoadisplay_cat.error() << "Failed to create CVDisplayLink.\n";
+    display_link = nil;
+    return false;
+  }
+
+  result = CVDisplayLinkSetCurrentCGDisplay(display_link, _display);
+  if (result != kCVReturnSuccess) {
+    cocoadisplay_cat.error() << "Failed to set CVDisplayLink's current display.\n";
+    CVDisplayLinkRelease(display_link);
+    display_link = nil;
+    return false;
+  }
+
+  result = CVDisplayLinkSetOutputCallback(display_link, &display_link_cb, this);
+  if (result != kCVReturnSuccess) {
+    cocoadisplay_cat.error() << "Failed to set CVDisplayLink output callback.\n";
+    CVDisplayLinkRelease(display_link);
+    display_link = nil;
+    return false;
+  }
+
+  result = CVDisplayLinkStart(display_link);
+  if (result != kCVReturnSuccess) {
+    cocoadisplay_cat.error() << "Failed to start the CVDisplayLink.\n";
+    CVDisplayLinkRelease(display_link);
+    display_link = nil;
+    return false;
+  }
+
+  _display_link = display_link;
+  return true;
+}
+
+/**
+ * The first time this method is called in a frame, waits for the vertical
+ * blanking interval.  If init_vsync has not first been called, does nothing.
+ *
+ * The given counter will be updated with the vblank counter.  If adaptive is
+ * true and the value differs from the current, no wait will occur.
+ */
+void CocoaGraphicsPipe::
+wait_vsync(uint32_t &counter, bool adaptive) {
+  if (_display_link == nil) {
+    return;
+  }
+
+  // Use direct atomic operations since we need this to be thread-safe even
+  // when compiling without thread support.
+  uint32_t current_count = __atomic_load_n(&_vsync_counter, __ATOMIC_SEQ_CST);
+  uint32_t diff = current_count - counter;
+  if (diff > 0) {
+    if (cocoadisplay_cat.is_spam()) {
+      cocoadisplay_cat.spam()
+        << "Missed vertical blanking interval by " << diff << " frames.\n";
+    }
+    if (adaptive) {
+      counter = current_count;
+      return;
+    }
+  }
+
+  // We only wait for the first window that gets flipped in a single frame,
+  // otherwise we end up halving our FPS when we have multiple windows!
+  int cur_frame = ClockObject::get_global_clock()->get_frame_count();
+  if (_last_wait_frame.exchange(cur_frame) == cur_frame) {
+    counter = current_count;
+    return;
+  }
+
+  patomic_wait(&_vsync_counter, current_count);
+  __atomic_load(&_vsync_counter, &counter, __ATOMIC_SEQ_CST);
+}
+
+/**
+ * Called whenever a display wants a frame.  The context argument contains the
+ * applicable CocoaGraphicsPipe.
+ */
+CVReturn CocoaGraphicsPipe::
+display_link_cb(CVDisplayLinkRef link, const CVTimeStamp *now,
+                const CVTimeStamp *output_time, CVOptionFlags flags_in,
+                CVOptionFlags *flags_out, void *context) {
+
+  CocoaGraphicsPipe *pipe = (CocoaGraphicsPipe *)context;
+  __atomic_fetch_add(&pipe->_vsync_counter, 1u, __ATOMIC_SEQ_CST);
+  patomic_notify_all(&pipe->_vsync_counter);
+
+  return kCVReturnSuccess;
+}

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

@@ -63,6 +63,7 @@ public:
   void handle_minimize_event(bool minimized);
   void handle_maximize_event(bool maximized);
   void handle_foreground_event(bool foreground);
+  void handle_backing_change_event();
   bool handle_close_request();
   void handle_close_event();
   void handle_key_event(NSEvent *event);

+ 209 - 105
panda/src/cocoadisplay/cocoaGraphicsWindow.mm

@@ -132,13 +132,20 @@ move_pointer(int device, int x, int y) {
     return true;
   }
 
+  // Mouse position is expressed in screen points and not pixels, but in Panda3D
+  // we are using pixel coordinates.
+  // Instead of using convertPointFromBacking and have complex logic to cope with
+  // the change of coordinate system, we cheat and directly use the contents scale
+  // of the view layer to convert pixel coordinates into screen point coordinates.
+  CGFloat contents_scale = _view.layer.contentsScale;
   if (device == 0) {
     CGPoint point;
     if (_properties.get_fullscreen()) {
-      point = CGPointMake(x, y);
+      point = CGPointMake(float(x) / contents_scale,
+                          float(y) / contents_scale);
     } else {
-      point = CGPointMake(x + _properties.get_x_origin(),
-                          y + _properties.get_y_origin());
+      point = CGPointMake((float(x) + _properties.get_x_origin()) / contents_scale,
+                          (float(y) + _properties.get_y_origin()) / contents_scale);
     }
 
     if (CGWarpMouseCursorPosition(point) == kCGErrorSuccess) {
@@ -302,35 +309,89 @@ open_window() {
     }
   }
 
+  // Configure the origin and the size of the window.
+  // On macOS, screen coordinates are expressed in "points" which are independant
+  // of the pixel density of the screen. Panda3D however, expresses the size and
+  // origin of a window in pixels.
+  // So, when opening a window, we need to convert the origin and size from pixel
+  // units into point units. However, this conversion depends on the pixel density
+  // of the screen, the backing scale factor.
+  // As the origin and size of a window depends on the size of the screen of the
+  // parent view, their size must be converted first from points to pixels.
+  // If a window (or a view) is not configured to support high-dpi screen, macOS
+  // will upscale the window (or view) when displayed on a high-dpi screen.
+  // Therefore its backing scale factor will always be 1.0
+  // In a Panda3D application, windows are always configured as high resolution
+  // capable, but the view is only configured as high resolution if the dpi-aware
+  // configuration flag is set.
+  // If the app is not dpi-aware, we must upscale its size and origin from points
+  // into pixels as the window is always high resolution capable.
+
   // Center the window if coordinates were set to -1 or -2 TODO: perhaps in
   // future, in the case of -1, it should use the origin used in a previous
   // run of Panda
+
+  // Size of the requested window
+  NSSize size = NSMakeSize(_properties.get_x_size(), _properties.get_y_size());
   NSRect container;
+  CGFloat backing_scale_factor = screen.backingScaleFactor;
   if (parent_nsview != NULL) {
-    container = [parent_nsview bounds];
+    // Convert parent view bounds into pixel units.
+    container = [parent_nsview convertRectToBacking:[parent_nsview bounds]];
+    // If the app is not dpi-aware, we must convert its size from points into
+    // pixels as the window is always high resolution capable
+    if (!dpi_aware) {
+      size = [parent_nsview convertSizeToBacking:size];
+    }
   } else {
     container = [screen frame];
     container.origin = NSMakePoint(0, 0);
+    container = [screen convertRectToBacking:container];
+    if (!dpi_aware) {
+      // Weirdly NSScreen does not respond to convertSizeToBacking, so we have to
+      // create a dummy rect just for converting the window size.
+      NSRect rect;
+      rect.origin = NSMakePoint(0, 0);
+      rect.size = size;
+      rect = [screen convertRectToBacking:rect];
+      size = rect.size;
+    }
   }
   int x = _properties.get_x_origin();
   int y = _properties.get_y_origin();
 
+  // As we are converting a single value and the view is not created yet, it's
+  // easier to simply use the backing  scale factor and don't bother with
+  // coordinate system transformations.
   if (x < 0) {
-    x = floor(container.size.width / 2 - _properties.get_x_size() / 2);
+    x = floor(container.size.width / 2 - size.width / 2);
+  } else if (!dpi_aware) {
+    x *= backing_scale_factor;
   }
   if (y < 0) {
-    y = floor(container.size.height / 2 - _properties.get_y_size() / 2);
+    y = floor(container.size.height / 2 - size.height / 2);
+  } else if (!dpi_aware) {
+    y *= backing_scale_factor;
+  }
+  if (dpi_aware) {
+    _properties.set_origin(x, y);
+  } else {
+    _properties.set_origin(x / backing_scale_factor, y / backing_scale_factor);
   }
-  _properties.set_origin(x, y);
 
   if (_parent_window_handle == (WindowHandle *)NULL) {
     // Content rectangle
     NSRect rect;
     if (_properties.get_fullscreen()) {
-      rect = container;
+      rect = [screen convertRectFromBacking:container];
     } else {
-      rect = NSMakeRect(x, container.size.height - _properties.get_y_size() - y,
-                        _properties.get_x_size(), _properties.get_y_size());
+      rect = NSMakeRect(x, container.size.height - size.height - y,
+                        size.width, size.height);
+      if (parent_nsview != NULL) {
+        rect = [parent_nsview convertRectFromBacking:rect];
+      } else {
+        rect = [screen convertRectFromBacking:rect];
+      }
     }
 
     // Configure the window decorations
@@ -388,8 +449,10 @@ open_window() {
     _parent_window_handle->attach_child(_window_handle);
   }
 
-  // Always disable application HiDPI support, Cocoa will do the eventual upscaling for us.
-  [_view setWantsBestResolutionOpenGLSurface:NO];
+  // Configure the view to be high resolution capable using the dpi-aware
+  // configuration flag. If dpi-aware is false, macOS will upscale the view
+  // for us.
+  [_view setWantsBestResolutionOpenGLSurface:dpi_aware];
   if (_properties.has_icon_filename()) {
     NSImage *image = load_image(_properties.get_icon_filename());
     if (image != nil) {
@@ -596,22 +659,6 @@ set_properties_now(WindowProperties &properties) {
           }
 
           if (switched) {
-            if (_window != nil) {
-              // For some reason, setting the style mask makes it give up its
-              // first-responder status.  And for some reason, we need to first
-              // restore the window to normal level before we switch fullscreen,
-              // otherwise we may get a black bar if we're currently on Z_top.
-              if (_properties.get_z_order() != WindowProperties::Z_normal) {
-                [_window setLevel: NSNormalWindowLevel];
-              }
-              if ([_window respondsToSelector:@selector(setStyleMask:)]) {
-                [_window setStyleMask:NSBorderlessWindowMask];
-              }
-              [_window makeFirstResponder:_view];
-              [_window setLevel:CGShieldingWindowLevel()];
-              [_window makeKeyAndOrderFront:nil];
-            }
-
             // We've already set the size property this way; clear it.
             properties.clear_size();
             _properties.set_size(width, height);
@@ -684,6 +731,9 @@ set_properties_now(WindowProperties &properties) {
                                 NSMiniaturizableWindowMask | NSResizableWindowMask ];
         }
         [_window makeFirstResponder:_view];
+        // Resize event fired by makeFirstResponder has an invalid backing scale factor
+        // The actual size must be reset afterward
+        handle_resize_event();
       }
     }
 
@@ -705,6 +755,9 @@ set_properties_now(WindowProperties &properties) {
                                NSMiniaturizableWindowMask | NSResizableWindowMask ];
       }
       [_window makeFirstResponder:_view];
+      // Resize event fired by makeFirstResponder has an invalid backing scale factor
+      // The actual size must be reset afterward
+      handle_resize_event();
     }
 
     properties.clear_undecorated();
@@ -715,10 +768,13 @@ set_properties_now(WindowProperties &properties) {
     int height = properties.get_y_size();
 
     if (!_properties.get_fullscreen()) {
+      // We use the view, not the window, to convert the frame size, expressed
+      // in pixels, into points as the "dpi awareness" is managed by the view.
+      NSSize size = [_view convertSizeFromBacking:NSMakeSize(width, height)];
       if (_window != nil) {
-        [_window setContentSize:NSMakeSize(width, height)];
+        [_window setContentSize:size];
       }
-      [_view setFrameSize:NSMakeSize(width, height)];
+      [_view setFrameSize:size];
 
       if (cocoadisplay_cat.is_debug()) {
         cocoadisplay_cat.debug()
@@ -768,12 +824,14 @@ set_properties_now(WindowProperties &properties) {
     // Get the frame for the screen
     NSRect frame;
     NSRect container;
+    // Note again that we are using the view to convert the frame and container
+    // size from points into pixels.
     if (_window != nil) {
       NSRect window_frame = [_window frame];
-      frame = [_window contentRectForFrameRect:window_frame];
+      frame = [_view convertRectToBacking:[_window contentRectForFrameRect:window_frame]];
       NSScreen *screen = [_window screen];
       nassertv(screen != nil);
-      container = [screen frame];
+      container = [_view convertRectToBacking:[screen frame]];
 
       // Prevent the centering from overlapping the Dock
       if (y < 0) {
@@ -783,8 +841,8 @@ set_properties_now(WindowProperties &properties) {
         }
       }
     } else {
-      frame = [_view frame];
-      container = [[_view superview] frame];
+      frame = [_view convertRectToBacking:[_view frame]];
+      container = [[_view superview] convertRectToBacking:[[_view superview] frame]];
     }
 
     if (x < 0) {
@@ -795,22 +853,22 @@ set_properties_now(WindowProperties &properties) {
     }
     _properties.set_origin(x, y);
 
-    if (!_properties.get_fullscreen()) {
-      // Remember, Mac OS X coordinates are flipped in the vertical axis.
-      frame.origin.x = x;
-      frame.origin.y = container.size.height - y - frame.size.height;
+    frame.origin.x = x;
+    // Y coordinate in backing store is not flipped, but origin is still at the bottom left
+    frame.origin.y = y - container.size.height;
 
-      if (cocoadisplay_cat.is_debug()) {
-        cocoadisplay_cat.debug()
-          << "Setting window content origin to "
-          << frame.origin.x << ", " << frame.origin.y << "\n";
-      }
+    if (cocoadisplay_cat.is_debug()) {
+      cocoadisplay_cat.debug()
+        << "Setting window content origin to "
+        << frame.origin.x << ", " << frame.origin.y << "\n";
+    }
 
-      if (_window != nil) {
-        [_window setFrame:[_window frameRectForContentRect:frame] display:NO];
-      } else {
-        [_view setFrame:frame];
-      }
+    if (_window != nil) {
+      frame = [_view convertRectFromBacking:frame];
+      [_window setFrame:[_window frameRectForContentRect:frame] display:NO];
+    } else {
+      frame = [_view convertRectFromBacking:frame];
+      [_view setFrame:frame];
     }
     properties.clear_origin();
   }
@@ -957,24 +1015,20 @@ unbind_context() {
 CFMutableArrayRef CocoaGraphicsWindow::
 find_display_modes(int width, int height) {
   CFDictionaryRef options = NULL;
-  // On macOS 10.15+ (Catalina), we want to select the display mode with the
-  // samescaling factor as the current view to avoid cropping or scaling issues.
-  // This is a workaround until HiDPI display or scaling factor is properly
-  // handled. CGDisplayCopyAllDisplayModes() does not return upscaled display
-  // mode unless explicitly asked with kCGDisplayShowDuplicateLowResolutionModes
+  // We want to select the display mode with the same scaling factor as the
+  // current view to avoid cropping or scaling issues.
+  // CGDisplayCopyAllDisplayModes() does not return upscaled display modes
+  // nor the current mode, unless explicitly asked with
+  // kCGDisplayShowDuplicateLowResolutionModes
   // (which is undocumented...).
-  bool macos_10_15_or_higher = false;
-  if (@available(macOS 10.15, *)) {
-    const CFStringRef dictkeys[] = {kCGDisplayShowDuplicateLowResolutionModes};
-    const CFBooleanRef dictvalues[] = {kCFBooleanTrue};
-    options = CFDictionaryCreate(NULL,
-                                 (const void **)dictkeys,
-                                 (const void **)dictvalues,
-                                 1,
-                                 &kCFCopyStringDictionaryKeyCallBacks,
-                                 &kCFTypeDictionaryValueCallBacks);
-    macos_10_15_or_higher = true;
-  }
+  const CFStringRef dictkeys[] = {kCGDisplayShowDuplicateLowResolutionModes};
+  const CFBooleanRef dictvalues[] = {kCFBooleanTrue};
+  options = CFDictionaryCreate(NULL,
+                               (const void **)dictkeys,
+                               (const void **)dictvalues,
+                               1,
+                               &kCFCopyStringDictionaryKeyCallBacks,
+                               &kCFTypeDictionaryValueCallBacks);
   CFMutableArrayRef valid_modes;
   valid_modes = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
 
@@ -985,29 +1039,39 @@ find_display_modes(int width, int height) {
 
   size_t num_modes = CFArrayGetCount(modes);
   CGDisplayModeRef mode;
-
-  // Get the current refresh rate and pixel encoding.
-  CFStringRef current_pixel_encoding;
-  double refresh_rate;
   mode = CGDisplayCopyDisplayMode(_display);
 
+  // Calculate requested display size and pixel size
+  CGSize display_size;
+  CGSize pixel_size;
+  if (dpi_aware) {
+    pixel_size = NSMakeSize(width, height);
+    display_size = [_view convertSizeFromBacking:pixel_size];
+  } else {
+    display_size = NSMakeSize(width, height);
+    // Calculate the pixel width and height of the fullscreen mode we want using
+    // the current display mode dimensions and pixel dimensions.
+    size_t pixel_width = (size_t(width) * CGDisplayModeGetPixelWidth(mode)) / CGDisplayModeGetWidth(mode);
+    size_t pixel_height = (size_t(height) * CGDisplayModeGetPixelHeight(mode)) / CGDisplayModeGetHeight(mode);
+    pixel_size = NSMakeSize(pixel_width, pixel_height);
+  }
+
   // First check if the current mode is adequate.
-  // This test not done for macOS 10.15 and above as the mode resolution is
-  // not enough to identify a mode.
-  if (!macos_10_15_or_higher &&
-      CGDisplayModeGetWidth(mode) == width &&
-      CGDisplayModeGetHeight(mode) == height) {
+  if (CGDisplayModeGetWidth(mode) == display_size.width &&
+      CGDisplayModeGetHeight(mode) == display_size.height &&
+      CGDisplayModeGetPixelWidth(mode) == pixel_size.width &&
+      CGDisplayModeGetPixelHeight(mode) == pixel_size.height) {
     CFArrayAppendValue(valid_modes, mode);
     CGDisplayModeRelease(mode);
     return valid_modes;
   }
 
+  // Get the current refresh rate and pixel encoding.
+  CFStringRef current_pixel_encoding;
+  double refresh_rate;
+
   current_pixel_encoding = CGDisplayModeCopyPixelEncoding(mode);
   refresh_rate = CGDisplayModeGetRefreshRate(mode);
-  // Calculate the pixel width and height of the fullscreen mode we want using
-  // the currentdisplay mode dimensions and pixel dimensions.
-  size_t expected_pixel_width = (size_t(width) * CGDisplayModeGetPixelWidth(mode)) / CGDisplayModeGetWidth(mode);
-  size_t expected_pixel_height = (size_t(height) * CGDisplayModeGetPixelHeight(mode)) / CGDisplayModeGetHeight(mode);
   CGDisplayModeRelease(mode);
 
   for (size_t i = 0; i < num_modes; ++i) {
@@ -1015,17 +1079,15 @@ find_display_modes(int width, int height) {
 
     CFStringRef pixel_encoding = CGDisplayModeCopyPixelEncoding(mode);
 
-    // As explained above, we want to select the fullscreen display mode using
-    // the same scaling factor, but only for MacOS 10.15+ To do this we check
-    // the mode width and height but also actual pixel widh and height.
-    if (CGDisplayModeGetWidth(mode) == width &&
-        CGDisplayModeGetHeight(mode) == height &&
+    // We select the fullscreen display mode using he same scaling factor
+    // To do this we check the mode width and height but also actual pixel widh
+    // and height.
+    if (CGDisplayModeGetWidth(mode) == display_size.width &&
+        CGDisplayModeGetHeight(mode) == display_size.height &&
         (int)(CGDisplayModeGetRefreshRate(mode) + 0.5) == (int)(refresh_rate + 0.5) &&
-        (!macos_10_15_or_higher ||
-        (CGDisplayModeGetPixelWidth(mode) == expected_pixel_width &&
-         CGDisplayModeGetPixelHeight(mode) == expected_pixel_height)) &&
+        CGDisplayModeGetPixelWidth(mode) == pixel_size.width &&
+        CGDisplayModeGetPixelHeight(mode) == pixel_size.height &&
         CFStringCompare(pixel_encoding, current_pixel_encoding, 0) == kCFCompareEqualTo) {
-
       if (CGDisplayModeGetRefreshRate(mode) == refresh_rate) {
         // Exact match for refresh rate, prioritize this.
         CFArrayInsertValueAtIndex(valid_modes, 0, mode);
@@ -1103,14 +1165,25 @@ do_switch_fullscreen(CGDisplayModeRef mode) {
 
     NSRect frame = [[[_view window] screen] frame];
     if (cocoadisplay_cat.is_debug()) {
-      NSString *str = NSStringFromRect(frame);
+      NSString *str = NSStringFromSize([_view convertSizeToBacking:frame.size]);
       cocoadisplay_cat.debug()
-        << "Switched to fullscreen, screen rect is now " << [str UTF8String] << "\n";
+        << "Switched to fullscreen, screen size is now " << [str UTF8String] << "\n";
     }
 
     if (_window != nil) {
+      // For some reason, setting the style mask makes it give up its
+      // first-responder status.
+      if ([_window respondsToSelector:@selector(setStyleMask:)]) {
+        [_window setStyleMask:NSBorderlessWindowMask];
+      }
+      [_window makeFirstResponder:_view];
+      [_window setLevel:CGShieldingWindowLevel()];
+      [_window makeKeyAndOrderFront:nil];
+
+      // Window and view frame must be updated *after* the window reconfiguration
+      // or the size is not set properly !
       [_window setFrame:frame display:YES];
-      [_view setFrame:NSMakeRect(0, 0, frame.size.width, frame.size.height)];
+      [_view setFrame:frame];
       [_window update];
     }
   }
@@ -1252,19 +1325,25 @@ load_cursor(const Filename &filename) {
  */
 void CocoaGraphicsWindow::
 handle_move_event() {
-  // Remember, Mac OS X uses flipped coordinates
   NSRect frame;
+  NSRect container;
   int x, y;
+  // Again, we are using the view to convert the frame and container size from
+  // points to pixels.
   if (_window == nil) {
-    frame = [_view frame];
-    x = frame.origin.x;
-    y = [[_view superview] bounds].size.height - frame.origin.y - frame.size.height;
+    frame = [_view convertRectToBacking:[_view frame]];
+    container = [_view convertRectToBacking:[[_view superview] frame]];
   } else {
-    frame = [_window contentRectForFrameRect:[_window frame]];
-    x = frame.origin.x;
-    y = [[_window screen] frame].size.height - frame.origin.y - frame.size.height;
+    frame = [_view convertRectToBacking:[_window contentRectForFrameRect:[_window frame]]];
+    NSScreen *screen = [_window screen];
+    nassertv(screen != nil);
+    container = [_view convertRectToBacking:[screen frame]];
   }
 
+  // Y coordinate in backing store is not flipped, but origin is still at the bottom left
+  x = frame.origin.x;
+  y = container.size.height + frame.origin.y;
+
   if (x != _properties.get_x_origin() ||
       y != _properties.get_y_origin()) {
 
@@ -1290,7 +1369,7 @@ handle_resize_event() {
     [_view setFrameSize:contentRect.size];
   }
 
-  NSRect frame = [_view convertRect:[_view bounds] toView:nil];
+  NSRect frame = [_view convertRectToBacking:[_view bounds]];
 
   WindowProperties properties;
   bool changed = false;
@@ -1403,6 +1482,22 @@ handle_foreground_event(bool foreground) {
   }
 }
 
+
+/**
+ * Called by the window delegate when the properties of backing store of the
+ * window have changed.
+ */
+void CocoaGraphicsWindow::
+handle_backing_change_event() {
+  if (cocoadisplay_cat.is_debug()) {
+    cocoadisplay_cat.debug() << "Backing store properties have changed\n";
+  }
+  // Trigger a resize event to update the window size in case the backing scale
+  // factor did change.
+  handle_resize_event();
+}
+
+
 /**
  * Called by the window delegate when the user requests to close the window.
  * This may not always be called, which is why there is also a
@@ -1693,6 +1788,13 @@ void CocoaGraphicsWindow::
 handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) {
   double nx, ny;
 
+  // Mouse position is received in screen points and not pixels, but in Panda3D
+  // we want to have the coordinates expressed in pixels.
+  // Instead of using convertPointFrom/toBackingStore and have complex logic to
+  // cope with the change of coordinate system, we cheat and directly use the
+  // contents scale of the view layer to convert screen point into pixels and
+  // vice-versa.
+  CGFloat contents_scale = _view.layer.contentsScale;
   if (absolute) {
     if (cocoadisplay_cat.is_spam()) {
       if (in_window != _input->get_pointer().get_in_window()) {
@@ -1704,14 +1806,14 @@ handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) {
       }
     }
 
-    nx = x;
-    ny = y;
+    nx = x * contents_scale;
+    ny = y * contents_scale;
 
   } else {
     // We received deltas, so add it to the current mouse position.
     PointerData md = _input->get_pointer();
-    nx = md.get_x() + x;
-    ny = md.get_y() + y;
+    nx = md.get_x() + x * contents_scale;
+    ny = md.get_y() + y * contents_scale;
   }
 
   if (_properties.get_mouse_mode() == WindowProperties::M_confined
@@ -1721,11 +1823,13 @@ handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) {
     nx = std::max(0., std::min((double) get_x_size() - 1, nx));
     ny = std::max(0., std::min((double) get_y_size() - 1, ny));
 
+    // Convert back mouse position to screen space using point units
     if (_properties.get_fullscreen()) {
-      point = CGPointMake(nx, ny);
+      point = CGPointMake(nx / contents_scale,
+                          ny / contents_scale);
     } else {
-      point = CGPointMake(nx + _properties.get_x_origin(),
-                          ny + _properties.get_y_origin());
+      point = CGPointMake((nx + _properties.get_x_origin()) / contents_scale,
+                          (ny + _properties.get_y_origin()) / contents_scale);
     }
 
     if (CGWarpMouseCursorPosition(point) == kCGErrorSuccess) {

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

@@ -29,6 +29,7 @@ class CocoaGraphicsWindow;
 - (void)windowDidDeminiaturize:(NSNotification *)notification;
 - (void)windowDidBecomeKey:(NSNotification *)notification;
 - (void)windowDidResignKey:(NSNotification *)notification;
+- (void)windowDidChangeBackingProperties:(NSNotification *)notification;
 - (BOOL)windowShouldClose:(id)sender;
 - (void)windowWillClose:(id)sender;
 

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

@@ -51,6 +51,10 @@
   _graphicsWindow->handle_foreground_event(false);
 }
 
+- (void) windowDidChangeBackingProperties:(NSNotification *)notification {
+  _graphicsWindow->handle_backing_change_event();
+}
+
 - (BOOL) windowShouldClose:(id)sender {
   if (cocoadisplay_cat.is_debug()) {
     cocoadisplay_cat.debug()

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

@@ -21,6 +21,7 @@
 NotifyCategoryDecl(cocoadisplay, EXPCL_PANDA_COCOADISPLAY, EXPTP_PANDA_COCOADISPLAY);
 
 extern ConfigVariableBool cocoa_invert_wheel_x;
+extern ConfigVariableBool dpi_aware;
 
 extern EXPCL_PANDA_COCOADISPLAY void init_libcocoadisplay();
 

+ 5 - 0
panda/src/cocoadisplay/config_cocoadisplay.mm

@@ -32,6 +32,11 @@ ConfigVariableBool cocoa_invert_wheel_x
 ("cocoa-invert-wheel-x", false,
  PRC_DESC("Set this to true to swap the wheel_left and wheel_right mouse "
           "button events, to restore to the pre-1.10.12 behavior."));
+ConfigVariableBool dpi_aware
+("dpi-aware", false,
+ PRC_DESC("The default behavior on macOS is for Panda3D to use upscaling on"
+          "high DPI screen. Set this to true to let the application use the"
+          "actual pixel density of the screen."));
 
 /**
  * Initializes the library.  This must be called at least once before any of

+ 0 - 7
panda/src/cocoagldisplay/cocoaGLGraphicsStateGuardian.h

@@ -20,7 +20,6 @@
 
 #import <AppKit/NSOpenGL.h>
 #import <OpenGL/OpenGL.h>
-#import <CoreVideo/CoreVideo.h>
 
 /**
  * A tiny specialization on GLGraphicsStateGuardian to add some Cocoa-specific
@@ -39,7 +38,6 @@ public:
                                CocoaGLGraphicsStateGuardian *share_with);
 
   virtual ~CocoaGLGraphicsStateGuardian();
-  bool setup_vsync();
 
   INLINE void lock_context();
   INLINE void unlock_context();
@@ -49,11 +47,6 @@ public:
   NSOpenGLPixelFormat *_format = nullptr;
   FrameBufferProperties _fbprops;
 
-  CVDisplayLinkRef _display_link = nullptr;
-  TrueMutexImpl _swap_lock;
-  TrueConditionVarImpl _swap_condition;
-  AtomicAdjust::Integer _last_wait_frame = 0;
-
 protected:
   virtual void query_gl_version();
   virtual void *do_get_extension_func(const char *name);

+ 1 - 62
panda/src/cocoagldisplay/cocoaGLGraphicsStateGuardian.mm

@@ -28,21 +28,6 @@
 #define NSAppKitVersionNumber10_7 1138
 #endif
 
-/**
- * Called whenever a display wants a frame.  The context argument contains the
- * applicable CocoaGLGraphicsStateGuardian.
- */
-static CVReturn
-display_link_cb(CVDisplayLinkRef link, const CVTimeStamp *now,
-                const CVTimeStamp* output_time, CVOptionFlags flags_in,
-                CVOptionFlags *flags_out, void *context) {
-  CocoaGLGraphicsStateGuardian *gsg = (CocoaGLGraphicsStateGuardian *)context;
-  gsg->_swap_lock.lock();
-  gsg->_swap_condition.notify();
-  gsg->_swap_lock.unlock();
-  return kCVReturnSuccess;
-}
-
 TypeHandle CocoaGLGraphicsStateGuardian::_type_handle;
 
 /**
@@ -51,8 +36,7 @@ TypeHandle CocoaGLGraphicsStateGuardian::_type_handle;
 CocoaGLGraphicsStateGuardian::
 CocoaGLGraphicsStateGuardian(GraphicsEngine *engine, GraphicsPipe *pipe,
                            CocoaGLGraphicsStateGuardian *share_with) :
-  GLGraphicsStateGuardian(engine, pipe),
-  _swap_condition(_swap_lock)
+  GLGraphicsStateGuardian(engine, pipe)
 {
   _share_context = nil;
   _context = nil;
@@ -71,57 +55,12 @@ CocoaGLGraphicsStateGuardian::
   if (_format != nil) {
     [_format release];
   }
-  if (_display_link != nil) {
-    CVDisplayLinkRelease(_display_link);
-    _display_link = nil;
-    _swap_lock.lock();
-    _swap_condition.notify();
-    _swap_lock.unlock();
-  }
   if (_context != nil) {
     [_context clearDrawable];
     [_context release];
   }
 }
 
-/**
- * Creates a CVDisplayLink, which tells us when the display the window is on
- * will want a frame.
- */
-bool CocoaGLGraphicsStateGuardian::
-setup_vsync() {
-  if (_display_link != nil) {
-    // Already set up.
-    return true;
-  }
-
-  CVReturn result = CVDisplayLinkCreateWithActiveCGDisplays(&_display_link);
-  if (result != kCVReturnSuccess) {
-    cocoadisplay_cat.error() << "Failed to create CVDisplayLink.\n";
-    return false;
-  }
-
-  result = CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(_display_link, (CGLContextObj)[_context CGLContextObj], (CGLPixelFormatObj)[_format CGLPixelFormatObj]);
-  if (result != kCVReturnSuccess) {
-    cocoadisplay_cat.error() << "Failed to set CVDisplayLink's current display.\n";
-    return false;
-  }
-
-  result = CVDisplayLinkSetOutputCallback(_display_link, &display_link_cb, this);
-  if (result != kCVReturnSuccess) {
-    cocoadisplay_cat.error() << "Failed to set CVDisplayLink output callback.\n";
-    return false;
-  }
-
-  result = CVDisplayLinkStart(_display_link);
-  if (result != kCVReturnSuccess) {
-    cocoadisplay_cat.error() << "Failed to start the CVDisplayLink.\n";
-    return false;
-  }
-
-  return true;
-}
-
 /**
  * Gets the FrameBufferProperties to match the indicated config.
  */

+ 1 - 0
panda/src/cocoagldisplay/cocoaGLGraphicsWindow.h

@@ -42,6 +42,7 @@ protected:
 
 private:
   bool _vsync_enabled = false;
+  uint32_t _vsync_counter = 0;
 
 public:
   static TypeHandle get_class_type() {

+ 9 - 8
panda/src/cocoagldisplay/cocoaGLGraphicsWindow.mm

@@ -159,13 +159,16 @@ end_flip() {
     CocoaGLGraphicsStateGuardian *cocoagsg;
     DCAST_INTO_V(cocoagsg, _gsg);
 
-    if (_vsync_enabled) {
-      AtomicAdjust::Integer cur_frame = ClockObject::get_global_clock()->get_frame_count();
-      if (AtomicAdjust::set(cocoagsg->_last_wait_frame, cur_frame) != cur_frame) {
-        cocoagsg->_swap_lock.lock();
-        cocoagsg->_swap_condition.wait();
-        cocoagsg->_swap_lock.unlock();
+    if (sync_video) {
+      CocoaGraphicsPipe *cocoapipe = (CocoaGraphicsPipe *)_pipe.p();
+      if (!_vsync_enabled) {
+        // If this fails, we don't keep trying.
+        cocoapipe->init_vsync(_vsync_counter);
+        _vsync_enabled = true;
       }
+      cocoapipe->wait_vsync(_vsync_counter);
+    } else {
+      _vsync_enabled = false;
     }
 
     cocoagsg->lock_context();
@@ -237,8 +240,6 @@ open_window() {
   }
   _fb_properties = cocoagsg->get_fb_properties();
 
-  _vsync_enabled = sync_video && cocoagsg->setup_vsync();
-
   return true;
 }
 

+ 17 - 9
panda/src/display/displayInformation.cxx

@@ -75,7 +75,6 @@ DisplayInformation::
 DisplayInformation::
 DisplayInformation() {
   DisplayInformation::DetectionState state;
-  int get_adapter_display_mode_state;
   int get_device_caps_state;
   int window_width;
   int window_height;
@@ -88,7 +87,6 @@ DisplayInformation() {
   uint64_t available_physical_memory;
 
   state = DisplayInformation::DS_unknown;
-  get_adapter_display_mode_state = false;
   get_device_caps_state = false;
   window_width = 0;
   window_height = 0;
@@ -101,7 +99,7 @@ DisplayInformation() {
   available_physical_memory = 0;
 
   _state = state;
-  _get_adapter_display_mode_state = get_adapter_display_mode_state;
+  _current_display_mode_index = -1;
   _get_device_caps_state = get_device_caps_state;
   _maximum_window_width = window_width;
   _maximum_window_height = window_height;
@@ -210,7 +208,17 @@ get_display_mode(int display_index) {
 }
 
 /**
- *
+ * Returns the index of the current display mode (determined at the time of
+ * application start) in the display mode array, or -1 if this could not be
+ * determined.
+ */
+int DisplayInformation::
+get_current_display_mode_index() const {
+  return _current_display_mode_index;
+}
+
+/**
+ * @deprecated use get_display_mode instead.
  */
 int DisplayInformation::
 get_display_mode_width (int display_index) {
@@ -225,7 +233,7 @@ get_display_mode_width (int display_index) {
 }
 
 /**
- *
+ * @deprecated use get_display_mode instead.
  */
 int DisplayInformation::
 get_display_mode_height (int display_index) {
@@ -240,7 +248,7 @@ get_display_mode_height (int display_index) {
 }
 
 /**
- *
+ * @deprecated use get_display_mode instead.
  */
 int DisplayInformation::
 get_display_mode_bits_per_pixel (int display_index) {
@@ -255,9 +263,9 @@ get_display_mode_bits_per_pixel (int display_index) {
 }
 
 /**
- *
+ * @deprecated use get_display_mode instead.
  */
-int DisplayInformation::
+double DisplayInformation::
 get_display_mode_refresh_rate (int display_index) {
   int value;
 
@@ -270,7 +278,7 @@ get_display_mode_refresh_rate (int display_index) {
 }
 
 /**
- *
+ * @deprecated use get_display_mode instead.
  */
 int DisplayInformation::
 get_display_mode_fullscreen_only (int display_index) {

+ 5 - 3
panda/src/display/displayInformation.h

@@ -21,7 +21,7 @@ PUBLISHED:
   int width;
   int height;
   int bits_per_pixel;
-  int refresh_rate;
+  double refresh_rate;
   int fullscreen_only;
 
   bool operator == (const DisplayMode &other) const;
@@ -56,11 +56,13 @@ PUBLISHED:
   const DisplayMode &get_display_mode(int display_index);
   MAKE_SEQ(get_display_modes, get_total_display_modes, get_display_mode);
 
+  int get_current_display_mode_index() const;
+
   // Older interface for display modes.
   int get_display_mode_width(int display_index);
   int get_display_mode_height(int display_index);
   int get_display_mode_bits_per_pixel(int display_index);
-  int get_display_mode_refresh_rate(int display_index);
+  double get_display_mode_refresh_rate(int display_index);
   int get_display_mode_fullscreen_only(int display_index);
 
   GraphicsStateGuardian::ShaderModel get_shader_model();
@@ -115,7 +117,7 @@ PUBLISHED:
 
 public:
   DetectionState _state;
-  int _get_adapter_display_mode_state;
+  int _current_display_mode_index;
   int _get_device_caps_state;
   int _maximum_window_width;
   int _maximum_window_height;

+ 31 - 10
panda/src/display/graphicsStateGuardian.cxx

@@ -64,6 +64,11 @@
 
 using std::string;
 
+static const LMatrix4 shadow_bias_mat(0.5f, 0.0f, 0.0f, 0.0f,
+                                      0.0f, 0.5f, 0.0f, 0.0f,
+                                      0.0f, 0.0f, 0.5f, 0.0f,
+                                      0.5f, 0.5f, 0.5f, 1.0f);
+
 //PStatCollector GraphicsStateGuardian::_vertex_buffer_switch_pcollector("Buffer switch:Vertex");
 //PStatCollector GraphicsStateGuardian::_index_buffer_switch_pcollector("Buffer switch:Index");
 //PStatCollector GraphicsStateGuardian::_shader_buffer_switch_pcollector("Buffer switch:Shader");
@@ -1543,9 +1548,30 @@ fetch_specified_part(Shader::ShaderMatInput part, InternalName *name,
     }
 
     const NodePath &np = _target_shader->get_shader_input_nodepath(name->get_parent());
-    nassertv(!np.is_empty());
+    const PandaNode *node = np.node();
 
-    fetch_specified_member(np, name->get_basename(), into[0]);
+    // This is the only matrix member we support from NodePath inputs.
+    if (node != nullptr && node->is_of_type(LensNode::get_class_type()) &&
+        name->get_basename() == "shadowViewMatrix") {
+      const LensNode *lnode = (const LensNode *)node;
+      const Lens *lens = lnode->get_lens();
+
+      LMatrix4 t = _inv_cs_transform->get_mat() *
+        _scene_setup->get_camera_transform()->get_mat() *
+        np.get_net_transform()->get_inverse()->get_mat() *
+        LMatrix4::convert_mat(_coordinate_system, lens->get_coordinate_system());
+
+      if (!lnode->is_of_type(PointLight::get_class_type())) {
+        t *= lens->get_projection_mat() * shadow_bias_mat;
+      }
+      *(LMatrix4f *)into = LCAST(float, t);
+    }
+    else {
+      display_cat.error()
+        << "Shader input " << *name << " requests invalid attribute "
+        << name->get_basename() << " from node " << np << "\n";
+      *(LMatrix4f *)into = LMatrix4f::ident_mat();
+    }
     return;
   }
   case Shader::SMO_vec_constant_x_attrib: {
@@ -1588,12 +1614,7 @@ fetch_specified_part(Shader::ShaderMatInput part, InternalName *name,
     }
     return;
   }
-  case Shader::SMO_apiview_to_apiclip_light_source_i: {
-    static const LMatrix4 biasmat(0.5f, 0.0f, 0.0f, 0.0f,
-                                  0.0f, 0.5f, 0.0f, 0.0f,
-                                  0.0f, 0.0f, 0.5f, 0.0f,
-                                  0.5f, 0.5f, 0.5f, 1.0f);
-
+  case Shader::SMO_apiview_to_apiclip_light_source_i: { // shadowViewMatrix
     const LightAttrib *target_light;
     _target_rs->get_attrib_def(target_light);
 
@@ -1616,14 +1637,14 @@ fetch_specified_part(Shader::ShaderMatInput part, InternalName *name,
         LMatrix4::convert_mat(_coordinate_system, lens->get_coordinate_system());
 
       if (!lnode->is_of_type(PointLight::get_class_type())) {
-        t *= lens->get_projection_mat() * biasmat;
+        t *= lens->get_projection_mat() * shadow_bias_mat;
       }
       ((LMatrix4f *)into)[i] = LCAST(float, t);
     }
 
     // Apply just the bias matrix otherwise.
     for (; i < (size_t)count; ++i) {
-      ((LMatrix4f *)into)[i] = LCAST(float, biasmat);
+      ((LMatrix4f *)into)[i] = LCAST(float, shadow_bias_mat);
     }
     return;
   }

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

@@ -1570,8 +1570,8 @@ reflect_uniform(int i, char *name_buffer, GLsizei name_buflen) {
             bind._func = Shader::SMF_compose;
             bind._part[0] = Shader::SMO_model_to_apiview;
             bind._arg[0] = nullptr;
-            bind._part[1] = Shader::SMO_apiview_to_apiclip_light_source_i;
-            bind._arg[1] = nullptr;
+            bind._part[1] = Shader::SMO_mat_constant_x_attrib;
+            bind._arg[1] = iname->get_parent()->append("shadowViewMatrix");
           } else {
             bind._part[0] = Shader::SMO_mat_constant_x_attrib;
             bind._arg[0] = InternalName::make(param_name);

+ 11 - 6
panda/src/gobj/geomVertexArrayData.cxx

@@ -215,19 +215,24 @@ is_prepared(PreparedGraphicsObjects *prepared_objects) const {
 VertexBufferContext *GeomVertexArrayData::
 prepare_now(PreparedGraphicsObjects *prepared_objects,
             GraphicsStateGuardianBase *gsg) {
-  if (_contexts == nullptr) {
+  if (_contexts != nullptr) {
+    Contexts::const_iterator ci;
+    ci = _contexts->find(prepared_objects);
+    if (ci != _contexts->end()) {
+      return (*ci).second;
+    }
+  } else {
     _contexts = new Contexts;
   }
-  Contexts::const_iterator ci;
-  ci = _contexts->find(prepared_objects);
-  if (ci != _contexts->end()) {
-    return (*ci).second;
-  }
 
   VertexBufferContext *vbc = prepared_objects->prepare_vertex_buffer_now(this, gsg);
   if (vbc != nullptr) {
     (*_contexts)[prepared_objects] = vbc;
   }
+  else if (_contexts->empty()) {
+    delete _contexts;
+    _contexts = nullptr;
+  }
   return vbc;
 }
 

+ 1 - 1
panda/src/gobj/shader.cxx

@@ -652,7 +652,6 @@ cp_add_mat_spec(ShaderMatSpec &spec) {
       case SMO_pixel_size:
       case SMO_texpad_x:
       case SMO_texpix_x:
-      case SMO_attr_material:
       case SMO_attr_color:
       case SMO_attr_colorscale:
       case SMO_satten_x:
@@ -684,6 +683,7 @@ cp_add_mat_spec(ShaderMatSpec &spec) {
         break;
 
       case SMO_identity:
+      case SMO_attr_material:
       case SMO_alight_x:
       case SMO_dlight_x:
       case SMO_plight_x:

+ 11 - 6
panda/src/gobj/shaderBuffer.cxx

@@ -76,19 +76,24 @@ is_prepared(PreparedGraphicsObjects *prepared_objects) const {
 BufferContext *ShaderBuffer::
 prepare_now(PreparedGraphicsObjects *prepared_objects,
             GraphicsStateGuardianBase *gsg) {
-  if (_contexts == nullptr) {
+  if (_contexts != nullptr) {
+    Contexts::const_iterator ci;
+    ci = _contexts->find(prepared_objects);
+    if (ci != _contexts->end()) {
+      return (*ci).second;
+    }
+  } else {
     _contexts = new Contexts;
   }
-  Contexts::const_iterator ci;
-  ci = _contexts->find(prepared_objects);
-  if (ci != _contexts->end()) {
-    return (*ci).second;
-  }
 
   BufferContext *vbc = prepared_objects->prepare_shader_buffer_now(this, gsg);
   if (vbc != nullptr) {
     (*_contexts)[prepared_objects] = vbc;
   }
+  else if (_contexts->empty()) {
+    delete _contexts;
+    _contexts = nullptr;
+  }
   return vbc;
 }
 

+ 1 - 13
panda/src/pnmimagetypes/bmp.h

@@ -92,19 +92,7 @@ BMPlenrgbtable(int classv, unsigned long bitcount)
                 pm_error(er_internal, "BMPlenrgbtable");
                 return 0;
         }
-        switch (classv)
-        {
-        case C_WIN:
-                lenrgb = 4;
-                break;
-        case C_OS2:
-                lenrgb = 3;
-                break;
-        default:
-                pm_error(er_internal, "BMPlenrgbtable");
-                return 0;
-        }
-
+        lenrgb = (classv == C_OS2) ? 3 : 4;
         return (1 << bitcount) * lenrgb;
 }
 

+ 1 - 0
panda/src/pnmimagetypes/pnmFileTypeBMP.h

@@ -55,6 +55,7 @@ public:
     unsigned long offBits;
 
     unsigned short  cBitCount;
+    unsigned short  cCompression;
     int             indexed;
     int             classv;
 

+ 80 - 34
panda/src/pnmimagetypes/pnmFileTypeBMPReader.cxx

@@ -177,6 +177,7 @@ BMPreadinfoheader(
         unsigned long  *pcx,
         unsigned long  *pcy,
         unsigned short *pcBitCount,
+        unsigned short *pcCompression,
         int            *pclassv)
 {
         unsigned long   cbFix;
@@ -185,6 +186,7 @@ BMPreadinfoheader(
         unsigned long   cx = 0;
         unsigned long   cy = 0;
         unsigned short  cBitCount = 0;
+        unsigned long   cCompression = 0;
         int             classv = 0;
 
         cbFix = GetLong(fp);
@@ -229,7 +231,9 @@ BMPreadinfoheader(
          * for the required total.
          */
         if (classv != C_OS2) {
-            for (int i = 0; i < (int)cbFix - 16; i += 4) {
+            cCompression = GetLong(fp);
+
+            for (int i = 0; i < (int)cbFix - 20; i += 4) {
                 GetLong(fp);
             }
         }
@@ -273,11 +277,13 @@ BMPreadinfoheader(
         pm_message("cy: %d", cy);
         pm_message("cPlanes: %d", cPlanes);
         pm_message("cBitCount: %d", cBitCount);
+        pm_message("cCompression: %d", cCompression);
 #endif
 
         *pcx = cx;
         *pcy = cy;
         *pcBitCount = cBitCount;
+        *pcCompression = cCompression;
         *pclassv = classv;
 
         *ppos += cbFix;
@@ -401,45 +407,84 @@ BMPreadbits(xel *array, xelval *alpha_array,
         unsigned long   cx,
         unsigned long   cy,
         unsigned short  cBitCount,
-        int             /* classv */,
+        unsigned long   cCompression,
         int             indexed,
         pixval         *R,
         pixval         *G,
         pixval         *B)
 {
-        long            y;
-
-        readto(fp, ppos, offBits);
+  long y;
 
-        if(cBitCount > 24 && cBitCount != 32)
-        {
-                pm_error("%s: cannot handle cBitCount: %d"
-                         ,ifname
-                         ,cBitCount);
-        }
+  readto(fp, ppos, offBits);
 
-        /*
-         * The picture is stored bottom line first, top line last
-         */
+  if (cBitCount > 24 && cBitCount != 32) {
+    pm_error("%s: cannot handle cBitCount: %d", ifname, cBitCount);
+  }
 
-        for (y = (long)cy - 1; y >= 0; y--)
-        {
-                int rc;
-                rc = BMPreadrow(fp, ppos, array + y*cx, alpha_array + y*cx, cx, cBitCount, indexed, R, G, B);
-                if(rc == -1)
-                {
-                        pm_error("%s: couldn't read row %d"
-                                 ,ifname
-                                 ,y);
-                }
-                if(rc%4)
-                {
-                        pm_error("%s: row had bad number of bytes: %d"
-                                 ,ifname
-                                 ,rc);
-                }
+  if (cCompression == 1) {
+    // RLE8 compression
+    xel *row = array + (cy - 1) * cx;
+    xel *p = row;
+    unsigned long nbyte = 0;
+    while (true) {
+      int first = GetByte(fp);
+      int second = GetByte(fp);
+      nbyte += 2;
+
+      if (first != 0) {
+        // Repeated index.
+        for (int i = 0; i < first; ++i) {
+          PPM_ASSIGN(*p, R[second], G[second], B[second]);
+          ++p;
         }
-
+      }
+      else if (second == 0) {
+        // End of line.
+        row -= cx;
+        p = row;
+      }
+      else if (second == 1) {
+        // End of image.
+        break;
+      }
+      else if (second == 2) {
+        // Delta.
+        int xoffset = GetByte(fp);
+        int yoffset = GetByte(fp);
+        nbyte += 2;
+        row -= cx * yoffset;
+        p += xoffset - cx * yoffset;
+      }
+      else {
+        // Absolute run.
+        for (int i = 0; i < second; ++i) {
+          int v = GetByte(fp);
+          ++nbyte;
+          PPM_ASSIGN(*p, R[v], G[v], B[v]);
+          ++p;
+        }
+        nbyte += second;
+        if (second % 2) {
+          // Pad to 16-bit boundary.
+          GetByte(fp);
+          ++nbyte;
+        }
+      }
+    }
+    *ppos += nbyte;
+  }
+  else {
+    // The picture is stored bottom line first, top line last
+    for (y = (long)cy - 1; y >= 0; y--) {
+      int rc = BMPreadrow(fp, ppos, array + y*cx, alpha_array + y*cx, cx, cBitCount, indexed, R, G, B);
+      if (rc == -1) {
+        pm_error("%s: couldn't read row %d", ifname, y);
+      }
+      if (rc % 4) {
+        pm_error("%s: row had bad number of bytes: %d", ifname, rc);
+      }
+    }
+  }
 }
 
 /**
@@ -474,7 +519,7 @@ Reader(PNMFileType *type, istream *file, bool owns_file, string magic_number) :
   pos = 0;
 
   BMPreadfileheader(file, &pos, &offBits);
-  BMPreadinfoheader(file, &pos, &cx, &cy, &cBitCount, &classv);
+  BMPreadinfoheader(file, &pos, &cx, &cy, &cBitCount, &cCompression, &classv);
 
   if (offBits != BMPoffbits(classv, cBitCount)) {
     pnmimage_bmp_cat.warning()
@@ -523,9 +568,10 @@ Reader(PNMFileType *type, istream *file, bool owns_file, string magic_number) :
 int PNMFileTypeBMP::Reader::
 read_data(xel *array, xelval *alpha_array) {
   BMPreadbits(array, alpha_array, _file, &pos, offBits, _x_size, _y_size,
-              cBitCount, classv, indexed, R, G, B);
+              cBitCount, cCompression, indexed, R, G, B);
 
-  if (pos != BMPlenfile(classv, cBitCount, _x_size, _y_size)) {
+  if (cCompression != 1 &&
+      pos != BMPlenfile(classv, cBitCount, _x_size, _y_size)) {
     pnmimage_bmp_cat.warning()
       << "Read " << pos << " bytes, expected to read "
       << BMPlenfile(classv, cBitCount, _x_size, _y_size) << " bytes\n";

+ 2 - 2
panda/src/putil/bitArray_ext.cxx

@@ -20,7 +20,7 @@
  */
 void Extension<BitArray>::
 __init__(PyObject *init_value) {
-  if (!PyLong_Check(init_value) || Py_SIZE(init_value) < 0) {
+  if (!PyLong_Check(init_value) || !PyLong_IsNonNegative(init_value)) {
     PyErr_SetString(PyExc_ValueError, "BitArray constructor requires a positive integer");
     return;
   }
@@ -76,7 +76,7 @@ __getstate__() const {
  */
 void Extension<BitArray>::
 __setstate__(PyObject *state) {
-  if (Py_SIZE(state) >= 0) {
+  if (PyLong_IsNonNegative(state)) {
     __init__(state);
   } else {
     PyObject *inverted = PyNumber_Invert(state);

+ 1 - 1
panda/src/putil/doubleBitMask_ext.I

@@ -17,7 +17,7 @@
 template<class BMType>
 INLINE void Extension<DoubleBitMask<BMType> >::
 __init__(PyObject *init_value) {
-  if (!PyLong_Check(init_value) || Py_SIZE(init_value) < 0) {
+  if (!PyLong_Check(init_value) || !PyLong_IsNonNegative(init_value)) {
     PyErr_SetString(PyExc_ValueError, "DoubleBitMask constructor requires a positive integer");
     return;
   }

+ 127 - 0
panda/src/tinydisplay/CMakeLists.txt

@@ -0,0 +1,127 @@
+if (NOT HAVE_TINYDISPLAY)
+  return()
+endif()
+
+set(P3TINYDISPLAY_HEADERS
+  config_tinydisplay.h
+  tinyGeomMunger.I tinyGeomMunger.h
+  tinySDLGraphicsPipe.I tinySDLGraphicsPipe.h
+  tinySDLGraphicsWindow.I tinySDLGraphicsWindow.h
+  tinyGraphicsBuffer.I tinyGraphicsBuffer.h
+  tinyGraphicsStateGuardian.I tinyGraphicsStateGuardian.h
+  tinyTextureContext.I tinyTextureContext.h
+  tinyWinGraphicsPipe.I tinyWinGraphicsPipe.h
+  tinyWinGraphicsWindow.I tinyWinGraphicsWindow.h
+  tinyXGraphicsPipe.I tinyXGraphicsPipe.h
+  tinyXGraphicsWindow.I tinyXGraphicsWindow.h
+  tinyOffscreenGraphicsPipe.I tinyOffscreenGraphicsPipe.h
+  srgb_tables.h
+  zbuffer.h
+  zfeatures.h
+  zgl.h
+  zline.h
+  zmath.h
+  ztriangle.h
+  ztriangle_two.h
+  ztriangle_code_1.h
+  ztriangle_code_2.h
+  ztriangle_code_3.h
+  ztriangle_code_4.h
+  ztriangle_table.h
+  store_pixel.h
+  store_pixel_code.h
+  store_pixel_table.h
+)
+
+set(P3TINYDISPLAY_SOURCES
+  clip.cxx
+  config_tinydisplay.cxx
+  error.cxx
+  image_util.cxx
+  init.cxx
+  td_light.cxx
+  memory.cxx
+  specbuf.cxx
+  store_pixel.cxx
+  td_texture.cxx
+  tinyGeomMunger.cxx
+  tinyGraphicsBuffer.cxx
+  tinyGraphicsStateGuardian.cxx
+  tinyOffscreenGraphicsPipe.cxx
+  tinySDLGraphicsPipe.cxx
+  tinySDLGraphicsWindow.cxx
+  tinyTextureContext.cxx
+  tinyWinGraphicsPipe.cxx
+  tinyWinGraphicsWindow.cxx
+  tinyXGraphicsPipe.cxx
+  tinyXGraphicsWindow.cxx
+  vertex.cxx
+  srgb_tables.cxx
+  zbuffer.cxx
+  zdither.cxx
+  zline.cxx
+  zmath.cxx
+)
+
+set(P3TINYDISPLAY_ZTRIANGLE_SOURCES
+  ztriangle_1.cxx
+  ztriangle_2.cxx
+  ztriangle_3.cxx
+  ztriangle_4.cxx
+  ztriangle_table.cxx
+)
+
+set_source_files_properties(${P3TINYDISPLAY_ZTRIANGLE_SOURCES}
+  PROPERTIES SKIP_UNITY_BUILD_INCLUSION YES)
+
+if(NOT MSVC)
+  set_source_files_properties(${P3TINYDISPLAY_ZTRIANGLE_SOURCES}
+    PROPERTIES COMPILE_FLAGS "-Wno-unused-but-set-variable")
+endif()
+
+if(HAVE_COCOA)
+  set(P3TINYDISPLAY_HEADERS ${P3TINYDISPLAY_HEADERS}
+    tinyCocoaGraphicsPipe.I tinyCocoaGraphicsPipe.h
+    tinyCocoaGraphicsWindow.I tinyCocoaGraphicsWindow.h)
+
+  set(P3TINYDISPLAY_SOURCES ${P3TINYDISPLAY_SOURCES}
+    tinyCocoaGraphicsPipe.cxx
+    tinyCocoaGraphicsWindow.mm)
+
+  set_source_files_properties(tinyCocoaGraphicsWindow.mm
+    PROPERTIES SKIP_UNITY_BUILD_INCLUSION YES)
+
+  add_compile_definitions(HAVE_COCOA)
+endif()
+
+composite_sources(p3tinydisplay P3TINYDISPLAY_SOURCES)
+
+# Determine the additional components to link in.
+set(COCOADISPLAY_LINK_TARGETS)
+
+if(WIN32)
+  list(APPEND COCOADISPLAY_LINK_TARGETS p3windisplay)
+endif()
+
+if(HAVE_X11)
+  list(APPEND COCOADISPLAY_LINK_TARGETS p3x11display)
+endif()
+
+if(HAVE_COCOA)
+  list(APPEND COCOADISPLAY_LINK_TARGETS p3cocoadisplay)
+endif()
+
+set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME "TinyDisplay")
+add_metalib(p3tinydisplay ${MODULE_TYPE}
+  ${P3TINYDISPLAY_HEADERS} ${P3TINYDISPLAY_SOURCES} ${P3TINYDISPLAY_ZTRIANGLE_SOURCES}
+  COMPONENTS ${COCOADISPLAY_LINK_TARGETS})
+unset(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME)
+
+set_target_properties(p3tinydisplay PROPERTIES DEFINE_SYMBOL BUILDING_TINYDISPLAY)
+
+install(TARGETS p3tinydisplay
+  EXPORT TinyDisplay COMPONENT TinyDisplay
+  DESTINATION ${MODULE_DESTINATION}
+  ARCHIVE COMPONENT TinyDisplayDevel)
+
+export_targets(TinyDisplay NAMESPACE "Panda3D::TinyDisplay::" COMPONENT TinyDisplayDevel)

+ 2 - 0
panda/src/tinydisplay/srgb_tables.cxx

@@ -1,3 +1,5 @@
+#include "srgb_tables.h"
+
 const unsigned short decode_sRGB[256] = { 0x0000, 0x0013, 0x0027, 0x003b,
   0x004f, 0x0063, 0x0077, 0x008b, 0x009f, 0x00b3, 0x00c6, 0x00db, 0x00f0,
   0x0107, 0x011f, 0x0139, 0x0153, 0x016f, 0x018c, 0x01aa, 0x01ca, 0x01eb,

+ 5 - 0
panda/src/tinydisplay/srgb_tables.h

@@ -1,6 +1,11 @@
+#ifndef SRGB_TABLES_H
+#define SRGB_TABLES_H
+
 /* 8-bit sRGB to 16-bit linear */
 extern const unsigned short decode_sRGB[256];
 
 /* 12-bit linear to 8-bit sRGB.  I used 12-bit because it can
    represent all possible 8-bit sRGB values. */
 extern const unsigned char encode_sRGB[4096];
+
+#endif

+ 2 - 0
panda/src/tinydisplay/tinyCocoaGraphicsWindow.h

@@ -62,6 +62,8 @@ private:
 
   small_vector<SwapBuffer, 2> _swap_chain;
   int _swap_index = 0;
+  uint32_t _vsync_counter = 0;
+  bool _vsync_enabled = false;
 
 public:
   static TypeHandle get_class_type() {

+ 15 - 0
panda/src/tinydisplay/tinyCocoaGraphicsWindow.mm

@@ -114,6 +114,21 @@ end_flip() {
   if (_flip_ready) {
     do_present();
     _swap_index = (_swap_index + 1) % _swap_chain.size();
+
+    // We don't really support proper VSync because we just update the backing
+    // store and let the OS update it when needed, but we still need to wait
+    // for VBlank so that the frame rate is appropriately limited.
+    if (sync_video) {
+      CocoaGraphicsPipe *cocoapipe = (CocoaGraphicsPipe *)_pipe.p();
+      if (!_vsync_enabled) {
+        // If this fails, we don't keep trying.
+        cocoapipe->init_vsync(_vsync_counter);
+        _vsync_enabled = true;
+      }
+      cocoapipe->wait_vsync(_vsync_counter);
+    } else {
+      _vsync_enabled = false;
+    }
   }
 
   GraphicsWindow::end_flip();

+ 2 - 0
panda/src/tinydisplay/zmath.h

@@ -1,6 +1,8 @@
 #ifndef __ZMATH__
 #define __ZMATH__
 
+#include "numeric_types.h"
+
 /* Matrix & Vertex */
 
 typedef struct {

+ 6 - 0
panda/src/wgldisplay/wglGraphicsStateGuardian.cxx

@@ -336,6 +336,8 @@ choose_pixel_format(const FrameBufferProperties &properties,
   _supports_pixel_format = has_extension("WGL_ARB_pixel_format");
   _supports_wgl_multisample = has_extension("WGL_ARB_multisample");
 
+  bool supports_pixel_format_float = _supports_pixel_format && has_extension("WGL_ARB_pixel_format_float");
+
   if (has_extension("WGL_ARB_create_context")) {
     _wglCreateContextAttribsARB =
       (PFNWGLCREATECONTEXTATTRIBSARBPROC)wglGetProcAddress("wglCreateContextAttribsARB");
@@ -381,6 +383,10 @@ choose_pixel_format(const FrameBufferProperties &properties,
     iattrib_list[ni++] = WGL_PIXEL_TYPE_ARB;
     iattrib_list[ni++] = WGL_TYPE_RGBA_ARB;
   }
+  else if (supports_pixel_format_float) {
+    iattrib_list[ni++] = WGL_PIXEL_TYPE_ARB;
+    iattrib_list[ni++] = WGL_TYPE_RGBA_FLOAT_ARB;
+  }
 
   if (need_pbuffer) {
     iattrib_list[ni++] = WGL_DRAW_TO_PBUFFER_ARB;

+ 12 - 3
panda/src/windisplay/winDetectDx.h

@@ -99,7 +99,6 @@ static int get_display_information (DisplaySearchParameters &display_search_para
 
   int success;
   DisplayInformation::DetectionState state;
-  int get_adapter_display_mode_state;
   int get_device_caps_state;
 
   GraphicsStateGuardian::ShaderModel shader_model;
@@ -117,6 +116,7 @@ static int get_display_information (DisplaySearchParameters &display_search_para
   int window_height;
   int window_bits_per_pixel;
   int total_display_modes;
+  int current_display_mode_index;
   DisplayMode *display_mode_array;
 
   uint64_t physical_memory;
@@ -139,6 +139,7 @@ static int get_display_information (DisplaySearchParameters &display_search_para
   window_height = 0;
   window_bits_per_pixel = 0;
   total_display_modes = 0;
+  current_display_mode_index = -1;
   display_mode_array = nullptr;
 
   minimum_width = display_search_parameters._minimum_width;
@@ -153,7 +154,6 @@ static int get_display_information (DisplaySearchParameters &display_search_para
   texture_memory = 0;
 
   state = DisplayInformation::DS_unknown;
-  get_adapter_display_mode_state = false;
   get_device_caps_state = false;
 
   physical_memory = 0;
@@ -193,6 +193,7 @@ static int get_display_information (DisplaySearchParameters &display_search_para
         device_type = D3DDEVTYPE_HAL;
 
         // windowed mode max res and format
+        bool get_adapter_display_mode_state = false;
         if (direct_3d -> GetAdapterDisplayMode (adapter, &current_d3d_display_mode) == D3D_OK) {
           if (debug) {
             printf ("current mode  w = %d h = %d r = %d f = %d \n",
@@ -428,6 +429,14 @@ static int get_display_information (DisplaySearchParameters &display_search_para
                       display_mode -> refresh_rate = d3d_display_mode.RefreshRate;
                       display_mode -> fullscreen_only = display_format_array [format_index].fullscreen_only;
 
+                      if (get_adapter_display_mode_state &&
+                          d3d_display_mode.Width == current_d3d_display_mode.Width &&
+                          d3d_display_mode.Height == current_d3d_display_mode.Height &&
+                          d3d_display_mode.RefreshRate == current_d3d_display_mode.RefreshRate &&
+                          d3d_display_mode.Format == current_d3d_display_mode.Format) {
+                        current_display_mode_index = total_display_modes;
+                      }
+
                       total_display_modes++;
                     }
                   }
@@ -587,7 +596,7 @@ static int get_display_information (DisplaySearchParameters &display_search_para
 
   if (success) {
     display_information -> _state = state;
-    display_information -> _get_adapter_display_mode_state = get_adapter_display_mode_state;
+    display_information -> _current_display_mode_index = current_display_mode_index;
     display_information -> _get_device_caps_state = get_device_caps_state;
     display_information -> _maximum_window_width = window_width;
     display_information -> _maximum_window_height = window_height;

+ 16 - 0
panda/src/windisplay/winGraphicsPipe.cxx

@@ -328,9 +328,21 @@ WinGraphicsPipe() {
     if (windisplay_cat.is_debug()) {
       windisplay_cat.debug() << "Using EnumDisplaySettings to fetch display information.\n";
     }
+
     pvector<DisplayMode> display_modes;
+    DisplayMode current_mode = {0};
+    int current_mode_index = -1;
     DEVMODE dm{};
     dm.dmSize = sizeof(dm);
+
+    if (EnumDisplaySettings(nullptr, ENUM_CURRENT_SETTINGS, &dm) != 0) {
+      current_mode.width = dm.dmPelsWidth;
+      current_mode.height = dm.dmPelsHeight;
+      current_mode.bits_per_pixel = dm.dmBitsPerPel;
+      current_mode.refresh_rate = dm.dmDisplayFrequency;
+      current_mode.fullscreen_only = 0;
+    }
+
     for (int i = 0; EnumDisplaySettings(nullptr, i, &dm) != 0; ++i) {
       DisplayMode mode;
       mode.width = dm.dmPelsWidth;
@@ -339,6 +351,9 @@ WinGraphicsPipe() {
       mode.refresh_rate = dm.dmDisplayFrequency;
       mode.fullscreen_only = 0;
       if (i == 0 || mode != display_modes.back()) {
+        if (current_mode_index < 0 && mode == current_mode) {
+          current_mode_index = (int)display_modes.size();
+        }
         display_modes.push_back(mode);
       }
     }
@@ -346,6 +361,7 @@ WinGraphicsPipe() {
     // Copy this information to the DisplayInformation object.
     _display_information->_total_display_modes = display_modes.size();
     if (!display_modes.empty()) {
+      _display_information->_current_display_mode_index = current_mode_index;
       _display_information->_display_mode_array = new DisplayMode[display_modes.size()];
       std::copy(display_modes.begin(), display_modes.end(),
                 _display_information->_display_mode_array);

+ 14 - 0
panda/src/x11display/x11GraphicsPipe.cxx

@@ -283,11 +283,25 @@ x11GraphicsPipe(const std::string &display) :
         x11display_cat.debug()
           << "Using XRRScreenResources to obtain display modes\n";
       }
+
+      // Query current configuration, we just grab the first CRTC for now,
+      // since we don't have a way to represent multiple monitors.
+      RRMode current_mode_id = 0;
+      if (res->ncrtc > 0) {
+        if (auto info = get_crtc_info(res.get(), res->crtcs[0])) {
+          current_mode_id = info->mode;
+        }
+      }
+
       _display_information->_total_display_modes = res->nmode;
       _display_information->_display_mode_array = new DisplayMode[res->nmode];
       for (int i = 0; i < res->nmode; ++i) {
         XRRModeInfo &mode = res->modes[i];
 
+        if (mode.id == current_mode_id) {
+          _display_information->_current_display_mode_index = i;
+        }
+
         DisplayMode *dm = _display_information->_display_mode_array + i;
         dm->width = mode.width;
         dm->height = mode.height;

+ 1 - 0
pandatool/CMakeLists.txt

@@ -28,6 +28,7 @@ add_subdirectory(src/imageprogs)
 add_subdirectory(src/lwo)
 add_subdirectory(src/lwoegg)
 add_subdirectory(src/lwoprogs)
+add_subdirectory(src/mac-stats)
 add_subdirectory(src/miscprogs)
 add_subdirectory(src/objegg)
 add_subdirectory(src/objprogs)

+ 34 - 7
pandatool/src/assimp/loaderFileTypeAssimp.cxx

@@ -57,22 +57,49 @@ get_extension() const {
  */
 string LoaderFileTypeAssimp::
 get_additional_extensions() const {
+  // This may be called at static init time, so ensure it is constructed now.
+  static ConfigVariableString assimp_disable_extensions
+  ("assimp-disable-extensions", "gltf glb",
+   PRC_DESC("A list of extensions (without preceding dot) that should not be "
+            "loaded via the Assimp loader, even if Assimp supports these "
+            "formats.  It is useful to set this for eg. gltf and glb files "
+            "to prevent them from being accidentally loaded via the Assimp "
+            "plug-in instead of via a superior plug-in like panda3d-gltf."));
+
+  bool has_disabled_exts = !assimp_disable_extensions.empty();
+
   aiString aexts;
   aiGetExtensionList(&aexts);
 
+  char *buffer = (char *)alloca(aexts.length + 2);
+  char *p = buffer;
+
   // The format is like: *.mdc;*.mdl;*.mesh.xml;*.mot
-  std::string ext;
   char *sub = strtok(aexts.data, ";");
   while (sub != nullptr) {
-    ext += sub + 2;
-    sub = strtok(nullptr, ";");
-
-    if (sub != nullptr) {
-      ext += ' ';
+    bool enabled = true;
+    if (has_disabled_exts) {
+      for (size_t i = 0; i < assimp_disable_extensions.get_num_words(); ++i) {
+        std::string disabled_ext = assimp_disable_extensions.get_word(i);
+        if (strcmp(sub + 2, disabled_ext.c_str()) == 0) {
+          enabled = false;
+          break;
+        }
+      }
+    }
+    if (enabled) {
+      *(p++) = ' ';
+      size_t len = strlen(sub + 2);
+      memcpy(p, sub + 2, len);
+      p += len;
     }
+
+    sub = strtok(nullptr, ";");
   }
 
-  return ext;
+  // Strip first space
+  ++buffer;
+  return std::string(buffer, p - buffer);
 }
 
 /**

+ 1 - 1
pandatool/src/gtk-stats/CMakeLists.txt

@@ -34,7 +34,7 @@ target_link_libraries(gtk-stats p3progbase p3pstatserver PKG::GTK3)
 
 # This program is NOT actually called gtk-stats. It's pstats-gtk on Win32 and
 # pstats everywhere else (as the Win32 GUI is not built).
-if(WIN32)
+if(WIN32 OR APPLE)
   set_target_properties(gtk-stats PROPERTIES OUTPUT_NAME "pstats-gtk")
 else()
   set_target_properties(gtk-stats PROPERTIES OUTPUT_NAME "pstats")

+ 53 - 0
pandatool/src/mac-stats/CMakeLists.txt

@@ -0,0 +1,53 @@
+if(NOT APPLE OR NOT HAVE_NET)
+  return()
+endif()
+
+set(MACSTATS_HEADERS
+  macStats.h
+  macStatsAppDelegate.h
+  macStatsChartMenu.h
+  macStatsChartMenuDelegate.h
+  macStatsFlameGraph.h
+  macStatsGraph.h
+  macStatsGraphView.h
+  macStatsGraphViewController.h
+  macStatsLabel.h
+  macStatsLabelStack.h
+  macStatsMonitor.h
+  macStatsPianoRoll.h
+  macStatsScaleArea.h
+  macStatsServer.h
+  macStatsStripChart.h
+  macStatsTimeline.h
+)
+
+set(MACSTATS_SOURCES
+  macStats.mm
+  macStatsAppDelegate.mm
+  macStatsChartMenu.mm
+  macStatsChartMenuDelegate.mm
+  macStatsFlameGraph.mm
+  macStatsGraph.mm
+  macStatsGraphView.mm
+  macStatsGraphViewController.mm
+  macStatsLabel.mm
+  macStatsLabelStack.mm
+  macStatsMonitor.mm
+  macStatsPianoRoll.mm
+  macStatsScaleArea.mm
+  macStatsServer.mm
+  macStatsStripChart.mm
+  macStatsTimeline.mm
+)
+
+composite_sources(mac-stats MACSTATS_SOURCES)
+add_executable(mac-stats ${MACSTATS_HEADERS} ${MACSTATS_SOURCES})
+target_link_libraries(mac-stats p3progbase p3pstatserver)
+target_link_libraries(mac-stats "-framework Cocoa")
+target_link_libraries(mac-stats "-framework Carbon")
+target_link_libraries(mac-stats "-framework Quartz")
+
+# This program is NOT actually called win-stats. It's just pstats
+set_target_properties(mac-stats PROPERTIES OUTPUT_NAME "pstats")
+
+install(TARGETS mac-stats EXPORT Tools COMPONENT Tools DESTINATION ${CMAKE_INSTALL_BINDIR})

+ 30 - 13
pandatool/src/mac-stats/macStatsTimeline.mm

@@ -232,9 +232,6 @@ draw_bar(int row, int from_x, int to_x, int collector_index,
 
     if ((to_x - from_x) >= scale * 4) {
       // Only bother drawing the text if we've got some space to draw on.
-      const PStatClientData *client_data = monitor->get_client_data();
-      const PStatCollectorDef &def = client_data->get_collector_def(collector_index);
-
       const CFStringRef keys[] = {
         (__bridge CFStringRef)NSForegroundColorAttributeName,
         (__bridge CFStringRef)NSFontAttributeName,
@@ -245,7 +242,7 @@ draw_bar(int row, int from_x, int to_x, int collector_index,
       };
       CFDictionaryRef attribs = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
 
-      CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, def._name.c_str(), kCFStringEncodingUTF8);
+      CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, collector_name.c_str(), kCFStringEncodingUTF8);
       CFAttributedStringRef astr = CFAttributedStringCreate(kCFAllocatorDefault, str, attribs);
 
       CTLineRef line = CTLineCreateWithAttributedString(astr);
@@ -260,23 +257,43 @@ draw_bar(int row, int from_x, int to_x, int collector_index,
       double text_left = std::max(from_x, 0) + scale / 2.0;
       double text_right = std::min(to_x, get_xsize()) - scale / 2.0;
       double text_top = top + (bottom - top - text_height) / 2.0 + text_height;
-/*
+
       if (text_width >= text_right - text_left) {
         size_t c = collector_name.rfind(':');
         if (text_right - text_left < scale * 6) {
           // It's a really tiny space.  Draw a single letter.
-          const char *ch = collector_name.data() + (c != std::string::npos ? c + 1 : 0);
-          pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER);
-          pango_layout_set_text(layout, ch, 1);
-        } else {
+          UniChar ch = *(collector_name.data() + (c != std::string::npos ? c + 1 : 0));
+
+          CFStringRef str = CFStringCreateWithCharacters(kCFAllocatorDefault, &ch, 1);
+          CFAttributedStringRef astr = CFAttributedStringCreate(kCFAllocatorDefault, str, attribs);
+
+          CTLineRef new_line = CTLineCreateWithAttributedString((CFAttributedStringRef)astr);
+          bounds = CTLineGetImageBounds(new_line, _ctx);
+          text_width = bounds.size.width;
+
+          CFRelease(line);
+          CFRelease(astr);
+          CFRelease(str);
+          line = new_line;
+        }
+        else {
           // Maybe just use everything after the last colon.
           if (c != std::string::npos) {
-            pango_layout_set_text(layout, collector_name.data() + c + 1,
-                                          collector_name.size() - c - 1);
-            pango_layout_get_pixel_size(layout, &text_width, &text_height);
+            const char *short_name = collector_name.data() + c + 1;
+            CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, short_name, kCFStringEncodingUTF8);
+            CFAttributedStringRef astr = CFAttributedStringCreate(kCFAllocatorDefault, str, attribs);
+
+            CTLineRef new_line = CTLineCreateWithAttributedString((CFAttributedStringRef)astr);
+            bounds = CTLineGetImageBounds(new_line, _ctx);
+            text_width = bounds.size.width;
+
+            CFRelease(line);
+            CFRelease(astr);
+            CFRelease(str);
+            line = new_line;
           }
         }
-      }*/
+      }
 
       if (text_width >= text_right - text_left) {
         // Have CoreText truncate to the correct length.

+ 2 - 1
requirements-test.txt

@@ -1,4 +1,5 @@
-pytest
+pytest>=3.9.0
 pytest-cov
 
 Pmw-py3==2.1
+setuptools>=15.2

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików