⚠️ Clean-Room / Educational

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)

Date:2025-12-19 StepID:0125 State: Verified

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 methodfetch_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