code wiki / (root) / nx_nxa_texm.nx

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}