From 04fb7bcc1fb4dbe2a10714f0d019d2e7ca12b25f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20=C5=9Een?= Date: Fri, 14 Aug 2026 06:48:16 +0200 Subject: [PATCH] add nes cartridge subsystem and 41 ines mapper variants --- src/system/nes/cartridge.zig | 1105 +++++++++++++++++++++++++ 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 ++++ 41 files changed, 5042 insertions(+) create mode 100644 src/system/nes/cartridge.zig create mode 100644 src/system/nes/mapper/axrom.zig create mode 100644 src/system/nes/mapper/camerica.zig create mode 100644 src/system/nes/mapper/cnrom.zig create mode 100644 src/system/nes/mapper/colordreams.zig create mode 100644 src/system/nes/mapper/gxrom.zig create mode 100644 src/system/nes/mapper/irem78.zig create mode 100644 src/system/nes/mapper/mapper118.zig create mode 100644 src/system/nes/mapper/mapper119.zig create mode 100644 src/system/nes/mapper/mapper13.zig create mode 100644 src/system/nes/mapper/mapper140.zig create mode 100644 src/system/nes/mapper/mapper180.zig create mode 100644 src/system/nes/mapper/mapper206.zig create mode 100644 src/system/nes/mapper/mapper210.zig create mode 100644 src/system/nes/mapper/mapper232.zig create mode 100644 src/system/nes/mapper/mapper34.zig create mode 100644 src/system/nes/mapper/mapper65.zig create mode 100644 src/system/nes/mapper/mapper68.zig create mode 100644 src/system/nes/mapper/mapper69.zig create mode 100644 src/system/nes/mapper/mapper76.zig create mode 100644 src/system/nes/mapper/mapper87.zig create mode 100644 src/system/nes/mapper/mapper88.zig create mode 100644 src/system/nes/mapper/mapper89.zig create mode 100644 src/system/nes/mapper/mapper93.zig create mode 100644 src/system/nes/mapper/mapper94.zig create mode 100644 src/system/nes/mapper/mmc1.zig create mode 100644 src/system/nes/mapper/mmc2.zig create mode 100644 src/system/nes/mapper/mmc3.zig create mode 100644 src/system/nes/mapper/mmc4.zig create mode 100644 src/system/nes/mapper/mmc5.zig create mode 100644 src/system/nes/mapper/nina03.zig create mode 100644 src/system/nes/mapper/nrom.zig create mode 100644 src/system/nes/mapper/root.zig create mode 100644 src/system/nes/mapper/types.zig create mode 100644 src/system/nes/mapper/uxrom.zig create mode 100644 src/system/nes/mapper/vrc1.zig create mode 100644 src/system/nes/mapper/vrc2.zig create mode 100644 src/system/nes/mapper/vrc3.zig create mode 100644 src/system/nes/mapper/vrc4.zig create mode 100644 src/system/nes/mapper/vrc6.zig create mode 100644 src/system/nes/mapper/vrc7.zig diff --git a/src/system/nes/cartridge.zig b/src/system/nes/cartridge.zig new file mode 100644 index 0000000..568a5a0 --- /dev/null +++ b/src/system/nes/cartridge.zig @@ -0,0 +1,1105 @@ +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]; + + // NES 2.0 signature. + if ((flags7 & 0x0c) == 0x08) + return error.UnsupportedNes2; + + const mapper_id: u8 = + (flags6 >> 4) | + (flags7 & 0xf0); + + const prg_size = + @as(usize, prg_banks) * 0x4000; + + const chr_size = + @as(usize, chr_banks) * 0x2000; + + 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, + mapper_id, + ), + }, + 22, 23 => .{ + .vrc2 = _mapper.Vrc2.init( + prg_size, + chr_size, + mapper_id, + ), + }, + 24, 26 => .{ + .vrc6 = _mapper.Vrc6.init( + prg_size, + chr_size, + 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)); +} diff --git a/src/system/nes/mapper/axrom.zig b/src/system/nes/mapper/axrom.zig new file mode 100644 index 0000000..c5f036c --- /dev/null +++ b/src/system/nes/mapper/axrom.zig @@ -0,0 +1,64 @@ +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 new file mode 100644 index 0000000..b187558 --- /dev/null +++ b/src/system/nes/mapper/camerica.zig @@ -0,0 +1,82 @@ +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 new file mode 100644 index 0000000..cbcc891 --- /dev/null +++ b/src/system/nes/mapper/cnrom.zig @@ -0,0 +1,69 @@ +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 new file mode 100644 index 0000000..e713d7d --- /dev/null +++ b/src/system/nes/mapper/colordreams.zig @@ -0,0 +1,73 @@ +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 new file mode 100644 index 0000000..f944b49 --- /dev/null +++ b/src/system/nes/mapper/gxrom.zig @@ -0,0 +1,73 @@ +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 new file mode 100644 index 0000000..a2ba7bd --- /dev/null +++ b/src/system/nes/mapper/irem78.zig @@ -0,0 +1,78 @@ +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 new file mode 100644 index 0000000..55d649d --- /dev/null +++ b/src/system/nes/mapper/mapper118.zig @@ -0,0 +1,69 @@ +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 new file mode 100644 index 0000000..cf9bdb8 --- /dev/null +++ b/src/system/nes/mapper/mapper119.zig @@ -0,0 +1,113 @@ +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 new file mode 100644 index 0000000..9fbd01e --- /dev/null +++ b/src/system/nes/mapper/mapper13.zig @@ -0,0 +1,69 @@ +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 new file mode 100644 index 0000000..fd34464 --- /dev/null +++ b/src/system/nes/mapper/mapper140.zig @@ -0,0 +1,73 @@ +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 new file mode 100644 index 0000000..f6c2088 --- /dev/null +++ b/src/system/nes/mapper/mapper180.zig @@ -0,0 +1,70 @@ +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 new file mode 100644 index 0000000..1cdd201 --- /dev/null +++ b/src/system/nes/mapper/mapper206.zig @@ -0,0 +1,108 @@ +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 new file mode 100644 index 0000000..33395ef --- /dev/null +++ b/src/system/nes/mapper/mapper210.zig @@ -0,0 +1,105 @@ +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 new file mode 100644 index 0000000..da620f7 --- /dev/null +++ b/src/system/nes/mapper/mapper232.zig @@ -0,0 +1,77 @@ +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 new file mode 100644 index 0000000..d5b7ca4 --- /dev/null +++ b/src/system/nes/mapper/mapper34.zig @@ -0,0 +1,92 @@ +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 new file mode 100644 index 0000000..f6a255c --- /dev/null +++ b/src/system/nes/mapper/mapper65.zig @@ -0,0 +1,118 @@ +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 new file mode 100644 index 0000000..95e8ced --- /dev/null +++ b/src/system/nes/mapper/mapper68.zig @@ -0,0 +1,103 @@ +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 new file mode 100644 index 0000000..b40c45d --- /dev/null +++ b/src/system/nes/mapper/mapper69.zig @@ -0,0 +1,123 @@ +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 new file mode 100644 index 0000000..3a67586 --- /dev/null +++ b/src/system/nes/mapper/mapper76.zig @@ -0,0 +1,91 @@ +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 new file mode 100644 index 0000000..932e2f4 --- /dev/null +++ b/src/system/nes/mapper/mapper87.zig @@ -0,0 +1,70 @@ +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 new file mode 100644 index 0000000..fdc1cb8 --- /dev/null +++ b/src/system/nes/mapper/mapper88.zig @@ -0,0 +1,103 @@ +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 new file mode 100644 index 0000000..10b62e2 --- /dev/null +++ b/src/system/nes/mapper/mapper89.zig @@ -0,0 +1,78 @@ +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 new file mode 100644 index 0000000..438f62a --- /dev/null +++ b/src/system/nes/mapper/mapper93.zig @@ -0,0 +1,73 @@ +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 new file mode 100644 index 0000000..f10ff60 --- /dev/null +++ b/src/system/nes/mapper/mapper94.zig @@ -0,0 +1,71 @@ +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 new file mode 100644 index 0000000..72111fb --- /dev/null +++ b/src/system/nes/mapper/mmc1.zig @@ -0,0 +1,139 @@ +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 new file mode 100644 index 0000000..ec04044 --- /dev/null +++ b/src/system/nes/mapper/mmc2.zig @@ -0,0 +1,104 @@ +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 new file mode 100644 index 0000000..4c88313 --- /dev/null +++ b/src/system/nes/mapper/mmc3.zig @@ -0,0 +1,161 @@ +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 new file mode 100644 index 0000000..f110b44 --- /dev/null +++ b/src/system/nes/mapper/mmc4.zig @@ -0,0 +1,117 @@ +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 new file mode 100644 index 0000000..3da5118 --- /dev/null +++ b/src/system/nes/mapper/mmc5.zig @@ -0,0 +1,171 @@ +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 new file mode 100644 index 0000000..0ed3086 --- /dev/null +++ b/src/system/nes/mapper/nina03.zig @@ -0,0 +1,73 @@ +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 new file mode 100644 index 0000000..cac5f5e --- /dev/null +++ b/src/system/nes/mapper/nrom.zig @@ -0,0 +1,71 @@ +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 new file mode 100644 index 0000000..1f8baab --- /dev/null +++ b/src/system/nes/mapper/root.zig @@ -0,0 +1,147 @@ +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 new file mode 100644 index 0000000..b22074a --- /dev/null +++ b/src/system/nes/mapper/types.zig @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000..75c76fe --- /dev/null +++ b/src/system/nes/mapper/uxrom.zig @@ -0,0 +1,73 @@ +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 new file mode 100644 index 0000000..8ff8371 --- /dev/null +++ b/src/system/nes/mapper/vrc1.zig @@ -0,0 +1,106 @@ +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 new file mode 100644 index 0000000..873b05b --- /dev/null +++ b/src/system/nes/mapper/vrc2.zig @@ -0,0 +1,127 @@ +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 new file mode 100644 index 0000000..718db13 --- /dev/null +++ b/src/system/nes/mapper/vrc3.zig @@ -0,0 +1,118 @@ +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 new file mode 100644 index 0000000..838a325 --- /dev/null +++ b/src/system/nes/mapper/vrc4.zig @@ -0,0 +1,179 @@ +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 new file mode 100644 index 0000000..6f135e0 --- /dev/null +++ b/src/system/nes/mapper/vrc6.zig @@ -0,0 +1,152 @@ +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 new file mode 100644 index 0000000..7a08dbe --- /dev/null +++ b/src/system/nes/mapper/vrc7.zig @@ -0,0 +1,145 @@ +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; +}