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.pyReference 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_renderingWhy 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): xxxWhere
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 separatelyPixel conversion
viewport X = nametable X * 256 + coarse X * 8 + fine X
viewport Y = nametable Y * 240 + coarse Y * 8 + fine YSuggested 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_yWhy 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