NES TDD

279. Таблица имён с атрибутами

Рендеринг фона таблицы имён с использованием палитр, выбранных по атрибутам.

Урок 279 из 356 · tests/chapter_05_rendering_pipeline/test_279_nametable_with_attributes.py

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

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

Файл для обновления

emulator/rendering/nametable_renderer.py

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

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

Этот шаг добавляет новый рендерер вместо изменения старого

old/simple:
    nametable_to_framebuffer(nametable, pattern_table, palette)

new/attribute-aware:
    nametable_with_attributes_to_framebuffer(
        nametable,
        attribute_table,
        pattern_table,
        background_palettes,
    )

Что такое background_palettes? background_palettes — это список из четырёх 4-цветных RGB-палитр:

background_palettes[0] -> palette ID 0
background_palettes[1] -> palette ID 1
background_palettes[2] -> palette ID 2
background_palettes[3] -> palette ID 3

Каждый пиксель тайла по-прежнему даёт только индекс цвета 0-3. Таблица атрибутов выбирает, какой идентификатор палитры использовать для области тайла:

palette_id = get_attribute_palette_id(attribute_table, tile_x, tile_y)
rgb = background_palettes[palette_id][color_index]

Важная аппаратная модель

Таблица атрибутов не хранит RGB-цвета. Она выбирает идентификатор фоновой палитры. Для этого шага мы передаём уже разрешённые RGB-фоновые палитры вручную. Позже другой шаг сможет строить эти палитры из палитровой RAM PPU и NES_PALETTE_RGB.

Пример возможной реализации

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 framebuffer

Правило совместимости

Оставьте nametable_to_framebuffer(..., palette) без изменений. Она остаётся простым рендерером с одной палитрой, используемым в более ранних тестах.

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

  • поиск в палитровой RAM PPU
  • поведение универсального цвета фона
  • скроллинг
  • спрайты
  • OAMDMA
  • отображение через pygame

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

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