Browse Source

Merge branch 'master' into shaderpipeline

rdb 1 year ago
parent
commit
34b7bb84a0
53 changed files with 680 additions and 279 deletions
  1. 1 0
      BACKERS.md
  2. 35 27
      direct/src/dcparser/dcClass_ext.cxx
  3. 2 0
      direct/src/dcparser/dcPacker_ext.cxx
  4. 31 30
      direct/src/distributed/ConnectionRepository.py
  5. 23 29
      direct/src/distributed/cConnectionRepository.cxx
  6. 0 4
      direct/src/showbase/Transitions.py
  7. 1 1
      dtool/Config.cmake
  8. 19 12
      dtool/src/dtoolbase/typeHandle_ext.cxx
  9. 1 1
      dtool/src/dtoolutil/pfstreamBuf.cxx
  10. 16 0
      dtool/src/interrogatedb/dtool_super_base.cxx
  11. 5 1
      dtool/src/interrogatedb/py_compat.cxx
  12. 85 0
      dtool/src/interrogatedb/py_compat.h
  13. 60 21
      dtool/src/interrogatedb/py_panda.cxx
  14. 7 0
      dtool/src/interrogatedb/py_panda.h
  15. 29 16
      dtool/src/interrogatedb/py_wrappers.cxx
  16. 2 2
      makepanda/makepandacore.py
  17. 1 1
      panda/src/cocoadisplay/cocoaGraphicsPipe.mm
  18. 25 20
      panda/src/cocoadisplay/cocoaGraphicsWindow.mm
  19. 5 5
      panda/src/cocoadisplay/cocoaPandaApp.mm
  20. 7 3
      panda/src/collide/collisionPolygon_ext.cxx
  21. 1 1
      panda/src/display/frameBufferProperties.cxx
  22. 4 2
      panda/src/display/frameBufferProperties_ext.cxx
  23. 24 0
      panda/src/display/graphicsEngine.cxx
  24. 4 2
      panda/src/display/windowProperties_ext.cxx
  25. 5 3
      panda/src/distort/projectionScreen.cxx
  26. 2 4
      panda/src/egg/eggNode_ext.cxx
  27. 2 0
      panda/src/event/asyncFuture_ext.cxx
  28. 21 12
      panda/src/event/pythonTask.cxx
  29. 4 2
      panda/src/express/pointerToArray_ext.I
  30. 4 4
      panda/src/gobj/textureCollection_ext.cxx
  31. 2 2
      panda/src/linmath/lorientation_src.I
  32. 4 4
      panda/src/pgraph/nodePathCollection_ext.cxx
  33. 6 4
      panda/src/pgraph/nodePath_ext.cxx
  34. 11 10
      panda/src/pgraph/pandaNode_ext.cxx
  35. 6 7
      panda/src/pgraph/pythonLoaderFileType.cxx
  36. 1 1
      panda/src/pgraph/renderState_ext.cxx
  37. 9 3
      panda/src/pgraph/shaderAttrib_ext.cxx
  38. 12 1
      panda/src/pgraph/shaderInput_ext.cxx
  39. 1 1
      panda/src/pipeline/cycleDataLockedStageReader.I
  40. 0 8
      panda/src/pipeline/threadWin32Impl.I
  41. 30 0
      panda/src/pipeline/threadWin32Impl.cxx
  42. 2 1
      panda/src/pipeline/threadWin32Impl.h
  43. 24 5
      panda/src/pstatclient/pStatClient_ext.cxx
  44. 5 1
      panda/src/putil/bitArray_ext.cxx
  45. 16 0
      panda/src/putil/clockObject.cxx
  46. 3 3
      panda/src/putil/config_putil.cxx
  47. 11 1
      panda/src/putil/doubleBitMask_ext.I
  48. 25 23
      panda/src/putil/typedWritable_ext.cxx
  49. 12 0
      panda/src/testbed/pview.cxx
  50. 2 0
      pandatool/src/deploy-stub/deploy-stub.c
  51. 29 0
      tests/event/test_futures.py
  52. 37 1
      tests/event/test_pythontask.py
  53. 6 0
      tests/interrogate/test_property.py

+ 1 - 0
BACKERS.md

@@ -33,6 +33,7 @@ This is a list of all the people who are contributing financially to Panda3D.  I
 * GameDev JONI
 * Max Rodriguez
 * Jethro Schoppenhorst
+* dabe
 
 ## Backers
 

+ 35 - 27
direct/src/dcparser/dcClass_ext.cxx

@@ -522,31 +522,35 @@ client_format_generate_CMU(PyObject *distobj, DOID_TYPE do_id,
   }
 
   // Also specify the optional fields.
-  int num_optional_fields = 0;
   if (PyObject_IsTrue(optional_fields)) {
-    num_optional_fields = PySequence_Size(optional_fields);
-  }
-  packer.raw_pack_uint16(num_optional_fields);
-
-  for (int i = 0; i < num_optional_fields; i++) {
-    PyObject *py_field_name = PySequence_GetItem(optional_fields, i);
-    std::string field_name = PyUnicode_AsUTF8(py_field_name);
-    Py_XDECREF(py_field_name);
-
-    DCField *field = _this->get_field_by_name(field_name);
-    if (field == nullptr) {
-      std::ostringstream strm;
-      strm << "No field named " << field_name << " in class " << _this->get_name()
-           << "\n";
-      nassert_raise(strm.str());
-      return Datagram();
-    }
-    packer.raw_pack_uint16(field->get_number());
-    packer.begin_pack(field);
-    if (!pack_required_field(packer, distobj, field)) {
-      return Datagram();
+    optional_fields = PySequence_Tuple(optional_fields);
+    Py_ssize_t num_optional_fields = PyTuple_GET_SIZE(optional_fields);
+    packer.raw_pack_uint16(num_optional_fields);
+
+    for (Py_ssize_t i = 0; i < num_optional_fields; i++) {
+      PyObject *py_field_name = PyTuple_GET_ITEM(optional_fields, i);
+      std::string field_name = PyUnicode_AsUTF8(py_field_name);
+
+      DCField *field = _this->get_field_by_name(field_name);
+      if (field == nullptr) {
+        std::ostringstream strm;
+        strm << "No field named " << field_name << " in class " << _this->get_name()
+             << "\n";
+        nassert_raise(strm.str());
+        Py_DECREF(optional_fields);
+        return Datagram();
+      }
+      packer.raw_pack_uint16(field->get_number());
+      packer.begin_pack(field);
+      if (!pack_required_field(packer, distobj, field)) {
+        Py_DECREF(optional_fields);
+        return Datagram();
+      }
+      packer.end_pack();
     }
-    packer.end_pack();
+    Py_DECREF(optional_fields);
+  } else {
+    packer.raw_pack_uint16(0);
   }
 
   return Datagram(packer.get_data(), packer.get_length());
@@ -602,13 +606,13 @@ ai_format_generate(PyObject *distobj, DOID_TYPE do_id,
 
   // Also specify the optional fields.
   if (has_optional_fields) {
-    int num_optional_fields = PySequence_Size(optional_fields);
+    optional_fields = PySequence_Tuple(optional_fields);
+    Py_ssize_t num_optional_fields = PyTuple_GET_SIZE(optional_fields);
     packer.raw_pack_uint16(num_optional_fields);
 
-    for (int i = 0; i < num_optional_fields; ++i) {
-      PyObject *py_field_name = PySequence_GetItem(optional_fields, i);
+    for (Py_ssize_t i = 0; i < num_optional_fields; ++i) {
+      PyObject *py_field_name = PyTuple_GET_ITEM(optional_fields, i);
       std::string field_name = PyUnicode_AsUTF8(py_field_name);
-      Py_XDECREF(py_field_name);
 
       DCField *field = _this->get_field_by_name(field_name);
       if (field == nullptr) {
@@ -616,6 +620,7 @@ ai_format_generate(PyObject *distobj, DOID_TYPE do_id,
         strm << "No field named " << field_name << " in class "
              << _this->get_name() << "\n";
         nassert_raise(strm.str());
+        Py_DECREF(optional_fields);
         return Datagram();
       }
 
@@ -623,10 +628,13 @@ ai_format_generate(PyObject *distobj, DOID_TYPE do_id,
 
       packer.begin_pack(field);
       if (!pack_required_field(packer, distobj, field)) {
+        Py_DECREF(optional_fields);
         return Datagram();
       }
       packer.end_pack();
     }
+
+    Py_DECREF(optional_fields);
   }
 
   return Datagram(packer.get_data(), packer.get_length());

+ 2 - 0
direct/src/dcparser/dcPacker_ext.cxx

@@ -136,6 +136,7 @@ pack_object(PyObject *object) {
       // The supplied object is not an instance of the expected class object,
       // but it is a sequence.  This is case (2).
       _this->push();
+      Py_BEGIN_CRITICAL_SECTION(object);
       int size = PySequence_Size(object);
       for (int i = 0; i < size; ++i) {
         PyObject *element = PySequence_GetItem(object, i);
@@ -146,6 +147,7 @@ pack_object(PyObject *object) {
           std::cerr << "Unable to extract item " << i << " from sequence.\n";
         }
       }
+      Py_END_CRITICAL_SECTION();
       _this->pop();
     } else {
       // The supplied object is not a sequence, and we weren't expecting a

+ 31 - 30
direct/src/distributed/ConnectionRepository.py

@@ -174,35 +174,6 @@ class ConnectionRepository(
         return retVal
 
     def generateGlobalObject(self, doId, dcname, values=None):
-        def applyFieldValues(distObj, dclass, values):
-            for i in range(dclass.getNumInheritedFields()):
-                field = dclass.getInheritedField(i)
-                if field.asMolecularField() is None:
-                    value = values.get(field.getName(), None)
-                    if value is None and field.isRequired():
-                        # Gee, this could be better.  What would really be
-                        # nicer is to get value from field.getDefaultValue
-                        # or similar, but that returns a binary string, not
-                        # a python tuple, like the following does.  If you
-                        # want to change something better, please go ahead.
-                        packer = DCPacker()
-                        packer.beginPack(field)
-                        packer.packDefaultValue()
-                        packer.endPack()
-
-                        unpacker = DCPacker()
-                        unpacker.setUnpackData(packer.getString())
-                        unpacker.beginUnpack(field)
-                        value = unpacker.unpackObject()
-                        unpacker.endUnpack()
-                    if value is not None:
-                        function = getattr(distObj, field.getName())
-                        if function is not None:
-                            function(*value)
-                        else:
-                            self.notify.error("\n\n\nNot able to find %s.%s"%(
-                                distObj.__class__.__name__, field.getName()))
-
         # Look up the dclass
         dclass = self.dclassesByName.get(dcname+self.dcSuffix)
         if dclass is None:
@@ -229,7 +200,37 @@ class ConnectionRepository(
         distObj.generateInit()  # Only called when constructed
         distObj.generate()
         if values is not None:
-            applyFieldValues(distObj, dclass, values)
+            for i in range(dclass.getNumInheritedFields()):
+                field = dclass.getInheritedField(i)
+                if field.asMolecularField() is None:
+                    value = values.get(field.getName(), None)
+                    if value is None and field.isRequired():
+                        # Gee, this could be better.  What would really be
+                        # nicer is to get value from field.getDefaultValue
+                        # or similar, but that returns a binary string, not
+                        # a python tuple, like the following does.  If you
+                        # want to change something better, please go ahead.
+                        packer = DCPacker()
+                        packer.beginPack(field)
+                        packer.packDefaultValue()
+                        packer.endPack()
+
+                        unpacker = DCPacker()
+                        unpacker.setUnpackData(packer.getString())
+                        unpacker.beginUnpack(field)
+                        value = unpacker.unpackObject()
+                        unpacker.endUnpack()
+                    if value is not None:
+                        function = getattr(distObj, field.getName())
+                        if function is not None:
+                            function(*value)
+                        else:
+                            self.notify.error(
+                                "\n\n\nNot able to find %s.%s" % (
+                                    distObj.__class__.__name__,
+                                    field.getName()
+                                )
+                            )
         distObj.announceGenerate()
         distObj.parentId = 0
         distObj.zoneId = 0

+ 23 - 29
direct/src/distributed/cConnectionRepository.cxx

@@ -688,11 +688,12 @@ handle_update_field() {
     nassertr(doId2do != nullptr, false);
 
     PyObject *doId = PyLong_FromUnsignedLong(do_id);
-    PyObject *distobj = PyDict_GetItem(doId2do, doId);
+    PyObject *distobj;
+    int result = PyDict_GetItemRef(doId2do, doId, &distobj);
     Py_DECREF(doId);
     Py_DECREF(doId2do);
 
-    if (distobj != nullptr) {
+    if (result > 0) {
       PyObject *dclass_obj = PyObject_GetAttrString(distobj, "dclass");
       nassertr(dclass_obj != nullptr, false);
 
@@ -711,9 +712,11 @@ handle_update_field() {
         nassertr(neverDisable != nullptr, false);
 
         unsigned int cNeverDisable = PyLong_AsLong(neverDisable);
+        Py_DECREF(neverDisable);
         if (!cNeverDisable) {
           // in quiet zone and distobj is disable-able drop update on the
           // floor
+          Py_DECREF(distobj);
 #if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
           PyGILState_Release(gstate);
 #endif
@@ -721,13 +724,8 @@ handle_update_field() {
         }
       }
 
-      // It's a good idea to ensure the reference count to distobj is raised
-      // while we call the update method--otherwise, the update method might
-      // get into trouble if it tried to delete the object from the doId2do
-      // map.
-      PyObject *distobj_ref = Py_NewRef(distobj);
-      invoke_extension(dclass).receive_update(distobj_ref, _di);
-      Py_DECREF(distobj_ref);
+      invoke_extension(dclass).receive_update(distobj, _di);
+      Py_DECREF(distobj);
 
       if (PyErr_Occurred()) {
 #if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
@@ -777,10 +775,11 @@ handle_update_field_owner() {
     PyObject *doId = PyLong_FromUnsignedLong(do_id);
 
     // pass the update to the owner view first
-    PyObject *distobjOV = PyDict_GetItem(doId2ownerView, doId);
+    PyObject *distobjOV;
+    int result = PyDict_GetItemRef(doId2ownerView, doId, &distobjOV);
     Py_DECREF(doId2ownerView);
 
-    if (distobjOV != nullptr) {
+    if (result > 0) {
       PyObject *dclass_obj = PyObject_GetAttrString(distobjOV, "dclass");
       nassertr(dclass_obj != nullptr, false);
 
@@ -798,32 +797,29 @@ handle_update_field_owner() {
       int field_id = packer.raw_unpack_uint16();
       DCField *field = dclass->get_field_by_index(field_id);
       if (field->is_ownrecv()) {
-        // It's a good idea to ensure the reference count to distobjOV is
-        // raised while we call the update method--otherwise, the update
-        // method might get into trouble if it tried to delete the object from
-        // the doId2do map.
-        PyObject *distobjOV_ref = Py_NewRef(distobjOV);
         // make a copy of the datagram iterator so that we can use the main
         // iterator for the non-owner update
         DatagramIterator _odi(_di);
-        invoke_extension(dclass).receive_update(distobjOV_ref, _odi);
-        Py_DECREF(distobjOV_ref);
+        invoke_extension(dclass).receive_update(distobjOV, _odi);
 
         if (PyErr_Occurred()) {
+          Py_DECREF(distobjOV);
 #if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
           PyGILState_Release(gstate);
 #endif
           return false;
         }
       }
+      Py_DECREF(distobjOV);
     }
 
     // now pass the update to the visible view
-    PyObject *distobj = PyDict_GetItem(doId2do, doId);
+    PyObject *distobj;
+    result = PyDict_GetItemRef(doId2do, doId, &distobj);
     Py_DECREF(doId);
     Py_DECREF(doId2do);
 
-    if (distobj != nullptr) {
+    if (result > 0) {
       PyObject *dclass_obj = PyObject_GetAttrString(distobj, "dclass");
       nassertr(dclass_obj != nullptr, false);
 
@@ -842,21 +838,17 @@ handle_update_field_owner() {
       //int field_id = packer.raw_unpack_uint16();
       //DCField *field = dclass->get_field_by_index(field_id);
       if (true) {//field->is_broadcast()) {
-        // It's a good idea to ensure the reference count to distobj is raised
-        // while we call the update method--otherwise, the update method might
-        // get into trouble if it tried to delete the object from the doId2do
-        // map.
-        PyObject *distobj_ref = Py_NewRef(distobj);
-        invoke_extension(dclass).receive_update(distobj_ref, _di);
-        Py_DECREF(distobj_ref);
+        invoke_extension(dclass).receive_update(distobj, _di);
 
         if (PyErr_Occurred()) {
+          Py_DECREF(distobj);
 #if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
           PyGILState_Release(gstate);
 #endif
           return false;
         }
       }
+      Py_DECREF(distobj);
     }
   }
 
@@ -956,12 +948,14 @@ describe_message(std::ostream &out, const string &prefix,
       nassertv(doId2do != nullptr);
 
       PyObject *doId = PyLong_FromUnsignedLong(do_id);
-      PyObject *distobj = PyDict_GetItem(doId2do, doId);
+      PyObject *distobj;
+      int result = PyDict_GetItemRef(doId2do, doId, &distobj);
       Py_DECREF(doId);
       Py_DECREF(doId2do);
 
-      if (distobj != nullptr) {
+      if (result > 0) {
         PyObject *dclass_obj = PyObject_GetAttrString(distobj, "dclass");
+        Py_DECREF(distobj);
         nassertv(dclass_obj != nullptr);
 
         PyObject *dclass_this = PyObject_GetAttrString(dclass_obj, "this");

+ 0 - 4
direct/src/showbase/Transitions.py

@@ -327,8 +327,6 @@ class Transitions:
         else:
             self.transitionIval = self.getIrisInIval(t, finishIval, blendType)
             self.__transitionFuture = AsyncFuture()
-            if finishIval:
-                self.transitionIval.append(finishIval)
             self.transitionIval.start()
             return self.__transitionFuture
 
@@ -349,8 +347,6 @@ class Transitions:
         else:
             self.transitionIval = self.getIrisOutIval(t, finishIval, blendType)
             self.__transitionFuture = AsyncFuture()
-            if finishIval:
-                self.transitionIval.append(finishIval)
             self.transitionIval.start()
             return self.__transitionFuture
 

+ 1 - 1
dtool/Config.cmake

@@ -265,7 +265,7 @@ if(BUILD_INTERROGATE)
     panda3d-interrogate
 
     GIT_REPOSITORY https://github.com/panda3d/interrogate.git
-    GIT_TAG e297384b30f1eae8ab839ec096fc3bb6bdb3edeb
+    GIT_TAG c343350a6e210029cfe3fd8468e530e1ea6bcead
 
     PREFIX ${_interrogate_dir}
     CMAKE_ARGS

+ 19 - 12
dtool/src/dtoolbase/typeHandle_ext.cxx

@@ -49,19 +49,26 @@ __reduce__() const {
   PyTypeObject *py_type = _this->get_python_type();
   if (py_type != nullptr && py_type->tp_dict != nullptr) {
     // Look for a get_class_type method, if it returns this handle.
-    PyObject *func = PyDict_GetItemString(py_type->tp_dict, "get_class_type");
-    if (func != nullptr && PyCallable_Check(func)) {
-      PyObject *result = PyObject_CallNoArgs(func);
-      TypeHandle *result_handle = nullptr;
-      if (result == nullptr) {
-        // Never mind.
-        PyErr_Clear();
-      }
-      else if (DtoolInstance_GetPointer(result, result_handle, Dtool_TypeHandle) &&
-               *result_handle == *_this) {
-        // It returned the correct result, so we can use this.
-        return Py_BuildValue("O()", func);
+    PyObject *func;
+    int result = PyDict_GetItemStringRef(py_type->tp_dict, "get_class_type", &func);
+    if (result > 0) {
+      if (PyCallable_Check(func)) {
+        PyObject *result = PyObject_CallNoArgs(func);
+        TypeHandle *result_handle = nullptr;
+        if (result == nullptr) {
+          // Never mind.
+          PyErr_Clear();
+        }
+        else if (DtoolInstance_GetPointer(result, result_handle, Dtool_TypeHandle) &&
+                 *result_handle == *_this) {
+          // It returned the correct result, so we can use this.
+          return Py_BuildValue("N()", func);
+        }
       }
+      Py_DECREF(func);
+    }
+    else if (result < 0) {
+      PyErr_Clear();
     }
   }
 

+ 1 - 1
dtool/src/dtoolutil/pfstreamBuf.cxx

@@ -185,7 +185,7 @@ is_open() const {
  */
 bool PipeStreamBuf::
 eof_pipe() const {
-  return (_pipe == nullptr) && feof(_pipe);
+  return (_pipe == nullptr) || feof(_pipe);
 }
 
 /**

+ 16 - 0
dtool/src/interrogatedb/dtool_super_base.cxx

@@ -61,8 +61,18 @@ static void Dtool_FreeInstance_DTOOL_SUPER_BASE(PyObject *self) {
  */
 Dtool_PyTypedObject *Dtool_GetSuperBase() {
   Dtool_TypeMap *type_map = Dtool_GetGlobalTypeMap();
+
+  // If we don't have the GIL, we have to protect this with a lock to make
+  // sure that there is only one DTOOL_SUPER_BASE instance in the world.
+#ifdef Py_GIL_DISABLED
+  PyMutex_Lock(&type_map->_lock);
+#endif
+
   auto it = type_map->find("DTOOL_SUPER_BASE");
   if (it != type_map->end()) {
+#ifdef Py_GIL_DISABLED
+    PyMutex_Unlock(&type_map->_lock);
+#endif
     return it->second;
   }
 
@@ -148,6 +158,9 @@ Dtool_PyTypedObject *Dtool_GetSuperBase() {
 
   if (PyType_Ready((PyTypeObject *)&super_base_type) < 0) {
     PyErr_SetString(PyExc_TypeError, "PyType_Ready(Dtool_DTOOL_SUPER_BASE)");
+#ifdef Py_GIL_DISABLED
+    PyMutex_Unlock(&type_map->_lock);
+#endif
     return nullptr;
   }
   Py_INCREF(&super_base_type._PyType);
@@ -155,6 +168,9 @@ Dtool_PyTypedObject *Dtool_GetSuperBase() {
   PyDict_SetItemString(super_base_type._PyType.tp_dict, "DtoolGetSuperBase", PyCFunction_New(&methods[0], (PyObject *)&super_base_type));
 
   (*type_map)["DTOOL_SUPER_BASE"] = &super_base_type;
+#ifdef Py_GIL_DISABLED
+  PyMutex_Unlock(&type_map->_lock);
+#endif
   return &super_base_type;
 }
 

+ 5 - 1
dtool/src/interrogatedb/py_compat.cxx

@@ -33,7 +33,11 @@ size_t PyLongOrInt_AsSize_t(PyObject *vv) {
   size_t bytes;
   int one = 1;
   int res = _PyLong_AsByteArray((PyLongObject *)vv, (unsigned char *)&bytes,
-                                SIZEOF_SIZE_T, (int)*(unsigned char*)&one, 0);
+                                SIZEOF_SIZE_T, (int)*(unsigned char*)&one, 0,
+#if PY_VERSION_HEX >= 0x030d0000
+                                , 1 // with_exceptions
+#endif
+                                );
 
   if (res < 0) {
     return (size_t)res;

+ 85 - 0
dtool/src/interrogatedb/py_compat.h

@@ -189,6 +189,20 @@ INLINE PyObject *_PyObject_FastCall(PyObject *func, PyObject **args, Py_ssize_t
   } while (0)
 #endif
 
+#if PY_VERSION_HEX < 0x03070000
+INLINE PyObject *PyImport_GetModule(PyObject *name) {
+  PyObject *modules = PyImport_GetModuleDict();
+  if (modules != nullptr) {
+    PyObject *module = PyDict_GetItem(modules, name);
+    if (module != nullptr) {
+      Py_INCREF(module);
+      return module;
+    }
+  }
+  return nullptr;
+}
+#endif
+
 /* Python 3.8 */
 #if PY_VERSION_HEX < 0x03080000
 INLINE PyObject *_PyLong_Rshift(PyObject *a, size_t shiftby) {
@@ -279,6 +293,77 @@ INLINE bool PyLong_IsNonNegative(PyObject *value) {
 }
 #endif
 
+/* Python 3.13 */
+
+#if PY_VERSION_HEX < 0x030D00A1
+#  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
+
 /* Other Python implementations */
 
 #endif  // HAVE_PYTHON

+ 60 - 21
dtool/src/interrogatedb/py_panda.cxx

@@ -328,9 +328,9 @@ static PyObject *Dtool_EnumType_New(PyTypeObject *subtype, PyObject *args, PyObj
   PyObject *value2member = PyDict_GetItemString(subtype->tp_dict, "_value2member_map_");
   nassertr_always(value2member != nullptr, nullptr);
 
-  PyObject *member = PyDict_GetItem(value2member, arg);
-  if (member != nullptr) {
-    return Py_NewRef(member);
+  PyObject *member;
+  if (PyDict_GetItemRef(value2member, arg, &member) > 0) {
+    return member;
   }
 
   PyObject *repr = PyObject_Repr(arg);
@@ -375,23 +375,26 @@ static PyObject *Dtool_EnumType_Repr(PyObject *self) {
  * should be a tuple of (name, value) pairs.
  */
 PyTypeObject *Dtool_EnumType_Create(const char *name, PyObject *names, const char *module) {
-  static PyObject *enum_class = nullptr;
 #if PY_VERSION_HEX >= 0x03040000
-  static PyObject *enum_meta = nullptr;
-  static PyObject *enum_create = nullptr;
-  if (enum_meta == nullptr) {
-    PyObject *enum_module = PyImport_ImportModule("enum");
-    nassertr_always(enum_module != nullptr, nullptr);
+  PyObject *enum_module = PyImport_ImportModule("enum");
+  nassertr_always(enum_module != nullptr, nullptr);
 
-    enum_class = PyObject_GetAttrString(enum_module, "Enum");
-    enum_meta = PyObject_GetAttrString(enum_module, "EnumMeta");
-    enum_create = PyObject_GetAttrString(enum_meta, "_create_");
-    nassertr(enum_meta != nullptr, nullptr);
-  }
+  PyObject *enum_meta = PyObject_GetAttrString(enum_module, "EnumMeta");
+  nassertr(enum_meta != nullptr, nullptr);
+
+  PyObject *enum_class = PyObject_GetAttrString(enum_module, "Enum");
+  Py_DECREF(enum_module);
+  nassertr(enum_class != nullptr, nullptr);
+
+  PyObject *enum_create = PyObject_GetAttrString(enum_meta, "_create_");
+  Py_DECREF(enum_meta);
 
   PyObject *result = PyObject_CallFunction(enum_create, (char *)"OsN", enum_class, name, names);
+  Py_DECREF(enum_create);
+  Py_DECREF(enum_class);
   nassertr(result != nullptr, nullptr);
 #else
+  static PyObject *enum_class = nullptr;
   static PyObject *name_str;
   static PyObject *name_sunder_str;
   static PyObject *value_str;
@@ -528,16 +531,39 @@ PyObject *DTool_CreatePyInstance(void *local_this, Dtool_PyTypedObject &in_class
  * Returns a borrowed reference to the global type dictionary.
  */
 Dtool_TypeMap *Dtool_GetGlobalTypeMap() {
+#if PY_VERSION_HEX >= 0x030d0000 // 3.13
+  PyObject *istate_dict = PyInterpreterState_GetDict(PyInterpreterState_Get());
+  PyObject *key = PyUnicode_InternFromString("_interrogate_types");
+  PyObject *capsule = PyDict_GetItem(istate_dict, key);
+  if (capsule != nullptr) {
+    Py_DECREF(key);
+    return (Dtool_TypeMap *)PyCapsule_GetPointer(capsule, nullptr);
+  }
+#else
   PyObject *capsule = PySys_GetObject((char *)"_interrogate_types");
   if (capsule != nullptr) {
     return (Dtool_TypeMap *)PyCapsule_GetPointer(capsule, nullptr);
-  } else {
-    Dtool_TypeMap *type_map = new Dtool_TypeMap;
-    capsule = PyCapsule_New((void *)type_map, nullptr, nullptr);
-    PySys_SetObject((char *)"_interrogate_types", capsule);
+  }
+#endif
+
+  Dtool_TypeMap *type_map = new Dtool_TypeMap;
+  capsule = PyCapsule_New((void *)type_map, nullptr, nullptr);
+
+#if PY_VERSION_HEX >= 0x030d0000 // 3.13
+  PyObject *result;
+  if (PyDict_SetDefaultRef(istate_dict, key, capsule, &result) != 0) {
+    // Another thread already beat us to it.
     Py_DECREF(capsule);
-    return type_map;
+    delete type_map;
+    capsule = result;
+    type_map = (Dtool_TypeMap *)PyCapsule_GetPointer(capsule, nullptr);
   }
+  Py_DECREF(key);
+#endif
+
+  PySys_SetObject((char *)"_interrogate_types", capsule);
+  Py_DECREF(capsule);
+  return type_map;
 }
 
 /**
@@ -594,6 +620,10 @@ PyObject *Dtool_PyModuleInitHelper(const LibraryDef *defs[], const char *modulen
 
   Dtool_TypeMap *type_map = Dtool_GetGlobalTypeMap();
 
+#ifdef Py_GIL_DISABLED
+  PyMutex_Lock(&type_map->_lock);
+#endif
+
   // the module level function inits....
   MethodDefmap functions;
   for (size_t i = 0; defs[i] != nullptr; i++) {
@@ -627,12 +657,19 @@ PyObject *Dtool_PyModuleInitHelper(const LibraryDef *defs[], const char *modulen
         if (it != type_map->end()) {
           types->type = it->second;
         } else {
-          return PyErr_Format(PyExc_NameError, "name '%s' is not defined", types->name);
+          PyErr_Format(PyExc_NameError, "name '%s' is not defined", types->name);
+#ifdef Py_GIL_DISABLED
+          PyMutex_Unlock(&type_map->_lock);
+#endif
+          return nullptr;
         }
         ++types;
       }
     }
   }
+#ifdef Py_GIL_DISABLED
+  PyMutex_Unlock(&type_map->_lock);
+#endif
 
   PyMethodDef *newdef = new PyMethodDef[functions.size() + 1];
   MethodDefmap::iterator mi;
@@ -886,7 +923,9 @@ bool Dtool_ExtractOptionalArg(PyObject **result, PyObject *args, PyObject *kwds,
       }
 
       // We got the item, we just need to make sure that it had the right key.
-#if PY_VERSION_HEX >= 0x03060000
+#if PY_VERSION_HEX >= 0x030d0000
+      return PyUnicode_CheckExact(key) && PyUnicode_EqualToUTF8(key, keyword);
+#elif PY_VERSION_HEX >= 0x03060000
       return PyUnicode_CheckExact(key) && _PyUnicode_EqualToASCIIString(key, keyword);
 #elif PY_MAJOR_VERSION >= 3
       return PyUnicode_CheckExact(key) && PyUnicode_CompareWithASCIIString(key, keyword) == 0;

+ 7 - 0
dtool/src/interrogatedb/py_panda.h

@@ -181,7 +181,14 @@ static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\
 // forward declared of typed object.  We rely on the fact that typed objects
 // are uniquly defined by an integer.
 
+#if PY_VERSION_HEX >= 0x030d0000
+class Dtool_TypeMap : public std::map<std::string, Dtool_PyTypedObject *> {
+public:
+  PyMutex _lock { 0 };
+};
+#else
 typedef std::map<std::string, Dtool_PyTypedObject *> Dtool_TypeMap;
+#endif
 
 EXPCL_PYPANDA Dtool_TypeMap *Dtool_GetGlobalTypeMap();
 

+ 29 - 16
dtool/src/interrogatedb/py_wrappers.cxx

@@ -17,25 +17,33 @@
 #endif
 
 static void _register_collection(PyTypeObject *type, const char *abc) {
-  PyObject *sys_modules = PyImport_GetModuleDict();
-  if (sys_modules != nullptr) {
-    PyObject *module = PyDict_GetItemString(sys_modules, _COLLECTIONS_ABC);
-    if (module != nullptr) {
-      PyObject *dict = PyModule_GetDict(module);
-      if (module != nullptr) {
 #if PY_MAJOR_VERSION >= 3
-        static PyObject *register_str = PyUnicode_InternFromString("register");
+  PyObject *module_name = PyUnicode_InternFromString(_COLLECTIONS_ABC);
 #else
-        static PyObject *register_str = PyString_InternFromString("register");
+  PyObject *module_name = PyString_InternFromString(_COLLECTIONS_ABC);
 #endif
-        PyObject *sequence = PyDict_GetItemString(dict, abc);
-        if (sequence != nullptr) {
-          if (PyObject_CallMethodOneArg(sequence, register_str, (PyObject *)type) == nullptr) {
-            PyErr_Print();
-          }
-        }
+  PyObject *module = PyImport_GetModule(module_name);
+  Py_DECREF(module_name);
+  if (module != nullptr) {
+    PyObject *dict = PyModule_GetDict(module);
+    if (dict != nullptr) {
+#if PY_MAJOR_VERSION >= 3
+      PyObject *register_str = PyUnicode_InternFromString("register");
+#else
+      PyObject *register_str = PyString_InternFromString("register");
+#endif
+      PyObject *obj = nullptr;
+      if (register_str == nullptr ||
+          PyDict_GetItemStringRef(dict, abc, &obj) <= 0 ||
+          PyObject_CallMethodOneArg(obj, register_str, (PyObject *)type) == nullptr) {
+        PyErr_Print();
       }
+      Py_XDECREF(obj);
+      Py_XDECREF(register_str);
+    } else {
+      PyErr_Clear();
     }
+    Py_DECREF(module);
   }
 }
 
@@ -1075,14 +1083,19 @@ static PyObject *Dtool_MutableMappingWrapper_update(PyObject *self, PyObject *ar
     return PyErr_Format(PyExc_TypeError, "%s.update() takes either a dict argument or keyword arguments", wrap->_base._name);
   }
 
+  PyObject *result = Py_None;
   PyObject *key, *value;
   Py_ssize_t pos = 0;
+  Py_BEGIN_CRITICAL_SECTION(dict);
   while (PyDict_Next(dict, &pos, &key, &value)) {
     if (wrap->_setitem_func(wrap->_base._self, key, value) != 0) {
-      return nullptr;
+      result = nullptr;
+      break;
     }
   }
-  return Py_NewRef(Py_None);
+  Py_END_CRITICAL_SECTION();
+
+  return Py_XNewRef(result);
 }
 
 /**

+ 2 - 2
makepanda/makepandacore.py

@@ -592,8 +592,8 @@ def GetInterrogateDir():
             return INTERROGATE_DIR
 
         dir = os.path.join(GetOutputDir(), "tmp", "interrogate")
-        if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.1.0.dist-info")):
-            oscmd("\"%s\" -m pip install --force-reinstall -t \"%s\" panda3d-interrogate==0.1.0" % (sys.executable, dir))
+        if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.2.0.dist-info")):
+            oscmd("\"%s\" -m pip install --force-reinstall -t \"%s\" panda3d-interrogate==0.2.0" % (sys.executable, dir))
 
         INTERROGATE_DIR = dir
 

+ 1 - 1
panda/src/cocoadisplay/cocoaGraphicsPipe.mm

@@ -177,7 +177,7 @@ load_display_information() {
 
   // Get processor information
   const NXArchInfo *ainfo = NXGetLocalArchInfo();
-  _display_information->_cpu_brand_string = strdup(ainfo->description);
+  _display_information->_cpu_brand_string.assign(ainfo->description);
 
   // Get version of Mac OS X
   SInt32 major, minor, bugfix;

+ 25 - 20
panda/src/cocoadisplay/cocoaGraphicsWindow.mm

@@ -32,7 +32,6 @@
 #import "cocoaPandaAppDelegate.h"
 
 #import <ApplicationServices/ApplicationServices.h>
-#import <Foundation/NSAutoreleasePool.h>
 #import <AppKit/NSApplication.h>
 #import <AppKit/NSCursor.h>
 #import <AppKit/NSEvent.h>
@@ -47,6 +46,12 @@ TypeHandle CocoaGraphicsWindow::_type_handle;
 #define NSAppKitVersionNumber10_14 1671
 #endif
 
+#if __MAC_OS_X_VERSION_MAX_ALLOWED < 1070
+enum {
+  NSFullScreenWindowMask = 1 << 14
+};
+#endif
+
 /**
  *
  */
@@ -177,7 +182,6 @@ void CocoaGraphicsWindow::
 process_events() {
   GraphicsWindow::process_events();
 
-  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   NSEvent *event = nil;
 
   while (true) {
@@ -212,8 +216,6 @@ process_events() {
     [_window update];
   }
 
-  [pool release];
-
   if (_context_needs_update && _gsg != nullptr) {
     update_context();
   }
@@ -728,13 +730,12 @@ set_properties_now(WindowProperties &properties) {
       // here.
       if (!properties.has_undecorated() && !_properties.get_undecorated() &&
           [_window respondsToSelector:@selector(setStyleMask:)]) {
-        if (properties.get_fixed_size()) {
-          [_window setStyleMask:NSTitledWindowMask | NSClosableWindowMask |
-                                NSMiniaturizableWindowMask ];
-        } else {
-          [_window setStyleMask:NSTitledWindowMask | NSClosableWindowMask |
-                                NSMiniaturizableWindowMask | NSResizableWindowMask ];
+        NSUInteger style = ([_window styleMask] & NSFullScreenWindowMask);
+        style |= NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask;
+        if (!properties.get_fixed_size()) {
+          style |= NSResizableWindowMask;
         }
+        [_window setStyleMask:style];
         [_window makeFirstResponder:_view];
         // Resize event fired by makeFirstResponder has an invalid backing scale factor
         // The actual size must be reset afterward
@@ -749,16 +750,14 @@ set_properties_now(WindowProperties &properties) {
     _properties.set_undecorated(properties.get_undecorated());
 
     if (!_properties.get_fullscreen()) {
-      if (properties.get_undecorated()) {
-        [_window setStyleMask: NSBorderlessWindowMask];
-      } else if (_properties.get_fixed_size()) {
-        // Fixed size windows should not show the resize button.
-        [_window setStyleMask: NSTitledWindowMask | NSClosableWindowMask |
-                               NSMiniaturizableWindowMask ];
-      } else {
-        [_window setStyleMask: NSTitledWindowMask | NSClosableWindowMask |
-                               NSMiniaturizableWindowMask | NSResizableWindowMask ];
+      NSUInteger style = ([_window styleMask] & NSFullScreenWindowMask);
+      if (!properties.get_undecorated()) {
+        style |= NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask;
+        if (!properties.get_fixed_size()) {
+          style |= NSResizableWindowMask;
+        }
       }
+      [_window setStyleMask:style];
       [_window makeFirstResponder:_view];
       // Resize event fired by makeFirstResponder has an invalid backing scale factor
       // The actual size must be reset afterward
@@ -1176,10 +1175,16 @@ do_switch_fullscreen(CGDisplayModeRef mode) {
     }
 
     if (_window != nil) {
+      // Exit macOS' own fullscreen mode, since our own fullscreen mode
+      // doesn't work properly with it.
+      if ([_window styleMask] & NSFullScreenWindowMask) {
+        [_window toggleFullScreen:nil];
+      }
+
       // For some reason, setting the style mask makes it give up its
       // first-responder status.
       if ([_window respondsToSelector:@selector(setStyleMask:)]) {
-        [_window setStyleMask:NSBorderlessWindowMask];
+        [_window setStyleMask:([_window styleMask] & NSFullScreenWindowMask)];
       }
       [_window makeFirstResponder:_view];
       [_window setLevel:CGShieldingWindowLevel()];

+ 5 - 5
panda/src/cocoadisplay/cocoaPandaApp.mm

@@ -16,11 +16,11 @@
 
 @implementation CocoaPandaApp
 - (void) sendEvent: (NSEvent *) event {
-  // This is a hack that allows us to receive cmd-key-up events correctly.
-  // Also prevent it from eating the inserthelp key.
-  if (([event type] == NSKeyUp && ([event modifierFlags] & NSCommandKeyMask))
-    ||([event type] == NSKeyDown && [event keyCode] == 0x72)) {
-
+  // This is a hack that allows us to receive cmd-key-up events correctly, as
+  // well as key-up events during a full-screen transition.
+  // Also prevent it from eating the insert/help key.
+  if ([event type] == NSKeyUp ||
+      ([event type] == NSKeyDown && [event keyCode] == 0x72)) {
     [[self keyWindow] sendEvent: event];
   } else {
     [super sendEvent: event];

+ 7 - 3
panda/src/collide/collisionPolygon_ext.cxx

@@ -72,6 +72,9 @@ convert_points(pvector<LPoint3> &vec, PyObject *points) {
     return false;
   }
 
+  bool success = true;
+
+  Py_BEGIN_CRITICAL_SECTION(seq);
   PyObject **items = PySequence_Fast_ITEMS(seq);
   Py_ssize_t len = PySequence_Fast_GET_SIZE(seq);
   void *ptr;
@@ -90,13 +93,14 @@ convert_points(pvector<LPoint3> &vec, PyObject *points) {
     }
     else {
       Dtool_Raise_TypeError("Argument must be of LPoint3 type.");
-      Py_DECREF(seq);
-      return false;
+      success = false;
+      break;
     }
   }
 
+  Py_END_CRITICAL_SECTION();
   Py_DECREF(seq);
-  return true;
+  return success;
 }
 
 #endif

+ 1 - 1
panda/src/display/frameBufferProperties.cxx

@@ -307,7 +307,7 @@ get_buffer_mask() const {
 
   //XXX rdb: some buffers only have a front buffer, some only a back buffer
   //if (_property[FBP_back_buffers] > 0) {
-    mask = RenderBuffer::T_front | RenderBuffer::T_back;
+     mask = RenderBuffer::T_front | RenderBuffer::T_back;
   //} else {
   //  mask = RenderBuffer::T_front;
   //}

+ 4 - 2
panda/src/display/frameBufferProperties_ext.cxx

@@ -52,13 +52,14 @@ __setstate__(PyObject *self, PyObject *props) {
   PyObject *key, *value;
   Py_ssize_t pos = 0;
 
+  Py_BEGIN_CRITICAL_SECTION(props);
   while (PyDict_Next(props, &pos, &key, &value)) {
     // Look for a writable property on the type by this name.
     PyObject *descr = _PyType_Lookup(type, key);
 
     if (descr != nullptr && Py_TYPE(descr)->tp_descr_set != nullptr) {
       if (Py_TYPE(descr)->tp_descr_set(descr, self, value) < 0) {
-        return;
+        break;
       }
     } else {
       PyObject *key_repr = PyObject_Repr(key);
@@ -67,9 +68,10 @@ __setstate__(PyObject *self, PyObject *props) {
                    PyUnicode_AsUTF8(key_repr)
                   );
       Py_DECREF(key_repr);
-      return;
+      break;
     }
   }
+  Py_END_CRITICAL_SECTION();
 }
 
 #endif  // HAVE_PYTHON

+ 24 - 0
panda/src/display/graphicsEngine.cxx

@@ -63,6 +63,13 @@
   #include <sys/time.h>
 #endif
 
+#ifdef __APPLE__
+extern "C" {
+  void *objc_autoreleasePoolPush();
+  void objc_autoreleasePoolPop(void *);
+};
+#endif
+
 using std::string;
 
 PT(GraphicsEngine) GraphicsEngine::_global_ptr;
@@ -2586,6 +2593,11 @@ void GraphicsEngine::WindowRenderer::
 do_frame(GraphicsEngine *engine, Thread *current_thread) {
   LightReMutexHolder holder(_wl_lock);
 
+#ifdef __APPLE__
+  // Enclose the entire frame in an autorelease pool.
+  void *pool = objc_autoreleasePoolPush();
+#endif
+
   if (!_cull.empty()) {
     engine->cull_to_bins(_cull, current_thread);
   }
@@ -2599,6 +2611,10 @@ do_frame(GraphicsEngine *engine, Thread *current_thread) {
     engine->process_events(_window, current_thread);
   }
 
+#ifdef __APPLE__
+  objc_autoreleasePoolPop(pool);
+#endif
+
   // If any GSG's on the list have no more outstanding pointers, clean them
   // up.  (We are in the draw thread for all of these GSG's.)
   if (any_done_gsgs()) {
@@ -2630,10 +2646,18 @@ void GraphicsEngine::WindowRenderer::
 do_windows(GraphicsEngine *engine, Thread *current_thread) {
   LightReMutexHolder holder(_wl_lock);
 
+#ifdef __APPLE__
+  void *pool = objc_autoreleasePoolPush();
+#endif
+
   engine->process_events(_window, current_thread);
 
   engine->make_contexts(_cdraw, current_thread);
   engine->make_contexts(_draw, current_thread);
+
+#ifdef __APPLE__
+  objc_autoreleasePoolPop(pool);
+#endif
 }
 
 /**

+ 4 - 2
panda/src/display/windowProperties_ext.cxx

@@ -91,13 +91,14 @@ __setstate__(PyObject *self, PyObject *props) {
   PyObject *key, *value;
   Py_ssize_t pos = 0;
 
+  Py_BEGIN_CRITICAL_SECTION(props);
   while (PyDict_Next(props, &pos, &key, &value)) {
     // Look for a writable property on the type by this name.
     PyObject *descr = _PyType_Lookup(type, key);
 
     if (descr != nullptr && Py_TYPE(descr)->tp_descr_set != nullptr) {
       if (Py_TYPE(descr)->tp_descr_set(descr, self, value) < 0) {
-        return;
+        break;
       }
     } else {
       PyObject *key_repr = PyObject_Repr(key);
@@ -106,9 +107,10 @@ __setstate__(PyObject *self, PyObject *props) {
                    PyUnicode_AsUTF8(key_repr)
                   );
       Py_DECREF(key_repr);
-      return;
+      break;
     }
   }
+  Py_END_CRITICAL_SECTION();
 }
 
 #endif  // HAVE_PYTHON

+ 5 - 3
panda/src/distort/projectionScreen.cxx

@@ -451,9 +451,11 @@ recompute_geom_node(const WorkingNodePath &np, LMatrix4 &rel_mat,
   int num_geoms = node->get_num_geoms();
   for (int i = 0; i < num_geoms; i++) {
     PT(Geom) geom = node->modify_geom(i);
-    distort_cat.debug()
-      << "  " << *node << " got geom " << geom
-      << ", cache_ref = " << geom->get_cache_ref_count() << "\n";
+    if (distort_cat.is_debug()) {
+      distort_cat.debug()
+        << "  " << *node << " got geom " << geom
+        << ", cache_ref = " << geom->get_cache_ref_count() << "\n";
+    }
     geom->test_ref_count_integrity();
     recompute_geom(geom, rel_mat);
   }

+ 2 - 4
panda/src/egg/eggNode_ext.cxx

@@ -24,13 +24,10 @@ __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);
+  PyObject *module = PyImport_GetModule(module_name);
   Py_DECREF(module_name);
   nassertr_always(module != nullptr, nullptr);
 
@@ -40,6 +37,7 @@ __reduce__() const {
   } else {
     func = PyObject_GetAttrString(module, "parse_egg_node");
   }
+  Py_DECREF(module);
   nassertr_always(func != nullptr, nullptr);
 
   // Get the egg syntax to pass to the parse_egg_node function.

+ 2 - 0
panda/src/event/asyncFuture_ext.cxx

@@ -146,6 +146,8 @@ static PyObject *gen_next_asyncfuture(PyObject *self) {
     PyObject *result = get_done_result(future);
     if (result != nullptr) {
       PyErr_SetObject(PyExc_StopIteration, result);
+      // PyErr_SetObject increased the reference count, so we no longer need our reference.
+      Py_DECREF(result);
     }
     return nullptr;
   }

+ 21 - 12
panda/src/event/pythonTask.cxx

@@ -174,9 +174,15 @@ get_args() {
       PyTuple_SET_ITEM(with_task, i, Py_NewRef(item));
     }
 
-    this->ref();
-    PyObject *self = DTool_CreatePyInstance(this, Dtool_PythonTask, true, false);
-    PyTuple_SET_ITEM(with_task, num_args, self);
+    // Check whether we have a Python wrapper.  This is not the case if the
+    // object has been created by C++ and never been exposed to Python code.
+    if (__self__ == nullptr) {
+      // A __self__ instance does not exist, let's create one now.
+      this->ref();
+      __self__ = DTool_CreatePyInstance(this, Dtool_PythonTask, true, false);
+    }
+
+    PyTuple_SET_ITEM(with_task, num_args, Py_NewRef(__self__));
     return with_task;
   }
   else {
@@ -357,11 +363,9 @@ PyObject *PythonTask::
 __getattribute__(PyObject *self, PyObject *attr) const {
   // We consult the instance dict first, since the user may have overridden a
   // method or something.
-  PyObject *item = PyDict_GetItem(__dict__, attr);
-
-  if (item != nullptr) {
-    // PyDict_GetItem returns a borrowed reference.
-    return Py_NewRef(item);
+  PyObject *item;
+  if (PyDict_GetItemRef(__dict__, attr, &item) > 0) {
+    return item;
   }
 
   return PyObject_GenericGetAttr(self, attr);
@@ -1004,11 +1008,16 @@ call_owner_method(const char *method_name) {
 void PythonTask::
 call_function(PyObject *function) {
   if (function != Py_None) {
-    this->ref();
-    PyObject *self = DTool_CreatePyInstance(this, Dtool_PythonTask, true, false);
-    PyObject *result = PyObject_CallOneArg(function, self);
+    // Check whether we have a Python wrapper.  This is not the case if the
+    // object has been created by C++ and never been exposed to Python code.
+    if (__self__ == nullptr) {
+      // A __self__ instance does not exist, let's create one now.
+      this->ref();
+      __self__ = DTool_CreatePyInstance(this, Dtool_PythonTask, true, false);
+    }
+
+    PyObject *result = PyObject_CallOneArg(function, __self__);
     Py_XDECREF(result);
-    Py_DECREF(self);
   }
 }
 

+ 4 - 2
panda/src/express/pointerToArray_ext.I

@@ -106,12 +106,13 @@ __init__(PyObject *self, PyObject *source) {
   // We need to initialize the this pointer before we can call push_back.
   DtoolInstance_INIT_PTR(self, this->_this);
 
+  Py_BEGIN_CRITICAL_SECTION(source);
   Py_ssize_t size = PySequence_Size(source);
   this->_this->reserve(size);
   for (Py_ssize_t i = 0; i < size; ++i) {
     PyObject *item = PySequence_GetItem(source, i);
     if (item == nullptr) {
-      return;
+      break;
     }
     PyObject *result = PyObject_CallFunctionObjArgs(push_back, self, item, nullptr);
     Py_DECREF(item);
@@ -121,10 +122,11 @@ __init__(PyObject *self, PyObject *source) {
       PyErr_Format(PyExc_TypeError,
                    "Element %zd in sequence passed to PointerToArray "
                    "constructor could not be added", i);
-      return;
+      break;
     }
     Py_DECREF(result);
   }
+  Py_END_CRITICAL_SECTION();
 }
 
 /**

+ 4 - 4
panda/src/gobj/textureCollection_ext.cxx

@@ -31,13 +31,14 @@ __init__(PyObject *self, PyObject *sequence) {
     return;
   }
 
+  Py_BEGIN_CRITICAL_SECTION(fast);
   Py_ssize_t size = PySequence_Fast_GET_SIZE(fast);
   _this->reserve(size);
 
   for (int i = 0; i < size; ++i) {
     PyObject *item = PySequence_Fast_GET_ITEM(fast, i);
     if (item == nullptr) {
-      return;
+      break;
     }
 
     Texture *tex;
@@ -48,13 +49,12 @@ __init__(PyObject *self, PyObject *sequence) {
       stream << "Element " << i << " in sequence passed to TextureCollection constructor is not a Texture";
       std::string str = stream.str();
       PyErr_SetString(PyExc_TypeError, str.c_str());
-      Py_DECREF(fast);
-      return;
+      break;
     } else {
       _this->add_texture(tex);
     }
   }
-
+  Py_END_CRITICAL_SECTION();
   Py_DECREF(fast);
 }
 

+ 2 - 2
panda/src/linmath/lorientation_src.I

@@ -70,7 +70,7 @@ FLOATNAME(LOrientation)(const FLOATNAME(LMatrix4) &m) {
  */
 INLINE_LINMATH FLOATNAME(LOrientation) FLOATNAME(LOrientation)::
 operator * (const FLOATNAME(LRotation) &other) const {
-  return multiply((FLOATNAME(LOrientation) &)other);
+  return multiply((const FLOATNAME(LOrientation) &)other);
 }
 
 /**
@@ -80,5 +80,5 @@ operator * (const FLOATNAME(LRotation) &other) const {
 INLINE_LINMATH FLOATNAME(LOrientation) FLOATNAME(LOrientation)::
 operator * (const FLOATNAME(LQuaternion) &other) const {
   nassert_raise("LOrientation * LQuaternion is undefined; use LOrientation * LRotation or LQuaternion * LQuaternion");
-  return multiply((FLOATNAME(LOrientation) &)other);
+  return multiply((const FLOATNAME(LOrientation) &)other);
 }

+ 4 - 4
panda/src/pgraph/nodePathCollection_ext.cxx

@@ -36,13 +36,14 @@ __init__(PyObject *self, PyObject *sequence) {
     return;
   }
 
+  Py_BEGIN_CRITICAL_SECTION(fast);
   Py_ssize_t size = PySequence_Fast_GET_SIZE(fast);
   _this->reserve(size);
 
   for (int i = 0; i < size; ++i) {
     PyObject *item = PySequence_Fast_GET_ITEM(fast, i);
     if (item == nullptr) {
-      return;
+      break;
     }
 
     NodePath *path;
@@ -52,13 +53,12 @@ __init__(PyObject *self, PyObject *sequence) {
       stream << "Element " << i << " in sequence passed to NodePathCollection constructor is not a NodePath";
       std::string str = stream.str();
       PyErr_SetString(PyExc_TypeError, str.c_str());
-      Py_DECREF(fast);
-      return;
+      break;
     } else {
       _this->add_path(*path);
     }
   }
-
+  Py_END_CRITICAL_SECTION();
   Py_DECREF(fast);
 }
 

+ 6 - 4
panda/src/pgraph/nodePath_ext.cxx

@@ -56,10 +56,10 @@ __deepcopy__(PyObject *self, PyObject *memo) const {
   extern struct Dtool_PyTypedObject Dtool_NodePath;
 
   // Borrowed reference.
-  PyObject *dupe = PyDict_GetItem(memo, self);
-  if (dupe != nullptr) {
+  PyObject *dupe;
+  if (PyDict_GetItemRef(memo, self, &dupe) > 0) {
     // Already in the memo dictionary.
-    return Py_NewRef(dupe);
+    return dupe;
   }
 
   NodePath *np_dupe;
@@ -279,19 +279,21 @@ set_shader_inputs(PyObject *args, PyObject *kwargs) {
   PyObject *key, *value;
   Py_ssize_t pos = 0;
 
+  Py_BEGIN_CRITICAL_SECTION(dict);
   while (PyDict_Next(kwargs, &pos, &key, &value)) {
     char *buffer;
     Py_ssize_t length;
     buffer = (char *)PyUnicode_AsUTF8AndSize(key, &length);
     if (buffer == nullptr) {
       Dtool_Raise_TypeError("NodePath.set_shader_inputs accepts only string keywords");
-      return;
+      break;
     }
 
     CPT_InternalName name(std::string(buffer, length));
     ShaderInput &input = attrib->_inputs[name];
     invoke_extension(&input).__init__(std::move(name), value);
   }
+  Py_END_CRITICAL_SECTION();
 
   if (!PyErr_Occurred()) {
     node->set_attrib(ShaderAttrib::return_new(attrib));

+ 11 - 10
panda/src/pgraph/pandaNode_ext.cxx

@@ -47,10 +47,10 @@ __deepcopy__(PyObject *self, PyObject *memo) const {
   extern struct Dtool_PyTypedObject Dtool_PandaNode;
 
   // Borrowed reference.
-  PyObject *dupe = PyDict_GetItem(memo, self);
-  if (dupe != nullptr) {
+  PyObject *dupe;
+  if (PyDict_GetItemRef(memo, self, &dupe) > 0) {
     // Already in the memo dictionary.
-    return Py_NewRef(dupe);
+    return dupe;
   }
 
   PT(PandaNode) node_dupe = _this->copy_subgraph();
@@ -122,12 +122,11 @@ get_python_tag(PyObject *key) const {
   }
 
   PyObject *dict = ((PythonTagDataImpl *)_this->_python_tag_data.p())->_dict;
-  PyObject *value = PyDict_GetItem(dict, key);
-  if (value == nullptr) {
-    value = Py_None;
+  PyObject *value;
+  if (PyDict_GetItemRef(dict, key, &value) == 0) {
+    value = Py_NewRef(Py_None);
   }
-  // PyDict_GetItem returns a borrowed reference.
-  return Py_NewRef(value);
+  return value;
 }
 
 /**
@@ -142,7 +141,7 @@ has_python_tag(PyObject *key) const {
   }
 
   PyObject *dict = ((PythonTagDataImpl *)_this->_python_tag_data.p())->_dict;
-  return (PyDict_GetItem(dict, key) != nullptr);
+  return PyDict_Contains(dict, key);
 }
 
 /**
@@ -157,7 +156,8 @@ clear_python_tag(PyObject *key) {
   }
 
   PyObject *dict = do_get_python_tags();
-  if (PyDict_GetItem(dict, key) != nullptr) {
+  Py_BEGIN_CRITICAL_SECTION(dict);
+  if (PyDict_Contains(dict, key)) {
     PyDict_DelItem(dict, key);
   }
 
@@ -166,6 +166,7 @@ clear_python_tag(PyObject *key) {
     // unique reference to the tags, so clear the tag object.
     _this->_python_tag_data.clear();
   }
+  Py_END_CRITICAL_SECTION();
 }
 
 /**

+ 6 - 7
panda/src/pgraph/pythonLoaderFileType.cxx

@@ -89,26 +89,25 @@ init(PyObject *loader) {
       return false;
     }
 
-    PyObject *sequence = PySequence_Fast(extensions, "extensions must be a sequence");
-    PyObject **items = PySequence_Fast_ITEMS(sequence);
-    Py_ssize_t num_items = PySequence_Fast_GET_SIZE(sequence);
+    PyObject *tuple = PySequence_Tuple(extensions);
+    Py_ssize_t num_items = PyTuple_GET_SIZE(tuple);
     Py_DECREF(extensions);
 
     if (num_items == 0) {
       PyErr_SetString(PyExc_ValueError, "extensions list may not be empty");
-      Py_DECREF(sequence);
+      Py_DECREF(tuple);
       return false;
     }
 
     bool found_extension = false;
 
     for (Py_ssize_t i = 0; i < num_items; ++i) {
-      PyObject *extension = items[i];
+      PyObject *extension = PyTuple_GET_ITEM(tuple, i);
       const char *extension_str;
       Py_ssize_t extension_len;
       extension_str = PyUnicode_AsUTF8AndSize(extension, &extension_len);
       if (extension_str == nullptr) {
-        Py_DECREF(sequence);
+        Py_DECREF(tuple);
         return false;
       }
 
@@ -127,7 +126,7 @@ init(PyObject *loader) {
         }
       }
     }
-    Py_DECREF(sequence);
+    Py_DECREF(tuple);
 
     if (!found_extension) {
       PyObject *repr = PyObject_Repr(loader);

+ 1 - 1
panda/src/pgraph/renderState_ext.cxx

@@ -56,7 +56,7 @@ make(PyObject *args, PyObject *kwds) {
 
   int override = 0;
   if (py_override != nullptr) {
-    override = _PyLong_AsInt(py_override);
+    override = PyLong_AsInt(py_override);
     if (override == -1 && PyErr_Occurred()) {
       return nullptr;
     }

+ 9 - 3
panda/src/pgraph/shaderAttrib_ext.cxx

@@ -46,22 +46,28 @@ set_shader_inputs(PyObject *args, PyObject *kwargs) const {
   PyObject *key, *value;
   Py_ssize_t pos = 0;
 
+  Py_BEGIN_CRITICAL_SECTION(kwargs);
   while (PyDict_Next(kwargs, &pos, &key, &value)) {
     char *buffer;
     Py_ssize_t length;
     buffer = (char *)PyUnicode_AsUTF8AndSize(key, &length);
     if (buffer == nullptr) {
       Dtool_Raise_TypeError("ShaderAttrib.set_shader_inputs accepts only string keywords");
-      delete attrib;
-      return nullptr;
+      break;
     }
 
     CPT_InternalName name(std::string(buffer, length));
     ShaderInput &input = attrib->_inputs[name];
     invoke_extension(&input).__init__(std::move(name), value);
   }
+  Py_END_CRITICAL_SECTION();
 
-  return ShaderAttrib::return_new(attrib);
+  if (!PyErr_Occurred()) {
+    return ShaderAttrib::return_new(attrib);
+  } else {
+    delete attrib;
+    return nullptr;
+  }
 }
 
 #endif  // HAVE_PYTHON

+ 12 - 1
panda/src/pgraph/shaderInput_ext.cxx

@@ -269,12 +269,20 @@ __init__(CPT_InternalName name, PyObject *value, int priority) {
 
   } else if (PySequence_Check(value) && !PyUnicode_CheckExact(value)) {
     // Iterate over the sequence to make sure all have the same type.
+#ifdef Py_GIL_DISABLED
+    PyObject *fast = PySequence_Tuple(value);
+#else
     PyObject *fast = PySequence_Fast(value, "unknown type passed to ShaderInput");
+#endif
     if (fast == nullptr) {
       return;
     }
 
+#ifdef Py_GIL_DISABLED
+    Py_ssize_t num_items = PyTuple_GET_SIZE(fast);
+#else
     Py_ssize_t num_items = PySequence_Fast_GET_SIZE(fast);
+#endif
     if (num_items <= 0) {
       // We can't determine the type of a list of size 0.
       _this->_type = ShaderInput::M_numeric;
@@ -289,6 +297,9 @@ __init__(CPT_InternalName name, PyObject *value, int priority) {
     for (Py_ssize_t i = 0; i < num_items; ++i) {
       PyObject *item = items[i];
 
+      //FIXME: if these items are not tuples, this is not thread-safe in the
+      // free-threaded build.  Convert everything to tuples, and push to a
+      // vector?
       if (PySequence_Check(item)) {
         Py_ssize_t itemsize = PySequence_Size(item);
         if (known_itemsize >= 0 && itemsize != known_itemsize) {
@@ -310,7 +321,7 @@ __init__(CPT_InternalName name, PyObject *value, int priority) {
             Dtool_Raise_TypeError("unknown element type in sequence passed as element of sequence passed to ShaderInput");
             Py_DECREF(subitem);
             Py_DECREF(fast);
-            break;
+            return;
           }
           Py_DECREF(subitem);
         }

+ 1 - 1
panda/src/pipeline/cycleDataLockedStageReader.I

@@ -184,7 +184,7 @@ CycleDataLockedStageReader(const CycleDataLockedStageReader<CycleDataType> &copy
 template<class CycleDataType>
 INLINE CycleDataLockedStageReader<CycleDataType>::
 CycleDataLockedStageReader(CycleDataLockedStageReader<CycleDataType> &&from) noexcept :
-  _pointer(from._cycler)
+  _pointer(from._pointer)
 {
   from._pointer = nullptr;
 }

+ 0 - 8
panda/src/pipeline/threadWin32Impl.I

@@ -63,14 +63,6 @@ is_simple_threads() {
   return false;
 }
 
-/**
- *
- */
-INLINE void ThreadWin32Impl::
-sleep(double seconds) {
-  Sleep((int)(seconds * 1000));
-}
-
 /**
  *
  */

+ 30 - 0
panda/src/pipeline/threadWin32Impl.cxx

@@ -25,6 +25,10 @@
 static thread_local Thread *_current_thread = nullptr;
 static patomic_flag _main_thread_known = ATOMIC_FLAG_INIT;
 
+#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
+#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002
+#endif
+
 #if _WIN32_WINNT < 0x0601
 // Requires Windows 7.
 static DWORD (__stdcall *EnableThreadProfiling)(HANDLE, DWORD, DWORD64, HANDLE *) = nullptr;
@@ -78,6 +82,11 @@ ThreadWin32Impl::
   }
 
   CloseHandle(_thread);
+
+  if (_timer != nullptr) {
+    CloseHandle(_timer);
+    _timer = nullptr;
+  }
 }
 
 /**
@@ -200,6 +209,27 @@ bind_thread(Thread *thread) {
   return thread;
 }
 
+
+/**
+ *
+ */
+void ThreadWin32Impl::
+sleep(double seconds) {
+  Thread *thread = get_current_thread();
+  ThreadWin32Impl *self = &thread->_impl;
+
+  HANDLE timer = self->_timer;
+  if (timer == nullptr) {
+    timer = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_MANUAL_RESET | CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
+    self->_timer = timer;
+  }
+
+  LARGE_INTEGER ft;
+  ft.QuadPart = seconds * -10000000LL;
+  SetWaitableTimer(timer, &ft, 0, nullptr, nullptr, 0);
+  WaitForSingleObject(timer, INFINITE);
+}
+
 /**
  * Returns the number of context switches that occurred on the current thread.
  * The first number is the total number of context switches reported by the OS,

+ 2 - 1
panda/src/pipeline/threadWin32Impl.h

@@ -48,7 +48,7 @@ public:
   INLINE static bool is_threading_supported();
   INLINE static bool is_true_threads();
   INLINE static bool is_simple_threads();
-  INLINE static void sleep(double seconds);
+  static void sleep(double seconds);
   INLINE static void yield();
   INLINE static void consider_yield();
 
@@ -72,6 +72,7 @@ private:
   bool _joinable;
   Status _status;
   HANDLE _profiling;
+  HANDLE _timer = nullptr;
 };
 
 #include "threadWin32Impl.I"

+ 24 - 5
panda/src/pstatclient/pStatClient_ext.cxx

@@ -72,11 +72,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 = "<unknown>";
+  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) : "<unknown>";
   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);
@@ -85,6 +95,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
@@ -179,23 +194,26 @@ make_c_function_collector(PyCFunctionObject *meth) {
       len = (dot - cls->tp_name) + 1;
     } else {
       // If there's no module name, we need to get it from __module__.
-      PyObject *py_mod_name = cls->tp_dict ? PyDict_GetItemString(cls->tp_dict, "__module__") : nullptr;
+      PyObject *py_mod_name = nullptr;
       const char *mod_name = nullptr;
-      if (py_mod_name != nullptr) {
+      if (cls->tp_dict != nullptr &&
+          PyDict_GetItemStringRef(cls->tp_dict, "__module__", &py_mod_name) > 0) {
         if (PyUnicode_Check(py_mod_name)) {
           mod_name = PyUnicode_AsUTF8(py_mod_name);
         } else {
           // Might be a descriptor.
+          Py_DECREF(py_mod_name);
           py_mod_name = PyObject_GetAttrString(meth->m_self, "__module__");
           if (py_mod_name != nullptr) {
             if (PyUnicode_Check(py_mod_name)) {
               mod_name = PyUnicode_AsUTF8(py_mod_name);
             }
-            Py_DECREF(py_mod_name);
           }
           else PyErr_Clear();
         }
       }
+      else PyErr_Clear();
+
       if (mod_name == nullptr) {
         // Is it a built-in, like int or dict?
         PyObject *builtins = PyEval_GetBuiltins();
@@ -206,6 +224,7 @@ make_c_function_collector(PyCFunctionObject *meth) {
         }
       }
       len = snprintf(buffer, sizeof(buffer), "%s:%s:%s()", mod_name, cls->tp_name, meth->m_ml->ml_name) - 2;
+      Py_XDECREF(py_mod_name);
     }
   }
   else if (meth->m_self != nullptr) {

+ 5 - 1
panda/src/putil/bitArray_ext.cxx

@@ -32,7 +32,11 @@ __init__(PyObject *init_value) {
     _PyLong_AsByteArray((PyLongObject *)init_value,
       (unsigned char *)&_this->_array[0],
       num_words * sizeof(BitArray::WordType),
-      1, 0);
+      1, 0
+#if PY_VERSION_HEX >= 0x030d0000
+      , 1 // with_exceptions
+#endif
+      );
   }
 }
 

+ 16 - 0
panda/src/putil/clockObject.cxx

@@ -499,6 +499,8 @@ wait_until(double want_time) {
 
   double wait_interval = (want_time - _actual_frame_time) - sleep_precision;
 
+  double now = get_real_time();
+
   if (wait_interval > 0.0) {
     Thread::sleep(wait_interval);
   }
@@ -507,6 +509,20 @@ wait_until(double want_time) {
   (*_start_clock_busy_wait)();
 #endif
 
+  _actual_frame_time = get_real_time();
+  if (_actual_frame_time > want_time) {
+    if (util_cat.is_debug()) {
+      util_cat.debug()
+        << "Overslept by " << (int)((_actual_frame_time - want_time) * 1000000)
+        << " us while waiting for next frame, consider raising sleep-precision.\n";
+    }
+  }
+  else if (util_cat.is_spam()) {
+    util_cat.spam()
+      << "Busy waiting for " << (int)((want_time - _actual_frame_time) * 1000000)
+      << " us.\n";
+  }
+
   // Now busy-wait until the actual time elapses.
   while (_actual_frame_time < want_time) {
     _actual_frame_time = get_real_time();

+ 3 - 3
panda/src/putil/config_putil.cxx

@@ -129,9 +129,9 @@ get_plugin_path() {
 
 ConfigVariableDouble sleep_precision
 ("sleep-precision", 0.01,
- PRC_DESC("This is the accuracy within which we can expect select() to "
-          "return precisely.  That is, if we use select() to request a "
-          "timeout of 1.0 seconds, we can expect to actually sleep for "
+ PRC_DESC("This is the accuracy within which we can expect the operating "
+          "system sleep call to return precisely.  That is, if we request "
+          "a timeout of 1.0 seconds, we can expect to actually sleep for "
           "somewhere between 1.0 and 1.0 + sleep-precision seconds."));
 
 ConfigVariableBool preload_textures

+ 11 - 1
panda/src/putil/doubleBitMask_ext.I

@@ -31,7 +31,11 @@ __init__(PyObject *init_value) {
   if (n > 0) {
     size_t num_bytes = (n + 7) / 8;
     unsigned char *bytes = (unsigned char *)alloca(num_bytes);
-    _PyLong_AsByteArray((PyLongObject *)init_value, bytes, num_bytes, 1, 0);
+    _PyLong_AsByteArray((PyLongObject *)init_value, bytes, num_bytes, 1, 0
+#if PY_VERSION_HEX >= 0x030d0000
+      , 1 // with_exceptions
+#endif
+    );
 
     for (size_t i = 0; i < num_bytes; ++i) {
       this->_this->store(bytes[i], i * 8, 8);
@@ -58,7 +62,13 @@ __int__() const {
   if (!this->_this->_hi.is_zero()) {
     PyObject *lo = result;
     PyObject *hi = invoke_extension(&this->_this->_hi).__int__();
+#if PY_VERSION_HEX >= 0x030d0000
+    PyObject *half_bits = PyLong_FromUnsignedLong(DoubleBitMask<BMType>::half_bits);
+    PyObject *shifted = PyNumber_Lshift(hi, half_bits);
+    Py_DECREF(half_bits);
+#else
     PyObject *shifted = _PyLong_Lshift(hi, DoubleBitMask<BMType>::half_bits);
+#endif
     Py_DECREF(hi);
     result = PyNumber_Or(shifted, lo);
     Py_DECREF(shifted);

+ 25 - 23
panda/src/putil/typedWritable_ext.cxx

@@ -336,35 +336,37 @@ find_global_decode(PyObject *this_class, const char *func_name) {
   // Get the module in which BamWriter is defined.
   PyObject *module_name = PyObject_GetAttrString((PyObject *)&Dtool_BamWriter, "__module__");
   if (module_name != nullptr) {
-    // borrowed reference
-    PyObject *sys_modules = PyImport_GetModuleDict();
-    if (sys_modules != nullptr) {
-      // borrowed reference
-      PyObject *module = PyDict_GetItem(sys_modules, module_name);
-      if (module != nullptr) {
-        PyObject *func = PyObject_GetAttrString(module, (char *)func_name);
-        if (func != nullptr) {
-          Py_DECREF(module_name);
-          return func;
-        }
+    PyObject *module = PyImport_GetModule(module_name);
+    Py_DECREF(module_name);
+    if (module != nullptr) {
+      PyObject *func = PyObject_GetAttrString(module, (char *)func_name);
+      Py_DECREF(module);
+      if (func != nullptr) {
+        return func;
       }
     }
-    Py_DECREF(module_name);
   }
 
   PyObject *bases = PyObject_GetAttrString(this_class, "__bases__");
   if (bases != nullptr) {
-    if (PySequence_Check(bases)) {
-      Py_ssize_t size = PySequence_Size(bases);
-      for (Py_ssize_t i = 0; i < size; ++i) {
-        PyObject *base = PySequence_GetItem(bases, i);
-        if (base != nullptr) {
-          PyObject *func = find_global_decode(base, func_name);
-          Py_DECREF(base);
-          if (func != nullptr) {
-            Py_DECREF(bases);
-            return func;
-          }
+    {
+      PyObject *tuple = PySequence_Tuple(bases);
+      Py_DECREF(bases);
+      if (tuple == nullptr) {
+        PyErr_Clear();
+        return nullptr;
+      }
+      bases = tuple;
+    }
+
+    Py_ssize_t size = PyTuple_GET_SIZE(bases);
+    for (Py_ssize_t i = 0; i < size; ++i) {
+      PyObject *base = PyTuple_GET_ITEM(bases, i);
+      if (base != nullptr) {
+        PyObject *func = find_global_decode(base, func_name);
+        if (func != nullptr) {
+          Py_DECREF(bases);
+          return func;
         }
       }
     }

+ 12 - 0
panda/src/testbed/pview.cxx

@@ -164,6 +164,17 @@ event_0(const Event *event, void *) {
   card_np.set_texture(buffer->get_texture());
 }
 
+void
+event_R(const Event *event, void *) {
+  // shift-R(eload): reload all textures
+  TextureCollection collection = TexturePool::get_global_ptr()->find_all_textures("*");
+  for (int i = 0; i < collection.size(); ++i)
+  {
+    nout << "Reloading texture " << collection[i]->get_filename() << std::endl;
+    collection[i]->reload();
+  }
+}
+
 void
 usage() {
   cerr <<
@@ -513,6 +524,7 @@ main(int argc, char **argv) {
     framework.define_key("shift-f", "flatten hierarchy", event_F, nullptr);
     framework.define_key("alt-enter", "toggle between window/fullscreen", event_Enter, nullptr);
     framework.define_key("2", "split the window", event_2, nullptr);
+    framework.define_key("shift-r", "reload textures", event_R, nullptr);
     if (pview_test_hack) {
       framework.define_key("0", "run quick hacky test", event_0, nullptr);
     }

+ 2 - 0
pandatool/src/deploy-stub/deploy-stub.c

@@ -712,7 +712,9 @@ int main(int argc, char *argv[]) {
       new_moddef->code = moddef->code;
       new_moddef->size = moddef->size < 0 ? -(moddef->size) : moddef->size;
       new_moddef->is_package = moddef->size < 0;
+#if PY_VERSION_HEX < 0x030d0000 // 3.13
       new_moddef->get_code = NULL;
+#endif
       new_moddef++;
     }
 #endif

+ 29 - 0
tests/event/test_futures.py

@@ -705,3 +705,32 @@ def test_task_manager_cleanup_non_panda_future():
     del coro_main # this should break the last strong reference to the mock future
 
     assert future_ref() is None, "MockFuture was not cleaned up!"
+
+def test_await_future_result_cleanup():
+    # Create a simple future and an object for it to return
+    future = core.AsyncFuture()
+
+    class TestObject:
+        pass
+
+    test_result = TestObject()
+    future_result_ref = weakref.ref(test_result)
+
+    # Setup an async environment and dispatch it to a task chain to await the future
+    async def coro_main():
+        nonlocal test_result
+        future.set_result(test_result)
+        del test_result
+        await future
+
+    task = core.PythonTask(coro_main(), 'coro_main')
+    task_mgr = core.AsyncTaskManager.get_global_ptr()
+    task_mgr.add(task)
+    # Poll the task_mgr so the PythonTask starts polling on future.done()
+    task_mgr.poll()
+    # Break all possible references to the future object
+    del task
+    del coro_main
+    del future # this should break the last strong reference to the future
+
+    assert future_result_ref() is None, "TestObject was not cleaned up!"

+ 37 - 1
tests/event/test_pythontask.py

@@ -1,4 +1,4 @@
-from panda3d.core import PythonTask
+from panda3d.core import AsyncTaskManager, PythonTask
 from contextlib import contextmanager
 import pytest
 import types
@@ -115,3 +115,39 @@ def test_pythontask_cycle():
                 break
         else:
             pytest.fail('not found in garbage')
+
+def test_task_persistent_wrapper():
+    task_mgr = AsyncTaskManager.get_global_ptr()
+    task_chain = task_mgr.make_task_chain("test_task_persistent_wrapper")
+
+    # Create a subclass of PythonTask
+    class PythonTaskSubclassTest(PythonTask):
+        pass
+
+    def task_main(task):
+        # Set our result to be the input task we got into this function
+        task.set_result(task)
+        # Verify we got the subclass
+        assert isinstance(task, PythonTaskSubclassTest)
+        return task.done
+
+    done_callback_reached = False
+
+    def done_callback(task):
+        # Verify we got the subclass
+        assert isinstance(task, PythonTaskSubclassTest)
+        nonlocal done_callback_reached
+        done_callback_reached = True
+
+    task = PythonTaskSubclassTest(task_main)
+    task.set_task_chain(task_chain.name)
+    task.set_upon_death(done_callback)
+    task_mgr.add(task)
+    task_chain.wait_for_tasks()
+
+    assert task.done()
+    assert not task.cancelled()
+    # Verify the task passed into the function is the same task we created
+    assert task.result() is task
+    # Verify the done callback worked and was tested
+    assert done_callback_reached

+ 6 - 0
tests/interrogate/test_property.py

@@ -609,3 +609,9 @@ def test_map_property_items():
 
     assert isinstance(prop.items(), collections_abc.MappingView)
     assert frozenset(prop.items()) == frozenset((('key', 'value'), ('key2', 'value2')))
+
+
+def test_static_property():
+    v1 = core.PandaSystem.version_string
+    v2 = core.PandaSystem.get_version_string()
+    assert v1 == v2