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

El Ciclo de Vida del Framebuffer: Limpieza de Fotogramas

Fecha: 2025-12-21 Step ID: 0199 Estado: ✅ VERIFIED

Resumen

El diagnóstico del Step 0198 ha revelado un fallo arquitectónico crítico: el framebuffer en C++ nunca se limpia. Tras el primer fotograma, cuando el juego apaga el renderizado del fondo (LCDC=0x80), nuestra PPU obedece correctamente y deja de dibujar, pero el framebuffer conserva los datos "fantasma" del fotograma anterior, que se muestran indefinidamente creando artefactos visuales.

Este Step implementa la solución profesional: un método clear_framebuffer() en la PPU de C++ que se llama desde el orquestador de Python al inicio de cada fotograma, asegurando que cada renderizado comience desde un estado limpio. Esta es una práctica estándar de gráficos por ordenador conocida como "Back Buffer Clearing".

Concepto de Hardware: El Back Buffer y el Ciclo de Vida del Framebuffer

En gráficos por ordenador, es una práctica estándar limpiar el "back buffer" (nuestro framebuffer) a un color de fondo predeterminado antes de dibujar un nuevo fotograma. Aunque el hardware real de la Game Boy lo hace implícitamente al redibujar cada píxel basándose en la VRAM actual en cada ciclo de pantalla, nuestro modelo de emulación simplificado, que no redibuja si el fondo está apagado, debe realizar esta limpieza de forma explícita.

El Problema del "Fantasma":

  • En el Step 0198, restauramos la precisión del hardware: la PPU solo renderiza si el Bit 0 del LCDC está activo.
  • Cuando el juego de Tetris muestra el logo de Nintendo, activa el fondo (LCDC=0x91) y la PPU renderiza correctamente el primer fotograma.
  • Después, el juego apaga el fondo (LCDC=0x80) para preparar la siguiente pantalla (probablemente copiando nuevos gráficos a la VRAM).
  • Nuestra PPU, ahora precisa, ve que el fondo está apagado y retorna inmediatamente desde render_scanline() sin dibujar nada.
  • El problema: El framebuffer nunca se limpia. Mantiene los datos del primer fotograma (el logo) indefinidamente.
  • Cuando el juego modifica la VRAM para preparar la siguiente pantalla, estos cambios se reflejan parcialmente en el framebuffer, creando una mezcla "fantasma" de datos antiguos y nuevos.

La Solución: Implementar un ciclo de vida explícito del framebuffer. Al inicio de cada fotograma, antes de que la CPU comience a ejecutar los ciclos, limpiamos el framebuffer estableciendo todos los píxeles a índice 0 (blanco en la paleta por defecto). Esto garantiza que cada renderizado comienza desde un lienzo limpio, tal como ocurre en el hardware real.

Implementación

Este Step implementa el método clear_framebuffer() en tres capas: C++, Cython y Python.

1. Método en PPU de C++

Se añade la declaración pública en PPU.hpp:

/**
 * Limpia el framebuffer, estableciendo todos los píxeles a índice 0 (blanco por defecto).
 * 
 * Este método debe llamarse al inicio de cada fotograma para asegurar que el
 * renderizado comienza desde un estado limpio. En hardware real, esto ocurre
 * implícitamente porque cada píxel se redibuja en cada ciclo, pero en nuestro
 * modelo de emulación, cuando el fondo está apagado (LCDC bit 0 = 0), no se
 * renderiza nada y el framebuffer conserva los datos del fotograma anterior.
 * 
 * Fuente: Práctica estándar de gráficos por ordenador (Back Buffer Clearing).
 */
void clear_framebuffer();

Y su implementación en PPU.cpp:

void PPU::clear_framebuffer() {
    // Rellena el framebuffer con el índice de color 0 (blanco en la paleta por defecto)
    std::fill(framebuffer_.begin(), framebuffer_.end(), 0);
}

Se requiere incluir <algorithm> para usar std::fill, que es altamente optimizado para este tipo de operaciones.

2. Exposición a través de Cython

Se añade la declaración en ppu.pxd:

void clear_framebuffer()

Y el wrapper en ppu.pyx:

def clear_framebuffer(self):
    """
    Limpia el framebuffer, estableciendo todos los píxeles a índice 0 (blanco por defecto).
    
    Este método debe llamarse al inicio de cada fotograma para asegurar que el
    renderizado comienza desde un estado limpio.
    """
    if self._ppu == NULL:
        return
    self._ppu.clear_framebuffer()

3. Integración en el Orquestador de Python

En viboy.py, dentro del método run(), se añade la llamada al inicio del bucle de fotogramas:

while self.running:
    # --- Step 0199: Limpiar el framebuffer al inicio de cada fotograma ---
    # Esto asegura que cada renderizado comienza desde un estado limpio.
    # Cuando el juego apaga el fondo (LCDC bit 0 = 0), la PPU no dibuja nada
    # pero el framebuffer debe estar limpio para evitar artefactos "fantasma".
    if self._use_cpp and self._ppu is not None:
        self._ppu.clear_framebuffer()
    
    # --- Bucle de Frame Completo (154 scanlines) ---
    for line in range(SCANLINES_PER_FRAME):
        # ... resto del bucle ...

Decisiones de Diseño

¿Por qué limpiar al inicio del fotograma y no al final? Limpiar al inicio garantiza que, incluso si ocurre un error o el juego no renderiza nada en un fotograma, la pantalla mostrará un estado limpio (blanco) en lugar de artefactos del fotograma anterior. Es el patrón estándar en renderizado de gráficos.

¿Por qué usar std::fill en lugar de un bucle manual? std::fill está altamente optimizado por el compilador y puede generar código vectorizado (SIMD) que es mucho más rápido que un bucle manual, especialmente para un buffer de 23040 bytes.

¿Por qué índice 0 (blanco) y no otro valor? El índice 0 corresponde al color más claro en la paleta estándar del Game Boy. Es el valor "neutro" que garantiza que, si no se renderiza nada, veremos un fondo limpio y uniforme en lugar de ruido visual.

Archivos Afectados

  • src/core/cpp/PPU.hpp - Añadida declaración del método clear_framebuffer()
  • src/core/cpp/PPU.cpp - Añadida implementación de clear_framebuffer() e include de <algorithm>
  • src/core/cython/ppu.pxd - Añadida declaración del método para Cython
  • src/core/cython/ppu.pyx - Añadido wrapper Python para clear_framebuffer()
  • src/viboy.py - Añadida llamada a clear_framebuffer() al inicio del bucle de fotogramas
  • docs/bitacora/entries/2025-12-21__0199__ciclo-vida-framebuffer-limpieza-fotogramas.html - Nueva entrada de bitácora
  • docs/bitacora/index.html - Actualizado con la nueva entrada
  • INFORME_FASE_2.md - Actualizado con el Step 0199

Tests y Verificación

La validación de este cambio es visual y funcional:

  1. Recompilación del módulo C++:
    python setup.py build_ext --inplace
    # O usando el script de PowerShell:
    .\rebuild_cpp.ps1
    Compilación exitosa sin errores ni warnings.
  2. Ejecución del emulador:
    python main.py roms/tetris.gb
  3. Resultado Esperado:
    • Frame 1: LCDC=0x91. La PPU renderiza el logo de Nintendo. Python lo muestra correctamente.
    • Frame 2 (y siguientes):
      • clear_framebuffer() pone todo el buffer a 0 (blanco).
      • El juego pone LCDC=0x80 (apaga el fondo).
      • Nuestra PPU, ahora precisa, ve que el fondo está apagado y no dibuja nada.
      • Python lee el framebuffer, que está lleno de ceros (blanco).
    • El resultado CORRECTO es una PANTALLA EN BLANCO.

Nota Importante: Una pantalla en blanco puede parecer un paso atrás, ¡pero es un salto adelante en precisión! Confirma que:

  • Nuestro ciclo de vida del framebuffer es correcto: cada fotograma comienza con un lienzo limpio.
  • Nuestra PPU obedece al hardware: cuando el juego apaga el fondo, no renderiza nada.
  • Una vez que el juego avance y active el fondo para la pantalla de título, la veremos aparecer sobre este lienzo blanco y limpio, sin artefactos "fantasma".

Fuentes Consultadas

  • Pan Docs: LCD Control Register (LCDC) - Bits de control del LCD, incluyendo Bit 0 (BG Display Enable)
  • Práctica estándar de gráficos por ordenador: "Back Buffer Clearing" - Patrón común en renderizado de gráficos para evitar artefactos visuales entre fotogramas

Integridad Educativa

Lo que Entiendo Ahora

  • El Ciclo de Vida del Framebuffer: En gráficos por ordenador, es esencial limpiar el back buffer antes de cada nuevo fotograma para evitar artefactos visuales. Esto es especialmente crítico cuando el renderizado es condicional (como en nuestro caso, donde solo renderizamos si el fondo está activo).
  • La Diferencia entre Hardware Real y Emulación: El hardware real de la Game Boy redibuja cada píxel en cada ciclo basándose en la VRAM actual, por lo que la "limpieza" ocurre implícitamente. En nuestra emulación, donde podemos omitir el renderizado si el fondo está apagado, debemos realizar esta limpieza de forma explícita.
  • El "Fantasma en el Framebuffer": Cuando el framebuffer no se limpia y el renderizado se omite, los datos del fotograma anterior permanecen visibles, creando artefactos visuales que se mezclan con los nuevos datos cuando el juego modifica la VRAM.
  • Optimización con std::fill: Usar funciones de la biblioteca estándar como std::fill permite al compilador generar código altamente optimizado, posiblemente usando instrucciones vectorizadas (SIMD) que son mucho más rápidas que un bucle manual.

Lo que Falta Confirmar

  • Comportamiento del Hardware Real: Cuando el Bit 0 del LCDC está apagado en una Game Boy real, ¿qué se muestra en la pantalla? ¿Una pantalla en blanco, o los datos de VRAM anteriores? Esto requiere verificación empírica o documentación más detallada.
  • Momentos de Activación del Fondo: ¿En qué momentos exactos del ciclo de vida de un juego típico (como Tetris) se activa y desactiva el Bit 0 del LCDC? Esto nos ayudaría a entender mejor el flujo de renderizado y preparar mejor nuestros tests.

Hipótesis y Suposiciones

Suposición: Asumimos que establecer el framebuffer a índice 0 (blanco) es el comportamiento correcto cuando no se renderiza nada. En el hardware real, cuando el Bit 0 del LCDC está apagado, la pantalla podría mostrar algo diferente (por ejemplo, podría estar "apagada" o mostrar datos residuales). Sin embargo, para nuestra emulación, un fondo blanco limpio es el comportamiento más razonable y profesional.

Próximos Pasos

  • [ ] Verificar el comportamiento del emulador cuando el juego activa el fondo nuevamente para mostrar la pantalla de título de Tetris
  • [ ] Confirmar que la pantalla en blanco es el comportamiento correcto cuando el Bit 0 del LCDC está apagado
  • [ ] Investigar si hay algún registro o bit adicional que controle qué se muestra cuando el fondo está desactivado
  • [ ] Continuar con la implementación de características adicionales de la PPU (Window, Sprites, etc.)