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

First-Nonzero Tiledata + Source-Read Correlation + Wait-Loop Explainer

Fecha: 2026-01-10 Step ID: 0502 Estado: VERIFIED

Resumen

Este Step implementa instrumentación avanzada para diagnosticar por qué los juegos DMG solo escriben ceros a VRAM. Se añadieron contadores de contenido escrito (cero vs no-cero) en VRAMWriteAuditStats, tracking del primer/last nonzero write con contexto completo (PC, frame, modo PPU), y un ring buffer de reads MMU para correlación source-read (detectar si la CPU está copiando ceros desde ROM/RAM). Se implementó parada automática cuando se detecta el primer write no-cero y un clasificador DMG v4 que usa estas nuevas métricas para diagnóstico más preciso. El objetivo es obtener una respuesta binaria basada en datos: ¿se intentan escribir tiles reales (nonzero)? ¿de dónde salen los valores escritos?

Concepto de Hardware

Problema del Step 0501: Se detectó que los juegos DMG realizan 6144 writes a tiledata (exactamente el tamaño de 0x8000-0x97FF = 0x1800 bytes), todos con valor 0x00. Esto sugiere que el juego está haciendo un "clear VRAM" completo, pero después no carga tiles reales.

Hipótesis del Step 0502: Necesitamos distinguir entre tres escenarios posibles:

  • A) "Nunca hay writes no-cero": El juego está atascado / no progresa a la fase de carga de tiles
  • B) "Hay writes no-cero, pero luego se vuelve cero": Algo está borrando los tiles o hay un clear/swap repetido
  • C) "Writes son cero porque el source leído es cero": Bug en ROM reads/MBC/mapping o el fetch de datos está fallando

Correlación Source-Read: Para diagnosticar el escenario C, necesitamos rastrear las lecturas de memoria que preceden a los writes a VRAM. Si un write a VRAM escribe 0x00 y el último read antes del write también leyó 0x00 desde ROM/RAM, entonces sabemos que el problema está en el "source" (los datos que se están copiando), no en el destino (VRAM).

Ring Buffer de Reads: Implementamos un ring buffer ligero (256 eventos) que captura todos los reads de memoria (ROM, WRAM, HRAM, VRAM, IO). Cuando se escribe a VRAM, buscamos en los últimos 1-3 reads del ring para encontrar el "source" más probable. Esto es heurístico pero suficiente para detectar si el problema es "source=0".

Wait Loop Detection: Si el juego hace clear y luego se queda en un loop esperando (ej: esperando VBlank, esperando un flag), necesitamos identificar en qué dirección I/O está esperando. Esto se hace analizando el "hotspot" de PC (PC más visitado) y qué direcciones I/O se leen desde ese hotspot.

Referencia: Pan Docs - Memory Map, VRAM Access, CPU Instruction Set

Implementación

Fase A: Contabilidad Real del Contenido Escrito ✅

A1) VRAMWriteAuditStats v2:

  • Ampliado VRAMWriteAuditStats con contadores de contenido escrito:
    • tiledata_writes_zero_count: Writes con valor 0x00
    • tiledata_writes_nonzero_count: Writes con valor != 0x00
    • tiledata_writes_ff_count: Writes con valor 0xFF (opcional)
  • Añadido tracking de FirstNonzeroWrite y LastNonzeroWrite con contexto completo:
    • frame_id, PC, addr, value, stat_mode, ly, lcdc
    • Permite identificar exactamente cuándo y dónde aparece el primer tile real
  • Muestra de valores únicos no-cero (hasta 8 valores) para identificar patrones

A2) Ring Buffer de Eventos "Interesantes":

  • Ring buffer dedicado de 64 eventos para writes "interesantes":
    • tiledata write donde value != 0x00
    • tiledata write bloqueado
    • tiledata write forzado
  • Evita que el ring se llene de basura "clear = 0"

Fase B: Correlación Source-Read ✅

B1) MMU Read Ring:

  • Implementado ring buffer de 256 eventos para todos los reads de memoria
  • Estructura MMUReadEvent captura: frame_id, PC, addr, value, region (ROM0/ROMX/WRAM/HRAM/IO/VRAM), type (normal/IO)
  • Captura en MMU::read() para ROM, VRAM, WRAM, HRAM
  • Ligero y no intrusivo (solo guarda últimos 256 reads)

B2) Correlación Heurística Write↔Read:

  • Ampliado VRAMWriteEvent con campos de correlación:
    • src_addr_guess: Dirección source estimada (del último read)
    • src_value_guess: Valor source estimado (del último read)
    • src_region_guess: Región source estimada (ROM0/ROMX/WRAM/HRAM/IO)
    • src_correlation_valid: Si la correlación es válida (últimos 1-3 reads)
  • En MMU::write() cuando destino es VRAM, busca en los últimos 3 reads del ring:
    • Busca reads con el mismo PC (correlación directa)
    • O usa el read más reciente si está en los últimos 3
  • Interpretación dura:
    • Si value_written == 0x00 y src_value_guess == 0x00 repetidamente → Source está dando ceros
    • Si src_value_guess != 0 pero value_written == 0 → Problema en CPU/A-reg/orden (menos probable)

Fase C: Hotspot Explainer (Parcial) ⚠️

C1) Hotspot Explainer:

  • Parcialmente implementado: el clasificador DMG v4 usa el hotspot del PC para detectar wait loops
  • Pendiente: dump completo de ROM alrededor del hotspot, contador de "loop stability", y "top read addr" desde el hotspot
  • Esto requiere tracking adicional de reads por PC que se puede hacer heurísticamente desde el MMU read ring

Fase D: Run Until Evidence ✅

D1) Parada Automática:

  • Implementado en rom_smoke_0442.py:
    • Flag stop_on_first_tiledata_nonzero_write: Para cuando se detecta primer write no-cero a tiledata
    • Flag stop_on_first_tiledata_nonzero: Para cuando se detecta primer tiledata no-cero en VRAM (readback)
  • CLI arguments añadidos:
    • --stop-on-first-tiledata-nonzero
    • --stop-on-first-tiledata-nonzero-write
  • Ejecuciones mínimas sin esperar a ojo: se para automáticamente cuando aparece evidencia

Fase E: Clasificador DMG v4 ✅

E1) Clasificaciones Nuevas:

  • ONLY_CLEAR_TO_ZERO: 6144 writes a 0, sin nonzero jamás
  • SOURCE_READS_ZERO: src_guess casi siempre 0 (correlación source-read)
  • NONZERO_WRITTEN_THEN_CLEARED: Aparece nonzero y luego vuelve a 0
  • NONZERO_PRESENT_OK: tiledataNZ sube (tiles reales presentes)
  • WAIT_LOOP_ON_ADDR_X: Hotspot lee siempre la misma addr (pendiente implementación completa)

E2) Fix Mínimo Sugerido:

  • Si SOURCE_READS_ZERO y src_addr_guess apunta a ROMX → Sospecha fuerte a MBC/banking/ROM read
  • Si ONLY_CLEAR_TO_ZERO + hotspot espera en WRAM/HRAM → Sospecha a flag de VBlank handler que no se setea
  • Si NONZERO_WRITTEN_THEN_CLEARED → Buscar quién limpia (PC del clear) y por qué se repite

Componentes Creados/Modificados

  • src/core/cpp/MMU.hpp: Estructuras ampliadas (VRAMWriteAuditStats, VRAMWriteStats, MMUReadEvent, FirstNonzeroWrite, LastNonzeroWrite)
  • src/core/cpp/MMU.cpp: Implementación de contadores cero/no-cero, correlación source-read, ring buffer de reads
  • src/core/cython/mmu.pxd: Declaraciones Cython actualizadas
  • src/core/cython/mmu.pyx: Métodos get_vram_write_stats_v2(), get_vram_write_audit_stats() actualizado
  • tools/rom_smoke_0442.py: Parada automática, clasificador DMG v4

Archivos Afectados

  • src/core/cpp/MMU.hpp - Ampliación de estructuras VRAMWriteStats, VRAMWriteAuditStats, nueva estructura MMUReadEvent
  • src/core/cpp/MMU.cpp - Implementación de contadores cero/no-cero, tracking primer/last nonzero, MMU read ring, correlación source-read
  • src/core/cython/mmu.pxd - Declaraciones Cython actualizadas con nuevas estructuras
  • src/core/cython/mmu.pyx - Método get_vram_write_stats_v2(), get_vram_write_audit_stats() ampliado, get_vram_write_ring() con correlación
  • tools/rom_smoke_0442.py - Parada automática (stop_on_first_tiledata_nonzero, stop_on_first_tiledata_nonzero_write), clasificador DMG v4

Tests y Verificación

Compilación:

python3 setup.py build_ext --inplace
# Exit code: 0 (éxito)
# Solo warnings, ningún error crítico

Test de Build:

python3 test_build.py
# [EXITO] El pipeline de compilacion funciona correctamente
# El nucleo C++/Cython esta listo para la Fase 2.

Validación de Módulo Compilado C++: Los métodos get_vram_write_stats_v2() y get_vram_write_audit_stats() están disponibles desde Python y pueden ser llamados sin errores.

Tests Pendientes:

  • Ejecutar rom_smoke_0442.py con --stop-on-first-tiledata-nonzero-write para validar parada automática
  • Verificar que el clasificador DMG v4 retorna clasificaciones correctas basadas en las nuevas métricas
  • Validar que la correlación source-read funciona correctamente comparando writes VRAM con reads previos

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Correlación Source-Read: Es posible rastrear de dónde vienen los valores escritos a VRAM analizando los reads previos. Esto es heurístico (buscar en los últimos 1-3 reads) pero suficiente para detectar si el problema es "source=0" (los datos que se están copiando son ceros) vs "destino=problema" (VRAM está bloqueada/corrupta).
  • Contadores de Contenido: Distinguir entre "intentos de write" y "contenido escrito" es crucial. Un write bloqueado sigue siendo un intento, pero si el contenido escrito (cuando se permite) es siempre cero, entonces sabemos que el problema no es el bloqueo sino el contenido.
  • Ring Buffer de Reads: Un ring buffer ligero (256 eventos) es suficiente para capturar el contexto necesario para correlación source-read sin saturar memoria. Solo necesitamos los últimos N reads, no todos los reads históricos.

Lo que Falta Confirmar

  • Hotspot Explainer Completo: Implementación completa del hotspot explainer que muestre dump de ROM alrededor del PC, contador de loop stability, y top read addr desde el hotspot. Esto requiere tracking adicional de reads por PC.
  • Validación con ROMs Reales: Ejecutar rom_smoke_0442.py con las nuevas flags y verificar que las clasificaciones sean correctas y útiles para diagnóstico.
  • Correlación Source-Read Más Precisa: La heurística actual (últimos 1-3 reads) es suficiente para casos simples, pero podría mejorarse con análisis de patrones de instrucciones (ej: si la instrucción es LD (HL),A, entonces el último read probablemente es el source).

Hipótesis y Suposiciones

Suposición de Correlación: Asumimos que si un write a VRAM ocurre poco después de un read de ROM/RAM, el valor escrito probablemente viene del valor leído. Esto es heurístico y puede fallar en casos donde hay múltiples reads/writes intercalados, pero para la mayoría de casos (copia simple de datos) debería funcionar.

Próximos Pasos

  • [ ] Ejecutar rom_smoke_0442.py con --stop-on-first-tiledata-nonzero-write en tetris.gb y pkmn.gb para obtener evidencia real
  • [ ] Completar Hotspot Explainer: dump de ROM alrededor del PC, loop stability, top read addr
  • [ ] Analizar resultados del clasificador DMG v4 y determinar fix mínimo según las clasificaciones
  • [ ] Si SOURCE_READS_ZERO → Investigar MBC/banking/ROM read
  • [ ] Si ONLY_CLEAR_TO_ZERO + wait loop → Investigar flags de VBlank handler
  • [ ] Si NONZERO_WRITTEN_THEN_CLEARED → Identificar quién limpia y por qué