- 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
779 lines
23 KiB
Rust
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);
|
|
}
|
|
}
|