melonDS/src/GBACart.cpp
Raphaël Zumer 22d11209b0 Split GBA Reset and Eject logic into two sets
This allows solving some crashes and provides more
flexibility in how GBA cartridges change state between
soft and hard resets. Since save states including GBA
data do not carry over the original save file path,
and the GBA cartridge is being reset along with the other
parts of the system, this is needed to avoid losing
the GBA state on reset following a state load, while
preserving the behavior where cartridges are ejected
when calling Stop().
2019-12-22 16:01:13 -05:00

774 lines
19 KiB
C++

/*
Copyright 2019 Arisotura, Raphaël Zumer
This file is part of melonDS.
melonDS is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
melonDS is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with melonDS. If not, see http://www.gnu.org/licenses/.
*/
#include <stdio.h>
#include <string.h>
#include "GBACart.h"
#include "CRC32.h"
#include "Platform.h"
namespace GBACart_SRAM
{
enum SaveType {
S_NULL,
S_EEPROM4K,
S_EEPROM64K,
S_SRAM256K,
S_FLASH512K,
S_FLASH1M
};
// from DeSmuME
struct FlashProperties
{
u8 state;
u8 cmd;
u8 device;
u8 manufacturer;
u8 bank;
};
u8* SRAM;
FILE* SRAMFile;
u32 SRAMLength;
SaveType SRAMType;
FlashProperties SRAMFlashState;
char SRAMPath[1024];
void (*WriteFunc)(u32 addr, u8 val);
void Write_Null(u32 addr, u8 val);
void Write_EEPROM(u32 addr, u8 val);
void Write_SRAM(u32 addr, u8 val);
void Write_Flash(u32 addr, u8 val);
bool Init()
{
SRAM = NULL;
SRAMFile = NULL;
return true;
}
void DeInit()
{
if (SRAMFile) fclose(SRAMFile);
if (SRAM) delete[] SRAM;
}
void Reset()
{
// do nothing, we don't want to clear GBA SRAM on reset
}
void Eject()
{
if (SRAMFile) fclose(SRAMFile);
if (SRAM) delete[] SRAM;
SRAM = NULL;
SRAMFile = NULL;
SRAMLength = 0;
SRAMType = S_NULL;
SRAMFlashState = {};
}
void DoSavestate(Savestate* file)
{
file->Section("GBCS"); // Game Boy [Advance] Cart Save
// logic mostly copied from NDSCart_SRAM
u32 oldlen = SRAMLength;
file->Var32(&SRAMLength);
if (SRAMLength != oldlen)
{
// reallocate save memory
if (oldlen) delete[] SRAM;
if (SRAMLength) SRAM = new u8[SRAMLength];
}
if (SRAMLength)
{
// fill save memory if data is present
file->VarArray(SRAM, SRAMLength);
}
else
{
// no save data, clear the current state
SRAMType = SaveType::S_NULL;
if (SRAMFile) fclose(SRAMFile);
SRAM = NULL;
SRAMFile = NULL;
return;
}
// persist some extra state info
file->Var8(&SRAMFlashState.bank);
file->Var8(&SRAMFlashState.cmd);
file->Var8(&SRAMFlashState.device);
file->Var8(&SRAMFlashState.manufacturer);
file->Var8(&SRAMFlashState.state);
file->Var8((u8*)&SRAMType);
}
void LoadSave(const char* path)
{
if (SRAM) delete[] SRAM;
strncpy(SRAMPath, path, 1023);
SRAMPath[1023] = '\0';
SRAMLength = 0;
FILE* f = Platform::OpenFile(SRAMPath, "r+b");
if (f)
{
fseek(f, 0, SEEK_END);
SRAMLength = (u32)ftell(f);
SRAM = new u8[SRAMLength];
fseek(f, 0, SEEK_SET);
fread(SRAM, SRAMLength, 1, f);
SRAMFile = f;
}
switch (SRAMLength)
{
case 512:
SRAMType = S_EEPROM4K;
WriteFunc = Write_EEPROM;
break;
case 8192:
SRAMType = S_EEPROM64K;
WriteFunc = Write_EEPROM;
break;
case 32768:
SRAMType = S_SRAM256K;
WriteFunc = Write_SRAM;
break;
case 65536:
SRAMType = S_FLASH512K;
WriteFunc = Write_Flash;
break;
case 128*1024:
SRAMType = S_FLASH1M;
WriteFunc = Write_Flash;
break;
default:
printf("!! BAD SAVE LENGTH %d\n", SRAMLength);
case 0:
SRAMType = S_NULL;
WriteFunc = Write_Null;
break;
}
if (SRAMType == S_FLASH512K)
{
// Panasonic 64K chip
SRAMFlashState.device = 0x1B;
SRAMFlashState.manufacturer = 0x32;
}
else if (SRAMType == S_FLASH1M)
{
// Sanyo 128K chip
SRAMFlashState.device = 0x13;
SRAMFlashState.manufacturer = 0x62;
}
}
void RelocateSave(const char* path, bool write)
{
if (!write)
{
LoadSave(path); // lazy
return;
}
strncpy(SRAMPath, path, 1023);
SRAMPath[1023] = '\0';
FILE *f = Platform::OpenFile(path, "r+b");
if (!f)
{
printf("GBACart_SRAM::RelocateSave: failed to create new file. fuck\n");
return;
}
SRAMFile = f;
fwrite(SRAM, SRAMLength, 1, SRAMFile);
}
// mostly ported from DeSmuME
u8 Read_Flash(u32 addr)
{
if (SRAMFlashState.cmd == 0) // no cmd
{
return *(u8*)&SRAM[addr + 0x10000 * SRAMFlashState.bank];
}
switch (SRAMFlashState.cmd)
{
case 0x90: // chip ID
if (addr == 0x0000) return SRAMFlashState.manufacturer;
if (addr == 0x0001) return SRAMFlashState.device;
break;
case 0xF0: // terminate command (TODO: break if non-Macronix chip and not at the end of an ID call?)
SRAMFlashState.state = 0;
SRAMFlashState.cmd = 0;
break;
case 0xA0: // write command
break; // ignore here, handled in Write_Flash()
case 0xB0: // bank switching (128K only)
break; // ignore here, handled in Write_Flash()
default:
printf("GBACart_SRAM::Read_Flash: unknown command 0x%02X @ 0x%04X\n", SRAMFlashState.cmd, addr);
break;
}
return 0xFF;
}
void Write_Null(u32 addr, u8 val) {}
void Write_EEPROM(u32 addr, u8 val)
{
// TODO: could be used in homebrew?
}
// mostly ported from DeSmuME
void Write_Flash(u32 addr, u8 val)
{
switch (SRAMFlashState.state)
{
case 0x00:
if (addr == 0x5555)
{
if (val == 0xF0)
{
// reset
SRAMFlashState.state = 0;
SRAMFlashState.cmd = 0;
return;
}
else if (val == 0xAA)
{
SRAMFlashState.state = 1;
return;
}
}
if (addr == 0x0000)
{
if (SRAMFlashState.cmd == 0xB0)
{
// bank switching
SRAMFlashState.bank = val;
SRAMFlashState.cmd = 0;
return;
}
}
break;
case 0x01:
if (addr == 0x2AAA && val == 0x55)
{
SRAMFlashState.state = 2;
return;
}
SRAMFlashState.state = 0;
break;
case 0x02:
if (addr == 0x5555)
{
// send command
switch (val)
{
case 0x80: // erase
SRAMFlashState.state = 0x80;
break;
case 0x90: // chip ID
SRAMFlashState.state = 0x90;
break;
case 0xA0: // write
SRAMFlashState.state = 0;
break;
default:
SRAMFlashState.state = 0;
break;
}
SRAMFlashState.cmd = val;
return;
}
SRAMFlashState.state = 0;
break;
// erase
case 0x80:
if (addr == 0x5555 && val == 0xAA)
{
SRAMFlashState.state = 0x81;
return;
}
SRAMFlashState.state = 0;
break;
case 0x81:
if (addr == 0x2AAA && val == 0x55)
{
SRAMFlashState.state = 0x82;
return;
}
SRAMFlashState.state = 0;
break;
case 0x82:
if (val == 0x30)
{
u32 start_addr = addr + 0x10000 * SRAMFlashState.bank;
memset((u8*)&SRAM[start_addr], 0xFF, 0x1000);
if (SRAMFile)
{
fseek(SRAMFile, start_addr, SEEK_SET);
fwrite((u8*)&SRAM[start_addr], 1, 0x1000, SRAMFile);
}
}
SRAMFlashState.state = 0;
SRAMFlashState.cmd = 0;
return;
// chip ID
case 0x90:
if (addr == 0x5555 && val == 0xAA)
{
SRAMFlashState.state = 0x91;
return;
}
SRAMFlashState.state = 0;
break;
case 0x91:
if (addr == 0x2AAA && val == 0x55)
{
SRAMFlashState.state = 0x92;
return;
}
SRAMFlashState.state = 0;
break;
case 0x92:
SRAMFlashState.state = 0;
SRAMFlashState.cmd = 0;
return;
default:
break;
}
if (SRAMFlashState.cmd == 0xA0) // write
{
Write_SRAM(addr + 0x10000 * SRAMFlashState.bank, val);
SRAMFlashState.state = 0;
SRAMFlashState.cmd = 0;
return;
}
printf("GBACart_SRAM::Write_Flash: unknown write 0x%02X @ 0x%04X (state: 0x%02X)\n",
val, addr, SRAMFlashState.state);
}
void Write_SRAM(u32 addr, u8 val)
{
u8 prev = *(u8*)&SRAM[addr];
if (prev != val)
{
*(u8*)&SRAM[addr] = val;
if (SRAMFile)
{
fseek(SRAMFile, addr, SEEK_SET);
fwrite((u8*)&SRAM[addr], 1, 1, SRAMFile);
}
}
}
u8 Read8(u32 addr)
{
if (SRAMType == S_NULL)
{
return 0xFF;
}
if (SRAMType == S_FLASH512K || SRAMType == S_FLASH1M)
{
return Read_Flash(addr);
}
return *(u8*)&SRAM[addr];
}
u16 Read16(u32 addr)
{
if (SRAMType == S_NULL)
{
return 0xFFFF;
}
if (SRAMType == S_FLASH512K || SRAMType == S_FLASH1M)
{
u16 val = Read_Flash(addr + 0) |
(Read_Flash(addr + 1) << 8);
return val;
}
return *(u16*)&SRAM[addr];
}
u32 Read32(u32 addr)
{
if (SRAMType == S_NULL)
{
return 0xFFFFFFFF;
}
if (SRAMType == S_FLASH512K || SRAMType == S_FLASH1M)
{
u32 val = Read_Flash(addr + 0) |
(Read_Flash(addr + 1) << 8) |
(Read_Flash(addr + 2) << 16) |
(Read_Flash(addr + 3) << 24);
return val;
}
return *(u32*)&SRAM[addr];
}
void Write8(u32 addr, u8 val)
{
u8 prev = *(u8*)&SRAM[addr];
WriteFunc(addr, val);
}
void Write16(u32 addr, u16 val)
{
u16 prev = *(u16*)&SRAM[addr];
WriteFunc(addr + 0, val & 0xFF);
WriteFunc(addr + 1, val >> 8 & 0xFF);
}
void Write32(u32 addr, u32 val)
{
u32 prev = *(u32*)&SRAM[addr];
WriteFunc(addr + 0, val & 0xFF);
WriteFunc(addr + 1, val >> 8 & 0xFF);
WriteFunc(addr + 2, val >> 16 & 0xFF);
WriteFunc(addr + 3, val >> 24 & 0xFF);
}
}
namespace GBACart
{
const char SOLAR_SENSOR_GAMECODES[10][5] =
{
"U3IJ", // Bokura no Taiyou - Taiyou Action RPG (Japan)
"U3IE", // Boktai - The Sun Is in Your Hand (USA)
"U3IP", // Boktai - The Sun Is in Your Hand (Europe)
"U32J", // Zoku Bokura no Taiyou - Taiyou Shounen Django (Japan)
"U32E", // Boktai 2 - Solar Boy Django (USA)
"U32P", // Boktai 2 - Solar Boy Django (Europe)
"U33J", // Shin Bokura no Taiyou - Gyakushuu no Sabata (Japan)
"A3IJ" // Boktai - The Sun Is in Your Hand (USA) (Sample)
};
bool CartInserted;
bool HasSolarSensor;
u8* CartROM;
u32 CartROMSize;
u32 CartCRC;
u32 CartID;
GPIO CartGPIO; // overridden GPIO parameters
bool Init()
{
if (!GBACart_SRAM::Init()) return false;
CartROM = NULL;
return true;
}
void DeInit()
{
if (CartROM) delete[] CartROM;
GBACart_SRAM::DeInit();
}
void Reset()
{
// Do not reset cartridge ROM.
// Prefer keeping the inserted cartridge on reset.
// This allows resetting a DS game without losing GBA state,
// and resetting to firmware without the slot being emptied.
// The Stop function will clear the cartridge state via Eject().
GBACart_SRAM::Reset();
GBACart_SolarSensor::Reset();
}
void Eject()
{
if (CartROM) delete[] CartROM;
CartInserted = false;
HasSolarSensor = false;
CartROM = NULL;
CartROMSize = 0;
CartCRC = NULL;
CartID = NULL;
CartGPIO = {};
GBACart_SRAM::Eject();
Reset();
}
void DoSavestate(Savestate* file)
{
file->Section("GBAC"); // Game Boy Advance Cartridge
// logic mostly copied from NDSCart
// first we need to reload the cart itself,
// since unlike with DS, it's not loaded in advance
file->Var32(&CartROMSize);
if (!CartROMSize) // no GBA cartridge state? nothing to do here
{
// do eject the cartridge if something is inserted
Eject();
return;
}
u32 oldCRC = CartCRC;
file->Var32(&CartCRC);
if (CartCRC != oldCRC)
{
// delete and reallocate ROM so that it is zero-padded to its full length
if (CartROM) delete[] CartROM;
CartROM = new u8[CartROMSize];
// clear the SRAM file handle; writes will not be committed
if (GBACart_SRAM::SRAMFile)
{
fclose(GBACart_SRAM::SRAMFile);
GBACart_SRAM::SRAMFile = NULL;
}
}
// only save/load the cartridge header
//
// GBA connectivity on DS mainly involves identifying the title currently
// inserted, reading save data, and issuing commands intercepted here
// (e.g. solar sensor signals). we don't know of any case where GBA ROM is
// read directly from DS software. therefore, it is more practical, both
// from the development and user experience perspectives, to avoid dealing
// with file dependencies, and store a small portion of ROM data that should
// satisfy the needs of all known software that reads from the GBA slot.
//
// note: in case of a state load, only the cartridge header is restored, but
// the rest of the ROM data is only cleared (zero-initialized) if the CRC
// differs. Therefore, loading the GBA cartridge associated with the save state
// in advance will maintain access to the full ROM contents.
file->VarArray(CartROM, 192);
CartInserted = true; // known, because CartROMSize > 0
file->Var32(&CartCRC);
file->Var32(&CartID);
file->Var8((u8*)&HasSolarSensor);
file->Var16(&CartGPIO.control);
file->Var16(&CartGPIO.data);
file->Var16(&CartGPIO.direction);
// now do the rest
GBACart_SRAM::DoSavestate(file);
if (HasSolarSensor) GBACart_SolarSensor::DoSavestate(file);
}
bool LoadROM(const char* path, const char* sram)
{
FILE* f = Platform::OpenFile(path, "rb");
if (!f)
{
return false;
}
if (CartInserted)
{
Reset();
}
fseek(f, 0, SEEK_END);
u32 len = (u32)ftell(f);
CartROMSize = 0x200;
while (CartROMSize < len)
CartROMSize <<= 1;
char gamecode[5] = { '\0' };
fseek(f, 0xAC, SEEK_SET);
fread(&gamecode, 1, 4, f);
printf("Game code: %s\n", gamecode);
for (int i = 0; i < sizeof(SOLAR_SENSOR_GAMECODES)/sizeof(SOLAR_SENSOR_GAMECODES[0]); i++)
{
if (strcmp(gamecode, SOLAR_SENSOR_GAMECODES[i]) == 0) HasSolarSensor = true;
}
if (HasSolarSensor)
{
printf("GBA solar sensor support detected!\n");
}
CartROM = new u8[CartROMSize];
memset(CartROM, 0, CartROMSize);
fseek(f, 0, SEEK_SET);
fread(CartROM, 1, len, f);
fclose(f);
CartCRC = CRC32(CartROM, CartROMSize);
printf("ROM CRC32: %08X\n", CartCRC);
CartInserted = true;
// save
printf("Save file: %s\n", sram);
GBACart_SRAM::LoadSave(sram);
return true;
}
void RelocateSave(const char* path, bool write)
{
// derp herp
GBACart_SRAM::RelocateSave(path, write);
}
// referenced from mGBA
void WriteGPIO(u32 addr, u16 val)
{
switch (addr)
{
case 0xC4:
CartGPIO.data &= ~CartGPIO.direction;
CartGPIO.data |= val & CartGPIO.direction;
if (HasSolarSensor) GBACart_SolarSensor::Process(&CartGPIO);
break;
case 0xC6:
CartGPIO.direction = val;
break;
case 0xC8:
CartGPIO.control = val;
break;
default:
printf("Unknown GBA GPIO write 0x%02X @ 0x%04X\n", val, addr);
}
// write the GPIO values in the ROM (if writable)
if (CartGPIO.control & 1)
{
*(u16*)&CartROM[0xC4] = CartGPIO.data;
*(u16*)&CartROM[0xC6] = CartGPIO.direction;
*(u16*)&CartROM[0xC8] = CartGPIO.control;
}
else
{
// GBATEK: "in write-only mode, reads return 00h (or [possibly] other data (...))"
// ambiguous, but mGBA sets ROM to 00h when switching to write-only, so do the same
*(u16*)&CartROM[0xC4] = 0;
*(u16*)&CartROM[0xC6] = 0;
*(u16*)&CartROM[0xC8] = 0;
}
}
}
namespace GBACart_SolarSensor
{
bool LightEdge;
u8 LightCounter;
u8 LightSample;
u8 LightLevel; // 0-10 range
// levels from mGBA
const int GBA_LUX_LEVELS[11] = { 0, 5, 11, 18, 27, 42, 62, 84, 109, 139, 183 };
#define LIGHT_VALUE (0xFF - (0x16 + GBA_LUX_LEVELS[LightLevel]))
void Reset()
{
LightEdge = false;
LightCounter = 0;
LightSample = 0xFF;
LightLevel = 0;
}
void DoSavestate(Savestate* file)
{
file->Var8((u8*)&LightEdge);
file->Var8(&LightCounter);
file->Var8(&LightSample);
file->Var8(&LightLevel);
}
void Process(GBACart::GPIO* gpio)
{
if (gpio->data & 4) return; // Boktai chip select
if (gpio->data & 2) // Reset
{
u8 prev = LightSample;
LightCounter = 0;
LightSample = LIGHT_VALUE;
printf("Solar sensor reset (sample: 0x%02X -> 0x%02X)\n", prev, LightSample);
}
if (gpio->data & 1 && LightEdge) LightCounter++;
LightEdge = !(gpio->data & 1);
bool sendBit = LightCounter >= LightSample;
if (gpio->control & 1)
{
gpio->data = (gpio->data & gpio->direction) | ((sendBit << 3) & ~gpio->direction & 0xF);
}
}
}