code wiki / (root) / nx_f32_hw.nx

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}