/* * HalfNES by Andrew Hoffman * Licensed under the GNU GPL Version 3. See LICENSE file */ package com.grapeshot.halfnes.mappers; import com.grapeshot.halfnes.utils; import com.grapeshot.halfnes.audio.MMC5SoundChip; import java.util.Arrays; /** * * @author Andrew */ public class MMC5Mapper extends Mapper { //the infamous kitchen sink mapper final int[] exram = new int[1024]; private int exramMode, chrMode, prgMode; private int wramWrite1, wramWrite2, multiplier1, multiplier2; private int prgpage, chrOr, wrambank; boolean scanctrEnable, irqPend; private final int[] chrregsA = new int[8]; private final int[] chrregsB = new int[4]; private final int[] prgregs = new int[4]; private final int[] chrmapB = new int[4]; private final boolean[] romHere = new boolean[3]; private int scanctrLine, irqCounter = 20; private final int[] fillnt = new int[1024]; private MMC5SoundChip soundchip; private boolean inFrame = false; @Override public void loadrom() throws BadMapperException { //needs to be in every mapper. Fill with initial cfg super.loadrom(); //on startup: prgregs[3] = (prgsize / 8192) - 1; prgregs[2] = (prgsize / 8192) - 1; prgregs[1] = (prgsize / 8192) - 1; prgregs[0] = (prgsize / 8192) - 1; prgMode = 3; setupPRG(); for (int i = 0; i < 8; ++i) { chr_map[i] = 1024 * i; } prgram = new int[65536]; } @Override public final void cartWrite(final int addr, final int data) { if (addr < 0x5c00) { //System.err.println("MMC5 Write register "+ utils.hex(addr) + " d " + utils.hex(data)); switch (addr) { case 0x5000: case 0x5001: case 0x5002: case 0x5003: case 0x5004: case 0x5005: case 0x5006: case 0x5007: case 0x5010: case 0x5011: case 0x5015: if (soundchip == null) { soundchip = new MMC5SoundChip(); cpuram.apu.addExpnSound(soundchip); } //sound chip soundchip.write(addr - 0x5000, data); break; case 0x5100: //prg mode select prgMode = data & 3; setupPRG(); break; case 0x5101: //chr mode select chrMode = data & 3; setupCHR(); break; case 0x5102: //wram write protect 1 wramWrite1 = data; break; case 0x5103: //wram write protect 2 wramWrite2 = data; break; case 0x5104: //exRAM mode - none of these are properly supported! exramMode = data & 3; break; case 0x5105: //mirror mode setMirroring(data, exram); break; case 0x5106: //fill tile Arrays.fill(fillnt, 0, 32 * 30, data); break; case 0x5107: //fill attribute Arrays.fill(fillnt, 32 * 30, fillnt.length, data & 0x3 + (data & 3) << 2 + (data & 3) << 4 + (data & 3) << 6); break; case 0x5113: //PRG RAM register wrambank = data & 7; break; case 0x5114: //prg reg 1 prgregs[0] = data & 0x7f; romHere[0] = ((data & (utils.BIT7)) != 0); setupPRG(); break; case 0x5115: //prg reg 2 prgregs[1] = data & 0x7f; romHere[1] = ((data & (utils.BIT7)) != 0); setupPRG(); break; case 0x5116: //prg reg 3 prgregs[2] = data & 0x7f; romHere[2] = ((data & (utils.BIT7)) != 0); setupPRG(); break; case 0x5117: //prg reg 4 prgregs[3] = data & 0x7f; setupPRG(); break; case 0x5120: chrregsA[0] = data | chrOr; setupCHR(); break; case 0x5121: chrregsA[1] = data | chrOr; setupCHR(); break; case 0x5122: chrregsA[2] = data | chrOr; setupCHR(); break; case 0x5123: chrregsA[3] = data | chrOr; setupCHR(); break; case 0x5124: chrregsA[4] = data | chrOr; setupCHR(); break; case 0x5125: chrregsA[5] = data | chrOr; setupCHR(); break; case 0x5126: chrregsA[6] = data | chrOr; setupCHR(); break; case 0x5127: chrregsA[7] = data | chrOr; setupCHR(); break; //chr regs A case 0x5128: chrregsB[0] = data | chrOr; setupCHR(); break; case 0x5129: chrregsB[1] = data | chrOr; setupCHR(); break; case 0x512a: chrregsB[2] = data | chrOr; setupCHR(); break; case 0x512b: chrregsB[3] = data | chrOr; setupCHR(); break; //chr regs b case 0x5130: //chr bank high bits (CHR_OR) //System.err.println(data); chrOr = (data & 3) << 8; break; case 0x5200: //splitscreen control if (((data & (utils.BIT7)) != 0)) { System.err.println("Split screen mode not supported yet"); } break; case 0x5201: //splitscreen scroll break; case 0x5202: //splitscreen chr page break; case 0x5203: //irq trigger scanctrLine = data; break; case 0x5204: //irq control scanctrEnable = ((data & (utils.BIT7)) != 0); break; case 0x5205: multiplier1 = data; break; case 0x5206: multiplier2 = data; break; default: break; } } else if (addr < 0x6000) { //exram exram[addr - 0x5c00] = data; } else if (addr < 0x8000) { final int wramaddr = wrambank * 8192 + (addr - 0x6000); //System.err.println("wrote wram " + utils.hex(wramaddr)); prgram[wramaddr] = data; } else if (addr < 0xA000 && !romHere[0] && prgMode == 3) { System.err.println("RAM write to 0x8000 area"); prgram[((prgregs[0] & 7) * 8192) + (addr - 0x8000)] = data; } else if (addr < 0xC000 && !romHere[1]) { int subaddr = (prgMode == 3) ? 0xA000 : 0x8000; int prgbank = (prgMode == 3) ? (prgregs[1] & 7) : ((prgregs[1] & 7) >> 1); int ramaddr = (prgbank * ((prgMode == 3) ? 8192 : 16384)) + (addr - subaddr); ////System.err.println("RAM write to 0xA000 area " + utils.hex(addr) + " " + prgbank); //System.err.println(utils.hex(ramaddr)); prgram[ramaddr] = data; } else if (addr < 0xE000 && !romHere[2]) { System.err.println("RAM write to 0xC000 area " + utils.hex(addr)); prgram[((prgregs[2] & 7) * 8192) + (addr - 0xc000)] = data; } else { System.err.println("unsupported mmc5 write " + utils.hex(addr) + romHere[0] + romHere[1] + romHere[2] + prgMode); } } @Override public final int cartRead(final int addr) { //hook for turning off PPU in frame flag since idk how the real thing works if (!ppu.renderingOn() || ppu.scanline > 241) { inFrame = false; } if (addr >= 0x8000) { //rom or maybe wram if (prgMode == 0 || ((prgMode == 1) && (addr >= 0xc000 || romHere[1])) || ((prgMode == 2) && ((addr >= 0xe000 || (addr >= 0xc000 && romHere[2]) || romHere[1])) || ((prgMode == 3) && (addr >= 0xe000 || (addr >= 0xc000 && romHere[2]) || (addr >= 0xa000 && romHere[1]) || romHere[0])))) { return prg[prg_map[((addr & 0x7fff)) >> 10] + (addr & 1023)]; } else { //don't know quite how to deal with this yet System.err.println("MMC5 wants RAM at " + utils.hex(addr)); return 0xffff; } } else if (addr >= 0x6000) { //wram int ramaddr = wrambank * 8192 + (addr - 0x6000); //System.err.println("reading prgram from " + utils.hex(ramaddr)); return prgram[ramaddr]; } else if (addr >= 0x5c00) { //exram return exram[addr - 0x5c00]; } else { switch (addr) { case 0x5015: //sound status if (soundchip != null) { return soundchip.status(); } return addr >> 8; case 0x5204: //irq status int stat = (irqPend ? 0x80 : 0) + (inFrame ? 0x40 : 0); if (irqPend) { irqPend = false; --cpu.interrupt; } return stat; case 0x5205: return (multiplier1 * multiplier2) & 0xff; case 0x5206: //multiplier return ((multiplier1 * multiplier2) >> 8) & 0xff; default: return addr >> 8; } } } public void setupPRG() { //does NOT support mapping RAM in yet! switch (prgMode) { default: case 0: setcpubank(32, 0, (prgregs[3] & 0x7f) >> 2); break; case 1: setcpubank(16, 16, (prgregs[3] & 0x7f) >> 1); setcpubank(16, 0, (prgregs[1] & 0x7f) >> 1); break; case 2: setcpubank(8, 24, prgregs[3] & 0x7f); setcpubank(8, 16, prgregs[2] & 0x7f); setcpubank(8, 8, (prgregs[1] & 0x7f) | 1); setcpubank(8, 0, (prgregs[1] & 0x7e)); break; case 3: setcpubank(8, 24, prgregs[3] & 0x7f); setcpubank(8, 16, prgregs[2] & 0x7f); setcpubank(8, 8, prgregs[1] & 0x7f); setcpubank(8, 0, prgregs[0] & 0x7f); break; } // System.err.println(prgMode); // utils.printarray(prgregs); // utils.printarray(prg_map); } public void setupCHR() { switch (chrMode) { default: case 0: setppubank(8, 0, chrregsA[7]); setppubankB(4, 0, chrregsB[3]); break; case 1: setppubank(4, 4, chrregsA[7]); setppubank(4, 0, chrregsA[3]); setppubankB(4, 0, chrregsB[3]); break; case 2: setppubank(2, 6, chrregsA[7]); setppubank(2, 4, chrregsA[5]); setppubank(2, 2, chrregsA[3]); setppubank(2, 0, chrregsA[1]); setppubankB(2, 2, chrregsB[3]); setppubankB(2, 0, chrregsB[1]); break; case 3: setppubank(1, 7, chrregsA[7]); setppubank(1, 6, chrregsA[6]); setppubank(1, 5, chrregsA[5]); setppubank(1, 4, chrregsA[4]); setppubank(1, 3, chrregsA[3]); setppubank(1, 2, chrregsA[2]); setppubank(1, 1, chrregsA[1]); setppubank(1, 0, chrregsA[0]); setppubankB(1, 3, chrregsB[3]); setppubankB(1, 2, chrregsB[2]); setppubankB(1, 1, chrregsB[1]); setppubankB(1, 0, chrregsB[0]); break; } } private void setppubank(int banksize, int bankpos, int banknum) { for (int i = 0; i < banksize; ++i) { chr_map[i + bankpos] = (1024 * ((banknum) + i)) % chrsize; } } private void setppubankB(int banksize, int bankpos, int banknum) { for (int i = 0; i < banksize; ++i) { chrmapB[i + bankpos] = (1024 * ((banknum) + i)) % chrsize; } } private void setcpubank(int banksize, int bankpos, int banknum) { for (int i = 0; i < banksize; ++i) { prg_map[i + bankpos] = (1024 * ((banknum * banksize) + i)) & (prgsize - 1); } } private int fetchcount, exlatch, lastfetch, prevfetch, prevprevfetch; private boolean spritemode = false; @Override public int ppuRead(final int addr) { //so how DO we detect which reads are which without //seeing the nametable reads? //well, as it turns out in the real NES, the MMC5 can in fact see everything //put on the PPU bus, whether or not the CS line is asserted for it. //must be something to do with 8x16 sprites, and with //the 34 reads per scanline of background //it reads 34 bg tiles (68 bytes) then 16 sprite tiles (32 bytes) if (addr < 0x2000) { // System.err.print("p"); //pattern table read if (++fetchcount == 3) { spritemode = true; //System.err.println(" sprites"); } if (spritemode) { return chr[chr_map[addr >> 10] + (addr & 1023)]; } else { //bg mode //System.err.print("t"); if (exramMode == 1) { if (exlatch == 2) { //fetch 3: tile bitmap a ++exlatch; return chr[((chrOr * 1024) | ((exram[lastfetch] & 0x3f) * 4096) | (addr & 4095)) % chr.length]; } else if (exlatch == 3) { //fetch 4: tile bitmap b (+ 8 bytes from tile bitmap a) exlatch = 0; return chr[((chrOr * 1024) | ((exram[lastfetch] & 0x3f) * 4096) | (addr & 4095)) % chr.length]; } } return chr[chrmapB[(addr >> 10) & 3] + (addr & 1023)]; } } else { // System.err.print("n"); //nametable read if (prevfetch == prevprevfetch && prevprevfetch == addr) { //last 3 fetches are the same and that's the signal //to increment the scan line counter //unfortunately I don't know how the MMC5 resets the counter when PPU is off yet incScanline(); exlatch = 0; } prevprevfetch = prevfetch; prevfetch = addr; spritemode = false; fetchcount = 0; // System.err.println(" bg"); if (exramMode == 1) { if (exlatch == 0) { //fetch 1: nametable fetch ++exlatch; lastfetch = addr & 0x3ff; } else if (exlatch == 1) { ++exlatch; //fetch 2: attribute table fetch int theone = exram[lastfetch]; return ((theone & 0xc0) >> 6) | ((theone & 0xc0) >> 4) | ((theone & 0xc0) >> 2) | (theone & 0xc0); } } return super.ppuRead(addr); } } public void incScanline() { if (!inFrame) { inFrame = true; irqCounter = 0; if (irqPend) { irqPend = false; --cpu.interrupt; } } else { if (irqCounter++ == scanctrLine) { irqPend = true; } if (irqPend && scanctrEnable) { ++cpu.interrupt; } } } public void setMirroring(int ntsetup, int[] exram) { //hook for the MMC5 switch (ntsetup & 3) { case 0: default: nt0 = pput0; break; case 1: nt0 = pput1; break; case 2: nt0 = exram; break; case 3: nt0 = fillnt; } ntsetup >>= 2; switch (ntsetup & 3) { case 0: default: nt1 = pput0; break; case 1: nt1 = pput1; break; case 2: nt1 = exram; break; case 3: nt1 = fillnt; break; } ntsetup >>= 2; switch (ntsetup & 3) { case 0: default: nt2 = pput0; break; case 1: nt2 = pput1; break; case 2: nt2 = exram; break; case 3: nt2 = fillnt; break; } ntsetup >>= 2; switch (ntsetup & 3) { case 0: default: nt3 = pput0; break; case 1: nt3 = pput1; break; case 2: nt3 = exram; break; case 3: nt3 = fillnt; break; } } }