code wiki / (root) / nx_fx.nx

nx_fx.nx source

↩ module page · 255 lines · 9507 B

1// fx.nx -- deterministic Q16.16 fixed-point math. 2// 3// Why fixed-point and not IEEE-754 float: 4// Rollback netcode (GGPO 2006, Skullgirls, Killer Instinct) requires 5// bit-identical simulation across machines. IEEE-754 + Math.sin 6// cannot deliver this -- different JS engines (V8, SpiderMonkey, 7// JavaScriptCore) ship different sin/cos implementations, and even 8// addition order can diverge under JIT inlining. Fixed-point 9// integer math is the only way to guarantee "same input → same bits 10// on every machine." 11// 12// Representation: Q16.16 13// - 64-bit signed integer: high 16 bits integer part, low 16 bits 14// fractional part. Negative values use two's complement. 15// - Range: ±32767.99998... blocks. Precision: 1/65536 ≈ 15 μm at 16// 1 m = 1 block. Adequate for voxel games, physics, gameplay 17// sim up to ~32 km arenas. 18// 19// Invariants (enforced, not hoped): 20// FX1 No IEEE-754 anywhere. NishiLang has no float type today, so 21// this is trivial -- but if floats ever land, fx.nx does not 22// use them. 23// FX2 fx_mul and fx_div round toward zero (arithmetic shift, not 24// round-to-nearest). Deterministic and simple; callers needing 25// round-to-nearest do `(a + (b >> 1)) / b` explicitly. 26// FX3 sin/cos via CORDIC algorithm -- 16 iterations, ~Q16.16 27// precision, no Math.sin dependency. Converges through a 28// precomputed atan table. Every implementation (NishiVM, 29// native C VM, future Nishi silicon) produces bit-identical 30// results by construction. 31// FX4 Overflow in fx_mul is possible if both operands exceed 16-bit 32// integer range. Callers are responsible for clamping; we do 33// NOT implement saturation arithmetic by default (silent 34// saturation masks bugs; explicit clamp is better). 35// 36// References: 37// - Volder 1959, "The CORDIC Trigonometric Computing Technique" 38// - Kota-Kuroda-Shimamura 1989, "A High-Speed Fixed-Point Multiplier 39// Using Carry-Propagation-Free Adders" 40// - GGPO 2006 whitepaper (rollback determinism rationale) 41 42// nx_safety_envelope: 43// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 44// sil_target: SIL1 45// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 46// verdict: NOT_YET_EVALUATED 47 48import "nx_syscalls.nx" 49const FX_MAGIC_51472: i64 = 51472 50const FX_MAGIC_30386: i64 = 30386 51const FX_MAGIC_16055: i64 = 16055 52const FX_MAGIC_8150: i64 = 8150 53const FX_MAGIC_4091: i64 = 4091 54const FX_MAGIC_2047: i64 = 2047 55const FX_MAGIC_1024: i64 = 1024 56 57// ---- scale knobs --------------------------------------------------- 58 59const FX_SHIFT: i64 = 16 60const FX_ONE: i64 = 65536 // 2^16 61const FX_HALF: i64 = 32768 // 0.5 in Q16.16 62const FX_FRAC_MASK: i64 = 0xFFFF // low 16 bits 63const FX_TWO_PI: i64 = 411775 // round(2π * 65536); 1 rev in Q16.16 64const FX_PI: i64 = 205887 // round(π * 65536) 65const FX_HALF_PI: i64 = 102944 // round(π/2 * 65536) 66 67// CORDIC scaling factor K_n for n=16 iterations: ≈ 0.6072529350088814 68// Stored in Q16.16 = round(0.6072529 * 65536) = 39797. 69const FX_CORDIC_K: i64 = 39797 70 71// ---- basic arithmetic --------------------------------------------- 72 73// Addition / subtraction are plain integer ops -- the same-format 74// operands, same-format result invariant means no scaling needed. 75func fx_add(a: i64, b: i64) -> i64 { return a + b } 76func fx_sub(a: i64, b: i64) -> i64 { return a - b } 77func fx_neg(a: i64) -> i64 { return 0 - a } 78 79// Q16.16 * Q16.16 = Q32.32; we shift right by 16 to get Q16.16 back. 80// Careful: intermediate product can overflow i64 if a and b both 81// exceed ~ Q16.0 range. FX4 responsibility. 82func fx_mul(a: i64, b: i64) -> i64 { 83 return (a * b) >> FX_SHIFT 84} 85 86// Q16.16 / Q16.16 = Q16.16. Shift LHS up by 16 before dividing so 87// the result carries the correct fractional bits. Does integer 88// truncation toward zero (FX2). 89func fx_div(a: i64, b: i64) -> i64 { 90 return (a << FX_SHIFT) / b 91} 92 93// ---- conversions --------------------------------------------------- 94 95func fx_from_int(n: i64) -> i64 { return n << FX_SHIFT } 96 97// Truncate Q16.16 to integer (toward zero). 98func fx_to_int(a: i64) -> i64 { return a >> FX_SHIFT } 99 100// Construct Q16.16 from (integer, numerator, denominator): useful for 101// exact rationals like "1/3 of a block". 102func fx_from_frac(num: i64, den: i64) -> i64 { 103 return (num << FX_SHIFT) / den 104} 105 106// ---- CORDIC sin/cos ----------------------------------------------- 107// 108// Rotation-mode CORDIC. Input: angle in Q16.16 radians (call 109// fx_normalize_angle first if you can't guarantee |angle| <= π/2). 110// Output: writes sin to *sin_out, cos to *cos_out, both Q16.16. 111// 112// Precomputed atan(2^-i) in Q16.16 for i = 0..15. Each entry is 113// round(atan(2^-i) * 65536). These are constant across all targets 114// by spec (not by local IEEE-754 evaluation). 115func fx_cordic_atan(i: i64) -> i64 { 116 if i == 0 { return FX_MAGIC_51472 } // atan(1) * 2^16 117 if i == 1 { return FX_MAGIC_30386 } // atan(1/2) 118 if i == 2 { return FX_MAGIC_16055 } // atan(1/4) 119 if i == 3 { return FX_MAGIC_8150 } // atan(1/8) 120 if i == 4 { return FX_MAGIC_4091 } // atan(1/16) 121 if i == 5 { return FX_MAGIC_2047 } // atan(1/32) 122 if i == 6 { return FX_MAGIC_1024 } // atan(1/64) 123 if i == 7 { return 512 } // atan(1/128) 124 if i == 8 { return 256 } // atan(1/256) 125 if i == 9 { return 128 } 126 if i == 10 { return 64 } 127 if i == 11 { return 32 } 128 if i == 12 { return 16 } 129 if i == 13 { return 8 } 130 if i == 14 { return 4 } 131 return 2 // atan(2^-15) 132} 133 134// Normalise angle into [-π, π] (Q16.16). Strips off full-rotation 135// multiples of 2π so CORDIC convergence stays stable. 136func fx_normalize_angle(a: i64) -> i64 { 137 var r: i64 = a 138 while r > FX_PI { r = r - FX_TWO_PI } 139 while r < (0 - FX_PI) { r = r + FX_TWO_PI } 140 return r 141} 142 143// Compute sin(theta) and cos(theta) for theta in Q16.16. Writes both 144// outputs to caller-supplied slots so callers can take both without 145// a second pass. 146// 147// CORDIC range is [-π/2, π/2] in the basic form. To cover the full 148// [-π, π] we reduce via quadrant rules: 149// θ ∈ [π/2, π] -> θ' = π - θ; sin = sin(θ'); cos = -cos(θ') 150// θ ∈ [-π, -π/2] -> θ' = -π - θ; sin = -sin(θ'); cos = -cos(θ') 151func fx_sin_cos(theta_raw: i64, sin_out: *i64, cos_out: *i64) -> i64 { 152 let theta: i64 = fx_normalize_angle(theta_raw) 153 var t: i64 = theta 154 var flip_cos: i64 = 0 155 var flip_sin: i64 = 0 156 157 if t > FX_HALF_PI { 158 t = FX_PI - t 159 flip_cos = 1 160 } 161 if t < (0 - FX_HALF_PI) { 162 t = (0 - FX_PI) - t 163 flip_sin = 1 164 flip_cos = 1 165 } 166 167 // Core CORDIC loop: start at (K, 0), iteratively converge on 168 // angle t. 169 var x: i64 = FX_CORDIC_K 170 var y: i64 = 0 171 var z: i64 = t 172 var i: i64 = 0 173 while i < 16 { 174 let atan_i: i64 = fx_cordic_atan(i) 175 let dx: i64 = y >> i 176 let dy: i64 = x >> i 177 if z >= 0 { 178 x = x - dx 179 y = y + dy 180 z = z - atan_i 181 } else { 182 x = x + dx 183 y = y - dy 184 z = z + atan_i 185 } 186 i = i + 1 187 } 188 189 if flip_sin == 1 { y = 0 - y } 190 if flip_cos == 1 { x = 0 - x } 191 192 *sin_out = y 193 *cos_out = x 194 return 0 195} 196 197// Convenience wrappers for callers that only want one component. 198func fx_sin(theta: i64) -> i64 { 199 let s_raw: *u8 = sys_mmap(16) 200 let c_raw: *u8 = sys_mmap(16) 201 let s: *i64 = s_raw as *i64 202 let c: *i64 = c_raw as *i64 203 fx_sin_cos(theta, s, c) 204 return *s 205} 206 207func fx_cos(theta: i64) -> i64 { 208 let s_raw: *u8 = sys_mmap(16) 209 let c_raw: *u8 = sys_mmap(16) 210 let s: *i64 = s_raw as *i64 211 let c: *i64 = c_raw as *i64 212 fx_sin_cos(theta, s, c) 213 return *c 214} 215 216// ---- game-sim convenience: u8 yaw angle -------------------------- 217// 218// Many game protocols (including ours) transmit yaw as a single byte: 219// 256 discrete angles equally spaced around the circle. This helper 220// converts a yaw byte to Q16.16 radians using the exact formula 221// angle = yaw * 2π / 256 (Q16.16) 222// implemented as (yaw * FX_TWO_PI) / 256, where FX_TWO_PI is the 223// same integer on every target -- no target-dependent rounding. 224func fx_yaw_u8_to_angle(yaw: i64) -> i64 { 225 let y: i64 = yaw & 0xFF 226 return (y * FX_TWO_PI) / 256 227} 228 229// ---- compile-only smoke ------------------------------------------ 230// 231// Exercises each primitive. Real validation (bit-exact comparison 232// with reference values computed offline) comes in fx_test.nx once 233// an execution harness is available on this host. 234func main() -> i64 { 235 // Trig smoke: sin(0) should be 0, cos(0) should be FX_ONE. 236 let zero: i64 = 0 237 let s0: i64 = fx_sin(zero) 238 let c0: i64 = fx_cos(zero) 239 240 // sin(π/2) ≈ 1.0 → close to FX_ONE. 241 let s1: i64 = fx_sin(FX_HALF_PI) 242 243 // Multiply: 2.5 * 4.0 = 10.0 in Q16.16. 244 let a: i64 = FX_ONE * 2 + FX_HALF // 2.5 245 let b: i64 = FX_ONE * 4 // 4.0 246 let prod: i64 = fx_mul(a, b) // expect 10 << 16 247 248 // Divide: 1.0 / 3.0 ≈ 21845 in Q16.16. 249 let third: i64 = fx_div(FX_ONE, FX_ONE * 3) 250 251 // Yaw conversion: yaw=64 (quarter-turn) → angle ≈ π/2. 252 let q: i64 = fx_yaw_u8_to_angle(64) 253 254 return s0 + c0 + s1 + prod + third + q 255}