nigig-org/crates/apps/map/src/cache.rs
andodeki 534d82cd3c fix(map): resolve all 51 compilation errors in map crate
- Remove invalid makepad_fast_inflate/makepad_mbtile_reader re-exports from lib.rs
- Rewrite tessellation.rs to use correct Makepad tessellation API (9/10 arg signatures)
- Use append_tessellated_geometry with VectorRenderParams for proper 19-float vertex format
- Move handle_finger_* methods from Widget trait impl to NigigMapView impl
- Fix TileEntry Clone issue by storing (TileKey, f32) in draw_entries buffer
- Add RenderContext lifetime parameters for Cx2d<'a, 'b>
- Make draw_geometry pub(crate) for render_graph access
- Re-export TileEntry from cache module
- Re-export select_label_text from label module
- Add to_json and to_overpass_response methods to MvtTileJsonBuilder
- Add enable/disable/set_zoom_range methods to RenderGraph
- Remove Geometry::free calls (resources released on drop)
- Fix test API mismatches (Vec4f::new -> vec4, arg order, missing fields)
- Add Clone derives to GlyphData, GlyphMetrics, SpriteData, SpriteImage
- Add len/is_empty/clear methods to GlyphLoader

Result: 530/535 tests passing (98.1% pass rate), library compiles cleanly
2026-07-28 18:53:13 +00:00

779 lines
23 KiB
Rust

use super::geometry::*;
use super::tile::*;
pub use super::tile::TileEntry;
use makepad_widgets::*;
use std::collections::{HashMap, HashSet};
/// Counts of tiles in each state, for status display.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TileStatusCounts {
pub ready: usize,
pub loading: usize,
pub failed: usize,
pub retrying: usize,
pub exhausted: usize,
pub features: usize,
}
/// Single owner of tile storage and lifecycle.
///
/// Answers: who evicts? who marks stale? who retries? who guarantees uniqueness?
/// All tile mutations go through `TileCache` methods.
pub struct TileCache {
pub(crate) tiles: HashMap<TileKey, TileEntry>,
frame_counter: u32,
style_epoch: u64,
max_tiles: usize,
stale_frame_threshold: u32,
// Pending eviction to prevent use-after-free during rendering
pending_eviction: Option<(HashSet<TileKey>, u32)>,
}
impl Default for TileCache {
fn default() -> Self {
Self {
tiles: HashMap::new(),
frame_counter: 0,
style_epoch: 0,
max_tiles: 640,
stale_frame_threshold: 240,
pending_eviction: None,
}
}
}
impl TileCache {
pub fn new(max_tiles: usize) -> Self {
Self {
max_tiles,
..Default::default()
}
}
// --- Query ---
pub fn get(&self, key: TileKey) -> Option<&TileEntry> {
self.tiles.get(&key)
}
pub fn get_mut(&mut self, key: TileKey) -> Option<&mut TileEntry> {
self.tiles.get_mut(&key)
}
pub fn contains(&self, key: TileKey) -> bool {
self.tiles.contains_key(&key)
}
pub fn len(&self) -> usize {
self.tiles.len()
}
pub fn is_empty(&self) -> bool {
self.tiles.is_empty()
}
pub fn is_ready(&self, key: TileKey) -> bool {
self.tiles.get(&key).is_some_and(|entry| {
if let TileLoadState::Ready {
fill_geometry,
stroke_geometry,
feature_count,
..
} = &entry.state
{
*feature_count > 0 || fill_geometry.is_some() || stroke_geometry.is_some()
} else {
false
}
})
}
pub fn is_loading(&self, key: TileKey) -> bool {
self.tiles
.get(&key)
.is_some_and(|e| matches!(e.state, TileLoadState::LoadingNetwork | TileLoadState::LoadingLocal))
}
pub fn is_failed(&self, key: TileKey) -> bool {
self.tiles
.get(&key)
.is_some_and(|e| matches!(e.state, TileLoadState::Failed { .. }))
}
pub fn find_ready_ancestor(&self, mut key: TileKey) -> Option<TileKey> {
while key.z > 0 {
key = TileKey {
z: key.z - 1,
x: key.x / 2,
y: key.y / 2,
};
if self.is_ready(key) {
return Some(key);
}
}
None
}
pub fn find_ready_descendants(&self, key: TileKey) -> Vec<TileKey> {
self.tiles
.iter()
.filter(|(candidate, entry)| {
matches!(entry.state, TileLoadState::Ready { .. })
&& is_descendant_tile(**candidate, key)
})
.map(|(k, _)| *k)
.collect()
}
pub fn loading_count(&self) -> usize {
self.tiles
.values()
.filter(|e| matches!(e.state, TileLoadState::LoadingNetwork | TileLoadState::LoadingLocal))
.count()
}
pub fn pending_loading(&self) -> usize {
self.tiles
.values()
.filter(|e| matches!(e.state, TileLoadState::LoadingNetwork))
.count()
}
pub fn status_counts(&self, visible: &[TileKey]) -> TileStatusCounts {
let mut counts = TileStatusCounts::default();
for key in visible {
let Some(entry) = self.tiles.get(key) else {
continue;
};
match &entry.state {
TileLoadState::LoadingNetwork | TileLoadState::LoadingLocal => counts.loading += 1,
TileLoadState::Ready { feature_count, .. } => {
counts.ready += 1;
counts.features += feature_count;
}
TileLoadState::Failed { .. } => {
counts.failed += 1;
if entry.attempts >= MAX_TILE_RETRIES {
counts.exhausted += 1;
} else {
counts.retrying += 1;
}
}
}
}
counts
}
pub fn is_ready_or_has_descendants(&self, key: TileKey) -> bool {
if self.is_ready(key) {
return true;
}
self.find_ready_descendants(key).len() > 0
}
// --- Mutation ---
pub fn insert_loading(&mut self, key: TileKey, state: TileLoadState) {
self.tiles.insert(
key,
TileEntry {
state,
last_used: self.frame_counter,
attempts: 0,
},
);
}
pub fn insert_ready(&mut self, cx: &mut Cx, tile_key: TileKey, buffers: TileBuffers) {
let fill_geometry =
if !buffers.fill_indices.is_empty() && !buffers.fill_vertices.is_empty() {
let geometry = Geometry::new(cx);
geometry.update(cx, buffers.fill_indices, buffers.fill_vertices);
Some(geometry)
} else {
None
};
let stroke_geometry =
if !buffers.stroke_indices.is_empty() && !buffers.stroke_vertices.is_empty() {
let geometry = Geometry::new(cx);
geometry.update(cx, buffers.stroke_indices, buffers.stroke_vertices);
Some(geometry)
} else {
None
};
self.tiles.insert(
tile_key,
TileEntry {
state: TileLoadState::Ready {
fill_geometry,
stroke_geometry,
feature_count: buffers.feature_count,
labels: buffers.labels,
pois: buffers.pois,
},
last_used: self.frame_counter,
attempts: 0,
},
);
}
pub fn mark_failed(&mut self, tile_key: TileKey, reason: &str) {
let attempts = self
.tiles
.get(&tile_key)
.map_or(1, |entry| entry.attempts.saturating_add(1));
let retry_delay = retry_delay_frames(attempts);
let retry_after = self.frame_counter.saturating_add(retry_delay);
self.tiles.insert(
tile_key,
TileEntry {
state: TileLoadState::Failed { retry_after },
last_used: self.frame_counter,
attempts,
},
);
log!(
"NigigMapView: tile z{} x{} y{} failed (attempt {}): {}",
tile_key.z,
tile_key.x,
tile_key.y,
attempts,
reason
);
}
pub fn remove(&mut self, key: TileKey) -> Option<TileEntry> {
self.tiles.remove(&key)
}
pub fn mark_visible(&mut self, key: TileKey) {
if let Some(entry) = self.tiles.get_mut(&key) {
entry.last_used = self.frame_counter;
}
}
pub fn tick(&mut self) {
// Perform pending eviction from previous frame (prevents use-after-free)
if let Some((visible, target_zoom)) = self.pending_eviction.take() {
self.evict_internal(&visible, target_zoom);
}
// Use u32 counter to limit memory usage. When about to wrap, reset all
// last_used values to 0 to prevent eviction logic from breaking.
if self.frame_counter == u32::MAX - 1 {
// Reset all last_used to 0 before wrap
for entry in self.tiles.values_mut() {
entry.last_used = 0;
}
self.frame_counter = 0;
} else {
self.frame_counter += 1;
}
}
pub fn frame_counter(&self) -> u32 {
self.frame_counter
}
pub fn should_retry(&self, key: TileKey, max_retries: u8) -> Option<u8> {
self.tiles.get(&key).and_then(|entry| {
if let TileLoadState::Failed { retry_after } = entry.state {
if entry.attempts < max_retries && self.frame_counter >= retry_after {
return Some(entry.attempts);
}
}
None
})
}
// --- Eviction ---
/// Set pending eviction to be performed at the start of the next frame.
/// This prevents use-after-free by deferring eviction until after rendering.
pub fn set_pending_eviction(&mut self, visible: HashSet<TileKey>, target_zoom: u32) {
self.pending_eviction = Some((visible, target_zoom));
}
/// Internal eviction method called from tick().
fn evict_internal(&mut self, visible: &HashSet<TileKey>, target_zoom: u32) {
if self.tiles.len() <= self.max_tiles {
return;
}
let min_keep_zoom = target_zoom.saturating_sub(2);
let max_keep_zoom = target_zoom.saturating_add(1);
let frame = self.frame_counter;
let threshold = self.stale_frame_threshold;
// Collect tiles to evict
let mut to_evict = Vec::new();
for (key, entry) in &self.tiles {
if visible.contains(key)
|| matches!(
entry.state,
TileLoadState::LoadingNetwork | TileLoadState::LoadingLocal
)
{
continue;
}
if key.z < min_keep_zoom || key.z > max_keep_zoom {
to_evict.push(*key);
continue;
}
if frame.saturating_sub(entry.last_used) > threshold {
to_evict.push(*key);
}
}
// Remove tiles (GPU resources already freed in evict())
for key in to_evict {
self.tiles.remove(&key);
}
}
pub fn evict(&mut self, visible: &HashSet<TileKey>, target_zoom: u32) {
if self.tiles.len() <= self.max_tiles {
return;
}
let min_keep_zoom = target_zoom.saturating_sub(2);
let max_keep_zoom = target_zoom.saturating_add(1);
let frame = self.frame_counter;
let threshold = self.stale_frame_threshold;
// Collect tiles to evict
let mut to_evict = Vec::new();
for (key, entry) in &self.tiles {
if visible.contains(key)
|| matches!(
entry.state,
TileLoadState::LoadingNetwork | TileLoadState::LoadingLocal
)
{
continue;
}
if key.z < min_keep_zoom || key.z > max_keep_zoom {
to_evict.push(*key);
continue;
}
if frame.saturating_sub(entry.last_used) > threshold {
to_evict.push(*key);
}
}
// Free GPU resources and remove tiles
for key in to_evict {
if let Some(_entry) = self.tiles.remove(&key) {
// Geometry GPU resources are released when the entry is dropped
}
}
}
// --- Theme change ---
pub fn clear_all(&mut self) {
self.style_epoch = self.style_epoch.wrapping_add(1);
if self.style_epoch == 0 {
self.style_epoch = 1;
}
self.tiles.clear();
}
pub fn style_epoch(&self) -> u64 {
self.style_epoch
}
pub fn set_style_epoch(&mut self, epoch: u64) {
self.style_epoch = epoch;
}
pub fn iter_all(&self) -> impl Iterator<Item = (&TileKey, &TileEntry)> {
self.tiles.iter()
}
pub fn iter_visible<'a>(
&'a self,
keys: &'a [TileKey],
) -> impl Iterator<Item = (TileKey, &'a TileEntry)> + 'a {
keys.iter().filter_map(move |&key| {
self.tiles.get(&key).map(|entry| (key, entry))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::style::MapThemeStyle;
fn key(z: u32, x: i32, y: i32) -> TileKey {
TileKey { z, x, y }
}
fn make_cache() -> TileCache {
TileCache::new(100)
}
#[test]
fn insert_and_get() {
let mut cache = make_cache();
let k = key(14, 9872, 8247);
cache.insert_loading(k, TileLoadState::LoadingLocal);
assert!(cache.get(k).is_some());
assert!(cache.is_loading(k));
}
#[test]
fn is_ready_with_empty_geometry() {
let mut cache = make_cache();
let k = key(14, 0, 0);
cache.tiles.insert(
k,
TileEntry {
state: TileLoadState::Ready {
fill_geometry: None,
stroke_geometry: None,
feature_count: 0,
labels: vec![],
pois: vec![],
},
last_used: 0,
attempts: 0,
},
);
assert!(!cache.is_ready(k), "0 features and no geometry should not be ready");
}
#[test]
fn is_ready_with_features() {
let mut cache = make_cache();
let k = key(14, 0, 0);
cache.tiles.insert(
k,
TileEntry {
state: TileLoadState::Ready {
fill_geometry: None,
stroke_geometry: None,
feature_count: 42,
labels: vec![],
pois: vec![],
},
last_used: 0,
attempts: 0,
},
);
assert!(cache.is_ready(k));
}
#[test]
fn mark_failed_increments_attempts() {
let mut cache = make_cache();
let k = key(14, 0, 0);
cache.insert_loading(k, TileLoadState::LoadingLocal);
cache.mark_failed(k, "test error");
assert!(cache.is_failed(k));
let entry = cache.get(k).unwrap();
assert_eq!(entry.attempts, 1);
cache.mark_failed(k, "test error 2");
let entry = cache.get(k).unwrap();
assert_eq!(entry.attempts, 2);
}
#[test]
fn find_ready_ancestor_found() {
let mut cache = make_cache();
// Insert a ready tile at z13
let parent = key(13, 4936, 4123);
cache.tiles.insert(
parent,
TileEntry {
state: TileLoadState::Ready {
fill_geometry: None,
stroke_geometry: None,
feature_count: 10,
labels: vec![],
pois: vec![],
},
last_used: 0,
attempts: 0,
},
);
// Child at z14 should find parent
let child = key(14, 9872, 8247);
assert_eq!(cache.find_ready_ancestor(child), Some(parent));
}
#[test]
fn find_ready_ancestor_not_found() {
let cache = make_cache();
let k = key(14, 0, 0);
assert_eq!(cache.find_ready_ancestor(k), None);
}
#[test]
fn find_ready_descendants() {
let mut cache = make_cache();
let parent = key(13, 100, 100);
let child1 = key(14, 200, 200);
let child2 = key(14, 201, 200);
let unrelated = key(14, 0, 0);
for k in [parent, child1, child2, unrelated] {
cache.tiles.insert(
k,
TileEntry {
state: TileLoadState::Ready {
fill_geometry: None,
stroke_geometry: None,
feature_count: 5,
labels: vec![],
pois: vec![],
},
last_used: 0,
attempts: 0,
},
);
}
let descendants = cache.find_ready_descendants(parent);
assert_eq!(descendants.len(), 2);
assert!(descendants.contains(&child1));
assert!(descendants.contains(&child2));
assert!(!descendants.contains(&unrelated));
}
#[test]
fn loading_count() {
let mut cache = make_cache();
cache.insert_loading(key(14, 0, 0), TileLoadState::LoadingLocal);
cache.insert_loading(key(14, 1, 0), TileLoadState::LoadingNetwork);
assert_eq!(cache.loading_count(), 2);
}
#[test]
fn status_counts_ready() {
let mut cache = make_cache();
let k = key(14, 0, 0);
cache.tiles.insert(
k,
TileEntry {
state: TileLoadState::Ready {
fill_geometry: None,
stroke_geometry: None,
feature_count: 100,
labels: vec![],
pois: vec![],
},
last_used: 0,
attempts: 0,
},
);
let counts = cache.status_counts(&[k]);
assert_eq!(counts.ready, 1);
assert_eq!(counts.features, 100);
}
#[test]
fn evict_removes_old_tiles() {
let mut cache = make_cache();
// Advance frame counter so old tiles become stale
for _ in 0..300 {
cache.tick();
}
// Insert 150 tiles at zoom 14 with last_used=0 (stale)
for i in 0..150 {
let k = key(14, i, 0);
cache.tiles.insert(
k,
TileEntry {
state: TileLoadState::Ready {
fill_geometry: None,
stroke_geometry: None,
feature_count: 1,
labels: vec![],
pois: vec![],
},
last_used: 0,
attempts: 0,
},
);
}
let visible: HashSet<TileKey> = (0..10).map(|i| key(14, i, 0)).collect();
cache.evict(&visible, 14);
assert!(cache.len() <= 100 + 10, "should have evicted some tiles, got {}", cache.len());
}
#[test]
fn evict_preserves_loading() {
let mut cache = make_cache();
for i in 0..150 {
let k = key(14, i, 0);
cache.tiles.insert(
k,
TileEntry {
state: if i < 5 {
TileLoadState::LoadingNetwork
} else {
TileLoadState::Ready {
fill_geometry: None,
stroke_geometry: None,
feature_count: 1,
labels: vec![],
pois: vec![],
}
},
last_used: 0,
attempts: 0,
},
);
}
let visible = HashSet::new();
cache.evict(&visible, 14);
// Loading tiles should be preserved
for i in 0..5 {
assert!(cache.contains(key(14, i, 0)));
}
}
#[test]
fn evict_preserves_visible() {
let mut cache = make_cache();
for i in 0..150 {
let k = key(14, i, 0);
cache.tiles.insert(
k,
TileEntry {
state: TileLoadState::Ready {
fill_geometry: None,
stroke_geometry: None,
feature_count: 1,
labels: vec![],
pois: vec![],
},
last_used: 0,
attempts: 0,
},
);
}
let visible: HashSet<TileKey> = (0..10).map(|i| key(14, i, 0)).collect();
cache.evict(&visible, 14);
for i in 0..10 {
assert!(cache.contains(key(14, i, 0)));
}
}
#[test]
fn clear_all_resets() {
let mut cache = make_cache();
cache.insert_loading(key(14, 0, 0), TileLoadState::LoadingLocal);
let epoch = cache.style_epoch();
cache.clear_all();
assert!(cache.is_empty());
assert_eq!(cache.style_epoch(), epoch + 1);
}
#[test]
fn style_epoch_monotonic() {
let mut cache = make_cache();
let e0 = cache.style_epoch();
cache.clear_all();
let e1 = cache.style_epoch();
cache.clear_all();
let e2 = cache.style_epoch();
assert!(e1 > e0);
assert!(e2 > e1);
}
#[test]
fn tick_increments_frame() {
let mut cache = make_cache();
let f0 = cache.frame_counter();
cache.tick();
assert_eq!(cache.frame_counter(), f0 + 1);
}
#[test]
fn should_retry_after_delay() {
let mut cache = make_cache();
let k = key(14, 0, 0);
cache.mark_failed(k, "test");
// Should not retry immediately
assert!(cache.should_retry(k, 6).is_none());
// Advance frames past the retry delay
for _ in 0..31 {
cache.tick();
}
assert!(cache.should_retry(k, 6).is_some());
}
#[test]
fn should_retry_exhausted() {
let mut cache = make_cache();
let k = key(14, 0, 0);
// Fail 6 times (MAX_TILE_RETRIES)
for _ in 0..6 {
cache.mark_failed(k, "test");
}
// Advance past retry delay
for _ in 0..400 {
cache.tick();
}
assert!(cache.should_retry(k, 6).is_none(), "should not retry after max attempts");
}
#[test]
fn remove_returns_entry() {
let mut cache = make_cache();
let k = key(14, 0, 0);
cache.insert_loading(k, TileLoadState::LoadingLocal);
assert!(cache.remove(k).is_some());
assert!(!cache.contains(k));
}
#[test]
fn mark_visible_updates_last_used() {
let mut cache = make_cache();
let k = key(14, 0, 0);
cache.insert_loading(k, TileLoadState::LoadingLocal);
cache.tick();
cache.tick();
cache.mark_visible(k);
assert_eq!(cache.get(k).unwrap().last_used, 2);
}
#[test]
fn iter_visible_yields_ready() {
let mut cache = make_cache();
let k1 = key(14, 0, 0);
let k2 = key(14, 1, 0);
cache.tiles.insert(
k1,
TileEntry {
state: TileLoadState::Ready {
fill_geometry: None,
stroke_geometry: None,
feature_count: 5,
labels: vec![],
pois: vec![],
},
last_used: 0,
attempts: 0,
},
);
cache.insert_loading(k2, TileLoadState::LoadingLocal);
let keys = vec![k1, k2];
let visible: Vec<_> = cache.iter_visible(&keys).collect();
assert_eq!(visible.len(), 2);
}
#[test]
fn evict_noop_when_under_limit() {
let mut cache = make_cache();
for i in 0..10 {
cache.insert_loading(key(14, i, 0), TileLoadState::LoadingLocal);
}
let before = cache.len();
cache.evict(&HashSet::new(), 14);
assert_eq!(cache.len(), before);
}
}