346. 完成扫描线滚动帧
发布一份完整的定时扫描线帧,并重置当前记录缓冲区。
第 346 / 356 · tests/chapter_13_scrolling/test_346_complete_scanline_scroll_frame.py
要更新的文件
emulator/ppu/ppu.py参考资料
https://www.nesdev.org/wiki/PPU_rendering为什么需要这一步
PPU 时序会把可见扫描线状态记录到一个可变的当前帧列表中。高层渲染器必须消费来自已结束那一帧的稳定数据,而不是 PPU 仍在修改的列表。
因此 PPU 拥有两个不同的值
current_scanline_scroll_states:
mutable list used while the active frame is being stepped
completed_scanline_scroll_states:
immutable tuple published after the frame finishes在帧边界处
all 240 entries exist:
publish a 240-state tuple
any entry is missing or the list length is not 240:
publish an empty tuple
after either result:
replace current state with a fresh [None] * 240 list为什么对不完整的数据发布空元组?未知的行绝不能继承一个猜测出来的地址。空元组成为一个明确的信号:后续渲染在这一帧应继续使用现有的帧级兼容路径。
直观模型
current list = notebook still being written
completed tuple = sealed notebook safe for the renderer重要不变量
- 完成的数据恰好包含 240 个状态或零个状态
- 完成的数据是不可变的
- 当前容器和完成容器不是同一个对象
- 记录下一帧不会改变已完成的帧
- 发布发生在预渲染完成之后、计数器进入帧 0 之前
常见误解
帧完成并不发生在扫描线 241 处 VBlank 开始的时候。预渲染扫描线 261 仍属于模拟器进入下一帧之前的时序序列。
超出范围
- 在帧缓冲渲染中消费已完成的状态
- 不透明度掩码合成
- 精灵零命中的相关变更
完整示例实现
# emulator/ppu/ppu.py
@dataclass
class PPU:
...
current_scanline_scroll_states: list[BackgroundScanlineState | None] = field(
default_factory= lambda: [None] * 240
)
# --- NEW LINE: LAST COMPLETE TIMED SCANLINE FRAME ---
completed_scanline_scroll_states: tuple[
BackgroundScanlineState, ...
] = ()
...
# --- NEW BLOCK: PUBLISH AND RESET SCANLINE STATES ---
def _complete_scanline_scroll_frame(self) -> None:
current = self.current_scanline_scroll_states
if (
len(current) == 240
and all(state is not None for state in current)
):
self.completed_scanline_scroll_states = tuple(
state
for state in current
if state is not None
)
else:
self.completed_scanline_scroll_states = ()
self.current_scanline_scroll_states = [None] * 240
def step(self, cycles: int = 1) -> None:
...
if self.cycle >= PPU_CYCLES_PER_SCANLINE:
self.cycle = 0
self.scanline += 1
if self.scanline >= PPU_SCANLINES_PER_FRAME:
# --- NEW LINE: PUBLISH BEFORE ENTERING THE NEXT FRAME ---
self._complete_scanline_scroll_frame()
self.scanline = 0
self.frame += 1
...运行本课
uv run pytest tests/chapter_13_scrolling/test_346_complete_scanline_scroll_frame.py -v