316. Fast pygame framebuffer draw
Improve pygame framebuffer drawing by replacing per-pixel rectangles with bulk blit.
Lesson 316 of 356 · tests/chapter_10_performance/test_316_fast_pygame_framebuffer_draw.py
File to update
tools/show_framebuffer.pyWhy this step exists
The original draw_framebuffer() was intentionally simple and educational:
for each framebuffer pixel:
create/fill one scaled pygame rectangleFor a NES frame, that means
256 * 240 = 61,440 pygame drawing operations per frameThat is slow because Python repeatedly crosses into pygame/SDL for thousands of tiny rectangles.
This step keeps the old implementation for comparison by renaming it
old_draw_framebuffer(...)Then it creates a new faster draw_framebuffer(...) that:
1. packs the framebuffer pixels into one RGB byte buffer
2. creates one pygame Surface from that buffer
3. scales that Surface when needed
4. blits the complete image to the windowMental model
Old path:
Python -> pygame draw tiny rect
Python -> pygame draw tiny rect
Python -> pygame draw tiny rect
... 61,440 times per frame
New path:
Python builds one RGB buffer
pygame uploads/scales/blits one imageBulk operations are usually much faster than many small Python-to-pygame calls.
Example implementation
def old_draw_framebuffer(
surface: pygame.Surface,
framebuffer: Framebuffer,
scale: int,
) -> None:
# Keep the old rectangle-based implementation for comparison.
...
def draw_framebuffer(
surface: pygame.Surface,
framebuffer: Framebuffer,
scale: int,
) -> None:
# Write the framebuffer to pygame surface using one image upload.
rgb_bytes = bytearray(framebuffer.width * framebuffer.height * 3)
write_index = 0
for color in framebuffer.pixels:
red, green, blue = color
rgb_bytes[write_index] = red
rgb_bytes[write_index + 1] = green
rgb_bytes[write_index + 2] = blue
write_index += 3
frame_surface = pygame.image.frombuffer(
bytes(rgb_bytes),
(framebuffer.width, framebuffer.height),
"RGB",
)
if scale == 1:
surface.blit(frame_surface, (0, 0))
return
scaled_surface = pygame.transform.scale(
frame_surface,
(framebuffer.width * scale, framebuffer.height * scale),
)
surface.blit(scaled_surface, (0, 0))Important boundary
This optimization belongs in tools/show_framebuffer.py. The emulator core still produces a pure Framebuffer and must not import pygame.
Out of scope
- caching surfaces/buffers
- NumPy/surfarray
- Numba
- changing main.py
- frame pacing / speed cap
Run this lesson
uv run pytest tests/chapter_10_performance/test_316_fast_pygame_framebuffer_draw.py -v