#[rustfmt::skip]
pub static NES_PALETTE: [[u8; 3]; 512] = [
[ 85, 85, 85], [ 0, 31, 111], [ 10, 13, 146], [ 40, 1, 146],
[ 70, 0, 111], [ 89, 0, 53], [ 89, 3, 1], [ 70, 18, 0],
[ 40, 37, 0], [ 10, 54, 0], [ 0, 63, 0], [ 0, 62, 1],
[ 0, 50, 53], [ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0],
[162, 162, 162], [ 12, 81, 200], [ 47, 52, 252], [ 95, 30, 252],
[140, 19, 200], [168, 21, 115], [168, 35, 30], [140, 60, 0],
[ 95, 90, 0], [ 47, 116, 0], [ 12, 130, 0], [ 0, 127, 30],
[ 0, 109, 115], [ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0],
[255, 255, 255], [ 81, 166, 255], [126, 132, 255], [181, 105, 255],
[232, 91, 255], [255, 93, 204], [255, 111, 105], [232, 141, 31],
[181, 175, 0], [126, 204, 0], [ 81, 220, 31], [ 57, 217, 105],
[ 57, 197, 204], [ 62, 62, 62], [ 0, 0, 0], [ 0, 0, 0],
[255, 255, 255], [179, 217, 255], [200, 203, 255], [224, 190, 255],
[245, 184, 255], [255, 185, 234], [255, 193, 191], [245, 207, 155],
[224, 221, 135], [200, 234, 135], [179, 240, 155], [168, 239, 191],
[168, 231, 234], [170, 170, 170], [ 0, 0, 0], [ 0, 0, 0],
[ 86, 51, 49], [ 0, 8, 69], [ 11, 0, 101], [ 39, 0, 103],
[ 68, 0, 77], [ 87, 0, 33], [ 89, 0, 0], [ 70, 6, 0],
[ 42, 18, 0], [ 13, 29, 0], [ 0, 34, 0], [ 0, 31, 0],
[ 0, 19, 21], [ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0],
[164, 109, 107], [ 13, 42, 136], [ 48, 23, 183], [ 93, 9, 187],
[137, 4, 148], [165, 7, 82], [169, 21, 16], [141, 38, 0],
[ 97, 59, 0], [ 51, 77, 0], [ 16, 84, 0], [ 0, 79, 3],
[ 0, 61, 64], [ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0],
[255, 181, 178], [ 84, 105, 210], [127, 81, 255], [180, 63, 255],
[228, 56, 223], [255, 61, 150], [255, 79, 72], [233, 100, 10],
[184, 125, 0], [131, 145, 0], [ 88, 153, 3], [ 63, 147, 55],
[ 59, 127, 130], [ 63, 34, 33], [ 0, 0, 0], [ 0, 0, 0],
[255, 181, 178], [183, 149, 191], [203, 138, 211], [226, 130, 213],
[246, 127, 196], [255, 129, 166], [255, 137, 132], [248, 147, 101],
[227, 157, 84], [204, 166, 82], [185, 169, 97], [173, 167, 124],
[171, 158, 158], [173, 116, 113], [ 0, 0, 0], [ 0, 0, 0],
[ 43, 77, 30], [ 0, 26, 61], [ 0, 9, 86], [ 11, 0, 83],
[ 31, 0, 53], [ 47, 0, 13], [ 50, 1, 0], [ 38, 14, 0],
[ 17, 33, 0], [ 0, 50, 0], [ 0, 61, 0], [ 0, 57, 0],
[ 0, 44, 19], [ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0],
[ 97, 149, 77], [ 0, 72, 123], [ 13, 44, 161], [ 46, 22, 157],
[ 78, 10, 111], [104, 15, 49], [108, 29, 0], [ 89, 54, 0],
[ 56, 83, 0], [ 22, 110, 0], [ 0, 126, 0], [ 0, 120, 1],
[ 0, 100, 59], [ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0],
[164, 237, 135], [ 35, 152, 185], [ 66, 119, 226], [106, 92, 222],
[143, 76, 173], [171, 82, 103], [175, 101, 32], [155, 131, 0],
[118, 164, 0], [ 77, 194, 0], [ 45, 212, 0], [ 23, 205, 42],
[ 20, 183, 115], [ 28, 55, 17], [ 0, 0, 0], [ 0, 0, 0],
[164, 237, 135], [107, 201, 155], [122, 187, 172], [140, 175, 170],
[155, 168, 150], [167, 170, 122], [169, 179, 90], [160, 192, 64],
[145, 207, 50], [127, 219, 52], [112, 227, 68], [101, 224, 95],
[ 99, 215, 127], [104, 157, 82], [ 0, 0, 0], [ 0, 0, 0],
[ 49, 48, 26], [ 0, 7, 51], [ 0, 0, 75], [ 13, 0, 75],
[ 32, 0, 51], [ 49, 0, 11], [ 52, 0, 0], [ 39, 4, 0],
[ 19, 16, 0], [ 0, 27, 0], [ 0, 34, 0], [ 0, 30, 0],
[ 0, 19, 15], [ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0],
[106, 105, 69], [ 0, 41, 108], [ 19, 22, 144], [ 50, 8, 144],
[ 81, 2, 108], [107, 5, 46], [111, 18, 0], [ 92, 35, 0],
[ 60, 55, 0], [ 27, 73, 0], [ 4, 83, 0], [ 0, 78, 0],
[ 0, 60, 52], [ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0],
[177, 175, 124], [ 45, 102, 167], [ 76, 79, 205], [114, 61, 205],
[149, 51, 167], [177, 56, 98], [182, 75, 28], [160, 95, 0],
[125, 119, 0], [ 86, 140, 0], [ 55, 151, 0], [ 32, 144, 33],
[ 29, 124, 105], [ 32, 32, 13], [ 0, 0, 0], [ 0, 0, 0],
[177, 175, 124], [119, 144, 142], [133, 134, 157], [150, 126, 157],
[165, 121, 142], [177, 124, 113], [179, 132, 82], [170, 141, 59],
[155, 152, 46], [138, 160, 46], [124, 165, 59], [113, 162, 85],
[111, 154, 116], [113, 112, 74], [ 0, 0, 0], [ 0, 0, 0],
[ 54, 56, 107], [ 0, 19, 120], [ 4, 6, 154], [ 25, 0, 151],
[ 47, 0, 117], [ 60, 0, 63], [ 57, 0, 11], [ 40, 1, 0],
[ 14, 13, 0], [ 0, 28, 0], [ 0, 38, 0], [ 0, 39, 13],
[ 0, 31, 66], [ 0, 0, 0], [ 0, 0, 5], [ 0, 0, 5],
[115, 117, 196], [ 3, 60, 214], [ 34, 39, 255], [ 70, 18, 255],
[104, 7, 210], [123, 6, 131], [119, 14, 50], [ 93, 30, 0],
[ 53, 50, 0], [ 19, 74, 0], [ 0, 89, 0], [ 0, 91, 53],
[ 0, 80, 135], [ 0, 0, 5], [ 0, 0, 5], [ 0, 0, 5],
[189, 192, 255], [ 56, 128, 255], [ 97, 104, 255], [139, 78, 255],
[177, 63, 255], [198, 61, 231], [194, 72, 138], [165, 92, 65],
[119, 116, 27], [ 78, 144, 30], [ 46, 161, 69], [ 30, 163, 142],
[ 33, 151, 236], [ 37, 38, 81], [ 0, 0, 5], [ 0, 0, 5],
[189, 192, 255], [131, 165, 255], [150, 155, 255], [168, 143, 255],
[184, 136, 255], [193, 135, 255], [191, 140, 233], [179, 149, 198],
[159, 160, 178], [141, 172, 180], [126, 179, 200], [118, 180, 234],
[120, 175, 255], [122, 124, 206], [ 0, 0, 5], [ 0, 0, 5],
[ 54, 38, 61], [ 0, 5, 73], [ 4, 0, 105], [ 24, 0, 105],
[ 45, 0, 80], [ 57, 0, 39], [ 57, 0, 2], [ 40, 0, 0],
[ 14, 8, 0], [ 0, 19, 0], [ 0, 25, 0], [ 0, 24, 0],
[ 0, 16, 25], [ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0],
[115, 90, 126], [ 3, 36, 143], [ 34, 17, 190], [ 68, 4, 190],
[100, 0, 153], [119, 0, 93], [119, 7, 31], [ 93, 22, 0],
[ 53, 42, 0], [ 21, 59, 0], [ 0, 68, 0], [ 0, 67, 13],
[ 0, 55, 70], [ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0],
[189, 153, 205], [ 56, 93, 223], [ 97, 70, 255], [137, 52, 255],
[173, 42, 234], [194, 44, 168], [194, 56, 96], [165, 76, 29],
[119, 99, 0], [ 81, 119, 0], [ 50, 130, 21], [ 33, 128, 73],
[ 33, 114, 142], [ 37, 24, 43], [ 0, 0, 0], [ 0, 0, 0],
[189, 153, 205], [131, 128, 212], [150, 118, 233], [167, 109, 233],
[182, 105, 217], [191, 106, 189], [191, 111, 158], [179, 120, 126],
[160, 131, 107], [143, 139, 107], [128, 144, 122], [120, 143, 148],
[120, 137, 178], [122, 96, 133], [ 0, 0, 0], [ 0, 0, 0],
[ 34, 51, 51], [ 0, 15, 69], [ 0, 3, 94], [ 8, 0, 91],
[ 28, 0, 60], [ 40, 0, 22], [ 40, 0, 0], [ 28, 1, 0],
[ 9, 11, 0], [ 0, 26, 0], [ 0, 36, 0], [ 0, 35, 0],
[ 0, 27, 30], [ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0],
[ 83, 109, 110], [ 0, 53, 137], [ 10, 33, 173], [ 42, 12, 169],
[ 73, 2, 123], [ 92, 3, 65], [ 92, 12, 9], [ 73, 28, 0],
[ 43, 47, 0], [ 11, 72, 0], [ 0, 87, 0], [ 0, 85, 18],
[ 0, 73, 78], [ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0],
[144, 181, 182], [ 29, 118, 212], [ 57, 94, 251], [ 97, 68, 247],
[133, 53, 196], [153, 55, 132], [153, 67, 64], [133, 88, 14],
[ 99, 111, 0], [ 59, 139, 0], [ 29, 156, 24], [ 14, 154, 77],
[ 14, 140, 146], [ 20, 34, 34], [ 0, 0, 0], [ 0, 0, 0],
[144, 181, 182], [ 93, 154, 194], [107, 144, 210], [124, 132, 208],
[139, 125, 188], [148, 126, 161], [148, 132, 131], [139, 141, 106],
[125, 151, 92], [107, 163, 93], [ 93, 171, 112], [ 85, 170, 137],
[ 85, 164, 167], [ 89, 116, 117], [ 0, 0, 0], [ 0, 0, 0],
[ 38, 38, 38], [ 0, 5, 56], [ 0, 0, 79], [ 10, 0, 79],
[ 29, 0, 56], [ 41, 0, 18], [ 41, 0, 0], [ 29, 0, 0],
[ 10, 8, 0], [ 0, 19, 0], [ 0, 25, 0], [ 0, 24, 0],
[ 0, 16, 18], [ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0],
[ 90, 90, 90], [ 0, 36, 116], [ 14, 17, 151], [ 45, 4, 151],
[ 76, 0, 116], [ 94, 0, 59], [ 94, 7, 4], [ 76, 22, 0],
[ 45, 42, 0], [ 14, 59, 0], [ 0, 68, 0], [ 0, 67, 4],
[ 0, 55, 59], [ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0],
[153, 153, 153], [ 36, 92, 182], [ 66, 70, 221], [103, 52, 221],
[138, 42, 182], [158, 44, 118], [158, 56, 52], [137, 76, 5],
[103, 99, 0], [ 66, 119, 0], [ 36, 130, 5], [ 20, 128, 52],
[ 20, 114, 118], [ 24, 24, 24], [ 0, 0, 0], [ 0, 0, 0],
[153, 153, 153], [102, 128, 165], [116, 118, 181], [132, 109, 181],
[147, 105, 165], [155, 106, 139], [155, 111, 110], [147, 120, 85],
[132, 130, 72], [116, 139, 72], [102, 143, 85], [ 94, 143, 110],
[ 94, 137, 139], [ 96, 96, 96], [ 0, 0, 0], [ 0, 0, 0],
];
pub const NES_PALETTE_LEN: usize = 512;
#[inline]
#[must_use]
pub fn nes_rgb(pixel: u16) -> [u8; 3] {
NES_PALETTE[usize::from(pixel & 0x01ff)]
}
#[inline]
#[must_use]
pub fn nes_rgb_parts(index: u8, emphasis: u8) -> [u8; 3] {
nes_rgb((u16::from(emphasis & 0x07) << 6) | u16::from(index & 0x3f))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_named_colours_are_where_the_docs_say() {
assert_eq!(nes_rgb(0x00), [85, 85, 85]);
assert_eq!(nes_rgb(0x0f), [0, 0, 0]);
assert_eq!(nes_rgb(0x11), [12, 81, 200]);
assert_eq!(nes_rgb(0x16), [168, 35, 30]);
assert_eq!(nes_rgb(0x30), [255, 255, 255]);
}
#[test]
fn the_forced_blacks_are_black_everywhere() {
for emphasis in 0..8u8 {
for row in 0..4u8 {
for hue in [0x0e, 0x0f] {
let index = row * 0x10 + hue;
let rgb = nes_rgb_parts(index, emphasis);
let bound = if emphasis == 0 { 0 } else { 8 };
assert!(
rgb.iter().all(|c| *c <= bound),
"index {index:#04x} emphasis {emphasis:03b} is {rgb:?}"
);
}
}
}
}
#[test]
fn emphasis_only_ever_takes_light_away() {
for index in 0..64u8 {
let plain = nes_rgb_parts(index, 0);
let all = nes_rgb_parts(index, 0b111);
for channel in 0..3 {
assert!(
all[channel] <= plain[channel],
"index {index:#04x} channel {channel} went from {} to {}",
plain[channel],
all[channel]
);
}
}
}
#[cfg(feature = "std")]
#[test]
fn the_table_is_what_the_signal_model_produces() {
const LEVELS: [f64; 8] = [0.350, 0.518, 0.962, 1.550, 1.094, 1.506, 1.962, 1.962];
const ATTENUATION: f64 = 0.746;
const PHASE_OFFSET: f64 = 4.1;
const CHROMA_GAIN: f64 = 1.5;
const GAMMA: f64 = 1.2;
fn in_phase(hue: i32, phase: i32) -> bool {
(hue + phase).rem_euclid(12) < 6
}
fn signal(pixel: u16, phase: i32) -> f64 {
let hue = i32::from(pixel & 0x0f);
let emphasis = (pixel >> 6) & 0x07;
let mut row = usize::from((pixel >> 4) & 0x03);
if hue > 13 {
row = 1; }
let mut low = LEVELS[row];
let mut high = LEVELS[4 + row];
if hue == 0 {
low = high; }
if hue > 12 {
high = low; }
let mut level = if in_phase(hue, phase) { high } else { low };
if (emphasis & 1 != 0 && in_phase(0, phase))
|| (emphasis & 2 != 0 && in_phase(4, phase))
|| (emphasis & 4 != 0 && in_phase(8, phase))
{
level *= ATTENUATION;
}
(level - LEVELS[1]) / (LEVELS[7] - LEVELS[1])
}
fn channel(v: f64) -> u8 {
let v = if v <= 0.0 { 0.0 } else { v.powf(GAMMA) } * 255.0;
if v < 0.0 {
0
} else if v > 255.0 {
255
} else {
(v + 0.5) as u8
}
}
for pixel in 0..NES_PALETTE_LEN {
let pixel = pixel as u16;
let (mut y, mut i, mut q) = (0.0f64, 0.0f64, 0.0f64);
for phase in 0..12 {
let s = signal(pixel, phase);
let angle = core::f64::consts::PI * (f64::from(phase) + PHASE_OFFSET) / 6.0;
y += s;
i += s * angle.cos();
q += s * angle.sin();
}
y /= 12.0;
i = i * CHROMA_GAIN / 12.0;
q = q * CHROMA_GAIN / 12.0;
let want = [
channel(y + 0.956 * i + 0.621 * q),
channel(y - 0.272 * i - 0.647 * q),
channel(y - 1.106 * i + 1.703 * q),
];
let got = NES_PALETTE[usize::from(pixel)];
for c in 0..3 {
assert!(
got[c].abs_diff(want[c]) <= 1,
"pixel {pixel:#05x} channel {c}: table {got:?}, model {want:?}"
);
}
}
}
}