code wiki / _hdl_build / nx_mesh2glb.nx

nx_mesh2glb.nx source

↩ module page · 909 lines · 50748 B

1// nx_mesh2glb.nx -- NXMSH2 -> INTERACTIVE .glb (operator 2026-08-05: "a picture vs the other files' 2// full interactivity panel -- that's dumb"). The measurement organs emit NXMSH2 (per-tri colour = the 3// deviation heatmap lives there); the showcase viewer consumes .glb. This organ closes that seam, so a 4// FITTED body and a PAINTED HEATMAP both become drag-to-rotate panels, not screenshots -- an interactive 5// deviation map is a capability the reference industrial tools ship as static report pages. 6// Positions PASS THROUGH as raw f32 bits (zero re-quantisation); per-tri colour is replicated to the 7// tri's 3 vertices as normalized u8 VEC4; per-vertex normals are {VEC3, FLOAT} and UNIT. 8// CORRECTED 2026-08-25. That last clause used to read "normals transcode to normalized i16 -- the exact 9// accessor types our proven nishi_walk.glb already uses, so the viewer needs nothing new". nishi_walk.glb 10// was NOT proven; it was simply never checked against the specification. KhronosGroup/glTF-Validator 11// rejects {VEC3, SHORT normalized} for NORMAL outright (MESH_PRIMITIVE_ATTRIBUTES_ACCESSOR_INVALID_FORMAT) 12// and rejects its 6-byte element as unaligned (MESH_PRIMITIVE_ACCESSOR_UNALIGNED). So this exporter 13// inherited a defect BY CITING ANOTHER OF OUR FILES AS THE STANDARD -- which is how a house style becomes 14// indistinguishable from a specification. The bounds were wrong for the same reason: see the JSON below. 15// nx_mesh2glb <in.nxmesh> <out.glb> | selftest 16// license_tier: ORIGINAL expect_exit: 0 17import "nx_syscalls.nx" 18import "nx_nxa.nx" 19import "nx_glbnorm_lib.nx" 20import "nx_nxmesh_lib.nx" 21import "nx_asset_prov_lib.nx" 22 23// gn_f32_key maps a binary32 to a monotonic integer, so these two sentinels bracket every possible key. 24const MG_KEY_ABOVE_ALL: i64 = 4294967296 // one past the largest key gn_f32_key can return 25const MG_KEY_BELOW_ALL: i64 = 0 - 1 // one below the smallest; keys are never negative 26const MG_NRM_GAIN: i64 = 4 // permille x 4 = 4000 for a unit component: the largest 27 // whole multiple of the source scale that stays inside 28 // nx_glbnorm_lib's GN_IN_MAX of 4096 29const MG_VEC3_F32_BYTES: i64 = 12 30const MG_V3: i64 = 3 31// ===== NXANIM01 -> .glb: THE RIGGED EXPORT LEG (2026-08-22) ===== 32// WHY. nx_roundtrip measured the estate's ingest as FORM-ONLY end to end: the four rigged donors lose 33// all nine named parts on a round trip, because nx_gltf2mesh reads JOINTS_0 as a name key and THIS 34// organ emitted no skin at all. NXMSH2 structurally cannot carry a rig (84-byte triangle records, no 35// joint section), so the rig-capable container is NXANIM01 (.nxa): VERT TRIS SKEL SKIN are first-class 36// sections there. ONE organ dispatching on container -- ONE GLB writer, two inputs -- so the 37// interactive panel and the rig oracle can never drift from each other. 38// NXA facts are READ FROM knowledge/nxa_format_spec.md and nx_nxa.nx (the ONLY place magic, tags and 39// checksum are defined); every section is located through nxa_find, which VERIFIES version, TOC and 40// payload checksums BEFORE returning -- a truncated file is refused by the format's own integrity. 41// VERT: [nverts][x y z ...] integer 0.01 mm, model space (spec UNIT CORRECTION seq1285) 42// TRIS: [ntris][a b c ...] one i64 word per index, CCW 43// SKEL: [njoints] + 8 words/joint [parent][tx ty tz 0.01mm][qx qy qz qw q12]; parent -1 = root 44// SKIN: [nverts] + 8 words/vertex [j0 j1 j2 j3][w0 w1 w2 w3 q12] (spec: sum w = 4096) 45// glTF 2.0 facts relied on: "The units for all linear distances are meters."; skins[].joints are NODE 46// indices; inverseBindMatrices is MAT4 FLOAT with count == joints.length; JOINTS_0 is VEC4 47// UNSIGNED_BYTE or UNSIGNED_SHORT; WEIGHTS_0 is VEC4 FLOAT; an accessor's byteOffset must be a 48// multiple of its component size; when NORMAL is absent "client implementations MUST calculate flat 49// normals" -- so NORMAL is omitted here on purpose (nx_gltf2mesh ignores it; smooth normals are a 50// viewer-quality rung, not a rig-fidelity rung). 51// BIND ORIENTATION IS NOT CARRIED, DELIBERATELY. The estate's normative runtime LBS (spec ANIM channel 52// 2, nx_nxa_rig_emit) is defined ABOUT THE BIND TRANSLATION with identity orientation: 53// M x = D(x - bind_t) + bind_t + dt. Mirroring that, each joint node carries translation only and its 54// inverse-bind matrix is translate(-bind_t) -- exactly what nx_gltf_anim, the proven incumbent skin 55// writer, emits. Carrying SKEL's bind quaternion would require IBM = R^T * T(-t) and rotations composed 56// down the hierarchy, i.e. a change to the estate's RIG CONVENTION, not to this exporter. It is 57// announced in every receipt as bind_rotation=dropped-identity-convention so the limit is visible. 58const MG_CONTAINER_NXMSH2: i64 = 1 59const MG_CONTAINER_NXA: i64 = 2 60const MG_NXA_HDR_BYTES: i64 = 32 // magic, version, nsect, toccheck 61const MG_NXA_TOC_WORDS: i64 = 4 // tag, byte_off, word_len, check 62const MG_NXA_UNITS_PER_M: i64 = 100000 // VERT/SKEL are 0.01 mm; 100000 of them per metre 63const MG_Q12: i64 = 4096 // SKIN weights are q12 64const MG_SKEL_REC_WORDS: i64 = 8 65const MG_SKIN_REC_WORDS: i64 = 8 66const MG_SKEL_PARENT: i64 = 0 67const MG_SKEL_TX: i64 = 1 68const MG_SKEL_ROOT: i64 = 0 - 1 // parent = -1 marks a root joint (spec) 69const MG_SKIN_W0: i64 = 4 // weights follow the four joint slots 70const MG_GL_UNSIGNED_BYTE: i64 = 5121 // glTF componentType enum, named for what it is 71const MG_GL_UNSIGNED_SHORT: i64 = 5123 72const MG_GL_UNSIGNED_INT: i64 = 5125 73const MG_GL_FLOAT: i64 = 5126 74// bufferView.target. The NXMSH2 path omitted these and the Khronos validator raised 75// BUFFER_VIEW_TARGET_MISSING four times on every file it wrote -- a hint, not an error, but a hint the 76// sibling emitter nx_gltf_export has always satisfied. Named rather than spelled as bare 34962/34963. 77const MG_GL_ARRAY_BUFFER: i64 = 34962 // vertex attribute data 78const MG_GL_ELEMENT_ARRAY_BUFFER: i64 = 34963 // index data 79const MG_U8_MAX: i64 = 255 // JOINTS_0 may be UNSIGNED_BYTE only while every index fits 80const MG_U16_MAX: i64 = 65535 // UNSIGNED_SHORT ceiling: more joints REFUSES, never wraps 81const MG_F32_EXP_BIAS: i64 = 127 82const MG_F32_MANT_BITS: i64 = 23 83const MG_F32_SIGN_SHIFT: i64 = 31 84const MG_VEC3: i64 = 3 85const MG_VEC4: i64 = 4 86const MG_F32_BYTES: i64 = 4 87const MG_U32_BYTES: i64 = 4 88const MG_U16_BYTES: i64 = 2 89const MG_U8_BYTES: i64 = 1 90const MG_MAT4_BYTES: i64 = 64 91const MG_GLB_HDR_BYTES: i64 = 12 92const MG_CHUNK_HDR_BYTES: i64 = 8 93const MG_GLB_VERSION: i64 = 2 94const MG_JSON_BASE: i64 = 4096 // the JSON that does not scale with joint count 95const MG_JSON_PER_JOINT: i64 = 128 // worst case measured 105 B: a node (name + 3 translations of 96 // 14 chars + children) + its entry in a parent's children 97 // list + its entry in skins.joints; 128 leaves slack 98const MG_PERMILLE: i64 = 1000 99const MG_ASCII_ZERO: i64 = 48 100const MG_ASCII_MINUS: i64 = 45 101const MG_ASCII_DOT: i64 = 46 102const MG_ASCII_SPACE: i64 = 32 103const MG_DECIMAL: i64 = 10 104const MG_EXIT_REFUSE: i64 = 3 // the exit the NXMSH2 path already uses for a refused input 105const MG_EXIT_BUDGET: i64 = 4 106const MG_EXIT_IO: i64 = 6 107const MG_MAGIC_32767: i64 = 32767 108const MG_MAGIC_65535: i64 = 65535 109const MG_MAGIC_100000: i64 = 100000 110// ===== NO SILENT CAPS (2026-08-22) ===== 111// This organ carried THREE caps and all three are gone: 112// MG_CAP 33554432 the NXMSH2 path re-read the file through a 32 MiB buffer and STOPPED SILENTLY at 113// the cap, then refused an INTACT mesh past it as "truncated triangle records" -- the 114// right exit for the wrong reason (a prefix read published as the whole file). 115// MG_MAXTRI 400000 guarded no fixed structure: every buffer below was already sized from nt. 116// MG_OCAP 25165824 refused loudly, but was a picked ceiling on an output whose size is computable. 117// The input is read ONCE by sys_read_file (it sizes its buffer from the file and cannot short-read) in 118// mg_dispatch and handed to BOTH container paths; every output buffer is DERIVED from the header counts, 119// and the derivation is announced in the receipt as out_cap_derived / out_used. A buffer cap is not a 120// number to tune -- raising it only moves the guess. 121const MG_MSH_FIXED_HDR_BYTES: i64 = 16 // magic[8] + u32 nlay + u32 ntri: the bytes needed to read the counts 122const MG_MSH_LAYROW_BYTES: i64 = 24 123const MG_MSH_TRIREC_BYTES: i64 = 84 // 9 position + 9 normal + 3 colour float32 124// NXMSH2 COLOUR CONVENTION, MEASURED 2026-08-26: the field holds a float32 in 0..1 -- a per-mille value 125// already divided by MG_PERMILLE at the writer. Three witnesses agree and this file agreed with none of 126// them: nx_meshview's reader (mv_f32(w,1000)*255/1000, whose own comment names the convention), the 127// md_paint writer (md_enc(r,1000)), and the bg_f32(c,1000) writer that comment cites. See mg_col_u8. 128const MG_COL_DARK_SUM: i64 = 12 // at or below this the source carried no colour at all 129const MG_BONE_R: i64 = 216 // the meshview bone fallback, inherited deliberately so that 130const MG_BONE_G: i64 = 210 // a genuinely uncoloured mesh is never rendered invisible 131const MG_BONE_B: i64 = 198 132const MG_ALLOC_SLACK_BYTES: i64 = 64 // mmap headroom past a DERIVED length; not a budget, not a cap 133const MG_M8388607: i64 = 8388607 134const MG_M8388608: i64 = 8388608 135const MG_BIG: i64 = 4611686018427387903 136 137func hw(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 138func pn(v: i64) -> i64 { let b: *u8=sys_mmap(32) as *u8; var x: i64=v; var ng: i64=0; if x<0{ng=1;x=0-x} var i: i64=31; if x==0{b[i]=48 as u8;i=i-1} while x>0{b[i]=(48+x%10) as u8;x=x/10;i=i-1} if ng==1{b[i]=45 as u8;i=i-1} sys_write(1,(b as i64+i+1) as *u8,31-i); return 0 } 139func mg_u32(b: *u8, o: i64) -> i64 { return (b[o] as i64) + ((b[o+1] as i64)<<8) + ((b[o+2] as i64)<<16) + ((b[o+3] as i64)<<24) } 140func mg_w8(b: *u8, o: i64, v: i64) -> i64 { b[o] = (v&255) as u8; return o+1 } 141func mg_w16(b: *u8, o: i64, v: i64) -> i64 { b[o]=(v&255) as u8; b[o+1]=((v>>8)&255) as u8; return o+2 } 142func mg_w32(b: *u8, o: i64, v: i64) -> i64 { b[o]=(v&255) as u8; b[o+1]=((v>>8)&255) as u8; b[o+2]=((v>>16)&255) as u8; b[o+3]=((v>>24)&255) as u8; return o+4 } 143func mg_align4(x: i64) -> i64 { return (x+3)/4*4 } 144func mg_cat(o: *u8, at: i64, s: *u8) -> i64 { var i: i64=0; var a: i64=at; while s[i]!=(0 as u8){o[a]=s[i]; a=a+1; i=i+1} return a } 145func mg_num(o: *u8, at: i64, v: i64) -> i64 { 146 let b: *u8 = sys_mmap(32) as *u8 147 var x: i64 = v 148 var ng: i64 = 0 149 if x < 0 { ng = 1; x = 0-x } 150 var i: i64 = 31 151 if x == 0 { b[i]=48 as u8; i=i-1 } 152 while x > 0 { b[i]=(48+x%10) as u8; x=x/10; i=i-1 } 153 if ng == 1 { b[i]=45 as u8; i=i-1 } 154 var a: i64 = at 155 var j: i64 = i+1 156 while j <= 31 { o[a] = b[j]; a=a+1; j=j+1 } 157 return a 158} 159// f32 bits -> scaled integer (value * scale), for min/max compare and permille decode 160func mg_f32i(w: i64, scale: i64) -> i64 { 161 let sign: i64 = (w >> 31) & 1 162 let expo: i64 = (w >> 23) & 255 163 if expo == 0 { return 0 } 164 var mant: i64 = (w & MG_M8388607) | MG_M8388608 165 let sh: i64 = expo - 127 166 var v: i64 = 0 167 if sh >= 23 { if sh - 23 > 30 { return 0 } } 168 if sh >= 23 { v = mant * scale * (1 << (sh - 23)) } 169 if sh < 23 { if 23 - sh > 62 { return 0 } } 170 if sh < 23 { v = (mant * scale) >> (23 - sh) } 171 if sign == 1 { return 0 - v } 172 return v 173} 174// ONE owner for decoding an NXMSH2 colour channel into a glTF COLOR_0 byte. 175// THE DEFECT THIS REPLACES, PROVEN BY CONTROL 2026-08-26 rather than by reading: the three call sites 176// computed `mg_f32i(w, 255) / 1000`. mg_f32i ALREADY returns value*scale, so for the canonical stored 177// 0.9 that is (0.9*255)/1000 = 0 -- and zero on all three channels then trips the bone fallback below. 178// Every painted deviation heatmap therefore emitted as UNIFORM BONE while this organ printed the note 179// "per-tri colour carried -- a painted HEATMAP stays a heatmap in the interactive viewer". 180// THE CONTROL: the .glb built from a fully-painted 120,704-triangle heatmap was BYTE-IDENTICAL 181// (sha256 fd3915b8d655f38f...) to the .glb built from the SAME mesh unpainted. Not lossy -- TOTAL, and 182// the fallback made the loss look like a design decision, which is why it survived unnoticed. 183// COMPOSED, not re-typed: nx_nxmesh_lib owns the NXMSH2 colour codec for the whole estate. Spelling 184// the arithmetic here a second time is precisely how this organ came to disagree with the other three. 185func mg_col_u8(w: i64) -> i64 { return nm_col_u8_of(w) } 186func mg_refuse(reason: *u8) -> i64 { hw("MESH2GLB REFUSED: " as *u8); hw(reason); hw("\n" as *u8); return 0 } 187func mg_nxa_refuse(reason: *u8) -> i64 { hw("MESH2GLB REFUSED (NXANIM01): " as *u8); hw(reason); hw("\n" as *u8); return 0 } 188 189// f32 bits of the rational v/den, integer-only. Generalises st_enc1000 (now its /1000 case) so there is 190// ONE encoder in this file. EXACT for every dyadic rational that fits 23 mantissa bits -- which is why 191// q12 weights survive the trip bit-for-bit and the gate can assert a DERIVED tolerance of zero. 192func mg_f32_frac(v: i64, den: i64) -> i64 { 193 if v == 0 { return 0 } 194 var neg: i64 = 0 195 var m: i64 = v 196 if m < 0 { neg = 1; m = 0-m } 197 var e: i64 = 0 198 var num: i64 = m 199 var d: i64 = den 200 while num >= d*2 { d = d*2; e = e+1 } 201 while num < d { num = num*2; e = e-1 } 202 let frac: i64 = ((num - d)*MG_M8388608)/d 203 var bits: i64 = ((e+MG_F32_EXP_BIAS) << MG_F32_MANT_BITS) | (frac & MG_M8388607) 204 if neg == 1 { bits = bits | (1<<MG_F32_SIGN_SHIFT) } 205 return bits 206} 207// decimal text of num/den with exactly as many fraction digits as den has zeros (den = 10^k). glTF 208// JSON min/max and node translations are real numbers in metres; mg_num prints integers only. 209func mg_dec(o: *u8, at: i64, num: i64, den: i64) -> i64 { 210 var a: i64 = at 211 var n: i64 = num 212 if n < 0 { o[a] = MG_ASCII_MINUS as u8; a = a+1; n = 0-n } 213 a = mg_num(o, a, n/den) 214 var digits: i64 = 0 215 var dd: i64 = den 216 while dd > 1 { dd = dd/MG_DECIMAL; digits = digits+1 } 217 if digits > 0 { 218 o[a] = MG_ASCII_DOT as u8 219 a = a+1 220 var f: i64 = n - (n/den)*den 221 var p: i64 = den/MG_DECIMAL 222 while p >= 1 { 223 o[a] = (MG_ASCII_ZERO + f/p) as u8 224 a = a+1 225 f = f - (f/p)*p 226 p = p/MG_DECIMAL 227 } 228 } 229 return a 230} 231// sniff the container from bytes already read. NXANIM01 is an exact 8-byte check and is tested FIRST 232// because both containers begin with 'N' -- one byte cannot tell them apart. 233func mg_container(b: *u8, n: i64) -> i64 { 234 if n >= MG_NXA_HDR_BYTES { let h: *i64 = b as *i64; if h[0] == nxa_magic() { return MG_CONTAINER_NXA } } 235 return MG_CONTAINER_NXMSH2 236} 237// word length of section `tag` from the TOC (nxa_find has already verified the TOC checksum), or -1 238func mg_nxa_wl(b: *u8, tag: i64) -> i64 { 239 let h: *i64 = b as *i64 240 let ns: i64 = h[2] 241 let tb: *i64 = ((b as i64) + MG_NXA_HDR_BYTES) as *i64 242 var s: i64 = 0 243 while s < ns { if tb[s*MG_NXA_TOC_WORDS] == tag { return tb[s*MG_NXA_TOC_WORDS+2] } s = s + 1 } 244 return 0-1 245} 246func mg_ibm_translate(bin: *u8, at0: i64, tx: i64, ty: i64, tz: i64) -> i64 { 247 // column-major translate(-t): three identity columns, then the translation column 248 var at: i64 = at0 249 let one: i64 = mg_f32_frac(1, 1) 250 at = mg_w32(bin, at, one); at = mg_w32(bin, at, 0); at = mg_w32(bin, at, 0); at = mg_w32(bin, at, 0) 251 at = mg_w32(bin, at, 0); at = mg_w32(bin, at, one); at = mg_w32(bin, at, 0); at = mg_w32(bin, at, 0) 252 at = mg_w32(bin, at, 0); at = mg_w32(bin, at, 0); at = mg_w32(bin, at, one); at = mg_w32(bin, at, 0) 253 at = mg_w32(bin, at, mg_f32_frac(0-tx, MG_NXA_UNITS_PER_M)) 254 at = mg_w32(bin, at, mg_f32_frac(0-ty, MG_NXA_UNITS_PER_M)) 255 at = mg_w32(bin, at, mg_f32_frac(0-tz, MG_NXA_UNITS_PER_M)) 256 at = mg_w32(bin, at, one) 257 return at 258} 259func mg_convert_nxa(b: *u8, n: i64, outp: *u8) -> i64 { 260 let wv: i64 = nxa_find(b, n, nxa_tag4("VERT" as *u8)) 261 if wv == 0-2 { mg_nxa_refuse("future NXA version -- refusing" as *u8); return MG_EXIT_REFUSE } 262 if wv == 0-3 { mg_nxa_refuse("corrupt NXA -- TOC or VERT payload checksum failed (truncated?)" as *u8); return MG_EXIT_REFUSE } 263 if wv < 0 { mg_nxa_refuse("no VERT section" as *u8); return MG_EXIT_REFUSE } 264 let wt: i64 = nxa_find(b, n, nxa_tag4("TRIS" as *u8)) 265 if wt == 0-3 { mg_nxa_refuse("corrupt NXA -- TRIS payload checksum failed" as *u8); return MG_EXIT_REFUSE } 266 if wt < 0 { mg_nxa_refuse("no TRIS section" as *u8); return MG_EXIT_REFUSE } 267 let ws: i64 = nxa_find(b, n, nxa_tag4("SKEL" as *u8)) 268 let wk: i64 = nxa_find(b, n, nxa_tag4("SKIN" as *u8)) 269 if ws == 0-3 { mg_nxa_refuse("corrupt NXA -- SKEL payload checksum failed" as *u8); return MG_EXIT_REFUSE } 270 if wk == 0-3 { mg_nxa_refuse("corrupt NXA -- SKIN payload checksum failed" as *u8); return MG_EXIT_REFUSE } 271 // a rig is SKEL AND SKIN together; one without the other is a half-rig and is named as such 272 var skinned: i64 = 0 273 if ws >= 0 { if wk >= 0 { skinned = 1 } } 274 if ws >= 0 { if wk < 0 { mg_nxa_refuse("SKEL without SKIN -- a skeleton nothing is bound to" as *u8); return MG_EXIT_REFUSE } } 275 if wk >= 0 { if ws < 0 { mg_nxa_refuse("SKIN without SKEL -- weights bound to no skeleton" as *u8); return MG_EXIT_REFUSE } } 276 let w: *i64 = b as *i64 277 let nv: i64 = w[wv] 278 let nt: i64 = w[wt] 279 if nv <= 0 { mg_nxa_refuse("VERT count is zero" as *u8); return MG_EXIT_REFUSE } 280 if nt <= 0 { mg_nxa_refuse("no triangles" as *u8); return MG_EXIT_REFUSE } 281 // nxa_find proved each payload fits the file; prove the COUNTS fit their payloads -- a count larger 282 // than its own section would read the next section as geometry and call it a mesh 283 if 1 + nv*MG_VEC3 > mg_nxa_wl(b, nxa_tag4("VERT" as *u8)) { mg_nxa_refuse("VERT count exceeds its section" as *u8); return MG_EXIT_REFUSE } 284 if 1 + nt*MG_VEC3 > mg_nxa_wl(b, nxa_tag4("TRIS" as *u8)) { mg_nxa_refuse("TRIS count exceeds its section" as *u8); return MG_EXIT_REFUSE } 285 var i: i64 = 0 286 while i < nt*MG_VEC3 { 287 let ix: i64 = w[wt+1+i] 288 if ix < 0 { mg_nxa_refuse("TRIS index out of range" as *u8); return MG_EXIT_REFUSE } 289 if ix >= nv { mg_nxa_refuse("TRIS index out of range" as *u8); return MG_EXIT_REFUSE } 290 i = i + 1 291 } 292 var nj: i64 = 0 293 var nroots: i64 = 0 294 if skinned == 1 { 295 nj = w[ws] 296 if nj <= 0 { mg_nxa_refuse("SKEL count is zero" as *u8); return MG_EXIT_REFUSE } 297 if nj > MG_U16_MAX { mg_nxa_refuse("more joints than UNSIGNED_SHORT can index -- refusing, never wrapping" as *u8); return MG_EXIT_REFUSE } 298 if 1 + nj*MG_SKEL_REC_WORDS > mg_nxa_wl(b, nxa_tag4("SKEL" as *u8)) { mg_nxa_refuse("SKEL count exceeds its section" as *u8); return MG_EXIT_REFUSE } 299 if w[wk] != nv { mg_nxa_refuse("SKIN count != VERT count" as *u8); return MG_EXIT_REFUSE } 300 if 1 + nv*MG_SKIN_REC_WORDS > mg_nxa_wl(b, nxa_tag4("SKIN" as *u8)) { mg_nxa_refuse("SKIN count exceeds its section" as *u8); return MG_EXIT_REFUSE } 301 var j: i64 = 0 302 while j < nj { 303 let p: i64 = w[ws+1+j*MG_SKEL_REC_WORDS+MG_SKEL_PARENT] 304 if p == MG_SKEL_ROOT { nroots = nroots + 1 } else { 305 if p < 0 { mg_nxa_refuse("SKEL parent out of range" as *u8); return MG_EXIT_REFUSE } 306 if p >= nj { mg_nxa_refuse("SKEL parent out of range" as *u8); return MG_EXIT_REFUSE } 307 if p == j { mg_nxa_refuse("SKEL joint is its own parent" as *u8); return MG_EXIT_REFUSE } 308 } 309 j = j + 1 310 } 311 if nroots == 0 { mg_nxa_refuse("SKEL has no root joint" as *u8); return MG_EXIT_REFUSE } 312 var v: i64 = 0 313 while v < nv { 314 var s: i64 = 0 315 while s < MG_VEC4 { 316 let ji: i64 = w[wk+1+v*MG_SKIN_REC_WORDS+s] 317 if ji < 0 { mg_nxa_refuse("SKIN joint index out of range" as *u8); return MG_EXIT_REFUSE } 318 if ji >= nj { mg_nxa_refuse("SKIN joint index out of range" as *u8); return MG_EXIT_REFUSE } 319 s = s + 1 320 } 321 v = v + 1 322 } 323 } 324 // ---- BIN layout (every block a multiple of 4 bytes, so every accessor offset is aligned) ---- 325 var jcomp: i64 = MG_GL_UNSIGNED_BYTE 326 var jsz: i64 = MG_U8_BYTES 327 if nj > MG_U8_MAX { jcomp = MG_GL_UNSIGNED_SHORT; jsz = MG_U16_BYTES } 328 let posLen: i64 = nv*MG_VEC3*MG_F32_BYTES 329 let joOff: i64 = posLen 330 var joLen: i64 = 0 331 var weLen: i64 = 0 332 var ibmLen: i64 = 0 333 if skinned == 1 { joLen = nv*MG_VEC4*jsz; weLen = nv*MG_VEC4*MG_F32_BYTES; ibmLen = nj*MG_MAT4_BYTES } 334 let weOff: i64 = joOff + joLen 335 let idxOff: i64 = weOff + weLen 336 let idxLen: i64 = nt*MG_VEC3*MG_U32_BYTES 337 let ibmOff: i64 = idxOff + idxLen 338 let binLen: i64 = mg_align4(ibmOff + ibmLen) 339 let jcap: i64 = MG_JSON_BASE + nj*MG_JSON_PER_JOINT 340 // DERIVED output capacity: GLB header + two chunk headers + the JSON's own derived budget (aligned) + BIN. 341 // jj <= jcap is asserted below, so the assembled total <= out_cap - slack BY CONSTRUCTION. No picked ceiling. 342 let out_cap: i64 = MG_GLB_HDR_BYTES + MG_CHUNK_HDR_BYTES + mg_align4(jcap) + MG_CHUNK_HDR_BYTES + binLen + MG_ALLOC_SLACK_BYTES 343 let bin: *u8 = sys_mmap(binLen + MG_ALLOC_SLACK_BYTES) 344 let mn: *i64 = sys_mmap(MG_VEC3*8) as *i64 345 let mx: *i64 = sys_mmap(MG_VEC3*8) as *i64 346 var ax0: i64 = 0 347 while ax0 < MG_VEC3 { mn[ax0] = MG_BIG; mx[ax0] = 0-MG_BIG; ax0 = ax0 + 1 } 348 var at: i64 = 0 349 var v2: i64 = 0 350 while v2 < nv { 351 var ax: i64 = 0 352 while ax < MG_VEC3 { 353 let u: i64 = w[wv+1+v2*MG_VEC3+ax] 354 if u < mn[ax] { mn[ax] = u } 355 if u > mx[ax] { mx[ax] = u } 356 at = mg_w32(bin, at, mg_f32_frac(u, MG_NXA_UNITS_PER_M)) 357 ax = ax + 1 358 } 359 v2 = v2 + 1 360 } 361 if skinned == 1 { 362 at = joOff 363 var v3: i64 = 0 364 while v3 < nv { 365 var s3: i64 = 0 366 while s3 < MG_VEC4 { 367 let ji3: i64 = w[wk+1+v3*MG_SKIN_REC_WORDS+s3] 368 if jsz == MG_U8_BYTES { at = mg_w8(bin, at, ji3) } else { at = mg_w16(bin, at, ji3) } 369 s3 = s3 + 1 370 } 371 v3 = v3 + 1 372 } 373 at = weOff 374 var v4: i64 = 0 375 while v4 < nv { 376 var s4: i64 = 0 377 while s4 < MG_VEC4 { 378 at = mg_w32(bin, at, mg_f32_frac(w[wk+1+v4*MG_SKIN_REC_WORDS+MG_SKIN_W0+s4], MG_Q12)) 379 s4 = s4 + 1 380 } 381 v4 = v4 + 1 382 } 383 } 384 at = idxOff 385 var i2: i64 = 0 386 while i2 < nt*MG_VEC3 { at = mg_w32(bin, at, w[wt+1+i2]); i2 = i2 + 1 } 387 if skinned == 1 { 388 at = ibmOff 389 var j2: i64 = 0 390 while j2 < nj { 391 let r: i64 = ws+1+j2*MG_SKEL_REC_WORDS+MG_SKEL_TX 392 at = mg_ibm_translate(bin, at, w[r], w[r+1], w[r+2]) 393 j2 = j2 + 1 394 } 395 } 396 // ---- children lists by counting sort (O(nj), no per-joint rescan) ---- 397 let kidcnt: *i64 = sys_mmap((nj+1)*8) as *i64 398 let kidstart: *i64 = sys_mmap((nj+2)*8) as *i64 399 let kids: *i64 = sys_mmap((nj+1)*8) as *i64 400 let cursor: *i64 = sys_mmap((nj+1)*8) as *i64 401 var firstroot: i64 = 0-1 402 if skinned == 1 { 403 var j3: i64 = 0 404 while j3 < nj { kidcnt[j3] = 0; j3 = j3 + 1 } 405 j3 = 0 406 while j3 < nj { 407 let p3: i64 = w[ws+1+j3*MG_SKEL_REC_WORDS+MG_SKEL_PARENT] 408 if p3 >= 0 { kidcnt[p3] = kidcnt[p3] + 1 } else { if firstroot < 0 { firstroot = j3 } } 409 j3 = j3 + 1 410 } 411 kidstart[0] = 0 412 j3 = 0 413 while j3 < nj { kidstart[j3+1] = kidstart[j3] + kidcnt[j3]; cursor[j3] = kidstart[j3]; j3 = j3 + 1 } 414 j3 = 0 415 while j3 < nj { 416 let p4: i64 = w[ws+1+j3*MG_SKEL_REC_WORDS+MG_SKEL_PARENT] 417 if p4 >= 0 { kids[cursor[p4]] = j3; cursor[p4] = cursor[p4] + 1 } 418 j3 = j3 + 1 419 } 420 } 421 // ---- JSON ---- 422 let json: *u8 = sys_mmap(jcap + MG_ALLOC_SLACK_BYTES) 423 var jj: i64 = 0 424 jj = mg_cat(json, jj, "{\x22asset\x22:{\x22version\x22:\x222.0\x22,\x22generator\x22:\x22nishi nx_mesh2glb nxa\x22},\x22scene\x22:0,\x22scenes\x22:[{\x22nodes\x22:[0" as *u8) 425 if skinned == 1 { 426 var j5: i64 = 0 427 while j5 < nj { 428 if w[ws+1+j5*MG_SKEL_REC_WORDS+MG_SKEL_PARENT] == MG_SKEL_ROOT { jj = mg_cat(json, jj, "," as *u8); jj = mg_num(json, jj, j5+1) } 429 j5 = j5 + 1 430 } 431 } 432 jj = mg_cat(json, jj, "]}],\x22nodes\x22:[" as *u8) 433 if skinned == 1 { 434 jj = mg_cat(json, jj, "{\x22mesh\x22:0,\x22skin\x22:0}" as *u8) 435 var j6: i64 = 0 436 while j6 < nj { 437 // the per-joint slack is asserted BEFORE each node so the buffer can never be overrun 438 if jj + MG_JSON_PER_JOINT > jcap { mg_nxa_refuse("JSON exceeded its derived budget" as *u8); return MG_EXIT_BUDGET } 439 let r6: i64 = ws+1+j6*MG_SKEL_REC_WORDS 440 let p6: i64 = w[r6+MG_SKEL_PARENT] 441 var tx: i64 = w[r6+MG_SKEL_TX] 442 var ty: i64 = w[r6+MG_SKEL_TX+1] 443 var tz: i64 = w[r6+MG_SKEL_TX+2] 444 if p6 >= 0 { 445 let rp: i64 = ws+1+p6*MG_SKEL_REC_WORDS+MG_SKEL_TX 446 tx = tx - w[rp]; ty = ty - w[rp+1]; tz = tz - w[rp+2] 447 } 448 jj = mg_cat(json, jj, ",{\x22name\x22:\x22j" as *u8) 449 jj = mg_num(json, jj, j6) 450 jj = mg_cat(json, jj, "\x22,\x22translation\x22:[" as *u8) 451 jj = mg_dec(json, jj, tx, MG_NXA_UNITS_PER_M); jj = mg_cat(json, jj, "," as *u8) 452 jj = mg_dec(json, jj, ty, MG_NXA_UNITS_PER_M); jj = mg_cat(json, jj, "," as *u8) 453 jj = mg_dec(json, jj, tz, MG_NXA_UNITS_PER_M); jj = mg_cat(json, jj, "]" as *u8) 454 if kidcnt[j6] > 0 { 455 jj = mg_cat(json, jj, ",\x22children\x22:[" as *u8) 456 var c6: i64 = kidstart[j6] 457 while c6 < kidstart[j6+1] { 458 if c6 > kidstart[j6] { jj = mg_cat(json, jj, "," as *u8) } 459 jj = mg_num(json, jj, kids[c6] + 1) 460 c6 = c6 + 1 461 } 462 jj = mg_cat(json, jj, "]" as *u8) 463 } 464 jj = mg_cat(json, jj, "}" as *u8) 465 j6 = j6 + 1 466 } 467 jj = mg_cat(json, jj, "],\x22skins\x22:[{\x22inverseBindMatrices\x22:4,\x22skeleton\x22:" as *u8) 468 jj = mg_num(json, jj, firstroot + 1) 469 jj = mg_cat(json, jj, ",\x22joints\x22:[" as *u8) 470 var j7: i64 = 0 471 while j7 < nj { if j7 > 0 { jj = mg_cat(json, jj, "," as *u8) } jj = mg_num(json, jj, j7+1); j7 = j7 + 1 } 472 jj = mg_cat(json, jj, "]}]," as *u8) 473 jj = mg_cat(json, jj, "\x22meshes\x22:[{\x22primitives\x22:[{\x22attributes\x22:{\x22POSITION\x22:0,\x22JOINTS_0\x22:1,\x22WEIGHTS_0\x22:2},\x22indices\x22:3,\x22material\x22:0}]}]," as *u8) 474 } else { 475 jj = mg_cat(json, jj, "{\x22mesh\x22:0}]," as *u8) 476 jj = mg_cat(json, jj, "\x22meshes\x22:[{\x22primitives\x22:[{\x22attributes\x22:{\x22POSITION\x22:0},\x22indices\x22:1,\x22material\x22:0}]}]," as *u8) 477 } 478 jj = mg_cat(json, jj, "\x22materials\x22:[{\x22pbrMetallicRoughness\x22:{\x22metallicFactor\x22:0,\x22roughnessFactor\x22:1},\x22doubleSided\x22:true}]," as *u8) 479 jj = mg_cat(json, jj, "\x22accessors\x22:[{\x22bufferView\x22:0,\x22componentType\x22:" as *u8) 480 jj = mg_num(json, jj, MG_GL_FLOAT) 481 jj = mg_cat(json, jj, ",\x22count\x22:" as *u8) 482 jj = mg_num(json, jj, nv) 483 jj = mg_cat(json, jj, ",\x22type\x22:\x22VEC3\x22,\x22min\x22:[" as *u8) 484 jj = mg_dec(json, jj, mn[0], MG_NXA_UNITS_PER_M); jj = mg_cat(json, jj, "," as *u8) 485 jj = mg_dec(json, jj, mn[1], MG_NXA_UNITS_PER_M); jj = mg_cat(json, jj, "," as *u8) 486 jj = mg_dec(json, jj, mn[2], MG_NXA_UNITS_PER_M); jj = mg_cat(json, jj, "],\x22max\x22:[" as *u8) 487 jj = mg_dec(json, jj, mx[0], MG_NXA_UNITS_PER_M); jj = mg_cat(json, jj, "," as *u8) 488 jj = mg_dec(json, jj, mx[1], MG_NXA_UNITS_PER_M); jj = mg_cat(json, jj, "," as *u8) 489 jj = mg_dec(json, jj, mx[2], MG_NXA_UNITS_PER_M); jj = mg_cat(json, jj, "]}" as *u8) 490 if skinned == 1 { 491 jj = mg_cat(json, jj, ",{\x22bufferView\x22:1,\x22componentType\x22:" as *u8) 492 jj = mg_num(json, jj, jcomp) 493 jj = mg_cat(json, jj, ",\x22count\x22:" as *u8) 494 jj = mg_num(json, jj, nv) 495 jj = mg_cat(json, jj, ",\x22type\x22:\x22VEC4\x22},{\x22bufferView\x22:2,\x22componentType\x22:" as *u8) 496 jj = mg_num(json, jj, MG_GL_FLOAT) 497 jj = mg_cat(json, jj, ",\x22count\x22:" as *u8) 498 jj = mg_num(json, jj, nv) 499 jj = mg_cat(json, jj, ",\x22type\x22:\x22VEC4\x22},{\x22bufferView\x22:3,\x22componentType\x22:" as *u8) 500 jj = mg_num(json, jj, MG_GL_UNSIGNED_INT) 501 jj = mg_cat(json, jj, ",\x22count\x22:" as *u8) 502 jj = mg_num(json, jj, nt*MG_VEC3) 503 jj = mg_cat(json, jj, ",\x22type\x22:\x22SCALAR\x22},{\x22bufferView\x22:4,\x22componentType\x22:" as *u8) 504 jj = mg_num(json, jj, MG_GL_FLOAT) 505 jj = mg_cat(json, jj, ",\x22count\x22:" as *u8) 506 jj = mg_num(json, jj, nj) 507 jj = mg_cat(json, jj, ",\x22type\x22:\x22MAT4\x22}]," as *u8) 508 jj = mg_cat(json, jj, "\x22bufferViews\x22:[{\x22buffer\x22:0,\x22byteOffset\x22:0,\x22byteLength\x22:" as *u8) 509 jj = mg_num(json, jj, posLen) 510 jj = mg_cat(json, jj, "},{\x22buffer\x22:0,\x22byteOffset\x22:" as *u8) 511 jj = mg_num(json, jj, joOff) 512 jj = mg_cat(json, jj, ",\x22byteLength\x22:" as *u8) 513 jj = mg_num(json, jj, joLen) 514 jj = mg_cat(json, jj, "},{\x22buffer\x22:0,\x22byteOffset\x22:" as *u8) 515 jj = mg_num(json, jj, weOff) 516 jj = mg_cat(json, jj, ",\x22byteLength\x22:" as *u8) 517 jj = mg_num(json, jj, weLen) 518 jj = mg_cat(json, jj, "},{\x22buffer\x22:0,\x22byteOffset\x22:" as *u8) 519 jj = mg_num(json, jj, idxOff) 520 jj = mg_cat(json, jj, ",\x22byteLength\x22:" as *u8) 521 jj = mg_num(json, jj, idxLen) 522 jj = mg_cat(json, jj, "},{\x22buffer\x22:0,\x22byteOffset\x22:" as *u8) 523 jj = mg_num(json, jj, ibmOff) 524 jj = mg_cat(json, jj, ",\x22byteLength\x22:" as *u8) 525 jj = mg_num(json, jj, ibmLen) 526 jj = mg_cat(json, jj, "}]," as *u8) 527 } else { 528 jj = mg_cat(json, jj, ",{\x22bufferView\x22:1,\x22componentType\x22:" as *u8) 529 jj = mg_num(json, jj, MG_GL_UNSIGNED_INT) 530 jj = mg_cat(json, jj, ",\x22count\x22:" as *u8) 531 jj = mg_num(json, jj, nt*MG_VEC3) 532 jj = mg_cat(json, jj, ",\x22type\x22:\x22SCALAR\x22}]," as *u8) 533 jj = mg_cat(json, jj, "\x22bufferViews\x22:[{\x22buffer\x22:0,\x22byteOffset\x22:0,\x22byteLength\x22:" as *u8) 534 jj = mg_num(json, jj, posLen) 535 jj = mg_cat(json, jj, "},{\x22buffer\x22:0,\x22byteOffset\x22:" as *u8) 536 jj = mg_num(json, jj, idxOff) 537 jj = mg_cat(json, jj, ",\x22byteLength\x22:" as *u8) 538 jj = mg_num(json, jj, idxLen) 539 jj = mg_cat(json, jj, "}]," as *u8) 540 } 541 jj = mg_cat(json, jj, "\x22buffers\x22:[{\x22byteLength\x22:" as *u8) 542 jj = mg_num(json, jj, binLen) 543 jj = mg_cat(json, jj, "}]}" as *u8) 544 if jj > jcap { mg_nxa_refuse("JSON exceeded its derived budget" as *u8); return MG_EXIT_BUDGET } 545 let jsonPad: i64 = mg_align4(jj) 546 // ---- GLB assembly (identical shape to the NXMSH2 path) ---- 547 let total: i64 = MG_GLB_HDR_BYTES + MG_CHUNK_HDR_BYTES + jsonPad + MG_CHUNK_HDR_BYTES + binLen 548 let out: *u8 = sys_mmap(out_cap) 549 var o: i64 = 0 550 o = mg_cat(out, o, "glTF" as *u8) 551 o = mg_w32(out, o, MG_GLB_VERSION) 552 o = mg_w32(out, o, total) 553 o = mg_w32(out, o, jsonPad) 554 o = mg_cat(out, o, "JSON" as *u8) 555 var q: i64 = 0 556 while q < jj { out[o] = json[q]; o = o + 1; q = q + 1 } 557 while q < jsonPad { out[o] = MG_ASCII_SPACE as u8; o = o + 1; q = q + 1 } 558 o = mg_w32(out, o, binLen) 559 o = mg_cat(out, o, "BIN" as *u8) 560 o = mg_w8(out, o, 0) 561 q = 0 562 while q < binLen { out[o] = bin[q]; o = o + 1; q = q + 1 } 563 let ofd: i64 = sys_openat_wr(outp, MODE_0644) 564 if ofd < 0 { mg_nxa_refuse("output unwritable" as *u8); return MG_EXIT_IO } 565 sys_write(ofd, out, o) 566 sys_close(ofd) 567 hw("{\x22organ\x22:\x22nx_mesh2glb\x22,\x22container\x22:\x22NXANIM01\x22,\x22verts\x22:" as *u8); pn(nv) 568 hw(",\x22tris\x22:" as *u8); pn(nt) 569 hw(",\x22joints\x22:" as *u8); pn(nj) 570 hw(",\x22roots\x22:" as *u8); pn(nroots) 571 hw(",\x22skinned\x22:" as *u8); pn(skinned) 572 hw(",\x22joints_ctype\x22:" as *u8); pn(jcomp) 573 hw(",\x22glb_bytes\x22:" as *u8); pn(o) 574 hw(",\x22out_cap_derived\x22:" as *u8); pn(out_cap) 575 hw(",\x22out_used\x22:" as *u8); pn(o) 576 hw(",\x22units\x22:\x22m (0.01mm x 1/100000)\x22,\x22bind_rotation\x22:\x22dropped-identity-convention\x22,\x22normals\x22:\x22omitted (spec: client computes flat normals)\x22}\n" as *u8) 577 return 0 578} 579// ONE entry, two containers. The file is read once by sys_read_file (sizes itself from the file, 580// cannot short-read), sniffed, and dispatched. The NXMSH2 path is byte-for-byte the incumbent's. 581// THE EXPORT DOOR READS THE PROVENANCE VERDICT IN-PROCESS (modding MD9, 2026-09-06). The input's own sha256 is looked up in 582// the provenance journal through nx_asset_prov_lib: a row that REFUSES (a licence the rights table lacks, a NO redistribution 583// right) closes this door by name and writes nothing; NO-ROW and NO-JOURNAL mean a house asset (never ingested from outside) 584// and pass; an unreadable rights table ABSTAINS -- announced as UNOBSERVABLE, never read as clearance; REVIEW (a conditional 585// or unread licence) passes with the line printed, because this door is a local export and the publish doors (MD19 to MD23) 586// are where a human decision is required. Private use is never refused. The journal path is data (jrnl=<path>) so a gate can 587// drive this door on its own scratch journal. 588func mg_provenance(b: *u8, n: i64, jrnl: *u8) -> i64 { 589 let sha: *u8 = sys_mmap(PV_SHA_HEX + 1) 590 pv_hash_bytes(b, n, sha) 591 let res: *i64 = sys_mmap(8 * PV_RES_N) as *i64 592 let rc: i64 = pv_verdict(jrnl, sha, res) 593 let reason: i64 = res[PV_RES_REASON] 594 if reason == PV_R_TABLE_UNREADABLE { hw("PROVENANCE UNOBSERVABLE rights-table-unreadable -- this door cannot judge, it does not acquit: " as *u8); pv_print(sha, rc, res); return 0 } 595 if rc == LG_RC_REFUSE { 596 if reason == PV_R_NO_ROW { hw("PROVENANCE house-asset (no row): " as *u8); pv_print(sha, rc, res); return 0 } 597 if reason == PV_R_NO_JOURNAL { hw("PROVENANCE house-asset (no journal): " as *u8); pv_print(sha, rc, res); return 0 } 598 hw("EXPORT-REFUSED provenance: " as *u8); pv_print(sha, rc, res) 599 return 0 - 1 600 } 601 hw("PROVENANCE " as *u8); pv_print(sha, rc, res) 602 return 0 603} 604func mg_dispatch(inp: *u8, outp: *u8, jrnl: *u8) -> i64 { 605 let lp: *i64 = sys_mmap(16) as *i64 606 let b: *u8 = sys_read_file(inp, lp) 607 if (b as i64) == 0 { mg_refuse("input unreadable" as *u8); return MG_EXIT_REFUSE } 608 if mg_provenance(b, lp[0], jrnl) < 0 { return MG_EXIT_REFUSE } 609 if mg_container(b, lp[0]) == MG_CONTAINER_NXA { return mg_convert_nxa(b, lp[0], outp) } 610 return mg_convert_msh(b, lp[0], outp) 611} 612 613// path entry kept for the selftest; the dispatcher hands the already-read whole-file buffer to mg_convert_msh 614func mg_convert(inp: *u8, outp: *u8) -> i64 { 615 let lp: *i64 = sys_mmap(16) as *i64 616 let b: *u8 = sys_read_file(inp, lp) 617 if (b as i64) == 0 { mg_refuse("input unreadable" as *u8); return 3 } 618 return mg_convert_msh(b, lp[0], outp) 619} 620// NXMSH2 -> glb over a WHOLE-FILE buffer. The incumbent re-read the file here through a 32 MiB cap and 621// stopped silently, so an intact mesh past the cap was refused as "truncated triangle records" -- the right 622// exit for the wrong reason. The size check below is now against the FILE, never a cap. 623func mg_convert_msh(b: *u8, n: i64, outp: *u8) -> i64 { 624 if n < MG_MSH_FIXED_HDR_BYTES { mg_refuse("too small for NXMSH2" as *u8); return 3 } 625 if b[0] != (78 as u8) { mg_refuse("not NXMSH2" as *u8); return 3 } 626 if b[5] != (50 as u8) { mg_refuse("not NXMSH2 v2" as *u8); return 3 } 627 let nlay: i64 = mg_u32(b, 8) 628 let nt: i64 = mg_u32(b, 12) 629 if nt <= 0 { mg_refuse("no triangles" as *u8); return 3 } 630 let hdr: i64 = MG_MSH_FIXED_HDR_BYTES + nlay*MG_MSH_LAYROW_BYTES 631 if hdr + nt*MG_MSH_TRIREC_BYTES > n { mg_refuse("truncated triangle records" as *u8); return 3 } 632 let nv: i64 = nt*3 633 let ni: i64 = nt*3 634 // ---- BIN layout ---- 635 let posLen: i64 = nv*12 636 let nrmOff: i64 = posLen 637 let nrmLen: i64 = nv*MG_VEC3_F32_BYTES 638 let colOff: i64 = mg_align4(nrmOff + nrmLen) 639 let colLen: i64 = nv*4 640 let idxOff: i64 = colOff + colLen 641 let idxLen: i64 = ni*4 642 let binLen: i64 = mg_align4(idxOff + idxLen) 643 // DERIVED, never budgeted: GLB header + two chunk headers + a JSON of fixed shape (its budget asserted 644 // below) + the BIN computed above. A 1.2M-vertex mesh simply gets a 31 MB buffer; mmap faults pages in 645 // on demand, so headroom costs address space, not resident memory. 646 let out_cap: i64 = MG_GLB_HDR_BYTES + MG_CHUNK_HDR_BYTES + mg_align4(MG_JSON_BASE) + MG_CHUNK_HDR_BYTES + binLen + MG_ALLOC_SLACK_BYTES 647 let bin: *u8 = sys_mmap(binLen + MG_ALLOC_SLACK_BYTES) 648 // track min/max position BITS per axis (compare in decoded um space) 649 let mnb: *i64 = sys_mmap(48) as *i64 650 let mxb: *i64 = sys_mmap(48) as *i64 651 let mnv: *i64 = sys_mmap(48) as *i64 652 let mxv: *i64 = sys_mmap(48) as *i64 653 var a2: i64 = 0 654 while a2 < 3 { mnv[a2] = MG_KEY_ABOVE_ALL; mxv[a2] = MG_KEY_BELOW_ALL; mnb[a2] = 0; mxb[a2] = 0; a2 = a2 + 1 } 655 var t: i64 = 0 656 var at: i64 = 0 657 while t < nt { 658 var c: i64 = 0 659 while c < 3 { 660 var ax: i64 = 0 661 while ax < 3 { 662 let w: i64 = mg_u32(b, hdr + t*84 + c*12 + ax*4) 663 at = mg_w32(bin, at, w) 664 // Compare by MONOTONIC KEY, not by a rounded value. The old line compared mg_f32i(w,1000) 665 // -- milli-units -- so two distinct float32s could TIE and the first seen won, leaving a 666 // declared bound strictly inside the data. That is exactly the ACCESSOR_ELEMENT_OUT_OF_MIN 667 // _BOUND this file shipped: 30 vertices below its own declared minimum. 668 let dv: i64 = gn_f32_key(w) 669 if dv < mnv[ax] { mnv[ax] = dv; mnb[ax] = w } 670 if dv > mxv[ax] { mxv[ax] = dv; mxb[ax] = w } 671 ax = ax + 1 672 } 673 c = c + 1 674 } 675 t = t + 1 676 } 677 // normals: per-vertex f32 permille -> {VEC3, FLOAT}, UNIT. See the header for why this is no longer 678 // normalized i16. The three components are renormalised TOGETHER, so the emitted vector is unit even 679 // when the source's was not -- and a zero source normal becomes the named +Y fallback rather than a 680 // third validator error. 681 let nun: *i64 = sys_mmap(MG_V3 * 8) as *i64 682 t = 0 683 at = nrmOff 684 while t < nt { 685 var c2: i64 = 0 686 while c2 < 3 { 687 let bx: i64 = mg_f32i(mg_u32(b, hdr + t*84 + 36 + c2*12 + 0*4), MG_NRM_GAIN) 688 let by: i64 = mg_f32i(mg_u32(b, hdr + t*84 + 36 + c2*12 + 1*4), MG_NRM_GAIN) 689 let bz: i64 = mg_f32i(mg_u32(b, hdr + t*84 + 36 + c2*12 + 2*4), MG_NRM_GAIN) 690 gn_unit3(bx, by, bz, nun) 691 at = mg_w32(bin, at, gn_f32(nun[0])) 692 at = mg_w32(bin, at, gn_f32(nun[1])) 693 at = mg_w32(bin, at, gn_f32(nun[2])) 694 c2 = c2 + 1 695 } 696 t = t + 1 697 } 698 // colors: per-TRI f32 permille -> u8 x3 verts, alpha 255; zero colour falls back to bone so the 699 // panel is never invisible (the meshview lesson, inherited deliberately) 700 t = 0 701 at = colOff 702 while t < nt { 703 var cr: i64 = mg_col_u8(mg_u32(b, hdr + t*84 + 72)) 704 var cg: i64 = mg_col_u8(mg_u32(b, hdr + t*84 + 76)) 705 var cb: i64 = mg_col_u8(mg_u32(b, hdr + t*84 + 80)) 706 if cr + cg + cb < MG_COL_DARK_SUM { cr = MG_BONE_R; cg = MG_BONE_G; cb = MG_BONE_B } 707 var c3: i64 = 0 708 while c3 < 3 { 709 at = mg_w8(bin, at, cr) 710 at = mg_w8(bin, at, cg) 711 at = mg_w8(bin, at, cb) 712 at = mg_w8(bin, at, 255) 713 c3 = c3 + 1 714 } 715 t = t + 1 716 } 717 // indices 0..nv-1 718 t = 0 719 at = idxOff 720 while t < ni { at = mg_w32(bin, at, t); t = t + 1 } 721 // ---- JSON ---- 722 let json: *u8 = sys_mmap(MG_JSON_BASE + MG_ALLOC_SLACK_BYTES) 723 var jj: i64 = 0 724 jj = mg_cat(json, jj, "{\x22asset\x22:{\x22version\x22:\x222.0\x22,\x22generator\x22:\x22nishi nx_mesh2glb\x22},\x22scene\x22:0,\x22scenes\x22:[{\x22nodes\x22:[0]}],\x22nodes\x22:[{\x22mesh\x22:0}]," as *u8) 725 jj = mg_cat(json, jj, "\x22meshes\x22:[{\x22primitives\x22:[{\x22attributes\x22:{\x22POSITION\x22:0,\x22NORMAL\x22:1,\x22COLOR_0\x22:2},\x22indices\x22:3,\x22material\x22:0}]}]," as *u8) 726 jj = mg_cat(json, jj, "\x22materials\x22:[{\x22pbrMetallicRoughness\x22:{\x22metallicFactor\x22:0,\x22roughnessFactor\x22:1},\x22doubleSided\x22:true}]," as *u8) 727 jj = mg_cat(json, jj, "\x22accessors\x22:[{\x22bufferView\x22:0,\x22componentType\x22:5126,\x22count\x22:" as *u8) 728 jj = mg_num(json, jj, nv) 729 jj = mg_cat(json, jj, ",\x22type\x22:\x22VEC3\x22,\x22min\x22:[" as *u8) 730 // SPEC FIX 2026-08-25. These used to be "min/max as decoded integers (mm truncation of um) -- viewers 731 // use these only for framing". That comment was a GUESS about the format and it was wrong: glTF 2.0 732 // requires POSITION min/max to be the ACTUAL componentwise extremes, and the validator compares them 733 // numerically. Declaring min -444 for data whose true minimum is -444.77398681640625 produced 734 // ACCESSOR_MIN_MISMATCH, three ACCESSOR_MAX_MISMATCH and -- worst -- ACCESSOR_ELEMENT_OUT_OF_MIN_BOUND, 735 // i.e. the file advertised bounds that 30 of its own vertices fell outside. The bounds are now rendered 736 // EXACTLY, from mnb/mxb: the very float32 bit patterns that were written into the BIN chunk. Those two 737 // arrays were already being maintained here and had never been read by anything. 738 jj = gn_dec_f32(mnb[0], json, jj) 739 jj = mg_cat(json, jj, "," as *u8) 740 jj = gn_dec_f32(mnb[1], json, jj) 741 jj = mg_cat(json, jj, "," as *u8) 742 jj = gn_dec_f32(mnb[2], json, jj) 743 jj = mg_cat(json, jj, "],\x22max\x22:[" as *u8) 744 jj = gn_dec_f32(mxb[0], json, jj) 745 jj = mg_cat(json, jj, "," as *u8) 746 jj = gn_dec_f32(mxb[1], json, jj) 747 jj = mg_cat(json, jj, "," as *u8) 748 jj = gn_dec_f32(mxb[2], json, jj) 749 jj = mg_cat(json, jj, "]}," as *u8) 750 jj = mg_cat(json, jj, "{\x22bufferView\x22:1,\x22componentType\x22:5126,\x22count\x22:" as *u8) 751 jj = mg_num(json, jj, nv) 752 jj = mg_cat(json, jj, ",\x22type\x22:\x22VEC3\x22}," as *u8) 753 jj = mg_cat(json, jj, "{\x22bufferView\x22:2,\x22componentType\x22:5121,\x22normalized\x22:true,\x22count\x22:" as *u8) 754 jj = mg_num(json, jj, nv) 755 jj = mg_cat(json, jj, ",\x22type\x22:\x22VEC4\x22}," as *u8) 756 jj = mg_cat(json, jj, "{\x22bufferView\x22:3,\x22componentType\x22:5125,\x22count\x22:" as *u8) 757 jj = mg_num(json, jj, ni) 758 jj = mg_cat(json, jj, ",\x22type\x22:\x22SCALAR\x22}]," as *u8) 759 jj = mg_cat(json, jj, "\x22bufferViews\x22:[{\x22buffer\x22:0,\x22byteOffset\x22:0,\x22byteLength\x22:" as *u8) 760 jj = mg_num(json, jj, posLen) 761 jj = mg_cat(json, jj, ",\x22target\x22:" as *u8) 762 jj = mg_num(json, jj, MG_GL_ARRAY_BUFFER) 763 jj = mg_cat(json, jj, "},{\x22buffer\x22:0,\x22byteOffset\x22:" as *u8) 764 jj = mg_num(json, jj, nrmOff) 765 jj = mg_cat(json, jj, ",\x22byteLength\x22:" as *u8) 766 jj = mg_num(json, jj, nrmLen) 767 jj = mg_cat(json, jj, ",\x22target\x22:" as *u8) 768 jj = mg_num(json, jj, MG_GL_ARRAY_BUFFER) 769 jj = mg_cat(json, jj, "},{\x22buffer\x22:0,\x22byteOffset\x22:" as *u8) 770 jj = mg_num(json, jj, colOff) 771 jj = mg_cat(json, jj, ",\x22byteLength\x22:" as *u8) 772 jj = mg_num(json, jj, colLen) 773 jj = mg_cat(json, jj, ",\x22target\x22:" as *u8) 774 jj = mg_num(json, jj, MG_GL_ARRAY_BUFFER) 775 jj = mg_cat(json, jj, "},{\x22buffer\x22:0,\x22byteOffset\x22:" as *u8) 776 jj = mg_num(json, jj, idxOff) 777 jj = mg_cat(json, jj, ",\x22byteLength\x22:" as *u8) 778 jj = mg_num(json, jj, idxLen) 779 jj = mg_cat(json, jj, ",\x22target\x22:" as *u8) 780 jj = mg_num(json, jj, MG_GL_ELEMENT_ARRAY_BUFFER) 781 jj = mg_cat(json, jj, "}]," as *u8) 782 jj = mg_cat(json, jj, "\x22buffers\x22:[{\x22byteLength\x22:" as *u8) 783 jj = mg_num(json, jj, binLen) 784 jj = mg_cat(json, jj, "}]}" as *u8) 785 if jj > MG_JSON_BASE { mg_refuse("JSON exceeded its derived budget" as *u8); return 4 } 786 let jsonPad: i64 = mg_align4(jj) 787 // ---- GLB assembly ---- 788 let total: i64 = 12 + 8 + jsonPad + 8 + binLen 789 let out: *u8 = sys_mmap(out_cap) 790 var o: i64 = 0 791 o = mg_cat(out, o, "glTF" as *u8) 792 o = mg_w32(out, o, 2) 793 o = mg_w32(out, o, total) 794 o = mg_w32(out, o, jsonPad) 795 o = mg_cat(out, o, "JSON" as *u8) 796 var q: i64 = 0 797 while q < jj { out[o] = json[q]; o = o + 1; q = q + 1 } 798 while q < jsonPad { out[o] = 32 as u8; o = o + 1; q = q + 1 } 799 o = mg_w32(out, o, binLen) 800 o = mg_w8(out, o, 66) 801 o = mg_w8(out, o, 73) 802 o = mg_w8(out, o, 78) 803 o = mg_w8(out, o, 0) 804 q = 0 805 while q < binLen { out[o] = bin[q]; o = o + 1; q = q + 1 } 806 let ofd: i64 = sys_openat_wr(outp, MODE_0644) 807 if ofd < 0 { mg_refuse("output unwritable" as *u8); return 6 } 808 sys_write(ofd, out, o) 809 sys_close(ofd) 810 hw("{\x22organ\x22:\x22nx_mesh2glb\x22,\x22tris\x22:" as *u8); pn(nt) 811 hw(",\x22verts\x22:" as *u8); pn(nv) 812 hw(",\x22glb_bytes\x22:" as *u8); pn(o) 813 hw(",\x22out_cap_derived\x22:" as *u8); pn(out_cap) 814 hw(",\x22out_used\x22:" as *u8); pn(o) 815 hw(",\x22note\x22:\x22positions pass through as raw f32 bits (zero requantisation); per-tri colour carried -- a painted HEATMAP stays a heatmap in the interactive viewer\x22}\n" as *u8) 816 return 0 817} 818 819// ---- teeth (literal-only fixture, nx_cc 1785936860 discipline) ---- 820func st_enc1000(v: i64) -> i64 { return mg_f32_frac(v, MG_PERMILLE) } // the /1000 case of the ONE encoder 821func st_fix(path: *u8) -> i64 { 822 let b: *u8 = sys_mmap(256) 823 b[0]=78 as u8; b[1]=88 as u8; b[2]=77 as u8; b[3]=83 as u8 824 b[4]=72 as u8; b[5]=50 as u8; b[6]=0 as u8; b[7]=0 as u8 825 mg_w32(b, 8, 1) 826 mg_w32(b, 12, 1) 827 var q: i64 = 0 828 while q < 16 { b[16+q] = 0 as u8; q = q + 1 } 829 b[16]=115 as u8 830 mg_w32(b, 32, 0) 831 mg_w32(b, 36, 1) 832 var k: i64 = 0 833 while k < 21 { mg_w32(b, 40 + k*4, 0); k = k + 1 } 834 mg_w32(b, 52, st_enc1000(MG_MAGIC_100000)) 835 mg_w32(b, 68, st_enc1000(MG_MAGIC_100000)) 836 mg_w32(b, 40+72, st_enc1000(900)) 837 mg_w32(b, 40+76, st_enc1000(120)) 838 mg_w32(b, 40+80, st_enc1000(80)) 839 mg_w32(b, 124, 0) 840 let fd: i64 = sys_openat_wr(path, 420) 841 if fd < 0 { return 0 - 1 } 842 sys_write(fd, b, 128) 843 sys_close(fd) 844 return 0 845} 846func mg_hasstr(b: *u8, n: i64, lit: *u8) -> i64 { 847 var m: i64 = 0 848 while lit[m] != (0 as u8) { m = m + 1 } 849 var i: i64 = 0 850 while i + m <= n { 851 var k: i64 = 0 852 var ok: i64 = 1 853 while k < m { if b[i+k] != lit[k] { ok = 0; k = m } else { k = k + 1 } } 854 if ok == 1 { return 1 } 855 i = i + 1 856 } 857 return 0 858} 859func mg_selftest() -> i64 { 860 var fails: i64 = 0 861 st_fix("/tmp/mg_t.nxmesh" as *u8) 862 hw("T0 convert a 1-tri red fixture -> valid glb with COLOR_0:\n" as *u8) 863 if mg_convert("/tmp/mg_t.nxmesh" as *u8, "/tmp/mg_t.glb" as *u8) != 0 { fails = fails + 1; hw("T0 FAIL convert refused\n" as *u8) } else { 864 let vlp: *i64 = sys_mmap(16) as *i64 865 let vb0: *u8 = sys_read_file("/tmp/mg_t.glb" as *u8, vlp) 866 var vb: *u8 = vb0 867 var vn: i64 = 0 868 if (vb0 as i64) != 0 { vn = vlp[0] } else { vb = sys_mmap(16) } 869 var ok: i64 = 1 870 if vn < 100 { ok = 0 } 871 if vb[0] != (103 as u8) { ok = 0 } 872 if mg_u32(vb, 8) != vn { ok = 0 } 873 if mg_hasstr(vb, vn, "COLOR_0" as *u8) == 0 { ok = 0 } 874 if ok == 1 { hw("T0 PASS header+length+COLOR_0\n" as *u8) } else { fails = fails + 1; hw("T0 FAIL glb invalid\n" as *u8) } 875 } 876 hw("T1 absent input must REFUSE:\n" as *u8) 877 if mg_convert("/tmp/mg_absent_zz.nxmesh" as *u8, "/tmp/mg_x.glb" as *u8) == 0 { fails = fails + 1; hw("T1 FAIL\n" as *u8) } else { hw("T1 PASS\n" as *u8) } 878 if fails == 0 { hw("MESH2GLB-SELFTEST GREEN 2/2\n" as *u8); return 0 } 879 hw("MESH2GLB-SELFTEST RED fails=" as *u8); pn(fails); hw("\n" as *u8) 880 return 1 881} 882 883// "selftest" is matched as a WHOLE WORD. The incumbent sniffed ONE BYTE (a1[0] == 's'), so any input 884// path beginning with 's' -- e.g. sites/nishifamily/world/ref9d.nxa, the estate's own shipped asset -- 885// silently ran the selftest instead of converting, and the caller saw a clean exit 0 with no output 886// file. A verb test that matches a prefix is a verb test that matches the wrong subject. 887func mg_is_selftest(a: *u8) -> i64 { 888 let lit: *u8 = "selftest" as *u8 889 var i: i64 = 0 890 while lit[i] != (0 as u8) { if a[i] != lit[i] { return 0 } i = i + 1 } 891 if a[i] != (0 as u8) { return 0 } 892 return 1 893} 894func main(argc: i64, argv: *i64) -> i64 { 895 if argc < 2 { 896 hw("usage: nx_mesh2glb <in.nxmesh|in.nxa> <out.glb> | selftest\n" as *u8) 897 sys_exit(2) 898 return 2 899 } 900 let a1: *u8 = argv[1] as *u8 901 if mg_is_selftest(a1) == 1 { let rc: i64 = mg_selftest(); sys_exit(rc); return rc } 902 if argc < 3 { hw("usage: nx_mesh2glb <in.nxmesh|in.nxa> <out.glb>\n" as *u8); sys_exit(2); return 2 } 903 // jrnl=<path> as an optional third argument: the provenance journal is data, so a gate drives this door on its own 904 var jrnl: *u8 = PV_JRNL_DEFAULT 905 if argc >= 4 { let a3: *u8 = argv[3] as *u8; if a3[0] == (106 as u8) { if a3[1] == (114 as u8) { if a3[2] == (110 as u8) { if a3[3] == (108 as u8) { if a3[4] == (61 as u8) { jrnl = a3 + 5 } } } } } } 906 let rc2: i64 = mg_dispatch(argv[1] as *u8, argv[2] as *u8, jrnl) 907 sys_exit(rc2) 908 return rc2 909}