#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OamDmaState {
Idle,
Pending { page: u8 },
Aligning { page: u8 },
Reading { page: u8, offset: u8 },
Writing { page: u8, offset: u8, value: u8 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DmcDmaState {
Idle,
Halt { halted_addr: u16 },
Dummy { halted_addr: u16 },
Aligning { halted_addr: u16 },
Reading,
}
#[derive(Debug, Clone)]
pub struct DmaController {
pub oam_state: OamDmaState,
pub dmc_state: DmcDmaState,
pub cycles_consumed: u16,
oam_needs_align: bool,
is_get_cycle: bool,
}
impl Default for DmaController {
fn default() -> Self {
Self::new()
}
}
impl DmaController {
pub fn new() -> Self {
Self {
oam_state: OamDmaState::Idle,
dmc_state: DmcDmaState::Idle,
cycles_consumed: 0,
oam_needs_align: false,
is_get_cycle: true,
}
}
pub fn is_active(&self) -> bool {
self.oam_state != OamDmaState::Idle || self.dmc_state != DmcDmaState::Idle
}
pub fn oam_dma_active(&self) -> bool {
self.oam_state != OamDmaState::Idle
}
pub fn dmc_dma_active(&self) -> bool {
self.dmc_state != DmcDmaState::Idle
}
pub fn start_oam_dma(&mut self, page: u8, is_odd_cycle: bool) {
self.oam_state = OamDmaState::Pending { page };
self.oam_needs_align = is_odd_cycle;
self.cycles_consumed = 0;
}
pub fn start_dmc_dma(&mut self, halted_addr: u16) {
if self.dmc_state == DmcDmaState::Idle {
self.dmc_state = DmcDmaState::Halt { halted_addr };
}
}
pub fn should_start_dmc_dma(&self) -> bool {
matches!(self.dmc_state, DmcDmaState::Halt { .. })
}
pub fn step(&mut self, total_cpu_cycles: u64) -> DmaAction {
self.is_get_cycle = total_cpu_cycles.is_multiple_of(2);
if self.dmc_state != DmcDmaState::Idle {
return self.step_dmc_dma();
}
if self.oam_state != OamDmaState::Idle {
return self.step_oam_dma();
}
DmaAction::None
}
fn step_dmc_dma(&mut self) -> DmaAction {
self.cycles_consumed += 1;
match self.dmc_state {
DmcDmaState::Idle => DmaAction::None,
DmcDmaState::Halt { halted_addr } => {
self.dmc_state = DmcDmaState::Dummy { halted_addr };
DmaAction::DummyRead(halted_addr)
}
DmcDmaState::Dummy { halted_addr } => {
if !self.is_get_cycle {
self.dmc_state = DmcDmaState::Aligning { halted_addr };
DmaAction::DummyRead(halted_addr)
} else {
self.dmc_state = DmcDmaState::Reading;
DmaAction::DmcRead
}
}
DmcDmaState::Aligning { .. } => {
self.dmc_state = DmcDmaState::Reading;
DmaAction::DmcRead
}
DmcDmaState::Reading => {
self.dmc_state = DmcDmaState::Idle;
DmaAction::None
}
}
}
fn step_oam_dma(&mut self) -> DmaAction {
self.cycles_consumed += 1;
match self.oam_state {
OamDmaState::Idle => DmaAction::None,
OamDmaState::Pending { page } => {
if self.oam_needs_align {
self.oam_state = OamDmaState::Aligning { page };
DmaAction::DummyRead(0) } else {
self.oam_state = OamDmaState::Reading { page, offset: 0 };
DmaAction::DummyRead(0) }
}
OamDmaState::Aligning { page } => {
self.oam_state = OamDmaState::Reading { page, offset: 0 };
DmaAction::DummyRead(0) }
OamDmaState::Reading { page, offset } => {
let addr = ((page as u16) << 8) | (offset as u16);
self.oam_state = OamDmaState::Writing {
page,
offset,
value: 0, };
DmaAction::OamRead(addr)
}
OamDmaState::Writing {
page,
offset,
value,
} => {
let next_offset = offset.wrapping_add(1);
if next_offset == 0 {
self.oam_state = OamDmaState::Idle;
} else {
self.oam_state = OamDmaState::Reading {
page,
offset: next_offset,
};
}
DmaAction::OamWrite(value)
}
}
}
pub fn set_oam_read_value(&mut self, value: u8) {
if let OamDmaState::Writing { page, offset, .. } = self.oam_state {
self.oam_state = OamDmaState::Writing {
page,
offset,
value,
};
}
}
pub fn take_cycles_consumed(&mut self) -> u16 {
let cycles = self.cycles_consumed;
self.cycles_consumed = 0;
cycles
}
pub fn reset(&mut self) {
self.oam_state = OamDmaState::Idle;
self.dmc_state = DmcDmaState::Idle;
self.cycles_consumed = 0;
self.oam_needs_align = false;
self.is_get_cycle = true;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DmaAction {
None,
DummyRead(u16),
OamRead(u16),
OamWrite(u8),
DmcRead,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_controller_is_idle() {
let dma = DmaController::new();
assert!(!dma.is_active());
assert_eq!(dma.oam_state, OamDmaState::Idle);
assert_eq!(dma.dmc_state, DmcDmaState::Idle);
}
#[test]
fn test_oam_dma_start_on_even_cycle() {
let mut dma = DmaController::new();
dma.start_oam_dma(0x02, false);
assert!(dma.is_active());
assert!(dma.oam_dma_active());
assert!(!dma.oam_needs_align);
}
#[test]
fn test_oam_dma_start_on_odd_cycle() {
let mut dma = DmaController::new();
dma.start_oam_dma(0x02, true);
assert!(dma.is_active());
assert!(dma.oam_dma_active());
assert!(dma.oam_needs_align);
}
#[test]
fn test_dmc_dma_start() {
let mut dma = DmaController::new();
dma.start_dmc_dma(0x8000);
assert!(dma.is_active());
assert!(dma.dmc_dma_active());
assert!(dma.should_start_dmc_dma());
}
#[test]
fn test_dmc_has_priority_over_oam() {
let mut dma = DmaController::new();
dma.start_oam_dma(0x02, false);
dma.start_dmc_dma(0x8000);
let action = dma.step(0);
assert!(
matches!(action, DmaAction::DummyRead(_)),
"DMC should take priority"
);
assert!(dma.dmc_dma_active());
}
#[test]
fn test_set_oam_read_value_updates_writing_state() {
let mut dma = DmaController::new();
dma.start_oam_dma(0x02, false);
let action = dma.step(0);
assert!(matches!(action, DmaAction::DummyRead(_)));
let action = dma.step(1);
assert!(matches!(action, DmaAction::OamRead(0x0200)));
dma.set_oam_read_value(0xAB);
match dma.oam_state {
OamDmaState::Writing { value, .. } => assert_eq!(value, 0xAB),
_ => panic!("expected OAM DMA to be in Writing state"),
}
}
#[test]
fn test_take_cycles_consumed_returns_and_resets_counter() {
let mut dma = DmaController::new();
dma.start_oam_dma(0x02, false);
let _ = dma.step(0);
let _ = dma.step(1);
let cycles = dma.take_cycles_consumed();
assert!(cycles >= 2);
assert_eq!(dma.take_cycles_consumed(), 0);
}
#[test]
fn test_reset_clears_state_and_counters() {
let mut dma = DmaController::new();
dma.start_oam_dma(0x02, true);
dma.start_dmc_dma(0x8000);
let _ = dma.step(0);
dma.reset();
assert_eq!(dma.oam_state, OamDmaState::Idle);
assert_eq!(dma.dmc_state, DmcDmaState::Idle);
assert_eq!(dma.cycles_consumed, 0);
assert!(!dma.is_active());
}
}