279. Nametable with attributes
Render nametable background using attribute-selected palettes.
Lesson 279 of 356 · tests/chapter_05_rendering_pipeline/test_279_nametable_with_attributes.py
Reference
https://www.nesdev.org/wiki/PPU_attribute_tablesFile to update
emulator/rendering/nametable_renderer.pyWhy this step exists
The first nametable renderer used one shared 4-color palette for the entire background. That was a useful starting point, but real NES backgrounds use the attribute table to choose between four background palettes in different regions.
This step adds a new renderer instead of changing the old one
old/simple:
nametable_to_framebuffer(nametable, pattern_table, palette)
new/attribute-aware:
nametable_with_attributes_to_framebuffer(
nametable,
attribute_table,
pattern_table,
background_palettes,
)What are background_palettes? background_palettes is a list of four 4-color RGB palettes:
background_palettes[0] -> palette ID 0
background_palettes[1] -> palette ID 1
background_palettes[2] -> palette ID 2
background_palettes[3] -> palette ID 3Each tile pixel still produces only a color index 0-3. The attribute table selects which palette ID to use for the tile region:
palette_id = get_attribute_palette_id(attribute_table, tile_x, tile_y)
rgb = background_palettes[palette_id][color_index]Important hardware model
The attribute table does not store RGB colors. It selects a background palette ID. For this step, we pass already-resolved RGB background palettes manually. Later, another step can build those palettes from PPU palette RAM and NES_PALETTE_RGB.
Suggested implementation example
from emulator.rendering.attribute_table import get_attribute_palette_id
BackgroundPalettes = list[list[RGBColor]]
def nametable_with_attributes_to_framebuffer(
nametable_bytes: bytes,
attribute_table: bytes,
pattern_table_bytes: bytes,
background_palettes: BackgroundPalettes,
) -> Framebuffer:
if len(nametable_bytes) != NAMETABLE_SIZE:
raise ValueError("Nametable visible tile area must be 960 bytes")
decoded_tiles = decode_pattern_table(pattern_table_bytes)
framebuffer = Framebuffer(width=BACKGROUND_WIDTH, height=BACKGROUND_HEIGHT)
for tile_y in range(NAMETABLE_ROWS):
for tile_x in range(NAMETABLE_TILES_PER_ROW):
nametable_index = tile_y * NAMETABLE_TILES_PER_ROW + tile_x
tile_id = nametable_bytes[nametable_index]
tile = decoded_tiles[tile_id]
palette_id = get_attribute_palette_id(attribute_table, tile_x, tile_y)
palette = background_palettes[palette_id]
for row in range(CHR_TILE_HEIGHT):
for col in range(CHR_TILE_WIDTH):
color_index = tile[row][col]
rgb = palette[color_index]
pixel_x = tile_x * CHR_TILE_WIDTH + col
pixel_y = tile_y * CHR_TILE_HEIGHT + row
framebuffer.set_pixel(pixel_x, pixel_y, rgb)
return framebufferCompatibility rule
Keep nametable_to_framebuffer(..., palette) unchanged. It remains the simple one-palette renderer used by earlier tests.
Out of scope
- PPU palette RAM lookup
- universal background color behavior
- scrolling
- sprites
- OAMDMA
- pygame display
Run this lesson
uv run pytest tests/chapter_05_rendering_pipeline/test_279_nametable_with_attributes.py -v