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