From 80d67945d50708781ce7d838f4b9160380c4d527 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 31 Aug 2021 15:39:46 +0200 Subject: [PATCH 01/68] readme: Remove "Gold Sponsors" heading [skip ci] --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 3be11f9b58..70619892c7 100644 --- a/README.md +++ b/README.md @@ -250,10 +250,3 @@ to everyone who has donated! - -### Gold Sponsors -[![](https://opencollective.com/panda3d/tiers/gold-sponsor/0/avatar.svg?avatarHeight=128)](https://opencollective.com/panda3d/tiers/gold-sponsor/0/website) -[![](https://opencollective.com/panda3d/tiers/gold-sponsor/1/avatar.svg?avatarHeight=128)](https://opencollective.com/panda3d/tiers/gold-sponsor/1/website) -[![](https://opencollective.com/panda3d/tiers/gold-sponsor/2/avatar.svg?avatarHeight=128)](https://opencollective.com/panda3d/tiers/gold-sponsor/2/website) -[![](https://opencollective.com/panda3d/tiers/gold-sponsor/3/avatar.svg?avatarHeight=128)](https://opencollective.com/panda3d/tiers/gold-sponsor/3/website) -[![](https://opencollective.com/panda3d/tiers/gold-sponsor/4/avatar.svg?avatarHeight=128)](https://opencollective.com/panda3d/tiers/gold-sponsor/4/website) From 224d21352ae7ec8ff18682cad27b0fd667148dd5 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 14 Oct 2021 09:47:03 +0200 Subject: [PATCH 02/68] dist: Suppress warnings about missing __builtin__ and _winapi --- direct/src/dist/FreezeTool.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/direct/src/dist/FreezeTool.py b/direct/src/dist/FreezeTool.py index ea6919802f..e10c0aec09 100644 --- a/direct/src/dist/FreezeTool.py +++ b/direct/src/dist/FreezeTool.py @@ -125,6 +125,21 @@ if sys.version_info < (3, 0): 'direct.showbase.ShowBaseGlobal': ['builtins'], 'py._builtin': ['builtins'], }) +else: + # And ignore attempts to conditionally import __builtin__ in Python 3, + # suppressing a warning message. + ignoreImports.update({ + 'direct.p3d.AppRunner': ['__builtin__'], + 'direct.showbase.ContainerLeakDetector': ['__builtin__'], + 'direct.showbase.LeakDetectors': ['__builtin__'], + 'direct.showbase.MessengerLeakDetector': ['__builtin__'], + 'direct.showbase.ObjectReport': ['__builtin__'], + 'direct.showbase.ProfileSession': ['__builtin__'], + 'direct.showbase.PythonUtil': ['__builtin__'] + ignoreImports['direct.showbase.PythonUtil'], + 'direct.showbase.ShowBase': ['__builtin__'], + 'direct.showbase.ShowBaseGlobal': ['__builtin__'], + 'py._builtin': ['__builtin__'], + }) # These are overrides for specific modules. @@ -688,7 +703,7 @@ okMissing = [ 'email.Iterators', '_subprocess', 'gestalt', 'java.lang', 'direct.extensions_native.extensions_darwin', '_manylinux', 'collections.Iterable', 'collections.Mapping', 'collections.MutableMapping', - 'collections.Sequence', 'numpy_distutils', + 'collections.Sequence', 'numpy_distutils', '_winapi', ] class Freezer: From 98e3b1f03e69318df5f161eb0d4b6f9a59a1da14 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 14 Oct 2021 09:47:46 +0200 Subject: [PATCH 03/68] dist: update linecache module with missing __all__ item --- direct/src/dist/FreezeTool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/direct/src/dist/FreezeTool.py b/direct/src/dist/FreezeTool.py index e10c0aec09..d4af3c3ff8 100644 --- a/direct/src/dist/FreezeTool.py +++ b/direct/src/dist/FreezeTool.py @@ -147,7 +147,7 @@ overrideModules = { # Used by the warnings module, among others, to get line numbers. Since # we set __file__, this would cause it to try and extract Python code # lines from the main executable, which we don't want. - 'linecache': """__all__ = ["getline", "clearcache", "checkcache"] + 'linecache': """__all__ = ["getline", "clearcache", "checkcache", "lazycache"] cache = {} From e9b50927342e679d3e98e819fa4e7e9ee29e9600 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 14 Oct 2021 09:48:25 +0200 Subject: [PATCH 04/68] dist: Fix frozen modules not packaged (such as zipimport in Python 3.8+) --- direct/src/dist/FreezeTool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/direct/src/dist/FreezeTool.py b/direct/src/dist/FreezeTool.py index d4af3c3ff8..c8b42263fe 100644 --- a/direct/src/dist/FreezeTool.py +++ b/direct/src/dist/FreezeTool.py @@ -2563,9 +2563,9 @@ class PandaModuleFinder(modulefinder.ModuleFinder): (or self.path if None). Returns a tuple like (fp, path, stuff), where stuff is a tuple like (suffix, mode, type). """ - if imp.is_frozen(name): - # Don't pick up modules that are frozen into p3dpython. - raise ImportError("'%s' is a frozen module" % (name)) + #if imp.is_frozen(name): + # # Don't pick up modules that are frozen into p3dpython. + # raise ImportError("'%s' is a frozen module" % (name)) if parent is not None: fullname = parent.__name__+'.'+name From 64345c71b62ac15f85c4ab21b9ce74c819f44909 Mon Sep 17 00:00:00 2001 From: "Stephen A. Imhoff" Date: Thu, 14 Oct 2021 11:09:00 -0700 Subject: [PATCH 05/68] Update version number on release/1.10.x branch to 1.10.11 --- dtool/PandaVersion.pp | 4 ++-- setup.cfg | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dtool/PandaVersion.pp b/dtool/PandaVersion.pp index db041fd61f..d5cc7fea53 100644 --- a/dtool/PandaVersion.pp +++ b/dtool/PandaVersion.pp @@ -7,7 +7,7 @@ // place to put this. // Use spaces to separate the major, minor, and sequence numbers here. -#define PANDA_VERSION 1 10 10 +#define PANDA_VERSION 1 10 11 // This variable will be defined to false in the CVS repository, but // scripts that generate source tarballs and/or binary releases for @@ -30,7 +30,7 @@ // any distributable Panda3D packages. If you are making a Panda3D // build which you will be using to produce a distributable Panda3D // package, you should set this string appropriately. -#define PANDA_PACKAGE_VERSION +#define PANDA_PACKAGE_VERSION // We also define a version for the Panda3D plugin/runtime, // i.e. nppanda3d.dll, p3dactivex.ocx, and panda3d.exe. This is an diff --git a/setup.cfg b/setup.cfg index 44e2b5dbef..6fd743f75a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = Panda3D -version = 1.10.10 +version = 1.10.11 url = https://www.panda3d.org/ description = Panda3D is a framework for 3D rendering and game development for Python and C++ programs. license = Modified BSD License From de334f5597076029143b37d13fd5c0b2ba4e2f77 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Oct 2021 13:28:35 +0200 Subject: [PATCH 06/68] PythonUtil: remove deadlink from normalDistrib docstring --- direct/src/showbase/PythonUtil.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/direct/src/showbase/PythonUtil.py b/direct/src/showbase/PythonUtil.py index 7f8830fc85..ea08247823 100644 --- a/direct/src/showbase/PythonUtil.py +++ b/direct/src/showbase/PythonUtil.py @@ -1173,8 +1173,6 @@ def normalDistrib(a, b, gauss=random.gauss): uniformly onto the curve inside [a, b] ------------------------------------------------------------------------ - https://statweb.stanford.edu/~naras/jsm/NormalDensity/NormalDensity.html - The 68-95-99.7% Rule ==================== All normal density curves satisfy the following property which is often From 618e6f3feea3abc6edb382cc12ac3a9278b25af8 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Oct 2021 13:45:38 +0200 Subject: [PATCH 07/68] pgui: Fix slider thumb entering dragging state on button press This caused updating the slider while a button is pressed and the mouse is held over the thumb not to work. Fixes #1188 --- panda/src/pgui/pgSliderBar.cxx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/panda/src/pgui/pgSliderBar.cxx b/panda/src/pgui/pgSliderBar.cxx index e666d2a496..d423549ccb 100644 --- a/panda/src/pgui/pgSliderBar.cxx +++ b/panda/src/pgui/pgSliderBar.cxx @@ -605,8 +605,10 @@ item_press(PGItem *item, const MouseWatcherParameter ¶m) { ClockObject::get_global_clock()->get_frame_time() + scroll_initial_delay; } else if (item == _thumb_button) { - _scroll_button_held = nullptr; - begin_drag(); + if (((PGButton *)item)->has_click_button(param.get_button())) { + _scroll_button_held = nullptr; + begin_drag(); + } } } From cd9712270b1257bdcc67dd8eb1460c0e01991c29 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Oct 2021 13:51:04 +0200 Subject: [PATCH 08/68] dcparser: slight code cleanup in dcPacker.cxx --- direct/src/dcparser/dcPacker.cxx | 98 +++++++++++++++----------------- 1 file changed, 47 insertions(+), 51 deletions(-) diff --git a/direct/src/dcparser/dcPacker.cxx b/direct/src/dcparser/dcPacker.cxx index 810744c2ab..2a5a923bc2 100644 --- a/direct/src/dcparser/dcPacker.cxx +++ b/direct/src/dcparser/dcPacker.cxx @@ -637,71 +637,67 @@ pack_object(PyObject *object) { // had to add this for basic 64 and unsigned data to get packed right .. Not // sure if we can just do the rest this way.. - switch(pack_type) - { + switch (pack_type) { case PT_int64: - if(PyLong_Check(object)) - { - pack_int64(PyLong_AsLongLong(object)); - return; - } + if (PyLong_Check(object)) { + pack_int64(PyLong_AsLongLong(object)); + return; + } #if PY_MAJOR_VERSION < 3 - else if (PyInt_Check(object)) - { - pack_int64(PyInt_AsLong(object)); - return; - } + else if (PyInt_Check(object)) { + pack_int64(PyInt_AsLong(object)); + return; + } #endif - break; + break; + case PT_uint64: - if(PyLong_Check(object)) - { - pack_uint64(PyLong_AsUnsignedLongLong(object)); - return; - } + if (PyLong_Check(object)) { + pack_uint64(PyLong_AsUnsignedLongLong(object)); + return; + } #if PY_MAJOR_VERSION < 3 - else if(PyInt_Check(object)) - { - PyObject *obj1 = PyNumber_Long(object); - pack_int(PyLong_AsUnsignedLongLong(obj1)); - Py_DECREF(obj1); - return; - } + else if (PyInt_Check(object)) { + PyObject *obj1 = PyNumber_Long(object); + pack_int(PyLong_AsUnsignedLongLong(obj1)); + Py_DECREF(obj1); + return; + } #endif - break; + break; + case PT_int: - if(PyLong_Check(object)) - { - pack_int(PyLong_AsLong(object)); - return; - } + if (PyLong_Check(object)) { + pack_int(PyLong_AsLong(object)); + return; + } #if PY_MAJOR_VERSION < 3 - else if (PyInt_Check(object)) - { - pack_int(PyInt_AsLong(object)); - return; - } + else if (PyInt_Check(object)) { + pack_int(PyInt_AsLong(object)); + return; + } #endif - break; + break; + case PT_uint: - if(PyLong_Check(object)) - { - pack_uint(PyLong_AsUnsignedLong(object)); - return; - } + if (PyLong_Check(object)) { + pack_uint(PyLong_AsUnsignedLong(object)); + return; + } #if PY_MAJOR_VERSION < 3 - else if (PyInt_Check(object)) - { - PyObject *obj1 = PyNumber_Long(object); - pack_uint(PyLong_AsUnsignedLong(obj1)); - Py_DECREF(obj1); - return; - } + else if (PyInt_Check(object)) { + PyObject *obj1 = PyNumber_Long(object); + pack_uint(PyLong_AsUnsignedLong(obj1)); + Py_DECREF(obj1); + return; + } #endif - break; + break; + default: - break; + break; } + if (PyLong_Check(object)) { pack_int(PyLong_AsLong(object)); #if PY_MAJOR_VERSION < 3 From ca061c98a6bc3215fa740224663e5b0646ddd798 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Oct 2021 13:55:47 +0200 Subject: [PATCH 09/68] dcparser: Improve assertion error message for invalid UTF-8 data --- direct/src/dcparser/dcPacker.cxx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/direct/src/dcparser/dcPacker.cxx b/direct/src/dcparser/dcPacker.cxx index 2a5a923bc2..4ba255a78c 100644 --- a/direct/src/dcparser/dcPacker.cxx +++ b/direct/src/dcparser/dcPacker.cxx @@ -889,6 +889,10 @@ unpack_object() { unpack_string(str); #if PY_MAJOR_VERSION >= 3 object = PyUnicode_FromStringAndSize(str.data(), str.size()); + if (object == nullptr) { + nassert_raise("Unable to decode UTF-8 string; use blob type for binary data"); + return nullptr; + } #else object = PyString_FromStringAndSize(str.data(), str.size()); #endif From 594a26662ec3e6cbf1916a3831cb0c5ace81176a Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Oct 2021 13:59:39 +0200 Subject: [PATCH 10/68] dist: Automatically include cacert.pem when depending on certifi --- direct/src/dist/commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index 47422a7e47..061f641e67 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -113,6 +113,7 @@ PACKAGE_DATA_DIRS = { ('cefpython3/Chromium Embedded Framework.framework/Chromium Embedded Framework', '', {'PKG_DATA_MAKE_EXECUTABLE'}), ], 'pytz': [('pytz/zoneinfo/*', 'zoneinfo', ())], + 'certifi': [('certifi/cacert.pem', '', {})], } # Some dependencies have extra directories that need to be scanned for DLLs. From 9b55279fdefb2cd92cf9fc0c49c3f66f1965749c Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Oct 2021 14:00:04 +0200 Subject: [PATCH 11/68] glgsg: Minor code cleanup in FBO code --- panda/src/glstuff/glGraphicsBuffer_src.I | 16 ++++++++++------ panda/src/glstuff/glGraphicsBuffer_src.h | 14 ++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/panda/src/glstuff/glGraphicsBuffer_src.I b/panda/src/glstuff/glGraphicsBuffer_src.I index 021116b7ff..4a3250eee8 100644 --- a/panda/src/glstuff/glGraphicsBuffer_src.I +++ b/panda/src/glstuff/glGraphicsBuffer_src.I @@ -11,14 +11,18 @@ * @date 2006-01-15 */ +/** + * + */ INLINE int CLP(GraphicsBuffer):: -get_multisample_count() -{ - return _requested_multisamples; +get_multisample_count() { + return _requested_multisamples; } +/** + * + */ INLINE int CLP(GraphicsBuffer):: -get_coverage_sample_count() -{ - return _requested_coverage_samples; +get_coverage_sample_count() { + return _requested_coverage_samples; } diff --git a/panda/src/glstuff/glGraphicsBuffer_src.h b/panda/src/glstuff/glGraphicsBuffer_src.h index c383a0a5d4..68c2dab74f 100644 --- a/panda/src/glstuff/glGraphicsBuffer_src.h +++ b/panda/src/glstuff/glGraphicsBuffer_src.h @@ -23,11 +23,14 @@ * ARB_draw_buffers extensions. This design has significant advantages over * the older wglGraphicsBuffer and glxGraphicsBuffer: * - * * Can export depth and stencil. * Supports auxiliary bitplanes. * - * Supports non-power-of-two padding. * Supports tracking of host window - * size. * Supports cumulative render-to-texture. * Faster than pbuffers. * - * Can render onto a texture without clearing it first. * Supports - * multisample antialiased rendering. + * - Can export depth and stencil. + * - Supports auxiliary bitplanes. + * - Supports non-power-of-two padding. + * - Supports tracking of host window size. + * - Supports cumulative render-to-texture. + * - Faster than pbuffers. + * - Can render onto a texture without clearing it first. + * - Supports multisample antialiased rendering. * * Some of these deserve a little explanation. Auxiliary bitplanes are * additional bitplanes above and beyond the normal depth,stencil,color. One @@ -45,7 +48,6 @@ * EXT_framebuffer_blit to allow for multisample antialiasing these offscreen * render targets. If these extensions are unavailable the buffer will render * as if multisamples is 0. - * */ class EXPCL_GL CLP(GraphicsBuffer) : public GraphicsBuffer { public: From 57d7bc2f536048142ff38b948ccf2ee0bba00f97 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Oct 2021 14:00:25 +0200 Subject: [PATCH 12/68] display: document make_texture_buffer() behavior when size is 0 Fixes panda3d/panda3d-docs#100 --- panda/src/display/graphicsOutput.cxx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/panda/src/display/graphicsOutput.cxx b/panda/src/display/graphicsOutput.cxx index 1f97ade9e7..58d4099a45 100644 --- a/panda/src/display/graphicsOutput.cxx +++ b/panda/src/display/graphicsOutput.cxx @@ -834,6 +834,9 @@ get_active_display_region(int n) const { * which will be a texture suitable for applying to geometry within the scene * rendered into this window. * + * If you pass zero as the buffer size, the buffer will have the same size as + * the host window, and will automatically be resized when the host window is. + * * If tex is not NULL, it is the texture that will be set up for rendering * into; otherwise, a new Texture object will be created. In either case, the * target texture can be retrieved from the return value with From 2ae6e5c6cec3e9cd5b46c3151429fc48c7a5c336 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Oct 2021 17:28:01 +0200 Subject: [PATCH 13/68] glgsg: Remove unused variable from glGSG --- panda/src/glstuff/glGraphicsStateGuardian_src.h | 1 - 1 file changed, 1 deletion(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index 8aecf8c852..c0af1f5228 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -722,7 +722,6 @@ protected: typedef pmap DirectionalLights; DirectionalLights _dlights; - int _pass_number; GLuint _geom_display_list; GLuint _current_vbuffer_index; GLuint _current_ibuffer_index; From b7b2ce0cd67f153d11bd3cca33f07215317543a8 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Oct 2021 18:06:18 +0200 Subject: [PATCH 14/68] Replace FreeNode with Libera Chat [skip ci] --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bab8a9fcfc..4be6705cc4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,7 @@ It is important for acceptance that the change is implemented in a way that fits the general design principles of the Panda3D API, and fits well with the general priorities of the team. Therefore, prior discussion with other developers is critical. Issues can be used to facilitate this, but we also invite you to -visit the #development channel on Discord (or #panda3d-devel on FreeNode IRC). +visit the #development channel on Discord (or #panda3d-devel on Libera Chat). We also recommend that you familiarize yourself with the established coding style and design patterns of Panda3D, to reduce the amount of changes that have From 77c6bc199db6035eac010c9d6b5379db7f0be9e7 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 27 Oct 2021 10:46:43 +0200 Subject: [PATCH 15/68] gobj: Remove unused member variable from TexturePeeker --- panda/src/gobj/texturePeeker.h | 1 - 1 file changed, 1 deletion(-) diff --git a/panda/src/gobj/texturePeeker.h b/panda/src/gobj/texturePeeker.h index 2894e349cc..011766b1cc 100644 --- a/panda/src/gobj/texturePeeker.h +++ b/panda/src/gobj/texturePeeker.h @@ -89,7 +89,6 @@ private: int _y_size; int _z_size; int _is_cube; - int _unused1; int _pixel_width; Texture::Format _format; Texture::ComponentType _component_type; From a4ea476cceb02d5c58142e148d4ff177134f013c Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 27 Oct 2021 10:53:35 +0200 Subject: [PATCH 16/68] linmath: Implement read-only buffer protocol support for vectors Fixes #1194 --- panda/src/linmath/lvecBase2_ext_src.I | 40 +++++++++++++++++++++++++++ panda/src/linmath/lvecBase2_ext_src.h | 2 ++ panda/src/linmath/lvecBase2_src.h | 2 ++ panda/src/linmath/lvecBase3_ext_src.I | 40 +++++++++++++++++++++++++++ panda/src/linmath/lvecBase3_ext_src.h | 2 ++ panda/src/linmath/lvecBase3_src.h | 2 ++ panda/src/linmath/lvecBase4_ext_src.I | 40 +++++++++++++++++++++++++++ panda/src/linmath/lvecBase4_ext_src.h | 2 ++ panda/src/linmath/lvecBase4_src.h | 2 ++ tests/linmath/test_lvector2.py | 8 ++++++ tests/linmath/test_lvector3.py | 9 ++++++ tests/linmath/test_lvector4.py | 10 +++++++ 12 files changed, 159 insertions(+) diff --git a/panda/src/linmath/lvecBase2_ext_src.I b/panda/src/linmath/lvecBase2_ext_src.I index 5d731fec7a..5b36e339ec 100644 --- a/panda/src/linmath/lvecBase2_ext_src.I +++ b/panda/src/linmath/lvecBase2_ext_src.I @@ -357,5 +357,45 @@ __ceil__(PyObject *self) const { return py_vec; } +/** + * + */ +INLINE_LINMATH int Extension:: +__getbuffer__(PyObject *self, Py_buffer *view, int flags) const { + if ((flags & PyBUF_WRITABLE) == PyBUF_WRITABLE) { + PyErr_SetString(PyExc_BufferError, + "Object is not writable."); + return -1; + } + + static const char format[2] = {FLOATTOKEN, 0}; + static const Py_ssize_t shape = FLOATNAME(LVecBase2)::num_components; + + Py_INCREF(self); + + view->buf = (void *)_this->get_data(); + view->obj = self; + view->len = 2 * sizeof(FLOATTYPE); + view->readonly = 1; + view->itemsize = sizeof(FLOATTYPE); + view->format = nullptr; + if ((flags & PyBUF_FORMAT) == PyBUF_FORMAT) { + view->format = (char *)format; + } + view->ndim = 1; + view->shape = nullptr; + if ((flags & PyBUF_ND) == PyBUF_ND) { + view->shape = (Py_ssize_t *)&shape; + } + view->strides = nullptr; + if ((flags & PyBUF_STRIDES) == PyBUF_STRIDES) { + view->strides = &view->itemsize; + } + view->suboffsets = nullptr; + view->internal = nullptr; + + return 0; +} + #undef PYNUMBER_FLOATTYPE #undef PY_AS_FLOATTYPE diff --git a/panda/src/linmath/lvecBase2_ext_src.h b/panda/src/linmath/lvecBase2_ext_src.h index dad3807548..372c805517 100644 --- a/panda/src/linmath/lvecBase2_ext_src.h +++ b/panda/src/linmath/lvecBase2_ext_src.h @@ -34,6 +34,8 @@ public: INLINE_LINMATH PyObject *__round__(PyObject *self) const; INLINE_LINMATH PyObject *__floor__(PyObject *self) const; INLINE_LINMATH PyObject *__ceil__(PyObject *self) const; + + INLINE_LINMATH int __getbuffer__(PyObject *self, Py_buffer *view, int flags) const; }; #include "lvecBase2_ext_src.I" diff --git a/panda/src/linmath/lvecBase2_src.h b/panda/src/linmath/lvecBase2_src.h index 9e6af9170f..7f451b67b3 100644 --- a/panda/src/linmath/lvecBase2_src.h +++ b/panda/src/linmath/lvecBase2_src.h @@ -160,6 +160,8 @@ PUBLISHED: INLINE_LINMATH void write_datagram(Datagram &destination) const; INLINE_LINMATH void read_datagram(DatagramIterator &source); + EXTENSION(INLINE_LINMATH int __getbuffer__(PyObject *self, Py_buffer *view, int flags) const); + public: // The underlying implementation is via the Eigen library, if available. diff --git a/panda/src/linmath/lvecBase3_ext_src.I b/panda/src/linmath/lvecBase3_ext_src.I index 85486df43d..c59add2465 100644 --- a/panda/src/linmath/lvecBase3_ext_src.I +++ b/panda/src/linmath/lvecBase3_ext_src.I @@ -370,5 +370,45 @@ __ceil__(PyObject *self) const { return py_vec; } +/** + * + */ +INLINE_LINMATH int Extension:: +__getbuffer__(PyObject *self, Py_buffer *view, int flags) const { + if ((flags & PyBUF_WRITABLE) == PyBUF_WRITABLE) { + PyErr_SetString(PyExc_BufferError, + "Object is not writable."); + return -1; + } + + static const char format[2] = {FLOATTOKEN, 0}; + static const Py_ssize_t shape = FLOATNAME(LVecBase3)::num_components; + + Py_INCREF(self); + + view->buf = (void *)_this->get_data(); + view->obj = self; + view->len = 3 * sizeof(FLOATTYPE); + view->readonly = 1; + view->itemsize = sizeof(FLOATTYPE); + view->format = nullptr; + if ((flags & PyBUF_FORMAT) == PyBUF_FORMAT) { + view->format = (char *)format; + } + view->ndim = 1; + view->shape = nullptr; + if ((flags & PyBUF_ND) == PyBUF_ND) { + view->shape = (Py_ssize_t *)&shape; + } + view->strides = nullptr; + if ((flags & PyBUF_STRIDES) == PyBUF_STRIDES) { + view->strides = &view->itemsize; + } + view->suboffsets = nullptr; + view->internal = nullptr; + + return 0; +} + #undef PYNUMBER_FLOATTYPE #undef PY_AS_FLOATTYPE diff --git a/panda/src/linmath/lvecBase3_ext_src.h b/panda/src/linmath/lvecBase3_ext_src.h index 6d3d9c85a9..e5e509147e 100644 --- a/panda/src/linmath/lvecBase3_ext_src.h +++ b/panda/src/linmath/lvecBase3_ext_src.h @@ -34,6 +34,8 @@ public: INLINE_LINMATH PyObject *__round__(PyObject *self) const; INLINE_LINMATH PyObject *__floor__(PyObject *self) const; INLINE_LINMATH PyObject *__ceil__(PyObject *self) const; + + INLINE_LINMATH int __getbuffer__(PyObject *self, Py_buffer *view, int flags) const; }; #include "lvecBase3_ext_src.I" diff --git a/panda/src/linmath/lvecBase3_src.h b/panda/src/linmath/lvecBase3_src.h index 64785a4747..8594f1b921 100644 --- a/panda/src/linmath/lvecBase3_src.h +++ b/panda/src/linmath/lvecBase3_src.h @@ -181,6 +181,8 @@ PUBLISHED: INLINE_LINMATH void write_datagram(Datagram &destination) const; INLINE_LINMATH void read_datagram(DatagramIterator &source); + EXTENSION(INLINE_LINMATH int __getbuffer__(PyObject *self, Py_buffer *view, int flags) const); + public: // The underlying implementation is via the Eigen library, if available. diff --git a/panda/src/linmath/lvecBase4_ext_src.I b/panda/src/linmath/lvecBase4_ext_src.I index b18012cbdc..d1be8a5316 100644 --- a/panda/src/linmath/lvecBase4_ext_src.I +++ b/panda/src/linmath/lvecBase4_ext_src.I @@ -388,5 +388,45 @@ __ceil__(PyObject *self) const { return py_vec; } +/** + * + */ +INLINE_LINMATH int Extension:: +__getbuffer__(PyObject *self, Py_buffer *view, int flags) const { + if ((flags & PyBUF_WRITABLE) == PyBUF_WRITABLE) { + PyErr_SetString(PyExc_BufferError, + "Object is not writable."); + return -1; + } + + static const char format[2] = {FLOATTOKEN, 0}; + static const Py_ssize_t shape = FLOATNAME(LVecBase4)::num_components; + + Py_INCREF(self); + + view->buf = (void *)_this->get_data(); + view->obj = self; + view->len = 4 * sizeof(FLOATTYPE); + view->readonly = 1; + view->itemsize = sizeof(FLOATTYPE); + view->format = nullptr; + if ((flags & PyBUF_FORMAT) == PyBUF_FORMAT) { + view->format = (char *)format; + } + view->ndim = 1; + view->shape = nullptr; + if ((flags & PyBUF_ND) == PyBUF_ND) { + view->shape = (Py_ssize_t *)&shape; + } + view->strides = nullptr; + if ((flags & PyBUF_STRIDES) == PyBUF_STRIDES) { + view->strides = &view->itemsize; + } + view->suboffsets = nullptr; + view->internal = nullptr; + + return 0; +} + #undef PYNUMBER_FLOATTYPE #undef PY_AS_FLOATTYPE diff --git a/panda/src/linmath/lvecBase4_ext_src.h b/panda/src/linmath/lvecBase4_ext_src.h index da9dcd3d4b..b21e1b7ab7 100644 --- a/panda/src/linmath/lvecBase4_ext_src.h +++ b/panda/src/linmath/lvecBase4_ext_src.h @@ -34,6 +34,8 @@ public: INLINE_LINMATH PyObject *__round__(PyObject *self) const; INLINE_LINMATH PyObject *__floor__(PyObject *self) const; INLINE_LINMATH PyObject *__ceil__(PyObject *self) const; + + INLINE_LINMATH int __getbuffer__(PyObject *self, Py_buffer *view, int flags) const; }; #include "lvecBase4_ext_src.I" diff --git a/panda/src/linmath/lvecBase4_src.h b/panda/src/linmath/lvecBase4_src.h index e5a23374a4..129b427c2d 100644 --- a/panda/src/linmath/lvecBase4_src.h +++ b/panda/src/linmath/lvecBase4_src.h @@ -187,6 +187,8 @@ PUBLISHED: INLINE_LINMATH void write_datagram(Datagram &destination) const; INLINE_LINMATH void read_datagram(DatagramIterator &source); + EXTENSION(INLINE_LINMATH int __getbuffer__(PyObject *self, Py_buffer *view, int flags) const); + public: // The underlying implementation is via the Eigen library, if available. diff --git a/tests/linmath/test_lvector2.py b/tests/linmath/test_lvector2.py index bbf658dead..84bda15444 100644 --- a/tests/linmath/test_lvector2.py +++ b/tests/linmath/test_lvector2.py @@ -143,3 +143,11 @@ def test_vec2_floordiv(type): v = type(i) v //= -j assert v.x == i // -j + + +def test_vec2_buffer(): + v = Vec2(1.5, -10.0) + m = memoryview(v) + assert len(m) == 2 + assert m[0] == 1.5 + assert m[1] == -10.0 diff --git a/tests/linmath/test_lvector3.py b/tests/linmath/test_lvector3.py index 1b6dc02995..e12a1650f0 100644 --- a/tests/linmath/test_lvector3.py +++ b/tests/linmath/test_lvector3.py @@ -128,3 +128,12 @@ def test_vec3_floordiv(type): v = type(i) v //= -j assert v.x == i // -j + + +def test_vec3_buffer(): + v = Vec3(0.5, 2.0, -10.0) + m = memoryview(v) + assert len(m) == 3 + assert m[0] == 0.5 + assert m[1] == 2.0 + assert m[2] == -10.0 diff --git a/tests/linmath/test_lvector4.py b/tests/linmath/test_lvector4.py index 8e651472a6..060953fbc8 100644 --- a/tests/linmath/test_lvector4.py +++ b/tests/linmath/test_lvector4.py @@ -144,3 +144,13 @@ def test_vec4_floordiv(type): v = type(i) v //= -j assert v.x == i // -j + + +def test_vec4_buffer(): + v = Vec4(0, 0.5, 2.0, -4.0) + m = memoryview(v) + assert len(m) == 4 + assert m[0] == 0 + assert m[1] == 0.5 + assert m[2] == 2.0 + assert m[3] == -4.0 From ad7cbcd4ec447d4423e05b0e0b813327bd722bf3 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 27 Oct 2021 11:20:19 +0200 Subject: [PATCH 17/68] bullet: Fix memory leak getting persistent manifolds from Python These classes ought to be returned by value. Cleaner fix will be in master. Fixes #1193 --- panda/src/bullet/bulletPersistentManifold.cxx | 17 +++++++++++++++++ panda/src/bullet/bulletPersistentManifold.h | 5 +++-- panda/src/bullet/bulletWorld.cxx | 13 +++++++++++++ panda/src/bullet/bulletWorld.h | 5 +++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/panda/src/bullet/bulletPersistentManifold.cxx b/panda/src/bullet/bulletPersistentManifold.cxx index 9876ff35ee..f5e96d0464 100644 --- a/panda/src/bullet/bulletPersistentManifold.cxx +++ b/panda/src/bullet/bulletPersistentManifold.cxx @@ -49,6 +49,8 @@ get_contact_processing_threshold() const { */ void BulletPersistentManifold:: clear_manifold() { + nassertv_always(_manifold != nullptr); + LightMutexHolder holder(BulletWorld::get_global_lock()); _manifold->clearManifold(); @@ -59,6 +61,8 @@ clear_manifold() { */ PandaNode *BulletPersistentManifold:: get_node0() { + nassertr_always(_manifold != nullptr, nullptr); + LightMutexHolder holder(BulletWorld::get_global_lock()); #if BT_BULLET_VERSION >= 281 @@ -75,6 +79,8 @@ get_node0() { */ PandaNode *BulletPersistentManifold:: get_node1() { + nassertr_always(_manifold != nullptr, nullptr); + LightMutexHolder holder(BulletWorld::get_global_lock()); #if BT_BULLET_VERSION >= 281 @@ -91,6 +97,8 @@ get_node1() { */ int BulletPersistentManifold:: get_num_manifold_points() const { + nassertr_always(_manifold != nullptr, 0); + LightMutexHolder holder(BulletWorld::get_global_lock()); return _manifold->getNumContacts(); @@ -107,3 +115,12 @@ get_manifold_point(int idx) const { return new BulletManifoldPoint(_manifold->getContactPoint(idx)); } + +/** + * + */ +BulletManifoldPoint BulletPersistentManifold:: +__get_manifold_point(int idx) const { + LightMutexHolder holder(BulletWorld::get_global_lock()); + return BulletManifoldPoint(_manifold->getContactPoint(idx)); +} diff --git a/panda/src/bullet/bulletPersistentManifold.h b/panda/src/bullet/bulletPersistentManifold.h index 25feb57493..e97192f81e 100644 --- a/panda/src/bullet/bulletPersistentManifold.h +++ b/panda/src/bullet/bulletPersistentManifold.h @@ -35,7 +35,8 @@ PUBLISHED: int get_num_manifold_points() const; BulletManifoldPoint *get_manifold_point(int idx) const; - MAKE_SEQ(get_manifold_points, get_num_manifold_points, get_manifold_point); + BulletManifoldPoint __get_manifold_point(int idx) const; + MAKE_SEQ(get_manifold_points, get_num_manifold_points, __get_manifold_point); PN_stdfloat get_contact_breaking_threshold() const; PN_stdfloat get_contact_processing_threshold() const; @@ -44,7 +45,7 @@ PUBLISHED: MAKE_PROPERTY(node0, get_node0); MAKE_PROPERTY(node1, get_node1); - MAKE_SEQ_PROPERTY(manifold_points, get_num_manifold_points, get_manifold_point); + MAKE_SEQ_PROPERTY(manifold_points, get_num_manifold_points, __get_manifold_point); MAKE_PROPERTY(contact_breaking_threshold, get_contact_breaking_threshold); MAKE_PROPERTY(contact_processing_threshold, get_contact_processing_threshold); diff --git a/panda/src/bullet/bulletWorld.cxx b/panda/src/bullet/bulletWorld.cxx index 9884b45bd7..5590f6aee6 100644 --- a/panda/src/bullet/bulletWorld.cxx +++ b/panda/src/bullet/bulletWorld.cxx @@ -1070,6 +1070,19 @@ get_manifold(int idx) const { return (ptr) ? new BulletPersistentManifold(ptr) : nullptr; } +/** + * + */ +BulletPersistentManifold BulletWorld:: +__get_manifold(int idx) const { + LightMutexHolder holder(get_global_lock()); + + nassertr(idx < _dispatcher->getNumManifolds(), BulletPersistentManifold(nullptr)); + + btPersistentManifold *ptr = _dispatcher->getManifoldByIndexInternal(idx); + return BulletPersistentManifold(ptr); +} + /** * */ diff --git a/panda/src/bullet/bulletWorld.h b/panda/src/bullet/bulletWorld.h index 67607923dd..a910e5f503 100644 --- a/panda/src/bullet/bulletWorld.h +++ b/panda/src/bullet/bulletWorld.h @@ -128,7 +128,8 @@ PUBLISHED: // Manifolds int get_num_manifolds() const; BulletPersistentManifold *get_manifold(int idx) const; - MAKE_SEQ(get_manifolds, get_num_manifolds, get_manifold); + BulletPersistentManifold __get_manifold(int idx) const; + MAKE_SEQ(get_manifolds, get_num_manifolds, __get_manifold); // Collision filtering void set_group_collision_flag(unsigned int group1, unsigned int group2, bool enable); @@ -168,7 +169,7 @@ PUBLISHED: MAKE_SEQ_PROPERTY(characters, get_num_characters, get_character); MAKE_SEQ_PROPERTY(vehicles, get_num_vehicles, get_vehicle); MAKE_SEQ_PROPERTY(constraints, get_num_constraints, get_constraint); - MAKE_SEQ_PROPERTY(manifolds, get_num_manifolds, get_manifold); + MAKE_SEQ_PROPERTY(manifolds, get_num_manifolds, __get_manifold); MAKE_PROPERTY(force_update_all_aabbs, get_force_update_all_aabbs, set_force_update_all_aabbs); From 3d8f824081a9cd753309ccd4e62fc3e0a5d9aba0 Mon Sep 17 00:00:00 2001 From: Fireclaw Date: Mon, 8 Jun 2020 22:10:28 +0200 Subject: [PATCH 18/68] Add Distributed Network samples 1: Simple client-server connection 2: Client-server connection with timeManager 3: Create distributed objects, sending and receiving messages 4: Distributed model file 5: Simple text chat 6: Simple smooth moving actor --- .../networking/01-simple-connection/client.py | 123 ++++++++++++++ .../networking/01-simple-connection/server.py | 31 ++++ .../02-time-synced-connection/AIRepository.py | 81 ++++++++++ .../02-time-synced-connection/client.py | 136 ++++++++++++++++ .../02-time-synced-connection/server.py | 34 ++++ .../03-distributed-node/AIDGameObject.py | 29 ++++ .../03-distributed-node/AIDGameObjectAI.py | 25 +++ .../03-distributed-node/AIRepository.py | 86 ++++++++++ .../03-distributed-node/ClientRepository.py | 130 +++++++++++++++ .../03-distributed-node/DGameObject.py | 15 ++ .../03-distributed-node/ServerRepository.py | 20 +++ .../networking/03-distributed-node/client.py | 136 ++++++++++++++++ .../networking/03-distributed-node/sample.dc | 17 ++ .../networking/03-distributed-node/server.py | 18 +++ .../04-distributed-model/AIRepository.py | 81 ++++++++++ .../04-distributed-model/ClientRepository.py | 116 ++++++++++++++ .../networking/04-distributed-model/DModel.py | 61 +++++++ .../04-distributed-model/ServerRepository.py | 20 +++ .../networking/04-distributed-model/client.py | 43 +++++ .../networking/04-distributed-model/sample.dc | 3 + .../networking/04-distributed-model/server.py | 18 +++ .../networking/05-small-chat/AIRepository.py | 81 ++++++++++ .../05-small-chat/ClientRepository.py | 104 ++++++++++++ samples/networking/05-small-chat/client.py | 85 ++++++++++ samples/networking/05-small-chat/message.py | 25 +++ samples/networking/05-small-chat/sample.dc | 5 + samples/networking/05-small-chat/server.py | 34 ++++ .../06-simple-avatar/AIRepository.py | 81 ++++++++++ .../06-simple-avatar/ClientRepository.py | 103 ++++++++++++ .../DistributedSmoothActor.py | 50 ++++++ .../06-simple-avatar/ServerRepository.py | 20 +++ samples/networking/06-simple-avatar/client.py | 151 ++++++++++++++++++ .../06-simple-avatar/models/ralph-run.egg.pz | Bin 0 -> 5040 bytes .../06-simple-avatar/models/ralph-walk.egg.pz | Bin 0 -> 6458 bytes .../06-simple-avatar/models/ralph.egg.pz | Bin 0 -> 184607 bytes .../06-simple-avatar/models/ralph.jpg | Bin 0 -> 83587 bytes samples/networking/06-simple-avatar/sample.dc | 6 + samples/networking/06-simple-avatar/server.py | 18 +++ samples/networking/direct.dc | 91 +++++++++++ 39 files changed, 2077 insertions(+) create mode 100644 samples/networking/01-simple-connection/client.py create mode 100644 samples/networking/01-simple-connection/server.py create mode 100644 samples/networking/02-time-synced-connection/AIRepository.py create mode 100644 samples/networking/02-time-synced-connection/client.py create mode 100644 samples/networking/02-time-synced-connection/server.py create mode 100644 samples/networking/03-distributed-node/AIDGameObject.py create mode 100644 samples/networking/03-distributed-node/AIDGameObjectAI.py create mode 100644 samples/networking/03-distributed-node/AIRepository.py create mode 100644 samples/networking/03-distributed-node/ClientRepository.py create mode 100644 samples/networking/03-distributed-node/DGameObject.py create mode 100644 samples/networking/03-distributed-node/ServerRepository.py create mode 100644 samples/networking/03-distributed-node/client.py create mode 100755 samples/networking/03-distributed-node/sample.dc create mode 100644 samples/networking/03-distributed-node/server.py create mode 100644 samples/networking/04-distributed-model/AIRepository.py create mode 100644 samples/networking/04-distributed-model/ClientRepository.py create mode 100644 samples/networking/04-distributed-model/DModel.py create mode 100644 samples/networking/04-distributed-model/ServerRepository.py create mode 100644 samples/networking/04-distributed-model/client.py create mode 100755 samples/networking/04-distributed-model/sample.dc create mode 100644 samples/networking/04-distributed-model/server.py create mode 100644 samples/networking/05-small-chat/AIRepository.py create mode 100644 samples/networking/05-small-chat/ClientRepository.py create mode 100755 samples/networking/05-small-chat/client.py create mode 100755 samples/networking/05-small-chat/message.py create mode 100755 samples/networking/05-small-chat/sample.dc create mode 100644 samples/networking/05-small-chat/server.py create mode 100644 samples/networking/06-simple-avatar/AIRepository.py create mode 100644 samples/networking/06-simple-avatar/ClientRepository.py create mode 100644 samples/networking/06-simple-avatar/DistributedSmoothActor.py create mode 100644 samples/networking/06-simple-avatar/ServerRepository.py create mode 100644 samples/networking/06-simple-avatar/client.py create mode 100644 samples/networking/06-simple-avatar/models/ralph-run.egg.pz create mode 100644 samples/networking/06-simple-avatar/models/ralph-walk.egg.pz create mode 100644 samples/networking/06-simple-avatar/models/ralph.egg.pz create mode 100644 samples/networking/06-simple-avatar/models/ralph.jpg create mode 100755 samples/networking/06-simple-avatar/sample.dc create mode 100644 samples/networking/06-simple-avatar/server.py create mode 100755 samples/networking/direct.dc diff --git a/samples/networking/01-simple-connection/client.py b/samples/networking/01-simple-connection/client.py new file mode 100644 index 0000000000..2f5d1cef33 --- /dev/null +++ b/samples/networking/01-simple-connection/client.py @@ -0,0 +1,123 @@ +# all imports needed by the engine itself +from direct.showbase.ShowBase import ShowBase + +# initialize the engine +base = ShowBase() + +# initialize the client + +from direct.distributed.ClientRepository import ClientRepository +from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import TextNode + + +base.accept("escape", exit) + +# Function to put instructions on the screen. +def addInstructions(pos, msg): + return OnscreenText(text=msg, style=1, fg=(0, 0, 0, 1), shadow=(1, 1, 1, 1), + parent=base.a2dTopLeft, align=TextNode.ALeft, + pos=(0.08, -pos - 0.04), scale=.06) + +# Function to put title on the screen. +def addTitle(text): + return OnscreenText(text=text, style=1, pos=(-0.1, 0.09), scale=.08, + parent=base.a2dBottomRight, align=TextNode.ARight, + fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1)) + +title = addTitle("Panda3D: Tutorial - Distributed Network (NOT CONNECTED)") +inst2 = addInstructions(0.06, "esc: Close the client") +inst2 = addInstructions(0.12, "See console output") + +def setConnectedMessage(): + title["text"] = "Panda3D: Tutorial - Distributed Network (CONNECTED)" + +base.accept("client-ready", setConnectedMessage) + +# +# CLIENT +# + +class GameClientRepository(ClientRepository): + + def __init__(self): + dcFileNames = ['../direct.dc'] + + # a distributed object of our game. + self.distributedObject = None + self.aiDGameObect = None + + ClientRepository.__init__( + self, + dcFileNames = dcFileNames, + threadedNet = True) + + # Set the same port as configured on the server to be able to connect + # to it + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # Set the IP or hostname of the server we want to connect to + hostname = ConfigVariableString('server-host', '127.0.0.1').getValue() + + # Build the URL from the server hostname and port. If your server + # uses another protocol then http you should change it accordingly. + # Make sure to pass the connectMethod to the ClientRepository.__init__ + # call too. Available connection methods are: + # self.CM_HTTP, self.CM_NET and self.CM_NATIVE + self.url = URLSpec('http://{}:{}'.format(hostname, tcpPort)) + + # Attempt a connection to the server + self.connect([self.url], + successCallback = self.connectSuccess, + failureCallback = self.connectFailure) + + def lostConnection(self): + """ This should be overridden by a derived class to handle an + unexpectedly lost connection to the gameserver. """ + # Handle the disconnection from the server. This can be a reconnect, + # simply exiting the application or anything else. + exit() + + def connectFailure(self, statusCode, statusString): + """ Something went wrong """ + # here we could create a reconnect task to try and connect again. + exit() + + def connectSuccess(self): + """ Successfully connected. But we still can't really do + anything until we've got the doID range. """ + + # Mark interest for zone 1, 2 and 3. There won't be anything in them, + # it's just to display how it works for this small example. + self.setInterestZones([1, 2, 3]) + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if self.haveCreateAuthority(): + # we already have one + self.gotCreateReady() + else: + # Not yet, keep waiting a bit longer. + self.accept(self.uniqueName('createReady'), self.gotCreateReady) + + def gotCreateReady(self): + """ Ready to enter the world. Expand our interest to include + any other zones """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if not self.haveCreateAuthority(): + # Not ready yet. + return + + # we are ready now, so ignore further createReady events + self.ignore(self.uniqueName('createReady')) + + print("Client Ready") + base.messenger.send("client-ready") + +# Start the client +GameClientRepository() + +base.run() diff --git a/samples/networking/01-simple-connection/server.py b/samples/networking/01-simple-connection/server.py new file mode 100644 index 0000000000..51926a0feb --- /dev/null +++ b/samples/networking/01-simple-connection/server.py @@ -0,0 +1,31 @@ +# all imports needed by the engine itself +from direct.showbase.ShowBase import ShowBase + +# all imports needed by the server +from direct.distributed.ServerRepository import ServerRepository +from panda3d.core import ConfigVariableInt + +# initialize the engine +base = ShowBase(windowType='none') + +# the main server class +class GameServerRepository(ServerRepository): + """The server repository class""" + def __init__(self): + """initialise the server class""" + + # get the port number from the configuration file + # if it doesn't exist, we use 4400 as the default + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # list of all needed .dc files + dcFileNames = ['../direct.dc'] + + # initialise a threaded server on this machine with + # the port number and the dc filenames + ServerRepository.__init__(self, tcpPort, dcFileNames=dcFileNames, threadedNet=True) + +# start the server +GameServerRepository() + +base.run() diff --git a/samples/networking/02-time-synced-connection/AIRepository.py b/samples/networking/02-time-synced-connection/AIRepository.py new file mode 100644 index 0000000000..0b462f0557 --- /dev/null +++ b/samples/networking/02-time-synced-connection/AIRepository.py @@ -0,0 +1,81 @@ +from direct.distributed.ClientRepository import ClientRepository +from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString + +class AIRepository(ClientRepository): + def __init__(self): + """ The AI Repository usually lives on a server and is responsible for + server side logic that will handle game objects """ + + # List of all dc files that are of interest to this AI Repository + dcFileNames = ['../direct.dc'] + + # Initialize the repository. We pass it the dc files and as this is an + # AI repository the dcSuffix AI. This will make sure any later calls to + # createDistributedObject will use the correct version. + # The connectMethod + ClientRepository.__init__( + self, + dcFileNames = dcFileNames, + dcSuffix = 'AI', + threadedNet = True) + + # Set the same port as configured on the server to be able to connect + # to it + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # Set the IP or hostname of the server we want to connect to + hostname = ConfigVariableString('server-host', '127.0.0.1').getValue() + + # Build the URL from the server hostname and port. If your server + # doesn't use http you should change it accordingly. Make sure to pass + # the connectMethod to the ClientRepository.__init__ call too. + # Available connection methods are: + # self.CM_HTTP, self.CM_NET and self.CM_NATIVE + url = URLSpec('http://{}:{}'.format(hostname, tcpPort)) + + # Attempt a connection to the server + self.connect([url], + successCallback = self.connectSuccess, + failureCallback = self.connectFailure) + + def connectFailure(self, statusCode, statusString): + """ something went wrong """ + print("Couldn't connect. Make sure to run server.py first!") + raise(StandardError, statusString) + + def connectSuccess(self): + """ Successfully connected. But we still can't really do + anything until we've got the doID range. """ + # The Client Repository will throw this event as soon as it has a doID + # range and would be able to create distributed objects + self.accept('createReady', self.gotCreateReady) + + def lostConnection(self): + """ This should be overridden by a derived class to handle an + unexpectedly lost connection to the gameserver. """ + exit() + + def gotCreateReady(self): + """ Now we're ready to go! """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if not self.haveCreateAuthority(): + # Not ready yet. + return + + # we are ready now, so ignore further createReady events + self.ignore('createReady') + + # Create a Distributed Object by name. This will look up the object in + # the dc files passed to the repository earlier + self.timeManager = self.createDistributedObject( + className = 'TimeManagerAI', # The Name of the Class we want to initialize + zoneId = 1) # The Zone this Object will live in + + print("AI Repository Ready") + + def deallocateChannel(self, doID): + """ This method will be called whenever a client disconnects from the + server. The given doID is the ID of the client who left us. """ + print("Client left us: ", doID) diff --git a/samples/networking/02-time-synced-connection/client.py b/samples/networking/02-time-synced-connection/client.py new file mode 100644 index 0000000000..23ed1b3a0b --- /dev/null +++ b/samples/networking/02-time-synced-connection/client.py @@ -0,0 +1,136 @@ +# all imports needed by the engine itself +from direct.showbase.ShowBase import ShowBase + +# initialize the engine +base = ShowBase() + +# initialize the client + +from direct.distributed.ClientRepository import ClientRepository +from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import TextNode + + +base.accept("escape", exit) + +# Function to put instructions on the screen. +def addInstructions(pos, msg): + return OnscreenText(text=msg, style=1, fg=(0, 0, 0, 1), shadow=(1, 1, 1, 1), + parent=base.a2dTopLeft, align=TextNode.ALeft, + pos=(0.08, -pos - 0.04), scale=.06) + +# Function to put title on the screen. +def addTitle(text): + return OnscreenText(text=text, style=1, pos=(-0.1, 0.09), scale=.08, + parent=base.a2dBottomRight, align=TextNode.ARight, + fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1)) + +title = addTitle("Panda3D: Tutorial - Distributed Network (NOT CONNECTED)") +inst1 = addInstructions(0.06, "esc: Close the client") +inst2 = addInstructions(0.12, "See console output") + +def setConnectedMessage(): + title["text"] = "Panda3D: Tutorial - Distributed Network (CONNECTED)" + +base.accept("client-ready", setConnectedMessage) + +# +# CLIENT +# + +class GameClientRepository(ClientRepository): + + def __init__(self): + dcFileNames = ['../direct.dc'] + + # a distributed object of our game. + self.distributedObject = None + self.aiDGameObect = None + + ClientRepository.__init__( + self, + dcFileNames = dcFileNames, + threadedNet = True) + + # Set the same port as configured on the server to be able to connect + # to it + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # Set the IP or hostname of the server we want to connect to + hostname = ConfigVariableString('server-host', '127.0.0.1').getValue() + + # Build the URL from the server hostname and port. If your server + # uses another protocol then http you should change it accordingly. + # Make sure to pass the connectMethod to the ClientRepository.__init__ + # call too. Available connection methods are: + # self.CM_HTTP, self.CM_NET and self.CM_NATIVE + self.url = URLSpec('http://{}:{}'.format(hostname, tcpPort)) + + # Attempt a connection to the server + self.connect([self.url], + successCallback = self.connectSuccess, + failureCallback = self.connectFailure) + + def lostConnection(self): + """ This should be overridden by a derived class to handle an + unexpectedly lost connection to the gameserver. """ + # Handle the disconnection from the server. This can be a reconnect, + # simply exiting the application or anything else. + exit() + + def connectFailure(self, statusCode, statusString): + """ Something went wrong """ + # we could create a reconnect task to try and connect again. + exit() + + def connectSuccess(self): + """ Successfully connected. But we still can't really do + anything until we've got the doID range. """ + + # Make sure we have interest in the by the AIRepository defined + # TimeManager zone, so we always see it even if we switch to + # another zone. + self.setInterestZones([1]) + + # We must wait for the TimeManager to be fully created and + # synced before we can enter another zone and wait for the + # game object. The uniqueName is important that we get the + # correct, our sync message from the TimeManager and not + # accidentaly a message from another client + self.acceptOnce(self.uniqueName('gotTimeSync'), self.syncReady) + + def syncReady(self): + """ Now we've got the TimeManager manifested, and we're in + sync with the server time. Now we can enter the world. Check + to see if we've received our doIdBase yet. """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if self.haveCreateAuthority(): + # we already have one + self.gotCreateReady() + else: + # Not yet, keep waiting a bit longer. + self.accept(self.uniqueName('createReady'), self.gotCreateReady) + + def gotCreateReady(self): + """ Ready to enter the world. Expand our interest to include + any other zones """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if not self.haveCreateAuthority(): + # Not ready yet. + return + + # we are ready now, so ignore further createReady events + self.ignore(self.uniqueName('createReady')) + + print("Client Ready") + base.messenger.send("client-ready") + +# Start the client +client = GameClientRepository() + +base.run() diff --git a/samples/networking/02-time-synced-connection/server.py b/samples/networking/02-time-synced-connection/server.py new file mode 100644 index 0000000000..6f587de4cf --- /dev/null +++ b/samples/networking/02-time-synced-connection/server.py @@ -0,0 +1,34 @@ +# all imports needed by the engine itself +from direct.showbase.ShowBase import ShowBase + +# all imports needed by the server +from direct.distributed.ServerRepository import ServerRepository +from panda3d.core import ConfigVariableInt + +from AIRepository import AIRepository + +# initialize the engine +base = ShowBase(windowType='none') + +# the main server class +class GameServerRepository(ServerRepository): + """The server repository class""" + def __init__(self): + """initialise the server class""" + + # get the port number from the configuration file + # if it doesn't exist, we use 4400 as the default + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # list of all needed .dc files + dcFileNames = ['../direct.dc'] + + # initialise a threaded server on this machine with + # the port number and the dc filenames + ServerRepository.__init__(self, tcpPort, dcFileNames=dcFileNames, threadedNet=True) + +# start the server +GameServerRepository() +AIRepository() + +base.run() diff --git a/samples/networking/03-distributed-node/AIDGameObject.py b/samples/networking/03-distributed-node/AIDGameObject.py new file mode 100644 index 0000000000..9c4aab012b --- /dev/null +++ b/samples/networking/03-distributed-node/AIDGameObject.py @@ -0,0 +1,29 @@ +from direct.distributed.DistributedObject import DistributedObject + +class AIDGameObject(DistributedObject): + """ This class is a DirectObject which will be created and managed by the + AI Repository. """ + + def __init__(self, cr): + DistributedObject.__init__(self, cr) + + def announceGenerate(self): + """ The AI has created this object, so we send it's distributed object ID + over to the client. That way the client can actually grab the object + and use it to communicate with the AI. Alternatively store it in the + Client Repository in self.cr """ + base.messenger.send(self.cr.uniqueName('AIDGameObjectGenerated'), [self.doId]) + # call the base class method + DistributedObject.announceGenerate(self) + + def d_requestDataFromAI(self): + """ Request some data from the AI and passing it some data from us. """ + data = ("Some Data", 1, -1.25) + print("Sending game data:", data) + self.sendUpdate('messageRoundtripToAI', [data]) + + def messageRoundtripToClient(self, data): + """ Here we expect the answer from the AI from a previous + messageRoundtripToAI call """ + print("Got Data:", data) + print("Roundtrip message complete") diff --git a/samples/networking/03-distributed-node/AIDGameObjectAI.py b/samples/networking/03-distributed-node/AIDGameObjectAI.py new file mode 100644 index 0000000000..4fda4364fb --- /dev/null +++ b/samples/networking/03-distributed-node/AIDGameObjectAI.py @@ -0,0 +1,25 @@ +from direct.distributed.DistributedObjectAI import DistributedObjectAI + +class AIDGameObjectAI(DistributedObjectAI): + def __init__(self, aiRepository): + DistributedObjectAI.__init__(self, aiRepository) + + def messageRoundtripToAI(self, data): + """ The client sent us some data to process. So work with it and send + changed data back to the requesting client """ + requesterId = self.air.getAvatarIdFromSender() + print("Got client data:", data, "from client with ID", requesterId) + + # do something with the data + aiChangedData = ( + data[0] + " from the AI", + data[1] + 1, + data[2]) + + print("Sending modified game data back:", aiChangedData) + self.d_messageRoundtripToClient(aiChangedData, requesterId) + + def d_messageRoundtripToClient(self, data, requesterId): + """ Send the given data to the requesting client """ + print("Send message to back to:", requesterId) + self.sendUpdateToAvatarId(requesterId, 'messageRoundtripToClient', [data]) diff --git a/samples/networking/03-distributed-node/AIRepository.py b/samples/networking/03-distributed-node/AIRepository.py new file mode 100644 index 0000000000..a51dca16f9 --- /dev/null +++ b/samples/networking/03-distributed-node/AIRepository.py @@ -0,0 +1,86 @@ +from direct.distributed.ClientRepository import ClientRepository +from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString +from AIDGameObjectAI import AIDGameObjectAI + +class AIRepository(ClientRepository): + def __init__(self): + """ The AI Repository usually lives on a server and is responsible for + server side logic that will handle game objects """ + + # List of all dc files that are of interest to this AI Repository + dcFileNames = ['../direct.dc', 'sample.dc'] + + # Initialize the repository. We pass it the dc files and as this is an + # AI repository the dcSuffix AI. This will make sure any later calls to + # createDistributedObject will use the correct version. + # The connectMethod + ClientRepository.__init__( + self, + dcFileNames = dcFileNames, + dcSuffix = 'AI', + threadedNet = True) + + # Set the same port as configured on the server to be able to connect + # to it + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # Set the IP or hostname of the server we want to connect to + hostname = ConfigVariableString('server-host', '127.0.0.1').getValue() + + # Build the URL from the server hostname and port. If your server + # doesn't use http you should change it accordingly. Make sure to pass + # the connectMethod to the ClientRepository.__init__ call too. + # Available connection methods are: + # self.CM_HTTP, self.CM_NET and self.CM_NATIVE + url = URLSpec('http://{}:{}'.format(hostname, tcpPort)) + + # Attempt a connection to the server + self.connect([url], + successCallback = self.connectSuccess, + failureCallback = self.connectFailure) + + def connectFailure(self, statusCode, statusString): + """ something went wrong """ + print("Couldn't connect. Make sure to run server.py first!") + raise(StandardError, statusString) + + def connectSuccess(self): + """ Successfully connected. But we still can't really do + anything until we've got the doID range. """ + # The Client Repository will throw this event as soon as it has a doID + # range and would be able to create distributed objects + self.accept('createReady', self.gotCreateReady) + + def lostConnection(self): + """ This should be overridden by a derived class to handle an + unexpectedly lost connection to the gameserver. """ + exit() + + def gotCreateReady(self): + """ Now we're ready to go! """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if not self.haveCreateAuthority(): + # Not ready yet. + return + + # we are ready now, so ignore further createReady events + self.ignore('createReady') + + # Create a Distributed Object by name. This will look up the object in + # the dc files passed to the repository earlier + self.timeManager = self.createDistributedObject( + className = 'TimeManagerAI', # The Name of the Class we want to initialize + zoneId = 1) # The Zone this Object will live in + + self.gameDistObject = self.createDistributedObject( + className = 'AIDGameObjectAI', + zoneId = 2) + + print("AI Repository Ready") + + def deallocateChannel(self, doID): + """ This method will be called whenever a client disconnects from the + server. The given doID is the ID of the client who left us. """ + print("Client left us: ", doID) diff --git a/samples/networking/03-distributed-node/ClientRepository.py b/samples/networking/03-distributed-node/ClientRepository.py new file mode 100644 index 0000000000..f413cf6ca8 --- /dev/null +++ b/samples/networking/03-distributed-node/ClientRepository.py @@ -0,0 +1,130 @@ +from direct.distributed.ClientRepository import ClientRepository +from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString +from DGameObject import DGameObject + +class GameClientRepository(ClientRepository): + + def __init__(self): + dcFileNames = ['../direct.dc', 'sample.dc'] + + # a distributed object of our game. + self.distributedObject = None + self.aiDGameObect = None + + ClientRepository.__init__( + self, + dcFileNames = dcFileNames, + threadedNet = True) + + # Set the same port as configured on the server to be able to connect + # to it + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # Set the IP or hostname of the server we want to connect to + hostname = ConfigVariableString('server-host', '127.0.0.1').getValue() + + # Build the URL from the server hostname and port. If your server + # uses another protocol then http you should change it accordingly. + # Make sure to pass the connectMethod to the ClientRepository.__init__ + # call too. Available connection methods are: + # self.CM_HTTP, self.CM_NET and self.CM_NATIVE + self.url = URLSpec('http://{}:{}'.format(hostname, tcpPort)) + + # Attempt a connection to the server + self.connect([self.url], + successCallback = self.connectSuccess, + failureCallback = self.connectFailure) + + def lostConnection(self): + """ This should be overridden by a derived class to handle an + unexpectedly lost connection to the gameserver. """ + # Handle the disconnection from the server. This can be a reconnect, + # simply exiting the application or anything else. + exit() + + def connectFailure(self, statusCode, statusString): + """ Something went wrong """ + exit() + + def connectSuccess(self): + """ Successfully connected. But we still can't really do + anything until we've got the doID range. """ + + # Make sure we have interest in the by the AIRepository defined + # TimeManager zone, so we always see it even if we switch to + # another zone. + self.setInterestZones([1]) + + # We must wait for the TimeManager to be fully created and + # synced before we can enter another zone and wait for the + # game object. The uniqueName is important that we get the + # correct, our sync message from the TimeManager and not + # accidentaly a message from another client + self.acceptOnce(self.uniqueName('gotTimeSync'), self.syncReady) + + def syncReady(self): + """ Now we've got the TimeManager manifested, and we're in + sync with the server time. Now we can enter the world. Check + to see if we've received our doIdBase yet. """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if self.haveCreateAuthority(): + # we already have one + self.gotCreateReady() + else: + # Not yet, keep waiting a bit longer. + self.accept(self.uniqueName('createReady'), self.gotCreateReady) + + def gotCreateReady(self): + """ Ready to enter the world. Expand our interest to include + any other zones """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if not self.haveCreateAuthority(): + # Not ready yet. + return + + # we are ready now, so ignore further createReady events + self.ignore(self.uniqueName('createReady')) + + self.join() + + print("Client Ready") + + def join(self): + """ Join a game/room/whatever """ + self.accept(self.uniqueName('AIDGameObjectGenerated'), self.aiDGameObectGenerated) + + # set our intersted zones to let the client see all distributed obects + # in those zones + self.setInterestZones([1, 2]) + + # Manifest a object on the server. The object will have our "base" doId. + self.distributedObject = DGameObject(self) + self.createDistributedObject( + distObj = self.distributedObject, + zoneId = 2) + + base.messenger.send("client-joined") + print("Joined") + + def aiDGameObectGenerated(self, doId): + print("AIDGameObect was generated") + self.aiDGameObect = self.doId2do[doId] + + def sendGameData(self): + if not self.distributedObject: return + + print("send game data") + + # send a message to the server + self.distributedObject.d_sendGameData() + + def sendRoundtripToAI(self): + if not self.aiDGameObect: return + + print("Initiate roundtrip message to AI Server") + + self.aiDGameObect.d_requestDataFromAI() diff --git a/samples/networking/03-distributed-node/DGameObject.py b/samples/networking/03-distributed-node/DGameObject.py new file mode 100644 index 0000000000..c3eb37a320 --- /dev/null +++ b/samples/networking/03-distributed-node/DGameObject.py @@ -0,0 +1,15 @@ +from direct.distributed.DistributedObject import DistributedObject + +class DGameObject(DistributedObject): + def __init__(self, cr): + DistributedObject.__init__(self, cr) + + def sendGameData(self, data): + """ Method that can be called from the clients with an sendUpdate call """ + print(data) + + def d_sendGameData(self): + """ A method to send an update message to the server. The d_ stands + for distributed """ + # send the message to the server + self.sendUpdate('sendGameData', [('ValueA', 123, 1.25)]) diff --git a/samples/networking/03-distributed-node/ServerRepository.py b/samples/networking/03-distributed-node/ServerRepository.py new file mode 100644 index 0000000000..59da3a6aa4 --- /dev/null +++ b/samples/networking/03-distributed-node/ServerRepository.py @@ -0,0 +1,20 @@ +# all imports needed by the server +from direct.distributed.ServerRepository import ServerRepository +from panda3d.core import ConfigVariableInt + +# the main server class +class GameServerRepository(ServerRepository): + """The server repository class""" + def __init__(self): + """initialise the server class""" + + # get the port number from the configuration file + # if it doesn't exist, we use 4400 as the default + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # list of all needed .dc files + dcFileNames = ['../direct.dc', 'sample.dc'] + + # initialise a threaded server on this machine with + # the port number and the dc filenames + ServerRepository.__init__(self, tcpPort, dcFileNames=dcFileNames, threadedNet=True) diff --git a/samples/networking/03-distributed-node/client.py b/samples/networking/03-distributed-node/client.py new file mode 100644 index 0000000000..23ed1b3a0b --- /dev/null +++ b/samples/networking/03-distributed-node/client.py @@ -0,0 +1,136 @@ +# all imports needed by the engine itself +from direct.showbase.ShowBase import ShowBase + +# initialize the engine +base = ShowBase() + +# initialize the client + +from direct.distributed.ClientRepository import ClientRepository +from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import TextNode + + +base.accept("escape", exit) + +# Function to put instructions on the screen. +def addInstructions(pos, msg): + return OnscreenText(text=msg, style=1, fg=(0, 0, 0, 1), shadow=(1, 1, 1, 1), + parent=base.a2dTopLeft, align=TextNode.ALeft, + pos=(0.08, -pos - 0.04), scale=.06) + +# Function to put title on the screen. +def addTitle(text): + return OnscreenText(text=text, style=1, pos=(-0.1, 0.09), scale=.08, + parent=base.a2dBottomRight, align=TextNode.ARight, + fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1)) + +title = addTitle("Panda3D: Tutorial - Distributed Network (NOT CONNECTED)") +inst1 = addInstructions(0.06, "esc: Close the client") +inst2 = addInstructions(0.12, "See console output") + +def setConnectedMessage(): + title["text"] = "Panda3D: Tutorial - Distributed Network (CONNECTED)" + +base.accept("client-ready", setConnectedMessage) + +# +# CLIENT +# + +class GameClientRepository(ClientRepository): + + def __init__(self): + dcFileNames = ['../direct.dc'] + + # a distributed object of our game. + self.distributedObject = None + self.aiDGameObect = None + + ClientRepository.__init__( + self, + dcFileNames = dcFileNames, + threadedNet = True) + + # Set the same port as configured on the server to be able to connect + # to it + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # Set the IP or hostname of the server we want to connect to + hostname = ConfigVariableString('server-host', '127.0.0.1').getValue() + + # Build the URL from the server hostname and port. If your server + # uses another protocol then http you should change it accordingly. + # Make sure to pass the connectMethod to the ClientRepository.__init__ + # call too. Available connection methods are: + # self.CM_HTTP, self.CM_NET and self.CM_NATIVE + self.url = URLSpec('http://{}:{}'.format(hostname, tcpPort)) + + # Attempt a connection to the server + self.connect([self.url], + successCallback = self.connectSuccess, + failureCallback = self.connectFailure) + + def lostConnection(self): + """ This should be overridden by a derived class to handle an + unexpectedly lost connection to the gameserver. """ + # Handle the disconnection from the server. This can be a reconnect, + # simply exiting the application or anything else. + exit() + + def connectFailure(self, statusCode, statusString): + """ Something went wrong """ + # we could create a reconnect task to try and connect again. + exit() + + def connectSuccess(self): + """ Successfully connected. But we still can't really do + anything until we've got the doID range. """ + + # Make sure we have interest in the by the AIRepository defined + # TimeManager zone, so we always see it even if we switch to + # another zone. + self.setInterestZones([1]) + + # We must wait for the TimeManager to be fully created and + # synced before we can enter another zone and wait for the + # game object. The uniqueName is important that we get the + # correct, our sync message from the TimeManager and not + # accidentaly a message from another client + self.acceptOnce(self.uniqueName('gotTimeSync'), self.syncReady) + + def syncReady(self): + """ Now we've got the TimeManager manifested, and we're in + sync with the server time. Now we can enter the world. Check + to see if we've received our doIdBase yet. """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if self.haveCreateAuthority(): + # we already have one + self.gotCreateReady() + else: + # Not yet, keep waiting a bit longer. + self.accept(self.uniqueName('createReady'), self.gotCreateReady) + + def gotCreateReady(self): + """ Ready to enter the world. Expand our interest to include + any other zones """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if not self.haveCreateAuthority(): + # Not ready yet. + return + + # we are ready now, so ignore further createReady events + self.ignore(self.uniqueName('createReady')) + + print("Client Ready") + base.messenger.send("client-ready") + +# Start the client +client = GameClientRepository() + +base.run() diff --git a/samples/networking/03-distributed-node/sample.dc b/samples/networking/03-distributed-node/sample.dc new file mode 100755 index 0000000000..651d79b35c --- /dev/null +++ b/samples/networking/03-distributed-node/sample.dc @@ -0,0 +1,17 @@ +import DGameObject +import AIDGameObject/AI + +struct gameDataModel { + string value_a; + uint8 value_b; + int8 value_c/100; +} + +dclass DGameObject: DistributedObject { + sendGameData(gameDataModel data) broadcast; +}; + +dclass AIDGameObject: DistributedObject { + messageRoundtripToAI(gameDataModel data) p2p; + messageRoundtripToClient(gameDataModel data) p2p; +} diff --git a/samples/networking/03-distributed-node/server.py b/samples/networking/03-distributed-node/server.py new file mode 100644 index 0000000000..0aab404a46 --- /dev/null +++ b/samples/networking/03-distributed-node/server.py @@ -0,0 +1,18 @@ +# all imports needed by the engine itself +from direct.showbase.ShowBase import ShowBase + +# import our own repositories +from ServerRepository import GameServerRepository +from AIRepository import AIRepository + +# initialize the engine +base = ShowBase(windowType='none') + +# instantiate the server +GameServerRepository() + +# The AI Repository to manage server side (AI) clients +AIRepository() + +# start the server +base.run() diff --git a/samples/networking/04-distributed-model/AIRepository.py b/samples/networking/04-distributed-model/AIRepository.py new file mode 100644 index 0000000000..9e46f024d3 --- /dev/null +++ b/samples/networking/04-distributed-model/AIRepository.py @@ -0,0 +1,81 @@ +from direct.distributed.ClientRepository import ClientRepository +from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString + +class AIRepository(ClientRepository): + def __init__(self): + """ The AI Repository usually lives on a server and is responsible for + server side logic that will handle game objects """ + + # List of all dc files that are of interest to this AI Repository + dcFileNames = ['../direct.dc', 'sample.dc'] + + # Initialize the repository. We pass it the dc files and as this is an + # AI repository the dcSuffix AI. This will make sure any later calls to + # createDistributedObject will use the correct version. + # The connectMethod + ClientRepository.__init__( + self, + dcFileNames = dcFileNames, + dcSuffix = 'AI', + threadedNet = True) + + # Set the same port as configured on the server to be able to connect + # to it + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # Set the IP or hostname of the server we want to connect to + hostname = ConfigVariableString('server-host', '127.0.0.1').getValue() + + # Build the URL from the server hostname and port. If your server + # doesn't use http you should change it accordingly. Make sure to pass + # the connectMethod to the ClientRepository.__init__ call too. + # Available connection methods are: + # self.CM_HTTP, self.CM_NET and self.CM_NATIVE + url = URLSpec('http://{}:{}'.format(hostname, tcpPort)) + + # Attempt a connection to the server + self.connect([url], + successCallback = self.connectSuccess, + failureCallback = self.connectFailure) + + def connectFailure(self, statusCode, statusString): + """ something went wrong """ + print("Couldn't connect. Make sure to run server.py first!") + raise(StandardError, statusString) + + def connectSuccess(self): + """ Successfully connected. But we still can't really do + anything until we've got the doID range. """ + # The Client Repository will throw this event as soon as it has a doID + # range and would be able to create distributed objects + self.accept('createReady', self.gotCreateReady) + + def lostConnection(self): + """ This should be overridden by a derived class to handle an + unexpectedly lost connection to the gameserver. """ + exit() + + def gotCreateReady(self): + """ Now we're ready to go! """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if not self.haveCreateAuthority(): + # Not ready yet. + return + + # we are ready now, so ignore further createReady events + self.ignore('createReady') + + # Create a Distributed Object by name. This will look up the object in + # the dc files passed to the repository earlier + self.timeManager = self.createDistributedObject( + className = 'TimeManagerAI', # The Name of the Class we want to initialize + zoneId = 1) # The Zone this Object will live in + + print("AI Repository Ready") + + def deallocateChannel(self, doID): + """ This method will be called whenever a client disconnects from the + server. The given doID is the ID of the client who left us. """ + print("Client left us: ", doID) diff --git a/samples/networking/04-distributed-model/ClientRepository.py b/samples/networking/04-distributed-model/ClientRepository.py new file mode 100644 index 0000000000..f2b5ec7c82 --- /dev/null +++ b/samples/networking/04-distributed-model/ClientRepository.py @@ -0,0 +1,116 @@ +from direct.distributed.ClientRepository import ClientRepository +from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString +from random import random + +class GameClientRepository(ClientRepository): + + def __init__(self): + dcFileNames = ['../direct.dc', 'sample.dc'] + + ClientRepository.__init__( + self, + dcFileNames = dcFileNames, + threadedNet = True) + + # Set the same port as configured on the server to be able to connect + # to it + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # Set the IP or hostname of the server we want to connect to + hostname = ConfigVariableString('server-host', '127.0.0.1').getValue() + + # Build the URL from the server hostname and port. If your server + # uses another protocol then http you should change it accordingly. + # Make sure to pass the connectMethod to the ClientRepository.__init__ + # call too. Available connection methods are: + # self.CM_HTTP, self.CM_NET and self.CM_NATIVE + self.url = URLSpec('http://{}:{}'.format(hostname, tcpPort)) + + # Attempt a connection to the server + self.connect([self.url], + successCallback = self.connectSuccess, + failureCallback = self.connectFailure) + + def lostConnection(self): + """ This should be overridden by a derived class to handle an + unexpectedly lost connection to the gameserver. """ + # Handle the disconnection from the server. This can be a reconnect, + # simply exiting the application or anything else. + exit() + + def connectFailure(self, statusCode, statusString): + """ Something went wrong """ + exit() + + def connectSuccess(self): + """ Successfully connected. But we still can't really do + anything until we've got the doID range. """ + + # Make sure we have interest in the by the AIRepository defined + # TimeManager zone, so we always see it even if we switch to + # another zone. + self.setInterestZones([1]) + + # We must wait for the TimeManager to be fully created and + # synced before we can enter another zone and wait for the + # game object. The uniqueName is important that we get the + # correct, our sync message from the TimeManager and not + # accidentaly a message from another client + self.acceptOnce(self.uniqueName('gotTimeSync'), self.syncReady) + + def syncReady(self): + """ Now we've got the TimeManager manifested, and we're in + sync with the server time. Now we can enter the world. Check + to see if we've received our doIdBase yet. """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if self.haveCreateAuthority(): + # we already have one + self.gotCreateReady() + else: + # Not yet, keep waiting a bit longer. + self.accept(self.uniqueName('createReady'), self.gotCreateReady) + + def gotCreateReady(self): + """ Ready to enter the world. Expand our interest to include + any other zones """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if not self.haveCreateAuthority(): + # Not ready yet. + return + + # we are ready now, so ignore further createReady events + self.ignore(self.uniqueName('createReady')) + + self.join() + + print("Client Ready") + + def join(self): + """ Join a game/room/whatever """ + # set our intersted zones to let the client see all distributed obects + # in those zones + self.setInterestZones([1, 2]) + + # Manifest a object on the server. The object will have our "base" doId. + self.myDistributedModel = self.createDistributedObject( + className = "DModel", + zoneId = 2) + + x = random() + z = random() + # set position for this local client + self.myDistributedModel.setPos(x, 10, z) + # make sure already connected clients will get notified of the + # position by calling the distributred (d_*) version of the method + self.myDistributedModel.d_setPos(x, 10, z) + + base.messenger.send("client-joined") + print("Joined") + + def modelReady(self, doId): + print("AIDGameObect was generated") + self.aiDGameObect = self.doId2do[doId] diff --git a/samples/networking/04-distributed-model/DModel.py b/samples/networking/04-distributed-model/DModel.py new file mode 100644 index 0000000000..047e95ab85 --- /dev/null +++ b/samples/networking/04-distributed-model/DModel.py @@ -0,0 +1,61 @@ +from direct.distributed.DistributedNode import DistributedNode + +class DModel(DistributedNode): + def __init__(self, cr): + DistributedNode.__init__(self, cr) + + # Load up the visible representation of this avatar. + self.model = loader.loadModel('smiley.egg') + self.model.reparentTo(self) + + def announceGenerate(self): + """ This method is called after generate(), after all of the + required fields have been filled in. At the time of this call, + the distributed object is ready for use. """ + + DistributedNode.announceGenerate(self) + + # Now that the object has been fully manifested, we can parent + # it into the scene. + self.reparentTo(render) + + def disable(self): + """ This method is called when the object is removed from the + scene, for instance because it left the zone. It is balanced + against generate(): for each generate(), there will be a + corresponding disable(). Everything that was done in + generate() or announceGenerate() should be undone in disable(). + + After a disable(), the object might be cached in memory in case + it will eventually reappear. The DistributedObject should be + prepared to receive another generate() for an object that has + already received disable(). + + Note that the above is only strictly true for *cacheable* + objects. Most objects are, by default, non-cacheable; you + have to call obj.setCacheable(True) (usually in the + constructor) to make it cacheable. Until you do this, your + non-cacheable object will always receive a delete() whenever + it receives a disable(), and it will never be stored in a + cache. + """ + + # Take it out of the scene graph. + self.detachNode() + + DistributedNode.disable(self) + + def delete(self): + """ This method is called after disable() when the object is to + be completely removed, for instance because the other user + logged off. We will not expect to see this object again; it + will not be cached. This is stronger than disable(), and the + object may remove any structures it needs to in order to allow + it to be completely deleted from memory. This balances against + __init__(): every DistributedObject that is created will + eventually get delete() called for it exactly once. """ + + # Clean out self.model, so we don't have a circular reference. + self.model = None + + DistributedNode.delete(self) diff --git a/samples/networking/04-distributed-model/ServerRepository.py b/samples/networking/04-distributed-model/ServerRepository.py new file mode 100644 index 0000000000..59da3a6aa4 --- /dev/null +++ b/samples/networking/04-distributed-model/ServerRepository.py @@ -0,0 +1,20 @@ +# all imports needed by the server +from direct.distributed.ServerRepository import ServerRepository +from panda3d.core import ConfigVariableInt + +# the main server class +class GameServerRepository(ServerRepository): + """The server repository class""" + def __init__(self): + """initialise the server class""" + + # get the port number from the configuration file + # if it doesn't exist, we use 4400 as the default + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # list of all needed .dc files + dcFileNames = ['../direct.dc', 'sample.dc'] + + # initialise a threaded server on this machine with + # the port number and the dc filenames + ServerRepository.__init__(self, tcpPort, dcFileNames=dcFileNames, threadedNet=True) diff --git a/samples/networking/04-distributed-model/client.py b/samples/networking/04-distributed-model/client.py new file mode 100644 index 0000000000..0a8373b469 --- /dev/null +++ b/samples/networking/04-distributed-model/client.py @@ -0,0 +1,43 @@ +# all imports needed by the engine itself +from direct.showbase.ShowBase import ShowBase + +# import our own repositories +from ClientRepository import GameClientRepository + +# initialize the engine +base = ShowBase() + +# initialize the client +client = GameClientRepository() + +base.accept("escape", exit) + + + + +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import TextNode + +# Function to put instructions on the screen. +def addInstructions(pos, msg): + return OnscreenText(text=msg, style=1, fg=(0, 0, 0, 1), shadow=(1, 1, 1, 1), + parent=base.a2dTopLeft, align=TextNode.ALeft, + pos=(0.08, -pos - 0.04), scale=.06) + +# Function to put title on the screen. +def addTitle(text): + return OnscreenText(text=text, style=1, pos=(-0.1, 0.09), scale=.08, + parent=base.a2dBottomRight, align=TextNode.ARight, + fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1)) + +title = addTitle("Panda3D: Tutorial - Distributed Network (NOT CONNECTED)") +inst1 = addInstructions(0.06, "esc: Close the client") +inst2 = addInstructions(0.12, "See console output") + +def setConnectedMessage(): + title["text"] = "Panda3D: Tutorial - Distributed Network (CONNECTED)" + +base.accept("client-joined", setConnectedMessage) + +# start the client +base.run() diff --git a/samples/networking/04-distributed-model/sample.dc b/samples/networking/04-distributed-model/sample.dc new file mode 100755 index 0000000000..2a0e86b154 --- /dev/null +++ b/samples/networking/04-distributed-model/sample.dc @@ -0,0 +1,3 @@ +import DModel + +dclass DModel: DistributedNode { } diff --git a/samples/networking/04-distributed-model/server.py b/samples/networking/04-distributed-model/server.py new file mode 100644 index 0000000000..0aab404a46 --- /dev/null +++ b/samples/networking/04-distributed-model/server.py @@ -0,0 +1,18 @@ +# all imports needed by the engine itself +from direct.showbase.ShowBase import ShowBase + +# import our own repositories +from ServerRepository import GameServerRepository +from AIRepository import AIRepository + +# initialize the engine +base = ShowBase(windowType='none') + +# instantiate the server +GameServerRepository() + +# The AI Repository to manage server side (AI) clients +AIRepository() + +# start the server +base.run() diff --git a/samples/networking/05-small-chat/AIRepository.py b/samples/networking/05-small-chat/AIRepository.py new file mode 100644 index 0000000000..9e46f024d3 --- /dev/null +++ b/samples/networking/05-small-chat/AIRepository.py @@ -0,0 +1,81 @@ +from direct.distributed.ClientRepository import ClientRepository +from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString + +class AIRepository(ClientRepository): + def __init__(self): + """ The AI Repository usually lives on a server and is responsible for + server side logic that will handle game objects """ + + # List of all dc files that are of interest to this AI Repository + dcFileNames = ['../direct.dc', 'sample.dc'] + + # Initialize the repository. We pass it the dc files and as this is an + # AI repository the dcSuffix AI. This will make sure any later calls to + # createDistributedObject will use the correct version. + # The connectMethod + ClientRepository.__init__( + self, + dcFileNames = dcFileNames, + dcSuffix = 'AI', + threadedNet = True) + + # Set the same port as configured on the server to be able to connect + # to it + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # Set the IP or hostname of the server we want to connect to + hostname = ConfigVariableString('server-host', '127.0.0.1').getValue() + + # Build the URL from the server hostname and port. If your server + # doesn't use http you should change it accordingly. Make sure to pass + # the connectMethod to the ClientRepository.__init__ call too. + # Available connection methods are: + # self.CM_HTTP, self.CM_NET and self.CM_NATIVE + url = URLSpec('http://{}:{}'.format(hostname, tcpPort)) + + # Attempt a connection to the server + self.connect([url], + successCallback = self.connectSuccess, + failureCallback = self.connectFailure) + + def connectFailure(self, statusCode, statusString): + """ something went wrong """ + print("Couldn't connect. Make sure to run server.py first!") + raise(StandardError, statusString) + + def connectSuccess(self): + """ Successfully connected. But we still can't really do + anything until we've got the doID range. """ + # The Client Repository will throw this event as soon as it has a doID + # range and would be able to create distributed objects + self.accept('createReady', self.gotCreateReady) + + def lostConnection(self): + """ This should be overridden by a derived class to handle an + unexpectedly lost connection to the gameserver. """ + exit() + + def gotCreateReady(self): + """ Now we're ready to go! """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if not self.haveCreateAuthority(): + # Not ready yet. + return + + # we are ready now, so ignore further createReady events + self.ignore('createReady') + + # Create a Distributed Object by name. This will look up the object in + # the dc files passed to the repository earlier + self.timeManager = self.createDistributedObject( + className = 'TimeManagerAI', # The Name of the Class we want to initialize + zoneId = 1) # The Zone this Object will live in + + print("AI Repository Ready") + + def deallocateChannel(self, doID): + """ This method will be called whenever a client disconnects from the + server. The given doID is the ID of the client who left us. """ + print("Client left us: ", doID) diff --git a/samples/networking/05-small-chat/ClientRepository.py b/samples/networking/05-small-chat/ClientRepository.py new file mode 100644 index 0000000000..3dd82bd88a --- /dev/null +++ b/samples/networking/05-small-chat/ClientRepository.py @@ -0,0 +1,104 @@ +from direct.distributed.ClientRepository import ClientRepository +from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString +from message import Message + +class GameClientRepository(ClientRepository): + + def __init__(self): + dcFileNames = ['../direct.dc', 'sample.dc'] + + ClientRepository.__init__( + self, + dcFileNames = dcFileNames, + threadedNet = True) + + # Set the same port as configured on the server to be able to connect + # to it + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # Set the IP or hostname of the server we want to connect to + hostname = ConfigVariableString('server-host', '127.0.0.1').getValue() + + # Build the URL from the server hostname and port. If your server + # uses another protocol then http you should change it accordingly. + # Make sure to pass the connectMethod to the ClientRepository.__init__ + # call too. Available connection methods are: + # self.CM_HTTP, self.CM_NET and self.CM_NATIVE + self.url = URLSpec('http://{}:{}'.format(hostname, tcpPort)) + + # Attempt a connection to the server + self.connect([self.url], + successCallback = self.connectSuccess, + failureCallback = self.connectFailure) + + def lostConnection(self): + ''' This should be overridden by a derived class to handle an + unexpectedly lost connection to the gameserver. ''' + # Handle the disconnection from the server. This can be a reconnect, + # simply exiting the application or anything else. + exit() + + def connectFailure(self, statusCode, statusString): + ''' Something went wrong ''' + # we could create a reconnect task to try and connect again. + exit() + + def connectSuccess(self): + ''' Successfully connected. But we still can't really do + anything until we've got the doID range. ''' + + # Make sure we have interest in the by the AIRepository defined + # TimeManager zone, so we always see it even if we switch to + # another zone. + self.setInterestZones([1]) + + # We must wait for the TimeManager to be fully created and + # synced before we can enter another zone and wait for the + # game object. The uniqueName is important that we get the + # correct, our sync message from the TimeManager and not + # accidentaly a message from another client + self.acceptOnce(self.uniqueName('gotTimeSync'), self.syncReady) + + def syncReady(self): + ''' Now we've got the TimeManager manifested, and we're in + sync with the server time. Now we can enter the world. Check + to see if we've received our doIdBase yet. ''' + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if self.haveCreateAuthority(): + # we already have one + self.gotCreateReady() + else: + # Not yet, keep waiting a bit longer. + self.accept(self.uniqueName('createReady'), self.gotCreateReady) + + def gotCreateReady(self): + ''' Ready to enter the world. Expand our interest to include + any other zones ''' + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if not self.haveCreateAuthority(): + # Not ready yet. + return + + # we are ready now, so ignore further createReady events + self.ignore(self.uniqueName('createReady')) + + # create a instance of the message class + msg = Message(self) + # and create the distributed Object with it + self.createDistributedObject( + distObj = msg, zoneId = 1) + # save the created Distributed Object + # in self.msg for later usage + self.msg = msg + + def sendMessage(self, msgText): + '''Function to call the send function of the message class, + which sends the Message over the Network to the other users''' + + sentText = "{}: {}".format(self.doIdBase, msgText) + + self.msg.b_sendText(sentText) diff --git a/samples/networking/05-small-chat/client.py b/samples/networking/05-small-chat/client.py new file mode 100755 index 0000000000..4a76cb26ed --- /dev/null +++ b/samples/networking/05-small-chat/client.py @@ -0,0 +1,85 @@ +from direct.showbase.ShowBase import ShowBase +from direct.distributed.ClientRepository import ClientRepository +from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString +from message import Message +from ClientRepository import GameClientRepository +from direct.gui.DirectGui import DirectButton, DirectEntry, DirectFrame +from direct.gui import DirectGuiGlobals as DGG + +# initialize the engine +base = ShowBase() + +client = GameClientRepository() + +# Setup the GUI + +# this frame will contain all our GUI elements +frameMain = DirectFrame(frameColor = (0, 0, 0, 1)) + +def clearText(): + ''' Write an empty string in the textbox ''' + txt_msg.enterText('') + +def setDefaultText(): + ''' Write the default message in the textbox ''' + txt_msg.enterText('Your Message') + +def send(args=None): + ''' Send the text written in the message textbox to the clients ''' + client.sendMessage(txt_msg.get()) + txt_msg.enterText('') + +# the Textbox where we write our Messages, which we +# want to send over to the other users +txt_msg = DirectEntry( + initialText = 'Your Message', + cursorKeys = True, + pos = (-0.9,0,-0.9), + scale = 0.1, + width = 14, + focusInCommand = clearText, + focusOutCommand = setDefaultText, + command = send, + parent = frameMain) + +# a button to initiate the sending of the message +btn_send = DirectButton( + text = 'Send', + pos = (0.75,0,-0.9), + scale = 0.15, + command = send, + parent = frameMain) + +# This will show all sent messages +txt_messages = DirectEntry( + cursorKeys = False, + pos = (-0.8,0,0.5), + scale = 0.1, + width = 14, + numLines = 10, + state = DGG.DISABLED, + parent = frameMain) + +# Function which will write the given text in the +# textbox, where we store all send and recieved messages. +# This Function will only write the last 10 messages and +# cut of the erlier messages +def setText(messageText): + # get all messages from the textbox + parts = txt_messages.get().split('\n') + if len(parts) >= 10: + cutParts = '' + # as the textbox can only hold 10 lines cut out the first entry + for i in range(1,len(parts)): + cutParts += parts[i] + '\n' + txt_messages.enterText(cutParts + messageText) + else: + txt_messages.enterText(txt_messages.get() + '\n' + messageText) + +# create a DirectObject instance, which will then catch the events and +# handle them with the given functions +base.accept('setText', setText) +base.accept('escape', exit) + +# start the application +base.run() diff --git a/samples/networking/05-small-chat/message.py b/samples/networking/05-small-chat/message.py new file mode 100755 index 0000000000..879935e37e --- /dev/null +++ b/samples/networking/05-small-chat/message.py @@ -0,0 +1,25 @@ +from direct.distributed.DistributedObject import DistributedObject +from direct.showbase.MessengerGlobal import messenger + +class Message(DistributedObject): + def __init__(self, clientRepo): + DistributedObject.__init__(self, clientRepo) + + def sendText(self, messageText): + """Function which is caled for local changes only""" + # send an event, which will set the text on the + #print "got a message" + messenger.send("setText", [messageText]) + + def d_sendText(self, messageText): + """Function which is caled to send the message over the network + therfore the d_ suffix stands for distributed""" + #print "send message %s" % messageText + self.sendUpdate("sendText", [messageText]) + + def b_sendText(self, messageText): + """Function which combines the local and distributed functionality, + so the sendText and d_sendText functions are called. + The b_ suffix stands for both""" + self.sendText(messageText) + self.d_sendText(messageText) diff --git a/samples/networking/05-small-chat/sample.dc b/samples/networking/05-small-chat/sample.dc new file mode 100755 index 0000000000..08c6f4505f --- /dev/null +++ b/samples/networking/05-small-chat/sample.dc @@ -0,0 +1,5 @@ +from message import Message + +dclass Message: DistributedObject { + sendText(string messageText) broadcast; +} diff --git a/samples/networking/05-small-chat/server.py b/samples/networking/05-small-chat/server.py new file mode 100644 index 0000000000..874e41ec79 --- /dev/null +++ b/samples/networking/05-small-chat/server.py @@ -0,0 +1,34 @@ +# all imports needed by the engine itself +from direct.showbase.ShowBase import ShowBase + +# all imports needed by the server +from direct.distributed.ServerRepository import ServerRepository +from panda3d.core import ConfigVariableInt + +from AIRepository import AIRepository + +# initialize the engine +base = ShowBase(windowType='none') + +# the main server class +class GameServerRepository(ServerRepository): + """The server repository class""" + def __init__(self): + """initialise the server class""" + + # get the port number from the configuration file + # if it doesn't exist, we use 4400 as the default + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # list of all needed .dc files + dcFileNames = ['../direct.dc', 'sample.dc'] + + # initialise a threaded server on this machine with + # the port number and the dc filenames + ServerRepository.__init__(self, tcpPort, dcFileNames=dcFileNames, threadedNet=True) + +# start the server +GameServerRepository() +AIRepository() + +base.run() diff --git a/samples/networking/06-simple-avatar/AIRepository.py b/samples/networking/06-simple-avatar/AIRepository.py new file mode 100644 index 0000000000..9e46f024d3 --- /dev/null +++ b/samples/networking/06-simple-avatar/AIRepository.py @@ -0,0 +1,81 @@ +from direct.distributed.ClientRepository import ClientRepository +from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString + +class AIRepository(ClientRepository): + def __init__(self): + """ The AI Repository usually lives on a server and is responsible for + server side logic that will handle game objects """ + + # List of all dc files that are of interest to this AI Repository + dcFileNames = ['../direct.dc', 'sample.dc'] + + # Initialize the repository. We pass it the dc files and as this is an + # AI repository the dcSuffix AI. This will make sure any later calls to + # createDistributedObject will use the correct version. + # The connectMethod + ClientRepository.__init__( + self, + dcFileNames = dcFileNames, + dcSuffix = 'AI', + threadedNet = True) + + # Set the same port as configured on the server to be able to connect + # to it + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # Set the IP or hostname of the server we want to connect to + hostname = ConfigVariableString('server-host', '127.0.0.1').getValue() + + # Build the URL from the server hostname and port. If your server + # doesn't use http you should change it accordingly. Make sure to pass + # the connectMethod to the ClientRepository.__init__ call too. + # Available connection methods are: + # self.CM_HTTP, self.CM_NET and self.CM_NATIVE + url = URLSpec('http://{}:{}'.format(hostname, tcpPort)) + + # Attempt a connection to the server + self.connect([url], + successCallback = self.connectSuccess, + failureCallback = self.connectFailure) + + def connectFailure(self, statusCode, statusString): + """ something went wrong """ + print("Couldn't connect. Make sure to run server.py first!") + raise(StandardError, statusString) + + def connectSuccess(self): + """ Successfully connected. But we still can't really do + anything until we've got the doID range. """ + # The Client Repository will throw this event as soon as it has a doID + # range and would be able to create distributed objects + self.accept('createReady', self.gotCreateReady) + + def lostConnection(self): + """ This should be overridden by a derived class to handle an + unexpectedly lost connection to the gameserver. """ + exit() + + def gotCreateReady(self): + """ Now we're ready to go! """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if not self.haveCreateAuthority(): + # Not ready yet. + return + + # we are ready now, so ignore further createReady events + self.ignore('createReady') + + # Create a Distributed Object by name. This will look up the object in + # the dc files passed to the repository earlier + self.timeManager = self.createDistributedObject( + className = 'TimeManagerAI', # The Name of the Class we want to initialize + zoneId = 1) # The Zone this Object will live in + + print("AI Repository Ready") + + def deallocateChannel(self, doID): + """ This method will be called whenever a client disconnects from the + server. The given doID is the ID of the client who left us. """ + print("Client left us: ", doID) diff --git a/samples/networking/06-simple-avatar/ClientRepository.py b/samples/networking/06-simple-avatar/ClientRepository.py new file mode 100644 index 0000000000..145242260b --- /dev/null +++ b/samples/networking/06-simple-avatar/ClientRepository.py @@ -0,0 +1,103 @@ +from direct.distributed.ClientRepository import ClientRepository +from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString +from DistributedSmoothActor import DistributedSmoothActor + +class GameClientRepository(ClientRepository): + + def __init__(self): + dcFileNames = ['../direct.dc', 'sample.dc'] + + # a distributed object of our game. + self.distributedObject = None + self.aiDGameObect = None + + ClientRepository.__init__( + self, + dcFileNames = dcFileNames, + threadedNet = True) + + # Set the same port as configured on the server to be able to connect + # to it + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # Set the IP or hostname of the server we want to connect to + hostname = ConfigVariableString('server-host', '127.0.0.1').getValue() + + # Build the URL from the server hostname and port. If your server + # uses another protocol then http you should change it accordingly. + # Make sure to pass the connectMethod to the ClientRepository.__init__ + # call too. Available connection methods are: + # self.CM_HTTP, self.CM_NET and self.CM_NATIVE + self.url = URLSpec('http://{}:{}'.format(hostname, tcpPort)) + + # Attempt a connection to the server + self.connect([self.url], + successCallback = self.connectSuccess, + failureCallback = self.connectFailure) + + def lostConnection(self): + """ This should be overridden by a derived class to handle an + unexpectedly lost connection to the gameserver. """ + # Handle the disconnection from the server. This can be a reconnect, + # simply exiting the application or anything else. + exit() + + def connectFailure(self, statusCode, statusString): + """ Something went wrong """ + exit() + + def connectSuccess(self): + """ Successfully connected. But we still can't really do + anything until we've got the doID range. """ + + # Make sure we have interest in the by the AIRepository defined + # TimeManager zone, so we always see it even if we switch to + # another zone. + self.setInterestZones([1]) + + # We must wait for the TimeManager to be fully created and + # synced before we can enter another zone and wait for the + # game object. The uniqueName is important that we get the + # correct, our sync message from the TimeManager and not + # accidentaly a message from another client + self.acceptOnce(self.uniqueName('gotTimeSync'), self.syncReady) + + def syncReady(self): + """ Now we've got the TimeManager manifested, and we're in + sync with the server time. Now we can enter the world. Check + to see if we've received our doIdBase yet. """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if self.haveCreateAuthority(): + # we already have one + self.gotCreateReady() + else: + # Not yet, keep waiting a bit longer. + self.accept(self.uniqueName('createReady'), self.gotCreateReady) + + def gotCreateReady(self): + """ Ready to enter the world. Expand our interest to include + any other zones """ + + # This method checks whether we actually have a valid doID range + # to create distributed objects yet + if not self.haveCreateAuthority(): + # Not ready yet. + return + + # we are ready now, so ignore further createReady events + self.ignore(self.uniqueName('createReady')) + + self.join() + + print("Client Ready") + + def join(self): + """ Join a game/room/whatever """ + # set our intersted zones to let the client see all distributed obects + # in those zones + self.setInterestZones([1, 2]) + + base.messenger.send('client-joined') + print("Joined") diff --git a/samples/networking/06-simple-avatar/DistributedSmoothActor.py b/samples/networking/06-simple-avatar/DistributedSmoothActor.py new file mode 100644 index 0000000000..516f43c9f7 --- /dev/null +++ b/samples/networking/06-simple-avatar/DistributedSmoothActor.py @@ -0,0 +1,50 @@ +from direct.distributed.DistributedSmoothNode import DistributedSmoothNode +from panda3d.core import NodePath + +from direct.actor.Actor import Actor + +class DistributedSmoothActor(DistributedSmoothNode, Actor): + def __init__(self, cr): + Actor.__init__(self, "models/ralph", + {"run": "models/ralph-run", + "walk": "models/ralph-walk"}) + DistributedSmoothNode.__init__(self, cr) + self.setCacheable(1) + self.setScale(.2) + + def generate(self): + DistributedSmoothNode.generate(self) + self.activateSmoothing(True, False) + self.startSmooth() + + def announceGenerate(self): + DistributedSmoothNode.announceGenerate(self) + self.reparentTo(render) + + def disable(self): + # remove all anims, on all parts and all lods + self.stopSmooth() + if (not self.isEmpty()): + Actor.unloadAnims(self, None, None, None) + DistributedSmoothNode.disable(self) + + def delete(self): + try: + self.DistributedActor_deleted + except: + self.DistributedActor_deleted = 1 + DistributedSmoothNode.delete(self) + Actor.delete(self) + + def start(self): + # Let the DistributedSmoothNode take care of broadcasting the + # position updates several times a second. + self.startPosHprBroadcast() + + def loop(self, animName): + self.sendUpdate("loop", [animName]) + return Actor.loop(self, animName) + + def pose(self, animName, frame): + self.sendUpdate("pose", [animName, frame]) + return Actor.pose(self, animName, frame) diff --git a/samples/networking/06-simple-avatar/ServerRepository.py b/samples/networking/06-simple-avatar/ServerRepository.py new file mode 100644 index 0000000000..59da3a6aa4 --- /dev/null +++ b/samples/networking/06-simple-avatar/ServerRepository.py @@ -0,0 +1,20 @@ +# all imports needed by the server +from direct.distributed.ServerRepository import ServerRepository +from panda3d.core import ConfigVariableInt + +# the main server class +class GameServerRepository(ServerRepository): + """The server repository class""" + def __init__(self): + """initialise the server class""" + + # get the port number from the configuration file + # if it doesn't exist, we use 4400 as the default + tcpPort = ConfigVariableInt('server-port', 4400).getValue() + + # list of all needed .dc files + dcFileNames = ['../direct.dc', 'sample.dc'] + + # initialise a threaded server on this machine with + # the port number and the dc filenames + ServerRepository.__init__(self, tcpPort, dcFileNames=dcFileNames, threadedNet=True) diff --git a/samples/networking/06-simple-avatar/client.py b/samples/networking/06-simple-avatar/client.py new file mode 100644 index 0000000000..c19cd6b2d4 --- /dev/null +++ b/samples/networking/06-simple-avatar/client.py @@ -0,0 +1,151 @@ +# all imports needed by the engine itself +from direct.showbase.ShowBase import ShowBase +from panda3d.core import KeyboardButton, NodePath, PandaNode + +# import our own repositories +from ClientRepository import GameClientRepository +from DistributedSmoothActor import DistributedSmoothActor + +# initialize the engine +base = ShowBase() + +base.disableMouse() + +class Avatar: + def __init__(self, cr): + self.cr = cr + self.ralph = DistributedSmoothActor(self.cr) + + self.cr.createDistributedObject( + distObj = self.ralph, + zoneId = 2) + + # Create a floater object, which floats 2 units above ralph. We + # use this as a target for the camera to look at. + + self.floater = NodePath(PandaNode("floater")) + self.floater.reparentTo(self.ralph) + self.floater.setZ(2.0) + + # We will use this for checking if keyboard keys are pressed + self.isDown = base.mouseWatcherNode.isButtonDown + + taskMgr.add(self.move, "moveTask") + + # Set up the camera + base.camera.setPos(self.ralph.getX(), self.ralph.getY() + 10, 2) + + # start the avatar + self.ralph.start() + + # Accepts arrow keys to move either the player or the menu cursor, + # Also deals with grid checking and collision detection + def move(self, task): + + # Get the time that elapsed since last frame. We multiply this with + # the desired speed in order to find out with which distance to move + # in order to achieve that desired speed. + dt = globalClock.getDt() + + # If the camera-left key is pressed, move camera left. + # If the camera-right key is pressed, move camera right. + + if self.isDown(KeyboardButton.asciiKey(b"j")): + base.camera.setX(base.camera, -20 * dt) + if self.isDown(KeyboardButton.asciiKey(b"k")): + base.camera.setX(base.camera, +20 * dt) + + # If a move-key is pressed, move ralph in the specified direction. + + if self.isDown(KeyboardButton.asciiKey(b"a")): + self.ralph.setH(self.ralph.getH() + 300 * dt) + if self.isDown(KeyboardButton.asciiKey(b"d")): + self.ralph.setH(self.ralph.getH() - 300 * dt) + if self.isDown(KeyboardButton.asciiKey(b"w")): + self.ralph.setY(self.ralph, -20 * dt) + if self.isDown(KeyboardButton.asciiKey(b"s")): + self.ralph.setY(self.ralph, +10 * dt) + + # update distributed position and rotation + #self.ralph.setDistPos(self.ralph.getX(), self.ralph.getY(), self.ralph.getZ()) + #self.ralph.setDistHpr(self.ralph.getH(), self.ralph.getP(), self.ralph.getR()) + + # If ralph is moving, loop the run animation. + # If he is standing still, stop the animation. + currentAnim = self.ralph.getCurrentAnim() + + if self.isDown(KeyboardButton.asciiKey(b"w")): + if currentAnim != "run": + self.ralph.loop("run") + elif self.isDown(KeyboardButton.asciiKey(b"s")): + # Play the walk animation backwards. + if currentAnim != "walk": + self.ralph.loop("walk") + self.ralph.setPlayRate(-1.0, "walk") + elif self.isDown(KeyboardButton.asciiKey(b"a")) or self.isDown(KeyboardButton.asciiKey(b"d")): + if currentAnim != "walk": + self.ralph.loop("walk") + self.ralph.setPlayRate(1.0, "walk") + else: + if currentAnim is not None: + self.ralph.stop() + self.ralph.pose("walk", 5) + self.isMoving = False + + # If the camera is too far from ralph, move it closer. + # If the camera is too close to ralph, move it farther. + + camvec = self.ralph.getPos() - base.camera.getPos() + camvec.setZ(0) + camdist = camvec.length() + camvec.normalize() + if camdist > 10.0: + base.camera.setPos(base.camera.getPos() + camvec * (camdist - 10)) + camdist = 10.0 + if camdist < 5.0: + base.camera.setPos(base.camera.getPos() - camvec * (5 - camdist)) + camdist = 5.0 + + # The camera should look in ralph's direction, + # but it should also try to stay horizontal, so look at + # a floater which hovers above ralph's head. + base.camera.lookAt(self.floater) + + return task.cont + +base.accept("escape", exit) + +# initialize the client +client = GameClientRepository() + + +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import TextNode + +# Function to put instructions on the screen. +def addInstructions(pos, msg): + return OnscreenText(text=msg, style=1, fg=(0, 0, 0, 1), shadow=(1, 1, 1, 1), + parent=base.a2dTopLeft, align=TextNode.ALeft, + pos=(0.08, -pos - 0.04), scale=.06) + +# Function to put title on the screen. +def addTitle(text): + return OnscreenText(text=text, style=1, pos=(-0.1, 0.09), scale=.08, + parent=base.a2dBottomRight, align=TextNode.ARight, + fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1)) + +title = addTitle("Panda3D: Tutorial - Distributed Network (NOT CONNECTED)") +inst1 = addInstructions(0.06, "W|A|S|D: Move avatar)") +inst2 = addInstructions(0.12, "esc: Close the client") +inst3 = addInstructions(0.24, "See console output") + +def clientJoined(): + title["text"] = "Panda3D: Tutorial - Distributed Network (CONNECTED)" + + # Setup our avatar + Avatar(client) + +base.accept("client-joined", clientJoined) + +# start the client +base.run() diff --git a/samples/networking/06-simple-avatar/models/ralph-run.egg.pz b/samples/networking/06-simple-avatar/models/ralph-run.egg.pz new file mode 100644 index 0000000000000000000000000000000000000000..e50ff41d24b38d9598a998900ff71f0529a686e3 GIT binary patch literal 5040 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 G&+_MQN4TQ^ literal 0 HcmV?d00001 diff --git a/samples/networking/06-simple-avatar/models/ralph-walk.egg.pz b/samples/networking/06-simple-avatar/models/ralph-walk.egg.pz new file mode 100644 index 0000000000000000000000000000000000000000..955352549c64cdee5636dd51f897330b3dacb60c GIT binary patch literal 6458 zcmV-A8O7#!+U;FkZyh(1efO_u1oO1OZLs)R1q23?V1e14U>_#go!zH}k|=Qy*)n7g zHqOBReaqnn{ee%uOX)p29&;S1HJO0rhcKY)C+2c>POK&dv17Dm!dwFqw_Tus?uK)Mx6qw4=yVn#EsT>aw7$;H1sULcXv3tWEj{K>`3D|fs(+t=f( zt%}!QaziCaO~>1Fsk9xoTaDjb*o>+xH^EsGZXTVu(UM~lT4t*bWt+*Ye;QCmX^ zwphTw)w0Q3WmsH8YKx^+Vp%M&p=IH*<&qaGZ%+4@{LX1P{WW~!{^k3+Y&F|gv{!u4 zreFPZT}Bg$y}G5uWVM#m{8)41QDajcO@jYz7HvUeLaEu0D*9GA`|JJw{qu1vuWsdU zuiO7QstxWg{pqc&(#tNky*pk0yZw!Y+hfx z``Rqq=FS&SPG7wI^~AVw>fAheZa00d)c@zp`iHeyFm1o8{aY>HTJRsI-~RM=^={FG z+ce^~&A8H#CxaKw#y0;8>@dsduCnZ8)tFF^U0kbjIPTI~EH1me7A+W-yRtSZ!-95o zl_I<&HB~;TO?VPlkpRVm_taRkO~CWrC_mOL0;M zuO6$Cp(@P(PSACH2#N_c^ZOz7=hKt# zK0kkUIvO&!8d$d*TDKou*A2~nonk;tT((W;g-5zKY-bNNgV43JW8e(0C0N(vgj#>q3s$oQ{6{_x}j5c2ak z0}oUAW?B7w)gaxuSE{_%UYTixe?R%Zk4PC1AxLh(h&MfRRkRAQ*lwplW@4l|0pd9Q z>0=Y7)SBu|&%C<&OsXoi9V7n=WDXqsF7kI(^I$vRRvP9$x{DY+9?@OwElDjM)mynh zf4x(0eSQA?OVIj{34}<@67{Hw5<*B>5>V?gx~c|AchOa2jzPWk)syqTpSgrST&_`V z&8gO<5vFTiS_J`ZnZHNS#gf((@MkJ($t4bph;>!LJu!l&MNn2{r-DwW)e1VvFjs0J z1uhkxHe)IUqylU}%Bo4g55!i367^rCftKT*2A;u!YE&K51Ob~!6}+1uALT_`TSjHF ztg)uNl(;4hwp3XY=I>M{Ro|6wJU_B0c97wkL*ohfSrne2mfEjG^DAI<5UHO)*0X{d zdM);o(zKs6bWJ~z?W9Vc7m#D?Cz7}KIr-#C*0h~iKUUFMK^PlltBFYNC$Vlv+bTi@ z&!I>|HzO;^@x7fC-KDt%> z`vkKjdyzO1wO;E`+N=N!GatAVl9u~p>K#dCnA)M@9-D_!tLAzHxpzu?_RaZ&1oTg4 z%x*eXmj(UU2+T``GR)B8d9{Xtg@Fc5tV_b&59qK6^#UC`DFq5xIlv?CHmD)kfRGic zG*de@w++Sc7RA`gGOLTOP*@z$4ayuNyj5uT0ysn_m+>rXh?+qym01~a)rc8_r!4fR z3+4rD&{{OM)7@EYcd&RsDk|fvx8sIGWO_SbdUPMs-v{1W{iL~@3~H9D8y2j(PNi40 zxka|e?EEk-K@#C7S8R(bDV%?<3Oy)TuATg?K}Pc z`_pe2}CK! z33yFLe8(76V>k#k0@v$I0pOjaZwIy)g@G%e1Fy1HI$TuiZ3aT74dMUvwhQPqFfq9Ne5)5HZDm;SJ@?MmXS(Zp3#v+*!Ae^;=FOACp zC>Nm^0$G+&9I4@NP@}?7TPCaqY68rOVM2|(&W-P7|1sdKC4m;OUSwSSyYXj>CD~LQ zE!X$Ysi;b5${XrVG{``u5L18>CU(qD1Sp|17@X7z6$04@+~shj)GMbGNLHcq04IG6oxd zH@ce0hXfcIYy`8Z%vnedbVz|+fp9MwN(kC$$utzL@^8Re=nrr;Go6%@lzb}tQw-XG zlUdNpr=kKBW3*efxL^=zv>7S~v$KLh2Q3aH4PC*Op+&GS7`td$SR+-Khw#fGv7~n2 zv}*U;@v2y2?cB#waw;R3vZZGtCs zFpG?ENKJ*8p}wOwz-D5u0DX$GBnIQLpllwSmLXW?9ii}LP}@cLdVuy8lJq<~R7JgK zEeZ_%s49UGGZD4~pdP#m%%qT3G5!M003eozRnfwhADn-0!10*#k`{EgxY8ghvt*eJS*8nw`QsX58^zv;4jZkPal%o z>sSv9=BKsNNFAbpH)eV`$V)(!$$mh0-EV|>i)lP&IYJ&4&2tZ=Gp2}f#~>-jKbVeE z@d%?jW0dTfB4Xl-l!Fz$TdBbS@s&Bt(88Dt0PbAsuqKi zyaF_rrMeHu!&50#J0y^ICGporV1}iNN5(lnd-l`W>BqvzcbRED?hyrXjm>#WXf1hW zJB)E)76c4Nfx5GKe2k$f7?D7$A6oWh90_Jbi71rOD%b~%o+*!}dnD1xmIV+Yaj1nx z_Zf>CrjME%XHY&0-+tile;%drUsV46-XF| zjC9YOB@ifKBEUwV5~8y$c{T~OLd#qS!N_u*Mvixds~2b}i!vro{~+{+0Uqv{^by_j z&UB7A<=TusCk{+zdt?sPO+xq6M|X6EgxDjhvE`h46oSZ=^grd)JqW5f&gV{L*tqt& zQ@AjN>X`_PK841R2Sd!$VNbs_4MHMv5W3SP(eJ?33DxstT+h|m=2d9Pm<;GCw6S0U zvNEwbCJF9QcR)|~cu5C}sfCN*7#xS4X?yY9PyhT1I;0?X2rPeoa(4E>7X-{m|AAq+ zbe#v_sNKzuo`>|A8A7)?^g~KM6qPeAPD^2Uj7~hj26y2nW;w7VXdo!!4(}C3%}dB6 z1oXxc#J>5XKX#>{t>^<)*;k_1X{-8`#`M-5sL5Uod%=Xsy|?$5Oh==@cGlw-RYV}N znC_SZ@b&rWhv1*O+TP5Fce-cy)C{NXomKJ>aoQaf*>V1;?#KM_98w+!u;gC+2x{QI zcm>Sq6IrcwJep%3<{opsWzf`NAZ8h`TyVpN2c;vzQ0Xt(kvG*gbZkTtA}cG zj2PZz*nz9p8xfAX(Be1=audt@@>mg~Gucp?GiaN~%G1$7z9IeSZl7G~AEn1?n3}nF zST*K2xWg*2PmE!CO2L@e%))VEGWPfRrRYbD!Q3CJlsx@ZM$Bh)w9;PFHVW zCID5&=eeB)*U3+^wz>AEHjm(oGOK_k0>kEpb}<8-dDj3kP;MCa?T{>fS67t}s*lT8 zMGq)cmNVMwPEdyKgPPl6gF$L-yVmB31LU~I^(VB-=xOHcF{iMSXcK8L6U-xrOdp!& z_H%$WrEf|yoiI$#j!B%n2mH;l1GBC1zAQD)IFrMaRH`>8cwS7fj&-Ve3u6qOo5KK0 zx=A>{pTnxsP1mz{GE*aIEHiTiDI#v<(iowLJ#(|os2^cj=xb@Vcull5d9WpM2OY{J za|4a3JA;um+dxLN+F2{8U?;##Qe*Z>ftFzCYvu`L9$U$Dj;9L6!K5-E7OIY7*&9>o zkyKw?_y-u@bm{G;ZlxOxtcMV5hM8jnZK81pZXc@r_(`+dQf=l)J>n7ZqFXv;_ zExF$f+PWtcwV3N%ATfHr;wG5~;-hk{{joZ!Zs`-c^!?oAr<(bME`2}q0xo?*xA2=C zC}m%P8~kPmB(fL66~EcX;imGD{D+_Fc7Ce(|IAPI_I`tdEf3$%euL9wuG7=AFJ4}p zJp>2Zaj8a)i>i|^H4$G;9hr>GtQPJ}5T*7|&9=4y+lzKEMi#A82FVEk8otxW0; zrLt;t_YpA#pJrYauWYTq4+p`GjQ`aU-NF!g>|r^k$Elw{kc_phF=zqZ1%z+4dUh9+ z-b{w&#K6SHrDsZLbd}ZA19i3J?3Z9uf-!y0T9BNEsdh|K3fxPPp1DAp3{c12o=QxW zmx>kULZke`Z70T!@;j3hCVltRv68%v#DV^cNb6Z;+SCq6=I&kg%TfK1G1^l0E$n>x z5vv9w$6AZ@rCdyB%;RNC?!Ilxg}fAeD*gV7ap@jiO@*rnb}ru`F(9K0(J5X}S0~)v zmKK-JX}K0zHga3)xE2~Pb5n=F;=35KMSh4Sas3pV0XS}PoJlBXym)vwv|`N#(!~3- zi|F2Vd7*3T%kytfVCWu_hwIgq6!TU3CsI8TWL_$ZJaN}HViNbV2X0ig6`t;!L#*uq&4LyUt`jy*oG3MvV?XaGV zIz``%S3H)OGwo=0OI+sI+HWaA%tM@xZ+6Wi!`%vJZVl{9DrD&PHSR4_J%!hIDCT4i zTr5baoqem9z)fZ<9ht)tHhJ7r80;Ql?i*Mcn)|*LLU1J7Fw_Yqi&}P^nM)e)%;Y-* z0(|SDujnBM1@zWH(>VAATG1fa6-1d^TM07b{Z zzLkLSP-m$elCt##W3bECjY{9>T7rerscaiAD);;fRJ5<~HfCb_ygpQ zyni1R%Dr^Xe@rm<4fMPf$ee-84GZ4{GWTcGpz4iCdpR5&xEX0b6rB4eK>r8Oy-%RK z_c;99Umn^gIPTS*SYw89gR$f$?w2dc#@oI6${tnP=PFekbhRMC ze`ywkiIn{21@q(rGhS>6uZewP^ayYW;$M~tAvsLK1@(LgOQq*SXss!XOVN;+@kXOA zrJuyjZCWvZn^zQ2(BW1EOrJPOyL8@{OA!sN4swYt9OfkMW8$t|TyW=_q#~tn98*Hf z6XXTB(?1mw6E{ln)DWmk>$^Spa%x`*O&9#Qgci+OM|k`ai1gq14U&$UG+EyYV={TA%@AEx7Zt{&d@+6xD=7?u5v&! z?}=pjO!6-Rku53NJ7*|F);;klQ!+kv1XFIBsg9Y@l|qJwb$th3Yt@NsU>aNK(I72p zEIQCKU-QV-mg*gd`4wH@Fx4;!eiD-RHtcYdvEr2&isr7Lw1lpNL~8176qRns`O&h8 z)0+qNrgPEMvgPEtxPAju|JlXU2Q%l7na9nUElwbLcTZH+=vG%Sb=+fu8Q?Qf zwWU}mRyT0QRy_kfB%NCoL*2BPjC*h|^>;HMRl%is2!6yQ>fll4~(IiYNK{oem@d=?^Z{4eC zAaawz15*(D%fZR|Oj2$c-}Azu!CT&B7IJ=X%Ck1}_2xho=IhR@?~^KGMnxA|%i6V$ z>R3-zZUae8Cd{>lrE_(qo`yk%$9)~CQLCp3gZ~V@^qx7?H76jCYjWt8?K7m&KLIKX zs8EOJVI~V-9bL;S=&yPmLVpsv!5a~{^vriBl>EMkWq|>91qDK7Zi$n2SR8V34qWJi zuid#&YMx~bRlk{Ee@n4LJ7~JivkrE+0oe>@I`O1e?Lh_3DWP%cLpSy^IEeeaYy`tL zEyMbVEg-Viu_BCQpG%Qph1j)dmhlpyo|q%f6(X30Gs{fOqg`6r;=Lv)_T%eo-*0|; UesTFtXI^>z==Gz20ymr$g%+8k{{R30 literal 0 HcmV?d00001 diff --git a/samples/networking/06-simple-avatar/models/ralph.egg.pz b/samples/networking/06-simple-avatar/models/ralph.egg.pz new file mode 100644 index 0000000000000000000000000000000000000000..5749c1165088cad12c9a8d289f70d83dfaa04a21 GIT binary patch literal 184607 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

fqnj?GC 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

) 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 Date: Tue, 2 Nov 2021 14:08:27 +0100 Subject: [PATCH 19/68] workflow: Update Ubuntu builder to 18.04 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a06b563405..ecca594aa9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,12 +6,12 @@ jobs: if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')" strategy: matrix: - os: [ubuntu-16.04, windows-2016, macOS-latest] + os: [ubuntu-18.04, windows-2016, macOS-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v1 - name: Install dependencies (Ubuntu) - if: matrix.os == 'ubuntu-16.04' + if: matrix.os == 'ubuntu-18.04' run: | sudo apt-get update sudo apt-get install build-essential bison flex libfreetype6-dev libgl1-mesa-dev libjpeg-dev libode-dev libopenal-dev libpng-dev libssl-dev libvorbis-dev libx11-dev libxcursor-dev libxrandr-dev nvidia-cg-toolkit zlib1g-dev From e87a2a1f0f9524bcd208ad8bc3031ab0ed49244b Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 2 Nov 2021 15:01:18 +0100 Subject: [PATCH 20/68] showbase: Fix typo in API reference --- direct/src/showbase/ShowBase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index 72197ffabf..5efe908ecd 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -728,7 +728,7 @@ class ShowBase(DirectObject.DirectObject): of :meth:`~panda3d.core.GraphicsWindow.setUnexposedDraw()`. :param callbackWindowDict: If not None, a - :class:`~panda3d.core.CallbackGraphicWindow` + :class:`~panda3d.core.CallbackGraphicsWindow` is created instead, which allows the caller to create the actual window with its own OpenGL context, and direct Panda's rendering From c1888ba43709123323f38eda266b3744e59d7c46 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 2 Nov 2021 15:01:42 +0100 Subject: [PATCH 21/68] makepackage: Add Python 3.11 to installer.nsi --- makepanda/installer.nsi | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/makepanda/installer.nsi b/makepanda/installer.nsi index 0600d8c449..6696a0cf0b 100644 --- a/makepanda/installer.nsi +++ b/makepanda/installer.nsi @@ -424,6 +424,7 @@ SectionGroup "Python modules" SecGroupPython !insertmacro PyBindingSection 3.8-32 .cp38-win32.pyd !insertmacro PyBindingSection 3.9-32 .cp39-win32.pyd !insertmacro PyBindingSection 3.10-32 .cp310-win32.pyd + !insertmacro PyBindingSection 3.11-32 .cp311-win32.pyd !else !insertmacro PyBindingSection 3.5 .cp35-win_amd64.pyd !insertmacro PyBindingSection 3.6 .cp36-win_amd64.pyd @@ -431,6 +432,7 @@ SectionGroup "Python modules" SecGroupPython !insertmacro PyBindingSection 3.8 .cp38-win_amd64.pyd !insertmacro PyBindingSection 3.9 .cp39-win_amd64.pyd !insertmacro PyBindingSection 3.10 .cp310-win_amd64.pyd + !insertmacro PyBindingSection 3.11 .cp311-win_amd64.pyd !endif SectionGroupEnd @@ -540,6 +542,7 @@ Function .onInit ${If} ${AtLeastWin8} !insertmacro MaybeEnablePyBindingSection 3.9-32 !insertmacro MaybeEnablePyBindingSection 3.10-32 + !insertmacro MaybeEnablePyBindingSection 3.11-32 ${EndIf} !else !insertmacro MaybeEnablePyBindingSection 3.5 @@ -549,6 +552,7 @@ Function .onInit ${If} ${AtLeastWin8} !insertmacro MaybeEnablePyBindingSection 3.9 !insertmacro MaybeEnablePyBindingSection 3.10 + !insertmacro MaybeEnablePyBindingSection 3.11 ${EndIf} !endif @@ -562,6 +566,10 @@ Function .onInit SectionSetFlags ${SecPyBindings3.10} ${SF_RO} SectionSetInstTypes ${SecPyBindings3.10} 0 !endif + !ifdef SecPyBindings3.11 + SectionSetFlags ${SecPyBindings3.11} ${SF_RO} + SectionSetInstTypes ${SecPyBindings3.11} 0 + !endif ${EndUnless} FunctionEnd @@ -873,6 +881,7 @@ Section Uninstall !insertmacro RemovePythonPath 3.8-32 !insertmacro RemovePythonPath 3.9-32 !insertmacro RemovePythonPath 3.10-32 + !insertmacro RemovePythonPath 3.11-32 !else !insertmacro RemovePythonPath 3.5 !insertmacro RemovePythonPath 3.6 @@ -880,6 +889,7 @@ Section Uninstall !insertmacro RemovePythonPath 3.8 !insertmacro RemovePythonPath 3.9 !insertmacro RemovePythonPath 3.10 + !insertmacro RemovePythonPath 3.11 !endif SetDetailsPrint both @@ -955,6 +965,7 @@ SectionEnd !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.8-32} $(DESC_SecPyBindings3.8-32) !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.9-32} $(DESC_SecPyBindings3.9-32) !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.10-32} $(DESC_SecPyBindings3.10-32) + !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.11-32} $(DESC_SecPyBindings3.11-32) !else !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.5} $(DESC_SecPyBindings3.5) !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.6} $(DESC_SecPyBindings3.6) @@ -962,6 +973,7 @@ SectionEnd !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.8} $(DESC_SecPyBindings3.8) !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.9} $(DESC_SecPyBindings3.9) !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.10} $(DESC_SecPyBindings3.10) + !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.11} $(DESC_SecPyBindings3.11) !endif !ifdef INCLUDE_PYVER !insertmacro MUI_DESCRIPTION_TEXT ${SecPython} $(DESC_SecPython) From 9587965c5dcedd958dc2aa3454b77ccf36db90c0 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 2 Nov 2021 15:02:14 +0100 Subject: [PATCH 22/68] bullet: Add some API documentation strings for BulletDebugNode --- panda/src/bullet/bulletDebugNode.I | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/panda/src/bullet/bulletDebugNode.I b/panda/src/bullet/bulletDebugNode.I index eae681aba5..e29863669f 100644 --- a/panda/src/bullet/bulletDebugNode.I +++ b/panda/src/bullet/bulletDebugNode.I @@ -20,7 +20,7 @@ INLINE BulletDebugNode:: } /** - * + * If true, displays collision shapes in wireframe mode. */ INLINE void BulletDebugNode:: show_wireframe(bool show) { @@ -39,7 +39,8 @@ get_show_wireframe() const { } /** - * + * If true, display limits defined for constraints, e.g. a pivot axis or maximum + * amplitude. */ INLINE void BulletDebugNode:: show_constraints(bool show) { @@ -58,7 +59,7 @@ get_show_constraints() const { } /** - * + * If true, displays axis aligned bounding boxes for objects. */ INLINE void BulletDebugNode:: show_bounding_boxes(bool show) { @@ -77,7 +78,7 @@ get_show_bounding_boxes() const { } /** - * + * If true, displays normal vectors for triangle mesh and heightfield faces. */ INLINE void BulletDebugNode:: show_normals(bool show) { @@ -92,4 +93,4 @@ INLINE bool BulletDebugNode:: get_show_normals() const { return _drawer._normals; -} \ No newline at end of file +} From 04c6636744ce583ed6104b7fed20e3d8310562d4 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 2 Nov 2021 15:02:40 +0100 Subject: [PATCH 23/68] pgraph: Add some missing properties to PlaneNode --- panda/src/pgraph/planeNode.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/panda/src/pgraph/planeNode.h b/panda/src/pgraph/planeNode.h index 2b58225abd..6debd7a217 100644 --- a/panda/src/pgraph/planeNode.h +++ b/panda/src/pgraph/planeNode.h @@ -65,6 +65,11 @@ PUBLISHED: INLINE void set_clip_effect(int clip_effect); INLINE int get_clip_effect() const; + MAKE_PROPERTY(plane, get_plane, set_plane); + MAKE_PROPERTY(viz_scale, get_viz_scale, set_viz_scale); + MAKE_PROPERTY(priority, get_priority, set_priority); + MAKE_PROPERTY(clip_effect, get_clip_effect, set_clip_effect); + public: INLINE static UpdateSeq get_sort_seq(); From 29294cb9749a6497fd5babb60bcfaf0ba6aeb0d9 Mon Sep 17 00:00:00 2001 From: Epihaius Date: Mon, 1 Nov 2021 15:48:04 +0100 Subject: [PATCH 24/68] interval: Redefine some properties in MetaInterval class The `play_rate` property invokes the underlying C++ `set_play_rate` method, which leads to a bug (#1202). The property is now redefined to make it invoke the overriding `MetaInterval.set_play_rate` method instead. Fixes #1202 Closes #1204 --- direct/src/interval/MetaInterval.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/direct/src/interval/MetaInterval.py b/direct/src/interval/MetaInterval.py index 4f31c9839d..8c2e26908f 100644 --- a/direct/src/interval/MetaInterval.py +++ b/direct/src/interval/MetaInterval.py @@ -348,10 +348,14 @@ class MetaInterval(CMetaInterval): def getManager(self): return self.__manager + manager = property(getManager, setManager) + def setT(self, t): self.__updateIvals() CMetaInterval.setT(self, t) + t = property(CMetaInterval.getT, setT) + def start(self, startT = 0.0, endT = -1.0, playRate = 1.0): self.__updateIvals() self.setupPlay(startT, endT, playRate, 0) @@ -475,6 +479,8 @@ class MetaInterval(CMetaInterval): else: CMetaInterval.setPlayRate(self, playRate) + play_rate = property(CMetaInterval.getPlayRate, setPlayRate) + def __doPythonCallbacks(self): # This function invokes any Python-level Intervals that need # to be invoked at this point in time. It must be called @@ -549,6 +555,8 @@ class MetaInterval(CMetaInterval): self.__updateIvals() return CMetaInterval.getDuration(self) + duration = property(getDuration) + def __repr__(self, *args, **kw): # This function overrides from the parent level to force it to # update the interval list first, if necessary. From cf681eb8dd2e8dfbb84b7978d3ac453f1df175d0 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 2 Nov 2021 15:34:13 +0100 Subject: [PATCH 25/68] bullet: Clean up fix for #1193 See ad7cbcd4ec447d4423e05b0e0b813327bd722bf3 --- panda/src/bullet/bulletPersistentManifold.cxx | 14 +------------- panda/src/bullet/bulletPersistentManifold.h | 7 +++---- panda/src/bullet/bulletWorld.cxx | 15 +-------------- panda/src/bullet/bulletWorld.h | 7 +++---- 4 files changed, 8 insertions(+), 35 deletions(-) diff --git a/panda/src/bullet/bulletPersistentManifold.cxx b/panda/src/bullet/bulletPersistentManifold.cxx index f5e96d0464..ba1ca9a2e3 100644 --- a/panda/src/bullet/bulletPersistentManifold.cxx +++ b/panda/src/bullet/bulletPersistentManifold.cxx @@ -104,23 +104,11 @@ get_num_manifold_points() const { return _manifold->getNumContacts(); } -/** - * - */ -BulletManifoldPoint *BulletPersistentManifold:: -get_manifold_point(int idx) const { - LightMutexHolder holder(BulletWorld::get_global_lock()); - - nassertr(idx < _manifold->getNumContacts(), nullptr) - - return new BulletManifoldPoint(_manifold->getContactPoint(idx)); -} - /** * */ BulletManifoldPoint BulletPersistentManifold:: -__get_manifold_point(int idx) const { +get_manifold_point(int idx) const { LightMutexHolder holder(BulletWorld::get_global_lock()); return BulletManifoldPoint(_manifold->getContactPoint(idx)); } diff --git a/panda/src/bullet/bulletPersistentManifold.h b/panda/src/bullet/bulletPersistentManifold.h index 06e9b47564..13954ede0c 100644 --- a/panda/src/bullet/bulletPersistentManifold.h +++ b/panda/src/bullet/bulletPersistentManifold.h @@ -34,9 +34,8 @@ PUBLISHED: PandaNode *get_node1(); int get_num_manifold_points() const; - BulletManifoldPoint *get_manifold_point(int idx) const; - BulletManifoldPoint __get_manifold_point(int idx) const; - MAKE_SEQ(get_manifold_points, get_num_manifold_points, __get_manifold_point); + BulletManifoldPoint get_manifold_point(int idx) const; + MAKE_SEQ(get_manifold_points, get_num_manifold_points, get_manifold_point); PN_stdfloat get_contact_breaking_threshold() const; PN_stdfloat get_contact_processing_threshold() const; @@ -45,7 +44,7 @@ PUBLISHED: MAKE_PROPERTY(node0, get_node0); MAKE_PROPERTY(node1, get_node1); - MAKE_SEQ_PROPERTY(manifold_points, get_num_manifold_points, __get_manifold_point); + MAKE_SEQ_PROPERTY(manifold_points, get_num_manifold_points, get_manifold_point); MAKE_PROPERTY(contact_breaking_threshold, get_contact_breaking_threshold); MAKE_PROPERTY(contact_processing_threshold, get_contact_processing_threshold); diff --git a/panda/src/bullet/bulletWorld.cxx b/panda/src/bullet/bulletWorld.cxx index 4874f86db3..8a4bbfb0d3 100644 --- a/panda/src/bullet/bulletWorld.cxx +++ b/panda/src/bullet/bulletWorld.cxx @@ -1061,24 +1061,11 @@ contact_test_pair(PandaNode *node0, PandaNode *node1) const { return cb; } -/** - * - */ -BulletPersistentManifold *BulletWorld:: -get_manifold(int idx) const { - LightMutexHolder holder(get_global_lock()); - - nassertr(idx < _dispatcher->getNumManifolds(), nullptr); - - btPersistentManifold *ptr = _dispatcher->getManifoldByIndexInternal(idx); - return (ptr) ? new BulletPersistentManifold(ptr) : nullptr; -} - /** * */ BulletPersistentManifold BulletWorld:: -__get_manifold(int idx) const { +get_manifold(int idx) const { LightMutexHolder holder(get_global_lock()); nassertr(idx < _dispatcher->getNumManifolds(), BulletPersistentManifold(nullptr)); diff --git a/panda/src/bullet/bulletWorld.h b/panda/src/bullet/bulletWorld.h index 669c8944d3..83c0ac634c 100644 --- a/panda/src/bullet/bulletWorld.h +++ b/panda/src/bullet/bulletWorld.h @@ -127,9 +127,8 @@ PUBLISHED: // Manifolds int get_num_manifolds() const; - BulletPersistentManifold *get_manifold(int idx) const; - BulletPersistentManifold __get_manifold(int idx) const; - MAKE_SEQ(get_manifolds, get_num_manifolds, __get_manifold); + BulletPersistentManifold get_manifold(int idx) const; + MAKE_SEQ(get_manifolds, get_num_manifolds, get_manifold); // Collision filtering void set_group_collision_flag(unsigned int group1, unsigned int group2, bool enable); @@ -169,7 +168,7 @@ PUBLISHED: MAKE_SEQ_PROPERTY(characters, get_num_characters, get_character); MAKE_SEQ_PROPERTY(vehicles, get_num_vehicles, get_vehicle); MAKE_SEQ_PROPERTY(constraints, get_num_constraints, get_constraint); - MAKE_SEQ_PROPERTY(manifolds, get_num_manifolds, __get_manifold); + MAKE_SEQ_PROPERTY(manifolds, get_num_manifolds, get_manifold); MAKE_PROPERTY(force_update_all_aabbs, get_force_update_all_aabbs, set_force_update_all_aabbs); From 77710bb813688209e1cb136952a33b90b0f2e1b0 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 2 Nov 2021 15:35:54 +0100 Subject: [PATCH 26/68] workflow: Change Ubuntu Xenial builder to Bionic --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed4959bf82..71d41e1b8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: matrix: profile: - - ubuntu-xenial-standard-unity-makefile + - ubuntu-bionic-standard-unity-makefile - ubuntu-bionic-coverage-ninja - macos-eigen-coverage-unity-xcode - macos-nometa-standard-makefile @@ -19,8 +19,8 @@ jobs: - windows-nopython-nometa-standard-msvc include: - - profile: ubuntu-xenial-standard-unity-makefile - os: ubuntu-16.04 + - profile: ubuntu-bionic-standard-unity-makefile + os: ubuntu-18.04 config: Standard unity: YES generator: Unix Makefiles From 32584f0a68fd1043321d8d9dfe5863cc35c1c83f Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 9 Nov 2021 15:46:14 +0100 Subject: [PATCH 27/68] pgraph: Fix prepare_scene() not properly invoking the shader generator --- panda/src/pgraph/geomNode.cxx | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/panda/src/pgraph/geomNode.cxx b/panda/src/pgraph/geomNode.cxx index eb1148b126..a485851448 100644 --- a/panda/src/pgraph/geomNode.cxx +++ b/panda/src/pgraph/geomNode.cxx @@ -396,6 +396,24 @@ r_prepare_scene(GraphicsStateGuardianBase *gsg, const RenderState *node_state, geom_state = state_munger->munge_state(geom_state); } + // As well as the shaders. + const ShaderAttrib *sa; + if (geom_state->get_attrib(sa)) { + Shader *shader = (Shader *)sa->get_shader(); + if (shader != nullptr) { + prepared_objects->enqueue_shader(shader); + } + else if (sa->auto_shader()) { + gsg->ensure_generated_shader(geom_state); + } + else if (munger->is_of_type(StateMunger::get_class_type())) { + // Premunge the state for the fixed-function pipeline. + StateMunger *state_munger = (StateMunger *)munger.p(); + geom_state = state_munger->munge_state(geom_state); + } + // TODO: prepare the shader inputs. + } + // And now prepare each of the textures. const TextureAttrib *ta; if (geom_state->get_attrib(ta)) { @@ -408,16 +426,6 @@ r_prepare_scene(GraphicsStateGuardianBase *gsg, const RenderState *node_state, } } } - - // As well as the shaders. - const ShaderAttrib *sa; - if (geom_state->get_attrib(sa)) { - Shader *shader = (Shader *)sa->get_shader(); - if (shader != nullptr) { - prepared_objects->enqueue_shader(shader); - } - // TODO: prepare the shader inputs. - } } PandaNode::r_prepare_scene(gsg, node_state, transformer, current_thread); From ab6d3b2e15d9671a7f7748470a5ad39b22f71535 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 16 Nov 2021 15:04:46 +0100 Subject: [PATCH 28/68] gobj: Show better error if calling tex.write() with unknown extension --- panda/src/gobj/texture.cxx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/panda/src/gobj/texture.cxx b/panda/src/gobj/texture.cxx index d6c30296d9..940944daef 100644 --- a/panda/src/gobj/texture.cxx +++ b/panda/src/gobj/texture.cxx @@ -30,6 +30,7 @@ #include "pnmImage.h" #include "pnmReader.h" #include "pfmFile.h" +#include "pnmFileTypeRegistry.h" #include "virtualFileSystem.h" #include "datagramInputFile.h" #include "datagramOutputFile.h" @@ -5176,11 +5177,19 @@ do_write_one(CData *cdata, const Filename &fullpath, int z, int n) { success = pfm.write(fullpath); } else { // Writing a normal, integer texture. + PNMFileType *type = + PNMFileTypeRegistry::get_global_ptr()->get_type_from_extension(fullpath); + if (type == nullptr) { + gobj_cat.error() + << "Texture::write() - couldn't determine type from extension: " << fullpath << endl; + return false; + } + PNMImage pnmimage; if (!do_store_one(cdata, pnmimage, z, n)) { return false; } - success = pnmimage.write(fullpath); + success = pnmimage.write(fullpath, type); } if (!success) { From b6e521715403a5aa02eb3c070d9c39339cba533a Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 16 Nov 2021 15:09:44 +0100 Subject: [PATCH 29/68] ai: Add name property to AICharacter class Closes #1205 --- contrib/src/ai/aiCharacter.cxx | 4 ++++ contrib/src/ai/aiCharacter.h | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/contrib/src/ai/aiCharacter.cxx b/contrib/src/ai/aiCharacter.cxx index 58485081c3..8f46bd8ee1 100644 --- a/contrib/src/ai/aiCharacter.cxx +++ b/contrib/src/ai/aiCharacter.cxx @@ -117,6 +117,10 @@ NodePath AICharacter::get_char_render() { return _window_render; } +std::string AICharacter::get_name() { + return _name; +} + void AICharacter::set_pf_guide(bool pf_guide) { _pf_guide = pf_guide; } diff --git a/contrib/src/ai/aiCharacter.h b/contrib/src/ai/aiCharacter.h index 81b3a01210..5ea5e590ec 100644 --- a/contrib/src/ai/aiCharacter.h +++ b/contrib/src/ai/aiCharacter.h @@ -46,6 +46,8 @@ class EXPCL_PANDAAI AICharacter : public ReferenceCount { void set_char_render(NodePath render); NodePath get_char_render(); + std::string get_name(); + PUBLISHED: double get_mass(); void set_mass(double m); @@ -65,6 +67,8 @@ PUBLISHED: explicit AICharacter(std::string model_name, NodePath model_np, double mass, double movt_force, double max_force); ~AICharacter(); + + MAKE_PROPERTY(name, get_name); }; #endif From 0f7a575ac059d643e52298521dcbae0765e9384c Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 16 Nov 2021 15:15:27 +0100 Subject: [PATCH 30/68] bullet: Add bullet-split-impulse config variable Fixes #1201 Co-authored-by: K9Kraken --- panda/src/bullet/bulletWorld.cxx | 2 +- panda/src/bullet/config_bullet.cxx | 5 +++++ panda/src/bullet/config_bullet.h | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/panda/src/bullet/bulletWorld.cxx b/panda/src/bullet/bulletWorld.cxx index 5590f6aee6..b14a4c7e18 100644 --- a/panda/src/bullet/bulletWorld.cxx +++ b/panda/src/bullet/bulletWorld.cxx @@ -125,7 +125,7 @@ BulletWorld() { // Some prefered settings _world->getDispatchInfo().m_enableSPU = true; // default: true _world->getDispatchInfo().m_useContinuous = true; // default: true - _world->getSolverInfo().m_splitImpulse = false; // default: false + _world->getSolverInfo().m_splitImpulse = bullet_split_impulse; _world->getSolverInfo().m_numIterations = bullet_solver_iterations; } diff --git a/panda/src/bullet/config_bullet.cxx b/panda/src/bullet/config_bullet.cxx index e4627c931e..caee5fb2c6 100644 --- a/panda/src/bullet/config_bullet.cxx +++ b/panda/src/bullet/config_bullet.cxx @@ -100,6 +100,11 @@ PRC_DESC("Specifies if events should be send when new contacts are " "contact events might create more load on the event queue " "then you might want! Default value is FALSE.")); +ConfigVariableBool bullet_split_impulse +("bullet-split-impulse", false, +PRC_DESC("Penetrating recovery won't add momentum. " + "btContactSolverInfo::m_splitImpulse. Default value is false.")); + ConfigVariableInt bullet_solver_iterations ("bullet-solver-iterations", 10, PRC_DESC("Specifies the number of iterations for the Bullet contact " diff --git a/panda/src/bullet/config_bullet.h b/panda/src/bullet/config_bullet.h index 7b8a52e122..8ba8b6b7a8 100644 --- a/panda/src/bullet/config_bullet.h +++ b/panda/src/bullet/config_bullet.h @@ -33,6 +33,7 @@ extern ConfigVariableEnum bullet_broadphase_al extern ConfigVariableEnum bullet_filter_algorithm; extern ConfigVariableDouble bullet_sap_extents; extern ConfigVariableBool bullet_enable_contact_events; +extern ConfigVariableBool bullet_split_impulse; extern ConfigVariableInt bullet_solver_iterations; extern ConfigVariableBool bullet_additional_damping; extern ConfigVariableDouble bullet_additional_damping_linear_factor; From df7892b68372241422d13a5bba4579cb612750d3 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 16 Nov 2021 09:44:44 +0100 Subject: [PATCH 31/68] dtoolutil: Fix _WIN32 macros that should be _MSC_VER --- dtool/src/dtoolutil/filename.cxx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dtool/src/dtoolutil/filename.cxx b/dtool/src/dtoolutil/filename.cxx index 0998a8f7a5..b3b84a84d7 100644 --- a/dtool/src/dtoolutil/filename.cxx +++ b/dtool/src/dtoolutil/filename.cxx @@ -1876,13 +1876,13 @@ open_read(std::ifstream &stream) const { #endif stream.clear(); -#ifdef _WIN32 +#ifdef _MSC_VER wstring os_specific = to_os_specific_w(); stream.open(os_specific.c_str(), open_mode); #else string os_specific = to_os_specific(); stream.open(os_specific.c_str(), open_mode); -#endif // _WIN32 +#endif // _MSC_VER return (!stream.fail()); } @@ -1926,11 +1926,11 @@ open_write(std::ofstream &stream, bool truncate) const { #endif stream.clear(); -#ifdef _WIN32 +#ifdef _MSC_VER wstring os_specific = to_os_specific_w(); #else string os_specific = to_os_specific(); -#endif // _WIN32 +#endif // _MSC_VER stream.open(os_specific.c_str(), open_mode); return (!stream.fail()); @@ -1958,11 +1958,11 @@ open_append(std::ofstream &stream) const { #endif stream.clear(); -#ifdef _WIN32 +#ifdef _MSC_VER wstring os_specific = to_os_specific_w(); #else string os_specific = to_os_specific(); -#endif // _WIN32 +#endif // _MSC_VER stream.open(os_specific.c_str(), open_mode); return (!stream.fail()); @@ -2000,11 +2000,11 @@ open_read_write(std::fstream &stream, bool truncate) const { #endif stream.clear(); -#ifdef _WIN32 +#ifdef _MSC_VER wstring os_specific = to_os_specific_w(); #else string os_specific = to_os_specific(); -#endif // _WIN32 +#endif // _MSC_VER stream.open(os_specific.c_str(), open_mode); return (!stream.fail()); @@ -2032,11 +2032,11 @@ open_read_append(std::fstream &stream) const { #endif stream.clear(); -#ifdef _WIN32 +#ifdef _MSC_VER wstring os_specific = to_os_specific_w(); #else string os_specific = to_os_specific(); -#endif // _WIN32 +#endif // _MSC_VER stream.open(os_specific.c_str(), open_mode); return (!stream.fail()); From fac6916151cd148c4c8b69aa037962bebe1d7138 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 16 Nov 2021 12:36:15 +0100 Subject: [PATCH 32/68] pgraph: Major traversal optimizations In particular, these optimize the traversal of nodes that are not in view, which are discarded more efficiently. This change affects both the cull and collision traversers. --- contrib/src/speedtree/speedTreeNode.cxx | 12 +- contrib/src/speedtree/speedTreeNode.h | 1 - panda/src/bullet/bulletDebugNode.cxx | 12 +- panda/src/bullet/bulletDebugNode.h | 1 - panda/src/collide/collisionLevelState.I | 147 +++++++++-- panda/src/collide/collisionLevelState.h | 4 +- panda/src/collide/collisionLevelStateBase.I | 31 ++- panda/src/collide/collisionLevelStateBase.h | 5 + panda/src/collide/collisionNode.cxx | 25 +- panda/src/collide/collisionNode.h | 1 - panda/src/collide/collisionTraverser.cxx | 191 +++++++-------- panda/src/collide/collisionVisualizer.cxx | 19 +- panda/src/collide/collisionVisualizer.h | 1 - .../src/grutil/pipeOcclusionCullTraverser.cxx | 2 +- panda/src/grutil/rigidBodyCombiner.cxx | 3 +- panda/src/grutil/shaderTerrainMesh.cxx | 10 +- panda/src/grutil/shaderTerrainMesh.h | 1 - panda/src/parametrics/ropeNode.cxx | 12 +- panda/src/parametrics/ropeNode.h | 1 - panda/src/parametrics/sheetNode.cxx | 12 +- panda/src/parametrics/sheetNode.h | 1 - panda/src/pgraph/config_pgraph.h | 2 +- panda/src/pgraph/cullPlanes.cxx | 35 ++- panda/src/pgraph/cullPlanes.h | 11 +- panda/src/pgraph/cullTraverser.I | 153 ++++++++---- panda/src/pgraph/cullTraverser.cxx | 140 +++++++---- panda/src/pgraph/cullTraverser.h | 36 ++- panda/src/pgraph/cullTraverserData.I | 134 +++++++--- panda/src/pgraph/cullTraverserData.cxx | 228 ++++++++---------- panda/src/pgraph/cullTraverserData.h | 24 +- panda/src/pgraph/geomNode.cxx | 15 +- panda/src/pgraph/geomNode.h | 1 - panda/src/pgraph/instancedNode.cxx | 40 ++- panda/src/pgraph/occluderNode.cxx | 13 +- panda/src/pgraph/occluderNode.h | 1 - panda/src/pgraph/pandaNode.I | 65 +++++ panda/src/pgraph/pandaNode.cxx | 126 ++++++---- panda/src/pgraph/pandaNode.h | 32 ++- panda/src/pgraph/planeNode.cxx | 12 +- panda/src/pgraph/planeNode.h | 1 - panda/src/pgraph/portalNode.cxx | 13 +- panda/src/pgraph/portalNode.h | 1 - panda/src/pgraph/showBoundsEffect.h | 2 +- panda/src/pgraph/transformState.I | 14 ++ panda/src/pgraph/transformState.h | 1 + panda/src/pgraphnodes/callbackNode.cxx | 12 +- panda/src/pgraphnodes/callbackNode.h | 1 - panda/src/pgraphnodes/computeNode.cxx | 12 +- panda/src/pgraphnodes/computeNode.h | 1 - panda/src/pgraphnodes/fadeLodNode.cxx | 65 ++--- panda/src/pgraphnodes/lodNode.cxx | 41 ++-- .../src/pgraphnodes/nodeCullCallbackData.cxx | 11 +- panda/src/pgraphnodes/selectiveChildNode.I | 16 +- panda/src/pgraphnodes/selectiveChildNode.cxx | 41 ---- panda/src/pgraphnodes/selectiveChildNode.h | 17 +- panda/src/pgraphnodes/sequenceNode.cxx | 19 +- panda/src/pgraphnodes/sequenceNode.h | 1 - panda/src/pgraphnodes/switchNode.cxx | 19 +- panda/src/pgraphnodes/switchNode.h | 1 - panda/src/pgui/pgEntry.cxx | 3 +- panda/src/pgui/pgItem.cxx | 15 +- panda/src/pgui/pgItem.h | 1 - panda/src/pgui/pgTop.cxx | 17 +- panda/src/pgui/pgTop.h | 1 - .../src/pipeline/cycleDataLockedStageReader.I | 4 + panda/src/text/textNode.cxx | 15 +- panda/src/text/textNode.h | 1 - 67 files changed, 1015 insertions(+), 891 deletions(-) diff --git a/contrib/src/speedtree/speedTreeNode.cxx b/contrib/src/speedtree/speedTreeNode.cxx index 198160b584..ac0884e641 100644 --- a/contrib/src/speedtree/speedTreeNode.cxx +++ b/contrib/src/speedtree/speedTreeNode.cxx @@ -1002,17 +1002,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { return true; } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool SpeedTreeNode:: -is_renderable() const { - return true; -} - /** * Adds the node's contents to the CullResult we are building up during the * cull traversal, so that it will be drawn at render time. For most nodes @@ -1162,6 +1151,7 @@ set_transparent_texture_mode(SpeedTree::ETextureAlphaRenderMode eMode) const { void SpeedTreeNode:: init_node() { PandaNode::set_cull_callback(); + PandaNode::set_renderable(); _is_valid = false; _needs_repopulate = false; diff --git a/contrib/src/speedtree/speedTreeNode.h b/contrib/src/speedtree/speedTreeNode.h index 3c95bbce76..9c3f0ca886 100644 --- a/contrib/src/speedtree/speedTreeNode.h +++ b/contrib/src/speedtree/speedTreeNode.h @@ -157,7 +157,6 @@ public: GeomTransformer &transformer); virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual bool is_renderable() const; virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data); void prepare_scene(GraphicsStateGuardianBase *gsgbase, const RenderState *net_state); diff --git a/panda/src/bullet/bulletDebugNode.cxx b/panda/src/bullet/bulletDebugNode.cxx index 8b758c59ca..7bc26ae1c1 100644 --- a/panda/src/bullet/bulletDebugNode.cxx +++ b/panda/src/bullet/bulletDebugNode.cxx @@ -48,6 +48,7 @@ BulletDebugNode(const char *name) : PandaNode(name) { set_bounds(bounds); set_final(true); set_overall_hidden(true); + set_renderable(); } /** @@ -151,17 +152,6 @@ draw_mask_changed() { } } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool BulletDebugNode:: -is_renderable() const { - return true; -} - /** * Adds the node's contents to the CullResult we are building up during the * cull traversal, so that it will be drawn at render time. For most nodes diff --git a/panda/src/bullet/bulletDebugNode.h b/panda/src/bullet/bulletDebugNode.h index 720e26fbd3..471c059f99 100644 --- a/panda/src/bullet/bulletDebugNode.h +++ b/panda/src/bullet/bulletDebugNode.h @@ -55,7 +55,6 @@ public: virtual bool safe_to_combine_children() const; virtual bool safe_to_flatten_below() const; - virtual bool is_renderable() const; virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data); private: diff --git a/panda/src/collide/collisionLevelState.I b/panda/src/collide/collisionLevelState.I index db618f99e7..1001f6d08d 100644 --- a/panda/src/collide/collisionLevelState.I +++ b/panda/src/collide/collisionLevelState.I @@ -30,9 +30,9 @@ CollisionLevelState(const NodePath &node_path) : */ template INLINE CollisionLevelState:: -CollisionLevelState(const CollisionLevelState &parent, PandaNode *child) : +CollisionLevelState(const CollisionLevelState &parent, const PandaNode::DownConnection &child, MaskType mask) : CollisionLevelStateBase(parent, child), - _current(parent._current) + _current(mask) { } #endif // CPPPARSER @@ -101,21 +101,21 @@ prepare_collider(const ColliderDef &def, const NodePath &root) { template bool CollisionLevelState:: any_in_bounds() { -#ifndef NDEBUG +#ifdef NDEBUG + const bool is_spam = false; +#else + const bool is_spam = collide_cat.is_spam(); +#endif int indent_level = 0; - if (collide_cat.is_spam()) { + if (is_spam) { indent_level = _node_path.get_num_nodes() * 2; collide_cat.spam(); indent(collide_cat.spam(false), indent_level) << "Considering " << _node_path.get_node_path() << "\n"; } -#endif // NDEBUG - PandaNode *pnode = node(); - - CPT(BoundingVolume) node_bv = pnode->get_bounds(); - if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) { - const GeometricBoundingVolume *node_gbv = (const GeometricBoundingVolume *)node_bv.p(); + if (_node_gbv != nullptr) { + PandaNode *pnode = node(); CollideMask this_mask = pnode->get_net_collide_mask(); int num_colliders = get_num_colliders(); @@ -131,13 +131,11 @@ any_in_bounds() { // Also don't test a node with itself, or with any of its // descendants. if (pnode == cnode) { -#ifndef NDEBUG - if (collide_cat.is_spam()) { + if (is_spam) { indent(collide_cat.spam(false), indent_level) << "Not comparing " << c << " to " << _node_path << " (same node)\n"; } -#endif // NDEBUG } else { // There are bits in common, and it's not the same instance, so go @@ -148,16 +146,14 @@ any_in_bounds() { is_in = true; // If there's no bounding volume, we're implicitly in. if (col_gbv != nullptr) { - is_in = (node_gbv->contains(col_gbv) != 0); + is_in = (_node_gbv->contains(col_gbv) != 0); _node_volume_pcollector.add_level(1); -#ifndef NDEBUG - if (collide_cat.is_spam()) { + if (is_spam) { indent(collide_cat.spam(false), indent_level) << "Comparing " << c << ": " << *col_gbv - << " to " << *node_gbv << ", is_in = " << is_in << "\n"; + << " to " << *_node_gbv << ", is_in = " << is_in << "\n"; } -#endif // NDEBUG } } } @@ -171,8 +167,7 @@ any_in_bounds() { } } -#ifndef NDEBUG - if (collide_cat.is_spam()) { + if (is_spam) { int num_active_colliders = 0; int num_colliders = get_num_colliders(); for (int c = 0; c < num_colliders; c++) { @@ -201,11 +196,121 @@ any_in_bounds() { collide_cat.spam(false) << "\n"; } -#endif // NDEBUG return has_any_collider(); } #endif // CPPPARSER +#ifndef CPPPARSER +/** + * Checks the bounding volume of the given child of the current node against + * each of our colliders. Returns a mask indicating which colliders are inside + * of the bounding volume. + */ +template +MaskType CollisionLevelState:: +get_child_mask(const PandaNode::DownConnection &child) const { + PandaNode *pnode = child.get_child(); +#ifdef NDEBUG + const bool is_spam = false; +#else + const bool is_spam = collide_cat.is_spam(); +#endif + int indent_level = 0; + if (is_spam) { + indent_level = (_node_path.get_num_nodes() + 1) * 2; + collide_cat.spam(); + indent(collide_cat.spam(false), indent_level) + << "Considering " << _node_path << "/" << pnode->get_name() << "\n"; + } + + MaskType mask = _current; + + const GeometricBoundingVolume *node_gbv = child.get_bounds(); + if (node_gbv != nullptr) { + CollideMask node_mask = child.get_net_collide_mask(); + + int num_colliders = get_num_colliders(); + for (int c = 0; c < num_colliders; c++) { + if (mask.get_bit(c)) { + CollisionNode *cnode = get_collider_node(c); + bool is_in = false; + + // Don't even bother testing the bounding volume if there are no + // collide bits in common between our collider and this node. + CollideMask from_mask = cnode->get_from_collide_mask() & _include_mask; + if (!(from_mask & node_mask).is_zero()) { + // Also don't test a node with itself, or with any of its + // descendants. + if (pnode == cnode) { + if (is_spam) { + indent(collide_cat.spam(false), indent_level) + << "Not comparing " << c << " to " << _node_path << "/" + << pnode->get_name() << " (same node)\n"; + } + + } else { + // There are bits in common, and it's not the same instance, so go + // ahead and try the bounding volume. + const GeometricBoundingVolume *col_gbv = + get_local_bound(c); + + is_in = true; // If there's no bounding volume, we're implicitly in. + + if (col_gbv != nullptr) { + is_in = (node_gbv->contains(col_gbv) != 0); + _node_volume_pcollector.add_level(1); + + if (is_spam) { + indent(collide_cat.spam(false), indent_level) + << "Comparing " << c << ": " << *col_gbv + << " to " << *node_gbv << ", is_in = " << is_in << "\n"; + } + } + } + } + + if (!is_in) { + // This collider cannot intersect with any geometry at this node or + // below. + mask.clear_bit(c); + } + } + } + } + + if (is_spam) { + int num_active_colliders = 0; + int num_colliders = get_num_colliders(); + for (int c = 0; c < num_colliders; c++) { + if (mask.get_bit(c)) { + num_active_colliders++; + } + } + + collide_cat.spam(); + indent(collide_cat.spam(false), indent_level) + << _node_path.get_node_path() << "/" << pnode->get_name() << " has " + << num_active_colliders << " interested colliders"; + if (num_colliders != 0) { + collide_cat.spam(false) + << " ("; + for (int c = 0; c < num_colliders; c++) { + if (mask.get_bit(c)) { + CollisionNode *cnode = get_collider_node(c); + collide_cat.spam(false) + << " " << c << ". " << cnode->get_name(); + } + } + collide_cat.spam(false) + << " )"; + } + collide_cat.spam(false) + << "\n"; + } + return mask; +} +#endif // CPPPARSER + #ifndef CPPPARSER /** * Applies the inverse transform from the current node, if any, onto all the diff --git a/panda/src/collide/collisionLevelState.h b/panda/src/collide/collisionLevelState.h index f06a025f1e..851b908a8c 100644 --- a/panda/src/collide/collisionLevelState.h +++ b/panda/src/collide/collisionLevelState.h @@ -37,7 +37,8 @@ public: #ifndef CPPPARSER INLINE CollisionLevelState(const NodePath &node_path); INLINE CollisionLevelState(const CollisionLevelState &parent, - PandaNode *child); + const PandaNode::DownConnection &child, + MaskType mask); INLINE CollisionLevelState(const CollisionLevelState ©); INLINE void operator = (const CollisionLevelState ©); @@ -45,6 +46,7 @@ public: INLINE void prepare_collider(const ColliderDef &def, const NodePath &root); bool any_in_bounds(); + MaskType get_child_mask(const PandaNode::DownConnection &child) const; bool apply_transform(); INLINE static bool has_max_colliders(); diff --git a/panda/src/collide/collisionLevelStateBase.I b/panda/src/collide/collisionLevelStateBase.I index 20923a9c22..5b649838f7 100644 --- a/panda/src/collide/collisionLevelStateBase.I +++ b/panda/src/collide/collisionLevelStateBase.I @@ -18,7 +18,8 @@ INLINE CollisionLevelStateBase:: CollisionLevelStateBase(const NodePath &node_path) : _node_path(node_path), _colliders(get_class_type()), - _include_mask(CollideMask::all_on()) + _include_mask(CollideMask::all_on()), + _node_gbv(node_path.node()->get_bounds()->as_geometric_bounding_volume()) { } @@ -30,7 +31,21 @@ CollisionLevelStateBase(const CollisionLevelStateBase &parent, PandaNode *child) _node_path(parent._node_path, child), _colliders(parent._colliders), _include_mask(parent._include_mask), - _local_bounds(parent._local_bounds) + _local_bounds(parent._local_bounds), + _node_gbv(child->get_bounds()->as_geometric_bounding_volume()) +{ +} + +/** + * This constructor goes to the next child node in the traversal. + */ +INLINE CollisionLevelStateBase:: +CollisionLevelStateBase(const CollisionLevelStateBase &parent, const PandaNode::DownConnection &child) : + _node_path(parent._node_path, child.get_child()), + _colliders(parent._colliders), + _include_mask(parent._include_mask), + _local_bounds(parent._local_bounds), + _node_gbv(child.get_bounds()) { } @@ -43,7 +58,8 @@ CollisionLevelStateBase(const CollisionLevelStateBase ©) : _colliders(copy._colliders), _include_mask(copy._include_mask), _local_bounds(copy._local_bounds), - _parent_bounds(copy._parent_bounds) + _parent_bounds(copy._parent_bounds), + _node_gbv(copy._node_gbv) { } @@ -57,6 +73,7 @@ operator = (const CollisionLevelStateBase ©) { _include_mask = copy._include_mask; _local_bounds = copy._local_bounds; _parent_bounds = copy._parent_bounds; + _node_gbv = copy._node_gbv; } /** @@ -113,6 +130,14 @@ get_collider_node_path(int n) const { return _colliders[n]._node_path; } +/** + * Returns the bounding volume of the current node. + */ +INLINE const GeometricBoundingVolume *CollisionLevelStateBase:: +get_node_bound() const { + return _node_gbv; +} + /** * Returns the bounding volume of the indicated collider, transformed into the * current node's transform space. diff --git a/panda/src/collide/collisionLevelStateBase.h b/panda/src/collide/collisionLevelStateBase.h index 4c2abccb58..f84d492965 100644 --- a/panda/src/collide/collisionLevelStateBase.h +++ b/panda/src/collide/collisionLevelStateBase.h @@ -52,6 +52,8 @@ public: INLINE CollisionLevelStateBase(const NodePath &node_path); INLINE CollisionLevelStateBase(const CollisionLevelStateBase &parent, PandaNode *child); + INLINE CollisionLevelStateBase(const CollisionLevelStateBase &parent, + const PandaNode::DownConnection &child); INLINE CollisionLevelStateBase(const CollisionLevelStateBase ©); INLINE void operator = (const CollisionLevelStateBase ©); @@ -67,6 +69,7 @@ public: INLINE const CollisionSolid *get_collider(int n) const; INLINE CollisionNode *get_collider_node(int n) const; INLINE NodePath get_collider_node_path(int n) const; + INLINE const GeometricBoundingVolume *get_node_bound() const; INLINE const GeometricBoundingVolume *get_local_bound(int n) const; INLINE const GeometricBoundingVolume *get_parent_bound(int n) const; @@ -80,6 +83,8 @@ protected: Colliders _colliders; CollideMask _include_mask; + const GeometricBoundingVolume *_node_gbv = nullptr; + typedef PTA(CPT(GeometricBoundingVolume)) BoundingVolumes; BoundingVolumes _local_bounds; BoundingVolumes _parent_bounds; diff --git a/panda/src/collide/collisionNode.cxx b/panda/src/collide/collisionNode.cxx index 1196ba20c7..2df893ef18 100644 --- a/panda/src/collide/collisionNode.cxx +++ b/panda/src/collide/collisionNode.cxx @@ -43,6 +43,7 @@ CollisionNode(const std::string &name) : _collider_sort(0) { set_cull_callback(); + set_renderable(); // CollisionNodes are hidden by default. set_overall_hidden(true); @@ -186,11 +187,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { CPT(CollisionSolid) solid = (*si).get_read_pointer(); PT(PandaNode) node = solid->get_viz(trav, data, false); if (node != nullptr) { - CullTraverserData next_data(data, node); - // We don't want to inherit the render state from above for these guys. - next_data._state = RenderState::make_empty(); - trav->traverse(next_data); + trav->traverse_down(data, node, data._net_transform, RenderState::make_empty()); } } @@ -208,12 +206,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { CPT(CollisionSolid) solid = (*si).get_read_pointer(); PT(PandaNode) node = solid->get_viz(trav, data, false); if (node != nullptr) { - CullTraverserData next_data(data, node); - - next_data._net_transform = - next_data._net_transform->compose(transform); - next_data._state = get_last_pos_state(); - trav->traverse(next_data); + trav->traverse_down(data, node, + data._net_transform->compose(transform), get_last_pos_state()); } } } @@ -223,17 +217,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { return true; } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool CollisionNode:: -is_renderable() const { - return true; -} - /** * A simple downcast check. Returns true if this kind of node happens to * inherit from CollisionNode, false otherwise. diff --git a/panda/src/collide/collisionNode.h b/panda/src/collide/collisionNode.h index 8dd46f6a28..fee71fce78 100644 --- a/panda/src/collide/collisionNode.h +++ b/panda/src/collide/collisionNode.h @@ -43,7 +43,6 @@ public: virtual CollideMask get_legal_collide_mask() const; virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual bool is_renderable() const; virtual bool is_collision_node() const; virtual void output(std::ostream &out) const; diff --git a/panda/src/collide/collisionTraverser.cxx b/panda/src/collide/collisionTraverser.cxx index 2feb236241..23459d88f3 100644 --- a/panda/src/collide/collisionTraverser.cxx +++ b/panda/src/collide/collisionTraverser.cxx @@ -288,7 +288,9 @@ traverse(const NodePath &root) { #ifdef DO_PSTATS PStatTimer pass_timer(get_pass_collector(pass)); #endif - r_traverse_single(level_states[pass], pass); + if (level_states[pass].any_in_bounds()) { + r_traverse_single(level_states[pass], pass); + } } } } @@ -562,22 +564,13 @@ prepare_colliders_single(CollisionTraverser::LevelStatesSingle &level_states, */ void CollisionTraverser:: r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) { - if (!level_state.any_in_bounds()) { - return; - } if (!level_state.apply_transform()) { return; } PandaNode *node = level_state.node(); if (node->is_collision_node()) { - CollisionNode *cnode; - DCAST_INTO_V(cnode, node); - CPT(BoundingVolume) node_bv = cnode->get_bounds(); - const GeometricBoundingVolume *node_gbv = nullptr; - if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) { - DCAST_INTO_V(node_gbv, node_bv); - } + CollisionNode *cnode = (CollisionNode *)node; CollisionEntry entry; entry._into_node = cnode; @@ -589,13 +582,14 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) { int num_colliders = level_state.get_num_colliders(); for (int c = 0; c < num_colliders; ++c) { if (level_state.has_collider(c)) { - entry._from_node = level_state.get_collider_node(c); + CollisionNode *from_node = level_state.get_collider_node(c); - if ((entry._from_node->get_from_collide_mask() & + if ((from_node->get_from_collide_mask() & cnode->get_into_collide_mask()) != 0) { #ifdef DO_PSTATS // PStatTimer collide_timer(_solid_collide_collectors[pass]); #endif + entry._from_node = from_node; entry._from_node_path = level_state.get_collider_node_path(c); entry._from = level_state.get_collider(c); @@ -603,7 +597,7 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) { entry, level_state.get_parent_bound(c), level_state.get_local_bound(c), - node_gbv); + level_state.get_node_bound()); } } } @@ -616,13 +610,7 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) { } #endif - GeomNode *gnode; - DCAST_INTO_V(gnode, node); - CPT(BoundingVolume) node_bv = gnode->get_bounds(); - const GeometricBoundingVolume *node_gbv = nullptr; - if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) { - DCAST_INTO_V(node_gbv, node_bv); - } + GeomNode *gnode = (GeomNode *)node; CollisionEntry entry; entry._into_node = gnode; @@ -648,7 +636,7 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) { entry, level_state.get_parent_bound(c), level_state.get_local_bound(c), - node_gbv); + level_state.get_node_bound()); } } } @@ -658,9 +646,14 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) { // If it's a switch node or sequence node, visit just the one visible // child. int index = node->get_visible_child(); - if (index >= 0 && index < node->get_num_children()) { - CollisionLevelStateSingle next_state(level_state, node->get_child(index)); - r_traverse_single(next_state, pass); + PandaNode::Children children = node->get_children(); + if (index >= 0 && index < children.get_num_children()) { + const PandaNode::DownConnection &child = children.get_child_connection(index); + CollisionLevelStateSingle::CurrentMask mask = level_state.get_child_mask(child); + if (!mask.is_zero()) { + CollisionLevelStateSingle next_state(level_state, child, mask); + r_traverse_single(next_state, pass); + } } } else if (node->is_lod_node()) { @@ -673,12 +666,16 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) { PandaNode::Children children = node->get_children(); int num_children = children.get_num_children(); for (int i = 0; i < num_children; ++i) { - CollisionLevelStateSingle next_state(level_state, children.get_child(i)); - if (i != index) { - next_state.set_include_mask(next_state.get_include_mask() & - ~GeomNode::get_default_collide_mask()); + const PandaNode::DownConnection &child = children.get_child_connection(i); + CollisionLevelStateSingle::CurrentMask mask = level_state.get_child_mask(child); + if (!mask.is_zero()) { + CollisionLevelStateSingle next_state(level_state, child, mask); + if (i != index) { + next_state.set_include_mask(next_state.get_include_mask() & + ~GeomNode::get_default_collide_mask()); + } + r_traverse_single(next_state, pass); } - r_traverse_single(next_state, pass); } } else { @@ -686,8 +683,12 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) { PandaNode::Children children = node->get_children(); int num_children = children.get_num_children(); for (int i = 0; i < num_children; ++i) { - CollisionLevelStateSingle next_state(level_state, children.get_child(i)); - r_traverse_single(next_state, pass); + const PandaNode::DownConnection &child = children.get_child_connection(i); + CollisionLevelStateSingle::CurrentMask mask = level_state.get_child_mask(child); + if (!mask.is_zero()) { + CollisionLevelStateSingle next_state(level_state, child, mask); + r_traverse_single(next_state, pass); + } } } } @@ -773,22 +774,13 @@ prepare_colliders_double(CollisionTraverser::LevelStatesDouble &level_states, */ void CollisionTraverser:: r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) { - if (!level_state.any_in_bounds()) { - return; - } if (!level_state.apply_transform()) { return; } PandaNode *node = level_state.node(); if (node->is_collision_node()) { - CollisionNode *cnode; - DCAST_INTO_V(cnode, node); - CPT(BoundingVolume) node_bv = cnode->get_bounds(); - const GeometricBoundingVolume *node_gbv = nullptr; - if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) { - DCAST_INTO_V(node_gbv, node_bv); - } + CollisionNode *cnode = (CollisionNode *)node; CollisionEntry entry; entry._into_node = cnode; @@ -814,7 +806,7 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) { entry, level_state.get_parent_bound(c), level_state.get_local_bound(c), - node_gbv); + level_state.get_node_bound()); } } } @@ -827,13 +819,7 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) { } #endif - GeomNode *gnode; - DCAST_INTO_V(gnode, node); - CPT(BoundingVolume) node_bv = gnode->get_bounds(); - const GeometricBoundingVolume *node_gbv = nullptr; - if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) { - DCAST_INTO_V(node_gbv, node_bv); - } + GeomNode *gnode = (GeomNode *)node; CollisionEntry entry; entry._into_node = gnode; @@ -859,7 +845,7 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) { entry, level_state.get_parent_bound(c), level_state.get_local_bound(c), - node_gbv); + level_state.get_node_bound()); } } } @@ -869,9 +855,14 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) { // If it's a switch node or sequence node, visit just the one visible // child. int index = node->get_visible_child(); - if (index >= 0 && index < node->get_num_children()) { - CollisionLevelStateDouble next_state(level_state, node->get_child(index)); - r_traverse_double(next_state, pass); + PandaNode::Children children = node->get_children(); + if (index >= 0 && index < children.get_num_children()) { + const PandaNode::DownConnection &child = children.get_child_connection(index); + CollisionLevelStateDouble::CurrentMask mask = level_state.get_child_mask(child); + if (!mask.is_zero()) { + CollisionLevelStateDouble next_state(level_state, child, mask); + r_traverse_double(next_state, pass); + } } } else if (node->is_lod_node()) { @@ -880,16 +871,20 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) { // visit all other levels without GeomNode::get_default_collide_mask(), // allowing only collision with CollisionNodes and special geometry under // higher levels of detail. - int index = DCAST(LODNode, node)->get_lowest_switch(); + int index = ((LODNode *)node)->get_lowest_switch(); PandaNode::Children children = node->get_children(); int num_children = children.get_num_children(); for (int i = 0; i < num_children; ++i) { - CollisionLevelStateDouble next_state(level_state, children.get_child(i)); - if (i != index) { - next_state.set_include_mask(next_state.get_include_mask() & - ~GeomNode::get_default_collide_mask()); + const PandaNode::DownConnection &child = children.get_child_connection(i); + CollisionLevelStateDouble::CurrentMask mask = level_state.get_child_mask(child); + if (!mask.is_zero()) { + CollisionLevelStateDouble next_state(level_state, child, mask); + if (i != index) { + next_state.set_include_mask(next_state.get_include_mask() & + ~GeomNode::get_default_collide_mask()); + } + r_traverse_double(next_state, pass); } - r_traverse_double(next_state, pass); } } else { @@ -897,8 +892,12 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) { PandaNode::Children children = node->get_children(); int num_children = children.get_num_children(); for (int i = 0; i < num_children; ++i) { - CollisionLevelStateDouble next_state(level_state, children.get_child(i)); - r_traverse_double(next_state, pass); + const PandaNode::DownConnection &child = children.get_child_connection(i); + CollisionLevelStateDouble::CurrentMask mask = level_state.get_child_mask(child); + if (!mask.is_zero()) { + CollisionLevelStateDouble next_state(level_state, child, mask); + r_traverse_double(next_state, pass); + } } } } @@ -984,22 +983,13 @@ prepare_colliders_quad(CollisionTraverser::LevelStatesQuad &level_states, */ void CollisionTraverser:: r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) { - if (!level_state.any_in_bounds()) { - return; - } if (!level_state.apply_transform()) { return; } PandaNode *node = level_state.node(); if (node->is_collision_node()) { - CollisionNode *cnode; - DCAST_INTO_V(cnode, node); - CPT(BoundingVolume) node_bv = cnode->get_bounds(); - const GeometricBoundingVolume *node_gbv = nullptr; - if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) { - DCAST_INTO_V(node_gbv, node_bv); - } + CollisionNode *cnode = (CollisionNode *)node; CollisionEntry entry; entry._into_node = cnode; @@ -1025,7 +1015,7 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) { entry, level_state.get_parent_bound(c), level_state.get_local_bound(c), - node_gbv); + level_state.get_node_bound()); } } } @@ -1038,13 +1028,7 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) { } #endif - GeomNode *gnode; - DCAST_INTO_V(gnode, node); - CPT(BoundingVolume) node_bv = gnode->get_bounds(); - const GeometricBoundingVolume *node_gbv = nullptr; - if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) { - DCAST_INTO_V(node_gbv, node_bv); - } + GeomNode *gnode = (GeomNode *)node; CollisionEntry entry; entry._into_node = gnode; @@ -1070,7 +1054,7 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) { entry, level_state.get_parent_bound(c), level_state.get_local_bound(c), - node_gbv); + level_state.get_node_bound()); } } } @@ -1080,9 +1064,14 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) { // If it's a switch node or sequence node, visit just the one visible // child. int index = node->get_visible_child(); - if (index >= 0 && index < node->get_num_children()) { - CollisionLevelStateQuad next_state(level_state, node->get_child(index)); - r_traverse_quad(next_state, pass); + PandaNode::Children children = node->get_children(); + if (index >= 0 && index < children.get_num_children()) { + const PandaNode::DownConnection &child = children.get_child_connection(index); + CollisionLevelStateQuad::CurrentMask mask = level_state.get_child_mask(child); + if (!mask.is_zero()) { + CollisionLevelStateQuad next_state(level_state, child, mask); + r_traverse_quad(next_state, pass); + } } } else if (node->is_lod_node()) { @@ -1091,16 +1080,20 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) { // visit all other levels without GeomNode::get_default_collide_mask(), // allowing only collision with CollisionNodes and special geometry under // higher levels of detail. - int index = DCAST(LODNode, node)->get_lowest_switch(); + int index = ((LODNode *)node)->get_lowest_switch(); PandaNode::Children children = node->get_children(); int num_children = children.get_num_children(); for (int i = 0; i < num_children; ++i) { - CollisionLevelStateQuad next_state(level_state, children.get_child(i)); - if (i != index) { - next_state.set_include_mask(next_state.get_include_mask() & - ~GeomNode::get_default_collide_mask()); + const PandaNode::DownConnection &child = children.get_child_connection(i); + CollisionLevelStateQuad::CurrentMask mask = level_state.get_child_mask(child); + if (!mask.is_zero()) { + CollisionLevelStateQuad next_state(level_state, child, mask); + if (i != index) { + next_state.set_include_mask(next_state.get_include_mask() & + ~GeomNode::get_default_collide_mask()); + } + r_traverse_quad(next_state, pass); } - r_traverse_quad(next_state, pass); } } else { @@ -1108,8 +1101,12 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) { PandaNode::Children children = node->get_children(); int num_children = children.get_num_children(); for (int i = 0; i < num_children; ++i) { - CollisionLevelStateQuad next_state(level_state, children.get_child(i)); - r_traverse_quad(next_state, pass); + const PandaNode::DownConnection &child = children.get_child_connection(i); + CollisionLevelStateQuad::CurrentMask mask = level_state.get_child_mask(child); + if (!mask.is_zero()) { + CollisionLevelStateQuad next_state(level_state, child, mask); + r_traverse_quad(next_state, pass); + } } } } @@ -1162,10 +1159,7 @@ compare_collider_to_node(CollisionEntry &entry, // CollisionNodes. We are already filtering out tests for a // CollisionNode into itself. CPT(BoundingVolume) solid_bv = entry._into->get_bounds(); - const GeometricBoundingVolume *solid_gbv = nullptr; - if (solid_bv->is_of_type(GeometricBoundingVolume::get_class_type())) { - solid_gbv = (const GeometricBoundingVolume *)solid_bv.p(); - } + const GeometricBoundingVolume *solid_gbv = solid_bv->as_geometric_bounding_volume(); compare_collider_to_solid(entry, from_node_gbv, solid_gbv); } @@ -1198,14 +1192,13 @@ compare_collider_to_geom_node(CollisionEntry &entry, if (geom != nullptr) { CPT(BoundingVolume) geom_bv = geom->get_bounds(); const GeometricBoundingVolume *geom_gbv = nullptr; - if (num_geoms > 1 && - geom_bv->is_of_type(GeometricBoundingVolume::get_class_type())) { + if (num_geoms > 1) { // Only bother to test against each geom's bounding volume if we // have more than one geom in the node, as a slight optimization. // (If the node contains just one geom, then the node's bounding // volume, which we just tested, is the same as the geom's bounding // volume.) - DCAST_INTO_V(geom_gbv, geom_bv); + geom_gbv = geom_bv->as_geometric_bounding_volume(); } compare_collider_to_geom(entry, geom, from_node_gbv, geom_gbv); diff --git a/panda/src/collide/collisionVisualizer.cxx b/panda/src/collide/collisionVisualizer.cxx index 44fefce973..d8e70d11df 100644 --- a/panda/src/collide/collisionVisualizer.cxx +++ b/panda/src/collide/collisionVisualizer.cxx @@ -43,6 +43,7 @@ TypeHandle CollisionVisualizer::_type_handle; CollisionVisualizer:: CollisionVisualizer(const std::string &name) : PandaNode(name), _lock("CollisionVisualizer") { set_cull_callback(); + set_renderable(); // We always want to render the CollisionVisualizer node itself (even if it // doesn't appear to have any geometry within it). @@ -62,6 +63,7 @@ CollisionVisualizer(const CollisionVisualizer ©) : _normal_scale(copy._normal_scale) { set_cull_callback(); + set_renderable(); // We always want to render the CollisionVisualizer node itself (even if it // doesn't appear to have any geometry within it). @@ -145,12 +147,9 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { bool was_detected = (solid_info._detected_count > 0); PT(PandaNode) node = solid->get_viz(trav, xform_data, !was_detected); if (node != nullptr) { - CullTraverserData next_data(xform_data, node); - // We don't want to inherit the render state from above for these // guys. - next_data._state = get_viz_state(); - trav->traverse(next_data); + trav->traverse_down(xform_data, node, xform_data._net_transform, get_viz_state()); } } @@ -251,18 +250,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { return true; } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool CollisionVisualizer:: -is_renderable() const { - return true; -} - - /** * Writes a brief description of the node to the indicated output stream. * This is invoked by the << operator. It may be overridden in derived diff --git a/panda/src/collide/collisionVisualizer.h b/panda/src/collide/collisionVisualizer.h index cc245e101b..d289b74c0c 100644 --- a/panda/src/collide/collisionVisualizer.h +++ b/panda/src/collide/collisionVisualizer.h @@ -50,7 +50,6 @@ public: // from parent class PandaNode. virtual PandaNode *make_copy() const; virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual bool is_renderable() const; virtual void output(std::ostream &out) const; // from parent class CollisionRecorder. diff --git a/panda/src/grutil/pipeOcclusionCullTraverser.cxx b/panda/src/grutil/pipeOcclusionCullTraverser.cxx index 2a7f27a878..62828319b3 100644 --- a/panda/src/grutil/pipeOcclusionCullTraverser.cxx +++ b/panda/src/grutil/pipeOcclusionCullTraverser.cxx @@ -290,7 +290,7 @@ bool PipeOcclusionCullTraverser:: is_in_view(CullTraverserData &data) { _next_query = nullptr; - if (!CullTraverser::is_in_view(data)) { + if (!data.is_in_view(get_camera_mask())) { return false; } if (!_live) { diff --git a/panda/src/grutil/rigidBodyCombiner.cxx b/panda/src/grutil/rigidBodyCombiner.cxx index fb2c3959ab..76ec13a538 100644 --- a/panda/src/grutil/rigidBodyCombiner.cxx +++ b/panda/src/grutil/rigidBodyCombiner.cxx @@ -145,8 +145,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { } // Render the internal scene only--this is the optimized scene. - CullTraverserData next_data(data, _internal_root); - trav->traverse(next_data); + trav->traverse_down(data, _internal_root); // Do not directly render the nodes beneath this node. return false; diff --git a/panda/src/grutil/shaderTerrainMesh.cxx b/panda/src/grutil/shaderTerrainMesh.cxx index 4683ee6ba7..4a2281a941 100644 --- a/panda/src/grutil/shaderTerrainMesh.cxx +++ b/panda/src/grutil/shaderTerrainMesh.cxx @@ -107,6 +107,7 @@ ShaderTerrainMesh::ShaderTerrainMesh() : _update_enabled(true), _heightfield_tex(nullptr) { + set_renderable(); } /** @@ -440,14 +441,7 @@ void ShaderTerrainMesh::do_create_chunk_geom() { } /** - * @copydoc PandaNode::is_renderable() - */ -bool ShaderTerrainMesh::is_renderable() const { - return true; -} - -/** - * @copydoc PandaNode::is_renderable() + * @copydoc PandaNode::safe_to_flatten() */ bool ShaderTerrainMesh::safe_to_flatten() const { return false; diff --git a/panda/src/grutil/shaderTerrainMesh.h b/panda/src/grutil/shaderTerrainMesh.h index 9b08e12c0d..a0b8541e85 100644 --- a/panda/src/grutil/shaderTerrainMesh.h +++ b/panda/src/grutil/shaderTerrainMesh.h @@ -85,7 +85,6 @@ PUBLISHED: public: // Methods derived from PandaNode - virtual bool is_renderable() const; virtual bool safe_to_flatten() const; virtual bool safe_to_combine() const; virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data); diff --git a/panda/src/parametrics/ropeNode.cxx b/panda/src/parametrics/ropeNode.cxx index 4daf80e8ea..5d49c2dda3 100644 --- a/panda/src/parametrics/ropeNode.cxx +++ b/panda/src/parametrics/ropeNode.cxx @@ -71,6 +71,7 @@ RopeNode(const std::string &name) : PandaNode(name) { set_cull_callback(); + set_renderable(); } /** @@ -162,17 +163,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { return true; } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool RopeNode:: -is_renderable() const { - return true; -} - /** * */ diff --git a/panda/src/parametrics/ropeNode.h b/panda/src/parametrics/ropeNode.h index e4647e304d..4cb8542dc0 100644 --- a/panda/src/parametrics/ropeNode.h +++ b/panda/src/parametrics/ropeNode.h @@ -45,7 +45,6 @@ public: virtual bool safe_to_transform() const; virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual bool is_renderable() const; PUBLISHED: enum RenderMode { diff --git a/panda/src/parametrics/sheetNode.cxx b/panda/src/parametrics/sheetNode.cxx index 734082f577..620a35512f 100644 --- a/panda/src/parametrics/sheetNode.cxx +++ b/panda/src/parametrics/sheetNode.cxx @@ -70,6 +70,7 @@ SheetNode(const std::string &name) : PandaNode(name) { set_cull_callback(); + set_renderable(); } /** @@ -140,17 +141,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { return true; } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool SheetNode:: -is_renderable() const { - return true; -} - /** * */ diff --git a/panda/src/parametrics/sheetNode.h b/panda/src/parametrics/sheetNode.h index 97a8d80a71..1d27d840a8 100644 --- a/panda/src/parametrics/sheetNode.h +++ b/panda/src/parametrics/sheetNode.h @@ -43,7 +43,6 @@ public: virtual bool safe_to_transform() const; virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual bool is_renderable() const; PUBLISHED: INLINE void set_surface(NurbsSurfaceEvaluator *surface); diff --git a/panda/src/pgraph/config_pgraph.h b/panda/src/pgraph/config_pgraph.h index f1bec0168e..59551133aa 100644 --- a/panda/src/pgraph/config_pgraph.h +++ b/panda/src/pgraph/config_pgraph.h @@ -30,7 +30,7 @@ NotifyCategoryDecl(pgraph, EXPCL_PANDA_PGRAPH, EXPTP_PANDA_PGRAPH); NotifyCategoryDecl(loader, EXPCL_PANDA_PGRAPH, EXPTP_PANDA_PGRAPH); NotifyCategoryDecl(portal, EXPCL_PANDA_PGRAPH, EXPTP_PANDA_PGRAPH); -extern ConfigVariableBool fake_view_frustum_cull; +extern EXPCL_PANDA_PGRAPH ConfigVariableBool fake_view_frustum_cull; extern ConfigVariableBool clip_plane_cull; extern ConfigVariableBool allow_portal_cull; extern ConfigVariableBool debug_portal_cull; diff --git a/panda/src/pgraph/cullPlanes.cxx b/panda/src/pgraph/cullPlanes.cxx index fafbae6cfa..7cfd67f30b 100644 --- a/panda/src/pgraph/cullPlanes.cxx +++ b/panda/src/pgraph/cullPlanes.cxx @@ -79,26 +79,33 @@ xform(const LMatrix4 &mat) const { * will be added to the state, unless those ClipPlanes are also listed in * off_attrib. */ -CPT(CullPlanes) CullPlanes:: -apply_state(const CullTraverser *trav, const CullTraverserData *data, +void CullPlanes:: +apply_state(CPT(CullPlanes) &planes, + const CullTraverser *trav, const CullTraverserData *data, const ClipPlaneAttrib *net_attrib, const ClipPlaneAttrib *off_attrib, - const OccluderEffect *node_effect) const { + const OccluderEffect *node_effect) { if (net_attrib == nullptr && node_effect == nullptr) { - return this; + return; } PT(CullPlanes) new_planes; - if (get_ref_count() == 1) { - new_planes = (CullPlanes *)this; - } else { - new_planes = new CullPlanes(*this); + if (planes != nullptr) { + if (planes->get_ref_count() == 1) { + new_planes = (CullPlanes *)planes.p(); + } else { + new_planes = new CullPlanes(*planes); + } } CPT(TransformState) net_transform = nullptr; if (net_attrib != nullptr) { int num_on_planes = net_attrib->get_num_on_planes(); + if (num_on_planes > 0 && new_planes.is_null()) { + new_planes = new CullPlanes; + } + for (int i = 0; i < num_on_planes; ++i) { NodePath clip_plane = net_attrib->get_on_plane(i); Planes::const_iterator pi = new_planes->_planes.find(clip_plane); @@ -128,13 +135,17 @@ apply_state(const CullTraverser *trav, const CullTraverserData *data, const Lens *lens = scene->get_lens(); int num_on_occluders = node_effect->get_num_on_occluders(); + if (num_on_occluders > 0 && new_planes.is_null()) { + new_planes = new CullPlanes; + } + for (int i = 0; i < num_on_occluders; ++i) { NodePath occluder = node_effect->get_on_occluder(i); Occluders::const_iterator oi = new_planes->_occluders.find(occluder); if (oi == new_planes->_occluders.end()) { // Here's a new occluder; consider adding it to the list. OccluderNode *occluder_node = DCAST(OccluderNode, occluder.node()); - nassertr(occluder_node->get_num_vertices() == 4, new_planes); + nassertv(occluder_node->get_num_vertices() == 4); CPT(TransformState) occluder_transform = occluder.get_transform(scene->get_cull_center()); @@ -247,7 +258,7 @@ apply_state(const CullTraverser *trav, const CullTraverserData *data, // existing occluder volumes. bool is_enclosed = false; Occluders::const_iterator oi; - for (oi = _occluders.begin(); oi != _occluders.end(); ++oi) { + for (oi = new_planes->_occluders.begin(); oi != new_planes->_occluders.end(); ++oi) { int occluder_result = (*oi).second->contains(occluder_gbv); if ((occluder_result & BoundingVolume::IF_all) != 0) { is_enclosed = true; @@ -292,14 +303,14 @@ apply_state(const CullTraverser *trav, const CullTraverserData *data, if (show_occluder_volumes) { // Draw the frustum for visualization. - nassertr(net_transform != nullptr, new_planes); + nassertv(net_transform != nullptr); trav->draw_bounding_volume(frustum, data->get_internal_transform(trav)); } } } } - return new_planes; + planes = std::move(new_planes); } /** diff --git a/panda/src/pgraph/cullPlanes.h b/panda/src/pgraph/cullPlanes.h index 9480854853..8688e30054 100644 --- a/panda/src/pgraph/cullPlanes.h +++ b/panda/src/pgraph/cullPlanes.h @@ -53,11 +53,12 @@ public: static CPT(CullPlanes) make_empty(); CPT(CullPlanes) xform(const LMatrix4 &mat) const; - CPT(CullPlanes) apply_state(const CullTraverser *trav, - const CullTraverserData *data, - const ClipPlaneAttrib *net_attrib, - const ClipPlaneAttrib *off_attrib, - const OccluderEffect *node_effect) const; + static void apply_state(CPT(CullPlanes) &planes, + const CullTraverser *trav, + const CullTraverserData *data, + const ClipPlaneAttrib *net_attrib, + const ClipPlaneAttrib *off_attrib, + const OccluderEffect *node_effect); CPT(CullPlanes) do_cull(int &result, CPT(RenderState) &state, const GeometricBoundingVolume *node_gbv) const; diff --git a/panda/src/pgraph/cullTraverser.I b/panda/src/pgraph/cullTraverser.I index 98bc781b50..7b243bc7c7 100644 --- a/panda/src/pgraph/cullTraverser.I +++ b/panda/src/pgraph/cullTraverser.I @@ -183,6 +183,14 @@ get_effective_incomplete_render() const { return _effective_incomplete_render; } +/** + * Returns true if fake view frustum culling is active. + */ +INLINE bool CullTraverser:: +get_fake_view_frustum_cull() const { + return _fake_view_frustum_cull; +} + /** * Flushes the PStatCollectors used during traversal. */ @@ -195,53 +203,106 @@ flush_level() { } /** - * This is implemented inline to reduce recursion. + * Calls traverse_down on each child. */ INLINE void CullTraverser:: -do_traverse(CullTraverserData &data) { - if (is_in_view(data)) { - if (pgraph_cat.is_spam()) { - pgraph_cat.spam() - << "\n" << data.get_node_path() - << " " << data._draw_mask << "\n"; - } - - PandaNodePipelineReader *node_reader = data.node_reader(); - int fancy_bits = node_reader->get_fancy_bits(); - - if (fancy_bits == 0 && data._cull_planes->is_empty()) { - // Nothing interesting in this node; just move on. - - } else { - // Something in this node is worth taking a closer look. - const RenderEffects *node_effects = node_reader->get_effects(); - if (node_effects->has_show_bounds()) { - // If we should show the bounding volume for this node, make it up - // now. - show_bounds(data, node_effects->has_show_tight_bounds()); - } - - data.apply_transform_and_state(this); - - const FogAttrib *fog = (const FogAttrib *) - node_reader->get_state()->get_attrib(FogAttrib::get_class_slot()); - - if (fog != nullptr && fog->get_fog() != nullptr) { - // If we just introduced a FogAttrib here, call adjust_to_camera() - // now. This maybe isn't the perfect time to call it, but it's good - // enough; and at this time we have all the information we need for - // it. - fog->get_fog()->adjust_to_camera(get_camera_transform()); - } - - if (fancy_bits & PandaNode::FB_cull_callback) { - PandaNode *node = data.node(); - if (!node->cull_callback(this, data)) { - return; - } - } - } - - traverse_below(data); +traverse_below(CullTraverserData &data) { + PandaNodePipelineReader *node_reader = data.node_reader(); + PandaNode::Children children = node_reader->get_children(); + node_reader->release(); + int num_children = children.get_num_children(); + for (int i = 0; i < num_children; ++i) { + traverse_down(data, children.get_child_connection(i), data._state); } } + +/** + * Traverses a child of the given node/data. + */ +INLINE void CullTraverser:: +traverse_down(const CullTraverserData &data, PandaNode *node) { + traverse_down(data, node, data._net_transform, data._state); +} + +/** + * Traverses down into the given node/data, overriding the current transform + * and state. Note that the state and transform on the given node are still + * applied. + */ +INLINE void CullTraverser:: +traverse_down(const CullTraverserData &data, PandaNode *node, + const TransformState *net_transform, const RenderState *state) { + PandaNodePipelineReader node_reader(node, data._node_reader.get_current_thread()); + + int result = data.is_child_in_view(node_reader, _camera_mask); + if (result == BoundingVolume::IF_no_intersection) { +#ifdef NDEBUG + return; +#else + if (UNLIKELY(_fake_view_frustum_cull)) { + do_fake_cull(data, node, net_transform, state); + } + return; +#endif + } + + GeometricBoundingVolume *view_frustum = nullptr; + if ((result & BoundingVolume::IF_all) == 0 && !node_reader.is_final()) { + view_frustum = data._view_frustum; + } + + CullTraverserData next_data(data, std::move(node_reader), net_transform, state, view_frustum); + if (data._cull_planes != nullptr) { + const GeometricBoundingVolume *node_gbv = node_reader.get_bounds()->as_geometric_bounding_volume(); + if (!next_data.apply_cull_planes(data._cull_planes, node_gbv)) { + return; + } + } + + do_traverse(next_data); +} + +/** + * Traverses down into the given node, as though it were a child node of the + * current node. + */ +INLINE void CullTraverser:: +traverse_down(const CullTraverserData &data, const PandaNode::DownConnection &child) { + traverse_down(data, child, data._state); +} + +/** + * Traverses down into the given node, as though it were a child node of the + * current node, except that the current state is ignored and replaced with the + * given state. The state on the child node is still applied. + */ +INLINE void CullTraverser:: +traverse_down(const CullTraverserData &data, const PandaNode::DownConnection &child, const RenderState *state) { + int result = data.is_child_in_view(child, _camera_mask); + if (result == BoundingVolume::IF_no_intersection) { +#ifdef NDEBUG + return; +#else + if (UNLIKELY(_fake_view_frustum_cull)) { + do_fake_cull(data, child.get_child(), data._net_transform, state); + } + return; +#endif + } + + PandaNodePipelineReader node_reader(child.get_child(), data._node_reader.get_current_thread()); + + GeometricBoundingVolume *view_frustum = nullptr; + if ((result & BoundingVolume::IF_all) == 0 && !node_reader.is_final()) { + view_frustum = data._view_frustum; + } + + CullTraverserData next_data(data, std::move(node_reader), data._net_transform, state, view_frustum); + + if (data._cull_planes != nullptr && + !next_data.apply_cull_planes(data._cull_planes, child.get_bounds())) { + return; + } + + do_traverse(next_data); +} diff --git a/panda/src/pgraph/cullTraverser.cxx b/panda/src/pgraph/cullTraverser.cxx index eee63e026b..1183707240 100644 --- a/panda/src/pgraph/cullTraverser.cxx +++ b/panda/src/pgraph/cullTraverser.cxx @@ -17,6 +17,7 @@ #include "transformState.h" #include "renderState.h" #include "colorAttrib.h" +#include "textureAttrib.h" #include "renderModeAttrib.h" #include "cullFaceAttrib.h" #include "depthOffsetAttrib.h" @@ -48,14 +49,15 @@ TypeHandle CullTraverser::_type_handle; CullTraverser:: CullTraverser() : _gsg(nullptr), - _current_thread(Thread::get_current_thread()) + _current_thread(Thread::get_current_thread()), + _camera_mask(DrawMask::all_on()), + _fake_view_frustum_cull(false), + _has_tag_state_key(false), + _initial_state(RenderState::make_empty()), + _cull_handler(nullptr), + _portal_clipper(nullptr), + _effective_incomplete_render(false) { - _camera_mask = DrawMask::all_on(); - _has_tag_state_key = false; - _initial_state = RenderState::make_empty(); - _cull_handler = nullptr; - _portal_clipper = nullptr; - _effective_incomplete_render = true; } /** @@ -67,6 +69,7 @@ CullTraverser(const CullTraverser ©) : _current_thread(copy._current_thread), _scene_setup(copy._scene_setup), _camera_mask(copy._camera_mask), + _fake_view_frustum_cull(copy._fake_view_frustum_cull), _has_tag_state_key(copy._has_tag_state_key), _tag_state_key(copy._tag_state_key), _initial_state(copy._initial_state), @@ -99,6 +102,10 @@ set_scene(SceneSetup *scene_setup, GraphicsStateGuardianBase *gsg, _effective_incomplete_render = _gsg->get_incomplete_render() && dr_incomplete_render; _view_frustum = scene_setup->get_view_frustum(); + +#ifndef NDEBUG + _fake_view_frustum_cull = fake_view_frustum_cull; +#endif } /** @@ -132,7 +139,9 @@ traverse(const NodePath &root) { _initial_state, _view_frustum, _current_thread); - traverse(data); + if (data.is_in_view(_camera_mask)) { + do_traverse(data); + } // Finally add the lines to be drawn if (debug_portal_cull) { @@ -143,45 +152,68 @@ traverse(const NodePath &root) { NodePath cull_center = _scene_setup->get_cull_center(); CPT(TransformState) transform = cull_center.get_transform(root); - CullTraverserData my_data(data, portal_viewer._previous); - my_data._net_transform = my_data._net_transform->compose(transform); - traverse(my_data); + CullTraverserData my_data(data, portal_viewer._previous, + data._net_transform->compose(transform), + data._state, data._view_frustum); + if (my_data.is_in_view(_camera_mask)) { + do_traverse(my_data); + } } else { CullTraverserData data(root, TransformState::make_identity(), _initial_state, _view_frustum, _current_thread); - do_traverse(data); + if (data.is_in_view(_camera_mask)) { + do_traverse(data); + } } } /** - * Traverses from the next node with the given data, which has been - * constructed with the node but has not yet been converted into the node's - * space. + * Internal method called by traverse() and traverse_down(). Traverses the + * given node, assuming it has already been checked with is_in_view(). */ void CullTraverser:: -traverse(CullTraverserData &data) { - do_traverse(data); -} +do_traverse(CullTraverserData &data) { +#ifndef NDEBUG + if (UNLIKELY(pgraph_cat.is_spam())) { + pgraph_cat.spam() + << "\n" << data.get_node_path() + << " " << data._draw_mask << "\n"; + } +#endif -/** - * Traverses all the children of the indicated node, with the given data, - * which has been converted into the node's space. - */ -void CullTraverser:: -traverse_below(CullTraverserData &data) { - _nodes_pcollector.add_level(1); - PandaNodePipelineReader *node_reader = data.node_reader(); PandaNode *node = data.node(); + PandaNodePipelineReader *node_reader = data.node_reader(); + int fancy_bits = node_reader->get_fancy_bits(); - if (!data.is_this_node_hidden(_camera_mask)) { + if ((fancy_bits & ~PandaNode::FB_renderable) == 0 && data._cull_planes == nullptr) { + // Nothing interesting in this node; just move on. + } + else { + // Something in this node is worth taking a closer look. + if (fancy_bits & PandaNode::FB_show_bounds) { + // If we should show the bounding volume for this node, make it up + // now. + show_bounds(data, (fancy_bits & PandaNode::FB_show_tight_bounds) != 0); + } + + data.apply_transform_and_state(this); + + if (fancy_bits & PandaNode::FB_cull_callback) { + if (!node->cull_callback(this, data)) { + return; + } + } + } + + if ((fancy_bits & PandaNode::FB_renderable) != 0 && + !data.is_this_node_hidden(_camera_mask)) { node->add_for_draw(this, data); // Check for a decal effect. - const RenderEffects *node_effects = node_reader->get_effects(); - if (node_effects->has_decal()) { + if (fancy_bits & PandaNode::FB_decal) { // If we *are* implementing decals with DepthOffsetAttribs, apply it // now, so that each child of this node gets offset by a tiny amount. data._state = data._state->compose(get_depth_offset_state()); @@ -195,22 +227,15 @@ traverse_below(CullTraverserData &data) { } } + _nodes_pcollector.add_level(1); + // Now visit all the node's children. PandaNode::Children children = node_reader->get_children(); node_reader->release(); int num_children = children.get_num_children(); - if (!node->has_selective_visibility()) { - for (int i = 0; i < num_children; ++i) { - CullTraverserData next_data(data, children.get_child(i)); - do_traverse(next_data); - } - } else { - int i = node->get_first_visible_child(); - while (i < num_children) { - CullTraverserData next_data(data, children.get_child(i)); - do_traverse(next_data); - i = node->get_next_visible_child(i); - } + for (int i = 0; i < num_children; ++i) { + const PandaNode::DownConnection &child = children.get_child_connection(i); + traverse_down(data, child, data._state); } } @@ -246,13 +271,28 @@ draw_bounding_volume(const BoundingVolume *vol, } /** - * Returns true if the current node is fully or partially within the viewing - * area and should be drawn, or false if it (and all of its children) should - * be pruned. + * Implements fake-view-frustum-cull. */ -bool CullTraverser:: -is_in_view(CullTraverserData &data) { - return data.is_in_view(_camera_mask); +void CullTraverser:: +do_fake_cull(const CullTraverserData &data, PandaNode *child, + const TransformState *net_transform, const RenderState *state) { +#ifndef NDEBUG + // Once someone asks for this pointer, we hold its reference count and never + // free it. + static CPT(RenderState) fake_view_frustum_cull_state; + if (fake_view_frustum_cull_state == nullptr) { + fake_view_frustum_cull_state = RenderState::make + (ColorAttrib::make_flat(LColor(1.0f, 0.0f, 0.0f, 1.0f)), + TextureAttrib::make_all_off(), + RenderModeAttrib::make(RenderModeAttrib::M_wireframe), + RenderState::get_max_priority()); + } + + CullTraverserData next_data(data, child, net_transform, + state->compose(fake_view_frustum_cull_state), + nullptr); + do_traverse(next_data); +#endif } /** @@ -503,7 +543,7 @@ compute_point(const BoundingSphere *sphere, * Returns a RenderState for rendering the outside surfaces of the bounding * volume visualizations. */ -CPT(RenderState) CullTraverser:: +const RenderState *CullTraverser:: get_bounds_outer_viz_state() { // Once someone asks for this pointer, we hold its reference count and never // free it. @@ -521,7 +561,7 @@ get_bounds_outer_viz_state() { * Returns a RenderState for rendering the inside surfaces of the bounding * volume visualizations. */ -CPT(RenderState) CullTraverser:: +const RenderState *CullTraverser:: get_bounds_inner_viz_state() { // Once someone asks for this pointer, we hold its reference count and never // free it. @@ -538,7 +578,7 @@ get_bounds_inner_viz_state() { /** * Returns a RenderState for increasing the DepthOffset by one. */ -CPT(RenderState) CullTraverser:: +const RenderState *CullTraverser:: get_depth_offset_state() { // Once someone asks for this pointer, we hold its reference count and never // free it. diff --git a/panda/src/pgraph/cullTraverser.h b/panda/src/pgraph/cullTraverser.h index b8ecbe084b..a243a165cc 100644 --- a/panda/src/pgraph/cullTraverser.h +++ b/panda/src/pgraph/cullTraverser.h @@ -76,22 +76,33 @@ PUBLISHED: INLINE PortalClipper *get_portal_clipper() const; INLINE bool get_effective_incomplete_render() const; - - void traverse(const NodePath &root); - void traverse(CullTraverserData &data); - virtual void traverse_below(CullTraverserData &data); - - virtual void end_traverse(); + INLINE bool get_fake_view_frustum_cull() const; INLINE static void flush_level(); + void traverse(const NodePath &root); + void do_traverse(CullTraverserData &data); + INLINE void traverse_below(CullTraverserData &data); + + virtual void end_traverse(); + void draw_bounding_volume(const BoundingVolume *vol, const TransformState *internal_transform) const; -protected: - INLINE void do_traverse(CullTraverserData &data); +public: + INLINE void traverse_down(const CullTraverserData &data, PandaNode *child); + INLINE void traverse_down(const CullTraverserData &data, PandaNode *child, + const TransformState *net_transform, + const RenderState *state); + INLINE void traverse_down(const CullTraverserData &data, + const PandaNode::DownConnection &child); + INLINE void traverse_down(const CullTraverserData &data, + const PandaNode::DownConnection &child, + const RenderState *state); - virtual bool is_in_view(CullTraverserData &data); + void do_fake_cull(const CullTraverserData &data, PandaNode *child, + const TransformState *net_transform, + const RenderState *state); public: // Statistics @@ -106,14 +117,15 @@ private: PT(Geom) make_tight_bounds_viz(PandaNode *node) const; static LVertex compute_point(const BoundingSphere *sphere, PN_stdfloat latitude, PN_stdfloat longitude); - static CPT(RenderState) get_bounds_outer_viz_state(); - static CPT(RenderState) get_bounds_inner_viz_state(); - static CPT(RenderState) get_depth_offset_state(); + static const RenderState *get_bounds_outer_viz_state(); + static const RenderState *get_bounds_inner_viz_state(); + static const RenderState *get_depth_offset_state(); GraphicsStateGuardianBase *_gsg; Thread *_current_thread; PT(SceneSetup) _scene_setup; DrawMask _camera_mask; + bool _fake_view_frustum_cull; bool _has_tag_state_key; std::string _tag_state_key; CPT(RenderState) _initial_state; diff --git a/panda/src/pgraph/cullTraverserData.I b/panda/src/pgraph/cullTraverserData.I index cc5126dc07..ff456b01cb 100644 --- a/panda/src/pgraph/cullTraverserData.I +++ b/panda/src/pgraph/cullTraverserData.I @@ -17,22 +17,18 @@ INLINE CullTraverserData:: CullTraverserData(const NodePath &start, const TransformState *net_transform, - const RenderState *state, + CPT(RenderState) state, GeometricBoundingVolume *view_frustum, Thread *current_thread) : _next(nullptr), _start(start._head), _node_reader(start.node(), current_thread), _net_transform(net_transform), - _state(state), + _state(std::move(state)), _view_frustum(view_frustum), - _cull_planes(CullPlanes::make_empty()), _draw_mask(DrawMask::all_on()), _portal_depth(0) { - // Only update the bounding volume if we're going to end up needing it. - bool check_bounds = (view_frustum != nullptr); - _node_reader.check_cached(check_bounds); } /** @@ -40,24 +36,46 @@ CullTraverserData(const NodePath &start, * node down in the traversal. */ INLINE CullTraverserData:: -CullTraverserData(const CullTraverserData &parent, PandaNode *child) : +CullTraverserData(const CullTraverserData &parent, PandaNode *child, + const TransformState *net_transform, + CPT(RenderState) state, + GeometricBoundingVolume *view_frustum) : _next(&parent), #ifdef _DEBUG _start(nullptr), #endif _node_reader(child, parent._node_reader.get_current_thread()), - _net_transform(parent._net_transform), - _state(parent._state), - _view_frustum(parent._view_frustum), - _cull_planes(parent._cull_planes), + _net_transform(net_transform), + _state(std::move(state)), + _view_frustum(view_frustum), + _instances(parent._instances), + _draw_mask(parent._draw_mask), + _portal_depth(parent._portal_depth) +{ +} + +/** + * This constructor creates a CullTraverserData object that reflects the next + * node down in the traversal. + */ +INLINE CullTraverserData:: +CullTraverserData(const CullTraverserData &parent, + PandaNodePipelineReader &&node_reader, + const TransformState *net_transform, + CPT(RenderState) state, + GeometricBoundingVolume *view_frustum) : + _next(&parent), +#ifdef _DEBUG + _start(nullptr), +#endif + _node_reader(std::move(node_reader)), + _net_transform(net_transform), + _state(std::move(state)), + _view_frustum(view_frustum), _instances(parent._instances), _draw_mask(parent._draw_mask), _portal_depth(parent._portal_depth) { - // Only update the bounding volume if we're going to end up needing it. - bool check_bounds = !_cull_planes->is_empty() || - (_view_frustum != nullptr); - _node_reader.check_cached(check_bounds); } /** @@ -125,32 +143,82 @@ get_net_transform(const CullTraverser *) const { } /** - * Returns true if the current node is within the view frustum, false - * otherwise. If the node's bounding volume falls completely within the view - * frustum, this will also reset the view frustum pointer, saving some work - * for future nodes. + * Returns intersection flags if the current node may be within the view + * frustum, 0 otherwise. */ -INLINE bool CullTraverserData:: -is_in_view(const DrawMask &camera_mask) { - if (_node_reader.get_transform()->is_invalid()) { - // If the transform is invalid, forget it. - return false; - } +INLINE int CullTraverserData:: +is_in_view(const DrawMask &camera_mask) const { + bool check_bounds = (_view_frustum != nullptr || _cull_planes != nullptr); + _node_reader.check_cached(check_bounds); if (!_node_reader.compare_draw_mask(_draw_mask, camera_mask)) { // If there are no draw bits in common with the camera, the node is out. - return false; + return BoundingVolume::IF_no_intersection; } - if (_view_frustum == nullptr && - _cull_planes->is_empty()) { - // If the transform is valid, but we don't have a frustum or any clip - // planes or occluders, it's always in. - return true; + if (_node_reader.get_transform()->is_invalid()) { + // If the transform is invalid, forget it. + return BoundingVolume::IF_no_intersection; } - // Otherwise, compare the bounding volume to the frustum. - return is_in_view_impl(); + if (_view_frustum != nullptr) { + const GeometricBoundingVolume *node_gbv = _node_reader.get_bounds()->as_geometric_bounding_volume(); + nassertr(node_gbv != nullptr, BoundingVolume::IF_no_intersection); + + return _view_frustum->contains(node_gbv); + } + + return BoundingVolume::IF_possible; +} + +/** + * Returns intersection flags if the given child of the current node may be + * within the view frustum, 0 otherwise. + */ +INLINE int CullTraverserData:: +is_child_in_view(const PandaNodePipelineReader &node_reader, const DrawMask &camera_mask) const { + bool check_bounds = (_view_frustum != nullptr || _cull_planes != nullptr); + node_reader.check_cached(check_bounds); + + if (!node_reader.compare_draw_mask(_draw_mask, camera_mask)) { + // If there are no draw bits in common with the camera, the node is out. + return BoundingVolume::IF_no_intersection; + } + + if (_node_reader.get_transform()->is_invalid()) { + // If the transform is invalid, forget it. + return BoundingVolume::IF_no_intersection; + } + + if (_view_frustum != nullptr) { + const GeometricBoundingVolume *node_gbv = node_reader.get_bounds()->as_geometric_bounding_volume(); + nassertr(node_gbv != nullptr, BoundingVolume::IF_no_intersection); + + return _view_frustum->contains(node_gbv); + } + return BoundingVolume::IF_possible; +} + +/** + * Returns intersection flags if the given child of the current node may be + * within the view frustum, 0 otherwise. + */ +INLINE int CullTraverserData:: +is_child_in_view(const PandaNode::DownConnection &child, const DrawMask &camera_mask) const { + // No need to call check_cached here, since we presumably already called it + // on the parent. + if (!child.compare_draw_mask(_draw_mask, camera_mask)) { + // If there are no draw bits in common with the camera, the node is out. + return BoundingVolume::IF_no_intersection; + } + + if (_view_frustum != nullptr) { + const GeometricBoundingVolume *node_gbv = child.get_bounds(); + nassertr(node_gbv != nullptr, BoundingVolume::IF_no_intersection); + + return _view_frustum->contains(node_gbv); + } + return BoundingVolume::IF_possible; } /** diff --git a/panda/src/pgraph/cullTraverserData.cxx b/panda/src/pgraph/cullTraverserData.cxx index 12f71ef12f..cd1a535899 100644 --- a/panda/src/pgraph/cullTraverserData.cxx +++ b/panda/src/pgraph/cullTraverserData.cxx @@ -15,9 +15,6 @@ #include "cullTraverser.h" #include "config_pgraph.h" #include "pandaNode.h" -#include "colorAttrib.h" -#include "textureAttrib.h" -#include "renderModeAttrib.h" #include "clipPlaneAttrib.h" #include "boundingPlane.h" #include "billboardEffect.h" @@ -64,10 +61,28 @@ apply_transform_and_state(CullTraverser *trav) { } if (clip_plane_cull) { - _cull_planes = _cull_planes->apply_state(trav, this, - (const ClipPlaneAttrib *)node_state->get_attrib(ClipPlaneAttrib::get_class_slot()), - (const ClipPlaneAttrib *)_node_reader.get_off_clip_planes(), - (const OccluderEffect *)node_effects->get_effect(OccluderEffect::get_class_type())); + const ClipPlaneAttrib *cpa = (const ClipPlaneAttrib *) + node_state->get_attrib(ClipPlaneAttrib::get_class_slot()); + const OccluderEffect *occluders = (const OccluderEffect *) + node_effects->get_effect(OccluderEffect::get_class_type()); + + if (cpa != nullptr || occluders != nullptr) { + CullPlanes::apply_state(_cull_planes, trav, this, cpa, + (const ClipPlaneAttrib *)_node_reader.get_off_clip_planes(), + occluders); + } + } + + const FogAttrib *fog_attr; + if (node_state->get_attrib(fog_attr)) { + Fog *fog = fog_attr->get_fog(); + if (fog != nullptr) { + // If we just introduced a FogAttrib here, call adjust_to_camera() + // now. This maybe isn't the perfect time to call it, but it's good + // enough; and at this time we have all the information we need for + // it. + fog->adjust_to_camera(trav->get_camera_transform()); + } } } @@ -88,35 +103,90 @@ apply_transform(const TransformState *node_transform) { _net_transform = _net_transform->compose(node_transform); - if ((_view_frustum != nullptr) || - (!_cull_planes->is_empty())) { + if (_view_frustum != nullptr || _cull_planes != nullptr) { // We need to move the viewing frustums into the node's coordinate space // by applying the node's inverse transform. - if (node_transform->is_singular()) { - // But we can't invert a singular transform! Instead of trying, we'll - // just give up on frustum culling from this point down. - _view_frustum = nullptr; - _cull_planes = CullPlanes::make_empty(); - - } else { - CPT(TransformState) inv_transform = - node_transform->invert_compose(TransformState::make_identity()); - + const LMatrix4 *inverse_mat = node_transform->get_inverse_mat(); + if (inverse_mat != nullptr) { // Copy the bounding volumes for the frustums so we can transform // them. if (_view_frustum != nullptr) { _view_frustum = _view_frustum->make_copy()->as_geometric_bounding_volume(); nassertv(_view_frustum != nullptr); - _view_frustum->xform(inv_transform->get_mat()); + _view_frustum->xform(*inverse_mat); } - _cull_planes = _cull_planes->xform(inv_transform->get_mat()); + if (_cull_planes != nullptr) { + _cull_planes = _cull_planes->xform(*inverse_mat); + } + } + else { + // But we can't invert a singular transform! Instead of trying, we'll + // just give up on frustum culling from this point down. + pgraph_cat.warning() + << "Singular transformation detected on node: " << get_node_path() << "\n"; + _view_frustum = nullptr; + _cull_planes = nullptr; } } } } +/** + * Returns intersection flags if any of the children under the current node are + * in view if first transformed by the given transform, false otherwise. + */ +bool CullTraverserData:: +is_instance_in_view(const TransformState *instance_transform, const DrawMask &camera_mask) const { + PT(GeometricBoundingVolume) view_frustum_p; + const GeometricBoundingVolume *view_frustum = nullptr; + + if (_view_frustum != nullptr) { + if (!instance_transform->is_identity()) { + // We need to move the viewing frustums into the node's coordinate space + // by applying the node's inverse transform. + const LMatrix4 *inverse_mat = instance_transform->get_inverse_mat(); + if (inverse_mat != nullptr) { + // Copy the bounding volumes for the frustums so we can transform them. + view_frustum_p = _view_frustum->make_copy()->as_geometric_bounding_volume(); + nassertr(view_frustum_p != nullptr, false); + + view_frustum_p->xform(*inverse_mat); + view_frustum = view_frustum_p; + } else { + // Don't render instances with a singular transformation. + return false; + } + } else { + view_frustum = _view_frustum; + } + } + + PandaNode::Children children = _node_reader.get_children(); + int num_children = children.get_num_children(); + for (int i = 0; i < num_children; ++i) { + const PandaNode::DownConnection &child = children.get_child_connection(i); + + if (!child.compare_draw_mask(_draw_mask, camera_mask)) { + // If there are no draw bits in common with the camera, the node is out. + continue; + } + + if (view_frustum == nullptr) { + return true; + } + + const GeometricBoundingVolume *node_gbv = child.get_bounds(); + nassertd(node_gbv != nullptr) continue; + + if (view_frustum->contains(node_gbv)) { + return true; + } + } + return false; +} + /** * The private, recursive implementation of get_node_path(), this returns the * NodePathComponent representing the NodePath. @@ -152,127 +222,37 @@ r_get_node_path() const { } /** - * The private implementation of is_in_view(). + * Applies the cull planes. Returns true if the node should still be rendered, + * false if it should be culled. */ bool CullTraverserData:: -is_in_view_impl() { - const GeometricBoundingVolume *node_gbv = nullptr; - - if (_view_frustum != nullptr) { - node_gbv = _node_reader.get_bounds()->as_geometric_bounding_volume(); - nassertr(node_gbv != nullptr, false); - - int result = _view_frustum->contains(node_gbv); - - if (pgraph_cat.is_spam()) { - pgraph_cat.spam() - << get_node_path() << " cull result = " << std::hex << result << std::dec << "\n"; - } - - if (result == BoundingVolume::IF_no_intersection) { - // No intersection at all. Cull. -#ifdef NDEBUG - return false; -#else - if (!fake_view_frustum_cull) { - return false; - } - - // If we have fake view-frustum culling enabled, instead of actually - // culling an object we simply force it to be drawn in red wireframe. - _view_frustum = nullptr; - _state = _state->compose(get_fake_view_frustum_cull_state()); -#endif - - } else if ((result & BoundingVolume::IF_all) != 0) { - // The node and its descendents are completely enclosed within the - // frustum. No need to cull further. - _view_frustum = nullptr; - - } else { - // The node is partially, but not completely, within the viewing - // frustum. - if (_node_reader.is_final()) { - // Normally we'd keep testing child bounding volumes as we continue - // down. But this node has the "final" flag, so the user is claiming - // that there is some important reason we should consider everything - // visible at this point. So be it. - _view_frustum = nullptr; - } - } - } - - if (!_cull_planes->is_empty()) { - if (node_gbv == nullptr) { - node_gbv = _node_reader.get_bounds()->as_geometric_bounding_volume(); - nassertr(node_gbv != nullptr, false); - } - +apply_cull_planes(const CullPlanes *planes, const GeometricBoundingVolume *node_gbv) { + if (!planes->is_empty()) { // Also cull against the current clip planes. int result; - _cull_planes = _cull_planes->do_cull(result, _state, node_gbv); + CPT(CullPlanes) new_planes = planes->do_cull(result, _state, node_gbv); if (pgraph_cat.is_spam()) { pgraph_cat.spam() << get_node_path() << " cull planes cull result = " << std::hex << result << std::dec << "\n"; - _cull_planes->write(pgraph_cat.spam(false)); - } - - if (_node_reader.is_final()) { - // Even though the node may be partially within the clip planes, do no - // more culling against them below this node. - _cull_planes = CullPlanes::make_empty(); - - if (pgraph_cat.is_spam()) { - pgraph_cat.spam() - << get_node_path() << " is_final, cull planes disabled, state:\n"; - _state->write(pgraph_cat.spam(false), 2); - } + new_planes->write(pgraph_cat.spam(false)); } if (result == BoundingVolume::IF_no_intersection) { // No intersection at all. Cull. -#ifdef NDEBUG return false; -#else - if (!fake_view_frustum_cull) { - return false; - } - _cull_planes = CullPlanes::make_empty(); - _state = _state->compose(get_fake_view_frustum_cull_state()); -#endif - - } else if ((result & BoundingVolume::IF_all) != 0) { - // The node and its descendents are completely in front of all of the + } + else if ((result & BoundingVolume::IF_all) != 0) { + // The node and its descendants are completely in front of all of the // clip planes and occluders. The do_cull() call should therefore have // removed all of the clip planes and occluders. - nassertr(_cull_planes->is_empty(), true); + nassertr(new_planes->is_empty(), true); + } + else if (!_node_reader.is_final() && !new_planes->is_empty()) { + _cull_planes = std::move(new_planes); } } return true; } - -/** - * Returns a RenderState for rendering stuff in red wireframe, strictly for - * the fake_view_frustum_cull effect. - */ -const RenderState *CullTraverserData:: -get_fake_view_frustum_cull_state() { -#ifdef NDEBUG - return nullptr; -#else - // Once someone asks for this pointer, we hold its reference count and never - // free it. - static CPT(RenderState) state; - if (state == nullptr) { - state = RenderState::make - (ColorAttrib::make_flat(LColor(1.0f, 0.0f, 0.0f, 1.0f)), - TextureAttrib::make_all_off(), - RenderModeAttrib::make(RenderModeAttrib::M_wireframe), - RenderState::get_max_priority()); - } - return state; -#endif -} diff --git a/panda/src/pgraph/cullTraverserData.h b/panda/src/pgraph/cullTraverserData.h index cdb8dd2a94..9fe4755e34 100644 --- a/panda/src/pgraph/cullTraverserData.h +++ b/panda/src/pgraph/cullTraverserData.h @@ -42,11 +42,19 @@ class EXPCL_PANDA_PGRAPH CullTraverserData { public: INLINE CullTraverserData(const NodePath &start, const TransformState *net_transform, - const RenderState *state, + CPT(RenderState) state, GeometricBoundingVolume *view_frustum, Thread *current_thread); INLINE CullTraverserData(const CullTraverserData &parent, - PandaNode *child); + PandaNode *child, + const TransformState *net_transform, + CPT(RenderState) state, + GeometricBoundingVolume *view_frustum); + INLINE CullTraverserData(const CullTraverserData &parent, + PandaNodePipelineReader &&node_reader, + const TransformState *net_transform, + CPT(RenderState) state, + GeometricBoundingVolume *view_frustum); PUBLISHED: INLINE PandaNode *node() const; @@ -62,14 +70,21 @@ PUBLISHED: INLINE CPT(TransformState) get_internal_transform(const CullTraverser *trav) const; INLINE const TransformState *get_net_transform(const CullTraverser *trav) const; - INLINE bool is_in_view(const DrawMask &camera_mask); + INLINE int is_in_view(const DrawMask &camera_mask) const; INLINE bool is_this_node_hidden(const DrawMask &camera_mask) const; + bool apply_cull_planes(const CullPlanes *planes, const GeometricBoundingVolume *node_gbv); + void apply_transform_and_state(CullTraverser *trav); void apply_transform(const TransformState *node_transform); MAKE_PROPERTY(node_path, get_node_path); +public: + bool is_instance_in_view(const TransformState *instance_transform, const DrawMask &camera_mask) const; + INLINE int is_child_in_view(const PandaNodePipelineReader &node_reader, const DrawMask &camera_mask) const; + INLINE int is_child_in_view(const PandaNode::DownConnection &child, const DrawMask &camera_mask) const; + private: // We store a chain leading all the way to the root, so that we can compose // a NodePath. We may be able to eliminate this requirement in the future. @@ -88,9 +103,6 @@ public: private: PT(NodePathComponent) r_get_node_path() const; - - bool is_in_view_impl(); - static const RenderState *get_fake_view_frustum_cull_state(); }; /* okcircular */ diff --git a/panda/src/pgraph/geomNode.cxx b/panda/src/pgraph/geomNode.cxx index c9f379addc..b7e9842beb 100644 --- a/panda/src/pgraph/geomNode.cxx +++ b/panda/src/pgraph/geomNode.cxx @@ -59,6 +59,8 @@ GeomNode(const std::string &name) : // GeomNodes have a certain set of bits on by default. set_into_collide_mask(get_default_collide_mask()); + + set_renderable(); } /** @@ -490,17 +492,6 @@ calc_tight_bounds(LPoint3 &min_point, LPoint3 &max_point, bool &found_any, return next_transform; } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool GeomNode:: -is_renderable() const { - return true; -} - /** * Adds the node's contents to the CullResult we are building up during the * cull traversal, so that it will be drawn at render time. For most nodes @@ -569,7 +560,7 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { // Cull this Geom. continue; } - if (!data._cull_planes->is_empty()) { + if (data._cull_planes != nullptr) { // Also cull the Geom against the cull planes. CPT(BoundingVolume) geom_volume = geom->get_bounds(current_thread); const GeometricBoundingVolume *geom_gbv = diff --git a/panda/src/pgraph/geomNode.h b/panda/src/pgraph/geomNode.h index 1402d07b5a..c6e28516cb 100644 --- a/panda/src/pgraph/geomNode.h +++ b/panda/src/pgraph/geomNode.h @@ -50,7 +50,6 @@ public: bool &found_any, const TransformState *transform, Thread *current_thread) const; - virtual bool is_renderable() const; virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data); virtual CollideMask get_legal_collide_mask() const; diff --git a/panda/src/pgraph/instancedNode.cxx b/panda/src/pgraph/instancedNode.cxx index 567b31316e..c1a51281ea 100644 --- a/panda/src/pgraph/instancedNode.cxx +++ b/panda/src/pgraph/instancedNode.cxx @@ -218,32 +218,29 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { instances = new_list; } - if (data._view_frustum != nullptr || !data._cull_planes->is_empty()) { - // Culling is on, so we need to figure out which instances are visible. - Children children = data.node_reader()->get_children(); - data.node_reader()->release(); - - // Keep track of which instances should be culled away. + if (data._view_frustum != nullptr || data._cull_planes != nullptr) { + // Culling is on, so we need to figure out which instances should be culled. BitArray culled_instances; culled_instances.set_range(0, instances->size()); for (size_t ii = 0; ii < instances->size(); ++ii) { - CullTraverserData instance_data(data); - instance_data.apply_transform((*instances)[ii].get_transform()); + if (data.is_instance_in_view((*instances)[ii].get_transform(), trav->get_camera_mask())) { + culled_instances.clear_bit(ii); + } + } - for (size_t ci = 0; ci < children.size(); ++ci) { - CullTraverserData child_data(instance_data, children.get_child(ci)); - if (child_data.is_in_view(trav->get_camera_mask())) { - // Yep, the instance is in view. - culled_instances.clear_bit(ii); - break; - } + if (!culled_instances.is_zero() && trav->get_fake_view_frustum_cull()) { + // The culled instances are drawn with the fake-view-frustum-cull effect. + data._instances = instances->without(culled_instances ^ BitArray::range(0, instances->size())); + + Children children = data.node_reader()->get_children(); + int num_children = children.get_num_children(); + for (int i = 0; i < num_children; ++i) { + trav->do_fake_cull(data, children.get_child(i), data._net_transform, data._state); } } instances = instances->without(culled_instances); - } else { - data.node_reader()->release(); } if (instances->empty()) { @@ -259,15 +256,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { data._cull_planes = CullPlanes::make_empty(); return true; - - /* - for (const InstanceList::Instance &instance : *instances) { - CullTraverserData instance_data(data); - instance_data.apply_transform(instance.get_transform()); - trav->traverse_below(instance_data); - } - return false; - */ } /** diff --git a/panda/src/pgraph/occluderNode.cxx b/panda/src/pgraph/occluderNode.cxx index 09f8bbd038..a1db42c3c3 100644 --- a/panda/src/pgraph/occluderNode.cxx +++ b/panda/src/pgraph/occluderNode.cxx @@ -55,6 +55,7 @@ OccluderNode(const std::string &name) : PandaNode(name) { set_cull_callback(); + set_renderable(); // OccluderNodes are hidden by default. set_overall_hidden(true); set_double_sided(false); @@ -156,18 +157,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { return true; } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool OccluderNode:: -is_renderable() const { - return true; -} - - /** * Writes a brief description of the node to the indicated output stream. * This is invoked by the << operator. It may be overridden in derived diff --git a/panda/src/pgraph/occluderNode.h b/panda/src/pgraph/occluderNode.h index ebc5db9bc3..484c38753d 100644 --- a/panda/src/pgraph/occluderNode.h +++ b/panda/src/pgraph/occluderNode.h @@ -42,7 +42,6 @@ public: virtual void xform(const LMatrix4 &mat); virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual bool is_renderable() const; virtual void output(std::ostream &out) const; diff --git a/panda/src/pgraph/pandaNode.I b/panda/src/pgraph/pandaNode.I index 8cbea5c871..bf961413c5 100644 --- a/panda/src/pgraph/pandaNode.I +++ b/panda/src/pgraph/pandaNode.I @@ -773,6 +773,59 @@ get_sort() const { return _sort; } +/** + * Returns the net collide mask of this child. Requires that update_cached() + * has been called on the parent. + */ +INLINE CollideMask PandaNode::DownConnection:: +get_net_collide_mask() const { + return _net_collide_mask; +} + +/** + * Returns the external bounding volume of this child. Requires that + * update_cached() has been called on the parent. + */ +INLINE const GeometricBoundingVolume *PandaNode::DownConnection:: +get_bounds() const { + return _external_bounds; +} + +/** + * Compares the running draw mask computed during a traversal with this child's + * net draw masks. Returns true if the child should be traversed into, or + * false if there is nothing at this level or below that will be visible to + * the indicated camera_mask. + */ +INLINE bool PandaNode::DownConnection:: +compare_draw_mask(DrawMask running_draw_mask, DrawMask camera_mask) const { + // As a special case, if net_draw_show_mask is all 0, it means either that + // all nodes under this node are hidden to all cameras, or that none of them + // are renderable nodes (or some combination). In either case, we might as + // well short-circuit. + if (_net_draw_show_mask.is_zero()) { + return false; + } + + DrawMask net_draw_control_mask, net_draw_show_mask; + net_draw_control_mask = _net_draw_control_mask; + net_draw_show_mask = _net_draw_show_mask; + + // Now the bits that are not in net_draw_control_mask--that is, those bits + // that are not changed by any of the nodes at this level and below--are + // taken from running_draw_mask, which is inherited from above. On the + // other hand, the bits that *are* in net_draw_control_mask--those bits that + // are changed by any of the nodes at this level and below--are taken from + // net_draw_show_mask, which is propagated upwards from below. + + // This way, we will traverse into this node if it has any children which + // want to be visited by the traversal, but we will avoid traversing into it + // if all of its children are hidden to this camera. + DrawMask compare_mask = (running_draw_mask & ~net_draw_control_mask) | (net_draw_show_mask & net_draw_control_mask); + + return !((compare_mask & PandaNode::_overall_bit).is_zero()) && !((compare_mask & camera_mask).is_zero()); +} + /** * */ @@ -1351,6 +1404,18 @@ get_child(int n) const { return (*down)[n].get_child(); } +/** + * Returns the nth child node of this node. See get_num_children(). Also see + * get_children(), if your intention is to iterate through the complete list + * of children; get_children() is preferable in this case. + */ +INLINE const PandaNode::DownConnection &PandaNodePipelineReader:: +get_child_connection(int n) const { + CPT(PandaNode::Down) down = _cdata->get_down(); + //nassertr(n >= 0 && n < (int)down->size(), nullptr); + return (*down)[n]; +} + /** * Returns the sort index of the nth child node of this node (that is, the * number that was passed to add_child()). See get_num_children(). diff --git a/panda/src/pgraph/pandaNode.cxx b/panda/src/pgraph/pandaNode.cxx index fabae9a272..9e13b3edfe 100644 --- a/panda/src/pgraph/pandaNode.cxx +++ b/panda/src/pgraph/pandaNode.cxx @@ -27,6 +27,8 @@ #include "config_mathutil.h" #include "lightReMutexHolder.h" #include "graphicsStateGuardianBase.h" +#include "decalEffect.h" +#include "showBoundsEffect.h" using std::ostream; using std::ostringstream; @@ -387,46 +389,6 @@ cull_callback(CullTraverser *, CullTraverserData &) { return true; } -/** - * Should be overridden by derived classes to return true if this kind of node - * has some restrictions on the set of children that should be rendered. Node - * with this property include LODNodes, SwitchNodes, and SequenceNodes. - * - * If this function returns true, get_first_visible_child() and - * get_next_visible_child() will be called to walk through the list of - * children during cull, instead of iterating through the entire list. This - * method is called after cull_callback(), so cull_callback() may be - * responsible for the decisions as to which children are visible at the - * moment. - */ -bool PandaNode:: -has_selective_visibility() const { - return false; -} - -/** - * Returns the index number of the first visible child of this node, or a - * number >= get_num_children() if there are no visible children of this node. - * This is called during the cull traversal, but only if - * has_selective_visibility() has already returned true. See - * has_selective_visibility(). - */ -int PandaNode:: -get_first_visible_child() const { - return 0; -} - -/** - * Returns the index number of the next visible child of this node following - * the indicated child, or a number >= get_num_children() if there are no more - * visible children of this node. See has_selective_visibility() and - * get_first_visible_child(). - */ -int PandaNode:: -get_next_visible_child(int n) const { - return n + 1; -} - /** * Should be overridden by derived classes to return true if this kind of node * has the special property that just one of its children is visible at any @@ -460,7 +422,8 @@ get_visible_child() const { */ bool PandaNode:: is_renderable() const { - return false; + CDReader cdata(_cycler); + return (cdata->_fancy_bits & FB_renderable) != 0; } /** @@ -1003,7 +966,15 @@ set_effect(const RenderEffect *effect) { OPEN_ITERATE_CURRENT_AND_UPSTREAM(_cycler, current_thread) { CDStageWriter cdata(_cycler, pipeline_stage, current_thread); cdata->_effects = cdata->_effects->add_effect(effect); + cdata->set_fancy_bit(FB_effects, true); + if (effect->get_type() == DecalEffect::get_class_type()) { + cdata->set_fancy_bit(FB_decal, true); + } + else if (effect->get_type() == ShowBoundsEffect::get_class_type()) { + cdata->set_fancy_bit(FB_show_bounds, true); + cdata->set_fancy_bit(FB_show_tight_bounds, ((const ShowBoundsEffect *)effect)->get_tight()); + } } CLOSE_ITERATE_CURRENT_AND_UPSTREAM(_cycler); mark_bam_modified(); @@ -1018,7 +989,14 @@ clear_effect(TypeHandle type) { OPEN_ITERATE_CURRENT_AND_UPSTREAM(_cycler, current_thread) { CDStageWriter cdata(_cycler, pipeline_stage, current_thread); cdata->_effects = cdata->_effects->remove_effect(type); + cdata->set_fancy_bit(FB_effects, !cdata->_effects->is_empty()); + if (type == DecalEffect::get_class_type()) { + cdata->set_fancy_bit(FB_decal, false); + } + else if (type == ShowBoundsEffect::get_class_type()) { + cdata->set_fancy_bit(FB_show_bounds | FB_show_tight_bounds, false); + } } CLOSE_ITERATE_CURRENT_AND_UPSTREAM(_cycler); mark_bam_modified(); @@ -1067,6 +1045,9 @@ set_effects(const RenderEffects *effects, Thread *current_thread) { CDStageWriter cdata(_cycler, pipeline_stage, current_thread); cdata->_effects = effects; cdata->set_fancy_bit(FB_effects, !effects->is_empty()); + cdata->set_fancy_bit(FB_decal, effects->has_decal()); + cdata->set_fancy_bit(FB_show_bounds, effects->has_show_bounds()); + cdata->set_fancy_bit(FB_show_tight_bounds, effects->has_show_tight_bounds()); } CLOSE_ITERATE_CURRENT_AND_UPSTREAM(_cycler); mark_bam_modified(); @@ -1402,7 +1383,8 @@ copy_all_properties(PandaNode *other) { } static const int change_bits = (FB_transform | FB_state | FB_effects | - FB_tag | FB_draw_mask); + FB_tag | FB_draw_mask | FB_decal | + FB_show_bounds | FB_show_tight_bounds); cdataw->_fancy_bits = (cdataw->_fancy_bits & ~change_bits) | (cdatar->_fancy_bits & change_bits); @@ -2536,6 +2518,24 @@ disable_cull_callback() { mark_bam_modified(); } +/** + * Called by a derived class to indicate that there is some value to visiting + * this particular node during the cull traversal for any camera. This will be + * used to optimize the result of get_net_draw_show_mask(), so that any subtrees + * that contain only nodes for which is_renderable() is false need not be + * visited. It also indicates that add_for_draw() should be called if the + * object is determined to be in view of the camera. + */ +void PandaNode:: +set_renderable() { + Thread *current_thread = Thread::get_current_thread(); + OPEN_ITERATE_CURRENT_AND_UPSTREAM(_cycler, current_thread) { + CDStageWriter cdata(_cycler, pipeline_stage, current_thread); + cdata->set_fancy_bit(FB_renderable, true); + } + CLOSE_ITERATE_CURRENT_AND_UPSTREAM(_cycler); +} + /** * The private implementation of remove_child(), for a particular pipeline * stage. @@ -3272,7 +3272,7 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe // Start with a clean slate. CollideMask net_collide_mask = cdata->_into_collide_mask; DrawMask net_draw_control_mask, net_draw_show_mask; - bool renderable = is_renderable(); + bool renderable = (cdata->_fancy_bits & FB_renderable) != 0; if (renderable) { // If this node is itself renderable, it contributes to the net draw @@ -3292,16 +3292,22 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe off_clip_planes = ClipPlaneAttrib::make(); } - // Also get the list of the node's children. - Children children(cdata); - int num_vertices = cdata->_internal_vertices; + // Also get the list of the node's children. When the cdataw destructs, it + // will also release the lock, since we've got all the data we need from the + // node. + PT(Down) down; + { + CDStageWriter cdataw(_cycler, pipeline_stage, cdata); + down = cdataw->modify_down(); + } + // Now that we've got all the data we need from the node, we can release // the lock. - _cycler.release_read_stage(pipeline_stage, cdata.take_pointer()); + //_cycler.release_read_stage(pipeline_stage, cdata.take_pointer()); - int num_children = children.get_num_children(); + int num_children = down->size(); // We need to keep references to the bounding volumes, since in a threaded // environment the pointers might go away while we're working (since we're @@ -3334,7 +3340,8 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe // Now expand those contents to include all of our children. for (int i = 0; i < num_children; ++i) { - PandaNode *child = children.get_child(i); + DownConnection &connection = (*down)[i]; + PandaNode *child = connection.get_child(); const ClipPlaneAttrib *orig_cp = DCAST(ClipPlaneAttrib, off_clip_planes); @@ -3348,7 +3355,9 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe // Child needs update. CDStageWriter child_cdataw = child->update_cached(update_bounds, pipeline_stage, child_cdata); - net_collide_mask |= child_cdataw->_net_collide_mask; + CollideMask child_collide_mask = child_cdataw->_net_collide_mask; + net_collide_mask |= child_collide_mask; + connection._net_collide_mask = child_collide_mask; if (drawmask_cat.is_debug()) { drawmask_cat.debug(false) @@ -3425,11 +3434,18 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe child_volumes[child_volumes_i++] = child_cdataw->_external_bounds; } num_vertices += child_cdataw->_nested_vertices; + + connection._external_bounds = child_cdataw->_external_bounds->as_geometric_bounding_volume(); } + connection._net_draw_control_mask = child_control_mask; + connection._net_draw_show_mask = child_show_mask; + } else { // Child is good. - net_collide_mask |= child_cdata->_net_collide_mask; + CollideMask child_collide_mask = child_cdata->_net_collide_mask; + net_collide_mask |= child_collide_mask; + connection._net_collide_mask = child_collide_mask; // See comments in similar block above. if (drawmask_cat.is_debug()) { @@ -3477,7 +3493,12 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe child_volumes[child_volumes_i++] = child_cdata->_external_bounds; } num_vertices += child_cdata->_nested_vertices; + + connection._external_bounds = child_cdata->_external_bounds->as_geometric_bounding_volume(); } + + connection._net_draw_control_mask = child_control_mask; + connection._net_draw_show_mask = child_show_mask; } } @@ -3865,6 +3886,9 @@ complete_pointers(TypedWritable **p_list, BamReader *manager) { set_fancy_bit(FB_state, !_state->is_empty()); set_fancy_bit(FB_effects, !_effects->is_empty()); set_fancy_bit(FB_tag, !_tag_data.is_empty()); + set_fancy_bit(FB_decal, _effects->has_decal()); + set_fancy_bit(FB_show_bounds, _effects->has_show_bounds()); + set_fancy_bit(FB_show_tight_bounds, _effects->has_show_tight_bounds()); // Mark the bounds stale. ++_next_update; diff --git a/panda/src/pgraph/pandaNode.h b/panda/src/pgraph/pandaNode.h index d4254f315c..d078839b7e 100644 --- a/panda/src/pgraph/pandaNode.h +++ b/panda/src/pgraph/pandaNode.h @@ -46,6 +46,7 @@ #include "lightReMutex.h" #include "extension.h" #include "simpleHashMap.h" +#include "geometricBoundingVolume.h" class NodePathComponent; class CullTraverser; @@ -97,12 +98,9 @@ public: Thread *current_thread = Thread::get_current_thread()) const; virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual bool has_selective_visibility() const; - virtual int get_first_visible_child() const; - virtual int get_next_visible_child(int n) const; virtual bool has_single_child_visibility() const; virtual int get_visible_child() const; - virtual bool is_renderable() const; + virtual bool is_renderable() const final; //CHANGED: see set_renderable() virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data); PUBLISHED: @@ -326,6 +324,10 @@ PUBLISHED: FB_tag = 0x0010, FB_draw_mask = 0x0020, FB_cull_callback = 0x0040, + FB_renderable = 0x0080, + FB_decal = 0x0100, + FB_show_bounds = 0x0200, + FB_show_tight_bounds = 0x0400, }; INLINE int get_fancy_bits(Thread *current_thread = Thread::get_current_thread()) const; @@ -371,6 +373,8 @@ protected: void set_cull_callback(); void disable_cull_callback(); + void set_renderable(); + public: virtual void r_prepare_scene(GraphicsStateGuardianBase *gsg, const RenderState *node_state, @@ -459,11 +463,26 @@ public: INLINE void set_child(PandaNode *child); INLINE int get_sort() const; + INLINE CollideMask get_net_collide_mask() const; + INLINE const GeometricBoundingVolume *get_bounds() const; + + INLINE bool compare_draw_mask(DrawMask running_draw_mask, + DrawMask camera_mask) const; + private: // Child pointers are reference counted. That way, holding a pointer to // the root of a subgraph keeps the entire subgraph around. PT(PandaNode) _child; int _sort; + + // These values are cached here so that a traverser can efficiently check + // whether a node is in view before recursing. + CollideMask _net_collide_mask; + CPT(GeometricBoundingVolume) _external_bounds; + DrawMask _net_draw_control_mask; + DrawMask _net_draw_show_mask; + + friend class PandaNode; }; private: @@ -718,6 +737,10 @@ PUBLISHED: INLINE PandaNode *get_child(size_t n) const; INLINE int get_child_sort(size_t n) const; + INLINE const DownConnection &get_child_connection(size_t n) const { + return (*_down)[n]; + } + PUBLISHED: INLINE PandaNode *operator [](size_t n) const { return get_child(n); } INLINE size_t size() const { return get_num_children(); } @@ -865,6 +888,7 @@ public: INLINE int get_num_children() const; INLINE PandaNode *get_child(int n) const; + INLINE const PandaNode::DownConnection &get_child_connection(int n) const; INLINE int get_child_sort(int n) const; INLINE int find_child(PandaNode *node) const; diff --git a/panda/src/pgraph/planeNode.cxx b/panda/src/pgraph/planeNode.cxx index a54e385e60..dbae5ff960 100644 --- a/panda/src/pgraph/planeNode.cxx +++ b/panda/src/pgraph/planeNode.cxx @@ -65,6 +65,7 @@ PlaneNode(const std::string &name, const LPlane &plane) : _clip_effect(~0) { set_cull_callback(); + set_renderable(); // PlaneNodes are hidden by default. set_overall_hidden(true); @@ -148,17 +149,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { return true; } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool PlaneNode:: -is_renderable() const { - return true; -} - /** * Returns a newly-allocated BoundingVolume that represents the internal * contents of the node. Should be overridden by PandaNode classes that diff --git a/panda/src/pgraph/planeNode.h b/panda/src/pgraph/planeNode.h index 6debd7a217..2a77d68bc2 100644 --- a/panda/src/pgraph/planeNode.h +++ b/panda/src/pgraph/planeNode.h @@ -46,7 +46,6 @@ public: virtual void xform(const LMatrix4 &mat); virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual bool is_renderable() const; PUBLISHED: INLINE void set_plane(const LPlane &plane); diff --git a/panda/src/pgraph/portalNode.cxx b/panda/src/pgraph/portalNode.cxx index c7ad0444fe..09f854b0d9 100644 --- a/panda/src/pgraph/portalNode.cxx +++ b/panda/src/pgraph/portalNode.cxx @@ -48,6 +48,7 @@ PortalNode(const std::string &name) : _flags(0) { set_cull_callback(); + set_renderable(); _visible = false; _open = true; @@ -307,18 +308,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { return true; } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool PortalNode:: -is_renderable() const { - return true; -} - - /** * Writes a brief description of the node to the indicated output stream. * This is invoked by the << operator. It may be overridden in derived diff --git a/panda/src/pgraph/portalNode.h b/panda/src/pgraph/portalNode.h index de78847dab..602dd96139 100644 --- a/panda/src/pgraph/portalNode.h +++ b/panda/src/pgraph/portalNode.h @@ -45,7 +45,6 @@ public: virtual void enable_clipping_planes(); virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual bool is_renderable() const; virtual void output(std::ostream &out) const; diff --git a/panda/src/pgraph/showBoundsEffect.h b/panda/src/pgraph/showBoundsEffect.h index e0f8a7950f..58ed65e1bc 100644 --- a/panda/src/pgraph/showBoundsEffect.h +++ b/panda/src/pgraph/showBoundsEffect.h @@ -25,7 +25,7 @@ class FactoryParams; * this node. This is generally used only during development to help identify * bounding volume issues. */ -class EXPCL_PANDA_PGRAPH ShowBoundsEffect : public RenderEffect { +class EXPCL_PANDA_PGRAPH ShowBoundsEffect final : public RenderEffect { private: INLINE ShowBoundsEffect(); diff --git a/panda/src/pgraph/transformState.I b/panda/src/pgraph/transformState.I index 437c625bc4..908575ea73 100644 --- a/panda/src/pgraph/transformState.I +++ b/panda/src/pgraph/transformState.I @@ -472,6 +472,20 @@ get_mat() const { return _mat; } +/** + * Returns the inverse of the matrix, or nullptr if it's singular. + */ +INLINE const LMatrix4 *TransformState:: +get_inverse_mat() const { + if (!is_singular()) { + nassertr(_inv_mat != nullptr, nullptr); + return _inv_mat; + } + else { + return nullptr; + } +} + /** * Returns the pos component of the 2-d transform. It is an error to call * this if has_pos() or is_2d() returned false. diff --git a/panda/src/pgraph/transformState.h b/panda/src/pgraph/transformState.h index 3e835fcebf..7c34f3ff6e 100644 --- a/panda/src/pgraph/transformState.h +++ b/panda/src/pgraph/transformState.h @@ -142,6 +142,7 @@ PUBLISHED: INLINE PN_stdfloat get_uniform_scale() const; INLINE const LVecBase3 &get_shear() const; INLINE const LMatrix4 &get_mat() const; + INLINE const LMatrix4 *get_inverse_mat() const; INLINE LVecBase2 get_pos2d() const; INLINE PN_stdfloat get_rotate2d() const; diff --git a/panda/src/pgraphnodes/callbackNode.cxx b/panda/src/pgraphnodes/callbackNode.cxx index bf0a233f3c..2e555dfd1e 100644 --- a/panda/src/pgraphnodes/callbackNode.cxx +++ b/panda/src/pgraphnodes/callbackNode.cxx @@ -30,6 +30,7 @@ CallbackNode(const std::string &name) : PandaNode(name) { PandaNode::set_cull_callback(); + PandaNode::set_renderable(); // Set up a default, infinite bounding volume, unless the user tells us // otherwise. Not sure if this is a great idea, because it means a naive @@ -102,17 +103,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { return true; } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool CallbackNode:: -is_renderable() const { - return true; -} - /** * Adds the node's contents to the CullResult we are building up during the * cull traversal, so that it will be drawn at render time. For most nodes diff --git a/panda/src/pgraphnodes/callbackNode.h b/panda/src/pgraphnodes/callbackNode.h index 0a4d101903..f658137ddc 100644 --- a/panda/src/pgraphnodes/callbackNode.h +++ b/panda/src/pgraphnodes/callbackNode.h @@ -44,7 +44,6 @@ public: virtual bool safe_to_combine() const; virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual bool is_renderable() const; virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data); virtual void output(std::ostream &out) const; diff --git a/panda/src/pgraphnodes/computeNode.cxx b/panda/src/pgraphnodes/computeNode.cxx index 560e1a8365..a2f0ca5f15 100644 --- a/panda/src/pgraphnodes/computeNode.cxx +++ b/panda/src/pgraphnodes/computeNode.cxx @@ -32,6 +32,7 @@ ComputeNode(const std::string &name) : _dispatcher(new ComputeNode::Dispatcher) { set_internal_bounds(new OmniBoundingVolume); + set_renderable(); } /** @@ -65,17 +66,6 @@ safe_to_combine() const { return false; } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool ComputeNode:: -is_renderable() const { - return true; -} - /** * Adds the node's contents to the CullResult we are building up during the * cull traversal, so that it will be drawn at render time. For most nodes diff --git a/panda/src/pgraphnodes/computeNode.h b/panda/src/pgraphnodes/computeNode.h index 475aca2355..e1d0f6f073 100644 --- a/panda/src/pgraphnodes/computeNode.h +++ b/panda/src/pgraphnodes/computeNode.h @@ -47,7 +47,6 @@ public: virtual PandaNode *make_copy() const; virtual bool safe_to_combine() const; - virtual bool is_renderable() const; virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data); virtual void output(std::ostream &out) const; diff --git a/panda/src/pgraphnodes/fadeLodNode.cxx b/panda/src/pgraphnodes/fadeLodNode.cxx index f523d01795..265594279b 100644 --- a/panda/src/pgraphnodes/fadeLodNode.cxx +++ b/panda/src/pgraphnodes/fadeLodNode.cxx @@ -164,54 +164,37 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { nassertr(elapsed >= 0.0f && elapsed <= _fade_time, false); + Children children = get_children(); if (elapsed < half_fade_time) { // FIRST HALF OF FADE Fade the new LOD in with z writing off Keep // drawing the old LOD opaque with z writing on - if (out_child >= 0 && out_child < get_num_children()) { - PandaNode *child = get_child(out_child); - if (child != nullptr) { - CullTraverserData next_data_out(data, child); - next_data_out._state = - next_data_out._state->compose(get_fade_1_old_state()); - trav->traverse(next_data_out); - } + if (out_child >= 0 && out_child < children.get_num_children()) { + const PandaNode::DownConnection &child = children.get_child_connection(out_child); + trav->traverse_down(data, child, + data._state->compose(get_fade_1_old_state())); } - if (in_child >= 0 && in_child < get_num_children()) { - PandaNode *child = get_child(in_child); - if (child != nullptr) { - CullTraverserData next_data_in(data, child); - - PN_stdfloat in_alpha = elapsed / half_fade_time; - next_data_in._state = - next_data_in._state->compose(get_fade_1_new_state(in_alpha)); - trav->traverse(next_data_in); - } + if (in_child >= 0 && in_child < children.get_num_children()) { + const PandaNode::DownConnection &child = children.get_child_connection(in_child); + PN_stdfloat in_alpha = elapsed / half_fade_time; + trav->traverse_down(data, child, + data._state->compose(get_fade_1_new_state(in_alpha))); } } else { // SECOND HALF OF FADE: Fade out the old LOD with z write off and // draw the opaque new LOD with z write on - if (in_child >= 0 && in_child < get_num_children()) { - PandaNode *child = get_child(in_child); - if (child != nullptr) { - CullTraverserData next_data_in(data, child); - next_data_in._state = - next_data_in._state->compose(get_fade_2_new_state()); - trav->traverse(next_data_in); - } + if (in_child >= 0 && in_child < children.get_num_children()) { + const PandaNode::DownConnection &child = children.get_child_connection(in_child); + trav->traverse_down(data, child, + data._state->compose(get_fade_2_new_state())); } - if (out_child >= 0 && out_child < get_num_children()) { - PandaNode *child = get_child(out_child); - if (child != nullptr) { - CullTraverserData next_data_out(data, child); - - PN_stdfloat out_alpha = 1.0f - (elapsed - half_fade_time) / half_fade_time; - next_data_out._state = - next_data_out._state->compose(get_fade_2_old_state(out_alpha)); - trav->traverse(next_data_out); - } + if (out_child >= 0 && out_child < children.get_num_children()) { + const PandaNode::DownConnection &child = children.get_child_connection(out_child); + PN_stdfloat out_alpha = 1.0f - (elapsed - half_fade_time) / half_fade_time; + trav->traverse_down(data, child, + data._state->compose(get_fade_2_old_state(out_alpha))); } } } @@ -222,12 +205,10 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { // This is the normal case: we're not in the middle of a transition; we're // just drawing one child of the LOD. int index = ldata->_fade_in; - if (index >= 0 && index < get_num_children()) { - PandaNode *child = get_child(index); - if (child != nullptr) { - CullTraverserData next_data(data, child); - trav->traverse(next_data); - } + Children children = get_children(); + if (index >= 0 && index < children.get_num_children()) { + const PandaNode::DownConnection &child = children.get_child_connection(index); + trav->traverse_down(data, child, data._state); } } diff --git a/panda/src/pgraphnodes/lodNode.cxx b/panda/src/pgraphnodes/lodNode.cxx index 2b66c8fa36..a5574dcaa8 100644 --- a/panda/src/pgraphnodes/lodNode.cxx +++ b/panda/src/pgraphnodes/lodNode.cxx @@ -146,7 +146,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { PN_stdfloat lod_scale = cdata->_lod_scale * trav->get_scene()->get_camera_node()->get_lod_scale(); - int num_children = std::min(get_num_children(), (int)cdata->_switch_vector.size()); + Children children = get_children(); + int num_children = std::min(children.get_num_children(), cdata->_switch_vector.size()); if (data._instances == nullptr || cdata->_got_force_switch) { LPoint3 center = cdata->_center * rel_transform->get_mat(); @@ -163,11 +164,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { if (in_range) { // This switch level is in range. Draw its children. - PandaNode *child = get_child(index); - if (child != nullptr) { - CullTraverserData next_data(data, child); - trav->traverse(next_data); - } + const PandaNode::DownConnection &child = children.get_child_connection(index); + trav->traverse_down(data, child); } } } @@ -193,12 +191,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { CPT(InstanceList) instances = data._instances->without(in_range[index]); if (!instances->empty()) { // At least one instance is visible in this switch level. - PandaNode *child = get_child(index); - if (child != nullptr) { - CullTraverserData next_data(data, child); - next_data._instances = instances; - trav->traverse(next_data); - } + const PandaNode::DownConnection &child = children.get_child_connection(index); + trav->traverse_down(data, child); } } } @@ -435,26 +429,23 @@ show_switches_cull_callback(CullTraverser *trav, CullTraverserData &data) { if (in_range) { // This switch level is in range. Draw its children in the funny // wireframe mode. - if (index < get_num_children()) { - PandaNode *child = get_child(index); - if (child != nullptr) { - CullTraverserData next_data3(data, child); - next_data3._state = next_data3._state->compose(sw.get_viz_model_state()); - trav->traverse(next_data3); - } + Children children = get_children(); + if (index < children.get_num_children()) { + const PandaNode::DownConnection &child = children.get_child_connection(index); + trav->traverse_down(data, child, data._state->compose(sw.get_viz_model_state())); } // And draw the spindle in this color. - CullTraverserData next_data2(data, sw.get_spindle_viz()); - next_data2.apply_transform(viz_transform); - trav->traverse(next_data2); + //CullTraverserData next_data2(data, sw.get_spindle_viz()); + //next_data2.apply_transform(viz_transform); + //trav->traverse(next_data2); } // Draw the rings for this switch level. We do this after we have drawn // the geometry and the spindle. - CullTraverserData next_data(data, sw.get_ring_viz()); - next_data.apply_transform(viz_transform); - trav->traverse(next_data); + //CullTraverserData next_data(data, sw.get_ring_viz()); + //next_data.apply_transform(viz_transform); + //trav->traverse(next_data); } } diff --git a/panda/src/pgraphnodes/nodeCullCallbackData.cxx b/panda/src/pgraphnodes/nodeCullCallbackData.cxx index c1f0ac7464..5a9dbc9e0a 100644 --- a/panda/src/pgraphnodes/nodeCullCallbackData.cxx +++ b/panda/src/pgraphnodes/nodeCullCallbackData.cxx @@ -56,13 +56,6 @@ upcall() { } } - // Now visit all the node's children. - PandaNodePipelineReader *node_reader = _data.node_reader(); - PandaNode::Children children = node_reader->get_children(); - node_reader->release(); - int num_children = children.get_num_children(); - for (int i = 0; i < num_children; ++i) { - CullTraverserData next_data(_data, children.get_child(i)); - _trav->traverse(next_data); - } + // Now traverse below. + _trav->traverse_below(_data); } diff --git a/panda/src/pgraphnodes/selectiveChildNode.I b/panda/src/pgraphnodes/selectiveChildNode.I index beafc0669d..46b40ba3a7 100644 --- a/panda/src/pgraphnodes/selectiveChildNode.I +++ b/panda/src/pgraphnodes/selectiveChildNode.I @@ -16,8 +16,7 @@ */ INLINE SelectiveChildNode:: SelectiveChildNode(const std::string &name) : - PandaNode(name), - _selected_child(0) + PandaNode(name) { } @@ -26,17 +25,6 @@ SelectiveChildNode(const std::string &name) : */ INLINE SelectiveChildNode:: SelectiveChildNode(const SelectiveChildNode ©) : - PandaNode(copy), - _selected_child(copy._selected_child) + PandaNode(copy) { } - -/** - * Selects the indicated child for rendering. This is normally called during - * the cull_callback() method, but it may be called at any time. - */ -INLINE void SelectiveChildNode:: -select_child(int n) { - nassertv(n >= 0); - _selected_child = n; -} diff --git a/panda/src/pgraphnodes/selectiveChildNode.cxx b/panda/src/pgraphnodes/selectiveChildNode.cxx index 0fb38560aa..cfe5e258b4 100644 --- a/panda/src/pgraphnodes/selectiveChildNode.cxx +++ b/panda/src/pgraphnodes/selectiveChildNode.cxx @@ -14,44 +14,3 @@ #include "selectiveChildNode.h" TypeHandle SelectiveChildNode::_type_handle; - - -/** - * Should be overridden by derived classes to return true if this kind of node - * has some restrictions on the set of children that should be rendered. Node - * with this property include LODNodes, SwitchNodes, and SequenceNodes. - * - * If this function returns true, get_first_visible_child() and - * get_next_visible_child() will be called to walk through the list of - * children during cull, instead of iterating through the entire list. This - * method is called after cull_callback(), so cull_callback() may be - * responsible for the decisions as to which children are visible at the - * moment. - */ -bool SelectiveChildNode:: -has_selective_visibility() const { - return true; -} - -/** - * Returns the index number of the first visible child of this node, or a - * number >= get_num_children() if there are no visible children of this node. - * This is called during the cull traversal, but only if - * has_selective_visibility() has already returned true. See - * has_selective_visibility(). - */ -int SelectiveChildNode:: -get_first_visible_child() const { - return _selected_child; -} - -/** - * Returns the index number of the next visible child of this node following - * the indicated child, or a number >= get_num_children() if there are no more - * visible children of this node. See has_selective_visibility() and - * get_first_visible_child(). - */ -int SelectiveChildNode:: -get_next_visible_child(int n) const { - return get_num_children(); -} diff --git a/panda/src/pgraphnodes/selectiveChildNode.h b/panda/src/pgraphnodes/selectiveChildNode.h index 29b0d4581a..aa23a49f1d 100644 --- a/panda/src/pgraphnodes/selectiveChildNode.h +++ b/panda/src/pgraphnodes/selectiveChildNode.h @@ -21,6 +21,8 @@ /** * A base class for nodes like LODNode and SequenceNode that select only one * visible child at a time. + * + * This class is now vestigial. */ class EXPCL_PANDA_PGRAPHNODES SelectiveChildNode : public PandaNode { PUBLISHED: @@ -29,21 +31,6 @@ PUBLISHED: protected: INLINE SelectiveChildNode(const SelectiveChildNode ©); -public: - virtual bool has_selective_visibility() const; - virtual int get_first_visible_child() const; - virtual int get_next_visible_child(int n) const; - -protected: - INLINE void select_child(int n); - -private: - // Not sure if this should be cycled or not. It's not exactly thread-safe - // not to cycle it, but it doesn't really need the full pipeline control. - // It's probably a problem in the non-thread-safe design; need to rethink - // the design a bit. - int _selected_child; - public: static TypeHandle get_class_type() { return _type_handle; diff --git a/panda/src/pgraphnodes/sequenceNode.cxx b/panda/src/pgraphnodes/sequenceNode.cxx index 06f0019e6d..48614edbcd 100644 --- a/panda/src/pgraphnodes/sequenceNode.cxx +++ b/panda/src/pgraphnodes/sequenceNode.cxx @@ -88,21 +88,10 @@ make_copy() const { * should be culled. */ bool SequenceNode:: -cull_callback(CullTraverser *, CullTraverserData &) { - select_child(get_frame()); - return true; -} - -/** - * Returns the index number of the first visible child of this node, or a - * number >= get_num_children() if there are no visible children of this node. - * This is called during the cull traversal, but only if - * has_selective_visibility() has already returned true. See - * has_selective_visibility(). - */ -int SequenceNode:: -get_first_visible_child() const { - return get_frame(); +cull_callback(CullTraverser *trav, CullTraverserData &data) { + const PandaNode::DownConnection &child = data.node_reader()->get_child_connection(get_frame()); + trav->traverse_down(data, child); + return false; } /** diff --git a/panda/src/pgraphnodes/sequenceNode.h b/panda/src/pgraphnodes/sequenceNode.h index 96b7698131..ba67bbf18c 100644 --- a/panda/src/pgraphnodes/sequenceNode.h +++ b/panda/src/pgraphnodes/sequenceNode.h @@ -43,7 +43,6 @@ public: virtual bool safe_to_combine_children() const; virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual int get_first_visible_child() const; virtual bool has_single_child_visibility() const; virtual int get_visible_child() const; diff --git a/panda/src/pgraphnodes/switchNode.cxx b/panda/src/pgraphnodes/switchNode.cxx index d16afc56c4..1e3430cb2e 100644 --- a/panda/src/pgraphnodes/switchNode.cxx +++ b/panda/src/pgraphnodes/switchNode.cxx @@ -103,21 +103,10 @@ make_copy() const { * should be culled. */ bool SwitchNode:: -cull_callback(CullTraverser *, CullTraverserData &) { - select_child(get_visible_child()); - return true; -} - -/** - * Returns the index number of the first visible child of this node, or a - * number >= get_num_children() if there are no visible children of this node. - * This is called during the cull traversal, but only if - * has_selective_visibility() has already returned true. See - * has_selective_visibility(). - */ -int SwitchNode:: -get_first_visible_child() const { - return get_visible_child(); +cull_callback(CullTraverser *trav, CullTraverserData &data) { + const PandaNode::DownConnection &child = data.node_reader()->get_child_connection(get_visible_child()); + trav->traverse_down(data, child); + return false; } /** diff --git a/panda/src/pgraphnodes/switchNode.h b/panda/src/pgraphnodes/switchNode.h index 2df989cbd7..1fe3353a68 100644 --- a/panda/src/pgraphnodes/switchNode.h +++ b/panda/src/pgraphnodes/switchNode.h @@ -34,7 +34,6 @@ public: virtual bool safe_to_combine_children() const; virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual int get_first_visible_child() const; virtual bool has_single_child_visibility() const; PUBLISHED: diff --git a/panda/src/pgui/pgEntry.cxx b/panda/src/pgui/pgEntry.cxx index 2fc40d546a..5922ac7f50 100644 --- a/panda/src/pgui/pgEntry.cxx +++ b/panda/src/pgui/pgEntry.cxx @@ -184,8 +184,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { } // Now render the text. - CullTraverserData next_data(data, _text_render_root.node()); - trav->traverse(next_data); + trav->traverse_down(data, _text_render_root.node()); // Now continue to render everything else below this node. return true; diff --git a/panda/src/pgui/pgItem.cxx b/panda/src/pgui/pgItem.cxx index 09a3fbcaac..8229f434dc 100644 --- a/panda/src/pgui/pgItem.cxx +++ b/panda/src/pgui/pgItem.cxx @@ -67,6 +67,7 @@ PGItem(const string &name) : _flags(0) { set_cull_callback(); + set_renderable(); } /** @@ -278,25 +279,13 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { if (state_def_root != nullptr) { // This item has a current state definition that we should use to render // the item. - CullTraverserData next_data(data, state_def_root); - trav->traverse(next_data); + trav->traverse_down(data, state_def_root); } // Now continue to render everything else below this node. return true; } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool PGItem:: -is_renderable() const { - return true; -} - /** * Called when needed to recompute the node's _internal_bound object. Nodes * that contain anything of substance should redefine this to do the right diff --git a/panda/src/pgui/pgItem.h b/panda/src/pgui/pgItem.h index 5467029d52..a27c08d46c 100644 --- a/panda/src/pgui/pgItem.h +++ b/panda/src/pgui/pgItem.h @@ -65,7 +65,6 @@ protected: virtual void draw_mask_changed(); virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual bool is_renderable() const; virtual void compute_internal_bounds(CPT(BoundingVolume) &internal_bounds, int &internal_vertices, diff --git a/panda/src/pgui/pgTop.cxx b/panda/src/pgui/pgTop.cxx index 3c0948ebda..f4d9f31fda 100644 --- a/panda/src/pgui/pgTop.cxx +++ b/panda/src/pgui/pgTop.cxx @@ -29,6 +29,10 @@ PGTop(const std::string &name) : { set_cull_callback(); + // We flag the PGTop as renderable, even though it technically doesn't have + // anything to render, but we do need the traverser to visit it every frame. + set_renderable(); + _start_sort = 0; // A PGTop node normally has an infinite bounding volume. Screw culling. @@ -111,19 +115,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { return false; } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool PGTop:: -is_renderable() const { - // We flag the PGTop as renderable, even though it technically doesn't have - // anything to render, but we do need the traverser to visit it every frame. - return true; -} - /** * Sets the MouseWatcher pointer that the PGTop object registers its PG items * with. This must be set before the PG items are active. diff --git a/panda/src/pgui/pgTop.h b/panda/src/pgui/pgTop.h index 1d5e739c8e..f5e6bac554 100644 --- a/panda/src/pgui/pgTop.h +++ b/panda/src/pgui/pgTop.h @@ -46,7 +46,6 @@ protected: public: virtual PandaNode *make_copy() const; virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual bool is_renderable() const; PUBLISHED: void set_mouse_watcher(MouseWatcher *watcher); diff --git a/panda/src/pipeline/cycleDataLockedStageReader.I b/panda/src/pipeline/cycleDataLockedStageReader.I index a471aac0b7..6cee154bb7 100644 --- a/panda/src/pipeline/cycleDataLockedStageReader.I +++ b/panda/src/pipeline/cycleDataLockedStageReader.I @@ -111,7 +111,9 @@ INLINE CycleDataLockedStageReader:: template INLINE const CycleDataType *CycleDataLockedStageReader:: operator -> () const { +#ifdef _DEBUG nassertr(_pointer != nullptr, _cycler->cheat()); +#endif return _pointer; } @@ -122,7 +124,9 @@ operator -> () const { template INLINE CycleDataLockedStageReader:: operator const CycleDataType * () const { +#ifdef _DEBUG nassertr(_pointer != nullptr, _cycler->cheat()); +#endif return _pointer; } diff --git a/panda/src/text/textNode.cxx b/panda/src/text/textNode.cxx index be904d7972..0a8d666432 100644 --- a/panda/src/text/textNode.cxx +++ b/panda/src/text/textNode.cxx @@ -61,6 +61,7 @@ PStatCollector TextNode::_text_generate_pcollector("*:Generate Text"); TextNode:: TextNode(const string &name) : PandaNode(name) { set_cull_callback(); + set_renderable(); if (text_small_caps) { TextProperties::set_small_caps(true); @@ -487,25 +488,13 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { PT(PandaNode) internal_geom = do_get_internal_geom(); if (internal_geom != nullptr) { // Render the text with this node. - CullTraverserData next_data(data, internal_geom); - trav->traverse(next_data); + trav->traverse_down(data, internal_geom); } // Now continue to render everything else below this node. return true; } -/** - * Returns true if there is some value to visiting this particular node during - * the cull traversal for any camera, false otherwise. This will be used to - * optimize the result of get_net_draw_show_mask(), so that any subtrees that - * contain only nodes for which is_renderable() is false need not be visited. - */ -bool TextNode:: -is_renderable() const { - return true; -} - /** * Called when needed to recompute the node's _internal_bound object. Nodes * that contain anything of substance should redefine this to do the right diff --git a/panda/src/text/textNode.h b/panda/src/text/textNode.h index 3ace43895e..e40f1c9030 100644 --- a/panda/src/text/textNode.h +++ b/panda/src/text/textNode.h @@ -286,7 +286,6 @@ public: Thread *current_thread) const; virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data); - virtual bool is_renderable() const; virtual void compute_internal_bounds(CPT(BoundingVolume) &internal_bounds, int &internal_vertices, From 60c55896718c9ed596632b06b0fb303d67667db2 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 16 Nov 2021 14:04:54 +0100 Subject: [PATCH 33/68] pgraph: Optimize handling/checking of identity/invalid transforms --- panda/src/pgraph/transformState.I | 29 +++- panda/src/pgraph/transformState.cxx | 241 ++++++++++++++-------------- panda/src/pgraph/transformState.h | 10 +- tests/pgraph/test_transforms.py | 68 ++++++++ 4 files changed, 221 insertions(+), 127 deletions(-) create mode 100644 tests/pgraph/test_transforms.py diff --git a/panda/src/pgraph/transformState.I b/panda/src/pgraph/transformState.I index 908575ea73..6876d4c2a0 100644 --- a/panda/src/pgraph/transformState.I +++ b/panda/src/pgraph/transformState.I @@ -40,6 +40,31 @@ get_hash() const { return _hash; } +/** + * Constructs an identity transform. + */ +INLINE CPT(TransformState) TransformState:: +make_identity() { + if (UNLIKELY(_states_lock == nullptr)) { + init_states(); + } + + return _identity_state; +} + +/** + * Constructs an invalid transform; for instance, the result of inverting a + * singular matrix. + */ +INLINE CPT(TransformState) TransformState:: +make_invalid() { + if (UNLIKELY(_states_lock == nullptr)) { + init_states(); + } + + return _invalid_state; +} + /** * Makes a new TransformState with the specified components. */ @@ -204,7 +229,7 @@ make_pos_rotate_scale2d(const LVecBase2 &pos, PN_stdfloat rotate, */ INLINE bool TransformState:: is_identity() const { - return ((_flags & F_is_identity) != 0); + return this == _identity_state; } /** @@ -214,7 +239,7 @@ is_identity() const { */ INLINE bool TransformState:: is_invalid() const { - return ((_flags & F_is_invalid) != 0); + return this == _invalid_state; } /** diff --git a/panda/src/pgraph/transformState.cxx b/panda/src/pgraph/transformState.cxx index aac37c306b..c80b88e677 100644 --- a/panda/src/pgraph/transformState.cxx +++ b/panda/src/pgraph/transformState.cxx @@ -51,18 +51,17 @@ CacheStats TransformState::_cache_stats; TypeHandle TransformState::_type_handle; /** - * Actually, this could be a private constructor, since no one inherits from - * TransformState, but gcc gives us a spurious warning if all constructors are - * private. + * */ TransformState:: -TransformState() : _lock("TransformState") { +TransformState() : + _flags(F_is_identity | F_singular_known | F_is_2d), + _lock("TransformState") { + if (_states_lock == nullptr) { init_states(); } - _saved_entry = -1; - _flags = F_is_identity | F_singular_known | F_is_2d; - _inv_mat = nullptr; + _cache_stats.add_num_states(1); #ifdef DO_MEMORY_USAGE @@ -232,43 +231,13 @@ operator == (const TransformState &other) const { } } -/** - * Constructs an identity transform. - */ -CPT(TransformState) TransformState:: -make_identity() { - // The identity state is asked for so often, we make it a special case and - // store a pointer forever once we find it the first time. - if (_identity_state == nullptr) { - TransformState *state = new TransformState; - _identity_state = return_unique(state); - } - - return _identity_state; -} - -/** - * Constructs an invalid transform; for instance, the result of inverting a - * singular matrix. - */ -CPT(TransformState) TransformState:: -make_invalid() { - if (_invalid_state == nullptr) { - TransformState *state = new TransformState; - state->_flags = F_is_invalid | F_singular_known | F_is_singular | F_components_known | F_mat_known; - _invalid_state = return_unique(state); - } - - return _invalid_state; -} - /** * Makes a new TransformState with the specified components. */ CPT(TransformState) TransformState:: make_pos_hpr_scale_shear(const LVecBase3 &pos, const LVecBase3 &hpr, const LVecBase3 &scale, const LVecBase3 &shear) { - nassertr(!(pos.is_nan() || hpr.is_nan() || scale.is_nan() || shear.is_nan()) , make_invalid()); + nassertr(!(pos.is_nan() || hpr.is_nan() || scale.is_nan() || shear.is_nan()), make_invalid()); // Make a special-case check for the identity transform. if (pos == LVecBase3(0.0f, 0.0f, 0.0f) && hpr == LVecBase3(0.0f, 0.0f, 0.0f) && @@ -293,7 +262,7 @@ make_pos_hpr_scale_shear(const LVecBase3 &pos, const LVecBase3 &hpr, CPT(TransformState) TransformState:: make_pos_quat_scale_shear(const LVecBase3 &pos, const LQuaternion &quat, const LVecBase3 &scale, const LVecBase3 &shear) { - nassertr(!(pos.is_nan() || quat.is_nan() || scale.is_nan() || shear.is_nan()) , make_invalid()); + nassertr(!(pos.is_nan() || quat.is_nan() || scale.is_nan() || shear.is_nan()), make_invalid()); // Make a special-case check for the identity transform. if (pos == LVecBase3(0.0f, 0.0f, 0.0f) && quat == LQuaternion::ident_quat() && @@ -336,7 +305,7 @@ CPT(TransformState) TransformState:: make_pos_rotate_scale_shear2d(const LVecBase2 &pos, PN_stdfloat rotate, const LVecBase2 &scale, PN_stdfloat shear) { - nassertr(!(pos.is_nan() || cnan(rotate) || scale.is_nan() || cnan(shear)) , make_invalid()); + nassertr(!(pos.is_nan() || cnan(rotate) || scale.is_nan() || cnan(shear)), make_invalid()); // Make a special-case check for the identity transform. if (pos == LVecBase2(0.0f, 0.0f) && rotate == 0.0f && @@ -700,7 +669,7 @@ invert_compose(const TransformState *other) const { if (other == this) { // a->invert_compose(a) always produces identity. - return make_identity(); + return _identity_state; } if (!transform_cache) { @@ -1421,6 +1390,40 @@ init_states() { _states_lock = new LightReMutex("TransformState::_states_lock"); _cache_stats.init(); nassertv(Thread::get_current_thread() == Thread::get_main_thread()); + + // The identity and invalid states are asked for so often, we make them a + // special case and store a pointer forever. + { + TransformState *state = new TransformState; + state->_pos.set(0.0f, 0.0f, 0.0f); + state->_scale.set(1.0f, 1.0f, 1.0f); + state->_shear.set(0.0f, 0.0f, 0.0f); + state->_hpr.set(0.0f, 0.0f, 0.0f); + state->_quat.set(1.0f, 0.0f, 0.0f, 0.0f); + state->_norm_quat.set(1.0f, 0.0f, 0.0f, 0.0f); + state->_mat.set(1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f); + state->_inv_mat = new LMatrix4(state->_mat); + state->_hash = int_hash::add_hash(0, F_is_invalid | F_is_identity | F_is_2d); + state->_flags = F_is_identity | F_singular_known | F_components_known + | F_has_components | F_mat_known | F_quat_known | F_hpr_known + | F_uniform_scale | F_identity_scale | F_is_2d | F_hash_known + | F_norm_quat_known; + state->cache_ref(); + state->_saved_entry = _states.store(state, nullptr); + _identity_state = state; + } + { + TransformState *state = new TransformState; + state->_hash = int_hash::add_hash(0, F_is_invalid); + state->_flags = F_is_singular | F_singular_known | F_components_known + | F_mat_known | F_is_invalid | F_hash_known; + state->cache_ref(); + state->_saved_entry = _states.store(state, nullptr); + _invalid_state = state; + } } /** @@ -1604,7 +1607,7 @@ do_invert_compose(const TransformState *other) const { // First, invert our own transform. if (scale == 0.0f) { ((TransformState *)this)->_flags |= F_is_singular | F_singular_known; - return make_invalid(); + return _invalid_state; } scale = 1.0f / scale; quat.invert_in_place(); @@ -1633,7 +1636,7 @@ do_invert_compose(const TransformState *other) const { // First, invert our own transform. if (scale == 0.0f) { ((TransformState *)this)->_flags |= F_is_singular | F_singular_known; - return make_invalid(); + return _invalid_state; } scale = 1.0f / scale; quat.invert_in_place(); @@ -1657,7 +1660,7 @@ do_invert_compose(const TransformState *other) const { pgraph_cat.warning() << "Unexpected singular matrix found for " << *this << "\n"; } else { - nassertr(_inv_mat != nullptr, make_invalid()); + nassertr(_inv_mat != nullptr, _invalid_state); LMatrix4 new_mat; new_mat.multiply(other->get_mat(), *_inv_mat); if (!new_mat.almost_equal(result->get_mat(), 0.1)) { @@ -1676,12 +1679,12 @@ do_invert_compose(const TransformState *other) const { } if (is_singular()) { - return make_invalid(); + return _invalid_state; } // Now that is_singular() has returned false, we can assume that _inv_mat // has been allocated and filled in. - nassertr(_inv_mat != nullptr, make_invalid()); + nassertr(_inv_mat != nullptr, _invalid_state); if (is_2d() && other->is_2d()) { const LMatrix4 &i = *_inv_mat; @@ -2009,40 +2012,40 @@ do_calc_hash() { int flags = (_flags & significant_flags); _hash = int_hash::add_hash(_hash, flags); - if ((_flags & (F_is_invalid | F_is_identity)) == 0) { - // Only bother to put the rest of the stuff in the hash if the transform - // is not invalid or empty. + nassertv((flags & (F_is_invalid | F_is_identity)) == 0); - if ((_flags & F_components_given) != 0) { - // If the transform was specified componentwise, hash it componentwise. - _hash = _pos.add_hash(_hash); - if ((_flags & F_hpr_given) != 0) { - _hash = _hpr.add_hash(_hash); + // Only bother to put the rest of the stuff in the hash if the transform + // is not invalid or empty. - } else if ((_flags & F_quat_given) != 0) { - _hash = _quat.add_hash(_hash); + if ((_flags & F_components_given) != 0) { + // If the transform was specified componentwise, hash it componentwise. + _hash = _pos.add_hash(_hash); + if ((_flags & F_hpr_given) != 0) { + _hash = _hpr.add_hash(_hash); + + } else if ((_flags & F_quat_given) != 0) { + _hash = _quat.add_hash(_hash); + } + + _hash = _scale.add_hash(_hash); + _hash = _shear.add_hash(_hash); + + } else { + // Otherwise, hash the matrix . . . + if (_uniquify_matrix) { + // . . . but only if the user thinks that's worthwhile. + if ((_flags & F_mat_known) == 0) { + // Calculate the matrix without doubly-locking. + do_calc_mat(); } - - _hash = _scale.add_hash(_hash); - _hash = _shear.add_hash(_hash); + _hash = _mat.add_hash(_hash); } else { - // Otherwise, hash the matrix . . . - if (_uniquify_matrix) { - // . . . but only if the user thinks that's worthwhile. - if ((_flags & F_mat_known) == 0) { - // Calculate the matrix without doubly-locking. - do_calc_mat(); - } - _hash = _mat.add_hash(_hash); + // Otherwise, hash the pointer only--any two different matrix-based + // TransformStates are considered to be different, even if their + // matrices have the same values. - } else { - // Otherwise, hash the pointer only--any two different matrix-based - // TransformStates are considered to be different, even if their - // matrices have the same values. - - _hash = pointer_hash::add_hash(_hash, this); - } + _hash = pointer_hash::add_hash(_hash, this); } } @@ -2063,7 +2066,7 @@ calc_singular() { PStatTimer timer(_transform_calc_pcollector); - nassertv((_flags & F_is_invalid) == 0); + nassertv((_flags & (F_is_invalid | F_is_identity)) == 0); // We determine if a matrix is singular by attempting to invert it (and we // save the result of this invert operation for a subsequent @@ -2100,39 +2103,30 @@ do_calc_components() { PStatTimer timer(_transform_calc_pcollector); - nassertv((_flags & F_is_invalid) == 0); - if ((_flags & F_is_identity) != 0) { - _scale.set(1.0f, 1.0f, 1.0f); - _shear.set(0.0f, 0.0f, 0.0f); - _hpr.set(0.0f, 0.0f, 0.0f); - _quat = LQuaternion::ident_quat(); - _pos.set(0.0f, 0.0f, 0.0f); - _flags |= F_has_components | F_components_known | F_hpr_known | F_quat_known | F_uniform_scale | F_identity_scale; + nassertv((_flags & (F_is_invalid | F_is_identity)) == 0); + + // If we don't have components and we're not identity, the only other + // explanation is that we were constructed via a matrix. + nassertv((_flags & F_mat_known) != 0); + + if ((_flags & F_mat_known) == 0) { + do_calc_mat(); + } + bool possible = decompose_matrix(_mat, _scale, _shear, _hpr, _pos); + if (!possible) { + // Some matrices can't be decomposed into scale, hpr, pos. In this + // case, we now know that we cannot compute the components; but the + // closest approximations are stored, at least. + _flags |= F_components_known | F_hpr_known; } else { - // If we don't have components and we're not identity, the only other - // explanation is that we were constructed via a matrix. - nassertv((_flags & F_mat_known) != 0); - - if ((_flags & F_mat_known) == 0) { - do_calc_mat(); - } - bool possible = decompose_matrix(_mat, _scale, _shear, _hpr, _pos); - if (!possible) { - // Some matrices can't be decomposed into scale, hpr, pos. In this - // case, we now know that we cannot compute the components; but the - // closest approximations are stored, at least. - _flags |= F_components_known | F_hpr_known; - - } else { - // Otherwise, we do have the components, or at least the hpr. - _flags |= F_has_components | F_components_known | F_hpr_known; - check_uniform_scale(); - } - - // However, we can always get at least the pos. - _mat.get_row3(_pos, 3); + // Otherwise, we do have the components, or at least the hpr. + _flags |= F_has_components | F_components_known | F_hpr_known; + check_uniform_scale(); } + + // However, we can always get at least the pos. + _mat.get_row3(_pos, 3); } /** @@ -2148,7 +2142,7 @@ do_calc_hpr() { PStatTimer timer(_transform_calc_pcollector); - nassertv((_flags & F_is_invalid) == 0); + nassertv((_flags & (F_is_invalid | F_is_identity)) == 0); if ((_flags & F_components_known) == 0) { do_calc_components(); } @@ -2174,7 +2168,7 @@ calc_quat() { PStatTimer timer(_transform_calc_pcollector); - nassertv((_flags & F_is_invalid) == 0); + nassertv((_flags & (F_is_invalid | F_is_identity)) == 0); if ((_flags & F_components_known) == 0) { do_calc_components(); } @@ -2214,20 +2208,16 @@ do_calc_mat() { PStatTimer timer(_transform_calc_pcollector); - nassertv((_flags & F_is_invalid) == 0); - if ((_flags & F_is_identity) != 0) { - _mat = LMatrix4::ident_mat(); + nassertv((_flags & (F_is_invalid | F_is_identity)) == 0); - } else { - // If we don't have a matrix and we're not identity, the only other - // explanation is that we were constructed via components. - nassertv((_flags & F_components_known) != 0); - if ((_flags & F_hpr_known) == 0) { - do_calc_hpr(); - } - - compose_matrix(_mat, _scale, _shear, get_hpr(), _pos); + // If we don't have a matrix and we're not identity, the only other + // explanation is that we were constructed via components. + nassertv((_flags & F_components_known) != 0); + if ((_flags & F_hpr_known) == 0) { + do_calc_hpr(); } + + compose_matrix(_mat, _scale, _shear, get_hpr(), _pos); _flags |= F_mat_known; } @@ -2341,7 +2331,18 @@ make_from_bam(const FactoryParams ¶ms) { parse_params(params, scan, manager); state->fillin(scan, manager); - manager->register_change_this(change_this, state); + + if (state->_flags & F_is_identity) { + delete state; + return (TypedWritable *)_identity_state.p(); + } + else if (state->_flags & F_is_invalid) { + delete state; + return (TypedWritable *)_invalid_state.p(); + } + else { + manager->register_change_this(change_this, state); + } return state; } diff --git a/panda/src/pgraph/transformState.h b/panda/src/pgraph/transformState.h index 7c34f3ff6e..60d377c06e 100644 --- a/panda/src/pgraph/transformState.h +++ b/panda/src/pgraph/transformState.h @@ -52,7 +52,7 @@ class FactoryParams; * And instead of modifying a TransformState object, create a new one. */ class EXPCL_PANDA_PGRAPH TransformState final : public NodeCachedReferenceCount { -protected: +private: TransformState(); public: @@ -69,8 +69,8 @@ PUBLISHED: bool operator == (const TransformState &other) const; INLINE size_t get_hash() const; - static CPT(TransformState) make_identity(); - static CPT(TransformState) make_invalid(); + INLINE static CPT(TransformState) make_identity(); + INLINE static CPT(TransformState) make_invalid(); INLINE static CPT(TransformState) make_pos(const LVecBase3 &pos); INLINE static CPT(TransformState) make_hpr(const LVecBase3 &hpr); INLINE static CPT(TransformState) make_quat(const LQuaternion &quat); @@ -268,7 +268,7 @@ private: // This iterator records the entry corresponding to this TransformState // object in the above global set. We keep the index around so we can // remove it when the TransformState destructs. - int _saved_entry; + int _saved_entry = -1; // This data structure manages the job of caching the composition of two // TransformStates. It's complicated because we have to be sure to remove @@ -372,7 +372,7 @@ private: LVecBase3 _hpr, _scale, _shear; LQuaternion _quat, _norm_quat; LMatrix4 _mat; - LMatrix4 *_inv_mat; + LMatrix4 *_inv_mat = nullptr; size_t _hash; unsigned int _flags; diff --git a/tests/pgraph/test_transforms.py b/tests/pgraph/test_transforms.py new file mode 100644 index 0000000000..d4ac4d2fdb --- /dev/null +++ b/tests/pgraph/test_transforms.py @@ -0,0 +1,68 @@ +from panda3d.core import TransformState, Mat4, Mat3 + + +def test_transform_identity(): + state = TransformState.make_identity() + + assert state.is_identity() + assert not state.is_invalid() + assert not state.is_singular() + assert state.is_2d() + + assert state.has_components() + assert not state.components_given() + assert not state.hpr_given() + assert not state.quat_given() + assert state.has_pos() + assert state.has_hpr() + assert state.has_quat() + assert state.has_scale() + assert state.has_identity_scale() + assert state.has_uniform_scale() + assert state.has_shear() + assert not state.has_nonzero_shear() + assert state.has_mat() + + assert state.get_pos() == (0, 0, 0) + assert state.get_hpr() == (0, 0, 0) + assert state.get_quat() == (1, 0, 0, 0) + assert state.get_norm_quat() == (1, 0, 0, 0) + assert state.get_scale() == (1, 1, 1) + assert state.get_uniform_scale() == 1 + assert state.get_shear() == (0, 0, 0) + assert state.get_mat() == Mat4.ident_mat() + + assert state.get_pos2d() == (0, 0) + assert state.get_rotate2d() == 0 + assert state.get_scale2d() == (1, 1) + assert state.get_shear2d() == 0 + assert state.get_mat3() == Mat3.ident_mat() + + state2 = TransformState.make_identity() + assert state.this == state2.this + + +def test_transform_invalid(): + state = TransformState.make_invalid() + + assert not state.is_identity() + assert state.is_invalid() + assert state.is_singular() + assert not state.is_2d() + + assert not state.has_components() + assert not state.components_given() + assert not state.hpr_given() + assert not state.quat_given() + assert not state.has_pos() + assert not state.has_hpr() + assert not state.has_quat() + assert not state.has_scale() + assert not state.has_identity_scale() + assert not state.has_uniform_scale() + assert not state.has_shear() + assert not state.has_nonzero_shear() + assert not state.has_mat() + + state2 = TransformState.make_invalid() + assert state.this == state2.this From 9ebfef4b8304637d9d632d455db2eb02ff259d1a Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 16 Nov 2021 14:48:17 +0100 Subject: [PATCH 34/68] pgraph: Change TransformState hashing not to require grabbing lock Instead, set it atomically, with a special hash value used instead of a flag for indicating that the hash is not known. --- panda/src/pgraph/transformState.I | 13 +------ panda/src/pgraph/transformState.cxx | 59 ++++++++++++++++------------- panda/src/pgraph/transformState.h | 12 ++++-- 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/panda/src/pgraph/transformState.I b/panda/src/pgraph/transformState.I index 6876d4c2a0..6097430e91 100644 --- a/panda/src/pgraph/transformState.I +++ b/panda/src/pgraph/transformState.I @@ -856,8 +856,8 @@ INLINE void TransformState:: check_hash() const { // This pretends to be a const function, even though it's not, because it // only updates a transparent cache value. - if ((_flags & F_hash_known) == 0) { - ((TransformState *)this)->calc_hash(); + if (_hash == H_unknown) { + calc_hash(); } } @@ -937,15 +937,6 @@ check_mat() const { } } -/** - * Computes the hash value. - */ -INLINE void TransformState:: -calc_hash() { - LightMutexHolder holder(_lock); - do_calc_hash(); -} - /** * Derives the components from the matrix, if possible. */ diff --git a/panda/src/pgraph/transformState.cxx b/panda/src/pgraph/transformState.cxx index c80b88e677..f02e77203a 100644 --- a/panda/src/pgraph/transformState.cxx +++ b/panda/src/pgraph/transformState.cxx @@ -1406,10 +1406,10 @@ init_states() { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f); state->_inv_mat = new LMatrix4(state->_mat); - state->_hash = int_hash::add_hash(0, F_is_invalid | F_is_identity | F_is_2d); + state->_hash = H_identity; state->_flags = F_is_identity | F_singular_known | F_components_known | F_has_components | F_mat_known | F_quat_known | F_hpr_known - | F_uniform_scale | F_identity_scale | F_is_2d | F_hash_known + | F_uniform_scale | F_identity_scale | F_is_2d | F_norm_quat_known; state->cache_ref(); state->_saved_entry = _states.store(state, nullptr); @@ -1417,9 +1417,9 @@ init_states() { } { TransformState *state = new TransformState; - state->_hash = int_hash::add_hash(0, F_is_invalid); - state->_flags = F_is_singular | F_singular_known | F_components_known - | F_mat_known | F_is_invalid | F_hash_known; + state->_hash = H_invalid; + state->_flags = F_is_singular | F_singular_known | F_components_known + | F_mat_known | F_is_invalid; state->cache_ref(); state->_saved_entry = _states.store(state, nullptr); _invalid_state = state; @@ -2002,15 +2002,18 @@ remove_cache_pointers() { * Computes a suitable hash value for phash_map. */ void TransformState:: -do_calc_hash() { +calc_hash() const { + // It's OK not to grab the lock here, because (1) this does not depend on + // cached values (only given components are considered), and (2) the hash + // itself is set atomically. PStatTimer timer(_transform_hash_pcollector); - _hash = 0; + AtomicAdjust::Integer hash = 0; static const int significant_flags = (F_is_invalid | F_is_identity | F_components_given | F_hpr_given | F_is_2d); int flags = (_flags & significant_flags); - _hash = int_hash::add_hash(_hash, flags); + hash = int_hash::add_hash(hash, flags); nassertv((flags & (F_is_invalid | F_is_identity)) == 0); @@ -2019,37 +2022,41 @@ do_calc_hash() { if ((_flags & F_components_given) != 0) { // If the transform was specified componentwise, hash it componentwise. - _hash = _pos.add_hash(_hash); + hash = _pos.add_hash(hash); if ((_flags & F_hpr_given) != 0) { - _hash = _hpr.add_hash(_hash); - - } else if ((_flags & F_quat_given) != 0) { - _hash = _quat.add_hash(_hash); + hash = _hpr.add_hash(hash); + } + else if ((_flags & F_quat_given) != 0) { + hash = _quat.add_hash(hash); } - _hash = _scale.add_hash(_hash); - _hash = _shear.add_hash(_hash); - - } else { + hash = _scale.add_hash(hash); + hash = _shear.add_hash(hash); + } + else { // Otherwise, hash the matrix . . . if (_uniquify_matrix) { // . . . but only if the user thinks that's worthwhile. - if ((_flags & F_mat_known) == 0) { - // Calculate the matrix without doubly-locking. - do_calc_mat(); - } - _hash = _mat.add_hash(_hash); - - } else { + check_mat(); + hash = _mat.add_hash(hash); + } + else { // Otherwise, hash the pointer only--any two different matrix-based // TransformStates are considered to be different, even if their // matrices have the same values. - _hash = pointer_hash::add_hash(_hash, this); + hash = pointer_hash::add_hash(hash, this); } } - _flags |= F_hash_known; + if (hash == H_unknown || hash == H_identity || hash == H_invalid) { + // Arbitrarily offset the hash not to conflict with these special values. + hash += 0x10000; + } + + // We don't care if some other thread set this in the meantime, since every + // thread should have computed the same hash. + AtomicAdjust::set(_hash, hash); } /** diff --git a/panda/src/pgraph/transformState.h b/panda/src/pgraph/transformState.h index 60d377c06e..288d26b315 100644 --- a/panda/src/pgraph/transformState.h +++ b/panda/src/pgraph/transformState.h @@ -326,8 +326,7 @@ private: INLINE void check_quat() const; INLINE void check_norm_quat() const; INLINE void check_mat() const; - INLINE void calc_hash(); - void do_calc_hash(); + void calc_hash() const; void calc_singular(); INLINE void calc_components(); void do_calc_components(); @@ -365,7 +364,6 @@ private: F_has_nonzero_shear = 0x00004000, F_is_destructing = 0x00008000, F_is_2d = 0x00010000, - F_hash_known = 0x00020000, F_norm_quat_known = 0x00040000, }; LPoint3 _pos; @@ -373,7 +371,13 @@ private: LQuaternion _quat, _norm_quat; LMatrix4 _mat; LMatrix4 *_inv_mat = nullptr; - size_t _hash; + + enum HashValue : AtomicAdjust::Integer { + H_unknown = 0, + H_identity = 1, + H_invalid = 2, + }; + mutable AtomicAdjust::Integer _hash = H_unknown; unsigned int _flags; From 3aa77a1260089704d67617ccb3967cddc940685b Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 16 Nov 2021 17:31:27 +0100 Subject: [PATCH 35/68] framework: Fix unnecessary ternary check --- panda/src/framework/windowFramework.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/framework/windowFramework.cxx b/panda/src/framework/windowFramework.cxx index 0d3f387eff..454729889b 100644 --- a/panda/src/framework/windowFramework.cxx +++ b/panda/src/framework/windowFramework.cxx @@ -675,7 +675,7 @@ load_default_model(const NodePath &parent) { GeomNode *geomnode = new GeomNode("tri"); PT(GeomVertexData) vdata = new GeomVertexData - ("tri", vertex_colors_prefer_packed ? GeomVertexFormat::get_v3n3ct2() : GeomVertexFormat::get_v3n3ct2(), + ("tri", GeomVertexFormat::get_v3n3t2(), Geom::UH_static); GeomVertexWriter vertex(vdata, InternalName::get_vertex()); GeomVertexWriter normal(vdata, InternalName::get_normal()); From 6adb6be4b508274132f7e9a4f20eeecdcfcc4e4a Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 16 Nov 2021 17:55:32 +0100 Subject: [PATCH 36/68] egg2pg: slight comment cleanup in characterMaker.cxx [skip ci] --- panda/src/egg2pg/characterMaker.cxx | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/panda/src/egg2pg/characterMaker.cxx b/panda/src/egg2pg/characterMaker.cxx index f087ebf88e..0c8bb03650 100644 --- a/panda/src/egg2pg/characterMaker.cxx +++ b/panda/src/egg2pg/characterMaker.cxx @@ -36,10 +36,6 @@ using std::string; - - - - /** * */ @@ -589,16 +585,15 @@ determine_bin_home(EggBin *egg_bin) { // ahead and make an implicit node for the joint. if (egg_group->get_dcs_type() == EggGroup::DC_none) { -/* - * Unless the user specifically forbade exposing the joint by putting an - * explicit " { none }" entry in the joint. In this case, we return NULL - * to treat the geometry as dynamic (and animate it by animating its - * vertices), but display lists and vertex buffers will perform better if more - * geometry is rigid. There's a tradeoff, though, since the cull traverser - * will have to do more work with additional transforms in the scene graph, - * and this may also break up the geometry into more individual pieces, which - * is the biggest limiting factor on modern PC graphics cards. - */ + // Unless the user specifically forbade exposing the joint by putting an + // explicit " { none }" entry in the joint. In this case, we return + // nullptr to treat the geometry as dynamic (and animate it by animating + // its vertices), but display lists and vertex buffers will perform better + // if more geometry is rigid. There's a tradeoff, though, since the cull + // traverser will have to do more work with additional transforms in the + // scene graph, and this may also break up the geometry into more + // individual pieces, which is the biggest limiting factor on modern PC + // graphics cards. return nullptr; } From 13db762a3338fc3a66330390ab68c9fa07b10d22 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 28 Nov 2021 13:56:21 +0100 Subject: [PATCH 37/68] makepanda: Add missing version of Windows 10 SDK --- makepanda/makepandacore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index b82b840027..4e03d3c2e2 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -2896,7 +2896,7 @@ def SetupVisualStudioEnviron(): elif not win_kit.endswith('\\'): win_kit += '\\' - for vnum in 10150, 10240, 10586, 14393, 15063, 16299, 17134, 17763, 18362: + for vnum in 10150, 10240, 10586, 14393, 15063, 16299, 17134, 17763, 18362, 19041: version = "10.0.{0}.0".format(vnum) if os.path.isfile(win_kit + "Include\\" + version + "\\ucrt\\assert.h"): print("Using Universal CRT %s" % (version)) From ea61e53ab27d91b5335323b8ba3e8159f6b88f0f Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 28 Nov 2021 13:56:55 +0100 Subject: [PATCH 38/68] event: Fix ButtonEvent docstring --- panda/src/event/buttonEvent.h | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/panda/src/event/buttonEvent.h b/panda/src/event/buttonEvent.h index e839924c48..b5f338bfb9 100644 --- a/panda/src/event/buttonEvent.h +++ b/panda/src/event/buttonEvent.h @@ -46,17 +46,18 @@ class DatagramIterator; class EXPCL_PANDA_EVENT ButtonEvent { public: enum Type { -/* - * T_down and T_up represent a button changing state correspondingly. - * T_resume_down is a special event that is only thrown when focus is returned - * to a window and a button is detected as being held down at that point; it - * indicates that the button should be considered down now (if it wasn't - * already), but it didn't just get pressed down at this moment, it was - * depressed some time ago. It's mainly used for correct tracking of modifier - * keys like shift and control, and can be ignored for other keys. - */ + // T_down is sent when a button was just pressed. T_down, + + // T_resume_down is a special event that is only thrown when focus is + // returned to a window and a button is detected as being held down at that + // point; it indicates that the button should be considered down now (if it + // wasn't already), but it didn't just get pressed down at this moment, it + // was depressed some time ago. It's mainly used for correct tracking of + // modifier keys like shift and control, and can be ignored for other keys. T_resume_down, + + // T_down is sent when a button is released. T_up, // T_repeat is sent for each a keyrepeat event generated by the system, From 54de9f9fa41ebedfc3ec4818cfca7e4730a50028 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 28 Nov 2021 13:57:20 +0100 Subject: [PATCH 39/68] glgsg: Fix possibility of simple RAM image going out of scope --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 8aba915666..54a62e10bb 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -13853,7 +13853,8 @@ upload_simple_texture(CLP(TextureContext) *gtc) { #endif GLenum external_format = GL_BGRA; - const unsigned char *image_ptr = tex->get_simple_ram_image(); + CPTA_uchar image = tex->get_simple_ram_image(); + const unsigned char *image_ptr = image.p(); if (image_ptr == nullptr) { return false; } From 8f9d23ff295a340a178beeffeaeebe1636c77715 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 28 Nov 2021 13:58:13 +0100 Subject: [PATCH 40/68] linmath: Remove redundant sin() calculation in LQuaternion --- panda/src/linmath/lquaternion_src.cxx | 1 - 1 file changed, 1 deletion(-) diff --git a/panda/src/linmath/lquaternion_src.cxx b/panda/src/linmath/lquaternion_src.cxx index cd48827eea..430b9971dc 100644 --- a/panda/src/linmath/lquaternion_src.cxx +++ b/panda/src/linmath/lquaternion_src.cxx @@ -106,7 +106,6 @@ set_hpr(const FLOATNAME(LVecBase3) &hpr, CoordinateSystem cs) { v = FLOATNAME(LVector3)::right(cs); a = deg_2_rad(hpr[1] * 0.5f); csincos(a, &s, &c); - s = csin(a); quat_p.set(c, v[0] * s, v[1] * s, v[2] * s); v = FLOATNAME(LVector3)::forward(cs); a = deg_2_rad(hpr[2] * 0.5f); From 5cad0eb973d1cbff7b659a425c9b8541819c3792 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 28 Nov 2021 13:59:21 +0100 Subject: [PATCH 41/68] glgsg: Don't use simple RAM image in shaders for non-sampler2D types Simple RAM images are always 2D right now, so this would otherwise cause an error. --- panda/src/glstuff/glShaderContext_src.cxx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/panda/src/glstuff/glShaderContext_src.cxx b/panda/src/glstuff/glShaderContext_src.cxx index e0db33e350..450fb69367 100644 --- a/panda/src/glstuff/glShaderContext_src.cxx +++ b/panda/src/glstuff/glShaderContext_src.cxx @@ -2952,10 +2952,13 @@ update_shader_texture_bindings(ShaderContext *prev) { // Bindless texturing wasn't supported or didn't work, so let's just bind // the texture normally. + // Note that simple RAM images are always 2-D for now, so to avoid errors, + // we must load the real texture if this is not for a sampler2D. + bool force = (spec._desired_type != Texture::TT_2d_texture); #ifndef OPENGLES if (multi_bind) { // Multi-bind case. - if (!_glgsg->update_texture(gtc, false)) { + if (!_glgsg->update_texture(gtc, force)) { textures[i] = 0; } else { gtc->set_active(true); @@ -2975,7 +2978,7 @@ update_shader_texture_bindings(ShaderContext *prev) { { // Non-multibind case. _glgsg->set_active_texture_stage(i); - if (!_glgsg->update_texture(gtc, false)) { + if (!_glgsg->update_texture(gtc, force)) { continue; } _glgsg->apply_texture(gtc); From 5eaa67fbc98b7b987d5d52131e06100fadf32c34 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 28 Nov 2021 14:00:59 +0100 Subject: [PATCH 42/68] gobj: Add TexturePool::get_texture() for querying tex from pool --- panda/src/gobj/texturePool.I | 23 ++++++++++++++ panda/src/gobj/texturePool.cxx | 56 ++++++++++++++++++++++++++++++++++ panda/src/gobj/texturePool.h | 16 ++++++++++ 3 files changed, 95 insertions(+) diff --git a/panda/src/gobj/texturePool.I b/panda/src/gobj/texturePool.I index 1c25485f51..c04627d75c 100644 --- a/panda/src/gobj/texturePool.I +++ b/panda/src/gobj/texturePool.I @@ -33,6 +33,29 @@ verify_texture(const Filename &filename) { return load_texture(filename) != nullptr; } +/** + * Returns the texture that has already been previously loaded, or NULL + * otherwise. + */ +INLINE Texture *TexturePool:: +get_texture(const Filename &filename, int primary_file_num_channels, + bool read_mipmaps) { + return get_global_ptr()->ns_get_texture(filename, primary_file_num_channels, + read_mipmaps); +} + +/** + * Returns the texture that has already been previously loaded, or NULL + * otherwise. + */ +INLINE Texture *TexturePool:: +get_texture(const Filename &filename, const Filename &alpha_filename, + int primary_file_num_channels, int alpha_file_channel, + bool read_mipmaps) { + return get_global_ptr()->ns_get_texture(filename, primary_file_num_channels, + read_mipmaps); +} + /** * Loads the given filename up into a texture, if it has not already been * loaded, and returns the new texture. If a texture with the same filename diff --git a/panda/src/gobj/texturePool.cxx b/panda/src/gobj/texturePool.cxx index c09df1d9e3..40c67b9c92 100644 --- a/panda/src/gobj/texturePool.cxx +++ b/panda/src/gobj/texturePool.cxx @@ -197,6 +197,62 @@ ns_has_texture(const Filename &orig_filename) { return false; } +/** + * The nonstatic implementation of get_texture(). + */ +Texture *TexturePool:: +ns_get_texture(const Filename &orig_filename, int primary_file_num_channels, + bool read_mipmaps) { + LookupKey key; + key._primary_file_num_channels = primary_file_num_channels; + { + MutexHolder holder(_lock); + resolve_filename(key._fullpath, orig_filename, read_mipmaps, LoaderOptions()); + + Textures::const_iterator ti; + ti = _textures.find(key); + if (ti != _textures.end()) { + // This texture was previously loaded. + Texture *tex = (*ti).second; + nassertr(!tex->get_fullpath().empty(), tex); + return tex; + } + } + + return nullptr; +} + +/** + * The nonstatic implementation of get_texture(). + */ +Texture *TexturePool:: +ns_get_texture(const Filename &orig_filename, + const Filename &orig_alpha_filename, + int primary_file_num_channels, + int alpha_file_channel, + bool read_mipmaps) { + LookupKey key; + key._primary_file_num_channels = primary_file_num_channels; + key._alpha_file_channel = alpha_file_channel; + { + MutexHolder holder(_lock); + LoaderOptions options; + resolve_filename(key._fullpath, orig_filename, read_mipmaps, options); + resolve_filename(key._alpha_fullpath, orig_alpha_filename, read_mipmaps, options); + + Textures::const_iterator ti; + ti = _textures.find(key); + if (ti != _textures.end()) { + // This texture was previously loaded. + Texture *tex = (*ti).second; + nassertr(!tex->get_fullpath().empty(), tex); + return tex; + } + } + + return nullptr; +} + /** * The nonstatic implementation of load_texture(). */ diff --git a/panda/src/gobj/texturePool.h b/panda/src/gobj/texturePool.h index de6812e4d5..388dc415bf 100644 --- a/panda/src/gobj/texturePool.h +++ b/panda/src/gobj/texturePool.h @@ -38,6 +38,14 @@ class EXPCL_PANDA_GOBJ TexturePool { PUBLISHED: INLINE static bool has_texture(const Filename &filename); INLINE static bool verify_texture(const Filename &filename); + INLINE static Texture *get_texture(const Filename &filename, + int primary_file_num_channels = 0, + bool read_mipmaps = false); + INLINE static Texture *get_texture(const Filename &filename, + const Filename &alpha_filename, + int primary_file_num_channels = 0, + int alpha_file_channel = 0, + bool read_mipmaps = false); BLOCKING INLINE static Texture *load_texture(const Filename &filename, int primary_file_num_channels = 0, bool read_mipmaps = false, @@ -96,6 +104,14 @@ private: TexturePool(); bool ns_has_texture(const Filename &orig_filename); + Texture *ns_get_texture(const Filename &filename, + int primary_file_num_channels = 0, + bool read_mipmaps = false); + Texture *ns_get_texture(const Filename &filename, + const Filename &alpha_filename, + int primary_file_num_channels = 0, + int alpha_file_channel = 0, + bool read_mipmaps = false); Texture *ns_load_texture(const Filename &orig_filename, int primary_file_num_channels, bool read_mipmaps, From c0a9123ddeb559a83f50e6e60b2ec93045e3fef6 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 28 Nov 2021 14:01:33 +0100 Subject: [PATCH 43/68] gobj: Fix TextureReloadRequest not reloading in certain situations This prevented async texture loading from working unless mipmapping was enabled --- panda/src/gobj/textureReloadRequest.cxx | 40 ++++++++++++++----------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/panda/src/gobj/textureReloadRequest.cxx b/panda/src/gobj/textureReloadRequest.cxx index b63acb6090..af6454c33a 100644 --- a/panda/src/gobj/textureReloadRequest.cxx +++ b/panda/src/gobj/textureReloadRequest.cxx @@ -22,28 +22,34 @@ TypeHandle TextureReloadRequest::_type_handle; AsyncTask::DoneStatus TextureReloadRequest:: do_task() { // Don't reload the texture if it doesn't need it. - if (_texture->was_image_modified(_pgo)) { - double delay = async_load_delay; - if (delay != 0.0) { - Thread::sleep(delay); - } + if (!_texture->was_image_modified(_pgo) && + (_allow_compressed ? _texture->has_ram_image() : _texture->has_uncompressed_ram_image())) { + return DS_done; + } - if (_texture->was_image_modified(_pgo)) { - if (_allow_compressed) { - _texture->get_ram_image(); - } else { - _texture->get_uncompressed_ram_image(); - } + double delay = async_load_delay; + if (delay != 0.0) { + Thread::sleep(delay); - // Now that we've loaded the texture, we should ensure it actually gets - // prepared--even if it's no longer visible in the frame--or it may - // become a kind of a leak (if the texture is never rendered again on - // this GSG, we'll just end up carrying the texture memory in RAM - // forever, instead of dumping it as soon as it gets prepared). - _pgo->enqueue_texture(_texture); + if (!_texture->was_image_modified(_pgo) && + (_allow_compressed ? _texture->has_ram_image() : _texture->has_uncompressed_ram_image())) { + return DS_done; } } + if (_allow_compressed) { + _texture->get_ram_image(); + } else { + _texture->get_uncompressed_ram_image(); + } + + // Now that we've loaded the texture, we should ensure it actually gets + // prepared--even if it's no longer visible in the frame--or it may become a + // kind of a leak (if the texture is never rendered again on this GSG, we'll + // just end up carrying the texture memory in RAM forever, instead of dumping + // it as soon as it gets prepared). + _pgo->enqueue_texture(_texture); + // Don't continue the task; we're done. return DS_done; } From 2abf867203993d1430d01e83280340db421d6459 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 28 Nov 2021 14:04:41 +0100 Subject: [PATCH 44/68] gobj: Don't load textures when loading bam file if preloading is off This is wasteful (especially when the bam file already has the simple RAM images baked in if needed) since we are throwing away the texture data right away anyway. Just locate it on disk, it will be loaded later when it is needed. Fixes #1208 --- panda/src/gobj/texture.cxx | 57 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/panda/src/gobj/texture.cxx b/panda/src/gobj/texture.cxx index 940944daef..ccd6997691 100644 --- a/panda/src/gobj/texture.cxx +++ b/panda/src/gobj/texture.cxx @@ -10315,10 +10315,12 @@ make_this_from_bam(const FactoryParams ¶ms) { // object to read all of the attributes from the bam stream. Texture *dummy = this; AutoTextureScale auto_texture_scale = ATS_unspecified; + bool has_simple_ram_image = false; { CDWriter cdata_dummy(dummy->_cycler, true); dummy->do_fillin_body(cdata_dummy, scan, manager); auto_texture_scale = cdata_dummy->_auto_texture_scale; + has_simple_ram_image = !cdata_dummy->_simple_ram_image._image.empty(); } if (filename.empty()) { @@ -10351,6 +10353,61 @@ make_this_from_bam(const FactoryParams ¶ms) { case TT_1d_texture: case TT_2d_texture: case TT_1d_texture_array: + // If we don't want to preload textures, and we already have a simple + // RAM image (or don't need one), we don't need to load it from disk. + // We do check for it in the texture pool first, though, in case it has + // already been loaded. + if ((options.get_texture_flags() & LoaderOptions::TF_preload) == 0 && + (has_simple_ram_image || (options.get_texture_flags() & LoaderOptions::TF_preload_simple) == 0)) { + if (alpha_filename.empty()) { + me = TexturePool::get_texture(filename, primary_file_num_channels, + has_read_mipmaps); + } else { + me = TexturePool::get_texture(filename, alpha_filename, + primary_file_num_channels, + alpha_file_channel, + has_read_mipmaps); + } + if (me != nullptr && me->get_texture_type() == texture_type) { + // We can use this. + break; + } + + // We don't have a texture, but we didn't need to preload it, so we + // can just use this one. We just need to know where we can find it + // when we do need to reload it. + Filename fullpath = filename; + Filename alpha_fullpath = alpha_filename; + const DSearchPath &model_path = get_model_path(); + if (vfs->resolve_filename(fullpath, model_path) && + (alpha_fullpath.empty() || vfs->resolve_filename(alpha_fullpath, model_path))) { + me = dummy; + me->set_name(name); + + { + CDWriter cdata_me(me->_cycler, true); + cdata_me->_filename = filename; + cdata_me->_alpha_filename = alpha_filename; + cdata_me->_fullpath = fullpath; + cdata_me->_alpha_fullpath = alpha_fullpath; + cdata_me->_primary_file_num_channels = primary_file_num_channels; + cdata_me->_alpha_file_channel = alpha_file_channel; + cdata_me->_texture_type = texture_type; + cdata_me->_loaded_from_image = true; + cdata_me->_has_read_mipmaps = has_read_mipmaps; + } + + // To manage the reference count, explicitly ref it now, then unref + // it in the finalize callback. + me->ref(); + manager->register_finalize(me); + + // Do add it to the cache now, so that future uses of this same + // texture are unified. + TexturePool::add_texture(me); + return me; + } + } if (alpha_filename.empty()) { me = TexturePool::load_texture(filename, primary_file_num_channels, has_read_mipmaps, options); From 980399d14239ab516da5498520b7d06599628794 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 28 Nov 2021 14:36:20 +0100 Subject: [PATCH 45/68] event: Expose ButtonEvent to Python --- panda/src/event/buttonEvent.I | 32 +++++++++++++++++++++++++++++++ panda/src/event/buttonEvent.h | 16 +++++++++++++++- panda/src/event/buttonEventList.h | 2 ++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/panda/src/event/buttonEvent.I b/panda/src/event/buttonEvent.I index f082a4bee9..959f66dc47 100644 --- a/panda/src/event/buttonEvent.I +++ b/panda/src/event/buttonEvent.I @@ -149,3 +149,35 @@ update_mods(ModifierButtons &mods) const { return false; } } + +/** + * + */ +INLINE ButtonHandle ButtonEvent:: +get_button() const { + return _button; +} + +/** + * + */ +INLINE int ButtonEvent:: +get_keycode() const { + return _keycode; +} + +/** + * + */ +INLINE ButtonEvent::Type ButtonEvent:: +get_type() const { + return _type; +} + +/** + * + */ +INLINE double ButtonEvent:: +get_time() const { + return _time; +} diff --git a/panda/src/event/buttonEvent.h b/panda/src/event/buttonEvent.h index b5f338bfb9..9da453a827 100644 --- a/panda/src/event/buttonEvent.h +++ b/panda/src/event/buttonEvent.h @@ -44,7 +44,7 @@ class DatagramIterator; * character set. */ class EXPCL_PANDA_EVENT ButtonEvent { -public: +PUBLISHED: enum Type { // T_down is sent when a button was just pressed. T_down, @@ -85,6 +85,7 @@ public: T_raw_up, }; +public: INLINE ButtonEvent(); INLINE ButtonEvent(ButtonHandle button, Type type, double time = ClockObject::get_global_clock()->get_frame_time()); INLINE ButtonEvent(int keycode, double time = ClockObject::get_global_clock()->get_frame_time()); @@ -93,17 +94,30 @@ public: INLINE ButtonEvent(const ButtonEvent ©); INLINE void operator = (const ButtonEvent ©); +PUBLISHED: INLINE bool operator == (const ButtonEvent &other) const; INLINE bool operator != (const ButtonEvent &other) const; INLINE bool operator < (const ButtonEvent &other) const; +public: INLINE bool update_mods(ModifierButtons &mods) const; + INLINE ButtonHandle get_button() const; + INLINE int get_keycode() const; + INLINE Type get_type() const; + INLINE double get_time() const; + void output(std::ostream &out) const; void write_datagram(Datagram &dg) const; void read_datagram(DatagramIterator &scan); +PUBLISHED: + MAKE_PROPERTY(button, get_button); + MAKE_PROPERTY(keycode, get_keycode); + MAKE_PROPERTY(type, get_type); + MAKE_PROPERTY(time, get_time); + public: // _button will be filled in if type is T_down, T_resume_down, or T_up. ButtonHandle _button; diff --git a/panda/src/event/buttonEventList.h b/panda/src/event/buttonEventList.h index 0a7822fc63..a3cbcfb1e7 100644 --- a/panda/src/event/buttonEventList.h +++ b/panda/src/event/buttonEventList.h @@ -47,6 +47,8 @@ PUBLISHED: virtual void output(std::ostream &out) const; void write(std::ostream &out, int indent_level = 0) const; + MAKE_SEQ_PROPERTY(events, get_num_events, get_event); + private: typedef pvector Events; Events _events; From 15c84afbe9a4ece2ffd6d80ab460e00d3bddc79f Mon Sep 17 00:00:00 2001 From: loonaticx Date: Sun, 28 Nov 2021 16:12:44 +0100 Subject: [PATCH 46/68] doc: Improve egg syntax documentation [skip ci] Closes #1199 --- panda/src/doc/eggSyntax.txt | 149 ++++++++++++++----------- panda/src/doc/howto.MultiGenModelFlags | 20 ++-- 2 files changed, 92 insertions(+), 77 deletions(-) diff --git a/panda/src/doc/eggSyntax.txt b/panda/src/doc/eggSyntax.txt index 4b57d91192..b81eb45c0e 100644 --- a/panda/src/doc/eggSyntax.txt +++ b/panda/src/doc/eggSyntax.txt @@ -3,7 +3,7 @@ THE PHILOSOPHY OF EGG FILES (vs. bam files) Egg files are used by Panda3D to describe many properties of a scene: simple geometry, including special effects and collision surfaces, characters including skeletons, morphs, and multiple-joint -assignments, and character animation tables. +assignments, and character animation tables. Egg files are designed to be the lingua franca of model manipulation for Panda tools. A number of utilities are provided that read and @@ -168,8 +168,9 @@ appear before they are referenced. The remaining formats are generic and specify only the semantic meaning of the channels. The size of the texels is determined by the width of the components in the image file. RGBA is the most - general; RGB is the same, but without any alpha channel. RGBM is - like RGBA, except that it requests only one bit of alpha, if the + general; RGB is the same, but without any alpha channel. + + RGBM is like RGBA, except that it requests only one bit of alpha, if the graphics card can provide that, to leave more room for the RGB components, which is especially important for older 16-bit graphics cards (the "M" stands for "mask", as in a cutout). @@ -211,7 +212,7 @@ appear before they are referenced. Although less often used, for 3-d textures wrapw may also be specified, and it behaves similarly to wrapu and wrapv. - There are other legal values in addtional to REPEAT and CLAMP. + There are other legal values in additional to REPEAT and CLAMP. The full list is: CLAMP @@ -278,7 +279,7 @@ appear before they are referenced. / (number of views). read-mipmaps { flag } - + If this flag is nonzero, then pre-generated mipmap levels will be loaded along with the texture. In this case, the filename should contain a sequence of one or more hash mark ("#") characters, @@ -413,7 +414,7 @@ appear before they are referenced. combine-alpha-operand2 { src-alpha } saved-result { flag } - + If flag is nonzero, then it indicates that this particular texture stage will be supplied as the "last_saved_result" source for any future texture stages. @@ -704,7 +705,7 @@ appear before they are referenced. [name] { u v [w] [tangent] [binormal] [morph-list] } This gives the texture coordinates of the vertex. This must be - specified if a texture is to be mapped onto this geometry. + specified if a texture is to be mapped onto this geometry. The texture coordinates are usually two-dimensional, with two component values (u v), but they may also be three-dimensional, @@ -739,7 +740,7 @@ appear before they are referenced. meaning to custom code or a custom shader. Like named UV's, there may be multiple Aux entries for a given vertex, each with a different name. - + name { vertices } @@ -758,18 +759,18 @@ appear before they are referenced. At the present time, the DynamicVertexPool is not implemented in Panda3D. - + GEOMETRY ENTRIES - name { - [attributes] - { - indices - { pool-name } - } + name { + [attributes] + { + indices + { pool-name } + } } A polygon consists of a sequence of vertices from a single vertex @@ -822,7 +823,7 @@ GEOMETRY ENTRIES used unless all vertices also have a normal. If no normal is defined, none will be supplied. The polygon normal, like the vertex normal, may be morphed by specifying a series of - entries. + entries. The polygon normal is used only for lighting and environment mapping calculations, and is not related to the implicit normal @@ -844,7 +845,7 @@ GEOMETRY ENTRIES disabled, and polygons are one-sided; specifying a nonzero value disables backface culling for this particular polygon and allows it to be viewed from either side. - + bin { bin-name } @@ -916,12 +917,12 @@ GEOMETRY ENTRIES or even within a texture. - name { - [attributes] - { - indices - { pool-name } - } + name { + [attributes] + { + indices + { pool-name } + } } A patch is similar to a polygon, but it is a special primitive that @@ -936,12 +937,12 @@ GEOMETRY ENTRIES specified for Patch. - name { - [attributes] - { - indices - { pool-name } - } + name { + [attributes] + { + indices + { pool-name } + } } A PointLight is a set of single points. One point is drawn for each @@ -964,12 +965,12 @@ GEOMETRY ENTRIES viewer normally. - name { - [attributes] - { - indices - { pool-name } - } + name { + [attributes] + { + indices + { pool-name } + } [component attributes] } @@ -985,11 +986,11 @@ GEOMETRY ENTRIES line segment, as in TriangleStrip, below. - name { - [attributes] - { - indices - { pool-name } + name { + [attributes] + { + indices + { pool-name } } [component attributes] } @@ -1013,7 +1014,7 @@ GEOMETRY ENTRIES It is possible for the individual triangles of a triangle strip to have a separate normal and/or color. If so, a entry should be given for each so-modified triangle: - + index { { r g b a [morph-list] } { x y z [morph-list] } @@ -1024,11 +1025,11 @@ GEOMETRY ENTRIES must always follow the vertex list. - name { - [attributes] - { - indices - { pool-name } + name { + [attributes] + { + indices + { pool-name } } [component attributes] } @@ -1057,7 +1058,7 @@ itself doesn't support them and will always create static curves and surfaces. External tools like egg-qtess, however, may respect them. { - [attributes] + [attributes] { order } { knot-list } @@ -1083,7 +1084,7 @@ surfaces. External tools like egg-qtess, however, may respect them. type { curve-type } - This defines the semanting meaning of this curve, either XYZ, HPR, + This defines the semantic meaning of this curve, either XYZ, HPR, or T. If the type is XYZ, the curve will automatically be transformed between Y-up and Z-up if necessary; otherwise, it will be left alone. @@ -1107,16 +1108,16 @@ surfaces. External tools like egg-qtess, however, may respect them. name { - [attributes] + [attributes] { u-order v-order } { u-knot-list } { v-knot-list } - { - indices - { pool-name } - } + { + indices + { pool-name } + } } A NURBS surface is an extension of a NURBS curve into two parametric @@ -1137,7 +1138,7 @@ surfaces. External tools like egg-qtess, however, may respect them. These define the number of subdivisions to make in the U and V directions to represent the surface. A uniform subdivision is always made, and trim curves are not respected (though they will - be drawn in if the trim curves themselves also have a subiv + be drawn in if the trim curves themselves also have a subdiv parameter). This is only intended as a cheesy visualization. @@ -1194,7 +1195,7 @@ surfaces. External tools like egg-qtess, however, may respect them. Although the egg syntax supports trim curves, there are at present no egg processing tools that respect them. For instance, egg-qtess - ignores trim curves and always tesselates the entire NURBS surface. + ignores trim curves and always tessellates the entire NURBS surface. MORPH DESCRIPTION ENTRIES @@ -1267,7 +1268,7 @@ GROUPING ENTRIES attributes of the group: GROUP BINARY ATTRIBUTES - + These attributes may be either on or off; they are off by default. They are turned on by specifying a non-zero "boolean-value". @@ -1310,6 +1311,15 @@ GROUPING ENTRIES vertices; joints and morphs appearing outside of a hierarchy identified with a flag are undefined. + { structured } + + This is an optional alternative for the flag. + By default, Panda will collapse all of the geometry in a group (with the { 1 } flag) + a single node. While this is optimal for conditions such as characters moving around + a scene, it may be suboptimal for larger or more complex characters. + This entry is typically generated by the egg-optchar program with the "-dart structured" flag. + { structured } implies { 1 }. + { boolean-value } This attribute indicates that the child nodes of this group @@ -1446,7 +1456,7 @@ GROUPING ENTRIES of the billboard, not at the origin of the scene. { - { + { in out [fade] { x y z } } } @@ -1483,7 +1493,7 @@ GROUPING ENTRIES Valid types so far are: Plane - + The geometry represents an infinite plane. The first polygon found in the group will define the plane. @@ -1550,7 +1560,7 @@ GROUPING ENTRIES most compatibility. keep - + Don't discard the visible geometry after using it to define a collision surface; create both an invisible collision surface and the visible geometry. @@ -1619,7 +1629,8 @@ GROUPING ENTRIES There may also be additional predefined egg object types not listed here; see the *.pp files that are installed into the etc - directory for a complete list. + directory for a complete list. Additionally, you can reference + the file located in $PANDA/src/doc/howto.MultiGenModelFlags { transform-definition } @@ -1683,9 +1694,9 @@ GROUPING ENTRIES current group, regardless of the group in which the geometry is actually defined. See the description, below. - { + { fps { float-value } - num-frames { integer-value } + num-frames { integer-value } } One or more AnimPreload entries may appear within the that @@ -1720,7 +1731,7 @@ GROUPING ENTRIES geometry in the scene graph. The syntax is: name { - { group-name } + { group-name } [ { group-name } ... ] } @@ -1746,8 +1757,8 @@ GROUPING ENTRIES and it may contain other joints. A tree of nodes only makes sense within a character - definition, which is created by applying the flag to a group. - See , above. + definition, which is created by applying the flag to a group. + See , above. The vertex assignment is crucial. This is how the geometry of a character is made to move with the joints. The character's geometry @@ -1790,7 +1801,7 @@ GROUPING ENTRIES bundles, or any one of the following ( entries are optional, and default as shown): - name { + name { fps { 24 } { values } } @@ -1844,7 +1855,7 @@ GROUPING ENTRIES an animation sequence. - name { + name { width { table-width } fps { 24 } { values } @@ -1913,6 +1924,10 @@ ANIMATION STRUCTURE animated model description. Without the flag, joints will be treated as ordinary groups, and morphs will be ignored. + It is important to note that utilizing { 1 } will collapse all of the + model's geometry into a single node. To omit this, use { structured } instead. + (See above.) + In the above, UPPERCASE NAMES represent an arbitrary name that you may choose. The name of the enclosing group, CHARACTER_NAME, is taken as the name of the animated model. It should generally match diff --git a/panda/src/doc/howto.MultiGenModelFlags b/panda/src/doc/howto.MultiGenModelFlags index ac67b7e93f..3989859803 100644 --- a/panda/src/doc/howto.MultiGenModelFlags +++ b/panda/src/doc/howto.MultiGenModelFlags @@ -3,7 +3,7 @@ This document describes the different kinds of model flags one can place in the comment field of MultiGen group beads. The general format for a model -flag is: +flag is: { {value} } The most up-to-date version of this document can be found in: @@ -30,7 +30,7 @@ The most up-to-date version of this document can be found in: { {camera-barrier} } Invisible collision surface for camera and colliders { {camera-barrier-sphere} } Invisible sphere collision surface for camera and colliders { {backstage} } Modeling reference object - { {1} } Decal the node below to me + { {1} } Decal the node below to me (like a window on a wall) { fps { # } } Set rate of animation for a pfSequence @@ -52,12 +52,12 @@ common flag/value pairs and describes what they are used for. which one places the flag (so names like red-hut are more useful than names like o34). - { {1} } + { {1} } - Used to show/hide, change the color, or change the collision properties + Used to show/hide, change the color, or change the collision properties of a chunk. - { {1} } + { {1} } Used to move, rotate, or scale a chunk of the model. Also can be used (like the flag) to show/hide, change the color, and change the @@ -91,7 +91,7 @@ common flag/value pairs and describes what they are used for. a door, for example, so the player can tell when the avatar has moved through the door. - BACKSTAGE objects are not translated over to the player. Modelers - should use this flag on reference objects that they include to help + should use this flag on reference objects that they include to help in the modeling task (such as scale references) IMPORTANT NOTE: @@ -108,7 +108,7 @@ common flag/value pairs and describes what they are used for. ********** PROPERTIES ********** -These are used to control properties of selected chunks. +These are used to control properties of selected chunks. { fps { frame-rate } } @@ -124,7 +124,7 @@ These are used to control properties of selected chunks. Multiple Flag/value pairs can be combined within an single field. For example: - { {1} + { {1} {barrier} } Generally, the flag can be combined with most other flags @@ -134,7 +134,7 @@ These are used to control properties of selected chunks. could also be written as: {{1}{barrier}} - + 3) Where to place the flags All model flags except flags are generally placed in the @@ -154,6 +154,6 @@ These are used to control properties of selected chunks. 4) Flags at different levels in the model Flags in lower level beads generally override flags in upper level - beads. + beads. 5) For more detailed information see $PANDA/src/doc/eggSyntax.txt. From 480226a97a691755740e8ac1b53db5f3bc70a7f8 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 28 Nov 2021 16:14:36 +0100 Subject: [PATCH 47/68] cmath: Use combined sincos on GNU where it is available --- dtool/src/dtoolbase/cmath.I | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dtool/src/dtoolbase/cmath.I b/dtool/src/dtoolbase/cmath.I index b303671fbb..8f834d278f 100644 --- a/dtool/src/dtoolbase/cmath.I +++ b/dtool/src/dtoolbase/cmath.I @@ -64,10 +64,12 @@ csincos(float v, float *sin_result, float *cos_result) { fstp DWORD ptr [edx] fstp DWORD ptr [eax] } -#else //!_X86_ +#elif defined(_GNU_SOURCE) + sincosf(v, sin_result, cos_result); +#else *sin_result = sinf(v); *cos_result = cosf(v); -#endif //!_X86_ +#endif } /** @@ -231,6 +233,8 @@ csincos(double v, double *sin_result, double *cos_result) { fstp QWORD ptr [edx] fstp QWORD ptr [eax] } +#elif defined(_GNU_SOURCE) + sincos(v, sin_result, cos_result); #else //!_X86_ *sin_result = sin(v); *cos_result = cos(v); From c9806436494fb4a31a096e92effb9c92a81447b4 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 28 Nov 2021 16:21:59 +0100 Subject: [PATCH 48/68] chan: Lock PartBundle while binding animation This seems like the right thing to do that could prevent some potential race conditions; let me known if this is breaking anything. --- panda/src/chan/partBundle.cxx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/panda/src/chan/partBundle.cxx b/panda/src/chan/partBundle.cxx index ddd31898ee..2cd6eca594 100644 --- a/panda/src/chan/partBundle.cxx +++ b/panda/src/chan/partBundle.cxx @@ -599,6 +599,10 @@ do_bind_anim(AnimControl *control, AnimBundle *anim, return false; } + // Grabbing the lock early prevents any other thread in stage 0 from also + // trying to modify the channel list at the same time. + CDLockedReader cdata(_cycler); + plist holes; int channel_index = 0; pick_channel_index(holes, channel_index); @@ -616,7 +620,6 @@ do_bind_anim(AnimControl *control, AnimBundle *anim, subset.is_include_empty(), bound_joints, subset); control->setup_anim(this, anim, channel_index, bound_joints); - CDReader cdata(_cycler); determine_effective_channels(cdata); return true; From 7dab0df0619feb9bb57a535f2225c5d4cc842e79 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 28 Nov 2021 16:48:45 +0100 Subject: [PATCH 49/68] cmath: Fix compilation error on macOS with sincos --- dtool/src/dtoolbase/cmath.I | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dtool/src/dtoolbase/cmath.I b/dtool/src/dtoolbase/cmath.I index 8f834d278f..12ec33f1df 100644 --- a/dtool/src/dtoolbase/cmath.I +++ b/dtool/src/dtoolbase/cmath.I @@ -64,6 +64,8 @@ csincos(float v, float *sin_result, float *cos_result) { fstp DWORD ptr [edx] fstp DWORD ptr [eax] } +#elif defined(__APPLE__) + __sincosf(v, sin_result, cos_result); #elif defined(_GNU_SOURCE) sincosf(v, sin_result, cos_result); #else @@ -233,6 +235,8 @@ csincos(double v, double *sin_result, double *cos_result) { fstp QWORD ptr [edx] fstp QWORD ptr [eax] } +#elif defined(__APPLE__) + __sincos(v, sin_result, cos_result); #elif defined(_GNU_SOURCE) sincos(v, sin_result, cos_result); #else //!_X86_ From 4af5bc9e6d860e6a082f69674ec8f646ee90004c Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 29 Nov 2021 13:11:32 +0100 Subject: [PATCH 50/68] event: Support lambdas as tasks via new FunctionAsyncTask This is intended to replace GenericAsyncTask. --- panda/src/event/CMakeLists.txt | 2 + panda/src/event/asyncTaskManager.I | 13 ++++ panda/src/event/asyncTaskManager.h | 4 ++ panda/src/event/config_event.cxx | 2 + panda/src/event/functionAsyncTask.I | 83 +++++++++++++++++++++++++ panda/src/event/functionAsyncTask.cxx | 81 ++++++++++++++++++++++++ panda/src/event/functionAsyncTask.h | 83 +++++++++++++++++++++++++ panda/src/event/genericAsyncTask.h | 2 + panda/src/event/p3event_composite1.cxx | 1 + panda/src/framework/windowFramework.cxx | 21 ++----- panda/src/framework/windowFramework.h | 4 +- 11 files changed, 278 insertions(+), 18 deletions(-) create mode 100644 panda/src/event/functionAsyncTask.I create mode 100644 panda/src/event/functionAsyncTask.cxx create mode 100644 panda/src/event/functionAsyncTask.h diff --git a/panda/src/event/CMakeLists.txt b/panda/src/event/CMakeLists.txt index b0464ff793..47136d1b5e 100644 --- a/panda/src/event/CMakeLists.txt +++ b/panda/src/event/CMakeLists.txt @@ -9,6 +9,7 @@ set(P3EVENT_HEADERS config_event.h buttonEvent.I buttonEvent.h buttonEventList.I buttonEventList.h + functionAsyncTask.h functionAsyncTask.I genericAsyncTask.h genericAsyncTask.I pointerEvent.I pointerEvent.h pointerEventList.I pointerEventList.h @@ -28,6 +29,7 @@ set(P3EVENT_SOURCES asyncTaskSequence.cxx buttonEvent.cxx buttonEventList.cxx + functionAsyncTask.cxx genericAsyncTask.cxx pointerEvent.cxx pointerEventList.cxx diff --git a/panda/src/event/asyncTaskManager.I b/panda/src/event/asyncTaskManager.I index 8cf788dc9e..4af23b4bcf 100644 --- a/panda/src/event/asyncTaskManager.I +++ b/panda/src/event/asyncTaskManager.I @@ -32,6 +32,19 @@ get_clock() { return _clock; } +#ifndef CPPPARSER +/** + * Adds a new task which calls the indicated function to the task manager. + * Returns the newly created FunctionAsyncTask object. + */ +INLINE AsyncTask *AsyncTaskManager:: +add(const std::string &name, FunctionAsyncTask::TaskFunction function) { + AsyncTask *task = new FunctionAsyncTask(name, std::move(function)); + add(task); + return task; +} +#endif + /** * Returns the number of tasks that are currently active or sleeping within * the task manager. diff --git a/panda/src/event/asyncTaskManager.h b/panda/src/event/asyncTaskManager.h index 0bb137deb4..0982e5e2c7 100644 --- a/panda/src/event/asyncTaskManager.h +++ b/panda/src/event/asyncTaskManager.h @@ -30,6 +30,7 @@ #include "clockObject.h" #include "ordered_vector.h" #include "indirectCompareNames.h" +#include "functionAsyncTask.h" /** * A class to manage a loose queue of isolated tasks, which can be performed @@ -64,6 +65,9 @@ PUBLISHED: BLOCKING bool remove_task_chain(const std::string &name); void add(AsyncTask *task); +#ifndef CPPPARSER + INLINE AsyncTask *add(const std::string &name, FunctionAsyncTask::TaskFunction function); +#endif bool has_task(AsyncTask *task) const; AsyncTask *find_task(const std::string &name) const; diff --git a/panda/src/event/config_event.cxx b/panda/src/event/config_event.cxx index ccae0d95ae..93dc8d916b 100644 --- a/panda/src/event/config_event.cxx +++ b/panda/src/event/config_event.cxx @@ -22,6 +22,7 @@ #include "event.h" #include "eventHandler.h" #include "eventParameter.h" +#include "functionAsyncTask.h" #include "genericAsyncTask.h" #include "pointerEventList.h" @@ -49,6 +50,7 @@ ConfigureFn(config_event) { EventHandler::init_type(); EventStoreInt::init_type("EventStoreInt"); EventStoreDouble::init_type("EventStoreDouble"); + FunctionAsyncTask::init_type(); GenericAsyncTask::init_type(); ButtonEventList::register_with_read_factory(); diff --git a/panda/src/event/functionAsyncTask.I b/panda/src/event/functionAsyncTask.I new file mode 100644 index 0000000000..aa413dacf0 --- /dev/null +++ b/panda/src/event/functionAsyncTask.I @@ -0,0 +1,83 @@ +/** + * 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 functionAsyncTask.I + * @author rdb + * @date 2021-11-29 + */ + +/** + * + */ +INLINE FunctionAsyncTask:: +FunctionAsyncTask(const std::string &name) : + AsyncTask(name) +{ +} + +/** + * + */ +INLINE FunctionAsyncTask:: +FunctionAsyncTask(const std::string &name, FunctionAsyncTask::TaskFunction function) : + AsyncTask(name), + _function(std::move(function)) +{ +} + +/** + * Replaces the function that is called when the task runs. + */ +INLINE void FunctionAsyncTask:: +set_function(TaskFunction function) { + _function = std::move(function); +} + +/** + * Returns the function that is called when the task runs. + */ +INLINE const FunctionAsyncTask::TaskFunction &FunctionAsyncTask:: +get_function() const { + return _function; +} + +/** + * Replaces the function that is called when the task begins. This is an + * optional function. + */ +INLINE void FunctionAsyncTask:: +set_upon_birth(BirthFunction upon_birth) { + _upon_birth = std::move(upon_birth); +} + +/** + * Returns the function that is called when the task begins, or NULL if the + * function is not defined. + */ +INLINE const FunctionAsyncTask::BirthFunction &FunctionAsyncTask:: +get_upon_birth() const { + return _upon_birth; +} + +/** + * Replaces the function that is called when the task ends. This is an + * optional function. + */ +INLINE void FunctionAsyncTask:: +set_upon_death(FunctionAsyncTask::DeathFunction upon_death) { + _upon_death = upon_death; +} + +/** + * Returns the function that is called when the task ends, or NULL if the + * function is not defined. + */ +INLINE const FunctionAsyncTask::DeathFunction &FunctionAsyncTask:: +get_upon_death() const { + return _upon_death; +} diff --git a/panda/src/event/functionAsyncTask.cxx b/panda/src/event/functionAsyncTask.cxx new file mode 100644 index 0000000000..54b591918d --- /dev/null +++ b/panda/src/event/functionAsyncTask.cxx @@ -0,0 +1,81 @@ +/** + * 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 functionAsyncTask.cxx + * @author rdb + * @date 2021-11-29 + */ + +#include "functionAsyncTask.h" +#include "pnotify.h" + +#ifndef CPPPARSER + +TypeHandle FunctionAsyncTask::_type_handle; + +/** + * Override this function to return true if the task can be successfully + * executed, false if it cannot. Mainly intended as a sanity check when + * attempting to add the task to a task manager. + * + * This function is called with the lock held. + */ +bool FunctionAsyncTask:: +is_runnable() { + return !!_function; +} + +/** + * Override this function to do something useful for the task. + * + * This function is called with the lock *not* held. + */ +AsyncTask::DoneStatus FunctionAsyncTask:: +do_task() { + nassertr(_function, DS_interrupt); + return _function(this); +} + +/** + * Override this function to do something useful when the task has been added + * to the active queue. + * + * This function is called with the lock *not* held. + */ +void FunctionAsyncTask:: +upon_birth(AsyncTaskManager *manager) { + AsyncTask::upon_birth(manager); + + if (_upon_birth) { + _upon_birth(this); + } +} + +/** + * Override this function to do something useful when the task has been + * removed from the active queue. The parameter clean_exit is true if the + * 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 + * 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 FunctionAsyncTask:: +upon_death(AsyncTaskManager *manager, bool clean_exit) { + AsyncTask::upon_death(manager, clean_exit); + + if (_upon_death) { + _upon_death(this, clean_exit); + } +} + +#endif // CPPPARSER diff --git a/panda/src/event/functionAsyncTask.h b/panda/src/event/functionAsyncTask.h new file mode 100644 index 0000000000..297d71c691 --- /dev/null +++ b/panda/src/event/functionAsyncTask.h @@ -0,0 +1,83 @@ +/** + * 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 functionAsyncTask.h + * @author rdb + * @date 2021-11-29 + */ + +#ifndef FUNCTIONASYNCTASK_H +#define FUNCTIONASYNCTASK_H + +#include "pandabase.h" + +#include "asyncTask.h" + +#ifndef CPPPARSER +#include + +/** + * Associates a generic std::function (eg. a lambda) with an AsyncTask object. + * You can use this when you want to create an AsyncTask without having to + * subclass. + * + * @since 1.11.0 + */ +class EXPCL_PANDA_EVENT FunctionAsyncTask final : public AsyncTask { +public: + typedef std::function TaskFunction; + typedef std::function BirthFunction; + typedef std::function DeathFunction; + + INLINE FunctionAsyncTask(const std::string &name = std::string()); + INLINE FunctionAsyncTask(const std::string &name, TaskFunction function); + ALLOC_DELETED_CHAIN(FunctionAsyncTask); + + INLINE void set_function(TaskFunction function); + INLINE const TaskFunction &get_function() const; + + INLINE void set_upon_birth(BirthFunction function); + INLINE const BirthFunction &get_upon_birth() const; + + INLINE void set_upon_death(DeathFunction function); + INLINE const DeathFunction &get_upon_death() const; + +protected: + virtual bool is_runnable() override; + virtual DoneStatus do_task() override; + virtual void upon_birth(AsyncTaskManager *manager) override; + virtual void upon_death(AsyncTaskManager *manager, bool clean_exit) override; + +private: + TaskFunction _function; + BirthFunction _upon_birth; + DeathFunction _upon_death; + +public: + static TypeHandle get_class_type() { + return _type_handle; + } + static void init_type() { + AsyncTask::init_type(); + register_type(_type_handle, "FunctionAsyncTask", + AsyncTask::get_class_type()); + } + virtual TypeHandle get_type() const override { + return get_class_type(); + } + virtual TypeHandle force_init_type() override {init_type(); return get_class_type();} + +private: + static TypeHandle _type_handle; +}; + +#include "functionAsyncTask.I" + +#endif // CPPPARSER + +#endif diff --git a/panda/src/event/genericAsyncTask.h b/panda/src/event/genericAsyncTask.h index f35d780e3b..ce7f635156 100644 --- a/panda/src/event/genericAsyncTask.h +++ b/panda/src/event/genericAsyncTask.h @@ -22,6 +22,8 @@ * Associates a generic C-style function pointer with an AsyncTask object. * You can use this when you want to create an AsyncTask without having to * subclass. + * + * @deprecated See FunctionAsyncTask instead, which is more powerful. */ class EXPCL_PANDA_EVENT GenericAsyncTask : public AsyncTask { public: diff --git a/panda/src/event/p3event_composite1.cxx b/panda/src/event/p3event_composite1.cxx index dc72d2b03f..34fcb9a0c9 100644 --- a/panda/src/event/p3event_composite1.cxx +++ b/panda/src/event/p3event_composite1.cxx @@ -7,6 +7,7 @@ #include "asyncTaskSequence.cxx" #include "buttonEvent.cxx" #include "buttonEventList.cxx" +#include "functionAsyncTask.cxx" #include "genericAsyncTask.cxx" #include "pointerEvent.cxx" #include "pointerEventList.cxx" diff --git a/panda/src/framework/windowFramework.cxx b/panda/src/framework/windowFramework.cxx index 454729889b..ae10964b54 100644 --- a/panda/src/framework/windowFramework.cxx +++ b/panda/src/framework/windowFramework.cxx @@ -1429,8 +1429,11 @@ create_anim_controls() { setup_shuttle_button("4", 2, st_play_button); setup_shuttle_button(":", 3, st_forward_button); - _update_anim_controls_task = new GenericAsyncTask("controls", st_update_anim_controls, (void *)this); - _panda_framework->get_task_mgr().add(_update_anim_controls_task); + AsyncTaskManager &task_mgr = _panda_framework->get_task_mgr(); + _update_anim_controls_task = task_mgr.add("controls", [this](AsyncTask *task) { + update_anim_controls(); + return AsyncTask::DS_cont; + }); } /** @@ -1443,7 +1446,7 @@ destroy_anim_controls() { _panda_framework->get_event_handler().remove_hooks_with((void *)this); if (_update_anim_controls_task != nullptr) { - _panda_framework->get_task_mgr().remove(_update_anim_controls_task); + _update_anim_controls_task->remove(); _update_anim_controls_task.clear(); } } @@ -1559,18 +1562,6 @@ forward_button() { control->pose(control->get_frame() + 1); } - -/** - * The static task function. - */ -AsyncTask::DoneStatus WindowFramework:: -st_update_anim_controls(GenericAsyncTask *, void *data) { - WindowFramework *self = (WindowFramework *)data; - self->update_anim_controls(); - return AsyncTask::DS_cont; -} - - /** * The static event handler function. */ diff --git a/panda/src/framework/windowFramework.h b/panda/src/framework/windowFramework.h index b6e93041eb..782989aad9 100644 --- a/panda/src/framework/windowFramework.h +++ b/panda/src/framework/windowFramework.h @@ -154,8 +154,6 @@ private: void play_button(); void forward_button(); - static AsyncTask::DoneStatus st_update_anim_controls(GenericAsyncTask *task, void *data); - static void st_back_button(const Event *, void *data); static void st_pause_button(const Event *, void *data); static void st_play_button(const Event *, void *data); @@ -183,7 +181,7 @@ private: PT(PGSliderBar) _anim_slider; PT(PGSliderBar) _play_rate_slider; PT(TextNode) _frame_number; - PT(GenericAsyncTask) _update_anim_controls_task; + PT(AsyncTask) _update_anim_controls_task; NodePath _mouse; NodePath _button_thrower; From 653cc3e091baed18300fd01120fc10339794f5aa Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 29 Nov 2021 14:48:06 +0100 Subject: [PATCH 51/68] event: Support C++11 lambdas as event handlers It's unfortunate that we now have three maps on EventHandler for three different types of functions. I'd love to unify them all under std::function, but the fact that it doesn't support comparison means the behavior would be different. Perhaps in the future we can create a new interface or deprecate the existing behaviors and unify everything under std::function. --- panda/src/event/eventHandler.cxx | 44 ++++++++++++++++++++++++++ panda/src/event/eventHandler.h | 14 ++++++++ panda/src/framework/pandaFramework.cxx | 33 +++++++++++++++++++ panda/src/framework/pandaFramework.h | 3 ++ 4 files changed, 94 insertions(+) diff --git a/panda/src/event/eventHandler.cxx b/panda/src/event/eventHandler.cxx index e3e389e1bf..5ad4bbe0e6 100644 --- a/panda/src/event/eventHandler.cxx +++ b/panda/src/event/eventHandler.cxx @@ -104,6 +104,20 @@ dispatch_event(const Event *event) { } } + // now for lambda hooks + LambdaHooks::const_iterator lhi; + lhi = _lambdahooks.find(event->get_name()); + + if (lhi != _lambdahooks.end()) { + // found one + const LambdaFunctions &functions = (*lhi).second; + + size_t num_functions = functions.size(); + for (size_t i = 0; i < num_functions; ++i) { + functions[i](event); + } + } + // Finally, check for futures that need to be triggered. Futures::iterator fi; fi = _futures.find(event->get_name()); @@ -189,6 +203,19 @@ add_hook(const string &event_name, EventCallbackFunction *function, return _cbhooks[event_name].insert(CallbackFunction(function, data)).second; } +/** + * Adds the indicated function to the list of those that will be called when + * the named event is thrown. Returns true if the function was successfully + * added, false if it was already defined on the indicated event name. This + * version stores an arbitrary C++ lambda. + */ +void EventHandler:: +add_hook(const string &event_name, EventLambda function) { + assert(!event_name.empty()); + assert(function); + _lambdahooks[event_name].push_back(function); +} + /** * Returns true if there is any hook added on the indicated event name, false * otherwise. @@ -212,6 +239,14 @@ has_hook(const string &event_name) const { } } + LambdaHooks::const_iterator lhi; + lhi = _lambdahooks.find(event_name); + if (lhi != _lambdahooks.end()) { + if (!(*lhi).second.empty()) { + return true; + } + } + return false; } @@ -306,6 +341,14 @@ remove_hooks(const string &event_name) { _cbhooks.erase(chi); } + LambdaHooks::iterator lhi = _lambdahooks.find(event_name); + if (lhi != _lambdahooks.end()) { + if (!(*lhi).second.empty()) { + any_removed = true; + } + _lambdahooks.erase(lhi); + } + return any_removed; } @@ -344,6 +387,7 @@ void EventHandler:: remove_all_hooks() { _hooks.clear(); _cbhooks.clear(); + _lambdahooks.clear(); } /** diff --git a/panda/src/event/eventHandler.h b/panda/src/event/eventHandler.h index 8e616f1581..4aa3d0924e 100644 --- a/panda/src/event/eventHandler.h +++ b/panda/src/event/eventHandler.h @@ -23,6 +23,10 @@ #include "pset.h" #include "pmap.h" +#ifndef CPPPARSER +#include +#endif + class EventQueue; /** @@ -40,6 +44,12 @@ public: typedef void EventFunction(const Event *); typedef void EventCallbackFunction(const Event *, void *); +#ifdef CPPPARSER + typedef EventFunction EventLambda; +#else + typedef std::function EventLambda; +#endif + PUBLISHED: explicit EventHandler(EventQueue *ev_queue); ~EventHandler() {} @@ -58,6 +68,7 @@ public: bool add_hook(const std::string &event_name, EventFunction *function); bool add_hook(const std::string &event_name, EventCallbackFunction *function, void *data); + void add_hook(const std::string &event_name, EventLambda function); bool has_hook(const std::string &event_name) const; bool has_hook(const std::string &event_name, EventFunction *function) const; bool has_hook(const std::string &event_name, EventCallbackFunction *function, @@ -78,10 +89,13 @@ protected: typedef std::pair CallbackFunction; typedef pset CallbackFunctions; typedef pmap CallbackHooks; + typedef pvector LambdaFunctions; + typedef pmap LambdaHooks; typedef pmap Futures; Hooks _hooks; CallbackHooks _cbhooks; + LambdaHooks _lambdahooks; Futures _futures; EventQueue &_queue; diff --git a/panda/src/framework/pandaFramework.cxx b/panda/src/framework/pandaFramework.cxx index c046deef8a..6c0fb681fc 100644 --- a/panda/src/framework/pandaFramework.cxx +++ b/panda/src/framework/pandaFramework.cxx @@ -310,6 +310,39 @@ define_key(const string &event_name, const string &description, } } +/** + * Sets up a handler for the indicated key. When the key is pressed in a + * window, the given callback will be called. The description is a one-line + * description of the function of the key, for display to the user. + */ +void PandaFramework:: +define_key(const string &event_name, const string &description, + EventHandler::EventLambda function) { + if (_event_handler.has_hook(event_name)) { + // If there is already a hook for the indicated keyname, we're most likely + // replacing a previous definition of a key. Search for the old + // definition and remove it. + KeyDefinitions::iterator di; + di = _key_definitions.begin(); + while (di != _key_definitions.end() && (*di)._event_name != event_name) { + ++di; + } + if (di != _key_definitions.end()) { + _key_definitions.erase(di); + } + } + + // Now add a new hook for the keyname, and also add the new description. + _event_handler.add_hook(event_name, function); + + if (!description.empty()) { + KeyDefinition keydef; + keydef._event_name = event_name; + keydef._description = description; + _key_definitions.push_back(keydef); + } +} + /** * Fills in the indicated window properties structure according to the normal * window properties for this application. diff --git a/panda/src/framework/pandaFramework.h b/panda/src/framework/pandaFramework.h index 3c901d8e27..18747dcfa1 100644 --- a/panda/src/framework/pandaFramework.h +++ b/panda/src/framework/pandaFramework.h @@ -56,6 +56,9 @@ public: const std::string &description, EventHandler::EventCallbackFunction *function, void *data); + void define_key(const std::string &event_name, + const std::string &description, + EventHandler::EventLambda function); INLINE void set_window_title(const std::string &title); virtual void get_default_window_props(WindowProperties &props); From ab9830b6ffe927dc616afc992bbf8796833f0398 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 29 Nov 2021 14:49:41 +0100 Subject: [PATCH 52/68] egg-qtess: Squelch compiler warning --- pandatool/src/egg-qtess/isoPlacer.cxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pandatool/src/egg-qtess/isoPlacer.cxx b/pandatool/src/egg-qtess/isoPlacer.cxx index fda96904e3..f37588f463 100644 --- a/pandatool/src/egg-qtess/isoPlacer.cxx +++ b/pandatool/src/egg-qtess/isoPlacer.cxx @@ -109,11 +109,13 @@ place(int count, pvector &iso_points) { int i; // Count up the average curvature. + /* double avg_curve = 0.0; for (i = 0; i < _maxi; i++) { avg_curve += _cscore[i]; } avg_curve /= (double)_maxi; + */ // Find all the local maxima in the curvature table. These are bend points. typedef pvector BendPoints; From b42fd4ee4f77af8934a278e2f3c7b9d83cf14c1f Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 29 Nov 2021 14:50:01 +0100 Subject: [PATCH 53/68] makepanda: Don't copy over old Python 2-only Pmw versions --- makepanda/makepanda.py | 6 ++++-- makepanda/makepandacore.py | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index adbd685f13..145ffe19df 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -3071,8 +3071,10 @@ else: if not PkgSkip("PANDATOOL"): CopyAllFiles(GetOutputDir()+"/plugins/", "pandatool/src/scripts/", ".mel") CopyAllFiles(GetOutputDir()+"/plugins/", "pandatool/src/scripts/", ".ms") -if not PkgSkip("PYTHON") and os.path.isdir(GetThirdpartyBase()+"/Pmw"): - CopyTree(GetOutputDir()+'/Pmw', GetThirdpartyBase()+'/Pmw') + +if not PkgSkip("PYTHON") and os.path.isdir(GetThirdpartyBase() + "/Pmw"): + CopyTree(GetOutputDir() + "/Pmw", GetThirdpartyBase() + "/Pmw", exclude=["Pmw_1_3", "Pmw_1_3_3"]) + ConditionalWriteFile(GetOutputDir()+'/include/ctl3d.h', '/* dummy file to make MAX happy */') # Since Eigen is included by all sorts of core headers, as a convenience diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index d86620e18f..2201ddbdc5 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -3104,13 +3104,16 @@ def CopyAllHeaders(dir, skip=[]): WriteBinaryFile(dstfile, ReadBinaryFile(srcfile)) JustBuilt([dstfile], [srcfile]) -def CopyTree(dstdir, srcdir, omitVCS=True): +def CopyTree(dstdir, srcdir, omitVCS=True, exclude=()): if os.path.isdir(dstdir): source_entries = os.listdir(srcdir) for entry in source_entries: srcpth = os.path.join(srcdir, entry) dstpth = os.path.join(dstdir, entry) + if entry in exclude: + continue + if os.path.islink(srcpth) or os.path.isfile(srcpth): if not omitVCS or entry not in VCS_FILES: CopyFile(dstpth, srcpth) @@ -3120,7 +3123,7 @@ def CopyTree(dstdir, srcdir, omitVCS=True): # Delete files in dstdir that are not in srcdir. for entry in os.listdir(dstdir): - if entry not in source_entries: + if entry not in source_entries or entry in exclude: path = os.path.join(dstdir, entry) if os.path.islink(path) or os.path.isfile(path): os.remove(path) @@ -3136,6 +3139,13 @@ def CopyTree(dstdir, srcdir, omitVCS=True): if subprocess.call(['cp', '-R', '-f', srcdir, dstdir]) != 0: exit("Copy failed.") + for entry in exclude: + path = os.path.join(dstdir, entry) + if os.path.islink(path) or os.path.isfile(path): + os.remove(path) + elif os.path.isdir(path): + shutil.rmtree(path) + if omitVCS: DeleteVCS(dstdir) From 9e7ca636626e9b393e0e65a6b06daf1124f69cb0 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 29 Nov 2021 16:01:44 +0100 Subject: [PATCH 54/68] Add missing @since to docstrings of some new methods --- panda/src/display/displayRegion.I | 2 ++ panda/src/event/asyncTaskManager.I | 2 ++ panda/src/pgraph/depthBiasAttrib.h | 2 ++ 3 files changed, 6 insertions(+) diff --git a/panda/src/display/displayRegion.I b/panda/src/display/displayRegion.I index ef18ab693e..f77a78bf31 100644 --- a/panda/src/display/displayRegion.I +++ b/panda/src/display/displayRegion.I @@ -160,6 +160,8 @@ set_dimensions(const LVecBase4 &dimensions) { * Changes the range of the depth buffer this DisplayRegion writes to. * The parameters range from 0 to 1. It is legal for the near value to be * larger than the far value. + * + * @since 1.11.0 */ INLINE void DisplayRegion:: set_depth_range(PN_stdfloat near_depth, PN_stdfloat far_depth) { diff --git a/panda/src/event/asyncTaskManager.I b/panda/src/event/asyncTaskManager.I index 4af23b4bcf..6be7ef3508 100644 --- a/panda/src/event/asyncTaskManager.I +++ b/panda/src/event/asyncTaskManager.I @@ -36,6 +36,8 @@ get_clock() { /** * Adds a new task which calls the indicated function to the task manager. * Returns the newly created FunctionAsyncTask object. + * + * @since 1.11.0 */ INLINE AsyncTask *AsyncTaskManager:: add(const std::string &name, FunctionAsyncTask::TaskFunction function) { diff --git a/panda/src/pgraph/depthBiasAttrib.h b/panda/src/pgraph/depthBiasAttrib.h index 75fcdd3dc9..fea972bd81 100644 --- a/panda/src/pgraph/depthBiasAttrib.h +++ b/panda/src/pgraph/depthBiasAttrib.h @@ -37,6 +37,8 @@ class FactoryParams; * with a value of 1 beneath another DepthBiasAttrib with a value of 2 * presents a net offset of 3. (A DepthBiasAttrib will not, however, * combine with any other DepthBiasAttribs with a lower override parameter.) + * + * @since 1.11.0 */ class EXPCL_PANDA_PGRAPH DepthBiasAttrib : public RenderAttrib { private: From fa0ea312ea3d50e9c663b6e1d17868a885342f09 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 30 Nov 2021 10:32:33 +0100 Subject: [PATCH 55/68] gobj: Move TextureReloadRequest to new Texture::async_ensure_ram_image() The task is implemented with just a simple lambda, much more compact than a whole TextureReloadRequest task. The latter is now deprecated. Since Loader can't be used within Texture, there's now a new task chain, configurable with texture-reload-num-threads and texture-reload-thread-priority. This does mean that it no longer happens on the same thread as model loads, but I think that's fine, and perhaps texture reloads should be higher priority than model loads anyway since a long texture reload delay with allow-incomplete-render directly and negatively affects user experience during gameplay. The new name also better communicates that it just calls get_ram_image(), it doesn't force a reload, but we could add an async_reload() for that if we want. --- panda/src/display/graphicsStateGuardian.cxx | 27 +------- panda/src/gobj/config_gobj.cxx | 17 +++++ panda/src/gobj/config_gobj.h | 3 + panda/src/gobj/texture.cxx | 70 +++++++++++++++++++++ panda/src/gobj/texture.h | 6 +- panda/src/gobj/textureReloadRequest.h | 2 + 6 files changed, 99 insertions(+), 26 deletions(-) diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx index 3e73649ad1..797dba0fcd 100644 --- a/panda/src/display/graphicsStateGuardian.cxx +++ b/panda/src/display/graphicsStateGuardian.cxx @@ -3581,31 +3581,8 @@ async_reload_texture(TextureContext *tc) { priority = _current_display_region->get_texture_reload_priority(); } - string task_name = string("reload:") + tc->get_texture()->get_name(); - PT(AsyncTaskManager) task_mgr = _loader->get_task_manager(); - - // See if we are already loading this task. - AsyncTaskCollection orig_tasks = task_mgr->find_tasks(task_name); - 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()) && - ((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(std::max(task->get_priority(), priority)); - return (AsyncFuture *)task; - } - } - - // This texture has not yet been queued to be reloaded. Queue it up now. - PT(AsyncTask) request = - new TextureReloadRequest(task_name, - _prepared_objects, tc->get_texture(), - _supports_compressed_texture); - request->set_priority(priority); - _loader->load_async(request); - return (AsyncFuture *)request.p(); + Texture *tex = tc->get_texture(); + return tex->async_ensure_ram_image(_supports_compressed_texture, priority); } /** diff --git a/panda/src/gobj/config_gobj.cxx b/panda/src/gobj/config_gobj.cxx index b093925367..f3e8adbaf8 100644 --- a/panda/src/gobj/config_gobj.cxx +++ b/panda/src/gobj/config_gobj.cxx @@ -366,6 +366,23 @@ ConfigVariableDouble simple_image_threshold "simple images. Generally the value should be considerably " "less than 1.")); +ConfigVariableInt texture_reload_num_threads + ("texture-reload-num-threads", 1, + PRC_DESC("The number of threads that will be started by the Texture class " + "to reload textures asynchronously. These threads will only be " + "started if the asynchronous interface is used, and if threading " + "support is compiled into Panda. The default is one thread, " + "which allows textures to be loaded one at a time in a single " + "asychronous thread. You can set this higher, particularly if " + "you have many CPU's available, to allow loading multiple models " + "simultaneously.")); + +ConfigVariableEnum texture_reload_thread_priority + ("texture-reload-thread-priority", TP_normal, + PRC_DESC("The default thread priority to assign to the threads created for " + "asynchronous texture loading. The default is 'normal'; you may " + "also specify 'low', 'high', or 'urgent'.")); + ConfigVariableInt geom_cache_size ("geom-cache-size", 5000, PRC_DESC("Specifies the maximum number of entries in the cache " diff --git a/panda/src/gobj/config_gobj.h b/panda/src/gobj/config_gobj.h index f44df52a3c..1713a56751 100644 --- a/panda/src/gobj/config_gobj.h +++ b/panda/src/gobj/config_gobj.h @@ -24,6 +24,7 @@ #include "configVariableString.h" #include "configVariableList.h" #include "autoTextureScale.h" +#include "threadPriority.h" NotifyCategoryDecl(gobj, EXPCL_PANDA_GOBJ, EXPTP_PANDA_GOBJ); NotifyCategoryDecl(shader, EXPCL_PANDA_GOBJ, EXPTP_PANDA_GOBJ); @@ -62,6 +63,8 @@ extern EXPCL_PANDA_GOBJ ConfigVariableBool textures_auto_power_2; extern EXPCL_PANDA_GOBJ ConfigVariableBool textures_header_only; extern EXPCL_PANDA_GOBJ ConfigVariableInt simple_image_size; extern EXPCL_PANDA_GOBJ ConfigVariableDouble simple_image_threshold; +extern EXPCL_PANDA_GOBJ ConfigVariableInt texture_reload_num_threads; +extern EXPCL_PANDA_GOBJ ConfigVariableEnum texture_reload_thread_priority; extern EXPCL_PANDA_GOBJ ConfigVariableInt geom_cache_size; extern EXPCL_PANDA_GOBJ ConfigVariableInt geom_cache_min_frames; diff --git a/panda/src/gobj/texture.cxx b/panda/src/gobj/texture.cxx index bc6d218e38..9633379a82 100644 --- a/panda/src/gobj/texture.cxx +++ b/panda/src/gobj/texture.cxx @@ -43,6 +43,7 @@ #include "streamReader.h" #include "texturePeeker.h" #include "convert_srgb.h" +#include "asyncTaskManager.h" #ifdef HAVE_SQUISH #include @@ -1019,6 +1020,69 @@ load_related(const InternalName *suffix) const { return res; } +/** + * Schedules a background task that reloads the the Texture from its disk file + * if there is not currently a RAM image (or uncompressed RAM image, if + * allow_compression is false). + * + * A higher priority value indicates that this texture should be reloaded sooner + * than textures with a lower priority value. If the reload hasn't taken place + * yet, you can call this again to update the priority value. + * + * If someone else reloads the texture using an explicit call to reload() while + * an async reload request is pending, the async reload request is cancelled. + */ +PT(AsyncFuture) Texture:: +async_ensure_ram_image(bool allow_compression, int priority) { + CDLockedReader cdata(_cycler); + if (allow_compression ? do_has_ram_image(cdata) : do_has_uncompressed_ram_image(cdata)) { + // We already have a RAM image. + PT(AsyncFuture) fut = new AsyncFuture; + fut->set_result(nullptr); + return fut; + } + if (!do_can_reload(cdata)) { + // We don't have a filename to load from. This is an error. + return nullptr; + } + + AsyncTask *task = cdata->_reload_task; + if (task != nullptr) { + // 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(std::max(task->get_priority(), priority)); + return (AsyncFuture *)task; + } + + CDWriter cdataw(_cycler, cdata, true); + + string task_name = string("reload:") + get_name(); + AsyncTaskManager *task_mgr = AsyncTaskManager::get_global_ptr(); + static PT(AsyncTaskChain) chain = task_mgr->make_task_chain("texture_reload"); + chain->set_num_threads(texture_reload_num_threads); + chain->set_thread_priority(texture_reload_thread_priority); + + double delay = async_load_delay; + + // This texture has not yet been queued to be reloaded. Queue it up now. + task = new FunctionAsyncTask(task_name, [=](AsyncTask *task) { + if (delay != 0.0) { + Thread::sleep(delay); + } + if (allow_compression) { + get_ram_image(); + } else { + get_uncompressed_ram_image(); + } + return AsyncTask::DS_done; + }); + task->set_task_chain("texture_reload"); + task->set_priority(priority); + task_mgr->add(task); + cdataw->_reload_task = task; + return (AsyncFuture *)task; +} + /** * Replaces the current system-RAM image with the new data, converting it * first if necessary from the indicated component-order format. See @@ -5611,6 +5675,12 @@ do_reload_ram_image(CData *cdata, bool allow_compression) { cache->store(record); } } + + // Remove any pending asynchronous reload operation. + if (cdata->_reload_task != nullptr) { + cdata->_reload_task->remove(); + cdata->_reload_task = nullptr; + } } /** diff --git a/panda/src/gobj/texture.h b/panda/src/gobj/texture.h index 0254db323a..3c78553c4c 100644 --- a/panda/src/gobj/texture.h +++ b/panda/src/gobj/texture.h @@ -45,7 +45,7 @@ #include "bamCacheRecord.h" #include "pnmImage.h" #include "pfmFile.h" -#include "asyncFuture.h" +#include "asyncTask.h" class TextureContext; class FactoryParams; @@ -448,6 +448,7 @@ PUBLISHED: MAKE_PROPERTY(expected_ram_image_size, get_expected_ram_image_size); MAKE_PROPERTY(expected_ram_page_size, get_expected_ram_page_size); + PT(AsyncFuture) async_ensure_ram_image(bool allow_compression = true, int priority = 0); INLINE CPTA_uchar get_ram_image(); INLINE CompressionMode get_ram_image_compression() const; INLINE CPTA_uchar get_uncompressed_ram_image(); @@ -784,6 +785,7 @@ protected: void do_set_pad_size(CData *cdata, int x, int y, int z); virtual bool do_can_reload(const CData *cdata) const; bool do_reload(CData *cdata); + AsyncFuture *do_async_ensure_ram_image(const CData *cdata, bool allow_compression, int priority); INLINE AutoTextureScale do_get_auto_texture_scale(const CData *cdata) const; @@ -1032,6 +1034,8 @@ protected: ModifiedPageRanges _modified_pages; + PT(AsyncTask) _reload_task; + public: static TypeHandle get_class_type() { return _type_handle; diff --git a/panda/src/gobj/textureReloadRequest.h b/panda/src/gobj/textureReloadRequest.h index 625ac3ce3e..1010cd7ec5 100644 --- a/panda/src/gobj/textureReloadRequest.h +++ b/panda/src/gobj/textureReloadRequest.h @@ -27,6 +27,8 @@ * force the texture's image to be re-read from disk. It is used by * GraphicsStateGuardian::async_reload_texture(), when get_incomplete_render() * is true. + * + * @deprecated Use Texture::async_ensure_ram_image() instead. */ class EXPCL_PANDA_GOBJ TextureReloadRequest : public AsyncTask { public: From 37b45a010d8d528114fded6e3f56ecd209346af1 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 30 Nov 2021 10:56:48 +0100 Subject: [PATCH 56/68] gobj: More efficient tracking of simple image changes Remove the separate simple_image_modified field, instead, the image_modified value is updated if the simple image is changed while there is no RAM image. This robustifies the tracking on the GSG end, which previously ended up repeatedly calling upload_texture() unnecessarily --- panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx | 5 +-- panda/src/dxgsg9/dxTextureContext9.cxx | 2 +- .../glstuff/glGraphicsStateGuardian_src.cxx | 22 +++++++++---- panda/src/glstuff/glTextureContext_src.I | 1 + panda/src/glstuff/glTextureContext_src.cxx | 1 + panda/src/glstuff/glTextureContext_src.h | 1 + panda/src/gobj/texture.I | 26 ++++----------- panda/src/gobj/texture.cxx | 21 ++++-------- panda/src/gobj/texture.h | 4 --- panda/src/gobj/textureContext.I | 33 ------------------- panda/src/gobj/textureContext.h | 4 --- .../tinydisplay/tinyGraphicsStateGuardian.cxx | 7 ++-- 12 files changed, 36 insertions(+), 91 deletions(-) diff --git a/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx b/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx index 35f86ae3d0..371a73f47e 100644 --- a/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx +++ b/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx @@ -357,10 +357,7 @@ upload_texture(DXTextureContext9 *dtc, bool force) { async_reload_texture(dtc); has_image = _supports_compressed_texture ? tex->has_ram_image() : tex->has_uncompressed_ram_image(); if (!has_image) { - if (dtc->was_simple_image_modified()) { - return dtc->create_simple_texture(*_screen); - } - return true; + return dtc->create_simple_texture(*_screen); } } } diff --git a/panda/src/dxgsg9/dxTextureContext9.cxx b/panda/src/dxgsg9/dxTextureContext9.cxx index 2c90a56910..19a094fc45 100644 --- a/panda/src/dxgsg9/dxTextureContext9.cxx +++ b/panda/src/dxgsg9/dxTextureContext9.cxx @@ -1157,7 +1157,7 @@ create_simple_texture(DXScreenData &scrn) { goto error_exit; } - mark_simple_loaded(); + mark_loaded(); return true; error_exit: diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 30541e4a0a..61480ec021 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -7420,6 +7420,7 @@ framebuffer_copy_to_texture(Texture *tex, int view, int z, } gtc->_has_storage = true; + gtc->_simple_loaded = false; gtc->_uses_mipmaps = uses_mipmaps; gtc->_internal_format = internal_format; gtc->_width = width; @@ -13135,7 +13136,7 @@ apply_sampler(GLuint unit, const SamplerState &sampler, CLP(TextureContext) *gtc } } - if (sampler.uses_mipmaps() && !gtc->_uses_mipmaps && !gl_ignore_mipmaps) { + if (sampler.uses_mipmaps() && !gtc->_uses_mipmaps && !gtc->_simple_loaded && !gl_ignore_mipmaps) { // The texture wasn't created with mipmaps, but we are trying to sample it // with mipmaps. We will need to reload it. GLCAT.info() @@ -13178,10 +13179,7 @@ upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps) { async_reload_texture(gtc); has_image = _supports_compressed_texture ? tex->has_ram_image() : tex->has_uncompressed_ram_image(); if (!has_image) { - if (gtc->was_simple_image_modified()) { - return upload_simple_texture(gtc); - } - return true; + return upload_simple_texture(gtc); } } } @@ -13551,6 +13549,7 @@ upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps) { } gtc->_has_storage = true; + gtc->_simple_loaded = false; gtc->_immutable = true; gtc->_uses_mipmaps = uses_mipmaps; gtc->_internal_format = internal_format; @@ -13598,6 +13597,7 @@ upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps) { if (success) { if (needs_reload) { gtc->_has_storage = true; + gtc->_simple_loaded = false; gtc->_uses_mipmaps = uses_mipmaps; gtc->_internal_format = internal_format; gtc->_width = width; @@ -14234,7 +14234,17 @@ upload_simple_texture(CLP(TextureContext) *gtc) { width, height, 0, external_format, component_type, image_ptr); - gtc->mark_simple_loaded(); + gtc->_has_storage = true; + gtc->_simple_loaded = true; + gtc->_immutable = false; + gtc->_uses_mipmaps = false; + gtc->_internal_format = internal_format; + gtc->_width = width; + gtc->_height = height; + gtc->_depth = 1; + gtc->update_data_size_bytes(width * height * 4); + + gtc->mark_loaded(); report_my_gl_errors(); return true; diff --git a/panda/src/glstuff/glTextureContext_src.I b/panda/src/glstuff/glTextureContext_src.I index 33bda9a58f..51db7538ba 100644 --- a/panda/src/glstuff/glTextureContext_src.I +++ b/panda/src/glstuff/glTextureContext_src.I @@ -29,6 +29,7 @@ CLP(TextureContext)(CLP(GraphicsStateGuardian) *glgsg, _handle_resident = false; #endif _has_storage = false; + _simple_loaded = false; _immutable = false; _uses_mipmaps = false; _generate_mipmaps = false; diff --git a/panda/src/glstuff/glTextureContext_src.cxx b/panda/src/glstuff/glTextureContext_src.cxx index e36f8f756e..37bad3ae3a 100644 --- a/panda/src/glstuff/glTextureContext_src.cxx +++ b/panda/src/glstuff/glTextureContext_src.cxx @@ -82,6 +82,7 @@ reset_data() { _handle_resident = false; #endif _has_storage = false; + _simple_loaded = false; _immutable = false; #ifndef OPENGLES_1 diff --git a/panda/src/glstuff/glTextureContext_src.h b/panda/src/glstuff/glTextureContext_src.h index 667e356d60..41a89cb32b 100644 --- a/panda/src/glstuff/glTextureContext_src.h +++ b/panda/src/glstuff/glTextureContext_src.h @@ -64,6 +64,7 @@ public: // or glTexStorage2D() call. If none of these have changed, we can reload // the texture image with a glTexSubImage2D(). bool _has_storage; + bool _simple_loaded; bool _immutable; bool _uses_mipmaps; bool _generate_mipmaps; diff --git a/panda/src/gobj/texture.I b/panda/src/gobj/texture.I index 223a86aab5..6409ef8f26 100644 --- a/panda/src/gobj/texture.I +++ b/panda/src/gobj/texture.I @@ -28,7 +28,6 @@ make_copy() const { CDWriter cdata_tex(tex->_cycler, true); cdata_tex->inc_properties_modified(); cdata_tex->inc_image_modified(); - cdata_tex->inc_simple_image_modified(); return tex; } @@ -232,7 +231,6 @@ clear_image() { do_clear_ram_image(cdata); do_clear_simple_ram_image(cdata); cdata->inc_image_modified(); - cdata->inc_simple_image_modified(); } /** @@ -1766,6 +1764,12 @@ INLINE void Texture:: set_simple_ram_image(CPTA_uchar image, int x_size, int y_size) { CDWriter cdata(_cycler, true); do_set_simple_ram_image(cdata, image, x_size, y_size); + + // If we don't have a RAM image currently, we need to update this, to let the + // GSG know that it needs to update the simple image it is currently using. + if (!do_has_ram_image(cdata)) { + cdata->inc_image_modified(); + } } /** @@ -1797,16 +1801,6 @@ get_image_modified() const { return cdata->_image_modified; } -/** - * Returns a sequence number which is guaranteed to change at least every time - * the texture's "simple" image data is modified. - */ -INLINE UpdateSeq Texture:: -get_simple_image_modified() const { - CDReader cdata(_cycler); - return cdata->_simple_image_modified; -} - /** * Specifies the power-of-2 texture-scaling mode that will be applied to this * particular texture when it is next loaded from disk. See @@ -2466,14 +2460,6 @@ inc_image_modified() { _modified_pages[0]._modified = _image_modified; } -/** - * - */ -INLINE void Texture::CData:: -inc_simple_image_modified() { - ++_simple_image_modified; -} - /** * */ diff --git a/panda/src/gobj/texture.cxx b/panda/src/gobj/texture.cxx index 9633379a82..d065d2dcd8 100644 --- a/panda/src/gobj/texture.cxx +++ b/panda/src/gobj/texture.cxx @@ -1378,7 +1378,12 @@ new_simple_ram_image(int x_size, int y_size) { cdata->_simple_ram_image._image = PTA_uchar::empty_array(expected_page_size); cdata->_simple_ram_image._page_size = expected_page_size; cdata->_simple_image_date_generated = (int32_t)time(nullptr); - cdata->inc_simple_image_modified(); + + // If we don't have a RAM image currently, we need to update this, to let the + // GSG know that it needs to update the simple image it is currently using. + if (!do_has_ram_image(cdata)) { + cdata->inc_image_modified(); + } return cdata->_simple_ram_image._image; } @@ -5525,7 +5530,6 @@ unlocked_ensure_ram_image(bool allow_compression) { cdataw->_component_type = cdata_tex->_component_type; cdataw->inc_properties_modified(); - cdataw->inc_image_modified(); } cdataw->_keep_ram_image = cdata_tex->_keep_ram_image; @@ -5535,9 +5539,7 @@ unlocked_ensure_ram_image(bool allow_compression) { nassertr(_reloading, nullptr); _reloading = false; - // We don't generally increment the cdata->_image_modified semaphore, - // because this is just a reload, and presumably the image hasn't changed - // (unless we hit the if condition above). + cdataw->inc_image_modified(); _cvar.notify_all(); @@ -7007,7 +7009,6 @@ do_clear(CData *cdata) { cdata->inc_properties_modified(); cdata->inc_image_modified(); - cdata->inc_simple_image_modified(); } /** @@ -7696,7 +7697,6 @@ do_set_simple_ram_image(CData *cdata, CPTA_uchar image, int x_size, int y_size) cdata->_simple_ram_image._image = image.cast_non_const(); cdata->_simple_ram_image._page_size = image.size(); cdata->_simple_image_date_generated = (int32_t)time(nullptr); - cdata->inc_simple_image_modified(); } /** @@ -7791,11 +7791,6 @@ do_clear_simple_ram_image(CData *cdata) { cdata->_simple_ram_image._image.clear(); cdata->_simple_ram_image._page_size = 0; cdata->_simple_image_date_generated = 0; - - // We allow this exception: we update the _simple_image_modified here, since - // no one really cares much about that anyway, and it's convenient to do it - // here. - cdata->inc_simple_image_modified(); } /** @@ -10610,7 +10605,6 @@ do_fillin_body(CData *cdata, DatagramIterator &scan, BamReader *manager) { cdata->_simple_ram_image._image = image; cdata->_simple_ram_image._page_size = u_size; - cdata->inc_simple_image_modified(); } if (manager->get_file_minor_ver() >= 45) { @@ -10808,7 +10802,6 @@ CData(const Texture::CData ©) { _properties_modified = copy._properties_modified; _image_modified = copy._image_modified; - _simple_image_modified = copy._simple_image_modified; _modified_pages = copy._modified_pages; } diff --git a/panda/src/gobj/texture.h b/panda/src/gobj/texture.h index 3c78553c4c..de3d90e5ea 100644 --- a/panda/src/gobj/texture.h +++ b/panda/src/gobj/texture.h @@ -521,10 +521,8 @@ PUBLISHED: INLINE UpdateSeq get_properties_modified() const; INLINE UpdateSeq get_image_modified() const; - INLINE UpdateSeq get_simple_image_modified() const; MAKE_PROPERTY(properties_modified, get_properties_modified); MAKE_PROPERTY(image_modified, get_image_modified); - MAKE_PROPERTY(simple_image_modified, get_simple_image_modified); SparseArray get_image_modified_pages(UpdateSeq since, int n = 0) const; @@ -961,7 +959,6 @@ protected: INLINE void inc_properties_modified(); INLINE void inc_image_modified(); void inc_image_page_modified(int z); - INLINE void inc_simple_image_modified(); Filename _filename; Filename _alpha_filename; @@ -1030,7 +1027,6 @@ protected: UpdateSeq _properties_modified; UpdateSeq _image_modified; - UpdateSeq _simple_image_modified; ModifiedPageRanges _modified_pages; diff --git a/panda/src/gobj/textureContext.I b/panda/src/gobj/textureContext.I index a4be4a4935..70836cdb5b 100644 --- a/panda/src/gobj/textureContext.I +++ b/panda/src/gobj/textureContext.I @@ -67,15 +67,6 @@ was_image_modified() const { return _image_modified != get_texture()->get_image_modified(); } -/** - * Returns true if the texture's "simple" image has been modified since the - * last time mark_simple_loaded() was called. - */ -INLINE bool TextureContext:: -was_simple_image_modified() const { - return _simple_image_modified != get_texture()->get_simple_image_modified(); -} - /** * Returns true if the given page of the texture image has been modified since * the last time mark_loaded() was called. @@ -103,15 +94,6 @@ get_image_modified() const { return _image_modified; } -/** - * Returns a sequence number which is guaranteed to change at least every time - * the texture's "simple" image data is modified. - */ -INLINE UpdateSeq TextureContext:: -get_simple_image_modified() const { - return _simple_image_modified; -} - /** * Returns a SparseArray indicating which pages of the texture have been * modified since the last call to mark_loaded(). @@ -146,20 +128,6 @@ mark_loaded() { set_resident(true); } -/** - * Should be called after the texture's "simple" image has been loaded into - * graphics memory. - */ -INLINE void TextureContext:: -mark_simple_loaded() { - _properties_modified = get_texture()->get_properties_modified(); - _simple_image_modified = get_texture()->get_simple_image_modified(); - update_modified(std::max(_properties_modified, _simple_image_modified)); - - // The texture's not exactly resident now, but some part of it is. - set_resident(true); -} - /** * Should be called after the texture has been forced out of texture memory. */ @@ -167,7 +135,6 @@ INLINE void TextureContext:: mark_unloaded() { _properties_modified = UpdateSeq::old(); _image_modified = UpdateSeq::old(); - _simple_image_modified = UpdateSeq::old(); update_modified(UpdateSeq::old()); set_resident(false); diff --git a/panda/src/gobj/textureContext.h b/panda/src/gobj/textureContext.h index b531c49504..de828781e2 100644 --- a/panda/src/gobj/textureContext.h +++ b/panda/src/gobj/textureContext.h @@ -43,19 +43,16 @@ PUBLISHED: INLINE bool was_modified() const; INLINE bool was_properties_modified() const; INLINE bool was_image_modified() const; - INLINE bool was_simple_image_modified() const; INLINE bool was_image_page_modified(int z, int n) const; INLINE UpdateSeq get_properties_modified() const; INLINE UpdateSeq get_image_modified() const; - INLINE UpdateSeq get_simple_image_modified() const; INLINE SparseArray get_image_modified_pages(int n = 0) const; public: INLINE void update_data_size_bytes(size_t new_data_size_bytes); INLINE void mark_loaded(); - INLINE void mark_simple_loaded(); INLINE void mark_unloaded(); INLINE void mark_needs_reload(); @@ -66,7 +63,6 @@ private: int _view; UpdateSeq _properties_modified; UpdateSeq _image_modified; - UpdateSeq _simple_image_modified; public: static TypeHandle get_class_type() { diff --git a/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx b/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx index d8e80db919..8442cae415 100644 --- a/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx +++ b/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx @@ -2387,10 +2387,7 @@ upload_texture(TinyTextureContext *gtc, bool force, bool uses_mipmaps) { // meantime load a temporary simple image in its place. async_reload_texture(gtc); if (!tex->has_ram_image()) { - if (gtc->was_simple_image_modified()) { - return upload_simple_texture(gtc); - } - return true; + return upload_simple_texture(gtc); } } } @@ -2558,7 +2555,7 @@ upload_simple_texture(TinyTextureContext *gtc) { ZTextureLevel *dest = &gltex->levels[0]; memcpy(dest->pixmap, image_ptr, image_size); - gtc->mark_simple_loaded(); + gtc->mark_loaded(); return true; } From b1b05c2be96e0ffb3a036fedcc1f982f98b4287f Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 1 Dec 2021 11:31:45 +0100 Subject: [PATCH 57/68] thread: Allow arbitrary std::function (incl. lambdas) in GenericThread --- panda/src/pipeline/genericThread.I | 23 +++-------------------- panda/src/pipeline/genericThread.cxx | 23 +++++++++++++++++------ panda/src/pipeline/genericThread.h | 18 ++++++++++-------- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/panda/src/pipeline/genericThread.I b/panda/src/pipeline/genericThread.I index 11634fd226..a7b8fbb047 100644 --- a/panda/src/pipeline/genericThread.I +++ b/panda/src/pipeline/genericThread.I @@ -15,31 +15,14 @@ * Replaces the function that is called when the thread runs. */ INLINE void GenericThread:: -set_function(GenericThread::ThreadFunc *function) { - _function = function; +set_function(std::function function) { + _function = std::move(function); } /** * Returns the function that is called when the thread runs. */ -INLINE GenericThread::ThreadFunc *GenericThread:: +INLINE const std::function &GenericThread:: get_function() const { return _function; } - -/** - * Replaces the void pointer that is passed to the thread function. This is - * any arbitrary pointer; the thread object does no processing on it. - */ -INLINE void GenericThread:: -set_user_data(void *user_data) { - _user_data = user_data; -} - -/** - * Returns the void pointer that is passed to the thread function. - */ -INLINE void *GenericThread:: -get_user_data() const { - return _user_data; -} diff --git a/panda/src/pipeline/genericThread.cxx b/panda/src/pipeline/genericThread.cxx index 64eb0b4814..3d5248667f 100644 --- a/panda/src/pipeline/genericThread.cxx +++ b/panda/src/pipeline/genericThread.cxx @@ -14,6 +14,8 @@ #include "genericThread.h" #include "pnotify.h" +#ifndef CPPPARSER + TypeHandle GenericThread::_type_handle; /** @@ -23,8 +25,6 @@ GenericThread:: GenericThread(const std::string &name, const std::string &sync_name) : Thread(name, sync_name) { - _function = nullptr; - _user_data = nullptr; } /** @@ -33,8 +33,17 @@ GenericThread(const std::string &name, const std::string &sync_name) : GenericThread:: GenericThread(const std::string &name, const std::string &sync_name, GenericThread::ThreadFunc *function, void *user_data) : Thread(name, sync_name), - _function(function), - _user_data(user_data) + _function(std::bind(function, user_data)) +{ +} + +/** + * + */ +GenericThread:: +GenericThread(const std::string &name, const std::string &sync_name, std::function function) : + Thread(name, sync_name), + _function(std::move(function)) { } @@ -43,6 +52,8 @@ GenericThread(const std::string &name, const std::string &sync_name, GenericThre */ void GenericThread:: thread_main() { - nassertv(_function != nullptr); - (*_function)(_user_data); + nassertv(_function); + _function(); } + +#endif diff --git a/panda/src/pipeline/genericThread.h b/panda/src/pipeline/genericThread.h index 6e6a6ade6a..b13185af1d 100644 --- a/panda/src/pipeline/genericThread.h +++ b/panda/src/pipeline/genericThread.h @@ -17,6 +17,9 @@ #include "pandabase.h" #include "thread.h" +#ifndef CPPPARSER +#include + /** * A generic thread type that allows calling a C-style thread function without * having to subclass. @@ -27,19 +30,16 @@ public: GenericThread(const std::string &name, const std::string &sync_name); GenericThread(const std::string &name, const std::string &sync_name, ThreadFunc *function, void *user_data); + GenericThread(const std::string &name, const std::string &sync_name, std::function function); - INLINE void set_function(ThreadFunc *function); - INLINE ThreadFunc *get_function() const; - - INLINE void set_user_data(void *user_data); - INLINE void *get_user_data() const; + INLINE void set_function(std::function function); + INLINE const std::function &get_function() const; protected: virtual void thread_main(); private: - ThreadFunc *_function; - void *_user_data; + std::function _function; public: static TypeHandle get_class_type() { @@ -61,4 +61,6 @@ private: #include "genericThread.I" -#endif +#endif // CPPPARSER + +#endif // GENERICTHREAD_H From 11edd1a0dca72273a595352c490165ae3e6e33b3 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 1 Dec 2021 11:34:48 +0100 Subject: [PATCH 58/68] ffmpeg: Change thread to use lambda --- panda/src/ffmpeg/ffmpegVideoCursor.cxx | 12 +++--------- panda/src/ffmpeg/ffmpegVideoCursor.h | 1 - 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/panda/src/ffmpeg/ffmpegVideoCursor.cxx b/panda/src/ffmpeg/ffmpegVideoCursor.cxx index ed77a9da30..5cc3d2cffb 100644 --- a/panda/src/ffmpeg/ffmpegVideoCursor.cxx +++ b/panda/src/ffmpeg/ffmpegVideoCursor.cxx @@ -274,7 +274,9 @@ start_thread() { // Create and start the thread object. _thread_status = TS_wait; - _thread = new GenericThread(_filename.get_basename(), _sync_name, st_thread_main, this); + _thread = new GenericThread(_filename.get_basename(), _sync_name, [this]{ + thread_main(); + }); if (!_thread->start(_thread_priority, true)) { // Couldn't start the thread. _thread = nullptr; @@ -668,14 +670,6 @@ cleanup() { } } -/** - * The thread main function, static version (for passing to GenericThread). - */ -void FfmpegVideoCursor:: -st_thread_main(void *self) { - ((FfmpegVideoCursor *)self)->thread_main(); -} - /** * The thread main function. */ diff --git a/panda/src/ffmpeg/ffmpegVideoCursor.h b/panda/src/ffmpeg/ffmpegVideoCursor.h index 58d63eb6ba..21e648af41 100644 --- a/panda/src/ffmpeg/ffmpegVideoCursor.h +++ b/panda/src/ffmpeg/ffmpegVideoCursor.h @@ -134,7 +134,6 @@ private: private: // The following functions will be called in the sub-thread. - static void st_thread_main(void *self); void thread_main(); bool do_poll(); From 985ec3061c90af8248202eed612a0112ed91a012 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 4 Dec 2021 20:37:14 +0100 Subject: [PATCH 59/68] makepanda: Fix deprecated spelling for threading API The new spelling is available since Python 2.6, so there is no chance of breakage here. --- makepanda/makepanda.py | 2 +- makepanda/makepandacore.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 19aa7cc626..d9bfb9e4e3 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -7093,7 +7093,7 @@ def ParallelMake(tasklist): # Create the workers for slave in range(THREADCOUNT): th = threading.Thread(target=BuildWorker, args=[taskqueue, donequeue]) - th.setDaemon(1) + th.daemon = True th.start() # Feed tasks to the workers. tasksqueued = 0 diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index 4e03d3c2e2..b785a36089 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -24,7 +24,7 @@ SUFFIX_LIB = [".lib",".ilb"] VCS_DIRS = set(["CVS", "CVSROOT", ".git", ".hg", "__pycache__"]) VCS_FILES = set([".cvsignore", ".gitignore", ".gitmodules", ".hgignore"]) STARTTIME = time.time() -MAINTHREAD = threading.currentThread() +MAINTHREAD = threading.current_thread() OUTPUTDIR = "built" CUSTOM_OUTPUTDIR = False THIRDPARTYBASE = None @@ -243,7 +243,7 @@ def ProgressOutput(progress, msg, target = None): sys.stdout.flush() sys.stderr.flush() prefix = "" - thisthread = threading.currentThread() + thisthread = threading.current_thread() if thisthread is MAINTHREAD: if progress is None: prefix = "" @@ -273,7 +273,7 @@ def ProgressOutput(progress, msg, target = None): def exit(msg = ""): sys.stdout.flush() sys.stderr.flush() - if (threading.currentThread() == MAINTHREAD): + if threading.current_thread() == MAINTHREAD: SaveDependencyCache() print("Elapsed Time: " + PrettyTime(time.time() - STARTTIME)) print(msg) From 6419acb56f83c3fa8bd65f7dd1cdaa5fe380291f Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 4 Dec 2021 20:37:50 +0100 Subject: [PATCH 60/68] actor: Fix deadlinks to renamed manual page in API reference --- direct/src/actor/Actor.py | 4 ++-- direct/src/actor/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/direct/src/actor/Actor.py b/direct/src/actor/Actor.py index 1d3fd2b0c4..27708c6889 100644 --- a/direct/src/actor/Actor.py +++ b/direct/src/actor/Actor.py @@ -1,7 +1,7 @@ """Actor module: contains the Actor class. -See the :ref:`models-and-actors` page in the Programming Guide to learn -more about loading models and animated actors. +See the :ref:`loading-actors-and-animations` page in the Programming Guide +to learn more about loading animated models. """ __all__ = ['Actor'] diff --git a/direct/src/actor/__init__.py b/direct/src/actor/__init__.py index 14d116777f..191aacd08f 100644 --- a/direct/src/actor/__init__.py +++ b/direct/src/actor/__init__.py @@ -5,6 +5,6 @@ the lower-level :class:`panda3d.core.Character` implementation. It loads and controls an animated character and manages the animations playing on it. -See the :ref:`models-and-actors` page in the Programming Guide to learn -more about loading models and animated actors. +See the :ref:`loading-actors-and-animations` page in the Programming Guide +to learn more about loading animated models. """ From e2c26adcf03e8bdf739aef70115d6eb3b6d2d6b8 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 4 Dec 2021 20:38:13 +0100 Subject: [PATCH 61/68] event: Mention that ButtonEvent API is not stable for Python use --- panda/src/event/buttonEvent.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/panda/src/event/buttonEvent.h b/panda/src/event/buttonEvent.h index 9da453a827..d80eabf701 100644 --- a/panda/src/event/buttonEvent.h +++ b/panda/src/event/buttonEvent.h @@ -42,6 +42,9 @@ class DatagramIterator; * keyboard (and other buttons for which there is a corresponding ButtonHandle * object), while keystroke events are defined across the entire Unicode * character set. + * + * This API should not be considered stable and may change in a future version + * of Panda3D. */ class EXPCL_PANDA_EVENT ButtonEvent { PUBLISHED: From 82ebf908fa07ca5e0fd42b82ba41864fb47109b6 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 4 Dec 2021 20:38:31 +0100 Subject: [PATCH 62/68] gobj: Fix spelling error in doc of async-load-delay config variable --- panda/src/gobj/config_gobj.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/gobj/config_gobj.cxx b/panda/src/gobj/config_gobj.cxx index 07075a94a8..16a35981c5 100644 --- a/panda/src/gobj/config_gobj.cxx +++ b/panda/src/gobj/config_gobj.cxx @@ -487,7 +487,7 @@ PRC_DESC("If this is nonzero, it represents an artificial delay, " "in seconds, that is imposed on every asynchronous load attempt " "(within the thread). Its purpose is to help debug errors that " "may occur when an asynchronous load is delayed. The " - "delay is per-model, and all aync loads will be queued " + "delay is per-model, and all async loads will be queued " "up behind the delay--it is as if the time it takes to read a " "file is increased by this amount per read.")); From 24755bc8ebde6caec95b5995d75f3c59b7f5d4c1 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 4 Dec 2021 20:40:29 +0100 Subject: [PATCH 63/68] gui: Allow OnscreenImage to be used before ShowBase is created Use TexturePool directly instead of a loader being present in the builtins Fixes #1209 --- direct/src/gui/OnscreenImage.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/direct/src/gui/OnscreenImage.py b/direct/src/gui/OnscreenImage.py index fc3487cc72..a70d371b97 100644 --- a/direct/src/gui/OnscreenImage.py +++ b/direct/src/gui/OnscreenImage.py @@ -114,7 +114,9 @@ class OnscreenImage(DirectObject, NodePath): tex = image else: # It's a Texture file name - tex = loader.loadTexture(image) + tex = TexturePool.loadTexture(image) + if not tex: + raise IOError('Could not load texture: %s' % (image)) cm = CardMaker('OnscreenImage') cm.setFrame(-1, 1, -1, 1) self.assign(parent.attachNewNode(cm.generate(), sort)) From 420d1cee61fd85042eedd4e97eec024269725b03 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 4 Dec 2021 20:43:07 +0100 Subject: [PATCH 64/68] makepanda: Fix unclosed file handles --- makepanda/makepandacore.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index b785a36089..97f79e951b 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -1599,8 +1599,8 @@ def PkgConfigGetIncDirs(pkgname, tool = "pkg-config"): else: handle = os.popen(LocateBinary(tool) + " --cflags") result = handle.read().strip() - if len(result) == 0: return [] handle.close() + if len(result) == 0: return [] dirs = [] for opt in result.split(" "): if (opt.startswith("-I")): @@ -3637,7 +3637,8 @@ def UpdatePythonVersionInfoFile(new_info): json_data = [] if os.path.isfile(json_file) and not PkgSkip("PYTHON"): try: - json_data = json.load(open(json_file, 'r')) + with open(json_file, 'r') as fh: + json_data = json.load(fh) except: json_data = [] @@ -3655,7 +3656,9 @@ def UpdatePythonVersionInfoFile(new_info): if VERBOSE: print("Writing %s" % (json_file)) - json.dump(json_data, open(json_file, 'w'), indent=4) + + with open(json_file, 'w') as fh: + json.dump(json_data, fh, indent=4) def ReadPythonVersionInfoFile(): From 80892ede94a28edcdd0e6ed08de40b75ded972e3 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 4 Dec 2021 13:31:09 +0200 Subject: [PATCH 65/68] assimp: Support compilation with Assimp 5.x Closes #1212 --- pandatool/src/assimp/pandaLogger.cxx | 9 +++++++++ pandatool/src/assimp/pandaLogger.h | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/pandatool/src/assimp/pandaLogger.cxx b/pandatool/src/assimp/pandaLogger.cxx index 2b92cfbc17..04745e2bc8 100644 --- a/pandatool/src/assimp/pandaLogger.cxx +++ b/pandatool/src/assimp/pandaLogger.cxx @@ -38,6 +38,15 @@ void PandaLogger::OnDebug(const char *message) { assimp_cat.debug() << message << "\n"; } +/** + * + */ +void PandaLogger::OnVerboseDebug(const char *message) { + if (assimp_cat.is_spam()) { + assimp_cat.spam() << message << "\n"; + } +} + /** * */ diff --git a/pandatool/src/assimp/pandaLogger.h b/pandatool/src/assimp/pandaLogger.h index dbd2165ce6..2fdbd44609 100644 --- a/pandatool/src/assimp/pandaLogger.h +++ b/pandatool/src/assimp/pandaLogger.h @@ -30,11 +30,17 @@ protected: INLINE bool attachStream(Assimp::LogStream*, unsigned int) { return false; }; + INLINE bool detachStream(Assimp::LogStream*, unsigned int) { + return false; + }; + + // Kept for compatibility with Assimp 4.x INLINE bool detatchStream(Assimp::LogStream*, unsigned int) { return false; }; void OnDebug(const char *message); + void OnVerboseDebug(const char *message); void OnError(const char *message); void OnInfo(const char *message); void OnWarn(const char *message); From cdadd00e72cb149fe09faaa168f558f5e4e0920e Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 4 Dec 2021 21:57:55 +0100 Subject: [PATCH 66/68] makepanda: Optimize WriteEmbeddedStringFile function This function took ages with a debug build of Python --- makepanda/makepandacore.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index 2e3bc76ed8..ccb1aaad71 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -3276,6 +3276,22 @@ def WriteResourceFile(basename, **kwargs): return basename +def GenerateEmbeddedStringFile(string_name, data): + yield 'extern const char %s[] = {\n' % (string_name) + i = 0 + for byte in data: + if i == 0: + yield ' ' + + yield ' 0x%02x,' % (byte) + i += 1 + if i >= 12: + yield '\n' + i = 0 + + yield '\n};\n' + + def WriteEmbeddedStringFile(basename, inputs, string_name=None): if os.path.splitext(basename)[1] not in SUFFIX_INC: basename += '.cxx' @@ -3300,20 +3316,7 @@ def WriteEmbeddedStringFile(basename, inputs, string_name=None): data.append(0) - output = 'extern const char %s[] = {\n' % (string_name) - - i = 0 - for byte in data: - if i == 0: - output += ' ' - - output += ' 0x%02x,' % (byte) - i += 1 - if i >= 12: - output += '\n' - i = 0 - - output += '\n};\n' + output = ''.join(GenerateEmbeddedStringFile(string_name, data)) ConditionalWriteFile(target, output) return target From a99a1ed9919cacba59e6c33c6208c563d69295f1 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 5 Dec 2021 00:00:52 +0100 Subject: [PATCH 67/68] makepanda: Android build fixes --- makepanda/makepanda.py | 34 +++++----------------------------- makepanda/makepandacore.py | 21 ++++----------------- 2 files changed, 9 insertions(+), 46 deletions(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 46401e9ed8..ebce941f71 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -1308,10 +1308,6 @@ def CompileCxx(obj,src,opts): if "SYSROOT" in SDK: if GetTarget() != "android": cmd += ' --sysroot=%s' % (SDK["SYSROOT"]) - else: - ndk_dir = SDK["ANDROID_NDK"].replace('\\', '/') - cmd += ' -isystem %s/sysroot/usr/include' % (ndk_dir) - cmd += ' -isystem %s/sysroot/usr/include/%s' % (ndk_dir, SDK["ANDROID_TRIPLE"]) cmd += ' -no-canonical-prefixes' # Android-specific flags. @@ -1320,33 +1316,24 @@ def CompileCxx(obj,src,opts): if GetTarget() == "android": # Most of the specific optimization flags here were # just copied from the default Android Makefiles. - if "ANDROID_API" in SDK: - cmd += ' -D__ANDROID_API__=' + str(SDK["ANDROID_API"]) if "ANDROID_GCC_TOOLCHAIN" in SDK: cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/') cmd += ' -ffunction-sections -funwind-tables' + cmd += ' -target ' + SDK["ANDROID_TRIPLE"] if arch == 'armv7a': - cmd += ' -target armv7-none-linux-androideabi' cmd += ' -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16' cmd += ' -fno-integrated-as' elif arch == 'arm': - cmd += ' -target armv5te-none-linux-androideabi' cmd += ' -march=armv5te -mtune=xscale -msoft-float' cmd += ' -fno-integrated-as' - elif arch == 'aarch64': - cmd += ' -target aarch64-none-linux-android' elif arch == 'mips': - cmd += ' -target mipsel-none-linux-android' cmd += ' -mips32' elif arch == 'mips64': - cmd += ' -target mips64el-none-linux-android' cmd += ' -fintegrated-as' elif arch == 'x86': - cmd += ' -target i686-none-linux-android' cmd += ' -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32' cmd += ' -mstackrealign' elif arch == 'x86_64': - cmd += ' -target x86_64-none-linux-android' cmd += ' -march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel' cmd += " -Wa,--noexecstack" @@ -1848,28 +1835,16 @@ def CompileLink(dll, obj, opts): if "ANDROID_GCC_TOOLCHAIN" in SDK: cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/') cmd += " -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now" + cmd += ' -target ' + SDK["ANDROID_TRIPLE"] if arch == 'armv7a': - cmd += ' -target armv7-none-linux-androideabi' cmd += " -march=armv7-a -Wl,--fix-cortex-a8" - elif arch == 'arm': - cmd += ' -target armv5te-none-linux-androideabi' - elif arch == 'aarch64': - cmd += ' -target aarch64-none-linux-android' elif arch == 'mips': - cmd += ' -target mipsel-none-linux-android' cmd += ' -mips32' - elif arch == 'mips64': - cmd += ' -target mips64el-none-linux-android' - elif arch == 'x86': - cmd += ' -target i686-none-linux-android' - elif arch == 'x86_64': - cmd += ' -target x86_64-none-linux-android' cmd += ' -lc -lm' else: cmd += " -pthread" - - if "SYSROOT" in SDK: - cmd += " --sysroot=%s -no-canonical-prefixes" % (SDK["SYSROOT"]) + if "SYSROOT" in SDK: + cmd += " --sysroot=%s -no-canonical-prefixes" % (SDK["SYSROOT"]) if LDFLAGS != "": cmd += " " + LDFLAGS @@ -2446,6 +2421,7 @@ def WriteConfigSettings(): dtool_config["PHAVE_GLOB_H"] = 'UNDEF' dtool_config["PHAVE_LOCKF"] = 'UNDEF' dtool_config["HAVE_VIDEO4LINUX"] = 'UNDEF' + dtool_config["HAVE_X11"] = 'UNDEF' if (GetOptimize() <= 2 and GetTarget() == "windows"): dtool_config["USE_DEBUG_PYTHON"] = '1' diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index ccb1aaad71..f9befc3905 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -404,8 +404,8 @@ def SetTarget(target, arch=None): # 64-bit platforms were introduced in Android 21. ANDROID_API = 21 else: - # Default to the lowest API level supported by NDK r16. - ANDROID_API = 14 + # Default to the lowest API level still supported by Google. + ANDROID_API = 19 # Determine the prefix for our gcc tools, eg. arm-linux-androideabi-gcc global ANDROID_ABI, ANDROID_TRIPLE @@ -433,6 +433,7 @@ def SetTarget(target, arch=None): else: exit('Android architecture must be arm, armv7a, aarch64, mips, mips64, x86 or x86_64') + ANDROID_TRIPLE += str(ANDROID_API) TOOLCHAIN_PREFIX = ANDROID_TRIPLE + '-' elif target == 'linux': @@ -2540,15 +2541,6 @@ def SdkLocateAndroid(): SDK["SYSROOT"] = os.path.join(ndk_root, 'platforms', 'android-%s' % (api), arch_dir).replace('\\', '/') #IncDirectory("ALWAYS", os.path.join(SDK["SYSROOT"], 'usr', 'include')) - # Starting with NDK r16, libc++ is the recommended STL to use. - stdlibc = os.path.join(ndk_root, 'sources', 'cxx-stl', 'llvm-libc++') - IncDirectory("ALWAYS", os.path.join(stdlibc, 'include').replace('\\', '/')) - LibDirectory("ALWAYS", os.path.join(stdlibc, 'libs', abi).replace('\\', '/')) - - stl_lib = os.path.join(stdlibc, 'libs', abi, 'libc++_shared.so') - LibName("ALWAYS", stl_lib.replace('\\', '/')) - CopyFile(os.path.join(GetOutputDir(), 'lib', 'libc++_shared.so'), stl_lib) - # The Android support library polyfills C++ features not available in the # STL that ships with Android. support = os.path.join(ndk_root, 'sources', 'android', 'support', 'include') @@ -2957,12 +2949,7 @@ def SetupBuildEnvironment(compiler): # Now extract the preprocessor's include directories. cmd = GetCXX() + " -x c++ -v -E " + os.devnull - if "ANDROID_NDK" in SDK: - ndk_dir = SDK["ANDROID_NDK"].replace('\\', '/') - cmd += ' -isystem %s/sysroot/usr/include' % (ndk_dir) - cmd += ' -isystem %s/sysroot/usr/include/%s' % (ndk_dir, SDK["ANDROID_TRIPLE"]) - else: - cmd += sysroot_flag + cmd += sysroot_flag null = open(os.devnull, 'w') handle = subprocess.Popen(cmd, stdout=null, stderr=subprocess.PIPE, shell=True) From c977df33a3e15713500d44855ac908cbaca4949c Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 5 Dec 2021 00:01:24 +0100 Subject: [PATCH 68/68] express: Fix issue on platforms without signed cha --- panda/src/express/virtualFileSystem.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/express/virtualFileSystem.cxx b/panda/src/express/virtualFileSystem.cxx index 0092d9943e..1655c214be 100644 --- a/panda/src/express/virtualFileSystem.cxx +++ b/panda/src/express/virtualFileSystem.cxx @@ -156,7 +156,7 @@ mount(const Filename &physical_filename, const Filename &mount_point, // read-write on multifiles and .zip files. flags |= MF_read_only; - char ch = stream->get(); + int ch = stream->get(); if (ch == '#' || ch == 'p') { // It *might* be a multifile. while (ch == '#') {