Step 0425: Spec-Correct JOYP + Address Wrap (Remove Hacks)
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_romwque 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:
- Escritura: Solo los bits 4-5 son escribibles. Los bits 0-3 son read-only.
- Lectura: Los bits 4-5 se leen tal como fueron escritos (NO se invierten).
- Selección: Un bit = 0 significa "seleccionado". Ambas filas pueden seleccionarse simultáneamente.
- 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: 0x10000 → 0x0000, 0x1C000 → 0xC000
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
- Constructor: Inicialización a
src/core/cpp/Joypad.hpp: Comentarios actualizados para reflejar comportamiento spec-correctsrc/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()
- Línea 935: Eliminado bypass
src/core/cpp/MMU.hpp:- Eliminado flag
test_mode_allow_rom_writes_ - Eliminada declaración de
set_test_mode_allow_rom_writes()
- Eliminado flag
2. Wrapper Cython
src/core/cython/mmu.pyx: Eliminado métodoset_test_mode_allow_rom_writes()src/core/cython/mmu.pxd: Eliminada declaración deset_test_mode_allow_rom_writes()
3. Tests (Python)
tests/conftest.py: Eliminado fixturemmu_romwtests/test_core_joypad.py:- 8 tests actualizados con valores spec-correct
- Dirección con bit4=0: Esperado
0xEE(antes0xDE) - Acción con bit5=0: Esperado
0xDE(antes0xEE) - 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))
- 4 tests actualizados para usar
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
- 1 test actualizado:
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.
Referencias
- Pan Docs: Joypad Input - https://gbdev.io/pandocs/Joypad_Input.html
- Pan Docs: Memory Map - https://gbdev.io/pandocs/Memory_Map.html
- GBEDG: Game Boy Complete Technical Reference
- Step 0424: Contexto de inversión bits 4-5 (hack a eliminar)
- Step 0419: Introducción de test_mode_allow_rom_writes (hack a eliminar)