code wiki / (root) / nx_voxgrid.nx

nx_voxgrid.nx source

↩ module page · 271 lines · 10313 B

1// nx_tissue.nx -- 3D voxel grid storage with palette packing. 2// 3// Biology naming per [[feedback-naming-discipline-no-industry- 4// competitor-overlap]]: a tissue is a 3D mass of cells with shared 5// structure and function. Composes naturally with existing biology 6// primitives (cells form tissue, tissue holds the 3D structure of an 7// organ). Avoids: 8// - "chunk" (Minecraft trademark proximity, Java HashMap conflation) 9// - "lattice" (already taken by nx_lattice.nx -- 2D Minkowski math) 10// - "voxel_grid" (too descriptive of industry term) 11// - "volume" (generic infra term) 12// 13// THIS IS THE OTHER UNLOCK PRIMITIVE for Minecraft-equivalent on $5 14// ESP32 class hardware. Pair with nx_palette (already shipped) and 15// a 32x32x32 (32768 voxel) tissue at 3 bits per voxel fits in 12288 16// bytes. Six tissues arrayed in a 3x2x1 grid form a walkable scene 17// in 72 KiB -- comfortably within ESP32 SRAM (520 KiB total). 18// 19// Composes: 20// nx_palette -- the sub-byte packing layer (nx_palette_pack / 21// nx_palette_unpack); tissue wraps the (x,y,z) -> 22// linear-index addressing on top. 23// nx_budget -- tissue memory consumption is a RAM budget item; 24// caller registers nx_pal_packed_bytes() with budget 25// nx_tier -- bpp scales with tier (3bpp at MCU, 4bpp at family, 26// 8bpp at workstation+ for cache friendliness) 27// nx_pathway -- a tissue lives in a cell; cell budget covers the 28// tissue's packed byte count 29// 30// V1 ships cubic + cuboid tissues with palette indices. Real-time 31// streaming from flash to RAM (chunk paging) is queued; today the 32// caller materializes the whole tissue in RAM. 33// 34// Gap list (V1 honest perf verdict): 35// - no flash-paging (whole tissue in RAM) 36// - no RLE compression atop palette pack (each voxel costs bpp bits) 37// - no greedy-meshing helper (caller's geometry layer) 38// - no neighbor-iteration helper (queued) 39// - no spatial hash (32^3 = 32K voxels still fits direct addressing) 40// 41// genealogy_id: biology_tissue + nx_palette + classic_voxel_chunk_storage 42// lineage_id: substrate_tissue_v1 43// 44// nx_safety_envelope: 45// intended_use: "3D voxel grid with palette-packed storage 46// for memory-constrained worlds (MCU class)" 47// sil_target: SIL1 48// evidence: [coord_bounds_validated, palette_bpp_inherited, 49// index_arithmetic_no_overflow_for_32_cube] 50// verdict: NOT_YET_EVALUATED 51 52import "nx_syscalls.nx" 53import "nx_tier.nx" 54import "nx_palette.nx" 55 56// ===== Sealed enum: NxTissueVerdict =============================== 57 58const NX_TIS_OK: nx_int = 0 59const NX_TIS_ERR_BAD_BPP: nx_int = 1 60const NX_TIS_ERR_BAD_COORD: nx_int = 2 61const NX_TIS_ERR_BAD_INDEX: nx_int = 3 62const NX_TIS_ERR_BAD_DIM: nx_int = 4 63 64// ===== Struct: NxTissue =========================================== 65// 66// dim_* is each axis voxel count. bpp is the palette bits-per-voxel. 67// packed points to a contiguous nx_pal_packed_bytes()-sized buffer. 68// n_voxels is the cached dim_x * dim_y * dim_z product. 69 70struct NxTissue { 71 dim_x: nx_size, 72 dim_y: nx_size, 73 dim_z: nx_size, 74 bpp: nx_int, 75 n_voxels: nx_size, 76 packed: *u8, 77} 78 79// ===== nx_tissue_new ============================================== 80// 81// Allocate + zero a tissue of given dims at given bpp. dims must be 82// at least 1 along each axis. bpp must be one of NX_BPP_1/2/3/4/8. 83 84func nx_tissue_new(dim_x: nx_size, 85 dim_y: nx_size, 86 dim_z: nx_size, 87 bpp: nx_int) -> *NxTissue { 88 if dim_x <= 0 { return (0 as i64) as *NxTissue } 89 if dim_y <= 0 { return (0 as i64) as *NxTissue } 90 if dim_z <= 0 { return (0 as i64) as *NxTissue } 91 if nx_pal_bpp_is_valid(bpp) == 0 { return (0 as i64) as *NxTissue } 92 let t: *NxTissue = (sys_mmap(48)) as *NxTissue 93 t.dim_x = dim_x 94 t.dim_y = dim_y 95 t.dim_z = dim_z 96 t.bpp = bpp 97 t.n_voxels = dim_x * dim_y * dim_z 98 let packed_bytes: nx_size = nx_pal_packed_bytes(t.n_voxels, bpp) 99 t.packed = (sys_mmap(packed_bytes)) as *u8 100 var i: nx_size = 0 101 while i < packed_bytes { 102 t.packed[i] = 0 103 i = i + 1 104 } 105 return t 106} 107 108// ===== nx_tissue_storage_bytes ==================================== 109 110func nx_tissue_storage_bytes(t: *NxTissue) -> nx_size { 111 return nx_pal_packed_bytes(t.n_voxels, t.bpp) 112} 113 114// ===== nx_tissue_in_bounds ======================================== 115 116func nx_tissue_in_bounds(t: *NxTissue, 117 x: nx_size, y: nx_size, z: nx_size) -> nx_int { 118 if x >= t.dim_x { return 0 } 119 if y >= t.dim_y { return 0 } 120 if z >= t.dim_z { return 0 } 121 return 1 122} 123 124// ===== _tissue_linear_index ======================================= 125// 126// Row-major: x varies fastest, then y, then z. Matches the linear 127// order nx_palette_pack expects, so a single byte-blob is correct 128// for both pack and unpack. 129 130func _tissue_linear_index(t: *NxTissue, 131 x: nx_size, y: nx_size, z: nx_size) -> nx_size { 132 return (z * t.dim_y + y) * t.dim_x + x 133} 134 135// ===== _tissue_unpack_one ========================================= 136// 137// Reads exactly one palette index from packed[] at the given linear 138// offset. Same bit-mechanics as nx_palette_unpack but for a single 139// voxel; cheaper than unpacking a whole region when caller only 140// needs one cell. 141 142func _tissue_unpack_one(packed: *u8, linear: nx_size, bpp: nx_int) -> nx_int { 143 let bpp_s: nx_size = bpp as nx_size 144 let bit_off: nx_size = linear * bpp_s 145 let byte_off: nx_size = bit_off / 8 146 let bit_in_byte: nx_size = bit_off - (byte_off * 8) 147 let low_room: nx_size = 8 - bit_in_byte 148 let mask: nx_int = (1 << bpp) - 1 149 if bpp_s <= low_room { 150 let b: nx_int = (packed[byte_off] as i64) & 255 151 return (b >> bit_in_byte) & mask 152 } 153 let spill: nx_size = bpp_s - low_room 154 let b0: nx_int = (packed[byte_off] as i64) & 255 155 let b1: nx_int = (packed[byte_off + 1] as i64) & 255 156 let low_mask: nx_int = (1 << low_room) - 1 157 let low_part: nx_int = (b0 >> bit_in_byte) & low_mask 158 let high_part: nx_int = b1 & ((1 << spill) - 1) 159 return low_part | (high_part << low_room) 160} 161 162// ===== _tissue_pack_one =========================================== 163// 164// Writes exactly one palette index at the given linear offset. 165// Read-modify-write the byte(s) involved -- standard sub-byte pack 166// idiom. Clears the destination bit-field FIRST so over-writes 167// (e.g., changing voxel from index 7 to index 1) don't OR-leak. 168 169func _tissue_pack_one(packed: *u8, linear: nx_size, bpp: nx_int, idx: nx_int) -> nx_int { 170 let bpp_s: nx_size = bpp as nx_size 171 let bit_off: nx_size = linear * bpp_s 172 let byte_off: nx_size = bit_off / 8 173 let bit_in_byte: nx_size = bit_off - (byte_off * 8) 174 let low_room: nx_size = 8 - bit_in_byte 175 if bpp_s <= low_room { 176 let cur: nx_int = (packed[byte_off] as i64) & 255 177 let field_mask: nx_int = (((1 << bpp) - 1) << bit_in_byte) & 255 178 let inv: nx_int = (255 ^ field_mask) & 255 179 let cleared: nx_int = cur & inv 180 let placed: nx_int = (idx & ((1 << bpp) - 1)) << bit_in_byte 181 packed[byte_off] = (cleared | placed) as u8 182 return 0 183 } 184 let spill: nx_size = bpp_s - low_room 185 let low_mask: nx_int = (1 << low_room) - 1 186 let low_part: nx_int = idx & low_mask 187 let high_part: nx_int = (idx >> low_room) & ((1 << spill) - 1) 188 189 let cur0: nx_int = (packed[byte_off] as i64) & 255 190 let field0: nx_int = (low_mask << bit_in_byte) & 255 191 let inv0: nx_int = (255 ^ field0) & 255 192 packed[byte_off] = ((cur0 & inv0) | (low_part << bit_in_byte)) as u8 193 194 let cur1: nx_int = (packed[byte_off + 1] as i64) & 255 195 let field1: nx_int = (1 << spill) - 1 196 let inv1: nx_int = (255 ^ field1) & 255 197 packed[byte_off + 1] = ((cur1 & inv1) | high_part) as u8 198 return 0 199} 200 201// ===== nx_tissue_set ============================================== 202 203func nx_tissue_set(t: *NxTissue, 204 x: nx_size, y: nx_size, z: nx_size, 205 idx: nx_int) -> nx_int { 206 if nx_tissue_in_bounds(t, x, y, z) == 0 { return NX_TIS_ERR_BAD_COORD } 207 if idx < 0 { return NX_TIS_ERR_BAD_INDEX } 208 if idx > nx_pal_max_index(t.bpp) { return NX_TIS_ERR_BAD_INDEX } 209 let lin: nx_size = _tissue_linear_index(t, x, y, z) 210 _tissue_pack_one(t.packed, lin, t.bpp, idx) 211 return NX_TIS_OK 212} 213 214// ===== nx_tissue_get ============================================== 215// 216// Returns -1 on out-of-bounds (caller may treat as "air"). Otherwise 217// returns the palette index at (x,y,z). 218 219func nx_tissue_get(t: *NxTissue, 220 x: nx_size, y: nx_size, z: nx_size) -> nx_int { 221 if nx_tissue_in_bounds(t, x, y, z) == 0 { return -1 } 222 let lin: nx_size = _tissue_linear_index(t, x, y, z) 223 return _tissue_unpack_one(t.packed, lin, t.bpp) 224} 225 226// ===== nx_tissue_count_nonzero ==================================== 227// 228// How many voxels are non-zero (palette index 0 conventionally = air). 229// Used by raycast layers to early-skip empty tissues. 230 231func nx_tissue_count_nonzero(t: *NxTissue) -> nx_size { 232 var hits: nx_size = 0 233 var z: nx_size = 0 234 while z < t.dim_z { 235 var y: nx_size = 0 236 while y < t.dim_y { 237 var x: nx_size = 0 238 while x < t.dim_x { 239 if nx_tissue_get(t, x, y, z) > 0 { hits = hits + 1 } 240 x = x + 1 241 } 242 y = y + 1 243 } 244 z = z + 1 245 } 246 return hits 247} 248 249// ===== nx_tissue_fill ============================================= 250// 251// Bulk-fill all voxels with a single palette index. Convenient for 252// world generation step 1 (fill with "stone" then carve). 253 254func nx_tissue_fill(t: *NxTissue, idx: nx_int) -> nx_int { 255 if idx < 0 { return NX_TIS_ERR_BAD_INDEX } 256 if idx > nx_pal_max_index(t.bpp) { return NX_TIS_ERR_BAD_INDEX } 257 var z: nx_size = 0 258 while z < t.dim_z { 259 var y: nx_size = 0 260 while y < t.dim_y { 261 var x: nx_size = 0 262 while x < t.dim_x { 263 nx_tissue_set(t, x, y, z, idx) 264 x = x + 1 265 } 266 y = y + 1 267 } 268 z = z + 1 269 } 270 return NX_TIS_OK 271}