rustial_engine/map_state/
tile_selection.rs1use super::*;
2
3impl MapState {
4 pub fn update_terrain(&mut self) {
6 if self.terrain.enabled() {
7 let cam_world = self.mercator_camera_world();
8 let meshes = self.terrain.update(
9 &self.viewport_bounds,
10 self.zoom_level,
11 cam_world,
12 self.camera.projection(),
13 self.camera.distance(),
14 self.camera.pitch(),
15 );
16 self.terrain_meshes = Arc::new(meshes);
17 self.hillshade_rasters =
18 Arc::new(self.terrain.visible_hillshade_rasters().to_vec());
19
20 self.request_coordinator.report_demand(
22 SourcePriority::Terrain,
23 self.terrain.pending_count(),
24 );
25 }
26 }
27
28 pub(super) fn desired_terrain_tiles(&self) -> Option<Vec<rustial_math::TileId>> {
39 let use_covering = self.terrain.enabled()
40 && self.camera.pitch() > 0.3
41 && self.camera.mode() == crate::camera::CameraMode::Perspective;
42
43 if !use_covering || self.visible_tiles.is_empty() {
44 return None;
45 }
46
47 let raster_actuals: HashSet<TileId> = self
54 .visible_tiles
55 .iter()
56 .filter(|vt| vt.data.is_some())
57 .map(|vt| vt.actual)
58 .collect();
59
60 let has_raster_ancestor = |tile: &TileId| -> bool {
61 if raster_actuals.contains(tile) {
63 return true;
64 }
65 let mut current = *tile;
67 for _ in 0..8u8 {
68 let Some(parent) = current.parent() else { break };
69 if raster_actuals.contains(&parent) {
70 return true;
71 }
72 current = parent;
73 }
74 false
75 };
76
77 let mut terrain_tiles: Vec<rustial_math::TileId> = self
78 .visible_tiles
79 .iter()
80 .map(|tile| tile.target)
81 .collect();
82
83 let strict_tiles = if let Some(view) = self.camera.flat_tile_view() {
84 visible_tiles_flat_view_with_config(
85 &self.viewport_bounds,
86 self.zoom_level,
87 &view,
88 &FlatTileSelectionConfig {
89 footprint_pitch_threshold_rad: 0.0,
90 footprint_min_tiles: 0,
91 ..FlatTileSelectionConfig::default()
92 },
93 )
94 } else {
95 visible_tiles(&self.viewport_bounds, self.zoom_level)
96 };
97
98 let mut seen = HashSet::with_capacity(terrain_tiles.len() + strict_tiles.len());
99 terrain_tiles.retain(|tile| seen.insert(*tile));
100 for tile in strict_tiles {
101 if seen.insert(tile) && has_raster_ancestor(&tile) {
102 terrain_tiles.push(tile);
103 }
104 }
105
106 if self.camera.pitch() > 0.4 {
111 let target_world = self.camera.target_world();
112 let eye = target_world + self.camera.eye_offset();
113 let eye_ground = WorldCoord::new(eye.x, eye.y, 0.0);
114
115 let altitude = (eye.z - target_world.z).abs().max(1.0);
118 let half = altitude * 1.5;
119 let near_bounds = WorldBounds::new(
120 WorldCoord::new(eye_ground.position.x - half, eye_ground.position.y - half, 0.0),
121 WorldCoord::new(eye_ground.position.x + half, eye_ground.position.y + half, 0.0),
122 );
123 for tile in visible_tiles(&near_bounds, self.zoom_level) {
124 if seen.insert(tile) && has_raster_ancestor(&tile) {
125 terrain_tiles.push(tile);
126 }
127 }
128 }
129
130 let cam = self.mercator_camera_world();
131
132 let max_base_tiles = terrain_base_tile_budget(terrain_tiles.len());
136 if terrain_tiles.len() > max_base_tiles {
137 terrain_tiles.sort_by(|a, b| {
138 let da = tile_center_dist_sq(a, cam);
139 let db = tile_center_dist_sq(b, cam);
140 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
141 });
142 terrain_tiles.truncate(max_base_tiles);
143 }
144
145 if self.camera.pitch() > 0.5 && self.zoom_level > 2 {
151 let base_tiles: Vec<TileId> = terrain_tiles.clone();
152 let is_ancestor_of_existing = |candidate: &TileId| -> bool {
153 base_tiles.iter().any(|t| {
154 if t.zoom <= candidate.zoom { return false; }
155 let dz = t.zoom - candidate.zoom;
156 (t.x >> dz) == candidate.x && (t.y >> dz) == candidate.y
157 })
158 };
159 let mut horizon_budget = terrain_horizon_tile_budget(max_base_tiles, self.camera.pitch());
160 let mut hz = self.zoom_level.saturating_sub(2);
161 while hz > 0 && horizon_budget > 0 {
162 let coarse = visible_tiles(&self.viewport_bounds, hz);
163 let mut coarse_sorted: Vec<_> = coarse
164 .into_iter()
165 .filter(|t| !seen.contains(t) && !is_ancestor_of_existing(t) && has_raster_ancestor(t))
166 .map(|t| {
167 let d = tile_center_dist_sq(&t, cam);
168 (t, d)
169 })
170 .collect();
171 coarse_sorted.sort_by(|a, b| {
173 b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
174 });
175 let take = coarse_sorted.len().min(horizon_budget);
176 for (t, _) in coarse_sorted.into_iter().take(take) {
177 if seen.insert(t) {
178 terrain_tiles.push(t);
179 horizon_budget -= 1;
180 }
181 }
182 hz = hz.saturating_sub(2);
183 }
184 }
185
186 Some(terrain_tiles)
187 }
188
189 pub(super) fn update_terrain_with_tiles(&mut self, tiles: &[rustial_math::TileId]) {
194 if self.terrain.enabled() {
195 let meshes = self.terrain.update_with_tiles(
196 tiles,
197 self.zoom_level,
198 self.camera.projection(),
199 );
200 self.terrain_meshes = Arc::new(meshes);
201 self.hillshade_rasters =
202 Arc::new(self.terrain.visible_hillshade_rasters().to_vec());
203
204 self.request_coordinator.report_demand(
206 SourcePriority::Terrain,
207 self.terrain.pending_count(),
208 );
209 }
210 }
211
212 pub(super) fn update_tile_layers(&mut self) {
220 use crate::layer::LayerKind;
221 use crate::layers::TileLayer;
222
223 self.request_coordinator.begin_frame();
226
227 let zoom_level = self.zoom_level;
228 let camera_world = self.mercator_camera_world();
229 let flat_view = self.camera.flat_tile_view();
230 let camera_distance = self.camera.distance();
231 let viewport_bounds = self.viewport_bounds.clone();
232 let predicted_viewport_bounds = self.predicted_viewport_bounds().clone();
233 let predicted_target_world = self.camera_motion_state.predicted_target_world;
234 let should_speculatively_prefetch = self.camera_motion_state.pan_velocity_world.length_squared() > 1e-9;
235 let zoom_prefetch_direction = zoom_prefetch_direction(self.camera_zoom_delta);
236 let prefetch_route = self.prefetch_route.clone();
237
238 let use_covering = self.terrain.enabled()
239 && self.camera.pitch() > 0.3
240 && self.camera.mode() == crate::camera::CameraMode::Perspective;
241
242 let covering_params = if use_covering {
243 let fzoom = self.fractional_zoom();
244 self.camera.covering_camera(fzoom).map(|cam| {
245 let opts = rustial_math::CoveringTilesOptions {
246 min_zoom: 0,
247 max_zoom: rustial_math::MAX_ZOOM,
248 round_zoom: false,
249 tile_size: 256,
250 max_tiles: 512,
251 allow_variable_zoom: true,
252 render_world_copies: true,
253 };
254 (cam, opts)
255 })
256 } else {
257 None
258 };
259
260 let raster_budget = self.request_coordinator.budget_for(SourcePriority::Raster);
264
265 for layer in self.layers.iter_mut() {
266 if !layer.visible() {
267 continue;
268 }
269 if layer.kind() == LayerKind::Tile {
270 if let Some(tile_layer) = layer.as_any_mut().downcast_mut::<TileLayer>() {
271 let mut sel = tile_layer.selection_config().clone();
273 sel.max_requests_per_frame = raster_budget;
274 tile_layer.set_selection_config(sel);
275
276 if let (Some(ref frustum), Some((ref cam, ref opts))) =
277 (self.frustum.as_ref(), covering_params.as_ref())
278 {
279 tile_layer.update_with_covering(
280 frustum,
281 cam,
282 opts,
283 camera_world,
284 );
285 } else {
286 tile_layer.update_with_view(
287 &viewport_bounds,
288 zoom_level,
289 camera_world,
290 camera_distance,
291 flat_view.as_ref(),
292 );
293 }
294
295 let stats = tile_layer.last_selection_stats();
297 let desired: HashSet<TileId> = tile_layer
298 .visible_tiles()
299 .tiles
300 .iter()
301 .map(|t| t.target)
302 .collect();
303 let mut total_raster_requested = stats.requested_tiles;
304 let raster_pending_demand = stats.fallback_visible_tiles + stats.missing_visible_tiles;
305 let mut remaining_speculative_budget = speculative_prefetch_budget(
306 raster_budget,
307 stats.requested_tiles,
308 );
309
310 if remaining_speculative_budget > 0 && should_speculatively_prefetch {
312 let prefetched = tile_layer.prefetch_with_view(
313 &predicted_viewport_bounds,
314 zoom_level,
315 (predicted_target_world.x, predicted_target_world.y),
316 None,
317 remaining_speculative_budget,
318 );
319 total_raster_requested += prefetched;
320 remaining_speculative_budget = remaining_speculative_budget.saturating_sub(prefetched);
321 }
322
323 if remaining_speculative_budget > 0 {
325 if let Some(direction) = zoom_prefetch_direction {
326 let prefetched = tile_layer.prefetch_zoom_direction(
327 camera_world,
328 direction,
329 remaining_speculative_budget,
330 );
331 total_raster_requested += prefetched;
332 remaining_speculative_budget = remaining_speculative_budget.saturating_sub(prefetched);
333 }
334 }
335
336 if remaining_speculative_budget > 0 {
338 if let Some(ref route) = prefetch_route {
339 let prefetched = tile_layer.prefetch_route(
340 route,
341 zoom_level,
342 camera_world,
343 remaining_speculative_budget,
344 );
345 total_raster_requested += prefetched;
346 }
347 }
348
349 self.request_coordinator.report(
350 SourcePriority::Raster,
351 desired,
352 total_raster_requested,
353 raster_pending_demand,
354 );
355
356 self.visible_tiles = Arc::new(
357 tile_layer.visible_tiles().tiles.clone(),
358 );
359 }
360 }
361 }
362
363 self.update_streamed_vector_source_layers(
366 zoom_level,
367 camera_world,
368 camera_distance,
369 &viewport_bounds,
370 flat_view.as_ref(),
371 covering_params.as_ref(),
372 );
373
374 self.request_coordinator.finish_frame();
376 }
377
378 pub(super) fn update_streamed_vector_source_layers(
379 &mut self,
380 zoom_level: u8,
381 camera_world: (f64, f64),
382 camera_distance: f64,
383 viewport_bounds: &WorldBounds,
384 flat_view: Option<&rustial_math::FlatTileView>,
385 covering_params: Option<&(rustial_math::CoveringCamera, rustial_math::CoveringTilesOptions)>,
386 ) {
387 let vector_budget = self.request_coordinator.budget_for(SourcePriority::Vector);
390 let source_count = self.streamed_vector_sources.len().max(1);
391 let per_source_budget = if vector_budget == usize::MAX {
392 usize::MAX
393 } else {
394 (vector_budget / source_count).max(1)
396 };
397
398 let mut total_vector_requested = 0usize;
399 let mut all_vector_desired = HashSet::new();
400 let predicted_viewport_bounds = self.predicted_viewport_bounds().clone();
401 let predicted_target_world = self.camera_motion_state.predicted_target_world;
402 let should_speculatively_prefetch = self.camera_motion_state.pan_velocity_world.length_squared() > 1e-9;
403 let zoom_prefetch_direction = zoom_prefetch_direction(self.camera_zoom_delta);
404 let prefetch_route = self.prefetch_route.clone();
405
406 for source_layer in self.streamed_vector_sources.values_mut() {
407 let mut sel = source_layer.selection_config().clone();
409 sel.max_requests_per_frame = per_source_budget;
410 source_layer.set_selection_config(sel);
411
412 if let (Some(ref frustum), Some((cam, opts))) = (self.frustum.as_ref(), covering_params)
413 {
414 source_layer.update_with_covering(frustum, cam, opts, camera_world);
415 } else {
416 source_layer.update_with_view(
417 viewport_bounds,
418 zoom_level,
419 camera_world,
420 camera_distance,
421 flat_view,
422 );
423 }
424
425 let stats = source_layer.last_selection_stats();
427 total_vector_requested += stats.requested_tiles;
428 for tile in source_layer.visible_tiles().tiles.iter() {
429 all_vector_desired.insert(tile.target);
430 }
431
432 let mut remaining_speculative_budget = speculative_prefetch_budget(
433 per_source_budget,
434 stats.requested_tiles,
435 );
436
437 if remaining_speculative_budget > 0 && should_speculatively_prefetch {
439 let prefetched = source_layer.prefetch_with_view(
440 &predicted_viewport_bounds,
441 zoom_level,
442 (predicted_target_world.x, predicted_target_world.y),
443 None,
444 remaining_speculative_budget,
445 );
446 total_vector_requested += prefetched;
447 remaining_speculative_budget = remaining_speculative_budget.saturating_sub(prefetched);
448 }
449
450 if remaining_speculative_budget > 0 {
452 if let Some(direction) = zoom_prefetch_direction {
453 let prefetched = source_layer.prefetch_zoom_direction(
454 camera_world,
455 direction,
456 remaining_speculative_budget,
457 );
458 total_vector_requested += prefetched;
459 remaining_speculative_budget = remaining_speculative_budget.saturating_sub(prefetched);
460 }
461 }
462
463 if remaining_speculative_budget > 0 {
465 if let Some(ref route) = prefetch_route {
466 let prefetched = source_layer.prefetch_route(
467 route,
468 zoom_level,
469 camera_world,
470 remaining_speculative_budget,
471 );
472 total_vector_requested += prefetched;
473 }
474 }
475 }
476
477 self.request_coordinator.report(
479 SourcePriority::Vector,
480 all_vector_desired,
481 total_vector_requested,
482 total_vector_requested,
483 );
484 }
485
486}
487
488fn tile_center_dist_sq(tile: &rustial_math::TileId, cam: (f64, f64)) -> f64 {
490 let b = rustial_math::tile_bounds_world(tile);
491 let cx = (b.min.position.x + b.max.position.x) * 0.5;
492 let cy = (b.min.position.y + b.max.position.y) * 0.5;
493 let dx = cx - cam.0;
494 let dy = cy - cam.1;
495 dx * dx + dy * dy
496}
497
498fn speculative_prefetch_budget(total_budget: usize, visible_requests: usize) -> usize {
499 if total_budget == 0 {
500 return 0;
501 }
502
503 if total_budget == usize::MAX {
504 return DEFAULT_SPECULATIVE_PREFETCH_REQUEST_BUDGET;
505 }
506
507 let remaining = total_budget.saturating_sub(visible_requests);
508 if remaining == 0 {
509 return 0;
510 }
511
512 let speculative_cap = ((total_budget as f64) * SPECULATIVE_PREFETCH_BUDGET_FRACTION)
513 .ceil() as usize;
514 remaining.min(speculative_cap.max(1))
515}
516
517fn zoom_prefetch_direction(zoom_delta: f64) -> Option<ZoomPrefetchDirection> {
518 if zoom_delta > ZOOM_DIRECTION_PREFETCH_THRESHOLD {
519 Some(ZoomPrefetchDirection::In)
520 } else if zoom_delta < -ZOOM_DIRECTION_PREFETCH_THRESHOLD {
521 Some(ZoomPrefetchDirection::Out)
522 } else {
523 None
524 }
525}
526