code wiki / _hdl_build / nx_gltf_anim_gate.nx
nx_gltf_anim_gate.nx source
↩ module page · 139 lines · 8084 B
1// nx_gltf_anim_gate.nx -- export the being as a SKINNED, WALKING .glb. Mesh the skin, bind each vertex to its nearest
2// part's deepest FK bone, export with a walk-cycle animation, self-verify the container + names. Writes knowledge/nishi_being_walk.glb.
3//
4// D001 MIGRATION 2026-08-22: this gate rolled its own verdict (a hand `fails` counter + sys_exit) so /api/promote
5// refused it and nothing outside could read its outcome -- nx_gate_green could not judge it and it wrote no
6// harness.jrnl frame, so flake and erosion were invisible for it. Migrated BY HAND onto per-tooth gv_check, NOT via
7// nx_gate_dry_apply (whose candidate collapses N teeth into ONE boolean: ctr[0]=green; ctr[1]=1 -- a one-state judge
8// cannot see a dropped conjunct). T0..T3 keep their exact semantics. `return gv_verdict(...)` so the EXIT CODE carries
9// the verdict -- /api/gate_run derives its verdict from the exit code.
10//
11// WHY THIS GATE WAS REBUILT AT ALL: a fresh build of the unchanged source measured 88,019 B against a served 139,953 B
12// (375 permil, the same shrink as the matched-vintage control). nx_contentdiff named ONE lost run --
13// `"animations":[{"name":"walk","samplers":[` -- and this gate's own T3 proved on the STAGED binary's ARTIFACT that the
14// walk animation is still declared, so the run was a refactored literal, not lost capability. That proof is what T3
15// exists for; the promote refusal was about the VERDICT SHAPE, not the content.
16// license_tier: ORIGINAL expect_exit: 0
17import "nx_syscalls.nx"
18import "nx_gate_verdict.nx"
19import "nx_trimesh.nx"
20import "nx_isosurf.nx"
21import "nx_bodyatlas.nx"
22import "nx_gltf_anim.nx"
23
24func ru32(buf: *u8, at: i64) -> i64 { return (buf[at] as i64) + ((buf[at+1] as i64)<<8) + ((buf[at+2] as i64)<<16) + ((buf[at+3] as i64)<<24) }
25func hasstr(buf: *u8, n: i64, needle: *u8) -> i64 { var m: i64=0; while needle[m]!=(0 as u8){m=m+1} var i: i64=0; while i+m<=n { var k: i64=0; while k<m { if buf[i+k]!=needle[k] {k=m+9} else {k=k+1} } if k==m {return 1} i=i+1 } return 0 }
26
27// SDF sampling grid over the body atlas (inherited; the atlas is the canon these cover)
28const GX: i64 = 32
29const GY: i64 = 110
30const GZ: i64 = 20
31const GC: i64 = 18
32const OX: i64 = 0-288
33const OY: i64 = 0-990
34const OZ: i64 = 0-180
35
36// ---- hoisted by purpose (were raw literals in main) ----
37// a limb counts as BOUND when more than this many vertices map to its bone. INHERITED threshold, named so it is
38// visible; the honest derivation would be a fraction of the limb's vertex share from the atlas, not a count.
39const GA_LIMB_BOUND_MIN_VERTS: i64 = 20
40// packed skin colour r + g*256 + b*65536, used for every vertex and for the unassigned fallback
41const GA_SKIN_R: i64 = 216
42const GA_SKIN_G: i64 = 172
43const GA_SKIN_B: i64 = 152
44const GA_BYTE_RADIX: i64 = 256
45// export buffer. gltf_anim_export takes no cap, so this is a bound the callee cannot announce -- T4 below turns a
46// silent overflow into a RED instead of a silent truncation.
47const GA_OUT_CAP: i64 = 8388608
48// GLB container layout, per glTF 2.0 binary spec: magic 'g'(0x67) at 0, u32 total length at 8, first chunk length at
49// 12; each chunk is 8 header bytes (u32 length, u32 type); BIN chunk type is 'B'(0x42) as the first type byte.
50const GLB_MAGIC_G: i64 = 103
51const GLB_TOTAL_LEN_OFF: i64 = 8
52const GLB_FIRST_CHUNK_LEN_OFF: i64 = 12
53const GLB_HDR_BYTES: i64 = 12
54const GLB_CHUNKHDR_BYTES: i64 = 8
55const GLB_CHUNK_TYPE_BIN: i64 = 66
56const GA_JOINTS: i64 = 9
57const GA_VEC3_WORDS: i64 = 3
58const GA_OUT_PATH: *u8 = "knowledge/nishi_being_walk.glb"
59
60func main() -> i64 {
61 let ctr: *i64 = gv_ctr()
62 gv_head("nx_gltf_anim_gate -- skinned WALKING being -> .glb" as *u8)
63 let skin_rgb: i64 = GA_SKIN_R + GA_SKIN_G*GA_BYTE_RADIX + GA_SKIN_B*GA_BYTE_RADIX*GA_BYTE_RADIX
64 atlas_build(0)
65 let grid: *i64 = sys_mmap((GX+1)*(GY+1)*(GZ+1)*8) as *i64
66 var i: i64 = 0
67 while i <= GX { var jj: i64=0; while jj<=GY { var k: i64=0; while k<=GZ { grid[(i*(GY+1)+jj)*(GZ+1)+k] = ba_sdf(OX+i*GC, OY+jj*GC, OZ+k*GC, 1); k=k+1 } jj=jj+1 } i=i+1 }
68 tm_reset()
69 surface_nets(grid, GX, GY, GZ, OX, OY, OZ, GC, 0, skin_rgb)
70 let NV: i64 = tm_nv()
71 // per-vertex joint + colour, from nearest part
72 let jarr: *i64 = sys_mmap(NV*8) as *i64
73 let vp: *i64 = sys_mmap(GA_VEC3_WORDS*8) as *i64
74 let jc: *i64 = sys_mmap(GA_JOINTS*8) as *i64
75 var b: i64 = 0
76 while b < GA_JOINTS { jc[b]=0; b=b+1 }
77 i = 0
78 while i < NV {
79 tm_vpos(i, vp)
80 let kk: i64 = ba_nearest(vp[0], vp[1], vp[2], 1)
81 var jt: i64 = 0
82 if kk >= 0 { jt = ba_part_bone(kk); tm_vcol(i, ba_part_color(kk)) } else { tm_vcol(i, skin_rgb) }
83 jarr[i] = jt
84 jc[jt] = jc[jt] + 1
85 i = i + 1
86 }
87 gv_puts(" verts=" as *u8); gv_num(NV)
88 gv_puts(" joint spread root=" as *u8); gv_num(jc[0])
89 gv_puts(" Lsh=" as *u8); gv_num(jc[1])
90 gv_puts(" Lel=" as *u8); gv_num(jc[2])
91 gv_puts(" Rsh=" as *u8); gv_num(jc[3])
92 gv_puts(" Lhip=" as *u8); gv_num(jc[5])
93 gv_puts(" Lkn=" as *u8); gv_num(jc[6])
94 gv_puts("\n" as *u8)
95 var t0: i64 = 0
96 if jc[1] > GA_LIMB_BOUND_MIN_VERTS { if jc[5] > GA_LIMB_BOUND_MIN_VERTS { if jc[6] > GA_LIMB_BOUND_MIN_VERTS { t0 = 1 } } }
97 gv_check("T0 vertices bound across the FK bones (arms + legs rigged)" as *u8, t0, ctr)
98
99 let out: *u8 = sys_mmap(GA_OUT_CAP)
100 let total: i64 = gltf_anim_export(out, jarr)
101 gv_puts(" glb bytes=" as *u8); gv_num(total); gv_puts(" cap=" as *u8); gv_num(GA_OUT_CAP); gv_puts("\n" as *u8)
102 // T4 first in ORDER OF TRUST: every later tooth reads `out`, and a total past the cap means the buffer was
103 // overrun before any of them looked. A cap the callee cannot announce must at least be CAUGHT here.
104 var t4: i64 = 0
105 if total > 0 { if total < GA_OUT_CAP { t4 = 1 } }
106 gv_check("T4 export total inside the buffer cap (an overrun would corrupt every tooth below)" as *u8, t4, ctr)
107
108 // self-verify container + rigging/animation presence
109 var t1: i64 = 0
110 if (out[0] as i64)==GLB_MAGIC_G { if ru32(out,GLB_TOTAL_LEN_OFF)==total { t1=1 } }
111 gv_check("T1 glTF header magic + declared total length" as *u8, t1, ctr)
112 let jsonLen: i64 = ru32(out,GLB_FIRST_CHUNK_LEN_OFF)
113 let binLenPos: i64 = GLB_HDR_BYTES+GLB_CHUNKHDR_BYTES+jsonLen
114 let binLen: i64 = ru32(out,binLenPos)
115 var t2: i64 = 0
116 if (GLB_HDR_BYTES+GLB_CHUNKHDR_BYTES+jsonLen+GLB_CHUNKHDR_BYTES+binLen)==total { if (out[binLenPos+4] as i64)==GLB_CHUNK_TYPE_BIN { t2=1 } }
117 gv_check("T2 chunk arithmetic sums to total + BIN chunk type" as *u8, t2, ctr)
118 let jsonEnd: i64 = GLB_HDR_BYTES+GLB_CHUNKHDR_BYTES+jsonLen
119 var t3: i64 = 0
120 if hasstr(out,jsonEnd,"animations"as *u8)==1 { if hasstr(out,jsonEnd,"skins"as *u8)==1 { if hasstr(out,jsonEnd,"JOINTS_0"as *u8)==1 { if hasstr(out,jsonEnd,"\"walk\""as *u8)==1 { t3=1 } } } }
121 gv_check("T3 JSON declares skin + JOINTS_0 + a 'walk' animation" as *u8, t3, ctr)
122 // NEG-CONTROL: T3 is a substring test, and an EMPTY animations array beside a node merely NAMED "walk" would
123 // satisfy every one of its four needles. Assert the empty-array spelling is ABSENT, paired with T3 via gv_bite so
124 // a JSON emitter that declared the key and nothing under it cannot score a pass.
125 var empty_anims: i64 = hasstr(out,jsonEnd,"\"animations\":[]"as *u8)
126 gv_bite("neg-control-walk-animation-is-not-an-empty-array" as *u8, 1 - empty_anims, 1 - t3, ctr)
127
128 let fd: i64 = sys_openat_wr(GA_OUT_PATH, MODE_0644)
129 let wr: i64 = sys_write(fd, out, total)
130 sys_close(fd)
131 gv_puts(" wrote " as *u8); gv_puts(GA_OUT_PATH); gv_puts(" bytes=" as *u8); gv_num(wr); gv_puts(" of=" as *u8); gv_num(total); gv_puts("\n" as *u8)
132 var t5: i64 = 0
133 if wr == total { t5 = 1 }
134 gv_check("T5 artifact written in full (bytes written == export total)" as *u8, t5, ctr)
135
136 let rc: i64 = gv_verdict("GLTF-ANIM-GATE" as *u8, ctr, "the being exports as a SKINNED, WALKING .glb (9-joint FK rig + walk-cycle quaternion keyframes); residual: skinning in our own web viewer" as *u8)
137 sys_exit(rc)
138 return rc
139}