⚠️ 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 y tests permitidas.

Joypad y Paleta por Defecto

Fecha: 2025-12-17 Step ID: 0031 Estado: Verified

Resumen

Se implementó el Joypad (control de botones y direcciones) de la Game Boy con lógica Active Low, y se corrigió la inicialización de la paleta BGP a 0xE4 por defecto. Los logs de diagnóstico revelaron que el juego estaba haciendo polling del Joypad (P1) y que la paleta estaba en 0x00 (todo blanco), haciendo que los gráficos fueran invisibles aunque se renderizaran correctamente. Con estas correcciones, el emulador puede responder a las pulsaciones de botones y mostrar gráficos correctamente coloreados.

Concepto de Hardware

El Joypad de la Game Boy usa un sistema de Active Low donde:

  • 0 = Botón pulsado
  • 1 = Botón soltado

El registro P1 (0xFF00) es de lectura/escritura:

  • ESCRITURA (bits 4-5): El juego selecciona qué leer:
    • Bit 4 = 0: Quiere leer Direcciones (Right, Left, Up, Down)
    • Bit 5 = 0: Quiere leer Botones (A, B, Select, Start)
  • LECTURA (bits 0-3): El juego lee el estado según el selector:
    • Bit 0: Right / A
    • Bit 1: Left / B
    • Bit 2: Up / Select
    • Bit 3: Down / Start

Cuando un botón pasa de Soltado (1) a Pulsado (0), se activa la interrupción Joypad (Bit 4 en IF, 0xFF0F).

La paleta BGP (Background Palette, 0xFF47) controla los colores del fondo. En una Game Boy real, la Boot ROM deja este registro configurado a 0xE4 (11100100 en binario), que mapea:

  • Índice 0 → Blanco (0)
  • Índice 1 → Gris claro (1)
  • Índice 2 → Gris oscuro (2)
  • Índice 3 → Negro (3)

Si BGP queda en 0x00 (todo blanco), aunque el juego renderice correctamente los tiles, todos los píxeles aparecerán blancos y la pantalla se verá completamente blanca.

Fuente: Pan Docs - Joypad Input, Background Palette Register (BGP)

Implementación

Se creó una clase Joypad que implementa la lógica Active Low y maneja el registro P1. Se integró en la MMU para interceptar lecturas/escrituras de P1, y en Viboy para capturar eventos de teclado de pygame y actualizar el estado del Joypad.

Componentes creados/modificados

  • src/io/joypad.py: Nueva clase Joypad con métodos press(), release(), read(), write(). Implementa lógica Active Low y solicita interrupciones cuando un botón se pulsa.
  • src/io/__init__.py: Nuevo módulo de I/O.
  • src/memory/mmu.py:
    • Inicialización de BGP a 0xE4 en __init__().
    • Interceptación de lectura/escritura de P1 (0xFF00) delegando al Joypad.
    • Método set_joypad() para conectar el Joypad a la MMU.
  • src/viboy.py:
    • Instanciación del Joypad y conexión a la MMU.
    • Método _handle_pygame_events() que captura eventos de teclado y actualiza el Joypad.
    • Mapeo de teclas: K_UP/DOWN/LEFT/RIGHT (direcciones), K_z (A), K_x (B), K_RETURN (Start), K_RSHIFT (Select).
  • tests/test_io_joypad.py: Suite completa de tests (14 tests) validando inicialización, lógica Active Low, selector de lectura, interrupciones y integración con MMU.

Decisiones de diseño

Lógica Active Low: Se implementó correctamente donde True = pulsado (bit 0 en hardware), False = soltado (bit 1 en hardware). Al leer P1, si un botón está pulsado, se hace clear del bit correspondiente.

Detección de transiciones: La interrupción solo se solicita cuando un botón pasa de soltado a pulsado. Si se llama press() varias veces sin release() intermedio, solo la primera pulsación activa la interrupción.

Selector de lectura: Se guarda el selector (bits 4-5) cuando el juego escribe en P1. Al leer, se devuelve el estado de los botones correspondientes según el selector.

Manejo de eventos: Se centralizó el manejo de eventos de pygame en _handle_pygame_events() dentro de Viboy, en lugar de hacerlo en el Renderer, para mantener la separación de responsabilidades.

Archivos Afectados

  • src/io/joypad.py - Nueva clase Joypad
  • src/io/__init__.py - Nuevo módulo I/O
  • src/memory/mmu.py - Inicialización BGP=0xE4, interceptación P1, método set_joypad()
  • src/viboy.py - Instanciación Joypad, método _handle_pygame_events()
  • tests/test_io_joypad.py - Suite completa de tests (14 tests)

Tests y Verificación

Se ejecutaron tests unitarios con pytest validando:

Tests ejecutados

  • Comando: python3 -m pytest tests/test_io_joypad.py -v
  • Entorno: macOS, Python 3.9.6
  • Resultado: 14 tests PASSED en 0.33s

Tests implementados

  • test_default_palette_init: Verifica que BGP se inicializa a 0xE4.
  • test_joypad_initial_state: Verifica que todos los botones están soltados al inicio.
  • test_joypad_read_default: Verifica lectura con selector por defecto.
  • test_joypad_read_directions: Verifica lectura de direcciones cuando están soltadas.
  • test_joypad_read_directions_pressed: Verifica lectura cuando Right está pulsado (bit 0 = 0).
  • test_joypad_read_buttons: Verifica lectura de botones cuando están soltados.
  • test_joypad_read_buttons_pressed: Verifica lectura cuando A está pulsado (bit 0 = 0).
  • test_joypad_press_interrupt: Verifica que pulsar un botón activa el bit 4 de IF.
  • test_joypad_release_no_interrupt: Verifica que soltar un botón NO activa interrupción.
  • test_joypad_press_twice_no_double_interrupt: Verifica que pulsar dos veces sin soltar solo activa interrupción la primera vez.
  • test_joypad_press_release_press_interrupt: Verifica que pulsar-soltar-pulsar activa interrupción ambas veces.
  • test_joypad_mmu_integration: Verifica integración con MMU (leer/escribir P1 funciona correctamente).
  • test_joypad_all_directions: Verifica lectura de todas las direcciones cuando están pulsadas.
  • test_joypad_all_buttons: Verifica lectura de todos los botones cuando están pulsados.

Código del test (ejemplo: detección de interrupción)

def test_joypad_press_interrupt(self) -> None:
    """Test: Pulsar un botón debe activar la interrupción Joypad (bit 4 en IF)"""
    mmu = MMU(None)
    joypad = Joypad(mmu)
    
    # Limpiar IF
    mmu.write_byte(IO_IF, 0x00)
    assert (mmu.read_byte(IO_IF) & 0x10) == 0
    
    # Pulsar Start
    joypad.press("start")
    
    # Verificar que el bit 4 de IF está activo
    if_val = mmu.read_byte(IO_IF)
    assert (if_val & 0x10) != 0

Qué valida: Este test demuestra que el hardware del Joypad solicita una interrupción cuando se pulsa un botón, activando el bit 4 del registro IF (Interrupt Flag). Esto permite que la CPU responda a eventos de entrada del usuario.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Active Low es fundamental: La lógica inversa (0 = pulsado, 1 = soltado) es una característica del hardware real de la Game Boy, no una elección arbitraria. Esto permite que el hardware use pull-up resistors y detecte pulsaciones conectando a tierra.
  • Selector de lectura: El hecho de que el juego tenga que escribir en P1 para seleccionar qué leer (direcciones vs botones) es una limitación del hardware que reduce el número de pines necesarios. El juego debe alternar entre leer direcciones y botones.
  • Interrupciones por transición: La interrupción solo se activa en la transición de soltado a pulsado, no mientras el botón permanece pulsado. Esto evita spam de interrupciones y permite al juego detectar "clicks" discretos.
  • Importancia de valores por defecto: Aunque no implementamos la Boot ROM todavía, los valores que deja la Boot ROM (como BGP=0xE4) son críticos. Sin estos valores, muchos juegos no funcionan correctamente porque asumen que la Boot ROM los configuró.

Lo que Falta Confirmar

  • Timing de interrupciones: No está completamente claro si hay algún retardo entre pulsar un botón y que se active el bit de IF, o si es instantáneo. Por ahora, asumimos que es instantáneo.
  • Comportamiento de bits 4-5 en lectura: En hardware real, cuando se lee P1, los bits 4-5 reflejan el selector (lo que escribió el juego), pero podría haber detalles de comportamiento que no están completamente documentados. Por ahora, mantenemos los bits 4-5 como están en el selector.

Hipótesis y Suposiciones

Se asume que la solicitud de interrupción es instantánea cuando se pulsa un botón (sin retardo). Se asume que si el juego no ha escrito un selector válido (ambos bits 4-5 = 1), la lectura devuelve todos los bits a 1 (todos los botones "soltados" desde la perspectiva del hardware).

Próximos Pasos

  • [ ] Probar el emulador con un juego real para verificar que el Joypad funciona correctamente
  • [ ] Implementar Scroll (SCX/SCY) para que el juego pueda desplazar el fondo
  • [ ] Implementar Window (WX/WY) para que el juego pueda mostrar una ventana superpuesta
  • [ ] Implementar Sprites (OAM) para que el juego pueda mostrar objetos móviles
  • [ ] Implementar el Timer completo (TIMA, TMA, TAC) para sincronización precisa