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 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-08 17:47:25 -07:00
parent 2a4824fcb8
commit f2cc2ea2c6
6 changed files with 50 additions and 1 deletions
+10
View File
@@ -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.
+6
View File
@@ -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<bool>(wsv, "scene_predicate_per_frame");
}
return loaded;
}
+3
View File
@@ -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<DrawDistanceClampSite> 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.
+2
View File
@@ -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);
+28 -1
View File
@@ -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);
+1
View File
@@ -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,