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