Compare commits
86 Commits
52 changed files with 4983 additions and 212 deletions
@ -0,0 +1,6 @@
|
||||
* Sprites |
||||
- Sprite priority and conflict resolution is NYI. |
||||
|
||||
* Various timing issues. |
||||
- Check over the DMG timer routines. |
||||
- OAM DMA transfer seems to end early. |
@ -0,0 +1,93 @@
|
||||
/* GameBoyInstance.Mbc.cpp
|
||||
* |
||||
* Handles the emulation of the various memory bank controllers (MBC) |
||||
* contained within game cartridges. |
||||
*/ |
||||
|
||||
#include <sstream> |
||||
|
||||
#include "../../PlipEmulationException.h" |
||||
#include "../../PlipUtility.h" |
||||
|
||||
#include "GameBoyInstance.h" |
||||
|
||||
namespace Plip::Core::GameBoy { |
||||
void GameBoyInstance::MbcInit() { |
||||
switch(m_mbc) { |
||||
case None: break; |
||||
case Mbc1: |
||||
m_mbcRomBank = 1; |
||||
if(m_hasRam) { |
||||
m_cartRam->SetReadable(false); |
||||
m_cartRam->SetWritable(false); |
||||
} |
||||
break; |
||||
default: |
||||
std::stringstream ex; |
||||
ex << "invalid/unsupported memory bank controller: " |
||||
<< PlipUtility::FormatHex((int)m_mbc, 2); |
||||
throw Plip::PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
} |
||||
|
||||
void GameBoyInstance::MbcCycle(PlipMemoryValue lastWrite) { |
||||
switch(m_mbc) { |
||||
case None: break; |
||||
case Mbc1: Mbc1Cycle(lastWrite); break; |
||||
default: |
||||
std::stringstream ex; |
||||
ex << "invalid/unsupported memory bank controller: " |
||||
<< PlipUtility::FormatHex((int)m_mbc, 2); |
||||
throw Plip::PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
} |
||||
|
||||
void GameBoyInstance::Mbc1Cycle(PlipMemoryValue lastWrite) { |
||||
if(lastWrite.address >= 0x8000) return; |
||||
|
||||
if(lastWrite.address < 0x2000) { |
||||
// RAM enable register.
|
||||
if(m_hasRam) { |
||||
m_mbcRamEnabled = (lastWrite.value & 0xF) == 0xA; |
||||
m_cartRam->SetReadable(m_mbcRamEnabled); |
||||
m_cartRam->SetWritable(m_mbcRamEnabled); |
||||
} |
||||
} else if(lastWrite.address < 0x4000) { |
||||
// ROM bank number. If this is set to zero, this will automatically
|
||||
// be bumped up by one, regardless of what's in the high bit.
|
||||
m_mbcRomBank &= 0b11100000; |
||||
auto newBank = lastWrite.value & 0b11111; |
||||
m_mbcRomBank |= newBank + (newBank == 0 ? 1 : 0); |
||||
m_mbcRomBank &= (m_romBanks - 1); |
||||
m_memory->AssignBlock(m_rom, m_addrBankedRom, |
||||
0x4000 * m_mbcRomBank, 0x4000); |
||||
} else if(lastWrite.address < 0x6000) { |
||||
// RAM bank number, or the upper bits of the ROM bank number on
|
||||
// 1MB carts.
|
||||
if(m_romBanks >= 64) { |
||||
// Selects the upper bits of ROM.
|
||||
m_mbcRomBank &= 0b11100000; |
||||
m_mbcRomBank |= (lastWrite.value & 0b11) << 5; |
||||
m_mbcRomBank &= (m_romBanks - 1); |
||||
m_memory->AssignBlock(m_rom, m_addrBankedRom, |
||||
0x4000 * m_mbcRomBank, 0x4000); |
||||
|
||||
// If the advanced ROM banking mode bit is set, this bit impacts
|
||||
// bank 0.
|
||||
if(m_mbcMode) { |
||||
m_memory->AssignBlock(m_rom, m_addrRom, |
||||
0x2000 * m_mbcRomBank & 0b01100000, |
||||
0x4000); |
||||
} |
||||
} else if(m_hasRam && m_mbcRamEnabled && m_mbcMode == 1 && m_cartRamBanks > 1) { |
||||
// Selects the RAM bank.
|
||||
m_mbcRamBank = (lastWrite.value & 0b11) & (m_cartRamBanks - 1); |
||||
m_memory->AssignBlock(m_cartRam, m_addrCartRam, |
||||
0x2000 * m_mbcRamBank, 0x2000); |
||||
} |
||||
} else { |
||||
// Banking mode select.
|
||||
m_mbcMode = lastWrite.value & 0b1; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,100 @@
|
||||
/* GameBoyInstance.timer.cpp
|
||||
* |
||||
* Simulated timer implementation in the GameBoy core. |
||||
*/ |
||||
|
||||
#include "GameBoyInstance.h" |
||||
|
||||
#include "../../PlipEmulationException.h" |
||||
#include "../../PlipUtility.h" |
||||
|
||||
namespace Plip::Core::GameBoy { |
||||
void GameBoyInstance::TimerExecute() { |
||||
m_timerLast = m_timer; |
||||
if(m_lastWrite.address == m_addrRegisters + m_regDivider) { |
||||
// Reset the timer variable if DIV is written to.
|
||||
m_timer = 0; |
||||
} |
||||
|
||||
// The timer increases by 4 every cycle.
|
||||
m_timer += 4; |
||||
|
||||
if(m_timerTimaOverflow) { |
||||
// Reload TIMA and raise an interrupt (if applicable).
|
||||
auto tma = m_ioRegisters->GetByte(m_regTma); |
||||
m_ioRegisters->SetByte(m_regTima, tma); |
||||
|
||||
// Quirk: If IF is written during the interrupt cycle, the
|
||||
// written value will override this.
|
||||
if(m_lastWrite.address != Plip::Cpu::SharpLr35902::MemInterruptFlag) |
||||
m_cpu->Interrupt(INTERRUPT_TIMER); |
||||
} |
||||
|
||||
// Handle TIMA.
|
||||
TimerTima(); |
||||
|
||||
// Update the divider register.
|
||||
m_ioRegisters->SetByte(m_regDivider, m_timer >> 8); |
||||
} |
||||
|
||||
inline uint8_t GameBoyInstance::TimerFallingEdgeBit(uint8_t tac) { |
||||
switch(tac & 0b11) { |
||||
case 0b00: |
||||
return 9; |
||||
case 0b01: |
||||
return 3; |
||||
case 0b10: |
||||
return 5; |
||||
case 0b11: |
||||
return 7; |
||||
} |
||||
|
||||
return 0; // Compiler appeasement. :)
|
||||
} |
||||
|
||||
inline bool GameBoyInstance::TimerFallingEdgeDetection(uint8_t bit) const { |
||||
return BIT_TEST(m_timerLast, bit) && !BIT_TEST(m_timer, bit); |
||||
} |
||||
|
||||
inline void GameBoyInstance::TimerIncreaseTima() { |
||||
uint8_t tima = m_ioRegisters->GetByte(m_regTima) + 1; |
||||
|
||||
if(tima == 0) { |
||||
if(m_lastWrite.address == m_addrRegisters + m_regTima) { |
||||
// Quirk: If TIMA is written to during the cycle that causes it to
|
||||
// overflow, the pending reset and interrupt are cancelled.
|
||||
tima = m_lastWrite.value; |
||||
} else { |
||||
// TIMA overflow.
|
||||
m_timerTimaOverflow = true; |
||||
} |
||||
} |
||||
|
||||
m_ioRegisters->SetByte(m_regTima, tima); |
||||
} |
||||
|
||||
void GameBoyInstance::TimerTima() { |
||||
auto tac = m_ioRegisters->GetByte(m_regTac) & 0b111; |
||||
auto freqBit = TimerFallingEdgeBit(tac); |
||||
m_timerTimaOverflow = false; |
||||
|
||||
if(TimerFallingEdgeDetection(freqBit) && BIT_TEST(tac, 2)) { |
||||
TimerIncreaseTima(); |
||||
} else if(BIT_TEST(m_timer, freqBit) && BIT_TEST(m_timerTacLast, 2) && !BIT_TEST(tac, 2)) { |
||||
// Quirk: Increase TIMA if the corresponding bit is set when disabling
|
||||
// the timer.
|
||||
TimerIncreaseTima(); |
||||
} else if(m_lastWrite.address == m_addrRegisters + m_regTac |
||||
&& !BIT_TEST(m_timerTacLast, 2) && BIT_TEST(tac, 2) |
||||
&& (m_timerTacLast & 0b11) == 0 && (tac & 0b11) == 1) { |
||||
// Quirk: Increase TIMA if the timer goes from disabled to enabled, and
|
||||
// the multiplexer goes from 0 to 1 (agh).
|
||||
TimerIncreaseTima(); |
||||
} |
||||
|
||||
m_timerTacLast = tac; |
||||
|
||||
// Write TAC back into memory to ensure that only the low 3 bits are set.
|
||||
m_ioRegisters->SetByte(m_regTac, tac); |
||||
} |
||||
} |
@ -0,0 +1,439 @@
|
||||
/* GameBoyInstance.Video.cpp
|
||||
* |
||||
* Simulated graphics hardware for the GameBoy core. |
||||
*/ |
||||
|
||||
#include <cstring> |
||||
#include <sstream> |
||||
|
||||
#include "../../PlipEmulationException.h" |
||||
#include "../../PlipUtility.h" |
||||
|
||||
#include "GameBoyInstance.h" |
||||
|
||||
namespace Plip::Core::GameBoy { |
||||
void GameBoyInstance::VideoCycle() { |
||||
bool result; |
||||
|
||||
switch(m_videoMode) { |
||||
case m_videoModeHBlank: |
||||
result = VideoHBlank(); |
||||
break; |
||||
case m_videoModeVBlank: |
||||
result = VideoVBlank(); |
||||
break; |
||||
case m_videoModeOamSearch: |
||||
result = VideoOamSearch(); |
||||
break; |
||||
case m_videoModePicGen: |
||||
result = VideoVidGen(); |
||||
break; |
||||
default: |
||||
std::stringstream ex; |
||||
ex << "Invalid video mode: " |
||||
<< PlipUtility::FormatHex(m_videoMode, 1) << "\n\n" |
||||
<< m_cpu->DumpRegisters(); |
||||
throw Plip::PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
|
||||
if(result) { |
||||
m_dotCount += 1; |
||||
} else { |
||||
// Mode transition.
|
||||
VideoModePreTransition(); |
||||
m_videoMode = ++m_videoMode > 3 ? 0 : m_videoMode; |
||||
if(m_videoMode == 1 && m_videoLy < m_screenHeight) |
||||
m_videoMode++; // only transition to 1 after the last HBlank
|
||||
VideoModePostTransition(); |
||||
} |
||||
|
||||
auto stat = m_ioRegisters->GetByte(m_regLcdcStatus) & 0b11111000; |
||||
stat |= (m_videoCoincidence << 2) | m_videoMode; |
||||
m_ioRegisters->SetByte(m_regLcdcStatus, stat); |
||||
} |
||||
|
||||
void GameBoyInstance::VideoExecute() { |
||||
auto lcdc = m_ioRegisters->GetByte(m_regLcdControl); |
||||
|
||||
// Run 4 dot clock cycles (4.19MHz) if the display is enabled.
|
||||
if(BIT_TEST(m_videoLastLcdc, 7) && !BIT_TEST(lcdc, 7)) { |
||||
// The LCD display was disabled during this CPU cycle. Flag all
|
||||
// video memory as being writable.
|
||||
m_oam->SetWritable(true); |
||||
m_videoRam->SetWritable(true); |
||||
|
||||
// When the LCD is disabled, the screen should go blank.
|
||||
memset(m_videoBuffer, 0xFF, m_videoBufferSize); |
||||
m_video->BeginDraw(); |
||||
m_video->Draw(m_videoBuffer); |
||||
m_video->EndDraw(); |
||||
m_video->Render(); |
||||
} else if(!BIT_TEST(m_videoLastLcdc, 7) && BIT_TEST(lcdc, 7)) { |
||||
// The LCD display was enabled during this CPU cycle. Set the
|
||||
// video memory accessibility appropriately.
|
||||
VideoSetMemoryPermissions(); |
||||
m_lcdBlankFrame = true; |
||||
} |
||||
|
||||
if(BIT_TEST(lcdc, 7)) { |
||||
for(auto dotCycle = 0; dotCycle < m_dotsPerCycle; dotCycle++) { |
||||
VideoCycle(); |
||||
} |
||||
} |
||||
|
||||
m_ioRegisters->SetByte(m_regLy, m_videoLy); |
||||
m_videoLastLcdc = lcdc; |
||||
} |
||||
|
||||
bool GameBoyInstance::VideoHBlank() const { |
||||
return m_dotCount < m_videoScanlineTime; |
||||
} |
||||
|
||||
bool GameBoyInstance::VideoOamSearch() { |
||||
if(m_dotCount != 0) return m_dotCount < m_videoOamScanTime; |
||||
|
||||
auto spriteHeight = ((m_ioRegisters->GetByte(m_regLcdControl) >> 2) & 1) ? 16 : 8; |
||||
|
||||
// Just do everything at once since the OAM will be locked at this point.
|
||||
m_spriteListIdx = 0; |
||||
memset(m_spriteList, 0xFF, m_maxSpritesPerScanline); |
||||
for(auto sprIdx = 0; sprIdx < m_maxSpritesInOam; sprIdx++) { |
||||
auto sprAddr = 4 * sprIdx; |
||||
auto sprY = m_oam->GetByte(sprAddr); |
||||
auto sprX = m_oam->GetByte(sprAddr + 1); |
||||
|
||||
if(sprX == 0 || sprX >= m_screenWidth + 8) continue; |
||||
if(m_videoLy < sprY - 16 || m_videoLy >= sprY - (16 - spriteHeight)) |
||||
continue; |
||||
|
||||
m_spriteList[m_spriteListIdx++] = sprIdx; |
||||
if(m_spriteListIdx == m_maxSpritesPerScanline) break; |
||||
} |
||||
|
||||
return m_dotCount < m_videoOamScanTime; |
||||
} |
||||
|
||||
void GameBoyInstance::VideoModePostTransition() { |
||||
uint8_t stat = m_ioRegisters->GetByte(m_regLcdcStatus); |
||||
VideoSetMemoryPermissions(); |
||||
|
||||
// New mode.
|
||||
switch(m_videoMode) { |
||||
case m_videoModeHBlank: |
||||
if(BIT_TEST(stat, 3)) m_cpu->Interrupt(INTERRUPT_LCDSTAT); |
||||
break; |
||||
|
||||
case m_videoModeVBlank: |
||||
m_dotCount = 0; |
||||
|
||||
// Plot the current buffer to the screen and flip buffers.
|
||||
m_video->BeginDraw(); |
||||
m_video->Draw(m_videoBuffer); |
||||
m_video->EndDraw(); |
||||
m_video->Render(); |
||||
|
||||
m_lcdBlankFrame = false; |
||||
|
||||
m_cpu->Interrupt(INTERRUPT_VBLANK); |
||||
if(BIT_TEST(stat, 4)) m_cpu->Interrupt(INTERRUPT_LCDSTAT); |
||||
break; |
||||
|
||||
case m_videoModeOamSearch: |
||||
memset(m_spriteList, 0x00, m_maxSpritesPerScanline); |
||||
m_dotCount = 0; |
||||
m_videoCoincidence = m_videoLy == m_ioRegisters->GetByte(m_regLyCompare) ? 1 : 0; |
||||
|
||||
if(BIT_TEST(stat, 5)) // OAM interrupt
|
||||
m_cpu->Interrupt(INTERRUPT_LCDSTAT); |
||||
|
||||
if(m_videoCoincidence && BIT_TEST(stat, 6)) // LYC=LY interrupt.
|
||||
m_cpu->Interrupt(INTERRUPT_LCDSTAT); |
||||
|
||||
break; |
||||
|
||||
case m_videoModePicGen: |
||||
m_dotCount = 0; |
||||
m_vidGenStage = BackgroundScrolling; |
||||
break; |
||||
|
||||
default: |
||||
std::stringstream ex; |
||||
ex << "Invalid video mode: " |
||||
<< PlipUtility::FormatHex(m_videoMode, 1) << "\n\n" |
||||
<< m_cpu->DumpRegisters(); |
||||
throw Plip::PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
} |
||||
|
||||
void GameBoyInstance::VideoModePreTransition() { |
||||
// Previous mode.
|
||||
switch(m_videoMode) { |
||||
case m_videoModeHBlank: |
||||
m_videoLx = 0; |
||||
m_videoLy++; |
||||
break; |
||||
case m_videoModeVBlank: |
||||
m_videoLy = 0; |
||||
break; |
||||
case m_videoModeOamSearch: |
||||
case m_videoModePicGen: |
||||
break; |
||||
default: |
||||
std::stringstream ex; |
||||
ex << "Invalid video mode: " |
||||
<< PlipUtility::FormatHex(m_videoMode, 1) << "\n\n" |
||||
<< m_cpu->DumpRegisters(); |
||||
throw Plip::PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
} |
||||
|
||||
void GameBoyInstance::VideoOamDmaTransfer() { |
||||
auto srcAddr = (m_ioRegisters->GetByte(m_regOamDmaTransfer, true) << 8) | m_videoOamDmaTransferIdx; |
||||
m_oam->SetByte(m_videoOamDmaTransferIdx++, m_memory->GetByte(srcAddr, true), true); |
||||
} |
||||
|
||||
void GameBoyInstance::VideoOamDmaTransferEnd() { |
||||
// The DMA transfer is finished. Set the RAM accessibility back
|
||||
// to a normal state.
|
||||
m_videoOamDmaTransferIdx = -1; |
||||
|
||||
if(m_cartRam != nullptr) { |
||||
m_cartRam->SetReadable(true); |
||||
m_cartRam->SetWritable(true); |
||||
} |
||||
|
||||
m_rom->SetReadable(true); |
||||
|
||||
m_workRam->SetReadable(true); |
||||
m_workRam->SetWritable(true); |
||||
|
||||
m_videoRam->SetReadable(true); |
||||
m_videoRam->SetWritable(true); |
||||
|
||||
m_oam->SetReadable(true); |
||||
m_oam->SetWritable(true); |
||||
|
||||
m_ioRegisters->SetReadable(true); |
||||
m_ioRegisters->SetWritable(true); |
||||
} |
||||
|
||||
void GameBoyInstance::VideoOamDmaTransferStart() { |
||||
// Set up the system for the DMA transfer.
|
||||
m_videoOamDmaTransferIdx = 0; |
||||
|
||||
if(m_cartRam != nullptr) { |
||||
m_cartRam->SetReadable(false); |
||||
m_cartRam->SetWritable(false); |
||||
} |
||||
|
||||
m_rom->SetReadable(false); |
||||
|
||||
m_workRam->SetReadable(false); |
||||
m_workRam->SetWritable(false); |
||||
|
||||
m_videoRam->SetReadable(false); |
||||
m_videoRam->SetWritable(false); |
||||
|
||||
m_oam->SetReadable(false); |
||||
m_oam->SetWritable(false); |
||||
|
||||
m_ioRegisters->SetReadable(false); |
||||
m_ioRegisters->SetWritable(false); |
||||
} |
||||
|
||||
void GameBoyInstance::VideoSetMemoryPermissions() { |
||||
switch(m_videoMode) { |
||||
case m_videoModeHBlank: |
||||
case m_videoModeVBlank: |
||||
m_oam->SetWritable(true); |
||||
m_videoRam->SetWritable(true); |
||||
break; |
||||
case m_videoModeOamSearch: |
||||
m_oam->SetWritable(false); |
||||
m_videoRam->SetWritable(true); |
||||
break; |
||||
case m_videoModePicGen: |
||||
m_oam->SetWritable(false); |
||||
m_videoRam->SetWritable(false); |
||||
break; |
||||
default: |
||||
std::stringstream ex; |
||||
ex << "Invalid video mode: " |
||||
<< PlipUtility::FormatHex(m_videoMode, 1) << "\n\n" |
||||
<< m_cpu->DumpRegisters(); |
||||
throw Plip::PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
} |
||||
|
||||
bool GameBoyInstance::VideoVBlank() { |
||||
if(m_dotCount > 0) |
||||
if(m_dotCount % (m_videoOamScanTime + m_videoScanlineTime) == 0) |
||||
m_videoLy++; |
||||
|
||||
return m_dotCount < m_videoVBlankTime; |
||||
} |
||||
|
||||
bool GameBoyInstance::VideoVidGen() { |
||||
uint8_t scx; |
||||
|
||||
switch(m_vidGenStage) { |
||||
case BackgroundScrolling: |
||||
// Pause the dot clock to simulate the background shifter.
|
||||
scx = m_ioRegisters->GetByte(m_regScx); |
||||
if(++m_vidGenTick > scx % 8) { |
||||
m_vidGenTick = 0; |
||||
m_vidGenStage = Drawing; |
||||
} |
||||
break; |
||||
|
||||
case Drawing: |
||||
VideoVidGenDraw(); |
||||
m_videoLx++; |
||||
|
||||
break; |
||||
|
||||
default: |
||||
std::stringstream ex; |
||||
ex << "Invalid video generation stage: " |
||||
<< PlipUtility::FormatHex((int)m_vidGenStage, 1) << "\n\n" |
||||
<< m_cpu->DumpRegisters(); |
||||
throw Plip::PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
|
||||
return m_videoLx < m_screenWidth; |
||||
} |
||||
|
||||
#pragma clang diagnostic push |
||||
#pragma ide diagnostic ignored "readability-function-cognitive-complexity" |
||||
inline void GameBoyInstance::VideoVidGenDraw() { |
||||
// TODO: Add timing routine.
|
||||
uint8_t pixelColor, pixelDataLow, pixelDataHigh, pixelDataCombined; |
||||
uint8_t tileIdx, tilePX, tilePY, tileShift; |
||||
uint8_t lineOffset; |
||||
|
||||
auto pos = (m_videoLy * m_screenWidth) + m_videoLx; |
||||
|
||||
auto lcdc = m_ioRegisters->GetByte(m_regLcdControl); |
||||
bool lcdEnabled = BIT_TEST(lcdc, 7); |
||||
if(!lcdEnabled) { |
||||
Plot(255, pos); |
||||
return; |
||||
} |
||||
|
||||
auto scx = m_ioRegisters->GetByte(m_regScx); |
||||
auto scy = m_ioRegisters->GetByte(m_regScy); |
||||
auto bgp = m_ioRegisters->GetByte(m_regBgp); |
||||
|
||||
if(BIT_TEST(lcdc, 0)) { // BG/Window Display Enabled
|
||||
auto tileDataAddr = m_vramTileBase + (BIT_TEST(lcdc, 4) ? 0 : m_vramTileBlockOffset); |
||||
auto tileMapAddr = m_vramBgBase + (BIT_TEST(lcdc, 3) ? m_vramBgBlockOffset : 0); |
||||
auto tileX = ((scx + m_videoLx) / 8) % 32; |
||||
auto tileY = ((scy + m_videoLy) / 8) % 32; |
||||
tilePX = (scx + m_videoLx) % 8; |
||||
tilePY = (scy + m_videoLy) % 8; |
||||
|
||||
auto mapIdx = (tileY * 32) + tileX; |
||||
tileIdx = m_videoRam->GetByte(tileMapAddr + mapIdx); |
||||
|
||||
lineOffset = tilePY * 2; |
||||
|
||||
pixelDataLow = m_videoRam->GetByte(tileDataAddr + (tileIdx * 16) + lineOffset, true); |
||||
pixelDataHigh = m_videoRam->GetByte(tileDataAddr + (tileIdx * 16) + lineOffset + 1, true); |
||||
tileShift = 7 - tilePX; |
||||
pixelDataCombined = (((pixelDataHigh >> tileShift) & 0b1) << 1) |
||||
| ((pixelDataLow >> tileShift) & 0b1); |
||||
|
||||
pixelColor = (bgp >> (pixelDataCombined * 2)) & 0b11; |
||||
Plot(pixelColor, pos); |
||||
|
||||
if(BIT_TEST(lcdc, 5)) { // Window Display Enabled
|
||||
auto wx = m_ioRegisters->GetByte(m_regWx) - 7; |
||||
auto wy = m_ioRegisters->GetByte(m_regWy); |
||||
|
||||
if(!(wx > 166 || wy > 143) || m_videoLx >= wx || m_videoLy >= wy) { |
||||
tileMapAddr = m_vramBgBase + (BIT_TEST(lcdc, 6) ? m_vramBgBlockOffset : 0); |
||||
tileX = ((m_videoLx + wx) / 8) % 32; |
||||
tileY = ((m_videoLy + wy) / 8) % 32; |
||||
tilePX = (m_videoLx + wx) % 8; |
||||
tilePY = (m_videoLy + wy) % 8; |
||||
|
||||
mapIdx = (tileY * 32) + tileX; |
||||
tileIdx = m_videoRam->GetByte(tileMapAddr + mapIdx, true); |
||||
|
||||
lineOffset = tilePY * 2; |
||||
|
||||
pixelDataLow = m_videoRam->GetByte(tileDataAddr + (tileIdx * 16) + lineOffset, true); |
||||
pixelDataHigh = m_videoRam->GetByte(tileDataAddr + (tileIdx * 16) + lineOffset + 1, true); |
||||
tileShift = 7 - tilePX; |
||||
pixelDataCombined = (((pixelDataHigh >> tileShift) & 0b1) << 1) |
||||
| ((pixelDataLow >> tileShift) & 0b1); |
||||
|
||||
pixelColor = (bgp >> (pixelDataCombined * 2)) & 0b11; |
||||
Plot(pixelColor, pos); |
||||
} |
||||
} |
||||
} else { |
||||
// BG/Window display disabled. Draw a white pixel.
|
||||
Plot(0b00, pos); |
||||
} |
||||
|
||||
if(!BIT_TEST(lcdc, 1)) return; |
||||
|
||||
// OBJ Display Enabled
|
||||
auto obp0 = m_ioRegisters->GetByte(m_regObp0); |
||||
auto obp1 = m_ioRegisters->GetByte(m_regObp1); |
||||
bool doubleHeight = BIT_TEST(lcdc, 2); |
||||
|
||||
for(auto i = 0; i < m_maxSpritesPerScanline; i++) { |
||||
if(m_spriteList[i] == 0xFF) break; |
||||
|
||||
auto base = 4 * m_spriteList[i]; |
||||
|
||||
// Fetch the sprite attributes from the OAM.
|
||||
auto sprAttr = m_oam->GetByte(base + 3, true); |
||||
bool sprFlipX = BIT_TEST(sprAttr, 5); |
||||
bool sprFlipY = BIT_TEST(sprAttr, 6); |
||||
bool sprPriority = !BIT_TEST(sprAttr, 7); |
||||
auto sprPalette = BIT_TEST(sprAttr, 4) ? obp1 : obp0; |
||||
|
||||
// Position and tile number.
|
||||
auto sprY = m_oam->GetByte(base, true); |
||||
auto sprX = m_oam->GetByte(base + 1, true); |
||||
tileIdx = m_oam->GetByte(base + 2, true); |
||||
if(doubleHeight) tileIdx &= 0b11111110; // LSB is ignored for 8x16 sprites.
|
||||
|
||||
// Check to see if the sprite should even be drawn.
|
||||
if(m_videoLx < sprX - 8 || m_videoLx >= sprX) continue; // X value out of range.
|
||||
if(!sprPriority && pixelColor > 0) continue; // Background/window overlaps sprite.
|
||||
|
||||
// Figure out which pixel should be considered.
|
||||
auto sprHeight = doubleHeight ? 16 : 8; |
||||
tilePX = m_videoLx - (sprX - 8); |
||||
if(sprFlipX) tilePX += -7; |
||||
if(sprFlipY) tilePY += -(doubleHeight ? 15 : 7); |
||||
tilePY = m_videoLy - (sprY - 16); |
||||
if(doubleHeight && tilePY >= 8) { |
||||
// Move to the second tile of the 8x16 sprite.
|
||||
tilePY %= 8; |
||||
tileIdx |= 0b1; |
||||
} else if(doubleHeight && tilePY < 8) { |
||||
// Move to the first tile (this will likely only be used if the sprite
|
||||
// is flipped).
|
||||
tileIdx &= 0b11111110; |
||||
} |
||||
lineOffset = tilePY * 2; |
||||
|
||||
pixelDataLow = m_videoRam->GetByte(m_vramTileBase + (tileIdx * 16) + lineOffset, true); |
||||
pixelDataHigh = m_videoRam->GetByte(m_vramTileBase + (tileIdx * 16) + lineOffset + 1, true); |
||||
tileShift = 7 - tilePX; |
||||
pixelDataCombined = (((pixelDataHigh >> tileShift) & 0b1) << 1) |
||||
| ((pixelDataLow >> tileShift) & 0b1); |
||||
|
||||
if(pixelDataCombined == 0) continue; // Transparent pixel.
|
||||
|
||||
pixelColor = (sprPalette >> (pixelDataCombined * 2)) & 0b11; |
||||
Plot(pixelColor, pos); |
||||
} |
||||
} |
||||
#pragma clang diagnostic pop |
||||
} |
@ -0,0 +1,358 @@
|
||||
/* 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; |
||||
} |
||||
} |
@ -0,0 +1,242 @@
|
||||
/* GameBoyInstance.h
|
||||
* |
||||
* A GameBoy core for Plip. |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include "../PlipCore.h" |
||||
#include "../../Cpu/SharpLr35902/SharpLr35902.h" |
||||
#include "../../Memory/PlipMemoryMap.h" |
||||
#include "../../Memory/PlipMemoryRam.h" |
||||
#include "../../Memory/PlipMemoryRom.h" |
||||
|
||||
namespace Plip::Core::GameBoy { |
||||
class GameBoyInstance : public PlipCore { |
||||
public: |
||||
GameBoyInstance(PlipAudio *audio, PlipInput *input, PlipVideo *video, PlipConfig *config); |
||||
~GameBoyInstance(); |
||||
|
||||
void ClearBreakpoint() override; |
||||
void Delta(long ns) override; |
||||
std::string DumpRegisters() override; |
||||
PlipError Load(const std::string &path) override; |
||||
void Redraw() override; |
||||
void SetBreakpoint(uint32_t pc) override; |
||||
|
||||
const int BaseClockRate = 4194304; |
||||
const int InputRight = 0; |
||||
const int InputLeft = 1; |
||||
const int InputUp = 2; |
||||
const int InputDown = 3; |
||||
const int InputA = 4; |
||||
const int InputB = 5; |
||||
const int InputSelect = 6; |
||||
const int InputStart = 7; |
||||
|
||||
private: |
||||
inline void Plot(uint8_t color, int pos) { |
||||
if(m_lcdBlankFrame) color = 0b00; |
||||
|
||||
switch(color) { |
||||
case 0b00: |
||||
// White
|
||||
m_videoFmt.plot(m_videoBuffer, pos, 255, 255, 255, 255); |
||||
break; |
||||
case 0b01: |
||||
// Light Gray
|
||||
m_videoFmt.plot(m_videoBuffer, pos, 172, 172, 172, 255); |
||||
break; |
||||
case 0b10: |
||||
// Dark Gray
|
||||
m_videoFmt.plot(m_videoBuffer, pos, 86, 86, 86, 255); |
||||
break; |
||||
case 0b11: |
||||
// Black
|
||||
m_videoFmt.plot(m_videoBuffer, pos, 0, 0, 0, 255); |
||||
break; |
||||
default: |
||||
// Huh. Okay, make it pretty obvious that something's up.
|
||||
m_videoFmt.plot(m_videoBuffer, pos, 255, 0, 0, 255); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
void BootRomFlagHandler(); |
||||
uint16_t GetRomBankCount(); |
||||
void InitCartRam(); |
||||
void InputRegisterHandler(); |
||||
void ReadCartFeatures(); |
||||
void ReadInput(); |
||||
void RegisterInput(); |
||||
|
||||
// GameBoyInstance.Mbc.cpp
|
||||
void MbcInit(); |
||||
void MbcCycle(PlipMemoryValue lastWrite); |
||||
void Mbc1Cycle(PlipMemoryValue lastWrite); |
||||
|
||||
// GameBoyInstance.timer.cpp
|
||||
void TimerExecute(); |
||||
[[nodiscard]] static uint8_t TimerFallingEdgeBit(uint8_t tac); |
||||
[[nodiscard]] bool TimerFallingEdgeDetection(uint8_t bit) const; |
||||
void TimerIncreaseTima(); |
||||
void TimerTima(); |
||||
|
||||
// GameBoyInstance.Video.cpp
|
||||
void VideoCycle(); |
||||
void VideoExecute(); |
||||
[[nodiscard]] bool VideoHBlank() const; |
||||
void VideoModePostTransition(); |
||||
void VideoModePreTransition(); |
||||
void VideoOamDmaTransfer(); |
||||
void VideoOamDmaTransferEnd(); |
||||
void VideoOamDmaTransferStart(); |
||||
[[nodiscard]] bool VideoOamSearch(); |
||||
void VideoSetMemoryPermissions(); |
||||
[[nodiscard]] bool VideoVBlank(); |
||||
[[nodiscard]] bool VideoVidGen(); |
||||
void VideoVidGenDraw(); |
||||
|
||||
// Core
|
||||
bool m_bootRomFlag = false; |
||||
uint32_t m_bp = 0xFFFFFFFF; |
||||
std::string m_bootRomPath; |
||||
Plip::Cpu::SharpLr35902 *m_cpu; |
||||
long m_cycleRemaining = 0; |
||||
int m_dotCyclesRemaining = 0; |
||||
const int m_dotsPerCycle = 4; |
||||
PlipMemoryValue m_lastWrite {}; |
||||
uint8_t *m_videoBuffer; |
||||
size_t m_videoBufferSize; |
||||
PlipVideoFormatInfo m_videoFmt {}; |
||||
|
||||
// Cartridge Information
|
||||
enum MemoryBankController { |
||||
None, |
||||
Mbc1, |
||||
Mbc2, |
||||
Mbc3, |
||||
Mbc5, |
||||
Mbc6, |
||||
Mbc7, |
||||
Mmm01, |
||||
PocketCamera, |
||||
BandaiTama5, |
||||
HuC1, |
||||
HuC3 |
||||
}; |
||||
|
||||
MemoryBankController m_mbc = None; |
||||
bool m_hasBattery = false; |
||||
bool m_hasCamera = false; |
||||
bool m_hasRam = false; |
||||
bool m_hasRtc = false; |
||||
bool m_hasSensor = false; |
||||
|
||||
uint16_t m_romBanks = 0; |
||||
uint16_t m_cartRamBanks = 0; |
||||
|
||||
// MBC
|
||||
uint8_t m_mbcMode = 0; |
||||
uint8_t m_mbcRomBank = 0; |
||||
uint8_t m_mbcRamBank = 0; |
||||
bool m_mbcRamEnabled = false; |
||||
|
||||
// Memory
|
||||
Plip::PlipMemoryRom *m_bootRom = nullptr; |
||||
Plip::PlipMemoryRom *m_rom = nullptr; |
||||
uint8_t m_unusableContents[0x60] {}; |
||||
Plip::PlipMemoryRom *m_unusable; |
||||
|
||||
Plip::PlipMemoryRam *m_cartRam = nullptr; |
||||
Plip::PlipMemoryRam *m_workRam; |
||||
Plip::PlipMemoryRam *m_videoRam; |
||||
Plip::PlipMemoryRam *m_oam; |
||||
Plip::PlipMemoryRam *m_ioRegisters; |
||||
Plip::PlipMemoryRam *m_highRam; |
||||
|
||||
static const uint32_t m_addrRom = 0x0000; |
||||
static const uint32_t m_addrBankedRom = 0x4000; |
||||
static const uint32_t m_addrVideoRam = 0x8000; |
||||
static const uint32_t m_addrWorkRam = 0xC000; |
||||
static const uint32_t m_addrCartRam = 0xA000; |
||||
static const uint32_t m_addrEchoRam = 0xE000; |
||||
static const uint32_t m_addrOam = 0xFE00; |
||||
static const uint32_t m_addrUnusable = 0xFEA0; |
||||
static const uint32_t m_addrRegisters = 0xFF00; |
||||
static const uint32_t m_addrHighRam = 0xFF80; |
||||
|
||||
// Boot
|
||||
static const uint32_t m_addrBootRomDisable = 0xFF50 - m_addrRegisters; |
||||
|
||||
// Input
|
||||
uint8_t m_keypad = 0; |
||||
|
||||
const uint32_t m_regJoypad = 0xFF00 - m_addrRegisters; |
||||
|
||||
// Timer
|
||||
static const uint32_t m_regDivider = 0xFF04 - m_addrRegisters; |
||||
static const uint32_t m_regTima = 0xFF05 - m_addrRegisters; |
||||
static const uint32_t m_regTma = 0xFF06 - m_addrRegisters; |
||||
static const uint32_t m_regTac = 0xFF07 - m_addrRegisters; |
||||
|
||||
uint16_t m_timer = 0; |
||||
uint16_t m_timerLast = 0; |
||||
uint8_t m_timerTacLast = 0; |
||||
bool m_timerTimaOverflow = false; |
||||
|
||||
// Video
|
||||
static const uint32_t m_regLcdControl = 0xFF40 - m_addrRegisters; |
||||
static const uint32_t m_regLcdcStatus = 0xFF41 - m_addrRegisters; |
||||
static const uint32_t m_regScy = 0xFF42 - m_addrRegisters; // R/W scroll Y
|
||||
static const uint32_t m_regScx = 0xFF43 - m_addrRegisters; // R/W - scroll X
|
||||
static const uint32_t m_regLy = 0xFF44 - m_addrRegisters; // R
|
||||
static const uint32_t m_regLyCompare = 0xFF45 - m_addrRegisters; // R/W
|
||||
static const uint32_t m_regOamDmaTransfer = 0xFF46 - m_addrRegisters; // R/W
|
||||
static const uint32_t m_regBgp = 0xFF47 - m_addrRegisters; // R/W - BG/window palette
|
||||
static const uint32_t m_regObp0 = 0xFF48 - m_addrRegisters; // R/W - object palette 0
|
||||
static const uint32_t m_regObp1 = 0xFF49 - m_addrRegisters; // R/W - object palette 0
|
||||
static const uint32_t m_regWy = 0xFF4A - m_addrRegisters; // R/W - window Y
|
||||
static const uint32_t m_regWx = 0xFF7B - m_addrRegisters; // R/W - window X - 7
|
||||
|
||||
static const uint32_t m_vramTileBase = 0x8000 - m_addrVideoRam; |
||||
static const uint32_t m_vramTileBlockOffset = 0x0800; |
||||
static const uint32_t m_vramBgBase = 0x9800 - m_addrVideoRam; |
||||
static const uint32_t m_vramBgBlockOffset = 0x0400; |
||||
|
||||
static const uint8_t m_maxSpritesPerScanline = 10; |
||||
static const uint8_t m_maxSpritesInOam = 40; |
||||
|
||||
static const uint32_t m_videoHBlankTime = 0; |
||||
static const uint32_t m_videoOamScanTime = 80; |
||||
static const uint32_t m_videoScanlineTime = 376; |
||||
static const uint32_t m_videoVBlankTime = 4560; |
||||
|
||||
static const uint32_t m_videoModeHBlank = 0; |
||||
static const uint32_t m_videoModeVBlank = 1; |
||||
static const uint32_t m_videoModeOamSearch = 2; |
||||
static const uint32_t m_videoModePicGen = 3; |
||||
|
||||
static const uint32_t m_screenWidth = 160; |
||||
static const uint32_t m_screenHeight = 144; |
||||
|
||||
enum VidGenStage { |
||||
BackgroundScrolling, |
||||
Drawing |
||||
}; |
||||
|
||||
int m_dotCount = 0; |
||||
bool m_lcdBlankFrame = false; |
||||
uint8_t *m_spriteList; |
||||
uint8_t *m_spriteListSorted; |
||||
uint8_t m_spriteListIdx = 0; |
||||
int m_videoCoincidence = 0; |
||||
int m_videoOamDmaTransferIdx = -1; |
||||
int m_videoMode = 0; |
||||
uint8_t m_videoLastLcdc = 0; |
||||
int m_videoLx = 0; |
||||
int m_videoLy = 0; |
||||
VidGenStage m_vidGenStage = BackgroundScrolling; |
||||
int m_vidGenTick = 0; |
||||
}; |
||||
} |
@ -0,0 +1,422 @@
|
||||
/* SharpLr35902.Decode.cpp
|
||||
* |
||||
* Sharp LR35902 instruction decoding. |
||||
*/ |
||||
|
||||
#include <sstream> |
||||
|
||||
#include "../../PlipEmulationException.h" |
||||
#include "../../PlipUtility.h" |
||||
|
||||
#include "SharpLr35902.h" |
||||
#include "SharpLr35902.Macros.h" |
||||
|
||||
namespace Plip::Cpu { |
||||
void SharpLr35902::Decode() { |
||||
switch(m_instr[0]) { |
||||
/*
|
||||
* Non-parameterized opcodes. |
||||
*/ |
||||
|
||||
// NOP
|
||||
case 0b00000000: |
||||
NUM_MCYCLES(2); break; |
||||
|
||||
// 0xCB
|
||||
case 0b11001011: |
||||
CYCLE(2) { FETCH; m_mcycle++; } |
||||
else { DecodeCB(); } |
||||
break; |
||||
|
||||
// RLCA
|
||||
case 0b00000111: |
||||
OpAccumRotateLeft(); break; |
||||
|
||||
// LD (nn), SP
|
||||
case 0b00001000: |
||||
OpLdMemSp(); break; |
||||
|
||||
// RRCA
|
||||
case 0b00001111: |
||||
OpAccumRotateRight(); break; |
||||
|
||||
// STOP
|
||||
case 0b00010000: |
||||
OpStop(); break; |
||||
|
||||
// JR e
|
||||
case 0b00011000: |
||||
OpJumpRelUnc(); break; |
||||
|
||||
// RLA
|
||||
case 0b00010111: |
||||
OpAccumRotateLeftThruCarry(); break; |
||||
|
||||
// RRA
|
||||
case 0b00011111: |
||||
OpAccumRotateRightThruCarry(); break; |
||||
|
||||
// DAA
|
||||
case 0b00100111: |
||||
OpAccumBcd(); break; |
||||
|
||||
// CPL
|
||||
case 0b00101111: |
||||
OpAccumFlip(); break; |
||||
|
||||
// SCF
|
||||
case 0b00110111: |
||||
OpSetCarry(); break; |
||||
|
||||
// CCF
|
||||
case 0b00111111: |
||||
OpFlipCarry(); break; |
||||
|
||||
// HALT
|
||||
case 0b01110110: |
||||
OpHalt(); break; |
||||
|
||||
// ADD A, n
|
||||
case 0b11000110: |
||||
OpAccumAddImm(); break; |
||||
|
||||
// RET
|
||||
case 0b11001001: |
||||
OpRetUnc(); break; |
||||
|
||||
// JP nn
|
||||
case 0b11000011: |
||||
OpJumpAbsUnc(); break; |
||||
|
||||
// CALL nn
|
||||
case 0b11001101: |
||||
OpCallUnc(); break; |
||||
|
||||
// ADC A, n
|
||||
case 0b11001110: |
||||
OpAccumAddCarryImm(); break; |
||||
|
||||
// RETI
|
||||
case 0b11011001: |
||||
OpRetImeUnc(); break; |
||||
|
||||
// SUB A, n
|
||||
case 0b11010110: |
||||
OpAccumSubImm(); break; |
||||
|
||||
// SBC A, n
|
||||
case 0b11011110: |
||||
OpAccumSubBorrowImm(); break; |
||||
|
||||
// LDH (n), A
|
||||
case 0b11100000: |
||||
OpLdMemHighImmAccum(); break; |
||||
|
||||
// LDH (C), A
|
||||
case 0b11100010: |
||||
OpLdMemHighCAccum(); break; |
||||
|
||||
// AND n
|
||||
case 0b11100110: |
||||
OpAccumAndImm(); break; |
||||
|
||||
// ADD SP, e
|
||||
case 0b11101000: |
||||
OpAddSpOffset(); break; |
||||
|
||||
// JP (HL)
|
||||
case 0b11101001: |
||||
OpJumpRegUnc(); break; |
||||
|
||||
// LD (nn), A
|
||||
case 0b11101010: |
||||
OpLdMemAccum(); break; |
||||
|
||||
// XOR n
|
||||
case 0b11101110: |
||||
OpAccumXorImm(); break; |
||||
|
||||
// LDH A, (n)
|
||||
case 0b11110000: |
||||
OpLdAccumMemHighImm(); break; |
||||
|
||||
// OR n
|
||||
case 0b11110110: |
||||
OpAccumOrImm(); break; |
||||
|
||||
// LDH A, (C)
|
||||
case 0b11110010: |
||||
OpLdAccumMemHighC(); break; |
||||
|
||||
// DI
|
||||
case 0b11110011: |
||||
OpDisableInterrupts(); break; |
||||
|
||||
// LD HL, SP+e
|
||||
case 0b11111000: |
||||
OpLdHlSpOffset(); break; |
||||
|
||||
// LD SP, HL
|
||||
case 0b11111001: |
||||
OpLdSpHl(); break; |
||||
|
||||
// LD A, (nn)
|
||||
case 0b11111010: |
||||
OpLdAccumMem(); break; |
||||
|
||||
// EI
|
||||
case 0b11111011: |
||||
OpEnableInterrupts(); break; |
||||
|
||||
// CP n
|
||||
case 0b11111110: |
||||
OpAccumCarryImm(); break; |
||||
|
||||
/*
|
||||
* Parameterized opcodes. |
||||
*/ |
||||
|
||||
// INC r (mask: 0b11000111, op: 0b00000100)
|
||||
case 0b00000100: case 0b00001100: case 0b00010100: case 0b00011100: |
||||
case 0b00100100: case 0b00101100: case 0b00110100: case 0b00111100: |
||||
OpIncReg(); break; |
||||
|
||||
// DEC r (mask: 0b11000111, op: 0b00000101)
|
||||
case 0b00000101: case 0b00001101: case 0b00010101: case 0b00011101: |
||||
case 0b00100101: case 0b00101101: case 0b00110101: case 0b00111101: |
||||
OpDecReg(); break; |
||||
|
||||
// INC rr (mask: 0b11001111, op: 0b00000011)
|
||||
case 0b00000011: case 0b00010011: case 0b00100011: case 0b00110011: |
||||
OpIncPair(); break; |
||||
|
||||
// DEC rr (mask: 0b11001111, op: 0b00001011)
|
||||
case 0b00001011: case 0b00011011: case 0b00101011: case 0b00111011: |
||||
OpDecPair(); break; |
||||
|
||||
// LD r, r' / LD r, (HL) / LD (HL), r
|
||||
// (mask: 0b11000000, 0b01000000)
|
||||
case 0b01000000: case 0b01000001: case 0b01000010: case 0b01000011: |
||||
case 0b01000100: case 0b01000101: case 0b01000110: case 0b01000111: |
||||
case 0b01001000: case 0b01001001: case 0b01001010: case 0b01001011: |
||||
case 0b01001100: case 0b01001101: case 0b01001110: case 0b01001111: |
||||
case 0b01010000: case 0b01010001: case 0b01010010: case 0b01010011: |
||||
case 0b01010100: case 0b01010101: case 0b01010110: case 0b01010111: |
||||
case 0b01011000: case 0b01011001: case 0b01011010: case 0b01011011: |
||||
case 0b01011100: case 0b01011101: case 0b01011110: case 0b01011111: |
||||
case 0b01100000: case 0b01100001: case 0b01100010: case 0b01100011: |
||||
case 0b01100100: case 0b01100101: case 0b01100110: case 0b01100111: |
||||
case 0b01101000: case 0b01101001: case 0b01101010: case 0b01101011: |
||||
case 0b01101100: case 0b01101101: case 0b01101110: case 0b01101111: |
||||
case 0b01110000: case 0b01110001: case 0b01110010: case 0b01110011: |
||||
case 0b01110100: case 0b01110101: /* 0b01110110 */ case 0b01110111: |
||||
case 0b01111000: case 0b01111001: case 0b01111010: case 0b01111011: |
||||
case 0b01111100: case 0b01111101: case 0b01111110: case 0b01111111: |
||||
OpLdRegReg(); break; |
||||
|
||||
// LD r, n (mask: 0b11000111, op: 0b00000110)
|
||||
case 0b00000110: case 0b00001110: case 0b00010110: case 0b00011110: |
||||
case 0b00100110: case 0b00101110: case 0b00110110: case 0b00111110: |
||||
OpLdRegImm(); break; |
||||
|
||||
// LD A, (rr) (mask: 0b11001111, op: 0b00001010)
|
||||
case 0b00001010: case 0b00011010: case 0b00101010: case 0b00111010: |
||||
OpLdRegMem(); break; |
||||
|
||||
// LD (rr), A (mask: 0b11001111, op: 0b00000010)
|
||||
case 0b00000010: case 0b00010010: case 0b00100010: case 0b00110010: |
||||
OpLdMemReg(); break; |
||||
|
||||
// LD rr, nn (mask: 0b11001111, op: 0b00000001)
|
||||
case 0b00000001: case 0b00010001: case 0b00100001: case 0b00110001: |
||||
OpLdReg16Imm16(); break; |
||||
|
||||
// JR cc, e (mask: 0b11100111, op: 0b00100000)
|
||||
case 0b00100000: case 0b00101000: case 0b00110000: case 0b00111000: |
||||
OpJumpRelCond(); break; |
||||
|
||||
// ADD A, r (mask: 0b11111000, op: 0b10000000)
|
||||
case 0b10000000: case 0b10000001: case 0b10000010: case 0b10000011: |
||||
case 0b10000100: case 0b10000101: case 0b10000110: case 0b10000111: |
||||
OpAdd(); break; |
||||
|
||||
// ADD HL, rr (mask: 0b11001111, op: 0b00001001)
|
||||
case 0b00001001: case 0b00011001: case 0b00101001: case 0b00111001: |
||||
OpAdd16(); break; |
||||
|
||||
// ADC A, r (mask: 0b11111000, op: 0b10001000)
|
||||
case 0b10001000: case 0b10001001: case 0b10001010: case 0b10001011: |
||||
case 0b10001100: case 0b10001101: case 0b10001110: case 0b10001111: |
||||
OpAddCarry(); break; |
||||
|
||||
// SUB A, r (mask: 0b11111000, 0b10010000)
|
||||
case 0b10010000: case 0b10010001: case 0b10010010: case 0b10010011: |
||||
case 0b10010100: case 0b10010101: case 0b10010110: case 0b10010111: |
||||
OpSub(); break; |
||||
|
||||
// SBC A, r (mask: 0b11111000, 0b10011000)
|
||||
case 0b10011000: case 0b10011001: case 0b10011010: case 0b10011011: |
||||
case 0b10011100: case 0b10011101: case 0b10011110: case 0b10011111: |
||||
OpSubBorrow(); break; |
||||
|
||||
// AND r (mask: 0b11111000, 0b10100000)
|
||||
case 0b10100000: case 0b10100001: case 0b10100010: case 0b10100011: |
||||
case 0b10100100: case 0b10100101: case 0b10100110: case 0b10100111: |
||||
OpAnd(); break; |
||||
|
||||
// XOR r (mask: 0b11111000, op: 0b10101000)
|
||||
case 0b10101000: case 0b10101001: case 0b10101010: case 0b10101011: |
||||
case 0b10101100: case 0b10101101: case 0b10101110: case 0b10101111: |
||||
OpXor(); break; |
||||
|
||||
// OR r (mask: 0b11111000, op: 0b10110000)
|
||||
case 0b10110000: case 0b10110001: case 0b10110010: case 0b10110011: |
||||
case 0b10110100: case 0b10110101: case 0b10110110: case 0b10110111: |
||||
OpOr(); break; |
||||
|
||||
// CP r (mask: 0b11111000, op: 0b10111000))
|
||||
case 0b10111000: case 0b10111001: case 0b10111010: case 0b10111011: |
||||
case 0b10111100: case 0b10111101: case 0b10111110: case 0b10111111: |
||||
OpCarry(); break; |
||||
|
||||
// RET cc (mask: 0b11100111, op: 0b11000000)
|
||||
case 0b11000000: case 0b11001000: case 0b11010000: case 0b11011000: |
||||
OpRetCond(); break; |
||||
|
||||
// JP cc, nn (mask: 0b11100111, op: 0b11000010)
|
||||
case 0b11000010: case 0b11001010: case 0b11010010: case 0b11011010: |
||||
OpJumpAbsCond(); break; |
||||
|
||||
// CALL cc, nn (mask: 0b11100111, op: 0b11000100)
|
||||
case 0b11000100: case 0b11001100: case 0b11010100: case 0b11011100: |
||||
OpCallCond(); break; |
||||
|
||||
// POP rr (mask: 0b11001111, op: 0b11000001)
|
||||
case 0b11000001: case 0b11010001: case 0b11100001: case 0b11110001: |
||||
OpPopReg16(); break; |
||||
|
||||
// PUSH rr (mask: 0b11001111, op: 0b11000101)
|
||||
case 0b11000101: case 0b11010101: case 0b11100101: case 0b11110101: |
||||
OpPushReg16(); break; |
||||
|
||||
// RST [00h, 08h, 10h, 18h, 20h, 28h, 30h, 38h]
|
||||
// (mask: 0b11000111, op: 0b11000111)
|
||||
case 0b11000111: case 0b11001111: case 0b11010111: case 0b11011111: |
||||
case 0b11100111: case 0b11101111: case 0b11110111: case 0b11111111: |
||||
OpFuncFixedUnc(); break; |
||||
|
||||
default: |
||||
std::stringstream ex; |
||||
ex << "unknown opcode: " << PlipUtility::FormatHex(m_instr[0], 2) |
||||
<< "\n\n" << DumpRegisters(); |
||||
throw PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
} |
||||
|
||||
void SharpLr35902::DecodeCB() { |
||||
switch(m_instr[1]) { |
||||
// RLC r (mask: 0b11111000, op: 0b00000000)
|
||||
case 0b00000000: case 0b00000001: case 0b00000010: case 0b00000011: |
||||
case 0b00000100: case 0b00000101: case 0b00000110: case 0b00000111: |
||||
OpRotateLeft(); break; |
||||
|
||||
// RRC r (mask: 0b11111000, op: 0b00001000)
|
||||
case 0b00001000: case 0b00001001: case 0b00001010: case 0b00001011: |
||||
case 0b00001100: case 0b00001101: case 0b00001110: case 0b00001111: |
||||
OpRotateRight(); break; |
||||
|
||||
// RL r (mask: 0b11111000, op: 0b00010000)
|
||||
case 0b00010000: case 0b00010001: case 0b00010010: case 0b00010011: |
||||
case 0b00010100: case 0b00010101: case 0b00010110: case 0b00010111: |
||||
OpRotateLeftThruCarry(); break; |
||||
|
||||
// RR r (mask: 0b11111000, op: 0b00011000)
|
||||
case 0b00011000: case 0b00011001: case 0b00011010: case 0b00011011: |
||||
case 0b00011100: case 0b00011101: case 0b00011110: case 0b00011111: |
||||
OpRotateRightThruCarry(); break; |
||||
|
||||
// SLA r (mask: 0b11111000, op: 0b00100000)
|
||||
case 0b00100000: case 0b00100001: case 0b00100010: case 0b00100011: |
||||
case 0b00100100: case 0b00100101: case 0b00100110: case 0b00100111: |
||||
OpShiftLeftArithmetic(); break; |
||||
|
||||
// SRA r (mask: 0b11111000, op: 0b00101000)
|
||||
case 0b00101000: case 0b00101001: case 0b00101010: case 0b00101011: |
||||
case 0b00101100: case 0b00101101: case 0b00101110: case 0b00101111: |
||||
OpShiftRightArithmetic(); break; |
||||
|
||||
// SWAP r (mask: 0b11111000, op: 0b00110000)
|
||||
case 0b00110000: case 0b00110001: case 0b00110010: case 0b00110011: |
||||
case 0b00110100: case 0b00110101: case 0b00110110: case 0b00110111: |
||||
OpNibbleSwap(); break; |
||||
|
||||
// SRL r (mask: 0b11111000, op: 0b00111000)
|
||||
case 0b00111000: case 0b00111001: case 0b00111010: case 0b00111011: |
||||
case 0b00111100: case 0b00111101: case 0b00111110: case 0b00111111: |
||||
OpShiftRightLogical(); break; |
||||
|
||||
// BIT n, r (mask: 0b11000000, op: 0b01000000)
|
||||
case 0b01000000: case 0b01000001: case 0b01000010: case 0b01000011: |
||||
case 0b01000100: case 0b01000101: case 0b01000110: case 0b01000111: |
||||
case 0b01001000: case 0b01001001: case 0b01001010: case 0b01001011: |
||||
case 0b01001100: case 0b01001101: case 0b01001110: case 0b01001111: |
||||
case 0b01010000: case 0b01010001: case 0b01010010: case 0b01010011: |
||||
case 0b01010100: case 0b01010101: case 0b01010110: case 0b01010111: |
||||
case 0b01011000: case 0b01011001: case 0b01011010: case 0b01011011: |
||||
case 0b01011100: case 0b01011101: case 0b01011110: case 0b01011111: |
||||
case 0b01100000: case 0b01100001: case 0b01100010: case 0b01100011: |
||||
case 0b01100100: case 0b01100101: case 0b01100110: case 0b01100111: |
||||
case 0b01101000: case 0b01101001: case 0b01101010: case 0b01101011: |
||||
case 0b01101100: case 0b01101101: case 0b01101110: case 0b01101111: |
||||
case 0b01110000: case 0b01110001: case 0b01110010: case 0b01110011: |
||||
case 0b01110100: case 0b01110101: case 0b01110110: case 0b01110111: |
||||
case 0b01111000: case 0b01111001: case 0b01111010: case 0b01111011: |
||||
case 0b01111100: case 0b01111101: case 0b01111110: case 0b01111111: |
||||
OpBitTest(); break; |
||||
|
||||
// RES n, r (mask: 0b11000000, op: 0b10000000)
|
||||
case 0b10000000: case 0b10000001: case 0b10000010: case 0b10000011: |
||||
case 0b10000100: case 0b10000101: case 0b10000110: case 0b10000111: |
||||
case 0b10001000: case 0b10001001: case 0b10001010: case 0b10001011: |
||||
case 0b10001100: case 0b10001101: case 0b10001110: case 0b10001111: |
||||
case 0b10010000: case 0b10010001: case 0b10010010: case 0b10010011: |
||||
case 0b10010100: case 0b10010101: case 0b10010110: case 0b10010111: |
||||
case 0b10011000: case 0b10011001: case 0b10011010: case 0b10011011: |
||||
case 0b10011100: case 0b10011101: case 0b10011110: case 0b10011111: |
||||
case 0b10100000: case 0b10100001: case 0b10100010: case 0b10100011: |
||||
case 0b10100100: case 0b10100101: case 0b10100110: case 0b10100111: |
||||
case 0b10101000: case 0b10101001: case 0b10101010: case 0b10101011: |
||||
case 0b10101100: case 0b10101101: case 0b10101110: case 0b10101111: |
||||
case 0b10110000: case 0b10110001: case 0b10110010: case 0b10110011: |
||||
case 0b10110100: case 0b10110101: case 0b10110110: case 0b10110111: |
||||
case 0b10111000: case 0b10111001: case 0b10111010: case 0b10111011: |
||||
case 0b10111100: case 0b10111101: case 0b10111110: case 0b10111111: |
||||
OpBitClear(); break; |
||||
|
||||
// SET n, r (mask: 0b11000000, op: 0b11000000)
|
||||
case 0b11000000: case 0b11000001: case 0b11000010: case 0b11000011: |
||||
case 0b11000100: case 0b11000101: case 0b11000110: case 0b11000111: |
||||
case 0b11001000: case 0b11001001: case 0b11001010: case 0b11001011: |
||||
case 0b11001100: case 0b11001101: case 0b11001110: case 0b11001111: |
||||
case 0b11010000: case 0b11010001: case 0b11010010: case 0b11010011: |
||||
case 0b11010100: case 0b11010101: case 0b11010110: case 0b11010111: |
||||
case 0b11011000: case 0b11011001: case 0b11011010: case 0b11011011: |
||||
case 0b11011100: case 0b11011101: case 0b11011110: case 0b11011111: |
||||
case 0b11100000: case 0b11100001: case 0b11100010: case 0b11100011: |
||||
case 0b11100100: case 0b11100101: case 0b11100110: case 0b11100111: |
||||
case 0b11101000: case 0b11101001: case 0b11101010: case 0b11101011: |
||||
case 0b11101100: case 0b11101101: case 0b11101110: case 0b11101111: |
||||
case 0b11110000: case 0b11110001: case 0b11110010: case 0b11110011: |
||||
case 0b11110100: case 0b11110101: case 0b11110110: case 0b11110111: |
||||
case 0b11111000: case 0b11111001: case 0b11111010: case 0b11111011: |
||||
case 0b11111100: case 0b11111101: case 0b11111110: case 0b11111111: |
||||
OpBitSet(); break; |
||||
|
||||
default: |
||||
std::stringstream ex; |
||||
ex << "unknown opcode: 0xCB " << PlipUtility::FormatHex(m_instr[1], 2) |
||||
<< "\n\n" << DumpRegisters(); |
||||
throw PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,99 @@
|
||||
/* SharpLr35902.Macros.h
|
||||
* |
||||
* Macros to assist with Sharp LR35902 emulation. |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include "../../PlipSupport.h" |
||||
|
||||
#define IDX_B 0b000 |
||||
#define IDX_C 0b001 |
||||
#define IDX_D 0b010 |
||||
#define IDX_E 0b011 |
||||
#define IDX_H 0b100 |
||||
#define IDX_L 0b101 |
||||
#define IDX_HL 0b110 |
||||
#define IDX_A 0b111 |
||||
|
||||
#define ADDR_BC 0b00 |
||||
#define ADDR_DE 0b01 |
||||
#define ADDR_HLP 0b10 // HL+
|
||||
#define ADDR_HLM 0b11 // HL-
|
||||
|
||||
#define IDX_16_BC 0b00 |
||||
#define IDX_16_DE 0b01 |
||||
#define IDX_16_HL 0b10 |
||||
#define IDX_16_SP 0b11 |
||||
|
||||
#define COND_NZ 0b00 |
||||
#define COND_Z 0b01 |
||||
#define COND_NC 0b10 |
||||
#define COND_C 0b11 |
||||
|
||||
#define ZERO 7 |
||||
#define SUBTRACT 6 |
||||
#define HALFCARRY 5 |
||||
#define CARRY 4 |
||||
|
||||
#define MEM_READ(addr) m_memory->GetByte(addr) |
||||
#define MEM_WRITE(addr, val) m_memory->SetByte(addr, val) |
||||
|
||||
#define FETCH m_instr.push_back(MEM_READ(m_reg.pc++)) |
||||
#define FETCH_CYCLE(cycle) do { if(m_mcycle == (cycle)) { FETCH; } } while(0) |
||||
#define FETCH_ADDR(addr) m_instr.push_back(MEM_READ(addr)) |
||||
#define FETCH_ADDR_CYCLE(cycle, addr) do { if(m_mcycle == (cycle)) { FETCH_ADDR(addr); } } while(0) |
||||
#define FETCH_IMM_CYCLE(cycle) do { if(m_mcycle == (cycle)) { FETCH; } } while(0) |
||||
|
||||
#define CYCLE(cycle) if(m_mcycle == (cycle)) |
||||
|
||||
#define BEGIN_EXECUTE m_allowFetch = false |
||||
#define END_EXECUTE m_instr.clear(); m_mcycle = 2; m_allowFetch = true |
||||
#define NUM_MCYCLES(val) do { if(++m_mcycle > (val)) { END_EXECUTE; } } while(0) |
||||
|
||||
#define OP_COND ((m_instr[0] >> 3) & 0b00000011) |
||||
#define OP_REG16(idx) ((m_instr[(idx)] >> 4) & 0b00000011) |
||||
#define OP_IDX(idx) ((m_instr[(idx)] >> 3) & 0b00000111) |
||||
#define OP_REG_X(idx) OP_IDX(idx) |
||||
#define OP_REG_Y(idx) (m_instr[(idx)] & 0b00000111) |
||||
|
||||
#define REG_BC COMBINE16LE(m_reg.b, m_reg.c) |
||||
#define REG_DE COMBINE16LE(m_reg.d, m_reg.e) |
||||
#define REG_HL COMBINE16LE(m_reg.h, m_reg.l) |
||||
|
||||
#define SET_PC_IMM m_reg.pc = (m_instr[2] << 8 | m_instr[1]) |
||||
#define SET_PC_STACK(cycle) \ |
||||
do { \
|
||||
CYCLE(cycle) { m_reg.pc &= 0xFF00; m_reg.pc |= STACK_POP; } \
|
||||
CYCLE((cycle) + 1) { m_reg.pc &= 0x00FF; m_reg.pc |= STACK_POP << 8; } \
|
||||
} while(0) |
||||
|
||||
#define STACK_PUSH(val) m_memory->SetByte(--m_reg.sp, (val)) |
||||
#define STACK_PUSH_PC(cycle) \ |
||||
do { \
|
||||
CYCLE(cycle) { STACK_PUSH(m_reg.pc >> 8); } \
|
||||
CYCLE((cycle) + 1) { STACK_PUSH(m_reg.pc & 0xFF); } \
|
||||
} while(0) |
||||
#define STACK_POP m_memory->GetByte(m_reg.sp++) |
||||
|
||||
#define FLAG_CLEAR(bit) BIT_CLEAR(m_reg.f, (bit)) |
||||
#define FLAG_FLIP(bit) BIT_FLIP(m_reg.f, (bit)) |
||||
#define FLAG_SET(bit) BIT_SET(m_reg.f, (bit)) |
||||
#define FLAG_TEST(bit) BIT_TEST(m_reg.f, (bit)) |
||||
|
||||
#define CHECK_BIT_CARRY(val) do { if(val) FLAG_SET(CARRY); else FLAG_CLEAR(CARRY); } while(0) |
||||
#define CHECK_CARRY(val) do { if((val) & 0xFF00) FLAG_SET(CARRY); else FLAG_CLEAR(CARRY); } while(0) |
||||
#define CHECK_CARRY16(val) do { if((val) & 0xFFFF0000) FLAG_SET(CARRY); else FLAG_CLEAR(CARRY); } while(0) |
||||
#define CHECK_ADD_HALFCARRY(left, right) do { \ |
||||
if(((left) & 0x0F) + ((right) & 0x0F) > 0x0F) FLAG_SET(HALFCARRY); \
|
||||
else FLAG_CLEAR(HALFCARRY); } while(0) |
||||
#define CHECK_SUB_HALFCARRY(left, right) do { \ |
||||
if(((left) & 0x0F) - ((right) & 0x0F) < 0x00) FLAG_SET(HALFCARRY); \
|
||||
else FLAG_CLEAR(HALFCARRY); } while(0) |
||||
#define CHECK_ADD_HALFCARRY16(left, right) do { \ |
||||
if(((left) & 0x0FFF) + ((right) & 0x0FFF) > 0x0FFF) FLAG_SET(HALFCARRY); \
|
||||
else FLAG_CLEAR(HALFCARRY); } while(0) |
||||
#define CHECK_SUB_HALFCARRY16(left, right) do { \ |
||||
if(((left) & 0x0FFF) - ((right) & 0x0FFF) < 0x0000) FLAG_SET(HALFCARRY); \
|
||||
else FLAG_CLEAR(HALFCARRY); } while(0) |
||||
#define CHECK_ZERO(val) do { if((val)) FLAG_CLEAR(ZERO); else FLAG_SET(ZERO); } while(0) |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,257 @@
|
||||
/* SharpLr35902.cpp
|
||||
* |
||||
* An implementation of the Sharp LR35902 (GameBoy) CPU. |
||||
*/ |
||||
|
||||
#include <sstream> |
||||
|
||||
#include "../../PlipEmulationException.h" |
||||
#include "../../PlipUtility.h" |
||||
|
||||
#include "SharpLr35902.h" |
||||
#include "SharpLr35902.Macros.h" |
||||
|
||||
namespace Plip::Cpu { |
||||
SharpLr35902::SharpLr35902(long hz, PlipMemoryMap *memoryMap) |
||||
: PlipCpu(hz, memoryMap) { |
||||
PerformReset(); |
||||
} |
||||
|
||||
void SharpLr35902::Cycle() { |
||||
// Decode an instruction if there's one in the cache and the CPU isn't
|
||||
// halted.
|
||||
if(!m_instr.empty() && !m_halt) |
||||
Decode(); |
||||
|
||||
// Handle interrupts here if (1) we're ready to fetch, (2) interrupts
|
||||
// are enabled, and (3) an interrupt request has been made.
|
||||
auto iFlag = m_memory->GetByte(MemInterruptFlag) & 0b00011111; |
||||
if(m_allowFetch && m_ime == ScheduledState::Enabled && iFlag && !m_isr) { |
||||
auto ie = m_memory->GetByte(MemInterruptEnabled); |
||||
|
||||
// Check for an enabled interrupt.
|
||||
m_isr = false; |
||||
for(m_isrIdx = 0; m_isrIdx <= 4; m_isrIdx++) { |
||||
if(ie & (iFlag & (1 << m_isrIdx))) { |
||||
m_isr = true; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if(m_isr) { |
||||
// Set up the CPU to jump to the interrupt handler.
|
||||
m_allowFetch = false; |
||||
m_ime = ScheduledState::Disabled; |
||||
m_isr = true; |
||||
m_mcycle = 1; |
||||
} |
||||
} |
||||
|
||||
if(m_isr) { |
||||
CYCLE(3) { |
||||
// Do one last check on IE. If this interrupt is no longer
|
||||
// enabled, cancel the interrupt request.
|
||||
auto ie = m_memory->GetByte(MemInterruptEnabled); |
||||
m_cancelInterrupt = !(ie & (1 << m_isrIdx)); |
||||
STACK_PUSH(m_reg.pc >> 8); |
||||
} |
||||
CYCLE(4) { |
||||
STACK_PUSH(m_reg.pc & 0xFF); |
||||
} |
||||
CYCLE(5) { |
||||
// If the interrupt was cancelled, set PC to 0x0000 instead
|
||||
// of the interrupt vector and skip updating IF.
|
||||
if(m_cancelInterrupt) { |
||||
m_reg.pc = 0x40 + (m_isrIdx * 0x8); |
||||
m_memory->SetByte(MemInterruptFlag, iFlag ^ (1 << m_isrIdx)); |
||||
} else { |
||||
m_reg.pc = 0; |
||||
} |
||||
|
||||
m_isr = false; |
||||
} |
||||
NUM_MCYCLES(5); |
||||
} |
||||
|
||||
// The LR35902 allows the next instruction to be fetched when the
|
||||
// previous instruction finishes its last execute stage.
|
||||
if(m_allowFetch && !m_halt && !m_isr) { |
||||
FETCH; |
||||
BEGIN_EXECUTE; |
||||
} |
||||
|
||||
// Enable interrupts if they are scheduled.
|
||||
if(m_ime == ScheduledState::Scheduled) m_ime = ScheduledState::Enabled; |
||||
} |
||||
|
||||
std::string SharpLr35902::DumpRegisters() const { |
||||
using util = PlipUtility; |
||||
std::stringstream dump; |
||||
|
||||
dump << util::DumpValue(" A", m_reg.a, 2) << '\n' |
||||
<< util::DumpValue(" F", m_reg.f, 2) << '\n' |
||||
<< util::DumpValue(" B", m_reg.b, 2) << '\n' |
||||
<< util::DumpValue(" C", m_reg.c, 2) << '\n' |
||||
<< util::DumpValue(" D", m_reg.d, 2) << '\n' |
||||
<< util::DumpValue(" E", m_reg.e, 2) << '\n' |
||||
<< util::DumpValue(" H", m_reg.h, 2) << '\n' |
||||
<< util::DumpValue(" L", m_reg.l, 2) << "\n\n" |
||||
<< util::DumpValue("PC", m_reg.pc, 4) << '\n' |
||||
<< util::DumpValue("SP", m_reg.sp, 4) << "\n" |
||||
<< "IME: " << (m_ime == ScheduledState::Enabled ? '1' : '0') << "\n\n" |
||||
<< "Instruction Cache:"; |
||||
|
||||
if(m_instr.empty()) { |
||||
dump << " [[empty]]"; |
||||
} else { |
||||
for(auto i : m_instr) |
||||
dump << " " << util::FormatHex(i, 2); |
||||
} |
||||
dump << "\nMCycle: " << std::to_string(m_mcycle); |
||||
|
||||
return dump.str(); |
||||
} |
||||
|
||||
uint16_t SharpLr35902::GetRegister16Pointer(uint8_t idx) { |
||||
uint16_t val; |
||||
|
||||
switch(idx) { |
||||
case ADDR_BC: return COMBINE16LE(m_reg.b, m_reg.c); |
||||
case ADDR_DE: return COMBINE16LE(m_reg.d, m_reg.e); |
||||
|
||||
case ADDR_HLP: |
||||
val = COMBINE16LE(m_reg.h, m_reg.l); |
||||
IncPair(&(m_reg.h), &(m_reg.l)); |
||||
return val; |
||||
|
||||
case ADDR_HLM: |
||||
val = COMBINE16LE(m_reg.h, m_reg.l); |
||||
DecPair(&(m_reg.h), &(m_reg.l)); |
||||
return val; |
||||
|
||||
default: |
||||
std::stringstream ex; |
||||
ex << "invalid address register pair index: " |
||||
<< PlipUtility::FormatHex(idx, 2) |
||||
<< "\n\n" << DumpRegisters(); |
||||
throw PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
} |
||||
|
||||
uint16_t SharpLr35902::GetRegister16Value(uint8_t idx) { |
||||
switch(idx) { |
||||
case IDX_16_BC: |
||||
return REG_BC; |
||||
case IDX_16_DE: |
||||
return REG_DE; |
||||
case IDX_16_HL: |
||||
return REG_HL; |
||||
case IDX_16_SP: |
||||
return m_reg.sp; |
||||
default: |
||||
std::stringstream ex; |
||||
ex << "invalid 16-bit register pair index: " |
||||
<< PlipUtility::FormatHex(idx, 2) |
||||
<< "\n\n" << DumpRegisters(); |
||||
throw PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
} |
||||
|
||||
uint8_t* SharpLr35902::GetRegister8(uint8_t idx) { |
||||
switch(idx) { |
||||
case IDX_A: return &(m_reg.a); |
||||
case IDX_B: return &(m_reg.b); |
||||
case IDX_C: return &(m_reg.c); |
||||
case IDX_D: return &(m_reg.d); |
||||
case IDX_E: return &(m_reg.e); |
||||
case IDX_H: return &(m_reg.h); |
||||
case IDX_L: return &(m_reg.l); |
||||
default: |
||||
std::stringstream ex; |
||||
ex << "invalid 8-bit register index: " |
||||
<< PlipUtility::FormatHex(idx, 2) |
||||
<< "\n\n" << DumpRegisters(); |
||||
throw PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
} |
||||
|
||||
std::tuple<uint8_t*, uint8_t*> SharpLr35902::GetRegisterPair(uint8_t idx) { |
||||
uint8_t *high, *low; |
||||
switch(idx) { |
||||
case IDX_16_BC: |
||||
high = &(m_reg.b); |
||||
low = &(m_reg.c); |
||||
break; |
||||
case IDX_16_DE: |
||||
high = &(m_reg.d); |
||||
low = &(m_reg.e); |
||||
break; |
||||
case IDX_16_HL: |
||||
high = &(m_reg.h); |
||||
low = &(m_reg.l); |
||||
break; |
||||
case IDX_16_SP: |
||||
low = (uint8_t*)(&(m_reg.sp)); |
||||
high = low + 1; // :)
|
||||
break; |
||||
default: |
||||
std::stringstream ex; |
||||
ex << "invalid 16-bit register pair index: " |
||||
<< PlipUtility::FormatHex(idx, 2) |
||||
<< "\n\n" << DumpRegisters(); |
||||
throw PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
return std::make_tuple(high, low); |
||||
} |
||||
|
||||
void SharpLr35902::Interrupt(uint8_t irq) { |
||||
auto iFlag = m_memory->GetByte(MemInterruptFlag); |
||||
iFlag |= irq; |
||||
m_memory->SetByte(MemInterruptFlag, iFlag); |
||||
|
||||
// Take the CPU out of halt mode if this particular interrupt is
|
||||
// enabled, even if the IME is disabled.
|
||||
auto iEnabled = m_memory->GetByte(MemInterruptEnabled); |
||||
if(iEnabled & iFlag) |
||||
m_halt = false; |
||||
} |
||||
|
||||
void SharpLr35902::PerformReset() { |
||||
m_reg.a = 0; |
||||
m_reg.f = 0; |
||||
m_reg.b = 0; |
||||
m_reg.c = 0; |
||||
m_reg.d = 0; |
||||
m_reg.e = 0; |
||||
m_reg.h = 0; |
||||
m_reg.l = 0; |
||||
|
||||
m_reg.sp = 0; |
||||
m_reg.pc = 0; |
||||
|
||||
m_ime = ScheduledState::Disabled; |
||||
|
||||
m_allowFetch = true; |
||||
m_instr.clear(); |
||||
m_mcycle = 0; |
||||
} |
||||
|
||||
void SharpLr35902::Reset() { |
||||
PerformReset(); |
||||
} |
||||
|
||||
bool SharpLr35902::TestConditional(uint8_t idx) const { |
||||
switch(idx) { |
||||
case COND_NZ: return !FLAG_TEST(ZERO); |
||||
case COND_Z: return FLAG_TEST(ZERO); |
||||
case COND_NC: return !FLAG_TEST(CARRY); |
||||
case COND_C: return FLAG_TEST(CARRY); |
||||
default: |
||||
std::stringstream ex; |
||||
ex << "invalid condition: " |
||||
<< PlipUtility::FormatHex(idx, 2) |
||||
<< "\n\n" << DumpRegisters(); |
||||
throw PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,167 @@
|
||||
/* SharpLr35902.h
|
||||
* |
||||
* An implementation of the Sharp LR35902 (GameBoy) CPU. |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <string> |
||||
#include <tuple> |
||||
#include <vector> |
||||
|
||||
#include "../PlipCpu.h" |
||||
#include "../../PlipSupport.h" |
||||
#include "../../Memory/PlipMemoryMap.h" |
||||
|
||||
#define INTERRUPT_VBLANK 0b00000001 |
||||
#define INTERRUPT_LCDSTAT 0b00000010 |
||||
#define INTERRUPT_TIMER 0b00000100 |
||||
#define INTERRUPT_SERIAL 0b00010000 |
||||
#define INTERRUPT_JOYPAD 0b00100000 |
||||
|
||||
namespace Plip::Cpu { |
||||
class SharpLr35902 : public PlipCpu { |
||||
public: |
||||
SharpLr35902(long hz, PlipMemoryMap* memoryMap); |
||||
|
||||
struct Registers { |
||||
uint8_t a; |
||||
uint8_t f; |
||||
uint8_t b; |
||||
uint8_t c; |
||||
uint8_t d; |
||||
uint8_t e; |
||||
uint8_t h; |
||||
uint8_t l; |
||||
|
||||
uint16_t sp; |
||||
uint16_t pc; |
||||
}; |
||||
|
||||
void Cycle() override; |
||||
[[nodiscard]] std::string DumpRegisters() const; |
||||
void Interrupt(uint8_t irq); |
||||
[[nodiscard]] uint32_t GetPc() override { return m_reg.pc; } |
||||
Registers GetRegisters() { return m_reg; } |
||||
void Reset() override; |
||||
|
||||
static const uint16_t MemInterruptEnabled = 0xFFFF; |
||||
static const uint16_t MemInterruptFlag = 0xFF0F; |
||||
|
||||
private: |
||||
void PerformReset(); |
||||
|
||||
void Decode(); |
||||
void DecodeCB(); |
||||
uint8_t* GetRegister8(uint8_t idx); |
||||
std::tuple<uint8_t*, uint8_t*> GetRegisterPair(uint8_t idx); |
||||
uint16_t GetRegister16Pointer(uint8_t idx); |
||||
uint16_t GetRegister16Value(uint8_t idx); |
||||
[[nodiscard]] bool TestConditional(uint8_t idx) const; |
||||
|
||||
static inline uint16_t Combine(const uint8_t *high, const uint8_t *low) { |
||||
return ((*high << 8) + *low); |
||||
} |
||||
|
||||
static inline void Split(uint16_t val, uint8_t *high, uint8_t *low) { |
||||
*high = val >> 8; |
||||
*low = val & 0xFF; |
||||
} |
||||
|
||||
static inline void DecPair(uint8_t *high, uint8_t *low, uint16_t amount = 1) { |
||||
uint16_t val = Combine(high, low) - amount; |
||||
Split(val, high, low); |
||||
} |
||||
|
||||
static inline void IncPair(uint8_t *high, uint8_t *low, uint16_t amount = 1) { |
||||
uint16_t val = Combine(high, low) + amount; |
||||
Split(val, high, low); |
||||
} |
||||
|
||||
// Standard opcodes.
|
||||
void OpAccumAddImm(); |
||||
void OpAccumAddCarryImm(); |
||||
void OpAccumAndImm(); |
||||
void OpAccumBcd(); |
||||
void OpAccumCarryImm(); |
||||
void OpAccumFlip(); |
||||
void OpAccumOrImm(); |
||||
void OpAccumRotateLeft(); |
||||
void OpAccumRotateLeftThruCarry(); |
||||
void OpAccumRotateRight(); |
||||
void OpAccumRotateRightThruCarry(); |
||||
void OpAccumSubImm(); |
||||
void OpAccumSubBorrowImm(); |
||||
void OpAccumXorImm(); |
||||
void OpAdd(); |
||||
void OpAdd16(); |
||||
void OpAddCarry(); |
||||
void OpAddSpOffset(); |
||||
void OpAnd(); |
||||
void OpCallCond(); |
||||
void OpCallUnc(); |
||||
void OpCarry(); |
||||
void OpDecReg(); |
||||
void OpDecPair(); |
||||
void OpDisableInterrupts(); |
||||
void OpEnableInterrupts(); |
||||
void OpFlipCarry(); |
||||
void OpFuncFixedUnc(); |
||||
void OpHalt(); |
||||
void OpIncReg(); |
||||
void OpIncPair(); |
||||
void OpJumpAbsCond(); |
||||
void OpJumpAbsUnc(); |
||||
void OpJumpRegUnc(); |
||||
void OpJumpRelCond(); |
||||
void OpJumpRelUnc(); |
||||
void OpLdAccumMem(); |
||||
void OpLdAccumMemHighImm(); |
||||
void OpLdAccumMemHighC(); |
||||
void OpLdHlSpOffset(); |
||||
void OpLdMemAccum(); |
||||
void OpLdMemHighImmAccum(); |
||||
void OpLdMemHighCAccum(); |
||||
void OpLdMemReg(); |
||||
void OpLdMemSp(); |
||||
void OpLdReg16Imm16(); |
||||
void OpLdRegImm(); |
||||
void OpLdRegMem(); |
||||
void OpLdRegReg(); |
||||
void OpLdSpHl(); |
||||
void OpOr(); |
||||
void OpPopReg16(); |
||||
void OpPushReg16(); |
||||
void OpRetCond(); |
||||
void OpRetUnc(); |
||||
void OpRetImeUnc(); |
||||
void OpSetCarry(); |
||||
void OpStop() const; |
||||
void OpSub(); |
||||
void OpSubBorrow(); |
||||
void OpXor(); |
||||
|
||||
// CB-prefixed opcodes.
|
||||
void OpBitClear(); |
||||
void OpBitSet(); |
||||
void OpBitTest(); |
||||
void OpNibbleSwap(); |
||||
void OpRotateLeft(); |
||||
void OpRotateLeftThruCarry(); |
||||
void OpRotateRight(); |
||||
void OpRotateRightThruCarry(); |
||||
void OpShiftLeftArithmetic(); |
||||
void OpShiftRightArithmetic(); |
||||
void OpShiftRightLogical(); |
||||
|
||||
bool m_allowFetch = true; |
||||
bool m_cancelInterrupt = false; |
||||
bool m_isr = false; |
||||
uint8_t m_isrIdx = 0; |
||||
bool m_halt = false; |
||||
ScheduledState m_ime = ScheduledState::Disabled; |
||||
std::vector<uint8_t> m_instr; |
||||
Registers m_reg {}; |
||||
uint8_t m_mcycle = 0; |
||||
}; |
||||
} |
@ -0,0 +1,29 @@
|
||||
/* PlipConfig.cpp
|
||||
* |
||||
* A configuration interface for Plip and its cores. |
||||
*/ |
||||
|
||||
#include "PlipConfig.h" |
||||
|
||||
#include <algorithm> |
||||
|
||||
namespace Plip { |
||||
static inline std::string ToLower(std::string str) { |
||||
std::transform(str.cbegin(), str.cend(), str.begin(), [](unsigned char c) { |
||||
return std::tolower(c); |
||||
}); |
||||
|
||||
return str; |
||||
} |
||||
|
||||
const std::string& PlipConfig::GetOption(const std::string &key) { |
||||
auto itConfig = m_config.find(ToLower(key)); |
||||
if(itConfig == m_config.cend()) return empty; |
||||
|
||||
return itConfig->second; |
||||
} |
||||
|
||||
void PlipConfig::SetOption(const std::string &key, const std::string &value) { |
||||
m_config.insert(std::pair<std::string, std::string>(ToLower(key), value)); |
||||
} |
||||
} |
@ -0,0 +1,22 @@
|
||||
/* PlipConfig.h
|
||||
* |
||||
* A configuration system for Plip and its cores. |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <string> |
||||
#include <unordered_map> |
||||
|
||||
namespace Plip { |
||||
class PlipConfig { |
||||
public: |
||||
virtual const std::string& GetOption(const std::string &key) final; |
||||
virtual void SetOption(const std::string &key, const std::string &value) final; |
||||
|
||||
const std::string empty = "\xff"; |
||||
|
||||
private: |
||||
std::unordered_map<std::string, std::string> m_config {}; |
||||
}; |
||||
} |
@ -0,0 +1,17 @@
|
||||
/* PlipInitializationException.h
|
||||
* |
||||
* An exception that may occur during emulation. |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <stdexcept> |
||||
#include <string> |
||||
#include <utility> |
||||
|
||||
namespace Plip { |
||||
struct PlipInitializationException : std::runtime_error { |
||||
explicit PlipInitializationException(const char *message) |
||||
: std::runtime_error(message) {} |
||||
}; |
||||
} |
@ -0,0 +1,21 @@
|
||||
/* PlipMacros.h
|
||||
* |
||||
* Macros that are useful to all Plip subsystem.s |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
namespace Plip { |
||||
enum class ScheduledState { |
||||
Disabled, |
||||
Enabled, |
||||
Scheduled |
||||
}; |
||||
} |
||||
|
||||
#define BIT_CLEAR(var, bit) var &= ~(1 << (bit)) |
||||
#define BIT_FLIP(var, bit) var ^= (1 << (bit)) |
||||
#define BIT_SET(var, bit) var |= (1 << (bit)) |
||||
#define BIT_TEST(var, bit) (var & (1 << (bit))) |
||||
|
||||
#define COMBINE16LE(high, low) (((high) << 8) | (low)) |
@ -0,0 +1,328 @@
|
||||
/* Console.cpp
|
||||
* |
||||
* A Quake-style console implementation. |
||||
*/ |
||||
|
||||
#include <list> |
||||
#include <sstream> |
||||
#include <utility> |
||||
|
||||
#include <SDL.h> |
||||
#include <SDL_image.h> |
||||
|
||||
#include "Console.h" |
||||
#include "StringUtil.h" |
||||
|
||||
namespace PlipSdl { |
||||
Console::Console(SdlWindow *wnd) { |
||||
m_video = wnd; |
||||
m_renderer = wnd->GetRenderer(); |
||||
m_vidFmt = Plip::PlipVideo::GetFormatInfo(m_video->GetFormat()); |
||||
IMG_Init(0); |
||||
|
||||
RegisterInternalCommands(); |
||||
} |
||||
|
||||
Console::~Console() { |
||||
IMG_Quit(); |
||||
} |
||||
|
||||
void Console::Clear() { |
||||
for(auto i = 0; i < m_conWidth * m_conHeight; i++) |
||||
m_conBuffer[i] = 0x20; |
||||
|
||||
m_cursor = 0; |
||||
} |
||||
|
||||
void Console::Draw() { |
||||
if(m_renderer == nullptr) return; |
||||
|
||||
// Background
|
||||
SDL_SetRenderDrawColor(m_renderer, 0x00, 0x00, 0x00, 0xC0); |
||||
SDL_RenderClear(m_renderer); |
||||
|
||||
// Foreground
|
||||
SDL_Rect src, dest; |
||||
src.w = dest.w = m_charWidth; |
||||
src.h = dest.h = m_charHeight; |
||||
for(auto i = 0; i < m_conWidth * m_conHeight; i++) { |
||||
src.x = (m_conBuffer[i] % m_charCountX) * m_charWidth; |
||||
src.y = (m_conBuffer[i] / m_charCountY) * m_charHeight; |
||||
dest.x = (i % m_conWidth) * m_charWidth; |
||||
dest.y = (i / m_conWidth) * m_charHeight; |
||||
|
||||
SDL_RenderCopy(m_renderer, m_fontTex, &src, &dest); |
||||
} |
||||
} |
||||
|
||||
void Console::EnterPressed() { |
||||
Write("\n"); |
||||
|
||||
if(m_input.empty()) { |
||||
NewCommand(); |
||||
return; |
||||
} |
||||
|
||||
std::string str(m_input.cbegin(), m_input.cend()); |
||||
if(StringUtil::Trim(str).empty()) { |
||||
NewCommand(); |
||||
return; |
||||
} |
||||
|
||||
std::vector<std::string> cmdLine(StringUtil::Split(str, ' ')); |
||||
auto cmd = StringUtil::ToLower(cmdLine[0]); |
||||
|
||||
// Build a list of candidates.
|
||||
std::list<Command> candidates; |
||||
auto len = cmd.length(); |
||||
for(const auto &it : m_commandList) { |
||||
if(it.name.substr(0, len) != cmd) continue; |
||||
candidates.push_back(it); |
||||
} |
||||
|
||||
if(candidates.empty()) { |
||||
// No candidates.
|
||||
WriteError("command not found"); |
||||
} else if(candidates.size() == 1) { |
||||
// Only one candidate. Execute the attached function.
|
||||
candidates.cbegin()->callback(this, cmdLine); |
||||
} else { |
||||
std::stringstream ss; |
||||
bool first = true; |
||||
ss << "Ambiguous command. Possible candidates: "; |
||||
|
||||
// Check for an exact match.
|
||||
for(const auto &it : candidates) { |
||||
if(it.name != cmd) { |
||||
if(first) first = false; |
||||
else ss << ", "; |
||||
ss << it.name; |
||||
continue; |
||||
} |
||||
|
||||
it.callback(this, cmdLine); |
||||
goto done; |
||||
} |
||||
|
||||
// Print the list of candidates.
|
||||
WriteLine(ss.str()); |
||||
} |
||||
|
||||
done: |
||||
NewCommand(); |
||||
} |
||||
|
||||
bool Console::GetConsoleEnabled() const { |
||||
return m_consoleEnabled; |
||||
} |
||||
|
||||
bool Console::LoadFont(const std::string &filename) { |
||||
auto newFont = IMG_Load(filename.c_str()); |
||||
if(!newFont) return false; |
||||
|
||||
if(m_fontSurf != nullptr) SDL_FreeSurface(m_fontSurf); |
||||
m_fontSurf = newFont; |
||||
|
||||
if(m_fontTex != nullptr) SDL_DestroyTexture(m_fontTex); |
||||
m_fontTex = SDL_CreateTextureFromSurface(m_renderer, m_fontSurf); |
||||
SDL_SetTextureBlendMode(m_fontTex, SDL_BLENDMODE_ADD); |
||||
|
||||
m_charWidth = m_fontSurf->w / m_charCountX; |
||||
m_charHeight = m_fontSurf->h / m_charCountY; |
||||
Resize(); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
bool Console::ParseInt(const std::string &str, int *val) { |
||||
if(str.empty()) return false; |
||||
|
||||
try { |
||||
*val = std::stoi(str, nullptr, 0); |
||||
} catch(std::invalid_argument&) { |
||||
return false; |
||||
} catch(std::out_of_range&) { |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
bool Console::ParseLong(const std::string &str, long *val) { |
||||
if(str.empty()) return false; |
||||
|
||||
try { |
||||
*val = std::stol(str, nullptr, 0); |
||||
} catch(std::invalid_argument&) { |
||||
return false; |
||||
} catch(std::out_of_range&) { |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
bool Console::ParseULong(const std::string &str, unsigned long *val) { |
||||
if(str.empty()) return false; |
||||
|
||||
try { |
||||
*val = std::stol(str, nullptr, 0); |
||||
} catch(std::invalid_argument&) { |
||||
return false; |
||||
} catch(std::out_of_range&) { |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
SdlUiEvent Console::ProcessEvents() { |
||||
SDL_Event ev; |
||||
auto uiEvent = SdlUiEvent::None; |
||||
|
||||
while(SDL_PollEvent(&ev)) { |
||||
int idx; |
||||
char ch; |
||||
switch(ev.type) { |
||||
case SDL_KEYDOWN: |
||||
if(ev.key.keysym.scancode == m_consoleKey) |
||||
ToggleConsole(); |
||||
|
||||
if(ev.key.keysym.scancode == SDL_SCANCODE_BACKSPACE) { |
||||
if(m_input.empty()) break; |
||||
|
||||
m_input.pop_back(); |
||||
m_conBuffer[--m_cursor] = 0x20; |
||||
} |
||||
|
||||
if(ev.key.keysym.scancode == SDL_SCANCODE_KP_ENTER || |
||||
ev.key.keysym.scancode == SDL_SCANCODE_RETURN || |
||||
ev.key.keysym.scancode == SDL_SCANCODE_RETURN2) { |
||||
EnterPressed(); |
||||
} |
||||
break; |
||||
|
||||
case SDL_TEXTINPUT: |
||||
idx = 0; |
||||
while((ch = ev.text.text[idx++]) != 0) { |
||||
m_input.push_back(ch); |
||||
Write(ch); |
||||
} |
||||
break; |
||||
|
||||
case SDL_QUIT: |
||||
uiEvent = SdlUiEvent::Quit; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
return uiEvent; |
||||
} |
||||
|
||||
void Console::RegisterCommand(const std::string &commandName, |
||||
std::function<void(Console*, const std::vector<std::string> &args)> callback) { |
||||
auto lowerName = StringUtil::ToLower(commandName); |
||||
|
||||
m_commandList.push_back({ lowerName, std::move(callback) }); |
||||
m_commandList.sort([](const Command &first, const Command &second) { |
||||
return first.name < second.name; |
||||
}); |
||||
} |
||||
|
||||
void Console::RegisterInternalCommands() { |
||||
RegisterCommand("help", [](Console *console, const std::vector<std::string>&) { |
||||
console->Write("valid commands: "); |
||||
|
||||
bool first = true; |
||||
for(auto &it : console->m_commandList) { |
||||
if(first) first = false; |
||||
else console->Write(", "); |
||||
|
||||
console->Write(it.name); |
||||
} |
||||
console->WriteLine(); |
||||
}); |
||||
} |
||||
|
||||
void Console::Resize() { |
||||
if(m_charWidth == 0 || m_charHeight == 0) return; |
||||
|
||||
delete m_conBuffer; |
||||
delete m_videoBuffer; |
||||
|
||||
m_wndWidth = m_video->GetWidth() * m_video->GetGameScale(); |
||||
m_wndHeight = m_video->GetHeight() * m_video->GetGameScale(); |
||||
m_conWidth = m_wndWidth / m_charWidth; |
||||
m_conHeight = m_wndHeight / m_charHeight; |
||||
|
||||
m_conBuffer = new uint8_t[m_conWidth * m_conHeight]; |
||||
m_videoBuffer = new uint8_t[m_wndWidth * m_wndHeight * m_vidFmt.pixelWidth]; |
||||
|
||||
Clear(); |
||||
} |
||||
|
||||
void Console::Run() { |
||||
m_video->BeginDrawConsole(); |
||||
Draw(); |
||||
m_video->EndDrawConsole(); |
||||
m_video->Render(); |
||||
} |
||||
|
||||
void Console::Scroll(int lines) { |
||||
if(lines <= 0) return; |
||||
if(lines >= m_conHeight) { |
||||
Clear(); |
||||
m_cursor = m_conWidth * (m_conHeight - 1); |
||||
} |
||||
|
||||
memcpy(m_conBuffer, |
||||
m_conBuffer + (m_conWidth * lines), |
||||
m_conWidth * m_conHeight - (m_conWidth * lines)); |
||||
memset(m_conBuffer + (m_conWidth * (m_conHeight - lines)), |
||||
0x20, lines * m_conWidth); |
||||
|
||||
m_cursor -= m_conWidth; |
||||
} |
||||
|
||||
void Console::SetConsoleEnabled(bool enabled) { |
||||
m_consoleEnabled = enabled; |
||||
m_video->SetConsoleEnabled(m_consoleEnabled); |
||||
} |
||||
|
||||
void Console::SetConsoleKey(SDL_Scancode scancode) { |
||||
m_consoleKey = scancode; |
||||
} |
||||
|
||||
void Console::SetConsoleKey(const std::string &binding) { |
||||
auto scancode = SDL_GetScancodeFromName(binding.c_str()); |
||||
SetConsoleKey(scancode); |
||||
} |
||||
|
||||
void Console::ToggleConsole() { |
||||
Initialize(); |
||||
SetConsoleEnabled(!m_consoleEnabled); |
||||
|
||||
if(m_consoleEnabled) |
||||
SDL_StartTextInput(); |
||||
else |
||||
SDL_StopTextInput(); |
||||
} |
||||
|
||||
void Console::Write(const char ch) { |
||||
Initialize(); |
||||
if(ch == '\n') { |
||||
auto row = m_cursor / m_conWidth; |
||||
m_cursor = (row + 1) * m_conWidth; |
||||
} else { |
||||
m_conBuffer[m_cursor++] = ch; |
||||
} |
||||
|
||||
if(m_cursor >= m_conWidth * m_conHeight) |
||||
Scroll(); |
||||
} |
||||
|
||||
void Console::Write(const std::string &str) { |
||||
for(auto ch : str) |
||||
Write(ch); |
||||
} |
||||
} |
@ -0,0 +1,107 @@
|
||||
/* Console.h
|
||||
* |
||||
* A Quake-style console implementation. |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <functional> |
||||
#include <list> |
||||
#include <vector> |
||||
|
||||
#include "SDL/SdlEvent.h" |
||||
#include "SDL/SdlWindow.h" |
||||
|
||||
namespace PlipSdl { |
||||
class Console { |
||||
public: |
||||
explicit Console(SdlWindow *wnd); |
||||
~Console(); |
||||
|
||||
static bool ParseInt(const std::string &str, int *val); |
||||
static bool ParseLong(const std::string &str, long *val); |
||||
static bool ParseULong(const std::string &str, unsigned long *val); |
||||
|
||||
void Clear(); |
||||
void Draw(); |
||||
[[nodiscard]] bool GetConsoleEnabled() const; |
||||
bool LoadFont(const std::string &filename); |
||||
SdlUiEvent ProcessEvents(); |
||||
void RegisterCommand(const std::string &commandName, |
||||
std::function<void(Console*, const std::vector<std::string> &args)> callback); |
||||
void Resize(); |
||||
void Run(); |
||||
void SetConsoleEnabled(bool enabled); |
||||
void SetConsoleKey(SDL_Scancode scancode); |
||||
void SetConsoleKey(const std::string &binding); |
||||
void ToggleConsole(); |
||||
void Write(char ch); |
||||
void Write(const std::string &str); |
||||
|
||||
inline void WriteError(const std::string &str) { |
||||
Write("error: "); |
||||
WriteLine(str); |
||||
} |
||||
|
||||
inline void WriteLine(const std::string &str = "") { |
||||
if(!str.empty()) |
||||
Write(str); |
||||
Write('\n'); |
||||
} |
||||
|
||||
inline void WriteWarn(const std::string &str) { |
||||
Write("warn: "); |
||||
WriteLine(str); |
||||
} |
||||
|
||||
private: |
||||
struct Command { |
||||
std::string name; |
||||
std::function<void(Console*, const std::vector<std::string> &args)> callback; |
||||
}; |
||||
|
||||
void EnterPressed(); |
||||
void RegisterInternalCommands(); |
||||
void Scroll(int lines = 1); |
||||
|
||||
inline void DisplayPrompt() { |
||||
Write("] "); |
||||
} |
||||
|
||||
inline void Initialize() { |
||||
if(!m_firstUse) return; |
||||
|
||||
Resize(); |
||||
m_firstUse = false; |
||||
DisplayPrompt(); |
||||
} |
||||
|
||||
inline void NewCommand() { |
||||
m_input.clear(); |
||||
DisplayPrompt(); |
||||
} |
||||
|
||||
const int m_charCountX = 16; |
||||
const int m_charCountY = 16; |
||||
|
||||
bool m_consoleEnabled = false; |
||||
SDL_Scancode m_consoleKey {}; |
||||
bool m_firstUse = true; |
||||
|
||||
SDL_Renderer *m_renderer = nullptr; |
||||
SdlWindow *m_video; |
||||
uint8_t *m_videoBuffer = nullptr; |
||||
SDL_Surface *m_fontSurf = nullptr; |
||||
SDL_Texture *m_fontTex = nullptr; |
||||
Plip::PlipVideoFormatInfo m_vidFmt {}; |
||||
|
||||
int m_wndWidth = 0, m_wndHeight = 0; |
||||
int m_charWidth = 0, m_charHeight = 0; |
||||
int m_conWidth = 0, m_conHeight = 0; |
||||
int m_cursor = 0; |
||||
|
||||
uint8_t *m_conBuffer {}; |
||||
std::vector<char> m_input {}; |
||||
std::list<Command> m_commandList {}; |
||||
}; |
||||
} |
@ -0,0 +1,184 @@
|
||||
/* GameLoop.cpp
|
||||
* |
||||
* The main emulation loop. |
||||
*/ |
||||
|
||||
#include "GameLoop.h" |
||||
|
||||
namespace PlipSdl { |
||||
GameLoop::GameLoop(Plip::PlipInstance *plip, Console *console, SdlEvent *event, Timer *timer, int updateRate) { |
||||
m_plip = plip; |
||||
m_event = event; |
||||
m_timer = timer; |
||||
m_wnd = (SdlWindow*)plip->GetVideo(); |
||||
|
||||
m_updateRate = updateRate; |
||||
m_updateTime = 1000000000 / m_updateRate; |
||||
|
||||
m_console = console; |
||||
|
||||
m_console->RegisterCommand("bp", |
||||
[this](auto &&console, auto &&args) { ConsoleSetBreak(console, args); }); |
||||
m_console->RegisterCommand("clearbp", |
||||
[this](auto&&, auto&&) { m_plip->GetCore()->ClearBreakpoint(); }); |
||||
m_console->RegisterCommand("dump", |
||||
[this](auto &&console, auto &&args) { ConsoleDump(console, args); }); |
||||
m_console->RegisterCommand("mem", |
||||
[this](auto &&console, auto &&args) { ConsoleMem(console, args); }); |
||||
m_console->RegisterCommand("quit", |
||||
[this](auto&&, auto&&) { m_running = false; }); |
||||
} |
||||
|
||||
void GameLoop::ConsoleDump(Console *console, const std::vector<std::string> &args) { |
||||
console->WriteLine(m_plip->GetCore()->DumpRegisters()); |
||||
} |
||||
|
||||
void GameLoop::ConsoleMem(Console *console, const std::vector<std::string> &args) { |
||||
std::stringstream ss; |
||||
auto mem = m_plip->GetCore()->GetMemoryMap(); |
||||
|
||||
if(args.size() < 2) { |
||||
console->WriteLine("usage: mem [address] ([value])"); |
||||
return; |
||||
} |
||||
|
||||
unsigned long input; |
||||
if(!Console::ParseULong(args[1], &input)) { |
||||
console->WriteError("unable to parse address"); |
||||
return; |
||||
} |
||||
|
||||
auto addr = (uint32_t)input; |
||||
if(addr >= mem->GetLength()) { |
||||
ss << "specified address is out of range\n" |
||||
<< "maximum value: " << PrintHex(mem->GetLength() - 1, 8) << "\n"; |
||||
console->WriteError(ss.str()); |
||||
return; |
||||
} |
||||
|
||||
uint8_t val; |
||||
if(args.size() < 3) { |
||||
val = mem->GetByte(addr); |
||||
} else { |
||||
if(!Console::ParseULong(args[2], &input)) { |
||||
console->WriteError("unable to parse value"); |
||||
return; |
||||
} |
||||
|
||||
if(input > 0xFF) |
||||
console->WriteWarn("value will be truncated"); |
||||
|
||||
val = input & 0xFF; |
||||
mem->SetByte(addr, val); |
||||
} |
||||
|
||||
ss << "[" << PrintHex(addr, 8) << "] == " << std::to_string(val) << " (" |
||||
<< PrintHex(val, 2) << ")"; |
||||
console->WriteLine(ss.str()); |
||||
} |
||||
|
||||
void GameLoop::ConsoleSetBreak(Console *console, const std::vector<std::string> &args) { |
||||
std::stringstream ss; |
||||
|
||||
if(args.size() < 2) { |
||||
console->WriteLine("usage: bp [addr]"); |
||||
return; |
||||
} |
||||
|
||||
unsigned long input; |
||||
if(!Console::ParseULong(args[1], &input)) { |
||||
console->WriteError("unable to parse address"); |
||||
return; |
||||
} |
||||
|
||||
auto core = m_plip->GetCore(); |
||||
auto mem = core->GetMemoryMap(); |
||||
if(input >= core->GetMemoryMap()->GetLength()) { |
||||
ss << "specified address is out of range\n" |
||||
<< "maximum value: " << PrintHex(mem->GetLength() - 1, 8) << "\n"; |
||||
console->WriteError(ss.str()); |
||||
return; |
||||
} |
||||
|
||||
core->SetBreakpoint((uint32_t)input); |
||||
} |
||||
|
||||
void GameLoop::Play() { |
||||
auto audio = m_plip->GetAudio(); |
||||
auto core = m_plip->GetCore(); |
||||
auto turbo = false; |
||||
|
||||
m_running = true; |
||||
SdlUiEvent uiEvent; |
||||
while(m_running) { |
||||
m_timer->StopwatchStart(); |
||||
m_wnd->SetDrawPauseIcon(core->GetPaused()); |
||||
|
||||
if(m_console->GetConsoleEnabled()) { |
||||
// Console is enabled. Don't run the core, but make sure that its
|
||||
// video output remains current.
|
||||
uiEvent = m_console->ProcessEvents(); |
||||
core->Redraw(); |
||||
m_console->Run(); |
||||
} else { |
||||
uiEvent = m_event->ProcessEvents(); |
||||
|
||||
if(core->GetPaused()) { |
||||
if(uiEvent == SdlUiEvent::Step) { |
||||
m_plip->Run(core->GetStepTime()); |
||||
} else if(uiEvent == SdlUiEvent::FrameAdvance) { |
||||
m_plip->Run(m_updateTime); |
||||
} |
||||
|
||||
// Ensure that the screen stays updated.
|
||||
core->Redraw(); |
||||
} else { |
||||
// As implemented, this will not be able to compensate for the host being
|
||||
// unable to keep up with the emulation core.
|
||||
// TODO: Fix this so that it will skip frames where appropriate.
|
||||
m_plip->Run(m_updateTime); |
||||
} |
||||
} |
||||
|
||||
switch(uiEvent) { |
||||
case SdlUiEvent::PlayPause: |
||||
core->SetPaused(!core->GetPaused()); |
||||
break; |
||||
|
||||
case SdlUiEvent::ToggleConsole: |
||||
m_console->ToggleConsole(); |
||||
break; |
||||
|
||||
case SdlUiEvent::TurboOff: |
||||
turbo = false; |
||||
break; |
||||
|
||||
case SdlUiEvent::TurboOn: |
||||
turbo = true; |
||||
break; |
||||
|
||||
case SdlUiEvent::Quit: |
||||
m_running = false; |
||||
break; |
||||
|
||||
default: break; // clang-tidy appeasement
|
||||
} |
||||
|
||||
auto time = m_timer->StopwatchStop(); |
||||
auto delay = m_updateTime - time; |
||||
|
||||
if(!turbo) { |
||||
while(delay < 0) |
||||
delay += m_updateTime; |
||||
|
||||
m_timer->Nanosleep(delay); |
||||
} |
||||
} |
||||
|
||||
audio->DequeueAll(); |
||||
} |
||||
|
||||
void GameLoop::SetStep(bool value) { |
||||
m_plip->GetCore()->SetPaused(value); |
||||
} |
||||
} |
@ -0,0 +1,49 @@
|
||||
/* GameLoop.h
|
||||
* |
||||
* The main emulation loop. |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <iomanip> |
||||
#include <sstream> |
||||
|
||||
#include "PlipInstance.h" |
||||
|
||||
#include "Console.h" |
||||
#include "SDL/SdlEvent.h" |
||||
#include "SDL/SdlWindow.h" |
||||
#include "Timer/Timer.h" |
||||
|
||||
namespace PlipSdl { |
||||
class GameLoop { |
||||
public: |
||||
GameLoop(Plip::PlipInstance *plip, Console *console, SdlEvent *event, Timer *timer, int updateRate); |
||||
|
||||
void Play(); |
||||
void SetStep(bool value); |
||||
|
||||
private: |
||||
static inline std::string PrintHex(uintmax_t val, int precision) { |
||||
std::stringstream fmt; |
||||
fmt << "0x" << std::uppercase << std::setfill('0') << std::setw(precision) |
||||
<< std::hex << val; |
||||
return fmt.str(); |
||||
}; |
||||
|
||||
void ConsoleDump(Console *console, const std::vector<std::string> &args); |
||||
void ConsoleMem(Console *console, const std::vector<std::string> &args); |
||||
void ConsoleSetBreak(Console *console, const std::vector<std::string> &args); |
||||
|
||||
Plip::PlipInstance *m_plip; |
||||
Console *m_console; |
||||
SdlEvent *m_event; |
||||
Timer *m_timer; |
||||
SdlWindow *m_wnd; |
||||
|
||||
int m_updateRate; |
||||
int m_updateTime; |
||||
|
||||
bool m_running {}; |
||||
}; |
||||
} |
@ -0,0 +1,54 @@
|
||||
/* StringUtil.h
|
||||
* |
||||
* A bunch of common string manipulation function. |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <string> |
||||
#include <vector> |
||||
|
||||
namespace PlipSdl { |
||||
class StringUtil { |
||||
public: |
||||
static inline std::vector<std::string> Split(std::string str, unsigned char ch) { |
||||
std::vector<std::string> results; |
||||
|
||||
str = Trim(str); |
||||
while(true) { |
||||
auto idx = str.find(ch, 0); |
||||
auto val = Trim(str.substr(0, idx)); |
||||
results.push_back(val); |
||||
|
||||
if(idx == std::string::npos) |
||||
break; |
||||
|
||||
str = Trim(str.substr(idx + 1, std::string::npos)); |
||||
} |
||||
|
||||
return results; |
||||
} |
||||
|
||||
static inline std::string ToLower(std::string str) { |
||||
std::transform(str.cbegin(), str.cend(), str.begin(), [](unsigned char c) { |
||||
return std::tolower(c); |
||||
}); |
||||
|
||||
return str; |
||||
} |
||||
|
||||
static inline std::string Trim(std::string str) { |
||||
// Trim front.
|
||||
str.erase(str.cbegin(), std::find_if(str.cbegin(), str.cend(), [](unsigned char c) { |
||||
return !std::isspace(c); |
||||
})); |
||||
|
||||
// Trim back.
|
||||
str.erase(std::find_if(str.crbegin(), str.crend(), [](unsigned char c) { |
||||
return !std::isspace(c); |
||||
}).base(), str.cend()); |
||||
|
||||
return str; |
||||
} |
||||
}; |
||||
} |
After Width: | Height: | Size: 12 KiB |
Loading…
Reference in new issue