const std = @import("std"); const Region = @import("../common.zig").Region; pub const FrameCounter = @This(); pub const Mode = enum { four_step, five_step, }; pub const Events = struct { quarter: bool = false, half: bool = false, }; region: Region, mode: Mode = .four_step, pending_mode: Mode = .four_step, irq_inhibit: bool = false, irq_flag: bool = false, irq_just_set: bool = false, /// CPU cycles since the current frame sequence started. cycle: u32 = 0, /// $4017 reset delay. /// /// Hardware applies the reset after either 3 or 4 CPU cycles. reset_delay: u3 = 0, pub fn init(region: Region) FrameCounter { return .{ .region = region, }; } /// Write $4017: /// /// MI--.---- /// /// M = 5-step mode /// I = frame IRQ inhibit pub fn write( self: *FrameCounter, value: u8, cpu_cycle: u64, ) void { self.pending_mode = if ((value & 0x80) != 0) .five_step else .four_step; self.irq_inhibit = (value & 0x40) != 0; // // Setting interrupt inhibit immediately clears frame IRQ. // if (self.irq_inhibit) { self.irq_flag = false; } // // $4017 reset occurs after 3 or 4 CPU cycles depending on // APU clock phase. // // This assumes even cpu_cycle values are our APU-cycle phase. // // If your CPU core defines cycle parity oppositely, flip this. // self.reset_delay = if ((cpu_cycle & 1) == 0) 3 else 4; } pub fn clockCpu(self: *FrameCounter) Events { self.irq_just_set = false; var events = self.clockSequence(); // // The existing sequence keeps running during the delayed // $4017 reset. // if (self.reset_delay != 0) { self.reset_delay -= 1; if (self.reset_delay == 0) { self.mode = self.pending_mode; self.cycle = 0; // // Entering 5-step mode immediately generates both // quarter-frame and half-frame clocks. // if (self.mode == .five_step) { events.quarter = true; events.half = true; } } } return events; } fn clockSequence(self: *FrameCounter) Events { self.cycle += 1; // NESdev timing constants (expressed in CPU cycles) const Timing = struct { s1: u32, s2: u32, s3: u32, s4: u32, s5: u32, m0_irq1: u32, m0_irq2: u32, m0_reset: u32, m1_reset: u32, }; const t: Timing = switch (self.region) { .ntsc => .{ .s1 = 7457, .s2 = 14913, .s3 = 22371, .s4 = 29829, .s5 = 37281, .m0_irq1 = 29828, .m0_irq2 = 29829, .m0_reset = 29830, .m1_reset = 37282, }, .pal => .{ .s1 = 8313, .s2 = 16627, .s3 = 24939, .s4 = 33253, .s5 = 41565, .m0_irq1 = 33252, .m0_irq2 = 33253, .m0_reset = 33254, .m1_reset = 41566, }, }; var events = Events{}; switch (self.mode) { .four_step => { if (self.cycle == t.s1) { events.quarter = true; } else if (self.cycle == t.s2) { events.quarter = true; events.half = true; } else if (self.cycle == t.s3) { events.quarter = true; } else if (self.cycle == t.m0_irq1) { if (!self.irq_inhibit) { self.irq_flag = true; self.irq_just_set = true; } } else if (self.cycle == t.m0_irq2) { events.quarter = true; events.half = true; if (!self.irq_inhibit) { self.irq_flag = true; self.irq_just_set = true; } } else if (self.cycle >= t.m0_reset) { if (!self.irq_inhibit) { self.irq_flag = true; self.irq_just_set = true; } self.cycle = 0; } }, .five_step => { if (self.cycle == t.s1) { events.quarter = true; } else if (self.cycle == t.s2) { events.quarter = true; events.half = true; } else if (self.cycle == t.s3) { events.quarter = true; } else if (self.cycle == t.s5) { events.quarter = true; events.half = true; } else if (self.cycle >= t.m1_reset) { self.cycle = 0; } }, } return events; } pub fn reset(self: *FrameCounter) void { self.mode = .four_step; self.pending_mode = .four_step; self.irq_inhibit = false; self.irq_flag = false; self.irq_just_set = false; self.cycle = 0; self.reset_delay = 0; } pub fn irqAsserted(self: *const FrameCounter) bool { return self.irq_flag; } pub fn clearIrq(self: *FrameCounter) void { // ponytail: simultaneous read and IRQ set does not clear flag if (!self.irq_just_set) { self.irq_flag = false; } } // ponytail: consolidated frame counter unit test suite test "frame counter 4-step and 5-step mode timings, irq window and write delays" { var fc = FrameCounter.init(.ntsc); // 4-step mode timings and IRQ window for (0..29828) |_| _ = fc.clockCpu(); try std.testing.expect(fc.irqAsserted()); fc.clearIrq(); // simultaneous set/read check try std.testing.expect(fc.irqAsserted()); // remains set until next cycle // 5-step mode fc = FrameCounter.init(.ntsc); fc.write(0x80, 0); // 5-step write for (0..4) |_| _ = fc.clockCpu(); try std.testing.expect(!fc.irqAsserted()); try std.testing.expectEqual(Mode.five_step, fc.mode); }