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] 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 0340c49d..85843c06 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 d1bfcf3f..0a2cd648 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 67b2f4a0..87d2c5bc 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 dfc7536c..83463a74 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 5bfe6257..28792e85 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 1aaecd67..4045d61e 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 71680a21..ba605da2 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;