#pragma once #include "stdafx.h" #include "BaseMapper.h" #include "PPU.h" #include "MMC5Audio.h" class MMC5 : public BaseMapper { private: static constexpr int ExRamSize = 0x400; static constexpr uint8_t NtWorkRamIndex = 4; static constexpr uint8_t NtEmptyIndex = 2; static constexpr uint8_t NtFillModeIndex = 3; unique_ptr _audio; uint8_t _prgRamProtect1; uint8_t _prgRamProtect2; uint8_t _fillModeTile; uint8_t _fillModeColor; bool _verticalSplitEnabled; bool _verticalSplitRightSide; uint8_t _verticalSplitDelimiterTile; uint8_t _verticalSplitScroll; uint8_t _verticalSplitBank; bool _splitInSplitRegion; uint32_t _splitVerticalScroll; uint32_t _splitTile; int32_t _splitTileNumber; uint8_t _multiplierValue1; uint8_t _multiplierValue2; uint8_t _nametableMapping; uint8_t _extendedRamMode; //Extended attribute mode fields (used when _extendedRamMode == 1) uint16_t _exAttributeLastNametableFetch; int8_t _exAttrLastFetchCounter; uint8_t _exAttrSelectedChrBank; uint8_t _prgMode; uint8_t _prgBanks[5]; //CHR-related fields uint8_t _chrMode; uint8_t _chrUpperBits; uint16_t _chrBanks[12]; uint16_t _lastChrReg; bool _spriteFetch; bool _largeSprites; //IRQ counter related fields uint8_t _irqCounterTarget; bool _irqEnabled; int16_t _previousScanline; uint8_t _irqCounter; bool _irqPending; bool _ppuInFrame; MemoryOperationType _lastVramOperationType; void SwitchPrgBank(uint16_t reg, uint8_t value) { _prgBanks[reg - 0x5113] = value; UpdatePrgBanks(); } void GetCpuBankInfo(uint16_t reg, uint8_t &bankNumber, PrgMemoryType &memoryType, uint8_t &accessType) { bankNumber = _prgBanks[reg-0x5113]; memoryType = PrgMemoryType::PrgRom; if((((bankNumber & 0x80) == 0x00) && reg != 0x5117) || reg == 0x5113) { bankNumber &= 0x07; accessType = MemoryAccessType::Read; if(_prgRamProtect1 == 0x02 && _prgRamProtect2 == 0x01) { accessType |= MemoryAccessType::Write; } // WRAM/SRAM mirroring logic (only supports existing/known licensed MMC5 boards) // Bank number // 0 1 2 3 4 5 6 7 // -------------------------- // None : - - - - - - - - // 1x 8kb : 0 0 0 0 - - - - // 2x 8kb : 0 0 0 0 1 1 1 1 // 1x 32kb : 0 1 2 3 - - - - int32_t realWorkRamSize = _workRamSize - MMC5::ExRamSize; if(IsNes20() || _romInfo.IsInDatabase) { memoryType = PrgMemoryType::WorkRam; if(HasBattery() && (bankNumber <= 3 || _saveRamSize > 0x2000)) { memoryType = PrgMemoryType::SaveRam; } if(_saveRamSize + realWorkRamSize != 0x4000 && bankNumber >= 4) { //When not 2x 8kb (=16kb), banks 4/5/6/7 select the empty socket and return open bus accessType = MemoryAccessType::NoAccess; } } else { memoryType = HasBattery() ? PrgMemoryType::SaveRam : PrgMemoryType::WorkRam; } if(memoryType == PrgMemoryType::WorkRam) { //Properly mirror work ram (by ignoring the extra 1kb ExRAM section) bankNumber &= (realWorkRamSize / 0x2000) - 1; if(_workRamSize == MMC5::ExRamSize) { accessType = MemoryAccessType::NoAccess; } } } else { accessType = MemoryAccessType::Read; bankNumber &= 0x7F; } } void UpdatePrgBanks() { uint8_t value; PrgMemoryType memoryType; uint8_t accessType; GetCpuBankInfo(0x5113, value, memoryType, accessType); SetCpuMemoryMapping(0x6000, 0x7FFF, value, memoryType, accessType); //PRG Bank 0 //Mode 0,1,2 - Ignored //Mode 3 - Select an 8KB PRG bank at $8000-$9FFF if(_prgMode == 3) { GetCpuBankInfo(0x5114, value, memoryType, accessType); SetCpuMemoryMapping(0x8000, 0x9FFF, value, memoryType, accessType); } //PRG Bank 1 //Mode 0 - Ignored //Mode 1,2 - Select a 16KB PRG bank at $8000-$BFFF (ignore bottom bit) //Mode 3 - Select an 8KB PRG bank at $A000-$BFFF GetCpuBankInfo(0x5115, value, memoryType, accessType); if(_prgMode == 1 || _prgMode == 2) { SetCpuMemoryMapping(0x8000, 0xBFFF, value & 0xFE, memoryType, accessType); } else if(_prgMode == 3) { SetCpuMemoryMapping(0xA000, 0xBFFF, value, memoryType, accessType); } //Mode 0,1 - Ignored //Mode 2,3 - Select an 8KB PRG bank at $C000-$DFFF if(_prgMode == 2 || _prgMode == 3) { GetCpuBankInfo(0x5116, value, memoryType, accessType); SetCpuMemoryMapping(0xC000, 0xDFFF, value, memoryType, accessType); } //Mode 0 - Select a 32KB PRG ROM bank at $8000-$FFFF (ignore bottom 2 bits) //Mode 1 - Select a 16KB PRG ROM bank at $C000-$FFFF (ignore bottom bit) //Mode 2,3 - Select an 8KB PRG ROM bank at $E000-$FFFF GetCpuBankInfo(0x5117, value, memoryType, accessType); if(_prgMode == 0) { SetCpuMemoryMapping(0x8000, 0xFFFF, value & 0x7C, memoryType, accessType); } else if(_prgMode == 1) { SetCpuMemoryMapping(0xC000, 0xFFFF, value & 0x7E, memoryType, accessType); } else if(_prgMode == 2 || _prgMode == 3) { SetCpuMemoryMapping(0xE000, 0xFFFF, value & 0x7F, memoryType, accessType); } } void SwitchChrBank(uint16_t reg, uint8_t value) { _chrBanks[reg - 0x5120] = value | (_chrUpperBits << 8); if(_largeSprites) { _lastChrReg = reg; } else { //Using 8x8 sprites resets the last written to bank logic //Unsure about this part (hasn't been tested specifically, but would make sense) _lastChrReg = 0; } UpdateChrBanks(); } void UpdateChrBanks() { if(!_largeSprites) { //Using 8x8 sprites resets the last written to bank logic _lastChrReg = 0; } bool chrA = !_largeSprites || (_largeSprites && _spriteFetch) || (_lastVramOperationType != MemoryOperationType::PpuRenderingRead && _lastChrReg <= 0x5127); if(_chrMode == 0) { SelectChrPage8x(0, _chrBanks[chrA ? 0x07 : 0x0B] << 3); } else if(_chrMode == 1) { SelectChrPage4x(0, _chrBanks[chrA ? 0x03 : 0x0B] << 2); SelectChrPage4x(1, _chrBanks[chrA ? 0x07 : 0x0B] << 2); } else if(_chrMode == 2) { SelectChrPage2x(0, _chrBanks[chrA ? 0x01 : 0x09] << 1); SelectChrPage2x(1, _chrBanks[chrA ? 0x03 : 0x0B] << 1); SelectChrPage2x(2, _chrBanks[chrA ? 0x05 : 0x09] << 1); SelectChrPage2x(3, _chrBanks[chrA ? 0x07 : 0x0B] << 1); } else if(_chrMode == 3) { SelectCHRPage(0, _chrBanks[chrA ? 0x00 : 0x08]); SelectCHRPage(1, _chrBanks[chrA ? 0x01 : 0x09]); SelectCHRPage(2, _chrBanks[chrA ? 0x02 : 0x0A]); SelectCHRPage(3, _chrBanks[chrA ? 0x03 : 0x0B]); SelectCHRPage(4, _chrBanks[chrA ? 0x04 : 0x08]); SelectCHRPage(5, _chrBanks[chrA ? 0x05 : 0x09]); SelectCHRPage(6, _chrBanks[chrA ? 0x06 : 0x0A]); SelectCHRPage(7, _chrBanks[chrA ? 0x07 : 0x0B]); } } void ProcessCpuClock() override { _audio->Clock(); } virtual void NotifyVRAMAddressChange(uint16_t addr) override { PPU* ppu = _console->GetPpu(); if(ppu->GetControlFlags().BackgroundEnabled || ppu->GetControlFlags().SpritesEnabled) { int16_t currentScanline = ppu->GetCurrentScanline(); if(currentScanline != _previousScanline) { if(currentScanline >= 239 || currentScanline < 0) { _ppuInFrame = false; } else { if(!_ppuInFrame) { _ppuInFrame = true; _irqCounter = 0; _irqPending = false; _console->GetCpu()->ClearIrqSource(IRQSource::External); } else { _irqCounter++; if(_irqCounter == _irqCounterTarget) { _irqPending = true; if(_irqEnabled) { _console->GetCpu()->SetIrqSource(IRQSource::External); } } } } _previousScanline = currentScanline; } } else { _ppuInFrame = false; } } void SetNametableMapping(uint8_t value) { _nametableMapping = value; uint8_t nametables[4] = { 0, //"0 - On-board VRAM page 0" 1, //"1 - On-board VRAM page 1" _extendedRamMode <= 1 ? NtWorkRamIndex : NtEmptyIndex, //"2 - Internal Expansion RAM, only if the Extended RAM mode allows it ($5104 is 00/01); otherwise, the nametable will read as all zeros," NtFillModeIndex //"3 - Fill-mode data" }; for(int i = 0; i < 4; i++) { uint8_t nametableId = nametables[(value >> (i * 2)) & 0x03]; if(nametableId == NtWorkRamIndex) { SetPpuMemoryMapping(0x2000+i*0x400, 0x2000+i*0x400+0x3FF, _workRam, MemoryAccessType::ReadWrite); } else { SetNametable(i, nametableId); } } } void SetExtendedRamMode(uint8_t mode) { _extendedRamMode = mode; MemoryAccessType accessType; if(_extendedRamMode <= 1) { //"Mode 0/1 - Not readable (returns open bus), can only be written while the PPU is rendering (otherwise, 0 is written)" //See overridden WriteRam function for implementation accessType = MemoryAccessType::Write; } else if(_extendedRamMode == 2) { //"Mode 2 - Readable and writable" accessType = MemoryAccessType::ReadWrite; } else { //"Mode 3 - Read-only" accessType = MemoryAccessType::Read; } SetCpuMemoryMapping(0x5C00, 0x5FFF, PrgMemoryType::WorkRam, _workRamSize - MMC5::ExRamSize, accessType); SetNametableMapping(_nametableMapping); } void SetFillModeTile(uint8_t tile) { _fillModeTile = tile; memset(GetNametable(NtFillModeIndex), tile, 32 * 30); //32 tiles per row, 30 rows } void SetFillModeColor(uint8_t color) { _fillModeColor = color; uint8_t attributeByte = color | color << 2 | color << 4 | color << 6; memset(GetNametable(NtFillModeIndex) + 32 * 30, attributeByte, 64); //Attribute table is 64 bytes } bool IsSpriteFetch() { return _console->GetPpu()->GetCurrentCycle() >= 257 && _console->GetPpu()->GetCurrentCycle() < 321; } protected: virtual uint16_t GetPRGPageSize() override { return 0x2000; } virtual uint16_t GetCHRPageSize() override { return 0x400; } virtual uint16_t RegisterStartAddress() override { return 0x5000; } virtual uint16_t RegisterEndAddress() override { return 0x5206; } virtual uint32_t GetSaveRamPageSize() override { return 0x2000; } virtual uint32_t GetWorkRamPageSize() override { return 0x2000; } virtual bool ForceSaveRamSize() override { return true; } virtual bool ForceWorkRamSize() override { return true; } virtual uint32_t GetSaveRamSize() override { if(IsNes20()) { return _romInfo.NesHeader.GetSaveRamSize(); } else if(_romInfo.IsInDatabase) { return _romInfo.DatabaseInfo.SaveRamSize; } else { //Emulate as if a single 64k block of work/save ram existed return _romInfo.HasBattery ? 0x10000 : 0; } } virtual uint32_t GetWorkRamSize() override { if(IsNes20()) { return _romInfo.NesHeader.GetWorkRamSize() + MMC5::ExRamSize; } else if(_romInfo.IsInDatabase) { return _romInfo.DatabaseInfo.WorkRamSize + MMC5::ExRamSize; } else { //Emulate as if a single 64k block of work/save ram existed (+ 1kb of ExRAM) return (_romInfo.HasBattery ? 0 : 0x10000) + MMC5::ExRamSize; } } virtual bool AllowRegisterRead() override { return true; } virtual void InitMapper() override { _audio.reset(new MMC5Audio(_console)); _chrMode = 0; _prgRamProtect1 = 0; _prgRamProtect2 = 0; _extendedRamMode = 0; _nametableMapping = 0; _fillModeColor = 0; _fillModeTile = 0; _verticalSplitScroll = 0; _verticalSplitBank = 0; _verticalSplitEnabled = false; _verticalSplitDelimiterTile = 0; _verticalSplitRightSide = false; _multiplierValue1 = 0; _multiplierValue2 = 0; _chrUpperBits = 0; memset(_chrBanks, 0, sizeof(_chrBanks)); _lastChrReg = 0; _spriteFetch = false; _largeSprites = false; _exAttrLastFetchCounter = 0; _exAttributeLastNametableFetch = 0; _exAttrSelectedChrBank = 0; _irqPending = false; _irqCounterTarget = 0; _irqCounter = 0; _irqEnabled = false; _previousScanline = -1; _ppuInFrame = false; _lastVramOperationType = MemoryOperationType::Read; _splitInSplitRegion = false; _splitVerticalScroll = 0; _splitTile = 0; _splitTileNumber = -1; memset(GetNametable(NtEmptyIndex), 0, BaseMapper::NametableSize); SetExtendedRamMode(0); //"Additionally, Romance of the 3 Kingdoms 2 seems to expect it to be in 8k PRG mode ($5100 = $03)." WriteRegister(0x5100, 0x03); //"Games seem to expect $5117 to be $FF on powerup (last PRG page swapped in)." WriteRegister(0x5117, 0xFF); } void StreamState(bool saving) override { BaseMapper::StreamState(saving); ArrayInfo prgBanks = { _prgBanks, 5 }; ArrayInfo chrBanks = { _chrBanks, 12 }; SnapshotInfo audio{ _audio.get() }; Stream(_prgRamProtect1, _prgRamProtect2, _fillModeTile, _fillModeColor, _verticalSplitEnabled, _verticalSplitRightSide, _verticalSplitDelimiterTile, _verticalSplitScroll, _verticalSplitBank, _multiplierValue1, _multiplierValue2, _nametableMapping, _extendedRamMode, _exAttributeLastNametableFetch, _exAttrLastFetchCounter, _exAttrSelectedChrBank, _prgMode, prgBanks, _chrMode, _chrUpperBits, chrBanks, _lastChrReg, _spriteFetch, _largeSprites, _irqCounterTarget, _irqEnabled, _previousScanline, _irqCounter, _irqPending, _ppuInFrame, audio, _splitInSplitRegion, _splitVerticalScroll, _splitTile, _splitTileNumber, _lastVramOperationType); if(!saving) { UpdatePrgBanks(); SetNametableMapping(_nametableMapping); } } virtual void WriteRAM(uint16_t addr, uint8_t value) override { if(addr >= 0x5C00 && addr <= 0x5FFF && _extendedRamMode <= 1) { PPUControlFlags flags = _console->GetPpu()->GetControlFlags(); if(!flags.BackgroundEnabled && !flags.SpritesEnabled) { //Expansion RAM ($5C00-$5FFF, read/write) //Mode 0/1 - Not readable (returns open bus), can only be written while the PPU is rendering (otherwise, 0 is written) value = 0; } } BaseMapper::WriteRAM(addr, value); } virtual uint8_t MapperReadVRAM(uint16_t addr, MemoryOperationType memoryOperationType) override { PPU* ppu = _console->GetPpu(); if(_spriteFetch != IsSpriteFetch() || _largeSprites != ppu->GetControlFlags().LargeSprites || _lastVramOperationType != memoryOperationType) { _lastVramOperationType = memoryOperationType; _spriteFetch = IsSpriteFetch(); _largeSprites = ppu->GetControlFlags().LargeSprites; UpdateChrBanks(); } if(_extendedRamMode <= 1 && _verticalSplitEnabled && memoryOperationType == MemoryOperationType::PpuRenderingRead) { uint32_t cycle = ppu->GetCurrentCycle(); int32_t scanline = ppu->GetCurrentScanline(); if(cycle == 321) { _splitTileNumber = -1; if(scanline == -1) { _splitVerticalScroll = _verticalSplitScroll; } else if(scanline < 240) { _splitVerticalScroll++; } if(_splitVerticalScroll >= 240) { _splitVerticalScroll -= 240; } } if((cycle - 1) % 8 == 0 && cycle != 337) { _splitTileNumber++; } if(cycle < 256 || cycle >= 321) { if(addr >= 0x2000) { if((addr & 0x3FF) < 0x3C0) { if((_verticalSplitRightSide && _splitTileNumber >= _verticalSplitDelimiterTile) || (!_verticalSplitRightSide && _splitTileNumber < _verticalSplitDelimiterTile)) { //Split region _splitInSplitRegion = true; _splitTile = ((_splitVerticalScroll & 0xF8) << 2) | _splitTileNumber; return InternalReadRam(0x5C00 + _splitTile); } else { //Regular data, result can get modified by ex ram mode code below _splitInSplitRegion = false; } } else if(_splitInSplitRegion) { return InternalReadRam(0x5FC0 + ((_splitTile >> 4) & ~0x07) + ((_splitTile & 0x3F) >> 2)); } } else if(_splitInSplitRegion) { return _chrRom[(_verticalSplitBank % (GetCHRPageCount() / 4)) * 0x1000 + (((addr & ~0x07) | (_splitVerticalScroll & 0x07)) & 0xFFF)]; } } } if(_extendedRamMode == 1 && !IsSpriteFetch() && memoryOperationType == MemoryOperationType::PpuRenderingRead) { //"In Mode 1, nametable fetches are processed normally, and can come from CIRAM nametables, fill mode, or even Expansion RAM, but attribute fetches are replaced by data from Expansion RAM." //"Each byte of Expansion RAM is used to enhance the tile at the corresponding address in every nametable" //When fetching NT data, we set a flag and then alter the VRAM values read by the PPU on the following 3 cycles (palette, tile low/high byte) if(addr >= 0x2000 && (addr & 0x3FF) < 0x3C0) { //Nametable fetches _exAttributeLastNametableFetch = addr & 0x03FF; _exAttrLastFetchCounter = 3; } else if(_exAttrLastFetchCounter > 0) { //Attribute fetches _exAttrLastFetchCounter--; switch(_exAttrLastFetchCounter) { case 2: { //PPU palette fetch //Check work ram (expansion ram) to see which tile/palette to use //Use InternalReadRam to bypass the fact that the ram is supposed to be write-only in mode 0/1 uint8_t value = InternalReadRam(0x5C00 + _exAttributeLastNametableFetch); //"The pattern fetches ignore the standard CHR banking bits, and instead use the top two bits of $5130 and the bottom 6 bits from Expansion RAM to choose a 4KB bank to select the tile from." _exAttrSelectedChrBank = ((value & 0x3F) | (_chrUpperBits << 6)) % (_chrRomSize / 0x1000); //Return a byte containing the same palette 4 times - this allows the PPU to select the right palette no matter the shift value uint8_t palette = (value & 0xC0) >> 6; return palette | palette << 2 | palette << 4 | palette << 6; } case 1: case 0: //PPU tile data fetch (high byte & low byte) return _chrRom[_exAttrSelectedChrBank * 0x1000 + (addr & 0xFFF)]; } } } return BaseMapper::MapperReadVRAM(addr, memoryOperationType); } void WriteRegister(uint16_t addr, uint8_t value) override { if(addr >= 0x5113 && addr <= 0x5117) { SwitchPrgBank(addr, value); } else if(addr >= 0x5120 && addr <= 0x512B) { SwitchChrBank(addr, value); } else { switch(addr) { case 0x5000: case 0x5001: case 0x5002: case 0x5003: case 0x5004: case 0x5005: case 0x5006: case 0x5007: case 0x5010: case 0x5011: case 0x5015: _audio->WriteRegister(addr, value); break; case 0x5100: _prgMode = value & 0x03; UpdatePrgBanks(); break; case 0x5101: _chrMode = value & 0x03; UpdateChrBanks(); break; case 0x5102: _prgRamProtect1 = value & 0x03; UpdatePrgBanks(); break; case 0x5103: _prgRamProtect2 = value & 0x03; UpdatePrgBanks(); break; case 0x5104: SetExtendedRamMode(value & 0x03); break; case 0x5105: SetNametableMapping(value); break; case 0x5106: SetFillModeTile(value); break; case 0x5107: SetFillModeColor(value & 0x03); break; case 0x5130: _chrUpperBits = value & 0x03; break; case 0x5200: _verticalSplitEnabled = (value & 0x80) == 0x80; _verticalSplitRightSide = (value & 0x40) == 0x40; _verticalSplitDelimiterTile = (value & 0x1F); break; case 0x5201: _verticalSplitScroll = value; break; case 0x5202: _verticalSplitBank = value; break; case 0x5203: _irqCounterTarget = value; break; case 0x5204: _irqEnabled = (value & 0x80) == 0x80; if(!_irqEnabled) { _console->GetCpu()->ClearIrqSource(IRQSource::External); } else if(_irqEnabled && _irqPending) { _console->GetCpu()->SetIrqSource(IRQSource::External); } break; case 0x5205: _multiplierValue1 = value; break; case 0x5206: _multiplierValue2 = value; break; default: break; } } } uint8_t ReadRegister(uint16_t addr) override { switch(addr) { case 0x5010: case 0x5015: return _audio->ReadRegister(addr); case 0x5204: { uint8_t value = (_ppuInFrame ? 0x40 : 0x00) | (_irqPending ? 0x80 : 0x00); _irqPending = false; _console->GetCpu()->ClearIrqSource(IRQSource::External); return value; } case 0x5205: return (_multiplierValue1*_multiplierValue2) & 0xFF; case 0x5206: return (_multiplierValue1*_multiplierValue2) >> 8; } return _console->GetMemoryManager()->GetOpenBus(); } public: bool IsExtendedAttributes() { return _extendedRamMode == 1; } uint8_t GetExAttributeNtPalette(uint16_t ntAddr) { ntAddr &= 0x3FF; uint8_t value = InternalReadRam(0x5C00 + ntAddr); return (value & 0xC0) >> 6; } uint32_t GetExAttributeAbsoluteTileAddr(uint16_t ntAddr, uint16_t chrAddr) { ntAddr &= 0x3FF; uint8_t value = InternalReadRam(0x5C00 + ntAddr); //"The pattern fetches ignore the standard CHR banking bits, and instead use the top two bits of $5130 and the bottom 6 bits from Expansion RAM to choose a 4KB bank to select the tile from." uint16_t chrBank = ((value & 0x3F) | (_chrUpperBits << 6)) % (_chrRomSize / 0x1000); return chrBank * 0x1000 + (chrAddr & 0xFFF); } uint8_t GetExAttributeTileData(uint16_t ntAddr, uint16_t chrAddr) { return _chrRom[GetExAttributeAbsoluteTileAddr(ntAddr, chrAddr)]; } };