NES TDD

276. Таблица имён во фреймбуфер

Рендеринг упрощённого фона таблицы имён в данные фреймбуфера.

Урок 276 из 356 · tests/chapter_05_rendering_pipeline/test_276_nametable_to_framebuffer.py

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

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

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

emulator/rendering/nametable_renderer.py

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

Таблицы шаблонов содержат графику тайлов, но не описывают раскладку фона. Раскладку задают таблицы имён.

Что такое таблица имён? Таблица имён — это память PPU, хранящая, какой фоновый тайл появляется в каждой ячейке тайла на экране.

Минимальный пример

nametable[0] = 5

Это означает

top-left 8x8 background cell uses pattern table tile #5

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

pattern table = tile graphics dictionary
nametable     = tile layout/map
palette       = colors used by tile pixels

Упрощённая модель рендеринга для этого шага

nametable tile ID
    -> decoded_tiles[tile_id]
    -> tile pixel color index 0-3
    -> same shared 4-color palette[color_index]
    -> framebuffer pixel

Настоящая память таблицы имён NES

960 bytes tile IDs
64 bytes attribute table

Этот шаг намеренно использует только 960 видимых байтов тайлов и одну общую 4-цветную палитру для всех тайлов. Выбор палитры по таблице атрибутов — это более поздний шаг.

Важное упрощение

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

Размер экрана из геометрии таблицы имён

32 tiles across * 8 pixels = 256 pixels
30 tiles down   * 8 pixels = 240 pixels

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

from emulator.ppu.chr_decoder import (
    CHR_TILE_HEIGHT,
    CHR_TILE_WIDTH,
    decode_pattern_table,
)
from emulator.rendering.framebuffer import Framebuffer, RGBColor

NAMETABLE_ROWS = 30
NAMETABLE_TILES_PER_ROW = 32
NAMETABLE_SIZE = NAMETABLE_TILES_PER_ROW * NAMETABLE_ROWS

BACKGROUND_WIDTH = NAMETABLE_TILES_PER_ROW * CHR_TILE_WIDTH
BACKGROUND_HEIGHT = NAMETABLE_ROWS * CHR_TILE_HEIGHT


def nametable_to_framebuffer(
    nametable_bytes: bytes,
    pattern_table_bytes: bytes,
    palette: list[RGBColor],
) -> 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]

            for row in range(CHR_TILE_HEIGHT):
                for col in range(CHR_TILE_WIDTH):
                    # Basic starting point: every tile uses the same
                    # 4-color palette. A later renderer will use the
                    # attribute table to select different palettes.
                    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

Архитектурное правило

Не дублируйте здесь логику декодирования CHR. Переиспользуйте decode_pattern_table().

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

  • декодирование таблицы атрибутов
  • выбор палитры для каждого тайла
  • скроллинг
  • зеркалирование таблицы имён
  • чтение напрямую с шины PPU
  • спрайты
  • OAMDMA
  • отображение через pygame

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

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