Browse Source

Merge branch 'master' into webgl-port

rdb 3 years ago
parent
commit
5405a8ed61
56 changed files with 798 additions and 255 deletions
  1. 6 6
      .github/workflows/ci.yml
  2. 5 5
      README.md
  3. 9 1
      direct/src/dist/FreezeTool.py
  4. 44 33
      direct/src/dist/commands.py
  5. 17 12
      direct/src/filter/CommonFilters.py
  6. 2 2
      direct/src/filter/FilterManager.py
  7. 1 1
      direct/src/showbase/JobManager.py
  8. 3 1
      direct/src/showbase/ShowBase.py
  9. 17 10
      direct/src/task/Task.py
  10. 60 0
      doc/ReleaseNotes
  11. 7 2
      makepanda/installpanda.py
  12. 13 16
      makepanda/makepackage.py
  13. 16 2
      makepanda/makepanda.py
  14. 18 9
      makepanda/makepandacore.py
  15. 7 0
      makepanda/makewheel.py
  16. 6 1
      makepanda/test_wheel.py
  17. BIN
      models/plugin_images/auth_click.png
  18. BIN
      models/plugin_images/auth_ready.png
  19. BIN
      models/plugin_images/auth_rollover.png
  20. BIN
      models/plugin_images/download.png
  21. BIN
      models/plugin_images/failed.png
  22. BIN
      models/plugin_images/installer.bmp
  23. BIN
      models/plugin_images/panda3d.icns
  24. BIN
      models/plugin_images/play_click.png
  25. BIN
      models/plugin_images/play_ready.png
  26. BIN
      models/plugin_images/play_rollover.png
  27. 130 28
      panda/src/androiddisplay/androidGraphicsWindow.cxx
  28. 1 0
      panda/src/androiddisplay/androidGraphicsWindow.h
  29. 1 0
      panda/src/cocoadisplay/cocoaGraphicsWindow.mm
  30. 3 1
      panda/src/device/evdevInputDevice.cxx
  31. 28 25
      panda/src/device/winRawInputDevice.cxx
  32. 3 3
      panda/src/device/winRawInputDevice.h
  33. 29 0
      panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx
  34. 20 4
      panda/src/dxgsg9/dxTextureContext9.cxx
  35. 11 12
      panda/src/egg2pg/eggRenderState.cxx
  36. 42 0
      panda/src/egg2pg/eggSaver.cxx
  37. 7 0
      panda/src/framework/windowFramework.cxx
  38. 4 6
      panda/src/glstuff/glGraphicsStateGuardian_src.cxx
  39. 5 0
      panda/src/glstuff/glShaderContext_src.cxx
  40. 8 0
      panda/src/gobj/geomPrimitive.I
  41. 19 0
      panda/src/gobj/geomPrimitive.cxx
  42. 2 0
      panda/src/gobj/geomPrimitive.h
  43. 18 0
      panda/src/gobj/texture.cxx
  44. 2 2
      panda/src/gobj/texturePeeker.cxx
  45. 2 0
      panda/src/pgraph/lightAttrib.cxx
  46. 1 1
      panda/src/pgraph/pandaNode.cxx
  47. 4 0
      panda/src/pgraph/pythonLoaderFileType.cxx
  48. 36 41
      panda/src/pgraphnodes/sceneGraphAnalyzer.cxx
  49. 50 4
      panda/src/pnmimage/pnmBrush.cxx
  50. 1 0
      panda/src/pnmimage/pnmBrush.h
  51. 3 2
      panda/src/pnmimage/pnmImage.cxx
  52. 2 1
      panda/src/pnmimage/pnmImage.h
  53. 47 10
      panda/src/windisplay/winGraphicsWindow.cxx
  54. 2 0
      panda/src/x11display/x11GraphicsWindow.cxx
  55. 35 14
      pandatool/src/mayaprogs/mayapath.cxx
  56. 51 0
      tests/pgraph/test_nodepath.py

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

@@ -347,16 +347,16 @@ jobs:
       shell: powershell
       run: |
         $wc = New-Object System.Net.WebClient
-        $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.10/panda3d-1.10.10-tools-win64.zip", "thirdparty-tools.zip")
+        $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.11/panda3d-1.10.11-tools-win64.zip", "thirdparty-tools.zip")
         Expand-Archive -Path thirdparty-tools.zip
-        Move-Item -Path thirdparty-tools/panda3d-1.10.10/thirdparty -Destination .
+        Move-Item -Path thirdparty-tools/panda3d-1.10.11/thirdparty -Destination .
     - name: Get thirdparty packages (macOS)
       if: runner.os == 'macOS'
       run: |
-        curl -O https://www.panda3d.org/download/panda3d-1.10.10/panda3d-1.10.10-tools-mac.tar.gz
-        tar -xf panda3d-1.10.10-tools-mac.tar.gz
-        mv panda3d-1.10.10/thirdparty thirdparty
-        rmdir panda3d-1.10.10
+        curl -O https://www.panda3d.org/download/panda3d-1.10.11/panda3d-1.10.11-tools-mac.tar.gz
+        tar -xf panda3d-1.10.11-tools-mac.tar.gz
+        mv panda3d-1.10.11/thirdparty thirdparty
+        rmdir panda3d-1.10.11
         (cd thirdparty/darwin-libs-a && rm -rf rocket)
     - name: Set up Python 3.9
       uses: actions/setup-python@v2

+ 5 - 5
README.md

@@ -24,7 +24,7 @@ Installing Panda3D
 ==================
 
 The latest Panda3D SDK can be downloaded from
-[this page](https://www.panda3d.org/download/sdk-1-10-10/).
+[this page](https://www.panda3d.org/download/sdk-1-10-11/).
 If you are familiar with installing Python packages, you can use
 the following command:
 
@@ -54,7 +54,7 @@ Windows
 
 You can build Panda3D with the Microsoft Visual C++ 2015, 2017, 2019 or 2022
 compiler, which can be downloaded for free from the [Visual Studio site](https://visualstudio.microsoft.com/downloads/).
-You will also need to install the [Windows 10 SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk),
+You will also need to install the [Windows SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-sdk),
 and if you intend to target Windows Vista, you will also need the
 [Windows 8.1 SDK](https://go.microsoft.com/fwlink/p/?LinkId=323507).
 
@@ -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.10/panda3d-1.10.10-tools-win64.zip
-- https://www.panda3d.org/download/panda3d-1.10.10/panda3d-1.10.10-tools-win32.zip
+- https://www.panda3d.org/download/panda3d-1.10.11/panda3d-1.10.11-tools-win64.zip
+- https://www.panda3d.org/download/panda3d-1.10.11/panda3d-1.10.11-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
@@ -136,7 +136,7 @@ macOS
 -----
 
 On macOS, you will need to download a set of precompiled thirdparty packages in order to
-compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.10/panda3d-1.10.10-tools-mac.tar.gz).
+compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.11/panda3d-1.10.11-tools-mac.tar.gz).
 
 After placing the thirdparty directory inside the panda3d source directory,
 you may build Panda3D using a command like the following:

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

@@ -91,6 +91,8 @@ ignoreImports = {
 
     'toml.encoder': ['numpy'],
     'py._builtin': ['__builtin__'],
+
+    'site': ['android_log'],
 }
 
 if sys.version_info >= (3, 8):
@@ -2406,7 +2408,13 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
                     return None
 
                 try:
-                    fp = zip.open(fn.replace(os.path.sep, '/'), 'r')
+                    zip_fn = fn.replace(os.path.sep, '/')
+                    if zip_fn.startswith('deploy_libs/_tkinter.'):
+                        # If we have a tkinter wheel on the path, ignore the
+                        # _tkinter extension in deploy-libs.
+                        if any(entry.endswith(".whl") and os.path.basename(entry).startswith("tkinter-") for entry in self.path):
+                            return None
+                    fp = zip.open(zip_fn, 'r')
                 except KeyError:
                     return None
 

+ 44 - 33
direct/src/dist/commands.py

@@ -96,6 +96,7 @@ PACKAGE_DATA_DIRS = {
     ],
     'pytz': [('pytz/zoneinfo/*', 'zoneinfo', ())],
     'certifi': [('certifi/cacert.pem', '', {})],
+    '_tkinter_ext': [('_tkinter_ext/tcl/**', 'tcl', {})],
 }
 
 # Some dependencies have extra directories that need to be scanned for DLLs.
@@ -140,21 +141,6 @@ def get_data(path):
 
 FrozenImporter.find_spec = find_spec
 FrozenImporter.get_data = get_data
-
-# Set the TCL_LIBRARY directory to the location of the Tcl/Tk/Tix files.
-import os
-tcl_dir = os.path.join(os.path.dirname(sys.executable), 'tcl')
-if os.path.isdir(tcl_dir):
-    for dir in os.listdir(tcl_dir):
-        sub_dir = os.path.join(tcl_dir, dir)
-        if os.path.isdir(sub_dir):
-            if dir.startswith('tcl'):
-                os.environ['TCL_LIBRARY'] = sub_dir
-            if dir.startswith('tk'):
-                os.environ['TK_LIBRARY'] = sub_dir
-            if dir.startswith('tix'):
-                os.environ['TIX_LIBRARY'] = sub_dir
-del os
 """
 
 SITE_PY_ANDROID = """
@@ -277,7 +263,7 @@ class build_apps(setuptools.Command):
         self.exclude_modules = {}
         self.icons = {}
         self.platforms = [
-            'manylinux2010_x86_64',
+            'manylinux2014_x86_64',
             'macosx_10_9_x86_64',
             'win_amd64',
         ]
@@ -595,10 +581,15 @@ class build_apps(setuptools.Command):
             '-d', whldir,
             '-r', self.requirements_path,
             '--only-binary', ':all:',
-            '--platform', platform,
             '--abi', abi_tag,
+            '--platform', platform,
         ]
 
+        if platform.startswith('linux_'):
+            # Also accept manylinux.
+            arch = platform[6:]
+            pip_args += ['--platform', 'manylinux2014_' + arch]
+
         if self.use_optimized_wheels:
             pip_args += [
                 '--extra-index-url', self.optimized_wheel_index
@@ -766,6 +757,7 @@ class build_apps(setuptools.Command):
         path = sys.path[:]
         p3dwhl = None
         wheelpaths = []
+        has_tkinter_wheel = False
 
         if use_wheels:
             wheelpaths = self.download_wheels(platform)
@@ -775,6 +767,8 @@ class build_apps(setuptools.Command):
                     p3dwhlfn = whl
                     p3dwhl = self._get_zip_file(p3dwhlfn)
                     break
+                elif os.path.basename(whl).startswith('tkinter-'):
+                    has_tkinter_wheel = True
             else:
                 raise RuntimeError("Missing panda3d wheel for platform: {}".format(platform))
 
@@ -787,6 +781,11 @@ class build_apps(setuptools.Command):
                         distutils.log.WARN
                     )
 
+            for whl in wheelpaths:
+                if os.path.basename(whl).startswith('tkinter-'):
+                    has_tkinter_wheel = True
+                    break
+
             #whlfiles = {whl: self._get_zip_file(whl) for whl in wheelpaths}
 
             # Add whl files to the path so they are picked up by modulefinder
@@ -1026,6 +1025,10 @@ class build_apps(setuptools.Command):
         for appname, scriptname in self.console_apps.items():
             create_runtime(platform, appname, scriptname, True)
 
+        # Warn if tkinter is used but hasn't been added to requirements.txt
+        if not has_tkinter_wheel and '_tkinter' in freezer_modules:
+            self.warn("Detected use of tkinter, but tkinter is not specified in requirements.txt!")
+
         # Copy extension modules
         whl_modules = []
         whl_modules_ext = ''
@@ -1040,6 +1043,11 @@ class build_apps(setuptools.Command):
                 if not any(i.endswith(suffix) for suffix in ext_suffixes):
                     continue
 
+                if has_tkinter_wheel and i.startswith('deploy_libs/_tkinter.'):
+                    # Ignore this one, we have a separate tkinter package
+                    # nowadays that contains all the dependencies.
+                    continue
+
                 base = os.path.basename(i)
                 module, _, ext = base.partition('.')
                 whl_modules.append(module)
@@ -1111,22 +1119,6 @@ class build_apps(setuptools.Command):
             search_path = get_search_path_for(source_path)
             self.copy_with_dependencies(source_path, target_path, search_path)
 
-        # Copy over the tcl directory.
-        #TODO: get this to work on non-Windows platforms.
-        if sys.platform == "win32" and platform.startswith('win'):
-            tcl_dir = os.path.join(sys.prefix, 'tcl')
-
-            if os.path.isdir(tcl_dir) and 'tkinter' in freezer_modules:
-                self.announce('Copying Tcl files', distutils.log.INFO)
-                os.makedirs(os.path.join(binary_dir, 'tcl'))
-
-                for dir in os.listdir(tcl_dir):
-                    sub_dir = os.path.join(tcl_dir, dir)
-                    if os.path.isdir(sub_dir):
-                        target_dir = os.path.join(binary_dir, 'tcl', dir)
-                        self.announce('copying {0} -> {1}'.format(sub_dir, target_dir))
-                        shutil.copytree(sub_dir, target_dir)
-
         # Copy classes.dex on Android
         if use_wheels and platform.startswith('android'):
             self.copy(os.path.join(p3dwhlfn, 'deploy_libs', 'classes.dex'),
@@ -1151,9 +1143,14 @@ class build_apps(setuptools.Command):
                     target_dir = os.path.join(data_dir, target_dir)
 
                     for wf in filenames:
+                        if wf.endswith('/'):
+                            # Skip directories.
+                            continue
+
                         if wf.lower().startswith(source_dir.lower() + '/'):
                             if not srcglob.matches(wf.lower()):
                                 continue
+
                             wf = wf.replace('/', os.sep)
                             relpath = wf[len(source_dir) + 1:]
                             source_path = os.path.join(whl, wf)
@@ -1597,6 +1594,20 @@ class bdist_apps(setuptools.Command):
         'manylinux1_i686': ['gztar'],
         'manylinux2010_x86_64': ['gztar'],
         'manylinux2010_i686': ['gztar'],
+        'manylinux2014_x86_64': ['gztar'],
+        'manylinux2014_i686': ['gztar'],
+        'manylinux2014_aarch64': ['gztar'],
+        'manylinux2014_armv7l': ['gztar'],
+        'manylinux2014_ppc64': ['gztar'],
+        'manylinux2014_ppc64le': ['gztar'],
+        'manylinux2014_s390x': ['gztar'],
+        'manylinux_2_24_x86_64': ['gztar'],
+        'manylinux_2_24_i686': ['gztar'],
+        'manylinux_2_24_aarch64': ['gztar'],
+        'manylinux_2_24_armv7l': ['gztar'],
+        'manylinux_2_24_ppc64': ['gztar'],
+        'manylinux_2_24_ppc64le': ['gztar'],
+        'manylinux_2_24_s390x': ['gztar'],
         'android': ['aab'],
         # Everything else defaults to ['zip']
     }

+ 17 - 12
direct/src/filter/CommonFilters.py

@@ -1,21 +1,21 @@
 """
-
 Class CommonFilters implements certain common image
 postprocessing filters.  See the :ref:`common-image-filters` page for
 more information about how to use these filters.
 
-It is not ideal that these filters are all included in a single
-monolithic module.  Unfortunately, when you want to apply two filters
-at the same time, you have to compose them into a single shader, and
-the composition process isn't simply a question of concatenating them:
-you have to somehow make them work together.  I suspect that there
-exists some fairly simple framework that would make this automatable.
-However, until I write some more filters myself, I won't know what
-that framework is.  Until then, I'll settle for this
-clunky approach.  - Josh
-
+These filters are written in the Cg shading language.
 """
 
+# It is not ideal that these filters are all included in a single
+# monolithic module.  Unfortunately, when you want to apply two filters
+# at the same time, you have to compose them into a single shader, and
+# the composition process isn't simply a question of concatenating them:
+# you have to somehow make them work together.  I suspect that there
+# exists some fairly simple framework that would make this automatable.
+# However, until I write some more filters myself, I won't know what
+# that framework is.  Until then, I'll settle for this
+# clunky approach.  - Josh
+
 from panda3d.core import LVecBase4, LPoint2
 from panda3d.core import AuxBitplaneAttrib
 from panda3d.core import Texture, Shader, ATSNone
@@ -459,6 +459,11 @@ class CommonFilters:
         return True
 
     def setBloom(self, blend=(0.3,0.4,0.3,0.0), mintrigger=0.6, maxtrigger=1.0, desat=0.6, intensity=1.0, size="medium"):
+        """
+        Applies the Bloom filter to the output.
+        size can either be "off", "small", "medium", or "large".
+        Setting size to "off" will remove the Bloom filter.
+        """
         if size == 0 or size == "off":
             self.delBloom()
             return
@@ -548,7 +553,7 @@ class CommonFilters:
         return True
 
     def setBlurSharpen(self, amount=0.0):
-        """Enables the blur/sharpen filter. If the 'amount' parameter is 1.0, it will not have effect.
+        """Enables the blur/sharpen filter. If the 'amount' parameter is 1.0, it will not have any effect.
         A value of 0.0 means fully blurred, and a value higher than 1.0 sharpens the image."""
         fullrebuild = ("BlurSharpen" not in self.configuration)
         self.configuration["BlurSharpen"] = amount

+ 2 - 2
direct/src/filter/FilterManager.py

@@ -123,8 +123,8 @@ class FilterManager(DirectObject):
             winy = winy // div
 
         if mul != 1:
-            winx = winx * mul
-            winy = winy * mul
+            winx = int(round(winx * mul))
+            winy = int(round(winy * mul))
 
         return winx,winy
 

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

@@ -1,4 +1,4 @@
-from panda3d.core import ConfigVariableBool, ConfigVariableDouble
+from panda3d.core import ConfigVariableBool, ConfigVariableDouble, ClockObject
 from direct.directnotify.DirectNotifyGlobal import directNotify
 from direct.task.TaskManagerGlobal import taskMgr
 from direct.showbase.Job import Job

+ 3 - 1
direct/src/showbase/ShowBase.py

@@ -19,7 +19,9 @@ Built-in global variables
 Some key variables used in all Panda3D scripts are actually attributes of the
 ShowBase instance.  When creating an instance of this class, it will write many
 of these variables to the built-in scope of the Python interpreter, so that
-they are accessible to any Python module, without the need fors extra imports.
+they are accessible to any Python module, without the need for extra imports.
+For example, the ShowBase instance itself is accessible anywhere through the
+:data:`~builtins.base` variable.
 
 While these are handy for prototyping, we do not recommend using them in bigger
 projects, as it can make the code confusing to read to other Python developers,

+ 17 - 10
direct/src/task/Task.py

@@ -127,6 +127,8 @@ class TaskManager:
         self.destroyed = False
         self.fKeyboardInterrupt = False
         self.interruptCount = 0
+        if signal:
+            self.__prevHandler = signal.default_int_handler
 
         self._frameProfileQueue = []
 
@@ -168,7 +170,7 @@ class TaskManager:
         print('*** allowing mid-frame keyboard interrupt.')
         # Restore default interrupt handler
         if signal:
-            signal.signal(signal.SIGINT, signal.default_int_handler)
+            signal.signal(signal.SIGINT, self.__prevHandler)
         # and invoke it
         raise KeyboardInterrupt
 
@@ -475,25 +477,30 @@ class TaskManager:
         chains that are in sub-threads or that have frame budgets
         might execute their tasks differently. """
 
+        startFrameTime = self.globalClock.getRealTime()
+
         # 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.interruptCount = 0
+
         if signal:
-            signal.signal(signal.SIGINT, self.keyboardInterruptHandler)
+            self.__prevHandler = signal.signal(signal.SIGINT, self.keyboardInterruptHandler)
 
-        startFrameTime = self.globalClock.getRealTime()
+        try:
+            self.mgr.poll()
 
-        self.mgr.poll()
+            # This is the spot for an internal yield function
+            nextTaskTime = self.mgr.getNextWakeTime()
+            self.doYield(startFrameTime, nextTaskTime)
 
-        # This is the spot for an internal yield function
-        nextTaskTime = self.mgr.getNextWakeTime()
-        self.doYield(startFrameTime, nextTaskTime)
+        finally:
+            # Restore previous interrupt handler
+            if signal:
+                signal.signal(signal.SIGINT, self.__prevHandler)
+                self.__prevHandler = signal.default_int_handler
 
-        # Restore default interrupt handler
-        if signal:
-            signal.signal(signal.SIGINT, signal.default_int_handler)
         if self.fKeyboardInterrupt:
             raise KeyboardInterrupt
 

+ 60 - 0
doc/ReleaseNotes

@@ -1,3 +1,63 @@
+-----------------------  RELEASE 1.10.11  -----------------------
+
+Maintenance release containing assorted bug fixes and minor improvements.
+
+Rendering
+* Fix erratic shadow bug with multiple lights from gltf/blend2bam (#1153)
+* Fix erratic behavior of HW skinning shaders on non-animated models (#1207)
+* Fix errors with compressed luminance textures in DirectX 9 (#1198)
+* Implement screenshotting multisample backbuffer in DirectX 9 (#1225)
+
+Texture Loading
+* Don't load texture from disk when loading .bam if preloading is off (#1208)
+* Fix TextureReloadRequest not working properly when mipmapping is disabled
+* Add TexturePool.get_texture() method for querying textures in pool
+* Fix crash when opening a .txo, .dds or .ktx file fails
+* Improve error message when calling tex.write() with unknown extension
+
+Input
+* Generate horizontal scroll wheel events on Windows
+* Generate events for mouse buttons 4 and 5 on X11
+* Generate events for lmeta, rmeta and menu keys on Windows
+* Add raw event (raw-<) for key between shift and Z on ISO keyboards
+* Gracefully handle invalid raw input device data on Windows
+* Correctly handle negative axis input from Windows raw input devices (#1218)
+* FrSky RC controller is now registered as flight stick (#1218)
+
+Deployment
+* Support building with tkinter on all supported platforms (#780)
+* Fix issue with zipimport module not being packaged
+* Fix grayscale icons becoming blue when scaled automatically
+* Automatically include cacert.pem when depending on certifi
+* Suppress assorted spurious missing module warnings
+* Targeting linux_x86_64 / linux_i686 also allows use of manylinux wheels
+
+Build
+* Add support for Maya 2022 (#1213)
+* Support building with Visual Studio 2022
+* Support building with macOS 11.3 SDK (and work around clang crash)
+* Support building with Windows 11 SDK
+* Build Ubuntu .deb files with bindings for multiple Python 3 versions
+* Support compilation with Assimp 5.x (#1212)
+* Support building on manylinux_2_24
+
+Miscellaneous
+* Fix nodes with same tag key but different value getting flattened together
+* taskMgr.step() now restores previous SIGINT handler afterwards (#1180)
+* Add base.clock as alias for globalClock
+* FilterManager mul parameter now accepts floating-point values (#1231)
+* Assorted minor API documentation improvements
+* Fix memory leak getting Bullet persistent manifolds from Python (#1193)
+* Fix assertion in PythonLoaderFileType with debug Python build
+* Add missing property interface to PlaneNode
+* Fix prepare_scene() not properly invoking the Shader Generator
+* Add name property to AICharacter class (#1205)
+* Add bullet-split-impulse configuration variable (#1201)
+* Fix slider thumb entering dragging state on keyboard button press (#1188)
+* Allow OnscreenImage to be created before ShowBase is created (#1209)
+* Fix manager, t, play_rate, duration properties of Sequence/Parallel (#1202)
+* Expose ButtonEvent API to Python (UNSTABLE API, will be changed soon)
+
 -----------------------  RELEASE 1.10.10  -----------------------
 
 This release fixes assorted, mostly very minor bugs.

+ 7 - 2
makepanda/installpanda.py

@@ -10,10 +10,12 @@
 
 import os
 import sys
-import sysconfig
 from optparse import OptionParser
 from makepandacore import *
 
+# DO NOT CHANGE TO sysconfig - see GitHub issue #1230
+from distutils.sysconfig import get_python_lib
+
 
 MIME_INFO = (
     ("egg", "model/x-egg", "EGG model file", "pview"),
@@ -135,13 +137,16 @@ def GetLibDir():
 
     if os.path.isfile('/etc/debian_version'):
         return GetDebLibDir()
+    elif os.path.isfile('/etc/arch-release'):
+        # ArchLinux has lib64, but it is a symlink to lib.
+        return "lib"
     else:
         # Okay, maybe we're on an RPM-based system?
         return GetRPMLibDir()
 
     # If Python is installed into /usr/lib64, it's probably safe
     # to assume that we should install there as well.
-    python_lib = sysconfig.get_path("platlib")
+    python_lib = get_python_lib(1)
     if python_lib.startswith('/usr/lib64/') or \
        python_lib.startswith('/usr/local/lib64/'):
         return "lib64"

+ 13 - 16
makepanda/makepackage.py

@@ -6,7 +6,6 @@ import shutil
 import glob
 import re
 import subprocess
-import sysconfig
 from makepandacore import *
 from installpanda import *
 
@@ -219,18 +218,10 @@ def MakeInstallerLinux(version, debversion=None, rpmversion=None, rpmrelease=1,
                        python_versions=[], **kwargs):
     outputdir = GetOutputDir()
 
-    # We pack the default Python 3 version that ships with Ubuntu.
-    python3_ver = None
+    # Only pack the versions of Python included with this Ubuntu version.
     install_python_versions = []
-
-    # What's the system version of Python 3?
-    oscmd('python3 -V > "%s/tmp/python3_version.txt"' % (outputdir))
-    sys_python3_ver = '.'.join(ReadFile(outputdir + "/tmp/python3_version.txt").strip().split(' ')[1].split('.')[:2])
-
-    # Check that we built with support for it.
     for version_info in python_versions:
-        if version_info["version"] == sys_python3_ver:
-            python3_ver = sys_python3_ver
+        if os.path.isdir("/usr/lib/python" + version_info["version"]):
             install_python_versions.append(version_info)
 
     major_version = '.'.join(version.split('.')[:2])
@@ -317,9 +308,13 @@ def MakeInstallerLinux(version, debversion=None, rpmversion=None, rpmrelease=1,
         recommends = ReadFile("targetroot/debian/substvars_rec").replace("shlibs:Depends=", "").strip()
         provides = "panda3d"
 
-        if python3_ver:
-            depends += ", python%s" % (python3_ver)
-            recommends += ", python-pmw, python3-tk (>= %s)" % (python3_ver)
+        # Require at least one of the Python versions we built for.
+        if install_python_versions:
+            depends += ", " + " | ".join("python" + version_info["version"] for version_info in install_python_versions)
+
+            # But recommend the system version of Python 3.
+            recommends += ", python3"
+            recommends += ", python3-tk"
             provides += ", python3-panda3d"
 
         if not PkgSkip("NVIDIACG"):
@@ -336,7 +331,7 @@ def MakeInstallerLinux(version, debversion=None, rpmversion=None, rpmrelease=1,
         oscmd("chmod -R 755 targetroot/DEBIAN")
         oscmd("chmod 644 targetroot/DEBIAN/control targetroot/DEBIAN/md5sums")
         oscmd("chmod 644 targetroot/DEBIAN/conffiles targetroot/DEBIAN/symbols")
-        oscmd("fakeroot dpkg-deb -b targetroot %s_%s_%s.deb" % (pkg_name, pkg_version, pkg_arch))
+        oscmd("fakeroot dpkg-deb -Zxz -b targetroot %s_%s_%s.deb" % (pkg_name, pkg_version, pkg_arch))
 
     elif rpmbuild_present:
         # Invoke installpanda.py to install it into a temporary dir
@@ -913,7 +908,9 @@ def MakeInstallerAndroid(version, **kwargs):
                     shutil.copy(os.path.join(source_dir, base), target)
 
     # Copy the Python standard library to the .apk as well.
-    stdlib_source = sysconfig.get_path("stdlib")
+    # DO NOT CHANGE TO sysconfig - see #1230
+    from distutils.sysconfig 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)
 

+ 16 - 2
makepanda/makepanda.py

@@ -21,7 +21,6 @@ try:
     import threading
     import signal
     import shutil
-    import sysconfig
     import plistlib
     import queue
 except KeyboardInterrupt:
@@ -157,7 +156,7 @@ def usage(problem):
     print("  --nothing         (disable every third-party lib)")
     print("  --everything      (enable every third-party lib)")
     print("  --directx-sdk=X   (specify version of DirectX SDK to use: jun2010, aug2009)")
-    print("  --windows-sdk=X   (specify Windows SDK version, eg. 7.1, 8.1 or 10.  Default is 8.1)")
+    print("  --windows-sdk=X   (specify Windows SDK version, eg. 7.1, 8.1, 10 or 11.  Default is 8.1)")
     print("  --msvc-version=X  (specify Visual C++ version, eg. 10, 11, 12, 14, 14.1, 14.2, 14.3.  Default is 14)")
     print("  --use-icl         (experimental setting to use an intel compiler instead of MSVC on Windows)")
     print("")
@@ -416,6 +415,8 @@ elif target == 'linux' and (os.path.isfile("/lib/libc-2.5.so") or os.path.isfile
     # This is manylinux1.  A bit of a sloppy check, though.
     if GetTargetArch() in ('x86_64', 'amd64'):
         PLATFORM = 'manylinux1-x86_64'
+    elif GetTargetArch() in ('arm64', 'aarch64'):
+        PLATFORM = 'manylinux1-aarch64'
     else:
         PLATFORM = 'manylinux1-i686'
 
@@ -423,6 +424,8 @@ elif target == 'linux' and (os.path.isfile("/lib/libc-2.12.so") or os.path.isfil
     # Same sloppy check for manylinux2010.
     if GetTargetArch() in ('x86_64', 'amd64'):
         PLATFORM = 'manylinux2010-x86_64'
+    elif GetTargetArch() in ('arm64', 'aarch64'):
+        PLATFORM = 'manylinux2010-aarch64'
     else:
         PLATFORM = 'manylinux2010-i686'
 
@@ -430,9 +433,20 @@ elif target == 'linux' and (os.path.isfile("/lib/libc-2.17.so") or os.path.isfil
     # Same sloppy check for manylinux2014.
     if GetTargetArch() in ('x86_64', 'amd64'):
         PLATFORM = 'manylinux2014-x86_64'
+    elif GetTargetArch() in ('arm64', 'aarch64'):
+        PLATFORM = 'manylinux2014-aarch64'
     else:
         PLATFORM = 'manylinux2014-i686'
 
+elif target == 'linux' and (os.path.isfile("/lib/i386-linux-gnu/libc-2.24.so") or os.path.isfile("/lib/x86_64-linux-gnu/libc-2.24.so")) and os.path.isdir("/opt/python"):
+    # Same sloppy check for manylinux_2_24.
+    if GetTargetArch() in ('x86_64', 'amd64'):
+        PLATFORM = 'manylinux_2_24-x86_64'
+    elif GetTargetArch() in ('arm64', 'aarch64'):
+        PLATFORM = 'manylinux_2_24-aarch64'
+    else:
+        PLATFORM = 'manylinux_2_24-i686'
+
 elif not CrossCompiling():
     if HasTargetArch():
         # Replace the architecture in the platform string.

+ 18 - 9
makepanda/makepandacore.py

@@ -6,6 +6,7 @@
 ########################################################################
 
 import configparser
+from distutils import sysconfig # DO NOT CHANGE to sysconfig - see #1230
 import fnmatch
 import getpass
 import glob
@@ -17,7 +18,6 @@ import shutil
 import signal
 import subprocess
 import sys
-import sysconfig
 import threading
 import _thread as thread
 import time
@@ -1659,7 +1659,10 @@ def LocateLibrary(lib, lpath=[], prefer_static=False):
                 return os.path.join(dir, 'lib%s.a' % lib)
 
     for dir in lpath:
-        if target == 'darwin' and os.path.isfile(os.path.join(dir, 'lib%s.dylib' % lib)):
+        if target == 'windows':
+            if os.path.isfile(os.path.join(dir, lib + '.lib')):
+                return os.path.join(dir, lib + '.lib')
+        elif target == 'darwin' and os.path.isfile(os.path.join(dir, 'lib%s.dylib' % lib)):
             return os.path.join(dir, 'lib%s.dylib' % lib)
         elif target != 'darwin' and os.path.isfile(os.path.join(dir, 'lib%s.so' % lib)):
             return os.path.join(dir, 'lib%s.so' % lib)
@@ -2206,12 +2209,12 @@ def SdkLocatePython(prefer_thirdparty_python=False):
         LibDirectory("PYTHON", py_fwx + "/lib")
 
     #elif GetTarget() == 'windows':
-    #    SDK["PYTHON"] = os.path.dirname(sysconfig.get_path("include"))
+    #    SDK["PYTHON"] = os.path.dirname(sysconfig.get_python_inc())
     #    SDK["PYTHONVERSION"] = "python" + sysconfig.get_python_version()
     #    SDK["PYTHONEXEC"] = sys.executable
 
     else:
-        SDK["PYTHON"] = sysconfig.get_path("include")
+        SDK["PYTHON"] = sysconfig.get_python_inc()
         SDK["PYTHONVERSION"] = "python" + sysconfig.get_python_version() + abiflags
         SDK["PYTHONEXEC"] = os.path.realpath(sys.executable)
 
@@ -2318,7 +2321,7 @@ def SdkLocateWindows(version=None):
     if version == '10':
         version = '10.0'
 
-    if version and version.startswith('10.') and version.count('.') == 1:
+    if (version and version.startswith('10.') and version.count('.') == 1) or version == '11':
         # Choose the latest version of the Windows 10 SDK.
         platsdk = GetRegistryKey("SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots", "KitsRoot10")
 
@@ -2327,7 +2330,13 @@ def SdkLocateWindows(version=None):
             platsdk = "C:\\Program Files (x86)\\Windows Kits\\10\\"
 
         if platsdk and os.path.isdir(platsdk):
+            min_version = (10, 0, 0)
+            if version == '11':
+                version = '10.0'
+                min_version = (10, 0, 22000)
+
             incdirs = glob.glob(os.path.join(platsdk, 'Include', version + '.*.*'))
+
             max_version = ()
             for dir in incdirs:
                 verstring = os.path.basename(dir)
@@ -2345,7 +2354,7 @@ def SdkLocateWindows(version=None):
                     continue
 
                 vertuple = tuple(map(int, verstring.split('.')))
-                if vertuple > max_version:
+                if vertuple > max_version and vertuple > min_version:
                     version = verstring
                     max_version = vertuple
 
@@ -2788,7 +2797,7 @@ def SetupVisualStudioEnviron():
         elif not win_kit.endswith('\\'):
             win_kit += '\\'
 
-        for vnum in 10150, 10240, 10586, 14393, 15063, 16299, 17134, 17763, 18362, 19041:
+        for vnum in 10150, 10240, 10586, 14393, 15063, 16299, 17134, 17763, 18362, 19041, 20348, 22000:
             version = "10.0.{0}.0".format(vnum)
             if os.path.isfile(win_kit + "Include\\" + version + "\\ucrt\\assert.h"):
                 print("Using Universal CRT %s" % (version))
@@ -3517,8 +3526,8 @@ def GetCurrentPythonVersionInfo():
         "soabi": GetPythonABI(),
         "ext_suffix": GetExtensionSuffix(),
         "executable": sys.executable,
-        "purelib": sysconfig.get_path("purelib"),
-        "platlib": sysconfig.get_path("platlib"),
+        "purelib": sysconfig.get_python_lib(False),
+        "platlib": sysconfig.get_python_lib(True),
     }
 
 

+ 7 - 0
makepanda/makewheel.py

@@ -562,6 +562,7 @@ class WheelFile(object):
             print("Adding {0} from {1}".format(target_path, orig_source_path))
 
         zinfo = zipfile.ZipInfo.from_file(source_path, target_path)
+        zinfo.compress_type = self.zip_file.compression
         if zinfo.date_time > self.max_date_time:
             zinfo.date_time = self.max_date_time
 
@@ -653,6 +654,8 @@ def makewheel(version, output_dir, platform=None):
                     platform = platform.replace("linux", "manylinux2010")
                 elif os.path.isfile("/lib/libc-2.17.so") or os.path.isfile("/lib64/libc-2.17.so"):
                     platform = platform.replace("linux", "manylinux2014")
+                elif os.path.isfile("/lib/i386-linux-gnu/libc-2.24.so") or os.path.isfile("/lib/x86_64-linux-gnu/libc-2.24.so"):
+                    platform = platform.replace("linux", "manylinux_2_24")
 
     platform = platform.replace('-', '_').replace('.', '_')
 
@@ -783,6 +786,10 @@ if __debug__:
 
     for file in sorted(os.listdir(ext_mod_dir)):
         if file.endswith(ext_suffix):
+            if file.startswith('_tkinter.'):
+                # Tkinter is supplied in a separate wheel.
+                continue
+
             source_path = os.path.join(ext_mod_dir, file)
 
             if file.endswith('.pyd') and platform.startswith('cygwin'):

+ 6 - 1
makepanda/test_wheel.py

@@ -37,7 +37,12 @@ def test_wheel(wheel, verbose=False):
         sys.exit(1)
 
     # Install pytest into the environment, as well as our wheel.
-    packages = ["pytest", wheel]
+    packages = [wheel]
+    if sys.version_info >= (3, 10):
+        packages += ["pytest>=6.2.4"]
+    else:
+        packages += ["pytest"]
+
     if sys.version_info[0:2] == (3, 4):
         if sys.platform == "win32":
             packages += ["colorama==0.4.1"]

BIN
models/plugin_images/auth_click.png


BIN
models/plugin_images/auth_ready.png


BIN
models/plugin_images/auth_rollover.png


BIN
models/plugin_images/download.png


BIN
models/plugin_images/failed.png


BIN
models/plugin_images/installer.bmp


BIN
models/plugin_images/panda3d.icns


BIN
models/plugin_images/play_click.png


BIN
models/plugin_images/play_ready.png


BIN
models/plugin_images/play_rollover.png


+ 130 - 28
panda/src/androiddisplay/androidGraphicsWindow.cxx

@@ -45,6 +45,7 @@ AndroidGraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe,
                       GraphicsStateGuardian *gsg,
                       GraphicsOutput *host) :
   GraphicsWindow(engine, pipe, name, fb_prop, win_prop, flags, gsg, host),
+  _primary_pointer_down(false),
   _mouse_button_state(0)
 {
   AndroidGraphicsPipe *android_pipe;
@@ -516,12 +517,9 @@ handle_key_event(const AInputEvent *event) {
   // Is it an up or down event?
   int32_t action = AKeyEvent_getAction(event);
   if (action == AKEY_EVENT_ACTION_DOWN) {
-    if (AKeyEvent_getRepeatCount(event) > 0) {
-      _input->button_resume_down(button);
-    } else {
-      _input->button_down(button);
-    }
-  } else if (action == AKEY_EVENT_ACTION_UP) {
+    _input->button_down(button);
+  }
+  else if (action == AKEY_EVENT_ACTION_UP) {
     _input->button_up(button);
   }
   // TODO AKEY_EVENT_ACTION_MULTIPLE
@@ -535,33 +533,101 @@ handle_key_event(const AInputEvent *event) {
 int32_t AndroidGraphicsWindow::
 handle_motion_event(const AInputEvent *event) {
   int32_t action = AMotionEvent_getAction(event);
+  int32_t pointer_index = (action >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
   action &= AMOTION_EVENT_ACTION_MASK;
 
-  if (action == AMOTION_EVENT_ACTION_DOWN ||
-      action == AMOTION_EVENT_ACTION_UP) {
-    // The up event doesn't let us know which button is up, so we need to
-    // keep track of the button state ourselves.
-    int32_t button_state = AMotionEvent_getButtonState(event);
-    if (button_state == 0 && action == AMOTION_EVENT_ACTION_DOWN) {
-      button_state = AMOTION_EVENT_BUTTON_PRIMARY;
+  if (action == AMOTION_EVENT_ACTION_DOWN) {
+    _primary_pointer_down = true;
+  }
+  else if (action == AMOTION_EVENT_ACTION_UP
+        || action == AMOTION_EVENT_ACTION_CANCEL) {
+    _primary_pointer_down = false;
+  }
+
+  int32_t button_state = AMotionEvent_getButtonState(event);
+
+  // Emulate mouse click; as long as the primary pointer is held down,
+  // and no other mouse button is causing it, the primary mouse button
+  // is considered depressed.
+  if (button_state == 0 && _primary_pointer_down &&
+      action != AMOTION_EVENT_ACTION_BUTTON_RELEASE) {
+    button_state |= AMOTION_EVENT_BUTTON_PRIMARY;
+  }
+
+  int32_t changed = _mouse_button_state ^ button_state;
+  if (changed != 0) {
+    if (changed & AMOTION_EVENT_BUTTON_PRIMARY) {
+      if (button_state & AMOTION_EVENT_BUTTON_PRIMARY) {
+        _input->button_down(MouseButton::one());
+      } else {
+        _input->button_up(MouseButton::one());
+      }
     }
-    int32_t changed = _mouse_button_state ^ button_state;
-    if (changed != 0) {
-      if (changed & AMOTION_EVENT_BUTTON_PRIMARY) {
-        if (button_state & AMOTION_EVENT_BUTTON_PRIMARY) {
-          _input->button_down(MouseButton::one());
-        } else {
-          _input->button_up(MouseButton::one());
-        }
+    if (changed & AMOTION_EVENT_BUTTON_SECONDARY) {
+      if (button_state & AMOTION_EVENT_BUTTON_SECONDARY) {
+        _input->button_down(MouseButton::three());
+      } else {
+        _input->button_up(MouseButton::three());
       }
-      if (changed & AMOTION_EVENT_BUTTON_SECONDARY) {
-        if (button_state & AMOTION_EVENT_BUTTON_SECONDARY) {
-          _input->button_down(MouseButton::three());
-        } else {
-          _input->button_up(MouseButton::three());
-        }
+    }
+    if (changed & AMOTION_EVENT_BUTTON_TERTIARY) {
+      if (button_state & AMOTION_EVENT_BUTTON_TERTIARY) {
+        _input->button_down(MouseButton::two());
+      } else {
+        _input->button_up(MouseButton::two());
+      }
+    }
+    if (changed & AMOTION_EVENT_BUTTON_BACK) {
+      if (button_state & AMOTION_EVENT_BUTTON_BACK) {
+        _input->button_down(MouseButton::four());
+      } else {
+        _input->button_up(MouseButton::four());
+      }
+    }
+    if (changed & AMOTION_EVENT_BUTTON_FORWARD) {
+      if (button_state & AMOTION_EVENT_BUTTON_FORWARD) {
+        _input->button_down(MouseButton::five());
+      } else {
+        _input->button_up(MouseButton::five());
+      }
+    }
+    _mouse_button_state = button_state;
+  }
+
+  if (action == AMOTION_EVENT_ACTION_SCROLL) {
+    float v = AMotionEvent_getAxisValue(event, AMOTION_EVENT_AXIS_VSCROLL, pointer_index);
+    float h = AMotionEvent_getAxisValue(event, AMOTION_EVENT_AXIS_HSCROLL, pointer_index);
+
+    if (v > 0.5) {
+      while (v > 0) {
+        _input->button_down(MouseButton::wheel_up());
+        _input->button_up(MouseButton::wheel_up());
+        v -= 1;
+      }
+    }
+    else if (v < 0.5) {
+      while (v < 0) {
+        _input->button_down(MouseButton::wheel_down());
+        _input->button_up(MouseButton::wheel_down());
+        v += 1;
+      }
+    }
+
+    // In my tests the scroll direction is opposite of what the docs indicate,
+    // ie. a positive value is left, not right.
+    if (h > 0.5) {
+      while (h > 0) {
+        _input->button_down(MouseButton::wheel_left());
+        _input->button_up(MouseButton::wheel_left());
+        h -= 1;
+      }
+    }
+    else if (h < 0.5) {
+      while (h < 0) {
+        _input->button_down(MouseButton::wheel_right());
+        _input->button_up(MouseButton::wheel_right());
+        h += 1;
       }
-      _mouse_button_state = button_state;
     }
   }
 
@@ -816,6 +882,42 @@ map_button(int32_t keycode) {
       return KeyboardButton::f12();
     case AKEYCODE_NUM_LOCK:
       return KeyboardButton::num_lock();
+    case AKEYCODE_NUMPAD_0:
+      return KeyboardButton::ascii_key('0');
+    case AKEYCODE_NUMPAD_1:
+      return KeyboardButton::ascii_key('1');
+    case AKEYCODE_NUMPAD_2:
+      return KeyboardButton::ascii_key('2');
+    case AKEYCODE_NUMPAD_3:
+      return KeyboardButton::ascii_key('3');
+    case AKEYCODE_NUMPAD_4:
+      return KeyboardButton::ascii_key('4');
+    case AKEYCODE_NUMPAD_5:
+      return KeyboardButton::ascii_key('5');
+    case AKEYCODE_NUMPAD_6:
+      return KeyboardButton::ascii_key('6');
+    case AKEYCODE_NUMPAD_7:
+      return KeyboardButton::ascii_key('7');
+    case AKEYCODE_NUMPAD_8:
+      return KeyboardButton::ascii_key('8');
+    case AKEYCODE_NUMPAD_9:
+      return KeyboardButton::ascii_key('9');
+    case AKEYCODE_NUMPAD_DIVIDE:
+      return KeyboardButton::ascii_key('/');
+    case AKEYCODE_NUMPAD_MULTIPLY:
+      return KeyboardButton::ascii_key('*');
+    case AKEYCODE_NUMPAD_SUBTRACT:
+      return KeyboardButton::ascii_key('-');
+    case AKEYCODE_NUMPAD_ADD:
+      return KeyboardButton::ascii_key('+');
+    case AKEYCODE_NUMPAD_DOT:
+      return KeyboardButton::ascii_key('.');
+    case AKEYCODE_NUMPAD_COMMA:
+      return KeyboardButton::ascii_key(',');
+    case AKEYCODE_NUMPAD_ENTER:
+      return KeyboardButton::enter();
+    case AKEYCODE_NUMPAD_EQUALS:
+      return KeyboardButton::ascii_key('=');
     default:
       break;
   }

+ 1 - 0
panda/src/androiddisplay/androidGraphicsWindow.h

@@ -71,6 +71,7 @@ private:
   EGLDisplay _egl_display;
   EGLSurface _egl_surface;
 
+  bool _primary_pointer_down;
   int32_t _mouse_button_state;
 
   GraphicsWindowInputDevice *_input;

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

@@ -2158,6 +2158,7 @@ map_raw_key(unsigned short keycode) const {
   case 0x07: return KeyboardButton::ascii_key('x');
   case 0x08: return KeyboardButton::ascii_key('c');
   case 0x09: return KeyboardButton::ascii_key('v');
+  case 0x0A: return KeyboardButton::ascii_key('<');
   case 0x0B: return KeyboardButton::ascii_key('b');
   case 0x0C: return KeyboardButton::ascii_key('q');
   case 0x0D: return KeyboardButton::ascii_key('w');

+ 3 - 1
panda/src/device/evdevInputDevice.cxx

@@ -113,6 +113,8 @@ static const struct DeviceMapping {
   {0x046d, 0xc629, InputDevice::DeviceClass::spatial_mouse, 0},
   // 3Dconnexion Space Mouse Pro
   {0x046d, 0xc62b, InputDevice::DeviceClass::spatial_mouse, 0},
+  // FrSky Simulator
+  {0x0483, 0x5720, InputDevice::DeviceClass::flight_stick, 0},
   {0},
 };
 
@@ -960,7 +962,7 @@ map_button(int code, DeviceClass device_class, int quirks) {
       KeyboardButton::ascii_key('.'),
       ButtonHandle::none(),
       ButtonHandle::none(),
-      ButtonHandle::none(),
+      KeyboardButton::ascii_key('<'),
       KeyboardButton::f11(),
       KeyboardButton::f12(),
       ButtonHandle::none(),

+ 28 - 25
panda/src/device/winRawInputDevice.cxx

@@ -38,9 +38,6 @@ enum QuirkBits : int {
 
   // Axes on the right stick are swapped, using x for y and vice versa.
   QB_right_axes_swapped = 64,
-
-  // Using an RC (drone) controller as a gamepad instead of a flight stick
-  QB_rc_controller = 128,
 };
 
 // Some nonstandard gamepads have different button mappings.
@@ -89,7 +86,7 @@ static const struct DeviceMapping {
     {"face_y", "face_b", "face_a", "face_x", "lshoulder", "rshoulder", "ltrigger", "rtrigger", "back", "start", "lstick", "rstick"}
   },
   // FrSky Simulator
-  {0x0483, 0x5720, InputDevice::DeviceClass::gamepad, QB_rc_controller,
+  {0x0483, 0x5720, InputDevice::DeviceClass::flight_stick, 0,
     {0}
   },
   {0},
@@ -408,7 +405,8 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
           << ", UsagePage=0x" << hex << cap.UsagePage
           << ", Usage=0x" << cap.Range.UsageMin << "..0x" << cap.Range.UsageMax
           << dec << ", LogicalMin=" << cap.LogicalMin
-          << ", LogicalMax=" << cap.LogicalMax << "\n";
+          << ", LogicalMax=" << cap.LogicalMax
+          << ", BitSize=" << cap.BitSize << "\n";
       }
     } else {
       if (device_cat.is_debug()) {
@@ -418,7 +416,8 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
           << ", UsagePage=0x" << hex << cap.UsagePage
           << ", Usage=0x" << cap.NotRange.Usage
           << dec << ", LogicalMin=" << cap.LogicalMin
-          << ", LogicalMax=" << cap.LogicalMax << "\n";
+          << ", LogicalMax=" << cap.LogicalMax
+          << ", BitSize=" << cap.BitSize << "\n";
       }
     }
 
@@ -431,7 +430,7 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
 
       // My gamepads give this odd invalid range.
       if (cap.LogicalMin == 0 && cap.LogicalMax == -1) {
-        cap.LogicalMax = 65535;
+        cap.LogicalMax = (1 << cap.BitSize) - 1;
         is_signed = false;
       }
 
@@ -441,11 +440,7 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
         switch (usage) {
           case HID_USAGE_GENERIC_X:
           if (_device_class == DeviceClass::gamepad) {
-            if (quirks & QB_rc_controller) {
-              axis = Axis::right_x;
-            } else {
-              axis = Axis::left_x;
-            }
+            axis = Axis::left_x;
           } else if (_device_class == DeviceClass::flight_stick) {
             axis = Axis::roll;
           } else {
@@ -454,12 +449,8 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
           break;
         case HID_USAGE_GENERIC_Y:
           if (_device_class == DeviceClass::gamepad) {
-            if (quirks & QB_rc_controller) {
-              axis = Axis::right_y;
-            } else {
-              axis = Axis::left_y;
-              swap(cap.LogicalMin, cap.LogicalMax);
-            }
+            axis = Axis::left_y;
+            swap(cap.LogicalMin, cap.LogicalMax);
           } else if (_device_class == DeviceClass::flight_stick) {
             axis = Axis::pitch;
           } else {
@@ -476,8 +467,6 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
               } else {
                 axis = InputDevice::Axis::right_x;
               }
-            } else if (quirks & QB_rc_controller) {
-              axis = InputDevice::Axis::left_y;
             } else if ((quirks & QB_no_analog_triggers) == 0) {
               axis = Axis::left_trigger;
             }
@@ -500,8 +489,6 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
               if ((quirks & QB_no_analog_triggers) == 0) {
                 axis = Axis::left_trigger;
               }
-            } else if (quirks & QB_rc_controller) {
-              axis = Axis::left_x;
             } else {
               axis = Axis::right_x;
             }
@@ -577,6 +564,17 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
         }
       }
 
+      int sign_bit = 0;
+      if (cap.BitSize < 32) {
+        if (cap.LogicalMin < 0) {
+          sign_bit = 1 << (cap.BitSize - 1);
+        }
+        else if (is_signed) {
+          //XXX is this still necessary?
+          sign_bit = (1 << 15);
+        }
+      }
+
       int axis_index;
       if (!is_signed) {
         // All axes on the weird XInput-style mappings go from -1 to 1
@@ -584,7 +582,7 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
       } else {
         axis_index = add_axis(axis, cap.LogicalMin, cap.LogicalMax);
       }
-      _indices[data_index] = Index::axis(axis_index, is_signed);
+      _indices[data_index] = Index::axis(axis_index, sign_bit);
     }
   }
 
@@ -700,8 +698,13 @@ process_report(PCHAR ptr, size_t size) {
 
         const Index &idx = _indices[data[di].DataIndex];
         if (idx._axis >= 0) {
-          if (idx._signed) {
-            axis_changed(idx._axis, (SHORT)data[di].RawValue);
+          if (idx._sign_bit != 0) {
+            // Sign extend.
+            int value = data[di].RawValue;
+            if (value & idx._sign_bit) {
+              value -= (idx._sign_bit << 1);
+            }
+            axis_changed(idx._axis, value);
           } else {
             axis_changed(idx._axis, data[di].RawValue);
           }

+ 3 - 3
panda/src/device/winRawInputDevice.h

@@ -59,16 +59,16 @@ private:
       idx._button = index;
       return idx;
     }
-    static Index axis(int index, bool is_signed=true) {
+    static Index axis(int index, int sign_bit = 0) {
       Index idx;
       idx._axis = index;
-      idx._signed = is_signed;
+      idx._sign_bit = sign_bit;
       return idx;
     }
 
     int _button;
     int _axis;
-    bool _signed;
+    int _sign_bit;
   };
 
   // Maps a "data index" to either button index or axis index.

+ 29 - 0
panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx

@@ -2071,6 +2071,35 @@ do_framebuffer_copy_to_ram(Texture *tex, int view, int z,
 
     backbuffer -> GetDesc (&surface_description);
 
+    // We can't directly call GetRenderTargetData on a multisampled buffer.
+    // Instead, blit it into a temporary target.
+    if (surface_description.MultiSampleType != D3DMULTISAMPLE_NONE) {
+      IDirect3DSurface9 *resolved;
+      hr = _d3d_device->CreateRenderTarget(surface_description.Width,
+                                           surface_description.Height,
+                                           surface_description.Format,
+                                           D3DMULTISAMPLE_NONE, 0, FALSE,
+                                           &resolved, nullptr);
+      if (FAILED(hr)) {
+        dxgsg9_cat.error()
+          << "CreateRenderTarget failed" << D3DERRORSTRING(hr) << "\n";
+        backbuffer->Release();
+        return false;
+      }
+
+      _d3d_device->StretchRect(backbuffer, nullptr, resolved, nullptr, D3DTEXF_NONE);
+      if (FAILED(hr)) {
+        dxgsg9_cat.error()
+          << "StretchRect failed" << D3DERRORSTRING(hr) << "\n";
+        backbuffer->Release();
+        resolved->Release();
+        return false;
+      }
+
+      backbuffer->Release();
+      backbuffer = resolved;
+    }
+
     pool = D3DPOOL_SYSTEMMEM;
     hr = _d3d_device->CreateOffscreenPlainSurface(
                                                   surface_description.Width,

+ 20 - 4
panda/src/dxgsg9/dxTextureContext9.cxx

@@ -427,10 +427,26 @@ create_texture(DXScreenData &scrn) {
   }
 
   if (compress_texture) {
-    if (num_color_channels == 1) {
-      CHECK_FOR_FMT(ATI1);
-    } else if (num_alpha_bits == 0 && num_color_channels == 2) {
-      CHECK_FOR_FMT(ATI2);
+    // The ATI1 and ATI2 formats can't be compressed by the driver.
+    // Only choose them if we can compress them on the CPU.
+    // Also, don't choose ATI for a luminance texture, since it gets read as
+    // a texture with just a red channel.
+    if (num_alpha_bits == 0 && !needs_luminance) {
+      if (num_color_channels == 1) {
+        if (scrn._supported_tex_formats_mask & ATI1_FLAG) {
+          if (tex->compress_ram_image(Texture::CM_rgtc)) {
+            target_pixel_format = D3DFMT_ATI1;
+            goto found_matching_format;
+          }
+        }
+      } else if (num_color_channels == 2) {
+        if (scrn._supported_tex_formats_mask & ATI2_FLAG) {
+          if (tex->compress_ram_image(Texture::CM_rgtc)) {
+            target_pixel_format = D3DFMT_ATI2;
+            goto found_matching_format;
+          }
+        }
+      }
     }
     if (num_alpha_bits <= 1) {
       CHECK_FOR_FMT(DXT1);

+ 11 - 12
panda/src/egg2pg/eggRenderState.cxx

@@ -162,18 +162,17 @@ fill_state(EggPrimitive *egg_prim) {
         // transform, never bake it in.  In fact, we don't even care about its
         // UV's in this case, since we won't be using them.
         tex_mat_attrib = apply_tex_mat(tex_mat_attrib, def._stage, egg_tex);
-
-      } else {
-/*
- * Otherwise, we need to record that there is at least one texture on this
- * particular UV name and with this particular texture matrix.  If there are
- * no other textures, or if all of the other textures use the same texture
- * matrix, then tex_mats[uv_name].size() will remain 1 (which tells us we can
- * bake in the texture matrix to the UV's).  On the other hand, if there is
- * another texture on the same uv name but with a different transform, it will
- * increase tex_mats[uv_name].size() to at least 2, indicating we can't bake
- * in the texture matrix.
- */
+      }
+      else {
+        // Otherwise, we need to record that there is at least one texture on
+        // this particular UV name and with this particular texture matrix.
+        // If there are no other textures, or if all of the other textures use
+        // the same texture matrix, then tex_mats[uv_name].size() will remain
+        // 1 (which tells us we can bake in the texture matrix to the UV's).
+        // On the other hand, if there is another texture on the same uv name
+        // but with a different transform, it will increase
+        // tex_mats[uv_name].size() to at least 2, indicating we can't bake in
+        // the texture matrix.
         tex_mats[uv_name][egg_tex->get_transform3d()].push_back(&def);
       }
     }

+ 42 - 0
panda/src/egg2pg/eggSaver.cxx

@@ -800,6 +800,9 @@ convert_primitive(const GeomVertexData *vertex_data,
   // Check for a texture.
   const TextureAttrib *ta;
   if (net_state->get_attrib(ta)) {
+    const TexMatrixAttrib *tma = nullptr;
+    net_state->get_attrib(tma);
+
     for (size_t i = 0; i < ta->get_num_on_stages(); ++i) {
       TextureStage *tex_stage = ta->get_on_stage(i);
 
@@ -867,6 +870,45 @@ convert_primitive(const GeomVertexData *vertex_data,
           egg_tex->set_uv_name(name->get_basename());
         }
 
+        if (tma != nullptr && tma->has_stage(tex_stage)) {
+          CPT(TransformState) transform = tma->get_transform(tex_stage);
+          if (!transform->is_identity()) {
+            if (transform->has_components()) {
+              // If the transform can be represented componentwise, we prefer storing
+              // it that way in the egg file.
+              const LVecBase3 &scale = transform->get_scale();
+              const LQuaternion &quat = transform->get_quat();
+              const LVecBase3 &pos = transform->get_pos();
+              if (!scale.almost_equal(LVecBase3(1.0f, 1.0f, 1.0f))) {
+                if (transform->is_2d()) {
+                  egg_tex->add_scale2d(LVecBase2d(scale[0], scale[1]));
+                } else {
+                  egg_tex->add_scale3d(LCAST(double, scale));
+                }
+              }
+              if (!quat.is_identity()) {
+                if (transform->is_2d()) {
+                  egg_tex->add_rotate2d(transform->get_rotate2d());
+                } else {
+                  egg_tex->add_rotate3d(LCAST(double, quat));
+                }
+              }
+              if (!pos.almost_equal(LVecBase3::zero())) {
+                if (transform->is_2d()) {
+                  egg_tex->add_translate2d(LVector2d(pos[0], pos[1]));
+                } else {
+                  egg_tex->add_translate3d(LCAST(double, pos));
+                }
+              }
+            }
+            else if (transform->has_mat()) {
+              // Otherwise, we store the raw matrix.
+              const LMatrix4 &mat = transform->get_mat();
+              egg_tex->set_transform3d(LCAST(double, mat));
+            }
+          }
+        }
+
         egg_prim->add_texture(egg_tex);
       }
     }

+ 7 - 0
panda/src/framework/windowFramework.cxx

@@ -340,6 +340,13 @@ get_pixel_2d() {
     _pixel_2d = get_render_2d().attach_new_node(top);
     _pixel_2d.set_pos(-1, 0, 1);
 
+    // Tell the PGTop about our MouseWatcher object, so the PGui system can
+    // operate.
+    PandaNode *mouse_node = get_mouse().node();
+    if (mouse_node->is_of_type(MouseWatcher::get_class_type())) {
+      top->set_mouse_watcher(DCAST(MouseWatcher, mouse_node));
+    }
+
     if (_window->has_size()) {
       int x_size = _window->get_sbs_left_x_size();
       int y_size = _window->get_sbs_left_y_size();

+ 4 - 6
panda/src/glstuff/glGraphicsStateGuardian_src.cxx

@@ -850,12 +850,6 @@ reset() {
     Geom::GR_line_strip |
     Geom::GR_flat_last_vertex;
 
-#ifndef OPENGLES
-  if (_supports_geometry_shaders) {
-    _supported_geom_rendering |= Geom::GR_adjacency;
-  }
-#endif
-
   _supports_point_parameters = false;
 
 #ifdef OPENGLES_1
@@ -1809,6 +1803,10 @@ reset() {
     _supports_geometry_shaders = false;
     _glFramebufferTexture = nullptr;
   }
+
+  if (_supports_geometry_shaders) {
+    _supported_geom_rendering |= Geom::GR_adjacency;
+  }
 #endif
   _shader_caps._supports_glsl = _supports_glsl;
 

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

@@ -2538,6 +2538,11 @@ update_shader_vertex_arrays(ShaderContext *prev, bool force) {
                  _glgsg->_glVertexAttribI4ui != nullptr) {
           _glgsg->_glVertexAttribI4ui(p, 0, 1, 2, 3);
         }
+        else if (name == InternalName::get_transform_weight()) {
+          // NVIDIA doesn't seem to use to use these defaults by itself
+          static const GLfloat weights[4] = {0, 0, 0, 1};
+          _glgsg->_glVertexAttrib4fv(p, weights);
+        }
         else if (name == InternalName::get_instance_matrix()) {
           const LMatrix4 &ident_mat = LMatrix4::ident_mat();
 

+ 8 - 0
panda/src/gobj/geomPrimitive.I

@@ -604,6 +604,14 @@ get_modified() const {
   return _cdata->_modified;
 }
 
+/**
+ *
+ */
+INLINE const GeomVertexArrayData *GeomPrimitivePipelineReader::
+get_vertices() const {
+  return _vertices.p();
+}
+
 /**
  *
  */

+ 19 - 0
panda/src/gobj/geomPrimitive.cxx

@@ -2362,6 +2362,25 @@ get_num_primitives() const {
   }
 }
 
+/**
+ *
+ */
+int GeomPrimitivePipelineReader::
+get_num_faces() const {
+  int num_vertices_per_primitive = _object->get_num_vertices_per_primitive();
+
+  if (num_vertices_per_primitive == 0) {
+    int num_primitives = _cdata->_ends.size();
+    int num_vertices = get_num_vertices();
+    int min_num_vertices_per_primitive = _object->get_min_num_vertices_per_primitive();
+    int num_unused_vertices_per_primitive = _object->get_num_unused_vertices_per_primitive();
+    return num_vertices - (num_primitives * (min_num_vertices_per_primitive - 1)) - ((num_primitives - 1) * num_unused_vertices_per_primitive);
+  } else {
+    // Same as the number of primitives.
+    return (get_num_vertices() / num_vertices_per_primitive);
+  }
+}
+
 /**
  * Turns on all the bits corresponding to the vertices that are referenced
  * by this GeomPrimitive.

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

@@ -372,12 +372,14 @@ public:
   INLINE int get_num_vertices() const;
   int get_vertex(int i) const;
   int get_num_primitives() const;
+  int get_num_faces() const;
   void get_referenced_vertices(BitArray &bits) const;
   INLINE int get_min_vertex() const;
   INLINE int get_max_vertex() const;
   INLINE int get_data_size_bytes() const;
   INLINE UpdateSeq get_modified() const;
   bool check_valid(const GeomVertexDataPipelineReader *data_reader) const;
+  INLINE const GeomVertexArrayData *get_vertices() const;
   INLINE int get_index_stride() const;
   INLINE const unsigned char *get_read_pointer(bool force) const;
   INLINE int get_strip_cut_index() const;

+ 18 - 0
panda/src/gobj/texture.cxx

@@ -3680,6 +3680,12 @@ do_read_txo_file(CData *cdata, const Filename &fullpath) {
   }
 
   istream *in = file->open_read_file(true);
+  if (in == nullptr) {
+    gobj_cat.error()
+      << "Failed to open " << filename << " for reading.\n";
+    return false;
+  }
+
   bool success = do_read_txo(cdata, *in, fullpath);
   vfs->close_read_file(in);
 
@@ -3735,6 +3741,12 @@ do_read_dds_file(CData *cdata, const Filename &fullpath, bool header_only) {
   }
 
   istream *in = file->open_read_file(true);
+  if (in == nullptr) {
+    gobj_cat.error()
+      << "Failed to open " << filename << " for reading.\n";
+    return false;
+  }
+
   bool success = do_read_dds(cdata, *in, fullpath, header_only);
   vfs->close_read_file(in);
 
@@ -4415,6 +4427,12 @@ do_read_ktx_file(CData *cdata, const Filename &fullpath, bool header_only) {
   }
 
   istream *in = file->open_read_file(true);
+  if (in == nullptr) {
+    gobj_cat.error()
+      << "Failed to open " << filename << " for reading.\n";
+    return false;
+  }
+
   bool success = do_read_ktx(cdata, *in, fullpath, header_only);
   vfs->close_read_file(in);
 

+ 2 - 2
panda/src/gobj/texturePeeker.cxx

@@ -436,7 +436,7 @@ lookup_bilinear(LColor &color, PN_stdfloat u, PN_stdfloat v) const {
  * rectangle defined by the specified coordinate range.
  *
  * The texel color is linearly filtered over the entire region.  u, v, and w
- * will wrap around regardless of the texture's wrap mode.
+ * must be in the range [0, 1].
  */
 void TexturePeeker::
 filter_rect(LColor &color,
@@ -464,7 +464,7 @@ filter_rect(LColor &color,
  * rectangle defined by the specified coordinate range.
  *
  * The texel color is linearly filtered over the entire region.  u, v, and w
- * will wrap around regardless of the texture's wrap mode.
+ * must be in the range [0, 1].
  */
 void TexturePeeker::
 filter_rect(LColor &color,

+ 2 - 0
panda/src/pgraph/lightAttrib.cxx

@@ -469,6 +469,7 @@ replace_on_light(const NodePath &source, const NodePath &dest) const {
     slobj->attrib_unref();
 
     *it = dest;
+    attrib->_on_lights.sort();
   }
   return return_new(attrib);
 }
@@ -531,6 +532,7 @@ replace_off_light(const NodePath &source, const NodePath &dest) const {
     slobj->attrib_unref();
 
     *it = dest;
+    attrib->_off_lights.sort();
   }
   return return_new(attrib);
 }

+ 1 - 1
panda/src/pgraph/pandaNode.cxx

@@ -1306,7 +1306,7 @@ compare_tags(const PandaNode *other) const {
       return cmp;
     }
 
-    cmp = strcmp(a_data.get_key(ai).c_str(), b_data.get_key(bi).c_str());
+    cmp = strcmp(a_data.get_data(ai).c_str(), b_data.get_data(bi).c_str());
     if (cmp != 0) {
       return cmp;
     }

+ 4 - 0
panda/src/pgraph/pythonLoaderFileType.cxx

@@ -158,8 +158,12 @@ init(PyObject *loader) {
     }
     Py_DECREF(supports_compressed);
   }
+  else {
+    PyErr_Clear();
+  }
 
   _load_func = PyObject_GetAttrString(loader, "load_file");
+  PyErr_Clear();
   _save_func = PyObject_GetAttrString(loader, "save_file");
   PyErr_Clear();
 

+ 36 - 41
panda/src/pgraphnodes/sceneGraphAnalyzer.cxx

@@ -381,6 +381,7 @@ collect_statistics(GeomNode *geom_node) {
  */
 void SceneGraphAnalyzer::
 collect_statistics(const Geom *geom) {
+  Thread *current_thread = Thread::get_current_thread();
   CPT(GeomVertexData) vdata = geom->get_vertex_data();
   std::pair<VDatas::iterator, bool> result = _vdatas.insert(VDatas::value_type(vdata, VDataTracker()));
   if (result.second) {
@@ -408,7 +409,7 @@ collect_statistics(const Geom *geom) {
     }
     if (format->has_column(InternalName::get_normal())) {
       _num_normals += num_rows;
-      GeomVertexReader rnormal(vdata, InternalName::get_normal());
+      GeomVertexReader rnormal(vdata, InternalName::get_normal(), current_thread);
       while (!rnormal.is_at_end()) {
         LVector3f normal = rnormal.get_data3f();
         float length = normal.length();
@@ -439,52 +440,46 @@ collect_statistics(const Geom *geom) {
   int num_primitives = geom->get_num_primitives();
   for (int i = 0; i < num_primitives; ++i) {
     CPT(GeomPrimitive) prim = geom->get_primitive(i);
+    GeomPrimitivePipelineReader reader(prim, current_thread);
+    reader.get_referenced_vertices(tracker._referenced_vertices);
 
-    int num_vertices = prim->get_num_vertices();
-    int strip_cut_index = prim->get_strip_cut_index();
-    for (int vi = 0; vi < num_vertices; ++vi) {
-      int index = prim->get_vertex(vi);
-      if (index != strip_cut_index) {
-        tracker._referenced_vertices.set_bit(index);
-      }
-    }
-
-    if (prim->is_indexed()) {
-      collect_prim_statistics(prim->get_vertices());
+    if (reader.is_indexed()) {
+      collect_prim_statistics(reader.get_vertices());
       if (prim->is_composite()) {
-        collect_statistics(prim->get_mins());
-        collect_statistics(prim->get_maxs());
+        reader.check_minmax();
+        collect_statistics(reader.get_mins());
+        collect_statistics(reader.get_maxs());
       }
     }
 
     if (prim->is_of_type(GeomPoints::get_class_type())) {
-      _num_points += prim->get_num_primitives();
-
-    } else if (prim->is_of_type(GeomLines::get_class_type())) {
-      _num_lines += prim->get_num_primitives();
-
-    } else if (prim->is_of_type(GeomLinestrips::get_class_type())) {
-      _num_lines += prim->get_num_faces();
-
-    } else if (prim->is_of_type(GeomTriangles::get_class_type())) {
-      _num_tris += prim->get_num_primitives();
-      _num_individual_tris += prim->get_num_primitives();
-
-    } else if (prim->is_of_type(GeomTristrips::get_class_type())) {
-      _num_tris += prim->get_num_faces();
-      _num_tristrips += prim->get_num_primitives();
-      _num_triangles_in_strips += prim->get_num_faces();
-
-    } else if (prim->is_of_type(GeomTrifans::get_class_type())) {
-      _num_tris += prim->get_num_faces();
-      _num_trifans += prim->get_num_primitives();
-      _num_triangles_in_fans += prim->get_num_faces();
-
-    } else if (prim->is_of_type(GeomPatches::get_class_type())) {
-      _num_patches += prim->get_num_primitives();
-      _num_vertices_in_patches += prim->get_num_vertices();
-
-    } else {
+      _num_points += reader.get_num_primitives();
+    }
+    else if (prim->is_of_type(GeomLines::get_class_type())) {
+      _num_lines += reader.get_num_primitives();
+    }
+    else if (prim->is_of_type(GeomLinestrips::get_class_type())) {
+      _num_lines += reader.get_num_faces();
+    }
+    else if (prim->is_of_type(GeomTriangles::get_class_type())) {
+      _num_tris += reader.get_num_primitives();
+      _num_individual_tris += reader.get_num_primitives();
+    }
+    else if (prim->is_of_type(GeomTristrips::get_class_type())) {
+      _num_tris += reader.get_num_faces();
+      _num_tristrips += reader.get_num_primitives();
+      _num_triangles_in_strips += reader.get_num_faces();
+    }
+    else if (prim->is_of_type(GeomTrifans::get_class_type())) {
+      _num_tris += reader.get_num_faces();
+      _num_trifans += reader.get_num_primitives();
+      _num_triangles_in_fans += reader.get_num_faces();
+    }
+    else if (prim->is_of_type(GeomPatches::get_class_type())) {
+      _num_patches += reader.get_num_primitives();
+      _num_vertices_in_patches += reader.get_num_vertices();
+    }
+    else {
       pgraph_cat.warning()
         << "Unknown GeomPrimitive type in SceneGraphAnalyzer: "
         << prim->get_type() << "\n";

+ 50 - 4
panda/src/pnmimage/pnmBrush.cxx

@@ -142,6 +142,32 @@ public:
   }
 };
 
+// Adds a value to the pixel.
+class EXPCL_PANDA_PNMIMAGE PNMAddPixelBrush : public PNMPixelBrush {
+public:
+  PNMAddPixelBrush(const LColorf &color) : PNMPixelBrush(color) { }
+
+  virtual void draw(PNMImage &image, int x, int y, float pixel_scale) {
+    if (x >= 0 && x < image.get_x_size() &&
+        y >= 0 && y < image.get_y_size()) {
+      image.set_xel_a(x, y,
+        image.get_xel_a(x, y) + (_color * pixel_scale));
+    }
+  }
+
+  virtual void fill(PNMImage &image, int xfrom, int xto, int y,
+                    int xo, int yo) {
+    if (y >= 0 && y < image.get_y_size()) {
+      xfrom = max(xfrom, 0);
+      xto = max(xto, image.get_x_size() - 1);
+      for (int x = xfrom; x <= xto; ++x) {
+        image.set_xel_a(x, y,
+          image.get_xel_a(x, y) + _color);
+      }
+    }
+  }
+};
+
 // A PNMImageBrush is a family of brushes that draw an image at a time.
 class EXPCL_PANDA_PNMIMAGE PNMImageBrush : public PNMBrush {
 protected:
@@ -244,6 +270,22 @@ public:
   }
 };
 
+// Adds a constant value to the pixels
+class EXPCL_PANDA_PNMIMAGE PNMAddImageBrush : public PNMImageBrush {
+public:
+  PNMAddImageBrush(const PNMImage &image, float xc, float yc) :
+    PNMImageBrush(image, xc, yc) { }
+
+  virtual void draw(PNMImage &image, int x, int y, float pixel_scale) {
+    image.add_sub_image(_image, x, y, 0, 0, -1, -1, pixel_scale);
+  }
+
+  virtual void do_scanline(PNMImage &image, int xto, int yto,
+                           int xfrom, int yfrom, int x_size, int y_size) {
+    image.add_sub_image(_image, xto, yto, xfrom, yfrom, x_size, y_size);
+  }
+};
+
 /**
  *
  */
@@ -278,6 +320,9 @@ make_pixel(const LColorf &color, PNMBrush::BrushEffect effect) {
 
   case BE_lighten:
     return new PNMLightenPixelBrush(color);
+
+  case BE_add:
+    return new PNMAddPixelBrush(color);
   }
 
   pnmimage_cat.error()
@@ -296,6 +341,8 @@ make_spot(const LColorf &color, float radius, bool fuzzy,
 
   switch (effect) {
   case BE_set:
+  case BE_lighten:
+  case BE_add:
     bg.set(0, 0, 0, 0);
     break;
 
@@ -307,10 +354,6 @@ make_spot(const LColorf &color, float radius, bool fuzzy,
     bg.set(1, 1, 1, 1);
     break;
 
-  case BE_lighten:
-    bg.set(0, 0, 0, 0);
-    break;
-
   default:
     pnmimage_cat.error()
       << "**Invalid BrushEffect (" << (int)effect << ")**\n";
@@ -351,6 +394,9 @@ make_image(const PNMImage &image, float xc, float yc,
 
   case BE_lighten:
     return new PNMLightenImageBrush(image, xc, yc);
+
+  case BE_add:
+    return new PNMAddImageBrush(image, xc, yc);
   }
 
   pnmimage_cat.error()

+ 1 - 0
panda/src/pnmimage/pnmBrush.h

@@ -45,6 +45,7 @@ PUBLISHED:
     BE_blend,
     BE_darken,
     BE_lighten,
+    BE_add,
   };
 
   static PT(PNMBrush) make_transparent();

+ 3 - 2
panda/src/pnmimage/pnmImage.cxx

@@ -1980,13 +1980,14 @@ quantize(size_t max_colors) {
  * PerlinNoise2 class in mathutil.
  */
 void PNMImage::
-perlin_noise_fill(float sx, float sy, int table_size, unsigned long seed) {
+perlin_noise_fill(float sx, float sy, int table_size, unsigned long seed,
+                  float ox, float oy) {
   float x, y;
   float noise;
   PerlinNoise2 perlin (sx * _x_size, sy * _y_size, table_size, seed);
   for (x = 0; x < _x_size; ++x) {
     for (y = 0; y < _y_size; ++y) {
-      noise = perlin.noise(x, y);
+      noise = perlin.noise(x + ox, y + oy);
       set_xel(x, y, 0.5 * (noise + 1.0));
     }
   }

+ 2 - 1
panda/src/pnmimage/pnmImage.h

@@ -258,7 +258,8 @@ PUBLISHED:
   void make_histogram(Histogram &hist);
   void quantize(size_t max_colors);
   BLOCKING void perlin_noise_fill(float sx, float sy, int table_size = 256,
-                                  unsigned long seed = 0);
+                                  unsigned long seed = 0,
+                                  float ox = 0, float oy = 0);
   void perlin_noise_fill(StackedPerlinNoise2 &perlin);
 
   void remix_channels(const LMatrix4 &conv);

+ 47 - 10
panda/src/windisplay/winGraphicsWindow.cxx

@@ -33,6 +33,10 @@
 #define WM_TOUCH 0x0240
 #endif
 
+#ifndef WM_MOUSEHWHEEL
+#define WM_MOUSEHWHEEL 0x020E
+#endif
+
 #if WINVER < 0x0601
 // Not used on Windows XP, but we still need to define it.
 #define TOUCH_COORD_TO_PIXEL(l) ((l) / 100)
@@ -283,6 +287,17 @@ process_events() {
  */
 void WinGraphicsWindow::
 set_properties_now(WindowProperties &properties) {
+  if (properties.has_fullscreen() && !properties.get_fullscreen() &&
+      is_fullscreen()) {
+    if (do_windowed_switch()) {
+      _properties.set_fullscreen(false);
+      properties.clear_fullscreen();
+    } else {
+      windisplay_cat.warning()
+        << "Switching to windowed mode failed!\n";
+    }
+  }
+
   GraphicsWindow::set_properties_now(properties);
   if (!properties.is_any_specified()) {
     // The base class has already handled this case.
@@ -437,14 +452,6 @@ set_properties_now(WindowProperties &properties) {
         windisplay_cat.warning()
           << "Switching to fullscreen mode failed!\n";
       }
-    } else if (!properties.get_fullscreen() && is_fullscreen()){
-      if (do_windowed_switch()){
-        _properties.set_fullscreen(false);
-        properties.clear_fullscreen();
-      } else {
-        windisplay_cat.warning()
-          << "Switching to windowed mode failed!\n";
-      }
     }
   }
 
@@ -1808,6 +1815,31 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     }
     break;
 
+  case WM_MOUSEHWHEEL:
+    {
+      int delta = GET_WHEEL_DELTA_WPARAM(wparam);
+
+      POINT point;
+      GetCursorPos(&point);
+      ScreenToClient(hwnd, &point);
+      double time = get_message_time();
+
+      if (delta >= 0) {
+        while (delta > 0) {
+          handle_keypress(MouseButton::wheel_right(), point.x, point.y, time);
+          handle_keyrelease(MouseButton::wheel_right(), time);
+          delta -= WHEEL_DELTA;
+        }
+      } else {
+        while (delta < 0) {
+          handle_keypress(MouseButton::wheel_left(), point.x, point.y, time);
+          handle_keyrelease(MouseButton::wheel_left(), time);
+          delta += WHEEL_DELTA;
+        }
+      }
+      return 0;
+    }
+    break;
 
   case WM_IME_SETCONTEXT:
     if (!ime_hide)
@@ -2651,6 +2683,10 @@ lookup_key(WPARAM wparam) const {
   case VK_LMENU: return KeyboardButton::lalt();
   case VK_RMENU: return KeyboardButton::ralt();
 
+  case VK_LWIN: return KeyboardButton::lmeta();
+  case VK_RWIN: return KeyboardButton::rmeta();
+  case VK_APPS: return KeyboardButton::menu();
+
   default:
     int key = MapVirtualKey(wparam, 2);
     if (isascii(key) && key != 0) {
@@ -2796,6 +2832,7 @@ lookup_raw_key(LPARAM lparam) const {
 
   // A few additional keys don't fit well in the above table.
   switch (vsc) {
+  case 86: return KeyboardButton::ascii_key('<'); // Between lshift and z
   case 87: return KeyboardButton::f11();
   case 88: return KeyboardButton::f12();
   default: return ButtonHandle::none();
@@ -2815,10 +2852,10 @@ get_keyboard_map() const {
 
   wchar_t text[256];
   UINT vsc = 0;
-  unsigned short ex_vsc[] = {0x57, 0x58,
+  unsigned short ex_vsc[] = {0x56, 0x57, 0x58,
     0x011c, 0x011d, 0x0135, 0x0137, 0x0138, 0x0145, 0x0147, 0x0148, 0x0149, 0x014b, 0x014d, 0x014f, 0x0150, 0x0151, 0x0152, 0x0153, 0x015b, 0x015c, 0x015d};
 
-  for (int k = 1; k < 84 + 17; ++k) {
+  for (int k = 1; k < 84 + sizeof(ex_vsc) / sizeof(short); ++k) {
     if (k >= 84) {
       vsc = ex_vsc[k - 84];
     } else {

+ 2 - 0
panda/src/x11display/x11GraphicsWindow.cxx

@@ -2230,6 +2230,8 @@ get_mouse_button(XButtonEvent &button_event) {
     return MouseButton::wheel_left();
   } else if (index == x_wheel_right_button) {
     return MouseButton::wheel_right();
+  } else if (index >= 8) {
+    return MouseButton::button(index - 5);
   } else {
     return MouseButton::button(index - 1);
   }

+ 35 - 14
pandatool/src/mayaprogs/mayapath.cxx

@@ -58,15 +58,6 @@ using std::string;
 #define QUOTESTR(x) #x
 #define TOSTRING(x) QUOTESTR(x)
 
-#if defined(_WIN32)
-// Note: Filename::dso_filename changes .so to .dll automatically.
-static const Filename openmaya_filename = "bin/OpenMaya.so";
-#elif defined(IS_OSX)
-static const Filename openmaya_filename = "MacOS/libOpenMaya.dylib";
-#else
-static const Filename openmaya_filename = "lib/libOpenMaya.so";
-#endif  // _WIN32
-
 // Searches for python26.zip or whatever version it is.
 static Filename
 find_pyzip(const Filename &maya_location) {
@@ -122,6 +113,25 @@ get_version_number(const char *ver) {
   return 0;
 }
 
+static Filename
+get_openmaya_filename(const Filename &maya_location) {
+#ifdef _WIN32
+  // Note: Filename::dso_filename changes .so to .dll automatically.
+  // Maya 2022 has two versions of OpenMaya.dll, one for Python 3 and
+  // one for Python 2, in bin3 and bin2 folders.
+  Filename bin3 = Filename(maya_location, "bin3");
+  Filename bin3_openmaya = Filename::dso_filename(maya_location / "bin3/OpenMaya.so");
+  if (bin3_openmaya.is_regular_file()) {
+    return bin3_openmaya;
+  }
+  return Filename::dso_filename(maya_location / "bin/OpenMaya.so");
+#elif defined(IS_OSX)
+  return Filename::dso_filename(maya_location / "MacOS/libOpenMaya.dylib");
+#else
+  return Filename::dso_filename(maya_location / "lib/libOpenMaya.so");
+#endif  // _WIN32
+}
+
 #if defined(_WIN32)
 static void
 get_maya_location(const char *ver, string &loc) {
@@ -265,8 +275,8 @@ main(int argc, char *argv[]) {
     } else if (maya_location != standard_maya_location) {
       // If it *is* set, we verify that OpenMaya.dll matches the standard
       // version.
-      Filename openmaya_given = Filename::dso_filename(Filename(maya_location, openmaya_filename));
-      Filename openmaya_standard = Filename::dso_filename(Filename(standard_maya_location, openmaya_filename));
+      Filename openmaya_given = get_openmaya_filename(maya_location);
+      Filename openmaya_standard = get_openmaya_filename(standard_maya_location);
 
       if (openmaya_given != openmaya_standard) {
 #ifdef HAVE_OPENSSL
@@ -335,9 +345,9 @@ main(int argc, char *argv[]) {
   }
 
   // Look for OpenMaya.dll as a sanity check.
-  Filename openmaya = Filename::dso_filename(Filename(maya_location, openmaya_filename));
+  Filename openmaya = get_openmaya_filename(maya_location);
   if (!openmaya.is_regular_file()) {
-    cerr << "Could not find $MAYA_LOCATION/" << Filename::dso_filename(openmaya_filename).to_os_specific() << "!\n";
+    cerr << "Could not find OpenMaya library in $MAYA_LOCATION!\n";
     exit(1);
   }
 
@@ -395,7 +405,18 @@ main(int argc, char *argv[]) {
     if (path == nullptr) {
       path = "";
     }
-    string putenv_str = "PATH=" + bin.to_os_specific() + sep + path;
+    string putenv_str = "PATH=";
+
+    // On Windows, there may also be a bin3 or bin2 directory, we should
+    // add either one to the PATH.
+#ifdef _WIN32
+    Filename bin3 = Filename(maya_location, "bin3");
+    if (bin3.is_directory()) {
+      putenv_str += bin3.to_os_specific() + sep;
+    }
+#endif
+    putenv_str += bin.to_os_specific() + sep + path;
+
     char *putenv_cstr = strdup(putenv_str.c_str());
     putenv(putenv_cstr);
   }

+ 51 - 0
tests/pgraph/test_nodepath.py

@@ -176,6 +176,57 @@ def test_weak_nodepath_comparison():
     assert weak.node() == path.node()
 
 
+def test_nodepath_flatten_tags_identical():
+    from panda3d.core import NodePath, PandaNode
+
+    # Do flatten nodes with same tags
+    node1 = PandaNode("node1")
+    node1.set_tag("key", "value")
+    node2 = PandaNode("node2")
+    node2.set_tag("key", "value")
+
+    path = NodePath("parent")
+    path.node().add_child(node1)
+    path.node().add_child(node2)
+
+    path.flatten_strong()
+    assert len(path.children) == 1
+
+
+def test_nodepath_flatten_tags_same_key():
+    from panda3d.core import NodePath, PandaNode
+
+    # Don't flatten nodes with different tag keys
+    node1 = PandaNode("node1")
+    node1.set_tag("key1", "value")
+    node2 = PandaNode("node2")
+    node2.set_tag("key2", "value")
+
+    path = NodePath("parent")
+    path.node().add_child(node1)
+    path.node().add_child(node2)
+
+    path.flatten_strong()
+    assert len(path.children) == 2
+
+
+def test_nodepath_flatten_tags_same_value():
+    from panda3d.core import NodePath, PandaNode
+
+    # Don't flatten nodes with different tag values
+    node1 = PandaNode("node1")
+    node1.set_tag("key", "value1")
+    node2 = PandaNode("node2")
+    node2.set_tag("key", "value2")
+
+    path = NodePath("parent")
+    path.node().add_child(node1)
+    path.node().add_child(node2)
+
+    path.flatten_strong()
+    assert len(path.children) == 2
+
+
 def test_nodepath_python_tags():
     from panda3d.core import NodePath