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

La Foto Finish: Snapshot de Memoria

Fecha: 2025-12-22 Step ID: 0219 Estado: Draft

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 memoryview o el objeto C++ se destruye, el memoryview puede 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étodo run() - Captura del snapshot usando bytearray y paso al renderizador
  • src/gpu/renderer.py: Método render_frame() - Acepta parámetro opcional framebuffer_data

Cambios técnicos

1. Modificación en viboy.py:

  • Se reemplazó la verificación de current_ly == 144 por get_frame_ready_and_reset(), que es más robusto y maneja correctamente el estado del frame.
  • Se cambió la copia de bytes(fb_view) a bytearray(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 = None al método render_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étodo run() para captura de snapshot (líneas 753-789)
  • src/gpu/renderer.py - Modificación del método render_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 - bytearray and memoryview
  • Cython Documentation - Memory Views and Zero-Copy