⚠️ 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.

Fix: Comunicación de frame_ready C++ -> Python

Fecha: 2025-12-19 Step ID: 0123 Estado: Verified

Resumen

Después de desbloquear el bucle principal (Step 0122), el emulador se ejecutaba correctamente en la consola (logs de "Heartbeat" visibles), pero la ventana de Pygame permanecía en blanco o no aparecía. El diagnóstico reveló que aunque la PPU en C++ estaba avanzando correctamente y llegaba a V-Blank, no había forma de comunicarle a Python que un fotograma estaba listo para renderizar.

El problema era que el bucle de renderizado en `viboy.py` dependía de `self.ppu.is_frame_ready()`, pero el método existente no estaba siendo llamado correctamente o no estaba expuesto adecuadamente desde C++ a Python. Se renombró el método a `get_frame_ready_and_reset()` para mayor claridad y se verificó que la señal de comunicación funcione correctamente en toda la cadena C++ → Cython → Python.

Concepto de Hardware

En la Game Boy real, cuando la PPU completa las 144 líneas visibles y entra en V-Blank (línea 144), se dispara una interrupción V-Blank que notifica a la CPU que un fotograma completo está listo. El software del juego puede entonces actualizar sprites, tilemaps, y otros recursos gráficos durante el período de V-Blank (líneas 144-153, aproximadamente 1.1ms).

El problema en el emulador: En una arquitectura híbrida Python/C++, necesitamos un mecanismo de sincronización que permita que el núcleo C++ (PPU) comunique al frontend Python (renderer) que un fotograma está listo. Esta comunicación debe ser:

  • Thread-safe: Aunque en este caso no hay threads, el patrón debe ser seguro
  • No bloqueante: El bucle principal no debe esperar
  • De un solo uso: Cada fotograma debe renderizarse exactamente una vez
  • Eficiente: Sin overhead significativo en el bucle crítico

La solución: Implementar un patrón de "máquina de estados de un solo uso" usando una bandera booleana (`frame_ready_`) en C++ que se levanta cuando `LY == 144` y se baja automáticamente cuando Python consulta el estado. Esto garantiza que:

  • C++ levanta la bandera una vez por fotograma (cuando llega a V-Blank)
  • Python consulta la bandera en cada iteración del bucle
  • La bandera se resetea automáticamente después de ser leída (evita renderizados duplicados)
  • Si Python no consulta a tiempo, el siguiente fotograma simplemente sobrescribe la bandera (comportamiento correcto)

Implementación

El método `is_frame_ready()` ya existía en la implementación C++, pero se renombró a `get_frame_ready_and_reset()` para mayor claridad sobre su comportamiento. Se verificó que toda la cadena de comunicación funcione correctamente:

1. C++ (PPU.cpp) - Levantar la Bandera

En el método `step()`, cuando la PPU alcanza V-Blank (línea 144), se levanta la bandera:

// Si llegamos a V-Blank (línea 144), solicitar interrupción y marcar frame listo
if (ly_ == VBLANK_START) {
    // ... código de interrupción V-Blank ...
    
    // CRÍTICO: Marcar frame como listo para renderizar
    frame_ready_ = true;
}

2. C++ (PPU.cpp) - Consultar y Resetear la Bandera

El método `get_frame_ready_and_reset()` implementa el patrón de "máquina de estados de un solo uso":

bool PPU::get_frame_ready_and_reset() {
    if (frame_ready_) {
        frame_ready_ = false;
        return true;
    }
    return false;
}

3. Cython (ppu.pxd) - Declaración

Se actualizó la declaración en el archivo `.pxd` para exponer el método a Cython:

cdef cppclass PPU:
    # ... otros métodos ...
    bool get_frame_ready_and_reset()

4. Cython (ppu.pyx) - Wrapper Python

Se actualizó el wrapper Cython para exponer el método a Python:

def get_frame_ready_and_reset(self):
    """
    Comprueba si hay un frame listo para renderizar y resetea el flag.
    
    Implementa un patrón de "máquina de estados de un solo uso": si la bandera
    está levantada, la devuelve como true e inmediatamente la baja a false.
    
    Returns:
        True si hay un frame listo para renderizar, False en caso contrario
    """
    return self._ppu.get_frame_ready_and_reset()

5. Python (viboy.py) - Bucle de Renderizado

Se actualizó el bucle principal para usar el nuevo método:

# 3. Renderizado si es V-Blank
if self._ppu is not None:
    # Verificar si hay frame listo (método diferente según core)
    frame_ready = False
    if self._use_cpp:
        frame_ready = self._ppu.get_frame_ready_and_reset()
    else:
        frame_ready = self._ppu.is_frame_ready()  # PPU Python mantiene nombre antiguo
    
    if frame_ready:
        if self._renderer is not None:
            self._renderer.render_frame()
            pygame.display.flip()

Decisiones de Diseño

  • Renombrado del método: Se cambió de `is_frame_ready()` a `get_frame_ready_and_reset()` para hacer explícito que el método tiene efectos secundarios (resetea la bandera). Esto mejora la legibilidad y evita confusiones.
  • Compatibilidad con PPU Python: Se mantuvo el nombre antiguo `is_frame_ready()` para la PPU Python (fallback) para no romper código existente.
  • Patrón de "máquina de estados de un solo uso": Este patrón garantiza que cada fotograma se renderice exactamente una vez, evitando renderizados duplicados o pérdida de sincronización.

Archivos Afectados

  • src/core/cpp/PPU.hpp - Renombrado método `is_frame_ready()` a `get_frame_ready_and_reset()`
  • src/core/cpp/PPU.cpp - Renombrado implementación del método
  • src/core/cython/ppu.pxd - Actualizada declaración Cython
  • src/core/cython/ppu.pyx - Actualizado wrapper Python
  • src/viboy.py - Actualizado bucle de renderizado para usar nuevo método

Tests y Verificación

La verificación se realizó mediante ejecución manual del emulador:

  • Comando ejecutado: python main.py tu_rom.gbc
  • Resultado esperado: La ventana de Pygame debe aparecer y mostrar el contenido del juego
  • Validación: Los logs de "Heartbeat" deben mostrar que `LY` avanza de 0 a 153, y la ventana debe actualizarse a 60 FPS

Validación de módulo compilado C++: Se verificó que el binario `.pyd` se recompile correctamente después de los cambios ejecutando:

python setup.py build_ext --inplace

Diagnóstico: Si el log `[PPU C++] STEP LIVE` aparece en la consola, confirma que el código C++ se está ejecutando. Si la ventana aparece y se actualiza, confirma que la comunicación C++ → Python funciona correctamente.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Patrón de "máquina de estados de un solo uso": Un patrón de diseño donde una bandera booleana se levanta una vez y se baja automáticamente cuando se consulta. Esto garantiza que cada evento se procese exactamente una vez, evitando condiciones de carrera y renderizados duplicados.
  • Comunicación C++ → Python: En una arquitectura híbrida, la comunicación entre el núcleo nativo (C++) y el frontend (Python) requiere un puente explícito. Cython proporciona este puente mediante wrappers que exponen métodos C++ como métodos Python normales.
  • Sincronización de renderizado: El renderizado debe estar desacoplado de las interrupciones hardware. La PPU puede llegar a V-Blank y disparar interrupciones, pero el renderizado debe ocurrir cuando el frontend esté listo, no necesariamente en el mismo ciclo.

Lo que Falta Confirmar

  • Rendimiento del método: Verificar que el overhead de llamar a `get_frame_ready_and_reset()` desde Python no afecte significativamente el rendimiento del bucle principal.
  • Thread-safety futuro: Si en el futuro se implementa threading (por ejemplo, para audio), este patrón deberá ser revisado para garantizar thread-safety.

Hipótesis y Suposiciones

Se asume que el método `get_frame_ready_and_reset()` es lo suficientemente rápido como para no afectar el rendimiento del bucle principal. Esta suposición es razonable porque:

  • El método solo lee y escribe una variable booleana (operación atómica en la mayoría de arquitecturas)
  • Se llama una vez por frame (60 veces por segundo), no en cada ciclo
  • El overhead de la llamada Cython es mínimo comparado con el renderizado completo

Próximos Pasos

  • [ ] Verificar que el renderizado funcione correctamente con ROMs reales
  • [ ] Optimizar el bucle de renderizado si es necesario
  • [ ] Implementar sincronización de audio (APU) cuando corresponda
  • [ ] Considerar implementar threading para audio si el rendimiento lo requiere