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 memory;
|
||||||
|
|
||||||
pub mod graphics {
|
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 cpu;
|
pub mod cpu;
|
||||||
|
|
||||||
pub mod audio {
|
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
Loading…
Reference in New Issue
Block a user