Step 0425: Spec-Correct JOYP + Address Wrap (Remove Hacks)

← Volver al índice

Resumen Ejecutivo

Corrección definitiva del comportamiento de JOYP (FF00) y address wrapping a 16-bit según Pan Docs, eliminando todos los hacks introducidos en Steps anteriores (especialmente 0419 y 0424). El Step 0424 implementó inversión artificial de bits 4-5 en JOYP basándose en observaciones empíricas de tests, pero esta implementación contradice Pan Docs que especifica que los bits 4-5 se leen tal como fueron escritos.

Decisión crítica: Cuando un test contradice Pan Docs, se corrige el test, no el hardware. El objetivo de este Step es restablecer comportamiento spec-correct eliminando:

  • ❌ Inversión artificial de bits 4-5 en JOYP (Joypad.cpp)
  • ❌ Bypass test_mode_allow_rom_writes (3 ubicaciones: MMU.cpp, MMU.hpp, mmu.pyx)
  • ❌ Fixture mmu_romw que permitía escribir en ROM (conftest.py)

Resultado: ✅ 8/8 tests de Joypad + 11/11 tests de MMU pasan con comportamiento spec-correct. ✅ 215/225 tests totales (10 fallos pre-existentes no relacionados con este Step).

Concepto de Hardware (Pan Docs)

JOYP (FF00) - Registro P1

Fuente: Pan Docs - "Joypad Input" / GBEDG

Estructura del Registro:

Bit 7-6: No usados (siempre 1)
Bit 5 (P15): 0 = Selecciona botones de acción (A, B, Select, Start)
Bit 4 (P14): 0 = Selecciona botones de dirección (Right, Left, Up, Down)
Bit 3-0: Estado de botones (0 = presionado, 1 = suelto) [Read-Only]

Comportamiento Spec-Correct:

  1. Escritura: Solo los bits 4-5 son escribibles. Los bits 0-3 son read-only.
  2. Lectura: Los bits 4-5 se leen tal como fueron escritos (NO se invierten).
  3. Selección: Un bit = 0 significa "seleccionado". Ambas filas pueden seleccionarse simultáneamente.
  4. Nibble bajo (bits 0-3):
    • Si ninguna fila seleccionada (bits 4-5 = 11): nibble = 0xF
    • Si fila(s) seleccionada(s): nibble refleja estado de botones (AND de filas activas)

Ejemplo Práctico:

// Seleccionar fila de dirección
write(FF00, 0x20)  // bits 5-4 = 10 (bit 4 = 0 selecciona dirección)
// Estado: Botón "Derecha" presionado (bit 0 = 0)
read(FF00)  // Retorna 0xEE = 1110 1110
            // bits 7-6 = 11 (siempre)
            // bits 5-4 = 10 (tal como se escribió, SIN inversión)
            // bits 3-0 = 1110 (bit 0 = 0 indica "Derecha" presionado)

Address Wrap a 16-bit

Fuente: Pan Docs - "Memory Map"

El Game Boy usa direcciones de 16 bits (0x0000-0xFFFF). Cualquier dirección fuera de este rango debe hacer wrap automáticamente mediante addr &= 0xFFFF al inicio de MMU::read() y MMU::write().

Ejemplo: 0x100000x0000, 0x1C0000xC000

ROM Read-Only (Spec-Correct)

Fuente: Pan Docs - "Memory Bank Controllers"

En el Game Boy real, ROM (0x0000-0x7FFF) es siempre read-only. Las escrituras en este rango se interpretan como comandos para el Memory Bank Controller (MBC), NO como escrituras directas en memoria.

Implicación para tests: Los tests que necesiten ROM personalizada deben usar load_rom_py() con un bytearray preparado, no escribir directamente en ROM.

Implementación

Archivos Modificados

1. Hardware Core (C++)

  • src/core/cpp/Joypad.cpp:
    • Constructor: Inicialización a 0xCF (bits 4-5 = 00, spec-correct)
    • read_p1(): Eliminada inversión de bits 4-5, retornar (p1_register_ & 0x30) directamente
    • Caso especial: Si ninguna fila seleccionada, nibble = 0xF
  • src/core/cpp/Joypad.hpp: Comentarios actualizados para reflejar comportamiento spec-correct
  • src/core/cpp/MMU.cpp:
    • Línea 935: Eliminado bypass test_mode_allow_rom_writes_
    • Línea 1068: Eliminado bypass ROM_ONLY cuando rom_data_.empty()
    • Línea 3564: Eliminado método set_test_mode_allow_rom_writes()
  • src/core/cpp/MMU.hpp:
    • Eliminado flag test_mode_allow_rom_writes_
    • Eliminada declaración de set_test_mode_allow_rom_writes()

2. Wrapper Cython

  • src/core/cython/mmu.pyx: Eliminado método set_test_mode_allow_rom_writes()
  • src/core/cython/mmu.pxd: Eliminada declaración de set_test_mode_allow_rom_writes()

3. Tests (Python)

  • tests/conftest.py: Eliminado fixture mmu_romw
  • tests/test_core_joypad.py:
    • 8 tests actualizados con valores spec-correct
    • Dirección con bit4=0: Esperado 0xEE (antes 0xDE)
    • Acción con bit5=0: Esperado 0xDE (antes 0xEE)
    • Justificación: Pan Docs especifica que bits 4-5 NO se invierten
  • tests/test_mmu_rom_is_readonly_by_default.py:
    • 4 tests actualizados para usar load_rom_py()
    • Eliminado uso de fixture mmu_romw
    • Test de load_rom: Preparar ROM de 512 bytes, cargar con load_rom_py(bytes(custom_rom))
  • tests/test_core_mmu.py:
    • 1 test actualizado: test_mmu_address_wrapping
    • Cambio: Validar wrap con WRAM (0xC000) en lugar de ROM (0x0000)
    • Justificación: ROM es read-only, no se puede validar wrap escribiendo en ROM

Fragmentos de Código Clave

JOYP read_p1() Spec-Correct (Joypad.cpp)

uint8_t Joypad::read_p1() const {
    // Empezar con bits 0-3 a 1 (todos sueltos por defecto)
    uint8_t nibble = 0x0F;
    
    // Selección de fila según Pan Docs: bit=0 selecciona
    bool direction_row_selected = (p1_register_ & 0x10) == 0;
    bool action_row_selected = (p1_register_ & 0x20) == 0;
    
    if (direction_row_selected) {
        nibble &= direction_keys_;
    }
    
    if (action_row_selected) {
        nibble &= action_keys_;
    }
    
    // Caso especial: si ninguna fila está seleccionada
    if (!direction_row_selected && !action_row_selected) {
        nibble = 0x0F;
    }
    
    // Construir resultado spec-correct (SIN inversión bits 4-5)
    uint8_t result = 0xC0 | (p1_register_ & 0x30) | (nibble & 0x0F);
    
    return result;
}

ROM Read-Only Spec-Correct (MMU.cpp)

// En MMU::write() - Switch case para ROM (0x0000-0x7FFF)
case MBCType::ROM_ONLY:
default:
    // Step 0425: ROM es SIEMPRE read-only (spec-correct según Pan Docs).
    // Las escrituras en ROM se ignoran (o se interpretan como MBC).
    // NO permitir escrituras incluso si rom_data_ está vacío.
    return;

Tests y Verificación

Comandos Ejecutados

$ python3 setup.py build_ext --inplace
BUILD_EXIT=0 ✅

$ python3 test_build.py
TEST_BUILD_EXIT=0 ✅

$ pytest -q tests/test_core_joypad.py
8 passed in 0.37s ✅

$ pytest -q tests/test_mmu_rom_is_readonly_by_default.py
4 passed in 0.32s ✅

$ pytest -q tests/test_core_mmu.py
7 passed in 0.28s ✅

$ pytest -q
215 passed, 10 failed in 0.53s
(10 fallos pre-existentes NO relacionados: PPU rendering, Registers init, CPU control)

Evidencia de Tests (Fragmentos Clave)

Test JOYP Dirección (Spec-Correct)

def test_joypad_selection_direction(self):
    """
    Verifica que escribir en P1 selecciona la fila de dirección correctamente (spec-correct).
    
    Step 0425: Actualizado para reflejar comportamiento spec-correct según Pan Docs.
    Los bits 4-5 se leen TAL COMO fueron escritos (NO se invierten).
    """
    joypad = PyJoypad()
    
    # Presionar Derecha (dirección, índice 0)
    joypad.press_button(0)
    
    # Seleccionar fila de dirección (bit 4 = 0)
    # Escribir 0x20 = 0b00100000 (bit 5=1, bit 4=0)
    joypad.write_p1(0x20)
    
    # Leer P1. Debería mostrar Derecha presionada (bit 0 = 0)
    # Resultado esperado spec-correct: 0xEE = 1110 1110
    # (bits 7-6=1, bit 5=1, bit 4=0, bit 0=0 presionado, bits 3-1=1 sueltos)
    result = joypad.read_p1()
    assert result == 0xEE, f"Esperado 0xEE (spec-correct), obtenido 0x{result:02X}"

Test ROM Read-Only (Spec-Correct)

def test_rom_is_readonly_by_default(self, mmu):
    """
    Validar que ROM (0x0000-0x7FFF) es read-only (spec-correct).
    
    Concepto Hardware (Pan Docs):
    ------------------------------
    En Game Boy real, ROM (0x0000-0x7FFF) es memoria de solo lectura.
    Las escrituras en este rango se interpretan como comandos para el
    Memory Bank Controller (MBC), NO como escrituras directas.
    
    Step 0425: Eliminado uso de test_mode (hack no spec-correct).
    """
    # Intentar escribir en ROM (debe interpretarse como comando MBC, no escritura directa)
    original_value = mmu.read(0x0000)
    mmu.write(0x0000, 0x3E)  # Intentar escribir 0x3E
    
    # Verificar que NO se escribió (debe seguir siendo el valor original)
    readback = mmu.read(0x0000)
    assert readback == original_value, (
        f"ROM debe ser read-only (spec-correct). "
        f"Intentamos escribir 0x3E en 0x0000, pero se leyó 0x{readback:02X}"
    )

Validación de Módulo Compilado C++

✅ Todos los tests ejecutan contra el módulo nativo viboy_core.cpython-312-x86_64-linux-gnu.so

✅ Validación de comportamiento hardware-accurate mediante tests unitarios directos

✅ Logs de depuración confirman comportamiento spec-correct (bits 4-5 sin inversión)

Resultados y Análisis

Hacks Eliminados (100% Exitoso)

Ubicación Hack Eliminado Estado
Joypad.cpp:52 Inversión artificial bits 4-5 REMOVED
MMU.cpp:935 Bypass test_mode_allow_rom_writes REMOVED
MMU.cpp:1068 Bypass ROM_ONLY empty check REMOVED
MMU.cpp:3564 Método set_test_mode_allow_rom_writes() REMOVED
MMU.hpp:372 Flag test_mode_allow_rom_writes_ REMOVED
mmu.pyx:403 Wrapper Python set_test_mode_allow_rom_writes REMOVED
conftest.py:74 Fixture mmu_romw REMOVED

Tests Actualizados (13 tests, 100% pasan)

  • test_core_joypad.py: 8/8 tests con valores spec-correct
  • test_mmu_rom_is_readonly_by_default.py: 4/4 tests sin test_mode
  • test_core_mmu.py: 1/1 test actualizado (address wrap con WRAM)

Cobertura de Tests

Categoría                    Tests    Resultado
──────────────────────────────────────────────
Joypad (JOYP FF00)           8/8      ✅ PASS
MMU (ROM read-only)          4/4      ✅ PASS
MMU (Core functionality)     7/7      ✅ PASS
──────────────────────────────────────────────
TOTAL Step 0425             19/19     ✅ 100%

Suite Completa              215/225   ✅ 95.6%
(10 fallos pre-existentes en PPU/Registers/CPU, NO relacionados)

Impacto en el Código

  • Líneas eliminadas: ~150 líneas (código + comentarios de hacks)
  • Líneas actualizadas: ~80 líneas (tests)
  • Complejidad reducida: Eliminación de flags condicionales y bypasses
  • Integridad mejorada: Código 100% spec-correct según Pan Docs

Lecciones Aprendidas

1. Primacía de Pan Docs sobre Tests Empíricos

Problema: El Step 0424 implementó inversión de bits 4-5 basándose en observaciones de tests, contradiciendo Pan Docs.

Solución: Cuando un test contradice la documentación oficial, corregir el test, no el hardware. Pan Docs es la fuente de verdad para comportamiento spec-correct.

2. Test Mode es Deuda Técnica

Problema: El flag test_mode_allow_rom_writes permitía escribir en ROM, violando comportamiento real del hardware.

Solución: Los tests que necesiten ROM personalizada deben usar load_rom_py(), que simula correctamente la carga de un cartucho. Esto mantiene la integridad del emulador.

3. Validación con WRAM para Address Wrap

Problema: No se puede validar address wrap escribiendo en ROM (es read-only).

Solución: Usar WRAM (0xC000) que es escribible. Ejemplo: 0x1C000 & 0xFFFF = 0xC000.

4. Estrategia de Migración Incremental

Éxito: Eliminar hacks de forma atómica (un Step) minimiza riesgo. Los tests actualizados documentan explícitamente el cambio con referencias a Pan Docs.

Próximos Pasos

  1. Step 0426: Auditoría completa de ROMs reales (Tetris DX, Zelda DX, Pokemon Red) con comportamiento JOYP spec-correct
  2. Step 0427: Fix de 10 fallos restantes (PPU rendering, Registers initialization, CPU control IME)
  3. Step 0428: Implementación de Audio (APU) - Fase 2 completa

Referencias