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); }