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

Native CPU: INC/DEC Implementation and Initialization Loop Arrangement

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

Summary

Complete family of instructions implementedINC randDEC r8-bit on the native CPU (C++). This was a critical bug that caused the game's initialization loops to fail, leading to corrupted memory reads and ultimatelySegmentation Faults. The specific problem was that the opcode0x05(DEC B) was not implemented, causing memory cleanup loops to not execute correctly. With this implementation, games can correctly initialize their RAM and continue with their boot sequence.

Hardware Concept

The instructionsINC(Increment) andDEC(Decrement) are fundamental arithmetic operations in the LR35902 CPU. They increment or decrement an 8-bit register by 1, and update the condition flags according to the result.

Flags Affected by INC/DEC

  • Z (Zero): Activated (1) if the result is 0, deactivated (0) otherwise.
  • N (Subtract): INC always sets N=0 (it is increment), DEC always sets N=1 (it is decrement).
  • H (Half-Carry/Half-Borrow):
    • INC: Activated if there is "half-carry" of the low nibble (bit 3 → bit 4). Example: 0x0F + 1 = 0x10 (there is half-carry).
    • DEC: Activated if there is "half-borrow" of the low nibble (bit 4 → bit 3). Example: 0x10 - 1 = 0x0F (there is half-borrow).
  • C (Carry): NOT MODIFIED. This is a critical quirk of the Game Boy hardware. Unlike ADD/SUB, INC/DEC preserve the Carry flag.

8-bit INC/DEC opcodes

The CPU has 14 opcodes to increment/decrement 8-bit registers:

  • INC: 0x04 (B), 0x0C (C), 0x14 (D), 0x1C (E), 0x24 (H), 0x2C (L), 0x3C (A)
  • DEC: 0x05 (B), 0x0D (C), 0x15 (D), 0x1D (E), 0x25 (H), 0x2D (L), 0x3D (A)

everyone consumes1 M-Cycle(4 T-Cycles).

The Initialization Loop Bug

Game Boy games typically run initialization loops that clear the RAM (WRAM) by filling it with zeros. A typical loop looks like this:

XOR A ; A = 0, Z = 1
LD B, 0x20 ; B = counter (32 iterations)
LD HL, 0xC000 ; HL = WRAM start address
loop:
    LD (HL), A ; Write 0 to memory
    INC HL ; Advance pointer
    DEC B ; Decrement counter
    JR NZ, loop ; Jump if B != 0 (Z == 0)

The problem: YeahDEC B(0x05) is not implemented, the CPU does not update the Z flag. The Z flag remains at the previous value (1, from XOR A), and when it arrivesJR NZ, the "Not Zero" condition fails because Z=1. The loop runszero times, the RAM becomes full of "garbage" (residual values), and later the game reads invalid addresses and crashes.

Fountain: Pan Docs - CPU Instruction Set, section "INC r" and "DEC r": "C flag is not affected"

Implementation

Two private ALU helpers were implemented in the CPU class to handle the INC and DEC flag logic, and then all the corresponding opcodes were added in the main switch.

1. ALU Helpers: alu_inc() and alu_dec()

Two private methods were created inCPU.hppand were implemented inCPU.cpp:

uint8_t CPU::alu_inc(uint8_t value) {
    uint8_t result = value + 1;
    
    // Flags
    regs_->set_flag_z(result == 0);
    regs_->set_flag_n(false);  // Always 0 (it is increment)
    regs_->set_flag_h((value & 0x0F) == 0x0F);  // Half-carry
    // C: NOT affected (preserved)
    
    return result;
}

uint8_t CPU::alu_dec(uint8_t value) {
    uint8_t result = value - 1;
    
    // Flags
    regs_->set_flag_z(result == 0);
    regs_->set_flag_n(true);  // Always 1 (it is decrement)
    regs_->set_flag_h((value & 0x0F) == 0x00);  // Half-borrow
    // C: NOT affected (preserved)
    
    return result;
}

2. Implementation of Opcodes

Added all 8-bit INC/DEC opcodes on the switchCPU::step():

// INC opcodes
case 0x04: regs_->b = alu_inc(regs_->b); cycles = 1; break; // INC B
case 0x0C: regs_->c = alu_inc(regs_->c); cycles = 1; break; // INC C
case 0x14: regs_->d = alu_inc(regs_->d); cycles = 1; break; // INC D
case 0x1C: regs_->e = alu_inc(regs_->e); cycles = 1; break; // INC E
case 0x24: regs_->h = alu_inc(regs_->h); cycles = 1; break; // INC H
case 0x2C: regs_->l = alu_inc(regs_->l); cycles = 1; break; // INC L
case 0x3C: regs_->a = alu_inc(regs_->a); cycles = 1; break; // INC A

// DEC opcodes
case 0x05: regs_->b = alu_dec(regs_->b); cycles = 1; break; // DEC B
case 0x0D: regs_->c = alu_dec(regs_->c); cycles = 1; break; // DEC C
case 0x15: regs_->d = alu_dec(regs_->d); cycles = 1; break; // DEC D
case 0x1D: regs_->e = alu_dec(regs_->e); cycles = 1; break; // DEC E
case 0x25: regs_->h = alu_dec(regs_->h); cycles = 1; break; // DEC H
case 0x2D: regs_->l = alu_dec(regs_->l); cycles = 1; break; // DEC L
case 0x3D: regs_->a = alu_dec(regs_->a); cycles = 1; break; // DEC TO

Components created/modified

  • src/core/cpp/CPU.hpp: Added methodsalu_inc()andalu_dec()in the private section
  • src/core/cpp/CPU.cpp: Implementation of ALU helpers and all 8-bit INC/DEC opcodes
  • tests/test_core_cpu_inc_dec.py: Complete suite of unit tests to verify functionality

Design decisions

  • Reusable ALU Helpers: Created helper methods instead of duplicating code in each opcode. This facilitates maintenance and ensures consistency in flag calculation.
  • Explicit preservation of the C flag: Although the C flag is not modified, it was explicitly documented in the code to avoid future confusion. This is a critical hardware quirk.
  • Complete implementation: Implemented all 8-bit INC/DEC opcodes at once, not just DEC B. This prevents similar bugs in other loops of the game code.

Affected Files

  • src/core/cpp/CPU.hpp- Added methodsalu_inc()andalu_dec()
  • src/core/cpp/CPU.cpp- Implementation of ALU helpers and all 8-bit INC/DEC opcodes
  • tests/test_core_cpu_inc_dec.py- Complete unit test suite (new file)

Tests and Verification

Created a complete suite of unit tests intests/test_core_cpu_inc_dec.pywhich verifies:

  • Preservation of the C flag: Specific test for DEC B (0x05) that verifies that the Carry flag is preserved both when it is active and when it is deactivated
  • Half-Carry/Half-Borrow: Tests that verify correct detection of low nibble overflow
  • Flags Z and N: Verification that the flags are updated correctly according to the result
  • All opcodes: Tests that verify that all INC/DEC opcodes work correctly

Command executed:

pytest tests/test_core_cpu_inc_dec.py -v

Expected result: All tests pass (6 tests in total).

Critical Test: DEC B Preserve Carry

This is the most important test, since it verifies the behavior of the opcode that caused the crash:

def test_dec_b_preserves_carry(self):
    """Verify that DEC B (0x05) preserves the Carry flag."""
    mmu = PyMMU()
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    
    regs.pc = 0x8000
    regs.b = 0x01
    regs.set_flag_c(True) # Enable Carry
    
    mmu.write(0x8000, 0x05) # DEC B
    cpu.step()
    
    assert regs.b == 0x00
    assert regs.get_flag_z() == 1
    assert regs.get_flag_c() == 1, "C must have been preserved"

Native Validation: All tests validate the compiled C++ module through the Cython interface.

Sources consulted

  • Bread Docs: CPU Instruction Set, section "INC r" and "DEC r" - Flag specification and behavior of the Carry flag
  • Bread Docs: CPU Instruction Set, "Half-Carry Flag" section - Explanation of half-carry and half-borrow calculation

Educational Integrity

What I Understand Now

  • Preservation of the C flag in INC/DEC: Unlike ADD/SUB, the INC and DEC instructions DO NOT modify the Carry flag. This is a quirk of the LR35902 hardware that must be strictly adhered to for the loops to work properly.
  • Half-Carry vs Half-Borrow: INC detects "half-carry" when the low nibble is 0x0F and adding 1 causes overflow. DEC detects "half-borrow" when the low nibble is 0x00 and subtracting 1 produces underflow.
  • Importance of initialization loops: Games critically depend on RAM being clean (full of zeros) at startup. If the cleanup loops fail, the game reads corrupted data and crashes.

What remains to be confirmed

  • INC (HL) and DEC (HL): These variants that operate on memory (not registers) are not yet implemented. They consume 3 M-Cycles instead of 1.
  • Behavior in real loops: Verify that games now correctly execute their initialization loops without crashes.

Hypotheses and Assumptions

The implementation strictly follows the Pan Docs documentation. There are no unsupported assumptions.

Next Steps

  • [ ] Implement INC (HL) and DEC (HL) (opcodes that operate on memory)
  • [ ] Verify that games now run their initialization loops correctly
  • [ ] Continue to implement more CPU opcodes to increase compatibility