From e9ca97ad114c00dd0301540cf8378eea868d032e Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 10 Jul 2026 08:28:43 -0700 Subject: [PATCH] feat: finish MMX6 widescreen edge handling --- recompiler/src/config_loader.cpp | 9 ++++ recompiler/src/config_loader.h | 9 ++-- runtime/include/gpu.h | 9 +++- runtime/src/dirty_ram_interp.c | 9 ++-- runtime/src/gpu.c | 82 +++++++++++++++++++------------- runtime/src/main.cpp | 4 ++ runtime/src/overlay_sljit.c | 12 +++++ 7 files changed, 93 insertions(+), 41 deletions(-) diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 489b0912..d3ef4ec8 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -760,6 +760,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { // Optional [widescreen.cull] block — world-space draw-cull widening. std::vector ws_cull_bias_sites, ws_cull_range_sites, ws_cull_a1_sites; std::vector ws_cull_slti_sites; + int ws_cull_guard_pixels = 0; // Cull-signature immediates (screen_w_imms / screen_h_imms). Defaults are // the original Tomba signature (320-display: 0x140/0x141 + 0xE0/0xF1); a // game with a different display width overrides them (Ape Escape: 0x181). @@ -780,6 +781,13 @@ GameConfig load_game_config(const fs::path& config_path_in) { load_sites("range_sites", ws_cull_range_sites); load_sites("a1_sites", ws_cull_a1_sites); load_sites("slti_sites", ws_cull_slti_sites); + if (cull.contains("guard_pixels")) { + ws_cull_guard_pixels = toml::find(cull, "guard_pixels"); + if (ws_cull_guard_pixels < 0 || ws_cull_guard_pixels > 256) + throw std::runtime_error(fmt::format( + "{}: [widescreen.cull] guard_pixels must be in [0, 256]", + config_path.string())); + } if (cull.contains("screen_w_imms")) { ws_cull_w_imms.clear(); load_sites("screen_w_imms", ws_cull_w_imms); @@ -867,6 +875,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { /*ws_cull_range_sites*/ ws_cull_range_sites, /*ws_cull_a1_sites*/ ws_cull_a1_sites, /*ws_cull_slti_sites*/ ws_cull_slti_sites, + /*ws_cull_guard_pixels*/ ws_cull_guard_pixels, /*ws_cull_w_imms*/ ws_cull_w_imms, /*ws_cull_h_imms*/ ws_cull_h_imms, /*ws_backdrop_x_sites*/ ws_backdrop_x_sites, diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index f1440efb..6a412a78 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -422,6 +422,8 @@ struct GameConfig { // auto-detector cannot qualify (e.g. an X-only test with no height compare // in the same function — Ape Escape 0x8004AB64). Empty by default; regen. std::vector ws_cull_slti_sites; + // Extra per-side actor overdraw beyond the visible widescreen edge. + int ws_cull_guard_pixels = 0; // [widescreen.cull] screen_w_imms / screen_h_imms — the width/height // immediates of the GTE screen-extent reject signature, per game (the @@ -499,10 +501,9 @@ struct GameConfig { bool ws_nw_hud_corners = false; // [widescreen] nw_left_hud_packet_lo / nw_left_hud_packet_hi — optional - // half-open physical-RAM source range for a specifically identified left - // HUD assembly. In native-wide, only primitives submitted from this packet - // range move left by the reveal offset. This is safer for sprite-heavy 2D - // games where the broad nw_hud_corners heuristic would also catch scenery. + // half-open physical-RAM source range for a specifically identified HUD + // pool. In native-wide, primitives from this pool anchor by screen third + // (left/right), without moving similarly placed scenery from other pools. // Both values must be present together. Runtime-only; no regen required. uint32_t ws_nw_left_hud_packet_lo = 0; uint32_t ws_nw_left_hud_packet_hi = 0; diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 2472a15b..c3383826 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -129,6 +129,11 @@ int gpu_ws_present_native_43(void); /* Per-side X cull-margin (screen/world units) emitted into the game's draw- * cull immediates by the recompiler ([widescreen.cull]); 0 unless stretching. */ int psx_ws_x_margin(void); +void gpu_ws_set_cull_guard_pixels(int pixels); +void gpu_ws_set_explicit_cull_sites(const uint32_t *bias, int nbias, + const uint32_t *slti, int nslti); +int psx_ws_is_cull_bias_site(uint32_t pc); +int psx_ws_is_cull_slti_site(uint32_t pc); /* Shared render-funnel screen-X cull widening ([widescreen.cull] auto_screen_x): * the gcc emit, the sljit JIT, and the interpreter all route a flagged @@ -164,8 +169,8 @@ void psx_ws_note_gte_project(int nverts); * outer-third screen-space HUD sprites out to the true wide-frame corners * (they otherwise sit inset by the reveal). Runtime-only. Off by default. */ void gpu_ws_set_nw_hud_corners(int on); -/* Targeted alternative for sprite-heavy 2D games: move only primitives whose - * ordering-table packet lives in the configured half-open physical-RAM range. */ +/* Targeted alternative for sprite-heavy 2D games: corner-anchor only primitives + * whose ordering-table packet lives in the configured half-open RAM range. */ void gpu_ws_set_nw_left_hud_packet_range(uint32_t lo, uint32_t hi); void gpu_ws_begin_linked_list(void); /* Native-wide full-frame 2D backdrop stretch ([widescreen] nw_backdrop): diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 31224f0c..86f42732 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -1528,18 +1528,21 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) { return 1; } case 0x08: /* ADDI rt, rs, simm — same as ADDIU, sans overflow trap (we don't model traps here) */ - cpu->gpr[rt] = cpu->gpr[rs] + (uint32_t)simm; + cpu->gpr[rt] = cpu->gpr[rs] + (uint32_t)simm + + (psx_ws_is_cull_bias_site(pc) ? (uint32_t)psx_ws_x_margin() : 0u); cpu->gpr[0] = 0; return 0; case 0x09: /* ADDIU rt, rs, simm */ - cpu->gpr[rt] = cpu->gpr[rs] + (uint32_t)simm; + cpu->gpr[rt] = cpu->gpr[rs] + (uint32_t)simm + + (psx_ws_is_cull_bias_site(pc) ? (uint32_t)psx_ws_x_margin() : 0u); cpu->gpr[0] = 0; return 0; case 0x0A: /* SLTI */ /* Widescreen render-funnel RIGHT-edge widen (auto_screen_x) for the * signed min/max funnel idiom (`slti v, minSX, W`) — the paired left * edge is the bltz above. Identity at 4:3 (margin 0). */ - if (psx_ws_auto_cull_on() && psx_ws_is_cull_w_imm(imm) && ws_cull_site(pc)) + if (psx_ws_is_cull_slti_site(pc) || + (psx_ws_auto_cull_on() && psx_ws_is_cull_w_imm(imm) && ws_cull_site(pc))) cpu->gpr[rt] = (uint32_t)psx_ws_cull_slti(cpu->gpr[rs], imm); else cpu->gpr[rt] = ((int32_t)cpu->gpr[rs] < simm) ? 1u : 0u; diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index f98aae1f..9cba56e2 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -226,7 +226,41 @@ int ws_nw_extra(void) { return 2 * ws_nw_offset(); } * cull while still stretching; large = over-draw) at a fixed camera position. * -1 = normal computed margin. */ static int ws_margin_override = -1; +static int ws_cull_guard_pixels = 0; void gpu_ws_set_margin_override(int v) { ws_margin_override = v; } +void gpu_ws_set_cull_guard_pixels(int pixels) { + if (pixels < 0) pixels = 0; + if (pixels > 256) pixels = 256; + ws_cull_guard_pixels = pixels; +} + +#define WS_EXPLICIT_CULL_SITES_MAX 64 +static uint32_t ws_explicit_bias_sites[WS_EXPLICIT_CULL_SITES_MAX]; +static uint32_t ws_explicit_slti_sites[WS_EXPLICIT_CULL_SITES_MAX]; +static int ws_explicit_bias_n = 0; +static int ws_explicit_slti_n = 0; +void gpu_ws_set_explicit_cull_sites(const uint32_t *bias, int nbias, + const uint32_t *slti, int nslti) { + if (nbias < 0) nbias = 0; + if (nslti < 0) nslti = 0; + if (nbias > WS_EXPLICIT_CULL_SITES_MAX) nbias = WS_EXPLICIT_CULL_SITES_MAX; + if (nslti > WS_EXPLICIT_CULL_SITES_MAX) nslti = WS_EXPLICIT_CULL_SITES_MAX; + ws_explicit_bias_n = nbias; + ws_explicit_slti_n = nslti; + for (int i = 0; i < nbias; i++) ws_explicit_bias_sites[i] = bias[i] & 0x1FFFFFFFu; + for (int i = 0; i < nslti; i++) ws_explicit_slti_sites[i] = slti[i] & 0x1FFFFFFFu; +} +static int ws_explicit_site(const uint32_t *sites, int n, uint32_t pc) { + uint32_t p = pc & 0x1FFFFFFFu; + for (int i = 0; i < n; i++) if (sites[i] == p) return 1; + return 0; +} +int psx_ws_is_cull_bias_site(uint32_t pc) { + return ws_explicit_site(ws_explicit_bias_sites, ws_explicit_bias_n, pc); +} +int psx_ws_is_cull_slti_site(uint32_t pc) { + return ws_explicit_site(ws_explicit_slti_sites, ws_explicit_slti_n, pc); +} int psx_ws_x_margin(void) { if (ws_margin_override >= 0) return ws_margin_override; @@ -235,7 +269,7 @@ int psx_ws_x_margin(void) { * that previously fell outside the 4:3 cull window; the wide compositor then * rasterizes it into the revealed margins. Same recompiler emit sites as the * squash path ([widescreen.cull]); 0 at 4:3 so the cull stays byte-identical. */ - if (ws_native_wide_active()) return ws_nw_offset(); + if (ws_native_wide_active()) return ws_nw_offset() + ws_cull_guard_pixels; if (!ws_active()) return 0; return (160 * (ws_xden - ws_xnum) + ws_xnum / 2) / ws_xnum; } @@ -268,7 +302,6 @@ static int ws_mmx6_left_cols(void) { if (off <= 0) return 0; return (off + 15) / 16; /* ceil to whole tile columns */ } -static void mmx6_clear_void_before_bg(void); /* HOST-SIDE reveal mode (elective, default ON): the guest-side widen below overruns * the engine's 1024-slot BG packet buffer / 999-tile cap in dense stages (void/stale * columns). When host-side mode is active we keep the GUEST renderer at its NATIVE 21 @@ -1165,15 +1198,11 @@ static int ws_nw_left_hud_packet(void) { int matched = a >= ws_nw_left_hud_packet_lo && a < ws_nw_left_hud_packet_hi; return matched; } -static int32_t ws_nw_left_hud_shift(void) { - return ws_nw_left_hud_packet() ? -ws_nw_offset() : 0; -} static int32_t ws_nw_hud_shift(int32_t x, int32_t w) { if (!ws_native_wide_active()) return 0; int32_t off = ws_nw_offset(); if (off <= 0) return 0; - if (ws_nw_left_hud_packet()) return -off; - if (!ws_nw_hud_corners) return 0; + if (!ws_nw_left_hud_packet() && !ws_nw_hud_corners) return 0; int32_t W = ws_disp_w(); int32_t cx = 2 * x + w; /* 2*centre, avoids losing the half */ if (3 * cx < 2 * W) return -off; /* left third -> pull to left edge */ @@ -1338,19 +1367,11 @@ static void ws_clear_all_reveal_margins(void) { gr_wide_clear_margins((int)ws_fb_base[i], 0, 512, 0, 3); } -static void mmx6_clear_void_before_bg(void) { - static uint32_t cleared_generation[WS_FB_BASES] = { 0, 0, 0, 0 }; - if (!ws_clear_reveal || !ws_native_wide_active() || !g_mmx6_void_sides) - return; - int fb = -1; - for (int i = 0; i < ws_fb_n; i++) - if (ws_fb_base[i] == draw_area_left) { fb = i; break; } - if (fb < 0 || cleared_generation[fb] == g_mmx6_void_generation) return; - gr_wide_clear_margins((int)draw_area_left, 0, 512, 0, - g_mmx6_void_sides); - cleared_generation[fb] = g_mmx6_void_generation; -} -void gpu_ws_begin_linked_list(void) { mmx6_clear_void_before_bg(); } +/* Stage-init already clears both synthetic margins once. Do not keep clearing + * a guessed finite-map side here: MMX6's authored layers enter the reveal at + * different times, so the side guess produced a moving black trim over valid + * stage art. A stale reveal tile is safer than deleting submitted content. */ +void gpu_ws_begin_linked_list(void) { } /* Horizontal display range (GP1(06h)) */ @@ -1898,7 +1919,12 @@ static void gp0_exec_shaded_quad(void) { c[i] = rgb888_to_rgb555(gp0_cmd_buf[i * 2] & 0xFFFFFFu); parse_vertex(gp0_cmd_buf[1 + i * 2], &vx[i], &vy[i]); } - int32_t hud_dx = ws_nw_left_hud_shift(); + int32_t minx = vx[0], maxx = vx[0]; + for (int i = 1; i < 4; i++) { + if (vx[i] < minx) minx = vx[i]; + if (vx[i] > maxx) maxx = vx[i]; + } + int32_t hud_dx = ws_nw_hud_shift(minx, maxx - minx); if (hud_dx) for (int i = 0; i < 4; i++) vx[i] += hud_dx; ws_nw_backdrop_stretch_quad(vx, vy); /* full-frame 2D backdrop stretch (sky gradient; no-op else) */ @@ -2280,17 +2306,9 @@ static void gp0_exec_textured_16x16(void) { int raw_texture = (gp0_cmd_buf[0] >> 24) & 1; int32_t x0, y0; parse_vertex(gp0_cmd_buf[1], &x0, &y0); - /* MMX6 finite-map void: the widened renderer still submits stale ring slots - * for columns before/after the real map. DMA linked-list start has cleared - * that synthetic side once for this stage generation; suppress only those - * extra BG packets so they cannot paint stale tiles back over the void. */ - if (ws_clear_reveal && gp0_cmd_source_addr != 0xFFFFFFFFu) { - uint32_t src = gp0_cmd_source_addr & 0x1FFFFFFFu; - if (src >= MMX6_BG_BUF_LO && src < MMX6_BG_BUF_HI) { - if ((g_mmx6_void_sides & 1) && x0 < 0) return; - if ((g_mmx6_void_sides & 2) && x0 >= (int32_t)ws_disp_w()) return; - } - } + /* Never suppress MMX6 BG packets at a guessed finite-map boundary. The + * classifier cannot distinguish an authored layer entering the reveal from + * a stale ring slot; suppressing here caused the stage-start black flicker. */ int ws_w = ws_sprt_fixed_transform(&x0, 16); x0 += ws_nw_hud_shift(x0, 16); /* native-wide HUD corner re-anchor (no-op else) */ x0 += draw_offset_x; y0 += draw_offset_y; diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index c17c146d..020c8baa 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -2172,6 +2172,10 @@ int main(int argc, char** argv) { /* [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); + gpu_ws_set_cull_guard_pixels(gc.ws_cull_guard_pixels); + gpu_ws_set_explicit_cull_sites( + gc.ws_cull_bias_sites.data(), (int)gc.ws_cull_bias_sites.size(), + gc.ws_cull_slti_sites.data(), (int)gc.ws_cull_slti_sites.size()); /* [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. */ diff --git a/runtime/src/overlay_sljit.c b/runtime/src/overlay_sljit.c index 3c7c52c3..66e39a69 100644 --- a/runtime/src/overlay_sljit.c +++ b/runtime/src/overlay_sljit.c @@ -18,6 +18,7 @@ #include #include "sljitLir.h" +#include "gpu.h" #include "ws_backdrop_detect.h" /* shared backdrop-window detector (auto_backdrop) */ #ifdef _WIN32 @@ -834,6 +835,17 @@ void overlay_sljit_try_compile(uint32_t entry, if (!found_term || frag_words == 0 || frag_words > SLJIT_MAX_FRAG_INSNS) { s_declines++; return; } + + /* Explicit address-listed cull hooks carry a live aspect term. Generated + * overlay DLLs and the interpreter implement it directly; keep these rare + * fragments out of the generic JIT so a vanilla immediate is never cached. */ + for (uint32_t i = 0; i < frag_words; i++) { + uint32_t pc = entry + i * 4u; + if (psx_ws_is_cull_bias_site(pc) || psx_ws_is_cull_slti_site(pc)) { + s_declines++; + return; + } + } if (off0 + frag_words * 4u > size) { s_declines++; return; } /* delay slot off image */ /* Validate branch targets: in-range, aligned, not landing on a delay slot. */