qualia_core_db/domains/geospatial/
streaming.rs1use std::collections::{HashMap, HashSet};
2
3#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct CameraPose {
6 pub x: f64,
7 pub y: f64,
8 pub z: f64,
9 pub yaw: f64,
10 pub pitch: f64,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct StreamBudget {
16 pub max_tiles: usize,
17 pub timestamp: u64,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct StreamPlan {
23 pub required_tiles: Vec<u64>,
25 pub evict_tiles: Vec<u64>,
27}
28
29pub struct SceneStreamingPlanner {
31 pub pyramid: TilePyramid,
32 pub predictor: FrustumPredictor,
33 pub eviction: VramBudgetEviction,
34 pub active_lod: u8,
36}
37
38impl SceneStreamingPlanner {
39 pub fn new(
40 pyramid: TilePyramid,
41 predictor: FrustumPredictor,
42 max_tiles: usize,
43 active_lod: u8,
44 ) -> Self {
45 Self {
46 pyramid,
47 predictor,
48 eviction: VramBudgetEviction::new(max_tiles),
49 active_lod,
50 }
51 }
52
53 pub fn plan_stream(&mut self, camera_pose: &CameraPose, budget: &StreamBudget) -> StreamPlan {
55 self.eviction.max_tiles = budget.max_tiles;
56
57 let predicted = self.predictor.predict_intersecting_tiles(
58 camera_pose.x,
59 camera_pose.y,
60 camera_pose.z,
61 camera_pose.yaw,
62 camera_pose.pitch,
63 );
64
65 let mut required_tiles = Vec::new();
66 for tile_id in predicted {
67 if !self.pyramid.has_tile(self.active_lod, tile_id) {
68 continue;
69 }
70 if !self.eviction.active_tiles.contains_key(&tile_id) {
71 required_tiles.push(tile_id);
72 }
73 self.eviction.access_tile(tile_id, budget.timestamp);
74 }
75
76 required_tiles.sort_unstable();
77 required_tiles.dedup();
78
79 let mut evict_tiles = self.eviction.enforce_budget();
80 evict_tiles.sort_unstable();
81
82 StreamPlan {
83 required_tiles,
84 evict_tiles,
85 }
86 }
87}
88
89pub struct FrustumPredictor {
92 pub fov_degrees: f64,
93 pub aspect_ratio: f64,
94 pub near_clip: f64,
95 pub far_clip: f64,
96}
97
98impl FrustumPredictor {
99 pub fn new(fov: f64, aspect: f64, near: f64, far: f64) -> Self {
100 Self {
101 fov_degrees: fov,
102 aspect_ratio: aspect,
103 near_clip: near,
104 far_clip: far,
105 }
106 }
107
108 pub fn predict_intersecting_tiles(
110 &self,
111 camera_x: f64,
112 camera_y: f64,
113 camera_z: f64,
114 _yaw: f64,
115 _pitch: f64,
116 ) -> Vec<u64> {
117 let center_tile = ((camera_x / 100.0).floor() as u64)
120 ^ ((camera_y / 100.0).floor() as u64)
121 ^ ((camera_z / 100.0).floor() as u64);
122 vec![
123 center_tile,
124 center_tile.wrapping_add(1),
125 center_tile.wrapping_sub(1),
126 ]
127 }
128}
129
130pub struct VramBudgetEviction {
132 pub max_tiles: usize,
133 pub active_tiles: HashMap<u64, u64>, }
135
136impl VramBudgetEviction {
137 pub fn new(max_tiles: usize) -> Self {
138 Self {
139 max_tiles,
140 active_tiles: HashMap::new(),
141 }
142 }
143
144 pub fn access_tile(&mut self, tile_id: u64, timestamp: u64) {
145 self.active_tiles.insert(tile_id, timestamp);
146 }
147
148 pub fn enforce_budget(&mut self) -> Vec<u64> {
150 if self.active_tiles.len() <= self.max_tiles {
151 return vec![];
152 }
153
154 let mut sorted: Vec<_> = self.active_tiles.iter().collect();
155 sorted.sort_by_key(|&(_, ts)| *ts);
157
158 let to_evict_count = self.active_tiles.len() - self.max_tiles;
159 let mut evicted = Vec::with_capacity(to_evict_count);
160
161 for (id, _) in sorted.into_iter().take(to_evict_count) {
162 evicted.push(*id);
163 }
164
165 for id in &evicted {
166 self.active_tiles.remove(id);
167 }
168
169 evicted
170 }
171}
172
173pub struct TilePyramid {
175 pub layers: HashMap<u8, HashSet<u64>>, }
177
178impl TilePyramid {
179 pub fn new() -> Self {
180 Self {
181 layers: HashMap::new(),
182 }
183 }
184
185 pub fn register_tile(&mut self, lod: u8, tile_id: u64) {
186 self.layers.entry(lod).or_default().insert(tile_id);
187 }
188
189 pub fn has_tile(&self, lod: u8, tile_id: u64) -> bool {
190 self.layers
191 .get(&lod)
192 .map_or(false, |s| s.contains(&tile_id))
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 fn tile_id_at(x: f64, y: f64, z: f64) -> u64 {
201 ((x / 100.0).floor() as u64) ^ ((y / 100.0).floor() as u64) ^ ((z / 100.0).floor() as u64)
202 }
203
204 #[test]
205 fn vram_eviction_drops_oldest_first() {
206 let mut evict = VramBudgetEviction::new(2);
207 evict.access_tile(10, 1);
208 evict.access_tile(20, 2);
209 evict.access_tile(30, 3);
210 let dropped = evict.enforce_budget();
211 assert_eq!(dropped, vec![10]);
212 assert!(!evict.active_tiles.contains_key(&10));
213 }
214
215 #[test]
216 fn scene_streaming_planner_requests_missing_tiles() {
217 let mut pyramid = TilePyramid::new();
218 let pose = CameraPose {
219 x: 150.0,
220 y: 250.0,
221 z: 50.0,
222 yaw: 0.0,
223 pitch: 0.0,
224 };
225 let center = tile_id_at(pose.x, pose.y, pose.z);
226 pyramid.register_tile(0, center);
227 pyramid.register_tile(0, center.wrapping_add(1));
228
229 let mut planner = SceneStreamingPlanner::new(
230 pyramid,
231 FrustumPredictor::new(60.0, 16.0 / 9.0, 0.1, 1000.0),
232 8,
233 0,
234 );
235
236 let plan = planner.plan_stream(
237 &pose,
238 &StreamBudget {
239 max_tiles: 8,
240 timestamp: 100,
241 },
242 );
243
244 assert!(plan.required_tiles.contains(¢er));
245 assert!(plan.evict_tiles.is_empty());
246 }
247
248 #[test]
249 fn scene_streaming_planner_evicts_when_over_budget() {
250 let mut pyramid = TilePyramid::new();
251 let pose = CameraPose {
252 x: 100.0,
253 y: 200.0,
254 z: 0.0,
255 yaw: 0.0,
256 pitch: 0.0,
257 };
258 let center = tile_id_at(pose.x, pose.y, pose.z);
259 pyramid.register_tile(0, center);
260 pyramid.register_tile(0, center.wrapping_add(1));
261 pyramid.register_tile(0, center.wrapping_sub(1));
262
263 let mut planner =
264 SceneStreamingPlanner::new(pyramid, FrustumPredictor::new(90.0, 1.0, 0.1, 500.0), 2, 0);
265
266 planner.eviction.access_tile(999, 1);
268 planner.eviction.access_tile(888, 2);
269
270 let plan = planner.plan_stream(
271 &pose,
272 &StreamBudget {
273 max_tiles: 2,
274 timestamp: 50,
275 },
276 );
277
278 assert!(!plan.required_tiles.is_empty());
279 assert!(!plan.evict_tiles.is_empty());
280 assert!(plan.evict_tiles.contains(&999));
281 }
282
283 #[test]
284 fn scene_streaming_planner_skips_unregistered_tiles() {
285 let pyramid = TilePyramid::new();
286 let mut planner = SceneStreamingPlanner::new(
287 pyramid,
288 FrustumPredictor::new(60.0, 16.0 / 9.0, 0.1, 1000.0),
289 4,
290 0,
291 );
292 let plan = planner.plan_stream(
293 &CameraPose {
294 x: 0.0,
295 y: 0.0,
296 z: 0.0,
297 yaw: 0.0,
298 pitch: 0.0,
299 },
300 &StreamBudget {
301 max_tiles: 4,
302 timestamp: 1,
303 },
304 );
305 assert!(plan.required_tiles.is_empty());
306 }
307}