321. PPU 根据调度设置精灵零命中

让 PPU 时序在先前检测到的屏幕位置设置精灵 0 命中。

321 / 356 · tests/chapter_11_sprite_zero_hit/test_321_ppu_sets_scheduled_sprite_zero_hit.py

需要更新的文件

emulator/ppu/ppu.py

该步骤存在的原因

第 320 步可以找到第一个重叠的精灵 0/背景像素,并返回其屏幕位置:

(screen_x, screen_y)

PPU 不应在发现该位置时立即设置 PPUSTATUS 的第 6 位。它应当存储该位置,并且只有当模拟的 PPU 时序到达对应的可见像素时才设置该标志。

简化坐标映射

screen y -> PPU scanline y
screen x -> PPU cycle x + 1

可见帧缓冲坐标从 x=0 开始,而本项目简化的 PPU 时序将可见输出视为从周期 1 开始:

screen x=0 -> PPU cycle 1
screen x=1 -> PPU cycle 2
screen x=40 -> PPU cycle 41

建议的实现改动

# --- NEW LINE ---
SpriteZeroHitPosition = tuple[int, int]
# --- END NEW LINE ---


@dataclass
class PPU:
    ...
    scanline: int = 0
    frame: int = 0
    nmi_requested: bool = False
    # --- NEW BLOCK ---
    sprite_zero_hit_position: SpriteZeroHitPosition | None = None

    def set_sprite_zero_hit_position(
        self,
        position: SpriteZeroHitPosition | None,
    ) -> None:
        self.sprite_zero_hit_position = position
    # --- END NEW BLOCK ---

    def step(self, cycles: int = 1) -> None:
        ...
        for _ in range(cycles):
            self.cycle += 1

            # --- NEW BLOCK ---
            if self.sprite_zero_hit_position is not None:
                hit_x, hit_y = self.sprite_zero_hit_position

                if self.scanline == hit_y and self.cycle == hit_x + 1:
                    self.status |= SPRITE_ZERO_HIT
                    self.sprite_zero_hit_position = None
            # --- END NEW BLOCK ---

            ...

为什么要消费该位置?该位置描述的是一个未来的时序事件。事件触发后,将其设为 None 可以防止同一个已调度事件在之后的帧中再次触发。PPUSTATUS 标志本身则保持置位状态,直到第 319 步的预渲染清除阶段。

重要区别

set_sprite_zero_hit_position((x, y))
    stores a future position

PPU.step()
    sets SPRITE_ZERO_HIT when timing reaches that position

重要边界

PPU 只接收一个中性的 tuple[int, int] | None。它不得导入渲染模块,也不应知道 CHR/背景重叠是如何被检测出来的。

不在本步骤范围内

  • Console 接线
  • 调用 find_sprite_zero_hit_position()
  • 选择精灵/背景图案表
  • PPUMASK 渲染使能规则
  • x=255 硬件例外
  • OAM Y+1 修正
  • 《超级马里奥兄弟》验证

运行本课

uv run pytest tests/chapter_11_sprite_zero_hit/test_321_ppu_sets_scheduled_sprite_zero_hit.py -v