Step 0421: Fix Test Mode ROM Writes in Unit Tests
📋 Resumen Ejecutivo
Corrección sistemática de 10 fallos unitarios en tests de CPU causados por la ausencia de la activación del test_mode_allow_rom_writes implementado en Step 0419. Los tests estaban escribiendo instrucciones en ROM (0x0000-0x7FFF) sin activar el modo test, lo que causaba que el MMU interpretara las escrituras como comandos MBC en lugar de escribir directamente en memoria. Esto resultaba en que la CPU ejecutara valores incorrectos (0x00 o residuales) en lugar de las instrucciones del test.
Solución: Agregar mmu.set_test_mode_allow_rom_writes(True) sistemáticamente después de cada instancia de PyMMU() en los archivos de tests unitarios, incluyendo casos especiales dentro de loops.
Impacto: Eliminación completa de los 10 fallos originales y corrección preventiva en 55 tests CPU totales.
🔧 Concepto Técnico: Test Mode y ROM Writes
Contexto del Problema
En el hardware real de Game Boy, la memoria ROM (0x0000-0x7FFF) es de solo lectura. Los cartuchos usan Memory Bank Controllers (MBC) que interpretan escrituras a estas direcciones como comandos de control (cambio de banco ROM/RAM, habilitación de RAM, etc.) en lugar de modificar la memoria.
Implementación en el Emulador
El MMU de Viboy Color replica fielmente este comportamiento:
// src/core/cpp/MMU.cpp (línea ~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 ...
Necesidad del Test Mode
- Sin test mode:
mmu.write(0x0100, 0xC3)→ Interpretado como comando MBC "RAM-ENABLE" - Con test mode:
mmu.write(0x0100, 0xC3)→ Escribe opcode JP nn directamente en ROM
Este mecanismo permite a los tests unitarios escribir programas sintéticos en ROM sin cargar un archivo .gb real, manteniendo la fidelidad del emulador al hardware original durante la ejecución normal.
Fuente: Pan Docs - "Memory Bank Controllers", comportamiento estándar de escrituras en ROM.
🔍 Diagnóstico (T1)
Fallos Originales Detectados (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 fallos):
test_ldh_write_lcdc: esperaba 3 ciclos, obtuvo 1test_ldh_read_hram: assertion error en valor leídotest_ldh_offset_wraparound: assertion error en escritura a 0xFFFF
tests/test_core_cpu_jumps.py (3 fallos):
test_jp_absolute: PC debería ser 0xC000, es 0x0101test_jp_absolute_wraparound: PC debería ser 0xFFFF, es 0x0101test_jr_relative_positive: PC debería ser 0x0107, es 0x0101
tests/test_core_cpu_interrupts.py (4 fallos):
test_halt_wakeup_on_interrupt: cpu.halted debería ser True, es Falsetest_interrupt_dispatch_vblank: assertion error en PC vectortest_interrupt_priority: assertion error en prioridadtest_all_interrupt_vectors: IME debería ser 1, es 0
Evidencia de Causa Raíz
Al inspeccionar test_core_cpu_io.py:70-89, se observó que el test escribe directamente en ROM sin activar test mode:
mmu = PyMMU()
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
regs.pc = 0x0100
mmu.write(0x0100, 0xE0) # ❌ Interpretado como MBC, no escribe
mmu.write(0x0101, 0x40) # ❌ Interpretado como MBC, no escribe
Búsqueda confirmatoria mostró que test_core_cpu_alu.py (que pasa correctamente) SÍ usa set_test_mode_allow_rom_writes(True).
🛠️ Implementación (T3)
Fix Aplicado
Reemplazo sistemático del patrón de inicialización en todos los archivos de test:
mmu = PyMMU()
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
Después:
mmu = PyMMU()
mmu.set_test_mode_allow_rom_writes(True) # Step 0421: Permitir escrituras en ROM para testing
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
Archivos Modificados
- 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):
- Todos los tests de JP/JR (absolutos, relativos, condicionales)
- Aplicado con
replace_all=True
- tests/test_core_cpu_interrupts.py (8 tests):
- Tests de DI/EI, HALT, dispatch de interrupciones
- Caso especial: test_all_interrupt_vectors con loop interno
- tests/test_core_cpu_loads.py (24 tests):
- Tests de LD 8-bit (registro, inmediato, memoria)
- Tests de LD 16-bit, ADD HL, stack pointers
- Caso especial: test_ld_block_matrix con loop interno
- tests/test_core_cpu_stack.py (4 tests):
- Tests de PUSH/POP BC
- Tests de CALL/RET básico y anidado
Casos Especiales en Loops
Dos tests requerían tratamiento especial por crear instancias de MMU dentro de loops:
test_core_cpu_interrupts.py:366→ Loop sobre interrupt_configstest_core_cpu_loads.py:145→ Loop sobre test_cases
En ambos casos, se agregó mmu.set_test_mode_allow_rom_writes(True) inmediatamente después de mmu = PyMMU() dentro del loop.
✅ Tests y Verificación (T4)
Compilación
python3 setup.py build_ext --inplace
BUILD_EXIT=0 ✅
python3 test_build.py
TEST_BUILD_EXIT=0 ✅
Tests CPU Específicos
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 (después de corregir caso especial del loop)
Tests CPU Completos
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
Resultado Final
| Archivo de Test | Antes | Después | Tests Totales |
|---|---|---|---|
| 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 | ✅ (preventivo) | ✅ 24 PASSED | 24 |
| test_core_cpu_stack.py | ✅ (preventivo) | ✅ 4 PASSED | 4 |
| TOTAL | 10 FAILED | ✅ 55 PASSED | 55 |
Validación de Módulo Compilado C++
✅ Los tests ejecutan instrucciones reales compiladas en C++ (no código Python).
✅ El test mode permite escribir programas sintéticos en ROM sin comprometer la fidelidad del emulador.
📦 Comandos Git
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/informe_fase_2/parte_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
📚 Lecciones Aprendidas
- Consistencia en tests: Cuando se introduce un mecanismo como test_mode, todos los tests existentes y futuros deben usarlo.
- Refactor sistemático: El patrón de búsqueda/reemplazo con
replace_all=Truees efectivo, pero requiere verificación manual de casos especiales (loops, indentaciones). - Test de tests: Comparar archivos que pasan (test_core_cpu_alu.py) vs los que fallan (test_core_cpu_io.py) reveló rápidamente la causa raíz.
- Fidelidad al hardware: El test mode demuestra que es posible mantener fidelidad al hardware real (ROM read-only) mientras se permite testing sintético.
🚀 Próximos Pasos
- Step 0422+: Investigar los 5 fallos en
test_core_joypad.py(problema diferente, no relacionado con ROM writes) - Agregar un linter o pre-commit hook que detecte
PyMMU()sinset_test_mode_allow_rom_writes(True)en archivos de test - Documentar el patrón de test mode en
CONTRIBUTING.mdpara futuros colaboradores