nx_mesh.nx source
↩ module page · 336 lines · 12640 B
1// nx_mesh.nx -- typed 3D mesh container.
2//
3// L1 canonical primitive. Closes a bits-up gap: graphics stack
4// (nx_raster + nx_zbuf + nx_tex_sample + nx_vertex_pipeline +
5// nx_render_pass) operates on RAW i64 arrays for vertices, UVs,
6// and triangle indices. No typed envelope. Per the bits-up
7// canonical-layer cardinal, that's a missing L1 container --
8// every graphics consumer should compose against `NxMesh` rather
9// than re-deriving the layout convention each time.
10//
11// Same role for graphics that NxTensor plays for compute and Image
12// plays for pixel buffers.
13//
14// ===== Layout convention =========================================
15//
16// verts: [n_verts * 4] i64
17// per-vertex: (x_q14, y_q14, z_q14, packed_color)
18// matches nx_vertex_pipeline + nx_voxel_mesh existing layouts so
19// buffers compose directly.
20//
21// uvs: [n_verts * 2] i64 (NULL if mesh has no texture)
22// per-vertex: (u_q10, v_q10)
23// matches nx_tex_sample's Q10 UV convention.
24//
25// indices: [n_tris * 3] i64 triangle vertex indices
26// per-triangle: (i0, i1, i2), counter-clockwise winding for
27// front-face by convention.
28//
29// All buffers caller-owned or substrate-allocated via mmap; the
30// NxMesh struct owns the LIFETIME of any buffers it alloc'd.
31//
32// ===== Why a struct + not just docs ==============================
33//
34// Two callers (nx_render_pass + nx_voxel_mesh) already work with
35// these layouts. Per the rule-of-three + DRY cardinal: factor at
36// 2-and-a-half (one of the existing callers + the new graphics work
37// that wants meshes). The struct also lets us add verdicts at the
38// boundary -- caller can't accidentally pass a 3-vert mesh as if it
39// were 4-vert.
40//
41// genealogy_id: opengl_vbo_2003 + obj_wavefront_1991 + gltf_2015 +
42// ply_stanford_1994
43// lineage_id: substrate_mesh_v1
44
45// nx_safety_envelope:
46// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
47// sil_target: SIL1
48// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
49// verdict: NOT_YET_EVALUATED
50
51import "nx_syscalls.nx"
52import "nx_tier.nx"
53import "nx_loop.nx"
54
55const NX_MESH_VTX_STRIDE: nx_int = 4
56const NX_MESH_UV_STRIDE: nx_int = 2
57const NX_MESH_IDX_STRIDE: nx_int = 3
58
59// ===== Sealed-enum: MeshVerdict ===================================
60
61const NX_MESH_OK: nx_int = 0
62const NX_MESH_ERR_BAD_DIMS: nx_int = 1
63const NX_MESH_ERR_NULL_BUFFER: nx_int = 2
64const NX_MESH_ERR_OUT_OF_RANGE: nx_int = 3
65const NX_MESH_N_VERDICTS: nx_int = 4
66
67func nx_mesh_verdict_is_valid(v: nx_int) -> nx_int {
68 if v < 0 { return 0 }
69 if v >= NX_MESH_N_VERDICTS { return 0 }
70 return 1
71}
72
73// ===== Struct =====================================================
74
75struct NxMesh {
76 n_verts: nx_int,
77 n_tris: nx_int,
78 verts: *i64, // [n_verts * 4]
79 uvs: *i64, // [n_verts * 2]; 0 if no texture
80 indices: *i64, // [n_tris * 3]
81 has_uvs: nx_int // 1 if uvs non-null, 0 otherwise
82}
83
84const NX_MESH_BYTES: nx_int = 48 // 6 fields * 8
85
86// ===== Allocation =================================================
87//
88// Allocates buffers sized for `n_verts` vertices and `n_tris`
89// triangles. If `with_uvs` is 1, allocates the UV array; else
90// leaves uvs = null pointer. Caller fills via the setters below.
91
92func nx_mesh_alloc(n_verts: nx_int, n_tris: nx_int, with_uvs: nx_int) -> *NxMesh {
93 if n_verts <= 0 { return 0 as *NxMesh }
94 if n_tris <= 0 { return 0 as *NxMesh }
95
96 let m: *NxMesh = sys_mmap(NX_MESH_BYTES) as *NxMesh
97 m.n_verts = n_verts
98 m.n_tris = n_tris
99 m.verts = sys_mmap(n_verts * NX_MESH_VTX_STRIDE * 8) as *i64
100 m.indices = sys_mmap(n_tris * NX_MESH_IDX_STRIDE * 8) as *i64
101 if with_uvs == 1 {
102 m.uvs = sys_mmap(n_verts * NX_MESH_UV_STRIDE * 8) as *i64
103 m.has_uvs = 1
104 }
105 if with_uvs == 0 {
106 m.uvs = 0 as *i64
107 m.has_uvs = 0
108 }
109 return m
110}
111
112// ===== Setters ====================================================
113
114func nx_mesh_set_vertex(m: *NxMesh, idx: nx_int,
115 x_q14: i64, y_q14: i64, z_q14: i64, color: i64) -> nx_int {
116 if idx < 0 { return NX_MESH_ERR_OUT_OF_RANGE }
117 if idx >= m.n_verts { return NX_MESH_ERR_OUT_OF_RANGE }
118 let base: nx_int = idx * NX_MESH_VTX_STRIDE
119 m.verts[base + 0] = x_q14
120 m.verts[base + 1] = y_q14
121 m.verts[base + 2] = z_q14
122 m.verts[base + 3] = color
123 return NX_MESH_OK
124}
125
126func nx_mesh_set_uv(m: *NxMesh, idx: nx_int, u_q10: i64, v_q10: i64) -> nx_int {
127 if m.has_uvs != 1 { return NX_MESH_ERR_NULL_BUFFER }
128 if idx < 0 { return NX_MESH_ERR_OUT_OF_RANGE }
129 if idx >= m.n_verts { return NX_MESH_ERR_OUT_OF_RANGE }
130 let base: nx_int = idx * NX_MESH_UV_STRIDE
131 m.uvs[base + 0] = u_q10
132 m.uvs[base + 1] = v_q10
133 return NX_MESH_OK
134}
135
136func nx_mesh_set_triangle(m: *NxMesh, tri: nx_int,
137 i0: i64, i1: i64, i2: i64) -> nx_int {
138 if tri < 0 { return NX_MESH_ERR_OUT_OF_RANGE }
139 if tri >= m.n_tris { return NX_MESH_ERR_OUT_OF_RANGE }
140 let base: nx_int = tri * NX_MESH_IDX_STRIDE
141 m.indices[base + 0] = i0
142 m.indices[base + 1] = i1
143 m.indices[base + 2] = i2
144 return NX_MESH_OK
145}
146
147// ===== Getters ====================================================
148
149func nx_mesh_get_vertex_x(m: *NxMesh, idx: nx_int) -> i64 {
150 return m.verts[idx * NX_MESH_VTX_STRIDE + 0]
151}
152func nx_mesh_get_vertex_y(m: *NxMesh, idx: nx_int) -> i64 {
153 return m.verts[idx * NX_MESH_VTX_STRIDE + 1]
154}
155func nx_mesh_get_vertex_z(m: *NxMesh, idx: nx_int) -> i64 {
156 return m.verts[idx * NX_MESH_VTX_STRIDE + 2]
157}
158func nx_mesh_get_color(m: *NxMesh, idx: nx_int) -> i64 {
159 return m.verts[idx * NX_MESH_VTX_STRIDE + 3]
160}
161
162// ===== Validate ===================================================
163//
164// Checks that every triangle index is in [0, n_verts). Detects
165// out-of-range indices before they hit the rasterizer.
166
167func nx_mesh_validate(m: *NxMesh) -> nx_int {
168 if m.n_verts <= 0 { return NX_MESH_ERR_BAD_DIMS }
169 if m.n_tris <= 0 { return NX_MESH_ERR_BAD_DIMS }
170 if (m.verts as i64) == 0 { return NX_MESH_ERR_NULL_BUFFER }
171 if (m.indices as i64) == 0 { return NX_MESH_ERR_NULL_BUFFER }
172 if m.has_uvs == 1 {
173 if (m.uvs as i64) == 0 { return NX_MESH_ERR_NULL_BUFFER }
174 }
175 var t: nx_int = 0
176 var iter: nx_int = 0
177 var verdict: nx_int = NX_LOOP_RUNNING
178 let BUDGET: nx_int = m.n_tris
179 while verdict == NX_LOOP_RUNNING && iter < BUDGET {
180 let i0: i64 = m.indices[t * NX_MESH_IDX_STRIDE + 0]
181 let i1: i64 = m.indices[t * NX_MESH_IDX_STRIDE + 1]
182 let i2: i64 = m.indices[t * NX_MESH_IDX_STRIDE + 2]
183 if i0 < 0 { verdict = NX_LOOP_ABORTED }
184 if i1 < 0 { verdict = NX_LOOP_ABORTED }
185 if i2 < 0 { verdict = NX_LOOP_ABORTED }
186 if i0 >= m.n_verts { verdict = NX_LOOP_ABORTED }
187 if i1 >= m.n_verts { verdict = NX_LOOP_ABORTED }
188 if i2 >= m.n_verts { verdict = NX_LOOP_ABORTED }
189 t = t + 1
190 iter = iter + 1
191 }
192 if verdict == NX_LOOP_ABORTED { return NX_MESH_ERR_OUT_OF_RANGE }
193 return NX_MESH_OK
194}
195
196// ===== Factory: textured quad ====================================
197//
198// Builds a 4-vertex / 2-triangle quad in the XY plane at z = 0,
199// extending from (x_min, y_min) to (x_max, y_max) with full UV
200// coverage [0..1] across the quad. Caller provides Q14 corner
201// coords + a packed color.
202
203func nx_mesh_make_quad(x_min_q14: i64, y_min_q14: i64,
204 x_max_q14: i64, y_max_q14: i64,
205 color: i64) -> *NxMesh {
206 let m: *NxMesh = nx_mesh_alloc(4, 2, 1)
207 if (m as i64) == 0 { return m }
208
209 // Vertices: lower-left, lower-right, upper-right, upper-left.
210 nx_mesh_set_vertex(m, 0, x_min_q14, y_min_q14, 0, color)
211 nx_mesh_set_vertex(m, 1, x_max_q14, y_min_q14, 0, color)
212 nx_mesh_set_vertex(m, 2, x_max_q14, y_max_q14, 0, color)
213 nx_mesh_set_vertex(m, 3, x_min_q14, y_max_q14, 0, color)
214 // UVs: full [0..1024] coverage in Q10.
215 nx_mesh_set_uv(m, 0, 0, 0)
216 nx_mesh_set_uv(m, 1, 1024, 0)
217 nx_mesh_set_uv(m, 2, 1024, 1024)
218 nx_mesh_set_uv(m, 3, 0, 1024)
219 // Two triangles forming the quad (counter-clockwise).
220 nx_mesh_set_triangle(m, 0, 0, 1, 2)
221 nx_mesh_set_triangle(m, 1, 0, 2, 3)
222 return m
223}
224
225// ===== Factory: cube =============================================
226//
227// Builds an 8-vertex / 12-triangle (6 faces * 2 tris) axis-aligned
228// cube centred at the origin, half-extent = `half_q14`. All faces
229// the same color; per-face UVs map to full texture.
230
231func nx_mesh_make_cube(half_q14: i64, color: i64) -> *NxMesh {
232 let m: *NxMesh = nx_mesh_alloc(8, 12, 1)
233 if (m as i64) == 0 { return m }
234
235 let h: i64 = half_q14
236 let nh: i64 = 0 - half_q14
237 // 8 corners.
238 nx_mesh_set_vertex(m, 0, nh, nh, nh, color)
239 nx_mesh_set_vertex(m, 1, h, nh, nh, color)
240 nx_mesh_set_vertex(m, 2, h, h, nh, color)
241 nx_mesh_set_vertex(m, 3, nh, h, nh, color)
242 nx_mesh_set_vertex(m, 4, nh, nh, h, color)
243 nx_mesh_set_vertex(m, 5, h, nh, h, color)
244 nx_mesh_set_vertex(m, 6, h, h, h, color)
245 nx_mesh_set_vertex(m, 7, nh, h, h, color)
246 // Vertex UVs are placeholder corners; per-face UVs need vertex
247 // duplication which v1 doesn't do. Caller can rebind for
248 // texture-mapped cubes.
249 nx_mesh_set_uv(m, 0, 0, 0)
250 nx_mesh_set_uv(m, 1, 1024, 0)
251 nx_mesh_set_uv(m, 2, 1024, 1024)
252 nx_mesh_set_uv(m, 3, 0, 1024)
253 nx_mesh_set_uv(m, 4, 0, 0)
254 nx_mesh_set_uv(m, 5, 1024, 0)
255 nx_mesh_set_uv(m, 6, 1024, 1024)
256 nx_mesh_set_uv(m, 7, 0, 1024)
257 // 12 triangles (6 faces * 2), counter-clockwise outward winding.
258 // Front face (z = -h, looking toward +z): 0, 1, 2 + 0, 2, 3
259 nx_mesh_set_triangle(m, 0, 0, 1, 2)
260 nx_mesh_set_triangle(m, 1, 0, 2, 3)
261 // Back face (z = +h): 5, 4, 7 + 5, 7, 6
262 nx_mesh_set_triangle(m, 2, 5, 4, 7)
263 nx_mesh_set_triangle(m, 3, 5, 7, 6)
264 // Left face (x = -h): 4, 0, 3 + 4, 3, 7
265 nx_mesh_set_triangle(m, 4, 4, 0, 3)
266 nx_mesh_set_triangle(m, 5, 4, 3, 7)
267 // Right face (x = +h): 1, 5, 6 + 1, 6, 2
268 nx_mesh_set_triangle(m, 6, 1, 5, 6)
269 nx_mesh_set_triangle(m, 7, 1, 6, 2)
270 // Bottom face (y = -h): 4, 5, 1 + 4, 1, 0
271 nx_mesh_set_triangle(m, 8, 4, 5, 1)
272 nx_mesh_set_triangle(m, 9, 4, 1, 0)
273 // Top face (y = +h): 3, 2, 6 + 3, 6, 7
274 nx_mesh_set_triangle(m, 10, 3, 2, 6)
275 nx_mesh_set_triangle(m, 11, 3, 6, 7)
276 return m
277}
278
279// ===== Self-test ==================================================
280//
281// Closed-form invariants:
282// (a) Alloc with valid n_verts/n_tris returns non-null.
283// (b) Alloc with n_verts <= 0 returns null.
284// (c) Set + get vertex round-trips.
285// (d) Set triangle index out-of-range -> ERR_OUT_OF_RANGE.
286// (e) Validate accepts a quad factory mesh.
287// (f) Validate rejects a hand-poisoned mesh with index >= n_verts.
288// (g) Cube has 8 verts, 12 triangles.
289// (h) Verdict gate.
290
291func main() -> i64 {
292 // --- (a) Alloc ---
293 let m: *NxMesh = nx_mesh_alloc(4, 2, 1)
294 if (m as i64) == 0 { return 10 }
295 if m.n_verts != 4 { return 11 }
296 if m.n_tris != 2 { return 12 }
297 if m.has_uvs != 1 { return 13 }
298
299 // --- (b) Bad alloc ---
300 let bad: *NxMesh = nx_mesh_alloc(0, 2, 1)
301 if (bad as i64) != 0 { return 20 }
302
303 // --- (c) Set + get round-trip ---
304 nx_mesh_set_vertex(m, 0, 100, 200, 300, 555)
305 if nx_mesh_get_vertex_x(m, 0) != 100 { return 30 }
306 if nx_mesh_get_vertex_y(m, 0) != 200 { return 31 }
307 if nx_mesh_get_vertex_z(m, 0) != 300 { return 32 }
308 if nx_mesh_get_color(m, 0) != 555 { return 33 }
309
310 // --- (d) Out-of-range triangle ---
311 let v_oob: nx_int = nx_mesh_set_triangle(m, 5, 0, 1, 2)
312 if v_oob != NX_MESH_ERR_OUT_OF_RANGE { return 40 }
313
314 // --- (e) Quad factory + validate ---
315 let quad: *NxMesh = nx_mesh_make_quad(0, 0, 16384, 16384, 100)
316 if nx_mesh_validate(quad) != NX_MESH_OK { return 50 }
317
318 // --- (f) Poisoned index detection ---
319 quad.indices[0] = 99 // out of bounds for 4-vertex mesh
320 if nx_mesh_validate(quad) != NX_MESH_ERR_OUT_OF_RANGE { return 60 }
321
322 // --- (g) Cube factory ---
323 let cube: *NxMesh = nx_mesh_make_cube(8192, 200)
324 if cube.n_verts != 8 { return 70 }
325 if cube.n_tris != 12 { return 71 }
326 if nx_mesh_validate(cube) != NX_MESH_OK { return 72 }
327
328 // --- (h) Verdict gate ---
329 var vi: nx_int = 0
330 while vi < NX_MESH_N_VERDICTS {
331 if nx_mesh_verdict_is_valid(vi) != 1 { return 80 + vi }
332 vi = vi + 1
333 }
334
335 return 0
336}