347. Scanline horizontal pair

Select the horizontal logical nametable pair for one recorded scanline.

Lesson 347 of 356 · tests/chapter_13_scrolling/test_347_scanline_horizontal_pair.py

File to update

emulator/rendering/ppu_background_renderer.py

References

https://www.nesdev.org/wiki/PPU_scrolling
https://www.nesdev.org/wiki/PPU_nametables

Why this step exists

The completed timed frame stores one effective vram_addr and fine X value for each visible scanline. Before a later renderer can copy that scanline's pixels, it must know which two horizontally adjacent logical nametables form the source pair.

The nametable-select fields inside vram_addr are:

bit 10: horizontal nametable position inside a 512-pixel-wide pair
bit 11: vertical nametable row that selects the pair

Pair selection therefore uses only bit 11

bit 11 clear: ($2000, $2400)
bit 11 set:   ($2800, $2C00)

Intuitive model

upper logical row:  $2000 | $2400
lower logical row:  $2800 | $2C00

The helper chooses one complete row. A later step will use bit 10, coarse X, and fine X to choose the horizontal pixel position within that row.

Important invariants

  • only vram_addr bit 11 affects the selected pair
  • bit 10 does not change the pair
  • coarse X, coarse Y, fine Y, and fine X do not change the pair
  • returned values are logical PPU nametable addresses
  • cartridge mirroring remains PpuBus behavior
  • the helper is pure and performs no rendering or memory access

Common misconception

Bit 10 does not select between the upper and lower horizontal pairs. It identifies the left or right horizontal nametable within the pair and will become part of the viewport-X calculation in the next lesson.

Out of scope

  • decoding horizontal viewport X
  • reading nametable or pattern-table bytes
  • applying cartridge mirroring
  • composing framebuffer rows
  • selecting completed timed data versus the old fallback

Complete example implementation

# emulator/rendering/ppu_background_renderer.py

# --- UPDATED LINES: IMPORT THE RECORDED SCANLINE VALUE ---
from emulator.ppu.ppu import (
    BackgroundScanlineState,
    CTRL_BACKGROUND_PATTERN_TABLE,
    PPU,
)

...

# --- NEW BLOCK: SELECT ONE SCANLINE'S LOGICAL HORIZONTAL PAIR ---
def _scanline_horizontal_pair(
    state: BackgroundScanlineState,
) -> tuple[int, int]:
    nametable_y = (state.vram_addr >> 11) & 1
    left_base = BASE_NAMETABLE_ADDR + nametable_y * 0x0800

    return left_base, left_base + 0x0400

Run this lesson

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