pub mod dmc;
pub mod frame;
pub mod mixer;
pub mod noise;
pub mod pulse;
pub mod triangle;
pub mod units;
#[cfg(test)]
mod tests;
use alloc::boxed::Box;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;
use crate::core::clock::DomainId;
use crate::core::device::{
Device, DeviceClass, Export, ExportId, PropertySpec, RealizeCtx, ResetKind,
};
use crate::core::error::{Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::registry::Registry;
use crate::core::sched::{AccessKind, LazyHandle};
use crate::core::space::{
AccessConstraints, MemAttrs, MemOps, MemResult, Region as MmioRegion, RegionRef,
};
use crate::core::state::{ChunkReader, ChunkWriter};
use crate::core::sync::{AtomicU64, LockRank, Mutex, Ordering};
use crate::core::value::{Endian, Width};
use crate::core::wire::{Level, WireSource};
use crate::machine::realize::{BindCtx, Instance};
pub use dmc::{DmaKind, DmaRequest};
pub use frame::{Mode, Region};
use dmc::Dmc;
use frame::FrameCounter;
use mixer::SampleRing;
use noise::Noise;
use pulse::Pulse;
use triangle::Triangle;
mod reg {
pub(super) const PULSE1_CTRL: u8 = 0x00;
pub(super) const PULSE1_SWEEP: u8 = 0x01;
pub(super) const PULSE1_LO: u8 = 0x02;
pub(super) const PULSE1_HI: u8 = 0x03;
pub(super) const PULSE2_CTRL: u8 = 0x04;
pub(super) const PULSE2_SWEEP: u8 = 0x05;
pub(super) const PULSE2_LO: u8 = 0x06;
pub(super) const PULSE2_HI: u8 = 0x07;
pub(super) const TRI_LINEAR: u8 = 0x08;
pub(super) const TRI_LO: u8 = 0x0A;
pub(super) const TRI_HI: u8 = 0x0B;
pub(super) const NOISE_CTRL: u8 = 0x0C;
pub(super) const NOISE_PERIOD: u8 = 0x0E;
pub(super) const NOISE_LEN: u8 = 0x0F;
pub(super) const DMC_CTRL: u8 = 0x10;
pub(super) const DMC_LOAD: u8 = 0x11;
pub(super) const DMC_ADDR: u8 = 0x12;
pub(super) const DMC_LEN: u8 = 0x13;
pub(super) const STATUS: u8 = 0x15;
pub(super) const FRAME: u8 = 0x17;
}
#[derive(Debug)]
struct Core {
frame: FrameCounter,
pulse1: Pulse,
pulse2: Pulse,
triangle: Triangle,
noise: Noise,
dmc: Dmc,
ticks: u64,
irq_out: bool,
phase: u64,
samples: SampleRing,
}
impl Core {
fn new(region: Region, phase: u64, halt_ultrasonic: bool, capacity: usize) -> Core {
Core {
frame: FrameCounter::new(region),
pulse1: Pulse::new(true),
pulse2: Pulse::new(false),
triangle: Triangle::new(halt_ultrasonic),
noise: Noise::new(region),
dmc: Dmc::new(region),
ticks: 0,
irq_out: false,
phase,
samples: SampleRing::with_capacity(capacity),
}
}
#[inline]
fn on_put_cycle(&self) -> bool {
(self.ticks.wrapping_sub(1).wrapping_add(self.phase)) & 1 == 1
}
fn tick(&mut self) {
self.irq_out = self.irq_raw();
self.ticks += 1;
let now = self.ticks;
let event = self.frame.tick(now, self.on_put_cycle());
if event.quarter {
self.clock_quarter_frame();
}
if event.half {
self.clock_half_frame();
}
self.triangle.tick_timer();
if !self.on_put_cycle() {
self.pulse1.tick_timer();
self.pulse2.tick_timer();
self.noise.tick_timer();
self.dmc.tick_timer(now);
let sample = self.mix();
self.samples.push(sample);
}
}
fn clock_quarter_frame(&mut self) {
self.pulse1.envelope.clock();
self.pulse2.envelope.clock();
self.noise.envelope.clock();
self.triangle.clock_linear();
}
fn clock_half_frame(&mut self) {
self.pulse1.length.clock();
self.pulse1.clock_sweep();
self.pulse2.length.clock();
self.pulse2.clock_sweep();
self.triangle.length.clock();
self.noise.length.clock();
}
fn mix(&self) -> u16 {
mixer::mix(
self.pulse1.output(),
self.pulse2.output(),
self.triangle.output(),
self.noise.output(),
self.dmc.output(),
)
}
#[inline]
fn irq_raw(&self) -> bool {
(self.frame.irq() && !self.frame.inhibited()) || self.dmc.irq()
}
fn irq_asserted(&self) -> bool {
self.irq_out
}
fn write(&mut self, index: u8, value: u8) {
match index {
reg::PULSE1_CTRL => self.pulse1.write_control(value),
reg::PULSE1_SWEEP => self.pulse1.write_sweep(value),
reg::PULSE1_LO => self.pulse1.write_period_low(value),
reg::PULSE1_HI => self.pulse1.write_period_high(value),
reg::PULSE2_CTRL => self.pulse2.write_control(value),
reg::PULSE2_SWEEP => self.pulse2.write_sweep(value),
reg::PULSE2_LO => self.pulse2.write_period_low(value),
reg::PULSE2_HI => self.pulse2.write_period_high(value),
reg::TRI_LINEAR => self.triangle.write_linear(value),
reg::TRI_LO => self.triangle.write_period_low(value),
reg::TRI_HI => self.triangle.write_period_high(value),
reg::NOISE_CTRL => self.noise.write_control(value),
reg::NOISE_PERIOD => self.noise.write_period(value),
reg::NOISE_LEN => self.noise.write_length(value),
reg::DMC_CTRL => self.dmc.write_control(value),
reg::DMC_LOAD => self.dmc.write_output(value),
reg::DMC_ADDR => self.dmc.write_address(value),
reg::DMC_LEN => self.dmc.write_length(value),
reg::STATUS => self.write_status(value, self.ticks),
reg::FRAME => self.frame.write(value, self.on_put_cycle()),
_ => {}
}
}
fn write_status(&mut self, value: u8, now: u64) {
self.pulse1.length.set_enabled(value & 0x01 != 0);
self.pulse2.length.set_enabled(value & 0x02 != 0);
self.triangle.length.set_enabled(value & 0x04 != 0);
self.noise.length.set_enabled(value & 0x08 != 0);
self.dmc.clear_irq();
self.dmc.set_enabled(value & 0x10 != 0, now);
}
fn read_status(&mut self, open_bus: u8, peek: bool) -> u8 {
let mut value = open_bus & 0x20;
if self.pulse1.length.active() {
value |= 0x01;
}
if self.pulse2.length.active() {
value |= 0x02;
}
if self.triangle.length.active() {
value |= 0x04;
}
if self.noise.length.active() {
value |= 0x08;
}
if self.dmc.active() {
value |= 0x10;
}
if self.frame.read_irq(peek) {
value |= 0x40;
}
if self.dmc.irq() {
value |= 0x80;
}
value
}
fn reset(&mut self, kind: ResetKind, region: Region, halt_ultrasonic: bool) {
match kind {
ResetKind::Cold => {
let phase = self.phase;
let capacity = self.samples.capacity();
*self = Core::new(region, phase, halt_ultrasonic, capacity);
}
ResetKind::Warm | ResetKind::Bus => {
self.write_status(0x00, self.ticks);
self.frame.reset_warm();
self.dmc.reset_warm();
self.triangle.reset_phase();
self.samples.clear();
}
}
}
fn save(&self, w: &mut dyn crate::core::state::Sink) -> Result<()> {
self.frame.save(w)?;
self.pulse1.save(w)?;
self.pulse2.save(w)?;
self.triangle.save(w)?;
self.noise.save(w)?;
self.dmc.save(w)?;
w.write_u64(self.ticks)?;
w.write_u64(self.phase)
}
fn load<'a>(&mut self, r: &mut dyn crate::core::state::Source<'a>) -> Result<()> {
self.frame.load(r)?;
self.pulse1.load(r)?;
self.pulse2.load(r)?;
self.triangle.load(r)?;
self.noise.load(r)?;
self.dmc.load(r)?;
self.ticks = r.read_u64()?;
self.phase = r.read_u64()? & 1;
self.samples.clear();
Ok(())
}
}
struct ApuState {
core: Mutex<Core>,
irq: Mutex<Option<WireSource>>,
domain: Mutex<Option<DomainId>>,
lazy: Mutex<Option<LazyHandle>>,
ticks: AtomicU64,
region: Region,
halt_ultrasonic: bool,
}
impl fmt::Debug for ApuState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ApuState")
.field("region", &self.region)
.finish_non_exhaustive()
}
}
impl ApuState {
fn irq_level(&self) -> Level {
if self.core.lock().irq_asserted() {
Level::High
} else {
Level::Low
}
}
fn refresh_irq(&self) {
let level = self.irq_level();
let port = self.irq.lock().clone();
if let Some(port) = port {
port.set(level);
}
}
fn write(&self, index: u8, value: u8) {
{
let mut core = self.core.lock();
core.write(index, value);
self.publish(&core);
}
self.refresh_irq();
}
fn read_with(&self, index: u8, open_bus: u8, peek: bool) -> u8 {
if index != reg::STATUS {
return open_bus;
}
let value = self.core.lock().read_status(open_bus, peek);
if !peek {
self.refresh_irq();
}
value
}
fn publish(&self, core: &Core) {
self.ticks.store(core.ticks, Ordering::Relaxed);
}
fn sync(&self, attrs: MemAttrs) {
let handle = self.lazy.lock().clone();
let Some(handle) = handle else {
return;
};
let kind = if attrs.debug {
AccessKind::Debug
} else {
AccessKind::Guest
};
let _ = handle.sync(kind);
}
}
#[derive(Debug)]
pub struct Apu {
state: Arc<ApuState>,
windows: [RegionRef; 3],
}
impl Apu {
pub fn new(props: &Props) -> Result<Apu> {
let mut reader = props.reader();
let name = reader.or_enum("region", Region::Ntsc.name(), Region::NAMES)?;
let capacity = reader.or_range::<u64>("sample-buffer", 8192, 0..=1 << 24)?;
let halt_ultrasonic = reader.or("halt-ultrasonic", false)?;
let phase = reader.or_range::<u64>("put-phase", 0, 0..=1)?;
reader.finish()?;
let region = Region::from_name(name)
.ok_or_else(|| Error::Property(alloc::format!("unknown `region` `{name}`")))?;
let state = Arc::new(ApuState {
core: Mutex::with_rank(
LockRank::DEVICE,
Core::new(region, phase, halt_ultrasonic, capacity as usize),
),
irq: Mutex::with_rank(LockRank::WIRE, None),
domain: Mutex::with_rank(LockRank::LEAF, None),
lazy: Mutex::new(None),
ticks: AtomicU64::new(0),
region,
halt_ultrasonic,
});
let windows = core::array::from_fn(|i| {
let w = &WINDOWS[i];
Arc::new(MmioRegion::io(
w.region_name,
w.len,
Arc::new(ApuPort {
state: Arc::clone(&state),
first: w.offset as u8,
}) as Arc<dyn MemOps>,
))
});
Ok(Apu { state, windows })
}
pub fn tv_region(&self) -> Region {
self.state.region
}
pub fn connect_irq(&self, source: WireSource) {
*self.state.irq.lock() = Some(source);
self.refresh_irq();
}
pub fn irq_level(&self) -> Level {
self.state.irq_level()
}
fn refresh_irq(&self) {
self.state.refresh_irq();
}
pub fn attach_clock(&self, domain: DomainId) {
*self.state.domain.lock() = Some(domain);
}
pub fn clock_domain(&self) -> Option<DomainId> {
*self.state.domain.lock()
}
pub fn ticks(&self) -> u64 {
self.state.core.lock().ticks
}
pub fn frame_mode(&self) -> Mode {
self.state.core.lock().frame.mode()
}
pub fn frame_cycle(&self) -> u32 {
self.state.core.lock().frame.cycle()
}
pub fn dmc_output(&self) -> u8 {
self.state.core.lock().dmc.output()
}
pub fn advance(&self, cycles: u64) {
{
let mut core = self.state.core.lock();
for _ in 0..cycles {
core.tick();
}
self.state.publish(&core);
}
self.refresh_irq();
}
pub fn advance_to(&self, tick: u64) {
{
let mut core = self.state.core.lock();
while core.ticks < tick {
core.tick();
}
self.state.publish(&core);
}
self.refresh_irq();
}
pub fn attach_lazy(&self, handle: LazyHandle) {
*self.state.lazy.lock() = Some(handle);
}
pub fn write(&self, index: u8, value: u8) {
self.state.write(index, value);
}
pub fn read(&self, index: u8, bus: u8) -> u8 {
self.state.read_with(index, bus, false)
}
pub fn peek(&self, index: u8, bus: u8) -> u8 {
self.state.read_with(index, bus, true)
}
pub fn dma_request(&self) -> Option<DmaRequest> {
self.state.core.lock().dmc.dma_request()
}
pub fn dma_is_pending(&self, serial: u64) -> bool {
self.state.core.lock().dmc.dma_is_pending(serial)
}
pub fn dma_complete(&self, serial: u64, byte: u8) -> bool {
let now = self.state.core.lock().ticks;
let accepted = self.state.core.lock().dmc.dma_complete(serial, byte, now);
if accepted {
self.refresh_irq();
}
accepted
}
pub fn output(&self) -> u16 {
self.state.core.lock().mix()
}
pub fn take_samples(&self, out: &mut Vec<u16>) {
self.state.core.lock().samples.drain_into(out);
}
pub fn samples_dropped(&self) -> u64 {
self.state.core.lock().samples.dropped()
}
pub fn regions(&self) -> Vec<(u64, RegionRef)> {
WINDOWS
.iter()
.zip(self.windows.iter())
.map(|(w, region)| (w.offset, Arc::clone(region)))
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Window {
pub name: &'static str,
pub region_name: &'static str,
pub offset: u64,
pub len: u64,
}
pub static WINDOWS: &[Window; 3] = &[
Window {
name: "channels",
region_name: "nes.apu.channels",
offset: reg::PULSE1_CTRL as u64,
len: 0x14,
},
Window {
name: "status",
region_name: "nes.apu.status",
offset: reg::STATUS as u64,
len: 1,
},
Window {
name: "frame",
region_name: "nes.apu.frame",
offset: reg::FRAME as u64,
len: 1,
},
];
pub const IRQ_PIN: &str = "irq";
struct ApuPort {
state: Arc<ApuState>,
first: u8,
}
impl fmt::Debug for ApuPort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ApuPort")
.field("first", &self.first)
.finish_non_exhaustive()
}
}
impl ApuPort {
fn index(&self, offset: u64) -> Option<u8> {
u8::try_from(offset)
.ok()
.and_then(|o| self.first.checked_add(o))
}
}
impl MemOps for ApuPort {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(crate::core::error::BusError::BadAccess);
};
let index = self
.index(offset)
.ok_or(crate::core::error::BusError::BadAccess)?;
self.state.sync(attrs);
let bus = if index == reg::STATUS {
attrs.core_bus
} else {
attrs.bus
};
*byte = self.state.read_with(index, bus, attrs.debug);
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let [value] = src else {
return Err(crate::core::error::BusError::BadAccess);
};
let index = self
.index(offset)
.ok_or(crate::core::error::BusError::BadAccess)?;
if attrs.debug {
return Ok(());
}
self.state.sync(attrs);
self.state.write(index, *value);
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little).internal()
}
}
#[derive(Debug)]
pub struct DmcFetch {
state: Arc<ApuState>,
}
impl DmcFetch {
pub fn sync(&self) {
self.state.sync(MemAttrs::DEFAULT);
}
#[must_use]
pub fn request(&self) -> Option<DmaRequest> {
self.state.core.lock().dmc.dma_request()
}
#[must_use]
pub fn is_pending(&self, serial: u64) -> bool {
self.state.core.lock().dmc.dma_is_pending(serial)
}
pub fn withdraw(&self, serial: u64) {
self.state.core.lock().dmc.dma_withdraw(serial);
}
pub fn complete(&self, serial: u64, byte: u8) -> bool {
let now = self.state.core.lock().ticks;
let accepted = self.state.core.lock().dmc.dma_complete(serial, byte, now);
if accepted {
self.state.refresh_irq();
}
accepted
}
}
impl Device for Apu {
fn class(&self) -> &'static DeviceClass {
&APU_CLASS
}
fn export(&self, which: ExportId) -> Option<Export> {
(which == ExportId::DMC_FETCH).then(|| {
Export::Opaque(Arc::new(DmcFetch {
state: Arc::clone(&self.state),
}))
})
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, kind: ResetKind) {
{
let mut core = self.state.core.lock();
let region = self.state.region;
let halt = self.state.halt_ultrasonic;
core.reset(kind, region, halt);
self.state.publish(&core);
}
self.refresh_irq();
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
self.state.core.lock().save(w)
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
{
let mut core = self.state.core.lock();
core.load(r)?;
self.state.publish(&core);
}
self.refresh_irq();
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
let index = WINDOWS.iter().position(|w| w.name == name)?;
Some(Arc::clone(&self.windows[index]))
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
if port != IRQ_PIN {
return Err(Error::Config {
at: alloc::string::String::from(port),
message: alloc::format!("the APU drives only `{IRQ_PIN}`"),
});
}
self.connect_irq(source);
Ok(())
}
fn announce(&self, port: &str) {
if port == IRQ_PIN {
self.refresh_irq();
}
}
fn is_lazy(&self) -> bool {
true
}
fn current_tick(&self) -> u64 {
self.state.ticks.load(Ordering::Relaxed)
}
fn advance_to(&self, tick: u64) {
Apu::advance_to(self, tick);
}
fn next_event_tick(&self) -> Option<u64> {
None
}
fn attach_lazy(&self, handle: LazyHandle) {
Apu::attach_lazy(self, handle);
}
}
impl Instance for Apu {
fn bind(&self, ctx: &BindCtx<'_>) -> Result<()> {
if let Some(domain) = ctx.domain() {
self.attach_clock(domain);
}
Ok(())
}
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(APU_CLASS.name, |props| Ok(Arc::new(Apu::new(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PortDir, PropSchema};
let mut schema = ClassSchema::new(APU_CLASS.name)
.prop(PropSchema::new("region", ValueKind::Str).values(Region::NAMES))
.prop(PropSchema::new("sample-buffer", ValueKind::Uint))
.prop(PropSchema::new("halt-ultrasonic", ValueKind::Bool))
.prop(PropSchema::new("put-phase", ValueKind::Uint).range(0, 1))
.port(IRQ_PIN, PortDir::Out);
for window in WINDOWS {
schema = schema.region(window.name);
}
schema
}
static APU_PROPERTIES: &[PropertySpec] = &[
PropertySpec {
name: "region",
kind: ValueKind::Str,
required: false,
summary: "console variant: `ntsc` (RP2A03), `pal` (RP2A07) or `dendy` (UA6527P)",
},
PropertySpec {
name: "sample-buffer",
kind: ValueKind::Uint,
required: false,
summary: "audio ring capacity in samples; 0 produces no audio at all",
},
PropertySpec {
name: "halt-ultrasonic",
kind: ValueKind::Bool,
required: false,
summary: "halt the triangle when its period is below 2, trading accuracy for less popping",
},
PropertySpec {
name: "put-phase",
kind: ValueKind::Uint,
required: false,
summary: "CPU/APU cycle alignment at power-on (0 or 1); random on hardware",
},
];
pub static APU_CLASS: DeviceClass = DeviceClass {
name: "nes.apu",
version: 2,
summary: "NES APU (RP2A03 / RP2A07 / UA6527P audio): two pulse, triangle, noise, DMC",
properties: APU_PROPERTIES,
construct: |props| Ok(Box::new(Apu::new(props)?) as Box<dyn Device>),
};
pub fn register(registry: &mut Registry) -> Result<()> {
registry.add(&APU_CLASS)
}