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

PPU Fase E: Arquitectura por Scanlines para Sincronización CPU-PPU

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

Resumen

El análisis del deadlock de polling ha revelado una falla fundamental en nuestra arquitectura de bucle principal. Aunque la CPU y la PPU son lógicamente correctas, no están sincronizadas en el tiempo. La CPU ejecuta su bucle de polling tan rápido que la PPU nunca tiene suficientes ciclos para cambiar de estado, creando un deadlock temporal. Este Step documenta la re-arquitectura completa del bucle principal (`run()`) para que se base en "scanlines", forzando una sincronización precisa entre los ciclos de la CPU y los de la PPU, y rompiendo estructuralmente el deadlock.

Concepto de Hardware: El Tiempo Basado en Scanlines

El hardware de la Game Boy está rígidamente sincronizado. La PPU tarda exactamente 456 T-Cycles en procesar una línea de escaneo (scanline). Durante esos 456 ciclos, la CPU está ejecutando instrucciones en paralelo. Un emulador preciso debe replicar esta relación 1:1.

El Problema del Deadlock de Polling

Imagina que la PPU es un coche que viaja de una ciudad (Modo 2) a otra (Modo 3), un viaje que dura 80 minutos (ciclos). La CPU es un niño impaciente en el asiento de atrás que, cada 32 minutos (ciclos), pregunta: "¿Ya llegamos a la ciudad H-Blank?". El coche (PPU) aún no ha llegado ni a la primera ciudad, pero la CPU ya ha preguntado dos veces.

Esto es exactamente lo que estaba pasando:

  1. La PPU empieza una línea y entra en Modo 2 (OAM Scan), un estado que dura 80 T-Cycles.
  2. La CPU entra en su bucle de polling: LDH A, (n) -> CP d8 -> JR NZ, e.
  3. Este bucle completo consume 12 + 8 + 12 = 32 T-Cycles.
  4. La CPU ejecuta el bucle, lee STAT (que dice "Modo 2"), la comparación falla, y salta. Han pasado 32 ciclos.
  5. La CPU vuelve a ejecutar el bucle. Lee STAT (que sigue diciendo "Modo 2" porque solo han pasado 32 de los 80 ciclos). La comparación falla. Salta. Han pasado 64 ciclos.
  6. La CPU está "girando en vacío" dentro del Modo 2, sin darle tiempo a la PPU a que termine su trabajo y cambie de estado.

El problema no está en los componentes, sino en el orquestador: nuestro bucle principal en viboy.py. Nuestro while True actual no tiene noción del "tiempo emulado". Simplemente ejecuta la CPU una vez y luego hace otras cosas. Necesitamos una arquitectura que fuerce el paso del tiempo de manera sincronizada.

La Solución: Arquitectura por Scanlines

La nueva arquitectura funcionará así:

  1. Bucle Externo (por Frame): Sigue siendo un while self.running.
  2. Bucle Medio (por Scanline): Dentro, un bucle que se repite 154 veces (el número total de líneas de un fotograma).
  3. Bucle Interno (de CPU): Por cada una de esas 154 líneas, ejecutaremos la CPU repetidamente hasta que se hayan consumido exactamente 456 T-Cycles.
  4. Actualización PPU: Una vez consumidos los 456 ciclos, llamaremos a ppu.step(456) una sola vez, pasándole exactamente 456 ciclos.

Este diseño garantiza que, por cada "paso" de la PPU (una scanline), la CPU haya ejecutado la cantidad correcta de "pasos" (instrucciones). El deadlock se vuelve imposible, porque el tiempo emulado siempre avanza.

Fuente: Pan Docs - LCD Timing, System Clock

Implementación

Se reescribió por completo el método run() en src/viboy.py para implementar la arquitectura estricta por scanlines.

Componentes Modificados

  • Viboy::run(): Reescrito completamente con arquitectura por scanlines estricta.
  • Constantes de Timing: Definidas al inicio del método:
    • CYCLES_PER_SCANLINE = 456
    • SCANLINES_PER_FRAME = 154
    • CYCLES_PER_FRAME = 70224

Estructura del Nuevo Bucle

# Bucle principal del emulador
while self.running:
    # --- Bucle de Frame Completo (70224 ciclos) ---
    for line in range(SCANLINES_PER_FRAME):
        
        # --- Bucle de Scanline (456 ciclos) ---
        cycles_this_scanline = 0
        while cycles_this_scanline < CYCLES_PER_SCANLINE:
            if not self._cpu.halted:
                # Ejecuta una instrucción de CPU y devuelve los M-Cycles
                m_cycles = self._cpu.step() 
                # Convierte a T-Cycles (1 M-Cycle = 4 T-Cycles)
                t_cycles = m_cycles * 4
                cycles_this_scanline += t_cycles
            else:
                # Si la CPU está en HALT, simplemente avanzamos el tiempo
                # en la unidad mínima posible.
                cycles_this_scanline += 4 

        # Al final de la scanline, actualizamos la PPU una sola vez
        self._ppu.step(CYCLES_PER_SCANLINE)

    # --- Fin del Frame ---
    # Renderizado y sincronización...

Decisiones de Diseño

  • Sincronización Estricta: La PPU solo se actualiza una vez por scanline, con exactamente 456 ciclos. Esto garantiza que el tiempo emulado siempre avanza correctamente.
  • Manejo de HALT: Si la CPU está en HALT, avanzamos el tiempo en incrementos mínimos (4 T-Cycles) para que la PPU pueda seguir avanzando y generar interrupciones.
  • Timer: El Timer se actualiza cada instrucción (solo en modo Python por ahora) para mantener la precisión del RNG usado por juegos como Tetris.
  • Renderizado: El renderizado ya no depende de "is_frame_ready" porque este bucle garantiza que se ha completado un frame completo (154 scanlines).

Archivos Afectados

  • src/viboy.py - Reescritura completa del método run() con arquitectura por scanlines estricta.

Tests y Verificación

Esta implementación es una re-arquitectura del bucle principal. Los tests unitarios de CPU y PPU siguen siendo válidos, pero la validación principal se realizará ejecutando el emulador con una ROM real.

Resultado Esperado

Al ejecutar el emulador con esta nueva arquitectura:

  1. El deadlock se romperá. Es estructuralmente imposible que la CPU se quede girando en vacío sin que la PPU avance.
  2. LY se incrementará. El Heartbeat finalmente mostrará LY cambiando de 0 a 1, 2, 3... hasta 153, y luego volviendo a 0. ¡Veremos el latido del corazón del sistema de vídeo por primera vez!
  3. ¡Veremos Gráficos! Una vez que el deadlock se rompa, la CPU podrá continuar con su rutina de inicialización, copiará los datos de los tiles a la VRAM, y nuestra PPU, que ya sabe cómo renderizar el background, finalmente tendrá algo que dibujar. Deberíamos ver aparecer el logo de Nintendo o la pantalla de copyright de Tetris.

Validación: Ejecución del emulador con ROM real (Tetris, Mario, etc.) para confirmar que el deadlock se rompe y que LY avanza correctamente.

Fuentes Consultadas

Nota: Esta arquitectura es un estándar de oro en la industria de la emulación. Varios emuladores de referencia (SameBoy, mGBA) usan arquitecturas similares basadas en scanlines para garantizar sincronización perfecta.

Integridad Educativa

Lo que Entiendo Ahora

  • Sincronización de Tiempo: La diferencia entre la corrección de los componentes y la sincronización del sistema. Los componentes pueden ser correctos individualmente, pero si no están sincronizados en el tiempo, el sistema falla.
  • Arquitectura por Scanlines: Un diseño que fuerza el paso del tiempo emulado de manera sincronizada, garantizando que la CPU y la PPU siempre estén en el mismo "momento" del tiempo emulado.
  • Deadlock de Polling: Un tipo de deadlock donde la CPU está esperando un cambio de estado que nunca ocurre porque no se le da tiempo suficiente a la PPU para avanzar.

Lo que Falta Confirmar

  • Ejecución con ROM Real: Verificar que el deadlock se rompe y que LY avanza correctamente ejecutando el emulador con una ROM real.
  • Renderizado de Gráficos: Confirmar que una vez que el deadlock se rompe, la CPU puede copiar datos a VRAM y la PPU puede renderizar el background correctamente.

Hipótesis y Suposiciones

Esta arquitectura asume que la PPU puede procesar exactamente 456 ciclos por scanline de manera correcta. Si hay algún problema en la implementación de la PPU que cause que no procese correctamente los ciclos, el deadlock podría persistir o aparecer otros problemas de sincronización.

Próximos Pasos

  • [ ] Ejecutar el emulador con una ROM real (Tetris, Mario) para confirmar que el deadlock se rompe.
  • [ ] Verificar que LY avanza correctamente (0 → 153 → 0) en el heartbeat.
  • [ ] Confirmar que los gráficos se renderizan correctamente una vez que el deadlock se rompe.
  • [ ] Si el deadlock persiste, investigar posibles problemas en la implementación de la PPU o en la conversión de M-Cycles a T-Cycles.