Browse Source

Support coroutines and async/await in the task manager and loader

rdb 8 years ago
parent
commit
0c0f9adab9

+ 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')

+ 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

@@ -3616,6 +3616,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)
@@ -4209,6 +4210,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();
 

+ 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();