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
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
-1como 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-1cuando entra en HALT (caso0x76y FASE 2).src/viboy.py- Modificado el bucle principal para manejar el código especial-1y realizar avance rápido.tests/test_core_cpu_interrupts.py- Actualizado testtest_halt_stops_executiony añadido nuevo testtest_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
- Pan Docs: HALT behavior, Interrupts
- GBEDG: HALT (0x76)
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
LYse 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:
- El juego entrará en
HALTpara esperar V-Blank. - Nuestro
run()lo detectará, avanzará el tiempo hasta el final de la scanline actual. ppu.step(456)se llamará, yLYse incrementará.- Esto se repetirá para cada línea. Veremos en el
HeartbeatcómoLYcicla de 0 a 153. - Cuando
LYllegue a 144, la PPU generará una interrupción de V-Blank. handle_interrupts()en la CPU C++ lo detectará y despertará a la CPU deHALT.- 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.