code wiki / (root) / nx_nxa_write_lib.nx

nx_nxa_write_lib.nx source

↩ module page · 228 lines · 9853 B

1// nx_nxa_write_lib.nx -- THE NXANIM01 CONTAINER WRITER AS A LIBRARY (/compare/modding MD2, 2026-09-06). The estate held three 2// INLINE emitters of the same container (nx_meshrig, nx_fbx2nxa, nx_gltf2mesh) and one append-only section writer 3// (nx_nxa_posewrite_lib); a fourth reader (XPS) would have been a fourth copy. This lib composes nx_nxa.nx -- the ONLY 4// place the magic, the tag encoding and the rolling checksum are defined -- so a writer and every reader cannot drift. 5// LAYOUT (knowledge/nxa_format_spec.md, as nx_mesh2glb and nx_meshrig read it): header words [magic][version=1][nsect] 6// [toc check]; TOC entries of 4 words [tag][byte_off][word_len][check]; payloads of i64 words. VERT [nv][x y z ...] in 7// 0.01 mm model units; TRIS [nt][a b c ...] one word per index; SKEL [nj] + 8 words per joint [parent][tx ty tz] 8// [qx qy qz qw q12] with the bind rotation the IDENTITY quaternion by the estate's rig convention (stated, never smuggled); 9// SKIN [nv] + 8 words per vertex [j0 j1 j2 j3][w0 w1 w2 w3] with the four q12 weights summing to exactly 4096. 10// EVERY REFUSAL IS NAMED: a triangle index outside the vertex table, a skin joint outside the skeleton, a vertex whose 11// weights do not sum to 4096, or an empty mesh, all refuse BEFORE a byte is written -- the container never carries a 12// section the spec would call malformed. SKEL and SKIN are written together or not at all (nj == 0 writes VERT and TRIS only). 13// license_tier: ORIGINAL No hw writes (Rule 26). 14import "nx_syscalls.nx" 15import "nx_nxa.nx" 16 17const NXW_HDR_WORDS: i64 = 4 18const NXW_TOC_WORDS: i64 = 4 19const NXW_SKEL_REC: i64 = 8 20const NXW_SKIN_REC: i64 = 8 21const NXW_VERT_REC: i64 = 3 22const NXW_TRI_REC: i64 = 3 23const NXW_INF: i64 = 4 24const NXW_Q12: i64 = 4096 25const NXW_MODE: i64 = 420 26const NXW_ROOT: i64 = 0 - 1 27const NXW_ERR_OPEN: i64 = 0 - 1 28const NXW_ERR_SHORT_WRITE: i64 = 0 - 2 29const NXW_ERR_WEIGHTS: i64 = 0 - 3 30const NXW_ERR_JOINT: i64 = 0 - 4 31const NXW_ERR_TRI: i64 = 0 - 5 32const NXW_ERR_EMPTY: i64 = 0 - 6 33const NXW_ERR_PARENT: i64 = 0 - 7 34// receipt slots 35const NXW_R_BYTES: i64 = 0 36const NXW_R_SECTIONS: i64 = 1 37const NXW_R_WORDS: i64 = 2 38const NXW_R_FIRSTBAD: i64 = 3 39const NXW_R_N: i64 = 4 40 41func nxw_err_name(e: i64) -> *u8 { 42 if e == NXW_ERR_OPEN { return "cannot-open-output" as *u8 } 43 if e == NXW_ERR_SHORT_WRITE { return "short-write" as *u8 } 44 if e == NXW_ERR_WEIGHTS { return "skin-weights-do-not-sum-to-4096" as *u8 } 45 if e == NXW_ERR_JOINT { return "skin-joint-outside-skeleton" as *u8 } 46 if e == NXW_ERR_TRI { return "triangle-index-outside-vertex-table" as *u8 } 47 if e == NXW_ERR_EMPTY { return "empty-mesh" as *u8 } 48 if e == NXW_ERR_PARENT { return "joint-parent-outside-skeleton" as *u8 } 49 return "unnamed" as *u8 50} 51func nxw_nsect(nj: i64) -> i64 { if nj > 0 { return 4 } return 2 } 52// total words of the container for these counts (the caller sizes its buffer from this, never from a constant) 53func nxw_words(nv: i64, nt: i64, nj: i64) -> i64 { 54 var w: i64 = NXW_HDR_WORDS + nxw_nsect(nj) * NXW_TOC_WORDS + 1 + nv * NXW_VERT_REC + 1 + nt * NXW_TRI_REC 55 if nj > 0 { w = w + 1 + nj * NXW_SKEL_REC + 1 + nv * NXW_SKIN_REC } 56 return w 57} 58// validate the inputs; returns 0 or a named NXW_ERR_* with the first offending record index in rc[NXW_R_FIRSTBAD] 59func nxw_validate(nv: i64, tr: *i64, nt: i64, jpar: *i64, nj: i64, sj: *i64, sw: *i64, rc: *i64) -> i64 { 60 rc[NXW_R_FIRSTBAD] = 0 - 1 61 if nv <= 0 { return NXW_ERR_EMPTY } 62 if nt <= 0 { return NXW_ERR_EMPTY } 63 var i: i64 = 0 64 while i < nt * NXW_TRI_REC { 65 if tr[i] < 0 { rc[NXW_R_FIRSTBAD] = i / NXW_TRI_REC; return NXW_ERR_TRI } 66 if tr[i] >= nv { rc[NXW_R_FIRSTBAD] = i / NXW_TRI_REC; return NXW_ERR_TRI } 67 i = i + 1 68 } 69 if nj > 0 { 70 var j: i64 = 0 71 while j < nj { 72 if jpar[j] >= nj { rc[NXW_R_FIRSTBAD] = j; return NXW_ERR_PARENT } 73 j = j + 1 74 } 75 var v: i64 = 0 76 while v < nv { 77 var sum: i64 = 0 78 var k: i64 = 0 79 while k < NXW_INF { 80 let ji: i64 = sj[v * NXW_INF + k] 81 if ji < 0 { rc[NXW_R_FIRSTBAD] = v; return NXW_ERR_JOINT } 82 if ji >= nj { rc[NXW_R_FIRSTBAD] = v; return NXW_ERR_JOINT } 83 sum = sum + sw[v * NXW_INF + k] 84 k = k + 1 85 } 86 if sum != NXW_Q12 { rc[NXW_R_FIRSTBAD] = v; return NXW_ERR_WEIGHTS } 87 v = v + 1 88 } 89 } 90 return 0 91} 92// fill one TOC entry and return the payload's rolling check 93func nxw_toc(buf: *i64, toc0: i64, slot: i64, tag: *u8, word_off: i64, wl: i64) -> i64 { 94 let e: i64 = toc0 + slot * NXW_TOC_WORDS 95 buf[e] = nxa_tag4(tag) 96 buf[e + 1] = word_off * 8 97 buf[e + 2] = wl 98 buf[e + 3] = nxa_check2(1, ((buf as i64) + word_off * 8) as *i64, wl) 99 return buf[e + 3] 100} 101// build the whole container into buf (sized by nxw_words); returns the word count or a named error 102func nxw_build(buf: *i64, vx: *i64, nv: i64, tr: *i64, nt: i64, jpar: *i64, jpos: *i64, nj: i64, sj: *i64, sw: *i64, rc: *i64) -> i64 { 103 let bad: i64 = nxw_validate(nv, tr, nt, jpar, nj, sj, sw, rc) 104 if bad < 0 { return bad } 105 let ns: i64 = nxw_nsect(nj) 106 buf[0] = nxa_magic() 107 buf[1] = NXA_VER 108 buf[2] = ns 109 let toc0: i64 = NXW_HDR_WORDS 110 var wp: i64 = NXW_HDR_WORDS + ns * NXW_TOC_WORDS 111 // VERT 112 let vert_w: i64 = wp 113 buf[wp] = nv; wp = wp + 1 114 var i: i64 = 0 115 while i < nv * NXW_VERT_REC { buf[wp] = vx[i]; wp = wp + 1; i = i + 1 } 116 // TRIS 117 let tris_w: i64 = wp 118 buf[wp] = nt; wp = wp + 1 119 i = 0 120 while i < nt * NXW_TRI_REC { buf[wp] = tr[i]; wp = wp + 1; i = i + 1 } 121 nxw_toc(buf, toc0, 0, "VERT" as *u8, vert_w, 1 + nv * NXW_VERT_REC) 122 nxw_toc(buf, toc0, 1, "TRIS" as *u8, tris_w, 1 + nt * NXW_TRI_REC) 123 if nj > 0 { 124 let skel_w: i64 = wp 125 buf[wp] = nj; wp = wp + 1 126 var j: i64 = 0 127 while j < nj { 128 var par: i64 = jpar[j] 129 if par < 0 { par = NXW_ROOT } 130 buf[wp] = par 131 buf[wp + 1] = jpos[j * 3] 132 buf[wp + 2] = jpos[j * 3 + 1] 133 buf[wp + 3] = jpos[j * 3 + 2] 134 buf[wp + 4] = 0 135 buf[wp + 5] = 0 136 buf[wp + 6] = 0 137 buf[wp + 7] = NXW_Q12 138 wp = wp + NXW_SKEL_REC 139 j = j + 1 140 } 141 let skin_w: i64 = wp 142 buf[wp] = nv; wp = wp + 1 143 var v: i64 = 0 144 while v < nv { 145 buf[wp] = sj[v * NXW_INF] 146 buf[wp + 1] = sj[v * NXW_INF + 1] 147 buf[wp + 2] = sj[v * NXW_INF + 2] 148 buf[wp + 3] = sj[v * NXW_INF + 3] 149 buf[wp + 4] = sw[v * NXW_INF] 150 buf[wp + 5] = sw[v * NXW_INF + 1] 151 buf[wp + 6] = sw[v * NXW_INF + 2] 152 buf[wp + 7] = sw[v * NXW_INF + 3] 153 wp = wp + NXW_SKIN_REC 154 v = v + 1 155 } 156 nxw_toc(buf, toc0, 2, "SKEL" as *u8, skel_w, 1 + nj * NXW_SKEL_REC) 157 nxw_toc(buf, toc0, 3, "SKIN" as *u8, skin_w, 1 + nv * NXW_SKIN_REC) 158 } 159 buf[3] = nxa_check2(1, ((buf as i64) + toc0 * 8) as *i64, ns * NXW_TOC_WORDS) 160 rc[NXW_R_SECTIONS] = ns 161 rc[NXW_R_WORDS] = wp 162 return wp 163} 164// build and write; returns the bytes written or a named error (rc carries bytes, sections, words, first bad record) 165func nxw_write(path: *u8, vx: *i64, nv: i64, tr: *i64, nt: i64, jpar: *i64, jpos: *i64, nj: i64, sj: *i64, sw: *i64, rc: *i64) -> i64 { 166 var q: i64 = 0 167 while q < NXW_R_N { rc[q] = 0; q = q + 1 } 168 let words: i64 = nxw_words(nv, nt, nj) 169 let buf: *i64 = sys_mmap(words * 8 + 64) as *i64 170 let wp: i64 = nxw_build(buf, vx, nv, tr, nt, jpar, jpos, nj, sj, sw, rc) 171 if wp < 0 { return wp } 172 let fd: i64 = sys_openat_wr(path, NXW_MODE) 173 if fd < 0 { return NXW_ERR_OPEN } 174 let n: i64 = wp * 8 175 var done: i64 = 0 176 while done < n { let k: i64 = sys_write(fd, (buf as *u8) + done, n - done); if k <= 0 { break } done = done + k } 177 sys_close(fd) 178 rc[NXW_R_BYTES] = done 179 if done != n { return NXW_ERR_SHORT_WRITE } 180 return done 181} 182 183// Same-size section replacement preserves every unrelated byte, including unknown sections. 184// Caller owns semantic dependencies (e.g. bounds when replacing vertices). 185const NXW_ERR_CONTAINER: i64 = 0-8 186const NXW_ERR_EXTENT: i64 = 0-9 187const NXW_ERR_OVERLAP: i64 = 0-10 188func nxw_ranges_overlap(a: i64,an: i64,b: i64,bn: i64) -> i64 { 189 if an <= 0 || bn <= 0 { return 0 } 190 if a >= b { if a-b < bn { return 1 } } 191 else { if b-a < an { return 1 } } 192 return 0 193} 194func nxw_replace_same_size(b: *u8,n: i64,tag: i64,payload: *i64,words: i64,out: *u8,cap: i64) -> i64 { 195 let entry: i64 = nxa_section_entry(b,n,tag) 196 if entry < 0 { return NXW_ERR_CONTAINER } 197 let h: *i64 = b as *i64 198 if words < 0 || words != h[entry+2] || cap < n { return NXW_ERR_EXTENT } 199 if (out as i64) <= 0 || (payload as i64) <= 0 { return NXW_ERR_EXTENT } 200 if nxw_ranges_overlap(b as i64,n,out as i64,n) == 1 { return NXW_ERR_OVERLAP } 201 if nxw_ranges_overlap(payload as i64,words*8,out as i64,n) == 1 { return NXW_ERR_OVERLAP } 202 var i: i64 = 0 203 while i < h[2] { 204 let e: i64 = 4+i*4 205 if nxa_section_entry(b,n,h[e]) != e { return NXW_ERR_CONTAINER } 206 i=i+1 207 } 208 i=0 209 while i < h[2] { 210 let e: i64 = 4+i*4 211 var j: i64 = i+1 212 while j < h[2] { 213 let f: i64 = 4+j*4 214 if nxw_ranges_overlap(h[e+1],h[e+2]*8,h[f+1],h[f+2]*8) == 1 { return NXW_ERR_CONTAINER } 215 j=j+1 216 } 217 i=i+1 218 } 219 i=0 220 while i < n { out[i]=b[i]; i=i+1 } 221 let dest: *i64 = out as *i64 222 let at: i64 = h[entry+1]/8 223 i=0 224 while i < words { dest[at+i]=payload[i]; i=i+1 } 225 dest[entry+3]=nxa_check2(1,payload,words) 226 dest[3]=nxa_check2(1,((out as i64)+32) as *i64,h[2]*4) 227 return n 228}