test(nes): consolidate and simplify inline subsystem unit tests
This commit is contained in:
@@ -0,0 +1,877 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user