Skip to main content

rustynes_ppu/
palette.rs

1//! NES color palette.
2//!
3//! 64-entry palette used by the 2C02 to convert 6-bit NES indices into
4//! visible colors. Source: the FBX "Smooth (FBX)" / Mesen-default palette,
5//! widely cited as the closest practical match to a calibrated Sony PVM
6//! NTSC reference. Stored as `[R, G, B]` triples to avoid any byte-grouping
7//! ambiguity in 24-bit hex literals.
8//!
9//! See also: <https://www.firebrandx.com/nespalette.html> and
10//! `ref-docs/research-report.md` §NTSC composite filter.
11
12/// 64-entry RGB888 NES palette (FBX Smooth). Each entry is `[R, G, B]`.
13pub const NES_PALETTE: [[u8; 3]; 64] = [
14    [0x6A, 0x6D, 0x6A],
15    [0x00, 0x13, 0x80],
16    [0x1E, 0x00, 0x8A],
17    [0x39, 0x00, 0x7A],
18    [0x55, 0x00, 0x56],
19    [0x5A, 0x00, 0x18],
20    [0x4F, 0x10, 0x00],
21    [0x3D, 0x1C, 0x00],
22    [0x25, 0x3A, 0x00],
23    [0x00, 0x4E, 0x00],
24    [0x00, 0x46, 0x00],
25    [0x00, 0x4A, 0x18],
26    [0x00, 0x40, 0x5A],
27    [0x00, 0x00, 0x00],
28    [0x00, 0x00, 0x00],
29    [0x00, 0x00, 0x00],
30    [0xB9, 0xBC, 0xB9],
31    [0x18, 0x4B, 0xCF],
32    [0x4B, 0x24, 0xE8],
33    [0x7C, 0x12, 0xE0],
34    [0xAB, 0x13, 0xB5],
35    [0xB7, 0x21, 0x64],
36    [0xAB, 0x37, 0x18],
37    [0x8B, 0x5A, 0x00],
38    [0x5C, 0x7A, 0x00],
39    [0x20, 0x90, 0x00],
40    [0x00, 0x8F, 0x00],
41    [0x00, 0x8C, 0x42],
42    [0x00, 0x7D, 0x8A],
43    [0x00, 0x00, 0x00],
44    [0x00, 0x00, 0x00],
45    [0x00, 0x00, 0x00],
46    [0xFF, 0xFF, 0xFF],
47    [0x64, 0xA0, 0xFF],
48    [0x84, 0x79, 0xFF],
49    [0xAC, 0x68, 0xFF],
50    [0xDA, 0x60, 0xFF],
51    [0xE2, 0x6B, 0xC5],
52    [0xDC, 0x83, 0x4C],
53    [0xC3, 0x9A, 0x18],
54    [0x9C, 0xB0, 0x00],
55    [0x60, 0xC0, 0x00],
56    [0x30, 0xC8, 0x3C],
57    [0x28, 0xC5, 0x8C],
58    [0x3C, 0xB7, 0xC9],
59    [0x4C, 0x4C, 0x4C],
60    [0x00, 0x00, 0x00],
61    [0x00, 0x00, 0x00],
62    [0xFF, 0xFF, 0xFF],
63    [0xC8, 0xDD, 0xFF],
64    [0xD5, 0xCC, 0xFF],
65    [0xE5, 0xC7, 0xFF],
66    [0xF5, 0xC5, 0xFF],
67    [0xFA, 0xC9, 0xE6],
68    [0xF8, 0xD2, 0xBD],
69    [0xEF, 0xDA, 0x99],
70    [0xE1, 0xE1, 0x88],
71    [0xC8, 0xE7, 0x88],
72    [0xB0, 0xEA, 0x9C],
73    [0xA4, 0xEB, 0xBD],
74    [0xAA, 0xE5, 0xE2],
75    [0xB0, 0xB0, 0xB0],
76    [0x00, 0x00, 0x00],
77    [0x00, 0x00, 0x00],
78];
79
80/// Convert a 6-bit NES color index to an RGBA8 quad.
81#[must_use]
82pub const fn nes_color_to_rgba(idx: u8) -> [u8; 4] {
83    let rgb = NES_PALETTE[(idx & 0x3F) as usize];
84    [rgb[0], rgb[1], rgb[2], 0xFF]
85}
86
87/// Which PPU palette is active for the running console.
88///
89/// The default 2C02 ([`Self::Composite2C02`]) is the standard NES/Famicom
90/// composite palette ([`NES_PALETTE`] + `apply_emphasis`). The other variants
91/// are the hardware RGB palettes baked into the Vs. System / PlayChoice-10
92/// arcade PPUs (the 2C03 / 2C04-xxxx / 2C05), which output digital RGB rather
93/// than composite video. Selected from the NES 2.0 header byte 13 Vs. PPU type
94/// when the console type is Vs. System.
95///
96/// Source: nesdev "PPU palettes" (`https://www.nesdev.org/wiki/PPU_palettes`)
97/// — the per-PPU 9-bit DAC tables converted with `C = 255 * DAC / 7` (integer
98/// truncation, matching the wiki's rendered hex).
99#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
100pub enum PpuPalette {
101    /// Standard 2C02 NES/Famicom composite palette (default).
102    #[default]
103    Composite2C02,
104    /// 2C03 RGB PPU (PlayChoice-10, Vs. Duck Hunt / Tennis, Sharp C1). Shared
105    /// with the 2C05.
106    Rgb2C03,
107    /// RP2C04-0001 RGB PPU (Vs. System).
108    Rgb2C04_0001,
109    /// RP2C04-0002 RGB PPU (Vs. System).
110    Rgb2C04_0002,
111    /// RP2C04-0003 RGB PPU (Vs. System).
112    Rgb2C04_0003,
113    /// RP2C04-0004 RGB PPU (Vs. System).
114    Rgb2C04_0004,
115    /// 2C05 RGB PPU — uses the 2C03 master palette (the 2C05's quirks are the
116    /// `$2000`/`$2001` swap and the `$2002` identifier, not the colors).
117    Rgb2C05,
118}
119
120impl PpuPalette {
121    /// Returns the backing 64-entry RGB table, or `None` for the composite
122    /// 2C02 (which is handled by [`nes_color_to_rgba`] + `apply_emphasis`).
123    const fn rgb_table(self) -> Option<&'static [[u8; 3]; 64]> {
124        match self {
125            Self::Composite2C02 => None,
126            // The 2C05 shares the 2C03 master palette.
127            Self::Rgb2C03 | Self::Rgb2C05 => Some(&RGB_2C03),
128            Self::Rgb2C04_0001 => Some(&RGB_2C04_0001),
129            Self::Rgb2C04_0002 => Some(&RGB_2C04_0002),
130            Self::Rgb2C04_0003 => Some(&RGB_2C04_0003),
131            Self::Rgb2C04_0004 => Some(&RGB_2C04_0004),
132        }
133    }
134
135    /// True for the composite 2C02 (the default NES/Famicom path).
136    #[must_use]
137    pub const fn is_composite(self) -> bool {
138        matches!(self, Self::Composite2C02)
139    }
140}
141
142/// Convert a 6-bit color index to RGBA8 under the active PPU palette,
143/// applying the palette's emphasis model.
144///
145/// The default [`PpuPalette::Composite2C02`] path is byte-for-byte identical to
146/// `apply_emphasis(nes_color_to_rgba(idx), ...)` so normal NES/Famicom rendering
147/// is unchanged.
148///
149/// On the RGB PPUs (2C03 / 2C04 / 2C05) emphasis works the *opposite* way from
150/// the 2C02: rather than darkening the non-emphasized channels, each set
151/// emphasis bit forces its own channel to full brightness (`0xFF`). Per nesdev
152/// "PPU palettes": "emphasis on the RGB chips works differently; rather than
153/// 'darkening' the specific color chosen, it sets the corresponding channel to
154/// full brightness instead."
155#[must_use]
156pub const fn palette_color_to_rgba(
157    palette: PpuPalette,
158    idx: u8,
159    emph_red: bool,
160    emph_green: bool,
161    emph_blue: bool,
162) -> [u8; 4] {
163    match palette.rgb_table() {
164        None => {
165            // Default composite 2C02: byte-identical to the legacy path.
166            let rgba = nes_color_to_rgba(idx);
167            apply_emphasis(rgba, emph_red, emph_green, emph_blue)
168        }
169        Some(table) => {
170            let rgb = table[(idx & 0x3F) as usize];
171            let mut out = [rgb[0], rgb[1], rgb[2], 0xFF];
172            // RGB-PPU emphasis: force the emphasized channel to full brightness.
173            if emph_red {
174                out[0] = 0xFF;
175            }
176            if emph_green {
177                out[1] = 0xFF;
178            }
179            if emph_blue {
180                out[2] = 0xFF;
181            }
182            out
183        }
184    }
185}
186
187/// v2.8.0 Phase 4 — build the 512-entry `(emphasis << 6) | color` → RGBA8
188/// lookup the PPU's pixel-emit path uses.
189///
190/// Emphasis bit layout matches PPUMASK bits 7-5 shifted down: bit 0 = red,
191/// bit 1 = green, bit 2 = blue. Built from [`palette_color_to_rgba`] (the
192/// exact function the per-pixel path used to call), so the table is
193/// byte-identical to the direct computation by construction.
194#[must_use]
195pub const fn build_rgba_lut(palette: PpuPalette) -> [[u8; 4]; 512] {
196    let mut lut = [[0u8; 4]; 512];
197    let mut e = 0usize;
198    while e < 8 {
199        let mut c = 0usize;
200        while c < 64 {
201            #[allow(clippy::cast_possible_truncation)] // c < 64.
202            let idx = c as u8;
203            lut[(e << 6) | c] =
204                palette_color_to_rgba(palette, idx, e & 1 != 0, e & 2 != 0, e & 4 != 0);
205            c += 1;
206        }
207        e += 1;
208    }
209    lut
210}
211
212/// Build the 512-entry `(emphasis << 6) | color` → RGBA8 lookup from a custom
213/// 64-entry base palette (e.g. a loaded `.pal` file).
214///
215/// Applies the standard 2C02 composite emphasis model — the same `apply_emphasis`
216/// the default composite path uses — so a custom palette behaves like a drop-in
217/// replacement for `NES_PALETTE`.
218///
219/// v1.1.0 beta.1 (T-110-A3): the frontend feeds the parsed `.pal` here via
220/// `Ppu::set_custom_palette`. With no custom palette the PPU keeps using
221/// [`build_rgba_lut`] over its active [`PpuPalette`], so default rendering (and
222/// `AccuracyCoin` / the commercial oracle) is byte-identical.
223#[must_use]
224pub const fn build_rgba_lut_from_base(base: &[[u8; 3]; 64]) -> [[u8; 4]; 512] {
225    let mut lut = [[0u8; 4]; 512];
226    let mut e = 0usize;
227    while e < 8 {
228        let mut c = 0usize;
229        while c < 64 {
230            let rgb = base[c];
231            lut[(e << 6) | c] = apply_emphasis(
232                [rgb[0], rgb[1], rgb[2], 0xFF],
233                e & 1 != 0,
234                e & 2 != 0,
235                e & 4 != 0,
236            );
237            c += 1;
238        }
239        e += 1;
240    }
241    lut
242}
243
244/// Apply BGR emphasis (PPUMASK bits 7-5) to an RGBA8 pixel.
245///
246/// Per nesdev: emphasis dims the *non-emphasized* channels by ~25%. We
247/// apply 13/16 (~ 0.81) per non-active channel — close enough for visual
248/// regression without floating point.
249#[must_use]
250pub const fn apply_emphasis(
251    mut rgba: [u8; 4],
252    emph_red: bool,
253    emph_green: bool,
254    emph_blue: bool,
255) -> [u8; 4] {
256    if emph_red {
257        rgba[1] = ((rgba[1] as u16 * 13) >> 4) as u8;
258        rgba[2] = ((rgba[2] as u16 * 13) >> 4) as u8;
259    }
260    if emph_green {
261        rgba[0] = ((rgba[0] as u16 * 13) >> 4) as u8;
262        rgba[2] = ((rgba[2] as u16 * 13) >> 4) as u8;
263    }
264    if emph_blue {
265        rgba[0] = ((rgba[0] as u16 * 13) >> 4) as u8;
266        rgba[1] = ((rgba[1] as u16 * 13) >> 4) as u8;
267    }
268    rgba
269}
270
271/// 2C03 / 2C05 RGB PPU palette (PlayChoice-10, Vs. Duck Hunt/Tennis, Sharp C1).
272/// nesdev "PPU palettes" DAC table, `C = 255 * DAC / 7` (integer truncation).
273/// The trailing decimal comment on each row is the raw 9-bit DAC triple.
274const RGB_2C03: [[u8; 3]; 64] = [
275    [0x6D, 0x6D, 0x6D], // 333
276    [0x00, 0x24, 0x91], // 014
277    [0x00, 0x00, 0xDA], // 006
278    [0x6D, 0x48, 0xDA], // 326
279    [0x91, 0x00, 0x6D], // 403
280    [0xB6, 0x00, 0x6D], // 503
281    [0xB6, 0x24, 0x00], // 510
282    [0x91, 0x48, 0x00], // 420
283    [0x6D, 0x48, 0x00], // 320
284    [0x24, 0x48, 0x00], // 120
285    [0x00, 0x6D, 0x24], // 031
286    [0x00, 0x91, 0x00], // 040
287    [0x00, 0x48, 0x48], // 022
288    [0x00, 0x00, 0x00], // 000
289    [0x00, 0x00, 0x00], // 000
290    [0x00, 0x00, 0x00], // 000
291    [0xB6, 0xB6, 0xB6], // 555
292    [0x00, 0x6D, 0xDA], // 036
293    [0x00, 0x48, 0xFF], // 027
294    [0x91, 0x00, 0xFF], // 407
295    [0xB6, 0x00, 0xFF], // 507
296    [0xFF, 0x00, 0x91], // 704
297    [0xFF, 0x00, 0x00], // 700
298    [0xDA, 0x6D, 0x00], // 630
299    [0x91, 0x6D, 0x00], // 430
300    [0x24, 0x91, 0x00], // 140
301    [0x00, 0x91, 0x00], // 040
302    [0x00, 0xB6, 0x6D], // 053
303    [0x00, 0x91, 0x91], // 044
304    [0x00, 0x00, 0x00], // 000
305    [0x00, 0x00, 0x00], // 000
306    [0x00, 0x00, 0x00], // 000
307    [0xFF, 0xFF, 0xFF], // 777
308    [0x6D, 0xB6, 0xFF], // 357
309    [0x91, 0x91, 0xFF], // 447
310    [0xDA, 0x6D, 0xFF], // 637
311    [0xFF, 0x00, 0xFF], // 707
312    [0xFF, 0x6D, 0xFF], // 737
313    [0xFF, 0x91, 0x00], // 740
314    [0xFF, 0xB6, 0x00], // 750
315    [0xDA, 0xDA, 0x00], // 660
316    [0x6D, 0xDA, 0x00], // 360
317    [0x00, 0xFF, 0x00], // 070
318    [0x48, 0xFF, 0xDA], // 276
319    [0x00, 0xFF, 0xFF], // 077
320    [0x00, 0x00, 0x00], // 000
321    [0x00, 0x00, 0x00], // 000
322    [0x00, 0x00, 0x00], // 000
323    [0xFF, 0xFF, 0xFF], // 777
324    [0xB6, 0xDA, 0xFF], // 567
325    [0xDA, 0xB6, 0xFF], // 657
326    [0xFF, 0xB6, 0xFF], // 757
327    [0xFF, 0x91, 0xFF], // 747
328    [0xFF, 0xB6, 0xB6], // 755
329    [0xFF, 0xDA, 0x91], // 764
330    [0xFF, 0xFF, 0x48], // 772
331    [0xFF, 0xFF, 0x6D], // 773
332    [0xB6, 0xFF, 0x48], // 572
333    [0x91, 0xFF, 0x6D], // 473
334    [0x48, 0xFF, 0xDA], // 276
335    [0x91, 0xDA, 0xFF], // 467
336    [0x00, 0x00, 0x00], // 000
337    [0x00, 0x00, 0x00], // 000
338    [0x00, 0x00, 0x00], // 000
339];
340
341/// RP2C04-0001 RGB PPU palette (Vs. System). See [`RGB_2C03`] for the source.
342const RGB_2C04_0001: [[u8; 3]; 64] = [
343    [0xFF, 0xB6, 0xB6], // 755
344    [0xDA, 0x6D, 0xFF], // 637
345    [0xFF, 0x00, 0x00], // 700
346    [0x91, 0x91, 0xFF], // 447
347    [0x00, 0x91, 0x91], // 044
348    [0x24, 0x48, 0x00], // 120
349    [0x48, 0x48, 0x48], // 222
350    [0xFF, 0x00, 0x91], // 704
351    [0xFF, 0xFF, 0xFF], // 777
352    [0x6D, 0x6D, 0x6D], // 333
353    [0xFF, 0xB6, 0x00], // 750
354    [0xB6, 0x00, 0x6D], // 503
355    [0x91, 0x00, 0x6D], // 403
356    [0xDA, 0xDA, 0x00], // 660
357    [0x6D, 0x48, 0x00], // 320
358    [0xFF, 0xFF, 0xFF], // 777
359    [0x6D, 0xB6, 0xFF], // 357
360    [0xDA, 0xB6, 0x6D], // 653
361    [0x6D, 0x24, 0x00], // 310
362    [0x6D, 0xDA, 0x00], // 360
363    [0x91, 0xDA, 0xFF], // 467
364    [0xDA, 0xB6, 0xFF], // 657
365    [0xFF, 0xDA, 0x91], // 764
366    [0x00, 0x48, 0xFF], // 027
367    [0xFF, 0xDA, 0x00], // 760
368    [0x48, 0xFF, 0xDA], // 276
369    [0x00, 0x00, 0x00], // 000
370    [0x48, 0x00, 0x00], // 200
371    [0xDA, 0xDA, 0xDA], // 666
372    [0x91, 0x91, 0x91], // 444
373    [0xFF, 0x00, 0xFF], // 707
374    [0x00, 0x24, 0x91], // 014
375    [0x00, 0x00, 0x6D], // 003
376    [0xB6, 0xDA, 0xFF], // 567
377    [0xFF, 0xB6, 0xFF], // 757
378    [0x00, 0xFF, 0x00], // 070
379    [0x00, 0xFF, 0xFF], // 077
380    [0x00, 0x48, 0x48], // 022
381    [0x00, 0xB6, 0x6D], // 053
382    [0xB6, 0x00, 0xFF], // 507
383    [0x00, 0x00, 0x00], // 000
384    [0x91, 0x48, 0x00], // 420
385    [0xFF, 0x91, 0xFF], // 747
386    [0xB6, 0x24, 0x00], // 510
387    [0x91, 0x00, 0xFF], // 407
388    [0x00, 0x00, 0xDA], // 006
389    [0xFF, 0x91, 0x00], // 740
390    [0x00, 0x00, 0x00], // 000
391    [0x00, 0x00, 0x00], // 000
392    [0x24, 0x91, 0x00], // 140
393    [0xB6, 0xB6, 0xB6], // 555
394    [0x00, 0x6D, 0x24], // 031
395    [0xB6, 0xFF, 0x48], // 572
396    [0x6D, 0x48, 0xDA], // 326
397    [0xFF, 0xFF, 0x00], // 770
398    [0xDA, 0x6D, 0x00], // 630
399    [0x00, 0x48, 0x00], // 020
400    [0x00, 0x6D, 0xDA], // 036
401    [0x00, 0x91, 0x00], // 040
402    [0x24, 0x24, 0x24], // 111
403    [0xFF, 0xFF, 0x6D], // 773
404    [0xFF, 0x6D, 0xFF], // 737
405    [0x91, 0x6D, 0x00], // 430
406    [0x91, 0xFF, 0x6D], // 473
407];
408
409/// RP2C04-0002 RGB PPU palette (Vs. System). See [`RGB_2C03`] for the source.
410const RGB_2C04_0002: [[u8; 3]; 64] = [
411    [0x00, 0x00, 0x00], // 000
412    [0xFF, 0xB6, 0x00], // 750
413    [0x91, 0x6D, 0x00], // 430
414    [0xB6, 0xFF, 0x48], // 572
415    [0x91, 0xFF, 0x6D], // 473
416    [0xFF, 0x6D, 0xFF], // 737
417    [0x00, 0x91, 0x91], // 044
418    [0xB6, 0xDA, 0xFF], // 567
419    [0xFF, 0x00, 0x00], // 700
420    [0x91, 0x00, 0xFF], // 407
421    [0xFF, 0xFF, 0x6D], // 773
422    [0xFF, 0x91, 0xFF], // 747
423    [0xFF, 0xFF, 0xFF], // 777
424    [0xDA, 0x6D, 0xFF], // 637
425    [0x91, 0xDA, 0xFF], // 467
426    [0x00, 0x91, 0x00], // 040
427    [0x00, 0x48, 0x00], // 020
428    [0x6D, 0xB6, 0xFF], // 357
429    [0xB6, 0x24, 0x00], // 510
430    [0xDA, 0xDA, 0xDA], // 666
431    [0x00, 0xB6, 0x6D], // 053
432    [0x6D, 0xDA, 0x00], // 360
433    [0x48, 0x00, 0x00], // 200
434    [0x91, 0x91, 0xFF], // 447
435    [0x48, 0x48, 0x48], // 222
436    [0xFF, 0x00, 0xFF], // 707
437    [0x00, 0x00, 0x6D], // 003
438    [0x48, 0xFF, 0xDA], // 276
439    [0xDA, 0xB6, 0xFF], // 657
440    [0x6D, 0x48, 0x00], // 320
441    [0x00, 0x00, 0x00], // 000
442    [0x6D, 0x48, 0xDA], // 326
443    [0x91, 0x00, 0x6D], // 403
444    [0xFF, 0xDA, 0x91], // 764
445    [0xFF, 0x91, 0x00], // 740
446    [0xFF, 0xB6, 0xFF], // 757
447    [0x00, 0x6D, 0xDA], // 036
448    [0x6D, 0x24, 0x00], // 310
449    [0xB6, 0xB6, 0xB6], // 555
450    [0x00, 0x00, 0xDA], // 006
451    [0xB6, 0x00, 0xFF], // 507
452    [0xFF, 0xDA, 0x00], // 760
453    [0x6D, 0x6D, 0x6D], // 333
454    [0x24, 0x48, 0x00], // 120
455    [0x00, 0x48, 0xFF], // 027
456    [0x00, 0x00, 0x00], // 000
457    [0xDA, 0xDA, 0x00], // 660
458    [0xFF, 0xFF, 0xFF], // 777
459    [0xDA, 0xB6, 0x6D], // 653
460    [0x24, 0x24, 0x24], // 111
461    [0x00, 0xFF, 0x00], // 070
462    [0xDA, 0x6D, 0x00], // 630
463    [0x00, 0x48, 0x48], // 022
464    [0x00, 0x24, 0x91], // 014
465    [0xFF, 0x00, 0x91], // 704
466    [0x24, 0x91, 0x00], // 140
467    [0x00, 0x00, 0x00], // 000
468    [0x00, 0xFF, 0xFF], // 077
469    [0x91, 0x48, 0x00], // 420
470    [0xFF, 0xFF, 0x00], // 770
471    [0xFF, 0xB6, 0xB6], // 755
472    [0xB6, 0x00, 0x6D], // 503
473    [0x00, 0x6D, 0x24], // 031
474    [0x91, 0x91, 0x91], // 444
475];
476
477/// RP2C04-0003 RGB PPU palette (Vs. System). See [`RGB_2C03`] for the source.
478const RGB_2C04_0003: [[u8; 3]; 64] = [
479    [0xB6, 0x00, 0xFF], // 507
480    [0xFF, 0x6D, 0xFF], // 737
481    [0x91, 0xFF, 0x6D], // 473
482    [0xB6, 0xB6, 0xB6], // 555
483    [0x00, 0x91, 0x00], // 040
484    [0xFF, 0xFF, 0xFF], // 777
485    [0xB6, 0xDA, 0xFF], // 567
486    [0x24, 0x48, 0x00], // 120
487    [0x00, 0x24, 0x91], // 014
488    [0x00, 0x00, 0x00], // 000
489    [0xFF, 0xDA, 0x91], // 764
490    [0x6D, 0x48, 0x00], // 320
491    [0xFF, 0x00, 0x91], // 704
492    [0xDA, 0xDA, 0xDA], // 666
493    [0xDA, 0xB6, 0x6D], // 653
494    [0x91, 0xDA, 0xFF], // 467
495    [0x91, 0x91, 0xFF], // 447
496    [0x00, 0x91, 0x91], // 044
497    [0xB6, 0x00, 0x6D], // 503
498    [0x00, 0x48, 0xFF], // 027
499    [0x24, 0x91, 0x00], // 140
500    [0x91, 0x6D, 0x00], // 430
501    [0xDA, 0x6D, 0x00], // 630
502    [0x00, 0xB6, 0x6D], // 053
503    [0x6D, 0x6D, 0x6D], // 333
504    [0x6D, 0x48, 0xDA], // 326
505    [0x00, 0x00, 0x00], // 000
506    [0x00, 0x00, 0xDA], // 006
507    [0xFF, 0x00, 0x00], // 700
508    [0xB6, 0x24, 0x00], // 510
509    [0xFF, 0x91, 0xFF], // 747
510    [0xFF, 0xB6, 0xB6], // 755
511    [0xDA, 0x6D, 0xFF], // 637
512    [0x00, 0x48, 0x00], // 020
513    [0x00, 0x00, 0x6D], // 003
514    [0xFF, 0xFF, 0x00], // 770
515    [0x24, 0x24, 0x24], // 111
516    [0xFF, 0xB6, 0x00], // 750
517    [0xFF, 0x91, 0x00], // 740
518    [0xFF, 0xFF, 0xFF], // 777
519    [0x6D, 0xDA, 0x00], // 360
520    [0x91, 0x00, 0x6D], // 403
521    [0x6D, 0xB6, 0xFF], // 357
522    [0xFF, 0x00, 0xFF], // 707
523    [0x00, 0x6D, 0xDA], // 036
524    [0x91, 0x91, 0x91], // 444
525    [0x00, 0x00, 0x00], // 000
526    [0x6D, 0x24, 0x00], // 310
527    [0x00, 0xFF, 0xFF], // 077
528    [0x48, 0x00, 0x00], // 200
529    [0xB6, 0xFF, 0x48], // 572
530    [0xFF, 0xB6, 0xFF], // 757
531    [0x91, 0x48, 0x00], // 420
532    [0x00, 0xFF, 0x00], // 070
533    [0xDA, 0xDA, 0x00], // 660
534    [0x48, 0x48, 0x48], // 222
535    [0x00, 0x6D, 0x24], // 031
536    [0x00, 0x00, 0x00], // 000
537    [0xDA, 0xB6, 0xFF], // 657
538    [0xFF, 0xFF, 0x6D], // 773
539    [0x91, 0x00, 0xFF], // 407
540    [0x48, 0xFF, 0xDA], // 276
541    [0xFF, 0xDA, 0x00], // 760
542    [0x00, 0x48, 0x48], // 022
543];
544
545/// RP2C04-0004 RGB PPU palette (Vs. System). See [`RGB_2C03`] for the source.
546const RGB_2C04_0004: [[u8; 3]; 64] = [
547    [0x91, 0x6D, 0x00], // 430
548    [0x6D, 0x48, 0xDA], // 326
549    [0x00, 0x91, 0x91], // 044
550    [0xDA, 0xDA, 0x00], // 660
551    [0x00, 0x00, 0x00], // 000
552    [0xFF, 0xB6, 0xB6], // 755
553    [0x00, 0x24, 0x91], // 014
554    [0xDA, 0x6D, 0x00], // 630
555    [0xB6, 0xB6, 0xB6], // 555
556    [0x6D, 0x24, 0x00], // 310
557    [0x00, 0xFF, 0x00], // 070
558    [0x00, 0x00, 0x6D], // 003
559    [0xFF, 0xDA, 0x91], // 764
560    [0xFF, 0xFF, 0x00], // 770
561    [0x00, 0x91, 0x00], // 040
562    [0xB6, 0xFF, 0x48], // 572
563    [0xFF, 0x6D, 0xFF], // 737
564    [0x48, 0x00, 0x00], // 200
565    [0x00, 0x48, 0xFF], // 027
566    [0xFF, 0x91, 0xFF], // 747
567    [0x00, 0x00, 0x00], // 000
568    [0x48, 0x48, 0x48], // 222
569    [0xB6, 0x24, 0x00], // 510
570    [0xFF, 0x91, 0x00], // 740
571    [0xDA, 0xB6, 0x6D], // 653
572    [0x00, 0xB6, 0x6D], // 053
573    [0x91, 0x91, 0xFF], // 447
574    [0x24, 0x91, 0x00], // 140
575    [0x91, 0x00, 0x6D], // 403
576    [0x00, 0x00, 0x00], // 000
577    [0x91, 0xFF, 0x6D], // 473
578    [0x6D, 0xB6, 0xFF], // 357
579    [0xB6, 0x00, 0x6D], // 503
580    [0x00, 0x6D, 0x24], // 031
581    [0x91, 0x48, 0x00], // 420
582    [0x00, 0x00, 0xDA], // 006
583    [0x91, 0x00, 0xFF], // 407
584    [0xB6, 0x00, 0xFF], // 507
585    [0x6D, 0x6D, 0x6D], // 333
586    [0xFF, 0x00, 0x91], // 704
587    [0x00, 0x48, 0x48], // 022
588    [0xDA, 0xDA, 0xDA], // 666
589    [0x00, 0x6D, 0xDA], // 036
590    [0x00, 0x48, 0x00], // 020
591    [0x24, 0x24, 0x24], // 111
592    [0xFF, 0xFF, 0x6D], // 773
593    [0x91, 0x91, 0x91], // 444
594    [0xFF, 0x00, 0xFF], // 707
595    [0xFF, 0xB6, 0xFF], // 757
596    [0xFF, 0xFF, 0xFF], // 777
597    [0x6D, 0x48, 0x00], // 320
598    [0xFF, 0x00, 0x00], // 700
599    [0xFF, 0xDA, 0x00], // 760
600    [0x48, 0xFF, 0xDA], // 276
601    [0xFF, 0xFF, 0xFF], // 777
602    [0x91, 0xDA, 0xFF], // 467
603    [0x00, 0x00, 0x00], // 000
604    [0xFF, 0xB6, 0x00], // 750
605    [0xDA, 0x6D, 0xFF], // 637
606    [0xB6, 0xDA, 0xFF], // 567
607    [0x6D, 0xDA, 0x00], // 360
608    [0xDA, 0xB6, 0xFF], // 657
609    [0x00, 0xFF, 0xFF], // 077
610    [0x24, 0x48, 0x00], // 120
611];
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616
617    #[test]
618    fn custom_base_lut_matches_builtin_composite() {
619        // A custom palette equal to the built-in NES_PALETTE must produce the
620        // exact same 512-entry LUT as the default composite path — proving the
621        // `.pal` custom-palette route is a faithful drop-in (so loading the
622        // built-in colours is byte-identical, and AccuracyCoin/oracle-safe).
623        let from_base = build_rgba_lut_from_base(&NES_PALETTE);
624        let builtin = build_rgba_lut(PpuPalette::Composite2C02);
625        assert_eq!(from_base, builtin);
626    }
627
628    #[test]
629    fn nes_color_indices_round_trip() {
630        // Index 0x0F = $0F = black.
631        let [r, g, b, a] = nes_color_to_rgba(0x0F);
632        assert_eq!(a, 0xFF);
633        assert_eq!((r, g, b), (0, 0, 0));
634    }
635
636    #[test]
637    fn nes_color_index_30_is_white() {
638        let [r, g, b, _] = nes_color_to_rgba(0x30);
639        assert_eq!((r, g, b), (0xFF, 0xFF, 0xFF));
640    }
641
642    #[test]
643    fn emphasis_dims_other_channels() {
644        let base = nes_color_to_rgba(0x30);
645        let red = apply_emphasis(base, true, false, false);
646        assert_eq!(red[0], base[0]);
647        assert!(red[1] <= base[1]);
648        assert!(red[2] <= base[2]);
649    }
650
651    #[test]
652    fn composite_palette_is_byte_identical_to_legacy_path() {
653        // The default PpuPalette path MUST equal apply_emphasis(nes_color_to_rgba)
654        // for every index and every emphasis combination.
655        for idx in 0u8..64 {
656            for emask in 0u8..8 {
657                let (r, g, b) = (emask & 1 != 0, emask & 2 != 0, emask & 4 != 0);
658                let legacy = apply_emphasis(nes_color_to_rgba(idx), r, g, b);
659                let routed = palette_color_to_rgba(PpuPalette::Composite2C02, idx, r, g, b);
660                assert_eq!(legacy, routed, "idx={idx} emask={emask}");
661            }
662        }
663    }
664
665    #[test]
666    fn rgb_2c03_documented_entries() {
667        // nesdev "PPU palettes" 2C03/2C05 table, C = 255*DAC/7 truncation.
668        // $00 = 333 = #6D6D6D, $01 = 014 = #002491, $0F = 000 = #000000,
669        // $20 = 777 = #FFFFFF, $16 = 700 = #FF0000.
670        let none = (false, false, false);
671        assert_eq!(
672            palette_color_to_rgba(PpuPalette::Rgb2C03, 0x00, none.0, none.1, none.2),
673            [0x6D, 0x6D, 0x6D, 0xFF]
674        );
675        assert_eq!(
676            palette_color_to_rgba(PpuPalette::Rgb2C03, 0x01, none.0, none.1, none.2),
677            [0x00, 0x24, 0x91, 0xFF]
678        );
679        assert_eq!(
680            palette_color_to_rgba(PpuPalette::Rgb2C03, 0x0F, none.0, none.1, none.2),
681            [0x00, 0x00, 0x00, 0xFF]
682        );
683        assert_eq!(
684            palette_color_to_rgba(PpuPalette::Rgb2C03, 0x20, none.0, none.1, none.2),
685            [0xFF, 0xFF, 0xFF, 0xFF]
686        );
687        assert_eq!(
688            palette_color_to_rgba(PpuPalette::Rgb2C03, 0x16, none.0, none.1, none.2),
689            [0xFF, 0x00, 0x00, 0xFF]
690        );
691    }
692
693    #[test]
694    fn rgb_2c05_shares_2c03_table() {
695        for idx in 0u8..64 {
696            assert_eq!(
697                palette_color_to_rgba(PpuPalette::Rgb2C05, idx, false, false, false),
698                palette_color_to_rgba(PpuPalette::Rgb2C03, idx, false, false, false),
699                "idx={idx}"
700            );
701        }
702    }
703
704    #[test]
705    fn rgb_2c04_first_entries_match_wiki() {
706        let none = (false, false, false);
707        // First entry of each 2C04 permutation (index $00).
708        assert_eq!(
709            palette_color_to_rgba(PpuPalette::Rgb2C04_0001, 0x00, none.0, none.1, none.2),
710            [0xFF, 0xB6, 0xB6, 0xFF] // 755
711        );
712        assert_eq!(
713            palette_color_to_rgba(PpuPalette::Rgb2C04_0002, 0x00, none.0, none.1, none.2),
714            [0x00, 0x00, 0x00, 0xFF] // 000
715        );
716        assert_eq!(
717            palette_color_to_rgba(PpuPalette::Rgb2C04_0003, 0x00, none.0, none.1, none.2),
718            [0xB6, 0x00, 0xFF, 0xFF] // 507
719        );
720        assert_eq!(
721            palette_color_to_rgba(PpuPalette::Rgb2C04_0004, 0x00, none.0, none.1, none.2),
722            [0x91, 0x6D, 0x00, 0xFF] // 430
723        );
724    }
725
726    #[test]
727    fn rgb_emphasis_forces_channel_full_brightness() {
728        // RGB PPUs: an emphasis bit forces ITS channel to 0xFF (opposite of
729        // the 2C02's darkening). Start from index $00 = #6D6D6D on the 2C03.
730        let base = palette_color_to_rgba(PpuPalette::Rgb2C03, 0x00, false, false, false);
731        assert_eq!(base, [0x6D, 0x6D, 0x6D, 0xFF]);
732        let red = palette_color_to_rgba(PpuPalette::Rgb2C03, 0x00, true, false, false);
733        assert_eq!(red, [0xFF, 0x6D, 0x6D, 0xFF]);
734        let green = palette_color_to_rgba(PpuPalette::Rgb2C03, 0x00, false, true, false);
735        assert_eq!(green, [0x6D, 0xFF, 0x6D, 0xFF]);
736        let blue = palette_color_to_rgba(PpuPalette::Rgb2C03, 0x00, false, false, true);
737        assert_eq!(blue, [0x6D, 0x6D, 0xFF, 0xFF]);
738        let all = palette_color_to_rgba(PpuPalette::Rgb2C03, 0x00, true, true, true);
739        assert_eq!(all, [0xFF, 0xFF, 0xFF, 0xFF]);
740    }
741
742    #[test]
743    fn composite_default_and_is_composite() {
744        assert_eq!(PpuPalette::default(), PpuPalette::Composite2C02);
745        assert!(PpuPalette::Composite2C02.is_composite());
746        assert!(!PpuPalette::Rgb2C03.is_composite());
747        assert!(!PpuPalette::Rgb2C05.is_composite());
748    }
749
750    /// v2.8.0 Phase 4 — the pixel-emit LUT must equal the direct
751    /// computation for EVERY (palette, emphasis, color) combination; the
752    /// PPU's per-pixel path depends on this byte-for-byte.
753    #[test]
754    fn rgba_lut_matches_direct_computation_exhaustively() {
755        for palette in [
756            PpuPalette::Composite2C02,
757            PpuPalette::Rgb2C03,
758            PpuPalette::Rgb2C04_0001,
759            PpuPalette::Rgb2C04_0002,
760            PpuPalette::Rgb2C04_0003,
761            PpuPalette::Rgb2C04_0004,
762            PpuPalette::Rgb2C05,
763        ] {
764            let lut = build_rgba_lut(palette);
765            for e in 0..8usize {
766                for c in 0..64usize {
767                    #[allow(clippy::cast_possible_truncation)]
768                    let direct =
769                        palette_color_to_rgba(palette, c as u8, e & 1 != 0, e & 2 != 0, e & 4 != 0);
770                    assert_eq!(lut[(e << 6) | c], direct, "{palette:?} e={e} c={c}");
771                }
772            }
773        }
774    }
775}