From 264f00cee58eb2c0280dddcc86cea8e27022c048 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 10 Jul 2026 13:45:23 -0700 Subject: [PATCH] widescreen: native-wide flat/phase backdrop gates + FMV silent-hold (Tomba 2 Beach Town) - [widescreen] nw_flat_backdrop: in the native-wide mirror only, stretch untextured flat primitives across the wide output (flat-colour sky/water backdrops). Canonical 4:3 framebuffer stays byte-for-byte untouched. - [widescreen] nw_phase_backdrop: stretch textured prims emitted before the frame first shaded 3D primitive (authored 2D backdrop phase), isolating the sky from the textured 3D foreground. Both runtime-only, off by default, title-opted (draw ordering is title-specific). - [video] fmv_skip_no_xa_hold (4..3600 vblanks): presentation-side fast-forward latch after the last silent MDEC decode, so auto-skip covers a preloaded logo authored post-decode wait (Whoopee Camp logo). Guest timeline unchanged; default stays the generic 4-vblank hysteresis. - ws_dbg_stretch correlation modes 7 (all prims) / 8 (all flat prims). - fmv_state: JSON-escape config_path + report fmv_skip_no_xa_hold. Validated on Tomba 2: Release build, unattended first-attract captures at 4:3 / 16:9 / 21:9, zero unknown dispatches. Co-Authored-By: Claude Fable 5 --- docs/internal/FAITHFUL_TIMING_PLAN.md | 11 +++++++++++ recompiler/src/config_loader.cpp | 15 +++++++++++++++ recompiler/src/config_loader.h | 18 ++++++++++++++++++ runtime/include/gpu.h | 7 +++++++ runtime/src/debug_server.c | 14 +++++++++----- runtime/src/gpu.c | 10 ++++++++++ runtime/src/gpu_gl_renderer.c | 12 +++++++++--- runtime/src/main.cpp | 15 +++++++++++++-- 8 files changed, 92 insertions(+), 10 deletions(-) diff --git a/docs/internal/FAITHFUL_TIMING_PLAN.md b/docs/internal/FAITHFUL_TIMING_PLAN.md index 24bee13a..0be26a72 100644 --- a/docs/internal/FAITHFUL_TIMING_PLAN.md +++ b/docs/internal/FAITHFUL_TIMING_PLAN.md @@ -213,6 +213,17 @@ on a fixed region -> next. ## 5. Status / Log (update every session) +- **2026-07-10 (Tomba 2 Whoopee auto-skip dwell + native-wide Beach backdrop):** + Added an opt-in silent-MDEC post-decode hold so presentation-side FMV + auto-skip remains unpaced through a preloaded logo's authored release wait; + the faithful guest timeline is unchanged and the default remains four + vblanks. Added title-opted native-wide mirror gates for flat primitives and + the textured pre-shaded backdrop phase. This keeps the canonical 4:3 buffer + untouched while filling Tomba 2 Beach Town at 16:9 and 21:9 without stretching + the later 3D foreground. Also fixed JSON escaping in the `fmv_state` path and + extended its resolved-config reporting. Validated Release build, unattended + first-attract captures at 4:3/16:9/21:9, and zero unknown dispatches. + - **2026-07-02 (HLE PIVOT implemented — HLE as a first-class swappable tier, gbarecomp model):** USER-DIRECTED pivot (supersedes "no HLE" §0; CLAUDE.md amended 2026-07-02, memory hle_tier_architecture.md). Built the full stack this session: (1) EMITTER — diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index c27a2b74..efdf1d62 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -244,6 +244,13 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& if (video.contains("fmv_skip_no_xa")) { rt.video_fmv_skip_no_xa = toml::find(video, "fmv_skip_no_xa"); } + if (video.contains("fmv_skip_no_xa_hold")) { + rt.video_fmv_skip_no_xa_hold = + (int)toml::find(video, "fmv_skip_no_xa_hold"); + if (rt.video_fmv_skip_no_xa_hold < 4 || rt.video_fmv_skip_no_xa_hold > 3600) + throw std::runtime_error( + "[video] fmv_skip_no_xa_hold must be between 4 and 3600 vblanks"); + } if (video.contains("low_latency_input")) { rt.video_low_latency_input = toml::find(video, "low_latency_input"); } @@ -698,6 +705,8 @@ GameConfig load_game_config(const fs::path& config_path_in) { bool ws_gte_game_mode = false; bool ws_nw_hud_corners = false; bool ws_nw_backdrop = false; + bool ws_nw_flat_backdrop = false; + bool ws_nw_phase_backdrop = false; bool ws_offered = true; if (cfg.contains("widescreen")) { const toml::value& ws = toml::find(cfg, "widescreen"); @@ -726,6 +735,10 @@ GameConfig load_game_config(const fs::path& config_path_in) { ws_nw_hud_corners = toml::find(ws, "nw_hud_corners"); if (ws.contains("nw_backdrop")) ws_nw_backdrop = toml::find(ws, "nw_backdrop"); + if (ws.contains("nw_flat_backdrop")) + ws_nw_flat_backdrop = toml::find(ws, "nw_flat_backdrop"); + if (ws.contains("nw_phase_backdrop")) + ws_nw_phase_backdrop = toml::find(ws, "nw_phase_backdrop"); if (ws.contains("offer")) ws_offered = toml::find(ws, "offer"); } @@ -848,6 +861,8 @@ GameConfig load_game_config(const fs::path& config_path_in) { /*ws_gte_game_mode*/ ws_gte_game_mode, /*ws_nw_hud_corners*/ ws_nw_hud_corners, /*ws_nw_backdrop*/ ws_nw_backdrop, + /*ws_nw_flat_backdrop*/ ws_nw_flat_backdrop, + /*ws_nw_phase_backdrop*/ ws_nw_phase_backdrop, /*ws_offered*/ ws_offered, /*ws_bg2d_count_site*/ ws_bg2d_count_site, /*ws_bg2d_startcol_site*/ ws_bg2d_startcol_site, diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 080bcbff..1b4607fb 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -228,6 +228,12 @@ struct RuntimeConfig { // side only, guest timeline untouched. Per-game opt-in because MDEC use // outside movies (loading-screen stills) would briefly trigger it. bool video_fmv_skip_no_xa = false; + // fmv_skip_no_xa_hold: presentation-side fast-forward latch, in guest + // vblanks, after the most recent silent MDEC decode. Silent preloaded + // logos can retain an authored post-decode wait; a title may opt into a + // longer latch so auto-skip covers that wait too. Default preserves the + // generic four-frame inter-decode hysteresis. + int video_fmv_skip_no_xa_hold = 4; // aspect_ratio: display aspect "W:H" (default "4:3" = native). A wider // aspect (e.g. "16:9") enables the widescreen hack: the GTE squashes @@ -504,6 +510,18 @@ struct GameConfig { // Runtime-only — no regen. Off by default. bool ws_nw_backdrop = false; + // [widescreen] nw_flat_backdrop — in the native-wide mirror only, stretch + // untextured flat primitives across the wider output. This is for games + // whose authored 4:3 sky/backdrop is emitted as flat polygons rather than + // a recognizable full-frame textured quad. Runtime-only; off by default. + bool ws_nw_flat_backdrop = false; + + // [widescreen] nw_phase_backdrop — stretch textured primitives emitted + // before the frame's first shaded 3D primitive. This isolates an authored + // 2D sky/backdrop phase from the later textured foreground. Runtime-only; + // off by default because draw ordering is title-specific. + bool ws_nw_phase_backdrop = false; + // [widescreen] offer — whether the launcher OFFERS its EXPERIMENTAL // Widescreen toggle for this title. Default true. Set false while a // title's widescreen is unported/unvalidated (e.g. MMX4 at bring-up): diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 3bc6ed34..2e069673 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -169,6 +169,13 @@ void gpu_ws_set_nw_hud_corners(int on); * gradient / backdrop image) to fill the wide frame, so it no longer * pillarboxes at the reveal margins. Runtime-only. Off by default. */ void gpu_ws_set_nw_backdrop(int on); +/* Native-wide flat-polygon backdrop stretch ([widescreen] nw_flat_backdrop): + * stretch untextured primitives in the wide mirror without changing the + * canonical 4:3 framebuffer. Intended for flat-colour sky/water backdrops. */ +void gpu_ws_set_nw_flat_backdrop(int on); +int gpu_ws_nw_flat_backdrop_enabled(void); +/* Stretch the title-opted textured pre-3D backdrop phase in the wide mirror. */ +void gpu_ws_set_nw_phase_backdrop(int on); /* Backdrop screen-X correction ([widescreen.backdrop] x_sites). The parallax * 2D backdrop layer computes screen-X without the GTE, so it misses the diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index dcc8312f..60139a50 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -6614,7 +6614,7 @@ static void handle_ws_backdrop_stretch(int id, const char *json) /* prim<->pixel correlation gate (ws_dbg_stretch). Forces g_ws_bd_stretch_on=1 and * sets the selectable match mode used by the GL native-wide 2D-stretch gate so a * probe can stretch an exact prim set and screenshot which one fills the void. - * Args: mode (0..6), lo, hi (OT addrs, hex via _hex fields or decimal), clut. + * Args: mode (0..8), lo, hi (OT addrs, hex via _hex fields or decimal), clut. * Reports matched/tagged counts (last frame) + applied. */ static void handle_ws_dbg_stretch(int id, const char *json) { @@ -9493,24 +9493,28 @@ static void handle_mdec_state(int id, const char *json) static void handle_fmv_state(int id, const char *json) { (void)json; - extern void debug_get_fmv_config(int *, uint32_t *, uint32_t *, const char **); + extern void debug_get_fmv_config(int *, uint32_t *, uint32_t *, int *, const char **); extern uint32_t mdec_get_decode_count(void); extern int cdrom_xa_stream_active(void); extern uint16_t sio_get_pad_buttons(void); extern uint64_t cdrom_get_dataready_fires(void); extern uint64_t g_cdrom_deliver_count; - int auto_skip = -1; uint32_t total_table = 0, movie_id = 0; const char *cfg = "(null)"; - debug_get_fmv_config(&auto_skip, &total_table, &movie_id, &cfg); + int auto_skip = -1, no_xa_hold = 0; + uint32_t total_table = 0, movie_id = 0; const char *cfg = "(null)"; + char cfg_json[1024]; + debug_get_fmv_config(&auto_skip, &total_table, &movie_id, &no_xa_hold, &cfg); + json_escape_string(cfg_json, sizeof(cfg_json), cfg ? cfg : "(null)"); MDECDebugState s; mdec_debug_get_state(&s); send_fmt("{\"id\":%d,\"ok\":true," "\"config_path\":\"%s\"," "\"auto_skip_fmv\":%d," + "\"fmv_skip_no_xa_hold\":%d," "\"fmv_skip_total_table\":\"0x%08X\",\"fmv_skip_movie_id\":\"0x%08X\"," "\"mdec_decode_count\":%u,\"mdec_decode_macroblocks\":%u,\"mdec_dma_out_words\":%u," "\"xa_stream_active\":%d," "\"cd_dataready_fires\":%llu,\"cd_irq_delivered\":%llu," "\"pad1\":\"0x%04X\"}", - id, cfg ? cfg : "(null)", auto_skip, + id, cfg_json, auto_skip, no_xa_hold, total_table, movie_id, mdec_get_decode_count(), s.decode_macroblocks, s.dma_out_words, cdrom_xa_stream_active(), diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index d5d96f71..5a165866 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -945,6 +945,8 @@ static int ws_tagged_anchor(int32_t *out_ax) { * flower-field tiles — the 3D rock/foreground is untagged AND has narrow prims, * so tag/narrow heuristics alone mis-stretch and tear it. */ uint32_t g_ws_backdrop_lo = 0, g_ws_backdrop_hi = 0; +static int ws_nw_phase_backdrop = 0; +void gpu_ws_set_nw_phase_backdrop(int on) { ws_nw_phase_backdrop = on ? 1 : 0; } /* diag: per-frame min/max of the prim source addrs the GL gate sees */ uint32_t g_bdg_src_lo = 0xFFFFFFFFu, g_bdg_src_hi = 0; static uint32_t bdg_src_frame = 0xFFFFFFFFu; @@ -960,6 +962,8 @@ static uint32_t bdg_src_frame = 0xFFFFFFFFu; * 4 tagged : prim is sprite-tagged (psx_ws_prim_is_tagged) * 5 untag-tex : textured AND not sprite-tagged * 6 all-tex : every textured prim (sanity: should fill void + distort 3D) + * 7 all : every primitive (coarse correlation sanity check) + * 8 all-flat : every untextured primitive * Counters (per-frame, snapshotted by the present hook) report how many prims * matched and how many of those were sprite-tagged. */ int g_dbg_mode = 0; @@ -983,6 +987,8 @@ static int dbg_gate_match(void) { case 4: m = tagged; break; case 5: m = (textured && !tagged); break; case 6: m = textured; break; + case 7: m = 1; break; + case 8: m = !textured; break; default: m = 0; break; } if (m) { s_dbg_match_n++; if (tagged) s_dbg_match_tagged++; } @@ -1022,6 +1028,7 @@ int psx_ws_prim_in_backdrop(void) { if (gp0_cmd_source_addr > g_bdg_src_hi) g_bdg_src_hi = gp0_cmd_source_addr; } if (g_dbg_mode != 0) return dbg_gate_match(); /* correlation override */ + if (ws_nw_phase_backdrop && !ws_bg_phase_over()) return 1; /* Real gate: stretch the 2D backdrop = sprite-tagged prims drawn in the * background phase (before the 3D world). Fills the native-wide void for both * the flower grid and the sky band; HUD/characters (tagged but post-3D) and the @@ -1135,6 +1142,9 @@ static int32_t ws_nw_hud_shift(int32_t x, int32_t w) { * Transforms vx[0..3] IN PLACE (pre-draw_offset); returns 1 if it applied. */ static int ws_nw_backdrop = 0; void gpu_ws_set_nw_backdrop(int on) { ws_nw_backdrop = on ? 1 : 0; } +static int ws_nw_flat_backdrop = 0; +void gpu_ws_set_nw_flat_backdrop(int on) { ws_nw_flat_backdrop = on ? 1 : 0; } +int gpu_ws_nw_flat_backdrop_enabled(void) { return ws_nw_flat_backdrop; } static int ws_nw_backdrop_stretch_quad(int32_t *vx, const int32_t *vy) { if (!ws_nw_backdrop || !ws_native_wide_active()) return 0; int32_t extra = ws_nw_extra(); diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index d5a48fcc..5195e336 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -1118,14 +1118,20 @@ static void wide_target_end(GLint uXoff, GLint uXhalf) { extern int psx_ws_prim_is_tagged(void); /* gpu.c: is the current GP0 prim sprite-tagged? */ extern int psx_ws_prim_in_backdrop(void); /* gpu.c: is its source addr in the flower-field struct? */ +extern int gpu_ws_nw_flat_backdrop_enabled(void); /* gpu.c: per-title flat backdrop opt-in */ /* Per-prim gate: stretch this prim iff native-wide + feature on AND the prim's * source address is inside the flower-field backdrop data structure (precise — * excludes the 3D rock/foreground, which is untagged AND has narrow prims so the * earlier tag/narrow heuristic tore it). mode!=0 falls back to the old * tag+narrow heuristic for A/B. */ -static int bd_prim_gate(const int *xs, int n) { +static int bd_prim_gate(const int *xs, int n, int textured) { if (g_wide_w <= 0 || !g_ws_bd_stretch_on) return 0; + /* Some games draw their authored 4:3 sky/water as flat-colour polygons. + * Stretch those only in the native-wide mirror: the canonical framebuffer + * remains byte-for-byte 4:3, while the flat backdrop reaches the reveal + * margins. Opt-in because flat foreground geometry is title-dependent. */ + if (!textured && gpu_ws_nw_flat_backdrop_enabled()) return 1; if (g_ws_bd_phase_mode != 0) return psx_ws_prim_in_backdrop(); /* default: precise address gate */ /* mode 0: legacy tag+narrow heuristic (kept for comparison) */ int native_w = g_wide_w - 2 * g_wide_off; @@ -1355,7 +1361,7 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys, if (g_wide_cur && !s_wide_suppress && s_ws_ablate != 1 && !(!g_ws_bd_stretch_on && mirror_geo_center_only(xs, n))) { int dx = wide_dx(); - s_bd_gate = bd_prim_gate(xs, n); /* flat prims are immediate -> gate per prim */ + s_bd_gate = bd_prim_gate(xs, n, 0); /* flat prims are immediate -> gate per prim */ gl_perf_mirror_begin(); wide_target_begin(dx, s_geo_uXoff, s_geo_uXhalf); wide_set_bd_scale(s_geo_uXscale, s_geo_uXcenter); @@ -1420,7 +1426,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys, * state goes in the vertex; only these keys force a new draw. */ { int twx = s_tw_mask_x, twy = s_tw_mask_y, tox = s_tw_off_x, toy = s_tw_off_y; - int gate = bd_prim_gate(xs, 3); /* backdrop-stretch gate is also a batch key */ + int gate = bd_prim_gate(xs, 3, 1); /* backdrop-stretch gate is also a batch key */ /* STP draw-ORDER correctness. flush_tex_batch draws a batch in two passes * over the WHOLE batch (pass 1 = every prim's STP=0/opaque texels, pass 2 * = every prim's STP=1/semi texels with the PSX blend). For overlapping diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index cb26adb0..f4d84ddb 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -168,6 +168,7 @@ static int g_fmv_skip_end_total = 3; * movies never stream XA, so the default MDEC+XA detector misses them — * Tomba2's Whoopee Camp logo). Presentation-side fast-forward only. */ static int g_fmv_skip_no_xa = 0; +static int g_fmv_skip_no_xa_hold = 4; /* Low-latency present options (see [video] in config_loader). Measured on a * 60Hz box the dominant input->photon cost is NOT vsync at the swap (that * blocks ~tens of us) but that input is sampled ~one pacer-wait (~13.6ms) @@ -190,10 +191,12 @@ extern "C" uint32_t mdec_get_decode_count(void); * resolved. */ const char *g_active_config_path = nullptr; extern "C" void debug_get_fmv_config(int *auto_skip, uint32_t *total_table, - uint32_t *movie_id, const char **cfg_path) { + uint32_t *movie_id, int *no_xa_hold, + const char **cfg_path) { if (auto_skip) *auto_skip = g_auto_skip_fmv; if (total_table) *total_table = g_fmv_skip_total_table; if (movie_id) *movie_id = g_fmv_skip_movie_id; + if (no_xa_hold) *no_xa_hold = g_fmv_skip_no_xa_hold; if (cfg_path) *cfg_path = g_active_config_path ? g_active_config_path : "(null)"; } @@ -1644,7 +1647,8 @@ static void sdl_vblank_present(void) { * satisfy the MDEC+XA detector; with the per-game opt-in, MDEC * activity alone qualifies. */ int detected = mdec_decoding && (xa || g_fmv_skip_no_xa); - if (detected) s_fmv_hold = 4; + if (detected) s_fmv_hold = (!xa && g_fmv_skip_no_xa) + ? g_fmv_skip_no_xa_hold : 4; else if (s_fmv_hold > 0) s_fmv_hold--; fmv_skip_active = (s_fmv_hold > 0) && (xa || g_fmv_skip_no_xa); if (fmv_skip_active) { @@ -2150,6 +2154,7 @@ int main(int argc, char** argv) { if (gc.runtime.video_fmv_skip_end_total) g_fmv_skip_end_total = gc.runtime.video_fmv_skip_end_total; g_fmv_skip_no_xa = gc.runtime.video_fmv_skip_no_xa ? 1 : 0; + g_fmv_skip_no_xa_hold = gc.runtime.video_fmv_skip_no_xa_hold; g_ws_anchor_addr = gc.ws_sprite_anchor_addr; g_ws_hud_sprt = gc.ws_hud_sprt_squash; /* [widescreen] full_2d — opt a pure-2D sprite game (MMX6) into the @@ -2162,6 +2167,12 @@ int main(int argc, char** argv) { gpu_ws_set_nw_hud_corners(gc.ws_nw_hud_corners ? 1 : 0); /* [widescreen] nw_backdrop — stretch full-frame 2D sky backdrop. */ gpu_ws_set_nw_backdrop(gc.ws_nw_backdrop ? 1 : 0); + /* [widescreen] nw_flat_backdrop — stretch flat sky/backdrop prims + * in the native-wide mirror, preserving the canonical 4:3 image. */ + gpu_ws_set_nw_flat_backdrop(gc.ws_nw_flat_backdrop ? 1 : 0); + /* [widescreen] nw_phase_backdrop — stretch only the textured + * backdrop phase emitted before shaded 3D foreground geometry. */ + gpu_ws_set_nw_phase_backdrop(gc.ws_nw_phase_backdrop ? 1 : 0); /* [widescreen.cull] per-game gates + signature immediates for the * pattern-scanned interp/sljit widen hooks. A title that never * opted in must never have its live code scanned and rewritten. */