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.
Investigación de Carga de Tiles y Corrección
Resumen
Implementación de un conjunto completo de monitores de diagnóstico para investigar por qué los tiles no se están cargando en VRAM. El análisis del Step 0290 confirmó que [TILE-LOAD] detecta 0 cargas de tiles, lo que significa que el juego no está escribiendo datos de tiles en VRAM. Se implementaron cinco monitores nuevos: [VRAM-INIT] para verificar el estado inicial de VRAM, [TILE-LOAD-EXTENDED] para capturar TODAS las escrituras con contexto de timing, [CLEANUP-TRACE] para rastrear la rutina de limpieza VRAM (PC:0x36E3), [BLOCK-WRITE] para detectar cargas de tiles consecutivas, y un contador de frames en PPU para rastrear el timing de las operaciones.
Concepto de Hardware
Carga de Tiles en VRAM: Los tiles se cargan típicamente durante la inicialización del juego. Pueden cargarse desde ROM (directo o comprimido), desde RAM (datos pre-procesados), o usando DMA (aunque normalmente DMA es para OAM). Los tiles pueden cargarse antes del primer frame o durante V-Blank. Si los monitores se activan solo después de empezar a renderizar, pueden perderse cargas tempranas.
Timing de Carga: La carga puede ocurrir durante la inicialización (antes del primer frame), durante V-Blank (cuando VRAM es accesible), o durante H-Blank (en algunos casos). Si los monitores se activan solo después de empezar a renderizar, pueden perderse cargas tempranas.
Estado Inicial de VRAM: En hardware real, VRAM está inicializada a valores aleatorios al encender. Los juegos suelen limpiar VRAM antes de cargar sus propios datos. Algunos juegos esperan que VRAM tenga valores específicos (raro). El monitor [VRAM-INIT] verifica el estado inicial de VRAM después de cargar la ROM para entender si el juego espera que VRAM tenga datos desde el inicio o si la carga es responsabilidad del juego.
Rutinas de Limpieza: Muchos juegos ejecutan rutinas de limpieza que escriben ceros en VRAM antes de cargar sus propios datos. El Step 0288 identificó que PC:0x36E3 está escribiendo ceros en VRAM. El monitor [CLEANUP-TRACE] rastrea esta rutina para entender qué hace exactamente y si hay código después que debería cargar tiles.
Fuente: Pan Docs - "Video RAM (VRAM)", "Tile Data", "DMA Transfer", "LCD Timing"
Implementación
1. Contador de Frames en PPU
Se añadió un contador de frames global en PPU que se incrementa cada vez que LY vuelve a 0 (nuevo frame). Este contador es necesario para rastrear el timing de carga de tiles y determinar si los tiles se cargan antes del frame 0 o durante la inicialización.
// En PPU.hpp
uint64_t frame_counter_;
// En PPU.cpp constructor
frame_counter_(0)
// En PPU.cpp step(), cuando LY > 153
if (ly_ > 153) {
ly_ = 0;
frame_counter_++; // Incrementar contador de frames
// ...
}
// Método público para obtener el frame actual
uint64_t PPU::get_frame_counter() const {
return frame_counter_;
}
2. Monitor [VRAM-INIT] - Inspección de Estado Inicial
Se implementó la función MMU::inspect_vram_initial_state() que se llama desde MMU::load_rom() después de cargar la ROM. Esta función verifica el estado inicial de VRAM (0x8000-0x97FF) y reporta cuántos bytes no-cero hay, la primera dirección con datos no-cero, y el checksum del tilemap inicial (0x9800).
void MMU::inspect_vram_initial_state() {
// Verificar si VRAM tiene datos no-cero
int non_zero_count = 0;
uint16_t first_non_zero_addr = 0xFFFF;
for (uint16_t addr = 0x8000; addr <= 0x97FF; addr++) {
if (memory_[addr] != 0x00 && memory_[addr] != 0x7F) {
non_zero_count++;
if (first_non_zero_addr == 0xFFFF) {
first_non_zero_addr = addr;
}
}
}
printf("[VRAM-INIT] Estado inicial de VRAM: %d bytes no-cero (0x8000-0x97FF)\n", non_zero_count);
if (first_non_zero_addr != 0xFFFF) {
printf("[VRAM-INIT] Primer byte no-cero en: 0x%04X (valor: 0x%02X)\n",
first_non_zero_addr, memory_[first_non_zero_addr]);
} else {
printf("[VRAM-INIT] VRAM está completamente vacía (solo ceros)\n");
}
// Verificar checksum del tilemap inicial
uint16_t tilemap_checksum = 0;
for (int i = 0; i < 1024; i++) {
tilemap_checksum += memory_[0x9800 + i];
}
printf("[VRAM-INIT] Checksum del tilemap (0x9800): 0x%04X\n", tilemap_checksum);
}
3. Monitor [TILE-LOAD-EXTENDED] - Captura Extendida
Se extendió el monitor [TILE-LOAD] para capturar TODAS las escrituras en Tile Data (0x8000-0x97FF), incluyendo limpieza (0x00) pero marcándolas diferente. El monitor ahora rastrea el frame actual usando el contador de frames de PPU y marca si la escritura ocurre durante la inicialización (primeras 100 escrituras) o después.
// --- Step 0291: Monitor Extendido de Carga de Tiles ([TILE-LOAD-EXTENDED]) ---
if (addr >= 0x8000 && addr <= 0x97FF) {
// Obtener contador de frames desde PPU si está disponible
uint64_t frame_counter = 0;
if (ppu_ != nullptr) {
frame_counter = ppu_->get_frame_counter();
}
// Marcar el fin de la inicialización (aproximadamente después de 100 escrituras)
static int write_count = 0;
static bool is_initialization = true;
write_count++;
if (write_count > 100) {
is_initialization = false;
}
// Capturar TODAS las escrituras, marcando si son limpieza o datos reales
static int tile_load_extended_count = 0;
if (tile_load_extended_count < 1000) {
bool is_data = (value != 0x00 && value != 0x7F);
uint16_t tile_offset = addr - 0x8000;
uint8_t tile_id_approx = tile_offset / 16;
printf("[TILE-LOAD-EXT] %s | Write %04X=%02X (TileID~%d) PC:%04X Frame:%llu Init:%s\n",
is_data ? "DATA" : "CLEAR",
addr, value, tile_id_approx, debug_current_pc, frame_counter,
is_initialization ? "YES" : "NO");
tile_load_extended_count++;
}
}
4. Monitor [CLEANUP-TRACE] - Rastreo de Rutina de Limpieza
Se implementó un monitor que rastrea la ejecución alrededor de PC:0x36E3 para entender qué hace esta rutina y si hay código después que carga tiles. El Step 0288 identificó que PC:0x36E3 está escribiendo ceros en VRAM. El monitor captura el opcode en cada dirección alrededor de 0x36E3.
// --- Step 0291: Rastreo de Rutina de Limpieza VRAM (PC:0x36E3) ---
if (debug_current_pc >= 0x36E0 && debug_current_pc <= 0x36F0) {
static int cleanup_trace_count = 0;
if (cleanup_trace_count < 200) {
uint8_t op = read(debug_current_pc);
printf("[CLEANUP-TRACE] PC:0x%04X OP:0x%02X | Bank:%d\n",
debug_current_pc, op, current_rom_bank_);
cleanup_trace_count++;
}
}
5. Monitor [BLOCK-WRITE] - Detección de Escrituras Consecutivas
Se implementó un monitor que detecta escrituras consecutivas en VRAM que podrían ser carga de tiles en bloque (como un loop de copia). El monitor detecta cuando se escriben 16 bytes consecutivos (un tile completo) y reporta el rango de direcciones y el PC que originó la escritura.
// --- Step 0291: Monitor de Escrituras en Bloque ([BLOCK-WRITE]) ---
if (addr >= 0x8000 && addr <= 0x97FF) {
static uint16_t last_vram_addr = 0xFFFF;
static int consecutive_writes = 0;
if (addr == last_vram_addr + 1) {
consecutive_writes++;
if (consecutive_writes == 16) { // Un tile completo
printf("[BLOCK-WRITE] Posible carga de tile en bloque: 0x%04X-0x%04X desde PC:0x%04X\n",
addr - 15, addr, debug_current_pc);
}
} else {
consecutive_writes = 0;
}
last_vram_addr = addr;
}
Hipótesis a Investigar
Los monitores implementados permiten investigar cuatro hipótesis principales:
- Hipótesis 1: Timing - ¿Los tiles se cargan antes del frame 0 (durante inicialización antes de que los monitores se activen)? El monitor [TILE-LOAD-EXTENDED] con contador de frames permite verificar esto.
- Hipótesis 2: Borrado - ¿Los tiles se cargan pero luego se borran inmediatamente después? El monitor [CLEANUP-TRACE] rastrea la rutina de limpieza para verificar esto.
- Hipótesis 3: Métodos alternativos - ¿El juego usa DMA o compresión para cargar tiles que no detectamos? El monitor [BLOCK-WRITE] detecta cargas en bloque.
- Hipótesis 4: Estado inicial - ¿Debería VRAM tener datos desde el inicio (desde el constructor)? El monitor [VRAM-INIT] verifica el estado inicial.
Archivos Afectados
src/core/cpp/PPU.hpp- Añadido contador de frames y método gettersrc/core/cpp/PPU.cpp- Implementación del contador de framessrc/core/cpp/MMU.hpp- Añadida función inspect_vram_initial_state()src/core/cpp/MMU.cpp- Implementación de todos los monitores de diagnóstico
Tests y Verificación
La implementación se validó mediante compilación exitosa del módulo C++/Cython:
python setup.py build_ext --inplace
Resultado: Compilación exitosa sin errores (solo warnings menores no críticos).
Validación de módulo compilado C++: El módulo se compiló correctamente y está listo para ejecutarse con los nuevos monitores activos. Los monitores se activarán automáticamente cuando se ejecute el emulador y generarán logs que permitirán analizar las hipótesis planteadas.
Próximos pasos de verificación: Ejecutar el emulador con una ROM de prueba y analizar los logs generados por los monitores para determinar cuál de las hipótesis es correcta (o si es una combinación de ellas).
Fuentes Consultadas
- Pan Docs: Video RAM (VRAM)
- Pan Docs: Tile Data
- Pan Docs: DMA Transfer
- Pan Docs: LCD Timing
Integridad Educativa
Lo que Entiendo Ahora
- Timing de Carga de Tiles: Los tiles pueden cargarse en diferentes momentos: durante la inicialización (antes del primer frame), durante V-Blank, o durante H-Blank. Si los monitores se activan solo después de empezar a renderizar, pueden perderse cargas tempranas.
- Estado Inicial de VRAM: En hardware real, VRAM está inicializada a valores aleatorios al encender. Los juegos suelen limpiar VRAM antes de cargar sus propios datos. Algunos juegos esperan que VRAM tenga valores específicos (raro).
- Rutinas de Limpieza: Muchos juegos ejecutan rutinas de limpieza que escriben ceros en VRAM antes de cargar sus propios datos. Estas rutinas pueden ser críticas para entender el flujo de carga de tiles.
- Métodos Alternativos de Carga: Los juegos pueden usar métodos alternativos para cargar tiles, como DMA (aunque normalmente DMA es para OAM), compresión/decompresión, o escrituras en bloque mediante loops.
Lo que Falta Confirmar
- Análisis de Logs: Necesitamos ejecutar el emulador con los nuevos monitores activos y analizar los logs generados para determinar cuál de las hipótesis es correcta (o si es una combinación de ellas).
- Correcciones Basadas en Hallazgos: Una vez que identifiquemos la causa raíz del problema, necesitaremos aplicar las correcciones correspondientes (modificar timing de monitores, prevenir borrado prematuro, cargar tiles iniciales, implementar soporte para métodos alternativos, etc.).
Hipótesis y Suposiciones
Asumimos que el problema está relacionado con el timing de carga de tiles o con métodos alternativos de carga que no estamos detectando. Los monitores implementados están diseñados para capturar todos los escenarios posibles y permitir un análisis exhaustivo del problema.
Próximos Pasos
- [ ] Ejecutar el emulador con los nuevos monitores activos
- [ ] Analizar los logs generados por los monitores
- [ ] Determinar cuál de las hipótesis es correcta (o si es una combinación)
- [ ] Aplicar las correcciones correspondientes según los hallazgos
- [ ] Verificar que los tiles se carguen correctamente después de las correcciones
- [ ] Confirmar que la pantalla muestre gráficos correctamente