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 的颜色索引。属性表会为该图块所在区域选择要使用的调色板 ID:

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

重要的硬件模型

属性表并不存储 RGB 颜色,它只选择一个背景调色板 ID。在本步骤中,我们手动传入已经解析好的 RGB 背景调色板。之后的步骤可以从 PPU 调色板 RAM 和 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) 不变,它仍然是早期测试所使用的简单单调色板渲染器。

本步骤不涉及

  • PPU 调色板 RAM 查找
  • 通用背景色行为
  • 滚动
  • 精灵
  • OAMDMA
  • pygame 显示

运行本课

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