348. Scanline viewport x

Decode horizontal viewport X from one recorded scanline state.

Lesson 348 of 356 · tests/chapter_13_scrolling/test_348_scanline_viewport_x.py

File to update

emulator/rendering/ppu_background_renderer.py

Reference

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

Why this step exists

Step 347 selected the two logical nametables that form a horizontal source pair. A later row compositor also needs the exact pixel where this scanline begins inside that 512-pixel-wide pair.

The recorded address uses the same packed scrolling layout as t

yyy NN YYYYY XXXXX

Horizontal viewport X uses

XXXXX          coarse X tile column
low N bit      horizontal nametable selection, vram_addr bit 10
state.fine_x   pixel offset inside the first tile

Pixel conversion

viewport_x = nametable_x * 256 + coarse_x * 8 + fine_x

Example

nametable X = 1
coarse X    = 5
fine X      = 3

viewport X = 1 * 256 + 5 * 8 + 3 = 299

Why reuse decode_background_viewport_position? The existing pure decoder already owns the packed scrolling-field conversion. Both t and recorded v have the same bit layout, so repeating masks and dimensions in this renderer would create two implementations that could drift apart.

Important invariants

  • horizontal nametable bit 10 contributes 256 pixels
  • coarse X contributes eight pixels per tile
  • fine X contributes the final 0-7 pixel offset
  • vertical nametable, coarse Y, and fine Y do not affect viewport X
  • the result remains inside the logical horizontal range 0-511
  • the recorded address is not rewound again
  • the helper performs no rendering or memory access

Common misconception

The recorded address does not need another two-tile correction. Step 345 already rewound a copy of v twice before constructing BackgroundScanlineState. Repeating the rewind here would incorrectly shift every rendered row sixteen pixels left.

Out of scope

  • selecting logical nametable addresses
  • composing framebuffer rows
  • applying horizontal wrap to destination pixels
  • opacity-mask composition
  • choosing timed data versus the old frame-level fallback

Complete example implementation

# emulator/rendering/ppu_background_renderer.py

# --- NEW BLOCK: DECODE ONE SCANLINE'S HORIZONTAL VIEWPORT ---
def _scanline_viewport_x(state: BackgroundScanlineState) -> int:
    viewport_x, _ = decode_background_viewport_position(
        temp_vram_addr=state.vram_addr,
        fine_x=state.fine_x,
    )

    return viewport_x

Run this lesson

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