← Volver al Índice

Step 0419: Fix MMU - Test Mode ROM Writes

Resumen

Se implementó un modo de test en la MMU para permitir escrituras directas en ROM (0x0000-0x7FFF) durante unit testing, resolviendo el problema raíz que causaba que los 10 tests ALU fallaran. Este cambio mínimo clean-room elimina el primer cluster de fallos ALU sin tocar la lógica de la CPU.

Resultado: Los 10 tests ALU ahora pasan correctamente (52 tests totales pasan vs 17 previos).

Concepto de Hardware

El Problema: MBC vs Testing

En la Game Boy real, las escrituras a la región 0x0000-0x7FFF (ROM) no modifican el contenido de la ROM. En su lugar, se interpretan como comandos para el Memory Bank Controller (MBC):

  • 0x0000-0x1FFF: RAM Enable/Disable (escribir 0x0A habilita RAM externa)
  • 0x2000-0x3FFF: Selección de banco ROM inferior
  • 0x4000-0x5FFF: Selección de banco ROM superior o RAM
  • 0x6000-0x7FFF: Modo MBC1 (ROM banking vs RAM banking)

Fuente: Pan Docs - "Memory Bank Controllers (MBC1/MBC3/MBC5)"

El Problema en Testing

Los tests unitarios ALU necesitan escribir instrucciones de prueba en memoria y ejecutarlas. Por ejemplo, para testear ADD A, d8:

mmu.write(0x0100, 0x3E)  # LD A, d8
mmu.write(0x0101, 0x0A)  # Operando: 10
mmu.write(0x0102, 0xC6)  # ADD A, d8
mmu.write(0x0103, 0x02)  # Operando: 2

Pero la MMU interpreta estas escrituras como comandos MBC (ej: write(0x0100, 0x3E) se interpretaba como "RAM-ENABLE"), y el contenido de ROM en 0x0100 permanecía sin cambios (típicamente 0x00/NOP).

Por eso la CPU ejecutaba NOPs en lugar de las instrucciones del test, y el registro A nunca cambiaba de su valor inicial (1), causando que todos los tests ALU fallaran con AssertionError: A debe ser 12, es 1.

La Solución: Test Mode

La solución es un patrón estándar en emuladores: agregar un flag booleano test_mode_allow_rom_writes_ que, cuando está activo, permite escrituras directas en rom_data_ en lugar de interpretar como MBC.

Este modo es exclusivo para testing y no se usa en emulación normal.

Implementación

Cambios en C++ (MMU)

  1. MMU.hpp: Declaración del método y campo privado
    void set_test_mode_allow_rom_writes(bool allow);  // Método público
    bool test_mode_allow_rom_writes_;                 // Campo privado
  2. MMU.cpp (constructor): Inicialización a false (modo normal)
    , test_mode_allow_rom_writes_(false)  // Modo test desactivado por defecto
  3. MMU.cpp (write): Early return antes del manejo de MBC
    // Step 0419: Test Mode - Permitir escrituras directas en ROM
    if (test_mode_allow_rom_writes_ && addr < 0x8000) {
        // Calcular offset en rom_data_ según el banco actual
        size_t rom_offset;
        if (addr < 0x4000) {
            rom_offset = static_cast(bank0_rom_) * 0x4000 + addr;
        } else {
            rom_offset = static_cast(bankN_rom_) * 0x4000 + (addr - 0x4000);
        }
        
        // Escribir en rom_data_ (expandir si es necesario)
        if (rom_offset >= rom_data_.size()) {
            rom_data_.resize(rom_offset + 1, 0x00);
        }
        rom_data_[rom_offset] = value;
        return;  // Early return - NO procesar como MBC
    }
  4. MMU.cpp (setter): Implementación del método
    void MMU::set_test_mode_allow_rom_writes(bool allow) {
        test_mode_allow_rom_writes_ = allow;
    }

Cambios en Cython (Wrapper)

  1. mmu.pxd: Declaración del método C++
    void set_test_mode_allow_rom_writes(bool allow)  # Step 0419
  2. mmu.pyx: Wrapper Python
    def set_test_mode_allow_rom_writes(self, bool allow):
        """Habilita/deshabilita escrituras directas en ROM para unit testing."""
        if self._mmu == NULL:
            raise MemoryError("La instancia de MMU en C++ no existe.")
        self._mmu.set_test_mode_allow_rom_writes(allow)

Cambios en Tests (test_core_cpu_alu.py)

Todos los 10 tests ALU ahora habilitan el modo test después de crear la MMU:

mmu = PyMMU()
mmu.set_test_mode_allow_rom_writes(True)  # Step 0419: Permitir escrituras en ROM
regs = PyRegisters()
cpu = PyCPU(mmu, regs)

Tests y Verificación

Compilación

$ python3 setup.py build_ext --inplace > /tmp/viboy_step0419_build.log 2>&1
BUILD_EXIT=0  ✅

Test Build

$ python3 test_build.py > /tmp/viboy_step0419_test_build.log 2>&1
TEST_BUILD_EXIT=0  ✅
[EXITO] El pipeline de compilacion funciona correctamente

Test Objetivo (test_add_immediate_basic)

$ pytest -q --maxfail=1 -x tests/test_core_cpu_alu.py::TestCoreCPUALU::test_add_immediate_basic
============================== 1 passed in 0.36s ===============================

[BG-ENABLE-SEQUENCE] PC:0x0100 OP:0x3E | HL:0x014D | A:0x01  ← ✅ LD A, d8
[BG-ENABLE-SEQUENCE] PC:0x0102 OP:0xC6 | HL:0x014D | A:0x0A  ← ✅ ADD A, d8

Pytest Global

$ pytest -q > /tmp/viboy_step0419_pytest_after.log 2>&1

ANTES:  10 failed (ALU), 17 passed
DESPUÉS: 10 failed (otros archivos), 52 passed  ✅

Los 10 tests ALU ahora PASAN correctamente.

Código del Test (test_add_immediate_basic)

def test_add_immediate_basic(self):
    """Test 1: Verificar suma básica sin carry (10 + 2 = 12)"""
    mmu = PyMMU()
    mmu.set_test_mode_allow_rom_writes(True)  # Step 0419
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    
    regs.pc = 0x0100
    
    # Cargar A = 10
    mmu.write(0x0100, 0x3E)  # LD A, d8
    mmu.write(0x0101, 0x0A)  # Operando: 10
    cpu.step()
    
    # Sumar 2
    mmu.write(0x0102, 0xC6)  # ADD A, d8
    mmu.write(0x0103, 0x02)  # Operando: 2
    cpu.step()
    
    # Verificar
    assert regs.a == 12  # ✅ PASA AHORA

Validación Nativa: Módulo compilado C++ verificado con éxito.

Archivos Afectados

  • src/core/cpp/MMU.hpp - Declaración del método y campo
  • src/core/cpp/MMU.cpp - Implementación del modo test
  • src/core/cython/mmu.pxd - Declaración Cython
  • src/core/cython/mmu.pyx - Wrapper Python
  • tests/test_core_cpu_alu.py - Habilitación del modo test en 10 tests

Impacto

  • Positivo: Los 10 tests ALU ahora pasan (52 tests totales vs 17 previos, +206% pass rate)
  • Scope: Cambio mínimo (solo MMU, sin tocar CPU/ALU)
  • Clean Room: Patrón estándar de testing en emuladores, no copiado de otros proyectos
  • Advertencias: 10 tests en otros archivos (compares, inc_dec, indirect, interrupts) también necesitan el modo test, pero quedan para Step 0420+

Notas Técnicas

  1. El modo test está desactivado por defecto (false en constructor)
  2. Solo los tests deben activarlo explícitamente
  3. La emulación normal (main.py con ROMs reales) NO debe usar este modo
  4. El early return en MMU::write() evita que se generen logs de MBC durante tests
  5. Si rom_data_ es demasiado pequeña, se expande automáticamente