code wiki / (root) / nx_glbnorm_lib.nx

nx_glbnorm_lib.nx source

↩ module page · 297 lines · 15863 B

1// nx_glbnorm_lib.nx -- ONE OWNER for EMITTING A SPEC-CORRECT UNIT NORMAL INTO A glTF NORMAL ACCESSOR. 2// 3// WHY THIS EXISTS, MEASURED RATHER THAN ARGUED (2026-08-25). Every .glb this estate publishes was 4// invalid per glTF 2.0. KhronosGroup/glTF-Validator 2.0.0-dev.3.10 -- the specification body's own 5// reference implementation -- run under node over all 18 .glb files fetched from https://nishifamily.com 6// by curl (third-party client, third-party TLS, separate host), uncapped (maxIssues 0), returned: 7// files=18 clean=0 with_errors=18 threw=0 sum=18 -> 33,682 errors 8// of which ACCESSOR_VECTOR3_NON_UNIT x 33,656 -- every NORMAL emitted by nx_gltf_export sat at ~1000 9// scale (worst report: "not of unit length: 999.7844767748697") where the specification requires unit 10// length. The other 26 are three unrelated signatures in six files, recorded in the gate header. 11// 12// THE BUG'S JUSTIFICATION WAS A COMMENT, NOT A MEASUREMENT. Two files asserted that "renderers 13// re-normalise" -- nx_gltf_export.gl_vnormals and the nx_vnormals_lib header. The validator ranks it an 14// Error, not a warning. SIX of our own gates ran GREEN over this code and none could see it, because 15// every one of them measured our code against our own expectations. The referee's rule is a single line, 16// read out of its own shipping bytes rather than recalled: 17// if (Math.abs(Math.sqrt(r) - 1) > 0.00674) -> ACCESSOR_VECTOR3_NON_UNIT 18// so the bar is an ABSOLUTE tolerance of 0.00674 on |length - 1|. GN_TOL_FP below is derived from it. 19// 20// WHERE THE FIX BELONGS, AND WHERE IT DELIBERATELY DOES NOT. vn_smooth_area (nx_vnormals_lib) is the 21// shared owner of smooth per-vertex normals and emits at VN_SCALE = 1000. That scale is NOT the defect: 22// it also feeds nx_obj_export, where OBJ importers rescale and no specification is violated. Rescaling 23// the shared owner would silently change a proven artifact and the 18/18 gate that pins it, to fix a 24// violation that exists only at the glTF write site. So the normalisation lives HERE, at the write site, 25// and vn_smooth_area is left exactly as it is. 26// 27// THE ARITHMETIC IS EXACT, NOT APPROXIMATE, AND EVERY CONSTANT BELOW IS DERIVED. 28// Dividing by 1000 is not representable in binary32. Dividing by a POWER OF TWO is a pure exponent 29// subtraction and is exact, so a normal is rescaled to length 2^GN_K and emitted as the rational 30// m / 2^GN_K, which vm_int_to_f32 encodes with no loss at all. 31// GN_K = 23 MAXIMAL. Every emitted numerator satisfies |m| <= 2^GN_K * (1 + 1/lq) < 2^24, and 32// every integer below 2^24 is exactly representable in binary32. GN_K = 24 is not. 33// GN_M = 18 MAXIMAL for the envelope below. The root is taken at 2^GN_M extra bits of precision, 34// lq = floor(2^GN_M * sqrt(s)), which needs s << 2*GN_M to fit i64: 35// s <= 3 * GN_IN_MAX^2 = 50,331,648, and 50,331,648 << 36 = 3.46e18 < 2^63 = 9.22e18. 36// GN_M = 19 overflows. Without this refinement the floor of a plain integer root would 37// contribute a relative error near 1/1000 -- three orders worse than everything else 38// here, and the single largest term in the budget. 39// GN_IN_MAX = 4096 A MATCHED PAIR WITH GN_M, not an independent choice. It must admit both producers 40// unreduced -- VN_SCALE 1000 and tm_vnorm's fx4096 -- and 4096 is the largest power of 41// two for which GN_M can still be 18. A larger input is HALVED into the envelope 42// rather than refused, and the shift count is returned so a caller can see it happened. 43// 44// RESIDUAL LENGTH ERROR -- DERIVED FIRST, THEN MEASURED. 45// root term <= 1/lq <= 2^-GN_M = 3.815e-6 (worst case, s = 1) 46// truncation term <= sqrt(3) / 2^GN_K = 2.065e-7 47// total < 4.03e-6 against the referee's 0.00674 -> 1,676x margin for ANY legal input. 48// MEASURED over all 33,656 real normals in the 12 affected published files, worst |length - 1| was 49// 1.8995e-7 -- a 35,483x margin -- and the referee then returned 0 errors for all 12 of those files. 50// 51// NO SECOND RULER. The binary32 codec already has one owner in nx_vecmath (vm_int_to_f32 / 52// vm_f32_to_int, themselves the retirement of five private copies) and the integer root has one owner 53// in vm_isqrt. This file composes all three and defines no arithmetic of its own beyond the scaling 54// decision that is its whole reason to exist. 55// 56// 100 percent sovereign. No hardware writes (Rule 26). license_tier: ORIGINAL 57import "nx_vecmath.nx" 58 59const GN_K: i64 = 23 60const GN_M: i64 = 18 61const GN_IN_MAX: i64 = 4096 62const GN_ONE: i64 = 8388608 // 2^GN_K -- the numerator that means length 1.0 exactly 63const GN_MAX_NUM: i64 = 16777216 // 2^24 -- at and above this, binary32 stops representing integers exactly 64const GN_V3: i64 = 3 // components per normal, matching the estate's AoS buffers 65 66// A vertex touched by no usable face has no normal. vn_smooth_area answers +Y at VN_SCALE for that case 67// and names it a FALLBACK rather than an accident; this reproduces the same DIRECTION at this lib's 68// scale, so the two agree by construction instead of by coincidence. Emitting a zero vector instead 69// would trade one validator error for a black or NaN-shaded fragment AND still be non-unit. 70const GN_FB_X: i64 = 0 71const GN_FB_Y: i64 = 8388608 // == GN_ONE, deliberately spelled out beside its siblings 72const GN_FB_Z: i64 = 0 73 74// ---- THE RULER ----------------------------------------------------------------------------------- 75// Fixed-point scale the length check runs in. 19 rather than GN_K because the ruler must also be able 76// to measure the PRE-FIX ~1000-scale emission -- a ruler that cannot measure the bad case cannot be 77// used as its own negative control. 78const GN_ERR_FP: i64 = 19 79const GN_ERR_ONE: i64 = 524288 // 2^GN_ERR_FP 80const GN_TOL_FP: i64 = 3533 // floor(0.00674 * 2^19) = 3533.7 -> 3533: STRICTER than the referee 81 // by 0.7 units, i.e. wrong in the direction of refusing. 82const GN_RULER_MAX: i64 = 2048 // largest |component| this ruler can square: (2048*2^19)^2 * 3 = 3.46e18 83const GN_ERR_UNMEASURABLE: i64 = 0 - 1 84// DECLARED IMPRECISION: vm_f32_to_int rounds to nearest, so gn_unit_err resolves length to about 85// 1.9e-6 and reports 0 for an emission whose true error is 1.9e-7. It is a BOUND, not a value. The 86// exact figure for this lib's own output comes from gn_unit_err_exact below, which loses nothing. 87const GN_TOL_EXACT: i64 = 34 // ceil(2^GN_K / 2^GN_M + sqrt(3)) = ceil(32 + 1.733), in units of 2^-GN_K 88 89const GN_OK: i64 = 0 90 91// Halve an over-envelope vector until every component fits GN_IN_MAX. Returns the shift count, which is 92// 0 for both real producers -- measured, not assumed: over the 33,656 published normals it was 0 every 93// time. Division by 2 rather than a shift, so truncation is toward zero for negatives too. 94func gn_reduce(v: *i64) -> i64 { 95 var sh: i64 = 0 96 var m: i64 = vm_max(vm_max(vm_abs(v[0]), vm_abs(v[1])), vm_abs(v[2])) 97 while m > GN_IN_MAX { 98 v[0] = v[0] / 2 99 v[1] = v[1] / 2 100 v[2] = v[2] / 2 101 m = m / 2 102 sh = sh + 1 103 } 104 return sh 105} 106 107// THE FIX. Rescale (x,y,z) to length GN_ONE = 2^GN_K and write the three integer numerators to out[0..2]. 108// Returns the reduction shift count (0 on every production path). The sign is applied AFTER the divide so 109// the truncation is symmetric and does not depend on how the language rounds a negative quotient. 110func gn_unit3(x: i64, y: i64, z: i64, out: *i64) -> i64 { 111 out[0] = x 112 out[1] = y 113 out[2] = z 114 let sh: i64 = gn_reduce(out) 115 let s: i64 = out[0]*out[0] + out[1]*out[1] + out[2]*out[2] 116 if s == 0 { 117 out[0] = GN_FB_X 118 out[1] = GN_FB_Y 119 out[2] = GN_FB_Z 120 return sh 121 } 122 let lq: i64 = vm_isqrt(s << (GN_M * 2)) 123 var i: i64 = 0 124 while i < GN_V3 { 125 let neg: i64 = vm_sgn(out[i]) 126 let q: i64 = (vm_abs(out[i]) << (GN_K + GN_M)) / lq 127 if neg < 0 { 128 out[i] = 0 - q 129 } else { 130 out[i] = q 131 } 132 i = i + 1 133 } 134 return sh 135} 136 137// The binary32 bit pattern that MUST be written for a numerator gn_unit3 produced. Exact by construction: 138// |m| < 2^24 and the denominator is a power of two, so vm_int_to_f32 loses nothing. 139func gn_f32(m: i64) -> i64 { return vm_int_to_f32(m, GN_ONE) } 140 141// Recover the exact integer numerator from a binary32 this lib emitted. EXACT for that input, because the 142// value is m/2^GN_K with |m| < 2^24 and the scale asked for is the same power of two. 143func gn_num(bits: i64) -> i64 { return vm_f32_to_int(bits, GN_ONE) } 144 145// |length - 1| in units of 2^-GN_ERR_FP, computed from the RAW binary32 bit patterns actually written. 146// This is the predicate the gate applies to BOTH the fixed emission and the transcribed pre-fix one, so 147// a pass and a fail are the same measurement rather than two. 148// ABSTAINS (GN_ERR_UNMEASURABLE) rather than acquitting when a component is too large to square. 149func gn_unit_err(bx: i64, by: i64, bz: i64) -> i64 { 150 let fx: i64 = vm_f32_to_int(bx, GN_ERR_ONE) 151 let fy: i64 = vm_f32_to_int(by, GN_ERR_ONE) 152 let fz: i64 = vm_f32_to_int(bz, GN_ERR_ONE) 153 let lim: i64 = GN_RULER_MAX * GN_ERR_ONE 154 if vm_abs(fx) > lim { return GN_ERR_UNMEASURABLE } 155 if vm_abs(fy) > lim { return GN_ERR_UNMEASURABLE } 156 if vm_abs(fz) > lim { return GN_ERR_UNMEASURABLE } 157 let l: i64 = vm_isqrt(fx*fx + fy*fy + fz*fz) 158 return vm_abs(l - GN_ERR_ONE) 159} 160 161// |length - GN_ONE| in units of 2^-GN_K, with NO fixed-point loss -- valid only for bit patterns this lib 162// emitted, which is why it abstains on anything outside that range instead of reporting a number. 163func gn_unit_err_exact(bx: i64, by: i64, bz: i64) -> i64 { 164 let mx: i64 = gn_num(bx) 165 let my: i64 = gn_num(by) 166 let mz: i64 = gn_num(bz) 167 if vm_abs(mx) >= GN_MAX_NUM { return GN_ERR_UNMEASURABLE } 168 if vm_abs(my) >= GN_MAX_NUM { return GN_ERR_UNMEASURABLE } 169 if vm_abs(mz) >= GN_MAX_NUM { return GN_ERR_UNMEASURABLE } 170 let l: i64 = vm_isqrt(mx*mx + my*my + mz*mz) 171 return vm_abs(l - GN_ONE) 172} 173 174// ---- EXACT DECIMAL RENDERING OF A binary32 --------------------------------------------------------- 175// WHY THIS IS HERE AND WHY IT MUST BE EXACT. glTF 2.0 requires an accessor's min/max to MATCH the data, 176// and the validator compares them numerically, not approximately. Declaring a TRUNCATED INTEGER bound is 177// what produced ACCESSOR_MIN_MISMATCH, ACCESSOR_MAX_MISMATCH and ACCESSOR_ELEMENT_OUT_OF_MIN_BOUND on two 178// published files -- the writer declared min -444 for data whose true minimum is -444.77398681640625, so 179// 30 real vertices sat OUTSIDE the bounds the file itself advertised. The remedy is not a wider bound; it 180// is to render the bound FROM THE VERY BITS THAT WERE WRITTEN, which is what this function exists for. 181// 182// IT IS ALWAYS FINITE. Every binary32 is m * 2^-sh with m < 2^24 and sh <= 149, and m/2^sh = m*5^sh/10^sh 183// exactly -- so the decimal terminates. Digit bound, DERIVED: m contributes at most 8 digits and 5^149 has 184// 105, so at most 113; GN_DEC_MAX is 192, which is headroom, not a guess. The guard below still refuses 185// rather than truncating, because a silently truncated number would be the same class of defect this 186// function was written to remove. 187const GN_SIGN_SHIFT: i64 = 31 188const GN_DENORM_SH: i64 = 149 // 23 + 126: a denormal is frac * 2^-149 189const GN_DEC_MAX: i64 = 192 190const GN_DEC_BASE: i64 = 10 191const GN_DEC_FIVE: i64 = 5 192const GN_DEC_TWO: i64 = 2 193const GN_CH_ZERO: i64 = 48 194const GN_CH_MINUS: i64 = 45 195const GN_CH_DOT: i64 = 46 196const GN_DEC_OVERFLOW: i64 = 0 - 1 197 198// Map a binary32 bit pattern to a MONOTONIC integer key, so two floats compare EXACTLY as their values do. 199// WHY A BOUND NEEDS THIS. nx_mesh2glb picked its POSITION min/max by comparing values already rounded to 200// milli-units, so two DISTINCT float32s could tie and the first-seen one won. A bound chosen that way can 201// land strictly INSIDE the data -- which is ACCESSOR_ELEMENT_OUT_OF_MIN_BOUND, the same defect as the 202// truncated bound but far harder to see, because the number looks plausible. Comparing keys removes the 203// tie entirely rather than making the rounding finer, which would only move the tie somewhere else. 204const GN_U32_MASK: i64 = 4294967295 205const GN_SIGN_BIT: i64 = 2147483648 206func gn_f32_key(bits: i64) -> i64 { 207 if ((bits >> GN_SIGN_SHIFT) & 1) == 1 { return GN_U32_MASK - bits } 208 return bits + GN_SIGN_BIT 209} 210 211func gn_dec_f32(bits: i64, dst: *u8, at: i64) -> i64 { 212 let expo: i64 = (bits >> VM_F32_MANT_BITS) & VM_F32_EXP_MASK 213 let frac: i64 = bits & VM_F32_MANT_MASK 214 var o: i64 = at 215 if expo == 0 { 216 if frac == 0 { dst[o] = GN_CH_ZERO as u8; return o + 1 } 217 } 218 if ((bits >> GN_SIGN_SHIFT) & 1) == 1 { dst[o] = GN_CH_MINUS as u8; o = o + 1 } 219 var m: i64 = frac 220 var sh: i64 = GN_DENORM_SH 221 if expo > 0 { 222 m = frac + VM_F32_IMPLICIT 223 sh = VM_F32_MANT_BITS + VM_F32_EXP_BIAS - expo 224 } 225 let d: *u8 = sys_mmap(GN_DEC_MAX) 226 var n: i64 = 0 227 while m > 0 { d[n] = (m % GN_DEC_BASE) as u8; m = m / GN_DEC_BASE; n = n + 1 } 228 if n == 0 { d[0] = 0 as u8; n = 1 } 229 var t: i64 = 0 230 while t < sh { 231 var carry: i64 = 0 232 var i: i64 = 0 233 while i < n { 234 let v: i64 = (d[i] as i64) * GN_DEC_FIVE + carry 235 d[i] = (v % GN_DEC_BASE) as u8 236 carry = v / GN_DEC_BASE 237 i = i + 1 238 } 239 while carry > 0 { 240 if n >= GN_DEC_MAX { return GN_DEC_OVERFLOW } 241 d[n] = (carry % GN_DEC_BASE) as u8 242 carry = carry / GN_DEC_BASE 243 n = n + 1 244 } 245 t = t + 1 246 } 247 // sh < 0 is the MIRROR CASE: a large value is m * 2^(-sh), an integer, and needs the matching multiply 248 // by two. The first draft of this function silently omitted it and rendered FLT_MAX as the digits of 249 // its mantissa -- caught by the 39-integer-digit KAT, which is the whole reason the extremes are in 250 // the KAT set rather than a comfortable middle range. 251 var t2: i64 = sh 252 while t2 < 0 { 253 var carry2: i64 = 0 254 var k2: i64 = 0 255 while k2 < n { 256 let v2: i64 = (d[k2] as i64) * GN_DEC_TWO + carry2 257 d[k2] = (v2 % GN_DEC_BASE) as u8 258 carry2 = v2 / GN_DEC_BASE 259 k2 = k2 + 1 260 } 261 while carry2 > 0 { 262 if n >= GN_DEC_MAX { return GN_DEC_OVERFLOW } 263 d[n] = (carry2 % GN_DEC_BASE) as u8 264 carry2 = carry2 / GN_DEC_BASE 265 n = n + 1 266 } 267 t2 = t2 + 1 268 } 269 // sh <= 0 means the value is an integer: no point, no fraction. 270 if sh <= 0 { 271 var i2: i64 = n - 1 272 while i2 >= 0 { dst[o] = (GN_CH_ZERO + (d[i2] as i64)) as u8; o = o + 1; i2 = i2 - 1 } 273 return o 274 } 275 if n <= sh { dst[o] = GN_CH_ZERO as u8; o = o + 1 } 276 if n > sh { 277 var i3: i64 = n - 1 278 while i3 >= sh { dst[o] = (GN_CH_ZERO + (d[i3] as i64)) as u8; o = o + 1; i3 = i3 - 1 } 279 } 280 // Lowest non-zero fraction digit, found with a SEPARATE cursor: writing the exit sentinel into the 281 // scan index is the estate's own named hazard and it erases the answer it just found. 282 var lo: i64 = 0 283 var scan: i64 = 0 284 var stop: i64 = 0 285 while scan < sh { 286 if stop == 0 { 287 if (d[scan] as i64) != 0 { lo = scan; stop = 1 } 288 } 289 scan = scan + 1 290 } 291 if stop == 0 { return o } 292 dst[o] = GN_CH_DOT as u8 293 o = o + 1 294 var i4: i64 = sh - 1 295 while i4 >= lo { dst[o] = (GN_CH_ZERO + (d[i4] as i64)) as u8; o = o + 1; i4 = i4 - 1 } 296 return o 297}