mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
Merge branch 'master' of https://github.com/RetroPortingToolKit/psxrecomp into fix/held-746-20261022
# Conflicts: # runtime/src/main.cpp
This commit is contained in:
@@ -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 @@
|
||||
61bf4f6d29680623d215fbe407958408cab0a1e08dbda3c62baa25e607d63b7b
|
||||
86f7329c2692e76133c40fa11cc8cfac214302d8238e05369dc8b263f4dc76fd
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user