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.
DMG VBlank Handler Proof + HRAM[0xFFC5] Semantics + Boot-Skip A/B Test
Resumen
Este Step implementa instrumentación exhaustiva del CPU y MMU para diagnosticar por qué los juegos DMG (específicamente tetris.gb) no muestran contenido visual, a pesar de que los juegos CGB funcionan correctamente. Se añadió tracking detallado de IRQ (VBlank), RETI, HRAM[0xFFC5], y IF/IE, junto con un clasificador DMG v2 mejorado. Los resultados del A/B test (SIM_BOOT_LOGO=0 vs 1) muestran que el problema no está relacionado con el boot logo skip, ya que ambos casos producen resultados idénticos: VRAM_TILEDATA_ZERO, IRQTaken_VBlank=0, HRAM_FFC5_WriteCount=0.
Concepto de Hardware
Interrupciones en Game Boy: Cuando se dispara una interrupción en el Game Boy:
- El CPU verifica si IME está habilitado y si hay bits activos en IF & IE
- Si se cumple, se deshabilita IME, se hace push del PC actual, y se salta al vector correspondiente (0x40 para VBlank, 0x48 para LCD, etc.)
- El handler debe terminar con RETI, que restaura IME y retorna al código interrumpido
El tracking detallado permite verificar que el vector es correcto, el PC se guarda correctamente en el stack, IME se deshabilita correctamente, y el handler termina con RETI.
RETI (Return from Interrupt): RETI es una instrucción especial que hace pop del PC del stack (restaura el PC interrumpido) y habilita IME (permite nuevas interrupciones). Es equivalente a POP PC + EI, pero atómico.
HRAM[0xFFC5]: HRAM[0xFFC5] es una dirección en High RAM (0xFF80-0xFFFE) que algunos juegos DMG usan como flag de sincronización o comunicación entre el código principal y los handlers de interrupción. Si el juego espera que se escriba un valor específico en esta dirección durante el VBlank handler, y no se escribe, el juego podría quedar bloqueado esperando.
IF (Interrupt Flag, 0xFF0F): Registro que indica qué interrupciones están pendientes (bits 0-4: VBlank, LCD, Timer, Serial, Joypad). Se escribe para limpiar flags (bit=1 para limpiar).
IE (Interrupt Enable, 0xFFFF): Registro que indica qué interrupciones están habilitadas (bits 0-4).
Referencia: Pan Docs - Interrupts, LR35902 Instruction Set, Memory Map
Implementación
Fase A: VBlank Handler Proof ✅
A1) IRQTrace Real (Ampliado):
- Ampliación de
IRQTraceEventcon campos adicionales:pc_after: PC después de saltar al vectorvector_addr: Dirección del vector de interrupción (0x40, 0x48, 0x50, 0x58, 0x60)sp_before,sp_after: Stack pointer antes y después del pushime_before,ime_after: Estado de IME antes y después del servicioie,if_before,if_after: Valores de IE e IF antes y despuésirq_type: Tipo de IRQ (VBlank, LCD, Timer, Serial, Joypad)opcode_at_vector: Primer opcode en el vector (para debugging)
- Captura en
CPU::handle_interrupts(): Se capturan todos los campos antes y después del servicio de IRQ
A2) RETI Tracking:
- Nueva estructura
RETITraceEvent:frame: Frame en el que se ejecutó RETIpc: PC donde se ejecutó RETIreturn_addr: Dirección de retorno (leída del stack)ime_after: Estado de IME después de RETI (debe ser 1)sp_before,sp_after: Stack pointer antes y después
- Ring buffer de 64 eventos en
CPU - Captura en opcode
RETI(0xD9): Se capturan todos los campos durante la ejecución
A3) HRAM[0xFFC5] "Flag Semantics":
- Ampliación de
HRAMFFC5Tracking:write_count_total: Total de writes a 0xFFC5write_count_in_irq_vblank: Writes durante IRQ VBlank (placeholder por ahora)first_write_frame: Frame del primer write- Ring buffer
FFC5WriteEvent(últimos 8 writes):frame,pc,value
- Captura en
MMU::write()cuandoaddr == 0xFFC5
A5) IF/IE Correctness Proof:
- Ampliación de
IFIETracking:if_write_history_: Ring buffer de últimos 5 writes a IF (0xFF0F)ie_write_history_: Ring buffer de últimos 5 writes a IE (0xFFFF)- Cada entrada contiene:
pc,written(valor escrito),applied(valor aplicado después de write)
- Captura en
MMU::write()cuandoaddr == 0xFF0Foaddr == 0xFFFF
Fase B: DMG Progress Proof ✅
B1) "AfterClear+Progress" Snapshot:
- Nueva función
_classify_dmg_quick_v2():- Clasifica el estado DMG usando métricas del CPU y MMU:
pc_hotspot_top1: PC más frecuente (indica loops)irq_taken_vblank: Contador de IRQs VBlank tomados (del nuevo tracking)reti_count: Contador de RETI ejecutadoshram_ffc5_last_value,hram_ffc5_write_count_total,hram_ffc5_write_count_in_vblank: Métricas de HRAM[0xFFC5]lcdc,stat,ly: Estado del LCDvram_tiledata_nz,vram_tilemap_nz: Bytes no-cero en VRAM
- Categorías de clasificación:
WAITING_ON_FFC5: HRAM[0xFFC5] nunca escrito, juego esperandoIRQ_TAKEN_BUT_NO_RETI: IRQ tomado pero no hay RETIIRQ_OK_BUT_FLAG_NOT_SET: IRQ y RETI OK, pero flag no se escribeVRAM_TILEDATA_ZERO: VRAM tiledata vacía (causa raíz)OK_BUT_WHITE: Todo OK pero framebuffer blanco
- Clasifica el estado DMG usando métricas del CPU y MMU:
- Integración en
generate_snapshot(): Se añade secciónDMGQuickClassifierV2al snapshot
Fase C: Cython Exposure ✅
Archivos: src/core/cython/cpu.pxd, src/core/cython/cpu.pyx, src/core/cython/mmu.pyx
cpu.pxd: Declaración deRETITraceEventstructcpu.pyx: Métodosget_reti_trace_ring()yget_reti_count()para exponer tracking de RETImmu.pyx: Actualización deget_hram_ffc5_tracking()yget_if_ie_tracking()para incluir los nuevos campos (write_ring, if_write_history, ie_write_history)
Archivos Afectados
src/core/cpp/CPU.hpp- Ampliación deIRQTraceEvent, nueva estructuraRETITraceEvent, nuevos métodosget_reti_trace_ring()yget_reti_count()src/core/cpp/CPU.cpp- Implementación de tracking ampliado de IRQ y RETIsrc/core/cpp/MMU.hpp- Ampliación deHRAMFFC5TrackingyIFIETrackingcon ring bufferssrc/core/cpp/MMU.cpp- Implementación de tracking ampliado de HRAM[0xFFC5] e IF/IEsrc/core/cython/cpu.pxd- Declaración deRETITraceEventsrc/core/cython/cpu.pyx- Métodos para exponer tracking de RETIsrc/core/cython/mmu.pyx- Actualización de métodos para exponer tracking ampliadotools/rom_smoke_0442.py- Nueva función_classify_dmg_quick_v2()e integración en snapshot
Tests y Verificación
Compilación:
python setup.py build_ext --inplace
✅ Compilación exitosa sin errores
Ejecución de ROMs:
# tetris.gb (SIM_BOOT_LOGO=0)
export VIBOY_SIM_BOOT_LOGO=0
python3 tools/rom_smoke_0442.py roms/tetris.gb --frames 1200 > /tmp/viboy_0500_tetris_boot0.log
# tetris.gb (SIM_BOOT_LOGO=1)
export VIBOY_SIM_BOOT_LOGO=1
python3 tools/rom_smoke_0442.py roms/tetris.gb --frames 1200 > /tmp/viboy_0500_tetris_boot1.log
# pkmn.gb (SIM_BOOT_LOGO=0)
export VIBOY_SIM_BOOT_LOGO=0
python3 tools/rom_smoke_0442.py roms/pkmn.gb --frames 1200 > /tmp/viboy_0500_pkmn.log
Resultados del A/B Test (tetris.gb):
- SIM_BOOT_LOGO=0:
DMGQuickClassifier=VRAM_TILEDATA_ZEROVBlankReq=1139,VBlankServ=1139(IRQs se procesan)IRQTaken_VBlank=0(⚠️ tracking no detecta IRQs)HRAM_FFC5_WriteCount=0(nunca se escribe)VRAM_Regions_TiledataNZ=0,VRAM_Regions_TilemapNZ=1024(tilemap OK, tiledata vacía)
- SIM_BOOT_LOGO=1:
- Resultados idénticos a SIM_BOOT_LOGO=0
- Conclusión: El problema NO está relacionado con el boot logo skip
Resultados de pkmn.gb:
DMGQuickClassifier=VRAM_TILEDATA_ZEROVBlankReq=1141,VBlankServ=147(menos IRQs servidos)IRQTaken_VBlank=0(⚠️ tracking no detecta IRQs)VRAM_Regions_TiledataNZ=0,VRAM_Regions_TilemapNZ=2048(tilemap OK, tiledata vacía)
Hallazgos clave:
- ✅ A/B Test: Ambos casos (SIM_BOOT_LOGO=0 y 1) producen resultados idénticos → El problema no está en el boot logo skip
- ⚠️ IRQ Tracking Bug:
IRQTaken_VBlank=0peroVBlankServ=1139→ El tracking nuevo no está funcionando correctamente (posible bug) - 🔍 VRAM Tiledata: Consistente en todas las ROMs →
VRAM_Regions_TiledataNZ=0(causa raíz) - ✅ HRAM[0xFFC5]: Nunca se escribe en tetris.gb → No es la causa del bloqueo
Fuentes Consultadas
- Pan Docs: Interrupts, LR35902 Instruction Set, Memory Map, VRAM Access Restrictions
- GBEDG: Interrupt Handling, VBlank Timing
Integridad Educativa
Lo que Entiendo Ahora
- IRQ Tracking: El tracking detallado de IRQ permite verificar que los handlers se ejecutan correctamente, pero hay un bug en la implementación del contador
irq_taken_vblank_. - RETI Tracking: El tracking de RETI permite verificar que los handlers terminan correctamente, pero necesitamos verificar que los datos se están capturando correctamente.
- HRAM[0xFFC5]: Algunos juegos DMG usan esta dirección como flag, pero tetris.gb no la usa, por lo que no es la causa del bloqueo.
- A/B Test: El boot logo skip no afecta el comportamiento del juego DMG, lo que sugiere que el problema está en la emulación del hardware, no en el estado inicial.
Lo que Falta Confirmar
- IRQ Tracking Bug: Por qué
IRQTaken_VBlank=0cuandoVBlankServ=1139. Necesitamos verificar la implementación del contador. - VRAM Tiledata: Por qué los tiles no se cargan en VRAM. Necesitamos instrumentar los writes a VRAM tiledata y verificar restricciones de acceso.
- Timing: Si el timing de acceso a VRAM está causando que los writes fallen silenciosamente.
Hipótesis y Suposiciones
Suposición: El tracking de IRQTaken_VBlank debería actualizarse en CPU::handle_interrupts(), pero no lo hace. Esto podría ser un bug en la implementación.
Hipótesis: Los tiles no se cargan porque:
- El timing de acceso a VRAM está desincronizado
- O hay restricciones de acceso a VRAM que no estamos respetando
- O el MBC no está mapeando correctamente la ROM durante la carga
Próximos Pasos
- [ ] Fix IRQ Tracking Bug: Verificar e implementar correctamente el contador
irq_taken_vblank_enCPU::handle_interrupts() - [ ] VRAM Write Tracking: Instrumentar tracking de writes a VRAM tiledata (0x8000-0x97FF) para verificar si hay intentos de write que fallan
- [ ] VRAM Access Restrictions: Verificar que estamos respetando las restricciones de acceso a VRAM (solo durante HBlank y VBlank, no durante OAM Scan y Pixel Transfer)
- [ ] DMA Tracking: Instrumentar tracking de DMA (0xFF46) para verificar si hay transferencias de datos que no se completan correctamente
- [ ] Timing Verification: Verificar que el timing de acceso a VRAM coincide con el hardware real (modos PPU, ciclos de máquina)