D-20261028-01|快照取消正規化與本機信任載入¶
背景與驅動力¶
RollbackEngine.step 每幀先 saveState() 再 step()(src/network-sync/rollback-engine.ts 第 261 行)。
issue 001074 的 cost-r22 重量顯示 saveState 每幀約 23–29 ms,占每幀實付(step 加 save)約三到四成。
saveState 的現行流程(src/physics-engine/physics-engine.ts 第 1435–1528 行):
world.takeSnapshot()取 Rapier 原生快照與輪胎負載帳(純讀取,issue 001046)。- 計算
runtimeStateHash,以 dag-cbor 編 envelope,組成 SavedState v1。 - 以剛產生的 bytes 呼叫
world.restoreSnapshot(...),把 live world 換成反序列化的世界(正規化)。 理由寫在第 1508–1510 行註解與 physics-engine.md:Rapier snapshot 不保證保留 joint/contact solver 的暖啟動內部狀態,讓不中斷續跑與日後 load/replay 從同一份 serialized solver state 出發。
第 3 步走的是對不可信快照的完整驗證路徑 createValidatedSnapshotWorld(deterministic-world.ts
第 4934–5355 行):原生 restore 之外,還逐一讀回所有 immutable collider 的拓樸與幾何(含場地 trimesh
頂點與索引)、核對原生質量(另 restore 一份拋棄式世界做重算核對)、重算 runtime hash、比對 joint 拓樸。
loadState 也走同一路徑。
2026-10-27 研究(八車三場、現行 v10 場地與零件、wasm cf640ca2、Node 24.15.0)的關鍵量測:
- Rapier 原生 take/restore 本身各只約 1–2 ms;
saveState的 18–27 ms 中,約 13–21 ms 是第 3 步的 JS 驗證(CPU profile:colliderTopologyKey含幾何讀回約 27%、原生 deserialize 約 17%、nativeMassPropertyKeys約 11%、runtimeStateHashOf約 9%)。 - 取消第 3 步後,六個場景各 600 幀(含技能輸入、發射、零件損毀、細分停損)比較:不中斷、每幀 save→load→step、每 37 幀 load 三種跑法的逐幀 state hash 與逐幀原生快照 sha256 全部逐位元相同; 與現行(有正規化)的逐幀 state hash 鏈也相同(960 幀 × 六場景 × 四變體)。也就是現行 vendored Rapier 的快照對後續步進是無損的,正規化沒有改變任何軌跡。
- 快照 bytes 已超出 catch-up 單包上限:urban-heavy8 的 SavedState 從第 186 幀起超過
SYNC_SNAPSHOT_MAX_BYTES(4,194,304),最大 4,453,009(106.2%);urban-mixed8 最大 98.9%。 其中靜態場地約占 2.87 MB(issue 001091)。
考慮過的選項¶
- 維持現況(每幀驗證式正規化,load 一律全驗證)。否決:save 18–27 ms,比 canon 的 4 ms 高 4.5–6.8 倍, 且研究證明正規化不改變軌跡,只付成本。
- 保留正規化,改走免驗證快路徑(同一份自產 bytes 直接原生 restore,後處理不變)。save 降至 5.8–10.4 ms、逐幀 hash 與現行相同。可作為保守退路,但正規化本身已證實無作用,仍多付一次 restore 與 contact cache 重綁。
- 取消正規化,本機快照信任載入(本決定)。save 降至 3.4–5.4 ms;rollback 用的
loadState降至 2.3–6.5 ms(全驗證 13–33 ms)。決定性由「快照無損」守門測試保證,而非由每幀重建保證。 - 靜態場地不進快照(vendor 層:場地 collider 的 shape 以登錄表參照序列化,或拆靜態/動態兩個 原生集合)。量測顯示場地占原生快照 60–80% bytes,但只占 take/restore 時間約 0.6–1.2 ms; 對每幀成本的效益遠小於選項 3,主要價值在 bytes(catch-up 上限、記憶體、頻寬)。需改 vendor、重建 wasm、走 adoption,並改寫 immutable 拓樸驗證(場地改核 manifest digest)。列為後續決策,不在本 ADR。
- 差分或分層快照(靜態一次、動態每幀;或對前一幀做 delta)。rollback 的 StateBuffer 可受益,但 catch-up 與觀戰仍需完整快照、原生 restore 仍需完整 bytes;要達 canon 的 64 KB 需另做自訂序列化。 否決為本輪方案。
- vendor 層最小動態序列化(只序列化動態 body、contact、joint 的自訂格式)。是唯一可能接近 64 KB 的方向,但必須重建 broad phase BVH、narrow phase graph 與 island 的內部順序並證明逐位元相同, 風險與工作量最高。否決為本輪方案,留待 canon 效能目標重訂(issue 001090)後再評估。
決定¶
saveState改為純讀取:不再以剛產生的快照重建 live world。SavedState v1 bytes 格式不變。- 物理介面新增信任邊界:
loadState(state):維持現行完整驗證,用於所有來自本行程外的快照(catch-upsync-response、 觀戰 replay checkpoint、跨行程存檔或測試 fixture)。- 新增本機載入
loadOwnState(state: OwnSavedState):只接受本行程saveState產生、未離開記憶體的 bytes,跳過原生拓樸、幾何、質量與 runtime hash 核對;envelope 的 JS 欄位驗證與原子替換語意不變。saveState()回傳 branded 型別OwnSavedState,引擎另以執行期登記(同一份記憶體參照)核對; 以型別斷言偽造品牌、複製或世界重建前的 bytes 一律拒絕。isOwnSavedState(state)供呼叫端判斷。 世界重建、載入車輛或場地時,先前的自產登記整組作廢。 RollbackEngine.rollbackAndReplay從自己的 StateBuffer 載入時,自產快照用loadOwnState, 非自產快照(例如 catch-up 重置留下的外來快照)仍用loadState;applyCatchUp與觀戰一律用loadState。- 決定性改由守門測試保證:每次 vendor/wasm 採用與 SavedState 相關變更,都必須跑「不中斷 vs 每幀 save→load→step vs 間隔 load」三向比較,逐幀 state hash 與逐幀原生快照 bytes 全等,涵蓋公版 八車三場與技能、發射、損毀、細分停損情境,Node 與 Chromium 各一次。任何不等即視為快照有損, 本決定失效,退回選項 2。
- 因為逐幀軌跡與現行相同,golden 與 conformance 不需要重基線;但
physics-engine.md中「saveState 回傳前以同一份 snapshot 重建 live world」的規則要改寫為上述信任邊界與守門測試。
後果與影響¶
- 每幀實付(step 加 save)P50 降 15–22 ms:snowy 40.8→25.1/48.6→31.0 ms、urban 60.7→53.1/ 81.9→64.2 ms、jungle 71.0→47.2/79.5→54.3 ms(mixed8/heavy8;step 有 ±6 ms 量測噪聲)。rollback 8 幀重播省下的 load 約 11–28 ms。仍未達 canon 的 save ≤4 ms(剩 take 約 1–2 ms 加 runtime hash、 cbor 與 3–4 MB 拷貝約 2.3–3.3 ms),也與 60 Hz 加回滾的即時目標差距甚遠;001074 其他降本項 (bounded-step、CCD TOI、配對過濾)仍需處理。
- 不解決快照 bytes 超限。urban-heavy8 已超出 catch-up 與觀戰 checkpoint 上限(001091),需另立 ADR(選項 4,或暫時提高上限並同步 race message、觀戰 wire 與 offer cache 配額)。
- 失去的保障:舊做法讓 live world 每幀經過
applyLockedSettings與完整驗證,曾附帶把研究 harness 直接改的原生設定蓋回(issue 001026、000884 的紀錄)。新做法下 locked settings 只在建構與 load 時 套用;它們已序列化進快照並在遠端 load 時驗證,產品路徑不受影響,但研究 harness 不能再依賴每幀覆寫。 - 遠端快照的攻擊面不變:catch-up 與觀戰仍走完整驗證;本機載入只接受自產 bytes,信任邊界寫進介面 與 canon。
- 範圍:
- src:
physics-engine.ts(saveState、新增本機載入)、deterministic-world.ts(信任 restore 分支)、interfaces/physics.ts、network-sync/rollback-engine.ts、race-runtime/net-ports.ts;race-session.ts的觀戰 checkpoint 呼叫不需改。 - vendor:不需改。
- SavedState 版本:bytes 格式不變,維持 v1(pre_launch,不需相容層)。
- canon:
physics-engine.md(saveState 正規化段)、network-sync.md(第 5 節載入語意、第 8 節 數字待 001090 一併重訂)、interfaces.md、testing.md(新增快照無損守門)。 - golden/conformance:不需重基線;新增三向無損測試。
- pinning:不涉及。
待決事項¶
runtimeStateHash是否改為只在快照要送出本行程時(offer、觀戰 checkpoint)才計算,以再省約 1 ms。 本決定維持現狀:saveState每次都計算並寫入 envelope。- 快照 bytes 超限(001091)的治本:選項 4 的 vendor 方案另立 ADR。短期已把
SYNC_SNAPSHOT_MAX_BYTES調至 8 MiB 並同步衍生配額(見 network.md)。 - canon 效能目標(network-sync 第 8 節)與 001090 的口徑矛盾如何重訂。