diff --git a/docs/TCP_COMMANDS.md b/docs/TCP_COMMANDS.md index 486a7733..d937707a 100644 --- a/docs/TCP_COMMANDS.md +++ b/docs/TCP_COMMANDS.md @@ -493,9 +493,9 @@ between Play and TCP availability. ## Complete command index (generated) -**375 commands registered** — 362 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). +**376 commands registered** — 363 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). -84 of 375 have prose above; **291 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":""}` and read the reply, or find its `handle_*` function in the server source. +84 of 376 have prose above; **292 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":""}` and read the reply, or find its `handle_*` function in the server source. Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code. @@ -687,6 +687,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc | `mdec_trace` | ✓ | | | | `mdec_trace_clear` | ✓ | | | | `mem_words` | ✓ | | | +| `menu_host` | ✓ | | | | `mmio_clear` | ✓ | | ✓ | | `mmio_dump` | ✓ | | ✓ | | `mmx6_freshfix` | ✓ | | | diff --git a/runtime/include/psx_menu_host.h b/runtime/include/psx_menu_host.h index 01c77310..d85dfff3 100644 --- a/runtime/include/psx_menu_host.h +++ b/runtime/include/psx_menu_host.h @@ -77,6 +77,11 @@ typedef struct PsxMenuHost { void (*launcher_settings_get)(struct RecompLauncherCSettings *out); unsigned (*launcher_settings_apply)(const struct RecompLauncherCSettings *next); const char *assets_dir; /* the title's assets folder */ + /* 1 when the runtime chose this provider because the title chose none + * (the UI library's default provider, recomp_ui_psx_menu.h). Such a + * provider keeps the title's default presentation: it may decline to + * attach when it would only add a menu the title did not have. */ + int default_selection; } PsxMenuHost; typedef struct PsxMenuProvider { diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 3a7ee181..9ff60245 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -9267,6 +9267,17 @@ static void handle_host_pause(int id, const char *json) psx_host_pause_depth()); } +/* menu_host: in-game menu host state (psx_menu_host.h): provider, attached, + * open, pause, ui capture, and the video/netplay snapshot. */ +extern int psx_debug_menu_host_json(char *out, size_t cap); +static void handle_menu_host(int id, const char *json) +{ + char body[768]; + (void)json; + psx_debug_menu_host_json(body, sizeof body); + send_fmt("{\"id\":%d,\"ok\":true,%s}", id, body); +} + /* 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) @@ -15952,6 +15963,7 @@ static const CmdEntry s_commands[] = { { "debug_key", handle_debug_key }, { "host_pause", handle_host_pause }, { "host_key", handle_host_key }, + { "menu_host", handle_menu_host }, { "host_mouse", handle_host_mouse }, { "scanline", handle_scanline }, { "ws_backdrop_ring", handle_ws_backdrop_ring }, diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 796a46e1..96edd41c 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -3477,6 +3477,9 @@ static void shutdown_runtime(void); * session ends but the pending launch (and its seats/signals) must survive * into the launcher loop that starts it. */ static int g_ovl_launch_to_start = 0; +/* The in-game menu asked to go back to the frontend from a local game (not a + * match, no pending launch): the launcher opens normally, not on the lobby. */ +static int g_ovl_frontend_from_local = 0; static void recomp_overlay_detach(void); /* in-game menu host, below */ /* Set when this process started netplay from the lobby room — soft-exit returns @@ -16150,7 +16153,13 @@ void mh_video_get(PsxHostVideoSettings* out) { unsigned mh_restart_bits(const PsxHostVideoSettings& next) { std::lock_guard lk(g_ovl_mu); const PsxHostVideoSettings now = g_ovl_video_current; - unsigned out = psx_host_video_restart_bits(&g_ovl_video_boot, &next); + /* A restart group waits only when this edit touches it (next differs from + * the live value) and the result differs from what the session booted + * with. The live internal resolution moves under dynamic resolution, so + * comparing with boot alone flagged edits that never touched it (an + * in-game texture filter change relaunched the game). */ + unsigned out = psx_host_video_restart_bits(&g_ovl_video_boot, &next) & + psx_host_video_restart_bits(&now, &next); /* In a netplay match only presentation applies live (psx_video_apply_live); * the rest waits for the next session. */ if (g_ovl_netplay && @@ -16309,6 +16318,7 @@ PsxMenuHost g_menu_host = { nullptr, nullptr, nullptr, #endif nullptr, + 0, }; int ovl_sink(const void* ev, void*) { return g_ovl && g_ovl->event ? g_ovl->event(g_ovl->ctx, ev) : 0; } @@ -16316,9 +16326,25 @@ void ovl_draw(int w, int h, void*) { if (g_ovl && g_ovl->draw) g_ovl->draw(g_ovl } // namespace /* Main thread, once per GL context (windowed OpenGL only). */ +#if defined(__has_include) +#if __has_include("recomp_ui_psx_menu.h") +#include "recomp_ui_psx_menu.h" /* the maintained UI's provider: the default */ +#define PSX_MENU_HAS_DEFAULT_PROVIDER 1 +#endif +#endif + static void recomp_overlay_setup(const char* argv0, const std::string& title) { const char* e = std::getenv("PSX_OVERLAY"); const PsxMenuProvider* p = psx_menu_provider(); + g_menu_host.default_selection = 0; +#if defined(PSX_MENU_HAS_DEFAULT_PROVIDER) + /* No title choice: the UI the title is built with (recomp-ui) provides the + * menu, and decides from default_selection what to offer by default. */ + if (!p) { + p = recomp_ui_psx_menu_provider(); + g_menu_host.default_selection = 1; + } +#endif if (!p || g_ovl_on || !g_gl_active || g_headless || (e && *e == '0')) return; const std::filesystem::path dir = exe_dir_from_argv(argv0); g_ovl_name = title; @@ -16336,6 +16362,23 @@ static void recomp_overlay_setup(const char* argv0, const std::string& title) { std::fprintf(stdout, "psxrecomp: in-game menu ready (%s)\n", p->name ? p->name : "provider"); } +/* Debug server: menu host state (provider, open, pause, capture, snapshot). */ +extern "C" int psx_debug_menu_host_json(char* out, size_t cap) { + PsxHostVideoSettings v; + mh_video_get(&v); + const PsxMenuProvider* p = g_ovl ? g_ovl : psx_menu_provider(); + return std::snprintf(out, cap, + "\"provider\":\"%s\",\"attached\":%d,\"default_selection\":%d,\"open\":%d," + "\"paused\":%d,\"pause_depth\":%d,\"ui_capture\":%d,\"netplay\":%d," + "\"texture_filter\":%d,\"fmv_filter\":%d,\"present_linear\":%d,\"fullscreen\":%d," + "\"render_thread\":%d,\"frame_generation\":%d,\"disc_count\":%d,\"disc_mounted\":%d", + p && p->name ? p->name : "", g_ovl_on ? 1 : 0, g_menu_host.default_selection, + ovl_open() ? 1 : 0, g_ovl_pause_held ? 1 : 0, psx_host_pause_depth(), + psx_host_ui_capture_active(), mh_netplay_active(), v.texture_filter, v.fmv_filter, + v.present_linear, v.fullscreen, v.render_thread, v.frame_generation, mh_disc_count(), + mh_disc_mounted()); +} + /* Before the GL context goes away (session teardown), with it current: the * draw hook is removed first (it waits for an in-flight draw), then the * provider frees its GL objects. setup attaches again on the next context. */ @@ -16436,15 +16479,26 @@ static void host_pause_service(void) { g_ovl_pause_wanted = 0; if (g_ovl_pause_held) { psx_host_pause_pop(); g_ovl_pause_held = false; } if (g_ovl->set_open) g_ovl->set_open(g_ovl->ctx, 0); + /* A lobby match start (the runtime's pending-launch exit, or the + * provider's own lobby view asking once its match started) keeps + * the launch and resumes the room; any other request from a local + * game is a plain return: the launcher opens as at boot. */ + bool for_launch = false; +#if defined(RECOMP_LAUNCHER) && defined(PSX_HAS_RECOMP_NET) && defined(PSX_HAS_LOBBY_CLIENT) + for_launch = g_lnch_netplay_available && g_lnch_netplay_callbacks.launch_pending && + g_lnch_netplay_callbacks.launch_pending(g_lnch_netplay_callbacks.ctx); +#endif if (psx_netplay_active()) { netplay_soft_exit("menu_to_frontend"); } else { /* End this session and re-enter the frontend loop in-process * (the netplay soft-return path); it then starts whatever the * menu asked for, e.g. the lobby's match. */ - std::fprintf(stdout, "psxrecomp: in-game menu -> frontend loop\n"); + std::fprintf(stdout, "psxrecomp: in-game menu -> frontend loop%s\n", + for_launch ? " (lobby match)" : ""); g_netplay_from_lobby = 1; - g_ovl_launch_to_start = 1; + g_ovl_launch_to_start = for_launch ? 1 : 0; + g_ovl_frontend_from_local = for_launch ? 0 : 1; psx_request_return_to_lobby(); } return; @@ -20729,7 +20783,8 @@ soft_return_lobby: * happens; an automatch room is the server's and cannot. Leave it * here, and reopen the launcher on the browser instead. */ const int rui_resume_room = - ae_np_leave_automatch_room_after_match() ? 0 : 1; + (ae_np_leave_automatch_room_after_match() || g_ovl_frontend_from_local) ? 0 : 1; + g_ovl_frontend_from_local = 0; ae_rui_set_sidecar_paths(argv[0]); g_lnch_expected_serial = game_id; diff --git a/runtime/tests/test_menu_video_snapshot.py b/runtime/tests/test_menu_video_snapshot.py index 80539212..fbb877fa 100644 --- a/runtime/tests/test_menu_video_snapshot.py +++ b/runtime/tests/test_menu_video_snapshot.py @@ -52,7 +52,7 @@ int main() { assert(got.fullscreen==got.texture_filter && got.texture_filter==got.renderer && got.renderer==got.frame_generation && got.frame_generation==got.supersampling && got.supersampling==got.render_thread && got.render_thread==got.present_thread); - const auto next=values(2); + const auto next=values(9); /* differs from every live and boot value */ const unsigned restart=mh_video_apply(&next); assert(restart & PSX_VIDEO_RESTART_RENDERER); ++reads; @@ -61,6 +61,15 @@ int main() { for (int i=0;i<50000;++i) { live=values(1+i%8); netplay=i%2; mh_refresh_snapshot(); } draw.join(); assert(reads==50000 && wrong_thread_reads==0 && g_ovl_video_pending); + /* Restart bits only for groups the edit touches: when the live value has + * moved off boot (dynamic resolution moves the internal resolution), an + * edit that leaves that group at its live value asks for no restart; one + * that changes it does. */ + live=values(1); live.renderer=3; g_video_boot=values(1); netplay=0; mh_refresh_snapshot(); + PsxHostVideoSettings same=live; same.texture_filter=5; + assert(mh_restart_bits(same)==0); + PsxHostVideoSettings changed=live; changed.renderer=4; + assert(mh_restart_bits(changed) & PSX_VIDEO_RESTART_RENDERER); puts("menu video snapshot: coherent draw-thread reads and main-thread ownership PASS"); } '''