332. Ppu logical nametable to framebuffer
Render one selected logical PPU nametable as a framebuffer.
Lesson 332 of 356 · tests/chapter_13_scrolling/test_332_ppu_logical_nametable_to_framebuffer.py
File to update
emulator/rendering/ppu_background_renderer.pyReference
https://www.nesdev.org/wiki/PPU_nametablesWhy this step exists
The original helper always rendered logical nametable $2000. Horizontal scrolling also needs the adjacent logical nametable, so the helper now accepts one of:
$2000, $2400, $2800, $2C00Memory layout relative to the selected base
base + $000-$3BF: 960 visible tile IDs
base + $3C0-$3FF: 64 attribute bytesCompatibility
Omitting base_nametable_addr must still render $2000. Existing callers therefore do not need to change.
Mirroring boundary
Rendering requests logical addresses through PpuBus. PpuBus remains responsible for mapping those addresses to horizontally or vertically mirrored physical VRAM.
Out of scope
- selecting and composing two adjacent framebuffers
- opacity-mask address selection
- Console integration
- vertical viewport composition
- pygame
Example implementation
# emulator/rendering/ppu_background_renderer.py
# --- NEW LINES: ACCEPTED LOGICAL NAMETABLE BASE ADDRESSES ---
LOGICAL_NAMETABLE_BASE_ADDRS = (
0x2000,
0x2400,
0x2800,
0x2C00,
)
def ppu_background_to_framebuffer(
ppu: PPU,
# --- NEW LINE: OPTIONAL LOGICAL NAMETABLE SELECTION ---
base_nametable_addr: int = BASE_NAMETABLE_ADDR,
) -> Framebuffer:
# --- NEW BLOCK: REJECT NON-NAMETABLE BASE ADDRESSES ---
if base_nametable_addr not in LOGICAL_NAMETABLE_BASE_ADDRS:
raise ValueError(
"Logical nametable base address must be $2000, $2400, $2800, $2C00"
)
nametable_bytes = bytes(
# --- UPDATED LINE: READ FROM THE SELECTED NAMETABLE ---
ppu.ppu_bus.read(base_nametable_addr + offset)
for offset in range(NAMETABLE_SIZE)
)
# --- NEW LINE: DERIVE THE SELECTED ATTRIBUTE-TABLE BASE ---
attribute_table_base = base_nametable_addr + NAMETABLE_SIZE
attribute_table = bytes(
# --- UPDATED LINE: READ THE SELECTED ATTRIBUTE TABLE ---
ppu.ppu_bus.read(attribute_table_base + offset)
for offset in range(ATTR_TABLE_SIZE)
)
...
# Everything remains the same below this point
Run this lesson
uv run pytest tests/chapter_13_scrolling/test_332_ppu_logical_nametable_to_framebuffer.py -v