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.
Frame-Ready + VRAM Address Sanity + Buffer Swap
Resumen
Corrección crítica de la semántica de MMU::read_vram(): el método espera direcciones absolutas (0x8000-0x9FFF), pero el PPU estaba pasando offsets (calculados como tile_map_base - 0x8000). Esto causaba lecturas fuera de rango que devolvían 0xFF o 0, resultando en tiles vacíos y framebuffer en 0. Se corrigieron todas las llamadas a read_vram() en PPU.cpp para pasar direcciones absolutas, se modificó run_one_frame() para usar get_frame_ready_and_reset() en lugar de ciclos fijos, y se añadieron sanity checks de VRAM en los tests. Resultado: ✅ Correcciones aplicadas. ⚠️ Tests aún fallan - framebuffer devuelve 0 (requiere más investigación sobre timing/renderizado).
Concepto de Hardware
Problema identificado: El framebuffer en tests sale todo en 0, a pesar de que se escriben tiles correctamente. El plan identificó 4 posibles causas:
- (A) read_vram() usado con addr absoluta vs offset - Más probable:
MMU::read_vram()espera dirección absoluta (0x8000-0x9FFF), pero PPU pasaba offsets - (B) frame timing / frame_ready - Tests usaban 70224 ciclos fijos en lugar de esperar
frame_ready - (C) front/back swap o getter equivocado -
get_framebuffer_indices()podría devolver buffer incorrecto - (D) BG realmente no renderizando - Condición de estado (LCDC/STAT) incorrecta
Semántica de MMU::read_vram(): El método espera una dirección absoluta en el rango 0x8000-0x9FFF. Internamente calcula el offset: offset = addr - 0x8000. Si se pasa un offset directamente (ej: 0x1800 para 0x9800), el método intenta leer desde 0x8000 + 0x1800 = 0x9800, pero la validación if (addr < 0x8000 || addr > 0x9FFF) falla si el offset es menor que 0x8000, devolviendo 0xFF.
Frame-Ready vs Ciclos Fijos: Usar 70224 ciclos como verdad universal es incorrecto porque el timing real depende del estado del PPU. Es mejor usar get_frame_ready_and_reset() que devuelve True cuando LY pasa de 143 a 144 (inicio de V-Blank), indicando que un frame completo se ha renderizado.
Referencia: Pan Docs - VRAM Access, PPU Modes, Frame Timing. Step 0123 - Comunicación frame-ready C++-Python.
Implementación
El fix se implementó en cinco fases según el plan:
Fase A: Frame-Ready en lugar de Ciclos Fijos
Se modificó run_one_frame() para usar get_frame_ready_and_reset():
def run_one_frame(self):
"""Helper: Ejecutar hasta que PPU declare frame listo.
No usa 70224 como verdad universal. Step hasta que frame_ready == True.
Pone un cap (máximo 4 frames-worth) para evitar loops infinitos.
"""
max_cycles = 70224 * 4 # Cap: máximo 4 frames-worth
cycles_accumulated = 0
frame_ready = False
while not frame_ready and cycles_accumulated < max_cycles:
cycles = self.cpu.step()
cycles_accumulated += cycles
self.timer.step(cycles)
self.ppu.step(cycles)
# Verificar si hay frame listo
frame_ready = self.ppu.get_frame_ready_and_reset()
# Assert que se completó un frame
assert frame_ready, \
f"Frame no se completó después de {cycles_accumulated} ciclos (máximo {max_cycles})"
return cycles_accumulated
Fase B: Sanity Checks de VRAM en Tests
Se añadieron verificaciones de VRAM usando read_raw() antes de renderizar:
# Sanity check: Verificar que VRAM contiene lo escrito (usando read_raw)
assert self.mmu.read_raw(0x8000) == 0x55, \
f"Tile 0 byte1 en 0x8000 debe ser 0x55, es 0x{self.mmu.read_raw(0x8000):02X}"
assert self.mmu.read_raw(0x8001) == 0x33, \
f"Tile 0 byte2 en 0x8001 debe ser 0x33, es 0x{self.mmu.read_raw(0x8001):02X}"
assert self.mmu.read_raw(0x9800) == 0x00, \
f"Tilemap 0x9800[0] debe ser 0x00, es 0x{self.mmu.read_raw(0x9800):02X}"
assert self.mmu.read_raw(0x9C00) == 0x01, \
f"Tilemap 0x9C00[0] debe ser 0x01, es 0x{self.mmu.read_raw(0x9C00):02X}"
También se añadió verificación de que el framebuffer no está todo en 0:
# Verificar que no está todo en 0
non_zero_count = sum(1 for i in range(160 * 144) if (indices[i] & 0x03) != 0)
assert non_zero_count > 0, \
f"Framebuffer está todo en 0 ({non_zero_count} píxeles no-cero de {160*144})"
Fase C: Corregir Semántica de read_vram() - CRÍTICO
Se corrigieron todas las llamadas a read_vram() en PPU.cpp para pasar direcciones absolutas (no offsets). Se encontraron y corrigieron 12+ ocurrencias:
// ANTES (INCORRECTO - pasa offset):
uint16_t tile_map_offset = (tile_map_base - 0x8000) + i;
if (tile_map_offset < 0x2000) {
tile_ids_sample[i] = mmu_->read_vram(tile_map_offset); // ❌ Offset
}
// DESPUÉS (CORRECTO - pasa addr absoluta):
uint16_t tile_map_addr = tile_map_base + i;
if (tile_map_addr >= 0x8000 && tile_map_addr <= 0x9FFF) {
tile_ids_sample[i] = mmu_->read_vram(tile_map_addr); // ✅ Addr absoluta
}
Lugares corregidos:
- Línea 2198-2200: Diagnóstico de tilemap (muestra de 16 tile IDs)
- Línea 2256-2259: Verificación inmediata de tilemap cuando hay tiles
- Línea 2341-2344: Análisis de correspondencia tilemap-tiles
- Línea 2402-2405: Verificación de tile IDs en correspondencia
- Línea 2436-2439: Verificación de tilemap (primeros 4 tiles)
- Línea 2479-2482: Inspección de tilemap (primeras 32 bytes)
- Línea 2493-2496: Checksum de tilemap
- Línea 2549-2552: Diagnóstico frame 676
- Línea 2580-2583: Verificación siempre activa
- Línea 2617-2620: Dump visual de tilemap
- Línea 2846-2849: Renderizado de BG (crítico - código de producción)
- Línea 3065-3068: Verificación de tile addr
- Línea 3097-3100: Verificación de renderizado con tiles reales
Fase D: Verificar Getter de Framebuffer
Se verificó que get_framebuffer_indices() devuelve el buffer correcto (front post-swap). El método ya estaba correcto: devuelve framebuffer_front_ que es el buffer presentado después del swap.
Fase E: Verificar Estado BG (LCDC/STAT)
Los tests ya configuran correctamente LCDC (bit 7 = LCD ON, bit 0 = BG ON). No se encontraron problemas de estado.
Archivos Afectados
tests/test_bg_tilemap_base_and_scroll_0464.py- Modificadorun_one_frame()para usarget_frame_ready_and_reset(), añadidos sanity checks de VRAM conread_raw(), y verificación de framebuffer no-cerosrc/core/cpp/PPU.cpp- Corregidas 12+ llamadas aread_vram()para pasar direcciones absolutas (no offsets). Cambios en líneas: 2198-2200, 2256-2259, 2341-2344, 2402-2405, 2436-2439, 2479-2482, 2493-2496, 2549-2552, 2580-2583, 2617-2620, 2846-2849, 3065-3068, 3097-3100
Tests y Verificación
Comando ejecutado: pytest tests/test_bg_tilemap_base_and_scroll_0464.py -v
Resultado: ⚠️ Tests fallan - framebuffer devuelve 0 (0 píxeles no-cero de 23040)
Diagnóstico: Aunque las correcciones de semántica de read_vram() están aplicadas, el framebuffer sigue devolviendo 0. Esto sugiere que el problema puede ser:
- Timing: El frame no se completa correctamente antes de leer el framebuffer
- Renderizado: El PPU no está renderizando el BG por alguna condición no cumplida
- Swap: El swap de buffers no ocurre o ocurre después de leer
Código del Test:
def run_one_frame(self):
"""Helper: Ejecutar hasta que PPU declare frame listo."""
max_cycles = 70224 * 4
cycles_accumulated = 0
frame_ready = False
while not frame_ready and cycles_accumulated < max_cycles:
cycles = self.cpu.step()
cycles_accumulated += cycles
self.timer.step(cycles)
self.ppu.step(cycles)
frame_ready = self.ppu.get_frame_ready_and_reset()
assert frame_ready, f"Frame no se completó después de {cycles_accumulated} ciclos"
return cycles_accumulated
def test_tilemap_base_select_9800(self):
"""Test 1: tilemap base select (0x9800 vs 0x9C00) - Caso 0x9800."""
# ... setup tiles y tilemap ...
# Sanity check: Verificar que VRAM contiene lo escrito
assert self.mmu.read_raw(0x8000) == 0x55
assert self.mmu.read_raw(0x9800) == 0x00
# Correr 1 frame (usar helper que espera frame_ready)
cycles = self.run_one_frame()
# Verificar framebuffer
indices = self.ppu.get_framebuffer_indices()
non_zero_count = sum(1 for i in range(160 * 144) if (indices[i] & 0x03) != 0)
assert non_zero_count > 0, f"Framebuffer está todo en 0"
Validación Nativa: Validación de módulo compilado C++ mediante corrección de semántica de read_vram() en 12+ lugares críticos del código de renderizado.
Resultados
Implementaciones completadas:
- ✅
run_one_frame()modificado para usarget_frame_ready_and_reset()en lugar de ciclos fijos - ✅ Sanity checks de VRAM añadidos en tests (verificación con
read_raw()) - ✅ Todas las llamadas a
read_vram()enPPU.cppcorregidas para pasar direcciones absolutas (12+ lugares) - ✅ Verificación de getter de framebuffer (ya estaba correcto)
- ✅ Verificación de estado BG (LCDC/STAT correcto en tests)
Problemas conocidos:
- ⚠️ Tests aún fallan - framebuffer devuelve 0 (requiere más investigación sobre timing/renderizado)
Causa identificada y corregida: (A) read_vram() usado con addr absoluta vs offset - Todas las llamadas incorrectas fueron corregidas. El problema restante (framebuffer en 0) sugiere que hay otro factor (timing, condiciones de renderizado, etc.) que requiere más investigación.
Próximos Pasos
- Investigar por qué el framebuffer sigue devolviendo 0 después de corregir
read_vram() - Verificar timing del frame: ¿se completa realmente antes de leer el framebuffer?
- Verificar condiciones de renderizado del BG: ¿se cumplen todas las condiciones necesarias?
- Considerar añadir logging gated para diagnosticar el flujo de renderizado