using System; using BizHawk.Common; using BizHawk.Emulation.Common; using BizHawk.Emulation.Cores.Components; //simplifications/approximations: //* "Note that no commercial games rely on this mirroring -- therefore you can take the easy way out and simply give all MMC5 games 64k PRG-RAM." // (i.e. ignore chipselect/page select on prg-ram) //* in general PPU state is peeked directly instead of figuring out how the mmc5 actually accounts for things. //* Specifically, the tall sprite mode is peeked. this is annoying.. the mmc5 should not know about that until the first tall sprite appears and asks // for something from the right page. there should be a better way to determine this //* Specifically, the dot number / BG/OBJ phase status is used instead of counting reads. //* Specifically, the scanline number is used for IRQ instead of counting reads or whatever //TODO - tweak nametable / chr viewer to be more useful //FUTURE - we may need to split this into a separate MMC5 class. but for now it is just a pain. namespace BizHawk.Emulation.Cores.Nintendo.NES { [NES.INESBoardImplPriority] public sealed class ExROM : NES.NESBoardBase { //configuraton int prg_bank_mask_8k, chr_bank_mask_1k; //board setup (to be isolated from mmc5 code later, when we need the separate mmc5 class) //state int irq_target, irq_counter; bool irq_enabled, irq_pending, in_frame; int exram_mode, chr_mode, prg_mode; int chr_reg_high; int ab_mode; IntBuffer regs_a = new IntBuffer(8); IntBuffer regs_b = new IntBuffer(4); IntBuffer regs_prg = new IntBuffer(4); IntBuffer nt_modes = new IntBuffer(4); byte nt_fill_tile, nt_fill_attrib; int wram_bank; byte[] EXRAM = new byte[1024]; byte multiplicand, multiplier; MMC5Audio audio; //regeneratable state IntBuffer a_banks_1k = new IntBuffer(8); IntBuffer b_banks_1k = new IntBuffer(8); IntBuffer prg_banks_8k = new IntBuffer(4); byte product_low, product_high; int last_nt_read; bool irq_audio; public MemoryDomain GetExRAM() { return new MemoryDomainByteArray("ExRAM", MemoryDomain.Endian.Little, EXRAM, true, 1); } /// /// use with caution /// /// public byte[] GetExRAMArray() { return EXRAM; } public bool ExAttrActive { get { return exram_mode == 1; } } public override void SyncState(Serializer ser) { base.SyncState(ser); ser.Sync("irq_target", ref irq_target); ser.Sync("irq_counter", ref irq_counter); ser.Sync("irq_enabled", ref irq_enabled); ser.Sync("irq_pending", ref irq_pending); ser.Sync("in_frame", ref in_frame); ser.Sync("exram_mode", ref exram_mode); ser.Sync("chr_mode", ref chr_mode); ser.Sync("prg_mode", ref prg_mode); ser.Sync("chr_reg_high", ref chr_reg_high); ser.Sync("ab_mode", ref ab_mode); ser.Sync("regs_a", ref regs_a); ser.Sync("regs_b", ref regs_b); ser.Sync("regs_prg", ref regs_prg); ser.Sync("nt_modes", ref nt_modes); ser.Sync("nt_fill_tile", ref nt_fill_tile); ser.Sync("nt_fill_attrib", ref nt_fill_attrib); ser.Sync("wram_bank", ref wram_bank); ser.Sync("last_nt_read", ref last_nt_read); ser.Sync("EXRAM", ref EXRAM, false); SyncPRGBanks(); SyncCHRBanks(); SyncMultiplier(); SyncIRQ(); audio.SyncState(ser); } public override void Dispose() { regs_a.Dispose(); regs_b.Dispose(); regs_prg.Dispose(); a_banks_1k.Dispose(); b_banks_1k.Dispose(); prg_banks_8k.Dispose(); nt_modes.Dispose(); } public override bool Configure(NES.EDetectionOrigin origin) { //analyze board type switch (Cart.board_type) { case "MAPPER005": Cart.wram_size = 64; break; case "NES-ELROM": //Castlevania 3 - Dracula's Curse (U) case "HVC-ELROM": AssertPrg(128, 256); AssertChr(128); break; case "NES-EKROM": //Gemfire (U) AssertPrg(256); AssertChr(256); break; case "HVC-EKROM": break; case "NES-ETROM": case "HVC-ETROM": break; case "NES-EWROM": case "HVC-EWROM": break; default: return false; } prg_bank_mask_8k = Cart.prg_size / 8 - 1; if (Cart.chr_size > 0) chr_bank_mask_1k = Cart.chr_size - 1; else chr_bank_mask_1k = Cart.vram_size - 1; PoweronState(); if (NES.apu != null) audio = new MMC5Audio(NES.apu.ExternalQueue, (e) => { irq_audio = e; SyncIRQ(); }); return true; } void PoweronState() { //set all prg regs to use ROM regs_prg[0] = 0x80; regs_prg[1] = 0x80; regs_prg[2] = 0x80; regs_prg[3] = 0xFF; prg_mode = 3; SyncPRGBanks(); SyncCHRBanks(); SetMirrorType(EMirrorType.Vertical); } int PRGGetBank(int addr, out bool ram) { int bank_8k = addr >> 13; bank_8k = prg_banks_8k[bank_8k]; ram = (bank_8k & 0x80) == 0; if (!ram) bank_8k &= prg_bank_mask_8k; return bank_8k; } // wram: // [.... .CBB] // C = chip select // B = bank select (8K banks) // the following configurations are known: // 1) no wram // 2) 8K wram: 1x 8K // 3) 16K wram: 2x 8K // 4) 32K wram: 1x 32K // // for iNES, we assume 64K wram int? MaskWRAM(int bank) { bank &= 7; switch (Cart.wram_size) { case 0: return null; case 8: if (bank >= 4) return null; else return 0; case 16: return bank >> 2; case 32: if (bank >= 4) return null; else return bank & 3; case 64: return bank; default: throw new Exception(); } } void WriteWRAMActual(int bank, int offs, byte value) { int? bbank = MaskWRAM(bank); if (bbank.HasValue) WRAM[(int)bbank << 13 | offs] = value; } byte ReadWRAMActual(int bank, int offs) { int? bbank = MaskWRAM(bank); if (bbank.HasValue) return WRAM[(int)bbank << 13 | offs]; else return NES.DB; } //this could be handy, but probably not. I did it on accident. //TileCoord ComputeTXTYFromPPUTiming(int visible_scanline, int cycle) //{ // int py = visible_scanline; // int px = cycle; // if (cycle > 260) // { // py++; // px -= 322; // } // else px += 16; // int tx = px / 8; // int ty = py / 8; // return new TileCoord(tx, ty); //} int MapCHR(int addr) { int bank_1k = addr >> 10; int ofs = addr & ((1 << 10) - 1); if (exram_mode == 1 && NES.ppu.ppuphase == PPU.PPUPHASE.BG) { int exram_addr = last_nt_read; int bank_4k = EXRAM[exram_addr] & 0x3F; bank_1k = bank_4k * 4; // low 12 bits of address come from PPU // next 6 bits of address come from exram table // top 2 bits of address come from chr_reg_high bank_1k += chr_reg_high << 8; ofs = addr & (4 * 1024 - 1); goto MAPPED; } if (NES.ppu.reg_2000.obj_size_16) { bool isPattern = NES.ppu.PPUON; if (NES.ppu.ppuphase == PPU.PPUPHASE.OBJ && isPattern) bank_1k = a_banks_1k[bank_1k]; else if (NES.ppu.ppuphase == PPU.PPUPHASE.BG && isPattern) bank_1k = b_banks_1k[bank_1k]; else { if (ab_mode == 0) bank_1k = a_banks_1k[bank_1k]; else bank_1k = b_banks_1k[bank_1k]; } } else { bank_1k = a_banks_1k[bank_1k]; } MAPPED: bank_1k &= chr_bank_mask_1k; addr = (bank_1k<<10)|ofs; return addr; } public override byte ReadPPU(int addr) { if (addr < 0x2000) { addr = MapCHR(addr); return (VROM ?? VRAM)[addr]; } else { addr -= 0x2000; int nt_entry = addr & 0x3FF; if (nt_entry < 0x3C0) { //track the last nametable entry read so that subsequent pattern and attribute reads will know which exram address to use last_nt_read = nt_entry; } else { //attribute table if (exram_mode == 1) { //attribute will be in the top 2 bits of the exram byte int exram_addr = last_nt_read; int attribute = EXRAM[exram_addr] >> 6; //calculate tile address by getting x/y from last nametable int tx = last_nt_read & 0x1F; int ty = last_nt_read / 32; //attribute table address is just these coords shifted int atx = tx >> 1; int aty = ty >> 1; //figure out how we need to shift the attribute to fake out the ppu int at_shift = ((aty & 1) << 1) + (atx & 1); at_shift <<= 1; attribute <<= at_shift; return (byte)attribute; } } int nt = (addr >> 10) & 3; // &3 to read from the NT mirrors at 3xxx int offset = addr & ((1 << 10) - 1); nt = nt_modes[nt]; switch (nt) { case 0: //NES internal NTA return NES.CIRAM[offset]; case 1: //NES internal NTB return NES.CIRAM[0x400 | offset]; case 2: //use ExRAM as NT //TODO - additional r/w security if (exram_mode >= 2) return 0; else return EXRAM[offset]; case 3: // Fill Mode if (offset >= 0x3c0) return nt_fill_attrib; else return nt_fill_tile; default: throw new Exception(); } } } public override byte PeekPPU(int addr) { if (addr < 0x2000) { addr = MapCHR(addr); return (VROM ?? VRAM)[addr]; } else { addr -= 0x2000; int nt_entry = addr & 0x3FF; if (nt_entry < 0x3C0) { //track the last nametable entry read so that subsequent pattern and attribute reads will know which exram address to use //last_nt_read = nt_entry; } else { //attribute table if (exram_mode == 1) { //attribute will be in the top 2 bits of the exram byte int exram_addr = last_nt_read; int attribute = EXRAM[exram_addr] >> 6; //calculate tile address by getting x/y from last nametable int tx = last_nt_read & 0x1F; int ty = last_nt_read / 32; //attribute table address is just these coords shifted int atx = tx >> 1; int aty = ty >> 1; //figure out how we need to shift the attribute to fake out the ppu int at_shift = ((aty & 1) << 1) + (atx & 1); at_shift <<= 1; attribute <<= at_shift; return (byte)attribute; } } int nt = (addr >> 10) & 3; // &3 to read from the NT mirrors at 3xxx int offset = addr & ((1 << 10) - 1); nt = nt_modes[nt]; switch (nt) { case 0: //NES internal NTA return NES.CIRAM[offset]; case 1: //NES internal NTB return NES.CIRAM[0x400 | offset]; case 2: //use ExRAM as NT //TODO - additional r/w security if (exram_mode >= 2) return 0; else return EXRAM[offset]; case 3: // Fill Mode if (offset >= 0x3c0) return nt_fill_attrib; else return nt_fill_tile; default: throw new Exception(); } } } public override void WritePPU(int addr, byte value) { if (addr < 0x2000) { if (VRAM != null) VRAM[MapCHR(addr)] = value; } else { addr -= 0x2000; int nt = (addr >> 10) & 3; // &3 to read from the NT mirrors at 3xxx int offset = addr & ((1 << 10) - 1); nt = nt_modes[nt]; switch (nt) { case 0: //NES internal NTA NES.CIRAM[offset] = value; break; case 1: //NES internal NTB NES.CIRAM[0x400 | offset] = value; break; case 2: //use ExRAM as NT //TODO - additional r/w security EXRAM[offset] = value; break; case 3: //Fill Mode //what to do? break; default: throw new Exception(); } } } public override void WriteWRAM(int addr, byte value) { WriteWRAMActual(wram_bank, addr & 0x1fff, value); } public override byte ReadWRAM(int addr) { return ReadWRAMActual(wram_bank, addr & 0x1fff); } public override byte ReadPRG(int addr) { bool ram; byte ret; int offs = addr & 0x1fff; int bank = PRGGetBank(addr, out ram); if (ram) ret = ReadWRAMActual(bank, offs); else ret = ROM[bank << 13 | offs]; if (addr < 0x4000) audio.ReadROMTrigger(ret); return ret; } public override byte PeekCart(int addr) { if (addr >= 0x8000) return PeekPRG(addr - 0x8000); else if (addr >= 0x6000) return ReadWRAM(addr - 0x6000); else return PeekEXP(addr - 0x4000); } public byte PeekPRG(int addr) { bool ram; byte ret; int offs = addr & 0x1fff; int bank = PRGGetBank(addr, out ram); if (ram) ret = ReadWRAMActual(bank, offs); else ret = ROM[bank << 13 | offs]; //if (addr < 0x4000) // audio.ReadROMTrigger(ret); return ret; } public override void WritePRG(int addr, byte value) { bool ram; int bank = PRGGetBank(addr, out ram); if (ram) WriteWRAMActual(bank, addr & 0x1fff, value); } public override void WriteEXP(int addr, byte value) { //NES.LogLine("MMC5 WriteEXP: ${0:x4} = ${1:x2}", addr, value); if (addr >= 0x1000 && addr <= 0x1015) { audio.WriteExp(addr + 0x4000, value); return; } switch (addr) { case 0x1100: //$5100: [.... ..PP] PRG Mode Select: prg_mode = value & 3; SyncPRGBanks(); break; case 0x1101: //$5101: [.... ..CC] chr_mode = value & 3; SyncCHRBanks(); break; case 0x1102: //$5102: [.... ..AA] PRG-RAM Protect A case 0x1103: //$5103: [.... ..BB] PRG-RAM Protect B break; case 0x1104: //$5104: [.... ..XX] ExRAM mode exram_mode = value & 3; //NES.LogLine("exram mode set to: {0}", exram_mode); break; case 0x1105: //$5105: [DDCC BBAA] (nametable config) nt_modes[0] = (value >> 0) & 3; nt_modes[1] = (value >> 2) & 3; nt_modes[2] = (value >> 4) & 3; nt_modes[3] = (value >> 6) & 3; //NES.LogLine("nt_modes set to {0},{1},{2},{3}", nt_modes[0], nt_modes[1], nt_modes[2], nt_modes[3]); break; case 0x1106: //$5106: [TTTT TTTT] Fill Tile nt_fill_tile = value; break; case 0x1107: //$5107: [.... ..AA] Fill Attribute bits nt_fill_attrib = (byte)(value & 3); // extend out to fill all 4 positions nt_fill_attrib |= (byte)(nt_fill_attrib << 2); nt_fill_attrib |= (byte)(nt_fill_attrib << 4); break; case 0x1113: //$5113: [.... .PPP] (simplified, but technically inaccurate -- see below) wram_bank = value & 7; break; //$5114-5117: [RPPP PPPP] PRG select case 0x1114: case 0x1115: case 0x1116: case 0x1117: if (addr == 0x1117) value |= 0x80; regs_prg[addr - 0x1114] = value; SyncPRGBanks(); break; //$5120 - $5127 'A' Regs: case 0x1120: case 0x1121: case 0x1122: case 0x1123: case 0x1124: case 0x1125: case 0x1126: case 0x1127: ab_mode = 0; regs_a[addr - 0x1120] = value | (chr_reg_high<<8); //NES.LogLine("set bank A {0:x4} to {1:x2}", addr+0x4000, value); SyncCHRBanks(); break; //$5128 - $512B 'B' Regs: case 0x1128: case 0x1129: case 0x112A: case 0x112B: ab_mode = 1; regs_b[addr - 0x1128] = value | (chr_reg_high<<8); //NES.LogLine("set bank B {0:x4} to {1:x2}", addr + 0x4000, value); SyncCHRBanks(); break; case 0x1130: //$5130 [.... ..HH] 'High' CHR Reg: chr_reg_high = value & 3; break; case 0x1203: //$5203: [IIII IIII] IRQ Target irq_target = value; SyncIRQ(); break; case 0x1204: //$5204: [E... ....] IRQ Enable (0=disabled, 1=enabled) irq_enabled = (value & 0x80) != 0; SyncIRQ(); break; case 0x1205: //$5205: multiplicand multiplicand = value; SyncMultiplier(); break; case 0x1206: //$5206: multiplier multiplier = value; SyncMultiplier(); break; } //TODO - additional r/w timing security if (addr >= 0x1C00) { if(exram_mode != 3) EXRAM[addr - 0x1C00] = value; } } void SyncMultiplier() { int result = multiplicand*multiplier; product_low = (byte)(result&0xFF); product_high = (byte)((result>>8) & 0xFF); } public override byte ReadEXP(int addr) { byte ret = 0xFF; switch (addr) { case 0x1204: //$5204: [E... ....] IRQ Enable (0=disabled, 1=enabled) ret = (byte)((irq_pending ? 0x80 : 0) | (in_frame ? 0x40 : 0)); irq_pending = false; SyncIRQ(); break; case 0x1205: //$5205: low 8 bits of product ret = product_low; break; case 0x1206: //$5206: high 8 bits of product ret = product_high; break; case 0x1015: // $5015: apu status ret = audio.Read5015(); break; case 0x1010: // $5010: apu PCM ret = audio.Read5010(); break; } //TODO - additional r/w timing security if (addr >= 0x1C00) { if (exram_mode < 2) ret = 0xFF; else ret = EXRAM[addr - 0x1C00]; } return ret; } public byte PeekEXP(int addr) { byte ret = 0xFF; switch (addr) { case 0x1204: //$5204: [E... ....] IRQ Enable (0=disabled, 1=enabled) ret = (byte)((irq_pending ? 0x80 : 0) | (in_frame ? 0x40 : 0)); //irq_pending = false; //SyncIRQ(); break; case 0x1205: //$5205: low 8 bits of product ret = product_low; break; case 0x1206: //$5206: high 8 bits of product ret = product_high; break; case 0x1015: // $5015: apu status ret = audio.Read5015(); break; case 0x1010: // $5010: apu PCM ret = audio.Peek5010(); break; } //TODO - additional r/w timing security if (addr >= 0x1C00) { if (exram_mode < 2) ret = 0xFF; else ret = EXRAM[addr - 0x1C00]; } return ret; } void SyncIRQ() { IRQSignal = (irq_pending && irq_enabled) || irq_audio; } public override void ClockPPU() { if (NES.ppu.ppur.status.cycle != 336) return; int sl = NES.ppu.ppur.status.sl + 1; if (!NES.ppu.PPUON || sl >= 241) { // whenever rendering is off for any reason (vblank or forced disable // the irq counter resets, as well as the inframe flag (easily verifiable from software) in_frame = false; irq_counter = 0; irq_pending = false; SyncIRQ(); return; } if (!in_frame) { in_frame = true; irq_counter = 0; irq_pending = false; SyncIRQ(); } else { irq_counter++; if (irq_counter == irq_target) { irq_pending = true; SyncIRQ(); } } } public override void ClockCPU() { audio.Clock(); } void SetBank(IntBuffer target, int offset, int size, int value) { value &= ~(size-1); for (int i = 0; i < size; i++) { int index = i+offset; target[index] = value; value++; } } void SyncPRGBanks() { switch (prg_mode) { case 0: SetBank(prg_banks_8k, 0, 4, regs_prg[3]&~3); break; case 1: SetBank(prg_banks_8k, 0, 2, regs_prg[1] & ~1); SetBank(prg_banks_8k, 2, 2, regs_prg[3] & ~1); break; case 2: SetBank(prg_banks_8k, 0, 2, regs_prg[1] & ~1); SetBank(prg_banks_8k, 2, 1, regs_prg[2]); SetBank(prg_banks_8k, 3, 1, regs_prg[3]); break; case 3: SetBank(prg_banks_8k, 0, 1, regs_prg[0]); SetBank(prg_banks_8k, 1, 1, regs_prg[1]); SetBank(prg_banks_8k, 2, 1, regs_prg[2]); SetBank(prg_banks_8k, 3, 1, regs_prg[3]); break; } } void SyncCHRBanks() { //MASTER LOGIC: something like this this might be enough to work, but i'll play with it later //bank_1k >> (3 - chr_mode) << chr_mode | bank_1k & ( etc.etc. //TODO - do these need to have the last arguments multiplied by 8,4,2 to map to the right banks? switch (chr_mode) { case 0: SetBank(a_banks_1k, 0, 8, regs_a[7] * 8); SetBank(b_banks_1k, 0, 8, regs_b[3] * 8); break; case 1: SetBank(a_banks_1k, 0, 4, regs_a[3] * 4); SetBank(a_banks_1k, 4, 4, regs_a[7] * 4); SetBank(b_banks_1k, 0, 4, regs_b[3] * 4); SetBank(b_banks_1k, 4, 4, regs_b[3] * 4); break; case 2: SetBank(a_banks_1k, 0, 2, regs_a[1] * 2); SetBank(a_banks_1k, 2, 2, regs_a[3] * 2); SetBank(a_banks_1k, 4, 2, regs_a[5] * 2); SetBank(a_banks_1k, 6, 2, regs_a[7] * 2); SetBank(b_banks_1k, 0, 2, regs_b[1] * 2); SetBank(b_banks_1k, 2, 2, regs_b[3] * 2); SetBank(b_banks_1k, 4, 2, regs_b[1] * 2); SetBank(b_banks_1k, 6, 2, regs_b[3] * 2); break; case 3: SetBank(a_banks_1k, 0, 1, regs_a[0]); SetBank(a_banks_1k, 1, 1, regs_a[1]); SetBank(a_banks_1k, 2, 1, regs_a[2]); SetBank(a_banks_1k, 3, 1, regs_a[3]); SetBank(a_banks_1k, 4, 1, regs_a[4]); SetBank(a_banks_1k, 5, 1, regs_a[5]); SetBank(a_banks_1k, 6, 1, regs_a[6]); SetBank(a_banks_1k, 7, 1, regs_a[7]); SetBank(b_banks_1k, 0, 1, regs_b[0]); SetBank(b_banks_1k, 1, 1, regs_b[1]); SetBank(b_banks_1k, 2, 1, regs_b[2]); SetBank(b_banks_1k, 3, 1, regs_b[3]); SetBank(b_banks_1k, 4, 1, regs_b[0]); SetBank(b_banks_1k, 5, 1, regs_b[1]); SetBank(b_banks_1k, 6, 1, regs_b[2]); SetBank(b_banks_1k, 7, 1, regs_b[3]); break; } } } }