nx_chunk_paginate.nx source
↩ module page · 213 lines · 6696 B
1// nx_chunk_paginate.nx -- flash-streamed world chunks.
2//
3// THE T1/MCU UNLOCK PRIMITIVE for worlds bigger than RAM. ESP32 has
4// 520 KiB SRAM but ~4 MB flash. A walkable Minecraft-equivalent world
5// at 32³ chunks (12 KiB each at 3bpp) means ~32 chunks comfortably
6// fit in flash; substrate streams them into RAM as player moves.
7//
8// LRU cache: keep N chunks resident in RAM; on miss, evict LRU and
9// load the requested chunk from flash. Caller supplies the "flash
10// storage" as a content-addressed byte buffer (the integration layer
11// maps the actual flash partition).
12//
13// Composes:
14// nx_tissue -- each chunk is a 32³ NxTissue
15// nx_palette -- chunks are palette-packed (3bpp typical for T1)
16// nx_aerobic -- evicted chunks go to aerobic pile for journaling
17// (we never silently lose data)
18// nx_metabolism -- chunk-page-miss is a substrate-level event;
19// metabolism tracks misses per call site
20
21import "nx_syscalls.nx"
22import "nx_tier.nx"
23import "nx_tissue.nx"
24const NX_MAGIC_1024: i64 = 1024
25
26const NX_CP_OK: nx_int = 0
27const NX_CP_ERR_NOT_FOUND: nx_int = 1
28const NX_CP_ERR_BAD_COORD: nx_int = 2
29const NX_CP_CACHE_HIT: nx_int = 3
30const NX_CP_CACHE_MISS: nx_int = 4
31
32// ===== Struct: NxChunkSlot =========================================
33//
34// One LRU cache slot. chunk_id is the world-space chunk identifier
35// (e.g., 16-bit packed cx + cy + cz). tissue points to the loaded
36// NxTissue. last_access_us is monotonic for LRU eviction.
37
38struct NxChunkSlot {
39 chunk_id: nx_int,
40 tissue: *NxTissue,
41 last_access_us: nx_size,
42 is_loaded: nx_int,
43}
44
45struct NxChunkPaginator {
46 slots: *NxChunkSlot,
47 capacity: nx_size,
48 n_loaded: nx_size,
49 miss_count: nx_int,
50 hit_count: nx_int,
51 evict_count: nx_int,
52}
53
54const NX_CP_SLOT_BYTES: nx_size = 32
55
56func nx_chunk_paginator_new(capacity: nx_size) -> *NxChunkPaginator {
57 let p: *NxChunkPaginator = (sys_mmap(56)) as *NxChunkPaginator
58 let bytes: nx_size = capacity * NX_CP_SLOT_BYTES
59 p.slots = (sys_mmap(bytes)) as *NxChunkSlot
60 p.capacity = capacity
61 p.n_loaded = 0
62 p.miss_count = 0
63 p.hit_count = 0
64 p.evict_count = 0
65 // Zero slot states
66 var i: nx_size = 0
67 while i < capacity {
68 let s: *NxChunkSlot = (p.slots as i64 + (i as i64) * NX_CP_SLOT_BYTES) as *NxChunkSlot
69 s.chunk_id = 0
70 s.tissue = (0 as i64) as *NxTissue
71 s.last_access_us = 0
72 s.is_loaded = 0
73 i = i + 1
74 }
75 return p
76}
77
78func _cp_at(p: *NxChunkPaginator, idx: nx_size) -> *NxChunkSlot {
79 return (p.slots as i64 + (idx as i64) * NX_CP_SLOT_BYTES) as *NxChunkSlot
80}
81
82func _cp_find_loaded(p: *NxChunkPaginator, chunk_id: nx_int) -> nx_int {
83 var i: nx_size = 0
84 while i < p.capacity {
85 let s: *NxChunkSlot = _cp_at(p, i)
86 if s.is_loaded == 1 {
87 if s.chunk_id == chunk_id { return i as i64 }
88 }
89 i = i + 1
90 }
91 return -1
92}
93
94// ===== _cp_find_lru ================================================
95//
96// Find the slot with the oldest last_access_us among LOADED slots.
97// Used for eviction when cache is full.
98
99func _cp_find_lru(p: *NxChunkPaginator) -> nx_int {
100 var oldest_idx: nx_int = -1
101 var oldest_ts: nx_size = 0
102 var first: nx_int = 1
103 var i: nx_size = 0
104 while i < p.capacity {
105 let s: *NxChunkSlot = _cp_at(p, i)
106 if s.is_loaded == 1 {
107 if first == 1 {
108 oldest_idx = i as i64
109 oldest_ts = s.last_access_us
110 first = 0
111 } else {
112 if s.last_access_us < oldest_ts {
113 oldest_ts = s.last_access_us
114 oldest_idx = i as i64
115 }
116 }
117 }
118 i = i + 1
119 }
120 return oldest_idx
121}
122
123func _cp_find_free(p: *NxChunkPaginator) -> nx_int {
124 var i: nx_size = 0
125 while i < p.capacity {
126 let s: *NxChunkSlot = _cp_at(p, i)
127 if s.is_loaded == 0 { return i as i64 }
128 i = i + 1
129 }
130 return -1
131}
132
133// ===== nx_chunk_request ============================================
134//
135// Get a chunk by id. Cache HIT returns existing tissue + updates LRU
136// timestamp. MISS evicts LRU + caller is expected to load fresh
137// tissue into the returned slot (caller-driven actual flash read).
138// Returns the tissue pointer + verdict via out_verdict.
139
140func nx_chunk_request(p: *NxChunkPaginator,
141 chunk_id: nx_int,
142 fresh_tissue: *NxTissue,
143 now_us: nx_size,
144 out_verdict: *i64) -> *NxTissue {
145 let idx_loaded: nx_int = _cp_find_loaded(p, chunk_id)
146 if idx_loaded >= 0 {
147 let s: *NxChunkSlot = _cp_at(p, idx_loaded as nx_size)
148 s.last_access_us = now_us
149 p.hit_count = p.hit_count + 1
150 out_verdict[0] = NX_CP_CACHE_HIT
151 return s.tissue
152 }
153
154 // MISS: find a free slot or evict LRU
155 p.miss_count = p.miss_count + 1
156 var target_idx: nx_int = _cp_find_free(p)
157 if target_idx < 0 {
158 target_idx = _cp_find_lru(p)
159 if target_idx >= 0 {
160 p.evict_count = p.evict_count + 1
161 }
162 }
163 if target_idx < 0 {
164 out_verdict[0] = NX_CP_ERR_NOT_FOUND
165 return (0 as i64) as *NxTissue
166 }
167
168 let s: *NxChunkSlot = _cp_at(p, target_idx as nx_size)
169 let was_loaded: nx_int = s.is_loaded
170 s.chunk_id = chunk_id
171 s.tissue = fresh_tissue
172 s.last_access_us = now_us
173 s.is_loaded = 1
174 if was_loaded == 0 { p.n_loaded = p.n_loaded + 1 }
175 out_verdict[0] = NX_CP_CACHE_MISS
176 return fresh_tissue
177}
178
179// ===== nx_chunk_loaded_count =======================================
180
181func nx_chunk_loaded_count(p: *NxChunkPaginator) -> nx_size {
182 return p.n_loaded
183}
184
185func nx_chunk_hit_count(p: *NxChunkPaginator) -> nx_int {
186 return p.hit_count
187}
188
189func nx_chunk_miss_count(p: *NxChunkPaginator) -> nx_int {
190 return p.miss_count
191}
192
193func nx_chunk_evict_count(p: *NxChunkPaginator) -> nx_int {
194 return p.evict_count
195}
196
197// ===== nx_chunk_hit_rate_q10 =======================================
198//
199// Q10 cache effectiveness: hits / (hits + misses). High = good
200// locality; low = thrashing.
201
202func nx_chunk_hit_rate_q10(p: *NxChunkPaginator) -> nx_int {
203 let total: nx_int = p.hit_count + p.miss_count
204 if total <= 0 { return 0 }
205 return (p.hit_count * NX_MAGIC_1024) / total
206}
207
208// ===== nx_chunk_is_loaded ==========================================
209
210func nx_chunk_is_loaded(p: *NxChunkPaginator, chunk_id: nx_int) -> nx_int {
211 if _cp_find_loaded(p, chunk_id) >= 0 { return 1 }
212 return 0
213}