gpu gl/vk: mirrored-2D uv compensation for the RECT path too

The e86099f fix put the Beetle Calc_UVOffsets mirrored-uv bump in the
textured-POLY path only — but gpu.c's axis-aligned-quad fast path converts
mirrored quads (X/Y-flipped 2D sprites, e.g. every right-facing MMX entity
cel) into gr_draw_textured_rect_scaled calls with u0>u1 / v0>v1, so they
enter gpu_textured_rect and never see the poly compensation. That is why
the sprite slivers persisted on X (worst facing right / crouching) after
e86099f: the visible producers were all rect-path prims.

Fix: same +1 bump along each decreasing uv axis in gpu_textured_rect (GL
and VK), with the sampling bounds tightened to [min, max-1] post-bump
(max is the exclusive edge in both directions). Forward rects are
unaffected (bump is a no-op; bounds identical to before).

Frame-exact verification (deterministic attract, frame 2850, X idle
facing right): pre-fix GL vs software full-frame diff = 3707 px spread
across the sprite area; post-fix = 0 px in the entire gameplay area (the
only remaining diff is the attract logo overlay blink-phase offset caused
by the known GL blend-rounding timeline fork, tracked separately).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-07-09 18:46:38 -07:00
parent e8ad8ade3d
commit 148f171daf
2 changed files with 24 additions and 7 deletions
+16 -5
View File
@@ -1548,12 +1548,23 @@ static void gpu_textured_rect(int x,int y,int w,int h,
if (w <= 0 || h <= 0) return;
float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f;
float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb};
/* uv sampling bounds: forward mappings sample [u0, u1-1] (u1 is the
* exclusive edge); mirrored ones keep the full inclusive range. Crossing
* a 256 boundary means page wrap — widen to the full page. */
/* Mirrored-2D compensation, rect form (Beetle Calc_UVOffsets model —
* see tri_uv_mirror_offset). gpu.c routes axis-aligned MIRRORED quads
* (X/Y-flipped 2D sprites, e.g. right-facing MMX entities) through THIS
* path as scaled rects with u0>u1 / v0>v1 — they never reach the poly
* path. Center-sampled interpolation with the -1/64 grid shift floors
* one texel LOW along a decreasing axis (the PS1 DDA latches the
* top-left corner value), painting the cel's never-sampled edge texels
* as detached slivers at cell seams. Bump the uv range +1 along each
* decreasing axis so floor() lands on the exact PS1 texel. */
if (u1 < u0) { u0++; u1++; }
if (v1 < v0) { v0++; v1++; }
/* uv sampling bounds: post-bump, the max-uv end is the exclusive edge
* in BOTH directions — sample range is [min, max-1]. Crossing a 256
* boundary means page wrap — widen to the full page. */
int lim[4];
lim[0] = u0 < u1 ? u0 : u1; lim[2] = u0 < u1 ? u1 - 1 : u0;
lim[1] = v0 < v1 ? v0 : v1; lim[3] = v0 < v1 ? v1 - 1 : v0;
lim[0] = u0 < u1 ? u0 : u1; lim[2] = (u0 < u1 ? u1 : u0) - 1;
lim[1] = v0 < v1 ? v0 : v1; lim[3] = (v0 < v1 ? v1 : v0) - 1;
if (lim[2] < lim[0]) lim[2] = lim[0];
if (lim[3] < lim[1]) lim[3] = lim[1];
if ((lim[0] >> 8) == (lim[2] >> 8)) { lim[0] &= 255; lim[2] &= 255; }
+8 -2
View File
@@ -2633,9 +2633,15 @@ static void gpu_textured_rect(int x,int y,int w,int h, int u0,int v0,int u1,int
if (w <= 0 || h <= 0) return;
float mr = s_mod_r/255.0f, mg = s_mod_g/255.0f, mb = s_mod_b/255.0f;
float col[9] = { mr,mg,mb, mr,mg,mb, mr,mg,mb };
/* Mirrored-2D compensation, rect form — see the GL backend's
* gpu_textured_rect: axis-aligned mirrored quads arrive here as scaled
* rects with u0>u1 / v0>v1 and floor one texel low under center-sampled
* interpolation; bump the range +1 along each decreasing axis. */
if (u1 < u0) { u0++; u1++; }
if (v1 < v0) { v0++; v1++; }
int lim[4];
lim[0] = u0 < u1 ? u0 : u1; lim[2] = u0 < u1 ? u1 - 1 : u0;
lim[1] = v0 < v1 ? v0 : v1; lim[3] = v0 < v1 ? v1 - 1 : v0;
lim[0] = u0 < u1 ? u0 : u1; lim[2] = (u0 < u1 ? u1 : u0) - 1;
lim[1] = v0 < v1 ? v0 : v1; lim[3] = (v0 < v1 ? v1 : v0) - 1;
if (lim[2] < lim[0]) lim[2] = lim[0];
if (lim[3] < lim[1]) lim[3] = lim[1];
if ((lim[0] >> 8) == (lim[2] >> 8)) { lim[0] &= 255; lim[2] &= 255; } else { lim[0] = 0; lim[2] = 255; }