nx_pmx.nx source
↩ module page · 557 lines · 21931 B
1// nx_pmx.nx -- PMX (MikuMikuDance 2.x) MODEL READER: header, section census, and the BONE TABLE.
2//
3// WHY: measured 2026-08-07 from vrstormlab.com/dancexr/preparecontent -- PMX is DanceXR's primary
4// model format. It is also where the MMD standard BONE NAMES live, and bone names are what a
5// retarget binds to. nx_bvhfk gives us third-party motion in world space; PMX gives us the named
6// skeleton that motion is authored against.
7//
8// *YOU CANNOT REACH THE BONES WITHOUT WALKING EVERYTHING, WHICH IS WHY THE WALK PROVES ITSELF.
9// PMX vertex records are VARIABLE LENGTH -- the size depends on a per-vertex weight-deform byte
10// (BDEF1/BDEF2/BDEF4/SDEF/QDEF) and on index widths declared in the header globals. Miscount one
11// vertex by one byte and the bone section is garbage. There is no way to "just skip to bones".
12//
13// FAIL-CLOSED: every read is bounds-checked against the ACTUAL file length before it happens.
14// A count in a header is a CLAIM, never an authority -- the same discipline as nx_vmd, and here it
15// matters more because a lying vertex count would drive a variable-stride walk off the buffer.
16//
17// TEXT: PMX globals[0] declares UTF-16LE (0) or UTF-8 (1). Names are emitted as UTF-8 either way;
18// UTF-16LE is transcoded (BMP). We do NOT guess an encoding -- the file states it.
19//
20// usage: nx_pmx info <file.pmx> version, encoding, index widths, per-section counts
21// nx_pmx bones <file.pmx> bone table: index, parent, position, name
22// nx_pmx --kat selftest: 6 teeth, 3 of them anti-vacuity
23// exit 0 ok | 2 unreadable | 3 usage | 4 REFUSED | 1 KAT RED
24// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
25import "nx_gate_verdict.nx"
26const PX_MAGIC_2147483648: i64 = 2147483648
27const PX_MAGIC_4294967296: i64 = 4294967296
28const PX_MAGIC_32768: i64 = 32768
29const PX_MAGIC_65536: i64 = 65536
30const PX_MAGIC_2048: i64 = 2048
31const PX_MAGIC_1073741824: i64 = 1073741824
32const PX_MAGIC_1000000: i64 = 1000000
33
34const PX_HDRSIG: i64 = 4
35const PX_MAXBONE: i64 = 1024
36const PX_NAMEW: i64 = 128
37const PX_ENC_U16: i64 = 0
38const PX_ENC_U8: i64 = 1
39const PX_KATBUF: i64 = 8192
40
41// globals slots
42const PX_G_ENC: i64 = 0
43const PX_G_ADDUV: i64 = 1
44const PX_G_VIDX: i64 = 2
45const PX_G_TIDX: i64 = 3
46const PX_G_MIDX: i64 = 4
47const PX_G_BIDX: i64 = 5
48const PX_G_MOIDX: i64 = 6
49const PX_G_RIDX: i64 = 7
50
51// weight deform types
52const PX_BDEF1: i64 = 0
53const PX_BDEF2: i64 = 1
54const PX_BDEF4: i64 = 2
55const PX_SDEF: i64 = 3
56const PX_QDEF: i64 = 4
57
58// bone flags
59const PX_BF_TAILIDX: i64 = 1
60const PX_BF_IK: i64 = 32
61const PX_BF_INH_ROT: i64 = 256
62const PX_BF_INH_TRN: i64 = 512
63const PX_BF_FIXAXIS: i64 = 1024
64const PX_BF_LOCALAX: i64 = 2048
65const PX_BF_EXTPAR: i64 = 8192
66
67// refusal codes
68const PX_R_MAGIC: i64 = 0 - 1
69const PX_R_TRUNC: i64 = 0 - 2
70const PX_R_COUNT: i64 = 0 - 3
71const PX_R_CAP: i64 = 0 - 4
72const PX_R_DEFORM:i64 = 0 - 5
73
74func px_slen(s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { i = i + 1 } return i }
75
76func px_u8(b: *u8, o: i64) -> i64 { return (b[o] & 0xff) as i64 }
77func px_u16(b: *u8, o: i64) -> i64 { return px_u8(b,o) | (px_u8(b,o+1) << 8) }
78func px_u32(b: *u8, o: i64) -> i64 {
79 return px_u8(b,o) | (px_u8(b,o+1) << 8) | (px_u8(b,o+2) << 16) | (px_u8(b,o+3) << 24)
80}
81func px_i32(b: *u8, o: i64) -> i64 {
82 let v: i64 = px_u32(b, o)
83 if v >= PX_MAGIC_2147483648 { return v - PX_MAGIC_4294967296 }
84 return v
85}
86// SIGNED index of declared width; -1 means "none" and MUST survive as -1, not 255/65535.
87func px_idx(b: *u8, o: i64, w: i64) -> i64 {
88 if w == 1 { let v: i64 = px_u8(b,o); if v >= 128 { return v - 256 } return v }
89 if w == 2 { let v: i64 = px_u16(b,o); if v >= PX_MAGIC_32768 { return v - PX_MAGIC_65536 } return v }
90 return px_i32(b, o)
91}
92
93// ---- cursor discipline: C[0] = offset, and every advance is bounds-proven first ----
94func px_need(C: *i64, len: i64, n: i64) -> i64 {
95 if C[0] < 0 { return 0 }
96 if C[0] + n > len { return 0 }
97 return 1
98}
99
100// UTF-16LE (BMP) -> UTF-8. The file DECLARES its encoding; we never sniff it.
101func px_u16_to_u8(src: *u8, n: i64, dst: *u8, cap: i64) -> i64 {
102 var i: i64 = 0
103 var o: i64 = 0
104 while i + 1 < n {
105 let c: i64 = px_u8(src, i) | (px_u8(src, i+1) << 8)
106 if c < 128 { if o + 1 < cap { dst[o] = c as u8; o = o + 1 } }
107 else {
108 if c < PX_MAGIC_2048 {
109 if o + 2 < cap {
110 dst[o] = (192 | (c >> 6)) as u8
111 dst[o+1] = (128 | (c & 63)) as u8
112 o = o + 2
113 }
114 } else {
115 if o + 3 < cap {
116 dst[o] = (224 | (c >> 12)) as u8
117 dst[o+1] = (128 | ((c >> 6) & 63)) as u8
118 dst[o+2] = (128 | (c & 63)) as u8
119 o = o + 3
120 }
121 }
122 }
123 i = i + 2
124 }
125 dst[o] = 0 as u8
126 return o
127}
128
129// read one PMX text field (int32 byte-length + bytes) into dst as UTF-8
130func px_text(b: *u8, C: *i64, len: i64, enc: i64, dst: *u8, cap: i64) -> i64 {
131 if px_need(C, len, 4) == 0 { C[0] = 0 - 1; return PX_R_TRUNC }
132 let n: i64 = px_i32(b, C[0])
133 C[0] = C[0] + 4
134 if n < 0 { C[0] = 0 - 1; return PX_R_COUNT }
135 if px_need(C, len, n) == 0 { C[0] = 0 - 1; return PX_R_TRUNC }
136 if (dst as i64) != 0 {
137 if enc == PX_ENC_U16 { px_u16_to_u8(((b as i64) + C[0]) as *u8, n, dst, cap) }
138 else {
139 var i: i64 = 0
140 while i < n { if i < cap - 1 { dst[i] = b[C[0] + i] } i = i + 1 }
141 var e: i64 = n
142 if e > cap - 1 { e = cap - 1 }
143 dst[e] = 0 as u8
144 }
145 }
146 C[0] = C[0] + n
147 return 0
148}
149
150// per-vertex stride is DATA, not a constant: deform type + declared index widths decide it
151func px_vertex(b: *u8, C: *i64, len: i64, G: *i64) -> i64 {
152 let base: i64 = 12 + 12 + 8 + G[PX_G_ADDUV] * 16
153 if px_need(C, len, base + 1) == 0 { return PX_R_TRUNC }
154 C[0] = C[0] + base
155 let dt: i64 = px_u8(b, C[0])
156 C[0] = C[0] + 1
157 let w: i64 = G[PX_G_BIDX]
158 var need: i64 = 0
159 if dt == PX_BDEF1 { need = w }
160 else { if dt == PX_BDEF2 { need = w * 2 + 4 }
161 else { if dt == PX_BDEF4 { need = w * 4 + 16 }
162 else { if dt == PX_SDEF { need = w * 2 + 4 + 36 }
163 else { if dt == PX_QDEF { need = w * 4 + 16 }
164 else { return PX_R_DEFORM } } } } }
165 if px_need(C, len, need + 4) == 0 { return PX_R_TRUNC }
166 C[0] = C[0] + need + 4
167 return 0
168}
169
170func px_material(b: *u8, C: *i64, len: i64, G: *i64) -> i64 {
171 let enc: i64 = G[PX_G_ENC]
172 if px_text(b, C, len, enc, 0 as *u8, 0) < 0 { return PX_R_TRUNC }
173 if px_text(b, C, len, enc, 0 as *u8, 0) < 0 { return PX_R_TRUNC }
174 let fixed: i64 = 16 + 12 + 4 + 12 + 1 + 16 + 4
175 if px_need(C, len, fixed) == 0 { return PX_R_TRUNC }
176 C[0] = C[0] + fixed
177 let tw: i64 = G[PX_G_TIDX]
178 if px_need(C, len, tw * 2 + 2) == 0 { return PX_R_TRUNC }
179 C[0] = C[0] + tw * 2
180 C[0] = C[0] + 1
181 let toonref: i64 = px_u8(b, C[0])
182 C[0] = C[0] + 1
183 if toonref == 1 {
184 if px_need(C, len, 1) == 0 { return PX_R_TRUNC }
185 C[0] = C[0] + 1
186 } else {
187 if px_need(C, len, tw) == 0 { return PX_R_TRUNC }
188 C[0] = C[0] + tw
189 }
190 if px_text(b, C, len, enc, 0 as *u8, 0) < 0 { return PX_R_TRUNC }
191 if px_need(C, len, 4) == 0 { return PX_R_TRUNC }
192 C[0] = C[0] + 4
193 return 0
194}
195
196// H slots
197const PX_H_VER: i64 = 0
198const PX_H_NVERT: i64 = 1
199const PX_H_NSURF: i64 = 2
200const PX_H_NTEX: i64 = 3
201const PX_H_NMAT: i64 = 4
202const PX_H_NBONE: i64 = 5
203const PX_H_BONEOFF: i64 = 6
204const PX_H_WORDS: i64 = 12
205
206func px_walk(b: *u8, len: i64, G: *i64, H: *i64, names: *u8, bparent: *i64, bpos: *i64) -> i64 {
207 if len < 8 { return PX_R_MAGIC }
208 if b[0] != (80 as u8) { return PX_R_MAGIC }
209 if b[1] != (77 as u8) { return PX_R_MAGIC }
210 if b[2] != (88 as u8) { return PX_R_MAGIC }
211 if b[3] != (32 as u8) { return PX_R_MAGIC }
212 H[PX_H_VER] = px_u32(b, 4) // f32 bits; reported raw, never reinterpreted as a float
213 let C: *i64 = sys_mmap(16) as *i64
214 C[0] = 8
215 if px_need(C, len, 1) == 0 { return PX_R_TRUNC }
216 let ng: i64 = px_u8(b, C[0])
217 C[0] = C[0] + 1
218 if ng < 8 { return PX_R_COUNT }
219 if px_need(C, len, ng) == 0 { return PX_R_TRUNC }
220 var i: i64 = 0
221 while i < 8 { G[i] = px_u8(b, C[0] + i); i = i + 1 }
222 C[0] = C[0] + ng
223 let enc: i64 = G[PX_G_ENC]
224
225 // four header texts: name local, name universal, comment local, comment universal
226 if px_text(b, C, len, enc, names, PX_NAMEW) < 0 { return PX_R_TRUNC }
227 if px_text(b, C, len, enc, 0 as *u8, 0) < 0 { return PX_R_TRUNC }
228 if px_text(b, C, len, enc, 0 as *u8, 0) < 0 { return PX_R_TRUNC }
229 if px_text(b, C, len, enc, 0 as *u8, 0) < 0 { return PX_R_TRUNC }
230
231 // ---- vertices (variable stride) ----
232 if px_need(C, len, 4) == 0 { return PX_R_TRUNC }
233 let nv: i64 = px_i32(b, C[0])
234 C[0] = C[0] + 4
235 if nv < 0 { return PX_R_COUNT }
236 H[PX_H_NVERT] = nv
237 var v: i64 = 0
238 while v < nv {
239 let r: i64 = px_vertex(b, C, len, G)
240 if r < 0 { return r }
241 v = v + 1
242 }
243 // ---- surfaces (index count, NOT triangle count) ----
244 if px_need(C, len, 4) == 0 { return PX_R_TRUNC }
245 let ns: i64 = px_i32(b, C[0])
246 C[0] = C[0] + 4
247 if ns < 0 { return PX_R_COUNT }
248 H[PX_H_NSURF] = ns
249 if px_need(C, len, ns * G[PX_G_VIDX]) == 0 { return PX_R_TRUNC }
250 C[0] = C[0] + ns * G[PX_G_VIDX]
251 // ---- textures ----
252 if px_need(C, len, 4) == 0 { return PX_R_TRUNC }
253 let nt: i64 = px_i32(b, C[0])
254 C[0] = C[0] + 4
255 if nt < 0 { return PX_R_COUNT }
256 H[PX_H_NTEX] = nt
257 var t: i64 = 0
258 while t < nt {
259 if px_text(b, C, len, enc, 0 as *u8, 0) < 0 { return PX_R_TRUNC }
260 t = t + 1
261 }
262 // ---- materials ----
263 if px_need(C, len, 4) == 0 { return PX_R_TRUNC }
264 let nm: i64 = px_i32(b, C[0])
265 C[0] = C[0] + 4
266 if nm < 0 { return PX_R_COUNT }
267 H[PX_H_NMAT] = nm
268 var m: i64 = 0
269 while m < nm {
270 let rm: i64 = px_material(b, C, len, G)
271 if rm < 0 { return rm }
272 m = m + 1
273 }
274 // ---- bones ----
275 if px_need(C, len, 4) == 0 { return PX_R_TRUNC }
276 let nb: i64 = px_i32(b, C[0])
277 C[0] = C[0] + 4
278 if nb < 0 { return PX_R_COUNT }
279 if nb > PX_MAXBONE { return PX_R_CAP }
280 H[PX_H_NBONE] = nb
281 H[PX_H_BONEOFF] = C[0]
282 let bw: i64 = G[PX_G_BIDX]
283 var k: i64 = 0
284 while k < nb {
285 if px_text(b, C, len, enc, ((names as i64) + (k + 1) * PX_NAMEW) as *u8, PX_NAMEW) < 0 { return PX_R_TRUNC }
286 if px_text(b, C, len, enc, 0 as *u8, 0) < 0 { return PX_R_TRUNC }
287 if px_need(C, len, 12) == 0 { return PX_R_TRUNC }
288 bpos[k*3] = px_u32(b, C[0])
289 bpos[k*3 + 1] = px_u32(b, C[0] + 4)
290 bpos[k*3 + 2] = px_u32(b, C[0] + 8)
291 C[0] = C[0] + 12
292 if px_need(C, len, bw + 4 + 2) == 0 { return PX_R_TRUNC }
293 bparent[k] = px_idx(b, C[0], bw)
294 C[0] = C[0] + bw + 4
295 let flags: i64 = px_u16(b, C[0])
296 C[0] = C[0] + 2
297 // every conditional field below is DECLARED BY THE FLAGS -- skipping one desyncs the rest
298 if (flags & PX_BF_TAILIDX) != 0 {
299 if px_need(C, len, bw) == 0 { return PX_R_TRUNC }
300 C[0] = C[0] + bw
301 } else {
302 if px_need(C, len, 12) == 0 { return PX_R_TRUNC }
303 C[0] = C[0] + 12
304 }
305 if (flags & PX_BF_INH_ROT) != 0 {
306 if px_need(C, len, bw + 4) == 0 { return PX_R_TRUNC }
307 C[0] = C[0] + bw + 4
308 } else {
309 if (flags & PX_BF_INH_TRN) != 0 {
310 if px_need(C, len, bw + 4) == 0 { return PX_R_TRUNC }
311 C[0] = C[0] + bw + 4
312 }
313 }
314 if (flags & PX_BF_FIXAXIS) != 0 {
315 if px_need(C, len, 12) == 0 { return PX_R_TRUNC }
316 C[0] = C[0] + 12
317 }
318 if (flags & PX_BF_LOCALAX) != 0 {
319 if px_need(C, len, 24) == 0 { return PX_R_TRUNC }
320 C[0] = C[0] + 24
321 }
322 if (flags & PX_BF_EXTPAR) != 0 {
323 if px_need(C, len, 4) == 0 { return PX_R_TRUNC }
324 C[0] = C[0] + 4
325 }
326 if (flags & PX_BF_IK) != 0 {
327 if px_need(C, len, bw + 4 + 4 + 4) == 0 { return PX_R_TRUNC }
328 C[0] = C[0] + bw + 4 + 4
329 let nl: i64 = px_i32(b, C[0])
330 C[0] = C[0] + 4
331 if nl < 0 { return PX_R_COUNT }
332 var l: i64 = 0
333 while l < nl {
334 if px_need(C, len, bw + 1) == 0 { return PX_R_TRUNC }
335 C[0] = C[0] + bw
336 let lim: i64 = px_u8(b, C[0])
337 C[0] = C[0] + 1
338 if lim == 1 {
339 if px_need(C, len, 24) == 0 { return PX_R_TRUNC }
340 C[0] = C[0] + 24
341 }
342 l = l + 1
343 }
344 }
345 k = k + 1
346 }
347 return 0
348}
349
350func px_refuse(c: i64) -> i64 {
351 gv_puts("nx_pmx REFUSED code=" as *u8); gv_num(0 - c)
352 if c == PX_R_MAGIC { gv_puts(" not-a-PMX" as *u8) }
353 if c == PX_R_TRUNC { gv_puts(" truncated-or-count-exceeds-bytes" as *u8) }
354 if c == PX_R_COUNT { gv_puts(" negative-count" as *u8) }
355 if c == PX_R_CAP { gv_puts(" bone-cap" as *u8) }
356 if c == PX_R_DEFORM{ gv_puts(" unknown-weight-deform-type" as *u8) }
357 gv_puts("\n" as *u8)
358 return 4
359}
360
361// ===== KAT ========================================================
362func px_put(b: *u8, o: i64, s: *u8) -> i64 {
363 let n: i64 = px_slen(s)
364 var i: i64 = 0
365 while i < n { b[o + i] = s[i]; i = i + 1 }
366 return o + n
367}
368func px_p32(b: *u8, o: i64, v: i64) -> i64 {
369 b[o] = (v & 0xff) as u8; b[o+1] = ((v >> 8) & 0xff) as u8
370 b[o+2] = ((v >> 16) & 0xff) as u8; b[o+3] = ((v >> 24) & 0xff) as u8
371 return o + 4
372}
373func px_ptext(b: *u8, o: i64, s: *u8) -> i64 {
374 let n: i64 = px_slen(s)
375 var p: i64 = px_p32(b, o, n)
376 var i: i64 = 0
377 while i < n { b[p + i] = s[i]; i = i + 1 }
378 return p + n
379}
380
381// A REAL minimal PMX 2.0: UTF-8, 1-byte indices, 1 vertex (BDEF1), 3 surfaces, 0 textures,
382// 1 material, 2 bones (flags 0 -> vec3 tail, no IK/inherit/axis).
383func px_fixture(b: *u8) -> i64 {
384 var i: i64 = 0
385 while i < PX_KATBUF { b[i] = 0 as u8; i = i + 1 }
386 var o: i64 = px_put(b, 0, "PMX " as *u8)
387 o = px_p32(b, o, PX_MAGIC_1073741824) // f32 2.0 bits
388 b[o] = 8 as u8; o = o + 1
389 b[o] = 1 as u8; o = o + 1 // encoding UTF-8
390 b[o] = 0 as u8; o = o + 1 // addUV 0
391 b[o] = 1 as u8; o = o + 1 // vertex idx 1
392 b[o] = 1 as u8; o = o + 1 // texture idx 1
393 b[o] = 1 as u8; o = o + 1 // material idx 1
394 b[o] = 1 as u8; o = o + 1 // bone idx 1
395 b[o] = 1 as u8; o = o + 1 // morph idx 1
396 b[o] = 1 as u8; o = o + 1 // rigidbody idx 1
397 o = px_ptext(b, o, "KAT" as *u8)
398 o = px_ptext(b, o, "KAT" as *u8)
399 o = px_ptext(b, o, "" as *u8)
400 o = px_ptext(b, o, "" as *u8)
401 o = px_p32(b, o, 1) // 1 vertex
402 o = o + 12 + 12 + 8 // pos, normal, uv
403 b[o] = 0 as u8; o = o + 1 // BDEF1
404 b[o] = 0 as u8; o = o + 1 // bone index
405 o = o + 4 // edge scale
406 o = px_p32(b, o, 3) // 3 surface indices
407 o = o + 3
408 o = px_p32(b, o, 0) // 0 textures
409 o = px_p32(b, o, 1) // 1 material
410 o = px_ptext(b, o, "MAT" as *u8)
411 o = px_ptext(b, o, "" as *u8)
412 o = o + 16 + 12 + 4 + 12 // diffuse specular specularity ambient
413 b[o] = 0 as u8; o = o + 1 // draw flags
414 o = o + 16 + 4 // edge colour + scale
415 b[o] = 0 as u8; o = o + 1 // texture index
416 b[o] = 0 as u8; o = o + 1 // environment index
417 b[o] = 0 as u8; o = o + 1 // environment blend
418 b[o] = 1 as u8; o = o + 1 // toon reference = internal
419 b[o] = 0 as u8; o = o + 1 // toon value
420 o = px_ptext(b, o, "" as *u8) // meta
421 o = px_p32(b, o, 3) // surface count
422 o = px_p32(b, o, 2) // 2 bones
423 o = px_ptext(b, o, "root" as *u8)
424 o = px_ptext(b, o, "" as *u8)
425 o = o + 12 // position
426 b[o] = 255 as u8; o = o + 1 // parent -1
427 o = px_p32(b, o, 0) // layer
428 b[o] = 0 as u8; b[o+1] = 0 as u8; o = o + 2 // flags 0
429 o = o + 12 // tail vec3
430 o = px_ptext(b, o, "spine" as *u8)
431 o = px_ptext(b, o, "" as *u8)
432 o = o + 12
433 b[o] = 0 as u8; o = o + 1 // parent 0
434 o = px_p32(b, o, 0)
435 b[o] = 0 as u8; b[o+1] = 0 as u8; o = o + 2
436 o = o + 12
437 return o
438}
439
440func px_streq(a: *u8, s: *u8) -> i64 {
441 var i: i64 = 0
442 while s[i] != (0 as u8) { if a[i] != s[i] { return 0 } i = i + 1 }
443 if a[i] != (0 as u8) { return 0 }
444 return 1
445}
446
447func px_kat() -> i64 {
448 let ctr: *i64 = gv_ctr()
449 gv_head("nx_pmx KAT -- PMX walk; the variable-stride vertex record is what makes this provable" as *u8)
450 let b: *u8 = sys_mmap(PX_KATBUF)
451 let G: *i64 = sys_mmap(64) as *i64
452 let H: *i64 = sys_mmap(PX_H_WORDS * 8) as *i64
453 let names: *u8 = sys_mmap((PX_MAXBONE + 2) * PX_NAMEW)
454 let bparent: *i64 = sys_mmap(PX_MAXBONE * 8) as *i64
455 let bpos: *i64 = sys_mmap(PX_MAXBONE * 24) as *i64
456
457 let n: i64 = px_fixture(b)
458 let r: i64 = px_walk(b, n, G, H, names, bparent, bpos)
459
460 var t1: i64 = 0
461 if r == 0 {
462 if H[PX_H_NVERT] == 1 { if H[PX_H_NSURF] == 3 { if H[PX_H_NTEX] == 0 {
463 if H[PX_H_NMAT] == 1 { if H[PX_H_NBONE] == 2 { t1 = 1 } } } } }
464 }
465 gv_check("T1 section counts 1/3/0/1/2" as *u8, t1, ctr)
466
467 var t2: i64 = 0
468 if px_streq(names, "KAT" as *u8) == 1 { t2 = 1 }
469 gv_check("T2 model name decodes" as *u8, t2, ctr)
470
471 // T3 the bone table is the payload: reaching it at all proves the variable-stride vertex walk,
472 // the material walk and the flag-conditional bone walk were all byte-exact.
473 var t3: i64 = 0
474 if px_streq(((names as i64) + PX_NAMEW) as *u8, "root" as *u8) == 1 {
475 if px_streq(((names as i64) + 2 * PX_NAMEW) as *u8, "spine" as *u8) == 1 {
476 if bparent[0] == 0 - 1 { if bparent[1] == 0 { t3 = 1 } } }
477 }
478 gv_check("T3 bone names + parents (-1 root, 0 spine)" as *u8, t3, ctr)
479
480 // T4 ANTI-VACUITY: truncation mid-walk must REFUSE. The header and every count stay perfectly
481 // readable; only a bounds proof catches it.
482 let G2: *i64 = sys_mmap(64) as *i64
483 let H2: *i64 = sys_mmap(PX_H_WORDS * 8) as *i64
484 var t4: i64 = 0
485 if px_walk(b, n - 20, G2, H2, names, bparent, bpos) < 0 { t4 = 1 }
486 gv_check("T4 ANTI-VACUITY truncated file refused" as *u8, t4, ctr)
487
488 // T5 ANTI-VACUITY: bad magic refused before any count is trusted
489 let sav: u8 = b[0]
490 b[0] = 88 as u8
491 var t5: i64 = 0
492 if px_walk(b, n, G2, H2, names, bparent, bpos) == PX_R_MAGIC { t5 = 1 }
493 b[0] = sav
494 gv_check("T5 ANTI-VACUITY bad magic refused" as *u8, t5, ctr)
495
496 // T6 ANTI-VACUITY: THE LYING COUNT. Bytes untouched, magic valid, only the vertex count
497 // overwritten with a number the body cannot hold. A variable-stride walk that trusts it
498 // runs off the buffer.
499 let voff: i64 = 8 + 1 + 8 + 4 + 3 + 4 + 3 + 4 + 4
500 let savc: i64 = px_u32(b, voff)
501 px_p32(b, voff, PX_MAGIC_1000000)
502 var t6: i64 = 0
503 if px_walk(b, n, G2, H2, names, bparent, bpos) < 0 { t6 = 1 }
504 px_p32(b, voff, savc)
505 gv_check("T6 ANTI-VACUITY lying vertex count refused" as *u8, t6, ctr)
506
507 return gv_verdict("PMX-GATE" as *u8, ctr, "variable-stride walk is bounds-proven" as *u8)
508}
509
510func main(argc: i64, argv: *i64) -> i64 {
511 if argc < 2 { gv_puts("usage: nx_pmx info|bones <file.pmx> | --kat\n" as *u8); return 3 }
512 let verb: *u8 = argv[1] as *u8
513 if px_slen(verb) == 5 { if verb[0] == (45 as u8) { let r: i64 = px_kat(); sys_exit(r); return r } }
514 if argc < 3 { gv_puts("usage: nx_pmx info|bones <file.pmx> | --kat\n" as *u8); return 3 }
515 let lenp: *i64 = sys_mmap(16) as *i64
516 let buf: *u8 = sys_read_file(argv[2] as *u8, lenp)
517 if (buf as i64) == 0 { gv_puts("nx_pmx unreadable\n" as *u8); return 2 }
518 let G: *i64 = sys_mmap(64) as *i64
519 let H: *i64 = sys_mmap(PX_H_WORDS * 8) as *i64
520 let names: *u8 = sys_mmap((PX_MAXBONE + 2) * PX_NAMEW)
521 let bparent: *i64 = sys_mmap(PX_MAXBONE * 8) as *i64
522 let bpos: *i64 = sys_mmap(PX_MAXBONE * 24) as *i64
523 let r: i64 = px_walk(buf, lenp[0], G, H, names, bparent, bpos)
524 if r < 0 { return px_refuse(r) }
525
526 if verb[0] == (105 as u8) {
527 gv_puts("PMX bytes=" as *u8); gv_num(lenp[0])
528 gv_puts(" encoding=" as *u8)
529 if G[PX_G_ENC] == PX_ENC_U8 { gv_puts("UTF-8" as *u8) } else { gv_puts("UTF-16LE" as *u8) }
530 gv_puts("\nidx_widths vertex=" as *u8); gv_num(G[PX_G_VIDX])
531 gv_puts(" texture=" as *u8); gv_num(G[PX_G_TIDX])
532 gv_puts(" material=" as *u8); gv_num(G[PX_G_MIDX])
533 gv_puts(" bone=" as *u8); gv_num(G[PX_G_BIDX])
534 gv_puts("\nmodel=" as *u8); gv_puts(names)
535 gv_puts("\nvertices=" as *u8); gv_num(H[PX_H_NVERT])
536 gv_puts(" surface_indices=" as *u8); gv_num(H[PX_H_NSURF])
537 gv_puts(" triangles=" as *u8); gv_num(H[PX_H_NSURF] / 3)
538 gv_puts("\ntextures=" as *u8); gv_num(H[PX_H_NTEX])
539 gv_puts(" materials=" as *u8); gv_num(H[PX_H_NMAT])
540 gv_puts(" bones=" as *u8); gv_num(H[PX_H_NBONE])
541 gv_puts("\n" as *u8)
542 return 0
543 }
544 if verb[0] == (98 as u8) {
545 gv_puts("idx\tparent\tname\n" as *u8)
546 var k: i64 = 0
547 while k < H[PX_H_NBONE] {
548 gv_num(k); gv_puts("\t" as *u8); gv_num(bparent[k]); gv_puts("\t" as *u8)
549 gv_puts(((names as i64) + (k + 1) * PX_NAMEW) as *u8)
550 gv_puts("\n" as *u8)
551 k = k + 1
552 }
553 return 0
554 }
555 gv_puts("nx_pmx unknown verb\n" as *u8)
556 return 3
557}