Fix #219: Add threading support with mcrfpy.lock() context manager

Enables background Python threads to safely modify UI objects by
synchronizing with the render loop at frame boundaries.

Implementation:
- FrameLock class provides mutex/condvar synchronization
- GIL released during window.display() allowing background threads to run
- Safe window opens between frames for synchronized UI updates
- mcrfpy.lock() context manager blocks until safe window, then executes
- Main thread detection: lock() is a no-op when called from callbacks
  or script initialization (already synchronized)

Usage:
    import threading
    import mcrfpy

    def background_worker():
        with mcrfpy.lock():  # Blocks until safe
            player.x = new_x  # Safe to modify UI

    threading.Thread(target=background_worker).start()

The lock works transparently from any context - background threads get
actual synchronization, main thread calls (callbacks, init) get no-op.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-01-19 23:37:49 -05:00
commit 257e52327b
5 changed files with 269 additions and 1 deletions

View file

@ -25,6 +25,7 @@
#include "PyHeightMap.h" // Procedural generation heightmap (#193)
#include "PyBSP.h" // Procedural generation BSP (#202-206)
#include "PyNoiseSource.h" // Procedural generation noise (#207-208)
#include "PyLock.h" // Thread synchronization (#219)
#include "McRogueFaceVersion.h"
#include "GameEngine.h"
#include "ImGuiConsole.h"
@ -292,6 +293,17 @@ static PyMethodDef mcrfpyMethods[] = {
{"__getattr__", mcrfpy_module_getattr, METH_VARARGS,
"Module-level __getattr__ for dynamic properties (current_scene, scenes)"},
// #219: Thread synchronization
{"lock", PyLock::lock, METH_NOARGS,
MCRF_FUNCTION(lock,
MCRF_SIG("()", "_LockContext"),
MCRF_DESC("Get a context manager for thread-safe UI updates from background threads."),
MCRF_RETURNS("_LockContext: A context manager that blocks until safe to modify UI")
MCRF_NOTE("Use with `with mcrfpy.lock():` to safely modify UI objects from a background thread. "
"The context manager blocks until the render loop reaches a safe point between frames. "
"Without this, modifying UI from threads may cause visual glitches or crashes.")
)},
{NULL, NULL, 0, NULL}
};
@ -479,7 +491,13 @@ PyObject* PyInit_mcrfpy()
PyUILineType.tp_weaklistoffset = offsetof(PyUILineObject, weakreflist);
PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist);
PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist);
// #219 - Initialize PyLock context manager type
if (PyLock::init() < 0) {
std::cout << "ERROR: PyLock::init() failed" << std::endl;
return NULL;
}
// Process exported types - PyType_Ready AND add to module
int i = 0;
auto t = exported_types[i];