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

DMG VBlank Handler Proof + HRAM[0xFFC5] Semantics + Boot-Skip A/B Test

Fecha: 2026-01-09 Step ID: 0500 Estado: VERIFIED

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:

  1. El CPU verifica si IME está habilitado y si hay bits activos en IF & IE
  2. 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.)
  3. 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 IRQTraceEvent con campos adicionales:
    • pc_after: PC después de saltar al vector
    • vector_addr: Dirección del vector de interrupción (0x40, 0x48, 0x50, 0x58, 0x60)
    • sp_before, sp_after: Stack pointer antes y después del push
    • ime_before, ime_after: Estado de IME antes y después del servicio
    • ie, if_before, if_after: Valores de IE e IF antes y después
    • irq_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ó RETI
    • pc: PC donde se ejecutó RETI
    • return_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 0xFFC5
    • write_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() cuando addr == 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() cuando addr == 0xFF0F o addr == 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 ejecutados
      • hram_ffc5_last_value, hram_ffc5_write_count_total, hram_ffc5_write_count_in_vblank: Métricas de HRAM[0xFFC5]
      • lcdc, stat, ly: Estado del LCD
      • vram_tiledata_nz, vram_tilemap_nz: Bytes no-cero en VRAM
    • Categorías de clasificación:
      • WAITING_ON_FFC5: HRAM[0xFFC5] nunca escrito, juego esperando
      • IRQ_TAKEN_BUT_NO_RETI: IRQ tomado pero no hay RETI
      • IRQ_OK_BUT_FLAG_NOT_SET: IRQ y RETI OK, pero flag no se escribe
      • VRAM_TILEDATA_ZERO: VRAM tiledata vacía (causa raíz)
      • OK_BUT_WHITE: Todo OK pero framebuffer blanco
  • Integración en generate_snapshot(): Se añade sección DMGQuickClassifierV2 al 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 de RETITraceEvent struct
  • cpu.pyx: Métodos get_reti_trace_ring() y get_reti_count() para exponer tracking de RETI
  • mmu.pyx: Actualización de get_hram_ffc5_tracking() y get_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 de IRQTraceEvent, nueva estructura RETITraceEvent, nuevos métodos get_reti_trace_ring() y get_reti_count()
  • src/core/cpp/CPU.cpp - Implementación de tracking ampliado de IRQ y RETI
  • src/core/cpp/MMU.hpp - Ampliación de HRAMFFC5Tracking y IFIETracking con ring buffers
  • src/core/cpp/MMU.cpp - Implementación de tracking ampliado de HRAM[0xFFC5] e IF/IE
  • src/core/cython/cpu.pxd - Declaración de RETITraceEvent
  • src/core/cython/cpu.pyx - Métodos para exponer tracking de RETI
  • src/core/cython/mmu.pyx - Actualización de métodos para exponer tracking ampliado
  • tools/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_ZERO
    • VBlankReq=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_ZERO
  • VBlankReq=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=0 pero VBlankServ=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=0 cuando VBlankServ=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_ en CPU::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)