commit 5c528eb8d224a9c46ef25084e37b062eb4e8ebbc Author: YayIguess Date: Mon Feb 5 03:09:55 2024 -0800 initial upload diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..788d578 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +cmake-build-debug/ +.idea + +roms/ +bootrom.bin diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3d32882 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.25) +project(GBpp) +set(CMAKE_CXX_STANDARD 23) + +find_package(SDL2 REQUIRED) +include_directories(${SDL2_INCLUDE_DIRS}) + +add_executable(GBpp src/main.cpp src/gameboy.cpp src/opcode.cpp + src/interupts.cpp src/ppu.cpp) +target_link_libraries(GBpp ${SDL2_LIBRARIES}) \ No newline at end of file diff --git a/src/defines.hpp b/src/defines.hpp new file mode 100644 index 0000000..043466d --- /dev/null +++ b/src/defines.hpp @@ -0,0 +1,51 @@ +#ifndef GBPP_SRC_DEFINES_HPP_ +#define GBPP_SRC_DEFINES_HPP_ + +#define Word uint16_t +#define Byte uint8_t + +// Bit Name Set Clr Expl. +// 7 zf Z NZ Zero Flag +// 6 n - - Add/Sub-Flag (BCD) +// 5 h - - Half Carry Flag (BCD) +// 4 cy C NC Carry Flag +// 3-0 - - - Not used (always zero) +#define CARRY_FLAG 4 //'C' +#define HALFCARRY_FLAG 5 //'H' +#define SUBTRACT_FLAG 6 //'N' +#define ZERO_FLAG 7 //'Z' + +#define VBLANK_INTERRUPT 0 +#define LCD_STAT_INTERRUPT 1 +#define TIMER_INTERRUPT 2 +#define SERIAL_INTERRUPT 3 +#define JOYPAD_INTERRUPT 4 + +#define T_CLOCK_FREQ 4194304 + +#define DIVIDER_REGISTER_FREQ 16384 + +#define BOOTROM_SIZE 0x100 + +#define RESOLUTION_X 160 +#define RESOLUTION_Y 144 +#define SCREEN_BPP 3 + +#define ROM_BANK_SIZE 0x4000 + +#define SCANLINES_PER_FRAME 154 +#define SCANLINE_DURATION 456 +#define FRAME_DURATION 70224 +#define MODE2_DURATION 80 +#define MODE3_BASE_DURATION 168 +#define MODE0_3_DURATION 376 //mode3 is 168 to 291, mode0 85 to 208 +#define MODE1_DURATION 4560 + +#define H_SYNC 9198 +#define V_SYNC 59.73 +#define HBlank_DURATION 204 //GPU_MODE 0 +#define SCANLINE_OAM_FREQ 80 //GPU_MODE 2 +#define SCANLINE_VRAM_FREQ 80 //GPU_MODE 3 + + +#endif diff --git a/src/gameboy.cpp b/src/gameboy.cpp new file mode 100644 index 0000000..18654b1 --- /dev/null +++ b/src/gameboy.cpp @@ -0,0 +1,94 @@ +#include +#include "gameboy.hpp" + +bool AddressSpace::getBootromState() +{ + return bootromLoaded; +} + +void AddressSpace::unmapBootrom() +{ + bootromLoaded = false; +} + +void AddressSpace::mapBootrom() +{ + bootromLoaded = true; +} + +void AddressSpace::loadBootrom(std::string filename) +{ + std::ifstream file; + int size = std::filesystem::file_size(filename); + if(size != 256) + { + std::cerr << "Bootrom was an unexpected size!\nQuitting!\n" << std::endl; + exit(1); + } + file.open(filename, std::ios::binary); + file.read(reinterpret_cast(bootrom), BOOTROM_SIZE); +} + +void AddressSpace::loadGame(std::string filename) +{ + game.open(filename, std::ios::binary); + + if(!game.is_open()) + { + std::cerr << "Game was not found!\nQuitting!\n" << std::endl; + exit(1); + } + game.read(reinterpret_cast(memoryLayout.romBank1), ROM_BANK_SIZE*2); +} + +void GameBoy::addCycles(uint8_t ticks) +{ + cycles = (cycles+ticks) % T_CLOCK_FREQ; + lastOpTicks = ticks; +} + +void test() +{ + printf(""); +} + +void GameBoy::start(std::string bootrom, std::string game) +{ + addressSpace.loadBootrom(bootrom); + addressSpace.loadGame(game); + + bool quit = false; + while(!quit) + { + // Event loop: Check and handle SDL events +// if(SDL_PollEvent(&event)) +// { +// if(event.type == SDL_QUIT) +// { +// quit = true; // Set the quit flag when the close button is hit +// } +// } + + opcodeHandler(); + interruptHandler(); + //timing(); + ppuUpdate(); + if(PC == 0x8c && DE.hi == 0) + test(); + if(PC > 0xFF && addressSpace.getBootromState()) + { + addressSpace.unmapBootrom(); + } + if(PC > 0x2FF) + { + test(); + } + int cyclesSince = cyclesSinceLastRefresh(); + if(cyclesSince > FRAME_DURATION) + { + lastRefresh = cycles; + SDL2present(); + } + } +} + diff --git a/src/gameboy.hpp b/src/gameboy.hpp new file mode 100644 index 0000000..291d9e0 --- /dev/null +++ b/src/gameboy.hpp @@ -0,0 +1,277 @@ +#ifndef GBPP_SRC_GAMEBOY_HPP_ +#define GBPP_SRC_GAMEBOY_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include "defines.hpp" + +//two bits per colour +enum Colour +{ + black = 0b11, + darkGray = 0b10, + lightGray = 0b01, + white = 0b00 +}; + +enum PPUMode +{ + mode0, // Horizontal Blank (Mode 0): No access to video RAM, occurs during horizontal blanking period. + mode1, // Vertical Blank (Mode 1): No access to video RAM, occurs during vertical blanking period. + mode2, // OAM Search (Mode 2): Access to OAM (Object Attribute Memory) only, sprite evaluation. + mode3 // Pixel Transfer (Mode 3): Access to both OAM and video RAM, actual pixel transfer to the screen. +}; + +union RegisterPair +{ + Word reg; //register.reg == (hi << 8) + lo. (hi is more significant than lo) + + struct + { + Byte lo; + Byte hi; + }; +}; + +class AddressSpace +{ + private: + bool bootromLoaded = true; + Byte bootrom[BOOTROM_SIZE]; + std::ifstream game; + + public: + AddressSpace() + { + // Initialize the memory to zero + memoryLayout = {}; + std::memset(memoryLayout.memory, 0, sizeof(memoryLayout.memory)); + } + + // Nested union for the memory layout + union MemoryLayout + { + Byte memory[0x10000]; + struct + { + Byte romBank1[ROM_BANK_SIZE]; // Mapped to 0x0000 + Byte romBankSwitch[ROM_BANK_SIZE]; // Mapped to 0x4000 + Byte vram[0x2000]; // Mapped to 0x8000 + Byte externalRam[0x2000]; // Mapped to 0xA000 + Byte memoryBank1[0x1000]; // Mapped to 0xC000 + Byte memoryBank2[0x1000]; // Mapped to 0xD000 + Byte echoRam[0x1E00]; // Mapped to 0xE000 (Echo RAM, mirrors 0xC000 to 0xDFFF) + Byte spriteAttributeTable[0xA0]; // Mapped to 0xFE00 + Byte notUsable[0x60]; // Mapped to 0xFEA0 + Byte io[0x80]; // Mapped to 0xFF00, 0xFF0F is interrupt flag + Byte specialRam[0x7F]; // Mapped to 0xFF80 + Byte interuptEnableReg; // Mapped to 0xFFFF + }; + } memoryLayout; + + bool getBootromState(); + void unmapBootrom(); + void mapBootrom(); + void loadBootrom(std::string filename); + void loadGame(std::string filename); + + //overload [] for echo ram and bootrom support + Byte operator[](uint32_t address) const + { + if(address >= 0xE000 && address < 0xFE00) + return memoryLayout.echoRam[address - 0xE000]; + if(address < 0x0100 && bootromLoaded) + return bootrom[address]; + else + return memoryLayout.memory[address]; + } + Byte& operator[](uint32_t address) + { + if(address >= 0xE000 && address < 0xFE00) + return memoryLayout.echoRam[address - 0xE000]; + if(address < 0x0100 && bootromLoaded) + return bootrom[address]; + else + return memoryLayout.memory[address]; + } +}; + +class GameBoy { + private: + uint32_t cycles = 0; + uint32_t lastOpTicks = 0; + uint32_t lastRefresh = 0; + uint32_t lastScanline = 0; + uint32_t cyclesToStayInHblank = -1; + + uint8_t IME = 0; //enables interupts + + //Accumulator and flags + RegisterPair AF; + //General purpose CPU registers + RegisterPair BC; + RegisterPair DE; + RegisterPair HL; + + Word SP = 0xFFFE; //stack pointer + Word PC = 0x0000; //program counter + + AddressSpace addressSpace; + + //General purpose hardware registers + Byte* JOYP = &addressSpace[0xFF00]; + Byte* SB = &addressSpace[0xFF01]; + Byte* SC = &addressSpace[0xFF02]; + Byte* DIV = &addressSpace[0xFF04]; + + //Timer registers + Byte* TIMA = &addressSpace[0xFF05]; + Byte* TMA = &addressSpace[0xFF15]; //unused + Byte* TAC = &addressSpace[0xFF16]; + + //interrupt flag and enable + Byte* IF = &addressSpace[0xFF0F]; + Byte* IE = &addressSpace[0xFFFF]; + + //Sound registers + Byte* NR10 = &addressSpace[0xFF10]; + Byte* NR11 = &addressSpace[0xFF11]; + Byte* NR12 = &addressSpace[0xFF12]; + Byte* NR13 = &addressSpace[0xFF13]; + Byte* NR14 = &addressSpace[0xFF14]; + Byte* NR20 = &addressSpace[0xFF15]; //unused + Byte* NR21 = &addressSpace[0xFF16]; + Byte* NR22 = &addressSpace[0xFF17]; + Byte* NR23 = &addressSpace[0xFF18]; + Byte* NR24 = &addressSpace[0xFF19]; + Byte* NR30 = &addressSpace[0xFF1A]; + Byte* NR31 = &addressSpace[0xFF1B]; + Byte* NR32 = &addressSpace[0xFF1C]; + Byte* NR33 = &addressSpace[0xFF1D]; + Byte* NR34 = &addressSpace[0xFF1E]; + Byte* NR40 = &addressSpace[0xFF1F]; //unused + Byte* NR41 = &addressSpace[0xFF20]; + Byte* NR42 = &addressSpace[0xFF21]; + Byte* NR43 = &addressSpace[0xFF22]; + Byte* NR44 = &addressSpace[0xFF23]; + Byte* NR50 = &addressSpace[0xFF24]; + Byte* NR51 = &addressSpace[0xFF25]; + Byte* NR52 = &addressSpace[0xFF26]; + Byte* waveRam = &addressSpace[0xFF30]; //WaveRam[0x10] + + //PPU registers + Byte* LCDC = &addressSpace[0xFF40]; + Byte* STAT = &addressSpace[0xFF41]; + Byte* SCY = &addressSpace[0xFF42]; + Byte* SCX = &addressSpace[0xF43]; + Byte* LY = &addressSpace[0xFF44]; + Byte* LYC = &addressSpace[0xFF45]; + Byte* DMA = &addressSpace[0xFF46]; + Byte* BGP = &addressSpace[0xFF47]; + Byte* OBP0 = &addressSpace[0xFF48]; + Byte* OBP1 = &addressSpace[0xFF49]; + Byte* WY = &addressSpace[0xFF4A]; + Byte* WX = &addressSpace[0xFF4B]; + + PPUMode currentMode; + + //3 colour channels + uint32_t* framebuffer = new uint32_t[RESOLUTION_X*RESOLUTION_Y*SCREEN_BPP]; + SDL_Window *screen; + SDL_Renderer *renderer; + SDL_Texture *texture; + SDL_Event event; + + void opcodeHandler(); + void ppuUpdate(); + void drawLine(); + void SDL2present(); + + void checkPPUMode(); + void setPPUMode(PPUMode mode); + int cyclesSinceLastScanline(); + int cyclesSinceLastRefresh(); + + void interruptHandler(); + bool testInterruptEnabled(Byte interrupt); + void resetInterrupt(Byte interrupt); + + void VBlankHandle(); + void LCDStatHandle(); + void timerHandle(); + void serialHandle(); + void joypadHandle(); + + void setFlag(Byte bit); + void resetFlag(Byte bit); + bool getFlag(Byte bit) const; + + Word getWordPC(); + Byte getBytePC(); + Word getWordSP(); + Byte getByteSP(); + + void addCycles(Byte ticks); + + //OPCODE FUNCTIONS + template + void ld(T& dest, T src); + template + void orBitwise(T &dest, T src); + template + void andBitwise(T &dest, T src); + template + void xorBitwise(T& dest, T src); + template + void bit(T testBit, T reg); + template + void jp(T address); + template + bool jrNZ(T offset); + template + void inc(T& reg); + template + void call(T address); + void halt(); + void stop(); + template + void ldW(T dest, T src); + template + void cp(T value); + template + void dec(T& reg); + template + bool jrZ(T offset); + template + void sub(T value); + template + void jr(T OFFSET); + template + void push(T reg); + template + void rl(T& reg); + template + void pop(T& reg); + template + void rla(T& reg); + template + void rst(T address); + void ret(); + template + void add(T& reg, T value); + void cpl(); + void ccf(); + void swap(Byte &value); + + public: + void start(std::string bootrom, std::string game); + void SDL2setup(); + void SDL2destroy(); +}; + +#endif //GBPP_SRC_GAMEBOY_HPP_ diff --git a/src/interupts.cpp b/src/interupts.cpp new file mode 100644 index 0000000..6696afc --- /dev/null +++ b/src/interupts.cpp @@ -0,0 +1,78 @@ +#include "defines.hpp" +#include "gameboy.hpp" + +bool GameBoy::testInterruptEnabled(Byte interrupt) +{ + return (*IE) & (Byte) (1 << interrupt); +} + +void GameBoy::resetInterrupt(Byte interrupt) +{ + *IF &= ~(1 << interrupt); +} + +void GameBoy::interruptHandler() +{ + if(!IME) + return; + + if(*IF & (Byte) (1 << VBLANK_INTERRUPT) && testInterruptEnabled(VBLANK_INTERRUPT)) + VBlankHandle(); + if(*IF & (Byte) (1 << LCD_STAT_INTERRUPT) && testInterruptEnabled(LCD_STAT_INTERRUPT)) + LCDStatHandle(); + if(*IF & (Byte) (1 << TIMER_INTERRUPT) && testInterruptEnabled(TIMER_INTERRUPT)) + timerHandle(); + if(*IF & (Byte) (1 << SERIAL_INTERRUPT) && testInterruptEnabled(SERIAL_INTERRUPT)) + serialHandle(); + if(*IF & (Byte) (1 << JOYPAD_INTERRUPT) && testInterruptEnabled(JOYPAD_INTERRUPT)) + joypadHandle(); +} + +void GameBoy::VBlankHandle() +{ + printf("VBlank interrupt"); + IME = 0; + push(PC); + PC = 0x40; + resetInterrupt(VBLANK_INTERRUPT); +} + +void GameBoy::LCDStatHandle() +{ + printf("LCD stat interrupt"); + IME = 0; + push(PC); + addCycles(16); + PC = 0x48; + resetInterrupt(LCD_STAT_INTERRUPT); +} + +void GameBoy::timerHandle() +{ + printf("timer interrupt"); + IME = 0; + push(PC); + addCycles(16); + PC = 0x50; + resetInterrupt(TIMER_INTERRUPT); +} + +void GameBoy::serialHandle() +{ + printf("serial interrupt"); + IME = 0; + push(PC); + addCycles(16); + PC = 0x58; + resetInterrupt(SERIAL_INTERRUPT); +} + +void GameBoy::joypadHandle() +{ + printf("joypad interrupt"); + IME = 0; + push(PC); + addCycles(16); + PC = 0x60; + resetInterrupt(JOYPAD_INTERRUPT); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..da4648c --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,18 @@ +#include +#include +#include +#include +#include "gameboy.hpp" +#include "defines.hpp" + +namespace fs = std::filesystem; + +int main(int argc, char** argv) +{ + auto* gb = new GameBoy(); + gb->SDL2setup(); + gb->start("/home/braiden/Code/GBpp/bootrom.bin", "/home/braiden/Code/GBpp/roms/DrMario.gb"); + gb->SDL2destroy(); + delete gb; + return 0; +} diff --git a/src/ppu.cpp b/src/ppu.cpp new file mode 100644 index 0000000..d154dd6 --- /dev/null +++ b/src/ppu.cpp @@ -0,0 +1,212 @@ +#include "gameboy.hpp" +#include "defines.hpp" + + + +void GameBoy::ppuUpdate() +{ + //test HBlank + checkPPUMode(); + + if(cyclesToStayInHblank != -1) + { + if (cyclesToStayInHblank < cyclesSinceLastScanline()) + { + return; + } + if(cyclesToStayInHblank >= cyclesSinceLastScanline()) + { + lastScanline = cycles; + cyclesToStayInHblank = -1; + } + } + + // Check the PPU mode (HBlank, VBlank, OAM Search, or Pixel Transfer) + Byte mode = (*STAT)&0x03; + switch(mode) + { + case 0: + if(cyclesSinceLastScanline() > MODE2_DURATION + MODE3_BASE_DURATION) + { + drawLine(); + cyclesToStayInHblank = SCANLINE_DURATION - cyclesSinceLastScanline(); + lastScanline = cycles; + (*LY)++; + if((*LY) > 153) + (*LY) = 0; + } + currentMode = PPUMode::mode0; + break; + + //vblank + case 1: + if(currentMode != PPUMode::mode1) + { + drawLine(); + } + if(cyclesSinceLastScanline() > SCANLINE_DURATION) + { + lastScanline = cycles; + (*LY)++; + if((*LY) > 153) + (*LY) = 0; + } + currentMode = PPUMode::mode1; + break; + case 2: + currentMode = PPUMode::mode2; + break; + case 3: + currentMode = PPUMode::mode3; + break; + } + + if ((*LY) == (*LYC) || (*STAT)&(1 << 6)) + { + // Request STAT interrupt if LY matches LYC + //bug on DMG models triggers a STAT interrupt anytime the STAT register is written + //Road Rage and Zerd no Denetsu rely on this + (*STAT) |= (1 << 2); + } + + // Check for STAT interrupts and request if needed (e.g., when entering specific modes) + bool hBlankInterruptEnabled = (*STAT)&(1 << 3); + bool vBlankInterruptEnabled = (*STAT)&(1 << 4);/* Determine if VBlank interrupt is enabled */ + bool oamInterruptEnabled = (*STAT)&(1 << 5);/* Determine if OAM Search interrupt is enabled */ + + if (currentMode == PPUMode::mode0 && hBlankInterruptEnabled) + { + // Request HBlank interrupt + } + else if (currentMode == PPUMode::mode1 && vBlankInterruptEnabled) + { + // Request VBlank interrupt + } + else if (currentMode == PPUMode::mode2 && oamInterruptEnabled) + { + // Request OAM Search interrupt + } +} + +int GameBoy::cyclesSinceLastScanline() +{ + int difference = cycles - lastScanline; + if (difference < 0) + { + // Handle the case when cycles has wrapped around (overflowed) + difference += T_CLOCK_FREQ; + } + return difference; +} + +int GameBoy::cyclesSinceLastRefresh() +{ + int difference = cycles - lastRefresh; + if (difference < 0) + { + // Handle the case when cycles has wrapped around (overflowed) + difference += T_CLOCK_FREQ; + } + return difference; +} + +void GameBoy::checkPPUMode() +{ + int oamFetchTime = 0; + if ((*LY) < 144) + { + int currentDuration = cyclesSinceLastScanline(); + // Active Display Period (HBlank, OAM Search, and Pixel Transfer) + if(currentDuration < MODE2_DURATION) + setPPUMode(PPUMode::mode2); + else if(currentDuration < MODE2_DURATION + MODE3_BASE_DURATION + oamFetchTime) + setPPUMode(PPUMode::mode3); + else + setPPUMode(PPUMode::mode0); + } + // VBlank Period + else + setPPUMode(PPUMode::mode1); +} + +void GameBoy::setPPUMode(PPUMode mode) +{ + switch(mode) + { + case PPUMode::mode0: + (*STAT) &= ~0x03; + break; + case PPUMode::mode1: + (*STAT) &= ~0x03; + (*STAT) |= 0x01; + //set vblank interrupt flag + (*IF) |= 0x01; + break; + case PPUMode::mode2: + (*STAT) &= ~0x03; + (*STAT) |= 0x02; + break; + case PPUMode::mode3: + (*STAT) &= ~0x03; + (*STAT) |= 0x03; + break; + } +} + +void GameBoy::drawLine() +{ + uint8_t line = (*LY); + + // Calculate the starting index of the current scanline in the framebuffer + uint32_t lineStartIndex = line * RESOLUTION_X; + + // Pointer to the current line's pixel data in the framebuffer + uint32_t* currentLinePixels = framebuffer + lineStartIndex; + + // For example, if you are setting the entire scanline to a color (e.g., white): + for (int i = 0; i < RESOLUTION_X; i++) + { + // Assuming white color is represented as 0xFFFFFFFF (ARGB format) +// if(currentLinePixels[i] == 0xFFFFFFFF) +// currentLinePixels[i] = 0xFF000000; +// else + currentLinePixels[i] = 0xFFFFFFFF; + } +} + +void GameBoy::SDL2setup() +{ + SDL_Init(SDL_INIT_EVERYTHING); + screen = SDL_CreateWindow("GBpp", + SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + RESOLUTION_X, RESOLUTION_Y, + SDL_WINDOW_OPENGL); + + // Create an SDL renderer to draw on the window + renderer = SDL_CreateRenderer(screen, -1, SDL_RENDERER_ACCELERATED); + + // Create an SDL texture to hold the framebuffer data + texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, + SDL_TEXTUREACCESS_STREAMING, RESOLUTION_X, RESOLUTION_Y); +} + +void GameBoy::SDL2destroy() +{ + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(screen); + SDL_Quit(); +} + +void GameBoy::SDL2present() +{ + // Update the SDL texture with the framebuffer data + SDL_UpdateTexture(texture, NULL, framebuffer, RESOLUTION_X * sizeof(uint32_t)); + + // Clear the renderer and render the texture + SDL_RenderClear(renderer); + SDL_RenderCopy(renderer, texture, NULL, NULL); + + // Present the renderer on the screen + SDL_RenderPresent(renderer); +} \ No newline at end of file