feat: finish MMX6 widescreen edge handling

This commit is contained in:
Matthew Stanley
2026-07-10 08:28:43 -07:00
parent 79cdb12eed
commit e9ca97ad11
7 changed files with 93 additions and 41 deletions
+9
View File
@@ -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<uint32_t> ws_cull_bias_sites, ws_cull_range_sites, ws_cull_a1_sites;
std::vector<uint32_t> 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<int>(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,
+5 -4
View File
@@ -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<uint32_t> 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;
+7 -2
View File
@@ -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):
+6 -3
View File
@@ -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;
+50 -32
View File
@@ -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;
+4
View File
@@ -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. */
+12
View File
@@ -18,6 +18,7 @@
#include <stdlib.h>
#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. */