316. 快速的 pygame 帧缓冲绘制

通过用批量 blit 替代逐像素矩形绘制,改进 pygame 帧缓冲的绘制方式。

316 / 356 · tests/chapter_10_performance/test_316_fast_pygame_framebuffer_draw.py

需要更新的文件

tools/show_framebuffer.py

为什么需要这一步

最初的 draw_framebuffer() 有意保持简单,便于教学:

for each framebuffer pixel:
    create/fill one scaled pygame rectangle

对于一帧 NES 画面来说,这意味着

256 * 240 = 61,440 pygame drawing operations per frame

这种方式很慢,因为 Python 需要为成千上万个微小的矩形反复进入 pygame/SDL。

这一步保留了旧的实现以便对比,将其重命名为

old_draw_framebuffer(...)

然后创建一个更快的新版 draw_framebuffer(...),它会:

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 window

心智模型

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 image

批量操作通常比大量小规模的 Python 到 pygame 调用要快得多。

示例实现

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))

重要的边界

这项优化应放在 tools/show_framebuffer.py 中。模拟器核心仍然只生成纯粹的 Framebuffer,不得导入 pygame。

本步骤范围之外

  • 缓存 surface/缓冲区
  • NumPy/surfarray
  • Numba
  • 更改 main.py
  • 帧节奏 / 速度上限

运行本课

uv run pytest tests/chapter_10_performance/test_316_fast_pygame_framebuffer_draw.py -v