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 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-08 18:51:52 -07:00
parent facdf01ac6
commit e21226feb7
8 changed files with 100 additions and 3 deletions
+13
View File
@@ -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
+13
View File
@@ -9258,6 +9258,18 @@ static void handle_host_pause(int id, const char *json)
psx_host_pause_depth());
}
/* host_key name=<SDL scancode 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 },
+16
View File
@@ -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; }
+36 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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
+1
View File
@@ -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);
+19
View File
@@ -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");
+1 -1
View File
@@ -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;
}