343. Vertical vram timing

Apply vertical v increment and vertical t-to-v copying at PPU timing dots.

Lesson 343 of 356 · tests/chapter_13_scrolling/test_343_vertical_vram_timing.py

File to update

emulator/ppu/ppu.py

References

https://www.nesdev.org/wiki/PPU_scrolling#At_dot_256_of_each_scanline

Why this step exists

Steps 341 and 342 implemented pure vertical address mechanisms. This step connects them to PPU time.

While rendering is enabled

visible and pre-render scanlines, dot 256:
    increment fine/coarse Y in v

pre-render scanline, every dot 280-304:
    copy fine Y, vertical nametable, and coarse Y from t into v

Simplified timing

visible scanline:
    ... 255 256 257 ...
            |   |
            |   +-- horizontal reload
            +------ vertical increment

pre-render scanline 261:
    ... 256 257 ... 280----------------304 ...
        |   |       |
        |   |       +-- vertical reload active at every dot
        |   +---------- horizontal reload
        +-------------- vertical increment

Why repeat the vertical reload at every dot 280-304? If CPU writes change t during that interval, a later dot can copy the newer vertical state. Treating the interval as one operation at dot 280 would lose that behavior.

Rendering is enabled when either the PPUMASK background bit or sprite bit is set. Post-render and VBlank scanlines do not perform these automatic updates.

Important ordering at dot 256

The horizontal fetch increment runs first and the vertical increment runs second. They own different fields, so both changes must be visible in the resulting v.

Out of scope

  • effective viewport recording per scanline
  • two-tile prefetch compensation
  • framebuffer row composition
  • opacity-mask row composition

Complete example implementation

# emulator/ppu/ppu.py

class PPU:
    ...

    # --- NEW BLOCK: APPLY VERTICAL ADDRESS TIMING ---
    def _step_vertical_rendering_address(self) -> None:
        rendering_enabled = self.mask & (
            MASK_SHOW_BACKGROUND | MASK_SHOW_SPRITES
        )
        if not rendering_enabled:
            return

        rendering_scanline = (
            0 <= self.scanline < 240
            or self.scanline == PPU_PRE_RENDER_SCANLINE
        )
        if not rendering_scanline:
            return

        if self.cycle == 256:
            self.vram_addr = increment_vertical_vram_addr(
                self.vram_addr
            )

        vertical_reload = (
            self.scanline == PPU_PRE_RENDER_SCANLINE
            and 280 <= self.cycle <= 304
        )
        if vertical_reload:
            self.vram_addr = copy_vertical_scroll_bits(
                self.vram_addr,
                self.temp_vram_addr,
            )

    def step(self, cycles: int = 1) -> None:
        ...

        for _ in range(cycles):
            self.cycle += 1
            self._step_horizontal_rendering_address()

            # --- NEW LINE: APPLY VERTICAL TIMING AT THE CURRENT DOT ---
            self._step_vertical_rendering_address()

            ...

Run this lesson

uv run pytest tests/chapter_13_scrolling/test_343_vertical_vram_timing.py -v