code wiki / (root) / nx_glbnorm_lib.nx

nx_glbnorm_lib.nx

buildroot/runtime/nx_glbnorm_lib.nx

15863 B297 linesdepth 2pulls 2 transitivereach 42 importersview sourcekind library
docsdependenciesstructsconstsfunctions

about

nx_glbnorm_lib.nx -- ONE OWNER for EMITTING A SPEC-CORRECT UNIT NORMAL INTO A glTF NORMAL ACCESSOR. WHY THIS EXISTS, MEASURED RATHER THAN ARGUED (2026-08-25). Every .glb this estate publishes was invalid per glTF 2.0. KhronosGroup/glTF-Validator 2.0.0-dev.3.10 -- the specification body's own reference implementation -- run under node over all 18 .glb files fetched from https://nishifamily.com by curl (third-party client, third-party TLS, separate host), uncapped (maxIssues 0), returned: files=18 clean=0 with_errors=18 threw=0 sum=18 -> 33,682 errors of which ACCESSOR_VECTOR3_NON_UNIT x 33,656 -- every NORMAL emitted by nx_gltf_export sat at ~1000 scale (worst report: "not of unit length: 999.7844767748697") where the specification requires unit length. The other 26 are three unrelated signatures in six files, recorded in the gate header. THE BUG'S JUSTIFICATION WAS A COMMENT, NOT A MEASUREMENT. Two files asserted that "renderers re-normalise" -- nx_gltf_export.gl_vnormals and the nx_vnormals_lib header. The validator ranks it an Error, not a warning. SIX of our own gates ran GREEN over this code and none could see it, because every one of them measured our code against our own expectations. The referee's rule is a single line, read out of its own shipping bytes rather than recalled: if (Math.abs(Math.sqrt(r) - 1) > 0.00674) -> ACCESSOR_VECTOR3_NON_UNIT so the bar is an ABSOLUTE tolerance of 0.00674 on |length - 1|. GN_TOL_FP below is derived from it. WHERE THE FIX BELONGS, AND WHERE IT DELIBERATELY DOES NOT. vn_smooth_area (nx_vnormals_lib) is the shared owner of smooth per-vertex normals and emits at VN_SCALE = 1000. That scale is NOT the defect: it also feeds nx_obj_export, where OBJ importers rescale and no specification is violated. Rescaling the shared owner would silently change a proven artifact and the 18/18 gate that pins it, to fix a violation that exists only at the glTF write site. So the normalisation lives HERE, at the write site, and vn_smooth_area is left exactly as it is. THE ARITHMETIC IS EXACT, NOT APPROXIMATE, AND EVERY CONSTANT BELOW IS DERIVED. Dividing by 1000 is not representable in binary32. Dividing by a POWER OF TWO is a pure exponent subtraction and is exact, so a normal is rescaled to length 2^GN_K and emitted as the rational m / 2^GN_K, which vm_int_to_f32 encodes with no loss at all. GN_K = 23 MAXIMAL. Every emitted numerator satisfies |m| <= 2^GN_K * (1 + 1/lq) < 2^24, and every integer below 2^24 is exactly representable in binary32. GN_K = 24 is not. GN_M = 18 MAXIMAL for the envelope below. The root is taken at 2^GN_M extra bits of precision, lq = floor(2^GN_M * sqrt(s)), which needs s << 2*GN_M to fit i64: s <= 3 * GN_IN_MAX^2 = 50,331,648, and 50,331,648 << 36 = 3.46e18 < 2^63 = 9.22e18. GN_M = 19 overflows. Without this refinement the floor of a plain integer root would contribute a relative error near 1/1000 -- three orders worse than everything else here, and the single largest term in the budget. GN_IN_MAX = 4096 A MATCHED PAIR WITH GN_M, not an independent choice. It must admit both producers unreduced -- VN_SCALE 1000 and tm_vnorm's fx4096 -- and 4096 is the largest power of

dependencies 1 imports · 10 importers

nx_vecmath.nx nx_glbnorm_lib.nx nx_glbnorm_gate.nx nx_gltf2mesh_normal_candidate_t337 nx_gltf_anim.nx nx_gltf_export.nx nx_gltf_export_material_candidate_ nx_gltf_export_multipart_candidate nx_gltf_mesh.nx nx_mesh2glb.nx nx_mesh2glb_neutral_candidate_t346 nx_normal_pointer_probe_t337.nx

imports: nx_vecmath.nx

imported by: nx_glbnorm_gate.nxnx_gltf2mesh_normal_candidate_t337.nxnx_gltf_anim.nxnx_gltf_export.nxnx_gltf_export_material_candidate_t340.nxnx_gltf_export_multipart_candidate_t345.nxnx_gltf_mesh.nxnx_mesh2glb.nxnx_mesh2glb_neutral_candidate_t346.nxnx_normal_pointer_probe_t337.nx

structs

none

consts

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
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
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
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
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
89const GN_OK: i64 = 0
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
204const GN_U32_MASK: i64 = 4294967295
205const GN_SIGN_BIT: i64 = 2147483648

functions

94func gn_reduce(v: *i64) -> i64
called by 1: gn_unit3 calls 2: vm_maxvm_abs
110func gn_unit3(x: i64, y: i64, z: i64, out: *i64) -> i64
139func gn_f32(m: i64) -> i64 { return vm_int_to_f32(m, GN_ONE) }
143func gn_num(bits: i64) -> i64 { return vm_f32_to_int(bits, GN_ONE) }
149func gn_unit_err(bx: i64, by: i64, bz: i64) -> i64
163func gn_unit_err_exact(bx: i64, by: i64, bz: i64) -> i64
called by 1: gg_measure calls 3: gn_numvm_absvm_isqrt
206func gn_f32_key(bits: i64) -> i64
211func gn_dec_f32(bits: i64, dst: *u8, at: i64) -> i64