Nugget
Loading...
Searching...
No Matches
raster-expected-phase11.h
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#pragma once
28
29// Phase-11 hardware-truth values for dither characterization. All
30// HW_VERIFIED on SCPH-5501.
31//
32// Key findings:
33//
34// 1. The PS1 4x4 Bayer dither matrix matches the psx-spx
35// documentation exactly. At pixel (sx, sy) the offset is:
36// offset[(sx & 3)][(sy & 3)] (or (sy & 3, sx & 3) - see below)
37// Pattern (rows = y mod 4, cols = x mod 4):
38// -4 +0 -3 +1
39// +2 -2 +3 -1
40// -3 +1 -4 +0
41// +3 -1 +2 -2
42// Each offset is an 8-bit-space addition applied BEFORE the 5-bit
43// truncation. So at a base 8-bit value that sits on a 5-bit
44// boundary (multiples of 8: 0, 8, 16, ..., 248), only the
45// negative-offset cells cross to the next-lower 5-bit value -
46// producing a 2x2 alternating checkerboard at those bases.
47// At 5-bit-fractional bases the pattern is richer.
48//
49// 2. Dither is SCREEN-SPACE-ANCHORED. The triangle origin doesn't
50// change which Bayer cell covers a given screen pixel. Four
51// triangles at origins (0,0), (0,4), (4,0), (4,4) all probed at
52// screen (16, 16) - the three that draw the pixel all read the
53// same dithered value. (The first triangle's (16, 16) lies
54// OUTSIDE its bounding hypotenuse and reads sentinel.)
55//
56// 3. Dither is channel-independent. Single-channel constant tris
57// (R=128 only, G=128 only, B=128 only) all produce the same per-
58// cell offset pattern at the cells, just expressed in the channel
59// that varied. The dither table applies the same offset to R, G,
60// and B simultaneously.
61//
62// 4. Saturation at chosen test cells did NOT surface. The cells
63// probed under R=4 all had offsets in {-4, +0, -3, +1} which all
64// truncate to R5=0 (input <= 5). Same at R=252 - all offsets
65// land within the 248..255 range that truncates to R5=31. Phase-
66// 12 (ABR) or a follow-up phase-N can revisit saturation with
67// base values that put exactly one cell across the truncation
68// boundary in both directions.
69
70#include "raster-helpers.h"
71
72#define DT_NOMINAL_MID 0x4210u
73#define DT_DITHER_MID 0x3defu /* R=15, G=15, B=15 = mid-gray with -1 LSB */
74
75// ============================================================================
76// DT_BAYER_MID: 16 cells at (sx, sy) = (8..11, 8..11). cx = sx mod 4,
77// cy = sy mod 4 since sx mod 4 = (sx - 8) here. The captured pattern
78// alternates 0x3def (cells with -ve Bayer offset) and 0x4210 (cells
79// with non-negative offset).
80// ============================================================================
81
82#define DT_BAYER_MID_8_8 DT_DITHER_MID /* cell(0,0) -4 -> 15 */
83#define DT_BAYER_MID_9_8 DT_NOMINAL_MID /* cell(1,0) +0 -> 16 */
84#define DT_BAYER_MID_10_8 DT_DITHER_MID /* cell(2,0) -3 -> 15 */
85#define DT_BAYER_MID_11_8 DT_NOMINAL_MID /* cell(3,0) +1 -> 16 */
86#define DT_BAYER_MID_8_9 DT_NOMINAL_MID /* cell(0,1) +2 -> 16 */
87#define DT_BAYER_MID_9_9 DT_DITHER_MID /* cell(1,1) -2 -> 15 */
88#define DT_BAYER_MID_10_9 DT_NOMINAL_MID /* cell(2,1) +3 -> 16 */
89#define DT_BAYER_MID_11_9 DT_DITHER_MID /* cell(3,1) -1 -> 15 */
90#define DT_BAYER_MID_8_10 DT_DITHER_MID /* cell(0,2) -3 -> 15 */
91#define DT_BAYER_MID_9_10 DT_NOMINAL_MID /* cell(1,2) +1 -> 16 */
92#define DT_BAYER_MID_10_10 DT_DITHER_MID /* cell(2,2) -4 -> 15 */
93#define DT_BAYER_MID_11_10 DT_NOMINAL_MID /* cell(3,2) +0 -> 16 */
94#define DT_BAYER_MID_8_11 DT_NOMINAL_MID /* cell(0,3) +3 -> 16 */
95#define DT_BAYER_MID_9_11 DT_DITHER_MID /* cell(1,3) -1 -> 15 */
96#define DT_BAYER_MID_10_11 DT_NOMINAL_MID /* cell(2,3) +2 -> 16 */
97#define DT_BAYER_MID_11_11 DT_DITHER_MID /* cell(3,3) -2 -> 15 */
98
99// ============================================================================
100// DT_BASE_R: R-only base sweep at R=0x40, 0x80, 0xC0. All three are
101// multiples of 8 so they sit on 5-bit boundaries; same alternating
102// pattern as mid-gray, just at lower / mid / higher 5-bit values.
103// Confirms dither offsets are ADDITIVE (independent of base).
104// ============================================================================
105
106#define DT_BASE_R40_8_8 0x0007u /* R5=7 at -ve cells */
107#define DT_BASE_R40_9_8 0x0008u /* R5=8 at non-neg cells */
108#define DT_BASE_R40_10_8 0x0007u
109#define DT_BASE_R40_11_8 0x0008u
110#define DT_BASE_R40_8_9 0x0008u
111#define DT_BASE_R40_9_9 0x0007u
112#define DT_BASE_R40_10_9 0x0008u
113#define DT_BASE_R40_11_9 0x0007u
114#define DT_BASE_R40_8_10 0x0007u
115#define DT_BASE_R40_9_10 0x0008u
116#define DT_BASE_R40_10_10 0x0007u
117#define DT_BASE_R40_11_10 0x0008u
118#define DT_BASE_R40_8_11 0x0008u
119#define DT_BASE_R40_9_11 0x0007u
120#define DT_BASE_R40_10_11 0x0008u
121#define DT_BASE_R40_11_11 0x0007u
122
123#define DT_BASE_R80_8_8 0x000fu /* R5=15 at -ve cells */
124#define DT_BASE_R80_9_8 0x0010u /* R5=16 at non-neg cells */
125#define DT_BASE_R80_10_8 0x000fu
126#define DT_BASE_R80_11_8 0x0010u
127#define DT_BASE_R80_8_9 0x0010u
128#define DT_BASE_R80_9_9 0x000fu
129#define DT_BASE_R80_10_9 0x0010u
130#define DT_BASE_R80_11_9 0x000fu
131#define DT_BASE_R80_8_10 0x000fu
132#define DT_BASE_R80_9_10 0x0010u
133#define DT_BASE_R80_10_10 0x000fu
134#define DT_BASE_R80_11_10 0x0010u
135#define DT_BASE_R80_8_11 0x0010u
136#define DT_BASE_R80_9_11 0x000fu
137#define DT_BASE_R80_10_11 0x0010u
138#define DT_BASE_R80_11_11 0x000fu
139
140#define DT_BASE_RC0_8_8 0x0017u /* R5=23 at -ve cells */
141#define DT_BASE_RC0_9_8 0x0018u /* R5=24 at non-neg cells */
142#define DT_BASE_RC0_10_8 0x0017u
143#define DT_BASE_RC0_11_8 0x0018u
144#define DT_BASE_RC0_8_9 0x0018u
145#define DT_BASE_RC0_9_9 0x0017u
146#define DT_BASE_RC0_10_9 0x0018u
147#define DT_BASE_RC0_11_9 0x0017u
148#define DT_BASE_RC0_8_10 0x0017u
149#define DT_BASE_RC0_9_10 0x0018u
150#define DT_BASE_RC0_10_10 0x0017u
151#define DT_BASE_RC0_11_10 0x0018u
152#define DT_BASE_RC0_8_11 0x0018u
153#define DT_BASE_RC0_9_11 0x0017u
154#define DT_BASE_RC0_10_11 0x0018u
155#define DT_BASE_RC0_11_11 0x0017u
156
157// ============================================================================
158// DT_CHAN_G / DT_CHAN_B: channel-independence verification.
159// G nominal: 0x0010<<5 = 0x0200, dithered: 0x000f<<5 = 0x01e0.
160// B nominal: 0x0010<<10 = 0x4000, dithered: 0x000f<<10 = 0x3c00.
161// ============================================================================
162
163#define DT_CHAN_G80_8_8 0x01e0u /* -ve cell */
164#define DT_CHAN_G80_9_8 0x0200u /* non-neg cell */
165#define DT_CHAN_G80_10_8 0x01e0u
166#define DT_CHAN_G80_11_8 0x0200u
167
168#define DT_CHAN_B80_8_8 0x3c00u /* -ve cell */
169#define DT_CHAN_B80_9_8 0x4000u /* non-neg cell */
170#define DT_CHAN_B80_10_8 0x3c00u
171#define DT_CHAN_B80_11_8 0x4000u
172
173// ============================================================================
174// DT_POS: position sweep. Probe screen-space (16, 16) across four
175// triangle origins. (16, 16) is cell (0, 0) - Bayer offset -4 -> 15
176// LSB on each channel -> 0x3def.
177//
178// The (0, 0) triangle's bounding hypotenuse runs from (31, 0) to
179// (0, 31); the point (16, 16) lies on x+y=32 which is OUTSIDE that
180// hypotenuse (32 > 31). So the first triangle doesn't even cover
181// (16, 16) - it reads sentinel. The other three triangles (shifted
182// +4 in X, Y, or both) extend coverage to include (16, 16) and read
183// the dithered value.
184//
185// All three drawing triangles return the SAME value (0x3def). This
186// confirms screen-space anchoring: the dither cell at (16, 16) does
187// not depend on which triangle covers it.
188// ============================================================================
189
190#define DT_POS_00_AT_16_16 RASTER_SENTINEL /* outside triangle bound */
191#define DT_POS_04_AT_16_16 DT_DITHER_MID
192#define DT_POS_40_AT_16_16 DT_DITHER_MID
193#define DT_POS_44_AT_16_16 DT_DITHER_MID
194
195// ============================================================================
196// DT_SAT_LOW: R=4 input. Bayer offsets at probed cells are {-4, +0,
197// -3, +1}. 4-4=0, 4+0=4, 4-3=1, 4+1=5 - all truncate to R5=0. No
198// saturation surfaces here because the offsets stay in the [0, 5]
199// 8-bit range which collapses uniformly to R5=0. To probe actual
200// saturation (clamp-vs-wrap behavior at the 0 or 255 boundary), use
201// base values that put exactly one cell across the boundary; a
202// follow-up phase can revisit if the refactor needs that case.
203// ============================================================================
204
205#define DT_SAT_LOW_R04_8_8 0x0000u
206#define DT_SAT_LOW_R04_9_8 0x0000u
207#define DT_SAT_LOW_R04_10_8 0x0000u
208#define DT_SAT_LOW_R04_11_8 0x0000u
209
210// DT_SAT_HIGH: R=0xFC. Bayer offsets {-4, +0, -3, +1}. 252-4=248,
211// 252+0=252, 252-3=249, 252+1=253 - all truncate to R5=31. Same
212// caveat: real saturation behavior unprobed here.
213#define DT_SAT_HIGH_RFC_8_8 0x001fu
214#define DT_SAT_HIGH_RFC_9_8 0x001fu
215#define DT_SAT_HIGH_RFC_10_8 0x001fu
216#define DT_SAT_HIGH_RFC_11_8 0x001fu
217
218// ============================================================================
219// DT_SAT_CROSS: probes the cells where the Bayer offset pushes the
220// dithered value across 0 or 255. These are the diagnostic cases for
221// clamp-vs-wrap behavior at the channel boundary.
222//
223// Cell (0, 0) has offset -4 at screen (8, 8). At base R=3, output =
224// 3-4 = -1. Clamp policy: 0 -> R5=0. Wrap-as-uint8 policy: 255 ->
225// R5=31. The visible difference distinguishes the policies.
226//
227// Cell (2, 1) has offset +3 at screen (10, 9). At base R=255, output
228// = 258. Clamp policy: 255 -> R5=31. Wrap policy: 2 -> R5=0.
229//
230// Reference cells with offset 0 (no crossing) provide controls. The
231// "land exactly at boundary" cases (R=4 cell -4 -> 0, R=252 cell +3
232// -> 255) confirm the boundary itself doesn't shift.
233// ============================================================================
234
235#define DT_SAT_CROSS_UNDER_R3_8_8 0x0000u /* offset -4 underflow, predict clamp */
236#define DT_SAT_CROSS_UNDER_R3_10_10 0x0000u /* offset -4 underflow, predict clamp */
237#define DT_SAT_CROSS_LAND_R4_8_8 0x0000u /* offset -4 lands at 0 exactly */
238#define DT_SAT_CROSS_OVER_R255_10_9 0x001fu /* offset +3 overflow, predict clamp */
239#define DT_SAT_CROSS_OVER_R255_8_11 0x001fu /* offset +3 overflow, predict clamp */
240#define DT_SAT_CROSS_LAND_R252_10_9 0x001fu /* offset +3 lands at 255 exactly */