test(nes): consolidate and simplify inline subsystem unit tests

This commit is contained in:
2026-08-27 07:16:26 +02:00
parent f028213c11
commit 57f349fa17
35 changed files with 6856 additions and 0 deletions
+672
View File
@@ -0,0 +1,672 @@
const std = @import("std");
const LengthCounter = @import("length_counter.zig");
pub const Triangle = @This();
/// 32-step triangle DAC sequence.
///
/// Unlike pulse, the triangle directly sends one of these
/// 4-bit values to the mixer.
const sequence = [32]u4{
15, 14, 13, 12, 11, 10, 9, 8,
7, 6, 5, 4, 3, 2, 1, 0,
0, 1, 2, 3, 4, 5, 6, 7,
8, 9, 10, 11, 12, 13, 14, 15,
};
length_counter: LengthCounter = .{},
// $4008
// CRRR.RRRR
/// Bit 7 of $4008.
///
/// This has two functions:
///
/// - halts the length counter
/// - controls whether linear_counter_reload remains set
control: bool = false,
/// Bits 6..0 of $4008.
linear_reload_value: u7 = 0,
/// Current linear counter.
linear_counter: u7 = 0,
/// Set by every write to $400B.
linear_reload: bool = false,
// Timer
/// 11-bit timer period:
///
/// HHHLLLLLLLL
timer_period: u16 = 0,
/// Internal timer divider.
///
/// Triangle timer runs at the CPU clock, unlike pulse timers
/// which run at CPU / 2.
timer_counter: u16 = 0,
// Sequencer
/// Current position in the 32-step waveform.
sequence_position: u5 = 0,
pub fn init() Triangle {
return .{};
}
// $4015
/// Triangle is bit 2 of $4015.
///
/// Clearing it immediately clears the length counter.
///
/// Importantly, this does NOT reset the sequencer or linear counter.
pub fn setEnabled(self: *Triangle, enabled: bool) void {
self.length_counter.setEnabled(enabled);
}
/// Status returned as bit 2 when reading $4015.
///
/// $4015 status reports whether the length counter is non-zero.
pub fn active(self: *const Triangle) bool {
return self.length_counter.active();
}
// $4008
// CRRR.RRRR
//
// C = control / length-counter halt
// R = linear-counter reload value
pub fn writeLinear(self: *Triangle, value: u8) void {
self.control = (value & 0x80) != 0;
self.linear_reload_value = @truncate(value & 0x7f);
//
// IMPORTANT:
//
// Writing $4008 itself does NOT set linear_reload.
//
// $400B does that.
//
}
// $400A
// LLLL.LLLL
pub fn writeTimerLow(self: *Triangle, value: u8) void {
self.timer_period =
(self.timer_period & 0x0700) |
@as(u16, value);
}
// $400B
// LLLL.LHHH
//
// bits 7..3 = length counter index
// bits 2..0 = high timer bits
//
// Side effect:
// set linear-counter reload flag
pub fn writeTimerHigh(self: *Triangle, value: u8) void {
self.timer_period =
(self.timer_period & 0x00ff) |
(@as(u16, value & 0x07) << 8);
const length_index: u5 = @truncate(value >> 3);
self.length_counter.load(length_index);
//
// Every $400B write sets the reload flag.
//
self.linear_reload = true;
//
// Unlike pulse $4003/$4007:
//
// DO NOT reset sequence_position.
//
// DO NOT reset timer_counter.
//
}
// $4009
/// $4009 is unused.
pub fn writeUnused(_: *Triangle, _: u8) void {}
// Linear counter
/// Clocked on every quarter-frame.
///
/// NES behavior, in this exact order:
///
/// 1. If reload flag set:
/// counter = reload value
///
/// Otherwise, if counter > 0:
/// counter--
///
/// 2. If control flag clear:
/// reload flag = false
///
pub fn clockQuarterFrame(self: *Triangle) void {
if (self.linear_reload) {
self.linear_counter = self.linear_reload_value;
} else if (self.linear_counter != 0) {
self.linear_counter -= 1;
}
if (!self.control) {
self.linear_reload = false;
}
}
// Length counter
/// Clocked on every half-frame.
pub fn clockHalfFrame(self: *Triangle) void {
//
// $4008 bit 7 is also the length-counter halt flag.
//
self.length_counter.clock(self.control);
}
// Timer / waveform sequencer
/// Clock EVERY CPU cycle.
///
/// This differs from Pulse:
///
/// Pulse timer -> CPU / 2
/// Triangle timer -> CPU
///
pub fn clockTimer(self: *Triangle) void {
if (self.timer_counter == 0) {
self.timer_counter = self.timer_period;
//
// The waveform advances only when BOTH counters are non-zero.
//
if (self.linear_counter != 0 and
self.length_counter.active())
{
self.sequence_position +%= 1;
}
} else {
self.timer_counter -= 1;
}
}
// Output
/// Current 4-bit DAC output.
///
/// IMPORTANT:
///
/// Do NOT do this:
///
/// if (linear_counter == 0) return 0;
///
/// and do NOT do:
///
/// if (length_counter == 0) return 0;
///
/// Those counters stop the waveform sequencer. They do not force
/// the triangle DAC to zero.
///
/// When halted, the channel retains its current sequence value.
pub fn output(self: *const Triangle) u4 {
return sequence[@intCast(self.sequence_position)];
}
pub fn sequencerRunning(self: *const Triangle) bool {
return self.linear_counter != 0 and
self.length_counter.active();
}
// Inspection helpers
pub fn timerPeriod(self: *const Triangle) u16 {
return self.timer_period;
}
pub fn length(self: *const Triangle) u8 {
return self.length_counter.value;
}
pub fn linear(self: *const Triangle) u7 {
return self.linear_counter;
}
pub fn sequencePosition(self: *const Triangle) u5 {
return self.sequence_position;
}
// Tests
test "$4008 sets control and reload value" {
var triangle = Triangle.init();
triangle.writeLinear(0b1010_1010);
try std.testing.expect(triangle.control);
try std.testing.expectEqual(
@as(u7, 0x2a),
triangle.linear_reload_value,
);
}
test "$4008 does not set reload flag" {
var triangle = Triangle.init();
triangle.writeLinear(0xff);
try std.testing.expect(!triangle.linear_reload);
}
test "$400A sets timer low bits" {
var triangle = Triangle.init();
triangle.timer_period = 0x500;
triangle.writeTimerLow(0xab);
try std.testing.expectEqual(
@as(u16, 0x5ab),
triangle.timer_period,
);
}
test "$400B sets timer high bits" {
var triangle = Triangle.init();
triangle.timer_period = 0x0ab;
triangle.writeTimerHigh(0x05);
try std.testing.expectEqual(
@as(u16, 0x5ab),
triangle.timer_period,
);
}
test "$400B sets linear reload flag" {
var triangle = Triangle.init();
try std.testing.expect(!triangle.linear_reload);
triangle.writeTimerHigh(0);
try std.testing.expect(triangle.linear_reload);
}
test "$400B does not reset sequencer position" {
var triangle = Triangle.init();
triangle.sequence_position = 17;
triangle.writeTimerHigh(0);
try std.testing.expectEqual(
@as(u5, 17),
triangle.sequence_position,
);
}
test "$400B does not reset timer divider" {
var triangle = Triangle.init();
triangle.timer_counter = 123;
triangle.writeTimerHigh(0x05);
try std.testing.expectEqual(
@as(u16, 123),
triangle.timer_counter,
);
}
test "$400B loads length counter when enabled" {
var triangle = Triangle.init();
triangle.setEnabled(true);
// Length table index 0 = 10.
triangle.writeTimerHigh(0);
try std.testing.expectEqual(
@as(u8, 10),
triangle.length_counter.value,
);
}
test "$400B cannot load length while disabled" {
var triangle = Triangle.init();
triangle.setEnabled(false);
triangle.writeTimerHigh(0);
try std.testing.expectEqual(
@as(u8, 0),
triangle.length_counter.value,
);
}
test "$4015 disable clears length counter" {
var triangle = Triangle.init();
triangle.setEnabled(true);
triangle.writeTimerHigh(0);
try std.testing.expectEqual(
@as(u8, 10),
triangle.length_counter.value,
);
triangle.setEnabled(false);
try std.testing.expectEqual(
@as(u8, 0),
triangle.length_counter.value,
);
}
test "linear counter reloads when reload flag set" {
var triangle = Triangle.init();
triangle.linear_reload_value = 42;
triangle.linear_reload = true;
triangle.clockQuarterFrame();
try std.testing.expectEqual(
@as(u7, 42),
triangle.linear_counter,
);
}
test "linear counter decrements without reload" {
var triangle = Triangle.init();
triangle.linear_counter = 10;
triangle.linear_reload = false;
triangle.clockQuarterFrame();
try std.testing.expectEqual(
@as(u7, 9),
triangle.linear_counter,
);
}
test "linear counter stops at zero" {
var triangle = Triangle.init();
triangle.linear_counter = 0;
triangle.clockQuarterFrame();
try std.testing.expectEqual(
@as(u7, 0),
triangle.linear_counter,
);
}
test "control clear clears reload flag" {
var triangle = Triangle.init();
triangle.control = false;
triangle.linear_reload = true;
triangle.linear_reload_value = 10;
triangle.clockQuarterFrame();
try std.testing.expectEqual(
@as(u7, 10),
triangle.linear_counter,
);
try std.testing.expect(!triangle.linear_reload);
}
test "control set preserves reload flag" {
var triangle = Triangle.init();
triangle.control = true;
triangle.linear_reload = true;
triangle.linear_reload_value = 10;
triangle.clockQuarterFrame();
try std.testing.expectEqual(
@as(u7, 10),
triangle.linear_counter,
);
try std.testing.expect(triangle.linear_reload);
//
// Because reload remains set, every quarter frame reloads
// the linear counter back to 10.
//
triangle.linear_counter = 3;
triangle.clockQuarterFrame();
try std.testing.expectEqual(
@as(u7, 10),
triangle.linear_counter,
);
}
test "length counter decrements when control clear" {
var triangle = Triangle.init();
triangle.setEnabled(true);
triangle.writeTimerHigh(0);
try std.testing.expectEqual(
@as(u8, 10),
triangle.length_counter.value,
);
triangle.control = false;
triangle.clockHalfFrame();
try std.testing.expectEqual(
@as(u8, 9),
triangle.length_counter.value,
);
}
test "control flag halts length counter" {
var triangle = Triangle.init();
triangle.setEnabled(true);
triangle.writeTimerHigh(0);
triangle.control = true;
triangle.clockHalfFrame();
try std.testing.expectEqual(
@as(u8, 10),
triangle.length_counter.value,
);
}
test "timer runs for period plus one CPU clocks" {
var triangle = Triangle.init();
triangle.timer_period = 2;
triangle.timer_counter = 2;
triangle.linear_counter = 1;
triangle.length_counter.value = 1;
triangle.sequence_position = 0;
triangle.clockTimer();
try std.testing.expectEqual(
@as(u16, 1),
triangle.timer_counter,
);
try std.testing.expectEqual(
@as(u5, 0),
triangle.sequence_position,
);
triangle.clockTimer();
try std.testing.expectEqual(
@as(u16, 0),
triangle.timer_counter,
);
try std.testing.expectEqual(
@as(u5, 0),
triangle.sequence_position,
);
triangle.clockTimer();
try std.testing.expectEqual(
@as(u16, 2),
triangle.timer_counter,
);
try std.testing.expectEqual(
@as(u5, 1),
triangle.sequence_position,
);
}
test "sequencer does not advance when linear counter is zero" {
var triangle = Triangle.init();
triangle.timer_counter = 0;
triangle.timer_period = 10;
triangle.length_counter.value = 1;
triangle.linear_counter = 0;
triangle.sequence_position = 5;
triangle.clockTimer();
try std.testing.expectEqual(
@as(u5, 5),
triangle.sequence_position,
);
}
test "sequencer does not advance when length counter is zero" {
var triangle = Triangle.init();
triangle.timer_counter = 0;
triangle.timer_period = 10;
triangle.length_counter.value = 0;
triangle.linear_counter = 1;
triangle.sequence_position = 5;
triangle.clockTimer();
try std.testing.expectEqual(
@as(u5, 5),
triangle.sequence_position,
);
}
test "sequencer advances when both counters are nonzero" {
var triangle = Triangle.init();
triangle.timer_counter = 0;
triangle.timer_period = 10;
triangle.length_counter.value = 1;
triangle.linear_counter = 1;
triangle.sequence_position = 5;
triangle.clockTimer();
try std.testing.expectEqual(
@as(u5, 6),
triangle.sequence_position,
);
}
test "sequencer wraps after 31" {
var triangle = Triangle.init();
triangle.timer_counter = 0;
triangle.length_counter.value = 1;
triangle.linear_counter = 1;
triangle.sequence_position = 31;
triangle.clockTimer();
try std.testing.expectEqual(
@as(u5, 0),
triangle.sequence_position,
);
}
test "triangle waveform values" {
var triangle = Triangle.init();
triangle.sequence_position = 0;
try std.testing.expectEqual(@as(u4, 15), triangle.output());
triangle.sequence_position = 15;
try std.testing.expectEqual(@as(u4, 0), triangle.output());
triangle.sequence_position = 16;
try std.testing.expectEqual(@as(u4, 0), triangle.output());
triangle.sequence_position = 31;
try std.testing.expectEqual(@as(u4, 15), triangle.output());
}
test "halted triangle retains current DAC output" {
var triangle = Triangle.init();
triangle.sequence_position = 7;
//
// Sequence position 7 = DAC value 8.
//
try std.testing.expectEqual(
@as(u4, 8),
triangle.output(),
);
//
// Stop both gating counters.
//
triangle.linear_counter = 0;
triangle.length_counter.value = 0;
//
// The DAC is NOT forced to zero.
//
try std.testing.expectEqual(
@as(u4, 8),
triangle.output(),
);
}