From f2cc2ea2c67cf657061ec17364d3c69e4b2f869c Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:47:25 -0700 Subject: [PATCH] widescreen: opt-in per-frame world-scene predicate [widescreen] scene_predicate_per_frame (PSX_WS_SCENE_CACHE=0/1) evaluates a mod's world-scene predicate once per VBlank instead of on every primitive and GTE call; reset, savestate load and a new predicate invalidate it. Default off (the answer may trail a scene change by one frame). R4 (release, M4, race, macOS sample): gpu_ws_present_native_43 2.4 % -> 0.3 % of the emulation thread (r4_in_race 2.2 % -> 0). frame_fingerprint A/B, off vs on: 9000 frames from boot (intro, title, attract) and 3600 race frames from a savestate, every column identical, 0 dispatch misses. Co-Authored-By: Claude Opus 5.5 --- docs/WIDESCREEN.md | 10 ++++++++++ recompiler/src/config_loader.cpp | 6 ++++++ recompiler/src/config_loader.h | 3 +++ runtime/include/gpu.h | 2 ++ runtime/src/gpu.c | 29 ++++++++++++++++++++++++++++- runtime/src/main.cpp | 1 + 6 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/WIDESCREEN.md b/docs/WIDESCREEN.md index d7884a1e..3bd53bd2 100644 --- a/docs/WIDESCREEN.md +++ b/docs/WIDESCREEN.md @@ -517,3 +517,13 @@ To enable 21:9 later: widen the backdrop generation, then promote the launcher's 2-state toggle to a 3-way Off / 16:9 / 21:9 (the `cycle_aspect` callback already cycles `aspect_index` 0/1/2 and is the scaffold). Full feasibility analysis in the session memory `native_wide_fov_autocull.md`. + +## `scene_predicate_per_frame` (opt-in) + +`[widescreen] scene_predicate_per_frame = true` evaluates a mod's world-scene +predicate (`psx_mod_set_world_scene_predicate`) once per VBlank instead of on +every primitive and GTE call. `PSX_WS_SCENE_CACHE=0/1` overrides it for one +run. The answer can trail a scene change by up to one frame, so turn it on per +title only after a `frame_fingerprint` A/B across scene transitions. In R4 the +predicate was 2.4 % of the emulation thread in races and drops to 0.3 %. Reset +and savestate load invalidate the cached value. diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 3024b8ea..166a6a32 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -2752,6 +2752,12 @@ GameConfig load_game_config(const fs::path& config_path_in) { loaded.ws_cull_masked_reject_sites = std::move(ws_cull_masked_reject_sites); loaded.draw_distance_clamp_sites = parse_draw_distance_clamps(cfg, config_path); + if (cfg.contains("widescreen")) { + const toml::value& wsv = toml::find(cfg, "widescreen"); + if (wsv.contains("scene_predicate_per_frame")) + loaded.ws_scene_predicate_per_frame = + toml::find(wsv, "scene_predicate_per_frame"); + } return loaded; } diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 1cefbd03..74e636f9 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -1443,6 +1443,9 @@ struct GameConfig { // [[draw_distance.clamp]] -- opt-in "keep far geometry" clamps. Empty by // default; inert until a mod switches them on; regen required. std::vector draw_distance_clamp_sites; + // [widescreen] scene_predicate_per_frame -- evaluate a mod's world-scene + // predicate once per VBlank (gpu.c). Opt-in; check with a fingerprint A/B. + bool ws_scene_predicate_per_frame = false; }; // Effective clip_edge_width: explicit value, else screen_w_imms[0], else 320. diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index b2821fcd..3987657e 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -184,6 +184,8 @@ void gpu_ws_configure(int aspect_num, int aspect_den, /* [widescreen] full_2d: opt a pure-2D sprite game into the widescreen present * path (treat every in-game frame as gameplay, since it never tags 3D prims). */ void gpu_ws_set_full_2d(int on); +/* Opt-in: a mod world-scene predicate is evaluated once per VBlank (gpu.c). */ +void gpu_ws_set_scene_predicate_per_frame(int on); /* Signed 16-bit camera/min/max; active is a nonzero byte. Requires bg2d hooks. * Zero addresses disable the feature. No guest memory is written. */ void gpu_ws_set_view_anchor(uint32_t camera, uint32_t min, uint32_t max, uint32_t active); diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 8101a9e8..eece807e 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -421,11 +421,37 @@ static int ws_full_2d_mode(void) { return ws_full_2d || env; } static PSXModWorldScenePredicate s_ws_world_scene_predicate; +static int s_ws_scene_cache_valid; void psx_mod_set_world_scene_predicate(PSXModWorldScenePredicate predicate) { s_ws_world_scene_predicate = predicate; + s_ws_scene_cache_valid = 0; +} +/* Opt-in ([widescreen] scene_predicate_per_frame, PSX_WS_SCENE_CACHE=0/1): + * evaluate a mod's world-scene predicate once per VBlank instead of on every + * primitive and GTE call. R4's predicate reads several guest words through the + * full memory bus, 2-4 % of the emulation thread in races. The answer can then + * lag a scene change by up to one frame, which is why it is opt-in and must be + * checked per title with a fingerprint A/B across scene transitions. Reset and + * savestate load invalidate it (ws_reset_scene_history). */ +static int s_ws_scene_cache_cfg = 0; +static uint32_t s_ws_scene_cache_frame = 0; +static int s_ws_scene_cache_val = 0; +void gpu_ws_set_scene_predicate_per_frame(int on) { s_ws_scene_cache_cfg = on ? 1 : 0; s_ws_scene_cache_valid = 0; } +static int ws_scene_cache_on(void) { + static int env = -2; + if (env == -2) { const char *e = getenv("PSX_WS_SCENE_CACHE"); env = e ? (e[0] == '1') : -1; } + return env >= 0 ? env : s_ws_scene_cache_cfg; } static int ws_mod_world_scene(void) { - return ws_mode == 2 && s_ws_world_scene_predicate && s_ws_world_scene_predicate(); + if (!(ws_mode == 2 && s_ws_world_scene_predicate)) return 0; + if (!ws_scene_cache_on()) return s_ws_world_scene_predicate(); + const uint32_t f = (uint32_t)s_frame_count; + if (!s_ws_scene_cache_valid || s_ws_scene_cache_frame != f) { + s_ws_scene_cache_val = s_ws_world_scene_predicate() ? 1 : 0; + s_ws_scene_cache_frame = f; + s_ws_scene_cache_valid = 1; + } + return s_ws_scene_cache_val; } static int ws_game_mode(void) { if (ws_mod_world_scene()) return 1; @@ -488,6 +514,7 @@ static int ws_2d_only_scene(void) { /* Host-derived presentation evidence is not serialized guest state. Relearn it * after reset/load instead of carrying the old scene's classification over. */ static void ws_reset_scene_history(void) { + s_ws_scene_cache_valid = 0; uint32_t expired = (uint32_t)s_frame_count - 1000u; memset(&ws_scene_latch, 0, sizeof ws_scene_latch); ws_bdx_reset(&ws_bdx); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index cf33df28..a8cd2217 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -16410,6 +16410,7 @@ int main(int argc, char** argv) { * widescreen present path. Applied to the GPU layer up front so the * ws engage at game entry classifies every frame as gameplay. */ gpu_ws_set_full_2d(gc.ws_full_2d ? 1 : 0); + gpu_ws_set_scene_predicate_per_frame(gc.ws_scene_predicate_per_frame ? 1 : 0); /* [widescreen.bg2d] engine tile-ring layout for the freshness * refill shared by the MMX5/MMX6 2D background hook. */ gpu_ws_bg2d_configure(gc.ws_bg2d_layer_base,