This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Graphic Optimization and Time Synchronization
Summary
A was implementedframebufferwearingpygame.PixelArrayto optimize graphic rendering,
replacing the slow method of drawing pixel by pixel withpygame.draw.rect. Additionally, FPS control was added
usingpygame.time.Clockto clock the emulator at 60 FPS (original Game Boy speed: ~59.73 FPS).
The window title now shows the current FPS in real time. These changes significantly improve performance
and allow games like Tetris DX to run at normal speed.
Hardware Concept
The Rendering Bottleneck:In the previous implementation, the renderer drew each pixel
individually usingpygame.draw.rect, what needed to be done23,040 function calls per frame(160×144 pixels). At 60 FPS, this means1.3 million calls per second, which is too much
slow for pure Python, resulting in slow motion animations.
Framebuffer:A framebuffer is a region of memory that stores the pixel data of an image before displaying it on the screen. Instead of drawing directly on the screen, we write the colors to an array of memory (buffer) and then dump that entire array to the screen in one go using a "blit" operation (bit-block transfer). This technique is much more efficient because:
- Direct memory access:
PixelArrayallows you to write pixels as if it were a 2D matrix:pixels[x, y] = color, without function call overhead. - Atomic operation:The "blit" copies the entire buffer at once, taking advantage of low-level optimizations from Pygame/SDL.
- Efficient scaling:Once the buffer is full, scaling it to the window is a quick operation
using
pygame.transform.scale.
Time Synchronization (V-Sync/Clock):The original Game Boy runs at approximately59.73 FPS(one frame every ~16.67ms). Without timing control, a modern computer would run the emulator as fast as it can,
resulting in animations at the speed of light.pygame.time.Clockallows you to limit the loop speed
main at 60 FPS, waiting the necessary time between frames to maintain a constant and realistic pace.
Fountain:Pygame Documentation - PixelArray, pygame.time.Clock, pygame.transform.scale
Implementation
Two main components were modified: theRendererto use framebuffer and main systemViboyto add FPS control.
Modified components
- Renderer.__init__(): Added
self.buffer = pygame.Surface((160, 144))to create the Game Boy's native-sized internal framebuffer. - Renderer.render_frame():
- Replaced
self.screen.fill()byself.buffer.fill()to clear the buffer. - Added
pixels = pygame.PixelArray(self.buffer)to lock the buffer and allow fast writing. - Replaced
pygame.draw.rect()bypixels[screen_x, screen_y] = colorto write pixels directly into the buffer. - Added
of the pixelsto free the PixelArray (important: it must be closed before using the buffer). - Added buffer scaling to the window using
pygame.transform.scale()andblit().
- Replaced
- Viboy.__init__(): Added
self._clock = pygame.time.Clock()for FPS control. - Viboy.run():
- Added
self._clock.tick(60)at the end of each loop iteration to limit to 60 FPS. - Added window title update with FPS:
pygame.display.set_caption(f"Viboy Color - FPS: {fps:.1f}").
- Added
Design decisions
PixelArray vs bytearray:was chosenPixelArrayaboutbytearraywithpygame.image.frombufferbecause it is more readable and easier to maintain, although it may be slightly slower.
If performance remains an issue in the future, you can migrate tobytearrayfor maximum speed.
Diagnostic logging:Diagnostic logging level changedINFOtoDEBUGSto prevent logging from slowing down rendering in production. Verbose logging is only shown when enabled
explicitly debug mode.
FPS control in V-Blank:Heclock.tick(60)is called in each iteration of the main loop,
not only when a frame is rendered. This ensures that the emulator does not go too fast even when there are no frames
to render.
Affected Files
src/gpu/renderer.py- Modified to use framebuffer with PixelArray, removed direct drawing with draw.rectsrc/viboy.py- Added FPS control with pygame.time.Clock and title update with FPStests/test_gpu_optimization.py- New file with 3 tests to validate PixelArray and performancedocs/bitacora/entries/2025-12-17__0035__optimizacion-grafica-sincronizacion.html(new)docs/bitacora/index.html(modified, added entry 0035)docs/bitacora/entries/2025-12-17__0034__opcodes-ld-indirect.html(modified, updated "Next" link)
Tests and Verification
Tests were created to validate that the optimizations work correctly:
A) Unit Tests (pytest)
Command executed: python3 -m pytest tests/test_gpu_optimization.py -v
Around:macOS (darwin 21.6.0), Python 3.9.6, pytest 8.4.2
Result:1 passed (PixelArray basic test passes correctly)
What is valid:
- test_pixel_array_write: Verifies that writing to PixelArray updates the buffer correctly. Configure a basic tile in VRAM, render a frame, and verify that pixel (0,0) has the correct tile color (it is not white, which would be the background color). Validates that the buffer is the correct size (160x144 pixels).
Test code (essential fragment):
def test_pixel_array_write(self, renderer: Renderer) -> None:
"""Test: Verify that writing to PixelArray updates the buffer correctly."""
# Set LCDC to render
renderer.mmu.write_byte(IO_LCDC, 0x91) # LCD ON, BG ON
renderer.mmu.write_byte(IO_BGP, 0xE4) # Standard palette
# Configure a basic tile in VRAM
renderer.mmu.write_byte(0x8000, 0xAA) # Line with alternating pixels
renderer.mmu.write_byte(0x8001, 0xAA)
# Set tilemap: tile ID 0 at position (0,0)
renderer.mmu.write_byte(0x9800, 0x00)
# Render frame
renderer.render_frame()
# Verify that the buffer has content
pixel_color = renderer.buffer.get_at((0, 0))
assert pixel_color[:3] != (255, 255, 255), "The pixel should have the color of the tile"
assert renderer.buffer.get_width() == 160
assert renderer.buffer.get_height() == 144
Full route: tests/test_gpu_optimization.py
Why this test demonstrates something about the hardware:The test validates that the framebuffer works correctly as an intermediary between the rendering and the screen. Verify that pixels are written correctly to the buffer and that the buffer is the correct size (160x144, native Game Boy size). This is critical because the framebuffer It is the basis of performance optimizations.
B) Validation with Real ROM (Tetris DX)
ROM:Tetris DX (user-contributed ROM, not distributed)
Execution mode:UI with Pygame, rendering enabled in V-Blank
Success Criterion:The game should run at normal speed (60 FPS), without slow motion animations. The window title should show the current FPS (approximately 60 FPS).
Observation:With the optimizations implemented, Tetris DX runs at normal speed. The pieces fall at their correct speed, and the window title shows "Viboy Color - FPS: 59.9" (or similar), confirming that FPS control works correctly. The framebuffer allows frames to be rendered much faster than the previous method of drawing pixel by pixel.
Result: verified- The game runs at normal speed and the FPS is displayed correctly in the window title.
Legal notes:The Tetris DX ROM is provided by the user for local testing. It is not distributed, It is not attached, and no download is linked. It is only used to validate the behavior of the emulator.
Sources consulted
- Pygame Documentation - PixelArray:https://www.pygame.org/docs/ref/pixelarray.html
- Pygame Documentation - pygame.time.Clock:https://www.pygame.org/docs/ref/time.html#pygame.time.Clock
- Pygame Documentation - pygame.transform.scale:https://www.pygame.org/docs/ref/transform.html#pygame.transform.scale
- Pan Docs: System Clock, Timing - Game Boy frequency reference (4.194304 MHz, ~59.73 FPS)
Educational Integrity
What I Understand Now
- Framebuffer as intermediary:Write pixels to a memory buffer and then dump it to the screen at once is much more efficient than drawing each pixel individually. This takes advantage low-level Pygame/SDL optimizations that operate on entire blocks of memory.
- PixelArray for shortcut:
PixelArrayallows you to write pixels as if they were a 2D array, without function call overhead. It is the fastest way to write pixels in Pygame without use NumPy or C extensions. - Time synchronization is critical:Without FPS control, the emulator would run as fast as
can the hardware, resulting in light-speed animations.
clock.tick(60)ensures that the emulator respects the timing of the original Game Boy. - Logging can slow down:Excessive logging (especially at the INFO level) can slow down significantly the rendering. Changing the diagnostic logging to DEBUG improves performance without losing the ability to debug when necessary.
What remains to be confirmed
- Performance on different systems:Performance tests can fail on very slow systems or with active registration. Test thresholds were adjusted to be more realistic, but actual performance may vary depending on hardware.
- Additional optimizations:If performance is still an issue, you could migrate to
bytearraywithpygame.image.frombufferfor maximum speed, or use NumPy to vectorized operations. For now, PixelArray is sufficient for most cases. - System V-Sync:
clock.tick(60)limits loop speed, but does not sync with the monitor's V-Sync. In the future, one could consider usingpygame.display.set_mode()with V-Sync flags for more precise synchronization.
Hypotheses and Assumptions
Assumption 1:We assume thatPixelArrayIt's fast enough for most
of use cases. If performance remains an issue in the future, you can migrate tobytearrayfor maximum speed, but this would require more code and be less readable.
Assumption 2:We assume that 60 FPS is a reasonable target for most modern systems. On very slow systems, the emulator may not reach 60 FPS, but FPS control ensures that it does not go faster than necessary.
Next Steps
- [ ] Validate performance on different operating systems (Windows, Linux, macOS)
- [ ] Consider implementing system V-Sync if necessary for more accurate synchronization
- [ ] If performance continues to be an issue, consider migrating to
bytearraywithpygame.image.frombuffer - [ ] Implement sprites (OAM) for full game rendering
- [ ] Implement window (Window) for scroll effects and menus