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

Complete 8-bit INC/DEC (All Variants)

Date:2025-12-17 StepID:0027 State: Verified

Summary

All variants of8-bit INC/DECthat were missing from the CPU. In step 9 INC/DEC had been implemented for B, C and A, but D, E, H, L and the version were left out. in memory (HL). The emulator crashed at opcode 0x1D (DEC E) when running Tetris DX, which confirmed that these critical instructions for handling loop counters were missing. With this implementation, 8-bit unary arithmetic is complete and the emulator can advance beyond initialization in real games.

Hardware Concept

The instructionsINC(Increment) andDEC(Decrement) are operations unary arithmetic that increments or decrements a value by 1. On the Game Boy, these instructions They are available for all 8-bit registers (A, B, C, D, E, H, L) and for indirect memory (HL).

Opcode Table Pattern:

  • Column x4:INC (B, D, H, (HL))
  • Column x5:DEC (B, D, H, (HL))
  • Column xC:INC (C, E, L, A)
  • xD column:DEC (C, E, L, A)

Flags Behavior:

  • Z (Zero):Activated if the result is 0
  • N (Subtract):Always 1 in DEC, always 0 in INC
  • H (Half-Carry/Half-Borrow):It is activated when there is carry/borrow from bit 3 to 4 (low nibble)
  • C (Carry): DO NOT TOUCH- This is a critical quirk of the LR35902 hardware

INC/DEC (HL) - Read-Modify-Write Operations:

The INC (HL) and DEC (HL) instructions are special because they operate on indirect memory. These instructions perform aRead-Modify-Write:

  1. They read the memory value at the address pointed to by HL
  2. They modify the value (increase or decrease)
  3. They write the new value back to memory

For this reason, these instructions consume3 M-Cycles (12 T-Cycles)instead of 1: one for reading, one for writing, and one for internal operation.

Fountain:Pan Docs - CPU Instruction Set (INC/DEC instructions, Flags behavior)

Implementation

Implemented the following missing opcodes, reusing the helper methods_inc_nand_dec_nthat already existed from step 9:

Implemented Opcodes

  • 0x14:INC D - Increase the D register
  • 0x15:DEC D - Decrements the D register
  • 0x1C:INC E - Increase the E register
  • 0x1D:DEC E - Decrease the E register (The culprit of the crash!)
  • 0x24:INC H - Increases the H register
  • 0x25:DEC H - Decrements the H register
  • 0x2C:INC L - Increments the L register
  • 0x2D:DEC L - Decrements the L register
  • 0x34:INC (HL) - Increments the value in memory pointed to by HL
  • 0x35:DEC (HL) - Decrements the value in memory pointed to by HL

Created/Modified Components

  • src/cpu/core.py:
    • Added 10 new methods:_op_inc_d, _op_dec_d, _op_inc_e, _op_dec_e, _op_inc_h, _op_dec_h, _op_inc_l, _op_dec_l, _op_inc_hl_ptr, _op_dec_hl_ptr
    • Updated the opcode table to register the new handlers
    • Methods for individual records follow the same pattern as B, C and A (1 M-Cycle)
    • The methods for (HL) implement Read-Modify-Write (3 M-Cycles)
  • tests/test_cpu_inc_dec_full.py:
    • New test file with 10 complete tests
    • Tests for all registers (D, E, H, L)
    • Tests for indirect memory (HL)
    • Tests for preservation of the C flag
    • Tests for wrap-around and flag activation

Design Decisions

Reuse of Helpers:It was decided to reuse the methods_inc_nand_dec_nexisting instead of duplicating logic. This ensures consistency in flag behavior and facilitates maintenance.

Read-Modify-Write for (HL):The INC (HL) and DEC (HL) instructions explicitly implement the Read-Modify-Write sequence usingmmu.read_byte()andmmu.write_byte(). This is important because on real hardware, these operations require multiple memory accesses.

Consistent Logging:All methods include DEBUG logging with the same format as the existing instructions, making debugging easier.

Affected Files

  • src/cpu/core.py- Added 10 new opcode methods and updated the opcode table
  • tests/test_cpu_inc_dec_full.py- New file with complete test suite (10 tests)

Tests and Verification

A complete TDD test suite was created with 10 tests that validate all implemented variants:

Unit Tests (pytest)

Command executed: python3 -m pytest tests/test_cpu_inc_dec_full.py -v

Around:macOS, Python 3.9.6

Result: 10/10 PASSED testsin 0.59 seconds

What is valid:

  • test_inc_dec_e:Verify that DEC E works correctly and affects flags Z, N, H (but NOT C). This test is critical because DEC E (0x1D) is the opcode that caused the crash in Tetris when it was not implemented. Includes wrap-around cases (0x00 -> 0xFF) and flag activation.
  • test_inc_dec_d, test_inc_dec_h, test_inc_dec_l:They verify that INC/DEC work correctly for all remaining records.
  • test_inc_hl_memory:Verifies that INC (HL) correctly performs the Read-Modify-Write operation. It puts 0x0F in (HL), executes INC (HL), and verifies that the memory has 0x10 and Flag H=1 (half-carry).
  • test_dec_hl_memory:Similar to test_inc_hl_memory but for DEC (HL).
  • test_inc_hl_memory_zero_flag, test_dec_hl_memory_zero_flag:They verify that the operations in memory they correctly activate the Z flag when the result is 0.
  • test_inc_preserves_carry, test_dec_preserves_carry:They verify that the C flag is NOT modified during INC/DEC operations, which is a critical feature of the LR35902 hardware.

Critical Test Code (test_inc_dec_e)

def test_inc_dec_e(self, cpu: CPU) -> None:
    """
    Verify that DEC E works correctly and affects flags Z, N, H.
    
    This test is critical because DEC E (0x1D) is the opcode that caused
    the crash in Tetris when it was not implemented.
    """
    # Test 1: DEC E from non-zero value
    cpu.registers.set_e(0x05)
    cpu.registers.set_f(0x00) # Clear all flags
    cycles = cpu._op_dec_e()
    
    assert cycles == 1
    assert cpu.registers.get_e() == 0x04
    assert not cpu.registers.get_flag_z() # Not zero
    assert cpu.registers.get_flag_n() # It is a subtraction
    assert not cpu.registers.get_flag_h() # No half-borrow
    assert not cpu.registers.get_flag_c() # C is not touched
    
    # Test 2: DEC E from 0x01 (must give 0x00 and activate Z)
    cpu.registers.set_e(0x01)
    cpu.registers.set_f(0x00)
    cycles = cpu._op_dec_e()
    
    assert cycles == 1
    assert cpu.registers.get_e() == 0x00
    assert cpu.registers.get_flag_z() # Is zero
    assert cpu.registers.get_flag_n() # It is a subtraction
    assert not cpu.registers.get_flag_h() # No half-borrow
    assert not cpu.registers.get_flag_c() # C is not touched
    
    # Test 3: DEC E from 0x00 (wrap-around to 0xFF)
    cpu.registers.set_e(0x00)
    cpu.registers.set_f(0x00)
    cycles = cpu._op_dec_e()
    
    assert cycles == 1
    assert cpu.registers.get_e() == 0xFF
    assert not cpu.registers.get_flag_z() # Not zero
    assert cpu.registers.get_flag_n() # It is a subtraction
    assert cpu.registers.get_flag_h() # There is half-borrow (0x0 -> 0xF)
    assert not cpu.registers.get_flag_c() # C is not touched
    
    # Test 4: INC E from 0x0F (must activate H)
    cpu.registers.set_e(0x0F)
    cpu.registers.set_f(0x00)
    cycles = cpu._op_inc_e()
    
    assert cycles == 1
    assert cpu.registers.get_e() == 0x10
    assert not cpu.registers.get_flag_z() # Not zero
    assert not cpu.registers.get_flag_n() # It is a sum
    assert cpu.registers.get_flag_h() # There is half-carry (0xF -> 0x10)
    assert not cpu.registers.get_flag_c() # C is not touched

Why this test demonstrates the behavior of the hardware:

  • Verify that DEC E correctly decrements the register value
  • Confirms that the Z flag is activated when the result is 0
  • Confirm that the N flag is always activated in DEC (it is a subtraction)
  • Confirms that the H flag is activated when there is half-borrow (0x0 -> 0xF)
  • Critical:Confirm that the C flag is NOT modified, which is a quirk of the LR35902 hardware
  • Verify correct wrap-around (0x00 -> 0xFF)

Indirect Memory Tests (test_inc_hl_memory)

def test_inc_hl_memory(self, cpu: CPU) -> None:
    """
    Check INC (HL) with Read-Modify-Write operation.
    
    Put 0x0F in (HL), run INC (HL), and verify that:
    - Memory has 0x10
    - Flag H=1 (half-carry)
    """
    # Configure HL to point to a RAM address
    hl_addr = 0xC000 # Game Boy RAM address
    cpu.registers.set_hl(hl_addr)
    
    # Write 0x0F to memory
    cpu.mmu.write_byte(hl_addr, 0x0F)
    
    # Clear flags
    cpu.registers.set_f(0x00)
    
    # Run INC (HL)
    cycles = cpu._op_inc_hl_ptr()
    
    # Check cycles (3 M-Cycles for Read-Modify-Write)
    assert cycles == 3
    
    # Verify that memory was updated correctly
    assert cpu.mmu.read_byte(hl_addr) == 0x10
    
    # Check flags
    assert not cpu.registers.get_flag_z() # 0x10 is not zero
    assert not cpu.registers.get_flag_n() # It is a sum
    assert cpu.registers.get_flag_h() # There is half-carry (0xF -> 0x10)
    assert not cpu.registers.get_flag_c() # C is not touched

Why this test demonstrates the behavior of the hardware:

  • Verifies that INC (HL) correctly performs the Read-Modify-Write operation (read, modify, write)
  • Confirms that it consumes 3 M-Cycles (12 T-Cycles) instead of 1, as required by the hardware
  • Verify that memory is updated correctly
  • Confirms that the flags are updated correctly during the in-memory operation

Validation with Real ROM (Tetris DX)

ROM:Tetris DX (user-contributed ROM, not distributed)

Execution mode:Headless, 10,000 instruction limit, INFO logging enabled

Success Criterion:The emulator should run without crashing at the 0x1D (DEC E) opcode that previously caused the error. The E register should change correctly during execution, confirming that DEC E works.

Observation:

  • ✅ The emulator ran10,000 error-free instructions
  • ✅ There was no crash at 0x1D (DEC E) - the problem was completely resolved
  • ✅ The E register changed correctly during execution (0x00 → 0xC9 → 0xBB → ... → 0x43), confirming that DEC E works
  • ✅ The PC is in a loop between 0x1383-0x1389, which is normal for a game waiting for V-Blank
  • ✅ LY (Line Y) correctly increased to 125 lines, confirming that the PPU timing works
  • ✅ Record A also changed correctly, confirming that other operations work

Relevant logs (displayed every 100 instructions):

Instruction 100: PC=0x1384, A=0xCF, E=0xC9, LY=1
Instruction 200: PC=0x1386, A=0xBF, E=0xBB, LY=2
Instruction 300: PC=0x1388, A=0x06, E=0xAC, LY=3
Instruction 400: PC=0x1383, A=0x9E, E=0x9E, LY=5
...
Instruction 1300: PC=0x1387, A=0x1E, E=0x1D, LY=16
...
Instruction 10000: PC=0x1386, A=0x43, E=0x43, LY=125

✅ 10,000 instructions executed without errors
   end PC = 0x1386
   A = 0x43
   E = 0x43
   LY = 125

Result: verified- The crash at 0x1D (DEC E) has been completely resolved. The emulator can now run Tetris DX beyond initialization, reaching a V-Blank wait loop which is normal game behavior.

Legal notes:The Tetris DX ROM is provided by the user for local testing. It is not distributed, it is not included in the repository, and no downloads are linked.

Sources consulted

Educational Integrity

What I Understand Now

  • Opcode Pattern:The INC/DEC instructions follow a clear pattern in the opcode table: x4/x5 columns for B/D/H/(HL) and xC/xD columns for C/E/L/A. This pattern makes memorization easier and systematic implementation.
  • Preservation of Flag C:A critical feature of the LR35902 hardware is that INC/DEC 8 bits DO NOT modify the C (Carry) flag. This is different from many other architectures and is important for the conditional logic of games.
  • Read-Modify-Write:Operations in indirect memory (HL) require multiple memory accesses, which is reflected in the consumption of 3 M-Cycles instead of 1.
  • Half-Carry/Half-Borrow:The H flag is activated when there is carry/borrow from bit 3 to 4 (low nibble), which is useful for BCD (Binary Coded Decimal) operations although it is not used much on the Game Boy.

What remains to be confirmed

  • Exact Timing:For now, we assume that INC/DEC (HL) consumes exactly 3 M-Cycles according to the documentation. If in the future there are timing problems with real games, we might need verify exact timing with real hardware tests.

Hypotheses and Assumptions

Assumption 1:We assume that the behavior of flags in INC/DEC is identical for all registers and for indirect memory. This is supported by documentation, but we have not Verified with real hardware.

Assumption 2: RESOLVED- Tested with Tetris DX after this implementation and it was confirmed that the crash in 0x1D has been completely resolved. The emulator executed 10,000 instructions without errors, reaching a V-Blank waiting loop which is normal game behavior.

Next Steps

  • [x] Run Tetris DX to verify that the crash at 0x1D (DEC E) has been resolved -FILLED
  • [ ] Continue with the implementation of missing instructions according to the execution logs
  • [ ] Analyze the loop at 0x1383-0x1389 to understand what the game is waiting for
  • [ ] If background rendering has already been implemented, verify that Tetris DX displays the title screen