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