From 425ebe0938ec49f38c624c1fd298d35235cc25e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20=C5=9Een?= Date: Thu, 27 Aug 2026 00:04:59 +0200 Subject: [PATCH] Remove nes implementation so that we can implement it in a better way from scratch. --- src/system/nes/apu.zig | 276 ------ src/system/nes/bus.zig | 157 ---- src/system/nes/cartridge.zig | 1131 ------------------------- src/system/nes/conformance.zig | 345 -------- src/system/nes/controller.zig | 70 -- src/system/nes/mapper/axrom.zig | 64 -- src/system/nes/mapper/camerica.zig | 82 -- src/system/nes/mapper/cnrom.zig | 69 -- src/system/nes/mapper/colordreams.zig | 73 -- src/system/nes/mapper/gxrom.zig | 73 -- src/system/nes/mapper/irem78.zig | 78 -- src/system/nes/mapper/mapper118.zig | 69 -- src/system/nes/mapper/mapper119.zig | 113 --- src/system/nes/mapper/mapper13.zig | 69 -- src/system/nes/mapper/mapper140.zig | 73 -- src/system/nes/mapper/mapper180.zig | 70 -- src/system/nes/mapper/mapper206.zig | 108 --- src/system/nes/mapper/mapper210.zig | 105 --- src/system/nes/mapper/mapper232.zig | 77 -- src/system/nes/mapper/mapper34.zig | 92 -- src/system/nes/mapper/mapper65.zig | 118 --- src/system/nes/mapper/mapper68.zig | 103 --- src/system/nes/mapper/mapper69.zig | 123 --- src/system/nes/mapper/mapper76.zig | 91 -- src/system/nes/mapper/mapper87.zig | 70 -- src/system/nes/mapper/mapper88.zig | 103 --- src/system/nes/mapper/mapper89.zig | 78 -- src/system/nes/mapper/mapper93.zig | 73 -- src/system/nes/mapper/mapper94.zig | 71 -- src/system/nes/mapper/mmc1.zig | 139 --- src/system/nes/mapper/mmc2.zig | 104 --- src/system/nes/mapper/mmc3.zig | 161 ---- src/system/nes/mapper/mmc4.zig | 117 --- src/system/nes/mapper/mmc5.zig | 171 ---- src/system/nes/mapper/nina03.zig | 73 -- src/system/nes/mapper/nrom.zig | 71 -- src/system/nes/mapper/root.zig | 147 ---- src/system/nes/mapper/types.zig | 9 - src/system/nes/mapper/uxrom.zig | 73 -- src/system/nes/mapper/vrc1.zig | 106 --- src/system/nes/mapper/vrc2.zig | 127 --- src/system/nes/mapper/vrc3.zig | 118 --- src/system/nes/mapper/vrc4.zig | 179 ---- src/system/nes/mapper/vrc6.zig | 152 ---- src/system/nes/mapper/vrc7.zig | 145 ---- src/system/nes/ppu.zig | 779 ----------------- src/system/nes/root.zig | 421 --------- 47 files changed, 7116 deletions(-) delete mode 100644 src/system/nes/apu.zig delete mode 100644 src/system/nes/bus.zig delete mode 100644 src/system/nes/cartridge.zig delete mode 100644 src/system/nes/conformance.zig delete mode 100644 src/system/nes/controller.zig delete mode 100644 src/system/nes/mapper/axrom.zig delete mode 100644 src/system/nes/mapper/camerica.zig delete mode 100644 src/system/nes/mapper/cnrom.zig delete mode 100644 src/system/nes/mapper/colordreams.zig delete mode 100644 src/system/nes/mapper/gxrom.zig delete mode 100644 src/system/nes/mapper/irem78.zig delete mode 100644 src/system/nes/mapper/mapper118.zig delete mode 100644 src/system/nes/mapper/mapper119.zig delete mode 100644 src/system/nes/mapper/mapper13.zig delete mode 100644 src/system/nes/mapper/mapper140.zig delete mode 100644 src/system/nes/mapper/mapper180.zig delete mode 100644 src/system/nes/mapper/mapper206.zig delete mode 100644 src/system/nes/mapper/mapper210.zig delete mode 100644 src/system/nes/mapper/mapper232.zig delete mode 100644 src/system/nes/mapper/mapper34.zig delete mode 100644 src/system/nes/mapper/mapper65.zig delete mode 100644 src/system/nes/mapper/mapper68.zig delete mode 100644 src/system/nes/mapper/mapper69.zig delete mode 100644 src/system/nes/mapper/mapper76.zig delete mode 100644 src/system/nes/mapper/mapper87.zig delete mode 100644 src/system/nes/mapper/mapper88.zig delete mode 100644 src/system/nes/mapper/mapper89.zig delete mode 100644 src/system/nes/mapper/mapper93.zig delete mode 100644 src/system/nes/mapper/mapper94.zig delete mode 100644 src/system/nes/mapper/mmc1.zig delete mode 100644 src/system/nes/mapper/mmc2.zig delete mode 100644 src/system/nes/mapper/mmc3.zig delete mode 100644 src/system/nes/mapper/mmc4.zig delete mode 100644 src/system/nes/mapper/mmc5.zig delete mode 100644 src/system/nes/mapper/nina03.zig delete mode 100644 src/system/nes/mapper/nrom.zig delete mode 100644 src/system/nes/mapper/root.zig delete mode 100644 src/system/nes/mapper/types.zig delete mode 100644 src/system/nes/mapper/uxrom.zig delete mode 100644 src/system/nes/mapper/vrc1.zig delete mode 100644 src/system/nes/mapper/vrc2.zig delete mode 100644 src/system/nes/mapper/vrc3.zig delete mode 100644 src/system/nes/mapper/vrc4.zig delete mode 100644 src/system/nes/mapper/vrc6.zig delete mode 100644 src/system/nes/mapper/vrc7.zig delete mode 100644 src/system/nes/ppu.zig diff --git a/src/system/nes/apu.zig b/src/system/nes/apu.zig deleted file mode 100644 index eab9f36..0000000 --- a/src/system/nes/apu.zig +++ /dev/null @@ -1,276 +0,0 @@ -const std = @import("std"); - -const Apu = @This(); - -pub const sample_rate: u32 = 44_100; - -sample_buffer: [2048]i16 = [_]i16{0} ** 2048, -sample_count: usize = 0, -cycle_counter: u32 = 0, - -// Pulse 1 -pulse1_enabled: bool = false, -pulse1_timer_reload: u11 = 0, -pulse1_timer: u11 = 0, -pulse1_duty: u2 = 0, -pulse1_duty_pos: u3 = 0, -pulse1_volume: u4 = 0, - -// Pulse 2 -pulse2_enabled: bool = false, -pulse2_timer_reload: u11 = 0, -pulse2_timer: u11 = 0, -pulse2_duty: u2 = 0, -pulse2_duty_pos: u3 = 0, -pulse2_volume: u4 = 0, - -// Triangle -triangle_enabled: bool = false, -triangle_timer_reload: u11 = 0, -triangle_timer: u11 = 0, -triangle_seq_pos: u5 = 0, - -// Noise -noise_enabled: bool = false, -noise_timer_reload: u12 = 0, -noise_timer: u12 = 0, -noise_lfsr: u16 = 1, -noise_volume: u4 = 0, - -// DMC -dmc_enabled: bool = false, -dmc_loop: bool = false, -dmc_rate_index: u4 = 0, -dmc_timer: u12 = 0, -dmc_timer_reload: u12 = 0, -dmc_output_level: u7 = 0, -dmc_sample_address: u16 = 0xC000, -dmc_sample_length: u16 = 1, -dmc_current_address: u16 = 0xC000, -dmc_bytes_remaining: u16 = 0, -dmc_shift_register: u8 = 0, -dmc_bits_remaining: u4 = 0, -dmc_silence: bool = true, - -const duty_tables: [4][8]u8 = .{ - .{ 0, 1, 0, 0, 0, 0, 0, 0 }, // 12.5% - .{ 0, 1, 1, 0, 0, 0, 0, 0 }, // 25% - .{ 0, 1, 1, 1, 1, 0, 0, 0 }, // 50% - .{ 1, 0, 0, 1, 1, 1, 1, 1 }, // 75% -}; - -const triangle_table: [32]u4 = .{ - 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, -}; - -const noise_periods: [16]u12 = .{ - 4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068, -}; - -const dmc_periods: [16]u12 = .{ - 428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54, -}; - -pub fn reset(self: *Apu) void { - self.sample_count = 0; - self.cycle_counter = 0; - self.pulse1_enabled = false; - self.pulse2_enabled = false; - self.triangle_enabled = false; - self.noise_enabled = false; - self.noise_lfsr = 1; - self.dmc_enabled = false; - self.dmc_output_level = 0; -} - -pub inline fn tick(self: *Apu) void { - // Pulse 1 timer - if (self.pulse1_timer == 0) { - self.pulse1_timer = self.pulse1_timer_reload; - self.pulse1_duty_pos +%= 1; - } else { - self.pulse1_timer -= 1; - } - - // Pulse 2 timer - if (self.pulse2_timer == 0) { - self.pulse2_timer = self.pulse2_timer_reload; - self.pulse2_duty_pos +%= 1; - } else { - self.pulse2_timer -= 1; - } - - // Triangle timer - if (self.triangle_timer == 0) { - self.triangle_timer = self.triangle_timer_reload; - self.triangle_seq_pos +%= 1; - } else { - self.triangle_timer -= 1; - } - - // Noise timer - if (self.noise_timer == 0) { - self.noise_timer = self.noise_timer_reload; - const feedback = (self.noise_lfsr & 1) ^ ((self.noise_lfsr >> 1) & 1); - self.noise_lfsr = (self.noise_lfsr >> 1) | (feedback << 14); - } else { - self.noise_timer -= 1; - } - - // DMC timer - if (self.dmc_timer == 0) { - self.dmc_timer = self.dmc_timer_reload; - if (!self.dmc_silence) { - if ((self.dmc_shift_register & 1) != 0) { - if (self.dmc_output_level <= 125) self.dmc_output_level += 2; - } else { - if (self.dmc_output_level >= 2) self.dmc_output_level -= 2; - } - self.dmc_shift_register >>= 1; - } - - if (self.dmc_bits_remaining > 0) { - self.dmc_bits_remaining -= 1; - } - if (self.dmc_bits_remaining == 0) { - self.dmc_silence = true; - } - } else { - self.dmc_timer -= 1; - } - - // Sample downsampling at ~44.1 kHz (every ~40 CPU cycles) - self.cycle_counter += 1; - if (self.cycle_counter >= 40) { - self.cycle_counter = 0; - if (self.sample_count < self.sample_buffer.len) { - const p1_val: i32 = if (self.pulse1_enabled and duty_tables[self.pulse1_duty][self.pulse1_duty_pos] != 0 and self.pulse1_timer_reload > 8) - @as(i32, self.pulse1_volume) - else - 0; - - const p2_val: i32 = if (self.pulse2_enabled and duty_tables[self.pulse2_duty][self.pulse2_duty_pos] != 0 and self.pulse2_timer_reload > 8) - @as(i32, self.pulse2_volume) - else - 0; - - const tri_val: i32 = if (self.triangle_enabled and self.triangle_timer_reload > 2) - @as(i32, triangle_table[self.triangle_seq_pos]) - else - 0; - - const noise_val: i32 = if (self.noise_enabled and (self.noise_lfsr & 1) == 0) - @as(i32, self.noise_volume) - else - 0; - - const dmc_val: i32 = if (self.dmc_enabled) @as(i32, self.dmc_output_level) else 0; - - const sample_i32 = (p1_val + p2_val) * 400 + tri_val * 350 + noise_val * 300 + dmc_val * 150; - self.sample_buffer[self.sample_count] = @intCast(std.math.clamp(sample_i32, -32000, 32000)); - self.sample_count += 1; - } - } -} - -pub fn write(self: *Apu, address: u16, value: u8) void { - switch (address) { - // Pulse 1 - 0x4000 => { - self.pulse1_duty = @truncate((value >> 6) & 3); - self.pulse1_volume = @truncate(value & 0x0f); - }, - 0x4002 => { - self.pulse1_timer_reload = (self.pulse1_timer_reload & 0x0700) | value; - }, - 0x4003 => { - self.pulse1_timer_reload = (self.pulse1_timer_reload & 0x00ff) | (@as(u11, value & 7) << 8); - self.pulse1_duty_pos = 0; - }, - - // Pulse 2 - 0x4004 => { - self.pulse2_duty = @truncate((value >> 6) & 3); - self.pulse2_volume = @truncate(value & 0x0f); - }, - 0x4006 => { - self.pulse2_timer_reload = (self.pulse2_timer_reload & 0x0700) | value; - }, - 0x4007 => { - self.pulse2_timer_reload = (self.pulse2_timer_reload & 0x00ff) | (@as(u11, value & 7) << 8); - self.pulse2_duty_pos = 0; - }, - - // Triangle - 0x400a => { - self.triangle_timer_reload = (self.triangle_timer_reload & 0x0700) | value; - }, - 0x400b => { - self.triangle_timer_reload = (self.triangle_timer_reload & 0x00ff) | (@as(u11, value & 7) << 8); - }, - - // Noise - 0x400c => { - self.noise_volume = @truncate(value & 0x0f); - }, - 0x400e => { - self.noise_timer_reload = noise_periods[value & 0x0f]; - }, - - // DMC - 0x4010 => { - self.dmc_loop = (value & 0x40) != 0; - self.dmc_rate_index = @truncate(value & 0x0f); - self.dmc_timer_reload = dmc_periods[self.dmc_rate_index]; - }, - 0x4011 => { - self.dmc_output_level = @truncate(value & 0x7f); - }, - 0x4012 => { - self.dmc_sample_address = 0xC000 + (@as(u16, value) << 6); - }, - 0x4013 => { - self.dmc_sample_length = (@as(u16, value) << 4) + 1; - }, - else => {}, - } -} - -pub fn readStatus(_: *Apu) u8 { - return 0; -} - -pub fn writeStatus(self: *Apu, value: u8) void { - self.pulse1_enabled = (value & 0x01) != 0; - self.pulse2_enabled = (value & 0x02) != 0; - self.triangle_enabled = (value & 0x04) != 0; - self.noise_enabled = (value & 0x08) != 0; - self.dmc_enabled = (value & 0x10) != 0; -} - -pub fn writeFrameCounter(_: *Apu, _: u8) void {} - -pub fn irqAsserted(_: *const Apu) bool { - return false; -} - -test "NES APU - Sound Synthesis & Sample Generation" { - var apu = Apu{}; - apu.reset(); - - // Enable Pulse 1 and Pulse 2 - apu.writeStatus(0x03); - - // Setup Pulse 1 volume and frequency - apu.write(0x4000, 0xbf); // 50% duty, max volume 15 - apu.write(0x4002, 100); - apu.write(0x4003, 0); - - // Tick APU for 100 cycles to produce PCM samples - for (0..100) |_| { - apu.tick(); - } - - try std.testing.expect(apu.sample_count > 0); -} diff --git a/src/system/nes/bus.zig b/src/system/nes/bus.zig deleted file mode 100644 index 09f0967..0000000 --- a/src/system/nes/bus.zig +++ /dev/null @@ -1,157 +0,0 @@ -const Cartridge = @import("cartridge.zig"); - -const Controller = @import("controller.zig"); - -const Ppu = @import("ppu.zig"); - -const Apu = @import("apu.zig"); - -const Bus = @This(); - -ram: [0x800]u8 = - [_]u8{0} ** 0x800, - -cartridge: Cartridge, - -ppu: Ppu = .{}, -apu: Apu = .{}, - -controllers: [2]Controller = .{ - .{}, - .{}, -}, - -open_bus: u8 = 0, - -oam_dma_page: ?u8 = null, - -pub fn init( - rom: []const u8, -) Cartridge.Error!Bus { - return .{ - .cartridge = try Cartridge.init(rom), - }; -} - -pub fn reset( - self: *Bus, -) void { - self.ppu.reset(); - self.apu.reset(); - - for (&self.controllers) |*controller| - controller.reset(); - - self.oam_dma_page = null; -} - -pub inline fn read( - self: *Bus, - address: u16, -) u8 { - const value: u8 = switch (address) { - // 2 KiB internal RAM + mirrors. - 0x0000...0x1fff => self.ram[address & 0x07ff], - - // PPU registers + mirrors. - 0x2000...0x3fff => self.ppu.cpuRead( - &self.cartridge, - address & 7, - ), - - // APU status. - 0x4015 => self.apu.readStatus(), - - // Controller 1. - 0x4016 => self.controllers[0].read(), - - // Controller 2. - 0x4017 => self.controllers[1].read(), - - // Cartridge space. - 0x4020...0xffff => self.cartridge.cpuRead(address), - - else => self.open_bus, - }; - - self.open_bus = value; - - return value; -} - -pub inline fn write( - self: *Bus, - address: u16, - value: u8, -) void { - self.open_bus = value; - - switch (address) { - 0x0000...0x1fff => self.ram[address & 0x07ff] = - value, - - 0x2000...0x3fff => self.ppu.cpuWrite( - &self.cartridge, - address & 7, - value, - ), - - 0x4000...0x4013 => self.apu.write( - address, - value, - ), - - // OAMDMA - 0x4014 => self.oam_dma_page = value, - - 0x4015 => self.apu.writeStatus(value), - - 0x4016 => { - self.controllers[0].write(value); - self.controllers[1].write(value); - }, - - 0x4017 => self.apu.writeFrameCounter(value), - - else => self.cartridge.cpuWrite( - address, - value, - ), - } -} - -/// Called exactly once for every CPU cycle. -pub inline fn tick( - self: *Bus, -) void { - self.apu.tick(); - - // NTSC NES: PPU runs at 3× CPU clock. - self.ppu.tick(&self.cartridge); - self.ppu.tick(&self.cartridge); - self.ppu.tick(&self.cartridge); -} - -pub fn nmiLine( - self: *Bus, -) bool { - return self.ppu.nmiAsserted(); -} - -pub fn irqLine( - self: *Bus, -) bool { - return self.apu.irqAsserted() or - self.cartridge.irqAsserted(); -} - -pub fn takeOamDmaPage( - self: *Bus, -) ?u8 { - const page = - self.oam_dma_page; - - self.oam_dma_page = null; - - return page; -} diff --git a/src/system/nes/cartridge.zig b/src/system/nes/cartridge.zig deleted file mode 100644 index e15f4ff..0000000 --- a/src/system/nes/cartridge.zig +++ /dev/null @@ -1,1131 +0,0 @@ -const std = @import("std"); -const _mapper = @import("mapper/root.zig"); - -const Mapper = _mapper.Mapper; -const Mirroring = _mapper.Mirroring; - -pub const Error = error{ - InvalidHeader, - UnsupportedNes2, - UnsupportedMapper, - InvalidPrgSize, - InvalidChrSize, - TruncatedRom, -}; - -const Cartridge = @This(); -/// Borrowed directly from the loaded ROM image. -/// -/// The ROM buffer must therefore outlive Cartridge. -prg_rom: []const u8, -chr_rom: []const u8, - -/// Used when CHR ROM bank count is zero. -chr_ram: [0x2000]u8 = - [_]u8{0} ** 0x2000, - -chr_is_ram: bool, - -/// Initial NROM PRG RAM support. -prg_ram: [0x2000]u8 = - [_]u8{0} ** 0x2000, - -mapper: Mapper, -cached_mirroring: Mirroring = .vertical, - -pub fn init( - rom: []const u8, -) Error!Cartridge { - if (rom.len < 16) - return error.TruncatedRom; - - if (!std.mem.eql( - u8, - rom[0..4], - "NES\x1a", - )) { - return error.InvalidHeader; - } - - const prg_banks = rom[4]; - const chr_banks = rom[5]; - - const flags6 = rom[6]; - const flags7 = rom[7]; - - const is_nes2 = (flags7 & 0x0c) == 0x08; - - var mapper_id: u16 = (flags6 >> 4) | (flags7 & 0xf0); - var prg_size: usize = @as(usize, prg_banks) * 0x4000; - var chr_size: usize = @as(usize, chr_banks) * 0x2000; - - if (is_nes2) { - mapper_id |= @as(u16, rom[8] & 0x0f) << 8; - - const prg_msb = rom[9] & 0x0f; - if (prg_msb < 0x0f) { - prg_size = ((@as(usize, prg_msb) << 8) | prg_banks) * 0x4000; - } else { - const exponent = (prg_banks >> 2) & 0x3f; - const multiplier = prg_banks & 0x03; - prg_size = (@as(usize, 1) << @intCast(exponent)) * (multiplier * 2 + 1); - } - - const chr_msb = (rom[9] >> 4) & 0x0f; - if (chr_msb < 0x0f) { - chr_size = ((@as(usize, chr_msb) << 8) | chr_banks) * 0x2000; - } else { - const exponent = (chr_banks >> 2) & 0x3f; - const multiplier = chr_banks & 0x03; - chr_size = (@as(usize, 1) << @intCast(exponent)) * (multiplier * 2 + 1); - } - } - - switch (mapper_id) { - 0, 3 => { - if (prg_size != 0x4000 and prg_size != 0x8000) - return error.InvalidPrgSize; - }, - 1, 2, 4, 5, 7, 9, 10, 11, 13, 21, 22, 23, 24, 25, 26, 34, 65, 66, 68, 69, 71, 73, 75, 76, 78, 79, 85, 87, 88, 89, 93, 94, 118, 119, 140, 180, 206, 210, 232 => { - if (prg_size < 0x4000) - return error.InvalidPrgSize; - }, - else => return error.UnsupportedMapper, - } - - const trainer_size: usize = - if ((flags6 & 0x04) != 0) - 512 - else - 0; - - const prg_start = - 16 + trainer_size; - - const prg_end = - prg_start + prg_size; - - const chr_end = - prg_end + chr_size; - - if (chr_end > rom.len) - return error.TruncatedRom; - - const _mirroring: Mirroring = - if ((flags6 & 0x08) != 0) - .four_screen - else if ((flags6 & 0x01) != 0) - .vertical - else - .horizontal; - - const mapper: Mapper = switch (mapper_id) { - 0 => .{ - .nrom = _mapper.Nrom.init( - prg_size, - _mirroring, - ), - }, - 1 => .{ - .mmc1 = _mapper.Mmc1.init( - prg_size, - chr_size, - ), - }, - 2 => .{ - .uxrom = _mapper.Uxrom.init( - prg_size, - _mirroring, - ), - }, - 3 => .{ - .cnrom = _mapper.Cnrom.init( - prg_size, - chr_size, - _mirroring, - ), - }, - 4 => .{ - .mmc3 = _mapper.Mmc3.init( - prg_size, - chr_size, - ), - }, - 5 => .{ - .mmc5 = _mapper.Mmc5.init( - prg_size, - chr_size, - ), - }, - 7 => .{ - .axrom = _mapper.Axrom.init( - prg_size, - ), - }, - 9 => .{ - .mmc2 = _mapper.Mmc2.init( - prg_size, - chr_size, - ), - }, - 10 => .{ - .mmc4 = _mapper.Mmc4.init( - prg_size, - chr_size, - ), - }, - 11 => .{ - .color_dreams = _mapper.ColorDreams.init( - prg_size, - chr_size, - _mirroring, - ), - }, - 13 => .{ - .mapper13 = _mapper.Mapper13.init( - prg_size, - _mirroring, - ), - }, - 21, 25 => .{ - .vrc4 = _mapper.Vrc4.init( - prg_size, - chr_size, - @truncate(mapper_id), - ), - }, - 22, 23 => .{ - .vrc2 = _mapper.Vrc2.init( - prg_size, - chr_size, - @truncate(mapper_id), - ), - }, - 24, 26 => .{ - .vrc6 = _mapper.Vrc6.init( - prg_size, - chr_size, - @truncate(mapper_id), - ), - }, - 34 => .{ - .mapper34 = _mapper.Mapper34.init( - prg_size, - chr_size, - _mirroring, - ), - }, - 65 => .{ - .mapper65 = _mapper.Mapper65.init( - prg_size, - chr_size, - _mirroring, - ), - }, - 66 => .{ - .gxrom = _mapper.Gxrom.init( - prg_size, - chr_size, - _mirroring, - ), - }, - 68 => .{ - .mapper68 = _mapper.Mapper68.init( - prg_size, - chr_size, - _mirroring, - ), - }, - 69 => .{ - .mapper69 = _mapper.Mapper69.init( - prg_size, - chr_size, - _mirroring, - ), - }, - 71 => .{ - .camerica = _mapper.Camerica.init( - prg_size, - _mirroring, - ), - }, - 73 => .{ - .vrc3 = _mapper.Vrc3.init( - prg_size, - _mirroring, - ), - }, - 75 => .{ - .vrc1 = _mapper.Vrc1.init( - prg_size, - chr_size, - ), - }, - 76 => .{ - .mapper76 = _mapper.Mapper76.init( - prg_size, - chr_size, - _mirroring, - ), - }, - 78 => .{ - .irem78 = _mapper.Irem78.init( - prg_size, - chr_size, - ), - }, - 79 => .{ - .nina03 = _mapper.Nina03.init( - prg_size, - chr_size, - _mirroring, - ), - }, - 85 => .{ - .vrc7 = _mapper.Vrc7.init( - prg_size, - chr_size, - ), - }, - 87 => .{ - .mapper87 = _mapper.Mapper87.init( - prg_size, - chr_size, - _mirroring, - ), - }, - 88 => .{ - .mapper88 = _mapper.Mapper88.init( - prg_size, - chr_size, - _mirroring, - ), - }, - 89 => .{ - .mapper89 = _mapper.Mapper89.init( - prg_size, - chr_size, - ), - }, - 93 => .{ - .mapper93 = _mapper.Mapper93.init( - prg_size, - _mirroring, - ), - }, - 94 => .{ - .mapper94 = _mapper.Mapper94.init( - prg_size, - _mirroring, - ), - }, - 118 => .{ - .mapper118 = _mapper.Mapper118.init( - prg_size, - chr_size, - ), - }, - 119 => .{ - .mapper119 = _mapper.Mapper119.init( - prg_size, - chr_size, - ), - }, - 140 => .{ - .mapper140 = _mapper.Mapper140.init( - prg_size, - chr_size, - _mirroring, - ), - }, - 180 => .{ - .mapper180 = _mapper.Mapper180.init( - prg_size, - _mirroring, - ), - }, - 206 => .{ - .mapper206 = _mapper.Mapper206.init( - prg_size, - chr_size, - _mirroring, - ), - }, - 210 => .{ - .mapper210 = _mapper.Mapper210.init( - prg_size, - chr_size, - ), - }, - 232 => .{ - .mapper232 = _mapper.Mapper232.init( - prg_size, - _mirroring, - ), - }, - else => unreachable, - }; - - return .{ - .prg_rom = rom[prg_start..prg_end], - .chr_rom = rom[prg_end..chr_end], - .chr_is_ram = (chr_banks == 0), - .mapper = mapper, - .cached_mirroring = mapper.mirroring(), - }; -} - -pub inline fn cpuRead( - self: *Cartridge, - address: u16, -) u8 { - if (address >= 0x6000 and - address <= 0x7fff) - { - return self.prg_ram[ - @as(usize, address) - 0x6000 - ]; - } - - if (self.mapper.cpuMapRead(address)) |offset| { - return self.prg_rom[offset]; - } - - return 0; -} - -pub inline fn cpuWrite( - self: *Cartridge, - address: u16, - value: u8, -) void { - if (address >= 0x6000 and - address <= 0x7fff) - { - self.prg_ram[ - @as(usize, address) - 0x6000 - ] = value; - - return; - } - - self.mapper.cpuWrite( - address, - value, - ); - self.cached_mirroring = self.mapper.mirroring(); -} - -pub inline fn ppuRead( - self: *Cartridge, - address: u16, -) u8 { - const offset = - self.mapper.ppuMapRead(address) orelse return 0; - - if ((offset & 0x80000000) != 0) - return self.chr_ram[offset & 0x1fff]; - - if (self.chr_is_ram) - return self.chr_ram[offset]; - - return self.chr_rom[offset]; -} - -pub inline fn ppuWrite( - self: *Cartridge, - address: u16, - value: u8, -) void { - const offset = - self.mapper.ppuMapWrite(address) orelse return; - - if ((offset & 0x80000000) != 0) { - self.chr_ram[offset & 0x1fff] = value; - return; - } - - if (!self.chr_is_ram) - return; - - self.chr_ram[offset] = value; -} - -pub inline fn mirroring( - self: *const Cartridge, -) Mirroring { - return self.cached_mirroring; -} - -pub fn irqAsserted( - self: *const Cartridge, -) bool { - return self.mapper.irqAsserted(); -} - -pub fn handleScanline( - self: *Cartridge, -) void { - self.mapper.handleScanline(); -} - -test "NES Cartridge - UxROM Mapper 2 Bank Switching" { - const Uxrom = _mapper.Uxrom; - var uxrom = Uxrom.init(0x20000, .horizontal); // 128 KB (8 banks) - - // Bank 0 at $8000 by default - try std.testing.expectEqual(@as(?usize, 0x0000), uxrom.cpuMapRead(0x8000)); - // Fixed bank 7 at $C000 - try std.testing.expectEqual(@as(?usize, 0x1C000), uxrom.cpuMapRead(0xC000)); - - // Switch to bank 3 by writing to $8000 - uxrom.cpuWrite(0x8000, 3); - try std.testing.expectEqual(@as(?usize, 0x0C000), uxrom.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x1C000), uxrom.cpuMapRead(0xC000)); -} - -test "NES Cartridge - MMC1 Mapper 1 Serial Shift Register" { - const Mmc1 = _mapper.Mmc1; - var mmc1 = Mmc1.init(0x20000, 0x20000); // 128 KB PRG, 128 KB CHR - - // Write 0x01 into PRG bank register ($E000) bit by bit: 1, 0, 0, 0, 0 - mmc1.cpuWrite(0xE000, 1); - mmc1.cpuWrite(0xE000, 0); - mmc1.cpuWrite(0xE000, 0); - mmc1.cpuWrite(0xE000, 0); - mmc1.cpuWrite(0xE000, 0); - - // Bank 1 at $8000 - try std.testing.expectEqual(@as(?usize, 0x04000), mmc1.cpuMapRead(0x8000)); -} - -test "NES Cartridge - CNROM Mapper 3 CHR Bank Switching" { - const Cnrom = _mapper.Cnrom; - var cnrom = Cnrom.init(0x8000, 0x8000, .vertical); // 32 KB PRG, 32 KB CHR (4 CHR banks) - - // Default CHR bank 0 - try std.testing.expectEqual(@as(?usize, 0x0000), cnrom.ppuMapRead(0x0000)); - - // Switch to CHR bank 2 - cnrom.cpuWrite(0x8000, 2); - try std.testing.expectEqual(@as(?usize, 0x4000), cnrom.ppuMapRead(0x0000)); -} - -test "NES Cartridge - MMC3 Mapper 4 IRQ & Banking" { - const Mmc3 = _mapper.Mmc3; - var mmc3 = Mmc3.init(0x40000, 0x20000); // 256 KB PRG, 128 KB CHR - - // Set IRQ latch to 5, reload and enable IRQ - mmc3.cpuWrite(0xC000, 5); - mmc3.cpuWrite(0xC001, 0); - mmc3.cpuWrite(0xE001, 0); - - // 1 reload scanline + 5 decrement scanlines = 6 scanlines to reach 0 - for (0..6) |_| { - mmc3.handleScanline(); - } - - try std.testing.expect(mmc3.irqAsserted()); -} - -test "NES Cartridge - AxROM Mapper 7 Banking & Single-Screen Mirroring" { - const Axrom = _mapper.Axrom; - var axrom = Axrom.init(0x40000); // 256 KB PRG (8 banks) - - // Bank 0 at $8000 by default - try std.testing.expectEqual(@as(?usize, 0x00000), axrom.cpuMapRead(0x8000)); - try std.testing.expectEqual(Mirroring.single_screen_lower, axrom.mirroring()); - - // Switch to bank 3 and single-screen upper - axrom.cpuWrite(0x8000, 0x13); - try std.testing.expectEqual(@as(?usize, 0x18000), axrom.cpuMapRead(0x8000)); - try std.testing.expectEqual(Mirroring.single_screen_upper, axrom.mirroring()); -} - -test "NES Cartridge - GxROM Mapper 66 PRG & CHR Banking" { - const Gxrom = _mapper.Gxrom; - var gxrom = Gxrom.init(0x20000, 0x8000, .horizontal); // 128 KB PRG (4 banks), 32 KB CHR (4 banks) - - // Bank 0 by default - try std.testing.expectEqual(@as(?usize, 0x0000), gxrom.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x0000), gxrom.ppuMapRead(0x0000)); - - // Select PRG bank 2 (0x20) and CHR bank 3 (0x03) -> write 0x23 - gxrom.cpuWrite(0x8000, 0x23); - try std.testing.expectEqual(@as(?usize, 0x10000), gxrom.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x6000), gxrom.ppuMapRead(0x0000)); -} - -test "NES Cartridge - MMC2 Mapper 9 PPU Tile Fetch Latch Banking" { - const Mmc2 = _mapper.Mmc2; - var mmc2 = Mmc2.init(0x20000, 0x20000); // 128 KB PRG, 128 KB CHR (32 CHR 4K banks) - - // Set CHR 0FD = 2, 0FE = 5 - mmc2.cpuWrite(0xB000, 2); - mmc2.cpuWrite(0xC000, 5); - - // Initial read (latch0 is 0FD by default) -> bank 2 - try std.testing.expectEqual(@as(?usize, 0x2000), mmc2.ppuMapRead(0x0000)); - - // PPU reads tile $0FE0 -> toggles latch0 to 0FE (bank 5) - _ = mmc2.ppuMapRead(0x0FE0); - try std.testing.expectEqual(@as(?usize, 0x5000), mmc2.ppuMapRead(0x0000)); - - // PPU reads tile $0FD0 -> toggles latch0 back to 0FD (bank 2) - _ = mmc2.ppuMapRead(0x0FD0); - try std.testing.expectEqual(@as(?usize, 0x2000), mmc2.ppuMapRead(0x0000)); -} - -test "NES Cartridge - Color Dreams Mapper 11 PRG & CHR Banking" { - const ColorDreams = _mapper.ColorDreams; - var cd = ColorDreams.init(0x20000, 0x20000, .vertical); // 128 KB PRG (4 banks), 128 KB CHR (16 banks) - - // Bank 0 by default - try std.testing.expectEqual(@as(?usize, 0x0000), cd.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x0000), cd.ppuMapRead(0x0000)); - - // Select PRG bank 2 (bits 0..1) and CHR bank 5 (bits 4..7) -> write 0x52 - cd.cpuWrite(0x8000, 0x52); - try std.testing.expectEqual(@as(?usize, 0x10000), cd.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0xA000), cd.ppuMapRead(0x0000)); -} - -test "NES Cartridge - Camerica Mapper 71 PRG Banking & Mirroring" { - const Camerica = _mapper.Camerica; - var cam = Camerica.init(0x40000, .vertical); // 256 KB PRG (16 banks of 16K) - - // Default: Bank 0 at $8000, Bank 15 at $C000 - try std.testing.expectEqual(@as(?usize, 0x00000), cam.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x3C000), cam.cpuMapRead(0xC000)); - - // Switch bank 5 at $8000 by writing to $C000 - cam.cpuWrite(0xC000, 5); - try std.testing.expectEqual(@as(?usize, 0x14000), cam.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x3C000), cam.cpuMapRead(0xC000)); - - // Select single screen upper mirroring by writing to $9000 - cam.cpuWrite(0x9000, 0x10); - try std.testing.expectEqual(Mirroring.single_screen_upper, cam.mirroring()); -} - -test "NES Cartridge - NINA-03 Mapper 79 Expansion Register Banking" { - const Nina03 = _mapper.Nina03; - var nina = Nina03.init(0x10000, 0x10000, .horizontal); // 64 KB PRG (2 banks of 32K), 64 KB CHR (8 banks of 8K) - - // Default bank 0 - try std.testing.expectEqual(@as(?usize, 0x0000), nina.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x0000), nina.ppuMapRead(0x0000)); - - // Write $4100: bit 3 = 1 (PRG bank 1), bits 0..2 = 3 (CHR bank 3) -> write 0x0B - nina.cpuWrite(0x4100, 0x0B); - try std.testing.expectEqual(@as(?usize, 0x8000), nina.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x6000), nina.ppuMapRead(0x0000)); -} - -test "NES Cartridge - Irem Mapper 78 PRG, CHR Banking & Mirroring" { - const Irem78 = _mapper.Irem78; - var irem = Irem78.init(0x20000, 0x20000); // 128 KB PRG (8 banks of 16K), 128 KB CHR (16 banks of 8K) - - // Default: bank 0 at $8000, bank 7 at $C000, bank 0 at PPU $0000 - try std.testing.expectEqual(@as(?usize, 0x00000), irem.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x1C000), irem.cpuMapRead(0xC000)); - try std.testing.expectEqual(@as(?usize, 0x0000), irem.ppuMapRead(0x0000)); - try std.testing.expectEqual(Mirroring.single_screen_lower, irem.mirroring()); - - // Write to $8000: PRG bank 3, CHR bank 4 (0x40), single screen upper (bit 3 = 1 -> 0x08) -> write 0x4B - irem.cpuWrite(0x8000, 0x4B); - try std.testing.expectEqual(@as(?usize, 0x0C000), irem.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x08000), irem.ppuMapRead(0x0000)); - try std.testing.expectEqual(Mirroring.single_screen_upper, irem.mirroring()); -} - -test "NES Cartridge - Jaleco Mapper 87 CHR Banking" { - const Mapper87 = _mapper.Mapper87; - var m87 = Mapper87.init(0x8000, 0x8000, .horizontal); // 32 KB PRG, 32 KB CHR (4 banks of 8K) - - // Default CHR bank 0 - try std.testing.expectEqual(@as(?usize, 0x0000), m87.ppuMapRead(0x0000)); - - // Write $6000 with value 1 (bit 1=0, bit 0=1 -> chr_bank = 2) - m87.cpuWrite(0x6000, 1); - try std.testing.expectEqual(@as(?usize, 0x4000), m87.ppuMapRead(0x0000)); -} - -test "NES Cartridge - UN1ROM Mapper 94 PRG Banking" { - const Mapper94 = _mapper.Mapper94; - var m94 = Mapper94.init(0x40000, .vertical); // 256 KB PRG (16 banks of 16K) - - // Default: bank 0 at $8000, bank 15 at $C000 - try std.testing.expectEqual(@as(?usize, 0x00000), m94.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x3C000), m94.cpuMapRead(0xC000)); - - // Write $8000 with (3 << 2) = 12 -> selects bank 3 - m94.cpuWrite(0x8000, 12); - try std.testing.expectEqual(@as(?usize, 0x0C000), m94.cpuMapRead(0x8000)); -} - -test "NES Cartridge - Jaleco Command Mapper 140 PRG & CHR Banking" { - const Mapper140 = _mapper.Mapper140; - var m140 = Mapper140.init(0x20000, 0x20000, .horizontal); // 128 KB PRG (4 banks of 32K), 128 KB CHR (16 banks of 8K) - - // Default bank 0 - try std.testing.expectEqual(@as(?usize, 0x0000), m140.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x0000), m140.ppuMapRead(0x0000)); - - // Write $6000 with PRG bank 2 (0x20) and CHR bank 5 (0x05) -> write 0x25 - m140.cpuWrite(0x6000, 0x25); - try std.testing.expectEqual(@as(?usize, 0x10000), m140.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0xA000), m140.ppuMapRead(0x0000)); -} - -test "NES Cartridge - Crazy Climber Mapper 180 PRG Banking" { - const Mapper180 = _mapper.Mapper180; - var m180 = Mapper180.init(0x20000, .vertical); // 128 KB PRG (8 banks of 16K) - - // Default: fixed bank 0 at $8000, bank 0 at $C000 - try std.testing.expectEqual(@as(?usize, 0x0000), m180.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x0000), m180.cpuMapRead(0xC000)); - - // Write $8000 with bank 5 -> switches $C000 to bank 5 - m180.cpuWrite(0x8000, 5); - try std.testing.expectEqual(@as(?usize, 0x0000), m180.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x14000), m180.cpuMapRead(0xC000)); -} - -test "NES Cartridge - Konami VRC1 Mapper 75 PRG, CHR Banking & Mirroring" { - const Vrc1 = _mapper.Vrc1; - var vrc = Vrc1.init(0x20000, 0x20000); // 128 KB PRG (16 banks of 8K), 128 KB CHR (32 banks of 4K) - - // Default bank 0 at $8000, fixed bank 15 at $E000 - try std.testing.expectEqual(@as(?usize, 0x00000), vrc.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x1E000), vrc.cpuMapRead(0xE000)); - try std.testing.expectEqual(Mirroring.vertical, vrc.mirroring()); - - // Switch PRG bank 0 to 4 ($8000), PRG bank 1 to 7 ($A000), PRG bank 2 to 2 ($C000) - vrc.cpuWrite(0x8000, 4); - vrc.cpuWrite(0xA000, 7); - vrc.cpuWrite(0xC000, 2); - try std.testing.expectEqual(@as(?usize, 0x08000), vrc.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x0E000), vrc.cpuMapRead(0xA000)); - try std.testing.expectEqual(@as(?usize, 0x04000), vrc.cpuMapRead(0xC000)); - - // Select horizontal mirroring via $9000 bit 0 = 1 - vrc.cpuWrite(0x9000, 1); - try std.testing.expectEqual(Mirroring.horizontal, vrc.mirroring()); -} - -test "NES Cartridge - Camerica Quattro Mapper 232 PRG Outer Block & Inner Bank Switching" { - const Mapper232 = _mapper.Mapper232; - var m232 = Mapper232.init(0x40000, .vertical); // 256 KB PRG (16 banks of 16K = 4 blocks of 64K) - - // Default: block 0 (banks 0..3), bank 0 at $8000, bank 3 at $C000 - try std.testing.expectEqual(@as(?usize, 0x00000), m232.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x0C000), m232.cpuMapRead(0xC000)); - - // Select outer block 2 by writing (2 << 3) = 0x10 to $8000 - m232.cpuWrite(0x8000, 0x10); - // Select inner bank 1 by writing 1 to $A000 - m232.cpuWrite(0xA000, 1); - - // Block 2 (banks 8..11), inner bank 1 -> bank 9 (0x24000) at $8000, fixed bank 11 (0x2C000) at $C000 - try std.testing.expectEqual(@as(?usize, 0x24000), m232.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x2C000), m232.cpuMapRead(0xC000)); -} - -test "NES Cartridge - Konami VRC3 Mapper 73 PRG Banking & IRQ Timer Overflow" { - const Vrc3 = _mapper.Vrc3; - var vrc = Vrc3.init(0x20000, .vertical); // 128 KB PRG (8 banks of 16K) - - // Default: bank 0 at $8000, fixed bank 7 at $C000 - try std.testing.expectEqual(@as(?usize, 0x00000), vrc.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x1C000), vrc.cpuMapRead(0xC000)); - - // Select PRG bank 3 at $F000 - vrc.cpuWrite(0xF000, 3); - try std.testing.expectEqual(@as(?usize, 0x0C000), vrc.cpuMapRead(0x8000)); - - // Set IRQ Latch to 0xFFFE - vrc.cpuWrite(0x8000, 0x0E); // Latch low nibble = E - vrc.cpuWrite(0x9000, 0x0F); // Latch mid-low nibble = F - vrc.cpuWrite(0xA000, 0x0F); // Latch mid-high nibble = F - vrc.cpuWrite(0xB000, 0x0F); // Latch high nibble = F (0xFFFE) - - // Enable IRQ via $C000 (value = 2 -> enable=true, 16-bit mode) - vrc.cpuWrite(0xC000, 2); - try std.testing.expectEqual(false, vrc.irqAsserted()); - - // Tick 1 cycle -> counter reaches 0xFFFF - vrc.tickCycle(); - try std.testing.expectEqual(false, vrc.irqAsserted()); - - // Tick 2nd cycle -> counter overflows from 0xFFFF -> IRQ asserted! - vrc.tickCycle(); - try std.testing.expectEqual(true, vrc.irqAsserted()); - - // Acknowledge IRQ via $D000 - vrc.cpuWrite(0xD000, 0); - try std.testing.expectEqual(false, vrc.irqAsserted()); -} - -test "NES Cartridge - Namco 175/340 Mapper 210 PRG, CHR Banking & Mirroring" { - const Mapper210 = _mapper.Mapper210; - var m210 = Mapper210.init(0x20000, 0x20000); // 128 KB PRG (16 banks of 8K), 128 KB CHR (128 banks of 1K) - - // Default bank 0 at $8000, fixed bank 15 at $E000 - try std.testing.expectEqual(@as(?usize, 0x00000), m210.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x1E000), m210.cpuMapRead(0xE000)); - try std.testing.expectEqual(Mirroring.vertical, m210.mirroring()); - - // Select PRG bank 5 at $C000 - m210.cpuWrite(0xC000, 5); - try std.testing.expectEqual(@as(?usize, 0x0A000), m210.cpuMapRead(0x8000)); - - // Select CHR bank 10 for PPU range $0400-$07FF via write to $8800 - m210.cpuWrite(0x8800, 10); - try std.testing.expectEqual(@as(?usize, 0x2800), m210.ppuMapRead(0x0400)); - - // Select horizontal mirroring via write 0x40 to $E000 - m210.cpuWrite(0xE000, 0x40); - try std.testing.expectEqual(Mirroring.horizontal, m210.mirroring()); -} - -test "NES Cartridge - DxROM Mapper 206 PRG & CHR Banking" { - const Mapper206 = _mapper.Mapper206; - var m206 = Mapper206.init(0x20000, 0x20000, .horizontal); // 128 KB PRG (16 banks of 8K), 128 KB CHR (128 banks of 1K) - - // Select PRG bank register 6 (writes to $8000 then $8001) - m206.cpuWrite(0x8000, 6); - m206.cpuWrite(0x8001, 3); // PRG bank 3 at $8000 - try std.testing.expectEqual(@as(?usize, 0x06000), m206.cpuMapRead(0x8000)); - - // Select CHR register 0 (writes 4 to CHR bank 0 at PPU $0000-$07FF) - m206.cpuWrite(0x8000, 0); - m206.cpuWrite(0x8001, 4); - try std.testing.expectEqual(@as(?usize, 0x1000), m206.ppuMapRead(0x0000)); -} - -test "NES Cartridge - Sunsoft-2 Mapper 89 PRG, CHR Banking & Mirroring" { - const Mapper89 = _mapper.Mapper89; - var m89 = Mapper89.init(0x20000, 0x20000); // 128 KB PRG (8 banks of 16K), 128 KB CHR (16 banks of 8K) - - // Default bank 0 at $8000, fixed bank 7 at $C000 - try std.testing.expectEqual(@as(?usize, 0x00000), m89.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x1C000), m89.cpuMapRead(0xC000)); - try std.testing.expectEqual(Mirroring.single_screen_lower, m89.mirroring()); - - // Write to $8000: PRG bank 2 (0x20), CHR bank 9 (0x81 -> bit 7=1, bit 0=1 -> bank 9), upper mirroring (0x08) -> write 0xA9 - m89.cpuWrite(0x8000, 0xA9); - try std.testing.expectEqual(@as(?usize, 0x08000), m89.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x12000), m89.ppuMapRead(0x0000)); - try std.testing.expectEqual(Mirroring.single_screen_upper, m89.mirroring()); -} - -test "NES Cartridge - Sunsoft-3 Mapper 93 PRG Banking & Mirroring" { - const Mapper93 = _mapper.Mapper93; - var m93 = Mapper93.init(0x20000, .vertical); // 128 KB PRG (8 banks of 16K) - - // Default bank 0 at $8000, fixed bank 7 at $C000 - try std.testing.expectEqual(@as(?usize, 0x00000), m93.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x1C000), m93.cpuMapRead(0xC000)); - try std.testing.expectEqual(Mirroring.vertical, m93.mirroring()); - - // Write to $8000: PRG bank 3 (0x30), horizontal mirroring (0x01) -> write 0x31 - m93.cpuWrite(0x8000, 0x31); - try std.testing.expectEqual(@as(?usize, 0x0C000), m93.cpuMapRead(0x8000)); - try std.testing.expectEqual(Mirroring.horizontal, m93.mirroring()); -} - -test "NES Cartridge - TxSROM Mapper 118 MMC3 Banking & Bit 7 Mirroring" { - const Mapper118 = _mapper.Mapper118; - var m118 = Mapper118.init(0x20000, 0x20000); // 128 KB PRG, 128 KB CHR - - // Default mirroring is single_screen_lower - try std.testing.expectEqual(Mirroring.single_screen_lower, m118.mirroring()); - - // Select CHR bank 0 via $8000=0, write 0x80 to $8001 -> sets bit 7 of CHR bank -> upper screen mirroring - m118.cpuWrite(0x8000, 0); - m118.cpuWrite(0x8001, 0x80); - try std.testing.expectEqual(Mirroring.single_screen_upper, m118.mirroring()); - - // Select CHR bank 0 via $8000=0, write 0x00 to $8001 -> clears bit 7 -> lower screen mirroring - m118.cpuWrite(0x8000, 0); - m118.cpuWrite(0x8001, 0x00); - try std.testing.expectEqual(Mirroring.single_screen_lower, m118.mirroring()); -} - -test "NES Cartridge - TQROM Mapper 119 CHR RAM/ROM Hybrid Switching" { - const Mapper119 = _mapper.Mapper119; - var m119 = Mapper119.init(0x20000, 0x20000); // 128 KB PRG, 128 KB CHR ROM - - // Select CHR bank 0 via $8000=0, write 0x05 to $8001 (bit 6 clear) -> CHR ROM offset (0x05 & 0xfe = 4 -> offset 0x1000) - m119.cpuWrite(0x8000, 0); - m119.cpuWrite(0x8001, 0x05); - try std.testing.expectEqual(@as(?usize, 0x1000), m119.ppuMapRead(0x0000)); - - // Select CHR bank 0 via $8000=0, write 0x42 to $8001 (bit 6 set) -> CHR RAM offset (0x80000000 flag + (0x42 & 0xfe) = 2 -> bank 2 * 0x400) - m119.cpuWrite(0x8000, 0); - m119.cpuWrite(0x8001, 0x42); - try std.testing.expectEqual(@as(?usize, 0x80000800), m119.ppuMapRead(0x0000)); -} - -test "NES Cartridge - Namco 109 Mapper 76 PRG & 2KB CHR Banking" { - const Mapper76 = _mapper.Mapper76; - var m76 = Mapper76.init(0x20000, 0x20000, .horizontal); // 128 KB PRG (16 banks of 8K), 128 KB CHR (64 banks of 2K) - - // Default PRG banks: last two fixed at $C000 (bank 14) & $E000 (bank 15) - try std.testing.expectEqual(@as(?usize, 0x1C000), m76.cpuMapRead(0xC000)); - try std.testing.expectEqual(@as(?usize, 0x1E000), m76.cpuMapRead(0xE000)); - - // Select register 6 ($8000=6), write bank 5 to $8001 -> PRG bank 5 at $8000 - m76.cpuWrite(0x8000, 6); - m76.cpuWrite(0x8001, 5); - try std.testing.expectEqual(@as(?usize, 0x0A000), m76.cpuMapRead(0x8000)); - - // Select register 2 ($8000=2), write CHR bank 3 to $8001 -> 2KB CHR bank 3 at PPU $0000 - m76.cpuWrite(0x8000, 2); - m76.cpuWrite(0x8001, 3); - try std.testing.expectEqual(@as(?usize, 0x1800), m76.ppuMapRead(0x0000)); -} - -test "NES Cartridge - Namco 118 Mapper 88 PRG & Bit 6 CHR Block Selection" { - const Mapper88 = _mapper.Mapper88; - var m88 = Mapper88.init(0x20000, 0x20000, .vertical); // 128 KB PRG, 128 KB CHR - - // Select register 2 ($8000=2), write bank 0x45 (bit 6 set -> upper 32K CHR block) -> maps bank (0x05 | 0x40) = 0x45 (69) -> 69 * 0x400 = 0x11400 offset at PPU $1000 - m88.cpuWrite(0x8000, 2); - m88.cpuWrite(0x8001, 0x45); - try std.testing.expectEqual(@as(?usize, 0x11400), m88.ppuMapRead(0x1000)); -} - -test "NES Cartridge - MMC4 Mapper 10 PRG Banking & PPU Latch CHR Switching" { - const Mmc4 = _mapper.Mmc4; - var mmc4 = Mmc4.init(0x20000, 0x20000); // 128 KB PRG (8 banks of 16K), 128 KB CHR (32 banks of 4K) - - // Fixed last bank (bank 7) at $C000 - try std.testing.expectEqual(@as(?usize, 0x1C000), mmc4.cpuMapRead(0xC000)); - - // Write PRG bank 3 to $A000 - mmc4.cpuWrite(0xA000, 3); - try std.testing.expectEqual(@as(?usize, 0x0C000), mmc4.cpuMapRead(0x8000)); - - // Configure CHR bank for latch $FD (bank 2) and $FE (bank 5) via $B000 and $C000 - mmc4.cpuWrite(0xB000, 2); - mmc4.cpuWrite(0xC000, 5); - - // Default latch is $FE -> reading PPU $0000 yields bank 5 (0x5000 offset) - try std.testing.expectEqual(@as(?usize, 0x05000), mmc4.ppuMapRead(0x0000)); - - // Trigger latch $FD by reading tile at $0FD0 -> now PPU $0000 yields bank 2 (0x02000 offset) - _ = mmc4.ppuMapRead(0x0fd0); - try std.testing.expectEqual(@as(?usize, 0x02000), mmc4.ppuMapRead(0x0000)); -} - -test "NES Cartridge - CPROM Mapper 13 CHR RAM 4KB Bank Switching" { - const Mapper13 = _mapper.Mapper13; - var m13 = Mapper13.init(0x8000, .vertical); // 32 KB PRG - - // PPU $0000-$0FFF maps to fixed CHR RAM bank 0 - try std.testing.expectEqual(@as(?usize, 0x80000000), m13.ppuMapRead(0x0000)); - - // Default CHR RAM bank 0 at $1000-$1FFF - try std.testing.expectEqual(@as(?usize, 0x80000000), m13.ppuMapRead(0x1000)); - - // Write bank 2 to $8000 -> PPU $1000 maps to CHR RAM bank 2 (0x80002000 offset) - m13.cpuWrite(0x8000, 2); - try std.testing.expectEqual(@as(?usize, 0x80002000), m13.ppuMapRead(0x1000)); -} - -test "NES Cartridge - Mapper 34 BNROM & NINA-01 Banking" { - const Mapper34 = _mapper.Mapper34; - - // Test BNROM (chr_size = 0) - var bnrom = Mapper34.init(0x20000, 0, .vertical); // 128 KB PRG (4 banks of 32K) - bnrom.cpuWrite(0x8000, 2); - try std.testing.expectEqual(@as(?usize, 0x10000), bnrom.cpuMapRead(0x8000)); - - // Test NINA-01 (chr_size > 0) - var nina01 = Mapper34.init(0x20000, 0x10000, .horizontal); // 128 KB PRG, 64 KB CHR - nina01.cpuWrite(0x7ffd, 1); - nina01.cpuWrite(0x7ffe, 3); - nina01.cpuWrite(0x7fff, 5); - - try std.testing.expectEqual(@as(?usize, 0x08000), nina01.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x03000), nina01.ppuMapRead(0x0000)); - try std.testing.expectEqual(@as(?usize, 0x05000), nina01.ppuMapRead(0x1000)); -} - -test "NES Cartridge - Sunsoft-4 Mapper 68 PRG, CHR Banking & Mirroring" { - const Mapper68 = _mapper.Mapper68; - var m68 = Mapper68.init(0x20000, 0x20000, .vertical); // 128 KB PRG (8 banks of 16K), 128 KB CHR (64 banks of 2K) - - // Fixed last PRG bank (bank 7) at $C000 - try std.testing.expectEqual(@as(?usize, 0x1C000), m68.cpuMapRead(0xC000)); - - // Write PRG bank 4 to $F000 - m68.cpuWrite(0xF000, 4); - try std.testing.expectEqual(@as(?usize, 0x10000), m68.cpuMapRead(0x8000)); - - // Write CHR bank 7 to $8000 (PPU $0000) and bank 12 to $B000 (PPU $1800) - m68.cpuWrite(0x8000, 7); - m68.cpuWrite(0xB000, 12); - try std.testing.expectEqual(@as(?usize, 0x03800), m68.ppuMapRead(0x0000)); - try std.testing.expectEqual(@as(?usize, 0x06000), m68.ppuMapRead(0x1800)); - - // Write mirroring control to $E000 -> 1 = horizontal - m68.cpuWrite(0xE000, 1); - try std.testing.expectEqual(Mirroring.horizontal, m68.mirroring()); -} - -test "NES Cartridge - Irem H3001 Mapper 65 PRG & 1KB CHR Banking" { - const Mapper65 = _mapper.Mapper65; - var m65 = Mapper65.init(0x20000, 0x20000, .vertical); // 128 KB PRG (16 banks of 8K), 128 KB CHR (128 banks of 1K) - - // Fixed last bank (bank 15) at $E000 - try std.testing.expectEqual(@as(?usize, 0x1E000), m65.cpuMapRead(0xE000)); - - // Write PRG bank 5 to $8000 and bank 9 to $A000 - m65.cpuWrite(0x8000, 5); - m65.cpuWrite(0xA000, 9); - try std.testing.expectEqual(@as(?usize, 0x0A000), m65.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x12000), m65.cpuMapRead(0xA000)); - - // Write CHR bank 15 to $B003 (PPU $0C00) - m65.cpuWrite(0xB003, 15); - try std.testing.expectEqual(@as(?usize, 0x03C00), m65.ppuMapRead(0x0C00)); -} - -test "NES Cartridge - Sunsoft FME-7 Mapper 69 PRG & CHR Banking" { - const Mapper69 = _mapper.Mapper69; - var m69 = Mapper69.init(0x20000, 0x20000, .vertical); // 128 KB PRG (16 banks of 8K), 128 KB CHR (128 banks of 1K) - - // Fixed last bank (bank 15) at $E000 - try std.testing.expectEqual(@as(?usize, 0x1E000), m69.cpuMapRead(0xE000)); - - // Command 9 (PRG bank at $8000) -> write bank 4 - m69.cpuWrite(0x8000, 9); - m69.cpuWrite(0xA000, 4); - try std.testing.expectEqual(@as(?usize, 0x08000), m69.cpuMapRead(0x8000)); - - // Command 3 (CHR bank at PPU $0C00) -> write bank 18 - m69.cpuWrite(0x8000, 3); - m69.cpuWrite(0xA000, 18); - try std.testing.expectEqual(@as(?usize, 0x04800), m69.ppuMapRead(0x0C00)); - - // Command C (Mirroring) -> write 1 = horizontal - m69.cpuWrite(0x8000, 0x0c); - m69.cpuWrite(0xA000, 1); - try std.testing.expectEqual(Mirroring.horizontal, m69.mirroring()); -} - -test "NES Cartridge - Konami VRC4 Mapper 21 & 25 PRG, CHR Banking & IRQ Timer" { - const Vrc4 = _mapper.Vrc4; - - // Test Mapper 25 (VRC4b/d - shift_a=0, shift_b=2) - var vrc4 = Vrc4.init(0x20000, 0x20000, 25); // 128 KB PRG (16 banks of 8K), 128 KB CHR (128 banks of 1K) - - // Fixed last bank (bank 15) at $E000 and second-last (bank 14) at $C000 - try std.testing.expectEqual(@as(?usize, 0x1C000), vrc4.cpuMapRead(0xC000)); - try std.testing.expectEqual(@as(?usize, 0x1E000), vrc4.cpuMapRead(0xE000)); - - // Write PRG bank 6 to $8000 - vrc4.cpuWrite(0x8000, 6); - try std.testing.expectEqual(@as(?usize, 0x0C000), vrc4.cpuMapRead(0x8000)); - - // Write CHR bank 27 to PPU $0400 slot 1: lo at $B001 (line 1), hi at $B005 (line 3) - vrc4.cpuWrite(0xB001, 0x0b); - vrc4.cpuWrite(0xB005, 0x01); // bank = (1 << 4) | 0x0b = 27 - try std.testing.expectEqual(@as(?usize, 0x06C00), vrc4.ppuMapRead(0x0400)); -} - -test "NES Cartridge - Konami VRC2 Mapper 22 & 23 PRG, CHR Banking & Mirroring" { - const Vrc2 = _mapper.Vrc2; - - // Test Mapper 22 (VRC2a - shift_a=1, shift_b=0, CHR right-shifted by 1) - var vrc2 = Vrc2.init(0x20000, 0x20000, 22); // 128 KB PRG (16 banks of 8K), 128 KB CHR (128 banks of 1K) - - // Fixed last bank (bank 15) at $E000 and second-last (bank 14) at $C000 - try std.testing.expectEqual(@as(?usize, 0x1C000), vrc2.cpuMapRead(0xC000)); - - // Write PRG bank 7 to $8000 - vrc2.cpuWrite(0x8000, 7); - try std.testing.expectEqual(@as(?usize, 0x0E000), vrc2.cpuMapRead(0x8000)); - - // Write CHR bank 30 to PPU $0000 slot 0: lo at $B000 (line 0), hi at $B001 (line 2) -> bank_val = 60, >> 1 = 30 - vrc2.cpuWrite(0xB000, 0x0c); - vrc2.cpuWrite(0xB001, 0x03); // bank = ((3 << 4) | 0x0c) >> 1 = 60 >> 1 = 30 - try std.testing.expectEqual(@as(?usize, 0x07800), vrc2.ppuMapRead(0x0000)); - - // Mirroring write to $9000 -> 1 = horizontal - vrc2.cpuWrite(0x9000, 1); - try std.testing.expectEqual(Mirroring.horizontal, vrc2.mirroring()); -} - -test "NES Cartridge - Konami VRC6 Mapper 24 & 26 PRG & CHR Banking" { - const Vrc6 = _mapper.Vrc6; - - // Test Mapper 24 (VRC6a - shift_a=0, shift_b=1) - var vrc6 = Vrc6.init(0x40000, 0x20000, 24); // 256 KB PRG (16 banks of 16K), 128 KB CHR (128 banks of 1K) - - // Fixed last 8KB bank (bank 31) at $E000 - try std.testing.expectEqual(@as(?usize, 0x3E000), vrc6.cpuMapRead(0xE000)); - - // Write 16KB PRG bank 3 to $8000 (PPU $8000-$BFFF) - vrc6.cpuWrite(0x8000, 3); - try std.testing.expectEqual(@as(?usize, 0x0C000), vrc6.cpuMapRead(0x8000)); - - // Write 8KB PRG bank 11 to $C000 (PPU $C000-$DFFF) - vrc6.cpuWrite(0xC000, 11); - try std.testing.expectEqual(@as(?usize, 0x16000), vrc6.cpuMapRead(0xC000)); - - // Write CHR bank 42 to PPU $0800 slot 2: $D002 (line 2) - vrc6.cpuWrite(0xD002, 42); - try std.testing.expectEqual(@as(?usize, 0x0A800), vrc6.ppuMapRead(0x0800)); -} - -test "NES Cartridge - Konami VRC7 Mapper 85 PRG & CHR Banking" { - const Vrc7 = _mapper.Vrc7; - var vrc7 = Vrc7.init(0x20000, 0x20000); // 128 KB PRG (16 banks of 8K), 128 KB CHR (128 banks of 1K) - - // Fixed last bank (bank 15) at $E000 - try std.testing.expectEqual(@as(?usize, 0x1E000), vrc7.cpuMapRead(0xE000)); - - // Write PRG bank 5 to $8000 and bank 9 to $8010 - vrc7.cpuWrite(0x8000, 5); - vrc7.cpuWrite(0x8010, 9); - try std.testing.expectEqual(@as(?usize, 0x0A000), vrc7.cpuMapRead(0x8000)); - try std.testing.expectEqual(@as(?usize, 0x12000), vrc7.cpuMapRead(0xA000)); - - // Write CHR bank 19 to PPU $0C00 slot 3: $B010 - vrc7.cpuWrite(0xB010, 19); - try std.testing.expectEqual(@as(?usize, 0x04C00), vrc7.ppuMapRead(0x0C00)); -} - -test "NES Cartridge - Nintendo MMC5 Mapper 5 PRG, CHR Banking & Multiplier" { - const Mmc5 = _mapper.Mmc5; - var mmc5 = Mmc5.init(0x40000, 0x20000); // 256 KB PRG (32 banks of 8K), 128 KB CHR (128 banks of 1K) - - // Fixed last bank (bank 31) at $E000 - try std.testing.expectEqual(@as(?usize, 0x3E000), mmc5.cpuMapRead(0xE000)); - - // Write PRG bank 7 to $5115 ($8000) - mmc5.cpuWrite(0x5115, 7); - try std.testing.expectEqual(@as(?usize, 0x0E000), mmc5.cpuMapRead(0x8000)); - - // Test Hardware Multiplier: 14 * 15 = 210 (0x00D2) - mmc5.cpuWrite(0x5205, 14); - mmc5.cpuWrite(0x5206, 15); - try std.testing.expectEqual(@as(?u8, 0xd2), mmc5.readRegister(0x5205)); - try std.testing.expectEqual(@as(?u8, 0x00), mmc5.readRegister(0x5206)); -} - -test "NES Cartridge - NES 2.0 Header Parsing" { - // Header for 128KB PRG, 128KB CHR, Mapper 1 (MMC1), NES 2.0 signature (flags7 0x08) - const nes2_header = [_]u8{ - 'N', 'E', 'S', 0x1A, 0x08, 0x10, 0x13, 0x08, - 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, - } ++ ([_]u8{0} ** (128 * 1024 + 128 * 1024)); - - const cart = try Cartridge.init(&nes2_header); - try std.testing.expectEqual(@as(usize, 128 * 1024), cart.prg_rom.len); - try std.testing.expectEqual(@as(usize, 128 * 1024), cart.chr_rom.len); -} diff --git a/src/system/nes/conformance.zig b/src/system/nes/conformance.zig deleted file mode 100644 index de324da..0000000 --- a/src/system/nes/conformance.zig +++ /dev/null @@ -1,345 +0,0 @@ -// minimal system test runner for nes-test-roms & NES system contract conformance -const std = @import("std"); -const testing = std.testing; -const build_options = @import("build_options"); -const contract = @import("contract"); -const Nes = @import("root.zig"); - -fn loadRomAlloc(allocator: std.mem.Allocator, relative_path: []const u8) ![]u8 { - const base_dir = build_options.nes_test_roms_dir orelse return error.TestRomsNotFetched; - var dir = try std.Io.Dir.openDirAbsolute(testing.io, base_dir, .{}); - defer dir.close(testing.io); - return dir.readFileAlloc(testing.io, relative_path, allocator, @enumFromInt(10 * 1024 * 1024)); -} - -/// Runs a blargg test ROM on the full NES system implementation. -/// Blargg test ROMs signal completion via $6000 (0x80 = running, 0x00 = passed) -/// and output result text starting at $6004. -pub fn runBlarggTest(relative_path: []const u8, max_instructions: u64) !void { - const rom_bytes = try loadRomAlloc(testing.allocator, relative_path); - defer testing.allocator.free(rom_bytes); - - var nes: Nes = undefined; - try nes.init(rom_bytes); - - var i: u64 = 0; - var result_ready = false; - - while (i < max_instructions) : (i += 1) { - _ = nes.step() orelse break; - - const status = nes.bus.read(0x6000); - if (status != 0x80) { - const sig0 = nes.bus.read(0x6001); - const sig1 = nes.bus.read(0x6002); - const sig2 = nes.bus.read(0x6003); - if (sig0 == 0xDE and sig1 == 0xB0 and sig2 == 0x61) { - result_ready = true; - break; - } - } - } - - const status = nes.bus.read(0x6000); - - var msg_buf: [256]u8 = undefined; - var msg_len: usize = 0; - while (msg_len < msg_buf.len) : (msg_len += 1) { - const char = nes.bus.read(@intCast(0x6004 + msg_len)); - if (char == 0) break; - msg_buf[msg_len] = char; - } - const msg = msg_buf[0..msg_len]; - - if (!result_ready and status == 0x80) { - std.debug.print( - "\n[TIMEOUT] Test '{s}' timed out after {d} instructions. Text output: {s}\n", - .{ relative_path, i, msg }, - ); - return error.TestTimeout; - } - - if (status != 0) { - std.debug.print( - "\n[FAIL] Test '{s}' failed with status 0x{X:0>2}: {s}\n", - .{ relative_path, status, msg }, - ); - return error.TestFailed; - } -} - -/// Runs Kevtris' nestest.nes ROM on full NES system (PC = $C000). -pub fn runNesTestSystem(relative_path: []const u8, max_instructions: u64) !void { - const rom_bytes = try loadRomAlloc(testing.allocator, relative_path); - defer testing.allocator.free(rom_bytes); - - var nes: Nes = undefined; - try nes.init(rom_bytes); - nes.cpu.registers.pc = 0xC000; - - var previous_pc: u16 = 0; - var i: u64 = 0; - - while (i < max_instructions) : (i += 1) { - previous_pc = nes.cpu.registers.pc; - _ = nes.step() orelse break; - - if (nes.cpu.registers.pc == previous_pc) break; - } - - const err1 = nes.bus.read(0x0002); - const err2 = nes.bus.read(0x0003); - if (err1 != 0 or err2 != 0) { - std.debug.print( - "\n[FAIL] Nestest system test failed with error codes $0002: 0x{X:0>2}, $0003: 0x{X:0>2} at PC 0x{X:0>4}\n", - .{ err1, err2, nes.cpu.registers.pc }, - ); - return error.NestestFailed; - } -} - -test "NES System Conformance - Contract SystemSpec" { - comptime contract.validateSystem(Nes); - try testing.expectEqualStrings("Nintendo Entertainment System", Nes.spec.name); - try testing.expectEqual(@as(usize, 1), Nes.spec.video_outputs.len); - try testing.expectEqual(@as(usize, 1), Nes.spec.audio_outputs.len); - try testing.expectEqual(@as(usize, 2), Nes.spec.input_devices.len); - try testing.expectEqual(@as(usize, 1), Nes.spec.storage_devices.len); -} - -test "NES System Conformance - Video Frame Output" { - var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000); - @memcpy(rom[0..4], "NES\x1a"); - rom[4] = 1; - rom[5] = 1; - - var nes: Nes = undefined; - try nes.init(&rom); - nes.runFrame(); - - const frame = nes.videoFrame(0); - try testing.expectEqual(@as(u32, 256), frame.width); - try testing.expectEqual(@as(u32, 240), frame.height); - try testing.expectEqual(contract.PixelFormat.rgb565, frame.format); - try testing.expectEqual(256 * 240 * 2, frame.data.len); -} - -test "NES System Conformance - Audio Buffer Output" { - var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000); - @memcpy(rom[0..4], "NES\x1a"); - rom[4] = 1; - rom[5] = 1; - - var nes: Nes = undefined; - try nes.init(&rom); - nes.runFrame(); - - const audio_buf = nes.audioBuffer(0); - try testing.expectEqual(contract.AudioSampleFormat.i16, audio_buf.format); - try testing.expectEqual(@as(u8, 1), audio_buf.channels); - try testing.expect(audio_buf.data.len > 0); -} - -test "NES System Conformance - Controller Input Shift & Strobe" { - var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000); - @memcpy(rom[0..4], "NES\x1a"); - rom[4] = 1; - rom[5] = 1; - - var nes: Nes = undefined; - try nes.init(&rom); - - // Press Button A (bit 0) and Button Select (bit 2) -> 0b00000101 - nes.setInput(0, .{ .buttons = 0x05 }); - - // Strobe controller - nes.bus.write(0x4016, 1); - nes.bus.write(0x4016, 0); - - // Read serial bits - try testing.expectEqual(@as(u8, 1), nes.bus.read(0x4016) & 1); // Button A (pressed) - try testing.expectEqual(@as(u8, 0), nes.bus.read(0x4016) & 1); // Button B - try testing.expectEqual(@as(u8, 1), nes.bus.read(0x4016) & 1); // Button Select (pressed) -} - -test "NES System Conformance - Storage & Save State Integrity" { - var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000); - @memcpy(rom[0..4], "NES\x1a"); - rom[4] = 1; - rom[5] = 1; - - var nes: Nes = undefined; - try nes.init(&rom); - - // Storage SRAM test - const sram_data = [_]u8{ 0xDE, 0xAD, 0xBE, 0xEF }; - try nes.loadStorage(0, &sram_data); - const view = nes.storageView(0); - try testing.expectEqualSlices(u8, &sram_data, view.data[0..4]); - - // Save state test - var save_buf: [64 * 1024]u8 = undefined; - const written = try nes.saveState(&save_buf); - nes.reset(); - try nes.loadState(save_buf[0..written]); - const view_after = nes.storageView(0); - try testing.expectEqualSlices(u8, &sram_data, view_after.data[0..4]); -} - -test "NES System Conformance - APU DMC Sample Playback & DAC" { - var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000); - @memcpy(rom[0..4], "NES\x1a"); - rom[4] = 1; - rom[5] = 1; - - var nes: Nes = undefined; - try nes.init(&rom); - - // Enable DMC in APU status ($4015 bit 4) - nes.bus.write(0x4015, 0x10); - // Write DMC output level ($4011) to 64 - nes.bus.write(0x4011, 64); - // Setup rate ($4010) - nes.bus.write(0x4010, 0x05); - - nes.runFrame(); - - const audio_buf = nes.audioBuffer(0); - try testing.expect(audio_buf.data.len > 0); -} - -test "NES System Conformance - PPU PPUMASK Grayscale & Color Emphasis" { - var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000); - @memcpy(rom[0..4], "NES\x1a"); - rom[4] = 1; - rom[5] = 1; - - var nes: Nes = undefined; - try nes.init(&rom); - - // Enable Grayscale (bit 0) and Red/Green/Blue Emphasis (bits 5,6,7 -> 0xE1) in PPUMASK ($2001) - nes.bus.write(0x2001, 0xE1); - nes.runFrame(); - - const frame = nes.videoFrame(0); - try testing.expectEqual(256 * 240 * 2, frame.data.len); -} - -test "NES System Conformance - All Mappers iNES Header Support" { - const mappers = [_]struct { id: u8, prg_banks: u8, chr_banks: u8 }{ - .{ .id = 0, .prg_banks = 1, .chr_banks = 1 }, // NROM - .{ .id = 1, .prg_banks = 8, .chr_banks = 8 }, // MMC1 - .{ .id = 2, .prg_banks = 8, .chr_banks = 0 }, // UxROM - .{ .id = 3, .prg_banks = 2, .chr_banks = 4 }, // CNROM - .{ .id = 4, .prg_banks = 16, .chr_banks = 16 }, // MMC3 - .{ .id = 5, .prg_banks = 16, .chr_banks = 16 }, // MMC5 - .{ .id = 7, .prg_banks = 8, .chr_banks = 0 }, // AxROM - .{ .id = 9, .prg_banks = 8, .chr_banks = 16 }, // MMC2 - .{ .id = 10, .prg_banks = 8, .chr_banks = 16 }, // MMC4 - .{ .id = 11, .prg_banks = 4, .chr_banks = 16 }, // Color Dreams - .{ .id = 13, .prg_banks = 2, .chr_banks = 0 }, // CPROM - .{ .id = 21, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC4a/c - .{ .id = 22, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC2a - .{ .id = 23, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC2b/VRC4e - .{ .id = 24, .prg_banks = 16, .chr_banks = 16 }, // Konami VRC6a - .{ .id = 25, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC4b/d - .{ .id = 26, .prg_banks = 16, .chr_banks = 16 }, // Konami VRC6b - .{ .id = 34, .prg_banks = 8, .chr_banks = 8 }, // BNROM / NINA-01 - .{ .id = 65, .prg_banks = 8, .chr_banks = 16 }, // Irem H3001 - .{ .id = 66, .prg_banks = 4, .chr_banks = 4 }, // GxROM - .{ .id = 68, .prg_banks = 8, .chr_banks = 16 }, // Sunsoft-4 - .{ .id = 69, .prg_banks = 8, .chr_banks = 16 }, // Sunsoft FME-7 - .{ .id = 71, .prg_banks = 8, .chr_banks = 0 }, // Camerica - .{ .id = 73, .prg_banks = 8, .chr_banks = 0 }, // Konami VRC3 - .{ .id = 75, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC1 - .{ .id = 76, .prg_banks = 8, .chr_banks = 16 }, // Namco 109 - .{ .id = 78, .prg_banks = 8, .chr_banks = 16 }, // Irem 74HC161/32 - .{ .id = 79, .prg_banks = 4, .chr_banks = 8 }, // NINA-03 - .{ .id = 85, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC7 - .{ .id = 87, .prg_banks = 2, .chr_banks = 4 }, // Jaleco 74x139 - .{ .id = 88, .prg_banks = 8, .chr_banks = 16 }, // Namco 118 - .{ .id = 89, .prg_banks = 8, .chr_banks = 16 }, // Sunsoft-2 - .{ .id = 93, .prg_banks = 8, .chr_banks = 0 }, // Sunsoft-3 - .{ .id = 94, .prg_banks = 16, .chr_banks = 0 }, // UN1ROM - .{ .id = 118, .prg_banks = 8, .chr_banks = 16 }, // TxSROM - .{ .id = 119, .prg_banks = 8, .chr_banks = 16 }, // TQROM - .{ .id = 140, .prg_banks = 4, .chr_banks = 16 }, // Jaleco Command - .{ .id = 180, .prg_banks = 8, .chr_banks = 0 }, // Crazy Climber - .{ .id = 206, .prg_banks = 8, .chr_banks = 16 }, // DxROM / Namco 108 - .{ .id = 210, .prg_banks = 8, .chr_banks = 16 }, // Namco 175/340 - .{ .id = 232, .prg_banks = 16, .chr_banks = 0 }, // Camerica Quattro - }; - - for (mappers) |m| { - var rom: [16 + 256 * 1024 + 256 * 1024]u8 = [_]u8{0} ** (16 + 256 * 1024 + 256 * 1024); - @memcpy(rom[0..4], "NES\x1a"); - rom[4] = m.prg_banks; - rom[5] = m.chr_banks; - rom[6] = (m.id & 0x0F) << 4; - rom[7] = m.id & 0xF0; - - var nes: Nes = undefined; - try nes.init(&rom); - nes.runFrame(); - } -} - -test "NES System - Nestest Conformance" { - if (build_options.nes_test_roms_dir == null) return error.SkipZigTest; - try runNesTestSystem("other/nestest.nes", 100_000); -} - -test "NES System - Instr Test: 01-implied" { - if (build_options.nes_test_roms_dir == null) return error.SkipZigTest; - try runBlarggTest("nes_instr_test/rom_singles/01-implied.nes", 10_000_000); -} - -test "NES System - Instr Test: 02-immediate" { - if (build_options.nes_test_roms_dir == null) return error.SkipZigTest; - try runBlarggTest("nes_instr_test/rom_singles/02-immediate.nes", 10_000_000); -} - -test "NES System - Instr Test: 03-zero_page" { - if (build_options.nes_test_roms_dir == null) return error.SkipZigTest; - try runBlarggTest("nes_instr_test/rom_singles/03-zero_page.nes", 10_000_000); -} - -test "NES System - Instr Test: 04-zp_xy" { - if (build_options.nes_test_roms_dir == null) return error.SkipZigTest; - try runBlarggTest("nes_instr_test/rom_singles/04-zp_xy.nes", 10_000_000); -} - -test "NES System - Instr Test: 05-absolute" { - if (build_options.nes_test_roms_dir == null) return error.SkipZigTest; - try runBlarggTest("nes_instr_test/rom_singles/05-absolute.nes", 10_000_000); -} - -test "NES System - Instr Test: 06-abs_xy" { - if (build_options.nes_test_roms_dir == null) return error.SkipZigTest; - try runBlarggTest("nes_instr_test/rom_singles/06-abs_xy.nes", 10_000_000); -} - -test "NES System - Instr Test: 07-ind_x" { - if (build_options.nes_test_roms_dir == null) return error.SkipZigTest; - try runBlarggTest("nes_instr_test/rom_singles/07-ind_x.nes", 10_000_000); -} - -test "NES System - Instr Test: 08-ind_y" { - if (build_options.nes_test_roms_dir == null) return error.SkipZigTest; - try runBlarggTest("nes_instr_test/rom_singles/08-ind_y.nes", 10_000_000); -} - -test "NES System - Instr Test: 09-branches" { - if (build_options.nes_test_roms_dir == null) return error.SkipZigTest; - try runBlarggTest("nes_instr_test/rom_singles/09-branches.nes", 10_000_000); -} - -test "NES System - Instr Test: 10-stack" { - if (build_options.nes_test_roms_dir == null) return error.SkipZigTest; - try runBlarggTest("nes_instr_test/rom_singles/10-stack.nes", 10_000_000); -} - -test "NES System - Instr Test: 11-special" { - if (build_options.nes_test_roms_dir == null) return error.SkipZigTest; - try runBlarggTest("nes_instr_test/rom_singles/11-special.nes", 10_000_000); -} diff --git a/src/system/nes/controller.zig b/src/system/nes/controller.zig deleted file mode 100644 index cd1b587..0000000 --- a/src/system/nes/controller.zig +++ /dev/null @@ -1,70 +0,0 @@ -pub const Button = enum(u3) { - a = 0, - b = 1, - select = 2, - start = 3, - up = 4, - down = 5, - left = 6, - right = 7, -}; - -const Controller = @This(); - -buttons: u8 = 0, - -shift_register: u8 = 0, -strobe: bool = false, - -pub fn reset( - self: *Controller, -) void { - self.shift_register = 0; - self.strobe = false; -} - -pub fn setButtons( - self: *Controller, - buttons: u8, -) void { - self.buttons = buttons; - - if (self.strobe) - self.shift_register = buttons; -} - -pub fn write( - self: *Controller, - value: u8, -) void { - const new_strobe = - (value & 1) != 0; - - if (new_strobe) { - self.shift_register = - self.buttons; - } else if (self.strobe) { - // Falling edge latches controller. - self.shift_register = - self.buttons; - } - - self.strobe = new_strobe; -} - -pub fn read( - self: *Controller, -) u8 { - if (self.strobe) - return self.buttons & 1; - - const value = - self.shift_register & 1; - - // Real controllers return 1 after all - // eight buttons have shifted out. - self.shift_register = - (self.shift_register >> 1) | 0x80; - - return value; -} diff --git a/src/system/nes/mapper/axrom.zig b/src/system/nes/mapper/axrom.zig deleted file mode 100644 index c5f036c..0000000 --- a/src/system/nes/mapper/axrom.zig +++ /dev/null @@ -1,64 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Axrom = @This(); - -prg_bank_select: u8 = 0, -mirroring_select: u1 = 0, -prg_bank_count: usize, - -pub fn init( - prg_size: usize, -) Axrom { - return .{ - .prg_bank_count = prg_size / 0x8000, - }; -} - -pub fn cpuMapRead( - self: *const Axrom, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address - 0x8000); - const bank = @as(usize, self.prg_bank_select & 0x07) % self.prg_bank_count; - return bank * 0x8000 + offset; -} - -pub fn cpuWrite( - self: *Axrom, - address: u16, - value: u8, -) void { - if (address >= 0x8000) { - self.prg_bank_select = value & 0x07; - self.mirroring_select = @truncate((value >> 4) & 1); - } -} - -pub fn ppuMapRead( - _: *const Axrom, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn ppuMapWrite( - _: *const Axrom, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Axrom, -) Mirroring { - return if (self.mirroring_select == 0) .single_screen_lower else .single_screen_upper; -} - -pub fn irqAsserted(_: *const Axrom) bool { - return false; -} diff --git a/src/system/nes/mapper/camerica.zig b/src/system/nes/mapper/camerica.zig deleted file mode 100644 index b187558..0000000 --- a/src/system/nes/mapper/camerica.zig +++ /dev/null @@ -1,82 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Camerica = @This(); - -prg_bank: u8 = 0, -mirroring_select: u1 = 0, -mirroring_mode: Mirroring, -has_single_screen_mirroring: bool = false, - -prg_bank_count: usize, - -pub fn init( - prg_size: usize, - _mirroring: Mirroring, -) Camerica { - return .{ - .prg_bank_count = prg_size / 0x4000, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const Camerica, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - if (address < 0xC000) { - const offset = @as(usize, address - 0x8000); - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - return bank * 0x4000 + offset; - } else { - const offset = @as(usize, address - 0xC000); - const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0; - return fixed_bank * 0x4000 + offset; - } -} - -pub fn cpuWrite( - self: *Camerica, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - if (address >= 0x8000 and address <= 0x9FFF) { - self.has_single_screen_mirroring = true; - self.mirroring_select = @truncate((value >> 4) & 1); - } else if (address >= 0xC000) { - self.prg_bank = value & 0x0F; - } -} - -pub fn ppuMapRead( - _: *const Camerica, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn ppuMapWrite( - _: *const Camerica, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Camerica, -) Mirroring { - if (self.has_single_screen_mirroring) { - return if (self.mirroring_select == 0) .single_screen_lower else .single_screen_upper; - } - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Camerica) bool { - return false; -} diff --git a/src/system/nes/mapper/cnrom.zig b/src/system/nes/mapper/cnrom.zig deleted file mode 100644 index cbcc891..0000000 --- a/src/system/nes/mapper/cnrom.zig +++ /dev/null @@ -1,69 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Cnrom = @This(); - -prg_16k: bool, -chr_bank_select: u8 = 0, -chr_bank_count: usize, -mirroring_mode: Mirroring, - -pub fn init( - prg_size: usize, - chr_size: usize, - _mirroring: Mirroring, -) Cnrom { - return .{ - .prg_16k = prg_size == 0x4000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const Cnrom, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address - 0x8000); - return if (self.prg_16k) offset & 0x3fff else offset; -} - -pub fn cpuWrite( - self: *Cnrom, - address: u16, - value: u8, -) void { - if (address >= 0x8000) { - self.chr_bank_select = value; - } -} - -pub fn ppuMapRead( - self: *const Cnrom, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const bank = @as(usize, self.chr_bank_select) % self.chr_bank_count; - return bank * 0x2000 + @as(usize, address); -} - -pub fn ppuMapWrite( - _: *const Cnrom, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Cnrom, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Cnrom) bool { - return false; -} diff --git a/src/system/nes/mapper/colordreams.zig b/src/system/nes/mapper/colordreams.zig deleted file mode 100644 index e713d7d..0000000 --- a/src/system/nes/mapper/colordreams.zig +++ /dev/null @@ -1,73 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const ColorDreams = @This(); - -prg_bank: u8 = 0, -chr_bank: u8 = 0, - -prg_bank_count: usize, -chr_bank_count: usize, -mirroring_mode: Mirroring, - -pub fn init( - prg_size: usize, - chr_size: usize, - _mirroring: Mirroring, -) ColorDreams { - return .{ - .prg_bank_count = prg_size / 0x8000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const ColorDreams, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address - 0x8000); - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - return bank * 0x8000 + offset; -} - -pub fn cpuWrite( - self: *ColorDreams, - address: u16, - value: u8, -) void { - if (address >= 0x8000) { - self.prg_bank = value & 0x03; - self.chr_bank = (value >> 4) & 0x0F; - } -} - -pub fn ppuMapRead( - self: *const ColorDreams, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const bank = @as(usize, self.chr_bank) % self.chr_bank_count; - return bank * 0x2000 + @as(usize, address); -} - -pub fn ppuMapWrite( - _: *const ColorDreams, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const ColorDreams, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const ColorDreams) bool { - return false; -} diff --git a/src/system/nes/mapper/gxrom.zig b/src/system/nes/mapper/gxrom.zig deleted file mode 100644 index f944b49..0000000 --- a/src/system/nes/mapper/gxrom.zig +++ /dev/null @@ -1,73 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Gxrom = @This(); - -prg_bank: u8 = 0, -chr_bank: u8 = 0, - -prg_bank_count: usize, -chr_bank_count: usize, -mirroring_mode: Mirroring, - -pub fn init( - prg_size: usize, - chr_size: usize, - _mirroring: Mirroring, -) Gxrom { - return .{ - .prg_bank_count = prg_size / 0x8000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const Gxrom, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address - 0x8000); - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - return bank * 0x8000 + offset; -} - -pub fn cpuWrite( - self: *Gxrom, - address: u16, - value: u8, -) void { - if (address >= 0x8000) { - self.chr_bank = value & 0x03; - self.prg_bank = (value >> 4) & 0x03; - } -} - -pub fn ppuMapRead( - self: *const Gxrom, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const bank = @as(usize, self.chr_bank) % self.chr_bank_count; - return bank * 0x2000 + @as(usize, address); -} - -pub fn ppuMapWrite( - _: *const Gxrom, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Gxrom, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Gxrom) bool { - return false; -} diff --git a/src/system/nes/mapper/irem78.zig b/src/system/nes/mapper/irem78.zig deleted file mode 100644 index a2ba7bd..0000000 --- a/src/system/nes/mapper/irem78.zig +++ /dev/null @@ -1,78 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Irem78 = @This(); - -prg_bank: u8 = 0, -chr_bank: u8 = 0, -mirroring_select: u1 = 0, - -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, -) Irem78 { - return .{ - .prg_bank_count = prg_size / 0x4000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1, - }; -} - -pub fn cpuMapRead( - self: *const Irem78, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - if (address < 0xC000) { - const offset = @as(usize, address - 0x8000); - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - return bank * 0x4000 + offset; - } else { - const offset = @as(usize, address - 0xC000); - const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0; - return fixed_bank * 0x4000 + offset; - } -} - -pub fn cpuWrite( - self: *Irem78, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - self.prg_bank = value & 0x07; - self.mirroring_select = @truncate((value >> 3) & 1); - self.chr_bank = (value >> 4) & 0x0F; -} - -pub fn ppuMapRead( - self: *const Irem78, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const bank = @as(usize, self.chr_bank) % self.chr_bank_count; - return bank * 0x2000 + @as(usize, address); -} - -pub fn ppuMapWrite( - _: *const Irem78, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Irem78, -) Mirroring { - return if (self.mirroring_select == 0) .single_screen_lower else .single_screen_upper; -} - -pub fn irqAsserted(_: *const Irem78) bool { - return false; -} diff --git a/src/system/nes/mapper/mapper118.zig b/src/system/nes/mapper/mapper118.zig deleted file mode 100644 index 55d649d..0000000 --- a/src/system/nes/mapper/mapper118.zig +++ /dev/null @@ -1,69 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; -const Mmc3 = @import("mmc3.zig"); - -const Mapper118 = @This(); - -mmc3: Mmc3, -mirror_bit: u1 = 0, - -pub fn init( - prg_size: usize, - chr_size: usize, -) Mapper118 { - return .{ - .mmc3 = Mmc3.init(prg_size, chr_size), - }; -} - -pub fn cpuMapRead( - self: *const Mapper118, - address: u16, -) ?usize { - return self.mmc3.cpuMapRead(address); -} - -pub fn cpuWrite( - self: *Mapper118, - address: u16, - value: u8, -) void { - if (address == 0x8001) { - if (self.mmc3.bank_select <= 5) { - self.mirror_bit = @truncate((value >> 7) & 1); - } - } - self.mmc3.cpuWrite(address, value); -} - -pub fn ppuMapRead( - self: *Mapper118, - address: u16, -) ?usize { - return self.mmc3.ppuMapRead(address); -} - -pub fn ppuMapWrite( - self: *const Mapper118, - address: u16, -) ?usize { - return self.mmc3.ppuMapWrite(address); -} - -pub fn mirroring( - self: *const Mapper118, -) Mirroring { - return if (self.mirror_bit == 1) .single_screen_upper else .single_screen_lower; -} - -pub fn irqAsserted( - self: *const Mapper118, -) bool { - return self.mmc3.irqAsserted(); -} - -pub fn handleScanline( - self: *Mapper118, -) void { - self.mmc3.handleScanline(); -} diff --git a/src/system/nes/mapper/mapper119.zig b/src/system/nes/mapper/mapper119.zig deleted file mode 100644 index cf9bdb8..0000000 --- a/src/system/nes/mapper/mapper119.zig +++ /dev/null @@ -1,113 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; -const Mmc3 = @import("mmc3.zig"); - -const Mapper119 = @This(); - -mmc3: Mmc3, - -pub fn init( - prg_size: usize, - chr_size: usize, -) Mapper119 { - return .{ - .mmc3 = Mmc3.init(prg_size, chr_size), - }; -} - -pub fn cpuMapRead( - self: *const Mapper119, - address: u16, -) ?usize { - return self.mmc3.cpuMapRead(address); -} - -pub fn cpuWrite( - self: *Mapper119, - address: u16, - value: u8, -) void { - self.mmc3.cpuWrite(address, value); -} - -pub fn ppuMapRead( - self: *Mapper119, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const bank_val = self.getChrBankValue(address); - const offset = (address % 0x0400); - - if ((bank_val & 0x40) != 0) { - // Bit 6 is set -> Route to CHR RAM - const ram_bank = @as(usize, bank_val & 7); - return 0x80000000 | (ram_bank * 0x0400 + offset); - } else { - // Route to CHR ROM - return self.mmc3.ppuMapRead(address); - } -} - -pub fn ppuMapWrite( - self: *const Mapper119, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const bank_val = self.getChrBankValue(address); - const offset = (address % 0x0400); - - if ((bank_val & 0x40) != 0) { - // Bit 6 is set -> Route write to CHR RAM - const ram_bank = @as(usize, bank_val & 7); - return 0x80000000 | (ram_bank * 0x0400 + offset); - } else { - return self.mmc3.ppuMapWrite(address); - } -} - -fn getChrBankValue(self: *const Mapper119, address: u16) u8 { - const chr_mode = (self.mmc3.bank_select & 0x80) != 0; - const addr_k = address / 0x0400; - - if (!chr_mode) { - return switch (addr_k) { - 0, 1 => (self.mmc3.bank_registers[0] & 0xfe) + @as(u8, @intCast(addr_k & 1)), - 2, 3 => (self.mmc3.bank_registers[1] & 0xfe) + @as(u8, @intCast(addr_k & 1)), - 4 => self.mmc3.bank_registers[2], - 5 => self.mmc3.bank_registers[3], - 6 => self.mmc3.bank_registers[4], - 7 => self.mmc3.bank_registers[5], - else => unreachable, - }; - } else { - return switch (addr_k) { - 0 => self.mmc3.bank_registers[2], - 1 => self.mmc3.bank_registers[3], - 2 => self.mmc3.bank_registers[4], - 3 => self.mmc3.bank_registers[5], - 4, 5 => (self.mmc3.bank_registers[0] & 0xfe) + @as(u8, @intCast(addr_k & 1)), - 6, 7 => (self.mmc3.bank_registers[1] & 0xfe) + @as(u8, @intCast(addr_k & 1)), - else => unreachable, - }; - } -} - -pub fn mirroring( - self: *const Mapper119, -) Mirroring { - return self.mmc3.mirroring(); -} - -pub fn irqAsserted( - self: *const Mapper119, -) bool { - return self.mmc3.irqAsserted(); -} - -pub fn handleScanline( - self: *Mapper119, -) void { - self.mmc3.handleScanline(); -} diff --git a/src/system/nes/mapper/mapper13.zig b/src/system/nes/mapper/mapper13.zig deleted file mode 100644 index 9fbd01e..0000000 --- a/src/system/nes/mapper/mapper13.zig +++ /dev/null @@ -1,69 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper13 = @This(); - -chr_bank: u8 = 0, -fixed_mirroring: Mirroring, -prg_mask: usize, - -pub fn init( - prg_size: usize, - mirroring_mode: Mirroring, -) Mapper13 { - return .{ - .fixed_mirroring = mirroring_mode, - .prg_mask = if (prg_size > 0) prg_size - 1 else 0x7fff, - }; -} - -pub fn cpuMapRead( - self: *const Mapper13, - address: u16, -) ?usize { - if (address < 0x8000) return null; - return (@as(usize, address) - 0x8000) & self.prg_mask; -} - -pub fn cpuWrite( - self: *Mapper13, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - self.chr_bank = value & 0x03; -} - -pub fn ppuMapRead( - self: *const Mapper13, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - if (address < 0x1000) { - return 0x80000000 | @as(usize, address); - } else { - const bank = @as(usize, self.chr_bank & 3); - const offset = @as(usize, address & 0x0fff); - return 0x80000000 | (bank * 0x1000 + offset); - } -} - -pub fn ppuMapWrite( - self: *const Mapper13, - address: u16, -) ?usize { - return self.ppuMapRead(address); -} - -pub fn mirroring( - self: *const Mapper13, -) Mirroring { - return self.fixed_mirroring; -} - -pub fn irqAsserted( - _: *const Mapper13, -) bool { - return false; -} diff --git a/src/system/nes/mapper/mapper140.zig b/src/system/nes/mapper/mapper140.zig deleted file mode 100644 index fd34464..0000000 --- a/src/system/nes/mapper/mapper140.zig +++ /dev/null @@ -1,73 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper140 = @This(); - -prg_bank: u8 = 0, -chr_bank: u8 = 0, - -prg_bank_count: usize, -chr_bank_count: usize, -mirroring_mode: Mirroring, - -pub fn init( - prg_size: usize, - chr_size: usize, - _mirroring: Mirroring, -) Mapper140 { - return .{ - .prg_bank_count = prg_size / 0x8000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const Mapper140, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address - 0x8000); - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - return bank * 0x8000 + offset; -} - -pub fn cpuWrite( - self: *Mapper140, - address: u16, - value: u8, -) void { - if (address >= 0x6000 and address <= 0x7FFF) { - self.prg_bank = (value >> 4) & 0x03; - self.chr_bank = value & 0x0F; - } -} - -pub fn ppuMapRead( - self: *const Mapper140, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const bank = @as(usize, self.chr_bank) % self.chr_bank_count; - return bank * 0x2000 + @as(usize, address); -} - -pub fn ppuMapWrite( - _: *const Mapper140, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Mapper140, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Mapper140) bool { - return false; -} diff --git a/src/system/nes/mapper/mapper180.zig b/src/system/nes/mapper/mapper180.zig deleted file mode 100644 index f6c2088..0000000 --- a/src/system/nes/mapper/mapper180.zig +++ /dev/null @@ -1,70 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper180 = @This(); - -prg_bank: u8 = 0, -prg_bank_count: usize, -mirroring_mode: Mirroring, - -pub fn init( - prg_size: usize, - _mirroring: Mirroring, -) Mapper180 { - return .{ - .prg_bank_count = prg_size / 0x4000, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const Mapper180, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - if (address < 0xC000) { - const offset = @as(usize, address - 0x8000); - return offset; // Fixed bank 0 - } else { - const offset = @as(usize, address - 0xC000); - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - return bank * 0x4000 + offset; - } -} - -pub fn cpuWrite( - self: *Mapper180, - address: u16, - value: u8, -) void { - if (address >= 0x8000) { - self.prg_bank = value & 0x07; - } -} - -pub fn ppuMapRead( - _: *const Mapper180, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn ppuMapWrite( - _: *const Mapper180, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Mapper180, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Mapper180) bool { - return false; -} diff --git a/src/system/nes/mapper/mapper206.zig b/src/system/nes/mapper/mapper206.zig deleted file mode 100644 index 1cdd201..0000000 --- a/src/system/nes/mapper/mapper206.zig +++ /dev/null @@ -1,108 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper206 = @This(); - -bank_select: u3 = 0, - -prg_bank0: u8 = 0, -prg_bank1: u8 = 1, - -chr_banks: [6]u8 = [_]u8{ 0, 2, 4, 5, 6, 7 }, - -prg_bank_count: usize, -chr_bank_count: usize, -mirroring_mode: Mirroring, - -pub fn init( - prg_size: usize, - chr_size: usize, - _mirroring: Mirroring, -) Mapper206 { - return .{ - .prg_bank_count = prg_size / 0x2000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const Mapper206, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - if (address < 0xA000) { - const offset = @as(usize, address - 0x8000); - const bank = @as(usize, self.prg_bank0) % self.prg_bank_count; - return bank * 0x2000 + offset; - } else if (address < 0xC000) { - const offset = @as(usize, address - 0xA000); - const bank = @as(usize, self.prg_bank1) % self.prg_bank_count; - return bank * 0x2000 + offset; - } else if (address < 0xE000) { - const offset = @as(usize, address - 0xC000); - const bank = if (self.prg_bank_count >= 2) self.prg_bank_count - 2 else 0; - return bank * 0x2000 + offset; - } else { - const offset = @as(usize, address - 0xE000); - const bank = if (self.prg_bank_count >= 1) self.prg_bank_count - 1 else 0; - return bank * 0x2000 + offset; - } -} - -pub fn cpuWrite( - self: *Mapper206, - address: u16, - value: u8, -) void { - if (address >= 0x8000 and address <= 0x9FFF) { - if ((address & 1) == 0) { - self.bank_select = @truncate(value & 0x07); - } else { - switch (self.bank_select) { - 0...5 => |i| self.chr_banks[i] = value & 0x3F, - 6 => self.prg_bank0 = value & 0x0F, - 7 => self.prg_bank1 = value & 0x0F, - } - } - } -} - -pub fn ppuMapRead( - self: *const Mapper206, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - if (address < 0x0800) { - const bank = (@as(usize, self.chr_banks[0] & 0xFE)) % self.chr_bank_count; - return bank * 0x0400 + @as(usize, address); - } else if (address < 0x1000) { - const bank = (@as(usize, self.chr_banks[1] & 0xFE)) % self.chr_bank_count; - return bank * 0x0400 + @as(usize, address - 0x0800); - } else { - const idx = 2 + (address - 0x1000) / 0x0400; - const offset = (address - 0x1000) % 0x0400; - const bank = @as(usize, self.chr_banks[idx]) % self.chr_bank_count; - return bank * 0x0400 + @as(usize, offset); - } -} - -pub fn ppuMapWrite( - _: *const Mapper206, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Mapper206, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Mapper206) bool { - return false; -} diff --git a/src/system/nes/mapper/mapper210.zig b/src/system/nes/mapper/mapper210.zig deleted file mode 100644 index 33395ef..0000000 --- a/src/system/nes/mapper/mapper210.zig +++ /dev/null @@ -1,105 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper210 = @This(); - -prg_bank0: u8 = 0, -prg_bank1: u8 = 0, -prg_bank2: u8 = 0, - -chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 }, -mirroring_mode: Mirroring = .vertical, - -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, -) Mapper210 { - return .{ - .prg_bank_count = prg_size / 0x2000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1, - }; -} - -pub fn cpuMapRead( - self: *const Mapper210, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - if (address < 0xA000) { - const offset = @as(usize, address - 0x8000); - const bank = @as(usize, self.prg_bank0) % self.prg_bank_count; - return bank * 0x2000 + offset; - } else if (address < 0xC000) { - const offset = @as(usize, address - 0xA000); - const bank = @as(usize, self.prg_bank1) % self.prg_bank_count; - return bank * 0x2000 + offset; - } else if (address < 0xE000) { - const offset = @as(usize, address - 0xC000); - const bank = @as(usize, self.prg_bank2) % self.prg_bank_count; - return bank * 0x2000 + offset; - } else { - const offset = @as(usize, address - 0xE000); - const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0; - return fixed_bank * 0x2000 + offset; - } -} - -pub fn cpuWrite( - self: *Mapper210, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - if (address >= 0x8000 and address <= 0xBFFF) { - const index = (address - 0x8000) / 0x0800; - self.chr_banks[index] = value; - } else if (address >= 0xC000 and address <= 0xC7FF) { - self.prg_bank0 = value & 0x3F; - } else if (address >= 0xC800 and address <= 0xCFFF) { - self.prg_bank1 = value & 0x3F; - } else if (address >= 0xD000 and address <= 0xD7FF) { - self.prg_bank2 = value & 0x3F; - } else if (address >= 0xE000 and address <= 0xE7FF) { - self.mirroring_mode = switch (@as(u2, @truncate(value >> 6))) { - 0 => .vertical, - 1 => .horizontal, - 2 => .single_screen_lower, - 3 => .single_screen_upper, - }; - } -} - -pub fn ppuMapRead( - self: *const Mapper210, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const bank_idx = address / 0x0400; - const offset = address % 0x0400; - const bank = @as(usize, self.chr_banks[bank_idx]) % self.chr_bank_count; - return bank * 0x0400 + offset; -} - -pub fn ppuMapWrite( - _: *const Mapper210, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Mapper210, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Mapper210) bool { - return false; -} diff --git a/src/system/nes/mapper/mapper232.zig b/src/system/nes/mapper/mapper232.zig deleted file mode 100644 index da620f7..0000000 --- a/src/system/nes/mapper/mapper232.zig +++ /dev/null @@ -1,77 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper232 = @This(); - -outer_block: u8 = 0, -inner_bank: u8 = 0, - -prg_bank_count: usize, -mirroring_mode: Mirroring, - -pub fn init( - prg_size: usize, - _mirroring: Mirroring, -) Mapper232 { - return .{ - .prg_bank_count = prg_size / 0x4000, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const Mapper232, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const block_start = (@as(usize, self.outer_block) * 4) % self.prg_bank_count; - - if (address < 0xC000) { - const offset = @as(usize, address - 0x8000); - const bank = (block_start + (@as(usize, self.inner_bank) & 3)) % self.prg_bank_count; - return bank * 0x4000 + offset; - } else { - const offset = @as(usize, address - 0xC000); - const bank = (block_start + 3) % self.prg_bank_count; - return bank * 0x4000 + offset; - } -} - -pub fn cpuWrite( - self: *Mapper232, - address: u16, - value: u8, -) void { - if (address >= 0x8000 and address <= 0x9FFF) { - self.outer_block = (value >> 3) & 0x03; - } else if (address >= 0xA000) { - self.inner_bank = value & 0x03; - } -} - -pub fn ppuMapRead( - _: *const Mapper232, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn ppuMapWrite( - _: *const Mapper232, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Mapper232, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Mapper232) bool { - return false; -} diff --git a/src/system/nes/mapper/mapper34.zig b/src/system/nes/mapper/mapper34.zig deleted file mode 100644 index d5b7ca4..0000000 --- a/src/system/nes/mapper/mapper34.zig +++ /dev/null @@ -1,92 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper34 = @This(); - -prg_bank: u8 = 0, -chr_bank0: u8 = 0, -chr_bank1: u8 = 1, - -prg_bank_count: usize, -chr_bank_count: usize, -fixed_mirroring: Mirroring, -is_nina01: bool, - -pub fn init( - prg_size: usize, - chr_size: usize, - mirroring_mode: Mirroring, -) Mapper34 { - return .{ - .prg_bank_count = if (prg_size >= 0x8000) prg_size / 0x8000 else 1, - .chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 1, - .fixed_mirroring = mirroring_mode, - .is_nina01 = (chr_size > 0), - }; -} - -pub fn cpuMapRead( - self: *const Mapper34, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address & 0x7fff); - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - - return bank * 0x8000 + offset; -} - -pub fn cpuWrite( - self: *Mapper34, - address: u16, - value: u8, -) void { - if (self.is_nina01) { - switch (address) { - 0x7ffd => self.prg_bank = value, - 0x7ffe => self.chr_bank0 = value, - 0x7fff => self.chr_bank1 = value, - else => {}, - } - } else { - if (address >= 0x8000) { - self.prg_bank = value; - } - } -} - -pub fn ppuMapRead( - self: *const Mapper34, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - if (self.is_nina01) { - const offset = @as(usize, address & 0x0fff); - const bank_raw = if (address < 0x1000) self.chr_bank0 else self.chr_bank1; - const bank = @as(usize, bank_raw) % self.chr_bank_count; - return bank * 0x1000 + offset; - } else { - return @as(usize, address); - } -} - -pub fn ppuMapWrite( - self: *const Mapper34, - address: u16, -) ?usize { - return self.ppuMapRead(address); -} - -pub fn mirroring( - self: *const Mapper34, -) Mirroring { - return self.fixed_mirroring; -} - -pub fn irqAsserted( - _: *const Mapper34, -) bool { - return false; -} diff --git a/src/system/nes/mapper/mapper65.zig b/src/system/nes/mapper/mapper65.zig deleted file mode 100644 index f6a255c..0000000 --- a/src/system/nes/mapper/mapper65.zig +++ /dev/null @@ -1,118 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper65 = @This(); - -prg_banks: [3]u8 = [_]u8{ 0, 1, 2 }, -chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 }, - -mirroring_mode: Mirroring = .vertical, - -irq_enabled: bool = false, -irq_counter: u16 = 0, -irq_reload: u16 = 0, -irq_assert: bool = false, - -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, - mirroring_mode: Mirroring, -) Mapper65 { - return .{ - .prg_bank_count = prg_size / 0x2000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1, - .mirroring_mode = mirroring_mode, - }; -} - -pub fn cpuMapRead( - self: *const Mapper65, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address & 0x1fff); - const last_bank = self.prg_bank_count - 1; - - switch ((address - 0x8000) / 0x2000) { - 0 => return (@as(usize, self.prg_banks[0]) % self.prg_bank_count) * 0x2000 + offset, - 1 => return (@as(usize, self.prg_banks[1]) % self.prg_bank_count) * 0x2000 + offset, - 2 => return (@as(usize, self.prg_banks[2]) % self.prg_bank_count) * 0x2000 + offset, - 3 => return last_bank * 0x2000 + offset, - else => unreachable, - } -} - -pub fn cpuWrite( - self: *Mapper65, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - switch (address) { - 0x8000 => self.prg_banks[0] = value, - 0xa000 => self.prg_banks[1] = value, - 0xc000 => self.prg_banks[2] = value, - - 0x9001 => self.mirroring_mode = if ((value & 0x80) != 0) .horizontal else .vertical, - - 0x9003 => { - self.irq_enabled = (value & 0x80) != 0; - self.irq_assert = false; - }, - 0x9004 => { - self.irq_reload = (self.irq_reload & 0xff00) | value; - }, - 0x9005 => { - self.irq_reload = (self.irq_reload & 0x00ff) | (@as(u16, value) << 8); - self.irq_counter = self.irq_reload; - self.irq_assert = false; - }, - - 0xb000 => self.chr_banks[0] = value, - 0xb001 => self.chr_banks[1] = value, - 0xb002 => self.chr_banks[2] = value, - 0xb003 => self.chr_banks[3] = value, - 0xb004 => self.chr_banks[4] = value, - 0xb005 => self.chr_banks[5] = value, - 0xb006 => self.chr_banks[6] = value, - 0xb007 => self.chr_banks[7] = value, - else => {}, - } -} - -pub fn ppuMapRead( - self: *const Mapper65, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const slot = address / 0x0400; - const offset = @as(usize, address & 0x03ff); - const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count; - - return bank * 0x0400 + offset; -} - -pub fn ppuMapWrite( - self: *const Mapper65, - address: u16, -) ?usize { - return self.ppuMapRead(address); -} - -pub fn mirroring( - self: *const Mapper65, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted( - self: *const Mapper65, -) bool { - return self.irq_assert; -} diff --git a/src/system/nes/mapper/mapper68.zig b/src/system/nes/mapper/mapper68.zig deleted file mode 100644 index 95e8ced..0000000 --- a/src/system/nes/mapper/mapper68.zig +++ /dev/null @@ -1,103 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper68 = @This(); - -chr_banks: [4]u8 = [_]u8{ 0, 0, 0, 0 }, -nt_banks: [2]u8 = [_]u8{ 0, 0 }, -prg_bank: u8 = 0, -nametable_control: u8 = 0, - -prg_bank_count: usize, -chr_bank_count: usize, -fixed_mirroring: Mirroring, - -pub fn init( - prg_size: usize, - chr_size: usize, - mirroring_mode: Mirroring, -) Mapper68 { - return .{ - .prg_bank_count = prg_size / 0x4000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x0800 else 1, - .fixed_mirroring = mirroring_mode, - }; -} - -pub fn cpuMapRead( - self: *const Mapper68, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address & 0x3fff); - const last_bank = self.prg_bank_count - 1; - - if (address < 0xc000) { - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - return bank * 0x4000 + offset; - } else { - return last_bank * 0x4000 + offset; - } -} - -pub fn cpuWrite( - self: *Mapper68, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - switch (address & 0xf000) { - 0x8000 => self.chr_banks[0] = value, - 0x9000 => self.chr_banks[1] = value, - 0xa000 => self.chr_banks[2] = value, - 0xb000 => self.chr_banks[3] = value, - 0xc000 => self.nt_banks[0] = value, - 0xd000 => self.nt_banks[1] = value, - 0xe000 => self.nametable_control = value, - 0xf000 => self.prg_bank = value & 0x0f, - else => {}, - } -} - -pub fn ppuMapRead( - self: *const Mapper68, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const slot = address / 0x0800; - const offset = @as(usize, address & 0x07ff); - const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count; - - return bank * 0x0800 + offset; -} - -pub fn ppuMapWrite( - self: *const Mapper68, - address: u16, -) ?usize { - return self.ppuMapRead(address); -} - -pub fn mirroring( - self: *const Mapper68, -) Mirroring { - if ((self.nametable_control & 0x10) == 0) { - switch (self.nametable_control & 0x03) { - 0 => return .vertical, - 1 => return .horizontal, - 2 => return .single_screen_lower, - 3 => return .single_screen_upper, - else => unreachable, - } - } - return self.fixed_mirroring; -} - -pub fn irqAsserted( - _: *const Mapper68, -) bool { - return false; -} diff --git a/src/system/nes/mapper/mapper69.zig b/src/system/nes/mapper/mapper69.zig deleted file mode 100644 index b40c45d..0000000 --- a/src/system/nes/mapper/mapper69.zig +++ /dev/null @@ -1,123 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper69 = @This(); - -command: u8 = 0, -chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 }, -prg_banks: [4]u8 = [_]u8{ 0, 1, 2, 3 }, // $6000, $8000, $A000, $C000 -mirroring_mode: Mirroring = .vertical, - -irq_enabled: bool = false, -irq_counter: u16 = 0, -irq_assert: bool = false, - -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, - mirroring_mode: Mirroring, -) Mapper69 { - return .{ - .prg_bank_count = prg_size / 0x2000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1, - .mirroring_mode = mirroring_mode, - }; -} - -pub fn cpuMapRead( - self: *const Mapper69, - address: u16, -) ?usize { - if (address < 0x6000) return null; - - const offset = @as(usize, address & 0x1fff); - const last_bank = self.prg_bank_count - 1; - - if (address < 0x8000) { - const bank = @as(usize, self.prg_banks[0] & 0x3f) % self.prg_bank_count; - return bank * 0x2000 + offset; - } - - switch ((address - 0x8000) / 0x2000) { - 0 => return (@as(usize, self.prg_banks[1] & 0x3f) % self.prg_bank_count) * 0x2000 + offset, - 1 => return (@as(usize, self.prg_banks[2] & 0x3f) % self.prg_bank_count) * 0x2000 + offset, - 2 => return (@as(usize, self.prg_banks[3] & 0x3f) % self.prg_bank_count) * 0x2000 + offset, - 3 => return last_bank * 0x2000 + offset, - else => unreachable, - } -} - -pub fn cpuWrite( - self: *Mapper69, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - if (address < 0xa000) { - self.command = value & 0x0f; - } else if (address < 0xc000) { - switch (self.command) { - 0...7 => |i| self.chr_banks[i] = value, - 8 => self.prg_banks[0] = value, - 9 => self.prg_banks[1] = value, - 0xa => self.prg_banks[2] = value, - 0xb => self.prg_banks[3] = value, - 0xc => { - switch (value & 0x03) { - 0 => self.mirroring_mode = .vertical, - 1 => self.mirroring_mode = .horizontal, - 2 => self.mirroring_mode = .single_screen_lower, - 3 => self.mirroring_mode = .single_screen_upper, - else => unreachable, - } - }, - 0xd => { - self.irq_enabled = (value & 0x01) != 0; - self.irq_assert = false; - }, - 0xe => { - self.irq_counter = (self.irq_counter & 0xff00) | value; - }, - 0xf => { - self.irq_counter = (self.irq_counter & 0x00ff) | (@as(u16, value) << 8); - }, - else => {}, - } - } -} - -pub fn ppuMapRead( - self: *const Mapper69, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const slot = address / 0x0400; - const offset = @as(usize, address & 0x03ff); - const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count; - - return bank * 0x0400 + offset; -} - -pub fn ppuMapWrite( - self: *const Mapper69, - address: u16, -) ?usize { - return self.ppuMapRead(address); -} - -pub fn mirroring( - self: *const Mapper69, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted( - self: *const Mapper69, -) bool { - return self.irq_assert; -} diff --git a/src/system/nes/mapper/mapper76.zig b/src/system/nes/mapper/mapper76.zig deleted file mode 100644 index 3a67586..0000000 --- a/src/system/nes/mapper/mapper76.zig +++ /dev/null @@ -1,91 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper76 = @This(); - -bank_select: u8 = 0, -bank_registers: [8]u8 = [_]u8{0} ** 8, -prg_bank_count: usize, -chr_bank_count: usize, -fixed_mirroring: Mirroring, - -pub fn init( - prg_size: usize, - chr_size: usize, - mirroring_mode: Mirroring, -) Mapper76 { - return .{ - .prg_bank_count = prg_size / 0x2000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x0800 else 1, - .fixed_mirroring = mirroring_mode, - }; -} - -pub fn cpuMapRead( - self: *const Mapper76, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address & 0x1fff); - const last_bank = self.prg_bank_count - 1; - const second_last_bank = self.prg_bank_count - 2; - - const r6 = @as(usize, self.bank_registers[6]) % self.prg_bank_count; - const r7 = @as(usize, self.bank_registers[7]) % self.prg_bank_count; - - switch ((address - 0x8000) / 0x2000) { - 0 => return r6 * 0x2000 + offset, - 1 => return r7 * 0x2000 + offset, - 2 => return second_last_bank * 0x2000 + offset, - 3 => return last_bank * 0x2000 + offset, - else => unreachable, - } -} - -pub fn cpuWrite( - self: *Mapper76, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - if ((address & 1) == 0) { - self.bank_select = value & 0x07; - } else { - self.bank_registers[self.bank_select] = value; - } -} - -pub fn ppuMapRead( - self: *const Mapper76, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const offset = @as(usize, address & 0x07ff); - const slot = address / 0x0800; - const reg_idx = @as(usize, 2 + slot); - const bank = (@as(usize, self.bank_registers[reg_idx])) % self.chr_bank_count; - - return bank * 0x0800 + offset; -} - -pub fn ppuMapWrite( - self: *const Mapper76, - address: u16, -) ?usize { - return self.ppuMapRead(address); -} - -pub fn mirroring( - self: *const Mapper76, -) Mirroring { - return self.fixed_mirroring; -} - -pub fn irqAsserted( - _: *const Mapper76, -) bool { - return false; -} diff --git a/src/system/nes/mapper/mapper87.zig b/src/system/nes/mapper/mapper87.zig deleted file mode 100644 index 932e2f4..0000000 --- a/src/system/nes/mapper/mapper87.zig +++ /dev/null @@ -1,70 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper87 = @This(); - -chr_bank: u8 = 0, -prg_bank_count: usize, -chr_bank_count: usize, -mirroring_mode: Mirroring, - -pub fn init( - prg_size: usize, - chr_size: usize, - _mirroring: Mirroring, -) Mapper87 { - return .{ - .prg_bank_count = prg_size / 0x8000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const Mapper87, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address - 0x8000); - const bank = if (self.prg_bank_count > 0) (self.prg_bank_count - 1) else 0; - return bank * 0x8000 + offset; -} - -pub fn cpuWrite( - self: *Mapper87, - address: u16, - value: u8, -) void { - if (address >= 0x6000 and address <= 0x7FFF) { - self.chr_bank = ((value & 1) << 1) | ((value >> 1) & 1); - } -} - -pub fn ppuMapRead( - self: *const Mapper87, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const bank = @as(usize, self.chr_bank) % self.chr_bank_count; - return bank * 0x2000 + @as(usize, address); -} - -pub fn ppuMapWrite( - _: *const Mapper87, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Mapper87, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Mapper87) bool { - return false; -} diff --git a/src/system/nes/mapper/mapper88.zig b/src/system/nes/mapper/mapper88.zig deleted file mode 100644 index fdc1cb8..0000000 --- a/src/system/nes/mapper/mapper88.zig +++ /dev/null @@ -1,103 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper88 = @This(); - -bank_select: u8 = 0, -bank_registers: [8]u8 = [_]u8{0} ** 8, -prg_bank_count: usize, -chr_bank_count: usize, -fixed_mirroring: Mirroring, - -pub fn init( - prg_size: usize, - chr_size: usize, - mirroring_mode: Mirroring, -) Mapper88 { - return .{ - .prg_bank_count = prg_size / 0x2000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1, - .fixed_mirroring = mirroring_mode, - }; -} - -pub fn cpuMapRead( - self: *const Mapper88, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address & 0x1fff); - const last_bank = self.prg_bank_count - 1; - const second_last_bank = self.prg_bank_count - 2; - - const r6 = @as(usize, self.bank_registers[6] & 0x3f) % self.prg_bank_count; - const r7 = @as(usize, self.bank_registers[7] & 0x3f) % self.prg_bank_count; - - switch ((address - 0x8000) / 0x2000) { - 0 => return r6 * 0x2000 + offset, - 1 => return r7 * 0x2000 + offset, - 2 => return second_last_bank * 0x2000 + offset, - 3 => return last_bank * 0x2000 + offset, - else => unreachable, - } -} - -pub fn cpuWrite( - self: *Mapper88, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - if ((address & 1) == 0) { - self.bank_select = value & 0x07; - } else { - self.bank_registers[self.bank_select] = value; - } -} - -pub fn ppuMapRead( - self: *const Mapper88, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const offset = @as(usize, address & 0x03ff); - const page = address / 0x0400; - - var bank: usize = 0; - switch (page) { - 0, 1 => bank = (@as(usize, self.bank_registers[0] & 0xfe) + (page & 1)), - 2, 3 => bank = (@as(usize, self.bank_registers[1] & 0xfe) + (page & 1)), - 4, 5, 6, 7 => { - const reg_idx = page - 2; // registers 2, 3, 4, 5 - const val = self.bank_registers[reg_idx]; - // Bit 6 maps to 64KB CHR block selection (bit 6 -> 0x40 added to bank number) - bank = (@as(usize, val & 0x3f) | (@as(usize, val & 0x40))); - }, - else => unreachable, - } - - bank = bank % self.chr_bank_count; - return bank * 0x0400 + offset; -} - -pub fn ppuMapWrite( - self: *const Mapper88, - address: u16, -) ?usize { - return self.ppuMapRead(address); -} - -pub fn mirroring( - self: *const Mapper88, -) Mirroring { - return self.fixed_mirroring; -} - -pub fn irqAsserted( - _: *const Mapper88, -) bool { - return false; -} diff --git a/src/system/nes/mapper/mapper89.zig b/src/system/nes/mapper/mapper89.zig deleted file mode 100644 index 10b62e2..0000000 --- a/src/system/nes/mapper/mapper89.zig +++ /dev/null @@ -1,78 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper89 = @This(); - -prg_bank: u8 = 0, -chr_bank: u8 = 0, -mirroring_mode: Mirroring = .single_screen_lower, - -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, -) Mapper89 { - return .{ - .prg_bank_count = prg_size / 0x4000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1, - }; -} - -pub fn cpuMapRead( - self: *const Mapper89, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - if (address < 0xC000) { - const offset = @as(usize, address - 0x8000); - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - return bank * 0x4000 + offset; - } else { - const offset = @as(usize, address - 0xC000); - const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0; - return fixed_bank * 0x4000 + offset; - } -} - -pub fn cpuWrite( - self: *Mapper89, - address: u16, - value: u8, -) void { - if (address >= 0x8000) { - self.prg_bank = (value >> 4) & 0x07; - self.chr_bank = (value & 0x07) | ((value & 0x80) >> 4); - self.mirroring_mode = if ((value & 0x08) != 0) .single_screen_upper else .single_screen_lower; - } -} - -pub fn ppuMapRead( - self: *const Mapper89, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const bank = @as(usize, self.chr_bank) % self.chr_bank_count; - return bank * 0x2000 + @as(usize, address); -} - -pub fn ppuMapWrite( - _: *const Mapper89, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Mapper89, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Mapper89) bool { - return false; -} diff --git a/src/system/nes/mapper/mapper93.zig b/src/system/nes/mapper/mapper93.zig deleted file mode 100644 index 438f62a..0000000 --- a/src/system/nes/mapper/mapper93.zig +++ /dev/null @@ -1,73 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper93 = @This(); - -prg_bank: u8 = 0, -mirroring_mode: Mirroring, - -prg_bank_count: usize, - -pub fn init( - prg_size: usize, - _mirroring: Mirroring, -) Mapper93 { - return .{ - .prg_bank_count = prg_size / 0x4000, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const Mapper93, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - if (address < 0xC000) { - const offset = @as(usize, address - 0x8000); - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - return bank * 0x4000 + offset; - } else { - const offset = @as(usize, address - 0xC000); - const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0; - return fixed_bank * 0x4000 + offset; - } -} - -pub fn cpuWrite( - self: *Mapper93, - address: u16, - value: u8, -) void { - if (address >= 0x8000) { - self.prg_bank = (value >> 4) & 0x07; - self.mirroring_mode = if ((value & 1) != 0) .horizontal else .vertical; - } -} - -pub fn ppuMapRead( - _: *const Mapper93, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn ppuMapWrite( - _: *const Mapper93, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Mapper93, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Mapper93) bool { - return false; -} diff --git a/src/system/nes/mapper/mapper94.zig b/src/system/nes/mapper/mapper94.zig deleted file mode 100644 index f10ff60..0000000 --- a/src/system/nes/mapper/mapper94.zig +++ /dev/null @@ -1,71 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mapper94 = @This(); - -prg_bank: u8 = 0, -prg_bank_count: usize, -mirroring_mode: Mirroring, - -pub fn init( - prg_size: usize, - _mirroring: Mirroring, -) Mapper94 { - return .{ - .prg_bank_count = prg_size / 0x4000, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const Mapper94, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - if (address < 0xC000) { - const offset = @as(usize, address - 0x8000); - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - return bank * 0x4000 + offset; - } else { - const offset = @as(usize, address - 0xC000); - const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0; - return fixed_bank * 0x4000 + offset; - } -} - -pub fn cpuWrite( - self: *Mapper94, - address: u16, - value: u8, -) void { - if (address >= 0x8000) { - self.prg_bank = (value >> 2) & 0x07; - } -} - -pub fn ppuMapRead( - _: *const Mapper94, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn ppuMapWrite( - _: *const Mapper94, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Mapper94, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Mapper94) bool { - return false; -} diff --git a/src/system/nes/mapper/mmc1.zig b/src/system/nes/mapper/mmc1.zig deleted file mode 100644 index 72111fb..0000000 --- a/src/system/nes/mapper/mmc1.zig +++ /dev/null @@ -1,139 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mmc1 = @This(); - -shift_register: u8 = 0x10, -control: u8 = 0x0c, // Default: PRG mode 3 (fix $C000) -chr_bank_0: u8 = 0, -chr_bank_1: u8 = 0, -prg_bank: u8 = 0, - -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, -) Mmc1 { - return .{ - .prg_bank_count = prg_size / 0x4000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 2, - }; -} - -pub fn cpuMapRead( - self: *const Mmc1, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const prg_mode = (self.control >> 2) & 0x03; - const offset = @as(usize, address & 0x3fff); - - switch (prg_mode) { - 0, 1 => { - // 32 KB mode - const bank = @as(usize, self.prg_bank & 0x0e) % self.prg_bank_count; - return bank * 0x4000 + @as(usize, address - 0x8000); - }, - 2 => { - // Fix first bank at $8000, switch 16 KB bank at $C000 - if (address < 0xc000) { - return offset; - } else { - const bank = @as(usize, self.prg_bank & 0x0f) % self.prg_bank_count; - return bank * 0x4000 + offset; - } - }, - 3 => { - // Fix last bank at $C000, switch 16 KB bank at $8000 - if (address < 0xc000) { - const bank = @as(usize, self.prg_bank & 0x0f) % self.prg_bank_count; - return bank * 0x4000 + offset; - } else { - const last_bank = self.prg_bank_count - 1; - return last_bank * 0x4000 + offset; - } - }, - else => unreachable, - } -} - -pub fn cpuWrite( - self: *Mmc1, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - // Bit 7 reset flag - if ((value & 0x80) != 0) { - self.shift_register = 0x10; - self.control |= 0x0c; - return; - } - - const complete = (self.shift_register & 1) != 0; - self.shift_register = (self.shift_register >> 1) | ((value & 1) << 4); - - if (complete) { - const reg_data = self.shift_register; - self.shift_register = 0x10; - - switch ((address >> 13) & 0x03) { - 0 => self.control = reg_data, - 1 => self.chr_bank_0 = reg_data, - 2 => self.chr_bank_1 = reg_data, - 3 => self.prg_bank = reg_data, - else => unreachable, - } - } -} - -pub fn ppuMapRead( - self: *const Mmc1, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const chr_mode = (self.control & 0x10) != 0; - if (!chr_mode) { - // 8 KB CHR mode - const bank = @as(usize, self.chr_bank_0 & 0x1e) % self.chr_bank_count; - return bank * 0x1000 + @as(usize, address); - } else { - // 4 KB CHR mode - if (address < 0x1000) { - const bank = @as(usize, self.chr_bank_0) % self.chr_bank_count; - return bank * 0x1000 + @as(usize, address & 0x0fff); - } else { - const bank = @as(usize, self.chr_bank_1) % self.chr_bank_count; - return bank * 0x1000 + @as(usize, address & 0x0fff); - } - } -} - -pub fn ppuMapWrite( - _: *const Mmc1, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Mmc1, -) Mirroring { - return switch (self.control & 0x03) { - 0 => .single_screen_lower, - 1 => .single_screen_upper, - 2 => .vertical, - 3 => .horizontal, - else => unreachable, - }; -} - -pub fn irqAsserted(_: *const Mmc1) bool { - return false; -} diff --git a/src/system/nes/mapper/mmc2.zig b/src/system/nes/mapper/mmc2.zig deleted file mode 100644 index ec04044..0000000 --- a/src/system/nes/mapper/mmc2.zig +++ /dev/null @@ -1,104 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mmc2 = @This(); - -prg_bank: u8 = 0, -chr_0fd: u8 = 0, -chr_0fe: u8 = 0, -chr_1fd: u8 = 0, -chr_1fe: u8 = 0, - -latch0: u1 = 0, // 0 = 0FD, 1 = 0FE -latch1: u1 = 0, // 0 = 1FD, 1 = 1FE - -mirroring_mode: Mirroring = .vertical, - -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, -) Mmc2 { - return .{ - .prg_bank_count = prg_size / 0x2000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 1, - }; -} - -pub fn cpuMapRead( - self: *const Mmc2, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - if (address < 0xA000) { - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - return bank * 0x2000 + @as(usize, address - 0x8000); - } else { - const offset = @as(usize, address - 0xA000); - const fixed_start = if (self.prg_bank_count >= 3) (self.prg_bank_count - 3) * 0x2000 else 0; - return fixed_start + offset; - } -} - -pub fn cpuWrite( - self: *Mmc2, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - switch (address & 0xF000) { - 0xA000 => self.prg_bank = value & 0x0F, - 0xB000 => self.chr_0fd = value & 0x1F, - 0xC000 => self.chr_0fe = value & 0x1F, - 0xD000 => self.chr_1fd = value & 0x1F, - 0xE000 => self.chr_1fe = value & 0x1F, - 0xF000 => self.mirroring_mode = if ((value & 1) == 0) .vertical else .horizontal, - else => {}, - } -} - -pub fn ppuMapRead( - self: *Mmc2, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - var bank: usize = 0; - if (address < 0x1000) { - const selected_bank = if (self.latch0 == 0) self.chr_0fd else self.chr_0fe; - bank = @as(usize, selected_bank) % self.chr_bank_count; - } else { - const selected_bank = if (self.latch1 == 0) self.chr_1fd else self.chr_1fe; - bank = @as(usize, selected_bank) % self.chr_bank_count; - } - - // Check PPU tile latches - if (address == 0x0FD0) self.latch0 = 0; - if (address == 0x0FE0) self.latch0 = 1; - if (address == 0x1FD0) self.latch1 = 0; - if (address == 0x1FE0) self.latch1 = 1; - - return bank * 0x1000 + @as(usize, address & 0x0FFF); -} - -pub fn ppuMapWrite( - _: *const Mmc2, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Mmc2, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Mmc2) bool { - return false; -} diff --git a/src/system/nes/mapper/mmc3.zig b/src/system/nes/mapper/mmc3.zig deleted file mode 100644 index 4c88313..0000000 --- a/src/system/nes/mapper/mmc3.zig +++ /dev/null @@ -1,161 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mmc3 = @This(); - -bank_select: u8 = 0, -bank_registers: [8]u8 = [_]u8{0} ** 8, - -mirroring_mode: Mirroring = .horizontal, - -irq_latch: u8 = 0, -irq_counter: u8 = 0, -irq_reload: bool = false, -irq_enabled: bool = false, -irq_pending: bool = false, - -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, -) Mmc3 { - return .{ - .prg_bank_count = prg_size / 0x2000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 8, - }; -} - -pub fn cpuMapRead( - self: *const Mmc3, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address & 0x1fff); - const prg_mode = (self.bank_select & 0x40) != 0; - - const last_bank = self.prg_bank_count - 1; - const second_last_bank = self.prg_bank_count - 2; - - const r6 = @as(usize, self.bank_registers[6] & 0x3f) % self.prg_bank_count; - const r7 = @as(usize, self.bank_registers[7] & 0x3f) % self.prg_bank_count; - - switch ((address - 0x8000) / 0x2000) { - 0 => { // $8000-$9FFF - const bank = if (!prg_mode) r6 else second_last_bank; - return bank * 0x2000 + offset; - }, - 1 => { // $A000-$BFFF - return r7 * 0x2000 + offset; - }, - 2 => { // $C000-$DFFF - const bank = if (!prg_mode) second_last_bank else r6; - return bank * 0x2000 + offset; - }, - 3 => { // $E000-$FFFF - return last_bank * 0x2000 + offset; - }, - else => unreachable, - } -} - -pub fn cpuWrite( - self: *Mmc3, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - switch (address & 0xe001) { - 0x8000 => self.bank_select = value, - 0x8001 => { - const reg = self.bank_select & 0x07; - self.bank_registers[reg] = value; - }, - 0xa000 => { - self.mirroring_mode = if ((value & 1) != 0) .horizontal else .vertical; - }, - 0xa001 => {}, // PRG RAM protect - 0xc000 => self.irq_latch = value, - 0xc001 => self.irq_reload = true, - 0xe000 => { - self.irq_enabled = false; - self.irq_pending = false; - }, - 0xe001 => self.irq_enabled = true, - else => {}, - } -} - -pub fn ppuMapRead( - self: *const Mmc3, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const chr_mode = (self.bank_select & 0x80) != 0; - const offset = @as(usize, address & 0x03ff); - - var bank: usize = 0; - const addr_k = address / 0x0400; - - if (!chr_mode) { - switch (addr_k) { - 0, 1 => bank = (@as(usize, self.bank_registers[0] & 0xfe) + (addr_k & 1)), - 2, 3 => bank = (@as(usize, self.bank_registers[1] & 0xfe) + (addr_k & 1)), - 4 => bank = self.bank_registers[2], - 5 => bank = self.bank_registers[3], - 6 => bank = self.bank_registers[4], - 7 => bank = self.bank_registers[5], - else => unreachable, - } - } else { - switch (addr_k) { - 0 => bank = self.bank_registers[2], - 1 => bank = self.bank_registers[3], - 2 => bank = self.bank_registers[4], - 3 => bank = self.bank_registers[5], - 4, 5 => bank = (@as(usize, self.bank_registers[0] & 0xfe) + (addr_k & 1)), - 6, 7 => bank = (@as(usize, self.bank_registers[1] & 0xfe) + (addr_k & 1)), - else => unreachable, - } - } - - bank = bank % self.chr_bank_count; - return bank * 0x0400 + offset; -} - -pub fn ppuMapWrite( - _: *const Mmc3, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn handleScanline(self: *Mmc3) void { - if (self.irq_counter == 0 or self.irq_reload) { - self.irq_counter = self.irq_latch; - self.irq_reload = false; - } else { - self.irq_counter -= 1; - } - - if (self.irq_counter == 0 and self.irq_enabled) { - self.irq_pending = true; - } -} - -pub fn mirroring( - self: *const Mmc3, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted( - self: *const Mmc3, -) bool { - return self.irq_pending; -} diff --git a/src/system/nes/mapper/mmc4.zig b/src/system/nes/mapper/mmc4.zig deleted file mode 100644 index f110b44..0000000 --- a/src/system/nes/mapper/mmc4.zig +++ /dev/null @@ -1,117 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mmc4 = @This(); - -prg_bank: u8 = 0, - -chr_banks_fd: [2]u8 = [_]u8{ 0, 0 }, -chr_banks_fe: [2]u8 = [_]u8{ 0, 0 }, - -latch: [2]u8 = [_]u8{ 0xfe, 0xfe }, - -mirroring_mode: Mirroring = .vertical, -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, -) Mmc4 { - return .{ - .prg_bank_count = prg_size / 0x4000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 2, - }; -} - -pub fn cpuMapRead( - self: *const Mmc4, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address & 0x3fff); - const last_bank = self.prg_bank_count - 1; - - if (address < 0xc000) { - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - return bank * 0x4000 + offset; - } else { - return last_bank * 0x4000 + offset; - } -} - -pub fn cpuWrite( - self: *Mmc4, - address: u16, - value: u8, -) void { - if (address < 0xa000) return; - - switch (address & 0xf000) { - 0xa000 => self.prg_bank = value & 0x0f, - 0xb000 => self.chr_banks_fd[0] = value & 0x1f, - 0xc000 => self.chr_banks_fe[0] = value & 0x1f, - 0xd000 => self.chr_banks_fd[1] = value & 0x1f, - 0xe000 => self.chr_banks_fe[1] = value & 0x1f, - 0xf000 => self.mirroring_mode = if ((value & 1) != 0) .horizontal else .vertical, - else => {}, - } -} - -pub fn ppuMapRead( - self: *Mmc4, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const slot = address / 0x1000; - const offset = @as(usize, address & 0x0fff); - - const bank_raw = if (self.latch[slot] == 0xfd) - self.chr_banks_fd[slot] - else - self.chr_banks_fe[slot]; - - const bank = @as(usize, bank_raw) % self.chr_bank_count; - const result = bank * 0x1000 + offset; - - // Check PPU tile latch triggers ($0FD0-$0FDF, $0FE0-$0FEF, $1FD0-$1FDF, $1FE0-$1FEF) - if (address == 0x0fd0 or address == 0x0fe0) { - self.latch[0] = @truncate(address >> 4); - } else if (address == 0x1fd0 or address == 0x1fe0) { - self.latch[1] = @truncate(address >> 4); - } - - return result; -} - -pub fn ppuMapWrite( - self: *const Mmc4, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const slot = address / 0x1000; - const offset = @as(usize, address & 0x0fff); - - const bank_raw = if (self.latch[slot] == 0xfd) - self.chr_banks_fd[slot] - else - self.chr_banks_fe[slot]; - - const bank = @as(usize, bank_raw) % self.chr_bank_count; - return bank * 0x1000 + offset; -} - -pub fn mirroring( - self: *const Mmc4, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted( - _: *const Mmc4, -) bool { - return false; -} diff --git a/src/system/nes/mapper/mmc5.zig b/src/system/nes/mapper/mmc5.zig deleted file mode 100644 index 3da5118..0000000 --- a/src/system/nes/mapper/mmc5.zig +++ /dev/null @@ -1,171 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Mmc5 = @This(); - -prg_mode: u2 = 3, -chr_mode: u2 = 0, - -prg_banks: [4]u8 = [_]u8{ 0, 0, 1, 2 }, // $6000, $8000, $A000, $C000 -chr_banks: [12]u16 = [_]u16{0} ** 12, - -mult_a: u8 = 0, -mult_b: u8 = 0, - -irq_scanline: u8 = 0, -irq_enabled: bool = false, -irq_assert: bool = false, -irq_in_frame: bool = false, -current_scanline: u8 = 0, - -mirroring_mode: Mirroring = .vertical, -ex_ram: [1024]u8 = [_]u8{0} ** 1024, - -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, -) Mmc5 { - return .{ - .prg_bank_count = prg_size / 0x2000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1, - }; -} - -pub fn cpuMapRead( - self: *const Mmc5, - address: u16, -) ?usize { - if (address < 0x5c00) return null; - - if (address >= 0x5c00 and address <= 0x5fff) { - return 0x80000000 | @as(usize, address - 0x5c00); - } - - if (address < 0x8000) { - const bank = @as(usize, self.prg_banks[0] & 0x7f) % self.prg_bank_count; - return bank * 0x2000 + (address & 0x1fff); - } - - const last_bank = self.prg_bank_count - 1; - const offset = @as(usize, address & 0x1fff); - - switch ((address - 0x8000) / 0x2000) { - 0 => return (@as(usize, self.prg_banks[1] & 0x7f) % self.prg_bank_count) * 0x2000 + offset, - 1 => return (@as(usize, self.prg_banks[2] & 0x7f) % self.prg_bank_count) * 0x2000 + offset, - 2 => return (@as(usize, self.prg_banks[3] & 0x7f) % self.prg_bank_count) * 0x2000 + offset, - 3 => return last_bank * 0x2000 + offset, - else => unreachable, - } -} - -pub fn cpuWrite( - self: *Mmc5, - address: u16, - value: u8, -) void { - if (address >= 0x5c00 and address <= 0x5fff) { - self.ex_ram[address - 0x5c00] = value; - return; - } - - switch (address) { - 0x5100 => self.prg_mode = @truncate(value & 3), - 0x5101 => self.chr_mode = @truncate(value & 3), - 0x5105 => { - switch (@as(u2, @truncate(value & 3))) { - 0 => self.mirroring_mode = .vertical, - 1 => self.mirroring_mode = .horizontal, - 2 => self.mirroring_mode = .single_screen_lower, - 3 => self.mirroring_mode = .single_screen_upper, - } - }, - 0x5114 => self.prg_banks[0] = value, - 0x5115 => self.prg_banks[1] = value, - 0x5116 => self.prg_banks[2] = value, - 0x5117 => self.prg_banks[3] = value, - 0x5120...0x512b => |addr| { - const idx = addr - 0x5120; - self.chr_banks[idx] = value; - }, - 0x5203 => self.irq_scanline = value, - 0x5204 => { - self.irq_enabled = (value & 0x80) != 0; - self.irq_assert = false; - }, - 0x5205 => self.mult_a = value, - 0x5206 => self.mult_b = value, - else => {}, - } -} - -pub fn readRegister( - self: *Mmc5, - address: u16, -) ?u8 { - switch (address) { - 0x5204 => { - const res: u8 = (if (self.irq_assert) @as(u8, 0x80) else 0) | (if (self.irq_in_frame) @as(u8, 0x40) else 0); - self.irq_assert = false; - return res; - }, - 0x5205 => { - const prod = @as(u16, self.mult_a) * @as(u16, self.mult_b); - return @truncate(prod); - }, - 0x5206 => { - const prod = @as(u16, self.mult_a) * @as(u16, self.mult_b); - return @truncate(prod >> 8); - }, - else => return null, - } -} - -pub fn ppuMapRead( - self: *const Mmc5, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const slot = address / 0x0400; - const offset = @as(usize, address & 0x03ff); - const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count; - - return bank * 0x0400 + offset; -} - -pub fn ppuMapWrite( - self: *const Mmc5, - address: u16, -) ?usize { - return self.ppuMapRead(address); -} - -pub fn handleScanline( - self: *Mmc5, -) void { - self.current_scanline += 1; - if (self.current_scanline == self.irq_scanline) { - if (self.irq_enabled) { - self.irq_assert = true; - } - } - if (self.current_scanline >= 240) { - self.current_scanline = 0; - self.irq_in_frame = false; - } -} - -pub fn mirroring( - self: *const Mmc5, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted( - self: *const Mmc5, -) bool { - return self.irq_assert; -} diff --git a/src/system/nes/mapper/nina03.zig b/src/system/nes/mapper/nina03.zig deleted file mode 100644 index 0ed3086..0000000 --- a/src/system/nes/mapper/nina03.zig +++ /dev/null @@ -1,73 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Nina03 = @This(); - -prg_bank: u8 = 0, -chr_bank: u8 = 0, - -prg_bank_count: usize, -chr_bank_count: usize, -mirroring_mode: Mirroring, - -pub fn init( - prg_size: usize, - chr_size: usize, - _mirroring: Mirroring, -) Nina03 { - return .{ - .prg_bank_count = prg_size / 0x8000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const Nina03, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address - 0x8000); - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - return bank * 0x8000 + offset; -} - -pub fn cpuWrite( - self: *Nina03, - address: u16, - value: u8, -) void { - if (address >= 0x4100 and address <= 0x5FFF) { - self.prg_bank = @truncate((value >> 3) & 1); - self.chr_bank = value & 0x07; - } -} - -pub fn ppuMapRead( - self: *const Nina03, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const bank = @as(usize, self.chr_bank) % self.chr_bank_count; - return bank * 0x2000 + @as(usize, address); -} - -pub fn ppuMapWrite( - _: *const Nina03, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Nina03, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Nina03) bool { - return false; -} diff --git a/src/system/nes/mapper/nrom.zig b/src/system/nes/mapper/nrom.zig deleted file mode 100644 index cac5f5e..0000000 --- a/src/system/nes/mapper/nrom.zig +++ /dev/null @@ -1,71 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Nrom = @This(); - -prg_16k: bool, -mirroring_mode: Mirroring, - -pub fn init( - prg_size: usize, - _mirroring: Mirroring, -) Nrom { - return .{ - .prg_16k = prg_size == 0x4000, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const Nrom, - address: u16, -) ?usize { - if (address < 0x8000) - return null; - - const offset = - @as(usize, address) - 0x8000; - - return if (self.prg_16k) - offset & 0x3fff - else - offset; -} - -pub fn cpuWrite( - _: *Nrom, - _: u16, - _: u8, -) void { - // NROM has no mapper registers. -} - -pub fn ppuMapRead( - _: *const Nrom, - address: u16, -) ?usize { - if (address >= 0x2000) - return null; - - return address; -} - -pub fn ppuMapWrite( - _: *const Nrom, - address: u16, -) ?usize { - if (address >= 0x2000) - return null; - - return address; -} - -pub fn mirroring( - self: *const Nrom, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Nrom) bool { - return false; -} diff --git a/src/system/nes/mapper/root.zig b/src/system/nes/mapper/root.zig deleted file mode 100644 index 1f8baab..0000000 --- a/src/system/nes/mapper/root.zig +++ /dev/null @@ -1,147 +0,0 @@ -pub const Nrom = @import("nrom.zig"); -pub const Uxrom = @import("uxrom.zig"); -pub const Mmc1 = @import("mmc1.zig"); -pub const Cnrom = @import("cnrom.zig"); -pub const Mmc3 = @import("mmc3.zig"); -pub const Mmc4 = @import("mmc4.zig"); -pub const Mmc5 = @import("mmc5.zig"); -pub const Axrom = @import("axrom.zig"); -pub const Gxrom = @import("gxrom.zig"); -pub const Mmc2 = @import("mmc2.zig"); -pub const ColorDreams = @import("colordreams.zig"); -pub const Camerica = @import("camerica.zig"); -pub const Mapper13 = @import("mapper13.zig"); -pub const Mapper34 = @import("mapper34.zig"); -pub const Mapper65 = @import("mapper65.zig"); -pub const Mapper68 = @import("mapper68.zig"); -pub const Mapper69 = @import("mapper69.zig"); -pub const Nina03 = @import("nina03.zig"); -pub const Mapper76 = @import("mapper76.zig"); -pub const Irem78 = @import("irem78.zig"); -pub const Mapper87 = @import("mapper87.zig"); -pub const Mapper88 = @import("mapper88.zig"); -pub const Mapper89 = @import("mapper89.zig"); -pub const Mapper93 = @import("mapper93.zig"); -pub const Mapper94 = @import("mapper94.zig"); -pub const Mapper118 = @import("mapper118.zig"); -pub const Mapper119 = @import("mapper119.zig"); -pub const Mapper140 = @import("mapper140.zig"); -pub const Mapper180 = @import("mapper180.zig"); -pub const Vrc1 = @import("vrc1.zig"); -pub const Vrc2 = @import("vrc2.zig"); -pub const Mapper232 = @import("mapper232.zig"); -pub const Vrc3 = @import("vrc3.zig"); -pub const Vrc4 = @import("vrc4.zig"); -pub const Vrc6 = @import("vrc6.zig"); -pub const Vrc7 = @import("vrc7.zig"); -pub const Mapper210 = @import("mapper210.zig"); -pub const Mapper206 = @import("mapper206.zig"); - -const types = @import("types.zig"); -pub const Mirroring = types.Mirroring; - -pub const Mapper = union(enum) { - nrom: Nrom, - uxrom: Uxrom, - mmc1: Mmc1, - cnrom: Cnrom, - mmc3: Mmc3, - mmc4: Mmc4, - mmc5: Mmc5, - axrom: Axrom, - gxrom: Gxrom, - mmc2: Mmc2, - color_dreams: ColorDreams, - camerica: Camerica, - mapper13: Mapper13, - mapper34: Mapper34, - mapper65: Mapper65, - mapper68: Mapper68, - mapper69: Mapper69, - nina03: Nina03, - mapper76: Mapper76, - irem78: Irem78, - mapper87: Mapper87, - mapper88: Mapper88, - mapper89: Mapper89, - mapper93: Mapper93, - mapper94: Mapper94, - mapper118: Mapper118, - mapper119: Mapper119, - mapper140: Mapper140, - mapper180: Mapper180, - vrc1: Vrc1, - vrc2: Vrc2, - mapper232: Mapper232, - vrc3: Vrc3, - vrc4: Vrc4, - vrc6: Vrc6, - vrc7: Vrc7, - mapper210: Mapper210, - mapper206: Mapper206, - - pub fn cpuMapRead( - self: *const Mapper, - address: u16, - ) ?usize { - return switch (self.*) { - inline else => |*mapper| mapper.cpuMapRead(address), - }; - } - - pub fn cpuWrite( - self: *Mapper, - address: u16, - value: u8, - ) void { - switch (self.*) { - inline else => |*mapper| mapper.cpuWrite(address, value), - } - } - - pub fn ppuMapRead( - self: *Mapper, - address: u16, - ) ?usize { - return switch (self.*) { - inline else => |*mapper| mapper.ppuMapRead(address), - }; - } - - pub fn ppuMapWrite( - self: *const Mapper, - address: u16, - ) ?usize { - return switch (self.*) { - inline else => |*mapper| mapper.ppuMapWrite(address), - }; - } - - pub fn mirroring( - self: *const Mapper, - ) Mirroring { - return switch (self.*) { - inline else => |*mapper| mapper.mirroring(), - }; - } - - pub fn irqAsserted( - self: *const Mapper, - ) bool { - return switch (self.*) { - inline else => |*mapper| mapper.irqAsserted(), - }; - } - - pub fn handleScanline( - self: *Mapper, - ) void { - switch (self.*) { - .mmc3 => |*mmc3| mmc3.handleScanline(), - .mmc5 => |*mmc5| mmc5.handleScanline(), - .mapper118 => |*m118| m118.handleScanline(), - .mapper119 => |*m119| m119.handleScanline(), - else => {}, - } - } -}; diff --git a/src/system/nes/mapper/types.zig b/src/system/nes/mapper/types.zig deleted file mode 100644 index b22074a..0000000 --- a/src/system/nes/mapper/types.zig +++ /dev/null @@ -1,9 +0,0 @@ -pub const Mirroring = enum { - horizontal, - vertical, - four_screen, - - // Needed by later mappers. - single_screen_lower, - single_screen_upper, -}; diff --git a/src/system/nes/mapper/uxrom.zig b/src/system/nes/mapper/uxrom.zig deleted file mode 100644 index 75c76fe..0000000 --- a/src/system/nes/mapper/uxrom.zig +++ /dev/null @@ -1,73 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Uxrom = @This(); - -prg_bank_select: u8 = 0, -prg_bank_count: usize, -mirroring_mode: Mirroring, - -pub fn init( - prg_size: usize, - _mirroring: Mirroring, -) Uxrom { - return .{ - .prg_bank_count = prg_size / 0x4000, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const Uxrom, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address & 0x3fff); - - if (address < 0xc000) { - // Switchable 16 KB bank - const bank = @as(usize, self.prg_bank_select) % self.prg_bank_count; - return bank * 0x4000 + offset; - } else { - // Fixed to last 16 KB bank - const last_bank = self.prg_bank_count - 1; - return last_bank * 0x4000 + offset; - } -} - -pub fn cpuWrite( - self: *Uxrom, - address: u16, - value: u8, -) void { - if (address >= 0x8000) { - self.prg_bank_select = value; - } -} - -pub fn ppuMapRead( - _: *const Uxrom, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn ppuMapWrite( - _: *const Uxrom, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Uxrom, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Uxrom) bool { - return false; -} diff --git a/src/system/nes/mapper/vrc1.zig b/src/system/nes/mapper/vrc1.zig deleted file mode 100644 index 8ff8371..0000000 --- a/src/system/nes/mapper/vrc1.zig +++ /dev/null @@ -1,106 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Vrc1 = @This(); - -prg_bank0: u8 = 0, -prg_bank1: u8 = 0, -prg_bank2: u8 = 0, - -chr_bank0: u8 = 0, -chr_bank1: u8 = 0, - -mirroring_mode: Mirroring = .vertical, - -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, -) Vrc1 { - return .{ - .prg_bank_count = prg_size / 0x2000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 1, - }; -} - -pub fn cpuMapRead( - self: *const Vrc1, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - if (address < 0xA000) { - const offset = @as(usize, address - 0x8000); - const bank = @as(usize, self.prg_bank0) % self.prg_bank_count; - return bank * 0x2000 + offset; - } else if (address < 0xC000) { - const offset = @as(usize, address - 0xA000); - const bank = @as(usize, self.prg_bank1) % self.prg_bank_count; - return bank * 0x2000 + offset; - } else if (address < 0xE000) { - const offset = @as(usize, address - 0xC000); - const bank = @as(usize, self.prg_bank2) % self.prg_bank_count; - return bank * 0x2000 + offset; - } else { - const offset = @as(usize, address - 0xE000); - const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0; - return fixed_bank * 0x2000 + offset; - } -} - -pub fn cpuWrite( - self: *Vrc1, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - switch (address & 0xF000) { - 0x8000 => self.prg_bank0 = value & 0x0F, - 0x9000 => { - self.mirroring_mode = if ((value & 1) != 0) .horizontal else .vertical; - self.chr_bank0 = (self.chr_bank0 & 0x0F) | ((value & 0x02) << 3); - self.chr_bank1 = (self.chr_bank1 & 0x0F) | ((value & 0x04) << 2); - }, - 0xA000 => self.prg_bank1 = value & 0x0F, - 0xC000 => self.prg_bank2 = value & 0x0F, - 0xE000 => self.chr_bank0 = (self.chr_bank0 & 0x10) | (value & 0x0F), - 0xF000 => self.chr_bank1 = (self.chr_bank1 & 0x10) | (value & 0x0F), - else => {}, - } -} - -pub fn ppuMapRead( - self: *const Vrc1, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - if (address < 0x1000) { - const bank = @as(usize, self.chr_bank0) % self.chr_bank_count; - return bank * 0x1000 + @as(usize, address); - } else { - const bank = @as(usize, self.chr_bank1) % self.chr_bank_count; - return bank * 0x1000 + @as(usize, address - 0x1000); - } -} - -pub fn ppuMapWrite( - _: *const Vrc1, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Vrc1, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(_: *const Vrc1) bool { - return false; -} diff --git a/src/system/nes/mapper/vrc2.zig b/src/system/nes/mapper/vrc2.zig deleted file mode 100644 index 873b05b..0000000 --- a/src/system/nes/mapper/vrc2.zig +++ /dev/null @@ -1,127 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Vrc2 = @This(); - -prg_banks: [2]u8 = [_]u8{ 0, 1 }, -chr_banks_lo: [8]u4 = [_]u4{0} ** 8, -chr_banks_hi: [8]u5 = [_]u5{0} ** 8, - -mirroring_mode: Mirroring = .vertical, - -mapper_id: u8, -shift_a: u3, -shift_b: u3, - -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, - mapper_id: u8, -) Vrc2 { - const shift_a: u3 = if (mapper_id == 22) 1 else 0; - const shift_b: u3 = if (mapper_id == 22) 0 else 1; - - return .{ - .mapper_id = mapper_id, - .prg_bank_count = prg_size / 0x2000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1, - .shift_a = shift_a, - .shift_b = shift_b, - }; -} - -pub fn cpuMapRead( - self: *const Vrc2, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address & 0x1fff); - const last_bank = self.prg_bank_count - 1; - const second_last_bank = self.prg_bank_count - 2; - - const r0 = @as(usize, self.prg_banks[0] & 0x1f) % self.prg_bank_count; - const r1 = @as(usize, self.prg_banks[1] & 0x1f) % self.prg_bank_count; - - switch ((address - 0x8000) / 0x2000) { - 0 => return r0 * 0x2000 + offset, - 1 => return r1 * 0x2000 + offset, - 2 => return second_last_bank * 0x2000 + offset, - 3 => return last_bank * 0x2000 + offset, - else => unreachable, - } -} - -pub fn cpuWrite( - self: *Vrc2, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - const reg_addr = address & 0xf000; - const a = @as(u1, @truncate((address >> self.shift_a) & 1)); - const b = @as(u1, @truncate((address >> self.shift_b) & 1)); - const line = (@as(u2, b) << 1) | a; - - switch (reg_addr) { - 0x8000 => self.prg_banks[0] = value & 0x1f, - 0x9000 => { - self.mirroring_mode = if ((value & 0x01) != 0) .horizontal else .vertical; - }, - 0xa000 => self.prg_banks[1] = value & 0x1f, - 0xb000, 0xc000, 0xd000, 0xe000 => { - const slot_base = ((reg_addr - 0xb000) / 0x1000) * 2; - const is_hi = (line & 2) != 0; - const is_odd = (line & 1) != 0; - const slot = slot_base + (if (is_odd) @as(usize, 1) else 0); - - if (!is_hi) { - self.chr_banks_lo[slot] = @truncate(value & 0x0f); - } else { - self.chr_banks_hi[slot] = @truncate(value & 0x1f); - } - }, - else => {}, - } -} - -pub fn ppuMapRead( - self: *const Vrc2, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const slot = address / 0x0400; - const offset = @as(usize, address & 0x03ff); - - var bank_val = (@as(usize, self.chr_banks_hi[slot]) << 4) | @as(usize, self.chr_banks_lo[slot]); - if (self.mapper_id == 22) { - bank_val >>= 1; - } - const bank = bank_val % self.chr_bank_count; - - return bank * 0x0400 + offset; -} - -pub fn ppuMapWrite( - self: *const Vrc2, - address: u16, -) ?usize { - return self.ppuMapRead(address); -} - -pub fn mirroring( - self: *const Vrc2, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted( - _: *const Vrc2, -) bool { - return false; -} diff --git a/src/system/nes/mapper/vrc3.zig b/src/system/nes/mapper/vrc3.zig deleted file mode 100644 index 718db13..0000000 --- a/src/system/nes/mapper/vrc3.zig +++ /dev/null @@ -1,118 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Vrc3 = @This(); - -prg_bank: u8 = 0, -prg_bank_count: usize, -mirroring_mode: Mirroring, - -irq_latch: u16 = 0, -irq_counter: u16 = 0, -irq_enabled: bool = false, -irq_mode_8bit: bool = false, -irq_asserted_flag: bool = false, - -pub fn init( - prg_size: usize, - _mirroring: Mirroring, -) Vrc3 { - return .{ - .prg_bank_count = prg_size / 0x4000, - .mirroring_mode = _mirroring, - }; -} - -pub fn cpuMapRead( - self: *const Vrc3, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - if (address < 0xC000) { - const offset = @as(usize, address - 0x8000); - const bank = @as(usize, self.prg_bank) % self.prg_bank_count; - return bank * 0x4000 + offset; - } else { - const offset = @as(usize, address - 0xC000); - const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0; - return fixed_bank * 0x4000 + offset; - } -} - -pub fn cpuWrite( - self: *Vrc3, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - switch (address & 0xF000) { - 0x8000 => self.irq_latch = (self.irq_latch & 0xFFF0) | (value & 0x0F), - 0x9000 => self.irq_latch = (self.irq_latch & 0xFF0F) | ((@as(u16, value) & 0x0F) << 4), - 0xA000 => self.irq_latch = (self.irq_latch & 0xF0FF) | ((@as(u16, value) & 0x0F) << 8), - 0xB000 => self.irq_latch = (self.irq_latch & 0x0FFF) | ((@as(u16, value) & 0x0F) << 12), - 0xC000 => { - self.irq_enabled = (value & 0x02) != 0; - self.irq_mode_8bit = (value & 0x04) != 0; - self.irq_asserted_flag = false; - if (self.irq_enabled) { - self.irq_counter = self.irq_latch; - } - }, - 0xD000 => { - self.irq_asserted_flag = false; - }, - 0xF000 => { - self.prg_bank = value & 0x07; - }, - else => {}, - } -} - -pub fn tickCycle(self: *Vrc3) void { - if (self.irq_enabled) { - if (self.irq_mode_8bit) { - const low = @as(u8, @truncate(self.irq_counter)); - if (low == 0xFF) { - self.irq_counter = (self.irq_counter & 0xFF00) | @as(u16, @as(u8, @truncate(self.irq_latch))); - self.irq_asserted_flag = true; - } else { - self.irq_counter += 1; - } - } else { - if (self.irq_counter == 0xFFFF) { - self.irq_counter = self.irq_latch; - self.irq_asserted_flag = true; - } else { - self.irq_counter += 1; - } - } - } -} - -pub fn ppuMapRead( - _: *const Vrc3, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn ppuMapWrite( - _: *const Vrc3, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - return address; -} - -pub fn mirroring( - self: *const Vrc3, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted(self: *const Vrc3) bool { - return self.irq_asserted_flag; -} diff --git a/src/system/nes/mapper/vrc4.zig b/src/system/nes/mapper/vrc4.zig deleted file mode 100644 index 838a325..0000000 --- a/src/system/nes/mapper/vrc4.zig +++ /dev/null @@ -1,179 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Vrc4 = @This(); - -prg_banks: [2]u8 = [_]u8{ 0, 1 }, -chr_banks_lo: [8]u4 = [_]u4{0} ** 8, -chr_banks_hi: [8]u5 = [_]u5{0} ** 8, - -prg_mode: u1 = 0, -mirroring_mode: Mirroring = .vertical, - -irq_reload: u8 = 0, -irq_counter: u8 = 0, -irq_enabled: bool = false, -irq_enable_on_ack: bool = false, -irq_mode: u1 = 0, // 0 = cycle, 1 = scanline -irq_prescaler: i16 = 341, -irq_assert: bool = false, - -shift_a: u3, -shift_b: u3, - -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, - mapper_id: u8, -) Vrc4 { - const shift_a: u3 = if (mapper_id == 21) 1 else 0; - const shift_b: u3 = if (mapper_id == 21) 2 else 2; - - return .{ - .prg_bank_count = prg_size / 0x2000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1, - .shift_a = shift_a, - .shift_b = shift_b, - }; -} - -pub fn cpuMapRead( - self: *const Vrc4, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address & 0x1fff); - const last_bank = self.prg_bank_count - 1; - const second_last_bank = self.prg_bank_count - 2; - - const r0 = @as(usize, self.prg_banks[0] & 0x1f) % self.prg_bank_count; - const r1 = @as(usize, self.prg_banks[1] & 0x1f) % self.prg_bank_count; - - const bank_idx = (address - 0x8000) / 0x2000; - var bank: usize = 0; - - if (self.prg_mode == 0) { - switch (bank_idx) { - 0 => bank = r0, - 1 => bank = r1, - 2 => bank = second_last_bank, - 3 => bank = last_bank, - else => unreachable, - } - } else { - switch (bank_idx) { - 0 => bank = second_last_bank, - 1 => bank = r1, - 2 => bank = r0, - 3 => bank = last_bank, - else => unreachable, - } - } - - return bank * 0x2000 + offset; -} - -pub fn cpuWrite( - self: *Vrc4, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - const reg_addr = address & 0xf000; - const a = @as(u1, @truncate((address >> self.shift_a) & 1)); - const b = @as(u1, @truncate((address >> self.shift_b) & 1)); - const line = (@as(u2, b) << 1) | a; - - switch (reg_addr) { - 0x8000 => { - self.prg_banks[0] = value & 0x1f; - }, - 0x9000 => { - if (line == 0) { - switch (@as(u2, @truncate(value & 3))) { - 0 => self.mirroring_mode = .vertical, - 1 => self.mirroring_mode = .horizontal, - 2 => self.mirroring_mode = .single_screen_lower, - 3 => self.mirroring_mode = .single_screen_upper, - } - } else if (line == 1 or line == 2) { - self.prg_mode = @truncate((value >> 1) & 1); - } - }, - 0xa000 => { - self.prg_banks[1] = value & 0x1f; - }, - 0xb000, 0xc000, 0xd000, 0xe000 => { - const slot_base = ((reg_addr - 0xb000) / 0x1000) * 2; - const is_hi = (line & 2) != 0; - const is_odd = (line & 1) != 0; - const slot = slot_base + (if (is_odd) @as(usize, 1) else 0); - - if (!is_hi) { - self.chr_banks_lo[slot] = @truncate(value & 0x0f); - } else { - self.chr_banks_hi[slot] = @truncate(value & 0x1f); - } - }, - 0xf000 => { - switch (line) { - 0 => self.irq_reload = (self.irq_reload & 0xf0) | (value & 0x0f), - 1 => self.irq_reload = (self.irq_reload & 0x0f) | ((value & 0x0f) << 4), - 2 => { - self.irq_mode = @truncate((value >> 2) & 1); - self.irq_enabled = (value & 0x02) != 0; - self.irq_enable_on_ack = (value & 0x01) != 0; - if (self.irq_enabled) { - self.irq_counter = self.irq_reload; - self.irq_prescaler = 341; - } - self.irq_assert = false; - }, - 3 => { - self.irq_enabled = self.irq_enable_on_ack; - self.irq_assert = false; - }, - } - }, - else => {}, - } -} - -pub fn ppuMapRead( - self: *const Vrc4, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const slot = address / 0x0400; - const offset = @as(usize, address & 0x03ff); - - const bank_val = (@as(usize, self.chr_banks_hi[slot]) << 4) | @as(usize, self.chr_banks_lo[slot]); - const bank = bank_val % self.chr_bank_count; - - return bank * 0x0400 + offset; -} - -pub fn ppuMapWrite( - self: *const Vrc4, - address: u16, -) ?usize { - return self.ppuMapRead(address); -} - -pub fn mirroring( - self: *const Vrc4, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted( - self: *const Vrc4, -) bool { - return self.irq_assert; -} diff --git a/src/system/nes/mapper/vrc6.zig b/src/system/nes/mapper/vrc6.zig deleted file mode 100644 index 6f135e0..0000000 --- a/src/system/nes/mapper/vrc6.zig +++ /dev/null @@ -1,152 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Vrc6 = @This(); - -prg_banks: [2]u8 = [_]u8{ 0, 1 }, -chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 }, - -mirroring_mode: Mirroring = .vertical, - -irq_reload: u8 = 0, -irq_counter: u8 = 0, -irq_enabled: bool = false, -irq_enable_on_ack: bool = false, -irq_mode: u1 = 0, -irq_assert: bool = false, - -shift_a: u3, -shift_b: u3, - -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, - mapper_id: u8, -) Vrc6 { - const shift_a: u3 = if (mapper_id == 26) 1 else 0; - const shift_b: u3 = if (mapper_id == 26) 0 else 1; - - return .{ - .prg_bank_count = prg_size / 0x2000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1, - .shift_a = shift_a, - .shift_b = shift_b, - }; -} - -pub fn cpuMapRead( - self: *const Vrc6, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const last_bank = self.prg_bank_count - 1; - - if (address < 0xc000) { - const offset = @as(usize, address & 0x3fff); - const bank16 = @as(usize, self.prg_banks[0] & 0x0f) % (self.prg_bank_count / 2); - return bank16 * 0x4000 + offset; - } else if (address < 0xe000) { - const offset = @as(usize, address & 0x1fff); - const bank8 = @as(usize, self.prg_banks[1] & 0x1f) % self.prg_bank_count; - return bank8 * 0x2000 + offset; - } else { - const offset = @as(usize, address & 0x1fff); - return last_bank * 0x2000 + offset; - } -} - -pub fn cpuWrite( - self: *Vrc6, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - const reg_addr = address & 0xf000; - const a = @as(u1, @truncate((address >> self.shift_a) & 1)); - const b = @as(u1, @truncate((address >> self.shift_b) & 1)); - const line = (@as(u2, b) << 1) | a; - - switch (reg_addr) { - 0x8000 => { - if (line == 0) self.prg_banks[0] = value & 0x0f; - }, - 0x9000...0xb000 => { - if (reg_addr == 0xb000 and line == 3) { - switch (@as(u2, @truncate((value >> 2) & 3))) { - 0 => self.mirroring_mode = .vertical, - 1 => self.mirroring_mode = .horizontal, - 2 => self.mirroring_mode = .single_screen_lower, - 3 => self.mirroring_mode = .single_screen_upper, - } - } - }, - 0xc000 => { - if (line == 0) self.prg_banks[1] = value & 0x1f; - }, - 0xd000 => { - const slot = @as(usize, line); - self.chr_banks[slot] = value; - }, - 0xe000 => { - const slot = 4 + @as(usize, line); - self.chr_banks[slot] = value; - }, - 0xf000 => { - switch (line) { - 0 => self.irq_reload = value, - 1 => { - self.irq_mode = @truncate((value >> 2) & 1); - self.irq_enabled = (value & 0x02) != 0; - self.irq_enable_on_ack = (value & 0x01) != 0; - if (self.irq_enabled) { - self.irq_counter = self.irq_reload; - } - self.irq_assert = false; - }, - 2 => { - self.irq_enabled = self.irq_enable_on_ack; - self.irq_assert = false; - }, - else => {}, - } - }, - else => {}, - } -} - -pub fn ppuMapRead( - self: *const Vrc6, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const slot = address / 0x0400; - const offset = @as(usize, address & 0x03ff); - const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count; - - return bank * 0x0400 + offset; -} - -pub fn ppuMapWrite( - self: *const Vrc6, - address: u16, -) ?usize { - return self.ppuMapRead(address); -} - -pub fn mirroring( - self: *const Vrc6, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted( - self: *const Vrc6, -) bool { - return self.irq_assert; -} diff --git a/src/system/nes/mapper/vrc7.zig b/src/system/nes/mapper/vrc7.zig deleted file mode 100644 index 7a08dbe..0000000 --- a/src/system/nes/mapper/vrc7.zig +++ /dev/null @@ -1,145 +0,0 @@ -const types = @import("types.zig"); -const Mirroring = types.Mirroring; - -const Vrc7 = @This(); - -prg_banks: [3]u8 = [_]u8{ 0, 1, 2 }, -chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 }, - -mirroring_mode: Mirroring = .vertical, - -irq_reload: u8 = 0, -irq_counter: u8 = 0, -irq_enabled: bool = false, -irq_enable_on_ack: bool = false, -irq_mode: u1 = 0, -irq_assert: bool = false, - -prg_bank_count: usize, -chr_bank_count: usize, - -pub fn init( - prg_size: usize, - chr_size: usize, -) Vrc7 { - return .{ - .prg_bank_count = prg_size / 0x2000, - .chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1, - }; -} - -pub fn cpuMapRead( - self: *const Vrc7, - address: u16, -) ?usize { - if (address < 0x8000) return null; - - const offset = @as(usize, address & 0x1fff); - const last_bank = self.prg_bank_count - 1; - - switch ((address - 0x8000) / 0x2000) { - 0 => return (@as(usize, self.prg_banks[0] & 0x3f) % self.prg_bank_count) * 0x2000 + offset, - 1 => return (@as(usize, self.prg_banks[1] & 0x3f) % self.prg_bank_count) * 0x2000 + offset, - 2 => return (@as(usize, self.prg_banks[2] & 0x3f) % self.prg_bank_count) * 0x2000 + offset, - 3 => return last_bank * 0x2000 + offset, - else => unreachable, - } -} - -pub fn cpuWrite( - self: *Vrc7, - address: u16, - value: u8, -) void { - if (address < 0x8000) return; - - const reg_addr = address & 0xf000; - const a0 = if ((address & 0x10) != 0 or (address & 0x08) != 0) @as(u2, 1) else 0; - const a1 = if ((address & 0x20) != 0) @as(u2, 2) else 0; - const line = a0 | a1; - - switch (reg_addr) { - 0x8000 => { - if (line == 0) self.prg_banks[0] = value & 0x3f; - if (line == 1 or line == 2) self.prg_banks[1] = value & 0x3f; - }, - 0x9000 => { - if (line == 0) self.prg_banks[2] = value & 0x3f; - if (line == 2) { - switch (@as(u2, @truncate(value & 3))) { - 0 => self.mirroring_mode = .vertical, - 1 => self.mirroring_mode = .horizontal, - 2 => self.mirroring_mode = .single_screen_lower, - 3 => self.mirroring_mode = .single_screen_upper, - } - } - }, - 0xa000 => { - if (line == 0) self.chr_banks[0] = value; - if (line == 1 or line == 2) self.chr_banks[1] = value; - }, - 0xb000 => { - if (line == 0) self.chr_banks[2] = value; - if (line == 1 or line == 2) self.chr_banks[3] = value; - }, - 0xc000 => { - if (line == 0) self.chr_banks[4] = value; - if (line == 1 or line == 2) self.chr_banks[5] = value; - }, - 0xd000 => { - if (line == 0) self.chr_banks[6] = value; - if (line == 1 or line == 2) self.chr_banks[7] = value; - }, - 0xe000 => { - if (line == 0) self.irq_reload = value; - if (line == 1 or line == 2) { - self.irq_mode = @truncate((value >> 2) & 1); - self.irq_enabled = (value & 0x02) != 0; - self.irq_enable_on_ack = (value & 0x01) != 0; - if (self.irq_enabled) { - self.irq_counter = self.irq_reload; - } - self.irq_assert = false; - } - }, - 0xf000 => { - if (line == 0) { - self.irq_enabled = self.irq_enable_on_ack; - self.irq_assert = false; - } - }, - else => {}, - } -} - -pub fn ppuMapRead( - self: *const Vrc7, - address: u16, -) ?usize { - if (address >= 0x2000) return null; - - const slot = address / 0x0400; - const offset = @as(usize, address & 0x03ff); - const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count; - - return bank * 0x0400 + offset; -} - -pub fn ppuMapWrite( - self: *const Vrc7, - address: u16, -) ?usize { - return self.ppuMapRead(address); -} - -pub fn mirroring( - self: *const Vrc7, -) Mirroring { - return self.mirroring_mode; -} - -pub fn irqAsserted( - self: *const Vrc7, -) bool { - return self.irq_assert; -} diff --git a/src/system/nes/ppu.zig b/src/system/nes/ppu.zig deleted file mode 100644 index 2cc1cb1..0000000 --- a/src/system/nes/ppu.zig +++ /dev/null @@ -1,779 +0,0 @@ -const std = @import("std"); -const Cartridge = @import("cartridge.zig"); - -const mapper = @import("mapper/root.zig"); -const Mirroring = mapper.Mirroring; - -const Ppu = @This(); - -pub const width = 256; -pub const height = 240; - -// NTSC 2C02 64-color system palette mapped directly to RGB565 format -pub const nes_ntsc_palette_rgb565: [64]u16 = .{ - 0x52AA, 0x00EE, 0x0892, 0x3011, 0x400C, 0x5806, 0x5020, 0x38C0, - 0x2140, 0x09C0, 0x0200, 0x01E0, 0x0187, 0x0000, 0x0000, 0x0000, - 0x9CB3, 0x0A78, 0x319D, 0x58FC, 0x88B6, 0xA08C, 0x9904, 0x79E0, - 0x52C0, 0x2B80, 0x0BE0, 0x03A5, 0x032F, 0x0000, 0x0000, 0x0000, - 0xEF7D, 0x4CDD, 0x7BFD, 0xB31D, 0xE2BD, 0xEACE, 0xEB4C, 0xD444, - 0xA540, 0x7620, 0x4E84, 0x3E6D, 0x3DA9, 0x3DF7, 0x0000, 0x0000, - 0xEF7D, 0xAE6D, 0xBD7D, 0xD59D, 0xED7D, 0xED7A, 0xEDB6, 0xE632, - 0xCE8F, 0xB6EF, 0xAF12, 0x9F16, 0xA6BD, 0xA514, 0x0000, 0x0000, -}; - -framebuffer: [width * height]u16 = - [_]u16{0} ** (width * height), - -/// Four-screen sized so the same structure -/// can handle that mode without allocation. -nametable: [0x1000]u8 = - [_]u8{0} ** 0x1000, - -palette: [32]u8 = - [_]u8{0} ** 32, - -oam: [256]u8 = - [_]u8{0} ** 256, - -ctrl: u8 = 0, -mask: u8 = 0, -status: u8 = 0, - -oam_addr: u8 = 0, - -vram_addr: u16 = 0, -temp_addr: u16 = 0, - -fine_x: u3 = 0, -write_latch: bool = false, - -read_buffer: u8 = 0, -io_latch: u8 = 0, - -dot: u16 = 0, -scanline: u16 = 261, - -frame_number: u64 = 0, -frame_ready: bool = false, - -// internal shift registers & tile latches for background rendering -bg_shift_pattern_lo: u16 = 0, -bg_shift_pattern_hi: u16 = 0, -bg_shift_attrib_lo: u16 = 0, -bg_shift_attrib_hi: u16 = 0, - -next_tile_id: u8 = 0, -next_tile_attrib: u8 = 0, -next_tile_lsb: u8 = 0, -next_tile_msb: u8 = 0, - -// secondary OAM scanline sprite evaluation buffer -sec_oam_pattern_lo: [8]u8 = [_]u8{0} ** 8, -sec_oam_pattern_hi: [8]u8 = [_]u8{0} ** 8, -sec_oam_attribute: [8]u8 = [_]u8{0} ** 8, -sec_oam_x: [8]u8 = [_]u8{0} ** 8, -sec_oam_is_zero: [8]bool = [_]bool{false} ** 8, -sec_oam_count: u8 = 0, - -pub fn reset( - self: *Ppu, -) void { - self.ctrl = 0; - self.mask = 0; - self.status = 0; - - self.oam_addr = 0; - - self.vram_addr = 0; - self.temp_addr = 0; - - self.fine_x = 0; - self.write_latch = false; - - self.read_buffer = 0; - self.io_latch = 0; - - self.dot = 0; - self.scanline = 261; - - self.frame_ready = false; - - self.bg_shift_pattern_lo = 0; - self.bg_shift_pattern_hi = 0; - self.bg_shift_attrib_lo = 0; - self.bg_shift_attrib_hi = 0; - self.next_tile_id = 0; - self.next_tile_attrib = 0; - self.next_tile_lsb = 0; - self.next_tile_msb = 0; - self.sec_oam_count = 0; -} - -pub fn cpuRead( - self: *Ppu, - cartridge: *Cartridge, - register: u16, -) u8 { - const reg = register & 7; - - const value: u8 = switch (reg) { - // PPUSTATUS - 2 => blk: { - const result = - (self.status & 0xe0) | - (self.io_latch & 0x1f); - - self.status &= ~@as(u8, 0x80); - self.write_latch = false; - - break :blk result; - }, - - // OAMDATA - 4 => self.oam[self.oam_addr], - - // PPUDATA - 7 => self.readData(cartridge), - - else => self.io_latch, - }; - - self.io_latch = value; - - return value; -} - -pub fn cpuWrite( - self: *Ppu, - cartridge: *Cartridge, - register: u16, - value: u8, -) void { - const reg = register & 7; - - self.io_latch = value; - - switch (reg) { - // PPUCTRL - 0 => { - self.ctrl = value; - - self.temp_addr = - (self.temp_addr & 0xf3ff) | - (@as(u16, value & 0x03) << 10); - }, - - // PPUMASK - 1 => { - self.mask = value; - }, - - // OAMADDR - 3 => { - self.oam_addr = value; - }, - - // OAMDATA - 4 => { - self.writeOamDma(value); - }, - - // PPUSCROLL - 5 => { - if (!self.write_latch) { - self.fine_x = - @truncate(value & 7); - - self.temp_addr = - (self.temp_addr & 0xffe0) | - @as(u16, value >> 3); - - self.write_latch = true; - } else { - self.temp_addr = - (self.temp_addr & 0x8c1f) | - (@as(u16, value & 0x07) << 12) | - (@as(u16, value & 0xf8) << 2); - - self.write_latch = false; - } - }, - - // PPUADDR - 6 => { - if (!self.write_latch) { - self.temp_addr = - (self.temp_addr & 0x00ff) | - (@as(u16, value & 0x3f) << 8); - - self.write_latch = true; - } else { - self.temp_addr = - (self.temp_addr & 0x7f00) | - @as(u16, value); - - self.vram_addr = - self.temp_addr; - - self.write_latch = false; - } - }, - - // PPUDATA - 7 => { - self.writeMemory( - cartridge, - self.vram_addr, - value, - ); - - self.incrementVramAddress(); - }, - - else => {}, - } -} - -pub fn writeOamDma( - self: *Ppu, - value: u8, -) void { - self.oam[self.oam_addr] = value; - self.oam_addr +%= 1; -} - -pub fn tick( - self: *Ppu, - cartridge: *Cartridge, -) void { - const rendering_enabled = (self.mask & 0x18) != 0; - const is_visible_scanline = (self.scanline < 240); - const is_prerender_scanline = (self.scanline == 261); - - // Start of VBlank. - if (self.scanline == 241 and - self.dot == 1) - { - self.status |= 0x80; - } - - // Pre-render line. - if (is_prerender_scanline and - self.dot == 1) - { - // Clear: VBlank, sprite zero hit, sprite overflow - self.status &= ~@as(u8, 0xe0); - } - - // --- Rendering Pipeline --- - if (rendering_enabled and (is_visible_scanline or is_prerender_scanline)) { - if ((self.dot >= 1 and self.dot <= 256) or (self.dot >= 321 and self.dot <= 336)) { - self.shiftBackgroundRegisters(); - - const cycle_in_tile = (self.dot - 1) % 8; - switch (cycle_in_tile) { - 0 => { - self.loadShiftRegisters(); - const nt_addr = 0x2000 | (self.vram_addr & 0x0fff); - self.next_tile_id = self.readMemory(cartridge, nt_addr); - }, - 2 => { - const attr_addr = 0x23c0 | - (self.vram_addr & 0x0c00) | - ((self.vram_addr >> 4) & 0x38) | - ((self.vram_addr >> 2) & 0x07); - const attr_byte = self.readMemory(cartridge, attr_addr); - const shift: u3 = @intCast(((self.vram_addr >> 4) & 4) | (self.vram_addr & 2)); - self.next_tile_attrib = (attr_byte >> shift) & 0x03; - }, - 4 => { - const bg_table: u16 = if ((self.ctrl & 0x10) != 0) 0x1000 else 0x0000; - const fine_y = (self.vram_addr >> 12) & 7; - const pattern_addr = bg_table + (@as(u16, self.next_tile_id) << 4) + fine_y; - self.next_tile_lsb = self.readMemory(cartridge, pattern_addr); - }, - 6 => { - const bg_table: u16 = if ((self.ctrl & 0x10) != 0) 0x1000 else 0x0000; - const fine_y = (self.vram_addr >> 12) & 7; - const pattern_addr = bg_table + (@as(u16, self.next_tile_id) << 4) + fine_y + 8; - self.next_tile_msb = self.readMemory(cartridge, pattern_addr); - }, - 7 => { - self.incrementCoarseX(); - }, - else => {}, - } - } - - if (self.dot == 256) { - self.incrementY(); - } - - if (self.dot == 257) { - self.loadShiftRegisters(); - self.copyHorizontal(); - } - - if (is_prerender_scanline and self.dot >= 280 and self.dot <= 304) { - self.copyVertical(); - } - - if (self.dot == 257 and is_visible_scanline) { - self.evaluateSprites(cartridge); - } - - if (self.dot == 260 and is_visible_scanline) { - cartridge.handleScanline(); - } - } - - // --- Pixel Output --- - if (is_visible_scanline and self.dot >= 1 and self.dot <= 256) { - self.renderPixel(cartridge); - } - - // --- Dot & Scanline Counters --- - self.dot += 1; - - if (self.dot == 341) { - self.dot = 0; - self.scanline += 1; - - if (self.scanline == 262) { - self.scanline = 0; - - self.frame_number +%= 1; - self.frame_ready = true; - } - } -} - -pub fn nmiAsserted( - self: *const Ppu, -) bool { - const nmi_enabled = - (self.ctrl & 0x80) != 0; - - const in_vblank = - (self.status & 0x80) != 0; - - return nmi_enabled and in_vblank; -} - -fn shiftBackgroundRegisters(self: *Ppu) void { - if ((self.mask & 0x08) != 0) { - self.bg_shift_pattern_lo <<= 1; - self.bg_shift_pattern_hi <<= 1; - self.bg_shift_attrib_lo <<= 1; - self.bg_shift_attrib_hi <<= 1; - } -} - -fn loadShiftRegisters(self: *Ppu) void { - self.bg_shift_pattern_lo = (self.bg_shift_pattern_lo & 0xff00) | self.next_tile_lsb; - self.bg_shift_pattern_hi = (self.bg_shift_pattern_hi & 0xff00) | self.next_tile_msb; - - const attr_lo: u16 = if ((self.next_tile_attrib & 1) != 0) 0x00ff else 0x0000; - const attr_hi: u16 = if ((self.next_tile_attrib & 2) != 0) 0x00ff else 0x0000; - self.bg_shift_attrib_lo = (self.bg_shift_attrib_lo & 0xff00) | attr_lo; - self.bg_shift_attrib_hi = (self.bg_shift_attrib_hi & 0xff00) | attr_hi; -} - -inline fn incrementCoarseX(self: *Ppu) void { - if ((self.vram_addr & 0x001f) == 31) { - self.vram_addr &= ~@as(u16, 0x001f); - self.vram_addr ^= 0x0400; - } else { - self.vram_addr += 1; - } -} - -inline fn incrementY(self: *Ppu) void { - if ((self.vram_addr & 0x7000) != 0x7000) { - self.vram_addr += 0x1000; - } else { - self.vram_addr &= ~@as(u16, 0x7000); - var y = (self.vram_addr & 0x03e0) >> 5; - if (y == 29) { - y = 0; - self.vram_addr ^= 0x0800; - } else if (y == 31) { - y = 0; - } else { - y += 1; - } - self.vram_addr = (self.vram_addr & ~@as(u16, 0x03e0)) | (y << 5); - } -} - -inline fn copyHorizontal(self: *Ppu) void { - self.vram_addr = (self.vram_addr & ~@as(u16, 0x041f)) | (self.temp_addr & 0x041f); -} - -inline fn copyVertical(self: *Ppu) void { - self.vram_addr = (self.vram_addr & ~@as(u16, 0x7be0)) | (self.temp_addr & 0x7be0); -} - -inline fn evaluateSprites( - self: *Ppu, - cartridge: *Cartridge, -) void { - self.sec_oam_count = 0; - const sprite_height: u16 = if ((self.ctrl & 0x20) != 0) 16 else 8; - - var oam_idx: usize = 0; - var i: usize = 0; - while (i < 64) : ({ - i += 1; - oam_idx += 4; - }) { - const sprite_y = @as(u16, self.oam[oam_idx]); - const diff = @as(i16, @intCast(self.scanline)) - @as(i16, @intCast(sprite_y)); - - if (diff >= 0 and diff < sprite_height) { - if (self.sec_oam_count < 8) { - const idx = self.sec_oam_count; - const tile = self.oam[oam_idx + 1]; - const attr = self.oam[oam_idx + 2]; - const x = self.oam[oam_idx + 3]; - - var row: u16 = @intCast(diff); - // Vertical flip - if ((attr & 0x80) != 0) { - row = (sprite_height - 1) - row; - } - - var pattern_addr: u16 = 0; - if (sprite_height == 8) { - const pattern_table: u16 = if ((self.ctrl & 0x08) != 0) 0x1000 else 0x0000; - pattern_addr = pattern_table + (@as(u16, tile) << 4) + row; - } else { - // 8x16 mode - const pattern_table: u16 = if ((tile & 1) != 0) 0x1000 else 0x0000; - const tile_index: u16 = tile & 0xfe; - if (row < 8) { - pattern_addr = pattern_table + (tile_index << 4) + row; - } else { - pattern_addr = pattern_table + ((tile_index + 1) << 4) + (row - 8); - } - } - - var pat_lo = self.readMemory(cartridge, pattern_addr); - var pat_hi = self.readMemory(cartridge, pattern_addr + 8); - - // Horizontal flip - if ((attr & 0x40) != 0) { - pat_lo = @bitReverse(pat_lo); - pat_hi = @bitReverse(pat_hi); - } - - self.sec_oam_pattern_lo[idx] = pat_lo; - self.sec_oam_pattern_hi[idx] = pat_hi; - self.sec_oam_attribute[idx] = attr; - self.sec_oam_x[idx] = x; - self.sec_oam_is_zero[idx] = (i == 0); - - self.sec_oam_count += 1; - } else { - // Sprite overflow - self.status |= 0x20; - break; - } - } - } -} - -fn renderPixel( - self: *Ppu, - _: *Cartridge, -) void { - const x = self.dot - 1; - - var bg_pixel: u8 = 0; - var bg_palette: u8 = 0; - - if ((self.mask & 0x08) != 0 and (x >= 8 or (self.mask & 0x02) != 0)) { - const bit_shift: u4 = 15 - @as(u4, self.fine_x); - const p1 = @as(u8, @truncate((self.bg_shift_pattern_lo >> bit_shift) & 1)); - const p2 = @as(u8, @truncate((self.bg_shift_pattern_hi >> bit_shift) & 1)); - bg_pixel = (p2 << 1) | p1; - - const a1 = @as(u8, @truncate((self.bg_shift_attrib_lo >> bit_shift) & 1)); - const a2 = @as(u8, @truncate((self.bg_shift_attrib_hi >> bit_shift) & 1)); - bg_palette = (a2 << 1) | a1; - } - - var fg_pixel: u8 = 0; - var fg_palette: u8 = 0; - var fg_priority: bool = false; - var is_sprite_zero: bool = false; - - if (self.sec_oam_count > 0 and (self.mask & 0x10) != 0 and (x >= 8 or (self.mask & 0x04) != 0)) { - for (0..self.sec_oam_count) |i| { - const spr_x = self.sec_oam_x[i]; - if (x >= spr_x and x < spr_x + 8) { - const offset: u3 = @intCast(x - spr_x); - const shift: u3 = 7 - offset; - const p1 = (self.sec_oam_pattern_lo[i] >> shift) & 1; - const p2 = (self.sec_oam_pattern_hi[i] >> shift) & 1; - const pixel = (p2 << 1) | p1; - - if (pixel != 0) { - fg_pixel = pixel; - fg_palette = 4 + (self.sec_oam_attribute[i] & 0x03); - fg_priority = (self.sec_oam_attribute[i] & 0x20) != 0; - is_sprite_zero = self.sec_oam_is_zero[i]; - break; - } - } - } - } - - var final_palette_entry: u16 = 0; - - if (bg_pixel == 0 and fg_pixel == 0) { - final_palette_entry = 0x00; - } else if (bg_pixel == 0 and fg_pixel != 0) { - final_palette_entry = @as(u16, fg_palette) * 4 + fg_pixel; - } else if (bg_pixel != 0 and fg_pixel == 0) { - final_palette_entry = @as(u16, bg_palette) * 4 + bg_pixel; - } else { - // Both bg and fg pixels present - if (is_sprite_zero and x < 255 and (self.mask & 0x18) == 0x18) { - self.status |= 0x40; // Sprite 0 hit - } - - if (fg_priority) { - final_palette_entry = @as(u16, bg_palette) * 4 + bg_pixel; - } else { - final_palette_entry = @as(u16, fg_palette) * 4 + fg_pixel; - } - } - - var color_idx = self.palette[mapPalette(0x3f00 + final_palette_entry)] & 0x3f; - if ((self.mask & 0x01) != 0) { - color_idx &= 0x30; - } - var pixel_rgb = nes_ntsc_palette_rgb565[color_idx]; - - const emphasis = (self.mask >> 5) & 7; - if (emphasis != 0) { - var r = (pixel_rgb >> 11) & 0x1F; - var g = (pixel_rgb >> 5) & 0x3F; - var b = pixel_rgb & 0x1F; - - // Red emphasis (bit 5) - if ((emphasis & 1) != 0) { - g = (g * 3) / 4; - b = (b * 3) / 4; - } - // Green emphasis (bit 6) - if ((emphasis & 2) != 0) { - r = (r * 3) / 4; - b = (b * 3) / 4; - } - // Blue emphasis (bit 7) - if ((emphasis & 4) != 0) { - r = (r * 3) / 4; - g = (g * 3) / 4; - } - - pixel_rgb = (@as(u16, r) << 11) | (@as(u16, g) << 5) | @as(u16, b); - } - - self.framebuffer[self.scanline * 256 + x] = pixel_rgb; -} - -fn incrementVramAddress( - self: *Ppu, -) void { - const increment: u16 = - if ((self.ctrl & 0x04) != 0) - 32 - else - 1; - - self.vram_addr = - (self.vram_addr +% increment) & - 0x3fff; -} - -fn readData( - self: *Ppu, - cartridge: *Cartridge, -) u8 { - const address = - self.vram_addr & 0x3fff; - - const value: u8 = - if (address < 0x3f00) blk: { - const previous = - self.read_buffer; - - self.read_buffer = - self.readMemory( - cartridge, - address, - ); - - break :blk previous; - } else blk: { - const result = - self.readMemory( - cartridge, - address, - ); - - // Palette reads aren't delayed, but - // still update the internal buffer. - self.read_buffer = - self.readMemory( - cartridge, - address - 0x1000, - ); - - break :blk result; - }; - - self.incrementVramAddress(); - - return value; -} - -inline fn readMemory( - self: *Ppu, - cartridge: *Cartridge, - address_: u16, -) u8 { - const address = - address_ & 0x3fff; - - return switch (address) { - 0x0000...0x1fff => cartridge.ppuRead(address), - - 0x2000...0x3eff => self.nametable[ - self.mapNametable( - cartridge.mirroring(), - address, - ) - ], - - 0x3f00...0x3fff => self.palette[ - mapPalette(address) - ], - - else => unreachable, - }; -} - -inline fn writeMemory( - self: *Ppu, - cartridge: *Cartridge, - address_: u16, - value: u8, -) void { - const address = - address_ & 0x3fff; - - switch (address) { - 0x0000...0x1fff => cartridge.ppuWrite( - address, - value, - ), - - 0x2000...0x3eff => self.nametable[ - self.mapNametable( - cartridge.mirroring(), - address, - ) - ] = value, - - 0x3f00...0x3fff => self.palette[ - mapPalette(address) - ] = value, - - else => unreachable, - } -} - -inline fn mapNametable( - _: *const Ppu, - mirroring: Mirroring, - address: u16, -) usize { - const relative = - (@as(usize, address) - 0x2000) & - 0x0fff; - - const table = - relative >> 10; - - const offset = - relative & 0x3ff; - - const mapped_table: usize = - switch (mirroring) { - .vertical => switch (table) { - 0, 2 => 0, - 1, 3 => 1, - else => unreachable, - }, - - .horizontal => switch (table) { - 0, 1 => 0, - 2, 3 => 1, - else => unreachable, - }, - - .single_screen_lower => 0, - .single_screen_upper => 1, - - .four_screen => table, - }; - - return mapped_table * 0x400 + - offset; -} - -inline fn mapPalette( - address: u16, -) usize { - var index: usize = - (@as(usize, address) - 0x3f00) & - 0x1f; - - // Universal background color mirrors. - switch (index) { - 0x10 => index = 0x00, - 0x14 => index = 0x04, - 0x18 => index = 0x08, - 0x1c => index = 0x0c, - else => {}, - } - - return index; -} - -test "NES PPU - Palette Mirroring & NTSC Palette LUT" { - try std.testing.expectEqual(@as(usize, 0), mapPalette(0x3F00)); - try std.testing.expectEqual(@as(usize, 0), mapPalette(0x3F10)); - try std.testing.expectEqual(@as(usize, 4), mapPalette(0x3F04)); - try std.testing.expectEqual(@as(usize, 4), mapPalette(0x3F14)); - - // Verify system palette RGB565 LUT contains non-zero color constants - try std.testing.expectEqual(@as(u16, 0x52AA), nes_ntsc_palette_rgb565[0]); - try std.testing.expectEqual(@as(u16, 0xEF7D), nes_ntsc_palette_rgb565[0x20]); -} - -test "NES PPU - Scroll Address Increments" { - var ppu = Ppu{}; - ppu.reset(); - - ppu.vram_addr = 0x001F; // Coarse X = 31 - ppu.incrementCoarseX(); - try std.testing.expectEqual(@as(u16, 0x0400), ppu.vram_addr); // Coarse X = 0, switched nametable X - - ppu.vram_addr = 0x73A0; // Coarse Y = 29, Fine Y = 7 - ppu.incrementY(); - try std.testing.expectEqual(@as(u16, 0x0800), ppu.vram_addr); // Coarse Y = 0, Fine Y = 0, switched nametable Y -} diff --git a/src/system/nes/root.zig b/src/system/nes/root.zig index 840d27a..e69de29 100644 --- a/src/system/nes/root.zig +++ b/src/system/nes/root.zig @@ -1,421 +0,0 @@ -const std = @import("std"); - -const contract = @import("contract"); -const _cpu = @import("cpu"); - -const Bus = @import("bus.zig"); -const Cartridge = @import("cartridge.zig"); - -const controller = @import("controller.zig"); -pub const Button = controller.Button; - -// Adjust this enum spelling to whatever your CPU package -// currently exports for the Ricoh 2A03. -const Cpu = _cpu.m6502.Cpu( - Bus, - .ricoh2a03, - .cycle, -); - -const Nes = @This(); -pub const spec = contract.SystemSpec{ - .name = "Nintendo Entertainment System", - - .video_outputs = &.{ - .{ - .max_width = 256, - .max_height = 240, - - .format = .rgb565, - - .aspect_ratio = .{ - .numerator = 4, - .denominator = 3, - }, - - .refresh_rate = .{ - .numerator = 60, - .denominator = 1, - }, - }, - }, - - .audio_outputs = &.{ - .{ - .sample_rate = 44_100, - .channels = 1, - .format = .i16, - }, - }, - - .input_devices = &.{ - .{ - .button_count = 8, - }, - .{ - .button_count = 8, - }, - }, - - .storage_devices = &.{ - .{ - .name = "Battery PRG RAM", - .min_size = 0, - .max_size = 8192, - .persistent = true, - .removable = false, - .writable = true, - }, - }, - - .save_state = .{ - .max_size = 32 * 1024, - }, -}; - -bus: Bus = undefined, -cpu: Cpu = undefined, - -/// Initialize in place. -/// -/// `rom` must remain valid for the lifetime of this NES -/// because Cartridge stores slices into it. -pub fn init( - self: *Nes, - rom: []const u8, -) Cartridge.Error!void { - self.bus = - try Bus.init(rom); - - self.cpu = - Cpu.init(&self.bus); - - self.reset(); -} - -pub fn reset( - self: *Nes, -) void { - self.bus.reset(); - self.cpu.reset(); -} - -pub fn step( - self: *Nes, -) ?u8 { - const cycles = self.cpu.step() orelse return null; - self.serviceOamDma(); - return cycles; -} - -pub fn runFrame( - self: *Nes, -) void { - self.bus.ppu.frame_ready = false; - self.bus.apu.sample_count = 0; - - while (!self.bus.ppu.frame_ready) { - _ = self.step() orelse - return; - } -} - -pub const conformance = @import("conformance.zig"); - -test { - _ = conformance; -} - -comptime { - contract.validateSystem(Nes); -} - -pub fn videoFrame( - self: *const Nes, - output: usize, -) contract.VideoFrame { - std.debug.assert(output == 0); - - return .{ - .data = std.mem.sliceAsBytes( - self.bus.ppu.framebuffer[0..], - ), - - .width = 256, - .height = 240, - - .pitch = 256 * @sizeOf(u16), - - .format = .rgb565, - - .frame_number = self.bus.ppu.frame_number, - }; -} - -pub fn audioBuffer( - self: *const Nes, - output: usize, -) contract.AudioBuffer { - std.debug.assert(output == 0); - - const samples = self.bus.apu.sample_buffer[0..self.bus.apu.sample_count]; - - return .{ - .data = std.mem.sliceAsBytes(samples), - .frames = self.bus.apu.sample_count, - - .sample_rate = 44_100, - .channels = 1, - .format = .i16, - }; -} - -pub fn setInput( - self: *Nes, - device: usize, - input: contract.DeviceInput, -) void { - std.debug.assert(device < 2); - - self.bus.controllers[device] - .setButtons( - @truncate(input.buttons), - ); -} - -pub fn storageView( - self: *const Nes, - slot: usize, -) contract.StorageView { - std.debug.assert(slot == 0); - return .{ - .data = &self.bus.cartridge.prg_ram, - .generation = 1, - }; -} - -pub fn loadStorage( - self: *Nes, - slot: usize, - data: []const u8, -) contract.StorageLoadError!void { - if (slot != 0) return error.InvalidSlot; - if (data.len > self.bus.cartridge.prg_ram.len) return error.InvalidSize; - @memcpy(self.bus.cartridge.prg_ram[0..data.len], data); -} - -fn serviceOamDma( - self: *Nes, -) void { - const page = - self.bus.takeOamDmaPage() orelse return; - - const odd_cycle = - (self.cpu.cycles & 1) != 0; - - // Mandatory DMA halt cycle. - self.cpu.tick(); - - // Alignment cycle when necessary. - if (odd_cycle) - self.cpu.tick(); - - const base = - @as(u16, page) << 8; - - for (0..256) |i| { - // DMA read cycle. - const value = - self.cpu.readCycle( - base | - @as(u16, @intCast(i)), - ); - - // DMA write cycle. - self.bus.ppu.writeOamDma(value); - self.cpu.tick(); - } -} - -pub fn saveState( - self: *const Nes, - buffer: []u8, -) contract.state.Error!usize { - var offset: usize = 0; - - if (buffer.len < 8) return error.BufferTooSmall; - @memcpy(buffer[offset..][0..8], "6SOZNES1"); - offset += 8; - - // CPU - if (offset + 15 > buffer.len) return error.BufferTooSmall; - std.mem.writeInt(u16, buffer[offset..][0..2], self.cpu.registers.pc, .little); - offset += 2; - buffer[offset] = self.cpu.registers.a; - offset += 1; - buffer[offset] = self.cpu.registers.x; - offset += 1; - buffer[offset] = self.cpu.registers.y; - offset += 1; - buffer[offset] = self.cpu.registers.sp; - offset += 1; - buffer[offset] = @bitCast(self.cpu.registers.status); - offset += 1; - std.mem.writeInt(u64, buffer[offset..][0..8], self.cpu.cycles, .little); - offset += 8; - - // Bus RAM & PRG RAM - if (offset + self.bus.ram.len + self.bus.cartridge.prg_ram.len > buffer.len) return error.BufferTooSmall; - @memcpy(buffer[offset..][0..self.bus.ram.len], &self.bus.ram); - offset += self.bus.ram.len; - @memcpy(buffer[offset..][0..self.bus.cartridge.prg_ram.len], &self.bus.cartridge.prg_ram); - offset += self.bus.cartridge.prg_ram.len; - - if (self.bus.cartridge.chr_is_ram) { - if (offset + self.bus.cartridge.chr_ram.len > buffer.len) return error.BufferTooSmall; - @memcpy(buffer[offset..][0..self.bus.cartridge.chr_ram.len], &self.bus.cartridge.chr_ram); - offset += self.bus.cartridge.chr_ram.len; - } - - // PPU State - const ppu_size = 1 + 1 + 1 + 1 + 2 + 2 + 1 + 1 + 2 + 2 + 4096 + 32 + 256; - if (offset + ppu_size > buffer.len) return error.BufferTooSmall; - - buffer[offset] = self.bus.ppu.ctrl; - offset += 1; - buffer[offset] = self.bus.ppu.mask; - offset += 1; - buffer[offset] = self.bus.ppu.status; - offset += 1; - buffer[offset] = self.bus.ppu.oam_addr; - offset += 1; - std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.vram_addr, .little); - offset += 2; - std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.temp_addr, .little); - offset += 2; - buffer[offset] = self.bus.ppu.fine_x; - offset += 1; - buffer[offset] = if (self.bus.ppu.write_latch) 1 else 0; - offset += 1; - std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.dot, .little); - offset += 2; - std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.scanline, .little); - offset += 2; - - @memcpy(buffer[offset..][0..4096], &self.bus.ppu.nametable); - offset += 4096; - @memcpy(buffer[offset..][0..32], &self.bus.ppu.palette); - offset += 32; - @memcpy(buffer[offset..][0..256], &self.bus.ppu.oam); - offset += 256; - - return offset; -} - -pub fn loadState( - self: *Nes, - buffer: []const u8, -) contract.state.Error!void { - var offset: usize = 0; - - if (buffer.len < 8) return error.InvalidState; - if (!std.mem.eql(u8, buffer[0..8], "6SOZNES1")) return error.InvalidState; - offset += 8; - - // CPU - if (offset + 15 > buffer.len) return error.InvalidState; - self.cpu.registers.pc = std.mem.readInt(u16, buffer[offset..][0..2], .little); - offset += 2; - self.cpu.registers.a = buffer[offset]; - offset += 1; - self.cpu.registers.x = buffer[offset]; - offset += 1; - self.cpu.registers.y = buffer[offset]; - offset += 1; - self.cpu.registers.sp = buffer[offset]; - offset += 1; - self.cpu.registers.status = @bitCast(buffer[offset]); - offset += 1; - self.cpu.cycles = std.mem.readInt(u64, buffer[offset..][0..8], .little); - offset += 8; - - // Bus RAM & PRG RAM - if (offset + self.bus.ram.len + self.bus.cartridge.prg_ram.len > buffer.len) return error.InvalidState; - @memcpy(&self.bus.ram, buffer[offset..][0..self.bus.ram.len]); - offset += self.bus.ram.len; - @memcpy(&self.bus.cartridge.prg_ram, buffer[offset..][0..self.bus.cartridge.prg_ram.len]); - offset += self.bus.cartridge.prg_ram.len; - - if (self.bus.cartridge.chr_is_ram) { - if (offset + self.bus.cartridge.chr_ram.len > buffer.len) return error.InvalidState; - @memcpy(&self.bus.cartridge.chr_ram, buffer[offset..][0..self.bus.cartridge.chr_ram.len]); - offset += self.bus.cartridge.chr_ram.len; - } - - // PPU State - const ppu_size = 1 + 1 + 1 + 1 + 2 + 2 + 1 + 1 + 2 + 2 + 4096 + 32 + 256; - if (offset + ppu_size > buffer.len) return error.InvalidState; - - self.bus.ppu.ctrl = buffer[offset]; - offset += 1; - self.bus.ppu.mask = buffer[offset]; - offset += 1; - self.bus.ppu.status = buffer[offset]; - offset += 1; - self.bus.ppu.oam_addr = buffer[offset]; - offset += 1; - self.bus.ppu.vram_addr = std.mem.readInt(u16, buffer[offset..][0..2], .little); - offset += 2; - self.bus.ppu.temp_addr = std.mem.readInt(u16, buffer[offset..][0..2], .little); - offset += 2; - self.bus.ppu.fine_x = @truncate(buffer[offset]); - offset += 1; - self.bus.ppu.write_latch = buffer[offset] != 0; - offset += 1; - self.bus.ppu.dot = std.mem.readInt(u16, buffer[offset..][0..2], .little); - offset += 2; - self.bus.ppu.scanline = std.mem.readInt(u16, buffer[offset..][0..2], .little); - offset += 2; - - @memcpy(&self.bus.ppu.nametable, buffer[offset..][0..4096]); - offset += 4096; - @memcpy(&self.bus.ppu.palette, buffer[offset..][0..32]); - offset += 32; - @memcpy(&self.bus.ppu.oam, buffer[offset..][0..256]); - offset += 256; -} - -test "NES System - Save State Serialization Roundtrip" { - // Construct dummy iNES NROM header + 16K PRG + 8K CHR - var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000); - @memcpy(rom[0..4], "NES\x1a"); - rom[4] = 1; // 16K PRG - rom[5] = 1; // 8K CHR - - var nes: Nes = undefined; - try nes.init(&rom); - - // Mutate state - nes.cpu.registers.pc = 0x1234; - nes.cpu.registers.a = 0x42; - nes.bus.ram[0x05] = 0xAA; - nes.bus.ppu.vram_addr = 0x2050; - - var save_buf: [32768]u8 = undefined; - const bytes_written = try nes.saveState(&save_buf); - - // Reset nes - nes.reset(); - try std.testing.expect(nes.cpu.registers.pc != 0x1234); - - // Load state - try nes.loadState(save_buf[0..bytes_written]); - try std.testing.expectEqual(@as(u16, 0x1234), nes.cpu.registers.pc); - try std.testing.expectEqual(@as(u8, 0x42), nes.cpu.registers.a); - try std.testing.expectEqual(@as(u8, 0xAA), nes.bus.ram[0x05]); - try std.testing.expectEqual(@as(u16, 0x2050), nes.bus.ppu.vram_addr); -} - -comptime { - contract.validateSystem(Nes); -}