327. PpuBus 水平/垂直镜像

在 PpuBus 中应用水平或垂直名称表镜像。

327 / 356 · tests/chapter_12_mirroring/test_327_ppu_bus_horizontal_vertical_mirroring.py

需要更新的文件

emulator/bus/ppu_bus.py

该步骤存在的原因

镜像位现在会通过以下路径传递

INesHeader
    -> Cartridge
    -> Mapper000
    -> PpuBus.mapper

PpuBus 终于可以在将四个逻辑名称表映射到两个物理名称表 RAM 区域时使用该元数据了。

逻辑名称表

table 0: $2000-$23FF
table 1: $2400-$27FF
table 2: $2800-$2BFF
table 3: $2C00-$2FFF

每个表占 $400 字节。完整的逻辑窗口为 $1000 字节,而当前的物理支持窗口为 $800 字节。

垂直镜像

logical:  0 1 2 3
physical: 0 1 0 1

[A B]
[A B]

水平镜像

logical:  0 1 2 3
physical: 0 0 1 1

[A A]
[B B]

建议的实现示例

# emulator/bus/ppu_bus.py

NAMETABLE_SIZE = 0x800
NAMETABLE_BYTES_PER_TABLE = 0x400
NAMETABLE_LOGICAL_SIZE = 0x1000


def normalize_nametable_addr(self, addr: int) -> int:
    if 0x3000 <= addr <= 0x3EFF:
        addr -= 0x1000

    logical_offset = (
        addr - NAMETABLE_START
    ) % NAMETABLE_LOGICAL_SIZE

    logical_table = logical_offset // NAMETABLE_BYTES_PER_TABLE
    offset_inside_table = logical_offset % NAMETABLE_BYTES_PER_TABLE

    is_vertical_mirroring = (
        True
        if self.mapper is None
        else self.mapper.is_vertical_mirroring
    )

    if is_vertical_mirroring:
        physical_table = logical_table % 2
    else:
        physical_table = logical_table // 2

    return (
        NAMETABLE_START
        + physical_table * NAMETABLE_BYTES_PER_TABLE
        + offset_inside_table
    )

为什么在没有 mapper 时也要保留垂直行为?历史上的教程测试直接构造 PpuBus,并且在卡带镜像出现之前就引入了旧的固定 $800 环绕行为。带 mapper 的总线使用真实的卡带元数据;不带 mapper 的总线则保留这种历史测试模型。

重要区别

镜像决定的是内存别名,而不会移动可见视口。当前的渲染器仍绘制固定的名称表区域,因此水平卷动是之后单独的一章内容。

不在本步骤范围内

  • 提取卷动位置
  • 跨相邻名称表裁剪视口
  • 四屏镜像
  • mapper 控制的动态镜像
  • 商业 ROM 测试夹具

运行本课

uv run pytest tests/chapter_12_mirroring/test_327_ppu_bus_horizontal_vertical_mirroring.py -v