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_documented_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_documented_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 the FBX base plus the
146/// documented emphasis model (`apply_documented_emphasis`, v2.9.8); with no
147/// emphasis bit set it is exactly [`nes_color_to_rgba`].
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: FBX plus the documented emphasis change.
166            let rgba = nes_color_to_rgba(idx);
167            apply_documented_emphasis(rgba, idx, 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 2C02 composite emphasis model — the same
216/// `apply_documented_emphasis` the default composite path uses (v2.9.8) — so a
217/// custom palette behaves like a drop-in 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            #[allow(clippy::cast_possible_truncation)] // c < 64.
232            let idx = c as u8;
233            lut[(e << 6) | c] = apply_documented_emphasis(
234                [rgb[0], rgb[1], rgb[2], 0xFF],
235                idx,
236                e & 1 != 0,
237                e & 2 != 0,
238                e & 4 != 0,
239            );
240            c += 1;
241        }
242        e += 1;
243    }
244    lut
245}
246
247/// Apply PPUMASK emphasis (bits 7-5) to colour `idx`'s RGBA8 pixel using the
248/// documented composite model (v2.9.8, `T-EMPHASIS-MODEL`): the model's change
249/// for this colour and emphasis, [`crate::emphasis::EMPHASIS_DELTA`], added to
250/// the base and clamped. Columns `$E`/`$F` and emphasis 0 are unchanged. See
251/// `emphasis.rs` for the model and its source.
252#[must_use]
253pub const fn apply_documented_emphasis(
254    rgba: [u8; 4],
255    idx: u8,
256    emph_red: bool,
257    emph_green: bool,
258    emph_blue: bool,
259) -> [u8; 4] {
260    let e = (emph_red as usize) | ((emph_green as usize) << 1) | ((emph_blue as usize) << 2);
261    let d = crate::emphasis::EMPHASIS_DELTA[(e << 6) | (idx & 0x3F) as usize];
262    [
263        crate::emphasis::apply_delta(rgba[0], d[0]),
264        crate::emphasis::apply_delta(rgba[1], d[1]),
265        crate::emphasis::apply_delta(rgba[2], d[2]),
266        rgba[3],
267    ]
268}
269
270/// 2C03 / 2C05 RGB PPU palette (PlayChoice-10, Vs. Duck Hunt/Tennis, Sharp C1).
271/// nesdev "PPU palettes" DAC table, `C = 255 * DAC / 7` (integer truncation).
272/// The trailing decimal comment on each row is the raw 9-bit DAC triple.
273const RGB_2C03: [[u8; 3]; 64] = [
274    [0x6D, 0x6D, 0x6D], // 333
275    [0x00, 0x24, 0x91], // 014
276    [0x00, 0x00, 0xDA], // 006
277    [0x6D, 0x48, 0xDA], // 326
278    [0x91, 0x00, 0x6D], // 403
279    [0xB6, 0x00, 0x6D], // 503
280    [0xB6, 0x24, 0x00], // 510
281    [0x91, 0x48, 0x00], // 420
282    [0x6D, 0x48, 0x00], // 320
283    [0x24, 0x48, 0x00], // 120
284    [0x00, 0x6D, 0x24], // 031
285    [0x00, 0x91, 0x00], // 040
286    [0x00, 0x48, 0x48], // 022
287    [0x00, 0x00, 0x00], // 000
288    [0x00, 0x00, 0x00], // 000
289    [0x00, 0x00, 0x00], // 000
290    [0xB6, 0xB6, 0xB6], // 555
291    [0x00, 0x6D, 0xDA], // 036
292    [0x00, 0x48, 0xFF], // 027
293    [0x91, 0x00, 0xFF], // 407
294    [0xB6, 0x00, 0xFF], // 507
295    [0xFF, 0x00, 0x91], // 704
296    [0xFF, 0x00, 0x00], // 700
297    [0xDA, 0x6D, 0x00], // 630
298    [0x91, 0x6D, 0x00], // 430
299    [0x24, 0x91, 0x00], // 140
300    [0x00, 0x91, 0x00], // 040
301    [0x00, 0xB6, 0x6D], // 053
302    [0x00, 0x91, 0x91], // 044
303    [0x00, 0x00, 0x00], // 000
304    [0x00, 0x00, 0x00], // 000
305    [0x00, 0x00, 0x00], // 000
306    [0xFF, 0xFF, 0xFF], // 777
307    [0x6D, 0xB6, 0xFF], // 357
308    [0x91, 0x91, 0xFF], // 447
309    [0xDA, 0x6D, 0xFF], // 637
310    [0xFF, 0x00, 0xFF], // 707
311    [0xFF, 0x6D, 0xFF], // 737
312    [0xFF, 0x91, 0x00], // 740
313    [0xFF, 0xB6, 0x00], // 750
314    [0xDA, 0xDA, 0x00], // 660
315    [0x6D, 0xDA, 0x00], // 360
316    [0x00, 0xFF, 0x00], // 070
317    [0x48, 0xFF, 0xDA], // 276
318    [0x00, 0xFF, 0xFF], // 077
319    [0x00, 0x00, 0x00], // 000
320    [0x00, 0x00, 0x00], // 000
321    [0x00, 0x00, 0x00], // 000
322    [0xFF, 0xFF, 0xFF], // 777
323    [0xB6, 0xDA, 0xFF], // 567
324    [0xDA, 0xB6, 0xFF], // 657
325    [0xFF, 0xB6, 0xFF], // 757
326    [0xFF, 0x91, 0xFF], // 747
327    [0xFF, 0xB6, 0xB6], // 755
328    [0xFF, 0xDA, 0x91], // 764
329    [0xFF, 0xFF, 0x48], // 772
330    [0xFF, 0xFF, 0x6D], // 773
331    [0xB6, 0xFF, 0x48], // 572
332    [0x91, 0xFF, 0x6D], // 473
333    [0x48, 0xFF, 0xDA], // 276
334    [0x91, 0xDA, 0xFF], // 467
335    [0x00, 0x00, 0x00], // 000
336    [0x00, 0x00, 0x00], // 000
337    [0x00, 0x00, 0x00], // 000
338];
339
340/// RP2C04-0001 RGB PPU palette (Vs. System). See [`RGB_2C03`] for the source.
341const RGB_2C04_0001: [[u8; 3]; 64] = [
342    [0xFF, 0xB6, 0xB6], // 755
343    [0xDA, 0x6D, 0xFF], // 637
344    [0xFF, 0x00, 0x00], // 700
345    [0x91, 0x91, 0xFF], // 447
346    [0x00, 0x91, 0x91], // 044
347    [0x24, 0x48, 0x00], // 120
348    [0x48, 0x48, 0x48], // 222
349    [0xFF, 0x00, 0x91], // 704
350    [0xFF, 0xFF, 0xFF], // 777
351    [0x6D, 0x6D, 0x6D], // 333
352    [0xFF, 0xB6, 0x00], // 750
353    [0xB6, 0x00, 0x6D], // 503
354    [0x91, 0x00, 0x6D], // 403
355    [0xDA, 0xDA, 0x00], // 660
356    [0x6D, 0x48, 0x00], // 320
357    [0xFF, 0xFF, 0xFF], // 777
358    [0x6D, 0xB6, 0xFF], // 357
359    [0xDA, 0xB6, 0x6D], // 653
360    [0x6D, 0x24, 0x00], // 310
361    [0x6D, 0xDA, 0x00], // 360
362    [0x91, 0xDA, 0xFF], // 467
363    [0xDA, 0xB6, 0xFF], // 657
364    [0xFF, 0xDA, 0x91], // 764
365    [0x00, 0x48, 0xFF], // 027
366    [0xFF, 0xDA, 0x00], // 760
367    [0x48, 0xFF, 0xDA], // 276
368    [0x00, 0x00, 0x00], // 000
369    [0x48, 0x00, 0x00], // 200
370    [0xDA, 0xDA, 0xDA], // 666
371    [0x91, 0x91, 0x91], // 444
372    [0xFF, 0x00, 0xFF], // 707
373    [0x00, 0x24, 0x91], // 014
374    [0x00, 0x00, 0x6D], // 003
375    [0xB6, 0xDA, 0xFF], // 567
376    [0xFF, 0xB6, 0xFF], // 757
377    [0x00, 0xFF, 0x00], // 070
378    [0x00, 0xFF, 0xFF], // 077
379    [0x00, 0x48, 0x48], // 022
380    [0x00, 0xB6, 0x6D], // 053
381    [0xB6, 0x00, 0xFF], // 507
382    [0x00, 0x00, 0x00], // 000
383    [0x91, 0x48, 0x00], // 420
384    [0xFF, 0x91, 0xFF], // 747
385    [0xB6, 0x24, 0x00], // 510
386    [0x91, 0x00, 0xFF], // 407
387    [0x00, 0x00, 0xDA], // 006
388    [0xFF, 0x91, 0x00], // 740
389    [0x00, 0x00, 0x00], // 000
390    [0x00, 0x00, 0x00], // 000
391    [0x24, 0x91, 0x00], // 140
392    [0xB6, 0xB6, 0xB6], // 555
393    [0x00, 0x6D, 0x24], // 031
394    [0xB6, 0xFF, 0x48], // 572
395    [0x6D, 0x48, 0xDA], // 326
396    [0xFF, 0xFF, 0x00], // 770
397    [0xDA, 0x6D, 0x00], // 630
398    [0x00, 0x48, 0x00], // 020
399    [0x00, 0x6D, 0xDA], // 036
400    [0x00, 0x91, 0x00], // 040
401    [0x24, 0x24, 0x24], // 111
402    [0xFF, 0xFF, 0x6D], // 773
403    [0xFF, 0x6D, 0xFF], // 737
404    [0x91, 0x6D, 0x00], // 430
405    [0x91, 0xFF, 0x6D], // 473
406];
407
408/// RP2C04-0002 RGB PPU palette (Vs. System). See [`RGB_2C03`] for the source.
409const RGB_2C04_0002: [[u8; 3]; 64] = [
410    [0x00, 0x00, 0x00], // 000
411    [0xFF, 0xB6, 0x00], // 750
412    [0x91, 0x6D, 0x00], // 430
413    [0xB6, 0xFF, 0x48], // 572
414    [0x91, 0xFF, 0x6D], // 473
415    [0xFF, 0x6D, 0xFF], // 737
416    [0x00, 0x91, 0x91], // 044
417    [0xB6, 0xDA, 0xFF], // 567
418    [0xFF, 0x00, 0x00], // 700
419    [0x91, 0x00, 0xFF], // 407
420    [0xFF, 0xFF, 0x6D], // 773
421    [0xFF, 0x91, 0xFF], // 747
422    [0xFF, 0xFF, 0xFF], // 777
423    [0xDA, 0x6D, 0xFF], // 637
424    [0x91, 0xDA, 0xFF], // 467
425    [0x00, 0x91, 0x00], // 040
426    [0x00, 0x48, 0x00], // 020
427    [0x6D, 0xB6, 0xFF], // 357
428    [0xB6, 0x24, 0x00], // 510
429    [0xDA, 0xDA, 0xDA], // 666
430    [0x00, 0xB6, 0x6D], // 053
431    [0x6D, 0xDA, 0x00], // 360
432    [0x48, 0x00, 0x00], // 200
433    [0x91, 0x91, 0xFF], // 447
434    [0x48, 0x48, 0x48], // 222
435    [0xFF, 0x00, 0xFF], // 707
436    [0x00, 0x00, 0x6D], // 003
437    [0x48, 0xFF, 0xDA], // 276
438    [0xDA, 0xB6, 0xFF], // 657
439    [0x6D, 0x48, 0x00], // 320
440    [0x00, 0x00, 0x00], // 000
441    [0x6D, 0x48, 0xDA], // 326
442    [0x91, 0x00, 0x6D], // 403
443    [0xFF, 0xDA, 0x91], // 764
444    [0xFF, 0x91, 0x00], // 740
445    [0xFF, 0xB6, 0xFF], // 757
446    [0x00, 0x6D, 0xDA], // 036
447    [0x6D, 0x24, 0x00], // 310
448    [0xB6, 0xB6, 0xB6], // 555
449    [0x00, 0x00, 0xDA], // 006
450    [0xB6, 0x00, 0xFF], // 507
451    [0xFF, 0xDA, 0x00], // 760
452    [0x6D, 0x6D, 0x6D], // 333
453    [0x24, 0x48, 0x00], // 120
454    [0x00, 0x48, 0xFF], // 027
455    [0x00, 0x00, 0x00], // 000
456    [0xDA, 0xDA, 0x00], // 660
457    [0xFF, 0xFF, 0xFF], // 777
458    [0xDA, 0xB6, 0x6D], // 653
459    [0x24, 0x24, 0x24], // 111
460    [0x00, 0xFF, 0x00], // 070
461    [0xDA, 0x6D, 0x00], // 630
462    [0x00, 0x48, 0x48], // 022
463    [0x00, 0x24, 0x91], // 014
464    [0xFF, 0x00, 0x91], // 704
465    [0x24, 0x91, 0x00], // 140
466    [0x00, 0x00, 0x00], // 000
467    [0x00, 0xFF, 0xFF], // 077
468    [0x91, 0x48, 0x00], // 420
469    [0xFF, 0xFF, 0x00], // 770
470    [0xFF, 0xB6, 0xB6], // 755
471    [0xB6, 0x00, 0x6D], // 503
472    [0x00, 0x6D, 0x24], // 031
473    [0x91, 0x91, 0x91], // 444
474];
475
476/// RP2C04-0003 RGB PPU palette (Vs. System). See [`RGB_2C03`] for the source.
477const RGB_2C04_0003: [[u8; 3]; 64] = [
478    [0xB6, 0x00, 0xFF], // 507
479    [0xFF, 0x6D, 0xFF], // 737
480    [0x91, 0xFF, 0x6D], // 473
481    [0xB6, 0xB6, 0xB6], // 555
482    [0x00, 0x91, 0x00], // 040
483    [0xFF, 0xFF, 0xFF], // 777
484    [0xB6, 0xDA, 0xFF], // 567
485    [0x24, 0x48, 0x00], // 120
486    [0x00, 0x24, 0x91], // 014
487    [0x00, 0x00, 0x00], // 000
488    [0xFF, 0xDA, 0x91], // 764
489    [0x6D, 0x48, 0x00], // 320
490    [0xFF, 0x00, 0x91], // 704
491    [0xDA, 0xDA, 0xDA], // 666
492    [0xDA, 0xB6, 0x6D], // 653
493    [0x91, 0xDA, 0xFF], // 467
494    [0x91, 0x91, 0xFF], // 447
495    [0x00, 0x91, 0x91], // 044
496    [0xB6, 0x00, 0x6D], // 503
497    [0x00, 0x48, 0xFF], // 027
498    [0x24, 0x91, 0x00], // 140
499    [0x91, 0x6D, 0x00], // 430
500    [0xDA, 0x6D, 0x00], // 630
501    [0x00, 0xB6, 0x6D], // 053
502    [0x6D, 0x6D, 0x6D], // 333
503    [0x6D, 0x48, 0xDA], // 326
504    [0x00, 0x00, 0x00], // 000
505    [0x00, 0x00, 0xDA], // 006
506    [0xFF, 0x00, 0x00], // 700
507    [0xB6, 0x24, 0x00], // 510
508    [0xFF, 0x91, 0xFF], // 747
509    [0xFF, 0xB6, 0xB6], // 755
510    [0xDA, 0x6D, 0xFF], // 637
511    [0x00, 0x48, 0x00], // 020
512    [0x00, 0x00, 0x6D], // 003
513    [0xFF, 0xFF, 0x00], // 770
514    [0x24, 0x24, 0x24], // 111
515    [0xFF, 0xB6, 0x00], // 750
516    [0xFF, 0x91, 0x00], // 740
517    [0xFF, 0xFF, 0xFF], // 777
518    [0x6D, 0xDA, 0x00], // 360
519    [0x91, 0x00, 0x6D], // 403
520    [0x6D, 0xB6, 0xFF], // 357
521    [0xFF, 0x00, 0xFF], // 707
522    [0x00, 0x6D, 0xDA], // 036
523    [0x91, 0x91, 0x91], // 444
524    [0x00, 0x00, 0x00], // 000
525    [0x6D, 0x24, 0x00], // 310
526    [0x00, 0xFF, 0xFF], // 077
527    [0x48, 0x00, 0x00], // 200
528    [0xB6, 0xFF, 0x48], // 572
529    [0xFF, 0xB6, 0xFF], // 757
530    [0x91, 0x48, 0x00], // 420
531    [0x00, 0xFF, 0x00], // 070
532    [0xDA, 0xDA, 0x00], // 660
533    [0x48, 0x48, 0x48], // 222
534    [0x00, 0x6D, 0x24], // 031
535    [0x00, 0x00, 0x00], // 000
536    [0xDA, 0xB6, 0xFF], // 657
537    [0xFF, 0xFF, 0x6D], // 773
538    [0x91, 0x00, 0xFF], // 407
539    [0x48, 0xFF, 0xDA], // 276
540    [0xFF, 0xDA, 0x00], // 760
541    [0x00, 0x48, 0x48], // 022
542];
543
544/// RP2C04-0004 RGB PPU palette (Vs. System). See [`RGB_2C03`] for the source.
545const RGB_2C04_0004: [[u8; 3]; 64] = [
546    [0x91, 0x6D, 0x00], // 430
547    [0x6D, 0x48, 0xDA], // 326
548    [0x00, 0x91, 0x91], // 044
549    [0xDA, 0xDA, 0x00], // 660
550    [0x00, 0x00, 0x00], // 000
551    [0xFF, 0xB6, 0xB6], // 755
552    [0x00, 0x24, 0x91], // 014
553    [0xDA, 0x6D, 0x00], // 630
554    [0xB6, 0xB6, 0xB6], // 555
555    [0x6D, 0x24, 0x00], // 310
556    [0x00, 0xFF, 0x00], // 070
557    [0x00, 0x00, 0x6D], // 003
558    [0xFF, 0xDA, 0x91], // 764
559    [0xFF, 0xFF, 0x00], // 770
560    [0x00, 0x91, 0x00], // 040
561    [0xB6, 0xFF, 0x48], // 572
562    [0xFF, 0x6D, 0xFF], // 737
563    [0x48, 0x00, 0x00], // 200
564    [0x00, 0x48, 0xFF], // 027
565    [0xFF, 0x91, 0xFF], // 747
566    [0x00, 0x00, 0x00], // 000
567    [0x48, 0x48, 0x48], // 222
568    [0xB6, 0x24, 0x00], // 510
569    [0xFF, 0x91, 0x00], // 740
570    [0xDA, 0xB6, 0x6D], // 653
571    [0x00, 0xB6, 0x6D], // 053
572    [0x91, 0x91, 0xFF], // 447
573    [0x24, 0x91, 0x00], // 140
574    [0x91, 0x00, 0x6D], // 403
575    [0x00, 0x00, 0x00], // 000
576    [0x91, 0xFF, 0x6D], // 473
577    [0x6D, 0xB6, 0xFF], // 357
578    [0xB6, 0x00, 0x6D], // 503
579    [0x00, 0x6D, 0x24], // 031
580    [0x91, 0x48, 0x00], // 420
581    [0x00, 0x00, 0xDA], // 006
582    [0x91, 0x00, 0xFF], // 407
583    [0xB6, 0x00, 0xFF], // 507
584    [0x6D, 0x6D, 0x6D], // 333
585    [0xFF, 0x00, 0x91], // 704
586    [0x00, 0x48, 0x48], // 022
587    [0xDA, 0xDA, 0xDA], // 666
588    [0x00, 0x6D, 0xDA], // 036
589    [0x00, 0x48, 0x00], // 020
590    [0x24, 0x24, 0x24], // 111
591    [0xFF, 0xFF, 0x6D], // 773
592    [0x91, 0x91, 0x91], // 444
593    [0xFF, 0x00, 0xFF], // 707
594    [0xFF, 0xB6, 0xFF], // 757
595    [0xFF, 0xFF, 0xFF], // 777
596    [0x6D, 0x48, 0x00], // 320
597    [0xFF, 0x00, 0x00], // 700
598    [0xFF, 0xDA, 0x00], // 760
599    [0x48, 0xFF, 0xDA], // 276
600    [0xFF, 0xFF, 0xFF], // 777
601    [0x91, 0xDA, 0xFF], // 467
602    [0x00, 0x00, 0x00], // 000
603    [0xFF, 0xB6, 0x00], // 750
604    [0xDA, 0x6D, 0xFF], // 637
605    [0xB6, 0xDA, 0xFF], // 567
606    [0x6D, 0xDA, 0x00], // 360
607    [0xDA, 0xB6, 0xFF], // 657
608    [0x00, 0xFF, 0xFF], // 077
609    [0x24, 0x48, 0x00], // 120
610];
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615
616    #[test]
617    fn custom_base_lut_matches_builtin_composite() {
618        // A custom palette equal to the built-in NES_PALETTE must produce the
619        // exact same 512-entry LUT as the default composite path — proving the
620        // `.pal` custom-palette route is a faithful drop-in (so loading the
621        // built-in colours is byte-identical, and AccuracyCoin/oracle-safe).
622        let from_base = build_rgba_lut_from_base(&NES_PALETTE);
623        let builtin = build_rgba_lut(PpuPalette::Composite2C02);
624        assert_eq!(from_base, builtin);
625    }
626
627    #[test]
628    fn nes_color_indices_round_trip() {
629        // Index 0x0F = $0F = black.
630        let [r, g, b, a] = nes_color_to_rgba(0x0F);
631        assert_eq!(a, 0xFF);
632        assert_eq!((r, g, b), (0, 0, 0));
633    }
634
635    #[test]
636    fn nes_color_index_30_is_white() {
637        let [r, g, b, _] = nes_color_to_rgba(0x30);
638        assert_eq!((r, g, b), (0xFF, 0xFF, 0xFF));
639    }
640
641    #[test]
642    fn emphasis_dims_other_channels() {
643        let base = nes_color_to_rgba(0x30);
644        let red = apply_documented_emphasis(base, 0x30, true, false, false);
645        assert_eq!(red[0], base[0]);
646        assert!(red[1] <= base[1]);
647        assert!(red[2] <= base[2]);
648    }
649
650    #[test]
651    fn composite_palette_is_fbx_plus_the_documented_emphasis() {
652        // v2.9.8: the default PpuPalette path is FBX with the documented
653        // emphasis change added, for every index and emphasis; and with no
654        // emphasis it is FBX exactly, so an un-emphasised frame did not change.
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 expected = apply_documented_emphasis(nes_color_to_rgba(idx), idx, r, g, b);
659                let routed = palette_color_to_rgba(PpuPalette::Composite2C02, idx, r, g, b);
660                assert_eq!(expected, routed, "idx={idx} emask={emask}");
661                if emask == 0 {
662                    assert_eq!(routed, nes_color_to_rgba(idx), "idx={idx}");
663                }
664            }
665        }
666    }
667
668    #[test]
669    fn rgb_2c03_documented_entries() {
670        // nesdev "PPU palettes" 2C03/2C05 table, C = 255*DAC/7 truncation.
671        // $00 = 333 = #6D6D6D, $01 = 014 = #002491, $0F = 000 = #000000,
672        // $20 = 777 = #FFFFFF, $16 = 700 = #FF0000.
673        let none = (false, false, false);
674        assert_eq!(
675            palette_color_to_rgba(PpuPalette::Rgb2C03, 0x00, none.0, none.1, none.2),
676            [0x6D, 0x6D, 0x6D, 0xFF]
677        );
678        assert_eq!(
679            palette_color_to_rgba(PpuPalette::Rgb2C03, 0x01, none.0, none.1, none.2),
680            [0x00, 0x24, 0x91, 0xFF]
681        );
682        assert_eq!(
683            palette_color_to_rgba(PpuPalette::Rgb2C03, 0x0F, none.0, none.1, none.2),
684            [0x00, 0x00, 0x00, 0xFF]
685        );
686        assert_eq!(
687            palette_color_to_rgba(PpuPalette::Rgb2C03, 0x20, none.0, none.1, none.2),
688            [0xFF, 0xFF, 0xFF, 0xFF]
689        );
690        assert_eq!(
691            palette_color_to_rgba(PpuPalette::Rgb2C03, 0x16, none.0, none.1, none.2),
692            [0xFF, 0x00, 0x00, 0xFF]
693        );
694    }
695
696    #[test]
697    fn rgb_2c05_shares_2c03_table() {
698        for idx in 0u8..64 {
699            assert_eq!(
700                palette_color_to_rgba(PpuPalette::Rgb2C05, idx, false, false, false),
701                palette_color_to_rgba(PpuPalette::Rgb2C03, idx, false, false, false),
702                "idx={idx}"
703            );
704        }
705    }
706
707    #[test]
708    fn rgb_2c04_first_entries_match_wiki() {
709        let none = (false, false, false);
710        // First entry of each 2C04 permutation (index $00).
711        assert_eq!(
712            palette_color_to_rgba(PpuPalette::Rgb2C04_0001, 0x00, none.0, none.1, none.2),
713            [0xFF, 0xB6, 0xB6, 0xFF] // 755
714        );
715        assert_eq!(
716            palette_color_to_rgba(PpuPalette::Rgb2C04_0002, 0x00, none.0, none.1, none.2),
717            [0x00, 0x00, 0x00, 0xFF] // 000
718        );
719        assert_eq!(
720            palette_color_to_rgba(PpuPalette::Rgb2C04_0003, 0x00, none.0, none.1, none.2),
721            [0xB6, 0x00, 0xFF, 0xFF] // 507
722        );
723        assert_eq!(
724            palette_color_to_rgba(PpuPalette::Rgb2C04_0004, 0x00, none.0, none.1, none.2),
725            [0x91, 0x6D, 0x00, 0xFF] // 430
726        );
727    }
728
729    #[test]
730    fn rgb_emphasis_forces_channel_full_brightness() {
731        // RGB PPUs: an emphasis bit forces ITS channel to 0xFF (opposite of
732        // the 2C02's darkening). Start from index $00 = #6D6D6D on the 2C03.
733        let base = palette_color_to_rgba(PpuPalette::Rgb2C03, 0x00, false, false, false);
734        assert_eq!(base, [0x6D, 0x6D, 0x6D, 0xFF]);
735        let red = palette_color_to_rgba(PpuPalette::Rgb2C03, 0x00, true, false, false);
736        assert_eq!(red, [0xFF, 0x6D, 0x6D, 0xFF]);
737        let green = palette_color_to_rgba(PpuPalette::Rgb2C03, 0x00, false, true, false);
738        assert_eq!(green, [0x6D, 0xFF, 0x6D, 0xFF]);
739        let blue = palette_color_to_rgba(PpuPalette::Rgb2C03, 0x00, false, false, true);
740        assert_eq!(blue, [0x6D, 0x6D, 0xFF, 0xFF]);
741        let all = palette_color_to_rgba(PpuPalette::Rgb2C03, 0x00, true, true, true);
742        assert_eq!(all, [0xFF, 0xFF, 0xFF, 0xFF]);
743    }
744
745    #[test]
746    fn composite_default_and_is_composite() {
747        assert_eq!(PpuPalette::default(), PpuPalette::Composite2C02);
748        assert!(PpuPalette::Composite2C02.is_composite());
749        assert!(!PpuPalette::Rgb2C03.is_composite());
750        assert!(!PpuPalette::Rgb2C05.is_composite());
751    }
752
753    /// v2.8.0 Phase 4 — the pixel-emit LUT must equal the direct
754    /// computation for EVERY (palette, emphasis, color) combination; the
755    /// PPU's per-pixel path depends on this byte-for-byte.
756    #[test]
757    fn rgba_lut_matches_direct_computation_exhaustively() {
758        for palette in [
759            PpuPalette::Composite2C02,
760            PpuPalette::Rgb2C03,
761            PpuPalette::Rgb2C04_0001,
762            PpuPalette::Rgb2C04_0002,
763            PpuPalette::Rgb2C04_0003,
764            PpuPalette::Rgb2C04_0004,
765            PpuPalette::Rgb2C05,
766        ] {
767            let lut = build_rgba_lut(palette);
768            for e in 0..8usize {
769                for c in 0..64usize {
770                    #[allow(clippy::cast_possible_truncation)]
771                    let direct =
772                        palette_color_to_rgba(palette, c as u8, e & 1 != 0, e & 2 != 0, e & 4 != 0);
773                    assert_eq!(lut[(e << 6) | c], direct, "{palette:?} e={e} c={c}");
774                }
775            }
776        }
777    }
778}