code wiki / (root) / nx_palette.nx

nx_palette.nx source

↩ module page · 177 lines · 7003 B

1// nx_palette.nx -- sub-byte voxel/pixel palette encoder. 2// 3// THE unlock primitive for Minecraft-equivalent on $5 ESP32-class 4// hardware. At 3 bits per voxel and an 8-color palette, a full 32x32x32 5// chunk fits in 12288 bytes (12 KiB). Pair with chunk paging from 6// flash and the entire walkable world streams within the working RAM 7// of an RP2040 / Cortex-M0+ class part. This is what makes a true 8// Minecraft demo on $5 of silicon feasible -- without sub-byte packing, 9// the same chunk needs 32 KiB minimum (one byte per voxel) which 10// blows past most MCU SRAM budgets. 11// 12// Composes: 13// nx_tier -- packing density scales naturally with NX_TIER_*; 14// MCU tier uses 1-3bpp, workstation tier may stay 15// at 8bpp for cache reasons 16// nx_budget -- the saved RAM bytes flow directly into the cell's 17// ram budget headroom 18// 19// V1 supports 1bpp (2-color), 2bpp (4-color), 3bpp (8-color), 4bpp 20// (16-color), and 8bpp pass-through. Bit-level packing is LSB-first 21// within each byte so unpack is a small shift + mask, friendly to 22// embedded targets without barrel shifters. 23// 24// Gap list (V1 honest perf verdict): 25// - no RLE compression on top of packing (queued for chunk save format) 26// - no dithering helper for 1bpp displays (caller's responsibility) 27// - palette index lookup not included (caller supplies palette) 28// - no SIMD unpack on workstation tier (scalar only) 29// - no compile-time-specialized variants per bpp (one function with 30// branch; per-bpp specialization queued behind benchmark gate) 31// 32// genealogy_id: classic_video_indexed_color + minecraft_chunk_format 33// lineage_id: substrate_palette_v1 34// 35// nx_safety_envelope: 36// intended_use: "Sub-byte palette index packing/unpacking for 37// memory-constrained voxel/pixel data" 38// sil_target: SIL1 39// evidence: [bit_alignment_correct, lossless_roundtrip, 40// bpp_validated_at_entry] 41// verdict: NOT_YET_EVALUATED 42 43import "nx_syscalls.nx" 44import "nx_tier.nx" 45 46// ===== Sealed enum: NxBpp ========================================= 47 48const NX_BPP_1: nx_int = 1 49const NX_BPP_2: nx_int = 2 50const NX_BPP_3: nx_int = 3 51const NX_BPP_4: nx_int = 4 52const NX_BPP_8: nx_int = 8 53 54// ===== Sealed enum: NxPaletteVerdict ============================= 55 56const NX_PAL_OK: nx_int = 0 57const NX_PAL_ERR_BAD_BPP: nx_int = 1 58const NX_PAL_ERR_BAD_INDEX: nx_int = 2 59 60// ===== nx_pal_bpp_is_valid ======================================= 61 62func nx_pal_bpp_is_valid(bpp: nx_int) -> nx_int { 63 if bpp == NX_BPP_1 { return 1 } 64 if bpp == NX_BPP_2 { return 1 } 65 if bpp == NX_BPP_3 { return 1 } 66 if bpp == NX_BPP_4 { return 1 } 67 if bpp == NX_BPP_8 { return 1 } 68 return 0 69} 70 71// ===== nx_pal_max_index ========================================== 72// 73// Maximum palette index storable at this bpp. Caller uses this to 74// validate that the source indices fit before packing. 75 76func nx_pal_max_index(bpp: nx_int) -> nx_int { 77 if bpp == NX_BPP_1 { return 1 } 78 if bpp == NX_BPP_2 { return 3 } 79 if bpp == NX_BPP_3 { return 7 } 80 if bpp == NX_BPP_4 { return 15 } 81 if bpp == NX_BPP_8 { return 255 } 82 return 0 83} 84 85// ===== nx_pal_packed_bytes ======================================= 86// 87// Bytes needed to store n_voxels indices at bpp. Rounds up so the 88// caller's allocation always covers the trailing partial byte. 89 90func nx_pal_packed_bytes(n_voxels: nx_size, bpp: nx_int) -> nx_size { 91 let total_bits: nx_size = n_voxels * (bpp as nx_size) 92 return (total_bits + 7) / 8 93} 94 95// ===== nx_palette_pack =========================================== 96// 97// Pack n_voxels palette indices from src (one index per byte, 0..255) 98// into dst (LSB-first within each byte). Returns NX_PAL_OK on success, 99// or NX_PAL_ERR_BAD_INDEX if any src index exceeds nx_pal_max_index(bpp). 100// dst must be at least nx_pal_packed_bytes(n_voxels, bpp) in size. 101 102func nx_palette_pack(src: *u8, n_voxels: nx_size, bpp: nx_int, dst: *u8) -> nx_int { 103 if nx_pal_bpp_is_valid(bpp) == 0 { return NX_PAL_ERR_BAD_BPP } 104 let max_idx: nx_int = nx_pal_max_index(bpp) 105 106 // Zero dst so OR-in bits compose cleanly. 107 let dst_bytes: nx_size = nx_pal_packed_bytes(n_voxels, bpp) 108 var z: nx_size = 0 109 while z < dst_bytes { 110 dst[z] = 0 111 z = z + 1 112 } 113 114 let bpp_s: nx_size = bpp as nx_size 115 var i: nx_size = 0 116 while i < n_voxels { 117 let idx_val: nx_int = (src[i] as i64) & 255 118 if idx_val > max_idx { return NX_PAL_ERR_BAD_INDEX } 119 let bit_off: nx_size = i * bpp_s 120 let byte_off: nx_size = bit_off / 8 121 let bit_in_byte: nx_size = bit_off - (byte_off * 8) 122 // Low bits go into current byte; spill bits (if any) into next. 123 let low_room: nx_size = 8 - bit_in_byte 124 if bpp_s <= low_room { 125 let cur: nx_int = (dst[byte_off] as i64) & 255 126 let placed: nx_int = (idx_val & 255) << bit_in_byte 127 dst[byte_off] = (cur | placed) as u8 128 } else { 129 let spill: nx_size = bpp_s - low_room 130 let low_mask: nx_int = (1 << low_room) - 1 131 let low_part: nx_int = idx_val & low_mask 132 let high_part: nx_int = (idx_val >> low_room) & ((1 << spill) - 1) 133 let cur0: nx_int = (dst[byte_off] as i64) & 255 134 dst[byte_off] = (cur0 | (low_part << bit_in_byte)) as u8 135 let cur1: nx_int = (dst[byte_off + 1] as i64) & 255 136 dst[byte_off + 1] = (cur1 | high_part) as u8 137 } 138 i = i + 1 139 } 140 return NX_PAL_OK 141} 142 143// ===== nx_palette_unpack ========================================= 144// 145// Inverse of pack. Reads n_voxels indices from src (LSB-first) and 146// writes them one-byte-per-index into dst. Caller-allocated dst must 147// have at least n_voxels bytes. 148 149func nx_palette_unpack(src: *u8, n_voxels: nx_size, bpp: nx_int, dst: *u8) -> nx_int { 150 if nx_pal_bpp_is_valid(bpp) == 0 { return NX_PAL_ERR_BAD_BPP } 151 let bpp_s: nx_size = bpp as nx_size 152 let mask: nx_int = (1 << bpp) - 1 153 154 var i: nx_size = 0 155 while i < n_voxels { 156 let bit_off: nx_size = i * bpp_s 157 let byte_off: nx_size = bit_off / 8 158 let bit_in_byte: nx_size = bit_off - (byte_off * 8) 159 let low_room: nx_size = 8 - bit_in_byte 160 var v: nx_int = 0 161 if bpp_s <= low_room { 162 let b: nx_int = (src[byte_off] as i64) & 255 163 v = (b >> bit_in_byte) & mask 164 } else { 165 let spill: nx_size = bpp_s - low_room 166 let b0: nx_int = (src[byte_off] as i64) & 255 167 let b1: nx_int = (src[byte_off + 1] as i64) & 255 168 let low_mask: nx_int = (1 << low_room) - 1 169 let low_part: nx_int = (b0 >> bit_in_byte) & low_mask 170 let high_part: nx_int = b1 & ((1 << spill) - 1) 171 v = low_part | (high_part << low_room) 172 } 173 dst[i] = (v & 255) as u8 174 i = i + 1 175 } 176 return NX_PAL_OK 177}