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.
Fix Mínimo y Correcto (Cerrar Bug de API)
Resumen
Corrección del bug de API identificado en Step 0467: get_framebuffer_indices() no "presenta" automáticamente como get_framebuffer(), causando que tests lean el buffer equivocado (front limpio antes del swap en lugar del frame presentado). Se implementó un nuevo getter get_presented_framebuffer_indices_ptr() que garantiza present automático si hay swap pendiente (igual que get_framebuffer_ptr()). Todos los tests 0464 fueron actualizados para usar el nuevo getter y ahora pasan correctamente. ✅ Bug de API cerrado.
Concepto de Hardware
Bug de API Identificado en Step 0467: El problema NO era que BG no renderizara (bg_pixels_written=23040), ni que los bytes leídos fueran incorrectos (last_tile_bytes=[85, 51]). El problema era que get_framebuffer_indices_ptr() NO hacía "present automático" como get_framebuffer_ptr().
Present Automático: Cuando un frame se completa (LY=144), el contenido renderizado está en el back buffer. El swap al front buffer ocurre cuando se llama a get_frame_ready_and_reset(). Sin embargo, para que los tests puedan leer el frame más reciente sin tener que llamar explícitamente a reset, los getters de framebuffer deben hacer "present automático": si hay un swap pendiente (framebuffer_swap_pending_), hacer el swap antes de devolver el puntero.
Diseño Problemático:
get_framebuffer_ptr(): Hace present automático si hay swap pendiente (línea 1389)get_framebuffer_indices_ptr(): NO hace present automático, solo devuelveframebuffer_front_.data()(línea 1420)
Problema: Tener dos getters con contratos distintos sobre cuándo se "presenta" el frame es fuente de bugs. Tests leen el buffer equivocado (front limpio antes del swap en lugar del frame presentado).
Solución: Crear un getter nuevo get_presented_framebuffer_indices_ptr() que garantice present. Este método NO es const porque puede hacer swap (mutar estado), igual que get_framebuffer_ptr().
Referencia: Step 0364 - Doble Buffering. Step 0428 - Present Automático en get_framebuffer_ptr(). Step 0457 - Debug API para tests. Step 0467 - Diagnóstico del bug.
Implementación
Se implementó un nuevo getter "presented" que garantiza present automático, sin romper compatibilidad (el getter antiguo sigue existiendo).
Fase A: Getter "Presented Indices" Explícito
Se añadió get_presented_framebuffer_indices_ptr() en C++:
// En PPU.hpp:
/**
* Step 0468: Getter "presented" para framebuffer indices.
*
* Garantiza que devuelve el último frame presentado (hace present automático
* si hay swap pendiente, igual que get_framebuffer_ptr()).
*
* Contrato: Siempre devuelve el frame más reciente renderizado y presentado.
*
* @return Puntero al framebuffer de índices presentado (23040 bytes)
*/
const uint8_t* get_presented_framebuffer_indices_ptr();
// En PPU.cpp:
const uint8_t* PPU::get_presented_framebuffer_indices_ptr() {
// --- Step 0468: Present automático si hay swap pendiente ---
if (framebuffer_swap_pending_) {
swap_framebuffers();
framebuffer_swap_pending_ = false;
}
// Devolver el buffer front (estable, actualizado con el contenido más reciente)
return framebuffer_front_.data();
}
Nota: Este método NO es const porque puede hacer swap (mutar estado). Esto es consistente con get_framebuffer_ptr() que tampoco es const.
Fase B: Exposición a Python
Se expuso el nuevo método a Python en ppu.pxd y ppu.pyx:
// En ppu.pxd:
const uint8_t* get_presented_framebuffer_indices_ptr() # Step 0468
// En ppu.pyx:
def get_presented_framebuffer_indices(self):
"""
Step 0468: Obtiene el framebuffer de índices presentado.
Garantiza que devuelve el último frame presentado (hace present automático
si hay swap pendiente, igual que get_framebuffer()).
Returns:
bytes de 23040 bytes (160*144), valores 0..3 del frame presentado
"""
if self._ppu == NULL:
return None
cdef const uint8_t* indices_ptr = self._ppu.get_presented_framebuffer_indices_ptr()
if indices_ptr == NULL:
return None
# Crear bytes desde el puntero (23040 bytes = 160*144)
return (indices_ptr)
Fase C: Actualización de Tests 0464
Todos los tests 0464 fueron actualizados para usar get_presented_framebuffer_indices() en lugar de get_framebuffer_indices():
test_tilemap_base_select_9800(): Eliminado experimento pre/post reset (ya no es necesario), usar getter "presented"test_tilemap_base_select_9C00(): Usar getter "presented"test_scx_pixel_scroll_0_to_7(): Usar getter "presented"
Ejemplo de cambio:
# Antes (problemático):
indices = self.ppu.get_framebuffer_indices() # Puede leer front limpio
# Después (correcto):
indices = self.ppu.get_presented_framebuffer_indices() # Garantiza present
Archivos Afectados
src/core/cpp/PPU.hpp- Añadido métodoget_presented_framebuffer_indices_ptr()src/core/cpp/PPU.cpp- Implementadoget_presented_framebuffer_indices_ptr()con present automáticosrc/core/cython/ppu.pxd- Añadida declaración deget_presented_framebuffer_indices_ptr()src/core/cython/ppu.pyx- Añadido wrapper Pythonget_presented_framebuffer_indices()tests/test_bg_tilemap_base_and_scroll_0464.py- Actualizados todos los tests para usarget_presented_framebuffer_indices(), eliminado experimento pre/post reset
Tests y Verificación
Comando ejecutado:
pytest -q tests/test_bg_tilemap_base_and_scroll_0464.py
Resultado: ✅ 3 passed in 1.19s
Tests que pasan:
- ✅
test_tilemap_base_select_9800()- Verifica selección de tilemap base 0x9800 - ✅
test_tilemap_base_select_9C00()- Verifica selección de tilemap base 0x9C00 - ✅
test_scx_pixel_scroll_0_to_7()- Verifica scroll horizontal SCX 0-7
Código del Test:
def test_tilemap_base_select_9800(self):
# ... setup tiles y tilemaps ...
# Correr frame
self.run_one_frame()
# Usar getter "presented" que garantiza present automático
indices = self.ppu.get_presented_framebuffer_indices()
assert indices is not None
assert len(indices) == 23040
# Verificar patrón esperado
row0_start = 0 * 160
expected_p0 = [0, 1, 2, 3, 0, 1, 2, 3]
for i in range(8):
actual_idx = indices[row0_start + i] & 0x03
expected_idx = expected_p0[i]
assert actual_idx == expected_idx
Validación Nativa: ✅ Compilación exitosa. Módulo C++ compilado correctamente. Método get_presented_framebuffer_indices_ptr() expuesto correctamente a Python.
Validación Real (ROMs): Se ejecutaron rom_smoke para tetris.gb, pkmn.gb, tetris_dx.gbc, mario.gbc (240 frames cada uno) y grid UI. Todos completaron sin crashes. Logs muestran diagnóstico PPU-TILEMAP-DIAG cuando está gated.
Fuentes Consultadas
- 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() - Step 0467: Diagnóstico del bug - Evidencias recopiladas (nz_pre=0, nz_post=17280)
Integridad Educativa
Lo que Entiendo Ahora
- Consistencia de API: Todos los getters de framebuffer deben tener el mismo contrato sobre cuándo se "presenta" el frame. Tener getters con contratos distintos es fuente de bugs.
- Present Automático: Los getters que devuelven el framebuffer deben hacer present automático si hay swap pendiente. Esto asegura que Python siempre vea el contenido más reciente sin tener que llamar explícitamente a reset.
- Métodos No-Const: Los métodos que hacen present automático NO pueden ser
constporque mutan el estado (hacen swap). Esto es consistente con el diseño deget_framebuffer_ptr(). - Compatibilidad hacia atrás: Se puede añadir nuevos getters sin romper compatibilidad. El getter antiguo
get_framebuffer_indices_ptr()sigue existiendo (puede ser útil para casos donde se quiere leer sin hacer present).
Lo que Falta Confirmar
- Impacto en otros tests: Verificar si hay otros tests que usen
get_framebuffer_indices()y deban actualizarse aget_presented_framebuffer_indices(). - Uso del getter antiguo: Si hay casos donde se quiere leer sin hacer present, el getter antiguo puede ser útil. Por ahora, todos los tests usan el getter "presented".
Hipótesis y Suposiciones
Hipótesis confirmada: El problema era puramente de API/sincronización de presentación. get_framebuffer_indices_ptr() no hacía present automático como get_framebuffer_ptr(). Con el nuevo getter "presented", todos los tests pasan correctamente.
Decisión de diseño: Se eligió crear un nuevo getter en lugar de modificar el existente para mantener compatibilidad hacia atrás. El getter antiguo puede ser útil para casos donde se quiera leer sin hacer present (aunque por ahora no hay casos conocidos).
Próximos Pasos
- [x] Implementar
get_presented_framebuffer_indices_ptr() - [x] Exponer a Python como
get_presented_framebuffer_indices() - [x] Actualizar tests 0464 para usar el nuevo getter
- [x] Verificar que todos los tests pasan
- [ ] Verificar si hay otros tests que deban actualizarse
- [ ] Continuar con bugs de emulación real (no de infraestructura de test)