Host pause API (in-game overlay P1)

psx_host_pause_push/pop: a nesting, host-owned pause. While it is held the
runtime freezes the guest at the vblank present (as the save-state menu
does), keeps presenting the last frame and pauses audio. Push refuses
during netplay. Nothing pushes unless an overlay provider (or the debug
server's new host_pause command) asks, so behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-08 18:50:44 -07:00
parent 54abcb70b4
commit 138b44a58e
9 changed files with 182 additions and 1 deletions
+3
View File
@@ -793,6 +793,9 @@ if(BUILD_TESTING)
tests/test_pad_external_input.c src/pad_external_input.c)
target_include_directories(pad_external_input_test PRIVATE include src)
add_test(NAME pad_external_input_test COMMAND pad_external_input_test)
add_executable(host_overlay_test tests/test_host_overlay.c src/host_overlay.c)
target_include_directories(host_overlay_test PRIVATE include src)
add_test(NAME host_overlay_test COMMAND host_overlay_test)
add_executable(gte_register_access_test
tests/test_gte_register_access.cpp
+30
View File
@@ -0,0 +1,30 @@
/* host_overlay.h -- hooks for an optional in-game menu ("overlay provider").
*
* Every hook is inert until something registers with it: a title with no
* overlay provider runs exactly as before. Sections are added by the
* docs/IN_GAME_OVERLAY.md PR stack (P1 pause, P2 draw, P3 input, ...).
*/
#ifndef PSX_HOST_OVERLAY_H
#define PSX_HOST_OVERLAY_H
#ifdef __cplusplus
extern "C" {
#endif
/* ---- P1: host pause --------------------------------------------------------
* A nesting pause owned by the host (not the guest): while depth > 0 the
* runtime holds the guest at the vblank present, keeps presenting the last
* frame and pauses audio. Push refuses (returns 0) while the refusal probe
* says so -- the runtime installs psx_netplay_active() there, because pausing
* one peer would stall the session. Main thread only. */
typedef int (*PsxHostPauseRefuseFn)(void);
int psx_host_pause_push(const char *reason);
void psx_host_pause_pop(void);
int psx_host_pause_depth(void);
const char *psx_host_pause_reason(void);
void psx_host_pause_set_refuse_probe(PsxHostPauseRefuseFn fn);
#ifdef __cplusplus
}
#endif
#endif
+3
View File
@@ -21,6 +21,9 @@ typedef struct PsxSdlAudioSpec {
SDL_AudioDeviceID psx_sdl_audio_open(
const PsxSdlAudioSpec *want, PsxSdlAudioSpec *have);
int psx_sdl_audio_resume(SDL_AudioDeviceID device);
/* Host pause (host_overlay.h P1): stop/start the device without dropping
* queued samples. */
void psx_sdl_audio_set_paused(SDL_AudioDeviceID device, int paused);
void psx_sdl_audio_close(SDL_AudioDeviceID device);
void psx_sdl_audio_clear(SDL_AudioDeviceID device);
Uint32 psx_sdl_audio_queued_size(SDL_AudioDeviceID device);
+1
View File
@@ -390,6 +390,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/psx_openxr.c
${PSXRECOMP_ROOT}/runtime/src/mod_controller_source.c
${PSXRECOMP_ROOT}/runtime/src/pad_external_input.c
${PSXRECOMP_ROOT}/runtime/src/host_overlay.c
${PSXRECOMP_ROOT}/runtime/src/mod_pad_transform.c
${PSXRECOMP_ROOT}/runtime/src/vr_pose_math.c
${PSXRECOMP_ROOT}/runtime/src/gpu_vk_renderer.c
+13
View File
@@ -9246,6 +9246,18 @@ static void handle_debug_key(int id, const char *json)
send_fmt("{\"id\":%d,\"ok\":%s,\"state\":\"%s\"}", id, ok ? "true" : "false", msg);
}
/* host_pause on=<1|0>: push/pop a host pause (host_overlay.h P1). */
#include "host_overlay.h"
static void handle_host_pause(int id, const char *json)
{
int on = json_get_int(json, "on", -1);
int ok = 1;
if (on == 1) ok = psx_host_pause_push("debug");
else if (on == 0) psx_host_pause_pop();
send_fmt("{\"id\":%d,\"ok\":%s,\"depth\":%d}", id, ok ? "true" : "false",
psx_host_pause_depth());
}
static void handle_ws_nw(int id, const char *json)
{
int on = json_get_int(json, "on", -1);
@@ -15907,6 +15919,7 @@ static const CmdEntry s_commands[] = {
{ "window_size", handle_window_size },
{ "ws_nw", handle_ws_nw },
{ "debug_key", handle_debug_key },
{ "host_pause", handle_host_pause },
{ "scanline", handle_scanline },
{ "ws_backdrop_ring", handle_ws_backdrop_ring },
{ "ws_ui_groups", handle_ws_ui_groups },
+29
View File
@@ -0,0 +1,29 @@
/* host_overlay.c -- see host_overlay.h. */
#include "host_overlay.h"
#include <stddef.h>
/* ---- P1: host pause ------------------------------------------------------ */
#define PSX_HOST_PAUSE_MAX 8
static int s_pause_depth = 0;
static const char *s_pause_reason[PSX_HOST_PAUSE_MAX];
static PsxHostPauseRefuseFn s_pause_refuse = NULL;
int psx_host_pause_push(const char *reason) {
if (s_pause_refuse && s_pause_refuse()) return 0;
if (s_pause_depth >= PSX_HOST_PAUSE_MAX) return 0;
s_pause_reason[s_pause_depth++] = reason ? reason : "host";
return 1;
}
void psx_host_pause_pop(void) {
if (s_pause_depth > 0) s_pause_reason[--s_pause_depth] = NULL;
}
int psx_host_pause_depth(void) { return s_pause_depth; }
const char *psx_host_pause_reason(void) {
return s_pause_depth > 0 ? s_pause_reason[s_pause_depth - 1] : NULL;
}
void psx_host_pause_set_refuse_probe(PsxHostPauseRefuseFn fn) { s_pause_refuse = fn; }
+53 -1
View File
@@ -125,7 +125,8 @@ extern "C" void psx_event_step_conservative_env_init(void);
#include "sbi_setup.h"
#include "disc_path.h"
#include "iso_reader.h" /* text-image guard: extract the boot EXE from the disc */
#include "psx_keybinds.h" /* configurable keyboard->DualShock keybinds (keybinds.ini) */
#include "psx_keybinds.h"
#include "host_overlay.h" /* optional in-game menu hooks (P1 pause, ...) */ /* configurable keyboard->DualShock keybinds (keybinds.ini) */
#include "psx_window_icon.h"
#if defined(RECOMP_LAUNCHER)
@@ -8021,6 +8022,53 @@ static void rewind_host_pause_loop(void) {
savestate_input_guard_arm();
}
/* Host pause (host_overlay.h, P1). Same freeze as the save-state menu: the
* guest is held at the vblank present, the last frame keeps presenting and
* audio is paused, until every psx_host_pause_push() has been popped. Nothing
* pushes unless an overlay provider is registered, so titles without one never
* enter this loop. */
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 void host_pause_loop(void) {
psx_local_mouse_begin(sdl_window, false);
psx_local_mouse_reset();
freeze_heartbeat_set_paused(1);
psx_sdl_audio_set_paused(sdl_audio_device, 1);
while (psx_host_pause_depth() > 0) {
#ifndef PSX_NO_DEBUG_TOOLS
debug_server_poll();
#endif
SDL_Event ev;
while (!g_headless && SDL_PollEvent(&ev)) {
(void)psx_local_mouse_event(ev);
if (host_pause_event(ev)) continue;
if (ev.type == SDL_QUIT) {
psx_crash_trace_set_exit_origin("sdl_window_close");
shutdown_runtime();
std::exit(0);
} else if (ev.type == SDL_CONTROLLERDEVICEADDED) {
refresh_player_devices();
} else if (ev.type == SDL_CONTROLLERDEVICEREMOVED) {
close_controller();
refresh_player_devices();
}
}
host_pause_service();
if (!g_headless) rewind_pause_present();
starvation_watchdog_heartbeat();
SDL_Delay(g_headless ? 1 : 8);
}
psx_sdl_audio_set_paused(sdl_audio_device, 0);
freeze_heartbeat_set_paused(0);
/* Swallow the still-held resume press so it doesn't reach the game. */
savestate_input_guard_arm();
}
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. */
static void savestate_menu_host_pause_loop(void) {
psx_local_mouse_begin(sdl_window, false);
@@ -8486,6 +8534,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
savestate_menu_host_pause_loop();
if (psx_rewind_is_open())
rewind_host_pause_loop();
if (psx_host_pause_depth() > 0)
host_pause_loop();
} else if (host_pad_layer(nullptr, nullptr, nullptr, nullptr)) {
/* Headless with the debug host layer: the host shortcuts that need no
* window (Rewind capture/open, fast-forward latch) poll as they would
@@ -8872,6 +8922,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
// A hotkey acquired during pacing may open a host menu.
if (savestate_menu_open) savestate_menu_host_pause_loop();
if (psx_rewind_is_open()) rewind_host_pause_loop();
if (psx_host_pause_depth() > 0) host_pause_loop();
psx_local_mouse_begin(sdl_window, local_mouse_live(override));
} else {
SDL_PumpEvents(); // retain native timing when no policy exists
@@ -15888,6 +15939,7 @@ int main(int argc, char** argv) {
#if defined(RECOMP_LAUNCHER)
launcher_boot_timing_mark("host:main_enter");
#endif
psx_host_pause_set_refuse_probe(host_pause_refuse_netplay);
/* --audit-mod-plugins <manifest-root>...: build-time check, no emulation.
* Every trusted implementation this executable registered must be named
+12
View File
@@ -67,6 +67,13 @@ extern "C" int psx_sdl_audio_resume(SDL_AudioDeviceID)
: -1;
}
extern "C" void psx_sdl_audio_set_paused(SDL_AudioDeviceID, int paused)
{
if (!s_audio_stream) return;
if (paused) (void)SDL_PauseAudioStreamDevice(s_audio_stream);
else (void)SDL_ResumeAudioStreamDevice(s_audio_stream);
}
extern "C" void psx_sdl_audio_close(SDL_AudioDeviceID)
{
if (s_audio_stream) SDL_DestroyAudioStream(s_audio_stream);
@@ -140,6 +147,11 @@ extern "C" int psx_sdl_audio_resume(SDL_AudioDeviceID device)
return 0;
}
extern "C" void psx_sdl_audio_set_paused(SDL_AudioDeviceID device, int paused)
{
SDL_PauseAudioDevice(device, paused ? 1 : 0);
}
extern "C" void psx_sdl_audio_close(SDL_AudioDeviceID device)
{
SDL_CloseAudioDevice(device);
+38
View File
@@ -0,0 +1,38 @@
/* test_host_overlay.c -- unit tests for host_overlay.c hooks. */
#include "host_overlay.h"
#include <stdio.h>
#include <string.h>
static int g_fail = 0;
#define CHECK(c) do { if (!(c)) { fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #c); g_fail++; } } while (0)
static int refuse_on = 0;
static int refuse(void) { return refuse_on; }
static void test_pause(void) {
CHECK(psx_host_pause_depth() == 0 && psx_host_pause_reason() == NULL);
CHECK(psx_host_pause_push("overlay"));
CHECK(psx_host_pause_push("other"));
CHECK(psx_host_pause_depth() == 2 && strcmp(psx_host_pause_reason(), "other") == 0);
psx_host_pause_pop();
CHECK(strcmp(psx_host_pause_reason(), "overlay") == 0);
psx_host_pause_pop();
psx_host_pause_pop(); /* extra pop is harmless */
CHECK(psx_host_pause_depth() == 0);
psx_host_pause_set_refuse_probe(refuse);
refuse_on = 1;
CHECK(!psx_host_pause_push("netplay") && psx_host_pause_depth() == 0);
refuse_on = 0;
CHECK(psx_host_pause_push(NULL) && strcmp(psx_host_pause_reason(), "host") == 0);
psx_host_pause_pop();
for (int i = 0; i < 8; i++) CHECK(psx_host_pause_push("n"));
CHECK(!psx_host_pause_push("overflow"));
for (int i = 0; i < 8; i++) psx_host_pause_pop();
}
int main(void) {
test_pause();
printf("host_overlay_test: %s\n", g_fail ? "FAIL" : "ok");
return g_fail ? 1 : 0;
}