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}