From e21226feb74a0a3c583aa3c58f8bc222e120bbda Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:51:52 -0700 Subject: [PATCH] Overlay input sink and UI capture gate (in-game overlay P3) psx_host_set_input_sink: an overlay provider sees host SDL events first, in the event drain and the host pause loop, and may consume them. psx_host_set_ui_capture: while on, local game input reads released and centred (netplay included, so peers receive "nothing pressed") and the external-input gate stays closed. Debug builds gain host_key, which pushes a real SDL key event for scripted runs. Inert without a provider. Co-Authored-By: Claude Opus 5.5 --- runtime/include/host_overlay.h | 13 +++++++++ runtime/src/debug_server.c | 13 +++++++++ runtime/src/host_overlay.c | 16 +++++++++++ runtime/src/main.cpp | 37 ++++++++++++++++++++++++- runtime/src/pad_external_input.c | 2 +- runtime/src/pad_external_input.h | 1 + runtime/tests/test_host_overlay.c | 19 +++++++++++++ runtime/tests/test_pad_external_input.c | 2 +- 8 files changed, 100 insertions(+), 3 deletions(-) diff --git a/runtime/include/host_overlay.h b/runtime/include/host_overlay.h index 89e44d62..3bb5e8c9 100644 --- a/runtime/include/host_overlay.h +++ b/runtime/include/host_overlay.h @@ -44,6 +44,19 @@ 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 */ +/* ---- 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. + * While ui_capture is on, local game input reads as released/centred, in + * netplay too (the peer then simply receives "nothing pressed"), so a menu + * open over a running session never leaks presses into the game. + * `event` is an SDL_Event*. NULL unregisters. */ +typedef int (*PsxHostInputSinkFn)(const void *event, void *ctx); +void psx_host_set_input_sink(PsxHostInputSinkFn fn, void *ctx); +int psx_host_input_sink_dispatch(const void *event); +void psx_host_set_ui_capture(int on); +int psx_host_ui_capture_active(void); + #ifdef __cplusplus } #endif diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index a5240ff9..a95cb426 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -9258,6 +9258,18 @@ static void handle_host_pause(int id, const char *json) psx_host_pause_depth()); } +/* host_key name= down=<1|0> (both when omitted). */ +extern int psx_debug_push_host_key(const char *name, int down); +static void handle_host_key(int id, const char *json) +{ + char name[64] = ""; + json_get_str(json, "name", name, sizeof name); + int down = json_get_int(json, "down", -1); + int ok = down < 0 ? (psx_debug_push_host_key(name, 1) && psx_debug_push_host_key(name, 0)) + : psx_debug_push_host_key(name, down); + send_fmt("{\"id\":%d,\"ok\":%s}", id, ok ? "true" : "false"); +} + static void handle_ws_nw(int id, const char *json) { int on = json_get_int(json, "on", -1); @@ -15920,6 +15932,7 @@ static const CmdEntry s_commands[] = { { "ws_nw", handle_ws_nw }, { "debug_key", handle_debug_key }, { "host_pause", handle_host_pause }, + { "host_key", handle_host_key }, { "scanline", handle_scanline }, { "ws_backdrop_ring", handle_ws_backdrop_ring }, { "ws_ui_groups", handle_ws_ui_groups }, diff --git a/runtime/src/host_overlay.c b/runtime/src/host_overlay.c index 3b6f1223..78fe52e3 100644 --- a/runtime/src/host_overlay.c +++ b/runtime/src/host_overlay.c @@ -101,3 +101,19 @@ void psx_host_overlay_draw(int width, int height) { atomic_fetch_sub(&s_draw_inflight, 1); } +/* ---- P3: input sink -------------------------------------------------------- */ +static PsxHostInputSinkFn s_sink_fn = NULL; +static void *s_sink_ctx = NULL; +static volatile int s_ui_capture = 0; + +void psx_host_set_input_sink(PsxHostInputSinkFn fn, void *ctx) { + s_sink_fn = fn; + s_sink_ctx = ctx; +} + +int psx_host_input_sink_dispatch(const void *event) { + return (s_sink_fn && event) ? (s_sink_fn(event, s_sink_ctx) != 0) : 0; +} + +void psx_host_set_ui_capture(int on) { s_ui_capture = on ? 1 : 0; } +int psx_host_ui_capture_active(void) { return s_ui_capture; } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index a27b0d93..ac7c6160 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -6176,6 +6176,8 @@ static int capture_host_layer_pad(PsxNetPad* out, uint16_t word, static int capture_pad_slot_view(int s, PsxNetPad* out, bool guarded, bool host_view) { if (!out) return 0; + /* An open overlay menu owns the controls: the game sees released pads. */ + if (psx_host_ui_capture_active()) guarded = true; out->buttons = 0xFFFFu; out->lx = out->ly = out->rx = out->ry = 0x80u; out->analog = 0; @@ -6276,6 +6278,12 @@ static int capture_pad_slot_view(int s, PsxNetPad* out, bool guarded, out->buttons = btn; out->lx = st[0]; out->ly = st[1]; out->rx = st[2]; out->ry = st[3]; + if (psx_host_ui_capture_active()) { + /* Overlay open during netplay: publish "nothing pressed", which every + * peer simulates identically. */ + out->buttons = 0xFFFFu; + out->lx = out->ly = out->rx = out->ry = 0x80u; + } out->analog = (uint8_t)frame_type; out->connected = 1; return 1; @@ -8030,7 +8038,9 @@ static void rewind_host_pause_loop(void) { static void host_pause_service(void); /* P6 fills this in; no-op until then */ static void host_pause_service(void) {} static bool host_pause_event(const SDL_Event& ev); /* P3: input sink */ -static bool host_pause_event(const SDL_Event&) { return false; } +static bool host_pause_event(const SDL_Event& ev) { + return psx_host_input_sink_dispatch(&ev) != 0; +} static void host_pause_loop(void) { psx_local_mouse_begin(sdl_window, false); @@ -8075,6 +8085,28 @@ static void host_pause_loop(void) { savestate_input_guard_arm(); } +/* Debug: inject a host key / gamepad button as an SDL event, so scripted + * runs can drive host UI (the overlay) through the real event path. */ +extern "C" int psx_debug_push_host_key(const char *name, int down) { + const SDL_Scancode sc = SDL_GetScancodeFromName(name ? name : ""); + if (sc == SDL_SCANCODE_UNKNOWN) return 0; + SDL_Event ev; + SDL_zero(ev); +#if defined(PSX_SDL3) + ev.type = down ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP; + ev.key.scancode = sc; + ev.key.key = SDL_GetKeyFromScancode(sc, SDL_KMOD_NONE, false); + ev.key.down = down != 0; + ev.key.windowID = sdl_window ? SDL_GetWindowID(sdl_window) : 0; +#else + ev.type = down ? SDL_KEYDOWN : SDL_KEYUP; + ev.key.keysym.scancode = sc; + ev.key.keysym.sym = SDL_GetKeyFromScancode(sc); + ev.key.state = down ? SDL_PRESSED : SDL_RELEASED; +#endif + return SDL_PushEvent(&ev) ? 1 : 0; +} + static int host_pause_refuse_netplay(void) { return psx_netplay_active() ? 1 : 0; } /* Freeze guest in vblank present while the save-state slot menu is open. */ @@ -8220,6 +8252,8 @@ static bool drain_host_events() { SDL_Event ev; while (SDL_PollEvent(&ev)) { if (psx_local_mouse_event(ev)) continue; + /* In-game overlay provider sees events first (host_overlay.h P3). */ + if (psx_host_input_sink_dispatch(&ev)) continue; if (ev.type == SDL_QUIT) { if (psx_netplay_active()) { netplay_soft_exit("sdl_window_close"); @@ -8366,6 +8400,7 @@ static bool local_mouse_live(int override_word) { g.savestate_menu_open = savestate_menu_open; g.rewind_open = psx_rewind_is_open(); g.input_guard = savestate_input_guard_active(); + g.ui_capture = psx_host_ui_capture_active(); return pad_ext_live(&g) != 0; } diff --git a/runtime/src/pad_external_input.c b/runtime/src/pad_external_input.c index 04c51898..e0ebec74 100644 --- a/runtime/src/pad_external_input.c +++ b/runtime/src/pad_external_input.c @@ -5,7 +5,7 @@ int pad_ext_live(const PadExtGate *g) { return g && !g->injected_input && !g->headless && !g->netplay_active && !g->netplay_resim && !g->selfcheck_locked && !g->selfcheck_resim && !g->render_pass && !g->savestate_menu_open && !g->rewind_open && - !g->input_guard; + !g->input_guard && !g->ui_capture; } /* Stage 2b: run the title transform over the resolved pad and pack its diff --git a/runtime/src/pad_external_input.h b/runtime/src/pad_external_input.h index 6f7598fb..9679f9cd 100644 --- a/runtime/src/pad_external_input.h +++ b/runtime/src/pad_external_input.h @@ -85,6 +85,7 @@ typedef struct PadExtGate { int savestate_menu_open; int rewind_open; int input_guard; /* savestate input guard armed */ + int ui_capture; /* host overlay owns input (host_overlay.h P3) */ } PadExtGate; int pad_ext_live(const PadExtGate *g); diff --git a/runtime/tests/test_host_overlay.c b/runtime/tests/test_host_overlay.c index cf2ad265..da8ef608 100644 --- a/runtime/tests/test_host_overlay.c +++ b/runtime/tests/test_host_overlay.c @@ -111,9 +111,28 @@ static void test_draw_reentrant(void) { CHECK(n == 1 && !psx_host_overlay_has_draw()); } +static int sunk = 0; +static int sink(const void *ev, void *ctx) { (void)ctx; sunk++; return *(const int *)ev == 1; } + +static void test_sink(void) { + int a = 1, b = 2; + CHECK(!psx_host_input_sink_dispatch(&a)); /* none registered */ + psx_host_set_input_sink(sink, NULL); + CHECK(psx_host_input_sink_dispatch(&a) && !psx_host_input_sink_dispatch(&b) && sunk == 2); + CHECK(!psx_host_input_sink_dispatch(NULL) && sunk == 2); + psx_host_set_input_sink(NULL, NULL); + CHECK(!psx_host_input_sink_dispatch(&a)); + CHECK(!psx_host_ui_capture_active()); + psx_host_set_ui_capture(5); + CHECK(psx_host_ui_capture_active() == 1); + psx_host_set_ui_capture(0); + CHECK(!psx_host_ui_capture_active()); +} + int main(void) { test_draw_lifetime(); test_draw_reentrant(); + test_sink(); test_draw(); test_pause(); printf("host_overlay_test: %s\n", g_fail ? "FAIL" : "ok"); diff --git a/runtime/tests/test_pad_external_input.c b/runtime/tests/test_pad_external_input.c index c36fd424..04d9d8c8 100644 --- a/runtime/tests/test_pad_external_input.c +++ b/runtime/tests/test_pad_external_input.c @@ -280,7 +280,7 @@ int main(void) { int *f[] = { &g.injected_input, &g.headless, &g.netplay_active, &g.netplay_resim, &g.selfcheck_locked, &g.selfcheck_resim, &g.render_pass, &g.savestate_menu_open, &g.rewind_open, - &g.input_guard }; + &g.input_guard, &g.ui_capture }; for (unsigned i = 0; i < sizeof f / sizeof f[0]; i++) { *f[i] = 1; CHECK(pad_ext_live(&g) == 0); *f[i] = 0; }