350. Выбрать путь синхронизированного framebuffer
Выбрать композицию синхронизированного framebuffer только когда доступен один завершённый кадр.
Урок 350 из 356 · tests/chapter_13_scrolling/test_350_select_timed_framebuffer_path.py
Файл для обновления
emulator/rendering/ppu_background_renderer.pyСсылки
https://www.nesdev.org/wiki/PPU_rendering
https://www.nesdev.org/wiki/PPU_scrollingЗачем нужен этот шаг
Приватный синхронизированный композитор требует один BackgroundScanlineState для каждой видимой строки. Это свидетельство недоступно при запуске и может отсутствовать после неполного кадра, поэтому публичный адаптер нуждается в явной границе совместимости:
exactly 240 completed states -> use timed row composition
any other tuple length -> use the existing t + fine-X snapshot pathПочему требовать точную длину вместо truthiness? Пустой кортеж — это false, но частичный кортеж с одной записью или 239 записями — это true. Частичные данные времени всё ещё оставляют неизвестные строки и не должны входить в композитор, который требует все 240 состояний. Структурный инвариант, а не то, является ли кортеж непустым, решает, какой механизм безопасен.
Почему сохранить старый путь? Новый PPU не имеет завершённого кадра. Исторические вызывающие и рендеринг при запуске уже имеют детерминированное поведение на основе temp_vram_addr плюс fine_x. Сохранение этого тела неизменным обеспечивает безопасный fallback до публикации первого завершённого синхронизированного кадра.
Поток управления
ppu_background_viewport_to_framebuffer(ppu)
|
v
completed length == 240?
/ yes no
| |
v v
timed row helper existing snapshot path
| |
+------ return --+Важные инварианты
- ровно 240 записей выбирают синхронизированную вспомогательную функцию
- синхронизированная композиция возвращается немедленно
- старый исходный рендерер и полнокадровый композитор также не запускаются
- пустые, частичные и переполненные кортежи выбирают установленный fallback
- fallback всё ещё декодирует ppu.temp_vram_addr и ppu.fine_x
- выбор opacity-mask остаётся неизменным в этом уроке
Распространённое заблуждение
Не ловите ValueError синхронизированной вспомогательной функции и молча не повторяйте fallback. Публичный gate владеет ожидаемой доступностью; ошибка после gate завершённого кадра указывает на нарушенный инвариант, который должен остаться видимым при отладке.
Вне области действия
- синхронизированная композиция opacity-mask
- коррекция приоритета sprite/background
- интеграция маски sprite-zero-hit
- полная вертикальная прокрутка исходной строки
Полный пример реализации
# emulator/rendering/ppu_background_renderer.py
def ppu_background_viewport_to_framebuffer(ppu: PPU) -> Framebuffer:
# --- NEW BLOCK: USE TIMED DATA ONLY WHEN THE FRAME IS COMPLETE ---
if (
len(ppu.completed_scanline_scroll_states)
== NAMETABLE_PIXEL_HEIGHT
):
return _timed_scanlines_to_framebuffer(ppu)
# Existing temp_vram_addr + fine_x fallback remains unchanged below.
viewport_x, _ = decode_background_viewport_position(
temp_vram_addr=ppu.temp_vram_addr,
fine_x=ppu.fine_x,
)
...Ручная контрольная точка после этого урока
С вашим собственным легальным ROM Super Mario Bros. вы теперь можете играть достаточно далеко, чтобы попытаться поймать гриб, наблюдая за синхронизированным горизонтальным background. На этом точном этапе гриб может появиться перед твёрдой плиткой background, которая должна визуально его закрывать. Этот симптом ожидаем: строки RGB framebuffer теперь используют синхронизированные состояния scanline, но маска opacity background, используемая для приоритета sprite, всё ещё использует один старый снимок уровня кадра. Следующий урок будет составлять строки opacity-mask из одних и тех же синхронизированных состояний, чтобы решения о цвете и приоритете использовали идентичные координаты экрана.
Запустить этот урок
uv run pytest tests/chapter_13_scrolling/test_350_select_timed_framebuffer_path.py -v