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
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:
- Semántica de
get_frame_ready_and_reset()/ swap de buffers: El reset/swap deja leyendo el buffer equivocado o limpiado - El frame "ready" no está siendo real:
run_one_frame()esperaget_frame_ready_and_reset()pero jamás llega - 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:
- Step hasta que
is_frame_ready()devuelvaTrue(sin resetear) - Leer framebuffer ANTES de reset (
buf_pre) - Llamar a
get_frame_ready_and_reset()(hace swap) - Leer framebuffer DESPUÉS de reset (
buf_post) - 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étodois_frame_ready() constsrc/core/cpp/PPU.cpp- Implementadois_frame_ready()y corregido error de compilación (tile_map_offsetno declarado)src/core/cython/ppu.pxd- Añadida declaración deis_frame_ready() constsrc/core/cython/ppu.pyx- Añadido wrapper Python deis_frame_ready()tests/test_bg_tilemap_base_and_scroll_0464.py- Modificadotest_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 esconst, 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)