260. 基于时序生成 PPU 的 VBlank
基于 PPU 时序实现 VBlank 的生成。
第 260 / 356 · tests/chapter_04_ppu_timing_and_vblank/test_260_ppu_vblank_generation_from_timing.py
参考资料
https://www.nesdev.org/wiki/PPU_rendering#Line-by-line_timing
https://www.nesdev.org/wiki/PPU_registers#PPUSTATUS需要更新的文件
emulator/ppu/ppu.py需要添加的常量
PPU_VBLANK_START_SCANLINE = 241
PPU_PRE_RENDER_SCANLINE = 261为什么需要这一步
许多 NES 程序在写入 PPU 内存之前会等待 VBlank。它们通常会轮询 PPUSTATUS($2002),直到第 7 位被置位。
本教程步骤的概念性行为
when the PPU enters scanline 241:
set PPUSTATUS bit 7, VBLANK_STARTED
when the PPU enters scanline 261, the pre-render scanline:
clear PPUSTATUS bit 7这样可以在还没有实现完整渲染的情况下,给 ROM 提供一个基本的帧信号。
建议的实现示例
PPU_VBLANK_START_SCANLINE = 241
PPU_PRE_RENDER_SCANLINE = 261
def step(self, cycles: int = 1) -> None:
for _ in range(cycles):
self.cycle += 1
if self.cycle >= PPU_CYCLES_PER_SCANLINE:
self.cycle = 0
self.scanline += 1
if self.scanline >= PPU_SCANLINES_PER_FRAME:
self.scanline = 0
self.frame += 1
# Simplified scanline-level timing event.
if self.scanline == PPU_VBLANK_START_SCANLINE:
self.status |= VBLANK_STARTED
if self.scanline == PPU_PRE_RENDER_SCANLINE:
self.status &= ~VBLANK_STARTED重要的简化说明
这并不是逐点精确的 PPU 时序。在真实硬件上,状态变化发生在具体的 PPU 点(dot)/周期上,通常建模在相关扫描线的第 1 个点/周期附近。目前,本教程使用的是“进入扫描线时”的行为方式,因为目标是先讲清楚帧/VBlank 的概念,之后再追求完整的周期精度。
目前不属于本教程目标的内容
- 逐点/逐周期精确的 VBlank 时序
- 奇数帧的周期跳过
- 在可见扫描线期间渲染像素
- 0 号精灵命中(sprite 0 hit)
- 精灵溢出
- NMI 的产生/CPU 中断处理
运行本课
uv run pytest tests/chapter_04_ppu_timing_and_vblank/test_260_ppu_vblank_generation_from_timing.py -v