Files
ParsonsMasterSystem2026/Core/Memory/SmsMemoryBus.cs

206 lines
7.3 KiB
C#

using Core.Interfaces;
using System;
namespace Core.Memory
{
public class SmsMemoryBus : IMemory
{
// SMS has 8KB of internal Work RAM
private readonly byte[] _workRam = new byte[0x2000];
// The entire game cartridge loaded into one big array
private byte[] _cartridgeRom = Array.Empty<byte>();
// The Paging Registers (Which 16KB chunk is in which slot?)
// Default startup state: Bank 0, 1, and 2 in order.
private int _romBank0 = 0;
private int _romBank1 = 1;
private int _romBank2 = 2;
// --- NEW: Cartridge RAM ---
private byte _mapperControl = 0; // 0xFFFC
private byte[] _cartridgeRam = new byte[0x8000]; // 32KB Max Cart RAM
// A flag to handle cartridges that don't use paging (like early 32KB games)
private bool _isCartridgeLoaded = false;
public void LoadCartridge(byte[] romData)
{
_cartridgeRom = romData;
_isCartridgeLoaded = true;
// Reset the mapper on boot
_romBank0 = 0;
_romBank1 = 1;
_romBank2 = 2;
// Clean the RAM
CleanRAMData();
}
public byte Read(ushort address)
{
if (address < 0x4000) // ROM Slot 0 (0x0000 - 0x3FFF)
{
// SMS Hardware Quirk: The first 1KB (0x0000 - 0x03FF) is NEVER paged.
if (address < 0x0400) return ReadFromCartridge(0, address);
return ReadFromCartridge(_romBank0, address);
}
if (address < 0x8000) // ROM Slot 1 (0x4000 - 0x7FFF)
{
return ReadFromCartridge(_romBank1, address & 0x3FFF);
}
if (address < 0xC000) // ROM Slot 2 (0x8000 - 0xBFFF)
{
// --- THE MISSING LINK: Read from Save RAM if enabled! ---
if ((_mapperControl & 0x08) != 0)
{
int ramBank = (_mapperControl & 0x04) != 0 ? 1 : 0;
int ramOffset = (ramBank * 0x4000) + (address & 0x3FFF);
return _cartridgeRam[ramOffset];
}
// Otherwise, read from the ROM as usual
return ReadFromCartridge(_romBank2, address & 0x3FFF);
}
// System RAM (0xC000 - 0xFFFF)
return _workRam[address & 0x1FFF];
}
//public byte Read(ushort address)
//{
// if (address < 0x4000) // ROM Slot 0 (0x0000 - 0x3FFF)
// {
// // SMS Hardware Quirk: The first 1KB (0x0000 - 0x03FF) is NEVER paged.
// // It is hardwired to Bank 0 so the interrupt handlers don't crash.
// if (address < 0x0400) return ReadFromCartridge(0, address);
// return ReadFromCartridge(_romBank0, address);
// }
// if (address < 0x8000) // ROM Slot 1 (0x4000 - 0x7FFF)
// {
// return ReadFromCartridge(_romBank1, address & 0x3FFF);
// }
// if (address < 0xC000) // ROM Slot 2 (0x8000 - 0xBFFF)
// {
// // Check if Cartridge RAM is enabled (Bit 3 of Mapper Control)
// if ((_mapperControl & 0x08) != 0)
// {
// // Bit 2 decides if we read the first 16KB half or the second 16KB half of the RAM chip
// int ramBank = (_mapperControl & 0x04) != 0 ? 1 : 0;
// int ramOffset = (ramBank * 0x4000) + (address & 0x3FFF);
// return _cartridgeRam[ramOffset];
// }
// return ReadFromCartridge(_romBank2, address & 0x3FFF);
// }
// // If we are here, we are in System RAM (0xC000 - 0xFFFF)
// // The & 0x1FFF handles the 8KB mirroring automatically!
// return _workRam[address & 0x1FFF];
//}
//public void Write(ushort address, byte value)
//{
// // --- 1. CARTRIDGE RAM (Save Data) ---
// if (address < 0xC000)
// {
// if (address >= 0x8000 && (_mapperControl & 0x08) != 0)
// {
// int ramBank = (_mapperControl & 0x04) != 0 ? 1 : 0;
// int ramOffset = (ramBank * 0x4000) + (address & 0x3FFF);
// _cartridgeRam[ramOffset] = value;
// }
// // You cannot write to a ROM cartridge!
// return;
// }
// // --- 2. SYSTEM RAM ---
// _workRam[address & 0x1FFF] = value;
// // --- 3. SEGA MAPPER (With Hardware Mirroring!) ---
// // By checking the masked address (address & 0x1FFF), we perfectly
// // catch writes to 0xFFFC-0xFFFF *and* their 0xDFFC-0xDFFF mirrors!
// int mapperAddress = address & 0x1FFF;
// if (mapperAddress == 0x1FFC) _mapperControl = value;
// else if (mapperAddress == 0x1FFD) _romBank0 = value;
// else if (mapperAddress == 0x1FFE) _romBank1 = value;
// else if (mapperAddress == 0x1FFF) _romBank2 = value;
//}
public void Write(ushort address, byte value)
{
if (address < 0xC000)
{
// Bypass the lock if they are trying to save their game!
if (address >= 0x8000 && (_mapperControl & 0x08) != 0)
{
int ramBank = (_mapperControl & 0x04) != 0 ? 1 : 0;
int ramOffset = (ramBank * 0x4000) + (address & 0x3FFF);
_cartridgeRam[ramOffset] = value;
return;
}
// You cannot write to a ROM cartridge!
return;
}
// Write to System RAM
_workRam[address & 0x1FFF] = value;
// --- THE SEGA MAPPER ---
if (address == 0xFFFC) // <-- ADD THIS
{
_mapperControl = value;
}
else if (address == 0xFFFD)
{
_romBank0 = value;
}
// Write to System RAM
_workRam[address & 0x1FFF] = value;
// --- THE SEGA MAPPER ---
// If the CPU wrote to the very top of memory, it is commanding a bank swap!
if (address == 0xFFFD)
{
_romBank0 = value;
}
else if (address == 0xFFFE)
{
_romBank1 = value;
}
else if (address == 0xFFFF)
{
_romBank2 = value;
}
}
private byte ReadFromCartridge(int bankIndex, int offset)
{
if (!_isCartridgeLoaded) return 0xFF; // Floating bus if no game
int absoluteAddress = (bankIndex * 0x4000) + offset;
// Safety check in case a game asks for a bank outside its file size
if (absoluteAddress >= _cartridgeRom.Length)
{
// Wrap around (mirrors the ROM if it's smaller than the requested bank)
absoluteAddress %= _cartridgeRom.Length;
}
return _cartridgeRom[absoluteAddress];
}
public void CleanRAMData()
{
Array.Clear(_workRam, 0, _workRam.Length);
Array.Clear(_cartridgeRam, 0, _cartridgeRam.Length);
}
}
}