Step 0410 - Diagnóstico DMA/HDMA y Causa de TileData=0
Fecha: 2026-01-01 | Step ID: 0410 | Estado: VERIFIED
Resumen
Implementación de instrumentación completa de DMA/HDMA y escrituras CPU a TileData para diagnosticar
por qué pkmn.gb y Oro.gbc tienen 0% de TileData efectivo. El diagnóstico
reveló el problema raíz: los juegos Pokémon limpian VRAM escribiendo 6,144 bytes de ceros (100% del TileData),
pero quedan bloqueados en un wait-loop antes de poder cargar los tiles reales. El wait-loop
espera interrupciones (IF=0x00 mientras IE=0x0D) que nunca llegan o se procesan incorrectamente.
La comparación con Tetris DX (funcional) confirmó el patrón: Tetris escribe 30,720 bytes a TileData con 35.81% no-cero, mientras que Pokémon solo escribe ceros. No hay uso de HDMA en ninguno de los juegos analizados. El problema NO es de DMA/HDMA, sino de interrupciones/timing que bloquean la carga de tiles.
Concepto de Hardware
DMA (Direct Memory Access) en Game Boy
OAM DMA (0xFF46): Transferencia rápida de 160 bytes desde ROM/RAM a OAM (Object Attribute Memory, 0xFE00-0xFE9F). Se activa escribiendo el byte alto de la dirección fuente en 0xFF46. Durante la transferencia, la CPU solo puede acceder a HRAM (0xFF80-0xFFFE). Duración: 160 × 4 ciclos = 640 T-Cycles.
CGB HDMA (0xFF51-0xFF55): DMA mejorado del Game Boy Color que permite transferencias más largas a VRAM (0x8000-0x9FFF). Dos modos:
- General DMA: Transferencia inmediata y bloqueante.
- HBlank DMA: Transferencia incremental de 16 bytes por HBlank, no bloqueante.
Registros HDMA:
- FF51 (HDMA1): Source High (bits 12-15 de dirección fuente)
- FF52 (HDMA2): Source Low (bits 4-11, bits 0-3 forzados a 0)
- FF53 (HDMA3): Destination High (bits 12-15, rango 0x8000-0x9FF0)
- FF54 (HDMA4): Destination Low (bits 4-11, bits 0-3 forzados a 0)
- FF55 (HDMA5): Length/Mode/Start (bits 0-6: longitud en bloques de 16 bytes - 1, bit 7: 0=General, 1=HBlank)
Fuentes: Pan Docs - "OAM DMA Transfer" (FF46), "CGB Registers" (FF51-FF55), "VRAM DMA Transfers"
Archivos Afectados
src/core/cpp/MMU.hpp- Añadidos contadores de DMA/HDMA y TileData CPU (6 nuevos miembros)src/core/cpp/MMU.cpp- Instrumentación completa de OAM DMA, HDMA, escrituras CPU a TileData, y resumen finalsrc/core/cython/mmu.pxd- Declaración delog_dma_vram_summary()src/core/cython/mmu.pyx- Wrapper Python para el método de resumensrc/viboy.py- Llamada al resumen DMA/VRAM en bloquefinallylogs/step0410_*.log- Logs de diagnóstico de pkmn.gb, Oro.gbc, y tetris_dx.gbc
Implementación
1. Contadores de Diagnóstico (MMU.hpp)
// --- Step 0410: Contadores de DMA/HDMA ---
mutable int oam_dma_count_; // Contador de OAM DMA (0xFF46)
mutable int hdma_start_count_; // Contador de HDMA starts (0xFF55)
mutable int hdma_bytes_transferred_; // Total de bytes transferidos por HDMA
mutable int vram_tiledata_cpu_writes_; // Escrituras CPU a 0x8000-0x97FF
mutable int vram_tiledata_cpu_nonzero_; // Escrituras CPU no-cero a TileData
mutable int vram_tiledata_cpu_log_count_; // Contador de logs de TileData (primeras N)
2. Instrumentación de OAM DMA (MMU.cpp)
if (addr == 0xFF46) {
oam_dma_count_++;
uint16_t source_base = static_cast<uint16_t>(value) << 8;
uint16_t source_end = source_base + 159;
// Determinar región de origen
const char* source_region = "Unknown";
if (source_base >= 0x0000 && source_base < 0x4000) source_region = "ROM Bank 0";
else if (source_base >= 0x4000 && source_base < 0x8000) source_region = "ROM Bank N";
else if (source_base >= 0x8000 && source_base < 0xA000) source_region = "VRAM";
else if (source_base >= 0xA000 && source_base < 0xC000) source_region = "ExtRAM";
else if (source_base >= 0xC000 && source_base < 0xE000) source_region = "WRAM";
if (oam_dma_count_ <= 50) {
printf("[DMA] #%d | PC:0x%04X Bank:%d | Src:0x%04X-0x%04X (%s) -> OAM(0xFE00-0xFE9F)\n",
oam_dma_count_, debug_current_pc, current_rom_bank_,
source_base, source_end, source_region);
}
// Ejecutar transferencia (160 bytes)
for (int i = 0; i < 160; i++) {
uint16_t source_addr = source_base + i;
uint8_t data = read(source_addr);
if ((0xFE00 + i) < MEMORY_SIZE) {
memory_[0xFE00 + i] = data;
}
}
memory_[addr] = value;
return;
}
3. Instrumentación de HDMA (MMU.cpp)
if (addr == 0xFF55) {
uint16_t source = ((hdma1_ << 8) | (hdma2_ & 0xF0));
uint16_t dest = 0x8000 | (((hdma3_ & 0x1F) << 8) | (hdma4_ & 0xF0));
uint16_t length = ((value & 0x7F) + 1) * 0x10;
bool is_hblank_dma = (value & 0x80) != 0;
hdma_start_count_++;
// Determinar destino y origen
const char* dest_region = (dest >= 0x8000 && dest < 0x9800) ? "TileData" : "TileMap";
const char* source_region = /* ... detectar región ... */;
if (hdma_start_count_ <= 50) {
printf("[HDMA] #%d | PC:0x%04X Bank:%d | Mode:%s | Src:0x%04X(%s) -> Dst:0x%04X(%s) | Len:%d bytes\n",
hdma_start_count_, debug_current_pc, current_rom_bank_,
is_hblank_dma ? "HBlank" : "General",
source, source_region, dest, dest_region, length);
}
// Copiar datos y contar bytes no-cero
int bytes_copied = 0;
int nonzero_bytes = 0;
for (uint16_t i = 0; i < length; i++) {
uint8_t byte = read(source + i);
if (byte != 0) nonzero_bytes++;
// Escribir a VRAM bank seleccionado
uint16_t vram_addr = dest + i;
if (vram_addr >= 0x8000 && vram_addr <= 0x9FFF) {
uint16_t offset = vram_addr - 0x8000;
if (vram_bank_ == 0) {
vram_bank0_[offset] = byte;
} else {
vram_bank1_[offset] = byte;
}
bytes_copied++;
}
}
hdma_bytes_transferred_ += bytes_copied;
if (hdma_start_count_ <= 50) {
printf("[HDMA-DONE] Transferidos %d bytes (nonzero:%d) | Total acumulado: %d bytes\n",
bytes_copied, nonzero_bytes, hdma_bytes_transferred_);
}
hdma5_ = 0xFF;
hdma_active_ = false;
hdma_length_remaining_ = 0;
return;
}
4. Instrumentación de Escrituras CPU a TileData (MMU.cpp)
if (addr >= 0x8000 && addr <= 0x9FFF) {
// Contar escrituras por CPU al área de TileData
if (addr >= 0x8000 && addr <= 0x97FF) {
vram_tiledata_cpu_writes_++;
if (value != 0x00) {
vram_tiledata_cpu_nonzero_++;
}
// Loggear primeras 50 escrituras
if (vram_tiledata_cpu_log_count_ < 50) {
printf("[TILEDATA-CPU] Write #%d | PC:0x%04X Bank:%d VRAMBank:%d | Addr:0x%04X <- 0x%02X\n",
vram_tiledata_cpu_writes_, debug_current_pc, current_rom_bank_,
vram_bank_, addr, value);
vram_tiledata_cpu_log_count_++;
}
// Resumen periódico cada 1000 escrituras
if (vram_tiledata_cpu_writes_ % 1000 == 0 && vram_tiledata_cpu_writes_ > 0) {
printf("[TILEDATA-CPU-SUMMARY] Total:%d Nonzero:%d (%.1f%%)\n",
vram_tiledata_cpu_writes_, vram_tiledata_cpu_nonzero_,
(vram_tiledata_cpu_nonzero_ * 100.0) / vram_tiledata_cpu_writes_);
}
}
// Escribir a banco VRAM seleccionado
uint16_t offset = addr - 0x8000;
if (vram_bank_ == 0) {
vram_bank0_[offset] = value;
} else {
vram_bank1_[offset] = value;
}
return;
}
5. Resumen Final (MMU.cpp)
void MMU::log_dma_vram_summary() {
printf("\n");
printf("========================================\n");
printf("[DMA/VRAM SUMMARY] Step 0410 - Diagnóstico DMA/HDMA\n");
printf("========================================\n");
// OAM DMA
printf("[DMA/VRAM] OAM DMA (0xFF46):\n");
printf("[DMA/VRAM] Total de transferencias: %d\n", oam_dma_count_);
printf("[DMA/VRAM] Bytes transferidos: %d (160 bytes × %d)\n", oam_dma_count_ * 160, oam_dma_count_);
// HDMA
printf("[DMA/VRAM] CGB HDMA (0xFF51-0xFF55):\n");
printf("[DMA/VRAM] Total de starts: %d\n", hdma_start_count_);
printf("[DMA/VRAM] Bytes transferidos: %d\n", hdma_bytes_transferred_);
// Escrituras CPU a TileData
printf("[DMA/VRAM] Escrituras CPU a TileData (0x8000-0x97FF):\n");
printf("[DMA/VRAM] Total escrituras: %d\n", vram_tiledata_cpu_writes_);
printf("[DMA/VRAM] Escrituras no-cero: %d\n", vram_tiledata_cpu_nonzero_);
if (vram_tiledata_cpu_writes_ > 0) {
printf("[DMA/VRAM] Porcentaje no-cero: %.2f%%\n",
(vram_tiledata_cpu_nonzero_ * 100.0) / vram_tiledata_cpu_writes_);
}
// Análisis automático
printf("[DMA/VRAM] Análisis:\n");
if (oam_dma_count_ == 0 && hdma_start_count_ == 0 && vram_tiledata_cpu_writes_ == 0) {
printf("[DMA/VRAM] ⚠️ NO HAY ACTIVIDAD DE CARGA DE GRÁFICOS\n");
} else if (hdma_start_count_ > 0 && hdma_bytes_transferred_ == 0) {
printf("[DMA/VRAM] ⚠️ HDMA START SIN TRANSFERENCIA\n");
} else if (vram_tiledata_cpu_writes_ > 0 && vram_tiledata_cpu_nonzero_ == 0) {
printf("[DMA/VRAM] ⚠️ ESCRITURAS CPU PERO TODOS CEROS\n");
} else if (hdma_bytes_transferred_ > 0 || vram_tiledata_cpu_nonzero_ > 0) {
printf("[DMA/VRAM] ✓ HAY ACTIVIDAD DE CARGA DE GRÁFICOS\n");
}
printf("========================================\n\n");
}
Tests y Verificación
Comandos de Ejecución
python3 setup.py build_ext --inplace
timeout 45s python3 main.py roms/pkmn.gb > logs/step0410_pkmn_dma_vram.log 2>&1
timeout 45s python3 main.py roms/Oro.gbc > logs/step0410_oro_dma_vram.log 2>&1
timeout 30s python3 main.py roms/tetris_dx.gbc > logs/step0410_tetris_dx_dma_vram.log 2>&1
Resultados - pkmn.gb (Pokémon Rojo)
[DMA/VRAM SUMMARY] Step 0410 - Diagnóstico DMA/HDMA
========================================
[DMA/VRAM] OAM DMA (0xFF46):
[DMA/VRAM] Total de transferencias: 609
[DMA/VRAM] Bytes transferidos: 97440 (160 bytes × 609)
[DMA/VRAM] CGB HDMA (0xFF51-0xFF55):
[DMA/VRAM] Total de starts: 0
[DMA/VRAM] Bytes transferidos: 0
[DMA/VRAM] Escrituras CPU a TileData (0x8000-0x97FF):
[DMA/VRAM] Total escrituras: 6144
[DMA/VRAM] Escrituras no-cero: 0
[DMA/VRAM] Porcentaje no-cero: 0.00%
[DMA/VRAM] Análisis:
[DMA/VRAM] ⚠️ ESCRITURAS CPU PERO TODOS CEROS
[DMA/VRAM] Se escribió a TileData pero todos los valores son 0x00.
========================================
Resultados - Oro.gbc (Pokémon Oro)
[DMA/VRAM SUMMARY] Step 0410 - Diagnóstico DMA/HDMA
========================================
[DMA/VRAM] OAM DMA (0xFF46):
[DMA/VRAM] Total de transferencias: 2
[DMA/VRAM] Bytes transferidos: 320 (160 bytes × 2)
[DMA/VRAM] CGB HDMA (0xFF51-0xFF55):
[DMA/VRAM] Total de starts: 0
[DMA/VRAM] Bytes transferidos: 0
[DMA/VRAM] Escrituras CPU a TileData (0x8000-0x97FF):
[DMA/VRAM] Total escrituras: 6144
[DMA/VRAM] Escrituras no-cero: 0
[DMA/VRAM] Porcentaje no-cero: 0.00%
[DMA/VRAM] Análisis:
[DMA/VRAM] ⚠️ ESCRITURAS CPU PERO TODOS CEROS
[DMA/VRAM] Se escribió a TileData pero todos los valores son 0x00.
========================================
Resultados - tetris_dx.gbc (Baseline Funcional)
[DMA/VRAM SUMMARY] Step 0410 - Diagnóstico DMA/HDMA
========================================
[DMA/VRAM] OAM DMA (0xFF46):
[DMA/VRAM] Total de transferencias: 0
[DMA/VRAM] Bytes transferidos: 0 (160 bytes × 0)
[DMA/VRAM] CGB HDMA (0xFF51-0xFF55):
[DMA/VRAM] Total de starts: 0
[DMA/VRAM] Bytes transferidos: 0
[DMA/VRAM] Escrituras CPU a TileData (0x8000-0x97FF):
[DMA/VRAM] Total escrituras: 30720
[DMA/VRAM] Escrituras no-cero: 11000
[DMA/VRAM] Porcentaje no-cero: 35.81%
[DMA/VRAM] Análisis:
[DMA/VRAM] ✓ HAY ACTIVIDAD DE CARGA DE GRÁFICOS
[DMA/VRAM] El juego ha cargado datos no-cero en VRAM.
========================================
Wait-Loop Detectado (pkmn.gb)
[WAIT-LOOP] ===== BUCLE DE ESPERA DETECTADO EN BANK 28, PC 0x614D =====
[WAIT-LOOP] Iter:0 PC:0x614D | IME:1 IE:0x0D IF:0x00
[WAIT-MMIO-READ] PC:0x614D -> IE(0xFFFF) = 0x0D (VBlank+LCD+Joypad habilitadas)
[WAIT-MMIO-READ] PC:0x614D -> IF(0xFF0F) = 0x00 (ninguna interrupción pendiente)
[WAIT-MMIO-READ] PC:0x614D -> LCDC(0xFF40) = 0xE3 (LCD ON)
[WAIT-MMIO-READ] PC:0x614D -> LY(0xFF44) = 0x20 (línea 32)
// El juego está bloqueado esperando una interrupción que nunca llega
Análisis Comparativo
| Juego | OAM DMA | HDMA | Escrituras TileData | % No-Cero | Estado |
|---|---|---|---|---|---|
| pkmn.gb | 609 | 0 | 6,144 | 0.00% | ❌ Solo ceros |
| Oro.gbc | 2 | 0 | 6,144 | 0.00% | ❌ Solo ceros |
| tetris_dx.gbc | 0 | 0 | 30,720 | 35.81% | ✅ Funcional |
Conclusión del Diagnóstico
✅ PROBLEMA RAÍZ IDENTIFICADO:
- Los juegos Pokémon limpian VRAM correctamente (6,144 bytes = 384 tiles × 16 bytes)
- Después de limpiar, quedan bloqueados en un wait-loop esperando interrupciones (IF=0x00, IE=0x0D)
- Las interrupciones no llegan o se procesan incorrectamente, impidiendo que el juego progrese
- Sin progreso, nunca se cargan los tiles reales (todos los valores quedan en 0x00)
- El problema NO es de DMA/HDMA (ningún juego usa HDMA en esta fase)
Conclusión
El Step 0410 ha sido un éxito diagnóstico total. La instrumentación completa de DMA/HDMA y escrituras CPU a TileData reveló el problema raíz de por qué los juegos Pokémon no cargan gráficos:
- Los juegos limpian VRAM correctamente: 6,144 bytes de ceros (100% del TileData)
- Después de limpiar, quedan bloqueados en un wait-loop en Bank 28, PC 0x614D
- El wait-loop espera interrupciones que nunca llegan: IF=0x00 mientras IE=0x0D (VBlank+LCD+Joypad habilitadas)
- Sin salir del wait-loop, nunca cargan los tiles reales (por eso TileData queda en 0%)
El problema NO es de DMA/HDMA (ningún juego usa HDMA en esta fase inicial), sino de emulación incorrecta de interrupciones o timing. El siguiente paso (Step 0411) debe investigar:
- ¿Por qué las interrupciones VBlank/LCD STAT no se disparan o no se entregan?
- ¿Se está limpiando IF prematuramente?
- ¿El timing de interrupciones es incorrecto?
- ¿El ISR VBlank se ejecuta pero retorna inmediatamente sin permitir progreso?
Siguiente Paso
Step 0411: Investigar el mecanismo de interrupciones (VBlank, LCD STAT) y su timing. Instrumentar:
- Disparos de interrupciones (cuando se activa cada bit de IF)
- Clears de interrupciones (cuando se limpia IF)
- Ejecución de ISRs (entrada/salida de cada handler)
- Timing de VBlank y Mode changes de PPU