nx_roundtrip.nx source
↩ module page · 398 lines · 16524 B
1// nx_roundtrip.nx -- THE SELF-ORACLE: nishi -> glTF -> nishi, and NAME WHAT THE TRIP LOSES.
2//
3// WHY THIS EXISTS. Every other fidelity oracle in this estate is currently blocked on ground truth: the
4// rigged reference corpus was measured 2026-08-22 and is DEGENERATE (eleven .nxa files byte-identical in
5// geometry), and D:\rigs is empty. A ROUND TRIP needs none of that. It compares our own ingest against our
6// own export, so it is available today, it carries no licensing question, and it measures the one thing an
7// external corpus can never tell us: what OUR OWN PIPELINE destroys.
8//
9// IT COMPOSES, IT DOES NOT REIMPLEMENT. It forks nx_mesh2glb (NXMSH2 -> glb) then nx_gltf2mesh
10// (glb -> NXMSH2) and compares the input with the output. There is exactly one glTF writer and one glTF
11// reader in the estate and this organ is neither of them.
12//
13// WHAT "IDENTICAL" MEANS HERE, AND WHY NOT BYTE IDENTITY. glTF is JSON plus a binary blob; key order,
14// float formatting and accessor packing are all free to change without meaning changing. Byte-comparing
15// the containers would report LOSSY on a lossless trip. So the assertion is SEMANTIC: triangle count,
16// layer count, layer NAMES, and vertex POSITIONS.
17//
18// THE POSITION TOLERANCE IS ZERO, AND THAT IS DERIVED, NOT CHOSEN. nx_mesh2glb's own header states
19// positions "PASS THROUGH as raw f32 bits (zero re-quantisation)", and its code copies the 4-byte word
20// with mg_w32 without decoding it. A pass-through has no error term, so the correct tolerance is EXACT BIT
21// EQUALITY. Picking an epsilon would have hidden a real requantisation bug behind a number nobody derived.
22// If positions ever stop matching bit-for-bit, that is a REGRESSION IN THE WRITER, not rounding.
23//
24// NORMALS AND COLOURS ARE KNOWN-LOSSY BY DESIGN and are deliberately NOT asserted: the writer transcodes
25// per-vertex normals f32 -> normalized i16 and per-tri colour f32 -> u8. Asserting on them would make this
26// organ permanently RED for a documented, intended conversion, and a detector that is permanently red is
27// one everyone learns to ignore. They are REPORTED, never scored.
28//
29// EXITS: 0 IDENTICAL | 1 LOSSY | 2 usage | 3 UNOBSERVABLE
30// 3 IS NOT A SOFTER 1. A missing tool, a fork that returns 127, an empty output, or an unreadable container
31// means the trip WAS NOT OBSERVED -- reporting that as LOSSY would blame the format for a harness failure.
32// This estate has been bitten by exactly that confusion, so the two are kept structurally apart.
33//
34// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
35import "nx_syscalls.nx"
36import "nx_tool_run.nx"
37
38const RT_EXIT_IDENTICAL: i64 = 0
39const RT_EXIT_LOSSY: i64 = 1
40const RT_EXIT_USAGE: i64 = 2
41const RT_EXIT_UNOBSERVABLE: i64 = 3
42
43// ---- NXMSH2 layout, read from the writer (nx_mesh2glb.nx) rather than assumed ----
44// magic is discriminated on TWO bytes: 'NXANIM01' also begins with 'N', so one byte cannot tell the
45// estate's two containers apart. b[5]=='2' is what nx_mesh2glb itself checks.
46const RT_MAGIC_N: i64 = 78 // 'N' at offset 0
47const RT_MAGIC_2: i64 = 50 // '2' at offset 5
48const RT_MAGIC_2_OFF: i64 = 5
49const RT_NLAY_OFF: i64 = 8 // u32 layer count
50const RT_NTRI_OFF: i64 = 12 // u32 triangle count
51const RT_HDR_BASE: i64 = 16 // first layer record
52const RT_LAYER_STRIDE: i64 = 24 // name[16] + u32 off + u32 count
53const RT_LAYER_NAME_LEN: i64 = 16
54const RT_LAYER_OFF_FLD: i64 = 16
55const RT_LAYER_CNT_FLD: i64 = 20
56const RT_TRI_STRIDE: i64 = 84 // 36 position + 36 normal + 12 colour
57const RT_POS_BYTES: i64 = 36 // the only bytes asserted; see header
58const RT_MIN_CONTAINER: i64 = 44 // the smallest readable NXMSH2, per the writer's own guard
59
60const RT_WORD: i64 = 8
61const RT_U32_BYTES: i64 = 4
62const RT_BYTE_MASK: i64 = 255
63const RT_STDOUT: i64 = 1
64const RT_PATH_CAP: i64 = 4096
65const RT_CAP_CAP: i64 = 262144 // capture buffer for a forked tool's stdout
66const RT_ARGV_SLOTS: i64 = 8
67const RT_SCRATCH: i64 = 64
68const RT_DECIMAL: i64 = 10
69const RT_ASCII_ZERO: i64 = 48
70const RT_ASCII_MINUS: i64 = 45
71const RT_ASCII_NL: i64 = 10
72const RT_ASCII_SLASH: i64 = 47
73const RT_EXEC_NOT_FOUND: i64 = 127 // execve failure surfaces here; NOT a lossy trip
74const RT_VERB_CHECK: i64 = 99 // 'c'
75
76// how many differing layer names to name individually before summarising. A CAP THAT ANNOUNCES:
77// the count is always printed in full, so this bounds the PRINTOUT, never the measurement.
78const RT_NAME_REPORT_MAX: i64 = 12
79
80const RT_M2G_DEFAULT: *u8 = "/volume1/homes/elderwesto/nishihost/_offc/nx_mesh2glb.elf"
81const RT_G2M_DEFAULT: *u8 = "/volume1/homes/elderwesto/nishihost/_offc/nx_gltf2mesh.elf"
82const RT_MID_NAME: *u8 = "rt_mid.glb"
83const RT_BACK_NAME: *u8 = "rt_back.nxmesh"
84
85func rt_out(s: *u8) -> i64 {
86 var n: i64 = 0
87 while s[n] != 0 { n = n + 1 }
88 sys_write(RT_STDOUT, s, n)
89 return 0
90}
91func rt_u32(b: *u8, o: i64) -> i64 {
92 return (b[o] as i64) + ((b[o+1] as i64)<<8) + ((b[o+2] as i64)<<16) + ((b[o+3] as i64)<<24)
93}
94func rt_bputs(buf: *u8, off: i64, s: *u8) -> i64 {
95 var o: i64 = off
96 var i: i64 = 0
97 while s[i] != 0 { buf[o] = s[i]; o = o + 1; i = i + 1 }
98 return o
99}
100func rt_bputnum(buf: *u8, off: i64, v: i64, scratch: *u8) -> i64 {
101 var o: i64 = off
102 var n: i64 = v
103 if n < 0 { buf[o] = RT_ASCII_MINUS; o = o + 1; n = 0 - n }
104 var k: i64 = 0
105 if n == 0 { scratch[k] = RT_ASCII_ZERO; k = 1 }
106 while n > 0 {
107 let q: i64 = n / RT_DECIMAL
108 scratch[k] = RT_ASCII_ZERO + (n - q*RT_DECIMAL)
109 n = q
110 k = k + 1
111 }
112 while k > 0 { k = k - 1; buf[o] = scratch[k]; o = o + 1 }
113 return o
114}
115func rt_say(label: *u8, v: i64, scratch: *u8, line: *u8) -> i64 {
116 var o: i64 = rt_bputs(line, 0, label)
117 o = rt_bputnum(line, o, v, scratch)
118 line[o] = RT_ASCII_NL
119 sys_write(RT_STDOUT, line, o + 1)
120 return 0
121}
122// join <dir>/<name> into dst, NUL-terminated. Returns 0 on success, -1 if it would not fit --
123// a path that does not fit REFUSES rather than being silently truncated into a wrong path.
124func rt_join(dst: *u8, dir: *u8, name: *u8) -> i64 {
125 var o: i64 = 0
126 var i: i64 = 0
127 while dir[i] != 0 {
128 if o >= RT_PATH_CAP - 2 { return 0 - 1 }
129 dst[o] = dir[i]
130 o = o + 1
131 i = i + 1
132 }
133 if o > 0 { if dst[o-1] != RT_ASCII_SLASH { dst[o] = RT_ASCII_SLASH as u8; o = o + 1 } }
134 i = 0
135 while name[i] != 0 {
136 if o >= RT_PATH_CAP - 2 { return 0 - 1 }
137 dst[o] = name[i]
138 o = o + 1
139 i = i + 1
140 }
141 dst[o] = 0
142 return 0
143}
144// fork a converter with two path args; returns the child exit code, or a negative harness sentinel
145func rt_run2(tool: *u8, a1: *u8, a2: *u8, cap: *u8, olen: *i64) -> i64 {
146 let av: *i64 = sys_mmap(RT_WORD*RT_ARGV_SLOTS) as *i64
147 av[0] = tool as i64
148 av[1] = a1 as i64
149 av[2] = a2 as i64
150 av[3] = 0
151 return tr_run_capture(tool, av, cap, RT_CAP_CAP, olen)
152}
153// validate an NXMSH2 container. out[0]=nlay out[1]=ntri out[2]=hdr_bytes. returns 1 ok, 0 not readable.
154func rt_hdr(b: *u8, n: i64, out: *i64) -> i64 {
155 if n < RT_MIN_CONTAINER { return 0 }
156 if (b[0] as i64) != RT_MAGIC_N { return 0 }
157 if (b[RT_MAGIC_2_OFF] as i64) != RT_MAGIC_2 { return 0 }
158 let nlay: i64 = rt_u32(b, RT_NLAY_OFF)
159 let ntri: i64 = rt_u32(b, RT_NTRI_OFF)
160 if ntri <= 0 { return 0 }
161 if nlay < 0 { return 0 }
162 let hdr: i64 = RT_HDR_BASE + nlay*RT_LAYER_STRIDE
163 // REFUSE a container whose declared triangles do not fit the bytes present. A short read that was
164 // parsed anyway would compare against uninitialised memory and could report IDENTICAL on garbage.
165 if hdr + ntri*RT_TRI_STRIDE > n { return 0 }
166 out[0] = nlay
167 out[1] = ntri
168 out[2] = hdr
169 return 1
170}
171func rt_name_eq(a: *u8, ao: i64, b: *u8, bo: i64) -> i64 {
172 var i: i64 = 0
173 while i < RT_LAYER_NAME_LEN {
174 if a[ao + i] != b[bo + i] { return 0 }
175 i = i + 1
176 }
177 return 1
178}
179func rt_print_name(b: *u8, off: i64) -> i64 {
180 var k: i64 = 0
181 while k < RT_LAYER_NAME_LEN {
182 let ch: i64 = b[off + k] as i64
183 if ch >= 32 { if ch < 127 { sys_write(RT_STDOUT, ((b as i64) + off + k) as *u8, 1) } }
184 k = k + 1
185 }
186 return 0
187}
188
189func rt_usage() -> i64 {
190 rt_out("usage: nx_roundtrip check <in.nxmesh> <scratch-dir> [mesh2glb.elf] [gltf2mesh.elf]\n" as *u8)
191 rt_out(" forks NXMSH2 -> glb -> NXMSH2 and names what the trip loses.\n" as *u8)
192 rt_out(" asserts: triangle count, layer count, layer names, and vertex positions (EXACT bits --\n" as *u8)
193 rt_out(" the writer documents zero requantisation, so there is no error term to allow).\n" as *u8)
194 rt_out(" reports but does NOT score: normals (f32->i16) and colour (f32->u8), lossy BY DESIGN.\n" as *u8)
195 rt_out(" exits: 0 IDENTICAL | 1 LOSSY | 2 usage | 3 UNOBSERVABLE\n" as *u8)
196 return RT_EXIT_USAGE
197}
198
199func rt_check(inp: *u8, scratch_dir: *u8, m2g: *u8, g2m: *u8) -> i64 {
200 let line: *u8 = sys_mmap(RT_PATH_CAP)
201 let sc: *u8 = sys_mmap(RT_SCRATCH)
202 let midp: *u8 = sys_mmap(RT_PATH_CAP)
203 let backp: *u8 = sys_mmap(RT_PATH_CAP)
204 let cap: *u8 = sys_mmap(RT_CAP_CAP)
205 let olen: *i64 = sys_mmap(RT_WORD*2) as *i64
206 let ha: *i64 = sys_mmap(RT_WORD*4) as *i64
207 let hb: *i64 = sys_mmap(RT_WORD*4) as *i64
208 let lp: *i64 = sys_mmap(RT_WORD*2) as *i64
209
210 rt_out("NX-ROUNDTRIP check\n input: " as *u8)
211 rt_out(inp)
212 rt_out("\n" as *u8)
213
214 if rt_join(midp, scratch_dir, RT_MID_NAME) != 0 {
215 rt_out("NX-ROUNDTRIP verdict=UNOBSERVABLE reason=scratch-path-too-long\n" as *u8)
216 return RT_EXIT_UNOBSERVABLE
217 }
218 if rt_join(backp, scratch_dir, RT_BACK_NAME) != 0 {
219 rt_out("NX-ROUNDTRIP verdict=UNOBSERVABLE reason=scratch-path-too-long\n" as *u8)
220 return RT_EXIT_UNOBSERVABLE
221 }
222 // REMOVE PRIOR OUTPUTS BEFORE MEASURING. Without this a stale intermediate from an earlier run
223 // could be compared and reported as a successful trip that never happened.
224 sys_unlinkat(midp)
225 sys_unlinkat(backp)
226
227 // ---- read the input FIRST: an unreadable input is UNOBSERVABLE, never LOSSY ----
228 let ab: *u8 = sys_read_file(inp, lp)
229 if (ab as i64) == 0 {
230 rt_out("NX-ROUNDTRIP verdict=UNOBSERVABLE reason=input-unreadable\n" as *u8)
231 return RT_EXIT_UNOBSERVABLE
232 }
233 let an: i64 = lp[0]
234 if rt_hdr(ab, an, ha) == 0 {
235 rt_out("NX-ROUNDTRIP verdict=UNOBSERVABLE reason=input-not-a-valid-NXMSH2-container\n" as *u8)
236 return RT_EXIT_UNOBSERVABLE
237 }
238 rt_say(" in_layers=" as *u8, ha[0], sc, line)
239 rt_say(" in_tris=" as *u8, ha[1], sc, line)
240
241 // ---- leg 1: NXMSH2 -> glb ----
242 let rc1: i64 = rt_run2(m2g, inp, midp, cap, olen)
243 rt_say(" mesh2glb_rc=" as *u8, rc1, sc, line)
244 if rc1 < 0 {
245 rt_out("NX-ROUNDTRIP verdict=UNOBSERVABLE reason=mesh2glb-harness-failure\n" as *u8)
246 return RT_EXIT_UNOBSERVABLE
247 }
248 if rc1 == RT_EXEC_NOT_FOUND {
249 rt_out("NX-ROUNDTRIP verdict=UNOBSERVABLE reason=mesh2glb-not-executable-at-that-path\n" as *u8)
250 return RT_EXIT_UNOBSERVABLE
251 }
252 if rc1 != 0 {
253 rt_out("NX-ROUNDTRIP verdict=UNOBSERVABLE reason=mesh2glb-refused-the-input\n" as *u8)
254 return RT_EXIT_UNOBSERVABLE
255 }
256 // ANTI-VACUITY: the intermediate must EXIST AND BE NON-EMPTY. A trip whose first leg silently
257 // produced nothing would otherwise compare nothing with nothing and read as a clean pass.
258 let mb: *u8 = sys_read_file(midp, lp)
259 if (mb as i64) == 0 {
260 rt_out("NX-ROUNDTRIP verdict=UNOBSERVABLE reason=intermediate-glb-absent\n" as *u8)
261 return RT_EXIT_UNOBSERVABLE
262 }
263 let mn: i64 = lp[0]
264 rt_say(" intermediate_glb_bytes=" as *u8, mn, sc, line)
265 if mn <= 0 {
266 rt_out("NX-ROUNDTRIP verdict=UNOBSERVABLE reason=intermediate-glb-empty\n" as *u8)
267 return RT_EXIT_UNOBSERVABLE
268 }
269
270 // ---- leg 2: glb -> NXMSH2 ----
271 let rc2: i64 = rt_run2(g2m, midp, backp, cap, olen)
272 rt_say(" gltf2mesh_rc=" as *u8, rc2, sc, line)
273 if rc2 < 0 {
274 rt_out("NX-ROUNDTRIP verdict=UNOBSERVABLE reason=gltf2mesh-harness-failure\n" as *u8)
275 return RT_EXIT_UNOBSERVABLE
276 }
277 if rc2 == RT_EXEC_NOT_FOUND {
278 rt_out("NX-ROUNDTRIP verdict=UNOBSERVABLE reason=gltf2mesh-not-executable-at-that-path\n" as *u8)
279 return RT_EXIT_UNOBSERVABLE
280 }
281 if rc2 != 0 {
282 rt_out("NX-ROUNDTRIP verdict=UNOBSERVABLE reason=gltf2mesh-refused-the-intermediate\n" as *u8)
283 return RT_EXIT_UNOBSERVABLE
284 }
285 let bb: *u8 = sys_read_file(backp, lp)
286 if (bb as i64) == 0 {
287 rt_out("NX-ROUNDTRIP verdict=UNOBSERVABLE reason=returned-container-absent\n" as *u8)
288 return RT_EXIT_UNOBSERVABLE
289 }
290 let bn: i64 = lp[0]
291 if rt_hdr(bb, bn, hb) == 0 {
292 rt_out("NX-ROUNDTRIP verdict=UNOBSERVABLE reason=returned-container-invalid\n" as *u8)
293 return RT_EXIT_UNOBSERVABLE
294 }
295 rt_say(" out_layers=" as *u8, hb[0], sc, line)
296 rt_say(" out_tris=" as *u8, hb[1], sc, line)
297
298 // ---- the comparison. Each axis reports independently; the verdict is their OR. ----
299 var lost: i64 = 0
300
301 var tri_ok: i64 = 0
302 if ha[1] == hb[1] { tri_ok = 1 }
303 if tri_ok == 0 {
304 rt_out(" LOST: triangle count\n" as *u8)
305 lost = lost + 1
306 }
307
308 var lay_ok: i64 = 0
309 if ha[0] == hb[0] { lay_ok = 1 }
310 if lay_ok == 0 {
311 rt_out(" LOST: layer count -- the named-part segmentation does not survive the trip\n" as *u8)
312 lost = lost + 1
313 }
314
315 // layer NAMES: only comparable when the counts match; when they do not, the count line above
316 // already carries the finding and a positional name compare would be meaningless.
317 if lay_ok == 1 {
318 var nm_bad: i64 = 0
319 var reported: i64 = 0
320 var L: i64 = 0
321 while L < ha[0] {
322 let ao: i64 = RT_HDR_BASE + L*RT_LAYER_STRIDE
323 let bo: i64 = RT_HDR_BASE + L*RT_LAYER_STRIDE
324 if rt_name_eq(ab, ao, bb, bo) == 0 {
325 nm_bad = nm_bad + 1
326 if reported < RT_NAME_REPORT_MAX {
327 rt_out(" LOST: layer name [" as *u8)
328 rt_print_name(ab, ao)
329 rt_out("] became [" as *u8)
330 rt_print_name(bb, bo)
331 rt_out("]\n" as *u8)
332 reported = reported + 1
333 }
334 }
335 L = L + 1
336 }
337 rt_say(" layer_names_differing=" as *u8, nm_bad, sc, line)
338 if nm_bad > reported {
339 rt_out(" <== the list above is a PREFIX of that count\n" as *u8)
340 }
341 if nm_bad > 0 { lost = lost + 1 }
342 }
343
344 // POSITIONS: exact bit equality, and only when the triangle counts agree.
345 if tri_ok == 1 {
346 var pos_bad: i64 = 0
347 var t: i64 = 0
348 while t < ha[1] {
349 let ao2: i64 = ha[2] + t*RT_TRI_STRIDE
350 let bo2: i64 = hb[2] + t*RT_TRI_STRIDE
351 var k: i64 = 0
352 var same: i64 = 1
353 while k < RT_POS_BYTES {
354 if ab[ao2 + k] != bb[bo2 + k] { same = 0 }
355 k = k + 1
356 }
357 if same == 0 { pos_bad = pos_bad + 1 }
358 t = t + 1
359 }
360 rt_say(" triangles_with_differing_positions=" as *u8, pos_bad, sc, line)
361 if pos_bad > 0 {
362 rt_out(" LOST: vertex positions -- the writer documents raw f32 pass-through, so ANY\n" as *u8)
363 rt_out(" difference here is a requantisation REGRESSION, not rounding\n" as *u8)
364 lost = lost + 1
365 }
366 }
367
368 rt_say(" axes_lost=" as *u8, lost, sc, line)
369 if lost > 0 {
370 rt_out("NX-ROUNDTRIP verdict=LOSSY\n" as *u8)
371 return RT_EXIT_LOSSY
372 }
373 rt_out("NX-ROUNDTRIP verdict=IDENTICAL\n" as *u8)
374 return RT_EXIT_IDENTICAL
375}
376
377func main(argc: i64, argv: *i64) -> i64 {
378 if argc < 4 {
379 let u: i64 = rt_usage()
380 sys_exit(u)
381 return u
382 }
383 let verb: *u8 = argv[1] as *u8
384 if verb[0] != RT_VERB_CHECK {
385 let u2: i64 = rt_usage()
386 sys_exit(u2)
387 return u2
388 }
389 let inp: *u8 = argv[2] as *u8
390 let dir: *u8 = argv[3] as *u8
391 var m2g: *u8 = RT_M2G_DEFAULT
392 var g2m: *u8 = RT_G2M_DEFAULT
393 if argc >= 5 { m2g = argv[4] as *u8 }
394 if argc >= 6 { g2m = argv[5] as *u8 }
395 let rc: i64 = rt_check(inp, dir, m2g, g2m)
396 sys_exit(rc)
397 return rc
398}