Nugget
Bare-metal libraries and examples for the original PlayStation
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
53// clang-format off
54
56
57// EWP1: slope exactly 1/2. (0, 0)-(2, 0)-(0, 4).
58// Right(y): y=0->2.0, y=1->1.5, y=2->1.0, y=3->0.5, y=4->0
59// Slow-path xmax = floor(right) - 1:
60// y=0: xmax=1, span [0,1]
61// y=1: xmax=0 (1.5->floor 1 -> -1=0), span [0,0]
62// y=2: xmax=-1 (1.0->floor 1 -> -1=0... but right is exactly 1)
63// y=3: xmax=-1 (0.5->floor 0 -> -1)
64// Hardware truth captured here. Integer-rightX rows: y=0, y=2, y=4.
65static void drawEWP1(void) {
66 rasterReset();
67 rasterClearTestRegion(0, 0, 8, 8);
68 rasterFlatTri(RASTER_CMD_RED, 0, 0, 2, 0, 0, 4);
69 rasterFlushPrimitive();
70}
71
72// EWP2: SF1 shape (0, 0)-(3, 0)-(0, 9), already tested in phase-3 but
73// re-probed here at MORE rows for edge-walker characterization. Slope
74// 1/3. Integer-rightX rows: y=0 (3.0), y=3 (2.0), y=6 (1.0), y=9 (0).
75// We expect: at y=2, right=2.333, xmax=1, x=0,1 drawn, x=2 sentinel.
76// at y=3, right=2.0, xmax=0, x=0 drawn, x=1,2 sentinel.
77// (Already confirmed in phase-3 but with no x=2 y=3 boundary probe.)
78static void drawEWP2(void) {
79 rasterReset();
80 rasterClearTestRegion(0, 0, 8, 16);
81 rasterFlatTri(RASTER_CMD_GREEN, 0, 0, 3, 0, 0, 9);
82 rasterFlushPrimitive();
83}
84
85// EWP3: position-shift of EWP2. Same shape but origin at (1, 0):
86// (1, 0)-(4, 0)-(1, 9). Tests whether the shifted triangle has
87// identical relative coverage. Edge-walker math should be position-
88// independent for integer offsets (the absolute screen position
89// doesn't enter the slope calc), so phase-3's EWP2 rules should
90// translate to (1..3, 0..8) here.
91static void drawEWP3(void) {
92 rasterReset();
93 rasterClearTestRegion(0, 0, 8, 16);
94 rasterFlatTri(RASTER_CMD_GREEN, 1, 0, 4, 0, 1, 9);
95 rasterFlushPrimitive();
96}
97
98// EWP4: irrational slope 7/9. (0, 0)-(7, 0)-(0, 9). Right(y) at each y:
99// y=0: 7.0 y=5: 3.111
100// y=1: 6.222 y=6: 2.333
101// y=2: 5.444 y=7: 1.556
102// y=3: 4.667 y=8: 0.778
103// y=4: 3.889 y=9: 0
104// Slow-path floor-1 rule:
105// y=0: xmax=6, span [0,6] (boundary: x=6 drawn, x=7 not)
106// y=1: xmax=5, [0..5]
107// y=2: xmax=4
108// y=3: xmax=3
109// y=4: xmax=2
110// y=5: xmax=2
111// y=6: xmax=1
112// y=7: xmax=0
113// y=8: xmax=-1 (right=0.778->floor 0 -> -1)
114// Hardware should KEEP narrow rows post-apex per phase-3 finding.
115static void drawEWP4(void) {
116 rasterReset();
117 rasterClearTestRegion(0, 0, 12, 12);
118 rasterFlatTri(RASTER_CMD_BLUE, 0, 0, 7, 0, 0, 9);
119 rasterFlushPrimitive();
120}
121
122// EWP5: slope 2/5. (0, 0)-(2, 0)-(0, 5). Slow path:
123// y=0: right=2.0, xmax=0 (floor 2 -> -1=1... actually slow is
124// right>>16 - 1, so right=2 -> 2 - 1 = 1)
125// Hmm wait let me recompute. right at y=0 is 2.0, encoded as
126// 2<<16 = 0x20000. right>>16 = 2. xmax = 2 - 1 = 1. Span [0, 1].
127// y=1: right=1.6, encoded 0x19999. right>>16=1, xmax=0. Span [0, 0].
128// y=2: right=1.2, right>>16=1, xmax=0. Span [0, 0].
129// y=3: right=0.8, right>>16=0, xmax=-1. Empty?
130// y=4: right=0.4, similar empty.
131// Per phase-3 hardware truth: narrow rows post-apex are KEPT. So
132// y=3, y=4 likely have x=0 drawn anyway.
133static void drawEWP5(void) {
134 rasterReset();
135 rasterClearTestRegion(0, 0, 8, 8);
136 rasterFlatTri(RASTER_CMD_RED, 0, 0, 2, 0, 0, 5);
137 rasterFlushPrimitive();
138}
139
140// EWP6: tall + narrow with slope 1/N where N is large. (0, 0)-(1, 0)-
141// (0, 15). One-pixel-wide column for the whole height. All 15 rows
142// should fill at x=0 per phase-3 SF2 generalization.
143static void drawEWP6(void) {
144 rasterReset();
145 rasterClearTestRegion(0, 0, 4, 20);
146 rasterFlatTri(RASTER_CMD_WHITE, 0, 0, 1, 0, 0, 15);
147 rasterFlushPrimitive();
148}
149
150// EWP7: slope that produces same right-edge integer values at
151// multiple non-modular rows. (0, 0)-(4, 0)-(0, 16). Slope 1/4.
152// Right(y) at every y: 4, 3.75, 3.5, 3.25, 3, 2.75, 2.5, 2.25, 2,
153// 1.75, 1.5, 1.25, 1, 0.75, 0.5, 0.25, 0
154// Integer-rightX rows: y=0,4,8,12,16. Many integer-boundary cases.
155static void drawEWP7(void) {
156 rasterReset();
157 rasterClearTestRegion(0, 0, 8, 20);
158 rasterFlatTri(RASTER_CMD_GREEN, 0, 0, 4, 0, 0, 16);
159 rasterFlushPrimitive();
160}
161
162) // CESTER_BODY
163
164// --------------------------------------------------------------------------
165// EWP1: slope 1/2
166// --------------------------------------------------------------------------
167
168CESTER_TEST(ewp1_y0_x0, gpu_raster_phase6,
169 drawEWP1();
170 /* y=0, right=2.0, expect x=0 drawn. */
172)
173
174CESTER_TEST(ewp1_y0_x1, gpu_raster_phase6,
175 drawEWP1();
176 /* y=0, right=2.0, slow xmax=1, x=1 drawn. */
178)
179
180CESTER_TEST(ewp1_y0_x2_right_vertex, gpu_raster_phase6,
181 drawEWP1();
182 /* y=0 right vertex - right edge excluded per top-left. */
184)
185
186CESTER_TEST(ewp1_y1_x0, gpu_raster_phase6,
187 drawEWP1();
188 /* y=1, right=1.5, x=0 drawn. */
190)
191
192CESTER_TEST(ewp1_y1_x1_boundary, gpu_raster_phase6,
193 drawEWP1();
194 /* y=1, right=1.5 (fractional). HW_VERIFIED 2026-05-16: hardware
195 INCLUDES x=floor(right)=1 when right is fractional. This is
196 neither legacy fast-path (which decrements unconditionally if
197 xmax>xmin) nor slow-path (which decrements unconditionally).
198 The canonical rule appears to be:
199 xmax = (rightX - 1) >> 16
200 Integer rightX excludes (right-vertex behavior), fractional
201 rightX keeps the floor pixel. */
203)
204
205CESTER_TEST(ewp1_y2_x0_integer_right, gpu_raster_phase6,
206 drawEWP1();
207 /* y=2, right=1.0 EXACTLY (integer-rightX row). Top-left rule
208 excludes the right edge at integer crossings. x=0 drawn,
209 x=1 not. */
211)
212
213CESTER_TEST(ewp1_y2_x1_integer_right, gpu_raster_phase6,
214 drawEWP1();
215 /* y=2, right=1.0 exactly. x=1 IS the integer boundary. */
217)
218
219CESTER_TEST(ewp1_y3_x0_narrow, gpu_raster_phase6,
220 drawEWP1();
221 /* y=3, right=0.5. Per phase-3 narrow-post-apex rule, x=0 likely
222 drawn (kept). */
224)
225
226CESTER_TEST(ewp1_y4_bottom_excluded, gpu_raster_phase6,
227 drawEWP1();
229)
230
231// --------------------------------------------------------------------------
232// EWP3: position-shift of SF1 shape (origin (1, 0))
233// --------------------------------------------------------------------------
234
235CESTER_TEST(ewp3_shifted_y0_x1, gpu_raster_phase6,
236 drawEWP3();
237 /* y=0, x=1 = relative (0,0) of shifted triangle. */
239)
240
241CESTER_TEST(ewp3_shifted_y0_x3, gpu_raster_phase6,
242 drawEWP3();
244)
245
246CESTER_TEST(ewp3_shifted_y0_x4_right_vertex, gpu_raster_phase6,
247 drawEWP3();
249)
250
251CESTER_TEST(ewp3_shifted_y3_x2_integer_right_boundary, gpu_raster_phase6,
252 drawEWP3();
253 /* y=3 of shifted shape = absolute y=3. Right edge at relative
254 x=2, absolute x=3. Integer-rightX row. */
256)
257
258CESTER_TEST(ewp3_shifted_y3_x3_excluded, gpu_raster_phase6,
259 drawEWP3();
261)
262
263// --------------------------------------------------------------------------
264// EWP4: irrational slope 7/9
265// --------------------------------------------------------------------------
266
267CESTER_TEST(ewp4_y0_x6, gpu_raster_phase6,
268 drawEWP4();
269 /* y=0, right=7, span [0..6]. x=6 drawn. */
271)
272
273CESTER_TEST(ewp4_y0_x7_right_vertex, gpu_raster_phase6,
274 drawEWP4();
276)
277
278CESTER_TEST(ewp4_y4_x2_irrational, gpu_raster_phase6,
279 drawEWP4();
280 /* y=4, right=3.889. Slow xmax=2, span [0..2]. x=2 drawn. */
282)
283
284CESTER_TEST(ewp4_y4_x3_boundary, gpu_raster_phase6,
285 drawEWP4();
286 /* y=4, right=3.889 (fractional). HW_VERIFIED: x=3 IS drawn
287 (same canonical xmax = (rightX-1)>>16 rule as EWP1 y=1). */
289)
290
291CESTER_TEST(ewp4_y7_narrow_x0, gpu_raster_phase6,
292 drawEWP4();
293 /* y=7, right=1.556, narrow row, hardware keeps. */
295)
296
297CESTER_TEST(ewp4_y8_narrow_post_apex, gpu_raster_phase6,
298 drawEWP4();
299 /* y=8, right=0.778. Per phase-3, narrow post-apex is KEPT. */
301)
302
303// --------------------------------------------------------------------------
304// EWP5: slope 2/5
305// --------------------------------------------------------------------------
306
307CESTER_TEST(ewp5_y0_x0, gpu_raster_phase6,
308 drawEWP5();
309 /* y=0, right=2.0 exact integer. */
311)
312
313CESTER_TEST(ewp5_y0_x1, gpu_raster_phase6,
314 drawEWP5();
316)
317
318CESTER_TEST(ewp5_y0_x2_right_vertex, gpu_raster_phase6,
319 drawEWP5();
321)
322
323CESTER_TEST(ewp5_y3_narrow_kept, gpu_raster_phase6,
324 drawEWP5();
325 /* y=3, right=0.8, narrow post-apex KEPT. */
327)
328
329CESTER_TEST(ewp5_y4_narrow_kept, gpu_raster_phase6,
330 drawEWP5();
331 /* y=4, right=0.4, even narrower. Still KEPT per phase-3. */
333)
334
335// --------------------------------------------------------------------------
336// EWP6: 1-pixel column, height 15
337// --------------------------------------------------------------------------
338
339CESTER_TEST(ewp6_y0, gpu_raster_phase6,
340 drawEWP6();
342)
343
344CESTER_TEST(ewp6_y7_mid, gpu_raster_phase6,
345 drawEWP6();
347)
348
349CESTER_TEST(ewp6_y14_last_row, gpu_raster_phase6,
350 drawEWP6();
352)
353
354CESTER_TEST(ewp6_y15_bottom_excluded, gpu_raster_phase6,
355 drawEWP6();
357)
358
359CESTER_TEST(ewp6_x1_right_edge_clean, gpu_raster_phase6,
360 drawEWP6();
362)
363
364// --------------------------------------------------------------------------
365// EWP7: slope 1/4, multiple integer-rightX rows (y=0,4,8,12,16)
366// --------------------------------------------------------------------------
367
368CESTER_TEST(ewp7_y0_x3, gpu_raster_phase6,
369 drawEWP7();
370 /* y=0, right=4.0 exact, span [0..3]. */
372)
373
374CESTER_TEST(ewp7_y0_x4_right_vertex, gpu_raster_phase6,
375 drawEWP7();
377)
378
379CESTER_TEST(ewp7_y4_x2_integer_right, gpu_raster_phase6,
380 drawEWP7();
381 /* y=4, right=3.0 EXACT integer. Slow xmax=2. */
383)
384
385CESTER_TEST(ewp7_y4_x3_integer_excluded, gpu_raster_phase6,
386 drawEWP7();
387 /* y=4, right=3.0. x=3 IS the integer boundary - excluded. */
389)
390
391CESTER_TEST(ewp7_y8_x1_integer_right, gpu_raster_phase6,
392 drawEWP7();
393 /* y=8, right=2.0 exact. */
395)
396
397CESTER_TEST(ewp7_y8_x2_excluded, gpu_raster_phase6,
398 drawEWP7();
400)
401
402CESTER_TEST(ewp7_y12_x0_integer_right, gpu_raster_phase6,
403 drawEWP7();
404 /* y=12, right=1.0 exact. xmax=0. */
406)
407
408CESTER_TEST(ewp7_y12_x1_excluded, gpu_raster_phase6,
409 drawEWP7();
411)
412
413CESTER_TEST(ewp7_y14_narrow_x0_kept, gpu_raster_phase6,
414 drawEWP7();
415 /* y=14, right=0.5. Narrow post-apex KEPT. */
417)
418
419CESTER_TEST(ewp7_y15_narrow_x0_kept, gpu_raster_phase6,
420 drawEWP7();
421 /* y=15, right=0.25. Even narrower, still KEPT per phase-3. */
423)
424
425CESTER_TEST(ewp7_y16_bottom_excluded, gpu_raster_phase6,
426 drawEWP7();
428)
CESTER_TEST(cddaLevelHalf, test_instances, static const uint8_t atv[4]={0x40, 0x00, 0x40, 0x00};unsigned changes=0;int peak=cddaPeak(atv, 1, &changes);ramsyscall_printf("CD-DA level at half ATV: peak %i, %u sign changes\n", peak, changes);cester_assert_int_ge(peak, 11300);cester_assert_int_le(peak, 11800);cester_assert_uint_ge(changes, 4);) CESTER_TEST(cddaLevelClipped
CESTER_BODY(static uint16_t s_cddaCapture;static int cddaSpuDma(uint32_t spuAddr, void *buf, uint32_t bytes, int isRead) { const uint16_t tsa=spuAddr > > 3;const uint16_t mode=isRead ? 0x30 :0x20;SPU_RAM_DTA=tsa;for(int i=0;i< 0xf01 &&SPU_RAM_DTA !=tsa;i++);SPU_CTRL=(SPU_CTRL &~0x30)|mode;for(volatile int i=0;i< 60;i++);for(int i=0;i< 0xf01 &&(SPU_STATUS &0x30) !=mode;i++);CDDA_SPU_DELAY=(CDDA_SPU_DELAY &0xf0ffffff)|(isRead ? 0x22000000 :0x20000000);DMA_CTRL[DMA_SPU].MADR=(uint32_t) buf &0x1fffffff;DMA_CTRL[DMA_SPU].BCR=((bytes > > 6)<< 16)|0x10;DMA_CTRL[DMA_SPU].CHCR=isRead ? 0x01000200 :0x01000201;int ok=0;for(unsigned t=0;t< 4000000u;t++) { if((DMA_CTRL[DMA_SPU].CHCR &0x01000000)==0) { ok=1;break;} } SPU_CTRL=SPU_CTRL &~0x30;for(volatile int i=0;i< 60;i++);return ok;} static int cddaSimpleCmd(uint8_t cmd) { uint8_t response[16];CDROM_REG0=0;CDROM_REG1=cmd;uint32_t timeout=2000000;int ok=waitCDRomIRQWithTimeout(&timeout);ackCDRomCause();readResponse(response);return ok;} static void cddaWaitPauseComplete() { initializeTime();uint32_t elapsed=0;while(elapsed< 5000000u) { uint32_t budget=200000u;int ok=waitCDRomIRQWithTimeout(&budget);elapsed=budget;if(!ok) continue;uint8_t cause=ackCDRomCause();uint8_t response[16];readResponse(response);if(cause==2) return;} } static int cddaAudioSetup(const uint8_t atv[4], int demute) { if(!resetCDRom()) return 0;DPCR|=0x000b0000;SPU_CTRL=0;for(volatile int i=0;i< 20000;i++);SPU_VOL_MAIN_LEFT=0;SPU_VOL_MAIN_RIGHT=0;SPU_KEY_OFF_LOW=0xffff;SPU_KEY_OFF_HIGH=0xffff;SPU_RAM_DTC=4;SPU_CTRL=0x8001;for(volatile int i=0;i< 20000;i++);for(int i=0;i< 0x400;i++) s_cddaCapture[i]=CDDA_MARKER;if(!cddaSpuDma(0x000, s_cddaCapture, 0x800, 0)) return 0;if(!cddaSimpleCmd(demute ? CDL_DEMUTE :CDL_MUTE)) return 0;CDROM_REG0=2;CDROM_REG2=atv[0];CDROM_REG3=atv[1];CDROM_REG0=3;CDROM_REG1=atv[2];CDROM_REG2=atv[3];CDROM_REG0=3;CDROM_REG3=0x20;CDROM_REG0=0;SPU_VOL_CD_LEFT=0x7fff;SPU_VOL_CD_RIGHT=0x7fff;SPU_CTRL=SPU_CTRL|0x8001;return 1;} static int cddaBufferPeak(unsigned *signChanges) { int peak=0;unsigned changes=0;int prev=0;for(int i=0;i< 0x200;i++) { int v=(int16_t) s_cddaCapture[i];int a=v< 0 ? -v :v;if(a > peak) peak=a;int sign=v >=0 ? 1 :-1;if(i &&sign !=prev) changes++;prev=sign;} *signChanges=changes;return peak;} static int cddaCapturePeak(unsigned *signChanges) { int peak=0;for(unsigned attempt=0;attempt< 8;attempt++) { if(attempt) { initializeTime();while(updateTime()< 250000u);} if(!cddaSpuDma(0x000, s_cddaCapture, 0x800, 1)) return -1;peak=cddaBufferPeak(signChanges);if(peak) { if(attempt) ramsyscall_printf("capture was empty %u time(s) before audio\n", attempt);break;} } return peak;} static int cddaPeak(const uint8_t atv[4], int demute, unsigned *signChanges) { if(!cddaAudioSetup(atv, demute)) return -1;if(!setMode(0)) return -1;CDROM_REG0=0;CDROM_REG2=0x02;if(!cddaSimpleCmd(CDL_PLAY)) return -1;initializeTime();while(updateTime()< 1500000u);int peak=cddaCapturePeak(signChanges);CDROM_REG0=0;CDROM_REG1=CDL_PAUSE;cddaWaitPauseComplete();CDROM_REG0=0;CDROM_REG2=0x00;CDROM_REG2=0x02;CDROM_REG2=0x16;if(cddaSimpleCmd(CDL_SETLOC) &&cddaSimpleCmd(CDL_SEEKL)) cddaWaitPauseComplete();return peak;}[0x400])
Definition cdda.c:37
#define RASTER_VRAM_GREEN
Definition raster-helpers.h:127
#define RASTER_VRAM_BLUE
Definition raster-helpers.h:129
#define RASTER_VRAM_WHITE
Definition raster-helpers.h:131
#define ASSERT_PIXEL_UNTOUCHED(x_, y_)
Definition raster-helpers.h:486
#define RASTER_VRAM_RED
Definition raster-helpers.h:125
#define RASTER_CMD_BLUE
Definition raster-helpers.h:128
#define RASTER_CMD_RED
Definition raster-helpers.h:124
#define RASTER_CMD_WHITE
Definition raster-helpers.h:130
#define RASTER_CMD_GREEN
Definition raster-helpers.h:126
#define ASSERT_PIXEL_EQ(expected, x_, y_)
Definition raster-helpers.h:473