nx_netquant.nx source
↩ module page · 112 lines · 5145 B
1// nx_netquant.nx -- sovereign BIT-LEVEL quantized + delta player-state codec for adverse
2// networks (the "girl on a bad mobile network plays seamlessly with a boy in Texas" problem).
3// Composes nx_bitio (MSB-first bit writer/reader). 100% nx, no syscalls -> wasm-friendly AND
4// daemon-reusable. NO logic ever leaves for JS -- the host only moves the resulting bytes.
5//
6// Two frame kinds (the keyframe+delta model, like video I/P frames or Quake3/Source snapshots):
7// FULL (keyframe): every field at minimal fixed width -> 88 bits = 11 bytes (raw was 28).
8// Lossless for in-range values (positions are integer voxels; yaw stored
9// mod 360 in 9 bits; pitch +bias in 8). Sent on join / periodic resync.
10// DELTA (vs a baseline): a 10-bit change-mask + zig-zag VARINT for each changed field.
11// Stationary peer -> ~2 bytes; a small walk -> a few bytes. This is the
12// low-bandwidth win that keeps the bad-network player smooth.
13//
14// Fields f[0..9] = x,y,z,yaw,pitch, species,caught,action,anim, flags
15// x,z: signed voxel (bias +131072, 18 bits each => +/-131071 lossless)
16// y: 0..4095 (12 bits) yaw: 0..359 mod 360 (9 bits) pitch: -85..85 (+128, 8 bits)
17// species 3b, caught 7b, action 3b, anim 6b, flags 4b
18// license_tier: ORIGINAL
19import "nx_bitio.nx"
20
21const NQ_NF: i64 = 10
22const NQ_BIAS: i64 = 131072 // position bias (2^17) -> 18-bit unsigned field
23const NQ_FULL_BITS: i64 = 88 // 18+12+18+9+8+3+7+3+6+4
24
25// zig-zag map signed<->unsigned (small magnitudes -> small codes) -- no reliance on >> sign
26func nq_zz(v: i64) -> i64 { if v < 0 { return ((0 - v) << 1) - 1 } return v << 1 }
27func nq_unzz(z: i64) -> i64 { if (z & 1) == 1 { return 0 - ((z + 1) >> 1) } return z >> 1 }
28// minimal bits to hold z (z>=0); 0 for zero
29func nq_bitlen(z: i64) -> i64 { var n: i64 = 0; var x: i64 = z; while x > 0 { n = n + 1; x = x >> 1 } return n }
30
31// VARINT over the bitstream: 5-bit length prefix + that many bits of zig-zag value.
32func nq_vput(buf: *u8, bp: i64, v: i64) -> i64 {
33 let z: i64 = nq_zz(v)
34 let nb: i64 = nq_bitlen(z)
35 var p: i64 = nx_bw_put(buf, bp, nb, 5)
36 if nb > 0 { p = nx_bw_put(buf, p, z, nb) }
37 return p
38}
39// bits a varint at bp occupies (5 + its length field) -- lets the caller advance the cursor
40func nq_vlen(buf: *u8, bp: i64) -> i64 { return 5 + nx_br_get(buf, bp, 5) }
41// value of a varint at bp
42func nq_vval(buf: *u8, bp: i64) -> i64 {
43 let nb: i64 = nx_br_get(buf, bp, 5)
44 var z: i64 = 0
45 if nb > 0 { z = nx_br_get(buf, bp + 5, nb) }
46 return nq_unzz(z)
47}
48
49// normalize yaw to [0,360)
50func nq_yawn(yaw: i64) -> i64 { return ((yaw % 360) + 360) % 360 }
51
52// FULL keyframe pack -> returns BIT count (NQ_FULL_BITS).
53func nq_pack_full(buf: *u8, f: *i64) -> i64 {
54 var bp: i64 = 0
55 bp = nx_bw_put(buf, bp, f[0] + NQ_BIAS, 18) // x
56 bp = nx_bw_put(buf, bp, f[1], 12) // y
57 bp = nx_bw_put(buf, bp, f[2] + NQ_BIAS, 18) // z
58 bp = nx_bw_put(buf, bp, nq_yawn(f[3]), 9) // yaw 0..359
59 bp = nx_bw_put(buf, bp, f[4] + 128, 8) // pitch +bias
60 bp = nx_bw_put(buf, bp, f[5], 3) // species
61 bp = nx_bw_put(buf, bp, f[6], 7) // caught
62 bp = nx_bw_put(buf, bp, f[7], 3) // action
63 bp = nx_bw_put(buf, bp, f[8] & 63, 6) // anim (low 6)
64 bp = nx_bw_put(buf, bp, f[9], 4) // flags
65 return bp
66}
67// FULL unpack -> f[0..9]; returns bits consumed.
68func nq_unpack_full(buf: *u8, f: *i64) -> i64 {
69 var bp: i64 = 0
70 f[0] = nx_br_get(buf, bp, 18) - NQ_BIAS; bp = bp + 18
71 f[1] = nx_br_get(buf, bp, 12); bp = bp + 12
72 f[2] = nx_br_get(buf, bp, 18) - NQ_BIAS; bp = bp + 18
73 f[3] = nx_br_get(buf, bp, 9); bp = bp + 9
74 f[4] = nx_br_get(buf, bp, 8) - 128; bp = bp + 8
75 f[5] = nx_br_get(buf, bp, 3); bp = bp + 3
76 f[6] = nx_br_get(buf, bp, 7); bp = bp + 7
77 f[7] = nx_br_get(buf, bp, 3); bp = bp + 3
78 f[8] = nx_br_get(buf, bp, 6); bp = bp + 6
79 f[9] = nx_br_get(buf, bp, 4); bp = bp + 4
80 return bp
81}
82
83// DELTA pack vs baseline -> returns BIT count. 10-bit change-mask then varint per changed field.
84func nq_pack_delta(buf: *u8, base: *i64, cur: *i64) -> i64 {
85 var mask: i64 = 0
86 var i: i64 = 0
87 while i < NQ_NF { if cur[i] != base[i] { mask = mask | (1 << i) } i = i + 1 }
88 var bp: i64 = nx_bw_put(buf, 0, mask, NQ_NF)
89 i = 0
90 while i < NQ_NF {
91 if (mask & (1 << i)) != 0 { bp = nq_vput(buf, bp, cur[i] - base[i]) }
92 i = i + 1
93 }
94 return bp
95}
96// DELTA unpack: apply onto baseline -> out[0..9]; returns bits consumed.
97func nq_unpack_delta(buf: *u8, base: *i64, out: *i64) -> i64 {
98 let mask: i64 = nx_br_get(buf, 0, NQ_NF)
99 var bp: i64 = NQ_NF
100 var i: i64 = 0
101 while i < NQ_NF {
102 if (mask & (1 << i)) != 0 {
103 let d: i64 = nq_vval(buf, bp)
104 bp = bp + nq_vlen(buf, bp)
105 out[i] = base[i] + d
106 } else {
107 out[i] = base[i]
108 }
109 i = i + 1
110 }
111 return bp
112}