Move graphics and audio out of lib.rs

This commit is contained in:
John 2024-06-25 13:36:04 -05:00
parent c5490780ca
commit b91105bbd1
3 changed files with 368 additions and 326 deletions

134
src/audio.rs Normal file
View File

@ -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<u8> {
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;
}
}
}
}

232
src/graphics.rs Normal file
View File

@ -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::<OAMTable>();
// SAFETY:
// [OAMEntry; NUM_OAM_ENTRIES] is sizeof<OAM_ENTRY> * 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::<OAMTable>();
// 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<u8> {
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 _
}
}

View File

@ -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::<OAMTable>();
// SAFETY:
// [OAMEntry; NUM_OAM_ENTRIES] is sizeof<OAM_ENTRY> * 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::<OAMTable>();
// 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<u8> {
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<u8> {
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 {