nx_fbx_texm.nx source
↩ module page · 1301 lines · 54891 B
1// nx_fbx_texm.nx -- AUTHORED FBX MATERIALS -> TEXC + TEXM. The donor half of the clay fix.
2//
3// THE MEASURED GAP THIS CLOSES. The four Fab donors (toon3d8 / dark_witch / dark_knight /
4// stylized_paladin) were ingested rigged (nx_fbx2nxa + nx_nxa_skin) but FLAT: their AUTHORED
5// texture maps were never carried, so the shipped cw_npc_atlas sampling path (TEXC+TEXM ->
6// world shader) has nothing to sample on them. nx_nxa_texm ship-mode BAKES maps from the genome;
7// a donor's surface is not bakeable -- it is authored, and the honest carry is the author's own
8// pixels behind the author's own UVs.
9//
10// RECON-MEASURED FACTS THIS DESIGN RESTS ON (2026-08-23, full 4-donor census, not sampled):
11// - Every geometry's UV layer is ByPolygonVertex + IndexToDirect. One resolution path.
12// - LayerElementMaterial is AllSame on 75 of 79 geometries; ByPolygon on 4. Policy for
13// ByPolygon: FIRST POLYGON WINS per control point -- a declared approximation at material
14// boundaries, never a fabricated map.
15// - Map roles come from OP connections (DiffuseColor / ShininessExponent / ReflectionFactor /
16// NormalMap / TransparencyFactor), never from filename guessing.
17// - Albedo sources: PNG-8 (ctype 0/2/3/6) and BASELINE JPEG (every toon JPG is SOF0).
18// Depth-16 PNG appears ONLY in normal maps -> the normal map is declared absent by depth.
19// - Knight hair material has no DiffuseColor texture -> authored scalar diffuse (P prop) fills
20// its chart. Authored scalar, not a fabricated pixel.
21//
22// SHAPE: one chart per DISTINCT diffuse source (scalar-filled charts get their own cell), grid
23// g = smallest g with g*g >= ncharts (derived), atlas res from knowledge/texture_ship_budget.conf
24// (the SAME owner nx_nxa_texm ship-mode reads -- one owner per number). TEXC stores FINAL
25// atlas-space Q16 UVs with chart ids, exactly the section shape nx_nxa_texc_lib writes, so the
26// shipped world sampler needs NO new convention. FBX v (origin bottom-left) is flipped to the
27// atlas row space (origin top-left) at ingest; declared here, applied once. Tiling UVs outside
28// [0,1] wrap by modulo before the cell mapping (a clamp would smear the border texel).
29//
30// GRAMMAR: nx_fbx_texm <in.fbx> <texdir> <in_rigged.nxa> <out.nxa>
31// EXITS: 0 OK | 1 io | 2 usage | 3 bad-input | 4 missing-section | 5 desync-refused
32//
33// REFUSALS ARE NAMED. Every skipped map and every unreadable source prints WHICH rule fired.
34
35import "nx_syscalls.nx"
36import "nx_nxa.nx"
37import "nx_fbx_core.nx"
38import "nx_inflate.nx"
39import "nx_png.nx"
40import "nx_jpeg_ascii.nx"
41
42const FT_OK: i64 = 0
43const FT_E_IO: i64 = 1
44const FT_E_USAGE: i64 = 2
45const FT_E_BAD: i64 = 3
46const FT_E_NOSEC: i64 = 4
47const FT_E_DESYNC: i64 = 5
48
49// NXA container geometry (format law shared with the texc/texm writers, not tuning).
50const FT_HDR: i64 = 32
51const FT_TOCE: i64 = 32
52const FT_MAXSEC: i64 = 64
53const FT_MODE: i64 = 0x1a4
54const FT_PAD: i64 = 4096
55const FT_WORD: i64 = 8
56
57// TEXC payload header: {nv, stride, grid, 0} then {u_q16, v_q16, chart} per vertex.
58const FT_TX_PHDR: i64 = 4
59const FT_TX_STRIDE: i64 = 3
60const FT_Q16: i64 = 65536
61
62// TEXM payload header words + per-map record words (format law shared with nx_nxa_texm).
63const FT_TM_HDRW: i64 = 6
64const FT_TM_RECW: i64 = 3
65const FT_TM_NMAP: i64 = 4
66const FT_TM_MODEL_METALROUGH: i64 = 2
67// Map slots follow the texbake order: 0 albedo, 1 spec(=metal under metalrough),
68// 2 gloss(=roughness under metalrough), 3 normal.
69const FT_SLOT_ALBEDO: i64 = 0
70const FT_SLOT_METAL: i64 = 1
71const FT_SLOT_ROUGH: i64 = 2
72
73// Ship-resolution owner: the SAME conf nx_nxa_texm ship-mode derives from. One owner per number.
74const FT_SHIP_CONF: *u8 = "knowledge/texture_ship_budget.conf"
75
76
77// Capacity ceilings DERIVED from the recon census (maxima: 33 geoms, 29 mats, 38 texs, 12988
78// conns, 380 models), sized 2-10x above so growth refuses loudly instead of truncating silently.
79const FT_MAXGEOM: i64 = 128
80const FT_MAXMAT: i64 = 128
81const FT_MAXTEX: i64 = 256
82const FT_MAXCONN: i64 = 65536
83const FT_MAXMODEL: i64 = 1024
84const FT_MAXCHART: i64 = 64
85const FT_MAXSLOT: i64 = 64
86const FT_NAMECAP: i64 = 256
87// stack frames are DERIVED per input: one pending frame can exist only per container node,
88// and a node header is at least 13 bytes -- so filesize/13 bounds them absolutely. The old
89// picked 8192 was measured flooding at 8190 on the knight (3,729 subdeformer containers).
90const FT_STACK_MIN: i64 = 1024
91
92const FT_FX_ONE: i64 = 1024 // fbxc_f64_fx unit (nx_fbx_core law)
93
94func ft_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
95func ft_err(s: *u8) -> i64 { sys_write(2, s, ft_slen(s)); return 0 }
96func ft_out(s: *u8) -> i64 { sys_write(1, s, ft_slen(s)); return 0 }
97func ft_outn(v: i64) -> i64 {
98 let b: *u8 = sys_mmap(32)
99 var x: i64 = v
100 var neg: i64 = 0
101 if x < 0 { neg = 1; x = 0 - x }
102 var n: i64 = 0
103 if x == 0 { b[0] = 48 as u8; n = 1 } else {
104 while x > 0 { b[n] = (48 + (x % 10)) as u8; x = x / 10; n = n + 1 }
105 }
106 if neg == 1 { b[n] = 45 as u8; n = n + 1 }
107 var i: i64 = 0
108 while i < n / 2 {
109 let t: u8 = b[i]; b[i] = b[n-1-i]; b[n-1-i] = t
110 i = i + 1
111 }
112 sys_write(1, b, n)
113 sys_munmap(b, 32)
114 return 0
115}
116func ft_nl() -> i64 { let b: *u8 = sys_mmap(8); b[0] = 10 as u8; sys_write(1, b, 1); sys_munmap(b, 8); return 0 }
117
118func ft_streq(a: *u8, b: *u8) -> i64 {
119 var i: i64 = 0
120 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
121 if b[i] != (0 as u8) { return 0 }
122 return 1
123}
124
125// basename of a path that may contain backslashes or slashes; copies into dst, returns length.
126func ft_basename(src: *u8, srclen: i64, dst: *u8) -> i64 {
127 var start: i64 = 0
128 var i: i64 = 0
129 while i < srclen {
130 if src[i] == (92 as u8) { start = i + 1 }
131 if src[i] == (47 as u8) { start = i + 1 }
132 i = i + 1
133 }
134 var n: i64 = 0
135 var j: i64 = start
136 while j < srclen {
137 if n < FT_NAMECAP - 1 { dst[n] = src[j]; n = n + 1 }
138 j = j + 1
139 }
140 dst[n] = 0 as u8
141 return n
142}
143
144func ft_pathjoin(dir: *u8, base: *u8, dst: *u8) -> i64 {
145 var n: i64 = 0
146 var i: i64 = 0
147 while dir[i] != (0 as u8) { dst[n] = dir[i]; n = n + 1; i = i + 1 }
148 if n > 0 { if dst[n-1] != (47 as u8) { dst[n] = 47 as u8; n = n + 1 } }
149 var j: i64 = 0
150 while base[j] != (0 as u8) { dst[n] = base[j]; n = n + 1; j = j + 1 }
151 dst[n] = 0 as u8
152 return n
153}
154
155// ---------------------------------------------------------------------------------------------
156// PNG-8 decode: inflate (inf_zlib, the estate's RFC1950 lib) + per-scanline defilter + to RGB.
157// Depth 8, ctype 0/2/3/6 (gray/rgb/palette/rgba); depth 16 and interlace REFUSE BY NAME --
158// measured: depth-16 appears only in normal maps, interlace appears nowhere in the corpus.
159// ---------------------------------------------------------------------------------------------
160func ft_png_load(path: *u8, prgb: *i64, pw: *i64, ph: *i64) -> i64 {
161 let lp: *i64 = sys_mmap(16) as *i64
162 let f: *u8 = sys_read_file(path, lp)
163 let flen: i64 = lp[0]
164 if (f as i64) == 0 { return 0 - 1 }
165 if flen < 45 { return 0 - 2 }
166 if (f[0] as i64) != 137 { return 0 - 2 }
167 let w: i64 = ((f[16] as i64) << 24) | ((f[17] as i64) << 16) | ((f[18] as i64) << 8) | (f[19] as i64)
168 let h: i64 = ((f[20] as i64) << 24) | ((f[21] as i64) << 16) | ((f[22] as i64) << 8) | (f[23] as i64)
169 let depth: i64 = f[24] as i64
170 let ctype: i64 = f[25] as i64
171 let interlace: i64 = f[28] as i64
172 if depth != 8 { ft_err("PNG-REFUSE depth!=8\n" as *u8); return 0 - 3 }
173 if interlace != 0 { ft_err("PNG-REFUSE interlaced\n" as *u8); return 0 - 4 }
174 var bpp: i64 = 0
175 if ctype == 0 { bpp = 1 }
176 if ctype == 2 { bpp = 3 }
177 if ctype == 3 { bpp = 1 }
178 if ctype == 4 { bpp = 2 }
179 if ctype == 6 { bpp = 4 }
180 if bpp == 0 { ft_err("PNG-REFUSE unsupported color type\n" as *u8); return 0 - 5 }
181 let idat: *u8 = sys_mmap(flen + 64)
182 var idn: i64 = 0
183 var plte: i64 = 0 - 1
184 var pltelen: i64 = 0
185 var p: i64 = 8
186 var stop: i64 = 0
187 while p + 12 <= flen {
188 if stop == 1 { p = flen } else {
189 let clen: i64 = ((f[p] as i64) << 24) | ((f[p+1] as i64) << 16) | ((f[p+2] as i64) << 8) | (f[p+3] as i64)
190 let c0: i64 = f[p+4] as i64
191 let c1: i64 = f[p+5] as i64
192 let c2: i64 = f[p+6] as i64
193 let c3: i64 = f[p+7] as i64
194 if c0 == 73 { if c1 == 68 { if c2 == 65 { if c3 == 84 {
195 var k: i64 = 0
196 while k < clen { idat[idn + k] = f[p + 8 + k]; k = k + 1 }
197 idn = idn + clen
198 } } } }
199 if c0 == 80 { if c1 == 76 { if c2 == 84 { if c3 == 69 { plte = p + 8; pltelen = clen } } } }
200 if c0 == 73 { if c1 == 69 { if c2 == 78 { if c3 == 68 { stop = 1 } } } }
201 p = p + 12 + clen
202 }
203 }
204 if idn == 0 { ft_err("PNG-REFUSE no IDAT\n" as *u8); return 0 - 6 }
205 if ctype == 3 { if plte < 0 { ft_err("PNG-REFUSE palette without PLTE\n" as *u8); return 0 - 7 } }
206 let stride: i64 = w * bpp
207 let rawcap: i64 = h * (stride + 1)
208 let raw: *u8 = sys_mmap(rawcap + 64)
209 let got: i64 = inf_zlib(idat, idn, raw, rawcap)
210 sys_munmap(idat, flen + 64)
211 if got != rawcap {
212 ft_err("PNG-REFUSE inflate size mismatch got=" as *u8); ft_outn(got)
213 ft_err(" want=" as *u8); ft_outn(rawcap); ft_nl()
214 return 0 - 8
215 }
216 let pix: *u8 = sys_mmap(h * stride + 64)
217 var r: i64 = 0
218 while r < h {
219 let flt: i64 = raw[r * (stride + 1)] as i64
220 let ro: i64 = r * (stride + 1) + 1
221 let po: i64 = r * stride
222 var x: i64 = 0
223 while x < stride {
224 var a: i64 = 0
225 var bup: i64 = 0
226 var cul: i64 = 0
227 if x >= bpp { a = pix[po + x - bpp] as i64 }
228 if r > 0 { bup = pix[po - stride + x] as i64 }
229 if x >= bpp { if r > 0 { cul = pix[po - stride + x - bpp] as i64 } }
230 var v: i64 = raw[ro + x] as i64
231 if flt == 1 { v = v + a }
232 if flt == 2 { v = v + bup }
233 if flt == 3 { v = v + (a + bup) / 2 }
234 if flt == 4 {
235 let pp: i64 = a + bup - cul
236 var pa: i64 = pp - a
237 if pa < 0 { pa = 0 - pa }
238 var pb: i64 = pp - bup
239 if pb < 0 { pb = 0 - pb }
240 var pc: i64 = pp - cul
241 if pc < 0 { pc = 0 - pc }
242 var pr: i64 = cul
243 if pa <= pb { if pa <= pc { pr = a } else { pr = cul } } else { if pb <= pc { pr = bup } else { pr = cul } }
244 v = v + pr
245 }
246 pix[po + x] = (v & 255) as u8
247 x = x + 1
248 }
249 r = r + 1
250 }
251 sys_munmap(raw, rawcap + 64)
252 let rgb: *u8 = sys_mmap(w * h * 3 + 64)
253 var q: i64 = 0
254 while q < w * h {
255 if ctype == 2 { rgb[q*3] = pix[q*3]; rgb[q*3+1] = pix[q*3+1]; rgb[q*3+2] = pix[q*3+2] }
256 if ctype == 6 { rgb[q*3] = pix[q*4]; rgb[q*3+1] = pix[q*4+1]; rgb[q*3+2] = pix[q*4+2] }
257 if ctype == 0 { rgb[q*3] = pix[q]; rgb[q*3+1] = pix[q]; rgb[q*3+2] = pix[q] }
258 if ctype == 4 { rgb[q*3] = pix[q*2]; rgb[q*3+1] = pix[q*2]; rgb[q*3+2] = pix[q*2] }
259 if ctype == 3 {
260 let pi: i64 = pix[q] as i64
261 if pi * 3 + 2 < pltelen {
262 rgb[q*3] = f[plte + pi*3]; rgb[q*3+1] = f[plte + pi*3 + 1]; rgb[q*3+2] = f[plte + pi*3 + 2]
263 }
264 }
265 q = q + 1
266 }
267 sys_munmap(pix, h * stride + 64)
268 prgb[0] = rgb as i64
269 pw[0] = w
270 ph[0] = h
271 return 0
272}
273
274func ft_img_load(path: *u8, prgb: *i64, pw: *i64, ph: *i64) -> i64 {
275 let lp: *i64 = sys_mmap(16) as *i64
276 let f: *u8 = sys_read_file(path, lp)
277 let flen: i64 = lp[0]
278 if (f as i64) == 0 { return 0 - 1 }
279 if flen < 8 { return 0 - 2 }
280 if (f[0] as i64) == 137 { return ft_png_load(path, prgb, pw, ph) }
281 if (f[0] as i64) == 255 { if (f[1] as i64) == 216 {
282 let dr: i64 = nx_jpeg_decode_rgb(f, flen, prgb, pw, ph)
283 if dr != NX_JPEG_ASCII_OK { ft_err("JPEG-REFUSE decoder rc=" as *u8); ft_outn(dr); ft_nl(); return 0 - 9 }
284 return 0
285 } }
286 ft_err("IMG-REFUSE neither PNG nor JPEG magic\n" as *u8)
287 return 0 - 10
288}
289
290// ---------------------------------------------------------------------------------------------
291// FORK-ISOLATED cell load. MEASURED 2026-08-23 with a minimal probe: nx_jpeg_decode_rgb leaks
292// ~3.2 GB of RSS PER 4096x4096 DECODE (never freed across calls; rss 1.1 MB -> 12.8 GB in four
293// rounds) and trips a real 112-byte ARENA-OVERRUN inside the lib on every call. Twelve decodes
294// in one process took the whole WSL VM down. Until the lib is fixed (defect named in the ship
295// report), each decode runs in a FORKED CHILD -- the estate's own fork-per-request idiom -- so
296// process exit frees the arena and the overrun's blast radius dies with the child. The child
297// ships back exactly one downsampled cell (cell*cell*3 bytes) through a scratch file.
298// ---------------------------------------------------------------------------------------------
299func ft_cell_load(path: *u8, dst: *u8, res: i64, cx0: i64, cy0: i64, cell: i64, tmpp: *u8) -> i64 {
300 let pid: i64 = sys_fork()
301 if pid < 0 { ft_err("FBXTEXM-REFUSE fork failed\n" as *u8); return 0 - 20 }
302 if pid == 0 {
303 // child: decode, downsample, write the cell, exit. Never returns to the caller's world.
304 let pr: *i64 = sys_mmap(8) as *i64
305 let pw: *i64 = sys_mmap(8) as *i64
306 let ph: *i64 = sys_mmap(8) as *i64
307 let rc: i64 = ft_img_load(path, pr, pw, ph)
308 if rc != 0 { sys_exit(10 + (0 - rc)) }
309 let cbuf: *u8 = sys_mmap(cell * cell * 3 + 64)
310 // downsample into a 0-origin cell then ship it
311 ft_downsample_into(pr[0] as *u8, pw[0], ph[0], cbuf, cell, 0, 0, cell)
312 let fd: i64 = sys_openat_wr(tmpp, FT_MODE)
313 if fd < 0 { sys_exit(3) }
314 let wrote: i64 = sys_write(fd, cbuf, cell * cell * 3)
315 sys_close(fd)
316 if wrote != cell * cell * 3 { sys_exit(4) }
317 sys_exit(0)
318 return 0
319 }
320 let st: *i64 = sys_mmap(16) as *i64
321 sys_wait4(pid, st, 0)
322 let raw: i64 = st[0]
323 // wait4 status: exit code in bits 8..15 for a normal exit; low byte nonzero = killed by signal
324 let sig: i64 = raw & 127
325 let code: i64 = (raw >> 8) & 255
326 if sig != 0 {
327 ft_err("cell child KILLED-BY-SIGNAL " as *u8); ft_outn(sig); ft_nl()
328 return 0 - 21
329 }
330 if code != 0 { return 0 - code }
331 let lp: *i64 = sys_mmap(16) as *i64
332 let cb: *u8 = sys_read_file(tmpp, lp)
333 if (cb as i64) == 0 { ft_err("cell scratch unreadable\n" as *u8); return 0 - 22 }
334 if lp[0] != cell * cell * 3 { ft_err("cell scratch wrong size\n" as *u8); return 0 - 23 }
335 var yy: i64 = 0
336 while yy < cell {
337 var xx: i64 = 0
338 while xx < cell {
339 let so: i64 = (yy * cell + xx) * 3
340 let dow: i64 = ((cy0 + yy) * res + cx0 + xx) * 3
341 dst[dow] = cb[so]
342 dst[dow+1] = cb[so+1]
343 dst[dow+2] = cb[so+2]
344 xx = xx + 1
345 }
346 yy = yy + 1
347 }
348 sys_munmap(cb, lp[0])
349 return 0
350}
351
352// box-downsample src (w*h rgb) into the atlas cell at (cx0, cy0) of size cell*cell.
353// source region per dest texel derives from the ratio; no picked kernel.
354func ft_downsample_into(src: *u8, w: i64, h: i64, atlas: *u8, res: i64, cx0: i64, cy0: i64, cell: i64) -> i64 {
355 var dy: i64 = 0
356 while dy < cell {
357 let sy0: i64 = dy * h / cell
358 var sy1: i64 = (dy + 1) * h / cell
359 if sy1 <= sy0 { sy1 = sy0 + 1 }
360 var dx: i64 = 0
361 while dx < cell {
362 let sx0: i64 = dx * w / cell
363 var sx1: i64 = (dx + 1) * w / cell
364 if sx1 <= sx0 { sx1 = sx0 + 1 }
365 var sr: i64 = 0
366 var sg: i64 = 0
367 var sb: i64 = 0
368 var cnt: i64 = 0
369 var sy: i64 = sy0
370 while sy < sy1 {
371 var sx: i64 = sx0
372 while sx < sx1 {
373 let o: i64 = (sy * w + sx) * 3
374 sr = sr + (src[o] as i64)
375 sg = sg + (src[o+1] as i64)
376 sb = sb + (src[o+2] as i64)
377 cnt = cnt + 1
378 sx = sx + 1
379 }
380 sy = sy + 1
381 }
382 let ao: i64 = ((cy0 + dy) * res + cx0 + dx) * 3
383 atlas[ao] = (sr / cnt) as u8
384 atlas[ao+1] = (sg / cnt) as u8
385 atlas[ao+2] = (sb / cnt) as u8
386 dx = dx + 1
387 }
388 dy = dy + 1
389 }
390 return 0
391}
392
393// SHIP RESOLUTION: the incumbent's own derivation, transplanted VERBATIM from nx_nxa_texm
394// (txm_conf_at + txm_ship_res, 2026-08-22) -- the largest measured rung whose bytes*cast_size
395// fits the declared budget, REFUSING (never defaulting) when the budget is unsourced. One owner
396// per number; the reader must be the owner's reader. HOIST DEBT: this belongs in a shared lib
397// imported by both organs -- transplant noted in the ship report so the next toucher hoists it.
398func ft_conf_at(path: *u8, key: *u8) -> i64 {
399 let lp: *i64 = sys_mmap(16) as *i64
400 let b: *u8 = sys_read_file(path, lp)
401 if (b as i64) == 0 { return 0 - 1 }
402 let n: i64 = lp[0]
403 let klen: i64 = ft_slen(key)
404 var i: i64 = 0
405 var at_line_start: i64 = 1
406 var found: i64 = 0
407 var result: i64 = 0 - 1
408 while i < n {
409 if found == 0 { if at_line_start == 1 {
410 var matched: i64 = 1
411 var k: i64 = 0
412 while k < klen {
413 if i + k >= n { matched = 0 }
414 if matched == 1 { if b[i + k] != key[k] { matched = 0 } }
415 k = k + 1
416 }
417 if matched == 1 {
418 var o: i64 = i + klen
419 var sep: i64 = 0
420 if o < n { if b[o] == (32 as u8) { sep = 1 } }
421 if o < n { if b[o] == (9 as u8) { sep = 1 } }
422 if sep == 1 {
423 var ws: i64 = 1
424 while ws == 1 {
425 if o >= n { ws = 0 }
426 if ws == 1 {
427 if b[o] == (32 as u8) { o = o + 1 } else {
428 if b[o] == (9 as u8) { o = o + 1 } else { ws = 0 }
429 }
430 }
431 }
432 var v: i64 = 0
433 var digits: i64 = 0
434 var scanning: i64 = 1
435 while scanning == 1 {
436 if o >= n { scanning = 0 }
437 if scanning == 1 {
438 let c: i64 = (b[o] & 255) as i64
439 if c < 48 { scanning = 0 }
440 if c > 57 { scanning = 0 }
441 if scanning == 1 { v = v*10 + (c - 48); digits = digits + 1; o = o + 1 }
442 }
443 }
444 if digits > 0 { result = v; found = 1 }
445 }
446 }
447 } }
448 if b[i] == (10 as u8) { at_line_start = 1 } else { at_line_start = 0 }
449 i = i + 1
450 }
451 return result
452}
453
454func ft_ship_res() -> i64 {
455 let budget: i64 = ft_conf_at(FT_SHIP_CONF, "texture_budget_bytes" as *u8)
456 if budget < 0 { return 0 - 1 }
457 let cast: i64 = ft_conf_at(FT_SHIP_CONF, "cast_size" as *u8)
458 if cast < 0 { return 0 - 1 }
459 let b4: i64 = ft_conf_at(FT_SHIP_CONF, "res_bytes_4096" as *u8)
460 if b4 >= 0 { if b4*cast <= budget { return 4096 } }
461 let b3: i64 = ft_conf_at(FT_SHIP_CONF, "res_bytes_2048" as *u8)
462 if b3 >= 0 { if b3*cast <= budget { return 2048 } }
463 let b2: i64 = ft_conf_at(FT_SHIP_CONF, "res_bytes_1024" as *u8)
464 if b2 >= 0 { if b2*cast <= budget { return 1024 } }
465 let b1: i64 = ft_conf_at(FT_SHIP_CONF, "res_bytes_512" as *u8)
466 if b1 >= 0 { if b1*cast <= budget { return 512 } }
467 return 0 - 1
468}
469
470// ---- FBX prop readers ----
471
472func ft_prop_i64(b: *u8, p: i64) -> i64 {
473 let t: i64 = b[p] as i64
474 if t == 76 { return fbxc_i64le(b, p + 1) }
475 if t == 73 { return fbxc_i32(b, p + 1) }
476 return 0 - 1
477}
478
479// first prop as string: out[0]=ptr to a NUL-copied buffer; returns len, 0 if not a string.
480func ft_prop_str(b: *u8, p: i64, out: *i64) -> i64 {
481 let t: i64 = b[p] as i64
482 if t != 83 { return 0 }
483 let n: i64 = fbxc_u32(b, p + 1)
484 let cp: *u8 = sys_mmap(n + 2)
485 var i: i64 = 0
486 while i < n { cp[i] = b[p + 5 + i]; i = i + 1 }
487 cp[n] = 0 as u8
488 out[0] = cp as i64
489 return n
490}
491
492func ft_str_is(s: *u8, n: i64, t: *u8) -> i64 {
493 var i: i64 = 0
494 while i < n {
495 if t[i] == (0 as u8) { return 0 }
496 if s[i] != t[i] { return 0 }
497 i = i + 1
498 }
499 if t[n] != (0 as u8) { return 0 }
500 return 1
501}
502
503// P row "DiffuseColor": skip 4 strings then read up to 3 D doubles as 0..255.
504func ft_prop_dcolor(b: *u8, p0: i64, rr: *i64, gg: *i64, bb: *i64, mi: i64) -> i64 {
505 var p: i64 = p0
506 var skipped: i64 = 0
507 while skipped < 4 {
508 let t: i64 = b[p] as i64
509 if t == 83 { p = p + 5 + fbxc_u32(b, p + 1); skipped = skipped + 1 } else { return 0 }
510 }
511 let tmp: *i64 = sys_mmap(32) as *i64
512 var vals: i64 = 0
513 var stop: i64 = 0
514 while vals < 3 {
515 if stop == 1 { vals = 3 } else {
516 let t2: i64 = b[p] as i64
517 if t2 == 68 {
518 let fx: i64 = fbxc_f64_fx(fbxc_i64le(b, p + 1))
519 var c255: i64 = fx * 255 / FT_FX_ONE
520 if c255 < 0 { c255 = 0 }
521 if c255 > 255 { c255 = 255 }
522 tmp[vals] = c255
523 p = p + 9
524 vals = vals + 1
525 } else { stop = 1 }
526 }
527 }
528 if vals >= 3 {
529 rr[mi] = tmp[0]
530 gg[mi] = tmp[1]
531 bb[mi] = tmp[2]
532 }
533 sys_munmap(tmp as *u8, 32)
534 return 0
535}
536
537// C row: S kind, L a, L b [, S prop]. returns 0 OO, 1 OP, -1 other; fills arrays at index n.
538func ft_conn_row(b: *u8, p0: i64, ca: *i64, cb: *i64, cp: *i64, n: i64) -> i64 {
539 var p: i64 = p0
540 if (b[p] as i64) != 83 { return 0 - 1 }
541 let kl: i64 = fbxc_u32(b, p + 1)
542 let ko: i64 = p + 5
543 var kind: i64 = 0 - 1
544 if kl == 2 {
545 if (b[ko] as i64) == 79 {
546 if (b[ko+1] as i64) == 79 { kind = 0 }
547 if (b[ko+1] as i64) == 80 { kind = 1 }
548 }
549 }
550 if kind < 0 { return 0 - 1 }
551 p = ko + kl
552 let a: i64 = ft_prop_i64(b, p)
553 if (b[p] as i64) == 76 { p = p + 9 } else { p = p + 5 }
554 let b2: i64 = ft_prop_i64(b, p)
555 if (b[p] as i64) == 76 { p = p + 9 } else { p = p + 5 }
556 ca[n] = a
557 cb[n] = b2
558 var role: i64 = 0
559 if kind == 1 {
560 if (b[p] as i64) == 83 {
561 let pl: i64 = fbxc_u32(b, p + 1)
562 let po: i64 = p + 5
563 if ft_str_is(((b as i64) + po) as *u8, pl, "DiffuseColor" as *u8) == 1 { role = 1 }
564 if ft_str_is(((b as i64) + po) as *u8, pl, "NormalMap" as *u8) == 1 { role = 2 }
565 if ft_str_is(((b as i64) + po) as *u8, pl, "ShininessExponent" as *u8) == 1 { role = 3 }
566 if ft_str_is(((b as i64) + po) as *u8, pl, "ReflectionFactor" as *u8) == 1 { role = 4 }
567 if ft_str_is(((b as i64) + po) as *u8, pl, "TransparencyFactor" as *u8) == 1 { role = 5 }
568 }
569 }
570 cp[n] = role
571 return kind
572}
573
574func nt2_rd64(b: *u8, off: i64) -> i64 {
575 var v: i64 = 0
576 var k: i64 = 7
577 while k >= 0 { v = v * 256 + (b[off + k] as i64); k = k - 1 }
578 return v
579}
580func nt2_wr64(b: *u8, off: i64, v: i64) -> i64 {
581 var x: i64 = v
582 var k: i64 = 0
583 while k < 8 { b[off + k] = (x & 255) as u8; x = x >> 8; k = k + 1 }
584 return 0
585}
586func nt2_tageq(b: *u8, off: i64, t: *u8) -> i64 {
587 if nt2_rd64(b, off) == nxa_tag4(t) { return 1 }
588 return 0
589}
590
591// rebuild the container: carry every section except old TEXC/TEXM, append ours, recompute checks.
592func ft_write_out(xb: *u8, xns: i64, uvq: *i64, uvwords: i64, res: i64, grid: i64,
593 moff: *i64, mlen: *i64, blobbytes: i64, blob: *u8, outp: *u8) -> i64 {
594 var blobwords: i64 = blobbytes / FT_WORD
595 if blobwords * FT_WORD < blobbytes { blobwords = blobwords + 1 }
596 let tmwords: i64 = FT_TM_HDRW + FT_TM_NMAP * FT_TM_RECW + blobwords
597 var carried: i64 = 0
598 var total: i64 = 0
599 var s: i64 = 0
600 while s < xns {
601 let e: i64 = FT_HDR + s * FT_TOCE
602 if nt2_tageq(xb, e, "TEXC" as *u8) == 0 { if nt2_tageq(xb, e, "TEXM" as *u8) == 0 {
603 total = total + nt2_rd64(xb, e + 16) * FT_WORD
604 carried = carried + 1
605 } }
606 s = s + 1
607 }
608 let nsec: i64 = carried + 2
609 if nsec >= FT_MAXSEC { ft_err("FBXTEXM-REFUSE section table full\n" as *u8); return FT_E_BAD }
610 let toclen: i64 = FT_HDR + nsec * FT_TOCE
611 total = toclen + total + uvwords * FT_WORD + tmwords * FT_WORD
612 let nb: *u8 = sys_mmap(total + FT_PAD)
613 if (nb as i64) == 0 { ft_err("FBXTEXM-REFUSE cannot allocate output\n" as *u8); return FT_E_IO }
614 nt2_wr64(nb, 0, nxa_magic())
615 nt2_wr64(nb, 8, 1)
616 nt2_wr64(nb, 16, nsec)
617 var wo: i64 = toclen
618 var ti: i64 = 0
619 var s2: i64 = 0
620 while s2 < xns {
621 let e2: i64 = FT_HDR + s2 * FT_TOCE
622 if nt2_tageq(xb, e2, "TEXC" as *u8) == 0 { if nt2_tageq(xb, e2, "TEXM" as *u8) == 0 {
623 let oldoff: i64 = nt2_rd64(xb, e2 + 8)
624 let wl: i64 = nt2_rd64(xb, e2 + 16)
625 let te: i64 = FT_HDR + ti * FT_TOCE
626 nt2_wr64(nb, te, nt2_rd64(xb, e2))
627 nt2_wr64(nb, te + 8, wo)
628 nt2_wr64(nb, te + 16, wl)
629 var k: i64 = 0
630 while k < wl * FT_WORD { nb[wo + k] = xb[oldoff + k]; k = k + 1 }
631 let pw: *i64 = ((nb as i64) + wo) as *i64
632 nt2_wr64(nb, te + 24, nxa_check2(1, pw, wl))
633 wo = wo + wl * FT_WORD
634 ti = ti + 1
635 } }
636 s2 = s2 + 1
637 }
638 let teC: i64 = FT_HDR + ti * FT_TOCE
639 nt2_wr64(nb, teC, nxa_tag4("TEXC" as *u8))
640 nt2_wr64(nb, teC + 8, wo)
641 nt2_wr64(nb, teC + 16, uvwords)
642 var k2: i64 = 0
643 while k2 < uvwords {
644 nt2_wr64(nb, wo + k2 * FT_WORD, uvq[k2])
645 k2 = k2 + 1
646 }
647 let pwC: *i64 = ((nb as i64) + wo) as *i64
648 nt2_wr64(nb, teC + 24, nxa_check2(1, pwC, uvwords))
649 wo = wo + uvwords * FT_WORD
650 ti = ti + 1
651 let teM: i64 = FT_HDR + ti * FT_TOCE
652 nt2_wr64(nb, teM, nxa_tag4("TEXM" as *u8))
653 nt2_wr64(nb, teM + 8, wo)
654 nt2_wr64(nb, teM + 16, tmwords)
655 nt2_wr64(nb, wo, FT_TM_NMAP)
656 nt2_wr64(nb, wo + 8, res)
657 nt2_wr64(nb, wo + 16, FT_TM_MODEL_METALROUGH)
658 nt2_wr64(nb, wo + 24, 0)
659 nt2_wr64(nb, wo + 32, 0)
660 nt2_wr64(nb, wo + 40, blobwords)
661 var m: i64 = 0
662 while m < FT_TM_NMAP {
663 let ro: i64 = wo + (FT_TM_HDRW + m * FT_TM_RECW) * FT_WORD
664 nt2_wr64(nb, ro, m)
665 nt2_wr64(nb, ro + 8, moff[m])
666 nt2_wr64(nb, ro + 16, mlen[m])
667 m = m + 1
668 }
669 let blobbase: i64 = wo + (FT_TM_HDRW + FT_TM_NMAP * FT_TM_RECW) * FT_WORD
670 var z: i64 = 0
671 while z < blobwords * FT_WORD { nb[blobbase + z] = 0 as u8; z = z + 1 }
672 var c: i64 = 0
673 while c < blobbytes { nb[blobbase + c] = blob[c]; c = c + 1 }
674 let pwM: *i64 = ((nb as i64) + wo) as *i64
675 nt2_wr64(nb, teM + 24, nxa_check2(1, pwM, tmwords))
676 wo = wo + tmwords * FT_WORD
677 let tb: *i64 = ((nb as i64) + FT_HDR) as *i64
678 nt2_wr64(nb, 24, nxa_check2(1, tb, nsec * 4))
679 let fd: i64 = sys_openat_wr(outp, FT_MODE)
680 if fd < 0 { ft_err("FBXTEXM-REFUSE cannot open output\n" as *u8); return FT_E_IO }
681 let wrote: i64 = sys_write(fd, nb, wo)
682 sys_close(fd)
683 if wrote != wo { ft_err("FBXTEXM-REFUSE short write\n" as *u8); return FT_E_IO }
684 return FT_OK
685}
686
687func main(argc: i64, argv: *i64) -> i64 {
688 if argc < 5 {
689 ft_err("usage: nx_fbx_texm <in.fbx> <texdir> <in_rigged.nxa> <out.nxa>\n" as *u8)
690 return FT_E_USAGE
691 }
692 let fbxp: *u8 = argv[1] as *u8
693 let texdir: *u8 = argv[2] as *u8
694 let inxa: *u8 = argv[3] as *u8
695 let outp: *u8 = argv[4] as *u8
696
697 let lp: *i64 = sys_mmap(16) as *i64
698 let fb: *u8 = sys_read_file(fbxp, lp)
699 let fn: i64 = lp[0]
700 if (fb as i64) == 0 { ft_err("FBXTEXM-REFUSE cannot read fbx\n" as *u8); return FT_E_IO }
701 let ver: i64 = fbxc_version(fb, fn)
702 if ver < 0 { ft_err("FBXTEXM-REFUSE not a binary FBX\n" as *u8); return FT_E_BAD }
703 let big: i64 = fbxc_big(ver)
704
705 let geo_id: *i64 = sys_mmap(FT_MAXGEOM * 8) as *i64
706 let geo_nv: *i64 = sys_mmap(FT_MAXGEOM * 8) as *i64
707 let geo_vbase: *i64 = sys_mmap(FT_MAXGEOM * 8) as *i64
708 let geo_uvp: *i64 = sys_mmap(FT_MAXGEOM * 8) as *i64
709 let geo_uvn: *i64 = sys_mmap(FT_MAXGEOM * 8) as *i64
710 let geo_uvip: *i64 = sys_mmap(FT_MAXGEOM * 8) as *i64
711 let geo_uvin: *i64 = sys_mmap(FT_MAXGEOM * 8) as *i64
712 let geo_pvip: *i64 = sys_mmap(FT_MAXGEOM * 8) as *i64
713 let geo_pvin: *i64 = sys_mmap(FT_MAXGEOM * 8) as *i64
714 let geo_matp: *i64 = sys_mmap(FT_MAXGEOM * 8) as *i64
715 let geo_matn: *i64 = sys_mmap(FT_MAXGEOM * 8) as *i64
716 var ngeo: i64 = 0
717
718 let mat_id: *i64 = sys_mmap(FT_MAXMAT * 8) as *i64
719 let mat_dr: *i64 = sys_mmap(FT_MAXMAT * 8) as *i64
720 let mat_dg: *i64 = sys_mmap(FT_MAXMAT * 8) as *i64
721 let mat_db: *i64 = sys_mmap(FT_MAXMAT * 8) as *i64
722 let mat_hasdc: *i64 = sys_mmap(FT_MAXMAT * 8) as *i64
723 var nmat: i64 = 0
724
725 let tex_id: *i64 = sys_mmap(FT_MAXTEX * 8) as *i64
726 let tex_fn: *i64 = sys_mmap(FT_MAXTEX * 8) as *i64
727 var ntex: i64 = 0
728
729 let cn_kind: *i64 = sys_mmap(FT_MAXCONN * 8) as *i64
730 let cn_a: *i64 = sys_mmap(FT_MAXCONN * 8) as *i64
731 let cn_b: *i64 = sys_mmap(FT_MAXCONN * 8) as *i64
732 let cn_prop: *i64 = sys_mmap(FT_MAXCONN * 8) as *i64
733 var nconn: i64 = 0
734
735 let model_id: *i64 = sys_mmap(FT_MAXMODEL * 8) as *i64
736 var nmodel: i64 = 0
737
738 // ---- iterative walk with EXPLICIT context per stack frame. Table appends happen at SCAN
739 // time in document order (the fbx2nxa vbase law); child ranges carry which geometry /
740 // material / texture they belong to, so attachment cannot cross-bind (the defect an
741 // implicit "current" pointer has under LIFO ordering).
742 // frame: {pos, end, ctxG, ctxM, ctxT, sub} sub: 0 none, 1 inside LayerElementUV,
743 // 2 inside LayerElementMaterial, 3 inside Properties70-of-material
744 var stkcap: i64 = fn / 13 + FT_STACK_MIN
745 let stk: *i64 = sys_mmap(stkcap * 6 * 8) as *i64
746 let hdr: *i64 = sys_mmap(FBXC_OUTN * 8) as *i64
747 let outw: *i64 = sys_mmap(64) as *i64
748 var sp: i64 = 1
749 stk[0] = 27
750 stk[1] = fn
751 stk[2] = 0 - 1
752 stk[3] = 0 - 1
753 stk[4] = 0 - 1
754 stk[5] = 0
755 var overflow: i64 = 0
756 var spmax: i64 = 0
757 while sp > 0 {
758 sp = sp - 1
759 var pos: i64 = stk[sp*6]
760 let end: i64 = stk[sp*6 + 1]
761 let cG: i64 = stk[sp*6 + 2]
762 let cM: i64 = stk[sp*6 + 3]
763 let cT: i64 = stk[sp*6 + 4]
764 let sub: i64 = stk[sp*6 + 5]
765 while pos < end {
766 if fbxc_hdr(fb, fn, pos, big, hdr) != 0 { pos = end } else {
767 let e: i64 = hdr[FBXC_END]
768 if e <= pos { pos = end } else {
769 let nlen: i64 = hdr[FBXC_NAMELEN]
770 let nameo: i64 = hdr[FBXC_NAMEOFF]
771 let props: i64 = hdr[FBXC_PROPSOFF]
772 let child0: i64 = props + hdr[FBXC_PLEN]
773 var pushG: i64 = cG
774 var pushM: i64 = cM
775 var pushT: i64 = cT
776 var pushSub: i64 = sub
777 var wantpush: i64 = 0
778 if fbxc_name_is(fb, nameo, nlen, "Geometry" as *u8) == 1 {
779 if ngeo >= FT_MAXGEOM { overflow = 1 } else {
780 geo_id[ngeo] = ft_prop_i64(fb, props)
781 pushG = ngeo
782 pushSub = 0
783 wantpush = 1
784 ngeo = ngeo + 1
785 }
786 }
787 if fbxc_name_is(fb, nameo, nlen, "Model" as *u8) == 1 {
788 if nmodel < FT_MAXMODEL { model_id[nmodel] = ft_prop_i64(fb, props); nmodel = nmodel + 1 }
789 }
790 if fbxc_name_is(fb, nameo, nlen, "Material" as *u8) == 1 {
791 if nmat >= FT_MAXMAT { overflow = 1 } else {
792 mat_id[nmat] = ft_prop_i64(fb, props)
793 mat_hasdc[nmat] = 0
794 pushM = nmat
795 pushSub = 0
796 wantpush = 1
797 nmat = nmat + 1
798 }
799 }
800 if fbxc_name_is(fb, nameo, nlen, "Texture" as *u8) == 1 {
801 if ntex >= FT_MAXTEX { overflow = 1 } else {
802 tex_id[ntex] = ft_prop_i64(fb, props)
803 tex_fn[ntex] = 0
804 pushT = ntex
805 pushSub = 0
806 wantpush = 1
807 ntex = ntex + 1
808 }
809 }
810 if fbxc_name_is(fb, nameo, nlen, "RelativeFilename" as *u8) == 1 {
811 if cT >= 0 {
812 let sl: i64 = ft_prop_str(fb, props, outw)
813 if sl > 0 {
814 if tex_fn[cT] == 0 {
815 let bn: *u8 = sys_mmap(FT_NAMECAP)
816 ft_basename(outw[0] as *u8, sl, bn)
817 tex_fn[cT] = bn as i64
818 }
819 }
820 }
821 }
822 if fbxc_name_is(fb, nameo, nlen, "Vertices" as *u8) == 1 {
823 if cG >= 0 {
824 if fbxc_array(fb, props + 1, 8, outw) == 0 { geo_nv[cG] = outw[2] / 3 }
825 }
826 }
827 if fbxc_name_is(fb, nameo, nlen, "PolygonVertexIndex" as *u8) == 1 {
828 if cG >= 0 {
829 if fbxc_array(fb, props + 1, 4, outw) == 0 {
830 geo_pvip[cG] = outw[0]
831 geo_pvin[cG] = outw[2]
832 }
833 }
834 }
835 if fbxc_name_is(fb, nameo, nlen, "LayerElementUV" as *u8) == 1 {
836 if cG >= 0 { pushSub = 1; wantpush = 1 }
837 }
838 if fbxc_name_is(fb, nameo, nlen, "LayerElementMaterial" as *u8) == 1 {
839 if cG >= 0 { pushSub = 2; wantpush = 1 }
840 }
841 if fbxc_name_is(fb, nameo, nlen, "UV" as *u8) == 1 {
842 if cG >= 0 { if sub == 1 {
843 if geo_uvp[cG] == 0 {
844 if fbxc_array(fb, props + 1, 8, outw) == 0 {
845 geo_uvp[cG] = outw[0]
846 geo_uvn[cG] = outw[2] / 2
847 }
848 }
849 } }
850 }
851 if fbxc_name_is(fb, nameo, nlen, "UVIndex" as *u8) == 1 {
852 if cG >= 0 { if sub == 1 {
853 if geo_uvip[cG] == 0 {
854 if fbxc_array(fb, props + 1, 4, outw) == 0 {
855 geo_uvip[cG] = outw[0]
856 geo_uvin[cG] = outw[2]
857 }
858 }
859 } }
860 }
861 if fbxc_name_is(fb, nameo, nlen, "Materials" as *u8) == 1 {
862 if cG >= 0 { if sub == 2 {
863 if geo_matp[cG] == 0 {
864 if fbxc_array(fb, props + 1, 4, outw) == 0 {
865 geo_matp[cG] = outw[0]
866 geo_matn[cG] = outw[2]
867 }
868 }
869 } }
870 }
871 if fbxc_name_is(fb, nameo, nlen, "P" as *u8) == 1 {
872 if cM >= 0 {
873 let sl2: i64 = ft_prop_str(fb, props, outw)
874 if sl2 > 0 {
875 if ft_str_is(outw[0] as *u8, sl2, "DiffuseColor" as *u8) == 1 {
876 ft_prop_dcolor(fb, props, mat_dr, mat_dg, mat_db, cM)
877 mat_hasdc[cM] = 1
878 }
879 }
880 }
881 }
882 if fbxc_name_is(fb, nameo, nlen, "C" as *u8) == 1 {
883 if nconn >= FT_MAXCONN { overflow = 1 } else {
884 let kk: i64 = ft_conn_row(fb, props, cn_a, cn_b, cn_prop, nconn)
885 if kk >= 0 { cn_kind[nconn] = kk; nconn = nconn + 1 }
886 }
887 }
888 if wantpush == 0 { wantpush = 1 } // descend everywhere; context rides the frame
889 // A leaf's trailing NUL record is 13 bytes (25 for v7500+); a child range that
890 // holds ONLY the terminator has no nodes. Pushing those flooded the stack on
891 // the knight (12,988 connection leaves) -- measured, then named, then fixed.
892 var nullen: i64 = 13
893 if big == 1 { nullen = 25 }
894 if e - child0 > nullen {
895 if sp >= stkcap - 1 { overflow = 1 } else {
896 if sp > spmax { spmax = sp }
897 stk[sp*6] = child0
898 stk[sp*6 + 1] = e
899 stk[sp*6 + 2] = pushG
900 stk[sp*6 + 3] = pushM
901 stk[sp*6 + 4] = pushT
902 stk[sp*6 + 5] = pushSub
903 sp = sp + 1
904 }
905 }
906 pos = e
907 }
908 }
909 }
910 }
911 if overflow == 1 {
912 ft_err("FBXTEXM-REFUSE a table cap was reached -- the counts at refusal name the offender:\n" as *u8)
913 ft_err(" geoms=" as *u8); ft_outn(ngeo); ft_err(" of " as *u8); ft_outn(FT_MAXGEOM)
914 ft_err(" mats=" as *u8); ft_outn(nmat); ft_err(" of " as *u8); ft_outn(FT_MAXMAT)
915 ft_err(" texs=" as *u8); ft_outn(ntex); ft_err(" of " as *u8); ft_outn(FT_MAXTEX)
916 ft_err(" models=" as *u8); ft_outn(nmodel); ft_err(" of " as *u8); ft_outn(FT_MAXMODEL)
917 ft_err(" conns=" as *u8); ft_outn(nconn); ft_err(" of " as *u8); ft_outn(FT_MAXCONN)
918 ft_err(" stack_peak=" as *u8); ft_outn(spmax); ft_err(" of " as *u8); ft_outn(stkcap)
919 ft_err("\n" as *u8)
920 return FT_E_BAD
921 }
922 ft_out("fbx walked: geoms=" as *u8); ft_outn(ngeo)
923 ft_out(" mats=" as *u8); ft_outn(nmat)
924 ft_out(" texs=" as *u8); ft_outn(ntex)
925 ft_out(" models=" as *u8); ft_outn(nmodel)
926 ft_out(" conns=" as *u8); ft_outn(nconn)
927 ft_nl()
928 if ngeo == 0 { ft_err("FBXTEXM-REFUSE no geometry\n" as *u8); return FT_E_BAD }
929
930 var vtotal: i64 = 0
931 var gi: i64 = 0
932 while gi < ngeo {
933 geo_vbase[gi] = vtotal
934 vtotal = vtotal + geo_nv[gi]
935 gi = gi + 1
936 }
937
938 let lp2: *i64 = sys_mmap(16) as *i64
939 let xb: *u8 = sys_read_file(inxa, lp2)
940 if (xb as i64) == 0 { ft_err("FBXTEXM-REFUSE cannot read rigged nxa\n" as *u8); return FT_E_IO }
941 if nt2_rd64(xb, 0) != nxa_magic() { ft_err("FBXTEXM-REFUSE rigged input is not an NXA\n" as *u8); return FT_E_BAD }
942 let xns: i64 = nt2_rd64(xb, 16)
943 var vo: i64 = 0 - 1
944 var s: i64 = 0
945 while s < xns {
946 let e5: i64 = FT_HDR + s*FT_TOCE
947 if nt2_tageq(xb, e5, "VERT" as *u8) == 1 { vo = nt2_rd64(xb, e5 + 8) }
948 s = s + 1
949 }
950 if vo < 0 { ft_err("FBXTEXM-REFUSE rigged nxa has no VERT\n" as *u8); return FT_E_NOSEC }
951 let xnv: i64 = nt2_rd64(xb, vo)
952 if xnv != vtotal {
953 ft_err("FBXTEXM-REFUSE DESYNC fbx-control-points=" as *u8); ft_outn(vtotal)
954 ft_err(" rigged-VERT=" as *u8); ft_outn(xnv); ft_nl()
955 return FT_E_DESYNC
956 }
957
958 // ---- geometry -> model (single pass over connections) ----
959 let geo_model: *i64 = sys_mmap(FT_MAXGEOM * 8) as *i64
960 var z0: i64 = 0
961 while z0 < ngeo { geo_model[z0] = 0 - 1; z0 = z0 + 1 }
962 var c0: i64 = 0
963 while c0 < nconn {
964 if cn_kind[c0] == 0 {
965 var gI: i64 = 0
966 while gI < ngeo {
967 if geo_id[gI] == cn_a[c0] {
968 var mo: i64 = 0
969 while mo < nmodel {
970 if model_id[mo] == cn_b[c0] { geo_model[gI] = cn_b[c0]; mo = nmodel } else { mo = mo + 1 }
971 }
972 gI = ngeo
973 } else { gI = gI + 1 }
974 }
975 }
976 c0 = c0 + 1
977 }
978
979 // ---- per-material role sources (single pass) ----
980 let mat_dfn: *i64 = sys_mmap(FT_MAXMAT * 8) as *i64
981 let mat_rfn: *i64 = sys_mmap(FT_MAXMAT * 8) as *i64
982 let mat_mfn: *i64 = sys_mmap(FT_MAXMAT * 8) as *i64
983 var c1: i64 = 0
984 while c1 < nconn {
985 if cn_kind[c1] == 1 {
986 var mi0: i64 = 0
987 while mi0 < nmat {
988 if mat_id[mi0] == cn_b[c1] {
989 var ti0: i64 = 0
990 while ti0 < ntex {
991 if tex_id[ti0] == cn_a[c1] {
992 if cn_prop[c1] == 1 { if mat_dfn[mi0] == 0 { mat_dfn[mi0] = tex_fn[ti0] } }
993 if cn_prop[c1] == 3 { if mat_rfn[mi0] == 0 { mat_rfn[mi0] = tex_fn[ti0] } }
994 if cn_prop[c1] == 4 { if mat_mfn[mi0] == 0 { mat_mfn[mi0] = tex_fn[ti0] } }
995 ti0 = ntex
996 } else { ti0 = ti0 + 1 }
997 }
998 mi0 = nmat
999 } else { mi0 = mi0 + 1 }
1000 }
1001 }
1002 c1 = c1 + 1
1003 }
1004
1005 // ---- charts: one per distinct diffuse basename; scalar materials chart individually ----
1006 let ch_file: *i64 = sys_mmap(FT_MAXCHART * 8) as *i64
1007 let ch_mat: *i64 = sys_mmap(FT_MAXCHART * 8) as *i64
1008 let ch_rough: *i64 = sys_mmap(FT_MAXCHART * 8) as *i64
1009 let ch_metal: *i64 = sys_mmap(FT_MAXCHART * 8) as *i64
1010 var nchart: i64 = 0
1011 let mat_chart: *i64 = sys_mmap(FT_MAXMAT * 8) as *i64
1012 var mi: i64 = 0
1013 while mi < nmat {
1014 var found: i64 = 0 - 1
1015 if mat_dfn[mi] != 0 {
1016 var k2c: i64 = 0
1017 while k2c < nchart {
1018 if ch_file[k2c] != 0 {
1019 if ft_streq(ch_file[k2c] as *u8, mat_dfn[mi] as *u8) == 1 { found = k2c; k2c = nchart } else { k2c = k2c + 1 }
1020 } else { k2c = k2c + 1 }
1021 }
1022 }
1023 if found < 0 {
1024 if nchart >= FT_MAXCHART { ft_err("FBXTEXM-REFUSE chart cap reached\n" as *u8); return FT_E_BAD }
1025 ch_file[nchart] = mat_dfn[mi]
1026 ch_mat[nchart] = mi
1027 ch_rough[nchart] = mat_rfn[mi]
1028 ch_metal[nchart] = mat_mfn[mi]
1029 found = nchart
1030 nchart = nchart + 1
1031 }
1032 mat_chart[mi] = found
1033 mi = mi + 1
1034 }
1035 if nchart == 0 { ft_err("FBXTEXM-REFUSE zero charts\n" as *u8); return FT_E_BAD }
1036 var g: i64 = 1
1037 while g * g < nchart { g = g + 1 }
1038 let res: i64 = ft_ship_res()
1039 if res < 0 { ft_err("FBXTEXM-REFUSE ship budget unsourced (knowledge/texture_ship_budget.conf) -- refusing, never defaulting
1040" as *u8); return FT_E_BAD }
1041 let cell: i64 = res / g
1042 ft_out("charts=" as *u8); ft_outn(nchart)
1043 ft_out(" grid=" as *u8); ft_outn(g)
1044 ft_out(" res=" as *u8); ft_outn(res)
1045 ft_out(" cell=" as *u8); ft_outn(cell)
1046 ft_nl()
1047
1048 // ---- per-geometry material slot tables (model material list in connection order) ----
1049 let slot_tab: *i64 = sys_mmap(FT_MAXGEOM * FT_MAXSLOT * 8) as *i64
1050 let slot_n: *i64 = sys_mmap(FT_MAXGEOM * 8) as *i64
1051 var gi1: i64 = 0
1052 while gi1 < ngeo {
1053 slot_n[gi1] = 0
1054 var c2: i64 = 0
1055 while c2 < nconn {
1056 if cn_kind[c2] == 0 { if cn_b[c2] == geo_model[gi1] {
1057 var mi1: i64 = 0
1058 while mi1 < nmat {
1059 if mat_id[mi1] == cn_a[c2] {
1060 if slot_n[gi1] < FT_MAXSLOT {
1061 slot_tab[gi1 * FT_MAXSLOT + slot_n[gi1]] = mi1
1062 slot_n[gi1] = slot_n[gi1] + 1
1063 }
1064 mi1 = nmat
1065 } else { mi1 = mi1 + 1 }
1066 }
1067 } }
1068 c2 = c2 + 1
1069 }
1070 gi1 = gi1 + 1
1071 }
1072
1073 // ---- per-vertex UV + chart (first-poly-wins; ByPolygon material via slot table) ----
1074 let uvq: *i64 = sys_mmap((FT_TX_PHDR + vtotal * FT_TX_STRIDE) * 8 + 64) as *i64
1075 let uvset: *u8 = sys_mmap(vtotal + 64)
1076 uvq[0] = vtotal
1077 uvq[1] = FT_TX_STRIDE
1078 uvq[2] = g
1079 uvq[3] = 0
1080 var covered: i64 = 0
1081 var gi3: i64 = 0
1082 while gi3 < ngeo {
1083 let base: i64 = geo_vbase[gi3]
1084 let pvin: i64 = geo_pvin[gi3]
1085 if pvin > 0 {
1086 let pvB: *u8 = geo_pvip[gi3] as *u8
1087 let uvB: *u8 = geo_uvip[gi3] as *u8
1088 let uvD: *u8 = geo_uvp[gi3] as *u8
1089 let lmB: *u8 = geo_matp[gi3] as *u8
1090 let lmn: i64 = geo_matn[gi3]
1091 // AllSame: the single Materials entry names the slot (NOT always 0)
1092 var gmat: i64 = 0 - 1
1093 var defslot: i64 = 0
1094 if lmn >= 1 { defslot = fbxc_i32(lmB, 0) }
1095 if defslot >= 0 { if defslot < slot_n[gi3] { gmat = slot_tab[gi3 * FT_MAXSLOT + defslot] } }
1096 if gmat < 0 { if slot_n[gi3] > 0 { gmat = slot_tab[gi3 * FT_MAXSLOT] } }
1097 var k3: i64 = 0
1098 var poly: i64 = 0
1099 while k3 < pvin {
1100 var iv: i64 = fbxc_i32(pvB, k3 * 4)
1101 var lastf: i64 = 0
1102 if iv < 0 { iv = 0 - iv - 1; lastf = 1 }
1103 let cpv: i64 = base + iv
1104 if cpv >= 0 { if cpv < vtotal { if uvset[cpv] == (0 as u8) {
1105 var u01: i64 = 0
1106 var v01: i64 = 0
1107 if geo_uvn[gi3] > 0 {
1108 var uvidx: i64 = k3
1109 if geo_uvin[gi3] > 0 { uvidx = fbxc_i32(uvB, k3 * 4) }
1110 if uvidx >= 0 { if uvidx < geo_uvn[gi3] {
1111 u01 = fbxc_f64_fx(fbxc_i64le(uvD, uvidx * 16))
1112 v01 = fbxc_f64_fx(fbxc_i64le(uvD, uvidx * 16 + 8))
1113 } }
1114 }
1115 // wrap tiling coords into [0,1) then flip v to row space
1116 u01 = ((u01 % FT_FX_ONE) + FT_FX_ONE) % FT_FX_ONE
1117 v01 = ((v01 % FT_FX_ONE) + FT_FX_ONE) % FT_FX_ONE
1118 v01 = FT_FX_ONE - v01
1119 var pm: i64 = gmat
1120 if lmn > 1 {
1121 if poly < lmn {
1122 let midx: i64 = fbxc_i32(lmB, poly * 4)
1123 if midx >= 0 { if midx < slot_n[gi3] { pm = slot_tab[gi3 * FT_MAXSLOT + midx] } }
1124 }
1125 }
1126 var chart: i64 = 0
1127 if pm >= 0 { chart = mat_chart[pm] }
1128 let tw: i64 = FT_Q16 / g
1129 let tx: i64 = chart % g
1130 let ty: i64 = chart / g
1131 let uu: i64 = tx*tw + 1 + (u01 * FT_Q16 / FT_FX_ONE) * (tw - 2) / FT_Q16
1132 let vv: i64 = ty*tw + 1 + (v01 * FT_Q16 / FT_FX_ONE) * (tw - 2) / FT_Q16
1133 uvq[FT_TX_PHDR + cpv*FT_TX_STRIDE] = uu
1134 uvq[FT_TX_PHDR + cpv*FT_TX_STRIDE + 1] = vv
1135 uvq[FT_TX_PHDR + cpv*FT_TX_STRIDE + 2] = chart
1136 uvset[cpv] = 1 as u8
1137 covered = covered + 1
1138 } } }
1139 if lastf == 1 { poly = poly + 1 }
1140 k3 = k3 + 1
1141 }
1142 }
1143 gi3 = gi3 + 1
1144 }
1145 ft_out("uv covered=" as *u8); ft_outn(covered)
1146 ft_out(" of " as *u8); ft_outn(vtotal)
1147 ft_nl()
1148 if covered * 2 < vtotal { ft_err("FBXTEXM-REFUSE under half the vertices got UVs -- wrong subject\n" as *u8); return FT_E_BAD }
1149
1150 // ---- albedo atlas (authored files; scalar-fill charts use the material's authored color) ----
1151 let atlas: *u8 = sys_mmap(res * res * 3 + 64)
1152 let pathbuf: *u8 = sys_mmap(FT_NAMECAP * 2)
1153 // per-chart scratch for the fork-isolated decoder, derived from the output path
1154 let tmpp: *u8 = sys_mmap(FT_NAMECAP * 2)
1155 var tn: i64 = 0
1156 while outp[tn] != (0 as u8) { tmpp[tn] = outp[tn]; tn = tn + 1 }
1157 let sfx: *u8 = ".cellwork" as *u8
1158 var sj: i64 = 0
1159 while sfx[sj] != (0 as u8) { tmpp[tn] = sfx[sj]; tn = tn + 1; sj = sj + 1 }
1160 tmpp[tn] = 0 as u8
1161 var loaded: i64 = 0
1162 var scalarfills: i64 = 0
1163 var ci: i64 = 0
1164 while ci < nchart {
1165 let cx0: i64 = (ci % g) * cell
1166 let cy0: i64 = (ci / g) * cell
1167 var done: i64 = 0
1168 if ch_file[ci] != 0 {
1169 ft_pathjoin(texdir, ch_file[ci] as *u8, pathbuf)
1170 let rc: i64 = ft_cell_load(pathbuf, atlas, res, cx0, cy0, cell, tmpp)
1171 if rc == 0 {
1172 done = 1
1173 loaded = loaded + 1
1174 ft_out("chart " as *u8); ft_outn(ci)
1175 ft_out(" albedo " as *u8); ft_out(ch_file[ci] as *u8)
1176 ft_nl()
1177 } else {
1178 ft_out("chart " as *u8); ft_outn(ci)
1179 ft_out(" albedo LOAD-REFUSED rc=" as *u8); ft_outn(rc)
1180 ft_out(" file " as *u8); ft_out(ch_file[ci] as *u8)
1181 ft_nl()
1182 }
1183 }
1184 if done == 0 {
1185 let m: i64 = ch_mat[ci]
1186 var r2: i64 = 128
1187 var g2: i64 = 128
1188 var b2: i64 = 128
1189 if mat_hasdc[m] == 1 { r2 = mat_dr[m]; g2 = mat_dg[m]; b2 = mat_db[m] }
1190 var yy: i64 = 0
1191 while yy < cell {
1192 var xx: i64 = 0
1193 while xx < cell {
1194 let o2: i64 = ((cy0 + yy) * res + cx0 + xx) * 3
1195 atlas[o2] = r2 as u8
1196 atlas[o2+1] = g2 as u8
1197 atlas[o2+2] = b2 as u8
1198 xx = xx + 1
1199 }
1200 yy = yy + 1
1201 }
1202 scalarfills = scalarfills + 1
1203 ft_out("chart " as *u8); ft_outn(ci)
1204 ft_out(" SCALAR-FILL authored-diffuse hasdc=" as *u8); ft_outn(mat_hasdc[m])
1205 ft_nl()
1206 }
1207 ci = ci + 1
1208 }
1209 ft_out("albedo atlas: loaded=" as *u8); ft_outn(loaded)
1210 ft_out(" scalar=" as *u8); ft_outn(scalarfills)
1211 ft_out(" of " as *u8); ft_outn(nchart)
1212 ft_nl()
1213
1214 // ---- rough + metal: carried only when EVERY chart decodes; else absent BY NAME ----
1215 let rough: *u8 = sys_mmap(res * res * 3 + 64)
1216 let metal: *u8 = sys_mmap(res * res * 3 + 64)
1217 var have_rough: i64 = 1
1218 var have_metal: i64 = 1
1219 var ci2: i64 = 0
1220 while ci2 < nchart {
1221 let cx1: i64 = (ci2 % g) * cell
1222 let cy1: i64 = (ci2 / g) * cell
1223 var rok: i64 = 0
1224 if ch_rough[ci2] != 0 {
1225 ft_pathjoin(texdir, ch_rough[ci2] as *u8, pathbuf)
1226 if ft_cell_load(pathbuf, rough, res, cx1, cy1, cell, tmpp) == 0 { rok = 1 }
1227 }
1228 if rok == 0 {
1229 if have_rough == 1 {
1230 ft_out("rough chart " as *u8); ft_outn(ci2)
1231 ft_out(" no decodable source -> ROUGH MAP ABSENT\n" as *u8)
1232 }
1233 have_rough = 0
1234 }
1235 var mok: i64 = 0
1236 if ch_metal[ci2] != 0 {
1237 ft_pathjoin(texdir, ch_metal[ci2] as *u8, pathbuf)
1238 if ft_cell_load(pathbuf, metal, res, cx1, cy1, cell, tmpp) == 0 { mok = 1 }
1239 }
1240 if mok == 0 {
1241 if have_metal == 1 {
1242 ft_out("metal chart " as *u8); ft_outn(ci2)
1243 ft_out(" no decodable source -> METAL MAP ABSENT\n" as *u8)
1244 }
1245 have_metal = 0
1246 }
1247 ci2 = ci2 + 1
1248 }
1249
1250 // ---- encode maps ----
1251 let fbp: *i64 = sys_mmap(res * res * 8 + 64) as *i64
1252 let blobcap: i64 = (res * res * 3 + res * 2 + 4096) * FT_TM_NMAP
1253 let blob: *u8 = sys_mmap(blobcap + 64)
1254 let moff: *i64 = sys_mmap(FT_TM_NMAP * 8) as *i64
1255 let mlen: *i64 = sys_mmap(FT_TM_NMAP * 8) as *i64
1256 var mslot: i64 = 0
1257 while mslot < FT_TM_NMAP { moff[mslot] = 0; mlen[mslot] = 0; mslot = mslot + 1 }
1258 var bo: i64 = 0
1259 var p2: i64 = 0
1260 while p2 < res * res {
1261 fbp[p2] = (atlas[p2*3] as i64) + (atlas[p2*3+1] as i64) * 256 + (atlas[p2*3+2] as i64) * 65536
1262 p2 = p2 + 1
1263 }
1264 let alen: i64 = write_png_buf(fbp, res, res, ((blob as i64) + bo) as *u8)
1265 if alen <= 0 { ft_err("FBXTEXM-REFUSE albedo PNG encode failed\n" as *u8); return FT_E_BAD }
1266 if bo + alen > blobcap { ft_err("FBXTEXM-REFUSE blob reserve exceeded -- refusing, not truncating\n" as *u8); return FT_E_BAD }
1267 moff[FT_SLOT_ALBEDO] = bo
1268 mlen[FT_SLOT_ALBEDO] = alen
1269 bo = bo + alen
1270 if have_metal == 1 {
1271 var p3: i64 = 0
1272 while p3 < res * res {
1273 fbp[p3] = (metal[p3*3] as i64) + (metal[p3*3+1] as i64) * 256 + (metal[p3*3+2] as i64) * 65536
1274 p3 = p3 + 1
1275 }
1276 let ml: i64 = write_png_buf(fbp, res, res, ((blob as i64) + bo) as *u8)
1277 if ml > 0 { if bo + ml <= blobcap { moff[FT_SLOT_METAL] = bo; mlen[FT_SLOT_METAL] = ml; bo = bo + ml } }
1278 }
1279 if have_rough == 1 {
1280 var p4: i64 = 0
1281 while p4 < res * res {
1282 fbp[p4] = (rough[p4*3] as i64) + (rough[p4*3+1] as i64) * 256 + (rough[p4*3+2] as i64) * 65536
1283 p4 = p4 + 1
1284 }
1285 let rl: i64 = write_png_buf(fbp, res, res, ((blob as i64) + bo) as *u8)
1286 if rl > 0 { if bo + rl <= blobcap { moff[FT_SLOT_ROUGH] = bo; mlen[FT_SLOT_ROUGH] = rl; bo = bo + rl } }
1287 }
1288 ft_out("normal map: ABSENT-BY-DEPTH (measured: donor normals are 16-bit PNG; this decoder refuses rather than quantize silently)\n" as *u8)
1289
1290 let rcw: i64 = ft_write_out(xb, xns, uvq, FT_TX_PHDR + vtotal * FT_TX_STRIDE, res, g, moff, mlen, bo, blob, outp)
1291 if rcw != 0 { return rcw }
1292 ft_out("FBXTEXM-OK verts=" as *u8); ft_outn(vtotal)
1293 ft_out(" charts=" as *u8); ft_outn(nchart)
1294 ft_out(" res=" as *u8); ft_outn(res)
1295 ft_out(" albedo_bytes=" as *u8); ft_outn(mlen[FT_SLOT_ALBEDO])
1296 ft_out(" metal_bytes=" as *u8); ft_outn(mlen[FT_SLOT_METAL])
1297 ft_out(" rough_bytes=" as *u8); ft_outn(mlen[FT_SLOT_ROUGH])
1298 ft_out(" normal_bytes=0" as *u8)
1299 ft_nl()
1300 return FT_OK
1301}