nx_fx.nx
buildroot/runtime/nx_fx.nx
about
fx.nx -- deterministic Q16.16 fixed-point math.
Why fixed-point and not IEEE-754 float:
Rollback netcode (GGPO 2006, Skullgirls, Killer Instinct) requires
bit-identical simulation across machines. IEEE-754 + Math.sin
cannot deliver this -- different JS engines (V8, SpiderMonkey,
JavaScriptCore) ship different sin/cos implementations, and even
addition order can diverge under JIT inlining. Fixed-point
integer math is the only way to guarantee "same input → same bits
on every machine."
Representation: Q16.16
- 64-bit signed integer: high 16 bits integer part, low 16 bits
fractional part. Negative values use two's complement.
- Range: ±32767.99998... blocks. Precision: 1/65536 ≈ 15 μm at
1 m = 1 block. Adequate for voxel games, physics, gameplay
sim up to ~32 km arenas.
Invariants (enforced, not hoped):
FX1 No IEEE-754 anywhere. NishiLang has no float type today, so
this is trivial -- but if floats ever land, fx.nx does not
use them.
FX2 fx_mul and fx_div round toward zero (arithmetic shift, not
round-to-nearest). Deterministic and simple; callers needing
round-to-nearest do `(a + (b >> 1)) / b` explicitly.
FX3 sin/cos via CORDIC algorithm -- 16 iterations, ~Q16.16
precision, no Math.sin dependency. Converges through a
precomputed atan table. Every implementation (NishiVM,
native C VM, future Nishi silicon) produces bit-identical
results by construction.
FX4 Overflow in fx_mul is possible if both operands exceed 16-bit
integer range. Callers are responsible for clamping; we do
NOT implement saturation arithmetic by default (silent
saturation masks bugs; explicit clamp is better).
References:
- Volder 1959, "The CORDIC Trigonometric Computing Technique"
- Kota-Kuroda-Shimamura 1989, "A High-Speed Fixed-Point Multiplier
Using Carry-Propagation-Free Adders"
- GGPO 2006 whitepaper (rollback determinism rationale)
dependencies 1 imports · 0 importers
imports: nx_syscalls.nx
imported by: nobody (leaf or entry point)
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| 49 | const FX_MAGIC_51472: i64 = 51472 |
| 50 | const FX_MAGIC_30386: i64 = 30386 |
| 51 | const FX_MAGIC_16055: i64 = 16055 |
| 52 | const FX_MAGIC_8150: i64 = 8150 |
| 53 | const FX_MAGIC_4091: i64 = 4091 |
| 54 | const FX_MAGIC_2047: i64 = 2047 |
| 55 | const FX_MAGIC_1024: i64 = 1024 |
| 59 | const FX_SHIFT: i64 = 16 |
| 60 | const FX_ONE: i64 = 65536 // 2^16 |
| 61 | const FX_HALF: i64 = 32768 // 0.5 in Q16.16 |
| 62 | const FX_FRAC_MASK: i64 = 0xFFFF // low 16 bits |
| 63 | const FX_TWO_PI: i64 = 411775 // round(2π * 65536); 1 rev in Q16.16 |
| 64 | const FX_PI: i64 = 205887 // round(π * 65536) |
| 65 | const FX_HALF_PI: i64 = 102944 // round(π/2 * 65536) |
| 69 | const FX_CORDIC_K: i64 = 39797 |
functions
| 75 | func fx_add(a: i64, b: i64) -> i64 { return a + b } |
| 76 | func fx_sub(a: i64, b: i64) -> i64 { return a - b } |
| 77 | func fx_neg(a: i64) -> i64 { return 0 - a } |
| 82 | func fx_mul(a: i64, b: i64) -> i64 called by 1: main |
| 89 | func fx_div(a: i64, b: i64) -> i64 called by 1: main |
| 95 | func fx_from_int(n: i64) -> i64 { return n << FX_SHIFT } |
| 98 | func fx_to_int(a: i64) -> i64 { return a >> FX_SHIFT } |
| 102 | func fx_from_frac(num: i64, den: i64) -> i64 |
| 115 | func fx_cordic_atan(i: i64) -> i64 called by 1: fx_sin_cos |
| 136 | func fx_normalize_angle(a: i64) -> i64 called by 1: fx_sin_cos |
| 151 | func fx_sin_cos(theta_raw: i64, sin_out: *i64, cos_out: *i64) -> i64 |
| 198 | func fx_sin(theta: i64) -> i64 |
| 207 | func fx_cos(theta: i64) -> i64 |
| 224 | func fx_yaw_u8_to_angle(yaw: i64) -> i64 called by 1: main |
| 234 | func main() -> i64 |