Step 0421: Fix Test Mode ROM Writes in Unit Tests

VERIFIED FIX TESTS
Date:2026-01-02 |StepID:0421 |Affected files:5 test files

📋 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)

Command:
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):

  1. test_ldh_write_lcdc: expected 3 cycles, got 1
  2. test_ldh_read_hram: assertion error on read value
  3. test_ldh_offset_wraparound: assertion error writing to 0xFFFF

tests/test_core_cpu_jumps.py (3 failures):

  1. test_jp_absolute: PC should be 0xC000, it is 0x0101
  2. test_jp_absolute_wraparound: PC should be 0xFFFF, it is 0x0101
  3. test_jr_relative_positive: PC should be 0x0107, it is 0x0101

tests/test_core_cpu_interrupts.py (4 failures):

  1. test_halt_wakeup_on_interrupt: cpu.halted should be True, it is False
  2. test_interrupt_dispatch_vblank: assertion error on PC vector
  3. test_interrupt_priority: assertion error in priority
  4. test_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:

Before:
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

  1. 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
  2. tests/test_core_cpu_jumps.py(14 tests):
    • All JP/JR tests (absolute, relative, conditional)
    • Applied withreplace_all=True
  3. tests/test_core_cpu_interrupts.py(8 tests):
    • DI/EI, HALT, interrupt dispatch tests
    • Special case:test_all_interrupt_vectors with internal loop
  4. 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
  5. 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_configs
  • test_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

test_core_cpu_io.py:
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 withreplace_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 intest_core_joypad.py(different problem, not related to ROM writes)
  • Add a linter or pre-commit hook that detectsPyMMU()withoutset_test_mode_allow_rom_writes(True)in test files
  • Document the test mode pattern inCONTRIBUTING.mdfor future collaborators