⚠️ Clean-Room / Educational

This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation.

Step 0423: Mass migration of CPU tests to WRAM and minimization of ROM-writes

← Return to index

Executive Summary

Massive and successful migration of 49 CPU tests from ROM (withset_test_mode_allow_rom_writes) to WRAM usingload_program()and fixturemmustandard. Technical debt reduction 98%: from 50 ROM-write hits to just 1 (legitimate fixturemmu_romwin conftest.py).

49/49
Migrated tests (100%)
98%
Reduction ROM-writes
118
Tests passing ✅
10
Known bugs (joypad/MMU)

Aim

Eliminate unnecessary use ofmmu_romw(ROM-writes) in CPU tests, executing code from WRAM (0xC000) usingload_program(). Stop ROM-writesonlywhere it is essential (interrupt vectors 0x0040-0x0060, which in this case was not needed either).

Initial State

After Step 0422:

  • 49 hitsofset_test_mode_allow_rom_writes(True)in 5 CPU test files
  • Tests running from ROM (0x0100, 0x0200, etc.) with ROM-writes enabled
  • fixturemmustandard (no ROM-writes) andload_program()available
  • Suite:118 passed, 10 failed(known joypad/MMU bugs)

Implementation

Migrated Files (49 tests total)

1. test_core_cpu_loads.py (18 tests)

Load operations (LD) and 16-bit arithmetic. All migrated tests to useload_program()and fixturemmu.

# BEFORE (with ROM-writes)
mmu = PyMMU()
mmu.set_test_mode_allow_rom_writes(True)
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
regs.pc = 0x0100
mmu.write(0x0100, 0x47) # LD B, A
cycles = cpu.step()

# AFTER (from WRAM, no ROM-writes)
def test_ld_b_a(self, mmu): # Fixture mmu injected
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    regs.a = 0x10
    load_program(mmu, regs, [0x47]) # Load into WRAM (0xC000)
    cycles = cpu.step()
    assert regs.b == 0x10

2. test_core_cpu_jumps.py (14 tests)

Jump instructions (JP, JR, conditionals). Expected address settings toTEST_EXEC_BASE + offset.

# Example: Positive relative JR
def test_jr_relative_positive(self, mmu):
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    load_program(mmu, regs, [0x18, 0x05]) # JR +5
    cycles = cpu.step()
    expected_pc = TEST_EXEC_BASE + 7 # Base + 2 (instruction) + 5 (offset)
    assert regs.pc == expected_pc

3. test_core_cpu_io.py (5 tests)

LDH (High Memory I/O) instructions. Direct migration without changes in I/O addresses.

# Example: LDH (n), A
def test_ldh_write(self, mmu):
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    regs.a = 0xAB
    load_program(mmu, regs, [0xE0, 0x40]) # LDH (n), A with offset 0x40
    cycles = cpu.step()
    assert mmu.read(0xFF40) == 0xAB # LCDC register

4. test_core_cpu_stack.py (4 tests)

Stack operations (PUSH/POP/CALL/RET). CALL uses WRAM addresses (0xC100+) as targets.

# Example: CALL/RET
def test_call_ret_basic(self, mmu):
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    regs.sp = 0xFFFE
    call_target = 0xC100 # Target on high WRAM
    mmu.write(call_target, 0xC9) # RET on target
    lsb, msb = call_target & 0xFF, (call_target >> 8) & 0xFF
    load_program(mmu, regs, [0xCD, lsb, msb]) # CALL
    cycles = cpu.step() # CALL
    assert regs.pc == call_target
    cycles = cpu.step() # RET
    assert regs.pc == TEST_EXEC_BASE + 3 # Return after CALL

5. test_core_cpu_interrupts.py (8 tests)

DI/EI/HALT and interrupt dispatcher. Dispatch tests DO NOT write ISR vectors, they only verify jumps.

# Example: EI with delayed activation
def test_ei_delayed_activation(self, mmu):
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    load_program(mmu, regs, [0xFB, 0x00]) # EI, NOP
    cpu.step() # EI
    assert cpu.get_ime() == 0 # Not yet active (delayed)
    cpu.step() # NOP
    assert cpu.get_ime() == 1 # Now active

# Example: Interrupt dispatch (without writing vectors)
def test_interrupt_dispatch_vblank(self, mmu):
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    regs.sp = 0xFFFE
    load_program(mmu, regs, [0xFB, 0x00, 0x00]) # EI, NOP, NOP
    cpu.step(); cpu.step() # Activate IME
    mmu.write(0xFF0F, 0x01) # Enable V-Blank in IF
    mmu.write(0xFFFF, 0x01) # Enable V-Blank in IE
    cycles = cpu.step() # Process interrupt
    assert regs.pc == 0x0040 # Jump to V-Blank vector (ROM, but we don't write there)

Migration Pattern

  1. Replacemmu = PyMMU()by fixturedef test_xxx(self, mmu):
  2. Eliminatemmu.set_test_mode_allow_rom_writes(True)
  3. Replace direct writes to ROM withload_program(mmu, regs, [opcodes])
  4. Adjust PC expectations:0x0100 → TEST_EXEC_BASE, 0x0102 → TEST_EXEC_BASE + 2
  5. In indirect writes (HL), use writable WRAM/HRAM addresses (e.g. 0xC100 instead of 0x0000)

Tests and Verification

ROM-write audit

$ grep -rn "set_test_mode_allow_rom_writes(True)" tests/ | wc -l

# BEFORE (Step 0422):
50 #49 tests + 1 conftest (legitimate fixture)

# AFTER (Step 0423):
1 # Only conftest.py (fixture mmu_romw)

# Reduction: 49 hits eliminated (98%)

Compilation and Tests

$python3 setup.py build_ext --inplace
✅ BUILD_EXIT=0

$python3 test_build.py
✅ TEST_BUILD_EXIT=0

$pytest -q
======================== 10 failed, 118 passed in 0.48s ========================
✅ PYTEST_EXIT=1 (expected for 10 known bugs)

# Bugs (ONLY known ones, NOT new ones):
- 8 joypad tests (test_core_joypad.py) ← Pending Step 0424
- 2 MMU tests (test_core_mmu.py) ← Pending Step 0424

# Tests migrated successfully:
- test_core_cpu_loads.py: 18/18 ✅
- test_core_cpu_jumps.py: 14/14 ✅
- test_core_cpu_io.py: 5/5 ✅
- test_core_cpu_stack.py: 4/4 ✅
- test_core_cpu_interrupts.py: 8/8 ✅

Total: 49/49 migrated tests (100%)

Native Module Validation

✅ All migrated tests run compiled C++ code from WRAM using Cython.

✅ MMU in standard mode (ROM read-only) working correctly.

load_program()verified helper in 49 tests.

Technical Concept: Running from WRAM vs ROM

Game Boy Memory Map

0x0000-0x7FFF: ROM (Read Only) ← Cannot write to real hardware
0x8000-0x9FFF: VRAM
0xA000-0xBFFF: External RAM (Cartridge)
0xC000-0xDFFF: WRAM (Work RAM) ← Yes writable
0xE000-0xFDFF: Echo RAM (WRAM mirror)
0xFE00-0xFE9F: OAM
0xFF00-0xFF7F: I/O Registers
0xFF80-0xFFFE: HRAM
0xFFFF: IE Register

Why WRAM is the right place for testing

  • Realism: On real hardware, you can't write to ROM. The tests must reflect this.
  • Flexibility: WRAM (8KB, 0xC000-0xDFFF) is enough for any test program.
  • Cleaning: Avoid the hacktest_mode_allow_rom_writeswhich only exists for tests.
  • Security: Production code (src/) should never assume that ROM is writable.

Cases where ROM-writes YES would be necessary

In this Step 0423, no test required them. But legitimate cases would include:

  • interruption vectors: Write ISR code at 0x0040 (V-Blank), 0x0048 (LCD STAT), etc.
  • MBC tests: Validate that MBC commands (0x0000-0x7FFF) DO NOT modify ROM.

For these cases, there is the fixturemmu_romwin conftest.py (the only remaining hit).

Lessons Learned

  1. Incremental migration is key: Validate each file before moving to the next reduces the risk of introducing errors.
  2. fixturemmucentralize logic: Future changes (ex: add logging) They are automatically applied to all tests.
  3. Helperload_program()it is robust: Used in 49 tests without failures, including edge cases (nested CALL, negative JR, interrupt dispatch).
  4. Interrupt tests DO NOT require ROM-writes: Validate the jump to vectors (0x0040-0x0060) does not require writing code in those vectors, just verifying that PC jumps correctly.
  5. Technical debt proactively eliminated: Mass migration avoids accumulating more tests with ROM-writes in the future.

Modified Files

  • tests/test_core_cpu_loads.py- 18 migrated tests
  • tests/test_core_cpu_jumps.py- 14 migrated tests
  • tests/test_core_cpu_io.py- 5 migrated tests
  • tests/test_core_cpu_stack.py- 4 migrated tests
  • tests/test_core_cpu_interrupts.py- 8 migrated tests
  • docs/bitacora/entries/2026-01-02__0423__*.html- This entry (new)
  • docs/bitacora/index.html- Updated with Step 0423
  • docs/report_phase_2/part_01_steps_0412_0450.md- Updated with Step 0423

Scope:Onlytests/anddocs/. No changes insrc/(guardrail fulfilled).

Conclusion

Step 0423 successfully completes mass migration of CPU tests to WRAM, reducing technical debt 98% ROM-writes. With 49 migrated tests and 0 new bugs introduced, the test harness is now cleaner, more realistic and maintainable. The remaining 10 faults (joypad/MMU) areacquaintancesandunrelatedwith this migration, pending for Step 0424.

✅ Step 0423 completed successfully
  • 49 migrated tests (100%)
  • 98% reduction in ROM-writes
  • 118 tests passed
  • 0 new bugs introduced
  • Solid base for Step 0424 (fix joypad/MMU)

Next Steps

Step 0424(Proposed): Fix 10 remaining joypad/MMU bugs

  • Investigate and fix 8 bugs in test_core_joypad.py
  • Investigate and fix 2 bugs in test_core_mmu.py
  • Objective: Complete suite at 128/128 tests passing (100%)