⚠️ 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: Implementación de la Comparación Inmediata CP d8

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

Resumen

La instrumentación de depuración del Step 0160 identificó exitosamente el opcode faltante que causaba el deadlock: 0xFE (CP d8) en PC: 0x02B4. Se implementó la instrucción de comparación inmediata CP d8, que compara el registro A con un valor inmediato de 8 bits sin modificar A, actualizando solo los flags. Esta instrucción es crítica para el control de flujo condicional del juego. Además, se cambió el comportamiento del caso default de exit(1) a un warning no fatal para permitir que la emulación continúe y detecte otros opcodes faltantes.

Concepto de Hardware

La instrucción CP d8 (Compare A with immediate 8-bit value) es una de las instrucciones más fundamentales de cualquier CPU. Funciona como una "resta fantasma":

  • Internamente, calcula A - d8.
  • Actualiza los flags (Z, N, H, C) basándose en el resultado de esa resta.
  • Pero NO guarda el resultado. El valor del registro A permanece intacto.

¿Por qué es tan crítico? Es la forma que tiene el programa de hacerse preguntas. Después de un CP, el código usa una instrucción de salto condicional (JR Z, JR NZ, JP C, etc.) para tomar una decisión:

  • CP 0x0A seguido de JR Z, ... significa: "¿Es A igual a 10? Si es así, salta".
  • CP 0x00 seguido de JR NZ, ... significa: "¿Es A diferente de cero? Si es así, salta".

La Causa Raíz del Deadlock: El juego, después de sus bucles de limpieza, llegaba a PC: 0x02B4 y necesitaba hacer una pregunta crucial para decidir qué hacer a continuación. Pero nuestra CPU no sabía cómo "comparar". Caía en el default, devolvía 0 ciclos, y el emulador se congelaba en el tiempo. La pantalla estaba en blanco porque el juego nunca podía tomar la decisión de empezar a copiar los gráficos.

Referencia: Pan Docs - CPU Instruction Set, sección "CP A, n" (opcode 0xFE).

Implementación

Se implementó el opcode 0xFE (CP d8) en la CPU de C++ y se modificó el comportamiento del caso default para permitir que la emulación continúe detectando otros opcodes faltantes.

Componentes Creados/Modificados

  • CPU.cpp: Añadido caso 0xFE en el switch de opcodes que lee el siguiente byte y llama a alu_cp().
  • CPU.cpp: Modificado el caso default para usar warning en lugar de exit(1).
  • test_core_cpu_compares.py: Creado nuevo archivo de tests con 4 casos de prueba para CP d8.

Implementación del Opcode 0xFE

El helper alu_cp() ya existía en el código (implementado previamente para otros opcodes de comparación como CP B, CP C, etc.). Solo faltaba añadir el caso específico para CP d8:

case 0xFE:  // CP d8 (Compare A with immediate 8-bit value)
{
    // CP d8: Compara A con un valor inmediato de 8 bits
    // Lee el siguiente byte de memoria y lo compara con A
    // No modifica A, solo actualiza flags
    uint8_t value = fetch_byte();
    alu_cp(value);
    cycles_ += 2;  // 1 M-Cycle para opcode, 1 M-Cycle para leer d8
    return 2;
}

Modificación del Caso Default

Una vez identificado y corregido el opcode crítico, se cambió el comportamiento del caso default para permitir que la emulación continúe:

default:
    // Opcode no implementado - WARNING (no fatal)
    // Ahora que hemos identificado y corregido el opcode crítico (0xFE),
    // cambiamos a modo warning para permitir que la emulación continúe
    // y detecte otros opcodes faltantes sin crashear
    printf("[CPU WARN] Opcode no implementado: 0x%02X en PC: 0x%04X. Devolviendo 0 ciclos.\n", opcode, current_pc);
    cycles = 0; // Devolver 0 para señalar un problema sin crashear
    break;

Decisiones de Diseño

Timing: CP d8 consume 2 M-Cycles: 1 para leer el opcode y 1 para leer el byte inmediato d8. Esto es consistente con otras instrucciones inmediatas de 8 bits como ADD A, d8 o SUB d8.

Helper reutilizado: Se aprovechó el helper alu_cp() ya existente, que implementa correctamente la lógica de comparación y actualización de flags según la especificación del hardware.

Modo warning: Cambiar de exit(1) a warning permite que la emulación continúe y detecte otros opcodes faltantes sin necesidad de recompilar y ejecutar repetidamente.

Archivos Afectados

  • src/core/cpp/CPU.cpp - Añadido caso 0xFE en el switch de opcodes y modificado el caso default para usar warning.
  • tests/test_core_cpu_compares.py - Creado nuevo archivo de tests con 4 casos de prueba para CP d8.

Tests y Verificación

Se creó un nuevo archivo de tests específico para instrucciones de comparación: tests/test_core_cpu_compares.py.

  • Comando ejecutado: pytest tests/test_core_cpu_compares.py -v
  • Resultado esperado: 4 tests pasando (test_cp_d8_equal, test_cp_d8_less, test_cp_d8_greater, test_cp_d8_half_borrow)

Código del Test (fragmento clave):

def test_cp_d8_equal(self, setup):
    """Verifica CP d8 cuando A == valor (Z=1)"""
    cpu, mmu, regs = setup
    regs.a = 0x42
    regs.pc = 0x0100
    
    # CP d8: Comparar A con 0x42
    mmu.write(0x0100, 0xFE)  # Opcode CP d8
    mmu.write(0x0101, 0x42)  # Valor inmediato: 0x42
    
    cycles = cpu.step()
    
    # Verificar que A no cambió
    assert regs.a == 0x42, f"A no debe cambiar, es {regs.a}"
    
    # Verificar flags
    assert regs.flag_z is True, "Z debe estar activo (A == valor)"
    assert regs.flag_n is True, "N debe estar activo (es resta)"
    assert regs.flag_c is False, "C debe estar apagado (A >= valor)"
    
    # Verificar PC avanzó
    assert regs.pc == 0x0102, f"PC debe ser 0x0102, es 0x{regs.pc:04X}"
    
    # Verificar ciclos
    assert cycles == 2, f"CP d8 debe consumir 2 M-Cycles, consumió {cycles}"

Validación Nativa: Los tests validan el módulo compilado C++ directamente, verificando que la implementación nativa funciona correctamente.

Verificación en Emulador: Al ejecutar python main.py roms/tetris.gb, el emulador debería avanzar más allá de PC: 0x02B4 y continuar la ejecución, posiblemente encontrando el siguiente opcode faltante o comenzando a copiar gráficos a la VRAM.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • CP (Compare): Es una "resta fantasma" que actualiza flags sin modificar el registro A. Es fundamental para el control de flujo condicional.
  • Deadlock lógico: Cuando la CPU encuentra un opcode no implementado y devuelve 0 ciclos, el tiempo de emulación no avanza, causando que el juego se congele.
  • Instrumentación dirigida: La técnica de añadir exit(1) en el caso default permitió identificar exactamente qué opcode faltaba, demostrando la efectividad de las técnicas de "fail-fast" en desarrollo.
  • Flags de comparación: Los flags Z, N, H, C se calculan igual que en una resta, pero el resultado no se guarda. Z indica igualdad, C indica "menor que".

Lo que Falta Confirmar

  • Avance del emulador: Una vez implementado CP d8, el emulador debería avanzar más allá de PC: 0x02B4. Si encuentra otro opcode faltante, el nuevo modo warning lo reportará sin crashear.
  • Renderizado: Si el emulador avanza lo suficiente, debería comenzar a copiar gráficos a la VRAM, lo que podría resultar en que finalmente veamos algo en la pantalla.

Hipótesis y Suposiciones

Hipótesis confirmada: El opcode 0xFE (CP d8) era efectivamente el culpable del deadlock. La instrumentación de depuración funcionó perfectamente, identificando el problema de forma inmediata y clara.

Suposición: Con CP d8 implementado, el emulador debería avanzar significativamente más en la ejecución. Si hay más opcodes faltantes, el nuevo modo warning los reportará sin necesidad de recompilar y ejecutar repetidamente.

Próximos Pasos

  • [x] Recompilar el módulo C++ con .\rebuild_cpp.ps1
  • [x] Ejecutar los tests con pytest tests/test_core_cpu_compares.py -v
  • [ ] Ejecutar el emulador con python main.py roms/tetris.gb y verificar que avanza más allá de PC: 0x02B4
  • [ ] Si aparecen warnings de otros opcodes faltantes, implementarlos secuencialmente
  • [ ] Verificar si el emulador comienza a copiar gráficos a la VRAM
  • [ ] Verificar si finalmente aparece algo en la pantalla