330. Compose horizontal framebuffer viewport
Compose a 256x240 horizontal viewport from two adjacent nametable framebuffers.
Lesson 330 of 356 · tests/chapter_13_scrolling/test_330_compose_horizontal_framebuffer_viewport.py
File to update
emulator/rendering/background_viewport.pyReference documentation
https://www.nesdev.org/wiki/PPU_scrolling
https://www.nesdev.org/wiki/PPU_nametablesWhy this step exists
Each rendered nametable is 256 pixels wide, but horizontal scrolling can make one screen contain pixels from two adjacent nametables. This step treats them as one logical 512x240 background and selects a 256x240 viewport from it.
Coordinate model
left framebuffer: logical X 0-255
right framebuffer: logical X 256-511For each destination screen X
logical X = (viewport X + screen X) modulo 512The modulo operation wraps the right edge of the logical pair back to its left edge.
Example seam
viewport X = 200
screen X 0-55 <- left X 200-255
screen X 56-255 <- right X 0-199Invariants
- both source framebuffers are exactly 256x240
- the returned framebuffer is exactly 256x240
- neither source framebuffer is mutated
- every output pixel comes from the same Y row in one source framebuffer
Common misconception
This function does not implement cartridge nametable mirroring. PpuBus owns address mirroring; this function only composes already-rendered logical neighbors.
Out of scope
- vertical scrolling
- background opacity-mask composition
- PPU or PpuBus reads
- nametable tile rendering
- Console integration
- pygame
- exact dot-timed v/t transfers
Complete example implementation
# emulator/rendering/background_viewport.py
from emulator.rendering.framebuffer import Framebuffer
def compose_horizontal_framebuffer_viewport(
left: Framebuffer,
right: Framebuffer,
viewport_x: int,
) -> Framebuffer:
expected_size = (
NAMETABLE_PIXEL_WIDTH,
NAMETABLE_PIXEL_HEIGHT,
)
if (left.width, left.height) != expected_size:
raise ValueError("Left nametable framebuffer must be 256x240")
if (right.width, right.height) != expected_size:
raise ValueError("Right nametable framebuffer must be 256x240")
logical_width = NAMETABLE_PIXEL_WIDTH * 2
result = Framebuffer(
width=NAMETABLE_PIXEL_WIDTH,
height=NAMETABLE_PIXEL_HEIGHT,
)
for screen_y in range(NAMETABLE_PIXEL_HEIGHT):
for screen_x in range(NAMETABLE_PIXEL_WIDTH):
logical_x = (viewport_x + screen_x) % logical_width
if logical_x < NAMETABLE_PIXEL_WIDTH:
source = left
source_x = logical_x
else:
source = right
source_x = logical_x - NAMETABLE_PIXEL_WIDTH
color = source.get_pixel(source_x, screen_y)
result.set_pixel(screen_x, screen_y, color)
return resultRun this lesson
uv run pytest tests/chapter_13_scrolling/test_330_compose_horizontal_framebuffer_viewport.py -v