diff --git a/README.md b/README.md index 0323d41b02..54f706a492 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ Reporting Issues If you encounter any bugs when using Panda3D, please report them in the bug tracker. This is hosted at: - https://bugs.launchpad.net/panda3d + https://github.com/panda3d/panda3d/issues Make sure to first use the search function to see if the bug has already been reported. When filling out a bug report, make sure that you include as much 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/README.md b/dtool/src/interrogate/README.md new file mode 100644 index 0000000000..4da044c8b5 --- /dev/null +++ b/dtool/src/interrogate/README.md @@ -0,0 +1,28 @@ +A key advantage of Panda3D is that it provides developers with the +ability to use both C++ and Python simultaneously. Essentially, Panda3D +gives programmers the best of both worlds, as they are able to take +advantage of the high performance and low-level programming found in +C++ in addition to the flexibility, interactive scripting, and +rapid-prototyping capabilities of Python. This feature is made possible +due to Python’s ability to call C libraries, and ultimately make use of +Panda3D’s Interrogate System: an automated C++ Extension Module +generation utility similar to SWIG. Although Python is the favored +scripting language of Panda3D, the engine is highly extensible in this +aspect, as any language that has a foreign function interface can make +use of the Interrogate System. + +The Interrogate System works like a compiler by scanning and parsing +C++ code for the Panda3D-specific, “PUBLISHED” keyword. This keyword +marks the particular methods of a class that are to be exposed within a +C++ Extension Module for that class which is eventually generated. One +benefit of using the “PUBLISHED” keyword is that it alleviates the need +for an interface file that provides function prototypes for the class +methods that will be exposed within the extension module, as is the +case with SWIG. Interrogate turns a class into a loose collection of +Python interface wrapper functions that make up the C++ Extension +Module. + +This package depends on the 'cppparser' package, which contains the +code that parses the C++ headers, and the 'interrogatedb' package, +which contains the intermediate representation of the interfaces that +can be saved to a database file for FFI generation tools to consume. 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 383e9b652c..a09b02d606 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -3713,6 +3713,7 @@ if (not RUNTIME): TargetAdd('p3event_composite2.obj', opts=OPTS, input='p3event_composite2.cxx') OPTS=['DIR:panda/src/event', 'PYTHON'] + TargetAdd('p3event_asyncTask_ext.obj', opts=OPTS, input='asyncTask_ext.cxx') TargetAdd('p3event_pythonTask.obj', opts=OPTS, input='pythonTask.cxx') IGATEFILES=GetDirectoryContents('panda/src/event', ["*.h", "*_composite*.cxx"]) TargetAdd('libp3event.in', opts=OPTS, input=IGATEFILES) @@ -4306,6 +4307,7 @@ if (not RUNTIME): TargetAdd('core.pyd', input='p3pipeline_pythonThread.obj') TargetAdd('core.pyd', input='p3putil_ext_composite.obj') TargetAdd('core.pyd', input='p3pnmimage_pfmFile_ext.obj') + TargetAdd('core.pyd', input='p3event_asyncTask_ext.obj') TargetAdd('core.pyd', input='p3event_pythonTask.obj') TargetAdd('core.pyd', input='p3gobj_ext_composite.obj') TargetAdd('core.pyd', input='p3pgraph_ext_composite.obj') 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/audio/audioManager.h b/panda/src/audio/audioManager.h index 6de21e15c4..c957581534 100644 --- a/panda/src/audio/audioManager.h +++ b/panda/src/audio/audioManager.h @@ -154,9 +154,10 @@ PUBLISHED: PN_stdfloat *ux, PN_stdfloat *uy, PN_stdfloat *uz); // Control the "relative scale that sets the distance factor" units for 3D - // spacialized audio. Default is 1.0 Fmod uses meters internally, so give a - // float in Units-per meter Don't know what Miles uses. Default is 1.0 - // which is adjust in panda to be feet. + // spacialized audio. This is a float in units-per-meter. Default value is + // 1.0, which means that Panda units are understood as meters; for e.g. + // feet, set 3.28. This factor is applied only to Fmod and OpenAL at the + // moment. virtual void audio_3d_set_distance_factor(PN_stdfloat factor); virtual PN_stdfloat audio_3d_get_distance_factor() const; diff --git a/panda/src/audiotraits/fmodAudioManager.cxx b/panda/src/audiotraits/fmodAudioManager.cxx index 6dec5648f9..cc1b8ecd0b 100644 --- a/panda/src/audiotraits/fmodAudioManager.cxx +++ b/panda/src/audiotraits/fmodAudioManager.cxx @@ -42,14 +42,8 @@ pset FmodAudioManager::_all_managers; bool FmodAudioManager::_system_is_valid = false; - -// This sets the distance factor for 3D audio to use feet. FMOD uses meters -// by default. Since Panda use feet we need to compensate for that with a -// factor of 3.28 This can be overwritten. You just need to call -// audio_3d_set_distance_factor(PN_stdfloat factor) and set your new factor. - PN_stdfloat FmodAudioManager::_doppler_factor = 1; -PN_stdfloat FmodAudioManager::_distance_factor = 3.28; +PN_stdfloat FmodAudioManager::_distance_factor = 1; PN_stdfloat FmodAudioManager::_drop_off_factor = 1; @@ -100,6 +94,8 @@ FmodAudioManager() { _up.y = 0; _up.z = 0; + _active = true; + _saved_outputtype = FMOD_OUTPUTTYPE_AUTODETECT; if (_system == (FMOD::System *)NULL) { diff --git a/panda/src/audiotraits/fmodAudioManager.h b/panda/src/audiotraits/fmodAudioManager.h index 2508d36b8a..751385e5a7 100644 --- a/panda/src/audiotraits/fmodAudioManager.h +++ b/panda/src/audiotraits/fmodAudioManager.h @@ -123,9 +123,11 @@ public: PN_stdfloat *fx, PN_stdfloat *fy, PN_stdfloat *fz, PN_stdfloat *ux, PN_stdfloat *uy, PN_stdfloat *uz); - // Control the "relative distance factor" for 3D spacialized audio. Default - // is 1.0 Fmod uses meters internally, so give a float in Units-per meter - // Don't know what Miles uses. + // Control the "relative scale that sets the distance factor" units for 3D + // spacialized audio. This is a float in units-per-meter. Default value is + // 1.0, which means that Panda units are understood as meters; for e.g. + // feet, set 3.28. This factor is applied only to Fmod and OpenAL at the + // moment. virtual void audio_3d_set_distance_factor(PN_stdfloat factor); virtual PN_stdfloat audio_3d_get_distance_factor() const; diff --git a/panda/src/audiotraits/fmodAudioSound.cxx b/panda/src/audiotraits/fmodAudioSound.cxx index a793b9bfa5..f10750ade3 100644 --- a/panda/src/audiotraits/fmodAudioSound.cxx +++ b/panda/src/audiotraits/fmodAudioSound.cxx @@ -55,6 +55,9 @@ FmodAudioSound(AudioManager *manager, Filename file_name, bool positional) { _velocity.y = 0; _velocity.z = 0; + _min_dist = 1.0; + _max_dist = 1000000000.0; + // Play Rate Variable _playrate = 1; diff --git a/panda/src/audiotraits/openalAudioManager.cxx b/panda/src/audiotraits/openalAudioManager.cxx index b6d68a0ec0..2e82a9c718 100644 --- a/panda/src/audiotraits/openalAudioManager.cxx +++ b/panda/src/audiotraits/openalAudioManager.cxx @@ -97,7 +97,7 @@ OpenALAudioManager() { _is_valid = true; // Init 3D attributes - _distance_factor = 3.28; + _distance_factor = 1; _drop_off_factor = 1; _position[0] = 0; @@ -715,12 +715,11 @@ audio_3d_get_listener_attributes(PN_stdfloat *px, PN_stdfloat *py, PN_stdfloat * *uz = _forward_up[4]; } - /** - * Set units per foot WARNING: OpenAL has no distance factor but we use this - * as a scale on the min/max distances of sounds to preserve FMOD - * compatibility. Also, adjusts the speed of sound to compensate for unit - * difference. OpenAL's default speed of sound is 343.3 m/s == 1126.3 ft/s + * Set value in units per meter + * WARNING: OpenAL has no distance factor but we use this as a scale + * on the min/max distances of sounds to preserve FMOD compatibility. + * Also adjusts the speed of sound to compensate for unit difference. */ void OpenALAudioManager:: audio_3d_set_distance_factor(PN_stdfloat factor) { @@ -732,7 +731,7 @@ audio_3d_set_distance_factor(PN_stdfloat factor) { alGetError(); // clear errors if (_distance_factor>0) { - alSpeedOfSound(1126.3*_distance_factor); + alSpeedOfSound(343.3*_distance_factor); al_audio_errcheck("alSpeedOfSound()"); // resets the doppler factor to the correct setting in case it was set to // 0.0 by a distance_factor<=0.0 @@ -752,7 +751,7 @@ audio_3d_set_distance_factor(PN_stdfloat factor) { } /** - * Sets units per foot + * Get value in units per meter */ PN_stdfloat OpenALAudioManager:: audio_3d_get_distance_factor() const { diff --git a/panda/src/audiotraits/openalAudioManager.h b/panda/src/audiotraits/openalAudioManager.h index 70089c138d..5917900869 100644 --- a/panda/src/audiotraits/openalAudioManager.h +++ b/panda/src/audiotraits/openalAudioManager.h @@ -84,11 +84,14 @@ class EXPCL_OPENAL_AUDIO OpenALAudioManager : public AudioManager { PN_stdfloat *fx, PN_stdfloat *fy, PN_stdfloat *fz, PN_stdfloat *ux, PN_stdfloat *uy, PN_stdfloat *uz); - // Control the "relative distance factor" for 3D spacialized audio in units- - // per-foot. Default is 1.0 OpenAL has no distance factor but we use this - // as a scale on the minmax distances of sounds to preserve FMOD - // compatibility. Also, adjusts the speed of sound to compensate for unit - // difference. + + // Control the "relative scale that sets the distance factor" units for 3D + // spacialized audio. This is a float in units-per-meter. Default value is + // 1.0, which means that Panda units are understood as meters; for e.g. + // feet, set 3.28. This factor is applied only to Fmod and OpenAL at the + // moment. + // OpenAL in fact has no distance factor like Fmod, but works with the speed + // of sound instead, so we use this factor to scale the speed of sound. virtual void audio_3d_set_distance_factor(PN_stdfloat factor); virtual PN_stdfloat audio_3d_get_distance_factor() const; diff --git a/panda/src/audiotraits/openalAudioSound.cxx b/panda/src/audiotraits/openalAudioSound.cxx index 43ebd149cb..da379bfd5c 100644 --- a/panda/src/audiotraits/openalAudioSound.cxx +++ b/panda/src/audiotraits/openalAudioSound.cxx @@ -48,7 +48,7 @@ OpenALAudioSound(OpenALAudioManager* manager, _balance(0), _play_rate(1.0), _positional(positional), - _min_dist(3.28f), + _min_dist(1.0f), _max_dist(1000000000.0f), _drop_off_factor(1.0f), _length(0.0), @@ -673,7 +673,7 @@ set_3d_min_distance(PN_stdfloat dist) { _manager->make_current(); alGetError(); // clear errors - alSourcef(_source,AL_REFERENCE_DISTANCE,_min_dist*_manager->audio_3d_get_distance_factor()); + alSourcef(_source,AL_REFERENCE_DISTANCE,_min_dist); al_audio_errcheck("alSourcefv(_source,AL_REFERENCE_DISTANCE)"); } } @@ -698,7 +698,7 @@ set_3d_max_distance(PN_stdfloat dist) { _manager->make_current(); alGetError(); // clear errors - alSourcef(_source,AL_MAX_DISTANCE,_max_dist*_manager->audio_3d_get_distance_factor()); + alSourcef(_source,AL_MAX_DISTANCE,_max_dist); al_audio_errcheck("alSourcefv(_source,AL_MAX_DISTANCE)"); } } diff --git a/panda/src/bullet/bulletDebugNode.cxx b/panda/src/bullet/bulletDebugNode.cxx index 196a9af09f..b4575dd3cf 100644 --- a/panda/src/bullet/bulletDebugNode.cxx +++ b/panda/src/bullet/bulletDebugNode.cxx @@ -13,20 +13,25 @@ #include "bulletDebugNode.h" +#include "cullHandler.h" +#include "cullTraverser.h" +#include "cullableObject.h" #include "geomLines.h" #include "geomVertexData.h" #include "geomTriangles.h" #include "geomVertexFormat.h" #include "geomVertexWriter.h" #include "omniBoundingVolume.h" +#include "pStatTimer.h" TypeHandle BulletDebugNode::_type_handle; +PStatCollector BulletDebugNode::_pstat_debug("App:Bullet:DoPhysics:Debug"); /** * */ BulletDebugNode:: -BulletDebugNode(const char *name) : GeomNode(name) { +BulletDebugNode(const char *name) : PandaNode(name), _debug_stale(true) { _wireframe = true; _constraints = true; @@ -37,40 +42,6 @@ BulletDebugNode(const char *name) : GeomNode(name) { set_bounds(bounds); set_final(true); set_overall_hidden(true); - - // Lines - { - PT(GeomVertexData) vdata; - PT(Geom) geom; - PT(GeomLines) prim; - - vdata = new GeomVertexData("", GeomVertexFormat::get_v3c4(), Geom::UH_stream); - - prim = new GeomLines(Geom::UH_stream); - prim->set_shade_model(Geom::SM_uniform); - - geom = new Geom(vdata); - geom->add_primitive(prim); - - add_geom(geom); - } - - // Triangles - { - PT(GeomVertexData) vdata; - PT(Geom) geom; - PT(GeomTriangles) prim; - - vdata = new GeomVertexData("", GeomVertexFormat::get_v3c4(), Geom::UH_stream); - - prim = new GeomTriangles(Geom::UH_stream); - prim->set_shade_model(Geom::SM_uniform); - - geom = new Geom(vdata); - geom->add_primitive(prim); - - add_geom(geom); - } } /** @@ -174,101 +145,134 @@ draw_mask_changed() { } } +/** + * Returns true if there is some value to visiting this particular node during + * the cull traversal for any camera, false otherwise. This will be used to + * optimize the result of get_net_draw_show_mask(), so that any subtrees that + * contain only nodes for which is_renderable() is false need not be visited. + */ +bool BulletDebugNode:: +is_renderable() const { + return true; +} + +/** + * Adds the node's contents to the CullResult we are building up during the + * cull traversal, so that it will be drawn at render time. For most nodes + * other than GeomNodes, this is a do-nothing operation. + */ +void BulletDebugNode:: +add_for_draw(CullTraverser *trav, CullTraverserData &data) { + PT(Geom) debug_lines; + PT(Geom) debug_triangles; + + { + LightMutexHolder holder(_lock); + if (_debug_world == nullptr) { + return; + } + if (_debug_stale) { + nassertv(_debug_world != nullptr); + PStatTimer timer(_pstat_debug); + + // Collect debug geometry data + _drawer._lines.clear(); + _drawer._triangles.clear(); + + _debug_world->debugDrawWorld(); + + // Render lines + { + PT(GeomVertexData) vdata = + new GeomVertexData("", GeomVertexFormat::get_v3c4(), Geom::UH_stream); + vdata->unclean_set_num_rows(_drawer._lines.size() * 2); + + GeomVertexWriter vwriter(vdata, InternalName::get_vertex()); + GeomVertexWriter cwriter(vdata, InternalName::get_color()); + + pvector::const_iterator lit; + for (lit = _drawer._lines.begin(); lit != _drawer._lines.end(); lit++) { + const Line &line = *lit; + + vwriter.set_data3(line._p0); + vwriter.set_data3(line._p1); + cwriter.set_data4(LVecBase4(line._color)); + cwriter.set_data4(LVecBase4(line._color)); + } + + PT(GeomPrimitive) prim = new GeomLines(Geom::UH_stream); + prim->set_shade_model(Geom::SM_uniform); + prim->add_next_vertices(_drawer._lines.size() * 2); + + debug_lines = new Geom(vdata); + debug_lines->add_primitive(prim); + _debug_lines = debug_lines; + } + + // Render triangles + { + PT(GeomVertexData) vdata = + new GeomVertexData("", GeomVertexFormat::get_v3c4(), Geom::UH_stream); + vdata->unclean_set_num_rows(_drawer._triangles.size() * 3); + + GeomVertexWriter vwriter(vdata, InternalName::get_vertex()); + GeomVertexWriter cwriter(vdata, InternalName::get_color()); + + pvector::const_iterator tit; + for (tit = _drawer._triangles.begin(); tit != _drawer._triangles.end(); tit++) { + const Triangle &tri = *tit; + + vwriter.set_data3(tri._p0); + vwriter.set_data3(tri._p1); + vwriter.set_data3(tri._p2); + cwriter.set_data4(LVecBase4(tri._color)); + cwriter.set_data4(LVecBase4(tri._color)); + cwriter.set_data4(LVecBase4(tri._color)); + } + + PT(GeomPrimitive) prim = new GeomTriangles(Geom::UH_stream); + prim->set_shade_model(Geom::SM_uniform); + prim->add_next_vertices(_drawer._triangles.size() * 3); + + debug_triangles = new Geom(vdata); + debug_triangles->add_primitive(prim); + _debug_triangles = debug_triangles; + } + + // Clear collected data. + _drawer._lines.clear(); + _drawer._triangles.clear(); + + _debug_stale = false; + } else { + debug_lines = _debug_lines; + debug_triangles = _debug_triangles; + } + } + + // Record them without any state or transform. + trav->_geoms_pcollector.add_level(2); + { + CullableObject *object = + new CullableObject(move(debug_lines), RenderState::make_empty(), trav->get_scene()->get_cs_world_transform()); + trav->get_cull_handler()->record_object(object, trav); + } + { + CullableObject *object = + new CullableObject(move(debug_triangles), RenderState::make_empty(), trav->get_scene()->get_cs_world_transform()); + trav->get_cull_handler()->record_object(object, trav); + } +} + /** * */ void BulletDebugNode:: sync_b2p(btDynamicsWorld *world) { + LightMutexHolder holder(_lock); - if (is_overall_hidden()) return; - - nassertv(get_num_geoms() == 2); - - // Collect debug geometry data - _drawer._lines.clear(); - _drawer._triangles.clear(); - - world->debugDrawWorld(); - - // Get inverse of this node's net transform - NodePath np = NodePath::any_path((PandaNode *)this); - LMatrix4 m = np.get_net_transform()->get_mat(); - m.invert_in_place(); - - // Render lines - { - PT(GeomVertexData) vdata; - PT(Geom) geom; - PT(GeomLines) prim; - - vdata = new GeomVertexData("", GeomVertexFormat::get_v3c4(), Geom::UH_stream); - - prim = new GeomLines(Geom::UH_stream); - prim->set_shade_model(Geom::SM_uniform); - - GeomVertexWriter vwriter = GeomVertexWriter(vdata, InternalName::get_vertex()); - GeomVertexWriter cwriter = GeomVertexWriter(vdata, InternalName::get_color()); - - int v = 0; - - pvector::const_iterator lit; - for (lit = _drawer._lines.begin(); lit != _drawer._lines.end(); lit++) { - Line line = *lit; - - vwriter.add_data3(m.xform_point(line._p0)); - vwriter.add_data3(m.xform_point(line._p1)); - cwriter.add_data4(LVecBase4(line._color)); - cwriter.add_data4(LVecBase4(line._color)); - - prim->add_vertex(v++); - prim->add_vertex(v++); - prim->close_primitive(); - } - - geom = new Geom(vdata); - geom->add_primitive(prim); - - set_geom(0, geom); - } - - // Render triangles - { - PT(GeomVertexData) vdata; - PT(Geom) geom; - PT(GeomTriangles) prim; - - vdata = new GeomVertexData("", GeomVertexFormat::get_v3c4(), Geom::UH_stream); - - prim = new GeomTriangles(Geom::UH_stream); - prim->set_shade_model(Geom::SM_uniform); - - GeomVertexWriter vwriter = GeomVertexWriter(vdata, InternalName::get_vertex()); - GeomVertexWriter cwriter = GeomVertexWriter(vdata, InternalName::get_color()); - - int v = 0; - - pvector::const_iterator tit; - for (tit = _drawer._triangles.begin(); tit != _drawer._triangles.end(); tit++) { - Triangle tri = *tit; - - vwriter.add_data3(m.xform_point(tri._p0)); - vwriter.add_data3(m.xform_point(tri._p1)); - vwriter.add_data3(m.xform_point(tri._p2)); - cwriter.add_data4(LVecBase4(tri._color)); - cwriter.add_data4(LVecBase4(tri._color)); - cwriter.add_data4(LVecBase4(tri._color)); - - prim->add_vertex(v++); - prim->add_vertex(v++); - prim->add_vertex(v++); - prim->close_primitive(); - } - - geom = new Geom(vdata); - geom->add_primitive(prim); - - set_geom(1, geom); - } + _debug_world = world; + _debug_stale = true; } /** @@ -431,8 +435,6 @@ register_with_read_factory() { */ void BulletDebugNode:: write_datagram(BamWriter *manager, Datagram &dg) { - // Don't upcall to GeomNode since we're not interested in storing the actual - // debug Geoms in the .bam file. PandaNode::write_datagram(manager, dg); dg.add_bool(_wireframe); @@ -464,8 +466,6 @@ make_from_bam(const FactoryParams ¶ms) { */ void BulletDebugNode:: fillin(DatagramIterator &scan, BamReader *manager) { - // Don't upcall to GeomNode since we're not interested in storing the actual - // debug Geoms in the .bam file. PandaNode::fillin(scan, manager); _wireframe = scan.get_bool(); diff --git a/panda/src/bullet/bulletDebugNode.h b/panda/src/bullet/bulletDebugNode.h index 987e1a7450..34943865f1 100644 --- a/panda/src/bullet/bulletDebugNode.h +++ b/panda/src/bullet/bulletDebugNode.h @@ -17,13 +17,12 @@ #include "pandabase.h" #include "bullet_includes.h" - -#include "geomNode.h" +#include "lightMutex.h" /** * */ -class EXPCL_PANDABULLET BulletDebugNode : public GeomNode { +class EXPCL_PANDABULLET BulletDebugNode : public PandaNode { PUBLISHED: BulletDebugNode(const char *name="debug"); @@ -53,6 +52,9 @@ public: virtual bool safe_to_combine_children() const; virtual bool safe_to_flatten_below() const; + virtual bool is_renderable() const; + virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data); + private: void sync_b2p(btDynamicsWorld *world); @@ -100,14 +102,22 @@ private: int _mode; }; + LightMutex _lock; DebugDraw _drawer; + bool _debug_stale; + btDynamicsWorld *_debug_world; + PT(Geom) _debug_lines; + PT(Geom) _debug_triangles; + bool _wireframe; bool _constraints; bool _bounds; friend class BulletWorld; + static PStatCollector _pstat_debug; + public: static void register_with_read_factory(); virtual void write_datagram(BamWriter *manager, Datagram &dg); @@ -121,9 +131,9 @@ public: return _type_handle; } static void init_type() { - GeomNode::init_type(); + PandaNode::init_type(); register_type(_type_handle, "BulletDebugNode", - GeomNode::get_class_type()); + PandaNode::get_class_type()); } virtual TypeHandle get_type() const { return get_class_type(); diff --git a/panda/src/bullet/bulletWorld.I b/panda/src/bullet/bulletWorld.I index 958376ec60..c2ecd2cec3 100644 --- a/panda/src/bullet/bulletWorld.I +++ b/panda/src/bullet/bulletWorld.I @@ -55,21 +55,12 @@ INLINE BulletWorld:: */ INLINE void BulletWorld:: set_debug_node(BulletDebugNode *node) { - nassertv(node); - - _debug = node; - _world->setDebugDrawer(&(_debug->_drawer)); -} - -/** - * - */ -INLINE void BulletWorld:: -clear_debug_node() { - - _debug = NULL; - _world->setDebugDrawer(NULL); + if (node != _debug) { + clear_debug_node(); + _debug = node; + _world->setDebugDrawer(&(_debug->_drawer)); + } } /** diff --git a/panda/src/bullet/bulletWorld.cxx b/panda/src/bullet/bulletWorld.cxx index e7f6f3e267..a7942aee02 100644 --- a/panda/src/bullet/bulletWorld.cxx +++ b/panda/src/bullet/bulletWorld.cxx @@ -17,6 +17,7 @@ #include "bulletSoftBodyWorldInfo.h" #include "collideMask.h" +#include "lightMutexHolder.h" #define clamp(x, x_min, x_max) max(min(x, x_max), x_min) @@ -24,7 +25,6 @@ TypeHandle BulletWorld::_type_handle; PStatCollector BulletWorld::_pstat_physics("App:Bullet:DoPhysics"); PStatCollector BulletWorld::_pstat_simulation("App:Bullet:DoPhysics:Simulation"); -PStatCollector BulletWorld::_pstat_debug("App:Bullet:DoPhysics:Debug"); PStatCollector BulletWorld::_pstat_p2b("App:Bullet:DoPhysics:SyncP2B"); PStatCollector BulletWorld::_pstat_b2p("App:Bullet:DoPhysics:SyncB2P"); @@ -127,6 +127,19 @@ get_world_info() { return BulletSoftBodyWorldInfo(_info); } +/** + * Removes a debug node that has been assigned to this BulletWorld. + */ +void BulletWorld:: +clear_debug_node() { + if (_debug != nullptr) { + LightMutexHolder holder(_debug->_lock); + _debug->_debug_world = nullptr; + _world->setDebugDrawer(nullptr); + _debug = nullptr; + } +} + /** * */ @@ -184,9 +197,7 @@ do_physics(PN_stdfloat dt, int max_substeps, PN_stdfloat stepsize) { // Render debug if (_debug) { - _pstat_debug.start(); _debug->sync_b2p(_world); - _pstat_debug.stop(); } _pstat_physics.stop(); diff --git a/panda/src/bullet/bulletWorld.h b/panda/src/bullet/bulletWorld.h index b077e05f7a..558d086b37 100644 --- a/panda/src/bullet/bulletWorld.h +++ b/panda/src/bullet/bulletWorld.h @@ -63,7 +63,7 @@ PUBLISHED: // Debug INLINE void set_debug_node(BulletDebugNode *node); - INLINE void clear_debug_node(); + void clear_debug_node(); INLINE BulletDebugNode *get_debug_node() const; INLINE bool has_debug_node() const; @@ -208,7 +208,6 @@ private: static PStatCollector _pstat_physics; static PStatCollector _pstat_simulation; - static PStatCollector _pstat_debug; static PStatCollector _pstat_p2b; static PStatCollector _pstat_b2p; diff --git a/panda/src/chan/README.md b/panda/src/chan/README.md new file mode 100644 index 0000000000..298bdf4345 --- /dev/null +++ b/panda/src/chan/README.md @@ -0,0 +1,5 @@ +This package contains the animation channels. This defines the various +kinds of AnimChannels that may be defined, as well as the MovingPart +class which binds to the channels and plays the animation. This is a +support library for char, as well as any other libraries that want to +define objects whose values change over time. diff --git a/panda/src/cull/README.md b/panda/src/cull/README.md new file mode 100644 index 0000000000..f5ae26c40d --- /dev/null +++ b/panda/src/cull/README.md @@ -0,0 +1,3 @@ +This package contains the Cull Traverser. The cull traversal collects +all state changes specified, and removes unnecessary state change +requests. Also does all the depth sorting for proper alphaing. diff --git a/panda/src/dgraph/README.md b/panda/src/dgraph/README.md new file mode 100644 index 0000000000..330c952a60 --- /dev/null +++ b/panda/src/dgraph/README.md @@ -0,0 +1,3 @@ +This package defines and manages the data graph, which is the hierarchy +of devices, tforms, and any other things which might have an input or +an output and need to execute every frame. diff --git a/panda/src/display/README.md b/panda/src/display/README.md new file mode 100644 index 0000000000..f9029b8a2b --- /dev/null +++ b/panda/src/display/README.md @@ -0,0 +1,2 @@ +This package contains the abstract display classes, including pipes, +windows, channels, and display regions. diff --git a/panda/src/dxgsg9/README.md b/panda/src/dxgsg9/README.md new file mode 100644 index 0000000000..72da6ba2f5 --- /dev/null +++ b/panda/src/dxgsg9/README.md @@ -0,0 +1,2 @@ +This package handles all communication with the DirectX backend, and +manages state to minimize redundant state changes. 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(); diff --git a/panda/src/pgraph/shaderInput.h b/panda/src/pgraph/shaderInput.h index d0ae3eae20..32fe514558 100644 --- a/panda/src/pgraph/shaderInput.h +++ b/panda/src/pgraph/shaderInput.h @@ -31,6 +31,7 @@ #include "shader.h" #include "texture.h" #include "shaderBuffer.h" +#include "extension.h" /** * This is a small container class that can hold any one of the value types diff --git a/panda/src/pgraphnodes/shaderGenerator.cxx b/panda/src/pgraphnodes/shaderGenerator.cxx index 0251f7f445..bfa7642ac8 100644 --- a/panda/src/pgraphnodes/shaderGenerator.cxx +++ b/panda/src/pgraphnodes/shaderGenerator.cxx @@ -1417,7 +1417,10 @@ synthesize_shader(const RenderState *rs, const GeomVertexAnimationSpec &anim) { text << "\t result.rgb = lerp(result, tex" << i << ", tex" << i << ".a).rgb;\n"; break; case TextureStage::M_blend: - text << "\t result.rgb = lerp(result, tex" << i << " * texcolor_" << i << ", tex" << i << ".r).rgb;\n"; + text << "\t result.rgb = lerp(result.rgb, texcolor_" << i << ".rgb, tex" << i << ".rgb);\n"; + if (key._calc_primary_alpha) { + text << "\t result.a *= tex" << i << ".a;\n"; + } break; case TextureStage::M_replace: text << "\t result = tex" << i << ";\n"; diff --git a/panda/src/testbed/pview.cxx b/panda/src/testbed/pview.cxx index c31537e2c5..6d68f099ab 100644 --- a/panda/src/testbed/pview.cxx +++ b/panda/src/testbed/pview.cxx @@ -25,6 +25,9 @@ #include "panda_getopt.h" #include "preprocess_argv.h" #include "graphicsPipeSelection.h" +#include "asyncTaskManager.h" +#include "asyncTask.h" +#include "boundingSphere.h" // By including checkPandaVersion.h, we guarantee that runtime attempts to run // pview will fail if it inadvertently links with the wrong version of @@ -231,6 +234,111 @@ report_version() { nout << "\n"; } +// Task that dynamically adjusts the camera len's near/far clipping +// planes to ensure the user can zoom in as close as needed to a model. +// +// Code adapted from WindowFramework::center_trackball(), but +// without moving the camera. When the camera is inside the model, +// the near clip is set to near-zero. +// +class AdjustCameraClipPlanesTask : public AsyncTask { +public: + AdjustCameraClipPlanesTask(const string &name, Camera *camera) : + AsyncTask(name), _camera(camera), _lens(camera->get_lens(0)), _sphere(NULL) + { + NodePath np = framework.get_models(); + PT(BoundingVolume) volume = np.get_bounds(); + + // We expect at least a geometric bounding volume around the world. + nassertv(volume != (BoundingVolume *)NULL); + nassertv(volume->is_of_type(GeometricBoundingVolume::get_class_type())); + CPT(GeometricBoundingVolume) gbv = DCAST(GeometricBoundingVolume, volume); + + if (np.has_parent()) { + CPT(TransformState) net_transform = np.get_parent().get_net_transform(); + PT(GeometricBoundingVolume) new_gbv = DCAST(GeometricBoundingVolume, gbv->make_copy()); + new_gbv->xform(net_transform->get_mat()); + gbv = new_gbv; + } + + // Determine the bounding sphere around the object. + if (gbv->is_infinite()) { + framework_cat.warning() + << "Infinite bounding volume for " << np << "\n"; + return; + } + + if (gbv->is_empty()) { + framework_cat.warning() + << "Empty bounding volume for " << np << "\n"; + return; + } + + // The BoundingVolume might be a sphere (it's likely), but since it + // might not, we'll take no chances and make our own sphere. + _sphere = new BoundingSphere(gbv->get_approx_center(), 0.0f); + if (!_sphere->extend_by(gbv)) { + framework_cat.warning() + << "Cannot determine bounding volume of " << np << "\n"; + return; + } + } + ALLOC_DELETED_CHAIN(AdjustCameraClipPlanesTask); + + virtual DoneStatus do_task() { + if (!_sphere) { + return DS_done; + } + + if (framework.get_num_windows() == 0) { + return DS_cont; + } + + WindowFramework *wf = framework.get_window(0); + if (!wf) { + return DS_cont; + } + + // Get current camera position. + NodePath cameraNP = wf->get_camera_group(); + LPoint3 pos = cameraNP.get_pos(); + + // See how far or close the camera is + LPoint3 center = _sphere->get_center(); + PN_stdfloat radius = _sphere->get_radius(); + + PN_stdfloat min_distance = 0.001 * radius; + + // Choose a suitable distance to view the whole volume in our frame. + // This is based on the camera lens in use. + PN_stdfloat distance; + CPT(GeometricBoundingVolume) gbv = DCAST(GeometricBoundingVolume, _sphere); + if (gbv->contains(pos)) { + // See as up-close to the model as possible + distance = min_distance; + } else { + // View from a distance + distance = (center - pos).length(); + } + + // Ensure the far plane is far enough back to see the entire object. + PN_stdfloat ideal_far_plane = distance + radius * 1.5; + _lens->set_far(max(_lens->get_default_far(), ideal_far_plane)); + + // And that the near plane is far enough forward, but if inside + // the sphere, keep above 0. + PN_stdfloat ideal_near_plane = max(min_distance * 10, distance - radius); + _lens->set_near(min(_lens->get_default_near(), ideal_near_plane)); + + return DS_cont; + } + + Camera *_camera; + Lens *_lens; + PT(BoundingSphere) _sphere; +}; + + int main(int argc, char **argv) { preprocess_argv(argc, argv); @@ -383,6 +491,9 @@ main(int argc, char **argv) { window->set_anim_controls(true); } + PT(AdjustCameraClipPlanesTask) task = new AdjustCameraClipPlanesTask("Adjust Camera Bounds", window->get_camera(0)); + framework.get_task_mgr().add(task); + framework.enable_default_keys(); framework.define_key("shift-w", "open a new window", event_W, NULL); framework.define_key("shift-f", "flatten hierarchy", event_F, NULL); diff --git a/samples/rocket-console/assets/Perfect DOS VGA 437.ttf b/samples/rocket-console/assets/Perfect DOS VGA 437.ttf new file mode 100644 index 0000000000..f5cbfc06fd Binary files /dev/null and b/samples/rocket-console/assets/Perfect DOS VGA 437.ttf differ diff --git a/samples/rocket-console/assets/console.rcss b/samples/rocket-console/assets/console.rcss new file mode 100755 index 0000000000..b1df4879f5 --- /dev/null +++ b/samples/rocket-console/assets/console.rcss @@ -0,0 +1,38 @@ +body +{ + font-family: "Perfect DOS VGA 437"; + font-weight: normal; + font-style: normal; + + // use all the allocated texture space + min-width: 100%; + min-height: 100%; + + background-color: #000; +} + + +text#content +{ + z-index: 2; + font-size: 30px; + + white-space: pre-wrap; + + margin: auto; + + text-align: left; + position: absolute; + + // account for non-proportionality of our 1024x512 + // buffer compared with VGA font proportions and + // wanting to center the screen with 40 columns + top: 16px; + left: 32px; + + width: 100%; + height: 100%; + + color: #888; + +} diff --git a/samples/rocket-console/assets/console.rml b/samples/rocket-console/assets/console.rml new file mode 100755 index 0000000000..7887f4ef7c --- /dev/null +++ b/samples/rocket-console/assets/console.rml @@ -0,0 +1,11 @@ + + + + Administrative Console + + + + + + + diff --git a/samples/rocket-console/assets/dos437.txt b/samples/rocket-console/assets/dos437.txt new file mode 100644 index 0000000000..614d29885e --- /dev/null +++ b/samples/rocket-console/assets/dos437.txt @@ -0,0 +1 @@ +from www.dafont.com/perfect-dos-vga-437.font (info at http://zehfernando.com/2015/revisiting-vga-fonts/) \ No newline at end of file diff --git a/samples/rocket-console/assets/loading.rml b/samples/rocket-console/assets/loading.rml new file mode 100755 index 0000000000..c85a5d3bed --- /dev/null +++ b/samples/rocket-console/assets/loading.rml @@ -0,0 +1,54 @@ + + + Main Menu + + + + + + + +
+ +
+ +
diff --git a/samples/rocket-console/assets/modenine.nfo b/samples/rocket-console/assets/modenine.nfo new file mode 100644 index 0000000000..c6af4f1f8b --- /dev/null +++ b/samples/rocket-console/assets/modenine.nfo @@ -0,0 +1,18 @@ +ModeNine + +Based on Andrew Bulhak's ModeSeven, in turn inspired by the screen +output of the BBC Micro. + +copyright: +(C) 1998 Andrew C. Bulhak +(C) 2001 Graham H Freeman + +Freely Distributable. + +All we ask is that this readme file must be included with the font package. +Impresarios of free font sites and shovelware cd-roms, this means you! + +If you think this font is doovy, let us know at fonts@grudnuk.com, and we +might actually make more fonts. + +Another fine Grudnuk Creations produkt | http://grudnuk.com/ \ No newline at end of file diff --git a/samples/rocket-console/assets/modenine.ttf b/samples/rocket-console/assets/modenine.ttf new file mode 100644 index 0000000000..c0b00462d7 Binary files /dev/null and b/samples/rocket-console/assets/modenine.ttf differ diff --git a/samples/rocket-console/assets/monitor.egg.pz b/samples/rocket-console/assets/monitor.egg.pz new file mode 100644 index 0000000000..9c9f3675fd Binary files /dev/null and b/samples/rocket-console/assets/monitor.egg.pz differ diff --git a/samples/rocket-console/assets/monitor.txt b/samples/rocket-console/assets/monitor.txt new file mode 100644 index 0000000000..f2e1f7265e --- /dev/null +++ b/samples/rocket-console/assets/monitor.txt @@ -0,0 +1,12 @@ + +This is an edited version of this file from blendswap.com (http://www.blendswap.com/blends/view/74468). + +VERY IMPORTANT LICENSE INFORMATION: + +This file has been released by buzo under the following license: + + Creative Commons Zero (Public Domain) + +You can use this model for any purposes according to the following conditions: + + There are no requirements for CC-Zero licensed blends. \ No newline at end of file diff --git a/samples/rocket-console/assets/rkt.rcss b/samples/rocket-console/assets/rkt.rcss new file mode 100755 index 0000000000..ebc49f50fb --- /dev/null +++ b/samples/rocket-console/assets/rkt.rcss @@ -0,0 +1,44 @@ +/* +* Default styles for all the basic elements. +*/ + +div +{ + display: block; +} + +p +{ + display: block; +} + +h1 +{ + display: block; +} + +em +{ + font-style: italic; +} + +strong +{ + font-weight: bold; +} + +datagrid +{ + display: block; +} + +select, dataselect, datacombo +{ + text-align: left; +} + +tabset tabs +{ + display: block; +} + diff --git a/samples/rocket-console/assets/takeyga_kb.egg b/samples/rocket-console/assets/takeyga_kb.egg new file mode 100644 index 0000000000..3085947839 --- /dev/null +++ b/samples/rocket-console/assets/takeyga_kb.egg @@ -0,0 +1,316 @@ + { Z-up } + takeyga_kb { + diffr { 0.800000 } + diffg { 0.800000 } + diffb { 0.800000 } + specr { 0.500000 } + specg { 0.500000 } + specb { 0.500000 } + shininess { 12.5 } + ambr { 1.000000 } + ambg { 1.000000 } + ambb { 1.000000 } + emitr { 0.000000 } + emitg { 0.000000 } + emitb { 0.000000 } +} + + Texture.001 { + "./tex/takeyga_kb_specular.dds" + envtype { MODULATE } + minfilter { LINEAR_MIPMAP_LINEAR } + magfilter { LINEAR_MIPMAP_LINEAR } + wrap { REPEAT } +} + + Tex { + "./tex/takeyga_kb_diffuse.dds" + envtype { MODULATE } + minfilter { LINEAR_MIPMAP_LINEAR } + magfilter { LINEAR_MIPMAP_LINEAR } + wrap { REPEAT } +} + + Texture { + "./tex/takeyga_kb_normal.dds" + envtype { NORMAL } + minfilter { LINEAR_MIPMAP_LINEAR } + magfilter { LINEAR_MIPMAP_LINEAR } + wrap { REPEAT } +} + + Cube.001 { + { + { + 0.08499996364116669 0.0 0.0 0.0 + 0.0 0.23999987542629242 0.0 0.0 + 0.0 0.0 0.02500000037252903 0.0 + 0.0 0.0 0.0 1.0 + } + } + + Cube.001 { + + 0 {0.082953 0.240000 -0.025000 + { + 0.158203125 0.595703125 + } + } + 1 {0.082953 -0.240000 -0.025000 + { + 0.98828125 0.6015625 + } + } + 2 {-0.085000 -0.240000 -0.025000 + { + 0.98828125 0.986328125 + } + } + 3 {-0.085000 0.240000 -0.025000 + { + 0.162109375 0.98828125 + } + } + 4 {0.073544 0.227744 -0.005546 + { + 0.990234375 0.017578125 + } + } + 5 {-0.067932 0.223167 0.018996 + { + 0.98046875 0.46875 + } + } + 6 {-0.067932 -0.223167 0.018996 + { + 0.017578125 0.470703125 + } + } + 7 {0.073544 -0.227744 -0.005546 + { + 0.01953125 0.013671875 + } + } + 8 {0.082372 0.237328 -0.015273 + { + 0.9794921875 0.552734375 + } + } + 9 {0.073544 0.227744 -0.005546 + { + 0.9765625 0.57421875 + } + } + 10 {0.073544 -0.227744 -0.005546 + { + 0.025390625 0.57421875 + } + } + 11 {0.082372 -0.237328 -0.015273 + { + 0.0234375 0.5537109375 + } + } + 12 {0.082372 -0.237328 -0.015273 + { + 0.0703125 0.6123046875 + } + } + 13 {0.073544 -0.227744 -0.005546 + { + 0.095703125 0.615234375 + } + } + 14 {-0.067932 -0.223167 0.018996 + { + 0.13671875 0.97265625 + } + } + 15 {-0.080884 -0.235006 0.001122 + { + 0.07421875 0.982421875 + } + } + 16 {-0.080884 -0.235006 0.001122 + { + 0.0166015625 0.5361328125 + } + } + 17 {-0.067932 -0.223167 0.018996 + { + 0.017578125 0.58203125 + } + } + 18 {-0.067932 0.223167 0.018996 + { + 0.978515625 0.580078125 + } + } + 19 {-0.080884 0.235006 0.001122 + { + 0.984375 0.5341796875 + } + } + 20 {0.082372 0.237328 -0.015273 + { + 0.0703125 0.6123046875 + } + } + 21 {0.082953 0.240000 -0.025000 + { + 0.044921875 0.609375 + } + } + 22 {-0.085000 0.240000 -0.025000 + { + 0.01171875 0.9921875 + } + } + 23 {-0.080884 0.235006 0.001122 + { + 0.07421875 0.982421875 + } + } + 24 {0.082953 0.240000 -0.025000 + { + 0.982421875 0.53125 + } + } + 25 {0.082372 0.237328 -0.015273 + { + 0.9794921875 0.552734375 + } + } + 26 {0.082372 -0.237328 -0.015273 + { + 0.0234375 0.5537109375 + } + } + 27 {0.082953 -0.240000 -0.025000 + { + 0.021484375 0.533203125 + } + } + 28 {0.082953 -0.240000 -0.025000 + { + 0.044921875 0.609375 + } + } + 29 {0.082372 -0.237328 -0.015273 + { + 0.0703125 0.6123046875 + } + } + 30 {-0.080884 -0.235006 0.001122 + { + 0.07421875 0.982421875 + } + } + 31 {-0.085000 -0.240000 -0.025000 + { + 0.01171875 0.9921875 + } + } + 32 {-0.085000 -0.240000 -0.025000 + { + 0.015625 0.490234375 + } + } + 33 {-0.080884 -0.235006 0.001122 + { + 0.0166015625 0.5361328125 + } + } + 34 {-0.080884 0.235006 0.001122 + { + 0.984375 0.5341796875 + } + } + 35 {-0.085000 0.240000 -0.025000 + { + 0.990234375 0.48828125 + } + } + 36 {0.073544 0.227744 -0.005546 + { + 0.095703125 0.615234375 + } + } + 37 {0.082372 0.237328 -0.015273 + { + 0.0703125 0.6123046875 + } + } + 38 {-0.080884 0.235006 0.001122 + { + 0.07421875 0.982421875 + } + } + 39 {-0.067932 0.223167 0.018996 + { + 0.13671875 0.97265625 + } + }} + + + { + { Tex } + { takeyga_kb } + {0.000000 0.000000 -1.000000} + { 0 1 2 3 { Cube.001 }} + } + { + { Tex } + { takeyga_kb } + {0.508027 -0.000000 0.861341} + { 4 5 6 7 { Cube.001 }} + } + { + { Tex } + { takeyga_kb } + {0.966167 -0.000000 0.257917} + { 8 9 10 11 { Cube.001 }} + } + { + { Tex } + { takeyga_kb } + {0.028240 -0.996449 0.079322} + { 12 13 14 15 { Cube.001 }} + } + { + { Tex } + { takeyga_kb } + {-0.978036 0.000000 0.208436} + { 16 17 18 19 { Cube.001 }} + } + { + { Tex } + { takeyga_kb } + {0.001260 0.999752 0.022233} + { 20 21 22 23 { Cube.001 }} + } + { + { Tex } + { takeyga_kb } + {0.999846 -0.000000 0.017550} + { 24 25 26 27 { Cube.001 }} + } + { + { Tex } + { takeyga_kb } + {0.001259 -0.999752 0.022233} + { 28 29 30 31 { Cube.001 }} + } + { + { Tex } + { takeyga_kb } + {-0.998928 0.000000 0.046291} + { 32 33 34 35 { Cube.001 }} + } + { + { Tex } + { takeyga_kb } + {0.028241 0.996449 0.079322} + { 36 37 38 39 { Cube.001 }} + } + } diff --git a/samples/rocket-console/assets/tex/takeyga_kb_diffuse.dds b/samples/rocket-console/assets/tex/takeyga_kb_diffuse.dds new file mode 100644 index 0000000000..406e689545 Binary files /dev/null and b/samples/rocket-console/assets/tex/takeyga_kb_diffuse.dds differ diff --git a/samples/rocket-console/assets/tex/takeyga_kb_normal.dds b/samples/rocket-console/assets/tex/takeyga_kb_normal.dds new file mode 100644 index 0000000000..a949e2be69 Binary files /dev/null and b/samples/rocket-console/assets/tex/takeyga_kb_normal.dds differ diff --git a/samples/rocket-console/assets/tex/takeyga_kb_specular.dds b/samples/rocket-console/assets/tex/takeyga_kb_specular.dds new file mode 100644 index 0000000000..a63868f99c Binary files /dev/null and b/samples/rocket-console/assets/tex/takeyga_kb_specular.dds differ diff --git a/samples/rocket-console/assets/window.rcss b/samples/rocket-console/assets/window.rcss new file mode 100755 index 0000000000..0e7adcedf9 --- /dev/null +++ b/samples/rocket-console/assets/window.rcss @@ -0,0 +1,56 @@ +body +{ + font-family: "MODENINE"; + font-weight: normal; + font-style: normal; + font-size: 15; + +} + +body.window +{ + padding-top: 43px; + padding-bottom: 20px; + + min-width: 250px; + + min-height: 135px; + max-height: 700px; + +} + + + +div#title_bar +{ + z-index: 1; + + position: absolute; + top: 0px; + left: 0px; + + text-align: center; + + color: #fff; + background-color: #22f; +} + + +div#title_bar span +{ + padding-top: 17px; + padding-bottom: 48px; + + font-size: 32; + font-weight: bold; + + outline-font-effect: outline; + outline-width: 1px; + outline-color: black; +} + +div#title_bar_content +{ + text-align: center; + color: #cff; +} diff --git a/samples/rocket-console/assets/window.rml b/samples/rocket-console/assets/window.rml new file mode 100755 index 0000000000..47336e7ab2 --- /dev/null +++ b/samples/rocket-console/assets/window.rml @@ -0,0 +1,42 @@ + diff --git a/samples/rocket-console/console.py b/samples/rocket-console/console.py new file mode 100644 index 0000000000..e32d2408d4 --- /dev/null +++ b/samples/rocket-console/console.py @@ -0,0 +1,153 @@ +""" +Simple console widget for rocket +""" +import sys, os.path + +# workaround: https://www.panda3d.org/forums/viewtopic.php?t=10062&p=99697#p99054 +#from panda3d import rocket +import _rocketcore as rocket + +from panda3d.rocket import RocketRegion, RocketInputHandler + +class Console(object): + def __init__(self, base, context, cols, rows, commandHandler): + self.base = base + + self.context = context + self.loadFonts() + self.cols = cols + self.rows = rows + self.commandHandler = commandHandler + + self.setupConsole() + self.allowEditing(True) + + def getTextContainer(self): + return self.textEl + + def setPrompt(self, prompt): + self.consolePrompt = prompt + + def allowEditing(self, editMode): + self.editMode = editMode + if editMode: + self.input = "" + if not self.lastLine: + self.addLine("") + self.newEditLine() + + def loadFonts(self): + rocket.LoadFontFace("Perfect DOS VGA 437.ttf") + + def setupConsole(self): + self.document = self.context.LoadDocument("console.rml") + if not self.document: + raise AssertionError("did not find console.rml") + + el = self.document.GetElementById('content') + + self.textEl = el + + # roundabout way of accessing the current object through rocket event... + + # add attribute to let Rocket know about the receiver + self.context.console = self + + # then reference through the string format (dunno how else to get the event...) + self.document.AddEventListener( + 'keydown', 'document.context.console.handleKeyDown(event)', True) + self.document.AddEventListener( + 'textinput', 'document.context.console.handleTextInput(event)', True) + + self.consolePrompt = "C:\\>" + + self.input = "" + self.lastLine = None + + self.blinkState = False + self.queueBlinkCursor() + + self.document.Show() + + def queueBlinkCursor(self): + self.base.taskMgr.doMethodLater(0.2, self.blinkCursor, 'blinkCursor') + + def blinkCursor(self, task): + self.blinkState = not self.blinkState + if self.editMode: + self.updateEditLine(self.input) + self.queueBlinkCursor() + + def escape(self, text): + return text. \ + replace('<', '<'). \ + replace('>', '>'). \ + replace('"', '"') + + def addLine(self, text): + curKids = list(self.textEl.child_nodes) + while len(curKids) >= self.rows: + self.textEl.RemoveChild(curKids[0]) + curKids = curKids[1:] + + line = self.document.CreateTextNode(self.escape(text) + '\n') + self.textEl.AppendChild(line) + self.lastLine = line + + def addLines(self, lines): + for line in lines: + self.addLine(line) + + def updateEditLine(self, newInput=''): + newText = self.consolePrompt + newInput + self.lastLine.text = self.escape(newText) + (self.blinkState and '_' or '') + self.input = newInput + + def scroll(self): + self.blinkState = False + self.updateEditLine(self.input + '\n') + + def handleKeyDown(self, event): + """ + Handle control keys + """ + keyId = event.parameters['key_identifier'] + if not self.editMode: + if keyId == rocket.key_identifier.PAUSE: + if event.parameters['ctrl_key']: + self.commandHandler(None) + + return + + if keyId == rocket.key_identifier.RETURN: + # emit line without cursor + self.scroll() + + # handle command + self.commandHandler(self.input) + + if self.editMode: + # start with new "command" + self.addLine(self.consolePrompt) + self.updateEditLine("") + + elif keyId == rocket.key_identifier.BACK: + self.updateEditLine(self.input[0:-1]) + + def handleTextInput(self, event): + if not self.editMode: + return + + # handle normal text character + data = event.parameters['data'] + if 32 <= data < 128: + self.updateEditLine(self.input + chr(data)) + + def newEditLine(self): + self.addLine("") + self.updateEditLine() + + def cls(self): + curKids = list(self.textEl.child_nodes) + for kid in curKids: + self.textEl.RemoveChild(kid) \ No newline at end of file diff --git a/samples/rocket-console/main.py b/samples/rocket-console/main.py new file mode 100644 index 0000000000..7269c4b960 --- /dev/null +++ b/samples/rocket-console/main.py @@ -0,0 +1,410 @@ +""" +Show how to use libRocket in Panda3D. +""" +import sys +from panda3d.core import loadPrcFile, loadPrcFileData, Point3,Vec4, Mat4, LoaderOptions # @UnusedImport +from panda3d.core import DirectionalLight, AmbientLight, PointLight +from panda3d.core import Texture, PNMImage +from panda3d.core import PandaSystem +import random +from direct.interval.LerpInterval import LerpHprInterval, LerpPosInterval, LerpFunc +from direct.showbase.ShowBase import ShowBase + +# workaround: https://www.panda3d.org/forums/viewtopic.php?t=10062&p=99697#p99054 +#from panda3d import rocket +import _rocketcore as rocket + +from panda3d.rocket import RocketRegion, RocketInputHandler + +loadPrcFileData("", "model-path $MAIN_DIR/assets") + +import console + +global globalClock + +class MyApp(ShowBase): + + def __init__(self): + ShowBase.__init__(self) + + self.win.setClearColor(Vec4(0.2, 0.2, 0.2, 1)) + + self.disableMouse() + + self.render.setShaderAuto() + + dlight = DirectionalLight('dlight') + alight = AmbientLight('alight') + dlnp = self.render.attachNewNode(dlight) + alnp = self.render.attachNewNode(alight) + dlight.setColor((0.8, 0.8, 0.5, 1)) + alight.setColor((0.2, 0.2, 0.2, 1)) + dlnp.setHpr(0, -60, 0) + self.render.setLight(dlnp) + self.render.setLight(alnp) + + # Put lighting on the main scene + plight = PointLight('plight') + plnp = self.render.attachNewNode(plight) + plnp.setPos(0, 0, 10) + self.render.setLight(plnp) + self.render.setLight(alnp) + + self.loadRocketFonts() + + self.loadingTask = None + + #self.startModelLoadingAsync() + self.startModelLoading() + + self.inputHandler = RocketInputHandler() + self.mouseWatcher.attachNewNode(self.inputHandler) + + self.openLoadingDialog() + + def loadRocketFonts(self): + """ Load fonts referenced from e.g. 'font-family' RCSS directives. + + Note: the name of the font as used in 'font-family' + is not always the same as the filename; + open the font in your OS to see its display name. + """ + rocket.LoadFontFace("modenine.ttf") + + + def startModelLoading(self): + self.monitorNP = None + self.keyboardNP = None + self.loadingError = False + + self.taskMgr.doMethodLater(1, self.loadModels, 'loadModels') + + def loadModels(self, task): + self.monitorNP = self.loader.loadModel("monitor") + self.keyboardNP = self.loader.loadModel("takeyga_kb") + + def startModelLoadingAsync(self): + """ + NOTE: this seems to invoke a few bugs (crashes, sporadic model + reading errors, etc) so is disabled for now... + """ + self.monitorNP = None + self.keyboardNP = None + self.loadingError = False + + # force the "loading" to take some time after the first run... + options = LoaderOptions() + options.setFlags(options.getFlags() | LoaderOptions.LFNoCache) + + def gotMonitorModel(model): + if not model: + self.loadingError = True + self.monitorNP = model + + self.loader.loadModel("monitor", loaderOptions=options, callback=gotMonitorModel) + + def gotKeyboardModel(model): + if not model: + self.loadingError = True + self.keyboardNP = model + + self.loader.loadModel("takeyga_kb", loaderOptions=options, callback=gotKeyboardModel) + + def openLoadingDialog(self): + self.userConfirmed = False + + self.windowRocketRegion = RocketRegion.make('pandaRocket', self.win) + self.windowRocketRegion.setActive(1) + + self.windowRocketRegion.setInputHandler(self.inputHandler) + + self.windowContext = self.windowRocketRegion.getContext() + + self.loadingDocument = self.windowContext.LoadDocument("loading.rml") + if not self.loadingDocument: + raise AssertionError("did not find loading.rml") + + self.loadingDots = 0 + el = self.loadingDocument.GetElementById('loadingLabel') + self.loadingText = el.first_child + self.stopLoadingTime = globalClock.getFrameTime() + 3 + self.loadingTask = self.taskMgr.add(self.cycleLoading, 'doc changer') + + + # note: you may encounter errors like 'KeyError: 'document'" + # when invoking events using methods from your own scripts with this + # obvious code: + # + # self.loadingDocument.AddEventListener('aboutToClose', + # self.onLoadingDialogDismissed, True) + # + # A workaround is to define callback methods in standalone Python + # files with event, self, and document defined to None. + # + # see https://www.panda3d.org/forums/viewtopic.php?f=4&t=16412 + # + + # Or, use this indirection technique to work around the problem, + # by publishing the app into the context, then accessing it through + # the document's context... + + self.windowContext.app = self + self.loadingDocument.AddEventListener('aboutToClose', + 'document.context.app.handleAboutToClose()', True) + + self.loadingDocument.Show() + + def handleAboutToClose(self): + self.userConfirmed = True + if self.monitorNP and self.keyboardNP: + self.onLoadingDialogDismissed() + + def attachCustomRocketEvent(self, document, rocketEventName, pandaHandler, once=False): + # handle custom event + + # note: you may encounter errors like 'KeyError: 'document'" + # when invoking events using methods from your own scripts with this + # obvious code: + # + # self.loadingDocument.AddEventListener('aboutToClose', + # self.onLoadingDialogDismissed, True) + # + # see https://www.panda3d.org/forums/viewtopic.php?f=4&t=16412 + + + # this technique converts Rocket events to Panda3D events + + pandaEvent = 'panda.' + rocketEventName + + document.AddEventListener( + rocketEventName, + "messenger.send('" + pandaEvent + "', [event])") + + if once: + self.acceptOnce(pandaEvent, pandaHandler) + else: + self.accept(pandaEvent, pandaHandler) + + + def cycleLoading(self, task): + """ + Update the "loading" text in the initial window until + the user presses Space, Enter, or Escape or clicks (see loading.rxml) + or sufficient time has elapsed (self.stopLoadingTime). + """ + text = self.loadingText + + now = globalClock.getFrameTime() + if self.monitorNP and self.keyboardNP: + text.text = "Ready" + if now > self.stopLoadingTime or self.userConfirmed: + self.onLoadingDialogDismissed() + return task.done + elif self.loadingError: + text.text = "Assets not found" + else: + count = 5 + intv = int(now * 4) % count # @UndefinedVariable + text.text = "Loading" + ("." * (1+intv)) + (" " * (2 - intv)) + + return task.cont + + def onLoadingDialogDismissed(self): + """ Once a models are loaded, stop 'loading' and proceed to 'start' """ + if self.loadingDocument: + if self.loadingTask: + self.taskMgr.remove(self.loadingTask) + self.loadingTask = None + + self.showStarting() + + def fadeOut(self, element, time): + """ Example updating RCSS attributes from code + by modifying the 'color' RCSS attribute to slowly + change from solid to transparent. + + element: the Rocket element whose style to modify + time: time in seconds for fadeout + """ + + # get the current color from RCSS effective style + color = element.style.color + # convert to RGBA form + prefix = color[:color.rindex(',')+1].replace('rgb(', 'rgba(') + + def updateAlpha(t): + # another way of setting style on a specific element + attr = 'color: ' + prefix + str(int(t)) +');' + element.SetAttribute('style', attr) + + alphaInterval = LerpFunc(updateAlpha, + duration=time, + fromData=255, + toData=0, + blendType='easeIn') + + return alphaInterval + + def showStarting(self): + """ Models are loaded, so update the dialog, + fade out, then transition to the console. """ + self.loadingText.text = 'Starting...' + + alphaInterval = self.fadeOut(self.loadingText, 0.5) + alphaInterval.setDoneEvent('fadeOutFinished') + + def fadeOutFinished(): + if self.loadingDocument: + self.loadingDocument.Close() + self.loadingDocument = None + self.createConsole() + + self.accept('fadeOutFinished', fadeOutFinished) + + alphaInterval.start() + + def createConsole(self): + """ Create the in-world console, which displays + a RocketRegion in a GraphicsBuffer, which appears + in a Texture on the monitor model. """ + + self.monitorNP.reparentTo(self.render) + self.monitorNP.setScale(1.5) + + self.keyboardNP.reparentTo(self.render) + self.keyboardNP.setHpr(-90, 0, 15) + self.keyboardNP.setScale(20) + + self.placeItems() + + self.setupRocketConsole() + + # re-enable mouse + mat=Mat4(self.camera.getMat()) + mat.invertInPlace() + self.mouseInterfaceNode.setMat(mat) + self.enableMouse() + + def placeItems(self): + self.camera.setPos(0, -20, 0) + self.camera.setHpr(0, 0, 0) + self.monitorNP.setPos(0, 0, 1) + self.keyboardNP.setPos(0, -5, -2.5) + + + def setupRocketConsole(self): + """ + Place a new rocket window onto a texture + bound to the front of the monitor. + """ + self.win.setClearColor(Vec4(0.5, 0.5, 0.8, 1)) + + faceplate = self.monitorNP.find("**/Faceplate") + assert faceplate + + mybuffer = self.win.makeTextureBuffer("Console Buffer", 1024, 512) + tex = mybuffer.getTexture() + tex.setMagfilter(Texture.FTLinear) + tex.setMinfilter(Texture.FTLinear) + + faceplate.setTexture(tex, 1) + + self.rocketConsole = RocketRegion.make('console', mybuffer) + self.rocketConsole.setInputHandler(self.inputHandler) + + self.consoleContext = self.rocketConsole.getContext() + self.console = console.Console(self, self.consoleContext, 40, 13, self.handleCommand) + + self.console.addLine("Panda DOS") + self.console.addLine("type 'help'") + self.console.addLine("") + + self.console.allowEditing(True) + + def handleCommand(self, command): + if command is None: + # hack for Ctrl-Break + self.spewInProgress = False + self.console.addLine("*** break ***") + self.console.allowEditing(True) + return + + command = command.strip() + if not command: + return + + tokens = [x.strip() for x in command.split(' ')] + command = tokens[0].lower() + + if command == 'help': + self.console.addLines([ + "Sorry, this is utter fakery.", + "You won't get much more", + "out of this simulation unless", + "you program it yourself. :)" + ]) + elif command == 'dir': + self.console.addLines([ + "Directory of C:\\:", + "HELP COM 72 05-06-2015 14:07", + "DIR COM 121 05-06-2015 14:11", + "SPEW COM 666 05-06-2015 15:02", + " 2 Files(s) 859 Bytes.", + " 0 Dirs(s) 7333 Bytes free.", + ""]) + elif command == 'cls': + self.console.cls() + elif command == 'echo': + self.console.addLine(' '.join(tokens[1:])) + elif command == 'ver': + self.console.addLine('Panda DOS v0.01 in Panda3D ' + PandaSystem.getVersionString()) + elif command == 'spew': + self.startSpew() + elif command == 'exit': + self.console.setPrompt("System is shutting down NOW!") + self.terminateMonitor() + else: + self.console.addLine("command not found") + + def startSpew(self): + self.console.allowEditing(False) + self.console.addLine("LINE NOISE 1.0") + self.console.addLine("") + + self.spewInProgress = True + + # note: spewage always occurs in 'doMethodLater'; + # time.sleep() would be pointless since the whole + # UI would be frozen during the wait. + self.queueSpew(2) + + def queueSpew(self, delay=0.1): + self.taskMgr.doMethodLater(delay, self.spew, 'spew') + + def spew(self, task): + # generate random spewage, just like on TV! + if not self.spewInProgress: + return + + def randchr(): + return chr(int(random.random() < 0.25 and 32 or random.randint(32, 127))) + + line = ''.join([randchr() for _ in range(40) ]) + + self.console.addLine(line) + self.queueSpew() + + def terminateMonitor(self): + alphaInterval = self.fadeOut(self.console.getTextContainer(), 2) + + alphaInterval.setDoneEvent('fadeOutFinished') + + def fadeOutFinished(): + sys.exit(0) + + self.accept('fadeOutFinished', fadeOutFinished) + + alphaInterval.start() + +app = MyApp() +app.run()