196 lines
5.7 KiB
Zig
196 lines
5.7 KiB
Zig
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));
|
|
}
|