diff --git a/docs/internal/FAITHFUL_TIMING_PLAN.md b/docs/internal/FAITHFUL_TIMING_PLAN.md index 03e11e83..aca34899 100644 --- a/docs/internal/FAITHFUL_TIMING_PLAN.md +++ b/docs/internal/FAITHFUL_TIMING_PLAN.md @@ -2080,3 +2080,15 @@ Guest-frame lookup merged multiple extra presentations and could not verify HUD stability. Added duplicate-frame/freeze/eviction regression coverage and retained legacy frame lookup. Six focused framework checks and all 21 title checks pass on Windows and Linux, and both runtime builds link successfully. + + +2026-10-21 BRIEF-5 continuation: private execution now owns IRQ/DMA/timer, +I-cache and callback nesting state, with code-validation/MMIO metadata captured +beside RAM. Generated game and built-in BIOS expose native code lookup for a +sealed worker dispatcher; loadable BIOS ABI advances to 3. Added bounded, +thread-local immutable render command capture, independent of the live SPSC +producer. Concurrent memory/IRQ and capture/consumer fixtures pass on Windows +and Mac; Mac syntax checks pass for the changed device modules, mod runtime, +and regenerated OpenBIOS. Worker scheduling/plugin host-state/GL handoff remain +in progress. No Stage 3 acceptance or push; ordered route/performance gates have +not started for this implementation. diff --git a/generated/OpenBIOS.emitter.sha b/generated/OpenBIOS.emitter.sha index 556d6b3b..c78b61f2 100644 --- a/generated/OpenBIOS.emitter.sha +++ b/generated/OpenBIOS.emitter.sha @@ -1 +1 @@ -74e1a7bb76ab90d30f3cf4d5627204d0184d88b9abc605b742e0f84934c4c464 +7b08de0636dd809d712346f2a0169885543dce64379d7d977320bfd2dfa59857 diff --git a/generated/OpenBIOS_dispatch.c b/generated/OpenBIOS_dispatch.c index 53da932f..4bb71158 100644 --- a/generated/OpenBIOS_dispatch.c +++ b/generated/OpenBIOS_dispatch.c @@ -21,7 +21,7 @@ extern void psx_restore_state_escape(void); extern void gte_execute(CPUState* cpu, uint32_t cmd); extern void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val); extern uint32_t gte_read_data(CPUState* cpu, uint8_t reg); -extern uint32_t g_debug_current_func_addr; +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; extern void debug_server_trace_dispatch(uint32_t func_addr); /* BIOS HLE tier (CLAUDE.md §0 amendment 2026-07-02): null-by-default * hook consulted at the top of every dispatch iteration, BEFORE any @@ -9057,13 +9057,30 @@ static PsxRecompFunc psx_bios_hit_body(CPUState* cpu, uint32_t addr, uint32_t ke return home; } +extern int psx_kernel_bless_dispatchable(uint32_t phys); +static int psx_bios_execution_lookup(CPUState* cpu, uint32_t addr, PsxExecutionCode* code) { + if (psx_bios_try_native_call_stub(cpu, addr)) { code->function = 0; code->resume_pc = cpu->pc; return 2; } + const uint32_t key = normalize(addr); + int lo = 0, hi = (int)(sizeof(dispatch_table) / sizeof(dispatch_table[0])) - 1; + while (lo <= hi) { + int mid = (lo + hi) / 2; + if (dispatch_table[mid].addr == key) { + if (key - 0x500u < 0x46A0u && !psx_kernel_bless_dispatchable(key)) return 0; + code->function = psx_bios_hit_body(cpu, addr, key, dispatch_table[mid].func); + code->resume_pc = addr; return 1; + } + if (dispatch_table[mid].addr < key) lo = mid + 1; else hi = mid - 1; + } + return 0; +} + extern int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr); extern int dirty_ram_is_dirty(uint32_t phys); extern int psx_kernel_bless_dispatchable(uint32_t phys); extern void fntrace_record(CPUState* cpu, uint32_t target); extern uint64_t g_dispatch_static_hits; -extern int g_psx_dispatch_depth; /* runtime-owned: shared dispatch state, not per-image */ +extern PSX_THREAD_LOCAL int g_psx_dispatch_depth; /* execution-thread dispatch state */ static void psx_dispatch_check_return_boundary(CPUState* cpu, uint32_t stop_addr) { if (stop_addr != 0u) { @@ -9328,4 +9345,5 @@ const PsxBiosBackend OpenBIOS_psx_bios_backend = { OpenBIOS_psx_bios_kernel_body_count, OpenBIOS_psx_bios_kernel_patch_ranges, OpenBIOS_psx_bios_kernel_patch_range_count, + psx_bios_execution_lookup, }; diff --git a/generated/OpenBIOS_full.c b/generated/OpenBIOS_full.c index aa6ac929..87a511dd 100644 --- a/generated/OpenBIOS_full.c +++ b/generated/OpenBIOS_full.c @@ -26,7 +26,9 @@ extern void cosim_instr(uint32_t pc); extern void debug_server_log_call_entry(uint32_t func_addr); extern void debug_server_cyc_observe(uint32_t block_leader_phys); #endif -extern uint32_t g_debug_last_store_pc; +#ifndef PSX_OVERLAY_DLL_BUILD +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; +#endif /* Phase 1.0e-d: per-block guest cycle accounting. * Compile generated code with -DPSX_ENABLE_BLOCK_CYCLES=1 to * activate cycle advancement at every block leader. */ diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 19833abc..831aed75 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -3724,7 +3724,7 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const { ss << "extern void gte_ws_set_suppress(int on); /* widescreen far-backdrop un-squash (gte.cpp) */\n"; if (!config_.overlay_mode && !config_.draw_distance_clamp_sites.empty()) ss << "extern uint32_t g_psx_draw_distance_clamp; /* [[draw_distance.clamp]] switch (draw_distance.c) */\n"; - ss << "extern uint32_t g_debug_last_store_pc; /* exact PC of the executing SW/SH/SB — wtrace/readtrace producer attribution (debug_server.c) */\n\n"; + ss << "#ifndef PSX_OVERLAY_DLL_BUILD\nextern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc;\n#endif\n\n"; } void CodeGenerator::emit_unaligned_helpers(std::ostream& ss, bool as_inline) const { diff --git a/recompiler/src/full_function_emitter.cpp b/recompiler/src/full_function_emitter.cpp index 515ded2d..348c2c93 100644 --- a/recompiler/src/full_function_emitter.cpp +++ b/recompiler/src/full_function_emitter.cpp @@ -1826,7 +1826,7 @@ void FullFunctionEmitter::emit_dispatch( out += "extern void gte_execute(CPUState* cpu, uint32_t cmd);\n"; out += "extern void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val);\n"; out += "extern uint32_t gte_read_data(CPUState* cpu, uint8_t reg);\n"; - out += "extern uint32_t g_debug_current_func_addr;\n"; + out += "extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr;\n"; out += "extern void debug_server_trace_dispatch(uint32_t func_addr);\n"; out += "/* BIOS HLE tier (CLAUDE.md \xC2\xA7""0 amendment 2026-07-02): null-by-default\n"; out += " * hook consulted at the top of every dispatch iteration, BEFORE any\n"; @@ -2229,6 +2229,23 @@ void FullFunctionEmitter::emit_dispatch( out += " }\n"; out += " return home;\n"; out += "}\n\n"; + out += "extern int psx_kernel_bless_dispatchable(uint32_t phys);\n"; + out += "static int psx_bios_execution_lookup(CPUState* cpu, uint32_t addr, PsxExecutionCode* code) {\n"; + out += " if (psx_bios_try_native_call_stub(cpu, addr)) { code->function = 0; code->resume_pc = cpu->pc; return 2; }\n"; + out += " const uint32_t key = normalize(addr);\n"; + out += " int lo = 0, hi = (int)(sizeof(dispatch_table) / sizeof(dispatch_table[0])) - 1;\n"; + out += " while (lo <= hi) {\n"; + out += " int mid = (lo + hi) / 2;\n"; + out += " if (dispatch_table[mid].addr == key) {\n"; + if (addr_model().has_kbless()) { + out += fmt::format(" if (key - 0x{:X}u < 0x{:X}u && !psx_kernel_bless_dispatchable(key)) return 0;\n", + addr_model().kbless_ram_lo(), addr_model().kbless_ram_hi() - addr_model().kbless_ram_lo()); + } + out += " code->function = psx_bios_hit_body(cpu, addr, key, dispatch_table[mid].func);\n"; + out += " code->resume_pc = addr; return 1;\n"; + out += " }\n"; + out += " if (dispatch_table[mid].addr < key) lo = mid + 1; else hi = mid - 1;\n"; + out += " }\n return 0;\n}\n\n"; out += "extern int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr);\n"; out += "extern int dirty_ram_is_dirty(uint32_t phys);\n"; @@ -2236,7 +2253,7 @@ void FullFunctionEmitter::emit_dispatch( out += "extern void fntrace_record(CPUState* cpu, uint32_t target);\n"; out += "extern uint64_t g_dispatch_static_hits;\n"; out += "\n"; - out += "extern int g_psx_dispatch_depth; /* runtime-owned: shared dispatch state, not per-image */\n\n"; + out += "extern PSX_THREAD_LOCAL int g_psx_dispatch_depth; /* execution-thread dispatch state */\n\n"; // Return PCs are compared in full, segment included, as psx_call_contract // does (docs/SEGMENT_AWARE_CODE.md §5.5): stop_addr is the link the call // wrote, in its body's segment. @@ -2536,6 +2553,7 @@ void FullFunctionEmitter::emit_dispatch( " {0}psx_bios_kernel_body_count," "\n" " {0}psx_bios_kernel_patch_ranges," "\n" " {0}psx_bios_kernel_patch_range_count," "\n" + " psx_bios_execution_lookup," "\n" "}};" "\n", g_sym_prefix); } @@ -2639,7 +2657,7 @@ EmitStats FullFunctionEmitter::emit( full_c += "extern void debug_server_log_call_entry(uint32_t func_addr);\n"; full_c += "extern void debug_server_cyc_observe(uint32_t block_leader_phys);\n"; full_c += "#endif\n"; - full_c += "extern uint32_t g_debug_last_store_pc;\n"; + full_c += "#ifndef PSX_OVERLAY_DLL_BUILD\nextern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc;\n#endif\n"; full_c += "/* Phase 1.0e-d: per-block guest cycle accounting.\n"; full_c += " * Compile generated code with -DPSX_ENABLE_BLOCK_CYCLES=1 to\n"; full_c += " * activate cycle advancement at every block leader. */\n"; diff --git a/recompiler/src/game_dispatch_emitter.cpp b/recompiler/src/game_dispatch_emitter.cpp index fc70ae12..a386af0b 100644 --- a/recompiler/src/game_dispatch_emitter.cpp +++ b/recompiler/src/game_dispatch_emitter.cpp @@ -32,7 +32,7 @@ bool emit_game_dispatch(const std::vector& units, const bool cps = units.front().codegen->cps_enabled(); std::ostringstream ds; ds << "/* Generated by PSXRecomp - dynamic dispatch table */\n"; - ds << "#include \"psx_runtime.h\"\n\n"; + ds << "#include \"psx_runtime.h\"\n#include \"psx_execution.h\"\n\n"; ds << "extern void psx_check_interrupts_dispatch_entry(CPUState* cpu, uint32_t resume_pc);\n\n"; ds << "extern int dirty_ram_text_native_ok_ranges_from(const uint32_t* lo_len_pairs, uint32_t count, uint32_t exec_pc);\n\n"; ds << "extern int dirty_ram_text_native_ok_ranges(const uint32_t* lo_len_pairs, uint32_t count);\n\n"; @@ -289,6 +289,10 @@ bool emit_game_dispatch(const std::vector& units, ds << "}\n\n"; ds << "/* Maps PS1 address to compiled game code. Returns 1 if dispatched, 0 if unknown. */\n"; + ds << "int psx_game_execution_lookup(uint32_t addr, PsxExecutionCode* code) {\n"; + ds << " const PsxGameDispatchEntry* entry = psx_game_find_entry(addr);\n"; + ds << " if (!entry || !psx_game_text_native_ok(addr)) return 0;\n"; + ds << " code->function = entry->fn; code->resume_pc = entry->resume_pc; return 1;\n}\n\n"; ds << "int psx_dispatch_game_compiled(CPUState* cpu, uint32_t addr) {\n"; ds << " const PsxGameDispatchEntry* entry = psx_game_find_entry(addr);\n"; ds << " if (!entry) return 0;\n"; diff --git a/recompiler/tests/test_segment_aware_codegen.py b/recompiler/tests/test_segment_aware_codegen.py index 9593329a..f75d2a2b 100644 --- a/recompiler/tests/test_segment_aware_codegen.py +++ b/recompiler/tests/test_segment_aware_codegen.py @@ -149,8 +149,8 @@ CONTRACT_HARNESS = r""" #include "cpu_state.h" #include #include -int g_psx_call_bail; -uint64_t g_psx_bail_first, g_psx_bail_resolved, g_psx_bail_flattened, g_psx_bail_anomaly; +PSX_THREAD_LOCAL int g_psx_call_bail; +PSX_THREAD_LOCAL uint64_t g_psx_bail_first, g_psx_bail_resolved, g_psx_bail_flattened, g_psx_bail_anomaly; void psx_bail_record(uint32_t a, uint32_t b, uint32_t c, uint32_t d) { (void)a; (void)b; (void)c; (void)d; } diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index dcac3d33..09c3317d 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1654,6 +1654,11 @@ if(BUILD_TESTING) find_package(Threads REQUIRED) target_link_libraries(render_thread_test PRIVATE Threads::Threads) add_test(NAME render_thread_test COMMAND render_thread_test) + add_executable(render_capture_test tests/test_render_capture.c src/render_thread.c) + target_include_directories(render_capture_test PRIVATE include) + target_link_libraries(render_capture_test PRIVATE Threads::Threads) + add_test(NAME render_capture_test COMMAND render_capture_test) + # Present thread core ([video] present_thread): submission order is # presentation order, slots are never composed while queued or on # screen, done tokens reach the next composer, stop drains. diff --git a/runtime/codegen_hash_sources.cmake b/runtime/codegen_hash_sources.cmake index f4697fa6..469d7f98 100644 --- a/runtime/codegen_hash_sources.cmake +++ b/runtime/codegen_hash_sources.cmake @@ -18,6 +18,7 @@ # overlay-cache hazard (same class as the "ws emission not in cg hash" gap). set(PSXRECOMP_CODEGEN_HASH_SRCS ${PSXRECOMP_CODEGEN_HASH_ROOT}/runtime/include/psx_thread_local.h + ${PSXRECOMP_CODEGEN_HASH_ROOT}/runtime/include/psx_execution.h ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/code_generator.cpp ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/mips_decoder.cpp ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/control_flow.cpp diff --git a/runtime/include/card_data_writes.h b/runtime/include/card_data_writes.h index ff6776d1..a2086f80 100644 --- a/runtime/include/card_data_writes.h +++ b/runtime/include/card_data_writes.h @@ -1,4 +1,3 @@ -#include "psx_thread_local.h" /* * card_data_writes.h — always-on capture of where the BIOS stores * incoming SIO data bytes during memory-card READ_DATA. @@ -33,7 +32,7 @@ extern "C" { typedef struct { uint64_t seq; /* monotonic across all entries */ - PSX_THREAD_LOCAL uint64_t psx_cycle_count; + uint64_t psx_cycle_count; uint32_t addr; /* dest physical RAM address */ uint32_t store_pc; /* g_debug_last_store_pc when stored */ uint32_t func_addr; /* g_debug_current_func_addr */ diff --git a/runtime/include/card_read_summary.h b/runtime/include/card_read_summary.h index 667176f5..fc67562a 100644 --- a/runtime/include/card_read_summary.h +++ b/runtime/include/card_read_summary.h @@ -1,4 +1,3 @@ -#include "psx_thread_local.h" /* * card_read_summary.h — non-evicting evidence ring for the first 32 * completed memory-card reads. @@ -27,7 +26,7 @@ extern "C" { typedef struct { uint64_t seq; /* monotonic; first record = 0 */ - PSX_THREAD_LOCAL uint64_t psx_cycle_count; /* guest cycle clock at completion */ + uint64_t psx_cycle_count; /* guest cycle clock at completion */ uint32_t current_func; /* g_debug_current_func_addr at MC_READ_END */ uint32_t last_store_pc; /* g_debug_last_store_pc at MC_READ_END */ uint16_t sector; diff --git a/runtime/include/cpu_state.h b/runtime/include/cpu_state.h index 4355071a..d57e7505 100644 --- a/runtime/include/cpu_state.h +++ b/runtime/include/cpu_state.h @@ -1,4 +1,5 @@ -/* ============================================================================ +#include "psx_thread_local.h" +/* ============================================================================ * cpu_state.h — Phase 2 runtime version * ---------------------------------------------------------------------------- * This is the RUNTIME version of the CPUState header. It has the same struct @@ -315,11 +316,11 @@ static inline uint32_t *psx_overlay_last_store_pc(void) { } #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 */ -extern uint64_t g_psx_bail_resolved; /* unwinds resolved at a call site */ -extern uint64_t g_psx_bail_flattened; /* unwinds flattened at outermost */ -extern uint64_t g_psx_bail_anomaly; /* bail flag seen where impossible */ +extern PSX_THREAD_LOCAL int g_psx_call_bail; +extern PSX_THREAD_LOCAL uint64_t g_psx_bail_first; /* contract violations detected */ +extern PSX_THREAD_LOCAL uint64_t g_psx_bail_resolved; /* unwinds resolved at a call site */ +extern PSX_THREAD_LOCAL uint64_t g_psx_bail_flattened; /* unwinds flattened at outermost */ +extern PSX_THREAD_LOCAL uint64_t g_psx_bail_anomaly; /* bail flag seen where impossible */ #endif /* Deduped wild-return source ledger (traps.c). Not present in overlay DLLs diff --git a/runtime/include/debug_server.h b/runtime/include/debug_server.h index d3033f79..04d09c49 100644 --- a/runtime/include/debug_server.h +++ b/runtime/include/debug_server.h @@ -1,4 +1,5 @@ -/* +#include "psx_thread_local.h" +/* * debug_server.h -- TCP debug server for PSX recomp v4 * * Non-blocking TCP server on localhost (default port 4370). @@ -127,7 +128,7 @@ int debug_server_is_connected(void); /* Current dispatch target — set by psx_dispatch() before each call. * Captured by the write-trace hook for function attribution. */ -extern uint32_t g_debug_current_func_addr; +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; /* Multi-range write-trace check. Called from memory.c write paths. * Iterates up to 8 configurable address ranges; records matching @@ -157,7 +158,7 @@ void debug_server_log_call_entry_fn(uint32_t func_addr); * store (sb/sh/sw/swl/swr/swc2). Read by SIO/MMIO write handlers to * attribute the writing instruction without needing a full call-stack * walk. Stale across non-store instructions but every store updates it. */ -extern uint32_t g_debug_last_store_pc; +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; /* SIO write PC tracer ring — records (pc, addr, value, byte_seq) for every * write to SIO_DATA / SIO_CTRL / SIO_STAT. Always-on; pulled via TCP cmd diff --git a/runtime/include/psx_bios_backend.h b/runtime/include/psx_bios_backend.h index 8d525d1f..f64a4e89 100644 --- a/runtime/include/psx_bios_backend.h +++ b/runtime/include/psx_bios_backend.h @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* psx_bios_backend.h — the routing seam between the runtime and whichever * recompiled BIOS is active. * @@ -25,6 +26,7 @@ #include #include "psx_bios_image.h" +#include "psx_execution.h" #ifdef __cplusplus extern "C" { @@ -56,10 +58,13 @@ typedef struct PsxBiosBackend { * each range's hi. Null/0 for an image that declares no slots. */ const PsxKernelPatchRange *kernel_patch_ranges; uint32_t kernel_patch_range_count; + /* Side-effect-free code selection for an isolated execution context. + * 1: function/resume_pc; 2: native vector instructions ran and set pc. */ + int (*execution_lookup)(struct CPUState *, uint32_t, PsxExecutionCode *); } PsxBiosBackend; /* Dispatch nesting depth, shared by every backend (psx_bios_backend.c). */ -extern int g_psx_dispatch_depth; +extern PSX_THREAD_LOCAL int g_psx_dispatch_depth; /* The backend in use. Null before psx_bios_select() runs; every forwarder and * every consumer of psx_bios_image depends on it being set first. */ diff --git a/runtime/include/psx_bios_module.h b/runtime/include/psx_bios_module.h index a092e8eb..163e3074 100644 --- a/runtime/include/psx_bios_module.h +++ b/runtime/include/psx_bios_module.h @@ -84,7 +84,7 @@ struct CPUState; struct OverlayCallbacks; struct PsxBiosBackend; -#define PSX_BIOS_MODULE_ABI_VERSION 2 +#define PSX_BIOS_MODULE_ABI_VERSION 3 /* Combined tag: ABI version (low 16) | codegen flavor (high 16), like * PSX_OVERLAY_ABI_TAG. The flavor comes from the same -DPSX_OVERLAY_FLAVOR diff --git a/runtime/include/psx_bios_module_glue.c.inc b/runtime/include/psx_bios_module_glue.c.inc index 3cfbdb26..49356a0a 100644 --- a/runtime/include/psx_bios_module_glue.c.inc +++ b/runtime/include/psx_bios_module_glue.c.inc @@ -40,9 +40,9 @@ static uint32_t s_mod_guest_count_dummy; * mappings): `extern int g_psx_dispatch_depth;` became * `extern int (*g_psx_bm_dispatch_depth_p);`, and so on. Defined here. */ int (**g_psx_bm_hle_hook_p)(CPUState *cpu, uint32_t phys) = &s_hle_hook_dummy; -int *g_psx_bm_dispatch_depth_p = &s_depth_dummy; +PSX_THREAD_LOCAL int *g_psx_bm_dispatch_depth_p = &s_depth_dummy; uint64_t *g_psx_bm_static_hits_p = &s_hits_dummy; -uint32_t *g_psx_bm_current_func_addr_p = &s_func_addr_dummy; +PSX_THREAD_LOCAL uint32_t *g_psx_bm_current_func_addr_p = &s_func_addr_dummy; uint64_t *g_psx_bm_bail_flattened_p = &s_flattened_dummy; uint32_t *g_psx_bm_ram_mask_p = &s_ram_mask_dummy; uint64_t *g_psx_bm_frame_count_p = &s_frame_count_dummy; diff --git a/runtime/include/psx_execution.h b/runtime/include/psx_execution.h new file mode 100644 index 00000000..5c899d5e --- /dev/null +++ b/runtime/include/psx_execution.h @@ -0,0 +1,22 @@ +#ifndef PSX_EXECUTION_H +#define PSX_EXECUTION_H +#include +#include "psx_thread_local.h" +#ifdef __cplusplus +extern "C" { +#endif +struct CPUState; +typedef struct PsxExecutionCode { + void (*function)(struct CPUState *); + uint32_t resume_pc; +} PsxExecutionCode; +typedef void (*PsxExecutionDispatch)(struct CPUState *, uint32_t, uint32_t); +typedef void (*PsxExecutionAbort)(const char *, uint32_t); +/* A private execution supplies its sealed dispatch table and abort landing. + * NULL selects the ordinary runtime. Never replace these on another thread. */ +extern PSX_THREAD_LOCAL PsxExecutionDispatch g_psx_execution_dispatch; +extern PSX_THREAD_LOCAL PsxExecutionAbort g_psx_execution_abort; +#ifdef __cplusplus +} +#endif +#endif diff --git a/runtime/include/psx_icache.h b/runtime/include/psx_icache.h index e45b9381..e718d78d 100644 --- a/runtime/include/psx_icache.h +++ b/runtime/include/psx_icache.h @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" #ifndef PSXRECOMP_PSX_ICACHE_H #define PSXRECOMP_PSX_ICACHE_H @@ -7,8 +8,8 @@ extern "C" { #endif -extern uint32_t g_psx_icache_tv[1024]; -extern int g_psx_icache_active; +extern PSX_THREAD_LOCAL uint32_t g_psx_icache_tv[1024]; +extern PSX_THREAD_LOCAL int g_psx_icache_active; extern int g_ls_replay_active; void psx_icache_reset(void); void psx_icache_fetch(CPUState *cpu, uint32_t addr); diff --git a/runtime/include/psx_memory.h b/runtime/include/psx_memory.h index b820bc9f..f6612f87 100644 --- a/runtime/include/psx_memory.h +++ b/runtime/include/psx_memory.h @@ -57,6 +57,15 @@ void memory_backing_destroy(PsxMemoryBacking *backing); void memory_backing_capture(PsxMemoryBacking *destination); PsxMemoryBacking *memory_backing_bind(PsxMemoryBacking *backing); extern PSX_THREAD_LOCAL uint8_t *g_psx_ram; +/* Code-validity and MMIO metadata accompanies the backing snapshot. Capture + * on the source owner; bind only on a dedicated worker after backing_bind. + * The active code image remains immutable until that worker has stopped. */ +typedef struct PsxMemoryExecutionState PsxMemoryExecutionState; +PsxMemoryExecutionState *memory_execution_create(void); +void memory_execution_destroy(PsxMemoryExecutionState *state); +void memory_execution_capture(PsxMemoryExecutionState *state); +void memory_execution_bind(const PsxMemoryExecutionState *state, const uint32_t *status); + int psx_ram_8mb_active(void); void psx_ram_reset_size_request(void); /* memory_init(): latch the requested geometry for this boot. */ diff --git a/runtime/include/render_thread.h b/runtime/include/render_thread.h index b5e2dc1a..9d14f35b 100644 --- a/runtime/include/render_thread.h +++ b/runtime/include/render_thread.h @@ -89,6 +89,21 @@ int rt_on_render_thread(void); void *rt_cmd_begin(uint16_t op, uint16_t flags, uint32_t payload_bytes); void rt_cmd_commit(void); +/* A dedicated guest worker records into its own bounded list. It never writes + * the SPSC ring. Ownership transfers only after end; the consumer executes a + * sealed list on the render thread, or drops it when its generation is late. + * Allocation happens before publication, never while guest code is running. */ +typedef struct RtCommandList RtCommandList; +typedef void (*RtCaptureAbort)(const char *reason); +RtCommandList *rt_list_create(size_t capacity); +void rt_list_destroy(RtCommandList *list); +void rt_capture_begin(RtCommandList *list, RtCaptureAbort failure); +void rt_capture_end(void); +int rt_capture_active(void); +size_t rt_list_bytes(const RtCommandList *list); +void rt_list_execute(const RtCommandList *list, RtExecFn exec, void *user); + + /* Close a guest frame: enqueue a frame marker, then block while more than * max_frames closed frames are unconsumed. */ void rt_frame_end(void); diff --git a/runtime/src/boot_state.c b/runtime/src/boot_state.c index 498aa193..690ead61 100644 --- a/runtime/src/boot_state.c +++ b/runtime/src/boot_state.c @@ -58,8 +58,8 @@ static double boot_state_mono_ms(void) { extern uint8_t* memory_get_ram_ptr(void); extern uint32_t memory_get_ram_bytes(void); extern uint8_t* memory_get_scratchpad_ptr(void); -extern uint32_t i_stat; -extern uint32_t i_mask; +extern PSX_THREAD_LOCAL uint32_t i_stat; +extern PSX_THREAD_LOCAL uint32_t i_mask; extern PSX_THREAD_LOCAL uint64_t psx_cycle_count; extern void timers_get_snapshot(uint16_t counter[3], uint32_t mode[3], uint16_t target[3], int32_t irq_line[3], diff --git a/runtime/src/card_data_writes.c b/runtime/src/card_data_writes.c index 38a2c4c6..cc23b600 100644 --- a/runtime/src/card_data_writes.c +++ b/runtime/src/card_data_writes.c @@ -1,11 +1,12 @@ +#include "psx_thread_local.h" /* card_data_writes.c — capture BIOS RAM destination of card data bytes. */ #include "card_data_writes.h" #include "psx_cycles.h" #include -extern uint32_t g_debug_current_func_addr; -extern uint32_t g_debug_last_store_pc; +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; /* "Armed" byte from the most recent SIO_RX_DATA read inside a card * data phase. The next matching RAM write captures it. */ diff --git a/runtime/src/card_read_summary.c b/runtime/src/card_read_summary.c index be11b852..16c37935 100644 --- a/runtime/src/card_read_summary.c +++ b/runtime/src/card_read_summary.c @@ -1,11 +1,12 @@ +#include "psx_thread_local.h" /* card_read_summary.c — non-evicting first-32 completed card reads. */ #include "card_read_summary.h" #include "psx_cycles.h" #include -extern uint32_t g_debug_current_func_addr; -extern uint32_t g_debug_last_store_pc; +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; static CardReadSummary s_ring[CARD_READ_SUMMARY_CAP]; static uint32_t s_count = 0; diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index d667ba3f..841bb148 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* * cdrom.c — PS1 CD-ROM controller (adapted from psxrecomp-v3) * @@ -53,11 +54,11 @@ extern uint32_t iso_cdda_track_pregap_lba(void* handle, int track); extern int iso_cdda_track_is_audio(void* handle, int track); /* I_STAT owned by memory.c — set bit 2 for CDROM IRQ */ -extern uint32_t i_stat; +extern PSX_THREAD_LOCAL uint32_t i_stat; /* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */ extern void psx_irq_raise(uint32_t bit, uint32_t detail); -extern uint32_t g_debug_current_func_addr; -extern uint32_t g_debug_last_store_pc; +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; extern uint64_t s_frame_count; /* CD-ROM state */ diff --git a/runtime/src/cosim.c b/runtime/src/cosim.c index aa544b90..edafdaba 100644 --- a/runtime/src/cosim.c +++ b/runtime/src/cosim.c @@ -36,7 +36,7 @@ typedef int sock_t; #define COSIM_SLEEP(ms) usleep((ms)*1000) #endif -extern uint32_t i_stat, i_mask; +extern PSX_THREAD_LOCAL uint32_t i_stat, i_mask; extern PSX_THREAD_LOCAL uint64_t psx_cycle_count; /* EXPERIMENTAL input-driven cosim: held pad state applied every frame by the headless diff --git a/runtime/src/cosim_state.c b/runtime/src/cosim_state.c index 8d12ea88..b3c98ec7 100644 --- a/runtime/src/cosim_state.c +++ b/runtime/src/cosim_state.c @@ -33,8 +33,8 @@ static inline uint64_t fnv_u64(uint64_t h, uint64_t v) { return fnv(h, &v, 8); } extern CPUState *debug_cpu_ptr; extern uint8_t *memory_get_ram_ptr(void); extern uint8_t *memory_get_scratchpad_ptr(void); -extern uint32_t i_stat; -extern uint32_t i_mask; +extern PSX_THREAD_LOCAL uint32_t i_stat; +extern PSX_THREAD_LOCAL uint32_t i_mask; extern PSX_THREAD_LOCAL uint64_t psx_cycle_count; extern uint32_t dirty_ram_get_bitmap_word(uint32_t word_index); extern uint32_t dirty_ram_get_bitmap_word_count(void); diff --git a/runtime/src/crash_trace.c b/runtime/src/crash_trace.c index 7bbe333e..4fbb27b2 100644 --- a/runtime/src/crash_trace.c +++ b/runtime/src/crash_trace.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* crash_trace.c — unified crash diagnostic dump. * * Writes a single JSON report file (psx_last_run_report.json) on: @@ -94,15 +95,15 @@ extern int psx_netplay_is_resimulating(void); extern uint64_t s_frame_count; /* Globals from debug_server.c we want to capture. */ -extern uint32_t g_debug_current_func_addr; -extern uint32_t g_debug_last_store_pc; +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; /* Native dispatch nesting depth (generated/SCPH1001_dispatch.c). Incremented per * nested psx_dispatch_call, decremented on return. A huge value at crash time is * the direct fingerprint of runaway recursion (the host C stack mirrors the guest * call graph, so an unbounded call chain overflows it -> STATUS_STACK_OVERFLOW * 0xC00000FD). Pairs with the dirty_block tail, which names the recursing PCs. */ -extern int g_psx_dispatch_depth; +extern PSX_THREAD_LOCAL int g_psx_dispatch_depth; /* Dispatch ring — accessor wrappers exported by debug_server.c. */ #define DISPATCH_TRACE_CAP (1 << 16) diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 31f1b0bd..e010730d 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* * debug_server.c -- TCP debug server for PSX recomp v4 * @@ -119,8 +120,8 @@ #include "debug_socket_io.h" /* ---- Externs from runtime ---- */ -extern uint32_t i_stat; -extern uint32_t i_mask; +extern PSX_THREAD_LOCAL uint32_t i_stat; +extern PSX_THREAD_LOCAL uint32_t i_mask; /* Memory access (from memory.c) */ extern uint32_t psx_read_word(uint32_t addr); @@ -491,10 +492,10 @@ static struct { uint32_t lo, hi; } s_wtrace_trans_ranges[WTRACE_TRANS_MAX_RANGES static int s_wtrace_trans_range_count = 0; /* Function attribution global — set by psx_dispatch() before each call. */ -uint32_t g_debug_current_func_addr = 0; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0; /* Last store PC — set by recompiler emitter before every store instruction. */ -uint32_t g_debug_last_store_pc = 0; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc = 0; /* Static dispatch hit counter — incremented by generated dispatch code on * every binary-search hit (i.e. successfully dispatched to static C). */ @@ -11391,9 +11392,9 @@ static void wtrace_fill_entry(WriteTraceEntry *e, uint64_t seq, uint32_t phys, uint32_t old_val, uint32_t new_val, uint8_t width) { - extern int g_dma_exec_depth; /* >0 while a DMA moves data (dma.c) */ - extern int g_dma_cur_ch; /* in-flight DMA channel, or -1 */ - extern uint32_t g_dma_initiator_pc; /* guest PC that kicked the DMA */ + extern PSX_THREAD_LOCAL int g_dma_exec_depth; /* >0 while a DMA moves data (dma.c) */ + extern PSX_THREAD_LOCAL int g_dma_cur_ch; /* in-flight DMA channel, or -1 */ + extern PSX_THREAD_LOCAL uint32_t g_dma_initiator_pc; /* guest PC that kicked the DMA */ uint32_t ra = debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0; e->seq = seq; e->addr = phys; diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 55090a50..b04cd557 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -283,8 +283,8 @@ static inline void exec_pc_table_record(uint32_t pc) { } /* From debug_server.c — keep our outer-frame attribution coherent. */ -extern uint32_t g_debug_current_func_addr; -extern uint32_t g_debug_last_store_pc; +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; /* Execution-mode tag for the event-timeline ring: 1 while a dirty_ram_dispatch * call is on the stack. Cleared defensively at the psx_check_interrupts longjmp @@ -1128,8 +1128,8 @@ int dirty_ram_re954_json(char *out, int cap) { * so the real driver address is not pre-assumed. NB: there is no due-cycle event * scheduler in this build (psx_cycles.c), so "next scheduled event cycle" is N/A; * cycles-advanced-per-frame + I_STAT/I_MASK cover event progress instead. */ -extern uint32_t i_stat; /* owned by memory.c */ -extern uint32_t i_mask; +extern PSX_THREAD_LOCAL uint32_t i_stat; /* owned by memory.c */ +extern PSX_THREAD_LOCAL uint32_t i_mask; enum { XOP_JAL = 0, XOP_JALR = 1, XOP_JR = 2, XOP_J = 3, XOP_DD = 4, XOP_BR = 5, XOP_RES = 6 /* watched-target call RESOLUTION: site = path code (XRES_*), @@ -2570,7 +2570,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ * still needs the oracle; this converts a hard crash into a catchable, diagnosed * bail (Rule 15: make the failure observable). */ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr) { - extern int g_psx_dispatch_depth; + extern PSX_THREAD_LOCAL int g_psx_dispatch_depth; extern void psx_fatal_halt(const char *reason); /* `addr` is architectural: the interpreter runs at exactly this PC, so a * retail RAM-mirror PC (0x807xxxxx) keeps its own $ra/EPC while fetch_word @@ -2759,7 +2759,7 @@ int dirty_ram_run_span(CPUState* cpu, uint32_t start_pc, uint32_t stop_pc, * boundary? Mirrors the gate in psx_check_interrupts: a pending+unmasked I_STAT * bit, COP0 IEc + IM2 set, and not already inside the exception handler. */ static int precise_irq_deliverable(CPUState *cpu) { - extern uint32_t i_stat; + extern PSX_THREAD_LOCAL uint32_t i_stat; uint32_t sr = cpu->cop0[12]; /* COP0 software interrupts (CAUSE.IP0/IP1 & SR.IM0/IM1): guest-raised via * mtc0 to CAUSE, independent of the INTC line (see psx_check_interrupts). */ @@ -2832,7 +2832,7 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) { for (int i = 0; i < MAX_PRECISE_INSNS; i++) { if (!irq_taken && precise_irq_deliverable(cpu)) { uint32_t committed = pc; - extern uint32_t i_stat; + extern PSX_THREAD_LOCAL uint32_t i_stat; g_slice_last_committed = committed; g_slice_last_istat = i_stat; g_slice_last_imask = i_mask; @@ -2896,7 +2896,7 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) { * after a take re-fires an IRQ the handler has not acked yet (an * 8-takes-in-11-insns storm), so gate on !irq_taken. */ if (!irq_taken && precise_irq_deliverable(cpu)) { - extern uint32_t i_stat; + extern PSX_THREAD_LOCAL uint32_t i_stat; g_slice_last_committed = committed; g_slice_last_istat = i_stat; g_slice_last_imask = i_mask; @@ -3281,7 +3281,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ s_entry_poll_enabled = (e && e[0] == '0') ? 0 : 1; } if (s_entry_poll_enabled) { - extern uint32_t i_stat; + extern PSX_THREAD_LOCAL uint32_t i_stat; uint32_t sr = cpu->cop0[12]; int deliverable = ((i_stat & i_mask) != 0u) && diff --git a/runtime/src/dma.c b/runtime/src/dma.c index b60d8add..bd647ce8 100644 --- a/runtime/src/dma.c +++ b/runtime/src/dma.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* dma.c — PS1 DMA controller simulation (Phase 3). * * Implements all 7 channel register reads/writes, DPCR, DICR, @@ -52,11 +53,11 @@ extern uint32_t psx_read_word(uint32_t addr); extern void psx_write_word(uint32_t addr, uint32_t val); /* Interrupt status — defined in memory.c */ -extern uint32_t i_stat; +extern PSX_THREAD_LOCAL uint32_t i_stat; /* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */ extern void psx_irq_raise(uint32_t bit, uint32_t detail); -extern uint32_t g_debug_current_func_addr; -extern uint32_t g_debug_last_store_pc; +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; extern uint64_t s_frame_count; /* ---- Per-channel registers ---- */ @@ -67,7 +68,7 @@ typedef struct { uint32_t chcr; /* +0x08: Channel control */ } DMAChannel; -static DMAChannel channels[7]; +static PSX_THREAD_LOCAL DMAChannel channels[7]; typedef struct { uint8_t active; @@ -78,12 +79,12 @@ typedef struct { uint32_t start_addr; /* madr at transfer start (CD overlay capture) */ } DMAAsyncChannel; -static DMAAsyncChannel mdec_async[2]; -static DMAAsyncChannel cdrom_async; -static DMAGPULinkedList gpu_linked_list; -static DMAGpuOtStats gpu_ot_stats; -static uint64_t gpu_ot_start_cycle; -static uint32_t gpu_ot_polls_this_walk; +static PSX_THREAD_LOCAL DMAAsyncChannel mdec_async[2]; +static PSX_THREAD_LOCAL DMAAsyncChannel cdrom_async; +static PSX_THREAD_LOCAL DMAGPULinkedList gpu_linked_list; +static PSX_THREAD_LOCAL DMAGpuOtStats gpu_ot_stats; +static PSX_THREAD_LOCAL uint64_t gpu_ot_start_cycle; +static PSX_THREAD_LOCAL uint32_t gpu_ot_polls_this_walk; /* ---- CD DMA transfer log ---- */ /* Every forward CH3 DMA that lands below 0x1C0000 (game data region) records @@ -93,9 +94,9 @@ static uint32_t gpu_ot_polls_this_walk; * overlay regions back to disc positions for extract_overlays.py. */ #define CD_DMA_LOG_CAP 65536 typedef struct { int lba; uint32_t dest; uint32_t size; } CdDmaEntry; -static CdDmaEntry cd_dma_log[CD_DMA_LOG_CAP]; -static uint32_t cd_dma_log_head = 0; -static uint32_t cd_dma_log_total = 0; +static PSX_THREAD_LOCAL CdDmaEntry cd_dma_log[CD_DMA_LOG_CAP]; +static PSX_THREAD_LOCAL uint32_t cd_dma_log_head = 0; +static PSX_THREAD_LOCAL uint32_t cd_dma_log_total = 0; static void cd_dma_log_push(int lba, uint32_t dest, uint32_t size) { cd_dma_log[cd_dma_log_head % CD_DMA_LOG_CAP].lba = lba; @@ -138,18 +139,18 @@ void cd_dma_log_get_entry(uint32_t idx, int *lba, uint32_t *dest, uint32_t * * DMA-sourced (not a CPU/dirty-interp store, whose g_debug_last_store_pc is accurate). * cur_ch/cur_madr/cur_bcr name the in-flight channel + its destination, so a wrong * DMA destination clobbering kernel data becomes directly visible. */ -int g_dma_exec_depth = 0; -int g_dma_cur_ch = -1; -uint32_t g_dma_cur_madr = 0; -uint32_t g_dma_cur_bcr = 0; +PSX_THREAD_LOCAL int g_dma_exec_depth = 0; +PSX_THREAD_LOCAL int g_dma_cur_ch = -1; +PSX_THREAD_LOCAL uint32_t g_dma_cur_madr = 0; +PSX_THREAD_LOCAL uint32_t g_dma_cur_bcr = 0; /* Guest PC that kicked the in-flight DMA (the CHCR store that set start/busy), * so write-provenance (wtrace / parity note_write) attributes DMA-sourced RAM * writes to the code that INITIATED the transfer, not to g_debug_last_store_pc * (which for an async transfer is a stale, unrelated CPU store). Captured at the * kick in trigger_dma_transfer and re-published for async channels whose RAM * writes run in a later advance_*() step. 0 = unknown. */ -uint32_t g_dma_initiator_pc = 0; -static uint32_t s_dma_ch_initiator_pc[7] = {0}; +PSX_THREAD_LOCAL uint32_t g_dma_initiator_pc = 0; +static PSX_THREAD_LOCAL uint32_t s_dma_ch_initiator_pc[7] = {0}; typedef struct { uint8_t active; @@ -157,22 +158,22 @@ typedef struct { uint32_t cycles_remaining; } DMADelayedComplete; -static DMADelayedComplete delayed_complete[7]; +static PSX_THREAD_LOCAL DMADelayedComplete delayed_complete[7]; /* ---- Global registers ---- */ -static uint32_t dpcr; /* 0x1F8010F0: DMA control (enable bits) */ -static uint32_t dicr; /* 0x1F8010F4: DMA interrupt control */ +static PSX_THREAD_LOCAL uint32_t dpcr; /* 0x1F8010F0: DMA control (enable bits) */ +static PSX_THREAD_LOCAL uint32_t dicr; /* 0x1F8010F4: DMA interrupt control */ #define DICR_WRITE_MASK 0x00FF807Fu #define DICR_RESET_MASK 0x7F000000u -static DMATraceEntry dma_trace[DMA_TRACE_CAP]; -static uint64_t dma_trace_seq; -static DMACDROMHistoryEntry cdrom_dma_history[DMA_CDROM_HISTORY_CAP]; -static uint64_t cdrom_dma_history_seq; -static DMACDROMHistoryEntry cdrom_dma_active_entry; -static uint8_t cdrom_dma_history_active; +static PSX_THREAD_LOCAL DMATraceEntry dma_trace[DMA_TRACE_CAP]; +static PSX_THREAD_LOCAL uint64_t dma_trace_seq; +static PSX_THREAD_LOCAL DMACDROMHistoryEntry cdrom_dma_history[DMA_CDROM_HISTORY_CAP]; +static PSX_THREAD_LOCAL uint64_t cdrom_dma_history_seq; +static PSX_THREAD_LOCAL DMACDROMHistoryEntry cdrom_dma_active_entry; +static PSX_THREAD_LOCAL uint8_t cdrom_dma_history_active; static uint32_t dicr_read_value(uint32_t v); @@ -823,8 +824,8 @@ static void start_async_gpu_linked_list(void) { * whole walk (and the GPU work it feeds) with in_exception set, which is what * the phase profiler's exc_share then measures. */ extern int psx_get_in_exception(void); -static DMAGpuHoldEntry s_hold_ring[DMA_GPU_HOLD_RING_CAP]; -static DMAGpuHoldStats s_hold_stats; +static PSX_THREAD_LOCAL DMAGpuHoldEntry s_hold_ring[DMA_GPU_HOLD_RING_CAP]; +static PSX_THREAD_LOCAL DMAGpuHoldStats s_hold_stats; static uint64_t hold_host_ticks(void) { #ifdef _WIN32 diff --git a/runtime/src/event_ring.c b/runtime/src/event_ring.c index 2fb1c3ce..e9d6c122 100644 --- a/runtime/src/event_ring.c +++ b/runtime/src/event_ring.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* * event_ring.c — always-on unified event-timeline ring. See event_ring.h. * @@ -13,10 +14,10 @@ #include /* ---- runtime globals captured at record time ---- */ -extern uint32_t i_stat; /* memory.c */ -extern uint32_t i_mask; /* memory.c */ -extern uint32_t g_debug_current_func_addr; /* debug_server */ -extern uint32_t g_debug_last_store_pc; /* debug_server */ +extern PSX_THREAD_LOCAL uint32_t i_stat; /* memory.c */ +extern PSX_THREAD_LOCAL uint32_t i_mask; /* memory.c */ +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; /* debug_server */ +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; /* debug_server */ /* Execution-mode sources (see overlay_loader.c / dirty_ram_interp.c). */ extern uint32_t overlay_loader_get_inprogress(void); /* in-progress native ovl fn, 0 if none */ diff --git a/runtime/src/fntrace.c b/runtime/src/fntrace.c index 6b08fcd7..79e2cf60 100644 --- a/runtime/src/fntrace.c +++ b/runtime/src/fntrace.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* fntrace.c — runtime side of psx_dispatch call ring. See fntrace.h. */ #include "fntrace.h" @@ -192,8 +193,8 @@ void fntrace_record(CPUState* cpu, uint32_t target) { * bail_top_wild_pc) so the dominant wild-returning function is readable even * when the bail storm makes the window "Not Responding". */ { - extern uint64_t g_psx_bail_flattened; - extern uint32_t g_debug_current_func_addr; + extern PSX_THREAD_LOCAL uint64_t g_psx_bail_flattened; + extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; extern void psx_bail_record(uint32_t site_ra, uint32_t site_sp, uint32_t wild_pc, uint32_t guest_sp); static uint64_t s_last_flat = 0; diff --git a/runtime/src/freeze_heartbeat.c b/runtime/src/freeze_heartbeat.c index 1d1d1468..25ba60e1 100644 --- a/runtime/src/freeze_heartbeat.c +++ b/runtime/src/freeze_heartbeat.c @@ -24,10 +24,10 @@ /* State accessors. All defined in other compilation units; declared here * to avoid pulling in heavy headers. */ extern uint64_t s_frame_count; /* debug_server.c */ -extern uint32_t g_debug_current_func_addr; /* debug_server.c */ -extern uint32_t g_debug_last_store_pc; /* debug_server.c */ -extern uint32_t i_stat; /* memory.c */ -extern uint32_t i_mask; /* memory.c */ +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; /* debug_server.c */ +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; /* debug_server.c */ +extern PSX_THREAD_LOCAL uint32_t i_stat; /* memory.c */ +extern PSX_THREAD_LOCAL uint32_t i_mask; /* memory.c */ extern uint64_t psx_get_cycle_count(void); extern void psx_get_freeze_diag(uint64_t *total_checks, @@ -150,7 +150,7 @@ static int s_sym_initialized = 0; #define RING_CAP 64 typedef struct { uint64_t frame_count; - PSX_THREAD_LOCAL uint64_t psx_cycle_count; + uint64_t psx_cycle_count; uint64_t exc_reentry; uint64_t dirty_ram_insns; uint32_t current_func; diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 94c8c19e..1f9c4c7e 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -4912,7 +4912,7 @@ static inline GpuContextType_lcf *gpu_context_lcf(void) { * raise the VBLANK bit when a synthetic vblank fires from a tight * BIOS-shell GPUSTAT poll loop, without going through the * vblank-callback path (which would call SDL_Delay/Sleep). */ -extern uint32_t i_stat; +extern PSX_THREAD_LOCAL uint32_t i_stat; /* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */ extern void psx_irq_raise(uint32_t bit, uint32_t detail); @@ -8468,8 +8468,8 @@ uint32_t gpu_get_opcode_count(uint8_t op) { return (*gpu_context_gp0_opcode_coun extern uint64_t s_frame_count; /* defined in debug_server.c */ #ifndef PSX_NO_DEBUG_TOOLS -extern uint32_t g_debug_last_store_pc; /* defined in debug_server.c */ -extern uint32_t g_debug_current_func_addr; /* defined in debug_server.c */ +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; /* defined in debug_server.c */ +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; /* defined in debug_server.c */ uint32_t debug_guest_ra(void); /* accessor in debug_server.c (guest $ra) */ uint32_t debug_guest_sp(void); /* accessor in debug_server.c (guest $sp) */ extern uint8_t *memory_get_ram_ptr(void); /* raw 2MB main-RAM base (no lockstep) */ diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index 0320d767..9233a015 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -21,7 +21,7 @@ extern "C" int gpu_ws_precise_nclip_enabled(void); * plus native replay of one candidate) is running. */ extern "C" int psx_overlay_shadow_diff_active(void); extern "C" uint64_t s_frame_count; /* debug_server.c */ -extern "C" uint32_t g_debug_last_store_pc; /* debug_server.c */ +extern "C" PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; /* debug_server.c */ extern "C" { PSX_THREAD_LOCAL int (*g_psx_projection_command)(CPUState*, uint32_t) = nullptr; } namespace PSXRecomp { @@ -686,7 +686,7 @@ extern "C" void gte_ws_set_dome_expand(int on, int aspect_num, int aspect_den) { // projects FAR (SZ >= threshold) vertices, so the sky-dome draw function can be // identified as the top far-vertex emitter — the target for the per-function // dome-expand bracket (the clean alternative to the scene-dependent depth gate). -extern "C" { extern uint32_t g_debug_current_func_addr; } +extern "C" { extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; } static PSX_THREAD_LOCAL uint32_t s_gte_caller_ra = 0; /* guest ra at gte_execute = return into the GAME fn that issued the projection (not the libgte leaf) */ #define WS_DOME_CALL_SITES_MAX 32 static PSX_THREAD_LOCAL uint32_t s_ws_dome_call_sites[WS_DOME_CALL_SITES_MAX]; diff --git a/runtime/src/guest_cycle_profile.c b/runtime/src/guest_cycle_profile.c index aa97235a..2af3c5bc 100644 --- a/runtime/src/guest_cycle_profile.c +++ b/runtime/src/guest_cycle_profile.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* guest_cycle_profile.c - always-on guest-cycle profile ring. * * Answers "where did the guest CPU spend its cycles between frame A and @@ -35,7 +36,7 @@ extern uint64_t s_frame_count; extern volatile uint32_t g_psx_last_fn_entry; extern volatile uint32_t g_psx_last_block; -extern uint32_t g_debug_current_func_addr; +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; extern uint32_t debug_guest_ra(void); extern uint32_t overlay_loader_native_inprogress(void); extern int psx_exec_phase(void); diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index 275c9a02..021b9535 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* * interrupts.c — v4 interrupt delivery for recompiled BIOS. * @@ -124,8 +125,8 @@ static void irq_record_outcome(uint8_t kind, uint8_t detail, uint32_t aux) { #define COP0_EPC 14 /* I_STAT and I_MASK are owned by memory.c */ -extern uint32_t i_stat; -extern uint32_t i_mask; +extern PSX_THREAD_LOCAL uint32_t i_stat; +extern PSX_THREAD_LOCAL uint32_t i_mask; /* Device-event cycle ring (device_trace.c): every hardware IRQ-raise edge is * recorded here with its guest cycle. */ @@ -159,7 +160,7 @@ extern uint32_t i_mask; * * Derived from PR #102 by Alexandros Mandravillis; the mirror call sites and * the single-owner refactor are ours. */ -static uint32_t *s_cause_ptr; +static PSX_THREAD_LOCAL uint32_t *s_cause_ptr; void psx_irq_refresh_cause_ip2(void) { @@ -186,7 +187,7 @@ void psx_irq_raise(uint32_t bit, uint32_t detail) { i_stat |= (1u << bit); psx_irq_refresh_cause_ip2(); - device_trace_note(bit, detail); + if (!g_psx_render_pass_active) device_trace_note(bit, detail); } /* Dispatch counter for vblank scheduling. */ @@ -235,7 +236,7 @@ typedef struct { static CosimIrqEntry s_cosim_irq_ring[COSIM_IRQ_RING_CAP]; static uint64_t s_cosim_irq_seq; extern int g_cosim_dirty_pump_site; -extern uint32_t g_debug_current_func_addr; +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; extern uint32_t cosim_last_block(void); extern uint32_t overlay_loader_get_inprogress(void); @@ -625,7 +626,7 @@ uint64_t psx_exception_setjmp_epoch(void) { return g_exc_setjmp_epoch; } * the thread scheduler is fiber-based everywhere. */ void* g_exception_owner_fiber = NULL; int g_pending_exception_longjmp = 0; -extern int g_psx_dispatch_depth; +extern PSX_THREAD_LOCAL int g_psx_dispatch_depth; /* Set by psx_check_interrupts_at while a compiled block-leader interrupt check * is in progress. If the delivered handler later RFEs to the sentinel outside @@ -676,7 +677,7 @@ int psx_irq_resume_context_snapshot_safe_at(uint32_t resume_pc) * dirty resume PC is the same PC the caller intends to serialize. Keep * call-return and precise-IRQ pump sites rejected; those are the contexts * that can pair a plausible resume PC with stale nested-call registers. */ - extern int g_call_unit_depth; + extern PSX_THREAD_LOCAL int g_call_unit_depth; if (in_exception || g_call_unit_depth != 0 || g_psx_dispatch_depth > 1) return 0; if (resume_pc == 0u || (resume_pc & 3u) != 0u) @@ -1553,7 +1554,7 @@ irq_deliver_eval: * clobber window because the CPU isn't stalled / the event wasn't torn down. */ { extern int dma_cdrom_transfer_active(void); - extern int g_dma_exec_depth; + extern PSX_THREAD_LOCAL int g_dma_exec_depth; extern uint64_t s_frame_count; g_irqctx_ring[g_irqctx_seq & (IRQCTX_RING_CAP - 1u)] = (IrqCtxEntry){ .seq = g_irqctx_seq, .cycle = psx_get_cycle_count(), @@ -2146,7 +2147,7 @@ irq_deliver_eval: * live GPRs when the IRQ was taken deep inside a nested call (the * async-poll callback) — switching there commits a poisoned TCB context * and the resumed thread faults on a smeared jumptable index. */ - extern int g_call_unit_depth; + extern PSX_THREAD_LOCAL int g_call_unit_depth; int at_outermost = (g_psx_dispatch_depth == 0 && g_call_unit_depth == 0); /* Defer only when nested AND the outgoing thread has a valid resume PC * of its OWN (g_exception_real_epc — the architectural EPC latched for diff --git a/runtime/src/load_accel.c b/runtime/src/load_accel.c index 3a43a141..962d32cf 100644 --- a/runtime/src/load_accel.c +++ b/runtime/src/load_accel.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* load_accel.c -- verified enhancement-phase load-time acceleration. * * PsyQ's VSync(-1) is used as a timeout clock by CD_sync/CD_ready loops. The @@ -23,7 +24,7 @@ #include -extern uint32_t g_debug_last_store_pc; +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; extern int g_precise_mode; static int s_enabled = 1; diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 8c884aa3..2b698f63 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -267,7 +267,7 @@ extern "C" { extern uint32_t g_slice_exit_pc, g_slice_exit_reason, g_slice_exit_iter; extern uint32_t g_slice_exit_dispatchable, g_slice_exit_dirty, g_slice_exit_in_text, g_slice_exit_want; /* memory.c */ - extern uint32_t i_stat, i_mask; + extern PSX_THREAD_LOCAL uint32_t i_stat, i_mask; extern uint64_t g_guest_store_count; extern uint64_t g_vblank_ack_count; /* interrupts.c */ @@ -276,9 +276,9 @@ extern "C" { extern uint64_t g_dirty_ram_blocks_run; extern uint64_t g_dirty_pump_count; /* overlay_loader.c */ - extern int g_call_unit_depth; + extern PSX_THREAD_LOCAL int g_call_unit_depth; /* psx_bios_backend.c */ - extern int g_psx_dispatch_depth; + extern PSX_THREAD_LOCAL int g_psx_dispatch_depth; } /* memory.c */ @@ -844,7 +844,7 @@ static void post_load_probe_on_vblank(int turbo_active, int present_reached) { extern uint64_t g_vblank_raise_count, g_vblank_deliver_count, g_vblank_ack_count; extern uint64_t g_dirty_ram_blocks_run; - extern uint32_t i_stat, i_mask; + extern PSX_THREAD_LOCAL uint32_t i_stat, i_mask; extern CPUState *debug_cpu_ptr; const uint64_t vb_r = g_vblank_raise_count - s_post_load_probe_vb_raise0; const uint64_t vb_d = g_vblank_deliver_count - s_post_load_probe_vb_deliv0; @@ -963,9 +963,9 @@ static void post_load_probe_on_vblank(int turbo_active, int present_reached) { const uint32_t irq_pc = psx_last_irq_check_pc(); const uint32_t resume_pc = psx_compiled_irq_resume_pc(); - extern uint32_t g_debug_current_func_addr; - extern uint32_t g_debug_last_store_pc; - extern int g_psx_dispatch_depth; + extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; + extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; + extern PSX_THREAD_LOCAL int g_psx_dispatch_depth; const uint32_t func = g_debug_current_func_addr; const uint32_t store_pc = g_debug_last_store_pc; const uint32_t live_pc = debug_cpu_ptr ? debug_cpu_ptr->pc : 0u; @@ -20412,10 +20412,10 @@ session_reboot: psx_netplay_cold_reset(); { extern uint64_t s_frame_count; - extern uint32_t g_debug_current_func_addr; - extern uint32_t g_debug_last_store_pc; - extern int g_call_unit_depth; - extern int g_psx_dispatch_depth; + extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; + extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; + extern PSX_THREAD_LOCAL int g_call_unit_depth; + extern PSX_THREAD_LOCAL int g_psx_dispatch_depth; s_frame_count = 0; g_debug_current_func_addr = 0; g_debug_last_store_pc = 0; @@ -21890,7 +21890,7 @@ session_reboot: * longjmp fixup). */ if (rematch_session) { extern PSX_THREAD_LOCAL int psx_in_device_service; - extern int g_call_unit_depth; + extern PSX_THREAD_LOCAL int g_call_unit_depth; char stall[96]; uint32_t sim = 0; int lead = 0; diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 4e5c3a73..3af8d853 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -28,6 +28,7 @@ #include "psx_cycles.h" #include "psx_icache.h" #include "psx_memory.h" +#include "psx_execution.h" #include "pgxp.h" #include "render_pass.h" #include "render_pass_plan.h" @@ -279,7 +280,7 @@ void memory_clear_low_boot_scratch(void) { #define RAM_LIVE (g_psx_ram_size) #define DIRTY_RAM_LIVE_BITMAP_WORDS \ (((RAM_LIVE >> DIRTY_RAM_PAGE_SHIFT) + 31u) / 32u) -static uint32_t dirty_ram_bitmap[DIRTY_RAM_BITMAP_WORDS]; +static PSX_THREAD_LOCAL uint32_t dirty_ram_bitmap[DIRTY_RAM_BITMAP_WORDS]; /* Monotonic generation for RAM-resident CODE changes (kernel install-stub * writes + DMA/EXE loads that mark executable ranges). Consumers that cache @@ -330,24 +331,24 @@ static inline void dirty_ram_mark_page(uint32_t phys) { #include "psx_bios_image.h" #include "kernel_patch_ranges.h" -static uint32_t s_kb_lo = 0, s_kb_span = 0, s_kb_rom_off = 0; +static PSX_THREAD_LOCAL uint32_t s_kb_lo = 0, s_kb_span = 0, s_kb_rom_off = 0; -static const PsxKernelPatchRange* s_kb_pr = 0; -static uint32_t s_kb_pr_n = 0; -static uint64_t kbless_patch_skips = 0; /* segments skipped, TCP counter */ +static PSX_THREAD_LOCAL const PsxKernelPatchRange* s_kb_pr = 0; +static PSX_THREAD_LOCAL uint32_t s_kb_pr_n = 0; +static PSX_THREAD_LOCAL uint64_t kbless_patch_skips = 0; /* segments skipped, TCP counter */ #define KBLESS_UNKNOWN 0u #define KBLESS_CLEAN 1u #define KBLESS_MISMATCH 2u #define KBLESS_MAX_ENTRIES 4096u -static uint8_t kbless_state[KBLESS_MAX_ENTRIES]; /* parallel to bodies[] */ -static int kbless_enabled = -1; /* env PSX_KERNEL_BLESS=0 disables */ +static PSX_THREAD_LOCAL uint8_t kbless_state[KBLESS_MAX_ENTRIES]; /* parallel to bodies[] */ +static PSX_THREAD_LOCAL int kbless_enabled = -1; /* env PSX_KERNEL_BLESS=0 disables */ /* Always-on counters (TCP kernel_bless). */ -static uint64_t kbless_native_hits = 0; -static uint64_t kbless_verifies = 0; -static uint64_t kbless_mismatches = 0; -static uint64_t kbless_invalidations = 0; +static PSX_THREAD_LOCAL uint64_t kbless_native_hits = 0; +static PSX_THREAD_LOCAL uint64_t kbless_verifies = 0; +static PSX_THREAD_LOCAL uint64_t kbless_mismatches = 0; +static PSX_THREAD_LOCAL uint64_t kbless_invalidations = 0; static int kbless_on(void) { if (kbless_enabled < 0) { @@ -582,18 +583,18 @@ void dirty_ram_clear_image_baseline(void) { * never frees it). Intentional data patches by the translation layer are blessed * into it via dirty_ram_text_bless so they are not mistaken for self-modifying * code — see dirty_ram_text_native_ok / dirty_ram_text_bless. */ -static uint8_t *text_ref_image = NULL; -static uint32_t text_ref_lo = 0, text_ref_hi = 0; -static uint32_t text_modified_bitmap[DIRTY_RAM_BITMAP_WORDS]; -static uint32_t text_diverged_bitmap[DIRTY_RAM_BITMAP_WORDS]; -static uint64_t g_text_native_blocked = 0; -static uint32_t g_text_diverged_pages = 0; -static uint64_t g_text_exact_mismatches = 0; -static uint32_t g_text_exact_last_range_lo = 0; -static uint32_t g_text_exact_last_range_len = 0; -static uint32_t g_text_exact_last_mismatch = 0; -static uint32_t g_text_exact_last_live = 0; -static uint32_t g_text_exact_last_ref = 0; +static PSX_THREAD_LOCAL uint8_t *text_ref_image = NULL; +static PSX_THREAD_LOCAL uint32_t text_ref_lo = 0, text_ref_hi = 0; +static PSX_THREAD_LOCAL uint32_t text_modified_bitmap[DIRTY_RAM_BITMAP_WORDS]; +static PSX_THREAD_LOCAL uint32_t text_diverged_bitmap[DIRTY_RAM_BITMAP_WORDS]; +static PSX_THREAD_LOCAL uint64_t g_text_native_blocked = 0; +static PSX_THREAD_LOCAL uint32_t g_text_diverged_pages = 0; +static PSX_THREAD_LOCAL uint64_t g_text_exact_mismatches = 0; +static PSX_THREAD_LOCAL uint32_t g_text_exact_last_range_lo = 0; +static PSX_THREAD_LOCAL uint32_t g_text_exact_last_range_len = 0; +static PSX_THREAD_LOCAL uint32_t g_text_exact_last_mismatch = 0; +static PSX_THREAD_LOCAL uint32_t g_text_exact_last_live = 0; +static PSX_THREAD_LOCAL uint32_t g_text_exact_last_ref = 0; void dirty_ram_register_text_image(uint32_t phys_lo, const uint8_t *bytes, uint32_t len) { @@ -902,8 +903,8 @@ void dirty_ram_set_bitmap_words(const uint32_t* words, uint32_t count) { * The bitmap is almost always empty, so the per-store cost on the common path * is a single bitmap lookup. */ -static uint32_t overlay_watch_bitmap[DIRTY_RAM_BITMAP_WORDS]; -static uint32_t overlay_page_gen[DIRTY_RAM_PAGE_COUNT]; +static PSX_THREAD_LOCAL uint32_t overlay_watch_bitmap[DIRTY_RAM_BITMAP_WORDS]; +static PSX_THREAD_LOCAL uint32_t overlay_page_gen[DIRTY_RAM_PAGE_COUNT]; void dirty_ram_reset_for_boot(void) { memset(dirty_ram_bitmap, 0, sizeof(dirty_ram_bitmap)); @@ -1039,24 +1040,24 @@ static inline void overlay_watch_note_write(uint32_t phys, uint32_t size) { /* Memory control registers: 0x1F801000..0x1F80103F (16 words) + 0x1F801060 (RAM size). * Includes expansion base/size, COM_DELAY, SPU_DELAY, CDROM_DELAY etc. */ -static uint32_t mem_ctrl[16]; /* indices 0..15 → addresses 0x1F801000..0x1F80103C */ -static uint32_t ram_size_reg; /* 0x1F801060 */ +static PSX_THREAD_LOCAL uint32_t mem_ctrl[16]; /* indices 0..15 → addresses 0x1F801000..0x1F80103C */ +static PSX_THREAD_LOCAL uint32_t ram_size_reg; /* 0x1F801060 */ /* KSEG2 no-op access telemetry (see the guards in the accessors). */ uint64_t g_kseg2_ignored_reads; uint64_t g_kseg2_ignored_writes; /* Cache control register (KSEG2: 0xFFFE0130). */ -static uint32_t cache_ctrl; +static PSX_THREAD_LOCAL uint32_t cache_ctrl; /* Pointer to cpu->cop0[12] (SR). Set once at init. * Used by write functions to check the IsC (Isolate Cache) bit: while it is * set, a CPU store goes to the caches and never reaches the bus (isc_store). */ -static const uint32_t *sr_ptr; +static PSX_THREAD_LOCAL const uint32_t *sr_ptr; /* Interrupt controller — non-static so hardware subsystems can set I_STAT bits. */ -uint32_t i_stat; /* 0x1F801070 — interrupt status (AND-acknowledge semantics) */ -uint32_t i_mask; /* 0x1F801074 — interrupt enable mask */ +PSX_THREAD_LOCAL uint32_t i_stat; /* 0x1F801070 — interrupt status (AND-acknowledge semantics) */ +PSX_THREAD_LOCAL uint32_t i_mask; /* 0x1F801074 — interrupt enable mask */ /* Shadow-diff device-access detector. run_shadow_diff arms g_shadow_mmio_watch * around its single authoritative (interpreter) probe pass; ANY MMIO touch bumps @@ -1072,8 +1073,8 @@ uint64_t g_shadow_mmio_hits = 0; * time-varying or side-effecting values (timer counters, CD response FIFO), * so any touch disqualifies the window. One increment per access. */ uint64_t g_mmio_access_count = 0; -#define SHADOW_NOTE_MMIO() do { g_mmio_access_count++; \ - g_shadow_mmio_hits += (uint64_t)g_shadow_mmio_watch; } while (0) +#define SHADOW_NOTE_MMIO() do { if (s_backing == &s_live_backing) { \ + g_mmio_access_count++; g_shadow_mmio_hits += (uint64_t)g_shadow_mmio_watch; } } while (0) /* Always-on guest store counter, bumped at the top of every raw store * (RAM, scratchpad, MMIO, even discarded KSEG2/IsC stores — over-counting @@ -1108,8 +1109,8 @@ static int imask_bit7_set_count = 0; static int imask_bit7_clear_count = 0; static void imask_trace_record(uint32_t old_val, uint32_t new_val, uint8_t width) { - extern uint32_t g_debug_current_func_addr; - extern uint32_t g_debug_last_store_pc; + extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; + extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; extern int psx_get_in_exception(void); ImaskTraceEntry *e = &imask_trace[imask_trace_idx]; e->old_mask = old_val; @@ -1177,7 +1178,7 @@ extern void debug_server_trace_ram_read_watch(uint32_t phys, uint32_t val); extern void debug_server_trace_entryint_write(uint32_t phys, uint32_t old_val, uint32_t new_val, uint8_t width); extern CPUState *debug_cpu_ptr; -extern uint32_t g_debug_last_store_pc; +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; /* Parity last-writer provenance (parity_trace.c): note every main-RAM write so * the watch-word last-writer table tracks the exact producing store. No-op @@ -1187,8 +1188,8 @@ extern void parity_trace_note_write(uint32_t addr, uint32_t width, uint32_t writ /* Effective writer PC for provenance: during a DMA transfer the last CPU-store * PC is stale/unrelated, so attribute DMA-sourced RAM writes to the PC that * kicked the DMA (dma.c). Matches the wtrace recorder's DMA attribution. */ -extern int g_dma_exec_depth; -extern uint32_t g_dma_initiator_pc; +extern PSX_THREAD_LOCAL int g_dma_exec_depth; +extern PSX_THREAD_LOCAL uint32_t g_dma_initiator_pc; static inline uint32_t effective_store_pc(void) { return (g_dma_exec_depth > 0 && g_dma_initiator_pc) ? g_dma_initiator_pc : g_debug_last_store_pc; @@ -1317,6 +1318,18 @@ static void unmapped_fatal(uint32_t vaddr, uint32_t phys, const char* op) { /* --- MMIO read/write helpers --- */ +static int private_mmio_read_allowed(uint32_t addr) { + if (s_backing == &s_live_backing) return 1; + if ((addr >= 0x1F801040u && addr <= 0x1F80105Fu) || + (addr >= 0x1F801800u && addr <= 0x1F801803u) || + (addr >= 0x1F801820u && addr <= 0x1F801827u) || + (addr >= 0x1F801C00u && addr <= 0x1F801FFFu)) { + if (g_psx_execution_abort) g_psx_execution_abort("device_read", addr); + abort(); /* A private execution must install a non-returning landing. */ + } + return 1; +} + static uint32_t mmio_read32_impl(uint32_t addr) { SHADOW_NOTE_MMIO(); /* Memory control: 0x1F801000..0x1F801020 */ @@ -1370,6 +1383,10 @@ static uint32_t mmio_read32_impl(uint32_t addr) { * single (side-effecting) read. Callers use mmio_read32; the body is _impl, so * the device read executes exactly once. */ static uint32_t mmio_read32(uint32_t addr) { + if (s_backing != &s_live_backing) { + private_mmio_read_allowed(addr); + return mmio_read32_impl(addr); + } if (g_psx_render_pass_active && !render_pass_mmio_read_allowed(addr, 4)) return 0; psx_devices_mmio_sync(); uint32_t v = mmio_read32_impl(addr); @@ -1380,7 +1397,7 @@ static uint32_t mmio_read32(uint32_t addr) { static void mmio_write32(uint32_t addr, uint32_t val) { psx_devices_mmio_sync(); SHADOW_NOTE_MMIO(); - debug_server_trace_mmio_write(addr, val, 4); + if (s_backing == &s_live_backing) debug_server_trace_mmio_write(addr, val, 4); /* Memory control: 0x1F801000..0x1F801020 */ if (addr >= 0x1F801000u && addr <= 0x1F80103Cu) { mem_ctrl[(addr - 0x1F801000u) >> 2] = val; @@ -1493,6 +1510,10 @@ static uint16_t mmio_read16_impl(uint32_t addr) { } static uint16_t mmio_read16(uint32_t addr) { + if (s_backing != &s_live_backing) { + private_mmio_read_allowed(addr); + return mmio_read16_impl(addr); + } if (g_psx_render_pass_active && !render_pass_mmio_read_allowed(addr, 2)) return 0; psx_devices_mmio_sync(); uint16_t v = mmio_read16_impl(addr); @@ -1503,7 +1524,7 @@ static uint16_t mmio_read16(uint32_t addr) { static void mmio_write16(uint32_t addr, uint16_t val) { psx_devices_mmio_sync(); SHADOW_NOTE_MMIO(); - debug_server_trace_mmio_write(addr, (uint32_t)val, 2); + if (s_backing == &s_live_backing) debug_server_trace_mmio_write(addr, (uint32_t)val, 2); /* Memory control: 0x1F801000..0x1F80103C — halfword lane RMW. */ if (addr >= 0x1F801000u && addr <= 0x1F80103Fu) { uint32_t idx = (addr - 0x1F801000u) >> 2; @@ -1613,6 +1634,10 @@ static uint8_t mmio_read8_impl(uint32_t addr) { } static uint8_t mmio_read8(uint32_t addr) { + if (s_backing != &s_live_backing) { + private_mmio_read_allowed(addr); + return mmio_read8_impl(addr); + } if (g_psx_render_pass_active && !render_pass_mmio_read_allowed(addr, 1)) return 0; psx_devices_mmio_sync(); uint8_t v = mmio_read8_impl(addr); @@ -1623,7 +1648,7 @@ static uint8_t mmio_read8(uint32_t addr) { static void mmio_write8(uint32_t addr, uint8_t val) { psx_devices_mmio_sync(); SHADOW_NOTE_MMIO(); - debug_server_trace_mmio_write(addr, (uint32_t)val, 1); + if (s_backing == &s_live_backing) debug_server_trace_mmio_write(addr, (uint32_t)val, 1); /* Memory control: 0x1F801000..0x1F80103C — byte lane RMW. Tomba2's late * attract byte-writes the EXP1 base register at 0x1F801000; only the * 32-bit path covered the block and the byte path fatal'd. */ @@ -1708,7 +1733,7 @@ static void mmio_write8(uint32_t addr, uint8_t val) { * (psx_get_in_exception): the replay runs only the block, never the handler. */ static int s_ls_op_active = 0; extern int g_ls_suppress_record; -extern int g_dma_exec_depth; +extern PSX_THREAD_LOCAL int g_dma_exec_depth; extern int psx_get_in_exception(void); static uint32_t psx_read_word_raw(uint32_t addr); /* data-shard capture feed (data_shards.c): record guest reads/writes while a @@ -1716,6 +1741,7 @@ static uint32_t psx_read_word_raw(uint32_t addr); * exception context (ISR work replays live), never under DMA (DMA writes * poison the window via ds_note_dma_write instead). */ uint32_t psx_read_word(uint32_t addr) { + if (s_backing != &s_live_backing) return psx_read_word_raw(addr); if (g_ls_mode == 2) return ls_read_hook(addr, 4, 0u); if (g_ds_recording && g_dma_exec_depth == 0 && !psx_get_in_exception()) ds_note_read(addr, 4); @@ -1758,7 +1784,7 @@ static uint32_t psx_read_word_raw(uint32_t addr) { memcpy(&v, &ram[phys], sizeof(v)); /* Targeted main-RAM read watch (debug). Flag is 0 in normal runs, so the * hot read path pays only a predictable branch. */ - if (g_ram_read_watch_active) debug_server_trace_ram_read_watch(phys, v); + if (s_backing == &s_live_backing && g_ram_read_watch_active) debug_server_trace_ram_read_watch(phys, v); return v; } { @@ -1820,11 +1846,11 @@ typedef struct { } D44Entry; D44Entry g_d44_ring[D44_RING_CAP]; uint64_t g_d44_seq = 0; -extern uint32_t g_debug_last_store_pc; -extern int g_dma_exec_depth; /* >0 while a DMA is moving data (dma.c) */ -extern int g_dma_cur_ch; /* channel of the in-flight DMA, else -1 */ -extern uint32_t g_dma_cur_madr; /* current MADR of the in-flight DMA */ -extern uint32_t g_dma_cur_bcr; /* BCR of the in-flight DMA */ +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; +extern PSX_THREAD_LOCAL int g_dma_exec_depth; /* >0 while a DMA is moving data (dma.c) */ +extern PSX_THREAD_LOCAL int g_dma_cur_ch; /* channel of the in-flight DMA, else -1 */ +extern PSX_THREAD_LOCAL uint32_t g_dma_cur_madr; /* current MADR of the in-flight DMA */ +extern PSX_THREAD_LOCAL uint32_t g_dma_cur_bcr; /* BCR of the in-flight DMA */ extern uint64_t s_frame_count; static inline void d44_note(uint32_t phys, uint32_t old, uint32_t val) { if (phys != D44_PHYS) return; @@ -1866,7 +1892,7 @@ uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES]; * writes RAM directly, so IsC does not apply while a transfer moves data. * Neither is a host store (psx_host_write_*: mods, FMV skip, debug pokes, * enhancement fills); like DMA it reaches memory whatever SR says. */ -int g_host_store_depth; /* >0 inside psx_host_write_* */ +PSX_THREAD_LOCAL int g_host_store_depth; /* >0 inside psx_host_write_* */ /* DMA and host stores have no instruction hook to carry or reset a PGXP * shadow. Validation on read cannot catch one that rewrites the identical @@ -1925,6 +1951,7 @@ uint64_t g_render_pass_mod_pages = 0; /* pages journaled, total */ static int rp_mod_journal(uint8_t *base, uint32_t page_base, uint32_t off, uint32_t width) { + if (s_backing != &s_live_backing) return 1; for (uint32_t pg = off / RP_MOD_PAGE; pg <= (off + width - 1u) / RP_MOD_PAGE; pg++) { const uint32_t index = page_base + pg; const uint8_t bit = (uint8_t)(1u << (index & 7u)); @@ -2001,12 +2028,12 @@ static void render_pass_store(uint32_t addr, uint32_t val, uint32_t width) { if (t.isolate_cache) isc_store(addr, val, width); t.mmio_write = render_pass_mmio_write; cls = render_pass_store_to(&t, addr, val, width); - if (cls >= 0) g_render_pass_dropped_writes[cls]++; + if (cls >= 0 && s_backing == &s_live_backing) g_render_pass_dropped_writes[cls]++; } static void psx_write_word_raw(uint32_t addr, uint32_t val); void psx_write_word(uint32_t addr, uint32_t val) { - extern void (*g_overlay_flush_pending_cycles)(void); + extern PSX_THREAD_LOCAL void (*g_overlay_flush_pending_cycles)(void); if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles(); if (g_psx_render_pass_active) { render_pass_store(addr, val, 4); return; } if (g_ls_mode == 2) { ls_write_hook(addr, 4, val); return; } @@ -2173,6 +2200,7 @@ static void psx_write_word_raw(uint32_t addr, uint32_t val) { static uint16_t psx_read_half_raw(uint32_t addr); uint16_t psx_read_half(uint32_t addr) { + if (s_backing != &s_live_backing) return psx_read_half_raw(addr); if (g_ls_mode == 2) return (uint16_t)ls_read_hook(addr, 2, 0u); if (g_ds_recording && g_dma_exec_depth == 0 && !psx_get_in_exception()) ds_note_read(addr, 2); @@ -2221,7 +2249,7 @@ static uint16_t psx_read_half_raw(uint32_t addr) { static void psx_write_half_raw(uint32_t addr, uint16_t val); void psx_write_half(uint32_t addr, uint16_t val) { - extern void (*g_overlay_flush_pending_cycles)(void); + extern PSX_THREAD_LOCAL void (*g_overlay_flush_pending_cycles)(void); if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles(); if (g_psx_render_pass_active) { render_pass_store(addr, val, 2); return; } if (g_ls_mode == 2) { ls_write_hook(addr, 2, val); return; } @@ -2302,6 +2330,7 @@ static void psx_write_half_raw(uint32_t addr, uint16_t val) { static uint8_t psx_read_byte_raw(uint32_t addr); uint8_t psx_read_byte(uint32_t addr) { + if (s_backing != &s_live_backing) return psx_read_byte_raw(addr); if (g_ls_mode == 2) return (uint8_t)ls_read_hook(addr, 1, 0u); if (g_ls_mode != 1 || s_ls_op_active || g_ls_suppress_record || g_dma_exec_depth > 0) return psx_read_byte_raw(addr); s_ls_op_active = 1; @@ -2576,7 +2605,7 @@ uint8_t psx_guest_read_byte(uint32_t addr) { return psx_read_byte(addr); } static void psx_write_byte_raw(uint32_t addr, uint8_t val); void psx_write_byte(uint32_t addr, uint8_t val) { - extern void (*g_overlay_flush_pending_cycles)(void); + extern PSX_THREAD_LOCAL void (*g_overlay_flush_pending_cycles)(void); if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles(); if (g_psx_render_pass_active) { render_pass_store(addr, val, 1); return; } if (g_ls_mode == 2) { ls_write_hook(addr, 1, val); return; } @@ -2663,3 +2692,76 @@ static void psx_write_byte_raw(uint32_t addr, uint8_t val) { } unmapped_fatal(addr, phys, "WRITE"); } + +struct PsxMemoryExecutionState { size_t bytes; unsigned char data[]; }; +typedef struct { void *address; size_t bytes; } MemoryExecutionRegion; +static size_t memory_execution_regions(MemoryExecutionRegion *regions) { + size_t n = 0; +#define MEMORY_REGION(name) do { regions[n].address = &(name); regions[n++].bytes = sizeof(name); } while (0) + MEMORY_REGION(dirty_ram_bitmap); + MEMORY_REGION(s_kb_lo); + MEMORY_REGION(s_kb_span); + MEMORY_REGION(s_kb_rom_off); + MEMORY_REGION(s_kb_pr); + MEMORY_REGION(s_kb_pr_n); + MEMORY_REGION(kbless_patch_skips); + MEMORY_REGION(kbless_state); + MEMORY_REGION(kbless_enabled); + MEMORY_REGION(kbless_native_hits); + MEMORY_REGION(kbless_verifies); + MEMORY_REGION(kbless_mismatches); + MEMORY_REGION(kbless_invalidations); + MEMORY_REGION(text_ref_image); + MEMORY_REGION(text_ref_lo); + MEMORY_REGION(text_ref_hi); + MEMORY_REGION(text_modified_bitmap); + MEMORY_REGION(text_diverged_bitmap); + MEMORY_REGION(g_text_native_blocked); + MEMORY_REGION(g_text_diverged_pages); + MEMORY_REGION(g_text_exact_mismatches); + MEMORY_REGION(g_text_exact_last_range_lo); + MEMORY_REGION(g_text_exact_last_range_len); + MEMORY_REGION(g_text_exact_last_mismatch); + MEMORY_REGION(g_text_exact_last_live); + MEMORY_REGION(g_text_exact_last_ref); + MEMORY_REGION(overlay_watch_bitmap); + MEMORY_REGION(overlay_page_gen); + MEMORY_REGION(mem_ctrl); + MEMORY_REGION(ram_size_reg); + MEMORY_REGION(cache_ctrl); + MEMORY_REGION(sr_ptr); + MEMORY_REGION(i_stat); + MEMORY_REGION(i_mask); +#undef MEMORY_REGION + return n; +} +PsxMemoryExecutionState *memory_execution_create(void) { + MemoryExecutionRegion regions[64]; + size_t n = memory_execution_regions(regions), bytes = 0; + for (size_t i = 0; i < n; ++i) bytes += regions[i].bytes; + PsxMemoryExecutionState *state = (PsxMemoryExecutionState *)malloc(sizeof(*state) + bytes); + if (state) state->bytes = bytes; + return state; +} +void memory_execution_destroy(PsxMemoryExecutionState *state) { free(state); } +void memory_execution_capture(PsxMemoryExecutionState *state) { + MemoryExecutionRegion regions[64]; + size_t n = memory_execution_regions(regions), offset = 0; + if (!state || s_backing != &s_live_backing) abort(); + for (size_t i = 0; i < n; ++i) { + if (offset + regions[i].bytes > state->bytes) abort(); + memcpy(state->data + offset, regions[i].address, regions[i].bytes); + offset += regions[i].bytes; + } +} +void memory_execution_bind(const PsxMemoryExecutionState *state, const uint32_t *status) { + MemoryExecutionRegion regions[64]; + size_t n = memory_execution_regions(regions), offset = 0; + if (!state || s_backing == &s_live_backing) abort(); + for (size_t i = 0; i < n; ++i) { + if (offset + regions[i].bytes > state->bytes) abort(); + memcpy(regions[i].address, state->data + offset, regions[i].bytes); + offset += regions[i].bytes; + } + sr_ptr = status; /* Never retain the source CPU's status pointer. */ +} diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 5bfe6257..dba050e0 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -142,10 +142,19 @@ struct RuntimeMods { /* The plan is a netplay match's own-view set: its hooks run only inside * psx_mod_render_local_view, its vblank/savestate callbacks not at all. */ bool netplay_view_plan = false; +}; + +/* Callback ownership belongs to the guest execution thread. The active + * plan and its hook tables remain immutable until all executions stop. */ +struct ModExecutionContext { const ModResolution::Plugin* current_plugin = nullptr; CPUState* current_function_cpu = nullptr; bool current_function_finished = false; }; +ModExecutionContext& execution() { + static thread_local ModExecutionContext value; + return value; +} RuntimeMods& state() { static RuntimeMods value; @@ -253,23 +262,22 @@ public: * only entry hooks would let VBlank erase the interrupted plugin or complete * a function that belongs to another callback. */ class PluginCallbackScope { - RuntimeMods& runtime; const ModResolution::Plugin* previous_plugin; CPUState* previous_cpu; bool previous_finished; public: PluginCallbackScope(RuntimeMods& s, const ModResolution::Plugin* plugin, CPUState* cpu = nullptr) - : runtime(s), previous_plugin(s.current_plugin), - previous_cpu(s.current_function_cpu), previous_finished(s.current_function_finished) { - s.current_plugin = plugin; - s.current_function_cpu = cpu; - s.current_function_finished = false; + : previous_plugin(execution().current_plugin), + previous_cpu(execution().current_function_cpu), previous_finished(execution().current_function_finished) { + execution().current_plugin = plugin; + execution().current_function_cpu = cpu; + execution().current_function_finished = false; } ~PluginCallbackScope() { - runtime.current_plugin = previous_plugin; - runtime.current_function_cpu = previous_cpu; - runtime.current_function_finished = previous_finished; + execution().current_plugin = previous_plugin; + execution().current_function_cpu = previous_cpu; + execution().current_function_finished = previous_finished; } PluginCallbackScope(const PluginCallbackScope&) = delete; PluginCallbackScope& operator=(const PluginCallbackScope&) = delete; @@ -293,7 +301,7 @@ std::vector& active_function_entry_hooks() { static std::vector value; return value; } -unsigned function_entry_depth; +thread_local unsigned function_entry_depth; std::vector& active_guest_functions() { static std::vector value; @@ -2603,9 +2611,9 @@ extern "C" void mod_runtime_on_savestate_loaded(void) { } s.main_applied = true; for (const ModResolution::Plugin& plugin : s.plan.plugins) { - s.current_plugin = &plugin; + execution().current_plugin = &plugin; mod_invoke_savestate_plugin(plugin.id); - s.current_plugin = nullptr; + execution().current_plugin = nullptr; } if (applied) std::fprintf(stdout, @@ -2998,10 +3006,10 @@ extern "C" void psx_mod_set_disc_request_provider(PSXModDiscRequestProvider prov return; } RuntimeMods& s = state(); - if (!s.initialized || !s.plan.ok || !s.activating || !s.current_plugin || + if (!s.initialized || !s.plan.ok || !s.activating || !execution().current_plugin || s.netplay_view_plan || psx_netplay_active()) return; g_disc_request_provider = provider; - g_disc_request_owner = s.current_plugin; + g_disc_request_owner = execution().current_plugin; } extern "C" int mod_runtime_disc_request(int *ready) { @@ -3150,10 +3158,10 @@ extern "C" int psx_mod_current_resource_path(const char* resource_id, if (!resource_id || !resource_id[0] || !out || out_size == 0) return 0; RuntimeMods& s = state(); - if (!s.initialized || !s.plan.ok || !s.current_plugin) return 0; + if (!s.initialized || !s.plan.ok || !execution().current_plugin) return 0; for (const ModResolution::Resource& resource : s.plan.resources) { - if (resource.package_id != s.current_plugin->package_id || - resource.feature_id != s.current_plugin->feature_id || + if (resource.package_id != execution().current_plugin->package_id || + resource.feature_id != execution().current_plugin->feature_id || resource.id != resource_id) continue; const std::string text = resource.path.u8string(); @@ -3168,7 +3176,7 @@ extern "C" int psx_mod_current_option_value(const char* option_id, char* out, uint32_t out_size) { using namespace PSXRecompV4; if (out && out_size) out[0] = '\0'; - const auto* plugin = state().current_plugin; + const auto* plugin = execution().current_plugin; if (!plugin) return 0; return psx_mod_option_value(plugin->package_id.c_str(), plugin->feature_id.c_str(), option_id, out, out_size); @@ -3239,10 +3247,10 @@ extern "C" int psx_mod_current_resource_bytes(const char* resource_id, if (size) *size = 0; if (!resource_id || !*resource_id || !bytes || !size) return 0; const auto& s = state(); - if (!s.initialized || !s.plan.ok || !s.current_plugin) return 0; + if (!s.initialized || !s.plan.ok || !execution().current_plugin) return 0; for (const auto& resource : s.plan.resources) { - if (resource.package_id == s.current_plugin->package_id && - resource.feature_id == s.current_plugin->feature_id && + if (resource.package_id == execution().current_plugin->package_id && + resource.feature_id == execution().current_plugin->feature_id && resource.id == resource_id && resource.bytes) { *bytes = resource.bytes->data(); *size = resource.bytes->size(); @@ -3416,8 +3424,8 @@ extern "C" int psx_mod_finish_function(CPUState* cpu) { using namespace PSXRecompV4; RuntimeMods& s = state(); if (psx_mod_host_output_scope()) return 0; - if (!cpu || s.current_function_cpu != cpu || !s.current_plugin) return 0; - s.current_function_finished = true; + if (!cpu || execution().current_function_cpu != cpu || !execution().current_plugin) return 0; + execution().current_function_finished = true; return 1; } @@ -3535,7 +3543,7 @@ static void run_host_output_hooks(CPUState* cpu, uint32_t address) { ++s_host_output_depth; it->callback(&view, address); --s_host_output_depth; - s.current_function_finished = false; + execution().current_function_finished = false; } } @@ -3544,8 +3552,8 @@ static void run_host_output_hooks(CPUState* cpu, uint32_t address) { static bool mod_host_output_write_blocked(void) { const auto& s = PSXRecompV4::state(); return psx_mod_host_output_scope() || - (s.netplay_view_plan && s.current_plugin && - s.current_plugin->netplay_host_output); + (s.netplay_view_plan && PSXRecompV4::execution().current_plugin && + PSXRecompV4::execution().current_plugin->netplay_host_output); } extern "C" int psx_mod_function_entry(CPUState* cpu, uint32_t address) { @@ -3567,7 +3575,7 @@ extern "C" int psx_mod_function_entry(CPUState* cpu, uint32_t address) { if (it->callback) it->callback(cpu, address); /* Either completion form skips the body: an entry callback that * called psx_mod_finish_function(), or a filter returning nonzero. */ - const bool handled = s.current_function_finished || + const bool handled = execution().current_function_finished || (it->filter && it->filter(cpu, address)); --function_entry_depth; if (handled) { @@ -3640,17 +3648,17 @@ extern "C" int psx_mod_function_entry_active(void) { extern "C" void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out) { out->depth = PSXRecompV4::function_entry_depth; - out->plugin = PSXRecompV4::state().current_plugin; - out->cpu = PSXRecompV4::state().current_function_cpu; - out->finished = PSXRecompV4::state().current_function_finished; + out->plugin = PSXRecompV4::execution().current_plugin; + out->cpu = PSXRecompV4::execution().current_function_cpu; + out->finished = PSXRecompV4::execution().current_function_finished; } extern "C" void mod_runtime_function_entry_context_restore(const ModFunctionEntryContext *in) { PSXRecompV4::function_entry_depth = in->depth; - PSXRecompV4::state().current_plugin = + PSXRecompV4::execution().current_plugin = static_cast(in->plugin); - PSXRecompV4::state().current_function_cpu = in->cpu; - PSXRecompV4::state().current_function_finished = in->finished != 0; + PSXRecompV4::execution().current_function_cpu = in->cpu; + PSXRecompV4::execution().current_function_finished = in->finished != 0; } /* Apply the committed plan's disc writes and overlays to one sector. The diff --git a/runtime/src/netplay_state_digest.c b/runtime/src/netplay_state_digest.c index 52e57a7a..afd6bfc6 100644 --- a/runtime/src/netplay_state_digest.c +++ b/runtime/src/netplay_state_digest.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" #include "netplay_state_digest.h" #include "mod_netplay.h" #include "cdrom.h" @@ -14,8 +15,8 @@ extern uint8_t* memory_get_ram_ptr(void); extern uint32_t memory_get_ram_bytes(void); -extern uint32_t i_stat; -extern uint32_t i_mask; +extern PSX_THREAD_LOCAL uint32_t i_stat; +extern PSX_THREAD_LOCAL uint32_t i_mask; extern void timers_get_snapshot(uint16_t counter[3], uint32_t mode[3], uint16_t target[3], int32_t irq_line[3], uint32_t frac[3]); diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index 5929d3b0..6386e27f 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" #include "overlay_loader.h" #include "overlay_api.h" #include "overlay_path_canon.h" @@ -543,7 +544,7 @@ static uint32_t s_last_write_size = 0; * before the guest-visible write; block/device boundaries flush through the * DLL glue directly. */ static OverlayFlushFn s_dll_flush[4096]; -OverlayFlushFn g_overlay_flush_pending_cycles = NULL; +PSX_THREAD_LOCAL OverlayFlushFn g_overlay_flush_pending_cycles = NULL; static OverlayFlushFn overlay_flush_enter(const Candidate *c) { OverlayFlushFn prev = g_overlay_flush_pending_cycles; @@ -2268,8 +2269,8 @@ static int s_irq_suppress_cdrom_only = 0; static int s_irq_post_dispatch_pump = 0; static int s_irq_defer_cdrom = 0; -extern uint32_t i_stat; -extern uint32_t i_mask; +extern PSX_THREAD_LOCAL uint32_t i_stat; +extern PSX_THREAD_LOCAL uint32_t i_mask; /* Nested call-unit depth (Ape memcard native<->interp resume-desync fix). * @@ -2296,7 +2297,7 @@ extern uint32_t i_mask; * inside units; the blanket skip survives only behind * PSX_OVERLAY_UNIT_IRQ_DEFER=1 for A/B bisection (overlay_unit_defer.h). * Incremented only when the A/B toggle is on (below). */ -int g_call_unit_depth = 0; +PSX_THREAD_LOCAL int g_call_unit_depth = 0; /* A/B toggle for the nested-unit IRQ deferral (PSX_OVERLAY_UNIT_DEFER, default * ON). Gates the depth INCREMENTS (so =0 leaves g_call_unit_depth at 0 and @@ -2553,7 +2554,7 @@ static void init_callbacks(void) { * static and interpreted stores do, in every build, because memory.c's * store filters read it as well as the debug tooling. */ { - extern uint32_t g_debug_last_store_pc; + extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; s_callbacks.last_store_pc = &g_debug_last_store_pc; } /* Widescreen hooks (ABI v3): overlay-emitted psx_ws_* calls forward to @@ -4271,7 +4272,7 @@ retry_candidates: * the current activation, so we permanently demote that entry to interp. Called * from memory.c only when the written page is watched. */ void overlay_loader_active_write_check(uint32_t phys, uint32_t size) { - extern uint32_t g_debug_last_store_pc; + extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; uint32_t p = phys & 0x1FFFFFFFu; for (int d = 0; d < s_active_depth; d++) { Candidate *c = &s_cand[s_active_stack[d]]; @@ -4713,7 +4714,7 @@ static void run_shadow_diff(CPUState *cpu, Candidate *c, uint32_t addr) { extern void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t return_addr); extern int gte_replay_side_effects_begin(void); extern void gte_replay_side_effects_end(void); - extern uint32_t g_debug_current_func_addr; + extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; extern uint64_t psx_exception_setjmp_epoch(void); extern int g_shadow_mmio_watch; @@ -4912,7 +4913,7 @@ static void shadow_escape_cleanup(void) { s_suppress_irq = s_shadow_saved_supp; g_exec_phase = s_shadow_saved_exec_phase; g_psx_call_bail = s_shadow_saved_call_bail; - { extern uint32_t g_debug_current_func_addr; + { extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; g_debug_current_func_addr = s_shadow_saved_debug_func; } s_irq_callcount = s_shadow_saved_irq_calls; s_irq_suppressed = s_shadow_saved_irq_supp; diff --git a/runtime/src/psx_bios_backend.c b/runtime/src/psx_bios_backend.c index d7890aa2..730daf15 100644 --- a/runtime/src/psx_bios_backend.c +++ b/runtime/src/psx_bios_backend.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* psx_bios_backend.c — routing between the runtime and the active BIOS. * * A build links every recompiled BIOS it ships (bundled OpenBIOS plus a retail @@ -19,6 +20,7 @@ #include "bios_hle.h" #include "cpu_state.h" #include "psx_bios_backend.h" +#include "psx_execution.h" /* ── State published to the rest of the runtime ─────────────────────────── */ @@ -37,7 +39,7 @@ uint32_t psx_bios_kernel_patch_range_count = 0; /* Dispatch nesting depth. Shared dispatch state, not per-image: each generated * dispatch used to define its own copy, which is precisely why two of them * could not be linked together. */ -int g_psx_dispatch_depth = 0; +PSX_THREAD_LOCAL int g_psx_dispatch_depth = 0; /* ── Registry ─────────────────────────────────────────────────────────────── * @@ -99,14 +101,19 @@ __attribute__((constructor)) static void psx_bios_registry_ctor(void) { registry * The generated BIOS C defines _psx_dispatch; these keep the unprefixed * names that ~66 runtime call sites and the game's generated C already use. */ +PSX_THREAD_LOCAL PsxExecutionDispatch g_psx_execution_dispatch; +PSX_THREAD_LOCAL PsxExecutionAbort g_psx_execution_abort; + void psx_dispatch(CPUState *cpu, uint32_t addr) { + if (g_psx_execution_dispatch) { g_psx_execution_dispatch(cpu, addr, 0); return; } if (!psx_bios_active || !psx_bios_active->dispatch) return; psx_bios_active->dispatch(cpu, addr); } void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t return_addr) { + if (g_psx_execution_dispatch) { g_psx_execution_dispatch(cpu, addr, return_addr); return; } if (!psx_bios_active || !psx_bios_active->dispatch_call) return; psx_bios_active->dispatch_call(cpu, addr, return_addr); } diff --git a/runtime/src/psx_bios_module.c b/runtime/src/psx_bios_module.c index f771a816..f8ca06a4 100644 --- a/runtime/src/psx_bios_module.c +++ b/runtime/src/psx_bios_module.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* psx_bios_module.c — host side of loadable BIOS backends (psx_bios_module.h). * * A bundled release links OpenBIOS only. When the player picks a retail dump @@ -203,7 +204,7 @@ int psx_bios_module_cache_path(const char *exe_dir, const char *stem, void psx_bios_module_fill_callbacks(PsxBiosModuleCallbacks *out) { extern int (*g_psx_bios_hle_hook)(struct CPUState *cpu, uint32_t phys); extern uint64_t g_dispatch_static_hits; - extern uint64_t g_psx_bail_flattened; + extern PSX_THREAD_LOCAL uint64_t g_psx_bail_flattened; extern int psx_game_address_in_text(uint32_t addr); extern uint64_t s_frame_count; extern uint32_t g_psx_mod_guest_functions; diff --git a/runtime/src/psx_cycles.c b/runtime/src/psx_cycles.c index 79939cae..632754e7 100644 --- a/runtime/src/psx_cycles.c +++ b/runtime/src/psx_cycles.c @@ -320,7 +320,7 @@ static uint32_t devices_cycles_to_next_internal_event(void) { * tens of thousands of host-side re-detections per second. VBlank remains an * unconditional ceiling so frame pacing is never crossed in one jump. */ static uint32_t devices_cycles_to_next_idle_event(void) { - extern uint32_t i_mask; + extern PSX_THREAD_LOCAL uint32_t i_mask; uint32_t best = interrupts_cycles_to_vblank(); uint32_t t = timers_cycles_to_irq(i_mask); if (t < best) best = t; uint32_t c = cdrom_cycles_to_irq(i_mask); if (c < best) best = c; @@ -723,7 +723,8 @@ void psx_idle_note_check(CPUState *cpu, uint32_t check_pc) { (void)cpu; (void)check_pc; return; #else - extern int g_psx_call_bail, g_precise_mode, g_ls_mode, g_ls_replay_active; + extern PSX_THREAD_LOCAL int g_psx_call_bail; + extern int g_precise_mode, g_ls_mode, g_ls_replay_active; extern int psx_get_in_exception(void); extern uint64_t g_guest_store_count, g_mmio_access_count; diff --git a/runtime/src/psx_icache.c b/runtime/src/psx_icache.c index 60749aa4..530e6c5f 100644 --- a/runtime/src/psx_icache.c +++ b/runtime/src/psx_icache.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* psx_icache.c — R3000A instruction-cache FETCH cost model (faithful). * * FAITHFUL_TIMING_PLAN.md axis-2 (I-cache). Transcribed from the in-tree Beetle @@ -14,7 +15,7 @@ #include int psx_icache_enabled(void) { - static int s = -1; + static PSX_THREAD_LOCAL int s = -1; if (s < 0) { const char* e = getenv("PSX_ICACHE"); s = (e == NULL || e[0] == '\0') ? 1 : (e[0] != '0'); @@ -25,16 +26,16 @@ int psx_icache_enabled(void) { /* Per-word tag+validity, mirroring Beetle ICache[idx].TV. Init to a value that can * never equal an aligned fetch address (aligned addrs have bits 0-1 = 0), so every * line starts cold (miss). */ -uint32_t g_psx_icache_tv[1024]; -int g_psx_icache_active = -1; +PSX_THREAD_LOCAL uint32_t g_psx_icache_tv[1024]; +PSX_THREAD_LOCAL int g_psx_icache_active = -1; /* Whole-call shadow: a transactional view of the cache tags so an AOT/diff * shadow replay can evolve the cache from the recorded entry state and then * restore the live view (master's shadow machinery, retargeted at the global * tag array the interpreter fast path exports). */ -static uint32_t s_icache_shadow_entry[1024]; -static uint32_t s_icache_shadow_live[1024]; -static int s_icache_shadow_state = 0; /* 0 none, 1 recorded, 2 replay */ +static PSX_THREAD_LOCAL uint32_t s_icache_shadow_entry[1024]; +static PSX_THREAD_LOCAL uint32_t s_icache_shadow_live[1024]; +static PSX_THREAD_LOCAL int s_icache_shadow_state = 0; /* 0 none, 1 recorded, 2 replay */ int psx_icache_shadow_record_begin(void) { if (s_icache_shadow_state != 0) return 0; diff --git a/runtime/src/psx_interpreter.c b/runtime/src/psx_interpreter.c index 90aa64cd..1b0f8be4 100644 --- a/runtime/src/psx_interpreter.c +++ b/runtime/src/psx_interpreter.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* psx_interpreter.c — Minimal R3000A interpreter for oracle builds. * * Implements the full MIPS I instruction set used by the PSX BIOS: @@ -22,11 +23,11 @@ #include /* I_STAT and I_MASK are owned by memory.c */ -extern uint32_t i_stat; +extern PSX_THREAD_LOCAL uint32_t i_stat; /* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */ extern void psx_irq_raise(uint32_t bit, uint32_t detail); extern void psx_irq_refresh_cause_ip2(void); /* interrupts.c — CAUSE.IP2 mirror */ -extern uint32_t i_mask; +extern PSX_THREAD_LOCAL uint32_t i_mask; #define COP0_SR 12 #define COP0_CAUSE 13 diff --git a/runtime/src/render_pass.c b/runtime/src/render_pass.c index a1b65001..16628e5c 100644 --- a/runtime/src/render_pass.c +++ b/runtime/src/render_pass.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* render_pass.c - host-timed render passes (psx_mod_render_pass_plan / * psx_mod_render_pass, mod_plugins.h). See docs/RENDER_PASSES.md. * @@ -44,8 +45,8 @@ extern uint16_t *gpu_get_vram_ptr(void); * the opt-in 8 MB RAM mod. Latched by memory_init() once per boot, so it * cannot change inside a pass, but it can between sessions. */ extern uint32_t memory_get_ram_bytes(void); -extern uint32_t i_stat; -extern uint32_t i_mask; +extern PSX_THREAD_LOCAL uint32_t i_stat; +extern PSX_THREAD_LOCAL uint32_t i_mask; /* gte.cpp: precision-tracking shadows (PGXP + the position cache) roll back * with the machine, or they describe the words a pass wrote. */ extern void gte_precision_checkpoint_begin(void); @@ -70,22 +71,22 @@ extern int psx_rewind_is_open(void); extern int psx_presentation_fast_forward(void); extern uint64_t g_guest_store_count; extern uint64_t g_mmio_access_count; -extern uint32_t g_debug_last_store_pc; -extern uint32_t g_debug_current_func_addr; -extern int g_psx_dispatch_depth; -extern int g_psx_call_bail; +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; +extern PSX_THREAD_LOCAL int g_psx_dispatch_depth; +extern PSX_THREAD_LOCAL int g_psx_call_bail; extern int g_ls_mode; extern int g_ls_replay_active; static int s_netplay_allowed; -extern void (*g_overlay_flush_pending_cycles)(void); +extern PSX_THREAD_LOCAL void (*g_overlay_flush_pending_cycles)(void); /* Host nesting that runtime frames set on entry and put back on exit. */ -extern int g_call_unit_depth; /* overlay_loader_call_native */ -extern int g_dma_exec_depth; /* dma.c kick / async writes */ -extern int g_host_store_depth; /* memory.c psx_host_write_* */ -extern int g_dma_cur_ch; -extern uint32_t g_dma_cur_madr; -extern uint32_t g_dma_cur_bcr; -extern uint32_t g_dma_initiator_pc; +extern PSX_THREAD_LOCAL int g_call_unit_depth; /* overlay_loader_call_native */ +extern PSX_THREAD_LOCAL int g_dma_exec_depth; /* dma.c kick / async writes */ +extern PSX_THREAD_LOCAL int g_host_store_depth; /* memory.c psx_host_write_* */ +extern PSX_THREAD_LOCAL int g_dma_cur_ch; +extern PSX_THREAD_LOCAL uint32_t g_dma_cur_madr; +extern PSX_THREAD_LOCAL uint32_t g_dma_cur_bcr; +extern PSX_THREAD_LOCAL uint32_t g_dma_initiator_pc; extern int g_dirty_interp_active; /* dirty-RAM interpreter */ extern int g_exec_phase; extern int g_precise_mode; diff --git a/runtime/src/render_thread.c b/runtime/src/render_thread.c index baaf3371..7b4670ca 100644 --- a/runtime/src/render_thread.c +++ b/runtime/src/render_thread.c @@ -10,6 +10,7 @@ * can never fall between a check and a wait. */ #include "render_thread.h" +#include "psx_thread_local.h" #include #include @@ -79,6 +80,70 @@ static void cpu_relax(void) { } #endif +/* List capture is independent of the live producer and every ring counter. */ +struct RtCommandList { + unsigned char *data; + size_t capacity, used, pending; + int sealed; +}; +static PSX_THREAD_LOCAL RtCommandList *t_capture; +static PSX_THREAD_LOCAL RtCaptureAbort t_capture_abort; +static void capture_fail(const char *reason) { + if (t_capture_abort) t_capture_abort(reason); + abort(); +} +RtCommandList *rt_list_create(size_t capacity) { + if (capacity < RT_ALIGN || capacity > UINT32_MAX) return NULL; + RtCommandList *list = (RtCommandList *)calloc(1, sizeof(*list)); + if (!list) return NULL; + list->data = (unsigned char *)malloc(capacity); + if (!list->data) { free(list); return NULL; } + list->capacity = capacity; + return list; +} +void rt_list_destroy(RtCommandList *list) { + if (!list) return; + if (list == t_capture) abort(); + free(list->data); free(list); +} +void rt_capture_begin(RtCommandList *list, RtCaptureAbort failure) { + if (!list || t_capture) abort(); + list->used = list->pending = 0; + list->sealed = 0; + t_capture = list; t_capture_abort = failure; +} +void rt_capture_end(void) { + if (!t_capture) return; + /* An abort can abandon the last uncommitted record. It is never visible. */ + t_capture->pending = 0; + t_capture->sealed = 1; + t_capture = NULL; t_capture_abort = NULL; +} +int rt_capture_active(void) { return t_capture != NULL; } +size_t rt_list_bytes(const RtCommandList *list) { + return list && list->sealed ? list->used : 0; +} +static void *capture_record(uint16_t op, uint16_t flags, uint32_t payload) { + RtCommandList *list = t_capture; + size_t size = (sizeof(RtCmd) + (uint64_t)payload + RT_ALIGN - 1u) & ~(uint64_t)(RT_ALIGN - 1u); + if (!op || list->pending) capture_fail("command_order"); + if (size > list->capacity - list->used) capture_fail("command_capacity"); + RtCmd *cmd = (RtCmd *)(list->data + list->used); + *cmd = (RtCmd){(uint32_t)size, op, flags, payload, 0}; + list->pending = size; + return cmd + 1; +} +void rt_list_execute(const RtCommandList *list, RtExecFn exec, void *user) { + if (!list || !list->sealed || !exec || t_capture) abort(); + for (size_t offset = 0; offset < list->used;) { + const RtCmd *cmd = (const RtCmd *)(list->data + offset); + if (cmd->size < sizeof(*cmd) || cmd->size % RT_ALIGN || + cmd->size > list->used - offset || cmd->payload > cmd->size - sizeof(*cmd)) abort(); + exec(user, cmd, cmd + 1); + offset += cmd->size; + } +} + #define OP_PAD 0u /* filler to the ring end; consumer skips it */ #define F_FRAME 0x8000u /* op 0 with this flag: frame marker */ @@ -306,6 +371,7 @@ static int pred_frames(uint64_t limit) { static int pred_released(uint64_t unused) { (void)unused; return atomic_load(&R.released); } void *rt_cmd_begin(uint16_t op, uint16_t flags, uint32_t payload_bytes) { + if (t_capture) return capture_record(op, flags, payload_bytes); if (!rt_running() || R.pending || op == OP_PAD) return NULL; uint64_t size = align_up(sizeof(RtCmd) + (uint64_t)payload_bytes); if (size > R.cap / 2) { R.oversize++; return NULL; } @@ -338,6 +404,10 @@ void *rt_cmd_begin(uint16_t op, uint16_t flags, uint32_t payload_bytes) { } void rt_cmd_commit(void) { + if (t_capture) { + if (!t_capture->pending) capture_fail("command_commit"); + t_capture->used += t_capture->pending; t_capture->pending = 0; return; + } if (!R.pending) return; R.pending = 0; uint64_t end = R.pend_start + R.pend_size; @@ -362,6 +432,7 @@ static void commit_marker(uint16_t flags) { } void rt_frame_end(void) { + if (t_capture) capture_fail("live_render_sync"); if (!rt_running()) return; if (R.held) { /* Nothing is queued while the context is held; count the frame as @@ -389,12 +460,14 @@ int rt_frames_ahead(void) { } void rt_drain(void) { + if (t_capture) capture_fail("live_render_sync"); if (!rt_running() || R.held) return; uint64_t w = atomic_load(&R.wpos); emu_wait(pred_drained, w); } void rt_acquire(const char *reason) { + if (t_capture) capture_fail("live_render_sync"); if (!rt_running() || R.held) return; uint64_t t0 = now_ns(); rt_drain(); @@ -415,6 +488,7 @@ void rt_acquire(const char *reason) { } void rt_release(void) { + if (t_capture) capture_fail("live_render_sync"); if (!rt_running() || !R.held) return; R.ctx(R.user, 0); mtx_lock(&R.mtx); @@ -476,6 +550,7 @@ int rt_start(const RtConfig *cfg) { } void rt_stop(void) { + if (t_capture) capture_fail("live_render_sync"); if (!rt_running()) return; int was_held = R.held; if (!was_held) rt_drain(); diff --git a/runtime/src/sio.c b/runtime/src/sio.c index 2d82ca85..975b927b 100644 --- a/runtime/src/sio.c +++ b/runtime/src/sio.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* * sio.c -- PS1 Serial I/O (SIO0) controller * @@ -21,7 +22,7 @@ #include /* I_STAT is owned by memory.c */ -extern uint32_t i_stat; +extern PSX_THREAD_LOCAL uint32_t i_stat; /* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */ extern void psx_irq_raise(uint32_t bit, uint32_t detail); @@ -492,9 +493,9 @@ static int s_card_handoff_count = 0; static int s_card_handoff_armed = 0; /* set after LOAD-style probe abort */ static void card_handoff_push(uint8_t kind, uint8_t byte) { - extern uint32_t g_debug_current_func_addr; - extern uint32_t g_debug_last_store_pc; - extern uint32_t i_mask; + extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; + extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; + extern PSX_THREAD_LOCAL uint32_t i_mask; extern uint64_t psx_get_cycle_count(void); SioCardHandoffEntry *e = &s_card_handoff[s_card_handoff_idx]; e->kind = kind; @@ -1959,7 +1960,7 @@ static void mc_process_byte(uint8_t tx_byte) { static void sio_process_byte(uint8_t tx_byte) { /* ---- Trace: capture pre-state ---- */ - extern uint32_t g_debug_current_func_addr; + extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; uint8_t trace_mc_pre = (uint8_t)mc_state; uint8_t trace_dev_pre = (uint8_t)active_device; uint8_t trace_irq_cd = (uint8_t)(sio_irq_countdown > 255 ? 255 : sio_irq_countdown); @@ -2398,7 +2399,7 @@ void sio_write(uint32_t addr, uint32_t value) { sio_mc_abort_ctrl = (uint16_t)value; } if (sio_txn_open) { - extern uint32_t g_debug_current_func_addr; + extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; txn_close(SIO_TXN_END_ABORT_RESET, mc_state, g_debug_current_func_addr); } sio_stat = SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY; @@ -2479,7 +2480,7 @@ void sio_write(uint32_t addr, uint32_t value) { /* SELECT deassertion cancels the device's pending DSR pulse. * It cannot create an ACK or preserve one for later delivery. */ if (active_device == DEV_MEMCARD && mc_state != MC_IDLE && sio_txn_open) { - extern uint32_t g_debug_current_func_addr; + extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; txn_close(SIO_TXN_END_ABORT_OTHER, mc_state, g_debug_current_func_addr); } /* Persist mc_flag back to the active slot before resetting. mc_flag @@ -2707,7 +2708,7 @@ static void sio_fire_ack_irq(void) { card_handoff_push(5, 0); } - extern uint32_t g_debug_current_func_addr; + extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; extern uint8_t psx_read_byte(uint32_t addr); SioIrqEntry *e = &sio_irq_buf[sio_irq_idx]; e->seq = sio_irq_seq; @@ -2876,7 +2877,7 @@ void sio_tick(int cycles) { event_ring_record_aux(EV_DEQ, (uint8_t)SRC_SIO, 1u); /* SIO shift IRQ fired */ /* SIO IRQ ring capture. */ - extern uint32_t g_debug_current_func_addr; + extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; extern uint8_t psx_read_byte(uint32_t addr); SioIrqEntry *e = &sio_irq_buf[sio_irq_idx]; e->seq = sio_irq_seq; diff --git a/runtime/src/starvation_ring.c b/runtime/src/starvation_ring.c index 076ec39d..22ed28b1 100644 --- a/runtime/src/starvation_ring.c +++ b/runtime/src/starvation_ring.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* starvation_ring.c — diagnostic-only. * * Always-on ring of the last 16K SIO/MMIO events with full SIO state @@ -38,10 +39,10 @@ static uint64_t host_us_now(void) { } #endif -extern uint32_t g_debug_current_func_addr; -extern uint32_t g_debug_last_store_pc; -extern uint32_t i_stat; -extern uint32_t i_mask; +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; +extern PSX_THREAD_LOCAL uint32_t i_stat; +extern PSX_THREAD_LOCAL uint32_t i_mask; extern int psx_get_in_exception(void); #if STARVATION_RING_ENABLED diff --git a/runtime/src/timers.c b/runtime/src/timers.c index 3bf5c0a5..747f8d08 100644 --- a/runtime/src/timers.c +++ b/runtime/src/timers.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* * timers.c — PS1 root counters (3 timers), v4 clean port. * @@ -47,8 +48,8 @@ typedef struct { int irq_line; /* toggle mode state */ } Timer; -static Timer timers[3]; -static uint32_t timer_frac[3]; +static PSX_THREAD_LOCAL Timer timers[3]; +static PSX_THREAD_LOCAL uint32_t timer_frac[3]; /* Always-on RootCounter/Timer IRQ-fire telemetry (Tomba 2 RCnt-wait diag): * counts how many times each timer raised its IRQ. If a game spins on an @@ -67,7 +68,7 @@ void timers_get_debug(int t, uint16_t *counter, uint16_t *target, } /* I_STAT is owned by memory.c — we poke it directly via this extern. */ -extern uint32_t i_stat; +extern PSX_THREAD_LOCAL uint32_t i_stat; /* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */ extern void psx_irq_raise(uint32_t bit, uint32_t detail); diff --git a/runtime/src/traps.c b/runtime/src/traps.c index f1ffc25b..4c6f3d79 100644 --- a/runtime/src/traps.c +++ b/runtime/src/traps.c @@ -100,11 +100,11 @@ int psx_is_dispatchable(uint32_t pc) } /* Dispatch call contract state (see cpu_state.h for the model). */ -int g_psx_call_bail = 0; -uint64_t g_psx_bail_first = 0; -uint64_t g_psx_bail_resolved = 0; -uint64_t g_psx_bail_flattened = 0; -uint64_t g_psx_bail_anomaly = 0; +PSX_THREAD_LOCAL int g_psx_call_bail = 0; +PSX_THREAD_LOCAL uint64_t g_psx_bail_first = 0; +PSX_THREAD_LOCAL uint64_t g_psx_bail_resolved = 0; +PSX_THREAD_LOCAL uint64_t g_psx_bail_flattened = 0; +PSX_THREAD_LOCAL uint64_t g_psx_bail_anomaly = 0; /* ---- Deduped bail-source ledger (Tomba 2 wild-return / splash-spin diag) ---- * psx_call_contract (cpu_state.h) calls psx_bail_record() at each FIRST-detected @@ -818,7 +818,7 @@ void psx_request_return_to_lobby(void) void psx_scheduler_run(CPUState* cpu) { - extern int g_psx_dispatch_depth; + extern PSX_THREAD_LOCAL int g_psx_dispatch_depth; g_in_scheduler_run = 1; g_sched_escape.reason = PSX_RUN_CONTINUE; for (;;) { @@ -844,7 +844,7 @@ void psx_scheduler_run(CPUState* cpu) * or dispatch_nonlocal_call) would skip its depth restore; clear the * nested-unit gate so IRQ checks are not wedged-off after the escape * (backstop for the Ape memcard native<->interp fix). */ - { extern int g_call_unit_depth; g_call_unit_depth = 0; } + { extern PSX_THREAD_LOCAL int g_call_unit_depth; g_call_unit_depth = 0; } /* flush_resume / savestate longjmp skips generated bb_defer cleanup. */ { extern PSX_THREAD_LOCAL int g_psx_cyc_bb_defer; diff --git a/runtime/tests/gpu_exec_stubs.inc b/runtime/tests/gpu_exec_stubs.inc index 82062771..465cde62 100644 --- a/runtime/tests/gpu_exec_stubs.inc +++ b/runtime/tests/gpu_exec_stubs.inc @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* Shared link stubs for tests that compile runtime/src/gpu.c directly and * execute GP0 packets without a GL context, mod bank or emulator core. * Include right after "../src/gpu.c". Define GPU_EXEC_REAL_UI_GROUP before @@ -7,8 +8,8 @@ uint64_t s_frame_count; PSX_THREAD_LOCAL uint64_t psx_cycle_count; /* gpu_timeline.c stamps notes with it */ int g_exec_phase; -uint32_t g_debug_current_func_addr; -uint32_t g_debug_last_store_pc; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; CPUState *debug_cpu_ptr; int g_psx_vram_dirty_tracking; uint32_t g_psx_ram_size = 0x00200000u; diff --git a/runtime/tests/overlay_pair_dedup_harness.c b/runtime/tests/overlay_pair_dedup_harness.c index c7354672..99597aba 100644 --- a/runtime/tests/overlay_pair_dedup_harness.c +++ b/runtime/tests/overlay_pair_dedup_harness.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" #define PSX_OVERLAY_DLL_BUILD 1 #include "overlay_loader.h" #undef PSX_OVERLAY_DLL_BUILD @@ -27,15 +28,15 @@ static uint8_t s_scratch[1024]; uint32_t g_psx_ram_size = RAM_SIZE; uint32_t g_psx_ram_mask = RAM_SIZE - 1u; -uint32_t g_debug_current_func_addr; -uint32_t g_debug_last_store_pc; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; uint32_t g_overlay_region_floor; -uint32_t i_stat, i_mask; +PSX_THREAD_LOCAL uint32_t i_stat, i_mask; int g_exec_phase; int g_idle_note_suppress; -int g_psx_call_bail; +PSX_THREAD_LOCAL int g_psx_call_bail; uint32_t g_dirty_ram_code_gen; -uint64_t g_psx_bail_first, g_psx_bail_resolved; +PSX_THREAD_LOCAL uint64_t g_psx_bail_first, g_psx_bail_resolved; uint64_t s_frame_count; int g_shadow_mmio_watch; uint64_t g_shadow_mmio_hits; diff --git a/runtime/tests/psx_ram_runtime_map_stubs.c b/runtime/tests/psx_ram_runtime_map_stubs.c index c2a4e685..94249db1 100644 --- a/runtime/tests/psx_ram_runtime_map_stubs.c +++ b/runtime/tests/psx_ram_runtime_map_stubs.c @@ -15,15 +15,15 @@ int g_stub_device_restores; /* ---- data ---------------------------------------------------------------- */ CPUState *debug_cpu_ptr; -uint32_t g_debug_current_func_addr; -uint32_t g_debug_last_store_pc; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; volatile int g_ds_recording; int g_event_step_conservative; int g_ls_mode; int g_ls_replay_active; int g_ls_suppress_record; int g_ram_read_watch_active; -void (*g_overlay_flush_pending_cycles)(void); +PSX_THREAD_LOCAL void (*g_overlay_flush_pending_cycles)(void); PSX_THREAD_LOCAL uint32_t g_psx_cyc_batch; PSX_THREAD_LOCAL uint32_t g_psx_cyc_batch_limit; PSX_THREAD_LOCAL uint32_t *g_psx_cyc_local_acc; @@ -34,7 +34,7 @@ void pgxp_invalidate_word(uint32_t addr) { (void)addr; } #endif #ifndef PSX_STUBS_REAL_ICACHE /* The I-cache model (psx_icache.c). isc_store_test links the real one. */ -uint32_t g_psx_icache_tv[1024]; +PSX_THREAD_LOCAL uint32_t g_psx_icache_tv[1024]; void psx_icache_isc_store(uint32_t biu, uint32_t addr, uint32_t value) { (void)biu; (void)addr; (void)value; __builtin_trap(); /* SR.IsC is never set here */ } diff --git a/runtime/tests/restore_edge_irq_stubs.c b/runtime/tests/restore_edge_irq_stubs.c index 16b54e82..ca747bdb 100644 --- a/runtime/tests/restore_edge_irq_stubs.c +++ b/runtime/tests/restore_edge_irq_stubs.c @@ -16,8 +16,8 @@ struct CPUState; uint8_t g_test_ram[0x200000]; PSX_THREAD_LOCAL uint64_t psx_cycle_count = 0; PSX_THREAD_LOCAL uint64_t psx_next_service_cycle = 0; -uint32_t i_stat = 0; -uint32_t i_mask = 0; +PSX_THREAD_LOCAL uint32_t i_stat = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; uint32_t g_psx_ram_size = 0x200000u; uint32_t g_psx_vblank_cycles = 564480u; @@ -46,11 +46,11 @@ int psx_netplay_rb_recover_null_pc(struct CPUState *c, uint32_t *out_pc) /* ---- Runtime globals the two units reference ---- */ uint64_t g_async_rfe_fire_count = 0; uint32_t g_async_rfe_resume_pc = 0; -int g_call_unit_depth = 0; +PSX_THREAD_LOCAL int g_call_unit_depth = 0; int g_cosim_dirty_pump_site = 0; int g_dirty_interp_active = 0; uint32_t g_dirty_safe_resume_pc = 0; -int g_dma_exec_depth = 0; +PSX_THREAD_LOCAL int g_dma_exec_depth = 0; int g_event_step_conservative = 0; int g_exec_phase = 0; int g_idle_skip_enabled = 0; @@ -60,7 +60,7 @@ PSX_THREAD_LOCAL uint32_t g_psx_cyc_batch = 0; PSX_THREAD_LOCAL uint32_t g_psx_cyc_batch_limit = 0; PSX_THREAD_LOCAL int g_psx_cyc_bb_defer = 0; PSX_THREAD_LOCAL uint32_t *g_psx_cyc_local_acc = NULL; -int g_psx_dispatch_depth = 0; +PSX_THREAD_LOCAL int g_psx_dispatch_depth = 0; PSX_THREAD_LOCAL int g_psx_guest_time_frozen = 0; uint32_t g_sentinel_reach_async = 0; uint64_t g_sentinel_reach_traps = 0; diff --git a/runtime/tests/test_bios_module_host.c b/runtime/tests/test_bios_module_host.c index 35ff96ce..5ab787fd 100644 --- a/runtime/tests/test_bios_module_host.c +++ b/runtime/tests/test_bios_module_host.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* test_bios_module_host.c — the host side of loadable BIOS backends * (runtime/src/psx_bios_module.c) against fake modules built by * runtime/CMakeLists.txt from tests/bios_module_fake.c. @@ -47,8 +48,8 @@ int psx_mod_dispatch_guest_function(struct CPUState *cpu, uint32_t address) { } int (*g_psx_bios_hle_hook)(struct CPUState *cpu, uint32_t phys) = 0; uint64_t g_dispatch_static_hits = 0; -uint32_t g_debug_current_func_addr = 0; -uint64_t g_psx_bail_flattened = 0; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0; +PSX_THREAD_LOCAL uint64_t g_psx_bail_flattened = 0; uint32_t g_psx_ram_mask = 0x1FFFFFu; void psx_bios_hle_configure(uint32_t a, uint32_t b) { (void)a; (void)b; } int autocompile_toolchain_available(void) { return 0; } diff --git a/runtime/tests/test_boot_state_handoff.c b/runtime/tests/test_boot_state_handoff.c index 2bcfe2ca..e98c2240 100644 --- a/runtime/tests/test_boot_state_handoff.c +++ b/runtime/tests/test_boot_state_handoff.c @@ -8,7 +8,7 @@ static uint8_t ram[2*1024*1024], spad[1024], spuram[512*1024]; static uint16_t vram[1024*512]; static int boot_clears, cd_handoffs; -uint32_t i_stat, i_mask, g_psx_icache_tv[1024]; +PSX_THREAD_LOCAL uint32_t i_stat, i_mask, g_psx_icache_tv[1024]; PSX_THREAD_LOCAL uint64_t psx_cycle_count; PSX_THREAD_LOCAL uint32_t g_psx_gcs_frac; uint32_t psx_guest_cycle_scale_config(void) { return 1; } diff --git a/runtime/tests/test_cause_ip2_combinational.c b/runtime/tests/test_cause_ip2_combinational.c index 241713b7..9e536ab6 100644 --- a/runtime/tests/test_cause_ip2_combinational.c +++ b/runtime/tests/test_cause_ip2_combinational.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* * Validate that COP0.CAUSE bit 10 (IP2) behaves as a COMBINATIONAL mirror of * the interrupt-controller line, not as a latch. @@ -34,8 +35,8 @@ /* ---- The unit under test ------------------------------------------------- * Mirrors runtime/src/interrupts.c. Kept in lockstep with it; if the two ever * disagree this test is the thing that should be updated last, not first. */ -uint32_t i_stat; -uint32_t i_mask; +PSX_THREAD_LOCAL uint32_t i_stat; +PSX_THREAD_LOCAL uint32_t i_mask; static uint32_t *s_cause_ptr; diff --git a/runtime/tests/test_cdrom_sector_fifo.c b/runtime/tests/test_cdrom_sector_fifo.c index 44e89231..6093d674 100644 --- a/runtime/tests/test_cdrom_sector_fifo.c +++ b/runtime/tests/test_cdrom_sector_fifo.c @@ -1,8 +1,9 @@ +#include "psx_thread_local.h" /* Synthetic-sector integration tests for the actual CD controller. */ #include "../src/cdrom.c" #define CHECK(x) do { if (!(x)) { fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #x); exit(1); } } while (0) -uint32_t i_stat, g_debug_current_func_addr, g_debug_last_store_pc; +PSX_THREAD_LOCAL uint32_t i_stat, g_debug_current_func_addr, g_debug_last_store_pc; uint64_t s_frame_count, psx_cycle_count; static int test_dma_active; int dma_cdrom_transfer_active(void) { return test_dma_active; } diff --git a/runtime/tests/test_dma_gpu_list_cpu_hold.c b/runtime/tests/test_dma_gpu_list_cpu_hold.c index 450143b9..b97c86cd 100644 --- a/runtime/tests/test_dma_gpu_list_cpu_hold.c +++ b/runtime/tests/test_dma_gpu_list_cpu_hold.c @@ -81,9 +81,9 @@ void gpu_ws_validate_linked_list_header(uint32_t addr, uint32_t h) { (void)addr; void gpu_ws_validate_linked_list_node(uint32_t addr, uint32_t n) { (void)addr; (void)n; } /* ---- observers and IRQ (inert) ----------------------------------------- */ -uint32_t i_stat; -uint32_t g_debug_current_func_addr; -uint32_t g_debug_last_store_pc; +PSX_THREAD_LOCAL uint32_t i_stat; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; uint64_t s_frame_count; uint64_t g_io_openbus_reads; uint64_t g_io_openbus_writes; diff --git a/runtime/tests/test_draw_distance_interp.c b/runtime/tests/test_draw_distance_interp.c index 988fe848..b3556edf 100644 --- a/runtime/tests/test_draw_distance_interp.c +++ b/runtime/tests/test_draw_distance_interp.c @@ -39,17 +39,17 @@ /* ---- link stubs for code reachable from exec_one_fetched ------------------ */ uint64_t s_frame_count; -uint32_t g_debug_current_func_addr; -uint32_t g_debug_last_store_pc; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; uint32_t g_dirty_ram_code_gen; uint64_t g_dispatch_static_hits; int g_rfe_escape_pending; int g_exc_escape_reason; -int g_psx_call_bail; -uint64_t g_psx_bail_first; -uint64_t g_psx_bail_resolved; -uint32_t i_stat; -uint32_t i_mask; +PSX_THREAD_LOCAL int g_psx_call_bail; +PSX_THREAD_LOCAL uint64_t g_psx_bail_first; +PSX_THREAD_LOCAL uint64_t g_psx_bail_resolved; +PSX_THREAD_LOCAL uint32_t i_stat; +PSX_THREAD_LOCAL uint32_t i_mask; PSX_THREAD_LOCAL uint64_t psx_cycle_count; uint32_t g_psx_mod_function_entry_hooks; uint32_t g_psx_mod_instruction_hooks; diff --git a/runtime/tests/test_gpu_gp0_history_guard.py b/runtime/tests/test_gpu_gp0_history_guard.py index 32d5c79a..4040d7fe 100644 --- a/runtime/tests/test_gpu_gp0_history_guard.py +++ b/runtime/tests/test_gpu_gp0_history_guard.py @@ -229,7 +229,7 @@ STUBS_C = r""" uint64_t s_frame_count = 0; PSX_THREAD_LOCAL uint64_t psx_cycle_count = 0; -uint32_t g_debug_last_store_pc = 0x80012340u; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc = 0x80012340u; uint32_t g_psx_ram_size = 0x00200000u; uint32_t g_psx_ram_mask = 0x001FFFFFu; void psx_video_timing_set_pal(int pal) { (void)pal; } @@ -256,10 +256,10 @@ uint16_t mod_texture_packet_bank(uint32_t p, const uint32_t *words, uint32_t n) int mod_texture_packet_precision(uint32_t p, float q[3], float xy[6]) { (void)p; (void)q; (void)xy; return 0; } void psx_host_write_half(uint32_t addr, uint16_t val) { (void)addr; (void)val; } -uint32_t g_debug_current_func_addr = 0x80045670u; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0x80045670u; CPUState *debug_cpu_ptr = 0; -uint32_t i_stat = 0; -uint32_t i_mask = 0; +PSX_THREAD_LOCAL uint32_t i_stat = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; static uint16_t *g_vram; uint32_t debug_guest_ra(void) { return 0x8000AAAAu; } diff --git a/runtime/tests/test_gte_deferred.c b/runtime/tests/test_gte_deferred.c index c1d92c85..aa11ea1f 100644 --- a/runtime/tests/test_gte_deferred.c +++ b/runtime/tests/test_gte_deferred.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* Production GTE deadline regressions. No BIOS, disc or copied guest code. */ #include "psx_cyc.h" #include @@ -5,10 +6,10 @@ int g_ls_replay_active = 0; int g_ls_mode = 0; int g_precise_mode = 0; -int g_psx_call_bail = 0; +PSX_THREAD_LOCAL int g_psx_call_bail = 0; int g_boot_state_cpu_timing_restored = 0; /* no restored boot state in this fixture */ int psx_mod_host_output_scope(void) { return 0; } /* no host-output mod callback */ -uint32_t i_mask = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; uint64_t g_guest_store_count = 0; uint64_t g_mmio_access_count = 0; diff --git a/runtime/tests/test_gte_register_access.cpp b/runtime/tests/test_gte_register_access.cpp index 5040dd7b..288d83f8 100644 --- a/runtime/tests/test_gte_register_access.cpp +++ b/runtime/tests/test_gte_register_access.cpp @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" #include "cpu_state.h" #include "gte.h" #include "hle_gte.h" @@ -76,7 +77,7 @@ static int g_test_netplay_active = 0; extern "C" int psx_netplay_active(void) { return g_test_netplay_active; } extern "C" { uint64_t s_frame_count = 0; -uint32_t g_debug_last_store_pc = 0; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc = 0; } uint64_t g_test_cycle = 0; diff --git a/runtime/tests/test_host_snapshot_guard.c b/runtime/tests/test_host_snapshot_guard.c index 970ceeef..79e0126a 100644 --- a/runtime/tests/test_host_snapshot_guard.c +++ b/runtime/tests/test_host_snapshot_guard.c @@ -1,9 +1,10 @@ +#include "psx_thread_local.h" #include "interrupts.c" int psx_mod_function_entry_active(void) { return 0; } int g_cosim_dirty_pump_site; -int g_call_unit_depth; -int g_psx_dispatch_depth; +PSX_THREAD_LOCAL int g_call_unit_depth; +PSX_THREAD_LOCAL int g_psx_dispatch_depth; uint32_t g_dirty_safe_resume_pc; static void check(int ok, const char* message) { diff --git a/runtime/tests/test_idle_skip_store_counter.c b/runtime/tests/test_idle_skip_store_counter.c index 2a44c349..ac90f2d3 100644 --- a/runtime/tests/test_idle_skip_store_counter.c +++ b/runtime/tests/test_idle_skip_store_counter.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* * Pin the idle-loop detector's store-counter extension * ([runtime] idle_skip_store_counters, psx_cycles.c). @@ -25,10 +26,10 @@ int g_ls_replay_active = 0; int g_ls_mode = 0; int g_precise_mode = 0; -int g_psx_call_bail = 0; +PSX_THREAD_LOCAL int g_psx_call_bail = 0; int g_boot_state_cpu_timing_restored = 0; /* boot_state.c, read by psx_cycles.c */ int psx_mod_host_output_scope(void) { return 0; } /* mod_runtime.cpp */ -uint32_t i_mask = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; uint64_t g_guest_store_count = 0; uint64_t g_mmio_access_count = 0; static uint32_t g_guest_last_store_addr = 0xFFFFFFFFu, g_guest_last_store_val; diff --git a/runtime/tests/test_interp_cop0_read_sample.c b/runtime/tests/test_interp_cop0_read_sample.c index a5f1a179..0f51ecf2 100644 --- a/runtime/tests/test_interp_cop0_read_sample.c +++ b/runtime/tests/test_interp_cop0_read_sample.c @@ -36,8 +36,8 @@ PSX_THREAD_LOCAL uint32_t g_psx_cyc_batch; PSX_THREAD_LOCAL uint32_t g_psx_cyc_batch_limit; PSX_THREAD_LOCAL int g_psx_cyc_bb_defer; PSX_THREAD_LOCAL uint32_t *g_psx_cyc_local_acc; -int g_psx_icache_active; /* 0: no fetch cost */ -uint32_t g_psx_icache_tv[1024]; +PSX_THREAD_LOCAL int g_psx_icache_active; /* 0: no fetch cost */ +PSX_THREAD_LOCAL uint32_t g_psx_icache_tv[1024]; static CPUState *s_device_cpu; static unsigned s_services; diff --git a/runtime/tests/test_isc_store.c b/runtime/tests/test_isc_store.c index 4920f6b5..43b691b9 100644 --- a/runtime/tests/test_isc_store.c +++ b/runtime/tests/test_isc_store.c @@ -38,12 +38,12 @@ extern void psx_write_word(uint32_t addr, uint32_t val); extern void psx_write_half(uint32_t addr, uint16_t val); extern void psx_write_byte(uint32_t addr, uint8_t val); -extern int g_host_store_depth; +extern PSX_THREAD_LOCAL int g_host_store_depth; extern uint32_t psx_read_word(uint32_t addr); extern uint8_t *memory_get_ram_ptr(void); extern uint8_t *memory_get_scratchpad_ptr(void); extern void memory_set_sr_ptr(const uint32_t *p); -extern int g_dma_exec_depth; +extern PSX_THREAD_LOCAL int g_dma_exec_depth; extern PSX_THREAD_LOCAL uint64_t psx_cycle_count; /* BIU values the kernel and the tests use (Beetle cpu.cpp BIU_* bits). */ diff --git a/runtime/tests/test_memory_backing.c b/runtime/tests/test_memory_backing.c index dedf43c7..f98e7b8f 100644 --- a/runtime/tests/test_memory_backing.c +++ b/runtime/tests/test_memory_backing.c @@ -17,10 +17,19 @@ extern uint8_t *memory_get_ram_ptr(void); extern uint8_t *memory_get_scratchpad_ptr(void); static atomic_int phase; static PsxMemoryBacking *copy; +static PsxMemoryExecutionState *metadata; +extern PSX_THREAD_LOCAL uint32_t i_stat, i_mask; +extern uint32_t psx_read_word(uint32_t); +static uint32_t worker_status; static uint8_t *live; static void replay(void) { CHECK(memory_get_ram_ptr() == live); CHECK(memory_backing_bind(copy) == NULL); + memory_execution_bind(metadata, &worker_status); + g_psx_render_pass_active = 1; + CHECK(psx_read_word(0x1F801070u) == 0x35); + CHECK(psx_read_word(0x1F801074u) == 0x17); + i_stat = 0x62; CHECK(g_psx_ram != live && g_psx_ram == memory_get_ram_ptr()); CHECK(g_psx_ram[0x700000] == 37); CHECK(memory_get_scratchpad_ptr()[17] == 19); @@ -29,6 +38,7 @@ static void replay(void) { atomic_store_explicit(&phase, 1, memory_order_release); while (atomic_load_explicit(&phase, memory_order_acquire) != 2) { } CHECK(g_psx_ram[0x700000] == 71); + CHECK(i_stat == 0x62 && i_mask == 0x17); CHECK(memory_backing_bind(NULL) == copy); CHECK(g_psx_ram == live && live[0x700000] == 41); } @@ -44,6 +54,10 @@ int main(void) { copy = memory_backing_create(); CHECK(copy); memory_backing_capture(copy); + metadata = memory_execution_create(); + CHECK(metadata); + i_stat = 0x35; i_mask = 0x17; + memory_execution_capture(metadata); #ifdef _WIN32 HANDLE thread = CreateThread(NULL, 0, entry, NULL, 0, NULL); CHECK(thread); @@ -55,6 +69,8 @@ int main(void) { CHECK(g_psx_ram == live && live[0x700000] == 37); CHECK(memory_get_scratchpad_ptr()[17] == 19); live[0x700000] = 41; + CHECK(i_stat == 0x35 && i_mask == 0x17); + i_stat = 0x43; i_mask = 0x29; atomic_store_explicit(&phase, 2, memory_order_release); #ifdef _WIN32 CHECK(WaitForSingleObject(thread, 10000) == WAIT_OBJECT_0); @@ -67,6 +83,8 @@ int main(void) { CHECK(psx_peek_word(0x80700000, &word) && (word & 255) == 71); CHECK(memory_get_scratchpad_ptr()[17] == 91); CHECK(memory_backing_bind(NULL) == copy); + CHECK(i_stat == 0x43 && i_mask == 0x29); + memory_execution_destroy(metadata); memory_backing_destroy(copy); return 0; } diff --git a/runtime/tests/test_memory_debug_trace_guard.py b/runtime/tests/test_memory_debug_trace_guard.py index 37fa5a66..bfdb8054 100644 --- a/runtime/tests/test_memory_debug_trace_guard.py +++ b/runtime/tests/test_memory_debug_trace_guard.py @@ -29,13 +29,13 @@ HARNESS_COMMON_H = r""" extern int g_ls_mode; extern int g_ls_suppress_record; extern volatile int g_ds_recording; -extern int g_dma_exec_depth; -extern int g_dma_cur_ch; -extern uint32_t g_dma_cur_madr; -extern uint32_t g_dma_cur_bcr; -extern uint32_t g_dma_initiator_pc; -extern uint32_t g_debug_last_store_pc; -extern uint32_t g_debug_current_func_addr; +extern PSX_THREAD_LOCAL int g_dma_exec_depth; +extern PSX_THREAD_LOCAL int g_dma_cur_ch; +extern PSX_THREAD_LOCAL uint32_t g_dma_cur_madr; +extern PSX_THREAD_LOCAL uint32_t g_dma_cur_bcr; +extern PSX_THREAD_LOCAL uint32_t g_dma_initiator_pc; +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; +extern PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; extern uint64_t s_frame_count; extern uint32_t g_overlay_region_floor; extern uint32_t g_text_image_lo; @@ -45,7 +45,7 @@ extern PSX_THREAD_LOCAL uint64_t g_psx_cycle_fast_limit; extern int g_event_step_conservative; extern int g_ls_replay_active; extern CPUState *debug_cpu_ptr; -extern void (*g_overlay_flush_pending_cycles)(void); +extern PSX_THREAD_LOCAL void (*g_overlay_flush_pending_cycles)(void); extern uint32_t g_dirty_ram_exec_page_bitmap[DIRTY_RAM_EXEC_PAGE_BITMAP_WORDS]; extern uint32_t g_dirty_ram_exec_pc_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS]; extern uint32_t g_dirty_ram_dispatch_pc_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS]; @@ -116,13 +116,13 @@ PsxBiosImageInfo psx_bios_image = {0}; int g_ls_mode = 0; int g_ls_suppress_record = 0; volatile int g_ds_recording = 0; -int g_dma_exec_depth = 0; -int g_dma_cur_ch = -1; -uint32_t g_dma_cur_madr = 0; -uint32_t g_dma_cur_bcr = 0; -uint32_t g_dma_initiator_pc = 0; -uint32_t g_debug_last_store_pc = 0; -uint32_t g_debug_current_func_addr = 0; +PSX_THREAD_LOCAL int g_dma_exec_depth = 0; +PSX_THREAD_LOCAL int g_dma_cur_ch = -1; +PSX_THREAD_LOCAL uint32_t g_dma_cur_madr = 0; +PSX_THREAD_LOCAL uint32_t g_dma_cur_bcr = 0; +PSX_THREAD_LOCAL uint32_t g_dma_initiator_pc = 0; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc = 0; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0; uint64_t s_frame_count = 0; uint32_t g_overlay_region_floor = 0x200000u; uint32_t g_text_image_lo = 0x10000u; @@ -132,7 +132,7 @@ PSX_THREAD_LOCAL uint64_t g_psx_cycle_fast_limit = 0; int g_event_step_conservative = 0; int g_ls_replay_active = 0; CPUState *debug_cpu_ptr = 0; -void (*g_overlay_flush_pending_cycles)(void) = 0; +PSX_THREAD_LOCAL void (*g_overlay_flush_pending_cycles)(void) = 0; uint32_t g_dirty_ram_exec_page_bitmap[DIRTY_RAM_EXEC_PAGE_BITMAP_WORDS] = {0}; uint32_t g_dirty_ram_exec_pc_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS] = {0}; uint32_t g_dirty_ram_dispatch_pc_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS] = {0}; diff --git a/runtime/tests/test_mod_netplay.c b/runtime/tests/test_mod_netplay.c index f5426feb..54f9874f 100644 --- a/runtime/tests/test_mod_netplay.c +++ b/runtime/tests/test_mod_netplay.c @@ -10,7 +10,7 @@ * the trusted plugin's host-state partition, without creating a game/window. */ static uint8_t ram[2048], scratchpad[1024], spu_ram[16]; static uint16_t vram[1024 * 512]; -uint32_t i_stat, i_mask, g_psx_icache_tv[1024]; +PSX_THREAD_LOCAL uint32_t i_stat, i_mask, g_psx_icache_tv[1024]; PSX_THREAD_LOCAL uint64_t psx_cycle_count; uint8_t* memory_get_ram_ptr(void) { return ram; } uint32_t memory_get_ram_bytes(void) { return sizeof(ram); } diff --git a/runtime/tests/test_muldiv_deferred.c b/runtime/tests/test_muldiv_deferred.c index 7339c3cd..b61a260f 100644 --- a/runtime/tests/test_muldiv_deferred.c +++ b/runtime/tests/test_muldiv_deferred.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* Production mult/div deadline regressions. No BIOS, disc or copied guest code. * Companion of test_gte_deferred.c: the MULT/DIV completion deadline and the * MFLO/MFHI stall must see CPU work still pending in the batch or a local @@ -9,10 +10,10 @@ int g_ls_replay_active = 0; int g_ls_mode = 0; int g_precise_mode = 0; -int g_psx_call_bail = 0; +PSX_THREAD_LOCAL int g_psx_call_bail = 0; int g_boot_state_cpu_timing_restored = 0; /* no restored boot state in this fixture */ int psx_mod_host_output_scope(void) { return 0; } /* no host-output mod callback */ -uint32_t i_mask = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; uint64_t g_guest_store_count = 0; uint64_t g_mmio_access_count = 0; diff --git a/runtime/tests/test_netplay_view_mods.cpp b/runtime/tests/test_netplay_view_mods.cpp index 3c3b6b1b..e789f480 100644 --- a/runtime/tests/test_netplay_view_mods.cpp +++ b/runtime/tests/test_netplay_view_mods.cpp @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" #include "mod_runtime.h" #include "mod_packages.h" #include "mod_plugins.h" @@ -42,8 +43,8 @@ extern "C" void gpu_hd_textures_set_dump_enabled(int) {} extern "C" int gpu_hd_textures_reload(char*, size_t) { return 0; } /* sio.c's environment (as in test_sio_jogcon_protocol.c). */ extern "C" { -uint32_t i_stat = 0, i_mask = 0; -uint32_t g_debug_current_func_addr = 0, g_debug_last_store_pc = 0; +PSX_THREAD_LOCAL uint32_t i_stat = 0, i_mask = 0; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0, g_debug_last_store_pc = 0; int psx_get_in_exception(void) { return 0; } uint64_t psx_get_cycle_count(void) { static uint64_t t; return t += 64; } uint32_t memory_get_sr(void) { return 0; } diff --git a/runtime/tests/test_overlay_store_pc_forwarding.c b/runtime/tests/test_overlay_store_pc_forwarding.c index 1398012a..6795afb6 100644 --- a/runtime/tests/test_overlay_store_pc_forwarding.c +++ b/runtime/tests/test_overlay_store_pc_forwarding.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* Overlay DLLs must update the runtime's store-PC breadcrumb (ABI v24). * * This TU is an overlay DLL in miniature: it is built with the same defines @@ -14,7 +15,7 @@ /* Verbatim from code_generator.cpp's emitted extern block. Must still compile * once cpu_state.h has mapped the name onto the host pointer. */ -extern uint32_t g_debug_last_store_pc; /* exact PC of the executing SW/SH/SB — wtrace/readtrace producer attribution (debug_server.c) */ +extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; /* exact PC of the executing SW/SH/SB — wtrace/readtrace producer attribution (debug_server.c) */ static uint32_t host_last_store_pc; static uint32_t host_ram_word; diff --git a/runtime/tests/test_pgxp_untracked_store.c b/runtime/tests/test_pgxp_untracked_store.c index 461d98d9..8221d03e 100644 --- a/runtime/tests/test_pgxp_untracked_store.c +++ b/runtime/tests/test_pgxp_untracked_store.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* DMA and host stores against the PGXP shadow, end to end through the REAL * memory.c store paths and the REAL pgxp.cpp engine. * @@ -22,8 +23,8 @@ extern void psx_write_word(uint32_t addr, uint32_t val); extern void psx_write_half(uint32_t addr, uint16_t val); extern void psx_write_byte(uint32_t addr, uint8_t val); -extern int g_dma_exec_depth; -extern int g_host_store_depth; +extern PSX_THREAD_LOCAL int g_dma_exec_depth; +extern PSX_THREAD_LOCAL int g_host_store_depth; extern void memory_set_sr_ptr(const uint32_t *p); /* gte.cpp position cache: off here, so only the dataflow shadow can answer. */ diff --git a/runtime/tests/test_psx_cycle_event_boundaries.c b/runtime/tests/test_psx_cycle_event_boundaries.c index 83b6e885..a4c5eb9e 100644 --- a/runtime/tests/test_psx_cycle_event_boundaries.c +++ b/runtime/tests/test_psx_cycle_event_boundaries.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* * Pin cross-device causality at a scheduler deadline. * @@ -12,8 +13,8 @@ int g_ls_replay_active = 0; int g_ls_mode = 0; int g_precise_mode = 0; -int g_psx_call_bail = 0; -uint32_t i_mask = 0; +PSX_THREAD_LOCAL int g_psx_call_bail = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; uint64_t g_guest_store_count = 0; uint64_t g_mmio_access_count = 0; diff --git a/runtime/tests/test_psx_guest_cycle_scale_gate.c b/runtime/tests/test_psx_guest_cycle_scale_gate.c index e65ecb66..64e21c8e 100644 --- a/runtime/tests/test_psx_guest_cycle_scale_gate.c +++ b/runtime/tests/test_psx_guest_cycle_scale_gate.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* [timing] guest_cycle_scale against the real psx_cycles.c: * - identical accounting across tiers: native per-instruction CPU charges, * the batched psx_cyc_charge hot path and an overlay DLL's raw batch @@ -33,8 +34,8 @@ int g_psx_load_delay = 1; int g_ls_replay_active = 0; int g_ls_mode = 0; int g_precise_mode = 0; -int g_psx_call_bail = 0; -uint32_t i_mask = 0; +PSX_THREAD_LOCAL int g_psx_call_bail = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; uint64_t g_guest_store_count = 0; uint64_t g_mmio_access_count = 0; diff --git a/runtime/tests/test_psx_pad_apply.c b/runtime/tests/test_psx_pad_apply.c index 4281ef13..d3dcd3ef 100644 --- a/runtime/tests/test_psx_pad_apply.c +++ b/runtime/tests/test_psx_pad_apply.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* Shared PsxNetPad -> SIO apply (offline, netplay, selfcheck) and pad * normalization, checked on the real SIO wire. */ @@ -11,10 +12,10 @@ uint32_t sio_snapshot_bytes(void); void sio_snapshot_write(uint8_t *p); int sio_snapshot_read(const uint8_t *p, uint32_t len); -uint32_t i_stat = 0; -uint32_t i_mask = 0; -uint32_t g_debug_current_func_addr = 0; -uint32_t g_debug_last_store_pc = 0; +PSX_THREAD_LOCAL uint32_t i_stat = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc = 0; int psx_get_in_exception(void) { return 0; } uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; } uint32_t psx_read_word(uint32_t addr) { (void)addr; return 0; } diff --git a/runtime/tests/test_render_capture.c b/runtime/tests/test_render_capture.c new file mode 100644 index 00000000..03eee92d --- /dev/null +++ b/runtime/tests/test_render_capture.c @@ -0,0 +1,77 @@ +/* Concurrent worker capture never becomes a second ring producer. */ +#include "render_thread.h" +#include +#include +#include +#include +#include +#include +#define CHECK(c) do { if (!(c)) abort(); } while (0) +static RtCommandList *list; +static atomic_int phase, executed; +static unsigned live_next, private_next; +static jmp_buf overflow; +static void fail_capture(const char *reason) { + CHECK(!strcmp(reason, "command_capacity")); longjmp(overflow, 1); +} +static void consume_private(void *user, const RtCmd *cmd, const void *payload) { + (void)user; + CHECK(rt_on_render_thread()); + CHECK(cmd->op == 7 && cmd->payload == sizeof(uint32_t)); + CHECK(*(const uint32_t *)payload == private_next++); +} +static void consume(void *user, const RtCmd *cmd, const void *payload) { + (void)user; + if (cmd->op == 1) { + CHECK(*(const uint32_t *)payload == live_next++); + } else { + CHECK(cmd->op == 2 && atomic_load_explicit(&phase, memory_order_acquire) == 2); + rt_list_execute(list, consume_private, NULL); + CHECK(private_next == 4096); + atomic_store_explicit(&executed, 1, memory_order_release); + } +} +static void context(void *user, int current) { (void)user; (void)current; } +static void *worker(void *user) { + (void)user; + rt_capture_begin(list, fail_capture); + CHECK(rt_capture_active()); + atomic_store_explicit(&phase, 1, memory_order_release); + for (uint32_t i = 0; i < 4096; ++i) { + uint32_t *p = rt_cmd_begin(7, 0, sizeof(i)); + CHECK(p); *p = i; rt_cmd_commit(); + } + rt_capture_end(); + CHECK(!rt_capture_active() && rt_list_bytes(list) == 4096 * 32u); + atomic_store_explicit(&phase, 2, memory_order_release); + return NULL; +} +int main(void) { + RtConfig cfg = {0}; + cfg.ring_bytes = 1u << 20; cfg.max_frames = 2; + cfg.exec = consume; cfg.ctx = context; + CHECK(rt_start(&cfg)); + list = rt_list_create(1u << 20); CHECK(list); + pthread_t thread; CHECK(!pthread_create(&thread, NULL, worker, NULL)); + while (!atomic_load_explicit(&phase, memory_order_acquire)) { } + for (uint32_t i = 0; i < 16000; ++i) { + uint32_t *p = rt_cmd_begin(1, 0, sizeof(i)); + CHECK(p); *p = i; rt_cmd_commit(); + } + CHECK(!pthread_join(thread, NULL)); + CHECK(rt_cmd_begin(2, 0, 0)); rt_cmd_commit(); + rt_drain(); + CHECK(live_next == 16000 && atomic_load_explicit(&executed, memory_order_acquire)); + RtStats stats; rt_get_stats(&stats); CHECK(stats.records == 16001); + rt_stop(); + /* Overflow abandons the extra; it cannot acquire or block the live ring. */ + RtCommandList *small = rt_list_create(32); CHECK(small); + rt_capture_begin(small, fail_capture); + if (!setjmp(overflow)) { + CHECK(rt_cmd_begin(1, 0, 4)); rt_cmd_commit(); + rt_cmd_begin(1, 0, 4); abort(); + } + rt_capture_end(); CHECK(rt_list_bytes(small) == 32); + rt_list_destroy(small); rt_list_destroy(list); + return 0; +} diff --git a/runtime/tests/test_render_pass_abort.c b/runtime/tests/test_render_pass_abort.c index d8ef3b54..180896e4 100644 --- a/runtime/tests/test_render_pass_abort.c +++ b/runtime/tests/test_render_pass_abort.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* Render-pass watchdog abort (render_pass.c psx_mod_render_pass). * * A pass that runs away is cut off by a longjmp from the frozen clock's @@ -43,21 +44,21 @@ int g_boot_state_cpu_timing_restored = 0; int psx_mod_host_output_scope(void) { return 0; } int g_ls_mode = 0; int g_precise_mode = 0; -int g_psx_call_bail = 0; -int g_psx_dispatch_depth = 0; -uint32_t i_stat = 0, i_mask = 0; +PSX_THREAD_LOCAL int g_psx_call_bail = 0; +PSX_THREAD_LOCAL int g_psx_dispatch_depth = 0; +PSX_THREAD_LOCAL uint32_t i_stat = 0, i_mask = 0; uint64_t g_guest_store_count = 0; uint64_t g_mmio_access_count = 0; -uint32_t g_debug_last_store_pc = 0; -uint32_t g_debug_current_func_addr = 0; -uint32_t g_psx_icache_tv[1024]; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc = 0; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0; +PSX_THREAD_LOCAL uint32_t g_psx_icache_tv[1024]; uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES]; -void (*g_overlay_flush_pending_cycles)(void) = NULL; -int g_call_unit_depth = 0; -int g_dma_exec_depth = 0; -int g_host_store_depth = 0; -int g_dma_cur_ch = -1; -uint32_t g_dma_cur_madr = 0, g_dma_cur_bcr = 0, g_dma_initiator_pc = 0; +PSX_THREAD_LOCAL void (*g_overlay_flush_pending_cycles)(void) = NULL; +PSX_THREAD_LOCAL int g_call_unit_depth = 0; +PSX_THREAD_LOCAL int g_dma_exec_depth = 0; +PSX_THREAD_LOCAL int g_host_store_depth = 0; +PSX_THREAD_LOCAL int g_dma_cur_ch = -1; +PSX_THREAD_LOCAL uint32_t g_dma_cur_madr = 0, g_dma_cur_bcr = 0, g_dma_initiator_pc = 0; int g_dirty_interp_active = 0; int g_exec_phase = 0; uint32_t g_dirty_safe_resume_pc = 0; diff --git a/runtime/tests/test_render_pass_freeze.c b/runtime/tests/test_render_pass_freeze.c index 39959bed..5a281c2e 100644 --- a/runtime/tests/test_render_pass_freeze.c +++ b/runtime/tests/test_render_pass_freeze.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* Render-pass time freeze (psx_cycle_freeze.h psx_cycle_freeze_begin/end). * A pass counts guest cycles but never services a device, and afterwards * every clock value is exactly as before. Build/run: @@ -14,8 +15,8 @@ int g_ls_replay_active = 0; int g_ls_mode = 0; int g_precise_mode = 0; -int g_psx_call_bail = 0; -uint32_t i_mask = 0; +PSX_THREAD_LOCAL int g_psx_call_bail = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; uint64_t g_guest_store_count = 0; uint64_t g_mmio_access_count = 0; diff --git a/runtime/tests/test_render_pass_sandbox.c b/runtime/tests/test_render_pass_sandbox.c index 17af5707..9b20ce55 100644 --- a/runtime/tests/test_render_pass_sandbox.c +++ b/runtime/tests/test_render_pass_sandbox.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* Render-pass sandbox: what a pass may write and what it gets back. * * 1. Store policy (render_pass_plan.c render_pass_store_to, the function @@ -57,21 +58,21 @@ int g_boot_state_cpu_timing_restored = 0; int psx_mod_host_output_scope(void) { return 0; } int g_ls_mode = 0; int g_precise_mode = 0; -int g_psx_call_bail = 0; -int g_psx_dispatch_depth = 0; -uint32_t i_stat = 0, i_mask = 0; +PSX_THREAD_LOCAL int g_psx_call_bail = 0; +PSX_THREAD_LOCAL int g_psx_dispatch_depth = 0; +PSX_THREAD_LOCAL uint32_t i_stat = 0, i_mask = 0; uint64_t g_guest_store_count = 0; uint64_t g_mmio_access_count = 0; -uint32_t g_debug_last_store_pc = 0; -uint32_t g_debug_current_func_addr = 0; -uint32_t g_psx_icache_tv[1024]; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc = 0; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0; +PSX_THREAD_LOCAL uint32_t g_psx_icache_tv[1024]; uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES]; -void (*g_overlay_flush_pending_cycles)(void) = NULL; -int g_call_unit_depth = 0; -int g_dma_exec_depth = 0; -int g_host_store_depth = 0; -int g_dma_cur_ch = -1; -uint32_t g_dma_cur_madr = 0, g_dma_cur_bcr = 0, g_dma_initiator_pc = 0; +PSX_THREAD_LOCAL void (*g_overlay_flush_pending_cycles)(void) = NULL; +PSX_THREAD_LOCAL int g_call_unit_depth = 0; +PSX_THREAD_LOCAL int g_dma_exec_depth = 0; +PSX_THREAD_LOCAL int g_host_store_depth = 0; +PSX_THREAD_LOCAL int g_dma_cur_ch = -1; +PSX_THREAD_LOCAL uint32_t g_dma_cur_madr = 0, g_dma_cur_bcr = 0, g_dma_initiator_pc = 0; int g_dirty_interp_active = 0; int g_exec_phase = 0; uint32_t g_dirty_safe_resume_pc = 0; diff --git a/runtime/tests/test_restore_edge_irq_replay.c b/runtime/tests/test_restore_edge_irq_replay.c index 8ff15e6b..7361882a 100644 --- a/runtime/tests/test_restore_edge_irq_replay.c +++ b/runtime/tests/test_restore_edge_irq_replay.c @@ -37,7 +37,7 @@ extern uint8_t g_test_ram[0x200000]; extern PSX_THREAD_LOCAL uint64_t psx_cycle_count; -extern uint32_t i_stat, i_mask; +extern PSX_THREAD_LOCAL uint32_t i_stat, i_mask; extern uint32_t g_dirty_safe_resume_pc; extern int (*g_test_recover_null_pc)(CPUState *, uint32_t *); extern uint32_t psx_read_word(uint32_t addr); diff --git a/runtime/tests/test_same_thread_rfe_k0.c b/runtime/tests/test_same_thread_rfe_k0.c index 72ac77fd..49b5bde9 100644 --- a/runtime/tests/test_same_thread_rfe_k0.c +++ b/runtime/tests/test_same_thread_rfe_k0.c @@ -10,7 +10,7 @@ #include extern uint8_t g_test_ram[0x200000]; extern PSX_THREAD_LOCAL uint64_t psx_cycle_count; -extern uint32_t i_stat, i_mask; +extern PSX_THREAD_LOCAL uint32_t i_stat, i_mask; extern uint32_t psx_read_word(uint32_t); extern void psx_host_write_word(uint32_t, uint32_t); #define HANDLER 0x80001000u diff --git a/runtime/tests/test_sio_card_irq_integrity.c b/runtime/tests/test_sio_card_irq_integrity.c index 0a4f3e83..02740c09 100644 --- a/runtime/tests/test_sio_card_irq_integrity.c +++ b/runtime/tests/test_sio_card_irq_integrity.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* Device-level regression: guest RAM is not SIO hardware state. * Uses production SIO with a synthetic clock/card, no BIOS or retail data. * Independent references: Beetle frontio.cpp DoDSRIRQ/Write and irq.cpp @@ -7,7 +8,7 @@ #include "../src/sio.c" #include -uint32_t i_stat, i_mask, g_debug_current_func_addr, g_debug_last_store_pc; +PSX_THREAD_LOCAL uint32_t i_stat, i_mask, g_debug_current_func_addr, g_debug_last_store_pc; static uint64_t clock_now; static int card_present; static int checks, failures; diff --git a/runtime/tests/test_sio_card_protocol.c b/runtime/tests/test_sio_card_protocol.c index 62fec5d2..6d01c7ff 100644 --- a/runtime/tests/test_sio_card_protocol.c +++ b/runtime/tests/test_sio_card_protocol.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* * test_sio_card_protocol.c — pin SIO memory card protocol against no$psx spec. * @@ -43,10 +44,10 @@ #endif /* ---- Stubs for sio.c's external dependencies ---- */ -uint32_t i_stat = 0; -uint32_t i_mask = 0; -uint32_t g_debug_current_func_addr = 0; -uint32_t g_debug_last_store_pc = 0; +PSX_THREAD_LOCAL uint32_t i_stat = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc = 0; int psx_get_in_exception(void) { return 0; } uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; } uint32_t psx_read_word(uint32_t addr) { (void)addr; return 0; } diff --git a/runtime/tests/test_sio_digital_command.c b/runtime/tests/test_sio_digital_command.c index 0293b4e7..5396c867 100644 --- a/runtime/tests/test_sio_digital_command.c +++ b/runtime/tests/test_sio_digital_command.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* Plain digital pads shift their ID during every command byte. * Synthetic bus transactions over the real sio.c; no BIOS/disc/core fixture. */ #include "sio.h" @@ -10,10 +11,10 @@ uint32_t sio_snapshot_bytes(void); void sio_snapshot_write(uint8_t *p); int sio_snapshot_read(const uint8_t *p, uint32_t len); -uint32_t i_stat = 0; -uint32_t i_mask = 0; -uint32_t g_debug_current_func_addr = 0; -uint32_t g_debug_last_store_pc = 0; +PSX_THREAD_LOCAL uint32_t i_stat = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc = 0; int psx_get_in_exception(void) { return 0; } uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; } uint32_t psx_read_word(uint32_t addr) { (void)addr; return 0; } diff --git a/runtime/tests/test_sio_dualshock_rumble.c b/runtime/tests/test_sio_dualshock_rumble.c index 65cf5cf1..a74deb1c 100644 --- a/runtime/tests/test_sio_dualshock_rumble.c +++ b/runtime/tests/test_sio_dualshock_rumble.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* DualShock rumble protocol regression: 0x4D map negotiation must feed the * small/large motor bytes from later 0x42 polls to the frontend-facing state. */ @@ -11,10 +12,10 @@ uint32_t sio_snapshot_bytes(void); void sio_snapshot_write(uint8_t *p); int sio_snapshot_read(const uint8_t *p, uint32_t len); -uint32_t i_stat = 0; -uint32_t i_mask = 0; -uint32_t g_debug_current_func_addr = 0; -uint32_t g_debug_last_store_pc = 0; +PSX_THREAD_LOCAL uint32_t i_stat = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc = 0; int psx_get_in_exception(void) { return 0; } uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; } uint32_t psx_read_word(uint32_t addr) { (void)addr; return 0; } diff --git a/runtime/tests/test_sio_jogcon_protocol.c b/runtime/tests/test_sio_jogcon_protocol.c index e49a90f3..efcd18c2 100644 --- a/runtime/tests/test_sio_jogcon_protocol.c +++ b/runtime/tests/test_sio_jogcon_protocol.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* JogCon wire protocol regression: ID, buttons, signed steering, direction, * command response, and rollback state. */ @@ -11,10 +12,10 @@ uint32_t sio_snapshot_bytes(void); void sio_snapshot_write(uint8_t *p); int sio_snapshot_read(const uint8_t *p, uint32_t len); -uint32_t i_stat = 0; -uint32_t i_mask = 0; -uint32_t g_debug_current_func_addr = 0; -uint32_t g_debug_last_store_pc = 0; +PSX_THREAD_LOCAL uint32_t i_stat = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc = 0; int psx_get_in_exception(void) { return 0; } uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; } uint32_t psx_read_word(uint32_t addr) { (void)addr; return 0; } diff --git a/runtime/tests/test_sio_negcon_protocol.c b/runtime/tests/test_sio_negcon_protocol.c index 94d5304e..0535eb5d 100644 --- a/runtime/tests/test_sio_negcon_protocol.c +++ b/runtime/tests/test_sio_negcon_protocol.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* NeGcon wire protocol regression: ID 0x23 poll layout (twist, I, II, L), * no config mode, device-swap edges against the 0x44 mode lock, deferred type * requests during config, 0x44 keeping the device, snapshot of the pad mode, @@ -13,10 +14,10 @@ uint32_t sio_snapshot_bytes(void); void sio_snapshot_write(uint8_t *p); int sio_snapshot_read(const uint8_t *p, uint32_t len); -uint32_t i_stat = 0; -uint32_t i_mask = 0; -uint32_t g_debug_current_func_addr = 0; -uint32_t g_debug_last_store_pc = 0; +PSX_THREAD_LOCAL uint32_t i_stat = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc = 0; int psx_get_in_exception(void) { return 0; } uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; } uint32_t psx_read_word(uint32_t addr) { (void)addr; return 0; } diff --git a/runtime/tests/test_sio_spu_istat_ownership.c b/runtime/tests/test_sio_spu_istat_ownership.c index b64f249f..57691f5d 100644 --- a/runtime/tests/test_sio_spu_istat_ownership.c +++ b/runtime/tests/test_sio_spu_istat_ownership.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* * test_sio_spu_istat_ownership.c — cross-device I_STAT ownership. * @@ -19,10 +20,10 @@ #include /* ---- Stubs for sio.c's external dependencies ---- */ -uint32_t i_stat = 0; -uint32_t i_mask = 0; -uint32_t g_debug_current_func_addr = 0; -uint32_t g_debug_last_store_pc = 0; +PSX_THREAD_LOCAL uint32_t i_stat = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc = 0; int psx_get_in_exception(void) { return 0; } uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; } uint32_t psx_read_word(uint32_t addr) { (void)addr; return 0; } diff --git a/runtime/tests/test_uncharged_span.c b/runtime/tests/test_uncharged_span.c index a1ea5274..c250f1d8 100644 --- a/runtime/tests/test_uncharged_span.c +++ b/runtime/tests/test_uncharged_span.c @@ -1,3 +1,4 @@ +#include "psx_thread_local.h" /* Uncharged guest-time spans (psx_cycle_freeze.h psx_cycle_uncharged_*), * the time layer of mod_plugins.h psx_mod_call_guest_uncharged. Code in a * span keeps its effects but consumes no guest time; a span over budget @@ -12,8 +13,8 @@ int g_ls_replay_active = 0; int g_ls_mode = 0; int g_precise_mode = 0; -int g_psx_call_bail = 0; -uint32_t i_mask = 0; +PSX_THREAD_LOCAL int g_psx_call_bail = 0; +PSX_THREAD_LOCAL uint32_t i_mask = 0; uint64_t g_guest_store_count = 0; uint64_t g_mmio_access_count = 0; diff --git a/runtime/tests/test_ws_cull_edge_interp.c b/runtime/tests/test_ws_cull_edge_interp.c index 623ebbd6..12a53548 100644 --- a/runtime/tests/test_ws_cull_edge_interp.c +++ b/runtime/tests/test_ws_cull_edge_interp.c @@ -37,8 +37,8 @@ /* ---- link stubs for code reachable from exec_one_fetched ------------------ */ uint64_t s_frame_count; -uint32_t g_debug_current_func_addr; -uint32_t g_debug_last_store_pc; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc; uint32_t g_dirty_ram_code_gen; uint64_t g_dispatch_static_hits; uint32_t g_psx_mod_instruction_hooks; @@ -50,11 +50,11 @@ void psx_mod_instruction(CPUState *cpu, uint32_t pc, uint32_t instruction) { } int g_rfe_escape_pending; int g_exc_escape_reason; -int g_psx_call_bail; -uint64_t g_psx_bail_first; -uint64_t g_psx_bail_resolved; -uint32_t i_stat; -uint32_t i_mask; +PSX_THREAD_LOCAL int g_psx_call_bail; +PSX_THREAD_LOCAL uint64_t g_psx_bail_first; +PSX_THREAD_LOCAL uint64_t g_psx_bail_resolved; +PSX_THREAD_LOCAL uint32_t i_stat; +PSX_THREAD_LOCAL uint32_t i_mask; PSX_THREAD_LOCAL uint64_t psx_cycle_count; /* Live RAM geometry (memory.c): retail 2 MiB, as the stub RAM below. */ uint32_t g_psx_ram_size = PSX_MAIN_RAM_RETAIL_BYTES; diff --git a/runtime/tests/test_wtrace_dump.py b/runtime/tests/test_wtrace_dump.py index 50cb49f2..febd63e9 100644 --- a/runtime/tests/test_wtrace_dump.py +++ b/runtime/tests/test_wtrace_dump.py @@ -80,10 +80,10 @@ static uint32_t s_wtrace_head=2; typedef struct { uint32_t pc; uint32_t gpr[32]; } CPUState; static CPUState *debug_cpu_ptr = NULL; static uint32_t s_frame_count = 0; -uint32_t g_debug_current_func_addr = 0; -uint32_t g_debug_last_store_pc = 0; -int g_dma_exec_depth = 0, g_dma_cur_ch = -1; -uint32_t g_dma_initiator_pc = 0; +PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr = 0; +PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc = 0; +PSX_THREAD_LOCAL int g_dma_exec_depth = 0, g_dma_cur_ch = -1; +PSX_THREAD_LOCAL uint32_t g_dma_initiator_pc = 0; static void debug_server_send_line(const char *line) { puts(line); } static void send_err(int id, const char *error) { (void)id; (void)error; abort(); } """