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
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étodosrc/core/cython/ppu.pxd- Actualizada declaración Cythonsrc/core/cython/ppu.pyx- Actualizado wrapper Pythonsrc/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
- Pan Docs: LCD Timing, V-Blank
- Documentación Cython: Interfaz C++/Python
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