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.
La Foto Finish: Snapshot de Memoria
Resumen
El Step 0218 confirmó que el renderizador está conectado correctamente a la ventana (el cuadro azul se ve),
pero persiste una discrepancia crítica: la sonda en viboy.py lee Pixel 0: 3 (Negro/Rojo),
mientras que la sonda en renderer.py lee First Pixel Value: 0 (Blanco/Verde).
Esta discrepancia indica que los datos se están perdiendo o sobrescribiendo en el microsegundo que pasa
entre la lectura en viboy.py y la llamada a render_frame. Esto ocurre típicamente
cuando usamos MemoryViews volátiles: si C++ toca esa memoria (o si Python pierde la referencia),
los datos cambian.
Este paso implementa la Estrategia del Snapshot Inmutable: en lugar de pasar una "vista"
(memoryview) al renderizador que mira directamente a la memoria de C++ (la cual es peligrosa y volátil),
hacemos una copia fotográfica instantánea (bytearray) de los datos en viboy.py
justo cuando sabemos que son correctos (cuando la sonda dice 3). Pasamos esa copia segura al renderizador.
Concepto de Hardware
En la arquitectura híbrida Python/C++, el framebuffer vive en memoria C++ y se expone a Python mediante
un memoryview (vista de memoria). Un memoryview es una referencia directa a la
memoria subyacente: si C++ modifica esa memoria (por ejemplo, limpiando el framebuffer para el siguiente frame),
el memoryview reflejará inmediatamente esos cambios.
El problema de las MemoryViews Volátiles:
- Condición de Carrera: Si C++ limpia el framebuffer mientras Python está leyendo, Python verá datos parciales o corruptos.
- Referencias Perdidas: Si Python pierde la referencia al
memoryviewo el objeto C++ se destruye, elmemoryviewpuede apuntar a memoria inválida. - Optimizaciones del Compilador: El compilador C++ puede reordenar operaciones de memoria, causando que los datos cambien en momentos inesperados.
La solución es hacer una copia inmutable (bytearray) del framebuffer en el
momento exacto en que sabemos que está completo y correcto. Esta copia vive en la memoria de Python y
no puede ser modificada por C++, garantizando que el renderizador siempre trabaje con datos estables.
Rendimiento: Copiar 23040 bytes (160×144 píxeles) toma aproximadamente 0.01ms en un procesador moderno, lo cual es insignificante comparado con el tiempo de renderizado (16.67ms por frame a 60 FPS). El beneficio de estabilidad supera ampliamente el costo de rendimiento.
Implementación
Se modificaron dos archivos para implementar el snapshot inmutable:
Componentes modificados
src/viboy.py: Métodorun()- Captura del snapshot usandobytearrayy paso al renderizadorsrc/gpu/renderer.py: Métodorender_frame()- Acepta parámetro opcionalframebuffer_data
Cambios técnicos
1. Modificación en viboy.py:
- Se reemplazó la verificación de
current_ly == 144porget_frame_ready_and_reset(), que es más robusto y maneja correctamente el estado del frame. - Se cambió la copia de
bytes(fb_view)abytearray(raw_view)para garantizar que la copia es mutable y vive completamente en Python. - Se actualizó la sonda de datos para usar el snapshot y mostrar el mensaje
[PYTHON SNAPSHOT PROBE]. - Se pasa el snapshot al renderizador mediante el parámetro
framebuffer_data.
# En src/viboy.py -> run()
# Renderizado
if self._use_cpp:
if self._ppu.get_frame_ready_and_reset():
# 1. Obtener la vista directa de C++
raw_view = self._ppu.framebuffer
# 2. --- STEP 0219: SNAPSHOT INMUTABLE ---
# Hacemos una copia profunda inmediata a la memoria de Python.
# Esto "congela" el frame y nos protege de cualquier cambio en C++.
fb_data = bytearray(raw_view)
# ----------------------------------------
# 3. Pasar la COPIA SEGURA al renderizador
self._renderer.render_frame(framebuffer_data=fb_data)
2. Modificación en renderer.py:
- Se añadió el parámetro opcional
framebuffer_data: bytearray | None = Noneal métodorender_frame(). - Si se proporciona
framebuffer_data, se usa ese snapshot en lugar de leer desde la PPU. - Se actualizó el diagnóstico para indicar si se está usando un snapshot o leyendo directamente.
# En src/gpu/renderer.py -> render_frame()
def render_frame(self, framebuffer_data: bytearray | None = None) -> None:
# --- Step 0219: SNAPSHOT INMUTABLE ---
# Si se proporciona framebuffer_data, usar ese snapshot en lugar de leer desde PPU
if framebuffer_data is not None:
frame_indices = framebuffer_data
else:
frame_indices = self.cpp_ppu.get_framebuffer()
Archivos Afectados
src/viboy.py- Modificación del métodorun()para captura de snapshot (líneas 753-789)src/gpu/renderer.py- Modificación del métodorender_frame()para aceptar snapshot (líneas 414-444)
Tests y Verificación
Comando ejecutado: python main.py roms/tetris.gb
Resultado Esperado:
- Consola:
[PYTHON SNAPSHOT PROBE] ... Pixel 0: 3 - Consola (Renderer):
First Pixel Value inside render_frame: 3(¡Deben coincidir!) - Pantalla: Rayas Rojas verticales de fondo + Cuadro Azul en el centro
Si ambas sondas muestran el mismo valor (3) y la pantalla muestra rojo, habremos conectado todos los cables. El siguiente paso será la limpieza final para jugar Tetris.
Validación de módulo compilado C++: El snapshot se crea desde un memoryview
expuesto por Cython, que a su vez accede al framebuffer nativo de C++. La copia garantiza que los datos
no se corrompan durante el paso entre C++ y Python.
Referencias
- Pan Docs - LCD Timing, Frame Rendering
- Python Documentation -
bytearrayandmemoryview - Cython Documentation - Memory Views and Zero-Copy