nx_glbnorm_lib.nx
buildroot/runtime/nx_glbnorm_lib.nx
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
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
| 59 | const GN_K: i64 = 23 |
| 60 | const GN_M: i64 = 18 |
| 61 | const GN_IN_MAX: i64 = 4096 |
| 62 | const GN_ONE: i64 = 8388608 // 2^GN_K -- the numerator that means length 1.0 exactly |
| 63 | const GN_MAX_NUM: i64 = 16777216 // 2^24 -- at and above this, binary32 stops representing integers exactly |
| 64 | const GN_V3: i64 = 3 // components per normal, matching the estate's AoS buffers |
| 70 | const GN_FB_X: i64 = 0 |
| 71 | const GN_FB_Y: i64 = 8388608 // == GN_ONE, deliberately spelled out beside its siblings |
| 72 | const GN_FB_Z: i64 = 0 |
| 78 | const GN_ERR_FP: i64 = 19 |
| 79 | const GN_ERR_ONE: i64 = 524288 // 2^GN_ERR_FP |
| 80 | const GN_TOL_FP: i64 = 3533 // floor(0.00674 * 2^19) = 3533.7 -> 3533: STRICTER than the referee |
| 82 | const GN_RULER_MAX: i64 = 2048 // largest |component| this ruler can square: (2048*2^19)^2 * 3 = 3.46e18 |
| 83 | const GN_ERR_UNMEASURABLE: i64 = 0 - 1 |
| 87 | const GN_TOL_EXACT: i64 = 34 // ceil(2^GN_K / 2^GN_M + sqrt(3)) = ceil(32 + 1.733), in units of 2^-GN_K |
| 89 | const GN_OK: i64 = 0 |
| 187 | const GN_SIGN_SHIFT: i64 = 31 |
| 188 | const GN_DENORM_SH: i64 = 149 // 23 + 126: a denormal is frac * 2^-149 |
| 189 | const GN_DEC_MAX: i64 = 192 |
| 190 | const GN_DEC_BASE: i64 = 10 |
| 191 | const GN_DEC_FIVE: i64 = 5 |
| 192 | const GN_DEC_TWO: i64 = 2 |
| 193 | const GN_CH_ZERO: i64 = 48 |
| 194 | const GN_CH_MINUS: i64 = 45 |
| 195 | const GN_CH_DOT: i64 = 46 |
| 196 | const GN_DEC_OVERFLOW: i64 = 0 - 1 |
| 204 | const GN_U32_MASK: i64 = 4294967295 |
| 205 | const GN_SIGN_BIT: i64 = 2147483648 |
functions
| 94 | func gn_reduce(v: *i64) -> i64 |
| 110 | func gn_unit3(x: i64, y: i64, z: i64, out: *i64) -> i64 |
| 139 | func gn_f32(m: i64) -> i64 { return vm_int_to_f32(m, GN_ONE) } called by 15: gg_measuregg_orphan_okmaing2_convertwrite_glb_coloredgl_write_glb_scalar+9 calls 1: vm_int_to_f32 |
| 143 | func gn_num(bits: i64) -> i64 { return vm_f32_to_int(bits, GN_ONE) } |
| 149 | func gn_unit_err(bx: i64, by: i64, bz: i64) -> i64 |
| 163 | func gn_unit_err_exact(bx: i64, by: i64, bz: i64) -> i64 |
| 206 | func gn_f32_key(bits: i64) -> i64 |
| 211 | func gn_dec_f32(bits: i64, dst: *u8, at: i64) -> i64 |