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 的大宗。
考慮過的選項¶
- A:只調高上限(並同步 race message、觀戰 wire、offer cache 與全域配額)。曾為短期措施;不解決 記憶體、頻寬與 StateBuffer 拷貝,新增場地內容會再次逼近。
- D-1:僅場地形狀登錄參照。
- D-2:所有不可變形狀登錄參照(場地、車輛零件、持彈、場地實體中建構期就存在、之後不改形狀的 trimesh/凸包)。本決定。
- D':連同 broad phase BVH 與 narrow phase 的場地部分一併參照化或 restore 時重建。否決:BVH 樹形與 pair 順序是步進結果的一部分,重建不保證逐位元相同;其 bytes 屬動態狀態,參照化也省不了。
- E:差分快照。否決為本輪方案:catch-up、觀戰 checkpoint 與原生 restore 仍需完整 bytes,差分需要 共同基準與重組成本,對單包上限無直接幫助;日後若要做應建立在本決定之上。
- F:vendor 自訂最小動態序列化。唯一可能接近 canon 的 64 KB 目標,但要證明 broad phase、narrow phase、island 與 solver 內部順序重建後逐位元相同,工作量與風險最高;留待 001090 效能目標重訂後評估。
- 既有研究碼
ResearchShapeTable(指標序號、owner token)。只在單一行程、單一交易內有效,不能跨 peer;不採用,本決定沿用其「thread-local 登錄+序列化時查表」的形式。
決定¶
- vendor(parry witness
SharedShapeserde):形狀線上格式為兩分支。內嵌分支維持上游TypedShape的原位元組;Arc位址在不可變形狀登錄表內的形狀寫TypedShape第 20 號 variantO4Registered(批次 id u64, 序號 u32)(16 B),排在全部上游 variant 之後。反序列化參照時從登錄表取 該批該序號的同一個Arc;批次或序號不存在即整份反序列化失敗(World.restoreSnapshot回傳 null)。 未登錄任何形狀時,快照與父代逐位元相同。 - 登錄表:每個 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。 - 批次 id:sha256。由 TS 對
coStaticShapeWireBytes的輸出計算(vendor 不新增雜湊依賴)。線上參照 取 sha256 的前 64 位元,SavedState envelope 另帶完整 sha256。選 64 位元前綴而不用 envelope 表序號: 原生快照自足、不需每次存取時傳入對映表,參照 16 B;前綴碰撞只能在同一個 wasm instance 內兩批不同 內容之間發生,登錄端以全等比對拒絕(決定性失敗,所有 peer 相同),載入端以完整 sha256 核對 envelope。 - TS(deterministic-world):建構期
recordImmutableBodyColliderTopology之後,把本次新增的不可變 collider 中 trimesh/凸包形狀依 handle 升冪登錄為一批(場地一批、每台車一批;同款車內容相同即成為 別名)。外來快照的完整驗證不再讀回幾何:collider 的登錄鍵等於建構期記錄的登錄鍵即通過,鍵不符 (改寫為內嵌或指到別的序號)退回完整幾何比對;其餘拓樸、質量、joint、runtime hash 驗證不變。DeterministicWorld.destroy以 token 逐一釋放本世界的登錄,編輯器、驗證用拋棄式世界與比賽世界並存時 各自持有自己的登錄,最後一個使用該批的世界銷毀時才移除。 - SavedState:envelope 新增
staticShapeBatches,依首次登錄順序列出本世界引用的批次[批次 id(sha256 前 8 bytes), sha256];載入端(loadState、savedStateFrame、loadOwnState)先核對 清單與本機世界登錄的批次逐位元全等,才交給原生 restore。維持 SavedState v1 原地改,不升號:pre_launch 沒有已發布的 v1 資料需辨識,新舊核心以engineVersion(wasm digest)區分,envelope 的鍵集合核對也會 拒收舊格式。 - 決定性守門:沿用 D-20261028-01 的三向無損測試,並追加兩項:(a) 參照模式下每幀另以完整內嵌範圍取得
原生快照,逐幀 sha256 在不中斷、每幀 save→load、間隔 load 三種跑法間全等;(b) 跨登錄表:甲輸出
SavedState,乙自建世界與登錄表後以完整驗證
loadState載入續跑,其後逐幀 state hash 與完整內嵌快照 全等。另有登錄負面測試(未知批次、sha256 不符、同 id 內容不同、就地修改)與世界生命週期測試。 - 上限:
SYNC_SNAPSHOT_MAX_BYTES退回 4 MiB,race raw、ingress、verified queue、重組上限、觀戰 wire、 source ingress burst 與 offer 全域配額隨之回到由 4 MiB 推導的值。 - 步進核心不變:不改求解、接觸、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 全量替換;srcdeterministic-world.ts、physics-engine.ts、system-constants/network/sync.ts、testing/determinism/snapshot-lossless.ts與新守門測試; conformancephysics/track-entity-snapshot因線上格式改變重基線;canonphysics-engine.md、network-sync.md、interfaces.md、testing.md、spectator.md、程式參數/network.md。 - pinning:
RAPIER_VERSION_CURRENT需同步新值,另取同意。
已裁決¶
- 範圍 D-2(全部不可變形狀);與 D-20261028-05 同代實作,與 D-20261028-01 獨立、可疊加。
- 批次 id 用 sha256,線上取 64 位元前綴,envelope 帶完整 sha256(見決定第 3 點)。
- 登錄表生命週期以每次登錄的 token 計,最後一筆釋放時移除整批(見決定第 2、4 點)。
- Chromium 已補跑三向無損、跨行程載入與 golden,與 Node 逐鏈相同。
- SavedState 維持 v1 原地改;conformance
track-entity-snapshot與本次 vendor repair 同批重基線。 - 上限退回 4 MiB;是否再收緊待 UGC 與 8 人極端場景量測後再定。
- history parent 命名
parent-001082待確認;pinningRAPIER_VERSION_CURRENT同步另取同意。