346. Завершить кадр прокрутки scanline

Опубликовать завершённый синхронизированный кадр scanline и сбросить текущий буфер записи.

Урок 346 из 356 · tests/chapter_13_scrolling/test_346_complete_scanline_scroll_frame.py

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

emulator/ppu/ppu.py

Ссылка

https://www.nesdev.org/wiki/PPU_rendering

Зачем нужен этот шаг

Время PPU записывает видимые состояния scanline в изменяемый список текущего кадра. Высокоуровневый рендерер должен потреблять стабильные данные из кадра, который уже завершился, а не список, который PPU всё ещё изменяет.

PPU поэтому владеет двумя разными значениями

current_scanline_scroll_states:
    mutable list used while the active frame is being stepped

completed_scanline_scroll_states:
    immutable tuple published after the frame finishes

На границе кадра

all 240 entries exist:
    publish a 240-state tuple

any entry is missing or the list length is not 240:
    publish an empty tuple

after either result:
    replace current state with a fresh [None] * 240 list

Почему опубликовать пустой кортеж для неполных данных? Неизвестная строка не должна наследовать угаданный адрес. Пустой кортеж становится чётким сигналом, что позже рендеринг должен продолжить использовать существующий путь совместимости уровня кадра для этого кадра.

Интуитивная модель

current list     = notebook still being written
completed tuple  = sealed notebook safe for the renderer

Важные инварианты

  • завершённые данные содержат ровно 240 состояний или нулевые состояния
  • завершённые данные неизменяемы
  • текущие и завершённые контейнеры — это не один и тот же объект
  • запись следующего кадра не может изменить завершённый кадр
  • публикация происходит после завершения pre-render и перед входом счётчиков в кадр 0

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

Завершение кадра не происходит, когда VBlank начинается на scanline 241. Pre-render scanline 261 всё ещё принадлежит последовательности времени перед тем, как эмулятор переходит к следующему кадру.

Вне области действия

  • потребление завершённых состояний в рендеринге framebuffer
  • композиция opacity-mask
  • изменения sprite-zero-hit

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

# emulator/ppu/ppu.py

@dataclass
class PPU:
    ...
    current_scanline_scroll_states: list[BackgroundScanlineState | None] = field(
            default_factory= lambda: [None] * 240
    )

    # --- NEW LINE: LAST COMPLETE TIMED SCANLINE FRAME ---
    completed_scanline_scroll_states: tuple[
        BackgroundScanlineState, ...
    ] = ()

    ...

    # --- NEW BLOCK: PUBLISH AND RESET SCANLINE STATES ---
    def _complete_scanline_scroll_frame(self) -> None:
        current = self.current_scanline_scroll_states

        if (
            len(current) == 240
            and all(state is not None for state in current)
        ):
            self.completed_scanline_scroll_states = tuple(
                state
                for state in current
                if state is not None
            )
        else:
            self.completed_scanline_scroll_states = ()

        self.current_scanline_scroll_states = [None] * 240

    def step(self, cycles: int = 1) -> None:
        ...

        if self.cycle >= PPU_CYCLES_PER_SCANLINE:
            self.cycle = 0
            self.scanline += 1

            if self.scanline >= PPU_SCANLINES_PER_FRAME:
                # --- NEW LINE: PUBLISH BEFORE ENTERING THE NEXT FRAME ---
                self._complete_scanline_scroll_frame()
                self.scanline = 0
                self.frame += 1

        ...

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

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