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}