331. 组合水平不透明度掩码视口

从两个相邻的名称表掩码组合出一个已滚动的不透明度掩码。

331 / 356 · tests/chapter_13_scrolling/test_331_compose_horizontal_opaque_mask_viewport.py

待更新文件

emulator/rendering/background_viewport.py

参考文档

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

现有流程

build_background_opaque_mask() converts one nametable into a 256x240 list[bool].
Sprite priority and sprite-zero-hit detection index that mask using screen
coordinates.

新行为

This step does not replace the existing mask builder. It composes two already
constructed masks into the same horizontal viewport introduced for framebuffers
in Step 330.

坐标模型

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

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

对于视口 X 200

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

不变量

  • 每个源都恰好包含 256 * 240 个布尔条目
  • 结果是一个新的 256 * 240 条目列表
  • 源掩码保持不变
  • Y 行不移动
  • 掩码映射与帧缓冲区映射完全一致

常见误解

该掩码并不是从最终的 RGB 颜色推导不透明度的。它保留的是原始背景图案颜色索引是否非零。

范围之外

  • 从 CHR 或名称表字节构建掩码
  • 读取 PPU 内存
  • 与 Console 集成
  • 与 sprite-zero-hit 集成
  • 垂直滚动
  • pygame

完整示例实现

# emulator/rendering/background_viewport.py

from emulator.rendering.nametable_renderer import BackgroundOpaqueMask


def compose_horizontal_opaque_mask_viewport(
    left: BackgroundOpaqueMask,
    right: BackgroundOpaqueMask,
    viewport_x: int,
) -> BackgroundOpaqueMask:
    expected_size = NAMETABLE_PIXEL_WIDTH * NAMETABLE_PIXEL_HEIGHT

    if len(left) != expected_size:
        raise ValueError(
            f"Left background opacity mask must contain {expected_size} entries"
        )

    if len(right) != expected_size:
        raise ValueError(
            f"Right background opacity mask must contain {expected_size} entries"
        )

    logical_width = NAMETABLE_PIXEL_WIDTH * 2
    result = [False] * expected_size

    for screen_y in range(NAMETABLE_PIXEL_HEIGHT):
        row_start = screen_y * NAMETABLE_PIXEL_WIDTH

        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

            destination_index = row_start + screen_x
            source_index = row_start + source_x
            result[destination_index] = source[source_index]

    return result

运行本课

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