This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Validation and Implementation of Immediate Loads (LD r, d8)
Summary
After diagnosis which revealed that the screen was blank andL.Y.was stuck at 0, the root cause was identified to be that the C++ CPU was returning 0 cycles when encountering unimplemented opcodes. This froze the time of the emulated machine, preventing the PPU from advancing.
Although the instructionsRH r, d8(immediate 8-bit loads) were already implemented in the C++ code, this step documents their critical importance and validates their complete operation through a parameterized test that verifies the 7 instructions:RH B, d8, RH C, d8, RH D, d8, RH E, d8, RH H, d8, RH L, d8, andRH A, d8.
These instructions arefundamentalbecause they are the first that any ROM executes at startup: they load immediate values into the registers to initialize the state of the machine. Without them, the CPU cannot advance beyond the first few instructions of the game code.
Hardware Concept
The instructionsRH r, d8(Load register with immediate 8-bit value) are part of the most basic instruction set of the LR35902 (Game Boy CPU). They allow an 8-bit constant value to be loaded directly into an 8-bit register.
Instruction Format
Each instructionRH r, d8consists of:
- Opcode(1 byte): Identifies the operation and the destination register
- d8(1 byte): The immediate value to load (0x00 - 0xFF)
LD opcodes r, d8
| Opcode | Instruction | M-Cycles | Bytes |
|---|---|---|---|
0x06 |
RH B, d8 | 2 | 2 |
0x0E |
RH C, d8 | 2 | 2 |
0x16 |
RH D, d8 | 2 | 2 |
0x1E |
RH E, d8 | 2 | 2 |
0x26 |
RH H, d8 | 2 | 2 |
0x2E |
RH L, d8 | 2 | 2 |
0x3E |
RH A, d8 | 2 | 2 |
Why are they Criticism?
When booting any Game Boy ROM, the boot/initialization codealwaysstarts executing sequences of instructionsRH r, d8for:
- Initialize records: Set starting values for B, C, D, E, H, L and A
- Set addresses: Prepare register pairs (HL, BC, DE) with initial memory addresses
- Set constants: Load magic values, masks, or initial flags
Without these instructions implemented, the CPU finds the firstRH r, d8in the ROM, enter the casedefaultof theswitch(opcode not implemented), returns 0 cycles, and the emulated machine time freezes. That is why the diagnosis showed thatL.Y.was stuck at 0 - the PPU was not receiving cycles because the CPU was not executing instructions.
Timing
All instructionsRH r, d8consume2 M-Cycles(8 T-Cycles):
- M-Cycle 1: Opcode reading (4 T-Cycles)
- M-Cycle 2: Reading the immediate byte d8 and writing to the register (4 T-Cycles)
The Program Counter (PC) advances 2 bytes: the opcode and the immediate value.
Implementation
The instructionsRH r, d8were already implemented insrc/core/cpp/CPU.cppinside the methodCPU::step(). However, validation was improved through a parameterized test that verifies all 7 instructions systematically.
Implemented Code
Each instruction follows the same simple pattern:
case 0x0E://LD C, d8
{
uint8_t value = fetch_byte(); // Read the next byte (d8)
regs_->c = value; // Assign to register
cycles_ += 2; // Accumulate 2 M-Cycles
return 2; // Return 2 M-Cycles
}
The methodfetch_byte()is in charge of:
- Read the memory byte at PC address
- Increase PC automatically
- Handle wrap-around in 16 bits (PC stays in the range 0x0000-0xFFFF)
Parameterized Test
A parameterized test was added usingpytest.mark.parametrizethat validates all instructionsRH r, d8in a single function:
@pytest.mark.parametrize("opcode,register_name,test_value", [
(0x06, 'b', 0x33), #LD B, d8
(0x0E, 'c', 0x42), # LD C, d8
(0x16, 'd', 0x55), #LD D, d8
(0x1E, 'e', 0x78), #LD E, d8
(0x26, 'h', 0x9A), #LD H, d8
(0x2E, 'l', 0xBC), # LD L, d8
(0x3E, 'a', 0xDE), # LD A, d8
])
def test_ld_register_immediate(self, opcode, register_name, test_value):
"""Validates that each instruction correctly loads the immediate value."""
# ... test implementation
This approach allows you to validate all instructions systematically and ensures that each one works correctly:
- ✅ Load the immediate value into the correct register
- ✅ Consumes exactly 2 M-Cycles
- ✅ Advance PC in 2 bytes
Affected Files
src/core/cpp/CPU.cpp- The instructions LD r, d8 were already implemented (lines 364-419)tests/test_core_cpu_loads.py- Added parameterized testtest_ld_register_immediateto validate all LD r, d8 instructions
Tests and Verification
Tests were run to validate that all instructionsRH r, d8they work correctly:
Command Executed
pytest tests/test_core_cpu_loads.py::TestLD_8bit_Immediate -v
Result
============================= test session starts =============================
platform win32 - Python 3.13.5, pytest-9.0.2, pluggy-1.6.0
collecting...collected 9 items
tests/test_core_cpu_loads.py::TestLD_8bit_Immediate::test_ld_register_immediate[6-b-51] PASSED [ 11%]
tests/test_core_cpu_loads.py::TestLD_8bit_Immediate::test_ld_register_immediate[14-c-66] PASSED [ 22%]
tests/test_core_cpu_loads.py::TestLD_8bit_Immediate::test_ld_register_immediate[22-d-85] PASSED [ 33%]
tests/test_core_cpu_loads.py::TestLD_8bit_Immediate::test_ld_register_immediate[30-e-120] PASSED [ 44%]
tests/test_core_cpu_loads.py::TestLD_8bit_Immediate::test_ld_register_immediate[38-h-154] PASSED [ 55%]
tests/test_core_cpu_loads.py::TestLD_8bit_Immediate::test_ld_register_immediate[46-l-188] PASSED [ 66%]
tests/test_core_cpu_loads.py::TestLD_8bit_Immediate::test_ld_register_immediate[62-to-222] PASSED [ 77%]
tests/test_core_cpu_loads.py::TestLD_8bit_Immediate::test_ld_b_immediate PASSED [ 88%]
tests/test_core_cpu_loads.py::TestLD_8bit_Immediate::test_ld_hl_immediate PASSED [100%]
============================== 9 passed in 0.07s ==============================
Test Code
The parameterized test validates that each instruction:
@pytest.mark.parametrize("opcode,register_name,test_value", [
(0x06, 'b', 0x33), #LD B, d8
(0x0E, 'c', 0x42), # LD C, d8
(0x16, 'd', 0x55), #LD D, d8
(0x1E, 'e', 0x78), #LD E, d8
(0x26, 'h', 0x9A), #LD H, d8
(0x2E, 'l', 0xBC), # LD L, d8
(0x3E, 'a', 0xDE), # LD A, d8
])
def test_ld_register_immediate(self, opcode, register_name, test_value):
"""Validates that each LD r, d8 instruction works correctly."""
mmu = PyMMU()
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
regs.pc = 0x0100
mmu.write(0x0100, opcode)
mmu.write(0x0101, test_value)
cycles = cpu.step()
# Verify that the registry has the correct value
register_value = getattr(regs, register_name)
assert register_value == test_value
assert cycles == 2
assert regs.pc == 0x0102
C++ Compiled Module Validation: All tests pass, confirming that the instructions are correctly implemented in the compiled native C++ code.
Sources consulted
- Bread Docs: CPU Instruction Set- Load Instructions Section (LD r, n)
- Game Boy CPU Manual: Opcode reference and instruction timing
Educational Integrity
What I Understand Now
- Importance of LD r, d8: These instructions are the first ones any ROM executes upon startup. They are essential to initialize the state of the machine.
- Diagnosis of the problem: When the CPU encounters an unimplemented opcode and returns 0 cycles, the emulated machine time freezes. This explains why LY was stuck at 0: the PPU was not receiving cycles because the CPU was not executing instructions.
- Consistent timing: All LD r, d8 instructions consume exactly 2 M-Cycles (8 T-Cycles), which is important for cycle-to-cycle synchronization with the PPU.
- fetch_byte() as an abstraction: The method
fetch_byte()It encapsulates memory read and PC increment, simplifying the implementation of all instructions that read memory operands.
What remains to be confirmed
- Missing Opcodes: Although LD r, d8 is implemented, there are many other opcodes that ROMs need. More instructions need to be implemented so that the ROMs can fully execute.
- Execution Progression: With LD r, d8 implemented, the CPU should be able to execute more instructions before encountering an unimplemented opcode. This could allow LY to advance slightly.
Hypotheses and Assumptions
Hypothesis: With the LD r, d8 instructions implemented and validated, the CPU should be able to execute the first initialization steps of any ROM. However, you will probably encounter other unimplemented opcodes soon after, so it will be necessary to continue implementing more instructions incrementally.
Incremental strategy: The correct approach is to implement the most fundamental instructions first (such as LD r, d8) and then add more instructions as the opcodes that the ROMs need are identified.
Next Steps
- [ ] Run a ROM and analyze what opcodes are found after the first LD r, d8 instructions
- [ ] Implement the following most common instructions that ROMs need (probably more load, jump, or arithmetic instructions)
- [ ] Continue with an incremental approach: identify missing opcodes → implement → validate with tests → document
- [ ] Monitor progress: check if LY starts to move forward when more instructions are implemented