Nugget
Loading...
Searching...
No Matches
edge-walker-precision.c
Go to the documentation of this file.
1/*
2
3MIT License
4
5Copyright (c) 2026 PCSX-Redux authors
6
7Permission is hereby granted, free of charge, to any person obtaining a copy
8of this software and associated documentation files (the "Software"), to deal
9in the Software without restriction, including without limitation the rights
10to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11copies of the Software, and to permit persons to whom the Software is
12furnished to do so, subject to the following conditions:
13
14The above copyright notice and this permission notice shall be included in all
15copies or substantial portions of the Software.
16
17THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23SOFTWARE.
24
25*/
26
27// Edge-walker integer-division precision oracle. PS1 vertices are
28// 11-bit integer - there is no fractional-coord input. The question
29// this suite probes is what happens INSIDE the rasterizer when its
30// per-row right-edge step accumulates 16.16 truncation across rows.
31//
32// soft.cc's `setupSectionsFlat3` (and family) sets:
33//
34// m_deltaRightX = ((v2->x - v1->x) << 16) / height;
35//
36// at 16.16 fixed-point. When (v2.x - v1.x) doesn't divide height
37// cleanly, the division truncates - and the truncation accumulates
38// per row. At rows where the *true* rightX is exactly an integer,
39// the accumulated truncation determines whether the next-row's edge
40// is "just below" or "just at/above" the integer, which decides
41// whether pixel x=integer is INSIDE or OUTSIDE the span under
42// top-left rule.
43//
44// This suite probes configurations where the integer-rightX boundary
45// hits multiple rows, plus integer-shifted variants of the same
46// shape, to characterize hardware's actual rounding behavior. The
47// refactor's fix (whether Bresenham-style integer accumulation or
48// extended fixed-point) needs this oracle to verify correct
49// convergence.
50//
51// Naming: EWP<n> = Edge-Walker Precision configuration n.
52
54
55// EWP1: slope exactly 1/2. (0, 0)-(2, 0)-(0, 4).
56// Right(y): y=0->2.0, y=1->1.5, y=2->1.0, y=3->0.5, y=4->0
57// Slow-path xmax = floor(right) - 1:
58// y=0: xmax=1, span [0,1]
59// y=1: xmax=0 (1.5->floor 1 -> -1=0), span [0,0]
60// y=2: xmax=-1 (1.0->floor 1 -> -1=0... but right is exactly 1)
61// y=3: xmax=-1 (0.5->floor 0 -> -1)
62// Hardware truth captured here. Integer-rightX rows: y=0, y=2, y=4.
63static void drawEWP1(void) {
64 rasterReset();
65 rasterClearTestRegion(0, 0, 8, 8);
66 rasterFlatTri(RASTER_CMD_RED, 0, 0, 2, 0, 0, 4);
67 rasterFlushPrimitive();
68}
69
70// EWP2: SF1 shape (0, 0)-(3, 0)-(0, 9), already tested in phase-3 but
71// re-probed here at MORE rows for edge-walker characterization. Slope
72// 1/3. Integer-rightX rows: y=0 (3.0), y=3 (2.0), y=6 (1.0), y=9 (0).
73// We expect: at y=2, right=2.333, xmax=1, x=0,1 drawn, x=2 sentinel.
74// at y=3, right=2.0, xmax=0, x=0 drawn, x=1,2 sentinel.
75// (Already confirmed in phase-3 but with no x=2 y=3 boundary probe.)
76static void drawEWP2(void) {
77 rasterReset();
78 rasterClearTestRegion(0, 0, 8, 16);
79 rasterFlatTri(RASTER_CMD_GREEN, 0, 0, 3, 0, 0, 9);
80 rasterFlushPrimitive();
81}
82
83// EWP3: position-shift of EWP2. Same shape but origin at (1, 0):
84// (1, 0)-(4, 0)-(1, 9). Tests whether the shifted triangle has
85// identical relative coverage. Edge-walker math should be position-
86// independent for integer offsets (the absolute screen position
87// doesn't enter the slope calc), so phase-3's EWP2 rules should
88// translate to (1..3, 0..8) here.
89static void drawEWP3(void) {
90 rasterReset();
91 rasterClearTestRegion(0, 0, 8, 16);
92 rasterFlatTri(RASTER_CMD_GREEN, 1, 0, 4, 0, 1, 9);
93 rasterFlushPrimitive();
94}
95
96// EWP4: irrational slope 7/9. (0, 0)-(7, 0)-(0, 9). Right(y) at each y:
97// y=0: 7.0 y=5: 3.111
98// y=1: 6.222 y=6: 2.333
99// y=2: 5.444 y=7: 1.556
100// y=3: 4.667 y=8: 0.778
101// y=4: 3.889 y=9: 0
102// Slow-path floor-1 rule:
103// y=0: xmax=6, span [0,6] (boundary: x=6 drawn, x=7 not)
104// y=1: xmax=5, [0..5]
105// y=2: xmax=4
106// y=3: xmax=3
107// y=4: xmax=2
108// y=5: xmax=2
109// y=6: xmax=1
110// y=7: xmax=0
111// y=8: xmax=-1 (right=0.778->floor 0 -> -1)
112// Hardware should KEEP narrow rows post-apex per phase-3 finding.
113static void drawEWP4(void) {
114 rasterReset();
115 rasterClearTestRegion(0, 0, 12, 12);
116 rasterFlatTri(RASTER_CMD_BLUE, 0, 0, 7, 0, 0, 9);
117 rasterFlushPrimitive();
118}
119
120// EWP5: slope 2/5. (0, 0)-(2, 0)-(0, 5). Slow path:
121// y=0: right=2.0, xmax=0 (floor 2 -> -1=1... actually slow is
122// right>>16 - 1, so right=2 -> 2 - 1 = 1)
123// Hmm wait let me recompute. right at y=0 is 2.0, encoded as
124// 2<<16 = 0x20000. right>>16 = 2. xmax = 2 - 1 = 1. Span [0, 1].
125// y=1: right=1.6, encoded 0x19999. right>>16=1, xmax=0. Span [0, 0].
126// y=2: right=1.2, right>>16=1, xmax=0. Span [0, 0].
127// y=3: right=0.8, right>>16=0, xmax=-1. Empty?
128// y=4: right=0.4, similar empty.
129// Per phase-3 hardware truth: narrow rows post-apex are KEPT. So
130// y=3, y=4 likely have x=0 drawn anyway.
131static void drawEWP5(void) {
132 rasterReset();
133 rasterClearTestRegion(0, 0, 8, 8);
134 rasterFlatTri(RASTER_CMD_RED, 0, 0, 2, 0, 0, 5);
135 rasterFlushPrimitive();
136}
137
138// EWP6: tall + narrow with slope 1/N where N is large. (0, 0)-(1, 0)-
139// (0, 15). One-pixel-wide column for the whole height. All 15 rows
140// should fill at x=0 per phase-3 SF2 generalization.
141static void drawEWP6(void) {
142 rasterReset();
143 rasterClearTestRegion(0, 0, 4, 20);
144 rasterFlatTri(RASTER_CMD_WHITE, 0, 0, 1, 0, 0, 15);
145 rasterFlushPrimitive();
146}
147
148// EWP7: slope that produces same right-edge integer values at
149// multiple non-modular rows. (0, 0)-(4, 0)-(0, 16). Slope 1/4.
150// Right(y) at every y: 4, 3.75, 3.5, 3.25, 3, 2.75, 2.5, 2.25, 2,
151// 1.75, 1.5, 1.25, 1, 0.75, 0.5, 0.25, 0
152// Integer-rightX rows: y=0,4,8,12,16. Many integer-boundary cases.
153static void drawEWP7(void) {
154 rasterReset();
155 rasterClearTestRegion(0, 0, 8, 20);
156 rasterFlatTri(RASTER_CMD_GREEN, 0, 0, 4, 0, 0, 16);
157 rasterFlushPrimitive();
158}
159
160) // CESTER_BODY
161
162// --------------------------------------------------------------------------
163// EWP1: slope 1/2
164// --------------------------------------------------------------------------
165
166CESTER_TEST(ewp1_y0_x0, gpu_raster_phase6,
167 drawEWP1();
168 /* y=0, right=2.0, expect x=0 drawn. */
170)
171
172CESTER_TEST(ewp1_y0_x1, gpu_raster_phase6,
173 drawEWP1();
174 /* y=0, right=2.0, slow xmax=1, x=1 drawn. */
176)
177
178CESTER_TEST(ewp1_y0_x2_right_vertex, gpu_raster_phase6,
179 drawEWP1();
180 /* y=0 right vertex - right edge excluded per top-left. */
182)
183
184CESTER_TEST(ewp1_y1_x0, gpu_raster_phase6,
185 drawEWP1();
186 /* y=1, right=1.5, x=0 drawn. */
188)
189
190CESTER_TEST(ewp1_y1_x1_boundary, gpu_raster_phase6,
191 drawEWP1();
192 /* y=1, right=1.5 (fractional). HW_VERIFIED 2026-05-16: hardware
193 INCLUDES x=floor(right)=1 when right is fractional. This is
194 neither legacy fast-path (which decrements unconditionally if
195 xmax>xmin) nor slow-path (which decrements unconditionally).
196 The canonical rule appears to be:
197 xmax = (rightX - 1) >> 16
198 Integer rightX excludes (right-vertex behavior), fractional
199 rightX keeps the floor pixel. */
201)
202
203CESTER_TEST(ewp1_y2_x0_integer_right, gpu_raster_phase6,
204 drawEWP1();
205 /* y=2, right=1.0 EXACTLY (integer-rightX row). Top-left rule
206 excludes the right edge at integer crossings. x=0 drawn,
207 x=1 not. */
209)
210
211CESTER_TEST(ewp1_y2_x1_integer_right, gpu_raster_phase6,
212 drawEWP1();
213 /* y=2, right=1.0 exactly. x=1 IS the integer boundary. */
215)
216
217CESTER_TEST(ewp1_y3_x0_narrow, gpu_raster_phase6,
218 drawEWP1();
219 /* y=3, right=0.5. Per phase-3 narrow-post-apex rule, x=0 likely
220 drawn (kept). */
222)
223
224CESTER_TEST(ewp1_y4_bottom_excluded, gpu_raster_phase6,
225 drawEWP1();
227)
228
229// --------------------------------------------------------------------------
230// EWP3: position-shift of SF1 shape (origin (1, 0))
231// --------------------------------------------------------------------------
232
233CESTER_TEST(ewp3_shifted_y0_x1, gpu_raster_phase6,
234 drawEWP3();
235 /* y=0, x=1 = relative (0,0) of shifted triangle. */
237)
238
239CESTER_TEST(ewp3_shifted_y0_x3, gpu_raster_phase6,
240 drawEWP3();
242)
243
244CESTER_TEST(ewp3_shifted_y0_x4_right_vertex, gpu_raster_phase6,
245 drawEWP3();
247)
248
249CESTER_TEST(ewp3_shifted_y3_x2_integer_right_boundary, gpu_raster_phase6,
250 drawEWP3();
251 /* y=3 of shifted shape = absolute y=3. Right edge at relative
252 x=2, absolute x=3. Integer-rightX row. */
254)
255
256CESTER_TEST(ewp3_shifted_y3_x3_excluded, gpu_raster_phase6,
257 drawEWP3();
259)
260
261// --------------------------------------------------------------------------
262// EWP4: irrational slope 7/9
263// --------------------------------------------------------------------------
264
265CESTER_TEST(ewp4_y0_x6, gpu_raster_phase6,
266 drawEWP4();
267 /* y=0, right=7, span [0..6]. x=6 drawn. */
269)
270
271CESTER_TEST(ewp4_y0_x7_right_vertex, gpu_raster_phase6,
272 drawEWP4();
274)
275
276CESTER_TEST(ewp4_y4_x2_irrational, gpu_raster_phase6,
277 drawEWP4();
278 /* y=4, right=3.889. Slow xmax=2, span [0..2]. x=2 drawn. */
280)
281
282CESTER_TEST(ewp4_y4_x3_boundary, gpu_raster_phase6,
283 drawEWP4();
284 /* y=4, right=3.889 (fractional). HW_VERIFIED: x=3 IS drawn
285 (same canonical xmax = (rightX-1)>>16 rule as EWP1 y=1). */
287)
288
289CESTER_TEST(ewp4_y7_narrow_x0, gpu_raster_phase6,
290 drawEWP4();
291 /* y=7, right=1.556, narrow row, hardware keeps. */
293)
294
295CESTER_TEST(ewp4_y8_narrow_post_apex, gpu_raster_phase6,
296 drawEWP4();
297 /* y=8, right=0.778. Per phase-3, narrow post-apex is KEPT. */
299)
300
301// --------------------------------------------------------------------------
302// EWP5: slope 2/5
303// --------------------------------------------------------------------------
304
305CESTER_TEST(ewp5_y0_x0, gpu_raster_phase6,
306 drawEWP5();
307 /* y=0, right=2.0 exact integer. */
309)
310
311CESTER_TEST(ewp5_y0_x1, gpu_raster_phase6,
312 drawEWP5();
314)
315
316CESTER_TEST(ewp5_y0_x2_right_vertex, gpu_raster_phase6,
317 drawEWP5();
319)
320
321CESTER_TEST(ewp5_y3_narrow_kept, gpu_raster_phase6,
322 drawEWP5();
323 /* y=3, right=0.8, narrow post-apex KEPT. */
325)
326
327CESTER_TEST(ewp5_y4_narrow_kept, gpu_raster_phase6,
328 drawEWP5();
329 /* y=4, right=0.4, even narrower. Still KEPT per phase-3. */
331)
332
333// --------------------------------------------------------------------------
334// EWP6: 1-pixel column, height 15
335// --------------------------------------------------------------------------
336
337CESTER_TEST(ewp6_y0, gpu_raster_phase6,
338 drawEWP6();
340)
341
342CESTER_TEST(ewp6_y7_mid, gpu_raster_phase6,
343 drawEWP6();
345)
346
347CESTER_TEST(ewp6_y14_last_row, gpu_raster_phase6,
348 drawEWP6();
350)
351
352CESTER_TEST(ewp6_y15_bottom_excluded, gpu_raster_phase6,
353 drawEWP6();
355)
356
357CESTER_TEST(ewp6_x1_right_edge_clean, gpu_raster_phase6,
358 drawEWP6();
360)
361
362// --------------------------------------------------------------------------
363// EWP7: slope 1/4, multiple integer-rightX rows (y=0,4,8,12,16)
364// --------------------------------------------------------------------------
365
366CESTER_TEST(ewp7_y0_x3, gpu_raster_phase6,
367 drawEWP7();
368 /* y=0, right=4.0 exact, span [0..3]. */
370)
371
372CESTER_TEST(ewp7_y0_x4_right_vertex, gpu_raster_phase6,
373 drawEWP7();
375)
376
377CESTER_TEST(ewp7_y4_x2_integer_right, gpu_raster_phase6,
378 drawEWP7();
379 /* y=4, right=3.0 EXACT integer. Slow xmax=2. */
381)
382
383CESTER_TEST(ewp7_y4_x3_integer_excluded, gpu_raster_phase6,
384 drawEWP7();
385 /* y=4, right=3.0. x=3 IS the integer boundary - excluded. */
387)
388
389CESTER_TEST(ewp7_y8_x1_integer_right, gpu_raster_phase6,
390 drawEWP7();
391 /* y=8, right=2.0 exact. */
393)
394
395CESTER_TEST(ewp7_y8_x2_excluded, gpu_raster_phase6,
396 drawEWP7();
398)
399
400CESTER_TEST(ewp7_y12_x0_integer_right, gpu_raster_phase6,
401 drawEWP7();
402 /* y=12, right=1.0 exact. xmax=0. */
404)
405
406CESTER_TEST(ewp7_y12_x1_excluded, gpu_raster_phase6,
407 drawEWP7();
409)
410
411CESTER_TEST(ewp7_y14_narrow_x0_kept, gpu_raster_phase6,
412 drawEWP7();
413 /* y=14, right=0.5. Narrow post-apex KEPT. */
415)
416
417CESTER_TEST(ewp7_y15_narrow_x0_kept, gpu_raster_phase6,
418 drawEWP7();
419 /* y=15, right=0.25. Even narrower, still KEPT per phase-3. */
421)
422
423CESTER_TEST(ewp7_y16_bottom_excluded, gpu_raster_phase6,
424 drawEWP7();
426)
CESTER_BODY(static int s_got40;static int s_got80;static uint32_t s_cause;static uint32_t s_epc;static uint32_t s_from;static uint32_t *s_resume;static uint32_t *s_regs;static uint32_t(*s_customhandler)()=NULL;static uint32_t s_oldIMASK;static uint32_t s_oldDPCR;static uint32_t s_oldDICR;uint32_t handler(uint32_t *regs, uint32_t from) { if(from==0x40) s_got40=1;if(from==0x80) s_got80=1;uint32_t cause;uint32_t epc;s_from=from;asm("mfc0 %0, $13\nnop\nmfc0 %1, $14\nnop" :"=r"(cause), "=r"(epc));s_cause=cause;s_epc=epc;if(s_customhandler) { return s_customhandler();} else { return s_resume ?((uint32_t) s_resume) :(epc+4);} } void installExceptionHandlers(uint32_t(*handler)(uint32_t *regs, uint32_t from));void uninstallExceptionHandlers();uint32_t branchbranch1();uint32_t branchbranch2();uint32_t jumpjump1();uint32_t jumpjump2();uint32_t cpu_LWR_LWL_half(uint32_t buff[], uint32_t initial);uint32_t cpu_LWR_LWL_nodelay(uint32_t buff[], uint32_t initial);uint32_t cpu_LWR_LWL_delayed(uint32_t buff[], uint32_t initial);uint32_t cpu_LWR_LWL_load_different(uint32_t buff[], uint32_t initial);uint32_t cpu_LW_LWR(uint32_t buff[], uint32_t initial);uint32_t cpu_delayed_load(uint32_t buff[], uint32_t override);uint32_t cpu_delayed_load_cancelled(uint32_t buff[], uint32_t override);uint64_t cpu_delayed_load_load(uint32_t buff[], uint32_t override);uint32_t linkandload();uint32_t lwandlink();uint32_t nolink();static int s_interruptsWereEnabled;) CESTER_BEFORE_EACH(cpu_tests
CESTER_TEST(cpu_cop0_basic_write_bp, cpu_tests, uint32_t expectedEPC;uint32_t t;volatile uint32_t *ptr=(volatile uint32_t *) 0x58; *ptr=1;__asm__ volatile("" " lui %0, 0b1100101010000000\n" " mtc0 %0, $7\n" " li %0, 0x58\n" " mtc0 %0, $5\n" " li %0, 0xfffffff0\n" " mtc0 %0, $9\n" :"=r"(t));cester_assert_uint_eq(1, *ptr);__asm__ volatile("la %0, 1f\n1:\nsw $0, 0x58($0)" :"=r"(expectedEPC));__asm__ volatile("mtc0 $0, $7\n");cester_assert_uint_eq(0, *ptr);cester_assert_uint_eq(1, s_got40);cester_assert_uint_eq(0, s_got80);cester_assert_uint_eq(0x40, s_from);cester_assert_uint_eq(expectedEPC, s_epc);) CESTER_TEST(cpu_cop0_kseg_write_bp
#define RASTER_VRAM_GREEN
Definition raster-helpers.h:126
#define RASTER_VRAM_BLUE
Definition raster-helpers.h:128
#define RASTER_VRAM_WHITE
Definition raster-helpers.h:130
#define ASSERT_PIXEL_UNTOUCHED(x_, y_)
Definition raster-helpers.h:485
#define RASTER_VRAM_RED
Definition raster-helpers.h:124
#define RASTER_CMD_BLUE
Definition raster-helpers.h:127
#define RASTER_CMD_RED
Definition raster-helpers.h:123
#define RASTER_CMD_WHITE
Definition raster-helpers.h:129
#define RASTER_CMD_GREEN
Definition raster-helpers.h:125
#define ASSERT_PIXEL_EQ(expected, x_, y_)
Definition raster-helpers.h:472