From 19e526d57d61452a86a33ff0de0a90633f3a628f Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Tue, 14 Jul 2026 10:00:18 -0700 Subject: [PATCH] gpu: reject oversized hardware primitives Apply the PS1 1023x511 size rule to every polygon triangle, line, and polyline continuation before widescreen transforms while preserving textured-command state latching. Co-authored-by: Martin Penkava Co-authored-by: Claude Fable 5 --- .../martin-pr16-gpu-primitive-size-reject.md | 33 +++++++ runtime/include/gpu_primitive_reject.h | 34 +++++++ runtime/src/gpu.c | 91 ++++++++++++------- runtime/tests/test_gpu_primitive_reject.c | 36 ++++++++ 4 files changed, 163 insertions(+), 31 deletions(-) create mode 100644 docs/internal/upstream/martin-pr16-gpu-primitive-size-reject.md create mode 100644 runtime/include/gpu_primitive_reject.h create mode 100644 runtime/tests/test_gpu_primitive_reject.c diff --git a/docs/internal/upstream/martin-pr16-gpu-primitive-size-reject.md b/docs/internal/upstream/martin-pr16-gpu-primitive-size-reject.md new file mode 100644 index 000000000..48d7437d3 --- /dev/null +++ b/docs/internal/upstream/martin-pr16-gpu-primitive-size-reject.md @@ -0,0 +1,33 @@ +# PR #16 GPU primitive-size rejection provenance + +This branch isolates Martin Penkava's hardware primitive-size rejection from +[`mstan/psxrecomp` PR #16](https://github.com/mstan/psxrecomp/pull/16), exact +source commit +[`cfc5d0eab94c7ddfd30ed23c4d3beecdc737c835`](https://github.com/mstan/psxrecomp/commit/cfc5d0eab94c7ddfd30ed23c4d3beecdc737c835). + +Last evaluated: 2026-07-14 + +| Item | Value | +| --- | --- | +| Head repository/branch | `shaneomac1337/psxrecomp`, `smackdown2-fixes` | +| Pull request/source commit | `mstan/psxrecomp` PR #16, `cfc5d0eab94c7ddfd30ed23c4d3beecdc737c835` | +| Local base | `7085721afe338a03cb114321a3576cdff420b732` (`origin/master`) | +| Local branch | `fix/pr16-gpu-primitive-size-reject-mpenkava` | + +The 1023-pixel horizontal and 511-pixel vertical limits are PS1 hardware rules +documented by No$PSX and implemented by independent emulators including Beetle +and DuckStation. The framework checks parsed coordinates before widescreen and +draw-offset transforms. Every flat, shaded, textured, and shaded-textured +polygon path is covered; quads reject each rendered triangle independently. +Mono/shaded lines and both polyline continuations use the same limits. Textured +commands still latch their texture-page state before a size rejection. + +The source commit's game name and observed scenes are validation evidence only; +no title identifier, address, or configuration is included here. + +Source authorship is retained with: + +```text +Co-authored-by: Martin Penkava +Co-authored-by: Claude Fable 5 +``` diff --git a/runtime/include/gpu_primitive_reject.h b/runtime/include/gpu_primitive_reject.h new file mode 100644 index 000000000..313b0c847 --- /dev/null +++ b/runtime/include/gpu_primitive_reject.h @@ -0,0 +1,34 @@ +#ifndef PSX_GPU_PRIMITIVE_REJECT_H +#define PSX_GPU_PRIMITIVE_REJECT_H + +#include + +/* PS1 hardware primitive-size rejection. Parsed coordinates are checked before + * widescreen transforms and draw offsets; offsets do not change distances. + * Quads are tested as their two rendered triangles independently. */ +static inline int psx_gpu_triangle_oversize(const int32_t* vx, + const int32_t* vy, + int a, int b, int c) { + int32_t minx = vx[a], maxx = vx[a]; + if (vx[b] < minx) minx = vx[b]; + if (vx[b] > maxx) maxx = vx[b]; + if (vx[c] < minx) minx = vx[c]; + if (vx[c] > maxx) maxx = vx[c]; + if (maxx - minx > 1023) return 1; + + int32_t miny = vy[a], maxy = vy[a]; + if (vy[b] < miny) miny = vy[b]; + if (vy[b] > maxy) maxy = vy[b]; + if (vy[c] < miny) miny = vy[c]; + if (vy[c] > maxy) maxy = vy[c]; + return maxy - miny > 511; +} + +static inline int psx_gpu_line_oversize(int32_t x0, int32_t y0, + int32_t x1, int32_t y1) { + int32_t dx = x0 > x1 ? x0 - x1 : x1 - x0; + int32_t dy = y0 > y1 ? y0 - y1 : y1 - y0; + return dx > 1023 || dy > 511; +} + +#endif diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 88e119ef1..5698a1150 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -11,6 +11,7 @@ */ #include "gpu.h" +#include "gpu_primitive_reject.h" #include "gpu_sw_renderer.h" #include "gpu_render.h" #include "text_xlate.h" @@ -2142,6 +2143,7 @@ static void gp0_exec_mono_tri(void) { for (int i = 0; i < 3; i++) { parse_vertex(gp0_cmd_buf[1 + i], &vx[i], &vy[i]); } + if (psx_gpu_triangle_oversize(vx, vy, 0, 1, 2)) return; ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { vx[i] += draw_offset_x; @@ -2158,6 +2160,9 @@ static void gp0_exec_mono_quad(void) { int32_t vx[4], vy[4]; for (int i = 0; i < 4; i++) parse_vertex(gp0_cmd_buf[1 + i], &vx[i], &vy[i]); + int rej_a = psx_gpu_triangle_oversize(vx, vy, 0, 1, 2); + int rej_b = psx_gpu_triangle_oversize(vx, vy, 2, 1, 3); + if (rej_a && rej_b) return; /* Full-screen filters are commonly encoded as an axis-aligned quad. Drawing * a semi-transparent quad as two independent triangles blends their shared @@ -2187,8 +2192,10 @@ static void gp0_exec_mono_quad(void) { vy[i] += draw_offset_y; } gr_set_semi_transparency(semi_trans, (int)semi_transparency); - gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color); - gr_draw_flat_triangle(vx[2], vy[2], vx[1], vy[1], vx[3], vy[3], color); + if (!rej_a) + gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color); + if (!rej_b) + gr_draw_flat_triangle(vx[2], vy[2], vx[1], vy[1], vx[3], vy[3], color); } /* Execute shaded triangle (GP0 0x30-0x33) — Gouraud shaded */ @@ -2201,6 +2208,7 @@ static void gp0_exec_shaded_tri(void) { c[i] = rgb888_to_rgb555(gp0_cmd_buf[i * 2] & 0xFFFFFFu); parse_vertex(gp0_cmd_buf[1 + i * 2], &vx[i], &vy[i]); } + if (psx_gpu_triangle_oversize(vx, vy, 0, 1, 2)) return; ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { vx[i] += draw_offset_x; @@ -2229,6 +2237,9 @@ 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]); } + int rej_a = psx_gpu_triangle_oversize(vx, vy, 0, 1, 2); + int rej_b = psx_gpu_triangle_oversize(vx, vy, 2, 1, 3); + if (rej_a && rej_b) return; ws_nw_backdrop_stretch_quad(vx, vy); /* full-frame 2D backdrop stretch (sky gradient; no-op else) */ ws_nw_hud_shift_vertices(vx, 4); for (int i = 0; i < 4; i++) { @@ -2244,12 +2255,14 @@ static void gp0_exec_shaded_quad(void) { } } gr_set_semi_transparency(semi_trans, (int)semi_transparency); - gr_draw_gouraud_triangle(vx[0], vy[0], c[0], - vx[1], vy[1], c[1], - vx[2], vy[2], c[2]); - gr_draw_gouraud_triangle(vx[2], vy[2], c[2], - vx[1], vy[1], c[1], - vx[3], vy[3], c[3]); + if (!rej_a) + gr_draw_gouraud_triangle(vx[0], vy[0], c[0], + vx[1], vy[1], c[1], + vx[2], vy[2], c[2]); + if (!rej_b) + gr_draw_gouraud_triangle(vx[2], vy[2], c[2], + vx[1], vy[1], c[1], + vx[3], vy[3], c[3]); } /* Helper: build texpage word from GPU state for SW renderer. @@ -2305,7 +2318,8 @@ static void gp0_exec_textured_tri(void) { /* Texpage from word 4 bits 16-31 */ uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[4] >> 16); uint16_t tpage = tpage_word & 0x1FF; - set_tpage_from_poly(tpage_word); + set_tpage_from_poly(tpage_word); /* latches even for size-rejected polys */ + if (psx_gpu_triangle_oversize(vx, vy, 0, 1, 2)) return; ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { @@ -2341,7 +2355,10 @@ static void gp0_exec_textured_quad(void) { uint16_t clut_y = (clut >> 6) & 0x1FF; uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[4] >> 16); uint16_t tpage = tpage_word & 0x1FF; - set_tpage_from_poly(tpage_word); + set_tpage_from_poly(tpage_word); /* latches even for size-rejected polys */ + int rej_a = psx_gpu_triangle_oversize(vx, vy, 0, 1, 2); + int rej_b = psx_gpu_triangle_oversize(vx, vy, 2, 1, 3); + if (rej_a && rej_b) return; /* Widescreen: tagged billboard quads carry CPU-computed pixel offsets the * GTE squash never saw — re-squash every X around the prim's anchor. */ @@ -2385,14 +2402,16 @@ static void gp0_exec_textured_quad(void) { } } - gr_draw_textured_triangle(vx[0], vy[0], u[0], v[0], - vx[1], vy[1], u[1], v[1], - vx[2], vy[2], u[2], v[2], - clut_x, clut_y, tpage); - gr_draw_textured_triangle(vx[2], vy[2], u[2], v[2], - vx[1], vy[1], u[1], v[1], - vx[3], vy[3], u[3], v[3], - clut_x, clut_y, tpage); + if (!rej_a) + gr_draw_textured_triangle(vx[0], vy[0], u[0], v[0], + vx[1], vy[1], u[1], v[1], + vx[2], vy[2], u[2], v[2], + clut_x, clut_y, tpage); + if (!rej_b) + gr_draw_textured_triangle(vx[2], vy[2], u[2], v[2], + vx[1], vy[1], u[1], v[1], + vx[3], vy[3], u[3], v[3], + clut_x, clut_y, tpage); } /* Execute shaded textured triangle (GP0 0x34-0x37) */ @@ -2417,7 +2436,8 @@ static void gp0_exec_shaded_textured_tri(void) { uint16_t clut_y = (clut >> 6) & 0x1FF; uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[5] >> 16); uint16_t tpage = tpage_word & 0x1FF; - set_tpage_from_poly(tpage_word); + set_tpage_from_poly(tpage_word); /* latches even for size-rejected polys */ + if (psx_gpu_triangle_oversize(vx, vy, 0, 1, 2)) return; ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { @@ -2457,7 +2477,10 @@ static void gp0_exec_shaded_textured_quad(void) { uint16_t clut_y = (clut >> 6) & 0x1FF; uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[5] >> 16); uint16_t tpage = tpage_word & 0x1FF; - set_tpage_from_poly(tpage_word); + set_tpage_from_poly(tpage_word); /* latches even for size-rejected polys */ + int rej_a = psx_gpu_triangle_oversize(vx, vy, 0, 1, 2); + int rej_b = psx_gpu_triangle_oversize(vx, vy, 2, 1, 3); + if (rej_a && rej_b) return; ws_nw_hud_shift_vertices(vx, 4); for (int i = 0; i < 4; i++) { @@ -2466,14 +2489,16 @@ static void gp0_exec_shaded_textured_quad(void) { } gr_set_semi_transparency(semi_trans, (int)semi_transparency); - gr_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0], - vx[1], vy[1], u[1], v[1], c[1], - vx[2], vy[2], u[2], v[2], c[2], - clut_x, clut_y, tpage, raw_texture); - gr_draw_shaded_textured_triangle(vx[2], vy[2], u[2], v[2], c[2], - vx[1], vy[1], u[1], v[1], c[1], - vx[3], vy[3], u[3], v[3], c[3], - clut_x, clut_y, tpage, raw_texture); + if (!rej_a) + gr_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0], + vx[1], vy[1], u[1], v[1], c[1], + vx[2], vy[2], u[2], v[2], c[2], + clut_x, clut_y, tpage, raw_texture); + if (!rej_b) + gr_draw_shaded_textured_triangle(vx[2], vy[2], u[2], v[2], c[2], + vx[1], vy[1], u[1], v[1], c[1], + vx[3], vy[3], u[3], v[3], c[3], + clut_x, clut_y, tpage, raw_texture); } /* Execute mono line (GP0 0x40-0x47) — Bresenham */ @@ -2483,6 +2508,7 @@ static void gp0_exec_mono_line(void) { int32_t x0, y0, x1, y1; parse_vertex(gp0_cmd_buf[1], &x0, &y0); parse_vertex(gp0_cmd_buf[2], &x1, &y1); + if (psx_gpu_line_oversize(x0, y0, x1, y1)) return; int32_t vx[2] = { x0, x1 }; ws_nw_hud_shift_vertices(vx, 2); x0 = vx[0]; x1 = vx[1]; @@ -2500,6 +2526,7 @@ static void gp0_exec_shaded_line(void) { int32_t x0, y0, x1, y1; parse_vertex(gp0_cmd_buf[1], &x0, &y0); parse_vertex(gp0_cmd_buf[3], &x1, &y1); + if (psx_gpu_line_oversize(x0, y0, x1, y1)) return; int32_t vx[2] = { x0, x1 }; ws_nw_hud_shift_vertices(vx, 2); x0 = vx[0]; x1 = vx[1]; @@ -3574,7 +3601,8 @@ static void gpu_write_gp0_body(uint32_t val) { int32_t x, y; parse_vertex(val, &x, &y); x += draw_offset_x; y += draw_offset_y; - if (polyline_has_prev) { + if (polyline_has_prev && + !psx_gpu_line_oversize(polyline_prev_x, polyline_prev_y, x, y)) { gr_draw_line(polyline_prev_x, polyline_prev_y, x, y, polyline_color); } polyline_prev_x = x; polyline_prev_y = y; @@ -3616,8 +3644,9 @@ static void gpu_write_gp0_body(uint32_t val) { int32_t x, y; parse_vertex(val, &x, &y); x += draw_offset_x; y += draw_offset_y; - gr_draw_shaded_line(polyline_prev_x, polyline_prev_y, polyline_prev_c, - x, y, polyline_color); + if (!psx_gpu_line_oversize(polyline_prev_x, polyline_prev_y, x, y)) + gr_draw_shaded_line(polyline_prev_x, polyline_prev_y, + polyline_prev_c, x, y, polyline_color); polyline_prev_x = x; polyline_prev_y = y; polyline_prev_c = polyline_color; polyline_has_prev = 1; diff --git a/runtime/tests/test_gpu_primitive_reject.c b/runtime/tests/test_gpu_primitive_reject.c new file mode 100644 index 000000000..ddd88607f --- /dev/null +++ b/runtime/tests/test_gpu_primitive_reject.c @@ -0,0 +1,36 @@ +#include "gpu_primitive_reject.h" + +#include + +static int failures; + +static void check(int condition, const char* name) { + if (!condition) { + fprintf(stderr, "FAIL: %s\n", name); + failures++; + } +} + +int main(void) { + int32_t x_ok[3] = { -512, 511, 0 }; + int32_t y_ok[3] = { -255, 256, 0 }; + int32_t x_wide[3] = { -512, 512, 0 }; + int32_t y_tall[3] = { -256, 256, 0 }; + + check(!psx_gpu_triangle_oversize(x_ok, y_ok, 0, 1, 2), + "inclusive 1023x511 boundary is accepted"); + check(psx_gpu_triangle_oversize(x_wide, y_ok, 0, 1, 2), + "triangle wider than 1023 is rejected"); + check(psx_gpu_triangle_oversize(x_ok, y_tall, 0, 1, 2), + "triangle taller than 511 is rejected"); + check(!psx_gpu_line_oversize(-512, -255, 511, 256), + "line boundary is accepted"); + check(psx_gpu_line_oversize(-512, 0, 512, 0), + "oversize horizontal line is rejected"); + check(psx_gpu_line_oversize(0, -256, 0, 256), + "oversize vertical line is rejected"); + + if (failures) return 1; + puts("PASS: PS1 primitive size rejection boundaries"); + return 0; +}