nx_nxa_check.nx source
↩ module page · 255 lines · 11565 B
1// nx_nxa_check.nx -- the NXA format's fsck. Validates an .nxa BEYOND the checksums nxa_find
2// already enforces: CLUS coverage invariants (every triangle in exactly one cluster, counts
3// <=128, cluster bounds truly CONTAIN their triangles' vertices). A format is only as strong
4// as the tool that refuses a bad file -- this is that tool ("a session must remember < a tool
5// that cannot do the wrong thing").
6// usage: nx_nxa_check <file.nxa> rc: 0 green, 1 invariant broken, 5 corrupt, 6 future
7// license_tier: ORIGINAL
8import "nx_syscalls.nx"
9import "nx_nxa.nx"
10const K_MAGIC_65535: i64 = 65535
11const K_MAGIC_32767: i64 = 32767
12const K_MAGIC_65536: i64 = 65536
13const K_MAGIC_4096: i64 = 4096
14const K_MAGIC_13421772: i64 = 13421772
15const K_MAGIC_20132659: i64 = 20132659
16
17func ncw(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
18func ncn(v: i64) -> i64 {
19 let t: *u8 = sys_mmap(32) as *u8
20 var m: i64 = v; var w: i64 = 0
21 if m<0 { t[w]=45 as u8; w=w+1; m=0-m }
22 if m==0 { t[w]=48 as u8; sys_write(1,t,w+1); return 0 }
23 let d: *u8 = sys_mmap(32) as *u8
24 var k: i64=0
25 while m>0 { d[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
26 var j: i64=0
27 while j<k { t[w]=d[k-1-j]; w=w+1; j=j+1 }
28 sys_write(1,t,w); return 0
29}
30
31// sign-extend a 16-bit lane out of an i64 word (channel-2 packed ANIM keys)
32func nc_i16(w: i64, sh: i64) -> i64 {
33 var v: i64 = (w >> sh) & K_MAGIC_65535
34 if v > K_MAGIC_32767 { v = v - K_MAGIC_65536 }
35 return v
36}
37
38func main(argc: i64, argv: *i64) -> i64 {
39 if argc < 2 { ncw("usage: nx_nxa_check <file.nxa>\n" as *u8); return 2 }
40 let lp: *i64 = sys_mmap(16) as *i64
41 let b: *u8 = sys_map_file(argv[1] as *u8, lp)
42 let flen: i64 = lp[0]
43 if flen < 96 { ncw("unreadable\n" as *u8); return 5 }
44 let h: *i64 = b as *i64
45 let vwo: i64 = nxa_find(b, flen, nxa_tag4("VERT" as *u8))
46 if vwo == 0 - 2 { ncw("NXA-FUTURE-VERSION\n" as *u8); return 6 }
47 if vwo < 0 { ncw("NXA-CORRUPT vert\n" as *u8); return 5 }
48 let two: i64 = nxa_find(b, flen, nxa_tag4("TRIS" as *u8))
49 if two < 0 { ncw("NXA-CORRUPT tris\n" as *u8); return 5 }
50 let nv: i64 = h[vwo]
51 let nt: i64 = h[two]
52 let vx: *i64 = ((b as i64) + vwo*8 + 8) as *i64
53 let tr: *i64 = ((b as i64) + two*8 + 8) as *i64
54 // TRIS invariant: every index in range
55 var t0: i64 = 0
56 while t0 < nt*3 {
57 if tr[t0] < 0 { ncw("RED tri index negative\n" as *u8); return 1 }
58 if tr[t0] >= nv { ncw("RED tri index out of range\n" as *u8); return 1 }
59 t0 = t0 + 1
60 }
61 ncw("verts=" as *u8); ncn(nv); ncw(" tris=" as *u8); ncn(nt)
62 let cwo: i64 = nxa_find(b, flen, nxa_tag4("CLUS" as *u8))
63 if cwo == 0 - 3 { ncw(" NXA-CORRUPT clus\n" as *u8); return 5 }
64 // CLUS is OPTIONAL like every rig section -- absence must NOT skip the rig teeth
65 // (the early-return here left cluster-less rigged files with ZERO validation, seq1286)
66 if cwo < 0 { ncw(" clusters=absent" as *u8) }
67 if cwo >= 0 {
68 let ncl: i64 = h[cwo]
69 let cl: *i64 = ((b as i64) + cwo*8 + 8) as *i64
70 ncw(" clusters=" as *u8); ncn(ncl); ncw("\n" as *u8)
71 var covered: i64 = 0
72 var ci: i64 = 0
73 while ci < ncl {
74 let cb: i64 = ci*10
75 let ts: i64 = cl[cb]
76 let tc: i64 = cl[cb+1]
77 if tc < 1 { ncw("RED cluster empty\n" as *u8); return 1 }
78 if tc > 128 { ncw("RED cluster >128 tris\n" as *u8); return 1 }
79 if ts != covered { ncw("RED cluster ranges not contiguous\n" as *u8); return 1 }
80 if ts + tc > nt { ncw("RED cluster past tris\n" as *u8); return 1 }
81 // bounds truly contain every vertex of every triangle in the cluster
82 var q: i64 = 0
83 while q < tc {
84 var e: i64 = 0
85 while e < 3 {
86 let vi: i64 = tr[(ts+q)*3 + e]
87 var a: i64 = 0
88 while a < 3 {
89 let v: i64 = vx[vi*3 + a]
90 if v < cl[cb+2+a] { ncw("RED vertex below cluster bounds\n" as *u8); return 1 }
91 if v > cl[cb+5+a] { ncw("RED vertex above cluster bounds\n" as *u8); return 1 }
92 a = a + 1
93 }
94 e = e + 1
95 }
96 q = q + 1
97 }
98 covered = covered + tc
99 ci = ci + 1
100 }
101 if covered != nt { ncw("RED clusters cover " as *u8); ncn(covered); ncw(" of " as *u8); ncn(nt); ncw("\n" as *u8); return 1 }
102 ncw("coverage=" as *u8); ncn(covered); ncw("/" as *u8); ncn(nt)
103 }
104 // SKEL/SKIN (optional pair): parents valid + acyclic; weights sum EXACTLY 4096 per vertex
105 let swo: i64 = nxa_find(b, flen, nxa_tag4("SKEL" as *u8))
106 if swo == 0 - 3 { ncw(" NXA-CORRUPT skel\n" as *u8); return 5 }
107 let kwo: i64 = nxa_find(b, flen, nxa_tag4("SKIN" as *u8))
108 if kwo == 0 - 3 { ncw(" NXA-CORRUPT skin\n" as *u8); return 5 }
109 if swo >= 0 {
110 let njj: i64 = h[swo]
111 let sk: *i64 = ((b as i64) + swo*8 + 8) as *i64
112 var jj: i64 = 0
113 while jj < njj {
114 let par: i64 = sk[jj*8]
115 if par >= njj { ncw(" RED joint parent out of range\n" as *u8); return 1 }
116 if par == jj { ncw(" RED joint is its own parent\n" as *u8); return 1 }
117 // acyclic: walking up must reach a root within njj hops
118 var cur: i64 = jj
119 var hops: i64 = 0
120 while cur >= 0 {
121 cur = sk[cur*8]
122 hops = hops + 1
123 if hops > njj { ncw(" RED joint parent cycle\n" as *u8); return 1 }
124 }
125 jj = jj + 1
126 }
127 ncw(" joints=" as *u8); ncn(njj)
128 }
129 if kwo >= 0 {
130 if swo < 0 { ncw(" RED skin without skeleton\n" as *u8); return 1 }
131 let nkv: i64 = h[kwo]
132 if nkv != nv { ncw(" RED skin vert count mismatch\n" as *u8); return 1 }
133 let njj2: i64 = h[swo]
134 let kn: *i64 = ((b as i64) + kwo*8 + 8) as *i64
135 var vv: i64 = 0
136 while vv < nkv {
137 var wsum: i64 = 0
138 var s: i64 = 0
139 while s < 4 {
140 if kn[vv*8+s] < 0 { ncw(" RED skin joint negative\n" as *u8); return 1 }
141 if kn[vv*8+s] >= njj2 { ncw(" RED skin joint out of range\n" as *u8); return 1 }
142 wsum = wsum + kn[vv*8+4+s]
143 s = s + 1
144 }
145 if wsum != K_MAGIC_4096 { ncw(" RED skin weights sum != 4096\n" as *u8); return 1 }
146 vv = vv + 1
147 }
148 ncw(" skin=" as *u8); ncn(nkv)
149 }
150 // ANIM (optional): joint range, monotonic key times, quats near unit length
151 let awo: i64 = nxa_find(b, flen, nxa_tag4("ANIM" as *u8))
152 if awo == 0 - 3 { ncw(" NXA-CORRUPT anim\n" as *u8); return 5 }
153 if awo >= 0 {
154 if swo < 0 { ncw(" RED anim without skeleton\n" as *u8); return 1 }
155 let njj3: i64 = h[swo]
156 let ntr: i64 = h[awo]
157 var rp: i64 = awo + 1
158 var tr2: i64 = 0
159 while tr2 < ntr {
160 let jt: i64 = h[rp]
161 if jt < 0 { ncw(" RED anim joint negative\n" as *u8); return 1 }
162 if jt >= njj3 { ncw(" RED anim joint out of range\n" as *u8); return 1 }
163 let ach: i64 = h[rp+1]
164 let nk: i64 = h[rp+2]
165 if nk < 1 { ncw(" RED anim track empty\n" as *u8); return 1 }
166 var pt2: i64 = 0 - 1
167 var kk2: i64 = 0
168 if ach == 2 {
169 // channel 2 = PACKED world-delta keys, 2 words:
170 // w0 [t_ms u16][dqx dqy dqz i16 q12] / w1 [dqw i16][dt xyz i16 mm]
171 while kk2 < nk {
172 let kb2: i64 = rp + 3 + kk2*2
173 let tt: i64 = h[kb2] & K_MAGIC_65535
174 if tt <= pt2 { if kk2 > 0 { ncw(" RED anim keys not monotonic\n" as *u8); return 1 } }
175 pt2 = tt
176 let qx: i64 = nc_i16(h[kb2], 16)
177 let qy: i64 = nc_i16(h[kb2], 32)
178 let qz: i64 = nc_i16(h[kb2], 48)
179 let qw: i64 = nc_i16(h[kb2+1], 0)
180 let n2: i64 = qx*qx + qy*qy + qz*qz + qw*qw
181 if n2 < K_MAGIC_13421772 { ncw(" RED anim quat far from unit\n" as *u8); return 1 }
182 if n2 > K_MAGIC_20132659 { ncw(" RED anim quat far from unit\n" as *u8); return 1 }
183 kk2 = kk2 + 1
184 }
185 rp = rp + 3 + nk*2
186 }
187 if ach != 2 {
188 while kk2 < nk {
189 let kb: i64 = rp + 3 + kk2*5
190 if h[kb] <= pt2 { if kk2 > 0 { ncw(" RED anim keys not monotonic\n" as *u8); return 1 } }
191 pt2 = h[kb]
192 let n2: i64 = h[kb+1]*h[kb+1] + h[kb+2]*h[kb+2] + h[kb+3]*h[kb+3] + h[kb+4]*h[kb+4]
193 if n2 < K_MAGIC_13421772 { ncw(" RED anim quat far from unit\n" as *u8); return 1 }
194 if n2 > K_MAGIC_20132659 { ncw(" RED anim quat far from unit\n" as *u8); return 1 }
195 kk2 = kk2 + 1
196 }
197 rp = rp + 3 + nk*5
198 }
199 tr2 = tr2 + 1
200 }
201 ncw(" anim_tracks=" as *u8); ncn(ntr)
202 }
203 // POSE (optional): pose_id unique, entry joints in range, quats near unit, walked
204 // length == the TOC's section length (an internal count cannot lie within a valid
205 // checksum -- this tooth closes that gap)
206 let pwo: i64 = nxa_find(b, flen, nxa_tag4("POSE" as *u8))
207 if pwo == 0 - 3 { ncw(" NXA-CORRUPT pose\n" as *u8); return 5 }
208 if pwo >= 0 {
209 if swo < 0 { ncw(" RED pose without skeleton\n" as *u8); return 1 }
210 let njp: i64 = h[swo]
211 let npz: i64 = h[pwo]
212 if npz < 1 { ncw(" RED pose count out of range\n" as *u8); return 1 }
213 if npz > 256 { ncw(" RED pose count out of range\n" as *u8); return 1 }
214 let seen: *i64 = sys_mmap(256*8 + 64) as *i64
215 var rp5: i64 = pwo + 1
216 var pz: i64 = 0
217 while pz < npz {
218 let pid: i64 = h[rp5]
219 var s9: i64 = 0
220 while s9 < pz {
221 if seen[s9] == pid { ncw(" RED pose id duplicate\n" as *u8); return 1 }
222 s9 = s9 + 1
223 }
224 seen[pz] = pid
225 let ne: i64 = h[rp5+1]
226 if ne < 1 { ncw(" RED pose entries out of range\n" as *u8); return 1 }
227 if ne > njp { ncw(" RED pose entries out of range\n" as *u8); return 1 }
228 var e9: i64 = 0
229 while e9 < ne {
230 let eb: i64 = rp5 + 2 + e9*8
231 if h[eb] < 0 { ncw(" RED pose joint out of range\n" as *u8); return 1 }
232 if h[eb] >= njp { ncw(" RED pose joint out of range\n" as *u8); return 1 }
233 let n3: i64 = h[eb+1]*h[eb+1] + h[eb+2]*h[eb+2] + h[eb+3]*h[eb+3] + h[eb+4]*h[eb+4]
234 if n3 < K_MAGIC_13421772 { ncw(" RED pose quat far from unit\n" as *u8); return 1 }
235 if n3 > K_MAGIC_20132659 { ncw(" RED pose quat far from unit\n" as *u8); return 1 }
236 e9 = e9 + 1
237 }
238 rp5 = rp5 + 2 + ne*8
239 pz = pz + 1
240 }
241 // walked words must equal the TOC's declared section length
242 let nsq: i64 = h[2]
243 let tbq: *i64 = ((b as i64) + 32) as *i64
244 var sq: i64 = 0
245 while sq < nsq {
246 if tbq[sq*4] == nxa_tag4("POSE" as *u8) {
247 if rp5 - pwo != tbq[sq*4+2] { ncw(" RED pose length mismatch\n" as *u8); return 1 }
248 }
249 sq = sq + 1
250 }
251 ncw(" poses=" as *u8); ncn(npz)
252 }
253 ncw(" NXA-CHECK GREEN\n" as *u8)
254 return 0
255}