code wiki / _hdl_build / nx_meshmerge.nx
nx_meshmerge.nx source
↩ module page · 403 lines · 19361 B
1// nx_meshmerge.nx -- SPLICE ONE NXMSH2 INTO A NAMED LAYER OF ANOTHER, WITH A PLACEMENT TRANSFORM.
2//
3// ★WHY THIS EXISTS, and it is the measured integration point rather than a guess. `nx_skullsdf` emits a
4// skull that scores headline 73 / detail_head 63 against the 171,248-triangle BodyParts3D oracle, while
5// the LIVE head path (`nx_skullgen`, seven lofted tubes) scores 46 / 14 -- +59% and 4.5x, DARK for weeks.
6// It is dark because nothing could put it INTO the body: `nx_body_gen` consumes a CANON (rules -> rings)
7// and emits three layers, and there was no way to hand it a finished MESH.
8//
9// ★THE AUDIT RAN FIRST (this lane's law) AND IT CHANGED THE DESIGN TWICE.
10// (1) NOT a conversion to the "shared" format: `nx_mesh3` calls itself the ecosystem interchange and is
11// statically capped at 4096 verts / 8192 tris. Our 2mm skull is 48,814 / 97,660 and the oracle is
12// 171,248 tris -- it holds about 5% of ONE skull. The shared format cannot carry this work.
13// (2) NOT a widening of `nx_body_gen`, whose signature is already 24 arguments. Compose, never widen.
14// ⇒ merge INSIDE NXMSH2, in its own organ.
15//
16// ★THE HARD PART IS THE FORMAT'S CONTRACT, NOT THE APPEND. NXMSH2 layers are CONTIGUOUS TRIANGLE RANGES
17// (start,count), so triangles cannot simply be appended -- adding to a middle layer would leave every
18// later layer's range wrong. This regroups ALL triangles by layer and rebuilds the table, which is the
19// same contract `nx_headcrop` already had to honour when it cropped a band.
20//
21// nx_meshmerge <out> <base> <add> <layer> [scale_permil] [tx] [ty] [tz]
22// nx_meshmerge selftest
23// layer = index into the base's layer table (0-based). scale_permil defaults 1000 (identity).
24// license_tier: ORIGINAL expect_exit: 0 No hw writes (Rule 26).
25import "nx_syscalls.nx"
26import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
27const MM_MAGIC_7000: i64 = 7000
28const MM_MAGIC_7001: i64 = 7001
29const MM_MAGIC_3500: i64 = 3500
30const MM_MAGIC_7010: i64 = 7010
31const MM_MAGIC_8388607: i64 = 8388607
32const MM_MAGIC_8388608: i64 = 8388608
33
34const MM_HDRB: i64 = 16 // magic(8) + nlayer(4) + ntri(4)
35const MM_LREC: i64 = 24 // per-layer record: name(16) + start(4) + count(4)
36const MM_TRIB: i64 = 84 // per-triangle: 9 f32 pos + 9 f32 nrm + 3 f32 colour
37const MM_IDXB: i64 = 4 // per-triangle layer id
38const MM_LENSLOT: i64 = 16
39
40// QUIET FLAG so the teeth can call the real merge without its JSON receipt interleaving into the gate
41// output. A static scalar, not a widened signature -- the merge has ONE definition and the teeth exercise
42// THAT one, because a test that calls a copy of the code proves nothing about the shipped path.
43static MM_QUIET: i64
44func mm_puts(s: *u8) -> i64 { if MM_QUIET == 1 { return 0 } var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
45func mm_say(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
46// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
47// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
48// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
49// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
50func mm_sn(v: i64) -> i64 { nxi_out(v); return 0 }
51// MM_QUIET must gate the NUMBER printer too. Gating only mm_puts silenced the receipt's TEXT while its
52// integers still streamed out, so the gate emitted a garbled run of digits between teeth -- a half-muted
53// instrument is its own small lie about what ran.
54// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
55// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
56// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
57// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
58func mm_pn(v: i64) -> i64 { nxi_out(v); return 0 }
59func mm_atoi(s: *u8) -> i64 {
60 var i: i64=0; var n: i64=0; var sg: i64=1
61 if s[0]==(45 as u8) { sg=0-1; i=1 }
62 while s[i]!=(0 as u8) { let c: i64=s[i] as i64; if c>=48 { if c<=57 { n=n*10+(c-48) } } i=i+1 }
63 return n*sg
64}
65func mm_streq(a: *u8, b: *u8) -> i64 {
66 var i: i64=0; var go: i64=1; var eq: i64=1
67 while go==1 { if a[i]!=b[i] { eq=0; go=0 } else { if a[i]==(0 as u8) { go=0 } else { i=i+1 } } }
68 return eq
69}
70func mm_rd32(b: *u8, o: i64) -> i64 {
71 return (b[o] as i64) | ((b[o+1] as i64)<<8) | ((b[o+2] as i64)<<16) | ((b[o+3] as i64)<<24)
72}
73func mm_wr32(b: *u8, o: i64, v: i64) -> i64 {
74 b[o] = (v & 255) as u8
75 b[o+1] = ((v >> 8) & 255) as u8
76 b[o+2] = ((v >> 16) & 255) as u8
77 b[o+3] = ((v >> 24) & 255) as u8
78 return 0
79}
80// IEEE-754 single <-> integer, value = v/scale. Byte-exact for the magnitudes this format carries.
81func mm_r_f32(bits: i64, scale: i64) -> i64 {
82 let s: i64 = (bits >> 31) & 1
83 let e: i64 = (bits >> 23) & 255
84 let m: i64 = bits & MM_MAGIC_8388607
85 if e == 0 { return 0 }
86 if e == 255 { return 0 }
87 let mant: i64 = MM_MAGIC_8388608 | m
88 let sh: i64 = e - 127 - 23
89 var v: i64 = 0
90 if sh >= 0 { if sh > 30 { return 0 } v = (mant*scale) << sh }
91 else { let rs: i64 = 0 - sh; if rs > 62 { return 0 } v = (mant*scale) >> rs }
92 if s == 1 { return 0 - v }
93 return v
94}
95func mm_f32(v: i64, scale: i64) -> i64 {
96 if v == 0 { return 0 }
97 var neg: i64 = 0
98 var m: i64 = v
99 if m < 0 { neg = 1; m = 0 - m }
100 var e: i64 = 127
101 var num: i64 = m
102 var den: i64 = scale
103 while num >= den*2 { den = den*2; e = e + 1 }
104 while num < den { num = num*2; e = e - 1 }
105 let frac: i64 = ((num - den) * MM_MAGIC_8388608) / den
106 var bits: i64 = (e << 23) | (frac & MM_MAGIC_8388607)
107 if neg == 1 { bits = bits | (1 << 31) }
108 return bits
109}
110// ★FAIL-CLOSED HEADER CHECK. A merge that runs on a non-NXMSH2 buffer would read triangle counts out of
111// arbitrary bytes and emit a plausible-looking mesh from garbage, which is the worst possible outcome
112// for a tool whose whole job is to be trusted with someone else's geometry.
113func mm_is_nxmsh2(b: *u8) -> i64 {
114 if b[0]!=(78 as u8) { return 0 }
115 if b[1]!=(88 as u8) { return 0 }
116 if b[2]!=(77 as u8) { return 0 }
117 if b[3]!=(83 as u8) { return 0 }
118 if b[4]!=(72 as u8) { return 0 }
119 if b[5]!=(50 as u8) { return 0 }
120 return 1
121}
122
123// ---- THE MERGE.
124// Regroups every triangle by layer so each layer stays ONE contiguous range, inserting `add`'s triangles
125// at the end of layer `tgt`. Returns the new triangle count, or a negative code.
126func mm_merge(outp: *u8, basep: *u8, addp: *u8, tgt: i64, scl: i64, tx: i64, ty: i64, tz: i64) -> i64 {
127 let bl: *i64 = sys_mmap(MM_LENSLOT) as *i64
128 let B: *u8 = sys_read_file(basep, bl)
129 if (B as i64) == 0 { mm_puts("{\x22error\x22:\x22cannot read base\x22}\n" as *u8); return 0-3 }
130 let al: *i64 = sys_mmap(MM_LENSLOT) as *i64
131 let A: *u8 = sys_read_file(addp, al)
132 if (A as i64) == 0 { mm_puts("{\x22error\x22:\x22cannot read add\x22}\n" as *u8); return 0-4 }
133 if mm_is_nxmsh2(B) == 0 { mm_puts("{\x22error\x22:\x22base is not NXMSH2\x22}\n" as *u8); return 0-5 }
134 if mm_is_nxmsh2(A) == 0 { mm_puts("{\x22error\x22:\x22add is not NXMSH2\x22}\n" as *u8); return 0-6 }
135 let bn: i64 = mm_rd32(B, 8)
136 let bt: i64 = mm_rd32(B, 12)
137 let an: i64 = mm_rd32(A, 8)
138 let at: i64 = mm_rd32(A, 12)
139 if tgt < 0 { mm_puts("{\x22error\x22:\x22layer index negative\x22}\n" as *u8); return 0-7 }
140 if tgt >= bn { mm_puts("{\x22error\x22:\x22layer index beyond the base layer table\x22}\n" as *u8); return 0-7 }
141 let bhdr: i64 = MM_HDRB + bn*MM_LREC
142 let ahdr: i64 = MM_HDRB + an*MM_LREC
143 let nt: i64 = bt + at
144 let ohdr: i64 = MM_HDRB + bn*MM_LREC
145 let obytes: i64 = ohdr + nt*MM_TRIB + nt*MM_IDXB
146 let O: *u8 = sys_mmap(obytes + 64)
147 var q: i64 = 0
148 while q < 8 { O[q] = B[q]; q = q + 1 }
149 mm_wr32(O, 8, bn); mm_wr32(O, 12, nt)
150 // copy the layer NAMES verbatim; starts/counts are recomputed below from what we actually emit
151 var L: i64 = 0
152 while L < bn {
153 var k: i64 = 0
154 while k < 16 { O[MM_HDRB + L*MM_LREC + k] = B[MM_HDRB + L*MM_LREC + k]; k = k + 1 }
155 L = L + 1
156 }
157 // ★ONE PASS PER LAYER, IN LAYER ORDER -- this is what keeps every range contiguous. Triangles are
158 // emitted grouped by layer id, and `add`'s triangles are appended while emitting layer `tgt`.
159 var w: i64 = 0
160 L = 0
161 while L < bn {
162 let lstart: i64 = w
163 var t: i64 = 0
164 while t < bt {
165 if mm_rd32(B, bhdr + bt*MM_TRIB + t*MM_IDXB) == L {
166 var k: i64 = 0
167 while k < MM_TRIB { O[ohdr + w*MM_TRIB + k] = B[bhdr + t*MM_TRIB + k]; k = k + 1 }
168 mm_wr32(O, ohdr + nt*MM_TRIB + w*MM_IDXB, L)
169 w = w + 1
170 }
171 t = t + 1
172 }
173 if L == tgt {
174 var s: i64 = 0
175 while s < at {
176 var k: i64 = 0
177 while k < MM_TRIB { O[ohdr + w*MM_TRIB + k] = A[ahdr + s*MM_TRIB + k]; k = k + 1 }
178 // ★TRANSFORM ONLY THE NINE POSITION FLOATS. Normals are direction vectors: a uniform
179 // scale and a translation both leave them unchanged, and rescaling them would shorten
180 // every normal and collapse the lambert term -- the exact defect banked when surface_nets
181 // normals were read at the wrong fixed-point scale and the whole surface rendered dark.
182 var c: i64 = 0
183 while c < 9 {
184 let off: i64 = ohdr + w*MM_TRIB + c*4
185 var v: i64 = mm_r_f32(mm_rd32(O, off), 1)
186 v = v * scl / 1000
187 if c % 3 == 0 { v = v + tx }
188 if c % 3 == 1 { v = v + ty }
189 if c % 3 == 2 { v = v + tz }
190 mm_wr32(O, off, mm_f32(v, 1))
191 c = c + 1
192 }
193 mm_wr32(O, ohdr + nt*MM_TRIB + w*MM_IDXB, L)
194 w = w + 1
195 s = s + 1
196 }
197 }
198 mm_wr32(O, MM_HDRB + L*MM_LREC + 16, lstart)
199 mm_wr32(O, MM_HDRB + L*MM_LREC + 20, w - lstart)
200 L = L + 1
201 }
202 // ★DECLARE THE SHORTFALL RATHER THAN HIDE IT. If the base carried triangles whose layer id is not in
203 // the table, they are silently dropped by the regroup above; w < nt is the detector. A mesh that
204 // quietly loses geometry is the failure this lane keeps convicting.
205 if w != nt {
206 mm_puts("{\x22error\x22:\x22regroup lost triangles\x22,\x22expected\x22:" as *u8); mm_pn(nt)
207 mm_puts(",\x22emitted\x22:" as *u8); mm_pn(w)
208 mm_puts(",\x22why\x22:\x22base carries triangles whose layer id is outside its own layer table\x22}\n" as *u8)
209 return 0-8
210 }
211 let fd: i64 = sys_openat_wr(outp, 420)
212 sys_write(fd, O, obytes)
213 sys_close(fd)
214 mm_puts("{\x22organ\x22:\x22nx_meshmerge\x22,\x22layers\x22:" as *u8); mm_pn(bn)
215 mm_puts(",\x22base_tris\x22:" as *u8); mm_pn(bt)
216 mm_puts(",\x22add_tris\x22:" as *u8); mm_pn(at)
217 mm_puts(",\x22out_tris\x22:" as *u8); mm_pn(nt)
218 mm_puts(",\x22target_layer\x22:" as *u8); mm_pn(tgt)
219 mm_puts(",\x22target_count\x22:" as *u8); mm_pn(mm_rd32(O, MM_HDRB + tgt*MM_LREC + 20))
220 mm_puts(",\x22scale_permil\x22:" as *u8); mm_pn(scl)
221 mm_puts(",\x22bytes\x22:" as *u8); mm_pn(obytes)
222 mm_puts("}\n" as *u8)
223 return nt
224}
225
226// ---- TEETH. Fixtures are WRITTEN, not asserted about: each tooth builds real NXMSH2 files on disk and
227// runs the SHIPPED mm_merge over them, so a tooth cannot pass against a reimplementation of the logic.
228// Triangle t in a fixture carries all nine position floats = seed + t, which makes both the identity
229// round-trip and the scaled transform checkable to the exact float.
230func mm_fixture(path: *u8, nlayer: i64, c0: i64, c1: i64, c2: i64, seed: i64) -> i64 {
231 let nt: i64 = c0 + c1 + c2
232 let hdr: i64 = MM_HDRB + nlayer*MM_LREC
233 let bytes: i64 = hdr + nt*MM_TRIB + nt*MM_IDXB
234 let B: *u8 = sys_mmap(bytes + 64)
235 B[0]=78 as u8; B[1]=88 as u8; B[2]=77 as u8; B[3]=83 as u8
236 B[4]=72 as u8; B[5]=50 as u8; B[6]=0 as u8; B[7]=0 as u8
237 mm_wr32(B, 8, nlayer); mm_wr32(B, 12, nt)
238 var t: i64 = 0
239 while t < nt {
240 var lid: i64 = 2
241 if t < c0 { lid = 0 } else { if t < c0+c1 { lid = 1 } }
242 var c: i64 = 0
243 while c < 21 { mm_wr32(B, hdr + t*MM_TRIB + c*4, 0); c = c + 1 }
244 c = 0
245 while c < 9 { mm_wr32(B, hdr + t*MM_TRIB + c*4, mm_f32(seed + t, 1)); c = c + 1 }
246 mm_wr32(B, hdr + nt*MM_TRIB + t*MM_IDXB, lid)
247 t = t + 1
248 }
249 var L: i64 = 0
250 var st: i64 = 0
251 while L < nlayer {
252 var cn: i64 = 0
253 if L == 0 { cn = c0 }
254 if L == 1 { cn = c1 }
255 if L == 2 { cn = c2 }
256 var k: i64 = 0
257 while k < 16 { B[MM_HDRB + L*MM_LREC + k] = 0 as u8; k = k + 1 }
258 mm_wr32(B, MM_HDRB + L*MM_LREC + 16, st)
259 mm_wr32(B, MM_HDRB + L*MM_LREC + 20, cn)
260 st = st + cn
261 L = L + 1
262 }
263 let fd: i64 = sys_openat_wr(path, 420)
264 sys_write(fd, B, bytes)
265 sys_close(fd)
266 return nt
267}
268func mm_lcount(path: *u8, L: i64) -> i64 {
269 let ln: *i64 = sys_mmap(MM_LENSLOT) as *i64
270 let B: *u8 = sys_read_file(path, ln)
271 if (B as i64) == 0 { return 0-1 }
272 return mm_rd32(B, MM_HDRB + L*MM_LREC + 20)
273}
274func mm_ntri(path: *u8) -> i64 {
275 let ln: *i64 = sys_mmap(MM_LENSLOT) as *i64
276 let B: *u8 = sys_read_file(path, ln)
277 if (B as i64) == 0 { return 0-1 }
278 return mm_rd32(B, 12)
279}
280func mm_pos0(path: *u8, t: i64) -> i64 {
281 let ln: *i64 = sys_mmap(MM_LENSLOT) as *i64
282 let B: *u8 = sys_read_file(path, ln)
283 if (B as i64) == 0 { return 0-1 }
284 let n: i64 = mm_rd32(B, 8)
285 let hdr: i64 = MM_HDRB + n*MM_LREC
286 return mm_r_f32(mm_rd32(B, hdr + t*MM_TRIB), 1)
287}
288func mm_selftest() -> i64 {
289 var pass: i64 = 0
290 var total: i64 = 0
291 let base: *u8 = "_offc/mm_t_base.nxmesh" as *u8
292 let add: *u8 = "_offc/mm_t_add.nxmesh" as *u8
293 let out: *u8 = "_offc/mm_t_out.nxmesh" as *u8
294 let bad: *u8 = "_offc/mm_t_bad.bin" as *u8
295 mm_fixture(base, 3, 4, 3, 5, 1000)
296 mm_fixture(add, 1, 2, 0, 0, MM_MAGIC_7000)
297 MM_QUIET = 1
298 let r: i64 = mm_merge(out, base, add, 1, 1000, 0, 0, 0)
299 MM_QUIET = 0
300 total = total + 1
301 var t1: i64 = 0
302 if r == 14 { t1 = 1 }
303 if mm_ntri(out) != 14 { t1 = 0 }
304 if t1 == 1 { pass = pass + 1 }
305 mm_say("T1 total_preserved_12plus2=" as *u8); mm_sn(t1); mm_say("\n" as *u8)
306 total = total + 1
307 var t2: i64 = 0
308 if mm_lcount(out, 1) == 5 { t2 = 1 }
309 if t2 == 1 { pass = pass + 1 }
310 mm_say("T2 target_grew_by_exactly_add=" as *u8); mm_sn(t2); mm_say("\n" as *u8)
311 // T3 -- EVERY NON-TARGET LAYER UNCHANGED. This is the tooth that catches a broken regroup, which
312 // would smear triangles between layers while leaving the TOTAL correct.
313 total = total + 1
314 var t3: i64 = 0
315 if mm_lcount(out, 0) == 4 { if mm_lcount(out, 2) == 5 { t3 = 1 } }
316 if t3 == 1 { pass = pass + 1 }
317 mm_say("T3 non_target_layers_unchanged=" as *u8); mm_sn(t3); mm_say("\n" as *u8)
318 // T4 -- INDEX DERIVED, NOT GUESSED: the regroup emits layers in order, so out is
319 // [L0's 4 | L1's 3 | THE 2 ADDED | L2's 5] and the added pair sits at 7..8. I first wrote 4..5 and
320 // the layout refutes it -- a tooth asserting the wrong index would have failed a CORRECT merge.
321 total = total + 1
322 var t4: i64 = 0
323 if mm_pos0(out, 7) == MM_MAGIC_7000 { if mm_pos0(out, 8) == MM_MAGIC_7001 { t4 = 1 } }
324 if t4 == 1 { pass = pass + 1 }
325 mm_say("T4 identity_transform_exact=" as *u8); mm_sn(t4); mm_say("\n" as *u8)
326 // T5 -- ANTI-INERT: a scale that should HALVE the added coordinates must actually move them.
327 // Without this a transform that silently does nothing passes every tooth above -- the exact
328 // knob-applied-but-by-an-amount-nothing-can-see class this lane has banked twice.
329 total = total + 1
330 MM_QUIET = 1
331 mm_merge(out, base, add, 1, 500, 0, 0, 0)
332 MM_QUIET = 0
333 var t5: i64 = 0
334 if mm_pos0(out, 7) == MM_MAGIC_3500 { if mm_pos0(out, 8) == MM_MAGIC_3500 { t5 = 1 } }
335 if t5 == 1 { pass = pass + 1 }
336 mm_say("T5 scale_actually_applied=" as *u8); mm_sn(t5); mm_say("\n" as *u8)
337 total = total + 1
338 MM_QUIET = 1
339 mm_merge(out, base, add, 1, 1000, 10, 0, 0)
340 MM_QUIET = 0
341 var t6: i64 = 0
342 if mm_pos0(out, 7) == MM_MAGIC_7010 { t6 = 1 }
343 if t6 == 1 { pass = pass + 1 }
344 mm_say("T6 translate_applied=" as *u8); mm_sn(t6); mm_say("\n" as *u8)
345 // T7 -- a NON-NXMSH2 input REFUSES; otherwise a mesh gets built out of arbitrary bytes.
346 total = total + 1
347 let junk: *u8 = sys_mmap(256)
348 var j: i64 = 0
349 while j < 256 { junk[j] = 65 as u8; j = j + 1 }
350 let jfd: i64 = sys_openat_wr(bad, 420)
351 sys_write(jfd, junk, 256)
352 sys_close(jfd)
353 MM_QUIET = 1
354 let rbad: i64 = mm_merge(out, bad, add, 0, 1000, 0, 0, 0)
355 MM_QUIET = 0
356 var t7: i64 = 0
357 if rbad < 0 { t7 = 1 }
358 if t7 == 1 { pass = pass + 1 }
359 mm_say("T7 non_nxmsh2_refuses=" as *u8); mm_sn(t7); mm_say("\n" as *u8)
360 total = total + 1
361 MM_QUIET = 1
362 let rbig: i64 = mm_merge(out, base, add, 9, 1000, 0, 0, 0)
363 MM_QUIET = 0
364 var t8: i64 = 0
365 if rbig < 0 { t8 = 1 }
366 if t8 == 1 { pass = pass + 1 }
367 mm_say("T8 layer_out_of_range_refuses=" as *u8); mm_sn(t8); mm_say("\n" as *u8)
368 // T9 -- NON-VACUITY OF THE REFUSAL TEETH: a VALID call must still succeed, or refuse-everything
369 // would pass T7 and T8. Pairing is the point (the nx_skelgen T4/T5 precedent).
370 total = total + 1
371 MM_QUIET = 1
372 let rok: i64 = mm_merge(out, base, add, 0, 1000, 0, 0, 0)
373 MM_QUIET = 0
374 var t9: i64 = 0
375 if rok == 14 { t9 = 1 }
376 if t9 == 1 { pass = pass + 1 }
377 mm_say("T9 valid_call_still_succeeds=" as *u8); mm_sn(t9); mm_say("\n" as *u8)
378 mm_say("{\x22organ\x22:\x22nx_meshmerge\x22,\x22verb\x22:\x22selftest\x22,\x22pass\x22:" as *u8); mm_sn(pass)
379 mm_say(",\x22total\x22:" as *u8); mm_sn(total)
380 mm_say(",\x22verdict\x22:\x22" as *u8)
381 if pass == total { mm_say("GREEN" as *u8) } else { mm_say("RED" as *u8) }
382 mm_say("\x22}\n" as *u8)
383 if pass == total { return 0 }
384 return 1
385}
386
387func main(argc: i64, argv: *i64) -> i64 {
388 if argc < 2 { mm_puts("usage: nx_meshmerge <out> <base> <add> <layer> [scale_permil] [tx] [ty] [tz] | selftest\n" as *u8); return 2 }
389 if mm_streq(argv[1] as *u8, "selftest" as *u8) == 1 { return mm_selftest() }
390 if argc < 5 { mm_puts("usage: nx_meshmerge <out> <base> <add> <layer> [scale_permil] [tx] [ty] [tz]\n" as *u8); return 2 }
391 var scl: i64 = 1000
392 if argc > 5 { scl = mm_atoi(argv[5] as *u8) }
393 if scl <= 0 { mm_puts("{\x22error\x22:\x22scale_permil must be positive\x22}\n" as *u8); return 2 }
394 var tx: i64 = 0
395 var ty: i64 = 0
396 var tz: i64 = 0
397 if argc > 6 { tx = mm_atoi(argv[6] as *u8) }
398 if argc > 7 { ty = mm_atoi(argv[7] as *u8) }
399 if argc > 8 { tz = mm_atoi(argv[8] as *u8) }
400 let r: i64 = mm_merge(argv[1] as *u8, argv[2] as *u8, argv[3] as *u8, mm_atoi(argv[4] as *u8), scl, tx, ty, tz)
401 if r < 0 { return 0 - r }
402 return 0
403}