345. Записать состояние прокрутки видимого scanline
Записать эффективное состояние viewport на точке 1 каждого видимого scanline.
Урок 345 из 356 · tests/chapter_13_scrolling/test_345_record_visible_scanline_scroll_state.py
Файл для обновления
emulator/ppu/ppu.pyСсылки
https://www.nesdev.org/wiki/PPU_rendering#Cycles_1-256
https://www.nesdev.org/wiki/PPU_scrolling#DetailsЗачем нужен этот шаг
PPU теперь обновляет v с горизонтальным и вертикальным временем рендеринга, но существующий высокоуровневый рендерер запускается после кадра. Ему нужна небольшая запись эффективного адреса, который представлял каждую видимую строку экрана.
Одно неизменяемое хранилище состояния
vram_addr:
the copied address represented by the prefetched pixels
fine_x:
the separate 0-7 pixel offset inside the first tileНа точке 1 реальный v на два столбца плиток впереди, потому что точки 321-336 выполнили prefetch первых двух плиток для текущего scanline. Запись поэтому выполняет:
copied visible v = real v rewound once, then rewound againПример
current scanline = 20
real v coarse X = 7
copied visible coarse X:
7 -> 6 -> 5
stored destination:
current_scanline_scroll_states[20]Отмотки изменяют горизонтальную позицию плитки, а не индекс scanline. Состояние принадлежит текущему scanline 20, а не scanline 18.
Условия записи
- рендеринг background или sprite включён
- scanline видим: 0-239
- текущая точка — 1
Важные инварианты
- буфер всегда содержит ровно 240 записей
- каждый PPU владеет отдельным буфером
- реальный PPU.vram_addr не изменяется записью
- fine X хранится отдельно
- post-render, VBlank и pre-render не записываются
Вне области действия
- архивирование завершённого кадра
- замена отсутствующих записей
- сброс текущего буфера кадра
- композиция framebuffer или opacity-mask
Полный пример реализации
# emulator/ppu/ppu.py
# --- NEW BLOCK: EFFECTIVE STATE FOR ONE VISIBLE SCANLINE ---
@dataclass(frozen=True)
class BackgroundScanlineState:
vram_addr: int
fine_x: int
@dataclass
class PPU:
...
# --- NEW LINE: CURRENT FRAME'S VISIBLE SCANLINE STATES ---
current_scanline_scroll_states: list[
BackgroundScanlineState | None
] = field(default_factory=lambda: [None] * 240)
...
# --- NEW BLOCK: RECORD THE CURRENT VISIBLE SCANLINE ---
def _record_visible_scanline_scroll_state(self) -> None:
rendering_enabled = self.mask & (
MASK_SHOW_BACKGROUND | MASK_SHOW_SPRITES
)
if not rendering_enabled:
return
if not 0 <= self.scanline < 240:
return
if self.cycle != 1:
return
visible_vram_addr = decrement_horizontal_vram_addr(
decrement_horizontal_vram_addr(self.vram_addr)
)
self.current_scanline_scroll_states[self.scanline] = (
BackgroundScanlineState(
vram_addr=visible_vram_addr,
fine_x=self.fine_x,
)
)
def step(self, cycles: int = 1) -> None:
...
for _ in range(cycles):
self.cycle += 1
self._step_horizontal_rendering_address()
self._step_vertical_rendering_address()
# --- NEW LINE: RECORD THE CURRENT SCANLINE AT DOT 1 ---
self._record_visible_scanline_scroll_state()
...Запустить этот урок
uv run pytest tests/chapter_13_scrolling/test_345_record_visible_scanline_scroll_state.py -v