diff --git a/docs/TCP_COMMANDS.md b/docs/TCP_COMMANDS.md index f2be50f7..c81a2eeb 100644 --- a/docs/TCP_COMMANDS.md +++ b/docs/TCP_COMMANDS.md @@ -493,9 +493,9 @@ between Play and TCP availability. ## Complete command index (generated) -**385 commands registered** — 372 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). +**386 commands registered** — 373 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). -85 of 385 have prose above; **300 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. +85 of 386 have prose above; **301 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. @@ -837,6 +837,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc | `unwatch` | ✓ | | ✓ | | `vblank_rate` | ✓ | | | | `video_info` | ✓ | | ✓ | +| `video_set` | ✓ | | | | `vk_perf` | ✓ | | | | `vram_peek` | ✓ | ✓ | ✓ | | `vsync_query_hle` | ✓ | | | diff --git a/generated/OpenBIOS.emitter.sha b/generated/OpenBIOS.emitter.sha index 70cb65a7..8979439c 100644 --- a/generated/OpenBIOS.emitter.sha +++ b/generated/OpenBIOS.emitter.sha @@ -1 +1 @@ -61bf4f6d29680623d215fbe407958408cab0a1e08dbda3c62baa25e607d63b7b +86f7329c2692e76133c40fa11cc8cfac214302d8238e05369dc8b263f4dc76fd diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 768350b7..3524e13a 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -3040,6 +3040,15 @@ UserSettings load_user_settings(const fs::path& path) { const auto n = toml::find(v, "supersampling"); if (n >= 1 && n <= 32) { s.supersampling = (int)n; s.has_supersampling = true; } }); + if (v.contains("supersample")) try_get([&]{ + const toml::value& ss = toml::find(v, "supersample"); + const double f = ss.is_integer() ? static_cast(ss.as_integer()) + : toml::get(ss); + if (f >= 1.0 && f <= 4.0) { + s.supersample_milli = static_cast(f * 1000.0 + 0.5); + s.has_supersample = true; + } + }); if (v.contains("internal_resolution")) try_get([&]{ const toml::value& ir = toml::find(v, "internal_resolution"); int value = 0; @@ -3492,6 +3501,8 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) { else f << "internal_resolution = " << s.internal_resolution << "\n"; } + if (s.has_supersample) + f << "supersample = " << (s.supersample_milli / 1000.0) << "\n"; if (s.has_dynamic_resolution) f << "dynamic_resolution = " << (s.dynamic_resolution ? "true" : "false") << "\n"; if (s.has_dynamic_resolution_min && diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 1cca1185..626ceb65 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -1563,6 +1563,9 @@ struct UserSettings { // it wins over supersampling, which is still written (capped at 4) so an // older runtime reading the same file degrades gracefully. bool has_internal_resolution = false; int internal_resolution = 0; + // Supersampling on top of the internal resolution, in thousandths + // (1000 off .. 4000), the player's pick of [video] supersample. + bool has_supersample = false; int supersample_milli = 1000; // Dynamic resolution (RuntimeConfig::video_dynamic_resolution, _min). // Written only once the player changed them, so a title's default can // still move in a later release. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index da100f89..94028664 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1409,6 +1409,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 diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index 9b287802..45e79835 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -398,6 +398,14 @@ int gl_renderer_request_internal_scale(int scale); * 0 when refused or deferred (inside a render pass; it then applies after * the next present). Never call it inside a render pass. */ int gl_renderer_step_internal_scale_now(int scale); +/* Live Internal resolution / Supersampling change: the scale to step to at + * a context-owned sync point (the new ceiling). Returns the scale applied, 0 when + * the backend cannot change it live. */ +int gl_renderer_set_internal_scale_live(int scale); +/* 1 when gl_renderer_set_internal_scale_live can apply on this session (no + * windowed high-resolution mode, no CPU-authoritative dual rendering, a live + * raster context). 0 means a resolution change needs a relaunch. */ +int gl_renderer_internal_scale_live_capable(void); typedef struct GlDynresStats { int level, ceiling, pending; /* pending: requested level, 0 = none */ uint64_t steps, deferred, refused; @@ -610,6 +618,7 @@ int gl_renderer_get_texture_window_batching(void); int gl_renderer_render_thread_start(int max_frames); void gl_renderer_render_thread_stop(void); int gl_renderer_render_thread_active(void); +int gl_renderer_vendor_is_nvidia(void); void gl_renderer_render_thread_frame_boundary(void); void gl_renderer_render_thread_sync(const char *reason); int gl_renderer_render_thread_json(char *out, size_t cap); diff --git a/runtime/include/host_overlay.h b/runtime/include/host_overlay.h index 06c2e504..9064e382 100644 --- a/runtime/include/host_overlay.h +++ b/runtime/include/host_overlay.h @@ -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. @@ -75,10 +85,11 @@ typedef struct PsxHostVideoSettings { int dynamic_resolution; /* 0/1 */ int frame_generation; /* 0/1 */ int renderer; /* restart */ - int internal_resolution; /* restart */ - int supersampling; /* restart */ + int internal_resolution; /* live (psx_video_apply_live) */ + int supersampling; /* legacy factor; restart */ int render_thread; /* restart */ int present_thread; /* restart */ + int supersample_milli; /* live: 1000 off, 1250, 1500, 2000 */ } PsxHostVideoSettings; /* Texture filter values: PSX_TEXTURE_FILTER_* and psx_texture_filter_from_ui @@ -99,6 +110,8 @@ typedef struct PsxHostVideoSettings { #define PSX_VIDEO_LIVE_PRESENT_LINEAR (1u << 4) #define PSX_VIDEO_LIVE_DYNRES (1u << 5) #define PSX_VIDEO_LIVE_FRAME_GEN (1u << 6) +/* Internal resolution or supersampling: the scale moves at the next frame. */ +#define PSX_VIDEO_LIVE_RESOLUTION (1u << 7) /* Pure helpers (unit-tested): which live fields differ, and which restart * fields differ from the values the session booted with. */ @@ -106,6 +119,13 @@ unsigned psx_host_video_live_changes(const PsxHostVideoSettings *prev, const PsxHostVideoSettings *next); unsigned psx_host_video_restart_bits(const PsxHostVideoSettings *boot, const PsxHostVideoSettings *next); +/* psx_host_video_restart_bits for a backend that may not move the resolution + * live: when res_live_ok is 0 (backend incapable, or a live change was + * refused), an internal resolution / supersample that differs from boot keeps + * PSX_VIDEO_RESTART_RESOLUTION. */ +unsigned psx_host_video_restart_bits_backend(const PsxHostVideoSettings *boot, + const PsxHostVideoSettings *next, + int res_live_ok); /* Runtime side (main.cpp). */ unsigned psx_video_apply_live(const PsxHostVideoSettings *next); void psx_video_current(PsxHostVideoSettings *out); diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index f0f9bd85..553bcc1b 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -10704,6 +10704,46 @@ static void handle_dynres_check(int id, const char *json) "\"restored\":%d,\"steps\":%s}", id, (unsigned long long)h0, bad, start, out); } +/* {"cmd":"video_set","internal_resolution":N,"supersample":M} - change the + * Internal resolution preset (lines, 1 native, -1 match display) and/or the + * supersample (thousandths, 1000..4000) through the in-game menu's live path. + * Invalid values are rejected without applying or saving anything. Returns + * the restart bits the change still needs (0 = applied live), the requested + * values, and the values the backend actually applied. */ +static void handle_video_set(int id, const char *json) +{ + extern void psx_video_current(PsxHostVideoSettings *v); + extern unsigned psx_video_apply_live(const PsxHostVideoSettings *next); + extern void psx_video_resolution_applied(int *ir, int *ss_milli, int *live_capable, + int *restart_pending); + const int k_absent = -1000000; + const int ir = json_get_int(json, "internal_resolution", k_absent); + const int ss = json_get_int(json, "supersample", k_absent); + if (ir == k_absent && ss == k_absent) { + send_err(id, "video_set needs internal_resolution and/or supersample"); + return; + } + if (ir != k_absent && !(ir == 1 || ir == -1 || (ir >= 2 && ir <= 8640))) { + send_err(id, "invalid internal_resolution (1 native, -1 match display, 2..8640 lines)"); + return; + } + if (ss != k_absent && (ss < 1000 || ss > 4000)) { + send_err(id, "invalid supersample (1000..4000 thousandths)"); + return; + } + PsxHostVideoSettings v; + psx_video_current(&v); + if (ir != k_absent) v.internal_resolution = ir; + if (ss != k_absent) v.supersample_milli = ss; + const unsigned restart = psx_video_apply_live(&v); + int air = 0, ass = 0, cap = 0, pend = 0; + psx_video_resolution_applied(&air, &ass, &cap, &pend); + send_fmt("{\"id\":%d,\"ok\":true,\"restart\":%u,\"internal_resolution\":%d," + "\"supersample\":%d,\"applied_internal_resolution\":%d," + "\"applied_supersample\":%d,\"live_capable\":%d,\"restart_pending\":%d}\n", + id, restart, v.internal_resolution, v.supersample_milli, air, ass, cap, pend); +} + static void handle_video_info(int id, const char *json) { (void)json; @@ -16522,6 +16562,7 @@ static const CmdEntry s_commands[] = { { "dump_buffer", handle_dump_buffer }, { "wide_full", handle_wide_full }, { "video_info", handle_video_info }, + { "video_set", handle_video_set }, { "hd_textures", handle_hd_textures }, { "dynres", handle_dynres }, { "dynres_force", handle_dynres_force }, diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 8f69bc9c..15a853a2 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -397,6 +397,8 @@ static int load_modern_gl(void) { static SDL_Window *s_win = NULL; static SDL_GLContext s_ctx = NULL; static uint16_t *s_vram = NULL; /* CPU VRAM array (gpu.c's storage) */ +/* GL_VENDOR names NVIDIA (captured at context creation). */ +static int s_vendor_nvidia = 0; static int s_swap_interval = 1; /* SDL_GL swap interval (vsync mode) */ /* ---- render thread ([video] render_thread, docs/RENDER_THREAD.md) -------- @@ -6475,6 +6477,10 @@ int gl_renderer_init_context(SDL_Window *win) { const char *gpu = (const char *)glGetString(0x1F01 /* GL_RENDERER */); fprintf(stdout, "psxrecomp: OpenGL context created (%s; %s)\n", ver ? ver : "?", gpu ? gpu : "?"); + { + const char *vendor = (const char *)glGetString(0x1F00 /* GL_VENDOR */); + s_vendor_nvidia = (vendor && strstr(vendor, "NVIDIA")) ? 1 : 0; + } /* All-or-nothing: any missing entry point / failed shader / bad FBO means * the whole GL renderer is unavailable and the runtime stays on the pure @@ -6575,6 +6581,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(); @@ -9947,8 +9954,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); } } @@ -11095,6 +11104,39 @@ int gl_renderer_request_internal_scale(int scale) { return 1; } +/* Settings -> Internal resolution / Supersampling changed in game. Drain the + * render queue and apply under the owned context before acknowledging it. + * A queued dynamic-resolution step is not proof its allocation succeeded. */ +/* Whether gl_renderer_set_internal_scale_live can move the scale on this + * session at all: not in windowed high-resolution mode, not with + * CPU-authoritative dual rendering, and only with a live raster context. */ +int gl_renderer_internal_scale_live_capable(void) { + return (s_ctx && s_raster_ok && !s_hiw && !s_cpu_auth_dual) ? 1 : 0; +} + +int gl_renderer_set_internal_scale_live(int scale) { + if (s_ctx) GL_RT_SYNC("set_internal_scale_live"); + if (!s_ctx || !s_raster_ok || s_hiw || s_cpu_auth_dual || s_pass_active) return 0; + int reason = 0; + if (scale < 1) scale = 1; + scale = psx_gl_clamp_full_vram_scale(scale, GL_MAX_INTERNAL_SCALE, s_gl_max_dim, + s_vram_budget, &reason); + if (!s_alloc_scale) { + if (!dyn_resources()) return 0; + s_alloc_scale = s_hr_scale; + } + const int previous_ceiling = s_alloc_scale; + s_alloc_scale = scale; /* dyn_apply may allocate beyond the old surface */ + if (!dyn_apply(scale) || s_hr_scale != scale) { + s_alloc_scale = previous_ceiling; + return 0; + } + s_dyn_pending = 0; + s_rthm_scale = scale; + s_req_scale = scale; + return scale; +} + int gl_renderer_step_internal_scale_now(int scale) { if (!s_alloc_scale || !s_raster_ok) return 0; if (scale > s_alloc_scale) scale = s_alloc_scale; @@ -14562,6 +14604,8 @@ void gl_renderer_render_thread_stop(void) { gr_refresh_backend(); } +int gl_renderer_vendor_is_nvidia(void) { return s_vendor_nvidia; } + int gl_renderer_render_thread_active(void) { return s_rth_on; } /* Frame boundary (once per vblank, after the present): hand the context back diff --git a/runtime/src/host_overlay.c b/runtime/src/host_overlay.c index 100f7a97..9ada6fa7 100644 --- a/runtime/src/host_overlay.c +++ b/runtime/src/host_overlay.c @@ -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; @@ -131,6 +177,9 @@ unsigned psx_host_video_live_changes(const PsxHostVideoSettings *a, if (a->present_linear != b->present_linear) m |= PSX_VIDEO_LIVE_PRESENT_LINEAR; if (a->dynamic_resolution != b->dynamic_resolution) m |= PSX_VIDEO_LIVE_DYNRES; if (a->frame_generation != b->frame_generation) m |= PSX_VIDEO_LIVE_FRAME_GEN; + if (a->internal_resolution != b->internal_resolution || + a->supersample_milli != b->supersample_milli) + m |= PSX_VIDEO_LIVE_RESOLUTION; return m; } @@ -139,8 +188,9 @@ unsigned psx_host_video_restart_bits(const PsxHostVideoSettings *boot, unsigned m = 0; if (!boot || !next) return 0; if (boot->renderer != next->renderer) m |= PSX_VIDEO_RESTART_RENDERER; - if (boot->internal_resolution != next->internal_resolution || - boot->supersampling != next->supersampling) + /* Internal resolution and supersample apply live; only the legacy + * integer factor still needs a relaunch. */ + if (boot->supersampling != next->supersampling) m |= PSX_VIDEO_RESTART_RESOLUTION; if (boot->render_thread != next->render_thread || boot->present_thread != next->present_thread) @@ -148,6 +198,17 @@ unsigned psx_host_video_restart_bits(const PsxHostVideoSettings *boot, return m; } +unsigned psx_host_video_restart_bits_backend(const PsxHostVideoSettings *boot, + const PsxHostVideoSettings *next, + int res_live_ok) { + unsigned m = psx_host_video_restart_bits(boot, next); + if (boot && next && !res_live_ok && + (boot->internal_resolution != next->internal_resolution || + boot->supersample_milli != next->supersample_milli)) + m |= PSX_VIDEO_RESTART_RESOLUTION; + return m; +} + /* ---- seamless relaunch ----------------------------------------------------- */ #include #include diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 2cc54da6..fc425137 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -3980,7 +3980,25 @@ static void sync_guest_cadence_to_video_standard(void) { static int host_driver_vsync_unreliable(void) { #ifdef _WIN32 - return 0; + /* Windows OpenGL, windowed or borderless: DWM composes the window and + * owns the display's vsync, so a driver swap interval adds nothing but a + * stall. On NVIDIA (R4 0.8, RTX 4080, 610.88) a vsync'd SwapBuffers holds + * the GPU queue the render thread submits to: the render thread's frame + * then costs a whole refresh (16.7 ms "GPU" at any internal scale, + * render_busy 100 %), back-pressure slows the guest to 52-57 Hz, and + * dynamic resolution drops for nothing. Swap interval 0 under the + * wall-clock pacer: 60.0 Hz, 4.4 ms. + * + * Scope is the measured case only: GL renderer active, the render thread + * running, GL_VENDOR NVIDIA, and not exclusive fullscreen (2). Other + * driver families, the unthreaded path and exclusive fullscreen keep + * driver vsync. Re-evaluated per call, so runtime fullscreen/renderer + * changes take effect. PSX_TRUST_DRIVER_VSYNC=1 keeps driver vsync. */ + if (const char *e = std::getenv("PSX_TRUST_DRIVER_VSYNC"); e && e[0] && e[0] != '0') + return 0; + return (g_video_renderer == 1 && g_gl_active && s_window_fullscreen.mode != 2 && + gl_renderer_render_thread_active() && + gl_renderer_vendor_is_nvidia()) ? 1 : 0; #else static int cached = -1; if (cached >= 0) @@ -4091,8 +4109,14 @@ static void log_present_cadence(void) { !g_netplay_vsync_forced_off) { if (host_driver_vsync_unreliable()) { std::printf("psxrecomp: present cadence: wall-clock pacer " - "(%.4f ms/frame); driver vsync off under WSL\n", - g_frame_period_ms); + "(%.4f ms/frame); driver vsync off (%s)\n", + g_frame_period_ms, +#ifdef _WIN32 + "Windows GL, NVIDIA render thread: DWM composes" +#else + "under WSL" +#endif + ); } else if (present_effective_swap_interval() != 0) { std::printf("psxrecomp: present cadence: wall-clock pacer " "(%.4f ms/frame); synchronized swaps on %.0f Hz panel\n", @@ -9113,9 +9137,11 @@ static bool drain_host_events() { (int)mod)) { const int target = s_window_fullscreen.mode ? 0 : (g_fullscreen ? g_fullscreen : 1); - if (psx_window_fullscreen_set(sdl_window, &s_window_fullscreen, target) == 0) + if (psx_window_fullscreen_set(sdl_window, &s_window_fullscreen, target) == 0) { + apply_present_cadence(); + log_present_cadence(); host_osd_push(target ? "Fullscreen" : "Windowed", 1500); - else + } else host_osd_push("Fullscreen change failed", 1500); } } @@ -10302,6 +10328,10 @@ static void render_thread_vblank(void) { gl_renderer_set_present_thread(g_present_thread, g_present_thread_slots); if (g_gl_active && gr_backend() == GR_BACKEND_OPENGL && gl_renderer_render_thread_start(g_render_thread_frames)) { + /* The NVIDIA policy only becomes eligible once the worker owns + * GL. Update the actual driver interval at that transition. */ + apply_present_cadence(); + log_present_cadence(); std::fprintf(stdout, "psxrecomp: render thread on (OpenGL, %d frame(s) in flight)\n", g_render_thread_frames); if (g_frame_generation) { @@ -10403,6 +10433,54 @@ static void dynres_setup(void) { static std::filesystem::path g_overlay_settings_path; /* settings.toml */ static PsxHostVideoSettings g_video_boot; /* as the session booted */ static bool g_video_boot_valid = false; +/* Live Internal resolution / Supersample. g_video_internal_res and + * g_video_ss_milli are what the player requested (shown and saved); these are + * what the backend last actually applied. g_video_res_live_failed holds a + * relaunch requirement after a live change the backend refused, and + * g_video_res_live_capable mirrors gl_renderer_internal_scale_live_capable() + * for the menu thread. */ +static int g_video_res_applied_ir = PSX_IR_UNSET; +static int g_video_res_applied_ss = 1000; +static std::mutex g_video_res_applied_mutex; +static std::atomic g_video_res_live_failed{false}; +static std::atomic g_video_res_live_capable{false}; + +static void video_res_set_applied(int ir, int ss) { + std::lock_guard lock(g_video_res_applied_mutex); + g_video_res_applied_ir = ir; + g_video_res_applied_ss = ss; +} + +static void video_res_refresh_capable(void) { + g_video_res_live_capable.store(g_gl_active && g_video_scale_applies && + gl_renderer_internal_scale_live_capable() != 0); +} + +/* Other restart-only settings still compare against boot. Resolution compares + * against its last confirmed application, including after a live A -> B. */ +static unsigned video_restart_bits(const PsxHostVideoSettings *boot, + const PsxHostVideoSettings *next) { + if (!boot || !next) return 0; + PsxHostVideoSettings applied = *boot; + { + std::lock_guard lock(g_video_res_applied_mutex); + applied.internal_resolution = g_video_res_applied_ir; + applied.supersample_milli = g_video_res_applied_ss; + } + return psx_host_video_restart_bits_backend( + &applied, next, 0); +} + +extern "C" void psx_video_resolution_applied(int *ir, int *ss_milli, int *live_capable, + int *restart_pending) { + { + std::lock_guard lock(g_video_res_applied_mutex); + if (ir) *ir = g_video_res_applied_ir; + if (ss_milli) *ss_milli = g_video_res_applied_ss; + } + if (live_capable) *live_capable = g_video_res_live_capable.load() ? 1 : 0; + if (restart_pending) *restart_pending = g_video_res_live_failed.load() ? 1 : 0; +} extern "C" void psx_video_current(PsxHostVideoSettings *v) { if (!v) return; @@ -10417,10 +10495,17 @@ extern "C" void psx_video_current(PsxHostVideoSettings *v) { v->frame_generation = g_frame_generation ? 1 : 0; v->renderer = g_video_renderer; v->internal_resolution = g_video_internal_res; - v->supersampling = g_video_scale; + /* The legacy factor as booted: the live scale moves with the internal + * resolution and supersample, which apply without a relaunch. */ + v->supersampling = g_video_boot_valid ? g_video_boot.supersampling : g_video_scale; v->render_thread = g_render_thread ? 1 : 0; v->present_thread = g_present_thread ? 1 : 0; - if (!g_video_boot_valid) { g_video_boot = *v; g_video_boot_valid = true; } + v->supersample_milli = g_video_ss_milli; + video_res_refresh_capable(); + if (!g_video_boot_valid) { + g_video_boot = *v; g_video_boot_valid = true; + video_res_set_applied(v->internal_resolution, v->supersample_milli); + } } extern "C" unsigned psx_video_apply_live(const PsxHostVideoSettings *next) { @@ -10443,8 +10528,11 @@ extern "C" unsigned psx_video_apply_live(const PsxHostVideoSettings *next) { if (m & ~present_only) deferred = PSX_VIDEO_RESTART_NETPLAY; m &= present_only; } - if (m & PSX_VIDEO_LIVE_FULLSCREEN) - (void)psx_window_fullscreen_set(sdl_window, &s_window_fullscreen, next->fullscreen); + if ((m & PSX_VIDEO_LIVE_FULLSCREEN) && + psx_window_fullscreen_set(sdl_window, &s_window_fullscreen, next->fullscreen) == 0) { + apply_present_cadence(); + log_present_cadence(); + } if (m & PSX_VIDEO_LIVE_TEXTURE_FILTER) { g_video_texfilter = psx_texture_filter_from_ui(next->texture_filter); if (g_mod_texfilter < 0) gr_set_texture_filter(g_video_texfilter); @@ -10461,6 +10549,36 @@ extern "C" unsigned psx_video_apply_live(const PsxHostVideoSettings *next) { if (!g_video_dynres) g_dynres.active = false; else { dynres_setup(); if (!g_dynres.active && gl_renderer_dynamic_resolution_ceiling() >= 2) g_dynres.active = true; } } + if (m & PSX_VIDEO_LIVE_RESOLUTION) { + /* The GL owner confirms allocation/application before this returns. */ + if (next->internal_resolution != PSX_IR_UNSET) + g_video_internal_res = next->internal_resolution; + if (next->supersample_milli >= PSX_SS_MILLI_MIN && + next->supersample_milli <= PSX_SS_MILLI_MAX) + g_video_ss_milli = next->supersample_milli; + /* The request is kept (and saved) either way; the session's scale + * only moves when the backend takes it. Otherwise the change waits + * for a relaunch and the restart bits say so. */ + const int prev_scale = g_video_scale; + video_res_refresh_capable(); + int applied = 0; + if (g_video_res_live_capable.load()) { + apply_internal_resolution(match_display_px_h(sdl_window)); + applied = gl_renderer_set_internal_scale_live(g_video_scale); + } + if (applied) { + std::fprintf(stdout, "psxrecomp: internal scale %dx (live)\n", applied); + g_video_scale = applied; + video_res_set_applied(g_video_internal_res, g_video_ss_milli); + g_video_res_live_failed.store(false); + dynres_setup(); /* the new scale is the controller's ceiling */ + } else { + g_video_scale = prev_scale; + g_video_res_live_failed.store(true); + std::fprintf(stdout, "psxrecomp: internal resolution change needs a " + "relaunch (live change unavailable)\n"); + } + } if ((m & PSX_VIDEO_LIVE_FRAME_GEN) && g_gl_active) { g_frame_generation = next->frame_generation ? 1 : 0; gl_renderer_set_frame_generation(g_frame_generation); @@ -10482,11 +10600,13 @@ extern "C" unsigned psx_video_apply_live(const PsxHostVideoSettings *next) { us.render_thread = next->render_thread != 0; us.has_render_thread = true; us.present_thread = next->present_thread != 0; us.has_present_thread = true; us.internal_resolution = next->internal_resolution; us.has_internal_resolution = true; + us.supersample_milli = next->supersample_milli; + us.has_supersample = next->supersample_milli >= 1000; (void)quality_save_user_settings(g_overlay_settings_path, us); } catch (...) { } } - return psx_host_video_restart_bits(&g_video_boot, next) | deferred; + return video_restart_bits(&g_video_boot, next) | deferred; } #ifndef PSX_NO_DEBUG_TOOLS @@ -17061,8 +17181,13 @@ unsigned mh_restart_bits(const PsxHostVideoSettings& next) { * 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) & + unsigned out = video_restart_bits(&g_ovl_video_boot, &next) & psx_host_video_restart_bits(&now, &next); + /* A resolution edit the backend cannot apply live waits for a relaunch + * (the pure helper treats resolution as live). */ + if ((now.internal_resolution != next.internal_resolution || + now.supersample_milli != next.supersample_milli)) + out |= video_restart_bits(&g_ovl_video_boot, &next) & PSX_VIDEO_RESTART_RESOLUTION; /* In a netplay match only presentation applies live (psx_video_apply_live); * the rest waits for the next session. */ if (g_ovl_netplay && @@ -17148,6 +17273,10 @@ PsxHostVideoSettings ovl_video_from(const RecompLauncherCSettings& ls) { v.renderer = ls.renderer; v.render_thread = ls.render_thread ? 1 : 0; v.present_thread = ls.present_thread ? 1 : 0; + if (ls.internal_resolution != 0) v.internal_resolution = ls.internal_resolution; +#if defined(RECOMP_LAUNCHER_HAS_SUPERSAMPLE) + if (ls.supersample_milli >= 1000) v.supersample_milli = ls.supersample_milli; +#endif return v; } void mh_launcher_settings_get(RecompLauncherCSettings* out) { @@ -17166,6 +17295,10 @@ void mh_launcher_settings_get(RecompLauncherCSettings* out) { ls.renderer = v.renderer; ls.render_thread = v.render_thread; ls.present_thread = v.present_thread; + ls.internal_resolution = internal_resolution_for_launcher(); +#if defined(RECOMP_LAUNCHER_HAS_SUPERSAMPLE) + ls.supersample_milli = v.supersample_milli; +#endif #if defined(RECOMP_LAUNCHER_HAS_QUALITY_PRESETS) /* Every row a preset governs, the hidden ones included, as the player * selected them (main-thread snapshot): a pick's next-launch rows and @@ -17213,6 +17346,9 @@ void mh_launcher_game_info(RecompLauncherCGameInfo* out) { gi.num_renderers = 2; gi.has_supersampling = 0; /* restart-only; set before boot */ gi.has_dynamic_resolution = 1; +#if defined(RECOMP_LAUNCHER_HAS_SUPERSAMPLE) + gi.has_supersample = 1; /* live with Internal resolution */ +#endif gi.has_render_pipeline = 1; gi.has_vsync = 0; gi.has_rewind_depth = 0; @@ -18571,6 +18707,7 @@ int main(int argc, char** argv) { g_video_internal_res = PSX_IR_UNSET; } if (us.has_internal_resolution) g_video_internal_res = us.internal_resolution; + if (us.has_supersample) g_video_ss_milli = us.supersample_milli; if (us.has_dynamic_resolution) g_video_dynres = us.dynamic_resolution ? 1 : 0; if (us.has_dynamic_resolution_min) g_video_dynres_min = us.dynamic_resolution_min; if (us.has_window_width) g_video_win_w = us.window_width; @@ -20368,6 +20505,12 @@ int main(int argc, char** argv) { DiscMenuSession disc_menu_session{}; session_reboot: + /* A new backend/session must not inherit the prior applied settings or + * a refused live request. Capture its boot baseline again on first read. */ + g_video_boot_valid = false; + video_res_set_applied(PSX_IR_UNSET, 1000); + g_video_res_live_failed.store(false); + g_video_res_live_capable.store(false); g_openxr_ready_execution_generation++; if (g_openxr_ready_execution_generation == 0) g_openxr_ready_execution_generation = 1; diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 7fa5a8c2..f44d43ab 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -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" @@ -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; diff --git a/runtime/tests/test_gl_render_thread.c b/runtime/tests/test_gl_render_thread.c index 32d276e7..0d095002 100644 --- a/runtime/tests/test_gl_render_thread.c +++ b/runtime/tests/test_gl_render_thread.c @@ -85,6 +85,11 @@ void host_osd_present_done(void){} int psx_rewind_overlay_image(const uint32_t **p,int *w,int *h){*p=NULL;*w=*h=0;return 0;} float psx_rewind_slide(void){return 0.0f;} int psx_savestate_menu_overlay_image(const uint32_t **p,int *w,int *h){*p=NULL;*w=*h=0;return 0;} +int psx_host_menu_overlay_image(const uint32_t **p,int *w,int *h){*p=NULL;*w=*h=0;return 0;} +void psx_video_telemetry_compose(int r,int s,int sw,int sh,int dw,int dh,int vw,int vh){ + (void)r;(void)s;(void)sw;(void)sh;(void)dw;(void)dh;(void)vw;(void)vh; +} +void psx_video_telemetry_present(int result){(void)result;} int present_shot_take(char *out,int n){(void)out;(void)n;return 0;} void present_shot_done(int ok){(void)ok;} void psx_host_sleep_ms(uint32_t ms){(void)ms;} @@ -105,6 +110,28 @@ static void check(int ok, const char *label) { checks++; if (!ok) { fprintf(stderr, "FAIL %s\n", label); failures++; } } +/* Force the real allocation-failure branch without exhausting VRAM. The + * fixture includes the backend, so no production fault-injection API is needed. */ +static void (APIENTRY *saved_renderbuffer_storage)(GLenum, GLenum, GLsizei, GLsizei); +static void APIENTRY refuse_renderbuffer_storage(GLenum target, GLenum format, + GLsizei w, GLsizei h) { + (void)format; + saved_renderbuffer_storage(target, 0 /* invalid format */, w, h); +} +static void test_live_resolution(int original_scale) { + check(gl_renderer_set_internal_scale_live(2) == 2, "live scale A applied"); + check(gl_renderer_set_internal_scale_live(1) == 1, "live A to B applied"); + gl_renderer_render_thread_sync("fixture_allocation_refusal"); + saved_renderbuffer_storage = p_glRenderbufferStorage; + p_glRenderbufferStorage = refuse_renderbuffer_storage; + check(gl_renderer_set_internal_scale_live(2) == 0, "live B to A allocation refused"); + p_glRenderbufferStorage = saved_renderbuffer_storage; + check(s_hr_scale == 1 && s_req_scale == 1 && s_alloc_scale == 1, + "refusal preserves applied scale and ceiling"); + check(!s_dyn_pending, "refused live request cannot apply later"); + check(gl_renderer_set_internal_scale_live(original_scale) == original_scale, + "live resolution recovers after refusal"); +} static uint64_t fnv(const void *pp, size_t n, uint64_t h) { const uint8_t *p = (const uint8_t *)pp; for (size_t i = 0; i < n; i++) { h ^= p[i]; h *= 0x100000001b3ull; } @@ -278,6 +305,9 @@ int main(int argc, char **argv) { check(gl_renderer_render_thread_start(2) == 1, "render thread started"); check(gl_renderer_render_thread_active(), "render thread active"); } + test_live_resolution(scale); + GlDynresStats live_baseline; + gl_renderer_dynres_stats(&live_baseline); if (dynres) { check(gl_renderer_dynamic_resolution_ceiling() == scale, "dynres ceiling"); if (threaded) gl_renderer_render_thread_measure(1); @@ -304,8 +334,9 @@ int main(int argc, char **argv) { gl_renderer_render_thread_sync("test"); GlDynresStats ds; gl_renderer_dynres_stats(&ds); - printf("dyn_steps=%llu asked=%d\n", (unsigned long long)ds.steps, nsteps); - check(ds.steps == (uint64_t)nsteps && ds.level == scale, "every step applied"); + const uint64_t scripted_steps = ds.steps - live_baseline.steps; + printf("dyn_steps=%llu asked=%d\n", (unsigned long long)scripted_steps, nsteps); + check(scripted_steps == (uint64_t)nsteps && ds.level == scale, "every step applied"); if (threaded) { GlRthCosts co; gl_renderer_render_thread_costs(&co); diff --git a/runtime/tests/test_host_overlay.c b/runtime/tests/test_host_overlay.c index 1aaecd67..8e2271f6 100644 --- a/runtime/tests/test_host_overlay.c +++ b/runtime/tests/test_host_overlay.c @@ -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); @@ -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. */ @@ -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; } @@ -143,7 +179,29 @@ static void test_video(void) { CHECK(psx_host_video_live_changes(&a, &b) == (PSX_VIDEO_LIVE_SCANLINES | PSX_VIDEO_LIVE_PRESENT_LINEAR | PSX_VIDEO_LIVE_FULLSCREEN)); CHECK(psx_host_video_restart_bits(&a, &b) == 0); - b.internal_resolution = 4; b.present_thread = 1; + b.internal_resolution = 4; b.supersample_milli = 1500; b.present_thread = 1; + CHECK(psx_host_video_live_changes(&a, &b) & PSX_VIDEO_LIVE_RESOLUTION); + CHECK(psx_host_video_restart_bits(&a, &b) == PSX_VIDEO_RESTART_THREADS); + /* Live-capable GL: no relaunch for resolution. Refused / non-GL (hiw, + * CPU-authoritative dual, no context, software): the change waits. */ + CHECK(psx_host_video_restart_bits_backend(&a, &b, 1) == PSX_VIDEO_RESTART_THREADS); + CHECK(psx_host_video_restart_bits_backend(&a, &b, 0) == + (PSX_VIDEO_RESTART_RESOLUTION | PSX_VIDEO_RESTART_THREADS)); + { + PsxHostVideoSettings c2 = a; + c2.supersample_milli = 1250; + CHECK(psx_host_video_restart_bits_backend(&a, &c2, 0) == PSX_VIDEO_RESTART_RESOLUTION); + CHECK(psx_host_video_restart_bits_backend(&a, &a, 0) == 0); + /* A -> B succeeded, then B -> A was refused. Boot A cannot stand in + * for the applied baseline B when deciding whether restart is needed. */ + PsxHostVideoSettings applied = a; + applied.internal_resolution = c2.internal_resolution; + applied.supersample_milli = c2.supersample_milli; + CHECK(psx_host_video_restart_bits_backend(&applied, &a, 0) == + PSX_VIDEO_RESTART_RESOLUTION); + CHECK(psx_host_video_restart_bits_backend(&applied, &c2, 0) == 0); + } + b.supersampling = 2; CHECK(psx_host_video_restart_bits(&a, &b) == (PSX_VIDEO_RESTART_RESOLUTION | PSX_VIDEO_RESTART_THREADS)); b.renderer = 0; CHECK(psx_host_video_restart_bits(&a, &b) & PSX_VIDEO_RESTART_RENDERER); @@ -237,6 +295,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(); diff --git a/runtime/tests/test_menu_video_snapshot.py b/runtime/tests/test_menu_video_snapshot.py index fbb877fa..5fc0c20b 100644 --- a/runtime/tests/test_menu_video_snapshot.py +++ b/runtime/tests/test_menu_video_snapshot.py @@ -31,6 +31,10 @@ extern "C" void psx_video_current(PsxHostVideoSettings *out) { extern "C" unsigned psx_host_video_restart_bits(const PsxHostVideoSettings *b,const PsxHostVideoSettings *n) { return b->renderer!=n->renderer ? PSX_VIDEO_RESTART_RENDERER : 0; } +/* main.cpp's backend-aware wrapper; no live resolution in this fixture. */ +static unsigned video_restart_bits(const PsxHostVideoSettings *b,const PsxHostVideoSettings *n) { + return psx_host_video_restart_bits(b,n); +} extern "C" unsigned psx_host_video_live_changes(const PsxHostVideoSettings *b,const PsxHostVideoSettings *n) { return b->texture_filter!=n->texture_filter ? PSX_VIDEO_LIVE_TEXTURE_FILTER : 0; } diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index c478d9ba..bd095287 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -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" @@ -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(); } @@ -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++; @@ -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); @@ -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; @@ -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"); }