Browse Source

Merge branch 'master' into cmake

Sam Edwards 6 years ago
parent
commit
039f5af34e
100 changed files with 1893 additions and 351 deletions
  1. 4 1
      .gitignore
  2. 1 0
      BACKERS.md
  3. 4 4
      README.md
  4. 13 1
      direct/src/dist/commands.py
  5. 14 5
      direct/src/filter/FilterManager.py
  6. 18 3
      direct/src/gui/DirectOptionMenu.py
  7. 2 3
      direct/src/showbase/SfxPlayer.py
  8. 18 6
      direct/src/showbase/ShowBase.py
  9. 0 23
      direct/src/stdpy/threading.py
  10. 8 4
      direct/src/tkpanels/AnimPanel.py
  11. 32 17
      direct/src/tkwidgets/Valuator.py
  12. 37 0
      doc/ReleaseNotes
  13. 96 43
      dtool/src/dtoolutil/executionEnvironment.cxx
  14. 317 0
      dtool/src/dtoolutil/iostream_ext.cxx
  15. 53 0
      dtool/src/dtoolutil/iostream_ext.h
  16. 1 0
      dtool/src/dtoolutil/p3dtoolutil_ext_composite.cxx
  17. 12 5
      dtool/src/interrogate/interfaceMakerPythonNative.cxx
  18. 26 13
      dtool/src/parser-inc/iostream
  19. 20 0
      dtool/src/prc/encryptStream.cxx
  20. 3 0
      dtool/src/prc/encryptStream.h
  21. 18 0
      dtool/src/prc/encryptStreamBuf.I
  22. 54 3
      dtool/src/prc/encryptStreamBuf.cxx
  23. 9 0
      dtool/src/prc/encryptStreamBuf.h
  24. 25 1
      dtool/src/prc/streamReader.I
  25. 2 0
      dtool/src/prc/streamReader.h
  26. 28 1
      dtool/src/prc/streamWriter.I
  27. 2 0
      dtool/src/prc/streamWriter.h
  28. 31 6
      makepanda/makepanda.py
  29. 15 2
      makepanda/makewheel.py
  30. 16 13
      makepanda/test_wheel.py
  31. 1 1
      panda/src/audiotraits/openalAudioManager.cxx
  32. 15 5
      panda/src/audiotraits/openalAudioSound.cxx
  33. 14 0
      panda/src/bullet/bulletRigidBodyNode.cxx
  34. 1 0
      panda/src/bullet/bulletRigidBodyNode.h
  35. 1 1
      panda/src/cocoadisplay/cocoaGraphicsBuffer.h
  36. 1 1
      panda/src/cocoadisplay/cocoaGraphicsPipe.h
  37. 1 1
      panda/src/cocoadisplay/cocoaGraphicsStateGuardian.h
  38. 1 1
      panda/src/cocoadisplay/cocoaGraphicsWindow.h
  39. 2 2
      panda/src/cocoadisplay/cocoaGraphicsWindow.mm
  40. 1 1
      panda/src/collide/collisionTraverser.h
  41. 61 11
      panda/src/device/evdevInputDevice.cxx
  42. 4 1
      panda/src/device/ioKitInputDevice.cxx
  43. 3 0
      panda/src/device/linuxInputDeviceManager.cxx
  44. 42 7
      panda/src/device/winRawInputDevice.cxx
  45. 8 0
      panda/src/display/graphicsStateGuardian.cxx
  46. 1 0
      panda/src/display/graphicsStateGuardian.h
  47. 1 4
      panda/src/express/multifile.cxx
  48. 2 2
      panda/src/express/zStreamBuf.cxx
  49. 2 1
      panda/src/ffmpeg/ffmpegAudioCursor.cxx
  50. 1 1
      panda/src/ffmpeg/ffmpegAudioCursor.h
  51. 3 1
      panda/src/ffmpeg/ffmpegVideoCursor.cxx
  52. 25 2
      panda/src/glstuff/glGraphicsStateGuardian_src.cxx
  53. 24 20
      panda/src/glstuff/glShaderContext_src.cxx
  54. 5 0
      panda/src/glstuff/glmisc_src.cxx
  55. 1 0
      panda/src/glstuff/glmisc_src.h
  56. 12 1
      panda/src/glxdisplay/glxGraphicsStateGuardian.cxx
  57. 0 8
      panda/src/linmath/lmatrix4_src.I
  58. 7 0
      panda/src/mathutil/perlinNoise2.cxx
  59. 4 2
      panda/src/movies/flacAudioCursor.cxx
  60. 1 1
      panda/src/movies/flacAudioCursor.h
  61. 3 2
      panda/src/movies/microphoneAudioDS.cxx
  62. 18 12
      panda/src/movies/movieAudioCursor.cxx
  63. 2 2
      panda/src/movies/movieAudioCursor.h
  64. 12 0
      panda/src/movies/movieTypeRegistry.cxx
  65. 55 12
      panda/src/movies/opusAudioCursor.cxx
  66. 2 10
      panda/src/movies/opusAudioCursor.h
  67. 8 5
      panda/src/movies/userDataAudio.cxx
  68. 2 2
      panda/src/movies/userDataAudio.h
  69. 6 3
      panda/src/movies/userDataAudioCursor.cxx
  70. 1 1
      panda/src/movies/userDataAudioCursor.h
  71. 56 9
      panda/src/movies/vorbisAudioCursor.cxx
  72. 1 10
      panda/src/movies/vorbisAudioCursor.h
  73. 46 10
      panda/src/movies/wavAudioCursor.cxx
  74. 1 1
      panda/src/movies/wavAudioCursor.h
  75. 1 0
      panda/src/ode/odeBody.h
  76. 1 1
      panda/src/ode/odeJoint.h
  77. 18 1
      panda/src/ode/odeJoint_ext.cxx
  78. 1 1
      panda/src/ode/odeJoint_ext.h
  79. 51 0
      panda/src/pgraph/shaderAttrib.cxx
  80. 1 0
      panda/src/pgraph/shaderAttrib.h
  81. 5 1
      panda/src/pgui/pgButton.cxx
  82. 2 0
      panda/src/pipeline/mutexDebug.I
  83. 2 0
      panda/src/pipeline/mutexDirect.I
  84. 4 0
      panda/src/pipeline/pmutex.h
  85. 53 0
      panda/src/pipeline/pmutex_ext.I
  86. 41 0
      panda/src/pipeline/pmutex_ext.h
  87. 4 0
      panda/src/pipeline/reMutex.h
  88. 4 0
      panda/src/pipeline/reMutexDirect.I
  89. 53 0
      panda/src/pipeline/reMutex_ext.I
  90. 41 0
      panda/src/pipeline/reMutex_ext.h
  91. 8 7
      panda/src/putil/sparseArray.cxx
  92. 12 1
      panda/src/wgldisplay/wglGraphicsStateGuardian.cxx
  93. 5 0
      panda/src/windisplay/config_windisplay.cxx
  94. 1 0
      panda/src/windisplay/config_windisplay.h
  95. 1 1
      panda/src/windisplay/winGraphicsWindow.cxx
  96. 8 30
      panda/src/x11display/x11GraphicsWindow.cxx
  97. 45 0
      tests/bullet/test_bullet_heightfield.py
  98. 30 0
      tests/display/test_glsl_shader.py
  99. 128 0
      tests/dtoolutil/test_iostream.py
  100. 36 0
      tests/linmath/test_lmatrix4.py

+ 4 - 1
.gitignore

@@ -4,11 +4,12 @@
 /targetroot/
 /dstroot/
 
-# Core dumps
+# Core dumps and traces
 core
 core.*
 vgcore.*
 *.core
+*.trace
 
 # Editor files/directories
 *.save
@@ -26,6 +27,7 @@ vgcore.*
 /+DESC
 /+MANIFEST
 /pkg-plist
+/debug.ks
 
 # Produced installer/executables
 /*.exe
@@ -36,6 +38,7 @@ vgcore.*
 /*.dmg
 /*.whl
 /*.txz
+/*.apk
 
 # CMake
 /build/

+ 1 - 0
BACKERS.md

@@ -22,6 +22,7 @@ This is a list of all the people who are contributing financially to Panda3D.  I
 ![Benefactors](https://opencollective.com/panda3d/tiers/benefactor.svg?avatarHeight=48&width=600)
 
 * Sam Edwards
+* Max Voss
 
 ## Backers
 

+ 4 - 4
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-2/).
+[this page](https://www.panda3d.org/download/sdk-1-10-3/).
 If you are familiar with installing Python packages, you can use
 the following comand:
 
@@ -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.2/panda3d-1.10.2-tools-win64.zip
-https://www.panda3d.org/download/panda3d-1.10.2/panda3d-1.10.2-tools-win32.zip
+https://www.panda3d.org/download/panda3d-1.10.3/panda3d-1.10.3-tools-win64.zip
+https://www.panda3d.org/download/panda3d-1.10.3/panda3d-1.10.3-tools-win32.zip
 
 After acquiring these dependencies, you may simply build Panda3D from the
 command prompt using the following command.  (Change `14.1` to `14` if you are
@@ -135,7 +135,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.2/panda3d-1.10.2-tools-mac.tar.gz).
+compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.3/panda3d-1.10.3-tools-mac.tar.gz).
 
 After placing the thirdparty directory inside the panda3d source directory,
 you may build Panda3D using a command like the following:

+ 13 - 1
direct/src/dist/commands.py

@@ -13,6 +13,7 @@ import stat
 import struct
 import imp
 import string
+import time
 
 import setuptools
 import distutils.log
@@ -30,6 +31,15 @@ if sys.version_info < (3, 0):
     # Python 3 defines these subtypes of IOError, but Python 2 doesn't.
     FileNotFoundError = IOError
 
+    # Warn the user.  They might be using Python 2 by accident.
+    print("=================================================================")
+    print("WARNING: You are using Python 2, which will soon be discontinued.")
+    print("WARNING: Please use Python 3 for best results and continued")
+    print("WARNING: support after the EOL date of December 31st, 2019.")
+    print("=================================================================")
+    sys.stdout.flush()
+    time.sleep(4.0)
+
 
 def _parse_list(input):
     if isinstance(input, basestring):
@@ -229,7 +239,9 @@ class build_apps(setuptools.Command):
         self.requirements_path = os.path.join(os.getcwd(), 'requirements.txt')
         self.use_optimized_wheels = True
         self.optimized_wheel_index = ''
-        self.pypi_extra_indexes = []
+        self.pypi_extra_indexes = [
+            'https://archive.panda3d.org/thirdparty',
+        ]
         self.file_handlers = {}
         self.exclude_dependencies = [
             # Windows

+ 14 - 5
direct/src/filter/FilterManager.py

@@ -124,7 +124,7 @@ class FilterManager(DirectObject):
 
         return winx,winy
 
-    def renderSceneInto(self, depthtex=None, colortex=None, auxtex=None, auxbits=0, textures=None):
+    def renderSceneInto(self, depthtex=None, colortex=None, auxtex=None, auxbits=0, textures=None, fbprops=None):
 
         """ Causes the scene to be rendered into the supplied textures
         instead of into the original window.  Puts a fullscreen quad
@@ -185,7 +185,10 @@ class FilterManager(DirectObject):
         # Choose the size of the offscreen buffer.
 
         (winx, winy) = self.getScaledSize(1,1,1)
-        buffer = self.createBuffer("filter-base", winx, winy, texgroup)
+        if fbprops is not None:
+            buffer = self.createBuffer("filter-base", winx, winy, texgroup, fbprops=fbprops)
+        else:
+            buffer = self.createBuffer("filter-base", winx, winy, texgroup)
 
         if (buffer == None):
             return None
@@ -236,7 +239,7 @@ class FilterManager(DirectObject):
 
         return quad
 
-    def renderQuadInto(self, name="filter-stage", mul=1, div=1, align=1, depthtex=None, colortex=None, auxtex0=None, auxtex1=None):
+    def renderQuadInto(self, name="filter-stage", mul=1, div=1, align=1, depthtex=None, colortex=None, auxtex0=None, auxtex1=None, fbprops=None):
 
         """ Creates an offscreen buffer for an intermediate
         computation. Installs a quad into the buffer.  Returns
@@ -250,7 +253,10 @@ class FilterManager(DirectObject):
 
         depthbits = bool(depthtex != None)
 
-        buffer = self.createBuffer(name, winx, winy, texgroup, depthbits)
+        if fbprops is not None:
+            buffer = self.createBuffer(name, winx, winy, texgroup, depthbits, fbprops=fbprops)
+        else:
+            buffer = self.createBuffer(name, winx, winy, texgroup, depthbits)
 
         if (buffer == None):
             return None
@@ -287,7 +293,7 @@ class FilterManager(DirectObject):
 
         return quad
 
-    def createBuffer(self, name, xsize, ysize, texgroup, depthbits=1):
+    def createBuffer(self, name, xsize, ysize, texgroup, depthbits=1, fbprops=None):
         """ Low-level buffer creation.  Not intended for public use. """
 
         winprops = WindowProperties()
@@ -297,6 +303,9 @@ class FilterManager(DirectObject):
         props.setRgbColor(1)
         props.setDepthBits(depthbits)
         props.setStereo(self.win.isStereo())
+        if fbprops is not None:
+            props.addProperties(fbprops)
+
         depthtex, colortex, auxtex0, auxtex1 = texgroup
         if (auxtex0 != None):
             props.setAuxRgba(1)

+ 18 - 3
direct/src/gui/DirectOptionMenu.py

@@ -22,10 +22,12 @@ class DirectOptionMenu(DirectButton):
             # List of items to display on the popup menu
             ('items',       [],             self.setItems),
             # Initial item to display on menu button
-            # Can be an interger index or the same string as the button
+            # Can be an integer index or the same string as the button
             ('initialitem', None,           DGG.INITOPT),
             # Amount of padding to place around popup button indicator
             ('popupMarkerBorder', (.1, .1), None),
+            # The initial position of the popup marker
+            ('popupMarker_pos', (0, 0, 0), None),
             # Background color to use to highlight popup menu items
             ('highlightColor', (.5, .5, .5, 1), None),
             # Extra scale to use on highlight popup menu items
@@ -42,6 +44,8 @@ class DirectOptionMenu(DirectButton):
         DirectButton.__init__(self, parent)
         # Record any user specified frame size
         self.initFrameSize = self['frameSize']
+        # Record any user specified popup marker position
+        self.initPopupMarkerPos = self['popupMarker_pos']
         # Create a small rectangular marker to distinguish this button
         # as a popup menu button
         self.popupMarker = self.createcomponent(
@@ -168,8 +172,13 @@ class DirectOptionMenu(DirectButton):
         else:
             # Or base it upon largest item
             bounds = [self.minX, self.maxX, self.minZ, self.maxZ]
-        pm.setPos(bounds[1] + pmw/2.0, 0,
-                  bounds[2] + (bounds[3] - bounds[2])/2.0)
+        if self.initPopupMarkerPos:
+            # Use specified position
+            pmPos = list(self.initPopupMarkerPos)
+        else:
+            # Or base the position on the frame size.
+            pmPos = [bounds[1] + pmw/2.0, 0, bounds[2] + (bounds[3] - bounds[2])/2.0]
+        pm.setPos(pmPos[0], pmPos[1], pmPos[2])
         # Adjust popup menu button to fit all items (or use user specified
         # frame size
         bounds[1] += pmw
@@ -184,6 +193,12 @@ class DirectOptionMenu(DirectButton):
         Adjust popup position if default position puts it outside of
         visible screen region
         """
+
+        # Needed attributes (such as minZ) won't be set unless the user has specified
+        # items to display. Let's assert that we've given items to work with.
+        items = self['items']
+        assert items and len(items) > 0, 'Cannot show an empty popup menu! You must add items!'
+
         # Show the menu
         self.popupMenu.show()
         # Make sure its at the right scale

+ 2 - 3
direct/src/showbase/SfxPlayer.py

@@ -53,6 +53,8 @@ class SfxPlayer:
                 d = node.getDistance(listenerNode)
             else:
                 d = node.getDistance(base.cam)
+        if not cutoff:
+            cutoff = self.cutoffDistance
         if d == None or d > cutoff:
             volume = 0
         else:
@@ -70,9 +72,6 @@ class SfxPlayer:
             self, sfx, looping = 0, interrupt = 1, volume = None,
             time = 0.0, node=None, listenerNode = None, cutoff = None):
         if sfx:
-            if not cutoff:
-                cutoff = self.cutoffDistance
-
             self.setFinalVolume(sfx, node, volume, listenerNode, cutoff)
 
             # don't start over if it's already playing, unless

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

@@ -829,9 +829,11 @@ class ShowBase(DirectObject.DirectObject):
             win.requestProperties(props)
 
         mainWindow = False
-        if self.win == None:
+        if self.win is None:
             mainWindow = True
             self.win = win
+            if hasattr(self, 'bufferViewer'):
+                self.bufferViewer.win = win
 
         self.winList.append(win)
 
@@ -1677,13 +1679,19 @@ class ShowBase(DirectObject.DirectObject):
         return self.mouseWatcherNode.getModifierButtons().isDown(
             KeyboardButton.meta())
 
-    def attachInputDevice(self, device, prefix=None):
+    def attachInputDevice(self, device, prefix=None, watch=False):
         """
         This function attaches an input device to the data graph, which will
         cause the device to be polled and generate events.  If a prefix is
         given and not None, it is used to prefix events generated by this
         device, separated by a hyphen.
 
+        The watch argument can be set to True (as of Panda3D 1.10.3) to set up
+        the default MouseWatcher to receive inputs from this device, allowing
+        it to be polled via mouseWatcherNode and control user interfaces.
+        Setting this to True will also make it generate unprefixed events,
+        regardless of the specified prefix.
+
         If you call this, you should consider calling detachInputDevice when
         you are done with the device or when it is disconnected.
         """
@@ -1694,13 +1702,17 @@ class ShowBase(DirectObject.DirectObject):
         idn = self.dataRoot.attachNewNode(InputDeviceNode(device, device.name))
 
         # Setup the button thrower to generate events for the device.
-        bt = idn.attachNewNode(ButtonThrower(device.name))
-        if prefix is not None:
-            bt.node().setPrefix(prefix + '-')
+        if prefix is not None or not watch:
+            bt = idn.attachNewNode(ButtonThrower(device.name))
+            if prefix is not None:
+                bt.node().setPrefix(prefix + '-')
+            self.deviceButtonThrowers.append(bt)
 
         assert self.notify.debug("Attached input device {0} with prefix {1}".format(device, prefix))
         self.__inputDeviceNodes[device] = idn
-        self.deviceButtonThrowers.append(bt)
+
+        if watch:
+            idn.node().addChild(self.mouseWatcherNode)
 
     def detachInputDevice(self, device):
         """

+ 0 - 23
direct/src/stdpy/threading.py

@@ -201,17 +201,6 @@ class Lock(core.Mutex):
     def __init__(self, name = "PythonLock"):
         core.Mutex.__init__(self, name)
 
-    def acquire(self, blocking = True):
-        if blocking:
-            core.Mutex.acquire(self)
-            return True
-        else:
-            return core.Mutex.tryAcquire(self)
-
-    __enter__ = acquire
-
-    def __exit__(self, t, v, tb):
-        self.release()
 
 class RLock(core.ReMutex):
     """ This class provides a wrapper around Panda's ReMutex object.
@@ -221,18 +210,6 @@ class RLock(core.ReMutex):
     def __init__(self, name = "PythonRLock"):
         core.ReMutex.__init__(self, name)
 
-    def acquire(self, blocking = True):
-        if blocking:
-            core.ReMutex.acquire(self)
-            return True
-        else:
-            return core.ReMutex.tryAcquire(self)
-
-    __enter__ = acquire
-
-    def __exit__(self, t, v, tb):
-        self.release()
-
 
 class Condition(core.ConditionVarFull):
     """ This class provides a wrapper around Panda's ConditionVarFull

+ 8 - 4
direct/src/tkpanels/AnimPanel.py

@@ -9,13 +9,16 @@ __all__ = ['AnimPanel', 'ActorControl']
 # Import Tkinter, Pmw, and the floater code from this directory tree.
 from direct.tkwidgets.AppShell import *
 from direct.showbase.TkGlobal import *
-import Pmw, sys
+import Pmw, sys, os
 from direct.task import Task
+from panda3d.core import Filename, getModelPath
 
 if sys.version_info >= (3, 0):
     from tkinter.simpledialog import askfloat
+    from tkinter.filedialog import askopenfilename
 else:
     from tkSimpleDialog import askfloat
+    from tkFileDialog import askopenfilename
 
 
 FRAMES = 0
@@ -273,7 +276,7 @@ class AnimPanel(AppShell):
             title = 'Load Animation',
             parent = self.component('hull')
             )
-        if (animFilename == ''):
+        if not animFilename:
             # no file selected, canceled
             return
 
@@ -369,8 +372,9 @@ class AnimPanel(AppShell):
     def destroy(self):
         # First clean up
         taskMgr.remove(self.id + '_UpdateTask')
-        self.destroyCallBack()
-        self.destroyCallBack = None
+        if self.destroyCallBack is not None:
+            self.destroyCallBack()
+            self.destroyCallBack = None
         AppShell.destroy(self)
 
 class ActorControl(Pmw.MegaWidget):

+ 32 - 17
direct/src/tkwidgets/Valuator.py

@@ -656,25 +656,35 @@ def rgbPanel(nodePath, callback = None, style = 'mini'):
     pButton.pack(expand = 1, fill = BOTH)
 
     # Update menu
-    menu = vgp.component('menubar').component('Valuator Group-menu')
+    menubar = vgp.component('menubar')
+    menubar.deletemenuitems('Valuator Group', 1, 1)
+
     # Some helper functions
     # Clear color
-    menu.insert_command(index = 1, label = 'Clear Color',
-                        command = lambda: nodePath.clearColor())
+    menubar.addmenuitem(
+        'Valuator Group', 'command',
+        label='Clear Color', command=lambda: nodePath.clearColor())
     # Set Clear Transparency
-    menu.insert_command(index = 2, label = 'Set Transparency',
-                        command = lambda: nodePath.setTransparency(1))
-    menu.insert_command(
-        index = 3, label = 'Clear Transparency',
-        command = lambda: nodePath.clearTransparency())
+    menubar.addmenuitem(
+        'Valuator Group', 'command',
+        label='Set Transparency', command=lambda: nodePath.setTransparency(1))
+    menubar.addmenuitem(
+        'Valuator Group', 'command',
+        label='Clear Transparency', command=lambda: nodePath.clearTransparency())
 
 
     # System color picker
-    menu.insert_command(index = 4, label = 'Popup Color Picker',
-                        command = popupColorPicker)
+    menubar.addmenuitem(
+        'Valuator Group', 'command',
+        label='Popup Color Picker', command=popupColorPicker)
+
+    menubar.addmenuitem(
+        'Valuator Group', 'command',
+        label='Print to log', command=printToLog)
 
-    menu.insert_command(index = 5, label = 'Print to log',
-                        command = printToLog)
+    menubar.addmenuitem(
+        'Valuator Group', 'command', 'Dismiss Valuator Group panel',
+        label='Dismiss', command=vgp.destroy)
 
     def setNodePathColor(color):
         nodePath.setColor(color[0]/255.0, color[1]/255.0,
@@ -724,18 +734,23 @@ def lightRGBPanel(light, style = 'mini'):
     # Update menu button
     vgp.component('menubar').component('Valuator Group-button')['text'] = (
         'Light Control Panel')
+
     # Add a print button which will also serve as a color tile
     pButton = Button(vgp.interior(), text = 'Print to Log',
                      bg = getTkColorString(initColor),
                      command = printToLog)
     pButton.pack(expand = 1, fill = BOTH)
+
     # Update menu
-    menu = vgp.component('menubar').component('Valuator Group-menu')
+    menubar = vgp.component('menubar')
     # System color picker
-    menu.insert_command(index = 4, label = 'Popup Color Picker',
-                        command = popupColorPicker)
-    menu.insert_command(index = 5, label = 'Print to log',
-                        command = printToLog)
+    menubar.addmenuitem(
+        'Valuator Group', 'command',
+        label='Popup Color Picker', command=popupColorPicker)
+    menubar.addmenuitem(
+        'Valuator Group', 'command',
+        label='Print to log', command=printToLog)
+
     def setLightColor(color):
         light.setColor(Vec4(color[0]/255.0, color[1]/255.0,
                             color[2]/255.0, color[3]/255.0))

+ 37 - 0
doc/ReleaseNotes

@@ -1,3 +1,40 @@
+------------------------  RELEASE 1.10.3  -----------------------
+
+This is another bugfix release that addresses a variety of issues
+in 1.10.2 and further improves the stability.
+
+* Fix crash when unplugging certain devices on macOS
+* Fix crash on macOS when using RIME input
+* Fix logging issues/crashes in apps deployed with Python 2.7
+* Fix issues when starting in fullscreen on Linux/X11
+* Fix mapping of several gamepads including Trust GXT 24
+* Fix Linux crash when no input devices are present
+* Unbreak support for matrix arrays in vertex data in OpenGL
+* Allow creating multisample FBO in OpenGL with non-MS host window
+* Support playing and looping compressed Ogg and WAV audio files
+* Fix generation of CollisionBox for transformed geometry in .egg
+* Fix Bullet rigid body transform not updating after reparenting
+* Fix sporadic color scales with lighting and custom GLSL shader
+* Prevent faulty shaders from shutting down GSG on some drivers
+* Allow None as either argument to OdeJoint.attach()
+* Fix BufferViewer when main window is not opened right away
+* Properly detect extension of pz/gz compressed video/audio files
+* Fix for invalid behavior of SparseArray methods to clear bits
+* FilterManager now allows overriding framebuffer properties
+* Fix detection of core-only OpenGL profile on some drivers
+* Add gl-forward-compatible config var for OpenGL context creation
+* Add paste-emit-keystrokes variable to disable Ctrl+V on Windows
+* Fix in-place |= operator on Panda types (such as SparseArray)
+* Fix rare FFmpeg "bad src image pointers" errors after seek
+* Fix uses of types.InstanceType in some obscure direct functions
+* Fix capsule-into-sphere collision test in degenerate case
+* KeyboardButton.ascii_key now also accepts a str character
+* Fix errors in various Tkinter DIRECT widgets
+* Expose save_egg_file/save_egg_data functions in Python API
+* Fix assertion error in BoundingBox.set_min_max
+* Fix typo in CollisionTraverser.respect_prev_transform property
+* Properly install Python bindings when building FreeBSD installer
+
 ------------------------  RELEASE 1.10.2  -----------------------
 
 This release fixes several more bugs, including a few regressions

+ 96 - 43
dtool/src/dtoolutil/executionEnvironment.cxx

@@ -85,6 +85,44 @@ extern char **GLOBAL_ARGV;
 extern int GLOBAL_ARGC;
 #endif
 
+// One of the responsibilities of ExecutionEnvironment is to determine the path
+// to the binary file that contains itself (this is useful for making other
+// components able to read files relative to Panda's installation directory).
+// When built statically, this is easy - just use the main executable filename.
+// When built shared, ExecutionEnvironment will introspect the memory map of
+// the running process to look for dynamic library paths matching this list of
+// predetermined filenames (ordered most likely to least likely).
+
+#ifndef LINK_ALL_STATIC
+static const char *const libp3dtool_filenames[] = {
+#if defined(LIBP3DTOOL_FILENAMES)
+
+  // The build system is communicating the expected filename(s) for the
+  // libp3dtool dynamic library - no guesswork needed.
+  LIBP3DTOOL_FILENAMES
+
+#elif defined(WIN32_VC)
+
+#ifdef _DEBUG
+  "libp3dtool_d.dll",
+#else
+  "libp3dtool.dll",
+#endif
+
+#elif defined(__APPLE__)
+
+  "libp3dtool." PANDA_ABI_VERSION_STR ".dylib",
+  "libp3dtool.dylib",
+
+#else
+
+  "libp3dtool.so." PANDA_ABI_VERSION_STR,
+  "libp3dtool.so",
+
+#endif
+};
+#endif /* !LINK_ALL_STATIC */
+
 // Linux with GNU libc does have global argvargc variables, but we can't
 // safely access them at stat init time--at least, not in libc5. (It does seem
 // to work with glibc2, however.)
@@ -546,13 +584,14 @@ read_args() {
   // First, we need to fill in _dtool_name.  This contains the full path to
   // the p3dtool library.
 
-#ifdef WIN32_VC
-#ifdef _DEBUG
-  HMODULE dllhandle = GetModuleHandle("libp3dtool_d.dll");
-#else
-  HMODULE dllhandle = GetModuleHandle("libp3dtool.dll");
-#endif
-  if (dllhandle != 0) {
+#ifndef LINK_ALL_STATIC
+#if defined(WIN32_VC)
+  for (const char *filename : libp3dtool_filenames) {
+    if (!_dtool_name.empty()) break;
+
+    HMODULE dllhandle = GetModuleHandle(filename);
+    if (!dllhandle) continue;
+
     static const DWORD buffer_size = 1024;
     wchar_t buffer[buffer_size];
     DWORD size = GetModuleFileNameW(dllhandle, buffer, buffer_size);
@@ -562,46 +601,44 @@ read_args() {
       _dtool_name = tmp;
     }
   }
-#endif
 
-#if defined(__APPLE__)
+#elif defined(__APPLE__)
   // And on OSX we don't have procselfmaps, but some _dyld_* functions.
 
-  if (_dtool_name.empty()) {
-    uint32_t ic = _dyld_image_count();
-    for (uint32_t i = 0; i < ic; ++i) {
-      const char *buffer = _dyld_get_image_name(i);
-      const char *tail = strrchr(buffer, '/');
-      if (tail && (strcmp(tail, "/libp3dtool." PANDA_ABI_VERSION_STR ".dylib") == 0
-                || strcmp(tail, "/libp3dtool.dylib") == 0)) {
+  uint32_t ic = _dyld_image_count();
+  for (uint32_t i = 0; i < ic; ++i) {
+    if (!_dtool_name.empty()) break;
+
+    const char *buffer = _dyld_get_image_name(i);
+    if (!buffer) continue;
+    const char *tail = strrchr(buffer, '/');
+    if (!tail) continue;
+
+    for (const char *filename : libp3dtool_filenames) {
+      if (strcmp(&tail[1], filename) == 0) {
         _dtool_name = buffer;
+        break;
       }
     }
   }
-#endif
 
-#if defined(RTLD_DI_ORIGIN)
+#elif defined(RTLD_DI_ORIGIN)
   // When building with glibc/uClibc, we typically have access to RTLD_DI_ORIGIN in Unix-like operating systems.
 
   char origin[PATH_MAX + 1];
 
-  if (_dtool_name.empty()) {
-    void *dtool_handle = dlopen("libp3dtool.so." PANDA_ABI_VERSION_STR, RTLD_NOW | RTLD_NOLOAD);
+  for (const char *filename : libp3dtool_filenames) {
+    if (!_dtool_name.empty()) break;
+
+    void *dtool_handle = dlopen(filename, RTLD_NOW | RTLD_NOLOAD);
     if (dtool_handle != nullptr && dlinfo(dtool_handle, RTLD_DI_ORIGIN, origin) != -1) {
       _dtool_name = origin;
-      _dtool_name += "/libp3dtool.so." PANDA_ABI_VERSION_STR;
-    } else {
-      // Try the version of libp3dtool.so without ABI suffix.
-      dtool_handle = dlopen("libp3dtool.so", RTLD_NOW | RTLD_NOLOAD);
-      if (dtool_handle != nullptr && dlinfo(dtool_handle, RTLD_DI_ORIGIN, origin) != -1) {
-        _dtool_name = origin;
-        _dtool_name += "/libp3dtool.so";
-      }
+      _dtool_name += '/';
+      _dtool_name += filename;
     }
   }
-#endif
 
-#if !defined(RTLD_DI_ORIGIN) && defined(RTLD_DI_LINKMAP)
+#elif defined(RTLD_DI_LINKMAP)
   // On platforms without RTLD_DI_ORIGIN, we can use dlinfo with RTLD_DI_LINKMAP to get the origin of a loaded library.
   if (_dtool_name.empty()) {
     struct link_map *map;
@@ -610,16 +647,24 @@ read_args() {
 #else
     void *self = dlopen(NULL, RTLD_NOW | RTLD_NOLOAD);
 #endif
-    dlinfo(self, RTLD_DI_LINKMAP, &map);
-
-    while (map != nullptr) {
-      const char *tail = strrchr(map->l_name, '/');
-      const char *head = strchr(map->l_name, '/');
-      if (tail && head && (strcmp(tail, "/libp3dtool.so." PANDA_ABI_VERSION_STR) == 0
-                        || strcmp(tail, "/libp3dtool.so") == 0)) {
-        _dtool_name = head;
+    if (dlinfo(self, RTLD_DI_LINKMAP, &map)) {
+      while (map != nullptr) {
+        if (!_dtool_name.empty()) break;
+
+        const char *head = strchr(map->l_name, '/');
+        if (!head) continue;
+        const char *tail = strrchr(head, '/');
+        if (!tail) continue;
+
+        for (const char *filename : libp3dtool_filenames) {
+          if (strcmp(&tail[1], filename) == 0) {
+            _dtool_name = head;
+            break;
+          }
+        }
+
+        map = map->l_next;
       }
-      map = map->l_next;
     }
   }
 #endif
@@ -634,19 +679,27 @@ read_args() {
     pifstream maps("/proc/self/maps");
 #endif
     while (!maps.fail() && !maps.eof()) {
+      if (!_dtool_name.empty()) break;
+
       char buffer[PATH_MAX];
       buffer[0] = 0;
       maps.getline(buffer, PATH_MAX);
-      const char *tail = strrchr(buffer, '/');
       const char *head = strchr(buffer, '/');
-      if (tail && head && (strcmp(tail, "/libp3dtool.so." PANDA_ABI_VERSION_STR) == 0
-                        || strcmp(tail, "/libp3dtool.so") == 0)) {
-        _dtool_name = head;
+      if (!head) continue;
+      const char *tail = strrchr(head, '/');
+      if (!tail) continue;
+
+      for (const char *filename : libp3dtool_filenames) {
+        if (strcmp(&tail[1], filename) == 0) {
+          _dtool_name = head;
+          break;
+        }
       }
     }
     maps.close();
   }
 #endif
+#endif /* !LINK_ALL_STATIC */
 
   // Now, we need to fill in _binary_name.  This contains the full path to the
   // currently running executable.

+ 317 - 0
dtool/src/dtoolutil/iostream_ext.cxx

@@ -0,0 +1,317 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file iostream_ext.cxx
+ * @author rdb
+ * @date 2017-07-24
+ */
+
+#include "iostream_ext.h"
+
+#ifdef HAVE_PYTHON
+
+#ifndef CPPPARSER
+extern struct Dtool_PyTypedObject Dtool_std_istream;
+#endif
+
+/**
+ * Reads the given number of bytes from the stream, returned as bytes object.
+ * If the given size is -1, all bytes are read from the stream.
+ */
+PyObject *Extension<istream>::
+read(int size) {
+  if (size < 0) {
+    return readall();
+  }
+
+  char *buffer;
+  std::streamsize read_bytes = 0;
+
+  if (size > 0) {
+    std::streambuf *buf = _this->rdbuf();
+    nassertr(buf != nullptr, nullptr);
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+    PyThreadState *_save;
+    Py_UNBLOCK_THREADS
+#endif
+
+    buffer = (char *)alloca((size_t)size);
+    read_bytes = buf->sgetn(buffer, (size_t)size);
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+    Py_BLOCK_THREADS
+#endif
+  }
+
+#if PY_MAJOR_VERSION >= 3
+  return PyBytes_FromStringAndSize(buffer, read_bytes);
+#else
+  return PyString_FromStringAndSize(buffer, read_bytes);
+#endif
+}
+
+/**
+ * Reads from the underlying stream, but using at most one call.  The number
+ * of returned bytes may therefore be less than what was requested, but it
+ * will always be greater than 0 until EOF is reached.
+ */
+PyObject *Extension<istream>::
+read1(int size) {
+  std::streambuf *buf = _this->rdbuf();
+  nassertr(buf != nullptr, nullptr);
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  PyThreadState *_save;
+  Py_UNBLOCK_THREADS
+#endif
+
+  std::streamsize avail = buf->in_avail();
+  if (avail == 0) {
+    avail = 4096;
+  }
+
+  if (size >= 0 && (std::streamsize)size < avail) {
+    avail = (std::streamsize)size;
+  }
+
+  // Don't read more than 4K at a time
+  if (avail > 4096) {
+    avail = 4096;
+  }
+
+  char *buffer = (char *)alloca(avail);
+  std::streamsize read_bytes = buf->sgetn(buffer, avail);
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  Py_BLOCK_THREADS
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+  return PyBytes_FromStringAndSize(buffer, read_bytes);
+#else
+  return PyString_FromStringAndSize(buffer, read_bytes);
+#endif
+}
+
+/**
+ * Reads all of the bytes in the stream.
+ */
+PyObject *Extension<istream>::
+readall() {
+  std::streambuf *buf = _this->rdbuf();
+  nassertr(buf != nullptr, nullptr);
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  PyThreadState *_save;
+  Py_UNBLOCK_THREADS
+#endif
+
+  std::vector<unsigned char> result;
+
+  static const size_t buffer_size = 4096;
+  char buffer[buffer_size];
+
+  std::streamsize count = buf->sgetn(buffer, buffer_size);
+  while (count != 0) {
+    thread_consider_yield();
+    result.insert(result.end(), buffer, buffer + count);
+    count = buf->sgetn(buffer, buffer_size);
+  }
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  Py_BLOCK_THREADS
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+  return PyBytes_FromStringAndSize((char *)result.data(), result.size());
+#else
+  return PyString_FromStringAndSize((char *)result.data(), result.size());
+#endif
+}
+
+/**
+ * Reads bytes into a preallocated, writable, bytes-like object, returning the
+ * number of bytes read.
+ */
+std::streamsize Extension<istream>::
+readinto(PyObject *b) {
+  std::streambuf *buf = _this->rdbuf();
+  nassertr(buf != nullptr, 0);
+
+  Py_buffer view;
+  if (PyObject_GetBuffer(b, &view, PyBUF_CONTIG) == -1) {
+    PyErr_SetString(PyExc_TypeError,
+      "write() requires a contiguous, read-write bytes-like object");
+    return 0;
+  }
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  PyThreadState *_save;
+  Py_UNBLOCK_THREADS
+#endif
+
+  std::streamsize count = buf->sgetn((char *)view.buf, view.len);
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  Py_BLOCK_THREADS
+#endif
+
+  PyBuffer_Release(&view);
+  return count;
+}
+
+/**
+ * Extracts one line up to and including the trailing newline character.
+ * Returns empty string when the end of file is reached.
+ */
+PyObject *Extension<istream>::
+readline(int size) {
+  std::streambuf *buf = _this->rdbuf();
+  nassertr(buf != nullptr, nullptr);
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  PyThreadState *_save;
+  Py_UNBLOCK_THREADS
+#endif
+
+  std::string line;
+  int ch = buf->sbumpc();
+  while (ch != EOF && (--size) != 0) {
+    line.push_back(ch);
+    if (ch == '\n') {
+      // Here's the newline character.
+      break;
+    }
+    ch = buf->sbumpc();
+  }
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  Py_BLOCK_THREADS
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+  return PyBytes_FromStringAndSize(line.data(), line.size());
+#else
+  return PyString_FromStringAndSize(line.data(), line.size());
+#endif
+}
+
+/**
+ * Reads all the lines at once and returns a list.  Also see the documentation
+ * for readline().
+ */
+PyObject *Extension<istream>::
+readlines(int hint) {
+  PyObject *lst = PyList_New(0);
+  if (lst == nullptr) {
+    return nullptr;
+  }
+
+  PyObject *py_line = readline(-1);
+
+  if (hint < 0) {
+    while (Py_SIZE(py_line) > 0) {
+      PyList_Append(lst, py_line);
+      Py_DECREF(py_line);
+
+      py_line = readline(-1);
+    }
+  } else {
+    size_t totchars = 0;
+    while (Py_SIZE(py_line) > 0) {
+      totchars += Py_SIZE(py_line);
+      PyList_Append(lst, py_line);
+      Py_DECREF(py_line);
+
+      if (totchars > hint) {
+        break;
+      }
+
+      py_line = readline(-1);
+    }
+  }
+
+  return lst;
+}
+
+/**
+ * Yields continuously to read all the lines from the istream.
+ */
+static PyObject *gen_next(PyObject *self) {
+  istream *stream = nullptr;
+  if (!Dtool_Call_ExtractThisPointer(self, Dtool_std_istream, (void **)&stream)) {
+    return nullptr;
+  }
+
+  PyObject *line = invoke_extension(stream).readline();
+  if (Py_SIZE(line) > 0) {
+    return line;
+  } else {
+    PyErr_SetObject(PyExc_StopIteration, nullptr);
+    return nullptr;
+  }
+}
+
+/**
+ * Iterates over the lines of the file.
+ */
+PyObject *Extension<istream>::
+__iter__(PyObject *self) {
+  return Dtool_NewGenerator(self, &gen_next);
+}
+
+/**
+ * Writes the bytes object to the stream.
+ */
+void Extension<ostream>::
+write(PyObject *b) {
+  std::streambuf *buf = _this->rdbuf();
+  nassertv(buf != nullptr);
+
+  Py_buffer view;
+  if (PyObject_GetBuffer(b, &view, PyBUF_CONTIG_RO) == -1) {
+    PyErr_SetString(PyExc_TypeError, "write() requires a contiguous buffer");
+    return;
+  }
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  PyThreadState *_save;
+  Py_UNBLOCK_THREADS
+  buf->sputn((const char *)view.buf, view.len);
+  Py_BLOCK_THREADS
+#else
+  buf->sputn((const char *)view.buf, view.len);
+#endif
+
+  PyBuffer_Release(&view);
+}
+
+/**
+ * Write a list of lines to the stream.  Line separators are not added, so it
+ * is usual for each of the lines provided to have a line separator at the
+ * end.
+ */
+void Extension<ostream>::
+writelines(PyObject *lines) {
+  PyObject *seq = PySequence_Fast(lines, "writelines() expects a sequence");
+  if (seq == nullptr) {
+    return;
+  }
+
+  PyObject **items = PySequence_Fast_ITEMS(seq);
+  Py_ssize_t len = PySequence_Fast_GET_SIZE(seq);
+
+  for (Py_ssize_t i = 0; i < len; ++i) {
+    write(items[i]);
+  }
+
+  Py_DECREF(seq);
+}
+
+#endif  // HAVE_PYTHON

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

@@ -0,0 +1,53 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file iostream_ext.h
+ * @author rdb
+ * @date 2017-07-24
+ */
+
+#ifndef IOSTREAM_EXT_H
+#define IOSTREAM_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include <iostream>
+#include "py_panda.h"
+
+/**
+ * These classes define the extension methods for istream and ostream, which
+ * are called instead of any C++ methods with the same prototype.
+ *
+ * These are designed to allow streams to be treated as file-like objects.
+ */
+template<>
+class Extension<istream> : public ExtensionBase<istream> {
+public:
+  PyObject *read(int size=-1);
+  PyObject *read1(int size=-1);
+  PyObject *readall();
+  std::streamsize readinto(PyObject *b);
+
+  PyObject *readline(int size=-1);
+  PyObject *readlines(int hint=-1);
+  PyObject *__iter__(PyObject *self);
+};
+
+template<>
+class Extension<ostream> : public ExtensionBase<ostream> {
+public:
+  void write(PyObject *b);
+  void writelines(PyObject *lines);
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // IOSTREAM_EXT_H

+ 1 - 0
dtool/src/dtoolutil/p3dtoolutil_ext_composite.cxx

@@ -1,3 +1,4 @@
 #include "filename_ext.cxx"
 #include "globPattern_ext.cxx"
+#include "iostream_ext.cxx"
 #include "textEncoder_ext.cxx"

+ 12 - 5
dtool/src/interrogate/interfaceMakerPythonNative.cxx

@@ -431,6 +431,12 @@ get_slotted_function_def(Object *obj, Function *func, FunctionRemap *remap,
     return true;
   }
 
+  if (method_name == "operator |=") {
+    def._answer_location = "nb_inplace_or";
+    def._wrapper_type = WT_inplace_binary_operator;
+    return true;
+  }
+
   if (method_name == "__ipow__") {
     def._answer_location = "nb_inplace_power";
     def._wrapper_type = WT_inplace_ternary_operator;
@@ -4946,13 +4952,14 @@ write_function_instance(ostream &out, FunctionRemap *remap,
       expected_params += "NoneType";
 
     } else if (TypeManager::is_char(type)) {
-      indent(out, indent_level) << "char " << param_name << default_expr << ";\n";
+      indent(out, indent_level) << "char *" << param_name << "_str;\n";
+      indent(out, indent_level) << "Py_ssize_t " << param_name << "_len;\n";
 
-      format_specifiers += "c";
-      parameter_list += ", &" + param_name;
+      format_specifiers += "s#";
+      parameter_list += ", &" + param_name + "_str, &" + param_name + "_len";
+      extra_param_check << " && " << param_name << "_len == 1";
 
-      // extra_param_check << " && isascii(" << param_name << ")";
-      pexpr_string = "(char) " + param_name;
+      pexpr_string = param_name + "_str[0]";
       expected_params += "char";
       only_pyobjects = false;
 

+ 26 - 13
dtool/src/parser-inc/iostream

@@ -1,16 +1,15 @@
-// Filename: iostream
-// Created by:  drose (12May00)
-//
-////////////////////////////////////////////////////////////////////
-//
-// PANDA 3D SOFTWARE
-// Copyright (c) Carnegie Mellon University.  All rights reserved.
-//
-// All use of this software is subject to the terms of the revised BSD
-// license.  You should have received a copy of this license along
-// with this source code in a file named "LICENSE."
-//
-////////////////////////////////////////////////////////////////////
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file iostream
+ * @author drose
+ * @date 2000-05-12
+ */
 
 // This file, and all the other files in this directory, aren't
 // intended to be compiled--they're just parsed by CPPParser (and
@@ -34,6 +33,9 @@ namespace std {
   __published:
     ostream(const ostream&) = delete;
 
+    __extension void write(PyObject *b);
+    __extension void writelines(PyObject *lines);
+
     void put(char c);
     void flush();
     streampos tellp();
@@ -43,10 +45,20 @@ namespace std {
   protected:
     ostream(ostream &&);
   };
+
   class istream : virtual public ios {
   __published:
     istream(const istream&) = delete;
 
+    __extension PyObject *read(int size=-1);
+    __extension PyObject *read1(int size=-1);
+    __extension PyObject *readall();
+    __extension std::streamsize readinto(PyObject *b);
+
+    __extension PyObject *readline(int size=-1);
+    __extension PyObject *readlines(int hint=-1);
+    __extension PyObject *__iter__(PyObject *self);
+
     int get();
     streampos tellg();
     void seekg(streampos pos);
@@ -55,6 +67,7 @@ namespace std {
   protected:
     istream(istream &&);
   };
+
   class iostream : public istream, public ostream {
   __published:
     iostream(const iostream&) = delete;

+ 20 - 0
dtool/src/prc/encryptStream.cxx

@@ -12,3 +12,23 @@
  */
 
 #include "encryptStream.h"
+
+/**
+ * Must be called immediately after open_read().  Decrypts the given number of
+ * bytes and checks that they match.  The amount of header bytes are added to
+ * an offset so that skipping to 0 will skip past the header.
+ *
+ * Returns true if the read magic matches the given magic, false on error.
+ */
+bool IDecryptStream::
+read_magic(const char *magic, size_t size) {
+  char *this_magic = (char *)alloca(size);
+  read(this_magic, size);
+
+  if (!fail() && gcount() == size && memcmp(this_magic, magic, size) == 0) {
+    _buf.set_magic_length(size);
+    return true;
+  } else {
+    return false;
+  }
+}

+ 3 - 0
dtool/src/prc/encryptStream.h

@@ -53,6 +53,9 @@ PUBLISHED:
   MAKE_PROPERTY(key_length, get_key_length);
   MAKE_PROPERTY(iteration_count, get_iteration_count);
 
+public:
+  bool read_magic(const char *magic, size_t size);
+
 private:
   EncryptStreamBuf _buf;
 };

+ 18 - 0
dtool/src/prc/encryptStreamBuf.I

@@ -81,3 +81,21 @@ INLINE int EncryptStreamBuf::
 get_iteration_count() const {
   return _iteration_count;
 }
+
+/**
+ * Sets the amount of the encrypted data at the beginning that are skipped
+ * when seeking back to zero.
+ */
+INLINE void EncryptStreamBuf::
+set_magic_length(size_t length) {
+  _magic_length = length;
+}
+
+/**
+ * Sets the amount of the encrypted data at the beginning that are skipped
+ * when seeking back to zero.
+ */
+INLINE size_t EncryptStreamBuf::
+get_magic_length() const {
+  return _magic_length;
+}

+ 54 - 3
dtool/src/prc/encryptStreamBuf.cxx

@@ -177,6 +177,7 @@ open_read(std::istream *source, bool owns_source, const std::string &password) {
 
   _read_overflow_buffer = new unsigned char[_read_block_size];
   _in_read_overflow_buffer = 0;
+  _finished = false;
   thread_consider_yield();
 }
 
@@ -322,6 +323,57 @@ close_write() {
   }
 }
 
+/**
+ * Implements seeking within the stream.  EncryptStreamBuf only allows seeking
+ * back to the beginning of the stream.
+ */
+std::streampos EncryptStreamBuf::
+seekoff(std::streamoff off, ios_seekdir dir, ios_openmode which) {
+  if (which != std::ios::in) {
+    // We can only do this with the input stream.
+    return -1;
+  }
+
+  if (off != 0 || dir != std::ios::beg) {
+    // We only know how to reposition to the beginning.
+    return -1;
+  }
+
+  size_t n = egptr() - gptr();
+  gbump(n);
+
+  if (_source->rdbuf()->pubseekpos(0, std::ios::in) == (std::streampos)0) {
+    int result = EVP_DecryptInit(_read_ctx, nullptr, nullptr, nullptr);
+    nassertr_always(result > 0, -1);
+
+    _source->clear();
+    _in_read_overflow_buffer = 0;
+    _finished = false;
+
+    // Skip past the header.
+    int iv_length = EVP_CIPHER_CTX_iv_length(_read_ctx);
+    _source->ignore(6 + iv_length);
+
+    // Ignore the magic bytes.
+    size_t magic_length = get_magic_length();
+    char *buffer = (char *)alloca(magic_length);
+    if (read_chars(buffer, magic_length) == magic_length) {
+      return 0;
+    }
+  }
+
+  return -1;
+}
+
+/**
+ * Implements seeking within the stream.  EncryptStreamBuf only allows seeking
+ * back to the beginning of the stream.
+ */
+std::streampos EncryptStreamBuf::
+seekpos(std::streampos pos, ios_openmode which) {
+  return seekoff(pos, std::ios::beg, which);
+}
+
 /**
  * Called by the system ostream implementation when its internal buffer is
  * filled, plus one character.
@@ -423,7 +475,7 @@ read_chars(char *start, size_t length) {
 
   do {
     // Get more bytes from the stream.
-    if (_read_ctx == nullptr) {
+    if (_read_ctx == nullptr || _finished) {
       return 0;
     }
 
@@ -439,8 +491,7 @@ read_chars(char *start, size_t length) {
     } else {
       result =
         EVP_DecryptFinal(_read_ctx, read_buffer, &bytes_read);
-      EVP_CIPHER_CTX_free(_read_ctx);
-      _read_ctx = nullptr;
+      _finished = true;
     }
 
     if (result <= 0) {

+ 9 - 0
dtool/src/prc/encryptStreamBuf.h

@@ -44,6 +44,12 @@ public:
   INLINE void set_iteration_count(int iteration_count);
   INLINE int get_iteration_count() const;
 
+  INLINE void set_magic_length(size_t length);
+  INLINE size_t get_magic_length() const;
+
+  virtual std::streampos seekoff(std::streamoff off, ios_seekdir dir, ios_openmode which);
+  virtual std::streampos seekpos(std::streampos pos, ios_openmode which);
+
 protected:
   virtual int overflow(int c);
   virtual int sync();
@@ -71,6 +77,9 @@ private:
 
   EVP_CIPHER_CTX *_write_ctx;
   size_t _write_block_size;
+
+  size_t _magic_length = 0;
+  bool _finished = false;
 };
 
 #include "encryptStreamBuf.I"

+ 25 - 1
dtool/src/prc/streamReader.I

@@ -43,7 +43,18 @@ StreamReader(const StreamReader &copy) :
 }
 
 /**
- * The copy constructor does not copy ownership of the stream.
+ * The move constructor steals ownership of the stream.
+ */
+INLINE StreamReader::
+StreamReader(StreamReader &&from) noexcept :
+  _in(from._in),
+  _owns_stream(from._owns_stream)
+{
+  from._owns_stream = false;
+}
+
+/**
+ * The copy assignment operator does not copy ownership of the stream.
  */
 INLINE void StreamReader::
 operator = (const StreamReader &copy) {
@@ -54,6 +65,19 @@ operator = (const StreamReader &copy) {
   _owns_stream = false;
 }
 
+/**
+ * The move assignment operator steals ownership of the stream.
+ */
+INLINE void StreamReader::
+operator = (StreamReader &&from) noexcept {
+  if (_owns_stream) {
+    delete _in;
+  }
+  _in = from._in;
+  _owns_stream = from._owns_stream;
+  from._owns_stream = false;
+}
+
 /**
  *
  */

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

@@ -31,7 +31,9 @@ public:
 PUBLISHED:
   INLINE explicit StreamReader(std::istream *in, bool owns_stream);
   INLINE StreamReader(const StreamReader &copy);
+  INLINE StreamReader(StreamReader &&from) noexcept;
   INLINE void operator = (const StreamReader &copy);
+  INLINE void operator = (StreamReader &&from) noexcept;
   INLINE ~StreamReader();
 
   INLINE std::istream *get_istream() const;

+ 28 - 1
dtool/src/prc/streamWriter.I

@@ -51,7 +51,21 @@ StreamWriter(const StreamWriter &copy) :
 }
 
 /**
- * The copy constructor does not copy ownership of the stream.
+ * The move constructor steals ownership of the stream.
+ */
+INLINE StreamWriter::
+StreamWriter(StreamWriter &&from) noexcept :
+#ifdef HAVE_PYTHON
+  softspace(0),
+#endif
+  _out(from._out),
+  _owns_stream(from._owns_stream)
+{
+  from._owns_stream = false;
+}
+
+/**
+ * The copy assignment operator does not copy ownership of the stream.
  */
 INLINE void StreamWriter::
 operator = (const StreamWriter &copy) {
@@ -62,6 +76,19 @@ operator = (const StreamWriter &copy) {
   _owns_stream = false;
 }
 
+/**
+ * The move assignment operator steals ownership of the stream.
+ */
+INLINE void StreamWriter::
+operator = (StreamWriter &&from) noexcept {
+  if (_owns_stream) {
+    delete _out;
+  }
+  _out = from._out;
+  _owns_stream = from._owns_stream;
+  from._owns_stream = false;
+}
+
 /**
  *
  */

+ 2 - 0
dtool/src/prc/streamWriter.h

@@ -32,7 +32,9 @@ public:
 PUBLISHED:
   INLINE explicit StreamWriter(std::ostream *out, bool owns_stream);
   INLINE StreamWriter(const StreamWriter &copy);
+  INLINE StreamWriter(StreamWriter &&from) noexcept;
   INLINE void operator = (const StreamWriter &copy);
+  INLINE void operator = (StreamWriter &&from) noexcept;
   INLINE ~StreamWriter();
 
   INLINE std::ostream *get_ostream() const;

+ 31 - 6
makepanda/makepanda.py

@@ -1009,12 +1009,26 @@ if (COMPILER=="GCC"):
 
     if GetTarget() == 'darwin':
         LibName("ALWAYS", "-framework AppKit")
+        LibName("IOKIT", "-framework IOKit")
+        LibName("QUARTZ", "-framework Quartz")
         LibName("AGL", "-framework AGL")
         LibName("CARBON", "-framework Carbon")
         LibName("COCOA", "-framework Cocoa")
         # Fix for a bug in OSX Leopard:
         LibName("GL", "-dylib_file /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib:/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib")
 
+        # Temporary exceptions to removal of this flag
+        if not PkgSkip("ROCKET"):
+            LibName("ROCKET", "-undefined dynamic_lookup")
+        if not PkgSkip("FFMPEG"):
+            LibName("FFMPEG", "-undefined dynamic_lookup")
+        if not PkgSkip("ASSIMP"):
+            LibName("ASSIMP", "-undefined dynamic_lookup")
+        if not PkgSkip("OPENEXR"):
+            LibName("OPENEXR", "-undefined dynamic_lookup")
+        if not PkgSkip("VRPN"):
+            LibName("VRPN", "-undefined dynamic_lookup")
+
     if GetTarget() == 'android':
         LibName("ALWAYS", '-llog')
         LibName("ANDROID", '-landroid')
@@ -1140,6 +1154,7 @@ def BracketNameWithQuotes(name):
     # Workaround for OSX bug - compiler doesn't like those flags quoted.
     if (name.startswith("-framework")): return name
     if (name.startswith("-dylib_file")): return name
+    if (name.startswith("-undefined ")): return name
 
     # Don't add quotes when it's not necessary.
     if " " not in name: return name
@@ -1813,9 +1828,11 @@ def CompileLink(dll, obj, opts):
                 cmd += ' -Wl,--allow-shlib-undefined'
         else:
             if (GetTarget() == "darwin"):
-                cmd = cxx + ' -undefined dynamic_lookup'
-                if ("BUNDLE" in opts or GetOrigExt(dll) == ".pyd"):
-                    cmd += ' -bundle '
+                cmd = cxx
+                if GetOrigExt(dll) == ".pyd":
+                    cmd += ' -bundle -undefined dynamic_lookup'
+                elif "BUNDLE" in opts:
+                    cmd += ' -bundle'
                 else:
                     install_name = '@loader_path/../lib/' + os.path.basename(dll)
                     cmd += ' -dynamiclib -install_name ' + install_name
@@ -2325,7 +2342,6 @@ DTOOL_CONFIG=[
     ("COMPILE_IN_DEFAULT_FONT",        '1',                      '1'),
     ("STDFLOAT_DOUBLE",                'UNDEF',                  'UNDEF'),
     ("HAVE_MAYA",                      '1',                      '1'),
-    ("HAVE_SOFTIMAGE",                 'UNDEF',                  'UNDEF'),
     ("REPORT_OPENSSL_ERRORS",          '1',                      '1'),
     ("USE_PANDAFILESTREAM",            '1',                      '1'),
     ("USE_DELETED_CHAIN",              '1',                      '1'),
@@ -2866,6 +2882,14 @@ for basename in del_files:
 p3d_init = """"Python bindings for the Panda3D libraries"
 
 __version__ = '%s'
+
+if __debug__:
+    import sys
+    if sys.version_info < (3, 0):
+        sys.stderr.write("WARNING: Python 2.7 will reach EOL after December 31, 2019.\\n")
+        sys.stderr.write("To suppress this warning, upgrade to Python 3.\\n")
+        sys.stderr.flush()
+    del sys
 """ % (WHLVERSION)
 
 if GetTarget() == 'windows':
@@ -3661,6 +3685,7 @@ IGATEFILES += [
     "globPattern_ext.h",
     "pandaFileStream.h",
     "lineStream.h",
+    "iostream_ext.h",
 ]
 TargetAdd('libp3dtoolutil.in', opts=OPTS, input=IGATEFILES)
 TargetAdd('libp3dtoolutil.in', opts=['IMOD:panda3d.core', 'ILIB:libp3dtoolutil', 'SRCDIR:dtool/src/dtoolutil'])
@@ -4204,7 +4229,7 @@ if (not RUNTIME):
   OPTS=['DIR:panda/metalibs/panda', 'BUILDING:PANDA', 'JPEG', 'PNG', 'HARFBUZZ',
       'TIFF', 'OPENEXR', 'ZLIB', 'OPENSSL', 'FREETYPE', 'FFTW', 'ADVAPI', 'WINSOCK2',
       'SQUISH', 'NVIDIACG', 'VORBIS', 'OPUS', 'WINUSER', 'WINMM', 'WINGDI', 'IPHLPAPI',
-      'SETUPAPI']
+      'SETUPAPI', 'IOKIT']
 
   TargetAdd('panda_panda.obj', opts=OPTS, input='panda.cxx')
 
@@ -4843,7 +4868,7 @@ if (GetTarget() == 'darwin' and PkgSkip("COCOA")==0 and PkgSkip("GL")==0 and not
   if (PkgSkip('PANDAFX')==0):
     TargetAdd('libpandagl.dll', input='libpandafx.dll')
   TargetAdd('libpandagl.dll', input=COMMON_PANDA_LIBS)
-  TargetAdd('libpandagl.dll', opts=['MODULE', 'GL', 'NVIDIACG', 'CGGL', 'COCOA', 'CARBON'])
+  TargetAdd('libpandagl.dll', opts=['MODULE', 'GL', 'NVIDIACG', 'CGGL', 'COCOA', 'CARBON', 'QUARTZ'])
 
 #
 # DIRECTORY: panda/src/wgldisplay/

+ 15 - 2
makepanda/makewheel.py

@@ -596,10 +596,23 @@ def makewheel(version, output_dir, platform=None):
 
     # Write the panda3d tree.  We use a custom empty __init__ since the
     # default one adds the bin directory to the PATH, which we don't have.
-    whl.write_file_data('panda3d/__init__.py', """"Python bindings for the Panda3D libraries"
+    p3d_init = """"Python bindings for the Panda3D libraries"
 
 __version__ = '{0}'
-""".format(version))
+""".format(version)
+
+    if '27' in ABI_TAG:
+        p3d_init += """
+if __debug__:
+    import sys
+    if sys.version_info < (3, 0):
+        sys.stderr.write("WARNING: Python 2.7 will reach EOL after December 31, 2019.\\n")
+        sys.stderr.write("To suppress this warning, upgrade to Python 3.\\n")
+        sys.stderr.flush()
+    del sys
+"""
+
+    whl.write_file_data('panda3d/__init__.py', p3d_init)
 
     # Copy the extension modules from the panda3d directory.
     ext_suffix = GetExtensionSuffix()

+ 16 - 13
makepanda/test_wheel.py

@@ -17,31 +17,34 @@ from optparse import OptionParser
 def test_wheel(wheel, verbose=False):
     envdir = tempfile.mkdtemp(prefix="venv-")
     print("Setting up virtual environment in {0}".format(envdir))
+    sys.stdout.flush()
 
+    # Make sure pip is up-to-date first.
+    subprocess.call([sys.executable, "-B", "-m", "pip", "install", "-U", "pip"])
+
+    # Create a virtualenv.
     if sys.version_info >= (3, 0):
-        subprocess.call([sys.executable, "-m", "venv", "--clear", envdir])
+        subprocess.call([sys.executable, "-B", "-m", "venv", "--clear", envdir])
     else:
-        subprocess.call([sys.executable, "-m", "virtualenv", "--clear", envdir])
+        subprocess.call([sys.executable, "-B", "-m", "virtualenv", "--clear", envdir])
 
-    # Make sure pip is up-to-date first.
-    if subprocess.call([sys.executable, "-m", "pip", "install", "-U", "pip"]) != 0:
+    # Determine the path to the Python interpreter.
+    if sys.platform == "win32":
+        python = os.path.join(envdir, "Scripts", "python.exe")
+    else:
+        python = os.path.join(envdir, "bin", "python")
+
+    # Upgrade pip inside the environment too.
+    if subprocess.call([python, "-m", "pip", "install", "-U", "pip"]) != 0:
         shutil.rmtree(envdir)
         sys.exit(1)
 
     # Install pytest into the environment, as well as our wheel.
-    if sys.platform == "win32":
-        pip = os.path.join(envdir, "Scripts", "pip.exe")
-    else:
-        pip = os.path.join(envdir, "bin", "pip")
-    if subprocess.call([pip, "install", "pytest", wheel]) != 0:
+    if subprocess.call([python, "-m", "pip", "install", "pytest", wheel]) != 0:
         shutil.rmtree(envdir)
         sys.exit(1)
 
     # Run the test suite.
-    if sys.platform == "win32":
-        python = os.path.join(envdir, "Scripts", "python.exe")
-    else:
-        python = os.path.join(envdir, "bin", "python")
     test_cmd = [python, "-m", "pytest", "tests"]
     if verbose:
         test_cmd.append("--verbose")

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

@@ -442,7 +442,7 @@ get_sound_data(MovieAudio *movie, int mode) {
     int channels = stream->audio_channels();
     int samples = (int)(stream->length() * stream->audio_rate());
     int16_t *data = new int16_t[samples * channels];
-    stream->read_samples(samples, data);
+    samples = stream->read_samples(samples, data);
     alBufferData(sd->_sample,
                  (channels>1) ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16,
                  data, samples * channels * 2, stream->audio_rate());

+ 15 - 5
panda/src/audiotraits/openalAudioSound.cxx

@@ -373,7 +373,6 @@ read_stream_data(int bytelen, unsigned char *buffer) {
   nassertr(has_sound_data(), 0);
 
   MovieAudioCursor *cursor = _sd->_stream;
-  double length = cursor->length();
   int channels = cursor->audio_channels();
   int rate = cursor->audio_rate();
   int space = bytelen / (channels * 2);
@@ -381,7 +380,7 @@ read_stream_data(int bytelen, unsigned char *buffer) {
 
   while (space && (_loops_completed < _playing_loops)) {
     double t = cursor->tell();
-    double remain = length - t;
+    double remain = cursor->length() - t;
     if (remain > 60.0) {
       remain = 60.0;
     }
@@ -403,9 +402,20 @@ read_stream_data(int bytelen, unsigned char *buffer) {
     if (samples > _sd->_stream->ready()) {
       samples = _sd->_stream->ready();
     }
-    cursor->read_samples(samples, (int16_t *)buffer);
-    size_t hval = AddHash::add_hash(0, (uint8_t*)buffer, samples*channels*2);
-    audio_debug("Streaming " << cursor->get_source()->get_name() << " at " << t << " hash " << hval);
+    samples = cursor->read_samples(samples, (int16_t *)buffer);
+    if (audio_cat.is_debug()) {
+      size_t hval = AddHash::add_hash(0, (uint8_t*)buffer, samples*channels*2);
+      audio_debug("Streaming " << cursor->get_source()->get_name() << " at " << t << " hash " << hval);
+    }
+    if (samples == 0) {
+      _loops_completed += 1;
+      cursor->seek(0.0);
+      if (_playing_loops >= 1000000000) {
+        // Prevent infinite loop if endlessly looping empty sound
+        return fill;
+      }
+      continue;
+    }
     fill += samples;
     space -= samples;
     buffer += (samples * channels * 2);

+ 14 - 0
panda/src/bullet/bulletRigidBodyNode.cxx

@@ -359,6 +359,20 @@ do_transform_changed() {
   }
 }
 
+/**
+ *
+ */
+void BulletRigidBodyNode::
+parents_changed() {
+
+  if (_motion.sync_disabled()) return;
+
+  if (get_num_parents() > 0) {
+    LightMutexHolder holder(BulletWorld::get_global_lock());
+    do_transform_changed();
+  }
+}
+
 /**
  *
  */

+ 1 - 0
panda/src/bullet/bulletRigidBodyNode.h

@@ -112,6 +112,7 @@ public:
   void do_sync_b2p();
 
 protected:
+  virtual void parents_changed();
   virtual void transform_changed();
 
 private:

+ 1 - 1
panda/src/cocoadisplay/cocoaGraphicsBuffer.h

@@ -21,7 +21,7 @@
  * This is a light wrapper around GLGraphicsBuffer (ie. FBOs) to interface
  * with Cocoa contexts, so that it can be used without a host window.
  */
-class CocoaGraphicsBuffer : public GLGraphicsBuffer {
+class EXPCL_PANDA_COCOADISPLAY CocoaGraphicsBuffer : public GLGraphicsBuffer {
 public:
   CocoaGraphicsBuffer(GraphicsEngine *engine, GraphicsPipe *pipe,
                       const std::string &name,

+ 1 - 1
panda/src/cocoadisplay/cocoaGraphicsPipe.h

@@ -33,7 +33,7 @@ class FrameBufferProperties;
  * This graphics pipe represents the interface for creating OpenGL graphics
  * windows on a Cocoa-based (e.g.  Mac OS X) client.
  */
-class CocoaGraphicsPipe : public GraphicsPipe {
+class EXPCL_PANDA_COCOADISPLAY CocoaGraphicsPipe : public GraphicsPipe {
 public:
   CocoaGraphicsPipe(CGDirectDisplayID display = CGMainDisplayID());
   virtual ~CocoaGraphicsPipe();

+ 1 - 1
panda/src/cocoadisplay/cocoaGraphicsStateGuardian.h

@@ -26,7 +26,7 @@
  * A tiny specialization on GLGraphicsStateGuardian to add some Cocoa-specific
  * information.
  */
-class CocoaGraphicsStateGuardian : public GLGraphicsStateGuardian {
+class EXPCL_PANDA_COCOADISPLAY CocoaGraphicsStateGuardian : public GLGraphicsStateGuardian {
 public:
   INLINE const FrameBufferProperties &get_fb_properties() const;
   void get_properties(FrameBufferProperties &properties,

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

@@ -29,7 +29,7 @@
  * An interface to the Cocoa system for managing OpenGL windows under Mac OS
  * X.
  */
-class CocoaGraphicsWindow : public GraphicsWindow {
+class EXPCL_PANDA_COCOADISPLAY CocoaGraphicsWindow : public GraphicsWindow {
 public:
   CocoaGraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe,
                       const std::string &name,

+ 2 - 2
panda/src/cocoadisplay/cocoaGraphicsWindow.mm

@@ -1618,7 +1618,7 @@ handle_key_event(NSEvent *event) {
   if ([event type] == NSKeyDown) {
     // Translate it to a unicode character for keystrokes.  I would use
     // interpretKeyEvents and insertText, but that doesn't handle dead keys.
-    TISInputSourceRef input_source = TISCopyCurrentKeyboardInputSource();
+    TISInputSourceRef input_source = TISCopyCurrentKeyboardLayoutInputSource();
     CFDataRef layout_data = (CFDataRef)TISGetInputSourceProperty(input_source, kTISPropertyUnicodeKeyLayoutData);
     const UCKeyboardLayout *layout = (const UCKeyboardLayout *)CFDataGetBytePtr(layout_data);
 
@@ -1827,7 +1827,7 @@ get_keyboard_map() const {
   const UCKeyboardLayout *layout;
 
   // Get the current keyboard layout data.
-  input_source = TISCopyCurrentKeyboardInputSource();
+  input_source = TISCopyCurrentKeyboardLayoutInputSource();
   layout_data = (CFDataRef) TISGetInputSourceProperty(input_source, kTISPropertyUnicodeKeyLayoutData);
   layout = (const UCKeyboardLayout *)CFDataGetBytePtr(layout_data);
 

+ 1 - 1
panda/src/collide/collisionTraverser.h

@@ -49,7 +49,7 @@ PUBLISHED:
 
   INLINE void set_respect_prev_transform(bool flag);
   INLINE bool get_respect_prev_transform() const;
-  MAKE_PROPERTY(respect_preV_transform, get_respect_prev_transform,
+  MAKE_PROPERTY(respect_prev_transform, get_respect_prev_transform,
                                         set_respect_prev_transform);
 
   void add_collider(const NodePath &collider, CollisionHandler *handler);

+ 61 - 11
panda/src/device/evdevInputDevice.cxx

@@ -62,6 +62,15 @@ enum QuirkBits {
   // We only connect it if it is reporting any events, because when Steam is
   // running, the Steam controller is muted in favour of a dummy Xbox device.
   QB_steam_controller = 32,
+
+  // Axes on the right stick are swapped, using x for y and vice versa.
+  QB_right_axes_swapped = 64,
+
+  // Has no trigger axes.
+  QB_no_analog_triggers = 128,
+
+  // Alternate button mapping.
+  QB_alt_button_mapping = 256,
 };
 
 static const struct DeviceMapping {
@@ -81,11 +90,15 @@ static const struct DeviceMapping {
   // Steam Controller (wireless)
   {0x28de, 0x1142, InputDevice::DeviceClass::unknown, QB_steam_controller},
   // Jess Tech Colour Rumble Pad
-  {0x0f30, 0x0111, InputDevice::DeviceClass::gamepad, 0},
-  // SPEED Link SL-6535-SBK-01
-  {0x0079, 0x0006, InputDevice::DeviceClass::gamepad, 0},
+  {0x0f30, 0x0111, InputDevice::DeviceClass::gamepad, QB_rstick_from_z | QB_right_axes_swapped},
+  // Trust GXT 24
+  {0x0079, 0x0006, InputDevice::DeviceClass::gamepad, QB_no_analog_triggers | QB_alt_button_mapping},
   // 8bitdo N30 Pro Controller
   {0x2dc8, 0x9001, InputDevice::DeviceClass::gamepad, QB_rstick_from_z},
+  // Generic gamepad
+  {0x0810, 0x0001, InputDevice::DeviceClass::gamepad, QB_no_analog_triggers | QB_alt_button_mapping | QB_rstick_from_z | QB_right_axes_swapped},
+  // Generic gamepad without sticks
+  {0x0810, 0xe501, InputDevice::DeviceClass::gamepad, QB_no_analog_triggers | QB_alt_button_mapping},
   // 3Dconnexion Space Traveller 3D Mouse
   {0x046d, 0xc623, InputDevice::DeviceClass::spatial_mouse, 0},
   // 3Dconnexion Space Pilot 3D Mouse
@@ -488,10 +501,16 @@ init_device() {
           break;
         case ABS_Z:
           if (quirks & QB_rstick_from_z) {
-            axis = InputDevice::Axis::right_x;
+            if (quirks & QB_right_axes_swapped) {
+              axis = InputDevice::Axis::right_y;
+            } else {
+              axis = InputDevice::Axis::right_x;
+            }
           } else if (_device_class == DeviceClass::gamepad) {
-            axis = InputDevice::Axis::left_trigger;
-            have_analog_triggers = true;
+            if ((quirks & QB_no_analog_triggers) == 0) {
+              axis = InputDevice::Axis::left_trigger;
+              have_analog_triggers = true;
+            }
           } else if (_device_class == DeviceClass::spatial_mouse) {
             axis = InputDevice::Axis::z;
           } else {
@@ -514,10 +533,19 @@ init_device() {
           break;
         case ABS_RZ:
           if (quirks & QB_rstick_from_z) {
-            axis = InputDevice::Axis::right_y;
+            if (quirks & QB_right_axes_swapped) {
+              axis = InputDevice::Axis::right_x;
+            } else {
+              axis = InputDevice::Axis::right_y;
+            }
           } else if (_device_class == DeviceClass::gamepad) {
-            axis = InputDevice::Axis::right_trigger;
-            have_analog_triggers = true;
+            if ((quirks & QB_no_analog_triggers) == 0) {
+              axis = InputDevice::Axis::right_trigger;
+              have_analog_triggers = true;
+            } else {
+              // Special weird case for Trust GXT 24
+              axis = InputDevice::Axis::right_y;
+            }
           } else {
             axis = InputDevice::Axis::yaw;
           }
@@ -537,8 +565,10 @@ init_device() {
           break;
         case ABS_GAS:
           if (_device_class == DeviceClass::gamepad) {
-            axis = InputDevice::Axis::right_trigger;
-            have_analog_triggers = true;
+            if ((quirks & QB_no_analog_triggers) == 0) {
+              axis = InputDevice::Axis::right_trigger;
+              have_analog_triggers = true;
+            }
           } else {
             axis = InputDevice::Axis::accelerator;
           }
@@ -963,6 +993,26 @@ map_button(int code, DeviceClass device_class, int quirks) {
       // BTN_THUMB and BTN_THUMB2 detect touching the touchpads.
       return ButtonHandle::none();
 
+    } else if (device_class == DeviceClass::gamepad &&
+               (quirks & QB_alt_button_mapping) != 0) {
+      static const ButtonHandle mapping[] = {
+        GamepadButton::face_y(),
+        GamepadButton::face_b(),
+        GamepadButton::face_a(),
+        GamepadButton::face_x(),
+        GamepadButton::lshoulder(),
+        GamepadButton::rshoulder(),
+        GamepadButton::ltrigger(),
+        GamepadButton::rtrigger(),
+        GamepadButton::back(),
+        GamepadButton::start(),
+        GamepadButton::lstick(),
+        GamepadButton::rstick(),
+      };
+      if ((code & 0xf) < 12) {
+        return mapping[code & 0xf];
+      }
+
     } else if (device_class == DeviceClass::gamepad) {
       // Based on "Jess Tech Colour Rumble Pad"
       static const ButtonHandle mapping[] = {

+ 4 - 1
panda/src/device/ioKitInputDevice.cxx

@@ -22,8 +22,11 @@
 #include "mouseButton.h"
 
 static void removal_callback(void *ctx, IOReturn result, void *sender) {
-  IOKitInputDevice *input_device = (IOKitInputDevice *)ctx;
+  // We need to hold a reference to this because it may otherwise be destroyed
+  // during the call to on_remove().
+  PT(IOKitInputDevice) input_device = (IOKitInputDevice *)ctx;
   nassertv(input_device != nullptr);
+  nassertv(input_device->test_ref_count_integrity());
   input_device->on_remove();
 }
 

+ 3 - 0
panda/src/device/linuxInputDeviceManager.cxx

@@ -61,6 +61,9 @@ LinuxInputDeviceManager() {
 
     // We'll want to sort the devices by index, since the order may be
     // meaningful (eg. for the Xbox wireless receiver).
+    if (indices.empty()) {
+      return;
+    }
     std::sort(indices.begin(), indices.end());
     _evdev_devices.resize(indices.back() + 1, nullptr);
 

+ 42 - 7
panda/src/device/winRawInputDevice.cxx

@@ -32,6 +32,12 @@ enum QuirkBits : int {
 
   // Throttle is reversed.
   QB_reversed_throttle = 4,
+
+  // Right stick uses Z and Rz inputs.
+  QB_rstick_from_z = 8,
+
+  // Axes on the right stick are swapped, using x for y and vice versa.
+  QB_right_axes_swapped = 64,
 };
 
 // Some nonstandard gamepads have different button mappings.
@@ -42,12 +48,17 @@ static const struct DeviceMapping {
   int quirks;
   const char *buttons[16];
 } mapping_presets[] = {
-  // SNES-style USB gamepad
+  // SNES-style USB gamepad, or cheap unbranded USB gamepad with no sticks
+  // ABXY are mapped based on their position, not based on their label.
   {0x0810, 0xe501, InputDevice::DeviceClass::gamepad, QB_no_analog_triggers,
-    {"face_x", "face_a", "face_b", "face_y", "lshoulder", "rshoulder", "none", "none", "back", "start"}
+    {"face_y", "face_b", "face_a", "face_x", "lshoulder", "rshoulder", "ltrigger", "rtrigger", "back", "start"}
+  },
+  // Unbranded generic cheap USB gamepad
+  {0x0810, 0x0001, InputDevice::DeviceClass::gamepad, QB_rstick_from_z | QB_no_analog_triggers | QB_right_axes_swapped,
+    {"face_y", "face_b", "face_a", "face_x", "lshoulder", "rshoulder", "ltrigger", "rtrigger", "back", "start", "lstick", "rstick"}
   },
-  // SPEED Link SL-6535-SBK-01
-  {0x0079, 0x0006, InputDevice::DeviceClass::gamepad, QB_no_analog_triggers,
+  // Trust GXT 24 / SPEED Link SL-6535-SBK-01
+  {0x0079, 0x0006, InputDevice::DeviceClass::gamepad, QB_rstick_from_z | QB_no_analog_triggers,
     {"face_y", "face_b", "face_a", "face_x", "lshoulder", "rshoulder", "ltrigger", "rtrigger", "back", "start", "lstick", "rstick"}
   },
   // T.Flight Hotas X
@@ -56,7 +67,7 @@ static const struct DeviceMapping {
   },
   // NVIDIA Shield Controller
   {0x0955, 0x7214, InputDevice::DeviceClass::gamepad, 0,
-    {"face_a", "face_b", "n", "face_x", "face_y", "rshoulder", "lshoulder", "rshoulder", "e", "f", "g", "start", "h", "lstick", "rstick", "i"}
+    {"face_a", "face_b", 0, "face_x", "face_y", "rshoulder", "lshoulder", "rshoulder", 0, 0, 0, "start", 0, "lstick", "rstick", 0}
   },
   {0},
 };
@@ -422,7 +433,14 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
           break;
         case HID_USAGE_GENERIC_Z:
           if (_device_class == DeviceClass::gamepad) {
-            if ((quirks & QB_no_analog_triggers) == 0) {
+            if (quirks & QB_rstick_from_z) {
+              if (quirks & QB_right_axes_swapped) {
+                axis = InputDevice::Axis::right_y;
+                swap(cap.LogicalMin, cap.LogicalMax);
+              } else {
+                axis = InputDevice::Axis::right_x;
+              }
+            } else if ((quirks & QB_no_analog_triggers) == 0) {
               axis = Axis::left_trigger;
             }
           } else if (_device_class == DeviceClass::flight_stick) {
@@ -455,7 +473,14 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
           break;
         case HID_USAGE_GENERIC_RZ:
           if (_device_class == DeviceClass::gamepad) {
-            if ((quirks & QB_no_analog_triggers) == 0) {
+            if (quirks & QB_rstick_from_z) {
+              if (quirks & QB_right_axes_swapped) {
+                axis = InputDevice::Axis::right_x;
+              } else {
+                axis = InputDevice::Axis::right_y;
+                swap(cap.LogicalMin, cap.LogicalMax);
+              }
+            } else if ((quirks & QB_no_analog_triggers) == 0) {
               axis = Axis::right_trigger;
             }
           } else {
@@ -481,6 +506,16 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
         break;
       }
 
+      // If this axis already exists, don't double-map it, but take the first
+      // one.  This is important for the Trust GXT 24 / SL-6535-SBK-01 which
+      // have a weird extra Z axis with DataIndex 2 that should be ignored.
+      for (size_t i = 0; i < _axes.size(); ++i) {
+        if (_axes[i].axis == axis) {
+          axis = Axis::none;
+          break;
+        }
+      }
+
       int axis_index;
       if (!is_signed) {
         // All axes on the weird XInput-style mappings go from -1 to 1

+ 8 - 0
panda/src/display/graphicsStateGuardian.cxx

@@ -1901,6 +1901,14 @@ fetch_ptr_parameter(const Shader::ShaderPtrSpec& spec) {
   return (_target_shader->get_shader_input_ptr(spec._arg));
 }
 
+/**
+ *
+ */
+bool GraphicsStateGuardian::
+fetch_ptr_parameter(const Shader::ShaderPtrSpec& spec, Shader::ShaderPtrData &data) {
+  return _target_shader->get_shader_input_ptr(spec._arg, data);
+}
+
 /**
  * Makes the specified DisplayRegion current.  All future drawing and clear
  * operations will be constrained within the given DisplayRegion.

+ 1 - 0
panda/src/display/graphicsStateGuardian.h

@@ -339,6 +339,7 @@ public:
   PT(Texture) fetch_specified_texture(Shader::ShaderTexSpec &spec,
                                       SamplerState &sampler, int &view);
   const Shader::ShaderPtrData *fetch_ptr_parameter(const Shader::ShaderPtrSpec& spec);
+  bool fetch_ptr_parameter(const Shader::ShaderPtrSpec &spec, Shader::ShaderPtrData &data);
 
   virtual void prepare_display_region(DisplayRegionPipelineReader *dr);
   virtual void clear_before_callback();

+ 1 - 4
panda/src/express/multifile.cxx

@@ -2068,10 +2068,7 @@ open_read_subfile(Subfile *subfile) {
     stream = wrapper;
 
     // Validate the password by confirming that the encryption header matches.
-    char this_header[_encrypt_header_size];
-    stream->read(this_header, _encrypt_header_size);
-    if (stream->fail() || stream->gcount() != (unsigned)_encrypt_header_size ||
-        memcmp(this_header, _encrypt_header, _encrypt_header_size) != 0) {
+    if (!wrapper->read_magic(_encrypt_header, _encrypt_header_size)) {
       express_cat.error()
         << "Unable to decrypt subfile " << subfile->_name << ".\n";
       delete stream;

+ 2 - 2
panda/src/express/zStreamBuf.cxx

@@ -202,8 +202,8 @@ seekoff(streamoff off, ios_seekdir dir, ios_openmode which) {
 
   gbump(n);
 
-  _source->seekg(0, ios::beg);
-  if (_source->tellg() == (streampos)0) {
+  if (_source->rdbuf()->pubseekpos(0, ios::in) == (streampos)0) {
+    _source->clear();
     _z_source.next_in = Z_NULL;
     _z_source.avail_in = 0;
     _z_source.next_out = Z_NULL;

+ 2 - 1
panda/src/ffmpeg/ffmpegAudioCursor.cxx

@@ -462,7 +462,7 @@ seek(double t) {
  * read.  Your buffer must be equal in size to N * channels.  Multiple-channel
  * audio will be interleaved.
  */
-void FfmpegAudioCursor::
+int FfmpegAudioCursor::
 read_samples(int n, int16_t *data) {
   int desired = n * _audio_channels;
 
@@ -486,4 +486,5 @@ read_samples(int n, int16_t *data) {
 
   }
   _samples_read += n;
+  return n;
 }

+ 1 - 1
panda/src/ffmpeg/ffmpegAudioCursor.h

@@ -45,7 +45,7 @@ PUBLISHED:
   virtual void seek(double offset);
 
 public:
-  virtual void read_samples(int n, int16_t *data);
+  virtual int read_samples(int n, int16_t *data);
 
 protected:
   void fetch_packet();

+ 3 - 1
panda/src/ffmpeg/ffmpegVideoCursor.cxx

@@ -755,7 +755,9 @@ do_poll() {
       PT(FfmpegBuffer) frame = do_alloc_frame();
       nassertr(frame != nullptr, false);
       _lock.release();
-      advance_to_frame(seek_frame);
+      if (seek_frame != _begin_frame) {
+        advance_to_frame(seek_frame);
+      }
       if (_frame_ready) {
         export_frame(frame);
         _lock.acquire();

+ 25 - 2
panda/src/glstuff/glGraphicsStateGuardian_src.cxx

@@ -597,8 +597,31 @@ reset() {
   query_glsl_version();
 
 #ifndef OPENGLES
-  bool core_profile = is_at_least_gl_version(3, 2) &&
-                      !has_extension("GL_ARB_compatibility");
+  // Determine whether this OpenGL context has compatibility features.
+  bool core_profile = false;
+
+  if (_gl_version_major >= 3) {
+    if (_gl_version_major > 3 || _gl_version_minor >= 2) {
+      // OpenGL 3.2 has a built-in way to check this.
+      GLint profile_mask = 0;
+      glGetIntegerv(GL_CONTEXT_PROFILE_MASK, &profile_mask);
+
+      if (profile_mask & GL_CONTEXT_CORE_PROFILE_BIT) {
+        core_profile = true;
+      } else if (profile_mask & GL_CONTEXT_COMPATIBILITY_PROFILE_BIT) {
+        core_profile = false;
+      } else {
+        core_profile = !has_extension("GL_ARB_compatibility");
+      }
+    } else {
+      // OpenGL 3.0/3.1.
+      GLint flags = 0;
+      glGetIntegerv(GL_CONTEXT_FLAGS, &flags);
+      if (flags & GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT) {
+        core_profile = true;
+      }
+    }
+  }
 
   if (GLCAT.is_debug()) {
     if (core_profile) {

+ 24 - 20
panda/src/glstuff/glShaderContext_src.cxx

@@ -2110,8 +2110,8 @@ issue_parameters(int altered) {
     for (int i = 0; i < (int)_shader->_ptr_spec.size(); ++i) {
       Shader::ShaderPtrSpec &spec = _shader->_ptr_spec[i];
 
-      const Shader::ShaderPtrData* ptr_data = _glgsg->fetch_ptr_parameter(spec);
-      if (ptr_data == nullptr) { //the input is not contained in ShaderPtrData
+      Shader::ShaderPtrData ptr_data;
+      if (!_glgsg->fetch_ptr_parameter(spec, ptr_data)) { //the input is not contained in ShaderPtrData
         release_resources();
         return;
       }
@@ -2119,18 +2119,18 @@ issue_parameters(int altered) {
       nassertd(spec._dim[1] > 0) continue;
 
       GLint p = spec._id._seqno;
-      int array_size = min(spec._dim[0], (int)ptr_data->_size / spec._dim[1]);
+      int array_size = min(spec._dim[0], (int)ptr_data._size / spec._dim[1]);
       switch (spec._type) {
       case Shader::SPT_float:
         {
           float *data = nullptr;
 
-          switch (ptr_data->_type) {
+          switch (ptr_data._type) {
           case Shader::SPT_int:
             // Convert int data to float data.
             data = (float*) alloca(sizeof(float) * array_size * spec._dim[1]);
             for (int i = 0; i < (array_size * spec._dim[1]); ++i) {
-              data[i] = (float)(((int*)ptr_data->_ptr)[i]);
+              data[i] = (float)(((int*)ptr_data._ptr)[i]);
             }
             break;
 
@@ -2138,7 +2138,7 @@ issue_parameters(int altered) {
             // Convert unsigned int data to float data.
             data = (float*) alloca(sizeof(float) * array_size * spec._dim[1]);
             for (int i = 0; i < (array_size * spec._dim[1]); ++i) {
-              data[i] = (float)(((unsigned int*)ptr_data->_ptr)[i]);
+              data[i] = (float)(((unsigned int*)ptr_data._ptr)[i]);
             }
             break;
 
@@ -2146,12 +2146,12 @@ issue_parameters(int altered) {
             // Downgrade double data to float data.
             data = (float*) alloca(sizeof(float) * array_size * spec._dim[1]);
             for (int i = 0; i < (array_size * spec._dim[1]); ++i) {
-              data[i] = (float)(((double*)ptr_data->_ptr)[i]);
+              data[i] = (float)(((double*)ptr_data._ptr)[i]);
             }
             break;
 
           case Shader::SPT_float:
-            data = (float*)ptr_data->_ptr;
+            data = (float*)ptr_data._ptr;
             break;
 
           default:
@@ -2171,8 +2171,8 @@ issue_parameters(int altered) {
         break;
 
       case Shader::SPT_int:
-        if (ptr_data->_type != Shader::SPT_int &&
-            ptr_data->_type != Shader::SPT_uint) {
+        if (ptr_data._type != Shader::SPT_int &&
+            ptr_data._type != Shader::SPT_uint) {
           GLCAT.error()
             << "Cannot pass floating-point data to integer shader input '" << spec._id._name << "'\n";
 
@@ -2183,18 +2183,18 @@ issue_parameters(int altered) {
 
         } else {
           switch (spec._dim[1]) {
-          case 1: _glgsg->_glUniform1iv(p, array_size, (int*)ptr_data->_ptr); continue;
-          case 2: _glgsg->_glUniform2iv(p, array_size, (int*)ptr_data->_ptr); continue;
-          case 3: _glgsg->_glUniform3iv(p, array_size, (int*)ptr_data->_ptr); continue;
-          case 4: _glgsg->_glUniform4iv(p, array_size, (int*)ptr_data->_ptr); continue;
+          case 1: _glgsg->_glUniform1iv(p, array_size, (int*)ptr_data._ptr); continue;
+          case 2: _glgsg->_glUniform2iv(p, array_size, (int*)ptr_data._ptr); continue;
+          case 3: _glgsg->_glUniform3iv(p, array_size, (int*)ptr_data._ptr); continue;
+          case 4: _glgsg->_glUniform4iv(p, array_size, (int*)ptr_data._ptr); continue;
           }
           nassertd(false) continue;
         }
         break;
 
       case Shader::SPT_uint:
-        if (ptr_data->_type != Shader::SPT_uint &&
-            ptr_data->_type != Shader::SPT_int) {
+        if (ptr_data._type != Shader::SPT_uint &&
+            ptr_data._type != Shader::SPT_int) {
           GLCAT.error()
             << "Cannot pass floating-point data to integer shader input '" << spec._id._name << "'\n";
 
@@ -2205,10 +2205,10 @@ issue_parameters(int altered) {
 
         } else {
           switch (spec._dim[1]) {
-          case 1: _glgsg->_glUniform1uiv(p, array_size, (GLuint *)ptr_data->_ptr); continue;
-          case 2: _glgsg->_glUniform2uiv(p, array_size, (GLuint *)ptr_data->_ptr); continue;
-          case 3: _glgsg->_glUniform3uiv(p, array_size, (GLuint *)ptr_data->_ptr); continue;
-          case 4: _glgsg->_glUniform4uiv(p, array_size, (GLuint *)ptr_data->_ptr); continue;
+          case 1: _glgsg->_glUniform1uiv(p, array_size, (GLuint *)ptr_data._ptr); continue;
+          case 2: _glgsg->_glUniform2uiv(p, array_size, (GLuint *)ptr_data._ptr); continue;
+          case 3: _glgsg->_glUniform3uiv(p, array_size, (GLuint *)ptr_data._ptr); continue;
+          case 4: _glgsg->_glUniform4uiv(p, array_size, (GLuint *)ptr_data._ptr); continue;
           }
           nassertd(false) continue;
         }
@@ -3206,6 +3206,10 @@ glsl_compile_and_link() {
     valid &= glsl_compile_shader(Shader::ST_compute);
   }
 
+  if (!valid) {
+    return false;
+  }
+
   // There might be warnings, so report those.  GLSLShaders::const_iterator
   // it; for (it = _glsl_shaders.begin(); it != _glsl_shaders.end(); ++it) {
   // glsl_report_shader_errors(*it); }

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

@@ -17,6 +17,11 @@ ConfigVariableInt gl_version
   ("gl-version", "",
    PRC_DESC("Set this to get an OpenGL context with a specific version."));
 
+ConfigVariableBool gl_forward_compatible
+  ("gl-forward-compatible", false,
+   PRC_DESC("Setting this to true will request a forward-compatible OpenGL "
+            "context, which will not support the fixed-function pipeline."));
+
 ConfigVariableBool gl_support_fbo
   ("gl-support-fbo", true,
    PRC_DESC("Configure this false if your GL's implementation of "

+ 1 - 0
panda/src/glstuff/glmisc_src.h

@@ -41,6 +41,7 @@
 // #define GSG_VERBOSE 1
 
 extern EXPCL_GL ConfigVariableInt gl_version;
+extern EXPCL_GL ConfigVariableBool gl_forward_compatible;
 extern EXPCL_GL ConfigVariableBool gl_support_fbo;
 extern ConfigVariableBool gl_cheap_textures;
 extern ConfigVariableBool gl_ignore_clamp;

+ 12 - 1
panda/src/glxdisplay/glxGraphicsStateGuardian.cxx

@@ -352,9 +352,20 @@ choose_pixel_format(const FrameBufferProperties &properties,
           attrib_list[n++] = gl_version[1];
         }
       }
+      int flags = 0;
       if (gl_debug) {
+        flags |= GLX_CONTEXT_DEBUG_BIT_ARB;
+      }
+      if (gl_forward_compatible) {
+        flags |= GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB;
+        if (gl_version.get_num_words() == 0 || gl_version[0] < 2) {
+          glxdisplay_cat.error()
+            << "gl-forward-compatible requires gl-version >= 3 0\n";
+        }
+      }
+      if (flags != 0) {
         attrib_list[n++] = GLX_CONTEXT_FLAGS_ARB;
-        attrib_list[n++] = GLX_CONTEXT_DEBUG_BIT_ARB;
+        attrib_list[n++] = flags;
       }
       attrib_list[n] = None;
       _context = _glXCreateContextAttribs(_display, _fbconfig, _share_context,

+ 0 - 8
panda/src/linmath/lmatrix4_src.I

@@ -484,13 +484,9 @@ get_col(int col) const {
  */
 INLINE_LINMATH FLOATNAME(LVecBase3) FLOATNAME(LMatrix4)::
 get_row3(int row) const {
-#ifdef HAVE_EIGEN
-  return FLOATNAME(LVecBase3)(_m.block<1, 3>(row, 0));
-#else
   return FLOATNAME(LVecBase3)((*this)(row, 0),
                               (*this)(row, 1),
                               (*this)(row, 2));
-#endif  // HAVE_EIGEN
 }
 
 /**
@@ -514,13 +510,9 @@ get_row3(FLOATNAME(LVecBase3) &result_vec,int row) const {
  */
 INLINE_LINMATH FLOATNAME(LVecBase3) FLOATNAME(LMatrix4)::
 get_col3(int col) const {
-#ifdef HAVE_EIGEN
-  return FLOATNAME(LVecBase3)(_m.block<1, 3>(0, col));
-#else
   return FLOATNAME(LVecBase3)((*this)(0, col),
                               (*this)(1, col),
                               (*this)(2, col));
-#endif  // HAVE_EIGEN
 }
 
 /**

+ 7 - 0
panda/src/mathutil/perlinNoise2.cxx

@@ -19,6 +19,9 @@
  */
 double PerlinNoise2::
 noise(const LVecBase2d &value) const {
+  // If this triggers, you passed in 0 for table_size.
+  nassertr(!_index.empty(), make_nan(0.0));
+
   // Convert the vector to our local coordinate space.
   LVecBase2d vec = _input_xform.xform_point(value);
 
@@ -41,9 +44,13 @@ noise(const LVecBase2d &value) const {
   double v = fade(y);
 
   // Hash coordinates of the 4 square corners (A, B, A + 1, and B + 1)
+  nassertr(X >= 0 && X + 1 < _index.size(), make_nan(0.0));
   int A = _index[X] + Y;
   int B = _index[X + 1] + Y;
 
+  nassertr(A >= 0 && A + 1 < _index.size(), make_nan(0.0));
+  nassertr(B >= 0 && B + 1 < _index.size(), make_nan(0.0));
+
   // and add blended results from 4 corners of square.
   double result =
     lerp(v, lerp(u, grad(_index[A], x, y),

+ 4 - 2
panda/src/movies/flacAudioCursor.cxx

@@ -118,8 +118,10 @@ seek(double t) {
  * read.  Your buffer must be equal in size to N * channels.  Multiple-channel
  * audio will be interleaved.
  */
-void FlacAudioCursor::
+int FlacAudioCursor::
 read_samples(int n, int16_t *data) {
   int desired = n * _audio_channels;
-  _samples_read += drflac_read_s16(_drflac, desired, data) / _audio_channels;
+  n = drflac_read_s16(_drflac, desired, data) / _audio_channels;
+  _samples_read += n;
+  return n;
 }

+ 1 - 1
panda/src/movies/flacAudioCursor.h

@@ -37,7 +37,7 @@ PUBLISHED:
   virtual void seek(double offset);
 
 public:
-  virtual void read_samples(int n, int16_t *data);
+  virtual int read_samples(int n, int16_t *data);
 
   bool _is_valid;
 

+ 3 - 2
panda/src/movies/microphoneAudioDS.cxx

@@ -91,7 +91,7 @@ public:
   int _samples_per_buffer;
 
 public:
-  virtual void read_samples(int n, int16_t *data);
+  virtual int read_samples(int n, int16_t *data);
   virtual int ready() const;
 
 public:
@@ -323,7 +323,7 @@ MicrophoneAudioCursorDS::
 /**
  *
  */
-void MicrophoneAudioCursorDS::
+int MicrophoneAudioCursorDS::
 read_samples(int n, int16_t *data) {
   int orign = n;
   if (_handle) {
@@ -373,6 +373,7 @@ read_samples(int n, int16_t *data) {
   if (n > 0) {
     memcpy(data, 0, n*2*_audio_channels);
   }
+  return orign - n;
 }
 
 /**

+ 18 - 12
panda/src/movies/movieAudioCursor.cxx

@@ -45,14 +45,14 @@ MovieAudioCursor::
  * read.  Your buffer must be equal in size to N * channels.  Multiple-channel
  * audio will be interleaved.
  */
-void MovieAudioCursor::
+int MovieAudioCursor::
 read_samples(int n, int16_t *data) {
 
   // This is the null implementation, which generates pure silence.  Normally,
   // this method will be overridden by a subclass.
 
   if (n <= 0) {
-    return;
+    return 0;
   }
 
   int desired = n * _audio_channels;
@@ -60,6 +60,7 @@ read_samples(int n, int16_t *data) {
     data[i] = 0;
   }
   _samples_read += n;
+  return n;
 }
 
 /**
@@ -92,23 +93,28 @@ read_samples(int n, Datagram *dg) {
  * This is not particularly efficient, but it may be a convenient way to
  * manipulate samples in python.
  */
-std::string MovieAudioCursor::
+vector_uchar MovieAudioCursor::
 read_samples(int n) {
-  std::ostringstream result;
+  vector_uchar result;
   int16_t tmp[4096];
   while (n > 0) {
     int blocksize = (4096 / _audio_channels);
-    if (blocksize > n) blocksize = n;
-    int words = blocksize * _audio_channels;
-    read_samples(blocksize, tmp);
-    for (int i=0; i<words; i++) {
+    if (blocksize > n) {
+      blocksize = n;
+    }
+    int nread = read_samples(blocksize, tmp);
+    if (nread == 0) {
+      return result;
+    }
+    int words = nread * _audio_channels;
+    for (int i = 0; i < words; ++i) {
       int16_t word = tmp[i];
-      result.put((char)(word & 255));
-      result.put((char)((word>>8) & 255));
+      result.push_back((uint8_t)(word & 255u));
+      result.push_back((uint8_t)((word >> 8) & 255u));
     }
-    n -= blocksize;
+    n -= nread;
   }
-  return result.str();
+  return result;
 }
 
 

+ 2 - 2
panda/src/movies/movieAudioCursor.h

@@ -48,10 +48,10 @@ PUBLISHED:
   virtual int ready() const;
   virtual void seek(double offset);
   void read_samples(int n, Datagram *dg);
-  std::string read_samples(int n);
+  vector_uchar read_samples(int n);
 
 public:
-  virtual void read_samples(int n, int16_t *data);
+  virtual int read_samples(int n, int16_t *data);
 
 protected:
   PT(MovieAudio) _source;

+ 12 - 0
panda/src/movies/movieTypeRegistry.cxx

@@ -31,6 +31,12 @@ PT(MovieAudio) MovieTypeRegistry::
 make_audio(const Filename &name) {
   string ext = downcase(name.get_extension());
 
+#ifdef HAVE_ZLIB
+  if (ext == "pz" || ext == "gz") {
+    ext = Filename(name.get_basename_wo_extension()).get_extension();
+  }
+#endif
+
   _audio_lock.lock();
 
   // Make sure that the list of audio types has been read in.
@@ -154,6 +160,12 @@ PT(MovieVideo) MovieTypeRegistry::
 make_video(const Filename &name) {
   string ext = downcase(name.get_extension());
 
+#ifdef HAVE_ZLIB
+  if (ext == "pz" || ext == "gz") {
+    ext = Filename(name.get_basename_wo_extension()).get_extension();
+  }
+#endif
+
   _video_lock.lock();
 
   // Make sure that the list of video types has been read in.

+ 55 - 12
panda/src/movies/opusAudioCursor.cxx

@@ -56,6 +56,22 @@ int cb_seek(void *stream, opus_int64 offset, int whence) {
     break;
 
   case SEEK_CUR:
+    // opusfile uses a seek with offset 0 to determine whether seeking is
+    // supported, but this is not good enough.  We seek to the end and back.
+    if (offset == 0) {
+      std::streambuf *buf = in->rdbuf();
+      std::streampos pos = buf->pubseekoff(0, std::ios::cur, std::ios::in);
+      if (pos < 0) {
+        return -1;
+      }
+      if (buf->pubseekoff(0, std::ios::end, std::ios::in) >= 0) {
+        // It worked; seek back to the previous location.
+        buf->pubseekpos(pos, std::ios::in);
+        return 0;
+      } else {
+        return -1;
+      }
+    }
     in->seekg(offset, std::ios::cur);
     break;
 
@@ -101,16 +117,7 @@ opus_int64 cb_tell(void *stream) {
   return in->tellg();
 }
 
-int cb_close(void *stream) {
-  istream *in = (istream *)stream;
-  nassertr(in != nullptr, EOF);
-
-  VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr();
-  vfs->close_read_file(in);
-  return 0;
-}
-
-static const OpusFileCallbacks callbacks = {cb_read, cb_seek, cb_tell, cb_close};
+static const OpusFileCallbacks callbacks = {cb_read, cb_seek, cb_tell, nullptr};
 
 TypeHandle OpusAudioCursor::_type_handle;
 
@@ -122,6 +129,7 @@ OpusAudioCursor::
 OpusAudioCursor(OpusAudio *src, istream *stream) :
   MovieAudioCursor(src),
   _is_valid(false),
+  _stream(stream),
   _link(0)
 {
   nassertv(stream != nullptr);
@@ -159,6 +167,11 @@ OpusAudioCursor::
     op_free(_op);
     _op = nullptr;
   }
+
+  if (_stream != nullptr) {
+    VirtualFileSystem::close_read_file(_stream);
+    _stream = nullptr;
+  }
 }
 
 /**
@@ -174,7 +187,32 @@ seek(double t) {
   t = std::max(t, 0.0);
 
   // Use op_time_seek_lap if cross-lapping is enabled.
-  int error = op_pcm_seek(_op, (ogg_int64_t)(t * 48000.0));
+  ogg_int64_t sample = (ogg_int64_t)(t * 48000.0);
+  int error = op_pcm_seek(_op, sample);
+
+  // Special case for seeking to the beginning; if normal seek fails, we may
+  // be able to explicitly seek to the beginning of the file and call op_open
+  // again.  This allows looping compressed .opus files.
+  if (error == OP_ENOSEEK && sample == 0) {
+    if (_stream->rdbuf()->pubseekpos(0, std::ios::in) == (std::streampos)0) {
+      OggOpusFile *op = op_open_callbacks((void *)_stream, &callbacks, nullptr, 0, nullptr);
+      if (op != nullptr) {
+        op_free(_op);
+        _op = op;
+      } else {
+        movies_cat.error()
+          << "Failed to reopen Opus file to seek to beginning.\n";
+        return;
+      }
+
+      // Reset this field for good measure, just in case this changed.
+      _audio_channels = op_channel_count(_op, -1);
+
+      _last_seek = 0.0;
+      _samples_read = 0;
+      return;
+    }
+  }
   if (error != 0) {
     movies_cat.error()
       << "Seek failed (error " << error << ").  Opus stream may not be seekable.\n";
@@ -190,7 +228,7 @@ seek(double t) {
  * read.  Your buffer must be equal in size to N * channels.  Multiple-channel
  * audio will be interleaved.
  */
-void OpusAudioCursor::
+int OpusAudioCursor::
 read_samples(int n, int16_t *data) {
   int16_t *end = data + (n * _audio_channels);
 
@@ -202,6 +240,9 @@ read_samples(int n, int16_t *data) {
       data += read_samples * _audio_channels;
       _samples_read += read_samples;
     } else {
+      if (read_samples == 0 && _length == 1.0E10) {
+        _length = op_pcm_tell(_op) / 48000.0;
+      }
       break;
     }
 
@@ -224,7 +265,9 @@ read_samples(int n, int16_t *data) {
   // Fill the rest of the buffer with silence.
   if (data < end) {
     memset(data, 0, (unsigned char *)end - (unsigned char *)data);
+    n -= (end - data) / _audio_channels;
   }
+  return n;
 }
 
 #endif // HAVE_OPUS

+ 2 - 10
panda/src/movies/opusAudioCursor.h

@@ -39,22 +39,14 @@ PUBLISHED:
   virtual void seek(double offset);
 
 public:
-  virtual void read_samples(int n, int16_t *data);
+  virtual int read_samples(int n, int16_t *data);
 
   bool _is_valid;
 
 protected:
   OggOpusFile *_op;
-
+  std::istream *_stream;
   int _link;
-  double _byte_rate;
-  int _block_align;
-  int _bytes_per_sample;
-  bool _is_float;
-
-  std::streampos _data_start;
-  std::streampos _data_pos;
-  size_t _data_size;
 
 public:
   static TypeHandle get_class_type() {

+ 8 - 5
panda/src/movies/userDataAudio.cxx

@@ -57,12 +57,14 @@ open() {
  * read.  Your buffer must be equal in size to N * channels.  Multiple-channel
  * audio will be interleaved.
  */
-void UserDataAudio::
+int UserDataAudio::
 read_samples(int n, int16_t *data) {
   int ready = (_data.size() / _desired_channels);
   int desired = n * _desired_channels;
-  int avail = ready * _desired_channels;
-  if (avail > desired) avail = desired;
+  if (n > ready) {
+    n = ready;
+  }
+  int avail = n * _desired_channels;
   for (int i=0; i<avail; i++) {
     data[i] = _data[i];
   }
@@ -72,6 +74,7 @@ read_samples(int n, int16_t *data) {
   for (int i=0; i<avail; i++) {
     _data.pop_front();
   }
+  return n;
 }
 
 /**
@@ -107,11 +110,11 @@ append(DatagramIterator *src, int n) {
  * but it may be convenient to deal with samples in python.
  */
 void UserDataAudio::
-append(const std::string &str) {
+append(const vector_uchar &str) {
   nassertv(!_aborted);
   int samples = str.size() / (2 * _desired_channels);
   int words = samples * _desired_channels;
-  for (int i=0; i<words; i++) {
+  for (int i = 0; i < words; ++i) {
     int c1 = ((unsigned char)str[i*2+0]);
     int c2 = ((unsigned char)str[i*2+1]);
     int16_t n = (c1 | (c2 << 8));

+ 2 - 2
panda/src/movies/userDataAudio.h

@@ -37,11 +37,11 @@ class EXPCL_PANDA_MOVIES UserDataAudio : public MovieAudio {
 
   void append(int16_t *data, int n);
   void append(DatagramIterator *src, int len=0x40000000);
-  void append(const std::string &str);
+  void append(const vector_uchar &);
   void done(); // A promise not to write any more samples.
 
  private:
-  void read_samples(int n, int16_t *data);
+  int read_samples(int n, int16_t *data);
   void update_cursor();
   int _desired_rate;
   int _desired_channels;

+ 6 - 3
panda/src/movies/userDataAudioCursor.cxx

@@ -47,12 +47,12 @@ UserDataAudioCursor::
  * read.  Your buffer must be equal in size to N * channels.  Multiple-channel
  * audio will be interleaved.
  */
-void UserDataAudioCursor::
+int UserDataAudioCursor::
 read_samples(int n, int16_t *data) {
   UserDataAudio *source = (UserDataAudio*)(MovieAudio*)_source;
 
-  if(source->_remove_after_read) {
-    source->read_samples(n, data);
+  if (source->_remove_after_read) {
+    n = source->read_samples(n, data);
   }
   else {
     int offset = _samples_read * _audio_channels;
@@ -66,9 +66,12 @@ read_samples(int n, int16_t *data) {
     for (int i=avail; i<desired; i++) {
       data[i] = 0;
     }
+
+    n = avail / _audio_channels;
   }
 
   _samples_read += n;
+  return n;
 }
 
 /**

+ 1 - 1
panda/src/movies/userDataAudioCursor.h

@@ -33,7 +33,7 @@ PUBLISHED:
   virtual ~UserDataAudioCursor();
 
 public:
-  virtual void read_samples(int n, int16_t *data);
+  virtual int read_samples(int n, int16_t *data);
   virtual int ready() const;
   virtual void seek(double offset);
 

+ 56 - 9
panda/src/movies/vorbisAudioCursor.cxx

@@ -91,19 +91,46 @@ seek(double t) {
   t = std::max(t, 0.0);
 
   // Use ov_time_seek_lap if cross-lapping is enabled.
+  int result;
   if (vorbis_seek_lap) {
-    if (ov_time_seek_lap(&_ov, t) != 0) {
-      movies_cat.error()
-        << "Seek failed.  Ogg Vorbis stream may not be seekable.\n";
-      return;
-    }
+    result = ov_time_seek_lap(&_ov, t);
   } else {
-    if (ov_time_seek(&_ov, t) != 0) {
-      movies_cat.error()
-        << "Seek failed.  Ogg Vorbis stream may not be seekable.\n";
+    result = ov_time_seek(&_ov, t);
+  }
+
+  // Special case for seeking to the beginning; if normal seek fails, we may
+  // be able to explicitly seek to the beginning of the file and call ov_open
+  // again.  This allows looping compressed .ogg files.
+  if (result == OV_ENOSEEK && t == 0.0) {
+    std::istream *stream = (std::istream *)_ov.datasource;
+
+    if (stream->rdbuf()->pubseekpos(0, std::ios::in) == (std::streampos)0) {
+      // Back up the callbacks, then destroy the stream, making sure to first
+      // unset the datasource so that it won't close the file.
+      ov_callbacks callbacks = _ov.callbacks;
+      _ov.datasource = nullptr;
+      ov_clear(&_ov);
+
+      if (ov_open_callbacks((void *)stream, &_ov, nullptr, 0, callbacks) != 0) {
+        movies_cat.error()
+          << "Failed to reopen Ogg Vorbis file to seek to beginning.\n";
+        return;
+      }
+
+      // Reset these fields for good measure, just in case the file changed.
+      vorbis_info *vi = ov_info(&_ov, -1);
+      _audio_channels = vi->channels;
+      _audio_rate = vi->rate;
+
+      _last_seek = 0.0;
+      _samples_read = 0;
       return;
     }
   }
+  if (result != 0) {
+    movies_cat.error()
+      << "Seek failed.  Ogg Vorbis stream may not be seekable.\n";
+  }
 
   _last_seek = ov_time_tell(&_ov);
   _samples_read = 0;
@@ -114,7 +141,7 @@ seek(double t) {
  * read.  Your buffer must be equal in size to N * channels.  Multiple-channel
  * audio will be interleaved.
  */
-void VorbisAudioCursor::
+int VorbisAudioCursor::
 read_samples(int n, int16_t *data) {
   int desired = n * _audio_channels;
 
@@ -131,6 +158,9 @@ read_samples(int n, int16_t *data) {
       buffer += read_bytes;
       length -= read_bytes;
     } else {
+      if (read_bytes == 0 && _length == 1.0E10) {
+        _length = ov_time_tell(&_ov);
+      }
       break;
     }
 
@@ -159,6 +189,7 @@ read_samples(int n, int16_t *data) {
   }
 
   _samples_read += n;
+  return n;
 }
 
 /**
@@ -199,6 +230,22 @@ cb_seek_func(void *datasource, ogg_int64_t offset, int whence) {
     break;
 
   case SEEK_CUR:
+    // Vorbis uses a seek with offset 0 to determine whether seeking is
+    // supported, but this is not good enough.  We seek to the end and back.
+    if (offset == 0) {
+      std::streambuf *buf = stream->rdbuf();
+      std::streampos pos = buf->pubseekoff(0, std::ios::cur, std::ios::in);
+      if (pos < 0) {
+        return -1;
+      }
+      if (buf->pubseekoff(0, std::ios::end, std::ios::in) >= 0) {
+        // It worked; seek back to the previous location.
+        buf->pubseekpos(pos, std::ios::in);
+        return 0;
+      } else {
+        return -1;
+      }
+    }
     stream->seekg(offset, std::ios::cur);
     break;
 

+ 1 - 10
panda/src/movies/vorbisAudioCursor.h

@@ -35,7 +35,7 @@ PUBLISHED:
   virtual void seek(double offset);
 
 public:
-  virtual void read_samples(int n, int16_t *data);
+  virtual int read_samples(int n, int16_t *data);
 
   bool _is_valid;
 
@@ -50,16 +50,7 @@ protected:
 #ifndef CPPPARSER
   OggVorbis_File _ov;
 #endif
-
   int _bitstream;
-  double _byte_rate;
-  int _block_align;
-  int _bytes_per_sample;
-  bool _is_float;
-
-  std::streampos _data_start;
-  std::streampos _data_pos;
-  size_t _data_size;
 
 public:
   static TypeHandle get_class_type() {

+ 46 - 10
panda/src/movies/wavAudioCursor.cxx

@@ -294,27 +294,61 @@ seek(double t) {
   t = std::max(t, 0.0);
   std::streampos pos = _data_start + (std::streampos) std::min((size_t) (t * _byte_rate), _data_size);
 
+  std::streambuf *buf = _stream->rdbuf();
+
   if (_can_seek_fast) {
-    _stream->seekg(pos);
-    if (_stream->tellg() != pos) {
+    if (buf->pubseekpos(pos, std::ios::in) != pos) {
       // Clearly, we can't seek fast.  Fall back to the case below.
       _can_seek_fast = false;
     }
   }
 
-  if (!_can_seek_fast) {
-    std::streampos current = _stream->tellg();
+  // Get the current position of the cursor in the file.
+  std::streampos current = buf->pubseekoff(0, std::ios::cur, std::ios::in);
 
+  if (!_can_seek_fast) {
     if (pos > current) {
       // It is ahead of our current position.  Skip ahead.
-      _reader.skip_bytes(pos - current);
+      _stream->ignore(pos - current);
+      current = pos;
 
     } else if (pos < current) {
-      // We'll have to reopen the file.  TODO
+      // Can we seek to the beginning?  Some streams, such as ZStream, let us
+      // rewind the stream.
+      if (buf->pubseekpos(0, std::ios::in) == (std::streampos)0) {
+        if (pos > _data_start && movies_cat.is_info()) {
+          Filename fn = get_source()->get_filename();
+          movies_cat.info()
+            << "Unable to seek backwards in " << fn.get_basename()
+            << "; seeking to beginning and skipping " << pos << " bytes.\n";
+        }
+        _stream->ignore(pos);
+        current = pos;
+      } else {
+        // No; close and reopen the file.
+        Filename fn = get_source()->get_filename();
+        movies_cat.warning()
+          << "Unable to seek backwards in " << fn.get_basename()
+          << "; reopening and skipping " << pos << " bytes.\n";
+
+        VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr();
+        std::istream *stream = vfs->open_read_file(get_source()->get_filename(), true);
+        if (stream != nullptr) {
+          vfs->close_read_file(_stream);
+          stream->ignore(pos);
+          _stream = stream;
+          _reader = StreamReader(stream, false);
+          current = pos;
+        } else {
+          movies_cat.error()
+            << "Unable to reopen " << fn << ".\n";
+          _can_seek = false;
+        }
+      }
     }
   }
 
-  _data_pos = _stream->tellg() - _data_start;
+  _data_pos = (size_t)current - _data_start;
   _last_seek = _data_pos / _byte_rate;
   _samples_read = 0;
 }
@@ -324,13 +358,13 @@ seek(double t) {
  * read.  Your buffer must be equal in size to N * channels.  Multiple-channel
  * audio will be interleaved.
  */
-void WavAudioCursor::
+int WavAudioCursor::
 read_samples(int n, int16_t *data) {
   int desired = n * _audio_channels;
   int read_samples = std::min(desired, ((int) (_data_size - _data_pos)) / _bytes_per_sample);
 
   if (read_samples <= 0) {
-    return;
+    return 0;
   }
 
   switch (_format) {
@@ -421,8 +455,10 @@ read_samples(int n, int16_t *data) {
   // Fill the rest of the buffer with silence.
   if (read_samples < desired) {
     memset(data + read_samples, 0, (desired - read_samples) * 2);
+    n = read_samples / _audio_channels;
   }
 
   _data_pos = _stream->tellg() - _data_start;
-  _samples_read += read_samples / _audio_channels;
+  _samples_read += n;
+  return n;
 }

+ 1 - 1
panda/src/movies/wavAudioCursor.h

@@ -31,7 +31,7 @@ PUBLISHED:
   virtual void seek(double offset);
 
 public:
-  virtual void read_samples(int n, int16_t *data);
+  virtual int read_samples(int n, int16_t *data);
 
   bool _is_valid;
 

+ 1 - 0
panda/src/ode/odeBody.h

@@ -133,6 +133,7 @@ PUBLISHED:
   OdeJoint get_joint(int index) const;
   MAKE_SEQ(get_joints, get_num_joints, get_joint);
   EXTENSION(INLINE PyObject *get_converted_joint(int i) const);
+  MAKE_SEQ_PROPERTY(joints, get_num_joints, get_converted_joint);
 
   INLINE void enable();
   INLINE void disable();

+ 1 - 1
panda/src/ode/odeJoint.h

@@ -83,7 +83,7 @@ PUBLISHED:
   INLINE void set_feedback(bool flag = true);
   INLINE OdeJointFeedback *get_feedback();
 
-  EXTENSION(void attach(const OdeBody *body1, const OdeBody *body2));
+  EXTENSION(void attach(PyObject *body1, PyObject *body2));
   void attach_bodies(const OdeBody &body1, const OdeBody &body2);
   void attach_body(const OdeBody &body, int index);
   void detach();

+ 18 - 1
panda/src/ode/odeJoint_ext.cxx

@@ -29,6 +29,7 @@
 #include "odePlane2dJoint.h"
 
 #ifndef CPPPARSER
+extern Dtool_PyTypedObject Dtool_OdeBody;
 extern Dtool_PyTypedObject Dtool_OdeJoint;
 extern Dtool_PyTypedObject Dtool_OdeBallJoint;
 extern Dtool_PyTypedObject Dtool_OdeHingeJoint;
@@ -48,7 +49,23 @@ extern Dtool_PyTypedObject Dtool_OdePlane2dJoint;
  * attached to the environment.
  */
 void Extension<OdeJoint>::
-attach(const OdeBody *body1, const OdeBody *body2) {
+attach(PyObject *param1, PyObject *param2) {
+  const OdeBody *body1 = nullptr;
+  if (param1 != Py_None) {
+    body1 = (const OdeBody *)DTOOL_Call_GetPointerThisClass(param1, &Dtool_OdeBody, 1, "OdeJoint.attach", true, true);
+    if (body1 == nullptr) {
+      return;
+    }
+  }
+
+  const OdeBody *body2 = nullptr;
+  if (param2 != Py_None) {
+    body2 = (const OdeBody *)DTOOL_Call_GetPointerThisClass(param2, &Dtool_OdeBody, 2, "OdeJoint.attach", true, true);
+    if (body2 == nullptr) {
+      return;
+    }
+  }
+
   if (body1 && body2) {
     _this->attach_bodies(*body1, *body2);
 

+ 1 - 1
panda/src/ode/odeJoint_ext.h

@@ -30,7 +30,7 @@
 template<>
 class Extension<OdeJoint> : public ExtensionBase<OdeJoint> {
 public:
-  void attach(const OdeBody *body1, const OdeBody *body2);
+  void attach(PyObject *body1, PyObject *body2);
 
   PyObject *convert() const;
 };

+ 51 - 0
panda/src/pgraph/shaderAttrib.cxx

@@ -422,6 +422,57 @@ get_shader_input_ptr(const InternalName *id) const {
   }
 }
 
+/**
+ * Returns the ShaderInput as a ShaderPtrData struct.  Assertion fails if
+ * there is none.  or if it is not a PTA(double/float)
+ */
+bool ShaderAttrib::
+get_shader_input_ptr(const InternalName *id, Shader::ShaderPtrData &data) const {
+  Inputs::const_iterator i = _inputs.find(id);
+  if (i != _inputs.end()) {
+    const ShaderInput &p = (*i).second;
+    if (p.get_value_type() == ShaderInput::M_numeric ||
+        p.get_value_type() == ShaderInput::M_vector) {
+
+      data = p.get_ptr();
+      return (data._ptr != nullptr);
+    }
+    if (p.get_value_type() == ShaderInput::M_param) {
+      // Temporary solution until the new param system
+      TypedWritableReferenceCount *param = p.get_value();
+      if (param != nullptr) {
+        if (param->is_of_type(ParamVecBase4f::get_class_type())) {
+          data._ptr = (void *)((const ParamVecBase4f *)param)->get_value().get_data();
+          data._size = 4;
+          data._type = Shader::SPT_float;
+          return true;
+        }
+        else if (param->is_of_type(ParamVecBase4i::get_class_type())) {
+          data._ptr = (void *)((const ParamVecBase4i *)param)->get_value().get_data();
+          data._size = 4;
+          data._type = Shader::SPT_int;
+          return true;
+        }
+        else if (param->is_of_type(ParamVecBase4d::get_class_type())) {
+          data._ptr = (void *)((const ParamVecBase4d *)param)->get_value().get_data();
+          data._size = 4;
+          data._type = Shader::SPT_float;
+          return true;
+        }
+      }
+    }
+    ostringstream strm;
+    strm << "Shader input " << id->get_name() << " was given an incompatible parameter type.\n";
+    nassert_raise(strm.str());
+    return false;
+  } else {
+    ostringstream strm;
+    strm << "Shader input " << id->get_name() << " is not present.\n";
+    nassert_raise(strm.str());
+    return false;
+  }
+}
+
 /**
  * Returns the ShaderInput as a texture.  Assertion fails if there is none, or
  * if it is not a texture.

+ 1 - 0
panda/src/pgraph/shaderAttrib.h

@@ -119,6 +119,7 @@ PUBLISHED:
   LVecBase4 get_shader_input_vector(InternalName *id) const;
   Texture *get_shader_input_texture(const InternalName *id, SamplerState *sampler=nullptr) const;
   const Shader::ShaderPtrData *get_shader_input_ptr(const InternalName *id) const;
+  bool get_shader_input_ptr(const InternalName *id, Shader::ShaderPtrData &data) const;
   const LMatrix4 &get_shader_input_matrix(const InternalName *id, LMatrix4 &matrix) const;
   ShaderBuffer *get_shader_input_buffer(const InternalName *id) const;
 

+ 5 - 1
panda/src/pgui/pgButton.cxx

@@ -115,7 +115,11 @@ release(const MouseWatcherParameter &param, bool background) {
   if (has_click_button(param.get_button())) {
     _button_down = false;
     if (get_active()) {
-      if (param.is_outside()) {
+      // Note that a "click" may come from a keyboard button press.  In that
+      // case, instead of checking that the mouse cursor is still over the
+      // button, we check whether the item has keyboard focus.
+      if (param.is_outside() &&
+          (MouseButton::is_mouse_button(param.get_button()) || !get_focus())) {
         set_state(S_ready);
       } else {
         set_state(S_rollover);

+ 2 - 0
panda/src/pipeline/mutexDebug.I

@@ -70,6 +70,8 @@ acquire(Thread *current_thread) const {
 /**
  * Returns immediately, with a true value indicating the mutex has been
  * acquired, and false indicating it has not.
+ *
+ * @deprecated Python users should use acquire(False), C++ users try_lock()
  */
 INLINE bool MutexDebug::
 try_acquire(Thread *current_thread) const {

+ 2 - 0
panda/src/pipeline/mutexDirect.I

@@ -60,6 +60,8 @@ acquire() const {
 /**
  * Returns immediately, with a true value indicating the mutex has been
  * acquired, and false indicating it has not.
+ *
+ * @deprecated Python users should use acquire(False), C++ users try_lock()
  */
 INLINE bool MutexDirect::
 try_acquire() const {

+ 4 - 0
panda/src/pipeline/pmutex.h

@@ -49,6 +49,10 @@ PUBLISHED:
 
   void operator = (const Mutex &copy) = delete;
 
+  EXTENSION(bool acquire(bool blocking=true) const);
+  EXTENSION(bool __enter__());
+  EXTENSION(void __exit__(PyObject *, PyObject *, PyObject *));
+
 public:
   // This is a global mutex set aside for the purpose of protecting Notify
   // messages from being interleaved between threads.

+ 53 - 0
panda/src/pipeline/pmutex_ext.I

@@ -0,0 +1,53 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file pmutex_ext.h
+ * @author rdb
+ * @date 2019-05-12
+ */
+
+/**
+ * Acquires the mutex.
+ */
+INLINE bool Extension<Mutex>::
+acquire(bool blocking) const {
+  if (_this->try_lock()) {
+    return true;
+  }
+
+  if (!blocking) {
+    return false;
+  }
+
+  // Release the GIL while we are waiting for the lock.
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  PyThreadState *_save;
+  Py_UNBLOCK_THREADS
+  _this->lock();
+  Py_BLOCK_THREADS
+#else
+  _this->lock();
+#endif
+  return true;
+}
+
+/**
+ * Acquires the mutex.
+ */
+INLINE bool Extension<Mutex>::
+__enter__() {
+  return acquire(true);
+}
+
+/**
+ * Releases the mutex.
+ */
+INLINE void Extension<Mutex>::
+__exit__(PyObject *, PyObject *, PyObject *) {
+  _this->unlock();
+}

+ 41 - 0
panda/src/pipeline/pmutex_ext.h

@@ -0,0 +1,41 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file pmutex_ext.h
+ * @author rdb
+ * @date 2019-05-12
+ */
+
+#ifndef PMUTEX_EXT_H
+#define PMUTEX_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "pmutex.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for Mutex, which are called
+ * instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<Mutex> : public ExtensionBase<Mutex> {
+public:
+  INLINE bool acquire(bool blocking) const;
+  INLINE bool __enter__();
+  INLINE void __exit__(PyObject *, PyObject *, PyObject *);
+};
+
+#include "pmutex_ext.I"
+
+#endif  // HAVE_PYTHON
+
+#endif  // PMUTEX_EXT_H

+ 4 - 0
panda/src/pipeline/reMutex.h

@@ -42,6 +42,10 @@ PUBLISHED:
   ~ReMutex() = default;
 
   void operator = (const ReMutex &copy) = delete;
+
+  EXTENSION(bool acquire(bool blocking=true) const);
+  EXTENSION(bool __enter__());
+  EXTENSION(void __exit__(PyObject *, PyObject *, PyObject *));
 };
 
 #include "reMutex.I"

+ 4 - 0
panda/src/pipeline/reMutexDirect.I

@@ -105,6 +105,8 @@ acquire(Thread *current_thread) const {
 /**
  * Returns immediately, with a true value indicating the mutex has been
  * acquired, and false indicating it has not.
+ *
+ * @deprecated Python users should use acquire(False), C++ users try_lock()
  */
 INLINE bool ReMutexDirect::
 try_acquire() const {
@@ -119,6 +121,8 @@ try_acquire() const {
 /**
  * Returns immediately, with a true value indicating the mutex has been
  * acquired, and false indicating it has not.
+ *
+ * @deprecated Python users should use acquire(False), C++ users try_lock()
  */
 INLINE bool ReMutexDirect::
 try_acquire(Thread *current_thread) const {

+ 53 - 0
panda/src/pipeline/reMutex_ext.I

@@ -0,0 +1,53 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file pmutex_ext.h
+ * @author rdb
+ * @date 2019-05-12
+ */
+
+/**
+ * Acquires the mutex.
+ */
+INLINE bool Extension<ReMutex>::
+acquire(bool blocking) const {
+  if (_this->try_lock()) {
+    return true;
+  }
+
+  if (!blocking) {
+    return false;
+  }
+
+  // Release the GIL while we are waiting for the lock.
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  PyThreadState *_save;
+  Py_UNBLOCK_THREADS
+  _this->lock();
+  Py_BLOCK_THREADS
+#else
+  _this->lock();
+#endif
+  return true;
+}
+
+/**
+ * Acquires the mutex.
+ */
+INLINE bool Extension<ReMutex>::
+__enter__() {
+  return acquire(true);
+}
+
+/**
+ * Releases the mutex.
+ */
+INLINE void Extension<ReMutex>::
+__exit__(PyObject *, PyObject *, PyObject *) {
+  _this->unlock();
+}

+ 41 - 0
panda/src/pipeline/reMutex_ext.h

@@ -0,0 +1,41 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file remutex_ext.h
+ * @author rdb
+ * @date 2019-05-12
+ */
+
+#ifndef REMUTEX_EXT_H
+#define REMUTEX_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "reMutex.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for ReMutex, which are called
+ * instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<ReMutex> : public ExtensionBase<ReMutex> {
+public:
+  INLINE bool acquire(bool blocking) const;
+  INLINE bool __enter__();
+  INLINE void __exit__(PyObject *, PyObject *, PyObject *);
+};
+
+#include "reMutex_ext.I"
+
+#endif  // HAVE_PYTHON
+
+#endif  // REMUTEX_EXT_H

+ 8 - 7
panda/src/putil/sparseArray.cxx

@@ -262,8 +262,8 @@ compare_to(const SparseArray &other) const {
       return -1;
     }
 
-    --ai;
-    --bi;
+    ++ai;
+    ++bi;
   }
 
   if (ai != _subranges.rend()) {
@@ -440,9 +440,9 @@ do_remove_range(int begin, int end) {
   if (si == _subranges.end()) {
     if (!_subranges.empty()) {
       si = _subranges.begin() + _subranges.size() - 1;
-      if ((*si)._end >= begin) {
+      if ((*si)._end > begin) {
         // The new range shortens the last element of the array on the right.
-        end = std::min(end, (*si)._begin);
+        end = std::max(begin, (*si)._begin);
         (*si)._end = end;
         // It might also shorten it on the left; fall through.
       } else {
@@ -462,10 +462,10 @@ do_remove_range(int begin, int end) {
     if (si != _subranges.begin()) {
       Subranges::iterator si2 = si;
       --si2;
-      if ((*si2)._end >= begin) {
+      if ((*si2)._end > begin) {
         // The new range shortens an element within the array on the right
         // (but does not intersect the next element).
-        end = std::min(end, (*si2)._begin);
+        end = std::max(begin, (*si2)._begin);
         (*si2)._end = end;
         // It might also shorten it on the left; fall through.
         si = si2;
@@ -488,7 +488,7 @@ do_remove_range(int begin, int end) {
   }
 
   // Check if the new range removes any elements to the left.
-  while (begin <= (*si)._begin) {
+  while (begin <= (*si)._begin || (*si)._begin >= (*si)._end) {
     if (si == _subranges.begin()) {
       _subranges.erase(si);
       return;
@@ -500,6 +500,7 @@ do_remove_range(int begin, int end) {
   }
 
   (*si)._end = std::min((*si)._end, begin);
+  nassertv((*si)._end > (*si)._begin);
 }
 
 /**

+ 12 - 1
panda/src/wgldisplay/wglGraphicsStateGuardian.cxx

@@ -609,9 +609,20 @@ make_context(HDC hdc) {
         attrib_list[n++] = gl_version[1];
       }
     }
+    int flags = 0;
     if (gl_debug) {
+      flags |= WGL_CONTEXT_DEBUG_BIT_ARB;
+    }
+    if (gl_forward_compatible) {
+      flags |= WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB;
+      if (gl_version.get_num_words() == 0 || gl_version[0] < 2) {
+        wgldisplay_cat.error()
+          << "gl-forward-compatible requires gl-version >= 3 0\n";
+      }
+    }
+    if (flags != 0) {
       attrib_list[n++] = WGL_CONTEXT_FLAGS_ARB;
-      attrib_list[n++] = WGL_CONTEXT_DEBUG_BIT_ARB;
+      attrib_list[n++] = flags;
     }
 #ifndef SUPPORT_FIXED_FUNCTION
     attrib_list[n++] = WGL_CONTEXT_PROFILE_MASK_ARB;

+ 5 - 0
panda/src/windisplay/config_windisplay.cxx

@@ -86,6 +86,11 @@ ConfigVariableBool swapbuffer_framelock
 ("swapbuffer-framelock", false,
  PRC_DESC("Set this true to enable HW swapbuffer frame-lock on 3dlabs cards"));
 
+ConfigVariableBool paste_emit_keystrokes
+("paste-emit-keystrokes", true,
+ PRC_DESC("Handle paste events (Ctrl-V) as separate keystroke events for each "
+          "pasted character."));
+
 /**
  * Initializes the library.  This must be called at least once before any of
  * the functions or classes in this library can be used.  Normally it will be

+ 1 - 0
panda/src/windisplay/config_windisplay.h

@@ -31,6 +31,7 @@ extern ConfigVariableBool ime_hide;
 extern ConfigVariableBool request_dxdisplay_information;
 extern ConfigVariableBool dpi_aware;
 extern ConfigVariableBool dpi_window_resize;
+extern ConfigVariableBool paste_emit_keystrokes;
 
 extern EXPCL_PANDAWIN ConfigVariableBool swapbuffer_framelock;
 

+ 1 - 1
panda/src/windisplay/winGraphicsWindow.cxx

@@ -1927,7 +1927,7 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
       // Handle Cntrl-V paste from clipboard.  Is there a better way to detect
       // this hotkey?
       if ((wparam=='V') && (GetKeyState(VK_CONTROL) < 0) &&
-          !_input_devices.empty()) {
+          !_input_devices.empty() && paste_emit_keystrokes) {
         HGLOBAL hglb;
         char *lptstr;
 

+ 8 - 30
panda/src/x11display/x11GraphicsWindow.cxx

@@ -589,7 +589,13 @@ set_properties_now(WindowProperties &properties) {
       // OK, first figure out which CRTC the window is on.  It may be on more
       // than one, actually, so grab a point in the center in order to figure
       // out which one it's more-or-less mostly on.
-      LPoint2i center = _properties.get_origin() + _properties.get_size() / 2;
+      LPoint2i center(0, 0);
+      if (_properties.has_origin()) {
+        center = _properties.get_origin();
+        if (_properties.has_size()) {
+          center += _properties.get_size() / 2;
+        }
+      }
       int x, y, width, height;
       x11_pipe->find_fullscreen_crtc(center, x, y, width, height);
 
@@ -628,7 +634,7 @@ set_properties_now(WindowProperties &properties) {
         // We may need to change the screen resolution.  The code below is
         // suboptimal; in the future, we probably want to only touch the CRTC
         // that the window is on.
-        XRRScreenConfiguration *conf = _XRRGetScreenInfo(_display, _xwindow);
+        XRRScreenConfiguration *conf = _XRRGetScreenInfo(_display, _xwindow ? _xwindow : x11_pipe->get_root());
         SizeID old_size_id = x11_pipe->_XRRConfigCurrentConfiguration(conf, &_orig_rotation);
         SizeID new_size_id = (SizeID) -1;
         int num_sizes = 0;
@@ -1010,34 +1016,6 @@ open_window() {
   // Make sure we are not making X11 calls from other threads.
   LightReMutexHolder holder(x11GraphicsPipe::_x_mutex);
 
-  if (_properties.get_fullscreen() && x11_pipe->_have_xrandr) {
-    XRRScreenConfiguration* conf = _XRRGetScreenInfo(_display, x11_pipe->get_root());
-    if (_orig_size_id == (SizeID) -1) {
-      _orig_size_id = x11_pipe->_XRRConfigCurrentConfiguration(conf, &_orig_rotation);
-    }
-    int num_sizes, new_size_id = -1;
-    XRRScreenSize *xrrs;
-    xrrs = x11_pipe->_XRRSizes(_display, 0, &num_sizes);
-    for (int i = 0; i < num_sizes; ++i) {
-      if (xrrs[i].width == _properties.get_x_size() &&
-          xrrs[i].height == _properties.get_y_size()) {
-        new_size_id = i;
-      }
-    }
-    if (new_size_id == -1) {
-      x11display_cat.error()
-        << "Videocard has no supported display resolutions at specified res ("
-        << _properties.get_x_size() << " x " << _properties.get_y_size() <<")\n";
-      _orig_size_id = -1;
-      return false;
-    }
-    if (new_size_id != _orig_size_id) {
-      _XRRSetScreenConfig(_display, conf, x11_pipe->get_root(), new_size_id, _orig_rotation, CurrentTime);
-    } else {
-      _orig_size_id = -1;
-    }
-  }
-
   X11_Window parent_window = x11_pipe->get_root();
   WindowHandle *window_handle = _properties.get_parent_window();
   if (window_handle != nullptr) {

+ 45 - 0
tests/bullet/test_bullet_heightfield.py

@@ -0,0 +1,45 @@
+import pytest
+# Skip these tests if we can't import bullet.
+bullet = pytest.importorskip("panda3d.bullet")
+
+from panda3d.bullet import BulletWorld, BulletRigidBodyNode, ZUp
+from panda3d.bullet import BulletHeightfieldShape, BulletSphereShape
+from panda3d.core import NodePath, PNMImage
+
+
+def make_node(name, BulletShape, *args):
+    # Returns a BulletRigidBodyNode for the given shape
+    shape = BulletShape(*args)
+    node = BulletRigidBodyNode(name)
+    node.add_shape(shape)
+    return node
+
+
+def test_sphere_into_heightfield():
+    root = NodePath("root")
+    world = BulletWorld()
+    # Create PNMImage to construct Heightfield with
+    img = PNMImage(10, 10, 1)
+    img.fill_val(255)
+    # Make our nodes
+    heightfield = make_node("Heightfield", BulletHeightfieldShape, img, 1, ZUp)
+    sphere = make_node("Sphere", BulletSphereShape, 1)
+    # Attach to world
+    np1 = root.attach_new_node(sphere)
+    np1.set_pos(0, 0, 1)
+    world.attach(sphere)
+
+    np2 = root.attach_new_node(heightfield)
+    np2.set_pos(0, 0, 0)
+    world.attach(heightfield)
+
+    assert world.get_num_rigid_bodies() == 2
+    test = world.contact_test_pair(sphere, heightfield)
+    assert test.get_num_contacts() > 0
+    assert test.get_contact(0).get_node0() == sphere
+    assert test.get_contact(0).get_node1() == heightfield
+
+    # Increment sphere's Z coordinate, no longer colliding
+    np1.set_pos(0, 0, 2)
+    test = world.contact_test_pair(sphere, heightfield)
+    assert test.get_num_contacts() == 0

+ 30 - 0
tests/display/test_glsl_shader.py

@@ -294,6 +294,36 @@ def test_glsl_pta_mat4(gsg):
     run_glsl_test(gsg, code, preamble, {'pta': pta})
 
 
+def test_glsl_param_vec4(gsg):
+    param = core.ParamVecBase4((0, 1, 2, 3))
+
+    preamble = """
+    uniform vec4 param;
+    """
+    code = """
+    assert(param.x == 0.0);
+    assert(param.y == 1.0);
+    assert(param.z == 2.0);
+    assert(param.w == 3.0);
+    """
+    run_glsl_test(gsg, code, preamble, {'param': param})
+
+
+def test_glsl_param_ivec4(gsg):
+    param = core.ParamVecBase4i((0, 1, 2, 3))
+
+    preamble = """
+    uniform ivec4 param;
+    """
+    code = """
+    assert(param.x == 0);
+    assert(param.y == 1);
+    assert(param.z == 2);
+    assert(param.w == 3);
+    """
+    run_glsl_test(gsg, code, preamble, {'param': param})
+
+
 def test_glsl_write_extract_image_buffer(gsg):
     # Tests that we can write to a buffer texture on the GPU, and then extract
     # the data on the CPU.  We test two textures since there was in the past a

+ 128 - 0
tests/dtoolutil/test_iostream.py

@@ -0,0 +1,128 @@
+from panda3d.core import StringStream
+
+import pytest
+
+
+ISTREAM_DATA = b'abcdefghijklmnopqrstuvwxyz' * 500
+
[email protected]
+def istream():
+    return StringStream(ISTREAM_DATA)
+
+
+def test_istream_readall(istream):
+    assert istream.readall() == ISTREAM_DATA
+    assert istream.readall() == b''
+    assert istream.readall() == b''
+    assert istream.tellg() == len(ISTREAM_DATA)
+
+
+def test_istream_read(istream):
+    assert istream.read() == ISTREAM_DATA
+    assert istream.read() == b''
+    assert istream.read() == b''
+    assert istream.tellg() == len(ISTREAM_DATA)
+
+
+def test_istream_read_size(istream):
+    assert istream.read(100) == ISTREAM_DATA[:100]
+    assert istream.read(5000) == ISTREAM_DATA[100:5100]
+    assert istream.read(5000) == ISTREAM_DATA[5100:10100]
+    assert istream.read(5000) == ISTREAM_DATA[10100:15100]
+    assert istream.read() == b''
+    assert istream.tellg() == len(ISTREAM_DATA)
+
+
+def test_istream_read1(istream):
+    accumulated = b''
+    data = istream.read1()
+    while data:
+        accumulated += data
+        data = istream.read1()
+
+    assert accumulated == ISTREAM_DATA
+    assert istream.tellg() == len(ISTREAM_DATA)
+
+
+def test_istream_read1_size(istream):
+    accumulated = b''
+    data = istream.read1(4000)
+    while data:
+        accumulated += data
+        data = istream.read1(4000)
+
+    assert accumulated == ISTREAM_DATA
+    assert istream.tellg() == len(ISTREAM_DATA)
+
+
+def test_istream_readinto(istream):
+    ba = bytearray()
+    assert istream.readinto(ba) == 0
+    assert istream.tellg() == 0
+
+    ba = bytearray(10)
+    assert istream.readinto(ba) == 10
+    assert ba == ISTREAM_DATA[:10]
+    assert istream.tellg() == 10
+
+    ba = bytearray(len(ISTREAM_DATA))
+    assert istream.readinto(ba) == len(ISTREAM_DATA) - 10
+    assert ba[:len(ISTREAM_DATA)-10] == ISTREAM_DATA[10:]
+    assert istream.tellg() == len(ISTREAM_DATA)
+
+
+def test_istream_readline():
+    # Empty stream
+    stream = StringStream(b'')
+    assert stream.readline() == b''
+    assert stream.readline() == b''
+
+    # Single line without newline
+    stream = StringStream(b'A')
+    assert stream.readline() == b'A'
+    assert stream.readline() == b''
+
+    # Single newline
+    stream = StringStream(b'\n')
+    assert stream.readline() == b'\n'
+    assert stream.readline() == b''
+
+    # Line with text followed by empty line
+    stream = StringStream(b'A\n\n')
+    assert stream.readline() == b'A\n'
+    assert stream.readline() == b'\n'
+    assert stream.readline() == b''
+
+    # Preserve null byte
+    stream = StringStream(b'\x00\x00')
+    assert stream.readline() == b'\x00\x00'
+
+
+def test_istream_readlines():
+    istream = StringStream(b'a')
+    assert istream.readlines() == [b'a']
+    assert istream.readlines() == []
+
+    istream = StringStream(b'a\nb\nc\n')
+    assert istream.readlines() == [b'a\n', b'b\n', b'c\n']
+
+    istream = StringStream(b'\na\nb\nc')
+    assert istream.readlines() == [b'\n', b'a\n', b'b\n', b'c']
+
+    istream = StringStream(b'\n\n\n')
+    assert istream.readlines() == [b'\n', b'\n', b'\n']
+
+
+def test_istream_iter():
+    istream = StringStream(b'a')
+    assert tuple(istream) == (b'a',)
+    assert tuple(istream) == ()
+
+    istream = StringStream(b'a\nb\nc\n')
+    assert tuple(istream) == (b'a\n', b'b\n', b'c\n')
+
+    istream = StringStream(b'\na\nb\nc')
+    assert tuple(istream) == (b'\n', b'a\n', b'b\n', b'c')
+
+    istream = StringStream(b'\n\n\n')
+    assert tuple(istream) == (b'\n', b'\n', b'\n')

+ 36 - 0
tests/linmath/test_lmatrix4.py

@@ -87,3 +87,39 @@ def test_mat4_invert_correct(type):
 
     assert (mat * inv).is_identity()
     assert (inv * mat).is_identity()
+
+
[email protected]("type", (core.LMatrix4d, core.LMatrix4f))
+def test_mat4_rows(type):
+    mat = type((1, 2, 3, 4,
+                5, 6, 7, 8,
+                9, 10, 11, 12,
+                13, 14, 15, 16))
+
+    assert mat.rows[0] == (1, 2, 3, 4)
+    assert mat.rows[1] == (5, 6, 7, 8)
+    assert mat.rows[2] == (9, 10, 11, 12)
+    assert mat.rows[3] == (13, 14, 15, 16)
+
+    assert mat.get_row3(0) == (1, 2, 3)
+    assert mat.get_row3(1) == (5, 6, 7)
+    assert mat.get_row3(2) == (9, 10, 11)
+    assert mat.get_row3(3) == (13, 14, 15)
+
+
[email protected]("type", (core.LMatrix4d, core.LMatrix4f))
+def test_mat4_cols(type):
+    mat = type((1, 5, 9, 13,
+                2, 6, 10, 14,
+                3, 7, 11, 15,
+                4, 8, 12, 16))
+
+    assert mat.cols[0] == (1, 2, 3, 4)
+    assert mat.cols[1] == (5, 6, 7, 8)
+    assert mat.cols[2] == (9, 10, 11, 12)
+    assert mat.cols[3] == (13, 14, 15, 16)
+
+    assert mat.get_col3(0) == (1, 2, 3)
+    assert mat.get_col3(1) == (5, 6, 7)
+    assert mat.get_col3(2) == (9, 10, 11)
+    assert mat.get_col3(3) == (13, 14, 15)

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