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