From 3abf3a0c8819c0807cf53bc70be485af9c305761 Mon Sep 17 00:00:00 2001 From: Younguk Kim Date: Tue, 28 Nov 2017 13:04:57 +0900 Subject: [PATCH 01/25] Add HAVE_PYTHON macro to build without python --- dtool/src/interrogatedb/py_wrappers.cxx | 4 ++++ dtool/src/interrogatedb/py_wrappers.h | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/dtool/src/interrogatedb/py_wrappers.cxx b/dtool/src/interrogatedb/py_wrappers.cxx index db4e229348..4d56eee41a 100755 --- a/dtool/src/interrogatedb/py_wrappers.cxx +++ b/dtool/src/interrogatedb/py_wrappers.cxx @@ -13,6 +13,8 @@ #include "py_wrappers.h" +#ifdef HAVE_PYTHON + #if PY_VERSION_HEX >= 0x03040000 #define _COLLECTIONS_ABC "_collections_abc" #elif PY_VERSION_HEX >= 0x03030000 @@ -1669,3 +1671,5 @@ Dtool_NewStaticProperty(PyTypeObject *type, const PyGetSetDef *getset) { } return (PyObject *)descr; } + +#endif // HAVE_PYTHON diff --git a/dtool/src/interrogatedb/py_wrappers.h b/dtool/src/interrogatedb/py_wrappers.h index 66f2183299..7bf2c2e19f 100755 --- a/dtool/src/interrogatedb/py_wrappers.h +++ b/dtool/src/interrogatedb/py_wrappers.h @@ -16,6 +16,8 @@ #include "py_panda.h" +#ifdef HAVE_PYTHON + /** * These classes are returned from properties that require a subscript * interface, ie. something.children[i] = 3. @@ -71,4 +73,6 @@ EXPCL_INTERROGATEDB Dtool_MappingWrapper *Dtool_NewMutableMappingWrapper(PyObjec EXPCL_INTERROGATEDB PyObject *Dtool_NewGenerator(PyObject *self, const char *name, iternextfunc func); EXPCL_INTERROGATEDB PyObject *Dtool_NewStaticProperty(PyTypeObject *obj, const PyGetSetDef *getset); -#endif +#endif // HAVE_PYTHON + +#endif // PY_WRAPPERS_H From f54b8be6760ac7638dde6bdc777a924f7024d20c Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 3 Dec 2017 15:48:02 +0100 Subject: [PATCH 02/25] Separate out Python compat hacks from py_panda.h into py_compat.h --- .../p3interrogatedb_composite2.cxx | 1 + dtool/src/interrogatedb/py_compat.cxx | 54 ++++++ dtool/src/interrogatedb/py_compat.h | 169 ++++++++++++++++++ dtool/src/interrogatedb/py_panda.cxx | 40 +---- dtool/src/interrogatedb/py_panda.h | 120 +------------ 5 files changed, 229 insertions(+), 155 deletions(-) create mode 100755 dtool/src/interrogatedb/py_compat.cxx create mode 100755 dtool/src/interrogatedb/py_compat.h diff --git a/dtool/src/interrogatedb/p3interrogatedb_composite2.cxx b/dtool/src/interrogatedb/p3interrogatedb_composite2.cxx index a46ce81558..fda72f2ce8 100644 --- a/dtool/src/interrogatedb/p3interrogatedb_composite2.cxx +++ b/dtool/src/interrogatedb/p3interrogatedb_composite2.cxx @@ -5,4 +5,5 @@ #include "interrogate_interface.cxx" #include "interrogate_request.cxx" #include "py_panda.cxx" +#include "py_compat.cxx" #include "py_wrappers.cxx" diff --git a/dtool/src/interrogatedb/py_compat.cxx b/dtool/src/interrogatedb/py_compat.cxx new file mode 100755 index 0000000000..f7f02607c0 --- /dev/null +++ b/dtool/src/interrogatedb/py_compat.cxx @@ -0,0 +1,54 @@ +/** + * 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 py_compat.cxx + * @author rdb + * @date 2017-12-03 + */ + +#include "py_compat.h" + +#ifdef HAVE_PYTHON + +PyTupleObject Dtool_EmptyTuple = {PyVarObject_HEAD_INIT(nullptr, 0)}; + +#if PY_MAJOR_VERSION < 3 +/** + * Given a long or int, returns a size_t, or raises an OverflowError if it is + * out of range. + */ +size_t PyLongOrInt_AsSize_t(PyObject *vv) { + if (PyInt_Check(vv)) { + long value = PyInt_AS_LONG(vv); + if (value < 0) { + PyErr_SetString(PyExc_OverflowError, + "can't convert negative value to size_t"); + return (size_t)-1; + } + return (size_t)value; + } + + if (!PyLong_Check(vv)) { + Dtool_Raise_TypeError("a long or int was expected"); + return (size_t)-1; + } + + size_t bytes; + int one = 1; + int res = _PyLong_AsByteArray((PyLongObject *)vv, (unsigned char *)&bytes, + SIZEOF_SIZE_T, (int)*(unsigned char*)&one, 0); + + if (res < 0) { + return (size_t)res; + } else { + return bytes; + } +} +#endif + +#endif // HAVE_PYTHON diff --git a/dtool/src/interrogatedb/py_compat.h b/dtool/src/interrogatedb/py_compat.h new file mode 100755 index 0000000000..852ba95a02 --- /dev/null +++ b/dtool/src/interrogatedb/py_compat.h @@ -0,0 +1,169 @@ +/** + * 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 py_compat.h + * @author rdb + * @date 2017-12-02 + */ + +#ifndef PY_COMPAT_H +#define PY_COMPAT_H + +#include "dtoolbase.h" + +#ifdef HAVE_PYTHON + +// The contents of this file were originally part of py_panda.h. It +// specifically contains polyfills that are required to maintain compatibility +// with Python 2 and older versions of Python 3. + +// These compatibility hacks are sorted by Python version that removes the +// need for the respective hack. + +#ifdef _POSIX_C_SOURCE +# undef _POSIX_C_SOURCE +#endif + +#ifdef _XOPEN_SOURCE +# undef _XOPEN_SOURCE +#endif + +// See PEP 353 +#define PY_SSIZE_T_CLEAN 1 + +#include "Python.h" + +/* Python 2.4 */ + +// 2.4 macros which aren't available in 2.3 +#ifndef Py_RETURN_NONE +# define Py_RETURN_NONE return Py_INCREF(Py_None), Py_None +#endif + +#ifndef Py_RETURN_TRUE +# define Py_RETURN_TRUE return Py_INCREF(Py_True), Py_True +#endif + +#ifndef Py_RETURN_FALSE +# define Py_RETURN_FALSE return Py_INCREF(Py_False), Py_False +#endif + +/* Python 2.5 */ + +// Prior to Python 2.5, we didn't have Py_ssize_t. +#if PY_VERSION_HEX < 0x02050000 +typedef int Py_ssize_t; +# define PyInt_FromSsize_t PyInt_FromLong +# define PyInt_AsSsize_t PyInt_AsLong +#endif + +/* Python 2.6 */ + +#ifndef Py_TYPE +# define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) +#endif + +/* Python 2.7, 3.1 */ + +#ifndef PyVarObject_HEAD_INIT + #define PyVarObject_HEAD_INIT(type, size) \ + PyObject_HEAD_INIT(type) size, +#endif + +/* Python 2.7, 3.2 */ + +#if PY_VERSION_HEX < 0x03020000 +# define PyErr_NewExceptionWithDoc(name, doc, base, dict) \ + PyErr_NewException(name, base, dict) +#endif + +/* Python 3.0 */ + +// Always on in Python 3 +#ifndef Py_TPFLAGS_CHECKTYPES +# define Py_TPFLAGS_CHECKTYPES 0 +#endif + +// Macros for writing code that will compile in both versions. +#if PY_MAJOR_VERSION >= 3 +# define nb_nonzero nb_bool +# define nb_divide nb_true_divide +# define nb_inplace_divide nb_inplace_true_divide + +# define PyLongOrInt_Check(x) PyLong_Check(x) +# define PyLongOrInt_AS_LONG PyLong_AS_LONG +# define PyInt_Check PyLong_Check +# define PyInt_AsLong PyLong_AsLong +# define PyInt_AS_LONG PyLong_AS_LONG +# define PyLongOrInt_AsSize_t PyLong_AsSize_t +#else +# define PyLongOrInt_Check(x) (PyInt_Check(x) || PyLong_Check(x)) +// PyInt_FromSize_t automatically picks the right type. +# define PyLongOrInt_AS_LONG PyInt_AsLong + +EXPCL_INTERROGATEDB size_t PyLongOrInt_AsSize_t(PyObject *); +#endif + +// Which character to use in PyArg_ParseTuple et al for a byte string. +#if PY_MAJOR_VERSION >= 3 +# define FMTCHAR_BYTES "y" +#else +# define FMTCHAR_BYTES "s" +#endif + +/* Python 3.2 */ + +#if PY_VERSION_HEX < 0x03020000 +typedef long Py_hash_t; +#endif + +/* Python 3.3 */ + +#if PY_MAJOR_VERSION >= 3 +// Python 3 versions before 3.3.3 defined this incorrectly. +# undef _PyErr_OCCURRED +# define _PyErr_OCCURRED() (PyThreadState_GET()->curexc_type) + +// Python versions before 3.3 did not define this. +# if PY_VERSION_HEX < 0x03030000 +# define PyUnicode_AsUTF8 _PyUnicode_AsString +# define PyUnicode_AsUTF8AndSize _PyUnicode_AsStringAndSize +# endif +#endif + +/* Python 3.6 */ + +// Used to implement _PyObject_CallNoArg +extern EXPCL_INTERROGATEDB PyTupleObject Dtool_EmptyTuple; + +#ifndef _PyObject_CallNoArg +# define _PyObject_CallNoArg(func) PyObject_Call((func), (PyObject *)&Dtool_EmptyTuple, nullptr) +#endif + +// Python versions before 3.6 didn't require longlong support to be enabled. +#ifndef HAVE_LONG_LONG +# define PyLong_FromLongLong(x) PyLong_FromLong((long) (x)) +# define PyLong_FromUnsignedLongLong(x) PyLong_FromUnsignedLong((unsigned long) (x)) +# define PyLong_AsLongLong(x) PyLong_AsLong(x) +# define PyLong_AsUnsignedLongLong(x) PyLong_AsUnsignedLong(x) +# define PyLong_AsUnsignedLongLongMask(x) PyLong_AsUnsignedLongMask(x) +# define PyLong_AsLongLongAndOverflow(x) PyLong_AsLongAndOverflow(x) +#endif + +/* Other Python implementations */ + +// _PyErr_OCCURRED is an undocumented macro version of PyErr_Occurred. +// Some implementations of the CPython API (e.g. PyPy's cpyext) do not define +// it, so in these cases we just silently fall back to PyErr_Occurred. +#ifndef _PyErr_OCCURRED +# define _PyErr_OCCURRED() PyErr_Occurred() +#endif + +#endif // HAVE_PYTHON + +#endif // PY_COMPAT_H diff --git a/dtool/src/interrogatedb/py_panda.cxx b/dtool/src/interrogatedb/py_panda.cxx index 5e453f207c..d218a6c12c 100644 --- a/dtool/src/interrogatedb/py_panda.cxx +++ b/dtool/src/interrogatedb/py_panda.cxx @@ -17,8 +17,6 @@ #ifdef HAVE_PYTHON -PyTupleObject Dtool_EmptyTuple; - PyMemberDef standard_type_members[] = { {(char *)"this", (sizeof(void*) == sizeof(int)) ? T_UINT : T_ULONGLONG, offsetof(Dtool_PyInstDef, _ptr_to_object), READONLY, (char *)"C++ 'this' pointer, if any"}, {(char *)"this_ownership", T_BOOL, offsetof(Dtool_PyInstDef, _memory_rules), READONLY, (char *)"C++ 'this' ownership rules"}, @@ -33,39 +31,6 @@ static RuntimeTypeMap runtime_type_map; static RuntimeTypeSet runtime_type_set; static NamedTypeMap named_type_map; -#if PY_MAJOR_VERSION < 3 -/** - * Given a long or int, returns a size_t, or raises an OverflowError if it is - * out of range. - */ -size_t PyLongOrInt_AsSize_t(PyObject *vv) { - if (PyInt_Check(vv)) { - long value = PyInt_AS_LONG(vv); - if (value < 0) { - PyErr_SetString(PyExc_OverflowError, - "can't convert negative value to size_t"); - return (size_t)-1; - } - return (size_t)value; - } - - if (!PyLong_Check(vv)) { - Dtool_Raise_TypeError("a long or int was expected"); - return (size_t)-1; - } - - size_t bytes; - int one = 1; - int res = _PyLong_AsByteArray((PyLongObject *)vv, (unsigned char *)&bytes, - SIZEOF_SIZE_T, (int)*(unsigned char*)&one, 0); - - if (res < 0) { - return (size_t)res; - } else { - return bytes; - } -} -#endif /** * Given a valid (non-NULL) PyObject, does a simple check to see if it might @@ -646,8 +611,9 @@ PyObject *Dtool_PyModuleInitHelper(LibraryDef *defs[], const char *modulename) { return Dtool_Raise_TypeError("PyType_Ready(Dtool_StaticProperty_Type)"); } - // Initialize the "empty tuple". - (void)PyObject_INIT_VAR((PyObject *)&Dtool_EmptyTuple, &PyTuple_Type, 0); +#ifdef Py_TRACE_REFS + _Py_AddToAllObjects((PyObject *)&Dtool_EmptyTuple, 0); +#endif // Initialize the base class of everything. Dtool_PyModuleClassInit_DTOOL_SUPER_BASE(NULL); diff --git a/dtool/src/interrogatedb/py_panda.h b/dtool/src/interrogatedb/py_panda.h index adcce27b9c..fad7ccedb0 100644 --- a/dtool/src/interrogatedb/py_panda.h +++ b/dtool/src/interrogatedb/py_panda.h @@ -20,131 +20,15 @@ #define Py_DEBUG #endif -#ifndef NO_RUNTIME_TYPES - -#include "dtoolbase.h" -#include "typedObject.h" -#include "typeRegistry.h" - -#endif - #include "pnotify.h" #include "vector_uchar.h" #if defined(HAVE_PYTHON) && !defined(CPPPARSER) -#ifdef _POSIX_C_SOURCE -#undef _POSIX_C_SOURCE -#endif - -#ifdef _XOPEN_SOURCE -#undef _XOPEN_SOURCE -#endif - -#define PY_SSIZE_T_CLEAN 1 - -#include "Python.h" +// py_compat.h includes Python.h. +#include "py_compat.h" #include "structmember.h" -#ifndef HAVE_LONG_LONG -#define PyLong_FromLongLong(x) PyLong_FromLong((long) (x)) -#define PyLong_FromUnsignedLongLong(x) PyLong_FromUnsignedLong((unsigned long) (x)) -#define PyLong_AsLongLong(x) PyLong_AsLong(x) -#define PyLong_AsUnsignedLongLong(x) PyLong_AsUnsignedLong(x) -#define PyLong_AsUnsignedLongLongMask(x) PyLong_AsUnsignedLongMask(x) -#define PyLong_AsLongLongAndOverflow(x) PyLong_AsLongAndOverflow(x) -#endif - -#if PY_VERSION_HEX < 0x02050000 - -// Prior to Python 2.5, we didn't have Py_ssize_t. -typedef int Py_ssize_t; -#define PyInt_FromSsize_t PyInt_FromLong -#define PyInt_AsSsize_t PyInt_AsLong - -#endif // PY_VERSION_HEX - -// 2.4 macros which aren't available in 2.3 -#ifndef Py_RETURN_NONE -inline PyObject* doPy_RETURN_NONE() -{ Py_INCREF(Py_None); return Py_None; } -#define Py_RETURN_NONE return doPy_RETURN_NONE() -#endif - -#ifndef Py_RETURN_TRUE -inline PyObject* doPy_RETURN_TRUE() -{Py_INCREF(Py_True); return Py_True;} -#define Py_RETURN_TRUE return doPy_RETURN_TRUE() -#endif - -#ifndef Py_RETURN_FALSE -inline PyObject* doPy_RETURN_FALSE() -{Py_INCREF(Py_False); return Py_False;} -#define Py_RETURN_FALSE return doPy_RETURN_FALSE() -#endif - -#ifndef PyVarObject_HEAD_INIT -#define PyVarObject_HEAD_INIT(type, size) \ - PyObject_HEAD_INIT(type) size, -#endif - -#ifndef Py_TYPE -#define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) -#endif - -#ifndef Py_TPFLAGS_CHECKTYPES -// Always on in Python 3 -#define Py_TPFLAGS_CHECKTYPES 0 -#endif - -#if PY_MAJOR_VERSION >= 3 -// For writing code that will compile in both versions. -#define nb_nonzero nb_bool -#define nb_divide nb_true_divide -#define nb_inplace_divide nb_inplace_true_divide - -#define PyLongOrInt_Check(x) PyLong_Check(x) -#define PyLongOrInt_AS_LONG PyLong_AS_LONG -#define PyInt_Check PyLong_Check -#define PyInt_AsLong PyLong_AsLong -#define PyInt_AS_LONG PyLong_AS_LONG -#define PyLongOrInt_AsSize_t PyLong_AsSize_t -#else -#define PyLongOrInt_Check(x) (PyInt_Check(x) || PyLong_Check(x)) -// PyInt_FromSize_t automatically picks the right type. -#define PyLongOrInt_AS_LONG PyInt_AsLong - -EXPCL_INTERROGATEDB size_t PyLongOrInt_AsSize_t(PyObject *); - -// For more portably defining hash functions. -typedef long Py_hash_t; -#endif - -#if PY_MAJOR_VERSION >= 3 -// Python 3 versions before 3.3.3 defined this incorrectly. -#undef _PyErr_OCCURRED -#define _PyErr_OCCURRED() (PyThreadState_GET()->curexc_type) - -// Python versions before 3.3 did not define this. -#if PY_VERSION_HEX < 0x03030000 -#define PyUnicode_AsUTF8 _PyUnicode_AsString -#define PyUnicode_AsUTF8AndSize _PyUnicode_AsStringAndSize -#endif -#endif - -// Which character to use in PyArg_ParseTuple et al for a byte string. -#if PY_MAJOR_VERSION >= 3 -#define FMTCHAR_BYTES "y" -#else -#define FMTCHAR_BYTES "s" -#endif - -extern EXPCL_INTERROGATEDB PyTupleObject Dtool_EmptyTuple; - -#ifndef _PyObject_CallNoArg -#define _PyObject_CallNoArg(func) PyObject_Call((func), (PyObject *)&Dtool_EmptyTuple, NULL) -#endif - using namespace std; // this is tempory .. untill this is glued better into the panda build system From 2e20a0f16ed4c08030f978e46950ba635fca8a32 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 4 Dec 2017 19:51:29 +0100 Subject: [PATCH 03/25] Implement awaitable thread-safe future for async operations This introduces AsyncFuture as a new base class of AsyncTask. It's modelled after asyncio's Future class, except that it is thread-safe and you can use result() to block the current thread waiting for the future to finish (of course this is not necessary for use with coroutines). AsyncFuture should be used for any operation that finishes in the future, to get the benefit of awaitability within coroutines as well as a standard interface for querying status and results of the operation as well as cancelling it. As such, it's been implemented in various places, including texture.prepare() and win.trigger_copy(). Note that AsyncFuture is intended to be used *once*; it cannot be used more than once. As an example of how this works, tex.prepare() will return the same future as long as the prepare isn't complete, but when it is done, subsequent calls to tex.prepare() will return a new future. --- direct/src/interval/Interval.py | 4 +- direct/src/showbase/Loader.py | 36 ++- direct/src/showbase/Messenger.py | 12 +- direct/src/task/Task.py | 4 +- makepanda/makepanda.py | 4 +- panda/src/audio/audioLoadRequest.I | 24 +- panda/src/audio/audioLoadRequest.cxx | 3 +- panda/src/audio/audioLoadRequest.h | 5 - panda/src/display/graphicsOutput.I | 12 +- panda/src/display/graphicsOutput.cxx | 6 +- panda/src/display/graphicsOutput.h | 5 +- panda/src/display/graphicsStateGuardian.cxx | 19 +- panda/src/display/graphicsStateGuardian.h | 2 +- panda/src/event/asyncFuture.I | 109 +++++++++ panda/src/event/asyncFuture.cxx | 195 ++++++++++++++++ panda/src/event/asyncFuture.h | 135 +++++++++++ panda/src/event/asyncFuture_ext.cxx | 175 +++++++++++++++ .../{asyncTask_ext.h => asyncFuture_ext.h} | 14 +- panda/src/event/asyncTask.I | 9 - panda/src/event/asyncTask.cxx | 81 +++++-- panda/src/event/asyncTask.h | 24 +- panda/src/event/asyncTaskChain.cxx | 45 ++-- panda/src/event/asyncTaskChain.h | 3 +- panda/src/event/asyncTaskCollection.cxx | 38 ++-- panda/src/event/asyncTaskCollection.h | 10 +- panda/src/event/asyncTaskManager.I | 2 +- panda/src/event/asyncTaskManager.cxx | 20 +- panda/src/event/asyncTaskManager.h | 7 +- panda/src/event/asyncTask_ext.cxx | 75 ------- panda/src/event/config_event.cxx | 2 + panda/src/event/eventHandler.cxx | 32 +++ panda/src/event/eventHandler.h | 8 +- panda/src/event/eventParameter.I | 7 - panda/src/event/eventParameter.h | 3 +- panda/src/event/p3event_composite1.cxx | 1 + panda/src/event/pythonTask.I | 11 +- panda/src/event/pythonTask.cxx | 114 ++++------ panda/src/event/pythonTask.h | 15 +- panda/src/gobj/animateVerticesRequest.I | 7 +- panda/src/gobj/animateVerticesRequest.cxx | 1 - panda/src/gobj/animateVerticesRequest.h | 3 +- panda/src/gobj/config_gobj.cxx | 1 + panda/src/gobj/preparedGraphicsObjects.cxx | 154 ++++++++++++- panda/src/gobj/preparedGraphicsObjects.h | 56 ++++- panda/src/gobj/shader.cxx | 5 +- panda/src/gobj/shader.h | 3 +- panda/src/gobj/texture.cxx | 5 +- panda/src/gobj/texture.h | 3 +- panda/src/gobj/textureReloadRequest.I | 7 +- panda/src/gobj/textureReloadRequest.cxx | 1 - panda/src/gobj/textureReloadRequest.h | 3 +- panda/src/pgraph/geomNode.cxx | 9 +- panda/src/pgraph/loader.I | 1 + panda/src/pgraph/modelFlattenRequest.I | 28 +-- panda/src/pgraph/modelFlattenRequest.cxx | 4 +- panda/src/pgraph/modelFlattenRequest.h | 5 - panda/src/pgraph/modelLoadRequest.I | 29 +-- panda/src/pgraph/modelLoadRequest.cxx | 7 +- panda/src/pgraph/modelLoadRequest.h | 5 - panda/src/pgraph/modelSaveRequest.I | 18 +- panda/src/pgraph/modelSaveRequest.cxx | 2 - panda/src/pgraph/modelSaveRequest.h | 4 - panda/src/pipeline/asyncTaskBase.I | 12 - panda/src/pipeline/asyncTaskBase.cxx | 77 ------- panda/src/pipeline/asyncTaskBase.h | 61 ----- panda/src/pipeline/config_pipeline.cxx | 2 - panda/src/pipeline/p3pipeline_composite1.cxx | 1 - panda/src/pipeline/thread.I | 2 +- panda/src/pipeline/thread.h | 8 +- tests/event/test_futures.py | 212 ++++++++++++++++++ 70 files changed, 1404 insertions(+), 603 deletions(-) create mode 100644 panda/src/event/asyncFuture.I create mode 100644 panda/src/event/asyncFuture.cxx create mode 100644 panda/src/event/asyncFuture.h create mode 100644 panda/src/event/asyncFuture_ext.cxx rename panda/src/event/{asyncTask_ext.h => asyncFuture_ext.h} (69%) mode change 100755 => 100644 delete mode 100755 panda/src/event/asyncTask_ext.cxx delete mode 100644 panda/src/pipeline/asyncTaskBase.I delete mode 100644 panda/src/pipeline/asyncTaskBase.cxx delete mode 100644 panda/src/pipeline/asyncTaskBase.h create mode 100644 tests/event/test_futures.py diff --git a/direct/src/interval/Interval.py b/direct/src/interval/Interval.py index 21a79f016a..02a5f4bc00 100644 --- a/direct/src/interval/Interval.py +++ b/direct/src/interval/Interval.py @@ -116,8 +116,9 @@ class Interval(DirectObject): return self.currT def start(self, startT = 0.0, endT = -1.0, playRate = 1.0): + """ Starts the interval. Returns an awaitable. """ self.setupPlay(startT, endT, playRate, 0) - self.__spawnTask() + return self.__spawnTask() def loop(self, startT = 0.0, endT = -1.0, playRate = 1.0): self.setupPlay(startT, endT, playRate, 1) @@ -427,6 +428,7 @@ class Interval(DirectObject): task = Task(self.__playTask) task.interval = self taskMgr.add(task, taskName) + return task def __removeTask(self): # Kill old task(s), including those from a similarly-named but diff --git a/direct/src/showbase/Loader.py b/direct/src/showbase/Loader.py index 64fa593940..258cbf0019 100644 --- a/direct/src/showbase/Loader.py +++ b/direct/src/showbase/Loader.py @@ -66,7 +66,9 @@ class Loader(DirectObject): return not self.requests def result(self): - assert not self.requests, "Result is not ready." + "Returns the results, suspending the thread to wait if necessary." + for r in list(self.requests): + r.wait() if self.gotList: return self.objects else: @@ -132,7 +134,8 @@ class Loader(DirectObject): # model loading funcs def loadModel(self, modelPath, loaderOptions = None, noCache = None, allowInstance = False, okMissing = None, - callback = None, extraArgs = [], priority = None): + callback = None, extraArgs = [], priority = None, + blocking = None): """ Attempts to load a model or models from one or more relative pathnames. If the input modelPath is a string (a single model @@ -169,10 +172,10 @@ class Loader(DirectObject): If callback is not None, then the model load will be performed asynchronously. In this case, loadModel() will initiate a background load and return immediately. The return value will - be an object that may later be passed to - loader.cancelRequest() to cancel the asynchronous request. At - some later point, when the requested model(s) have finished - loading, the callback function will be invoked with the n + be an object that can be used to check the status, cancel the + request, or use it in an `await` expression. Unless callback + is the special value True, when the requested model(s) have + finished loading, it will be invoked with the n loaded models passed as its parameter list. It is possible that the callback will be invoked immediately, even before loadModel() returns. If you use callback, you may also @@ -224,7 +227,10 @@ class Loader(DirectObject): modelList = modelPath gotList = True - if callback is None: + if blocking is None: + blocking = callback is None + + if blocking: # We got no callback, so it's a synchronous load. result = [] @@ -362,7 +368,8 @@ class Loader(DirectObject): ModelPool.releaseModel(modelNode) def saveModel(self, modelPath, node, loaderOptions = None, - callback = None, extraArgs = [], priority = None): + callback = None, extraArgs = [], priority = None, + blocking = None): """ Saves the model (a NodePath or PandaNode) to the indicated filename path. Returns true on success, false on failure. If a callback is used, the model is saved asynchronously, and the @@ -397,7 +404,10 @@ class Loader(DirectObject): # From here on, we deal with a list of (filename, node) pairs. modelList = list(zip(modelList, nodeList)) - if callback is None: + if blocking is None: + blocking = callback is None + + if blocking: # We got no callback, so it's a synchronous save. result = [] @@ -1059,7 +1069,7 @@ class Loader(DirectObject): return cb, i = self._requests[request] - if cb.cancelled(): + if cb.cancelled() or request.cancelled(): # Shouldn't be here. del self._requests[request] return @@ -1068,7 +1078,11 @@ class Loader(DirectObject): if not cb.requests: del self._requests[request] - cb.gotObject(i, request.result() or None) + result = request.result() + if isinstance(result, PandaNode): + result = NodePath(result) + + cb.gotObject(i, result) load_model = loadModel unload_model = unloadModel diff --git a/direct/src/showbase/Messenger.py b/direct/src/showbase/Messenger.py index 08062e4a3e..4ac1b96f56 100644 --- a/direct/src/showbase/Messenger.py +++ b/direct/src/showbase/Messenger.py @@ -109,6 +109,12 @@ class Messenger: if record[0] <= 0: del self._id2object[id] + def future(self, event): + """ Returns a future that is triggered by the given event name. This + will function only once. """ + + return eventMgr.eventHandler.get_future(event) + def accept(self, event, object, method, extraArgs=[], persistent=1): """ accept(self, string, DirectObject, Function, List, Boolean) @@ -409,10 +415,14 @@ class Messenger: # Release the lock temporarily while we call the method. self.lock.release() try: - method (*(extraArgs + sentArgs)) + result = method (*(extraArgs + sentArgs)) finally: self.lock.acquire() + if hasattr(result, 'cr_await'): + # It's a coroutine, so schedule it with the task manager. + taskMgr.add(result) + def clear(self): """ Start fresh with a clear dict diff --git a/direct/src/task/Task.py b/direct/src/task/Task.py index 08baa924a5..8181638264 100644 --- a/direct/src/task/Task.py +++ b/direct/src/task/Task.py @@ -74,7 +74,9 @@ Task = PythonTask # Copy the module-level enums above into the class level. This funny # syntax is necessary because it's a C++-wrapped extension type, not a # true Python class. -Task.DtoolClassDict['done'] = done +# We can't override 'done', which is already a known method. We have a +# special check in PythonTask for when the method is being returned. +#Task.DtoolClassDict['done'] = done Task.DtoolClassDict['cont'] = cont Task.DtoolClassDict['again'] = again Task.DtoolClassDict['pickup'] = pickup diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 75e4ecfe45..a231ec6182 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -3632,7 +3632,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_asyncFuture_ext.obj', opts=OPTS, input='asyncFuture_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) @@ -4226,7 +4226,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_asyncFuture_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 0daf2df619..647692315d 100644 --- a/panda/src/audio/audioLoadRequest.I +++ b/panda/src/audio/audioLoadRequest.I @@ -20,8 +20,7 @@ AudioLoadRequest(AudioManager *audio_manager, const string &filename, bool positional) : _audio_manager(audio_manager), _filename(filename), - _positional(positional), - _is_ready(false) + _positional(positional) { } @@ -55,31 +54,22 @@ get_positional() const { * Returns true if this request has completed, false if it is still pending. * When this returns true, you may retrieve the sound loaded by calling * get_sound(). + * Equivalent to `req.done() and not req.cancelled()`. + * @see done() */ INLINE bool AudioLoadRequest:: is_ready() const { - return _is_ready; + return (FutureState)AtomicAdjust::get(_future_state) == FS_finished; } /** * 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 + * there was an error. It is an error to call this unless done() 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; + nassertr_always(done(), nullptr); + return (AudioSound *)_result; } diff --git a/panda/src/audio/audioLoadRequest.cxx b/panda/src/audio/audioLoadRequest.cxx index c44780b354..e3a04d38b8 100644 --- a/panda/src/audio/audioLoadRequest.cxx +++ b/panda/src/audio/audioLoadRequest.cxx @@ -21,8 +21,7 @@ TypeHandle AudioLoadRequest::_type_handle; */ AsyncTask::DoneStatus AudioLoadRequest:: do_task() { - _sound = _audio_manager->get_sound(_filename, _positional); - _is_ready = true; + set_result(_audio_manager->get_sound(_filename, _positional)); // Don't continue the task; we're done. return DS_done; diff --git a/panda/src/audio/audioLoadRequest.h b/panda/src/audio/audioLoadRequest.h index d70a37d5b7..6d0e59bf4c 100644 --- a/panda/src/audio/audioLoadRequest.h +++ b/panda/src/audio/audioLoadRequest.h @@ -43,8 +43,6 @@ PUBLISHED: INLINE bool is_ready() const; INLINE AudioSound *get_sound() const; - INLINE AudioSound *result() const; - protected: virtual DoneStatus do_task(); @@ -53,9 +51,6 @@ private: string _filename; bool _positional; - bool _is_ready; - PT(AudioSound) _sound; - public: static TypeHandle get_class_type() { return _type_handle; diff --git a/panda/src/display/graphicsOutput.I b/panda/src/display/graphicsOutput.I index 40a90af57e..393dbaadb5 100644 --- a/panda/src/display/graphicsOutput.I +++ b/panda/src/display/graphicsOutput.I @@ -489,10 +489,16 @@ get_child_sort() const { /** * When the GraphicsOutput is in triggered copy mode, this function triggers * the copy (at the end of the next frame). + * @returns a future that can be awaited. */ -INLINE void GraphicsOutput:: -trigger_copy() { - _trigger_copy = true; +INLINE AsyncFuture *GraphicsOutput:: +trigger_copy() { + AsyncFuture *future = _trigger_copy; + if (future == nullptr) { + future = new AsyncFuture; + _trigger_copy = future; + } + return future; } /** diff --git a/panda/src/display/graphicsOutput.cxx b/panda/src/display/graphicsOutput.cxx index 40895cff1f..46b051cde0 100644 --- a/panda/src/display/graphicsOutput.cxx +++ b/panda/src/display/graphicsOutput.cxx @@ -115,7 +115,6 @@ GraphicsOutput(GraphicsEngine *engine, GraphicsPipe *pipe, _sbs_left_dimensions.set(0.0f, 1.0f, 0.0f, 1.0f); _sbs_right_dimensions.set(0.0f, 1.0f, 0.0f, 1.0f); _delete_flag = false; - _trigger_copy = false; if (_fb_properties.is_single_buffered()) { _draw_buffer_type = RenderBuffer::T_front; @@ -1435,7 +1434,10 @@ copy_to_textures() { } } } - _trigger_copy = false; + if (_trigger_copy != nullptr) { + _trigger_copy->set_result(nullptr); + _trigger_copy = nullptr; + } return okflag; } diff --git a/panda/src/display/graphicsOutput.h b/panda/src/display/graphicsOutput.h index 295467f458..2f9eff8683 100644 --- a/panda/src/display/graphicsOutput.h +++ b/panda/src/display/graphicsOutput.h @@ -40,6 +40,7 @@ #include "cycleDataWriter.h" #include "pipelineCycler.h" #include "updateSeq.h" +#include "asyncFuture.h" class PNMImage; class GraphicsEngine; @@ -197,7 +198,7 @@ PUBLISHED: INLINE int get_child_sort() const; MAKE_PROPERTY(child_sort, get_child_sort, set_child_sort); - INLINE void trigger_copy(); + INLINE AsyncFuture *trigger_copy(); INLINE DisplayRegion *make_display_region(); INLINE DisplayRegion *make_display_region(PN_stdfloat l, PN_stdfloat r, PN_stdfloat b, PN_stdfloat t); @@ -330,7 +331,7 @@ protected: int _target_tex_view; DisplayRegion *_prev_page_dr; PT(GeomNode) _texture_card; - bool _trigger_copy; + PT(AsyncFuture) _trigger_copy; class RenderTexture { public: diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx index f4672a0198..2d07f2c971 100644 --- a/panda/src/display/graphicsStateGuardian.cxx +++ b/panda/src/display/graphicsStateGuardian.cxx @@ -3184,15 +3184,15 @@ get_untextured_state() { * Should be called when a texture is encountered that needs to have its RAM * image reloaded, and get_incomplete_render() is true. This will fire off a * thread on the current Loader object that will request the texture to load - * its image. The image will be available at some point in the future (no - * event will be generated). + * its image. The image will be available at some point in the future. + * @returns a future object that can be used to check its status. */ -void GraphicsStateGuardian:: +AsyncFuture *GraphicsStateGuardian:: async_reload_texture(TextureContext *tc) { - nassertv(_loader != (Loader *)NULL); + nassertr(_loader != nullptr, nullptr); int priority = 0; - if (_current_display_region != (DisplayRegion *)NULL) { + if (_current_display_region != nullptr) { priority = _current_display_region->get_texture_reload_priority(); } @@ -3201,15 +3201,15 @@ async_reload_texture(TextureContext *tc) { // See if we are already loading this task. AsyncTaskCollection orig_tasks = task_mgr->find_tasks(task_name); - int num_tasks = orig_tasks.get_num_tasks(); - for (int ti = 0; ti < num_tasks; ++ti) { + size_t num_tasks = orig_tasks.get_num_tasks(); + for (size_t ti = 0; ti < num_tasks; ++ti) { AsyncTask *task = orig_tasks.get_task(ti); if (task->is_exact_type(TextureReloadRequest::get_class_type()) && - DCAST(TextureReloadRequest, task)->get_texture() == tc->get_texture()) { + ((TextureReloadRequest *)task)->get_texture() == tc->get_texture()) { // This texture is already queued to be reloaded. Don't queue it again, // just make sure the priority is updated, and return. task->set_priority(max(task->get_priority(), priority)); - return; + return (AsyncFuture *)task; } } @@ -3220,6 +3220,7 @@ async_reload_texture(TextureContext *tc) { _supports_compressed_texture); request->set_priority(priority); _loader->load_async(request); + return (AsyncFuture *)request.p(); } /** diff --git a/panda/src/display/graphicsStateGuardian.h b/panda/src/display/graphicsStateGuardian.h index 52cae8ff28..8a0ca541d0 100644 --- a/panda/src/display/graphicsStateGuardian.h +++ b/panda/src/display/graphicsStateGuardian.h @@ -460,7 +460,7 @@ protected: static CPT(RenderState) get_unclipped_state(); static CPT(RenderState) get_untextured_state(); - void async_reload_texture(TextureContext *tc); + AsyncFuture *async_reload_texture(TextureContext *tc); protected: PT(SceneSetup) _scene_null; diff --git a/panda/src/event/asyncFuture.I b/panda/src/event/asyncFuture.I new file mode 100644 index 0000000000..07191a34c9 --- /dev/null +++ b/panda/src/event/asyncFuture.I @@ -0,0 +1,109 @@ +/** + * 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 asyncFuture.I + * @author rdb + * @date 2017-11-28 + */ + +/** + * Initializes the future in the pending state. + */ +INLINE AsyncFuture:: +AsyncFuture() : + _manager(nullptr), + _cvar(nullptr), + _future_state(FS_pending), + _result(nullptr) { +} + +/** + * Returns true if the future is done or has been cancelled. It is always + * safe to call this. + */ +INLINE bool AsyncFuture:: +done() const { + return (FutureState)AtomicAdjust::get(_future_state) != FS_pending; +} + +/** + * Returns true if the future was cancelled. It is always safe to call this. + */ +INLINE bool AsyncFuture:: +cancelled() const { + return (FutureState)AtomicAdjust::get(_future_state) == FS_cancelled; +} + +/** + * Sets the event name that will be triggered when the future finishes. Will + * not be triggered if the future is cancelled, but it will be triggered for + * a coroutine task that exits with an exception. + */ +INLINE void AsyncFuture:: +set_done_event(const string &done_event) { + _done_event = done_event; +} + +/** + * Returns the event name that will be triggered when the future finishes. + * See set_done_event(). + */ +INLINE const string &AsyncFuture:: +get_done_event() const { + return _done_event; +} + +/** + * Returns this future's result. Can only be called if done() returns true. + */ +INLINE TypedObject *AsyncFuture:: +get_result() const { + // This is thread safe, since _result may no longer be modified after the + // state is changed to "done". + nassertr_always(done(), nullptr); + return _result; +} + +/** + * Returns this future's result as a pair of TypedObject, ReferenceCount + * pointers. Can only be called if done() returns true. + */ +INLINE void AsyncFuture:: +get_result(TypedObject *&ptr, ReferenceCount *&ref_ptr) const { + // This is thread safe, since _result may no longer be modified after the + // state is changed to "done". + nassertd(done()) { + ptr = nullptr; + ref_ptr = nullptr; + } + ptr = _result; + ref_ptr = _result_ref.p(); +} + +/** + * Sets this future's result. Can only be called if done() returns false. + */ +INLINE void AsyncFuture:: +set_result(nullptr_t) { + set_result(nullptr, nullptr); +} + +INLINE void AsyncFuture:: +set_result(TypedObject *result) { + set_result(result, nullptr); +} + +INLINE void AsyncFuture:: +set_result(TypedReferenceCount *result) { + set_result(result, result); +} + +INLINE void AsyncFuture:: +set_result(TypedWritableReferenceCount *result) { + set_result(result, result); +} diff --git a/panda/src/event/asyncFuture.cxx b/panda/src/event/asyncFuture.cxx new file mode 100644 index 0000000000..bb299239d7 --- /dev/null +++ b/panda/src/event/asyncFuture.cxx @@ -0,0 +1,195 @@ +/** + * 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 asyncFuture.cxx + * @author rdb + * @date 2017-11-28 + */ + +#include "asyncFuture.h" +#include "asyncTask.h" +#include "asyncTaskManager.h" +#include "conditionVarFull.h" +#include "config_event.h" +#include "pStatTimer.h" +#include "throw_event.h" + +TypeHandle AsyncFuture::_type_handle; + +/** + * Destroys the future. Assumes notify_done() has already been called. + */ +AsyncFuture:: +~AsyncFuture() { + delete _cvar; + nassertv(_waiting_tasks.empty()); +} + +/** + * Cancels the future. Returns true if it was cancelled, or false if the + * future was already done. + * In the case of a task, this is equivalent to remove(). + */ +bool AsyncFuture:: +cancel() { + if (!done()) { + if (_manager == nullptr) { + _manager = AsyncTaskManager::get_global_ptr(); + } + MutexHolder holder(_manager->_lock); + notify_done(false); + return true; + } else { + return false; + } +} + +/** + * + */ +void AsyncFuture:: +output(ostream &out) const { + out << get_type(); +} + +/** + * Waits until the future is done. + */ +void AsyncFuture:: +wait() { + if (done()) { + return; + } + + // If we don't have a manager, use the global one. + if (_manager == nullptr) { + _manager = AsyncTaskManager::get_global_ptr(); + } + + MutexHolder holder(_manager->_lock); + if (!done()) { + if (_cvar == nullptr) { + _cvar = new ConditionVarFull(_manager->_lock); + } + if (task_cat.is_debug()) { + task_cat.debug() + << "Waiting for future " << *this << "\n"; + } + PStatTimer timer(AsyncTaskChain::_wait_pcollector); + while (!done()) { + _cvar->wait(); + } + } +} + +/** + * Waits until the future is done, or until the timeout is reached. + */ +void AsyncFuture:: +wait(double timeout) { + if (done()) { + return; + } + + // If we don't have a manager, use the global one. + if (_manager == nullptr) { + _manager = AsyncTaskManager::get_global_ptr(); + } + + MutexHolder holder(_manager->_lock); + if (!done()) { + if (_cvar == nullptr) { + _cvar = new ConditionVarFull(_manager->_lock); + } + if (task_cat.is_debug()) { + task_cat.debug() + << "Waiting up to " << timeout << " seconds for future " << *this << "\n"; + } + PStatTimer timer(AsyncTaskChain::_wait_pcollector); + _cvar->wait(timeout); + } +} + +/** + * Schedules the done callbacks. Assumes the manager's lock is held, and that + * the future is currently in the 'pending' state. + * @param clean_exit true if finished successfully, false if cancelled. + */ +void AsyncFuture:: +notify_done(bool clean_exit) { + nassertv(_manager != nullptr); + nassertv(_manager->_lock.debug_is_locked()); + nassertv(_future_state == FS_pending); + + pvector::iterator it; + for (it = _waiting_tasks.begin(); it != _waiting_tasks.end(); ++it) { + AsyncTask *task = *it; + nassertd(task->_manager == _manager) continue; + task->_state = AsyncTask::S_active; + task->_chain->_active.push_back(task); + --task->_chain->_num_awaiting_tasks; + unref_delete(task); + } + _waiting_tasks.clear(); + + // For historical reasons, we don't send the "done event" if the future was + // cancelled. + if (clean_exit && !_done_event.empty()) { + PT_Event event = new Event(_done_event); + event->add_parameter(EventParameter(this)); + throw_event(move(event)); + } + + nassertv_always(FS_pending == + (FutureState)AtomicAdjust::compare_and_exchange( + _future_state, + (AtomicAdjust::Integer)FS_pending, + (AtomicAdjust::Integer)(clean_exit ? FS_finished : FS_cancelled))); + + // Finally, notify any threads that may be waiting. + if (_cvar != nullptr) { + _cvar->notify_all(); + } +} + +/** + * Sets this future's result. Can only be done while the future is not done. + * Calling this marks the future as done and schedules the done callbacks. + * + * This variant takes two pointers; the second one is only set if this object + * inherits from ReferenceCount, so that a reference can be held. + * + * Assumes the manager's lock is *not* held. + */ +void AsyncFuture:: +set_result(TypedObject *ptr, ReferenceCount *ref_ptr) { + nassertv(!done()); + // If we don't have a manager, use the global one. + if (_manager == nullptr) { + _manager = AsyncTaskManager::get_global_ptr(); + } + MutexHolder holder(_manager->_lock); + _result = ptr; + _result_ref = ref_ptr; + notify_done(true); +} + +/** + * Indicates that the given task is waiting for this future to complete. When + * the future is done, it will reactivate the given task. + * Assumes the manager's lock is held. + */ +void AsyncFuture:: +add_waiting_task(AsyncTask *task) { + nassertv(!done()); + nassertv(_manager != nullptr); + nassertv(_manager->_lock.debug_is_locked()); + task->ref(); + _waiting_tasks.push_back(task); + nassertv(task->_manager == _manager); +} diff --git a/panda/src/event/asyncFuture.h b/panda/src/event/asyncFuture.h new file mode 100644 index 0000000000..456c872c04 --- /dev/null +++ b/panda/src/event/asyncFuture.h @@ -0,0 +1,135 @@ +/** + * 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 asyncFuture.h + * @author rdb + * @date 2017-11-28 + */ + +#ifndef ASYNCFUTURE_H +#define ASYNCFUTURE_H + +#include "pandabase.h" +#include "typedReferenceCount.h" +#include "typedWritableReferenceCount.h" +#include "conditionVar.h" +#include "atomicAdjust.h" + +class AsyncTaskManager; +class AsyncTask; +class ConditionVarFull; + +/** + * This class represents a thread-safe handle to a promised future result of + * an asynchronous operation, providing methods to query its status and result + * as well as register callbacks for this future's completion. + * + * An AsyncFuture can be awaited from within a coroutine or task. It keeps + * track of a list of tasks waiting for this future so that they are + * automatically reactivated upon this future's completion. + * + * A task itself is also a subclass of AsyncFuture. Other subclasses are + * possible, although subclassing is not necessary for most purposes. + * + * The `done()` method is used to check whether the future has completed and + * a result is available (whether it is cancelled or finished). + * + * To get the result of the future in C++, use `wait()` and `get_result()`. + * In Python, the functionality of both of those calls is wrapped into the + * `result()` method, which waits for the future to complete before either + * returning the result or throwing an exception if the future was cancelled. + * However, it is preferable to use the `await` keyword when running from a + * coroutine. + * + * This API aims to mirror and be compatible with Python's Future class. + */ +class EXPCL_PANDA_EVENT AsyncFuture : public TypedReferenceCount { +PUBLISHED: + INLINE AsyncFuture(); + virtual ~AsyncFuture(); + + EXTENSION(static PyObject *__await__(PyObject *self)); + EXTENSION(static PyObject *__iter__(PyObject *self)); + + INLINE bool done() const; + INLINE bool cancelled() const; + EXTENSION(PyObject *result(double timeout = -1.0) const); + + virtual bool cancel(); + + INLINE void set_done_event(const string &done_event); + INLINE const string &get_done_event() const; + MAKE_PROPERTY(done_event, get_done_event, set_done_event); + + virtual void output(ostream &out) const; + + void wait(); + void wait(double timeout); + + INLINE void set_result(nullptr_t); + INLINE void set_result(TypedObject *result); + INLINE void set_result(TypedReferenceCount *result); + INLINE void set_result(TypedWritableReferenceCount *result); + +public: + INLINE TypedObject *get_result() const; + INLINE void get_result(TypedObject *&ptr, ReferenceCount *&ref_ptr) const; + + void notify_done(bool clean_exit); + +private: + void set_result(TypedObject *ptr, ReferenceCount *ref_ptr); + void add_waiting_task(AsyncTask *task); + +protected: + enum FutureState { + FS_pending, + FS_finished, + FS_cancelled, + }; + + AsyncTaskManager *_manager; + ConditionVarFull *_cvar; + TypedObject *_result; + PT(ReferenceCount) _result_ref; + AtomicAdjust::Integer _future_state; + + string _done_event; + + // Tasks waiting for this one to complete. These are reference counted, but + // we can't store them in a PointerTo for circular dependency reasons. + pvector _waiting_tasks; + + friend class PythonTask; + +public: + static TypeHandle get_class_type() { + return _type_handle; + } + static void init_type() { + TypedReferenceCount::init_type(); + register_type(_type_handle, "AsyncFuture", + TypedReferenceCount::get_class_type()); + } + virtual TypeHandle get_type() const { + return get_class_type(); + } + virtual TypeHandle force_init_type() {init_type(); return get_class_type();} + +private: + static TypeHandle _type_handle; +}; + +INLINE ostream &operator << (ostream &out, const AsyncFuture &fut) { + fut.output(out); + return out; +}; + +#include "asyncFuture.I" + +#endif diff --git a/panda/src/event/asyncFuture_ext.cxx b/panda/src/event/asyncFuture_ext.cxx new file mode 100644 index 0000000000..49b1ab4d66 --- /dev/null +++ b/panda/src/event/asyncFuture_ext.cxx @@ -0,0 +1,175 @@ +/** + * 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 asyncFuture_ext.h + * @author rdb + * @date 2017-10-29 + */ + +#include "asyncFuture_ext.h" +#include "pythonTask.h" + +#ifdef HAVE_PYTHON + +#ifndef CPPPARSER +extern struct Dtool_PyTypedObject Dtool_AsyncFuture; +extern struct Dtool_PyTypedObject Dtool_TypedObject; +#endif + +/** + * Get the result of a future, which may be a PythonTask. Assumes that the + * future is already done. + */ +static PyObject *get_done_result(const AsyncFuture *future) { + if (!future->cancelled()) { + if (future->is_of_type(PythonTask::get_class_type())) { + // If it's a PythonTask, defer to its get_result(), since it may store + // any PyObject value or raise an exception. + const PythonTask *task = (const PythonTask *)future; + return task->get_result(); + } else { + ReferenceCount *ref_ptr; + TypedObject *ptr; + future->get_result(ptr, ref_ptr); + + if (ptr == nullptr) { + Py_INCREF(Py_None); + return Py_None; + } + + if (ref_ptr != nullptr) { + ref_ptr->ref(); + } + + return DTool_CreatePyInstanceTyped + ((void *)ptr, Dtool_TypedObject, (ref_ptr != nullptr), false, + ptr->get_type_index()); + } + } else { + // If the future was cancelled, we should raise an exception. + static PyObject *exc_type = nullptr; + if (exc_type == nullptr) { + // Get the CancelledError that asyncio uses, too. + PyObject *module = PyImport_ImportModule("concurrent.futures._base"); + if (module != nullptr) { + exc_type = PyObject_GetAttrString(module, "CancelledError"); + Py_DECREF(module); + } + // If we can't get that, we should pretend and make our own. + if (exc_type == nullptr) { + exc_type = PyErr_NewExceptionWithDoc("concurrent.futures._base.CancelledError", + "The Future was cancelled.", + nullptr, nullptr); + } + } + Py_INCREF(exc_type); + PyErr_Restore(exc_type, nullptr, nullptr); + return nullptr; + } +} + +/** + * Yields continuously until the task has finished. + */ +static PyObject *gen_next(PyObject *self) { + const AsyncFuture *future = nullptr; + if (!Dtool_Call_ExtractThisPointer(self, Dtool_AsyncFuture, (void **)&future)) { + return nullptr; + } + + if (!future->done()) { + // Continue awaiting the result. + Py_INCREF(self); + return self; + } else { + PyObject *result = get_done_result(future); + if (result != nullptr) { + 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; +} + +/** + * Returns the result of this future, unless it was cancelled, in which case + * it returns CancelledError. + * If the future is not yet done, waits until the result is available. If a + * timeout is passed and the future is not done within the given timeout, + * raises TimeoutError. + */ +PyObject *Extension:: +result(double timeout) const { + if (!_this->done()) { + // Not yet done? Wait until it is done, or until a timeout occurs. But + // first check to make sure we're not trying to deadlock the thread. + Thread *current_thread = Thread::get_current_thread(); + if (_this == (const AsyncFuture *)current_thread->get_current_task()) { + PyErr_SetString(PyExc_RuntimeError, "cannot call task.result() from within the task"); + return nullptr; + } + + // Release the GIL for the duration. +#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS) + PyThreadState *_save; + Py_UNBLOCK_THREADS +#endif + //TODO: check why gcc and clang don't like infinity timeout. + if (cinf(timeout) || timeout < 0.0) { + _this->wait(); + } else { + _this->wait(timeout); + } +#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS) + Py_BLOCK_THREADS +#endif + + if (!_this->done()) { + // It timed out. Raise an exception. + static PyObject *exc_type = nullptr; + if (exc_type == nullptr) { + // Get the TimeoutError that asyncio uses, too. + PyObject *module = PyImport_ImportModule("concurrent.futures._base"); + if (module != nullptr) { + exc_type = PyObject_GetAttrString(module, "TimeoutError"); + Py_DECREF(module); + } + // If we can't get that, we should pretend and make our own. + if (exc_type == nullptr) { + exc_type = PyErr_NewExceptionWithDoc("concurrent.futures._base.TimeoutError", + "The operation exceeded the given deadline.", + nullptr, nullptr); + } + } + Py_INCREF(exc_type); + PyErr_Restore(exc_type, nullptr, nullptr); + return nullptr; + } + } + + return get_done_result(_this); +} + +#endif diff --git a/panda/src/event/asyncTask_ext.h b/panda/src/event/asyncFuture_ext.h old mode 100755 new mode 100644 similarity index 69% rename from panda/src/event/asyncTask_ext.h rename to panda/src/event/asyncFuture_ext.h index 89fd4cb8f4..498225478d --- a/panda/src/event/asyncTask_ext.h +++ b/panda/src/event/asyncFuture_ext.h @@ -6,13 +6,13 @@ * license. You should have received a copy of this license along * with this source code in a file named "LICENSE." * - * @file asyncTask_ext.h + * @file asyncFuture_ext.h * @author rdb * @date 2017-10-29 */ -#ifndef ASYNCTASK_EXT_H -#define ASYNCTASK_EXT_H +#ifndef ASYNCFUTURE_EXT_H +#define ASYNCFUTURE_EXT_H #include "extension.h" #include "py_panda.h" @@ -21,15 +21,17 @@ #ifdef HAVE_PYTHON /** - * Extension class for AsyncTask + * Extension class for AsyncFuture */ template<> -class Extension : public ExtensionBase { +class Extension : public ExtensionBase { public: static PyObject *__await__(PyObject *self); static PyObject *__iter__(PyObject *self) { return __await__(self); } + + PyObject *result(double timeout = -1.0) const; }; #endif // HAVE_PYTHON -#endif // ASYNCTASK_EXT_H +#endif // ASYNCFUTURE_EXT_H diff --git a/panda/src/event/asyncTask.I b/panda/src/event/asyncTask.I index 3b4476afb7..869b0b33f7 100644 --- a/panda/src/event/asyncTask.I +++ b/panda/src/event/asyncTask.I @@ -181,15 +181,6 @@ set_done_event(const string &done_event) { _done_event = done_event; } -/** - * Returns the event name that will be triggered when the task finishes. See - * set_done_event(). - */ -INLINE const string &AsyncTask:: -get_done_event() const { - return _done_event; -} - /** * Returns the amount of time elapsed during the task's previous run cycle, in * seconds. diff --git a/panda/src/event/asyncTask.cxx b/panda/src/event/asyncTask.cxx index fb251aebd6..17f0a0630d 100644 --- a/panda/src/event/asyncTask.cxx +++ b/panda/src/event/asyncTask.cxx @@ -35,7 +35,6 @@ AsyncTask(const string &name) : _priority(0), _state(S_inactive), _servicing_thread(NULL), - _manager(NULL), _chain(NULL), _start_time(0.0), _start_frame(0), @@ -68,11 +67,27 @@ AsyncTask:: * S_inactive (or possible S_servicing_removed). This is a no-op if the state * is already S_inactive. */ -void AsyncTask:: +bool AsyncTask:: remove() { - if (_manager != (AsyncTaskManager *)NULL) { - _manager->remove(this); + AsyncTaskManager *manager = _manager; + if (manager != nullptr) { + nassertr(_chain->_manager == manager, false); + if (task_cat.is_debug()) { + task_cat.debug() + << "Removing " << *this << "\n"; + } + MutexHolder holder(manager->_lock); + if (_chain->do_remove(this, true)) { + return true; + } else { + if (task_cat.is_debug()) { + task_cat.debug() + << " (unable to remove " << *this << ")\n"; + } + return false; + } } + return false; } /** @@ -379,11 +394,26 @@ jump_to_task_chain(AsyncTaskManager *manager) { */ AsyncTask::DoneStatus AsyncTask:: unlock_and_do_task() { - nassertr(_manager != (AsyncTaskManager *)NULL, DS_done); + nassertr(_manager != nullptr, DS_done); PT(ClockObject) clock = _manager->get_clock(); + // Indicate that this task is now the current task running on the thread. Thread *current_thread = Thread::get_current_thread(); - record_task(current_thread); + nassertr(current_thread->_current_task == nullptr, DS_interrupt); + + void *ptr = AtomicAdjust::compare_and_exchange_ptr + ((void * TVOLATILE &)current_thread->_current_task, + (void *)nullptr, (void *)this); + + // If the return value is other than nullptr, someone else must have + // assigned the task first, in another thread. That shouldn't be possible. + + // But different versions of gcc appear to have problems compiling these + // assertions correctly. +#ifndef __GNUC__ + nassertr(ptr == nullptr, DS_interrupt); + nassertr(current_thread->_current_task == this, DS_interrupt); +#endif // __GNUC__ // It's important to release the lock while the task is being serviced. _manager->_lock.release(); @@ -403,11 +433,36 @@ unlock_and_do_task() { _chain->_time_in_frame += _dt; - clear_task(current_thread); + // Now indicate that this is no longer the current task. + nassertr(current_thread->_current_task == this, status); + + ptr = AtomicAdjust::compare_and_exchange_ptr + ((void * TVOLATILE &)current_thread->_current_task, + (void *)this, (void *)nullptr); + + // If the return value is other than this, someone else must have assigned + // the task first, in another thread. That shouldn't be possible. + + // But different versions of gcc appear to have problems compiling these + // assertions correctly. +#ifndef __GNUC__ + nassertr(ptr == this, DS_interrupt); + nassertr(current_thread->_current_task == nullptr, DS_interrupt); +#endif // __GNUC__ return status; } +/** + * Cancels this task. This is equivalent to remove(). + */ +bool AsyncTask:: +cancel() { + bool result = remove(); + nassertr(done(), false); + return result; +} + /** * Override this function to return true if the task can be successfully * executed, false if it cannot. Mainly intended as a sanity check when @@ -477,22 +532,16 @@ upon_birth(AsyncTaskManager *manager) { * task has been removed because it exited normally (returning DS_done), or * false if it was removed for some other reason (e.g. * AsyncTaskManager::remove()). By the time this method is called, _manager - * has been cleared, so the parameter manager indicates the original + * may have been cleared, so the parameter manager indicates the original * AsyncTaskManager that owned this task. * - * The normal behavior is to throw the done_event only if clean_exit is true. - * * This function is called with the lock *not* held. */ void AsyncTask:: upon_death(AsyncTaskManager *manager, bool clean_exit) { - if (clean_exit && !_done_event.empty()) { - PT_Event event = new Event(_done_event); - event->add_parameter(EventParameter(this)); - throw_event(event); - } + //NB. done_event is now being thrown in AsyncFuture::notify_done(). - // Also throw a generic remove event for the manager. + // Throw a generic remove event for the manager. if (manager != (AsyncTaskManager *)NULL) { string remove_name = manager->get_name() + "-removeTask"; PT_Event event = new Event(remove_name); diff --git a/panda/src/event/asyncTask.h b/panda/src/event/asyncTask.h index b50a4a6e54..a941cd4b36 100644 --- a/panda/src/event/asyncTask.h +++ b/panda/src/event/asyncTask.h @@ -15,8 +15,8 @@ #define ASYNCTASK_H #include "pandabase.h" - -#include "asyncTaskBase.h" +#include "asyncFuture.h" +#include "namable.h" #include "pmutex.h" #include "conditionVar.h" #include "pStatCollector.h" @@ -29,7 +29,7 @@ class AsyncTaskChain; * Normally, you would subclass from this class, and override do_task(), to * define the functionality you wish to have the task perform. */ -class EXPCL_PANDA_EVENT AsyncTask : public AsyncTaskBase { +class EXPCL_PANDA_EVENT AsyncTask : public AsyncFuture, public Namable { public: AsyncTask(const string &name = string()); ALLOC_DELETED_CHAIN(AsyncTask); @@ -62,7 +62,7 @@ PUBLISHED: INLINE bool is_alive() const; INLINE AsyncTaskManager *get_manager() const; - void remove(); + bool remove(); INLINE void set_delay(double delay); INLINE void clear_delay(); @@ -92,7 +92,6 @@ PUBLISHED: INLINE int get_priority() const; INLINE void set_done_event(const string &done_event); - INLINE const string &get_done_event() const; INLINE double get_dt() const; INLINE double get_max_dt() const; @@ -100,13 +99,12 @@ 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(); + virtual bool cancel() FINAL; + virtual bool is_runnable(); virtual DoneStatus do_task(); virtual void upon_birth(AsyncTaskManager *manager); @@ -120,11 +118,9 @@ protected: double _wake_time; int _sort; int _priority; - string _done_event; State _state; Thread *_servicing_thread; - AsyncTaskManager *_manager; AsyncTaskChain *_chain; double _start_time; @@ -135,9 +131,6 @@ 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; @@ -150,9 +143,9 @@ public: return _type_handle; } static void init_type() { - AsyncTaskBase::init_type(); + AsyncFuture::init_type(); register_type(_type_handle, "AsyncTask", - AsyncTaskBase::get_class_type()); + AsyncFuture::get_class_type()); } virtual TypeHandle get_type() const { return get_class_type(); @@ -162,6 +155,7 @@ public: private: static TypeHandle _type_handle; + friend class AsyncFuture; friend class AsyncTaskManager; friend class AsyncTaskChain; friend class AsyncTaskSequence; diff --git a/panda/src/event/asyncTaskChain.cxx b/panda/src/event/asyncTaskChain.cxx index 46975efb7c..bda6113bd1 100644 --- a/panda/src/event/asyncTaskChain.cxx +++ b/panda/src/event/asyncTaskChain.cxx @@ -456,24 +456,21 @@ do_add(AsyncTask *task) { /** * Removes the indicated task from this chain. Returns true if removed, false * otherwise. Assumes the lock is already held. The task->upon_death() - * method is *not* called. + * method is called with clean_exit=false if upon_death is given. */ bool AsyncTaskChain:: -do_remove(AsyncTask *task) { - bool removed = false; - +do_remove(AsyncTask *task, bool upon_death) { nassertr(task->_chain == this, false); switch (task->_state) { case AsyncTask::S_servicing: - // This task is being serviced. + // This task is being serviced. upon_death will be called afterwards. task->_state = AsyncTask::S_servicing_removed; - removed = true; - break; + return true; case AsyncTask::S_servicing_removed: - // Being serviced, though it will be removed later. - break; + // Being serviced, though it is already marked to be removed afterwards. + return false; case AsyncTask::S_sleeping: // Sleeping, easy. @@ -482,10 +479,9 @@ do_remove(AsyncTask *task) { nassertr(index != -1, false); _sleeping.erase(_sleeping.begin() + index); make_heap(_sleeping.begin(), _sleeping.end(), AsyncTaskSortWakeTime()); - removed = true; - cleanup_task(task, false, false); + cleanup_task(task, upon_death, false); } - break; + return true; case AsyncTask::S_active: { @@ -503,15 +499,15 @@ do_remove(AsyncTask *task) { nassertr(index != -1, false); } } - removed = true; - cleanup_task(task, false, false); + cleanup_task(task, upon_death, false); + return true; } default: break; } - return removed; + return false; } /** @@ -776,27 +772,18 @@ cleanup_task(AsyncTask *task, bool upon_death, bool clean_exit) { PT(AsyncTask) hold_task = task; task->_state = AsyncTask::S_inactive; - task->_chain = NULL; - task->_manager = NULL; + task->_chain = nullptr; --_num_tasks; --(_manager->_num_tasks); _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 && !task->done()) { + task->notify_done(clean_exit); } + task->_manager = nullptr; + if (upon_death) { _manager->_lock.release(); task->upon_death(_manager, clean_exit); diff --git a/panda/src/event/asyncTaskChain.h b/panda/src/event/asyncTaskChain.h index 3dded59317..512a7b9fa0 100644 --- a/panda/src/event/asyncTaskChain.h +++ b/panda/src/event/asyncTaskChain.h @@ -96,7 +96,7 @@ protected: typedef pvector< PT(AsyncTask) > TaskHeap; void do_add(AsyncTask *task); - bool do_remove(AsyncTask *task); + bool do_remove(AsyncTask *task, bool upon_death=false); void do_wait_for_tasks(); void do_cleanup(); @@ -206,6 +206,7 @@ public: private: static TypeHandle _type_handle; + friend class AsyncFuture; friend class AsyncTaskChainThread; friend class AsyncTask; friend class AsyncTaskManager; diff --git a/panda/src/event/asyncTaskCollection.cxx b/panda/src/event/asyncTaskCollection.cxx index 38a07c6875..80999362e1 100644 --- a/panda/src/event/asyncTaskCollection.cxx +++ b/panda/src/event/asyncTaskCollection.cxx @@ -63,14 +63,15 @@ add_task(AsyncTask *task) { */ bool AsyncTaskCollection:: remove_task(AsyncTask *task) { - int task_index = -1; - for (int i = 0; task_index == -1 && i < (int)_tasks.size(); i++) { + size_t task_index = (size_t)-1; + for (size_t i = 0; i < _tasks.size(); ++i) { if (_tasks[i] == task) { task_index = i; + break; } } - if (task_index == -1) { + if (task_index == (size_t)-1) { // The indicated task was not a member of the collection. return false; } @@ -129,12 +130,12 @@ void AsyncTaskCollection:: remove_duplicate_tasks() { AsyncTasks new_tasks; - int num_tasks = get_num_tasks(); - for (int i = 0; i < num_tasks; i++) { + size_t num_tasks = get_num_tasks(); + for (size_t i = 0; i < num_tasks; i++) { PT(AsyncTask) task = get_task(i); bool duplicated = false; - for (int j = 0; j < i && !duplicated; j++) { + for (size_t j = 0; j < i && !duplicated; j++) { duplicated = (task == get_task(j)); } @@ -152,7 +153,7 @@ remove_duplicate_tasks() { */ bool AsyncTaskCollection:: has_task(AsyncTask *task) const { - for (int i = 0; i < get_num_tasks(); i++) { + for (size_t i = 0; i < get_num_tasks(); i++) { if (task == get_task(i)) { return true; } @@ -174,8 +175,8 @@ clear() { */ AsyncTask *AsyncTaskCollection:: find_task(const string &name) const { - int num_tasks = get_num_tasks(); - for (int i = 0; i < num_tasks; i++) { + size_t num_tasks = get_num_tasks(); + for (size_t i = 0; i < num_tasks; ++i) { AsyncTask *task = get_task(i); if (task->get_name() == name) { return task; @@ -187,7 +188,7 @@ find_task(const string &name) const { /** * Returns the number of AsyncTasks in the collection. */ -int AsyncTaskCollection:: +size_t AsyncTaskCollection:: get_num_tasks() const { return _tasks.size(); } @@ -196,8 +197,8 @@ get_num_tasks() const { * Returns the nth AsyncTask in the collection. */ AsyncTask *AsyncTaskCollection:: -get_task(int index) const { - nassertr(index >= 0 && index < (int)_tasks.size(), NULL); +get_task(size_t index) const { + nassertr(index < _tasks.size(), nullptr); return _tasks[index]; } @@ -206,7 +207,7 @@ get_task(int index) const { * Removes the nth AsyncTask from the collection. */ void AsyncTaskCollection:: -remove_task(int index) { +remove_task(size_t index) { // If the pointer to our internal array is shared by any other // AsyncTaskCollections, we have to copy the array now so we won't // inadvertently modify any of our brethren AsyncTaskCollection objects. @@ -217,7 +218,7 @@ remove_task(int index) { _tasks.v() = old_tasks.v(); } - nassertv(index >= 0 && index < (int)_tasks.size()); + nassertv(index < _tasks.size()); _tasks.erase(_tasks.begin() + index); } @@ -226,9 +227,8 @@ remove_task(int index) { * get_task(), but it may be a more convenient way to access it. */ AsyncTask *AsyncTaskCollection:: -operator [] (int index) const { - nassertr(index >= 0 && index < (int)_tasks.size(), NULL); - +operator [] (size_t index) const { + nassertr(index < _tasks.size(), nullptr); return _tasks[index]; } @@ -236,7 +236,7 @@ operator [] (int index) const { * Returns the number of tasks in the collection. This is the same thing as * get_num_tasks(). */ -int AsyncTaskCollection:: +size_t AsyncTaskCollection:: size() const { return _tasks.size(); } @@ -260,7 +260,7 @@ output(ostream &out) const { */ void AsyncTaskCollection:: write(ostream &out, int indent_level) const { - for (int i = 0; i < get_num_tasks(); i++) { + for (size_t i = 0; i < get_num_tasks(); i++) { indent(out, indent_level) << *get_task(i) << "\n"; } } diff --git a/panda/src/event/asyncTaskCollection.h b/panda/src/event/asyncTaskCollection.h index 824ec433c2..4ed6f6a59b 100644 --- a/panda/src/event/asyncTaskCollection.h +++ b/panda/src/event/asyncTaskCollection.h @@ -41,12 +41,12 @@ PUBLISHED: AsyncTask *find_task(const string &name) const; - int get_num_tasks() const; - AsyncTask *get_task(int index) const; + size_t get_num_tasks() const; + AsyncTask *get_task(size_t index) const; MAKE_SEQ(get_tasks, get_num_tasks, get_task); - void remove_task(int index); - AsyncTask *operator [] (int index) const; - int size() const; + void remove_task(size_t index); + AsyncTask *operator [] (size_t index) const; + size_t size() const; INLINE void operator += (const AsyncTaskCollection &other); INLINE AsyncTaskCollection operator + (const AsyncTaskCollection &other) const; diff --git a/panda/src/event/asyncTaskManager.I b/panda/src/event/asyncTaskManager.I index 20294f63dc..c1e5775a55 100644 --- a/panda/src/event/asyncTaskManager.I +++ b/panda/src/event/asyncTaskManager.I @@ -36,7 +36,7 @@ get_clock() { * Returns the number of tasks that are currently active or sleeping within * the task manager. */ -INLINE int AsyncTaskManager:: +INLINE size_t AsyncTaskManager:: get_num_tasks() const { MutexHolder holder(_lock); return _num_tasks; diff --git a/panda/src/event/asyncTaskManager.cxx b/panda/src/event/asyncTaskManager.cxx index b95ddbf109..5e1bf9601c 100644 --- a/panda/src/event/asyncTaskManager.cxx +++ b/panda/src/event/asyncTaskManager.cxx @@ -307,25 +307,20 @@ find_tasks_matching(const GlobPattern &pattern) const { */ bool AsyncTaskManager:: remove(AsyncTask *task) { - // We pass this up to the multi-task remove() flavor. Do we care about the - // tiny cost of creating an AsyncTaskCollection here? Probably not. - AsyncTaskCollection tasks; - tasks.add_task(task); - return remove(tasks) != 0; + return task->remove(); } /** * Removes all of the tasks in the AsyncTaskCollection. Returns the number of * tasks removed. */ -int AsyncTaskManager:: +size_t AsyncTaskManager:: remove(const AsyncTaskCollection &tasks) { MutexHolder holder(_lock); - int num_removed = 0; + size_t num_removed = 0; - int num_tasks = tasks.get_num_tasks(); - int i; - for (i = 0; i < num_tasks; ++i) { + size_t num_tasks = tasks.get_num_tasks(); + for (size_t i = 0; i < num_tasks; ++i) { PT(AsyncTask) task = tasks.get_task(i); if (task->_manager != this) { @@ -337,10 +332,7 @@ remove(const AsyncTaskCollection &tasks) { task_cat.debug() << "Removing " << *task << "\n"; } - if (task->_chain->do_remove(task)) { - _lock.release(); - task->upon_death(this, false); - _lock.acquire(); + if (task->_chain->do_remove(task, true)) { ++num_removed; } else { if (task_cat.is_debug()) { diff --git a/panda/src/event/asyncTaskManager.h b/panda/src/event/asyncTaskManager.h index 8432b217a9..d843508de7 100644 --- a/panda/src/event/asyncTaskManager.h +++ b/panda/src/event/asyncTaskManager.h @@ -71,13 +71,13 @@ PUBLISHED: AsyncTaskCollection find_tasks_matching(const GlobPattern &pattern) const; bool remove(AsyncTask *task); - int remove(const AsyncTaskCollection &tasks); + size_t remove(const AsyncTaskCollection &tasks); BLOCKING void wait_for_tasks(); BLOCKING void stop_threads(); void start_threads(); - INLINE int get_num_tasks() const; + INLINE size_t get_num_tasks() const; AsyncTaskCollection get_tasks() const; AsyncTaskCollection get_active_tasks() const; @@ -126,7 +126,7 @@ protected: typedef ov_set > TaskChains; TaskChains _task_chains; - int _num_tasks; + size_t _num_tasks; TasksByName _tasks_by_name; PT(ClockObject) _clock; @@ -151,6 +151,7 @@ public: private: static TypeHandle _type_handle; + friend class AsyncFuture; friend class AsyncTaskChain; friend class AsyncTaskChain::AsyncTaskChainThread; friend class AsyncTask; diff --git a/panda/src/event/asyncTask_ext.cxx b/panda/src/event/asyncTask_ext.cxx deleted file mode 100755 index bbfb46d2c1..0000000000 --- a/panda/src/event/asyncTask_ext.cxx +++ /dev/null @@ -1,75 +0,0 @@ -/** - * 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/config_event.cxx b/panda/src/event/config_event.cxx index 0f335981aa..86a8df8361 100644 --- a/panda/src/event/config_event.cxx +++ b/panda/src/event/config_event.cxx @@ -12,6 +12,7 @@ */ #include "config_event.h" +#include "asyncFuture.h" #include "asyncTask.h" #include "asyncTaskChain.h" #include "asyncTaskManager.h" @@ -31,6 +32,7 @@ NotifyCategoryDef(event, ""); NotifyCategoryDef(task, ""); ConfigureFn(config_event) { + AsyncFuture::init_type(); AsyncTask::init_type(); AsyncTaskChain::init_type(); AsyncTaskManager::init_type(); diff --git a/panda/src/event/eventHandler.cxx b/panda/src/event/eventHandler.cxx index d67d0661dd..4e15354839 100644 --- a/panda/src/event/eventHandler.cxx +++ b/panda/src/event/eventHandler.cxx @@ -27,6 +27,26 @@ EventHandler:: EventHandler(EventQueue *ev_queue) : _queue(*ev_queue) { } +/** + * Returns a pending future that will be marked as done when the event is next + * fired. + */ +AsyncFuture *EventHandler:: +get_future(const string &event_name) { + Futures::iterator fi; + fi = _futures.find(event_name); + + // If we already have a future, but someone cancelled it, we need to create + // a new future instead. + if (fi != _futures.end() && !fi->second->cancelled()) { + return fi->second; + } else { + AsyncFuture *fut = new AsyncFuture; + _futures[event_name] = fut; + return fut; + } +} + /** * The main processing loop of the EventHandler. This function must be called * periodically to service events. Walks through each pending event and calls @@ -81,6 +101,18 @@ dispatch_event(const Event *event) { ((*cfi).first)(event, (*cfi).second); } } + + // Finally, check for futures that need to be triggered. + Futures::const_iterator fi; + fi = _futures.find(event->get_name()); + + if (fi != _futures.end()) { + AsyncFuture *fut = (*fi).second; + if (!fut->done()) { + fut->set_result((TypedReferenceCount *)event); + } + _futures.erase(fi); + } } diff --git a/panda/src/event/eventHandler.h b/panda/src/event/eventHandler.h index 05952a5082..39d84475a4 100644 --- a/panda/src/event/eventHandler.h +++ b/panda/src/event/eventHandler.h @@ -18,6 +18,7 @@ #include "event.h" #include "pt_Event.h" +#include "asyncFuture.h" #include "pset.h" #include "pmap.h" @@ -41,10 +42,13 @@ public: PUBLISHED: explicit EventHandler(EventQueue *ev_queue); + ~EventHandler() {} + + AsyncFuture *get_future(const string &event_name); void process_events(); - virtual void dispatch_event(const Event *); + virtual void dispatch_event(const Event *event); void write(ostream &out) const; @@ -74,9 +78,11 @@ protected: typedef pair CallbackFunction; typedef pset CallbackFunctions; typedef pmap CallbackHooks; + typedef pmap Futures; Hooks _hooks; CallbackHooks _cbhooks; + Futures _futures; EventQueue &_queue; static EventHandler *_global_event_handler; diff --git a/panda/src/event/eventParameter.I b/panda/src/event/eventParameter.I index a6f512d72f..14727106e6 100644 --- a/panda/src/event/eventParameter.I +++ b/panda/src/event/eventParameter.I @@ -11,13 +11,6 @@ * @date 1999-02-08 */ -/** - * Defines an EventParameter that stores nothing: the "empty" parameter. - */ -INLINE EventParameter:: -EventParameter() { -} - /** * Defines an EventParameter that stores a pointer to any kind of * TypedWritableReferenceCount object. This is the most general constructor. diff --git a/panda/src/event/eventParameter.h b/panda/src/event/eventParameter.h index ea960c156e..b0cbcc1376 100644 --- a/panda/src/event/eventParameter.h +++ b/panda/src/event/eventParameter.h @@ -34,7 +34,8 @@ */ class EXPCL_PANDA_EVENT EventParameter { PUBLISHED: - INLINE EventParameter(); + INLINE EventParameter() DEFAULT_CTOR; + INLINE EventParameter(nullptr_t) {}; INLINE EventParameter(const TypedWritableReferenceCount *ptr); INLINE EventParameter(const TypedReferenceCount *ptr); INLINE EventParameter(int value); diff --git a/panda/src/event/p3event_composite1.cxx b/panda/src/event/p3event_composite1.cxx index 57cc83159c..dc72d2b03f 100644 --- a/panda/src/event/p3event_composite1.cxx +++ b/panda/src/event/p3event_composite1.cxx @@ -1,3 +1,4 @@ +#include "asyncFuture.cxx" #include "asyncTask.cxx" #include "asyncTaskChain.cxx" #include "asyncTaskCollection.cxx" diff --git a/panda/src/event/pythonTask.I b/panda/src/event/pythonTask.I index 7280b4e7d6..78f8e30cc7 100644 --- a/panda/src/event/pythonTask.I +++ b/panda/src/event/pythonTask.I @@ -45,17 +45,12 @@ get_owner() const { */ INLINE void PythonTask:: set_result(PyObject *result) { + // Note that we don't call notify_done() here since the done status will be + // automatically notified upon the task's completion. nassertv(is_alive()); + nassertv(!done()); 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 1b4adc0921..31647540f2 100644 --- a/panda/src/event/pythonTask.cxx +++ b/panda/src/event/pythonTask.cxx @@ -25,7 +25,7 @@ TypeHandle PythonTask::_type_handle; #ifndef CPPPARSER extern struct Dtool_PyTypedObject Dtool_TypedReferenceCount; -extern struct Dtool_PyTypedObject Dtool_AsyncTask; +extern struct Dtool_PyTypedObject Dtool_AsyncFuture; extern struct Dtool_PyTypedObject Dtool_PythonTask; #endif @@ -238,8 +238,8 @@ set_owner(PyObject *owner) { * exception occurred within this task, it is raised instead. */ PyObject *PythonTask:: -result() const { - nassertr(!is_alive(), nullptr); +get_result() const { + nassertr(done(), nullptr); if (_exception == nullptr) { // The result of the call is stored in _exc_value. @@ -273,49 +273,6 @@ exception() const { } }*/ -/** - * 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; - } -} - /** * Maps from an expression like "task.attr_name = v". This is customized here * so we can support some traditional task interfaces that supported directly @@ -620,36 +577,39 @@ do_python_task() { } } - // 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; + // Tell the task chain we want to kill ourselves. We indicate this is + // a "clean exit" because we still want to run the done callbacks on + // exception. + return DS_done; } } else if (DtoolCanThisBeAPandaInstance(result)) { - // We are waiting for a task to finish. - void *ptr = ((Dtool_PyInstDef *)result)->_My_Type->_Dtool_UpcastInterface(result, &Dtool_AsyncTask); + // We are waiting for an AsyncFuture (eg. other task) to finish. + void *ptr = ((Dtool_PyInstDef *)result)->_My_Type->_Dtool_UpcastInterface(result, &Dtool_AsyncFuture); 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); + AsyncFuture *fut = (AsyncFuture *)ptr; + AsyncTaskManager *manager = fut->_manager; + if (manager == nullptr) { + manager = _manager; + fut->_manager = manager; + } nassertr(manager == _manager, DS_interrupt); - manager->_lock.acquire(); - if (task != (AsyncTask *)this) { - if (task->is_alive()) { + MutexHolder holder(manager->_lock); + if (fut != (AsyncFuture *)this) { + if (!fut->done()) { if (task_cat.is_debug()) { task_cat.debug() - << *this << " is now awaiting <" << *task << ">.\n"; + << *this << " is now awaiting <" << *fut << ">.\n"; } - task->_waiting_tasks.push_back(this); + fut->add_waiting_task(this); } else { // The task is already done. Continue at next opportunity. + if (task_cat.is_debug()) { + task_cat.debug() + << *this << " would await <" << *fut << ">, were it not already done.\n"; + } Py_DECREF(result); - manager->_lock.release(); return DS_cont; } } else { @@ -658,14 +618,12 @@ do_python_task() { 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. + // We are waiting for a non-Panda future to finish. We currently + // implement this by 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); @@ -680,7 +638,7 @@ do_python_task() { if (task_cat.is_debug()) { PyObject *str = PyObject_ASCII(result); task_cat.debug() - << *this << " is now awaiting " << PyUnicode_AsUTF8(str) << ".\n"; + << *this << " is now polling " << PyUnicode_AsUTF8(str) << ".done()\n"; Py_DECREF(str); } #endif @@ -746,6 +704,24 @@ do_python_task() { } } + // This is unfortunate, but some are returning task.done, which nowadays + // conflicts with the AsyncFuture method. Check if that is being returned. + PyMethodDef *meth = nullptr; + if (PyCFunction_Check(result)) { + meth = ((PyCFunctionObject *)result)->m_ml; +#if PY_MAJOR_VERSION >= 3 + } else if (Py_TYPE(result) == &PyMethodDescr_Type) { +#else + } else if (strcmp(Py_TYPE(result)->tp_name, "method_descriptor") == 0) { +#endif + meth = ((PyMethodDescrObject *)result)->d_method; + } + + if (meth != nullptr && strcmp(meth->ml_name, "done") == 0) { + Py_DECREF(result); + return DS_done; + } + ostringstream strm; #if PY_MAJOR_VERSION >= 3 PyObject *str = PyObject_ASCII(result); diff --git a/panda/src/event/pythonTask.h b/panda/src/event/pythonTask.h index 59c3c65c5f..869a0691ab 100644 --- a/panda/src/event/pythonTask.h +++ b/panda/src/event/pythonTask.h @@ -20,6 +20,7 @@ #ifdef HAVE_PYTHON #include "py_panda.h" +#include "extension.h" /** * This class exists to allow association of a Python function or coroutine @@ -44,12 +45,14 @@ PUBLISHED: INLINE PyObject *get_owner() const; INLINE void set_result(PyObject *result); - PyObject *result() const; + +public: + // This is exposed only for the result() function in asyncFuture_ext.cxx + // to use, which is why it is not published. + PyObject *get_result() const; //PyObject *exception() const; - static PyObject *__await__(PyObject *self); - INLINE static PyObject *__iter__(PyObject *self); - +PUBLISHED: int __setattr__(PyObject *self, PyObject *attr, PyObject *v); int __delattr__(PyObject *self, PyObject *attr); PyObject *__getattr__(PyObject *attr) const; @@ -101,8 +104,6 @@ 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); @@ -125,6 +126,8 @@ private: bool _registered_to_owner; mutable bool _retrieved_exception; + friend class Extension; + public: static TypeHandle get_class_type() { return _type_handle; diff --git a/panda/src/gobj/animateVerticesRequest.I b/panda/src/gobj/animateVerticesRequest.I index 01226e4694..eb6ee589d0 100644 --- a/panda/src/gobj/animateVerticesRequest.I +++ b/panda/src/gobj/animateVerticesRequest.I @@ -16,15 +16,16 @@ */ INLINE AnimateVerticesRequest:: AnimateVerticesRequest(GeomVertexData *geom_vertex_data) : - _geom_vertex_data(geom_vertex_data), - _is_ready(false) + _geom_vertex_data(geom_vertex_data) { } /** * Returns true if this request has completed, false if it is still pending. + * Equivalent to `req.done() and not req.cancelled()`. + * @see done() */ INLINE bool AnimateVerticesRequest:: is_ready() const { - return _is_ready; + return (FutureState)AtomicAdjust::get(_future_state) == FS_finished; } diff --git a/panda/src/gobj/animateVerticesRequest.cxx b/panda/src/gobj/animateVerticesRequest.cxx index 7c152406ed..f2ded8cdd1 100644 --- a/panda/src/gobj/animateVerticesRequest.cxx +++ b/panda/src/gobj/animateVerticesRequest.cxx @@ -26,7 +26,6 @@ do_task() { // There is no need to store or return a result. The GeomVertexData caches // the result and it will be used later in the rendering process. _geom_vertex_data->animate_vertices(true, current_thread); - _is_ready = true; // Don't continue the task; we're done. return AsyncTask::DS_done; diff --git a/panda/src/gobj/animateVerticesRequest.h b/panda/src/gobj/animateVerticesRequest.h index 8b20c4fe9d..be20651a13 100644 --- a/panda/src/gobj/animateVerticesRequest.h +++ b/panda/src/gobj/animateVerticesRequest.h @@ -40,11 +40,10 @@ PUBLISHED: INLINE bool is_ready() const; protected: - virtual AsyncTask::DoneStatus do_task(); + virtual AsyncTask::DoneStatus do_task(); private: PT(GeomVertexData) _geom_vertex_data; - bool _is_ready; public: static TypeHandle get_class_type() { diff --git a/panda/src/gobj/config_gobj.cxx b/panda/src/gobj/config_gobj.cxx index 4df77f0527..84876a2b8f 100644 --- a/panda/src/gobj/config_gobj.cxx +++ b/panda/src/gobj/config_gobj.cxx @@ -572,6 +572,7 @@ ConfigureFn(config_gobj) { ParamTextureImage::init_type(); ParamTextureSampler::init_type(); PerspectiveLens::init_type(); + PreparedGraphicsObjects::EnqueuedObject::init_type(); QueryContext::init_type(); SamplerContext::init_type(); SamplerState::init_type(); diff --git a/panda/src/gobj/preparedGraphicsObjects.cxx b/panda/src/gobj/preparedGraphicsObjects.cxx index 5c9ec855e3..233edb46b3 100644 --- a/panda/src/gobj/preparedGraphicsObjects.cxx +++ b/panda/src/gobj/preparedGraphicsObjects.cxx @@ -27,6 +27,8 @@ #include "config_gobj.h" #include "throw_event.h" +TypeHandle PreparedGraphicsObjects::EnqueuedObject::_type_handle; + int PreparedGraphicsObjects::_name_index = 0; /** @@ -191,7 +193,25 @@ void PreparedGraphicsObjects:: enqueue_texture(Texture *tex) { ReMutexHolder holder(_lock); - _enqueued_textures.insert(tex); + _enqueued_textures.insert(EnqueuedTextures::value_type(tex, nullptr)); +} + +/** + * Like enqueue_texture, but returns an AsyncFuture that can be used to query + * the status of the texture's preparation. + */ +PT(PreparedGraphicsObjects::EnqueuedObject) PreparedGraphicsObjects:: +enqueue_texture_future(Texture *tex) { + ReMutexHolder holder(_lock); + + pair result = + _enqueued_textures.insert(EnqueuedTextures::value_type(tex, nullptr)); + if (result.first->second == nullptr) { + result.first->second = new EnqueuedObject(this, tex); + } + PT(EnqueuedObject) fut = result.first->second; + nassertr(!fut->cancelled(), fut) + return fut; } /** @@ -220,6 +240,9 @@ dequeue_texture(Texture *tex) { EnqueuedTextures::iterator qi = _enqueued_textures.find(tex); if (qi != _enqueued_textures.end()) { + if (qi->second != nullptr) { + qi->second->notify_removed(); + } _enqueued_textures.erase(qi); return true; } @@ -291,6 +314,17 @@ release_all_textures() { } _prepared_textures.clear(); + + // Mark any futures as cancelled. + EnqueuedTextures::iterator qti; + for (qti = _enqueued_textures.begin(); + qti != _enqueued_textures.end(); + ++qti) { + if (qti->second != nullptr) { + qti->second->notify_removed(); + } + } + _enqueued_textures.clear(); return num_textures; @@ -665,10 +699,28 @@ prepare_geom_now(Geom *geom, GraphicsStateGuardianBase *gsg) { * when the GSG is next ready to do this (presumably at the next frame). */ void PreparedGraphicsObjects:: -enqueue_shader(Shader *se) { +enqueue_shader(Shader *shader) { ReMutexHolder holder(_lock); - _enqueued_shaders.insert(se); + _enqueued_shaders.insert(EnqueuedShaders::value_type(shader, nullptr)); +} + +/** + * Like enqueue_shader, but returns an AsyncFuture that can be used to query + * the status of the shader's preparation. + */ +PT(PreparedGraphicsObjects::EnqueuedObject) PreparedGraphicsObjects:: +enqueue_shader_future(Shader *shader) { + ReMutexHolder holder(_lock); + + pair result = + _enqueued_shaders.insert(EnqueuedShaders::value_type(shader, nullptr)); + if (result.first->second == nullptr) { + result.first->second = new EnqueuedObject(this, shader); + } + PT(EnqueuedObject) fut = result.first->second; + nassertr(!fut->cancelled(), fut) + return fut; } /** @@ -697,6 +749,9 @@ dequeue_shader(Shader *se) { EnqueuedShaders::iterator qi = _enqueued_shaders.find(se); if (qi != _enqueued_shaders.end()) { + if (qi->second != nullptr) { + qi->second->notify_removed(); + } _enqueued_shaders.erase(qi); return true; } @@ -759,6 +814,17 @@ release_all_shaders() { } _prepared_shaders.clear(); + + // Mark any futures as cancelled. + EnqueuedShaders::iterator qsi; + for (qsi = _enqueued_shaders.begin(); + qsi != _enqueued_shaders.end(); + ++qsi) { + if (qsi->second != nullptr) { + qsi->second->notify_removed(); + } + } + _enqueued_shaders.clear(); return num_shaders; @@ -1358,6 +1424,73 @@ prepare_shader_buffer_now(ShaderBuffer *data, GraphicsStateGuardianBase *gsg) { return bc; } +/** + * Creates a new future for the given object. + */ +PreparedGraphicsObjects::EnqueuedObject:: +EnqueuedObject(PreparedGraphicsObjects *pgo, TypedWritableReferenceCount *object) : + _pgo(pgo), + _object(object) { +} + +/** + * Indicates that the preparation request is done. + */ +void PreparedGraphicsObjects::EnqueuedObject:: +set_result(SavedContext *context) { + nassertv(!done()); + AsyncFuture::set_result(context); + _pgo = nullptr; +} + +/** + * Called by PreparedGraphicsObjects to indicate that the preparation request + * has been cancelled. + */ +void PreparedGraphicsObjects::EnqueuedObject:: +notify_removed() { + _pgo = nullptr; + nassertv_always(AsyncFuture::cancel()); +} + +/** + * Cancels the pending preparation request. Has no effect if the preparation + * is already complete or was already cancelled. + */ +bool PreparedGraphicsObjects::EnqueuedObject:: +cancel() { + PreparedGraphicsObjects *pgo = _pgo; + if (_object == nullptr || pgo == nullptr) { + nassertr(done(), false); + return false; + } + + // We don't upcall here, because the dequeue function will end up calling + // notify_removed(). + _result = nullptr; + _pgo = nullptr; + + if (_object->is_of_type(Texture::get_class_type())) { + return pgo->dequeue_texture((Texture *)_object.p()); + + } else if (_object->is_of_type(Geom::get_class_type())) { + return pgo->dequeue_geom((Geom *)_object.p()); + + } else if (_object->is_of_type(Shader::get_class_type())) { + return pgo->dequeue_shader((Shader *)_object.p()); + + } else if (_object->is_of_type(GeomVertexArrayData::get_class_type())) { + return pgo->dequeue_vertex_buffer((GeomVertexArrayData *)_object.p()); + + } else if (_object->is_of_type(GeomPrimitive::get_class_type())) { + return pgo->dequeue_index_buffer((GeomPrimitive *)_object.p()); + + } else if (_object->is_of_type(ShaderBuffer::get_class_type())) { + return pgo->dequeue_shader_buffer((ShaderBuffer *)_object.p()); + } + return false; +} + /** * This is called by the GraphicsStateGuardian to indicate that it is about to * begin processing of the frame. @@ -1446,11 +1579,15 @@ begin_frame(GraphicsStateGuardianBase *gsg, Thread *current_thread) { for (qti = _enqueued_textures.begin(); qti != _enqueued_textures.end(); ++qti) { - Texture *tex = (*qti); + Texture *tex = qti->first; + TextureContext *first_tc = nullptr; for (int view = 0; view < tex->get_num_views(); ++view) { TextureContext *tc = tex->prepare_now(view, this, gsg); - if (tc != (TextureContext *)NULL) { + if (tc != nullptr) { gsg->update_texture(tc, true); + if (view == 0 && qti->second != nullptr) { + qti->second->set_result(tc); + } } } } @@ -1481,8 +1618,11 @@ begin_frame(GraphicsStateGuardianBase *gsg, Thread *current_thread) { for (qsi = _enqueued_shaders.begin(); qsi != _enqueued_shaders.end(); ++qsi) { - Shader *shader = (*qsi); - shader->prepare_now(this, gsg); + Shader *shader = qsi->first; + ShaderContext *sc = shader->prepare_now(this, gsg); + if (qti->second != nullptr) { + qti->second->set_result(sc); + } } _enqueued_shaders.clear(); diff --git a/panda/src/gobj/preparedGraphicsObjects.h b/panda/src/gobj/preparedGraphicsObjects.h index 1dbde2d305..3eaed37167 100644 --- a/panda/src/gobj/preparedGraphicsObjects.h +++ b/panda/src/gobj/preparedGraphicsObjects.h @@ -29,6 +29,7 @@ #include "reMutex.h" #include "bufferResidencyTracker.h" #include "adaptiveLru.h" +#include "asyncFuture.h" class TextureContext; class SamplerContext; @@ -38,6 +39,7 @@ class VertexBufferContext; class IndexBufferContext; class BufferContext; class GraphicsStateGuardianBase; +class SavedContext; /** * A table of objects that are saved within the graphics context for reference @@ -158,6 +160,56 @@ PUBLISHED: GraphicsStateGuardianBase *gsg); public: + /** + * This is a handle to an enqueued object, from which the result can be + * obtained upon completion. + */ + class EXPCL_PANDA_GOBJ EnqueuedObject FINAL : public AsyncFuture { + public: + EnqueuedObject(PreparedGraphicsObjects *pgo, TypedWritableReferenceCount *object); + + TypedWritableReferenceCount *get_object() { return _object.p(); } + SavedContext *get_result() { return (SavedContext *)AsyncFuture::get_result(); } + void set_result(SavedContext *result); + + void notify_removed(); + virtual bool cancel() FINAL; + + PUBLISHED: + MAKE_PROPERTY(object, get_object); + + private: + PreparedGraphicsObjects *_pgo; + PT(TypedWritableReferenceCount) const _object; + + public: + static TypeHandle get_class_type() { + return _type_handle; + } + static void init_type() { + AsyncFuture::init_type(); + register_type(_type_handle, "EnqueuedObject", + AsyncFuture::get_class_type()); + } + virtual TypeHandle get_type() const { + return get_class_type(); + } + virtual TypeHandle force_init_type() {init_type(); return get_class_type();} + + private: + static TypeHandle _type_handle; + }; + + // These are variations of enqueue_xxx that also return a future. They are + // used to implement texture->prepare(), etc. They are only marked public + // so we don't have to define a whole bunch of friend classes. + PT(EnqueuedObject) enqueue_texture_future(Texture *tex); + //PT(EnqueuedObject) enqueue_geom_future(Geom *geom); + PT(EnqueuedObject) enqueue_shader_future(Shader *shader); + //PT(EnqueuedObject) enqueue_vertex_buffer_future(GeomVertexArrayData *data); + //PT(EnqueuedObject) enqueue_index_buffer_future(GeomPrimitive *data); + //PT(EnqueuedObject) enqueue_shader_buffer_future(ShaderBuffer *data); + void begin_frame(GraphicsStateGuardianBase *gsg, Thread *current_thread); void end_frame(Thread *current_thread); @@ -167,11 +219,11 @@ private: private: typedef phash_set Textures; - typedef phash_set< PT(Texture) > EnqueuedTextures; + typedef phash_map< PT(Texture), PT(EnqueuedObject) > EnqueuedTextures; typedef phash_set Geoms; typedef phash_set< PT(Geom) > EnqueuedGeoms; typedef phash_set Shaders; - typedef phash_set< PT(Shader) > EnqueuedShaders; + typedef phash_map< PT(Shader), PT(EnqueuedObject) > EnqueuedShaders; typedef phash_set Buffers; typedef phash_set< PT(GeomVertexArrayData) > EnqueuedVertexBuffers; typedef phash_set< PT(GeomPrimitive) > EnqueuedIndexBuffers; diff --git a/panda/src/gobj/shader.cxx b/panda/src/gobj/shader.cxx index d60e51838f..a2c54a5d0b 100644 --- a/panda/src/gobj/shader.cxx +++ b/panda/src/gobj/shader.cxx @@ -3407,9 +3407,10 @@ parse_eof() { * Use this function instead of prepare_now() to preload textures from a user * interface standpoint. */ -void Shader:: +PT(AsyncFuture) Shader:: prepare(PreparedGraphicsObjects *prepared_objects) { - prepared_objects->enqueue_shader(this); + PT(PreparedGraphicsObjects::EnqueuedObject) obj = prepared_objects->enqueue_shader_future(this); + return obj.p(); } /** diff --git a/panda/src/gobj/shader.h b/panda/src/gobj/shader.h index 6226b02960..3b23b2d698 100644 --- a/panda/src/gobj/shader.h +++ b/panda/src/gobj/shader.h @@ -32,6 +32,7 @@ #include "pta_LVecBase3.h" #include "pta_LVecBase2.h" #include "epvector.h" +#include "asyncFuture.h" #ifdef HAVE_CG // I don't want to include the Cg header file into panda as a whole. Instead, @@ -109,7 +110,7 @@ PUBLISHED: INLINE bool get_cache_compiled_shader() const; INLINE void set_cache_compiled_shader(bool flag); - void prepare(PreparedGraphicsObjects *prepared_objects); + PT(AsyncFuture) prepare(PreparedGraphicsObjects *prepared_objects); bool is_prepared(PreparedGraphicsObjects *prepared_objects) const; bool release(PreparedGraphicsObjects *prepared_objects); int release_all(); diff --git a/panda/src/gobj/texture.cxx b/panda/src/gobj/texture.cxx index d3dd47dbb9..bddee6510b 100644 --- a/panda/src/gobj/texture.cxx +++ b/panda/src/gobj/texture.cxx @@ -1417,9 +1417,10 @@ peek() { * Use this function instead of prepare_now() to preload textures from a user * interface standpoint. */ -void Texture:: +PT(AsyncFuture) Texture:: prepare(PreparedGraphicsObjects *prepared_objects) { - prepared_objects->enqueue_texture(this); + PT(PreparedGraphicsObjects::EnqueuedObject) obj = prepared_objects->enqueue_texture_future(this); + return obj.p(); } /** diff --git a/panda/src/gobj/texture.h b/panda/src/gobj/texture.h index 15dae5f02d..6974b1c1fc 100644 --- a/panda/src/gobj/texture.h +++ b/panda/src/gobj/texture.h @@ -43,6 +43,7 @@ #include "colorSpace.h" #include "geomEnums.h" #include "bamCacheRecord.h" +#include "asyncFuture.h" class PNMImage; class PfmFile; @@ -522,7 +523,7 @@ PUBLISHED: MAKE_PROPERTY(auto_texture_scale, get_auto_texture_scale, set_auto_texture_scale); - void prepare(PreparedGraphicsObjects *prepared_objects); + PT(AsyncFuture) prepare(PreparedGraphicsObjects *prepared_objects); bool is_prepared(PreparedGraphicsObjects *prepared_objects) const; bool was_image_modified(PreparedGraphicsObjects *prepared_objects) const; size_t get_data_size_bytes(PreparedGraphicsObjects *prepared_objects) const; diff --git a/panda/src/gobj/textureReloadRequest.I b/panda/src/gobj/textureReloadRequest.I index 4cc22d6d9c..d17bdc72b4 100644 --- a/panda/src/gobj/textureReloadRequest.I +++ b/panda/src/gobj/textureReloadRequest.I @@ -22,8 +22,7 @@ TextureReloadRequest(const string &name, AsyncTask(name), _pgo(pgo), _texture(texture), - _allow_compressed(allow_compressed), - _is_ready(false) + _allow_compressed(allow_compressed) { nassertv(_pgo != (PreparedGraphicsObjects *)NULL); nassertv(_texture != (Texture *)NULL); @@ -58,8 +57,10 @@ get_allow_compressed() const { /** * Returns true if this request has completed, false if it is still pending. + * Equivalent to `req.done() and not req.cancelled()`. + * @see done() */ INLINE bool TextureReloadRequest:: is_ready() const { - return _is_ready; + return (FutureState)AtomicAdjust::get(_future_state) == FS_finished; } diff --git a/panda/src/gobj/textureReloadRequest.cxx b/panda/src/gobj/textureReloadRequest.cxx index 0b5964e2ef..4c17b550b7 100644 --- a/panda/src/gobj/textureReloadRequest.cxx +++ b/panda/src/gobj/textureReloadRequest.cxx @@ -43,7 +43,6 @@ do_task() { _texture->prepare(_pgo); } } - _is_ready = true; // Don't continue the task; we're done. return DS_done; diff --git a/panda/src/gobj/textureReloadRequest.h b/panda/src/gobj/textureReloadRequest.h index ce0684a135..1e97c99ea2 100644 --- a/panda/src/gobj/textureReloadRequest.h +++ b/panda/src/gobj/textureReloadRequest.h @@ -43,6 +43,8 @@ PUBLISHED: INLINE bool get_allow_compressed() const; INLINE bool is_ready() const; + MAKE_PROPERTY(texture, get_texture); + protected: virtual DoneStatus do_task(); @@ -50,7 +52,6 @@ private: PT(PreparedGraphicsObjects) _pgo; PT(Texture) _texture; bool _allow_compressed; - bool _is_ready; public: static TypeHandle get_class_type() { diff --git a/panda/src/pgraph/geomNode.cxx b/panda/src/pgraph/geomNode.cxx index 11d319212f..8c9da5d331 100644 --- a/panda/src/pgraph/geomNode.cxx +++ b/panda/src/pgraph/geomNode.cxx @@ -38,6 +38,7 @@ #include "boundingBox.h" #include "boundingSphere.h" #include "config_mathutil.h" +#include "preparedGraphicsObjects.h" bool allow_flatten_color = ConfigVariableBool @@ -382,14 +383,14 @@ r_prepare_scene(GraphicsStateGuardianBase *gsg, const RenderState *node_state, int num_arrays = vdata_reader.get_num_arrays(); for (int i = 0; i < num_arrays; ++i) { CPT(GeomVertexArrayData) array = vdata_reader.get_array(i); - ((GeomVertexArrayData *)array.p())->prepare(prepared_objects); + prepared_objects->enqueue_vertex_buffer((GeomVertexArrayData *)array.p()); } // And also each of the index arrays. int num_primitives = geom->get_num_primitives(); for (int i = 0; i < num_primitives; ++i) { CPT(GeomPrimitive) prim = geom->get_primitive(i); - ((GeomPrimitive *)prim.p())->prepare(prepared_objects); + prepared_objects->enqueue_index_buffer((GeomPrimitive *)prim.p()); } if (munger->is_of_type(StateMunger::get_class_type())) { @@ -405,7 +406,7 @@ r_prepare_scene(GraphicsStateGuardianBase *gsg, const RenderState *node_state, Texture *texture = ta->get_on_texture(ta->get_on_stage(i)); // TODO: prepare the sampler states, if specified. if (texture != nullptr) { - texture->prepare(prepared_objects); + prepared_objects->enqueue_texture(texture); } } } @@ -415,7 +416,7 @@ r_prepare_scene(GraphicsStateGuardianBase *gsg, const RenderState *node_state, if (geom_state->get_attrib(sa)) { Shader *shader = (Shader *)sa->get_shader(); if (shader != nullptr) { - shader->prepare(prepared_objects); + prepared_objects->enqueue_shader(shader); } // TODO: prepare the shader inputs. } diff --git a/panda/src/pgraph/loader.I b/panda/src/pgraph/loader.I index 25295c32dc..c86753b3be 100644 --- a/panda/src/pgraph/loader.I +++ b/panda/src/pgraph/loader.I @@ -135,6 +135,7 @@ stop_threads() { /** * Removes a pending asynchronous load request. Returns true if successful, * false otherwise. + * @deprecated use task.cancel() to cancel the request instead. */ INLINE bool Loader:: remove(AsyncTask *task) { diff --git a/panda/src/pgraph/modelFlattenRequest.I b/panda/src/pgraph/modelFlattenRequest.I index 84e58744da..75f177e6a8 100644 --- a/panda/src/pgraph/modelFlattenRequest.I +++ b/panda/src/pgraph/modelFlattenRequest.I @@ -18,8 +18,7 @@ INLINE ModelFlattenRequest:: ModelFlattenRequest(PandaNode *orig) : AsyncTask(orig->get_name()), - _orig(orig), - _is_ready(false) + _orig(orig) { } @@ -35,32 +34,21 @@ 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 * result(). + * Equivalent to `req.done() and not req.cancelled()`. + * @see done() */ INLINE bool ModelFlattenRequest:: is_ready() const { - return _is_ready; + return (FutureState)AtomicAdjust::get(_future_state) == FS_finished; } /** * Returns the flattened copy of the model. It is an error to call this - * unless is_ready() returns true. + * unless done() returns true. + * @deprecated Use result() instead. */ INLINE PandaNode *ModelFlattenRequest:: get_model() const { - 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(); - } + nassertr_always(done(), nullptr); + return (PandaNode *)_result; } diff --git a/panda/src/pgraph/modelFlattenRequest.cxx b/panda/src/pgraph/modelFlattenRequest.cxx index 2b02b58f00..0af2cafa15 100644 --- a/panda/src/pgraph/modelFlattenRequest.cxx +++ b/panda/src/pgraph/modelFlattenRequest.cxx @@ -35,8 +35,8 @@ do_task() { np.attach_new_node(_orig); } np.flatten_strong(); - _model = np.get_child(0).node(); - _is_ready = true; + + set_result(np.get_child(0).node()); // Don't continue the task; we're done. return DS_done; diff --git a/panda/src/pgraph/modelFlattenRequest.h b/panda/src/pgraph/modelFlattenRequest.h index d3ff059482..a166490edb 100644 --- a/panda/src/pgraph/modelFlattenRequest.h +++ b/panda/src/pgraph/modelFlattenRequest.h @@ -39,18 +39,13 @@ 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); protected: virtual DoneStatus do_task(); private: PT(PandaNode) _orig; - bool _is_ready; - PT(PandaNode) _model; public: static TypeHandle get_class_type() { diff --git a/panda/src/pgraph/modelLoadRequest.I b/panda/src/pgraph/modelLoadRequest.I index 2b8ef41544..3fb37a1bdc 100644 --- a/panda/src/pgraph/modelLoadRequest.I +++ b/panda/src/pgraph/modelLoadRequest.I @@ -38,31 +38,24 @@ get_loader() const { } /** - * 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 - * get_model(). + * Returns true if this request has completed, false if it is still pending or + * if it has been cancelled. When this returns true, you may retrieve the + * model loaded by calling get_model(). + * Equivalent to `req.done() and not req.cancelled()`. + * @see done() */ INLINE bool ModelLoadRequest:: is_ready() const { - return _is_ready; + return (FutureState)AtomicAdjust::get(_future_state) == FS_finished; } /** - * Returns the model 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 model that was loaded asynchronously, if any, or null if there + * was an error. It is an error to call this unless done() returns true. + * @deprecated Use result() instead. */ INLINE PandaNode *ModelLoadRequest:: get_model() const { - nassertr(_is_ready, NULL); - return _model; + nassertr_always(done(), nullptr); + return (PandaNode *)_result; } diff --git a/panda/src/pgraph/modelLoadRequest.cxx b/panda/src/pgraph/modelLoadRequest.cxx index bc1b767b5c..4feba702ef 100644 --- a/panda/src/pgraph/modelLoadRequest.cxx +++ b/panda/src/pgraph/modelLoadRequest.cxx @@ -28,8 +28,7 @@ ModelLoadRequest(const string &name, AsyncTask(name), _filename(filename), _options(options), - _loader(loader), - _is_ready(false) + _loader(loader) { } @@ -43,8 +42,8 @@ do_task() { Thread::sleep(delay); } - _model = _loader->load_sync(_filename, _options); - _is_ready = true; + PT(PandaNode) model = _loader->load_sync(_filename, _options); + set_result(model); // Don't continue the task; we're done. return DS_done; diff --git a/panda/src/pgraph/modelLoadRequest.h b/panda/src/pgraph/modelLoadRequest.h index 946a9db503..e743623fa3 100644 --- a/panda/src/pgraph/modelLoadRequest.h +++ b/panda/src/pgraph/modelLoadRequest.h @@ -43,15 +43,12 @@ 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; MAKE_PROPERTY(filename, get_filename); MAKE_PROPERTY(options, get_options); MAKE_PROPERTY(loader, get_loader); - MAKE_PROPERTY(ready, is_ready); protected: virtual DoneStatus do_task(); @@ -60,8 +57,6 @@ private: Filename _filename; LoaderOptions _options; PT(Loader) _loader; - bool _is_ready; - PT(PandaNode) _model; public: static TypeHandle get_class_type() { diff --git a/panda/src/pgraph/modelSaveRequest.I b/panda/src/pgraph/modelSaveRequest.I index 14940aaf0f..e84b8929af 100644 --- a/panda/src/pgraph/modelSaveRequest.I +++ b/panda/src/pgraph/modelSaveRequest.I @@ -49,28 +49,20 @@ get_loader() const { * Returns true if this request has completed, false if it is still pending. * When this returns true, you may retrieve the success flag with * get_success(). + * Equivalent to `req.done() and not req.cancelled()`. + * @see done() */ INLINE bool ModelSaveRequest:: is_ready() const { - return _is_ready; + return (FutureState)AtomicAdjust::get(_future_state) == FS_finished; } /** * Returns the true if the model was saved successfully, false otherwise. It - * is an error to call this unless is_ready() returns true. + * is an error to call this unless done() returns true. */ INLINE bool ModelSaveRequest:: 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); + nassertr_always(done(), false); return _success; } diff --git a/panda/src/pgraph/modelSaveRequest.cxx b/panda/src/pgraph/modelSaveRequest.cxx index 45f985454a..31c50cfdf6 100644 --- a/panda/src/pgraph/modelSaveRequest.cxx +++ b/panda/src/pgraph/modelSaveRequest.cxx @@ -30,7 +30,6 @@ ModelSaveRequest(const string &name, _options(options), _node(node), _loader(loader), - _is_ready(false), _success(false) { } @@ -46,7 +45,6 @@ do_task() { } _success = _loader->save_sync(_filename, _options, _node); - _is_ready = true; // Don't continue the task; we're done. return DS_done; diff --git a/panda/src/pgraph/modelSaveRequest.h b/panda/src/pgraph/modelSaveRequest.h index 5fe3c683ae..7bbe1e331d 100644 --- a/panda/src/pgraph/modelSaveRequest.h +++ b/panda/src/pgraph/modelSaveRequest.h @@ -46,13 +46,10 @@ 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); protected: virtual DoneStatus do_task(); @@ -62,7 +59,6 @@ private: LoaderOptions _options; PT(PandaNode) _node; PT(Loader) _loader; - bool _is_ready; bool _success; public: diff --git a/panda/src/pipeline/asyncTaskBase.I b/panda/src/pipeline/asyncTaskBase.I deleted file mode 100644 index 09abcb3a3d..0000000000 --- a/panda/src/pipeline/asyncTaskBase.I +++ /dev/null @@ -1,12 +0,0 @@ -/** - * 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 asyncTaskBase.I - * @author drose - * @date 2010-02-09 - */ diff --git a/panda/src/pipeline/asyncTaskBase.cxx b/panda/src/pipeline/asyncTaskBase.cxx deleted file mode 100644 index 4d91d83764..0000000000 --- a/panda/src/pipeline/asyncTaskBase.cxx +++ /dev/null @@ -1,77 +0,0 @@ -/** - * 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 asyncTaskBase.cxx - * @author drose - * @date 2010-02-09 - */ - -#include "asyncTaskBase.h" -#include "thread.h" -#include "atomicAdjust.h" - -TypeHandle AsyncTaskBase::_type_handle; - -/** - * - */ -AsyncTaskBase:: -AsyncTaskBase() { -} - -/** - * - */ -AsyncTaskBase:: -~AsyncTaskBase() { -} - -/** - * Indicates that this task is now the current task running on the indicated - * thread, presumably the current thread. - */ -void AsyncTaskBase:: -record_task(Thread *current_thread) { - nassertv(current_thread->_current_task == NULL); - - void *result = AtomicAdjust::compare_and_exchange_ptr - ((void * TVOLATILE &)current_thread->_current_task, - (void *)NULL, (void *)this); - - // If the return value is other than NULL, someone else must have assigned - // the task first, in another thread. That shouldn't be possible. - - // But different versions of gcc appear to have problems compiling these - // assertions correctly. -#ifndef __GNUC__ - nassertv(result == NULL); - nassertv(current_thread->_current_task == this); -#endif // __GNUC__ -} - -/** - * Indicates that this task is no longer running on the indicated thread. - */ -void AsyncTaskBase:: -clear_task(Thread *current_thread) { - nassertv(current_thread->_current_task == this); - - void *result = AtomicAdjust::compare_and_exchange_ptr - ((void * TVOLATILE &)current_thread->_current_task, - (void *)this, (void *)NULL); - - // If the return value is other than this, someone else must have assigned - // the task first, in another thread. That shouldn't be possible. - - // But different versions of gcc appear to have problems compiling these - // assertions correctly. -#ifndef __GNUC__ - nassertv(result == this); - nassertv(current_thread->_current_task == NULL); -#endif // __GNUC__ -} diff --git a/panda/src/pipeline/asyncTaskBase.h b/panda/src/pipeline/asyncTaskBase.h deleted file mode 100644 index cdf3884b87..0000000000 --- a/panda/src/pipeline/asyncTaskBase.h +++ /dev/null @@ -1,61 +0,0 @@ -/** - * 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 asyncTaskBase.h - * @author drose - * @date 2010-02-09 - */ - -#ifndef ASYNCTASKBASE_H -#define ASYNCTASKBASE_H - -#include "pandabase.h" - -#include "typedReferenceCount.h" -#include "namable.h" - -class Thread; - -/** - * The abstract base class for AsyncTask. This is defined here only so we can - * store a pointer to the current task on the Thread. - */ -class EXPCL_PANDA_PIPELINE AsyncTaskBase : public TypedReferenceCount, public Namable { -protected: - AsyncTaskBase(); -public: - ALLOC_DELETED_CHAIN(AsyncTaskBase); - -PUBLISHED: - virtual ~AsyncTaskBase(); - -protected: - void record_task(Thread *current_thread); - void clear_task(Thread *current_thread); - -public: - static TypeHandle get_class_type() { - return _type_handle; - } - static void init_type() { - TypedReferenceCount::init_type(); - register_type(_type_handle, "AsyncTaskBase", - TypedReferenceCount::get_class_type()); - } - virtual TypeHandle get_type() const { - return get_class_type(); - } - virtual TypeHandle force_init_type() {init_type(); return get_class_type();} - -private: - static TypeHandle _type_handle; -}; - -#include "asyncTaskBase.I" - -#endif diff --git a/panda/src/pipeline/config_pipeline.cxx b/panda/src/pipeline/config_pipeline.cxx index 24b23da485..6dc24178f0 100644 --- a/panda/src/pipeline/config_pipeline.cxx +++ b/panda/src/pipeline/config_pipeline.cxx @@ -12,7 +12,6 @@ */ #include "config_pipeline.h" -#include "asyncTaskBase.h" #include "mainThread.h" #include "externalThread.h" #include "genericThread.h" @@ -67,7 +66,6 @@ init_libpipeline() { } initialized = true; - AsyncTaskBase::init_type(); MainThread::init_type(); ExternalThread::init_type(); GenericThread::init_type(); diff --git a/panda/src/pipeline/p3pipeline_composite1.cxx b/panda/src/pipeline/p3pipeline_composite1.cxx index 40dd5b3647..487c5ca0ad 100644 --- a/panda/src/pipeline/p3pipeline_composite1.cxx +++ b/panda/src/pipeline/p3pipeline_composite1.cxx @@ -1,4 +1,3 @@ -#include "asyncTaskBase.cxx" #include "conditionVar.cxx" #include "conditionVarDebug.cxx" #include "conditionVarDirect.cxx" diff --git a/panda/src/pipeline/thread.I b/panda/src/pipeline/thread.I index 321d24da6a..12a8607e63 100644 --- a/panda/src/pipeline/thread.I +++ b/panda/src/pipeline/thread.I @@ -277,7 +277,7 @@ preempt() { * AsyncTaskManager), if any, or NULL if the thread is not currently servicing * a task. */ -INLINE AsyncTaskBase *Thread:: +INLINE AsyncTask *Thread:: get_current_task() const { return _current_task; } diff --git a/panda/src/pipeline/thread.h b/panda/src/pipeline/thread.h index 80844ea409..4dbebacc8b 100644 --- a/panda/src/pipeline/thread.h +++ b/panda/src/pipeline/thread.h @@ -28,7 +28,7 @@ class ReMutex; class MutexDebug; class ConditionVarDebug; class ConditionVarFullDebug; -class AsyncTaskBase; +class AsyncTask; /** * A thread; that is, a lightweight process. This is an abstract base class; @@ -89,7 +89,7 @@ PUBLISHED: BLOCKING INLINE void join(); INLINE void preempt(); - INLINE AsyncTaskBase *get_current_task() const; + INLINE AsyncTask *get_current_task() const; INLINE void set_python_index(int index); @@ -142,7 +142,7 @@ private: int _pipeline_stage; PStatsCallback *_pstats_callback; bool _joinable; - AsyncTaskBase *_current_task; + AsyncTask *_current_task; int _python_index; @@ -184,7 +184,7 @@ private: friend class ThreadPosixImpl; friend class ThreadSimpleImpl; friend class MainThread; - friend class AsyncTaskBase; + friend class AsyncTask; }; INLINE ostream &operator << (ostream &out, const Thread &thread); diff --git a/tests/event/test_futures.py b/tests/event/test_futures.py new file mode 100644 index 0000000000..c353834257 --- /dev/null +++ b/tests/event/test_futures.py @@ -0,0 +1,212 @@ +from panda3d import core +import pytest +import threading +import time +import sys + +if sys.version_info >= (3,): + from concurrent.futures._base import TimeoutError, CancelledError +else: + TimeoutError = Exception + CancelledError = Exception + + +def test_future_cancelled(): + fut = core.AsyncFuture() + + assert not fut.done() + assert not fut.cancelled() + fut.cancel() + assert fut.done() + assert fut.cancelled() + + with pytest.raises(CancelledError): + fut.result() + + # Works more than once + with pytest.raises(CancelledError): + fut.result() + + +def test_future_timeout(): + fut = core.AsyncFuture() + + with pytest.raises(TimeoutError): + fut.result(0.001) + + # Works more than once + with pytest.raises(TimeoutError): + fut.result(0.001) + + +def test_future_wait(): + fut = core.AsyncFuture() + + # Launch a thread to set the result value. + def thread_main(): + time.sleep(0.001) + fut.set_result(None) + + thread = threading.Thread(target=thread_main) + thread.start() + + # Make sure it didn't sneakily already run the thread + assert not fut.done() + + assert fut.result() is None + + assert fut.done() + assert not fut.cancelled() + assert fut.result() is None + + +def test_future_wait_cancel(): + fut = core.AsyncFuture() + + # Launch a thread to cancel the future. + def thread_main(): + time.sleep(0.001) + fut.cancel() + + thread = threading.Thread(target=thread_main) + thread.start() + + # Make sure it didn't sneakily already run the thread + assert not fut.done() + + with pytest.raises(CancelledError): + fut.result() + + assert fut.done() + assert fut.cancelled() + with pytest.raises(CancelledError): + fut.result() + + +def test_task_cancel(): + task_mgr = core.AsyncTaskManager.get_global_ptr() + task = core.PythonTask(lambda task: task.done) + task_mgr.add(task) + + assert not task.done() + task_mgr.remove(task) + assert task.done() + assert task.cancelled() + + with pytest.raises(CancelledError): + task.result() + + +def test_task_cancel_during_run(): + task_mgr = core.AsyncTaskManager.get_global_ptr() + task_chain = task_mgr.make_task_chain("test_task_cancel_during_run") + + def task_main(task): + task.remove() + + # It won't yet be marked done until after it returns. + assert not task.done() + return task.done + + task = core.PythonTask(task_main) + task.set_task_chain(task_chain.name) + task_mgr.add(task) + task_chain.wait_for_tasks() + + assert task.done() + assert task.cancelled() + with pytest.raises(CancelledError): + task.result() + + +def test_task_result(): + task_mgr = core.AsyncTaskManager.get_global_ptr() + task_chain = task_mgr.make_task_chain("test_task_result") + + def task_main(task): + task.set_result(42) + + # It won't yet be marked done until after it returns. + assert not task.done() + return core.PythonTask.done + + task = core.PythonTask(task_main) + task.set_task_chain(task_chain.name) + task_mgr.add(task) + task_chain.wait_for_tasks() + + assert task.done() + assert not task.cancelled() + assert task.result() == 42 + + +def test_coro_exception(): + task_mgr = core.AsyncTaskManager.get_global_ptr() + task_chain = task_mgr.make_task_chain("test_coro_exception") + + def coro_main(): + raise RuntimeError + yield None + + task = core.PythonTask(coro_main()) + task.set_task_chain(task_chain.name) + task_mgr.add(task) + task_chain.wait_for_tasks() + + assert task.done() + assert not task.cancelled() + with pytest.raises(RuntimeError): + task.result() + + +def test_event_future(): + queue = core.EventQueue() + handler = core.EventHandler(queue) + + fut = handler.get_future("test") + + # If we ask again, we should get the same one. + assert handler.get_future("test") == fut + + event = core.Event("test") + handler.dispatch_event(event) + + assert fut.done() + assert not fut.cancelled() + assert fut.result() == event + + +def test_event_future_cancel(): + # This is a very strange thing to do, but it's possible, so let's make + # sure it gives defined behavior. + queue = core.EventQueue() + handler = core.EventHandler(queue) + + fut = handler.get_future("test") + fut.cancel() + + assert fut.done() + assert fut.cancelled() + + event = core.Event("test") + handler.dispatch_event(event) + + assert fut.done() + assert fut.cancelled() + + +def test_event_future_cancel2(): + queue = core.EventQueue() + handler = core.EventHandler(queue) + + # Make sure we get a new future if we cancelled the first one. + fut = handler.get_future("test") + fut.cancel() + fut2 = handler.get_future("test") + + assert fut != fut2 + assert fut.done() + assert fut.cancelled() + assert not fut2.done() + assert not fut2.cancelled() + From 7a46b2ca60aacc1b8f1c87a7a101546bd6214120 Mon Sep 17 00:00:00 2001 From: deflected Date: Tue, 28 Nov 2017 19:22:13 +0200 Subject: [PATCH 04/25] ShowBase: Minor fixes over aspect ratio and size calculations - Do not take into accound SBS if it is not enabled - Always return value for getSize() - Properly handle SBS for window-events - Redo positioning of aspect2d(p) markers exactly as they are created when adjusting aspect ratio Signed-off-by: deflected --- direct/src/showbase/ShowBase.py | 60 ++++++++++++++++----------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index 2d43dba169..bc322214f5 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -1258,9 +1258,9 @@ class ShowBase(DirectObject.DirectObject): if win == None: win = self.win - if win != None and win.hasSize() and win.getSbsLeftYSize() != 0: + if win != None and win.getSideBySideStereo() and \ + win.hasSize() and win.getSbsLeftYSize() != 0: aspectRatio = float(win.getSbsLeftXSize()) / float(win.getSbsLeftYSize()) - else: if win == None or not hasattr(win, "getRequestedProperties"): props = WindowProperties.getDefault() @@ -1295,8 +1295,7 @@ class ShowBase(DirectObject.DirectObject): if not props.hasSize(): props = WindowProperties.getDefault() - if props.hasSize(): - return props.getXSize(), props.getYSize() + return props.getXSize(), props.getYSize() def makeCamera(self, win, sort = 0, scene = None, displayRegion = (0, 1, 0, 1), stereo = None, @@ -2723,13 +2722,14 @@ class ShowBase(DirectObject.DirectObject): # changed and update the camera lenses and aspect2d parameters self.adjustWindowAspectRatio(self.getAspectRatio()) - # Temporary hasattr for old Pandas - if not hasattr(win, 'getSbsLeftXSize'): - self.pixel2d.setScale(2.0 / win.getXSize(), 1.0, 2.0 / win.getYSize()) - self.pixel2dp.setScale(2.0 / win.getXSize(), 1.0, 2.0 / win.getYSize()) - else: + if win.getSideBySideStereo() and win.hasSize() and win.getSbsLeftYSize() != 0: self.pixel2d.setScale(2.0 / win.getSbsLeftXSize(), 1.0, 2.0 / win.getSbsLeftYSize()) self.pixel2dp.setScale(2.0 / win.getSbsLeftXSize(), 1.0, 2.0 / win.getSbsLeftYSize()) + else: + xsize, ysize = self.getSize() + if xsize > 0 and ysize > 0: + self.pixel2d.setScale(2.0 / xsize, 1.0, 2.0 / ysize) + self.pixel2dp.setScale(2.0 / xsize, 1.0, 2.0 / ysize) def adjustWindowAspectRatio(self, aspectRatio): """ This function is normally called internally by @@ -2774,34 +2774,34 @@ class ShowBase(DirectObject.DirectObject): self.a2dpRight = aspectRatio # Reposition the aspect2d marker nodes - self.a2dTopCenter.setPos(0, self.a2dTop, self.a2dTop) - self.a2dBottomCenter.setPos(0, self.a2dBottom, self.a2dBottom) + self.a2dTopCenter.setPos(0, 0, self.a2dTop) + self.a2dTopCenterNs.setPos(0, 0, self.a2dTop) + self.a2dBottomCenter.setPos(0, 0, self.a2dBottom) + self.a2dBottomCenterNs.setPos(0, 0, self.a2dBottom) self.a2dLeftCenter.setPos(self.a2dLeft, 0, 0) - self.a2dRightCenter.setPos(self.a2dRight, 0, 0) - self.a2dTopLeft.setPos(self.a2dLeft, self.a2dTop, self.a2dTop) - self.a2dTopRight.setPos(self.a2dRight, self.a2dTop, self.a2dTop) - self.a2dBottomLeft.setPos(self.a2dLeft, self.a2dBottom, self.a2dBottom) - self.a2dBottomRight.setPos(self.a2dRight, self.a2dBottom, self.a2dBottom) - - # Reposition the aspect2d marker nodes - self.a2dTopCenterNs.setPos(0, self.a2dTop, self.a2dTop) - self.a2dBottomCenterNs.setPos(0, self.a2dBottom, self.a2dBottom) self.a2dLeftCenterNs.setPos(self.a2dLeft, 0, 0) + self.a2dRightCenter.setPos(self.a2dRight, 0, 0) self.a2dRightCenterNs.setPos(self.a2dRight, 0, 0) - self.a2dTopLeftNs.setPos(self.a2dLeft, self.a2dTop, self.a2dTop) - self.a2dTopRightNs.setPos(self.a2dRight, self.a2dTop, self.a2dTop) - self.a2dBottomLeftNs.setPos(self.a2dLeft, self.a2dBottom, self.a2dBottom) - self.a2dBottomRightNs.setPos(self.a2dRight, self.a2dBottom, self.a2dBottom) + + self.a2dTopLeft.setPos(self.a2dLeft, 0, self.a2dTop) + self.a2dTopLeftNs.setPos(self.a2dLeft, 0, self.a2dTop) + self.a2dTopRight.setPos(self.a2dRight, 0, self.a2dTop) + self.a2dTopRightNs.setPos(self.a2dRight, 0, self.a2dTop) + self.a2dBottomLeft.setPos(self.a2dLeft, 0, self.a2dBottom) + self.a2dBottomLeftNs.setPos(self.a2dLeft, 0, self.a2dBottom) + self.a2dBottomRight.setPos(self.a2dRight, 0, self.a2dBottom) + self.a2dBottomRightNs.setPos(self.a2dRight, 0, self.a2dBottom) # Reposition the aspect2dp marker nodes - self.a2dpTopCenter.setPos(0, self.a2dpTop, self.a2dpTop) - self.a2dpBottomCenter.setPos(0, self.a2dpBottom, self.a2dpBottom) + self.a2dpTopCenter.setPos(0, 0, self.a2dpTop) + self.a2dpBottomCenter.setPos(0, 0, self.a2dpBottom) self.a2dpLeftCenter.setPos(self.a2dpLeft, 0, 0) self.a2dpRightCenter.setPos(self.a2dpRight, 0, 0) - self.a2dpTopLeft.setPos(self.a2dpLeft, self.a2dpTop, self.a2dpTop) - self.a2dpTopRight.setPos(self.a2dpRight, self.a2dpTop, self.a2dpTop) - self.a2dpBottomLeft.setPos(self.a2dpLeft, self.a2dpBottom, self.a2dpBottom) - self.a2dpBottomRight.setPos(self.a2dpRight, self.a2dpBottom, self.a2dpBottom) + + self.a2dpTopLeft.setPos(self.a2dpLeft, 0, self.a2dpTop) + self.a2dpTopRight.setPos(self.a2dpRight, 0, self.a2dpTop) + self.a2dpBottomLeft.setPos(self.a2dpLeft, 0, self.a2dpBottom) + self.a2dpBottomRight.setPos(self.a2dpRight, 0, self.a2dpBottom) # If anybody needs to update their GUI, put a callback on this event messenger.send("aspectRatioChanged") From bdd53d60fc1729ba310251a78d8ae5156a1b0ee7 Mon Sep 17 00:00:00 2001 From: deflected Date: Wed, 29 Nov 2017 10:38:26 +0200 Subject: [PATCH 05/25] ShowBase: Fixed crash when want-render2dp is False - Fixed crash in ShowBase when want-render2dp setting is set to 0(False). Signed-off-by: deflected --- direct/src/showbase/ShowBase.py | 52 +++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index bc322214f5..99a8b77abf 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -968,7 +968,9 @@ class ShowBase(DirectObject.DirectObject): if isinstance(self.win, GraphicsWindow): self.setupMouse(self.win) self.makeCamera2d(self.win) - self.makeCamera2dp(self.win) + + if self.wantRender2dp: + self.makeCamera2dp(self.win) if oldLens != None: # Restore the previous lens properties. @@ -1559,9 +1561,11 @@ class ShowBase(DirectObject.DirectObject): # Tell the gui system about our new mouse watcher. self.aspect2d.node().setMouseWatcher(mw.node()) - self.aspect2dp.node().setMouseWatcher(mw.node()) self.pixel2d.node().setMouseWatcher(mw.node()) - self.pixel2dp.node().setMouseWatcher(mw.node()) + if self.wantRender2dp: + self.aspect2dp.node().setMouseWatcher(mw.node()) + self.pixel2dp.node().setMouseWatcher(mw.node()) + mw.node().addRegion(PGMouseWatcherBackground()) return self.buttonThrowers[0] @@ -2729,7 +2733,8 @@ class ShowBase(DirectObject.DirectObject): xsize, ysize = self.getSize() if xsize > 0 and ysize > 0: self.pixel2d.setScale(2.0 / xsize, 1.0, 2.0 / ysize) - self.pixel2dp.setScale(2.0 / xsize, 1.0, 2.0 / ysize) + if self.wantRender2dp: + self.pixel2dp.setScale(2.0 / xsize, 1.0, 2.0 / ysize) def adjustWindowAspectRatio(self, aspectRatio): """ This function is normally called internally by @@ -2753,11 +2758,12 @@ class ShowBase(DirectObject.DirectObject): self.a2dLeft = -1 self.a2dRight = 1.0 # Don't forget 2dp - self.aspect2dp.setScale(1.0, aspectRatio, aspectRatio) - self.a2dpTop = 1.0 / aspectRatio - self.a2dpBottom = - 1.0 / aspectRatio - self.a2dpLeft = -1 - self.a2dpRight = 1.0 + if self.wantRender2dp: + self.aspect2dp.setScale(1.0, aspectRatio, aspectRatio) + self.a2dpTop = 1.0 / aspectRatio + self.a2dpBottom = - 1.0 / aspectRatio + self.a2dpLeft = -1 + self.a2dpRight = 1.0 else: # If the window is WIDE, lets expand the left and right @@ -2767,11 +2773,12 @@ class ShowBase(DirectObject.DirectObject): self.a2dLeft = -aspectRatio self.a2dRight = aspectRatio # Don't forget 2dp - self.aspect2dp.setScale(1.0 / aspectRatio, 1.0, 1.0) - self.a2dpTop = 1.0 - self.a2dpBottom = -1.0 - self.a2dpLeft = -aspectRatio - self.a2dpRight = aspectRatio + if self.wantRender2dp: + self.aspect2dp.setScale(1.0 / aspectRatio, 1.0, 1.0) + self.a2dpTop = 1.0 + self.a2dpBottom = -1.0 + self.a2dpLeft = -aspectRatio + self.a2dpRight = aspectRatio # Reposition the aspect2d marker nodes self.a2dTopCenter.setPos(0, 0, self.a2dTop) @@ -2793,15 +2800,16 @@ class ShowBase(DirectObject.DirectObject): self.a2dBottomRightNs.setPos(self.a2dRight, 0, self.a2dBottom) # Reposition the aspect2dp marker nodes - self.a2dpTopCenter.setPos(0, 0, self.a2dpTop) - self.a2dpBottomCenter.setPos(0, 0, self.a2dpBottom) - self.a2dpLeftCenter.setPos(self.a2dpLeft, 0, 0) - self.a2dpRightCenter.setPos(self.a2dpRight, 0, 0) + if self.wantRender2dp: + self.a2dpTopCenter.setPos(0, 0, self.a2dpTop) + self.a2dpBottomCenter.setPos(0, 0, self.a2dpBottom) + self.a2dpLeftCenter.setPos(self.a2dpLeft, 0, 0) + self.a2dpRightCenter.setPos(self.a2dpRight, 0, 0) - self.a2dpTopLeft.setPos(self.a2dpLeft, 0, self.a2dpTop) - self.a2dpTopRight.setPos(self.a2dpRight, 0, self.a2dpTop) - self.a2dpBottomLeft.setPos(self.a2dpLeft, 0, self.a2dpBottom) - self.a2dpBottomRight.setPos(self.a2dpRight, 0, self.a2dpBottom) + self.a2dpTopLeft.setPos(self.a2dpLeft, 0, self.a2dpTop) + self.a2dpTopRight.setPos(self.a2dpRight, 0, self.a2dpTop) + self.a2dpBottomLeft.setPos(self.a2dpLeft, 0, self.a2dpBottom) + self.a2dpBottomRight.setPos(self.a2dpRight, 0, self.a2dpBottom) # If anybody needs to update their GUI, put a callback on this event messenger.send("aspectRatioChanged") From 46189dc1bf794f6ee10ab77a1b22381713967df4 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 4 Dec 2017 22:51:41 +0100 Subject: [PATCH 06/25] Compile fix for macOS build [skip ci] --- panda/src/event/eventHandler.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/event/eventHandler.cxx b/panda/src/event/eventHandler.cxx index 4e15354839..020ba905d5 100644 --- a/panda/src/event/eventHandler.cxx +++ b/panda/src/event/eventHandler.cxx @@ -103,7 +103,7 @@ dispatch_event(const Event *event) { } // Finally, check for futures that need to be triggered. - Futures::const_iterator fi; + Futures::iterator fi; fi = _futures.find(event->get_name()); if (fi != _futures.end()) { From 3e83f8c65e64b50ec979444107d3bd9858c721f6 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 12 Dec 2017 15:25:21 +0100 Subject: [PATCH 07/25] Fix priority argument ignored in NodePath::set_shader_input --- panda/src/pgraph/nodePath_ext.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/pgraph/nodePath_ext.cxx b/panda/src/pgraph/nodePath_ext.cxx index db517c8453..0172fe0c29 100644 --- a/panda/src/pgraph/nodePath_ext.cxx +++ b/panda/src/pgraph/nodePath_ext.cxx @@ -251,7 +251,7 @@ set_shader_input(CPT_InternalName name, PyObject *value, int priority) { } ShaderInput &input = attrib->_inputs[name]; - invoke_extension(&input).__init__(move(name), value); + invoke_extension(&input).__init__(move(name), value, priority); if (!_PyErr_OCCURRED()) { node->set_attrib(ShaderAttrib::return_new(attrib)); From 18678214aefcef579adc070ad7ac52bc3a304f50 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 12 Dec 2017 16:20:59 +0100 Subject: [PATCH 08/25] interrogate: pass "args" tuple to function that takes PyObject *args --- dtool/src/interrogate/functionRemap.cxx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dtool/src/interrogate/functionRemap.cxx b/dtool/src/interrogate/functionRemap.cxx index 27ec829479..42a420644f 100644 --- a/dtool/src/interrogate/functionRemap.cxx +++ b/dtool/src/interrogate/functionRemap.cxx @@ -797,6 +797,7 @@ setup_properties(const InterrogateFunction &ifunc, InterfaceMaker *interface_mak (_parameters[first_param + 1]._name == "kwargs" || _parameters[first_param + 1]._name == "kwds")) { _flags |= F_explicit_args; + _args_type = InterfaceMaker::AT_keyword_args; } } @@ -909,6 +910,13 @@ setup_properties(const InterrogateFunction &ifunc, InterfaceMaker *interface_mak break; } } + } else if (_args_type == InterfaceMaker::AT_single_arg) { + // If it takes an argument named "args", we are directly passing the + // "args" tuple to the function. + if (_parameters[first_param]._name == "args") { + _flags |= F_explicit_args; + _args_type = InterfaceMaker::AT_varargs; + } } } break; From 193e4b5f5946251264654749913dc273fcf10eec Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 12 Dec 2017 22:01:34 +0100 Subject: [PATCH 09/25] interrogate: clean py_panda.h; use macros to access Dtool_PyInstDef --- dtool/src/dtoolutil/filename_ext.cxx | 2 +- .../interfaceMakerPythonNative.cxx | 56 ++++++------- dtool/src/interrogatedb/py_compat.h | 6 ++ dtool/src/interrogatedb/py_panda.I | 48 +++++++++-- dtool/src/interrogatedb/py_panda.cxx | 84 +++++++------------ dtool/src/interrogatedb/py_panda.h | 39 +++++---- panda/src/event/pythonTask.cxx | 7 +- panda/src/express/pointerToArray_ext.I | 4 +- panda/src/gobj/texture.h | 4 +- panda/src/gobj/texture_ext.cxx | 26 +++--- panda/src/pgraph/nodePathCollection_ext.cxx | 3 +- panda/src/pgraph/nodePath_ext.cxx | 4 +- panda/src/pgraph/shaderInput_ext.cxx | 71 ++++++++-------- panda/src/putil/typedWritable_ext.cxx | 4 +- 14 files changed, 184 insertions(+), 174 deletions(-) diff --git a/dtool/src/dtoolutil/filename_ext.cxx b/dtool/src/dtoolutil/filename_ext.cxx index 1c9bf78e7e..3d71317786 100644 --- a/dtool/src/dtoolutil/filename_ext.cxx +++ b/dtool/src/dtoolutil/filename_ext.cxx @@ -55,7 +55,7 @@ __init__(PyObject *path) { if (Py_TYPE(path) == &Dtool_Filename._PyType) { // Copy constructor. - (*_this) = *((Filename *)((Dtool_PyInstDef *)path)->_ptr_to_object); + *_this = *(Filename *)DtoolInstance_VOID_PTR(path); return; } diff --git a/dtool/src/interrogate/interfaceMakerPythonNative.cxx b/dtool/src/interrogate/interfaceMakerPythonNative.cxx index 904df4c153..3a861a4844 100644 --- a/dtool/src/interrogate/interfaceMakerPythonNative.cxx +++ b/dtool/src/interrogate/interfaceMakerPythonNative.cxx @@ -1094,14 +1094,14 @@ write_class_details(ostream &out, Object *obj) { // Write support methods to cast from and to pointers of this type. { out << "static void *Dtool_UpcastInterface_" << ClassName << "(PyObject *self, Dtool_PyTypedObject *requested_type) {\n"; - out << " Dtool_PyTypedObject *SelfType = ((Dtool_PyInstDef *)self)->_My_Type;\n"; - out << " if (SelfType != Dtool_Ptr_" << ClassName << ") {\n"; + out << " Dtool_PyTypedObject *type = DtoolInstance_TYPE(self);\n"; + out << " if (type != &Dtool_" << ClassName << ") {\n"; out << " printf(\"" << ClassName << " ** Bad Source Type-- Requesting Conversion from %s to %s\\n\", Py_TYPE(self)->tp_name, requested_type->_PyType.tp_name); fflush(NULL);\n";; out << " return NULL;\n"; out << " }\n"; out << "\n"; - out << " " << cClassName << " *local_this = (" << cClassName << " *)((Dtool_PyInstDef *)self)->_ptr_to_object;\n"; - out << " if (requested_type == Dtool_Ptr_" << ClassName << ") {\n"; + out << " " << cClassName << " *local_this = (" << cClassName << " *)DtoolInstance_VOID_PTR(self);\n"; + out << " if (requested_type == &Dtool_" << ClassName << ") {\n"; out << " return local_this;\n"; out << " }\n"; @@ -2220,13 +2220,13 @@ write_module_class(ostream &out, Object *obj) { // provide a writable buffer or a readonly buffer. const string const_this = "(const " + cClassName + " *)local_this"; if (remap_const != NULL && remap_nonconst != NULL) { - out << " if (!((Dtool_PyInstDef *)self)->_is_const) {\n"; + out << " if (!DtoolInstance_IS_CONST(self)) {\n"; out << " return " << remap_nonconst->call_function(out, 4, false, "local_this", params_nonconst) << ";\n"; out << " } else {\n"; out << " return " << remap_const->call_function(out, 4, false, const_this, params_const) << ";\n"; out << " }\n"; } else if (remap_nonconst != NULL) { - out << " if (!((Dtool_PyInstDef *)self)->_is_const) {\n"; + out << " if (!DtoolInstance_IS_CONST(self)) {\n"; out << " return " << remap_nonconst->call_function(out, 4, false, "local_this", params_nonconst) << ";\n"; out << " } else {\n"; out << " Dtool_Raise_TypeError(\"Cannot call " << ClassName << ".__getbuffer__() on a const object.\");\n"; @@ -2285,7 +2285,7 @@ write_module_class(ostream &out, Object *obj) { string return_expr; const string const_this = "(const " + cClassName + " *)local_this"; if (remap_const != NULL && remap_nonconst != NULL) { - out << " if (!((Dtool_PyInstDef *)self)->_is_const) {\n"; + out << " if (!DtoolInstance_IS_CONST(self)) {\n"; return_expr = remap_nonconst->call_function(out, 4, false, "local_this", params_nonconst); if (!return_expr.empty()) { out << " " << return_expr << ";\n"; @@ -3078,7 +3078,7 @@ write_module_class(ostream &out, Object *obj) { out << " Dtool_" << ClassName << "._PyType.tp_bases = PyTuple_Pack(" << bases.size() << baseargs << ");\n"; } else { - out << " Dtool_" << ClassName << "._PyType.tp_base = (PyTypeObject *)Dtool_Ptr_DTOOL_SUPER_BASE;\n"; + out << " Dtool_" << ClassName << "._PyType.tp_base = (PyTypeObject *)&Dtool_DTOOL_SUPER_BASE;\n"; } int num_nested = obj->_itype.number_of_nested_types(); @@ -3515,8 +3515,9 @@ write_function_for_name(ostream &out, Object *obj, out << " if (!Dtool_Call_ExtractThisPointer_NonConst(self, Dtool_" << ClassName << ", " << "(void **)&local_this, \"" << classNameFromCppName(cClassName, false) << "." << methodNameFromCppName(remap, cClassName, false) << "\")) {\n"; + } else { - out << " if (!Dtool_Call_ExtractThisPointer(self, Dtool_" << ClassName << ", (void **)&local_this)) {\n"; + out << " if (!DtoolInstance_GetPointer(self, local_this, Dtool_" << ClassName << ")) {\n"; } error_return(out, 4, return_flags); @@ -3616,7 +3617,7 @@ write_function_for_name(ostream &out, Object *obj, if (strip_keyword_args) { // None of the remaps take any keyword arguments, so let's check that // we take none. This saves some checks later on. - indent(out, 4) << "if (kwds == NULL || ((PyDictObject *)kwds)->ma_used == 0) {\n"; + indent(out, 4) << "if (kwds == NULL || PyDict_GET_SIZE(kwds) == 0) {\n"; if (min_args == 1 && min_args == 1) { indent(out, 4) << " PyObject *arg = PyTuple_GET_ITEM(args, 0);\n"; write_function_forset(out, mii->second, min_args, max_args, expected_params, 6, @@ -3890,11 +3891,10 @@ write_coerce_constructor(ostream &out, Object *obj, bool is_const) { // Note: this relies on the PT() being initialized to NULL. This is // currently the case in all invocations, but this may not be true in the // future. - out << " DTOOL_Call_ExtractThisPointerForType(args, &Dtool_" << ClassName << ", (void**)&coerced.cheat());\n"; - out << " if (coerced != NULL) {\n"; + out << " if (DtoolInstance_GetPointer(args, coerced.cheat(), Dtool_" << ClassName << ")) {\n"; out << " // The argument is already of matching type, no need to coerce.\n"; if (!is_const) { - out << " if (!((Dtool_PyInstDef *)args)->_is_const) {\n"; + out << " if (!DtoolInstance_IS_CONST(args)) {\n"; out << " // A non-const instance is required, which this is.\n"; out << " coerced->ref();\n"; out << " return true;\n"; @@ -3909,9 +3909,8 @@ write_coerce_constructor(ostream &out, Object *obj, bool is_const) { out << cClassName << " *Dtool_Coerce_" << ClassName << "(PyObject *args, " << cClassName << " &coerced) {\n"; out << " " << cClassName << " *local_this;\n"; - out << " DTOOL_Call_ExtractThisPointerForType(args, &Dtool_" << ClassName << ", (void**)&local_this);\n"; - out << " if (local_this != NULL) {\n"; - out << " if (((Dtool_PyInstDef *)args)->_is_const) {\n"; + out << " if (DtoolInstance_GetPointer(args, local_this, Dtool_" << ClassName << ")) {\n"; + out << " if (DtoolInstance_IS_CONST(args)) {\n"; out << " // This is a const object. Make a copy.\n"; out << " coerced = *(const " << cClassName << " *)local_this;\n"; out << " return &coerced;\n"; @@ -4340,7 +4339,7 @@ write_function_forset(ostream &out, if (all_nonconst) { // Yes, they do. Check that the parameter has the required constness. indent(out, indent_level) - << "if (!((Dtool_PyInstDef *)self)->_is_const) {\n"; + << "if (!DtoolInstance_IS_CONST(self)) {\n"; indent_level += 2; verify_const = false; } @@ -4406,7 +4405,7 @@ write_function_forset(ostream &out, if (verify_const && (remap->_has_this && !remap->_const_method)) { // If it's a non-const method, we only allow a non-const this. indent(out, indent_level) - << "if (!((Dtool_PyInstDef *)self)->_is_const) {\n"; + << "if (!DtoolInstance_IS_CONST(self)) {\n"; } else { indent(out, indent_level) << "{\n"; @@ -4441,7 +4440,7 @@ write_function_forset(ostream &out, if (verify_const && (remap->_has_this && !remap->_const_method)) { indent(out, indent_level) - << "if (!((Dtool_PyInstDef *)self)->_is_const) {\n"; + << "if (!DtoolInstance_IS_CONST(self)) {\n"; } else { indent(out, indent_level) << "{\n"; @@ -5525,14 +5524,13 @@ write_function_instance(ostream &out, FunctionRemap *remap, // This function does the same thing in this case and is slightly // simpler. But maybe we should just reorganize these functions // entirely? - extra_convert << ";\n"; - if (is_optional) { - extra_convert << " "; - } - extra_convert - << "DTOOL_Call_ExtractThisPointerForType(" << param_name - << ", Dtool_Ptr_" << make_safe_name(class_name) - << ", (void **)&" << param_name << "_this);\n"; + extra_convert << " = NULL;\n"; + int indent_level = is_optional ? 2 : 0; + indent(extra_convert, indent_level) + << "DtoolInstance_GetPointer(" << param_name + << ", " << param_name << "_this" + << ", *Dtool_Ptr_" << make_safe_name(class_name) + << ");\n"; } else { extra_convert << boolalpha << " = (" << class_name << " *)" @@ -6796,7 +6794,7 @@ write_getset(ostream &out, Object *obj, Property *property) { out << " if (wrap != NULL) {\n" " wrap->_getitem_func = &Dtool_" << ClassName << "_" << ielem.get_name() << "_Mapping_Getitem;\n"; if (!property->_setter_remaps.empty()) { - out << " if (!((Dtool_PyInstDef *)self)->_is_const) {\n"; + out << " if (!DtoolInstance_IS_CONST(self)) {\n"; out << " wrap->_setitem_func = &Dtool_" << ClassName << "_" << ielem.get_name() << "_Mapping_Setitem;\n"; out << " }\n"; } @@ -6828,7 +6826,7 @@ write_getset(ostream &out, Object *obj, Property *property) { " wrap->_len_func = &Dtool_" << ClassName << "_" << ielem.get_name() << "_Len;\n" " wrap->_getitem_func = &Dtool_" << ClassName << "_" << ielem.get_name() << "_Sequence_Getitem;\n"; if (!property->_setter_remaps.empty()) { - out << " if (!((Dtool_PyInstDef *)self)->_is_const) {\n"; + out << " if (!DtoolInstance_IS_CONST(self)) {\n"; out << " wrap->_setitem_func = &Dtool_" << ClassName << "_" << ielem.get_name() << "_Sequence_Setitem;\n"; if (property->_inserter != nullptr) { out << " wrap->_insert_func = &Dtool_" << ClassName << "_" << ielem.get_name() << "_Sequence_insert;\n"; diff --git a/dtool/src/interrogatedb/py_compat.h b/dtool/src/interrogatedb/py_compat.h index 852ba95a02..46537114aa 100755 --- a/dtool/src/interrogatedb/py_compat.h +++ b/dtool/src/interrogatedb/py_compat.h @@ -155,6 +155,12 @@ extern EXPCL_INTERROGATEDB PyTupleObject Dtool_EmptyTuple; # define PyLong_AsLongLongAndOverflow(x) PyLong_AsLongAndOverflow(x) #endif +/* Python 3.7 */ + +#ifndef PyDict_GET_SIZE +# define PyDict_GET_SIZE(mp) (((PyDictObject *)mp)->ma_used) +#endif + /* Other Python implementations */ // _PyErr_OCCURRED is an undocumented macro version of PyErr_Occurred. diff --git a/dtool/src/interrogatedb/py_panda.I b/dtool/src/interrogatedb/py_panda.I index 53d59a9639..1986a128c5 100644 --- a/dtool/src/interrogatedb/py_panda.I +++ b/dtool/src/interrogatedb/py_panda.I @@ -11,20 +11,54 @@ * @date 2016-06-06 */ +#ifdef _MSC_VER +#define _IS_FINAL(T) (__is_sealed(T)) +#elif defined(__GNUC__) +#define _IS_FINAL(T) (__is_final(T)) +#else +#define _IS_FINAL(T) (0) +#endif + /** * Template function that can be used to extract any TypedObject pointer from * a wrapped Python object. */ template INLINE bool -DTOOL_Call_ExtractThisPointer(PyObject *self, T *&into) { - if (DtoolCanThisBeAPandaInstance(self)) { +DtoolInstance_GetPointer(PyObject *self, T *&into) { + if (DtoolInstance_Check(self)) { Dtool_PyTypedObject *target_class = Dtool_RuntimeTypeDtoolType(get_type_handle(T).get_index()); - if (target_class != NULL) { - into = (T*) ((Dtool_PyInstDef *)self)->_My_Type->_Dtool_UpcastInterface(self, target_class); - return (into != NULL); + if (target_class != nullptr) { + if (_IS_FINAL(T)) { + if (DtoolInstance_TYPE(self) == target_class) { + into = (T *)DtoolInstance_VOID_PTR(self); + } + } else { + into = (T *)DtoolInstance_UPCAST(self, *target_class); + } + return (into != nullptr); } } - into = NULL; + into = nullptr; + return false; +} + +/** + * Template function that can be used to extract any TypedObject pointer from + * a wrapped Python object. In this case, the Dtool_PyTypedObject is known. + */ +template INLINE bool +DtoolInstance_GetPointer(PyObject *self, T *&into, Dtool_PyTypedObject &target_class) { + if (DtoolInstance_Check(self)) { + if (_IS_FINAL(T)) { + if (DtoolInstance_TYPE(self) == &target_class) { + into = (T *)DtoolInstance_VOID_PTR(self); + } + } else { + into = (T *)DtoolInstance_UPCAST(self, target_class); + } + return (into != nullptr); + } + into = nullptr; return false; } @@ -73,7 +107,7 @@ Dtool_CheckNoArgs(PyObject *args) { ALWAYS_INLINE bool Dtool_CheckNoArgs(PyObject *args, PyObject *kwds) { return PyTuple_GET_SIZE(args) == 0 && - (kwds == NULL || ((PyDictObject *)kwds)->ma_used == 0); + (kwds == nullptr || PyDict_GET_SIZE(kwds) == 0); } /** diff --git a/dtool/src/interrogatedb/py_panda.cxx b/dtool/src/interrogatedb/py_panda.cxx index d218a6c12c..76733237a8 100644 --- a/dtool/src/interrogatedb/py_panda.cxx +++ b/dtool/src/interrogatedb/py_panda.cxx @@ -31,34 +31,14 @@ static RuntimeTypeMap runtime_type_map; static RuntimeTypeSet runtime_type_set; static NamedTypeMap named_type_map; - -/** - * Given a valid (non-NULL) PyObject, does a simple check to see if it might - * be an instance of a Panda type. It does this using a signature that is - * encoded on each instance. - */ -bool DtoolCanThisBeAPandaInstance(PyObject *self) { - // simple sanity check for the class type..size.. will stop basic foobars.. - // It is arguably better to use something like this: - // PyType_IsSubtype(Py_TYPE(self), &Dtool_DTOOL_SUPER_BASE._PyType) ...but - // probably not as fast. - if (Py_TYPE(self)->tp_basicsize >= (int)sizeof(Dtool_PyInstDef)) { - Dtool_PyInstDef *pyself = (Dtool_PyInstDef *) self; - if (pyself->_signature == PY_PANDA_SIGNATURE) { - return true; - } - } - return false; -} - /** */ void DTOOL_Call_ExtractThisPointerForType(PyObject *self, Dtool_PyTypedObject *classdef, void **answer) { - if (DtoolCanThisBeAPandaInstance(self)) { - *answer = ((Dtool_PyInstDef *)self)->_My_Type->_Dtool_UpcastInterface(self, classdef); + if (DtoolInstance_Check(self)) { + *answer = DtoolInstance_UPCAST(self, *classdef); } else { - *answer = NULL; + *answer = nullptr; } } @@ -68,12 +48,12 @@ void DTOOL_Call_ExtractThisPointerForType(PyObject *self, Dtool_PyTypedObject *c * was of the wrong type, raises an AttributeError. */ bool Dtool_Call_ExtractThisPointer(PyObject *self, Dtool_PyTypedObject &classdef, void **answer) { - if (self == NULL || !DtoolCanThisBeAPandaInstance(self) || ((Dtool_PyInstDef *)self)->_ptr_to_object == NULL) { + if (self == nullptr || !DtoolInstance_Check(self) || DtoolInstance_VOID_PTR(self) == nullptr) { Dtool_Raise_TypeError("C++ object is not yet constructed, or already destructed."); return false; } - *answer = ((Dtool_PyInstDef *)self)->_My_Type->_Dtool_UpcastInterface(self, &classdef); + *answer = DtoolInstance_UPCAST(self, classdef); return true; } @@ -88,12 +68,12 @@ bool Dtool_Call_ExtractThisPointer(PyObject *self, Dtool_PyTypedObject &classdef bool Dtool_Call_ExtractThisPointer_NonConst(PyObject *self, Dtool_PyTypedObject &classdef, void **answer, const char *method_name) { - if (self == NULL || !DtoolCanThisBeAPandaInstance(self) || ((Dtool_PyInstDef *)self)->_ptr_to_object == NULL) { + if (self == nullptr || !DtoolInstance_Check(self) || DtoolInstance_VOID_PTR(self) == nullptr) { Dtool_Raise_TypeError("C++ object is not yet constructed, or already destructed."); return false; } - if (((Dtool_PyInstDef *)self)->_is_const) { + if (DtoolInstance_IS_CONST(self)) { // All overloads of this function are non-const. PyErr_Format(PyExc_TypeError, "Cannot call %s() on a const object.", @@ -101,7 +81,7 @@ bool Dtool_Call_ExtractThisPointer_NonConst(PyObject *self, Dtool_PyTypedObject return false; } - *answer = ((Dtool_PyInstDef *)self)->_My_Type->_Dtool_UpcastInterface(self, &classdef); + *answer = DtoolInstance_UPCAST(self, classdef); return true; } @@ -131,19 +111,19 @@ void * DTOOL_Call_GetPointerThisClass(PyObject *self, Dtool_PyTypedObject *classdef, int param, const string &function_name, bool const_ok, bool report_errors) { - // if (PyErr_Occurred()) { return NULL; } - if (self == NULL) { + // if (PyErr_Occurred()) { return nullptr; } + if (self == nullptr) { if (report_errors) { - return Dtool_Raise_TypeError("self is NULL"); + return Dtool_Raise_TypeError("self is nullptr"); } - return NULL; + return nullptr; } - if (DtoolCanThisBeAPandaInstance(self)) { - void *result = ((Dtool_PyInstDef *)self)->_My_Type->_Dtool_UpcastInterface(self, classdef); + if (DtoolInstance_Check(self)) { + void *result = DtoolInstance_UPCAST(self, *classdef); - if (result != NULL) { - if (const_ok || !((Dtool_PyInstDef *)self)->_is_const) { + if (result != nullptr) { + if (const_ok || !DtoolInstance_IS_CONST(self)) { return result; } @@ -152,7 +132,7 @@ DTOOL_Call_GetPointerThisClass(PyObject *self, Dtool_PyTypedObject *classdef, "%s() argument %d may not be const", function_name.c_str(), param); } - return NULL; + return nullptr; } } @@ -160,17 +140,14 @@ DTOOL_Call_GetPointerThisClass(PyObject *self, Dtool_PyTypedObject *classdef, return Dtool_Raise_ArgTypeError(self, param, function_name.c_str(), classdef->_PyType.tp_name); } - return NULL; + return nullptr; } void *DTOOL_Call_GetPointerThis(PyObject *self) { - if (self != NULL) { - if (DtoolCanThisBeAPandaInstance(self)) { - Dtool_PyInstDef * pyself = (Dtool_PyInstDef *) self; - return pyself->_ptr_to_object; - } + if (self != nullptr && DtoolInstance_Check(self)) { + return DtoolInstance_VOID_PTR(self); } - return NULL; + return nullptr; } /** @@ -713,7 +690,7 @@ PyObject *Dtool_BorrowThisReference(PyObject *self, PyObject *args) { PyObject *to_in = NULL; if (PyArg_UnpackTuple(args, "Dtool_BorrowThisReference", 2, 2, &to_in, &from_in)) { - if (DtoolCanThisBeAPandaInstance(from_in) && DtoolCanThisBeAPandaInstance(to_in)) { + if (DtoolInstance_Check(from_in) && DtoolInstance_Check(to_in)) { Dtool_PyInstDef *from = (Dtool_PyInstDef *) from_in; Dtool_PyInstDef *to = (Dtool_PyInstDef *) to_in; @@ -758,9 +735,8 @@ PyObject *Dtool_AddToDictionary(PyObject *self1, PyObject *args) { } Py_hash_t DTOOL_PyObject_HashPointer(PyObject *self) { - if (self != NULL && DtoolCanThisBeAPandaInstance(self)) { - Dtool_PyInstDef * pyself = (Dtool_PyInstDef *) self; - return (Py_hash_t) pyself->_ptr_to_object; + if (self != nullptr && DtoolInstance_Check(self)) { + return (Py_hash_t)DtoolInstance_VOID_PTR(self); } return -1; } @@ -933,14 +909,14 @@ bool Dtool_ExtractArg(PyObject **result, PyObject *args, PyObject *kwds, const char *keyword) { if (PyTuple_GET_SIZE(args) == 1) { - if (kwds == NULL || ((PyDictObject *)kwds)->ma_used == 0) { + if (kwds == nullptr || PyDict_GET_SIZE(kwds) == 0) { *result = PyTuple_GET_ITEM(args, 0); return true; } } else if (PyTuple_GET_SIZE(args) == 0) { PyObject *key; Py_ssize_t ppos = 0; - if (kwds != NULL && ((PyDictObject *)kwds)->ma_used == 1 && + if (kwds != nullptr && PyDict_GET_SIZE(kwds) == 1 && PyDict_Next(kwds, &ppos, &key, result)) { // We got the item, we just need to make sure that it had the right key. #if PY_VERSION_HEX >= 0x03060000 @@ -961,7 +937,7 @@ bool Dtool_ExtractArg(PyObject **result, PyObject *args, PyObject *kwds, */ bool Dtool_ExtractArg(PyObject **result, PyObject *args, PyObject *kwds) { if (PyTuple_GET_SIZE(args) == 1 && - (kwds == NULL || ((PyDictObject *)kwds)->ma_used == 0)) { + (kwds == nullptr || PyDict_GET_SIZE(kwds) == 0)) { *result = PyTuple_GET_ITEM(args, 0); return true; } @@ -979,12 +955,12 @@ bool Dtool_ExtractOptionalArg(PyObject **result, PyObject *args, PyObject *kwds, const char *keyword) { if (PyTuple_GET_SIZE(args) == 1) { - if (kwds == NULL || ((PyDictObject *)kwds)->ma_used == 0) { + if (kwds == nullptr || PyDict_GET_SIZE(kwds) == 0) { *result = PyTuple_GET_ITEM(args, 0); return true; } } else if (PyTuple_GET_SIZE(args) == 0) { - if (kwds != NULL && ((PyDictObject *)kwds)->ma_used == 1) { + if (kwds != nullptr && PyDict_GET_SIZE(kwds) == 1) { PyObject *key; Py_ssize_t ppos = 0; if (!PyDict_Next(kwds, &ppos, &key, result)) { @@ -1011,7 +987,7 @@ bool Dtool_ExtractOptionalArg(PyObject **result, PyObject *args, PyObject *kwds, * Variant of Dtool_ExtractOptionalArg that does not accept a keyword argument. */ bool Dtool_ExtractOptionalArg(PyObject **result, PyObject *args, PyObject *kwds) { - if (kwds != NULL && ((PyDictObject *)kwds)->ma_used != 0) { + if (kwds != nullptr && PyDict_GET_SIZE(kwds) != 0) { return false; } if (PyTuple_GET_SIZE(args) == 1) { diff --git a/dtool/src/interrogatedb/py_panda.h b/dtool/src/interrogatedb/py_panda.h index fad7ccedb0..74f13680f8 100644 --- a/dtool/src/interrogatedb/py_panda.h +++ b/dtool/src/interrogatedb/py_panda.h @@ -127,7 +127,7 @@ static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\ #else // NDEBUG #define Define_Dtool_FreeInstance_Private(CLASS_NAME,CNAME)\ static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\ - if (((Dtool_PyInstDef *)self)->_ptr_to_object != NULL) {\ + if (DtoolInstance_VOID_PTR(self) != nullptr) {\ if (((Dtool_PyInstDef *)self)->_memory_rules) {\ cerr << "Detected leak for " << #CLASS_NAME \ << " which interrogate cannot delete.\n"; \ @@ -139,9 +139,9 @@ static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\ #define Define_Dtool_FreeInstance(CLASS_NAME,CNAME)\ static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\ - if (((Dtool_PyInstDef *)self)->_ptr_to_object != NULL) {\ + if (DtoolInstance_VOID_PTR(self) != nullptr) {\ if (((Dtool_PyInstDef *)self)->_memory_rules) {\ - delete ((CNAME *)((Dtool_PyInstDef *)self)->_ptr_to_object);\ + delete (CNAME *)DtoolInstance_VOID_PTR(self);\ }\ }\ Py_TYPE(self)->tp_free(self);\ @@ -149,9 +149,9 @@ static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\ #define Define_Dtool_FreeInstanceRef(CLASS_NAME,CNAME)\ static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\ - if (((Dtool_PyInstDef *)self)->_ptr_to_object != NULL) {\ + if (DtoolInstance_VOID_PTR(self) != nullptr) {\ if (((Dtool_PyInstDef *)self)->_memory_rules) {\ - unref_delete((CNAME *)((Dtool_PyInstDef *)self)->_ptr_to_object);\ + unref_delete((CNAME *)DtoolInstance_VOID_PTR(self));\ }\ }\ Py_TYPE(self)->tp_free(self);\ @@ -163,8 +163,17 @@ static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\ Py_TYPE(self)->tp_free(self);\ } -// Simple Recognition Functions.. -EXPCL_INTERROGATEDB bool DtoolCanThisBeAPandaInstance(PyObject *self); +// Use DtoolInstance_Check to check whether a PyObject* is a DtoolInstance. +#define DtoolInstance_Check(obj) \ + (Py_TYPE(obj)->tp_basicsize >= (int)sizeof(Dtool_PyInstDef) && \ + ((Dtool_PyInstDef *)obj)->_signature == PY_PANDA_SIGNATURE) + +// These macros access the DtoolInstance without error checking. +#define DtoolInstance_TYPE(obj) (((Dtool_PyInstDef *)obj)->_My_Type) +#define DtoolInstance_IS_CONST(obj) (((Dtool_PyInstDef *)obj)->_is_const) +#define DtoolInstance_VOID_PTR(obj) (((Dtool_PyInstDef *)obj)->_ptr_to_object) +#define DtoolInstance_INIT_PTR(obj, ptr) { ((Dtool_PyInstDef *)obj)->_ptr_to_object = (void*)(ptr); } +#define DtoolInstance_UPCAST(obj, type) (((Dtool_PyInstDef *)(obj))->_My_Type->_Dtool_UpcastInterface((obj), &(type))) // ** HACK ** allert.. Need to keep a runtime type dictionary ... that is // forward declared of typed object. We rely on the fact that typed objects @@ -192,20 +201,14 @@ EXPCL_INTERROGATEDB bool Dtool_Call_ExtractThisPointer(PyObject *self, Dtool_PyT EXPCL_INTERROGATEDB bool Dtool_Call_ExtractThisPointer_NonConst(PyObject *self, Dtool_PyTypedObject &classdef, void **answer, const char *method_name); -template INLINE bool DTOOL_Call_ExtractThisPointer(PyObject *self, T *&into); +template INLINE bool DtoolInstance_GetPointer(PyObject *self, T *&into); +template INLINE bool DtoolInstance_GetPointer(PyObject *self, T *&into, Dtool_PyTypedObject &classdef); // Functions related to error reporting. EXPCL_INTERROGATEDB bool _Dtool_CheckErrorOccurred(); -// _PyErr_OCCURRED is an undocumented macro version of PyErr_Occurred. -// Some implementations of the CPython API (e.g. PyPy's cpyext) do not define -// it, so in these cases we just silently fall back to PyErr_Occurred. -#ifndef _PyErr_OCCURRED -#define _PyErr_OCCURRED() PyErr_Occurred() -#endif - #ifdef NDEBUG -#define Dtool_CheckErrorOccurred() (UNLIKELY(_PyErr_OCCURRED() != NULL)) +#define Dtool_CheckErrorOccurred() (UNLIKELY(_PyErr_OCCURRED() != nullptr)) #else #define Dtool_CheckErrorOccurred() (UNLIKELY(_Dtool_CheckErrorOccurred())) #endif @@ -232,8 +235,8 @@ EXPCL_INTERROGATEDB PyObject *Dtool_Return_Bool(bool value); EXPCL_INTERROGATEDB PyObject *_Dtool_Return(PyObject *value); #ifdef NDEBUG -#define Dtool_Return_None() (_PyErr_OCCURRED() != NULL ? NULL : (Py_INCREF(Py_None), Py_None)) -#define Dtool_Return(value) (_PyErr_OCCURRED() != NULL ? NULL : value) +#define Dtool_Return_None() (LIKELY(_PyErr_OCCURRED() == nullptr) ? (Py_INCREF(Py_None), Py_None) : nullptr) +#define Dtool_Return(value) (LIKELY(_PyErr_OCCURRED() == nullptr) ? value : nullptr) #else #define Dtool_Return_None() _Dtool_Return_None() #define Dtool_Return(value) _Dtool_Return(value) diff --git a/panda/src/event/pythonTask.cxx b/panda/src/event/pythonTask.cxx index 31647540f2..bda2706407 100644 --- a/panda/src/event/pythonTask.cxx +++ b/panda/src/event/pythonTask.cxx @@ -583,12 +583,11 @@ do_python_task() { return DS_done; } - } else if (DtoolCanThisBeAPandaInstance(result)) { + } else if (DtoolInstance_Check(result)) { // We are waiting for an AsyncFuture (eg. other task) to finish. - void *ptr = ((Dtool_PyInstDef *)result)->_My_Type->_Dtool_UpcastInterface(result, &Dtool_AsyncFuture); - if (ptr != nullptr) { + AsyncFuture *fut = (AsyncFuture *)DtoolInstance_UPCAST(result, Dtool_AsyncFuture); + if (fut != nullptr) { // Suspend execution of this task until this other task has completed. - AsyncFuture *fut = (AsyncFuture *)ptr; AsyncTaskManager *manager = fut->_manager; if (manager == nullptr) { manager = _manager; diff --git a/panda/src/express/pointerToArray_ext.I b/panda/src/express/pointerToArray_ext.I index 6b8a9d54d0..e2356c7a4b 100644 --- a/panda/src/express/pointerToArray_ext.I +++ b/panda/src/express/pointerToArray_ext.I @@ -97,7 +97,7 @@ __init__(PyObject *self, PyObject *source) { // Now construct the internal list by copying the elements one-at-a-time // from Python. - PyObject *dict = ((Dtool_PyInstDef *)self)->_My_Type->_PyType.tp_dict; + PyObject *dict = DtoolInstance_TYPE(self)->_PyType.tp_dict; PyObject *push_back = PyDict_GetItemString(dict, "push_back"); if (push_back == NULL) { PyErr_BadArgument(); @@ -105,7 +105,7 @@ __init__(PyObject *self, PyObject *source) { } // We need to initialize the this pointer before we can call push_back. - ((Dtool_PyInstDef *)self)->_ptr_to_object = (void *)this->_this; + DtoolInstance_INIT_PTR(self, this->_this); Py_ssize_t size = PySequence_Size(source); this->_this->reserve(size); diff --git a/panda/src/gobj/texture.h b/panda/src/gobj/texture.h index 6974b1c1fc..6b649b9ba5 100644 --- a/panda/src/gobj/texture.h +++ b/panda/src/gobj/texture.h @@ -43,10 +43,10 @@ #include "colorSpace.h" #include "geomEnums.h" #include "bamCacheRecord.h" +#include "pnmImage.h" +#include "pfmFile.h" #include "asyncFuture.h" -class PNMImage; -class PfmFile; class TextureContext; class FactoryParams; class PreparedGraphicsObjects; diff --git a/panda/src/gobj/texture_ext.cxx b/panda/src/gobj/texture_ext.cxx index 507fa41fc4..260070f058 100644 --- a/panda/src/gobj/texture_ext.cxx +++ b/panda/src/gobj/texture_ext.cxx @@ -33,15 +33,12 @@ set_ram_image(PyObject *image, Texture::CompressionMode compression, nassertv(compression != Texture::CM_default); // Check if perhaps a PointerToArray object was passed in. - if (DtoolCanThisBeAPandaInstance(image)) { - Dtool_PyInstDef *inst = (Dtool_PyInstDef *)image; - - if (inst->_My_Type == &Dtool_ConstPointerToArray_unsigned_char) { - _this->set_ram_image(*(const CPTA_uchar *)inst->_ptr_to_object, compression, page_size); + if (DtoolInstance_Check(image)) { + if (DtoolInstance_TYPE(image) == &Dtool_ConstPointerToArray_unsigned_char) { + _this->set_ram_image(*(const CPTA_uchar *)DtoolInstance_VOID_PTR(image), compression, page_size); return; - - } else if (inst->_My_Type == &Dtool_PointerToArray_unsigned_char) { - _this->set_ram_image(*(const PTA_uchar *)inst->_ptr_to_object, compression, page_size); + } else if (DtoolInstance_TYPE(image) == &Dtool_PointerToArray_unsigned_char) { + _this->set_ram_image(*(const PTA_uchar *)DtoolInstance_VOID_PTR(image), compression, page_size); return; } } @@ -99,15 +96,12 @@ set_ram_image(PyObject *image, Texture::CompressionMode compression, void Extension:: set_ram_image_as(PyObject *image, const string &provided_format) { // Check if perhaps a PointerToArray object was passed in. - if (DtoolCanThisBeAPandaInstance(image)) { - Dtool_PyInstDef *inst = (Dtool_PyInstDef *)image; - - if (inst->_My_Type == &Dtool_ConstPointerToArray_unsigned_char) { - _this->set_ram_image_as(*(const CPTA_uchar *)inst->_ptr_to_object, provided_format); + if (DtoolInstance_Check(image)) { + if (DtoolInstance_TYPE(image) == &Dtool_ConstPointerToArray_unsigned_char) { + _this->set_ram_image_as(*(const CPTA_uchar *)DtoolInstance_VOID_PTR(image), provided_format); return; - - } else if (inst->_My_Type == &Dtool_PointerToArray_unsigned_char) { - _this->set_ram_image_as(*(const PTA_uchar *)inst->_ptr_to_object, provided_format); + } else if (DtoolInstance_TYPE(image) == &Dtool_PointerToArray_unsigned_char) { + _this->set_ram_image_as(*(const PTA_uchar *)DtoolInstance_VOID_PTR(image), provided_format); return; } } diff --git a/panda/src/pgraph/nodePathCollection_ext.cxx b/panda/src/pgraph/nodePathCollection_ext.cxx index c6f17b7aa4..32f5ac0d13 100644 --- a/panda/src/pgraph/nodePathCollection_ext.cxx +++ b/panda/src/pgraph/nodePathCollection_ext.cxx @@ -46,8 +46,7 @@ __init__(PyObject *self, PyObject *sequence) { } NodePath *path; - DTOOL_Call_ExtractThisPointerForType(item, &Dtool_NodePath, (void **)&path); - if (path == (NodePath *)NULL) { + if (!DtoolInstance_GetPointer(item, path, Dtool_NodePath)) { // Unable to add item--probably it wasn't of the appropriate type. ostringstream stream; stream << "Element " << i << " in sequence passed to NodePathCollection constructor is not a NodePath"; diff --git a/panda/src/pgraph/nodePath_ext.cxx b/panda/src/pgraph/nodePath_ext.cxx index 0172fe0c29..1c7576290a 100644 --- a/panda/src/pgraph/nodePath_ext.cxx +++ b/panda/src/pgraph/nodePath_ext.cxx @@ -114,7 +114,7 @@ __reduce_persist__(PyObject *self, PyObject *pickler) const { // It's OK if there's no bamWriter. PyErr_Clear(); } else { - DTOOL_Call_ExtractThisPointerForType(py_writer, &Dtool_BamWriter, (void **)&writer); + DtoolInstance_GetPointer(py_writer, writer, Dtool_BamWriter); Py_DECREF(py_writer); } } @@ -228,7 +228,7 @@ py_decode_NodePath_from_bam_stream_persist(PyObject *unpickler, vector_uchar dat // It's OK if there's no bamReader. PyErr_Clear(); } else { - DTOOL_Call_ExtractThisPointerForType(py_reader, &Dtool_BamReader, (void **)&reader); + DtoolInstance_GetPointer(py_reader, reader, Dtool_BamReader); Py_DECREF(py_reader); } } diff --git a/panda/src/pgraph/shaderInput_ext.cxx b/panda/src/pgraph/shaderInput_ext.cxx index 00f42d7d53..d09a971b46 100644 --- a/panda/src/pgraph/shaderInput_ext.cxx +++ b/panda/src/pgraph/shaderInput_ext.cxx @@ -91,80 +91,79 @@ __init__(CPT_InternalName name, PyObject *value, int priority) { _this->_stored_vector = LCAST(PN_stdfloat, vec); } - } else if (DtoolCanThisBeAPandaInstance(value)) { - Dtool_PyInstDef *inst = (Dtool_PyInstDef *)value; + } else if (DtoolInstance_Check(value)) { void *ptr; - if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_Texture))) { + if ((ptr = DtoolInstance_UPCAST(value, Dtool_Texture))) { _this->_type = ShaderInput::M_texture; _this->_value = (Texture *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_NodePath))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_NodePath))) { _this->_type = ShaderInput::M_nodepath; _this->_value = new ParamNodePath(*(const NodePath *)ptr); - } else if (inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToVoid)) { - if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_float))) { + } else if (DtoolInstance_UPCAST(value, Dtool_PointerToVoid)) { + if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_float))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_float *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_double))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_double))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_double *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_int))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_int))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_int *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_UnalignedLVecBase4f))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_UnalignedLVecBase4f))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_LVecBase4f *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_LVecBase3f))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_LVecBase3f))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_LVecBase3f *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_LVecBase2f))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_LVecBase2f))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_LVecBase2f *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_UnalignedLMatrix4f))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_UnalignedLMatrix4f))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_LMatrix4f *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_LMatrix3f))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_LMatrix3f))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_LMatrix3f *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_UnalignedLVecBase4d))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_UnalignedLVecBase4d))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_LVecBase4d *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_LVecBase3d))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_LVecBase3d))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_LVecBase3d *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_LVecBase2d))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_LVecBase2d))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_LVecBase2d *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_UnalignedLMatrix4d))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_UnalignedLMatrix4d))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_LMatrix4d *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_LMatrix3d))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_LMatrix3d))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_LMatrix3d *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_UnalignedLVecBase4i))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_UnalignedLVecBase4i))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_LVecBase4i *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_LVecBase3i))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_LVecBase3i))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_LVecBase3i *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_PointerToArray_LVecBase2i))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_PointerToArray_LVecBase2i))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const PTA_LVecBase2i *)ptr; @@ -173,81 +172,81 @@ __init__(CPT_InternalName name, PyObject *value, int priority) { return; } - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_LMatrix4f))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_LMatrix4f))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const LMatrix4f *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_LMatrix3f))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_LMatrix3f))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const LMatrix3f *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_LMatrix4d))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_LMatrix4d))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const LMatrix4d *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_LMatrix3d))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_LMatrix3d))) { _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = *(const LMatrix3d *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_LVecBase4f))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_LVecBase4f))) { const LVecBase4f &vec = *(const LVecBase4f *)ptr; _this->_type = ShaderInput::M_vector; _this->_stored_ptr = vec; _this->_stored_vector = LCAST(PN_stdfloat, vec); - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_LVecBase3f))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_LVecBase3f))) { const LVecBase3f &vec = *(const LVecBase3f *)ptr; _this->_type = ShaderInput::M_vector; _this->_stored_ptr = vec; _this->_stored_vector.set(vec[0], vec[1], vec[2], 0); - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_LVecBase2f))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_LVecBase2f))) { const LVecBase2f &vec = *(const LVecBase2f *)ptr; _this->_type = ShaderInput::M_vector; _this->_stored_ptr = vec; _this->_stored_vector.set(vec[0], vec[1], 0, 0); - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_LVecBase4d))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_LVecBase4d))) { const LVecBase4d &vec = *(const LVecBase4d *)ptr; _this->_type = ShaderInput::M_vector; _this->_stored_ptr = vec; _this->_stored_vector = LCAST(PN_stdfloat, vec); - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_LVecBase3d))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_LVecBase3d))) { const LVecBase3d &vec = *(const LVecBase3d *)ptr; _this->_type = ShaderInput::M_vector; _this->_stored_ptr = vec; _this->_stored_vector.set(vec[0], vec[1], vec[2], 0); - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_LVecBase2d))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_LVecBase2d))) { const LVecBase2d &vec = *(const LVecBase2d *)ptr; _this->_type = ShaderInput::M_vector; _this->_stored_ptr = vec; _this->_stored_vector.set(vec[0], vec[1], 0, 0); - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_LVecBase4i))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_LVecBase4i))) { const LVecBase4i &vec = *(const LVecBase4i *)ptr; _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = vec; _this->_stored_vector = LCAST(PN_stdfloat, vec); - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_LVecBase3i))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_LVecBase3i))) { const LVecBase3i &vec = *(const LVecBase3i *)ptr; _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = vec; _this->_stored_vector.set(vec[0], vec[1], vec[2], 0); - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_LVecBase2i))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_LVecBase2i))) { const LVecBase2i &vec = *(const LVecBase2i *)ptr; _this->_type = ShaderInput::M_numeric; _this->_stored_ptr = vec; _this->_stored_vector.set(vec[0], vec[1], 0, 0); - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_ShaderBuffer))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_ShaderBuffer))) { _this->_type = ShaderInput::M_buffer; _this->_value = (ShaderBuffer *)ptr; - } else if ((ptr = inst->_My_Type->_Dtool_UpcastInterface(value, &Dtool_ParamValueBase))) { + } else if ((ptr = DtoolInstance_UPCAST(value, Dtool_ParamValueBase))) { _this->_type = ShaderInput::M_param; _this->_value = (ParamValueBase *)ptr; diff --git a/panda/src/putil/typedWritable_ext.cxx b/panda/src/putil/typedWritable_ext.cxx index 6cc04c54d7..efaaac63fb 100644 --- a/panda/src/putil/typedWritable_ext.cxx +++ b/panda/src/putil/typedWritable_ext.cxx @@ -15,6 +15,8 @@ #ifdef HAVE_PYTHON +#include "bamWriter.h" + #ifndef CPPPARSER extern Dtool_PyTypedObject Dtool_BamWriter; #endif // CPPPARSER @@ -65,7 +67,7 @@ __reduce_persist__(PyObject *self, PyObject *pickler) const { // It's OK if there's no bamWriter. PyErr_Clear(); } else { - DTOOL_Call_ExtractThisPointerForType(py_writer, &Dtool_BamWriter, (void **)&writer); + DtoolInstance_GetPointer(py_writer, writer, Dtool_BamWriter); Py_DECREF(py_writer); } } From 9bfc425b75c013a656e158b2ee032c51efb30451 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 12 Dec 2017 23:27:51 +0100 Subject: [PATCH 10/25] Fix assertion when rendering bounding volumes (broken since 6f8b379) --- panda/src/pgraph/cullResult.cxx | 2 ++ panda/src/pgraph/cullTraverser.cxx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/panda/src/pgraph/cullResult.cxx b/panda/src/pgraph/cullResult.cxx index 53751f07ca..0118888108 100644 --- a/panda/src/pgraph/cullResult.cxx +++ b/panda/src/pgraph/cullResult.cxx @@ -105,6 +105,8 @@ add_object(CullableObject *object, const CullTraverser *traverser) { static const LColor flash_multisample_color(0.78f, 0.05f, 0.81f, 1.0f); static const LColor flash_dual_color(0.92, 0.01f, 0.01f, 1.0f); + nassertv(object->_draw_callback != nullptr || object->_geom != nullptr); + bool force = !traverser->get_effective_incomplete_render(); Thread *current_thread = traverser->get_current_thread(); CullBinManager *bin_manager = CullBinManager::get_global_ptr(); diff --git a/panda/src/pgraph/cullTraverser.cxx b/panda/src/pgraph/cullTraverser.cxx index a796718828..7ed990637b 100644 --- a/panda/src/pgraph/cullTraverser.cxx +++ b/panda/src/pgraph/cullTraverser.cxx @@ -232,7 +232,7 @@ draw_bounding_volume(const BoundingVolume *vol, if (bounds_viz != (Geom *)NULL) { _geoms_pcollector.add_level(2); CullableObject *outer_viz = - new CullableObject(move(bounds_viz), get_bounds_outer_viz_state(), + new CullableObject(bounds_viz, get_bounds_outer_viz_state(), internal_transform); _cull_handler->record_object(outer_viz, this); From e4a817b46921a7d35329e4889e7731025d731c2f Mon Sep 17 00:00:00 2001 From: deflected Date: Wed, 13 Dec 2017 13:40:02 +0200 Subject: [PATCH 11/25] Loader: Fixed crash when canceling async loading of model - Clean up the _loader after we clean up the requests, not before that Signed-off-by: deflected --- direct/src/showbase/Loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/direct/src/showbase/Loader.py b/direct/src/showbase/Loader.py index 258cbf0019..17c9cea25e 100644 --- a/direct/src/showbase/Loader.py +++ b/direct/src/showbase/Loader.py @@ -50,10 +50,10 @@ class Loader(DirectObject): 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._loader = None self.requests = None self.requestList = None From 29f50be15e0b3f542809296ad598876179b13dfa Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 16 Dec 2017 21:01:43 +0100 Subject: [PATCH 12/25] Fix bug: copy-to-ram depth texture binding gets format set to RGB This was a regression introduced by be8f4de33789cbb96551bb34b1ba85e7134d1e73 Fixes: #212 --- panda/src/display/graphicsOutput.cxx | 15 ++++++++++++--- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 12 ++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/panda/src/display/graphicsOutput.cxx b/panda/src/display/graphicsOutput.cxx index 46b051cde0..4d8a1e27a1 100644 --- a/panda/src/display/graphicsOutput.cxx +++ b/panda/src/display/graphicsOutput.cxx @@ -275,12 +275,21 @@ add_render_texture(Texture *tex, RenderTextureMode mode, // Choose a default bitplane. if (plane == RTP_COUNT) { - if (tex->get_format() == Texture::F_depth_stencil) { + switch (tex->get_format()) { + case Texture::F_depth_stencil: plane = RTP_depth_stencil; - } else if (tex->get_format() == Texture::F_depth_component) { + break; + + case Texture::F_depth_component: + case Texture::F_depth_component16: + case Texture::F_depth_component24: + case Texture::F_depth_component32: plane = RTP_depth; - } else { + break; + + default: plane = RTP_color; + break; } } diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 45450ae98d..dce9cc7d89 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -6455,6 +6455,18 @@ framebuffer_copy_to_ram(Texture *tex, int view, int z, } break; + case Texture::F_depth_component16: + component_type = Texture::T_unsigned_short; + break; + + case Texture::F_depth_component24: + component_type = Texture::T_unsigned_int; + break; + + case Texture::F_depth_component32: + component_type = Texture::T_float; + break; + default: if (_current_properties->get_srgb_color()) { if (_current_properties->get_alpha_bits()) { From 9e8060d402e8053eabc985168665c0e25ba8b0b8 Mon Sep 17 00:00:00 2001 From: sean5470 Date: Sat, 16 Dec 2017 14:33:14 -0500 Subject: [PATCH 13/25] Moved MAX plugin line to MAX instead of Maya section Moved the search line for .ms files from the Maya section, to the MAX section where it should be. Closes: #214 --- makepanda/installer.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makepanda/installer.nsi b/makepanda/installer.nsi index c5e76fc766..bd139f7083 100755 --- a/makepanda/installer.nsi +++ b/makepanda/installer.nsi @@ -588,6 +588,7 @@ Section "3ds Max plug-ins" SecMaxPlugins SetOutPath $INSTDIR\plugins File /nonfatal /r "${BUILT}\plugins\*.dle" File /nonfatal /r "${BUILT}\plugins\*.dlo" + File /nonfatal /r "${BUILT}\plugins\*.ms" File "${SOURCE}\doc\INSTALLING-PLUGINS.TXT" SectionEnd !endif @@ -603,7 +604,6 @@ Section "Maya plug-ins" SecMayaPlugins SetOutPath $INSTDIR\plugins File /nonfatal /r "${BUILT}\plugins\*.mll" File /nonfatal /r "${BUILT}\plugins\*.mel" - File /nonfatal /r "${BUILT}\plugins\*.ms" File "${SOURCE}\doc\INSTALLING-PLUGINS.TXT" SectionEnd !endif From 348b1c344d9029a52682aa9a04e350ba6596820c Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 19 Dec 2017 15:15:28 +0100 Subject: [PATCH 14/25] texture: support uint/float/half/sRGB textures in TexturePeeker --- panda/src/gobj/texture.I | 55 ++++++++++++++++++ panda/src/gobj/texture.h | 3 + panda/src/gobj/texturePeeker.cxx | 57 ++++++++++++++++++- panda/src/gobj/texturePeeker.h | 2 + tests/gobj/test_texture_peek.py | 96 ++++++++++++++++++++++++++++++++ 5 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 tests/gobj/test_texture_peek.py diff --git a/panda/src/gobj/texture.I b/panda/src/gobj/texture.I index 29b1702280..841c8aa9bf 100644 --- a/panda/src/gobj/texture.I +++ b/panda/src/gobj/texture.I @@ -2324,6 +2324,61 @@ get_unsigned_short(const unsigned char *&p) { return (double)v.us / 65535.0; } +/** + * This is used by store() to retrieve the next consecutive component value + * from the indicated element of the array, which is taken to be an array of + * unsigned ints. + */ +INLINE double Texture:: +get_unsigned_int(const unsigned char *&p) { + union { + unsigned int ui; + uchar uc[4]; + } v; + v.uc[0] = (*p++); + v.uc[1] = (*p++); + v.uc[2] = (*p++); + v.uc[3] = (*p++); + return (double)v.ui / 4294967295.0; +} + +/** + * This is used by store() to retrieve the next consecutive component value + * from the indicated element of the array, which is taken to be an array of + * floats. + */ +INLINE double Texture:: +get_float(const unsigned char *&p) { + double v = *((float *)p); + p += 4; + return v; +} + +/** + * This is used by store() to retrieve the next consecutive component value + * from the indicated element of the array, which is taken to be an array of + * half-floats. + */ +INLINE double Texture:: +get_half_float(const unsigned char *&p) { + union { + uint32_t ui; + float uf; + } v; + uint16_t in = *(uint16_t *)p; + p += 2; + uint32_t t1 = in & 0x7fff; // Non-sign bits + uint32_t t2 = in & 0x8000; // Sign bit + uint32_t t3 = in & 0x7c00; // Exponent + t1 <<= 13; // Align mantissa on MSB + t2 <<= 16; // Shift sign bit into position + t1 += 0x38000000; // Adjust bias + t1 = (t3 == 0 ? 0 : t1); // Denormals-as-zero + t1 |= t2; // Re-insert sign bit + v.ui = t1; + return v.uf; +} + /** * Returns true if the indicated filename ends in .txo or .txo.pz or .txo.gz, * false otherwise. diff --git a/panda/src/gobj/texture.h b/panda/src/gobj/texture.h index 6b649b9ba5..60c16b3675 100644 --- a/panda/src/gobj/texture.h +++ b/panda/src/gobj/texture.h @@ -856,6 +856,9 @@ private: INLINE static void store_scaled_short(unsigned char *&p, int value, double scale); INLINE static double get_unsigned_byte(const unsigned char *&p); INLINE static double get_unsigned_short(const unsigned char *&p); + INLINE static double get_unsigned_int(const unsigned char *&p); + INLINE static double get_float(const unsigned char *&p); + INLINE static double get_half_float(const unsigned char *&p); INLINE static bool is_txo_filename(const Filename &fullpath); INLINE static bool is_dds_filename(const Filename &fullpath); diff --git a/panda/src/gobj/texturePeeker.cxx b/panda/src/gobj/texturePeeker.cxx index 42e619e501..b4e1be9224 100644 --- a/panda/src/gobj/texturePeeker.cxx +++ b/panda/src/gobj/texturePeeker.cxx @@ -82,6 +82,18 @@ TexturePeeker(Texture *tex, Texture::CData *cdata) { _get_component = Texture::get_unsigned_short; break; + case Texture::T_unsigned_int: + _get_component = Texture::get_unsigned_int; + break; + + case Texture::T_float: + _get_component = Texture::get_float; + break; + + case Texture::T_half_float: + _get_component = Texture::get_half_float; + break; + default: // Not supported. _image.clear(); @@ -123,7 +135,6 @@ TexturePeeker(Texture *tex, Texture::CData *cdata) { break; case Texture::F_rgb: - case Texture::F_srgb: case Texture::F_rgb5: case Texture::F_rgb8: case Texture::F_rgb12: @@ -135,7 +146,6 @@ TexturePeeker(Texture *tex, Texture::CData *cdata) { break; case Texture::F_rgba: - case Texture::F_srgb_alpha: case Texture::F_rgbm: case Texture::F_rgba4: case Texture::F_rgba5: @@ -146,6 +156,25 @@ TexturePeeker(Texture *tex, Texture::CData *cdata) { case Texture::F_rgb10_a2: _get_texel = get_texel_rgba; break; + + case Texture::F_srgb: + if (_component_type == Texture::T_unsigned_byte) { + _get_texel = get_texel_srgb; + } else { + gobj_cat.error() + << "sRGB texture should have component type T_unsigned_byte\n"; + } + break; + + case Texture::F_srgb_alpha: + if (_component_type == Texture::T_unsigned_byte) { + _get_texel = get_texel_srgba; + } else { + gobj_cat.error() + << "sRGB texture should have component type T_unsigned_byte\n"; + } + break; + default: // Not supported. gobj_cat.error() << "Unsupported texture peeker format: " @@ -570,3 +599,27 @@ get_texel_rgba(LColor &color, const unsigned char *&p, GetComponentFunc *get_com color[0] = (*get_component)(p); color[3] = (*get_component)(p); } + +/** + * Gets the color of the texel at byte p, given that the texture is in format + * F_srgb or similar. + */ +void TexturePeeker:: +get_texel_srgb(LColor &color, const unsigned char *&p, GetComponentFunc *get_component) { + color[2] = decode_sRGB_float(*p++); + color[1] = decode_sRGB_float(*p++); + color[0] = decode_sRGB_float(*p++); + color[3] = 1.0f; +} + +/** + * Gets the color of the texel at byte p, given that the texture is in format + * F_srgb_alpha or similar. + */ +void TexturePeeker:: +get_texel_srgba(LColor &color, const unsigned char *&p, GetComponentFunc *get_component) { + color[2] = decode_sRGB_float(*p++); + color[1] = decode_sRGB_float(*p++); + color[0] = decode_sRGB_float(*p++); + color[3] = (*get_component)(p); +} diff --git a/panda/src/gobj/texturePeeker.h b/panda/src/gobj/texturePeeker.h index cb503bf041..ffe3aba2b1 100644 --- a/panda/src/gobj/texturePeeker.h +++ b/panda/src/gobj/texturePeeker.h @@ -79,6 +79,8 @@ private: static void get_texel_la(LColor &color, const unsigned char *&p, GetComponentFunc *get_component); static void get_texel_rgb(LColor &color, const unsigned char *&p, GetComponentFunc *get_component); static void get_texel_rgba(LColor &color, const unsigned char *&p, GetComponentFunc *get_component); + static void get_texel_srgb(LColor &color, const unsigned char *&p, GetComponentFunc *get_component); + static void get_texel_srgba(LColor &color, const unsigned char *&p, GetComponentFunc *get_component); int _x_size; int _y_size; diff --git a/tests/gobj/test_texture_peek.py b/tests/gobj/test_texture_peek.py new file mode 100644 index 0000000000..accd1b02d0 --- /dev/null +++ b/tests/gobj/test_texture_peek.py @@ -0,0 +1,96 @@ +from panda3d.core import Texture, LColor +from array import array + + +def peeker_from_pixel(component_type, format, data): + """ Creates a 1-pixel texture with the given settings and pixel data, + then returns a TexturePeeker as result of calling texture.peek(). """ + + tex = Texture("") + tex.setup_1d_texture(1, component_type, format) + tex.set_ram_image(data) + peeker = tex.peek() + assert peeker.has_pixel(0, 0) + return peeker + + +def test_texture_peek_ubyte(): + maxval = 255 + data = array('B', (2, 1, 0, maxval)) + peeker = peeker_from_pixel(Texture.T_unsigned_byte, Texture.F_rgba, data) + + col = LColor() + peeker.fetch_pixel(col, 0, 0) + col *= maxval + assert col == (0, 1, 2, maxval) + + +def test_texture_peek_ushort(): + maxval = 65535 + data = array('H', (2, 1, 0, maxval)) + peeker = peeker_from_pixel(Texture.T_unsigned_short, Texture.F_rgba, data) + + col = LColor() + peeker.fetch_pixel(col, 0, 0) + col *= maxval + assert col == (0, 1, 2, maxval) + + +def test_texture_peek_uint(): + maxval = 4294967295 + data = array('I', (2, 1, 0, maxval)) + peeker = peeker_from_pixel(Texture.T_unsigned_int, Texture.F_rgba, data) + + col = LColor() + peeker.fetch_pixel(col, 0, 0) + col *= maxval + assert col == (0, 1, 2, maxval) + + +def test_texture_peek_float(): + data = array('f', (1.0, 0.0, -2.0, 10000.0)) + peeker = peeker_from_pixel(Texture.T_float, Texture.F_rgba, data) + + col = LColor() + peeker.fetch_pixel(col, 0, 0) + assert col == (-2.0, 0.0, 1.0, 10000.0) + + +def test_texture_peek_half(): + # Python's array class doesn't support half floats, so we hardcode the + # binary representation of these numbers: + data = array('H', ( + 0b0011110000000000, # 1.0 + 0b1100000000000000, # -2.0 + 0b0111101111111111, # 65504.0 + 0b0011010101010101, # 0.333251953125 + )) + peeker = peeker_from_pixel(Texture.T_half_float, Texture.F_rgba, data) + + col = LColor() + peeker.fetch_pixel(col, 0, 0) + assert col == (65504.0, -2.0, 1.0, 0.333251953125) + + +def test_texture_peek_srgb(): + # 188 = roughly middle gray + data = array('B', [188, 188, 188]) + peeker = peeker_from_pixel(Texture.T_unsigned_byte, Texture.F_srgb, data) + + col = LColor() + peeker.fetch_pixel(col, 0, 0) + + # We allow some imprecision. + assert col.almost_equal((0.5, 0.5, 0.5, 1.0), 1 / 255.0) + + +def test_texture_peek_srgba(): + # 188 = middle gray + data = array('B', [188, 188, 188, 188]) + peeker = peeker_from_pixel(Texture.T_unsigned_byte, Texture.F_srgb_alpha, data) + + col = LColor() + peeker.fetch_pixel(col, 0, 0) + + # We allow some imprecision. + assert col.almost_equal((0.5, 0.5, 0.5, 188 / 255.0), 1 / 255.0) From e4e24eee56e12f76a5708cda6a607d3c97af4e4e Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 19 Dec 2017 18:44:44 +0100 Subject: [PATCH 15/25] glgsg: use T_float when downloading 24-bit depth textures This is to match behavior with previous Panda3D versions. See #212 --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 3 --- 1 file changed, 3 deletions(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index dce9cc7d89..660eeb8d9a 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -6460,9 +6460,6 @@ framebuffer_copy_to_ram(Texture *tex, int view, int z, break; case Texture::F_depth_component24: - component_type = Texture::T_unsigned_int; - break; - case Texture::F_depth_component32: component_type = Texture::T_float; break; From e9a50c889888352fa08a7b0ebc3ea794a4bf4fcb Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 19 Dec 2017 19:27:36 +0100 Subject: [PATCH 16/25] texture: fix get_clear_data() return type, make it work with sRGB --- .../glstuff/glGraphicsStateGuardian_src.cxx | 8 +++---- panda/src/gobj/texture.I | 8 +++---- panda/src/gobj/texture.cxx | 22 +++++++++++++++++-- panda/src/gobj/texture.h | 2 +- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 660eeb8d9a..5330129f9d 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -12323,20 +12323,20 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload, if (_supports_clear_texture) { // We can do that with the convenient glClearTexImage // function. - string clear_data = tex->get_clear_data(); + vector_uchar clear_data = tex->get_clear_data(); _glClearTexImage(gtc->_index, n - mipmap_bias, external_format, - component_type, (void *)clear_data.data()); + component_type, (void *)&clear_data[0]); continue; } } else { if (_supports_clear_buffer) { // For buffer textures we need to clear the underlying // storage. - string clear_data = tex->get_clear_data(); + vector_uchar clear_data = tex->get_clear_data(); _glClearBufferData(GL_TEXTURE_BUFFER, internal_format, external_format, - component_type, (const void *)clear_data.data()); + component_type, (const void *)&clear_data[0]); continue; } } diff --git a/panda/src/gobj/texture.I b/panda/src/gobj/texture.I index 841c8aa9bf..8f3a91a54c 100644 --- a/panda/src/gobj/texture.I +++ b/panda/src/gobj/texture.I @@ -279,12 +279,12 @@ clear_clear_color() { * Returns the raw image data for a single pixel if it were set to the clear * color. */ -INLINE string Texture:: +INLINE vector_uchar Texture:: get_clear_data() const { CDReader cdata(_cycler); - unsigned char data[16]; - size_t size = do_get_clear_data(cdata, data); - return string((char *)data, size); + vector_uchar data(16); + data.resize(do_get_clear_data(cdata, &data[0])); + return data; } /** diff --git a/panda/src/gobj/texture.cxx b/panda/src/gobj/texture.cxx index bddee6510b..c65172364e 100644 --- a/panda/src/gobj/texture.cxx +++ b/panda/src/gobj/texture.cxx @@ -5584,10 +5584,28 @@ do_get_clear_data(const CData *cdata, unsigned char *into) const { nassertr(cdata->_has_clear_color, 0); nassertr(cdata->_num_components <= 4, 0); - // TODO: encode the color into the sRGB color space if used switch (cdata->_component_type) { case T_unsigned_byte: - { + if (is_srgb(cdata->_format)) { + xel color; + xelval alpha; + encode_sRGB_uchar(cdata->_clear_color, color, alpha); + switch (cdata->_num_components) { + case 2: + into[1] = (unsigned char)color.g; + case 1: + into[0] = (unsigned char)color.r; + break; + case 4: + into[3] = (unsigned char)alpha; + case 3: // BGR <-> RGB + into[0] = (unsigned char)color.b; + into[1] = (unsigned char)color.g; + into[2] = (unsigned char)color.r; + break; + } + break; + } else { LColor scaled = cdata->_clear_color.fmin(LColor(1)).fmax(LColor::zero()); scaled *= 255; switch (cdata->_num_components) { diff --git a/panda/src/gobj/texture.h b/panda/src/gobj/texture.h index 60c16b3675..49e2f48dbd 100644 --- a/panda/src/gobj/texture.h +++ b/panda/src/gobj/texture.h @@ -265,7 +265,7 @@ PUBLISHED: INLINE LColor get_clear_color() const; INLINE void set_clear_color(const LColor &color); INLINE void clear_clear_color(); - INLINE string get_clear_data() const; + INLINE vector_uchar get_clear_data() const; MAKE_PROPERTY2(clear_color, has_clear_color, get_clear_color, set_clear_color, clear_clear_color); From 9dcfcbf5fae9d29796da91be62fef552d22956d4 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 19 Dec 2017 20:02:10 +0100 Subject: [PATCH 17/25] texture: support store() on more component types (incl. sRGB) This changes behaviour for sRGB textures, which weren't previously converting to the correct color space. Also add unit tests for storing to PNMImage. Closes: #212 --- panda/src/gobj/texture.cxx | 116 +++++++++++++++++++++++++--------- panda/src/gobj/texture.h | 3 +- panda/src/pnmimage/pnmImage.I | 2 +- tests/gobj/test_texture.py | 90 ++++++++++++++++++++++++++ 4 files changed, 179 insertions(+), 32 deletions(-) create mode 100644 tests/gobj/test_texture.py diff --git a/panda/src/gobj/texture.cxx b/panda/src/gobj/texture.cxx index c65172364e..c4a8149ab4 100644 --- a/panda/src/gobj/texture.cxx +++ b/panda/src/gobj/texture.cxx @@ -5088,7 +5088,8 @@ do_store_one(CData *cdata, PNMImage &pnmimage, int z, int n) { return convert_to_pnmimage(pnmimage, do_get_expected_mipmap_x_size(cdata, n), do_get_expected_mipmap_y_size(cdata, n), - cdata->_num_components, cdata->_component_width, + cdata->_num_components, cdata->_component_type, + is_srgb(cdata->_format), cdata->_ram_images[n]._image, do_get_ram_mipmap_page_size(cdata, n), z); } @@ -5111,12 +5112,14 @@ do_store_one(CData *cdata, PfmFile &pfm, int z, int n) { if (cdata->_component_type != T_float) { // PfmFile by way of PNMImage. PNMImage pnmimage; - bool success = convert_to_pnmimage(pnmimage, - do_get_expected_mipmap_x_size(cdata, n), - do_get_expected_mipmap_y_size(cdata, n), - cdata->_num_components, cdata->_component_width, - cdata->_ram_images[n]._image, - do_get_ram_mipmap_page_size(cdata, n), z); + bool success = + convert_to_pnmimage(pnmimage, + do_get_expected_mipmap_x_size(cdata, n), + do_get_expected_mipmap_y_size(cdata, n), + cdata->_num_components, cdata->_component_type, + is_srgb(cdata->_format), + cdata->_ram_images[n]._image, + do_get_ram_mipmap_page_size(cdata, n), z); if (!success) { return false; } @@ -8055,25 +8058,28 @@ convert_from_pfm(PTA_uchar &image, size_t page_size, int z, */ bool Texture:: convert_to_pnmimage(PNMImage &pnmimage, int x_size, int y_size, - int num_components, int component_width, - CPTA_uchar image, size_t page_size, int z) { + int num_components, ComponentType component_type, + bool is_srgb, CPTA_uchar image, size_t page_size, int z) { xelval maxval = 0xff; - if (component_width > 1) { + if (component_type != T_unsigned_byte && component_type != T_byte) { maxval = 0xffff; } - pnmimage.clear(x_size, y_size, num_components, maxval); + ColorSpace color_space = is_srgb ? CS_sRGB : CS_linear; + pnmimage.clear(x_size, y_size, num_components, maxval, nullptr, color_space); bool has_alpha = pnmimage.has_alpha(); bool is_grayscale = pnmimage.is_grayscale(); int idx = page_size * z; nassertr(idx + page_size <= image.size(), false); - const unsigned char *p = &image[idx]; - if (component_width == 1) { - xel *array = pnmimage.get_array(); + xel *array = pnmimage.get_array(); + xelval *alpha = pnmimage.get_alpha_array(); + + switch (component_type) { + case T_unsigned_byte: if (is_grayscale) { + const unsigned char *p = &image[idx]; if (has_alpha) { - xelval *alpha = pnmimage.get_alpha_array(); for (int j = y_size-1; j >= 0; j--) { xel *row = array + j * x_size; xelval *alpha_row = alpha + j * x_size; @@ -8090,9 +8096,10 @@ convert_to_pnmimage(PNMImage &pnmimage, int x_size, int y_size, } } } + nassertr(p == &image[idx] + page_size, false); } else { + const unsigned char *p = &image[idx]; if (has_alpha) { - xelval *alpha = pnmimage.get_alpha_array(); for (int j = y_size-1; j >= 0; j--) { xel *row = array + j * x_size; xelval *alpha_row = alpha + j * x_size; @@ -8113,29 +8120,78 @@ convert_to_pnmimage(PNMImage &pnmimage, int x_size, int y_size, } } } + nassertr(p == &image[idx] + page_size, false); } + break; - } else if (component_width == 2) { - for (int j = y_size-1; j >= 0; j--) { - for (int i = 0; i < x_size; i++) { - if (is_grayscale) { - pnmimage.set_gray(i, j, get_unsigned_short(p)); - } else { - pnmimage.set_blue(i, j, get_unsigned_short(p)); - pnmimage.set_green(i, j, get_unsigned_short(p)); - pnmimage.set_red(i, j, get_unsigned_short(p)); - } - if (has_alpha) { - pnmimage.set_alpha(i, j, get_unsigned_short(p)); + case T_unsigned_short: + { + const uint16_t *p = (const uint16_t *)&image[idx]; + + for (int j = y_size-1; j >= 0; j--) { + xel *row = array + j * x_size; + xelval *alpha_row = alpha + j * x_size; + for (int i = 0; i < x_size; i++) { + PPM_PUTB(row[i], *p++); + if (!is_grayscale) { + PPM_PUTG(row[i], *p++); + PPM_PUTR(row[i], *p++); + } + if (has_alpha) { + alpha_row[i] = *p++; + } } } + nassertr((const unsigned char *)p == &image[idx] + page_size, false); } + break; - } else { + case T_unsigned_int: + { + const uint32_t *p = (const uint32_t *)&image[idx]; + + for (int j = y_size-1; j >= 0; j--) { + xel *row = array + j * x_size; + xelval *alpha_row = alpha + j * x_size; + for (int i = 0; i < x_size; i++) { + PPM_PUTB(row[i], (*p++) >> 16u); + if (!is_grayscale) { + PPM_PUTG(row[i], (*p++) >> 16u); + PPM_PUTR(row[i], (*p++) >> 16u); + } + if (has_alpha) { + alpha_row[i] = (*p++) >> 16u; + } + } + } + nassertr((const unsigned char *)p == &image[idx] + page_size, false); + } + break; + + case T_half_float: + { + const unsigned char *p = &image[idx]; + + for (int j = y_size-1; j >= 0; j--) { + for (int i = 0; i < x_size; i++) { + pnmimage.set_blue(i, j, get_half_float(p)); + if (!is_grayscale) { + pnmimage.set_green(i, j, get_half_float(p)); + pnmimage.set_red(i, j, get_half_float(p)); + } + if (has_alpha) { + pnmimage.set_alpha(i, j, get_half_float(p)); + } + } + } + nassertr(p == &image[idx] + page_size, false); + } + break; + + default: return false; } - nassertr(p == &image[idx] + page_size, false); return true; } diff --git a/panda/src/gobj/texture.h b/panda/src/gobj/texture.h index 49e2f48dbd..5816b8c538 100644 --- a/panda/src/gobj/texture.h +++ b/panda/src/gobj/texture.h @@ -801,7 +801,8 @@ private: int z, const PfmFile &pfm, int num_components, int component_width); static bool convert_to_pnmimage(PNMImage &pnmimage, int x_size, int y_size, - int num_components, int component_width, + int num_components, + ComponentType component_type, bool is_srgb, CPTA_uchar image, size_t page_size, int z); static bool convert_to_pfm(PfmFile &pfm, int x_size, int y_size, diff --git a/panda/src/pnmimage/pnmImage.I b/panda/src/pnmimage/pnmImage.I index 173f543e1e..d4a7026483 100644 --- a/panda/src/pnmimage/pnmImage.I +++ b/panda/src/pnmimage/pnmImage.I @@ -80,7 +80,7 @@ to_val(float input_value) const { switch (_xel_encoding) { case XE_generic: case XE_generic_alpha: - return clamp_val((int)(input_value * get_maxval() + 0.5f)); + return (int)(min(1.0f, max(0.0f, input_value)) * get_maxval() + 0.5f); case XE_generic_sRGB: case XE_generic_sRGB_alpha: diff --git a/tests/gobj/test_texture.py b/tests/gobj/test_texture.py new file mode 100644 index 0000000000..8fa0ed27c8 --- /dev/null +++ b/tests/gobj/test_texture.py @@ -0,0 +1,90 @@ +from panda3d.core import Texture, PNMImage +from array import array + + +def image_from_stored_pixel(component_type, format, data): + """ Creates a 1-pixel texture with the given settings and pixel data, + then returns a PNMImage as result of calling texture.store(). """ + + tex = Texture("") + tex.setup_1d_texture(1, component_type, format) + tex.set_ram_image(data) + + img = PNMImage() + assert tex.store(img) + return img + + +def test_texture_store_unsigned_byte(): + data = array('B', (2, 1, 0, 0xff)) + img = image_from_stored_pixel(Texture.T_unsigned_byte, Texture.F_rgba, data) + + assert img.maxval == 0xff + pix = img.get_pixel(0, 0) + assert tuple(pix) == (0, 1, 2, 0xff) + + +def test_texture_store_unsigned_short(): + data = array('H', (2, 1, 0, 0xffff)) + img = image_from_stored_pixel(Texture.T_unsigned_short, Texture.F_rgba, data) + + assert img.maxval == 0xffff + pix = img.get_pixel(0, 0) + assert tuple(pix) == (0, 1, 2, 0xffff) + + +def test_texture_store_unsigned_int(): + data = array('I', (0x30000, 2, 0, 0xffffffff)) + img = image_from_stored_pixel(Texture.T_unsigned_int, Texture.F_rgba, data) + + assert img.maxval == 0xffff + pix = img.get_pixel(0, 0) + assert tuple(pix) == (0, 0, 3, 0xffff) + + +def test_texture_store_float(): + data = array('f', (0.5, 0.0, -2.0, 10000.0)) + img = image_from_stored_pixel(Texture.T_float, Texture.F_rgba, data) + + assert img.maxval == 0xffff + col = img.get_xel_a(0, 0) + assert col.almost_equal((0.0, 0.0, 0.5, 1.0), 1 / 255.0) + + +def test_texture_store_half(): + # Python's array class doesn't support half floats, so we hardcode the + # binary representation of these numbers: + data = array('H', ( + # -[exp][mantissa] + 0b0011110000000000, # 1.0 + 0b1100000000000000, # -2.0 + 0b0111101111111111, # 65504.0 + 0b0011010101010101, # 0.333251953125 + )) + img = image_from_stored_pixel(Texture.T_half_float, Texture.F_rgba, data) + + assert img.maxval == 0xffff + col = img.get_xel_a(0, 0) + assert col.almost_equal((1.0, 0.0, 1.0, 0.333251953125), 1 / 255.0) + + +def test_texture_store_srgb(): + # 188 = roughly middle gray + data = array('B', [188, 188, 188]) + img = image_from_stored_pixel(Texture.T_unsigned_byte, Texture.F_srgb, data) + + # We allow some imprecision. + assert img.maxval == 0xff + col = img.get_xel(0, 0) + assert col.almost_equal((0.5, 0.5, 0.5), 1 / 255.0) + + +def test_texture_store_srgb_alpha(): + # 188 = middle gray + data = array('B', [188, 188, 188, 188]) + img = image_from_stored_pixel(Texture.T_unsigned_byte, Texture.F_srgb_alpha, data) + + # We allow some imprecision. + assert img.maxval == 0xff + col = img.get_xel_a(0, 0) + assert col.almost_equal((0.5, 0.5, 0.5, 188 / 255.0), 1 / 255.0) From 6faaabe0344b4d7359dc60d858542a20912702f4 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 19 Dec 2017 23:37:36 +0100 Subject: [PATCH 18/25] makepanda: fix build error with maya2008 on macOS --- makepanda/makepanda.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index a231ec6182..f2c3d32f20 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -6370,6 +6370,8 @@ for VER in MAYAVERSIONS: continue elif GetTarget() == 'darwin' and int(VNUM) >= 2009: ARCH_OPTS = ['NOARCH:PPC'] + elif GetTarget() == 'darwin': + ARCH_OPTS = ['NOARCH:X86_64'] else: ARCH_OPTS = [] From 061e0c4862b1339bb74176aa5a72d68b9e71fe61 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 19 Dec 2017 23:38:45 +0100 Subject: [PATCH 19/25] cocoa: allow getting a GL 3.2+ context on macOS 10.7+ using gl-version --- .../cocoadisplay/cocoaGraphicsStateGuardian.mm | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/panda/src/cocoadisplay/cocoaGraphicsStateGuardian.mm b/panda/src/cocoadisplay/cocoaGraphicsStateGuardian.mm index 30630036ab..ec25b94486 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsStateGuardian.mm +++ b/panda/src/cocoadisplay/cocoaGraphicsStateGuardian.mm @@ -24,6 +24,10 @@ #define kCGLRendererIDMatchingMask 0x00FE7F00 #endif +#ifndef NSAppKitVersionNumber10_7 +#define NSAppKitVersionNumber10_7 1138 +#endif + TypeHandle CocoaGraphicsStateGuardian::_type_handle; /** @@ -207,7 +211,8 @@ choose_pixel_format(const FrameBufferProperties &properties, attribs.push_back(NSOpenGLPFAAccelerated); } - attribs.push_back(NSOpenGLPFAWindow); + // This seems to cause getting a 3.2+ context to fail. + //attribs.push_back(NSOpenGLPFAWindow); if (need_pbuffer) { attribs.push_back(NSOpenGLPFAPixelBuffer); @@ -217,6 +222,16 @@ choose_pixel_format(const FrameBufferProperties &properties, attribs.push_back(NSOpenGLPFAScreenMask); attribs.push_back(CGDisplayIDToOpenGLDisplayMask(display)); + // Set OpenGL version if a minimum was requested. + if (gl_version.size() >= 1 && NSAppKitVersionNumber >= NSAppKitVersionNumber10_7) { + //NB. There is also NSOpenGLProfileVersion4_1Core, but this seems to cause + // a software implementation to be selected on my mac mini running 10.11. + if (gl_version[0] >= 4 || (gl_version.size() >= 2 && gl_version[0] == 3 && gl_version[1] >= 2)) { + attribs.push_back((NSOpenGLPixelFormatAttribute)99); // NSOpenGLPFAOpenGLProfile + attribs.push_back((NSOpenGLPixelFormatAttribute)0x3200); // NSOpenGLProfileVersion3_2Core + } + } + // End of the array attribs.push_back((NSOpenGLPixelFormatAttribute)0); From 096d54de0039cd3d5d15534177a30d8a404f4b5e Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 19 Dec 2017 23:44:15 +0100 Subject: [PATCH 20/25] Add double-precision versions of encode_sRGB_uchar --- panda/src/pnmimage/convert_srgb.I | 14 ++++++++++++++ panda/src/pnmimage/convert_srgb.h | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/panda/src/pnmimage/convert_srgb.I b/panda/src/pnmimage/convert_srgb.I index 57d8d42170..f49cc38203 100644 --- a/panda/src/pnmimage/convert_srgb.I +++ b/panda/src/pnmimage/convert_srgb.I @@ -153,3 +153,17 @@ encode_sRGB_uchar(const LColorf &color, xel &into, xelval &into_alpha) { into_alpha = (xelval) (color[3] * 255.f + 0.5f); #endif } + + +/** + * Double-precision versions of the above. + */ +INLINE void +encode_sRGB_uchar(const LColord &color, xel &into) { + return encode_sRGB_uchar(LCAST(float, color), into); +} + +INLINE void +encode_sRGB_uchar(const LColord &color, xel &into, xelval &into_alpha) { + return encode_sRGB_uchar(LCAST(float, color), into, into_alpha); +} diff --git a/panda/src/pnmimage/convert_srgb.h b/panda/src/pnmimage/convert_srgb.h index dada8918b3..86bc8b07bf 100644 --- a/panda/src/pnmimage/convert_srgb.h +++ b/panda/src/pnmimage/convert_srgb.h @@ -46,6 +46,11 @@ EXPCL_PANDA_PNMIMAGE INLINE void encode_sRGB_uchar(const LColorf &from, EXPCL_PANDA_PNMIMAGE INLINE void encode_sRGB_uchar(const LColorf &from, xel &into, xelval &into_alpha); +EXPCL_PANDA_PNMIMAGE INLINE void encode_sRGB_uchar(const LColord &from, + xel &into); +EXPCL_PANDA_PNMIMAGE INLINE void encode_sRGB_uchar(const LColord &from, + xel &into, xelval &into_alpha); + // Use these functions if you know that SSE2 support is available. Otherwise, // they will crash! #if defined(__SSE2__) || defined(__i386__) || defined(_M_IX86) || defined(_M_X64) || defined(_M_AMD64) From f0b21ee969c00fd815ff1b05ad0d1b2cec229c1f Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 20 Dec 2017 01:23:00 +0100 Subject: [PATCH 21/25] Support old Python 2 buffer protocol in PTA and Texture ram_image This enables passing eg. str and array.array objects in Python 2 --- panda/src/express/pointerToArray_ext.I | 37 ++++++++++++++------------ panda/src/gobj/texture_ext.cxx | 23 ++++++++++++++++ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/panda/src/express/pointerToArray_ext.I b/panda/src/express/pointerToArray_ext.I index e2356c7a4b..644b5f0645 100644 --- a/panda/src/express/pointerToArray_ext.I +++ b/panda/src/express/pointerToArray_ext.I @@ -203,34 +203,37 @@ set_data(PyObject *data) { } PyBuffer_Release(&view); - } else { - Dtool_Raise_TypeError("PointerToArray.set_data() requires a buffer object"); + return; } -#else - // In Python 2.5 we didn't have the new buffer protocol, only str. - if (PyString_CheckExact(data)) { - int size = PyString_Size(data); - if (size % sizeof(Element) != 0) { +#endif + + // In Python 2, there was also an older buffer protocol, supported by eg. + // str and array objects. +#if PY_MAJOR_VERSION < 3 + // The old, deprecated buffer interface, as used by eg. the array module. + const void *buffer; + Py_ssize_t buffer_len; + if (!PyUnicode_CheckExact(data) && + PyObject_AsReadBuffer(data, &buffer, &buffer_len) == 0) { + if (buffer_len % sizeof(Element) != 0) { PyErr_Format(PyExc_ValueError, - "str object is not a multiple of %zu bytes", + "byte buffer is not a multiple of %zu bytes", sizeof(Element)); return; } - int num_elements = size / sizeof(Element); - this->_this->insert(this->_this->begin(), num_elements, Element()); - - // Hope there aren't any constructors or destructors involved here. - if (size != 0) { - const char *ptr = PyString_AsString(data); - memcpy(this->_this->p(), ptr, size); + if (buffer_len > 0) { + this->_this->resize(buffer_len / sizeof(Element)); + memcpy(this->_this->p(), buffer, buffer_len); } else { this->_this->clear(); } - } else { - Dtool_Raise_TypeError("PointerToArray.set_data() requires a str"); + + return; } #endif + + Dtool_Raise_TypeError("PointerToArray.set_data() requires a buffer object"); } /** diff --git a/panda/src/gobj/texture_ext.cxx b/panda/src/gobj/texture_ext.cxx index 260070f058..e81d40c15f 100644 --- a/panda/src/gobj/texture_ext.cxx +++ b/panda/src/gobj/texture_ext.cxx @@ -84,6 +84,29 @@ set_ram_image(PyObject *image, Texture::CompressionMode compression, } #endif +#if PY_MAJOR_VERSION < 3 + // The old, deprecated buffer interface, as used by eg. the array module. + const void *buffer; + Py_ssize_t buffer_len; + if (!PyUnicode_CheckExact(image) && + PyObject_AsReadBuffer(image, &buffer, &buffer_len) == 0) { + if (compression == Texture::CM_off) { + int component_width = _this->get_component_width(); + if (buffer_len % component_width != 0) { + PyErr_Format(PyExc_ValueError, + "byte buffer is not a multiple of %d bytes", + component_width); + return; + } + } + + PTA_uchar data = PTA_uchar::empty_array(buffer_len, Texture::get_class_type()); + memcpy(data.p(), buffer, buffer_len); + _this->set_ram_image(MOVE(data), compression, page_size); + return; + } +#endif + Dtool_Raise_ArgTypeError(image, 0, "Texture.set_ram_image", "CPTA_uchar or buffer"); } From f82a940878de5983b9f3dd19ba7680ef28f174e3 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 20 Dec 2017 14:16:05 +0100 Subject: [PATCH 22/25] glgsg: fix shader version in macOS 3.2+ context --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 5330129f9d..fab18bcb62 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -155,7 +155,11 @@ null_glBlendColor(GLclampf, GLclampf, GLclampf, GLclampf) { // drawing GUIs and such. static const string default_vshader = #ifndef OPENGLES +#ifdef __APPLE__ // Apple's GL 3.2 contexts require at least GLSL 1.50. + "#version 150\n" +#else "#version 130\n" +#endif "in vec4 p3d_Vertex;\n" "in vec4 p3d_Color;\n" "in vec2 p3d_MultiTexCoord0;\n" @@ -179,7 +183,11 @@ static const string default_vshader = static const string default_fshader = #ifndef OPENGLES +#ifdef __APPLE__ // Apple's GL 3.2 contexts require at least GLSL 1.50. + "#version 150\n" +#else "#version 130\n" +#endif "in vec2 texcoord;\n" "in vec4 color;\n" "out vec4 p3d_FragColor;\n" From ef7f856c46299f4d14711130269f11afad49e4c3 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 20 Dec 2017 14:17:53 +0100 Subject: [PATCH 23/25] cocoa: support windowless offscreen rendering on macOS Fixes: #183 --- panda/src/cocoadisplay/cocoaGraphicsBuffer.I | 12 ++ panda/src/cocoadisplay/cocoaGraphicsBuffer.h | 61 +++++++ panda/src/cocoadisplay/cocoaGraphicsBuffer.mm | 165 ++++++++++++++++++ panda/src/cocoadisplay/cocoaGraphicsPipe.I | 8 - panda/src/cocoadisplay/cocoaGraphicsPipe.h | 10 +- panda/src/cocoadisplay/cocoaGraphicsPipe.mm | 137 +++------------ .../cocoadisplay/cocoaGraphicsStateGuardian.I | 18 ++ .../cocoadisplay/cocoaGraphicsStateGuardian.h | 4 + panda/src/cocoadisplay/cocoaGraphicsWindow.mm | 44 +++-- panda/src/cocoadisplay/config_cocoadisplay.mm | 2 + .../cocoadisplay/p3cocoadisplay_composite1.mm | 1 + panda/src/glstuff/glGraphicsBuffer_src.cxx | 45 +++-- panda/src/glstuff/glGraphicsBuffer_src.h | 2 - 13 files changed, 352 insertions(+), 157 deletions(-) create mode 100644 panda/src/cocoadisplay/cocoaGraphicsBuffer.I create mode 100644 panda/src/cocoadisplay/cocoaGraphicsBuffer.h create mode 100644 panda/src/cocoadisplay/cocoaGraphicsBuffer.mm diff --git a/panda/src/cocoadisplay/cocoaGraphicsBuffer.I b/panda/src/cocoadisplay/cocoaGraphicsBuffer.I new file mode 100644 index 0000000000..1611af94d1 --- /dev/null +++ b/panda/src/cocoadisplay/cocoaGraphicsBuffer.I @@ -0,0 +1,12 @@ +/** + * 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 cocoaGraphicsBuffer.I + * @author rdb + * @date 2017-12-19 + */ diff --git a/panda/src/cocoadisplay/cocoaGraphicsBuffer.h b/panda/src/cocoadisplay/cocoaGraphicsBuffer.h new file mode 100644 index 0000000000..ab666b6f75 --- /dev/null +++ b/panda/src/cocoadisplay/cocoaGraphicsBuffer.h @@ -0,0 +1,61 @@ +/** + * 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 cocoaGraphicsBuffer.h + * @author rdb + * @date 2017-12-19 + */ + +#ifndef COCOAGRAPHICSBUFFER_H +#define COCOAGRAPHICSBUFFER_H + +#include "pandabase.h" +#include "glgsg.h" + +/** + * This is a light wrapper around GLGraphicsBuffer (ie. FBOs) to interface + * with Cocoa contexts, so that it can be used without a host window. + */ +class CocoaGraphicsBuffer : public GLGraphicsBuffer { +public: + CocoaGraphicsBuffer(GraphicsEngine *engine, GraphicsPipe *pipe, + const string &name, + const FrameBufferProperties &fb_prop, + const WindowProperties &win_prop, + int flags, + GraphicsStateGuardian *gsg, + GraphicsOutput *host); + + virtual bool begin_frame(FrameMode mode, Thread *current_thread); + virtual void end_frame(FrameMode mode, Thread *current_thread); + +protected: + virtual void close_buffer(); + virtual bool open_buffer(); + +public: + static TypeHandle get_class_type() { + return _type_handle; + } + static void init_type() { + GLGraphicsBuffer::init_type(); + register_type(_type_handle, "CocoaGraphicsBuffer", + GLGraphicsBuffer::get_class_type()); + } + virtual TypeHandle get_type() const { + return get_class_type(); + } + virtual TypeHandle force_init_type() {init_type(); return get_class_type();} + +private: + static TypeHandle _type_handle; +}; + +#include "cocoaGraphicsBuffer.I" + +#endif diff --git a/panda/src/cocoadisplay/cocoaGraphicsBuffer.mm b/panda/src/cocoadisplay/cocoaGraphicsBuffer.mm new file mode 100644 index 0000000000..ea6fb61e4e --- /dev/null +++ b/panda/src/cocoadisplay/cocoaGraphicsBuffer.mm @@ -0,0 +1,165 @@ +/** + * 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 cocoaGraphicsBuffer.mm + * @author rdb + * @date 2017-12-19 + */ + +#include "cocoaGraphicsBuffer.h" +#include "cocoaGraphicsStateGuardian.h" +#include "config_cocoadisplay.h" +#include "cocoaGraphicsPipe.h" + +#import + +TypeHandle CocoaGraphicsBuffer::_type_handle; + +/** + * + */ +CocoaGraphicsBuffer:: +CocoaGraphicsBuffer(GraphicsEngine *engine, GraphicsPipe *pipe, + const string &name, + const FrameBufferProperties &fb_prop, + const WindowProperties &win_prop, + int flags, + GraphicsStateGuardian *gsg, + GraphicsOutput *host) : // Ignore the host. + GLGraphicsBuffer(engine, pipe, name, fb_prop, win_prop, flags, gsg, nullptr) +{ +} + +/** + * This function will be called within the draw thread before beginning + * rendering for a given frame. It should do whatever setup is required, and + * return true if the frame should be rendered, or false if it should be + * skipped. + */ +bool CocoaGraphicsBuffer:: +begin_frame(FrameMode mode, Thread *current_thread) { + if (_gsg == nullptr) { + return false; + } + + CocoaGraphicsStateGuardian *cocoagsg; + DCAST_INTO_R(cocoagsg, _gsg, false); + nassertr(cocoagsg->_context != nil, false); + + // Lock the context and make it current. + { + PStatTimer timer(_make_current_pcollector, current_thread); + cocoagsg->lock_context(); + [cocoagsg->_context makeCurrentContext]; + } + + return GLGraphicsBuffer::begin_frame(mode, current_thread); +} + +/** + * This function will be called within the draw thread after rendering is + * completed for a given frame. It should do whatever finalization is + * required. + */ +void CocoaGraphicsBuffer:: +end_frame(FrameMode mode, Thread *current_thread) { + nassertv(_gsg != nullptr); + + GLGraphicsBuffer::end_frame(mode, current_thread); + + // Release the context. + CocoaGraphicsStateGuardian *cocoagsg; + DCAST_INTO_V(cocoagsg, _gsg); + cocoagsg->unlock_context(); +} + +/** + * Opens the buffer right now. Called from the window thread. Returns true + * if the buffer is successfully opened, or false if there was a problem. + */ +bool CocoaGraphicsBuffer:: +open_buffer() { + CocoaGraphicsPipe *cocoa_pipe; + DCAST_INTO_R(cocoa_pipe, _pipe, false); + + // GSG CreationInitialization + CocoaGraphicsStateGuardian *cocoagsg; + if (_gsg == nullptr) { + // There is no old gsg. Create a new one. + cocoagsg = new CocoaGraphicsStateGuardian(_engine, _pipe, nullptr); + cocoagsg->choose_pixel_format(_fb_properties, cocoa_pipe->get_display_id(), false); + _gsg = cocoagsg; + } else { + // If the old gsg has the wrong pixel format, create a new one that shares + // with the old gsg. + DCAST_INTO_R(cocoagsg, _gsg, false); + if (!cocoagsg->get_fb_properties().subsumes(_fb_properties)) { + cocoagsg = new CocoaGraphicsStateGuardian(_engine, _pipe, cocoagsg); + cocoagsg->choose_pixel_format(_fb_properties, cocoa_pipe->get_display_id(), false); + _gsg = cocoagsg; + } + } + + FrameBufferProperties desired_props(_fb_properties); + + // Lock the context, so we can safely operate on it. + cocoagsg->lock_context(); + + // Make the context current and initialize what we need. + [cocoagsg->_context makeCurrentContext]; + [cocoagsg->_context update]; + cocoagsg->reset_if_new(); + + // These properties are determined by choose_pixel_format. + _fb_properties.set_force_hardware(cocoagsg->_fbprops.get_force_hardware()); + _fb_properties.set_force_software(cocoagsg->_fbprops.get_force_software()); + + bool success = GLGraphicsBuffer::open_buffer(); + if (success) { + rebuild_bitplanes(); + if (_needs_rebuild) { + // If it still needs rebuild, then something must have gone wrong. + success = false; + } + } + + if (success && !_fb_properties.verify_hardware_software + (desired_props, cocoagsg->get_gl_renderer())) { + GLGraphicsBuffer::close_buffer(); + success = false; + } + + // Release the context. + cocoagsg->unlock_context(); + + if (!success) { + return false; + } + + return true; +} + +/** + * Closes the buffer right now. Called from the window thread. + */ +void CocoaGraphicsBuffer:: +close_buffer() { + if (_gsg != nullptr) { + CocoaGraphicsStateGuardian *cocoagsg; + cocoagsg = DCAST(CocoaGraphicsStateGuardian, _gsg); + + if (cocoagsg != nullptr && cocoagsg->_context != nil) { + cocoagsg->lock_context(); + GLGraphicsBuffer::close_buffer(); + cocoagsg->unlock_context(); + } + _gsg.clear(); + } else { + GLGraphicsBuffer::close_buffer(); + } +} diff --git a/panda/src/cocoadisplay/cocoaGraphicsPipe.I b/panda/src/cocoadisplay/cocoaGraphicsPipe.I index 874bf12fea..0ac4a79d83 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsPipe.I +++ b/panda/src/cocoadisplay/cocoaGraphicsPipe.I @@ -18,11 +18,3 @@ INLINE CGDirectDisplayID CocoaGraphicsPipe:: get_display_id() const { return _display; } - -/** - * Returns the Cocoa NSScreen pointer associated with this graphics pipe. - */ -INLINE NSScreen *CocoaGraphicsPipe:: -get_nsscreen() const { - return _screen; -} diff --git a/panda/src/cocoadisplay/cocoaGraphicsPipe.h b/panda/src/cocoadisplay/cocoaGraphicsPipe.h index d53456e332..6f2f863e9c 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsPipe.h +++ b/panda/src/cocoadisplay/cocoaGraphicsPipe.h @@ -35,13 +35,10 @@ class FrameBufferProperties; */ class CocoaGraphicsPipe : public GraphicsPipe { public: - CocoaGraphicsPipe(); - CocoaGraphicsPipe(CGDirectDisplayID display); - CocoaGraphicsPipe(NSScreen *screen); + CocoaGraphicsPipe(CGDirectDisplayID display = CGMainDisplayID()); virtual ~CocoaGraphicsPipe(); INLINE CGDirectDisplayID get_display_id() const; - INLINE NSScreen *get_nsscreen() const; virtual string get_interface_name() const; static PT(GraphicsPipe) pipe_constructor(); @@ -64,11 +61,8 @@ protected: private: void load_display_information(); - // _display and _screen refer to the same thing, NSScreen being the tiny - // Cocoa wrapper around the Quartz display ID. NSScreen isn't generally - // useful, but we need it when creating the window. + // This is the Quartz display identifier. CGDirectDisplayID _display; - NSScreen *_screen; friend class CocoaGraphicsWindow; diff --git a/panda/src/cocoadisplay/cocoaGraphicsPipe.mm b/panda/src/cocoadisplay/cocoaGraphicsPipe.mm index 10bef93e2e..0baa94f8ed 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsPipe.mm +++ b/panda/src/cocoadisplay/cocoaGraphicsPipe.mm @@ -12,7 +12,7 @@ */ #include "cocoaGraphicsPipe.h" -// #include "cocoaGraphicsBuffer.h" +#include "cocoaGraphicsBuffer.h" #include "cocoaGraphicsWindow.h" #include "cocoaGraphicsStateGuardian.h" #include "cocoaPandaApp.h" @@ -30,104 +30,32 @@ TypeHandle CocoaGraphicsPipe::_type_handle; -static void init_app() { - if (NSApp == nil) { - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - [CocoaPandaApp sharedApplication]; - -#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1060 - [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; -#endif - [NSApp finishLaunching]; - [NSApp activateIgnoringOtherApps:YES]; - - // Put Cocoa into thread-safe mode by spawning a thread which immediately - // exits. - NSThread* thread = [[NSThread alloc] init]; - [thread start]; - [thread autorelease]; - } -} - /** - * Uses the main screen (the one the user is most likely to be working in at - * the moment). + * Takes a CoreGraphics display ID, which defaults to the main display. */ CocoaGraphicsPipe:: -CocoaGraphicsPipe() { +CocoaGraphicsPipe(CGDirectDisplayID display) : _display(display) { _supported_types = OT_window | OT_buffer | OT_texture_buffer; _is_valid = true; - init_app(); + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - _screen = [NSScreen mainScreen]; - NSNumber *num = [[_screen deviceDescription] objectForKey: @"NSScreenNumber"]; - _display = (CGDirectDisplayID) [num longValue]; + // Put Cocoa into thread-safe mode by spawning a thread which immediately + // exits. + NSThread* thread = [[NSThread alloc] init]; + [thread start]; + [thread autorelease]; + + // We used to also obtain the corresponding NSScreen here, but this causes + // the application icon to start bouncing, which may be undesirable for + // apps that will never open a window. _display_width = CGDisplayPixelsWide(_display); _display_height = CGDisplayPixelsHigh(_display); load_display_information(); cocoadisplay_cat.debug() - << "Creating CocoaGraphicsPipe for main screen " - << _screen << " with display ID " << _display << "\n"; -} - -/** - * Takes a CoreGraphics display ID. - */ -CocoaGraphicsPipe:: -CocoaGraphicsPipe(CGDirectDisplayID display) { - _supported_types = OT_window | OT_buffer | OT_texture_buffer; - _is_valid = true; - _display = display; - - init_app(); - - // Iterate over the screens to find the one with our display ID. - NSEnumerator *e = [[NSScreen screens] objectEnumerator]; - while (NSScreen *screen = (NSScreen *) [e nextObject]) { - NSNumber *num = [[screen deviceDescription] objectForKey: @"NSScreenNumber"]; - if (display == (CGDirectDisplayID) [num longValue]) { - _screen = screen; - break; - } - } - - _display_width = CGDisplayPixelsWide(_display); - _display_height = CGDisplayPixelsHigh(_display); - load_display_information(); - - cocoadisplay_cat.debug() - << "Creating CocoaGraphicsPipe for screen " - << _screen << " with display ID " << _display << "\n"; -} - -/** - * Takes an NSScreen pointer. - */ -CocoaGraphicsPipe:: -CocoaGraphicsPipe(NSScreen *screen) { - _supported_types = OT_window | OT_buffer | OT_texture_buffer; - _is_valid = true; - - init_app(); - - if (screen == nil) { - _screen = [NSScreen mainScreen]; - } else { - _screen = screen; - } - NSNumber *num = [[_screen deviceDescription] objectForKey: @"NSScreenNumber"]; - _display = (CGDirectDisplayID) [num longValue]; - - _display_width = CGDisplayPixelsWide(_display); - _display_height = CGDisplayPixelsHigh(_display); - load_display_information(); - - cocoadisplay_cat.debug() - << "Creating CocoaGraphicsPipe for screen " - << _screen << " with display ID " << _display << "\n"; + << "Creating CocoaGraphicsPipe for display ID " << _display << "\n"; } /** @@ -308,10 +236,12 @@ make_output(const string &name, flags, gsg, host); } - // Second thing to try: a GLGraphicsBuffer + // Second thing to try: a GLGraphicsBuffer. This requires a context, so if + // we don't have a host window, we instead create a CocoaGraphicsBuffer, + // which wraps around GLGraphicsBuffer and manages a context. if (retry == 1) { - if (!gl_support_fbo || host == NULL || + if (!gl_support_fbo || (flags & (BF_require_parasite | BF_require_window)) != 0) { return NULL; } @@ -334,33 +264,14 @@ make_output(const string &name, precertify = true; } } - return new GLGraphicsBuffer(engine, this, name, fb_prop, win_prop, - flags, gsg, host); - } -/* - // Third thing to try: a CocoaGraphicsBuffer - if (retry == 2) { - if (((flags&BF_require_parasite)!=0)|| - ((flags&BF_require_window)!=0)|| - ((flags&BF_resizeable)!=0)|| - ((flags&BF_size_track_host)!=0)|| - ((flags&BF_can_bind_layered)!=0)) { - return NULL; + if (host != NULL) { + return new GLGraphicsBuffer(engine, this, name, fb_prop, win_prop, + flags, gsg, host); + } else { + return new CocoaGraphicsBuffer(engine, this, name, fb_prop, win_prop, + flags, gsg, host); } - - if (!support_rtt) { - if (((flags&BF_rtt_cumulative)!=0)|| - ((flags&BF_can_bind_every)!=0)) { - // If we require Render-to-Texture, but can't be sure we support it, - // bail. - return NULL; - } - } - - return new CocoaGraphicsBuffer(engine, this, name, fb_prop, win_prop, - flags, gsg, host); } -*/ // Nothing else left to try. return NULL; diff --git a/panda/src/cocoadisplay/cocoaGraphicsStateGuardian.I b/panda/src/cocoadisplay/cocoaGraphicsStateGuardian.I index d593a3fe86..586df26850 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsStateGuardian.I +++ b/panda/src/cocoadisplay/cocoaGraphicsStateGuardian.I @@ -19,3 +19,21 @@ INLINE const FrameBufferProperties &CocoaGraphicsStateGuardian:: get_fb_properties() const { return _fbprops; } + +/** + * Locks the context. + */ +INLINE void CocoaGraphicsStateGuardian:: +lock_context() { + nassertv(_context != nil); + CGLLockContext((CGLContextObj) [_context CGLContextObj]); +} + +/** + * Unlocks the context. + */ +INLINE void CocoaGraphicsStateGuardian:: +unlock_context() { + nassertv(_context != nil); + CGLUnlockContext((CGLContextObj) [_context CGLContextObj]); +} diff --git a/panda/src/cocoadisplay/cocoaGraphicsStateGuardian.h b/panda/src/cocoadisplay/cocoaGraphicsStateGuardian.h index a2420e55c2..2e83e60c34 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsStateGuardian.h +++ b/panda/src/cocoadisplay/cocoaGraphicsStateGuardian.h @@ -19,6 +19,7 @@ #include "glgsg.h" #import +#import /** * A tiny specialization on GLGraphicsStateGuardian to add some Cocoa-specific @@ -38,6 +39,9 @@ public: virtual ~CocoaGraphicsStateGuardian(); + INLINE void lock_context(); + INLINE void unlock_context(); + NSOpenGLContext *_share_context; NSOpenGLContext *_context; FrameBufferProperties _fbprops; diff --git a/panda/src/cocoadisplay/cocoaGraphicsWindow.mm b/panda/src/cocoadisplay/cocoaGraphicsWindow.mm index ebc9255d63..426c06cc78 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsWindow.mm +++ b/panda/src/cocoadisplay/cocoaGraphicsWindow.mm @@ -65,6 +65,16 @@ CocoaGraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe, _fullscreen_mode = NULL; _windowed_mode = NULL; + // Now that we know for sure we want a window, we can create the Cocoa app. + // This will cause the application icon to appear and start bouncing. + if (NSApp == nil) { +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1060 + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; +#endif + [NSApp finishLaunching]; + [NSApp activateIgnoringOtherApps:YES]; + } + GraphicsWindowInputDevice device = GraphicsWindowInputDevice::pointer_and_keyboard(this, "keyboard_mouse"); add_input_device(device); @@ -144,7 +154,7 @@ begin_frame(FrameMode mode, Thread *current_thread) { nassertr(_view != nil, false); // Place a lock on the context. - CGLLockContext((CGLContextObj) [cocoagsg->_context CGLContextObj]); + cocoagsg->lock_context(); // Set the drawable. if (_properties.get_fullscreen()) { @@ -210,7 +220,7 @@ end_frame(FrameMode mode, Thread *current_thread) { CocoaGraphicsStateGuardian *cocoagsg; DCAST_INTO_V(cocoagsg, _gsg); - CGLUnlockContext((CGLContextObj) [cocoagsg->_context CGLContextObj]); + cocoagsg->unlock_context(); if (mode == FM_render) { // end_render_texture(); @@ -239,7 +249,7 @@ end_flip() { CocoaGraphicsStateGuardian *cocoagsg; DCAST_INTO_V(cocoagsg, _gsg); - CGLLockContext((CGLContextObj) [cocoagsg->_context CGLContextObj]); + cocoagsg->lock_context(); // Swap the front and back buffer. [cocoagsg->_context flushBuffer]; @@ -247,7 +257,7 @@ end_flip() { // Flush the window [[_view window] flushWindow]; - CGLUnlockContext((CGLContextObj) [cocoagsg->_context CGLContextObj]); + cocoagsg->unlock_context(); } GraphicsWindow::end_flip(); } @@ -399,6 +409,16 @@ open_window() { } } + // Iterate over the screens to find the one with our display ID. + NSScreen *screen; + NSEnumerator *e = [[NSScreen screens] objectEnumerator]; + while (screen = (NSScreen *) [e nextObject]) { + NSNumber *num = [[screen deviceDescription] objectForKey: @"NSScreenNumber"]; + if (cocoa_pipe->_display == (CGDirectDisplayID) [num longValue]) { + break; + } + } + // Center the window if coordinates were set to -1 or -2 TODO: perhaps in // future, in the case of -1, it should use the origin used in a previous // run of Panda @@ -406,7 +426,7 @@ open_window() { if (parent_nsview != NULL) { container = [parent_nsview bounds]; } else { - container = [cocoa_pipe->_screen frame]; + container = [screen frame]; container.origin = NSMakePoint(0, 0); } int x = _properties.get_x_origin(); @@ -453,7 +473,7 @@ open_window() { _window = [[CocoaPandaWindow alloc] initWithContentRect: rect styleMask:windowStyle - screen:cocoa_pipe->_screen + screen:screen window:this]; if (_window == nil) { @@ -464,7 +484,7 @@ open_window() { } // Lock the context, so we can safely operate on it. - CGLLockContext((CGLContextObj) [cocoagsg->_context CGLContextObj]); + cocoagsg->lock_context(); // Create the NSView to render to. NSRect rect = NSMakeRect(0, 0, _properties.get_x_size(), _properties.get_y_size()); @@ -588,7 +608,7 @@ open_window() { cocoagsg->reset_if_new(); // Release the context. - CGLUnlockContext((CGLContextObj) [cocoagsg->_context CGLContextObj]); + cocoagsg->unlock_context(); if (!cocoagsg->is_valid()) { close_window(); @@ -637,9 +657,9 @@ close_window() { cocoagsg = DCAST(CocoaGraphicsStateGuardian, _gsg); if (cocoagsg != NULL && cocoagsg->_context != nil) { - CGLLockContext((CGLContextObj) [cocoagsg->_context CGLContextObj]); + cocoagsg->lock_context(); [cocoagsg->_context clearDrawable]; - CGLUnlockContext((CGLContextObj) [cocoagsg->_context CGLContextObj]); + cocoagsg->unlock_context(); } _gsg.clear(); } @@ -1429,9 +1449,9 @@ handle_close_event() { cocoagsg = DCAST(CocoaGraphicsStateGuardian, _gsg); if (cocoagsg != NULL && cocoagsg->_context != nil) { - CGLLockContext((CGLContextObj) [cocoagsg->_context CGLContextObj]); + cocoagsg->lock_context(); [cocoagsg->_context clearDrawable]; - CGLUnlockContext((CGLContextObj) [cocoagsg->_context CGLContextObj]); + cocoagsg->unlock_context(); } _gsg.clear(); } diff --git a/panda/src/cocoadisplay/config_cocoadisplay.mm b/panda/src/cocoadisplay/config_cocoadisplay.mm index 4f67bc1438..42f2f98a8f 100644 --- a/panda/src/cocoadisplay/config_cocoadisplay.mm +++ b/panda/src/cocoadisplay/config_cocoadisplay.mm @@ -12,6 +12,7 @@ */ #include "config_cocoadisplay.h" +#include "cocoaGraphicsBuffer.h" #include "cocoaGraphicsPipe.h" #include "cocoaGraphicsStateGuardian.h" #include "cocoaGraphicsWindow.h" @@ -40,6 +41,7 @@ init_libcocoadisplay() { } initialized = true; + CocoaGraphicsBuffer::init_type(); CocoaGraphicsPipe::init_type(); CocoaGraphicsStateGuardian::init_type(); CocoaGraphicsWindow::init_type(); diff --git a/panda/src/cocoadisplay/p3cocoadisplay_composite1.mm b/panda/src/cocoadisplay/p3cocoadisplay_composite1.mm index 28755a66dd..e85d1e08eb 100644 --- a/panda/src/cocoadisplay/p3cocoadisplay_composite1.mm +++ b/panda/src/cocoadisplay/p3cocoadisplay_composite1.mm @@ -1,4 +1,5 @@ #include "config_cocoadisplay.mm" +#include "cocoaGraphicsBuffer.mm" #include "cocoaGraphicsPipe.mm" #include "cocoaGraphicsStateGuardian.mm" #include "cocoaGraphicsWindow.mm" diff --git a/panda/src/glstuff/glGraphicsBuffer_src.cxx b/panda/src/glstuff/glGraphicsBuffer_src.cxx index e76f8fccb9..43f3dbd809 100644 --- a/panda/src/glstuff/glGraphicsBuffer_src.cxx +++ b/panda/src/glstuff/glGraphicsBuffer_src.cxx @@ -213,12 +213,20 @@ begin_frame(FrameMode mode, Thread *current_thread) { return false; } - if (!_host->begin_frame(FM_parasite, current_thread)) { - if (GLCAT.is_debug()) { - GLCAT.debug() - << get_name() << "'s host is not ready\n"; + if (_host != nullptr) { + if (!_host->begin_frame(FM_parasite, current_thread)) { + if (GLCAT.is_debug()) { + GLCAT.debug() + << get_name() << "'s host is not ready\n"; + } + return false; + } + } else { + // We don't have a host window, which is possible for CocoaGraphicsBuffer. + _gsg->set_current_properties(&get_fb_properties()); + if (!_gsg->begin_frame(current_thread)) { + return false; } - return false; } // Figure out the desired size of the buffer. @@ -235,7 +243,7 @@ begin_frame(FrameMode mode, Thread *current_thread) { } } if (_creation_flags & GraphicsPipe::BF_size_track_host) { - if (_host->get_size() != _size) { + if (_host != nullptr && _host->get_size() != _size) { // We also need to rebuild if we need to change size. _needs_rebuild = true; } @@ -356,7 +364,7 @@ rebuild_bitplanes() { // Calculate bitplane size. This can be larger than the buffer. if (_creation_flags & GraphicsPipe::BF_size_track_host) { - if (_host->get_size() != _size) { + if (_host != nullptr && _host->get_size() != _size) { set_size_and_recalc(_host->get_x_size(), _host->get_y_size()); } @@ -1253,7 +1261,11 @@ end_frame(FrameMode mode, Thread *current_thread) { generate_mipmaps(); } - _host->end_frame(FM_parasite, current_thread); + if (_host != nullptr) { + _host->end_frame(FM_parasite, current_thread); + } else { + glgsg->end_frame(current_thread); + } if (mode == FM_render) { trigger_flip(); @@ -1315,8 +1327,11 @@ bool CLP(GraphicsBuffer):: open_buffer() { report_my_gl_errors(); - // Double check that we have a host - nassertr(_host != 0, false); + // Double check that we have a valid gsg + nassertr(_gsg != nullptr, false); + if (!_gsg->is_valid()) { + return false; + } // Count total color buffers. int totalcolor = @@ -1491,8 +1506,10 @@ open_buffer() { _fb_properties.set_back_buffers(0); _fb_properties.set_indexed_color(0); _fb_properties.set_rgb_color(1); - _fb_properties.set_force_hardware(_host->get_fb_properties().get_force_hardware()); - _fb_properties.set_force_software(_host->get_fb_properties().get_force_software()); + if (_host != nullptr) { + _fb_properties.set_force_hardware(_host->get_fb_properties().get_force_hardware()); + _fb_properties.set_force_software(_host->get_fb_properties().get_force_software()); + } _is_valid = true; _needs_rebuild = true; @@ -1508,7 +1525,7 @@ open_buffer() { */ GraphicsOutput *CLP(GraphicsBuffer):: get_host() { - return _host; + return (_host != nullptr) ? _host : this; } /** @@ -1699,7 +1716,7 @@ report_my_errors(int line, const char *file) { */ void CLP(GraphicsBuffer):: check_host_valid() { - if ((_host == 0)||(!_host->is_valid())) { + if (_host != nullptr && !_host->is_valid()) { _rb_data_size_bytes = 0; if (_rb_context != NULL) { // We must delete this object first, because when the GSG destructs, so diff --git a/panda/src/glstuff/glGraphicsBuffer_src.h b/panda/src/glstuff/glGraphicsBuffer_src.h index d119148247..31f7c91912 100644 --- a/panda/src/glstuff/glGraphicsBuffer_src.h +++ b/panda/src/glstuff/glGraphicsBuffer_src.h @@ -86,8 +86,6 @@ protected: void report_my_errors(int line, const char *file); -private: - void bind_slot(int layer, bool rb_resize, Texture **attach, RenderTexturePlane plane, GLenum attachpoint); void bind_slot_multisample(bool rb_resize, Texture **attach, From c1fb44ad69ff8f08e92b29bf1148696edfcb26cd Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 21 Dec 2017 11:37:30 +0100 Subject: [PATCH 24/25] makewheel: fix binary data being doubled --- makepanda/makewheel.py | 1 - 1 file changed, 1 deletion(-) diff --git a/makepanda/makewheel.py b/makepanda/makewheel.py index 92037098dd..f727f10e73 100644 --- a/makepanda/makewheel.py +++ b/makepanda/makewheel.py @@ -343,7 +343,6 @@ class WheelFile(object): # Otherwise, just copy it over. temp.write(open(source_path, 'rb').read()) - temp.write(open(source_path, 'rb').read()) os.fchmod(temp.fileno(), os.fstat(temp.fileno()).st_mode | 0o111) temp.close() From ed5e5386b959b300bc2fd7e6440d1cf8f9bb9d32 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 21 Dec 2017 14:06:41 +0100 Subject: [PATCH 25/25] AsyncFuture improvements, incl. support for gathering futures --- direct/src/task/Task.py | 2 + panda/src/event/asyncFuture.I | 99 +++++++- panda/src/event/asyncFuture.cxx | 345 +++++++++++++++++++++------- panda/src/event/asyncFuture.h | 106 +++++++-- panda/src/event/asyncFuture_ext.cxx | 161 ++++++++++++- panda/src/event/asyncFuture_ext.h | 6 +- panda/src/event/asyncTask.h | 1 + panda/src/event/asyncTaskChain.cxx | 3 +- panda/src/event/config_event.cxx | 1 + panda/src/event/pythonTask.cxx | 39 ++-- panda/src/event/pythonTask.h | 3 +- tests/event/test_futures.py | 99 ++++++++ 12 files changed, 724 insertions(+), 141 deletions(-) diff --git a/direct/src/task/Task.py b/direct/src/task/Task.py index 8181638264..8645e5bc43 100644 --- a/direct/src/task/Task.py +++ b/direct/src/task/Task.py @@ -86,6 +86,8 @@ Task.DtoolClassDict['exit'] = exit pause = AsyncTaskPause Task.DtoolClassDict['pause'] = staticmethod(pause) +gather = Task.gather + def sequence(*taskList): seq = AsyncTaskSequence('sequence') for task in taskList: diff --git a/panda/src/event/asyncFuture.I b/panda/src/event/asyncFuture.I index 07191a34c9..37dca79038 100644 --- a/panda/src/event/asyncFuture.I +++ b/panda/src/event/asyncFuture.I @@ -17,7 +17,6 @@ INLINE AsyncFuture:: AsyncFuture() : _manager(nullptr), - _cvar(nullptr), _future_state(FS_pending), _result(nullptr) { } @@ -28,7 +27,7 @@ AsyncFuture() : */ INLINE bool AsyncFuture:: done() const { - return (FutureState)AtomicAdjust::get(_future_state) != FS_pending; + return (FutureState)AtomicAdjust::get(_future_state) >= FS_finished; } /** @@ -46,6 +45,7 @@ cancelled() const { */ INLINE void AsyncFuture:: set_done_event(const string &done_event) { + nassertv(!done()); _done_event = done_event; } @@ -104,6 +104,97 @@ set_result(TypedReferenceCount *result) { } INLINE void AsyncFuture:: -set_result(TypedWritableReferenceCount *result) { - set_result(result, result); +set_result(const EventParameter &result) { + set_result(result.get_ptr(), result.get_ptr()); +} + +/** + * Creates a new future that returns `done()` when all of the contained + * futures are done. + * + * Calling `cancel()` on the returned future will result in all contained + * futures that have not yet finished to be cancelled. + */ +INLINE AsyncFuture *AsyncFuture:: +gather(Futures futures) { + if (futures.empty()) { + AsyncFuture *fut = new AsyncFuture; + fut->_future_state = (AtomicAdjust::Integer)FS_finished; + return fut; + } else if (futures.size() == 1) { + return futures[0].p(); + } else { + return (AsyncFuture *)new AsyncGatheringFuture(move(futures)); + } +} + +/** + * Tries to atomically lock the future, assuming it is pending. Returns false + * if it is not in the pending state, implying it's either done or about to be + * cancelled. + */ +INLINE bool AsyncFuture:: +try_lock_pending() { + return set_future_state(FS_locked_pending); +} + +/** + * Should be called after try_lock_pending() returns true. + */ +INLINE void AsyncFuture:: +unlock(FutureState new_state) { + nassertv(new_state != FS_locked_pending); + FutureState orig_state = (FutureState)AtomicAdjust::set(_future_state, (AtomicAdjust::Integer)new_state); + nassertv(orig_state == FS_locked_pending); +} + +/** + * Atomically changes the future state from pending to another state. Returns + * true if successful, false if the future was already done. + * Note that once a future is in a "done" state (ie. cancelled or finished) it + * can never change state again. + */ +INLINE bool AsyncFuture:: +set_future_state(FutureState state) { + FutureState orig_state = (FutureState) + AtomicAdjust::compare_and_exchange( + _future_state, + (AtomicAdjust::Integer)FS_pending, + (AtomicAdjust::Integer)state); + + while (orig_state == FS_locked_pending) { + Thread::force_yield(); + orig_state = (FutureState)AtomicAdjust::compare_and_exchange( + _future_state, + (AtomicAdjust::Integer)FS_pending, + (AtomicAdjust::Integer)state); + } + + return orig_state == FS_pending; +} + +/** + * Returns the number of futures that were passed to the constructor. + */ +INLINE size_t AsyncGatheringFuture:: +get_num_futures() const { + return _futures.size(); +} + +/** + * Returns the nth future that was passed into the constructor. + */ +INLINE AsyncFuture *AsyncGatheringFuture:: +get_future(size_t i) const { + nassertr(i < _futures.size(), nullptr); + return _futures[i].p(); +} + +/** + * Returns the result of the nth future that was passed into the constructor. + */ +INLINE TypedObject *AsyncGatheringFuture:: +get_result(size_t i) const { + nassertr(i < _futures.size(), nullptr); + return _futures[i]->get_result(); } diff --git a/panda/src/event/asyncFuture.cxx b/panda/src/event/asyncFuture.cxx index bb299239d7..f1aaaba842 100644 --- a/panda/src/event/asyncFuture.cxx +++ b/panda/src/event/asyncFuture.cxx @@ -20,31 +20,33 @@ #include "throw_event.h" TypeHandle AsyncFuture::_type_handle; +TypeHandle AsyncGatheringFuture::_type_handle; /** * Destroys the future. Assumes notify_done() has already been called. */ AsyncFuture:: ~AsyncFuture() { - delete _cvar; - nassertv(_waiting_tasks.empty()); + // If this triggers, the future destroyed before it was cancelled, which is + // not valid. Unless we should simply call cancel() here? + nassertv(_waiting.empty()); } /** * Cancels the future. Returns true if it was cancelled, or false if the - * future was already done. + * future was already done. Either way, done() will return true after this + * call returns. + * * In the case of a task, this is equivalent to remove(). */ bool AsyncFuture:: cancel() { - if (!done()) { - if (_manager == nullptr) { - _manager = AsyncTaskManager::get_global_ptr(); - } - MutexHolder holder(_manager->_lock); + if (set_future_state(FS_cancelled)) { + // The compare-swap operation succeeded, so schedule the callbacks. notify_done(false); return true; } else { + // It's already done. return false; } } @@ -55,6 +57,22 @@ cancel() { void AsyncFuture:: output(ostream &out) const { out << get_type(); + FutureState state = (FutureState)AtomicAdjust::get(_future_state); + switch (state) { + case FS_pending: + case FS_locked_pending: + out << " (pending)"; + break; + case FS_finished: + out << " (finished)"; + break; + case FS_cancelled: + out << " (cancelled)"; + break; + default: + out << " (**INVALID**)"; + break; + } } /** @@ -66,25 +84,18 @@ wait() { return; } - // If we don't have a manager, use the global one. - if (_manager == nullptr) { - _manager = AsyncTaskManager::get_global_ptr(); + PStatTimer timer(AsyncTaskChain::_wait_pcollector); + if (task_cat.is_debug()) { + task_cat.debug() + << "Waiting for future " << *this << "\n"; } - MutexHolder holder(_manager->_lock); - if (!done()) { - if (_cvar == nullptr) { - _cvar = new ConditionVarFull(_manager->_lock); - } - if (task_cat.is_debug()) { - task_cat.debug() - << "Waiting for future " << *this << "\n"; - } - PStatTimer timer(AsyncTaskChain::_wait_pcollector); - while (!done()) { - _cvar->wait(); - } - } + // Continue to yield while the future isn't done. It may be more efficient + // to use a condition variable, but let's not add the extra complexity + // unless we're sure that we need it. + do { + Thread::force_yield(); + } while (!done()); } /** @@ -96,46 +107,52 @@ wait(double timeout) { return; } - // If we don't have a manager, use the global one. - if (_manager == nullptr) { - _manager = AsyncTaskManager::get_global_ptr(); + PStatTimer timer(AsyncTaskChain::_wait_pcollector); + if (task_cat.is_debug()) { + task_cat.debug() + << "Waiting up to " << timeout << " seconds for future " << *this << "\n"; } - MutexHolder holder(_manager->_lock); - if (!done()) { - if (_cvar == nullptr) { - _cvar = new ConditionVarFull(_manager->_lock); - } - if (task_cat.is_debug()) { - task_cat.debug() - << "Waiting up to " << timeout << " seconds for future " << *this << "\n"; - } - PStatTimer timer(AsyncTaskChain::_wait_pcollector); - _cvar->wait(timeout); - } + // Continue to yield while the future isn't done. It may be more efficient + // to use a condition variable, but let's not add the extra complexity + // unless we're sure that we need it. + ClockObject *clock = ClockObject::get_global_clock(); + double end = clock->get_real_time() + timeout; + do { + Thread::force_yield(); + } while (!done() && clock->get_real_time() < end); } /** - * Schedules the done callbacks. Assumes the manager's lock is held, and that - * the future is currently in the 'pending' state. + * Schedules the done callbacks. Called after the future has just entered the + * 'done' state. * @param clean_exit true if finished successfully, false if cancelled. */ void AsyncFuture:: notify_done(bool clean_exit) { - nassertv(_manager != nullptr); - nassertv(_manager->_lock.debug_is_locked()); - nassertv(_future_state == FS_pending); + nassertv(done()); - pvector::iterator it; - for (it = _waiting_tasks.begin(); it != _waiting_tasks.end(); ++it) { - AsyncTask *task = *it; - nassertd(task->_manager == _manager) continue; - task->_state = AsyncTask::S_active; - task->_chain->_active.push_back(task); - --task->_chain->_num_awaiting_tasks; - unref_delete(task); + // This will only be called by the thread that managed to set the + // _future_state away from the "pending" state, so this is thread safe. + + Futures::iterator it; + for (it = _waiting.begin(); it != _waiting.end(); ++it) { + AsyncFuture *fut = *it; + if (fut->is_task()) { + // It's a task. Make it active again. + wake_task((AsyncTask *)fut); + } else { + // It's a gathering future. Decrease the pending count on it, and if + // we're the last one, call notify_done() on it. + AsyncGatheringFuture *gather = (AsyncGatheringFuture *)fut; + if (!AtomicAdjust::dec(gather->_num_pending)) { + if (gather->set_future_state(FS_finished)) { + gather->notify_done(true); + } + } + } } - _waiting_tasks.clear(); + _waiting.clear(); // For historical reasons, we don't send the "done event" if the future was // cancelled. @@ -144,17 +161,6 @@ notify_done(bool clean_exit) { event->add_parameter(EventParameter(this)); throw_event(move(event)); } - - nassertv_always(FS_pending == - (FutureState)AtomicAdjust::compare_and_exchange( - _future_state, - (AtomicAdjust::Integer)FS_pending, - (AtomicAdjust::Integer)(clean_exit ? FS_finished : FS_cancelled))); - - // Finally, notify any threads that may be waiting. - if (_cvar != nullptr) { - _cvar->notify_all(); - } } /** @@ -168,28 +174,203 @@ notify_done(bool clean_exit) { */ void AsyncFuture:: set_result(TypedObject *ptr, ReferenceCount *ref_ptr) { - nassertv(!done()); - // If we don't have a manager, use the global one. - if (_manager == nullptr) { - _manager = AsyncTaskManager::get_global_ptr(); + // We don't strictly need to lock the future since only one thread is + // allowed to call set_result(), but we might as well. + FutureState orig_state = (FutureState)AtomicAdjust:: + compare_and_exchange(_future_state, (AtomicAdjust::Integer)FS_pending, + (AtomicAdjust::Integer)FS_locked_pending); + + while (orig_state == FS_locked_pending) { + Thread::force_yield(); + orig_state = (FutureState)AtomicAdjust:: + compare_and_exchange(_future_state, (AtomicAdjust::Integer)FS_pending, + (AtomicAdjust::Integer)FS_locked_pending); + } + + if (orig_state == FS_pending) { + _result = ptr; + _result_ref = ref_ptr; + unlock(FS_finished); + + // OK, now our thread owns the _waiting vector et al. + notify_done(true); + + } else if (orig_state == FS_cancelled) { + // This was originally illegal, but there is a chance that the future was + // cancelled while another thread was setting the result. So, we drop + // this, but we can issue a warning. + task_cat.warning() + << "Ignoring set_result() called on cancelled " << *this << "\n"; + + } else { + task_cat.error() + << "set_result() was called on finished " << *this << "\n"; } - MutexHolder holder(_manager->_lock); - _result = ptr; - _result_ref = ref_ptr; - notify_done(true); } /** * Indicates that the given task is waiting for this future to complete. When - * the future is done, it will reactivate the given task. - * Assumes the manager's lock is held. + * the future is done, it will reactivate the given task. If this is called + * while the future is already done, schedules the task immediately. + * Assumes the manager's lock is not held. + * @returns true if the future was pending, false if it was already done. + */ +bool AsyncFuture:: +add_waiting_task(AsyncTask *task) { + nassertr(task->is_runnable(), false); + + // We have to make sure we're not going to change state while we're in the + // process of adding the task. + if (try_lock_pending()) { + if (_manager == nullptr) { + _manager = task->_manager; + } + + _waiting.push_back(task); + + // Unlock the state. + unlock(); + nassertr(task->_manager == nullptr || task->_manager == _manager, true); + return true; + } else { + // It's already done. Wake the task immediately. + wake_task(task); + return false; + } +} + +/** + * Reactivates the given task. Assumes the manager lock is not held. */ void AsyncFuture:: -add_waiting_task(AsyncTask *task) { - nassertv(!done()); - nassertv(_manager != nullptr); - nassertv(_manager->_lock.debug_is_locked()); - task->ref(); - _waiting_tasks.push_back(task); - nassertv(task->_manager == _manager); +wake_task(AsyncTask *task) { + cerr << "waking task\n"; + AsyncTaskManager *manager = task->_manager; + if (manager == nullptr) { + // If it's an unscheduled task, schedule it on the same manager as the + // rest of the waiting tasks. + manager = _manager; + if (manager == nullptr) { + manager = AsyncTaskManager::get_global_ptr(); + } + } + + MutexHolder holder(manager->_lock); + switch (task->_state) { + case AsyncTask::S_servicing_removed: + nassertv(task->_manager == _manager); + // Re-adding a self-removed task; this just means clearing the removed + // flag. + task->_state = AsyncTask::S_servicing; + return; + + case AsyncTask::S_inactive: + // Schedule it immediately. + nassertv(task->_manager == nullptr); + + if (task_cat.is_debug()) { + task_cat.debug() + << "Adding " << *task << " (woken by future " << *this << ")\n"; + } + + { + manager->_lock.release(); + task->upon_birth(manager); + manager->_lock.acquire(); + nassertv(task->_manager == nullptr && + task->_state == AsyncTask::S_inactive); + + AsyncTaskChain *chain = manager->do_find_task_chain(task->_chain_name); + if (chain == nullptr) { + task_cat.warning() + << "Creating implicit AsyncTaskChain " << task->_chain_name + << " for " << manager->get_type() << " " << manager->get_name() << "\n"; + chain = manager->do_make_task_chain(task->_chain_name); + } + chain->do_add(task); + } + return; + + case AsyncTask::S_awaiting: + nassertv(task->_manager == _manager); + task->_state = AsyncTask::S_active; + task->_chain->_active.push_back(task); + --task->_chain->_num_awaiting_tasks; + return; + + default: + nassertv(false); + return; + } +} + +/** + * @see AsyncFuture::gather + */ +AsyncGatheringFuture:: +AsyncGatheringFuture(AsyncFuture::Futures futures) : + _futures(move(futures)), + _num_pending(0) { + + bool any_pending = false; + + AsyncFuture::Futures::const_iterator it; + for (it = _futures.begin(); it != _futures.end(); ++it) { + AsyncFuture *fut = *it; + // If this returns true, the future is not yet done and we need to + // register ourselves with it. This creates a circular reference, but it + // is resolved when the future is completed or cancelled. + if (fut->try_lock_pending()) { + if (_manager == nullptr) { + _manager = fut->_manager; + } + fut->_waiting.push_back((AsyncFuture *)this); + AtomicAdjust::inc(_num_pending); + fut->unlock(); + any_pending = true; + } + } + if (!any_pending) { + // Start in the done state if all the futures we were passed are done. + // Note that it is only safe to set this member in this manner if indeed + // no other future holds a reference to us. + _future_state = (AtomicAdjust::Integer)FS_finished; + } +} + +/** + * Cancels all the futures. Returns true if any futures were cancelled. + * Makes sure that all the futures finish before this one is marked done, in + * order to maintain the guarantee that calling result() is safe when done() + * returns true. + */ +bool AsyncGatheringFuture:: +cancel() { + if (!done()) { + // Temporarily increase the pending count so that the notify_done() + // callbacks won't end up causing it to be set to "finished". + AtomicAdjust::inc(_num_pending); + + bool any_cancelled = false; + AsyncFuture::Futures::const_iterator it; + for (it = _futures.begin(); it != _futures.end(); ++it) { + AsyncFuture *fut = *it; + if (fut->cancel()) { + any_cancelled = true; + } + } + + // Now change state to "cancelled" and call the notify_done() callbacks. + // Don't call notify_done() if another thread has beaten us to it. + if (set_future_state(FS_cancelled)) { + notify_done(false); + } + + // Decreasing the pending count is kind of pointless now, so we do it only + // in a debug build. + nassertr(!AtomicAdjust::dec(_num_pending), any_cancelled); + return any_cancelled; + } else { + return false; + } } diff --git a/panda/src/event/asyncFuture.h b/panda/src/event/asyncFuture.h index 456c872c04..e3a835eea3 100644 --- a/panda/src/event/asyncFuture.h +++ b/panda/src/event/asyncFuture.h @@ -17,7 +17,7 @@ #include "pandabase.h" #include "typedReferenceCount.h" #include "typedWritableReferenceCount.h" -#include "conditionVar.h" +#include "eventParameter.h" #include "atomicAdjust.h" class AsyncTaskManager; @@ -30,21 +30,29 @@ class ConditionVarFull; * as well as register callbacks for this future's completion. * * An AsyncFuture can be awaited from within a coroutine or task. It keeps - * track of a list of tasks waiting for this future so that they are - * automatically reactivated upon this future's completion. + * track of tasks waiting for this future and automatically reactivates them + * upon this future's completion. * * A task itself is also a subclass of AsyncFuture. Other subclasses are - * possible, although subclassing is not necessary for most purposes. + * not generally necessary, except to override the function of `cancel()`. * - * The `done()` method is used to check whether the future has completed and - * a result is available (whether it is cancelled or finished). + * Until the future is done, it is "owned" by the resolver thread, though it's + * still legal for other threads to query its state. When the resolver thread + * resolves this future using `set_result()`, or any thread calls `cancel()`, + * it instantly enters the "done" state, after which the result becomes a + * read-only field that all threads can access. * - * To get the result of the future in C++, use `wait()` and `get_result()`. - * In Python, the functionality of both of those calls is wrapped into the - * `result()` method, which waits for the future to complete before either - * returning the result or throwing an exception if the future was cancelled. + * When the future returns true for done(), a thread can use cancelled() to + * determine whether the future was cancelled or get_result() to access the + * result of the operation. Not all operations define a meaningful result + * value, so some will always return nullptr. + * + * In Python, the `cancelled()`, `wait()` and `get_result()` methods are + * wrapped up into a single `result()` method which waits for the future to + * complete before either returning the result or throwing an exception if the + * future was cancelled. * However, it is preferable to use the `await` keyword when running from a - * coroutine. + * coroutine, which only suspends the current task and not the entire thread. * * This API aims to mirror and be compatible with Python's Future class. */ @@ -58,7 +66,7 @@ PUBLISHED: INLINE bool done() const; INLINE bool cancelled() const; - EXTENSION(PyObject *result(double timeout = -1.0) const); + EXTENSION(PyObject *result(PyObject *timeout = Py_None) const); virtual bool cancel(); @@ -66,45 +74,63 @@ PUBLISHED: INLINE const string &get_done_event() const; MAKE_PROPERTY(done_event, get_done_event, set_done_event); + EXTENSION(PyObject *add_done_callback(PyObject *self, PyObject *fn)); + + EXTENSION(static PyObject *gather(PyObject *args)); + virtual void output(ostream &out) const; - void wait(); - void wait(double timeout); + BLOCKING void wait(); + BLOCKING void wait(double timeout); INLINE void set_result(nullptr_t); INLINE void set_result(TypedObject *result); INLINE void set_result(TypedReferenceCount *result); - INLINE void set_result(TypedWritableReferenceCount *result); + INLINE void set_result(const EventParameter &result); public: + void set_result(TypedObject *ptr, ReferenceCount *ref_ptr); + INLINE TypedObject *get_result() const; INLINE void get_result(TypedObject *&ptr, ReferenceCount *&ref_ptr) const; + typedef pvector Futures; + INLINE static AsyncFuture *gather(Futures futures); + + virtual bool is_task() const {return false;} + void notify_done(bool clean_exit); + bool add_waiting_task(AsyncTask *task); private: - void set_result(TypedObject *ptr, ReferenceCount *ref_ptr); - void add_waiting_task(AsyncTask *task); + void wake_task(AsyncTask *task); protected: enum FutureState { + // Pending states FS_pending, + FS_locked_pending, + + // Done states FS_finished, FS_cancelled, }; + INLINE bool try_lock_pending(); + INLINE void unlock(FutureState new_state = FS_pending); + INLINE bool set_future_state(FutureState state); AsyncTaskManager *_manager; - ConditionVarFull *_cvar; TypedObject *_result; PT(ReferenceCount) _result_ref; AtomicAdjust::Integer _future_state; string _done_event; - // Tasks waiting for this one to complete. These are reference counted, but - // we can't store them in a PointerTo for circular dependency reasons. - pvector _waiting_tasks; + // Tasks and gathering futures waiting for this one to complete. + Futures _waiting; + friend class AsyncGatheringFuture; + friend class AsyncTaskChain; friend class PythonTask; public: @@ -130,6 +156,44 @@ INLINE ostream &operator << (ostream &out, const AsyncFuture &fut) { return out; }; +/** + * Specific future that collects the results of several futures. + */ +class EXPCL_PANDA_EVENT AsyncGatheringFuture FINAL : public AsyncFuture { +private: + AsyncGatheringFuture(AsyncFuture::Futures futures); + +public: + virtual bool cancel() override; + + INLINE size_t get_num_futures() const; + INLINE AsyncFuture *get_future(size_t i) const; + INLINE TypedObject *get_result(size_t i) const; + +private: + const Futures _futures; + AtomicAdjust::Integer _num_pending; + + friend class AsyncFuture; + +public: + static TypeHandle get_class_type() { + return _type_handle; + } + static void init_type() { + AsyncFuture::init_type(); + register_type(_type_handle, "AsyncGatheringFuture", + AsyncFuture::get_class_type()); + } + virtual TypeHandle get_type() const { + return get_class_type(); + } + virtual TypeHandle force_init_type() {init_type(); return get_class_type();} + +private: + static TypeHandle _type_handle; +}; + #include "asyncFuture.I" #endif diff --git a/panda/src/event/asyncFuture_ext.cxx b/panda/src/event/asyncFuture_ext.cxx index 49b1ab4d66..3150c09d7d 100644 --- a/panda/src/event/asyncFuture_ext.cxx +++ b/panda/src/event/asyncFuture_ext.cxx @@ -12,12 +12,16 @@ */ #include "asyncFuture_ext.h" +#include "asyncTaskSequence.h" +#include "eventParameter.h" +#include "paramValue.h" #include "pythonTask.h" #ifdef HAVE_PYTHON #ifndef CPPPARSER extern struct Dtool_PyTypedObject Dtool_AsyncFuture; +extern struct Dtool_PyTypedObject Dtool_ParamValueBase; extern struct Dtool_PyTypedObject Dtool_TypedObject; #endif @@ -32,7 +36,45 @@ static PyObject *get_done_result(const AsyncFuture *future) { // any PyObject value or raise an exception. const PythonTask *task = (const PythonTask *)future; return task->get_result(); + + } else if (future->is_of_type(AsyncTaskSequence::get_class_type())) { + // If it's an AsyncTaskSequence, get the result for each task. + const AsyncTaskSequence *task = (const AsyncTaskSequence *)future; + Py_ssize_t num_tasks = (Py_ssize_t)task->get_num_tasks(); + PyObject *results = PyTuple_New(num_tasks); + + for (Py_ssize_t i = 0; i < num_tasks; ++i) { + PyObject *result = get_done_result(task->get_task(i)); + if (result != nullptr) { + // This steals a reference. + PyTuple_SET_ITEM(results, i, result); + } else { + Py_DECREF(results); + return nullptr; + } + } + return results; + + } else if (future->is_of_type(AsyncGatheringFuture::get_class_type())) { + // If it's an AsyncGatheringFuture, get the result for each future. + const AsyncGatheringFuture *gather = (const AsyncGatheringFuture *)future; + Py_ssize_t num_futures = (Py_ssize_t)gather->get_num_futures(); + PyObject *results = PyTuple_New(num_futures); + + for (Py_ssize_t i = 0; i < num_futures; ++i) { + PyObject *result = get_done_result(gather->get_future((size_t)i)); + if (result != nullptr) { + // This steals a reference. + PyTuple_SET_ITEM(results, i, result); + } else { + Py_DECREF(results); + return nullptr; + } + } + return results; + } else { + // It's any other future. ReferenceCount *ref_ptr; TypedObject *ptr; future->get_result(ptr, ref_ptr); @@ -42,13 +84,36 @@ static PyObject *get_done_result(const AsyncFuture *future) { return Py_None; } + TypeHandle type = ptr->get_type(); + if (type.is_derived_from(ParamValueBase::get_class_type())) { + // If this is a ParamValueBase, return the 'value' property. + // EventStoreInt and Double are not exposed to Python for some reason. + if (type == EventStoreInt::get_class_type()) { + return Dtool_WrapValue(((EventStoreInt *)ptr)->get_value()); + } else if (type == EventStoreDouble::get_class_type()) { + return Dtool_WrapValue(((EventStoreDouble *)ptr)->get_value()); + } + + ParamValueBase *value = (ParamValueBase *)ptr; + PyObject *wrap = DTool_CreatePyInstanceTyped + ((void *)value, Dtool_ParamValueBase, false, false, type.get_index()); + if (wrap != nullptr) { + PyObject *value = PyObject_GetAttrString(wrap, "value"); + if (value != nullptr) { + return value; + } + PyErr_Restore(nullptr, nullptr, nullptr); + Py_DECREF(wrap); + } + } + if (ref_ptr != nullptr) { ref_ptr->ref(); } return DTool_CreatePyInstanceTyped ((void *)ptr, Dtool_TypedObject, (ref_ptr != nullptr), false, - ptr->get_type_index()); + type.get_index()); } } else { // If the future was cancelled, we should raise an exception. @@ -62,8 +127,8 @@ static PyObject *get_done_result(const AsyncFuture *future) { } // If we can't get that, we should pretend and make our own. if (exc_type == nullptr) { - exc_type = PyErr_NewExceptionWithDoc("concurrent.futures._base.CancelledError", - "The Future was cancelled.", + exc_type = PyErr_NewExceptionWithDoc((char*)"concurrent.futures._base.CancelledError", + (char*)"The Future was cancelled.", nullptr, nullptr); } } @@ -121,7 +186,7 @@ __await__(PyObject *self) { * raises TimeoutError. */ PyObject *Extension:: -result(double timeout) const { +result(PyObject *timeout) const { if (!_this->done()) { // Not yet done? Wait until it is done, or until a timeout occurs. But // first check to make sure we're not trying to deadlock the thread. @@ -136,11 +201,15 @@ result(double timeout) const { PyThreadState *_save; Py_UNBLOCK_THREADS #endif - //TODO: check why gcc and clang don't like infinity timeout. - if (cinf(timeout) || timeout < 0.0) { + if (timeout == Py_None) { _this->wait(); } else { - _this->wait(timeout); + PyObject *num = PyNumber_Float(timeout); + if (num != nullptr) { + _this->wait(PyFloat_AS_DOUBLE(num)); + } else { + return Dtool_Raise_ArgTypeError(timeout, 0, "result", "float"); + } } #if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS) Py_BLOCK_THREADS @@ -158,8 +227,8 @@ result(double timeout) const { } // If we can't get that, we should pretend and make our own. if (exc_type == nullptr) { - exc_type = PyErr_NewExceptionWithDoc("concurrent.futures._base.TimeoutError", - "The operation exceeded the given deadline.", + exc_type = PyErr_NewExceptionWithDoc((char*)"concurrent.futures._base.TimeoutError", + (char*)"The operation exceeded the given deadline.", nullptr, nullptr); } } @@ -172,4 +241,78 @@ result(double timeout) const { return get_done_result(_this); } +/** + * Schedules the given function to be run as soon as the future is complete. + * This is also called if the future is cancelled. + * If the future is already done, the callback is scheduled right away. + */ +PyObject *Extension:: +add_done_callback(PyObject *self, PyObject *fn) { + if (!PyCallable_Check(fn)) { + return Dtool_Raise_ArgTypeError(fn, 0, "add_done_callback", "callable"); + } + + PythonTask *task = new PythonTask(fn); + Py_DECREF(task->_args); + task->_args = PyTuple_Pack(1, self); + task->_append_task = false; + task->_ignore_return = true; + + // If this is an AsyncTask, make sure it is scheduled on the same chain. + if (_this->is_task()) { + AsyncTask *this_task = (AsyncTask *)_this; + task->set_task_chain(this_task->get_task_chain()); + } + + _this->add_waiting_task(task); + + Py_INCREF(Py_None); + return Py_None; +} + +/** + * Creates a new future that returns `done()` when all of the contained + * futures are done. + * + * Calling `cancel()` on the returned future will result in all contained + * futures that have not yet finished to be cancelled. + */ +PyObject *Extension:: +gather(PyObject *args) { + if (!PyTuple_Check(args)) { + return Dtool_Raise_TypeError("args is not a tuple"); + } + + Py_ssize_t size = Py_SIZE(args); + AsyncFuture::Futures futures; + futures.reserve(size); + + for (Py_ssize_t i = 0; i < size; ++i) { + PyObject *item = PyTuple_GET_ITEM(args, i); + if (DtoolInstance_Check(item)) { + AsyncFuture *fut = (AsyncFuture *)DtoolInstance_UPCAST(item, Dtool_AsyncFuture); + if (fut != nullptr) { + futures.push_back(fut); + continue; + } +#if PY_VERSION_HEX >= 0x03050000 + } else if (PyCoro_CheckExact(item)) { + // We allow passing in a coroutine instead of a future. This causes it + // to be scheduled as a task. + futures.push_back(new PythonTask(item)); + continue; +#endif + } + return Dtool_Raise_ArgTypeError(item, i, "gather", "coroutine, task or future"); + } + + AsyncFuture *future = AsyncFuture::gather(move(futures)); + if (future != nullptr) { + future->ref(); + return DTool_CreatePyInstanceTyped((void *)future, Dtool_AsyncFuture, true, false, future->get_type_index()); + } else { + return PyErr_NoMemory(); + } +} + #endif diff --git a/panda/src/event/asyncFuture_ext.h b/panda/src/event/asyncFuture_ext.h index 498225478d..21786ee17b 100644 --- a/panda/src/event/asyncFuture_ext.h +++ b/panda/src/event/asyncFuture_ext.h @@ -29,7 +29,11 @@ public: static PyObject *__await__(PyObject *self); static PyObject *__iter__(PyObject *self) { return __await__(self); } - PyObject *result(double timeout = -1.0) const; + PyObject *result(PyObject *timeout = Py_None) const; + + PyObject *add_done_callback(PyObject *self, PyObject *fn); + + static PyObject *gather(PyObject *args); }; #endif // HAVE_PYTHON diff --git a/panda/src/event/asyncTask.h b/panda/src/event/asyncTask.h index a941cd4b36..d514b2f5a3 100644 --- a/panda/src/event/asyncTask.h +++ b/panda/src/event/asyncTask.h @@ -104,6 +104,7 @@ protected: DoneStatus unlock_and_do_task(); virtual bool cancel() FINAL; + virtual bool is_task() const FINAL {return true;} virtual bool is_runnable(); virtual DoneStatus do_task(); diff --git a/panda/src/event/asyncTaskChain.cxx b/panda/src/event/asyncTaskChain.cxx index bda6113bd1..47299d31ef 100644 --- a/panda/src/event/asyncTaskChain.cxx +++ b/panda/src/event/asyncTaskChain.cxx @@ -778,7 +778,8 @@ cleanup_task(AsyncTask *task, bool upon_death, bool clean_exit) { _manager->remove_task_by_name(task); - if (upon_death && !task->done()) { + if (upon_death && task->set_future_state(clean_exit ? AsyncFuture::FS_finished + : AsyncFuture::FS_cancelled)) { task->notify_done(clean_exit); } diff --git a/panda/src/event/config_event.cxx b/panda/src/event/config_event.cxx index 86a8df8361..85727d2d7c 100644 --- a/panda/src/event/config_event.cxx +++ b/panda/src/event/config_event.cxx @@ -33,6 +33,7 @@ NotifyCategoryDef(task, ""); ConfigureFn(config_event) { AsyncFuture::init_type(); + AsyncGatheringFuture::init_type(); AsyncTask::init_type(); AsyncTaskChain::init_type(); AsyncTaskManager::init_type(); diff --git a/panda/src/event/pythonTask.cxx b/panda/src/event/pythonTask.cxx index bda2706407..996587885e 100644 --- a/panda/src/event/pythonTask.cxx +++ b/panda/src/event/pythonTask.cxx @@ -45,6 +45,7 @@ PythonTask(PyObject *func_or_coro, const string &name) : _exc_traceback(nullptr), _generator(nullptr), _future_done(nullptr), + _ignore_return(false), _retrieved_exception(false) { nassertv(func_or_coro != nullptr); @@ -169,9 +170,7 @@ get_args() { } this->ref(); - PyObject *self = - DTool_CreatePyInstanceTyped(this, Dtool_TypedReferenceCount, - true, false, get_type_index()); + PyObject *self = DTool_CreatePyInstance(this, Dtool_PythonTask, true, false); PyTuple_SET_ITEM(with_task, num_args, self); return with_task; @@ -588,20 +587,21 @@ do_python_task() { AsyncFuture *fut = (AsyncFuture *)DtoolInstance_UPCAST(result, Dtool_AsyncFuture); if (fut != nullptr) { // Suspend execution of this task until this other task has completed. - AsyncTaskManager *manager = fut->_manager; - if (manager == nullptr) { - manager = _manager; - fut->_manager = manager; - } - nassertr(manager == _manager, DS_interrupt); - MutexHolder holder(manager->_lock); - if (fut != (AsyncFuture *)this) { - if (!fut->done()) { + if (fut != (AsyncFuture *)this && !fut->done()) { + if (fut->is_task()) { + // This is actually a task, do we need to schedule it with the + // manager? This allows doing something like + // await Task.pause(1.0) + // directly instead of having to do: + // await taskMgr.add(Task.pause(1.0)) + AsyncTask *task = (AsyncTask *)fut; + _manager->add(task); + } + if (fut->add_waiting_task(this)) { if (task_cat.is_debug()) { task_cat.debug() << *this << " is now awaiting <" << *fut << ">.\n"; } - fut->add_waiting_task(this); } else { // The task is already done. Continue at next opportunity. if (task_cat.is_debug()) { @@ -664,7 +664,7 @@ do_python_task() { return DS_interrupt; } - if (result == Py_None) { + if (result == Py_None || _ignore_return) { Py_DECREF(result); return DS_done; } @@ -861,15 +861,10 @@ void PythonTask:: call_function(PyObject *function) { if (function != Py_None) { this->ref(); - PyObject *self = - DTool_CreatePyInstanceTyped(this, Dtool_TypedReferenceCount, - true, false, get_type_index()); - PyObject *args = Py_BuildValue("(O)", self); - Py_DECREF(self); - - PyObject *result = PyObject_CallObject(function, args); + PyObject *self = DTool_CreatePyInstance(this, Dtool_PythonTask, true, false); + PyObject *result = PyObject_CallFunctionObjArgs(function, self, nullptr); Py_XDECREF(result); - Py_DECREF(args); + Py_DECREF(self); } } diff --git a/panda/src/event/pythonTask.h b/panda/src/event/pythonTask.h index 869a0691ab..48826769de 100644 --- a/panda/src/event/pythonTask.h +++ b/panda/src/event/pythonTask.h @@ -26,7 +26,7 @@ * This class exists to allow association of a Python function or coroutine * with the AsyncTaskManager. */ -class PythonTask : public AsyncTask { +class PythonTask FINAL : public AsyncTask { PUBLISHED: PythonTask(PyObject *function = Py_None, const string &name = string()); virtual ~PythonTask(); @@ -123,6 +123,7 @@ private: PyObject *_future_done; bool _append_task; + bool _ignore_return; bool _registered_to_owner; mutable bool _retrieved_exception; diff --git a/tests/event/test_futures.py b/tests/event/test_futures.py index c353834257..0778605da4 100644 --- a/tests/event/test_futures.py +++ b/tests/event/test_futures.py @@ -159,6 +159,105 @@ def test_coro_exception(): task.result() +def test_future_gather(): + fut1 = core.AsyncFuture() + fut2 = core.AsyncFuture() + + # 0 and 1 arguments are special + assert core.AsyncFuture.gather().done() + assert core.AsyncFuture.gather(fut1) == fut1 + + # Gathering not-done futures + gather = core.AsyncFuture.gather(fut1, fut2) + assert not gather.done() + + # One future done + fut1.set_result(1) + assert not gather.done() + + # Two futures done + fut2.set_result(2) + assert gather.done() + + assert not gather.cancelled() + assert tuple(gather.result()) == (1, 2) + + +def test_future_gather_cancel_inner(): + fut1 = core.AsyncFuture() + fut2 = core.AsyncFuture() + + # Gathering not-done futures + gather = core.AsyncFuture.gather(fut1, fut2) + assert not gather.done() + + # One future cancelled + fut1.cancel() + assert not gather.done() + + # Two futures cancelled + fut2.set_result(2) + assert gather.done() + + assert not gather.cancelled() + with pytest.raises(CancelledError): + assert gather.result() + + +def test_future_gather_cancel_outer(): + fut1 = core.AsyncFuture() + fut2 = core.AsyncFuture() + + # Gathering not-done futures + gather = core.AsyncFuture.gather(fut1, fut2) + assert not gather.done() + + assert gather.cancel() + assert gather.done() + assert gather.cancelled() + + with pytest.raises(CancelledError): + assert gather.result() + + +def test_future_done_callback(): + fut = core.AsyncFuture() + + # Use the list hack since Python 2 doesn't have the "nonlocal" keyword. + called = [False] + def on_done(arg): + assert arg == fut + called[0] = True + + fut.add_done_callback(on_done) + fut.cancel() + assert fut.done() + + task_mgr = core.AsyncTaskManager.get_global_ptr() + task_mgr.poll() + assert called[0] + + +def test_future_done_callback_already_done(): + # Same as above, but with the future already done when add_done_callback + # is called. + fut = core.AsyncFuture() + fut.cancel() + assert fut.done() + + # Use the list hack since Python 2 doesn't have the "nonlocal" keyword. + called = [False] + def on_done(arg): + assert arg == fut + called[0] = True + + fut.add_done_callback(on_done) + + task_mgr = core.AsyncTaskManager.get_global_ptr() + task_mgr.poll() + assert called[0] + + def test_event_future(): queue = core.EventQueue() handler = core.EventHandler(queue)