use alloc::boxed::Box;
use alloc::string::String;
use alloc::sync::{Arc, Weak};
use core::fmt;
use crate::core::device::{
Arbitration, CycleGate, Device, DeviceClass, Export, ExportId, PropertySpec, RealizeCtx,
ResetKind,
};
use crate::core::error::{BusError, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::space::{
AccessConstraints, AddressSpace, MemAttrs, MemOps, MemResult, Region as MmioRegion, RegionRef,
RequesterId,
};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{LockRank, Mutex};
use crate::core::value::{Endian, Width};
use crate::dev::apu::{DmaKind, DmcFetch};
use crate::machine::realize::{BindCtx, Instance};
const CLASS_NAME: &str = "nes.oamdma";
const STATE_VERSION: u32 = 3;
pub const PORT: &str = "port";
pub const OAM_LEN: u16 = 256;
pub const OAM_DATA_ADDR: u64 = 0x2004;
const INTERNAL_BASE: u64 = 0x4000;
const INTERNAL_MASK: u64 = 0xffe0;
pub const TRANSFER_CYCLES: u64 = 1 + 2 * OAM_LEN as u64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Oam {
page: u8,
index: u16,
latch: Option<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Job {
serial: u64,
addr: u16,
noop: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
struct Aftermath {
withdraw: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Act {
Release,
Halted,
Hold,
OamRead(u64),
OamWrite(u8),
DmcRead(u16, u64),
}
struct Shared {
state: Mutex<State>,
}
#[derive(Debug)]
struct State {
bus: Option<Weak<AddressSpace>>,
requester: RequesterId,
dmc: Option<Arc<DmcFetch>>,
phase: u64,
page: u8,
oam: Option<Oam>,
halted: bool,
job: Option<Job>,
transfers: u64,
}
impl fmt::Debug for Shared {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let state = self.state.lock();
f.debug_struct("Shared")
.field("page", &state.page)
.field("oam", &state.oam)
.field("halted", &state.halted)
.field("job", &state.job)
.field("transfers", &state.transfers)
.finish_non_exhaustive()
}
}
#[inline]
const fn is_get(cycle: u64, phase: u64) -> bool {
(cycle.wrapping_sub(1).wrapping_add(phase)) & 1 == 0
}
#[inline]
const fn phase_at_or_after(from: u64, phase: u64, get: bool) -> u64 {
if is_get(from, phase) == get {
from
} else {
from + 1
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Ready {
serial: u64,
addr: u16,
start: u64,
abort: bool,
}
impl Shared {
fn arm_oam(&self, page: u8) {
let mut state = self.state.lock();
state.page = page;
state.oam = Some(Oam {
page,
index: 0,
latch: None,
});
}
fn dmc(&self) -> Option<Arc<DmcFetch>> {
self.state.lock().dmc.clone()
}
fn phase(&self) -> u64 {
self.state.lock().phase
}
fn job_serial(&self) -> Option<u64> {
self.state.lock().job.map(|j| j.serial)
}
fn master(&self) -> Option<(Arc<AddressSpace>, MemAttrs)> {
let state = self.state.lock();
let bus = state.bus.as_ref().and_then(Weak::upgrade)?;
Some((bus, MemAttrs::DEFAULT.with_requester(state.requester)))
}
fn decide(
&self,
cycle: u64,
write: bool,
request: Option<Ready>,
alive: Option<bool>,
after: &mut Aftermath,
) -> Act {
let mut state = self.state.lock();
let phase = state.phase;
if alive == Some(false) {
state.job = None;
}
let ready = request.filter(|r| cycle >= r.start);
if !state.halted {
if ready.is_none() && state.oam.is_none() {
return Act::Release;
}
if write {
if let Some(r) = ready.filter(|r| r.abort) {
after.withdraw = Some(r.serial);
}
return Act::Release;
}
state.halted = true;
if let Some(r) = ready {
if r.abort {
after.withdraw = Some(r.serial);
if state.oam.is_none() {
state.halted = false;
return Act::Halted;
}
} else {
state.job = Some(Job {
serial: r.serial,
addr: r.addr,
noop: 1,
});
}
}
} else if state.job.is_none()
&& let Some(r) = ready.filter(|r| !r.abort)
{
state.job = Some(Job {
serial: r.serial,
addr: r.addr,
noop: 2,
});
}
let next = cycle + 1;
let get = is_get(next, phase);
let mut fetch = None;
if let Some(job) = &mut state.job {
if job.noop > 0 {
job.noop -= 1;
} else if get {
fetch = state.job.take();
}
}
if let Some(job) = fetch {
return Act::DmcRead(job.addr, job.serial);
}
if let Some(oam) = &mut state.oam {
if let Some(byte) = oam.latch.take() {
oam.index += 1;
if oam.index == OAM_LEN {
state.oam = None;
state.transfers += 1;
}
return Act::OamWrite(byte);
}
if get {
let addr = (u64::from(oam.page) << 8) | u64::from(oam.index);
return Act::OamRead(addr);
}
return Act::Hold;
}
if state.job.is_some() {
return Act::Hold;
}
state.halted = false;
Act::Release
}
fn latch_oam(&self, byte: u8) {
if let Some(oam) = &mut self.state.lock().oam {
oam.latch = Some(byte);
}
}
}
#[derive(Debug)]
pub struct OamDma {
shared: Arc<Shared>,
port: RegionRef,
dmc_link: Mutex<Option<String>>,
}
impl OamDma {
pub fn new(props: &Props) -> Result<OamDma> {
let mut r = props.reader();
let phase = r.or_range::<u64>("put-phase", 0, 0..=1)?;
let dmc = r.optional_link("dmc")?.map(|l| String::from(l.as_str()));
r.finish()?;
let unit = OamDma::default();
unit.shared.state.lock().phase = phase;
*unit.dmc_link.lock() = dmc;
Ok(unit)
}
pub fn attach_bus(&self, space: &Arc<AddressSpace>, requester: RequesterId) {
let mut state = self.shared.state.lock();
state.bus = Some(Arc::downgrade(space));
state.requester = requester;
}
pub fn attach_dmc(&self, dmc: Arc<DmcFetch>) {
self.shared.state.lock().dmc = Some(dmc);
}
pub fn set_put_phase(&self, phase: u64) {
self.shared.state.lock().phase = phase & 1;
}
#[must_use]
pub fn transfers(&self) -> u64 {
self.shared.state.lock().transfers
}
#[must_use]
pub fn page(&self) -> u8 {
self.shared.state.lock().page
}
#[must_use]
pub fn halted(&self) -> bool {
self.shared.state.lock().halted
}
#[must_use]
pub fn gate(&self) -> Arc<dyn CycleGate> {
Arc::new(Gate {
shared: Arc::clone(&self.shared),
})
}
}
impl Default for OamDma {
fn default() -> OamDma {
let shared = Arc::new(Shared {
state: Mutex::with_rank(
LockRank::DEVICE,
State {
bus: None,
requester: RequesterId::ANONYMOUS,
dmc: None,
phase: 0,
page: 0,
oam: None,
halted: false,
job: None,
transfers: 0,
},
),
});
let port = Arc::new(MmioRegion::io(
"nes.oamdma.4014",
1,
Arc::new(DmaPort {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
OamDma {
shared,
port,
dmc_link: Mutex::new(None),
}
}
}
#[derive(Debug)]
struct Gate {
shared: Arc<Shared>,
}
impl Gate {
fn perform(&self, act: Act, held: u64, data: u8, dmc: Option<&Arc<DmcFetch>>) -> u8 {
let Some((space, attrs)) = self.shared.master() else {
return data;
};
let attrs = attrs.with_bus(data);
match act {
Act::Release | Act::Halted | Act::Hold => data,
Act::OamRead(addr) => {
let (byte, pins) = self.read_with_conflict(&space, attrs, addr, held);
self.shared.latch_oam(byte);
pins
}
Act::OamWrite(byte) => {
let _ = space.write(OAM_DATA_ADDR, Width::U8, u64::from(byte), attrs);
byte
}
Act::DmcRead(addr, serial) => {
let (byte, pins) = self.read_with_conflict(&space, attrs, u64::from(addr), held);
if let Some(dmc) = dmc {
dmc.complete(serial, byte);
}
pins
}
}
}
fn read_with_conflict(
&self,
bus: &AddressSpace,
attrs: MemAttrs,
addr: u64,
held: u64,
) -> (u8, u8) {
let core_inside = held & INTERNAL_MASK == INTERNAL_BASE;
if !core_inside && addr & INTERNAL_MASK == INTERNAL_BASE {
return (attrs.bus, attrs.bus);
}
let external = match bus.read_driven(addr, Width::U8, attrs) {
Ok((v, _)) => v as u8,
Err(_) => attrs.bus,
};
if !core_inside {
return (external, external);
}
let alias = INTERNAL_BASE | (addr & 0x1f);
let attrs = attrs.with_bus(external).with_core_bus(external);
match bus.read_driven(alias, Width::U8, attrs) {
Ok((v, drives)) => {
let v = v as u8;
(v, if drives { v } else { external })
}
Err(_) => (external, external),
}
}
}
impl CycleGate for Gate {
fn arbitrate(&self, cycle: u64, held: u64, bus: u8, write: bool) -> Arbitration {
let dmc = self.shared.dmc();
let (request, alive) = match &dmc {
Some(dmc) => {
dmc.sync();
let phase = self.shared.phase();
let request = dmc.request().map(|r| Ready {
serial: r.serial,
addr: r.addr,
start: match r.kind {
DmaKind::Load => phase_at_or_after(r.at + 3, phase, true),
DmaKind::Reload | DmaKind::Abort => {
phase_at_or_after(r.at + 1, phase, false).max(r.not_before)
}
},
abort: r.kind == DmaKind::Abort,
});
let alive = self.shared.job_serial().map(|s| dmc.is_pending(s));
(request, alive)
}
None => (None, None),
};
let mut after = Aftermath::default();
let act = self.shared.decide(cycle, write, request, alive, &mut after);
if let (Some(serial), Some(dmc)) = (after.withdraw, dmc.as_ref()) {
dmc.withdraw(serial);
}
let data = self.perform(act, held, bus, dmc.as_ref());
match act {
Act::Release => Arbitration::Release,
Act::Halted => Arbitration::Halted,
Act::Hold => Arbitration::Hold,
_ => Arbitration::Steal(data),
}
}
}
#[derive(Debug)]
struct DmaPort {
shared: Arc<Shared>,
}
impl MemOps for DmaPort {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let ([byte], 0) = (dst, offset) else {
return Err(BusError::BadAccess);
};
*byte = attrs.bus;
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let ([value], 0) = (src, offset) else {
return Err(BusError::BadAccess);
};
if attrs.debug {
return Ok(());
}
self.shared.arm_oam(*value);
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little)
}
}
impl Device for OamDma {
fn class(&self) -> &'static DeviceClass {
&OAM_DMA_CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn export(&self, which: ExportId) -> Option<Export> {
(which == ExportId::CYCLE_GATE).then(|| Export::Gate(self.gate()))
}
fn reset(&self, _kind: ResetKind) {
let mut state = self.shared.state.lock();
state.page = 0;
state.oam = None;
state.halted = false;
state.job = None;
state.transfers = 0;
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let state = self.shared.state.lock();
w.write_u8(state.page)?;
w.write_u64(state.transfers)?;
match state.oam {
Some(oam) => {
w.write_bool(true)?;
w.write_u8(oam.page)?;
w.write_u16(oam.index)?;
w.write_bool(oam.latch.is_some())?;
w.write_u8(oam.latch.unwrap_or(0))?;
}
None => {
w.write_bool(false)?;
w.write_u8(0)?;
w.write_u16(0)?;
w.write_bool(false)?;
w.write_u8(0)?;
}
}
w.write_bool(state.halted)?;
match state.job {
Some(job) => {
w.write_bool(true)?;
w.write_u64(job.serial)?;
w.write_u16(job.addr)?;
w.write_u8(job.noop)
}
None => {
w.write_bool(false)?;
w.write_u64(0)?;
w.write_u16(0)?;
w.write_u8(0)
}
}
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let page = r.read_u8()?;
let transfers = r.read_u64()?;
let running = r.read_bool()?;
let oam_page = r.read_u8()?;
let index = r.read_u16()?;
let latched = r.read_bool()?;
let latch = r.read_u8()?;
let halted = r.read_bool()?;
let has_job = r.read_bool()?;
let serial = r.read_u64()?;
let addr = r.read_u16()?;
let noop = r.read_u8()?;
let mut state = self.shared.state.lock();
state.page = page;
state.transfers = transfers;
state.oam = running.then_some(Oam {
page: oam_page,
index,
latch: latched.then_some(latch),
});
state.halted = halted;
state.job = has_job.then_some(Job { serial, addr, noop });
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
match name {
PORT | "" => Some(Arc::clone(&self.port)),
_ => None,
}
}
}
impl Instance for OamDma {
fn bind(&self, ctx: &BindCtx<'_>) -> Result<()> {
let space = ctx.space().ok_or_else(|| crate::core::Error::Config {
at: String::from(ctx.path()),
message: String::from(
"the DMA unit masters the CPU bus: add `space = cpubus` to the object that \
declares it",
),
})?;
self.attach_bus(space, ctx.requester());
let wanted = self.dmc_link.lock().clone();
if let Some(name) = wanted {
self.attach_dmc(ctx.export_as::<DmcFetch>(&name, ExportId::DMC_FETCH)?);
}
Ok(())
}
}
static OAM_DMA_PROPERTIES: &[PropertySpec] = &[
PropertySpec {
name: "put-phase",
kind: ValueKind::Uint,
required: false,
summary: "which CPU cycles are puts (0 or 1); must match the APU's",
},
PropertySpec {
name: "dmc",
kind: ValueKind::Link,
required: false,
summary: "the APU whose DMC sample fetch shares this unit's /RDY line",
},
];
pub static OAM_DMA_CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "RP2A03 DMA unit: OAM DMA at $4014 and the DMC sample fetch, both halting the CPU",
properties: OAM_DMA_PROPERTIES,
construct: |props| Ok(Box::new(OamDma::new(props)?) as Box<dyn Device>),
};
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&OAM_DMA_CLASS)
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS_NAME, |props| Ok(Arc::new(OamDma::new(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PropSchema};
ClassSchema::new(CLASS_NAME)
.region(PORT)
.prop(PropSchema::new("put-phase", ValueKind::Uint).range(0, 1))
.prop(PropSchema::new("dmc", ValueKind::Link))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::space::{RamStore, Region};
use crate::core::state::{MachineShape, Migrations, StateReader, StateWriter};
use alloc::vec::Vec;
struct Bus {
space: Arc<AddressSpace>,
dma: OamDma,
oam: Arc<Oam2004>,
}
#[derive(Debug, Default)]
struct Oam2004 {
written: Mutex<Vec<u8>>,
}
impl MemOps for Oam2004 {
fn read(&self, _offset: u64, dst: &mut [u8], _attrs: MemAttrs) -> MemResult {
for byte in dst.iter_mut() {
*byte = 0;
}
Ok(())
}
fn write(&self, _offset: u64, src: &[u8], _attrs: MemAttrs) -> MemResult {
self.written.lock().extend_from_slice(src);
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little)
}
}
fn bus() -> Bus {
let space = Arc::new(AddressSpace::new("cpubus", 16));
let ram = Arc::new(RamStore::new(0x800));
let oam = Arc::new(Oam2004::default());
let dma = OamDma::default();
{
let mut topo = space.topology();
topo.map(
Arc::new(
Region::mirror("wram", Arc::new(Region::ram("ram", ram)), 0x2000)
.expect("mirrors"),
),
0,
)
.expect("maps");
topo.map(
Arc::new(MmioRegion::io(
"oamdata",
0x2000,
Arc::clone(&oam) as Arc<dyn MemOps>,
)),
0x2000,
)
.expect("maps");
topo.map(dma.region(PORT).expect("port"), 0x4014)
.expect("maps");
}
dma.attach_bus(&space, RequesterId::ANONYMOUS);
Bus { space, dma, oam }
}
fn wr(space: &AddressSpace, addr: u64, value: u8) {
space
.write(addr, Width::U8, u64::from(value), MemAttrs::DEFAULT)
.unwrap_or_else(|e| panic!("write {addr:#06x}: {e}"));
}
fn drive(dma: &OamDma, from: u64, held: u64) -> Vec<Arbitration> {
let gate = dma.gate();
let mut out = Vec::new();
let mut cycle = from;
loop {
let a = gate.arbitrate(cycle, held, 0x40, false);
out.push(a);
cycle += 1;
if a == Arbitration::Release {
return out;
}
assert!(out.len() < 1024, "the arbiter never released the core");
}
}
#[test]
fn a_write_copies_two_hundred_and_fifty_six_bytes_through_2004() {
let b = bus();
for i in 0..256u64 {
wr(&b.space, 0x0200 + i, (i as u8) ^ 0x5a);
}
wr(&b.space, 0x4014, 0x02);
assert!(b.oam.written.lock().is_empty(), "nothing moves yet");
drive(&b.dma, 1, 0x8000);
let written = b.oam.written.lock().clone();
assert_eq!(written.len(), 256);
for (i, byte) in written.iter().enumerate() {
assert_eq!(*byte, (i as u8) ^ 0x5a, "byte {i}");
}
assert_eq!(b.dma.page(), 0x02);
assert_eq!(b.dma.transfers(), 1);
}
#[test]
fn the_source_page_is_the_written_byte_shifted_up() {
let b = bus();
wr(&b.space, 0x0700, 0xc3);
wr(&b.space, 0x4014, 0x07);
drive(&b.dma, 1, 0x8000);
assert_eq!(b.oam.written.lock()[0], 0xc3);
}
#[test]
fn a_copy_costs_513_cycles_from_a_get_and_514_from_a_put() {
for (write_cycle, expected, holds) in [(1u64, 513usize, 0usize), (2, 514, 1)] {
let b = bus();
wr(&b.space, 0x4014, 0x02);
let acts = drive(&b.dma, write_cycle + 1, 0x8000);
assert_eq!(acts.len(), expected, "write on cycle {write_cycle}");
assert_eq!(
acts.iter().filter(|a| **a == Arbitration::Hold).count(),
holds,
"an alignment cycle only when the first get is a cycle further off"
);
}
}
#[test]
fn a_fetch_withdrawn_just_before_its_halt_costs_exactly_one_cycle() {
let (b, apu) = dmc_bus();
apu.write(0x10, 0x4f); apu.write(0x12, 0xb5);
apu.write(0x13, 0x00); apu.write(0x15, 0x10);
let acts = poll_loop(&b, &apu, apu.ticks() + 1, 500);
let first_reload = acts
.iter()
.map(|(c, _)| *c)
.nth(2)
.expect("a load and then a reload");
let gate = b.dma.gate();
let next = first_reload + 432;
let mut out = Vec::new();
for cycle in acts.last().expect("acts").0 + 1..next + 8 {
apu.advance_to(cycle);
if cycle == next - 2 {
apu.write(0x15, 0x00);
}
let a = gate.arbitrate(cycle, 0x8000, 0x40, false);
if a != Arbitration::Release {
out.push((cycle, a));
}
}
assert_eq!(
out.len(),
1,
"one cycle and no more, at {:?}",
out.iter().map(|(c, _)| *c).collect::<Vec<_>>()
);
assert_eq!(out[0].1, Arbitration::Halted);
}
#[test]
fn a_write_cycle_cannot_be_halted() {
let b = bus();
wr(&b.space, 0x4014, 0x02);
let gate = b.dma.gate();
for cycle in 1..=3 {
assert_eq!(
gate.arbitrate(cycle, 0x8000, 0x40, true),
Arbitration::Release,
"cycle {cycle}"
);
assert!(!b.dma.halted());
}
assert_ne!(gate.arbitrate(4, 0x8000, 0x40, false), Arbitration::Release);
assert!(b.dma.halted());
}
#[test]
fn a_debug_write_moves_nothing() {
let b = bus();
b.space
.write(0x4014, Width::U8, 0x02, MemAttrs::DEBUG)
.expect("accepted");
assert!(b.oam.written.lock().is_empty());
assert_eq!(b.dma.transfers(), 0);
assert_eq!(
b.dma.gate().arbitrate(1, 0x8000, 0x40, false),
Arbitration::Release,
"and arms nothing"
);
}
#[test]
fn the_register_reads_as_open_bus() {
let b = bus();
let value = b
.space
.read(0x4014, Width::U8, MemAttrs::DEFAULT.with_bus(0x40))
.expect("answered");
assert_eq!(value, 0x40, "$4014 is write-only");
let value = b
.space
.read(0x4014, Width::U8, MemAttrs::DEFAULT.with_bus(0xa5))
.expect("answered");
assert_eq!(value, 0xa5, "and it really is the bus, not a constant");
}
#[test]
fn an_idle_unit_never_holds_the_core() {
let b = bus();
let gate = b.dma.gate();
for cycle in 1..=8 {
assert_eq!(
gate.arbitrate(cycle, 0x8000, 0x40, false),
Arbitration::Release
);
}
}
#[test]
fn the_device_does_not_keep_its_own_space_alive() {
let b = bus();
let weak = Arc::downgrade(&b.space);
let Bus { space, dma, oam } = b;
drop(space);
drop(oam);
assert!(weak.upgrade().is_none(), "the space leaked");
assert!(dma.shared.master().is_none());
}
#[test]
fn state_round_trips() {
let b = bus();
wr(&b.space, 0x4014, 0x03);
let gate = b.dma.gate();
for cycle in 1..=40 {
gate.arbitrate(cycle, 0x8000, 0x40, false);
}
let mut shape = MachineShape::new();
shape.add_device("dma", CLASS_NAME).expect("unique path");
let mut writer = StateWriter::new(shape);
let mut chunk = writer
.chunk("dma", CLASS_NAME, STATE_VERSION)
.expect("one chunk");
b.dma.save(&mut chunk).expect("saves");
let bytes = writer.to_vec().expect("encodes");
let other = OamDma::default();
let reader = StateReader::new(&bytes).expect("decodes");
let chunk = reader
.load("dma", CLASS_NAME, STATE_VERSION, &Migrations::new())
.expect("finds the chunk");
other.load(&mut chunk.reader()).expect("loads");
assert_eq!(other.page(), b.dma.page());
assert_eq!(other.transfers(), b.dma.transfers());
assert_eq!(other.halted(), b.dma.halted());
let restored = other.shared.state.lock().oam;
let original = b.dma.shared.state.lock().oam;
assert_eq!(restored, original, "a half-finished copy survives");
}
#[test]
fn a_reset_clears_the_unit() {
let b = bus();
wr(&b.space, 0x4014, 0x03);
b.dma.reset(ResetKind::Cold);
assert_eq!(b.dma.page(), 0);
assert!(!b.dma.halted());
wr(&b.space, 0x4014, 0x00);
drive(&b.dma, 1, 0x8000);
assert_eq!(b.dma.transfers(), 1);
}
fn dmc_bus() -> (Bus, Arc<crate::dev::apu::Apu>) {
let b = bus();
let apu = Arc::new(
crate::dev::apu::Apu::new(&Props::new().with("region", "ntsc")).expect("an APU"),
);
let rom = Arc::new(RamStore::new(0x8000));
rom.write_at(0x6d40, &[0x05]).expect("in range");
{
let mut topo = b.space.topology();
topo.map(Arc::new(Region::ram("prg", rom)), 0x8000)
.expect("maps");
topo.map(apu.region("channels").expect("channels"), 0x4000)
.expect("maps");
topo.map(apu.region("status").expect("status"), 0x4015)
.expect("maps");
}
let fetch = apu
.export(ExportId::DMC_FETCH)
.and_then(|e| e.opaque().cloned())
.and_then(|h| h.downcast::<DmcFetch>().ok())
.expect("the APU offers its DMC");
b.dma.attach_dmc(fetch);
(b, apu)
}
fn poll_loop(
b: &Bus,
apu: &crate::dev::apu::Apu,
from: u64,
cycles: u64,
) -> Vec<(u64, Arbitration)> {
let gate = b.dma.gate();
let mut out = Vec::new();
for cycle in from..from + cycles {
apu.advance_to(cycle);
let a = gate.arbitrate(cycle, 0x8000, 0x40, false);
if a != Arbitration::Release {
out.push((cycle, a));
}
}
out
}
#[test]
fn a_dmc_fetch_inside_a_sprite_copy_costs_it_only_two_cycles() {
let run = |with_dmc: bool| -> u64 {
let (b, apu) = dmc_bus();
apu.write(0x10, 0x0f); apu.write(0x12, 0xb5); apu.write(0x13, 0x00); wr(&b.space, 0x4014, 0x00);
let gate = b.dma.gate();
let from = apu.ticks();
let mut cycle = from;
while b.dma.transfers() == 0 {
if with_dmc && cycle == from + 100 {
apu.advance_to(cycle);
apu.write(0x15, 0x10);
}
apu.advance_to(cycle);
gate.arbitrate(cycle, 0x8000, 0x40, false);
cycle += 1;
assert!(cycle < from + 1000, "the sprite copy never finished");
}
cycle - from
};
assert_eq!(
run(true) - run(false),
2,
"a fetch inside a copy costs two cycles, not its own four"
);
}
#[test]
fn a_dmc_fetch_halts_the_core_for_three_cycles_or_four() {
let (b, apu) = dmc_bus();
apu.write(0x10, 0x4f); apu.write(0x12, 0xb5); apu.write(0x13, 0x00); apu.write(0x15, 0x10); let scheduled = apu.ticks();
let acts = poll_loop(&b, &apu, scheduled + 1, 16);
let load: Vec<u64> = acts.iter().map(|(c, _)| *c).collect();
assert_eq!(
load.len() + 1,
3,
"a load fetch costs three cycles; answers at {load:?}"
);
assert!(
matches!(acts.last(), Some((_, Arbitration::Steal(0x05)))),
"and the last of them is the get, leaving the sample byte on the bus"
);
assert_eq!(load[1], load[0] + 1);
let after = load[1] + 2;
let acts = poll_loop(&b, &apu, after, 900);
let all: Vec<u64> = acts.iter().map(|(c, _)| *c).collect();
assert_eq!(all.len(), 6, "two reloads in the window: {all:?}");
assert_eq!(
all[3] - all[0],
432,
"the fastest rate is 432 cycles a byte"
);
let reload = &all[..3];
assert_eq!(
reload.len() + 1,
4,
"a reload fetch costs four cycles; answers at {reload:?}"
);
assert!(matches!(acts.last(), Some((_, Arbitration::Steal(0x05)))));
assert!(!is_get(reload[0], 0), "a reload halts on a put");
for pair in reload.windows(2) {
assert_eq!(pair[1], pair[0] + 1, "consecutive: {reload:?}");
}
}
#[test]
fn an_unknown_property_is_refused() {
let e = OamDma::new(&Props::new().with("page", 3u64)).expect_err("no such property");
assert!(alloc::format!("{e}").contains("page"), "{e}");
}
}