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

Fix Mínimo y Correcto (Cerrar Bug de API)

Fecha: 2026-01-04 Step ID: 0468 Estado: VERIFIED

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 devuelve framebuffer_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étodo get_presented_framebuffer_indices_ptr()
  • src/core/cpp/PPU.cpp - Implementado get_presented_framebuffer_indices_ptr() con present automático
  • src/core/cython/ppu.pxd - Añadida declaración de get_presented_framebuffer_indices_ptr()
  • src/core/cython/ppu.pyx - Añadido wrapper Python get_presented_framebuffer_indices()
  • tests/test_bg_tilemap_base_and_scroll_0464.py - Actualizados todos los tests para usar get_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 const porque mutan el estado (hacen swap). Esto es consistente con el diseño de get_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 a get_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)