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
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).
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 hitsof
set_test_mode_allow_rom_writes(True)in 5 CPU test files - Tests running from ROM (0x0100, 0x0200, etc.) with ROM-writes enabled
- fixture
mmustandard (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
- Replace
mmu = PyMMU()by fixturedef test_xxx(self, mmu): - Eliminate
mmu.set_test_mode_allow_rom_writes(True) - Replace direct writes to ROM with
load_program(mmu, regs, [opcodes]) - Adjust PC expectations:
0x0100 → TEST_EXEC_BASE,0x0102 → TEST_EXEC_BASE + 2 - 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 hack
test_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
- Incremental migration is key: Validate each file before moving to the next reduces the risk of introducing errors.
-
fixture
mmucentralize logic: Future changes (ex: add logging) They are automatically applied to all tests. -
Helper
load_program()it is robust: Used in 49 tests without failures, including edge cases (nested CALL, negative JR, interrupt dispatch). - 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.
- 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 teststests/test_core_cpu_jumps.py- 14 migrated teststests/test_core_cpu_io.py- 5 migrated teststests/test_core_cpu_stack.py- 4 migrated teststests/test_core_cpu_interrupts.py- 8 migrated testsdocs/bitacora/entries/2026-01-02__0423__*.html- This entry (new)docs/bitacora/index.html- Updated with Step 0423docs/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.
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%)