code wiki / (root) / nx_mesh2glb_neutral_candidate_t346.nx

nx_mesh2glb_neutral_candidate_t346.nx source

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