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}