⚠️ Clean-Room / Educativo

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

← Volver al índice

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

49/49
Tests migrados (100%)
98%
Reducción ROM-writes
118
Tests passing ✅
10
Fallos conocidos (joypad/MMU)

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 mmu estándar (sin ROM-writes) y load_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

  1. Reemplazar mmu = PyMMU() por fixture def test_xxx(self, mmu):
  2. Eliminar mmu.set_test_mode_allow_rom_writes(True)
  3. Reemplazar escrituras directas en ROM por load_program(mmu, regs, [opcodes])
  4. Ajustar expectativas de PC: 0x0100 → TEST_EXEC_BASE, 0x0102 → TEST_EXEC_BASE + 2
  5. 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_writes que 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

  1. Migración incremental es clave: Validar cada archivo antes de pasar al siguiente reduce riesgo de introducir fallos.
  2. Fixture mmu centraliza lógica: Cambios futuros (ej: agregar logging) se aplican automáticamente a todos los tests.
  3. Helper load_program() es robusto: Usado en 49 tests sin fallos, incluyendo edge cases (CALL anidado, JR negativo, interrupt dispatch).
  4. 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.
  5. 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 migrados
  • tests/test_core_cpu_jumps.py - 14 tests migrados
  • tests/test_core_cpu_io.py - 5 tests migrados
  • tests/test_core_cpu_stack.py - 4 tests migrados
  • tests/test_core_cpu_interrupts.py - 8 tests migrados
  • docs/bitacora/entries/2026-01-02__0423__*.html - Esta entrada (nuevo)
  • docs/bitacora/index.html - Actualizado con Step 0423
  • docs/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.

✅ Step 0423 completado exitosamente
  • 49 tests migrados (100%)
  • 98% reducción de ROM-writes
  • 118 tests passing
  • 0 fallos nuevos introducidos
  • Base sólida para Step 0424 (fix joypad/MMU)

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