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

Completar INC/DEC de 8 bits (Todas las Variantes)

Fecha: 2025-12-17 Step ID: 0027 Estado: Verified

Resumen

Se completaron todas las variantes de INC/DEC de 8 bits que faltaban en la CPU. En el paso 9 se habían implementado INC/DEC para B, C y A, pero se dejaron fuera D, E, H, L y la versión en memoria (HL). El emulador crasheaba en el opcode 0x1D (DEC E) cuando ejecutaba Tetris DX, lo que confirmaba que faltaban estas instrucciones críticas para el manejo de contadores de bucles. Con esta implementación, la aritmética unaria de 8 bits está completa y el emulador puede avanzar más allá de la inicialización en juegos reales.

Concepto de Hardware

Las instrucciones INC (Increment) y DEC (Decrement) son operaciones aritméticas unarias que incrementan o decrementan un valor en 1. En la Game Boy, estas instrucciones están disponibles para todos los registros de 8 bits (A, B, C, D, E, H, L) y para memoria indirecta (HL).

Patrón de la Tabla de Opcodes:

  • Columna x4: INC (B, D, H, (HL))
  • Columna x5: DEC (B, D, H, (HL))
  • Columna xC: INC (C, E, L, A)
  • Columna xD: DEC (C, E, L, A)

Comportamiento de Flags:

  • Z (Zero): Se activa si el resultado es 0
  • N (Subtract): Siempre 1 en DEC, siempre 0 en INC
  • H (Half-Carry/Half-Borrow): Se activa cuando hay carry/borrow del bit 3 al 4 (nibble bajo)
  • C (Carry): NO SE TOCA - Esta es una peculiaridad crítica del hardware LR35902

INC/DEC (HL) - Operaciones Read-Modify-Write:

Las instrucciones INC (HL) y DEC (HL) son especiales porque operan sobre memoria indirecta. Estas instrucciones realizan una operación de Read-Modify-Write:

  1. Leen el valor de memoria en la dirección apuntada por HL
  2. Modifican el valor (incrementan o decrementan)
  3. Escriben el nuevo valor de vuelta en memoria

Por esta razón, estas instrucciones consumen 3 M-Cycles (12 T-Cycles) en lugar de 1: uno para leer, uno para escribir, y uno para la operación interna.

Fuente: Pan Docs - CPU Instruction Set (INC/DEC instructions, Flags behavior)

Implementación

Se implementaron los siguientes opcodes faltantes, reutilizando los métodos helper _inc_n y _dec_n que ya existían desde el paso 9:

Opcodes Implementados

  • 0x14: INC D - Incrementa el registro D
  • 0x15: DEC D - Decrementa el registro D
  • 0x1C: INC E - Incrementa el registro E
  • 0x1D: DEC E - Decrementa el registro E (¡El culpable del crash!)
  • 0x24: INC H - Incrementa el registro H
  • 0x25: DEC H - Decrementa el registro H
  • 0x2C: INC L - Incrementa el registro L
  • 0x2D: DEC L - Decrementa el registro L
  • 0x34: INC (HL) - Incrementa el valor en memoria apuntada por HL
  • 0x35: DEC (HL) - Decrementa el valor en memoria apuntada por HL

Componentes Creados/Modificados

  • src/cpu/core.py:
    • Añadidos 10 nuevos métodos: _op_inc_d, _op_dec_d, _op_inc_e, _op_dec_e, _op_inc_h, _op_dec_h, _op_inc_l, _op_dec_l, _op_inc_hl_ptr, _op_dec_hl_ptr
    • Actualizada la tabla de opcodes para registrar los nuevos handlers
    • Los métodos para registros individuales siguen el mismo patrón que B, C y A (1 M-Cycle)
    • Los métodos para (HL) implementan Read-Modify-Write (3 M-Cycles)
  • tests/test_cpu_inc_dec_full.py:
    • Nuevo archivo de tests con 10 tests completos
    • Tests para todos los registros (D, E, H, L)
    • Tests para memoria indirecta (HL)
    • Tests para preservación del flag C
    • Tests para wrap-around y activación de flags

Decisiones de Diseño

Reutilización de Helpers: Se decidió reutilizar los métodos _inc_n y _dec_n existentes en lugar de duplicar lógica. Esto asegura consistencia en el comportamiento de flags y facilita el mantenimiento.

Read-Modify-Write para (HL): Las instrucciones INC (HL) y DEC (HL) implementan explícitamente la secuencia Read-Modify-Write usando mmu.read_byte() y mmu.write_byte(). Esto es importante porque en hardware real, estas operaciones requieren múltiples accesos a memoria.

Logging Consistente: Todos los métodos incluyen logging DEBUG con el mismo formato que las instrucciones existentes, facilitando la depuración.

Archivos Afectados

  • src/cpu/core.py - Añadidos 10 nuevos métodos de opcodes y actualizada la tabla de opcodes
  • tests/test_cpu_inc_dec_full.py - Nuevo archivo con suite completa de tests (10 tests)

Tests y Verificación

Se creó una suite completa de tests TDD con 10 tests que validan todas las variantes implementadas:

Tests Unitarios (pytest)

Comando ejecutado: python3 -m pytest tests/test_cpu_inc_dec_full.py -v

Entorno: macOS, Python 3.9.6

Resultado: 10/10 tests PASSED en 0.59 segundos

Qué valida:

  • test_inc_dec_e: Verifica que DEC E funciona correctamente y afecta flags Z, N, H (pero NO C). Este test es crítico porque DEC E (0x1D) es el opcode que causaba el crash en Tetris cuando no estaba implementado. Incluye casos de wrap-around (0x00 -> 0xFF) y activación de flags.
  • test_inc_dec_d, test_inc_dec_h, test_inc_dec_l: Verifican que INC/DEC funcionan correctamente para todos los registros restantes.
  • test_inc_hl_memory: Verifica que INC (HL) realiza correctamente la operación Read-Modify-Write. Pone 0x0F en (HL), ejecuta INC (HL), y verifica que la memoria tiene 0x10 y Flag H=1 (half-carry).
  • test_dec_hl_memory: Similar a test_inc_hl_memory pero para DEC (HL).
  • test_inc_hl_memory_zero_flag, test_dec_hl_memory_zero_flag: Verifican que las operaciones en memoria activan correctamente el flag Z cuando el resultado es 0.
  • test_inc_preserves_carry, test_dec_preserves_carry: Verifican que el flag C NO se modifica durante operaciones INC/DEC, que es una peculiaridad crítica del hardware LR35902.

Código del Test Crítico (test_inc_dec_e)

def test_inc_dec_e(self, cpu: CPU) -> None:
    """
    Verifica que DEC E funciona correctamente y afecta flags Z, N, H.
    
    Este test es crítico porque DEC E (0x1D) es el opcode que causaba
    el crash en Tetris cuando no estaba implementado.
    """
    # Test 1: DEC E desde valor no cero
    cpu.registers.set_e(0x05)
    cpu.registers.set_f(0x00)  # Limpiar todos los flags
    cycles = cpu._op_dec_e()
    
    assert cycles == 1
    assert cpu.registers.get_e() == 0x04
    assert not cpu.registers.get_flag_z()  # No es cero
    assert cpu.registers.get_flag_n()  # Es una resta
    assert not cpu.registers.get_flag_h()  # No hay half-borrow
    assert not cpu.registers.get_flag_c()  # C no se toca
    
    # Test 2: DEC E desde 0x01 (debe dar 0x00 y activar Z)
    cpu.registers.set_e(0x01)
    cpu.registers.set_f(0x00)
    cycles = cpu._op_dec_e()
    
    assert cycles == 1
    assert cpu.registers.get_e() == 0x00
    assert cpu.registers.get_flag_z()  # Es cero
    assert cpu.registers.get_flag_n()  # Es una resta
    assert not cpu.registers.get_flag_h()  # No hay half-borrow
    assert not cpu.registers.get_flag_c()  # C no se toca
    
    # Test 3: DEC E desde 0x00 (wrap-around a 0xFF)
    cpu.registers.set_e(0x00)
    cpu.registers.set_f(0x00)
    cycles = cpu._op_dec_e()
    
    assert cycles == 1
    assert cpu.registers.get_e() == 0xFF
    assert not cpu.registers.get_flag_z()  # No es cero
    assert cpu.registers.get_flag_n()  # Es una resta
    assert cpu.registers.get_flag_h()  # Hay half-borrow (0x0 -> 0xF)
    assert not cpu.registers.get_flag_c()  # C no se toca
    
    # Test 4: INC E desde 0x0F (debe activar H)
    cpu.registers.set_e(0x0F)
    cpu.registers.set_f(0x00)
    cycles = cpu._op_inc_e()
    
    assert cycles == 1
    assert cpu.registers.get_e() == 0x10
    assert not cpu.registers.get_flag_z()  # No es cero
    assert not cpu.registers.get_flag_n()  # Es una suma
    assert cpu.registers.get_flag_h()  # Hay half-carry (0xF -> 0x10)
    assert not cpu.registers.get_flag_c()  # C no se toca

Por qué este test demuestra el comportamiento del hardware:

  • Verifica que DEC E decrementa correctamente el valor del registro
  • Confirma que el flag Z se activa cuando el resultado es 0
  • Confirma que el flag N se activa siempre en DEC (es una resta)
  • Confirma que el flag H se activa cuando hay half-borrow (0x0 -> 0xF)
  • Crítico: Confirma que el flag C NO se modifica, que es una peculiaridad del hardware LR35902
  • Verifica el wrap-around correcto (0x00 -> 0xFF)

Tests de Memoria Indirecta (test_inc_hl_memory)

def test_inc_hl_memory(self, cpu: CPU) -> None:
    """
    Verifica INC (HL) con operación Read-Modify-Write.
    
    Pone 0x0F en (HL), ejecuta INC (HL), y verifica que:
    - La memoria tiene 0x10
    - Flag H=1 (half-carry)
    """
    # Configurar HL para apuntar a una dirección de RAM
    hl_addr = 0xC000  # Dirección en RAM de la Game Boy
    cpu.registers.set_hl(hl_addr)
    
    # Escribir 0x0F en memoria
    cpu.mmu.write_byte(hl_addr, 0x0F)
    
    # Limpiar flags
    cpu.registers.set_f(0x00)
    
    # Ejecutar INC (HL)
    cycles = cpu._op_inc_hl_ptr()
    
    # Verificar ciclos (3 M-Cycles para Read-Modify-Write)
    assert cycles == 3
    
    # Verificar que la memoria se actualizó correctamente
    assert cpu.mmu.read_byte(hl_addr) == 0x10
    
    # Verificar flags
    assert not cpu.registers.get_flag_z()  # 0x10 no es cero
    assert not cpu.registers.get_flag_n()  # Es una suma
    assert cpu.registers.get_flag_h()  # Hay half-carry (0xF -> 0x10)
    assert not cpu.registers.get_flag_c()  # C no se toca

Por qué este test demuestra el comportamiento del hardware:

  • Verifica que INC (HL) realiza correctamente la operación Read-Modify-Write (lee, modifica, escribe)
  • Confirma que consume 3 M-Cycles (12 T-Cycles) en lugar de 1, como requiere el hardware
  • Verifica que la memoria se actualiza correctamente
  • Confirma que los flags se actualizan correctamente durante la operación en memoria

Validación con ROM Real (Tetris DX)

ROM: Tetris DX (ROM aportada por el usuario, no distribuida)

Modo de ejecución: Headless, límite de 10,000 instrucciones, logging INFO activado

Criterio de éxito: El emulador debe ejecutar sin crashear en el opcode 0x1D (DEC E) que anteriormente causaba el error. El registro E debe cambiar correctamente durante la ejecución, confirmando que DEC E funciona.

Observación:

  • ✅ El emulador ejecutó 10,000 instrucciones sin errores
  • ✅ No hubo crash en 0x1D (DEC E) - el problema se resolvió completamente
  • ✅ El registro E cambió correctamente durante la ejecución (0x00 → 0xC9 → 0xBB → ... → 0x43), confirmando que DEC E funciona
  • ✅ El PC está en un bucle entre 0x1383-0x1389, lo cual es normal para un juego esperando V-Blank
  • ✅ LY (Línea Y) incrementó correctamente hasta 125 líneas, confirmando que el timing de la PPU funciona
  • ✅ El registro A también cambió correctamente, confirmando que otras operaciones funcionan

Logs relevantes (muestra cada 100 instrucciones):

Instrucción 100: PC=0x1384, A=0xCF, E=0xC9, LY=1
Instrucción 200: PC=0x1386, A=0xBF, E=0xBB, LY=2
Instrucción 300: PC=0x1388, A=0x06, E=0xAC, LY=3
Instrucción 400: PC=0x1383, A=0x9E, E=0x9E, LY=5
...
Instrucción 1300: PC=0x1387, A=0x1E, E=0x1D, LY=16
...
Instrucción 10000: PC=0x1386, A=0x43, E=0x43, LY=125

✅ Ejecutadas 10000 instrucciones sin errores
   PC final = 0x1386
   A = 0x43
   E = 0x43
   LY = 125

Resultado: verified - El crash en 0x1D (DEC E) se ha resuelto completamente. El emulador ahora puede ejecutar Tetris DX más allá de la inicialización, llegando a un bucle de espera de V-Blank que es comportamiento normal del juego.

Notas legales: La ROM de Tetris DX es aportada por el usuario para pruebas locales. No se distribuye, no se incluye en el repositorio, y no se enlazan descargas.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Patrón de Opcodes: Las instrucciones INC/DEC siguen un patrón claro en la tabla de opcodes: columnas x4/x5 para B/D/H/(HL) y columnas xC/xD para C/E/L/A. Este patrón facilita la memorización y la implementación sistemática.
  • Preservación del Flag C: Una peculiaridad crítica del hardware LR35902 es que INC/DEC de 8 bits NO modifican el flag C (Carry). Esto es diferente de muchas otras arquitecturas y es importante para la lógica condicional de los juegos.
  • Read-Modify-Write: Las operaciones en memoria indirecta (HL) requieren múltiples accesos a memoria, lo que se refleja en el consumo de 3 M-Cycles en lugar de 1.
  • Half-Carry/Half-Borrow: El flag H se activa cuando hay carry/borrow del bit 3 al 4 (nibble bajo), lo que es útil para operaciones BCD (Binary Coded Decimal) aunque no se use mucho en la Game Boy.

Lo que Falta Confirmar

  • Timing Exacto: Por ahora, asumimos que INC/DEC (HL) consume exactamente 3 M-Cycles según la documentación. Si en el futuro hay problemas de timing con juegos reales, podríamos necesitar verificar el timing exacto con tests de hardware real.

Hipótesis y Suposiciones

Suposición 1: Asumimos que el comportamiento de flags en INC/DEC es idéntico para todos los registros y para memoria indirecta. Esto está respaldado por la documentación, pero no lo hemos verificado con hardware real.

Suposición 2: RESUELTA - Se probó con Tetris DX después de esta implementación y se confirmó que el crash en 0x1D se ha resuelto completamente. El emulador ejecutó 10,000 instrucciones sin errores, llegando a un bucle de espera de V-Blank que es comportamiento normal del juego.

Próximos Pasos

  • [x] Ejecutar Tetris DX para verificar que el crash en 0x1D (DEC E) se ha resuelto - COMPLETADO
  • [ ] Continuar con la implementación de instrucciones faltantes según los logs de ejecución
  • [ ] Analizar el bucle en 0x1383-0x1389 para entender qué está esperando el juego
  • [ ] Si ya se implementó el renderizado de background, verificar que Tetris DX muestra la pantalla de título