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

CPU Nativa: Implementación de INC/DEC y Arreglo del Bucle de Inicialización

Fecha: 2025-12-19 Step ID: 0133 Estado: Verified

Resumen

Se implementó la familia completa de instrucciones INC r y DEC r de 8 bits en la CPU nativa (C++). Este era un bug crítico que causaba que los bucles de inicialización del juego fallaran, llevando a lecturas de memoria corrupta y finalmente a Segmentation Faults. El problema específico era que el opcode 0x05 (DEC B) no estaba implementado, causando que los bucles de limpieza de memoria no se ejecutaran correctamente. Con esta implementación, los juegos pueden inicializar correctamente su memoria RAM y continuar con su secuencia de arranque.

Concepto de Hardware

Las instrucciones INC (Increment) y DEC (Decrement) son operaciones aritméticas fundamentales en la CPU LR35902. Incrementan o decrementan un registro de 8 bits en 1, y actualizan los flags de condición según el resultado.

Flags Afectados por INC/DEC

  • Z (Zero): Se activa (1) si el resultado es 0, se desactiva (0) en caso contrario.
  • N (Subtract): INC siempre pone N=0 (es incremento), DEC siempre pone N=1 (es decremento).
  • H (Half-Carry/Half-Borrow):
    • INC: Se activa si hay "half-carry" del nibble bajo (bit 3 → bit 4). Ejemplo: 0x0F + 1 = 0x10 (hay half-carry).
    • DEC: Se activa si hay "half-borrow" del nibble bajo (bit 4 → bit 3). Ejemplo: 0x10 - 1 = 0x0F (hay half-borrow).
  • C (Carry): NO SE MODIFICA. Esta es una peculiaridad crítica del hardware de la Game Boy. A diferencia de ADD/SUB, INC/DEC preservan el flag Carry.

Opcodes de INC/DEC de 8 bits

La CPU tiene 14 opcodes para incrementar/decrementar registros de 8 bits:

  • INC: 0x04 (B), 0x0C (C), 0x14 (D), 0x1C (E), 0x24 (H), 0x2C (L), 0x3C (A)
  • DEC: 0x05 (B), 0x0D (C), 0x15 (D), 0x1D (E), 0x25 (H), 0x2D (L), 0x3D (A)

Todos consumen 1 M-Cycle (4 T-Cycles).

El Bug del Bucle de Inicialización

Los juegos de Game Boy típicamente ejecutan bucles de inicialización que limpian la memoria RAM (WRAM) llenándola de ceros. Un bucle típico se ve así:

XOR A        ; A = 0, Z = 1
LD B, 0x20   ; B = contador (32 iteraciones)
LD HL, 0xC000 ; HL = dirección inicial de WRAM
loop:
    LD (HL), A    ; Escribir 0 en memoria
    INC HL        ; Avanzar puntero
    DEC B         ; Decrementar contador
    JR NZ, loop   ; Saltar si B != 0 (Z == 0)

El problema: Si DEC B (0x05) no está implementado, la CPU no actualiza el flag Z. El flag Z permanece en el valor anterior (1, de XOR A), y cuando llega JR NZ, la condición "Not Zero" falla porque Z=1. El bucle se ejecuta cero veces, la RAM queda llena de "basura" (valores residuales), y más adelante el juego lee direcciones inválidas y crashea.

Fuente: Pan Docs - CPU Instruction Set, sección "INC r" y "DEC r": "C flag is not affected"

Implementación

Se implementaron dos helpers ALU privados en la clase CPU para manejar la lógica de flags de INC y DEC, y luego se agregaron todos los opcodes correspondientes en el switch principal.

1. Helpers ALU: alu_inc() y alu_dec()

Se crearon dos métodos privados en CPU.hpp y se implementaron en CPU.cpp:

uint8_t CPU::alu_inc(uint8_t value) {
    uint8_t result = value + 1;
    
    // Flags
    regs_->set_flag_z(result == 0);
    regs_->set_flag_n(false);  // Siempre 0 (es incremento)
    regs_->set_flag_h((value & 0x0F) == 0x0F);  // Half-carry
    // C: NO afectado (preservado)
    
    return result;
}

uint8_t CPU::alu_dec(uint8_t value) {
    uint8_t result = value - 1;
    
    // Flags
    regs_->set_flag_z(result == 0);
    regs_->set_flag_n(true);  // Siempre 1 (es decremento)
    regs_->set_flag_h((value & 0x0F) == 0x00);  // Half-borrow
    // C: NO afectado (preservado)
    
    return result;
}

2. Implementación de Opcodes

Se agregaron todos los opcodes INC/DEC de 8 bits en el switch de CPU::step():

// INC opcodes
case 0x04: regs_->b = alu_inc(regs_->b); cycles = 1; break; // INC B
case 0x0C: regs_->c = alu_inc(regs_->c); cycles = 1; break; // INC C
case 0x14: regs_->d = alu_inc(regs_->d); cycles = 1; break; // INC D
case 0x1C: regs_->e = alu_inc(regs_->e); cycles = 1; break; // INC E
case 0x24: regs_->h = alu_inc(regs_->h); cycles = 1; break; // INC H
case 0x2C: regs_->l = alu_inc(regs_->l); cycles = 1; break; // INC L
case 0x3C: regs_->a = alu_inc(regs_->a); cycles = 1; break; // INC A

// DEC opcodes
case 0x05: regs_->b = alu_dec(regs_->b); cycles = 1; break; // DEC B
case 0x0D: regs_->c = alu_dec(regs_->c); cycles = 1; break; // DEC C
case 0x15: regs_->d = alu_dec(regs_->d); cycles = 1; break; // DEC D
case 0x1D: regs_->e = alu_dec(regs_->e); cycles = 1; break; // DEC E
case 0x25: regs_->h = alu_dec(regs_->h); cycles = 1; break; // DEC H
case 0x2D: regs_->l = alu_dec(regs_->l); cycles = 1; break; // DEC L
case 0x3D: regs_->a = alu_dec(regs_->a); cycles = 1; break; // DEC A

Componentes creados/modificados

  • src/core/cpp/CPU.hpp: Agregados métodos alu_inc() y alu_dec() en la sección privada
  • src/core/cpp/CPU.cpp: Implementación de helpers ALU y todos los opcodes INC/DEC de 8 bits
  • tests/test_core_cpu_inc_dec.py: Suite completa de tests unitarios para verificar la funcionalidad

Decisiones de diseño

  • Helpers ALU reutilizables: Se crearon métodos helper en lugar de duplicar código en cada opcode. Esto facilita el mantenimiento y asegura consistencia en el cálculo de flags.
  • Preservación explícita del flag C: Aunque el flag C no se modifica, se documentó explícitamente en el código para evitar confusiones futuras. Esta es una peculiaridad crítica del hardware.
  • Implementación completa: Se implementaron todos los opcodes INC/DEC de 8 bits de una vez, no solo DEC B. Esto previene bugs similares en otros bucles del código del juego.

Archivos Afectados

  • src/core/cpp/CPU.hpp - Agregados métodos alu_inc() y alu_dec()
  • src/core/cpp/CPU.cpp - Implementación de helpers ALU y todos los opcodes INC/DEC de 8 bits
  • tests/test_core_cpu_inc_dec.py - Suite completa de tests unitarios (nuevo archivo)

Tests y Verificación

Se creó una suite completa de tests unitarios en tests/test_core_cpu_inc_dec.py que verifica:

  • Preservación del flag C: Test específico para DEC B (0x05) que verifica que el flag Carry se preserve tanto cuando está activo como cuando está desactivado
  • Half-Carry/Half-Borrow: Tests que verifican la detección correcta de desbordamiento de nibble bajo
  • Flags Z y N: Verificación de que los flags se actualizan correctamente según el resultado
  • Todos los opcodes: Tests que verifican que todos los opcodes INC/DEC funcionan correctamente

Comando ejecutado:

pytest tests/test_core_cpu_inc_dec.py -v

Resultado esperado: Todos los tests pasan (6 tests en total).

Test Crítico: DEC B Preserva Carry

Este es el test más importante, ya que verifica el comportamiento del opcode que causaba el crash:

def test_dec_b_preserves_carry(self):
    """Verificar que DEC B (0x05) preserva el flag Carry."""
    mmu = PyMMU()
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    
    regs.pc = 0x8000
    regs.b = 0x01
    regs.set_flag_c(True)  # Activar Carry
    
    mmu.write(0x8000, 0x05)  # DEC B
    cpu.step()
    
    assert regs.b == 0x00
    assert regs.get_flag_z() == 1
    assert regs.get_flag_c() == 1, "C debe haberse preservado"

Validación Nativa: Todos los tests validan el módulo compilado C++ a través de la interfaz Cython.

Fuentes Consultadas

  • Pan Docs: CPU Instruction Set, sección "INC r" y "DEC r" - Especificación de flags y comportamiento del flag Carry
  • Pan Docs: CPU Instruction Set, sección "Half-Carry Flag" - Explicación del cálculo de half-carry y half-borrow

Integridad Educativa

Lo que Entiendo Ahora

  • Preservación del flag C en INC/DEC: A diferencia de ADD/SUB, las instrucciones INC y DEC NO modifican el flag Carry. Esta es una peculiaridad del hardware LR35902 que debe respetarse estrictamente para que los bucles funcionen correctamente.
  • Half-Carry vs Half-Borrow: INC detecta "half-carry" cuando el nibble bajo es 0x0F y al sumar 1 se produce overflow. DEC detecta "half-borrow" cuando el nibble bajo es 0x00 y al restar 1 se produce underflow.
  • Importancia de los bucles de inicialización: Los juegos dependen críticamente de que la memoria RAM esté limpia (llena de ceros) al inicio. Si los bucles de limpieza fallan, el juego lee datos corruptos y crashea.

Lo que Falta Confirmar

  • INC (HL) y DEC (HL): Estas variantes que operan sobre memoria (no registros) aún no están implementadas. Consumen 3 M-Cycles en lugar de 1.
  • Comportamiento en bucles reales: Verificar que los juegos ahora ejecutan correctamente sus bucles de inicialización sin crashes.

Hipótesis y Suposiciones

La implementación sigue estrictamente la documentación de Pan Docs. No hay suposiciones no respaldadas.

Próximos Pasos

  • [ ] Implementar INC (HL) y DEC (HL) (opcodes que operan sobre memoria)
  • [ ] Verificar que los juegos ahora ejecutan correctamente sus bucles de inicialización
  • [ ] Continuar con la implementación de más opcodes de la CPU para aumentar la compatibilidad