1pub mod palettes; 2 3#[cfg(feature = "serde")] 4mod serialization; 5 6use crate::palettes::PaletteGenerationArgs; 7use bincode::{Decode, Encode}; 8use jgenesis_common::define_controller_inputs; 9use jgenesis_common::frontend::FiniteF64; 10use jgenesis_proc_macros::{EnumAll, EnumDisplay}; 11use std::error::Error; 12use std::fmt::{Display, Formatter}; 13use std::fs::File; 14use std::io::{BufReader, BufWriter, Read, Write}; 15use std::ops::Index; 16use std::path::Path; 17use std::sync::LazyLock; 18use std::{array, io}; 19 20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode, EnumDisplay, EnumAll)] 21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 22#[cfg_attr(feature = "clap", derive(jgenesis_proc_macros::CustomValueEnum))] 23pub enum NesAspectRatio { 24 #[default] 25 Ntsc, 26 Pal, 27 SquarePixels, 28 Stretched, 29} 30 31impl NesAspectRatio { 32 #[inline] 33 #[must_use] 34 pub fn to_pixel_aspect_ratio_f64(self) -> Option<f64> { 35 match self { 36 Self::Ntsc => Some(8.0 / 7.0), 37 Self::Pal => Some(11.0 / 8.0), 38 Self::SquarePixels => Some(1.0), 39 Self::Stretched => None, 40 } 41 } 42 43 #[inline] 44 #[must_use] 45 #[allow(clippy::missing_panics_doc)] 46 pub fn to_pixel_aspect_ratio(self) -> Option<FiniteF64> { 47 self.to_pixel_aspect_ratio_f64().map(|par| FiniteF64::try_from(par).unwrap()) 48 } 49} 50 51#[derive(Debug)] 52pub enum PaletteLoadError { 53 Io(io::Error), 54 IncorrectSize(u64), 55} 56 57impl Display for PaletteLoadError { 58 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 59 match self { 60 Self::Io(err) => write!(f, "I/O error loading palette: {err}"), 61 Self::IncorrectSize(len) => { 62 write!(f, "Incorrect palette size; expected {} bytes, was {len}", 512 * 3) 63 } 64 } 65 } 66} 67 68impl Error for PaletteLoadError { 69 fn source(&self) -> Option<&(dyn Error + 'static)> { 70 match self { 71 Self::Io(err) => Some(err), 72 Self::IncorrectSize(_) => None, 73 } 74 } 75} 76 77fn bytes_to_triples_array<const LEN: usize>(bytes: &[u8]) -> [(u8, u8, u8); LEN] { 78 array::from_fn(|i| (bytes[3 * i], bytes[3 * i + 1], bytes[3 * i + 2])) 79} 80 81#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 82pub struct NesPalette(pub [(u8, u8, u8); 512]); 83 84impl NesPalette { 85 /// Load a 512-color or 64-color palette from a file. 86 /// 87 /// 64-color palettes will be extrapolated to 512 colors. 88 /// 89 /// # Errors 90 /// 91 /// Returns an error if the file is too small or if there is an I/O error reading it. 92 pub fn read_from(path: &Path) -> Result<Self, PaletteLoadError> { 93 let file = File::open(path).map_err(PaletteLoadError::Io)?; 94 let metadata = file.metadata().map_err(PaletteLoadError::Io)?; 95 if metadata.len() < 64 * 3 { 96 return Err(PaletteLoadError::IncorrectSize(metadata.len())); 97 } 98 99 if metadata.len() < 512 * 3 { 100 // Assume 64-color palette 101 return Self::read_from_64_color(file); 102 } 103 104 let mut reader = BufReader::new(file); 105 let mut bytes = [0_u8; 512 * 3]; 106 reader.read_exact(&mut bytes).map_err(PaletteLoadError::Io)?; 107 108 Ok(Self(bytes_to_triples_array(&bytes))) 109 } 110 111 fn read_from_64_color(file: File) -> Result<Self, PaletteLoadError> { 112 let mut reader = BufReader::new(file); 113 let mut bytes = [0_u8; 64 * 3]; 114 reader.read_exact(&mut bytes).map_err(PaletteLoadError::Io)?; 115 116 let palette_64_color: [_; 64] = bytes_to_triples_array(&bytes); 117 Ok(palettes::extrapolate_64_to_512(&palette_64_color)) 118 } 119 120 /// Write palette to a file. 121 /// 122 /// # Errors 123 /// 124 /// Propagates any I/O errors encountered while creating or writing the file. 125 pub fn write_to(&self, path: &Path) -> io::Result<()> { 126 let file = File::create(path)?; 127 let mut writer = BufWriter::new(file); 128 129 for (r, g, b) in self.0 { 130 writer.write_all(&[r, g, b])?; 131 } 132 133 Ok(()) 134 } 135} 136 137impl Default for NesPalette { 138 fn default() -> Self { 139 static DEFAULT: LazyLock<NesPalette> = 140 LazyLock::new(|| palettes::generate(PaletteGenerationArgs::default())); 141 142 *DEFAULT 143 } 144} 145 146impl Index<usize> for NesPalette { 147 type Output = (u8, u8, u8); 148 149 fn index(&self, index: usize) -> &Self::Output { 150 &self.0[index] 151 } 152} 153 154#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 155#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 156pub struct Overscan { 157 pub top: u16, 158 pub bottom: u16, 159 pub left: u16, 160 pub right: u16, 161} 162 163impl Overscan { 164 pub const NONE: Self = Self { top: 0, bottom: 0, left: 0, right: 0 }; 165} 166 167impl Display for Overscan { 168 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 169 write!( 170 f, 171 "Overscan {{ top={}, bottom={}, left={}, right={} }}", 172 self.top, self.bottom, self.left, self.right 173 ) 174 } 175} 176 177#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode, EnumDisplay, EnumAll)] 178#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 179#[cfg_attr(feature = "clap", derive(jgenesis_proc_macros::CustomValueEnum))] 180pub enum NesAudioResampler { 181 LowPassNearestNeighbor, 182 #[default] 183 WindowedSinc, 184} 185 186define_controller_inputs! { 187 buttons: NesButton { 188 Up -> up "Up", 189 Left -> left "Left", 190 Right -> right "Right", 191 Down -> down "Down", 192 A -> a "A", 193 B -> b "B", 194 Start -> start "Start", 195 Select -> select "Select", 196 }, 197 non_gamepad_buttons: [ 198 ZapperFire "Fire", 199 ZapperForceOffscreen "Force offscreen (Hold)", 200 ], 201 joypad: NesJoypadState impl with_allow_opposing_directions, 202} 203 204impl NesButton { 205 #[inline] 206 #[must_use] 207 pub fn is_zapper(self) -> bool { 208 matches!(self, Self::ZapperFire | Self::ZapperForceOffscreen) 209 } 210} 211 212#[cfg(test)] 213mod tests { 214 use super::*; 215 216 #[test] 217 fn all_aspect_ratios_valid() { 218 for par in NesAspectRatio::ALL { 219 let _ = par.to_pixel_aspect_ratio(); 220 } 221 } 222}