329. Decode background viewport position

Decode a simplified background viewport position from PPU scrolling state.

Lesson 329 of 356 · tests/chapter_13_scrolling/test_329_decode_background_viewport_position.py

File to create

emulator/rendering/background_viewport.py

Reference documentation

https://www.nesdev.org/wiki/PPU_scrolling
https://www.nesdev.org/wiki/PPU_scrolling#PPU_internal_registers
https://www.nesdev.org/wiki/PPU_scrolling#During_rendering

Why this step exists

The PPU stores scrolling as packed hardware fields rather than ready-to-use pixel coordinates. Rendering needs a simple top-left viewport position, so this step decodes:

temp_vram_addr (t): yyy NN YYYYY XXXXX
fine_x (x):         xxx

Where

XXXXX -> coarse X tile position, 5 bits
YYYYY -> coarse Y tile position, 5 bits
NN    -> logical nametable X/Y selection
yyy   -> fine Y pixel position, 3 bits
xxx   -> fine X pixel position, 3 bits stored separately

Pixel conversion

viewport X = nametable X * 256 + coarse X * 8 + fine X
viewport Y = nametable Y * 240 + coarse Y * 8 + fine Y

Suggested implementation

# emulator/rendering/background_viewport.py

NAMETABLE_PIXEL_WIDTH = 256
NAMETABLE_PIXEL_HEIGHT = 240
TILE_PIXEL_SIZE = 8

BackgroundViewportPosition = tuple[int, int]


def decode_background_viewport_position(
    temp_vram_addr: int,
    fine_x: int,
) -> BackgroundViewportPosition:
    coarse_x = temp_vram_addr & 0b1_1111
    coarse_y = (temp_vram_addr >> 5) & 0b1_1111

    nametable_x = (temp_vram_addr >> 10) & 1
    nametable_y = (temp_vram_addr >> 11) & 1

    fine_y = (temp_vram_addr >> 12) & 0b111

    viewport_x = (
        nametable_x * NAMETABLE_PIXEL_WIDTH
        + coarse_x * TILE_PIXEL_SIZE
        + fine_x
    )

    viewport_y = (
        nametable_y * NAMETABLE_PIXEL_HEIGHT
        + coarse_y * TILE_PIXEL_SIZE
        + fine_y
    )

    return viewport_x, viewport_y

Why use t and x instead of the old scroll field? $2005 receives horizontal and vertical writes, while the compatibility scroll field stores only the latest byte. The hardware-style t and x fields preserve the complete packed state.

Accuracy boundary

Real hardware renders from current address v plus fine X after timed transfers from t into v. This tutorial currently uses t plus fine X as a frame-level snapshot. The exact dot-257 and pre-render-dot-280-304 transfers remain future timing work.

Another simplification

Real coarse Y values 30 and 31 have special wrapping behavior. This step only performs direct field-to-pixel decoding; viewport wrapping comes later.

Out of scope

  • reading nametable bytes
  • creating a framebuffer
  • crossing a nametable boundary
  • exact t -> v timing transfers
  • coarse Y 30/31 wrapping
  • commercial ROM fixtures

Run this lesson

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