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
+195
View File
@@ -0,0 +1,195 @@
const std = @import("std");
const common = @import("common.zig");
const Region = common.Region;
const Mirroring = common.Mirroring;
const Ppu = @import("ppu/root.zig").Ppu;
const Apu = @import("apu/root.zig").Apu;
const Controller = @import("controller.zig").Controller;
const Cartridge = @import("cartridge.zig").Cartridge;
pub const Bus = @This();
ram: [0x800]u8 = [_]u8{0} ** 0x800,
ciram: [0x800]u8 = [_]u8{0} ** 0x800,
ppu: Ppu,
apu: Apu,
controllers: [2]Controller = .{ Controller.init(), Controller.init() },
cartridge: ?Cartridge = null,
open_bus: u8 = 0,
// OAM DMA state machine
dma_page: u8 = 0,
dma_addr: u8 = 0,
dma_active: bool = false,
dma_dummy: bool = true,
pub fn init(region: Region, cart: ?Cartridge) Bus {
return .{
.ppu = Ppu.init(region),
.apu = Apu.init(region),
.cartridge = cart,
};
}
pub fn reset(self: *Bus) void {
self.ppu.reset();
self.apu.reset();
self.controllers[0].reset();
self.controllers[1].reset();
self.dma_active = false;
self.dma_dummy = true;
self.dma_addr = 0;
}
pub fn mapNametableAddress(mirroring_mode: Mirroring, address: u16) u11 {
const v = (address - 0x2000) & 0x0fff;
return switch (mirroring_mode) {
.horizontal => @truncate(((v >> 1) & 0x0400) | (v & 0x03ff)),
.vertical => @truncate(v & 0x07ff),
.single_screen_lower => @truncate(v & 0x03ff),
.single_screen_upper => @truncate(0x0400 | (v & 0x03ff)),
.four_screen => @truncate(v & 0x07ff),
};
}
pub const PpuBus = struct {
bus: *Bus,
pub fn read(self: *PpuBus, address: u16) u8 {
if (address < 0x2000) {
if (self.bus.cartridge) |*c| {
return c.ppuRead(address) orelse 0;
}
return 0;
} else if (address < 0x3f00) {
const mir = if (self.bus.cartridge) |*c| c.mirroring() else .horizontal;
const ciram_idx = mapNametableAddress(mir, address);
return self.bus.ciram[ciram_idx];
} else {
return self.bus.ppu.palette.read(address);
}
}
pub fn write(self: *PpuBus, address: u16, value: u8) void {
if (address < 0x2000) {
if (self.bus.cartridge) |*c| {
_ = c.ppuWrite(address, value);
}
} else if (address < 0x3f00) {
const mir = if (self.bus.cartridge) |*c| c.mirroring() else .horizontal;
const ciram_idx = mapNametableAddress(mir, address);
self.bus.ciram[ciram_idx] = value;
} else {
self.bus.ppu.palette.write(address, value);
}
}
};
pub inline fn getPpuBus(self: *Bus) PpuBus {
return PpuBus{ .bus = self };
}
pub fn read(self: *Bus, address: u16) u8 {
const val: u8 = switch (address) {
0x0000...0x1fff => self.ram[address & 0x07ff],
0x2000...0x3fff => blk: {
var ppu_bus = self.getPpuBus();
break :blk self.ppu.cpuRead(&ppu_bus, address) orelse self.open_bus;
},
0x4000...0x4014 => self.open_bus,
0x4015 => self.apu.readWithOpenBus(address, self.open_bus) orelse self.open_bus,
0x4016 => self.controllers[0].read(),
0x4017 => self.controllers[1].read(),
0x4018...0x401f => self.open_bus,
0x4020...0xffff => blk: {
if (self.cartridge) |*c| {
break :blk c.cpuRead(address) orelse self.open_bus;
}
break :blk self.open_bus;
},
};
self.open_bus = val;
return val;
}
pub fn write(self: *Bus, address: u16, value: u8) void {
self.open_bus = value;
switch (address) {
0x0000...0x1fff => self.ram[address & 0x07ff] = value,
0x2000...0x3fff => {
var ppu_bus = self.getPpuBus();
_ = self.ppu.cpuWrite(&ppu_bus, address, value);
},
0x4000...0x4013, 0x4015, 0x4017 => self.apu.write(address, value),
0x4014 => {
self.dma_page = value;
self.dma_addr = 0;
self.dma_active = true;
self.dma_dummy = true;
},
0x4016 => {
self.controllers[0].write(value);
self.controllers[1].write(value);
},
0x4018...0x401f => {},
0x4020...0xffff => {
if (self.cartridge) |*c| {
_ = c.cpuWrite(address, value);
}
},
}
}
/// Clocks one CPU cycle: steps APU and 3 PPU dots.
pub fn tick(self: *Bus) void {
self.apu.clockCpu();
self.ppu.registers.tickCpuCycle();
var ppu_bus = self.getPpuBus();
self.ppu.clock(&ppu_bus);
self.ppu.clock(&ppu_bus);
self.ppu.clock(&ppu_bus);
}
/// Executes one DMA CPU cycle step.
pub fn stepDma(self: *Bus) void {
if (self.dma_dummy) {
self.dma_dummy = false;
self.tick();
return;
}
const cpu_addr: u16 = (@as(u16, self.dma_page) << 8) | self.dma_addr;
const data = self.read(cpu_addr);
self.ppu.registers.oam[self.ppu.registers.oam_addr] = data;
self.ppu.registers.oam_addr +%= 1;
self.dma_addr +%= 1;
if (self.dma_addr == 0) {
self.dma_active = false;
}
self.tick();
}
pub fn nmiLine(self: *const Bus) bool {
return self.ppu.nmiLine();
}
pub fn irqLine(self: *const Bus) bool {
const apu_irq = self.apu.irqAsserted();
const cart_irq = if (self.cartridge) |*c| c.irqLine() else false;
return apu_irq or cart_irq;
}
// ponytail: consolidated bus memory mirroring test suite
test "bus internal RAM and CIRAM nametable mirroring" {
var bus = Bus.init(.ntsc, null);
bus.write(0x0005, 0x99);
try std.testing.expectEqual(@as(u8, 0x99), bus.read(0x0805));
var ppu_bus = bus.getPpuBus();
ppu_bus.write(0x2000, 0xaa);
try std.testing.expectEqual(@as(u8, 0xaa), ppu_bus.read(0x2400));
}