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

Arquitectura de HALT: "Avance Rápido" al Siguiente Evento

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

Resumen

El deadlock de polling ha sido resuelto por la arquitectura de scanlines, pero ha revelado un deadlock más sutil: la CPU ejecuta la instrucción HALT y nuestro bucle principal no avanza el tiempo de forma eficiente, manteniendo LY atascado en 0. Este Step documenta la implementación de una gestión de HALT inteligente que "avanza rápido" el tiempo hasta el final de la scanline actual, simulando correctamente una CPU en espera mientras el resto del hardware (PPU) sigue funcionando.

Concepto de Hardware: HALT y la Sincronización de Eventos

La instrucción HALT (opcode 0x76) pone la CPU en un estado de bajo consumo. La CPU deja de ejecutar instrucciones y espera a que se produzca una interrupción. Sin embargo, el resto del hardware (como la PPU) no se detiene. El reloj del sistema sigue "latiendo".

El Problema del "Gateo" de HALT

Nuestra simulación anterior de HALT era demasiado simplista:

else: # Si la CPU está en HALT
    cycles_this_scanline += 4

Esto es terriblemente ineficiente. Estamos simulando una CPU "dormida" avanzando el tiempo a paso de tortuga (4 ciclos a la vez). Se necesitarían 114 iteraciones de nuestro bucle de Python solo para completar una scanline. Mientras tanto, el Heartbeat se dispara, nos muestra LY=0, y nos hace creer que el sistema está congelado. No está congelado; está gateando.

El HALT del hardware no "gatea". La CPU se detiene, pero el resto del sistema (la PPU) sigue funcionando a toda velocidad hasta el siguiente evento. Debemos simular esto.

La Solución: "Avance Rápido" al Siguiente Evento

Cuando la CPU entra en HALT, no debemos avanzar el tiempo de 4 en 4 ciclos. Debemos calcular cuántos ciclos faltan para el siguiente evento significativo (el final de la scanline) y avanzar el tiempo de un solo salto. Esto es una optimización crítica y una simulación mucho más precisa del comportamiento del hardware.

Fuente: Pan Docs - HALT behavior, Interrupts

Implementación

Se modificaron dos componentes principales para implementar la gestión inteligente de HALT:

A. Señalizar HALT desde C++

Primero, necesitamos que CPU::step() nos comunique que ha entrado en estado HALT. Usaremos un valor de retorno especial (negativo) para esto.

En src/core/cpp/CPU.cpp, modificamos el caso 0x76 (HALT) y la FASE 2 de gestión de HALT:

// ========== FASE 2: Gestión de HALT ==========
// Si la CPU está en HALT, no ejecutar instrucciones
// Retornamos -1 para señalar al orquestador que debe hacer "avance rápido"
// hasta el siguiente evento (fin de scanline)
if (halted_) {
    cycles_ += 1;
    return -1;  // Código especial: señala HALT para avance rápido
}

// ... dentro del switch(opcode)
case 0x76:  // HALT
    halted_ = true;
    cycles_ += 1;  // HALT consume 1 M-Cycle
    return -1;  // Código especial: señala HALT para avance rápido

B. Modificar viboy.py para Manejar la Señal HALT

Ahora, el orquestador en Python reacciona a esta señal:

# En src/viboy.py, dentro del método run()

while cycles_this_scanline < CYCLES_PER_SCANLINE:
    # Ejecuta una instrucción de CPU y devuelve los M-Cycles
    # m_cycles puede ser negativo (-1) si la CPU entra en HALT
    m_cycles = self._cpu.step()
    
    if m_cycles == -1:
        # ¡La CPU ha entrado en HALT!
        # "Avance Rápido": Calculamos los ciclos restantes para
        # completar la scanline y los añadimos de un solo golpe.
        remaining_cycles_in_scanline = CYCLES_PER_SCANLINE - cycles_this_scanline
        t_cycles = remaining_cycles_in_scanline
        cycles_this_scanline += t_cycles
    else:
        # Instrucción normal: convertir M-Cycles a T-Cycles
        t_cycles = m_cycles * 4
        cycles_this_scanline += t_cycles

Decisiones de Diseño

  • Valor de Retorno Especial: Usamos -1 como código especial para señalar HALT. Esto es seguro porque ninguna instrucción normal devuelve un valor negativo de M-Cycles.
  • Avance Rápido: Cuando detectamos HALT, calculamos los ciclos restantes en la scanline actual y los añadimos de un solo golpe. Esto simula correctamente que la CPU está dormida pero el resto del hardware sigue funcionando.
  • Compatibilidad: El wrapper de Cython ya devuelve int, por lo que no necesitamos modificarlo.

Archivos Afectados

  • src/core/cpp/CPU.cpp - Modificado para devolver -1 cuando entra en HALT (caso 0x76 y FASE 2).
  • src/viboy.py - Modificado el bucle principal para manejar el código especial -1 y realizar avance rápido.
  • tests/test_core_cpu_interrupts.py - Actualizado test test_halt_stops_execution y añadido nuevo test test_halt_instruction_signals_correctly.

Tests y Verificación

Se ejecutaron los tests de interrupciones para validar el nuevo comportamiento de HALT:

Comando Ejecutado

pytest tests/test_core_cpu_interrupts.py::TestHALT -v

Resultado

tests/test_core_cpu_interrupts.py::TestHALT::test_halt_stops_execution PASSED
tests/test_core_cpu_interrupts.py::TestHALT::test_halt_instruction_signals_correctly PASSED
tests/test_core_cpu_interrupts.py::TestHALT::test_halt_wakeup_on_interrupt PASSED

============================== 3 passed in 0.05s ==============================

Código del Test

El nuevo test test_halt_instruction_signals_correctly valida que:

def test_halt_instruction_signals_correctly(self):
    """
    Step 0172: Verifica que HALT (0x76) activa el flag 'halted' y
    que step() devuelve -1 para señalarlo.
    """
    mmu = PyMMU()
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    
    # Configurar
    mmu.write(0x0100, 0x76)  # HALT
    regs.pc = 0x0100
    
    assert cpu.get_halted() == 0, "CPU no debe estar en HALT inicialmente"
    
    # Ejecutar
    cycles = cpu.step()
    
    # Verificar
    assert cycles == -1, "step() debe devolver -1 para señalar HALT"
    assert cpu.get_halted() == 1, "El flag 'halted' debe activarse"
    assert regs.pc == 0x0101, "PC debe haber avanzado 1 byte"

Validación de módulo compilado C++: Todos los tests pasan correctamente, confirmando que el módulo C++ compilado funciona como se espera.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • HALT y el Tiempo Emulado: Cuando la CPU entra en HALT, no significa que el tiempo se detiene. El resto del hardware (PPU, Timer, etc.) sigue funcionando. Nuestra simulación debe reflejar esto.
  • Optimización de Avance Rápido: En lugar de avanzar el tiempo de 4 en 4 ciclos durante HALT, podemos calcular los ciclos restantes hasta el siguiente evento y avanzar de un solo golpe. Esto es más eficiente y más preciso.
  • Señalización entre Componentes: Usar valores de retorno especiales (como -1) es una forma elegante de comunicar estados especiales entre el núcleo C++ y el orquestador Python.

Lo que Falta Confirmar

  • Ejecución con ROM Real: Verificar que con esta nueva arquitectura, cuando el juego entra en HALT esperando V-Blank, el tiempo avanza correctamente y LY se incrementa.
  • Despertar de HALT: Confirmar que cuando la PPU genera una interrupción V-Blank, la CPU se despierta correctamente del HALT y continúa su ejecución.

Hipótesis y Suposiciones

Esta implementación asume que el siguiente evento significativo después de HALT es siempre el final de la scanline actual. Esto es correcto para la mayoría de los casos, pero podría haber situaciones donde queramos avanzar hasta un evento más específico (como una interrupción de Timer). Por ahora, avanzar hasta el final de la scanline es suficiente y correcto.

Próximos Pasos

Este es el momento de la verdad. Con esta nueva arquitectura:

  1. El juego entrará en HALT para esperar V-Blank.
  2. Nuestro run() lo detectará, avanzará el tiempo hasta el final de la scanline actual.
  3. ppu.step(456) se llamará, y LY se incrementará.
  4. Esto se repetirá para cada línea. Veremos en el Heartbeat cómo LY cicla de 0 a 153.
  5. Cuando LY llegue a 144, la PPU generará una interrupción de V-Blank.
  6. handle_interrupts() en la CPU C++ lo detectará y despertará a la CPU de HALT.
  7. El juego continuará su ejecución.

Si todo va bien, deberíamos ver el logo de Nintendo o la pantalla de copyright de Tetris por primera vez.