340. Horizontal vram timing

Apply horizontal v increments and the horizontal t-to-v copy at PPU timing dots.

Lesson 340 of 356 · tests/chapter_13_scrolling/test_340_horizontal_vram_timing.py

File to update

emulator/ppu/ppu.py

References

https://www.nesdev.org/wiki/PPU_scrolling#During_rendering
https://www.nesdev.org/wiki/PPU_rendering#Line-by-line_timing

Why this step exists

Steps 338 and 339 implemented pure horizontal address mechanisms. This step connects them to PPU time.

On visible and pre-render scanlines while rendering is enabled

dots 8, 16, 24, ... 256:
    increment horizontal v after each fetched tile

dot 257:
    copy coarse X and horizontal nametable from t into v

dots 328 and 336:
    increment horizontal v while prefetching the first tiles for the next scanline

Rendering is enabled when either PPUMASK background or sprite rendering is enabled. The address timing is inactive during post-render and VBlank scanlines.

Simplified timeline

1---------256 257 --------320 321------336 ----340
tile fetches  reload X          prefetch tiles
  ^ every 8                     ^ 328 and 336

Important ordering

PPU.step() first advances self.cycle. The resulting value is treated as the current dot, then horizontal address timing is applied. Therefore, a PPU starting at cycle 256 performs the horizontal reload after one step at dot 257.

Common misconception

Horizontal increments are not limited to visible pixels. Dots 328 and 336 prepare the first two background tiles for the next scanline and also increment v.

Out of scope

  • vertical increment at dot 256
  • vertical t-to-v copy during pre-render
  • scanline viewport recording
  • framebuffer and opacity-mask changes

Complete example implementation

# emulator/ppu/ppu.py

class PPU:
    ...

    # --- NEW BLOCK: APPLY HORIZONTAL ADDRESS TIMING ---
    def _step_horizontal_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

        visible_fetch_increment = (
            1 <= self.cycle <= 256
            and self.cycle % 8 == 0
        )
        prefetch_increment = (
            321 <= self.cycle <= 336
            and self.cycle % 8 == 0
        )

        if visible_fetch_increment or prefetch_increment:
            self.vram_addr = increment_horizontal_vram_addr(
                self.vram_addr
            )

        if self.cycle == 257:
            self.vram_addr = copy_horizontal_scroll_bits(
                self.vram_addr,
                self.temp_vram_addr,
            )

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

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

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

            ...

Run this lesson

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