From 66c9264f300fe41f37a30c4ffbde8ba2a9fb8097 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 3 Aug 2025 11:59:28 +0200 Subject: [PATCH 1/4] bam: Add forward compatibility for bam 6.46 See #1657 and 052bd770300f407b11a428f3cfb74e24515abdd1 --- panda/src/pgraph/modelRoot.cxx | 8 ++++++++ panda/src/putil/bam.h | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/panda/src/pgraph/modelRoot.cxx b/panda/src/pgraph/modelRoot.cxx index 30dbf1e6a6..aae68b052e 100644 --- a/panda/src/pgraph/modelRoot.cxx +++ b/panda/src/pgraph/modelRoot.cxx @@ -41,6 +41,10 @@ register_with_read_factory() { void ModelRoot:: write_datagram(BamWriter *manager, Datagram &dg) { ModelNode::write_datagram(manager, dg); + + if (manager->get_file_minor_ver() >= 46) { + manager->write_handle(dg, TypeHandle::none()); + } } /** @@ -67,4 +71,8 @@ make_from_bam(const FactoryParams ¶ms) { void ModelRoot:: fillin(DatagramIterator &scan, BamReader *manager) { ModelNode::fillin(scan, manager); + + if (manager->get_file_minor_ver() >= 46) { + manager->read_handle(scan); + } } diff --git a/panda/src/putil/bam.h b/panda/src/putil/bam.h index 33ce0b9f38..31008f788c 100644 --- a/panda/src/putil/bam.h +++ b/panda/src/putil/bam.h @@ -32,7 +32,7 @@ static const unsigned short _bam_major_ver = 6; // Bumped to major version 6 on 2006-02-11 to factor out PandaNode::CData. static const unsigned short _bam_first_minor_ver = 14; -static const unsigned short _bam_last_minor_ver = 45; +static const unsigned short _bam_last_minor_ver = 46; static const unsigned short _bam_minor_ver = 44; // Bumped to minor version 14 on 2007-12-19 to change default ColorAttrib. // Bumped to minor version 15 on 2008-04-09 to add TextureAttrib::_implicit_sort. @@ -66,5 +66,6 @@ static const unsigned short _bam_minor_ver = 44; // Bumped to minor version 43 on 2018-12-06 to expand BillboardEffect and CompassEffect. // Bumped to minor version 44 on 2018-12-23 to rename CollisionTube to CollisionCapsule. // Bumped to minor version 45 on 2020-03-18 to add Texture::_clear_color. +// Bumped to minor version 46 on 2025-08-03 to add ModelRoot::_loader_type. #endif From aa554a2130a2b22fef1acd70956a3f12e31d992d Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 28 Aug 2025 10:24:34 +0200 Subject: [PATCH 2/4] task: Backport fix for generators without send() Backport of 00b5357b8ebfe521851f296b7d555d3ba1cf16f6 --- panda/src/event/pythonTask.cxx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/panda/src/event/pythonTask.cxx b/panda/src/event/pythonTask.cxx index 8395bd5adc..4b3a79f7ef 100644 --- a/panda/src/event/pythonTask.cxx +++ b/panda/src/event/pythonTask.cxx @@ -534,9 +534,15 @@ do_python_task() { // We are calling a generator. Use "send" rather than PyIter_Next since // we need to be able to read the value from a StopIteration exception. PyObject *func = PyObject_GetAttrString(_generator, "send"); - nassertr(func != nullptr, DS_interrupt); - result = PyObject_CallFunctionObjArgs(func, Py_None, nullptr); - Py_DECREF(func); + if (func != nullptr) { + result = PyObject_CallOneArg(func, Py_None); + Py_DECREF(func); + } else { + // It has no send(), just call next() directly. + nassertr(Py_TYPE(_generator)->tp_iternext != nullptr, DS_interrupt); + PyErr_Clear(); + result = Py_TYPE(_generator)->tp_iternext(_generator); + } if (result == nullptr) { // An error happened. If StopIteration, that indicates the task has From 86ba156a7bba32275785a466553bf324e5271b5a Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 28 Aug 2025 11:33:12 +0200 Subject: [PATCH 3/4] py_compat: Update for Python 3.13 and 3.14 --- dtool/src/interrogatedb/py_compat.h | 71 +++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/dtool/src/interrogatedb/py_compat.h b/dtool/src/interrogatedb/py_compat.h index a1ec1958ab..4b58a9e3b8 100644 --- a/dtool/src/interrogatedb/py_compat.h +++ b/dtool/src/interrogatedb/py_compat.h @@ -287,6 +287,77 @@ INLINE bool PyLong_IsNonNegative(PyObject *value) { # define PyLong_AsInt(x) (_PyLong_AsInt(x)) #endif +#if PY_VERSION_HEX < 0x030D00A1 +ALWAYS_INLINE int +PyModule_Add(PyObject *mod, const char *name, PyObject *value) { + int res = PyModule_AddObjectRef(mod, name, value); + Py_XDECREF(value); + return res; +} +#endif + +#if PY_VERSION_HEX < 0x030D00A1 +INLINE int +PyDict_GetItemRef(PyObject *mp, PyObject *key, PyObject **result) { +#if PY_MAJOR_VERSION >= 3 + PyObject *item = PyDict_GetItemWithError(mp, key); +#else + PyObject *item = _PyDict_GetItemWithError(mp, key); +#endif + if (item != nullptr) { + *result = Py_NewRef(item); + return 1; + } + *result = nullptr; + return PyErr_Occurred() ? -1 : 0; +} + +INLINE int +PyDict_GetItemStringRef(PyObject *mp, const char *key, PyObject **result) { + PyObject *item = nullptr; +#if PY_MAJOR_VERSION >= 3 + PyObject *key_obj = PyUnicode_FromString(key); + item = key_obj ? PyDict_GetItemWithError(mp, key_obj) : nullptr; +#else + PyObject *key_obj = PyString_FromString(key); + item = key_obj ? _PyDict_GetItemWithError(mp, key_obj) : nullptr; +#endif + Py_DECREF(key_obj); + if (item != nullptr) { + *result = Py_NewRef(item); + return 1; + } + *result = nullptr; + return PyErr_Occurred() ? -1 : 0; +} +#endif + +#if PY_VERSION_HEX >= 0x03050200 && PY_VERSION_HEX < 0x030D00A1 +# define PyThreadState_GetUnchecked() (_PyThreadState_UncheckedGet()) +#endif + +#if PY_VERSION_HEX < 0x030D00A2 +# define PyList_Extend(list, iterable) (PyList_SetSlice((list), PY_SSIZE_T_MAX, PY_SSIZE_T_MAX, (iterable))) +# define PyList_Clear(list) (PyList_SetSlice((list), 0, PY_SSIZE_T_MAX, nullptr)) +#endif + +#if PY_VERSION_HEX < 0x030D00A4 +# define PyList_GetItemRef(op, index) (Py_XNewRef(PyList_GetItem((op), (index)))) +#endif + +#if PY_VERSION_HEX < 0x030D00B3 +# define Py_BEGIN_CRITICAL_SECTION(op) { +# define Py_END_CRITICAL_SECTION() } +# define Py_BEGIN_CRITICAL_SECTION2(a, b) { +# define Py_END_CRITICAL_SECTION2() } +#endif + +/* Python 3.14 */ + +#if PY_VERSION_HEX < 0x030E00A8 +# define PyUnstable_Object_IsUniquelyReferenced(op) (Py_REFCNT((op)) == 1) +#endif + /* Other Python implementations */ // _PyErr_OCCURRED is an undocumented macro version of PyErr_Occurred. From b417d6a3738423440afa4dfaa897dbb927cbf753 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 28 Aug 2025 11:34:37 +0200 Subject: [PATCH 4/4] backport: A few thread safety things for free-threaded builds --- panda/src/event/pythonTask.cxx | 12 ++++++++++++ panda/src/pgraph/nodePath_ext.cxx | 8 ++++++++ panda/src/pgraph/pandaNode_ext.cxx | 8 ++++++++ panda/src/pstatclient/pStatClient_ext.cxx | 19 +++++++++++++++++-- 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/panda/src/event/pythonTask.cxx b/panda/src/event/pythonTask.cxx index 4b3a79f7ef..f5af3f48de 100644 --- a/panda/src/event/pythonTask.cxx +++ b/panda/src/event/pythonTask.cxx @@ -368,6 +368,17 @@ __getattr__(PyObject *attr) const { // tp_getattro slot (a la __getattribute__). So, we won't get here when the // attribute has already been found via other methods. +#if PY_VERSION_HEX >= 0x030D00A1 // 3.13 + PyObject *item = nullptr; + if (PyDict_GetItemRef(__dict__, attr, &item) == 0) { + // PyDict_GetItemRef does not raise an exception on missing attribute. + PyErr_Format(PyExc_AttributeError, + "'PythonTask' object has no attribute '%U'", + attr); + } + return item; + +#else // 3.12 and lower PyObject *item = PyDict_GetItem(__dict__, attr); if (item == nullptr) { @@ -387,6 +398,7 @@ __getattr__(PyObject *attr) const { // PyDict_GetItem returns a borrowed reference. Py_INCREF(item); return item; +#endif } /** diff --git a/panda/src/pgraph/nodePath_ext.cxx b/panda/src/pgraph/nodePath_ext.cxx index c4f7925f24..61e6e74907 100644 --- a/panda/src/pgraph/nodePath_ext.cxx +++ b/panda/src/pgraph/nodePath_ext.cxx @@ -55,6 +55,13 @@ PyObject *Extension:: __deepcopy__(PyObject *self, PyObject *memo) const { extern struct Dtool_PyTypedObject Dtool_NodePath; +#if PY_VERSION_HEX >= 0x030D00A1 // 3.13 + PyObject *dupe; + if (PyDict_GetItemRef(memo, self, &dupe) != 0) { + // Already in the memo dictionary (or an error happened). + return dupe; + } +#else // Borrowed reference. PyObject *dupe = PyDict_GetItem(memo, self); if (dupe != nullptr) { @@ -62,6 +69,7 @@ __deepcopy__(PyObject *self, PyObject *memo) const { Py_INCREF(dupe); return dupe; } +#endif NodePath *np_dupe; if (_this->is_empty()) { diff --git a/panda/src/pgraph/pandaNode_ext.cxx b/panda/src/pgraph/pandaNode_ext.cxx index d4071ff4c6..95e04aca14 100644 --- a/panda/src/pgraph/pandaNode_ext.cxx +++ b/panda/src/pgraph/pandaNode_ext.cxx @@ -46,6 +46,13 @@ PyObject *Extension:: __deepcopy__(PyObject *self, PyObject *memo) const { extern struct Dtool_PyTypedObject Dtool_PandaNode; +#if PY_VERSION_HEX >= 0x030D00A1 // 3.13 + PyObject *dupe; + if (PyDict_GetItemRef(memo, self, &dupe) != 0) { + // Already in the memo dictionary (or an error happened). + return dupe; + } +#else // Borrowed reference. PyObject *dupe = PyDict_GetItem(memo, self); if (dupe != nullptr) { @@ -53,6 +60,7 @@ __deepcopy__(PyObject *self, PyObject *memo) const { Py_INCREF(dupe); return dupe; } +#endif PT(PandaNode) node_dupe = _this->copy_subgraph(); diff --git a/panda/src/pstatclient/pStatClient_ext.cxx b/panda/src/pstatclient/pStatClient_ext.cxx index 684ac1db2a..6071b7dfbc 100644 --- a/panda/src/pstatclient/pStatClient_ext.cxx +++ b/panda/src/pstatclient/pStatClient_ext.cxx @@ -73,11 +73,21 @@ __declspec(noinline) make_python_frame_collector(PyFrameObject *frame, PyCodeObject *code) { #if PY_VERSION_HEX >= 0x030B0000 // 3.11 // Fetch the module name out of the frame's global scope. + const char *mod_name = ""; + PyObject *py_mod_name = nullptr; PyObject *globals = PyFrame_GetGlobals(frame); - PyObject *py_mod_name = PyDict_GetItemString(globals, "__name__"); +#if PY_VERSION_HEX >= 0x030D00A1 // 3.13 + if (PyDict_GetItemStringRef(globals, "__name__", &py_mod_name) > 0) { + mod_name = PyUnicode_AsUTF8(py_mod_name); + } +#else + py_mod_name = PyDict_GetItemString(globals, "__name__"); + if (py_mod_name != nullptr) { + mod_name = PyUnicode_AsUTF8(py_mod_name); + } +#endif Py_DECREF(globals); - const char *mod_name = py_mod_name ? PyUnicode_AsUTF8(py_mod_name) : ""; const char *meth_name = PyUnicode_AsUTF8(code->co_qualname); char buffer[1024]; size_t len = snprintf(buffer, sizeof(buffer), "%s:%s", mod_name, meth_name); @@ -86,6 +96,11 @@ make_python_frame_collector(PyFrameObject *frame, PyCodeObject *code) { buffer[i] = ':'; } } + +#if PY_VERSION_HEX >= 0x030D00A1 // 3.13 + Py_XDECREF(py_mod_name); +#endif + #else // Try to figure out the type name. There's no obvious way to do this. // It's possible that the first argument passed to this function is the