From 0127fcfc73048ad7f446096007e8820e1cad8328 Mon Sep 17 00:00:00 2001 From: yacineolivier-maker <234833416+yacineolivier-maker@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:58:39 +0200 Subject: [PATCH 1/3] Host HUD layer: psx_host_set_hud_draw_cb, drawn under the in-game menu A title can draw its own heads-up display on the host side (ImGui, like the in-game overlay provider), once per presented frame -- real and generated frames -- under the in-game menu and with the same thread and GL rules as psx_host_overlay_set_draw_cb. Registered once at title activation; NULL unregisters. No callback registered: no change in behaviour. Used by a modern race HUD for R4 (lap, timing, tachometer, track map) drawn at window resolution instead of the 320x240 native one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Rkyma8rbyPiccs9n2bkyG6 --- runtime/include/host_overlay.h | 8 ++++++++ runtime/src/gpu_gl_renderer.c | 6 ++++-- runtime/src/host_overlay.c | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/runtime/include/host_overlay.h b/runtime/include/host_overlay.h index 06c2e5042..d1bfcf3f0 100644 --- a/runtime/include/host_overlay.h +++ b/runtime/include/host_overlay.h @@ -46,6 +46,14 @@ void psx_host_overlay_set_draw_cb(PsxHostOverlayDrawFn fn, void *ctx); int psx_host_overlay_has_draw(void); void psx_host_overlay_draw(int width, int height); /* renderer side */ +/* ---- Game HUD layer --------------------------------------------------------- + * A title's own heads-up display, drawn once per presented frame (real and + * generated) under the in-game menu, same thread and GL rules as the overlay + * callback above. Set once (title activation) and kept for the session; NULL + * unregisters (no wait: the registrant keeps fn and ctx alive for the session). */ +void psx_host_set_hud_draw_cb(PsxHostOverlayDrawFn fn, void *ctx); +void psx_host_hud_draw(int width, int height); /* renderer side */ + /* ---- P3: input sink ----------------------------------------------------- * The sink sees every host SDL event first (main thread), in the normal event * drain and in the host pause loop. Returning non-zero consumes the event. diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index dab818372..67b2f4a02 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -9908,8 +9908,10 @@ static void gl_swap_with_osd(void) { } if (ov_image(3, &px, &ow, &oh) && px) gl_draw_osd_image(px, ow, oh, ww, wh, 0, 0, ww, wh); - /* In-game overlay provider (host_overlay.h P2), over everything - * the host composes; no-op unless one registered. */ + /* The title's HUD layer, then the in-game overlay provider + * (host_overlay.h P2) over everything; each a no-op unless one + * registered. */ + psx_host_hud_draw(ww, wh); psx_host_overlay_draw(ww, wh); } } diff --git a/runtime/src/host_overlay.c b/runtime/src/host_overlay.c index 100f7a978..dfc7536cf 100644 --- a/runtime/src/host_overlay.c +++ b/runtime/src/host_overlay.c @@ -101,6 +101,20 @@ void psx_host_overlay_draw(int width, int height) { atomic_fetch_sub(&s_draw_inflight, 1); } +/* ---- Game HUD layer ------------------------------------------------------- */ +static PsxHostOverlayDrawFn s_hud_fn = NULL; +static void *s_hud_ctx = NULL; + +void psx_host_set_hud_draw_cb(PsxHostOverlayDrawFn fn, void *ctx) { + s_hud_ctx = ctx; + s_hud_fn = fn; +} + +void psx_host_hud_draw(int width, int height) { + PsxHostOverlayDrawFn fn = s_hud_fn; + if (fn && width > 0 && height > 0) fn(width, height, s_hud_ctx); +} + /* ---- P3: input sink -------------------------------------------------------- */ static PsxHostInputSinkFn s_sink_fn = NULL; static void *s_sink_ctx = NULL; From d383ef210b0732b86224a1a5a3f49f4c84a1fecb Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 22 Oct 2026 17:03:06 -0700 Subject: [PATCH 2/3] Drain and reset session HUD callbacks --- runtime/CMakeLists.txt | 1 + runtime/include/host_overlay.h | 6 +++-- runtime/src/gpu_gl_renderer.c | 1 + runtime/src/host_overlay.c | 34 +++++++++++++++++++++++++- runtime/src/mod_runtime.cpp | 3 +++ runtime/tests/test_host_overlay.c | 38 ++++++++++++++++++++++++++++++ runtime/tests/test_mod_runtime.cpp | 6 +++++ 7 files changed, 86 insertions(+), 3 deletions(-) diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 0340c49d9..85843c068 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1396,6 +1396,7 @@ if(BUILD_TESTING) add_executable(mod_runtime_test src/mod_immediate.c + src/host_overlay.c tests/test_mod_runtime.cpp src/iso_reader_c.cpp src/host_span_ring.c diff --git a/runtime/include/host_overlay.h b/runtime/include/host_overlay.h index d1bfcf3f0..0a2cd648b 100644 --- a/runtime/include/host_overlay.h +++ b/runtime/include/host_overlay.h @@ -49,8 +49,10 @@ void psx_host_overlay_draw(int width, int height); /* renderer side */ /* ---- Game HUD layer --------------------------------------------------------- * A title's own heads-up display, drawn once per presented frame (real and * generated) under the in-game menu, same thread and GL rules as the overlay - * callback above. Set once (title activation) and kept for the session; NULL - * unregisters (no wait: the registrant keeps fn and ctx alive for the session). */ + * callback above. OpenGL only; Vulkan and the SDL renderer never invoke it. + * Replacing or NULL unregistering drains prior calls as for the overlay above; + * the owner may then release its old context. Registration is session-scoped: + * activation reset and GL shutdown clear it. Register again on activation. */ void psx_host_set_hud_draw_cb(PsxHostOverlayDrawFn fn, void *ctx); void psx_host_hud_draw(int width, int height); /* renderer side */ diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 67b2f4a02..87d2c5bcd 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -6550,6 +6550,7 @@ static void pass_resources_release(void); static void present_image_ring_gl_forget(void); void gl_renderer_shutdown(void) { gl_renderer_render_thread_stop(); /* drains; the context comes back to this thread */ + psx_host_set_hud_draw_cb(NULL, NULL); /* After the worker has stopped (it presents, and captures, on its own * thread) and before the context dies: forget the ring's GL names. */ present_image_ring_gl_forget(); diff --git a/runtime/src/host_overlay.c b/runtime/src/host_overlay.c index dfc7536cf..83463a744 100644 --- a/runtime/src/host_overlay.c +++ b/runtime/src/host_overlay.c @@ -104,15 +104,47 @@ void psx_host_overlay_draw(int width, int height) { /* ---- Game HUD layer ------------------------------------------------------- */ static PsxHostOverlayDrawFn s_hud_fn = NULL; static void *s_hud_ctx = NULL; +static atomic_flag s_hud_lock = ATOMIC_FLAG_INIT; +static atomic_int s_hud_inflight = 0; +#if defined(_MSC_VER) +static __declspec(thread) int s_in_hud = 0; +#else +static _Thread_local int s_in_hud = 0; +#endif + +static void hud_lock(void) { + while (atomic_flag_test_and_set_explicit(&s_hud_lock, memory_order_acquire)) + draw_yield(); +} +static void hud_unlock(void) { + atomic_flag_clear_explicit(&s_hud_lock, memory_order_release); +} void psx_host_set_hud_draw_cb(PsxHostOverlayDrawFn fn, void *ctx) { + hud_lock(); s_hud_ctx = ctx; s_hud_fn = fn; + hud_unlock(); + /* Match the overlay lifetime contract without allocating callback records. + * The single GL owner may unregister itself; its current call then ends + * before another frame can consume the replacement. */ + if (!s_in_hud) + while (atomic_load(&s_hud_inflight) > 0) draw_yield(); } void psx_host_hud_draw(int width, int height) { + if (width <= 0 || height <= 0) return; + hud_lock(); PsxHostOverlayDrawFn fn = s_hud_fn; - if (fn && width > 0 && height > 0) fn(width, height, s_hud_ctx); + void *ctx = s_hud_ctx; + atomic_fetch_add(&s_hud_inflight, 1); + hud_unlock(); + if (fn) { + s_in_hud = 1; + fn(width, height, ctx); + s_in_hud = 0; + } + atomic_fetch_sub(&s_hud_inflight, 1); } /* ---- P3: input sink -------------------------------------------------------- */ diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 5bfe62570..28792e851 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -1,5 +1,6 @@ #include "mod_runtime.h" #include "host_launch_timing.h" +#include "host_overlay.h" #include "cpu_state.h" #include "disc_path.h" @@ -2963,6 +2964,8 @@ bool mod_runtime_read_disc_file_sectors(const std::string& path, uint32_t max_by extern "C" void mod_runtime_activate_plugins(void) { using namespace PSXRecompV4; HostLaunchTimingScope timing(HOST_LAUNCH_PLUGIN_ACTIVATION); + /* Drain session-owned presentation callbacks before their contexts reset. */ + psx_host_set_hud_draw_cb(nullptr, nullptr); mod_reset_plugin_sessions(); RuntimeMods& s = state(); s.ticket_ready = false; diff --git a/runtime/tests/test_host_overlay.c b/runtime/tests/test_host_overlay.c index 1aaecd677..4045d61e1 100644 --- a/runtime/tests/test_host_overlay.c +++ b/runtime/tests/test_host_overlay.c @@ -75,6 +75,29 @@ static void* life_reader(void* arg) { while (!atomic_load(&g_stop)) psx_host_overlay_draw(64, 64); return NULL; } +static void* hud_life_reader(void* arg) { + (void)arg; + while (!atomic_load(&g_stop)) psx_host_hud_draw(64, 64); + return NULL; +} +static void test_hud_lifetime(void) { + pthread_t t; + atomic_store(&g_stop, 0); + pthread_create(&t, NULL, hud_life_reader, NULL); + for (int round = 0; round < 100; ++round) { + LifeCtx* c = (LifeCtx*)calloc(1, sizeof(*c)); + atomic_store(&c->alive, 1); + psx_host_set_hud_draw_cb(life_draw, c); + while (atomic_load(&c->calls) < 2) {} + /* Also models the activation-reset / renderer-shutdown clear. */ + psx_host_set_hud_draw_cb(NULL, NULL); + atomic_store(&c->alive, 0); + CHECK(atomic_load(&c->used_dead) == 0); + free(c); + } + atomic_store(&g_stop, 1); + pthread_join(t, NULL); +} static void test_draw_lifetime(void) { pthread_t t; atomic_store(&g_stop, 0); @@ -99,6 +122,7 @@ static void test_draw_lifetime(void) { } #else static void test_draw_lifetime(void) {} +static void test_hud_lifetime(void) {} #endif /* Changing the callback from inside it must not deadlock. */ @@ -114,6 +138,18 @@ static void test_draw_reentrant(void) { psx_host_overlay_draw(64, 64); CHECK(n == 1 && !psx_host_overlay_has_draw()); } +static void hud_self_unregister(int w, int h, void* p) { + (void)w; (void)h; + ++*(int*)p; + psx_host_set_hud_draw_cb(NULL, NULL); +} +static void test_hud_reentrant(void) { + int calls = 0; + psx_host_set_hud_draw_cb(hud_self_unregister, &calls); + psx_host_hud_draw(64, 64); + psx_host_hud_draw(64, 64); + CHECK(calls == 1); +} static int sunk = 0; static int sink(const void *ev, void *ctx) { (void)ctx; sunk++; return *(const int *)ev == 1; } @@ -237,6 +273,8 @@ static void test_cmdline_quote(void) { } int main(void) { + test_hud_lifetime(); + test_hud_reentrant(); test_draw_lifetime(); test_draw_reentrant(); test_cmdline_quote(); diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index 71680a21d..ba605da2c 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -1,6 +1,7 @@ #include "mod_runtime.h" #include "mod_netplay.h" #include "host_launch_timing.h" +#include "host_overlay.h" #include "host_file_identity.h" #include "mod_packages.h" #include "mod_plugins.h" @@ -107,6 +108,8 @@ extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) { } extern "C" void psx_ram_reset_size_request(void) {} static int hd_shutdown_calls, hd_configure_calls, hd_dump_value; +static int hud_calls; +static void old_session_hud(int, int, void*) { ++hud_calls; } static int hd_replacements_value, hd_reload_calls; static std::string hd_root; extern "C" void gpu_hd_textures_shutdown(void) { ++hd_shutdown_calls; hd_root.clear(); } @@ -724,7 +727,10 @@ int main(int argc, char** argv) { psx_mod_function_entry(&entry_cpu, 0x80003000u); check(g_psx_mod_function_entry_hooks == 0 && active_entry_hits == 0, "function-entry hooks must not run before plugin activation"); + psx_host_set_hud_draw_cb(old_session_hud, nullptr); mod_runtime_activate_plugins(); + psx_host_hud_draw(64, 64); + check(hud_calls == 0, "activation clears the prior session HUD before resetting plugins"); check(activation_calls == 1, "resolved trusted plugin must activate before runtime startup"); entry_cpu.pc = 0x80004000u; From 3733036297addf26d7b0a9698163e9f0a991555b Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 22 Oct 2026 17:25:02 -0700 Subject: [PATCH 3/3] Model non-unwinding guest callback escape in the Windows fixture --- runtime/tests/test_mod_runtime.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index ba605da2c..b7331f567 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -181,7 +181,18 @@ static int disabled_entry_hits; static int unselected_entry_hits; static uint32_t active_entry_last; static int entry_test_mode, nested_result; +#if defined(_WIN32) && defined(__GNUC__) && !defined(__clang__) +/* Model a guest scheduler jump which abandons native C++ scopes. Asking the + * Windows CRT longjmp to unwind those scopes can itself fault in SEH, before + * this fixture gets to verify the stranded callback-context guard. */ +static intptr_t callback_escape[5]; +#define callback_setjmp() __builtin_setjmp(callback_escape) +#define callback_longjmp() __builtin_longjmp(callback_escape, 1) +#else static std::jmp_buf callback_escape; +#define callback_setjmp() setjmp(callback_escape) +#define callback_longjmp() std::longjmp(callback_escape, 1) +#endif static uint32_t nested_pc; static void test_active_entry(CPUState* cpu, uint32_t address) { active_entry_hits++; @@ -208,12 +219,12 @@ static void test_active_entry(CPUState* cpu, uint32_t address) { cpu->gpr[2] = 0x87654321u; } else if (entry_test_mode == 5) { /* Guest scheduling/RFE can abandon the callback's C++ scope. */ - std::longjmp(callback_escape, 1); + callback_longjmp(); } else if (entry_test_mode == 6) { if (!psx_mod_finish_function(cpu)) failures++; ModFunctionEntryContext interrupted{}; mod_runtime_function_entry_context_save(&interrupted); - if (setjmp(callback_escape) == 0) { + if (callback_setjmp() == 0) { CPUState nested{}; entry_test_mode = 5; psx_mod_function_entry(&nested, address); @@ -877,7 +888,7 @@ int main(int argc, char** argv) { entry_test_mode = 0; entry_test_mode = 5; - if (setjmp(callback_escape) == 0) { + if (callback_setjmp() == 0) { psx_mod_function_entry(&entry_cpu, 0x80003000u); check(false, "callback must escape without its native cleanup"); }