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}