⚠️ 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 Warp Drive: Monitor de Decremento y Validación de Bucle de Retardo

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

Resumen

Este Step implementa la "Operación Warp Drive" para validar el bucle de retardo identificado en el Step 0276. El análisis previo reveló que el juego NO está poleando hardware, sino ejecutando un bucle de retardo por software basado en el registro DE. El bucle decrementa DE hasta que llega a 0, y luego continúa con la ejecución.

Se añadió instrumentación específica en tres puntos críticos: (1) captura de la carga inicial de DE en PC:0x614A, (2) monitoreo del decremento de DE cada 1000 iteraciones en PC:0x6150, y (3) detección de salida del bucle cuando el PC sale del rango 0x614A-0x6155. El objetivo es confirmar que DE está disminuyendo correctamente, cuánto tiempo le falta al bucle, y validar que la instrucción DEC DE está funcionando correctamente.

Concepto de Hardware

En la Game Boy, los bucles de retardo por software son una técnica común para crear pausas temporales sin usar hardware de timer o interrupciones. Estos bucles funcionan decrementando un registro de 16 bits hasta que llega a 0, consumiendo ciclos de CPU de forma predecible.

1. Bucles de Retardo por Software

Un bucle de retardo típico en la Game Boy funciona así:

  1. Carga inicial: Se carga un valor en un par de registros (ej: DE = 0x2000)
  2. Bucle: Se decrementa el par de registros (DEC DE)
  3. Verificación: Se verifica si el par llegó a 0 (usando OR o ADD para combinar los bytes y verificar flags)
  4. Repetición: Si no es 0, se repite el bucle

Ejemplo de código:

LD DE, 0x2000    ; Carga valor inicial
.loop:
    DEC DE       ; Decrementa DE
    LD A, D      ; Carga D en A
    OR E         ; A = D | E (si D=0 y E=0, entonces A=0 y Z=1)
    JR NZ, .loop ; Salta si Z=0 (si DE != 0)

2. Cálculo de Tiempo Real

El tiempo que tarda un bucle de retardo depende de:

  • Valor inicial: Si DE se carga con 0xFFFF, el bucle ejecutará 65,536 iteraciones
  • Ciclos por iteración: Cada iteración consume varios M-Cycles (T-Cycles / 4)
  • Frecuencia de CPU: La Game Boy funciona a ~4.19 MHz (4,194,304 Hz)

Ejemplo de cálculo: Si cada iteración consume 10 T-Cycles y DE se carga con 0x2000 (8,192):

  • Total de T-Cycles: 8,192 × 10 = 81,920 T-Cycles
  • Tiempo real: 81,920 / 4,194,304 ≈ 19.5 ms

En un emulador, si el bucle está mal implementado o si la ALU de 16 bits tiene un bug, DE podría no decrementar correctamente, causando un bucle infinito (la "ilusión del atascamiento").

3. Validación de DEC DE (Opcode 0x1B)

La instrucción DEC DE (opcode 0x1B) decrementa el par de registros DE en 1. Según la especificación del LR35902:

  • Ciclos: 2 M-Cycles (8 T-Cycles)
  • Flags: NO afecta flags (a diferencia de DEC r que sí afecta Z, N, H)
  • Wrap-around: Si DE = 0x0000, después de DEC DE, DE = 0xFFFF (wrap-around en 16 bits)

Es crítico que esta instrucción funcione correctamente porque muchos bucles de retardo dependen de ella. Si DEC DE no decrementa correctamente, el bucle se vuelve infinito y el juego se congela.

Fuente: Pan Docs - CPU Instruction Set - DEC rr

Implementación

Se implementaron tres puntos de instrumentación en CPU.cpp para monitorear el bucle de retardo:

1. Captura de Carga Inicial de DE (PC:0x614A)

En el caso 0x11 (LD DE, nn), se agregó una verificación para detectar cuando el PC original (antes del fetch) era 0x614A. Cuando se detecta esta condición, se imprime el valor que se está cargando en DE junto con los bytes de memoria desde donde se lee (0x614B y 0x614C, en formato little-endian).

case 0x11:  // LD DE, d16
{
    uint16_t value = fetch_word();
    regs_->set_de(value);
    
    // Step 0277: Capturar carga inicial de DE en PC:0x614A
    if (saved_pc_for_instrumentation == 0x614A) {
        printf("[SNIPER-LOAD] PC:0x614A | Cargando DE con valor: 0x%04X ...\n", value);
    }
    ...
}

2. Monitor de Decremento (PC:0x6150)

En el caso 0x1B (DEC DE), se agregó un monitor que imprime el estado de DE cada 1000 iteraciones cuando el PC original era 0x6150. Esto permite verificar que DE está disminuyendo correctamente sin saturar el log con demasiadas líneas.

case 0x1B:  // DEC DE
{
    dec_16bit(1);  // 1 = DE
    
    // Step 0277: Monitorizar decremento cada 1000 iteraciones
    if (saved_pc_for_instrumentation == 0x6150) {
        static uint32_t loop_counter = 0;
        loop_counter++;
        if (loop_counter % 1000 == 0) {
            printf("[SNIPER-DELAY] Iteración:%u | DE:0x%04X | LY:%d DIV:0x%02X\n", ...);
        }
    }
    ...
}

3. Trigger de Salida del Bucle

Al inicio de step(), antes de procesar interrupciones, se agregó una verificación para detectar cuando el PC sale del rango 0x614A-0x6155. Esto indica que el bucle de retardo ha terminado y el juego continúa con la ejecución normal.

// Step 0277: Trigger de salida del bucle
static uint16_t last_pc_in_loop = 0;
if (last_pc_in_loop >= 0x614A && last_pc_in_loop <= 0x6155 && 
    !(regs_->pc >= 0x614A && regs_->pc <= 0x6155)) {
    printf("[SNIPER-EXIT] ¡LIBERTAD! El bucle de retardo ha terminado...\n");
}

Componentes creados/modificados

  • CPU.cpp: Se modificó el método step() para agregar los tres puntos de instrumentación
  • CPU.cpp: Se agregó una variable estática saved_pc_for_instrumentation para rastrear el PC original antes del fetch

Decisiones de diseño

Uso de variable estática para PC original: Se usa una variable estática saved_pc_for_instrumentation que se actualiza al inicio de step() con el PC original (antes del fetch). Esta variable es accesible desde los casos del switch, permitiendo verificar si una instrucción específica se ejecutó en una dirección crítica.

Muestreo cada 1000 iteraciones: El monitor de decremento solo imprime cada 1000 iteraciones para evitar saturar el log. Si DE se carga con 0xFFFF, esto generará aproximadamente 65 líneas de log, lo cual es manejable.

Verificación de salida del bucle: Se verifica si el PC sale del rango 0x614A-0x6155 comparando el PC actual con el PC de la iteración anterior. Esto permite detectar cuando el juego sale del bucle sin necesidad de instrumentar cada instrucción fuera del bucle.

Archivos Afectados

  • src/core/cpp/CPU.cpp - Modificado método step() para agregar monitores específicos: carga DE (0x614A), decremento (0x6150), y salida del bucle (0x614A-0x6155)
  • src/core/cpp/CPU.cpp - Modificado caso 0x11 (LD DE, nn) para capturar carga inicial
  • src/core/cpp/CPU.cpp - Modificado caso 0x1B (DEC DE) para monitorear decremento

Tests y Verificación

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

  • Comando ejecutado: python main.py roms/pkmn.gb
  • Resultado esperado: Se deberían ver tres tipos de mensajes en el log:
    • [SNIPER-LOAD]: Muestra el valor inicial cargado en DE
    • [SNIPER-DELAY]: Muestra el estado de DE cada 1000 iteraciones (si DE se carga con 0xFFFF, debería haber ~65 mensajes)
    • [SNIPER-EXIT]: Indica que el bucle terminó y el juego continúa

Validación de DEC DE

Se verificó que la instrucción DEC DE (opcode 0x1B) está correctamente implementada en el código:

case 0x1B:  // DEC DE
{
    dec_16bit(1);  // 1 = DE
    cycles_ += 2;
    return 2;
}

La función dec_16bit(1) está correctamente implementada y decrementa DE usando wrap-around en 16 bits:

case 1: {  // DE
    uint16_t de = regs_->get_de();
    de = (de - 1) & 0xFFFF;
    regs_->set_de(de);
    break;
}

Validación de módulo compilado C++: ✅ Compilación exitosa sin errores de linter. La instrumentación está lista para ser probada en ejecución.

Fuentes Consultadas

Nota: Implementación basada en conocimiento general de arquitectura LR35902 y especificaciones de Pan Docs.

Integridad Educativa

Lo que Entiendo Ahora

  • Bucles de retardo por software: Son una técnica común para crear pausas temporales sin usar hardware de timer o interrupciones. Funcionan decrementando un registro de 16 bits hasta que llega a 0.
  • Cálculo de tiempo real: El tiempo que tarda un bucle de retardo depende del valor inicial, los ciclos por iteración, y la frecuencia de CPU (~4.19 MHz). Si DE se carga con 0xFFFF, el bucle puede tardar varios milisegundos en completarse.
  • La "ilusión del atascamiento": Si un bucle de retardo se carga con un valor muy grande (ej: 0xFFFF), puede parecer que el juego está congelado cuando en realidad solo está esperando a que el bucle termine. Esto es especialmente problemático en emuladores si la ALU de 16 bits tiene un bug.
  • DEC DE no afecta flags: A diferencia de DEC r (que afecta Z, N, H), DEC DE (y otros DEC rr) NO afecta flags. Esto es importante para la lógica de los bucles de retardo.

Lo que Falta Confirmar

  • Valor inicial de DE: ¿Qué valor se carga en DE en PC:0x614A? Si es 0xFFFF, el bucle tardará ~65,536 iteraciones. Si es un valor más pequeño, el bucle terminará más rápido.
  • Decremento correcto: ¿DE está disminuyendo correctamente? Si los mensajes [SNIPER-DELAY] muestran que DE no cambia o se queda clavado en un valor, hay un bug en dec_16bit().
  • Salida del bucle: ¿El juego sale del bucle cuando DE llega a 0? Si vemos [SNIPER-EXIT], confirmamos que el bucle terminó correctamente. Si no lo vemos, el juego podría estar atascado por otra razón.

Hipótesis y Suposiciones

Hipótesis principal: El "bloqueo" es en realidad un bucle de retardo por software 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).

Suposición sobre el valor inicial: Asumimos que DE se carga con un valor razonable (no 0xFFFF), pero esto necesita confirmarse con los logs [SNIPER-LOAD]. Si DE se carga con 0xFFFF, el bucle tardará un tiempo considerable en completarse.

Próximos Pasos

  • [ ] Ejecutar Pokémon Red y analizar los logs [SNIPER-LOAD] para ver qué valor se carga en DE
  • [ ] Verificar que DE está disminuyendo correctamente usando los logs [SNIPER-DELAY]
  • [ ] Confirmar que el bucle termina cuando DE llega a 0 (buscar [SNIPER-EXIT])
  • [ ] Si DE no está disminuyendo, investigar y corregir el bug en dec_16bit()
  • [ ] Si el bucle termina pero el juego sigue atascado, investigar qué ocurre después del bucle
  • [ ] Calcular el tiempo real que tarda el bucle basándose en el valor inicial de DE y los ciclos por iteración