331. Componer viewport de máscara de opacidad horizontal

Compone una máscara de opacidad desplazada a partir de dos máscaras de nametables adyacentes.

Lección 331 de 356 · tests/chapter_13_scrolling/test_331_compose_horizontal_opaque_mask_viewport.py

Archivo a actualizar

emulator/rendering/background_viewport.py

Documentación de referencia

https://www.nesdev.org/wiki/PPU_scrolling
https://www.nesdev.org/wiki/PPU_nametables

Flujo existente

build_background_opaque_mask() converts one nametable into a 256x240 list[bool].
Sprite priority and sprite-zero-hit detection index that mask using screen
coordinates.

Nuevo comportamiento

This step does not replace the existing mask builder. It composes two already
constructed masks into the same horizontal viewport introduced for framebuffers
in Step 330.

Modelo de coordenadas

left mask:  logical X   0-255
right mask: logical X 256-511

logical X = (viewport X + screen X) modulo 512

Para el viewport X 200

screen X   0-55  <- left X 200-255
screen X  56-255 <- right X   0-199

Invariantes

  • cada origen contiene exactamente 256 * 240 entradas booleanas
  • el resultado es una nueva lista de 256 * 240 entradas
  • las máscaras de origen permanecen sin cambios
  • las filas Y no se mueven
  • la asignación de la máscara coincide exactamente con la asignación del framebuffer

Error común

La máscara no deriva la opacidad de los colores RGB finales. Conserva si el índice de color del patrón de fondo original era distinto de cero.

Fuera del alcance

  • construir una máscara a partir de bytes de CHR o de nametable
  • leer memoria de la PPU
  • integración en Console
  • integración del sprite-zero-hit
  • desplazamiento vertical
  • pygame

Implementación de ejemplo completa

# emulator/rendering/background_viewport.py

from emulator.rendering.nametable_renderer import BackgroundOpaqueMask


def compose_horizontal_opaque_mask_viewport(
    left: BackgroundOpaqueMask,
    right: BackgroundOpaqueMask,
    viewport_x: int,
) -> BackgroundOpaqueMask:
    expected_size = NAMETABLE_PIXEL_WIDTH * NAMETABLE_PIXEL_HEIGHT

    if len(left) != expected_size:
        raise ValueError(
            f"Left background opacity mask must contain {expected_size} entries"
        )

    if len(right) != expected_size:
        raise ValueError(
            f"Right background opacity mask must contain {expected_size} entries"
        )

    logical_width = NAMETABLE_PIXEL_WIDTH * 2
    result = [False] * expected_size

    for screen_y in range(NAMETABLE_PIXEL_HEIGHT):
        row_start = screen_y * NAMETABLE_PIXEL_WIDTH

        for screen_x in range(NAMETABLE_PIXEL_WIDTH):
            logical_x = (viewport_x + screen_x) % logical_width

            if logical_x < NAMETABLE_PIXEL_WIDTH:
                source = left
                source_x = logical_x
            else:
                source = right
                source_x = logical_x - NAMETABLE_PIXEL_WIDTH

            destination_index = row_start + screen_x
            source_index = row_start + source_x
            result[destination_index] = source[source_index]

    return result

Ejecutar esta lección

uv run pytest tests/chapter_13_scrolling/test_331_compose_horizontal_opaque_mask_viewport.py -v