Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,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
Expand Down
10 changes: 10 additions & 0 deletions runtime/include/host_overlay.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ 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. 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 */

/* ---- 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.
Expand Down
7 changes: 5 additions & 2 deletions runtime/src/gpu_gl_renderer.c
Original file line number Diff line number Diff line change
Expand Up @@ -6575,6 +6575,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();
Expand Down Expand Up @@ -9947,8 +9948,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);
}
}
Expand Down
46 changes: 46 additions & 0 deletions runtime/src/host_overlay.c
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,52 @@ 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;
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;
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 -------------------------------------------------------- */
static PsxHostInputSinkFn s_sink_fn = NULL;
static void *s_sink_ctx = NULL;
Expand Down
3 changes: 3 additions & 0 deletions runtime/src/mod_runtime.cpp
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -2964,6 +2965,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;
Expand Down
38 changes: 38 additions & 0 deletions runtime/tests/test_host_overlay.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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. */
Expand All @@ -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; }
Expand Down Expand Up @@ -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();
Expand Down
23 changes: 20 additions & 3 deletions runtime/tests/test_mod_runtime.cpp
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -108,6 +109,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(); }
Expand Down Expand Up @@ -179,7 +182,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++;
Expand All @@ -206,12 +220,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);
Expand Down Expand Up @@ -773,7 +787,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;
Expand Down Expand Up @@ -920,7 +937,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");
}
Expand Down
Loading