Compare commits

...

2 Commits

  1. 1
      CMakeLists.txt
  2. 101
      libplip/Core/GameBoy/GameBoyInstance.Timer.cpp
  3. 33
      libplip/Core/GameBoy/GameBoyInstance.Video.cpp
  4. 160
      libplip/Core/GameBoy/GameBoyInstance.cpp
  5. 34
      libplip/Core/GameBoy/GameBoyInstance.h

1
CMakeLists.txt

@ -60,6 +60,7 @@ add_library(${lib_name}
# GameBoy
libplip/Core/GameBoy/GameBoyInstance.cpp
libplip/Core/GameBoy/GameBoyInstance.Mbc.cpp
libplip/Core/GameBoy/GameBoyInstance.Timer.cpp
libplip/Core/GameBoy/GameBoyInstance.Video.cpp
########

101
libplip/Core/GameBoy/GameBoyInstance.Timer.cpp

@ -0,0 +1,101 @@
/* GameBoyInstance.timer.cpp
*
* Simulated timer implementation in the GameBoy core.
*/
#include "GameBoyInstance.h"
namespace Plip::Core::GameBoy {
void GameBoyInstance::TimerDividerTick() {
if(m_lastWrite.address == m_addrRegisters + m_regDivider) {
// Falling edge detector quirk.
m_divFallingEdge = BIT_TEST(m_divider, 0);
// Reset timer.
m_divider = m_dividerTick = 0;
m_ioRegisters->SetByte(m_regDivider, m_divider);
} else {
m_dividerTick += 4;
if(m_dividerTick == 0) {
// Increment divider when the divider tick wraps around
// (4194304 / 16384 == 256).
m_divider++;
m_ioRegisters->SetByte(m_regDivider, m_divider);
}
}
}
void GameBoyInstance::TimerExecute() {
// Per-Cycle Initialization
m_divFallingEdge = false;
// Divider
TimerDividerTick();
// TIMA
m_tacLast = m_tac;
m_tac = m_ioRegisters->GetByte(m_regTac);
if(BIT_TEST(m_tac, 2)) {
TimerTimaCycle();
}
}
void GameBoyInstance::TimerTimaCycle() {
m_timerIntBlocked = false;
// Check for a scheduled interrupt.
if(m_timerIntScheduled) {
auto tma = m_ioRegisters->GetByte(m_regTma);
m_timer = tma;
m_ioRegisters->SetByte(m_regTima, m_timer);
m_cpu->Interrupt(INTERRUPT_TIMER);
m_timerIntScheduled = false;
}
// If the falling edge detector triggered on DIV, increment TIMA.
if(m_divFallingEdge) TimerTimaIncrement();
if(m_lastWrite.address == m_addrRegisters + m_regTima)
m_timerIntBlocked = true;
if(m_lastWrite.address == m_addrRegisters + m_regTac) {
// If the falling edge detector triggered on TAC, increment TIMA.
if(((m_tacLast & 0b11) == 0b01) && ((m_tac & 0b11) == 0b00))
TimerTimaIncrement();
}
// Increment internal timer and increment TIMA if necessary.
++m_timerTick;
switch(m_tac & 0b11) {
case 0b00: // 4096hz / 1024 clocks / 256 mcycles
if(m_timerTick == 0)
TimerTimaIncrement();
break;
case 0b01: // 262144hz / 16 clocks / 4 mcycles
if(m_timerTick % 4 == 0)
TimerTimaIncrement();
break;
case 0b10: // 65536hz / 64 clocks / 16 mcycles
if(m_timerTick % 16 == 0)
TimerTimaIncrement();
break;
case 0b11: // 16384hz / 256 clocks / 64 mcycles
if(m_timerTick % 64 == 0)
TimerTimaIncrement();
break;
}
}
inline void GameBoyInstance::TimerTimaIncrement() {
// TIMA was written to before the interrupt could be fired.
if(m_timerIntBlocked) return;
m_timer = m_ioRegisters->GetByte(m_regTima);
if(++m_timer == 0) {
// Timer overflowed. Schedule an interrupt.
m_timerIntScheduled = true;
m_ioRegisters->SetByte(m_addrRegisters + m_regTima, m_timer);
}
}
}

33
libplip/Core/GameBoy/GameBoyInstance.Video.cpp

@ -52,6 +52,39 @@ namespace Plip::Core::GameBoy {
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;
}

160
libplip/Core/GameBoy/GameBoyInstance.cpp

@ -75,12 +75,21 @@ namespace Plip::Core::GameBoy {
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) {
PlipMemoryValue lastWrite {};
m_cycleRemaining += ns;
m_dotCyclesRemaining += m_dotsPerCycle;
ReadInput();
@ -89,133 +98,31 @@ namespace Plip::Core::GameBoy {
// Run a single CPU cycle.
m_memory->ClearLastWrite();
m_cpu->Cycle();
lastWrite = m_memory->GetLastWrite();
m_lastWrite = m_memory->GetLastWrite();
// Emulate MBC functionality.
if(m_mbc != None) MbcCycle(lastWrite);
// Divider
auto divFallingEdge = false;
if(lastWrite.address == m_addrRegisters + m_regDivider) {
// Falling edge detector quirk.
divFallingEdge = BIT_TEST(m_divider, 0);
if(m_mbc != None) MbcCycle(m_lastWrite);
// Reset timer.
m_divider = m_dividerTick = 0;
m_ioRegisters->SetByte(m_regDivider, m_divider);
} else {
m_dividerTick += 4;
if(m_dividerTick == 0) {
// Increment divider when the divider tick wraps around
// (4194304 / 16384 == 256).
m_divider++;
m_ioRegisters->SetByte(m_regDivider, m_divider);
}
}
// Timer
auto oldTac = m_tac;
m_tac = m_ioRegisters->GetByte(m_regTac);
if(BIT_TEST(m_tac, 2)) {
m_timerIntBlocked = false;
// Check for a scheduled interrupt.
if(m_timerIntScheduled) {
auto tma = m_ioRegisters->GetByte(m_regTma);
m_timer = tma;
m_ioRegisters->SetByte(m_regTima, m_timer);
m_cpu->Interrupt(INTERRUPT_TIMER);
m_timerIntScheduled = false;
}
// If the falling edge detector triggered on DIV, increment TIMA.
if(divFallingEdge) IncrementTimer();
if(lastWrite.address == m_addrRegisters + m_regTima)
m_timerIntBlocked = true;
if(lastWrite.address == m_addrRegisters + m_regTac) {
// If the falling edge detector triggered on TAC, increment TIMA.
if(((oldTac & 0b11) == 0b01) && ((m_tac & 0b11) == 0b00))
IncrementTimer();
}
// Increment internal timer and increment TIMA if necessary.
++m_timerTick;
switch(m_tac & 0b11) {
case 0b00: // 4096hz / 1024 clocks / 256 mcycles
if(m_timerTick == 0)
IncrementTimer();
break;
case 0b01: // 262144hz / 16 clocks / 4 mcycles
if(m_timerTick % 4 == 0)
IncrementTimer();
break;
case 0b10: // 65536hz / 64 clocks / 16 mcycles
if(m_timerTick % 16 == 0)
IncrementTimer();
break;
case 0b11: // 16384hz / 256 clocks / 64 mcycles
if(m_timerTick % 64 == 0)
IncrementTimer();
break;
}
}
// Check the input register.
// 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);
// Divider/Timer
TimerExecute();
if(!m_bootRomFlag && m_cpu->GetRegisters().pc >= 0x100) {
m_bootRomFlag = true;
// Input
InputRegisterHandler();
// Swap the boot ROM out for the cartridge ROM.
m_memory->AssignBlock(m_rom, 0x0000, 0x0000, 0x0100);
}
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();
}
// Boot ROM
if(!m_bootRomFlag) {
BootRomFlagHandler();
} else {
m_ioRegisters->SetByte(m_addrBootRomDisable, 1);
}
m_ioRegisters->SetByte(m_regLy, m_videoLy);
m_videoLastLcdc = lcdc;
// PPU
VideoExecute();
// Cycle Timing
m_cycleRemaining -= m_cycleTime;
// Breakpoint
if(m_cpu->GetPc() == m_bp) {
m_cycleRemaining = 0;
m_paused = true;
@ -236,7 +143,7 @@ namespace Plip::Core::GameBoy {
case 0x01: return 4; // 64KB
case 0x02: return 8; // 128KB
case 0x03: return 16; // 256KB
case 0x04: return 32; // 512 KB
case 0x04: return 32; // 512KB
case 0x05: return 64; // 1024KB
case 0x06: return 128; // 2048KB
case 0x07: return 256; // 4096KB
@ -252,6 +159,21 @@ namespace Plip::Core::GameBoy {
}
}
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;

34
libplip/Core/GameBoy/GameBoyInstance.h

@ -62,32 +62,28 @@ namespace Plip::Core::GameBoy {
}
}
inline void IncrementTimer() {
// TIMA was written to before the interrupt could be fired.
if(m_timerIntBlocked) return;
m_timer = m_ioRegisters->GetByte(m_regTima);
if(++m_timer == 0) {
// Timer overflowed. Schedule an interrupt.
m_timerIntScheduled = true;
m_ioRegisters->SetByte(m_addrRegisters + m_regTima, m_timer);
}
}
void BootRomFlagHandler();
uint16_t GetRomBankCount();
void InitCartRam();
void InputRegisterHandler();
void ReadCartFeatures();
void ReadInput();
void RegisterInput();
// GameBoyInstance.mbc.cpp
// GameBoyInstance.Mbc.cpp
void MbcInit();
void MbcCycle(PlipMemoryValue lastWrite);
void Mbc1Cycle(PlipMemoryValue lastWrite);
// GameBoyInstance.video.cpp
// GameBoyInstance.timer.cpp
void TimerExecute();
void TimerDividerTick();
void TimerTimaCycle();
void TimerTimaIncrement();
// GameBoyInstance.Video.cpp
void VideoCycle();
void VideoExecute();
[[nodiscard]] bool VideoHBlank() const;
void VideoModePostTransition();
void VideoModePreTransition();
@ -104,6 +100,7 @@ namespace Plip::Core::GameBoy {
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 {};
@ -164,6 +161,9 @@ namespace Plip::Core::GameBoy {
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;
@ -175,11 +175,13 @@ namespace Plip::Core::GameBoy {
static const uint32_t m_regTma = 0xFF06 - m_addrRegisters;
static const uint32_t m_regTac = 0xFF07 - m_addrRegisters;
bool m_divFallingEdge = false;
uint8_t m_divider = 0;
uint8_t m_dividerTick = 0;
uint8_t m_tac = 0;
uint8_t m_tacLast = 0;
uint8_t m_timer = 0;
uint8_t m_timerTick;
uint8_t m_timerTick = 0;
bool m_timerIntScheduled = false;
bool m_timerIntBlocked = false;

Loading…
Cancel
Save