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}