350. 选择定时帧缓冲路径

仅当一个完整的帧可用时,才选择定时帧缓冲合成。

350 / 356 · tests/chapter_13_scrolling/test_350_select_timed_framebuffer_path.py

要更新的文件

emulator/rendering/ppu_background_renderer.py

参考资料

https://www.nesdev.org/wiki/PPU_rendering
https://www.nesdev.org/wiki/PPU_scrolling

为什么需要这一步

私有的定时合成器要求每个可见行都有一个 BackgroundScanlineState。这样的数据在启动期间不可用,并且在帧不完整之后也可能缺失,因此公共适配器需要一个明确的兼容边界:

exactly 240 completed states -> use timed row composition
any other tuple length       -> use the existing t + fine-X snapshot path

为什么要求精确长度而不是真值性?空元组是假值,但只含一项或 239 项的部分元组是真值。不完整的定时数据仍会留下未知的行,绝不能进入要求全部 240 个状态的合成器。决定哪种机制安全的,是结构不变量,而不是元组是否非空。

为什么要保留旧路径?新创建的 PPU 没有已完成的帧。历史调用方和启动渲染已经具有基于 temp_vram_addr 加 fine_x 的确定性行为。保持该主体不变,可以在第一个完整的定时帧发布之前提供一条安全的备用路径。

控制流

ppu_background_viewport_to_framebuffer(ppu)
                     |
                     v
         completed length == 240?
                /                               yes              no
               |                |
               v                v
      timed row helper    existing snapshot path
               |                |
               +------ return --+

重要不变量

  • 恰好 240 个条目会选择定时辅助函数
  • 定时合成立即返回
  • 旧的源渲染器和全帧合成器不会同时运行
  • 空元组、部分元组和超长元组会选择既有的备用路径
  • 备用路径仍会解码 ppu.temp_vram_addr 和 ppu.fine_x
  • 本课中不透明度掩码的选择保持不变

常见误解

不要捕获定时辅助函数的 ValueError 并悄悄重试备用路径。公共门控负责预期的可用性;在完整帧门控之后出现的错误表明不变量已损坏,在调试期间应当保持可见。

超出范围

  • 定时的不透明度掩码合成
  • 精灵/背景优先级修正
  • 精灵零命中掩码整合
  • 完整的垂直源行滚动

完整示例实现

# emulator/rendering/ppu_background_renderer.py

def ppu_background_viewport_to_framebuffer(ppu: PPU) -> Framebuffer:
    # --- NEW BLOCK: USE TIMED DATA ONLY WHEN THE FRAME IS COMPLETE ---
    if (
        len(ppu.completed_scanline_scroll_states)
        == NAMETABLE_PIXEL_HEIGHT
    ):
        return _timed_scanlines_to_framebuffer(ppu)

    # Existing temp_vram_addr + fine_x fallback remains unchanged below.
    viewport_x, _ = decode_background_viewport_position(
        temp_vram_addr=ppu.temp_vram_addr,
        fine_x=ppu.fine_x,
    )
    ...

本课后的手动检查点

用你自己合法的《超级马里奥兄弟》ROM,你现在可以玩到足够远,一边观察定时的水平背景,一边尝试接住蘑菇。正是在这个里程碑处,蘑菇可能出现在一个本应在视觉上遮挡它的实心背景图块前面。这种现象是预期之中的:RGB 帧缓冲的各行现在使用定时扫描线状态,但用于精灵优先级的背景不透明度掩码,仍然使用一份旧的帧级快照。下一课将用同样的定时状态来合成不透明度掩码行,使颜色和优先级决策使用完全相同的屏幕坐标。

运行本课

uv run pytest tests/chapter_13_scrolling/test_350_select_timed_framebuffer_path.py -v