跳轉到

D-20261028-03|輸入延遲、有界回滾與模擬 Worker

2026-10-29 關聯追補:已簽輸入流已由 D-20261029-01 新增 leave 事件;catch-up reset 時消費端不再重建回合終局狀態。D、R 與其餘協定語意不變。 待決事項 3(時鐘對齊)已由 D-20261029-03 解決;第 6 節「一步內追到前緣」已改為載入後分 tick 追趕,T 的估計加上 front 上下界夾限與多數參考幀;第 9 節驗簽仍在主執行緒,但改為非同步 WebCrypto+noble 複核並依到達順序交付;第 10 節觀戰重求加無進度看門狗;已知限制中 001104、001107、001108 的敘述由該決策取代。D、R 與其餘協定語意不變。 第 7 節「持續落後」已由 D-20261029-04 延伸一條驅逐路徑:catch-up 載入後追趕差距連續 PERSISTENT_LAG_DEMOTE_FRAMES 個正常 step 沒有縮小,即以 'lag' 自我退出;沿用同一常數,不新增常數。

背景與驅動力

issue 001074 的 cost-r22 量測(八車三場、v10 場地、wasm cf640ca2):每幀實付(engine.step 加 saveState)P50 47–92 ms,world step P95 44–135 ms。 改版前的 canon 是 60 Hz 純 rollback、ROLLBACK_FRAME_BUFFER_DEFAULT 8、每幀先存快照再步進,最深回滾一次要載入一份快照並重播 8 幀。 即使把 001074 所列降本全部疊加(含 D-20261028-01 取消正規化),「回滾 8 幀在一個 tick 內做完」在任何組合下都達不到(001074 取捨分析,差 1.2–2.8 倍以上)。

可利用的機制事實:

  • 玩家賽中唯一輸入是技能觸發,沒有轉向(D-20260705-03)。預測是延續 Hold、Event 不重放,所以只有遠端按下或放開技能時才會預測錯。
  • 輸入受理窗:frame < current − R 的 input 直接丟棄。遲到超過 R 的 input 被丟,送方本機卻用了真值,結果是 desync strike。所以 R×T 就是可容忍的單向延遲上限;改版前是 8×16.67=133 ms。
  • 加入 input delay D 之後,本機在 frame c 取樣的輸入排到 frame c+D 才生效,遠端只要在 D×T 內收到就完全不需預測;容忍上限變成 (D+R)×T。

2026-10-27 使用者裁決:8 人同場不退讓;延遲容忍維持單向 133 ms;接受 input delay、技能反應延遲上限 100 ms;回滾為有界頓挫、最深回滾上限 3 幀;採 Web Worker(單執行緒、不用 SharedArrayBuffer),否決多執行緒 Rapier 與權威伺服器;CCD 維持現狀。 2026-10-28 使用者核准 (D, R)=(6, 2)、runtimeStateHash 只在送出時計算、未達效能底線的裝置照常參賽並顯示效能警告、效能常數放 network/ namespace、catch-up 請求逾時不調整、local-test 也套 D。

考慮過的選項

  • 維持純 rollback、R=8:最深回滾一次 8 幀,與即時預算差距最大(否決)。
  • 純 rollback 縮 R(2–4)不加 delay:延遲容忍降到 33–67 ms,跨區對戰遲到 input 會被丟而觸發 desync 驅逐(否決)。
  • 降物理頻率(45/30 Hz):所有幀單位常數、golden、CCD 與偏航研究資產都要重做,且單步成本係數未實測(不在本輪;只在 D-20261028-04 的 L0 仍差 1.5 倍以上時才另案評估)。
  • 減為六車:違反「8 人同場不退讓」(否決)。
  • 多執行緒 Rapier(SAB、COOP/COEP):deterministic build 與 parallel feature 互斥,主站無法設 header(否決)。
  • 權威伺服器或房主權威:違反 P2P 無必要中心定位與回合多簽 anchor(否決)。
  • 固定 input delay D 加有界回滾 R,配合前緣快照與模擬 Worker(採納)。

決定

1. D 與 R

記 T=1000/FIXED_FRAMERATE_HZ ms(60 Hz 時 16.67 ms),延遲容忍 L_tol=133 ms,技能反應延遲上限 L_react=100 ms,回滾深度上限 R_max=3。

條件 式子(整數形式) 60 Hz 結果
容忍不退步 (D+R)×1000 ≥ L_tol×HZ D+R ≥ 8(7.98 進位)
反應上限 D×1000 ≤ L_react×HZ D ≤ 6
頓挫上限 1 ≤ R ≤ R_max R ≤ 3

可行解只有 (6, 2)、(5, 3)、(6, 3) 三組。定案 (D, R)=(6, 2):

  1. 單向延遲 100 ms 以內完全不預測、不回滾,是三組中最寬的無回滾區。
  2. 最深回滾只有 2 幀,對 D-20261028-04 的 L2(最深回滾頓挫 ≤ 3T)留下最多餘裕;R=3 的兩組在降本後估計約 4.1T。
  3. 容忍上限與改版前 R=8 相同(133.3 ms),玩家可連線範圍不退步。
  4. D=6 恰等於 100 ms 上限(6×1000=100×60),以整數式比較不會因浮點誤判。

「技能反應延遲」只指本決策新增、決定性的排程延遲 D×T。輸入取樣到下一個 fixed tick 的等待、Worker 傳遞與渲染延遲改版前就存在,不計入 100 ms;HUD 與文件以「技能在 D 幀後生效」描述,不宣稱端到端延遲。

2. 協定語意

  • 新增 INPUT_DELAY_FRAMES=D,固定常數、不協商、全房同值(與 R 同等級,屬 protocol 綁定常數)。重新引入 D-20260817-03 刪除的常數名,但語意從「文件零值」改為實際控制排程。local-test 同樣套 D。
  • 本機在 local frame c 取樣到的技能事件(含 Hold tick),一律寫入 InputBuffer[local][c+D],並以 frame=c+D 廣播與簽章。簽章 nonce 仍為 frame,單調不變。
  • 每回合 frame 0 到 D−1 沒有任何玩家的輸入:所有 peer 以「空輸入、已確定」處理,不需封包、也不得預測。倒數期間(GO 前)取樣到的事件照舊丟棄,不得補排進這 D 幀。
  • 輸入受理窗:frame < current − R 丟棄(式子不變,R 值改為 2);未來窗 frame > current + STATE_BUFFER_MAX_FRAMES × 2 丟棄(24 幀 > D+R,不變)。
  • 預測規則不變:缺包時延續 Hold、Event 不重放、只記實際取用的預測、同 (peer, frame) 首值鎖定、相異即 equivocation。
  • 回合共識錨 finality 式 latestFinal = floor(max(0, terminalFrame − ROLLBACK_FRAME_BUFFER_DEFAULT) / 120) × 120 形式不變,因為受理窗仍以 R 定義;R 改值使尾窗更靠近終局。
  • ROLLBACK_FRAME_BUFFER_DEFAULT=R=2;ROLLBACK_FRAME_BUFFER_MAX=R_max=3,改為「頓挫上限」語意;ROLLBACK_FRAME_BUFFER_MIN 刪除(原 7/10 界標是為 R=8 設計,新的下界由容忍不變式取代)。
  • 遠端相對本機的進度(lead)一律以排程前的 local frame 計算(input 幀扣 D),背景分頁 24 幀與 900 幀門檻的語意因此不因 +D 排程偏移。

3. 不變式(check:invariants)

  1. 1 ≤ ROLLBACK_FRAME_BUFFER_DEFAULT ≤ ROLLBACK_FRAME_BUFFER_MAX
  2. INPUT_DELAY_FRAMES × 1000 ≤ SKILL_INPUT_DELAY_MAX_MS × FIXED_FRAMERATE_HZ(反應上限,新常數 100)
  3. (INPUT_DELAY_FRAMES + ROLLBACK_FRAME_BUFFER_DEFAULT) × 1000 ≥ NETWORK_ONE_WAY_LATENCY_TOLERANCE_MS × FIXED_FRAMERATE_HZ(容忍下限,新常數 133)
  4. STATE_BUFFER_MAX_FRAMES ≥ ROLLBACK_FRAME_BUFFER_DEFAULT + CATCH_UP_OFFER_ALIGNMENT_FRAMES(保留窗內至少有一個已 final 的對齊候選;12 ≥ 10)
  5. STATE_BUFFER_MAX_FRAMES × 2 > INPUT_DELAY_FRAMES + ROLLBACK_FRAME_BUFFER_DEFAULT(未來窗容得下排程輸入)
  6. SNAPSHOT_INTERVAL_FRAMES % CATCH_UP_OFFER_ALIGNMENT_FRAMES === 0(checksum 與觀戰 checkpoint 幀必為對齊快照幀)
  7. 刪除舊的 CATCH_UP_OFFER_ALIGNMENT_FRAMES === ROLLBACK_FRAME_BUFFER_DEFAULT:對齊間隔維持 8,與 R 脫鉤(它服務的是「current 相差最多 7 幀的 peers 共享一個 aligned frame」,與回滾深度無關)。
  8. 刪除 STATE_BUFFER_MAX_FRAMES ≥ ROLLBACK_FRAME_BUFFER_MAX 的舊形式,由第 4 條取代。

4. StateBuffer 語意:前緣快照取代每幀快照

定義「已確定幀」:frame f 的所有 roster peer 輸入都是真值(或 f < D 的隱含空輸入),且不可能再收進任何 ≤ f 的輸入。

  • 存檔規則:步進 frame c 之前,只有下列情況才 saveState():
  • c 含任何預測輸入,且保留中還沒有 c 本身的快照——含預測的幀一律在該幀存檔;
  • c 是對齊幀(c % CATCH_UP_OFFER_ALIGNMENT_FRAMES === 0),供 catch-up offer、checksum 與觀戰 checkpoint 使用。
  • 不變式:任何可能被回滾的幀 m(c−R ≤ m < c 且 m 含預測)都存在保留快照 s,滿足 c−R ≤ s ≤ m。幀 m 的真值最晚在 current=m+R 到達,此時 s 只能是 m,所以含預測的幀必須自己存檔;只在預測區間開頭存一份、之後間隔補存的寬鬆規則會讓重播超過 R。
  • 回滾:預測錯誤落在 frame m 時,載入 ≤ m 的最近保留快照 s,用本機信任載入(D-20261028-01 的 loadOwnState)後重播 s…c−1。重播長度 ≤ R;s < c−R 視為實作錯誤,fail-closed(onPersistentDesync('window')),需有守門測試。s 之後的舊時間線快照作廢;重播中只在經過對齊幀或仍含預測的前緣幀時重存。
  • 穩態成本:單向延遲 ≤ D×T 時不需前緣快照,每幀實付=engine.step+對齊快照(每 8 幀一次)+checksum 幀的 hash。延遲落在 D×T 與 (D+R)×T 之間時,最壞約每幀一次 save,重播還會重存:單向延遲 D+2 幀時實測每 240 幀約 255 次 save。save 已因 D-20261028-01 降為數毫秒。
  • STATE_BUFFER_MAX_FRAMES(12)改為「保留時窗(幀)」語意:只保留距最近一次寫入 12 幀內的稀疏快照,不再保證每幀都有。catch-up offer 只取已 final 的對齊幀(a ≤ c−R,即其前置輸入都已不可再改),避免把含預測的快照當候選。

5. checksum、錨與觀戰 checkpoint 只取已確定狀態

checksum 幀 f(120 的倍數)步進時只記暫定 hash;回滾重播經過 f 時以重播後狀態覆寫;f 成為已確定幀後才轉為本地票並廣播。 此規則對改版前的 R=8 同樣成立,已由 issue 001092 先行落地。

引擎另外依幀序發出「已確定步進」串流:每幀恰一筆、在該幀已確定後才發出,回滾重播覆寫尚未發出的記錄;catch-up 先發重置事件(載入幀 F),之後由 F+1 續發。消費端一律只吃這條串流:

  • 回合共識錨只記已確定 checksum 幀的 hash;catch-up 重置時補記載入幀 F 的錨(F 是已驗證的對齊幀)。
  • 觀戰 checkpoint 只取已確定的對齊快照,不在 live 步另外 saveState。
  • 回合終局(完賽、時限、無存活車終局窗)以已確定幀判定:預測中的衝線或時限不會結束回合,終局延後到終局幀已確定。

6. catch-up 與背景分頁

  • offer 候選取保留窗內已 final 的對齊快照,最多兩個;剛過對齊幀後的 R 幀內只有一個合格候選(不變式 4 保證至少一個)。其餘 quorum、TTL、byte 配額、cooldown 不變。offer 階段預留最壞輸入日誌的空間:可 offer 的快照上限=SYNC_SNAPSHOT_MAX_BYTES 減最壞輸入日誌 bytes(由既有常數推導),送不出去的快照不參與 quorum。
  • sync-response 除快照外另帶 provider 的簽章輸入日誌:每 peer 在 F 之前最後一筆真值(預測延續基準),加上 [F, provider 目前幀) 的全部已受理真值;每筆保留原發送者的 input 簽章,落後端逐筆驗簽,任一筆不合格整份日誌不採用並改向下一位 voter。
  • 落後端載入 F 後以日誌(F 之前的延續基準與 [F, front − R) 以日誌為唯一權威)加上本機已持有的較新真值重播,追到前緣 T=provider front+估計單程延遲(RTT/2),之後本機輸入排程從 T+D 恢復。同 (peer, frame) 的已簽值互相矛盾視為 equivocation,與 live 路徑同處置。
  • 八人 mesh 驗證:一人凍結斷線 300 tick 後 catch-up,對齊當下 live peers 已超過 F+D+R;單向延遲 1–4 幀時對齊後 1800 幀內每輪 checksum 八票逐位元一致、恢復端輸入逐幀被採用。草稿中「[F, F+D) 一律空輸入」的規則在同一情境出現反例(live 已以最後真值延續 Hold 定案),不採用。
  • 已知限制:日誌只來自單一 provider、沒有 quorum 背書,provider 刻意漏筆時分歧只落在恢復端,最後由 checksum strike 自我驅逐(issue 001107);最壞情況(最大日誌、最長追趕)在單一步內完成會卡頓數秒(issue 001108);RTT/2 估計在非對稱延遲下有誤差,單向延遲 1–8 幀時 12 個 seed 中 7 個在 catch-up 後分歧(issue 001104)。
  • 背景分頁 24 幀門檻、900 幀前跳上限、2 人場自我驅逐規則不變。

7. 持續落後自我驅逐

賽中持續跟不上的 peer 由自己退出,不拖住全房:

  • 每個本機幀以遠端最近觀測進度計算 lead(已扣 D)。嚴格過半遠端、且至少兩名遠端的 lead > R,連續 PERSISTENT_LAG_DEMOTE_FRAMES(300 幀、5 秒)成立,即自我驅逐;任一幀不成立計數歸零。
  • 兩人場只有一名遠端,永不觸發(lead 來自對端自簽幀號,單一 peer 不得逼退對手)。
  • catch-up 已進入快照階段時暫停計數;probe 階段照常計數,但達門檻時延後到 probe 結果,失敗才驅逐,成功則進 catch-up、對齊後歸零。
  • 結束原因為 lag-evicted:沿用驅逐收攤(離場、記斷線),但不自報 DesyncEvent、不計入 desync 統計。結束畫面提示可回房以觀戰者身分觀看,不自動重新加入。
  • PERSISTENT_LAG_DEMOTE_FRAMES 屬本機處置政策常數,非協定。

8. 本機先行回饋(不影響共識)

技能按下的當下,主執行緒可以立即給「已受理」回饋:技能鈕按下視覺與 Hold 期間的按住狀態。

禁止預演物理結果:不得提前扣電量、播技能效果音或顯示技能效果;這些仍由 D 幀後的模擬輸出推導。回饋不進 InputEvent、wire、快照或 hash,也不違反「Skill 按鈕無 cooldown timer、無 disabled 預警」(賽內機制.md 第 2.4 節)。

9. 模擬 Worker(單執行緒、無 SharedArrayBuffer)

比賽、local-test 與觀戰 replay 的模擬改在 dedicated Web Worker 內執行;一個 Worker 只承載比賽回合或觀戰 replay 其中之一:

執行緒 職責
主執行緒 DOM/Angular UI、three.js 渲染與 renderAlpha 插值、輸入取樣、音訊、本機先行回饋;WebRTC RTCPeerConnection 與 DataChannel、wire encode/decode、Race Sign Key 簽章與驗簽、per-peer ingress token bucket;GLB fetch;visibilitychange 與生命週期事件
模擬 Worker Rapier WASM 與 DeterministicWorld、PhysicsEngine、world-builder、RollbackEngine、InputBuffer、StateBuffer、ChecksumExchanger、固定 timestep 迴圈(以 performance.now() 累加,不依賴 rAF);觀戰 replay world 與 replay 的 checkpoint 簽章、checksum 驗證
  • DataChannel 留在主執行緒:RTCDataChannel 在 Worker 中不可攜,且驗簽與 token bucket 已在 transport 層完成。主執行緒把已驗證的 NetworkMessage 依到達順序交給 Worker;Worker 以佇列實作 NetworkLayer.drain()。
  • 場地與車輛 GLB 由主執行緒 fetch,複製成私有 ArrayBuffer 後以 Transferable 轉移。catch-up 快照、settled checkpoint 與觀戰 checkpoint 以結構化複製傳遞、不 transfer。不使用 SharedArrayBuffer,因此不需要 COOP/COEP。
  • Worker 內的 Draco 解碼使用建置時生成的 ESM 包裝(上游解碼器原樣複製加 default export,生成器拒收含 eval、Function 或 importScripts 的來源)。
  • NetworkLayer.broadcast 在 Worker 端改為「已交給主執行緒橋接佇列」;真正交付 DataChannel 的結果(sync-response 是否送出、是否進 cooldown、是否刪 offer)由主執行緒以非同步回執通知 Worker,回執在下一步開頭套用。
  • 主執行緒每步收到 Worker 的 render frame 與 HUD 推導所需資料;HUD 與音訊路由仍在主執行緒消費,不回寫模擬。
  • 背景分頁:Worker 的計時器在背景不受節流,visibilitychange 必須由主執行緒主動轉送;local-test 以「切背景=暫停」為語意。迴圈有補步上限。
  • 環境沒有 Worker 時,同一份模擬核心改在主執行緒承載(行為相同,只失去執行緒隔離)。
  • 決定性不受影響:同一份 WASM、單執行緒、相同輸入序列;Worker 只改變「何時」執行,不改變「執行什麼」。Worker 與主執行緒 120 幀 hash 逐位元相同(Chromium、Firefox);golden 與 conformance 不需重基線。
  • 效益:回滾與 bounded-step 尖峰變成模擬落後,不再凍結畫面;模擬可獨占一個核心。它不降低單步成本,所以不取代 D-20261028-04 的預算。
  • 安全:GitHub Pages 無法替 Worker 腳本送 CSP header,meta CSP 只約束主文件與 Worker 的建立,Worker 本身沒有 CSP;因此 Worker 只載入本站同源模組,生成器的禁用規則與上游逐位元比對是 Worker 內動態程式碼的防線。不可信 GLB 解析仍只走 Sanitize Worker,模擬 Worker 只接受已 admission 的資產。

10. 觀戰 replay 失配重求

觀戰 replay checksum 失配時重求 checkpoint:允許連續重求 SPECTATOR_REPLAY_RESYNC_LIMIT(3)次,第 4 次連續失配才降級為精簡 fallback;成功對帳一次即歸零。來源端對已在 replay 的觀戰者再次選 replay 時,只在有更新的 checkpoint 時重送(每錨每觀戰者至多一次);觀戰端等待 checkpoint 超過 2×SNAPSHOT_INTERVAL_FRAMES 的時間仍未收到可接受者,計為一次失敗重求。

後果與影響

  • 延遲容忍不變(133 ms),單向 ≤ 100 ms 的對局不再有回滾;100–133 ms 區間最深回滾 2 幀。超過 133 ms 的 input 仍被丟並累積 desync strike。
  • 玩家技能生效固定晚 100 ms(全員同值、公平)。依賴精準時機的技能(跳躍、boost)手感會變,需 playtest 確認;本機先行回饋只緩解感知,不改生效時間。
  • pre_launch:wire 格式除 sync-response 新增輸入日誌外不變(input frame 欄位語意改為排程幀),protocol 常數改值依 專案生命週期.md 直接採用、版本維持 1,不建相容層。
  • 回合共識錨尾窗因 R 改值而移動;pinning vendor 的同式常數與錨模組需另取使用者同意後同步 vendor,否則 pinning 對 anchor 尾窗的判斷與 client 不同。
  • 與既有決策的關係:
  • amends D-20260817-03:撤回「純 rollback 不另加 input delay」與刪除 INPUT_DELAY_FRAMES,改為固定 D;該 ADR 其餘部分不變。
  • amends D-20260812-02:失配重載改為有上限的重求、超限降級;replay 改在 Worker 執行、checkpoint 只取已確定狀態。2–6 幀 jitter buffer 不變。
  • 不 amend D-20260810-03:finality 以受理窗定義的形式不變,只有常數值變。
  • 依賴 D-20261028-01 的 loadOwnState 與廉價 saveState。
  • 程式架構第 6.6 節「物理必然單執行緒」維持;Worker 不違反,只補充執行緒配置。
  • 效能預算的三層口徑、量測情境與效能警告見 D-20261028-04;本決策只定 D、R 與執行架構。
  • 現況規格:賽內機制.md 第 2 節、流程/比賽進行.md、network-sync、spectator、security 第 3 節、程式參數/network.md 第 8 節。

待決事項

  1. (D, R):已定 (6, 2)。
  2. 未達 D-20261028-04 L0 的裝置:已定照常參賽並顯示效能警告。
  3. 時鐘對齊與 frame-advantage 節流:另案,issue 001104(peer 間幀時鐘偏移與 catch-up 的 RTT/2 估計誤差都會吃掉 (D+R)×T 的容忍)。
  4. 本機先行回饋是否新增 UI cue 音訊槽位:不在本批。
  5. catch-up 後的本機輸入規則:已定為快照加簽章輸入日誌、追前緣後自 T+D 恢復(第 6 項),八人 mesh 驗證見上。
  6. checksum 假陽性:已由 issue 001092 處理。
  7. Worker 在 GitHub Pages 下的 CSP、WASM 編譯與計時:已驗證——Chromium 與 Firefox 的 Worker 內 WASM 編譯、hash 逐位元相同、前景計時皆通過,Chromium 背景分頁不節流;Worker 無 CSP(第 9 項)。Firefox 背景節流、WebKit/Safari 為殘留證據。
  8. 與 bounded-step 放寬、靜態場地參照研究的相依:保留;兩者只影響每步成本與快照 bytes,不改 D、R,但 L2 是否達標取決於它們。