From 8d22b80698805b4cb69108863786244a3daf3592 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 31 Dec 2020 16:43:37 +0100 Subject: [PATCH 01/19] display: Fix memory leak in GSG::get_prepared_textures() --- panda/src/display/graphicsStateGuardian_ext.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/panda/src/display/graphicsStateGuardian_ext.cxx b/panda/src/display/graphicsStateGuardian_ext.cxx index 3738ef82b8..9e30ec4f30 100644 --- a/panda/src/display/graphicsStateGuardian_ext.cxx +++ b/panda/src/display/graphicsStateGuardian_ext.cxx @@ -25,10 +25,11 @@ static bool traverse_callback(TextureContext *tc, void *data) { PyObject *element = DTool_CreatePyInstanceTyped(tex, Dtool_Texture, true, false, tex->get_type_index()); - tex->ref(); + tex.cheat() = nullptr; PyObject *list = (PyObject *) data; PyList_Append(list, element); + Py_DECREF(element); return true; } From ac991e4c5e6eb67694a5afb86283e00b04c8f952 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 31 Dec 2020 16:53:49 +0100 Subject: [PATCH 02/19] collide: Fix error with CollisionHandler's again_patterns property --- panda/src/collide/collisionHandlerEvent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/collide/collisionHandlerEvent.h b/panda/src/collide/collisionHandlerEvent.h index 808ee888c5..05f15f025a 100644 --- a/panda/src/collide/collisionHandlerEvent.h +++ b/panda/src/collide/collisionHandlerEvent.h @@ -61,7 +61,7 @@ PUBLISHED: MAKE_SEQ(get_out_patterns, get_num_out_patterns, get_out_pattern); MAKE_SEQ_PROPERTY(in_patterns, get_num_in_patterns, get_in_pattern); - MAKE_SEQ_PROPERTY(again_patterns, get_num_again_patterns, get_out_pattern); + MAKE_SEQ_PROPERTY(again_patterns, get_num_again_patterns, get_again_pattern); MAKE_SEQ_PROPERTY(out_patterns, get_num_out_patterns, get_out_pattern); void clear(); From a5557bc38d49c036bb8add0adef7ed937c425833 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 31 Dec 2020 16:47:00 +0100 Subject: [PATCH 03/19] stdpy: Fix pickle sometimes duplicating Panda objects We have to unify multiple Python wrappers pointing to the same C++ object. --- direct/src/stdpy/pickle.py | 41 +++++++++++++++++++------------------- tests/stdpy/test_pickle.py | 14 +++++++++++++ 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/direct/src/stdpy/pickle.py b/direct/src/stdpy/pickle.py index 1c59888958..db4a066572 100644 --- a/direct/src/stdpy/pickle.py +++ b/direct/src/stdpy/pickle.py @@ -22,7 +22,7 @@ Unfortunately, cPickle cannot be supported, because it does not support extensions of this nature. """ import sys -from panda3d.core import BamWriter, BamReader +from panda3d.core import BamWriter, BamReader, TypedObject if sys.version_info >= (3, 0): from copyreg import dispatch_table @@ -47,6 +47,7 @@ class _Pickler(BasePickler): def __init__(self, *args, **kw): self.bamWriter = BamWriter() + self._canonical = {} BasePickler.__init__(self, *args, **kw) # We have to duplicate most of the save() method, so we can add @@ -62,6 +63,21 @@ class _Pickler(BasePickler): self.save_pers(pid) return + # Check if this is a Panda type that we've already saved; if so, store + # a mapping to the canonical copy, so that Python's memoization system + # works properly. This is needed because Python uses id(obj) for + # memoization, but there may be multiple Python wrappers for the same + # C++ pointer, and we don't want that to result in duplication. + t = type(obj) + if issubclass(t, TypedObject.__base__): + canonical = self._canonical.get(obj.this) + if canonical is not None: + obj = canonical + else: + # First time we're seeing this C++ pointer; save it as the + # "canonical" version. + self._canonical[obj.this] = obj + # Check the memo x = self.memo.get(id(obj)) if x: @@ -69,7 +85,6 @@ class _Pickler(BasePickler): return # Check the type dispatch table - t = type(obj) f = self.dispatch.get(t) if f: f(self, obj) # Call unbound method with explicit self @@ -157,26 +172,10 @@ class Unpickler(BaseUnpickler): BaseUnpickler.dispatch[pickle.REDUCE] = load_reduce -if sys.version_info >= (3, 8): - # In Python 3.8 and up, we can use the C implementation of Pickler, which - # supports a reducer_override method. - class Pickler(pickle.Pickler): - def __init__(self, *args, **kw): - self.bamWriter = BamWriter() - pickle.Pickler.__init__(self, *args, **kw) +Pickler = _Pickler - def reducer_override(self, obj): - reduce = getattr(obj, "__reduce_persist__", None) - if reduce: - return reduce(self) - - return NotImplemented -else: - # Otherwise, we have to use our custom version that overrides save(). - Pickler = _Pickler - - if sys.version_info < (3, 0): - del _Pickler +if sys.version_info < (3, 0): + del _Pickler # Shorthands diff --git a/tests/stdpy/test_pickle.py b/tests/stdpy/test_pickle.py index 3e8a7dcd25..37c7df4b25 100644 --- a/tests/stdpy/test_pickle.py +++ b/tests/stdpy/test_pickle.py @@ -12,6 +12,20 @@ def test_reduce_persist(): assert tuple(parent2.children) == (child2,) +def test_pickle_copy(): + from panda3d.core import PandaNode, NodePath + + # Make two Python wrappers pointing to the same node + node1 = PandaNode("node") + node2 = NodePath(node1).node() + assert node1.this == node2.this + assert id(node1) != id(node2) + + # Test that pickling and loading still results in the same node object. + node1, node2 = loads(dumps([node1, node2])) + assert node1 == node2 + + def test_pickle_error(): class ErroneousPickleable(object): def __reduce__(self): From 8852c835fc6b362ce6e331c1e4d19fa9949930c6 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 31 Dec 2020 16:57:34 +0100 Subject: [PATCH 04/19] collide: Support pickling for CollisionTraverser, HandlerEvent+Queue Fixes #1090 --- makepanda/makepanda.py | 2 + panda/src/collide/collisionHandlerEvent.h | 4 + .../src/collide/collisionHandlerEvent_ext.cxx | 123 ++++++++++++++++++ panda/src/collide/collisionHandlerEvent_ext.h | 38 ++++++ panda/src/collide/collisionHandlerQueue.h | 3 + .../src/collide/collisionHandlerQueue_ext.cxx | 27 ++++ panda/src/collide/collisionHandlerQueue_ext.h | 37 ++++++ panda/src/collide/collisionTraverser.h | 4 + panda/src/collide/collisionTraverser_ext.cxx | 82 ++++++++++++ panda/src/collide/collisionTraverser_ext.h | 38 ++++++ panda/src/collide/p3collide_ext_composite.cxx | 3 + tests/collide/test_collision_handlers.py | 18 +++ tests/collide/test_collision_traverser.py | 31 +++++ 13 files changed, 410 insertions(+) create mode 100644 panda/src/collide/collisionHandlerEvent_ext.cxx create mode 100644 panda/src/collide/collisionHandlerEvent_ext.h create mode 100644 panda/src/collide/collisionHandlerQueue_ext.cxx create mode 100644 panda/src/collide/collisionHandlerQueue_ext.h create mode 100644 panda/src/collide/collisionTraverser_ext.cxx create mode 100644 panda/src/collide/collisionTraverser_ext.h create mode 100644 panda/src/collide/p3collide_ext_composite.cxx create mode 100644 tests/collide/test_collision_handlers.py create mode 100644 tests/collide/test_collision_traverser.py diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index fa7df48550..e493900a5e 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -4363,6 +4363,7 @@ if (not RUNTIME): IGATEFILES=GetDirectoryContents('panda/src/collide', ["*.h", "*_composite*.cxx"]) TargetAdd('libp3collide.in', opts=OPTS, input=IGATEFILES) TargetAdd('libp3collide.in', opts=['IMOD:panda3d.core', 'ILIB:libp3collide', 'SRCDIR:panda/src/collide']) + PyTargetAdd('p3collide_ext_composite.obj', opts=OPTS, input='p3collide_ext_composite.cxx') # # DIRECTORY: panda/src/parametrics/ @@ -4606,6 +4607,7 @@ if (not RUNTIME): if PkgSkip("FREETYPE")==0: PyTargetAdd('core.pyd', input="libp3pnmtext_igate.obj") + PyTargetAdd('core.pyd', input='p3collide_ext_composite.obj') PyTargetAdd('core.pyd', input='p3pipeline_pythonThread.obj') PyTargetAdd('core.pyd', input='p3putil_ext_composite.obj') PyTargetAdd('core.pyd', input='p3pnmimage_pfmFile_ext.obj') diff --git a/panda/src/collide/collisionHandlerEvent.h b/panda/src/collide/collisionHandlerEvent.h index 05f15f025a..3bdad89812 100644 --- a/panda/src/collide/collisionHandlerEvent.h +++ b/panda/src/collide/collisionHandlerEvent.h @@ -22,6 +22,7 @@ #include "vector_string.h" #include "pointerTo.h" +#include "extension.h" /** * A specialized kind of CollisionHandler that throws an event for each @@ -67,6 +68,9 @@ PUBLISHED: void clear(); void flush(); + EXTENSION(PyObject *__getstate__() const); + EXTENSION(void __setstate__(PyObject *state)); + protected: void throw_event_for(const vector_string &patterns, CollisionEntry *entry); void throw_event_pattern(const std::string &pattern, CollisionEntry *entry); diff --git a/panda/src/collide/collisionHandlerEvent_ext.cxx b/panda/src/collide/collisionHandlerEvent_ext.cxx new file mode 100644 index 0000000000..1315f0fc03 --- /dev/null +++ b/panda/src/collide/collisionHandlerEvent_ext.cxx @@ -0,0 +1,123 @@ +/** + * 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 collisionHandlerEvent_ext.cxx + * @author rdb + * @date 2020-12-31 + */ + +#include "collisionHandlerEvent_ext.h" + +#ifdef HAVE_PYTHON + +/** + * Implements pickling behavior. + */ +PyObject *Extension:: +__getstate__() const { + PyObject *state = PyTuple_New(3); + if (state == nullptr) { + return nullptr; + } + + size_t num_patterns; + PyObject *patterns; + + num_patterns = _this->get_num_in_patterns(); + patterns = PyTuple_New(num_patterns); + for (size_t i = 0; i < num_patterns; ++i) { + std::string pattern = _this->get_in_pattern(i); +#if PY_MAJOR_VERSION >= 3 + PyTuple_SET_ITEM(patterns, i, PyUnicode_FromStringAndSize(pattern.data(), pattern.size())); +#else + PyTuple_SET_ITEM(patterns, i, PyString_FromStringAndSize(pattern.data(), pattern.size())); +#endif + } + PyTuple_SET_ITEM(state, 0, patterns); + + num_patterns = _this->get_num_again_patterns(); + patterns = PyTuple_New(num_patterns); + for (size_t i = 0; i < num_patterns; ++i) { + std::string pattern = _this->get_again_pattern(i); +#if PY_MAJOR_VERSION >= 3 + PyTuple_SET_ITEM(patterns, i, PyUnicode_FromStringAndSize(pattern.data(), pattern.size())); +#else + PyTuple_SET_ITEM(patterns, i, PyString_FromStringAndSize(pattern.data(), pattern.size())); +#endif + } + PyTuple_SET_ITEM(state, 1, patterns); + + num_patterns = _this->get_num_out_patterns(); + patterns = PyTuple_New(num_patterns); + for (size_t i = 0; i < num_patterns; ++i) { + std::string pattern = _this->get_out_pattern(i); +#if PY_MAJOR_VERSION >= 3 + PyTuple_SET_ITEM(patterns, i, PyUnicode_FromStringAndSize(pattern.data(), pattern.size())); +#else + PyTuple_SET_ITEM(patterns, i, PyString_FromStringAndSize(pattern.data(), pattern.size())); +#endif + } + PyTuple_SET_ITEM(state, 2, patterns); + + return state; +} + +/** + * Takes the value returned by __getstate__ and uses it to freshly initialize + * this CollisionHandlerEvent object. + */ +void Extension:: +__setstate__(PyObject *state) { + nassertv(Py_SIZE(state) >= 3); + + PyObject *patterns; + + _this->clear_in_patterns(); + patterns = PyTuple_GET_ITEM(state, 0); + for (size_t i = 0; i < Py_SIZE(patterns); ++i) { + PyObject *pattern = PyTuple_GET_ITEM(patterns, i); + Py_ssize_t len = 0; +#if PY_MAJOR_VERSION >= 3 + const char *data = PyUnicode_AsUTF8AndSize(pattern, &len); +#else + char *data; + PyString_AsStringAndSize(pattern, &data, &len); +#endif + _this->add_in_pattern(std::string(data, len)); + } + + _this->clear_again_patterns(); + patterns = PyTuple_GET_ITEM(state, 1); + for (size_t i = 0; i < Py_SIZE(patterns); ++i) { + PyObject *pattern = PyTuple_GET_ITEM(patterns, i); + Py_ssize_t len = 0; +#if PY_MAJOR_VERSION >= 3 + const char *data = PyUnicode_AsUTF8AndSize(pattern, &len); +#else + char *data; + PyString_AsStringAndSize(pattern, &data, &len); +#endif + _this->add_again_pattern(std::string(data, len)); + } + + _this->clear_out_patterns(); + patterns = PyTuple_GET_ITEM(state, 2); + for (size_t i = 0; i < Py_SIZE(patterns); ++i) { + PyObject *pattern = PyTuple_GET_ITEM(patterns, i); + Py_ssize_t len = 0; +#if PY_MAJOR_VERSION >= 3 + const char *data = PyUnicode_AsUTF8AndSize(pattern, &len); +#else + char *data; + PyString_AsStringAndSize(pattern, &data, &len); +#endif + _this->add_out_pattern(std::string(data, len)); + } +} + +#endif diff --git a/panda/src/collide/collisionHandlerEvent_ext.h b/panda/src/collide/collisionHandlerEvent_ext.h new file mode 100644 index 0000000000..cd9a0b8024 --- /dev/null +++ b/panda/src/collide/collisionHandlerEvent_ext.h @@ -0,0 +1,38 @@ +/** + * 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 collisionHandlerEvent_ext.h + * @author rdb + * @date 2020-12-31 + */ + +#ifndef COLLISIONHANDLEREVENT_EXT_H +#define COLLISIONHANDLEREVENT_EXT_H + +#include "dtoolbase.h" + +#ifdef HAVE_PYTHON + +#include "extension.h" +#include "collisionHandlerEvent.h" +#include "py_panda.h" + +/** + * This class defines the extension methods for CollisionHandlerEvent, 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); +}; + +#endif // HAVE_PYTHON + +#endif // COLLISIONHANDLEREVENT_EXT_H diff --git a/panda/src/collide/collisionHandlerQueue.h b/panda/src/collide/collisionHandlerQueue.h index cfef93c540..48e48600b7 100644 --- a/panda/src/collide/collisionHandlerQueue.h +++ b/panda/src/collide/collisionHandlerQueue.h @@ -18,6 +18,7 @@ #include "collisionHandler.h" #include "collisionEntry.h" +#include "extension.h" /** * A special kind of CollisionHandler that does nothing except remember the @@ -45,6 +46,8 @@ PUBLISHED: void output(std::ostream &out) const; void write(std::ostream &out, int indent_level = 0) const; + EXTENSION(PyObject *__reduce__(PyObject *self) const); + private: typedef pvector< PT(CollisionEntry) > Entries; Entries _entries; diff --git a/panda/src/collide/collisionHandlerQueue_ext.cxx b/panda/src/collide/collisionHandlerQueue_ext.cxx new file mode 100644 index 0000000000..b439d78214 --- /dev/null +++ b/panda/src/collide/collisionHandlerQueue_ext.cxx @@ -0,0 +1,27 @@ +/** + * 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 collisionHandlerQueue_ext.cxx + * @author rdb + * @date 2020-12-31 + */ + +#include "collisionHandlerQueue_ext.h" + +#ifdef HAVE_PYTHON + +/** + * Implements pickling behavior. + */ +PyObject *Extension:: +__reduce__(PyObject *self) const { + // CollisionHandlerQueue has no interesting properties. + return Py_BuildValue("(O())", Py_TYPE(self)); +} + +#endif diff --git a/panda/src/collide/collisionHandlerQueue_ext.h b/panda/src/collide/collisionHandlerQueue_ext.h new file mode 100644 index 0000000000..23b2768e61 --- /dev/null +++ b/panda/src/collide/collisionHandlerQueue_ext.h @@ -0,0 +1,37 @@ +/** + * 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 collisionHandler_ext.h + * @author rdb + * @date 2020-12-31 + */ + +#ifndef COLLISIONHANDLERQUEUE_EXT_H +#define COLLISIONHANDLERQUEUE_EXT_H + +#include "dtoolbase.h" + +#ifdef HAVE_PYTHON + +#include "extension.h" +#include "collisionHandlerQueue.h" +#include "py_panda.h" + +/** + * This class defines the extension methods for CollisionHandlerQueue, which are + * called instead of any C++ methods with the same prototype. + */ +template<> +class Extension : public ExtensionBase { +public: + PyObject *__reduce__(PyObject *self) const; +}; + +#endif // HAVE_PYTHON + +#endif // COLLISIONHANDLERQUEUE_EXT_H diff --git a/panda/src/collide/collisionTraverser.h b/panda/src/collide/collisionTraverser.h index fc039e3f7d..77ef1f20f1 100644 --- a/panda/src/collide/collisionTraverser.h +++ b/panda/src/collide/collisionTraverser.h @@ -24,6 +24,7 @@ #include "pset.h" #include "register_type.h" +#include "extension.h" class CollisionNode; class CollisionRecorder; @@ -81,6 +82,9 @@ PUBLISHED: void output(std::ostream &out) const; void write(std::ostream &out, int indent_level) const; + EXTENSION(PyObject *__getstate__() const); + EXTENSION(void __setstate__(PyObject *state)); + private: typedef pvector LevelStatesSingle; void prepare_colliders_single(LevelStatesSingle &level_states, const NodePath &root); diff --git a/panda/src/collide/collisionTraverser_ext.cxx b/panda/src/collide/collisionTraverser_ext.cxx new file mode 100644 index 0000000000..5dd6c171f6 --- /dev/null +++ b/panda/src/collide/collisionTraverser_ext.cxx @@ -0,0 +1,82 @@ +/** + * 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 collisionTraverser_ext.cxx + * @author rdb + * @date 2020-12-31 + */ + +#include "collisionTraverser_ext.h" + +#ifdef HAVE_PYTHON + +/** + * Implements pickling behavior. + */ +PyObject *Extension:: +__getstate__() const { + extern struct Dtool_PyTypedObject Dtool_CollisionHandler; + extern struct Dtool_PyTypedObject Dtool_CollisionTraverser; + extern struct Dtool_PyTypedObject Dtool_NodePath; + + const std::string &name = _this->get_name(); + size_t num_colliders = _this->get_num_colliders(); + + PyObject *state = PyTuple_New(num_colliders * 2 + 3); +#if PY_MAJOR_VERSION >= 3 + PyTuple_SET_ITEM(state, 0, PyUnicode_FromStringAndSize(name.data(), name.size())); +#else + PyTuple_SET_ITEM(state, 0, PyString_FromStringAndSize(name.data(), name.size())); +#endif + PyTuple_SET_ITEM(state, 1, PyBool_FromLong(_this->get_respect_prev_transform())); + PyTuple_SET_ITEM(state, 2, PyLong_FromLong((long)num_colliders)); + + for (size_t i = 0; i < num_colliders; ++i) { + NodePath *collider = new NodePath(_this->get_collider(i)); + PyTuple_SET_ITEM(state, i * 2 + 3, + DTool_CreatePyInstance((void *)collider, Dtool_NodePath, true, false)); + + PT(CollisionHandler) handler = _this->get_handler(*collider); + handler->ref(); + PyTuple_SET_ITEM(state, i * 2 + 4, + DTool_CreatePyInstanceTyped((void *)handler.p(), Dtool_CollisionHandler, true, false, handler->get_type_index())); + handler.cheat() = nullptr; + } + + return state; +} + +/** + * Takes the value returned by __getstate__ and uses it to freshly initialize + * this CollisionTraverser object. + */ +void Extension:: +__setstate__(PyObject *state) { + _this->clear_colliders(); + + Py_ssize_t len = 0; +#if PY_MAJOR_VERSION >= 3 + const char *data = PyUnicode_AsUTF8AndSize(PyTuple_GET_ITEM(state, 0), &len); +#else + char *data; + PyString_AsStringAndSize(PyTuple_GET_ITEM(state, 0), &data, &len); +#endif + _this->set_name(std::string(data, len)); + + _this->set_respect_prev_transform(PyTuple_GET_ITEM(state, 1) != Py_False); + size_t num_colliders = (ssize_t)PyLong_AsLong(PyTuple_GET_ITEM(state, 2)); + + for (size_t i = 0; i < num_colliders; ++i) { + NodePath *collider = (NodePath *)DtoolInstance_VOID_PTR(PyTuple_GET_ITEM(state, i * 2 + 3)); + CollisionHandler *handler = (CollisionHandler *)DtoolInstance_VOID_PTR(PyTuple_GET_ITEM(state, i * 2 + 4)); + + _this->add_collider(*collider, handler); + } +} + +#endif diff --git a/panda/src/collide/collisionTraverser_ext.h b/panda/src/collide/collisionTraverser_ext.h new file mode 100644 index 0000000000..445e4bcf5d --- /dev/null +++ b/panda/src/collide/collisionTraverser_ext.h @@ -0,0 +1,38 @@ +/** + * 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 collisionTraverser_ext.h + * @author rdb + * @date 2020-12-31 + */ + +#ifndef COLLISIONTRAVERSER_EXT_H +#define COLLISIONTRAVERSER_EXT_H + +#include "dtoolbase.h" + +#ifdef HAVE_PYTHON + +#include "extension.h" +#include "collisionTraverser.h" +#include "py_panda.h" + +/** + * This class defines the extension methods for CollisionTraverser, 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); +}; + +#endif // HAVE_PYTHON + +#endif // COLLISIONTRAVERSER_EXT_H diff --git a/panda/src/collide/p3collide_ext_composite.cxx b/panda/src/collide/p3collide_ext_composite.cxx new file mode 100644 index 0000000000..2c3175044b --- /dev/null +++ b/panda/src/collide/p3collide_ext_composite.cxx @@ -0,0 +1,3 @@ +#include "collisionHandlerEvent_ext.cxx" +#include "collisionHandlerQueue_ext.cxx" +#include "collisionTraverser_ext.cxx" diff --git a/tests/collide/test_collision_handlers.py b/tests/collide/test_collision_handlers.py new file mode 100644 index 0000000000..56f59cf17a --- /dev/null +++ b/tests/collide/test_collision_handlers.py @@ -0,0 +1,18 @@ +from direct.stdpy.pickle import dumps, loads + + +def test_collision_handler_event_pickle(): + from panda3d.core import CollisionHandlerEvent + + handler = CollisionHandlerEvent() + handler.add_in_pattern("abcdefg") + handler.add_in_pattern("test") + handler.add_out_pattern("out pattern") + handler.add_again_pattern("again pattern") + handler.add_again_pattern("another again pattern") + + handler = loads(dumps(handler, -1)) + + assert tuple(handler.in_patterns) == ("abcdefg", "test") + assert tuple(handler.out_patterns) == ("out pattern",) + assert tuple(handler.again_patterns) == ("again pattern", "another again pattern") diff --git a/tests/collide/test_collision_traverser.py b/tests/collide/test_collision_traverser.py new file mode 100644 index 0000000000..fd1f93ee56 --- /dev/null +++ b/tests/collide/test_collision_traverser.py @@ -0,0 +1,31 @@ +from panda3d.core import CollisionTraverser, CollisionHandlerQueue +from panda3d.core import NodePath, CollisionNode + + + +def test_collision_traverser_pickle(): + from direct.stdpy.pickle import dumps, loads + + handler = CollisionHandlerQueue() + + collider1 = NodePath(CollisionNode("collider1")) + collider2 = NodePath(CollisionNode("collider2")) + + trav = CollisionTraverser("test123") + trav.respect_prev_transform = True + trav.add_collider(collider1, handler) + trav.add_collider(collider2, handler) + + trav = loads(dumps(trav, -1)) + assert trav.respect_prev_transform is True + + assert trav.name == "test123" + assert trav.get_num_colliders() == 2 + collider1 = trav.get_collider(0) + collider2 = trav.get_collider(1) + assert collider1.name == "collider1" + assert collider2.name == "collider2" + + # Two colliders must still be the same object; this only works with our own + # version of the pickle module, in direct.stdpy.pickle. + assert trav.get_handler(collider1) == trav.get_handler(collider2) From 1793c9a93845278ad98f39d2811ca142b1e69386 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 31 Dec 2020 18:11:50 +0100 Subject: [PATCH 05/19] interrogate: Fix __setstate__ not working for subclasses --- dtool/src/interrogate/interfaceMakerPythonNative.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dtool/src/interrogate/interfaceMakerPythonNative.cxx b/dtool/src/interrogate/interfaceMakerPythonNative.cxx index eea5ee7329..459ed7d5e5 100644 --- a/dtool/src/interrogate/interfaceMakerPythonNative.cxx +++ b/dtool/src/interrogate/interfaceMakerPythonNative.cxx @@ -3624,8 +3624,8 @@ write_function_for_name(ostream &out, Object *obj, 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" + << " DTool_PyInit_Finalize(self, local_this, " + << "((Dtool_PyInstDef *)self)->_My_Type, false, false);\n" << " if (local_this == nullptr) {\n" << " PyErr_NoMemory();\n"; } From 3e1d4aa6b5921c918ae1e8bcffbdc59e60fde2af Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 31 Dec 2020 18:12:52 +0100 Subject: [PATCH 06/19] interrogate: Fix memory leak in __setstate__ --- dtool/src/interrogate/interfaceMakerPythonNative.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dtool/src/interrogate/interfaceMakerPythonNative.cxx b/dtool/src/interrogate/interfaceMakerPythonNative.cxx index 459ed7d5e5..91114c747c 100644 --- a/dtool/src/interrogate/interfaceMakerPythonNative.cxx +++ b/dtool/src/interrogate/interfaceMakerPythonNative.cxx @@ -3625,7 +3625,7 @@ write_function_for_name(ostream &out, Object *obj, out << " }\n" << " " << cClassName << " *local_this = new " << cClassName << ";\n" << " DTool_PyInit_Finalize(self, local_this, " - << "((Dtool_PyInstDef *)self)->_My_Type, false, false);\n" + << "((Dtool_PyInstDef *)self)->_My_Type, true, false);\n" << " if (local_this == nullptr) {\n" << " PyErr_NoMemory();\n"; } From 339331c1ad349d4038fb02d2326522fe5f2df46e Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 1 Jan 2021 15:51:23 +0100 Subject: [PATCH 07/19] egg: Add properties to EggAnimPreload --- panda/src/egg/eggAnimPreload.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/panda/src/egg/eggAnimPreload.h b/panda/src/egg/eggAnimPreload.h index e2b52a5c64..1eef9f8084 100644 --- a/panda/src/egg/eggAnimPreload.h +++ b/panda/src/egg/eggAnimPreload.h @@ -37,6 +37,9 @@ PUBLISHED: INLINE bool has_num_frames() const; INLINE int get_num_frames() const; + MAKE_PROPERTY2(fps, has_fps, get_fps, set_fps, clear_fps); + MAKE_PROPERTY2(num_frames, has_num_frames, get_num_frames, set_num_frames, clear_num_frames); + virtual void write(std::ostream &out, int indent_level) const; private: From 152d317c8cb9a3250beced870fee883ddfe23829 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 1 Jan 2021 15:52:42 +0100 Subject: [PATCH 08/19] egg: Fix egg lexer state not being cleaned up after error --- panda/src/egg/lexer.cxx.prebuilt | 10 ++++++++++ panda/src/egg/lexer.lxx | 10 ++++++++++ panda/src/egg/lexerDefs.h | 1 + panda/src/egg/parser.cxx.prebuilt | 2 ++ panda/src/egg/parser.yxx | 2 ++ 5 files changed, 25 insertions(+) diff --git a/panda/src/egg/lexer.cxx.prebuilt b/panda/src/egg/lexer.cxx.prebuilt index 9e412bce1f..f0b0a2b4ec 100644 --- a/panda/src/egg/lexer.cxx.prebuilt +++ b/panda/src/egg/lexer.cxx.prebuilt @@ -971,6 +971,7 @@ extern "C" int eggyywrap(void); // declared below. static int yyinput(void); // declared by flex. +int eggyylex_destroy(void); //////////////////////////////////////////////////////////////////// // Static variables @@ -1018,6 +1019,15 @@ egg_init_lexer(istream &in, const string &filename) { initial_token = START_EGG; } +void +egg_cleanup_lexer() { + // Reset the lexer state. + eggyylex_destroy(); + + input_p = nullptr; + egg_filename.clear(); +} + void egg_start_group_body() { /* Set the initial state to begin within a group_body context, diff --git a/panda/src/egg/lexer.lxx b/panda/src/egg/lexer.lxx index e88e5a388f..88aed62e46 100644 --- a/panda/src/egg/lexer.lxx +++ b/panda/src/egg/lexer.lxx @@ -26,6 +26,7 @@ extern "C" int eggyywrap(void); // declared below. static int yyinput(void); // declared by flex. +int eggyylex_destroy(void); //////////////////////////////////////////////////////////////////// // Static variables @@ -73,6 +74,15 @@ egg_init_lexer(istream &in, const string &filename) { initial_token = START_EGG; } +void +egg_cleanup_lexer() { + // Reset the lexer state. + yylex_destroy(); + + input_p = nullptr; + egg_filename.clear(); +} + void egg_start_group_body() { /* Set the initial state to begin within a group_body context, diff --git a/panda/src/egg/lexerDefs.h b/panda/src/egg/lexerDefs.h index 4e39cb0b10..6b1f321cb5 100644 --- a/panda/src/egg/lexerDefs.h +++ b/panda/src/egg/lexerDefs.h @@ -21,6 +21,7 @@ #include void egg_init_lexer(std::istream &in, const std::string &filename); +void egg_cleanup_lexer(); void egg_start_group_body(); void egg_start_texture_body(); void egg_start_primitive_body(); diff --git a/panda/src/egg/parser.cxx.prebuilt b/panda/src/egg/parser.cxx.prebuilt index 576f851740..d9b32aced5 100644 --- a/panda/src/egg/parser.cxx.prebuilt +++ b/panda/src/egg/parser.cxx.prebuilt @@ -223,6 +223,8 @@ egg_cleanup_parser() { textures.clear(); materials.clear(); groups.clear(); + + egg_cleanup_lexer(); } diff --git a/panda/src/egg/parser.yxx b/panda/src/egg/parser.yxx index a41b08ba57..6f4ed4d443 100644 --- a/panda/src/egg/parser.yxx +++ b/panda/src/egg/parser.yxx @@ -153,6 +153,8 @@ egg_cleanup_parser() { textures.clear(); materials.clear(); groups.clear(); + + egg_cleanup_lexer(); } %} From 99f9352e7625e1118f683bfa96701593185bb2b8 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 1 Jan 2021 15:57:41 +0100 Subject: [PATCH 09/19] interrogate: improvements to __setstate__ handling: * Force single arg variant, easing argument parsing * Allow defining __setstate__ taking multiple args, leading to tuple unpack * Allow __setstate__ to be called on already initialized object (useful with __reduce__) --- dtool/src/interrogate/functionRemap.cxx | 3 ++ .../interfaceMakerPythonNative.cxx | 33 +++++++++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/dtool/src/interrogate/functionRemap.cxx b/dtool/src/interrogate/functionRemap.cxx index fd44f9f5fa..e0dec969c0 100644 --- a/dtool/src/interrogate/functionRemap.cxx +++ b/dtool/src/interrogate/functionRemap.cxx @@ -920,6 +920,9 @@ setup_properties(const InterrogateFunction &ifunc, InterfaceMaker *interface_mak || fname == "__delattr__") { // Just to prevent these from getting keyword arguments. + } else if (fname == "__setstate__") { + _args_type = InterfaceMaker::AT_single_arg; + } else { if (_args_type == InterfaceMaker::AT_varargs) { // Every other method can take keyword arguments, if they take more diff --git a/dtool/src/interrogate/interfaceMakerPythonNative.cxx b/dtool/src/interrogate/interfaceMakerPythonNative.cxx index 91114c747c..18cbc3a22a 100644 --- a/dtool/src/interrogate/interfaceMakerPythonNative.cxx +++ b/dtool/src/interrogate/interfaceMakerPythonNative.cxx @@ -3616,18 +3616,23 @@ write_function_for_name(ostream &out, Object *obj, std::string ClassName = make_safe_name(obj->_itype.get_scoped_name()); std::string cClassName = obj->_itype.get_true_name(); // string class_name = remap->_cpptype->get_simple_name(); + CPPStructType *struct_type = obj->_itype._cpptype->as_struct_type(); // 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_PyInstDef *)self)->_My_Type, true, false);\n" - << " if (local_this == nullptr) {\n" - << " PyErr_NoMemory();\n"; + if (remap->_cppfunc->get_local_name() == "__setstate__" && + !struct_type->is_abstract()) { + out << " " << cClassName << " *local_this = nullptr;\n" + << " if (DtoolInstance_VOID_PTR(self) == nullptr) {\n" + << " local_this = new " << cClassName << ";\n" + << " DTool_PyInit_Finalize(self, local_this, &Dtool_" << ClassName + << ", true, false);\n" + << " if (local_this == nullptr) {\n" + << " PyErr_NoMemory();\n"; + error_return(out, 6, return_flags); + out << " }\n" + << " } else if (!Dtool_Call_ExtractThisPointer_NonConst(self, Dtool_" << ClassName << ", " + << "(void **)&local_this, \"" << classNameFromCppName(cClassName, false) + << "." << methodNameFromCppName(remap, cClassName, false) << "\")) {\n"; } else if (all_nonconst) { // All remaps are non-const. Also check that this object isn't const. @@ -3664,6 +3669,14 @@ write_function_for_name(ostream &out, Object *obj, args_type = AT_varargs; } + // If this is a __setstate__ taking multiple arguments, and we're given a + // tuple as argument, unpack it. + if (args_type == AT_single_arg && max_required_args > 1 && + remap->_cppfunc->get_local_name() == "__setstate__") { + out << " PyObject *args = arg;\n"; + args_type = AT_varargs; + } + if (args_type == AT_keyword_args || args_type == AT_varargs) { max_required_args = collapse_default_remaps(map_sets, max_required_args); } From 9cb129597ce9efe3363f33a3c3ea8f74ee01dd10 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 1 Jan 2021 15:58:51 +0100 Subject: [PATCH 10/19] stdpy: pickle improvements * Define __all__ * Define missing exception types * clear_memo() now clears Panda-specific state as well --- direct/src/stdpy/pickle.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/direct/src/stdpy/pickle.py b/direct/src/stdpy/pickle.py index db4a066572..aa50209e13 100644 --- a/direct/src/stdpy/pickle.py +++ b/direct/src/stdpy/pickle.py @@ -21,6 +21,9 @@ shared context between all objects written by that Pickler. Unfortunately, cPickle cannot be supported, because it does not support extensions of this nature. """ +__all__ = ["PickleError", "PicklingError", "UnpicklingError", "Pickler", + "Unpickler", "dump", "dumps", "load", "loads"] + import sys from panda3d.core import BamWriter, BamReader, TypedObject @@ -33,7 +36,9 @@ else: # with the local pickle.py. pickle = __import__('pickle') +PickleError = pickle.PickleError PicklingError = pickle.PicklingError +UnpicklingError = pickle.UnpicklingError if sys.version_info >= (3, 0): BasePickler = pickle._Pickler @@ -43,13 +48,18 @@ else: BaseUnpickler = pickle.Unpickler -class _Pickler(BasePickler): +class Pickler(BasePickler): def __init__(self, *args, **kw): self.bamWriter = BamWriter() self._canonical = {} BasePickler.__init__(self, *args, **kw) + def clear_memo(self): + BasePickler.clear_memo(self) + self._canonical.clear() + self.bamWriter = BamWriter() + # We have to duplicate most of the save() method, so we can add # support for __reduce_persist__(). @@ -172,12 +182,6 @@ class Unpickler(BaseUnpickler): BaseUnpickler.dispatch[pickle.REDUCE] = load_reduce -Pickler = _Pickler - -if sys.version_info < (3, 0): - del _Pickler - - # Shorthands from io import BytesIO From a7042091befc850566a7ca22c6e4ff3d4ed14740 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 1 Jan 2021 16:37:21 +0100 Subject: [PATCH 11/19] py_panda: backport some py_compat.h definitions --- dtool/src/interrogatedb/py_compat.h | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/dtool/src/interrogatedb/py_compat.h b/dtool/src/interrogatedb/py_compat.h index 2152a115cc..940bf1f223 100644 --- a/dtool/src/interrogatedb/py_compat.h +++ b/dtool/src/interrogatedb/py_compat.h @@ -208,6 +208,28 @@ INLINE PyObject *_PyLong_Lshift(PyObject *a, size_t shiftby) { } #endif +#if PY_VERSION_HEX < 0x03090000 +INLINE EXPCL_PYPANDA PyObject *PyObject_CallNoArgs(PyObject *func) { + return _PyObject_CallNoArg(func); +} + +INLINE PyObject *PyObject_CallOneArg(PyObject *callable, PyObject *arg) { +#if PY_VERSION_HEX >= 0x03060000 + return _PyObject_FastCall(callable, &arg, 1); +#else + return PyObject_CallFunctionObjArgs(callable, arg, nullptr); +#endif +} + +INLINE PyObject *PyObject_CallMethodNoArgs(PyObject *obj, PyObject *name) { + return PyObject_CallMethodObjArgs(obj, name, nullptr); +} + +INLINE PyObject *PyObject_CallMethodOneArg(PyObject *obj, PyObject *name, PyObject *arg) { + return PyObject_CallMethodObjArgs(obj, name, arg, nullptr); +} +#endif + /* Other Python implementations */ // _PyErr_OCCURRED is an undocumented macro version of PyErr_Occurred. From f8ce3399600f81eda3700d9e29bb2baf84d976d3 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 1 Jan 2021 16:54:27 +0100 Subject: [PATCH 12/19] collide: Add pickle support for most collision handlers Also redo CollisionHandlerEvent pickling to use Datagram instead Related to #1090 --- panda/src/collide/collisionHandlerEvent.cxx | 46 +++++++ panda/src/collide/collisionHandlerEvent.h | 8 +- .../src/collide/collisionHandlerEvent_ext.cxx | 119 ++++++----------- panda/src/collide/collisionHandlerEvent_ext.h | 4 +- panda/src/collide/collisionHandlerFloor.cxx | 29 +++-- panda/src/collide/collisionHandlerFloor.h | 3 + panda/src/collide/collisionHandlerGravity.cxx | 29 +++++ panda/src/collide/collisionHandlerGravity.h | 3 + panda/src/collide/collisionHandlerPhysical.h | 5 + .../collide/collisionHandlerPhysical_ext.cxx | 120 ++++++++++++++++++ .../collide/collisionHandlerPhysical_ext.h | 38 ++++++ panda/src/collide/collisionHandlerPusher.cxx | 21 +++ panda/src/collide/collisionHandlerPusher.h | 3 + panda/src/collide/p3collide_ext_composite.cxx | 1 + tests/collide/test_collision_handlers.py | 94 +++++++++++++- 15 files changed, 429 insertions(+), 94 deletions(-) create mode 100644 panda/src/collide/collisionHandlerPhysical_ext.cxx create mode 100644 panda/src/collide/collisionHandlerPhysical_ext.h diff --git a/panda/src/collide/collisionHandlerEvent.cxx b/panda/src/collide/collisionHandlerEvent.cxx index 0fd949074e..668868f078 100644 --- a/panda/src/collide/collisionHandlerEvent.cxx +++ b/panda/src/collide/collisionHandlerEvent.cxx @@ -153,6 +153,52 @@ flush() { end_group(); } +/** + * Serializes this object, to implement pickle support. + */ +void CollisionHandlerEvent:: +write_datagram(Datagram &dg) const { + dg.add_uint32(_in_patterns.size()); + for (const std::string &pattern : _in_patterns) { + dg.add_string(pattern); + } + + dg.add_uint32(_again_patterns.size()); + for (const std::string &pattern : _again_patterns) { + dg.add_string(pattern); + } + + dg.add_uint32(_out_patterns.size()); + for (const std::string &pattern : _out_patterns) { + dg.add_string(pattern); + } +} + +/** + * Restores the object state from the given datagram, previously obtained using + * __getstate__. + */ +void CollisionHandlerEvent:: +read_datagram(DatagramIterator &scan) { + _in_patterns.clear(); + size_t num_in_patterns = scan.get_uint32(); + for (size_t i = 0; i < num_in_patterns; ++i) { + add_in_pattern(scan.get_string()); + } + + _again_patterns.clear(); + size_t num_again_patterns = scan.get_uint32(); + for (size_t i = 0; i < num_again_patterns; ++i) { + add_again_pattern(scan.get_string()); + } + + _out_patterns.clear(); + size_t num_out_patterns = scan.get_uint32(); + for (size_t i = 0; i < num_out_patterns; ++i) { + add_out_pattern(scan.get_string()); + } +} + /** * Throws whatever events are suggested by the list of patterns. */ diff --git a/panda/src/collide/collisionHandlerEvent.h b/panda/src/collide/collisionHandlerEvent.h index 3bdad89812..59173a42fc 100644 --- a/panda/src/collide/collisionHandlerEvent.h +++ b/panda/src/collide/collisionHandlerEvent.h @@ -68,8 +68,12 @@ PUBLISHED: void clear(); void flush(); - EXTENSION(PyObject *__getstate__() const); - EXTENSION(void __setstate__(PyObject *state)); + // These help implement Python pickle support. + EXTENSION(PyObject *__reduce__(PyObject *self) const); + EXTENSION(void __setstate__(PyObject *self, vector_uchar data)); + + void write_datagram(Datagram &destination) const; + void read_datagram(DatagramIterator &source); protected: void throw_event_for(const vector_string &patterns, CollisionEntry *entry); diff --git a/panda/src/collide/collisionHandlerEvent_ext.cxx b/panda/src/collide/collisionHandlerEvent_ext.cxx index 1315f0fc03..242a20da9f 100644 --- a/panda/src/collide/collisionHandlerEvent_ext.cxx +++ b/panda/src/collide/collisionHandlerEvent_ext.cxx @@ -12,6 +12,9 @@ */ #include "collisionHandlerEvent_ext.h" +#include "collisionHandlerFloor.h" +#include "collisionHandlerGravity.h" +#include "collisionHandlerPusher.h" #ifdef HAVE_PYTHON @@ -19,52 +22,35 @@ * Implements pickling behavior. */ PyObject *Extension:: -__getstate__() const { - PyObject *state = PyTuple_New(3); - if (state == nullptr) { +__reduce__(PyObject *self) const { + extern struct Dtool_PyTypedObject Dtool_Datagram; + + // Call the write_datagram method via Python, since it's not a virtual method + // on the C++ end. +#if PY_MAJOR_VERSION >= 3 + PyObject *method_name = PyUnicode_FromString("write_datagram"); +#else + PyObject *method_name = PyString_FromString("write_datagram"); +#endif + + Datagram dg; + PyObject *destination = DTool_CreatePyInstance(&dg, Dtool_Datagram, false, false); + + PyObject *retval = PyObject_CallMethodOneArg(self, method_name, destination); + Py_DECREF(method_name); + Py_DECREF(destination); + if (retval == nullptr) { return nullptr; } + Py_DECREF(retval); - size_t num_patterns; - PyObject *patterns; - - num_patterns = _this->get_num_in_patterns(); - patterns = PyTuple_New(num_patterns); - for (size_t i = 0; i < num_patterns; ++i) { - std::string pattern = _this->get_in_pattern(i); + const char *data = (const char *)dg.get_data(); + Py_ssize_t size = dg.get_length(); #if PY_MAJOR_VERSION >= 3 - PyTuple_SET_ITEM(patterns, i, PyUnicode_FromStringAndSize(pattern.data(), pattern.size())); + return Py_BuildValue("O()y#", Py_TYPE(self), data, size); #else - PyTuple_SET_ITEM(patterns, i, PyString_FromStringAndSize(pattern.data(), pattern.size())); + return Py_BuildValue("O()s#", Py_TYPE(self), data, size); #endif - } - PyTuple_SET_ITEM(state, 0, patterns); - - num_patterns = _this->get_num_again_patterns(); - patterns = PyTuple_New(num_patterns); - for (size_t i = 0; i < num_patterns; ++i) { - std::string pattern = _this->get_again_pattern(i); -#if PY_MAJOR_VERSION >= 3 - PyTuple_SET_ITEM(patterns, i, PyUnicode_FromStringAndSize(pattern.data(), pattern.size())); -#else - PyTuple_SET_ITEM(patterns, i, PyString_FromStringAndSize(pattern.data(), pattern.size())); -#endif - } - PyTuple_SET_ITEM(state, 1, patterns); - - num_patterns = _this->get_num_out_patterns(); - patterns = PyTuple_New(num_patterns); - for (size_t i = 0; i < num_patterns; ++i) { - std::string pattern = _this->get_out_pattern(i); -#if PY_MAJOR_VERSION >= 3 - PyTuple_SET_ITEM(patterns, i, PyUnicode_FromStringAndSize(pattern.data(), pattern.size())); -#else - PyTuple_SET_ITEM(patterns, i, PyString_FromStringAndSize(pattern.data(), pattern.size())); -#endif - } - PyTuple_SET_ITEM(state, 2, patterns); - - return state; } /** @@ -72,52 +58,25 @@ __getstate__() const { * this CollisionHandlerEvent object. */ void Extension:: -__setstate__(PyObject *state) { - nassertv(Py_SIZE(state) >= 3); +__setstate__(PyObject *self, vector_uchar data) { + extern struct Dtool_PyTypedObject Dtool_DatagramIterator; - PyObject *patterns; - - _this->clear_in_patterns(); - patterns = PyTuple_GET_ITEM(state, 0); - for (size_t i = 0; i < Py_SIZE(patterns); ++i) { - PyObject *pattern = PyTuple_GET_ITEM(patterns, i); - Py_ssize_t len = 0; + // Call the read_datagram method via Python, since it's not a virtual method + // on the C++ end. #if PY_MAJOR_VERSION >= 3 - const char *data = PyUnicode_AsUTF8AndSize(pattern, &len); + PyObject *method_name = PyUnicode_FromString("read_datagram"); #else - char *data; - PyString_AsStringAndSize(pattern, &data, &len); + PyObject *method_name = PyString_FromString("read_datagram"); #endif - _this->add_in_pattern(std::string(data, len)); - } - _this->clear_again_patterns(); - patterns = PyTuple_GET_ITEM(state, 1); - for (size_t i = 0; i < Py_SIZE(patterns); ++i) { - PyObject *pattern = PyTuple_GET_ITEM(patterns, i); - Py_ssize_t len = 0; -#if PY_MAJOR_VERSION >= 3 - const char *data = PyUnicode_AsUTF8AndSize(pattern, &len); -#else - char *data; - PyString_AsStringAndSize(pattern, &data, &len); -#endif - _this->add_again_pattern(std::string(data, len)); - } + Datagram dg(std::move(data)); + DatagramIterator scan(dg); + PyObject *source = DTool_CreatePyInstance(&scan, Dtool_DatagramIterator, false, false); - _this->clear_out_patterns(); - patterns = PyTuple_GET_ITEM(state, 2); - for (size_t i = 0; i < Py_SIZE(patterns); ++i) { - PyObject *pattern = PyTuple_GET_ITEM(patterns, i); - Py_ssize_t len = 0; -#if PY_MAJOR_VERSION >= 3 - const char *data = PyUnicode_AsUTF8AndSize(pattern, &len); -#else - char *data; - PyString_AsStringAndSize(pattern, &data, &len); -#endif - _this->add_out_pattern(std::string(data, len)); - } + PyObject *retval = PyObject_CallMethodOneArg(self, method_name, source); + Py_DECREF(method_name); + Py_DECREF(source); + Py_XDECREF(retval); } #endif diff --git a/panda/src/collide/collisionHandlerEvent_ext.h b/panda/src/collide/collisionHandlerEvent_ext.h index cd9a0b8024..22a3a5e680 100644 --- a/panda/src/collide/collisionHandlerEvent_ext.h +++ b/panda/src/collide/collisionHandlerEvent_ext.h @@ -29,8 +29,8 @@ template<> class Extension : public ExtensionBase { public: - PyObject *__getstate__() const; - void __setstate__(PyObject *state); + PyObject *__reduce__(PyObject *self) const; + void __setstate__(PyObject *self, vector_uchar data); }; #endif // HAVE_PYTHON diff --git a/panda/src/collide/collisionHandlerFloor.cxx b/panda/src/collide/collisionHandlerFloor.cxx index 506101cb41..8c9838eea7 100644 --- a/panda/src/collide/collisionHandlerFloor.cxx +++ b/panda/src/collide/collisionHandlerFloor.cxx @@ -41,18 +41,31 @@ CollisionHandlerFloor:: } /** - * + * Serializes this object, to implement pickle support. + */ +void CollisionHandlerFloor:: +write_datagram(Datagram &dg) const { + CollisionHandlerPhysical::write_datagram(dg); - * + dg.add_float64(_offset); + dg.add_float64(_reach); + dg.add_float64(_max_velocity); +} - * +/** + * Restores the object state from the given datagram, previously obtained using + * __getstate__. + */ +void CollisionHandlerFloor:: +read_datagram(DatagramIterator &scan) { + CollisionHandlerPhysical::read_datagram(scan); - * - - * - - * + _offset = scan.get_float64(); + _reach = scan.get_float64(); + _max_velocity = scan.get_float64(); +} +/** * */ PN_stdfloat CollisionHandlerFloor:: diff --git a/panda/src/collide/collisionHandlerFloor.h b/panda/src/collide/collisionHandlerFloor.h index d73ca008df..c4307fcc00 100644 --- a/panda/src/collide/collisionHandlerFloor.h +++ b/panda/src/collide/collisionHandlerFloor.h @@ -43,6 +43,9 @@ PUBLISHED: MAKE_PROPERTY(reach, get_reach, set_reach); MAKE_PROPERTY(max_velocity, get_max_velocity, set_max_velocity); + void write_datagram(Datagram &destination) const; + void read_datagram(DatagramIterator &source); + protected: PN_stdfloat set_highest_collision(const NodePath &target_node_path, const NodePath &from_node_path, const Entries &entries); virtual bool handle_entries(); diff --git a/panda/src/collide/collisionHandlerGravity.cxx b/panda/src/collide/collisionHandlerGravity.cxx index 023d220cd4..0b3eb5852c 100644 --- a/panda/src/collide/collisionHandlerGravity.cxx +++ b/panda/src/collide/collisionHandlerGravity.cxx @@ -46,6 +46,35 @@ CollisionHandlerGravity:: ~CollisionHandlerGravity() { } +/** + * Serializes this object, to implement pickle support. + */ +void CollisionHandlerGravity:: +write_datagram(Datagram &dg) const { + CollisionHandlerPhysical::write_datagram(dg); + + dg.add_float64(_offset); + dg.add_float64(_reach); + dg.add_float64(_max_velocity); + dg.add_float64(_gravity); + dg.add_bool(_legacy_mode); +} + +/** + * Restores the object state from the given datagram, previously obtained using + * __getstate__. + */ +void CollisionHandlerGravity:: +read_datagram(DatagramIterator &scan) { + CollisionHandlerPhysical::read_datagram(scan); + + _offset = scan.get_float64(); + _reach = scan.get_float64(); + _max_velocity = scan.get_float64(); + _gravity = scan.get_float64(); + _legacy_mode = scan.get_bool(); +} + /** * */ diff --git a/panda/src/collide/collisionHandlerGravity.h b/panda/src/collide/collisionHandlerGravity.h index de89091564..136da11462 100644 --- a/panda/src/collide/collisionHandlerGravity.h +++ b/panda/src/collide/collisionHandlerGravity.h @@ -64,6 +64,9 @@ PUBLISHED: MAKE_PROPERTY(gravity, get_gravity, set_gravity); MAKE_PROPERTY(max_velocity, get_max_velocity, set_max_velocity); + void write_datagram(Datagram &destination) const; + void read_datagram(DatagramIterator &source); + protected: PN_stdfloat set_highest_collision(const NodePath &target_node_path, const NodePath &from_node_path, const Entries &entries); virtual bool handle_entries(); diff --git a/panda/src/collide/collisionHandlerPhysical.h b/panda/src/collide/collisionHandlerPhysical.h index 9a9225b6b8..0147b6acf8 100644 --- a/panda/src/collide/collisionHandlerPhysical.h +++ b/panda/src/collide/collisionHandlerPhysical.h @@ -54,6 +54,9 @@ PUBLISHED: PUBLISHED: MAKE_PROPERTY2(center, has_center, get_center, set_center, clear_center); + EXTENSION(PyObject *__reduce__(PyObject *self) const); + EXTENSION(void __setstate__(PyObject *self, vector_uchar data, PyObject *nodepaths)); + protected: bool _has_contact; // Are we in contact with anything? @@ -83,6 +86,8 @@ protected: NodePath _center; + friend class Extension; + public: static TypeHandle get_class_type() { return _type_handle; diff --git a/panda/src/collide/collisionHandlerPhysical_ext.cxx b/panda/src/collide/collisionHandlerPhysical_ext.cxx new file mode 100644 index 0000000000..ceedf7f98e --- /dev/null +++ b/panda/src/collide/collisionHandlerPhysical_ext.cxx @@ -0,0 +1,120 @@ +/** + * 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 collisionHandlerPhysical_ext.cxx + * @author rdb + * @date 2020-12-31 + */ + +#include "collisionHandlerPhysical_ext.h" +#include "collisionHandlerEvent_ext.h" + +#ifdef HAVE_PYTHON + +/** + * Implements pickling behavior. + */ +PyObject *Extension:: +__reduce__(PyObject *self) const { + extern struct Dtool_PyTypedObject Dtool_Datagram; + extern struct Dtool_PyTypedObject Dtool_NodePath; + + // Create a tuple with all the NodePath pointers. + PyObject *nodepaths = PyTuple_New(_this->_colliders.size() * 2 + 1); + Py_ssize_t i = 0; + + if (_this->has_center()) { + const NodePath *center = &(_this->get_center()); + PyTuple_SET_ITEM(nodepaths, i++, + DTool_CreatePyInstance((void *)center, Dtool_NodePath, false, true)); + } else { + PyTuple_SET_ITEM(nodepaths, i++, Py_None); + Py_INCREF(Py_None); + } + + CollisionHandlerPhysical::Colliders::const_iterator it; + for (it = _this->_colliders.begin(); it != _this->_colliders.end(); ++it) { + const NodePath *collider = &(it->first); + const NodePath *target = &(it->second._target); + PyTuple_SET_ITEM(nodepaths, i++, + DTool_CreatePyInstance((void *)collider, Dtool_NodePath, false, true)); + PyTuple_SET_ITEM(nodepaths, i++, + DTool_CreatePyInstance((void *)target, Dtool_NodePath, false, true)); + } + + // Call the write_datagram method via Python, since it's not a virtual method + // on the C++ end. +#if PY_MAJOR_VERSION >= 3 + PyObject *method_name = PyUnicode_FromString("write_datagram"); +#else + PyObject *method_name = PyString_FromString("write_datagram"); +#endif + + Datagram dg; + PyObject *destination = DTool_CreatePyInstance(&dg, Dtool_Datagram, false, false); + + PyObject *retval = PyObject_CallMethodOneArg(self, method_name, destination); + Py_DECREF(method_name); + Py_DECREF(destination); + if (retval == nullptr) { + return nullptr; + } + Py_DECREF(retval); + + const char *data = (const char *)dg.get_data(); + Py_ssize_t size = dg.get_length(); +#if PY_MAJOR_VERSION >= 3 + return Py_BuildValue("O()(y#N)", Py_TYPE(self), data, size, nodepaths); +#else + return Py_BuildValue("O()(s#N)", Py_TYPE(self), data, size, nodepaths); +#endif +} + +/** + * Takes the value returned by __getstate__ and uses it to freshly initialize + * this CollisionHandlerPhysical object. + */ +void Extension:: +__setstate__(PyObject *self, vector_uchar data, PyObject *nodepaths) { + extern struct Dtool_PyTypedObject Dtool_DatagramIterator; + + // Call the read_datagram method via Python, since it's not a virtual method + // on the C++ end. +#if PY_MAJOR_VERSION >= 3 + PyObject *method_name = PyUnicode_FromString("read_datagram"); +#else + PyObject *method_name = PyString_FromString("read_datagram"); +#endif + + { + Datagram dg(std::move(data)); + DatagramIterator scan(dg); + PyObject *source = DTool_CreatePyInstance(&scan, Dtool_DatagramIterator, false, false); + + PyObject *retval = PyObject_CallMethodOneArg(self, method_name, source); + Py_DECREF(method_name); + Py_DECREF(source); + Py_XDECREF(retval); + } + + PyObject *center = PyTuple_GET_ITEM(nodepaths, 0); + if (center != Py_None) { + _this->set_center(*(NodePath *)DtoolInstance_VOID_PTR(center)); + } else { + _this->clear_center(); + } + + size_t num_nodepaths = Py_SIZE(nodepaths); + for (size_t i = 1; i < num_nodepaths;) { + NodePath *collider = (NodePath *)DtoolInstance_VOID_PTR(PyTuple_GET_ITEM(nodepaths, i++)); + NodePath *target = (NodePath *)DtoolInstance_VOID_PTR(PyTuple_GET_ITEM(nodepaths, i++)); + _this->add_collider(*collider, *target); + } +} + +#endif diff --git a/panda/src/collide/collisionHandlerPhysical_ext.h b/panda/src/collide/collisionHandlerPhysical_ext.h new file mode 100644 index 0000000000..8e74becd8f --- /dev/null +++ b/panda/src/collide/collisionHandlerPhysical_ext.h @@ -0,0 +1,38 @@ +/** + * 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 collisionHandlerPhysical_ext.h + * @author rdb + * @date 2020-12-31 + */ + +#ifndef COLLISIONHANDLERPHYSICAL_EXT_H +#define COLLISIONHANDLERPHYSICAL_EXT_H + +#include "dtoolbase.h" + +#ifdef HAVE_PYTHON + +#include "extension.h" +#include "collisionHandlerPhysical.h" +#include "py_panda.h" + +/** + * This class defines the extension methods for CollisionHandlerPhysical, which are + * called instead of any C++ methods with the same prototype. + */ +template<> +class Extension : public ExtensionBase { +public: + PyObject *__reduce__(PyObject *self) const; + void __setstate__(PyObject *self, vector_uchar data, PyObject *nodepaths); +}; + +#endif // HAVE_PYTHON + +#endif // COLLISIONHANDLERPHYSICAL_EXT_H diff --git a/panda/src/collide/collisionHandlerPusher.cxx b/panda/src/collide/collisionHandlerPusher.cxx index fb7dc87c66..7c3f6674c3 100644 --- a/panda/src/collide/collisionHandlerPusher.cxx +++ b/panda/src/collide/collisionHandlerPusher.cxx @@ -49,6 +49,27 @@ CollisionHandlerPusher:: ~CollisionHandlerPusher() { } +/** + * Serializes this object, to implement pickle support. + */ +void CollisionHandlerPusher:: +write_datagram(Datagram &dg) const { + CollisionHandlerPhysical::write_datagram(dg); + + dg.add_bool(_horizontal); +} + +/** + * Restores the object state from the given datagram, previously obtained using + * __getstate__. + */ +void CollisionHandlerPusher:: +read_datagram(DatagramIterator &scan) { + CollisionHandlerPhysical::read_datagram(scan); + + _horizontal = scan.get_bool(); +} + /** * Called by the parent class after all collisions have been detected, this * manages the various collisions and moves around the nodes as necessary. diff --git a/panda/src/collide/collisionHandlerPusher.h b/panda/src/collide/collisionHandlerPusher.h index 8f427ca4b2..99ea68104a 100644 --- a/panda/src/collide/collisionHandlerPusher.h +++ b/panda/src/collide/collisionHandlerPusher.h @@ -34,6 +34,9 @@ PUBLISHED: PUBLISHED: MAKE_PROPERTY(horizontal, get_horizontal, set_horizontal); + void write_datagram(Datagram &destination) const; + void read_datagram(DatagramIterator &source); + protected: virtual bool handle_entries(); virtual void apply_net_shove( diff --git a/panda/src/collide/p3collide_ext_composite.cxx b/panda/src/collide/p3collide_ext_composite.cxx index 2c3175044b..271fb1a7eb 100644 --- a/panda/src/collide/p3collide_ext_composite.cxx +++ b/panda/src/collide/p3collide_ext_composite.cxx @@ -1,3 +1,4 @@ #include "collisionHandlerEvent_ext.cxx" +#include "collisionHandlerPhysical_ext.cxx" #include "collisionHandlerQueue_ext.cxx" #include "collisionTraverser_ext.cxx" diff --git a/tests/collide/test_collision_handlers.py b/tests/collide/test_collision_handlers.py index 56f59cf17a..87d602ef3b 100644 --- a/tests/collide/test_collision_handlers.py +++ b/tests/collide/test_collision_handlers.py @@ -1,4 +1,5 @@ from direct.stdpy.pickle import dumps, loads +from panda3d.core import NodePath, CollisionNode def test_collision_handler_event_pickle(): @@ -7,12 +8,101 @@ def test_collision_handler_event_pickle(): handler = CollisionHandlerEvent() handler.add_in_pattern("abcdefg") handler.add_in_pattern("test") - handler.add_out_pattern("out pattern") handler.add_again_pattern("again pattern") handler.add_again_pattern("another again pattern") + handler.add_out_pattern("out pattern") handler = loads(dumps(handler, -1)) assert tuple(handler.in_patterns) == ("abcdefg", "test") - assert tuple(handler.out_patterns) == ("out pattern",) assert tuple(handler.again_patterns) == ("again pattern", "another again pattern") + assert tuple(handler.out_patterns) == ("out pattern",) + + +def test_collision_handler_queue_pickle(): + from panda3d.core import CollisionHandlerQueue + + handler = CollisionHandlerQueue() + handler = loads(dumps(handler, -1)) + assert type(handler) == CollisionHandlerQueue + + +def test_collision_handler_floor_pickle(): + from panda3d.core import CollisionHandlerFloor + + collider1 = NodePath(CollisionNode("collider1")) + collider2 = NodePath(CollisionNode("collider2")) + target1 = NodePath("target1") + target2 = NodePath("target2") + center = NodePath("center") + + handler = CollisionHandlerFloor() + handler.add_out_pattern("out pattern") + handler.add_collider(collider1, target1) + handler.add_collider(collider2, target2) + handler.center = center + handler.offset = 1.0 + handler.reach = 2.0 + handler.max_velocity = 3.0 + + handler = loads(dumps(handler, -1)) + + assert tuple(handler.in_patterns) == () + assert tuple(handler.again_patterns) == () + assert tuple(handler.out_patterns) == ("out pattern",) + assert handler.center.name == "center" + assert handler.offset == 1.0 + assert handler.reach == 2.0 + assert handler.max_velocity == 3.0 + + +def test_collision_handler_gravity_pickle(): + from panda3d.core import CollisionHandlerGravity + + collider1 = NodePath(CollisionNode("collider1")) + collider2 = NodePath(CollisionNode("collider2")) + target1 = NodePath("target1") + target2 = NodePath("target2") + + handler = CollisionHandlerGravity() + handler.add_out_pattern("out pattern") + handler.add_collider(collider1, target1) + handler.add_collider(collider2, target2) + handler.offset = 1.0 + handler.reach = 2.0 + handler.max_velocity = 3.0 + handler.gravity = -4.0 + + handler = loads(dumps(handler, -1)) + + assert tuple(handler.in_patterns) == () + assert tuple(handler.again_patterns) == () + assert tuple(handler.out_patterns) == ("out pattern",) + assert handler.center == None + assert handler.offset == 1.0 + assert handler.reach == 2.0 + assert handler.max_velocity == 3.0 + assert handler.gravity == -4.0 + + +def test_collision_handler_pusher_pickle(): + from panda3d.core import CollisionHandlerPusher + + collider1 = NodePath(CollisionNode("collider1")) + collider2 = NodePath(CollisionNode("collider2")) + target1 = NodePath("target1") + target2 = NodePath("target2") + + handler = CollisionHandlerPusher() + handler.add_again_pattern("again pattern") + handler.add_collider(collider1, target1) + handler.add_collider(collider2, target2) + handler.horizontal = True + + handler = loads(dumps(handler, -1)) + + assert tuple(handler.in_patterns) == () + assert tuple(handler.again_patterns) == ("again pattern",) + assert tuple(handler.out_patterns) == () + assert not handler.has_center() + assert handler.horizontal From c77593f3f0359aaaad8015e98dc0c2f64214d2bd Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 1 Jan 2021 16:58:08 +0100 Subject: [PATCH 13/19] egg: add pickle support to most EggData classes --- makepanda/makepanda.py | 4 +- panda/src/egg/eggComment.h | 2 + panda/src/egg/eggComment_ext.cxx | 32 ++++++++ panda/src/egg/eggComment_ext.h | 37 +++++++++ panda/src/egg/eggCoordinateSystem.h | 2 + panda/src/egg/eggCoordinateSystem_ext.cxx | 32 ++++++++ panda/src/egg/eggCoordinateSystem_ext.h | 37 +++++++++ panda/src/egg/eggNode.h | 2 + panda/src/egg/eggNode_ext.cxx | 93 +++++++++++++++++++++++ panda/src/egg/eggNode_ext.h | 42 ++++++++++ panda/src/egg/p3egg_ext_composite.cxx | 4 + 11 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 panda/src/egg/eggComment_ext.cxx create mode 100644 panda/src/egg/eggComment_ext.h create mode 100644 panda/src/egg/eggCoordinateSystem_ext.cxx create mode 100644 panda/src/egg/eggCoordinateSystem_ext.h create mode 100644 panda/src/egg/eggNode_ext.cxx create mode 100644 panda/src/egg/eggNode_ext.h create mode 100644 panda/src/egg/p3egg_ext_composite.cxx diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index e493900a5e..a80e85ad04 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -4945,7 +4945,7 @@ if not RUNTIME and not PkgSkip("EGG"): if "parser.h" in IGATEFILES: IGATEFILES.remove("parser.h") TargetAdd('libp3egg.in', opts=OPTS, input=IGATEFILES) TargetAdd('libp3egg.in', opts=['IMOD:panda3d.egg', 'ILIB:libp3egg', 'SRCDIR:panda/src/egg']) - PyTargetAdd('p3egg_eggGroupNode_ext.obj', opts=OPTS, input='eggGroupNode_ext.cxx') + PyTargetAdd('p3egg_ext_composite.obj', opts=OPTS, input='p3egg_ext_composite.cxx') # # DIRECTORY: panda/src/egg2pg/ @@ -5035,7 +5035,7 @@ if not RUNTIME and not PkgSkip("EGG"): PyTargetAdd('egg_module.obj', opts=['IMOD:panda3d.egg', 'ILIB:egg', 'IMPORT:panda3d.core']) PyTargetAdd('egg.pyd', input='egg_module.obj') - PyTargetAdd('egg.pyd', input='p3egg_eggGroupNode_ext.obj') + PyTargetAdd('egg.pyd', input='p3egg_ext_composite.obj') PyTargetAdd('egg.pyd', input='libp3egg_igate.obj') PyTargetAdd('egg.pyd', input='libp3egg2pg_igate.obj') PyTargetAdd('egg.pyd', input='libpandaegg.dll') diff --git a/panda/src/egg/eggComment.h b/panda/src/egg/eggComment.h index 17611ed5d5..5e02595d3c 100644 --- a/panda/src/egg/eggComment.h +++ b/panda/src/egg/eggComment.h @@ -41,6 +41,8 @@ PUBLISHED: virtual void write(std::ostream &out, int indent_level) const; + EXTENSION(PyObject *__reduce__() const); + private: std::string _comment; diff --git a/panda/src/egg/eggComment_ext.cxx b/panda/src/egg/eggComment_ext.cxx new file mode 100644 index 0000000000..f058964066 --- /dev/null +++ b/panda/src/egg/eggComment_ext.cxx @@ -0,0 +1,32 @@ +/** + * 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 eggComment_ext.cxx + * @author rdb + * @date 2021-01-01 + */ + +#include "eggComment_ext.h" + +#ifdef HAVE_PYTHON + +/** + * Implements pickle support. + */ +PyObject *Extension:: +__reduce__() const { + extern struct Dtool_PyTypedObject Dtool_EggComment; + + std::string node_name = _this->get_name(); + std::string comment = _this->get_comment(); + return Py_BuildValue("O(s#s#)", (PyObject *)&Dtool_EggComment, + node_name.data(), (Py_ssize_t)node_name.length(), + comment.data(), (Py_ssize_t)comment.length()); +} + +#endif diff --git a/panda/src/egg/eggComment_ext.h b/panda/src/egg/eggComment_ext.h new file mode 100644 index 0000000000..2f13498d43 --- /dev/null +++ b/panda/src/egg/eggComment_ext.h @@ -0,0 +1,37 @@ +/** + * 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 eggComment_ext.h + * @author rdb + * @date 2021-01-01 + */ + +#ifndef EGGCOMMENT_EXT_H +#define EGGCOMMENT_EXT_H + +#include "dtoolbase.h" + +#ifdef HAVE_PYTHON + +#include "extension.h" +#include "eggComment.h" +#include "py_panda.h" + +/** + * This class defines the extension methods for EggComment, which are + * called instead of any C++ methods with the same prototype. + */ +template<> +class Extension : public ExtensionBase { +public: + PyObject *__reduce__() const; +}; + +#endif // HAVE_PYTHON + +#endif // EGGCOMMENT_EXT_H diff --git a/panda/src/egg/eggCoordinateSystem.h b/panda/src/egg/eggCoordinateSystem.h index 54a76e6f78..a3e105827c 100644 --- a/panda/src/egg/eggCoordinateSystem.h +++ b/panda/src/egg/eggCoordinateSystem.h @@ -36,6 +36,8 @@ PUBLISHED: virtual void write(std::ostream &out, int indent_level) const; + EXTENSION(PyObject *__reduce__() const); + private: CoordinateSystem _value; diff --git a/panda/src/egg/eggCoordinateSystem_ext.cxx b/panda/src/egg/eggCoordinateSystem_ext.cxx new file mode 100644 index 0000000000..2b26b0abec --- /dev/null +++ b/panda/src/egg/eggCoordinateSystem_ext.cxx @@ -0,0 +1,32 @@ +/** + * 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 eggCoordinateSystem_ext.cxx + * @author rdb + * @date 2021-01-01 + */ + +#include "eggCoordinateSystem_ext.h" + +#ifdef HAVE_PYTHON + +/** + * Implements pickle support. + */ +PyObject *Extension:: +__reduce__() const { + extern struct Dtool_PyTypedObject Dtool_EggCoordinateSystem; + + // We can't use the regular EggNode handling for EggCoordinateSystem, because + // the node is removed from the EggData after reading. + // Oh well, this is more efficient anyway. + int value = _this->get_value(); + return Py_BuildValue("O(i)", (PyObject *)&Dtool_EggCoordinateSystem, value); +} + +#endif diff --git a/panda/src/egg/eggCoordinateSystem_ext.h b/panda/src/egg/eggCoordinateSystem_ext.h new file mode 100644 index 0000000000..c5dc3794c6 --- /dev/null +++ b/panda/src/egg/eggCoordinateSystem_ext.h @@ -0,0 +1,37 @@ +/** + * 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 eggCoordinateSystem_ext.h + * @author rdb + * @date 2021-01-01 + */ + +#ifndef EGGCOORDINATESYSTEM_EXT_H +#define EGGCOORDINATESYSTEM_EXT_H + +#include "dtoolbase.h" + +#ifdef HAVE_PYTHON + +#include "extension.h" +#include "eggCoordinateSystem.h" +#include "py_panda.h" + +/** + * This class defines the extension methods for EggCoordinateSystem, which are + * called instead of any C++ methods with the same prototype. + */ +template<> +class Extension : public ExtensionBase { +public: + PyObject *__reduce__() const; +}; + +#endif // HAVE_PYTHON + +#endif // EGGCOORDINATESYSTEM_EXT_H diff --git a/panda/src/egg/eggNode.h b/panda/src/egg/eggNode.h index 950ae5e257..48f4443ed4 100644 --- a/panda/src/egg/eggNode.h +++ b/panda/src/egg/eggNode.h @@ -22,6 +22,7 @@ #include "lmatrix.h" #include "pointerTo.h" #include "referenceCount.h" +#include "extension.h" class EggGroupNode; class EggRenderMode; @@ -90,6 +91,7 @@ PUBLISHED: void test_under_integrity() const { } #endif // _DEBUG + EXTENSION(PyObject *__reduce__() const); protected: enum UnderFlags { diff --git a/panda/src/egg/eggNode_ext.cxx b/panda/src/egg/eggNode_ext.cxx new file mode 100644 index 0000000000..3056e40d6d --- /dev/null +++ b/panda/src/egg/eggNode_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 eggNode_ext.cxx + * @author rdb + * @date 2021-01-01 + */ + +#include "eggNode_ext.h" +#include "eggData.h" + +#ifdef HAVE_PYTHON + +/** + * Implements pickle support. + */ +PyObject *Extension:: +__reduce__() const { + extern struct Dtool_PyTypedObject Dtool_EggNode; + + // Find the parse_egg_node function in this module. + PyObject *sys_modules = PyImport_GetModuleDict(); + nassertr_always(sys_modules != nullptr, nullptr); + + PyObject *module_name = PyObject_GetAttrString((PyObject *)&Dtool_EggNode, "__module__"); + nassertr_always(module_name != nullptr, nullptr); + + PyObject *module = PyDict_GetItem(sys_modules, module_name); + Py_DECREF(module_name); + nassertr_always(module != nullptr, nullptr); + + PyObject *func; + if (_this->is_of_type(EggData::get_class_type())) { + func = PyObject_GetAttrString(module, "parse_egg_data"); + } else { + func = PyObject_GetAttrString(module, "parse_egg_node"); + } + nassertr_always(func != nullptr, nullptr); + + // Get the egg syntax to pass to the parse_egg_node function. + std::ostringstream stream; + _this->write(stream, INT_MIN); + std::string data = stream.str(); + size_t length = data.size(); + + // Trim trailing whitespace. + while (length > 0 && isspace(data[length - 1])) { + --length; + } + + return Py_BuildValue("N(s#)", func, data.data(), (Py_ssize_t)length); +} + +/** + * Parses an EggData from the raw egg syntax. + */ +PT(EggData) parse_egg_data(const std::string &egg_syntax) { + PT(EggData) data = new EggData; + data->set_auto_resolve_externals(false); + + std::istringstream in(egg_syntax); + + if (!data->read(in)) { + PyErr_Format(PyExc_RuntimeError, "failed to parse egg data"); + return nullptr; + } + + return data; +} + +/** + * Parses a single egg node from the raw egg syntax. + */ +PT(EggNode) parse_egg_node(const std::string &egg_syntax) { + PT(EggData) data = parse_egg_data(egg_syntax); + if (data == nullptr) { + return nullptr; + } + + if (data->size() != 1) { + PyErr_Format(PyExc_RuntimeError, "expected exactly one node"); + return nullptr; + } + + return data->remove_child(data->get_first_child()); +} + +#endif diff --git a/panda/src/egg/eggNode_ext.h b/panda/src/egg/eggNode_ext.h new file mode 100644 index 0000000000..aa94aa41cd --- /dev/null +++ b/panda/src/egg/eggNode_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 eggNode_ext.h + * @author rdb + * @date 2021-01-01 + */ + +#ifndef EGGNODE_EXT_H +#define EGGNODE_EXT_H + +#include "dtoolbase.h" + +#ifdef HAVE_PYTHON + +#include "extension.h" +#include "eggNode.h" +#include "py_panda.h" + +/** + * This class defines the extension methods for EggNode, which are + * called instead of any C++ methods with the same prototype. + */ +template<> +class Extension : public ExtensionBase { +public: + PyObject *__reduce__() const; +}; + +BEGIN_PUBLISH +PT(EggData) parse_egg_data(const std::string &egg_syntax); +PT(EggNode) parse_egg_node(const std::string &egg_syntax); +END_PUBLISH + +#endif // HAVE_PYTHON + +#endif // EGGNODE_EXT_H diff --git a/panda/src/egg/p3egg_ext_composite.cxx b/panda/src/egg/p3egg_ext_composite.cxx new file mode 100644 index 0000000000..f94bca9c30 --- /dev/null +++ b/panda/src/egg/p3egg_ext_composite.cxx @@ -0,0 +1,4 @@ +#include "eggComment_ext.cxx" +#include "eggCoordinateSystem_ext.cxx" +#include "eggGroupNode_ext.cxx" +#include "eggNode_ext.cxx" From 3a5201fd65a3fbba0d76d010da3e40327d2b08a6 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 1 Jan 2021 17:08:34 +0100 Subject: [PATCH 14/19] tests: Add tests for pickling DoubleBitMask --- tests/putil/test_bitmask.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/putil/test_bitmask.py b/tests/putil/test_bitmask.py index b2bbdf9c79..5d6943e40b 100644 --- a/tests/putil/test_bitmask.py +++ b/tests/putil/test_bitmask.py @@ -34,3 +34,20 @@ def test_bitmask_pickle(): data = pickle.dumps(mask1, -1) mask2 = pickle.loads(data) assert mask1 == mask2 + + assert pickle.loads(pickle.dumps(DoubleBitMaskNative(0), -1)).is_zero() + + mask1 = DoubleBitMaskNative(0xffff0001) + data = pickle.dumps(mask1, -1) + mask2 = pickle.loads(data) + assert mask1 == mask2 + + mask1 = DoubleBitMaskNative(0x7fffffffffffffff) + data = pickle.dumps(mask1, -1) + mask2 = pickle.loads(data) + assert mask1 == mask2 + + mask1 = DoubleBitMaskNative(1 << (double_num_bits - 1)) + data = pickle.dumps(mask1, -1) + mask2 = pickle.loads(data) + assert mask1 == mask2 From e755f8713088c986bd3dc3f23d8e3f609e3a2bbe Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 1 Jan 2021 17:08:55 +0100 Subject: [PATCH 15/19] pgraph: Add pickling for LoaderFileTypeRegistry Useful to test that pickling singletons works --- panda/src/pgraph/loaderFileTypeRegistry.h | 2 ++ panda/src/pgraph/loaderFileTypeRegistry_ext.cxx | 10 ++++++++++ panda/src/pgraph/loaderFileTypeRegistry_ext.h | 2 ++ tests/pgraph/test_loader_types.py | 9 +++++++++ 4 files changed, 23 insertions(+) diff --git a/panda/src/pgraph/loaderFileTypeRegistry.h b/panda/src/pgraph/loaderFileTypeRegistry.h index cccb3af333..27f29664dd 100644 --- a/panda/src/pgraph/loaderFileTypeRegistry.h +++ b/panda/src/pgraph/loaderFileTypeRegistry.h @@ -53,6 +53,8 @@ PUBLISHED: static LoaderFileTypeRegistry *get_global_ptr(); + EXTENSION(PyObject *__reduce__() const); + private: void record_extension(const std::string &extension, LoaderFileType *type); diff --git a/panda/src/pgraph/loaderFileTypeRegistry_ext.cxx b/panda/src/pgraph/loaderFileTypeRegistry_ext.cxx index 20450e9195..eba66353df 100644 --- a/panda/src/pgraph/loaderFileTypeRegistry_ext.cxx +++ b/panda/src/pgraph/loaderFileTypeRegistry_ext.cxx @@ -18,6 +18,7 @@ #include "pythonLoaderFileType.h" extern struct Dtool_PyTypedObject Dtool_LoaderFileType; +extern struct Dtool_PyTypedObject Dtool_LoaderFileTypeRegistry; /** * Registers a loader file type that is implemented in Python. @@ -112,4 +113,13 @@ unregister_type(PyObject *type) { Py_XDECREF(save_func); } +/** + * Implements pickle support. + */ +PyObject *Extension:: +__reduce__() const { + PyObject *func = PyObject_GetAttrString((PyObject *)&Dtool_LoaderFileTypeRegistry, "get_global_ptr"); + return Py_BuildValue("N()", func); +} + #endif diff --git a/panda/src/pgraph/loaderFileTypeRegistry_ext.h b/panda/src/pgraph/loaderFileTypeRegistry_ext.h index 9c9815c20d..e5325101e6 100644 --- a/panda/src/pgraph/loaderFileTypeRegistry_ext.h +++ b/panda/src/pgraph/loaderFileTypeRegistry_ext.h @@ -33,6 +33,8 @@ public: void register_deferred_type(PyObject *entry_point); void unregister_type(PyObject *type); + + PyObject *__reduce__() const; }; #endif // HAVE_PYTHON diff --git a/tests/pgraph/test_loader_types.py b/tests/pgraph/test_loader_types.py index 8fca1c5849..2a0114852f 100644 --- a/tests/pgraph/test_loader_types.py +++ b/tests/pgraph/test_loader_types.py @@ -3,6 +3,7 @@ import pytest import tempfile import os from contextlib import contextmanager +import sys @pytest.fixture @@ -218,3 +219,11 @@ def test_loader_ram_cache(test_filename): assert model1 == model2 ModelPool.release_model(model2) + + +@pytest.mark.skipif(sys.version_info < (3, 4), reason="Requires Python 3.4") +def test_loader_file_type_registry_pickle(): + from direct.stdpy.pickle import dumps, loads + + registry = LoaderFileTypeRegistry.get_global_ptr() + assert loads(dumps(registry, -1)) == registry From 52b4df4aecabb72970e5ce3ec8c8206498c31471 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 1 Jan 2021 17:41:34 +0100 Subject: [PATCH 16/19] stdpy: Expose DEFAULT_PROTOCOL and HIGHEST_PROTOCOL in direct.stdpy.pickle --- direct/src/stdpy/pickle.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/direct/src/stdpy/pickle.py b/direct/src/stdpy/pickle.py index aa50209e13..2d82d90b6b 100644 --- a/direct/src/stdpy/pickle.py +++ b/direct/src/stdpy/pickle.py @@ -22,7 +22,7 @@ Unfortunately, cPickle cannot be supported, because it does not support extensions of this nature. """ __all__ = ["PickleError", "PicklingError", "UnpicklingError", "Pickler", - "Unpickler", "dump", "dumps", "load", "loads"] + "Unpickler", "dump", "dumps", "load", "loads", "HIGHEST_PROTOCOL"] import sys from panda3d.core import BamWriter, BamReader, TypedObject @@ -36,11 +36,16 @@ else: # with the local pickle.py. pickle = __import__('pickle') +HIGHEST_PROTOCOL = pickle.HIGHEST_PROTOCOL + PickleError = pickle.PickleError PicklingError = pickle.PicklingError UnpicklingError = pickle.UnpicklingError if sys.version_info >= (3, 0): + DEFAULT_PROTOCOL = pickle.DEFAULT_PROTOCOL + __all__.append("DEFAULT_PROTOCOL") + BasePickler = pickle._Pickler BaseUnpickler = pickle._Unpickler else: From b2462c1d8c97cda460a9ecb21b45332171ac2d78 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 1 Jan 2021 17:42:30 +0100 Subject: [PATCH 17/19] express: Support pickling PointerToArray objects --- 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 | 23 +++++++++++++++++++++ 4 files changed, 59 insertions(+) create mode 100644 tests/express/test_pointertoarray.py diff --git a/panda/src/express/pointerToArray.h b/panda/src/express/pointerToArray.h index 7ff07a5ef1..24a5322bc7 100644 --- a/panda/src/express/pointerToArray.h +++ b/panda/src/express/pointerToArray.h @@ -117,6 +117,8 @@ PUBLISHED: INLINE size_t count(const Element &) const; #ifdef HAVE_PYTHON + EXTENSION(PyObject *__reduce__(PyObject *self) const); + EXTENSION(int __getbuffer__(PyObject *self, Py_buffer *view, int flags)); EXTENSION(void __releasebuffer__(PyObject *self, Py_buffer *view) const); #endif @@ -273,6 +275,8 @@ PUBLISHED: INLINE size_t count(const Element &) const; #ifdef HAVE_PYTHON + EXTENSION(PyObject *__reduce__(PyObject *self) const); + EXTENSION(int __getbuffer__(PyObject *self, Py_buffer *view, int flags) const); EXTENSION(void __releasebuffer__(PyObject *self, Py_buffer *view) const); #endif diff --git a/panda/src/express/pointerToArray_ext.I b/panda/src/express/pointerToArray_ext.I index 5bcd1b77e9..435ebb8659 100644 --- a/panda/src/express/pointerToArray_ext.I +++ b/panda/src/express/pointerToArray_ext.I @@ -259,6 +259,25 @@ get_subdata(size_t n, size_t count) const { #endif } +/** + * Implements pickle support. + */ +template +INLINE PyObject *Extension >:: +__reduce__(PyObject *self) const { + // This preserves the distinction between a null vs. an empty PTA, though I'm + // not sure that this distinction matters to anyone. + if (this->_this->is_null()) { + return Py_BuildValue("O()", Py_TYPE(self)); + } + else if (this->_this->empty()) { + return Py_BuildValue("O(())", Py_TYPE(self)); + } + else { + return Py_BuildValue("O(N)", Py_TYPE(self), get_data()); + } +} + /** * Same as get_element(), this returns the nth element of the array. */ @@ -304,6 +323,15 @@ get_subdata(size_t n, size_t count) const { #endif } +/** + * Implements pickle support. + */ +template +INLINE PyObject *Extension >:: +__reduce__(PyObject *self) const { + return Py_BuildValue("O(N)", Py_TYPE(self), get_data()); +} + /** * This is used to implement the buffer protocol, in order to allow efficient * access to the array data through a Python multiview object. diff --git a/panda/src/express/pointerToArray_ext.h b/panda/src/express/pointerToArray_ext.h index f17d1b634a..ce6104791b 100644 --- a/panda/src/express/pointerToArray_ext.h +++ b/panda/src/express/pointerToArray_ext.h @@ -40,6 +40,8 @@ public: INLINE void set_data(PyObject *data); INLINE PyObject *get_subdata(size_t n, size_t count) const; + INLINE PyObject *__reduce__(PyObject *self) const; + INLINE int __getbuffer__(PyObject *self, Py_buffer *view, int flags); INLINE void __releasebuffer__(PyObject *self, Py_buffer *view) const; }; @@ -75,6 +77,8 @@ public: INLINE PyObject *get_data() const; INLINE PyObject *get_subdata(size_t n, size_t count) const; + INLINE PyObject *__reduce__(PyObject *self) const; + INLINE int __getbuffer__(PyObject *self, Py_buffer *view, int flags) const; INLINE void __releasebuffer__(PyObject *self, Py_buffer *view) const; }; diff --git a/tests/express/test_pointertoarray.py b/tests/express/test_pointertoarray.py new file mode 100644 index 0000000000..fe76c94128 --- /dev/null +++ b/tests/express/test_pointertoarray.py @@ -0,0 +1,23 @@ +def test_pta_float_pickle(): + from panda3d.core import PTA_float + from direct.stdpy.pickle import dumps, loads, HIGHEST_PROTOCOL + + null_pta = PTA_float() + + empty_pta = PTA_float([]) + + data_pta = PTA_float([1.0, 2.0, 3.0]) + data = data_pta.get_data() + + for proto in range(1, HIGHEST_PROTOCOL + 1): + null_pta2 = loads(dumps(null_pta, proto)) + assert null_pta2.is_null() + assert len(null_pta2) == 0 + + empty_pta2 = loads(dumps(empty_pta, proto)) + assert not empty_pta2.is_null() + assert len(empty_pta2) == 0 + + 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() From c81c506df3e21575a54929d811a0abad7d9c8083 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 1 Jan 2021 17:49:15 +0100 Subject: [PATCH 18/19] express: Fix PointerToArray comparison operator --- panda/src/express/pointerToArrayBase.h | 6 ++++++ tests/express/test_pointertoarray.py | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/panda/src/express/pointerToArrayBase.h b/panda/src/express/pointerToArrayBase.h index 3c985ff379..898d5c82ad 100644 --- a/panda/src/express/pointerToArrayBase.h +++ b/panda/src/express/pointerToArrayBase.h @@ -77,6 +77,12 @@ protected: PUBLISHED: INLINE ~PointerToArrayBase(); + +#ifdef CPPPARSER + // These are implemented in PointerToVoid, but expose them here. + INLINE bool operator == (const PointerToArrayBase &other) const; + INLINE bool operator != (const PointerToArrayBase &other) const; +#endif }; #include "pointerToArrayBase.I" diff --git a/tests/express/test_pointertoarray.py b/tests/express/test_pointertoarray.py index fe76c94128..850cdc33ed 100644 --- a/tests/express/test_pointertoarray.py +++ b/tests/express/test_pointertoarray.py @@ -1,3 +1,26 @@ +def test_pta_float_compare(): + from panda3d.core import PTA_float, CPTA_float + + # Two null PTAs + assert PTA_float() == PTA_float() + assert not (PTA_float() != PTA_float()) + + # Two non-null PTAs + assert PTA_float([1]) != PTA_float([1]) + assert not (PTA_float([1]) == PTA_float([1])) + + # A copy of a PTA + pta = PTA_float([1]) + assert pta == PTA_float(pta) + assert not (pta != PTA_float(pta)) + + # A const copy of a PTA + pta = PTA_float([1]) + cpta = CPTA_float(pta) + assert pta == cpta + assert not (pta != cpta) + + def test_pta_float_pickle(): from panda3d.core import PTA_float from direct.stdpy.pickle import dumps, loads, HIGHEST_PROTOCOL From a84f1b5595bf7d09b11763796950d49d124f4291 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 2 Jan 2021 02:21:38 +0100 Subject: [PATCH 19/19] express: distinguish between null vs empty in CPTA pickle as well --- panda/src/express/pointerToArray_ext.I | 9 ++++++++- tests/express/test_pointertoarray.py | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/panda/src/express/pointerToArray_ext.I b/panda/src/express/pointerToArray_ext.I index 435ebb8659..b64f973a5f 100644 --- a/panda/src/express/pointerToArray_ext.I +++ b/panda/src/express/pointerToArray_ext.I @@ -329,7 +329,14 @@ get_subdata(size_t n, size_t count) const { template INLINE PyObject *Extension >:: __reduce__(PyObject *self) const { - return Py_BuildValue("O(N)", Py_TYPE(self), get_data()); + // This preserves the distinction between a null vs. an empty PTA, though I'm + // not sure that this distinction matters to anyone. + if (!this->_this->is_null() && this->_this->empty()) { + return Py_BuildValue("O([])", Py_TYPE(self)); + } + else { + return Py_BuildValue("O(N)", Py_TYPE(self), get_data()); + } } /** diff --git a/tests/express/test_pointertoarray.py b/tests/express/test_pointertoarray.py index 850cdc33ed..e68fa2ca36 100644 --- a/tests/express/test_pointertoarray.py +++ b/tests/express/test_pointertoarray.py @@ -44,3 +44,28 @@ def test_pta_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_cpta_float_pickle(): + from panda3d.core import PTA_float, CPTA_float + from direct.stdpy.pickle import dumps, loads, HIGHEST_PROTOCOL + + null_pta = CPTA_float(PTA_float()) + + empty_pta = CPTA_float([]) + + data_pta = CPTA_float([1.0, 2.0, 3.0]) + data = data_pta.get_data() + + for proto in range(1, HIGHEST_PROTOCOL + 1): + null_pta2 = loads(dumps(null_pta, proto)) + assert null_pta2.is_null() + assert len(null_pta2) == 0 + + empty_pta2 = loads(dumps(empty_pta, proto)) + assert not empty_pta2.is_null() + assert len(empty_pta2) == 0 + + 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()