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

Operación Time-Lapse: Disección del Bucle de Polling y Monitor de Registros de Tiempo

Fecha: 2025-12-25 Step ID: 0276 Estado: Draft

Resumen

Este Step implementa la "Operación Time-Lapse" para diseccionar el bucle de polling activo en el que Pokémon Red está atrapado (PC: 614D - 6151). El análisis del Step 0275 reveló que el juego no está en HALT, sino que está poleando (revisando constantemente) una condición. La hipótesis es que el juego está esperando que un registro de hardware (como LY, DIV o el flag 0xD732) cambie, pero si nuestro Timer o PPU no están avanzando correctamente, el juego se queda atrapado en el tiempo.

Se añadió instrumentación en un punto crítico: Sniper Trace del bucle atrapado (614D-6155) para capturar exactamente qué opcodes ejecuta y qué valores lee de la memoria (LY, DIV, STAT, D732). El objetivo es identificar qué registro está siendo poleado y si el tiempo está "congelado" para la CPU, causando que el bucle de espera se vuelva infinito.

Concepto de Hardware

En la Game Boy, existen dos formas principales de sincronización entre el software y el hardware: interrupciones y polling. Mientras que las interrupciones son el método preferido (el hardware notifica al software cuando ocurre un evento), el polling es una alternativa que algunos juegos usan para verificar el estado del hardware de forma activa.

1. Polling vs Interrupciones

Interrupciones: El hardware notifica a la CPU cuando ocurre un evento (como V-Blank). La CPU puede estar ejecutando otras tareas y será interrumpida automáticamente cuando el evento ocurra. Esto es eficiente porque la CPU no necesita verificar constantemente el estado del hardware.

Polling: El software verifica activamente el estado del hardware leyendo registros repetidamente hasta que el valor cambia. Esto consume ciclos de CPU pero puede ser necesario cuando:

  • Las interrupciones están deshabilitadas (IE=0 o IME=0)
  • El juego necesita sincronización precisa con un evento específico
  • El juego está en una rutina crítica donde no puede permitir interrupciones

2. Registros de Hardware que se Pueden Pollar

Los juegos pueden leer varios registros de hardware para sincronización:

  • LY (0xFF44): Línea de escaneo actual (0-153). Se incrementa automáticamente por la PPU cada 456 T-Cycles. Los juegos pueden leer este registro para esperar a que la PPU complete un frame o llegue a una línea específica.
  • DIV (0xFF04): Registro de división del Timer. Se incrementa automáticamente cada 256 T-Cycles (frecuencia base del Timer). Los juegos pueden leer este registro para implementar retardos de tiempo o esperar a que pase un intervalo específico.
  • STAT (0xFF41): Estado de la PPU (modo actual, flags de coincidencia). Los juegos pueden leer este registro para esperar a que la PPU entre en un modo específico (como V-Blank).
  • Flags personalizados (ej: 0xD732): Algunos juegos usan flags en WRAM/HRAM para comunicación entre rutinas. Una ISR puede modificar estos flags, y el código principal puede pollarlos.

3. El Peligro del "Timer Fantasma"

Si un juego está poleando un registro de hardware (como DIV o LY) esperando que cambie, pero el emulador no está actualizando ese registro correctamente, el juego se queda atrapado en un bucle infinito. Esto es especialmente peligroso cuando:

  1. El Timer no avanza: Si el Timer no está siendo actualizado con los T-Cycles consumidos por la CPU, el registro DIV permanece estático. El juego lee DIV repetidamente esperando que cambie, pero nunca lo hace.
  2. La PPU no avanza: Si la PPU no está siendo actualizada después de cada instrucción, el registro LY permanece estático. El juego lee LY repetidamente esperando que cambie, pero nunca lo hace.
  3. El bucle es muy apretado: Si el bucle de polling consume todos los ciclos de CPU sin dar tiempo a que el Timer o la PPU avancen, el tiempo se "congela" desde la perspectiva del juego.

Este es el concepto del "Timer Fantasma": el hardware debería estar avanzando, pero desde la perspectiva del software, el tiempo está congelado.

4. Sincronización en run_scanline()

La función run_scanline() es crítica para evitar el "Timer Fantasma". Esta función ejecuta instrucciones de la CPU hasta acumular 456 T-Cycles (una scanline completa), pero después de cada instrucción actualiza la PPU y el Timer con los ciclos consumidos. Esto garantiza que:

  • La PPU avanza correctamente, actualizando LY y los modos de la PPU
  • El Timer avanza correctamente, actualizando DIV y TIMA
  • Incluso si la CPU está en un bucle apretado de polling, el hardware sigue avanzando

Si esta sincronización no está funcionando correctamente, el juego puede quedar atrapado en bucles de polling infinitos esperando que el hardware haga algo que nunca ocurre.

Implementación

Se implementó una instrumentación para diseccionar el bucle de polling y entender qué registro está siendo verificado:

Sniper Trace del Bucle de Polling (CPU.cpp)

Se añadió un bloque de instrumentación al final de CPU::step() que captura el estado de la CPU cuando el PC está en el rango 0x614A-0x6155 (el bucle de polling atrapado). El trace captura:

  • PC y Opcode: La dirección actual y el opcode que se está ejecutando
  • Registros de CPU: A, BC, HL (los registros más relevantes para el bucle)
  • Registros de Hardware: LY (0xFF44), DIV (0xFF04), STAT (0xFF41), y el flag 0xD732

El trace se limita a 40 pasos (unas 10 vueltas al bucle) para no saturar el log. Esto nos permite ver:

  • Si LY, DIV o STAT están cambiando (confirmando que el hardware avanza)
  • Si el flag 0xD732 está cambiando (confirmando que una ISR lo modifica)
  • Qué opcodes se ejecutan en el bucle (para desensamblar la condición de salida)
  • El patrón de valores que el juego está leyendo en cada iteración

Ubicación del código: El trace se ejecuta al final de step(), después de que la instrucción se ha ejecutado completamente. Esto garantiza que capturamos el estado real de los registros después de cada instrucción del bucle.

Archivos Afectados

  • src/core/cpp/CPU.cpp - Añadido Sniper Trace del bucle de polling (614D-6155) al final de step() con captura de registros de hardware (LY, DIV, STAT, D732) y registros de CPU (A, BC, HL)

Tests y Verificación

La verificación se realizó ejecutando Pokémon Red y analizando los logs generados:

  • Comando ejecutado: python main.py roms/pkmn.gb
  • Resultado: Se capturaron 40 trazas [SNIPER-LOOP] exitosamente
  • Análisis de logs: Las trazas revelaron información crítica sobre el bucle

Resultados del Análisis

Opcodes del bucle (desensamblado):

  • 614A: 11 - LD DE, nn (carga un valor en DE)
  • 614D: 00 - NOP
  • 614E: 00 - NOP
  • 614F: 00 - NOP
  • 6150: 1B - DEC DE (decrementa DE)
  • 6151: 7A - LD A, D (carga D en A)
  • 6152: B3 - OR E (A = A | E)
  • 6153: 20 - JR NZ, e (salto relativo si Z=0)

Estado de los registros de hardware:

  • LY: 20 (constante, no cambia) - ⚠️ Posible problema de sincronización de PPU
  • DIV: 15 → 16 (sí cambia) - ✅ Timer funciona correctamente
  • STAT: 03 → 00 (cambia) - ✅ PPU está actualizando STAT
  • D732: 00 (constante) - No se modifica durante el bucle

Interpretación del Bucle

El bucle en 614D-6153 NO está poleando hardware. Es un bucle de retardo basado en el registro DE:

  1. Decrementa DE (DEC DE en 6150)
  2. Carga D en A (LD A, D en 6151)
  3. Hace OR E (OR E en 6152) - Si D=0 y E=0, Z=1
  4. Salta si Z=0 (JR NZ en 6153) - Si Z=1, no salta y sale del bucle

Conclusión: El bucle espera a que DE llegue a 0. No está esperando que ningún registro de hardware cambie. El Timer funciona correctamente (DIV avanza), pero LY está estático en 20, lo que sugiere un posible problema de sincronización de la PPU.

Validación de módulo compilado C++: ✅ Compilación exitosa. Los logs [SNIPER-LOOP] aparecen correctamente cuando el PC entra en el rango 614A-6155.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Polling vs Interrupciones: Los juegos pueden usar polling cuando las interrupciones están deshabilitadas o cuando necesitan sincronización precisa. El polling consume ciclos de CPU pero es necesario en ciertos contextos.
  • Timer Fantasma: Si el Timer o la PPU no están siendo actualizados correctamente, los registros de hardware (DIV, LY) permanecen estáticos, causando que los bucles de polling se vuelvan infinitos.
  • Sincronización ciclo a ciclo: La función run_scanline() debe actualizar la PPU y el Timer después de cada instrucción para garantizar que el hardware avanza incluso cuando la CPU está en bucles apretados.

Hallazgos Confirmados

  • Condición de salida del bucle: ✅ Confirmado. El bucle NO está poleando hardware. Es un bucle de retardo basado en DE. El bucle termina cuando DE llega a 0 (cuando D=0 y E=0, OR E da Z=1 y JR NZ no salta).
  • Estado del Timer: ✅ Confirmado. El Timer está funcionando correctamente. DIV cambia de 15 a 16 durante el bucle, confirmando que la sincronización del Timer funciona.
  • Estado de la PPU: ⚠️ Problema identificado. LY está estático en 20 durante todo el bucle. Esto sugiere que la PPU no está avanzando correctamente o que el juego está en un momento donde LY no debería cambiar. STAT sí cambia (de 03 a 00), lo que indica que la PPU está procesando algo, pero LY permanece constante.

Hipótesis Revisada

Hipótesis original (REFUTADA): El juego está poleando un registro de hardware esperando que cambie. ❌ Esta hipótesis fue refutada: el bucle NO está poleando hardware, es un bucle de retardo basado en DE.

Nueva hipótesis: El bucle es un retardo simple que debería terminar cuando DE llega a 0. Si el juego sigue atascado después de que DE llega a 0, el problema está en otra parte (posiblemente en la lógica que sigue después del bucle, o en un problema de sincronización de la PPU que causa que LY esté estático).

Próximos Pasos

  • [✅] Ejecutar Pokémon Red y analizar las trazas [SNIPER-LOOP] - COMPLETADO
  • [✅] Verificar si LY, DIV o STAT están cambiando - COMPLETADO (DIV y STAT cambian, LY estático)
  • [✅] Desensamblar los opcodes capturados - COMPLETADO (bucle de retardo basado en DE)
  • [ ] Investigar por qué LY está estático en 20 durante el bucle (posible problema de sincronización de PPU)
  • [ ] Verificar si el bucle termina cuando DE llega a 0 (el juego debería continuar)
  • [ ] Si el juego sigue atascado después de que DE llega a 0, investigar qué ocurre después del bucle
  • [ ] Si LY está estático cuando debería cambiar, corregir la sincronización de la PPU en run_scanline()