Step 0421: Fix Test Mode ROM Writes in Unit Tests
📋 Executive Summary
Systematic correction of10 unit errorsin CPU tests caused by the lack of activation of thetest_mode_allow_rom_writesimplemented in Step 0419. The tests were writing instructions to ROM (0x0000-0x7FFF) without activating test mode, which caused the MMU to interpret the writes as MBC commands instead of writing directly to memory. This resulted in the CPU executing incorrect values (0x00 or residuals) instead of the test instructions.
Solution:Addmmu.set_test_mode_allow_rom_writes(True)systematically after each instance ofPyMMU()in unit test files, including special cases within loops.
Impact:Complete elimination of the 10 original bugs and preventive correction in 55 total CPU tests.
🔧 Technical Concept: Test Mode and ROM Writes
Context of the Problem
On real Game Boy hardware, the ROM (0x0000-0x7FFF) is read-only. The cartridges use Memory Bank Controllers (MBC) that interpret writes to these addresses as control commands (ROM/RAM bank change, RAM enable, etc.) rather than modifying the memory.
Implementation in the Emulator
The Viboy Color MMU faithfully replicates this behavior:
// src/core/cpp/MMU.cpp (line ~935)
if (test_mode_allow_rom_writes_ && addr< 0x8000) {
// Escribir directamente en rom_data_
rom_data_[rom_offset] = value;
return; // NO procesar como MBC
}
// ... código MBC normal ...
Need for Test Mode
- Without test mode:
mmu.write(0x0100, 0xC3)→ Interpreted as MBC command "RAM-ENABLE" - With test mode:
mmu.write(0x0100, 0xC3)→ Write JP nn opcode directly to ROM
This mechanism allows unit tests to write synthetic programs to ROM without loading an actual .gb file, maintaining the emulator's fidelity to the original hardware during normal execution.
Fountain:Pan Docs - "Memory Bank Controllers", standard ROM write behavior.
🔍 Diagnosis (T1)
Original Faults Detected (10 total)
pytest -q > /tmp/viboy_0421_before.log 2>&1
tail -n 80 /tmp/viboy_0421_before.log
tests/test_core_cpu_io.py (3 failures):
test_ldh_write_lcdc: expected 3 cycles, got 1test_ldh_read_hram: assertion error on read valuetest_ldh_offset_wraparound: assertion error writing to 0xFFFF
tests/test_core_cpu_jumps.py (3 failures):
test_jp_absolute: PC should be 0xC000, it is 0x0101test_jp_absolute_wraparound: PC should be 0xFFFF, it is 0x0101test_jr_relative_positive: PC should be 0x0107, it is 0x0101
tests/test_core_cpu_interrupts.py (4 failures):
test_halt_wakeup_on_interrupt: cpu.halted should be True, it is Falsetest_interrupt_dispatch_vblank: assertion error on PC vectortest_interrupt_priority: assertion error in prioritytest_all_interrupt_vectors: IME should be 1, it is 0
Root Cause Evidence
When inspectingtest_core_cpu_io.py:70-89, it was observed that the test writes directly to ROM without activating test mode:
mmu = PyMMU()
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
regs.pc = 0x0100
mmu.write(0x0100, 0xE0) # ❌ Interpreted as MBC, does not write
mmu.write(0x0101, 0x40) # ❌ Interpreted as MBC, does not write
Confirmatory search showed thattest_core_cpu_alu.py(which passes correctly) YES useset_test_mode_allow_rom_writes(True).
🛠️ Implementation (T3)
Fix Applied
Systematic replacement of the initialization pattern in all test files:
mmu = PyMMU()
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
After:
mmu = PyMMU()
mmu.set_test_mode_allow_rom_writes(True) # Step 0421: Allow ROM writes for testing
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
Modified Files
- tests/test_core_cpu_io.py(5 tests):
- test_ldh_write, test_ldh_read, test_ldh_write_lcdc
- test_ldh_read_hram, test_ldh_offset_wraparound
- tests/test_core_cpu_jumps.py(14 tests):
- All JP/JR tests (absolute, relative, conditional)
- Applied with
replace_all=True
- tests/test_core_cpu_interrupts.py(8 tests):
- DI/EI, HALT, interrupt dispatch tests
- Special case:test_all_interrupt_vectors with internal loop
- tests/test_core_cpu_loads.py(24 tests):
- LD 8-bit tests (register, immediate, memory)
- LD 16-bit tests, ADD HL, stack pointers
- Special case:test_ld_block_matrix with internal loop
- tests/test_core_cpu_stack.py(4 tests):
- PUSH/POP BC tests
- Basic and nested CALL/RET tests
Special Cases in Loops
Two tests required special treatment for creating MMU instances within loops:
test_core_cpu_interrupts.py:366→ Loop over interrupt_configstest_core_cpu_loads.py:145→ Loop over test_cases
In both cases, addedmmu.set_test_mode_allow_rom_writes(True)immediately aftermmu = PyMMU()inside the loop.
✅ Tests and Verification (T4)
Compilation
python3 setup.py build_ext --inplace
BUILD_EXIT=0 ✅
python3 test_build.py
TEST_BUILD_EXIT=0 ✅
Specific CPU Tests
pytest -q tests/test_core_cpu_io.py
IO_EXIT=0 ✅
5 passed in 0.36s
test_core_cpu_jumps.py:
pytest -q tests/test_core_cpu_jumps.py
JUMPS_EXIT=0 ✅
14 passed in 0.37s
test_core_cpu_interrupts.py:
pytest -q tests/test_core_cpu_interrupts.py
INTS_EXIT=0 ✅
8 passed in 0.38s (after correcting special loop case)
Complete CPU Tests
pytest -q tests/test_core_cpu_io.py \
tests/test_core_cpu_jumps.py \
tests/test_core_cpu_interrupts.py \
tests/test_core_cpu_loads.py \
tests/test_core_cpu_stack.py
CPU_TESTS_EXIT=0 ✅
55 passed in 0.58s
Bottom line
| Test File | Before | After | Total Tests |
|---|---|---|---|
| test_core_cpu_io.py | 3 FAILED | ✅ 5 PASSED | 5 |
| test_core_cpu_jumps.py | 3 FAILED | ✅ 14 PASSED | 14 |
| test_core_cpu_interrupts.py | 4 FAILED | ✅ 8 PASSED | 8 |
| test_core_cpu_loads.py | ✅ (preventive) | ✅ 24 PASSED | 24 |
| test_core_cpu_stack.py | ✅ (preventive) | ✅ 4 PASSED | 4 |
| TOTAL | 10 FAILED | ✅ 55 PASSED | 55 |
C++ Compiled Module Validation
✅ The tests execute real instructions compiled in C++ (not Python code).
✅ Test mode allows you to write synthetic programs in ROM without compromising the fidelity of the emulator.
📦 Git Commands
cd "$(git rev-parse --show-toplevel)"
git add tests/test_core_cpu_io.py \
tests/test_core_cpu_jumps.py \
tests/test_core_cpu_interrupts.py \
tests/test_core_cpu_loads.py \
tests/test_core_cpu_stack.py \
docs/bitacora/entries/2026-01-02__0421__fix-test-mode-rom-writes-in-unit-tests.html \
docs/bitacora/index.html \
docs/report_phase_2/part_01_steps_0412_0450.md
git commit -m "fix(tests): activate test_mode_allow_rom_writes in all CPU unit tests (Step 0421)"
git push
📚 Lessons Learned
- Consistency in tests:When a mechanism like test_mode is introduced, all existing and future tests must use it.
- Systematic refactor:The search/replace pattern with
replace_all=TrueIt is effective, but requires manual verification of special cases (loops, indentations). - Test of tests:Comparing files that pass (test_core_cpu_alu.py) vs those that fail (test_core_cpu_io.py) quickly revealed the root cause.
- Hardware Fidelity:The test mode demonstrates that it is possible to maintain fidelity to the real hardware (ROM read-only) while allowing synthetic testing.
🚀 Next Steps
- Step 0422+: Investigate the 5 bugs in
test_core_joypad.py(different problem, not related to ROM writes) - Add a linter or pre-commit hook that detects
PyMMU()withoutset_test_mode_allow_rom_writes(True)in test files - Document the test mode pattern in
CONTRIBUTING.mdfor future collaborators