lib.rsannotatedlib.rssource222 lines · 6.1 KB · raw
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 {

Load a 512-color or 64-color palette from a file.

64-color palettes will be extrapolated to 512 colors.

Errors

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    }
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    }

Write palette to a file.

Errors

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}
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}