Compare commits

...

86 Commits
master ... dmg

Author SHA1 Message Date
Ian Burgmyer a3f2c6e20b Sprites kinda work now. 3 years ago
Ian Burgmyer cd86c51725 Added (mostly) working OAM DMA transfer support. 3 years ago
Ian Burgmyer 067151e409 Added (totally untested) sprite support. 3 years ago
Ian Burgmyer 095b3fc761 Partial support for interrupt cancellation. 4 years ago
Ian Burgmyer e579b044aa Reorganized TIMA overflow handling. 4 years ago
Ian Burgmyer 65be130d17 Corrected HALT and timer interrupt behavior. 4 years ago
Ian Burgmyer 4b4d8caf56 Corrected DIV reset behavior. 4 years ago
Ian Burgmyer 84556e4882 Rewrote timer routines. 4 years ago
Ian Burgmyer 72b07c411b Corrected comment, initialized `m_timerTick`. 4 years ago
Ian Burgmyer 43ff5992ef Split the GB loop into separate functions/files. 4 years ago
Ian Burgmyer b076b9f454 Corrected HALT behavior, timer changes. 4 years ago
Ian Burgmyer 441ee50a44 Improved interrupt behavior. 4 years ago
Ian Burgmyer e85809995d Added turbo support to the frontend. 4 years ago
Ian Burgmyer 3e199f886b Corrected opcode: ADC, SBC 4 years ago
Ian Burgmyer b67ebd30b2 Corrected opcode: SRA (HL) 4 years ago
Ian Burgmyer eae0bf54ab Corrected opcodes: ADD SP, LD HL, POP, PUSH. 4 years ago
Ian Burgmyer 85bcadf16d Corrected opcodes: JP (HL), LD A, (rr) 4 years ago
Ian Burgmyer 18308c4e9d Corrected opcodes: RLCA, ADC, SBC, SWAP 4 years ago
Ian Burgmyer 5ce5a06984 Made function names more consistent, bugfixes. 4 years ago
Ian Burgmyer 268be207d3 Modified ZF fix to axe a mostly unnecessary and. 4 years ago
Ian Burgmyer bf9b23ce56 Corrected zero flag behavior. 4 years ago
Ian Burgmyer 15a535f2cb Fixed multiple bugs in LD m16, SP. 4 years ago
Ian Burgmyer b3b8fc5597 Improved LCD emulation. May still need refinement. 4 years ago
Ian Burgmyer 7af9c2ef99 Corrected interrupt behavior, and SET/RST. 4 years ago
Ian Burgmyer 927b37fb02 Breakpoint support, start paused flag. 4 years ago
Ian Burgmyer d8460550d8 Corrected some LCDC behavior, and the RES opcode. 4 years ago
Ian Burgmyer f886e894b9 Corrected LD r, (HL) and HALT behavior. 4 years ago
Ian Burgmyer cfe6bc1829 Corrected HALT opcode. 4 years ago
Ian Burgmyer ef96e4cb0e An icon is now displayed when emulation is paused. 4 years ago
Ian Burgmyer 8c215e5c5e The mem command's addr variable is now capped. 4 years ago
Ian Burgmyer dc891d78b6 Fixed compilation on Linux. 4 years ago
Ian Burgmyer ecc41d5530 Added a basic, console-based memory reader/editor. 4 years ago
Ian Burgmyer f08d33f5b9 Loop refactor, debug funcs, console func change. 4 years ago
Ian Burgmyer 58512ad4e7 Simplified Console::RegisterCommand(). 4 years ago
Ian Burgmyer 8298e5b1c8 Corrected console command sort routine. 4 years ago
Ian Burgmyer f667fa4cdf Implemented console. 4 years ago
Ian Burgmyer ba88707ebb Console now echoes lines typed into it. 4 years ago
Ian Burgmyer 6bcd73a13a Added a missing nodiscard hint. 4 years ago
Ian Burgmyer 6fac4c80ab The console can now display text using a font. 4 years ago
Ian Burgmyer 2d624d2a11 Font adjustment, added nodiscard hints. 4 years ago
Ian Burgmyer 94345a42f3 Added an 8x12 font and a reference to SDL2_image. 4 years ago
Ian Burgmyer d55a166ccb Console: clang-tidy appeasement 4 years ago
Ian Burgmyer 4ac765c29d The console can now be toggled (still a no-op atm) 4 years ago
Ian Burgmyer 274b7d2a46 WIP: Debug console. 4 years ago
Ian Burgmyer 3d7f3e0eff Implemented timer. 4 years ago
Ian Burgmyer 78a2d03e4d Added MBC1 support. 4 years ago
Ian Burgmyer 86e76ec656 Added window rendering support (untested). 4 years ago
Ian Burgmyer 8cb6ce745a Fixed compilation on Linux. 4 years ago
Ian Burgmyer 92f4f4dbe6 Added a missing [[nodiscard]] hint. 4 years ago
Ian Burgmyer f991ead8d7 Migrated to switch/case for LR35902 decoding. 4 years ago
Ian Burgmyer 3edf38191f More FindAddress() optimizations. 4 years ago
Ian Burgmyer 975e2ca37a Now runs at full speed on...an i9. :( 4 years ago
Ian Burgmyer 18e8081e49 Now properly displays the DMG boot ROM. 4 years ago
Ian Burgmyer 6acba32e5c Boot ROM almost displays correctly. 4 years ago
Ian Burgmyer 6d503fd797 Fixed several CPU bugs. DMG boot ROM executes! 4 years ago
Ian Burgmyer fe9f35eb80 Very basic video output. 4 years ago
Ian Burgmyer b8cff685a5 Fixed a dumb timing bug, moved render call. 4 years ago
Ian Burgmyer 39c6156854 Boot ROM support, partial input handler impl. 4 years ago
Ian Burgmyer f74b22ad6e Corrected LDH (n), A opcode definition. 4 years ago
Ian Burgmyer 6aa3d14a07 WIP: GB graphics hardware. 4 years ago
Ian Burgmyer 9c305f278d Core config support, general macros, GB vid change 4 years ago
Ian Burgmyer f0e49a4796 Added more memory address constants. 4 years ago
Ian Burgmyer bac46c1e3a Added GB video stub, a ton of consts, and a loop. 4 years ago
Ian Burgmyer 70dedbce21 GameBoy core now reads inputs. 4 years ago
Ian Burgmyer 144ac00281 Set up GB memory map, WOM support. 4 years ago
Ian Burgmyer f71a9cd0b9 Implemented interrupt...maybe. 4 years ago
Ian Burgmyer 6fee740916 Implemented HALT and STOP. 4 years ago
Ian Burgmyer 6ae14742e2 Added miscellaneous adds and loads (10 opcodes). 4 years ago
Ian Burgmyer d0aeafb0c3 Implemented jump/call/ret (30 opcodes). 4 years ago
Ian Burgmyer ada93c1a3a Implemented DAA. :| 4 years ago
Ian Burgmyer d9de3bf6ba Added 16-bit load/add/push/pop (16 ops). 4 years ago
Ian Burgmyer 85aafe2b5a Implemented DI/EI. 4 years ago
Ian Burgmyer 4c17fbc7f9 Macroized a bunch of things, fixed some major bugs 4 years ago
Ian Burgmyer 3c5c7ff625 Implemented accumulator math functions (15 ops) 4 years ago
Ian Burgmyer 2a81cd5db8 Tagged ops with their associated mnemonics. 4 years ago
Ian Burgmyer 73af26700d Implemented shift, rotates, and swaps (64 opcodes) 4 years ago
Ian Burgmyer 5ac8efed78 Implemented arithmetic/bitwise ops (64 opcodes). 4 years ago
Ian Burgmyer 6464a84a3e Implemented flag set/clear/test (192 CB opcodes). 4 years ago
Ian Burgmyer 5fb69e61b1 Removed pc argument from Reset(). 4 years ago
Ian Burgmyer 23bab288c2 Cleanup and bug fixes. 4 years ago
Ian Burgmyer 0ce11ff65c Implemented INC r and DEC r (16 instructions). 4 years ago
Ian Burgmyer c45032bdd6 Implemented INC rr and DEC rr (8 instructions). 4 years ago
Ian Burgmyer 022ccc24fd Lined up macro values, because why not :) 4 years ago
Ian Burgmyer 7d50c9900a Implemented 17 instrs (Addr<-Mem, Reg<-Mem, nop) 4 years ago
Ian Burgmyer 5b60c952a2 LR35902: Implemented 72 8-bit load opcodes. 4 years ago
Ian Burgmyer 909f053a6c GameBoy scaffolding. :) 4 years ago
  1. 43
      CMakeLists.txt
  2. 6
      ISSUES.txt
  3. 35
      libplip/Core/Chip8/Chip8Instance.cpp
  4. 8
      libplip/Core/Chip8/Chip8Instance.h
  5. 93
      libplip/Core/GameBoy/GameBoyInstance.Mbc.cpp
  6. 100
      libplip/Core/GameBoy/GameBoyInstance.Timer.cpp
  7. 439
      libplip/Core/GameBoy/GameBoyInstance.Video.cpp
  8. 358
      libplip/Core/GameBoy/GameBoyInstance.cpp
  9. 242
      libplip/Core/GameBoy/GameBoyInstance.h
  10. 15
      libplip/Core/PlipCore.cpp
  11. 24
      libplip/Core/PlipCore.h
  12. 4
      libplip/Cpu/Chip8/Chip8.cpp
  13. 6
      libplip/Cpu/Chip8/Chip8.h
  14. 3
      libplip/Cpu/PlipCpu.h
  15. 422
      libplip/Cpu/SharpLr35902/SharpLr35902.Decode.cpp
  16. 99
      libplip/Cpu/SharpLr35902/SharpLr35902.Macros.h
  17. 1348
      libplip/Cpu/SharpLr35902/SharpLr35902.Ops.cpp
  18. 257
      libplip/Cpu/SharpLr35902/SharpLr35902.cpp
  19. 167
      libplip/Cpu/SharpLr35902/SharpLr35902.h
  20. 10
      libplip/Memory/PlipMemory.h
  21. 97
      libplip/Memory/PlipMemoryMap.cpp
  22. 45
      libplip/Memory/PlipMemoryMap.h
  23. 32
      libplip/Memory/PlipMemoryRam.cpp
  24. 14
      libplip/Memory/PlipMemoryRam.h
  25. 36
      libplip/Memory/PlipMemoryRom.cpp
  26. 16
      libplip/Memory/PlipMemoryRom.h
  27. 29
      libplip/PlipConfig.cpp
  28. 22
      libplip/PlipConfig.h
  29. 17
      libplip/PlipInitializationException.h
  30. 10
      libplip/PlipInstance.cpp
  31. 3
      libplip/PlipInstance.h
  32. 21
      libplip/PlipSupport.h
  33. 8
      libplip/Video/PlipVideo.cpp
  34. 36
      libplip/Video/PlipVideo.h
  35. 52
      plip-sdl/Config.cpp
  36. 1
      plip-sdl/Config.h
  37. 328
      plip-sdl/Console.cpp
  38. 107
      plip-sdl/Console.h
  39. 184
      plip-sdl/GameLoop.cpp
  40. 49
      plip-sdl/GameLoop.h
  41. 48
      plip-sdl/SDL/SdlEvent.cpp
  42. 17
      plip-sdl/SDL/SdlEvent.h
  43. 120
      plip-sdl/SDL/SdlWindow.cpp
  44. 37
      plip-sdl/SDL/SdlWindow.h
  45. 54
      plip-sdl/StringUtil.h
  46. 2
      plip-sdl/Timer/Timer.h
  47. 7
      plip-sdl/Timer/TimerPosix.cpp
  48. 2
      plip-sdl/Timer/TimerPosix.h
  49. 2
      plip-sdl/Timer/TimerSdl.cpp
  50. 2
      plip-sdl/Timer/TimerSdl.h
  51. 118
      plip-sdl/main.cpp
  52. BIN
      res/ConsoleFont-8x12.png

43
CMakeLists.txt

@ -6,7 +6,10 @@ set(lib_name "plip")
project(${gui_name})
set(CMAKE_CXX_STANDARD 17)
INCLUDE(FindPkgConfig)
find_package(SDL2 REQUIRED)
PKG_SEARCH_MODULE(SDL2IMAGE REQUIRED SDL2_image>=2.0.0)
if(UNIX)
add_compile_definitions(UNIX)
@ -32,25 +35,45 @@ add_custom_target(
)
add_library(${lib_name}
################################
# Backend / Frontend Interface #
################################
libplip/PlipConfig.cpp
libplip/PlipInstance.cpp
libplip/PlipIo.cpp
libplip/PlipUtility.cpp
libplip/Core/PlipCore.cpp
libplip/Core/Chip8/Chip8Instance.cpp
libplip/Cpu/PlipCpu.cpp
libplip/Cpu/Chip8/Chip8.cpp
libplip/Cpu/Chip8/Chip8.Ops.cpp
libplip/Input/PlipInput.cpp
libplip/Input/PlipInputDefinition.cpp
libplip/Memory/PlipMemoryMap.cpp
libplip/Memory/PlipMemoryRam.cpp
libplip/Memory/PlipMemoryRom.cpp
libplip/Video/PlipVideo.cpp
##########
# Cores #
#########
# CHIP-8
libplip/Core/Chip8/Chip8Instance.cpp
# GameBoy
libplip/Core/GameBoy/GameBoyInstance.cpp
libplip/Core/GameBoy/GameBoyInstance.Mbc.cpp
libplip/Core/GameBoy/GameBoyInstance.Timer.cpp
libplip/Core/GameBoy/GameBoyInstance.Video.cpp
########
# CPUs #
########
# CHIP-8
libplip/Cpu/Chip8/Chip8.cpp
libplip/Cpu/Chip8/Chip8.Ops.cpp
# Sharp LR35902
libplip/Cpu/SharpLr35902/SharpLr35902.cpp
libplip/Cpu/SharpLr35902/SharpLr35902.Decode.cpp
libplip/Cpu/SharpLr35902/SharpLr35902.Ops.cpp
)
if(BIG_ENDIAN)
@ -65,6 +88,8 @@ add_dependencies(${lib_name} GENERATE_LIB_VERSION_HEADER)
add_executable(${gui_name}
plip-sdl/main.cpp
plip-sdl/Config.cpp
plip-sdl/Console.cpp
plip-sdl/GameLoop.cpp
plip-sdl/SDL/SdlAudio.cpp
plip-sdl/SDL/SdlEvent.cpp
@ -84,9 +109,11 @@ endif()
target_include_directories(${gui_name}
PRIVATE ${CMAKE_SOURCE_DIR}/libplip
PUBLIC ${SDL2_INCLUDE_DIRS}
PUBLIC ${SDL2IMAGE_INCLUDE_DIRS}
)
target_link_libraries(${gui_name}
plip
${SDL2_LIBRARIES}
${SDL2IMAGE_LIBRARIES}
)

6
ISSUES.txt

@ -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.

35
libplip/Core/Chip8/Chip8Instance.cpp

@ -11,8 +11,8 @@
#include "../../PlipIo.h"
namespace Plip::Core::Chip8 {
Chip8Instance::Chip8Instance(PlipAudio *audio, PlipInput *input, PlipVideo *video)
: Plip::PlipCore(audio, input, video) {
Chip8Instance::Chip8Instance(PlipAudio *audio, PlipInput *input, PlipVideo *video, PlipConfig *config)
: Plip::PlipCore(audio, input, video, config) {
using pa = Plip::PlipAudio;
m_ram = new PlipMemoryRam(RamSize);
@ -40,7 +40,7 @@ namespace Plip::Core::Chip8 {
m_videoOutput = malloc(ScreenWidth * ScreenHeight * m_videoFormat.pixelWidth);
m_cpu = new Cpu::Chip8(ClockRate, m_memory, CharacterSet, m_input);
m_cpu->Reset(0x200);
m_cpu->Reset();
m_cycleTime = m_cpu->GetCycleTime();
@ -56,11 +56,19 @@ namespace Plip::Core::Chip8 {
free(m_videoOutput);
}
void Chip8Instance::ClearBreakpoint() {
m_bp = 0xFFFFFFFF;
}
void Chip8Instance::Delta(long ns) {
m_cycleRemaining += ns;
do {
m_cpu->Cycle();
if(m_cpu->GetPc() == m_bp) {
m_paused = true;
break;
}
m_cycleRemaining -= m_cycleTime;
if(m_audio->GetQueueSize() < 1024) {
@ -91,7 +99,7 @@ namespace Plip::Core::Chip8 {
for(auto x = 0; x < ScreenWidth; x++) {
auto bit = row >> (63 - x) & 0x1;
m_videoFormat.plot(m_videoOutput, y * ScreenWidth + x,
bit * 255, bit * 255, bit * 255);
bit * 255, bit * 255, bit * 255, 255);
}
}
@ -101,9 +109,11 @@ namespace Plip::Core::Chip8 {
m_video->Render();
}
std::vector<float> Chip8Instance::GenerateSilence() {
using pa = Plip::PlipAudio;
std::string Chip8Instance::DumpRegisters() {
return m_cpu->DumpRegisters();
}
std::vector<float> Chip8Instance::GenerateSilence() {
std::vector<float> res;
for(auto i = 0; i < m_sampleCount; i++) {
for(auto c = 0; c < m_channels; c++)
@ -115,8 +125,6 @@ namespace Plip::Core::Chip8 {
}
std::vector<float> Chip8Instance::GenerateSine() {
using pa = Plip::PlipAudio;
std::vector<float> res;
for(auto i = 0; i < m_sampleCount; i++) {
for(auto c = 0; c < m_channels; c++)
@ -150,6 +158,17 @@ namespace Plip::Core::Chip8 {
return PlipError::Success;
}
void Chip8Instance::Redraw() {
m_video->BeginDraw();
m_video->Draw(m_videoOutput);
m_video->EndDraw();
m_video->Render();
}
void Chip8Instance::SetBreakpoint(uint32_t pc) {
m_bp = pc;
}
void Chip8Instance::WriteCharacterSet(uint32_t address) {
for(auto i = 0; i < m_charsetLength; i++)
m_memory->SetByte(address + i, m_charset[i]);

8
libplip/Core/Chip8/Chip8Instance.h

@ -19,11 +19,15 @@
namespace Plip::Core::Chip8 {
class Chip8Instance : public PlipCore {
public:
Chip8Instance(PlipAudio *audio, PlipInput *input, PlipVideo *video);
Chip8Instance(PlipAudio *audio, PlipInput *input, PlipVideo *video, PlipConfig *config);
~Chip8Instance();
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;
static const uint32_t ClockRate = 500;
static const uint32_t CharacterSet = 0x100;
@ -40,10 +44,10 @@ namespace Plip::Core::Chip8 {
int m_sampleCount;
int m_sampleRate;
uint32_t m_bp = 0xFFFFFFFF;
Cpu::Chip8 *m_cpu;
long m_cycleRemaining = 0;
long m_delayRemaining = DelayTimerTick;
long m_cycleTime = 0;
PlipMemoryRam *m_ram;
std::unordered_map<int, PlipInputDefinition> m_inputList;
Plip::PlipVideoFormatInfo m_videoFormat {};

93
libplip/Core/GameBoy/GameBoyInstance.Mbc.cpp

@ -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;
}
}
}

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

@ -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);
}
}

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

@ -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
}

358
libplip/Core/GameBoy/GameBoyInstance.cpp

@ -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;
}
}

242
libplip/Core/GameBoy/GameBoyInstance.h

@ -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;
};
}

15
libplip/Core/PlipCore.cpp

@ -6,12 +6,21 @@
#include "PlipCore.h"
namespace Plip {
PlipCore::PlipCore(PlipAudio *audio, PlipInput *input, PlipVideo *video) {
PlipCore::PlipCore(PlipAudio *audio, PlipInput *input, PlipVideo *video, PlipConfig *config) {
m_audio = audio;
m_config = config;
m_input = input;
m_video = video;
}
bool PlipCore::GetPaused() const {
return m_paused;
}
long PlipCore::GetStepTime() const {
return m_cycleTime;
}
std::vector<PlipCoreDescription> PlipCore::GetSupportedCores() {
std::vector<PlipCoreDescription> coreList;
@ -20,4 +29,8 @@ namespace Plip {
return coreList;
}
void PlipCore::SetPaused(bool value) {
m_paused = value;
}
}

24
libplip/Core/PlipCore.h

@ -7,8 +7,10 @@
#include <string>
#include <tuple>
#include <unordered_map>
#include <vector>
#include "../PlipConfig.h"
#include "../PlipError.h"
#include "../Audio/PlipAudio.h"
#include "../Input/PlipInput.h"
@ -17,7 +19,8 @@
namespace Plip {
enum class PlipValidCore {
Chip8
Chip8,
GameBoy
};
struct PlipCoreDescription {
@ -30,25 +33,42 @@ namespace Plip {
public:
static std::vector<PlipCoreDescription> GetSupportedCores();
[[nodiscard]] bool GetPaused() const;
[[nodiscard]] long GetStepTime() const;
void SetPaused(bool value);
virtual void ClearBreakpoint() = 0;
virtual void Delta(long ns) = 0;
virtual std::string DumpRegisters() = 0;
virtual PlipMemoryMap* GetMemoryMap() final { return m_memory; }
virtual PlipError Load(const std::string &path) = 0;
virtual void Redraw() = 0;
virtual void SetBreakpoint(uint32_t pc) = 0;
protected:
explicit PlipCore(PlipAudio *audio, PlipInput *input, PlipVideo *video);
PlipCore(PlipAudio *audio, PlipInput *input, PlipVideo *video, PlipConfig *config);
PlipAudio *m_audio;
PlipConfig *m_config;
PlipInput *m_input;
PlipVideo *m_video;
PlipMemoryMap *m_memory = new PlipMemoryMap();
long m_cycleTime {};
bool m_paused = false;
private:
static constexpr PlipCoreDescription m_supportedCores[] = {
{
"chip8",
PlipValidCore::Chip8,
"CHIP-8"
},
{
"gameboy",
PlipValidCore::GameBoy,
"Nintendo(R) GameBoy(TM)"
}
};
};

4
libplip/Cpu/Chip8/Chip8.cpp

@ -97,11 +97,11 @@ namespace Plip::Cpu {
return dump.str();
}
void Chip8::Reset(uint32_t pc) {
void Chip8::Reset() {
m_timerAudio = 0;
m_timerDelay = 0;
m_sp = 0;
m_pc = pc;
m_pc = 0x200;
m_i = 0;
for(auto &reg : m_reg)

6
libplip/Cpu/Chip8/Chip8.h

@ -18,15 +18,15 @@ namespace Plip::Cpu {
void Cycle() override;
void DelayTimer();
std::string DumpRegisters();
[[nodiscard]] uint32_t GetPc() override { return m_pc; }
uint64_t* GetVideo() { return m_videoBuffer; }
[[nodiscard]] bool IsAudioPlaying() const { return m_timerAudio >= 2; }
void Reset(uint32_t pc) override;
void Reset() override;
static const int VideoSize = 32; // 64 x 32
private:
std::string DumpRegisters();
inline uint16_t Fetch() {
uint8_t high = m_memory->GetByte(m_pc++);
uint8_t low = m_memory->GetByte(m_pc++);

3
libplip/Cpu/PlipCpu.h

@ -17,7 +17,8 @@ namespace Plip::Cpu {
void SetHz(long hz);
virtual void Cycle() = 0;
virtual void Reset(uint32_t pc) = 0;
[[nodiscard]] virtual uint32_t GetPc() = 0;
virtual void Reset() = 0;
protected:
PlipCpu(long hz, PlipMemoryMap* memoryMap);

422
libplip/Cpu/SharpLr35902/SharpLr35902.Decode.cpp

@ -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());
}
}
}

99
libplip/Cpu/SharpLr35902/SharpLr35902.Macros.h

@ -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)

1348
libplip/Cpu/SharpLr35902/SharpLr35902.Ops.cpp

File diff suppressed because it is too large Load Diff

257
libplip/Cpu/SharpLr35902/SharpLr35902.cpp

@ -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());
}
}
}

167
libplip/Cpu/SharpLr35902/SharpLr35902.h

@ -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;
};
}

10
libplip/Memory/PlipMemory.h

@ -14,9 +14,15 @@ namespace Plip {
return GetByte(offset);
};
virtual uint8_t GetByte(uint32_t address) = 0;
virtual uint8_t GetByte(uint32_t address, bool privileged = false) = 0;
virtual uint32_t GetLength() = 0;
virtual void SetByte(uint32_t address, uint8_t value) = 0;
[[nodiscard]] virtual bool GetReadable() const = 0;
[[nodiscard]] virtual uint8_t GetUnprivilegedValue() const = 0;
[[nodiscard]] virtual bool GetWritable() const = 0;
virtual void SetByte(uint32_t address, uint8_t value, bool privileged = false) = 0;
virtual void SetReadable(bool value) = 0;
virtual void SetUnprivilegedValue(uint8_t value) = 0;
virtual void SetWritable(bool value) = 0;
protected:
PlipMemory() = default;

97
libplip/Memory/PlipMemoryMap.cpp

@ -9,23 +9,29 @@
namespace Plip {
void PlipMemoryMap::AddBlock(PlipMemory *memory, uint32_t offset) {
AddBlock(memory, offset, memory->GetLength());
AddBlock(memory, offset, memory->GetLength() - offset);
}
void PlipMemoryMap::AddBlock(PlipMemory *memory, uint32_t offset, uint32_t length) {
uint32_t start = 0;
if(m_range.begin() != m_range.end()) {
auto last = m_range.back();
if(m_rangeList.begin() != m_rangeList.end()) {
auto last = m_rangeList.back();
start = last.startAddress + last.length;
}
m_range.push_back({
m_rangeList.push_back({
.startAddress = start,
.memory = memory,
.offset = offset,
.length = length
});
UpdateVector();
}
void PlipMemoryMap::AssignBlock(PlipMemory *memory, uint32_t address, uint32_t offset) {
AssignBlock(memory, address, offset, memory->GetLength() - offset);
}
void PlipMemoryMap::AssignBlock(PlipMemory *memory, uint32_t address, uint32_t offset, uint32_t length) {
@ -34,45 +40,54 @@ namespace Plip {
}
void PlipMemoryMap::AssignBlockDirect(PlipMemory *memory, uint32_t address, uint32_t offset, uint32_t length) {
auto it = m_range.begin();
auto end = m_range.end();
auto it = m_rangeList.begin();
auto end = m_rangeList.end();
while(it != end) {
if(it->startAddress > address) break;
it++;
}
m_range.insert(it, {
.startAddress = address,
.memory = memory,
.offset = offset,
.length = length
m_rangeList.insert(it, {
.startAddress = address,
.memory = memory,
.offset = offset,
.length = length
});
UpdateVector();
}
std::tuple<PlipMemory*, uint32_t> PlipMemoryMap::FindAddress(uint32_t address) {
for(auto const &memory : m_range) {
if(address < memory.startAddress || address > memory.startAddress + memory.length - 1)
continue;
void PlipMemoryMap::ClearLastRead() {
m_lastRead.address = 0;
m_lastRead.value = 0;
}
return { memory.memory, address - memory.startAddress + memory.offset };
}
void PlipMemoryMap::ClearLastWrite() {
m_lastWritten.address = 0;
m_lastWritten.value = 0;
}
return { nullptr, 0 };
uint8_t PlipMemoryMap::GetByte(uint32_t address, bool privileged) {
auto block = FindAddress(address);
if(block.memory == nullptr) return 0;
m_lastRead.address = address;
m_lastRead.value = block.memory->GetByte(block.offset, privileged);
return m_lastRead.value;
}
uint8_t PlipMemoryMap::GetByte(uint32_t address) {
auto [ memory, offset ] = FindAddress(address);
PlipMemoryValue PlipMemoryMap::GetLastRead() {
return m_lastRead;
}
if(memory == nullptr) return 0;
return memory->GetByte(offset);
PlipMemoryValue PlipMemoryMap::GetLastWrite() {
return m_lastWritten;
}
uint32_t PlipMemoryMap::GetLength() {
if(m_range.begin() == m_range.end()) return 0;
auto last = m_range.back();
return last.startAddress + last.length;
return m_range.crbegin()->startAddress + m_range.crbegin()->memory->GetLength();
}
PlipMemoryMap::BlockRangeResult PlipMemoryMap::IsBlockInRange(
@ -87,11 +102,18 @@ namespace Plip {
return PartiallyInRange;
}
void PlipMemoryMap::SetByte(uint32_t address, uint8_t value) {
auto [ memory, offset ] = FindAddress(address);
void PlipMemoryMap::SetByte(uint32_t address, uint8_t value, bool privileged) {
auto block = FindAddress(address);
if(memory == nullptr) return;
memory->SetByte(offset, value);
if(block.memory == nullptr) return;
block.memory->SetByte(block.offset, value, privileged);
m_lastWritten.address = address;
m_lastWritten.value = value;
}
void PlipMemoryMap::SetUnprivilegedValue(uint8_t value) {
for(auto block : m_range)
block.memory->SetUnprivilegedValue(value);
}
void PlipMemoryMap::UnassignBlock(uint32_t address, uint32_t length) {
@ -106,8 +128,8 @@ namespace Plip {
auto endAddress = address + length - 1; // end address
auto it = m_range.begin();
auto end = m_range.end();
auto it = m_rangeList.begin();
auto end = m_rangeList.end();
auto foundBlock = false;
while(it != end) {
@ -126,7 +148,7 @@ namespace Plip {
case CompletelyInRange:
// This block can be removed.
foundBlock = true;
it = m_range.erase(it);
it = m_rangeList.erase(it);
continue;
case PartiallyInRange:
@ -167,5 +189,14 @@ namespace Plip {
continue;
}
}
UpdateVector();
}
void PlipMemoryMap::UpdateVector() {
m_range.clear();
for(auto const &range : m_rangeList)
m_range.push_back(range);
m_rangeCount = m_range.size();
}
}

45
libplip/Memory/PlipMemoryMap.h

@ -7,6 +7,7 @@
#include <cstdint>
#include <list>
#include <vector>
#include "PlipMemory.h"
@ -18,14 +19,29 @@ namespace Plip {
uint32_t length;
};
struct PlipMemoryValue {
uint32_t address;
uint8_t value;
};
class PlipMemoryMap {
public:
uint8_t operator[] (uint32_t offset) {
return GetByte(offset);
};
void AddBlock(PlipMemory *memory, uint32_t offset = 0);
void AddBlock(PlipMemory *memory, uint32_t offset, uint32_t length);
void AssignBlock(PlipMemory *memory, uint32_t address, uint32_t offset = 0);
void AssignBlock(PlipMemory *memory, uint32_t address, uint32_t offset, uint32_t length);
uint8_t GetByte(uint32_t address);
void ClearLastRead();
void ClearLastWrite();
uint8_t GetByte(uint32_t address, bool privileged = false);
PlipMemoryValue GetLastRead();
PlipMemoryValue GetLastWrite();
uint32_t GetLength();
void SetByte(uint32_t address, uint8_t value);
void SetByte(uint32_t address, uint8_t value, bool privileged = false);
void SetUnprivilegedValue(uint8_t value);
void UnassignBlock(uint32_t address, uint32_t length);
private:
@ -35,10 +51,31 @@ namespace Plip {
CompletelyInRange
};
struct FindResults {
PlipMemory* memory;
uint32_t offset;
};
inline FindResults FindAddress(uint32_t address) {
for(auto i = 0; i < m_rangeCount; i++) {
auto range = m_range[i];
if(address < range.startAddress || address > range.startAddress + range.length - 1)
continue;
return { range.memory, address - range.startAddress + range.offset };
}
return { nullptr, 0 };
};
void AssignBlockDirect(PlipMemory *memory, uint32_t address, uint32_t offset, uint32_t length);
std::tuple<PlipMemory*, uint32_t> FindAddress(uint32_t address);
static inline BlockRangeResult IsBlockInRange(const PlipMemoryMapRange &block, uint32_t startAddress, uint32_t endAddress);
void UpdateVector();
std::list<PlipMemoryMapRange> m_range;
PlipMemoryValue m_lastRead {};
PlipMemoryValue m_lastWritten {};
std::list<PlipMemoryMapRange> m_rangeList;
std::vector<PlipMemoryMapRange> m_range;
int m_rangeCount;
};
}

32
libplip/Memory/PlipMemoryRam.cpp

@ -15,15 +15,39 @@ namespace Plip {
delete m_data;
}
uint8_t PlipMemoryRam::GetByte(uint32_t address) {
return m_data[address];
uint8_t PlipMemoryRam::GetByte(uint32_t address, bool privileged) {
return m_readable || privileged ? m_data[address] : m_unprivileged;
}
uint32_t PlipMemoryRam::GetLength() {
return m_length;
}
void PlipMemoryRam::SetByte(uint32_t address, uint8_t value) {
m_data[address] = value;
bool PlipMemoryRam::GetReadable() const {
return m_readable;
}
uint8_t PlipMemoryRam::GetUnprivilegedValue() const {
return m_unprivileged;
}
bool PlipMemoryRam::GetWritable() const {
return m_writable;
}
void PlipMemoryRam::SetByte(uint32_t address, uint8_t value, bool privileged) {
if(m_writable || privileged) m_data[address] = value;
}
void PlipMemoryRam::SetReadable(bool value) {
m_readable = value;
}
void PlipMemoryRam::SetUnprivilegedValue(uint8_t value) {
m_unprivileged = value;
}
void PlipMemoryRam::SetWritable(bool value) {
m_writable = value;
}
}

14
libplip/Memory/PlipMemoryRam.h

@ -13,12 +13,22 @@ namespace Plip {
explicit PlipMemoryRam(uint32_t amount);
~PlipMemoryRam();
uint8_t GetByte(uint32_t address) override;
uint8_t GetByte(uint32_t address, bool privileged = false) override;
uint32_t GetLength() override;
void SetByte(uint32_t address, uint8_t value) override;
[[nodiscard]] bool GetReadable() const override;
[[nodiscard]] uint8_t GetUnprivilegedValue() const override;
[[nodiscard]] bool GetWritable() const override;
void SetByte(uint32_t address, uint8_t value, bool privileged = false) override;
void SetReadable(bool value) override;
void SetUnprivilegedValue(uint8_t value) override;
void SetWritable(bool value) override;
private:
uint32_t m_length;
uint8_t *m_data;
bool m_readable = true;
uint8_t m_unprivileged = 0x00;
bool m_writable = true;
};
}

36
libplip/Memory/PlipMemoryRom.cpp

@ -13,17 +13,49 @@ namespace Plip {
m_data = new uint8_t[m_length];
std::memcpy(m_data, data, m_length);
m_writtenData = new uint8_t[m_length];
}
PlipMemoryRom::~PlipMemoryRom() {
delete m_data;
delete m_writtenData;
}
uint8_t PlipMemoryRom::GetByte(uint32_t address) {
return m_data[address];
uint8_t PlipMemoryRom::GetByte(uint32_t address, bool privileged) {
return m_readable || privileged ? m_data[address] : m_unprivileged;
}
bool PlipMemoryRom::GetReadable() const {
return m_readable;
}
uint8_t PlipMemoryRom::GetUnprivilegedValue() const {
return m_unprivileged;
}
bool PlipMemoryRom::GetWritable() const {
return false;
}
uint8_t PlipMemoryRom::GetWrittenByte(uint32_t address) {
return m_writtenData[address];
}
uint32_t PlipMemoryRom::GetLength() {
return m_length;
}
void PlipMemoryRom::SetByte(uint32_t address, uint8_t value, bool privileged) {
m_writtenData[address] = value;
}
void PlipMemoryRom::SetReadable(bool value) {
m_readable = value;
}
void PlipMemoryRom::SetUnprivilegedValue(uint8_t value) {
m_unprivileged = value;
}
void PlipMemoryRom::SetWritable(bool value) {}
}

16
libplip/Memory/PlipMemoryRom.h

@ -13,12 +13,24 @@ namespace Plip {
explicit PlipMemoryRom(void *data, uint32_t length);
~PlipMemoryRom();
uint8_t GetByte(uint32_t address) override;
uint8_t GetByte(uint32_t address, bool privileged = false) override;
uint32_t GetLength() override;
void SetByte(uint32_t address, uint8_t value) override {};
[[nodiscard]] bool GetReadable() const override;
[[nodiscard]] uint8_t GetUnprivilegedValue() const override;
[[nodiscard]] bool GetWritable() const override;
void SetByte(uint32_t address, uint8_t value, bool privileged = false) override;
void SetReadable(bool value) override;
void SetUnprivilegedValue(uint8_t value) override;
void SetWritable(bool value) override;
uint8_t GetWrittenByte(uint32_t address);
private:
uint32_t m_length;
uint8_t *m_data;
uint8_t *m_writtenData;
bool m_readable = true;
uint8_t m_unprivileged = 0x00;
};
}

29
libplip/PlipConfig.cpp

@ -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));
}
}

22
libplip/PlipConfig.h

@ -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 {};
};
}

17
libplip/PlipInitializationException.h

@ -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) {}
};
}

10
libplip/PlipInstance.cpp

@ -7,6 +7,7 @@
#include "PlipVersion.h"
#include "Core/Chip8/Chip8Instance.h"
#include "Core/GameBoy/GameBoyInstance.h"
namespace Plip {
PlipInstance::PlipInstance(PlipVideo *video, PlipAudio *audio) {
@ -18,6 +19,10 @@ namespace Plip {
return m_audio;
}
PlipConfig* PlipInstance::GetConfig() {
return m_config;
}
PlipCore* PlipInstance::GetCore() {
return m_core;
}
@ -49,7 +54,10 @@ namespace Plip {
PlipError PlipInstance::Load(PlipValidCore core, const std::string &path) {
switch(core) {
case PlipValidCore::Chip8:
m_core = new Core::Chip8::Chip8Instance(m_audio, m_input, m_video);
m_core = new Core::Chip8::Chip8Instance(m_audio, m_input, m_video, m_config);
break;
case PlipValidCore::GameBoy:
m_core = new Core::GameBoy::GameBoyInstance(m_audio, m_input, m_video, m_config);
break;
default:
return PlipError::InvalidCore;

3
libplip/PlipInstance.h

@ -7,6 +7,7 @@
#include <string>
#include "PlipConfig.h"
#include "PlipError.h"
#include "Core/PlipCore.h"
#include "Audio/PlipAudio.h"
@ -23,6 +24,7 @@ namespace Plip {
PlipAudio* GetAudio();
PlipCore* GetCore();
PlipConfig* GetConfig();
PlipInput* GetInput();
PlipVideo* GetVideo();
PlipError Load(PlipValidCore core, const std::string &path);
@ -31,6 +33,7 @@ namespace Plip {
private:
PlipAudio *m_audio;
PlipCore *m_core = nullptr;
PlipConfig *m_config = new PlipConfig();
PlipInput *m_input = new PlipInput();
PlipVideo *m_video;
};

21
libplip/PlipSupport.h

@ -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))

8
libplip/Video/PlipVideo.cpp

@ -16,10 +16,10 @@ namespace Plip {
return { .pixelWidth = 3, .plot = PlipVideo::PlotBgr888 };
case PlipVideoFormat::XRGB8888:
return { .pixelWidth = 4, .plot = PlipVideo::PlotArgb8888 };
return { .pixelWidth = 4, .plot = PlipVideo::PlotXrgb8888 };
case PlipVideoFormat::XBGR8888:
return { .pixelWidth = 4, .plot = PlipVideo::PlotAbgr8888 };
return { .pixelWidth = 4, .plot = PlipVideo::PlotXbgr8888 };
case PlipVideoFormat::ARGB8888:
return { .pixelWidth = 4, .plot = PlipVideo::PlotArgb8888 };
@ -28,10 +28,10 @@ namespace Plip {
return { .pixelWidth = 4, .plot = PlipVideo::PlotAbgr8888 };
case PlipVideoFormat::RGBX8888:
return { .pixelWidth = 4, .plot = PlipVideo::PlotRgba8888 };
return { .pixelWidth = 4, .plot = PlipVideo::PlotRgbx8888 };
case PlipVideoFormat::BGRX8888:
return { .pixelWidth = 4, .plot = PlipVideo::PlotBgra8888 };
return { .pixelWidth = 4, .plot = PlipVideo::PlotBgrx8888 };
case PlipVideoFormat::RGBA8888:
return { .pixelWidth = 4, .plot = PlipVideo::PlotRgba8888 };

36
libplip/Video/PlipVideo.h

@ -24,7 +24,7 @@ namespace Plip {
struct PlipVideoFormatInfo {
uint8_t pixelWidth;
void (*plot)(void* data, int offset, uint8_t r, uint8_t g, uint8_t b);
void (*plot)(void* data, int offset, uint8_t r, uint8_t g, uint8_t b, uint8_t a);
};
class PlipVideo {
@ -43,34 +43,50 @@ namespace Plip {
static PlipVideoFormatInfo GetFormatInfo(PlipVideoFormat format);
// Pixel format functions.
static void PlotRgb888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b) {
static void PlotRgb888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
((uint8_t*)data)[offset * 3] = r;
((uint8_t*)data)[offset * 3 + 1] = g;
((uint8_t*)data)[offset * 3 + 2] = b;
}
static void PlotBgr888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b) {
static void PlotBgr888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
((uint8_t*)data)[offset * 3] = b;
((uint8_t*)data)[offset * 3 + 1] = g;
((uint8_t*)data)[offset * 3 + 2] = r;
}
static void PlotArgb8888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b) {
((uint32_t*)data)[offset] = (0xFF << 24) + (r << 16) + (g << 8) + b;
static void PlotAbgr8888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
((uint32_t*)data)[offset] = (a << 24) + (b << 16) + (g << 8) + r;
}
static void PlotAbgr8888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b) {
((uint32_t*)data)[offset] = (0xFF << 24) + (b << 16) + (g << 8) + r;
static void PlotArgb8888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
((uint32_t*)data)[offset] = (a << 24) + (r << 16) + (g << 8) + b;
}
static void PlotRgba8888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b) {
((uint32_t*)data)[offset] = (r << 24) + (g << 16) + (b << 8) + 0xFF;
static void PlotBgra8888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
((uint32_t*)data)[offset] = (b << 24) + (g << 16) + (r << 8) + a;
}
static void PlotBgra8888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b) {
static void PlotBgrx8888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
((uint32_t*)data)[offset] = (b << 24) + (g << 16) + (r << 8) + 0xFF;
}
static void PlotRgba8888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
((uint32_t*)data)[offset] = (r << 24) + (g << 16) + (b << 8) + a;
}
static void PlotRgbx8888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
((uint32_t*)data)[offset] = (r << 24) + (g << 16) + (b << 8) + 0xFF;
}
static void PlotXbgr8888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
((uint32_t*)data)[offset] = (0xFF << 24) + (b << 16) + (g << 8) + r;
}
static void PlotXrgb8888(void *data, int offset, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
((uint32_t*)data)[offset] = (0xFF << 24) + (r << 16) + (g << 8) + b;
}
protected:
PlipVideo() = default;
};

52
plip-sdl/Config.cpp

@ -8,28 +8,16 @@
#include <iostream>
#include "Config.h"
#include "StringUtil.h"
namespace PlipSdl {
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;
}
const std::unordered_map<std::string, std::string> *Config::GetSection(const std::string &key) {
auto keyLower = StringUtil::ToLower(key);
static inline std::string ToLower(std::string str) {
std::transform(str.cbegin(), str.cend(), str.begin(), [](unsigned char c) {
return std::tolower(c);
});
auto itSection = m_section.find(keyLower);
if(itSection == m_section.end()) return nullptr;
return str;
return &(itSection->second);
}
const std::string &Config::GetValue(const std::string &key) {
@ -37,14 +25,14 @@ namespace PlipSdl {
}
const std::string &Config::GetValue(const std::string &section, const std::string &key) {
auto secLower = ToLower(section);
auto keyLower = ToLower(key);
auto secLower = StringUtil::ToLower(section);
auto keyLower = StringUtil::ToLower(key);
auto itSection = m_section.find(secLower);
if(itSection == m_section.end()) return empty;
if(itSection == m_section.cend()) return empty;
auto itKey = itSection->second.find(keyLower);
if(itKey == itSection->second.end()) return empty;
if(itKey == itSection->second.cend()) return empty;
return itKey->second;
}
@ -60,7 +48,7 @@ namespace PlipSdl {
std::string line;
int lineNum = 0;
while(std::getline(file, line)) {
line = Trim(line);
line = StringUtil::Trim(line);
++lineNum;
if(line.empty()) continue;
@ -70,9 +58,9 @@ namespace PlipSdl {
if(line.front() == '[' && line.back() == ']') {
// Section
line.erase(line.begin());
line.erase(line.end() - 1);
section = Trim(line);
line.erase(line.cbegin());
line.erase(line.cend() - 1);
section = StringUtil::Trim(line);
continue;
}
@ -83,8 +71,8 @@ namespace PlipSdl {
continue;
}
auto key = Trim(line.substr(0, equals));
auto value = Trim(line.substr(equals + 1, std::string::npos));
auto key = StringUtil::Trim(line.substr(0, equals));
auto value = StringUtil::Trim(line.substr(equals + 1, std::string::npos));
SetValue(section, key, value);
}
@ -97,11 +85,11 @@ namespace PlipSdl {
}
void Config::SetValue(const std::string &section, const std::string &key, const std::string &value) {
auto secLower = ToLower(section);
auto keyLower = ToLower(key);
auto secLower = StringUtil::ToLower(section);
auto keyLower = StringUtil::ToLower(key);
auto itSection = m_section.find(secLower);
if(itSection == m_section.end()) {
if(itSection == m_section.cend()) {
// O.o
auto newSection = std::pair<std::string, std::unordered_map<std::string, std::string>>
(secLower, std::unordered_map<std::string, std::string>());
@ -111,7 +99,7 @@ namespace PlipSdl {
}
auto itKey = itSection->second.find(keyLower);
if(itKey == itSection->second.end()) {
if(itKey == itSection->second.cend()) {
itSection->second.insert(std::pair<std::string, std::string>(keyLower, value));
return;
}

1
plip-sdl/Config.h

@ -12,6 +12,7 @@
namespace PlipSdl {
class Config {
public:
const std::unordered_map<std::string, std::string> *GetSection(const std::string &key);
const std::string &GetValue(const std::string &key);
const std::string &GetValue(const std::string &section, const std::string &key);
bool LoadFile(const std::string &filename);

328
plip-sdl/Console.cpp

@ -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);
}
}

107
plip-sdl/Console.h

@ -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 {};
};
}

184
plip-sdl/GameLoop.cpp

@ -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);
}
}

49
plip-sdl/GameLoop.h

@ -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 {};
};
}

48
plip-sdl/SDL/SdlEvent.cpp

@ -24,11 +24,25 @@ namespace PlipSdl {
while(SDL_PollEvent(&ev)) {
switch(ev.type) {
case SDL_KEYDOWN:
UpdateDigitalInput(ev.key.keysym.scancode, true);
if(ev.key.keysym.scancode == m_consoleKey)
uiEvent = SdlUiEvent::ToggleConsole;
else if(ev.key.keysym.scancode == m_frameAdvanceKey)
uiEvent = SdlUiEvent::FrameAdvance;
else if(ev.key.keysym.scancode == m_pauseKey)
uiEvent = SdlUiEvent::PlayPause;
else if(ev.key.keysym.scancode == m_stepKey)
uiEvent = SdlUiEvent::Step;
else if(ev.key.keysym.scancode == m_turboKey)
uiEvent = SdlUiEvent::TurboOn;
else
UpdateDigitalInput(ev.key.keysym.scancode, true);
break;
case SDL_KEYUP:
UpdateDigitalInput(ev.key.keysym.scancode, false);
if(ev.key.keysym.scancode == m_turboKey)
uiEvent = SdlUiEvent::TurboOff;
else
UpdateDigitalInput(ev.key.keysym.scancode, false);
break;
case SDL_QUIT:
@ -40,6 +54,36 @@ namespace PlipSdl {
return uiEvent;
}
void SdlEvent::SetKey(const std::string &action, SDL_Scancode scancode) {
// TODO: Oof, this is nasty. Kindly revamp this. :)
switch(Hash(action.c_str())) {
case Hash("console"):
m_consoleKey = scancode;
break;
case Hash("frameadvance"):
m_frameAdvanceKey = scancode;
break;
case Hash("pause"):
m_pauseKey = scancode;
break;
case Hash("step"):
m_stepKey = scancode;
break;
case Hash("turbo"):
m_turboKey = scancode;
break;
}
}
void SdlEvent::SetKey(const std::string &action, const std::string &binding) {
auto scancode = SDL_GetScancodeFromName(binding.c_str());
SetKey(action, scancode);
}
void SdlEvent::UpdateDigitalInput(SDL_Scancode scancode, bool value) {
auto it = m_digitalBinding.find(scancode);
if(it == m_digitalBinding.cend()) return;

17
plip-sdl/SDL/SdlEvent.h

@ -12,6 +12,12 @@
namespace PlipSdl {
enum class SdlUiEvent {
None,
FrameAdvance,
PlayPause,
Step,
ToggleConsole,
TurboOff,
TurboOn,
Quit
};
@ -21,11 +27,22 @@ namespace PlipSdl {
void AddDigitalBinding(int id, SDL_Scancode scancode);
void AddDigitalBinding(int id, const std::string &binding);
void SetKey(const std::string &action, SDL_Scancode scancode);
void SetKey(const std::string &action, const std::string &binding);
SdlUiEvent ProcessEvents();
private:
static constexpr unsigned int Hash(const char *str, int h = 0) {
return !str[h] ? 5381 : (Hash(str, h + 1) * 33) ^ str[h];
}
void UpdateDigitalInput(SDL_Scancode scancode, bool value);
SDL_Scancode m_consoleKey {};
SDL_Scancode m_frameAdvanceKey {};
SDL_Scancode m_pauseKey {};
SDL_Scancode m_stepKey {};
SDL_Scancode m_turboKey {};
std::unordered_map<SDL_Scancode, int> m_digitalBinding;
Plip::PlipInput *m_input;
};

120
plip-sdl/SDL/SdlWindow.cpp

@ -10,17 +10,17 @@
#include "SdlWindow.h"
namespace PlipSdl {
SdlWindow::SdlWindow(int scale, const std::string &title) {
SdlWindow::SdlWindow(int gameScale, const std::string &title) {
std::stringstream error;
m_scale = scale;
m_gameScale = gameScale;
SDL_InitSubSystem(SDL_INIT_VIDEO);
// Try to create a window.
m_window = SDL_CreateWindow(title.c_str(),
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
m_width * m_scale, m_height * m_scale, 0);
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
m_width * m_gameScale, m_height * m_gameScale, 0);
if(m_window == nullptr) {
error << "Unable to create SDL window: " << SDL_GetError();
@ -57,46 +57,83 @@ namespace PlipSdl {
// Try to create a small starting texture. This will be removed and
// recreated later, but we want to ensure that we can make one in the
// first place.
CreateTexture();
CreateConsoleTexture();
CreateGameTexture();
}
SdlWindow::~SdlWindow() {
if(m_texture != nullptr) SDL_DestroyTexture(m_texture);
if(m_gameTex != nullptr) SDL_DestroyTexture(m_gameTex);
if(m_renderer != nullptr) SDL_DestroyRenderer(m_renderer);
if(m_window != nullptr) SDL_DestroyWindow(m_window);
}
bool SdlWindow::BeginDraw() {
return SDL_LockTexture(m_texture, nullptr, &m_texData, &m_pitch) == 0;
return SDL_LockTexture(m_gameTex, nullptr, &m_texData, &m_pitch) == 0;
}
bool SdlWindow::BeginDrawConsole() {
if(!SDL_RenderTargetSupported(m_renderer)) return false;
SDL_SetRenderTarget(m_renderer, m_conTex);
return true;
}
void SdlWindow::Clear() {
SDL_RenderClear(m_renderer);
}
void SdlWindow::CreateTexture() {
if(m_texture != nullptr) SDL_DestroyTexture(m_texture);
void SdlWindow::CreateTexture(SDL_Texture **texture, int width, int height,
SDL_BlendMode blendMode, SDL_TextureAccess access) {
if(*texture != nullptr) SDL_DestroyTexture(*texture);
uint32_t pixelFormat = SelectSdlFormat(m_format);
m_texture = nullptr;
m_texture = SDL_CreateTexture(m_renderer,
pixelFormat, SDL_TEXTUREACCESS_STREAMING,
m_width, m_height);
*texture = SDL_CreateTexture(m_renderer,
pixelFormat, access,
width, height);
SDL_SetTextureBlendMode(*texture, blendMode);
if(m_texture == nullptr) {
if(*texture == nullptr) {
std::stringstream error;
error << "Unable to create SDL texture: " << SDL_GetError();
throw Plip::PlipVideoException(error.str().c_str());
}
}
void SdlWindow::CreateConsoleTexture() {
CreateTexture(&m_conTex, m_width * m_gameScale, m_height * m_gameScale,
SDL_BLENDMODE_BLEND, SDL_TEXTUREACCESS_TARGET);
}
void SdlWindow::CreateGameTexture() {
CreateTexture(&m_gameTex, m_width, m_height);
}
void SdlWindow::Draw(void *data) {
memcpy(m_texData, data, m_pitch * m_height);
}
bool SdlWindow::EndDraw() {
SDL_UnlockTexture(m_texture);
// Overlay the pause icon if it's enabled.
if(m_drawPauseIcon) {
auto sX = m_width - m_pauseOffsetX - m_pauseSizeX;
auto xRun = m_pauseSizeX / 3;
auto maxY = m_height - m_pauseOffsetY;
auto sY = maxY - m_pauseSizeY;
for(auto y = sY; y < maxY; y++) {
auto offset = (y * m_width) + sX;
for(auto x = 0; x < m_pauseSizeX; x++) {
if(x / xRun == 1) continue;
m_fmtInfo.plot(m_texData, offset + x, 255, 0, 0, 255);
}
}
}
SDL_UnlockTexture(m_gameTex);
return true;
}
bool SdlWindow::EndDrawConsole() {
SDL_SetRenderTarget(m_renderer, nullptr);
return true;
}
@ -104,16 +141,25 @@ namespace PlipSdl {
return m_format;
}
int SdlWindow::GetGameScale() const {
return m_gameScale;
}
int SdlWindow::GetHeight() {
return m_height;
}
SDL_Renderer *SdlWindow::GetRenderer() const {
return m_renderer;
}
int SdlWindow::GetWidth() {
return m_width;
}
void SdlWindow::Render() {
SDL_RenderCopy(m_renderer, m_texture, nullptr, nullptr);
SDL_RenderCopy(m_renderer, m_gameTex, nullptr, nullptr);
if(m_drawConsole) SDL_RenderCopy(m_renderer, m_conTex, nullptr, nullptr);
SDL_RenderPresent(m_renderer);
}
@ -122,57 +168,61 @@ namespace PlipSdl {
m_height = height;
// Resize the window.
SDL_SetWindowSize(m_window, m_width * m_scale, m_height * m_scale);
SDL_SetWindowSize(m_window, m_width * m_gameScale, m_height * m_gameScale);
// Destroy and recreate the texture.
CreateTexture();
CreateConsoleTexture();
CreateGameTexture();
}
bool SdlWindow::SelectFormat(uint32_t format) {
switch(format) {
case SDL_PIXELFORMAT_RGB24:
m_format = Plip::PlipVideoFormat::RGB888;
return true;
break;
case SDL_PIXELFORMAT_BGR24:
m_format = Plip::PlipVideoFormat::BGR888;
return true;
break;
case SDL_PIXELFORMAT_RGB888:
m_format = Plip::PlipVideoFormat::XRGB8888;
return true;
break;
case SDL_PIXELFORMAT_BGR888:
m_format = Plip::PlipVideoFormat::XBGR8888;
return true;
break;
case SDL_PIXELFORMAT_ARGB8888:
m_format = Plip::PlipVideoFormat::ARGB8888;
return true;
break;
case SDL_PIXELFORMAT_ABGR8888:
m_format = Plip::PlipVideoFormat::ABGR8888;
return true;
break;
case SDL_PIXELFORMAT_RGBX8888:
m_format = Plip::PlipVideoFormat::RGBX8888;
return true;
break;
case SDL_PIXELFORMAT_BGRX8888:
m_format = Plip::PlipVideoFormat::BGRX8888;
return true;
break;
case SDL_PIXELFORMAT_RGBA8888:
m_format = Plip::PlipVideoFormat::RGBA8888;
return true;
break;
case SDL_PIXELFORMAT_BGRA8888:
m_format = Plip::PlipVideoFormat::BGRA8888;
return true;
break;
default:
return false;
}
m_fmtInfo = Plip::PlipVideo::GetFormatInfo(m_format);
return true;
}
uint32_t SdlWindow::SelectSdlFormat(Plip::PlipVideoFormat format) {
@ -204,9 +254,17 @@ namespace PlipSdl {
}
}
void SdlWindow::SetScale(int scale) {
m_scale = scale;
SDL_SetWindowSize(m_window, m_width * m_scale, m_height * m_scale);
void SdlWindow::SetConsoleEnabled(bool enabled) {
m_drawConsole = enabled;
}
void SdlWindow::SetDrawPauseIcon(bool value) {
m_drawPauseIcon = value;
}
void SdlWindow::SetGameScale(int scale) {
m_gameScale = scale;
SDL_SetWindowSize(m_window, m_width * m_gameScale, m_height * m_gameScale);
}
void SdlWindow::SetTitle(std::string title) {

37
plip-sdl/SDL/SdlWindow.h

@ -12,41 +12,62 @@
namespace PlipSdl {
class SdlWindow : public Plip::PlipVideo {
public:
explicit SdlWindow(int scale = 1, const std::string &title = "");
explicit SdlWindow(int gameScale = 1, const std::string &title = "");
~SdlWindow();
bool BeginDraw() override;
void Clear() override;
void Draw(void *data) override;
bool EndDraw() override;
Plip::PlipVideoFormat GetFormat() override;
int GetHeight() override;
int GetWidth() override;
[[nodiscard]] Plip::PlipVideoFormat GetFormat() override;
[[nodiscard]] int GetHeight() override;
[[nodiscard]] int GetWidth() override;
void Render() override;
void Resize(int width, int height) override;
void SetTitle(std::string title) override;
void SetScale(int scale);
bool BeginDrawConsole();
bool EndDrawConsole();
[[nodiscard]] int GetGameScale() const;
[[nodiscard]] SDL_Renderer *GetRenderer() const;
void SetConsoleEnabled(bool enabled);
void SetDrawPauseIcon(bool value);
void SetGameScale(int scale);
private:
void CreateTexture();
void CreateTexture(SDL_Texture **texture, int width, int height,
SDL_BlendMode blendMode = SDL_BLENDMODE_NONE,
SDL_TextureAccess access = SDL_TEXTUREACCESS_STREAMING);
void CreateConsoleTexture();
void CreateGameTexture();
bool SelectFormat(uint32_t format);
static uint32_t SelectSdlFormat(Plip::PlipVideoFormat format);
const int m_initWidth = 64;
const int m_initHeight = 64;
const int m_pauseOffsetX = 8;
const int m_pauseOffsetY = 8;
const int m_pauseSizeX = 24; // should be a multiple of 3
const int m_pauseSizeY = 24;
int m_width = m_initWidth;
int m_height = m_initHeight;
int m_scale;
int m_gameScale;
bool m_drawPauseIcon = false;
void *m_texData = nullptr;
int m_pitch = -1;
bool m_drawConsole = false;
SDL_Window *m_window = nullptr;
SDL_Renderer *m_renderer = nullptr;
SDL_Texture *m_texture = nullptr;
SDL_Texture *m_conTex = nullptr;
SDL_Texture *m_gameTex = nullptr;
Plip::PlipVideoFormat m_format = Plip::PlipVideoFormat::Unknown;
Plip::PlipVideoFormatInfo m_fmtInfo {};
};
}

54
plip-sdl/StringUtil.h

@ -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;
}
};
}

2
plip-sdl/Timer/Timer.h

@ -10,7 +10,7 @@ namespace PlipSdl {
public:
virtual void Nanosleep(long ns) = 0;
virtual void StopwatchStart() = 0;
virtual long StopwatchStop() = 0;
virtual unsigned long StopwatchStop() = 0;
protected:
Timer() = default;

7
plip-sdl/Timer/TimerPosix.cpp

@ -19,12 +19,13 @@ namespace PlipSdl {
clock_gettime(CLOCK_MONOTONIC, &m_stopwatchVal);
}
long TimerPosix::StopwatchStop() {
unsigned long TimerPosix::StopwatchStop() {
struct timespec end_val;
clock_gettime(CLOCK_MONOTONIC, &end_val);
return ((end_val.tv_sec - m_stopwatchVal.tv_sec) * 1000000)
+ (end_val.tv_nsec - m_stopwatchVal.tv_nsec);
unsigned long start = m_stopwatchVal.tv_sec * 1000000000 + m_stopwatchVal.tv_nsec;
unsigned long end = end_val.tv_sec * 1000000000 + end_val.tv_nsec;
return end - start;
}
#pragma clang diagnostic pop
}

2
plip-sdl/Timer/TimerPosix.h

@ -16,7 +16,7 @@ namespace PlipSdl {
void Nanosleep(long ns) override;
void StopwatchStart() override;
long StopwatchStop() override;
unsigned long StopwatchStop() override;
private:
struct timespec m_stopwatchVal {};

2
plip-sdl/Timer/TimerSdl.cpp

@ -33,7 +33,7 @@ namespace PlipSdl {
m_stopwatchVal = SDL_GetTicks();
}
long TimerSdl::StopwatchStop() {
unsigned long TimerSdl::StopwatchStop() {
Uint32 endVal = SDL_GetTicks();
if(endVal >= m_stopwatchVal)
return MS_TO_NS(endVal - m_stopwatchVal);

2
plip-sdl/Timer/TimerSdl.h

@ -18,7 +18,7 @@ namespace PlipSdl {
void Nanosleep(long ns) override;
void StopwatchStart() override;
long StopwatchStop() override;
unsigned long StopwatchStop() override;
private:
float m_sleepSkew = 0;

118
plip-sdl/main.cpp

@ -5,12 +5,15 @@
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
#include "cxxopts.hpp"
#include "PlipInstance.h"
#include "Config.h"
#include "Console.h"
#include "GameLoop.h"
#include "SDL/SdlAudio.h"
#include "SDL/SdlEvent.h"
#include "SDL/SdlWindow.h"
@ -26,41 +29,19 @@ std::vector<std::vector<std::string>> defaultConfig = {
{ "video", "targetFps", "60" }
};
std::unordered_map<std::string, SDL_Scancode> defaultFrontendKeys = {
{ "console", SDL_SCANCODE_GRAVE },
{ "frameadvance", SDL_SCANCODE_RIGHTBRACKET },
{ "pause", SDL_SCANCODE_BACKSLASH },
{ "step", SDL_SCANCODE_LEFTBRACKET },
{ "turbo", SDL_SCANCODE_TAB }
};
std::vector<std::vector<std::string>> intParamMapping = {
{ "scale", "video", "scale" },
{ "fps" , "video", "targetFps" }
};
void gameLoop(Plip::PlipInstance *plip, PlipSdl::Config *config, PlipSdl::SdlEvent *event, PlipSdl::Timer *timer) {
auto audio = plip->GetAudio();
auto video = plip->GetVideo();
auto targetFps = config->GetValue<int>("video", "targetFps");
auto frameTime = 1000000000 / targetFps;
auto running = true;
while(running) {
timer->StopwatchStart();
if(event->ProcessEvents() == PlipSdl::SdlUiEvent::Quit)
running = false;
// 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.
plip->Run(frameTime);
auto time = timer->StopwatchStop();
auto delay = frameTime - time;
while(delay < 0)
delay += frameTime;
timer->Nanosleep(delay);
}
audio->DequeueAll();
}
cxxopts::ParseResult parseCmdLine(int argc, char **argv) {
try {
cxxopts::Options options(argv[0]);
@ -81,9 +62,13 @@ cxxopts::ParseResult parseCmdLine(int argc, char **argv) {
("V,version", "displays the version information and exits")
;
options.add_options("Debug")
("p,pause", "start emulation in a paused state")
;
options.add_options("Video")
( "f,fps", "sets the target frame rate", cxxopts::value<int>()->default_value("60"))
( "s,scale", "sets the default window scaling", cxxopts::value<int>()->default_value("1"))
("f,fps", "sets the target frame rate", cxxopts::value<int>()->default_value("60"))
("s,scale", "sets the default window scaling", cxxopts::value<int>()->default_value("1"))
;
options.parse_positional({"core", "filename", "positional"});
@ -143,8 +128,8 @@ int main(int argc, char **argv) {
auto config = new PlipSdl::Config();
for(auto opt : defaultConfig)
config->SetValue(opt[0], opt[1], opt[2]);
for(auto cfgSetting : defaultConfig)
config->SetValue(cfgSetting[0], cfgSetting[1], cfgSetting[2]);
if(opts["config"].count()) {
auto configFile = opts["config"].as<std::string>();
@ -181,16 +166,20 @@ int main(int argc, char **argv) {
}
auto videoScale = config->GetValue<int>("video", "scale");
auto targetFps = config->GetValue<int>("video", "targetFps");
auto wnd = new PlipSdl::SdlWindow(videoScale, version);
auto audio = new PlipSdl::SdlAudio();
auto plip = new Plip::PlipInstance(wnd, audio);
#ifdef UNIX
auto timer = new PlipSdl::TimerPosix();
#else
auto timer = new PlipSdl::TimerSdl();
#endif
// If the core config exists, send it to the core.
std::string coreConfigSection = "config." + coreName;
auto coreConfig = config->GetSection(coreConfigSection);
if(coreConfig != nullptr && !coreConfig->empty()) {
for(const auto &coreOption : *coreConfig) {
plip->GetConfig()->SetOption(coreOption.first, coreOption.second);
}
}
auto result = plip->Load(coreTag, filename);
switch(result) {
@ -206,17 +195,64 @@ int main(int argc, char **argv) {
auto input = plip->GetInput();
auto event = new PlipSdl::SdlEvent(input);
auto console = new PlipSdl::Console(wnd);
auto consoleFont = config->GetValue("console", "font");
if(consoleFont == config->empty) {
std::cout << "Console font path not defined!\n" << std::endl;
return 1;
}
console->LoadFont(consoleFont);
// Load the frontend keys.
for(auto &it : defaultFrontendKeys) {
auto key = config->GetValue("input.frontend", it.first);
if(key == config->empty)
event->SetKey(it.first, it.second);
else
event->SetKey(it.first, key);
}
// Set the console key.
auto consoleDefault = defaultFrontendKeys.find("console");
auto consoleKey = config->GetValue("input.frontend", "console");
if(consoleDefault == defaultFrontendKeys.cend()) {
std::cerr << "[BUG] No default console key set!" << std::endl;
if(consoleKey == config->empty) {
// Don't continue without a functional console.
std::cerr << "[ERROR] No console key set!" << std::endl;
return 2;
}
}
if(consoleKey == config->empty)
console->SetConsoleKey(consoleDefault->second);
else
console->SetConsoleKey(consoleKey);
// Load inputs for the active core.
std::string section = "input." + coreName;
for(const auto& coreInput : input->GetInputList()) {
for(const auto &coreInput : input->GetInputList()) {
auto key = config->GetValue(section, coreInput.second.GetDescription());
if(key == config->empty) continue;
event->AddDigitalBinding(coreInput.first, key);
}
gameLoop(plip, config, event, timer);
// Configure timer.
#ifdef UNIX
auto timer = new PlipSdl::TimerPosix();
#else
auto timer = new PlipSdl::TimerSdl();
#endif
auto game = new PlipSdl::GameLoop(plip, console, event, timer, targetFps);
if(opts.count("pause"))
game->SetStep(true);
game->Play();
SDL_Quit();
return 0;

BIN
res/ConsoleFont-8x12.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Loading…
Cancel
Save