nx_f32_hw.nx source
↩ module page · 128 lines · 5722 B
1// nx_f32_hw.nx -- HARDWARE IEEE-754 f32 for NishiLang via the __f32_* compiler intrinsics
2// (R1b -> SSE scalar-single on the CPU FPU). Distinct from nx_f32.nx, which is the i64
3// SOFT-FLOAT emulation of the same binary32 -- IDENTICAL bit layout (binary32 in the low 32
4// of an i64), so the two are bit-for-bit cross-checkable (see nx_f32_vec_gate differential).
5// This is the FAST reusable MATH PART the sovereign renderer composes: scalar + vec3 now,
6// mat4 next. negate = x * -1.0f (subss deferred to a perf rung). license_tier: ORIGINAL
7//
8// ===== DO NOT COLLAPSE THESE TWO MODULES INTO ONE (2026-08-23) ===============
9// A tempting "optimisation" is to re-point nx_f32.nx's BODIES at the __f32_*
10// intrinsics, so every existing nx_f32_* caller gets the hardware path for free.
11// THAT WOULD DELETE AN ORACLE, and it was measured and refused this session:
12//
13// nx_f32.nx is an INDEPENDENT IEEE-754 implementation. It is the only thing in
14// the estate that proves this hardware path is CONFORMANT rather than merely
15// SELF-CONSISTENT. If the soft-float BECOMES the intrinsic, nx_f32_vec_gate's
16// differential compares the hardware path AGAINST ITSELF -- every agreement goes
17// vacuous and the estate loses its only independent check on its own float
18// backend. A slow reference whose only remaining job is to DISAGREE with the
19// fast one is doing the most valuable job in the codebase.
20//
21// The differential was measured exhaustively over every IEEE class (16x16 = 256
22// pairs, _offc_probe_f32equiv.nx): 254 agree BIT-FOR-BIT -- including the
23// rounding-sensitive 1.0+2^-24 / 1.0+2^-23 and the subnormals, which REFUTES any
24// double-rounding-through-f64 hazard -- and exactly 2 differ: inf + (-inf) yields
25// a NaN whose SIGN BIT differs (soft-float returns a fixed POSITIVE qNaN, x86 its
26// default NEGATIVE qNaN). IEEE-754 does not specify the sign of a NaN result, so
27// neither is wrong. Both estate routes for observing a NaN are structurally blind
28// to it anyway: nx_f32_classify reads only exponent+mantissa, and nx_f32_eq
29// returns 0 for any NaN before it ever compares bits.
30//
31// SO: THIS IS THE FAST PATH, AND IT IS THE ONE HOT CODE SHOULD CALL.
32// want speed -> f32_add / f32_mul / f32_div (here; one SSE instruction)
33// want the reference oracle -> nx_f32_add / nx_f32_mul / ... (nx_f32.nx)
34// Migrating a hot consumer from nx_f32_* to f32_* is a REVIEWABLE PER-CONSUMER
35// decision that keeps the oracle intact. nx_f32_vec_gate's anti-vacuity tooth
36// enforces this: collapsing the two paths turns it RED (bite-proven, exit 35).
37// ============================================================================
38
39// ---- scalar (hardware SSE) ----
40func f32_of(n: i64) -> i64 { return __f32_from_i64(n) } // int -> binary32 bits (cvtsi2ss)
41func f32_int(x: i64) -> i64 { return __f32_to_i64(x) } // binary32 -> int, truncate (cvttss2si)
42func f32_add(a: i64, b: i64) -> i64 { return __f32_add(a, b) } // addss
43func f32_mul(a: i64, b: i64) -> i64 { return __f32_mul(a, b) } // mulss
44func f32_div(a: i64, b: i64) -> i64 { return __f32_div(a, b) } // divss
45func f32_neg(a: i64) -> i64 { return __f32_mul(a, __f32_from_i64(0 - 1)) }
46func f32_sub(a: i64, b: i64) -> i64 { return __f32_add(a, f32_neg(b)) }
47
48// ---- vec3 (each component a binary32 carrier) ----
49func v3_dot(ax: i64, ay: i64, az: i64, bx: i64, by: i64, bz: i64) -> i64 {
50 return f32_add(f32_add(f32_mul(ax, bx), f32_mul(ay, by)), f32_mul(az, bz))
51}
52func v3_lensq(ax: i64, ay: i64, az: i64) -> i64 { return v3_dot(ax, ay, az, ax, ay, az) }
53func v3_cross(ax: i64, ay: i64, az: i64, bx: i64, by: i64, bz: i64, out: *i64) -> i64 {
54 out[0] = f32_sub(f32_mul(ay, bz), f32_mul(az, by))
55 out[1] = f32_sub(f32_mul(az, bx), f32_mul(ax, bz))
56 out[2] = f32_sub(f32_mul(ax, by), f32_mul(ay, bx))
57 return 0
58}
59func v3_scale(ax: i64, ay: i64, az: i64, s: i64, out: *i64) -> i64 {
60 out[0] = f32_mul(ax, s)
61 out[1] = f32_mul(ay, s)
62 out[2] = f32_mul(az, s)
63 return 0
64}
65
66// ---- mat4 (16 binary32 carriers, row-major) -- the projection/camera workhorse ----
67func m4_identity(out: *i64) -> i64 {
68 var i: i64 = 0
69 while i < 16 { out[i] = f32_of(0); i = i + 1 }
70 out[0] = f32_of(1)
71 out[5] = f32_of(1)
72 out[10] = f32_of(1)
73 out[15] = f32_of(1)
74 return 0
75}
76// out = m * v (column vector v[4]) -- the per-vertex transform.
77func m4_vec4(m: *i64, v: *i64, out: *i64) -> i64 {
78 var r: i64 = 0
79 while r < 4 {
80 var sum: i64 = f32_of(0)
81 var k: i64 = 0
82 while k < 4 { sum = f32_add(sum, f32_mul(m[r * 4 + k], v[k])); k = k + 1 }
83 out[r] = sum
84 r = r + 1
85 }
86 return 0
87}
88// out = a * b (4x4 * 4x4). out must not alias a or b.
89func m4_mul(a: *i64, b: *i64, out: *i64) -> i64 {
90 var r: i64 = 0
91 while r < 4 {
92 var c: i64 = 0
93 while c < 4 {
94 var sum: i64 = f32_of(0)
95 var k: i64 = 0
96 while k < 4 { sum = f32_add(sum, f32_mul(a[r * 4 + k], b[k * 4 + c])); k = k + 1 }
97 out[r * 4 + c] = sum
98 c = c + 1
99 }
100 r = r + 1
101 }
102 return 0
103}
104// translation matrix (right column = tx,ty,tz).
105func m4_translation(tx: i64, ty: i64, tz: i64, out: *i64) -> i64 {
106 m4_identity(out)
107 out[3] = tx
108 out[7] = ty
109 out[11] = tz
110 return 0
111}
112// rotation about Y / X from f32 cos & sin (caller supplies the trig -- no f32 trig lib yet).
113func m4_roty(c: i64, s: i64, out: *i64) -> i64 {
114 m4_identity(out)
115 out[0] = c
116 out[2] = s
117 out[8] = f32_neg(s)
118 out[10] = c
119 return 0
120}
121func m4_rotx(c: i64, s: i64, out: *i64) -> i64 {
122 m4_identity(out)
123 out[5] = c
124 out[6] = f32_neg(s)
125 out[9] = s
126 out[10] = c
127 return 0
128}