const std = @import("std"); const common = @import("../common.zig"); const Mirroring = common.Mirroring; // ponytail: Mapper 10 (MMC4) - Fire Emblem & Fire Emblem Gaiden // 16KB switchable PRG at $8000, fixed last 16KB at $C000-$FFFF. // Dual 4KB CHR latches toggled by PPU fetches at $xFD8-$xFDF and $xFE8-$xFEF. pub const Mmc4 = @This(); prg_bank: u8 = 0, chr_bank_0_fd: u8 = 0, chr_bank_0_fe: u8 = 0, chr_bank_1_fd: u8 = 0, chr_bank_1_fe: u8 = 0, latch_0: u1 = 0, latch_1: u1 = 0, mirroring_mode: Mirroring = .vertical, pub fn init() Mmc4 { return .{}; } pub fn cpuRead(self: *const Mmc4, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 { if (address >= 0x6000 and address <= 0x7fff) { const offset = address - 0x6000; if (offset < prg_ram.len) return prg_ram[offset]; return 0; } if (address >= 0x8000 and address <= 0xffff) { const total_16k_banks = @max(1, prg_rom.len / 0x4000); var bank: usize = 0; if (address < 0xc000) { bank = @as(usize, self.prg_bank) % total_16k_banks; const offset = (bank * 0x4000) + (address - 0x8000); if (offset < prg_rom.len) return prg_rom[offset]; } else { bank = total_16k_banks - 1; const offset = (bank * 0x4000) + (address - 0xc000); if (offset < prg_rom.len) return prg_rom[offset]; } return 0; } return null; } pub fn cpuWrite(self: *Mmc4, address: u16, value: u8, prg_ram: []u8) bool { if (address >= 0x6000 and address <= 0x7fff) { const offset = address - 0x6000; if (offset < prg_ram.len) { prg_ram[offset] = value; return true; } return false; } switch (address) { 0xa000...0xafff => { self.prg_bank = value & 0x0f; return true; }, 0xb000...0xbfff => { self.chr_bank_0_fd = value & 0x1f; return true; }, 0xc000...0xcfff => { self.chr_bank_0_fe = value & 0x1f; return true; }, 0xd000...0xdfff => { self.chr_bank_1_fd = value & 0x1f; return true; }, 0xe000...0xefff => { self.chr_bank_1_fe = value & 0x1f; return true; }, 0xf000...0xffff => { self.mirroring_mode = if ((value & 1) == 0) .vertical else .horizontal; return true; }, else => return false, } } pub fn ppuRead(self: *const Mmc4, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 { if (address < 0x2000) { if (chr_is_ram) { if (address < chr_ram.len) return chr_ram[address]; return 0; } if (chr_rom.len == 0) return 0; const total_4k_banks = @max(1, chr_rom.len / 0x1000); const bank = if (address < 0x1000) (if (self.latch_0 == 0) self.chr_bank_0_fd else self.chr_bank_0_fe) else (if (self.latch_1 == 0) self.chr_bank_1_fd else self.chr_bank_1_fe); const offset = ((@as(usize, bank) % total_4k_banks) * 0x1000) + (address & 0x0fff); if (offset < chr_rom.len) return chr_rom[offset]; return 0; } return null; } pub fn ppuWrite(_: *Mmc4, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool { if (address < 0x2000 and chr_is_ram) { if (address < chr_ram.len) { chr_ram[address] = value; return true; } } return false; } pub fn notifyPpuAddress(self: *Mmc4, address: u16) void { self.checkLatch(address); } inline fn checkLatch(self: *Mmc4, address: u16) void { switch (address) { 0x0fd8...0x0fdf => self.latch_0 = 0, 0x0fe8...0x0fef => self.latch_0 = 1, 0x1fd8...0x1fdf => self.latch_1 = 0, 0x1fe8...0x1fef => self.latch_1 = 1, else => {}, } } pub fn mirroring(self: *const Mmc4) Mirroring { return self.mirroring_mode; } pub fn irqLine(_: *const Mmc4) bool { return false; } test "mmc4 latch toggling" { var m = Mmc4.init(); var ram: [0x2000]u8 = undefined; _ = m.cpuWrite(0xa000, 3, &ram); // PRG bank 3 try std.testing.expectEqual(@as(u8, 3), m.prg_bank); m.notifyPpuAddress(0x1fe8); try std.testing.expectEqual(@as(u1, 1), m.latch_1); }