系列: 与 AI 一起学编程

粉红画面、分数失控、XR 崩溃。一个周六里的四个 bug。

一个周六里的四个互不相关的集成 bug。

四个 bug,一个周六 零重叠子系统,一次集成失败 死亡之粉 缺失着色器的兜底颜色 分数 x2 两个订阅者,一个事件 XR 崩溃 在设备就绪前就初始化 摄像机丢失 异步飞船生成上的竞态
每个子系统单独都能工作。集成才是演示能否存活的地方。

四个子系统里的四个 bug,每个单独都能通过单元测试——这正是那种让演示带着问题发布出去的失败模式。抓住它,靠的是那种能让一个示例真正在 RakuAI 上跑起来的纪律。

任何发布过演示的人都知道有那么一种特定的周六。上周末结束时,演示还是好好的。这个周六早上你拿起它。它现在以四种不同的方式坏掉了,而这四种方式彼此毫无关联。

今天就是这样。这个演示是 Shooter 游戏,我们把它作为一个规范样例发布。这些 bug 就像没被邀请却自己来了的派对客人一样等在那里。

坏了什么

演示能启动。这是好消息。之后:

画面是粉红色的。 任何在实时渲染器里工作过的人都知道”死亡之粉”。它是”缺失着色器”的占位颜色,亮粉色,尖叫着告诉你渲染管线里有什么东西没能找到它期望的着色器。演示里每一个程序化生成的物体都渲染成了这个颜色。飞船。小行星。粒子。全都是粉色。到处都是粉色。

分数失控了。 每次击杀都被记录了两次。击杀计数器成对地往上跳。打掉二十个敌人,”分数:四十”。打掉四十个敌人,”分数:八十”。表面上看起来像是游戏在对你好,但实际上排行榜被灌了水,而高分逻辑是在完全错误的阈值上触发的。

XR 启动崩溃了。 演示在平板屏幕上能干净启动。而我一插上头显、切换到 XR 模式的那一刻,直接硬退出。启动器上没有出现任何回溯信息。渲染线程在启动器能抓到日志之前就没了。

摄像机死活锁不上飞船。 标准的第三人称环绕摄像机,本应让飞船保持在画面中。启动时,摄像机生成在世界原点,并一直停在那里。飞船只是虚空中远处一个小小的点,仍然可见。

四个 bug。子系统零重叠。这就是那种能决定你到底有没有掌握这套工作流的周六早晨。

每个 bug 的成因

我想逐一讲一遍每个 bug,因为它们各自都教会了一个关于代码库在智能体驱动的工作流中如何生长又如何破碎的不同教训。

粉红画面

根本原因:着色器句柄在每次渲染调用时都是按字符串名称查找的,而假期期间的一次重构把着色器缓存的初始化挪到了启动序列里的另一个位置。程序化生成器现在会在着色器缓存预热完成之前就开始渲染。它们拿到的是空句柄的兜底值,渲染器尽职尽责地把它渲染成了亮粉色。

修复方法是创建一个显式的 ShaderCache 工具,在首次使用时缓存已解析的着色器引用,并暴露一个同步的 GetOrCreate API,让程序化生成器可以调用它而不必关心启动顺序。每个程序化生成器都被更新为使用它。粉红消失了。

教训:一个隐式存在于启动序列中的初始化顺序假设,被一次没有标记出这个假设的重构打破了。隐式的顺序是脆弱的。新的缓存让顺序变得显式且能自我修复。

分数失控

根本原因:GameplayHUD 在 UI 侧统计击杀,而 ScoreManager 也在仿真侧统计击杀。两者都在监听同一个击杀事件信号。两者都在往同一个共享的分数字段上累加。分数每次击杀都被增加了两次。

修复方法是确定分数的规范所有者。ScoreManager 拥有真值。GameplayHUDScoreManager 读取并渲染。HUD 里重复的自增被移除了。

教训:在一个事件驱动的引擎里,同一个事件的两个订阅者都会各自运行。如果两者都写入同一份状态,状态出错的程度就和写入的订阅者数量成正比。修复办法是画一条清楚的线,明确谁拥有什么状态。一旦画好这条线,这个 bug 就变得不可能发生。

XR 启动崩溃

根本原因:XR 子系统在底层图形设备就绪之前就试图初始化。在平板屏幕启动时,由于用户按下”开始”时图形设备总是已经就绪,顺序恰好能对上。在 XR 启动时,头显检测在图形设备被确认就绪之前就触发了 XR 初始化路径。XR 子系统解引用了一个空的设备句柄。崩溃。

修复方法是一个 XRBootstrap,它在启动过程中尽可能早的位置检测 XR,推迟真正的 XR 初始化直到图形设备确认就绪,并且如果链路中的任何环节报告了故障就优雅关闭。现在这个崩溃变成了一条干净的错误消息,用户会回退到平板屏幕模式。

教训:硬崩溃是最糟糕的一种错误,因为它会杀死本该告诉你发生了什么的日志。更早检测到失败并干净地报告出来,是值得付出的工程成本。

死活锁不上的摄像机

根本原因:环绕摄像机在启动时尝试瞄准玩家飞船。飞船是由程序化生成器异步生成的,比摄像机初始化晚了几帧。摄像机在第一帧就去找飞船,什么都没找到,然后就再也没有重试过。

修复方法是给环绕摄像机加上一个重试机制。它会在有限的帧数内持续寻找飞船,超过这个次数才放弃;一旦锁定,就保持锁定。现在摄像机会短暂地在原点生成,然后在游戏开始的第一秒内锁定到飞船上。

教训:在略微不同的时间表上初始化的系统之间的竞态条件会咬你一口。修复办法是一个有界的小重试,不会无限循环,并带有一个明确的超时,在重试耗尽时产生一条有用的错误信息。

我从 Shooter 演示这件事上具体学到的东西

三件事,全都令人不太舒服。

每个子系统单独工作时都是好的。集成出了问题。 渲染器能工作。分数系统能工作。XR 子系统能工作。摄像机能工作。而作为一个集成体验的 Shooter 演示不能工作。这正是那种每次都能逃过单元测试的失败,因为单元测试就其本质而言是孤立地检验各个部分。

十二月末假期期间的那次重构,是这些 bug 里至少两个的直接诱因。 着色器缓存的重组和击杀事件订阅的拆分,都是在假期冲刺窗口内落地的。两者单独看都是正确的 PR。两者都以不易察觉的方式弄坏了演示。教训是应该把集成演示作为 CI 的一部分来运行,而不仅仅是单元测试。这项工作作为一个单独的 PR 今天早上已经落地。

XR 启动路径需要它自己的启动守护者。 硬崩溃是不可接受的,因为它们会杀死诊断信息。任何涉及到非平凡硬件的启动路径都需要一个守护者。我们现在为 XR 有了这个。我们还没有为空间音频或 Wi-Fi 7 链路准备好。接下来两个周六会把这两项加进队列。

合作伙伴和开发者应该从中获得什么

如果你正在构建任何集成了多个带初始化顺序依赖的子系统的东西,教训和 Shooter 演示刚刚学到的是一样的。让初始化顺序变得显式。让失败模式变得优雅。把集成演示放到 CI 里跑,而不仅仅是单元测试。

如果你是一个正在考虑采用 Raku 的独立团队,Shooter 演示是一个我们在每次发布时都会真实测试的样例。如果它坏了,你会看到它坏。这才是那种比一份功能列表更重要的公开纪律信号。

如果你是一个正在考虑把你的模型接入一个 AR 体验的 AI 实验室,XR 启动路径现在有了一个站得住脚的守护者。你的模型不会在进来的路上看到一次硬崩溃。如果链路中有任何环节失败,你会得到干净的错误报告。这套基础设施已经就位。

四个 bug。一个周六。演示现在干净地构建通过了。那些派对客人已经被请出去了。

回去继续构建。

发布在一个会运行自己的样例的运行时上

RakuAI 通过集成 CI 和优雅的 XR 启动守护者,在每次发布时都测试自己的演示。这才是比一份功能列表更重要的公开纪律信号。开始在它之上构建吧。

← 所有文章