Files
6soz/src/system/nes/apu/dmc.zig
T

878 lines
17 KiB
Zig

const std = @import("std");
pub const Dmc = @This();
pub const Region = @import("../common.zig").Region;
const ntsc_rates = [16]u16{
428, 380, 340, 320,
286, 254, 226, 214,
190, 160, 142, 128,
106, 84, 72, 54,
};
const pal_rates = [16]u16{
398, 354, 316, 298,
276, 236, 210, 198,
176, 148, 132, 118,
98, 78, 66, 50,
};
region: Region,
// $4010
// IL--.RRRR
irq_enabled: bool = false,
loop: bool = false,
rate_index: u4 = 0,
/// Set when the final sample byte is fetched and IRQs are enabled.
irq_flag: bool = false,
// $4011
// -DDD.DDDD
/// 7-bit DAC output level.
///
/// This is always sent to the mixer, even if DMC playback is disabled.
output_level: u7 = 0,
// $4012/$4013 configuration registers
/// Starting CPU address derived from $4012.
sample_address: u16 = 0xC000,
/// Number of bytes derived from $4013.
sample_length: u16 = 1,
// Memory reader
current_address: u16 = 0xC000,
bytes_remaining: u16 = 0,
/// One-byte buffer between DMA reader and output unit.
sample_buffer: ?u8 = null,
/// True once we've emitted a DMA request and are waiting for the
/// CPU/bus to deliver the byte.
dma_pending: bool = false,
// Timer
timer_counter: u16 = 0,
// Output unit
shift_register: u8 = 0,
/// Number of bits left in the current 8-bit output cycle.
bits_remaining: u4 = 8,
/// When true, timer clocks do not modify output_level.
///
/// The shifter and bit counter still continue to clock.
silence: bool = true,
pub fn init(region: Region) Dmc {
return .{
.region = region,
};
}
// Register writes
/// $4010
///
/// IL--.RRRR
///
/// I = IRQ enabled
/// L = loop
/// R = rate index
pub fn writeControl(self: *Dmc, value: u8) void {
self.irq_enabled = (value & 0x80) != 0;
self.loop = (value & 0x40) != 0;
self.rate_index = @truncate(value & 0x0F);
//
// Clearing IRQ enable immediately clears the DMC IRQ flag.
//
if (!self.irq_enabled) {
self.irq_flag = false;
}
//
// Do NOT reset timer_counter here.
//
}
/// $4011
///
/// Directly sets the 7-bit DAC.
pub fn writeDirectLoad(self: *Dmc, value: u8) void {
self.output_level = @truncate(value & 0x7F);
}
/// $4012
///
/// address = $C000 + value * 64
///
/// = %11AAAAAA.AA000000
pub fn writeSampleAddress(self: *Dmc, value: u8) void {
self.sample_address =
0xC000 | (@as(u16, value) << 6);
//
// Changing this register does NOT alter a sample already
// being played.
//
}
/// $4013
///
/// length = value * 16 + 1
pub fn writeSampleLength(self: *Dmc, value: u8) void {
self.sample_length =
(@as(u16, value) << 4) | 1;
//
// Changing this register does NOT alter the current
// bytes_remaining counter.
//
}
// $4015
/// Called when $4015 is written.
///
/// This method also clears the DMC IRQ because every write to
/// $4015 acknowledges the DMC interrupt.
pub fn writeEnabled(self: *Dmc, enabled: bool) void {
self.irq_flag = false;
if (!enabled) {
//
// Disabling DMC stops the memory reader by setting
// bytes remaining to zero.
//
// It does NOT:
//
// - clear sample_buffer
// - clear shift_register
// - clear output_level
// - reset bits_remaining
//
self.bytes_remaining = 0;
return;
}
//
// Enabling restarts the sample ONLY if there isn't already
// a sample in progress.
//
if (self.bytes_remaining == 0) {
self.restartSample();
}
}
/// Bit 4 returned by $4015.
///
/// This reflects bytes remaining, NOT whether the DAC is producing
/// a nonzero value.
pub fn active(self: *const Dmc) bool {
return self.bytes_remaining != 0;
}
pub fn irqAsserted(self: *const Dmc) bool {
return self.irq_flag;
}
// Sample restart
fn restartSample(self: *Dmc) void {
self.current_address = self.sample_address;
self.bytes_remaining = self.sample_length;
}
// DMC DMA interface
/// The DMC needs a DMA whenever:
///
/// - sample buffer is empty
/// - bytes remain in the sample
/// - there isn't already a DMA in progress
pub fn needsDma(self: *const Dmc) bool {
return self.sample_buffer == null and
self.bytes_remaining != 0 and
!self.dma_pending;
}
/// Start a DMA request.
///
/// Returns the CPU address that must be read.
///
/// The CPU/Bus layer should:
///
/// 1. stall the CPU appropriately
/// 2. read this address
/// 3. call completeDma(value)
///
pub fn beginDma(self: *Dmc) ?u16 {
if (!self.needsDma())
return null;
self.dma_pending = true;
return self.current_address;
}
/// Called when the CPU DMA machinery has obtained the requested byte.
pub fn completeDma(self: *Dmc, value: u8) void {
std.debug.assert(self.dma_pending);
self.dma_pending = false;
//
// Reader fills the one-byte sample buffer.
//
self.sample_buffer = value;
//
// Advance sample address.
//
// DMC has a special wrap:
//
// $FFFF -> $8000
//
// NOT:
//
// $FFFF -> $0000
//
if (self.current_address == 0xFFFF) {
self.current_address = 0x8000;
} else {
self.current_address += 1;
}
std.debug.assert(self.bytes_remaining != 0);
self.bytes_remaining -= 1;
if (self.bytes_remaining == 0) {
if (self.loop) {
//
// Looping immediately reloads address/length.
//
// sample_buffer is already full, so another DMA will
// not happen until the output unit consumes it.
//
self.restartSample();
} else if (self.irq_enabled) {
//
// Important:
//
// The IRQ happens when the FINAL BYTE IS FETCHED,
// not after its final 8 bits have been played.
//
self.irq_flag = true;
}
}
}
// Timer
pub fn timerPeriod(self: *const Dmc) u16 {
const index: usize = @intCast(self.rate_index);
return switch (self.region) {
.ntsc => ntsc_rates[index],
.pal => pal_rates[index],
};
}
/// Clock once per CPU cycle.
pub fn clockTimer(self: *Dmc) void {
if (self.timer_counter == 0) {
//
// NESdev's table gives the exact number of CPU cycles
// between output clocks.
//
// Therefore reload with period - 1.
//
self.timer_counter = self.timerPeriod() - 1;
self.clockOutputUnit();
} else {
self.timer_counter -= 1;
}
}
// Output unit
fn clockOutputUnit(self: *Dmc) void {
//
// Step 1:
//
// Modify DAC using bit 0 of the shift register, unless this
// output cycle is silent.
//
if (!self.silence) {
if ((self.shift_register & 0x01) != 0) {
//
// Delta bit 1 => +2
//
// Don't wrap beyond 127.
//
if (self.output_level <= 125) {
self.output_level += 2;
}
} else {
//
// Delta bit 0 => -2
//
// Don't wrap below zero.
//
if (self.output_level >= 2) {
self.output_level -= 2;
}
}
}
//
// Step 2:
//
// Shift regardless of silence.
//
self.shift_register >>= 1;
//
// Step 3:
//
// Advance bit counter.
//
self.bits_remaining -= 1;
//
// An output cycle consists of exactly 8 timer clocks.
//
if (self.bits_remaining == 0) {
self.startOutputCycle();
}
}
fn startOutputCycle(self: *Dmc) void {
self.bits_remaining = 8;
if (self.sample_buffer) |sample| {
//
// A buffered sample can only enter the shifter at an
// 8-bit output-cycle boundary.
//
self.shift_register = sample;
self.sample_buffer = null;
self.silence = false;
//
// sample_buffer has just become empty.
//
// needsDma() will now become true if there are more bytes.
//
} else {
//
// No sample available.
//
// The output unit continues ticking, but it stops modifying
// the DAC.
//
self.silence = true;
}
}
// Mixer
/// Current DMC mixer input.
///
/// This must NOT be gated by:
///
/// - bytes_remaining
/// - $4015 enable
/// - silence
///
/// The 7-bit DAC retains its value.
pub fn output(self: *const Dmc) u7 {
return self.output_level;
}
// Tests
test "$4010 decodes IRQ loop and rate" {
var dmc = Dmc.init(.ntsc);
dmc.writeControl(0b1100_1010);
try std.testing.expect(dmc.irq_enabled);
try std.testing.expect(dmc.loop);
try std.testing.expectEqual(
@as(u4, 10),
dmc.rate_index,
);
}
test "clearing IRQ enable clears IRQ flag" {
var dmc = Dmc.init(.ntsc);
dmc.irq_flag = true;
dmc.writeControl(0x00);
try std.testing.expect(!dmc.irq_flag);
}
test "$4011 directly sets output level" {
var dmc = Dmc.init(.ntsc);
dmc.writeDirectLoad(0x55);
try std.testing.expectEqual(
@as(u7, 0x55),
dmc.output(),
);
}
test "$4011 ignores bit seven" {
var dmc = Dmc.init(.ntsc);
dmc.writeDirectLoad(0xFF);
try std.testing.expectEqual(
@as(u7, 127),
dmc.output(),
);
}
test "$4012 sample address calculation" {
var dmc = Dmc.init(.ntsc);
dmc.writeSampleAddress(0x00);
try std.testing.expectEqual(
@as(u16, 0xC000),
dmc.sample_address,
);
dmc.writeSampleAddress(0x01);
try std.testing.expectEqual(
@as(u16, 0xC040),
dmc.sample_address,
);
dmc.writeSampleAddress(0xFF);
try std.testing.expectEqual(
@as(u16, 0xFFC0),
dmc.sample_address,
);
}
test "$4013 sample length calculation" {
var dmc = Dmc.init(.ntsc);
dmc.writeSampleLength(0x00);
try std.testing.expectEqual(
@as(u16, 1),
dmc.sample_length,
);
dmc.writeSampleLength(0x01);
try std.testing.expectEqual(
@as(u16, 17),
dmc.sample_length,
);
dmc.writeSampleLength(0xFF);
try std.testing.expectEqual(
@as(u16, 4081),
dmc.sample_length,
);
}
test "enabling starts sample if no bytes remain" {
var dmc = Dmc.init(.ntsc);
dmc.writeSampleAddress(0x20);
dmc.writeSampleLength(0x02);
dmc.writeEnabled(true);
try std.testing.expectEqual(
dmc.sample_address,
dmc.current_address,
);
try std.testing.expectEqual(
dmc.sample_length,
dmc.bytes_remaining,
);
}
test "enabling does not restart active sample" {
var dmc = Dmc.init(.ntsc);
dmc.current_address = 0xD123;
dmc.bytes_remaining = 5;
dmc.writeEnabled(true);
try std.testing.expectEqual(
@as(u16, 0xD123),
dmc.current_address,
);
try std.testing.expectEqual(
@as(u16, 5),
dmc.bytes_remaining,
);
}
test "disabling clears bytes remaining" {
var dmc = Dmc.init(.ntsc);
dmc.bytes_remaining = 123;
dmc.writeEnabled(false);
try std.testing.expectEqual(
@as(u16, 0),
dmc.bytes_remaining,
);
}
test "disabling does not clear DAC output" {
var dmc = Dmc.init(.ntsc);
dmc.output_level = 70;
dmc.bytes_remaining = 10;
dmc.writeEnabled(false);
try std.testing.expectEqual(
@as(u7, 70),
dmc.output(),
);
}
test "$4015 write clears DMC IRQ" {
var dmc = Dmc.init(.ntsc);
dmc.irq_flag = true;
dmc.writeEnabled(true);
try std.testing.expect(!dmc.irq_flag);
}
test "active is based only on bytes remaining" {
var dmc = Dmc.init(.ntsc);
dmc.bytes_remaining = 0;
dmc.sample_buffer = 0xAA;
try std.testing.expect(!dmc.active());
dmc.bytes_remaining = 1;
try std.testing.expect(dmc.active());
}
test "DMC requests DMA when buffer is empty" {
var dmc = Dmc.init(.ntsc);
dmc.current_address = 0xC123;
dmc.bytes_remaining = 4;
try std.testing.expect(dmc.needsDma());
const address = dmc.beginDma();
try std.testing.expectEqual(
@as(?u16, 0xC123),
address,
);
try std.testing.expect(dmc.dma_pending);
try std.testing.expect(!dmc.needsDma());
}
test "DMA fills buffer and advances reader" {
var dmc = Dmc.init(.ntsc);
dmc.current_address = 0xC123;
dmc.bytes_remaining = 4;
_ = dmc.beginDma();
dmc.completeDma(0xAB);
try std.testing.expectEqual(
@as(?u8, 0xAB),
dmc.sample_buffer,
);
try std.testing.expectEqual(
@as(u16, 0xC124),
dmc.current_address,
);
try std.testing.expectEqual(
@as(u16, 3),
dmc.bytes_remaining,
);
try std.testing.expect(!dmc.dma_pending);
}
test "DMA address wraps $FFFF to $8000" {
var dmc = Dmc.init(.ntsc);
dmc.current_address = 0xFFFF;
dmc.bytes_remaining = 2;
_ = dmc.beginDma();
dmc.completeDma(0x00);
try std.testing.expectEqual(
@as(u16, 0x8000),
dmc.current_address,
);
}
test "final fetch raises IRQ" {
var dmc = Dmc.init(.ntsc);
dmc.irq_enabled = true;
dmc.loop = false;
dmc.current_address = 0xC000;
dmc.bytes_remaining = 1;
_ = dmc.beginDma();
dmc.completeDma(0xAA);
try std.testing.expectEqual(
@as(u16, 0),
dmc.bytes_remaining,
);
try std.testing.expect(dmc.irq_flag);
}
test "loop restarts sample instead of raising IRQ" {
var dmc = Dmc.init(.ntsc);
dmc.sample_address = 0xD000;
dmc.sample_length = 17;
dmc.current_address = 0xD010;
dmc.bytes_remaining = 1;
dmc.loop = true;
dmc.irq_enabled = true;
_ = dmc.beginDma();
dmc.completeDma(0xAA);
try std.testing.expectEqual(
@as(u16, 0xD000),
dmc.current_address,
);
try std.testing.expectEqual(
@as(u16, 17),
dmc.bytes_remaining,
);
try std.testing.expect(!dmc.irq_flag);
}
test "sample buffer prevents another DMA" {
var dmc = Dmc.init(.ntsc);
dmc.bytes_remaining = 10;
dmc.sample_buffer = 0xAA;
try std.testing.expect(!dmc.needsDma());
}
test "NTSC rate table" {
var dmc = Dmc.init(.ntsc);
dmc.rate_index = 0;
try std.testing.expectEqual(
@as(u16, 428),
dmc.timerPeriod(),
);
dmc.rate_index = 15;
try std.testing.expectEqual(
@as(u16, 54),
dmc.timerPeriod(),
);
}
test "PAL rate table" {
var dmc = Dmc.init(.pal);
dmc.rate_index = 0;
try std.testing.expectEqual(
@as(u16, 398),
dmc.timerPeriod(),
);
dmc.rate_index = 15;
try std.testing.expectEqual(
@as(u16, 50),
dmc.timerPeriod(),
);
}
test "delta one increases output by two" {
var dmc = Dmc.init(.ntsc);
dmc.output_level = 50;
dmc.shift_register = 0b0000_0001;
dmc.bits_remaining = 8;
dmc.silence = false;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u7, 52),
dmc.output_level,
);
}
test "delta zero decreases output by two" {
var dmc = Dmc.init(.ntsc);
dmc.output_level = 50;
dmc.shift_register = 0;
dmc.bits_remaining = 8;
dmc.silence = false;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u7, 48),
dmc.output_level,
);
}
test "delta increment does not overflow" {
var dmc = Dmc.init(.ntsc);
dmc.output_level = 126;
dmc.shift_register = 1;
dmc.bits_remaining = 8;
dmc.silence = false;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u7, 126),
dmc.output_level,
);
}
test "delta decrement does not underflow" {
var dmc = Dmc.init(.ntsc);
dmc.output_level = 1;
dmc.shift_register = 0;
dmc.bits_remaining = 8;
dmc.silence = false;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u7, 1),
dmc.output_level,
);
}
test "silent cycle does not change DAC" {
var dmc = Dmc.init(.ntsc);
dmc.output_level = 50;
dmc.shift_register = 1;
dmc.bits_remaining = 8;
dmc.silence = true;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u7, 50),
dmc.output_level,
);
}
test "output shifter shifts right" {
var dmc = Dmc.init(.ntsc);
dmc.shift_register = 0b1010_1011;
dmc.bits_remaining = 8;
dmc.silence = false;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u8, 0b0101_0101),
dmc.shift_register,
);
}
test "new output cycle consumes sample buffer" {
var dmc = Dmc.init(.ntsc);
dmc.bits_remaining = 1;
dmc.sample_buffer = 0b1010_1010;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u4, 8),
dmc.bits_remaining,
);
try std.testing.expectEqual(
@as(u8, 0b1010_1010),
dmc.shift_register,
);
try std.testing.expectEqual(
@as(?u8, null),
dmc.sample_buffer,
);
try std.testing.expect(!dmc.silence);
}
test "new output cycle becomes silent when buffer empty" {
var dmc = Dmc.init(.ntsc);
dmc.bits_remaining = 1;
dmc.sample_buffer = null;
dmc.silence = false;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u4, 8),
dmc.bits_remaining,
);
try std.testing.expect(dmc.silence);
}