278. Attribute table palette selection
Decode background palette selection from a nametable attribute table.
Lesson 278 of 356 · tests/chapter_05_rendering_pipeline/test_278_attribute_table_palette_selection.py
Reference
https://www.nesdev.org/wiki/PPU_attribute_tablesFile to create
emulator/rendering/attribute_table.pyWhy this step exists
The current nametable renderer uses one shared 4-color palette for every tile. Real NES backgrounds choose between four background sub-palettes using the nametable's attribute table.
This step does not render with attributes yet. It only answers one small question:
For tile coordinate (tile_x, tile_y), which palette ID does the attribute
table select?What is an attribute table?
Each nametable has
960 bytes tile IDs
64 bytes attribute tableThe attribute table is an 8x8 byte grid. Each attribute byte covers a 4x4 tile area, which is 32x32 pixels.
One attribute byte is split into four 2x2 tile quadrants
+-----------------------+
| top-left | top-right |
| 2x2 | 2x2 |
+----------+------------+
| bottom-l | bottom-r |
| 2x2 | 2x2 |
+-----------------------+Each quadrant stores a 2-bit palette ID
0b00 -> palette 0
0b01 -> palette 1
0b10 -> palette 2
0b11 -> palette 3Important distinction
The attribute table does not store RGB colors and does not store NES color indexes. It selects which background sub-palette to use. Later, palette RAM and the NES RGB palette will turn that selection into actual colors.
Bit layout inside one attribute byte
bits 0-1 -> top-left quadrant
bits 2-3 -> top-right quadrant
bits 4-5 -> bottom-left quadrant
bits 6-7 -> bottom-right quadrantThis step implements: unpack/read one quadrant from the byte.
Readable implementation example
TABLE_SIZE = 64
BYTES_PER_ROW = 8
def get_attribute_palette_id(
attribute_table: bytes,
tile_x: int,
tile_y: int,
) -> int:
if len(attribute_table) != TABLE_SIZE:
raise ValueError("Attribute table must be 64 bytes")
attribute_x = tile_x // 4
attribute_y = tile_y // 4
attribute_index = attribute_y * BYTES_PER_ROW + attribute_x
attribute_byte = attribute_table[attribute_index]
quadrant_x = (tile_x % 4) // 2
quadrant_y = (tile_y % 4) // 2
is_top_left = quadrant_x == 0 and quadrant_y == 0
is_top_right = quadrant_x == 1 and quadrant_y == 0
is_bottom_left = quadrant_x == 0 and quadrant_y == 1
if is_top_left:
return attribute_byte & 0b11
if is_top_right:
return (attribute_byte >> 2) & 0b11
if is_bottom_left:
return (attribute_byte >> 4) & 0b11
return (attribute_byte >> 6) & 0b11Why tile_x // 4 and tile_y // 4? Because each attribute byte covers 4x4 tiles.
Why (tile_x % 4) // 2?
Inside that 4x4 area
tile positions 0,1 belong to quadrant 0
tile positions 2,3 belong to quadrant 1So
0 % 4 // 2 -> 0
1 % 4 // 2 -> 0
2 % 4 // 2 -> 1
3 % 4 // 2 -> 1Out of scope
- rendering nametables with attributes
- PPU palette RAM lookup
- RGB palette conversion
- scrolling
- sprites
- pygame display
Run this lesson
uv run pytest tests/chapter_05_rendering_pipeline/test_278_attribute_table_palette_selection.py -v