粉紅畫面、分數暴衝、XR 當機。一個星期六的四個 bug。
四個 bug 分佈在四個子系統,每一個都通過各自的單元測試——這正是把壞掉的展示送出門的失效模式。抓住它的那套紀律,才能做出一個真正能在 RakuAI 上跑起來的範例。
有一種特定的星期六,凡是出貨過展示的人都認得。上個週末結束時,展示還好好的。這個星期六早上你把它拿起來。它現在壞成了四種不同的樣子,而且四種壞法彼此毫無關聯。
今天就是這樣。這個展示是我們作為經典範例之一出貨的 Shooter 遊戲。那些 bug 像沒被邀請的派對客人一樣候在那裡。
壞掉了什麼
展示能開機。這是好消息。之後就是:
畫面是粉紅色的。在即時渲染器裡工作過的人都認得這種死亡粉紅。它是「缺少著色器」的佔位顏色,亮粉紅,向你尖叫著渲染管線裡有什麼東西找不到它預期的著色器。展示中每一個程序化生成的物件都用這個顏色渲染。飛船。小行星。粒子。全是粉紅。到處粉紅。
分數暴衝。每一次擊殺都被登記兩次。擊殺計數器成雙上升。打倒二十個敵人,「分數:四十」。打倒四十個敵人,「分數:八十」。帳面上看起來像遊戲很大方,實際上排行榜被灌水,高分邏輯在完全錯誤的門檻上觸發。
XR 開機當機。平面螢幕上展示開機乾乾淨淨。我一插上頭戴裝置、切到 XR 模式的那一刻,硬性退出。啟動器裡撈不到任何回溯。渲染器執行緒在啟動器來得及抓到記錄之前就沒了。
攝影機不肯鎖定飛船。標準的第三人稱環繞攝影機,本該把飛船保持在畫面裡。開機時,攝影機在世界原點生成,然後就待在那裡。飛船隔著虛空看得見,是遠處的一個小點。
四個 bug。子系統零重疊。這種星期六早上,決定了你到底有沒有那套工作流程。
各自的成因
我想逐一講清楚,因為每一個都在教一堂不同的課,關於在代理驅動的工作流程裡,程式碼庫如何成長、又如何壞掉。
粉紅畫面
根本原因:著色器控制代碼在每次渲染呼叫時都以字串名稱查找,而假期期間的一次重構把著色器快取的初始化搬到了開機序列的另一個位置。程序化生成器現在在著色器快取暖機完成之前就開始渲染。它們拿回的是空控制代碼的後備值,渲染器則盡忠職守地把它畫成亮粉紅。
修法是建立一個明確的 ShaderCache 工具,在首次使用時快取解析後的著色器參照,並提供一個同步的 GetOrCreate API,讓程序化生成器可以呼叫而無需在意開機順序。每個程序化生成器都改用了它。粉紅消失。
這一課:一個隱含地活在開機序列裡的初始化順序假設,被一次沒有標示出這個假設的重構打破了。隱含的順序是脆弱的。新的快取讓順序變得明確,而且能自我修復。
分數暴衝
根本原因:GameplayHUD 在 UI 端統計擊殺,而 ScoreManager 同時也在模擬端統計擊殺。兩者都在監聽同一個擊殺事件訊號。兩者都在往一個共用的分數欄位累加。每次擊殺,分數被加了兩次。
修法是選定分數的正典擁有者。ScoreManager 擁有真相。GameplayHUD 從 ScoreManager 讀取並渲染。HUD 裡那個重複的累加被移除了。
這一課:在事件驅動的引擎裡,同一個事件的兩個訂閱者都會執行。如果兩者都寫入同一份狀態,狀態出錯的程度就和寫入的訂閱者數量成正比。修法是畫出一條清楚的線,講明誰擁有哪份狀態。線一旦畫下,這個 bug 就不可能發生了。
XR 開機當機
根本原因:XR 子系統試圖在底層圖形裝置就緒之前初始化。平面螢幕開機時,順序碰巧行得通,因為在使用者按下「Start」之前,圖形裝置總是已經就緒。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 開機守護者,在每個版本上測試自己的展示。這種公開紀律的訊號,比功能列表更重要。開始在它上面打造吧。