nx_nxa_texm.nx source
↩ module page · 496 lines · 24485 B
1// nx_nxa_texm.nx -- WRITE THE TEXM SECTION: the baked PBR map set carried INSIDE the asset.
2//
3// THE MEASURED GAP THIS CLOSES. nx_asset_floor_gate scores the shipped character 4/7 and prints
4// "PBR texture maps (TEXM) -- we shade procedurally from genome: ABSENT". An independent lane
5// measured that this -- not triangle count -- is why renders read as clay: at 28,092 tris the
6// silhouette is already at 0.94x a shipping real-time budget, and the body carries 104 joints
7// against VRM 1.0's 15 required. Adequate FORM with NO SURFACE is the definition of clay.
8// TEXC (the UV unwrap) has been present in the asset for some time and nx_nxa_texbake produces
9// real maps -- but NOTHING WROTE A MAP BACK INTO THE CONTAINER. Proven absence, not assumed:
10// grep TEXM over 23,018 files, coverage_complete=1 corpus_complete=1, returns exactly TWO hits,
11// both inside nx_asset_floor_gate, both READS. This organ is the missing writer.
12//
13// WHICH PBR MODEL TEXM STORES, AND WHY -- SETTLED HERE RATHER THAN LEFT IMPLICIT.
14// TEXM stores SPECULAR-GLOSSINESS: D(albedo) S(specular) G(gloss) N(normal), exactly the set
15// nx_nxa_texbake_lib bakes (NTB_MAP_ALBEDO/SPEC/GLOSS/NORMAL) and exactly the set
16// knowledge/asset_texture_floor.conf measured from 23 real shipped packages ("tex_map_set 4 maps
17// D(diffuse) S(specular) G(gloss) N|NM(normal)"). Storing anything else would mean transcoding at
18// bake time and recording a number no instrument in this estate produces.
19// !!THE CONSEQUENCE, STATED PLAINLY RATHER THAN DISCOVERED LATER: glTF 2.0 core is
20// METALLIC-ROUGHNESS, and KHR_materials_pbrSpecularGlossiness was ARCHIVED by Khronos. So a TEXM
21// set CANNOT be written to core glTF 2.0 without a conversion, and that conversion is LOSSY --
22// spec-gloss to metal-rough is not bijective (a dielectric with a coloured specular has no exact
23// metal-rough equivalent). Anyone exporting TEXM through glTF owes that conversion step; this
24// organ does not pretend it is free. The alternative -- baking metal-rough instead -- was NOT
25// chosen because it would make our own measured floor unusable as a comparison.
26//
27// ADDITIVE ONLY (rule 13). Every pre-existing section is copied forward and its checksum is
28// RECOMPUTED rather than copied, because a copied checksum validates its own bug -- the same
29// discipline nx_nxa_texc_lib's ntx_apply already applies. An existing TEXM is REPLACED, not
30// duplicated, so the organ is idempotent: running it twice yields one TEXM, never two.
31//
32// RESOLUTION IS NOT MINE TO PICK. The default is READ from knowledge/asset_texture_floor.conf
33// (tex_res_floor, measured at 4096 px from real package image headers). If the conf cannot be
34// read the organ REFUSES rather than inventing a default -- an unmeasured default is exactly the
35// magic number this estate forbids. A caller may override it explicitly, and the organ ANNOUNCES
36// the bytes each map cost so the resource decision is visible instead of buried.
37//
38// nx_nxa_texm <in.nxa> <out.nxa> [res] [seed] [hue]
39// EXITS: 0 OK | 2 usage | 3 bad container | 4 missing prerequisite section | 1 I/O
40// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
41import "nx_syscalls.nx"
42import "nx_nxa.nx"
43import "nx_nxa_texc_lib.nx"
44import "nx_nxa_texbake_lib.nx"
45import "nx_png.nx"
46// ONE OWNER of the relief law (moved here 2026-08-25). Imported directly so the ANNOUNCE
47// below can state whether the chosen rung actually CARRIES the band it is baking, rather
48// than reporting only what the map cost. A leaf lib -- it imports nx_syscalls and nothing else.
49import "nx_relief_lib.nx"
50
51// ---- exit contract ----
52const TXM_OK: i64 = 0
53const TXM_E_IO: i64 = 1
54const TXM_E_USAGE: i64 = 2
55const TXM_E_BAD: i64 = 3
56const TXM_E_NOSEC: i64 = 4
57
58// ---- TEXM section layout, in i64 words (the container is word-granular) ----
59// header: [nmaps][res][pbr_model][seed][hue][blob_words]
60// then one 3-word record per map: [map_id][blob_byte_off][blob_byte_len]
61// then the PNG blob area, byte-packed and padded up to a word boundary.
62const TXM_HDR_WORDS: i64 = 6
63const TXM_MAPREC_WORDS: i64 = 3
64const TXM_W_NMAPS: i64 = 0
65const TXM_W_RES: i64 = 1
66const TXM_W_MODEL: i64 = 2
67const TXM_W_SEED: i64 = 3
68const TXM_W_HUE: i64 = 4
69const TXM_W_BLOBW: i64 = 5
70// PBR model identifiers. 1 is what we store; 2 is named so a future metal-rough set is a VALUE,
71// not a silent reinterpretation of the same number.
72const TXM_MODEL_SPECGLOSS: i64 = 1
73const TXM_MODEL_METALROUGH: i64 = 2
74
75// ---- conf ----
76const TXM_CONF_PATH: *u8 = "knowledge/asset_texture_floor.conf"
77const TXM_CONF_KEY: *u8 = "tex_res_floor"
78const TXM_CONF_CAP: i64 = 65536
79
80// ---- defaults that are NOT thresholds ----
81// seed and hue select WHICH skin is baked, not whether it passes anything. Both are overridable.
82const TXM_DEFAULT_SEED: i64 = 1
83const TXM_RES_MIN: i64 = 8 // a PNG smaller than one filter block is not a texture
84
85const TXM_ASCII_NL: i64 = 10
86const TXM_ASCII_SP: i64 = 32
87const TXM_ASCII_TAB: i64 = 9
88const TXM_ASCII_ZERO: i64 = 48
89const TXM_ASCII_NINE: i64 = 57
90const TXM_DECIMAL: i64 = 10
91const TXM_RGB_BYTES: i64 = 3
92const TXM_PIX_G_SHIFT: i64 = 256
93const TXM_PIX_B_SHIFT: i64 = 65536
94const TXM_WORD_BYTES: i64 = 8
95const TXM_MISS: i64 = 0 - 1
96// ---- SHIP-RESOLUTION DERIVATION (CLOSURE 2) ----
97// The ship resolution is DERIVED from a texture-memory budget, never picked. tex_res_floor stays
98// the MEASURED bar; ship mode reads the budget conf and returns the largest measured rung that
99// fits. If the budget is unsourced/absent it REFUSES -- an unset budget must not silently default.
100const TXM_SHIP_CONF: *u8 = "knowledge/texture_ship_budget.conf"
101const TXM_SHIP_ARG: *u8 = "ship"
102// the fixed power-of-two texture ladder (a hardware constraint, not a tunable threshold). Each is
103// paired with its res_bytes_<n> key in the budget conf; the bytes are READ, only the rung set is
104// fixed here.
105const TXM_RUNG_4096: i64 = 4096
106const TXM_RUNG_2048: i64 = 2048
107const TXM_RUNG_1024: i64 = 1024
108const TXM_RUNG_512: i64 = 512
109
110func txm_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
111
112// nx_nxa.nx carries no atoi (checked: it exports exactly NXA_VER, nxa_magic, nxa_tag4,
113// nxa_check2, nxa_find), so argv parsing lives here. Returns TXM_MISS on a non-numeric argument
114// rather than 0 -- "the caller typed nonsense" and "the caller asked for zero" must not collapse
115// into the same answer, because one of them would silently become a resolution.
116func txm_atoi(s: *u8) -> i64 {
117 var i: i64 = 0
118 var v: i64 = 0
119 var digits: i64 = 0
120 var ok: i64 = 1
121 while s[i] != (0 as u8) {
122 let c: i64 = (s[i] & 255) as i64
123 if c < TXM_ASCII_ZERO { ok = 0 }
124 if c > TXM_ASCII_NINE { ok = 0 }
125 if ok == 1 { v = v*TXM_DECIMAL + (c - TXM_ASCII_ZERO); digits = digits + 1 }
126 i = i + 1
127 }
128 if ok == 0 { return TXM_MISS }
129 if digits == 0 { return TXM_MISS }
130 return v
131}
132
133// LINE-ANCHORED whitespace conf read: `key value unit comment`. Anchoring matters -- an
134// unanchored match would read a number out of the file's long prose header and answer confidently
135// with the wrong resolution.
136func txm_conf_int(key: *u8) -> i64 {
137 let lp: *i64 = sys_mmap(TXM_WORD_BYTES*2) as *i64
138 let b: *u8 = sys_read_file(TXM_CONF_PATH, lp)
139 if (b as i64) == 0 { return TXM_MISS }
140 let n: i64 = lp[0]
141 let klen: i64 = txm_slen(key)
142 var i: i64 = 0
143 var at_line_start: i64 = 1
144 var found: i64 = 0
145 var result: i64 = TXM_MISS
146 while i < n {
147 if found == 0 { if at_line_start == 1 {
148 var matched: i64 = 1
149 var k: i64 = 0
150 while k < klen {
151 if i + k >= n { matched = 0 }
152 if matched == 1 { if b[i + k] != key[k] { matched = 0 } }
153 k = k + 1
154 }
155 // the key must be followed by whitespace, or `tex_res_floor` also matches a longer key
156 if matched == 1 {
157 var o: i64 = i + klen
158 var sep: i64 = 0
159 if o < n { if b[o] == TXM_ASCII_SP { sep = 1 } }
160 if o < n { if b[o] == TXM_ASCII_TAB { sep = 1 } }
161 if sep == 1 {
162 // EXPLICIT FLAG, never a cursor clobber: a loop that exits by overwriting its
163 // own index cannot also report where it stopped, and the restore arithmetic is
164 // where that bug hides.
165 var ws: i64 = 1
166 while ws == 1 {
167 if o >= n { ws = 0 }
168 if ws == 1 {
169 if b[o] == TXM_ASCII_SP { o = o + 1 } else {
170 if b[o] == TXM_ASCII_TAB { o = o + 1 } else { ws = 0 }
171 }
172 }
173 }
174 var v: i64 = 0
175 var digits: i64 = 0
176 var scanning: i64 = 1
177 while scanning == 1 {
178 if o >= n { scanning = 0 }
179 if scanning == 1 {
180 let c: i64 = (b[o] & 255) as i64
181 if c < TXM_ASCII_ZERO { scanning = 0 }
182 if c > TXM_ASCII_NINE { scanning = 0 }
183 if scanning == 1 { v = v*TXM_DECIMAL + (c - TXM_ASCII_ZERO); digits = digits + 1; o = o + 1 }
184 }
185 }
186 if digits > 0 { result = v; found = 1 }
187 }
188 }
189 } }
190 if b[i] == TXM_ASCII_NL { at_line_start = 1 } else { at_line_start = 0 }
191 i = i + 1
192 }
193 return result
194}
195
196func txm_apply(inp: *u8, outp: *u8, res: i64, seed: i64, hue: i64) -> i64 {
197 let lp: *i64 = sys_mmap(TXM_WORD_BYTES*2) as *i64
198 let b: *u8 = sys_read_file(inp, lp)
199 if (b as i64) == 0 { nt_err("TEXM-REFUSE cannot read input NXA\n" as *u8); return TXM_E_IO }
200 let flen: i64 = lp[0]
201 if flen < NT_HDR { nt_err("TEXM-REFUSE file too short to be an NXA\n" as *u8); return TXM_E_BAD }
202 if nt_rd64(b, 0) != nxa_magic() { nt_err("TEXM-REFUSE not an NXA (bad magic)\n" as *u8); return TXM_E_BAD }
203 let ns: i64 = nt_rd64(b, 16)
204 if ns < 1 { nt_err("TEXM-REFUSE section count invalid\n" as *u8); return TXM_E_BAD }
205 if ns >= NT_MAXSEC { nt_err("TEXM-REFUSE section table full\n" as *u8); return TXM_E_BAD }
206 if flen < NT_HDR + ns*NT_TOCE { nt_err("TEXM-REFUSE toc runs past EOF\n" as *u8); return TXM_E_BAD }
207
208 // TEXC is a HARD PREREQUISITE: without a UV unwrap there is nothing to bake into.
209 var has_texc: i64 = 0
210 var tmi: i64 = TXM_MISS
211 var s: i64 = 0
212 while s < ns {
213 let e: i64 = NT_HDR + s*NT_TOCE
214 if nt_tageq(b, e, "TEXC" as *u8) == 1 { has_texc = 1 }
215 if nt_tageq(b, e, "TEXM" as *u8) == 1 { tmi = s }
216 s = s + 1
217 }
218 if has_texc == 0 { nt_err("TEXM-REFUSE no TEXC section -- run nx_nxa_texc first, there are no UVs to bake into\n" as *u8); return TXM_E_NOSEC }
219 if res < TXM_RES_MIN { nt_err("TEXM-REFUSE resolution below one filter block\n" as *u8); return TXM_E_BAD }
220
221 // ---- bake every map, PNG-compress each into the blob area ----
222 // Raw RGB8 at the measured 4096 floor would be 50 MB PER MAP; PNG is what makes an embedded
223 // map set a sane artifact rather than a 200 MB container. write_png_buf is the estate's own
224 // encoder -- composed, not re-implemented.
225 let atlas: *u8 = sys_mmap(res*res*TXM_RGB_BYTES + 64)
226 let fbp: *i64 = sys_mmap(res*res*TXM_WORD_BYTES + 64) as *i64
227 // PNG of noisy data can exceed raw; the reserve is derived from the raw size plus a full
228 // scanline-filter overhead, not guessed.
229 let blobcap: i64 = (res*res*TXM_RGB_BYTES + res*2 + 4096) * NTB_NMAP
230 let blob: *u8 = sys_mmap(blobcap + 64)
231 let moff: *i64 = sys_mmap(NTB_NMAP*TXM_WORD_BYTES + 64) as *i64
232 let mlen: *i64 = sys_mmap(NTB_NMAP*TXM_WORD_BYTES + 64) as *i64
233 var bo: i64 = 0
234 var m: i64 = 0
235 while m < NTB_NMAP {
236 let rc: i64 = ntb_bake_pbr(b, flen, res, seed, hue, m, atlas)
237 if rc != 0 { nt_err("TEXM-REFUSE a map failed to bake\n" as *u8); return TXM_E_BAD }
238 // atlas is RGB8; write_png_buf wants one packed pixel per i64 (c = r + g<<8 + b<<16),
239 // the exact convention ntb_write_atlas_png uses.
240 var p: i64 = 0
241 while p < res*res {
242 fbp[p] = (atlas[p*TXM_RGB_BYTES] as i64) + (atlas[p*TXM_RGB_BYTES+1] as i64)*TXM_PIX_G_SHIFT + (atlas[p*TXM_RGB_BYTES+2] as i64)*TXM_PIX_B_SHIFT
243 p = p + 1
244 }
245 let plen: i64 = write_png_buf(fbp, res, res, ((blob as i64) + bo) as *u8)
246 if plen <= 0 { nt_err("TEXM-REFUSE PNG encode produced nothing\n" as *u8); return TXM_E_BAD }
247 if bo + plen > blobcap { nt_err("TEXM-REFUSE blob reserve exceeded -- REFUSING rather than truncating a map\n" as *u8); return TXM_E_BAD }
248 moff[m] = bo
249 mlen[m] = plen
250 bo = bo + plen
251 m = m + 1
252 }
253 let blobbytes: i64 = bo
254 var blobwords: i64 = blobbytes / TXM_WORD_BYTES
255 if blobwords*TXM_WORD_BYTES < blobbytes { blobwords = blobwords + 1 }
256 let twords: i64 = TXM_HDR_WORDS + NTB_NMAP*TXM_MAPREC_WORDS + blobwords
257
258 // ---- rebuild the container: every section forward, TEXM replaced not duplicated ----
259 var nsec: i64 = ns
260 if tmi < 0 { nsec = ns + 1 }
261 let toclen: i64 = NT_HDR + nsec*NT_TOCE
262 var total: i64 = toclen
263 var s2: i64 = 0
264 while s2 < ns {
265 if s2 != tmi {
266 let e2: i64 = NT_HDR + s2*NT_TOCE
267 total = total + nt_rd64(b, e2 + 16)*TXM_WORD_BYTES
268 }
269 s2 = s2 + 1
270 }
271 total = total + twords*TXM_WORD_BYTES
272 let nb: *u8 = sys_mmap(total + NT_PAD)
273 if (nb as i64) == 0 { nt_err("TEXM-REFUSE cannot allocate output\n" as *u8); return TXM_E_IO }
274 nt_wr64(nb, 0, nxa_magic())
275 nt_wr64(nb, 8, NXA_VER)
276 nt_wr64(nb, 16, nsec)
277 var wo: i64 = toclen
278 var ti: i64 = 0
279 var s3: i64 = 0
280 var carried: i64 = 0
281 while s3 < ns {
282 if s3 != tmi {
283 let e3: i64 = NT_HDR + s3*NT_TOCE
284 let oldoff: i64 = nt_rd64(b, e3 + 8)
285 let wl: i64 = nt_rd64(b, e3 + 16)
286 let te: i64 = NT_HDR + ti*NT_TOCE
287 nt_wr64(nb, te, nt_rd64(b, e3))
288 nt_wr64(nb, te + 8, wo)
289 nt_wr64(nb, te + 16, wl)
290 var k2: i64 = 0
291 while k2 < wl*TXM_WORD_BYTES { nb[wo + k2] = b[oldoff + k2]; k2 = k2 + 1 }
292 // RECOMPUTE, never copy: a copied checksum validates its own bug.
293 let pw2: *i64 = ((nb as i64) + wo) as *i64
294 nt_wr64(nb, te + 24, nxa_check2(1, pw2, wl))
295 wo = wo + wl*TXM_WORD_BYTES
296 ti = ti + 1
297 carried = carried + 1
298 }
299 s3 = s3 + 1
300 }
301 // ---- append TEXM ----
302 let te4: i64 = NT_HDR + ti*NT_TOCE
303 nt_wr64(nb, te4, nxa_tag4("TEXM" as *u8))
304 nt_wr64(nb, te4 + 8, wo)
305 nt_wr64(nb, te4 + 16, twords)
306 nt_wr64(nb, wo + TXM_W_NMAPS*TXM_WORD_BYTES, NTB_NMAP)
307 nt_wr64(nb, wo + TXM_W_RES*TXM_WORD_BYTES, res)
308 nt_wr64(nb, wo + TXM_W_MODEL*TXM_WORD_BYTES, TXM_MODEL_SPECGLOSS)
309 nt_wr64(nb, wo + TXM_W_SEED*TXM_WORD_BYTES, seed)
310 nt_wr64(nb, wo + TXM_W_HUE*TXM_WORD_BYTES, hue)
311 nt_wr64(nb, wo + TXM_W_BLOBW*TXM_WORD_BYTES, blobwords)
312 var m2: i64 = 0
313 while m2 < NTB_NMAP {
314 let ro: i64 = wo + (TXM_HDR_WORDS + m2*TXM_MAPREC_WORDS)*TXM_WORD_BYTES
315 nt_wr64(nb, ro, m2)
316 nt_wr64(nb, ro + TXM_WORD_BYTES, moff[m2])
317 nt_wr64(nb, ro + TXM_WORD_BYTES*2, mlen[m2])
318 m2 = m2 + 1
319 }
320 let blobbase: i64 = wo + (TXM_HDR_WORDS + NTB_NMAP*TXM_MAPREC_WORDS)*TXM_WORD_BYTES
321 var z: i64 = 0
322 while z < blobwords*TXM_WORD_BYTES { nb[blobbase + z] = 0 as u8; z = z + 1 }
323 var c3: i64 = 0
324 while c3 < blobbytes { nb[blobbase + c3] = blob[c3]; c3 = c3 + 1 }
325 let pw3: *i64 = ((nb as i64) + wo) as *i64
326 nt_wr64(nb, te4 + 24, nxa_check2(1, pw3, twords))
327 wo = wo + twords*TXM_WORD_BYTES
328 let tb2: *i64 = ((nb as i64) + NT_HDR) as *i64
329 nt_wr64(nb, 24, nxa_check2(1, tb2, nsec*4))
330
331 let fd: i64 = sys_openat_wr(outp, NT_MODE)
332 if fd < 0 { nt_err("TEXM-REFUSE cannot open output\n" as *u8); return TXM_E_IO }
333 let wrote: i64 = sys_write(fd, nb, wo)
334 sys_close(fd)
335 if wrote != wo { nt_err("TEXM-REFUSE short write\n" as *u8); return TXM_E_IO }
336
337 // ANNOUNCE: an unannounced publish makes "did it land" a hunt instead of a number, and the
338 // per-map bytes make the resource cost of the chosen resolution VISIBLE rather than buried.
339 nt_outs("TEXM-OK maps=" as *u8); nt_outn(NTB_NMAP)
340 nt_outs(" res=" as *u8); nt_outn(res)
341 nt_outs(" model=specgloss sections_carried=" as *u8); nt_outn(carried)
342 nt_outs(" sections_out=" as *u8); nt_outn(nsec)
343 nt_outs(" albedo_bytes=" as *u8); nt_outn(mlen[NTB_MAP_ALBEDO])
344 nt_outs(" spec_bytes=" as *u8); nt_outn(mlen[NTB_MAP_SPEC])
345 nt_outs(" gloss_bytes=" as *u8); nt_outn(mlen[NTB_MAP_GLOSS])
346 nt_outs(" normal_bytes=" as *u8); nt_outn(mlen[NTB_MAP_NORMAL])
347 nt_outs(" texm_bytes=" as *u8); nt_outn(twords*TXM_WORD_BYTES)
348 nt_outs(" file_bytes=" as *u8); nt_outn(wo)
349 // PHYSICAL ADEQUACY, not just cost. txm_ship_res picks the largest rung that fits a
350 // texture-MEMORY budget and consults nothing physical, so a rung can be affordable and
351 // still bake a band on a lattice coarser than the band -- silently, because bytes look
352 // fine. A band survives only where lambda/texel >= 2; below that what lands is a coarser
353 // band wearing this one's name. Printing carried= makes that decidable from the receipt
354 // instead of requiring a reader to redo the arithmetic. res_needed is the whole-body
355 // answer; the pore band needs a PER-REGION atlas rather than a bigger whole-body one,
356 // which is a rung and not a knob -- so pore_carried=0 here is expected, not a regression.
357 nt_outs(" relief_carried=" as *u8); nt_outn(rlf_band_resolvable(res, RLF_RELIEF_LAMBDA_UM))
358 nt_outs(" pore_carried=" as *u8); nt_outn(rlf_band_resolvable(res, RLF_PORE_PITCH_UM))
359 nt_outs(" relief_res_needed=" as *u8); nt_outn(rlf_band_res_needed(RLF_BODY_UM, RLF_RELIEF_LAMBDA_UM))
360 nt_outs(" texel_um=" as *u8); nt_outn(rlf_texel_um(res))
361 nt_outs("\n" as *u8)
362 return TXM_OK
363}
364
365// numeric conf read from an ARBITRARY path (whitespace-separated key<ws>value), MISS on
366// non-numeric. txm_conf_int is the fixed-path floor reader; this is its path-parameterised twin so
367// the budget conf can be read without a second copy of the scan logic drifting from it.
368func txm_conf_at(path: *u8, key: *u8) -> i64 {
369 let lp: *i64 = sys_mmap(TXM_WORD_BYTES*2) as *i64
370 let b: *u8 = sys_read_file(path, lp)
371 if (b as i64) == 0 { return TXM_MISS }
372 let n: i64 = lp[0]
373 let klen: i64 = txm_slen(key)
374 var i: i64 = 0
375 var at_line_start: i64 = 1
376 var found: i64 = 0
377 var result: i64 = TXM_MISS
378 while i < n {
379 if found == 0 { if at_line_start == 1 {
380 var matched: i64 = 1
381 var k: i64 = 0
382 while k < klen {
383 if i + k >= n { matched = 0 }
384 if matched == 1 { if b[i + k] != key[k] { matched = 0 } }
385 k = k + 1
386 }
387 if matched == 1 {
388 var o: i64 = i + klen
389 var sep: i64 = 0
390 if o < n { if b[o] == TXM_ASCII_SP { sep = 1 } }
391 if o < n { if b[o] == TXM_ASCII_TAB { sep = 1 } }
392 if sep == 1 {
393 var ws: i64 = 1
394 while ws == 1 {
395 if o >= n { ws = 0 }
396 if ws == 1 {
397 if b[o] == TXM_ASCII_SP { o = o + 1 } else {
398 if b[o] == TXM_ASCII_TAB { o = o + 1 } else { ws = 0 }
399 }
400 }
401 }
402 var v: i64 = 0
403 var digits: i64 = 0
404 var scanning: i64 = 1
405 while scanning == 1 {
406 if o >= n { scanning = 0 }
407 if scanning == 1 {
408 let c: i64 = (b[o] & 255) as i64
409 if c < TXM_ASCII_ZERO { scanning = 0 }
410 if c > TXM_ASCII_NINE { scanning = 0 }
411 if scanning == 1 { v = v*TXM_DECIMAL + (c - TXM_ASCII_ZERO); digits = digits + 1; o = o + 1 }
412 }
413 }
414 if digits > 0 { result = v; found = 1 }
415 }
416 }
417 } }
418 if b[i] == TXM_ASCII_NL { at_line_start = 1 } else { at_line_start = 0 }
419 i = i + 1
420 }
421 return result
422}
423// is argv[3] the literal "ship"?
424func txm_is_ship(s: *u8) -> i64 {
425 var i: i64 = 0
426 while TXM_SHIP_ARG[i] != (0 as u8) {
427 if s[i] != TXM_SHIP_ARG[i] { return 0 }
428 i = i + 1
429 }
430 if s[i] != (0 as u8) { return 0 }
431 return 1
432}
433// DERIVE the ship resolution: the largest measured rung whose bytes*cast_size fits the budget.
434// Returns TXM_MISS when the budget is unsourced/missing OR nothing fits -- the caller then REFUSES
435// rather than defaulting. Reads MEASURED bytes from the conf; the only thing fixed in code is the
436// power-of-two rung set.
437func txm_ship_res(confpath: *u8) -> i64 {
438 let budget: i64 = txm_conf_at(confpath, "texture_budget_bytes" as *u8)
439 if budget == TXM_MISS { return TXM_MISS }
440 let cast: i64 = txm_conf_at(confpath, "cast_size" as *u8)
441 if cast == TXM_MISS { return TXM_MISS }
442 let b4: i64 = txm_conf_at(confpath, "res_bytes_4096" as *u8)
443 if b4 != TXM_MISS { if b4*cast <= budget { return TXM_RUNG_4096 } }
444 let b3: i64 = txm_conf_at(confpath, "res_bytes_2048" as *u8)
445 if b3 != TXM_MISS { if b3*cast <= budget { return TXM_RUNG_2048 } }
446 let b2: i64 = txm_conf_at(confpath, "res_bytes_1024" as *u8)
447 if b2 != TXM_MISS { if b2*cast <= budget { return TXM_RUNG_1024 } }
448 let b1: i64 = txm_conf_at(confpath, "res_bytes_512" as *u8)
449 if b1 != TXM_MISS { if b1*cast <= budget { return TXM_RUNG_512 } }
450 return TXM_MISS
451}
452func main(argc: i64, argv: *i64) -> i64 {
453 if argc < 3 {
454 nt_err("usage: nx_nxa_texm <in.nxa> <out.nxa> [res] [seed] [hue]\n" as *u8)
455 nt_err(" bakes the D/S/G/N spec-gloss map set and writes it as a TEXM section.\n" as *u8)
456 nt_err(" <in.nxa> is never modified; an existing TEXM is replaced (idempotent).\n" as *u8)
457 nt_err(" res defaults to tex_res_floor in knowledge/asset_texture_floor.conf (MEASURED, not picked).\n" as *u8)
458 sys_exit(TXM_E_USAGE)
459 return TXM_E_USAGE
460 }
461 var res: i64 = TXM_MISS
462 var ship_mode: i64 = 0
463 if argc >= 4 {
464 let a3: *u8 = argv[3] as *u8
465 if txm_is_ship(a3) == 1 { ship_mode = 1 } else { res = txm_atoi(a3) }
466 }
467 if ship_mode == 1 {
468 // ship mode reads the fixed budget conf by default; an explicit 4th arg overrides the path
469 // (a gate points this at a /tmp fixture so it never mutates the tracked production conf).
470 var bconf: *u8 = TXM_SHIP_CONF
471 if argc >= 5 { bconf = argv[4] as *u8 }
472 res = txm_ship_res(bconf)
473 if res == TXM_MISS {
474 nt_err("TEXM-REFUSE ship mode: texture_budget_bytes is UNSOURCED or missing in knowledge/texture_ship_budget.conf, or no rung fits -- REFUSING to derive against a budget nobody set\n" as *u8)
475 sys_exit(TXM_E_BAD)
476 return TXM_E_BAD
477 }
478 let floor: i64 = txm_conf_int(TXM_CONF_KEY)
479 nt_outs("TEXM-SHIP floor=" as *u8); nt_outn(floor)
480 nt_outs(" ship=" as *u8); nt_outn(res)
481 nt_outs(" (derived: largest MEASURED rung whose bytes*cast_size fits texture_budget_bytes)\n" as *u8)
482 }
483 if res == TXM_MISS { res = txm_conf_int(TXM_CONF_KEY) }
484 if res == TXM_MISS {
485 nt_err("TEXM-REFUSE cannot read tex_res_floor from the conf and no res was given -- REFUSING to invent a default resolution\n" as *u8)
486 sys_exit(TXM_E_BAD)
487 return TXM_E_BAD
488 }
489 var seed: i64 = TXM_DEFAULT_SEED
490 if argc >= 5 { seed = txm_atoi(argv[4] as *u8) }
491 var hue: i64 = NTB_DEFAULT_HUE
492 if argc >= 6 { hue = txm_atoi(argv[5] as *u8) }
493 let rc: i64 = txm_apply(argv[1] as *u8, argv[2] as *u8, res, seed, hue)
494 sys_exit(rc)
495 return rc
496}