NES TDD

316. Быстрая отрисовка framebuffer в pygame

Улучшаем отрисовку framebuffer в pygame, заменяя попиксельные прямоугольники на массовый blit.

Урок 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.

Вне рамок

  • кэширование поверхностей/буферов
  • NumPy/surfarray
  • Numba
  • изменение main.py
  • выравнивание кадров / ограничение скорости

Запустить этот урок

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