code wiki / _hdl_build / nx_gltf_export_gate.nx

nx_gltf_export_gate.nx source

↩ module page · 185 lines · 9619 B

1// nx_gltf_export_gate.nx -- prove the glTF/.glb export (roadmap R7, VRM foundation): mesh the bestiary, 2// write a binary glTF per variant, and validate the container: magic "glTF" + version 2 + total-length == 3// filesize + a JSON chunk (POSITION/NORMAL accessors) + a BIN chunk big enough for the geometry the JSON 4// declares. This is a REAL interchange format (three.js/Unity/Godot/Blender import it), the step up from 5// STL/OBJ toward riggable VRM avatars. 6// 7// MIGRATED OFF D001 2026-08-26. This gate used to hand-roll a `fails` tally and print its own 8// "GLTF-EXPORT-GATE 4/4 verdict=GREEN" line, so /api/promote refused it: nothing outside could read its 9// outcome, nx_gate_green could not judge it, and it recorded no harness.jrnl frame, leaving flake and 10// erosion invisible for it. It now inherits nx_gate_verdict -- per-tooth gv_check, so declared == executed 11// by construction, and the verdict is carried in the EXIT CODE where /api/gate_run reads it. 12// 13// The migration surfaced two vacuity defects, fixed here rather than carried across: 14// - the BIN-size check compared got_bin >= nv*24 + nf*12, which PASSES when the mesher returns nv=0 and 15// nf=0. A tooth that passes on the empty set is not a tooth. The vertex and triangle counts are now 16// declared SUBJECTS, so an empty mesh ends SKIP (no evidence) instead of scoring a silent GREEN. 17// - the six-variant export loop printed a line per file and asserted NOTHING about any of them, so a 18// write_glb returning 0 for every variant was invisible. It is a tooth now. 19// Two neg-controls drive the header detector and the JSON scanner with crafted inputs, because a checker 20// that answers the same way for everything scores 100% and proves nothing. 21// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 22import "nx_syscalls.nx" 23import "nx_sdfrender.nx" 24import "nx_meshgen.nx" 25import "nx_assetvariant.nx" 26import "nx_gltf_export.nx" 27import "nx_gate_verdict.nx" 28 29// Container constants, named rather than spelled as bare hex at the comparison site. 30const GX_MAGIC_GLTF: i64 = 0x46546C67 // "glTF", little-endian 31const GX_CHUNK_JSON: i64 = 0x4E4F534A // "JSON" 32const GX_CHUNK_BIN: i64 = 0x004E4942 // "BIN\0" 33const GX_VERSION: i64 = 2 // glTF 2.0 34const GX_HDR_BYTES: i64 = 12 // magic + version + total length 35const GX_CHUNKHDR: i64 = 8 // chunk length + chunk type 36const GX_VEC3_F32: i64 = 12 // three float32 per vertex attribute 37const GX_ATTRS: i64 = 2 // POSITION and NORMAL, both vec3 38const GX_IDX_BYTES: i64 = 4 // uint32 index 39const GX_TRI_IDX: i64 = 3 // indices per triangle 40 41// The bestiary table is GX_NVARIANT rows of GX_VT_FIELDS. Every bound over the table is DERIVED from 42// these two, so adding a variant cannot leave a loop or a tooth behind counting the old number. 43const GX_NVARIANT: i64 = 6 44const GX_VT_FIELDS: i64 = 5 45const GX_SCRATCH: i64 = 16 46 47func rd32(b: *u8, o: i64) -> i64 { return (b[o]&0xff) | ((b[o+1]&0xff)<<8) | ((b[o+2]&0xff)<<16) | ((b[o+3]&0xff)<<24) } 48 49func has_str(b: *u8, n: i64, s: *u8) -> i64 { 50 var sl: i64 = 0 51 while s[sl] != (0 as u8) { sl = sl + 1 } 52 var i: i64 = 0 53 while i + sl <= n { 54 var j: i64 = 0 55 var ok: i64 = 1 56 while j < sl { 57 if (b[i+j]&0xff) != (s[j]&0xff) { ok = 0; j = sl } else { j = j + 1 } 58 } 59 if ok == 1 { return 1 } 60 i = i + 1 61 } 62 return 0 63} 64 65// The header detector, isolated so a neg-control can drive it with a crafted bad buffer rather than 66// asserting only against the one file that happens to be good. Returns 1 when it REJECTS the header. 67func glb_magic_rejects(b: *u8) -> i64 { 68 if rd32(b, 0) != GX_MAGIC_GLTF { return 1 } 69 return 0 70} 71 72func main() -> i64 { 73 let ctr: *i64 = gv_ctr() 74 gv_head("nx_gltf_export_gate -- mesh -> binary glTF (.glb), R7 VRM foundation" as *u8) 75 76 let base: i64 = sys_mmap(sdf_bytes()) as i64 77 let F: *i64 = sys_mmap((MG_N + 1) * (MG_N + 1) * (MG_N + 1) * 8) as *i64 78 let cv: *i64 = sys_mmap(MG_N * MG_N * MG_N * 8) as *i64 79 let vb: *i64 = sys_mmap(MG_MAXV * 3 * 8) as *i64 80 let fb: *i64 = sys_mmap(MG_MAXF * 3 * 8) as *i64 81 let out: *i64 = sys_mmap(GX_SCRATCH) as *i64 82 83 let vt: *i64 = sys_mmap(GX_NVARIANT * GX_VT_FIELDS * 8) as *i64 84 var q: i64 = 0 85 vt[q]=0;vt[q+1]=0;vt[q+2]=0;vt[q+3]=0;vt[q+4]=100;q=q+GX_VT_FIELDS // human 86 vt[q]=0;vt[q+1]=0;vt[q+2]=1;vt[q+3]=0;vt[q+4]=80;q=q+GX_VT_FIELDS // elf 87 vt[q]=2;vt[q+1]=0;vt[q+2]=1;vt[q+3]=0;vt[q+4]=100;q=q+GX_VT_FIELDS // goblin 88 vt[q]=2;vt[q+1]=0;vt[q+2]=1;vt[q+3]=0;vt[q+4]=125;q=q+GX_VT_FIELDS // hobgoblin 89 vt[q]=1;vt[q+1]=0;vt[q+2]=1;vt[q+3]=1;vt[q+4]=100;q=q+GX_VT_FIELDS // wolf 90 vt[q]=1;vt[q+1]=1;vt[q+2]=1;vt[q+3]=1;vt[q+4]=125;q=q+GX_VT_FIELDS // dire wolf 91 92 var nv: i64 = 0 93 var nf: i64 = 0 94 var exported: i64 = 0 95 var v: i64 = 0 96 while v < GX_NVARIANT { 97 av_build(base, vt[v*GX_VT_FIELDS], vt[v*GX_VT_FIELDS+1], vt[v*GX_VT_FIELDS+2], vt[v*GX_VT_FIELDS+3], vt[v*GX_VT_FIELDS+4]) 98 mg_build(base, F, cv, vb, fb, out) 99 nv = out[0] 100 nf = out[1] 101 var nm: *u8 = "knowledge/synth_human.glb" as *u8 102 if v == 1 { nm = "knowledge/synth_elf.glb" as *u8 } 103 if v == 2 { nm = "knowledge/synth_goblin.glb" as *u8 } 104 if v == 3 { nm = "knowledge/synth_hobgoblin.glb" as *u8 } 105 if v == 4 { nm = "knowledge/synth_wolf.glb" as *u8 } 106 if v == 5 { nm = "knowledge/synth_direwolf.glb" as *u8 } 107 let glen: i64 = write_glb(vb, fb, nv, nf, nm) 108 gv_puts(" " as *u8) 109 gv_puts(nm) 110 gv_puts(" verts=" as *u8) 111 gv_num(nv) 112 gv_puts(" bytes=" as *u8) 113 gv_num(glen) 114 gv_puts("\n" as *u8) 115 if glen > 0 { exported = exported + 1 } 116 v = v + 1 117 } 118 gv_check("T0 every bestiary variant wrote a non-empty .glb" as *u8, exported == GX_NVARIANT, ctr) 119 120 // Re-mesh the variant whose file is validated in detail below. Driven from row 0 of the table, so 121 // this can never silently validate a file built from different parameters than it re-meshed. 122 av_build(base, vt[0], vt[1], vt[2], vt[3], vt[4]) 123 mg_build(base, F, cv, vb, fb, out) 124 nv = out[0] 125 nf = out[1] 126 gv_subjects("vertices in the re-meshed human variant" as *u8, nv, ctr) 127 gv_subjects("triangles in the re-meshed human variant" as *u8, nf, ctr) 128 129 let lp: *i64 = sys_mmap(GX_SCRATCH) as *i64 130 lp[0] = 0 131 let buf: *u8 = sys_read_file("knowledge/synth_human.glb" as *u8, lp) 132 let fsz: i64 = lp[0] 133 var readable: i64 = 0 134 if buf != (0 as *u8) { 135 if fsz >= GX_HDR_BYTES + GX_CHUNKHDR { readable = 1 } 136 } 137 gv_need("knowledge/synth_human.glb readable and at least one chunk long" as *u8, readable, ctr) 138 139 // Every byte-level tooth is inside this guard: with no readable file there is no evidence either 140 // way, and gv_need has already recorded that as a missing precondition (SKIP, never a pass). 141 if readable == 1 { 142 gv_check("T1 GLB magic is glTF" as *u8, glb_magic_rejects(buf) == 0, ctr) 143 gv_check("T2 container version is glTF 2" as *u8, rd32(buf, 4) == GX_VERSION, ctr) 144 gv_check("T3 declared total length equals the file size" as *u8, rd32(buf, 8) == fsz, ctr) 145 146 let jsonlen: i64 = rd32(buf, GX_HDR_BYTES) 147 gv_check("T4 chunk 0 is a JSON chunk" as *u8, rd32(buf, GX_HDR_BYTES + 4) == GX_CHUNK_JSON, ctr) 148 149 let bco: i64 = GX_HDR_BYTES + GX_CHUNKHDR + jsonlen 150 var binhdr_inbounds: i64 = 0 151 if jsonlen > 0 { 152 if bco + GX_CHUNKHDR <= fsz { binhdr_inbounds = 1 } 153 } 154 gv_check("T5 the BIN chunk header lies inside the file" as *u8, binhdr_inbounds == 1, ctr) 155 156 var bin_is_bin: i64 = 0 157 var got_bin: i64 = 0 158 if binhdr_inbounds == 1 { 159 got_bin = rd32(buf, bco) 160 if rd32(buf, bco + 4) == GX_CHUNK_BIN { bin_is_bin = 1 } 161 } 162 gv_check("T6 chunk 1 is a BIN chunk" as *u8, bin_is_bin == 1, ctr) 163 164 gv_check("T7 the JSON declares a POSITION accessor" as *u8, has_str(buf, fsz, "\"POSITION\":0" as *u8) == 1, ctr) 165 gv_check("T8 the JSON declares a NORMAL accessor (smooth-shaded)" as *u8, has_str(buf, fsz, "\"NORMAL\":1" as *u8) == 1, ctr) 166 167 // Bound to the counts declared as subjects above, so this cannot be satisfied by an empty mesh. 168 let want_bin: i64 = nv * GX_VEC3_F32 * GX_ATTRS + nf * GX_TRI_IDX * GX_IDX_BYTES 169 gv_check("T9 the BIN chunk holds the positions, normals and indices the mesh produced" as *u8, got_bin >= want_bin, ctr) 170 171 // A header checker that rejected everything would pass T1 on any file; drive it with a crafted 172 // non-glTF header and require it to stay silent on the real one. 173 let fake: *u8 = sys_mmap(GX_HDR_BYTES) 174 fake[0] = 0 as u8 175 fake[1] = 0 as u8 176 fake[2] = 0 as u8 177 fake[3] = 0 as u8 178 gv_bite("neg-control-glb-magic-detector-fires-on-a-non-glTF-header" as *u8, glb_magic_rejects(fake), glb_magic_rejects(buf), ctr) 179 180 // Likewise a scanner that answered 1 for every token would pass T7 and T8 vacuously. 181 gv_bite("neg-control-json-scan-finds-a-present-accessor-and-not-an-absent-one" as *u8, has_str(buf, fsz, "\"POSITION\":0" as *u8), has_str(buf, fsz, "\"NOSUCHACCESSOR\":7" as *u8), ctr) 182 } 183 184 return gv_verdict("gltf_export" as *u8, ctr, "the exported .glb is a valid glTF 2.0 container whose BIN chunk carries the geometry its JSON declares" as *u8) 185}