AxibugEmuOnline/AxibugEmuOnline.Client/Assets/Runtime/Mapper/MMC1.cs
2024-06-28 18:08:25 +08:00

171 lines
5.8 KiB
C#

using static AxibugEmuOnline.Client.UNES.Cartridge.VRAMMirroringMode;
namespace AxibugEmuOnline.Client.UNES.Mapper
{
[MapperDef(1)]
public class MMC1 : BaseMapper
{
// TODO: are MMC1 and MMC1A even different chip types?
public enum ChipType { MMC1, MMC1A, MMC1B, MMC1C }
public enum CHRBankingMode { Single, Double }
public enum PRGBankingMode { Switch32Kb, Switch16KbFixFirst, Switch16KbFixLast }
private readonly Cartridge.VRAMMirroringMode[] _mirroringModes = { Lower, Upper, Vertical, Horizontal };
private readonly ChipType _type;
private CHRBankingMode _chrBankingMode;
private PRGBankingMode _prgBankingMode;
private uint _serialData;
private int _serialPos;
private uint _control;
private readonly uint[] _chrBankOffsets = new uint[2];
private readonly uint[] _chrBanks = new uint[2];
private readonly uint[] _prgBankOffsets = new uint[2];
private uint _prgBank;
private bool _prgRAMEnabled;
private uint? _lastWritePC;
public MMC1(Emulator emulator) : this(emulator, ChipType.MMC1B)
{
}
public MMC1(Emulator emulator, ChipType chipType) : base(emulator)
{
_type = chipType;
if (chipType == ChipType.MMC1B) _prgRAMEnabled = true;
UpdateControl(0x0F);
_emulator.Cartridge.MirroringMode = Horizontal;
}
public override void InitializeMemoryMap(CPU cpu)
{
cpu.MapReadHandler(0x6000, 0x7FFF, address => _prgRAM[address - 0x6000]);
cpu.MapReadHandler(0x8000, 0xFFFF, address => _prgROM[_prgBankOffsets[(address - 0x8000) / 0x4000] + address % 0x4000]);
cpu.MapWriteHandler(0x6000, 0x7FFF, (address, val) =>
{
// PRG RAM is always enabled on MMC1A
if (_type == ChipType.MMC1A || _prgRAMEnabled)
_prgRAM[address - 0x6000] = val;
});
cpu.MapWriteHandler(0x8000, 0xFFFF, (address, val) =>
{
// Explicitly ignore the second write happening on consecutive cycles
// of an RMW instruction
var cycle = _emulator.CPU.PC;
if (cycle == _lastWritePC)
return;
_lastWritePC = cycle;
if ((val & 0x80) > 0)
{
_serialData = 0;
_serialPos = 0;
UpdateControl(_control | 0x0C);
}
else
{
_serialData |= (uint)((val & 0x1) << _serialPos);
_serialPos++;
if (_serialPos == 5)
{
// Address is incompletely decoded
address &= 0x6000;
if (address == 0x0000)
UpdateControl(_serialData);
else if (address == 0x2000)
UpdateCHRBank(0, _serialData);
else if (address == 0x4000)
UpdateCHRBank(1, _serialData);
else if (address == 0x6000)
UpdatePRGBank(_serialData);
_serialData = 0;
_serialPos = 0;
}
}
});
}
public override void InitializeMemoryMap(PPU ppu)
{
ppu.MapReadHandler(0x0000, 0x1FFF, address => _chrROM[_chrBankOffsets[address / 0x1000] + address % 0x1000]);
ppu.MapWriteHandler(0x0000, 0x1FFF, (address, val) => _chrROM[_chrBankOffsets[address / 0x1000] + address % 0x1000] = val);
}
private void UpdateControl(uint value)
{
_control = value;
_emulator.Cartridge.MirroringMode = _mirroringModes[value & 0x3];
_chrBankingMode = (CHRBankingMode)((value >> 4) & 0x1);
var prgMode = (value >> 2) & 0x3;
// Both 0 and 1 are 32Kb switch
if (prgMode == 0) prgMode = 1;
_prgBankingMode = (PRGBankingMode)(prgMode - 1);
UpdateCHRBank(1, _chrBanks[1]);
UpdateCHRBank(0, _chrBanks[0]);
UpdatePRGBank(_prgBank);
}
private void UpdatePRGBank(uint value)
{
_prgBank = value;
_prgRAMEnabled = (value & 0x10) == 0;
value &= 0xF;
switch (_prgBankingMode)
{
case PRGBankingMode.Switch32Kb:
value >>= 1;
value *= 0x4000;
_prgBankOffsets[0] = value;
_prgBankOffsets[1] = value + 0x4000;
break;
case PRGBankingMode.Switch16KbFixFirst:
_prgBankOffsets[0] = 0;
_prgBankOffsets[1] = value * 0x4000;
break;
case PRGBankingMode.Switch16KbFixLast:
_prgBankOffsets[0] = value * 0x4000;
_prgBankOffsets[1] = _lastBankOffset;
break;
}
}
private void UpdateCHRBank(uint bank, uint value)
{
_chrBanks[bank] = value;
// TODO FIXME: I feel like this branch should only be taken
// when bank == 0, but this breaks Final Fantasy
// When can banking mode change without UpdateCHRBank being called?
if (_chrBankingMode == CHRBankingMode.Single)
{
value = _chrBanks[0];
value >>= 1;
value *= 0x1000;
_chrBankOffsets[0] = value;
_chrBankOffsets[1] = value + 0x1000;
}
else
{
_chrBankOffsets[bank] = value * 0x1000;
}
}
}
}