Move graphics and audio out of lib.rs
This commit is contained in:
parent
c5490780ca
commit
b91105bbd1
134
src/audio.rs
Normal file
134
src/audio.rs
Normal 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
232
src/graphics.rs
Normal 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 _
|
||||
}
|
||||
}
|
328
src/lib.rs
328
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::<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 {
|
||||
|
Loading…
Reference in New Issue
Block a user