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

Stack Math Implementation (0xE8, 0xF8, 0xF9)

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

Resumen

Este Step implementa las tres instrucciones críticas de aritmética de pila ("Stack Math") que faltaban en la CPU: ADD SP, e (0xE8), LD HL, SP+e (0xF8) y LD SP, HL (0xF9).

El diagnóstico del Step 0267 reveló que el Stack Pointer estaba corrompido (SP:210A, apuntando a ROM). La causa más probable era la falta de estas instrucciones, que los juegos usan para gestionar variables locales en la pila.

La implementación es quirúrgica: los flags H y C se calculan basándose en el byte bajo de SP (como si fuera una suma de 8 bits), no en el resultado completo de 16 bits. Este comportamiento específico del hardware LR35902 es crítico para la precisión.

Concepto de Hardware

La Game Boy tiene unas instrucciones extrañas pero vitales para el lenguaje C (y para Pokémon): operar con el Stack Pointer (SP) como si fuera un registro de datos normal.

ADD SP, e (0xE8)

Suma un valor con signo (positivo o negativo) al SP. Se usa para reservar o liberar espacio para variables locales en la pila.

La trampa: Los flags H y C se calculan basándose en el byte bajo (como si fuera una suma de 8 bits), ¡no en el resultado de 16 bits! Es un comportamiento muy específico de la CPU LR35902.

  • Z: Siempre 0 (reset).
  • N: Siempre 0 (es suma).
  • H: Carry desde el bit 3 (nibble bajo). Fórmula: ((sp & 0xF) + (offset & 0xF)) > 0xF.
  • C: Carry desde el bit 7 (byte bajo). Fórmula: ((sp & 0xFF) + (offset & 0xFF)) > 0xFF.

Timing: 4 M-Cycles (16 T-Cycles).

LD HL, SP+e (0xF8)

Calcula la dirección de una variable en la pila y la pone en HL. Usa la misma lógica de flags extraña que ADD SP, e.

Importante: SP NO se modifica. Solo se usa para el cálculo.

Timing: 3 M-Cycles (12 T-Cycles).

LD SP, HL (0xF9)

Mueve HL a SP. Esencial para restaurar la pila después de operaciones temporales.

Flags: No afecta flags.

Timing: 2 M-Cycles (8 T-Cycles).

¿Por qué son críticas?

Si estas instrucciones faltan o están mal implementadas, el SP acaba apuntando a Narnia (o a la ROM 0x210A, como vimos en el Step 0267), y el juego explota. Los compiladores C generan código que usa estas instrucciones constantemente para:

  • Reservar espacio para variables locales: ADD SP, -8 (reserva 8 bytes).
  • Acceder a variables locales: LD HL, SP+4 (accede a la variable en offset +4).
  • Restaurar la pila: LD SP, HL (restaura SP desde un registro temporal).

Fuente: Pan Docs - "CPU Instruction Set", "ADD SP, r8", "LD HL, SP+r8", "LD SP, HL"

Implementación

Se implementaron los tres opcodes en el método step() de CPU.cpp, justo antes del prefijo CB (0xCB).

Case 0xE8: ADD SP, e

case 0xE8:  // ADD SP, e
{
    // Leer offset con signo
    uint8_t offset_raw = fetch_byte();
    int8_t offset = static_cast<int8_t>(offset_raw);
    
    // Guardar SP original para cálculo de flags
    uint16_t sp_old = regs_->sp;
    uint8_t sp_low = sp_old & 0xFF;
    
    // Calcular nuevo SP
    uint16_t sp_new = (sp_old + offset) & 0xFFFF;
    regs_->sp = sp_new;
    
    // Calcular flags (CRÍTICO: basados en byte bajo)
    regs_->set_flag_z(false);  // Z: siempre 0
    regs_->set_flag_n(false);  // N: siempre 0
    
    // H: Half-carry desde bit 3
    uint8_t offset_unsigned = static_cast<uint8_t>(offset_raw);
    uint8_t sp_low_nibble = sp_low & 0x0F;
    uint8_t offset_low_nibble = offset_unsigned & 0x0F;
    bool half_carry = (sp_low_nibble + offset_low_nibble) > 0x0F;
    regs_->set_flag_h(half_carry);
    
    // C: Carry desde bit 7
    bool carry = ((static_cast<uint16_t>(sp_low) + static_cast<uint16_t>(offset_unsigned)) & 0x100) != 0;
    regs_->set_flag_c(carry);
    
    cycles_ += 4;
    return 4;
}

Case 0xF8: LD HL, SP+e

case 0xF8:  // LD HL, SP+e
{
    // Leer offset con signo
    uint8_t offset_raw = fetch_byte();
    int8_t offset = static_cast<int8_t>(offset_raw);
    
    // Guardar SP para cálculo de flags (NO se modifica)
    uint16_t sp = regs_->sp;
    uint8_t sp_low = sp & 0xFF;
    
    // Calcular HL = SP + offset
    uint16_t hl_new = (sp + offset) & 0xFFFF;
    regs_->set_hl(hl_new);
    
    // Calcular flags (idéntico a ADD SP, e)
    regs_->set_flag_z(false);
    regs_->set_flag_n(false);
    
    uint8_t offset_unsigned = static_cast<uint8_t>(offset_raw);
    uint8_t sp_low_nibble = sp_low & 0x0F;
    uint8_t offset_low_nibble = offset_unsigned & 0x0F;
    bool half_carry = (sp_low_nibble + offset_low_nibble) > 0x0F;
    regs_->set_flag_h(half_carry);
    
    bool carry = ((static_cast<uint16_t>(sp_low) + static_cast<uint16_t>(offset_unsigned)) & 0x100) != 0;
    regs_->set_flag_c(carry);
    
    cycles_ += 3;
    return 3;
}

Case 0xF9: LD SP, HL

case 0xF9:  // LD SP, HL
{
    uint16_t hl = regs_->get_hl();
    regs_->sp = hl;
    cycles_ += 2;
    return 2;
}

Decisiones de Diseño

  • Cálculo de Flags: Se usa el byte bajo de SP y el offset como unsigned para el cálculo de flags, pero el offset se interpreta como signed para la suma real. Esto replica el comportamiento del hardware.
  • Ubicación en el Switch: Se insertaron justo antes del prefijo CB para mantener el orden lógico de opcodes.
  • Watchdog del Step 0267: Se mantiene activo para verificar que estas instrucciones no corrompan el SP.

Archivos Afectados

  • src/core/cpp/CPU.cpp - Agregados casos 0xE8, 0xF8 y 0xF9 en el método step().

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 mensaje [CRITICAL] SP CORRUPTION del Step 0267 debería dejar de aparecer (o aparecer mucho menos frecuentemente).
  • El GPS debería mostrar valores de SP "sanos" como DFFX o FFFX (en WRAM o HRAM).
  • El juego debería avanzar más allá del bucle de espera y mostrar gráficos nuevos en pantalla.

Nota: Los tests unitarios completos se pueden implementar en un Step futuro, siguiendo el patrón de tests/test_cpu_sp_arithmetic.py de la versión Python.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Flags en Stack Math: Los flags H y C en ADD SP, e y LD HL, SP+e se calculan basándose en el byte bajo de SP, no en el resultado completo de 16 bits. Esto es diferente a ADD HL, rr, donde los flags se calculan en los 12 bits bajos (H) y 16 bits (C).
  • Uso en Compiladores C: Estas instrucciones son fundamentales para el código generado por compiladores C, que las usan constantemente para gestionar el stack frame y variables locales.
  • Corrupción de SP: Si estas instrucciones faltan o están mal implementadas, el SP puede corromperse y apuntar a memoria de solo lectura (ROM), causando que el juego se estrelle.

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 en C++ o Python que validen el cálculo de flags en casos límite (overflow, underflow, etc.).

Hipótesis y Suposiciones

Asumimos que la implementación de flags es correcta basándonos en la documentación de Pan Docs y la implementación de referencia en Python (v0.0.1). Sin embargo, la validación final requiere ejecutar el emulador con ROMs reales y verificar que el comportamiento es correcto.

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 watchdog del Step 0267 sigue detectando corrupción, analizar qué otras instrucciones pueden estar causando el problema
  • [ ] Implementar tests unitarios completos para las tres instrucciones (opcional, puede ser un Step futuro)