Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/internal/upstream/martin-pr16-gpu-primitive-size-reject.md
Original file line number Diff line number Diff line change
@@ -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 <mpenkava1337@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
```
34 changes: 34 additions & 0 deletions runtime/include/gpu_primitive_reject.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#ifndef PSX_GPU_PRIMITIVE_REJECT_H
#define PSX_GPU_PRIMITIVE_REJECT_H

#include <stdint.h>

/* 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
91 changes: 60 additions & 31 deletions runtime/src/gpu.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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 */
Expand All @@ -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;
Expand Down Expand Up @@ -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++) {
Expand All @@ -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.
Expand Down Expand Up @@ -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++) {
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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) */
Expand All @@ -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++) {
Expand Down Expand Up @@ -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++) {
Expand All @@ -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 */
Expand All @@ -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];
Expand All @@ -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];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
36 changes: 36 additions & 0 deletions runtime/tests/test_gpu_primitive_reject.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#include "gpu_primitive_reject.h"

#include <stdio.h>

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;
}