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
 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
 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
 
     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.gotList = gotList
             self.callback = callback
             self.extraArgs = extraArgs
-            self.numRemaining = numObjects
-            self.cancelled = False
             self.requests = set()
+            self.requestList = []
 
         def gotObject(self, 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
     def __init__(self, base):
         self.base = base
         self.loader = PandaLoader.getGlobalPtr()
 
-        self.__requests = {}
+        self._requests = {}
 
         self.hook = "async_loader_%s" % (Loader.loaderIndex)
         Loader.loaderIndex += 1
@@ -180,7 +252,7 @@ class Loader(DirectObject):
             # requested models have been loaded, we'll invoke the
             # 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
             for modelPath in modelList:
                 request = self.loader.makeAsyncRequest(Filename(modelPath), loaderOptions)
@@ -189,26 +261,26 @@ class Loader(DirectObject):
                 request.setDoneEvent(self.hook)
                 self.loader.loadAsync(request)
                 cb.requests.add(request)
-                self.__requests[request] = (cb, i)
+                cb.requestList.append(request)
+                self._requests[request] = (cb, i)
                 i += 1
             return cb
 
     def cancelRequest(self, cb):
         """Cancels an aysynchronous loading or flatten request issued
         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):
         """ Returns true if an asynchronous loading or flatten request
         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)
 
@@ -344,7 +416,7 @@ class Loader(DirectObject):
             # requested models have been saved, we'll invoke the
             # 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
             for modelPath, node in modelList:
                 request = self.loader.makeAsyncSaveRequest(Filename(modelPath), loaderOptions, node)
@@ -353,7 +425,8 @@ class Loader(DirectObject):
                 request.setDoneEvent(self.hook)
                 self.loader.saveAsync(request)
                 cb.requests.add(request)
-                self.__requests[request] = (cb, i)
+                cb.requestList.append(request)
+                self._requests[request] = (cb, i)
                 i += 1
             return cb
 
@@ -880,13 +953,14 @@ class Loader(DirectObject):
             # requested sounds have been loaded, we'll invoke the
             # 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):
                 request = AudioLoadRequest(manager, soundPath, positional)
                 request.setDoneEvent(self.hook)
                 self.loader.loadAsync(request)
                 cb.requests.add(request)
-                self.__requests[request] = (cb, i)
+                cb.requestList.append(request)
+                self._requests[request] = (cb, i)
             return cb
 
     def unloadSfx(self, sfx):
@@ -944,14 +1018,15 @@ class Loader(DirectObject):
             callback = self.__asyncFlattenDone
             gotList = True
 
-        cb = Loader.Callback(len(modelList), gotList, callback, extraArgs)
+        cb = Loader.Callback(self, len(modelList), gotList, callback, extraArgs)
         i = 0
         for model in modelList:
             request = ModelFlattenRequest(model.node())
             request.setDoneEvent(self.hook)
             self.loader.loadAsync(request)
             cb.requests.add(request)
-            self.__requests[request] = (cb, i)
+            cb.requestList.append(request)
+            self._requests[request] = (cb, i)
             i += 1
         return cb
 
@@ -980,36 +1055,22 @@ class Loader(DirectObject):
         of loaded objects, and call the appropriate callback when it's
         time."""
 
-        if request not in self.__requests:
+        if request not in self._requests:
             return
 
-        cb, i = self.__requests[request]
-        if cb.cancelled:
+        cb, i = self._requests[request]
+        if cb.cancelled():
             # Shouldn't be here.
-            del self.__requests[request]
+            del self._requests[request]
             return
 
         cb.requests.discard(request)
         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
-    cancel_request = cancelRequest
-    is_request_pending = isRequestPending
     unload_model = unloadModel
     save_model = saveModel
     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
         to the task manager), or a callable function object.  If this
         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
         are passing in a Task object that already has a name.
@@ -385,6 +386,15 @@ class TaskManager:
             task = funcOrTask
         elif hasattr(funcOrTask, '__call__'):
             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:
             self.notify.error(
                 '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__") {
-      if (_has_this && _parameters.size() == 1 &&
+      if ((int)_parameters.size() == first_param &&
           TypeManager::is_pointer(_return_type->get_new_type())) {
         // It receives no parameters, and returns a pointer.
         _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 ()") {
     def._answer_location = "tp_call";
     def._wrapper_type = WT_none;
@@ -2798,6 +2816,20 @@ write_module_class(ostream &out, Object *obj) {
     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.
   out << "struct Dtool_PyTypedObject Dtool_" << ClassName << " = {\n";
   out << "  {\n";
@@ -2819,7 +2851,13 @@ write_module_class(ostream &out, Object *obj) {
   write_function_slot(out, 4, slots, "tp_setattr");
 
   // 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 << "#else\n";
   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)");
     }
 
+    if (PyType_Ready(&Dtool_GeneratorWrapper_Type) < 0) {
+      return Dtool_Raise_TypeError("PyType_Ready(Dtool_GeneratorWrapper)");
+    }
+
     if (PyType_Ready(&Dtool_StaticProperty_Type) < 0) {
       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 = {
   Dtool_SequenceWrapper_length,
   0, // sq_concat
@@ -1454,4 +1465,66 @@ PyTypeObject Dtool_SeqMapWrapper_Type = {
 #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

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

@@ -495,9 +495,15 @@ struct Dtool_SeqMapWrapper {
   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_MappingWrapper_Type;
 EXPCL_INTERROGATEDB extern PyTypeObject Dtool_SeqMapWrapper_Type;
+EXPCL_INTERROGATEDB extern PyTypeObject Dtool_GeneratorWrapper_Type;
 EXPCL_INTERROGATEDB extern PyTypeObject Dtool_StaticProperty_Type;
 
 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_Repr(...);
   EXPCL_PYSTUB int PyObject_RichCompareBool(...);
+  EXPCL_PYSTUB int PyObject_SelfIter(...);
   EXPCL_PYSTUB int PyObject_SetAttrString(...);
   EXPCL_PYSTUB int PyObject_Str(...);
   EXPCL_PYSTUB int PyObject_Type(...);
@@ -336,6 +337,7 @@ int PyObject_IsInstance(...) { return 0; }
 int PyObject_IsTrue(...) { return 0; }
 int PyObject_Repr(...) { return 0; }
 int PyObject_RichCompareBool(...) { return 0; }
+int PyObject_SelfIter(...) { return 0; }
 int PyObject_SetAttrString(...) { return 0; }
 int PyObject_Str(...) { 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')
 
   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')
   IGATEFILES=GetDirectoryContents('panda/src/event', ["*.h", "*_composite*.cxx"])
   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='p3putil_ext_composite.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='p3gobj_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::
 get_sound() const {
   nassertr(_is_ready, NULL);
   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 AudioSound *get_sound() const;
 
+  INLINE AudioSound *result() const;
+
 protected:
   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);
 
   // 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 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;
 
-
-// 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::_distance_factor = 3.28;
+PN_stdfloat FmodAudioManager::_distance_factor = 1;
 PN_stdfloat FmodAudioManager::_drop_off_factor = 1;
 
 
@@ -100,6 +94,8 @@ FmodAudioManager() {
   _up.y = 0;
   _up.z = 0;
 
+  _active = true;
+
   _saved_outputtype = FMOD_OUTPUTTYPE_AUTODETECT;
 
   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 *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 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.z = 0;
 
+  _min_dist = 1.0;
+  _max_dist = 1000000000.0;
+
   // Play Rate Variable
   _playrate = 1;
 

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

@@ -97,7 +97,7 @@ OpenALAudioManager() {
   _is_valid = true;
 
   // Init 3D attributes
-  _distance_factor = 3.28;
+  _distance_factor = 1;
   _drop_off_factor = 1;
 
   _position[0] = 0;
@@ -715,12 +715,11 @@ audio_3d_get_listener_attributes(PN_stdfloat *px, PN_stdfloat *py, PN_stdfloat *
   *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::
 audio_3d_set_distance_factor(PN_stdfloat factor) {
@@ -732,7 +731,7 @@ audio_3d_set_distance_factor(PN_stdfloat factor) {
   alGetError(); // clear errors
 
   if (_distance_factor>0) {
-    alSpeedOfSound(1126.3*_distance_factor);
+    alSpeedOfSound(343.3*_distance_factor);
     al_audio_errcheck("alSpeedOfSound()");
     // resets the doppler factor to the correct setting in case it was set to
     // 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::
 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 *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 PN_stdfloat audio_3d_get_distance_factor() const;
 

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

@@ -48,7 +48,7 @@ OpenALAudioSound(OpenALAudioManager* manager,
   _balance(0),
   _play_rate(1.0),
   _positional(positional),
-  _min_dist(3.28f),
+  _min_dist(1.0f),
   _max_dist(1000000000.0f),
   _drop_off_factor(1.0f),
   _length(0.0),
@@ -673,7 +673,7 @@ set_3d_min_distance(PN_stdfloat dist) {
     _manager->make_current();
 
     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)");
   }
 }
@@ -698,7 +698,7 @@ set_3d_max_distance(PN_stdfloat dist) {
     _manager->make_current();
 
     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)");
   }
 }

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

@@ -13,20 +13,25 @@
 
 #include "bulletDebugNode.h"
 
+#include "cullHandler.h"
+#include "cullTraverser.h"
+#include "cullableObject.h"
 #include "geomLines.h"
 #include "geomVertexData.h"
 #include "geomTriangles.h"
 #include "geomVertexFormat.h"
 #include "geomVertexWriter.h"
 #include "omniBoundingVolume.h"
+#include "pStatTimer.h"
 
 TypeHandle BulletDebugNode::_type_handle;
+PStatCollector BulletDebugNode::_pstat_debug("App:Bullet:DoPhysics:Debug");
 
 /**
  *
  */
 BulletDebugNode::
-BulletDebugNode(const char *name) : GeomNode(name) {
+BulletDebugNode(const char *name) : PandaNode(name), _debug_stale(true) {
 
   _wireframe = true;
   _constraints = true;
@@ -37,40 +42,6 @@ BulletDebugNode(const char *name) : GeomNode(name) {
   set_bounds(bounds);
   set_final(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::
 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);
 
   dg.add_bool(_wireframe);
@@ -464,8 +466,6 @@ make_from_bam(const FactoryParams &params) {
  */
 void BulletDebugNode::
 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);
 
   _wireframe = scan.get_bool();

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

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

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

@@ -55,21 +55,12 @@ INLINE BulletWorld::
  */
 INLINE void BulletWorld::
 set_debug_node(BulletDebugNode *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 "collideMask.h"
+#include "lightMutexHolder.h"
 
 #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_simulation("App:Bullet:DoPhysics:Simulation");
-PStatCollector BulletWorld::_pstat_debug("App:Bullet:DoPhysics:Debug");
 PStatCollector BulletWorld::_pstat_p2b("App:Bullet:DoPhysics:SyncP2B");
 PStatCollector BulletWorld::_pstat_b2p("App:Bullet:DoPhysics:SyncB2P");
 
@@ -127,6 +127,19 @@ get_world_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
   if (_debug) {
-    _pstat_debug.start();
     _debug->sync_b2p(_world);
-    _pstat_debug.stop();
   }
 
   _pstat_physics.stop();

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

@@ -63,7 +63,7 @@ PUBLISHED:
 
   // Debug
   INLINE void set_debug_node(BulletDebugNode *node);
-  INLINE void clear_debug_node();
+  void clear_debug_node();
   INLINE BulletDebugNode *get_debug_node() const;
   INLINE bool has_debug_node() const;
 
@@ -208,7 +208,6 @@ private:
 
   static PStatCollector _pstat_physics;
   static PStatCollector _pstat_simulation;
-  static PStatCollector _pstat_debug;
   static PStatCollector _pstat_p2b;
   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_sleeping:
   case S_active_nested:
+  case S_awaiting:
     return true;
 
   case S_inactive:

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

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

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

@@ -44,6 +44,7 @@ AsyncTaskChain(AsyncTaskManager *manager, const string &name) :
   _frame_sync(false),
   _num_busy_threads(0),
   _num_tasks(0),
+  _num_awaiting_tasks(0),
   _state(S_initial),
   _current_sort(-INT_MAX),
   _pickup_mode(false),
@@ -726,6 +727,13 @@ service_one_task(AsyncTaskChain::AsyncTaskChainThread *thread) {
           }
           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:
           // The task has finished.
           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);
 
+  // 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) {
     _manager->_lock.release();
     task->upon_death(_manager, clean_exit);
@@ -899,7 +921,7 @@ finish_sort_group() {
     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());
 
   _current_sort = -INT_MAX;

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

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

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

@@ -155,6 +155,7 @@ private:
   friend class AsyncTaskChain::AsyncTaskChainThread;
   friend class AsyncTask;
   friend class AsyncTaskSequence;
+  friend class PythonTask;
 };
 
 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
  * @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 "pythonThread.h"
+#include "asyncTaskManager.h"
 
 TypeHandle PythonTask::_type_handle;
 
 #ifndef CPPPARSER
 extern struct Dtool_PyTypedObject Dtool_TypedReferenceCount;
+extern struct Dtool_PyTypedObject Dtool_AsyncTask;
+extern struct Dtool_PyTypedObject Dtool_PythonTask;
 #endif
 
 /**
  *
  */
 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_upon_death(Py_None);
   set_owner(Py_None);
@@ -60,9 +84,24 @@ PythonTask(PyObject *function, const string &name) :
  */
 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(__dict__);
+  Py_XDECREF(_exception);
+  Py_XDECREF(_exc_value);
+  Py_XDECREF(_exc_traceback);
   Py_XDECREF(_generator);
   Py_XDECREF(_owner);
   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
  * 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
  * 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::
-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::
 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.
+    nassertr(_function != nullptr, DS_interrupt);
+
     PyObject *args = get_args();
     result = PythonThread::call_python_func(_function, 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
       // henceforth, instead of calling the function from the top again.
       if (task_cat.is_debug()) {
@@ -423,30 +533,166 @@ do_python_task() {
         Py_DECREF(str);
       }
       _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
+    }
   }
 
-  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);
 
-    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);
-      _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)) {
       // Don't print an error message for SystemExit.  Or rather, make it a
       // debug message.

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

@@ -22,8 +22,8 @@
 #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 {
 PUBLISHED:
@@ -32,16 +32,23 @@ PUBLISHED:
   ALLOC_DELETED_CHAIN(PythonTask);
 
   void set_function(PyObject *function);
-  PyObject *get_function();
+  INLINE PyObject *get_function();
 
   void set_args(PyObject *args, bool append_task);
   PyObject *get_args();
 
   void set_upon_death(PyObject *upon_death);
-  PyObject *get_upon_death();
+  INLINE PyObject *get_upon_death();
 
   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 __delattr__(PyObject *self, PyObject *attr);
@@ -94,6 +101,8 @@ protected:
   virtual void upon_death(AsyncTaskManager *manager, bool clean_exit);
 
 private:
+  static PyObject *gen_next(PyObject *self);
+
   void register_to_owner();
   void unregister_from_owner();
   void call_owner_method(const char *method_name);
@@ -102,12 +111,19 @@ private:
 private:
   PyObject *_function;
   PyObject *_args;
-  bool _append_task;
   PyObject *_upon_death;
   PyObject *_owner;
-  bool _registered_to_owner;
+
+  PyObject *_exception;
+  PyObject *_exc_value;
+  PyObject *_exc_traceback;
 
   PyObject *_generator;
+  PyObject *_future_done;
+
+  bool _append_task;
+  bool _registered_to_owner;
+  mutable bool _retrieved_exception;
 
 public:
   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.
  * When this returns true, you may retrieve the model loaded by calling
- * get_result().
+ * result().
  */
 INLINE bool ModelFlattenRequest::
 is_ready() const {
@@ -47,6 +47,20 @@ is_ready() const {
  */
 INLINE PandaNode *ModelFlattenRequest::
 get_model() const {
-  nassertr(_is_ready, NULL);
+  nassertr(_is_ready, nullptr);
   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 "pandaNode.h"
 #include "pointerTo.h"
+#include "nodePath.h"
 
 /**
  * This class object manages a single asynchronous request to flatten a model.
@@ -38,9 +39,10 @@ PUBLISHED:
   INLINE bool is_ready() const;
   INLINE PandaNode *get_model() const;
 
+  INLINE NodePath result() const;
+
   MAKE_PROPERTY(orig, get_orig);
   MAKE_PROPERTY(ready, is_ready);
-  MAKE_PROPERTY(model, get_model);
 
 protected:
   virtual DoneStatus do_task();

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

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

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

@@ -64,3 +64,13 @@ get_success() const {
   nassertr(_is_ready, false);
   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
  */
 
-#ifndef MODELSAVEREQUEST
-#define MODELSAVEREQUEST
+#ifndef MODELSAVEREQUEST_H
+#define MODELSAVEREQUEST_H
 
 #include "pandabase.h"
 
@@ -46,12 +46,13 @@ PUBLISHED:
   INLINE bool is_ready() const;
   INLINE bool get_success() const;
 
+  INLINE bool result() const;
+
   MAKE_PROPERTY(filename, get_filename);
   MAKE_PROPERTY(options, get_options);
   MAKE_PROPERTY(node, get_node);
   MAKE_PROPERTY(loader, get_loader);
   MAKE_PROPERTY(ready, is_ready);
-  MAKE_PROPERTY(success, get_success);
 
 protected:
   virtual DoneStatus do_task();

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

@@ -31,6 +31,7 @@
 #include "shader.h"
 #include "texture.h"
 #include "shaderBuffer.h"
+#include "extension.h"
 
 /**
  * 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";
       break;
     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;
     case TextureStage::M_replace:
       text << "\t result = tex" << i << ";\n";

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

@@ -25,6 +25,9 @@
 #include "panda_getopt.h"
 #include "preprocess_argv.h"
 #include "graphicsPipeSelection.h"
+#include "asyncTaskManager.h"
+#include "asyncTask.h"
+#include "boundingSphere.h"
 
 // By including checkPandaVersion.h, we guarantee that runtime attempts to run
 // pview will fail if it inadvertently links with the wrong version of
@@ -231,6 +234,111 @@ report_version() {
   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
 main(int argc, char **argv) {
   preprocess_argv(argc, argv);
@@ -383,6 +491,9 @@ main(int argc, char **argv) {
       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.define_key("shift-w", "open a new window", event_W, 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()