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

@ -13,6 +13,10 @@
#include "ImGuiConsole.h"
#include <memory>
#include <sstream>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <thread>
/**
* @brief Performance profiling metrics structure
@ -79,6 +83,59 @@ struct ProfilingMetrics {
}
};
/**
* @brief Thread synchronization primitive for safe UI updates from background threads (#219)
*
* Allows background Python threads to safely update UI objects by waiting for
* a "safe window" between frames when the render loop is not iterating the scene graph.
*
* Usage from Python:
* with mcrfpy.lock():
* frame.x = new_value # Safe to modify UI here
*/
class FrameLock {
private:
std::mutex mtx;
std::condition_variable cv;
bool safe_window = false;
std::atomic<int> waiting{0};
std::atomic<int> active{0};
public:
/**
* @brief Acquire the lock, blocking until safe window opens
*
* Called by mcrfpy.lock().__enter__. Releases GIL while waiting.
*/
void acquire();
/**
* @brief Release the lock
*
* Called by mcrfpy.lock().__exit__
*/
void release();
/**
* @brief Open the safe window, allowing waiting threads to proceed
*
* Called by render loop between frames
*/
void openWindow();
/**
* @brief Close the safe window, waiting for all active threads to finish
*
* Called by render loop before resuming rendering
*/
void closeWindow();
/**
* @brief Check if any threads are waiting for the lock
*/
bool hasWaiting() const { return waiting.load() > 0; }
};
class GameEngine
{
public:
@ -136,6 +193,10 @@ private:
ImGuiConsole console;
bool imguiInitialized = false;
// #219 - Thread synchronization for background Python threads
FrameLock frameLock;
std::thread::id main_thread_id; // For detecting if lock() is called from main thread
void updateViewport();
void testTimers();
@ -198,6 +259,10 @@ public:
int getSimulationTime() const { return simulation_time; }
void renderScene(); // Force render current scene (for synchronous screenshot)
// #219 - Thread synchronization for background threads
FrameLock& getFrameLock() { return frameLock; }
bool isMainThread() const { return std::this_thread::get_id() == main_thread_id; }
// global textures for scripts to access
std::vector<IndexTexture> textures;