Browse Source

Merge branch 'master' into deploy-ng

rdb 7 years ago
parent
commit
aebdefcf60
47 changed files with 785 additions and 364 deletions
  1. 4 4
      direct/src/distributed/ConnectionRepository.py
  2. 1 5
      direct/src/distributed/PyDatagramIterator.py
  3. 4 2
      direct/src/distributed/ServerRepository.py
  4. 0 1
      direct/src/interval/MetaInterval.py
  5. 0 9
      direct/src/showbase/ObjectPool.py
  6. 23 0
      direct/src/showbase/TkGlobal.py
  7. 1 1
      direct/src/stdpy/threading.py
  8. 0 2
      dtool/src/dtoolbase/dtoolsymbols.h
  9. 12 6
      dtool/src/dtoolbase/typeHandle.I
  10. 8 3
      dtool/src/dtoolbase/typeHandle.h
  11. 0 108
      dtool/src/newheader/newheader.cxx
  12. 75 5
      makepanda/makepanda.py
  13. 2 2
      panda/src/android/PandaActivity.java
  14. 23 0
      panda/src/android/PythonActivity.java
  15. 55 22
      panda/src/android/android_main.cxx
  16. 33 7
      panda/src/android/pview_manifest.xml
  17. 80 0
      panda/src/android/python_main.cxx
  18. 14 0
      panda/src/android/run_pview.sh
  19. 14 0
      panda/src/android/run_python.sh
  20. 34 0
      panda/src/android/site.py
  21. 0 3
      panda/src/androiddisplay/androidGraphicsWindow.cxx
  22. 17 0
      panda/src/audiotraits/config_openalAudio.cxx
  23. 2 0
      panda/src/audiotraits/config_openalAudio.h
  24. 46 7
      panda/src/audiotraits/openalAudioManager.cxx
  25. 2 0
      panda/src/audiotraits/openalAudioManager.h
  26. 41 4
      panda/src/audiotraits/openalAudioSound.I
  27. 89 38
      panda/src/audiotraits/openalAudioSound.cxx
  28. 5 3
      panda/src/audiotraits/openalAudioSound.h
  29. 18 0
      panda/src/bullet/bulletWorld.cxx
  30. 5 0
      panda/src/bullet/bulletWorld.h
  31. 0 1
      panda/src/char/character.h
  32. 12 6
      panda/src/char/characterJoint.cxx
  33. 6 0
      panda/src/char/characterJoint.h
  34. 0 10
      panda/src/char/jointVertexTransform.I
  35. 5 25
      panda/src/char/jointVertexTransform.cxx
  36. 0 7
      panda/src/char/jointVertexTransform.h
  37. 0 1
      panda/src/event/asyncFuture.cxx
  38. 85 54
      panda/src/pgraph/clipPlaneAttrib.cxx
  39. 16 1
      panda/src/pgraph/clipPlaneAttrib.h
  40. 23 23
      panda/src/pgraph/cullResult.cxx
  41. 1 1
      panda/src/pgraphnodes/shaderGenerator.cxx
  42. 2 1
      panda/src/putil/bamReader.cxx
  43. 4 0
      panda/src/vision/config_vision.cxx
  44. 3 0
      panda/src/vision/config_vision.h
  45. 16 1
      panda/src/vision/webcamVideoCursorV4L.cxx
  46. 1 0
      panda/src/vision/webcamVideoV4L.cxx
  47. 3 1
      tests/display/conftest.py

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

@@ -7,7 +7,7 @@ from direct.distributed.DoCollectionManager import DoCollectionManager
 from direct.showbase import GarbageReport
 from direct.showbase import GarbageReport
 from .PyDatagramIterator import PyDatagramIterator
 from .PyDatagramIterator import PyDatagramIterator
 
 
-import types
+import inspect
 import gc
 import gc
 
 
 __all__ = ["ConnectionRepository", "GCTrigger"]
 __all__ = ["ConnectionRepository", "GCTrigger"]
@@ -327,13 +327,13 @@ class ConnectionRepository(
             if classDef is None:
             if classDef is None:
                 self.notify.debug("No class definition for %s." % (className))
                 self.notify.debug("No class definition for %s." % (className))
             else:
             else:
-                if type(classDef) == types.ModuleType:
+                if inspect.ismodule(classDef):
                     if not hasattr(classDef, className):
                     if not hasattr(classDef, className):
                         self.notify.warning("Module %s does not define class %s." % (className, className))
                         self.notify.warning("Module %s does not define class %s." % (className, className))
                         continue
                         continue
                     classDef = getattr(classDef, className)
                     classDef = getattr(classDef, className)
 
 
-                if type(classDef) != types.ClassType and type(classDef) != types.TypeType:
+                if inspect.isclass(classDef):
                     self.notify.error("Symbol %s is not a class name." % (className))
                     self.notify.error("Symbol %s is not a class name." % (className))
                 else:
                 else:
                     dclass.setClassDef(classDef)
                     dclass.setClassDef(classDef)
@@ -388,7 +388,7 @@ class ConnectionRepository(
                     if classDef is None:
                     if classDef is None:
                         self.notify.error("No class definition for %s." % className)
                         self.notify.error("No class definition for %s." % className)
                     else:
                     else:
-                        if type(classDef) == types.ModuleType:
+                        if inspect.ismodule(classDef):
                             if not hasattr(classDef, className):
                             if not hasattr(classDef, className):
                                 self.notify.error("Module %s does not define class %s." % (className, className))
                                 self.notify.error("Module %s does not define class %s." % (className, className))
                             classDef = getattr(classDef, className)
                             classDef = getattr(classDef, className)

+ 1 - 5
direct/src/distributed/PyDatagramIterator.py

@@ -75,7 +75,7 @@ class PyDatagramIterator(DatagramIterator):
                     b = self.getUint8()
                     b = self.getUint8()
                     retVal.append((a, b))
                     retVal.append((a, b))
             else:
             else:
-                raise Exception("Error: No such type as: " + str(subAtomicType))
+                raise Exception("Error: No such type as: " + str(subatomicType))
         else:
         else:
             # See if it is in the handy dict
             # See if it is in the handy dict
             getFunc = self.FuncDict.get(subatomicType)
             getFunc = self.FuncDict.get(subatomicType)
@@ -121,8 +121,4 @@ class PyDatagramIterator(DatagramIterator):
             else:
             else:
                 raise Exception("Error: No such type as: " + str(subatomicType))
                 raise Exception("Error: No such type as: " + str(subatomicType))
 
 
-
-
         return retVal
         return retVal
-
-

+ 4 - 2
direct/src/distributed/ServerRepository.py

@@ -7,6 +7,8 @@ from direct.task import Task
 from direct.directnotify import DirectNotifyGlobal
 from direct.directnotify import DirectNotifyGlobal
 from direct.distributed.PyDatagram import PyDatagram
 from direct.distributed.PyDatagram import PyDatagram
 
 
+import inspect
+
 
 
 class ServerRepository:
 class ServerRepository:
 
 
@@ -273,12 +275,12 @@ class ServerRepository:
             if classDef == None:
             if classDef == None:
                 self.notify.debug("No class definition for %s." % (className))
                 self.notify.debug("No class definition for %s." % (className))
             else:
             else:
-                if type(classDef) == types.ModuleType:
+                if inspect.ismodule(classDef):
                     if not hasattr(classDef, className):
                     if not hasattr(classDef, className):
                         self.notify.error("Module %s does not define class %s." % (className, className))
                         self.notify.error("Module %s does not define class %s." % (className, className))
                     classDef = getattr(classDef, className)
                     classDef = getattr(classDef, className)
 
 
-                if type(classDef) != types.ClassType and type(classDef) != types.TypeType:
+                if inspect.isclass(classDef):
                     self.notify.error("Symbol %s is not a class name." % (className))
                     self.notify.error("Symbol %s is not a class name." % (className))
                 else:
                 else:
                     dclass.setClassDef(classDef)
                     dclass.setClassDef(classDef)

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

@@ -342,7 +342,6 @@ class MetaInterval(CMetaInterval):
     # with all of their associated Python callbacks:
     # with all of their associated Python callbacks:
 
 
     def setManager(self, manager):
     def setManager(self, manager):
-        rogerroger
         self.__manager = manager
         self.__manager = manager
         CMetaInterval.setManager(self, manager)
         CMetaInterval.setManager(self, manager)
 
 

+ 0 - 9
direct/src/showbase/ObjectPool.py

@@ -110,15 +110,6 @@ class ObjectPool:
                 print('TYPE: %s, %s objects' % (repr(typ), len(self._type2objs[typ])))
                 print('TYPE: %s, %s objects' % (repr(typ), len(self._type2objs[typ])))
                 print(getNumberedTypedSortedString(self._type2objs[typ]))
                 print(getNumberedTypedSortedString(self._type2objs[typ]))
 
 
-    def containerLenStr(self):
-        s  =   'Object Pool: Container Lengths'
-        s += '\n=============================='
-        lengths = list(self._len2obj.keys())
-        lengths.sort()
-        lengths.reverse()
-        for count in counts:
-            pass
-
     def printReferrers(self, numEach=3):
     def printReferrers(self, numEach=3):
         """referrers of the first few of each type of object"""
         """referrers of the first few of each type of object"""
         counts = list(set(self._count2types.keys()))
         counts = list(set(self._count2types.keys()))

+ 23 - 0
direct/src/showbase/TkGlobal.py

@@ -12,5 +12,28 @@ else:
 if '_Pmw' in sys.modules:
 if '_Pmw' in sys.modules:
     sys.modules['_Pmw'].__name__ = '_Pmw'
     sys.modules['_Pmw'].__name__ = '_Pmw'
 
 
+# Hack to workaround broken Pmw.NoteBook in Python 3
+def bordercolors(root, colorName):
+    lightRGB = []
+    darkRGB = []
+    for value in Pmw.Color.name2rgb(root, colorName, 1):
+        value40pc = (14 * value) // 10
+        if value40pc > int(Pmw.Color._MAX_RGB):
+            value40pc = int(Pmw.Color._MAX_RGB)
+        valueHalfWhite = (int(Pmw.Color._MAX_RGB) + value) // 2;
+        lightRGB.append(max(value40pc, valueHalfWhite))
+
+        darkValue = (60 * value) // 100
+        darkRGB.append(darkValue)
+
+    return (
+        '#%04x%04x%04x' % (lightRGB[0], lightRGB[1], lightRGB[2]),
+        '#%04x%04x%04x' % (darkRGB[0], darkRGB[1], darkRGB[2])
+    )
+
+Pmw.Color.bordercolors = bordercolors
+del bordercolors
+
+
 def spawnTkLoop():
 def spawnTkLoop():
     base.spawnTkLoop()
     base.spawnTkLoop()

+ 1 - 1
direct/src/stdpy/threading.py

@@ -302,7 +302,7 @@ class BoundedSemaphore(Semaphore):
         Semaphore.__init__(value)
         Semaphore.__init__(value)
 
 
     def release(self):
     def release(self):
-        if self.getCount() > value:
+        if self.getCount() > self.__max:
             raise ValueError
             raise ValueError
 
 
         Semaphore.release(self)
         Semaphore.release(self)

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

@@ -67,8 +67,6 @@
   can define all of these stupid symbols to the empty string.
   can define all of these stupid symbols to the empty string.
   */
   */
 
 
-#define EXPCL_EMPTY
-
 #ifdef BUILDING_DTOOL
 #ifdef BUILDING_DTOOL
   #define EXPCL_DTOOL EXPORT_CLASS
   #define EXPCL_DTOOL EXPORT_CLASS
   #define EXPTP_DTOOL EXPORT_TEMPL
   #define EXPTP_DTOOL EXPORT_TEMPL

+ 12 - 6
dtool/src/dtoolbase/typeHandle.I

@@ -194,9 +194,9 @@ output(ostream &out) const {
 /**
 /**
  * Returns a special zero-valued TypeHandle that is used to indicate no type.
  * Returns a special zero-valued TypeHandle that is used to indicate no type.
  */
  */
-INLINE TypeHandle TypeHandle::
+CONSTEXPR TypeHandle TypeHandle::
 none() {
 none() {
-  return _none;
+  return TypeHandle(0);
 }
 }
 
 
 /**
 /**
@@ -213,9 +213,15 @@ operator bool () const {
  *
  *
  * See TypeRegistry::find_type_by_id().
  * See TypeRegistry::find_type_by_id().
  */
  */
-INLINE TypeHandle TypeHandle::
+CONSTEXPR TypeHandle TypeHandle::
 from_index(int index) {
 from_index(int index) {
-  TypeHandle handle;
-  handle._index = index;
-  return handle;
+  return TypeHandle(index);
+}
+
+/**
+ * Private constructor for initializing a TypeHandle from an index, used by
+ * none() and by from_index().
+ */
+CONSTEXPR TypeHandle::
+TypeHandle(int index) : _index(index) {
 }
 }

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

@@ -80,6 +80,8 @@ class TypedObject;
  */
  */
 class EXPCL_DTOOL TypeHandle FINAL {
 class EXPCL_DTOOL TypeHandle FINAL {
 PUBLISHED:
 PUBLISHED:
+  TypeHandle() NOEXCEPT DEFAULT_CTOR;
+
   enum MemoryClass {
   enum MemoryClass {
     MC_singleton,
     MC_singleton,
     MC_array,
     MC_array,
@@ -127,7 +129,7 @@ PUBLISHED:
 
 
   INLINE int get_index() const;
   INLINE int get_index() const;
   INLINE void output(ostream &out) const;
   INLINE void output(ostream &out) const;
-  INLINE static TypeHandle none();
+  CONSTEXPR static TypeHandle none();
   INLINE operator bool () const;
   INLINE operator bool () const;
 
 
   MAKE_PROPERTY(index, get_index);
   MAKE_PROPERTY(index, get_index);
@@ -140,12 +142,15 @@ public:
   void *reallocate_array(void *ptr, size_t size) RETURNS_ALIGNED(MEMORY_HOOK_ALIGNMENT);
   void *reallocate_array(void *ptr, size_t size) RETURNS_ALIGNED(MEMORY_HOOK_ALIGNMENT);
   void deallocate_array(void *ptr);
   void deallocate_array(void *ptr);
 
 
-  INLINE static TypeHandle from_index(int index);
+  CONSTEXPR static TypeHandle from_index(int index);
 
 
 private:
 private:
-  int _index;
+  CONSTEXPR TypeHandle(int index);
+
+  // Only kept temporarily for ABI compatibility.
   static TypeHandle _none;
   static TypeHandle _none;
 
 
+  int _index;
   friend class TypeRegistry;
   friend class TypeRegistry;
 };
 };
 
 

+ 0 - 108
dtool/src/newheader/newheader.cxx

@@ -1,108 +0,0 @@
-/**
- * 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 newheader.cxx
- * @author drose
- * @date 2004-07-05
- */
-
-#include "dtoolbase.h"
-
-#include <stdio.h>
-#include <time.h>
-#include <stdlib.h>
-
-const char *cxx_style =
-"// Filename: %s\n"
-"// Created by:  %s (%s)\n"
-"//\n"
-"////////////////////////////////////////////////////////////////////\n"
-"//\n"
-"// PANDA 3D SOFTWARE\n"
-"// Copyright (c) Carnegie Mellon University.  All rights reserved.\n"
-"//\n"
-"// All use of this software is subject to the terms of the revised BSD\n"
-"// license.  You should have received a copy of this license along\n"
-"// with this source code in a file named \"LICENSE.\"\n"
-"//\n"
-"////////////////////////////////////////////////////////////////////\n"
-"\n";
-
-const char *c_style =
-"/* Filename: %s\n"
-" * Created by:  %s (%s)\n"
-" *\n"
-" * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n"
-" *\n"
-" * PANDA 3D SOFTWARE\n"
-" * Copyright (c) Carnegie Mellon University.  All rights reserved.\n"
-" *\n"
-" * All use of this software is subject to the terms of the revised BSD\n"
-" * license.  You should have received a copy of this license along\n"
-" * with this source code in a file named \"LICENSE.\"\n"
-" *\n"
-" * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */\n"
-"\n";
-
-struct FileDef {
-  const char *extension;
-  const char *header;
-};
-
-FileDef file_def[] = {
-  { "h", cxx_style },
-  { "cxx", cxx_style },
-  { "I", cxx_style },
-  { "T", cxx_style },
-  { "c", c_style },
-  { NULL, NULL },
-};
-
-void
-generate_header(const char *header, const string &filename) {
-  const char *username = getenv("USER");
-  if (username == NULL) {
-    username = "";
-  }
-
-  static const size_t max_date_buffer = 128;
-  char date_buffer[max_date_buffer];
-  time_t now = time(NULL);
-  strftime(date_buffer, max_date_buffer, "%d%b%y", localtime(&now));
-
-  printf(header, filename.c_str(), username, date_buffer);
-}
-
-int
-main(int argc, char *argv[]) {
-  if (argc < 2) {
-    cerr << "Must specify the filename to generate a header for.\n";
-    exit(1);
-  }
-
-  string filename = argv[1];
-  size_t dot = filename.rfind('.');
-  if (dot == string::npos) {
-    // No extension, no header.
-    return 0;
-  }
-
-  string extension = filename.substr(dot + 1);
-
-  size_t i = 0;
-  while (file_def[i].extension != NULL) {
-    if (extension == file_def[i].extension) {
-      generate_header(file_def[i].header, filename);
-      return 0;
-    }
-    i++;
-  }
-
-  // No matching extension, no problem.
-  return 0;
-}

+ 75 - 5
makepanda/makepanda.py

@@ -5179,7 +5179,7 @@ if (PkgSkip("SPEEDTREE")==0):
 # DIRECTORY: panda/src/testbed/
 # DIRECTORY: panda/src/testbed/
 #
 #
 
 
-if (not RTDIST and not RUNTIME and PkgSkip("PVIEW")==0 and GetTarget() != 'android'):
+if (not RTDIST and not RUNTIME and PkgSkip("PVIEW")==0):
   OPTS=['DIR:panda/src/testbed']
   OPTS=['DIR:panda/src/testbed']
   TargetAdd('pview_pview.obj', opts=OPTS, input='pview.cxx')
   TargetAdd('pview_pview.obj', opts=OPTS, input='pview.cxx')
   TargetAdd('pview.exe', input='pview_pview.obj')
   TargetAdd('pview.exe', input='pview_pview.obj')
@@ -5198,6 +5198,7 @@ if (not RUNTIME and GetTarget() == 'android'):
   TargetAdd('org/panda3d/android/NativeIStream.class', opts=OPTS, input='NativeIStream.java')
   TargetAdd('org/panda3d/android/NativeIStream.class', opts=OPTS, input='NativeIStream.java')
   TargetAdd('org/panda3d/android/NativeOStream.class', opts=OPTS, input='NativeOStream.java')
   TargetAdd('org/panda3d/android/NativeOStream.class', opts=OPTS, input='NativeOStream.java')
   TargetAdd('org/panda3d/android/PandaActivity.class', opts=OPTS, input='PandaActivity.java')
   TargetAdd('org/panda3d/android/PandaActivity.class', opts=OPTS, input='PandaActivity.java')
+  TargetAdd('org/panda3d/android/PythonActivity.class', opts=OPTS, input='PythonActivity.java')
 
 
   TargetAdd('p3android_composite1.obj', opts=OPTS, input='p3android_composite1.cxx')
   TargetAdd('p3android_composite1.obj', opts=OPTS, input='p3android_composite1.cxx')
   TargetAdd('libp3android.dll', input='p3android_composite1.obj')
   TargetAdd('libp3android.dll', input='p3android_composite1.obj')
@@ -5208,10 +5209,10 @@ if (not RUNTIME and GetTarget() == 'android'):
   TargetAdd('android_main.obj', opts=OPTS, input='android_main.cxx')
   TargetAdd('android_main.obj', opts=OPTS, input='android_main.cxx')
 
 
   if (not RTDIST and PkgSkip("PVIEW")==0):
   if (not RTDIST and PkgSkip("PVIEW")==0):
-    TargetAdd('pview_pview.obj', opts=OPTS, input='pview.cxx')
+    TargetAdd('libpview_pview.obj', opts=OPTS, input='pview.cxx')
     TargetAdd('libpview.dll', input='android_native_app_glue.obj')
     TargetAdd('libpview.dll', input='android_native_app_glue.obj')
     TargetAdd('libpview.dll', input='android_main.obj')
     TargetAdd('libpview.dll', input='android_main.obj')
-    TargetAdd('libpview.dll', input='pview_pview.obj')
+    TargetAdd('libpview.dll', input='libpview_pview.obj')
     TargetAdd('libpview.dll', input='libp3framework.dll')
     TargetAdd('libpview.dll', input='libp3framework.dll')
     if not PkgSkip("EGG"):
     if not PkgSkip("EGG"):
       TargetAdd('libpview.dll', input='libpandaegg.dll')
       TargetAdd('libpview.dll', input='libpandaegg.dll')
@@ -5219,6 +5220,17 @@ if (not RUNTIME and GetTarget() == 'android'):
     TargetAdd('libpview.dll', input=COMMON_PANDA_LIBS)
     TargetAdd('libpview.dll', input=COMMON_PANDA_LIBS)
     TargetAdd('libpview.dll', opts=['MODULE', 'ANDROID'])
     TargetAdd('libpview.dll', opts=['MODULE', 'ANDROID'])
 
 
+  if (not RTDIST and PkgSkip("PYTHON")==0):
+    OPTS += ['PYTHON']
+    TargetAdd('ppython_ppython.obj', opts=OPTS, input='python_main.cxx')
+    TargetAdd('libppython.dll', input='android_native_app_glue.obj')
+    TargetAdd('libppython.dll', input='android_main.obj')
+    TargetAdd('libppython.dll', input='ppython_ppython.obj')
+    TargetAdd('libppython.dll', input='libp3framework.dll')
+    TargetAdd('libppython.dll', input='libp3android.dll')
+    TargetAdd('libppython.dll', input=COMMON_PANDA_LIBS)
+    TargetAdd('libppython.dll', opts=['MODULE', 'ANDROID', 'PYTHON'])
+
 #
 #
 # DIRECTORY: panda/src/androiddisplay/
 # DIRECTORY: panda/src/androiddisplay/
 #
 #
@@ -7632,7 +7644,7 @@ def MakeInstallerAndroid():
                 continue
                 continue
             if '.so.' in line:
             if '.so.' in line:
                 dep = line.rpartition('.so.')[0] + '.so'
                 dep = line.rpartition('.so.')[0] + '.so'
-                oscmd("patchelf --replace-needed %s %s %s" % (line, dep, target))
+                oscmd("patchelf --replace-needed %s %s %s" % (line, dep, target), True)
             else:
             else:
                 dep = line
                 dep = line
 
 
@@ -7643,6 +7655,7 @@ def MakeInstallerAndroid():
                     copy_library(os.path.realpath(fulldep), dep)
                     copy_library(os.path.realpath(fulldep), dep)
                     break
                     break
 
 
+    # Now copy every lib in the lib dir, and its dependencies.
     for base in os.listdir(source_dir):
     for base in os.listdir(source_dir):
         if not base.startswith('lib'):
         if not base.startswith('lib'):
             continue
             continue
@@ -7654,6 +7667,59 @@ def MakeInstallerAndroid():
             continue
             continue
         copy_library(source, base)
         copy_library(source, base)
 
 
+    # Same for Python extension modules.  However, Android is strict about
+    # library naming, so we have a special naming scheme for these, in
+    # conjunction with a custom import hook to find these modules.
+    if not PkgSkip("PYTHON"):
+        suffix = GetExtensionSuffix()
+        source_dir = os.path.join(GetOutputDir(), "panda3d")
+        for base in os.listdir(source_dir):
+            if not base.endswith(suffix):
+                continue
+            modname = base[:-len(suffix)]
+            source = os.path.join(source_dir, base)
+            copy_library(source, "libpy.panda3d.{}.so".format(modname))
+
+        # Same for standard Python modules.
+        import _ctypes
+        source_dir = os.path.dirname(_ctypes.__file__)
+        for base in os.listdir(source_dir):
+            if not base.endswith('.so'):
+                continue
+            modname = base.partition('.')[0]
+            source = os.path.join(source_dir, base)
+            copy_library(source, "libpy.{}.so".format(modname))
+
+    def copy_python_tree(source_root, target_root):
+        for source_dir, dirs, files in os.walk(source_root):
+            if 'site-packages' in dirs:
+                dirs.remove('site-packages')
+
+            if not any(base.endswith('.py') for base in files):
+                continue
+
+            target_dir = os.path.join(target_root, os.path.relpath(source_dir, source_root))
+            target_dir = os.path.normpath(target_dir)
+            os.makedirs(target_dir, 0o755)
+
+            for base in files:
+                if base.endswith('.py'):
+                    target = os.path.join(target_dir, base)
+                    shutil.copy(os.path.join(source_dir, base), target)
+
+    # Copy the Python standard library to the .apk as well.
+    from distutils.sysconfig import get_python_lib
+    stdlib_source = get_python_lib(False, True)
+    stdlib_target = os.path.join("apkroot", "lib", "python{0}.{1}".format(*sys.version_info))
+    copy_python_tree(stdlib_source, stdlib_target)
+
+    # But also copy over our custom site.py.
+    shutil.copy("panda/src/android/site.py", os.path.join(stdlib_target, "site.py"))
+
+    # And now make a site-packages directory containing our direct/panda3d/pandac modules.
+    for tree in "panda3d", "direct", "pandac":
+        copy_python_tree(os.path.join(GetOutputDir(), tree), os.path.join(stdlib_target, "site-packages", tree))
+
     # Copy the models and config files to the virtual assets filesystem.
     # Copy the models and config files to the virtual assets filesystem.
     oscmd("mkdir apkroot/assets")
     oscmd("mkdir apkroot/assets")
     oscmd("cp -R %s apkroot/assets/models" % (os.path.join(GetOutputDir(), "models")))
     oscmd("cp -R %s apkroot/assets/models" % (os.path.join(GetOutputDir(), "models")))
@@ -7672,7 +7738,11 @@ def MakeInstallerAndroid():
     oscmd(aapt_cmd)
     oscmd(aapt_cmd)
 
 
     # And add all the libraries to it.
     # And add all the libraries to it.
-    oscmd("cd apkroot && aapt add ../%s classes.dex lib/%s/lib*.so" % (apk_unaligned, SDK["ANDROID_ABI"]))
+    oscmd("cd apkroot && aapt add ../%s classes.dex" % (apk_unaligned))
+    for path, dirs, files in os.walk('apkroot/lib'):
+        if files:
+            rel = os.path.relpath(path, 'apkroot')
+            oscmd("cd apkroot && aapt add ../%s %s/*" % (apk_unaligned, rel))
 
 
     # Now align the .apk, which is necessary for Android to load it.
     # Now align the .apk, which is necessary for Android to load it.
     oscmd("zipalign -v -p 4 %s %s" % (apk_unaligned, apk_unsigned))
     oscmd("zipalign -v -p 4 %s %s" % (apk_unaligned, apk_unsigned))

+ 2 - 2
panda/src/android/PandaActivity.java

@@ -87,9 +87,9 @@ public class PandaActivity extends NativeActivity {
         return path;
         return path;
     }
     }
 
 
-    public String getIntentOutputPath() {
+    public String getIntentOutputUri() {
         Intent intent = getIntent();
         Intent intent = getIntent();
-        return intent.getStringExtra("org.panda3d.OUTPUT_PATH");
+        return intent.getStringExtra("org.panda3d.OUTPUT_URI");
     }
     }
 
 
     public String getCacheDirString() {
     public String getCacheDirString() {

+ 23 - 0
panda/src/android/PythonActivity.java

@@ -0,0 +1,23 @@
+/**
+ * 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 PythonActivity.java
+ * @author rdb
+ * @date 2018-02-04
+ */
+
+package org.panda3d.android;
+
+import org.panda3d.android.PandaActivity;
+
+/**
+ * This is only declared as a separate class from PandaActivity so that we
+ * can have two separate activity definitions in ApplicationManifest.xml.
+ */
+public class PythonActivity extends PandaActivity {
+}

+ 55 - 22
panda/src/android/android_main.cxx

@@ -17,11 +17,14 @@
 #include "virtualFileSystem.h"
 #include "virtualFileSystem.h"
 #include "filename.h"
 #include "filename.h"
 #include "thread.h"
 #include "thread.h"
+#include "urlSpec.h"
 
 
 #include "config_display.h"
 #include "config_display.h"
 // #define OPENGLES_1 #include "config_androiddisplay.h"
 // #define OPENGLES_1 #include "config_androiddisplay.h"
 
 
 #include <android_native_app_glue.h>
 #include <android_native_app_glue.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
 
 
 // struct android_app* panda_android_app = NULL;
 // struct android_app* panda_android_app = NULL;
 
 
@@ -67,6 +70,55 @@ void android_main(struct android_app* app) {
   android_cat.info()
   android_cat.info()
     << "New native activity started on " << *current_thread << "\n";
     << "New native activity started on " << *current_thread << "\n";
 
 
+  // Were we given an optional location to write the stdout/stderr streams?
+  methodID = env->GetMethodID(activity_class, "getIntentOutputUri", "()Ljava/lang/String;");
+  jstring joutput_uri = (jstring) env->CallObjectMethod(activity->clazz, methodID);
+  if (joutput_uri != nullptr) {
+    const char *output_uri = env->GetStringUTFChars(joutput_uri, nullptr);
+
+    if (output_uri != nullptr && output_uri[0] != 0) {
+      URLSpec spec(output_uri);
+
+      if (spec.get_scheme() == "file") {
+        string path = spec.get_path();
+        int fd = open(path.c_str(), O_CREAT | O_TRUNC | O_WRONLY);
+        if (fd != -1) {
+          android_cat.info()
+            << "Writing standard output to file " << path << "\n";
+
+          dup2(fd, 1);
+          dup2(fd, 2);
+        } else {
+          android_cat.error()
+            << "Failed to open output path " << path << "\n";
+        }
+      } else if (spec.get_scheme() == "tcp") {
+        string host = spec.get_server();
+        int fd = socket(AF_INET, SOCK_STREAM, 0);
+        struct sockaddr_in serv_addr = {0};
+        serv_addr.sin_family = AF_INET;
+        serv_addr.sin_port = htons(spec.get_port());
+        serv_addr.sin_addr.s_addr = inet_addr(host.c_str());
+        if (connect(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == 0) {
+          android_cat.info()
+            << "Writing standard output to socket "
+            << spec.get_server_and_port() << "\n";
+          dup2(fd, 1);
+          dup2(fd, 2);
+        } else {
+          android_cat.error()
+            << "Failed to open output socket "
+            << spec.get_server_and_port() << "\n";
+        }
+        close(fd);
+      } else {
+        android_cat.error()
+          << "Unsupported scheme in output URI: " << output_uri << "\n";
+      }
+      env->ReleaseStringUTFChars(joutput_uri, output_uri);
+    }
+  }
+
   // Fetch the data directory.
   // Fetch the data directory.
   jmethodID get_appinfo = env->GetMethodID(activity_class, "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;");
   jmethodID get_appinfo = env->GetMethodID(activity_class, "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;");
 
 
@@ -186,28 +238,6 @@ void android_main(struct android_app* app) {
     }
     }
   }
   }
 
 
-  // Were we given an optional location to write the stdout/stderr streams?
-  methodID = env->GetMethodID(activity_class, "getIntentOutputPath", "()Ljava/lang/String;");
-  jstring joutput_path = (jstring) env->CallObjectMethod(activity->clazz, methodID);
-  if (joutput_path != nullptr) {
-    const char *output_path = env->GetStringUTFChars(joutput_path, nullptr);
-
-    if (output_path != nullptr && output_path[0] != 0) {
-      int fd = open(output_path, O_CREAT | O_TRUNC | O_WRONLY);
-      if (fd != -1) {
-        android_cat.info()
-          << "Writing standard output to file " << output_path << "\n";
-
-        dup2(fd, 1);
-        dup2(fd, 2);
-      } else {
-        android_cat.error()
-          << "Failed to open output path " << output_path << "\n";
-      }
-      env->ReleaseStringUTFChars(joutput_path, output_path);
-    }
-  }
-
   // Create bogus argc and argv for calling the main function.
   // Create bogus argc and argv for calling the main function.
   const char *argv[] = {"pview", nullptr, nullptr};
   const char *argv[] = {"pview", nullptr, nullptr};
   int argc = 1;
   int argc = 1;
@@ -266,6 +296,9 @@ void android_main(struct android_app* app) {
     env->ReleaseStringUTFChars(filename, filename_str);
     env->ReleaseStringUTFChars(filename, filename_str);
   }
   }
 
 
+  close(1);
+  close(2);
+
   // Detach the thread before exiting.
   // Detach the thread before exiting.
   activity->vm->DetachCurrentThread();
   activity->vm->DetachCurrentThread();
 }
 }

+ 33 - 7
panda/src/android/pview_manifest.xml

@@ -7,6 +7,7 @@
 
 
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.INTERNET" />
     <uses-sdk android:minSdkVersion="21" />
     <uses-sdk android:minSdkVersion="21" />
     <uses-feature android:glEsVersion="0x00020000" android:required="true" />
     <uses-feature android:glEsVersion="0x00020000" android:required="true" />
 
 
@@ -44,13 +45,38 @@
             <intent-filter>
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
                 <action android:name="android.intent.action.VIEW" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="*/*" scheme="content" host="com.termux.files" />
-                <data android:pathPattern=".*\\.egg" />
-                <data android:pathPattern=".*\\.egg.pz" />
-                <data android:pathPattern=".*\\.egg.gz" />
-                <data android:pathPattern=".*\\.bam" />
-                <data android:pathPattern=".*\\.bam.pz" />
-                <data android:pathPattern=".*\\.bam.gz" />
+                <data android:mimeType="*/*" android:scheme="content" android:host="com.termux.files" android:pathPattern=".*\\.egg" />
+            </intent-filter>
+        </activity>
+        <activity android:name="org.panda3d.android.PythonActivity"
+                android:label="Panda Python" android:theme="@android:style/Theme.NoTitleBar"
+                android:configChanges="orientation|keyboardHidden"
+                android:launchMode="singleInstance">
+
+            <meta-data android:name="android.app.lib_name"
+                       android:value="ppython" />
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="text/x-python" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="*/*" android:scheme="file" />
+                <data android:pathPattern=".*\\.py" />
+                <data android:pathPattern=".*\\.pyw" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="*/*" android:scheme="content" android:host="com.termux.files" />
+                <data android:pathPattern=".*\\.py" />
+                <data android:pathPattern=".*\\.pyw" />
             </intent-filter>
             </intent-filter>
         </activity>
         </activity>
     </application>
     </application>

+ 80 - 0
panda/src/android/python_main.cxx

@@ -0,0 +1,80 @@
+/**
+ * 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 python_main.cxx
+ * @author rdb
+ * @date 2018-02-12
+ */
+
+#include "dtoolbase.h"
+#include "config_android.h"
+#include "executionEnvironment.h"
+
+#undef _POSIX_C_SOURCE
+#undef _XOPEN_SOURCE
+#include <Python.h>
+#if PY_MAJOR_VERSION >= 3
+#include <wchar.h>
+#endif
+
+#include <dlfcn.h>
+
+/**
+ * The main entry point for the Python activity.  Called by android_main.
+ */
+int main(int argc, char *argv[]) {
+  if (argc <= 1) {
+    return 1;
+  }
+
+  // Help out Python by telling it which encoding to use
+  Py_FileSystemDefaultEncoding = "utf-8";
+
+  Py_SetProgramName(Py_DecodeLocale("ppython", nullptr));
+
+  // Set PYTHONHOME to the location of the .apk file.
+  string apk_path = ExecutionEnvironment::get_binary_name();
+  Py_SetPythonHome(Py_DecodeLocale(apk_path.c_str(), nullptr));
+
+  // We need to make zlib available to zipimport, but I don't know how
+  // we could inject our import hook before Py_Initialize, so instead
+  // load it as though it were a built-in module.
+  void *zlib = dlopen("libpy.zlib.so", RTLD_NOW);
+  if (zlib != nullptr) {
+    void *init = dlsym(zlib, "PyInit_zlib");
+    if (init != nullptr) {
+      PyImport_AppendInittab("zlib", (PyObject *(*)())init);
+    }
+  }
+
+  Py_Initialize();
+
+  // This is used by the import hook to locate the module libraries.
+  Filename dtool_name = ExecutionEnvironment::get_dtool_name();
+  string native_dir = dtool_name.get_dirname();
+  PyObject *py_native_dir = PyUnicode_FromStringAndSize(native_dir.c_str(), native_dir.size());
+  PySys_SetObject("_native_library_dir", py_native_dir);
+  Py_DECREF(py_native_dir);
+
+  int sts = 1;
+  FILE *fp = fopen(argv[1], "r");
+  if (fp != nullptr) {
+    int res = PyRun_AnyFile(fp, argv[1]);
+    if (res > 0) {
+      sts = 0;
+    } else {
+      android_cat.error() << "Error running " << argv[1] << "\n";
+      PyErr_Print();
+    }
+  } else {
+    android_cat.error() << "Unable to open " << argv[1] << "\n";
+  }
+
+  Py_Finalize();
+  return sts;
+}

+ 14 - 0
panda/src/android/run_pview.sh

@@ -0,0 +1,14 @@
+# This script can be used for launching the Panda viewer from the Android
+# terminal environment, for example from within termux.  It uses a socket
+# to pipe the command-line output back to the terminal.
+
+port=12345
+
+if [[ $# -eq 0 ]] ; then
+    echo "Pass full path of model"
+    exit 1
+fi
+
+am start --activity-clear-task -n org.panda3d.sdk/org.panda3d.android.PandaActivity --user 0 --es org.panda3d.OUTPUT_URI tcp://127.0.0.1:$port --grant-read-uri-permission --grant-write-uri-permission file://$(realpath $1)
+
+nc -l -p $port

+ 14 - 0
panda/src/android/run_python.sh

@@ -0,0 +1,14 @@
+# This script can be used for launching a Python script from the Android
+# terminal environment, for example from within termux.  It uses a socket
+# to pipe the command-line output back to the terminal.
+
+port=12345
+
+if [[ $# -eq 0 ]] ; then
+    echo "Pass full path of script"
+    exit 1
+fi
+
+am start --activity-clear-task -n org.panda3d.sdk/org.panda3d.android.PythonActivity --user 0 --es org.panda3d.OUTPUT_URI tcp://127.0.0.1:$port --grant-read-uri-permission --grant-write-uri-permission file://$(realpath $1)
+
+nc -l -p $port

+ 34 - 0
panda/src/android/site.py

@@ -0,0 +1,34 @@
+import sys
+import os
+
+from importlib.abc import Loader, MetaPathFinder
+from importlib.machinery import ModuleSpec
+
+if sys.version_info >= (3, 5):
+    from importlib import _bootstrap_external
+else:
+    from importlib import _bootstrap as _bootstrap_external
+
+sys.platform = "android"
+
+class AndroidExtensionFinder(MetaPathFinder):
+    @classmethod
+    def find_spec(cls, fullname, path=None, target=None):
+        soname = 'libpy.' + fullname + '.so'
+        path = os.path.join(sys._native_library_dir, soname)
+
+        if os.path.exists(path):
+            loader = _bootstrap_external.ExtensionFileLoader(fullname, path)
+            return ModuleSpec(fullname, loader, origin=path)
+
+
+def main():
+    """Adds the site-packages directory to the sys.path.
+    Also, registers the import hook for extension modules."""
+
+    sys.path.append('{0}/lib/python{1}.{2}/site-packages'.format(sys.prefix, *sys.version_info))
+    sys.meta_path.append(AndroidExtensionFinder)
+
+
+if not sys.flags.no_site:
+    main()

+ 0 - 3
panda/src/androiddisplay/androidGraphicsWindow.cxx

@@ -309,8 +309,6 @@ open_window() {
 
 
   _fb_properties = androidgsg->get_fb_properties();
   _fb_properties = androidgsg->get_fb_properties();
 
 
-  androiddisplay_cat.error() << "open_window done\n";
-
   return true;
   return true;
 }
 }
 
 
@@ -366,7 +364,6 @@ create_surface() {
 
 
   // Create a context.
   // Create a context.
   if (androidgsg->_context == EGL_NO_CONTEXT) {
   if (androidgsg->_context == EGL_NO_CONTEXT) {
-    androiddisplay_cat.error() << "creating context\n";
     if (!androidgsg->create_context()) {
     if (!androidgsg->create_context()) {
       return false;
       return false;
     }
     }

+ 17 - 0
panda/src/audiotraits/config_openalAudio.cxx

@@ -30,6 +30,23 @@ ConfigVariableString openal_device
  PRC_DESC("Specify the OpenAL device string for audio playback (no quotes).  If this "
  PRC_DESC("Specify the OpenAL device string for audio playback (no quotes).  If this "
           "is not specified, the OpenAL default device is used."));
           "is not specified, the OpenAL default device is used."));
 
 
+ConfigVariableInt openal_buffer_delete_retries
+("openal-buffer-delete-retries", 5,
+ PRC_DESC("If deleting a buffer fails due to still being in use, the OpenAL "
+          "sound plugin will wait a moment and retry deletion, with an "
+          "exponentially-increasing delay for each try.  This number "
+          "specifies how many repeat tries (not counting the initial try) "
+          "should be made before giving up and raising an error."));
+
+ConfigVariableDouble openal_buffer_delete_delay
+("openal-buffer-delete-delay", 0.001,
+ PRC_DESC("If deleting a buffer fails due to still being in use, the OpenAL "
+          "sound plugin will wait a moment and retry deletion, with an "
+          "exponentially-increasing delay for each try.  This number "
+          "specifies how long, in seconds, the OpenAL plugin will wait after "
+          "its first failed try.  The second try will be double this "
+          "delay, the third quadruple, and so on."));
+
 
 
 /**
 /**
  * Initializes the library.  This must be called at least once before any of
  * Initializes the library.  This must be called at least once before any of

+ 2 - 0
panda/src/audiotraits/config_openalAudio.h

@@ -26,5 +26,7 @@ extern "C" EXPCL_OPENAL_AUDIO void init_libOpenALAudio();
 extern "C" EXPCL_OPENAL_AUDIO Create_AudioManager_proc *get_audio_manager_func_openal_audio();
 extern "C" EXPCL_OPENAL_AUDIO Create_AudioManager_proc *get_audio_manager_func_openal_audio();
 
 
 extern ConfigVariableString openal_device;
 extern ConfigVariableString openal_device;
+extern ConfigVariableInt openal_buffer_delete_retries;
+extern ConfigVariableDouble openal_buffer_delete_delay;
 
 
 #endif // CONFIG_OPENALAUDIO_H
 #endif // CONFIG_OPENALAUDIO_H

+ 46 - 7
panda/src/audiotraits/openalAudioManager.cxx

@@ -524,7 +524,7 @@ get_sound(const string &file_name, bool positional, int mode) {
 void OpenALAudioManager::
 void OpenALAudioManager::
 uncache_sound(const string& file_name) {
 uncache_sound(const string& file_name) {
   ReMutexHolder holder(_lock);
   ReMutexHolder holder(_lock);
-  assert(is_valid());
+  nassertv(is_valid());
   Filename path = file_name;
   Filename path = file_name;
 
 
   VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr();
   VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr();
@@ -910,7 +910,7 @@ reduce_sounds_playing_to(unsigned int count) {
   int limit = _sounds_playing.size() - count;
   int limit = _sounds_playing.size() - count;
   while (limit-- > 0) {
   while (limit-- > 0) {
     SoundsPlaying::iterator sound = _sounds_playing.begin();
     SoundsPlaying::iterator sound = _sounds_playing.begin();
-    assert(sound != _sounds_playing.end());
+    nassertv(sound != _sounds_playing.end());
     // When the user stops a sound, there is still a PT in the user's hand.
     // When the user stops a sound, there is still a PT in the user's hand.
     // When we stop a sound here, however, this can remove the last PT.  This
     // When we stop a sound here, however, this can remove the last PT.  This
     // can cause an ugly recursion where stop calls the destructor, and the
     // can cause an ugly recursion where stop calls the destructor, and the
@@ -1052,7 +1052,7 @@ OpenALAudioManager::SoundData::
   if (_sample != 0) {
   if (_sample != 0) {
     if (_manager->_is_valid) {
     if (_manager->_is_valid) {
       _manager->make_current();
       _manager->make_current();
-      alDeleteBuffers(1,&_sample);
+      _manager->delete_buffer(_sample);
     }
     }
     _sample = 0;
     _sample = 0;
   }
   }
@@ -1111,8 +1111,8 @@ discard_excess_cache(int sample_limit) {
 
 
   while (((int)_expiring_samples.size()) > sample_limit) {
   while (((int)_expiring_samples.size()) > sample_limit) {
     SoundData *sd = (SoundData*)(_expiring_samples.front());
     SoundData *sd = (SoundData*)(_expiring_samples.front());
-    assert(sd->_client_count == 0);
-    assert(sd->_expire == _expiring_samples.begin());
+    nassertv(sd->_client_count == 0);
+    nassertv(sd->_expire == _expiring_samples.begin());
     _expiring_samples.pop_front();
     _expiring_samples.pop_front();
     _sample_cache.erase(_sample_cache.find(sd->_movie->get_filename()));
     _sample_cache.erase(_sample_cache.find(sd->_movie->get_filename()));
     audio_debug("Expiring: " << sd->_movie->get_filename().get_basename());
     audio_debug("Expiring: " << sd->_movie->get_filename().get_basename());
@@ -1121,10 +1121,49 @@ discard_excess_cache(int sample_limit) {
 
 
   while (((int)_expiring_streams.size()) > stream_limit) {
   while (((int)_expiring_streams.size()) > stream_limit) {
     SoundData *sd = (SoundData*)(_expiring_streams.front());
     SoundData *sd = (SoundData*)(_expiring_streams.front());
-    assert(sd->_client_count == 0);
-    assert(sd->_expire == _expiring_streams.begin());
+    nassertv(sd->_client_count == 0);
+    nassertv(sd->_expire == _expiring_streams.begin());
     _expiring_streams.pop_front();
     _expiring_streams.pop_front();
     audio_debug("Expiring: " << sd->_movie->get_filename().get_basename());
     audio_debug("Expiring: " << sd->_movie->get_filename().get_basename());
     delete sd;
     delete sd;
   }
   }
 }
 }
+
+/**
+ * Deletes an OpenAL buffer.  This is a special function because some
+ * implementations of OpenAL (e.g. Apple's) don't unlock the buffers
+ * immediately, due to needing to coordinate with another thread.  If this is
+ * the case, the alDeleteBuffers call will error back with AL_INVALID_OPERATION
+ * as if trying to delete an actively-used buffer, which will tell us to wait a
+ * bit and try again.
+ */
+void OpenALAudioManager::
+delete_buffer(ALuint buffer) {
+  ReMutexHolder holder(_lock);
+  int tries = 0;
+  ALuint error;
+
+  // Keep trying until we succeed (or give up).
+  while (true) {
+    alDeleteBuffers(1, &buffer);
+    error = alGetError();
+
+    if (error == AL_NO_ERROR) {
+      // Success!  This will happen right away 99% of the time.
+      return;
+    } else if (error != AL_INVALID_OPERATION) {
+      // We weren't expecting that.  This should be reported.
+      break;
+    } else if (tries >= openal_buffer_delete_retries.get_value()) {
+      // We ran out of retries.  Give up.
+      break;
+    } else {
+      // Make another try after (delay * 2^n) seconds.
+      Thread::sleep(openal_buffer_delete_delay.get_value() * (1 << tries));
+      tries++;
+    }
+  }
+
+  // If we got here, one of the breaks above happened, indicating an error.
+  audio_error("failed to delete a buffer: " << alGetString(error) );
+}

+ 2 - 0
panda/src/audiotraits/openalAudioManager.h

@@ -129,6 +129,8 @@ private:
   void decrement_client_count(SoundData *sd);
   void decrement_client_count(SoundData *sd);
   void discard_excess_cache(int limit);
   void discard_excess_cache(int limit);
 
 
+  void delete_buffer(ALuint buffer);
+
   void starting_sound(OpenALAudioSound* audio);
   void starting_sound(OpenALAudioSound* audio);
   void stopping_sound(OpenALAudioSound* audio);
   void stopping_sound(OpenALAudioSound* audio);
 
 

+ 41 - 4
panda/src/audiotraits/openalAudioSound.I

@@ -40,7 +40,7 @@ get_calibrated_clock(double rtc) const {
  *
  *
  * Returns true on success, false on failure.
  * Returns true on success, false on failure.
  */
  */
-bool OpenALAudioSound::
+INLINE bool OpenALAudioSound::
 require_sound_data() {
 require_sound_data() {
   if (_sd==0) {
   if (_sd==0) {
     _sd = _manager->get_sound_data(_movie, _desired_mode);
     _sd = _manager->get_sound_data(_movie, _desired_mode);
@@ -55,11 +55,48 @@ require_sound_data() {
 /**
 /**
  * Checks if the sound data record is present and releasable, and if so,
  * Checks if the sound data record is present and releasable, and if so,
  * releases it.
  * releases it.
+ *
+ * The sound data is "releasable" if it's from an ordinary, local file.  Remote
+ * streams cannot necessarily be reopened if lost, so we'll hold onto them if
+ * so.  The `force` argument overrides this, indicating we don't intend to
+ * reacquire the sound data.
  */
  */
-void OpenALAudioSound::
-release_sound_data() {
-  if ((_sd!=0) && (!_movie->get_filename().empty())) {
+INLINE void OpenALAudioSound::
+release_sound_data(bool force) {
+  if (!has_sound_data()) return;
+
+  if (force || !_movie->get_filename().empty()) {
     _manager->decrement_client_count(_sd);
     _manager->decrement_client_count(_sd);
     _sd = 0;
     _sd = 0;
   }
   }
 }
 }
+
+/**
+ * Checks if the sound has NOT been cleaned up yet.
+ */
+INLINE bool OpenALAudioSound::
+is_valid() const {
+  return _manager != NULL;
+}
+
+/**
+ * Checks if the sound is playing.  This is per the OpenALAudioManager's
+ * definition of "playing" -- as in, "will be called upon every update"
+ *
+ * This is mainly intended for use in asserts.
+ */
+INLINE bool OpenALAudioSound::
+is_playing() const {
+  // Manager only gives us a _source if we need it (to talk to OpenAL), so:
+  return _source != 0;
+}
+
+/**
+ * Checks if the sound has its SoundData structure open at the moment.
+ *
+ * This is mainly intended for use in asserts.
+ */
+INLINE bool OpenALAudioSound::
+has_sound_data() const {
+  return _sd != NULL;
+}

+ 89 - 38
panda/src/audiotraits/openalAudioSound.cxx

@@ -80,7 +80,7 @@ OpenALAudioSound(OpenALAudioManager* manager,
       audio_warning("stereo sound " << movie->get_filename() << " will not be spatialized");
       audio_warning("stereo sound " << movie->get_filename() << " will not be spatialized");
     }
     }
   }
   }
-  release_sound_data();
+  release_sound_data(false);
 }
 }
 
 
 
 
@@ -99,15 +99,14 @@ OpenALAudioSound::
 void OpenALAudioSound::
 void OpenALAudioSound::
 cleanup() {
 cleanup() {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
-  if (_manager == 0) {
+  if (!is_valid()) {
     return;
     return;
   }
   }
-  if (_source) {
+  if (is_playing()) {
     stop();
     stop();
   }
   }
-  if (_sd) {
-    _manager->decrement_client_count(_sd);
-    _sd = 0;
+  if (has_sound_data()) {
+    release_sound_data(true);
   }
   }
   _manager->release_sound(this);
   _manager->release_sound(this);
   _manager = 0;
   _manager = 0;
@@ -119,7 +118,8 @@ cleanup() {
 void OpenALAudioSound::
 void OpenALAudioSound::
 play() {
 play() {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
-  if (_manager == 0) return;
+
+  if (!is_valid()) return;
 
 
   PN_stdfloat px,py,pz,vx,vy,vz;
   PN_stdfloat px,py,pz,vx,vy,vz;
 
 
@@ -136,7 +136,7 @@ play() {
   }
   }
 
 
   _manager->starting_sound(this);
   _manager->starting_sound(this);
-  if (!_source) {
+  if (!is_playing()) {
     return;
     return;
   }
   }
 
 
@@ -195,11 +195,14 @@ play() {
 void OpenALAudioSound::
 void OpenALAudioSound::
 stop() {
 stop() {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
-  if (_manager==0) return;
 
 
-  if (_source) {
+  if (!is_valid()) return;
+
+  if (is_playing()) {
     _manager->make_current();
     _manager->make_current();
 
 
+    nassertv(has_sound_data());
+
     alGetError(); // clear errors
     alGetError(); // clear errors
     alSourceStop(_source);
     alSourceStop(_source);
     al_audio_errcheck("stopping a source");
     al_audio_errcheck("stopping a source");
@@ -208,15 +211,14 @@ stop() {
     for (int i=0; i<((int)(_stream_queued.size())); i++) {
     for (int i=0; i<((int)(_stream_queued.size())); i++) {
       ALuint buffer = _stream_queued[i]._buffer;
       ALuint buffer = _stream_queued[i]._buffer;
       if (buffer != _sd->_sample) {
       if (buffer != _sd->_sample) {
-        alDeleteBuffers(1, &buffer);
-        al_audio_errcheck("deleting a buffer");
+        _manager->delete_buffer(buffer);
       }
       }
     }
     }
     _stream_queued.resize(0);
     _stream_queued.resize(0);
   }
   }
 
 
   _manager->stopping_sound(this);
   _manager->stopping_sound(this);
-  release_sound_data();
+  release_sound_data(false);
 }
 }
 
 
 /**
 /**
@@ -225,6 +227,9 @@ stop() {
 void OpenALAudioSound::
 void OpenALAudioSound::
 finished() {
 finished() {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
+
+  if (!is_valid()) return;
+
   stop();
   stop();
   _current_time = _length;
   _current_time = _length;
   if (!_finished_event.empty()) {
   if (!_finished_event.empty()) {
@@ -255,7 +260,8 @@ get_loop() const {
 void OpenALAudioSound::
 void OpenALAudioSound::
 set_loop_count(unsigned long loop_count) {
 set_loop_count(unsigned long loop_count) {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
-  if (_manager==0) return;
+
+  if (!is_valid()) return;
 
 
   if (loop_count >= 1000000000) {
   if (loop_count >= 1000000000) {
     loop_count = 0;
     loop_count = 0;
@@ -282,9 +288,14 @@ void OpenALAudioSound::
 restart_stalled_audio() {
 restart_stalled_audio() {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ALenum status;
   ALenum status;
+
+  if (!is_valid()) return;
+  nassertv(is_playing());
+
   if (_stream_queued.size() == 0) {
   if (_stream_queued.size() == 0) {
     return;
     return;
   }
   }
+
   alGetError();
   alGetError();
   alGetSourcei(_source, AL_SOURCE_STATE, &status);
   alGetSourcei(_source, AL_SOURCE_STATE, &status);
   if (status != AL_PLAYING) {
   if (status != AL_PLAYING) {
@@ -298,6 +309,9 @@ restart_stalled_audio() {
 void OpenALAudioSound::
 void OpenALAudioSound::
 queue_buffer(ALuint buffer, int samples, int loop_index, double time_offset) {
 queue_buffer(ALuint buffer, int samples, int loop_index, double time_offset) {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
+
+  nassertv(is_playing());
+
   // Now push the buffer into the stream queue.
   // Now push the buffer into the stream queue.
   alGetError();
   alGetError();
   alSourceQueueBuffers(_source,1,&buffer);
   alSourceQueueBuffers(_source,1,&buffer);
@@ -322,6 +336,8 @@ ALuint OpenALAudioSound::
 make_buffer(int samples, int channels, int rate, unsigned char *data) {
 make_buffer(int samples, int channels, int rate, unsigned char *data) {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
 
 
+  nassertr(is_playing(), 0);
+
   // Allocate a buffer to hold the data.
   // Allocate a buffer to hold the data.
   alGetError();
   alGetError();
   ALuint buffer;
   ALuint buffer;
@@ -354,6 +370,8 @@ int OpenALAudioSound::
 read_stream_data(int bytelen, unsigned char *buffer) {
 read_stream_data(int bytelen, unsigned char *buffer) {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
 
 
+  nassertr(has_sound_data(), 0);
+
   MovieAudioCursor *cursor = _sd->_stream;
   MovieAudioCursor *cursor = _sd->_stream;
   double length = cursor->length();
   double length = cursor->length();
   int channels = cursor->audio_channels();
   int channels = cursor->audio_channels();
@@ -404,6 +422,9 @@ read_stream_data(int bytelen, unsigned char *buffer) {
 void OpenALAudioSound::
 void OpenALAudioSound::
 correct_calibrated_clock(double rtc, double t) {
 correct_calibrated_clock(double rtc, double t) {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
+
+  nassertv(is_playing());
+
   double cc = (rtc - _calibrated_clock_base) * _calibrated_clock_scale;
   double cc = (rtc - _calibrated_clock_base) * _calibrated_clock_scale;
   double diff = cc-t;
   double diff = cc-t;
   _calibrated_clock_decavg = (_calibrated_clock_decavg * 0.95) + (diff * 0.05);
   _calibrated_clock_decavg = (_calibrated_clock_decavg * 0.95) + (diff * 0.05);
@@ -435,6 +456,11 @@ correct_calibrated_clock(double rtc, double t) {
 void OpenALAudioSound::
 void OpenALAudioSound::
 pull_used_buffers() {
 pull_used_buffers() {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
+
+  if (!is_valid()) return;
+  nassertv(is_playing());
+  nassertv(has_sound_data());
+
   while (_stream_queued.size()) {
   while (_stream_queued.size()) {
     ALuint buffer = 0;
     ALuint buffer = 0;
     ALint num_buffers = 0;
     ALint num_buffers = 0;
@@ -446,18 +472,34 @@ pull_used_buffers() {
     int err = alGetError();
     int err = alGetError();
     if (err == AL_NO_ERROR) {
     if (err == AL_NO_ERROR) {
       if (_stream_queued[0]._buffer != buffer) {
       if (_stream_queued[0]._buffer != buffer) {
-        audio_error("corruption in stream queue");
-        cleanup();
-        return;
-      }
-      _stream_queued.pop_front();
-      if (_stream_queued.size()) {
-        double al = _stream_queued[0]._time_offset + _stream_queued[0]._loop_index * _length;
-        double rtc = TrueClock::get_global_ptr()->get_short_time();
-        correct_calibrated_clock(rtc, al);
-      }
-      if (buffer != _sd->_sample) {
-        alDeleteBuffers(1,&buffer);
+        // This is certainly atypical: most implementations of OpenAL unqueue
+        // buffers in FIFO order. However, some (e.g. Apple's) can unqueue
+        // buffers out-of-order if playback is interrupted. So, we don't freak
+        // out unless `buffer` isn't in _stream_queued at all.
+        bool found_culprit = false;
+        for (auto it = _stream_queued.begin(); it != _stream_queued.end(); ++it) {
+          if (it->_buffer == buffer) {
+            // Phew. Found it. Just remove that.
+            _stream_queued.erase(it);
+            found_culprit = true;
+            break;
+          }
+        }
+        if (!found_culprit) {
+          audio_error("corruption in stream queue");
+          cleanup();
+          return;
+        }
+      } else {
+        _stream_queued.pop_front();
+        if (_stream_queued.size()) {
+          double al = _stream_queued[0]._time_offset + _stream_queued[0]._loop_index * _length;
+          double rtc = TrueClock::get_global_ptr()->get_short_time();
+          correct_calibrated_clock(rtc, al);
+        }
+        if (buffer != _sd->_sample) {
+          _manager->delete_buffer(buffer);
+        }
       }
       }
     } else {
     } else {
       break;
       break;
@@ -474,6 +516,10 @@ push_fresh_buffers() {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
   static unsigned char data[65536];
   static unsigned char data[65536];
 
 
+  if (!is_valid()) return;
+  nassertv(is_playing());
+  nassertv(has_sound_data());
+
   if (_sd->_sample) {
   if (_sd->_sample) {
     while ((_loops_completed < _playing_loops) &&
     while ((_loops_completed < _playing_loops) &&
            (_stream_queued.size() < 100)) {
            (_stream_queued.size() < 100)) {
@@ -499,9 +545,9 @@ push_fresh_buffers() {
         break;
         break;
       }
       }
       ALuint buffer = make_buffer(samples, channels, rate, data);
       ALuint buffer = make_buffer(samples, channels, rate, data);
-      if (_manager == 0) return;
+      if (!is_valid() || !buffer) return;
       queue_buffer(buffer, samples, loop_index, time_offset);
       queue_buffer(buffer, samples, loop_index, time_offset);
-      if (_manager == 0) return;
+      if (!is_valid()) return;
       fill += samples;
       fill += samples;
     }
     }
   }
   }
@@ -523,7 +569,7 @@ set_time(PN_stdfloat time) {
 PN_stdfloat OpenALAudioSound::
 PN_stdfloat OpenALAudioSound::
 get_time() const {
 get_time() const {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
-  if (_manager == 0) {
+  if (!is_valid()) {
     return 0.0;
     return 0.0;
   }
   }
   return _current_time;
   return _current_time;
@@ -535,7 +581,9 @@ get_time() const {
 void OpenALAudioSound::
 void OpenALAudioSound::
 cache_time(double rtc) {
 cache_time(double rtc) {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
-  assert(_source != 0);
+
+  nassertv(is_playing());
+
   double t=get_calibrated_clock(rtc);
   double t=get_calibrated_clock(rtc);
   double max = _length * _playing_loops;
   double max = _length * _playing_loops;
   if (t >= max) {
   if (t >= max) {
@@ -553,7 +601,7 @@ set_volume(PN_stdfloat volume) {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
   _volume=volume;
   _volume=volume;
 
 
-  if (_source) {
+  if (is_playing()) {
     volume*=_manager->get_volume();
     volume*=_manager->get_volume();
     _manager->make_current();
     _manager->make_current();
     alGetError(); // clear errors
     alGetError(); // clear errors
@@ -597,7 +645,7 @@ void OpenALAudioSound::
 set_play_rate(PN_stdfloat play_rate) {
 set_play_rate(PN_stdfloat play_rate) {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
   _play_rate = play_rate;
   _play_rate = play_rate;
-  if (_source) {
+  if (is_playing()) {
     alSourcef(_source, AL_PITCH, play_rate);
     alSourcef(_source, AL_PITCH, play_rate);
   }
   }
 }
 }
@@ -639,7 +687,7 @@ set_3d_attributes(PN_stdfloat px, PN_stdfloat py, PN_stdfloat pz, PN_stdfloat vx
   _velocity[1] = vz;
   _velocity[1] = vz;
   _velocity[2] = -vy;
   _velocity[2] = -vy;
 
 
-  if (_source) {
+  if (is_playing()) {
     _manager->make_current();
     _manager->make_current();
 
 
     alGetError(); // clear errors
     alGetError(); // clear errors
@@ -675,7 +723,7 @@ set_3d_min_distance(PN_stdfloat dist) {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
   _min_dist = dist;
   _min_dist = dist;
 
 
-  if (_source) {
+  if (is_playing()) {
     _manager->make_current();
     _manager->make_current();
 
 
     alGetError(); // clear errors
     alGetError(); // clear errors
@@ -700,7 +748,7 @@ set_3d_max_distance(PN_stdfloat dist) {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
   _max_dist = dist;
   _max_dist = dist;
 
 
-  if (_source) {
+  if (is_playing()) {
     _manager->make_current();
     _manager->make_current();
 
 
     alGetError(); // clear errors
     alGetError(); // clear errors
@@ -725,7 +773,7 @@ set_3d_drop_off_factor(PN_stdfloat factor) {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
   _drop_off_factor = factor;
   _drop_off_factor = factor;
 
 
-  if (_source) {
+  if (is_playing()) {
     _manager->make_current();
     _manager->make_current();
 
 
     alGetError(); // clear errors
     alGetError(); // clear errors
@@ -743,13 +791,16 @@ get_3d_drop_off_factor() const {
 }
 }
 
 
 /**
 /**
- * Sets whether the sound is marked "active".  By default, the active flag
+ * Sets whether the sound is marked "active".  By default, the active flag is
  * true for all sounds.  If the active flag is set to false for any particular
  * true for all sounds.  If the active flag is set to false for any particular
  * sound, the sound will not be heard.
  * sound, the sound will not be heard.
  */
  */
 void OpenALAudioSound::
 void OpenALAudioSound::
 set_active(bool active) {
 set_active(bool active) {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
+
+  if (!is_valid()) return;
+
   if (_active!=active) {
   if (_active!=active) {
     _active=active;
     _active=active;
     if (_active) {
     if (_active) {
@@ -813,7 +864,7 @@ get_name() const {
 AudioSound::SoundStatus OpenALAudioSound::
 AudioSound::SoundStatus OpenALAudioSound::
 status() const {
 status() const {
   ReMutexHolder holder(OpenALAudioManager::_lock);
   ReMutexHolder holder(OpenALAudioManager::_lock);
-  if (_source==0) {
+  if (!is_playing()) {
     return AudioSound::READY;
     return AudioSound::READY;
   }
   }
   if ((_loops_completed >= _playing_loops)&&(_stream_queued.size()==0)) {
   if ((_loops_completed >= _playing_loops)&&(_stream_queued.size()==0)) {

+ 5 - 3
panda/src/audiotraits/openalAudioSound.h

@@ -117,11 +117,13 @@ private:
   void pull_used_buffers();
   void pull_used_buffers();
   void push_fresh_buffers();
   void push_fresh_buffers();
   INLINE bool require_sound_data();
   INLINE bool require_sound_data();
-  INLINE void release_sound_data();
+  INLINE void release_sound_data(bool force);
 
 
-private:
+  INLINE bool is_valid() const;
+  INLINE bool is_playing() const;
+  INLINE bool has_sound_data() const;
 
 
-  void do_stop();
+private:
 
 
   PT(MovieAudio) _movie;
   PT(MovieAudio) _movie;
   OpenALAudioManager::SoundData *_sd;
   OpenALAudioManager::SoundData *_sd;

+ 18 - 0
panda/src/bullet/bulletWorld.cxx

@@ -1104,6 +1104,24 @@ get_group_collision_flag(unsigned int group1, unsigned int group2) const {
   return _filter_cb2._collide[group1].get_bit(group2);
   return _filter_cb2._collide[group1].get_bit(group2);
 }
 }
 
 
+/**
+ *
+ */
+void BulletWorld::
+set_force_update_all_aabbs(bool force) {
+  LightMutexHolder holder(get_global_lock());
+  _world->setForceUpdateAllAabbs(force);
+}
+
+/**
+ *
+ */
+bool BulletWorld::
+get_force_update_all_aabbs() const {
+  LightMutexHolder holder(get_global_lock());
+  return _world->getForceUpdateAllAabbs();
+}
+
 /**
 /**
  *
  *
  */
  */

+ 5 - 0
panda/src/bullet/bulletWorld.h

@@ -134,6 +134,9 @@ PUBLISHED:
   void set_group_collision_flag(unsigned int group1, unsigned int group2, bool enable);
   void set_group_collision_flag(unsigned int group1, unsigned int group2, bool enable);
   bool get_group_collision_flag(unsigned int group1, unsigned int group2) const;
   bool get_group_collision_flag(unsigned int group1, unsigned int group2) const;
 
 
+  void set_force_update_all_aabbs(bool force);
+  bool get_force_update_all_aabbs() const;
+
   // Callbacks
   // Callbacks
   void set_contact_added_callback(CallbackObject *obj);
   void set_contact_added_callback(CallbackObject *obj);
   void clear_contact_added_callback();
   void clear_contact_added_callback();
@@ -166,6 +169,8 @@ PUBLISHED:
   MAKE_SEQ_PROPERTY(vehicles, get_num_vehicles, get_vehicle);
   MAKE_SEQ_PROPERTY(vehicles, get_num_vehicles, get_vehicle);
   MAKE_SEQ_PROPERTY(constraints, get_num_constraints, get_constraint);
   MAKE_SEQ_PROPERTY(constraints, get_num_constraints, get_constraint);
   MAKE_SEQ_PROPERTY(manifolds, get_num_manifolds, get_manifold);
   MAKE_SEQ_PROPERTY(manifolds, get_num_manifolds, get_manifold);
+  MAKE_PROPERTY(force_update_all_aabbs, get_force_update_all_aabbs,
+                                        set_force_update_all_aabbs);
 
 
 PUBLISHED: // Deprecated methods, will be removed soon
 PUBLISHED: // Deprecated methods, will be removed soon
   void attach_ghost(BulletGhostNode *node);
   void attach_ghost(BulletGhostNode *node);

+ 0 - 1
panda/src/char/character.h

@@ -30,7 +30,6 @@
 #include "sliderTable.h"
 #include "sliderTable.h"
 
 
 class CharacterJointBundle;
 class CharacterJointBundle;
-class ComputedVertices;
 
 
 /**
 /**
  * An animated character, with skeleton-morph animation and either soft-
  * An animated character, with skeleton-morph animation and either soft-

+ 12 - 6
panda/src/char/characterJoint.cxx

@@ -39,7 +39,8 @@ CharacterJoint(const CharacterJoint &copy) :
   MovingPartMatrix(copy),
   MovingPartMatrix(copy),
   _character(NULL),
   _character(NULL),
   _net_transform(copy._net_transform),
   _net_transform(copy._net_transform),
-  _initial_net_transform_inverse(copy._initial_net_transform_inverse)
+  _initial_net_transform_inverse(copy._initial_net_transform_inverse),
+  _skinning_matrix(copy._skinning_matrix)
 {
 {
   // We don't copy the sets of transform nodes.
   // We don't copy the sets of transform nodes.
 }
 }
@@ -60,9 +61,12 @@ CharacterJoint(Character *character,
   // update_internals() to get our _net_transform set properly.
   // update_internals() to get our _net_transform set properly.
   update_internals(root, parent, true, false, current_thread);
   update_internals(root, parent, true, false, current_thread);
 
 
-  // And then compute its inverse.  This is needed for ComputedVertices,
-  // during animation.
+  // And then compute its inverse.  This is needed to track changes in
+  // _net_transform as the joint moves, so we can recompute _skinning_matrix,
+  // which maps vertices from their initial positions to their animated
+  // positions.
   _initial_net_transform_inverse = invert(_net_transform);
   _initial_net_transform_inverse = invert(_net_transform);
+  _skinning_matrix = LMatrix4::ident_mat();
 }
 }
 
 
 /**
 /**
@@ -141,11 +145,13 @@ update_internals(PartBundle *root, PartGroup *parent, bool self_changed,
       }
       }
     }
     }
 
 
-    // Also tell our related JointVertexTransforms that they now need to
-    // recompute themselves.
+    // Recompute the transform used by any vertices animated by this joint.
+    _skinning_matrix = _initial_net_transform_inverse * _net_transform;
+
+    // Also tell our related JointVertexTransforms that we've changed their
+    // underlying matrix.
     VertexTransforms::iterator vti;
     VertexTransforms::iterator vti;
     for (vti = _vertex_transforms.begin(); vti != _vertex_transforms.end(); ++vti) {
     for (vti = _vertex_transforms.begin(); vti != _vertex_transforms.end(); ++vti) {
-      (*vti)->_matrix_stale = true;
       (*vti)->mark_modified(current_thread);
       (*vti)->mark_modified(current_thread);
     }
     }
   }
   }

+ 6 - 0
panda/src/char/characterJoint.h

@@ -108,6 +108,12 @@ public:
   LMatrix4 _net_transform;
   LMatrix4 _net_transform;
   LMatrix4 _initial_net_transform_inverse;
   LMatrix4 _initial_net_transform_inverse;
 
 
+  // This is the product of the above; the matrix that gets applied to a
+  // vertex (whose coordinates are in the coordinate space of the character
+  // in its neutral pose) to transform it from its neutral position to its
+  // animated position.
+  LMatrix4 _skinning_matrix;
+
 public:
 public:
   virtual TypeHandle get_type() const {
   virtual TypeHandle get_type() const {
     return get_class_type();
     return get_class_type();

+ 0 - 10
panda/src/char/jointVertexTransform.I

@@ -18,13 +18,3 @@ INLINE const CharacterJoint *JointVertexTransform::
 get_joint() const {
 get_joint() const {
   return _joint;
   return _joint;
 }
 }
-
-/**
- * Recomputes _matrix if it needs it.
- */
-INLINE void JointVertexTransform::
-check_matrix() const {
-  if (_matrix_stale) {
-    ((JointVertexTransform *)this)->compute_matrix();
-  }
-}

+ 5 - 25
panda/src/char/jointVertexTransform.cxx

@@ -24,8 +24,7 @@ TypeHandle JointVertexTransform::_type_handle;
  * Constructs an invalid object; used only by the bam loader.
  * Constructs an invalid object; used only by the bam loader.
  */
  */
 JointVertexTransform::
 JointVertexTransform::
-JointVertexTransform() :
-  _matrix_stale(true)
+JointVertexTransform()
 {
 {
 }
 }
 
 
@@ -35,8 +34,7 @@ JointVertexTransform() :
  */
  */
 JointVertexTransform::
 JointVertexTransform::
 JointVertexTransform(CharacterJoint *joint) :
 JointVertexTransform(CharacterJoint *joint) :
-  _joint(joint),
-  _matrix_stale(true)
+  _joint(joint)
 {
 {
   // Tell the joint that we need to be informed when it moves.
   // Tell the joint that we need to be informed when it moves.
   _joint->_vertex_transforms.insert(this);
   _joint->_vertex_transforms.insert(this);
@@ -57,8 +55,7 @@ JointVertexTransform::
  */
  */
 void JointVertexTransform::
 void JointVertexTransform::
 get_matrix(LMatrix4 &matrix) const {
 get_matrix(LMatrix4 &matrix) const {
-  check_matrix();
-  matrix = _matrix;
+  matrix = _joint->_skinning_matrix;
 }
 }
 
 
 /**
 /**
@@ -69,8 +66,7 @@ get_matrix(LMatrix4 &matrix) const {
  */
  */
 void JointVertexTransform::
 void JointVertexTransform::
 mult_matrix(LMatrix4 &result, const LMatrix4 &previous) const {
 mult_matrix(LMatrix4 &result, const LMatrix4 &previous) const {
-  check_matrix();
-  result.multiply(_matrix, previous);
+  result.multiply(_joint->_skinning_matrix, previous);
 }
 }
 
 
 /**
 /**
@@ -80,9 +76,7 @@ mult_matrix(LMatrix4 &result, const LMatrix4 &previous) const {
  */
  */
 void JointVertexTransform::
 void JointVertexTransform::
 accumulate_matrix(LMatrix4 &accum, PN_stdfloat weight) const {
 accumulate_matrix(LMatrix4 &accum, PN_stdfloat weight) const {
-  check_matrix();
-
-  accum.accumulate(_matrix, weight);
+  accum.accumulate(_joint->_skinning_matrix, weight);
 }
 }
 
 
 /**
 /**
@@ -93,19 +87,6 @@ output(ostream &out) const {
   out << _joint->get_name();
   out << _joint->get_name();
 }
 }
 
 
-/**
- * Recomputes _matrix if it needs it.  Uses locking.
- */
-void JointVertexTransform::
-compute_matrix() {
-  LightMutexHolder holder(_lock);
-  if (_matrix_stale) {
-    _matrix = _joint->_initial_net_transform_inverse * _joint->_net_transform;
-    _matrix_stale = false;
-  }
-}
-
-
 /**
 /**
  * Tells the BamReader how to create objects of type JointVertexTransform.
  * Tells the BamReader how to create objects of type JointVertexTransform.
  */
  */
@@ -165,6 +146,5 @@ fillin(DatagramIterator &scan, BamReader *manager) {
   VertexTransform::fillin(scan, manager);
   VertexTransform::fillin(scan, manager);
 
 
   manager->read_pointer(scan);
   manager->read_pointer(scan);
-  _matrix_stale = true;
   mark_modified(Thread::get_current_thread());
   mark_modified(Thread::get_current_thread());
 }
 }

+ 0 - 7
panda/src/char/jointVertexTransform.h

@@ -47,15 +47,8 @@ PUBLISHED:
   virtual void output(ostream &out) const;
   virtual void output(ostream &out) const;
 
 
 private:
 private:
-  INLINE void check_matrix() const;
-  void compute_matrix();
-
   PT(CharacterJoint) _joint;
   PT(CharacterJoint) _joint;
 
 
-  LMatrix4 _matrix;
-  bool _matrix_stale;
-  LightMutex _lock;
-
 public:
 public:
   static void register_with_read_factory();
   static void register_with_read_factory();
   virtual void write_datagram(BamWriter *manager, Datagram &dg);
   virtual void write_datagram(BamWriter *manager, Datagram &dg);

+ 0 - 1
panda/src/event/asyncFuture.cxx

@@ -244,7 +244,6 @@ add_waiting_task(AsyncTask *task) {
  */
  */
 void AsyncFuture::
 void AsyncFuture::
 wake_task(AsyncTask *task) {
 wake_task(AsyncTask *task) {
-  cerr << "waking task\n";
   AsyncTaskManager *manager = task->_manager;
   AsyncTaskManager *manager = task->_manager;
   if (manager == nullptr) {
   if (manager == nullptr) {
     // If it's an unscheduled task, schedule it on the same manager as the
     // If it's an unscheduled task, schedule it on the same manager as the

+ 85 - 54
panda/src/pgraph/clipPlaneAttrib.cxx

@@ -900,12 +900,52 @@ write_datagram(BamWriter *manager, Datagram &dg) {
 int ClipPlaneAttrib::
 int ClipPlaneAttrib::
 complete_pointers(TypedWritable **p_list, BamReader *manager) {
 complete_pointers(TypedWritable **p_list, BamReader *manager) {
   int pi = RenderAttrib::complete_pointers(p_list, manager);
   int pi = RenderAttrib::complete_pointers(p_list, manager);
-  AttribNodeRegistry *areg = AttribNodeRegistry::get_global_ptr();
 
 
   if (manager->get_file_minor_ver() >= 40) {
   if (manager->get_file_minor_ver() >= 40) {
     for (size_t i = 0; i < _off_planes.size(); ++i) {
     for (size_t i = 0; i < _off_planes.size(); ++i) {
       pi += _off_planes[i].complete_pointers(p_list + pi, manager);
       pi += _off_planes[i].complete_pointers(p_list + pi, manager);
+    }
+
+    for (size_t i = 0; i < _on_planes.size(); ++i) {
+      pi += _on_planes[i].complete_pointers(p_list + pi, manager);
+    }
+
+  } else {
+    BamAuxData *aux = (BamAuxData *)manager->get_aux_data(this, "planes");
+    nassertr(aux != NULL, pi);
 
 
+    int i;
+    aux->_off_list.reserve(aux->_num_off_planes);
+    for (i = 0; i < aux->_num_off_planes; ++i) {
+      PandaNode *node;
+      DCAST_INTO_R(node, p_list[pi++], pi);
+      aux->_off_list.push_back(node);
+    }
+
+    aux->_on_list.reserve(aux->_num_on_planes);
+    for (i = 0; i < aux->_num_on_planes; ++i) {
+      PandaNode *node;
+      DCAST_INTO_R(node, p_list[pi++], pi);
+      aux->_on_list.push_back(node);
+    }
+  }
+
+  return pi;
+}
+
+/**
+ * Called by the BamReader to perform any final actions needed for setting up
+ * the object after all objects have been read and all pointers have been
+ * completed.
+ */
+void ClipPlaneAttrib::
+finalize(BamReader *manager) {
+  if (manager->get_file_minor_ver() >= 40) {
+    AttribNodeRegistry *areg = AttribNodeRegistry::get_global_ptr();
+
+    // Check if any of the nodes we loaded are mentioned in the
+    // AttribNodeRegistry.  If so, replace them.
+    for (size_t i = 0; i < _off_planes.size(); ++i) {
       int n = areg->find_node(_off_planes[i]);
       int n = areg->find_node(_off_planes[i]);
       if (n != -1) {
       if (n != -1) {
         // If it's in the registry, replace it.
         // If it's in the registry, replace it.
@@ -914,8 +954,6 @@ complete_pointers(TypedWritable **p_list, BamReader *manager) {
     }
     }
 
 
     for (size_t i = 0; i < _on_planes.size(); ++i) {
     for (size_t i = 0; i < _on_planes.size(); ++i) {
-      pi += _on_planes[i].complete_pointers(p_list + pi, manager);
-
       int n = areg->find_node(_on_planes[i]);
       int n = areg->find_node(_on_planes[i]);
       if (n != -1) {
       if (n != -1) {
         // If it's in the registry, replace it.
         // If it's in the registry, replace it.
@@ -924,53 +962,46 @@ complete_pointers(TypedWritable **p_list, BamReader *manager) {
     }
     }
 
 
   } else {
   } else {
-    Planes::iterator ci = _off_planes.begin();
-    while (ci != _off_planes.end()) {
-      PandaNode *node;
-      DCAST_INTO_R(node, p_list[pi++], pi);
-
-      // We go through some effort to look up the node in the registry without
-      // creating a NodePath around it first (which would up, and then down,
-      // the reference count, possibly deleting the node).
-      int ni = areg->find_node(node->get_type(), node->get_name());
-      if (ni != -1) {
-        (*ci) = areg->get_node(ni);
+    // Now it's safe to convert our saved PandaNodes into NodePaths.
+    BamAuxData *aux = (BamAuxData *)manager->get_aux_data(this, "planes");
+    nassertv(aux != NULL);
+    nassertv(aux->_num_off_planes == (int)aux->_off_list.size());
+    nassertv(aux->_num_on_planes == (int)aux->_on_list.size());
+
+    AttribNodeRegistry *areg = AttribNodeRegistry::get_global_ptr();
+
+    _off_planes.reserve(aux->_off_list.size());
+    NodeList::iterator ni;
+    for (ni = aux->_off_list.begin(); ni != aux->_off_list.end(); ++ni) {
+      PandaNode *node = (*ni);
+      int n = areg->find_node(node->get_type(), node->get_name());
+      if (n != -1) {
+        // If it's in the registry, add that NodePath.
+        _off_planes.push_back(areg->get_node(n));
       } else {
       } else {
-        (*ci) = NodePath(node);
+        // Otherwise, add any arbitrary NodePath.  Complain if it's ambiguous.
+        _off_planes.push_back(NodePath(node));
       }
       }
-      ++ci;
     }
     }
 
 
-    ci = _on_planes.begin();
-    while (ci != _on_planes.end()) {
-      PandaNode *node;
-      DCAST_INTO_R(node, p_list[pi++], pi);
-
-      int ni = areg->find_node(node->get_type(), node->get_name());
-      if (ni != -1) {
-        (*ci) = areg->get_node(ni);
+    _on_planes.reserve(aux->_on_list.size());
+    for (ni = aux->_on_list.begin(); ni != aux->_on_list.end(); ++ni) {
+      PandaNode *node = (*ni);
+      int n = areg->find_node(node->get_type(), node->get_name());
+      if (n != -1) {
+        // If it's in the registry, add that NodePath.
+        _on_planes.push_back(areg->get_node(n));
+        node = _on_planes.back().node();
       } else {
       } else {
-        (*ci) = NodePath(node);
+        // Otherwise, add any arbitrary NodePath.  Complain if it's ambiguous.
+        _on_planes.push_back(NodePath(node));
       }
       }
-      ++ci;
     }
     }
   }
   }
 
 
+  // Now that the NodePaths have been filled in, we can sort the list.
   _off_planes.sort();
   _off_planes.sort();
   _on_planes.sort();
   _on_planes.sort();
-
-  return pi;
-}
-
-/**
- * Some objects require all of their nested pointers to have been completed
- * before the objects themselves can be completed.  If this is the case,
- * override this method to return true, and be careful with circular
- * references (which would make the object unreadable from a bam file).
- */
-bool ClipPlaneAttrib::
-require_fully_complete() const {
-  return true;
 }
 }
 
 
 /**
 /**
@@ -987,6 +1018,8 @@ make_from_bam(const FactoryParams &params) {
   parse_params(params, scan, manager);
   parse_params(params, scan, manager);
   attrib->fillin(scan, manager);
   attrib->fillin(scan, manager);
 
 
+  manager->register_finalize(attrib);
+
   return attrib;
   return attrib;
 }
 }
 
 
@@ -1000,26 +1033,24 @@ fillin(DatagramIterator &scan, BamReader *manager) {
 
 
   _off_all_planes = scan.get_bool();
   _off_all_planes = scan.get_bool();
 
 
-  int num_off_planes = scan.get_uint16();
-
-  // Push back an empty NodePath for each off Plane for now, until we get the
-  // actual list of pointers later in complete_pointers().
-  _off_planes.resize(num_off_planes);
   if (manager->get_file_minor_ver() >= 40) {
   if (manager->get_file_minor_ver() >= 40) {
-    for (int i = 0; i < num_off_planes; i++) {
+    _off_planes.resize(scan.get_uint16());
+    for (size_t i = 0; i < _off_planes.size(); ++i) {
       _off_planes[i].fillin(scan, manager);
       _off_planes[i].fillin(scan, manager);
     }
     }
-  } else {
-    manager->read_pointers(scan, num_off_planes);
-  }
 
 
-  int num_on_planes = scan.get_uint16();
-  _on_planes.resize(num_on_planes);
-  if (manager->get_file_minor_ver() >= 40) {
-    for (int i = 0; i < num_on_planes; i++) {
-      manager->read_pointer(scan);
+    _on_planes.resize(scan.get_uint16());
+    for (size_t i = 0; i < _on_planes.size(); ++i) {
+      _on_planes[i].fillin(scan, manager);
     }
     }
   } else {
   } else {
-    manager->read_pointers(scan, num_on_planes);
+    BamAuxData *aux = new BamAuxData;
+    manager->set_aux_data(this, "planes", aux);
+
+    aux->_num_off_planes = scan.get_uint16();
+    manager->read_pointers(scan, aux->_num_off_planes);
+
+    aux->_num_on_planes = scan.get_uint16();
+    manager->read_pointers(scan, aux->_num_on_planes);
   }
   }
 }
 }

+ 16 - 1
panda/src/pgraph/clipPlaneAttrib.h

@@ -125,11 +125,26 @@ PUBLISHED:
   }
   }
   MAKE_PROPERTY(class_slot, get_class_slot);
   MAKE_PROPERTY(class_slot, get_class_slot);
 
 
+public:
+  // This data is only needed when reading from a bam file.
+  typedef pvector<PT(PandaNode) > NodeList;
+  class BamAuxData : public BamReader::AuxData {
+  public:
+    // We hold a pointer to each of the PandaNodes on the on_list and
+    // off_list.  We will later convert these to NodePaths in
+    // finalize().
+    int _num_off_planes;
+    int _num_on_planes;
+    NodeList _off_list;
+    NodeList _on_list;
+  };
+
 public:
 public:
   static void register_with_read_factory();
   static void register_with_read_factory();
   virtual void write_datagram(BamWriter *manager, Datagram &dg);
   virtual void write_datagram(BamWriter *manager, Datagram &dg);
   virtual int complete_pointers(TypedWritable **plist, BamReader *manager);
   virtual int complete_pointers(TypedWritable **plist, BamReader *manager);
-  virtual bool require_fully_complete() const;
+
+  virtual void finalize(BamReader *manager);
 
 
 protected:
 protected:
   static TypedWritable *make_from_bam(const FactoryParams &params);
   static TypedWritable *make_from_bam(const FactoryParams &params);

+ 23 - 23
panda/src/pgraph/cullResult.cxx

@@ -128,6 +128,29 @@ add_object(CullableObject *object, const CullTraverser *traverser) {
     object->_state = object->_state->compose(get_rescale_normal_state(mode));
     object->_state = object->_state->compose(get_rescale_normal_state(mode));
   }
   }
 
 
+  // Check for a special wireframe setting.
+  const RenderModeAttrib *rmode;
+  if (object->_state->get_attrib(rmode)) {
+    if (rmode->get_mode() == RenderModeAttrib::M_filled_wireframe) {
+      CullableObject *wireframe_part = new CullableObject(*object);
+      wireframe_part->_state = get_wireframe_overlay_state(rmode);
+
+      if (wireframe_part->munge_geom
+          (_gsg, _gsg->get_geom_munger(wireframe_part->_state, current_thread),
+           traverser, force)) {
+        int wireframe_bin_index = bin_manager->find_bin("fixed");
+        CullBin *bin = get_bin(wireframe_bin_index);
+        nassertv(bin != (CullBin *)NULL);
+        check_flash_bin(wireframe_part->_state, bin_manager, wireframe_bin_index);
+        bin->add_object(wireframe_part, current_thread);
+      } else {
+        delete wireframe_part;
+      }
+
+      object->_state = object->_state->compose(get_wireframe_filled_state());
+    }
+  }
+
   // Check to see if there's a special transparency setting.
   // Check to see if there's a special transparency setting.
   const TransparencyAttrib *trans;
   const TransparencyAttrib *trans;
   if (object->_state->get_attrib(trans)) {
   if (object->_state->get_attrib(trans)) {
@@ -216,29 +239,6 @@ add_object(CullableObject *object, const CullTraverser *traverser) {
     }
     }
   }
   }
 
 
-  // Check for a special wireframe setting.
-  const RenderModeAttrib *rmode;
-  if (object->_state->get_attrib(rmode)) {
-    if (rmode->get_mode() == RenderModeAttrib::M_filled_wireframe) {
-      CullableObject *wireframe_part = new CullableObject(*object);
-      wireframe_part->_state = get_wireframe_overlay_state(rmode);
-
-      if (wireframe_part->munge_geom
-          (_gsg, _gsg->get_geom_munger(wireframe_part->_state, current_thread),
-           traverser, force)) {
-        int wireframe_bin_index = bin_manager->find_bin("fixed");
-        CullBin *bin = get_bin(wireframe_bin_index);
-        nassertv(bin != (CullBin *)NULL);
-        check_flash_bin(wireframe_part->_state, bin_manager, wireframe_bin_index);
-        bin->add_object(wireframe_part, current_thread);
-      } else {
-        delete wireframe_part;
-      }
-
-      object->_state = object->_state->compose(get_wireframe_filled_state());
-    }
-  }
-
   int bin_index = object->_state->get_bin_index();
   int bin_index = object->_state->get_bin_index();
   CullBin *bin = get_bin(bin_index);
   CullBin *bin = get_bin(bin_index);
   nassertv(bin != (CullBin *)NULL);
   nassertv(bin != (CullBin *)NULL);

+ 1 - 1
panda/src/pgraphnodes/shaderGenerator.cxx

@@ -344,7 +344,7 @@ analyze_renderstate(ShaderKey &key, const RenderState *rs) {
 
 
     case TextureStage::M_modulate_gloss:
     case TextureStage::M_modulate_gloss:
       if (shader_attrib->auto_gloss_on()) {
       if (shader_attrib->auto_gloss_on()) {
-        info._flags = ShaderKey::TF_map_glow;
+        info._flags = ShaderKey::TF_map_gloss;
       } else {
       } else {
         info._mode = TextureStage::M_modulate;
         info._mode = TextureStage::M_modulate;
         info._flags = ShaderKey::TF_has_rgb;
         info._flags = ShaderKey::TF_has_rgb;

+ 2 - 1
panda/src/putil/bamReader.cxx

@@ -1278,7 +1278,8 @@ p_read_object() {
       } else {
       } else {
         if (bam_cat.is_spam()) {
         if (bam_cat.is_spam()) {
           bam_cat.spam()
           bam_cat.spam()
-            << "Read a " << object->get_type() << ": " << (void *)object << "\n";
+            << "Read a " << object->get_type() << ": " << (void *)object
+            << " (id=" << object_id << ")\n";
         }
         }
       }
       }
     }
     }

+ 4 - 0
panda/src/vision/config_vision.cxx

@@ -26,6 +26,10 @@
 Configure(config_vision);
 Configure(config_vision);
 NotifyCategoryDef(vision, "");
 NotifyCategoryDef(vision, "");
 
 
+ConfigVariableBool v4l_blocking
+("v4l-blocking", false,
+ PRC_DESC("Set this to true if you want to block waiting for webcam frames."));
+
 ConfigureFn(config_vision) {
 ConfigureFn(config_vision) {
   init_libvision();
   init_libvision();
 }
 }

+ 3 - 0
panda/src/vision/config_vision.h

@@ -16,9 +16,12 @@
 
 
 #include "pandabase.h"
 #include "pandabase.h"
 #include "notifyCategoryProxy.h"
 #include "notifyCategoryProxy.h"
+#include "configVariableBool.h"
 
 
 NotifyCategoryDecl(vision, EXPCL_VISION, EXPTP_VISION);
 NotifyCategoryDecl(vision, EXPCL_VISION, EXPTP_VISION);
 
 
+extern ConfigVariableBool v4l_blocking;
+
 extern EXPCL_VISION void init_libvision();
 extern EXPCL_VISION void init_libvision();
 
 
 #endif
 #endif

+ 16 - 1
panda/src/vision/webcamVideoCursorV4L.cxx

@@ -209,7 +209,13 @@ WebcamVideoCursorV4L(WebcamVideoV4L *src) : MovieVideoCursor(src) {
 
 
   _buffers = NULL;
   _buffers = NULL;
   _buflens = NULL;
   _buflens = NULL;
-  _fd = open(src->_device.c_str(), O_RDWR);
+
+  int mode = O_RDWR;
+  if (!v4l_blocking) {
+    mode = O_NONBLOCK;
+  }
+
+  _fd = open(src->_device.c_str(), mode);
   if (-1 == _fd) {
   if (-1 == _fd) {
     vision_cat.error() << "Failed to open " << src->_device.c_str() << "\n";
     vision_cat.error() << "Failed to open " << src->_device.c_str() << "\n";
     return;
     return;
@@ -247,6 +253,10 @@ WebcamVideoCursorV4L(WebcamVideoV4L *src) : MovieVideoCursor(src) {
     _num_components = 4;
     _num_components = 4;
     break;
     break;
 
 
+  case V4L2_PIX_FMT_GREY:
+    _num_components = 1;
+    break;
+
   default:
   default:
     vision_cat.error() << "Unsupported pixel format " << src->get_pixel_format() << "!\n";
     vision_cat.error() << "Unsupported pixel format " << src->get_pixel_format() << "!\n";
     _ready = false;
     _ready = false;
@@ -393,6 +403,10 @@ fetch_buffer() {
   vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
   vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
   vbuf.memory = V4L2_MEMORY_MMAP;
   vbuf.memory = V4L2_MEMORY_MMAP;
   if (-1 == ioctl(_fd, VIDIOC_DQBUF, &vbuf) && errno != EIO) {
   if (-1 == ioctl(_fd, VIDIOC_DQBUF, &vbuf) && errno != EIO) {
+    if (errno == EAGAIN) {
+      // Simply nothing is available yet.
+      return NULL;
+    }
     vision_cat.error() << "Failed to dequeue buffer!\n";
     vision_cat.error() << "Failed to dequeue buffer!\n";
     return NULL;
     return NULL;
   }
   }
@@ -484,6 +498,7 @@ fetch_buffer() {
 
 
   case V4L2_PIX_FMT_BGR24:
   case V4L2_PIX_FMT_BGR24:
   case V4L2_PIX_FMT_BGR32:
   case V4L2_PIX_FMT_BGR32:
+  case V4L2_PIX_FMT_GREY:
     // Simplest case: copying every row verbatim.
     // Simplest case: copying every row verbatim.
     nassertr(old_bpl == new_bpl, NULL);
     nassertr(old_bpl == new_bpl, NULL);
 
 

+ 1 - 0
panda/src/vision/webcamVideoV4L.cxx

@@ -174,6 +174,7 @@ void find_all_webcams_v4l() {
             case V4L2_PIX_FMT_BGR32:
             case V4L2_PIX_FMT_BGR32:
             case V4L2_PIX_FMT_RGB24:
             case V4L2_PIX_FMT_RGB24:
             case V4L2_PIX_FMT_RGB32:
             case V4L2_PIX_FMT_RGB32:
+            case V4L2_PIX_FMT_GREY:
               break;
               break;
 
 
             default:
             default:

+ 3 - 1
tests/display/conftest.py

@@ -41,7 +41,9 @@ def window(graphics_pipe, graphics_engine):
     )
     )
     graphics_engine.open_windows()
     graphics_engine.open_windows()
 
 
-    assert win is not None
+    if win is None:
+        pytest.skip("GraphicsPipe cannot make windows")
+
     yield win
     yield win
 
 
     if win is not None:
     if win is not None: