⚠️ Clean-Room / Educational

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

Date:2025-12-17 StepID:0035 State: Verified

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 usingpygame.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__(): Addedself.buffer = pygame.Surface((160, 144))to create the Game Boy's native-sized internal framebuffer.
  • Renderer.render_frame():
    • Replacedself.screen.fill()byself.buffer.fill()to clear the buffer.
    • Addedpixels = pygame.PixelArray(self.buffer)to lock the buffer and allow fast writing.
    • Replacedpygame.draw.rect()bypixels[screen_x, screen_y] = colorto write pixels directly into the buffer.
    • Addedof the pixelsto free the PixelArray (important: it must be closed before using the buffer).
    • Added buffer scaling to the window usingpygame.transform.scale()andblit().
  • Viboy.__init__(): Addedself._clock = pygame.time.Clock()for FPS control.
  • Viboy.run():
    • Addedself._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}").

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.rect
  • src/viboy.py- Added FPS control with pygame.time.Clock and title update with FPS
  • tests/test_gpu_optimization.py- New file with 3 tests to validate PixelArray and performance
  • docs/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

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 tobytearraywithpygame.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 tobytearraywithpygame.image.frombuffer
  • [ ] Implement sprites (OAM) for full game rendering
  • [ ] Implement window (Window) for scroll effects and menus