This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Game Loop Architecture Fix
Summary
Fixed the architecture of the main loop (Game Loop) of the emulator. The critical problem was thatclock.tick(60)was inside the loop that executes one instruction per iteration, limiting execution to 60 instructions per second instead of ~4 million. The solution was to restructure the loop into two levels: an outer loop per frame (60 FPS) and an inner loop that executes all the instructions necessary to complete a frame (~70,224 T-Cycles). FPS control and event handling now run once per frame, outside of the internal instruction loop.
Hardware Concept
The Game Boy runs at 4.194304 MHz (4,194,304 cycles per second). A frame takes approximately 70,224 clock cycles to maintain 59.7 FPS. The main loop of an emulator must execute all the instructions necessary to complete a frame before synchronizing with real time.
Correct Game Loop architecture:
- External loop:For each frame (60 FPS). Here events are managed and time is synchronized.
- Inner loop:Executes all instructions necessary to complete a frame (~70,224 T-Cycles). The CPU executes instructions continuously until the PPU indicates that a frame is ready.
If the FPS control (clock.tick(60)) is placed inside the instruction loop, execution is limited to 60 instructions per second, resulting in extremely slow performance (6000 times slower than normal).
Fountain:Pan Docs - System Clock, Timing, Frame Rate
Implementation
The method was restructuredrun()insrc/viboy.pyto clearly separate the outer loop (per frame) from the inner loop (instructions).
Fixed loop structure:
# EXTERNAL LOOP: Per frame (60 FPS)
while True:
#1. Manage events (once per frame)
pygame.event.pump()
handle_pygame_events()
# 2. INNER LOOP: Execute a complete CPU/PPU frame
frame_cycles = 0
frame_rendered = False
max_cycles_per_frame = CYCLES_PER_FRAME * 4 * 2 # Safety limit
while not frame_rendered and frame_cycles< max_cycles_per_frame:
# Ejecutar una instrucción
cycles = self.tick()
frame_cycles += cycles * 4
# Si la PPU indica que un frame está listo, renderizar
if self._ppu.is_frame_ready():
frame_rendered = True
self._renderer.render_frame()
# 3. Control de FPS - FUERA del bucle interno
self._clock.tick(60) # Una vez por frame, no por instrucción
Modified components
src/viboy.py: Methodrun()Completely restructured with separate external/internal loop.
Design decisions
- Safety limit:Added a maximum limit of 2 frame cycles to avoid infinite loops if the PPU never indicates that a frame is ready.
- Periodic rendering:If a frame is not rendered (LCD off), periodic rendering for the visual heartbeat is maintained.
- Events once per frame:Pygame's event handling runs once per frame, not per instruction, to improve performance.
Affected Files
src/viboy.py- Complete restructuring of the methodrun()with outer/inner loop architecture
Tests and Verification
Pending verification:This fix should be tested by running the emulator with a ROM and verifying that:
- The cycle counter in the log went from ~169 cycles/second to ~4,000,000 cycles/second
- The LY record advances quickly (0 to 144 in milliseconds, not minutes)
- The game runs at real speed (60 FPS)
- Vital signs monitor shows cycles accumulating correctly
State:Draft - Pending verification with actual emulator execution.
Sources consulted
- Pan Docs: System Clock, Timing, Frame Rate
- General knowledge of emulator architecture and game loops
Educational Integrity
What I Understand Now
- Game Loop Architecture:An emulator needs to clearly separate instruction execution (inner loop) from real-time synchronization (outer loop). The FPS control must be outside the instruction loop.
- Performance:Place
clock.tick(60)within the instruction loop limits execution to 60 instructions per second, resulting in extremely slow performance (6000 times slower than normal). - Frame timing:One Game Boy frame lasts ~70,224 T-Cycles. The inner loop must execute all instructions necessary to complete a frame before synchronizing with real time.
What remains to be confirmed
- Performance verification:I need to run the emulator and verify that the cycle counter shows ~4 million cycles per second instead of ~169.
- LY speed:Verify that LY advances quickly (from 0 to 144 in milliseconds).
- Real FPS:Verify that the game runs at a real 60 FPS.
Hypotheses and Assumptions
I assume that restructuring the loop will solve the extreme performance problem. However, there may be other factors that affect performance (for example, expensive operations in rendering or event handling). Actual verification by running the emulator will confirm if this fix is sufficient.
Next Steps
- [ ] Run the emulator with a ROM and verify that the performance is correct (~4 million cycles/second)
- [ ] Verify that LY advances quickly (0 to 144 in milliseconds)
- [ ] Verify that the game runs at a real 60 FPS
- [ ] If performance is still slow, investigate other factors (rendering, event handling, etc.)