跳轉到

D-20261028-02|不可變形狀以登錄參照寫入快照

背景與驅動力

issue 001091:八車世界的 SavedState 超過 catch-up 單包上限 SYNC_SNAPSHOT_MAX_BYTES(4,194,304 B)。 urban-heavy8 最大 4,453,009 B(106.2%),自第 186 幀起持續超限;urban-mixed8 最大 98.9%。觀戰 checkpoint (base64url JSON)同樣超過 SPECTATOR_REPLAY_WIRE_MAX_BYTES。rollback StateBuffer 每格也要拷貝整份快照。 短期措施曾把上限調為 8 MiB 並同步衍生配額。

原生快照(vendored Rapier、bincode)把世界 9 個元件整包序列化:gravity、integration parameters、island manager、broad phase(BVH+pair 表)、narrow phase(contact/intersection graph、solver color mask)、rigid body set、collider set、impulse joints、multibody joints。每個 collider 的形狀(SharedShape= Arc<dyn Shape>)以完整內容內嵌,trimesh 含頂點、索引、內部 BVH、拓樸與擬法線(鬼三角)資料。

研究原型(cf640ca2 同源、三公版場地 v10、Node 24.15.0)把 bytes 拆到元件:

場地(只建場地、未放車) 原生快照 其中形狀內容 場地 collider 數 形狀參照化後
snowy(track-01) 1,620,153 B — 710 151,919 B
urban(track-02) 2,872,531 B 2,784,992 B 444 94,643 B
jungle(track-03) 2,362,000 B — 799 169,541 B

靜態場地的 bytes 幾乎全在 collider set 的形狀內容;場地 collider 的其餘欄位(位姿、材質、旗標、質量 快取、父 body)每個約 200 B。broad phase 與 narrow phase 中與場地相關的部分是動態狀態:BVH 葉節點的 樹形決定配對順序,narrow phase 的車對場地 pair 帶 manifold 與暖啟動 impulse;兩者都必須原樣保留。 車輛零件的凸包/trimesh 形狀同樣不可變,佔剩餘 bytes 的大宗。

考慮過的選項

  1. A:只調高上限(並同步 race message、觀戰 wire、offer cache 與全域配額)。曾為短期措施;不解決 記憶體、頻寬與 StateBuffer 拷貝,新增場地內容會再次逼近。
  2. D-1:僅場地形狀登錄參照。
  3. D-2:所有不可變形狀登錄參照(場地、車輛零件、持彈、場地實體中建構期就存在、之後不改形狀的 trimesh/凸包)。本決定。
  4. D':連同 broad phase BVH 與 narrow phase 的場地部分一併參照化或 restore 時重建。否決:BVH 樹形與 pair 順序是步進結果的一部分,重建不保證逐位元相同;其 bytes 屬動態狀態,參照化也省不了。
  5. E:差分快照。否決為本輪方案:catch-up、觀戰 checkpoint 與原生 restore 仍需完整 bytes,差分需要 共同基準與重組成本,對單包上限無直接幫助;日後若要做應建立在本決定之上。
  6. F:vendor 自訂最小動態序列化。唯一可能接近 canon 的 64 KB 目標,但要證明 broad phase、narrow phase、island 與 solver 內部順序重建後逐位元相同,工作量與風險最高;留待 001090 效能目標重訂後評估。
  7. 既有研究碼 ResearchShapeTable(指標序號、owner token)。只在單一行程、單一交易內有效,不能跨 peer;不採用,本決定沿用其「thread-local 登錄+序列化時查表」的形式。

決定

  1. vendor(parry witness SharedShape serde):形狀線上格式為兩分支。內嵌分支維持上游 TypedShape 的原位元組;Arc 位址在不可變形狀登錄表內的形狀寫 TypedShape 第 20 號 variant O4Registered(批次 id u64, 序號 u32)(16 B),排在全部上游 variant 之後。反序列化參照時從登錄表取 該批該序號的同一個 Arc;批次或序號不存在即整份反序列化失敗(World.restoreSnapshot 回傳 null)。 未登錄任何形狀時,快照與父代逐位元相同。
  2. 登錄表:每個 wasm instance 一張(thread-local),由內容定址的批次組成。批次一經插入不可變。每次 成功登錄都是一筆有自己 token 的登錄:新 id 插入批次;同 id 再次登錄時逐序號比對內嵌線上 bytes 全等, 相等才把新 Arc 位址設為同序號的別名,否則拒絕;已屬他批的 Arc 拒絕。釋放 token 時移除該次登錄的 別名 Arc 與鍵;某批最後一筆登錄釋放時移除整批與所有指向它的鍵。登錄表持有所有有鍵 Arc 的 clone, 有鍵期間位址不會被重用。binding:coStaticShapeWireBytes(每個形狀為 u64 LE 長度加內嵌 bincode bytes,一律內嵌)、coRegisterStaticShapes(回傳 [token, 1 插入/0 別名],錯誤為 [-1]/[-2])、 coReleaseStaticShapes(token)、coStaticShapeKey、coStaticShapesInline(成對進出的完整內嵌範圍)與 coStaticShapeRegistryCounts。
  3. 批次 id:sha256。由 TS 對 coStaticShapeWireBytes 的輸出計算(vendor 不新增雜湊依賴)。線上參照 取 sha256 的前 64 位元,SavedState envelope 另帶完整 sha256。選 64 位元前綴而不用 envelope 表序號: 原生快照自足、不需每次存取時傳入對映表,參照 16 B;前綴碰撞只能在同一個 wasm instance 內兩批不同 內容之間發生,登錄端以全等比對拒絕(決定性失敗,所有 peer 相同),載入端以完整 sha256 核對 envelope。
  4. TS(deterministic-world):建構期 recordImmutableBodyColliderTopology 之後,把本次新增的不可變 collider 中 trimesh/凸包形狀依 handle 升冪登錄為一批(場地一批、每台車一批;同款車內容相同即成為 別名)。外來快照的完整驗證不再讀回幾何:collider 的登錄鍵等於建構期記錄的登錄鍵即通過,鍵不符 (改寫為內嵌或指到別的序號)退回完整幾何比對;其餘拓樸、質量、joint、runtime hash 驗證不變。 DeterministicWorld.destroy 以 token 逐一釋放本世界的登錄,編輯器、驗證用拋棄式世界與比賽世界並存時 各自持有自己的登錄,最後一個使用該批的世界銷毀時才移除。
  5. SavedState:envelope 新增 staticShapeBatches,依首次登錄順序列出本世界引用的批次 [批次 id(sha256 前 8 bytes), sha256];載入端(loadState、savedStateFrame、loadOwnState)先核對 清單與本機世界登錄的批次逐位元全等,才交給原生 restore。維持 SavedState v1 原地改,不升號:pre_launch 沒有已發布的 v1 資料需辨識,新舊核心以 engineVersion(wasm digest)區分,envelope 的鍵集合核對也會 拒收舊格式。
  6. 決定性守門:沿用 D-20261028-01 的三向無損測試,並追加兩項:(a) 參照模式下每幀另以完整內嵌範圍取得 原生快照,逐幀 sha256 在不中斷、每幀 save→load、間隔 load 三種跑法間全等;(b) 跨登錄表:甲輸出 SavedState,乙自建世界與登錄表後以完整驗證 loadState 載入續跑,其後逐幀 state hash 與完整內嵌快照 全等。另有登錄負面測試(未知批次、sha256 不符、同 id 內容不同、就地修改)與世界生命週期測試。
  7. 上限:SYNC_SNAPSHOT_MAX_BYTES 退回 4 MiB,race raw、ingress、verified queue、重組上限、觀戰 wire、 source ingress burst 與 offer 全域配額隨之回到由 4 MiB 推導的值。
  8. 步進核心不變:不改求解、接觸、CCD 任何程式路徑。

後果與影響

正式實作量測(Node 24.15.0;公版三場 v10 × mixed8/heavy8;960 幀、前 60 幀暖機、無輸入;三變體串行交錯, CPU 忙碌 6–14%、同機無其他 node/cargo 程序)。「父代」=wasm cf640ca2 加 D-20261028-01;「D-2」=新核心加登錄、 容差 0.1 mm;「產品」=D-2 加 D-20261028-05(0.2 mm)。

場景 SavedState 最大(父代) SavedState 最大(產品) 觀戰 checkpoint(產品,估) save P50/P95/P99 ms 父代 → D-2 → 產品 每幀實付 P50/P95/P99 ms 父代 → D-2 → 產品 試算/幀 父代 → 產品
snowy-mixed8 2,891,130(68.93%) 838,856(20%) 1,118,775 3.9/4.9/5.3 → 2.4/3.3/4.3 → 2.4/3.2/4.1 28.3/55.1/84.0 → 22.7/44.7/70.1 → 23.1/34.2/49.3 2.264 → 2.078
snowy-heavy8 3,224,175(76.87%) 1,170,176(27.9%) 1,560,535 4.3/5.6/6.1 → 2.9/3.9/5.0 → 3.0/3.9/4.6 35.1/46.6/87.1 → 30.5/42.3/73.9 → 31.3/52.3/60.6 2.056 → 2.038
urban-mixed8 4,150,301(98.95%) 736,357(17.56%) 982,110 4.7/5.8/6.4 → 2.3/3.0/3.6 → 2.2/3.1/3.8 48.4/113.8/178.4 → 40.6/92.2/139.9 → 29.9/54.0/84.1 3.309 → 2.273
urban-heavy8 4,453,009(106.17%) 944,344(22.51%) 1,259,426 5.0/6.3/7.0 → 2.7/3.6/4.5 → 2.3/3.1/3.6 59.4/126.6/156.3 → 49.6/104.9/133.3 → 28.2/52.8/67.0 3.389 → 2.264
jungle-mixed8 3,653,845(87.11%) 783,016(18.67%) 1,044,322 4.6/5.6/6.2 → 2.5/3.4/4.2 → 2.6/4.8/6.0 43.8/107.6/134.1 → 37.2/90.2/119.9 → 38.0/82.8/132.4 2.913 → 2.267
jungle-heavy8 3,778,883(90.1%) 1,073,817(25.6%) 1,432,056 4.7/5.9/6.5 → 2.7/3.7/4.8 → 2.9/3.9/4.5 51.9/122.7/176.9 → 42.3/106.2/169.3 → 43.1/78.7/121.1 2.9 → 2.247
  • 001091 治本:最壞場景由 106.2% 降到 27.9%,六場 17.6–27.9%;觀戰 checkpoint 最大約 1.56 MB(wire 上限 5,657,942 B 的 27.6%)。SYNC_SNAPSHOT_MAX_BYTES 退回 4 MiB;若要收緊,2 MiB 對現行最大值仍約 44% 裕度,但 narrow phase 隨接觸數成長,宜待 UGC 與 8 人極端場景量測後再定。
  • D-2 單獨(容差不變)六場的 save 與每幀實付 P50/P95/P99 全部低於父代:save P50 降 1.4–2.4 ms,實付 P50 降 4.6–9.8 ms;逐幀 state hash 與父代相同(公版三場 mixed8 120 幀),完整內嵌快照逐幀等於父代原生快照。
  • 決定性:Node 六場景 600 幀三向無損(state hash、SavedState、原生快照、完整內嵌快照四條鏈全等);跨行程(兩個 子行程各自的登錄表)載入續跑 45 幀全等;Chromium(151.0.7922.34)snowy-mixed8、urban-heavy8、jungle-mixed8 三向無損與跨頁載入的各鏈與 Node 逐值相同;完整驗證 loadState 每幀載入 300 幀(三場)全等。
  • 登錄成本只在建構期:場地一批 444–867 個形狀,heavy8 八台同款車一批加七次別名。

  • 場地與零件內容不再影響每幀快照大小:UGC 大場地或高面數零件只增加建構期登錄(每批一次 sha256 與 別名全等比對),不增加每幀 bytes。同款零件在還原後共用同一份形狀記憶體。

  • 不變式:登錄的形狀建構後不得就地修改。Arc::make_mut 會因登錄表持有參照而複製,修改後的形狀失去 登錄鍵並改以內嵌寫入,不污染登錄內容;src/physics-engine 有守門測試禁止新增改形狀的呼叫。
  • 安全:外來快照只能引用本機已登錄的批次;引用不存在即原生反序列化失敗;envelope 清單與本機不符在原生 還原前拒絕;參照指到別的序號時登錄鍵不符,退回完整幾何比對。
  • 仍遠離 canon 的 64 KB 與 save ≤4 ms 目標;剩餘 bytes 以 narrow phase 與 broad phase 為主,只能靠 F 案。
  • 範圍:vendor 同代 repair(parent cf640ca2,history parent-001082,命名待確認);新 wasm digest、SDK digest、runtimeVersion 全量替換;src deterministic-world.ts、physics-engine.ts、 system-constants/network/sync.ts、testing/determinism/snapshot-lossless.ts 與新守門測試; conformance physics/track-entity-snapshot 因線上格式改變重基線;canon physics-engine.md、 network-sync.md、interfaces.md、testing.md、spectator.md、程式參數/network.md。
  • pinning:RAPIER_VERSION_CURRENT 需同步新值,另取同意。

已裁決

  1. 範圍 D-2(全部不可變形狀);與 D-20261028-05 同代實作,與 D-20261028-01 獨立、可疊加。
  2. 批次 id 用 sha256,線上取 64 位元前綴,envelope 帶完整 sha256(見決定第 3 點)。
  3. 登錄表生命週期以每次登錄的 token 計,最後一筆釋放時移除整批(見決定第 2、4 點)。
  4. Chromium 已補跑三向無損、跨行程載入與 golden,與 Node 逐鏈相同。
  5. SavedState 維持 v1 原地改;conformance track-entity-snapshot 與本次 vendor repair 同批重基線。
  6. 上限退回 4 MiB;是否再收緊待 UGC 與 8 人極端場景量測後再定。
  7. history parent 命名 parent-001082 待確認;pinning RAPIER_VERSION_CURRENT 同步另取同意。