Merge branch 'master' of https://github.com/RetroPortingToolKit/psxrecomp into fix/held-746-20261022

# Conflicts:
#	runtime/src/main.cpp
This commit is contained in:
Matthew Stanley
2026-10-22 18:01:49 -07:00
16 changed files with 475 additions and 26 deletions
+3 -2
View File
@@ -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":"<name>"}` 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":"<name>"}` 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` | ✓ | | |
+1 -1
View File
@@ -1 +1 @@
61bf4f6d29680623d215fbe407958408cab0a1e08dbda3c62baa25e607d63b7b
86f7329c2692e76133c40fa11cc8cfac214302d8238e05369dc8b263f4dc76fd
+11
View File
@@ -3040,6 +3040,15 @@ UserSettings load_user_settings(const fs::path& path) {
const auto n = toml::find<int64_t>(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<double>(ss.as_integer())
: toml::get<double>(ss);
if (f >= 1.0 && f <= 4.0) {
s.supersample_milli = static_cast<int>(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 &&
+3
View File
@@ -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.
+1
View File
@@ -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
+9
View File
@@ -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);
+22 -2
View File
@@ -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);
+41
View File
@@ -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 },
+46 -2
View File
@@ -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
+63 -2
View File
@@ -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 <stdlib.h>
#include <string.h>
+154 -11
View File
@@ -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<bool> g_video_res_live_failed{false};
static std::atomic<bool> g_video_res_live_capable{false};
static void video_res_set_applied(int ir, int ss) {
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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;
+3
View File
@@ -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;
+33 -2
View File
@@ -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);
+61 -1
View File
@@ -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();
@@ -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;
}
+20 -3
View File
@@ -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");
}