This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
FPS Limiter Check and Correction
Summary
Verification and correction of the FPS limiter that already existed in the code. Despiteclock.tick(60)was implemented, the FPS report in the title bar showed 300+ FPS, which suggested that the limiter was working but the report did not reflect the limited FPS correctly. Fixed FPS calculations in the title bar and performance monitor to reflect the actual limited FPS (~60 FPS) instead of the uncapped rendering FPS.
Hardware Concept
The original Game Boy runs59.7FPS(approximately 60 FPS), which means that each frame should last approximately16.67ms(1000ms / 60FPS). To maintain correct synchronization with real hardware, the emulator must:
- Run the emulation at the correct speed: 70,224 T cycles per frame (4.194304 MHz / 59.7 FPS)
- Limit rendering to 60 FPS: Use an FPS limiter (such as
pygame.Clock.tick(60)) to synchronize rendering with real time - Report limited FPS correctly: FPS report should reflect capped FPS, not uncapped rendering FPS
The problem encountered was that, although the limiter was active (the emulator was running at a real 60 FPS), the report showed the rendering FPS without limiting (300+ FPS). This happened because:
- He
clock.get_fps()may not work correctly if called before the tick takes effect - The performance monitor measured only the rendering time, not the time between frames (which includes waiting for the
clock.tick())
The solution was to calculate the FPS from the real time between consecutive frames (which includes the limiter timeout), instead of just using the render time.
Implementation
Fixes have been implemented in two main areas: the FPS report in the title bar and the performance monitor.
1. Fix FPS Report in Title Bar
Problem detected: The report usedclock.get_fps()which could return uncapped FPS instead of capped FPS.
Solution: Calculate the FPS from thetick_timereturned byclock.tick(), which is more precise:
#Step 0309: Correct FPS calculation to reflect limited FPS
if self._clock is not None:
tick_time_ms = self._clock.tick(TARGET_FPS)
# Temporary log for verification (every second)
if self.frame_count % 60 == 0:
print(f"[FPS-LIMITER] Frame {self.frame_count} | Tick time: {tick_time_ms:.2f}ms | Target: {TARGET_FPS} FPS")
# Title with FPS (every 60 frames to not slow down)
if self.frame_count % 60 == 0 and self._clock is not None:
import pygame
import time
# Option A: Use get_fps() which should return limited FPS
fps_from_clock = self._clock.get_fps()
# Option B: Calculate from tick_time (more precise)
if tick_time_ms is not None and tick_time_ms > 0:
fps_calculated = 1000.0 / tick_time_ms
# Use tick_time based calculation for greater precision
fps = fps_calculated
else:
# Fallback to get_fps() if tick_time is not available
fps = fps_from_clock if fps_from_clock > 0 else TARGET_FPS
pygame.display.set_caption(f"Viboy Color v0.0.2 - FPS: {fps:.1f}")
2. Synchronization Verification
Added a synchronization check that runs every minute (3600 frames) to detect drift between actual frames and expected frames:
#Step 0309: Synchronization check (every minute)
if not hasattr(self, '_start_time'):
import time
self._start_time = time.time()
if self.frame_count % 3600 == 0 and self.frame_count > 0: # Every minute (60 * 60 frames)
import time
elapsed_real = time.time() - self._start_time
expected_frames = elapsed_real * TARGET_FPS
current_frames = self.frame_count
drift = actual_frames - expected_frames
print(f"[SYNC-CHECK] Real: {elapsed_real:.1f}s | Expected: {expected_frames:.0f} frames | Actual: {actual_frames} | Drift: {drift:.0f}")
3. Performance Monitor Fix
Problem detected: The monitor measured only the rendering time, not the time between frames (which includes waiting for theclock.tick()).
Solution: Calculate the time between consecutive frames, which includes the limiter timeout:
# Step 0309: Calculate time between consecutive frames (includes clock.tick())
# Save time from the previous frame to calculate time between frames
self._last_frame_end_time = None
# In the performance monitor:
if self._performance_trace_enabled and frame_start is not None:
frame_end = time.time()
frame_time = (frame_end - frame_start) * 1000 # Render time in ms
# Calculate time between consecutive frames (includes clock.tick())
time_between_frames = None
fps_limited = None
if self._last_frame_end_time is not None:
time_between_frames = (frame_end - self._last_frame_end_time) * 1000 # ms
fps_limited = 1000.0 / time_between_frames if time_between_frames > 0 else 0
self._last_frame_end_time = frame_end
if self._performance_trace_count % 10 == 0:
fps_render = 1000.0 / frame_time if frame_time > 0 else 0
if fps_limited is not None:
print(f"[PERFORMANCE-TRACE] Frame {self._performance_trace_count} | "
f"Frame time (render): {frame_time:.2f}ms | FPS (render): {fps_render:.1f} | "
f"Time between frames: {time_between_frames:.2f}ms | FPS (limited): {fps_limited:.1f} | "
f"...")
Benefit: The monitor now reports both the rendering FPS (which can be high) and the limited FPS (which should be ~60 FPS), allowing you to identify synchronization problems.
Design decisions
- Use of
tick_timefor FPS calculation: More precise thanget_fps()because it directly reflects the time the limiter waited - Time measurement between frames: Allows you to distinguish between rendered FPS (unlimited) and limited FPS (real)
- Periodic sync check: Allows you to detect long-term drift that could indicate synchronization problems
Affected Files
src/viboy.py- Correction of the FPS report in title bar, limiter verification logs, and synchronization verificationsrc/gpu/renderer.py- Fix performance monitor to calculate time between frames and report limited FPS
Tests and Verification
Verification requires running the emulator and observing:
- Title bar: Should show FPS ≈ 60.0 (not 300+)
- Logs [FPS-LIMITER]: Should show tick time ≈ 16.67ms every second
- Logs [PERFORMANCE-TRACE]: Must show FPS (limited) ≈ 60.0
- Logs [SYNC-CHECK]: Should show drift close to 0 after 1 minute
Command for verification:
python main.py roms/pkmn.gb > perf_step_0309.log 2>&1
Expected analysis:
- Average FPS (limited) should be ≈ 60 FPS (not 300+)
- Minimum FPS must be > 55 FPS
- Maximum FPS should be< 65 FPS
- Frame time (limited) must be ≈ 16.67ms (1000ms / 60 FPS)
C++ Compiled Module Validation: No changes required in C++, only fixes in Python/Cython.
Sources consulted
- Pan Docs: LCD Timing -https://gbdev.io/pandocs/LCDC.html
- Pygame Clock Documentation:https://www.pygame.org/docs/ref/time.html#pygame.time.Clock
Educational Integrity
What I Understand Now
- FPS limiter:
clock.tick(60)limits rendering to 60 FPS by adding a wait if the frame was rendered too fast. The wait time is returned astick_time. - Rendering FPS vs Limited FPS: The rendering FPS can be very high (300+ FPS) if the rendering is fast, but the limited FPS should be ~60 FPS for correct timing.
- Time between frames: To measure limited FPS correctly, you must measure the time between the end of one frame and the end of the next, not just the rendering time.
What remains to be confirmed
- Practical verification: Run the emulator and verify that the reported FPS is ~60 FPS instead of 300+ FPS
- Long-term synchronization: Check with [SYNC-CHECK] that there is no significant drift after several minutes
Hypotheses and Assumptions
I assume thatclock.tick(60)It works correctly and that the problem was only the report. If after these fixes the reported FPS is still incorrect, there could be a problem with the limiter itself or with the synchronization between the emulation loop and the render.
Next Steps
- [ ] Run hands-on verification to confirm that the reported FPS is ~60 FPS
- [ ] Analyze logs [SYNC-CHECK] to verify that there is no significant drift
- [ ] If the FPS is still incorrect, investigate whether there is a problem with the limiter itself
- [ ] Consider disabling temporary logs [FPS-LIMITER] after successful verification