From 7d0c016c44ce38755023a10b2535bb8d9bf9385f Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 6 Oct 2020 11:50:16 +0200 Subject: [PATCH 01/12] Add Python 3.9 to supported Python versions in setup.cfg metadata [skip ci] --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 3addb096a0..82e013c11a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,6 +21,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 Programming Language :: Python :: Implementation :: CPython Topic :: Games/Entertainment Topic :: Multimedia From 92a0279bc5bc889a6afc73ffa4ea628edd7cbd27 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 2 Nov 2020 12:14:04 +0100 Subject: [PATCH 02/12] Update BACKERS.md [skip ci] --- BACKERS.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/BACKERS.md b/BACKERS.md index a42b006abe..4b5b5a5bb6 100644 --- a/BACKERS.md +++ b/BACKERS.md @@ -15,7 +15,6 @@ This is a list of all the people who are contributing financially to Panda3D. I * [Mitchell Stokes](https://opencollective.com/mitchell-stokes) * [Daniel Stokes](https://opencollective.com/daniel-stokes) * [David Rose](https://opencollective.com/david-rose) -* [Carnetsoft](https://cs-driving-simulator.com/) ## Benefactors @@ -23,14 +22,14 @@ This is a list of all the people who are contributing financially to Panda3D. I * Sam Edwards * Max Voss -* Will Nielsen ## Enthusiasts -![Benefactors](https://opencollective.com/panda3d/tiers/enthusiast.svg?avatarHeight=48&width=600) +![Enthusiasts](https://opencollective.com/panda3d/tiers/enthusiast.svg?avatarHeight=48&width=600) * Eric Thomson * Kyle Roach +* Brian Lach ## Backers From 8c3fd5b406b85ed2e88bb452d9c38796a36dc767 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 2 Nov 2020 12:18:22 +0100 Subject: [PATCH 03/12] collide: Release GIL while during CollisionTraverser traversal This is necessary to prevent deadlocking on the GIL if some Python code is trying to write some geometry while the CollisionTraverser is trying to read it. Fixes #1033 --- panda/src/collide/collisionTraverser.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/collide/collisionTraverser.h b/panda/src/collide/collisionTraverser.h index 19b249cb45..fc039e3f7d 100644 --- a/panda/src/collide/collisionTraverser.h +++ b/panda/src/collide/collisionTraverser.h @@ -64,7 +64,7 @@ PUBLISHED: void clear_colliders(); MAKE_SEQ_PROPERTY(colliders, get_num_colliders, get_collider); - void traverse(const NodePath &root); + BLOCKING void traverse(const NodePath &root); #if defined(DO_COLLISION_RECORDING) || !defined(CPPPARSER) void set_recorder(CollisionRecorder *recorder); From c53f461f3fe945bb3cdb9cbf9b1fcae1235f554a Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 17 Nov 2020 11:59:36 +0100 Subject: [PATCH 04/12] gobj: Fix typo in MatrixLens API documentation Fixes #1042 --- panda/src/gobj/matrixLens.I | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/gobj/matrixLens.I b/panda/src/gobj/matrixLens.I index 74cc9296cf..b607106231 100644 --- a/panda/src/gobj/matrixLens.I +++ b/panda/src/gobj/matrixLens.I @@ -54,7 +54,7 @@ operator = (const MatrixLens ©) { * Explicitly specifies the projection matrix. This matrix should convert X * and Y to the range [-film_size/2, film_size/2], where (-fs/2,-fs/2) is the * lower left corner of the screen and (fs/2, fs/2) is the upper right. Z - * should go to the range [-1, 1], where -1 is the far plane and 1 is the near + * should go to the range [-1, 1], where -1 is the near plane and 1 is the far * plane. Note that this is a left-handed Y-up coordinate system. * * The default film_size for a MatrixLens is 2, so the default range is [-1, From f88441c584e730243a8b5cfca31f297d90f876dd Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 17 Nov 2020 12:01:05 +0100 Subject: [PATCH 05/12] dist: Fix error building Windows executable in Python 3.9 --- direct/src/dist/pefile.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/direct/src/dist/pefile.py b/direct/src/dist/pefile.py index 548c09e1b7..df7aac8432 100755 --- a/direct/src/dist/pefile.py +++ b/direct/src/dist/pefile.py @@ -246,7 +246,11 @@ class VersionInfoResource(object): length, value_length = unpack('= (3, 2): + dwords.frombytes(bytes(data[40:offset])) + else: + dwords.fromstring(bytes(data[40:offset])) + if len(dwords) > 0: self.signature = dwords[0] if len(dwords) > 1: From f1ca8a9018f5e88b240dac4a260b01128ef321e2 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 17 Nov 2020 12:01:42 +0100 Subject: [PATCH 06/12] direct: Additional linking in Loader API documentation --- direct/src/showbase/Loader.py | 59 ++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/direct/src/showbase/Loader.py b/direct/src/showbase/Loader.py index fd6424d917..711a40f30e 100644 --- a/direct/src/showbase/Loader.py +++ b/direct/src/showbase/Loader.py @@ -192,21 +192,21 @@ class Loader(DirectObject): pathname), the return value will be a NodePath to the model loaded if the load was successful, or None otherwise. If the input modelPath is a list of pathnames, the return value will - be a list of NodePaths and/or Nones. + be a list of `.NodePath` objects and/or Nones. loaderOptions may optionally be passed in to control details about the way the model is searched and loaded. See the - LoaderOptions class for more. + `.LoaderOptions` class for more. - The default is to look in the ModelPool (RAM) cache first, and - return a copy from that if the model can be found there. If - the bam cache is enabled (via the model-cache-dir config + The default is to look in the `.ModelPool` (RAM) cache first, + and return a copy from that if the model can be found there. + If the bam cache is enabled (via the `model-cache-dir` config variable), then that will be consulted next, and if both caches fail, the file will be loaded from disk. If noCache is True, then neither cache will be consulted or updated. If allowInstance is True, a shared instance may be returned - from the ModelPool. This is dangerous, since it is easy to + from the `.ModelPool`. This is dangerous, since it is easy to accidentally modify the shared instance, and invalidate future load attempts of the same model. Normally, you should leave allowInstance set to False, which will always return a unique @@ -214,10 +214,10 @@ class Loader(DirectObject): If okMissing is True, None is returned if the model is not found or cannot be read, and no error message is printed. - Otherwise, an IOError is raised if the model is not found or + Otherwise, an `IOError` is raised if the model is not found or cannot be read (similar to attempting to open a nonexistent - file). (If modelPath is a list of filenames, then IOError is - raised if *any* of the models could not be loaded.) + file). (If modelPath is a list of filenames, then `IOError` + is raised if *any* of the models could not be loaded.) If callback is not None, then the model load will be performed asynchronously. In this case, loadModel() will initiate a @@ -235,7 +235,7 @@ class Loader(DirectObject): True asynchronous model loading requires Panda to have been compiled with threading support enabled (you can test - Thread.isThreadingSupported()). In the absence of threading + `.Thread.isThreadingSupported()`). In the absence of threading support, the asynchronous interface still exists and still behaves exactly as described, except that loadModel() might not return immediately. @@ -420,7 +420,7 @@ class Loader(DirectObject): def saveModel(self, modelPath, node, loaderOptions = None, callback = None, extraArgs = [], priority = None, blocking = None): - """ Saves the model (a NodePath or PandaNode) to the indicated + """ Saves the model (a `NodePath` or `PandaNode`) to the indicated filename path. Returns true on success, false on failure. If a callback is used, the model is saved asynchronously, and the true/false status is passed to the callback function. """ @@ -508,8 +508,8 @@ class Loader(DirectObject): """ modelPath is a string. - This loads a special model as a TextFont object, for rendering - text with a TextNode. A font file must be either a special + This loads a special model as a `TextFont` object, for rendering + text with a `TextNode`. A font file must be either a special egg file (or bam file) generated with egg-mkfont, which is considered a static font, or a standard font file (like a TTF file) that is supported by FreeType, which is considered a @@ -573,7 +573,7 @@ class Loader(DirectObject): If color is not None, it should be a VBase4 specifying the foreground color of the font. Specifying this option breaks - TextNode.setColor(), so you almost never want to use this + `TextNode.setColor()`, so you almost never want to use this option; the default (white) is the most appropriate for a font, as it allows text to have any arbitrary color assigned at generation time. However, if you want to use a colored @@ -695,7 +695,8 @@ class Loader(DirectObject): texturePath is a string. Attempt to load a texture from the given file path using - TexturePool class. + `TexturePool` class. Returns a `Texture` object, or raises + `IOError` if the file could not be loaded. okMissing should be True to indicate the method should return None if the texture file is not found. If it is False, the @@ -713,17 +714,17 @@ class Loader(DirectObject): the texture and the number of expected mipmap images. If minfilter or magfilter is not None, they should be a symbol - like SamplerState.FTLinear or SamplerState.FTNearest. (minfilter - may be further one of the Mipmap filter type symbols.) These - specify the filter mode that will automatically be applied to - the texture when it is loaded. Note that this setting may + like `SamplerState.FTLinear` or `SamplerState.FTNearest`. + (minfilter may be further one of the Mipmap filter type symbols.) + These specify the filter mode that will automatically be applied + to the texture when it is loaded. Note that this setting may override the texture's existing settings, even if it has - already been loaded. See egg-texture-cards for a more robust + already been loaded. See `egg-texture-cards` for a more robust way to apply per-texture filter types and settings. If anisotropicDegree is not None, it specifies the anisotropic degree to apply to the texture when it is loaded. Like minfilter and - magfilter, egg-texture-cards may be a more robust way to apply + magfilter, `egg-texture-cards` may be a more robust way to apply this setting. If multiview is true, it indicates to load a multiview or @@ -769,7 +770,7 @@ class Loader(DirectObject): """ texturePattern is a string that contains a sequence of one or more hash characters ('#'), which will be filled in with the - z-height number. Returns a 3-D Texture object, suitable for + z-height number. Returns a 3-D `Texture` object, suitable for rendering volumetric textures. okMissing should be True to indicate the method should return @@ -826,7 +827,7 @@ class Loader(DirectObject): """ texturePattern is a string that contains a sequence of one or more hash characters ('#'), which will be filled in with the - z-height number. Returns a 2-D Texture array object, suitable + z-height number. Returns a 2-D `Texture` array object, suitable for rendering array of textures. okMissing should be True to indicate the method should return @@ -884,7 +885,7 @@ class Loader(DirectObject): texturePattern is a string that contains a sequence of one or more hash characters ('#'), which will be filled in with the face index number (0 through 6). Returns a six-face cube map - Texture object. + `Texture` object. okMissing should be True to indicate the method should return None if the texture file is not found. If it is False, the @@ -951,8 +952,8 @@ class Loader(DirectObject): """Loads one or more sound files, specifically designated as a "sound effect" file (that is, uses the sfxManager to load the sound). There is no distinction between sound effect files - and music files other than the particular AudioManager used to - load the sound file, but this distinction allows the sound + and music files other than the particular `AudioManager` used + to load the sound file, but this distinction allows the sound effects and/or the music files to be adjusted as a group, independently of the other group.""" @@ -965,8 +966,8 @@ class Loader(DirectObject): """Loads one or more sound files, specifically designated as a "music" file (that is, uses the musicManager to load the sound). There is no distinction between sound effect files - and music files other than the particular AudioManager used to - load the sound file, but this distinction allows the sound + and music files other than the particular `AudioManager` used + to load the sound file, but this distinction allows the sound effects and/or the music files to be adjusted as a group, independently of the other group.""" if(self.base.musicManager): @@ -1052,7 +1053,7 @@ class Loader(DirectObject): callback = None, extraArgs = []): """ Performs a model.flattenStrong() operation in a sub-thread (if threading is compiled into Panda). The model may be a - single NodePath, or it may be a list of NodePaths. + single `.NodePath`, or it may be a list of NodePaths. Each model is duplicated and flattened in the sub-thread. From 7eba53cffac5e57e1e2e192d17d4ea92a4c8d14c Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 14 Nov 2020 02:09:25 +0200 Subject: [PATCH 07/12] showbase: Fix an entire DirectSession being (re)created on node selection Closes #1051 --- direct/src/showbase/ShowBase.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index 1eb66ad94d..72197ffabf 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -157,6 +157,7 @@ class ShowBase(DirectObject.DirectObject): self.wantStats = self.config.GetBool('want-pstats', 0) self.wantTk = False self.wantWx = False + self.wantDirect = False #: Fill this in with a function to invoke when the user "exits" #: the program by closing the main window. @@ -3270,7 +3271,12 @@ class ShowBase(DirectObject.DirectObject): def startDirect(self, fWantDirect = 1, fWantTk = 1, fWantWx = 0): self.startTk(fWantTk) self.startWx(fWantWx) + + if self.wantDirect == fWantDirect: + return + self.wantDirect = fWantDirect + if self.wantDirect: # Use importlib to prevent this import from being picked up # by modulefinder when packaging an application. From a30f4be15795030a74cc7685028f1cbf831e48d5 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 17 Nov 2020 21:01:17 +0100 Subject: [PATCH 08/12] readme: Fix Travis badge on release/1.10.x branch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 89ba13fa5a..662492472a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/panda3d/panda3d.svg?branch=master)](https://travis-ci.org/panda3d/panda3d) +[![Build Status](https://api.travis-ci.org/panda3d/panda3d.svg?branch=release/1.10.x)](https://travis-ci.org/panda3d/panda3d) [![OpenCollective](https://opencollective.com/panda3d/backers/badge.svg)](https://opencollective.com/panda3d) [![OpenCollective](https://opencollective.com/panda3d/sponsors/badge.svg)](https://opencollective.com/panda3d) From 3a2048e44af94f213a66a1c6591a306ac41cc7ff Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 17 Nov 2020 21:01:56 +0100 Subject: [PATCH 09/12] glgsg: Fix skinning shader being unable to render unskinned models Panda was adding a column with weights (0, 0, 0, 0), but these weights don't add up to 1 so no useful identity matrix can be produced. Instead it's better to fall back to the OpenGL default, (0, 0, 0, 1). I'm also defaulting the transform_index values to (0, 1, 2, 3) to support non-indexed skinning (although that's pretty esoteric, given that that only supports 4 transforms...) --- panda/src/display/standardMunger.cxx | 5 +++-- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 11 +++++++++++ panda/src/glstuff/glGraphicsStateGuardian_src.h | 2 ++ panda/src/glstuff/glShaderContext_src.cxx | 4 ++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/panda/src/display/standardMunger.cxx b/panda/src/display/standardMunger.cxx index 2dba4aa6c0..10b66762a9 100644 --- a/panda/src/display/standardMunger.cxx +++ b/panda/src/display/standardMunger.cxx @@ -123,8 +123,9 @@ munge_data_impl(const GeomVertexData *data) { } GeomVertexAnimationSpec animation = new_data->get_format()->get_animation(); - if (_shader_skinning || (_auto_shader && hardware_animated_vertices && - !basic_shaders_only && animation.get_animation_type() == AT_panda)) { + if ((_shader_skinning && animation.get_animation_type() != AT_none) || + (_auto_shader && hardware_animated_vertices && + !basic_shaders_only && animation.get_animation_type() == AT_panda)) { animation.set_hardware(4, true); } else if (hardware_animated_vertices && diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 9083c31acc..dd3e2e25b4 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -1909,6 +1909,8 @@ reset() { get_extension_func("glUniform3uiv"); _glUniform4uiv = (PFNGLUNIFORM4UIVPROC) get_extension_func("glUniform4uiv"); + _glVertexAttribI4ui = (PFNGLVERTEXATTRIBI4UIPROC) + get_extension_func("glVertexAttribI4ui"); } else if (has_extension("GL_EXT_gpu_shader4")) { _glBindFragDataLocation = (PFNGLBINDFRAGDATALOCATIONPROC) @@ -1923,10 +1925,13 @@ reset() { get_extension_func("glUniform3uivEXT"); _glUniform4uiv = (PFNGLUNIFORM4UIVPROC) get_extension_func("glUniform4uivEXT"); + _glVertexAttribI4ui = (PFNGLVERTEXATTRIBI4UIPROC) + get_extension_func("glVertexAttribI4uiEXT"); } else { _glBindFragDataLocation = nullptr; _glVertexAttribIPointer = nullptr; + _glVertexAttribI4ui = nullptr; } if (is_at_least_gl_version(4, 1) || has_extension("GL_ARB_vertex_attrib_64bit")) { @@ -1955,8 +1960,11 @@ reset() { get_extension_func("glVertexAttribPointerARB"); _glBindFragDataLocation = nullptr; + _glVertexAttribI4ui = nullptr; _glVertexAttribIPointer = nullptr; _glVertexAttribLPointer = nullptr; + } else { + _glVertexAttribI4ui = nullptr; } #endif @@ -2003,8 +2011,11 @@ reset() { if (is_at_least_gles_version(3, 0)) { _glVertexAttribIPointer = (PFNGLVERTEXATTRIBIPOINTERPROC) get_extension_func("glVertexAttribIPointer"); + _glVertexAttribI4ui = (PFNGLVERTEXATTRIBI4UIPROC) + get_extension_func("glVertexAttribI4ui"); } else { _glVertexAttribIPointer = nullptr; + _glVertexAttribI4ui = nullptr; } if (has_extension("GL_EXT_blend_func_extended")) { diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index 1b5a1cdfb4..4fa16e067e 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -186,6 +186,7 @@ typedef void (APIENTRYP PFNGLUNIFORMMATRIX4FVPROC) (GLint location, GLsizei coun typedef void (APIENTRYP PFNGLVALIDATEPROGRAMPROC) (GLuint program); typedef void (APIENTRYP PFNGLVERTEXATTRIB4FVPROC) (GLuint index, const GLfloat *v); typedef void (APIENTRYP PFNGLVERTEXATTRIB4DVPROC) (GLuint index, const GLdouble *v); +typedef void (APIENTRYP PFNGLVERTEXATTRIBI4UIPROC) (GLuint index, GLuint x, GLuint y, GLuint z, GLuint w); typedef void (APIENTRYP PFNGLVERTEXATTRIBPOINTERPROC) (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer); typedef void (APIENTRYP PFNGLVERTEXATTRIBIPOINTERPROC) (GLuint index, GLint size, GLenum type, GLsizei stride, const GLvoid *pointer); typedef void (APIENTRYP PFNGLVERTEXATTRIBLPOINTERPROC) (GLuint index, GLint size, GLenum type, GLsizei stride, const GLvoid *pointer); @@ -1010,6 +1011,7 @@ public: PFNGLVALIDATEPROGRAMPROC _glValidateProgram; PFNGLVERTEXATTRIB4FVPROC _glVertexAttrib4fv; PFNGLVERTEXATTRIB4DVPROC _glVertexAttrib4dv; + PFNGLVERTEXATTRIBI4UIPROC _glVertexAttribI4ui; PFNGLVERTEXATTRIBPOINTERPROC _glVertexAttribPointer; PFNGLVERTEXATTRIBIPOINTERPROC _glVertexAttribIPointer; PFNGLVERTEXATTRIBLPOINTERPROC _glVertexAttribLPointer; diff --git a/panda/src/glstuff/glShaderContext_src.cxx b/panda/src/glstuff/glShaderContext_src.cxx index 9dd82c1cdf..28429941c4 100644 --- a/panda/src/glstuff/glShaderContext_src.cxx +++ b/panda/src/glstuff/glShaderContext_src.cxx @@ -2529,6 +2529,10 @@ update_shader_vertex_arrays(ShaderContext *prev, bool force) { _glgsg->_glVertexAttrib4fv(p, _glgsg->_scene_graph_color.get_data()); #endif } + else if (name == InternalName::get_transform_index() && + _glgsg->_glVertexAttribI4ui != nullptr) { + _glgsg->_glVertexAttribI4ui(p, 0, 1, 2, 3); + } } } From 3abd1573154d30d9377cb1370615a0545118b18f Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 17 Nov 2020 21:05:08 +0100 Subject: [PATCH 10/12] gobj: Fix a bad assertion comparison --- panda/src/gobj/geomVertexData.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/gobj/geomVertexData.cxx b/panda/src/gobj/geomVertexData.cxx index d7bf35ba9d..d77ca492a1 100644 --- a/panda/src/gobj/geomVertexData.cxx +++ b/panda/src/gobj/geomVertexData.cxx @@ -659,7 +659,7 @@ copy_from(const GeomVertexData *source, bool keep_data_objects, for (size_t i = 0; i < blend.get_num_transforms(); i++) { int index = add_transform(transform_table, blend.get_transform(i), already_added); - nassertv(index <= 4); + nassertv(index < 4); weights[index] = blend.get_weight(i); } if (weight.has_column()) { From a6e6826939062d5432aff91d7082aca4a46e2586 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 22 Mar 2020 11:42:22 +0100 Subject: [PATCH 11/12] interrogate: support __getstate__ and __setstate__ The latter in particular will be called instead of __init__, so must construct the object. --- .../interfaceMakerPythonNative.cxx | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/dtool/src/interrogate/interfaceMakerPythonNative.cxx b/dtool/src/interrogate/interfaceMakerPythonNative.cxx index 2856c95fba..eea5ee7329 100644 --- a/dtool/src/interrogate/interfaceMakerPythonNative.cxx +++ b/dtool/src/interrogate/interfaceMakerPythonNative.cxx @@ -108,6 +108,8 @@ RenameSet methodRenameDictionary[] = { { "__reduce_persist__", "__reduce_persist__", 0 }, { "__copy__" , "__copy__", 0 }, { "__deepcopy__" , "__deepcopy__", 0 }, + { "__getstate__" , "__getstate__", 0 }, + { "__setstate__" , "__setstate__", 0 }, { "print" , "Cprint", 0 }, { "CInterval.set_t", "_priv__cSetT", 0 }, { nullptr, nullptr, -1 } @@ -3615,17 +3617,28 @@ write_function_for_name(ostream &out, Object *obj, std::string cClassName = obj->_itype.get_true_name(); // string class_name = remap->_cpptype->get_simple_name(); - // Extract pointer from 'self' parameter. - out << " " << cClassName << " *local_this = nullptr;\n"; - - if (all_nonconst) { + // If this is a non-static __setstate__, we run the default constructor. + if (remap->_cppfunc->get_local_name() == "__setstate__") { + out << " if (DtoolInstance_VOID_PTR(self) != nullptr) {\n" + << " Dtool_Raise_TypeError(\"C++ object is already constructed.\");\n"; + error_return(out, 4, return_flags); + out << " }\n" + << " " << cClassName << " *local_this = new " << cClassName << ";\n" + << " DTool_PyInit_Finalize(self, local_this, &Dtool_" << ClassName + << ", false, false);\n" + << " if (local_this == nullptr) {\n" + << " PyErr_NoMemory();\n"; + } + else if (all_nonconst) { // All remaps are non-const. Also check that this object isn't const. - out << " if (!Dtool_Call_ExtractThisPointer_NonConst(self, Dtool_" << ClassName << ", " + out << " " << cClassName << " *local_this = nullptr;\n" + << " if (!Dtool_Call_ExtractThisPointer_NonConst(self, Dtool_" << ClassName << ", " << "(void **)&local_this, \"" << classNameFromCppName(cClassName, false) << "." << methodNameFromCppName(remap, cClassName, false) << "\")) {\n"; - - } else { - out << " if (!DtoolInstance_GetPointer(self, local_this, Dtool_" << ClassName << ")) {\n"; + } + else { + out << " " << cClassName << " *local_this = nullptr;\n" + << " if (!DtoolInstance_GetPointer(self, local_this, Dtool_" << ClassName << ")) {\n"; } error_return(out, 4, return_flags); From 93900a203e3a2091be3b03a22f158dcf97d5d750 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 17 Nov 2020 23:36:06 +0100 Subject: [PATCH 12/12] putil: Backport part of 9d8c523dfa83f37cc15095bc8f4fae5f7f996bc6 Fixes #886 --- dtool/src/interrogatedb/py_compat.h | 16 ++++ panda/src/putil/bitArray.h | 6 ++ panda/src/putil/bitArray_ext.I | 12 +++ panda/src/putil/bitArray_ext.cxx | 101 ++++++++++++++++++++++ panda/src/putil/bitArray_ext.h | 42 +++++++++ panda/src/putil/bitMask.h | 1 + panda/src/putil/bitMask_ext.I | 35 ++++++++ panda/src/putil/bitMask_ext.h | 40 +++++++++ panda/src/putil/doubleBitMask.h | 6 ++ panda/src/putil/doubleBitMask_ext.I | 84 ++++++++++++++++++ panda/src/putil/doubleBitMask_ext.h | 44 ++++++++++ panda/src/putil/p3putil_ext_composite.cxx | 2 + panda/src/putil/sparseArray.h | 6 ++ panda/src/putil/sparseArray_ext.I | 12 +++ panda/src/putil/sparseArray_ext.cxx | 93 ++++++++++++++++++++ panda/src/putil/sparseArray_ext.h | 40 +++++++++ tests/putil/test_bitarray.py | 32 +++++++ tests/putil/test_bitmask.py | 40 +++++++-- tests/putil/test_sparsearray.py | 51 +++++++++++ 19 files changed, 656 insertions(+), 7 deletions(-) create mode 100644 panda/src/putil/bitArray_ext.I create mode 100644 panda/src/putil/bitArray_ext.cxx create mode 100644 panda/src/putil/bitArray_ext.h create mode 100644 panda/src/putil/bitMask_ext.I create mode 100644 panda/src/putil/bitMask_ext.h create mode 100644 panda/src/putil/doubleBitMask_ext.I create mode 100644 panda/src/putil/doubleBitMask_ext.h create mode 100644 panda/src/putil/sparseArray_ext.I create mode 100644 panda/src/putil/sparseArray_ext.cxx create mode 100644 panda/src/putil/sparseArray_ext.h diff --git a/dtool/src/interrogatedb/py_compat.h b/dtool/src/interrogatedb/py_compat.h index 5d0f3bf4ea..2152a115cc 100644 --- a/dtool/src/interrogatedb/py_compat.h +++ b/dtool/src/interrogatedb/py_compat.h @@ -192,6 +192,22 @@ INLINE PyObject *_PyObject_FastCall(PyObject *func, PyObject **args, Py_ssize_t } while (0) #endif +/* Python 3.8 */ +#if PY_VERSION_HEX < 0x03080000 +INLINE PyObject *_PyLong_Rshift(PyObject *a, size_t shiftby) { + PyObject *b = PyLong_FromLong(shiftby); + PyObject *result = PyNumber_Rshift(a, b); + Py_DECREF(b); + return result; +} +INLINE PyObject *_PyLong_Lshift(PyObject *a, size_t shiftby) { + PyObject *b = PyLong_FromLong(shiftby); + PyObject *result = PyNumber_Lshift(a, b); + Py_DECREF(b); + return result; +} +#endif + /* Other Python implementations */ // _PyErr_OCCURRED is an undocumented macro version of PyErr_Occurred. diff --git a/panda/src/putil/bitArray.h b/panda/src/putil/bitArray.h index 0338747295..a2b8354008 100644 --- a/panda/src/putil/bitArray.h +++ b/panda/src/putil/bitArray.h @@ -21,6 +21,7 @@ #include "typedObject.h" #include "indent.h" #include "pointerToArray.h" +#include "extension.h" #include "checksumHashGenerator.h" @@ -125,6 +126,9 @@ PUBLISHED: void operator <<= (int shift); void operator >>= (int shift); + EXTENSION(PyObject *__getstate__() const); + EXTENSION(void __setstate__(PyObject *state)); + public: void generate_hash(ChecksumHashGenerator &hashgen) const; @@ -138,6 +142,8 @@ private: Array _array; int _highest_bits; // Either 0 or 1. + friend class Extension; + public: void write_datagram(BamWriter *manager, Datagram &dg) const; void read_datagram(DatagramIterator &scan, BamReader *manager); diff --git a/panda/src/putil/bitArray_ext.I b/panda/src/putil/bitArray_ext.I new file mode 100644 index 0000000000..fb3a46156a --- /dev/null +++ b/panda/src/putil/bitArray_ext.I @@ -0,0 +1,12 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file bitArray_ext.I + * @author rdb + * @date 2020-03-21 + */ diff --git a/panda/src/putil/bitArray_ext.cxx b/panda/src/putil/bitArray_ext.cxx new file mode 100644 index 0000000000..215d920cb4 --- /dev/null +++ b/panda/src/putil/bitArray_ext.cxx @@ -0,0 +1,101 @@ +/** + * 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 bitArray_ext.cxx + * @author rdb + * @date 2020-03-21 + */ + +#include "bitArray_ext.h" + +#ifdef HAVE_PYTHON + +/** + * Creates a BitArray from a Python long object. + */ +void Extension:: +__init__(PyObject *init_value) { +#if PY_MAJOR_VERSION < 3 + if (PyInt_Check(init_value)) { + long value = PyInt_AS_LONG(init_value); + if (value >= 0) { + _this->set_word(0, value); + } else { + PyErr_SetString(PyExc_ValueError, "BitArray constructor requires a positive integer"); + } + return; + } +#endif + + if (!PyLong_Check(init_value) || Py_SIZE(init_value) < 0) { + PyErr_SetString(PyExc_ValueError, "BitArray constructor requires a positive integer"); + return; + } + + int n = _PyLong_NumBits(init_value); + if (n > 0) { + size_t num_words = (n + BitArray::num_bits_per_word - 1) / BitArray::num_bits_per_word; + _this->_array.resize(num_words); + _PyLong_AsByteArray((PyLongObject *)init_value, + (unsigned char *)&_this->_array[0], + num_words * sizeof(BitArray::WordType), + 1, 0); + } +} + +/** + * Returns the value of the BitArray as a picklable Python object. + * + * We could just return a list of words. However, different builds of Panda3D + * may have different sizes for the WordType, so we'd also need to add code to + * convert between different WordTypes. Instead, we'll just encode the whole + * array as a Python long, with infinite arrays stored as inverted longs. + */ +PyObject *Extension:: +__getstate__() const { + if (_this->_array.empty()) { + return PyLong_FromLong(-_this->_highest_bits); + } + + if (_this->_highest_bits == 0) { + return _PyLong_FromByteArray( + (const unsigned char *)&_this->_array[0], + _this->_array.size() * sizeof(BitArray::WordType), + 1, 0); + } else { + // This is an infinite array, so we invert it to make it a finite array and + // store it as an inverted long. + BitArray copy(*_this); + copy.invert_in_place(); + PyObject *state = _PyLong_FromByteArray( + (const unsigned char *)©._array[0], + copy._array.size() * sizeof(BitArray::WordType), + 1, 0); + PyObject *inverted = PyNumber_Invert(state); + Py_DECREF(state); + return inverted; + } +} + +/** + * Takes the value returned by __getstate__ and uses it to freshly initialize + * this BitArray object. + */ +void Extension:: +__setstate__(PyObject *state) { + if (Py_SIZE(state) >= 0) { + __init__(state); + } else { + PyObject *inverted = PyNumber_Invert(state); + __init__(inverted); + Py_DECREF(inverted); + _this->invert_in_place(); + } +} + +#endif diff --git a/panda/src/putil/bitArray_ext.h b/panda/src/putil/bitArray_ext.h new file mode 100644 index 0000000000..d39cdc0c0b --- /dev/null +++ b/panda/src/putil/bitArray_ext.h @@ -0,0 +1,42 @@ +/** + * 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 bitArray_ext.h + * @author rdb + * @date 2020-03-21 + */ + +#ifndef BITARRAY_EXT_H +#define BITARRAY_EXT_H + +#include "dtoolbase.h" + +#ifdef HAVE_PYTHON + +#include "extension.h" +#include "bitArray.h" +#include "py_panda.h" + +/** + * This class defines the extension methods for BitArray, which are called + * instead of any C++ methods with the same prototype. + */ +template<> +class Extension : public ExtensionBase { +public: + void __init__(PyObject *init_value); + + PyObject *__getstate__() const; + void __setstate__(PyObject *state); +}; + +#include "bitArray_ext.I" + +#endif // HAVE_PYTHON + +#endif // BITARRAY_EXT_H diff --git a/panda/src/putil/bitMask.h b/panda/src/putil/bitMask.h index ca5c9d8362..acd93d46ce 100644 --- a/panda/src/putil/bitMask.h +++ b/panda/src/putil/bitMask.h @@ -126,6 +126,7 @@ PUBLISHED: INLINE int get_key() const; INLINE bool __nonzero__() const; + EXTENSION(PyObject *__reduce__(PyObject *self) const); public: INLINE void generate_hash(ChecksumHashGenerator &hashgen) const; diff --git a/panda/src/putil/bitMask_ext.I b/panda/src/putil/bitMask_ext.I new file mode 100644 index 0000000000..63d87fd75d --- /dev/null +++ b/panda/src/putil/bitMask_ext.I @@ -0,0 +1,35 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file bitMask_ext.I + * @author rdb + * @date 2020-03-22 + */ + + +/** + * Returns the value as an integer. + */ +template +INLINE PyObject *Extension >:: +__int__() const { + return Dtool_WrapValue(this->_this->get_word()); +} + +/** + * This special Python method is implemented to provide support for the pickle + * module. + */ +template +INLINE PyObject *Extension >:: +__reduce__(PyObject *self) const { + // We should return at least a 2-tuple, (Class, (args)): the necessary class + // object whose constructor we should call (e.g. this), and the arguments + // necessary to reconstruct this object. + return Py_BuildValue("(O(k))", Py_TYPE(self), this->_this->get_word()); +} diff --git a/panda/src/putil/bitMask_ext.h b/panda/src/putil/bitMask_ext.h new file mode 100644 index 0000000000..2a84d85c74 --- /dev/null +++ b/panda/src/putil/bitMask_ext.h @@ -0,0 +1,40 @@ +/** + * 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 bitMask_ext.h + * @author rdb + * @date 2020-03-22 + */ + +#ifndef BITMASK_EXT_H +#define BITMASK_EXT_H + +#include "dtoolbase.h" + +#ifdef HAVE_PYTHON + +#include "extension.h" +#include "bitMask.h" +#include "py_panda.h" + +/** + * This class defines the extension methods for BitMask, which are called + * instead of any C++ methods with the same prototype. + */ +template +class Extension > : public ExtensionBase > { +public: + INLINE PyObject *__int__() const; + INLINE PyObject *__reduce__(PyObject *self) const; +}; + +#include "bitMask_ext.I" + +#endif // HAVE_PYTHON + +#endif // BITMASK_EXT_H diff --git a/panda/src/putil/doubleBitMask.h b/panda/src/putil/doubleBitMask.h index 7ebc114f5f..5712ad201e 100644 --- a/panda/src/putil/doubleBitMask.h +++ b/panda/src/putil/doubleBitMask.h @@ -17,6 +17,7 @@ #include "pandabase.h" #include "bitMask.h" +#include "extension.h" /** * This is a special BitMask type that is implemented as a pair of lesser @@ -38,6 +39,7 @@ PUBLISHED: }; constexpr DoubleBitMask() = default; + EXTENSION(DoubleBitMask(PyObject *init_value)); INLINE static DoubleBitMask all_on(); INLINE static DoubleBitMask all_off(); @@ -110,12 +112,16 @@ PUBLISHED: INLINE void operator <<= (int shift); INLINE void operator >>= (int shift); + EXTENSION(PyObject *__reduce__(PyObject *self) const); + public: INLINE void generate_hash(ChecksumHashGenerator &hashgen) const; private: BitMaskType _lo, _hi; + friend class Extension; + public: static TypeHandle get_class_type() { return _type_handle; diff --git a/panda/src/putil/doubleBitMask_ext.I b/panda/src/putil/doubleBitMask_ext.I new file mode 100644 index 0000000000..b774f22096 --- /dev/null +++ b/panda/src/putil/doubleBitMask_ext.I @@ -0,0 +1,84 @@ +/** + * 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 doubleBitMask_ext.I + * @author rdb + * @date 2020-04-01 + */ + +/** + * Initializes a DoubleBitMask from a Python long integer. + */ +template +INLINE void Extension >:: +__init__(PyObject *init_value) { +#if PY_MAJOR_VERSION < 3 + if (PyInt_Check(init_value)) { + long value = PyInt_AS_LONG(init_value); + if (value >= 0) { + this->_this->store((typename BMType::WordType)value, 0, sizeof(long) * 8 - 1); + } else { + PyErr_SetString(PyExc_ValueError, "DoubleBitMask constructor requires a positive integer"); + } + return; + } +#endif + + if (!PyLong_Check(init_value) || Py_SIZE(init_value) < 0) { + PyErr_SetString(PyExc_ValueError, "DoubleBitMask constructor requires a positive integer"); + return; + } + + int n = _PyLong_NumBits(init_value); + if (n > DoubleBitMask::num_bits) { + PyErr_SetString(PyExc_OverflowError, "value out of range for DoubleBitMask"); + return; + } + + if (n > 0) { + size_t num_bytes = (n + 7) / 8; + unsigned char *bytes = (unsigned char *)alloca(num_bytes); + _PyLong_AsByteArray((PyLongObject *)init_value, bytes, num_bytes, 1, 0); + + for (size_t i = 0; i < num_bytes; ++i) { + this->_this->store(bytes[i], i * 8, 8); + } + } +} + +/** + * Returns the value as an integer. + */ +template +INLINE PyObject *Extension >:: +__int__() const { + PyObject *result = invoke_extension(&this->_this->_lo).__int__(); + if (!this->_this->_hi.is_zero()) { + PyObject *lo = result; + PyObject *hi = invoke_extension(&this->_this->_hi).__int__(); + PyObject *shifted = _PyLong_Lshift(hi, DoubleBitMask::half_bits); + Py_DECREF(hi); + result = PyNumber_Or(shifted, lo); + Py_DECREF(shifted); + Py_DECREF(lo); + } + return result; +} + +/** + * This special Python method is implemented to provide support for the pickle + * module. + */ +template +INLINE PyObject *Extension >:: +__reduce__(PyObject *self) const { + // We should return at least a 2-tuple, (Class, (args)): the necessary class + // object whose constructor we should call (e.g. this), and the arguments + // necessary to reconstruct this object. + return Py_BuildValue("(O(N))", Py_TYPE(self), __int__()); +} diff --git a/panda/src/putil/doubleBitMask_ext.h b/panda/src/putil/doubleBitMask_ext.h new file mode 100644 index 0000000000..709e4e9604 --- /dev/null +++ b/panda/src/putil/doubleBitMask_ext.h @@ -0,0 +1,44 @@ +/** + * 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 doubleBitMask_ext.h + * @author rdb + * @date 2020-04-01 + */ + +#ifndef DOUBLEBITMASK_EXT_H +#define DOUBLEBITMASK_EXT_H + +#include "dtoolbase.h" + +#ifdef HAVE_PYTHON + +#include "extension.h" +#include "doubleBitMask.h" +#include "py_panda.h" + +#include "bitMask_ext.h" + +/** + * This class defines the extension methods for DoubleBitMask, which are called + * instead of any C++ methods with the same prototype. + */ +template +class Extension > : public ExtensionBase > { +public: + INLINE void __init__(PyObject *init_value); + + INLINE PyObject *__int__() const; + INLINE PyObject *__reduce__(PyObject *self) const; +}; + +#include "doubleBitMask_ext.I" + +#endif // HAVE_PYTHON + +#endif // DOUBLEBITMASK_EXT_H diff --git a/panda/src/putil/p3putil_ext_composite.cxx b/panda/src/putil/p3putil_ext_composite.cxx index 4bedeea559..4dde9e5c98 100644 --- a/panda/src/putil/p3putil_ext_composite.cxx +++ b/panda/src/putil/p3putil_ext_composite.cxx @@ -1,3 +1,5 @@ #include "bamReader_ext.cxx" +#include "bitArray_ext.cxx" #include "pythonCallbackObject.cxx" +#include "sparseArray_ext.cxx" #include "typedWritable_ext.cxx" diff --git a/panda/src/putil/sparseArray.h b/panda/src/putil/sparseArray.h index de6a508d29..fe76ab95ea 100644 --- a/panda/src/putil/sparseArray.h +++ b/panda/src/putil/sparseArray.h @@ -16,6 +16,7 @@ #include "pandabase.h" #include "ordered_vector.h" +#include "extension.h" class BitArray; class BamWriter; @@ -116,6 +117,9 @@ PUBLISHED: INLINE int get_subrange_begin(size_t n) const; INLINE int get_subrange_end(size_t n) const; + EXTENSION(PyObject *__getstate__() const); + EXTENSION(void __setstate__(PyObject *state)); + private: void do_add_range(int begin, int end); void do_remove_range(int begin, int end); @@ -140,6 +144,8 @@ private: Subranges _subranges; bool _inverse; + friend class Extension; + public: void write_datagram(BamWriter *manager, Datagram &dg) const; void read_datagram(DatagramIterator &scan, BamReader *manager); diff --git a/panda/src/putil/sparseArray_ext.I b/panda/src/putil/sparseArray_ext.I new file mode 100644 index 0000000000..a69e3ad287 --- /dev/null +++ b/panda/src/putil/sparseArray_ext.I @@ -0,0 +1,12 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file sparseArray_ext.I + * @author rdb + * @date 2020-03-21 + */ diff --git a/panda/src/putil/sparseArray_ext.cxx b/panda/src/putil/sparseArray_ext.cxx new file mode 100644 index 0000000000..16049ebd8d --- /dev/null +++ b/panda/src/putil/sparseArray_ext.cxx @@ -0,0 +1,93 @@ +/** + * 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 sparseArray_ext.cxx + * @author rdb + * @date 2020-03-21 + */ + +#include "sparseArray_ext.h" + +#ifdef HAVE_PYTHON + +/** + * Returns the value of the SparseArray as a picklable Python object. + * + * We store this as a tuple of integers. The first number indicates the first + * bit that is set to 1, the second number indicates the next bit that is set to + * 0, the third number indicates the next bit that is set to 1, etc. Using this + * method, we store an uneven number of integers for an inverted SparseArray, + * and an even number for a regular SparseArray. + * + * This table demonstrates the three different cases: + * + * | range | pickled | SparseArray | + * |------------|-------------|-------------| + * | 0-2, 4-8 | 0, 2, 4, 8 | 0-2, 4-8 | + * | 0-2, 4-... | 0, 2, 4 | ~ 2-4 | + * | 2-4, 6-... | 2, 4, 6 | ~ 0-2, 4-6 | + * + */ +PyObject *Extension:: +__getstate__() const { + PyObject *state; + Py_ssize_t index = 0; + size_t sri = 0; + size_t num_ranges = _this->get_num_subranges(); + + if (!_this->is_inverse()) { + state = PyTuple_New(num_ranges * 2); + } + else if (num_ranges > 0 && _this->get_subrange_begin(0) == 0) { + // Prevent adding a useless 0-0 range at the beginning. + state = PyTuple_New(num_ranges * 2 - 1); + PyTuple_SET_ITEM(state, index++, Dtool_WrapValue(_this->get_subrange_end(sri++))); + } + else { + state = PyTuple_New(num_ranges * 2 + 1); + PyTuple_SET_ITEM(state, index++, Dtool_WrapValue(0)); + } + + for (; sri < num_ranges; ++sri) { + PyTuple_SET_ITEM(state, index++, Dtool_WrapValue(_this->get_subrange_begin(sri))); + PyTuple_SET_ITEM(state, index++, Dtool_WrapValue(_this->get_subrange_end(sri))); + } + return state; +} + +/** + * Takes the tuple returned by __getstate__ and uses it to freshly initialize + * this SparseArray object. + */ +void Extension:: +__setstate__(PyObject *state) { + _this->clear(); + + Py_ssize_t i = 0; + Py_ssize_t len = PyTuple_GET_SIZE(state); + if (len % 2 != 0) { + // An uneven number of elements indicates an open final range. + // This translates to an inverted range in SparseArray's representation. + _this->invert_in_place(); + long first = PyLongOrInt_AS_LONG(PyTuple_GET_ITEM(state, 0)); + if (first != 0) { + // It doesn't start at 0, so we have to first disable this range. + _this->do_add_range(0, (int)first); + } + ++i; + } + + for (; i < len; i += 2) { + _this->do_add_range( + PyLongOrInt_AS_LONG(PyTuple_GET_ITEM(state, i)), + PyLongOrInt_AS_LONG(PyTuple_GET_ITEM(state, i + 1)) + ); + } +} + +#endif diff --git a/panda/src/putil/sparseArray_ext.h b/panda/src/putil/sparseArray_ext.h new file mode 100644 index 0000000000..4fb5c1c7b5 --- /dev/null +++ b/panda/src/putil/sparseArray_ext.h @@ -0,0 +1,40 @@ +/** + * 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 sparseArray_ext.h + * @author rdb + * @date 2020-03-21 + */ + +#ifndef SPARSEARRAY_EXT_H +#define SPARSEARRAY_EXT_H + +#include "dtoolbase.h" + +#ifdef HAVE_PYTHON + +#include "extension.h" +#include "sparseArray.h" +#include "py_panda.h" + +/** + * This class defines the extension methods for SparseArray, which are called + * instead of any C++ methods with the same prototype. + */ +template<> +class Extension : public ExtensionBase { +public: + PyObject *__getstate__() const; + void __setstate__(PyObject *state); +}; + +#include "sparseArray_ext.I" + +#endif // HAVE_PYTHON + +#endif // SPARSEARRAY_EXT_H diff --git a/tests/putil/test_bitarray.py b/tests/putil/test_bitarray.py index 66e4292e76..5da2fdc725 100644 --- a/tests/putil/test_bitarray.py +++ b/tests/putil/test_bitarray.py @@ -1,4 +1,36 @@ from panda3d.core import BitArray +import pickle +import pytest + + +def test_bitarray_allon(): + assert BitArray.all_on().is_all_on() + + +def test_bitarray_invert(): + assert ~BitArray(0) != BitArray(0) + assert (~BitArray(0)).is_all_on() + assert ~~BitArray(0) == BitArray(0) + assert ~~BitArray(123) == BitArray(123) + + +def test_bitarray_getstate(): + assert BitArray().__getstate__() == 0 + assert BitArray(0).__getstate__() == 0 + assert BitArray(100).__getstate__() == 100 + assert BitArray.all_on().__getstate__() == -1 + assert ~BitArray(100).__getstate__() == ~100 + + +def test_bitarray_pickle(): + ba = BitArray() + assert ba == pickle.loads(pickle.dumps(ba, -1)) + + ba = BitArray(0) + assert ba == pickle.loads(pickle.dumps(ba, -1)) + + ba = BitArray(123) + assert ba == pickle.loads(pickle.dumps(ba, -1)) def test_bitarray_has_any_of(): diff --git a/tests/putil/test_bitmask.py b/tests/putil/test_bitmask.py index e87984cac1..b2bbdf9c79 100644 --- a/tests/putil/test_bitmask.py +++ b/tests/putil/test_bitmask.py @@ -1,10 +1,36 @@ -from panda3d import core +from panda3d.core import BitMask16, BitMask32, BitMask64 +from panda3d.core import DoubleBitMaskNative, QuadBitMaskNative +import pickle +import pytest + + +double_num_bits = DoubleBitMaskNative.get_max_num_bits() +quad_num_bits = QuadBitMaskNative.get_max_num_bits() def test_bitmask_allon(): - assert core.BitMask16.all_on().is_all_on() - assert core.BitMask32.all_on().is_all_on() - assert core.BitMask64.all_on().is_all_on() - assert core.DoubleBitMaskNative.all_on().is_all_on() - assert core.QuadBitMaskNative.all_on().is_all_on() - assert core.BitArray.all_on().is_all_on() + assert BitMask16.all_on().is_all_on() + assert BitMask32.all_on().is_all_on() + assert BitMask64.all_on().is_all_on() + assert DoubleBitMaskNative.all_on().is_all_on() + assert QuadBitMaskNative.all_on().is_all_on() + + assert DoubleBitMaskNative((1 << double_num_bits) - 1).is_all_on() + assert QuadBitMaskNative((1 << quad_num_bits) - 1).is_all_on() + + +def test_bitmask_overflow(): + with pytest.raises(OverflowError): + DoubleBitMaskNative(1 << double_num_bits) + + with pytest.raises(OverflowError): + QuadBitMaskNative(1 << quad_num_bits) + + +def test_bitmask_pickle(): + assert pickle.loads(pickle.dumps(BitMask16(0), -1)).is_zero() + + mask1 = BitMask16(123) + data = pickle.dumps(mask1, -1) + mask2 = pickle.loads(data) + assert mask1 == mask2 diff --git a/tests/putil/test_sparsearray.py b/tests/putil/test_sparsearray.py index b7767d6e8a..5138914cdf 100644 --- a/tests/putil/test_sparsearray.py +++ b/tests/putil/test_sparsearray.py @@ -1,4 +1,5 @@ from panda3d import core +import pickle def test_sparse_array_set_bit_to(): @@ -232,3 +233,53 @@ def test_sparse_array_augm_assignment(): u = core.SparseArray() t ^= u assert s is t + + +def test_sparse_array_getstate(): + sa = core.SparseArray() + assert sa.__getstate__() == () + + sa = core.SparseArray() + sa.invert_in_place() + assert sa.__getstate__() == (0,) + + sa = core.SparseArray() + sa.set_range(0, 2) + sa.set_range(4, 4) + assert sa.__getstate__() == (0, 2, 4, 8) + + sa = core.SparseArray() + sa.invert_in_place() + sa.clear_range(2, 4) + assert sa.__getstate__() == (0, 2, 6) + + sa = core.SparseArray() + sa.invert_in_place() + sa.clear_range(0, 2) + sa.clear_range(4, 4) + assert sa.__getstate__() == (2, 4, 8) + + +def test_sparse_array_pickle(): + sa = core.SparseArray() + assert sa == pickle.loads(pickle.dumps(sa, -1)) + + sa = core.SparseArray() + sa.invert_in_place() + assert sa == pickle.loads(pickle.dumps(sa, -1)) + + sa = core.SparseArray() + sa.set_range(0, 2) + sa.set_range(4, 4) + assert sa == pickle.loads(pickle.dumps(sa, -1)) + + sa = core.SparseArray() + sa.invert_in_place() + sa.clear_range(2, 4) + assert sa == pickle.loads(pickle.dumps(sa, -1)) + + sa = core.SparseArray() + sa.invert_in_place() + sa.clear_range(0, 2) + sa.clear_range(4, 4) + assert sa == pickle.loads(pickle.dumps(sa, -1))