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