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