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 = Arc::new(self.terrain.visible_hillshade_rasters().to_vec());
18
19 self.request_coordinator
21 .report_demand(SourcePriority::Terrain, self.terrain.pending_count());
22 }
23 }
24
25 pub(super) fn desired_terrain_tiles(&self) -> Option<Vec<rustial_math::TileId>> {
36 let use_covering = self.terrain.enabled()
37 && self.camera.pitch() > 0.3
38 && self.camera.mode() == crate::camera::CameraMode::Perspective;
39
40 if !use_covering || self.visible_tiles.is_empty() {
41 return None;
42 }
43
44 let raster_actuals: HashSet<TileId> = self
51 .visible_tiles
52 .iter()
53 .filter(|vt| vt.data.is_some())
54 .map(|vt| vt.actual)
55 .collect();
56
57 let has_raster_ancestor = |tile: &TileId| -> bool {
58 if raster_actuals.contains(tile) {
60 return true;
61 }
62 let mut current = *tile;
64 for _ in 0..8u8 {
65 let Some(parent) = current.parent() else {
66 break;
67 };
68 if raster_actuals.contains(&parent) {
69 return true;
70 }
71 current = parent;
72 }
73 false
74 };
75
76 let mut terrain_tiles: Vec<rustial_math::TileId> =
77 self.visible_tiles.iter().map(|tile| tile.target).collect();
78
79 let strict_tiles = if let Some(view) = self.camera.flat_tile_view() {
80 visible_tiles_flat_view_with_config(
81 &self.viewport_bounds,
82 self.zoom_level,
83 &view,
84 &FlatTileSelectionConfig {
85 footprint_pitch_threshold_rad: 0.0,
86 footprint_min_tiles: 0,
87 ..FlatTileSelectionConfig::default()
88 },
89 )
90 } else {
91 visible_tiles(&self.viewport_bounds, self.zoom_level)
92 };
93
94 let mut seen = HashSet::with_capacity(terrain_tiles.len() + strict_tiles.len());
95 terrain_tiles.retain(|tile| seen.insert(*tile));
96 for tile in strict_tiles {
97 if seen.insert(tile) && has_raster_ancestor(&tile) {
98 terrain_tiles.push(tile);
99 }
100 }
101
102 if self.camera.pitch() > 0.4 {
107 let target_world = self.camera.target_world();
108 let eye = target_world + self.camera.eye_offset();
109 let eye_ground = WorldCoord::new(eye.x, eye.y, 0.0);
110
111 let altitude = (eye.z - target_world.z).abs().max(1.0);
114 let half = altitude * 1.5;
115 let near_bounds = WorldBounds::new(
116 WorldCoord::new(
117 eye_ground.position.x - half,
118 eye_ground.position.y - half,
119 0.0,
120 ),
121 WorldCoord::new(
122 eye_ground.position.x + half,
123 eye_ground.position.y + half,
124 0.0,
125 ),
126 );
127 for tile in visible_tiles(&near_bounds, self.zoom_level) {
128 if seen.insert(tile) && has_raster_ancestor(&tile) {
129 terrain_tiles.push(tile);
130 }
131 }
132 }
133
134 let cam = self.mercator_camera_world();
135
136 let max_base_tiles = terrain_base_tile_budget(terrain_tiles.len());
140 if terrain_tiles.len() > max_base_tiles {
141 terrain_tiles.sort_by(|a, b| {
142 let da = tile_center_dist_sq(a, cam);
143 let db = tile_center_dist_sq(b, cam);
144 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
145 });
146 terrain_tiles.truncate(max_base_tiles);
147 }
148
149 if self.camera.pitch() > 0.5 && self.zoom_level > 2 {
155 let base_tiles: Vec<TileId> = terrain_tiles.clone();
156 let is_ancestor_of_existing = |candidate: &TileId| -> bool {
157 base_tiles.iter().any(|t| {
158 if t.zoom <= candidate.zoom {
159 return false;
160 }
161 let dz = t.zoom - candidate.zoom;
162 (t.x >> dz) == candidate.x && (t.y >> dz) == candidate.y
163 })
164 };
165 let mut horizon_budget =
166 terrain_horizon_tile_budget(max_base_tiles, self.camera.pitch());
167 let mut hz = self.zoom_level.saturating_sub(2);
168 while hz > 0 && horizon_budget > 0 {
169 let coarse = visible_tiles(&self.viewport_bounds, hz);
170 let mut coarse_sorted: Vec<_> = coarse
171 .into_iter()
172 .filter(|t| {
173 !seen.contains(t) && !is_ancestor_of_existing(t) && has_raster_ancestor(t)
174 })
175 .map(|t| {
176 let d = tile_center_dist_sq(&t, cam);
177 (t, d)
178 })
179 .collect();
180 coarse_sorted
182 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
183 let take = coarse_sorted.len().min(horizon_budget);
184 for (t, _) in coarse_sorted.into_iter().take(take) {
185 if seen.insert(t) {
186 terrain_tiles.push(t);
187 horizon_budget -= 1;
188 }
189 }
190 hz = hz.saturating_sub(2);
191 }
192 }
193
194 Some(terrain_tiles)
195 }
196
197 pub(super) fn update_terrain_with_tiles(&mut self, tiles: &[rustial_math::TileId]) {
202 if self.terrain.enabled() {
203 let meshes =
204 self.terrain
205 .update_with_tiles(tiles, self.zoom_level, self.camera.projection());
206 self.terrain_meshes = Arc::new(meshes);
207 self.hillshade_rasters = Arc::new(self.terrain.visible_hillshade_rasters().to_vec());
208
209 self.request_coordinator
211 .report_demand(SourcePriority::Terrain, self.terrain.pending_count());
212 }
213 }
214
215 pub(super) fn update_tile_layers(&mut self) {
223 use crate::layer::LayerKind;
224 use crate::layers::TileLayer;
225
226 self.request_coordinator.begin_frame();
229
230 let zoom_level = self.zoom_level;
231 let camera_world = self.mercator_camera_world();
232 let flat_view = self.camera.flat_tile_view();
233 let camera_distance = self.camera.distance();
234 let viewport_bounds = self.viewport_bounds;
235 let predicted_viewport_bounds = *self.predicted_viewport_bounds();
236 let predicted_target_world = self.camera_motion_state.predicted_target_world;
237 let should_speculatively_prefetch =
238 self.camera_motion_state.pan_velocity_world.length_squared() > 1e-9;
239 let zoom_prefetch_direction = zoom_prefetch_direction(self.camera_zoom_delta);
240 let prefetch_route = self.prefetch_route.clone();
241
242 let use_covering = self.terrain.enabled()
243 && self.camera.pitch() > 0.3
244 && self.camera.mode() == crate::camera::CameraMode::Perspective;
245
246 let covering_params = if use_covering {
247 let fzoom = self.fractional_zoom();
248 self.camera.covering_camera(fzoom).map(|cam| {
249 let opts = rustial_math::CoveringTilesOptions {
250 min_zoom: 0,
251 max_zoom: rustial_math::MAX_ZOOM,
252 round_zoom: false,
253 tile_size: 256,
254 max_tiles: 512,
255 allow_variable_zoom: true,
256 render_world_copies: true,
257 };
258 (cam, opts)
259 })
260 } else {
261 None
262 };
263
264 let raster_budget = self.request_coordinator.budget_for(SourcePriority::Raster);
268
269 for layer in self.layers.iter_mut() {
270 if !layer.visible() {
271 continue;
272 }
273 if layer.kind() == LayerKind::Tile {
274 if let Some(tile_layer) = layer.as_any_mut().downcast_mut::<TileLayer>() {
275 let mut sel = tile_layer.selection_config().clone();
277 sel.max_requests_per_frame = raster_budget;
278 tile_layer.set_selection_config(sel);
279
280 if let (Some(frustum), Some((ref cam, ref opts))) =
281 (self.frustum.as_ref(), covering_params.as_ref())
282 {
283 tile_layer.update_with_covering(frustum, cam, opts, camera_world);
284 } else {
285 tile_layer.update_with_view(
286 &viewport_bounds,
287 zoom_level,
288 camera_world,
289 camera_distance,
290 flat_view.as_ref(),
291 );
292 }
293
294 let stats = tile_layer.last_selection_stats();
296 let desired: HashSet<TileId> = tile_layer
297 .visible_tiles()
298 .tiles
299 .iter()
300 .map(|t| t.target)
301 .collect();
302 let mut total_raster_requested = stats.requested_tiles;
303 let raster_pending_demand =
304 stats.fallback_visible_tiles + stats.missing_visible_tiles;
305 let mut remaining_speculative_budget =
306 speculative_prefetch_budget(raster_budget, stats.requested_tiles);
307
308 if remaining_speculative_budget > 0 && should_speculatively_prefetch {
310 let prefetched = tile_layer.prefetch_with_view(
311 &predicted_viewport_bounds,
312 zoom_level,
313 (predicted_target_world.x, predicted_target_world.y),
314 None,
315 remaining_speculative_budget,
316 );
317 total_raster_requested += prefetched;
318 remaining_speculative_budget =
319 remaining_speculative_budget.saturating_sub(prefetched);
320 }
321
322 if remaining_speculative_budget > 0 {
324 if let Some(direction) = zoom_prefetch_direction {
325 let prefetched = tile_layer.prefetch_zoom_direction(
326 camera_world,
327 direction,
328 remaining_speculative_budget,
329 );
330 total_raster_requested += prefetched;
331 remaining_speculative_budget =
332 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(tile_layer.visible_tiles().tiles.clone());
357 }
358 }
359 }
360
361 self.update_streamed_vector_source_layers(
364 zoom_level,
365 camera_world,
366 camera_distance,
367 &viewport_bounds,
368 flat_view.as_ref(),
369 covering_params.as_ref(),
370 );
371
372 self.request_coordinator.finish_frame();
374 }
375
376 pub(super) fn update_streamed_vector_source_layers(
377 &mut self,
378 zoom_level: u8,
379 camera_world: (f64, f64),
380 camera_distance: f64,
381 viewport_bounds: &WorldBounds,
382 flat_view: Option<&rustial_math::FlatTileView>,
383 covering_params: Option<&(
384 rustial_math::CoveringCamera,
385 rustial_math::CoveringTilesOptions,
386 )>,
387 ) {
388 let vector_budget = self.request_coordinator.budget_for(SourcePriority::Vector);
391 let source_count = self.streamed_vector_sources.len().max(1);
392 let per_source_budget = if vector_budget == usize::MAX {
393 usize::MAX
394 } else {
395 (vector_budget / source_count).max(1)
397 };
398
399 let mut total_vector_requested = 0usize;
400 let mut all_vector_desired = HashSet::new();
401 let predicted_viewport_bounds = *self.predicted_viewport_bounds();
402 let predicted_target_world = self.camera_motion_state.predicted_target_world;
403 let should_speculatively_prefetch =
404 self.camera_motion_state.pan_velocity_world.length_squared() > 1e-9;
405 let zoom_prefetch_direction = zoom_prefetch_direction(self.camera_zoom_delta);
406 let prefetch_route = self.prefetch_route.clone();
407
408 for source_layer in self.streamed_vector_sources.values_mut() {
409 let mut sel = source_layer.selection_config().clone();
411 sel.max_requests_per_frame = per_source_budget;
412 source_layer.set_selection_config(sel);
413
414 if let (Some(frustum), Some((cam, opts))) = (self.frustum.as_ref(), covering_params) {
415 source_layer.update_with_covering(frustum, cam, opts, camera_world);
416 } else {
417 source_layer.update_with_view(
418 viewport_bounds,
419 zoom_level,
420 camera_world,
421 camera_distance,
422 flat_view,
423 );
424 }
425
426 let stats = source_layer.last_selection_stats();
428 total_vector_requested += stats.requested_tiles;
429 for tile in source_layer.visible_tiles().tiles.iter() {
430 all_vector_desired.insert(tile.target);
431 }
432
433 let mut remaining_speculative_budget =
434 speculative_prefetch_budget(per_source_budget, stats.requested_tiles);
435
436 if remaining_speculative_budget > 0 && should_speculatively_prefetch {
438 let prefetched = source_layer.prefetch_with_view(
439 &predicted_viewport_bounds,
440 zoom_level,
441 (predicted_target_world.x, predicted_target_world.y),
442 None,
443 remaining_speculative_budget,
444 );
445 total_vector_requested += prefetched;
446 remaining_speculative_budget =
447 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 =
460 remaining_speculative_budget.saturating_sub(prefetched);
461 }
462 }
463
464 if remaining_speculative_budget > 0 {
466 if let Some(ref route) = prefetch_route {
467 let prefetched = source_layer.prefetch_route(
468 route,
469 zoom_level,
470 camera_world,
471 remaining_speculative_budget,
472 );
473 total_vector_requested += prefetched;
474 }
475 }
476 }
477
478 self.request_coordinator.report(
480 SourcePriority::Vector,
481 all_vector_desired,
482 total_vector_requested,
483 total_vector_requested,
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 =
513 ((total_budget as f64) * SPECULATIVE_PREFETCH_BUDGET_FRACTION).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}