330. 合成水平帧缓冲视口

从两个相邻的名称表帧缓冲中合成一个 256x240 的水平视口。

330 / 356 · tests/chapter_13_scrolling/test_330_compose_horizontal_framebuffer_viewport.py

需要更新的文件

emulator/rendering/background_viewport.py

参考文档

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

该步骤存在的原因

每个已渲染的名称表宽 256 像素,但水平卷动会导致一个屏幕同时包含来自两个相邻名称表的像素。本步骤将它们视为一个逻辑上的 512x240 背景,并从中选取一个 256x240 的视口。

坐标模型

left framebuffer:  logical X   0-255
right framebuffer: logical X 256-511

对于每个目标屏幕 X

logical X = (viewport X + screen X) modulo 512

取模运算会将逻辑组合体右边缘的部分环绕回其左边缘。

接缝示例

viewport X = 200

screen X   0-55  <- left X 200-255
screen X  56-255 <- right X   0-199

不变量

  • 两个源帧缓冲都恰好是 256x240
  • 返回的帧缓冲恰好是 256x240
  • 两个源帧缓冲均不会被修改
  • 每个输出像素都来自同一个源帧缓冲中的同一 Y 行

常见误解

该函数并未实现卡带名称表镜像。地址镜像由 PpuBus 负责;该函数只是合成已经渲染好的逻辑相邻区域。

不在本步骤范围内

  • 垂直卷动
  • 背景不透明度掩码的合成
  • PPU 或 PpuBus 的读取操作
  • 名称表图块渲染
  • Console 集成
  • pygame
  • 精确的按时钟点的 v/t 传输

完整的示例实现

# emulator/rendering/background_viewport.py

from emulator.rendering.framebuffer import Framebuffer


def compose_horizontal_framebuffer_viewport(
    left: Framebuffer,
    right: Framebuffer,
    viewport_x: int,
) -> Framebuffer:
    expected_size = (
        NAMETABLE_PIXEL_WIDTH,
        NAMETABLE_PIXEL_HEIGHT,
    )

    if (left.width, left.height) != expected_size:
        raise ValueError("Left nametable framebuffer must be 256x240")

    if (right.width, right.height) != expected_size:
        raise ValueError("Right nametable framebuffer must be 256x240")

    logical_width = NAMETABLE_PIXEL_WIDTH * 2
    result = Framebuffer(
        width=NAMETABLE_PIXEL_WIDTH,
        height=NAMETABLE_PIXEL_HEIGHT,
    )

    for screen_y in range(NAMETABLE_PIXEL_HEIGHT):
        for screen_x in range(NAMETABLE_PIXEL_WIDTH):
            logical_x = (viewport_x + screen_x) % logical_width

            if logical_x < NAMETABLE_PIXEL_WIDTH:
                source = left
                source_x = logical_x
            else:
                source = right
                source_x = logical_x - NAMETABLE_PIXEL_WIDTH

            color = source.get_pixel(source_x, screen_y)
            result.set_pixel(screen_x, screen_y, color)

    return result

运行本课

uv run pytest tests/chapter_13_scrolling/test_330_compose_horizontal_framebuffer_viewport.py -v