code wiki / (root) / nx_roundtrip.nx

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}