diff --git a/direct/src/showbase/Loader.py b/direct/src/showbase/Loader.py index 9edde2a9b7..64fa593940 100644 --- a/direct/src/showbase/Loader.py +++ b/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. - if not cb.cancelled: - cb.cancelled = True - for request in cb.requests: - self.loader.remove(request) - del self.__requests[request] - cb.requests = None + This is now deprecated: call cb.cancel() instead. """ + + 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] + 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() - - 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 diff --git a/direct/src/task/Task.py b/direct/src/task/Task.py index 215b240d16..08baa924a5 100644 --- a/direct/src/task/Task.py +++ b/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') diff --git a/dtool/src/interrogate/functionRemap.cxx b/dtool/src/interrogate/functionRemap.cxx index 5b04209c48..27ec829479 100644 --- a/dtool/src/interrogate/functionRemap.cxx +++ b/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; diff --git a/dtool/src/interrogate/interfaceMakerPythonNative.cxx b/dtool/src/interrogate/interfaceMakerPythonNative.cxx index e5a353eb51..4384a66253 100644 --- a/dtool/src/interrogate/interfaceMakerPythonNative.cxx +++ b/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) { diff --git a/dtool/src/interrogatedb/py_panda.cxx b/dtool/src/interrogatedb/py_panda.cxx index 76c4e7cf1b..6b0c9f93a5 100644 --- a/dtool/src/interrogatedb/py_panda.cxx +++ b/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 diff --git a/dtool/src/interrogatedb/py_panda.h b/dtool/src/interrogatedb/py_panda.h index c341bb6b33..bf6d038382 100644 --- a/dtool/src/interrogatedb/py_panda.h +++ b/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); diff --git a/dtool/src/pystub/pystub.cxx b/dtool/src/pystub/pystub.cxx index 935b3a47b6..90cdf17715 100644 --- a/dtool/src/pystub/pystub.cxx +++ b/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; } diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 696cc33857..ccd3d30bd0 100755 --- a/makepanda/makepanda.py +++ b/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') diff --git a/panda/src/audio/audioLoadRequest.I b/panda/src/audio/audioLoadRequest.I index 21eea901c5..0daf2df619 100644 --- a/panda/src/audio/audioLoadRequest.I +++ b/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; +} diff --git a/panda/src/audio/audioLoadRequest.h b/panda/src/audio/audioLoadRequest.h index 7e5fb3d23b..04b63fcb31 100644 --- a/panda/src/audio/audioLoadRequest.h +++ b/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(); diff --git a/panda/src/event/asyncTask.I b/panda/src/event/asyncTask.I index ae411d3d7c..3b4476afb7 100644 --- a/panda/src/event/asyncTask.I +++ b/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: diff --git a/panda/src/event/asyncTask.h b/panda/src/event/asyncTask.h index 97176df0a3..b50a4a6e54 100644 --- a/panda/src/event/asyncTask.h +++ b/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 _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; diff --git a/panda/src/event/asyncTaskChain.cxx b/panda/src/event/asyncTaskChain.cxx index bd5c391857..46975efb7c 100644 --- a/panda/src/event/asyncTaskChain.cxx +++ b/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::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; diff --git a/panda/src/event/asyncTaskChain.h b/panda/src/event/asyncTaskChain.h index 4f98ef284a..3dded59317 100644 --- a/panda/src/event/asyncTaskChain.h +++ b/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; diff --git a/panda/src/event/asyncTaskManager.h b/panda/src/event/asyncTaskManager.h index 967476c4af..a92a46b141 100644 --- a/panda/src/event/asyncTaskManager.h +++ b/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) { diff --git a/panda/src/event/asyncTask_ext.cxx b/panda/src/event/asyncTask_ext.cxx new file mode 100755 index 0000000000..bbfb46d2c1 --- /dev/null +++ b/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:: +__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 diff --git a/panda/src/event/asyncTask_ext.h b/panda/src/event/asyncTask_ext.h new file mode 100755 index 0000000000..89fd4cb8f4 --- /dev/null +++ b/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 : public ExtensionBase { +public: + static PyObject *__await__(PyObject *self); + static PyObject *__iter__(PyObject *self) { return __await__(self); } +}; + +#endif // HAVE_PYTHON + +#endif // ASYNCTASK_EXT_H diff --git a/panda/src/event/pythonTask.I b/panda/src/event/pythonTask.I index b2f9a0ef07..7280b4e7d6 100644 --- a/panda/src/event/pythonTask.I +++ b/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); +} diff --git a/panda/src/event/pythonTask.cxx b/panda/src/event/pythonTask.cxx index 3ca6178803..1b4adc0921 100644 --- a/panda/src/event/pythonTask.cxx +++ b/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; - if (_generator == (PyObject *)NULL) { + // 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 == 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. diff --git a/panda/src/event/pythonTask.h b/panda/src/event/pythonTask.h index d97be5795e..59c3c65c5f 100644 --- a/panda/src/event/pythonTask.h +++ b/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() { diff --git a/panda/src/pgraph/modelFlattenRequest.I b/panda/src/pgraph/modelFlattenRequest.I index 248d122c40..84e58744da 100644 --- a/panda/src/pgraph/modelFlattenRequest.I +++ b/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(); + } +} diff --git a/panda/src/pgraph/modelFlattenRequest.h b/panda/src/pgraph/modelFlattenRequest.h index 5716635f82..08a6e72901 100644 --- a/panda/src/pgraph/modelFlattenRequest.h +++ b/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(); diff --git a/panda/src/pgraph/modelLoadRequest.I b/panda/src/pgraph/modelLoadRequest.I index 7392194510..2b8ef41544 100644 --- a/panda/src/pgraph/modelLoadRequest.I +++ b/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 diff --git a/panda/src/pgraph/modelLoadRequest.h b/panda/src/pgraph/modelLoadRequest.h index 00f55e8052..423a807c38 100644 --- a/panda/src/pgraph/modelLoadRequest.h +++ b/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(); diff --git a/panda/src/pgraph/modelSaveRequest.I b/panda/src/pgraph/modelSaveRequest.I index d7ccd1dd09..14940aaf0f 100644 --- a/panda/src/pgraph/modelSaveRequest.I +++ b/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; +} diff --git a/panda/src/pgraph/modelSaveRequest.h b/panda/src/pgraph/modelSaveRequest.h index 878e28edee..fb0ca6551b 100644 --- a/panda/src/pgraph/modelSaveRequest.h +++ b/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();