mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Isolate compiled overlay bindings per execution thread
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user