From b91105bbd1af846edc5726ac1a058d559d2011fe Mon Sep 17 00:00:00 2001 From: John Date: Tue, 25 Jun 2024 13:36:04 -0500 Subject: [PATCH] Move graphics and audio out of lib.rs --- src/audio.rs | 134 ++++++++++++++++++++ src/graphics.rs | 232 ++++++++++++++++++++++++++++++++++ src/lib.rs | 328 +----------------------------------------------- 3 files changed, 368 insertions(+), 326 deletions(-) create mode 100644 src/audio.rs create mode 100644 src/graphics.rs diff --git a/src/audio.rs b/src/audio.rs new file mode 100644 index 0000000..9bc770e --- /dev/null +++ b/src/audio.rs @@ -0,0 +1,134 @@ +use crate::memory::io::BusIO; +use bitfield_struct::bitfield; + +#[derive(Debug, Default)] +pub struct APU { + regs: [u8; 0x20], + wave: [u8; 0x10], + amc: AMC, + pan: SndPan, + vol: Vol, +} + +impl BusIO for APU { + fn read(&self, addr: usize) -> Option { + match addr { + 0xff10..=0xff23 => self.regs.read(addr - 0xff10), + // 0xff10 => todo!("Channel 1 sweep"), + // 0xff11 => todo!("Channel 1 length timer & duty cycle"), + // 0xff12 => todo!("Channel 1 volume & envelope"), + // 0xff13 => todo!("Channel 1 period low [write-only]"), + // 0xff14 => todo!("Channel 1 period high & control [trigger, length enable, period]"), + 0xff24 => Some(self.vol.into_bits()), + 0xff25 => Some(self.pan.into_bits()), + 0xff26 => Some(self.amc.into_bits()), + 0xff30..=0xff3f => self.wave.read(addr - 0xff30), + 0xff76 => todo!("PCM12"), + 0xff77 => todo!("PCM34"), + _ => None, + } + } + + fn write(&mut self, addr: usize, data: u8) -> Option<()> { + #[allow(clippy::unit_arg)] + match addr { + 0xff10..0xff14 => todo!("Channel 1"), + 0xff16..=0xff19 => todo!("Channel 2"), + 0xff1a..=0xff1e => todo!("Channel 3"), + 0xff20..=0xff23 => todo!("Channel 4"), + 0xff24 => Some(self.vol = Vol::from_bits(data)), + // Sound panning + 0xff25 => Some(self.pan = SndPan::from_bits(data)), + // Audio Master Control + 0xff26 => Some(self.amc.set_power(data & 0x80 != 0)), + // Wave table memory + 0xff30..=0xff3f => self.wave.write(addr - 0xff30, data), + 0xff76 => todo!("PCM12"), + 0xff77 => todo!("PCM34"), + _ => None, + } + } +} + +#[derive(Debug, Default)] +pub struct Channel { + pub period: u16, + pub duty_step: u8, +} + +#[bitfield(u8)] +/// Audio Master Control register +pub struct AMC { + /// Set when CH1 is playing. Read-only. + ch1_on: bool, + /// Set when CH2 is playing. Read-only. + ch2_on: bool, + /// Set when CH3 is playing. Read-only. + ch3_on: bool, + /// Set when CH4 is playing. Read-only. + ch4_on: bool, + #[bits(3)] // 3 bits are unused + _reserved: (), + /// If false, all channels are muted. Read-writable. + power: bool, +} + +#[bitfield(u8)] +/// Sound Panning register. Setting `cNS` to true will send channel `N` to speaker `S` +pub struct SndPan { + c1r: bool, + c2r: bool, + c3r: bool, + c4r: bool, + c1l: bool, + c2l: bool, + c3l: bool, + c4l: bool, +} + +#[bitfield(u8)] +/// Volume control and VIN panning register +pub struct Vol { + #[bits(3)] + /// The volume of the right output + right: u8, + /// Whether VIN is sent to the right output + vin_right: bool, + #[bits(3)] + /// The volume of the left output + left: u8, + /// Whether VIN is sent to the left output + vin_left: bool, +} + +pub mod channels { + // TODO: remove + #![allow(dead_code)] + //! Channel controllers. Each channel controller has a function that takes a bus and outputs + + static WAVES: [[u8; 8]; 4] = [ + [0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0x0, 0xf], + [0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0x0, 0x0], + [0xf, 0xf, 0xf, 0xf, 0x0, 0x0, 0x0, 0x0], + [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf, 0xf], + ]; + + #[derive(Debug, Default)] + pub struct Pulse { + /// Ticks up from the initial value, and triggers a duty step when it reaches 0x800 + period: u16, + duty_step: u8, + duty_cycle: u8, + output: u8, + } + + impl Pulse { + pub fn tick(&mut self) { + self.period += 1; + if self.period > 0x7ff { + // move the duty step + self.duty_step = (self.duty_step + 1) % 8; + } + } + } +} diff --git a/src/graphics.rs b/src/graphics.rs new file mode 100644 index 0000000..9ec73cb --- /dev/null +++ b/src/graphics.rs @@ -0,0 +1,232 @@ +use bitfield_struct::bitfield; + +use crate::memory::io::BusIO; +use std::mem::size_of; + +/// The number of OAM entries supported by the PPU (on real hardware, 40) +pub const NUM_OAM_ENTRIES: usize = 40; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Mode { + HBlank, + VBlank, + OAMScan, + Drawing, +} +impl Mode { + const fn from_bits(value: u8) -> Self { + match value & 3 { + 0b00 => Self::HBlank, + 0b01 => Self::VBlank, + 0b10 => Self::OAMScan, + 0b11 => Self::Drawing, + _ => unreachable!(), + } + } + const fn into_bits(self) -> u8 { + self as _ + } +} + +type OAMTable = [OAMEntry; NUM_OAM_ENTRIES]; + +#[derive(Clone, Debug, PartialEq, PartialOrd)] +pub struct PPU { + lcdc: LcdC, // LCD Control + stat: Stat, // LCD Status + scy: u8, // viewport y coordinate + scx: u8, // viewport x coordinate + /// Current line. Read-only. + ly: u8, // holds values in range 0..=153 (144..=153 = vblank) + /// Line Y for Comparison. Triggers interrupt if enabled in `stat` + lyc: u8, // line y compare, triggers interrupt if enabled + + oam: OAMTable, +} + +impl PPU { + pub fn oam(&self) -> &[u8] { + let data = self.oam.as_slice().as_ptr().cast(); + let len = size_of::(); + // SAFETY: + // [OAMEntry; NUM_OAM_ENTRIES] is sizeof * NUM_OAM_ENTRIES long, + // and already aligned + unsafe { core::slice::from_raw_parts(data, len) } + } + pub fn oam_mut(&mut self) -> &mut [u8] { + let data = self.oam.as_mut_slice().as_mut_ptr().cast(); + let len = size_of::(); + // SAFETY: see above + unsafe { core::slice::from_raw_parts_mut(data, len) } + } + /// Gets the current PPU operating mode + pub fn mode(&self) -> Mode { + self.stat.mode() + } + pub fn set_mode(&mut self, mode: Mode) { + self.stat.set_mode(mode); + } + /// Gets the STAT interrupt selection config. + /// + /// | Bit | Name | + /// |:---:|:---------------| + /// | `0` | H Blank | + /// | `1` | V Blank | + /// | `2` | OAM Scan | + /// | `3` | LY Coincidence | + pub fn stat_interrupt_selection(&self) -> u8 { + (self.stat.into_bits() & 0b1111000) >> 3 + } + /// Gets the LY coincidence flag. Should be true when LY = LYC + pub fn ly_coincidence(&self) -> bool { + self.stat.lyc_hit() + } +} + +impl Default for PPU { + fn default() -> Self { + Self { + lcdc: Default::default(), + stat: Default::default(), + scy: Default::default(), + scx: Default::default(), + ly: Default::default(), + lyc: Default::default(), + oam: [Default::default(); NUM_OAM_ENTRIES], + } + } +} +impl BusIO for PPU { + fn read(&self, addr: usize) -> Option { + match addr { + 0xfe00..=0xfe9f => self.oam().read(addr - 0xfe00), + 0xff40 => Some(self.lcdc.into_bits()), + 0xff41 => Some(self.stat.into_bits()), + 0xff42 => Some(self.scy), + 0xff43 => Some(self.scx), + 0xff44 => Some(self.ly), + 0xff45 => Some(self.lyc), + _ => None, + } + } + + fn write(&mut self, addr: usize, data: u8) -> Option<()> { + match addr { + 0xfe00..=0xfe9f => self.oam_mut().write(addr - 0xfe00, data), + 0xff40 => Some(self.lcdc = LcdC::from_bits(data)), + 0xff41 => Some(self.stat.set_interrupts(Stat::from_bits(data).interrupts())), + 0xff42 => Some(self.scy = data), + 0xff43 => Some(self.scx = data), + 0xff44 => None, + 0xff45 => Some(self.lyc = data), + _ => None, + } + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct OAMEntry { + y: u8, // y coordinate of the bottom of a 16px sprite + x: u8, // x coordinate of the right of an 8px sprite + tid: u8, // tile identifier + attr: OAMAttr, // OAM Attributes +} + +#[bitfield(u8)] +/// OAM Attributes. Read-writable by CPU, read-only by PPU. +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct OAMAttr { + #[bits(3)] + /// Which palette (OBP 0-7) does this object use in CGB mode? + cgb_palette: u8, + #[bits(1)] + /// Which VRAM bank (0-1) contains this object's tile? + cgb_bank: u8, + #[bits(1)] + /// Which palette (OBP 0-1) does this object use in DMG mode? + dmg_palette: u8, + /// Is this sprite flipped horizontally? + x_flip: bool, + /// Is this sprite flipped vertically? + y_flip: bool, + /// Is this sprite drawn below the window and background layers? + priority: bool, +} + +#[bitfield(u8)] +/// LCD main control register +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LcdC { + /// In DMG mode, disables Window and Background layers. + /// In CGB mode, causes objects to render on top of Window and Background layers + bg_window_enable_or_priority: bool, + /// Controls whether objects are rendered or not + obj_enable: bool, + /// Controls the height of objects: false = 8px tall, true = 16px tall + obj_size: bool, + /// Controls bit 10 of the backgroud tile map address (false, true) => (0x9800, 0x9c00) + bg_tile_map_area: bool, + /// Controls which addressing mode to use when picking tiles for the window and background layers + bg_window_tile_data_area: bool, + /// Enables or disables the window + window_enable: bool, + /// Controls bit 10 of the window tile map address (false, true) => (0x9800, 0x9c00) + window_tile_map_area: bool, + /// Controls whether the LCD/PPU recieves power + lcd_ppu_enable: bool, +} + +#[bitfield(u8)] +/// LCD Status and Interrupt Control register +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Stat { + #[bits(2)] + /// The current PPU operating [Mode] + mode: Mode, + /// Set to true when LYC == LY. Read only. + lyc_hit: bool, + #[bits(4)] + /// Configures PPU to raise IRQ when a condition is met + interrupts: Interrupts, + #[bits(default = true)] + _reserved: bool, +} + +#[bitfield(u8)] +/// [Stat] register interrupt configuration +pub struct Interrupts { + /// Configures PPU to raise IRQ on VBlank start + hblank: bool, + /// Configures PPU to raise IRQ on VBlank start + vblank: bool, + /// Configures PPU to raise IRQ on OAM Scan start + oam: bool, + /// Configures PPU to raise IRQ on rising edge of [lyc_hit](Stat::lyc_hit) + lyc: bool, + #[bits(4)] + _unused: (), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DMGColor { + White, + LGray, + DGray, + Black, +} + +impl DMGColor { + pub const fn from_bits(value: u8) -> Self { + match value & 3 { + 0 => DMGColor::White, + 1 => DMGColor::LGray, + 2 => DMGColor::DGray, + 3 => DMGColor::Black, + _ => unreachable!(), + } + } + pub const fn to_bits(self) -> u8 { + self as _ + } +} diff --git a/src/lib.rs b/src/lib.rs index 785f404..08d3986 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,335 +59,11 @@ pub mod error { pub mod memory; -pub mod graphics { - use bitfield_struct::bitfield; - - use crate::memory::io::BusIO; - use std::mem::size_of; - - /// The number of OAM entries supported by the PPU (on real hardware, 40) - pub const NUM_OAM_ENTRIES: usize = 40; - - #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub enum Mode { - HBlank, - VBlank, - OAMScan, - Drawing, - } - impl Mode { - const fn from_bits(value: u8) -> Self { - match value & 3 { - 0b00 => Self::HBlank, - 0b01 => Self::VBlank, - 0b10 => Self::OAMScan, - 0b11 => Self::Drawing, - _ => unreachable!(), - } - } - const fn into_bits(self) -> u8 { - self as _ - } - } - - type OAMTable = [OAMEntry; NUM_OAM_ENTRIES]; - - #[derive(Clone, Debug, PartialEq, PartialOrd)] - pub struct PPU { - lcdc: LcdC, // LCD Control - stat: Stat, // LCD Status - scy: u8, // viewport y coordinate - scx: u8, // viewport x coordinate - /// Current line. Read-only. - ly: u8, // holds values in range 0..=153 (144..=153 = vblank) - /// Line Y for Comparison. Triggers interrupt if enabled in `stat` - lyc: u8, // line y compare, triggers interrupt if enabled - - oam: OAMTable, - } - - impl PPU { - pub fn oam(&self) -> &[u8] { - let data = self.oam.as_slice().as_ptr().cast(); - let len = size_of::(); - // SAFETY: - // [OAMEntry; NUM_OAM_ENTRIES] is sizeof * NUM_OAM_ENTRIES long, - // and already aligned - unsafe { core::slice::from_raw_parts(data, len) } - } - pub fn oam_mut(&mut self) -> &mut [u8] { - let data = self.oam.as_mut_slice().as_mut_ptr().cast(); - let len = size_of::(); - // SAFETY: see above - unsafe { core::slice::from_raw_parts_mut(data, len) } - } - /// Gets the current PPU operating mode - pub fn mode(&self) -> Mode { - self.stat.mode() - } - pub fn set_mode(&mut self, mode: Mode) { - self.stat.set_mode(mode); - } - /// Gets the STAT interrupt selection config. - /// - /// | Bit | Name | - /// |:---:|:---------------| - /// | `0` | H Blank | - /// | `1` | V Blank | - /// | `2` | OAM Scan | - /// | `3` | LY Coincidence | - pub fn stat_interrupt_selection(&self) -> u8 { - (self.stat.into_bits() & 0b1111000) >> 3 - } - /// Gets the LY coincidence flag. Should be true when LY = LYC - pub fn ly_coincidence(&self) -> bool { - self.stat.lyc_hit() - } - } - - impl Default for PPU { - fn default() -> Self { - Self { - lcdc: Default::default(), - stat: Default::default(), - scy: Default::default(), - scx: Default::default(), - ly: Default::default(), - lyc: Default::default(), - oam: [Default::default(); NUM_OAM_ENTRIES], - } - } - } - impl BusIO for PPU { - fn read(&self, addr: usize) -> Option { - match addr { - 0xfe00..=0xfe9f => self.oam().read(addr - 0xfe00), - 0xff40 => Some(self.lcdc.into_bits()), - 0xff41 => Some(self.stat.into_bits()), - 0xff42 => Some(self.scy), - 0xff43 => Some(self.scx), - 0xff44 => Some(self.ly), - 0xff45 => Some(self.lyc), - _ => None, - } - } - - fn write(&mut self, addr: usize, data: u8) -> Option<()> { - match addr { - 0xfe00..=0xfe9f => self.oam_mut().write(addr - 0xfe00, data), - 0xff40 => Some(self.lcdc = LcdC::from_bits(data)), - 0xff41 => Some(self.stat.set_interrupts(Stat::from_bits(data).interrupts())), - 0xff42 => Some(self.scy = data), - 0xff43 => Some(self.scx = data), - 0xff44 => None, - 0xff45 => Some(self.lyc = data), - _ => None, - } - } - } - - #[repr(C)] - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct OAMEntry { - y: u8, // y coordinate of the bottom of a 16px sprite - x: u8, // x coordinate of the right of an 8px sprite - tid: u8, // tile identifier - attr: OAMAttr, // OAM Attributes - } - - #[bitfield(u8)] - /// OAM Attributes. Read-writable by CPU, read-only by PPU. - #[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct OAMAttr { - #[bits(3)] - /// Which palette (OBP 0-7) does this object use in CGB mode? - cgb_palette: u8, - #[bits(1)] - /// Which VRAM bank (0-1) contains this object's tile? - cgb_bank: u8, - #[bits(1)] - /// Which palette (OBP 0-1) does this object use in DMG mode? - dmg_palette: u8, - /// Is this sprite flipped horizontally? - x_flip: bool, - /// Is this sprite flipped vertically? - y_flip: bool, - /// Is this sprite drawn below the window and background layers? - priority: bool, - } - - #[bitfield(u8)] - /// LCD main control register - #[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct LcdC { - /// In DMG mode, disables Window and Background layers. - /// In CGB mode, causes objects to render on top of Window and Background layers - bg_window_enable_or_priority: bool, - /// Controls whether objects are rendered or not - obj_enable: bool, - /// Controls the height of objects: false = 8px tall, true = 16px tall - obj_size: bool, - /// Controls bit 10 of the backgroud tile map address (false, true) => (0x9800, 0x9c00) - bg_tile_map_area: bool, - /// Controls which addressing mode to use when picking tiles for the window and background layers - bg_window_tile_data_area: bool, - /// Enables or disables the window - window_enable: bool, - /// Controls bit 10 of the window tile map address (false, true) => (0x9800, 0x9c00) - window_tile_map_area: bool, - /// Controls whether the LCD/PPU recieves power - lcd_ppu_enable: bool, - } - - #[bitfield(u8)] - /// LCD Status and Interrupt Control register - #[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct Stat { - #[bits(2)] - /// The current PPU operating [Mode] - mode: Mode, - /// Set to true when LYC == LY. Read only. - lyc_hit: bool, - #[bits(4)] - /// Configures PPU to raise IRQ when a condition is met - interrupts: Interrupts, - #[bits(default = true)] - _reserved: bool, - } - - #[bitfield(u8)] - /// [Stat] register interrupt configuration - pub struct Interrupts { - /// Configures PPU to raise IRQ on VBlank start - hblank: bool, - /// Configures PPU to raise IRQ on VBlank start - vblank: bool, - /// Configures PPU to raise IRQ on OAM Scan start - oam: bool, - /// Configures PPU to raise IRQ on rising edge of [lyc_hit](Stat::lyc_hit) - lyc: bool, - #[bits(4)] - _unused: (), - } - - #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub enum DMGColor { - White, - LGray, - DGray, - Black, - } - - impl DMGColor { - pub const fn from_bits(value: u8) -> Self { - match value & 3 { - 0 => DMGColor::White, - 1 => DMGColor::LGray, - 2 => DMGColor::DGray, - 3 => DMGColor::Black, - _ => unreachable!(), - } - } - pub const fn to_bits(self) -> u8 { - self as _ - } - } -} +pub mod graphics; pub mod cpu; -pub mod audio { - use crate::memory::io::BusIO; - use bitfield_struct::bitfield; - - #[derive(Debug, Default)] - pub struct APU { - wave: [u8; 0x10], - amc: AMC, - pan: SndPan, - vol: Vol, - } - - impl BusIO for APU { - fn read(&self, addr: usize) -> Option { - match addr { - 0xff10 => todo!("Channel 1 sweep"), - 0xff11 => todo!("Channel 1 length timer & duty cycle"), - 0xff12 => todo!("Channel 1 volume & envelope"), - 0xff13 => todo!("Channel 1 period low [write-only]"), - 0xff14 => todo!("Channel 1 period high & control [trigger, length enable, period]"), - 0xff24 => Some(self.vol.into_bits()), - 0xff25 => Some(self.pan.into_bits()), - 0xff26 => Some(self.amc.into_bits()), - 0xff30..=0xff3f => self.wave.read(addr - 0xff30), - _ => None, - } - } - - fn write(&mut self, addr: usize, data: u8) -> Option<()> { - #[allow(clippy::unit_arg)] - match addr { - 0xff10..=0xff14 => todo!("Channel 1"), - 0xff16..=0xff19 => todo!("Channel 2"), - 0xff1a..=0xff1e => todo!("Channel 3"), - 0xff20..=0xff23 => todo!("Channel 4"), - 0xff24 => Some(self.vol = Vol::from_bits(data)), - // Sound panning - 0xff25 => Some(self.pan = SndPan::from_bits(data)), - // Audio Master Control - 0xff26 => Some(self.amc.set_power(data & 0x80 != 0)), - // Wave table memory - 0xff30..=0xff3f => self.wave.write(addr - 0xff30, data), - _ => None, - } - } - } - - #[bitfield(u8)] - /// Audio Master Control register - pub struct AMC { - /// Set when CH1 is playing. Read-only. - ch1_on: bool, - /// Set when CH2 is playing. Read-only. - ch2_on: bool, - /// Set when CH3 is playing. Read-only. - ch3_on: bool, - /// Set when CH4 is playing. Read-only. - ch4_on: bool, - #[bits(3)] // 3 bits are unused - _reserved: (), - /// If false, all channels are muted. Read-writable. - power: bool, - } - - #[bitfield(u8)] - /// Sound Panning register. Setting `cNS` to true will send channel `N` to speaker `S` - pub struct SndPan { - c1r: bool, - c2r: bool, - c3r: bool, - c4r: bool, - c1l: bool, - c2l: bool, - c3l: bool, - c4l: bool, - } - - #[bitfield(u8)] - /// Volume control and VIN panning register - pub struct Vol { - #[bits(3)] - /// The volume of the right output - right: u8, - /// Whether VIN is sent to the right output - vin_right: bool, - #[bits(3)] - /// The volume of the left output - left: u8, - /// Whether VIN is sent to the left output - vin_left: bool, - } -} +pub mod audio; #[cfg(test)] mod tests {