289. $4014 处的 OAMDMA

在 CPU 地址 $4014 处实现 OAMDMA。

289 / 356 · tests/chapter_06_rom_startup_preparation/test_289_oamdma_4014.py

待更新文件

emulator/bus/cpu_bus.py

为什么需要这一步

真实的 NES 游戏通常会先在 CPU 内存中准备好精灵字节,然后触发 OAMDMA 把这些字节复制到 PPU OAM 中。即使在还没有实现精灵渲染的情况下,这个复制机制也很重要,因为它能让真实 ROM 的启动代码继续运行,并为未来的精灵渲染器提供真实的 OAM 数据。

什么是 OAM?OAM 指对象属性内存(Object Attribute Memory),它是 PPU 中 256 字节的精灵内存:

64 sprites * 4 bytes each = 256 bytes

每个精灵条目的形态是

byte 0: Y position
byte 1: tile index
byte 2: attributes
byte 3: X position

什么是 OAMDMA?OAMDMA 是一种 CPU 总线写入机制。向 $4014 写入一个字节会选中一个 CPU 内存页,并把该页中全部 256 个字节复制到 PPU OAM 中。

最小示例

CPU writes $02 to $4014

这意味着

copy CPU $0200-$02FF -> PPU.oam[0x00-0xFF]

建议的实现示例

def write(self, addr: int, value: int) -> None:
    ...

    # OAMDMA: copy one CPU page into PPU OAM.
    if addr == 0x4014:
        page_start = (value & 0xFF) << 8

        for offset in range(256):
            self.ppu.oam[offset] = self.read(page_start + offset)

        return

    ...

重要细节

OAMDMA 应该通过 CpuBus.read() 读取,而不是直接读原始 RAM。数据源是 CPU 地址空间。大多数游戏使用像 $0200-$02FF 这样的页,但使用总线读取可以保证机制正确,并避免以后的重构。

常见的误解

"OAMDMA means sprites are rendered."

不是。这一步只是把字节复制到 PPU.oam 中,精灵的解码/渲染留待以后实现。

读取行为

在本教程中,$4014 是一个仅由写入触发的寄存器。CpuBus.read($4014) 目前应保持不支持。

范围之外

  • 精灵渲染
  • 精灵优先级
  • 精灵透明度
  • 精灵 0 命中
  • 精灵溢出
  • 513/514 个 CPU 周期的 DMA 停顿时序
  • 手柄 $4016

运行本课

uv run pytest tests/chapter_06_rom_startup_preparation/test_289_oamdma_4014.py -v