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

Cerrar Framebuffer=0 con Prueba Corta y Concluyente

Fecha: 2026-01-03 Step ID: 0467 Estado: VERIFIED

Resumen

Diagnóstico definitivo del problema de framebuffer en 0 mediante experimento pre/post reset. Se añadió método is_frame_ready() que solo verifica sin resetear, se modificó el test para leer framebuffer ANTES y DESPUÉS de get_frame_ready_and_reset(), y se recopilaron 4 evidencias clave. Resultado: ✅ Causa identificada - get_framebuffer_indices() lee el buffer front limpio ANTES del swap. El framebuffer back tiene los datos correctos (nz_post=17280), pero el front está limpio hasta que se hace el swap. Solución: Añadir present automático a get_framebuffer_indices_ptr() (similar a get_framebuffer_ptr()).

Concepto de Hardware

Problema observado: Después del fix del 0466 (read_vram con addr absoluta), el framebuffer sigue todo en 0. El plan identificó 3 sospechosos:

  1. Semántica de get_frame_ready_and_reset() / swap de buffers: El reset/swap deja leyendo el buffer equivocado o limpiado
  2. El frame "ready" no está siendo real: run_one_frame() espera get_frame_ready_and_reset() pero jamás llega
  3. BG sí corre pero está escribiendo 0: PPU decodifica bitplanes mal o lee bytes equivocados

Doble Buffering en PPU: La PPU usa doble buffering para evitar condiciones de carrera:

  • framebuffer_front_: Buffer que Python lee (público, estable, no se modifica durante renderizado)
  • framebuffer_back_: Buffer donde C++ escribe (privado, se modifica durante renderizado)

Cuando se completa un frame (LY=144), get_frame_ready_and_reset() llama a swap_framebuffers() que intercambia los buffers y limpia el back (que ahora es el antiguo front).

Present Automático: get_framebuffer_ptr() hace "present automático" - si hay un swap pendiente (framebuffer_swap_pending_), hace el swap antes de devolver el puntero. Esto asegura que Python siempre vea el contenido más reciente. Sin embargo, get_framebuffer_indices_ptr() NO hace present automático, por lo que puede devolver el front limpio si se llama antes del swap.

Referencia: Pan Docs - PPU Modes, Frame Timing. Step 0364 - Doble Buffering. Step 0428 - Present Automático.

Implementación

El diagnóstico se implementó en cuatro fases según el plan:

Fase A: Método is_frame_ready() (Solo Verifica, No Resetea)

Se añadió método is_frame_ready() en C++ y Cython que solo verifica el estado de frame_ready_ sin resetearlo:

// En PPU.hpp:
bool is_frame_ready() const;  // Solo verifica, no resetea

// En PPU.cpp:
bool PPU::is_frame_ready() const {
    return frame_ready_;
}

// En ppu.pyx:
def is_frame_ready(self):
    """Verifica si hay frame listo sin resetear."""
    if self._ppu == NULL:
        return False
    return self._ppu.is_frame_ready()

Esto permite leer el framebuffer ANTES de llamar a get_frame_ready_and_reset().

Fase B: Contador de Píxeles BG Escritos (Ya Existía)

El contador bg_pixels_written_count_ ya existía con VIBOY_DEBUG_PPU y está expuesto a Python mediante get_bg_render_stats(). Se usó en el test para verificar que BG sí está escribiendo píxeles.

Fase C: Variables Debug de Últimos Bytes Leídos (Ya Existían)

Las variables last_tile_bytes_read_ y last_tile_addr_read_ ya existían con VIBOY_DEBUG_PPU y están expuestas a Python mediante get_last_tile_bytes_read_info(). Se usaron en el test para verificar qué bytes está leyendo el PPU.

Fase D: Experimento Pre/Post Reset

Se modificó el test test_tilemap_base_select_9800() para:

  1. Step hasta que is_frame_ready() devuelva True (sin resetear)
  2. Leer framebuffer ANTES de reset (buf_pre)
  3. Llamar a get_frame_ready_and_reset() (hace swap)
  4. Leer framebuffer DESPUÉS de reset (buf_post)
  5. Recopilar 4 evidencias: nz_pre, nz_post, bg_pixels_written, last_tile_bytes
# Step hasta que frame esté listo (sin resetear)
while not self.ppu.is_frame_ready() and cycles_accumulated < max_cycles:
    cycles = self.cpu.step()
    cycles_accumulated += cycles
    self.timer.step(cycles)
    self.ppu.step(cycles)

# Leer ANTES de reset
buf_pre = self.ppu.get_framebuffer_indices()
nz_pre = sum(1 for i in range(160 * 144) if (buf_pre[i] & 0x03) != 0)

# Ahora sí resetear
ready = self.ppu.get_frame_ready_and_reset()

# Leer DESPUÉS de reset
buf_post = self.ppu.get_framebuffer_indices()
nz_post = sum(1 for i in range(160 * 144) if (buf_post[i] & 0x03) != 0)

Archivos Afectados

  • src/core/cpp/PPU.hpp - Añadido método is_frame_ready() const
  • src/core/cpp/PPU.cpp - Implementado is_frame_ready() y corregido error de compilación (tile_map_offset no declarado)
  • src/core/cython/ppu.pxd - Añadida declaración de is_frame_ready() const
  • src/core/cython/ppu.pyx - Añadido wrapper Python de is_frame_ready()
  • tests/test_bg_tilemap_base_and_scroll_0464.py - Modificado test_tilemap_base_select_9800() para experimento pre/post reset

Tests y Verificación

Comando ejecutado:

VIBOY_DEBUG_PPU=1 pytest -v tests/test_bg_tilemap_base_and_scroll_0464.py::TestBGTilemapBaseAndScroll::test_tilemap_base_select_9800 -s

Resultado: ✅ Test pasa

Evidencias Recopiladas:

  • nz_pre=0, nz_post=17280: El framebuffer está todo en 0 ANTES del reset, pero tiene datos DESPUÉS del reset. Esto confirma que el problema es que get_framebuffer_indices() lee el buffer front limpio ANTES del swap.
  • row0_pre=[0, 0, 0, 0, 0, 0, 0, 0]: Confirmación de que el buffer pre está vacío.
  • row0_post=[0, 1, 2, 3, 0, 1, 2, 3]: El buffer post tiene el patrón correcto (P0).
  • bg_pixels_written=23040: BG sí está escribiendo píxeles (todos los 23040 píxeles).
  • last_tile_bytes=[85, 51], addr=0x8000, valid=True: Los bytes leídos son correctos (0x55=85, 0x33=51).

Diagnóstico: El problema es que get_framebuffer_indices_ptr() NO hace present automático como get_framebuffer_ptr(). Cuando se llama antes de get_frame_ready_and_reset(), devuelve el front limpio. Después del swap, el front tiene los datos correctos.

Solución Identificada: Añadir present automático a get_framebuffer_indices_ptr() (similar a get_framebuffer_ptr()). Sin embargo, el método es const, por lo que se necesita hacerlo no-const o crear una versión que haga present.

Validación de módulo compilado C++: ✅ Compilación exitosa. Método is_frame_ready() expuesto correctamente a Python.

Fuentes Consultadas

  • Pan Docs: PPU Modes, Frame Timing, V-Blank
  • Step 0364: Doble Buffering en PPU
  • Step 0428: Present Automático en get_framebuffer_ptr()
  • Step 0457: Debug API para tests - get_framebuffer_indices_ptr()

Integridad Educativa

Lo que Entiendo Ahora

  • Doble Buffering: La PPU usa doble buffering para evitar condiciones de carrera. El back buffer se escribe durante el renderizado, y se intercambia con el front cuando se completa un frame.
  • Present Automático: Los métodos que devuelven punteros al framebuffer deben hacer "present automático" - si hay un swap pendiente, hacer el swap antes de devolver el puntero. Esto asegura que Python siempre vea el contenido más reciente.
  • Experimento Pre/Post Reset: Para diagnosticar problemas de timing/buffering, es útil leer el framebuffer ANTES y DESPUÉS de operaciones críticas (como swap/reset) para identificar dónde se pierden los datos.

Lo que Falta Confirmar

  • Fix de get_framebuffer_indices_ptr(): Añadir present automático. El método es const, por lo que se necesita hacerlo no-const o crear una versión que haga present.
  • Impacto en otros tests: Verificar si otros tests se ven afectados por el cambio de get_framebuffer_indices_ptr() a no-const.

Hipótesis y Suposiciones

Hipótesis confirmada: El problema NO es que BG no escriba (bg_pixels_written=23040), ni que los bytes leídos sean incorrectos (last_tile_bytes=[85, 51]). El problema es que get_framebuffer_indices() lee el buffer front limpio ANTES del swap.

Próximos Pasos

  • [ ] Añadir present automático a get_framebuffer_indices_ptr() (hacerlo no-const o crear versión que haga present)
  • [ ] Verificar que todos los tests pasan después del fix
  • [ ] Documentar el cambio en el código (comentarios explicando por qué se hace present automático)