From 42372e139cf620da227eec26ce9b6458d0ad32b2 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 16 Dec 2022 19:37:28 +0100 Subject: [PATCH 1/9] motiontrail: Update API documentation --- direct/src/motiontrail/MotionTrail.py | 36 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/direct/src/motiontrail/MotionTrail.py b/direct/src/motiontrail/MotionTrail.py index 097f61ee09..2f33f48410 100644 --- a/direct/src/motiontrail/MotionTrail.py +++ b/direct/src/motiontrail/MotionTrail.py @@ -47,9 +47,9 @@ class MotionTrailFrame: class MotionTrail(NodePath, DirectObject): """Generates smooth geometry-based motion trails behind a moving object. - To use this class, first define the shape of the cross-section of the trail - by repeatedly calling `add_vertex()` and `set_vertex_color()`. - When this is done, `update_vertices()` must be called. + To use this class, first define the shape of the cross-section polygon that + is to be extruded along the motion trail by calling `add_vertex()` and + `set_vertex_color()`. When this is done, call `update_vertices()`. To generate the motion trail, either call `register_motion_trail()` to have Panda update it automatically, or periodically call the method @@ -80,6 +80,10 @@ class MotionTrail(NodePath, DirectObject): @classmethod def setGlobalEnable(cls, enable): + """Set this to False to have the task stop updating all motion trails. + This does not prevent updating them manually using the + `update_motion_trail()` method. + """ cls.global_enable = enable def __init__(self, name, parent_node_path): @@ -117,14 +121,14 @@ class MotionTrail(NodePath, DirectObject): self.continuous_motion_trail = True self.color_scale = 1.0 - ## How long the time window is for which the trail is computed. Can be - ## increased to obtain a longer trail, decreased for a shorter trail. + #: How long the time window is for which the trail is computed. Can be + #: increased to obtain a longer trail, decreased for a shorter trail. self.time_window = 1.0 - ## How often the trail updates, in seconds. The default is 0.0, which - ## has the trail updated every frame for the smoothest result. Higher - ## values will generate a choppier trail. The `use_nurbs` option can - ## compensate partially for this choppiness, however. + #: How often the trail updates, in seconds. The default is 0.0, which + #: has the trail updated every frame for the smoothest result. Higher + #: values will generate a choppier trail. The `use_nurbs` option can + #: compensate partially for this choppiness, however. self.sampling_time = 0.0 self.square_t = True @@ -135,9 +139,9 @@ class MotionTrail(NodePath, DirectObject): # node path states self.reparentTo(parent_node_path) - ## A `.GeomNode` object containing the generated geometry. By default - ## parented to the MotionTrail itself, but can be reparented elsewhere - ## if necessary. + #: A `.GeomNode` object containing the generated geometry. By default + #: parented to the MotionTrail itself, but can be reparented elsewhere + #: if necessary. self.geom_node = GeomNode("motion_trail") self.geom_node_path = self.attachNewNode(self.geom_node) node_path = self.geom_node_path @@ -167,9 +171,11 @@ class MotionTrail(NodePath, DirectObject): self.relative_to_render = False - ## Set this to True to use a NURBS curve to generate a smooth trail, - ## even if the underlying animation or movement is janky. + #: Set this to True to use a NURBS curve to generate a smooth trail, + #: even if the underlying animation or movement is janky. self.use_nurbs = False + + #: This can be changed to fine-tune the resolution of the NURBS curve. self.resolution_distance = 0.5 self.cmotion_trail = CMotionTrail() @@ -245,7 +251,7 @@ class MotionTrail(NodePath, DirectObject): return Task.cont def add_vertex(self, vertex_id, vertex_function=None, context=None): - """This must be called repeatedly to define the polygon that forms the + """This must be called initially to define the polygon that forms the cross-section of the generated motion trail geometry. The first argument is a user-defined vertex identifier, the second is a function that will be called with three parameters that should return the From 32297ecd025fa8c616d4f574be45d4ceb2cff096 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 16 Dec 2022 19:37:49 +0100 Subject: [PATCH 2/9] showbase: Fix some members not showing up in API documentation --- direct/src/showbase/ShowBase.py | 66 ++++++++++++++++----------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index 1f286588d2..8e39f7b6ab 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -1160,7 +1160,7 @@ class ShowBase(DirectObject.DirectObject): Creates the render scene graph, the primary scene graph for rendering 3-d geometry. """ - ## This is the root of the 3-D scene graph. + #: This is the root of the 3-D scene graph. self.render = NodePath('render') self.render.setAttrib(RescaleNormalAttrib.makeDefault()) @@ -1179,7 +1179,7 @@ class ShowBase(DirectObject.DirectObject): # for the benefit of creating DirectGui elements before ShowBase. from . import ShowBaseGlobal - ## This is the root of the 2-D scene graph. + #: This is the root of the 2-D scene graph. self.render2d = ShowBaseGlobal.render2d # Set up some overrides to turn off certain properties which @@ -1200,12 +1200,12 @@ class ShowBase(DirectObject.DirectObject): self.render2d.setMaterialOff(1) self.render2d.setTwoSided(1) - ## The normal 2-d DisplayRegion has an aspect ratio that - ## matches the window, but its coordinate system is square. - ## This means anything we parent to render2d gets stretched. - ## For things where that makes a difference, we set up - ## aspect2d, which scales things back to the right aspect - ## ratio along the X axis (Z is still from -1 to 1) + #: The normal 2-d DisplayRegion has an aspect ratio that + #: matches the window, but its coordinate system is square. + #: This means anything we parent to render2d gets stretched. + #: For things where that makes a difference, we set up + #: aspect2d, which scales things back to the right aspect + #: ratio along the X axis (Z is still from -1 to 1) self.aspect2d = ShowBaseGlobal.aspect2d aspectRatio = self.getAspectRatio() @@ -1213,13 +1213,13 @@ class ShowBase(DirectObject.DirectObject): self.a2dBackground = self.aspect2d.attachNewNode("a2dBackground") - ## The Z position of the top border of the aspect2d screen. + #: The Z position of the top border of the aspect2d screen. self.a2dTop = 1.0 - ## The Z position of the bottom border of the aspect2d screen. + #: The Z position of the bottom border of the aspect2d screen. self.a2dBottom = -1.0 - ## The X position of the left border of the aspect2d screen. + #: The X position of the left border of the aspect2d screen. self.a2dLeft = -aspectRatio - ## The X position of the right border of the aspect2d screen. + #: The X position of the right border of the aspect2d screen. self.a2dRight = aspectRatio self.a2dTopCenter = self.aspect2d.attachNewNode("a2dTopCenter") @@ -1259,9 +1259,9 @@ class ShowBase(DirectObject.DirectObject): self.a2dBottomRight.setPos(self.a2dRight, 0, self.a2dBottom) self.a2dBottomRightNs.setPos(self.a2dRight, 0, self.a2dBottom) - ## This special root, pixel2d, uses units in pixels that are relative - ## to the window. The upperleft corner of the window is (0, 0), - ## the lowerleft corner is (xsize, -ysize), in this coordinate system. + #: This special root, pixel2d, uses units in pixels that are relative + #: to the window. The upperleft corner of the window is (0, 0), + #: the lowerleft corner is (xsize, -ysize), in this coordinate system. self.pixel2d = self.render2d.attachNewNode(PGTop("pixel2d")) self.pixel2d.setPos(-1, 0, 1) xsize, ysize = self.getSize() @@ -1291,25 +1291,25 @@ class ShowBase(DirectObject.DirectObject): self.render2dp.setMaterialOff(1) self.render2dp.setTwoSided(1) - ## The normal 2-d DisplayRegion has an aspect ratio that - ## matches the window, but its coordinate system is square. - ## This means anything we parent to render2dp gets stretched. - ## For things where that makes a difference, we set up - ## aspect2dp, which scales things back to the right aspect - ## ratio along the X axis (Z is still from -1 to 1) + #: The normal 2-d DisplayRegion has an aspect ratio that + #: matches the window, but its coordinate system is square. + #: This means anything we parent to render2dp gets stretched. + #: For things where that makes a difference, we set up + #: aspect2dp, which scales things back to the right aspect + #: ratio along the X axis (Z is still from -1 to 1) self.aspect2dp = self.render2dp.attachNewNode(PGTop("aspect2dp")) self.aspect2dp.node().setStartSort(16384) aspectRatio = self.getAspectRatio() self.aspect2dp.setScale(1.0 / aspectRatio, 1.0, 1.0) - ## The Z position of the top border of the aspect2dp screen. + #: The Z position of the top border of the aspect2dp screen. self.a2dpTop = 1.0 - ## The Z position of the bottom border of the aspect2dp screen. + #: The Z position of the bottom border of the aspect2dp screen. self.a2dpBottom = -1.0 - ## The X position of the left border of the aspect2dp screen. + #: The X position of the left border of the aspect2dp screen. self.a2dpLeft = -aspectRatio - ## The X position of the right border of the aspect2dp screen. + #: The X position of the right border of the aspect2dp screen. self.a2dpRight = aspectRatio self.a2dpTopCenter = self.aspect2dp.attachNewNode("a2dpTopCenter") @@ -1333,9 +1333,9 @@ class ShowBase(DirectObject.DirectObject): self.a2dpBottomLeft.setPos(self.a2dpLeft, 0, self.a2dpBottom) self.a2dpBottomRight.setPos(self.a2dpRight, 0, self.a2dpBottom) - ## This special root, pixel2d, uses units in pixels that are relative - ## to the window. The upperleft corner of the window is (0, 0), - ## the lowerleft corner is (xsize, -ysize), in this coordinate system. + #: This special root, pixel2dp, uses units in pixels that are relative + #: to the window. The upperleft corner of the window is (0, 0), + #: the lowerleft corner is (xsize, -ysize), in this coordinate system. self.pixel2dp = self.render2dp.attachNewNode(PGTop("pixel2dp")) self.pixel2dp.node().setStartSort(16384) self.pixel2dp.setPos(-1, 0, 1) @@ -1656,11 +1656,11 @@ class ShowBase(DirectObject.DirectObject): mw = self.buttonThrowers[0].getParent() - ## A special ButtonThrower to generate keyboard events and - ## include the time from the OS. This is separate only to - ## support legacy code that did not expect a time parameter; it - ## will eventually be folded into the normal ButtonThrower, - ## above. + #: A special ButtonThrower to generate keyboard events and + #: include the time from the OS. This is separate only to + #: support legacy code that did not expect a time parameter; it + #: will eventually be folded into the normal ButtonThrower, + #: above. self.timeButtonThrower = mw.attachNewNode(ButtonThrower('timeButtons')) self.timeButtonThrower.node().setPrefix('time-') self.timeButtonThrower.node().setTimeFlag(1) From 7f916eeb74c25652c4237fa3a3be7044a87677c1 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 16 Dec 2022 19:38:33 +0100 Subject: [PATCH 3/9] gobj: Don't wastefully compute bounds for Geom with custom bounds This was done by `get_nested_vertices()`, which now won't call the expensive `compute_internal_bounds()` if there was a custom bounding volume set. --- doc/ReleaseNotes | 1 + panda/src/gobj/geom.I | 1 + panda/src/gobj/geom.cxx | 25 ++++++++++++++++++++++--- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/doc/ReleaseNotes b/doc/ReleaseNotes index eafbf4f3eb..4baeb47d6c 100644 --- a/doc/ReleaseNotes +++ b/doc/ReleaseNotes @@ -83,6 +83,7 @@ Miscellaneous * Fix interrogate syntax error with C++11-style attributes in declarators * Fix regression with BufferViewer in double-precision build (#1365) * Fix `PandaNode.nested_vertices` not updating properly +* Prevent Panda calculating bounding volume of Geom with custom bounding volume * Add `do_events()` and `process_event()` snake_case aliases in eventMgr * Support second arg of None in `replace_texture()` / `replace_material()` * Support `os.fspath()` for ConfigVariableFilename objects (#1406) diff --git a/panda/src/gobj/geom.I b/panda/src/gobj/geom.I index 3fa32ade36..0f6b5fc7e8 100644 --- a/panda/src/gobj/geom.I +++ b/panda/src/gobj/geom.I @@ -379,6 +379,7 @@ calc_tight_bounds(LPoint3 &min_point, LPoint3 &max_point, */ INLINE void Geom:: mark_internal_bounds_stale(CData *cdata) { + cdata->_nested_vertices = 0; cdata->_internal_bounds_stale = true; } diff --git a/panda/src/gobj/geom.cxx b/panda/src/gobj/geom.cxx index 820c67875e..11c49f14c8 100644 --- a/panda/src/gobj/geom.cxx +++ b/panda/src/gobj/geom.cxx @@ -1119,9 +1119,28 @@ int Geom:: get_nested_vertices(Thread *current_thread) const { CDLockedReader cdata(_cycler, current_thread); if (cdata->_internal_bounds_stale) { - CDWriter cdataw(((Geom *)this)->_cycler, cdata, false); - compute_internal_bounds(cdataw, current_thread); - return cdataw->_nested_vertices; + if (cdata->_user_bounds != nullptr) { + // Don't do the expensive compute_internal_bounds call. + if (cdata->_nested_vertices == 0) { + CDWriter cdataw(((Geom *)this)->_cycler, cdata, false); + int num_vertices = 0; + + Primitives::const_iterator pi; + for (pi = cdataw->_primitives.begin(); + pi != cdataw->_primitives.end(); + ++pi) { + GeomPrimitivePipelineReader reader((*pi).get_read_pointer(current_thread), current_thread); + num_vertices += reader.get_num_vertices(); + } + + cdataw->_nested_vertices = num_vertices; + return num_vertices; + } + } else { + CDWriter cdataw(((Geom *)this)->_cycler, cdata, false); + compute_internal_bounds(cdataw, current_thread); + return cdataw->_nested_vertices; + } } return cdata->_nested_vertices; } From e5a8eb5b1daac9401ee30b1fb12e804d1f8ab71a Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 19 Dec 2022 12:55:28 +0100 Subject: [PATCH 4/9] cocoadisplay: Fix Command+Q in green-button-style fullscreen mode It seems macOS has an extra NSWindow without a delegate in this mode --- panda/src/cocoadisplay/cocoaPandaAppDelegate.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/panda/src/cocoadisplay/cocoaPandaAppDelegate.mm b/panda/src/cocoadisplay/cocoaPandaAppDelegate.mm index cbb84bc205..f64b2be56a 100644 --- a/panda/src/cocoadisplay/cocoaPandaAppDelegate.mm +++ b/panda/src/cocoadisplay/cocoaPandaAppDelegate.mm @@ -39,7 +39,8 @@ // Ask all the windows whether they are OK to be closed. bool should_close = true; for (NSWindow *window in [app windows]) { - if (![[window delegate] windowShouldClose:window]) { + id delegate = [window delegate]; + if (delegate != nil && ![delegate windowShouldClose:window]) { should_close = false; } } From eafcfe1d7080721b732c2e76b09ef242e8944a49 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 19 Dec 2022 14:11:50 +0100 Subject: [PATCH 5/9] motiontrail: Fix Python implementation error in double-prec mode --- direct/src/motiontrail/MotionTrail.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/direct/src/motiontrail/MotionTrail.py b/direct/src/motiontrail/MotionTrail.py index 2f33f48410..33412c99c7 100644 --- a/direct/src/motiontrail/MotionTrail.py +++ b/direct/src/motiontrail/MotionTrail.py @@ -390,21 +390,21 @@ class MotionTrail(NodePath, DirectObject): def add_geometry_quad(self, v0, v1, v2, v3, c0, c1, c2, c3, t0, t1, t2, t3): - self.vertex_writer.addData3f(v0 [0], v0 [1], v0 [2]) - self.vertex_writer.addData3f(v1 [0], v1 [1], v1 [2]) - self.vertex_writer.addData3f(v2 [0], v2 [1], v2 [2]) - self.vertex_writer.addData3f(v3 [0], v3 [1], v3 [2]) + self.vertex_writer.addData3(v0[0], v0[1], v0[2]) + self.vertex_writer.addData3(v1[0], v1[1], v1[2]) + self.vertex_writer.addData3(v2[0], v2[1], v2[2]) + self.vertex_writer.addData3(v3[0], v3[1], v3[2]) - self.color_writer.addData4f(c0) - self.color_writer.addData4f(c1) - self.color_writer.addData4f(c2) - self.color_writer.addData4f(c3) + self.color_writer.addData4(c0) + self.color_writer.addData4(c1) + self.color_writer.addData4(c2) + self.color_writer.addData4(c3) if self.texture is not None: - self.texture_writer.addData2f(t0) - self.texture_writer.addData2f(t1) - self.texture_writer.addData2f(t2) - self.texture_writer.addData2f(t3) + self.texture_writer.addData2(t0) + self.texture_writer.addData2(t1) + self.texture_writer.addData2(t2) + self.texture_writer.addData2(t3) vertex_index = self.vertex_index From 0ace26a938bf879d92c101880bd849efec3938e1 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 19 Dec 2022 15:17:37 +0100 Subject: [PATCH 6/9] gobj: Fix double-precision colors not being clamped in GeomVertexWriter --- doc/ReleaseNotes | 1 + panda/src/gobj/geomVertexColumn.cxx | 50 +++++++++++++++++++++++++++++ panda/src/gobj/geomVertexColumn.h | 4 +++ 3 files changed, 55 insertions(+) diff --git a/doc/ReleaseNotes b/doc/ReleaseNotes index 4baeb47d6c..2b8d78a58b 100644 --- a/doc/ReleaseNotes +++ b/doc/ReleaseNotes @@ -81,6 +81,7 @@ Miscellaneous * Add various useful functions to interrogatedb module * Fix Python 3 issues unpacking uint types in Python 3 (#1380) * Fix interrogate syntax error with C++11-style attributes in declarators +* Fix double-precision color values not being clamped by GeomVertexWriter * Fix regression with BufferViewer in double-precision build (#1365) * Fix `PandaNode.nested_vertices` not updating properly * Prevent Panda calculating bounding volume of Geom with custom bounding volume diff --git a/panda/src/gobj/geomVertexColumn.cxx b/panda/src/gobj/geomVertexColumn.cxx index 9e6c7bd188..2c1c4b4d6f 100644 --- a/panda/src/gobj/geomVertexColumn.cxx +++ b/panda/src/gobj/geomVertexColumn.cxx @@ -4475,6 +4475,20 @@ get_data4f(const unsigned char *pointer) { return _v4; } +/** + * + */ +const LVecBase4d &GeomVertexColumn::Packer_argb_packed:: +get_data4d(const unsigned char *pointer) { + uint32_t dword = *(const uint32_t *)pointer; + _v4d.set(GeomVertexData::unpack_abcd_b(dword), + GeomVertexData::unpack_abcd_c(dword), + GeomVertexData::unpack_abcd_d(dword), + GeomVertexData::unpack_abcd_a(dword)); + _v4d /= 255.0; + return _v4d; +} + /** * */ @@ -4489,6 +4503,20 @@ set_data4f(unsigned char *pointer, const LVecBase4f &data) { (unsigned int)(min(max(data[2], 0.0f), 1.0f) * 255.0f)); } +/** + * + */ +void GeomVertexColumn::Packer_argb_packed:: +set_data4d(unsigned char *pointer, const LVecBase4d &data) { + // when packing an argb, we want to make sure we cap the input values at 1 + // since going above one will cause the value to be truncated. + *(uint32_t *)pointer = GeomVertexData::pack_abcd + ((unsigned int)(min(max(data[3], 0.0), 1.0) * 255.0), + (unsigned int)(min(max(data[0], 0.0), 1.0) * 255.0), + (unsigned int)(min(max(data[1], 0.0), 1.0) * 255.0), + (unsigned int)(min(max(data[2], 0.0), 1.0) * 255.0)); +} + /** * */ @@ -4500,6 +4528,17 @@ get_data4f(const unsigned char *pointer) { return _v4; } +/** + * + */ +const LVecBase4d &GeomVertexColumn::Packer_rgba_uint8_4:: +get_data4d(const unsigned char *pointer) { + _v4d.set((double)pointer[0], (double)pointer[1], + (double)pointer[2], (double)pointer[3]); + _v4d /= 255.0; + return _v4d; +} + /** * */ @@ -4511,6 +4550,17 @@ set_data4f(unsigned char *pointer, const LVecBase4f &data) { pointer[3] = (unsigned int)(min(max(data[3], 0.0f), 1.0f) * 255.0f); } +/** + * + */ +void GeomVertexColumn::Packer_rgba_uint8_4:: +set_data4d(unsigned char *pointer, const LVecBase4d &data) { + pointer[0] = (unsigned int)(min(max(data[0], 0.0), 1.0) * 255.0); + pointer[1] = (unsigned int)(min(max(data[1], 0.0), 1.0) * 255.0); + pointer[2] = (unsigned int)(min(max(data[2], 0.0), 1.0) * 255.0); + pointer[3] = (unsigned int)(min(max(data[3], 0.0), 1.0) * 255.0); +} + /** * */ diff --git a/panda/src/gobj/geomVertexColumn.h b/panda/src/gobj/geomVertexColumn.h index 1ff36b3eda..e76107a2dd 100644 --- a/panda/src/gobj/geomVertexColumn.h +++ b/panda/src/gobj/geomVertexColumn.h @@ -381,7 +381,9 @@ private: class Packer_argb_packed final : public Packer_color { public: virtual const LVecBase4f &get_data4f(const unsigned char *pointer); + virtual const LVecBase4d &get_data4d(const unsigned char *pointer); virtual void set_data4f(unsigned char *pointer, const LVecBase4f &value); + virtual void set_data4d(unsigned char *pointer, const LVecBase4d &value); virtual const char *get_name() const { return "Packer_argb_packed"; @@ -391,7 +393,9 @@ private: class Packer_rgba_uint8_4 final : public Packer_color { public: virtual const LVecBase4f &get_data4f(const unsigned char *pointer); + virtual const LVecBase4d &get_data4d(const unsigned char *pointer); virtual void set_data4f(unsigned char *pointer, const LVecBase4f &value); + virtual void set_data4d(unsigned char *pointer, const LVecBase4d &value); virtual const char *get_name() const { return "Packer_rgba_uint8_4"; From e67cd74725700b236bb1d2346fc329ac4d04a07a Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 19 Dec 2022 16:19:46 +0100 Subject: [PATCH 7/9] express: Implement `copy.deepcopy()` for PointerToArray It actually makes a unique copy of the underlying array. --- doc/ReleaseNotes | 1 + panda/src/express/pointerToArray.h | 4 ++ panda/src/express/pointerToArray_ext.I | 28 ++++++++++++ panda/src/express/pointerToArray_ext.h | 4 ++ tests/express/test_pointertoarray.py | 60 ++++++++++++++++++++++++++ 5 files changed, 97 insertions(+) diff --git a/doc/ReleaseNotes b/doc/ReleaseNotes index 2b8d78a58b..946879c85a 100644 --- a/doc/ReleaseNotes +++ b/doc/ReleaseNotes @@ -71,6 +71,7 @@ Miscellaneous * Fix texture transforms sometimes not being flattened (#1392) * Fix support for `#pragma include ` in GLSL shaders * Fix `ShaderBuffer.prepare()` not doing anything +* Implement deepcopy for PointerToArray * Fix bf-cbc encryption no longer working when building with OpenSSL 3.0 * PandaNode bounds_type property was erroneously marked read-only * Fix warnings when copying OdeTriMeshGeom objects diff --git a/panda/src/express/pointerToArray.h b/panda/src/express/pointerToArray.h index 24a5322bc7..65974c28bc 100644 --- a/panda/src/express/pointerToArray.h +++ b/panda/src/express/pointerToArray.h @@ -121,6 +121,8 @@ PUBLISHED: EXTENSION(int __getbuffer__(PyObject *self, Py_buffer *view, int flags)); EXTENSION(void __releasebuffer__(PyObject *self, Py_buffer *view) const); + + EXTENSION(PointerToArray __deepcopy__(PyObject *memo) const); #endif #else // CPPPARSER @@ -279,6 +281,8 @@ PUBLISHED: EXTENSION(int __getbuffer__(PyObject *self, Py_buffer *view, int flags) const); EXTENSION(void __releasebuffer__(PyObject *self, Py_buffer *view) const); + + EXTENSION(ConstPointerToArray __deepcopy__(PyObject *memo) const); #endif #else // CPPPARSER diff --git a/panda/src/express/pointerToArray_ext.I b/panda/src/express/pointerToArray_ext.I index 425f4d87e3..a56368f36b 100644 --- a/panda/src/express/pointerToArray_ext.I +++ b/panda/src/express/pointerToArray_ext.I @@ -510,6 +510,20 @@ __releasebuffer__(PyObject *self, Py_buffer *view) const { #endif } +/** + * A special Python method that is invoked by copy.deepcopy(pta). This makes + * sure that there is truly a unique copy of the array. + */ +template +INLINE PointerToArray Extension >:: +__deepcopy__(PyObject *memo) const { + PointerToArray copy; + if (!_this->is_null()) { + copy.v() = _this->v(); + } + return copy; +} + /** * This is used to implement the buffer protocol, in order to allow efficient * access to the array data through a Python multiview object. @@ -702,3 +716,17 @@ __releasebuffer__(PyObject *self, Py_buffer *view) const { } #endif } + +/** + * A special Python method that is invoked by copy.deepcopy(pta). This makes + * sure that there is truly a unique copy of the array. + */ +template +INLINE ConstPointerToArray Extension >:: +__deepcopy__(PyObject *memo) const { + PointerToArray copy; + if (!_this->is_null()) { + copy.v() = _this->v(); + } + return copy; +} diff --git a/panda/src/express/pointerToArray_ext.h b/panda/src/express/pointerToArray_ext.h index ce6104791b..6059aec062 100644 --- a/panda/src/express/pointerToArray_ext.h +++ b/panda/src/express/pointerToArray_ext.h @@ -44,6 +44,8 @@ public: INLINE int __getbuffer__(PyObject *self, Py_buffer *view, int flags); INLINE void __releasebuffer__(PyObject *self, Py_buffer *view) const; + + INLINE PointerToArray __deepcopy__(PyObject *memo) const; }; template<> @@ -81,6 +83,8 @@ public: INLINE int __getbuffer__(PyObject *self, Py_buffer *view, int flags) const; INLINE void __releasebuffer__(PyObject *self, Py_buffer *view) const; + + INLINE ConstPointerToArray __deepcopy__(PyObject *memo) const; }; template<> diff --git a/tests/express/test_pointertoarray.py b/tests/express/test_pointertoarray.py index e68fa2ca36..3c87373904 100644 --- a/tests/express/test_pointertoarray.py +++ b/tests/express/test_pointertoarray.py @@ -69,3 +69,63 @@ def test_cpta_float_pickle(): data_pta2 = loads(dumps(data_pta, proto)) assert tuple(data_pta2) == (1.0, 2.0, 3.0) assert data_pta2.get_data() == data_pta.get_data() + + +def test_pta_float_copy(): + from panda3d.core import PTA_float + from copy import copy + + null_pta = PTA_float() + assert copy(null_pta).is_null() + + empty_pta = PTA_float([]) + empty_pta_copy = copy(empty_pta) + assert not empty_pta_copy.is_null() + assert len(empty_pta_copy) == 0 + assert empty_pta_copy.get_ref_count() == 2 + + data_pta = PTA_float([1.0, 2.0, 3.0]) + data_pta_copy = copy(data_pta) + assert not data_pta_copy.is_null() + assert data_pta_copy.get_ref_count() == 2 + assert tuple(data_pta_copy) == (1.0, 2.0, 3.0) + + +def test_pta_float_deepcopy(): + from panda3d.core import PTA_float + from copy import deepcopy + + null_pta = PTA_float() + assert deepcopy(null_pta).is_null() + + empty_pta = PTA_float([]) + empty_pta_copy = deepcopy(empty_pta) + assert not empty_pta_copy.is_null() + assert len(empty_pta_copy) == 0 + assert empty_pta_copy.get_ref_count() == 1 + + data_pta = PTA_float([1.0, 2.0, 3.0]) + data_pta_copy = deepcopy(data_pta) + assert not data_pta_copy.is_null() + assert data_pta_copy.get_ref_count() == 1 + assert tuple(data_pta_copy) == (1.0, 2.0, 3.0) + + +def test_cpta_float_deepcopy(): + from panda3d.core import PTA_float, CPTA_float + from copy import deepcopy + + null_pta = CPTA_float(PTA_float()) + assert deepcopy(null_pta).is_null() + + empty_pta = CPTA_float([]) + empty_pta_copy = deepcopy(empty_pta) + assert not empty_pta_copy.is_null() + assert len(empty_pta_copy) == 0 + assert empty_pta_copy.get_ref_count() == 1 + + data_pta = CPTA_float([1.0, 2.0, 3.0]) + data_pta_copy = deepcopy(data_pta) + assert not data_pta_copy.is_null() + assert data_pta_copy.get_ref_count() == 1 + assert tuple(data_pta_copy) == (1.0, 2.0, 3.0) From 896346b99f7b497f346ac34ba7c7a21f6af765eb Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 19 Dec 2022 16:22:33 +0100 Subject: [PATCH 8/9] gobj: Implement `copy.deepcopy()` for Texture class Actually ensures that the underlying RAM images are really fully unique. --- doc/ReleaseNotes | 1 + panda/src/gobj/texture.h | 4 ++++ panda/src/gobj/texture_ext.cxx | 28 ++++++++++++++++++++++++++++ panda/src/gobj/texture_ext.h | 2 ++ tests/gobj/test_texture.py | 23 +++++++++++++++++++++++ 5 files changed, 58 insertions(+) diff --git a/doc/ReleaseNotes b/doc/ReleaseNotes index 946879c85a..2707d324d9 100644 --- a/doc/ReleaseNotes +++ b/doc/ReleaseNotes @@ -72,6 +72,7 @@ Miscellaneous * Fix support for `#pragma include ` in GLSL shaders * Fix `ShaderBuffer.prepare()` not doing anything * Implement deepcopy for PointerToArray +* Fix Texture deepcopy keeping a reference to the original RAM image * Fix bf-cbc encryption no longer working when building with OpenSSL 3.0 * PandaNode bounds_type property was erroneously marked read-only * Fix warnings when copying OdeTriMeshGeom objects diff --git a/panda/src/gobj/texture.h b/panda/src/gobj/texture.h index 86ca58fe02..c2ff9ad24d 100644 --- a/panda/src/gobj/texture.h +++ b/panda/src/gobj/texture.h @@ -46,6 +46,7 @@ #include "pnmImage.h" #include "pfmFile.h" #include "asyncFuture.h" +#include "extension.h" class TextureContext; class FactoryParams; @@ -472,6 +473,8 @@ PUBLISHED: MAKE_PROPERTY(keep_ram_image, get_keep_ram_image, set_keep_ram_image); MAKE_PROPERTY(cacheable, is_cacheable); + EXTENSION(PT(Texture) __deepcopy__(PyObject *memo) const); + BLOCKING INLINE bool compress_ram_image(CompressionMode compression = CM_on, QualityLevel quality_level = QL_default, GraphicsStateGuardianBase *gsg = nullptr); @@ -1110,6 +1113,7 @@ private: static TypeHandle _type_handle; + friend class Extension; friend class TextureContext; friend class PreparedGraphicsObjects; friend class TexturePool; diff --git a/panda/src/gobj/texture_ext.cxx b/panda/src/gobj/texture_ext.cxx index 9af792be08..78f1dad1b4 100644 --- a/panda/src/gobj/texture_ext.cxx +++ b/panda/src/gobj/texture_ext.cxx @@ -165,4 +165,32 @@ set_ram_image_as(PyObject *image, const std::string &provided_format) { Dtool_Raise_ArgTypeError(image, 0, "Texture.set_ram_image_as", "CPTA_uchar or buffer"); } +/** + * A special Python method that is invoked by copy.deepcopy(tex). This makes + * sure that the copy has a unique copy of the RAM image. + */ +PT(Texture) Extension:: +__deepcopy__(PyObject *memo) const { + PT(Texture) copy = _this->make_copy(); + { + Texture::CDWriter cdata(copy->_cycler, true); + for (Texture::RamImage &image : cdata->_ram_images) { + if (image._image.get_ref_count() > 1) { + PTA_uchar new_image; + new_image.v() = image._image.v(); + image._image = std::move(new_image); + } + } + { + Texture::RamImage &image = cdata->_simple_ram_image; + if (image._image.get_ref_count() > 1) { + PTA_uchar new_image; + new_image.v() = image._image.v(); + image._image = std::move(new_image); + } + } + } + return copy; +} + #endif // HAVE_PYTHON diff --git a/panda/src/gobj/texture_ext.h b/panda/src/gobj/texture_ext.h index b4f0559b08..bebe54557f 100644 --- a/panda/src/gobj/texture_ext.h +++ b/panda/src/gobj/texture_ext.h @@ -32,6 +32,8 @@ public: void set_ram_image(PyObject *image, Texture::CompressionMode compression = Texture::CM_off, size_t page_size = 0); void set_ram_image_as(PyObject *image, const std::string &provided_format); + + PT(Texture) __deepcopy__(PyObject *memo) const; }; #endif // HAVE_PYTHON diff --git a/tests/gobj/test_texture.py b/tests/gobj/test_texture.py index 4bf917a8c2..2cddddb855 100644 --- a/tests/gobj/test_texture.py +++ b/tests/gobj/test_texture.py @@ -134,3 +134,26 @@ def test_texture_clear_half(): assert col.y == -inf assert col.z == -inf assert math.isnan(col.w) + + +def test_texture_deepcopy(): + from copy import deepcopy + + empty_tex = Texture("empty-texture") + empty_tex.setup_2d_texture(16, 16, Texture.T_unsigned_byte, Texture.F_rgba) + assert not empty_tex.has_ram_image() + empty_tex2 = deepcopy(empty_tex) + assert empty_tex2.name == empty_tex.name + assert not empty_tex2.has_ram_image() + + tex = Texture("texture") + tex.setup_2d_texture(16, 16, Texture.T_unsigned_byte, Texture.F_rgba) + img = tex.make_ram_image() + assert tex.has_ram_image() + assert img.get_ref_count() == 2 + + tex2 = deepcopy(tex) + assert tex2.name == tex.name + assert tex2.has_ram_image() + img2 = tex2.get_ram_image() + assert img2.get_ref_count() == 2 From 0911040999d32cd76012ada940d9ad92bd694e2e Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 19 Dec 2022 16:38:19 +0100 Subject: [PATCH 9/9] express: Fix compile error with GCC --- panda/src/express/pointerToArray_ext.I | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/panda/src/express/pointerToArray_ext.I b/panda/src/express/pointerToArray_ext.I index a56368f36b..7ebb8afd4d 100644 --- a/panda/src/express/pointerToArray_ext.I +++ b/panda/src/express/pointerToArray_ext.I @@ -518,8 +518,8 @@ template INLINE PointerToArray Extension >:: __deepcopy__(PyObject *memo) const { PointerToArray copy; - if (!_this->is_null()) { - copy.v() = _this->v(); + if (!this->_this->is_null()) { + copy.v() = this->_this->v(); } return copy; } @@ -725,8 +725,8 @@ template INLINE ConstPointerToArray Extension >:: __deepcopy__(PyObject *memo) const { PointerToArray copy; - if (!_this->is_null()) { - copy.v() = _this->v(); + if (!this->_this->is_null()) { + copy.v() = this->_this->v(); } return copy; }