code wiki / (root) / nx_netquant.nx

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}