345. Record visible scanline scroll state
Record the effective viewport state at dot 1 of each visible scanline.
Lesson 345 of 356 · tests/chapter_13_scrolling/test_345_record_visible_scanline_scroll_state.py
File to update
emulator/ppu/ppu.pyReferences
https://www.nesdev.org/wiki/PPU_rendering#Cycles_1-256
https://www.nesdev.org/wiki/PPU_scrolling#DetailsWhy this step exists
The PPU now updates v with horizontal and vertical rendering timing, but the existing high-level renderer runs after the frame. It needs a small record of the effective address that represented each visible screen row.
One immutable state stores
vram_addr:
the copied address represented by the prefetched pixels
fine_x:
the separate 0-7 pixel offset inside the first tileAt dot 1, real v is two tile columns ahead because dots 321-336 prefetched the first two tiles for the current scanline. Recording therefore performs:
copied visible v = real v rewound once, then rewound againExample
current scanline = 20
real v coarse X = 7
copied visible coarse X:
7 -> 6 -> 5
stored destination:
current_scanline_scroll_states[20]The rewinds change horizontal tile position, not the scanline index. The state belongs to current scanline 20, not scanline 18.
Recording conditions
- background or sprite rendering is enabled
- scanline is visible: 0-239
- current dot is 1
Important invariants
- the buffer always has exactly 240 entries
- each PPU owns a separate buffer
- the real PPU.vram_addr is not modified by recording
- fine X is stored separately
- post-render, VBlank, and pre-render are not recorded
Out of scope
- archiving the completed frame
- replacing missing entries
- resetting the current-frame buffer
- framebuffer or opacity-mask composition
Complete example implementation
# 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()
...Run this lesson
uv run pytest tests/chapter_13_scrolling/test_345_record_visible_scanline_scroll_state.py -v