Browse Source

Merge branch 'master' into deploy-ng

Mitchell Stokes 8 years ago
parent
commit
aba26bd862
66 changed files with 2327 additions and 297 deletions
  1. 1 1
      README.md
  2. 107 46
      direct/src/showbase/Loader.py
  3. 10 0
      direct/src/task/Task.py
  4. 28 0
      dtool/src/interrogate/README.md
  5. 1 1
      dtool/src/interrogate/functionRemap.cxx
  6. 39 1
      dtool/src/interrogate/interfaceMakerPythonNative.cxx
  7. 73 0
      dtool/src/interrogatedb/py_panda.cxx
  8. 6 0
      dtool/src/interrogatedb/py_panda.h
  9. 2 0
      dtool/src/pystub/pystub.cxx
  10. 2 0
      makepanda/makepanda.py
  11. 15 2
      panda/src/audio/audioLoadRequest.I
  12. 2 0
      panda/src/audio/audioLoadRequest.h
  13. 4 3
      panda/src/audio/audioManager.h
  14. 3 7
      panda/src/audiotraits/fmodAudioManager.cxx
  15. 5 3
      panda/src/audiotraits/fmodAudioManager.h
  16. 3 0
      panda/src/audiotraits/fmodAudioSound.cxx
  17. 7 8
      panda/src/audiotraits/openalAudioManager.cxx
  18. 8 5
      panda/src/audiotraits/openalAudioManager.h
  19. 3 3
      panda/src/audiotraits/openalAudioSound.cxx
  20. 123 123
      panda/src/bullet/bulletDebugNode.cxx
  21. 15 5
      panda/src/bullet/bulletDebugNode.h
  22. 5 14
      panda/src/bullet/bulletWorld.I
  23. 14 3
      panda/src/bullet/bulletWorld.cxx
  24. 1 2
      panda/src/bullet/bulletWorld.h
  25. 5 0
      panda/src/chan/README.md
  26. 3 0
      panda/src/cull/README.md
  27. 3 0
      panda/src/dgraph/README.md
  28. 2 0
      panda/src/display/README.md
  29. 2 0
      panda/src/dxgsg9/README.md
  30. 1 0
      panda/src/event/asyncTask.I
  31. 10 0
      panda/src/event/asyncTask.h
  32. 23 1
      panda/src/event/asyncTaskChain.cxx
  33. 1 0
      panda/src/event/asyncTaskChain.h
  34. 1 0
      panda/src/event/asyncTaskManager.h
  35. 75 0
      panda/src/event/asyncTask_ext.cxx
  36. 35 0
      panda/src/event/asyncTask_ext.h
  37. 49 0
      panda/src/event/pythonTask.I
  38. 298 52
      panda/src/event/pythonTask.cxx
  39. 23 7
      panda/src/event/pythonTask.h
  40. 16 2
      panda/src/pgraph/modelFlattenRequest.I
  41. 3 1
      panda/src/pgraph/modelFlattenRequest.h
  42. 10 0
      panda/src/pgraph/modelLoadRequest.I
  43. 5 3
      panda/src/pgraph/modelLoadRequest.h
  44. 10 0
      panda/src/pgraph/modelSaveRequest.I
  45. 4 3
      panda/src/pgraph/modelSaveRequest.h
  46. 1 0
      panda/src/pgraph/shaderInput.h
  47. 4 1
      panda/src/pgraphnodes/shaderGenerator.cxx
  48. 111 0
      panda/src/testbed/pview.cxx
  49. BIN
      samples/rocket-console/assets/Perfect DOS VGA 437.ttf
  50. 38 0
      samples/rocket-console/assets/console.rcss
  51. 11 0
      samples/rocket-console/assets/console.rml
  52. 1 0
      samples/rocket-console/assets/dos437.txt
  53. 54 0
      samples/rocket-console/assets/loading.rml
  54. 18 0
      samples/rocket-console/assets/modenine.nfo
  55. BIN
      samples/rocket-console/assets/modenine.ttf
  56. BIN
      samples/rocket-console/assets/monitor.egg.pz
  57. 12 0
      samples/rocket-console/assets/monitor.txt
  58. 44 0
      samples/rocket-console/assets/rkt.rcss
  59. 316 0
      samples/rocket-console/assets/takeyga_kb.egg
  60. BIN
      samples/rocket-console/assets/tex/takeyga_kb_diffuse.dds
  61. BIN
      samples/rocket-console/assets/tex/takeyga_kb_normal.dds
  62. BIN
      samples/rocket-console/assets/tex/takeyga_kb_specular.dds
  63. 56 0
      samples/rocket-console/assets/window.rcss
  64. 42 0
      samples/rocket-console/assets/window.rml
  65. 153 0
      samples/rocket-console/console.py
  66. 410 0
      samples/rocket-console/main.py

+ 1 - 1
README.md

@@ -162,7 +162,7 @@ Reporting Issues
 If you encounter any bugs when using Panda3D, please report them in the bug
 If you encounter any bugs when using Panda3D, please report them in the bug
 tracker.  This is hosted at:
 tracker.  This is hosted at:
 
 
-  https://bugs.launchpad.net/panda3d
+  https://github.com/panda3d/panda3d/issues
 
 
 Make sure to first use the search function to see if the bug has already been
 Make sure to first use the search function to see if the bug has already been
 reported.  When filling out a bug report, make sure that you include as much
 reported.  When filling out a bug report, make sure that you include as much

+ 107 - 46
direct/src/showbase/Loader.py

@@ -21,31 +21,103 @@ class Loader(DirectObject):
     loaderIndex = 0
     loaderIndex = 0
 
 
     class Callback:
     class Callback:
-        def __init__(self, numObjects, gotList, callback, extraArgs):
+        """Returned by loadModel when used asynchronously.  This class is
+        modelled after Future, and can be awaited."""
+
+        # This indicates that this class behaves like a Future.
+        _asyncio_future_blocking = False
+
+        def __init__(self, loader, numObjects, gotList, callback, extraArgs):
+            self._loader = loader
             self.objects = [None] * numObjects
             self.objects = [None] * numObjects
             self.gotList = gotList
             self.gotList = gotList
             self.callback = callback
             self.callback = callback
             self.extraArgs = extraArgs
             self.extraArgs = extraArgs
-            self.numRemaining = numObjects
-            self.cancelled = False
             self.requests = set()
             self.requests = set()
+            self.requestList = []
 
 
         def gotObject(self, index, object):
         def gotObject(self, index, object):
             self.objects[index] = object
             self.objects[index] = object
-            self.numRemaining -= 1
 
 
-            if self.numRemaining == 0:
-                if self.gotList:
-                    self.callback(self.objects, *self.extraArgs)
-                else:
-                    self.callback(*(self.objects + self.extraArgs))
+            if not self.requests:
+                self._loader = None
+                if self.callback:
+                    if self.gotList:
+                        self.callback(self.objects, *self.extraArgs)
+                    else:
+                        self.callback(*(self.objects + self.extraArgs))
+
+        def cancel(self):
+            "Cancels the request.  Callback won't be called."
+            if self._loader:
+                self._loader = None
+                for request in self.requests:
+                    self._loader.loader.remove(request)
+                    del self._loader._requests[request]
+                self.requests = None
+                self.requestList = None
+
+        def cancelled(self):
+            "Returns true if the request was cancelled."
+            return self.requestList is None
+
+        def done(self):
+            "Returns true if all the requests were finished or cancelled."
+            return not self.requests
+
+        def result(self):
+            assert not self.requests, "Result is not ready."
+            if self.gotList:
+                return self.objects
+            else:
+                return self.objects[0]
+
+        def exception(self):
+            assert self.done() and not self.cancelled()
+            return None
+
+        def __await__(self):
+            """ Returns a generator that raises StopIteration when the loading
+            is complete.  This allows this class to be used with 'await'."""
+            if self.requests:
+                self._asyncio_future_blocking = True
+                yield self
+
+            # This should be a simple return, but older versions of Python
+            # don't allow return statements with arguments.
+            result = self.result()
+            exc = StopIteration(result)
+            exc.value = result
+            raise exc
+
+        def __aiter__(self):
+            """ This allows using `async for` to iterate asynchronously over
+            the results of this class.  It does guarantee to return the
+            results in order, though, even though they may not be loaded in
+            that order. """
+            requestList = self.requestList
+            assert requestList is not None, "Request was cancelled."
+
+            class AsyncIter:
+                index = 0
+                def __anext__(self):
+                    if self.index < len(requestList):
+                        i = self.index
+                        self.index = i + 1
+                        return requestList[i]
+                    else:
+                        raise StopAsyncIteration
+
+            iter = AsyncIter()
+            iter.objects = self.objects
+            return iter
 
 
     # special methods
     # special methods
     def __init__(self, base):
     def __init__(self, base):
         self.base = base
         self.base = base
         self.loader = PandaLoader.getGlobalPtr()
         self.loader = PandaLoader.getGlobalPtr()
 
 
-        self.__requests = {}
+        self._requests = {}
 
 
         self.hook = "async_loader_%s" % (Loader.loaderIndex)
         self.hook = "async_loader_%s" % (Loader.loaderIndex)
         Loader.loaderIndex += 1
         Loader.loaderIndex += 1
@@ -180,7 +252,7 @@ class Loader(DirectObject):
             # requested models have been loaded, we'll invoke the
             # requested models have been loaded, we'll invoke the
             # callback (passing it the models on the parameter list).
             # callback (passing it the models on the parameter list).
 
 
-            cb = Loader.Callback(len(modelList), gotList, callback, extraArgs)
+            cb = Loader.Callback(self, len(modelList), gotList, callback, extraArgs)
             i = 0
             i = 0
             for modelPath in modelList:
             for modelPath in modelList:
                 request = self.loader.makeAsyncRequest(Filename(modelPath), loaderOptions)
                 request = self.loader.makeAsyncRequest(Filename(modelPath), loaderOptions)
@@ -189,26 +261,26 @@ class Loader(DirectObject):
                 request.setDoneEvent(self.hook)
                 request.setDoneEvent(self.hook)
                 self.loader.loadAsync(request)
                 self.loader.loadAsync(request)
                 cb.requests.add(request)
                 cb.requests.add(request)
-                self.__requests[request] = (cb, i)
+                cb.requestList.append(request)
+                self._requests[request] = (cb, i)
                 i += 1
                 i += 1
             return cb
             return cb
 
 
     def cancelRequest(self, cb):
     def cancelRequest(self, cb):
         """Cancels an aysynchronous loading or flatten request issued
         """Cancels an aysynchronous loading or flatten request issued
         earlier.  The callback associated with the request will not be
         earlier.  The callback associated with the request will not be
-        called after cancelRequest() has been performed. """
+        called after cancelRequest() has been performed.
+
+        This is now deprecated: call cb.cancel() instead. """
 
 
-        if not cb.cancelled:
-            cb.cancelled = True
-            for request in cb.requests:
-                self.loader.remove(request)
-                del self.__requests[request]
-            cb.requests = None
+        cb.cancel()
 
 
     def isRequestPending(self, cb):
     def isRequestPending(self, cb):
         """ Returns true if an asynchronous loading or flatten request
         """ Returns true if an asynchronous loading or flatten request
         issued earlier is still pending, or false if it has completed or
         issued earlier is still pending, or false if it has completed or
-        been cancelled. """
+        been cancelled.
+
+        This is now deprecated: call cb.done() instead. """
 
 
         return bool(cb.requests)
         return bool(cb.requests)
 
 
@@ -344,7 +416,7 @@ class Loader(DirectObject):
             # requested models have been saved, we'll invoke the
             # requested models have been saved, we'll invoke the
             # callback (passing it the models on the parameter list).
             # callback (passing it the models on the parameter list).
 
 
-            cb = Loader.Callback(len(modelList), gotList, callback, extraArgs)
+            cb = Loader.Callback(self, len(modelList), gotList, callback, extraArgs)
             i = 0
             i = 0
             for modelPath, node in modelList:
             for modelPath, node in modelList:
                 request = self.loader.makeAsyncSaveRequest(Filename(modelPath), loaderOptions, node)
                 request = self.loader.makeAsyncSaveRequest(Filename(modelPath), loaderOptions, node)
@@ -353,7 +425,8 @@ class Loader(DirectObject):
                 request.setDoneEvent(self.hook)
                 request.setDoneEvent(self.hook)
                 self.loader.saveAsync(request)
                 self.loader.saveAsync(request)
                 cb.requests.add(request)
                 cb.requests.add(request)
-                self.__requests[request] = (cb, i)
+                cb.requestList.append(request)
+                self._requests[request] = (cb, i)
                 i += 1
                 i += 1
             return cb
             return cb
 
 
@@ -880,13 +953,14 @@ class Loader(DirectObject):
             # requested sounds have been loaded, we'll invoke the
             # requested sounds have been loaded, we'll invoke the
             # callback (passing it the sounds on the parameter list).
             # callback (passing it the sounds on the parameter list).
 
 
-            cb = Loader.Callback(len(soundList), gotList, callback, extraArgs)
+            cb = Loader.Callback(self, len(soundList), gotList, callback, extraArgs)
             for i, soundPath in enumerate(soundList):
             for i, soundPath in enumerate(soundList):
                 request = AudioLoadRequest(manager, soundPath, positional)
                 request = AudioLoadRequest(manager, soundPath, positional)
                 request.setDoneEvent(self.hook)
                 request.setDoneEvent(self.hook)
                 self.loader.loadAsync(request)
                 self.loader.loadAsync(request)
                 cb.requests.add(request)
                 cb.requests.add(request)
-                self.__requests[request] = (cb, i)
+                cb.requestList.append(request)
+                self._requests[request] = (cb, i)
             return cb
             return cb
 
 
     def unloadSfx(self, sfx):
     def unloadSfx(self, sfx):
@@ -944,14 +1018,15 @@ class Loader(DirectObject):
             callback = self.__asyncFlattenDone
             callback = self.__asyncFlattenDone
             gotList = True
             gotList = True
 
 
-        cb = Loader.Callback(len(modelList), gotList, callback, extraArgs)
+        cb = Loader.Callback(self, len(modelList), gotList, callback, extraArgs)
         i = 0
         i = 0
         for model in modelList:
         for model in modelList:
             request = ModelFlattenRequest(model.node())
             request = ModelFlattenRequest(model.node())
             request.setDoneEvent(self.hook)
             request.setDoneEvent(self.hook)
             self.loader.loadAsync(request)
             self.loader.loadAsync(request)
             cb.requests.add(request)
             cb.requests.add(request)
-            self.__requests[request] = (cb, i)
+            cb.requestList.append(request)
+            self._requests[request] = (cb, i)
             i += 1
             i += 1
         return cb
         return cb
 
 
@@ -980,36 +1055,22 @@ class Loader(DirectObject):
         of loaded objects, and call the appropriate callback when it's
         of loaded objects, and call the appropriate callback when it's
         time."""
         time."""
 
 
-        if request not in self.__requests:
+        if request not in self._requests:
             return
             return
 
 
-        cb, i = self.__requests[request]
-        if cb.cancelled:
+        cb, i = self._requests[request]
+        if cb.cancelled():
             # Shouldn't be here.
             # Shouldn't be here.
-            del self.__requests[request]
+            del self._requests[request]
             return
             return
 
 
         cb.requests.discard(request)
         cb.requests.discard(request)
         if not cb.requests:
         if not cb.requests:
-            del self.__requests[request]
-
-        object = None
-        if hasattr(request, "getModel"):
-            node = request.getModel()
-            if node is not None:
-                object = NodePath(node)
-
-        elif hasattr(request, "getSound"):
-            object = request.getSound()
-
-        elif hasattr(request, "getSuccess"):
-            object = request.getSuccess()
+            del self._requests[request]
 
 
-        cb.gotObject(i, object)
+        cb.gotObject(i, request.result() or None)
 
 
     load_model = loadModel
     load_model = loadModel
-    cancel_request = cancelRequest
-    is_request_pending = isRequestPending
     unload_model = unloadModel
     unload_model = unloadModel
     save_model = saveModel
     save_model = saveModel
     load_font = loadFont
     load_font = loadFont

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

@@ -333,6 +333,7 @@ class TaskManager:
         funcOrTask - either an existing Task object (not already added
         funcOrTask - either an existing Task object (not already added
         to the task manager), or a callable function object.  If this
         to the task manager), or a callable function object.  If this
         is a function, a new Task object will be created and returned.
         is a function, a new Task object will be created and returned.
+        You may also pass in a coroutine object.
 
 
         name - the name to assign to the Task.  Required, unless you
         name - the name to assign to the Task.  Required, unless you
         are passing in a Task object that already has a name.
         are passing in a Task object that already has a name.
@@ -385,6 +386,15 @@ class TaskManager:
             task = funcOrTask
             task = funcOrTask
         elif hasattr(funcOrTask, '__call__'):
         elif hasattr(funcOrTask, '__call__'):
             task = PythonTask(funcOrTask)
             task = PythonTask(funcOrTask)
+            if name is None:
+                name = getattr(funcOrTask, '__qualname__', None) or \
+                       getattr(funcOrTask, '__name__', None)
+        elif hasattr(funcOrTask, 'cr_await') or type(funcOrTask) == types.GeneratorType:
+            # It's a coroutine, or something emulating one.
+            task = PythonTask(funcOrTask)
+            if name is None:
+                name = getattr(funcOrTask, '__qualname__', None) or \
+                       getattr(funcOrTask, '__name__', None)
         else:
         else:
             self.notify.error(
             self.notify.error(
                 'add: Tried to add a task that was not a Task or a func')
                 'add: Tried to add a task that was not a Task or a func')

+ 28 - 0
dtool/src/interrogate/README.md

@@ -0,0 +1,28 @@
+A key advantage of Panda3D is that it provides developers with the
+ability to use both C++ and Python simultaneously. Essentially, Panda3D
+gives programmers the best of both worlds, as they are able to take
+advantage of the high performance and low-level programming found in
+C++ in addition to the flexibility, interactive scripting, and
+rapid-prototyping capabilities of Python. This feature is made possible
+due to Python’s ability to call C libraries, and ultimately make use of
+Panda3D’s Interrogate System: an automated C++ Extension Module
+generation utility similar to SWIG.  Although Python is the favored
+scripting language of Panda3D, the engine is highly extensible in this
+aspect, as any language that has a foreign function interface can make
+use of the Interrogate System.
+
+The Interrogate System works like a compiler by scanning and parsing
+C++ code for the Panda3D-specific, “PUBLISHED” keyword. This keyword
+marks the particular methods of a class that are to be exposed within a
+C++ Extension Module for that class which is eventually generated. One
+benefit of using the “PUBLISHED” keyword is that it alleviates the need
+for an interface file that provides function prototypes for the class
+methods that will be exposed within the extension module, as is the
+case with SWIG. Interrogate turns a class into a loose collection of
+Python interface wrapper functions that make up the C++ Extension
+Module.
+
+This package depends on the 'cppparser' package, which contains the
+code that parses the C++ headers, and the 'interrogatedb' package,
+which contains the intermediate representation of the interfaces that
+can be saved to a database file for FFI generation tools to consume.

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

@@ -846,7 +846,7 @@ setup_properties(const InterrogateFunction &ifunc, InterfaceMaker *interface_mak
       }
       }
 
 
     } else if (fname == "__iter__") {
     } else if (fname == "__iter__") {
-      if (_has_this && _parameters.size() == 1 &&
+      if ((int)_parameters.size() == first_param &&
           TypeManager::is_pointer(_return_type->get_new_type())) {
           TypeManager::is_pointer(_return_type->get_new_type())) {
         // It receives no parameters, and returns a pointer.
         // It receives no parameters, and returns a pointer.
         _flags |= F_iter;
         _flags |= F_iter;

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

@@ -499,6 +499,24 @@ get_slotted_function_def(Object *obj, Function *func, FunctionRemap *remap,
     }
     }
   }
   }
 
 
+  if (method_name == "__await__") {
+    def._answer_location = "am_await";
+    def._wrapper_type = WT_no_params;
+    return true;
+  }
+
+  if (method_name == "__aiter__") {
+    def._answer_location = "am_aiter";
+    def._wrapper_type = WT_no_params;
+    return true;
+  }
+
+  if (method_name == "__anext__") {
+    def._answer_location = "am_anext";
+    def._wrapper_type = WT_no_params;
+    return true;
+  }
+
   if (method_name == "operator ()") {
   if (method_name == "operator ()") {
     def._answer_location = "tp_call";
     def._answer_location = "tp_call";
     def._wrapper_type = WT_none;
     def._wrapper_type = WT_none;
@@ -2798,6 +2816,20 @@ write_module_class(ostream &out, Object *obj) {
     out << "};\n\n";
     out << "};\n\n";
   }
   }
 
 
+  bool have_async = false;
+  if (has_parent_class || slots.count("am_await") != 0 ||
+                          slots.count("am_aiter") != 0 ||
+                          slots.count("am_anext") != 0) {
+    out << "#if PY_VERSION_HEX >= 0x03050000\n";
+    out << "static PyAsyncMethods Dtool_AsyncMethods_" << ClassName << " = {\n";
+    write_function_slot(out, 2, slots, "am_await");
+    write_function_slot(out, 2, slots, "am_aiter");
+    write_function_slot(out, 2, slots, "am_anext");
+    out << "};\n";
+    out << "#endif\n\n";
+    have_async = true;
+  }
+
   // Output the actual PyTypeObject definition.
   // Output the actual PyTypeObject definition.
   out << "struct Dtool_PyTypedObject Dtool_" << ClassName << " = {\n";
   out << "struct Dtool_PyTypedObject Dtool_" << ClassName << " = {\n";
   out << "  {\n";
   out << "  {\n";
@@ -2819,7 +2851,13 @@ write_module_class(ostream &out, Object *obj) {
   write_function_slot(out, 4, slots, "tp_setattr");
   write_function_slot(out, 4, slots, "tp_setattr");
 
 
   // cmpfunc tp_compare;  (reserved in Python 3)
   // cmpfunc tp_compare;  (reserved in Python 3)
-  out << "#if PY_MAJOR_VERSION >= 3\n";
+  out << "#if PY_VERSION_HEX >= 0x03050000\n";
+  if (have_async) {
+    out << "    &Dtool_AsyncMethods_" << ClassName << ",\n";
+  } else {
+    out << "    0, // tp_as_async\n";
+  }
+  out << "#elif PY_MAJOR_VERSION >= 3\n";
   out << "    0, // tp_reserved\n";
   out << "    0, // tp_reserved\n";
   out << "#else\n";
   out << "#else\n";
   if (has_hash_compare) {
   if (has_hash_compare) {

+ 73 - 0
dtool/src/interrogatedb/py_panda.cxx

@@ -622,6 +622,10 @@ PyObject *Dtool_PyModuleInitHelper(LibraryDef *defs[], const char *modulename) {
       return Dtool_Raise_TypeError("PyType_Ready(Dtool_SeqMapWrapper)");
       return Dtool_Raise_TypeError("PyType_Ready(Dtool_SeqMapWrapper)");
     }
     }
 
 
+    if (PyType_Ready(&Dtool_GeneratorWrapper_Type) < 0) {
+      return Dtool_Raise_TypeError("PyType_Ready(Dtool_GeneratorWrapper)");
+    }
+
     if (PyType_Ready(&Dtool_StaticProperty_Type) < 0) {
     if (PyType_Ready(&Dtool_StaticProperty_Type) < 0) {
       return Dtool_Raise_TypeError("PyType_Ready(Dtool_StaticProperty_Type)");
       return Dtool_Raise_TypeError("PyType_Ready(Dtool_StaticProperty_Type)");
     }
     }
@@ -1111,6 +1115,13 @@ static int Dtool_SeqMapWrapper_setitem(PyObject *self, PyObject *key, PyObject *
   }
   }
 }
 }
 
 
+static PyObject *Dtool_GeneratorWrapper_iternext(PyObject *self) {
+  Dtool_GeneratorWrapper *wrap = (Dtool_GeneratorWrapper *)self;
+  nassertr(wrap, nullptr);
+  nassertr(wrap->_iternext_func, nullptr);
+  return wrap->_iternext_func(wrap->_base._self);
+}
+
 static PySequenceMethods Dtool_SequenceWrapper_SequenceMethods = {
 static PySequenceMethods Dtool_SequenceWrapper_SequenceMethods = {
   Dtool_SequenceWrapper_length,
   Dtool_SequenceWrapper_length,
   0, // sq_concat
   0, // sq_concat
@@ -1454,4 +1465,66 @@ PyTypeObject Dtool_SeqMapWrapper_Type = {
 #endif
 #endif
 };
 };
 
 
+/**
+ * This variant defines only a generator interface.
+ */
+PyTypeObject Dtool_GeneratorWrapper_Type = {
+  PyVarObject_HEAD_INIT(NULL, 0)
+  "generator wrapper",
+  sizeof(Dtool_GeneratorWrapper),
+  0, // tp_itemsize
+  Dtool_WrapperBase_dealloc,
+  0, // tp_print
+  0, // tp_getattr
+  0, // tp_setattr
+#if PY_MAJOR_VERSION >= 3
+  0, // tp_reserved
+#else
+  0, // tp_compare
+#endif
+  0, // tp_repr
+  0, // tp_as_number
+  0, // tp_as_sequence
+  0, // tp_as_mapping
+  0, // tp_hash
+  0, // tp_call
+  0, // tp_str
+  PyObject_GenericGetAttr,
+  PyObject_GenericSetAttr,
+  0, // tp_as_buffer
+  Py_TPFLAGS_DEFAULT | Py_TPFLAGS_CHECKTYPES,
+  0, // tp_doc
+  0, // tp_traverse
+  0, // tp_clear
+  0, // tp_richcompare
+  0, // tp_weaklistoffset
+  PyObject_SelfIter,
+  Dtool_GeneratorWrapper_iternext,
+  0, // tp_methods
+  0, // tp_members
+  0, // tp_getset
+  0, // tp_base
+  0, // tp_dict
+  0, // tp_descr_get
+  0, // tp_descr_set
+  0, // tp_dictoffset
+  0, // tp_init
+  PyType_GenericAlloc,
+  0, // tp_new
+  PyObject_Del,
+  0, // tp_is_gc
+  0, // tp_bases
+  0, // tp_mro
+  0, // tp_cache
+  0, // tp_subclasses
+  0, // tp_weaklist
+  0, // tp_del
+#if PY_VERSION_HEX >= 0x02060000
+  0, // tp_version_tag
+#endif
+#if PY_VERSION_HEX >= 0x03040000
+  0, // tp_finalize
+#endif
+};
+
 #endif  // HAVE_PYTHON
 #endif  // HAVE_PYTHON

+ 6 - 0
dtool/src/interrogatedb/py_panda.h

@@ -495,9 +495,15 @@ struct Dtool_SeqMapWrapper {
   objobjargproc _map_setitem_func;
   objobjargproc _map_setitem_func;
 };
 };
 
 
+struct Dtool_GeneratorWrapper {
+  Dtool_WrapperBase _base;
+  iternextfunc _iternext_func;
+};
+
 EXPCL_INTERROGATEDB extern PyTypeObject Dtool_SequenceWrapper_Type;
 EXPCL_INTERROGATEDB extern PyTypeObject Dtool_SequenceWrapper_Type;
 EXPCL_INTERROGATEDB extern PyTypeObject Dtool_MappingWrapper_Type;
 EXPCL_INTERROGATEDB extern PyTypeObject Dtool_MappingWrapper_Type;
 EXPCL_INTERROGATEDB extern PyTypeObject Dtool_SeqMapWrapper_Type;
 EXPCL_INTERROGATEDB extern PyTypeObject Dtool_SeqMapWrapper_Type;
+EXPCL_INTERROGATEDB extern PyTypeObject Dtool_GeneratorWrapper_Type;
 EXPCL_INTERROGATEDB extern PyTypeObject Dtool_StaticProperty_Type;
 EXPCL_INTERROGATEDB extern PyTypeObject Dtool_StaticProperty_Type;
 
 
 EXPCL_INTERROGATEDB PyObject *Dtool_NewStaticProperty(PyTypeObject *obj, const PyGetSetDef *getset);
 EXPCL_INTERROGATEDB PyObject *Dtool_NewStaticProperty(PyTypeObject *obj, const PyGetSetDef *getset);

+ 2 - 0
dtool/src/pystub/pystub.cxx

@@ -115,6 +115,7 @@ extern "C" {
   EXPCL_PYSTUB int PyObject_IsTrue(...);
   EXPCL_PYSTUB int PyObject_IsTrue(...);
   EXPCL_PYSTUB int PyObject_Repr(...);
   EXPCL_PYSTUB int PyObject_Repr(...);
   EXPCL_PYSTUB int PyObject_RichCompareBool(...);
   EXPCL_PYSTUB int PyObject_RichCompareBool(...);
+  EXPCL_PYSTUB int PyObject_SelfIter(...);
   EXPCL_PYSTUB int PyObject_SetAttrString(...);
   EXPCL_PYSTUB int PyObject_SetAttrString(...);
   EXPCL_PYSTUB int PyObject_Str(...);
   EXPCL_PYSTUB int PyObject_Str(...);
   EXPCL_PYSTUB int PyObject_Type(...);
   EXPCL_PYSTUB int PyObject_Type(...);
@@ -336,6 +337,7 @@ int PyObject_IsInstance(...) { return 0; }
 int PyObject_IsTrue(...) { return 0; }
 int PyObject_IsTrue(...) { return 0; }
 int PyObject_Repr(...) { return 0; }
 int PyObject_Repr(...) { return 0; }
 int PyObject_RichCompareBool(...) { return 0; }
 int PyObject_RichCompareBool(...) { return 0; }
+int PyObject_SelfIter(...) { return 0; }
 int PyObject_SetAttrString(...) { return 0; }
 int PyObject_SetAttrString(...) { return 0; }
 int PyObject_Str(...) { return 0; }
 int PyObject_Str(...) { return 0; }
 int PyObject_Type(...) { return 0; }
 int PyObject_Type(...) { return 0; }

+ 2 - 0
makepanda/makepanda.py

@@ -3713,6 +3713,7 @@ if (not RUNTIME):
   TargetAdd('p3event_composite2.obj', opts=OPTS, input='p3event_composite2.cxx')
   TargetAdd('p3event_composite2.obj', opts=OPTS, input='p3event_composite2.cxx')
 
 
   OPTS=['DIR:panda/src/event', 'PYTHON']
   OPTS=['DIR:panda/src/event', 'PYTHON']
+  TargetAdd('p3event_asyncTask_ext.obj', opts=OPTS, input='asyncTask_ext.cxx')
   TargetAdd('p3event_pythonTask.obj', opts=OPTS, input='pythonTask.cxx')
   TargetAdd('p3event_pythonTask.obj', opts=OPTS, input='pythonTask.cxx')
   IGATEFILES=GetDirectoryContents('panda/src/event', ["*.h", "*_composite*.cxx"])
   IGATEFILES=GetDirectoryContents('panda/src/event', ["*.h", "*_composite*.cxx"])
   TargetAdd('libp3event.in', opts=OPTS, input=IGATEFILES)
   TargetAdd('libp3event.in', opts=OPTS, input=IGATEFILES)
@@ -4306,6 +4307,7 @@ if (not RUNTIME):
   TargetAdd('core.pyd', input='p3pipeline_pythonThread.obj')
   TargetAdd('core.pyd', input='p3pipeline_pythonThread.obj')
   TargetAdd('core.pyd', input='p3putil_ext_composite.obj')
   TargetAdd('core.pyd', input='p3putil_ext_composite.obj')
   TargetAdd('core.pyd', input='p3pnmimage_pfmFile_ext.obj')
   TargetAdd('core.pyd', input='p3pnmimage_pfmFile_ext.obj')
+  TargetAdd('core.pyd', input='p3event_asyncTask_ext.obj')
   TargetAdd('core.pyd', input='p3event_pythonTask.obj')
   TargetAdd('core.pyd', input='p3event_pythonTask.obj')
   TargetAdd('core.pyd', input='p3gobj_ext_composite.obj')
   TargetAdd('core.pyd', input='p3gobj_ext_composite.obj')
   TargetAdd('core.pyd', input='p3pgraph_ext_composite.obj')
   TargetAdd('core.pyd', input='p3pgraph_ext_composite.obj')

+ 15 - 2
panda/src/audio/audioLoadRequest.I

@@ -62,11 +62,24 @@ is_ready() const {
 }
 }
 
 
 /**
 /**
- * Returns the sound that was loaded asynchronously, if any, or NULL if there
- * was an error.  It is an error to call this unless is_ready() returns true.
+ * Returns the sound that was loaded asynchronously, if any, or nullptr if
+ * there was an error.  It is an error to call this unless is_ready() returns
+ * true.
+ * @deprecated Use result() instead.
  */
  */
 INLINE AudioSound *AudioLoadRequest::
 INLINE AudioSound *AudioLoadRequest::
 get_sound() const {
 get_sound() const {
   nassertr(_is_ready, NULL);
   nassertr(_is_ready, NULL);
   return _sound;
   return _sound;
 }
 }
+
+/**
+ * Returns the sound that was loaded asynchronously, if any, or nullptr if
+ * there was an error.  It is an error to call this unless is_ready() returns
+ * true.
+ */
+INLINE AudioSound *AudioLoadRequest::
+result() const {
+  nassertr(_is_ready, nullptr);
+  return _sound;
+}

+ 2 - 0
panda/src/audio/audioLoadRequest.h

@@ -42,6 +42,8 @@ PUBLISHED:
   INLINE bool is_ready() const;
   INLINE bool is_ready() const;
   INLINE AudioSound *get_sound() const;
   INLINE AudioSound *get_sound() const;
 
 
+  INLINE AudioSound *result() const;
+
 protected:
 protected:
   virtual DoneStatus do_task();
   virtual DoneStatus do_task();
 
 

+ 4 - 3
panda/src/audio/audioManager.h

@@ -154,9 +154,10 @@ PUBLISHED:
                                                 PN_stdfloat *ux, PN_stdfloat *uy, PN_stdfloat *uz);
                                                 PN_stdfloat *ux, PN_stdfloat *uy, PN_stdfloat *uz);
 
 
   // Control the "relative scale that sets the distance factor" units for 3D
   // Control the "relative scale that sets the distance factor" units for 3D
-  // spacialized audio.  Default is 1.0 Fmod uses meters internally, so give a
-  // float in Units-per meter Don't know what Miles uses.  Default is 1.0
-  // which is adjust in panda to be feet.
+  // spacialized audio. This is a float in units-per-meter. Default value is
+  // 1.0, which means that Panda units are understood as meters; for e.g.
+  // feet, set 3.28. This factor is applied only to Fmod and OpenAL at the
+  // moment.
   virtual void audio_3d_set_distance_factor(PN_stdfloat factor);
   virtual void audio_3d_set_distance_factor(PN_stdfloat factor);
   virtual PN_stdfloat audio_3d_get_distance_factor() const;
   virtual PN_stdfloat audio_3d_get_distance_factor() const;
 
 

+ 3 - 7
panda/src/audiotraits/fmodAudioManager.cxx

@@ -42,14 +42,8 @@ pset<FmodAudioManager *> FmodAudioManager::_all_managers;
 
 
 bool FmodAudioManager::_system_is_valid = false;
 bool FmodAudioManager::_system_is_valid = false;
 
 
-
-// This sets the distance factor for 3D audio to use feet.  FMOD uses meters
-// by default.  Since Panda use feet we need to compensate for that with a
-// factor of 3.28 This can be overwritten.  You just need to call
-// audio_3d_set_distance_factor(PN_stdfloat factor) and set your new factor.
-
 PN_stdfloat FmodAudioManager::_doppler_factor = 1;
 PN_stdfloat FmodAudioManager::_doppler_factor = 1;
-PN_stdfloat FmodAudioManager::_distance_factor = 3.28;
+PN_stdfloat FmodAudioManager::_distance_factor = 1;
 PN_stdfloat FmodAudioManager::_drop_off_factor = 1;
 PN_stdfloat FmodAudioManager::_drop_off_factor = 1;
 
 
 
 
@@ -100,6 +94,8 @@ FmodAudioManager() {
   _up.y = 0;
   _up.y = 0;
   _up.z = 0;
   _up.z = 0;
 
 
+  _active = true;
+
   _saved_outputtype = FMOD_OUTPUTTYPE_AUTODETECT;
   _saved_outputtype = FMOD_OUTPUTTYPE_AUTODETECT;
 
 
   if (_system == (FMOD::System *)NULL) {
   if (_system == (FMOD::System *)NULL) {

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

@@ -123,9 +123,11 @@ public:
                                                 PN_stdfloat *fx, PN_stdfloat *fy, PN_stdfloat *fz,
                                                 PN_stdfloat *fx, PN_stdfloat *fy, PN_stdfloat *fz,
                                                 PN_stdfloat *ux, PN_stdfloat *uy, PN_stdfloat *uz);
                                                 PN_stdfloat *ux, PN_stdfloat *uy, PN_stdfloat *uz);
 
 
-  // Control the "relative distance factor" for 3D spacialized audio.  Default
-  // is 1.0 Fmod uses meters internally, so give a float in Units-per meter
-  // Don't know what Miles uses.
+  // Control the "relative scale that sets the distance factor" units for 3D
+  // spacialized audio. This is a float in units-per-meter. Default value is
+  // 1.0, which means that Panda units are understood as meters; for e.g.
+  // feet, set 3.28. This factor is applied only to Fmod and OpenAL at the
+  // moment.
   virtual void audio_3d_set_distance_factor(PN_stdfloat factor);
   virtual void audio_3d_set_distance_factor(PN_stdfloat factor);
   virtual PN_stdfloat audio_3d_get_distance_factor() const;
   virtual PN_stdfloat audio_3d_get_distance_factor() const;
 
 

+ 3 - 0
panda/src/audiotraits/fmodAudioSound.cxx

@@ -55,6 +55,9 @@ FmodAudioSound(AudioManager *manager, Filename file_name, bool positional) {
   _velocity.y = 0;
   _velocity.y = 0;
   _velocity.z = 0;
   _velocity.z = 0;
 
 
+  _min_dist = 1.0;
+  _max_dist = 1000000000.0;
+
   // Play Rate Variable
   // Play Rate Variable
   _playrate = 1;
   _playrate = 1;
 
 

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

@@ -97,7 +97,7 @@ OpenALAudioManager() {
   _is_valid = true;
   _is_valid = true;
 
 
   // Init 3D attributes
   // Init 3D attributes
-  _distance_factor = 3.28;
+  _distance_factor = 1;
   _drop_off_factor = 1;
   _drop_off_factor = 1;
 
 
   _position[0] = 0;
   _position[0] = 0;
@@ -715,12 +715,11 @@ audio_3d_get_listener_attributes(PN_stdfloat *px, PN_stdfloat *py, PN_stdfloat *
   *uz = _forward_up[4];
   *uz = _forward_up[4];
 }
 }
 
 
-
 /**
 /**
- * Set units per foot WARNING: OpenAL has no distance factor but we use this
- * as a scale on the min/max distances of sounds to preserve FMOD
- * compatibility.  Also, adjusts the speed of sound to compensate for unit
- * difference.  OpenAL's default speed of sound is 343.3 m/s == 1126.3 ft/s
+ * Set value in units per meter
+ * WARNING: OpenAL has no distance factor but we use this as a scale
+ *          on the min/max distances of sounds to preserve FMOD compatibility.
+ *          Also adjusts the speed of sound to compensate for unit difference.
  */
  */
 void OpenALAudioManager::
 void OpenALAudioManager::
 audio_3d_set_distance_factor(PN_stdfloat factor) {
 audio_3d_set_distance_factor(PN_stdfloat factor) {
@@ -732,7 +731,7 @@ audio_3d_set_distance_factor(PN_stdfloat factor) {
   alGetError(); // clear errors
   alGetError(); // clear errors
 
 
   if (_distance_factor>0) {
   if (_distance_factor>0) {
-    alSpeedOfSound(1126.3*_distance_factor);
+    alSpeedOfSound(343.3*_distance_factor);
     al_audio_errcheck("alSpeedOfSound()");
     al_audio_errcheck("alSpeedOfSound()");
     // resets the doppler factor to the correct setting in case it was set to
     // resets the doppler factor to the correct setting in case it was set to
     // 0.0 by a distance_factor<=0.0
     // 0.0 by a distance_factor<=0.0
@@ -752,7 +751,7 @@ audio_3d_set_distance_factor(PN_stdfloat factor) {
 }
 }
 
 
 /**
 /**
- * Sets units per foot
+ * Get value in units per meter
  */
  */
 PN_stdfloat OpenALAudioManager::
 PN_stdfloat OpenALAudioManager::
 audio_3d_get_distance_factor() const {
 audio_3d_get_distance_factor() const {

+ 8 - 5
panda/src/audiotraits/openalAudioManager.h

@@ -84,11 +84,14 @@ class EXPCL_OPENAL_AUDIO OpenALAudioManager : public AudioManager {
                                                 PN_stdfloat *fx, PN_stdfloat *fy, PN_stdfloat *fz,
                                                 PN_stdfloat *fx, PN_stdfloat *fy, PN_stdfloat *fz,
                                                 PN_stdfloat *ux, PN_stdfloat *uy, PN_stdfloat *uz);
                                                 PN_stdfloat *ux, PN_stdfloat *uy, PN_stdfloat *uz);
 
 
-  // Control the "relative distance factor" for 3D spacialized audio in units-
-  // per-foot.  Default is 1.0 OpenAL has no distance factor but we use this
-  // as a scale on the minmax distances of sounds to preserve FMOD
-  // compatibility.  Also, adjusts the speed of sound to compensate for unit
-  // difference.
+
+  // Control the "relative scale that sets the distance factor" units for 3D
+  // spacialized audio. This is a float in units-per-meter. Default value is
+  // 1.0, which means that Panda units are understood as meters; for e.g.
+  // feet, set 3.28. This factor is applied only to Fmod and OpenAL at the
+  // moment.
+  // OpenAL in fact has no distance factor like Fmod, but works with the speed
+  // of sound instead, so we use this factor to scale the speed of sound.
   virtual void audio_3d_set_distance_factor(PN_stdfloat factor);
   virtual void audio_3d_set_distance_factor(PN_stdfloat factor);
   virtual PN_stdfloat audio_3d_get_distance_factor() const;
   virtual PN_stdfloat audio_3d_get_distance_factor() const;
 
 

+ 3 - 3
panda/src/audiotraits/openalAudioSound.cxx

@@ -48,7 +48,7 @@ OpenALAudioSound(OpenALAudioManager* manager,
   _balance(0),
   _balance(0),
   _play_rate(1.0),
   _play_rate(1.0),
   _positional(positional),
   _positional(positional),
-  _min_dist(3.28f),
+  _min_dist(1.0f),
   _max_dist(1000000000.0f),
   _max_dist(1000000000.0f),
   _drop_off_factor(1.0f),
   _drop_off_factor(1.0f),
   _length(0.0),
   _length(0.0),
@@ -673,7 +673,7 @@ set_3d_min_distance(PN_stdfloat dist) {
     _manager->make_current();
     _manager->make_current();
 
 
     alGetError(); // clear errors
     alGetError(); // clear errors
-    alSourcef(_source,AL_REFERENCE_DISTANCE,_min_dist*_manager->audio_3d_get_distance_factor());
+    alSourcef(_source,AL_REFERENCE_DISTANCE,_min_dist);
     al_audio_errcheck("alSourcefv(_source,AL_REFERENCE_DISTANCE)");
     al_audio_errcheck("alSourcefv(_source,AL_REFERENCE_DISTANCE)");
   }
   }
 }
 }
@@ -698,7 +698,7 @@ set_3d_max_distance(PN_stdfloat dist) {
     _manager->make_current();
     _manager->make_current();
 
 
     alGetError(); // clear errors
     alGetError(); // clear errors
-    alSourcef(_source,AL_MAX_DISTANCE,_max_dist*_manager->audio_3d_get_distance_factor());
+    alSourcef(_source,AL_MAX_DISTANCE,_max_dist);
     al_audio_errcheck("alSourcefv(_source,AL_MAX_DISTANCE)");
     al_audio_errcheck("alSourcefv(_source,AL_MAX_DISTANCE)");
   }
   }
 }
 }

+ 123 - 123
panda/src/bullet/bulletDebugNode.cxx

@@ -13,20 +13,25 @@
 
 
 #include "bulletDebugNode.h"
 #include "bulletDebugNode.h"
 
 
+#include "cullHandler.h"
+#include "cullTraverser.h"
+#include "cullableObject.h"
 #include "geomLines.h"
 #include "geomLines.h"
 #include "geomVertexData.h"
 #include "geomVertexData.h"
 #include "geomTriangles.h"
 #include "geomTriangles.h"
 #include "geomVertexFormat.h"
 #include "geomVertexFormat.h"
 #include "geomVertexWriter.h"
 #include "geomVertexWriter.h"
 #include "omniBoundingVolume.h"
 #include "omniBoundingVolume.h"
+#include "pStatTimer.h"
 
 
 TypeHandle BulletDebugNode::_type_handle;
 TypeHandle BulletDebugNode::_type_handle;
+PStatCollector BulletDebugNode::_pstat_debug("App:Bullet:DoPhysics:Debug");
 
 
 /**
 /**
  *
  *
  */
  */
 BulletDebugNode::
 BulletDebugNode::
-BulletDebugNode(const char *name) : GeomNode(name) {
+BulletDebugNode(const char *name) : PandaNode(name), _debug_stale(true) {
 
 
   _wireframe = true;
   _wireframe = true;
   _constraints = true;
   _constraints = true;
@@ -37,40 +42,6 @@ BulletDebugNode(const char *name) : GeomNode(name) {
   set_bounds(bounds);
   set_bounds(bounds);
   set_final(true);
   set_final(true);
   set_overall_hidden(true);
   set_overall_hidden(true);
-
-  // Lines
-  {
-    PT(GeomVertexData) vdata;
-    PT(Geom) geom;
-    PT(GeomLines) prim;
-
-    vdata = new GeomVertexData("", GeomVertexFormat::get_v3c4(), Geom::UH_stream);
-
-    prim = new GeomLines(Geom::UH_stream);
-    prim->set_shade_model(Geom::SM_uniform);
-
-    geom = new Geom(vdata);
-    geom->add_primitive(prim);
-
-    add_geom(geom);
-  }
-
-  // Triangles
-  {
-    PT(GeomVertexData) vdata;
-    PT(Geom) geom;
-    PT(GeomTriangles) prim;
-
-    vdata = new GeomVertexData("", GeomVertexFormat::get_v3c4(), Geom::UH_stream);
-
-    prim = new GeomTriangles(Geom::UH_stream);
-    prim->set_shade_model(Geom::SM_uniform);
-
-    geom = new Geom(vdata);
-    geom->add_primitive(prim);
-
-    add_geom(geom);
-  }
 }
 }
 
 
 /**
 /**
@@ -175,100 +146,133 @@ draw_mask_changed() {
 }
 }
 
 
 /**
 /**
- *
+ * Returns true if there is some value to visiting this particular node during
+ * the cull traversal for any camera, false otherwise.  This will be used to
+ * optimize the result of get_net_draw_show_mask(), so that any subtrees that
+ * contain only nodes for which is_renderable() is false need not be visited.
  */
  */
-void BulletDebugNode::
-sync_b2p(btDynamicsWorld *world) {
-
-  if (is_overall_hidden()) return;
-
-  nassertv(get_num_geoms() == 2);
-
-  // Collect debug geometry data
-  _drawer._lines.clear();
-  _drawer._triangles.clear();
-
-  world->debugDrawWorld();
+bool BulletDebugNode::
+is_renderable() const {
+  return true;
+}
 
 
-  // Get inverse of this node's net transform
-  NodePath np = NodePath::any_path((PandaNode *)this);
-  LMatrix4 m = np.get_net_transform()->get_mat();
-  m.invert_in_place();
+/**
+ * Adds the node's contents to the CullResult we are building up during the
+ * cull traversal, so that it will be drawn at render time.  For most nodes
+ * other than GeomNodes, this is a do-nothing operation.
+ */
+void BulletDebugNode::
+add_for_draw(CullTraverser *trav, CullTraverserData &data) {
+  PT(Geom) debug_lines;
+  PT(Geom) debug_triangles;
 
 
-  // Render lines
   {
   {
-    PT(GeomVertexData) vdata;
-    PT(Geom) geom;
-    PT(GeomLines) prim;
-
-    vdata = new GeomVertexData("", GeomVertexFormat::get_v3c4(), Geom::UH_stream);
-
-    prim = new GeomLines(Geom::UH_stream);
-    prim->set_shade_model(Geom::SM_uniform);
-
-    GeomVertexWriter vwriter = GeomVertexWriter(vdata, InternalName::get_vertex());
-    GeomVertexWriter cwriter = GeomVertexWriter(vdata, InternalName::get_color());
-
-    int v = 0;
-
-    pvector<Line>::const_iterator lit;
-    for (lit = _drawer._lines.begin(); lit != _drawer._lines.end(); lit++) {
-      Line line = *lit;
-
-      vwriter.add_data3(m.xform_point(line._p0));
-      vwriter.add_data3(m.xform_point(line._p1));
-      cwriter.add_data4(LVecBase4(line._color));
-      cwriter.add_data4(LVecBase4(line._color));
-
-      prim->add_vertex(v++);
-      prim->add_vertex(v++);
-      prim->close_primitive();
+    LightMutexHolder holder(_lock);
+    if (_debug_world == nullptr) {
+      return;
+    }
+    if (_debug_stale) {
+      nassertv(_debug_world != nullptr);
+      PStatTimer timer(_pstat_debug);
+
+      // Collect debug geometry data
+      _drawer._lines.clear();
+      _drawer._triangles.clear();
+
+      _debug_world->debugDrawWorld();
+
+      // Render lines
+      {
+        PT(GeomVertexData) vdata =
+          new GeomVertexData("", GeomVertexFormat::get_v3c4(), Geom::UH_stream);
+        vdata->unclean_set_num_rows(_drawer._lines.size() * 2);
+
+        GeomVertexWriter vwriter(vdata, InternalName::get_vertex());
+        GeomVertexWriter cwriter(vdata, InternalName::get_color());
+
+        pvector<Line>::const_iterator lit;
+        for (lit = _drawer._lines.begin(); lit != _drawer._lines.end(); lit++) {
+          const Line &line = *lit;
+
+          vwriter.set_data3(line._p0);
+          vwriter.set_data3(line._p1);
+          cwriter.set_data4(LVecBase4(line._color));
+          cwriter.set_data4(LVecBase4(line._color));
+        }
+
+        PT(GeomPrimitive) prim = new GeomLines(Geom::UH_stream);
+        prim->set_shade_model(Geom::SM_uniform);
+        prim->add_next_vertices(_drawer._lines.size() * 2);
+
+        debug_lines = new Geom(vdata);
+        debug_lines->add_primitive(prim);
+        _debug_lines = debug_lines;
+      }
+
+      // Render triangles
+      {
+        PT(GeomVertexData) vdata =
+          new GeomVertexData("", GeomVertexFormat::get_v3c4(), Geom::UH_stream);
+        vdata->unclean_set_num_rows(_drawer._triangles.size() * 3);
+
+        GeomVertexWriter vwriter(vdata, InternalName::get_vertex());
+        GeomVertexWriter cwriter(vdata, InternalName::get_color());
+
+        pvector<Triangle>::const_iterator tit;
+        for (tit = _drawer._triangles.begin(); tit != _drawer._triangles.end(); tit++) {
+          const Triangle &tri = *tit;
+
+          vwriter.set_data3(tri._p0);
+          vwriter.set_data3(tri._p1);
+          vwriter.set_data3(tri._p2);
+          cwriter.set_data4(LVecBase4(tri._color));
+          cwriter.set_data4(LVecBase4(tri._color));
+          cwriter.set_data4(LVecBase4(tri._color));
+        }
+
+        PT(GeomPrimitive) prim = new GeomTriangles(Geom::UH_stream);
+        prim->set_shade_model(Geom::SM_uniform);
+        prim->add_next_vertices(_drawer._triangles.size() * 3);
+
+        debug_triangles = new Geom(vdata);
+        debug_triangles->add_primitive(prim);
+        _debug_triangles = debug_triangles;
+      }
+
+      // Clear collected data.
+      _drawer._lines.clear();
+      _drawer._triangles.clear();
+
+      _debug_stale = false;
+    } else {
+      debug_lines = _debug_lines;
+      debug_triangles = _debug_triangles;
     }
     }
-
-    geom = new Geom(vdata);
-    geom->add_primitive(prim);
-
-    set_geom(0, geom);
   }
   }
 
 
-  // Render triangles
+  // Record them without any state or transform.
+  trav->_geoms_pcollector.add_level(2);
   {
   {
-    PT(GeomVertexData) vdata;
-    PT(Geom) geom;
-    PT(GeomTriangles) prim;
-
-    vdata = new GeomVertexData("", GeomVertexFormat::get_v3c4(), Geom::UH_stream);
-
-    prim = new GeomTriangles(Geom::UH_stream);
-    prim->set_shade_model(Geom::SM_uniform);
-
-    GeomVertexWriter vwriter = GeomVertexWriter(vdata, InternalName::get_vertex());
-    GeomVertexWriter cwriter = GeomVertexWriter(vdata, InternalName::get_color());
-
-    int v = 0;
-
-    pvector<Triangle>::const_iterator tit;
-    for (tit = _drawer._triangles.begin(); tit != _drawer._triangles.end(); tit++) {
-      Triangle tri = *tit;
-
-      vwriter.add_data3(m.xform_point(tri._p0));
-      vwriter.add_data3(m.xform_point(tri._p1));
-      vwriter.add_data3(m.xform_point(tri._p2));
-      cwriter.add_data4(LVecBase4(tri._color));
-      cwriter.add_data4(LVecBase4(tri._color));
-      cwriter.add_data4(LVecBase4(tri._color));
-
-      prim->add_vertex(v++);
-      prim->add_vertex(v++);
-      prim->add_vertex(v++);
-      prim->close_primitive();
-    }
+    CullableObject *object =
+      new CullableObject(move(debug_lines), RenderState::make_empty(), trav->get_scene()->get_cs_world_transform());
+    trav->get_cull_handler()->record_object(object, trav);
+  }
+  {
+    CullableObject *object =
+      new CullableObject(move(debug_triangles), RenderState::make_empty(), trav->get_scene()->get_cs_world_transform());
+    trav->get_cull_handler()->record_object(object, trav);
+  }
+}
 
 
-    geom = new Geom(vdata);
-    geom->add_primitive(prim);
+/**
+ *
+ */
+void BulletDebugNode::
+sync_b2p(btDynamicsWorld *world) {
+  LightMutexHolder holder(_lock);
 
 
-    set_geom(1, geom);
-  }
+  _debug_world = world;
+  _debug_stale = true;
 }
 }
 
 
 /**
 /**
@@ -431,8 +435,6 @@ register_with_read_factory() {
  */
  */
 void BulletDebugNode::
 void BulletDebugNode::
 write_datagram(BamWriter *manager, Datagram &dg) {
 write_datagram(BamWriter *manager, Datagram &dg) {
-  // Don't upcall to GeomNode since we're not interested in storing the actual
-  // debug Geoms in the .bam file.
   PandaNode::write_datagram(manager, dg);
   PandaNode::write_datagram(manager, dg);
 
 
   dg.add_bool(_wireframe);
   dg.add_bool(_wireframe);
@@ -464,8 +466,6 @@ make_from_bam(const FactoryParams &params) {
  */
  */
 void BulletDebugNode::
 void BulletDebugNode::
 fillin(DatagramIterator &scan, BamReader *manager) {
 fillin(DatagramIterator &scan, BamReader *manager) {
-  // Don't upcall to GeomNode since we're not interested in storing the actual
-  // debug Geoms in the .bam file.
   PandaNode::fillin(scan, manager);
   PandaNode::fillin(scan, manager);
 
 
   _wireframe = scan.get_bool();
   _wireframe = scan.get_bool();

+ 15 - 5
panda/src/bullet/bulletDebugNode.h

@@ -17,13 +17,12 @@
 #include "pandabase.h"
 #include "pandabase.h"
 
 
 #include "bullet_includes.h"
 #include "bullet_includes.h"
-
-#include "geomNode.h"
+#include "lightMutex.h"
 
 
 /**
 /**
  *
  *
  */
  */
-class EXPCL_PANDABULLET BulletDebugNode : public GeomNode {
+class EXPCL_PANDABULLET BulletDebugNode : public PandaNode {
 
 
 PUBLISHED:
 PUBLISHED:
   BulletDebugNode(const char *name="debug");
   BulletDebugNode(const char *name="debug");
@@ -53,6 +52,9 @@ public:
   virtual bool safe_to_combine_children() const;
   virtual bool safe_to_combine_children() const;
   virtual bool safe_to_flatten_below() const;
   virtual bool safe_to_flatten_below() const;
 
 
+  virtual bool is_renderable() const;
+  virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);
+
 private:
 private:
   void sync_b2p(btDynamicsWorld *world);
   void sync_b2p(btDynamicsWorld *world);
 
 
@@ -100,14 +102,22 @@ private:
     int _mode;
     int _mode;
   };
   };
 
 
+  LightMutex _lock;
   DebugDraw _drawer;
   DebugDraw _drawer;
 
 
+  bool _debug_stale;
+  btDynamicsWorld *_debug_world;
+  PT(Geom) _debug_lines;
+  PT(Geom) _debug_triangles;
+
   bool _wireframe;
   bool _wireframe;
   bool _constraints;
   bool _constraints;
   bool _bounds;
   bool _bounds;
 
 
   friend class BulletWorld;
   friend class BulletWorld;
 
 
+  static PStatCollector _pstat_debug;
+
 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);
@@ -121,9 +131,9 @@ public:
     return _type_handle;
     return _type_handle;
   }
   }
   static void init_type() {
   static void init_type() {
-    GeomNode::init_type();
+    PandaNode::init_type();
     register_type(_type_handle, "BulletDebugNode",
     register_type(_type_handle, "BulletDebugNode",
-                  GeomNode::get_class_type());
+                  PandaNode::get_class_type());
   }
   }
   virtual TypeHandle get_type() const {
   virtual TypeHandle get_type() const {
     return get_class_type();
     return get_class_type();

+ 5 - 14
panda/src/bullet/bulletWorld.I

@@ -55,21 +55,12 @@ INLINE BulletWorld::
  */
  */
 INLINE void BulletWorld::
 INLINE void BulletWorld::
 set_debug_node(BulletDebugNode *node) {
 set_debug_node(BulletDebugNode *node) {
-
   nassertv(node);
   nassertv(node);
-
-  _debug = node;
-  _world->setDebugDrawer(&(_debug->_drawer));
-}
-
-/**
- *
- */
-INLINE void BulletWorld::
-clear_debug_node() {
-
-  _debug = NULL;
-  _world->setDebugDrawer(NULL);
+  if (node != _debug) {
+    clear_debug_node();
+    _debug = node;
+    _world->setDebugDrawer(&(_debug->_drawer));
+  }
 }
 }
 
 
 /**
 /**

+ 14 - 3
panda/src/bullet/bulletWorld.cxx

@@ -17,6 +17,7 @@
 #include "bulletSoftBodyWorldInfo.h"
 #include "bulletSoftBodyWorldInfo.h"
 
 
 #include "collideMask.h"
 #include "collideMask.h"
+#include "lightMutexHolder.h"
 
 
 #define clamp(x, x_min, x_max) max(min(x, x_max), x_min)
 #define clamp(x, x_min, x_max) max(min(x, x_max), x_min)
 
 
@@ -24,7 +25,6 @@ TypeHandle BulletWorld::_type_handle;
 
 
 PStatCollector BulletWorld::_pstat_physics("App:Bullet:DoPhysics");
 PStatCollector BulletWorld::_pstat_physics("App:Bullet:DoPhysics");
 PStatCollector BulletWorld::_pstat_simulation("App:Bullet:DoPhysics:Simulation");
 PStatCollector BulletWorld::_pstat_simulation("App:Bullet:DoPhysics:Simulation");
-PStatCollector BulletWorld::_pstat_debug("App:Bullet:DoPhysics:Debug");
 PStatCollector BulletWorld::_pstat_p2b("App:Bullet:DoPhysics:SyncP2B");
 PStatCollector BulletWorld::_pstat_p2b("App:Bullet:DoPhysics:SyncP2B");
 PStatCollector BulletWorld::_pstat_b2p("App:Bullet:DoPhysics:SyncB2P");
 PStatCollector BulletWorld::_pstat_b2p("App:Bullet:DoPhysics:SyncB2P");
 
 
@@ -127,6 +127,19 @@ get_world_info() {
   return BulletSoftBodyWorldInfo(_info);
   return BulletSoftBodyWorldInfo(_info);
 }
 }
 
 
+/**
+ * Removes a debug node that has been assigned to this BulletWorld.
+ */
+void BulletWorld::
+clear_debug_node() {
+  if (_debug != nullptr) {
+    LightMutexHolder holder(_debug->_lock);
+    _debug->_debug_world = nullptr;
+    _world->setDebugDrawer(nullptr);
+    _debug = nullptr;
+  }
+}
+
 /**
 /**
  *
  *
  */
  */
@@ -184,9 +197,7 @@ do_physics(PN_stdfloat dt, int max_substeps, PN_stdfloat stepsize) {
 
 
   // Render debug
   // Render debug
   if (_debug) {
   if (_debug) {
-    _pstat_debug.start();
     _debug->sync_b2p(_world);
     _debug->sync_b2p(_world);
-    _pstat_debug.stop();
   }
   }
 
 
   _pstat_physics.stop();
   _pstat_physics.stop();

+ 1 - 2
panda/src/bullet/bulletWorld.h

@@ -63,7 +63,7 @@ PUBLISHED:
 
 
   // Debug
   // Debug
   INLINE void set_debug_node(BulletDebugNode *node);
   INLINE void set_debug_node(BulletDebugNode *node);
-  INLINE void clear_debug_node();
+  void clear_debug_node();
   INLINE BulletDebugNode *get_debug_node() const;
   INLINE BulletDebugNode *get_debug_node() const;
   INLINE bool has_debug_node() const;
   INLINE bool has_debug_node() const;
 
 
@@ -208,7 +208,6 @@ private:
 
 
   static PStatCollector _pstat_physics;
   static PStatCollector _pstat_physics;
   static PStatCollector _pstat_simulation;
   static PStatCollector _pstat_simulation;
-  static PStatCollector _pstat_debug;
   static PStatCollector _pstat_p2b;
   static PStatCollector _pstat_p2b;
   static PStatCollector _pstat_b2p;
   static PStatCollector _pstat_b2p;
 
 

+ 5 - 0
panda/src/chan/README.md

@@ -0,0 +1,5 @@
+This package contains the animation channels.  This defines the various
+kinds of AnimChannels that may be defined, as well as the MovingPart
+class which binds to the channels and plays the animation.  This is a
+support library for char, as well as any other libraries that want to
+define objects whose values change over time.

+ 3 - 0
panda/src/cull/README.md

@@ -0,0 +1,3 @@
+This package contains the Cull Traverser.  The cull traversal collects
+all state changes specified, and removes unnecessary state change
+requests.  Also does all the depth sorting for proper alphaing.

+ 3 - 0
panda/src/dgraph/README.md

@@ -0,0 +1,3 @@
+This package defines and manages the data graph, which is the hierarchy
+of devices, tforms, and any other things which might have an input or
+an output and need to execute every frame.

+ 2 - 0
panda/src/display/README.md

@@ -0,0 +1,2 @@
+This package contains the abstract display classes, including pipes,
+windows, channels, and display regions.

+ 2 - 0
panda/src/dxgsg9/README.md

@@ -0,0 +1,2 @@
+This package handles all communication with the DirectX backend, and
+manages state to minimize redundant state changes.

+ 1 - 0
panda/src/event/asyncTask.I

@@ -33,6 +33,7 @@ is_alive() const {
   case S_servicing:
   case S_servicing:
   case S_sleeping:
   case S_sleeping:
   case S_active_nested:
   case S_active_nested:
+  case S_awaiting:
     return true;
     return true;
 
 
   case S_inactive:
   case S_inactive:

+ 10 - 0
panda/src/event/asyncTask.h

@@ -45,6 +45,7 @@ PUBLISHED:
     DS_exit,      // stop the enclosing sequence
     DS_exit,      // stop the enclosing sequence
     DS_pause,     // pause, then exit (useful within a sequence)
     DS_pause,     // pause, then exit (useful within a sequence)
     DS_interrupt, // interrupt the task manager, but run task again
     DS_interrupt, // interrupt the task manager, but run task again
+    DS_await,     // await a different task's completion
   };
   };
 
 
   enum State {
   enum State {
@@ -54,6 +55,7 @@ PUBLISHED:
     S_servicing_removed,  // Still servicing, but wants removal from manager.
     S_servicing_removed,  // Still servicing, but wants removal from manager.
     S_sleeping,
     S_sleeping,
     S_active_nested,      // active within a sequence.
     S_active_nested,      // active within a sequence.
+    S_awaiting,           // Waiting for a dependent task to complete
   };
   };
 
 
   INLINE State get_state() const;
   INLINE State get_state() const;
@@ -98,6 +100,9 @@ PUBLISHED:
 
 
   virtual void output(ostream &out) const;
   virtual void output(ostream &out) const;
 
 
+  EXTENSION(static PyObject *__await__(PyObject *self));
+  EXTENSION(static PyObject *__iter__(PyObject *self));
+
 protected:
 protected:
   void jump_to_task_chain(AsyncTaskManager *manager);
   void jump_to_task_chain(AsyncTaskManager *manager);
   DoneStatus unlock_and_do_task();
   DoneStatus unlock_and_do_task();
@@ -130,11 +135,16 @@ protected:
   double _total_dt;
   double _total_dt;
   int _num_frames;
   int _num_frames;
 
 
+  // Tasks waiting for this one to complete.
+  pvector<PT(AsyncTask)> _waiting_tasks;
+
   static AtomicAdjust::Integer _next_task_id;
   static AtomicAdjust::Integer _next_task_id;
 
 
   static PStatCollector _show_code_pcollector;
   static PStatCollector _show_code_pcollector;
   PStatCollector _task_pcollector;
   PStatCollector _task_pcollector;
 
 
+  friend class PythonTask;
+
 public:
 public:
   static TypeHandle get_class_type() {
   static TypeHandle get_class_type() {
     return _type_handle;
     return _type_handle;

+ 23 - 1
panda/src/event/asyncTaskChain.cxx

@@ -44,6 +44,7 @@ AsyncTaskChain(AsyncTaskManager *manager, const string &name) :
   _frame_sync(false),
   _frame_sync(false),
   _num_busy_threads(0),
   _num_busy_threads(0),
   _num_tasks(0),
   _num_tasks(0),
+  _num_awaiting_tasks(0),
   _state(S_initial),
   _state(S_initial),
   _current_sort(-INT_MAX),
   _current_sort(-INT_MAX),
   _pickup_mode(false),
   _pickup_mode(false),
@@ -726,6 +727,13 @@ service_one_task(AsyncTaskChain::AsyncTaskChainThread *thread) {
           }
           }
           break;
           break;
 
 
+        case AsyncTask::DS_await:
+          // The task wants to wait for another one to finish.
+          task->_state = AsyncTask::S_awaiting;
+          _cvar.notify_all();
+          ++_num_awaiting_tasks;
+          break;
+
         default:
         default:
           // The task has finished.
           // The task has finished.
           cleanup_task(task, true, true);
           cleanup_task(task, true, true);
@@ -775,6 +783,20 @@ cleanup_task(AsyncTask *task, bool upon_death, bool clean_exit) {
 
 
   _manager->remove_task_by_name(task);
   _manager->remove_task_by_name(task);
 
 
+  // Activate the tasks that were waiting for this one to finish.
+  if (upon_death) {
+    pvector<PT(AsyncTask)>::iterator it;
+    for (it = task->_waiting_tasks.begin(); it != task->_waiting_tasks.end(); ++it) {
+      AsyncTask *task = *it;
+      // Note that this task may not be on the same task chain.
+      nassertd(task->_manager == _manager) continue;
+      task->_state = AsyncTask::S_active;
+      task->_chain->_active.push_back(task);
+      --task->_chain->_num_awaiting_tasks;
+    }
+    task->_waiting_tasks.clear();
+  }
+
   if (upon_death) {
   if (upon_death) {
     _manager->_lock.release();
     _manager->_lock.release();
     task->upon_death(_manager, clean_exit);
     task->upon_death(_manager, clean_exit);
@@ -899,7 +921,7 @@ finish_sort_group() {
     filter_timeslice_priority();
     filter_timeslice_priority();
   }
   }
 
 
-  nassertr((size_t)_num_tasks == _active.size() + _this_active.size() + _next_active.size() + _sleeping.size(), true);
+  nassertr((size_t)_num_tasks == _active.size() + _this_active.size() + _next_active.size() + _sleeping.size() + (size_t)_num_awaiting_tasks, true);
   make_heap(_active.begin(), _active.end(), AsyncTaskSortPriority());
   make_heap(_active.begin(), _active.end(), AsyncTaskSortPriority());
 
 
   _current_sort = -INT_MAX;
   _current_sort = -INT_MAX;

+ 1 - 0
panda/src/event/asyncTaskChain.h

@@ -172,6 +172,7 @@ protected:
   bool _frame_sync;
   bool _frame_sync;
   int _num_busy_threads;
   int _num_busy_threads;
   int _num_tasks;
   int _num_tasks;
+  int _num_awaiting_tasks;
   TaskHeap _active;
   TaskHeap _active;
   TaskHeap _this_active;
   TaskHeap _this_active;
   TaskHeap _next_active;
   TaskHeap _next_active;

+ 1 - 0
panda/src/event/asyncTaskManager.h

@@ -155,6 +155,7 @@ private:
   friend class AsyncTaskChain::AsyncTaskChainThread;
   friend class AsyncTaskChain::AsyncTaskChainThread;
   friend class AsyncTask;
   friend class AsyncTask;
   friend class AsyncTaskSequence;
   friend class AsyncTaskSequence;
+  friend class PythonTask;
 };
 };
 
 
 INLINE ostream &operator << (ostream &out, const AsyncTaskManager &manager) {
 INLINE ostream &operator << (ostream &out, const AsyncTaskManager &manager) {

+ 75 - 0
panda/src/event/asyncTask_ext.cxx

@@ -0,0 +1,75 @@
+/**
+ * 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 asyncTask_ext.h
+ * @author rdb
+ * @date 2017-10-29
+ */
+
+#include "asyncTask_ext.h"
+#include "nodePath.h"
+
+#ifdef HAVE_PYTHON
+
+#ifndef CPPPARSER
+extern struct Dtool_PyTypedObject Dtool_AsyncTask;
+#endif
+
+/**
+ * Yields continuously until the task has finished.
+ */
+static PyObject *gen_next(PyObject *self) {
+  const AsyncTask *request = nullptr;
+  if (!Dtool_Call_ExtractThisPointer(self, Dtool_AsyncTask, (void **)&request)) {
+    return nullptr;
+  }
+
+  if (request->is_alive()) {
+    // Continue awaiting the result.
+    Py_INCREF(self);
+    return self;
+  } else {
+    // It's done.  Do we have a method like result(), eg. in the case of a
+    // ModelLoadRequest?  In that case we pass that value into the exception.
+    PyObject *method = PyObject_GetAttrString(self, "result");
+    PyObject *result = nullptr;
+    if (method != nullptr) {
+      if (PyCallable_Check(method)) {
+        result = _PyObject_CallNoArg(method);
+        Py_DECREF(method);
+        if (result == nullptr) {
+          // An exception happened.  Pass it on.
+          return nullptr;
+        }
+      }
+      Py_DECREF(method);
+    }
+    Py_INCREF(PyExc_StopIteration);
+    PyErr_Restore(PyExc_StopIteration, result, nullptr);
+    return nullptr;
+  }
+}
+
+/**
+ * Returns a generator that continuously yields an awaitable until the task
+ * has finished.  This allows syntax like `model = await loader.load...` to be
+ * used in a Python coroutine.
+ */
+PyObject *Extension<AsyncTask>::
+__await__(PyObject *self) {
+  Dtool_GeneratorWrapper *gen;
+  gen = (Dtool_GeneratorWrapper *)PyType_GenericAlloc(&Dtool_GeneratorWrapper_Type, 0);
+  if (gen != nullptr) {
+    Py_INCREF(self);
+    gen->_base._self = self;
+    gen->_iternext_func = &gen_next;
+  }
+  return (PyObject *)gen;
+}
+
+#endif

+ 35 - 0
panda/src/event/asyncTask_ext.h

@@ -0,0 +1,35 @@
+/**
+ * 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 asyncTask_ext.h
+ * @author rdb
+ * @date 2017-10-29
+ */
+
+#ifndef ASYNCTASK_EXT_H
+#define ASYNCTASK_EXT_H
+
+#include "extension.h"
+#include "py_panda.h"
+#include "modelLoadRequest.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * Extension class for AsyncTask
+ */
+template<>
+class Extension<AsyncTask> : public ExtensionBase<AsyncTask> {
+public:
+  static PyObject *__await__(PyObject *self);
+  static PyObject *__iter__(PyObject *self) { return __await__(self); }
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // ASYNCTASK_EXT_H

+ 49 - 0
panda/src/event/pythonTask.I

@@ -10,3 +10,52 @@
  * @author drose
  * @author drose
  * @date 2008-09-16
  * @date 2008-09-16
  */
  */
+
+/**
+ * Returns the function that is called when the task runs.
+ */
+INLINE PyObject *PythonTask::
+get_function() {
+  Py_INCREF(_function);
+  return _function;
+}
+
+/**
+ * Returns the function that is called when the task finishes.
+ */
+INLINE PyObject *PythonTask::
+get_upon_death() {
+  Py_INCREF(_upon_death);
+  return _upon_death;
+}
+
+/**
+ * Returns the "owner" object.  See set_owner().
+ */
+INLINE PyObject *PythonTask::
+get_owner() const {
+  Py_INCREF(_owner);
+  return _owner;
+}
+
+/**
+ * Sets the "result" of this task.  This is the value returned from an "await"
+ * expression on this task.
+ * This can only be called while the task is still alive.
+ */
+INLINE void PythonTask::
+set_result(PyObject *result) {
+  nassertv(is_alive());
+  nassertv(_exception == nullptr);
+  Py_INCREF(result);
+  Py_XDECREF(_exc_value);
+  _exc_value = result;
+}
+
+/**
+ * Same as __await__, for backward compatibility with the old coroutine way.
+ */
+INLINE PyObject *PythonTask::
+__iter__(PyObject *self) {
+  return __await__(self);
+}

+ 298 - 52
panda/src/event/pythonTask.cxx

@@ -19,28 +19,52 @@
 #include "py_panda.h"
 #include "py_panda.h"
 
 
 #include "pythonThread.h"
 #include "pythonThread.h"
+#include "asyncTaskManager.h"
 
 
 TypeHandle PythonTask::_type_handle;
 TypeHandle PythonTask::_type_handle;
 
 
 #ifndef CPPPARSER
 #ifndef CPPPARSER
 extern struct Dtool_PyTypedObject Dtool_TypedReferenceCount;
 extern struct Dtool_PyTypedObject Dtool_TypedReferenceCount;
+extern struct Dtool_PyTypedObject Dtool_AsyncTask;
+extern struct Dtool_PyTypedObject Dtool_PythonTask;
 #endif
 #endif
 
 
 /**
 /**
  *
  *
  */
  */
 PythonTask::
 PythonTask::
-PythonTask(PyObject *function, const string &name) :
-  AsyncTask(name)
-{
-  _function = NULL;
-  _args = NULL;
-  _upon_death = NULL;
-  _owner = NULL;
-  _registered_to_owner = false;
-  _generator = NULL;
+PythonTask(PyObject *func_or_coro, const string &name) :
+  AsyncTask(name),
+  _function(nullptr),
+  _args(nullptr),
+  _upon_death(nullptr),
+  _owner(nullptr),
+  _registered_to_owner(false),
+  _exception(nullptr),
+  _exc_value(nullptr),
+  _exc_traceback(nullptr),
+  _generator(nullptr),
+  _future_done(nullptr),
+  _retrieved_exception(false) {
+
+  nassertv(func_or_coro != nullptr);
+  if (func_or_coro == Py_None || PyCallable_Check(func_or_coro)) {
+    _function = func_or_coro;
+    Py_INCREF(_function);
+#if PY_VERSION_HEX >= 0x03050000
+  } else if (PyCoro_CheckExact(func_or_coro)) {
+    // We also allow passing in a coroutine, because why not.
+    _generator = func_or_coro;
+    Py_INCREF(_generator);
+#endif
+  } else if (PyGen_CheckExact(func_or_coro)) {
+    // Something emulating a coroutine.
+    _generator = func_or_coro;
+    Py_INCREF(_generator);
+  } else {
+    nassert_raise("Invalid function passed to PythonTask");
+  }
 
 
-  set_function(function);
   set_args(Py_None, true);
   set_args(Py_None, true);
   set_upon_death(Py_None);
   set_upon_death(Py_None);
   set_owner(Py_None);
   set_owner(Py_None);
@@ -60,9 +84,24 @@ PythonTask(PyObject *function, const string &name) :
  */
  */
 PythonTask::
 PythonTask::
 ~PythonTask() {
 ~PythonTask() {
-  Py_DECREF(_function);
+#ifndef NDEBUG
+  // If the coroutine threw an exception, and there was no opportunity to
+  // handle it, let the user know.
+  if (_exception != nullptr && !_retrieved_exception) {
+    task_cat.error()
+      << *this << " exception was never retrieved:\n";
+    PyErr_Restore(_exception, _exc_value, _exc_traceback);
+    PyErr_Print();
+    PyErr_Restore(nullptr, nullptr, nullptr);
+  }
+#endif
+
+  Py_XDECREF(_function);
   Py_DECREF(_args);
   Py_DECREF(_args);
   Py_DECREF(__dict__);
   Py_DECREF(__dict__);
+  Py_XDECREF(_exception);
+  Py_XDECREF(_exc_value);
+  Py_XDECREF(_exc_traceback);
   Py_XDECREF(_generator);
   Py_XDECREF(_generator);
   Py_XDECREF(_owner);
   Py_XDECREF(_owner);
   Py_XDECREF(_upon_death);
   Py_XDECREF(_upon_death);
@@ -83,15 +122,6 @@ set_function(PyObject *function) {
   }
   }
 }
 }
 
 
-/**
- * Returns the function that is called when the task runs.
- */
-PyObject *PythonTask::
-get_function() {
-  Py_INCREF(_function);
-  return _function;
-}
-
 /**
 /**
  * Replaces the argument list that is passed to the task function.  The
  * Replaces the argument list that is passed to the task function.  The
  * parameter should be a tuple or list of arguments, or None to indicate the
  * parameter should be a tuple or list of arguments, or None to indicate the
@@ -166,15 +196,6 @@ set_upon_death(PyObject *upon_death) {
   }
   }
 }
 }
 
 
-/**
- * Returns the function that is called when the task finishes.
- */
-PyObject *PythonTask::
-get_upon_death() {
-  Py_INCREF(_upon_death);
-  return _upon_death;
-}
-
 /**
 /**
  * Specifies a Python object that serves as the "owner" for the task.  This
  * Specifies a Python object that serves as the "owner" for the task.  This
  * owner object must have two methods: _addTask() and _clearTask(), which will
  * owner object must have two methods: _addTask() and _clearTask(), which will
@@ -212,12 +233,87 @@ set_owner(PyObject *owner) {
 }
 }
 
 
 /**
 /**
- * Returns the "owner" object.  See set_owner().
+ * Returns the result of this task's execution, as set by set_result() within
+ * the task or returned from a coroutine added to the task manager.  If an
+ * exception occurred within this task, it is raised instead.
  */
  */
 PyObject *PythonTask::
 PyObject *PythonTask::
-get_owner() {
-  Py_INCREF(_owner);
-  return _owner;
+result() const {
+  nassertr(!is_alive(), nullptr);
+
+  if (_exception == nullptr) {
+    // The result of the call is stored in _exc_value.
+    Py_XINCREF(_exc_value);
+    return _exc_value;
+  } else {
+    _retrieved_exception = true;
+    Py_INCREF(_exception);
+    Py_XINCREF(_exc_value);
+    Py_XINCREF(_exc_traceback);
+    PyErr_Restore(_exception, _exc_value, _exc_traceback);
+    return nullptr;
+  }
+}
+
+/**
+ * If an exception occurred during execution of this task, returns it.  This
+ * is only set if this task returned a coroutine or generator.
+ */
+/*PyObject *PythonTask::
+exception() const {
+  if (_exception == nullptr) {
+    Py_INCREF(Py_None);
+    return Py_None;
+  } else if (_exc_value == nullptr || _exc_value == Py_None) {
+    return _PyObject_CallNoArg(_exception);
+  } else if (PyTuple_Check(_exc_value)) {
+    return PyObject_Call(_exception, _exc_value, nullptr);
+  } else {
+    return PyObject_CallFunctionObjArgs(_exception, _exc_value, nullptr);
+  }
+}*/
+
+/**
+ * Returns an iterator that continuously yields an awaitable until the task
+ * has finished.
+ */
+PyObject *PythonTask::
+__await__(PyObject *self) {
+  Dtool_GeneratorWrapper *gen;
+  gen = (Dtool_GeneratorWrapper *)PyType_GenericAlloc(&Dtool_GeneratorWrapper_Type, 0);
+  if (gen != nullptr) {
+    Py_INCREF(self);
+    gen->_base._self = self;
+    gen->_iternext_func = &gen_next;
+  }
+  return (PyObject *)gen;
+}
+
+/**
+ * Yields continuously until a task has finished.
+ */
+PyObject *PythonTask::
+gen_next(PyObject *self) {
+  const PythonTask *task = nullptr;
+  if (!Dtool_Call_ExtractThisPointer(self, Dtool_PythonTask, (void **)&task)) {
+    return nullptr;
+  }
+
+  if (task->is_alive()) {
+    Py_INCREF(self);
+    return self;
+  } else if (task->_exception != nullptr) {
+    task->_retrieved_exception = true;
+    Py_INCREF(task->_exception);
+    Py_INCREF(task->_exc_value);
+    Py_INCREF(task->_exc_traceback);
+    PyErr_Restore(task->_exception, task->_exc_value, task->_exc_traceback);
+    return nullptr;
+  } else {
+    // The result of the call is stored in _exc_value.
+    PyErr_SetObject(PyExc_StopIteration, task->_exc_value);
+    return nullptr;
+  }
 }
 }
 
 
 /**
 /**
@@ -396,16 +492,30 @@ do_task() {
  */
  */
 AsyncTask::DoneStatus PythonTask::
 AsyncTask::DoneStatus PythonTask::
 do_python_task() {
 do_python_task() {
-  PyObject *result = NULL;
+  PyObject *result = nullptr;
+
+  // Are we waiting for a future to finish?
+  if (_future_done != nullptr) {
+    PyObject *is_done = PyObject_CallObject(_future_done, nullptr);
+    if (!PyObject_IsTrue(is_done)) {
+      // Nope, ask again next frame.
+      Py_DECREF(is_done);
+      return DS_cont;
+    }
+    Py_DECREF(is_done);
+    Py_DECREF(_future_done);
+    _future_done = nullptr;
+  }
 
 
-  if (_generator == (PyObject *)NULL) {
+  if (_generator == nullptr) {
     // We are calling the function directly.
     // We are calling the function directly.
+    nassertr(_function != nullptr, DS_interrupt);
+
     PyObject *args = get_args();
     PyObject *args = get_args();
     result = PythonThread::call_python_func(_function, args);
     result = PythonThread::call_python_func(_function, args);
     Py_DECREF(args);
     Py_DECREF(args);
 
 
-#ifdef PyGen_Check
-    if (result != (PyObject *)NULL && PyGen_Check(result)) {
+    if (result != nullptr && PyGen_Check(result)) {
       // The function has yielded a generator.  We will call into that
       // The function has yielded a generator.  We will call into that
       // henceforth, instead of calling the function from the top again.
       // henceforth, instead of calling the function from the top again.
       if (task_cat.is_debug()) {
       if (task_cat.is_debug()) {
@@ -423,30 +533,166 @@ do_python_task() {
         Py_DECREF(str);
         Py_DECREF(str);
       }
       }
       _generator = result;
       _generator = result;
-      result = NULL;
-    }
+      result = nullptr;
+
+#if PY_VERSION_HEX >= 0x03050000
+    } else if (result != nullptr && Py_TYPE(result)->tp_as_async != nullptr) {
+      // The function yielded a coroutine, or something of the sort.
+      if (task_cat.is_debug()) {
+        PyObject *str = PyObject_ASCII(_function);
+        PyObject *str2 = PyObject_ASCII(result);
+        task_cat.debug()
+          << PyUnicode_AsUTF8(str) << " in " << *this
+          << " yielded an awaitable: " << PyUnicode_AsUTF8(str2) << "\n";
+        Py_DECREF(str);
+        Py_DECREF(str2);
+      }
+      if (PyCoro_CheckExact(result)) {
+        // If a coroutine, am_await is possible but senseless, since we can
+        // just call send(None) on the coroutine itself.
+        _generator = result;
+      } else {
+        unaryfunc await = Py_TYPE(result)->tp_as_async->am_await;
+        _generator = await(result);
+        Py_DECREF(result);
+      }
+      result = nullptr;
 #endif
 #endif
+    }
   }
   }
 
 
-  if (_generator != (PyObject *)NULL) {
-    // We are calling a generator.
-    PyObject *func = PyObject_GetAttrString(_generator, "next");
-    nassertr(func != (PyObject *)NULL, DS_interrupt);
-
-    result = PyObject_CallObject(func, NULL);
+  if (_generator != nullptr) {
+    // We are calling a generator.  Use "send" rather than PyIter_Next since
+    // we need to be able to read the value from a StopIteration exception.
+    PyObject *func = PyObject_GetAttrString(_generator, "send");
+    nassertr(func != nullptr, DS_interrupt);
+    result = PyObject_CallFunctionObjArgs(func, Py_None, nullptr);
     Py_DECREF(func);
     Py_DECREF(func);
 
 
-    if (result == (PyObject *)NULL && PyErr_Occurred() &&
-        PyErr_ExceptionMatches(PyExc_StopIteration)) {
-      // "Catch" StopIteration and treat it like DS_done.
-      PyErr_Clear();
+    if (result == nullptr) {
+      // An error happened.  If StopIteration, that indicates the task has
+      // returned.  Otherwise, we need to save it so that it can be re-raised
+      // in the function that awaited this task.
       Py_DECREF(_generator);
       Py_DECREF(_generator);
-      _generator = NULL;
-      return DS_done;
+      _generator = nullptr;
+
+#if PY_VERSION_HEX >= 0x03030000
+      if (_PyGen_FetchStopIterationValue(&result) == 0) {
+#else
+      if (PyErr_ExceptionMatches(PyExc_StopIteration)) {
+        result = Py_None;
+        Py_INCREF(result);
+#endif
+        PyErr_Restore(nullptr, nullptr, nullptr);
+
+        // If we passed a coroutine into the task, eg. something like:
+        //   taskMgr.add(my_async_function())
+        // then we cannot rerun the task, so the return value is always
+        // assumed to be DS_done.  Instead, we pass the return value to the
+        // result of the `await` expression.
+        if (_function == nullptr) {
+          if (task_cat.is_debug()) {
+            task_cat.debug()
+              << *this << " received StopIteration from coroutine.\n";
+          }
+          // Store the result in _exc_value because that's not used anyway.
+          Py_XDECREF(_exc_value);
+          _exc_value = result;
+          return DS_done;
+        }
+      } else if (_function == nullptr) {
+        // We got an exception.  If this is a scheduled coroutine, we will
+        // keep it and instead throw it into whatever 'awaits' this task.
+        // Otherwise, fall through and handle it the regular way.
+        Py_XDECREF(_exception);
+        Py_XDECREF(_exc_value);
+        Py_XDECREF(_exc_traceback);
+        PyErr_Fetch(&_exception, &_exc_value, &_exc_traceback);
+        _retrieved_exception = false;
+
+        if (task_cat.is_debug()) {
+          if (_exception != nullptr && Py_TYPE(_exception) == &PyType_Type) {
+            task_cat.debug()
+              << *this << " received " << ((PyTypeObject *)_exception)->tp_name << " from coroutine.\n";
+          } else {
+            task_cat.debug()
+              << *this << " received exception from coroutine.\n";
+          }
+        }
+
+        // Tell the task chain we want to kill ourselves.  It doesn't really
+        // matter what we return if we set S_servicing_removed.  If we don't
+        // set it, however, it will think this was a clean exit.
+        _manager->_lock.acquire();
+        _state = S_servicing_removed;
+        _manager->_lock.release();
+        return DS_interrupt;
+      }
+
+    } else if (DtoolCanThisBeAPandaInstance(result)) {
+      // We are waiting for a task to finish.
+      void *ptr = ((Dtool_PyInstDef *)result)->_My_Type->_Dtool_UpcastInterface(result, &Dtool_AsyncTask);
+      if (ptr != nullptr) {
+        // Suspend execution of this task until this other task has completed.
+        AsyncTask *task = (AsyncTask *)ptr;
+        AsyncTaskManager *manager = task->_manager;
+        nassertr(manager != nullptr, DS_interrupt);
+        nassertr(manager == _manager, DS_interrupt);
+        manager->_lock.acquire();
+        if (task != (AsyncTask *)this) {
+          if (task->is_alive()) {
+            if (task_cat.is_debug()) {
+              task_cat.debug()
+                << *this << " is now awaiting <" << *task << ">.\n";
+            }
+            task->_waiting_tasks.push_back(this);
+          } else {
+            // The task is already done.  Continue at next opportunity.
+            Py_DECREF(result);
+            manager->_lock.release();
+            return DS_cont;
+          }
+        } else {
+          // This is an error.  If we wanted to be fancier we could also
+          // detect deeper circular dependencies.
+          task_cat.error()
+            << *this << " cannot await itself\n";
+        }
+        task->_manager->_lock.release();
+        Py_DECREF(result);
+        return DS_await;
+      }
+
+    } else {
+      // We are waiting for a future to finish.  We currently implement this
+      // by simply checking every frame whether the future is done.
+      PyObject *check = PyObject_GetAttrString(result, "_asyncio_future_blocking");
+      if (check != nullptr && check != Py_None) {
+        Py_DECREF(check);
+        // Next frame, check whether this future is done.
+        _future_done = PyObject_GetAttrString(result, "done");
+        if (_future_done == nullptr || !PyCallable_Check(_future_done)) {
+          task_cat.error()
+            << "future.done is not callable\n";
+          return DS_interrupt;
+        }
+#if PY_MAJOR_VERSION >= 3
+        if (task_cat.is_debug()) {
+          PyObject *str = PyObject_ASCII(result);
+          task_cat.debug()
+            << *this << " is now awaiting " << PyUnicode_AsUTF8(str) << ".\n";
+          Py_DECREF(str);
+        }
+#endif
+        Py_DECREF(result);
+        return DS_cont;
+      }
+      PyErr_Clear();
+      Py_XDECREF(check);
     }
     }
   }
   }
 
 
-  if (result == (PyObject *)NULL) {
+  if (result == nullptr) {
     if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_SystemExit)) {
     if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_SystemExit)) {
       // Don't print an error message for SystemExit.  Or rather, make it a
       // Don't print an error message for SystemExit.  Or rather, make it a
       // debug message.
       // debug message.

+ 23 - 7
panda/src/event/pythonTask.h

@@ -22,8 +22,8 @@
 #include "py_panda.h"
 #include "py_panda.h"
 
 
 /**
 /**
- * This class exists to allow association of a Python function with the
- * AsyncTaskManager.
+ * This class exists to allow association of a Python function or coroutine
+ * with the AsyncTaskManager.
  */
  */
 class PythonTask : public AsyncTask {
 class PythonTask : public AsyncTask {
 PUBLISHED:
 PUBLISHED:
@@ -32,16 +32,23 @@ PUBLISHED:
   ALLOC_DELETED_CHAIN(PythonTask);
   ALLOC_DELETED_CHAIN(PythonTask);
 
 
   void set_function(PyObject *function);
   void set_function(PyObject *function);
-  PyObject *get_function();
+  INLINE PyObject *get_function();
 
 
   void set_args(PyObject *args, bool append_task);
   void set_args(PyObject *args, bool append_task);
   PyObject *get_args();
   PyObject *get_args();
 
 
   void set_upon_death(PyObject *upon_death);
   void set_upon_death(PyObject *upon_death);
-  PyObject *get_upon_death();
+  INLINE PyObject *get_upon_death();
 
 
   void set_owner(PyObject *owner);
   void set_owner(PyObject *owner);
-  PyObject *get_owner();
+  INLINE PyObject *get_owner() const;
+
+  INLINE void set_result(PyObject *result);
+  PyObject *result() const;
+  //PyObject *exception() const;
+
+  static PyObject *__await__(PyObject *self);
+  INLINE static PyObject *__iter__(PyObject *self);
 
 
   int __setattr__(PyObject *self, PyObject *attr, PyObject *v);
   int __setattr__(PyObject *self, PyObject *attr, PyObject *v);
   int __delattr__(PyObject *self, PyObject *attr);
   int __delattr__(PyObject *self, PyObject *attr);
@@ -94,6 +101,8 @@ protected:
   virtual void upon_death(AsyncTaskManager *manager, bool clean_exit);
   virtual void upon_death(AsyncTaskManager *manager, bool clean_exit);
 
 
 private:
 private:
+  static PyObject *gen_next(PyObject *self);
+
   void register_to_owner();
   void register_to_owner();
   void unregister_from_owner();
   void unregister_from_owner();
   void call_owner_method(const char *method_name);
   void call_owner_method(const char *method_name);
@@ -102,12 +111,19 @@ private:
 private:
 private:
   PyObject *_function;
   PyObject *_function;
   PyObject *_args;
   PyObject *_args;
-  bool _append_task;
   PyObject *_upon_death;
   PyObject *_upon_death;
   PyObject *_owner;
   PyObject *_owner;
-  bool _registered_to_owner;
+
+  PyObject *_exception;
+  PyObject *_exc_value;
+  PyObject *_exc_traceback;
 
 
   PyObject *_generator;
   PyObject *_generator;
+  PyObject *_future_done;
+
+  bool _append_task;
+  bool _registered_to_owner;
+  mutable bool _retrieved_exception;
 
 
 public:
 public:
   static TypeHandle get_class_type() {
   static TypeHandle get_class_type() {

+ 16 - 2
panda/src/pgraph/modelFlattenRequest.I

@@ -34,7 +34,7 @@ get_orig() const {
 /**
 /**
  * Returns true if this request has completed, false if it is still pending.
  * Returns true if this request has completed, false if it is still pending.
  * When this returns true, you may retrieve the model loaded by calling
  * When this returns true, you may retrieve the model loaded by calling
- * get_result().
+ * result().
  */
  */
 INLINE bool ModelFlattenRequest::
 INLINE bool ModelFlattenRequest::
 is_ready() const {
 is_ready() const {
@@ -47,6 +47,20 @@ is_ready() const {
  */
  */
 INLINE PandaNode *ModelFlattenRequest::
 INLINE PandaNode *ModelFlattenRequest::
 get_model() const {
 get_model() const {
-  nassertr(_is_ready, NULL);
+  nassertr(_is_ready, nullptr);
   return _model;
   return _model;
 }
 }
+
+/**
+ * Returns the flattened copy of the model wrapped in a NodePath.  It is an
+ * error to call this unless is_ready() returns true.
+ */
+INLINE NodePath ModelFlattenRequest::
+result() const {
+  nassertr(_is_ready, NodePath::fail());
+  if (_model != nullptr) {
+    return NodePath(_model);
+  } else {
+    return NodePath::fail();
+  }
+}

+ 3 - 1
panda/src/pgraph/modelFlattenRequest.h

@@ -19,6 +19,7 @@
 #include "asyncTask.h"
 #include "asyncTask.h"
 #include "pandaNode.h"
 #include "pandaNode.h"
 #include "pointerTo.h"
 #include "pointerTo.h"
+#include "nodePath.h"
 
 
 /**
 /**
  * This class object manages a single asynchronous request to flatten a model.
  * This class object manages a single asynchronous request to flatten a model.
@@ -38,9 +39,10 @@ PUBLISHED:
   INLINE bool is_ready() const;
   INLINE bool is_ready() const;
   INLINE PandaNode *get_model() const;
   INLINE PandaNode *get_model() const;
 
 
+  INLINE NodePath result() const;
+
   MAKE_PROPERTY(orig, get_orig);
   MAKE_PROPERTY(orig, get_orig);
   MAKE_PROPERTY(ready, is_ready);
   MAKE_PROPERTY(ready, is_ready);
-  MAKE_PROPERTY(model, get_model);
 
 
 protected:
 protected:
   virtual DoneStatus do_task();
   virtual DoneStatus do_task();

+ 10 - 0
panda/src/pgraph/modelLoadRequest.I

@@ -37,6 +37,16 @@ get_loader() const {
   return _loader;
   return _loader;
 }
 }
 
 
+/**
+ * Returns the model that was loaded asynchronously as a NodePath, if any, or
+ * the empty NodePath if there was an error.
+ */
+INLINE NodePath ModelLoadRequest::
+result() const {
+  nassertr_always(_is_ready, NodePath::fail());
+  return NodePath(_model);
+}
+
 /**
 /**
  * Returns true if this request has completed, false if it is still pending.
  * Returns true if this request has completed, false if it is still pending.
  * When this returns true, you may retrieve the model loaded by calling
  * When this returns true, you may retrieve the model loaded by calling

+ 5 - 3
panda/src/pgraph/modelLoadRequest.h

@@ -11,8 +11,8 @@
  * @date 2006-08-29
  * @date 2006-08-29
  */
  */
 
 
-#ifndef MODELLOADREQUEST
-#define MODELLOADREQUEST
+#ifndef MODELLOADREQUEST_H
+#define MODELLOADREQUEST_H
 
 
 #include "pandabase.h"
 #include "pandabase.h"
 
 
@@ -22,6 +22,7 @@
 #include "pandaNode.h"
 #include "pandaNode.h"
 #include "pointerTo.h"
 #include "pointerTo.h"
 #include "loader.h"
 #include "loader.h"
+#include "nodePath.h"
 
 
 /**
 /**
  * A class object that manages a single asynchronous model load request.
  * A class object that manages a single asynchronous model load request.
@@ -42,6 +43,8 @@ PUBLISHED:
   INLINE const LoaderOptions &get_options() const;
   INLINE const LoaderOptions &get_options() const;
   INLINE Loader *get_loader() const;
   INLINE Loader *get_loader() const;
 
 
+  INLINE NodePath result() const;
+
   INLINE bool is_ready() const;
   INLINE bool is_ready() const;
   INLINE PandaNode *get_model() const;
   INLINE PandaNode *get_model() const;
 
 
@@ -49,7 +52,6 @@ PUBLISHED:
   MAKE_PROPERTY(options, get_options);
   MAKE_PROPERTY(options, get_options);
   MAKE_PROPERTY(loader, get_loader);
   MAKE_PROPERTY(loader, get_loader);
   MAKE_PROPERTY(ready, is_ready);
   MAKE_PROPERTY(ready, is_ready);
-  MAKE_PROPERTY(model, get_model);
 
 
 protected:
 protected:
   virtual DoneStatus do_task();
   virtual DoneStatus do_task();

+ 10 - 0
panda/src/pgraph/modelSaveRequest.I

@@ -64,3 +64,13 @@ get_success() const {
   nassertr(_is_ready, false);
   nassertr(_is_ready, false);
   return _success;
   return _success;
 }
 }
+
+/**
+ * Returns a boolean indicating whether the model saved correctly.  It is an
+ * error to call this unless is_ready() returns true.
+ */
+INLINE bool ModelSaveRequest::
+result() const {
+  nassertr(_is_ready, false);
+  return _success;
+}

+ 4 - 3
panda/src/pgraph/modelSaveRequest.h

@@ -11,8 +11,8 @@
  * @date 2012-12-19
  * @date 2012-12-19
  */
  */
 
 
-#ifndef MODELSAVEREQUEST
-#define MODELSAVEREQUEST
+#ifndef MODELSAVEREQUEST_H
+#define MODELSAVEREQUEST_H
 
 
 #include "pandabase.h"
 #include "pandabase.h"
 
 
@@ -46,12 +46,13 @@ PUBLISHED:
   INLINE bool is_ready() const;
   INLINE bool is_ready() const;
   INLINE bool get_success() const;
   INLINE bool get_success() const;
 
 
+  INLINE bool result() const;
+
   MAKE_PROPERTY(filename, get_filename);
   MAKE_PROPERTY(filename, get_filename);
   MAKE_PROPERTY(options, get_options);
   MAKE_PROPERTY(options, get_options);
   MAKE_PROPERTY(node, get_node);
   MAKE_PROPERTY(node, get_node);
   MAKE_PROPERTY(loader, get_loader);
   MAKE_PROPERTY(loader, get_loader);
   MAKE_PROPERTY(ready, is_ready);
   MAKE_PROPERTY(ready, is_ready);
-  MAKE_PROPERTY(success, get_success);
 
 
 protected:
 protected:
   virtual DoneStatus do_task();
   virtual DoneStatus do_task();

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

@@ -31,6 +31,7 @@
 #include "shader.h"
 #include "shader.h"
 #include "texture.h"
 #include "texture.h"
 #include "shaderBuffer.h"
 #include "shaderBuffer.h"
+#include "extension.h"
 
 
 /**
 /**
  * This is a small container class that can hold any one of the value types
  * This is a small container class that can hold any one of the value types

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

@@ -1417,7 +1417,10 @@ synthesize_shader(const RenderState *rs, const GeomVertexAnimationSpec &anim) {
       text << "\t result.rgb = lerp(result, tex" << i << ", tex" << i << ".a).rgb;\n";
       text << "\t result.rgb = lerp(result, tex" << i << ", tex" << i << ".a).rgb;\n";
       break;
       break;
     case TextureStage::M_blend:
     case TextureStage::M_blend:
-      text << "\t result.rgb = lerp(result, tex" << i << " * texcolor_" << i << ", tex" << i << ".r).rgb;\n";
+      text << "\t result.rgb = lerp(result.rgb, texcolor_" << i << ".rgb, tex" << i << ".rgb);\n";
+      if (key._calc_primary_alpha) {
+        text << "\t result.a *= tex" << i << ".a;\n";
+      }
       break;
       break;
     case TextureStage::M_replace:
     case TextureStage::M_replace:
       text << "\t result = tex" << i << ";\n";
       text << "\t result = tex" << i << ";\n";

+ 111 - 0
panda/src/testbed/pview.cxx

@@ -25,6 +25,9 @@
 #include "panda_getopt.h"
 #include "panda_getopt.h"
 #include "preprocess_argv.h"
 #include "preprocess_argv.h"
 #include "graphicsPipeSelection.h"
 #include "graphicsPipeSelection.h"
+#include "asyncTaskManager.h"
+#include "asyncTask.h"
+#include "boundingSphere.h"
 
 
 // By including checkPandaVersion.h, we guarantee that runtime attempts to run
 // By including checkPandaVersion.h, we guarantee that runtime attempts to run
 // pview will fail if it inadvertently links with the wrong version of
 // pview will fail if it inadvertently links with the wrong version of
@@ -231,6 +234,111 @@ report_version() {
   nout << "\n";
   nout << "\n";
 }
 }
 
 
+// Task that dynamically adjusts the camera len's near/far clipping
+// planes to ensure the user can zoom in as close as needed to a model.
+//
+// Code adapted from WindowFramework::center_trackball(), but
+// without moving the camera.  When the camera is inside the model,
+// the near clip is set to near-zero.
+//
+class AdjustCameraClipPlanesTask : public AsyncTask {
+public:
+  AdjustCameraClipPlanesTask(const string &name, Camera *camera) :
+    AsyncTask(name), _camera(camera), _lens(camera->get_lens(0)), _sphere(NULL)
+  {
+    NodePath np = framework.get_models();
+    PT(BoundingVolume) volume = np.get_bounds();
+
+    // We expect at least a geometric bounding volume around the world.
+    nassertv(volume != (BoundingVolume *)NULL);
+    nassertv(volume->is_of_type(GeometricBoundingVolume::get_class_type()));
+    CPT(GeometricBoundingVolume) gbv = DCAST(GeometricBoundingVolume, volume);
+
+    if (np.has_parent()) {
+      CPT(TransformState) net_transform = np.get_parent().get_net_transform();
+      PT(GeometricBoundingVolume) new_gbv = DCAST(GeometricBoundingVolume, gbv->make_copy());
+      new_gbv->xform(net_transform->get_mat());
+      gbv = new_gbv;
+    }
+
+    // Determine the bounding sphere around the object.
+    if (gbv->is_infinite()) {
+      framework_cat.warning()
+        << "Infinite bounding volume for " << np << "\n";
+      return;
+    }
+
+    if (gbv->is_empty()) {
+      framework_cat.warning()
+        << "Empty bounding volume for " << np << "\n";
+      return;
+    }
+
+    // The BoundingVolume might be a sphere (it's likely), but since it
+    // might not, we'll take no chances and make our own sphere.
+    _sphere = new BoundingSphere(gbv->get_approx_center(), 0.0f);
+    if (!_sphere->extend_by(gbv)) {
+      framework_cat.warning()
+        << "Cannot determine bounding volume of " << np << "\n";
+      return;
+    }
+  }
+  ALLOC_DELETED_CHAIN(AdjustCameraClipPlanesTask);
+
+  virtual DoneStatus do_task() {
+    if (!_sphere) {
+      return DS_done;
+    }
+
+    if (framework.get_num_windows() == 0) {
+      return DS_cont;
+    }
+
+    WindowFramework *wf = framework.get_window(0);
+    if (!wf) {
+      return DS_cont;
+    }
+
+    // Get current camera position.
+    NodePath cameraNP = wf->get_camera_group();
+    LPoint3 pos = cameraNP.get_pos();
+
+    // See how far or close the camera is
+    LPoint3 center = _sphere->get_center();
+    PN_stdfloat radius = _sphere->get_radius();
+
+    PN_stdfloat min_distance = 0.001 * radius;
+
+    // Choose a suitable distance to view the whole volume in our frame.
+    // This is based on the camera lens in use.
+    PN_stdfloat distance;
+    CPT(GeometricBoundingVolume) gbv = DCAST(GeometricBoundingVolume, _sphere);
+    if (gbv->contains(pos)) {
+      // See as up-close to the model as possible
+      distance = min_distance;
+    } else {
+      // View from a distance
+      distance = (center - pos).length();
+    }
+
+    // Ensure the far plane is far enough back to see the entire object.
+    PN_stdfloat ideal_far_plane = distance + radius * 1.5;
+    _lens->set_far(max(_lens->get_default_far(), ideal_far_plane));
+
+    // And that the near plane is far enough forward, but if inside
+    // the sphere, keep above 0.
+    PN_stdfloat ideal_near_plane = max(min_distance * 10, distance - radius);
+    _lens->set_near(min(_lens->get_default_near(), ideal_near_plane));
+
+    return DS_cont;
+  }
+
+  Camera *_camera;
+  Lens *_lens;
+  PT(BoundingSphere) _sphere;
+};
+
+
 int
 int
 main(int argc, char **argv) {
 main(int argc, char **argv) {
   preprocess_argv(argc, argv);
   preprocess_argv(argc, argv);
@@ -383,6 +491,9 @@ main(int argc, char **argv) {
       window->set_anim_controls(true);
       window->set_anim_controls(true);
     }
     }
 
 
+    PT(AdjustCameraClipPlanesTask) task = new AdjustCameraClipPlanesTask("Adjust Camera Bounds", window->get_camera(0));
+    framework.get_task_mgr().add(task);
+
     framework.enable_default_keys();
     framework.enable_default_keys();
     framework.define_key("shift-w", "open a new window", event_W, NULL);
     framework.define_key("shift-w", "open a new window", event_W, NULL);
     framework.define_key("shift-f", "flatten hierarchy", event_F, NULL);
     framework.define_key("shift-f", "flatten hierarchy", event_F, NULL);

BIN
samples/rocket-console/assets/Perfect DOS VGA 437.ttf


+ 38 - 0
samples/rocket-console/assets/console.rcss

@@ -0,0 +1,38 @@
+body
+{
+    font-family: "Perfect DOS VGA 437";
+    font-weight: normal;
+    font-style: normal;
+
+    // use all the allocated texture space
+    min-width: 100%;
+    min-height: 100%;
+
+    background-color: #000;
+}
+
+
+text#content
+{
+    z-index: 2;
+    font-size: 30px;
+
+    white-space: pre-wrap;
+
+    margin: auto;
+
+    text-align: left;
+    position: absolute;
+
+    // account for non-proportionality of our 1024x512
+    // buffer compared with VGA font proportions and
+    // wanting to center the screen with 40 columns
+    top: 16px;
+    left: 32px;
+
+    width: 100%;
+    height: 100%;
+
+    color: #888;
+
+}

+ 11 - 0
samples/rocket-console/assets/console.rml

@@ -0,0 +1,11 @@
+<rml>
+
+    <head>
+        <title>Administrative Console</title>
+        <link type="text/rcss" href="console.rcss"/>
+    </head>
+    <!-- events are bound strictly from code -->
+    <body >
+        <text id="content" />
+    </body>
+</rml>

+ 1 - 0
samples/rocket-console/assets/dos437.txt

@@ -0,0 +1 @@
+from www.dafont.com/perfect-dos-vga-437.font (info at http://zehfernando.com/2015/revisiting-vga-fonts/)

+ 54 - 0
samples/rocket-console/assets/loading.rml

@@ -0,0 +1,54 @@
+<rml>
+    <head >
+        <title>Main Menu</title>
+        <link type="text/template" href="window.rml" />
+        <style>
+            body
+            {
+                width: 400px;
+                height: 225px;
+
+                margin: auto;
+
+                background-color: #44f;
+            }
+
+
+            div#title_bar_content
+            {
+                font-size: 48;
+
+                //text-align: left;
+                position: absolute;
+                top: 40%;
+                //vertical-align: center;
+
+            }
+
+        </style>
+
+        <script>
+
+import _rocketcore as rocket
+
+# This handler overrides the 'onkeydown' handler from the template
+def OnKeyDown(event, document):
+    keyId = event.parameters['key_identifier']
+    if keyId in [ rocket.key_identifier.RETURN,
+                rocket.key_identifier.ESCAPE,
+                 rocket.key_identifier.SPACE ]:
+        FireClosing(document)
+
+# custom event
+def FireClosing(document):
+    document.DispatchEvent("aboutToClose", { }, False)
+
+    </script>
+
+    </head>
+    <body id='window' template="window" onclick='FireClosing(document)'>
+        <div id="title_bar_content" >
+            <label id="loadingLabel">Loading...</label>
+        </div>
+    </body>
+</rml>

+ 18 - 0
samples/rocket-console/assets/modenine.nfo

@@ -0,0 +1,18 @@
+ModeNine
+
+Based on Andrew Bulhak's ModeSeven, in turn inspired by the screen
+output of the BBC Micro.
+
+copyright: 
+(C) 1998 Andrew C. Bulhak
+(C) 2001 Graham H Freeman
+
+Freely Distributable. 
+
+All we ask is that this readme file must be included with the font package.
+Impresarios of free font sites and shovelware cd-roms, this means you!
+
+If you think this font is doovy, let us know at [email protected], and we
+might actually make more fonts. 
+
+Another fine Grudnuk Creations produkt | http://grudnuk.com/

BIN
samples/rocket-console/assets/modenine.ttf


BIN
samples/rocket-console/assets/monitor.egg.pz


+ 12 - 0
samples/rocket-console/assets/monitor.txt

@@ -0,0 +1,12 @@
+
+This is an edited version of this file from blendswap.com (http://www.blendswap.com/blends/view/74468).
+
+VERY IMPORTANT LICENSE INFORMATION:
+
+This file has been released by buzo under the following license:
+
+    Creative Commons Zero (Public Domain)
+
+You can use this model for any purposes according to the following conditions:
+
+    There are no requirements for CC-Zero licensed blends.

+ 44 - 0
samples/rocket-console/assets/rkt.rcss

@@ -0,0 +1,44 @@
+/*
+* Default styles for all the basic elements.
+*/
+
+div
+{
+    display: block;
+}
+
+p
+{
+    display: block;
+}
+
+h1
+{
+    display: block;
+}
+
+em
+{
+    font-style: italic;
+}
+
+strong
+{
+    font-weight: bold;
+}
+
+datagrid
+{
+    display: block;
+}
+
+select, dataselect, datacombo
+{
+    text-align: left;
+}
+
+tabset tabs
+{
+    display: block;
+}
+

+ 316 - 0
samples/rocket-console/assets/takeyga_kb.egg

@@ -0,0 +1,316 @@
+<CoordinateSystem> { Z-up } 
+<Material> takeyga_kb {
+  <Scalar> diffr { 0.800000 }
+  <Scalar> diffg { 0.800000 }
+  <Scalar> diffb { 0.800000 }
+  <Scalar> specr { 0.500000 }
+  <Scalar> specg { 0.500000 }
+  <Scalar> specb { 0.500000 }
+  <Scalar> shininess { 12.5 }
+  <Scalar> ambr { 1.000000 }
+  <Scalar> ambg { 1.000000 }
+  <Scalar> ambb { 1.000000 }
+  <Scalar> emitr { 0.000000 }
+  <Scalar> emitg { 0.000000 }
+  <Scalar> emitb { 0.000000 }
+}
+
+<Texture> Texture.001 {
+  "./tex/takeyga_kb_specular.dds"
+  <Scalar> envtype { MODULATE }
+  <Scalar> minfilter { LINEAR_MIPMAP_LINEAR }
+  <Scalar> magfilter { LINEAR_MIPMAP_LINEAR }
+  <Scalar> wrap { REPEAT }
+}
+
+<Texture> Tex {
+  "./tex/takeyga_kb_diffuse.dds"
+  <Scalar> envtype { MODULATE }
+  <Scalar> minfilter { LINEAR_MIPMAP_LINEAR }
+  <Scalar> magfilter { LINEAR_MIPMAP_LINEAR }
+  <Scalar> wrap { REPEAT }
+}
+
+<Texture> Texture {
+  "./tex/takeyga_kb_normal.dds"
+  <Scalar> envtype { NORMAL }
+  <Scalar> minfilter { LINEAR_MIPMAP_LINEAR }
+  <Scalar> magfilter { LINEAR_MIPMAP_LINEAR }
+  <Scalar> wrap { REPEAT }
+}
+
+  <Group> Cube.001 {
+    <Transform> {
+      <Matrix4> {
+        0.08499996364116669 0.0 0.0 0.0 
+        0.0 0.23999987542629242 0.0 0.0 
+        0.0 0.0 0.02500000037252903 0.0 
+        0.0 0.0 0.0 1.0 
+      }
+    }
+    
+    <VertexPool> Cube.001 {
+    
+      <Vertex> 0 {0.082953 0.240000 -0.025000
+        <UV>  {
+          0.158203125 0.595703125 
+        }
+      }
+      <Vertex> 1 {0.082953 -0.240000 -0.025000
+        <UV>  {
+          0.98828125 0.6015625 
+        }
+      }
+      <Vertex> 2 {-0.085000 -0.240000 -0.025000
+        <UV>  {
+          0.98828125 0.986328125 
+        }
+      }
+      <Vertex> 3 {-0.085000 0.240000 -0.025000
+        <UV>  {
+          0.162109375 0.98828125 
+        }
+      }
+      <Vertex> 4 {0.073544 0.227744 -0.005546
+        <UV>  {
+          0.990234375 0.017578125 
+        }
+      }
+      <Vertex> 5 {-0.067932 0.223167 0.018996
+        <UV>  {
+          0.98046875 0.46875 
+        }
+      }
+      <Vertex> 6 {-0.067932 -0.223167 0.018996
+        <UV>  {
+          0.017578125 0.470703125 
+        }
+      }
+      <Vertex> 7 {0.073544 -0.227744 -0.005546
+        <UV>  {
+          0.01953125 0.013671875 
+        }
+      }
+      <Vertex> 8 {0.082372 0.237328 -0.015273
+        <UV>  {
+          0.9794921875 0.552734375 
+        }
+      }
+      <Vertex> 9 {0.073544 0.227744 -0.005546
+        <UV>  {
+          0.9765625 0.57421875 
+        }
+      }
+      <Vertex> 10 {0.073544 -0.227744 -0.005546
+        <UV>  {
+          0.025390625 0.57421875 
+        }
+      }
+      <Vertex> 11 {0.082372 -0.237328 -0.015273
+        <UV>  {
+          0.0234375 0.5537109375 
+        }
+      }
+      <Vertex> 12 {0.082372 -0.237328 -0.015273
+        <UV>  {
+          0.0703125 0.6123046875 
+        }
+      }
+      <Vertex> 13 {0.073544 -0.227744 -0.005546
+        <UV>  {
+          0.095703125 0.615234375 
+        }
+      }
+      <Vertex> 14 {-0.067932 -0.223167 0.018996
+        <UV>  {
+          0.13671875 0.97265625 
+        }
+      }
+      <Vertex> 15 {-0.080884 -0.235006 0.001122
+        <UV>  {
+          0.07421875 0.982421875 
+        }
+      }
+      <Vertex> 16 {-0.080884 -0.235006 0.001122
+        <UV>  {
+          0.0166015625 0.5361328125 
+        }
+      }
+      <Vertex> 17 {-0.067932 -0.223167 0.018996
+        <UV>  {
+          0.017578125 0.58203125 
+        }
+      }
+      <Vertex> 18 {-0.067932 0.223167 0.018996
+        <UV>  {
+          0.978515625 0.580078125 
+        }
+      }
+      <Vertex> 19 {-0.080884 0.235006 0.001122
+        <UV>  {
+          0.984375 0.5341796875 
+        }
+      }
+      <Vertex> 20 {0.082372 0.237328 -0.015273
+        <UV>  {
+          0.0703125 0.6123046875 
+        }
+      }
+      <Vertex> 21 {0.082953 0.240000 -0.025000
+        <UV>  {
+          0.044921875 0.609375 
+        }
+      }
+      <Vertex> 22 {-0.085000 0.240000 -0.025000
+        <UV>  {
+          0.01171875 0.9921875 
+        }
+      }
+      <Vertex> 23 {-0.080884 0.235006 0.001122
+        <UV>  {
+          0.07421875 0.982421875 
+        }
+      }
+      <Vertex> 24 {0.082953 0.240000 -0.025000
+        <UV>  {
+          0.982421875 0.53125 
+        }
+      }
+      <Vertex> 25 {0.082372 0.237328 -0.015273
+        <UV>  {
+          0.9794921875 0.552734375 
+        }
+      }
+      <Vertex> 26 {0.082372 -0.237328 -0.015273
+        <UV>  {
+          0.0234375 0.5537109375 
+        }
+      }
+      <Vertex> 27 {0.082953 -0.240000 -0.025000
+        <UV>  {
+          0.021484375 0.533203125 
+        }
+      }
+      <Vertex> 28 {0.082953 -0.240000 -0.025000
+        <UV>  {
+          0.044921875 0.609375 
+        }
+      }
+      <Vertex> 29 {0.082372 -0.237328 -0.015273
+        <UV>  {
+          0.0703125 0.6123046875 
+        }
+      }
+      <Vertex> 30 {-0.080884 -0.235006 0.001122
+        <UV>  {
+          0.07421875 0.982421875 
+        }
+      }
+      <Vertex> 31 {-0.085000 -0.240000 -0.025000
+        <UV>  {
+          0.01171875 0.9921875 
+        }
+      }
+      <Vertex> 32 {-0.085000 -0.240000 -0.025000
+        <UV>  {
+          0.015625 0.490234375 
+        }
+      }
+      <Vertex> 33 {-0.080884 -0.235006 0.001122
+        <UV>  {
+          0.0166015625 0.5361328125 
+        }
+      }
+      <Vertex> 34 {-0.080884 0.235006 0.001122
+        <UV>  {
+          0.984375 0.5341796875 
+        }
+      }
+      <Vertex> 35 {-0.085000 0.240000 -0.025000
+        <UV>  {
+          0.990234375 0.48828125 
+        }
+      }
+      <Vertex> 36 {0.073544 0.227744 -0.005546
+        <UV>  {
+          0.095703125 0.615234375 
+        }
+      }
+      <Vertex> 37 {0.082372 0.237328 -0.015273
+        <UV>  {
+          0.0703125 0.6123046875 
+        }
+      }
+      <Vertex> 38 {-0.080884 0.235006 0.001122
+        <UV>  {
+          0.07421875 0.982421875 
+        }
+      }
+      <Vertex> 39 {-0.067932 0.223167 0.018996
+        <UV>  {
+          0.13671875 0.97265625 
+        }
+      }}
+    
+    
+    <Polygon> {
+      <TRef> { Tex }
+      <MRef> { takeyga_kb }
+      <Normal> {0.000000 0.000000 -1.000000}
+      <VertexRef> { 0 1 2 3 <Ref> { Cube.001 }} 
+    }
+    <Polygon> {
+      <TRef> { Tex }
+      <MRef> { takeyga_kb }
+      <Normal> {0.508027 -0.000000 0.861341}
+      <VertexRef> { 4 5 6 7 <Ref> { Cube.001 }} 
+    }
+    <Polygon> {
+      <TRef> { Tex }
+      <MRef> { takeyga_kb }
+      <Normal> {0.966167 -0.000000 0.257917}
+      <VertexRef> { 8 9 10 11 <Ref> { Cube.001 }} 
+    }
+    <Polygon> {
+      <TRef> { Tex }
+      <MRef> { takeyga_kb }
+      <Normal> {0.028240 -0.996449 0.079322}
+      <VertexRef> { 12 13 14 15 <Ref> { Cube.001 }} 
+    }
+    <Polygon> {
+      <TRef> { Tex }
+      <MRef> { takeyga_kb }
+      <Normal> {-0.978036 0.000000 0.208436}
+      <VertexRef> { 16 17 18 19 <Ref> { Cube.001 }} 
+    }
+    <Polygon> {
+      <TRef> { Tex }
+      <MRef> { takeyga_kb }
+      <Normal> {0.001260 0.999752 0.022233}
+      <VertexRef> { 20 21 22 23 <Ref> { Cube.001 }} 
+    }
+    <Polygon> {
+      <TRef> { Tex }
+      <MRef> { takeyga_kb }
+      <Normal> {0.999846 -0.000000 0.017550}
+      <VertexRef> { 24 25 26 27 <Ref> { Cube.001 }} 
+    }
+    <Polygon> {
+      <TRef> { Tex }
+      <MRef> { takeyga_kb }
+      <Normal> {0.001259 -0.999752 0.022233}
+      <VertexRef> { 28 29 30 31 <Ref> { Cube.001 }} 
+    }
+    <Polygon> {
+      <TRef> { Tex }
+      <MRef> { takeyga_kb }
+      <Normal> {-0.998928 0.000000 0.046291}
+      <VertexRef> { 32 33 34 35 <Ref> { Cube.001 }} 
+    }
+    <Polygon> {
+      <TRef> { Tex }
+      <MRef> { takeyga_kb }
+      <Normal> {0.028241 0.996449 0.079322}
+      <VertexRef> { 36 37 38 39 <Ref> { Cube.001 }} 
+    }
+  }

BIN
samples/rocket-console/assets/tex/takeyga_kb_diffuse.dds


BIN
samples/rocket-console/assets/tex/takeyga_kb_normal.dds


BIN
samples/rocket-console/assets/tex/takeyga_kb_specular.dds


+ 56 - 0
samples/rocket-console/assets/window.rcss

@@ -0,0 +1,56 @@
+body
+{
+    font-family: "MODENINE";
+    font-weight: normal;
+    font-style: normal;
+    font-size: 15;
+
+}
+
+body.window
+{
+    padding-top: 43px;
+    padding-bottom: 20px;
+
+    min-width: 250px;
+
+    min-height: 135px;
+    max-height: 700px;
+
+}
+
+
+
+div#title_bar
+{
+    z-index: 1;
+
+    position: absolute;
+    top: 0px;
+    left: 0px;
+
+    text-align: center;
+
+    color: #fff;
+    background-color: #22f;
+}
+
+
+div#title_bar span
+{
+    padding-top: 17px;
+    padding-bottom: 48px;
+
+    font-size: 32;
+    font-weight: bold;
+
+    outline-font-effect: outline;
+    outline-width: 1px;
+    outline-color: black;
+}
+
+div#title_bar_content
+{
+    text-align: center;
+    color: #cff;
+}

+ 42 - 0
samples/rocket-console/assets/window.rml

@@ -0,0 +1,42 @@
+<template name="window" content="content">
+    <head>
+        <link type="text/rcss" href="rkt.rcss"/>
+        <link type="text/rcss" href="window.rcss"/>
+
+
+        <script>
+import _rocketcore as rocket
+
+def OnLoad(document):
+    print "Rocket document loaded"
+
+# event handlers from templates can be overridden in windows using them
+def OnKeyDown(event, document):
+    keyId = event.parameters['key_identifier']
+    print "Base keydown: unhandled key ",keyId
+
+        </script>
+    </head>
+
+
+    <body class="window" onload='OnLoad(document)' onkeydown='OnKeyDown(event, document)'>
+        <div id="title_bar">
+            <handle move_target="#document">
+            <span id="title">Rocket Sample</span>
+
+                <div id="title_bar_content">
+                </div>
+            </handle>
+        </div>
+        <div id="window">
+            <div id="content">
+            </div>
+        </div>
+
+        <!-- drag and drop of window -->
+        <handle size_target="#document"
+             style="position: absolute; width: 16px; height: 16px; bottom: 0px; right: 0px;">
+        </handle>
+    </body>
+
+</template>

+ 153 - 0
samples/rocket-console/console.py

@@ -0,0 +1,153 @@
+"""
+Simple console widget for rocket
+"""
+import sys, os.path
+
+# workaround: https://www.panda3d.org/forums/viewtopic.php?t=10062&p=99697#p99054
+#from panda3d import rocket
+import _rocketcore as rocket
+
+from panda3d.rocket import RocketRegion, RocketInputHandler
+
+class Console(object):
+    def __init__(self, base, context, cols, rows, commandHandler):
+        self.base = base
+
+        self.context = context
+        self.loadFonts()
+        self.cols = cols
+        self.rows = rows
+        self.commandHandler = commandHandler
+
+        self.setupConsole()
+        self.allowEditing(True)
+
+    def getTextContainer(self):
+        return self.textEl
+
+    def setPrompt(self, prompt):
+        self.consolePrompt = prompt
+
+    def allowEditing(self, editMode):
+        self.editMode = editMode
+        if editMode:
+            self.input = ""
+            if not self.lastLine:
+                self.addLine("")
+            self.newEditLine()
+
+    def loadFonts(self):
+        rocket.LoadFontFace("Perfect DOS VGA 437.ttf")
+
+    def setupConsole(self):
+        self.document = self.context.LoadDocument("console.rml")
+        if not self.document:
+            raise AssertionError("did not find console.rml")
+
+        el = self.document.GetElementById('content')
+
+        self.textEl = el
+
+        # roundabout way of accessing the current object through rocket event...
+
+        # add attribute to let Rocket know about the receiver
+        self.context.console = self
+
+        # then reference through the string format (dunno how else to get the event...)
+        self.document.AddEventListener(
+            'keydown', 'document.context.console.handleKeyDown(event)', True)
+        self.document.AddEventListener(
+            'textinput', 'document.context.console.handleTextInput(event)', True)
+
+        self.consolePrompt = "C:\\>"
+
+        self.input = ""
+        self.lastLine = None
+
+        self.blinkState = False
+        self.queueBlinkCursor()
+
+        self.document.Show()
+
+    def queueBlinkCursor(self):
+        self.base.taskMgr.doMethodLater(0.2, self.blinkCursor, 'blinkCursor')
+
+    def blinkCursor(self, task):
+        self.blinkState = not self.blinkState
+        if self.editMode:
+            self.updateEditLine(self.input)
+        self.queueBlinkCursor()
+
+    def escape(self, text):
+        return text. \
+                replace('<', '&lt;'). \
+                replace('>', '&gt;'). \
+                replace('"', '&quot;')
+
+    def addLine(self, text):
+        curKids = list(self.textEl.child_nodes)
+        while len(curKids) >= self.rows:
+            self.textEl.RemoveChild(curKids[0])
+            curKids = curKids[1:]
+
+        line = self.document.CreateTextNode(self.escape(text) + '\n')
+        self.textEl.AppendChild(line)
+        self.lastLine = line
+
+    def addLines(self, lines):
+        for line in lines:
+            self.addLine(line)
+
+    def updateEditLine(self, newInput=''):
+        newText = self.consolePrompt + newInput
+        self.lastLine.text = self.escape(newText) + (self.blinkState and '_' or '')
+        self.input = newInput
+
+    def scroll(self):
+        self.blinkState = False
+        self.updateEditLine(self.input + '\n')
+
+    def handleKeyDown(self, event):
+        """
+        Handle control keys
+        """
+        keyId = event.parameters['key_identifier']
+        if not self.editMode:
+            if keyId == rocket.key_identifier.PAUSE:
+                if event.parameters['ctrl_key']:
+                    self.commandHandler(None)
+
+            return
+
+        if keyId == rocket.key_identifier.RETURN:
+            # emit line without cursor
+            self.scroll()
+
+            # handle command
+            self.commandHandler(self.input)
+
+            if self.editMode:
+                # start with new "command"
+                self.addLine(self.consolePrompt)
+                self.updateEditLine("")
+
+        elif keyId == rocket.key_identifier.BACK:
+            self.updateEditLine(self.input[0:-1])
+
+    def handleTextInput(self, event):
+        if not self.editMode:
+            return
+
+        # handle normal text character
+        data = event.parameters['data']
+        if 32 <= data < 128:
+            self.updateEditLine(self.input + chr(data))
+
+    def newEditLine(self):
+        self.addLine("")
+        self.updateEditLine()
+
+    def cls(self):
+        curKids = list(self.textEl.child_nodes)
+        for kid in curKids:
+            self.textEl.RemoveChild(kid)

+ 410 - 0
samples/rocket-console/main.py

@@ -0,0 +1,410 @@
+"""
+Show how to use libRocket in Panda3D.
+"""
+import sys
+from panda3d.core import loadPrcFile, loadPrcFileData, Point3,Vec4, Mat4, LoaderOptions  # @UnusedImport
+from panda3d.core import DirectionalLight, AmbientLight, PointLight
+from panda3d.core import Texture, PNMImage
+from panda3d.core import PandaSystem
+import random
+from direct.interval.LerpInterval import LerpHprInterval, LerpPosInterval, LerpFunc
+from direct.showbase.ShowBase import ShowBase
+
+# workaround: https://www.panda3d.org/forums/viewtopic.php?t=10062&p=99697#p99054
+#from panda3d import rocket
+import _rocketcore as rocket
+
+from panda3d.rocket import RocketRegion, RocketInputHandler
+
+loadPrcFileData("", "model-path $MAIN_DIR/assets")
+
+import console
+
+global globalClock
+
+class MyApp(ShowBase):
+
+    def __init__(self):
+        ShowBase.__init__(self)
+
+        self.win.setClearColor(Vec4(0.2, 0.2, 0.2, 1))
+
+        self.disableMouse()
+
+        self.render.setShaderAuto()
+
+        dlight = DirectionalLight('dlight')
+        alight = AmbientLight('alight')
+        dlnp = self.render.attachNewNode(dlight)
+        alnp = self.render.attachNewNode(alight)
+        dlight.setColor((0.8, 0.8, 0.5, 1))
+        alight.setColor((0.2, 0.2, 0.2, 1))
+        dlnp.setHpr(0, -60, 0)
+        self.render.setLight(dlnp)
+        self.render.setLight(alnp)
+
+        # Put lighting on the main scene
+        plight = PointLight('plight')
+        plnp = self.render.attachNewNode(plight)
+        plnp.setPos(0, 0, 10)
+        self.render.setLight(plnp)
+        self.render.setLight(alnp)
+
+        self.loadRocketFonts()
+
+        self.loadingTask = None
+
+        #self.startModelLoadingAsync()
+        self.startModelLoading()
+
+        self.inputHandler = RocketInputHandler()
+        self.mouseWatcher.attachNewNode(self.inputHandler)
+
+        self.openLoadingDialog()
+
+    def loadRocketFonts(self):
+        """ Load fonts referenced from e.g. 'font-family' RCSS directives.
+
+        Note: the name of the font as used in 'font-family'
+        is not always the same as the filename;
+        open the font in your OS to see its display name.
+        """
+        rocket.LoadFontFace("modenine.ttf")
+
+
+    def startModelLoading(self):
+        self.monitorNP = None
+        self.keyboardNP = None
+        self.loadingError = False
+
+        self.taskMgr.doMethodLater(1, self.loadModels, 'loadModels')
+
+    def loadModels(self, task):
+        self.monitorNP = self.loader.loadModel("monitor")
+        self.keyboardNP = self.loader.loadModel("takeyga_kb")
+
+    def startModelLoadingAsync(self):
+        """
+        NOTE: this seems to invoke a few bugs (crashes, sporadic model
+        reading errors, etc) so is disabled for now...
+        """
+        self.monitorNP = None
+        self.keyboardNP = None
+        self.loadingError = False
+
+        # force the "loading" to take some time after the first run...
+        options = LoaderOptions()
+        options.setFlags(options.getFlags() | LoaderOptions.LFNoCache)
+
+        def gotMonitorModel(model):
+            if not model:
+                self.loadingError = True
+            self.monitorNP = model
+
+        self.loader.loadModel("monitor", loaderOptions=options, callback=gotMonitorModel)
+
+        def gotKeyboardModel(model):
+            if not model:
+                self.loadingError = True
+            self.keyboardNP = model
+
+        self.loader.loadModel("takeyga_kb", loaderOptions=options, callback=gotKeyboardModel)
+
+    def openLoadingDialog(self):
+        self.userConfirmed = False
+
+        self.windowRocketRegion = RocketRegion.make('pandaRocket', self.win)
+        self.windowRocketRegion.setActive(1)
+
+        self.windowRocketRegion.setInputHandler(self.inputHandler)
+
+        self.windowContext = self.windowRocketRegion.getContext()
+
+        self.loadingDocument = self.windowContext.LoadDocument("loading.rml")
+        if not self.loadingDocument:
+            raise AssertionError("did not find loading.rml")
+
+        self.loadingDots = 0
+        el = self.loadingDocument.GetElementById('loadingLabel')
+        self.loadingText = el.first_child
+        self.stopLoadingTime = globalClock.getFrameTime() + 3
+        self.loadingTask = self.taskMgr.add(self.cycleLoading, 'doc changer')
+
+
+        # note: you may encounter errors like 'KeyError: 'document'"
+        # when invoking events using methods from your own scripts with this
+        # obvious code:
+        #
+        # self.loadingDocument.AddEventListener('aboutToClose',
+        #                                       self.onLoadingDialogDismissed, True)
+        #
+        # A workaround is to define callback methods in standalone Python
+        # files with event, self, and document defined to None.
+        #
+        # see https://www.panda3d.org/forums/viewtopic.php?f=4&t=16412
+        #
+
+        # Or, use this indirection technique to work around the problem,
+        # by publishing the app into the context, then accessing it through
+        # the document's context...
+
+        self.windowContext.app = self
+        self.loadingDocument.AddEventListener('aboutToClose',
+                                              'document.context.app.handleAboutToClose()', True)
+
+        self.loadingDocument.Show()
+
+    def handleAboutToClose(self):
+        self.userConfirmed = True
+        if self.monitorNP and self.keyboardNP:
+            self.onLoadingDialogDismissed()
+
+    def attachCustomRocketEvent(self, document, rocketEventName, pandaHandler, once=False):
+        # handle custom event
+
+        # note: you may encounter errors like 'KeyError: 'document'"
+        # when invoking events using methods from your own scripts with this
+        # obvious code:
+        #
+        # self.loadingDocument.AddEventListener('aboutToClose',
+        #                                       self.onLoadingDialogDismissed, True)
+        #
+        # see https://www.panda3d.org/forums/viewtopic.php?f=4&t=16412
+
+
+        # this technique converts Rocket events to Panda3D events
+
+        pandaEvent = 'panda.' + rocketEventName
+
+        document.AddEventListener(
+            rocketEventName,
+            "messenger.send('" + pandaEvent + "', [event])")
+
+        if once:
+            self.acceptOnce(pandaEvent, pandaHandler)
+        else:
+            self.accept(pandaEvent, pandaHandler)
+
+
+    def cycleLoading(self, task):
+        """
+        Update the "loading" text in the initial window until
+        the user presses Space, Enter, or Escape or clicks (see loading.rxml)
+        or sufficient time has elapsed (self.stopLoadingTime).
+        """
+        text = self.loadingText
+
+        now = globalClock.getFrameTime()
+        if self.monitorNP and self.keyboardNP:
+            text.text = "Ready"
+            if now > self.stopLoadingTime or self.userConfirmed:
+                self.onLoadingDialogDismissed()
+                return task.done
+        elif self.loadingError:
+            text.text = "Assets not found"
+        else:
+            count = 5
+            intv = int(now * 4) % count  # @UndefinedVariable
+            text.text = "Loading" + ("." * (1+intv)) + (" " * (2 - intv))
+
+        return task.cont
+
+    def onLoadingDialogDismissed(self):
+        """ Once a models are loaded, stop 'loading' and proceed to 'start' """
+        if self.loadingDocument:
+            if self.loadingTask:
+                self.taskMgr.remove(self.loadingTask)
+            self.loadingTask = None
+
+            self.showStarting()
+
+    def fadeOut(self, element, time):
+        """ Example updating RCSS attributes from code
+        by modifying the 'color' RCSS attribute to slowly
+        change from solid to transparent.
+
+        element: the Rocket element whose style to modify
+        time: time in seconds for fadeout
+        """
+
+        # get the current color from RCSS effective style
+        color = element.style.color
+        # convert to RGBA form
+        prefix = color[:color.rindex(',')+1].replace('rgb(', 'rgba(')
+
+        def updateAlpha(t):
+            # another way of setting style on a specific element
+            attr = 'color: ' + prefix + str(int(t)) +');'
+            element.SetAttribute('style', attr)
+
+        alphaInterval = LerpFunc(updateAlpha,
+                             duration=time,
+                             fromData=255,
+                             toData=0,
+                             blendType='easeIn')
+
+        return alphaInterval
+
+    def showStarting(self):
+        """ Models are loaded, so update the dialog,
+        fade out, then transition to the console. """
+        self.loadingText.text = 'Starting...'
+
+        alphaInterval = self.fadeOut(self.loadingText, 0.5)
+        alphaInterval.setDoneEvent('fadeOutFinished')
+
+        def fadeOutFinished():
+            if self.loadingDocument:
+                self.loadingDocument.Close()
+                self.loadingDocument = None
+                self.createConsole()
+
+        self.accept('fadeOutFinished', fadeOutFinished)
+
+        alphaInterval.start()
+
+    def createConsole(self):
+        """ Create the in-world console, which displays
+        a RocketRegion in a GraphicsBuffer, which appears
+        in a Texture on the monitor model. """
+
+        self.monitorNP.reparentTo(self.render)
+        self.monitorNP.setScale(1.5)
+
+        self.keyboardNP.reparentTo(self.render)
+        self.keyboardNP.setHpr(-90, 0, 15)
+        self.keyboardNP.setScale(20)
+
+        self.placeItems()
+
+        self.setupRocketConsole()
+
+        # re-enable mouse
+        mat=Mat4(self.camera.getMat())
+        mat.invertInPlace()
+        self.mouseInterfaceNode.setMat(mat)
+        self.enableMouse()
+
+    def placeItems(self):
+        self.camera.setPos(0, -20, 0)
+        self.camera.setHpr(0, 0, 0)
+        self.monitorNP.setPos(0, 0, 1)
+        self.keyboardNP.setPos(0, -5, -2.5)
+
+
+    def setupRocketConsole(self):
+        """
+        Place a new rocket window onto a texture
+        bound to the front of the monitor.
+        """
+        self.win.setClearColor(Vec4(0.5, 0.5, 0.8, 1))
+
+        faceplate = self.monitorNP.find("**/Faceplate")
+        assert faceplate
+
+        mybuffer = self.win.makeTextureBuffer("Console Buffer", 1024, 512)
+        tex = mybuffer.getTexture()
+        tex.setMagfilter(Texture.FTLinear)
+        tex.setMinfilter(Texture.FTLinear)
+
+        faceplate.setTexture(tex, 1)
+
+        self.rocketConsole = RocketRegion.make('console', mybuffer)
+        self.rocketConsole.setInputHandler(self.inputHandler)
+
+        self.consoleContext = self.rocketConsole.getContext()
+        self.console = console.Console(self, self.consoleContext, 40, 13, self.handleCommand)
+
+        self.console.addLine("Panda DOS")
+        self.console.addLine("type 'help'")
+        self.console.addLine("")
+
+        self.console.allowEditing(True)
+
+    def handleCommand(self, command):
+        if command is None:
+            # hack for Ctrl-Break
+            self.spewInProgress = False
+            self.console.addLine("*** break ***")
+            self.console.allowEditing(True)
+            return
+
+        command = command.strip()
+        if not command:
+            return
+
+        tokens = [x.strip() for x in command.split(' ')]
+        command = tokens[0].lower()
+
+        if command == 'help':
+            self.console.addLines([
+                "Sorry, this is utter fakery.",
+                "You won't get much more",
+                "out of this simulation unless",
+                "you program it yourself. :)"
+            ])
+        elif command == 'dir':
+            self.console.addLines([
+                "Directory of C:\\:",
+                "HELP     COM    72 05-06-2015 14:07",
+                "DIR      COM   121 05-06-2015 14:11",
+                "SPEW     COM   666 05-06-2015 15:02",
+                "   2 Files(s)  859 Bytes.",
+                "   0 Dirs(s)  7333 Bytes free.",
+                ""])
+        elif command == 'cls':
+            self.console.cls()
+        elif command == 'echo':
+            self.console.addLine(' '.join(tokens[1:]))
+        elif command == 'ver':
+            self.console.addLine('Panda DOS v0.01 in Panda3D ' + PandaSystem.getVersionString())
+        elif command == 'spew':
+            self.startSpew()
+        elif command == 'exit':
+            self.console.setPrompt("System is shutting down NOW!")
+            self.terminateMonitor()
+        else:
+            self.console.addLine("command not found")
+
+    def startSpew(self):
+        self.console.allowEditing(False)
+        self.console.addLine("LINE NOISE 1.0")
+        self.console.addLine("")
+
+        self.spewInProgress = True
+
+        # note: spewage always occurs in 'doMethodLater';
+        # time.sleep() would be pointless since the whole
+        # UI would be frozen during the wait.
+        self.queueSpew(2)
+
+    def queueSpew(self, delay=0.1):
+        self.taskMgr.doMethodLater(delay, self.spew, 'spew')
+
+    def spew(self, task):
+        # generate random spewage, just like on TV!
+        if not self.spewInProgress:
+            return
+
+        def randchr():
+            return chr(int(random.random() < 0.25 and 32 or random.randint(32, 127)))
+
+        line = ''.join([randchr() for _ in range(40) ])
+
+        self.console.addLine(line)
+        self.queueSpew()
+
+    def terminateMonitor(self):
+        alphaInterval = self.fadeOut(self.console.getTextContainer(), 2)
+
+        alphaInterval.setDoneEvent('fadeOutFinished')
+
+        def fadeOutFinished():
+            sys.exit(0)
+
+        self.accept('fadeOutFinished', fadeOutFinished)
+
+        alphaInterval.start()
+
+app = MyApp()
+app.run()