Este proyecto es educativo y Open Source. No se copia código de otros emuladores. Implementación basada únicamente en documentación técnica.
Step 0423: Migración masiva CPU tests a WRAM y minimización de ROM-writes
Resumen Ejecutivo
Migración masiva y exitosa de 49 tests de CPU de ROM (con set_test_mode_allow_rom_writes)
a WRAM usando load_program() y fixture mmu estándar. Reducción de deuda técnica
del 98%: de 50 hits de ROM-writes a solo 1 (fixture legítimo mmu_romw en conftest.py).
Objetivo
Eliminar el uso innecesario de mmu_romw (ROM-writes) en tests CPU, ejecutando
código desde WRAM (0xC000) mediante load_program(). Dejar ROM-writes solo
donde sea imprescindible (vectores de interrupción 0x0040-0x0060, que en este caso tampoco se necesitó).
Estado Inicial
Después del Step 0422:
- 49 hits de
set_test_mode_allow_rom_writes(True)en 5 archivos de tests CPU - Tests ejecutando desde ROM (0x0100, 0x0200, etc.) con ROM-writes habilitados
- Fixture
mmuestándar (sin ROM-writes) yload_program()disponibles - Suite: 118 passed, 10 failed (fallos conocidos de joypad/MMU)
Implementación
Archivos Migrados (49 tests total)
1. test_core_cpu_loads.py (18 tests)
Operaciones de carga (LD) y aritmética 16-bit. Todos los tests migrados a usar load_program() y fixture mmu.
# ANTES (con 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()
# DESPUÉS (desde WRAM, sin ROM-writes)
def test_ld_b_a(self, mmu): # Fixture mmu inyectado
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
regs.a = 0x10
load_program(mmu, regs, [0x47]) # Carga en WRAM (0xC000)
cycles = cpu.step()
assert regs.b == 0x10
2. test_core_cpu_jumps.py (14 tests)
Instrucciones de salto (JP, JR, condicionales). Ajustes de direcciones esperadas a TEST_EXEC_BASE + offset.
# Ejemplo: JR relativo positivo
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 (instrucción) + 5 (offset)
assert regs.pc == expected_pc
3. test_core_cpu_io.py (5 tests)
Instrucciones LDH (I/O de memoria alta). Migración directa sin cambios en direcciones I/O.
# Ejemplo: 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 con offset 0x40
cycles = cpu.step()
assert mmu.read(0xFF40) == 0xAB # LCDC register
4. test_core_cpu_stack.py (4 tests)
Operaciones de pila (PUSH/POP/CALL/RET). CALL usa direcciones WRAM (0xC100+) como targets.
# Ejemplo: CALL/RET
def test_call_ret_basic(self, mmu):
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
regs.sp = 0xFFFE
call_target = 0xC100 # Target en WRAM alta
mmu.write(call_target, 0xC9) # RET en destino
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 # Retorna después de CALL
5. test_core_cpu_interrupts.py (8 tests)
DI/EI/HALT y dispatcher de interrupciones. Tests de dispatch NO escriben vectores ISR, solo verifican saltos.
# Ejemplo: EI con 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 # Aún no activo (delayed)
cpu.step() # NOP
assert cpu.get_ime() == 1 # Ahora sí activo
# Ejemplo: Interrupt dispatch (sin escribir vectores)
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() # Activar IME
mmu.write(0xFF0F, 0x01) # Activar V-Blank en IF
mmu.write(0xFFFF, 0x01) # Habilitar V-Blank en IE
cycles = cpu.step() # Procesa interrupción
assert regs.pc == 0x0040 # Salta a vector V-Blank (ROM, pero no escribimos ahí)
Patrón de Migración
- Reemplazar
mmu = PyMMU()por fixturedef test_xxx(self, mmu): - Eliminar
mmu.set_test_mode_allow_rom_writes(True) - Reemplazar escrituras directas en ROM por
load_program(mmu, regs, [opcodes]) - Ajustar expectativas de PC:
0x0100 → TEST_EXEC_BASE,0x0102 → TEST_EXEC_BASE + 2 - En writes indirectos (HL), usar direcciones WRAM/HRAM escribibles (ej: 0xC100 en lugar de 0x0000)
Tests y Verificación
Auditoría de ROM-writes
$ grep -rn "set_test_mode_allow_rom_writes(True)" tests/ | wc -l
# ANTES (Step 0422):
50 # 49 tests + 1 conftest (fixture legítimo)
# DESPUÉS (Step 0423):
1 # Solo conftest.py (fixture mmu_romw)
# Reducción: 49 hits eliminados (98%)
Compilación y 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 (esperado por 10 fallos conocidos)
# Fallos (SOLO los conocidos, NO nuevos):
- 8 tests de joypad (test_core_joypad.py) ← Pendiente Step 0424
- 2 tests de MMU (test_core_mmu.py) ← Pendiente Step 0424
# Tests migrados exitosamente:
- 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 tests migrados (100%)
Validación de Módulo Nativo
✅ Todos los tests migrados ejecutan código compilado C++ desde WRAM mediante Cython.
✅ MMU en modo estándar (ROM read-only) funcionando correctamente.
✅ load_program() helper verificado en 49 tests.
Concepto Técnico: Ejecución desde WRAM vs ROM
Mapa de Memoria Game Boy
0x0000-0x7FFF: ROM (Read Only) ← No se puede escribir en hardware real
0x8000-0x9FFF: VRAM
0xA000-0xBFFF: External RAM (Cartridge)
0xC000-0xDFFF: WRAM (Work RAM) ← Sí se puede escribir
0xE000-0xFDFF: Echo RAM (espejo de WRAM)
0xFE00-0xFE9F: OAM
0xFF00-0xFF7F: I/O Registers
0xFF80-0xFFFE: HRAM
0xFFFF: IE Register
Por qué WRAM es el lugar correcto para tests
- Realismo: En hardware real, no puedes escribir en ROM. Los tests deben reflejar esto.
- Flexibilidad: WRAM (8KB, 0xC000-0xDFFF) es suficiente para cualquier programa de test.
- Limpieza: Evita el hack de
test_mode_allow_rom_writesque solo existe para tests. - Seguridad: El código de producción (src/) nunca debe asumir que ROM es escribible.
Casos donde ROM-writes SÍ serían necesarios
En este Step 0423, ningún test los requirió. Pero casos legítimos incluirían:
- Vectores de interrupción: Escribir código ISR en 0x0040 (V-Blank), 0x0048 (LCD STAT), etc.
- Tests de MBC: Validar que comandos MBC (0x0000-0x7FFF) NO modifican ROM.
Para estos casos, existe el fixture mmu_romw en conftest.py (el único hit restante).
Lecciones Aprendidas
- Migración incremental es clave: Validar cada archivo antes de pasar al siguiente reduce riesgo de introducir fallos.
-
Fixture
mmucentraliza lógica: Cambios futuros (ej: agregar logging) se aplican automáticamente a todos los tests. -
Helper
load_program()es robusto: Usado en 49 tests sin fallos, incluyendo edge cases (CALL anidado, JR negativo, interrupt dispatch). - Tests de interrupción NO necesitan ROM-writes: Validar el salto a vectores (0x0040-0x0060) no requiere escribir código en esos vectores, solo verificar que PC salta correctamente.
- Deuda técnica eliminada proactivamente: La migración masiva evita acumular más tests con ROM-writes en el futuro.
Archivos Modificados
tests/test_core_cpu_loads.py- 18 tests migradostests/test_core_cpu_jumps.py- 14 tests migradostests/test_core_cpu_io.py- 5 tests migradostests/test_core_cpu_stack.py- 4 tests migradostests/test_core_cpu_interrupts.py- 8 tests migradosdocs/bitacora/entries/2026-01-02__0423__*.html- Esta entrada (nuevo)docs/bitacora/index.html- Actualizado con Step 0423docs/informe_fase_2/parte_01_steps_0412_0450.md- Actualizado con Step 0423
Scope: Solo tests/ y docs/. Sin cambios en src/ (guardrail cumplido).
Conclusión
El Step 0423 completa exitosamente la migración masiva de tests CPU a WRAM, reduciendo la deuda técnica de ROM-writes del 98%. Con 49 tests migrados y 0 fallos nuevos introducidos, el test harness está ahora más limpio, realista y mantenible. Los 10 fallos restantes (joypad/MMU) son conocidos y no relacionados con esta migración, pendientes para Step 0424.
Próximos Pasos
Step 0424 (Propuesto): Fix 10 fallos restantes en joypad/MMU
- Investigar y corregir 8 fallos en test_core_joypad.py
- Investigar y corregir 2 fallos en test_core_mmu.py
- Objetivo: Suite completa a 128/128 tests passing (100%)