NES TDD

278. Выбор палитры по таблице атрибутов

Декодирование выбора фоновой палитры из таблицы атрибутов таблицы имён.

Урок 278 из 356 · tests/chapter_05_rendering_pipeline/test_278_attribute_table_palette_selection.py

Справочная информация

https://www.nesdev.org/wiki/PPU_attribute_tables

Файл для создания

emulator/rendering/attribute_table.py

Почему этот шаг важен

Текущий рендерер таблицы имён использует одну общую 4-цветную палитру для каждого тайла. Настоящие фоны NES выбирают между четырьмя фоновыми субпалитрами с помощью таблицы атрибутов таблицы имён.

Этот шаг пока не выполняет рендеринг с атрибутами. Он отвечает лишь на один небольшой вопрос:

For tile coordinate (tile_x, tile_y), which palette ID does the attribute
table select?

Что такое таблица атрибутов?

У каждой таблицы имён есть

960 bytes tile IDs
64 bytes attribute table

Таблица атрибутов — это сетка байтов размером 8x8. Каждый байт атрибута покрывает область 4x4 тайла, то есть 32x32 пикселя.

Один байт атрибута разбит на четыре квадранта размером 2x2 тайла

+-----------------------+
| top-left | top-right  |
|  2x2     |   2x2      |
+----------+------------+
| bottom-l | bottom-r   |
|  2x2     |   2x2      |
+-----------------------+

Каждый квадрант хранит 2-битный идентификатор палитры

0b00 -> palette 0
0b01 -> palette 1
0b10 -> palette 2
0b11 -> palette 3

Важное различие

Таблица атрибутов не хранит RGB-цвета и не хранит индексы цвета NES. Она выбирает, какую фоновую субпалитру использовать. Позже палитровая RAM и RGB-палитра NES превратят этот выбор в реальные цвета.

Расположение битов внутри одного байта атрибута

bits 0-1 -> top-left quadrant
bits 2-3 -> top-right quadrant
bits 4-5 -> bottom-left quadrant
bits 6-7 -> bottom-right quadrant

Этот шаг реализует: распаковку/чтение одного квадранта из байта.

Пример читаемой реализации

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) & 0b11

Почему tile_x // 4 и tile_y // 4? Потому что каждый байт атрибута покрывает область 4x4 тайла.

Почему (tile_x % 4) // 2?

Внутри этой области 4x4

tile positions 0,1 belong to quadrant 0
tile positions 2,3 belong to quadrant 1

Таким образом

0 % 4 // 2 -> 0
1 % 4 // 2 -> 0
2 % 4 // 2 -> 1
3 % 4 // 2 -> 1

Вне рамок этого шага

  • рендеринг таблиц имён с атрибутами
  • поиск в палитровой RAM PPU
  • преобразование в RGB-палитру
  • скроллинг
  • спрайты
  • отображение через pygame

Запустить этот урок

uv run pytest tests/chapter_05_rendering_pipeline/test_278_attribute_table_palette_selection.py -v