⚠️ 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: Implementar DEC (HL) para Romper Segundo Bucle Infinito

Fecha: 2025-12-20 Step ID: 0159 Estado: ✅ VERIFIED

Resumen

Se implementaron los opcodes faltantes INC (HL) (0x34) y DEC (HL) (0x35) en la CPU de C++ para completar la familia de instrucciones de incremento y decremento. Aunque el diagnóstico inicial apuntaba a DEC C (0x0D), este ya estaba implementado; el verdadero problema era la ausencia de los opcodes que operan sobre memoria indirecta. Con esta implementación, los bucles de limpieza de memoria en las ROMs ahora pueden ejecutarse correctamente, permitiendo que el PC avance más allá de la barrera de 0x0300 y la traza disparada capture el código que se ejecuta después de la inicialización.

Concepto de Hardware

El Game Boy LR35902 soporta instrucciones de incremento y decremento de 8 bits sobre registros directos (B, C, D, E, H, L, A) y también sobre memoria indirecta usando el par de registros HL como puntero.

Las instrucciones INC (HL) y DEC (HL) funcionan de la siguiente manera:

  • INC (HL) (opcode 0x34): Lee el valor de memoria en la dirección apuntada por HL, lo incrementa en 1, actualiza los flags Z, N y H según corresponda, y escribe el resultado de vuelta a memoria. Consume 3 M-Cycles (lectura + operación + escritura).
  • DEC (HL) (opcode 0x35): Lee el valor de memoria en la dirección apuntada por HL, lo decrementa en 1, actualiza los flags Z, N y H, y escribe el resultado de vuelta a memoria. También consume 3 M-Cycles.

Estos opcodes son críticos para bucles que limpian regiones de memoria (como los bucles de inicialización en las ROMs de Game Boy). Si faltan, la CPU devuelve 0 ciclos desde el default case del switch, causando que el motor de timing se detenga y LY se quede atascado en 0.

Referencia: Pan Docs - Instruction Set - Arithmetic Operations (INC/DEC).

Implementación

Se añadieron dos casos al switch principal de CPU::step() en src/core/cpp/CPU.cpp:

Opcodes Implementados

  • 0x34 - INC (HL): Utiliza alu_inc() para incrementar el valor leído de memoria y actualizar flags. Consume 3 M-Cycles.
  • 0x35 - DEC (HL): Utiliza alu_dec() para decrementar el valor leído de memoria y actualizar flags. Consume 3 M-Cycles.

Implementación en C++

Ambos opcodes siguen el mismo patrón:

  1. Obtener la dirección apuntada por HL usando regs_->get_hl().
  2. Leer el valor actual de memoria usando mmu_->read(addr).
  3. Aplicar la operación (incremento o decremento) usando los helpers ALU existentes (alu_inc() o alu_dec()).
  4. Escribir el resultado de vuelta a memoria usando mmu_->write(addr, result).
  5. Actualizar el contador de ciclos y retornar 3 M-Cycles.

Decisiones de Diseño

Se reutilizaron los helpers ALU existentes (alu_inc() y alu_dec()) en lugar de duplicar la lógica de cálculo de flags. Esto mantiene la consistencia y facilita el mantenimiento. Los helpers ya manejan correctamente:

  • Flag Z (resultado == 0)
  • Flag N (1 para DEC, 0 para INC)
  • Flag H (half-carry/half-borrow)
  • Preservación del flag C (quirk del hardware)

Archivos Afectados

  • src/core/cpp/CPU.cpp - Añadidos casos 0x34 (INC (HL)) y 0x35 (DEC (HL)) al switch principal
  • tests/test_core_cpu_inc_dec.py - Añadidos tres tests nuevos: test_inc_hl_indirect, test_dec_hl_indirect, y test_dec_hl_indirect_half_borrow

Tests y Verificación

Se añadieron tres tests unitarios para validar la implementación:

  • test_inc_hl_indirect: Verifica que INC (HL) incrementa correctamente el valor en memoria y actualiza flags.
  • test_dec_hl_indirect: Verifica que DEC (HL) decrementa correctamente el valor en memoria y activa el flag Z cuando el resultado es 0.
  • test_dec_hl_indirect_half_borrow: Verifica que DEC (HL) detecta correctamente el half-borrow (0x10 -> 0x0F) y activa el flag H.

Comando ejecutado:

pytest tests/test_core_cpu_inc_dec.py::TestCoreCPUIncDec::test_inc_hl_indirect \
       tests/test_core_cpu_inc_dec.py::TestCoreCPUIncDec::test_dec_hl_indirect \
       tests/test_core_cpu_inc_dec.py::TestCoreCPUIncDec::test_dec_hl_indirect_half_borrow -v

Resultado:

============================== 3 passed in 0.08s ==============================

Código del Test (Fragmento):

def test_dec_hl_indirect(self):
    """Verificar que DEC (HL) decrementa el valor en memoria apuntado por HL."""
    mmu = PyMMU()
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    
    regs.pc = 0x8000
    regs.hl = 0xC000
    mmu.write(0xC000, 0x01)  # Valor inicial en memoria
    
    mmu.write(0x8000, 0x35)  # DEC (HL)
    cpu.step()
    
    assert mmu.read(0xC000) == 0x00, "DEC (HL) debe decrementar el valor en memoria"
    assert regs.flag_z == True, "Z debe estar activo (resultado == 0)"
    assert regs.flag_n == True, "N debe estar activo (es decremento)"

Validación Nativa: Los tests validan directamente el módulo compilado C++ a través del wrapper Cython. La CPU nativa ejecuta las instrucciones y los tests verifican el resultado en memoria y los flags.

Fuentes Consultadas

  • Pan Docs: CPU Instruction Set - Sección de operaciones aritméticas (INC/DEC)
  • GBEDG: Instruction Timing - Timing de instrucciones de 8 bits sobre memoria indirecta

Nota: La implementación se basa en la especificación oficial del LR35902 documentada en Pan Docs. Los helpers ALU ya implementados (alu_inc, alu_dec) fueron reutilizados para mantener consistencia.

Integridad Educativa

Lo que Entiendo Ahora

  • Direccionamiento Indirecto: Las instrucciones que operan sobre (HL) requieren un acceso adicional a memoria, aumentando el tiempo de ejecución de 1 a 3 M-Cycles (lectura + operación + escritura).
  • Bucle Infinito por Opcode Faltante: Cuando la CPU encuentra un opcode no implementado, el default case del switch devuelve 0 ciclos, causando que el motor de timing se detenga y el emulador entre en un deadlock lógico donde el tiempo no avanza.
  • Diagnóstico de LY Atascado: Si LY está permanentemente en 0, indica que ppu.step() nunca recibe ciclos suficientes para avanzar una línea de escaneo, lo que apunta directamente a que la CPU está devolviendo 0 ciclos.

Lo que Falta Confirmar

  • Ejecución Real en ROMs: Aunque los tests unitarios pasan, falta verificar que los bucles de limpieza de memoria en las ROMs reales ahora se ejecutan correctamente y permiten que la inicialización continúe.
  • Traza Disparada: Con estos opcodes implementados, el PC debería avanzar más allá de 0x0300 y activar la traza disparada, proporcionando información sobre el código que se ejecuta después de los bucles de inicialización.

Hipótesis y Suposiciones

La hipótesis inicial era que DEC C (0x0D) faltaba, pero al revisar el código se descubrió que ya estaba implementado. El verdadero problema eran los opcodes de memoria indirecta INC (HL) y DEC (HL). Esto demuestra la importancia de verificar exhaustivamente todos los opcodes relacionados antes de asumir cuál es el culpable.

Próximos Pasos

  • [ ] Ejecutar el emulador con una ROM real (ej: tetris.gb) y verificar que LY ahora avanza correctamente
  • [ ] Confirmar que la traza disparada se activa cuando el PC alcanza 0x0300
  • [ ] Analizar las 100 instrucciones capturadas por la traza para identificar qué opcodes adicionales pueden estar faltando
  • [ ] Continuar implementando opcodes faltantes hasta que la inicialización de la ROM se complete