code wiki / (root) / nx_mesh.nx

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}