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