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