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

Implementación de Control de Flujo y Saltos en C++

Fecha: 2025-12-19 Step ID: 0106 Estado: Completado

Resumen

Se implementó el control de flujo básico de la CPU en C++, añadiendo instrucciones de salto absoluto (JP nn) y relativo (JR e, JR NZ e). Esta implementación rompe la linealidad de ejecución, permitiendo bucles y decisiones condicionales. La CPU ahora es prácticamente Turing Completa. Se aprovechó el manejo nativo de enteros con signo de C++ para simplificar los saltos relativos, eliminando la complejidad de simular complemento a dos que existía en Python. Todos los tests pasan (8/8).

Concepto de Hardware

El control de flujo es esencial para cualquier CPU: sin saltos (jumps), la ejecución solo podría ser lineal, sin posibilidad de bucles, condicionales o subrutinas. La CPU LR35902 implementa dos tipos principales de saltos:

  • Saltos Absolutos (JP nn): El opcode lee una dirección de 16 bits (Little-Endian) y establece el PC directamente a esa dirección. Consume 4 M-Cycles.
  • Saltos Relativos (JR e): El opcode lee un byte con signo (-128 a +127) y lo suma al PC actual. El offset es relativo a la posición DESPUÉS de leer el offset. Consume 3 M-Cycles si se toma el salto, 2 si no (en versiones condicionales).

Optimización C++ vs Python: Como se mencionó en el prompt, en Python tuvimos que hacer fórmulas matemáticas para simular el complemento a dos (ej: si el byte es >= 128, restar 256). En C++, el cast de uint8_t a int8_t es nativo del procesador: el compilador simplemente interpreta el mismo patrón de bits como un número con signo, generando la instrucción de ensamblador correcta automáticamente. Esto hace el código más limpio y eficiente: pc += (int8_t)offset; frente a la lógica condicional de Python.

Branch Prediction: Aunque estamos emulando, organizar los casos de salto juntos en el switch puede ayudar a la predicción de ramas del procesador host, mejorando el rendimiento del switch statement.

Timing Condicional: Las versiones condicionales de JR (como JR NZ, e) siempre leen el offset (para avanzar PC), pero solo ejecutan el salto si la condición es verdadera. Esto causa que consuman diferentes ciclos: 3 M-Cycles si saltan, 2 si no.

Implementación

Se añadió el helper fetch_word() para leer direcciones de 16 bits en formato Little-Endian (lee LSB primero, luego MSB, y los combina). Se implementaron 3 opcodes nuevos: JP nn (0xC3), JR e (0x18) y JR NZ, e (0x20).

Componentes creados/modificados

  • CPU.hpp: Añadida declaración de fetch_word() helper.
  • CPU.cpp:
    • Implementación de fetch_word() (lee 2 bytes Little-Endian).
    • Implementación de JP nn (0xC3) - salto absoluto de 4 M-Cycles.
    • Implementación de JR e (0x18) - salto relativo incondicional de 3 M-Cycles.
    • Implementación de JR NZ, e (0x20) - salto relativo condicional (3 ciclos si salta, 2 si no).
  • tests/test_core_cpu_jumps.py: Suite completa de 8 tests que validan:
    • Saltos absolutos (JP nn) con direcciones normales y wrap-around.
    • Saltos relativos positivos y negativos.
    • Saltos condicionales con condición verdadera y falsa.
    • Verificación crítica del manejo de negativos en C++.

Decisiones de diseño

  • fetch_word() reutiliza fetch_byte(): Para mantener consistencia y aprovechar el manejo de wrap-around de PC que ya existe en fetch_byte(). Esto también simplifica el código y reduce duplicación.
  • Cast explícito a int8_t: Aunque el compilador podría inferirlo, usar static_cast<int8_t>(offset_raw) hace explícita la intención y mejora la legibilidad del código.
  • Agrupación de saltos en switch: Los opcodes de salto se agrupan juntos en el switch (después de las operaciones ALU) para ayudar a la predicción de ramas del procesador host. Esto es una optimización menor pero importante en bucles de emulación.
  • JR NZ siempre lee offset: Aunque no se tome el salto, siempre leemos el offset para avanzar PC correctamente. Esto es crítico para la emulación correcta del timing y comportamiento del hardware.

Archivos Afectados

  • src/core/cpp/CPU.hpp - Añadida declaración de fetch_word()
  • src/core/cpp/CPU.cpp - Implementación de fetch_word() y 3 opcodes de salto
  • tests/test_core_cpu_jumps.py - Suite completa de tests (8 tests)

Tests y Verificación

Se creó una suite completa de 8 tests en test_core_cpu_jumps.py:

  • TestJumpAbsolute (2 tests):
    • test_jp_absolute(): Verifica salto absoluto a dirección específica (0xC000).
    • test_jp_absolute_wraparound(): Verifica salto a dirección máxima (0xFFFF).
  • TestJumpRelative (3 tests):
    • test_jr_relative_positive(): Verifica salto relativo positivo (+5).
    • test_jr_relative_negative(): CRÍTICO - Verifica manejo de offset negativo (-2) en C++.
    • test_jr_relative_loop(): Simula un bucle con salto relativo negativo (-3).
  • TestJumpRelativeConditional (3 tests):
    • test_jr_nz_condition_true(): Verifica que salta cuando Z=0 (condición verdadera, 3 ciclos).
    • test_jr_nz_condition_false(): Verifica que NO salta cuando Z=1 (condición falsa, 2 ciclos).
    • test_jr_nz_negative_when_condition_true(): Verifica saltos negativos con condición verdadera.

Resultados de validación:

tests/test_core_cpu_jumps.py::TestJumpAbsolute::test_jp_absolute PASSED
tests/test_core_cpu_jumps.py::TestJumpAbsolute::test_jp_absolute_wraparound PASSED
tests/test_core_cpu_jumps.py::TestJumpRelative::test_jr_relative_positive PASSED
tests/test_core_cpu_jumps.py::TestJumpRelative::test_jr_relative_negative PASSED
tests/test_core_cpu_jumps.py::TestJumpRelative::test_jr_relative_loop PASSED
tests/test_core_cpu_jumps.py::TestJumpRelativeConditional::test_jr_nz_condition_true PASSED
tests/test_core_cpu_jumps.py::TestJumpRelativeConditional::test_jr_nz_condition_false PASSED
tests/test_core_cpu_jumps.py::TestJumpRelativeConditional::test_jr_nz_negative_when_condition_true PASSED

============================== 8 passed in 0.05s ==============================

Todos los tests pasan correctamente, incluyendo el caso crítico de manejo de números negativos en C++. La CPU ahora puede ejecutar bucles y tomar decisiones condicionales.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Complemento a Dos Nativo: En C++, el cast de uint8_t a int8_t es una operación a nivel de bits: el mismo patrón de bits se interpreta de forma diferente. Esto es mucho más eficiente que simularlo en Python con operaciones aritméticas.
  • Little-Endian: La Game Boy almacena valores de 16 bits en memoria en formato Little-Endian (LSB primero). Esto es crítico para leer direcciones correctamente.
  • Timing Condicional: Las instrucciones de salto condicional siempre leen el offset (para mantener el comportamiento del hardware), pero solo ejecutan el salto si la condición es verdadera. Esto causa diferentes tiempos de ejecución (3 vs 2 M-Cycles).
  • PC relativo a lectura completa: En saltos relativos, el offset se suma al PC DESPUÉS de leer toda la instrucción (opcode + offset). Esto es importante para calcular correctamente la dirección de destino.

Lo que Falta Confirmar

  • Otros saltos condicionales: Existen más variantes condicionales (JR Z, JR C, JR NC) que deberían seguir el mismo patrón. Pendiente implementar en pasos futuros.
  • CALL y RET: Para subrutinas, necesitaremos CALL (salto absoluto que guarda dirección de retorno en stack) y RET (retorna desde subrutina). Pendiente para control de flujo avanzado.

Hipótesis y Suposiciones

Se asume que el comportamiento del hardware real es exactamente como se documenta en Pan Docs: los saltos relativos siempre leen el offset (incluso si no se toma el salto), y el timing es exactamente 3 M-Cycles si se salta, 2 si no. Esto se verifica mediante tests exhaustivos.

Próximos Pasos

  • [ ] Implementar más saltos condicionales (JR Z, JR C, JR NC)
  • [ ] Implementar CALL y RET para subrutinas
  • [ ] Implementar más instrucciones de carga/almacenamiento (LD)
  • [ ] Continuar expandiendo el conjunto de instrucciones básicas de la CPU