Skip to content

Commit 108fcce

Browse files
committed
Tetris is now fully playable!
1 parent 6b46f4c commit 108fcce

9 files changed

Lines changed: 265 additions & 4 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ add_executable(GameByte src/main.cpp
4242
src/core/mmu.cpp
4343
src/core/rom.cpp
4444
src/core/ppu.cpp
45+
src/core/joypad.cpp
4546
# Add other.cpp files as you create them
4647
)
4748

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,20 @@ GameByte uses a standard CMake file to do its builds. Currently, builds are offi
2323
You can build with a regular CMake build command, or utilize the VS Code launch tasks to quickly build and run the project in debug mode.
2424

2525
# Running
26-
You will need to provide your own Tetris ROM file from a legal source and place it alongside the executable in a file called `tetris.gb`. GameByte will only work with Tetris currently.
26+
You will need to provide your own Tetris ROM file from a legal source and place it alongside the executable in a file called `tetris.gb`. GameByte will only work with Tetris currently.
27+
28+
# Compatibility
29+
GameByte is a research emulator that is not intended to be fully accurate or usable with a lot of Game Boy games. However, a compatibility list showing tested games are listed below.
30+
31+
## States
32+
- **Playable** - Games that can be completed with playable performance and no game breaking glitches
33+
- **Ingame** - Games that can get in-game but cannot be finished, have serious glitches or insufficient performance
34+
- **Menus** - Games that initially run but cannot go ingame
35+
- **Nothing** - Games that do not initialize properly and/or do not load
36+
37+
## Compatibility List
38+
39+
| **Game Title** | **Publisher** | **Year** | **State** |
40+
|------------------|---------------|----------|-----------|
41+
| Tetris | Nintendo | 1989 | Playable |
42+
| Every other game | Anyone | Any time | Nothing |

src/core/cpu.cpp

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,7 @@ void CPU::init_instructions() {
518518
instructions[0x7C] = { "LD A, H", &CPU::LD_A_H };
519519
instructions[0x7D] = { "LD A, L", &CPU::LD_A_L };
520520

521+
instructions[0xB7] = { "OR A, A", &CPU::OR_A_A };
521522
instructions[0xB0] = { "OR A, B", &CPU::OR_A_B };
522523
instructions[0xB1] = { "OR A, C", &CPU::OR_A_C };
523524
instructions[0xB2] = { "OR A, D", &CPU::OR_A_D };
@@ -543,9 +544,16 @@ void CPU::init_instructions() {
543544
instructions[0xE6] = { "AND A, n8", &CPU::AND_A_n8 };
544545

545546
instructions[0x28] = { "JR Z, e8", &CPU::JR_Z_e8 };
547+
548+
instructions[0x30] = { "JR NC, e8", &CPU::JR_NC_e8 };
549+
instructions[0x38] = { "JR C, e8", &CPU::JR_C_e8 };
550+
546551
instructions[0xC0] = { "RET NZ", &CPU::RET_NZ };
547552
instructions[0xC8] = { "RET Z", &CPU::RET_Z };
548553

554+
instructions[0xD0] = { "RET NC", &CPU::RET_NC };
555+
instructions[0xD8] = { "RET C", &CPU::RET_C };
556+
549557
instructions[0xE1] = { "POP HL", &CPU::POP_HL };
550558
instructions[0xC1] = { "POP BC", &CPU::POP_BC };
551559
instructions[0xD1] = { "POP DE", &CPU::POP_DE };
@@ -660,6 +668,9 @@ void CPU::init_instructions() {
660668
instructions[0x73] = { "LD (HL), E", &CPU::LD_at_HL_E };
661669
instructions[0x74] = { "LD (HL), H", &CPU::LD_at_HL_H };
662670
instructions[0x75] = { "LD (HL), L", &CPU::LD_at_HL_L };
671+
672+
instructions[0x07] = { "RLCA", &CPU::RLCA };
673+
instructions[0x27] = { "DAA", &CPU::DAA };
663674
}
664675

665676
uint8_t CPU::XXX() {
@@ -1354,6 +1365,16 @@ uint8_t CPU::LD_A_A() {
13541365
return 4;
13551366
}
13561367

1368+
uint8_t CPU::OR_A_A() {
1369+
a |= a;
1370+
set_flag_z(a == 0);
1371+
set_flag_n(false);
1372+
set_flag_h(false);
1373+
set_flag_c(false);
1374+
1375+
return 4;
1376+
}
1377+
13571378
uint8_t CPU::OR_A_B() {
13581379
a |= b;
13591380
set_flag_z(a == 0);
@@ -1542,6 +1563,30 @@ uint8_t CPU::JR_Z_e8() {
15421563
return 8;
15431564
}
15441565

1566+
uint8_t CPU::JR_C_e8() {
1567+
int8_t offset = static_cast<int8_t>(mmu->read_byte(pc));
1568+
pc++;
1569+
1570+
if (get_flag_c()) {
1571+
pc += offset;
1572+
return 12;
1573+
}
1574+
1575+
return 8;
1576+
}
1577+
1578+
uint8_t CPU::JR_NC_e8() {
1579+
int8_t offset = static_cast<int8_t>(mmu->read_byte(pc));
1580+
pc++;
1581+
1582+
if (!get_flag_c()) {
1583+
pc += offset;
1584+
return 12;
1585+
}
1586+
1587+
return 8;
1588+
}
1589+
15451590
uint8_t CPU::RET_NZ() {
15461591
if (!get_flag_z()) {
15471592
// Pop address from stack into PC
@@ -1564,6 +1609,30 @@ uint8_t CPU::RET_Z() {
15641609
return 8;
15651610
}
15661611

1612+
uint8_t CPU::RET_NC() {
1613+
if (!get_flag_c()) {
1614+
// Pop address from stack into PC
1615+
pc = mmu->read_word(sp);
1616+
sp += 2;
1617+
1618+
return 20;
1619+
} else {
1620+
return 8;
1621+
}
1622+
}
1623+
1624+
uint8_t CPU::RET_C() {
1625+
if (get_flag_c()) {
1626+
// Pop address from stack into PC
1627+
pc = mmu->read_word(sp);
1628+
sp += 2;
1629+
1630+
return 20;
1631+
} else {
1632+
return 8;
1633+
}
1634+
}
1635+
15671636
uint8_t CPU::LD_A_BC_ptr() {
15681637
a = mmu->read_byte(get_bc());
15691638
return 8;
@@ -2247,4 +2316,55 @@ uint8_t CPU::LD_at_HL_L() {
22472316
mmu->write_byte(address, l);
22482317

22492318
return 8;
2319+
}
2320+
2321+
uint8_t CPU::RLCA() {
2322+
// Find bit 7 and rotate it
2323+
uint8_t bit7 = (a & 0x80) >> 7;
2324+
a = (a << 1) | bit7;
2325+
2326+
set_flag_z(false);
2327+
set_flag_n(false);
2328+
set_flag_h(false);
2329+
set_flag_c(bit7 == 1);
2330+
2331+
return 4;
2332+
}
2333+
2334+
uint8_t CPU::DAA() {
2335+
uint8_t adjustment = 0;
2336+
bool new_carry = false;
2337+
2338+
if (!get_flag_n()) {
2339+
// After an ADD operation
2340+
if (get_flag_c() || a > 0x99) {
2341+
adjustment |= 0x60;
2342+
new_carry = true;
2343+
}
2344+
if (get_flag_h() || (a & 0x0F) > 0x09) {
2345+
adjustment |= 0x06;
2346+
}
2347+
} else {
2348+
// After a SUB operation
2349+
if (get_flag_c()) {
2350+
adjustment |= 0x60;
2351+
new_carry = true;
2352+
}
2353+
if (get_flag_h()) {
2354+
adjustment |= 0x06;
2355+
}
2356+
2357+
// Subtract the adjustment instead of adding it
2358+
a -= adjustment;
2359+
}
2360+
2361+
if (!get_flag_n()) {
2362+
a += adjustment;
2363+
}
2364+
2365+
set_flag_z(a == 0);
2366+
set_flag_h(false);
2367+
set_flag_c(new_carry);
2368+
2369+
return 4;
22502370
}

src/core/cpu.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,9 @@ class CPU {
296296
// Copy 8-bit value from A register to A register (0x7F)
297297
uint8_t LD_A_A();
298298

299+
// Bitwise OR with A and A register, result in A (0xB7)
300+
uint8_t OR_A_A();
301+
299302
// Bitwise OR with A and B register, result in A (0xB0)
300303
uint8_t OR_A_B();
301304

@@ -359,12 +362,24 @@ class CPU {
359362
// Jump relative if zero flag set (0x28)
360363
uint8_t JR_Z_e8();
361364

365+
// Jump relative if carry flag not set (0x30)
366+
uint8_t JR_NC_e8();
367+
368+
// Jump relative if carry flag set (0x38)
369+
uint8_t JR_C_e8();
370+
362371
// Return from subroutine if zero flag not set (0xC0)
363372
uint8_t RET_NZ();
364373

365374
// Return from subroutine if zero flag set (0xC8)
366375
uint8_t RET_Z();
367376

377+
// Return from subroutine if carry flag not set (0xD0)
378+
uint8_t RET_NC();
379+
380+
// Return from subroutine if carry flag set (0xD8)
381+
uint8_t RET_C();
382+
368383
// Load A Indirect Group
369384
uint8_t LD_A_BC_ptr(); // 0x0A
370385
uint8_t LD_A_DE_ptr(); // 0x1A
@@ -554,6 +569,12 @@ class CPU {
554569

555570
// Write value of register L into address pointed to by HL (0x75)
556571
uint8_t LD_at_HL_L();
572+
573+
// Rotate A register left circular (0x07)
574+
uint8_t RLCA();
575+
576+
// Decimal Adjust Accumulator (0x27)
577+
uint8_t DAA();
557578
private:
558579
// Performs addition (ADD/ADC) and updates flags
559580
// carry: if true, adds the C flag to the sum

src/core/joypad.cpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#include "joypad.h"
2+
3+
uint8_t Joypad::get_joyp_state() {
4+
uint8_t res = 0xC0 | control_mask;
5+
uint8_t buttons = 0x0F;
6+
7+
// Direction keys selected (Bit 4 is 0)
8+
if (!(control_mask & 0x10)) {
9+
buttons &= direction_buttons;
10+
}
11+
12+
// Action keys selected (Bit 5 is 0)
13+
if (!(control_mask & 0x20)) {
14+
buttons &= action_buttons;
15+
}
16+
17+
return (res & 0xF0) | (buttons & 0x0F);
18+
}
19+
20+
bool Joypad::handle_sdl_event(const SDL_Event& e) {
21+
if (e.type != SDL_EVENT_KEY_DOWN && e.type != SDL_EVENT_KEY_UP) return false;
22+
23+
bool pressed = (e.type == SDL_EVENT_KEY_DOWN);
24+
SDL_Keycode key = e.key.key;
25+
bool interrupt_needed = false;
26+
27+
auto update_bit = [&](uint8_t& buttons, int bit, bool is_pressed) {
28+
if (is_pressed) {
29+
// If the button was previously NOT pressed (bit was 1), trigger interrupt
30+
if (buttons & (1 << bit)) interrupt_needed = true;
31+
buttons &= ~(1 << bit);
32+
} else {
33+
buttons |= (1 << bit);
34+
}
35+
};
36+
37+
switch (key) {
38+
// Directions
39+
case SDLK_RIGHT: update_bit(direction_buttons, 0, pressed); break;
40+
case SDLK_LEFT: update_bit(direction_buttons, 1, pressed); break;
41+
case SDLK_UP: update_bit(direction_buttons, 2, pressed); break;
42+
case SDLK_DOWN: update_bit(direction_buttons, 3, pressed); break;
43+
44+
// Actions
45+
case SDLK_Z: update_bit(action_buttons, 0, pressed); break; // A
46+
case SDLK_X: update_bit(action_buttons, 1, pressed); break; // B
47+
case SDLK_RSHIFT: update_bit(action_buttons, 2, pressed); break; // Select
48+
case SDLK_RETURN: update_bit(action_buttons, 3, pressed); break; // Start
49+
50+
default: break;
51+
}
52+
53+
return interrupt_needed;
54+
}

src/core/joypad.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#pragma once
2+
#include <cstdint>
3+
#include <SDL3/SDL.h>
4+
5+
class Joypad {
6+
public:
7+
// Button states (0 = pressed, 1 = released)
8+
uint8_t action_buttons = 0x0F; // Start, Select, B, A
9+
uint8_t direction_buttons = 0x0F; // Down, Up, Left, Right
10+
11+
// The value written by the CPU to $FF00 to select which buttons to read
12+
uint8_t control_mask = 0x30;
13+
14+
// Get current state of the joypad register ($FF00)
15+
uint8_t get_joyp_state();
16+
17+
// Handles SDL events and returns true if a Joypad Interrupt (bit 4) should be requested
18+
bool handle_sdl_event(const SDL_Event& event);
19+
};

src/core/mmu.cpp

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "mmu.h"
22
#include "cpu.h"
33
#include "ppu.h"
4+
#include "joypad.h"
45
#include <cstring>
56
#include <iostream>
67
#include <sstream>
@@ -26,13 +27,17 @@ void MMU::connect_ppu(PPU* p) {
2627
ppu = p;
2728
}
2829

30+
void MMU::connect_joypad(Joypad* j) {
31+
joypad = j;
32+
}
33+
2934
bool MMU::load_game(const uint8_t* data, size_t size) {
3035
// Clear cartridge memory
3136
memset(cart, 0, sizeof(cart));
3237

3338
if (size > sizeof(cart)) {
3439
// For now, if ROM is larger than 32KB, throw an error. In the future this needs to be handled by MBC logic.
35-
throw std::runtime_error("[MMU] ERROR: ROM size (" + std::to_string(size) + ") larger than 32KB. Bank switching is not currently supported.");
40+
throw std::runtime_error("[MMU] ERROR: ROM size (" + std::to_string(size) + ") larger than 32KB. Bank switching/MBC logic is not currently supported.");
3641
std::memcpy(cart, data, sizeof(cart));
3742
} else {
3843
std::memcpy(cart, data, size);
@@ -67,8 +72,8 @@ uint8_t MMU::read_byte(uint16_t address) {
6772
} else if (address >= 0xFF00 && address <= 0xFF7F) {
6873
// Joypad (0xFF00)
6974
if (address == 0xFF00) {
70-
return 0xFF; // No keys pressed (Active Low)
71-
}
75+
return joypad ? joypad->get_joyp_state() : 0xFF;
76+
}
7277

7378
// I/O Registers
7479
if (address == 0xFF04 && cpu) {
@@ -108,6 +113,15 @@ void MMU::write_byte(uint16_t address, uint8_t value) {
108113
std::stringstream ss;
109114

110115
// Special write cases (i.e. I/O registers, VRAM, etc)
116+
// Joypad
117+
if (address == 0xFF00) {
118+
if (joypad) {
119+
// Only bits 4 and 5 are writable by the CPU
120+
joypad->control_mask = (value & 0x30);
121+
}
122+
return;
123+
}
124+
111125
// PPU
112126
if (address >= 0xFF40 && address <= 0xFF47) {
113127
// Always update the I/O memory map so reads (like in PPU::draw_scanline) get the correct value

src/core/mmu.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
class CPU;
99
class PPU;
10+
class Joypad;
1011

1112
/**
1213
* @brief Implements the Game Boy's Memory Management Unit (MMU).alignas
@@ -37,6 +38,9 @@ class MMU {
3738
PPU* ppu = nullptr;
3839
void connect_ppu(PPU* p);
3940

41+
Joypad* joypad = nullptr;
42+
void connect_joypad(Joypad* j);
43+
4044
uint8_t read_byte(uint16_t address);
4145
void write_byte(uint16_t address, uint8_t value);
4246

0 commit comments

Comments
 (0)