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}