From 928085d5fbb1d4ec7bd722ee51616ec156c68f1b Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Tue, 14 Jul 2026 09:58:31 -0700 Subject: [PATCH] gpu: use exclusive PS1 software-raster edges Adapt PR #14's biased 32.32 triangle edge DDA and half-open bottom/right coverage across the software raster paths, with a focused pure edge-math regression test. Source: https://github.com/mstan/psxrecomp/pull/14 Source-commit: https://github.com/mstan/psxrecomp/commit/91a654f61dff18806f3eea43f72a30cb34430695 Co-authored-by: Kareem Olim --- .../kem0x-pr14-software-raster-edges.md | 13 ++++ runtime/src/gpu_sw_edges.h | 39 +++++++++++ runtime/src/gpu_sw_renderer.c | 68 ++++++------------- runtime/tests/test_gpu_sw_edges.c | 28 ++++++++ 4 files changed, 101 insertions(+), 47 deletions(-) create mode 100644 docs/internal/upstream/kem0x-pr14-software-raster-edges.md create mode 100644 runtime/src/gpu_sw_edges.h create mode 100644 runtime/tests/test_gpu_sw_edges.c diff --git a/docs/internal/upstream/kem0x-pr14-software-raster-edges.md b/docs/internal/upstream/kem0x-pr14-software-raster-edges.md new file mode 100644 index 00000000..781bd6cb --- /dev/null +++ b/docs/internal/upstream/kem0x-pr14-software-raster-edges.md @@ -0,0 +1,13 @@ +# PR 14 software-raster triangle coverage + +Evaluated and adapted on 2026-07-14 from Kareem Olim's +[PR #14](https://github.com/mstan/psxrecomp/pull/14), exact commit +[`91a654f61dff18806f3eea43f72a30cb34430695`](https://github.com/mstan/psxrecomp/commit/91a654f61dff18806f3eea43f72a30cb34430695). + +Adapted scope: the software rasterizer's biased 32.32 edge DDA and half-open +bottom/right triangle coverage for flat, Gouraud, textured, and shaded-textured +triangles. The reusable edge math was moved to a small tested internal header. + +Excluded: all title references, WebAssembly work, CD/SPU changes, GTE subpixel +and perspective experiments, presentation blending, and every other PR #14 +change. This branch changes only software triangle coverage. diff --git a/runtime/src/gpu_sw_edges.h b/runtime/src/gpu_sw_edges.h new file mode 100644 index 00000000..bb27a998 --- /dev/null +++ b/runtime/src/gpu_sw_edges.h @@ -0,0 +1,39 @@ +#ifndef PSXRECOMP_GPU_SW_EDGES_H +#define PSXRECOMP_GPU_SW_EDGES_H + +#include + +/* PS1 polygon coverage uses a biased 32.32 edge DDA with exclusive bottom + * and right edges. Float-truncated inclusive spans make adjacent polygons + * disagree by a pixel and draw a shared diagonal twice. */ +static inline int64_t psx_edge_fp(int x) { + return ((int64_t)x << 32) + ((1LL << 32) - (1 << 11)); +} + +static inline int64_t psx_edge_step(int dx, int dy) { + int64_t rounding = dx < 0 ? -(int64_t)(dy - 1) + : (dx > 0 ? (int64_t)(dy - 1) : 0); + return (((int64_t)dx << 32) + rounding) / dy; +} + +static inline int psx_edge_unfp(int64_t fp) { + return (int)((uint64_t)fp >> 32); +} + +/* Vertices must already be sorted by Y. y is in [y0,y2). */ +static inline void psx_triangle_edges_at_y(int x0, int y0, int x1, int y1, + int x2, int y2, int y, + int *long_x, int *short_x) { + int64_t long_step = psx_edge_step(x2 - x0, y2 - y0); + *long_x = psx_edge_unfp(psx_edge_fp(x0) + long_step * (y - y0)); + + if (y < y1) { + int64_t short_step = psx_edge_step(x1 - x0, y1 - y0); + *short_x = psx_edge_unfp(psx_edge_fp(x0) + short_step * (y - y0)); + } else { + int64_t short_step = psx_edge_step(x2 - x1, y2 - y1); + *short_x = psx_edge_unfp(psx_edge_fp(x1) + short_step * (y - y1)); + } +} + +#endif diff --git a/runtime/src/gpu_sw_renderer.c b/runtime/src/gpu_sw_renderer.c index 21b22477..df7b18ac 100644 --- a/runtime/src/gpu_sw_renderer.c +++ b/runtime/src/gpu_sw_renderer.c @@ -28,6 +28,7 @@ */ #include "gpu_sw_renderer.h" +#include "gpu_sw_edges.h" #include #include #include @@ -662,33 +663,18 @@ static void raster_flat_triangle(const RTarget *t, int dy_total = y2 - y0; if (dy_total == 0) return; - for (int y = y0; y <= y2; y++) { + for (int y = y0; y < y2; y++) { if (y < t->cy1 || y > t->cy2) continue; - int second_half = (y >= y1); - int seg_height = second_half ? (y2 - y1) : (y1 - y0); - if (seg_height == 0) seg_height = 1; - - float alpha = (float)(y - y0) / (float)dy_total; - float beta; - if (second_half) - beta = (float)(y - y1) / (float)seg_height; - else - beta = (float)(y - y0) / (float)seg_height; - - int xa = x0 + (int)((float)(x2 - x0) * alpha); - int xb; - if (second_half) - xb = x1 + (int)((float)(x2 - x1) * beta); - else - xb = x0 + (int)((float)(x1 - x0) * beta); + int xa, xb; + psx_triangle_edges_at_y(x0, y0, x1, y1, x2, y2, y, &xa, &xb); if (xa > xb) { int tt = xa; xa = xb; xb = tt; } int sx = max_i(xa, t->cx1); - int ex = min_i(xb, t->cx2); + int ex = min_i(xb, t->cx2 + 1); - for (int x = sx; x <= ex; x++) { + for (int x = sx; x < ex; x++) { put_opaque(t, x, y, color); } } @@ -741,7 +727,7 @@ static void raster_gouraud_triangle(const RTarget *t, int dy_total = y2 - y0; if (dy_total == 0) return; - for (int y = y0; y <= y2; y++) { + for (int y = y0; y < y2; y++) { if (y < t->cy1 || y > t->cy2) continue; int second_half = (y >= y1); @@ -756,12 +742,8 @@ static void raster_gouraud_triangle(const RTarget *t, beta = (float)(y - y0) / (float)seg_height; /* Interpolate X along edges */ - int xa = x0 + (int)((float)(x2 - x0) * alpha); - int xb; - if (second_half) - xb = x1 + (int)((float)(x2 - x1) * beta); - else - xb = x0 + (int)((float)(x1 - x0) * beta); + int xa, xb; + psx_triangle_edges_at_y(x0, y0, x1, y1, x2, y2, y, &xa, &xb); /* Interpolate colors along edges (in 5-bit space) */ int ra = r0 + (int)((float)(r2 - r0) * alpha); @@ -788,10 +770,10 @@ static void raster_gouraud_triangle(const RTarget *t, } int sx = max_i(xa, t->cx1); - int ex = min_i(xb, t->cx2); + int ex = min_i(xb, t->cx2 + 1); int span = xb - xa; - for (int x = sx; x <= ex; x++) { + for (int x = sx; x < ex; x++) { /* Interpolate color across the scanline */ uint16_t color; if (span > 0) { @@ -859,7 +841,7 @@ static void raster_textured_triangle(const RTarget *t, int dy_total = y2 - y0; if (dy_total == 0) return; - for (int y = y0; y <= y2; y++) { + for (int y = y0; y < y2; y++) { if (y < t->cy1 || y > t->cy2) continue; int second_half = (y >= y1); @@ -873,12 +855,8 @@ static void raster_textured_triangle(const RTarget *t, else beta = (float)(y - y0) / (float)seg_height; - int xa = x0 + (int)((float)(x2 - x0) * alpha); - int xb; - if (second_half) - xb = x1 + (int)((float)(x2 - x1) * beta); - else - xb = x0 + (int)((float)(x1 - x0) * beta); + int xa, xb; + psx_triangle_edges_at_y(x0, y0, x1, y1, x2, y2, y, &xa, &xb); float ua = u0 + (float)(u2 - u0) * alpha; float va = v0 + (float)(v2 - v0) * alpha; @@ -902,9 +880,9 @@ static void raster_textured_triangle(const RTarget *t, if (span == 0) span = 1; int sx = max_i(xa, t->cx1); - int ex = min_i(xb, t->cx2); + int ex = min_i(xb, t->cx2 + 1); - for (int x = sx; x <= ex; x++) { + for (int x = sx; x < ex; x++) { float t_val = (float)(x - xa) / (float)span; float fu = ua + (ub - ua) * t_val; float fv = va + (vb - va) * t_val; @@ -990,7 +968,7 @@ static void raster_shaded_textured_triangle(const RTarget *t, int dy_total = y2 - y0; if (dy_total == 0) return; - for (int y = y0; y <= y2; y++) { + for (int y = y0; y < y2; y++) { if (y < t->cy1 || y > t->cy2) continue; int second_half = (y >= y1); @@ -1004,12 +982,8 @@ static void raster_shaded_textured_triangle(const RTarget *t, else beta = (float)(y - y0) / (float)seg_height; - int xa = x0 + (int)((float)(x2 - x0) * alpha); - int xb; - if (second_half) - xb = x1 + (int)((float)(x2 - x1) * beta); - else - xb = x0 + (int)((float)(x1 - x0) * beta); + int xa, xb; + psx_triangle_edges_at_y(x0, y0, x1, y1, x2, y2, y, &xa, &xb); float ua = u0 + (float)(u2 - u0) * alpha; float va = v0 + (float)(v2 - v0) * alpha; @@ -1046,9 +1020,9 @@ static void raster_shaded_textured_triangle(const RTarget *t, if (span == 0) span = 1; int sx = max_i(xa, t->cx1); - int ex = min_i(xb, t->cx2); + int ex = min_i(xb, t->cx2 + 1); - for (int x = sx; x <= ex; x++) { + for (int x = sx; x < ex; x++) { float t_val = (float)(x - xa) / (float)span; float fu = ua + (ub - ua) * t_val; float fv = va + (vb - va) * t_val; diff --git a/runtime/tests/test_gpu_sw_edges.c b/runtime/tests/test_gpu_sw_edges.c new file mode 100644 index 00000000..51db44c2 --- /dev/null +++ b/runtime/tests/test_gpu_sw_edges.c @@ -0,0 +1,28 @@ +#include "../src/gpu_sw_edges.h" + +#include + +static int failures; +#define CHECK(condition, message) do { \ + if (!(condition)) { fprintf(stderr, "FAIL: %s\n", message); failures++; } \ +} while (0) + +int main(void) { + int long_x = 0, short_x = 0; + + /* A right triangle's final scanline is y2-1; y2 itself is excluded by the + * caller. Pin the DDA values used to produce the half-open X span. */ + psx_triangle_edges_at_y(0, 0, 8, 0, 0, 8, 0, &long_x, &short_x); + CHECK(long_x == 0 && short_x == 8, "top scanline spans [0,8)"); + psx_triangle_edges_at_y(0, 0, 8, 0, 0, 8, 7, &long_x, &short_x); + CHECK(long_x == 0 && short_x == 1, "last scanline spans one pixel"); + + CHECK(psx_edge_step(8, 8) == (1LL << 32), + "positive unit slope has exact 32.32 step"); + CHECK(psx_edge_step(-8, 8) == -(1LL << 32), + "negative unit slope has exact 32.32 step"); + + if (failures) return 1; + puts("ALL PASS"); + return 0; +}