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
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): Use
alu_inc()to increase the value read from memory and update flags. Consumes 3 M-Cycles. - 0x35 - DEC (HL): Use
alu_dec()to decrement the value read from memory and update flags. Consumes 3 M-Cycles.
C++ implementation
Both opcodes follow the same pattern:
- Get the address pointed to by HL using
regs_->get_hl(). - Read current value from memory using
mmu_->read(addr). - Apply the operation (increment or decrement) using the existing ALU helpers (
alu_inc()eitheralu_dec()). - Write the result back to memory using
mmu_->write(addr, result). - 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 switchtests/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, the
defaultswitch 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:Yeah
L.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 beyond
0x0300and 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 reaches
0x0300 - [ ] Analyze the 100 instructions captured by the trace to identify which additional opcodes may be missing
- [ ] Continue implementing missing opcodes until ROM initialization completes