331. Составление горизонтальной области просмотра маски непрозрачности

Составить маску непрозрачности с применённой прокруткой из двух соседних масок таблиц имён.

Урок 331 из 356 · tests/chapter_13_scrolling/test_331_compose_horizontal_opaque_mask_viewport.py

Файл для обновления

emulator/rendering/background_viewport.py

Справочная документация

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

Существующий поток выполнения

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.

Новое поведение

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.

Модель координат

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

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

Для X области просмотра 200

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

Инварианты

  • каждый источник содержит ровно 256 * 240 булевых значений
  • результат — новый список из 256 * 240 элементов
  • исходные маски остаются неизменными
  • строки по Y не смещаются
  • привязка координат маски в точности совпадает с привязкой координат фреймбуфера

Распространённое заблуждение

Маска не выводит непрозрачность из итоговых цветов RGB. Она сохраняет информацию о том, был ли исходный индекс цвета паттерна фона ненулевым.

Вне объёма работ

  • построение маски из байтов CHR или таблицы имён
  • чтение памяти PPU
  • интеграция Console
  • интеграция попадания нулевого спрайта
  • вертикальная прокрутка
  • pygame

Полный пример реализации

# 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

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

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