316. Dibujado rápido del framebuffer con pygame

Mejora el dibujado del framebuffer con pygame sustituyendo los rectángulos por píxel por un blit masivo.

Lección 316 de 356 · tests/chapter_10_performance/test_316_fast_pygame_framebuffer_draw.py

Archivo a actualizar

tools/show_framebuffer.py

Por qué existe este paso

La draw_framebuffer() original era intencionadamente sencilla y educativa:

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

Para un fotograma de NES, eso significa

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

Eso es lento porque Python cruza repetidamente hacia pygame/SDL para miles de rectángulos diminutos.

Este paso conserva la implementación antigua para comparación renombrándola

old_draw_framebuffer(...)

Luego crea una nueva draw_framebuffer(...) más rápida que:

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

Modelo mental

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

Las operaciones masivas suelen ser mucho más rápidas que muchas llamadas pequeñas de Python a pygame.

Implementación de ejemplo

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

Límite importante

Esta optimización pertenece a tools/show_framebuffer.py. El núcleo del emulador sigue produciendo un Framebuffer puro y no debe importar pygame.

Fuera de alcance

  • cachear superficies/búferes
  • NumPy/surfarray
  • Numba
  • cambiar main.py
  • ritmo de fotogramas / límite de velocidad

Ejecutar esta lección

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