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

Control Flow Completion (Calls, Rets, RSTs)

Fecha: 2025-12-23 Step ID: 0269 Estado: Draft

Resumen

Este Step completa el conjunto de instrucciones de control de flujo de la CPU implementando todas las instrucciones condicionales y RST que faltaban.

El diagnóstico del Step 0268 reveló que el Stack Pointer seguía corrompido (`SP:210A`) incluso después de implementar las matemáticas de pila. La causa raíz era un Desastre de Flujo de Control: si el juego ejecuta `CALL Z` o `RST 28` y no están implementadas, actúan como NOPs, desbalanceando la pila (un `RET` posterior sacará datos erróneos) y causando el crash.

Se implementaron 17 nuevas instrucciones: 4 retornos condicionales, 4 llamadas condicionales, 4 saltos absolutos condicionales, 8 restarts (RST) y 1 salto indirecto (JP HL). Sin estas instrucciones, la lógica del juego es un queso gruyère lleno de agujeros.

Concepto de Hardware

El control de flujo es el mecanismo que permite a la CPU cambiar el orden de ejecución de las instrucciones. En la Game Boy, hay varios tipos de instrucciones de control de flujo:

Retornos Condicionales (RET cc)

Las instrucciones `RET NZ`, `RET Z`, `RET NC` y `RET C` retornan de una subrutina solo si se cumple una condición específica basada en los flags. Si la condición no se cumple, la ejecución continúa normalmente.

  • RET NZ (0xC0): Retorna si Z=0 (Not Zero)
  • RET Z (0xC8): Retorna si Z=1 (Zero)
  • RET NC (0xD0): Retorna si C=0 (No Carry)
  • RET C (0xD8): Retorna si C=1 (Carry)

Timing: 5 M-Cycles si se cumple (hace pop y salta), 2 M-Cycles si no (continúa).

Llamadas Condicionales (CALL cc, nn)

Las instrucciones `CALL NZ`, `CALL Z`, `CALL NC` y `CALL C` llaman a una subrutina solo si se cumple una condición específica. CRÍTICO: Siempre leen `nn` (para mantener el PC alineado), pero solo hacen push y saltan si la condición se cumple.

  • CALL NZ, nn (0xC4): Llama si Z=0
  • CALL Z, nn (0xCC): Llama si Z=1
  • CALL NC, nn (0xD4): Llama si C=0
  • CALL C, nn (0xDC): Llama si C=1

Timing: 6 M-Cycles si se cumple (push y salta), 3 M-Cycles si no (lee nn y continúa).

Saltos Absolutos Condicionales (JP cc, nn)

Similar a las llamadas condicionales, pero sin modificar la pila. Solo saltan si se cumple la condición.

  • JP NZ, nn (0xC2): Salta si Z=0
  • JP Z, nn (0xCA): Salta si Z=1
  • JP NC, nn (0xD2): Salta si C=0
  • JP C, nn (0xDA): Salta si C=1

Timing: 4 M-Cycles si salta, 3 M-Cycles si no.

Restarts (RST n) - CRÍTICO PARA POKÉMON

Las instrucciones RST son llamadas rápidas de 1 byte que hacen `PUSH PC` y saltan a una dirección fija. Son extremadamente eficientes y Pokémon las usa intensivamente para funciones del sistema (cambio de bancos de memoria, manejo de gráficos, etc.).

Hay 8 instrucciones RST, cada una salta a una dirección específica:

  • RST 00 (0xC7): Salta a 0x0000
  • RST 08 (0xCF): Salta a 0x0008
  • RST 10 (0xD7): Salta a 0x0010
  • RST 18 (0xDF): Salta a 0x0018
  • RST 20 (0xE7): Salta a 0x0020
  • RST 28 (0xEF): Salta a 0x0028 (muy usado en Pokémon)
  • RST 30 (0xF7): Salta a 0x0030
  • RST 38 (0xFF): Salta a 0x0038

Timing: 4 M-Cycles (todas).

Implementación: `push_word(regs_->pc); regs_->pc = 0x00XX;`

Salto Indirecto (JP HL)

La instrucción `JP (HL)` (0xE9) permite saltar a una dirección calculada dinámicamente almacenada en HL. Es útil para tablas de saltos y funciones virtuales.

Timing: 1 M-Cycle.

¿Por qué son críticas?

Si estas instrucciones faltan o están mal implementadas:

  1. El juego intenta llamar a una función vital usando `CALL NZ, aaaa`.
  2. Si no está implementada, la CPU trata ese byte como un opcode no reconocido (o NOP), avanza el PC, pero no empuja nada al Stack ni salta.
  3. El juego sigue ejecutando linealmente.
  4. De repente se encuentra un `RET` (que sí tenemos implementado).
  5. Hace `POP` de la pila. Pero como nunca hicimos el `PUSH` del `CALL`, sacamos basura (o underflow).
  6. SP se rompe. PC salta a la basura (210A en ROM).

Fuente: Pan Docs - "CPU Instruction Set", "Control Flow Instructions", "RST Instructions"

Implementación

Se implementaron 17 nuevas instrucciones en el método step() de CPU.cpp, organizadas en secciones lógicas dentro del switch.

Retornos Condicionales

case 0xC0:  // RET NZ
{
    if (!regs_->get_flag_z()) {
        uint16_t return_addr = pop_word();
        regs_->pc = return_addr;
        cycles_ += 5;
        return 5;
    } else {
        cycles_ += 2;
        return 2;
    }
}

(Similar para RET Z, RET NC, RET C)

Llamadas Condicionales

case 0xC4:  // CALL NZ, nn
{
    uint16_t target = fetch_word();  // Siempre leer nn para mantener PC alineado
    
    if (!regs_->get_flag_z()) {
        uint16_t return_addr = regs_->pc;
        push_word(return_addr);
        regs_->pc = target;
        cycles_ += 6;
        return 6;
    } else {
        cycles_ += 3;
        return 3;
    }
}

Nota crítica: Siempre leemos `nn` incluso si la condición no se cumple, para mantener el PC alineado correctamente.

Saltos Absolutos Condicionales

case 0xC2:  // JP NZ, nn
{
    uint16_t target = fetch_word();  // Siempre leer nn
    
    if (!regs_->get_flag_z()) {
        regs_->pc = target;
        cycles_ += 4;
        return 4;
    } else {
        cycles_ += 3;
        return 3;
    }
}

Restarts (RST)

case 0xEF:  // RST 28 (Restart to 0x0028)
{
    uint16_t return_addr = regs_->pc;
    push_word(return_addr);
    regs_->pc = 0x0028;
    cycles_ += 4;
    return 4;
}

(Similar para los otros 7 RST: 0xC7, 0xCF, 0xD7, 0xDF, 0xE7, 0xF7, 0xFF)

Salto Indirecto

case 0xE9:  // JP (HL)
{
    uint16_t hl = regs_->get_hl();
    regs_->pc = hl;
    cycles_ += 1;
    return 1;
}

Decisiones de Diseño

  • Lectura de operandos: En instrucciones condicionales, siempre leemos los operandos (nn) incluso si la condición no se cumple, para mantener el PC alineado correctamente. Esto replica el comportamiento del hardware real.
  • Organización del código: Las instrucciones se agrupan en secciones lógicas (Retornos, Llamadas, Saltos, RST) para facilitar el mantenimiento.
  • Timing preciso: Cada instrucción retorna el número exacto de M-Cycles según Pan Docs, diferenciando entre cuando la condición se cumple y cuando no.

Archivos Afectados

  • src/core/cpp/CPU.cpp - Agregadas 17 nuevas instrucciones de control de flujo en el método step():
    • 4 retornos condicionales (0xC0, 0xC8, 0xD0, 0xD8)
    • 4 llamadas condicionales (0xC4, 0xCC, 0xD4, 0xDC)
    • 4 saltos absolutos condicionales (0xC2, 0xCA, 0xD2, 0xDA)
    • 8 restarts (0xC7, 0xCF, 0xD7, 0xDF, 0xE7, 0xEF, 0xF7, 0xFF)
    • 1 salto indirecto (0xE9)

Tests y Verificación

Validación de módulo compilado C++: Las instrucciones se implementaron directamente en C++ y requieren recompilación.

Comando de compilación:

.\rebuild_cpp.ps1

Comando de prueba:

python main.py roms/pkmn.gb

Verificaciones esperadas:

  • El Stack Pointer debería tener valores "sanos" como DFFX o FFFX (en WRAM o HRAM), no 210A.
  • El juego debería avanzar más allá del bucle de espera y mostrar gráficos nuevos en pantalla.
  • Si esto funciona, es el Jaque Mate: Pokémon usa RST intensivamente para cambiar bancos de memoria y manejar gráficos.

Nota: Los tests unitarios completos se pueden implementar en un Step futuro, siguiendo el patrón de tests existentes.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Desastre de Flujo de Control: Si una instrucción condicional (como CALL Z) no está implementada, actúa como NOP, desbalanceando la pila. Cuando luego se ejecuta un RET, saca datos erróneos y corrompe el SP.
  • Lectura de Operandos: En instrucciones condicionales, siempre debemos leer los operandos (nn) incluso si la condición no se cumple, para mantener el PC alineado correctamente.
  • RST en Pokémon: Las instrucciones RST son críticas para Pokémon, que las usa intensivamente para funciones del sistema como cambio de bancos de memoria y manejo de gráficos.

Lo que Falta Confirmar

  • Validación con ROMs reales: Necesitamos ejecutar el emulador con Pokémon Red y verificar que el SP ya no se corrompe y que el juego avanza correctamente.
  • Tests unitarios: Implementar tests unitarios completos que validen el comportamiento de las instrucciones condicionales en casos límite.

Hipótesis y Suposiciones

Asumimos que la implementación es correcta basándonos en la documentación de Pan Docs. Sin embargo, la validación final requiere ejecutar el emulador con ROMs reales y verificar que el comportamiento es correcto. Si el SP sigue corrompido después de este Step, necesitaremos investigar otras causas posibles (como instrucciones CB faltantes o problemas en la gestión de memoria).

Próximos Pasos

  • [ ] Recompilar el módulo C++ con .\rebuild_cpp.ps1
  • [ ] Ejecutar el emulador con Pokémon Red y verificar que el SP ya no se corrompe
  • [ ] Verificar que el juego avanza más allá del bucle de espera y muestra gráficos
  • [ ] Si el SP sigue corrompido, investigar otras causas posibles (instrucciones CB faltantes, problemas en gestión de memoria, etc.)
  • [ ] Implementar tests unitarios completos para las nuevas instrucciones (opcional, puede ser un Step futuro)