You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
358 lines
12 KiB
358 lines
12 KiB
/* GameBoyInstance.cpp |
|
* |
|
* A GameBoy core for Plip. |
|
*/ |
|
|
|
#include <cstring> |
|
#include <sstream> |
|
|
|
#include "../../PlipEmulationException.h" |
|
#include "../../PlipInitializationException.h" |
|
#include "../../PlipIo.h" |
|
#include "../../PlipUtility.h" |
|
|
|
#include "GameBoyInstance.h" |
|
|
|
#define READ_INPUT(idx) do { if(m_input->GetInput(idx).digital) m_keypad |= 1 << idx; } while(0) |
|
|
|
namespace Plip::Core::GameBoy { |
|
GameBoyInstance::GameBoyInstance(PlipAudio *audio, PlipInput *input, PlipVideo *video, PlipConfig *config) |
|
: PlipCore(audio, input, video, config) { |
|
using io = Plip::PlipIo; |
|
|
|
// Initialize input. |
|
RegisterInput(); |
|
m_memory->SetByte(m_regJoypad, 0b11111111); |
|
|
|
// Load the boot ROM. |
|
m_bootRomPath = m_config->GetOption("bootRom"); |
|
if(m_bootRomPath == m_config->empty) |
|
throw PlipInitializationException("Boot ROM must be specified in the core config."); |
|
if(!io::FileExists(m_bootRomPath)) |
|
throw PlipInitializationException("Specified Boot ROM could not be found."); |
|
|
|
auto size = io::GetSize(m_bootRomPath); |
|
auto data = io::ReadFile(m_bootRomPath, size); |
|
m_bootRom = new Plip::PlipMemoryRom(data.data(), size); |
|
|
|
// Initialize framebuffers and video subsystem. |
|
m_video->Resize(m_screenWidth, m_screenHeight); |
|
m_videoFmt = Plip::PlipVideo::GetFormatInfo(video->GetFormat()); |
|
m_videoBufferSize = m_videoFmt.pixelWidth * m_screenWidth * m_screenWidth; |
|
m_videoBuffer = new uint8_t[m_videoBufferSize]; |
|
m_videoMode = m_videoModeOamSearch; |
|
|
|
m_spriteList = new uint8_t[m_maxSpritesPerScanline] {}; |
|
m_spriteListSorted = new uint8_t[m_maxSpritesPerScanline] {}; |
|
|
|
// Paint the frame buffer white. |
|
memset(m_videoBuffer, 0xFF, m_videoBufferSize); |
|
|
|
// Initialize system RAM. |
|
m_videoRam = new Plip::PlipMemoryRam(0x2000); |
|
m_workRam = new Plip::PlipMemoryRam(0x2000); |
|
m_oam = new Plip::PlipMemoryRam(0xA0); |
|
m_unusable = new Plip::PlipMemoryRom(&m_unusableContents, 0x60); |
|
m_ioRegisters = new Plip::PlipMemoryRam(0x80); |
|
m_highRam = new Plip::PlipMemoryRam(0x80); |
|
|
|
m_memory->AssignBlock(m_videoRam, m_addrVideoRam); |
|
m_memory->AssignBlock(m_workRam, m_addrWorkRam); |
|
m_memory->AssignBlock(m_workRam, m_addrEchoRam); // ECHO RAM |
|
m_memory->AssignBlock(m_oam, m_addrOam); |
|
m_memory->AssignBlock(m_unusable, m_addrUnusable); |
|
m_memory->AssignBlock(m_ioRegisters, m_addrRegisters); |
|
m_memory->AssignBlock(m_highRam, m_addrHighRam); |
|
m_memory->SetUnprivilegedValue(0xFF); |
|
|
|
// Initialize CPU. |
|
m_cpu = new Cpu::SharpLr35902(BaseClockRate / 4, m_memory); |
|
m_cycleTime = m_cpu->GetCycleTime(); |
|
} |
|
|
|
GameBoyInstance::~GameBoyInstance() { |
|
delete m_cpu; |
|
delete m_spriteList; |
|
delete m_videoBuffer; |
|
} |
|
|
|
inline void GameBoyInstance::BootRomFlagHandler() { |
|
if(m_lastWrite.address != m_addrRegisters + m_addrBootRomDisable) return; |
|
if(m_lastWrite.value == 0) return; |
|
|
|
// Swap the boot ROM out for the cartridge ROM, then update the register. |
|
m_bootRomFlag = true; |
|
m_memory->AssignBlock(m_rom, 0x0000, 0x0000, 0x0100); |
|
m_ioRegisters->SetByte(m_addrBootRomDisable, 1); |
|
} |
|
|
|
void GameBoyInstance::ClearBreakpoint() { |
|
m_bp = 0xFFFFFFFF; |
|
} |
|
|
|
void GameBoyInstance::Delta(long ns) { |
|
m_cycleRemaining += ns; |
|
m_dotCyclesRemaining += m_dotsPerCycle; |
|
ReadInput(); |
|
|
|
do { |
|
// Run a single CPU cycle. |
|
m_memory->ClearLastWrite(); |
|
m_cpu->Cycle(); |
|
m_lastWrite = m_memory->GetLastWrite(); |
|
|
|
// Perform DMA transfer (if applicable). |
|
if(m_lastWrite.address == m_addrRegisters + m_regOamDmaTransfer && m_videoOamDmaTransferIdx < 0) |
|
VideoOamDmaTransferStart(); |
|
if(m_videoOamDmaTransferIdx >= 0) |
|
VideoOamDmaTransfer(); |
|
if(m_videoOamDmaTransferIdx > 0x9F) |
|
VideoOamDmaTransferEnd(); |
|
|
|
// Emulate MBC functionality. |
|
if(m_mbc != None) MbcCycle(m_lastWrite); |
|
|
|
// Divider/Timer |
|
TimerExecute(); |
|
|
|
// Input |
|
InputRegisterHandler(); |
|
|
|
// Boot ROM |
|
if(!m_bootRomFlag) { |
|
BootRomFlagHandler(); |
|
} else { |
|
m_ioRegisters->SetByte(m_addrBootRomDisable, 1); |
|
} |
|
|
|
// PPU |
|
VideoExecute(); |
|
|
|
// Cycle Timing |
|
m_cycleRemaining -= m_cycleTime; |
|
|
|
// Breakpoint |
|
if(m_cpu->GetPc() == m_bp) { |
|
m_cycleRemaining = 0; |
|
m_paused = true; |
|
break; |
|
} |
|
} while(m_cycleTime < m_cycleRemaining); |
|
} |
|
|
|
std::string GameBoyInstance::DumpRegisters() { |
|
return m_cpu->DumpRegisters(); |
|
} |
|
|
|
uint16_t GameBoyInstance::GetRomBankCount() { |
|
auto romSizeByte = m_rom->GetByte(0x0148); |
|
|
|
switch(romSizeByte) { |
|
case 0x00: return 0; // 32KB |
|
case 0x01: return 4; // 64KB |
|
case 0x02: return 8; // 128KB |
|
case 0x03: return 16; // 256KB |
|
case 0x04: return 32; // 512KB |
|
case 0x05: return 64; // 1024KB |
|
case 0x06: return 128; // 2048KB |
|
case 0x07: return 256; // 4096KB |
|
case 0x08: return 512; // 8192KB |
|
case 0x52: return 72; // 1152KB |
|
case 0x53: return 80; // 1280KB |
|
case 0x54: return 96; // 1536KB |
|
default: |
|
std::stringstream ex; |
|
ex << "invalid ROM size byte: " |
|
<< PlipUtility::FormatHex(romSizeByte, 2); |
|
throw Plip::PlipEmulationException(ex.str().c_str()); |
|
} |
|
} |
|
|
|
inline void GameBoyInstance::InputRegisterHandler() { |
|
// TODO: Simulate DMG/SGB propagation delay. |
|
auto inputReg = m_ioRegisters->GetByte(m_regJoypad); |
|
if(!BIT_TEST(inputReg, 5)) { |
|
// Button keys selected. |
|
BIT_SET(inputReg, 4); |
|
inputReg &= ~(m_keypad >> 4); // Pull the inputs low. |
|
} else if(!BIT_TEST(inputReg, 4)) { |
|
// Direction keys selected. |
|
BIT_SET(inputReg, 5); |
|
inputReg &= ~(m_keypad & 0b00001111); |
|
} |
|
m_ioRegisters->SetByte(m_regJoypad, inputReg); |
|
} |
|
|
|
PlipError GameBoyInstance::Load(const std::string &path) { |
|
using io = Plip::PlipIo; |
|
if(!io::FileExists(path)) return PlipError::FileNotFound; |
|
|
|
auto size = io::GetSize(path); |
|
auto data = io::ReadFile(path, size); |
|
m_rom = new Plip::PlipMemoryRom(data.data(), size); |
|
m_memory->AssignBlock(m_rom, m_addrRom, 0x0000, 0x8000); |
|
|
|
ReadCartFeatures(); |
|
if(m_mbc != None) m_romBanks = GetRomBankCount(); |
|
if(m_hasRam) InitCartRam(); |
|
MbcInit(); |
|
|
|
// Load the boot ROM into 0x0000-0x00FF. |
|
m_memory->AssignBlock(m_bootRom, 0x0000, 0x0000, 0x0100); |
|
m_bootRomFlag = false; |
|
|
|
return PlipError::Success; |
|
} |
|
|
|
void GameBoyInstance::InitCartRam() { |
|
auto ramSizeByte = m_rom->GetByte(0x0149); |
|
|
|
switch(ramSizeByte) { |
|
case 0x00: // 0KB |
|
case 0x01: // 2KB |
|
case 0x02: // 8KB |
|
m_cartRamBanks = 0; break; |
|
case 0x03: m_cartRamBanks = 4; break; // 32KB |
|
case 0x04: m_cartRamBanks = 16; break; // 128KB |
|
case 0x05: m_cartRamBanks = 8; break; // 64KB |
|
default: |
|
std::stringstream ex; |
|
ex << "invalid cart RAM size byte: " |
|
<< PlipUtility::FormatHex(ramSizeByte, 2); |
|
throw Plip::PlipEmulationException(ex.str().c_str()); |
|
} |
|
|
|
m_cartRam = new Plip::PlipMemoryRam(8192 * m_cartRamBanks); |
|
|
|
if(ramSizeByte == 0x00) { |
|
m_memory->AssignBlock(m_cartRam, m_addrCartRam); |
|
} else { |
|
m_memory->AssignBlock(m_cartRam, m_addrCartRam, 0x0000, 0x2000); |
|
} |
|
} |
|
|
|
void GameBoyInstance::ReadCartFeatures() { |
|
auto cartType = m_rom->GetByte(0x0147); |
|
|
|
// MBC |
|
switch(cartType) { |
|
case 0x00: case 0x08: case 0x09: |
|
m_mbc = None; break; |
|
|
|
case 0x01: case 0x02: case 0x03: |
|
m_mbc = Mbc1; break; |
|
|
|
default: |
|
std::stringstream ex; |
|
ex << "invalid/unsupported memory bank controller: " |
|
<< PlipUtility::FormatHex(cartType, 2); |
|
throw Plip::PlipEmulationException(ex.str().c_str()); |
|
} |
|
|
|
// RAM |
|
switch(cartType) { |
|
case 0x02: case 0x03: case 0x08: case 0x09: case 0x0C: case 0x0D: |
|
case 0x0F: case 0x10: case 0x12: case 0x13: case 0x1A: case 0x1B: |
|
case 0x1D: case 0x1E: case 0x22: case 0xFF: |
|
m_hasRam = true; break; |
|
default: |
|
m_hasRam = false; break; |
|
} |
|
|
|
// Battery |
|
switch(cartType) { |
|
case 0x03: case 0x06: case 0x09: case 0x0D: case 0x0F: case 0x10: |
|
case 0x13: case 0x1B: case 0x1E: case 0x22: case 0xFF: |
|
m_hasBattery = true; break; |
|
default: |
|
m_hasBattery = false; break; |
|
} |
|
|
|
// RTC |
|
switch(cartType) { |
|
case 0x0F: case 0x10: |
|
m_hasRtc = true; break; |
|
default: |
|
m_hasRtc = false; break; |
|
} |
|
|
|
// Camera |
|
m_hasCamera = cartType == 0xFC; |
|
|
|
// Sensor |
|
m_hasSensor = cartType == 0x22; |
|
} |
|
|
|
void GameBoyInstance::ReadInput() { |
|
m_keypad = 0; |
|
READ_INPUT(InputRight); |
|
READ_INPUT(InputLeft); |
|
READ_INPUT(InputUp); |
|
READ_INPUT(InputDown); |
|
READ_INPUT(InputA); |
|
READ_INPUT(InputB); |
|
READ_INPUT(InputSelect); |
|
READ_INPUT(InputStart); |
|
} |
|
|
|
void GameBoyInstance::Redraw() { |
|
m_video->BeginDraw(); |
|
m_video->Draw(m_videoBuffer); |
|
m_video->EndDraw(); |
|
m_video->Render(); |
|
} |
|
|
|
void GameBoyInstance::RegisterInput() { |
|
m_input->AddInput(InputRight, |
|
PlipInputDefinition( |
|
PlipInputType::Digital, |
|
"Right"), |
|
{ .digital = false }); |
|
|
|
m_input->AddInput(InputLeft, |
|
PlipInputDefinition( |
|
PlipInputType::Digital, |
|
"Left"), |
|
{ .digital = false }); |
|
|
|
m_input->AddInput(InputUp, |
|
PlipInputDefinition( |
|
PlipInputType::Digital, |
|
"Up"), |
|
{ .digital = false }); |
|
|
|
m_input->AddInput(InputDown, |
|
PlipInputDefinition( |
|
PlipInputType::Digital, |
|
"Down"), |
|
{ .digital = false }); |
|
|
|
m_input->AddInput(InputA, |
|
PlipInputDefinition( |
|
PlipInputType::Digital, |
|
"A"), |
|
{ .digital = false }); |
|
|
|
m_input->AddInput(InputB, |
|
PlipInputDefinition( |
|
PlipInputType::Digital, |
|
"B"), |
|
{ .digital = false }); |
|
|
|
m_input->AddInput(InputSelect, |
|
PlipInputDefinition( |
|
PlipInputType::Digital, |
|
"Select"), |
|
{ .digital = false }); |
|
|
|
m_input->AddInput(InputStart, |
|
PlipInputDefinition( |
|
PlipInputType::Digital, |
|
"Start"), |
|
{ .digital = false }); |
|
} |
|
|
|
void GameBoyInstance::SetBreakpoint(uint32_t pc) { |
|
m_bp = pc; |
|
} |
|
}
|
|
|