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 Final: Bucle de Emulación Nativo en C++
Resumen
El emulador había alcanzado un deadlock de sincronización final. Aunque todos los componentes C++ eran correctos (CPU, PPU, Interrupciones), el bucle principal en Python era demasiado lento y de grano grueso para simular la interacción ciclo a ciclo que la CPU y la PPU requieren durante los bucles de polling. Este Step documenta la solución definitiva: mover el bucle de emulación de grano fino (el bucle de scanline) completamente a C++, creando un método run_scanline() que encapsula toda la lógica de sincronización ciclo a ciclo a velocidad nativa.
Concepto de Hardware: La Verdadera Sincronización Ciclo a Ciclo
En el hardware real de la Game Boy, no hay un "orquestador" externo. La CPU ejecuta una instrucción y consume, digamos, 8 ciclos. En esos mismos 8 ciclos, la PPU, el Timer y la APU también avanzan 8 ciclos. La emulación verdaderamente precisa replica esto: después de cada instrucción de la CPU, todos los componentes deben ser actualizados con los ciclos consumidos.
El Problema de la Arquitectura Anterior
Nuestra arquitectura por scanlines en Python era una aproximación útil, pero tenía una limitación fundamental:
- La CPU ejecutaba múltiples instrucciones en un bucle Python hasta acumular 456 T-Cycles.
- La PPU solo se actualizaba una vez al final de la scanline, recibiendo todos los 456 ciclos de golpe.
- Durante el bucle de polling de la CPU (ej:
LDH A, (n) -> CP d8 -> JR NZ, e), la CPU leía el registro STAT repetidamente, pero la PPU no había cambiado de modo porque no había sido actualizada. - Esto creaba una paradoja: La CPU estaba esperando a la PPU, pero la PPU no podía avanzar hasta que la CPU terminara de esperar.
La Solución: Bucle de Emulación Nativo en C++
La solución es mover el bucle de emulación de grano fino completamente a C++, donde puede ejecutarse a velocidad nativa sin ninguna sobrecarga de llamadas entre Python y C++. El nuevo método run_scanline():
- Ejecuta instrucciones de la CPU hasta acumular exactamente 456 T-Cycles.
- Después de cada instrucción, actualiza la PPU con los ciclos consumidos.
- Esto garantiza que la PPU cambie de modo (Modo 2 → Modo 3 → Modo 0) en los ciclos exactos.
- Cuando la CPU lee el registro STAT en su bucle de polling, verá el cambio de modo inmediatamente y podrá continuar.
Fuente: Pan Docs - System Clock, LCD Timing, CPU-PPU Synchronization
Implementación
A. Modificación de CPU.hpp y CPU.cpp
Se añadieron dos nuevos métodos a la clase CPU:
setPPU(PPU* ppu): Conecta la PPU a la CPU para permitir sincronización ciclo a ciclo.run_scanline(): Ejecuta una scanline completa (456 T-Cycles) con sincronización ciclo a ciclo.
En src/core/cpp/CPU.hpp:
// Forward declaration
class PPU;
class CPU {
public:
void setPPU(PPU* ppu);
void run_scanline();
// ...
private:
PPU* ppu_; // Puntero a PPU (no poseído, opcional)
// ...
};
En src/core/cpp/CPU.cpp:
void CPU::setPPU(PPU* ppu) {
ppu_ = ppu;
}
void CPU::run_scanline() {
if (ppu_ == nullptr) return;
const int CYCLES_PER_SCANLINE = 456;
int cycles_this_scanline = 0;
while (cycles_this_scanline < CYCLES_PER_SCANLINE) {
// Ejecuta UNA instrucción
uint8_t m_cycles = step();
if (m_cycles == 0) m_cycles = 1;
if (halted_) {
m_cycles = 1; // Avance mínimo en HALT
}
int t_cycles = m_cycles * 4;
// ¡LA CLAVE! Actualiza la PPU después de CADA instrucción
ppu_->step(t_cycles);
cycles_this_scanline += t_cycles;
}
}
B. Actualización del Wrapper Cython
En src/core/cython/cpu.pyx, se expusieron los nuevos métodos a Python:
cdef class PyCPU:
# ...
def set_ppu(self, PyPPU ppu_wrapper):
if ppu_wrapper is None:
self._cpu.setPPU(NULL)
else:
cdef ppu.PPU* ppu_ptr = (ppu_wrapper)._ppu
self._cpu.setPPU(ppu_ptr)
def run_scanline(self):
self._cpu.run_scanline()
C. Simplificación de viboy.py
El método run() en src/viboy.py se simplificó drásticamente:
def run(self, debug: bool = False) -> None:
# ...
# Conectar PPU a CPU en el constructor
self._cpu.set_ppu(self._ppu)
# Bucle principal
while self.running:
for line in range(SCANLINES_PER_FRAME):
if not self.running:
break
# C++ se encarga de toda la emulación de una scanline
self._cpu.run_scanline()
# Renderizado y sincronización de frames
# ...
El bucle interno complejo de Python (que ejecutaba instrucciones hasta acumular 456 ciclos) fue completamente eliminado y reemplazado por una simple llamada a run_scanline().
Tests y Verificación
Este cambio arquitectónico profundo requiere verificación mediante ejecución del emulador completo. Los tests unitarios existentes siguen pasando, confirmando que no se rompió funcionalidad existente.
Comando de Compilación
.\rebuild_cpp.ps1
Comando de Ejecución
python main.py roms/tetris.gb --verbose
Resultado Esperado
Con esta arquitectura final:
- La CPU ejecutará su bucle de polling.
- Dentro de
run_scanline(), después de cadacpu.step(), se llamará appu.step(). - La PPU tendrá la oportunidad de cambiar de Modo 2 a Modo 3 y Modo 0 en los ciclos exactos.
- En una de sus iteraciones, el bucle de polling de la CPU leerá el registro STAT y verá que el modo ha cambiado. La condición
JR NZfallará. - El deadlock se romperá.
- La CPU continuará, copiará los gráficos a la VRAM.
- El Heartbeat mostrará a
LYincrementándose. - Y finalmente... veremos el logo de Nintendo en la pantalla.
Validación de Módulo Compilado C++
La implementación utiliza el módulo C++ compilado (viboy_core), asegurando que todo el bucle crítico de emulación se ejecute a velocidad nativa sin overhead de Python.
Impacto y Consecuencias
Este cambio representa la solución definitiva al problema de sincronización:
- Rendimiento: El bucle crítico de emulación ahora se ejecuta completamente en C++ nativo, eliminando toda la sobrecarga de llamadas entre Python y C++.
- Precisión: La PPU se actualiza después de cada instrucción, permitiendo sincronización ciclo a ciclo precisa.
- Resolución de Deadlocks: Los bucles de polling de la CPU ahora pueden leer cambios de estado de la PPU inmediatamente, rompiendo deadlocks estructurales.
- Simplicidad: El código Python se simplifica drásticamente, convirtiéndose en un mero orquestador de alto nivel (gestor de ventanas y frames).
Referencias
- Pan Docs: System Clock, LCD Timing, CPU-PPU Synchronization
- Archivos Modificados:
src/core/cpp/CPU.hppsrc/core/cpp/CPU.cppsrc/core/cython/cpu.pyxsrc/viboy.py