From 75e77e5ebf980a8040b20273065f92f6a470166a Mon Sep 17 00:00:00 2001 From: Opok Date: Thu, 23 Jul 2026 00:28:34 +0200 Subject: [PATCH 1/2] widescreen: widen draw-area early-outs by the native-wide reveal offset The FMV draw-area early-outs in gp0_exec_* reject prims against the guest's authored 4:3 draw area before the renderer's wide mirror can draw them. In native-wide mode every margin prim submitted through the widened game culls ([widescreen.cull] hooks) is killed at the door, so world and actor geometry pops in at the revealed edges. Reported on Xenogears; affects any title using cull widening. The mirror maps local_x = x - draw_area_left + offset and clips at [0, disp_w + 2*offset), so the host-side check is only lossless when it tests exactly that window. draw_area_cull_window() now computes [left - off, max(right, left + disp_w) + off]: with native-wide inactive the offset is 0 and the check is byte-identical to the original optimization (4:3 / squash / FMV keep the host-side skip); with native-wide active, margin prims pass while genuinely off-screen prims are still culled host-side. --- runtime/src/gpu.c | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 4398753d..a04ae1c8 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -2350,12 +2350,32 @@ static void raster_pixel(int32_t x, int32_t y, uint16_t color) { * prim (gpu_share ~0.9, host FPS ~5–10). Skip the host rasterizer when the * post-offset bbox cannot touch the draw area. Side effects that must still * run (texpage latch, oversize reject) happen before these checks. */ +/* Native-wide: the reveal lives in the renderer's wide mirror surface, where a + * guest prim maps to local_x = x - draw_area_left + offset and is clipped by + * the surface scissor at [0, disp_w + 2*offset). The early-out window below is + * widened by exactly that visible range, so margin prims submitted through the + * widened game culls are not rejected at the door. The offset is 0 whenever + * native-wide is inactive (4:3 / squash / FMV), which reduces the window to + * the authored draw area — byte-identical to the original check. */ +static inline void draw_area_cull_window(int32_t *xl, int32_t *xr) { + int32_t off = ws_nw_offset(); + int32_t right = (int32_t)draw_area_right; + if (off > 0) { + int32_t wide_right = (int32_t)draw_area_left + ws_disp_w(); + if (wide_right > right) right = wide_right; + } + *xl = (int32_t)draw_area_left - off; + *xr = right + off; +} + static inline int draw_area_out_point(int32_t x, int32_t y) { - return x < (int32_t)draw_area_left || x > (int32_t)draw_area_right + int32_t xl, xr; draw_area_cull_window(&xl, &xr); + return x < xl || x > xr || y < (int32_t)draw_area_top || y > (int32_t)draw_area_bottom; } static inline int draw_area_out_bbox(const int32_t *vx, const int32_t *vy, int n) { + int32_t xl, xr; draw_area_cull_window(&xl, &xr); int32_t minx = vx[0], maxx = vx[0], miny = vy[0], maxy = vy[0]; for (int i = 1; i < n; i++) { if (vx[i] < minx) minx = vx[i]; @@ -2363,14 +2383,15 @@ static inline int draw_area_out_bbox(const int32_t *vx, const int32_t *vy, int n if (vy[i] < miny) miny = vy[i]; if (vy[i] > maxy) maxy = vy[i]; } - return maxx < (int32_t)draw_area_left || minx > (int32_t)draw_area_right + return maxx < xl || minx > xr || maxy < (int32_t)draw_area_top || miny > (int32_t)draw_area_bottom; } static inline int draw_area_out_rect(int32_t x, int32_t y, int w, int h) { if (w <= 0 || h <= 0) return 1; - return (x + w - 1) < (int32_t)draw_area_left - || x > (int32_t)draw_area_right + int32_t xl, xr; draw_area_cull_window(&xl, &xr); + return (x + w - 1) < xl + || x > xr || (y + h - 1) < (int32_t)draw_area_top || y > (int32_t)draw_area_bottom; } From e5027ce068dbbf7db6c26b0d9a596155d9e0bac5 Mon Sep 17 00:00:00 2001 From: Opok Date: Fri, 24 Jul 2026 00:39:51 +0200 Subject: [PATCH 2/2] widescreen: stretch backdrop X-stores about screen centre in native-wide mode psx_ws_backdrop_x() was identity in native-wide (its ws_active() gate only covers the 4:3-squash path), so [widescreen.backdrop] x_sites did nothing there. Add a native-wide branch that stretches the stored screenX about the display centre by (disp_w + nw_extra)/disp_w, letting 4:3-authored 2D backdrops cover the widened frame. Verified on Xenogears battle mountain panels (POLY_FT4 quads with pre-calculated screen coords; main-EXE sh store sites 0x80027BE8/BEC/BF0/BF8, wtrace-evidenced). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- runtime/src/gpu.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 3b5056aa..7552f95a 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -1104,8 +1104,21 @@ int psx_ws_cull_bltz_at(const uint32_t *words, int n, int idx) { * was GPU-clipped → the blue void / half-rectangles at the edges) is pulled * in to cover the revealed FOV. Identity at 4:3 / boot / FMV / full-2D (the * exact ws_active() predicate the GTE squash uses), so one build serves both. + * In native-wide mode there is nothing to squash (the 4:3 frame is presented + * with side reveal instead), so the same sites are STRETCHED about the screen + * centre by (disp_w + nw_extra) / disp_w: a 4:3-authored backdrop then covers + * the widened frame instead of leaving unpainted margins (Xenogears battle + * mountain panels — pre-calculated POLY_FT4 screen coords stored by main-EXE + * `sh` sites, wtrace-evidenced). * x is the int16 screenX the handler was about to store. */ int psx_ws_backdrop_x(int x) { + if (ws_native_wide_active()) { + int32_t W = (int32_t)ws_disp_w(); + int32_t extra = ws_nw_extra(); + int32_t cx = W / 2; + int32_t d = (int16_t)x - cx; + return (int)(cx + (d * (W + extra) + (d >= 0 ? W / 2 : -W / 2)) / W); + } if (!ws_active()) return (int16_t)x; int32_t cx = ws_disp_w() / 2; /* screen centre (=160 @ 320) */ return ws_scale_about((int16_t)x, cx);