Merge codex/tomba2-whoopee-beach-fix: native-wide flat/phase backdrop gates + FMV silent-hold

Tomba 2 widescreen enablers: [widescreen] nw_flat_backdrop /
nw_phase_backdrop native-wide mirror gates (canonical 4:3 untouched),
[video] fmv_skip_no_xa_hold for silent preloaded-logo auto-skip,
ws_dbg_stretch modes 7/8, fmv_state JSON escaping.

Conflicts (config_loader.h/.cpp, main.cpp) were both-sides-adds vs the
MMX4/5 clear_reveal work - resolved keeping both feature sets.
Compile + boot sanity: Ape build-release rebuilt and boots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-07-10 13:48:56 -07:00
8 changed files with 92 additions and 10 deletions
+11
View File
@@ -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 —
+15
View File
@@ -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<bool>(video, "fmv_skip_no_xa");
}
if (video.contains("fmv_skip_no_xa_hold")) {
rt.video_fmv_skip_no_xa_hold =
(int)toml::find<int64_t>(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<bool>(video, "low_latency_input");
}
@@ -701,6 +708,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
uint32_t ws_nw_left_hud_packet_hi = 0;
bool ws_nw_backdrop = false;
bool ws_clear_reveal = false;
bool ws_nw_flat_backdrop = false;
bool ws_nw_phase_backdrop = false;
bool ws_offered = true;
bool ws_ultrawide_offered = false;
if (cfg.contains("widescreen")) {
@@ -751,6 +760,10 @@ GameConfig load_game_config(const fs::path& config_path_in) {
ws_nw_backdrop = toml::find<bool>(ws, "nw_backdrop");
if (ws.contains("clear_reveal"))
ws_clear_reveal = toml::find<bool>(ws, "clear_reveal");
if (ws.contains("nw_flat_backdrop"))
ws_nw_flat_backdrop = toml::find<bool>(ws, "nw_flat_backdrop");
if (ws.contains("nw_phase_backdrop"))
ws_nw_phase_backdrop = toml::find<bool>(ws, "nw_phase_backdrop");
if (ws.contains("offer"))
ws_offered = toml::find<bool>(ws, "offer");
if (ws.contains("offer_ultrawide"))
@@ -921,6 +934,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
/*ws_nw_left_hud_packet_hi*/ ws_nw_left_hud_packet_hi,
/*ws_nw_backdrop*/ ws_nw_backdrop,
/*ws_clear_reveal*/ ws_clear_reveal,
/*ws_nw_flat_backdrop*/ ws_nw_flat_backdrop,
/*ws_nw_phase_backdrop*/ ws_nw_phase_backdrop,
/*ws_offered*/ ws_offered,
/*ws_ultrawide_offered*/ ws_ultrawide_offered,
/*ws_bg2d_count_site*/ ws_bg2d_count_site,
+18
View File
@@ -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
@@ -524,6 +530,18 @@ struct GameConfig {
// only gate; any game-specific init hook remains regen-class. Off by default.
bool ws_clear_reveal = 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):
+7
View File
@@ -189,6 +189,13 @@ void gpu_ws_begin_linked_list(void);
* 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
+9 -5
View File
@@ -6867,7 +6867,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)
{
@@ -9785,24 +9785,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(),
+10
View File
@@ -1105,6 +1105,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;
@@ -1120,6 +1122,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;
@@ -1143,6 +1147,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++; }
@@ -1182,6 +1188,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
@@ -1345,6 +1352,9 @@ static void ws_nw_hud_shift_vertices(int32_t *vx, int count) {
* 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();
+9 -3
View File
@@ -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
+13 -2
View File
@@ -283,6 +283,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)
@@ -305,10 +306,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)";
}
@@ -1934,7 +1937,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) {
@@ -2486,6 +2490,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
@@ -2511,6 +2516,12 @@ int main(int argc, char** argv) {
gc.ws_nw_left_hud_packet_hi);
/* [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] clear_reveal — enable opted-in scene/map-boundary
* cleanup of synthetic native-wide margins. */
gpu_ws_set_clear_reveal(gc.ws_clear_reveal ? 1 : 0);