Isolate compiled overlay bindings per execution thread

This commit is contained in:
Shokunin
2026-10-21 16:27:49 -07:00
parent f4b6809138
commit 8d73618f79
6 changed files with 155 additions and 16 deletions
+6
View File
@@ -213,6 +213,12 @@ on a fixed region -> next.
## 5. Status / Log (update every session)
- 2026-10-21 BRIEF-5 prerequisite: compiled-overlay callback tables, pending
cycle charges and host dispatch pointers now bind per executing thread.
The production-shim two-thread test and existing store-PC/shim compilation
checks pass on Windows and macOS. This does not isolate the runtime or move
replay to a worker; Stage 3 acceptance gates have not been run.
- 2026-10-21 BRIEF-4 Stage 3 continuation: temporal GPU transactions now stay
on the render context owner. Recorded redraws carry exact PGXP model-word
provenance and homogeneous geometry, avoiding a second guest draw and the
+9
View File
@@ -1979,6 +1979,15 @@ if(BUILD_TESTING)
add_test(NAME overlay_store_pc_forwarding_test
COMMAND overlay_store_pc_forwarding_test)
find_package(Threads REQUIRED)
add_executable(overlay_thread_binding_test tests/test_overlay_thread_binding.c)
target_include_directories(overlay_thread_binding_test PRIVATE include)
target_compile_definitions(overlay_thread_binding_test PRIVATE
PSX_OVERLAY_DLL_BUILD PSX_NO_DEBUG_TOOLS PSX_ENABLE_BLOCK_CYCLES=1)
target_link_libraries(overlay_thread_binding_test PRIVATE Threads::Threads)
add_test(NAME overlay_thread_binding_test COMMAND overlay_thread_binding_test)
set_tests_properties(overlay_thread_binding_test PROPERTIES TIMEOUT 20)
# frame_fingerprint judge columns are blind to device-write service order.
add_executable(frame_fingerprint_test tests/test_frame_fingerprint.c)
target_include_directories(frame_fingerprint_test PRIVATE include)
+19 -5
View File
@@ -290,16 +290,30 @@ extern int32_t gte_nclip_exact_sign(int32_t native_mac0, uint32_t pc);
* PSX_OVERLAY_DLL_BUILD branch; see overlay_api.h / compile_overlays.py).
* ========================================================================== */
#ifdef PSX_OVERLAY_DLL_BUILD
extern int *g_psx_call_bail_p;
extern uint64_t *g_psx_bail_first_p;
extern uint64_t *g_psx_bail_resolved_p;
/* Each executing host thread binds its own overlay callback table. A shard
* can be entered concurrently by live execution and a private replay. */
#if defined(_MSC_VER)
#define PSX_OVERLAY_THREAD_LOCAL __declspec(thread)
#else
#define PSX_OVERLAY_THREAD_LOCAL __thread
#endif
extern PSX_OVERLAY_THREAD_LOCAL int *g_psx_call_bail_p;
extern PSX_OVERLAY_THREAD_LOCAL uint64_t *g_psx_bail_first_p;
extern PSX_OVERLAY_THREAD_LOCAL uint64_t *g_psx_bail_resolved_p;
#define g_psx_call_bail (*g_psx_call_bail_p)
#define g_psx_bail_first (*g_psx_bail_first_p)
#define g_psx_bail_resolved (*g_psx_bail_resolved_p)
/* The store-PC breadcrumb the emitter writes before every guest store is the
* runtime's, not the DLL's (ABI v24, overlay_api.h last_store_pc). */
extern uint32_t *g_psx_last_store_pc_p;
#define g_debug_last_store_pc (*g_psx_last_store_pc_p)
extern PSX_OVERLAY_THREAD_LOCAL uint32_t *g_psx_last_store_pc_p;
/* Generated shards also declare `extern uint32_t g_debug_last_store_pc`.
* Map that declaration to a pointer-returning function, rather than a second
* pointer declaration that would lose its thread-local storage qualifier.
* The inline accessor leaves the store path as a direct TLS pointer load. */
static inline uint32_t *psx_overlay_last_store_pc(void) {
return g_psx_last_store_pc_p;
}
#define g_debug_last_store_pc (*psx_overlay_last_store_pc())
#else
extern int g_psx_call_bail;
extern uint64_t g_psx_bail_first; /* contract violations detected */
+4 -1
View File
@@ -400,7 +400,10 @@ typedef struct OverlayCallbacks {
extern "C" {
#endif
/* Exported by every overlay DLL. Call once after LoadLibrary. */
/* Exported by every overlay DLL. Call on each executing thread before guest
* entry. The table, cycle accumulator and host-state pointers are thread-local;
* loading a shard does not bind it on other threads. Flush pending cycles
* before rebinding this thread to a different execution context. */
#ifdef _WIN32
__declspec(dllexport)
#else
+17 -10
View File
@@ -2,8 +2,8 @@
#include "ws_cull_edge.h" /* psx_ws_cull_bgez / psx_ws_clip_edge_x math */
/* ---- Overlay dispatch shim (inserted by compile_overlays.py) ----------- */
static OverlayCallbacks g_cbs;
static uint32_t s_pending_cycles;
static PSX_OVERLAY_THREAD_LOCAL OverlayCallbacks g_cbs;
static PSX_OVERLAY_THREAD_LOCAL uint32_t s_pending_cycles;
PSX_OVERLAY_EXPORT void overlay_flush_cycles(void) {
uint32_t cycles = s_pending_cycles;
@@ -16,27 +16,34 @@ PSX_OVERLAY_EXPORT void overlay_flush_cycles(void) {
* branch). They start at local dummies so a pre-init call can't fault. */
static int s_bail_dummy;
static uint64_t s_ctr_dummy0, s_ctr_dummy1;
int *g_psx_call_bail_p = &s_bail_dummy;
uint64_t *g_psx_bail_first_p = &s_ctr_dummy0;
uint64_t *g_psx_bail_resolved_p = &s_ctr_dummy1;
static PSX_OVERLAY_THREAD_LOCAL int s_bound_bail_dummy;
static PSX_OVERLAY_THREAD_LOCAL uint64_t s_bound_ctr_dummy0, s_bound_ctr_dummy1;
PSX_OVERLAY_THREAD_LOCAL int *g_psx_call_bail_p = &s_bail_dummy;
PSX_OVERLAY_THREAD_LOCAL uint64_t *g_psx_bail_first_p = &s_ctr_dummy0;
PSX_OVERLAY_THREAD_LOCAL uint64_t *g_psx_bail_resolved_p = &s_ctr_dummy1;
/* g_debug_last_store_pc (ABI v24): cpu_state.h maps the emitter's writes to
* *g_psx_last_store_pc_p. The runtime reads the value in every build (memory.c
* store filters, write attribution), so overlay_init points this at the host's
* copy; the local one only catches a store made before init or on a NULL host. */
static uint32_t s_last_store_pc_local;
uint32_t *g_psx_last_store_pc_p = &s_last_store_pc_local;
static PSX_OVERLAY_THREAD_LOCAL uint32_t s_bound_last_store_pc;
PSX_OVERLAY_THREAD_LOCAL uint32_t *g_psx_last_store_pc_p = &s_last_store_pc_local;
PSX_OVERLAY_EXPORT int overlay_abi(void) {
return PSX_OVERLAY_ABI_VERSION | (PSX_OVERLAY_FLAVOR << 16);
}
PSX_OVERLAY_EXPORT void overlay_init(const OverlayCallbacks *cbs) {
/* Binding is done on the thread that will execute the shard, before any
* guest call. Never carry a previous context's unpublished cycles into a
* new binding. The host must flush before rebinding a running context. */
s_pending_cycles = 0;
g_cbs = *cbs;
if (g_cbs.call_bail_flag) g_psx_call_bail_p = g_cbs.call_bail_flag;
if (g_cbs.bail_first) g_psx_bail_first_p = g_cbs.bail_first;
if (g_cbs.bail_resolved) g_psx_bail_resolved_p = g_cbs.bail_resolved;
g_psx_call_bail_p = g_cbs.call_bail_flag ? g_cbs.call_bail_flag : &s_bound_bail_dummy;
g_psx_bail_first_p = g_cbs.bail_first ? g_cbs.bail_first : &s_bound_ctr_dummy0;
g_psx_bail_resolved_p = g_cbs.bail_resolved ? g_cbs.bail_resolved : &s_bound_ctr_dummy1;
g_psx_last_store_pc_p = g_cbs.last_store_pc ? g_cbs.last_store_pc
: &s_last_store_pc_local;
: &s_bound_last_store_pc;
}
void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t ra) {
+100
View File
@@ -0,0 +1,100 @@
/* Concurrent calls through the production shard shim must keep the host's
* callbacks, pending cycles and mutable dispatch breadcrumbs separate. */
#include <stdlib.h>
#include <stdatomic.h>
#include "overlay_dispatch_preamble.c.inc"
/* Checks and thread creation must still execute in Release builds. */
#define assert(condition) do { if (!(condition)) abort(); } while (0)
#ifdef _WIN32
#include <windows.h>
#else
#include <pthread.h>
#endif
static atomic_int phase;
static unsigned live_cycles, replay_cycles;
static int live_bail, replay_bail;
static uint32_t live_pc, replay_pc;
static uint64_t live_first, replay_first, live_resolved, replay_resolved;
static void charge_live(uint32_t cycles) { live_cycles += cycles; }
static void charge_replay(uint32_t cycles) { replay_cycles += cycles; }
static void wait_for(int value) {
while (atomic_load_explicit(&phase, memory_order_acquire) != value) { }
}
static void signal_phase(int value) {
atomic_store_explicit(&phase, value, memory_order_release);
}
static void replay(void) {
OverlayCallbacks cbs = {0};
cbs.cpu_charge = charge_replay;
cbs.call_bail_flag = &replay_bail;
cbs.last_store_pc = &replay_pc;
cbs.bail_first = &replay_first;
cbs.bail_resolved = &replay_resolved;
overlay_init(&cbs);
psx_cpu_charge(11);
g_debug_last_store_pc = 0x80002000u;
g_psx_call_bail = 2;
g_psx_bail_first = 3;
g_psx_bail_resolved = 4;
signal_phase(1);
wait_for(2);
assert(g_debug_last_store_pc == 0x80002000u);
assert(g_psx_call_bail == 2);
overlay_flush_cycles();
assert(replay_cycles == 11);
/* Rebinding with missing pointers must not retain the former host's
* state, and fallback words must belong to this thread too. */
cbs.call_bail_flag = 0;
cbs.last_store_pc = 0;
cbs.bail_first = 0;
cbs.bail_resolved = 0;
overlay_init(&cbs);
g_psx_call_bail = 99;
g_debug_last_store_pc = 99;
signal_phase(3);
}
#ifdef _WIN32
static DWORD WINAPI replay_entry(LPVOID arg) { (void)arg; replay(); return 0; }
#else
static void *replay_entry(void *arg) { (void)arg; replay(); return 0; }
#endif
int main(void) {
OverlayCallbacks cbs = {0};
cbs.cpu_charge = charge_live;
cbs.call_bail_flag = &live_bail;
cbs.last_store_pc = &live_pc;
cbs.bail_first = &live_first;
cbs.bail_resolved = &live_resolved;
overlay_init(&cbs);
#ifdef _WIN32
HANDLE thread = CreateThread(0, 0, replay_entry, 0, 0, 0);
assert(thread);
#else
pthread_t thread;
assert(pthread_create(&thread, 0, replay_entry, 0) == 0);
#endif
wait_for(1);
psx_cpu_charge(7);
g_debug_last_store_pc = 0x80001000u;
g_psx_call_bail = 5;
g_psx_bail_first = 6;
g_psx_bail_resolved = 7;
overlay_flush_cycles();
assert(live_cycles == 7);
signal_phase(2);
wait_for(3);
#ifdef _WIN32
assert(WaitForSingleObject(thread, 10000) == WAIT_OBJECT_0);
CloseHandle(thread);
#else
assert(pthread_join(thread, 0) == 0);
#endif
assert(live_bail == 5 && replay_bail == 2);
assert(live_pc == 0x80001000u && replay_pc == 0x80002000u);
assert(live_first == 6 && replay_first == 3);
assert(live_resolved == 7 && replay_resolved == 4);
assert(g_debug_last_store_pc == live_pc);
assert(g_psx_call_bail == live_bail);
return 0;
}