code wiki / (root) / nx_interp.nx

nx_interp.nx source

↩ module page · 147 lines · 6200 B

1// nx_interp.nx -- sovereign snapshot INTERPOLATION buffer: the core trick that makes a peer on a 2// jittery/lossy link look smooth (render each remote peer slightly IN THE PAST and interpolate between 3// buffered snapshots -- the Valve/Source "interp" lag-comp model). Absorbs jitter (variable arrival), 4// hides loss (bracketing samples still exist), and tolerates reordering (sorted insert). 100% nx, no 5// syscalls -> wasm-friendly. The host only delivers raw bytes; ALL timing/interp logic is here. 6// 7// Per-peer history: a time-sorted ring of up to K snapshots. Layout (flat *i64): 8// hist[0] = count 9// entry i at base = 1 + i*IP_STRIDE: [time_ms, f0..f9] (f = x,y,z,yaw,pitch,species,caught,action,anim,flags) 10// Spatial fields (0..4) are LERPed between brackets (yaw=3 shortest-arc); enum fields (5..9) are 11// step-held to the earlier bracket (no nonsense half-states). license_tier: ORIGINAL 12 13const IP_STRIDE: i64 = 11 // time + 10 fields 14 15func ip_init(hist: *i64) -> i64 { hist[0] = 0; return 0 } 16func ip_count(hist: *i64) -> i64 { return hist[0] } 17func ip_time(hist: *i64, i: i64) -> i64 { return hist[1 + i*IP_STRIDE] } 18func ip_field(hist: *i64, i: i64, j: i64) -> i64 { return hist[1 + i*IP_STRIDE + 1 + j] } 19 20func ip_entset(hist: *i64, idx: i64, time: i64, f: *i64) -> i64 { 21 let base: i64 = 1 + idx*IP_STRIDE 22 hist[base] = time 23 var j: i64 = 0 24 while j < 10 { hist[base + 1 + j] = f[j]; j = j + 1 } 25 return 0 26} 27func ip_entswap(hist: *i64, a: i64, b: i64) -> i64 { 28 let ba: i64 = 1 + a*IP_STRIDE 29 let bb: i64 = 1 + b*IP_STRIDE 30 var j: i64 = 0 31 while j < IP_STRIDE { let t: i64 = hist[ba+j]; hist[ba+j] = hist[bb+j]; hist[bb+j] = t; j = j + 1 } 32 return 0 33} 34// drop the oldest (index 0), shifting [1..c-1] -> [0..c-2] 35func ip_shiftdrop(hist: *i64, c: i64) -> i64 { 36 var i: i64 = 0 37 while i < c - 1 { 38 let bd: i64 = 1 + i*IP_STRIDE 39 let bs: i64 = 1 + (i+1)*IP_STRIDE 40 var j: i64 = 0 41 while j < IP_STRIDE { hist[bd+j] = hist[bs+j]; j = j + 1 } 42 i = i + 1 43 } 44 return 0 45} 46// push a snapshot; keeps the buffer time-sorted (insertion) + capped at K (drop oldest). Reorder-safe. 47func ip_push(hist: *i64, K: i64, time: i64, f: *i64) -> i64 { 48 var c: i64 = hist[0] 49 if c >= K { ip_shiftdrop(hist, c); c = c - 1 } 50 ip_entset(hist, c, time, f) 51 c = c + 1 52 hist[0] = c 53 var i: i64 = c - 1 // bubble the new entry left into sorted position 54 var go: i64 = 1 55 while go == 1 { 56 if i == 0 { go = 0 } else { 57 if ip_time(hist, i-1) > ip_time(hist, i) { ip_entswap(hist, i-1, i); i = i - 1 } else { go = 0 } 58 } 59 } 60 return 0 61} 62// shortest-arc angle interpolation (handles 350->10 through 0, not the long way) 63func ip_lerp_angle(va: i64, vb: i64, num: i64, span: i64) -> i64 { 64 var d: i64 = (vb - va) % 360 65 if d > 180 { d = d - 360 } 66 if d < 0 - 180 { d = d + 360 } 67 var r: i64 = va + d * num / span 68 r = ((r % 360) + 360) % 360 69 return r 70} 71// sample the peer's pose at render-time rt into out[0..9]. 72// returns: 0 = no data (caller holds prior); 1 = interpolated; 2 = clamped before first; 3 = at/after last (hold; extrapolate is NET-R2) 73func ip_sample(hist: *i64, rt: i64, out: *i64) -> i64 { 74 let c: i64 = hist[0] 75 if c == 0 { return 0 } 76 if rt <= ip_time(hist, 0) { 77 var j: i64 = 0; while j < 10 { out[j] = ip_field(hist, 0, j); j = j + 1 } 78 return 2 79 } 80 let last: i64 = c - 1 81 if rt >= ip_time(hist, last) { 82 var j: i64 = 0; while j < 10 { out[j] = ip_field(hist, last, j); j = j + 1 } 83 return 3 84 } 85 var a: i64 = 0 86 var i: i64 = 0 87 while i < c { if ip_time(hist, i) <= rt { a = i } i = i + 1 } // last index with time <= rt 88 let b: i64 = a + 1 89 let ta: i64 = ip_time(hist, a) 90 let tb: i64 = ip_time(hist, b) 91 let span: i64 = tb - ta 92 let num: i64 = rt - ta 93 var j: i64 = 0 94 while j < 10 { 95 let va: i64 = ip_field(hist, a, j) 96 let vb: i64 = ip_field(hist, b, j) 97 if j <= 4 { 98 if j == 3 { out[j] = ip_lerp_angle(va, vb, num, span) } 99 else { out[j] = va + (vb - va) * num / span } 100 } else { 101 out[j] = va 102 } 103 j = j + 1 104 } 105 return 1 106} 107// NET-R2 dead-reckoning: when rt runs PAST the newest snapshot (a loss burst, or the playout delay is 108// too small), keep the peer MOVING by extrapolating from the last two snapshots' velocity, instead of 109// freezing. Capped at max_extrap_ms horizon so long loss HOLDS (bounded) rather than diverging wildly. 110// Delegates to ip_sample for the interior/before cases. Returns 4 = extrapolated (else ip_sample's codes). 111func ip_sample_ex(hist: *i64, rt: i64, max_extrap_ms: i64, out: *i64) -> i64 { 112 let c: i64 = hist[0] 113 if c == 0 { return 0 } 114 let last: i64 = c - 1 115 let tl: i64 = ip_time(hist, last) 116 if rt < tl { return ip_sample(hist, rt, out) } // interior or before-first -> normal path 117 let dt: i64 = rt - tl 118 if c >= 2 { 119 let prev: i64 = last - 1 120 let span: i64 = tl - ip_time(hist, prev) 121 if span > 0 { 122 if dt <= max_extrap_ms { 123 var j: i64 = 0 124 while j < 10 { 125 let vl: i64 = ip_field(hist, last, j) 126 if j <= 4 { 127 let vp: i64 = ip_field(hist, prev, j) 128 if j == 3 { 129 var dd: i64 = (vl - vp) % 360 130 if dd > 180 { dd = dd - 360 } 131 if dd < 0 - 180 { dd = dd + 360 } 132 var r: i64 = vl + dd * dt / span 133 r = ((r % 360) + 360) % 360 134 out[j] = r 135 } else { 136 out[j] = vl + (vl - vp) * dt / span // linear dead-reckoning 137 } 138 } else { out[j] = vl } 139 j = j + 1 140 } 141 return 4 142 } 143 } 144 } 145 var j: i64 = 0; while j < 10 { out[j] = ip_field(hist, last, j); j = j + 1 } // beyond horizon / no velocity -> hold (bounded) 146 return 3 147}