diff --git a/docs/MOD_PACKAGES.md b/docs/MOD_PACKAGES.md index de06eb4b..75d0a607 100644 --- a/docs/MOD_PACKAGES.md +++ b/docs/MOD_PACKAGES.md @@ -602,3 +602,25 @@ off, so the default presentation is unchanged: letterbox and pillarbox margins remain black. Enabling the feature without choosing artwork is also a no-op. The package supplies only the declaration and trusted plugin selection; archives still cannot load native code. + +### Retained-scene loading presentation (native-wide opt-in) + +A trusted game plugin can register +`psx_mod_set_retained_scene_predicate(predicate)` from `mod_plugins.h` when the +game keeps displaying its previous framebuffer while loading. The cheap, pure +emulation-thread callback must return nonzero only while that same scene is +retained. It must not call presentation APIs recursively. Passing NULL removes +the opt-in. Without registration, existing presentation behavior is unchanged. + +In native-wide mode this holds the previous wide/4:3 classification even if a +game-state flag or absent GTE activity would normally classify loading as 2D. +It does not force menus wide, stretch artwork, change guest rendering or memory, +or override the FMV veto. Release the predicate when a new scene replaces the +retained image, so ordinary classification resumes. Crash's experimental +adaptive feature uses its pending level transition and draw-skip globals for +this; these game-specific addresses do not belong in the framework. + +GPU reset and savestate restore discard this host-only history. A save loaded +directly into a frozen loading frame cannot recreate wide reveal strips absent +from the canonical saved framebuffer. `ws_scene_hold_test` covers long holds, +menu release, retained 4:3 scenes, FMV priority and timeline reset. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 0c41349c..1e033f30 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -23,6 +23,9 @@ psxrecomp_add_runtime_target(psx-runtime # (docs/ENHANCEMENTS.md G1.10). Titles get their _pgxp the same way. if(BUILD_TESTING) + add_executable(ws_scene_hold_test tests/test_ws_scene_hold.c) + target_include_directories(ws_scene_hold_test PRIVATE include) + add_test(NAME ws_scene_hold_test COMMAND ws_scene_hold_test) add_executable(mod_texture_banks_test tests/test_mod_texture_banks.c src/mod_texture_banks.c) target_include_directories(mod_texture_banks_test PRIVATE include) add_test(NAME mod_texture_banks_test COMMAND mod_texture_banks_test) diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index bacae0c2..d22c2edc 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -104,6 +104,16 @@ uint32_t psx_mod_display_width(void); /* Height companion to psx_mod_display_width(); same conventions. */ uint32_t psx_mod_display_height(void); +/* Opt-in presentation hold for a game that retains its previous framebuffer + * while loading. The pure, cheap emulation-thread predicate returns nonzero + * only while that SAME scene remains displayed; no GPU/API recursion allowed. + * Native-wide retains its prior wide/4:3 classification, never stretches art + * and never overrides FMV. NULL removes the opt-in. Host history is discarded + * on GPU reset/savestate restore, so loading a frozen scene cannot recreate + * missing widescreen strips. This does not change guest rendering or memory. */ +typedef int (*PSXModRetainedScenePredicate)(void); +void psx_mod_set_retained_scene_predicate(PSXModRetainedScenePredicate predicate); + /* * Read the committed value of one of this package's declared options, as the * player left it in the launcher (or the manifest default when untouched). diff --git a/runtime/include/ws_scene_hold.h b/runtime/include/ws_scene_hold.h new file mode 100644 index 00000000..a3307917 --- /dev/null +++ b/runtime/include/ws_scene_hold.h @@ -0,0 +1,19 @@ +#pragma once + +/* Host presentation history, not guest hardware state. Never serialize it: + * a restored canonical framebuffer cannot reconstruct discarded wide strips. */ +typedef struct WsSceneHold { int valid, native_43; } WsSceneHold; +static inline void ws_scene_hold_reset(WsSceneHold* state) { + state->valid = 0; state->native_43 = 1; +} +static inline int ws_scene_hold_classify(WsSceneHold* state, int hold, + int native_43, int fmv) { + /* FMV always wins, and cannot seed a later wide retained scene. */ + if (fmv) { + state->valid = 1; state->native_43 = 1; + return 1; + } + if (hold && state->valid) return state->native_43; + state->valid = 1; state->native_43 = native_43 != 0; + return state->native_43; +} diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 1d671cd0..719d33f0 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -27,6 +27,8 @@ #include "event_ring.h" #include "color_lut.h" #include "mod_runtime.h" +#include "mod_plugins.h" +#include "ws_scene_hold.h" #include "sio.h" #include "ws_cull_detect.h" #include "ws_aspect_cone_math.h" @@ -374,17 +376,28 @@ static int ws_2d_only_scene(void) { static uint32_t s_ws_fmv_frame_cache = 0xFFFFFFFFu; static int s_ws_fmv_cached = 0; +static PSXModRetainedScenePredicate s_ws_retained_scene_predicate; +static WsSceneHold s_ws_scene_hold; + +void psx_mod_set_retained_scene_predicate(PSXModRetainedScenePredicate predicate) { + s_ws_retained_scene_predicate = predicate; + ws_scene_hold_reset(&s_ws_scene_hold); +} int gpu_ws_present_native_43(void) { if (!ws_engaged()) return 0; - if (!ws_game_mode()) return 1; /* full-2D screen */ - if (ws_2d_only_scene()) return 1; /* 2D-only gameplay scene */ + int native_43 = !ws_game_mode() || ws_2d_only_scene(); + int hold_enabled = ws_mode == 2 && s_ws_retained_scene_predicate != NULL; + if (!hold_enabled && native_43) return 1; /* unchanged default */ uint32_t f = (uint32_t)s_frame_count; if (f != s_ws_fmv_frame_cache) { s_ws_fmv_frame_cache = f; GpuDisplayInfo di; gpu_get_display_info(&di); s_ws_fmv_cached = di.depth24 || mdec_recently_active(WS_FMV_HYSTERESIS); } + if (hold_enabled) + return ws_scene_hold_classify(&s_ws_scene_hold, + s_ws_retained_scene_predicate(), native_43, s_ws_fmv_cached); return s_ws_fmv_cached; } @@ -2853,6 +2866,7 @@ static void gpu_reset_state(int clear_vram) { gpustat_poll_count = 0; s_ws_fmv_frame_cache = 0xFFFFFFFFu; s_ws_fmv_cached = 0; + ws_scene_hold_reset(&s_ws_scene_hold); s_d24_upload_x1 = 0; s_d24_present_hold = 0; s_d24_prev_disp_h = 0; @@ -6123,6 +6137,7 @@ int gpu_snapshot_read(const uint8_t *p, uint32_t len) { if (len != gpu_snapshot_bytes()) return 0; pst_r_init(&r, p, len); if (!gpu_snap_parse(&r)) return 0; + ws_scene_hold_reset(&s_ws_scene_hold); ws_hud_anchor_clear(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE); ws_hud_anchor_clear(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE); ws_repeat_rect_tag_clear(ws_repeat_rect_tags); diff --git a/runtime/tests/test_ws_scene_hold.c b/runtime/tests/test_ws_scene_hold.c new file mode 100644 index 00000000..dd228c62 --- /dev/null +++ b/runtime/tests/test_ws_scene_hold.c @@ -0,0 +1,23 @@ +#include "ws_scene_hold.h" +#undef NDEBUG /* the Release test target must execute its assertions */ +#include +#include + +int main(void) { + WsSceneHold s; + ws_scene_hold_reset(&s); + assert(ws_scene_hold_classify(&s,0,0,0)==0); /* world */ + for (int i=0;i<300;++i) + assert(ws_scene_hold_classify(&s,1,1,0)==0); /* no GTE during load */ + assert(ws_scene_hold_classify(&s,0,1,0)==1); /* actual menu replaces it */ + assert(ws_scene_hold_classify(&s,1,0,0)==1); /* never force a 2D scene wide */ + assert(ws_scene_hold_classify(&s,0,0,0)==0); + assert(ws_scene_hold_classify(&s,1,0,1)==1); /* FMV veto */ + assert(ws_scene_hold_classify(&s,1,0,0)==1); + assert(ws_scene_hold_classify(&s,0,0,0)==0); + ws_scene_hold_reset(&s); /* cannot reuse unrelated pre-restore margins */ + assert(ws_scene_hold_classify(&s,1,1,0)==1); + assert(ws_scene_hold_classify(&s,0,0,0)==0); + puts("PASS retained scene, menu release, FMV veto and timeline reset"); + return 0; +}