use oxiui_core::UiError;
use crate::gpu::render_target::RenderTarget;
struct LayerEntry {
target: RenderTarget,
last_frame: u64,
generation: u64,
}
pub struct LayerCache {
entries: std::collections::HashMap<u64, LayerEntry>,
max_idle_frames: u64,
current_frame: u64,
max_layers: usize,
}
impl LayerCache {
pub fn new(max_layers: usize) -> Self {
Self {
entries: std::collections::HashMap::new(),
max_idle_frames: 4,
current_frame: 0,
max_layers: max_layers.max(1),
}
}
pub fn set_max_idle_frames(&mut self, frames: u64) {
self.max_idle_frames = frames.max(1);
}
pub fn begin_frame(&mut self) {
self.current_frame += 1;
let max_idle = self.max_idle_frames;
let current = self.current_frame;
self.entries
.retain(|_, entry| current - entry.last_frame <= max_idle);
}
pub fn get(&mut self, layer_id: u64) -> Option<&mut RenderTarget> {
let current = self.current_frame;
let entry = self.entries.get_mut(&layer_id)?;
entry.last_frame = current;
Some(&mut entry.target)
}
pub fn get_or_create(
&mut self,
device: &wgpu::Device,
layer_id: u64,
width: u32,
height: u32,
sample_count: u32,
) -> Result<&mut RenderTarget, UiError> {
let current = self.current_frame;
if !self.entries.contains_key(&layer_id) {
if self.entries.len() >= self.max_layers {
self.evict_lru();
}
let target = RenderTarget::new(device, width, height, sample_count)?;
self.entries.insert(
layer_id,
LayerEntry {
target,
last_frame: current,
generation: 0,
},
);
}
let entry = self.entries.get_mut(&layer_id).expect("just inserted");
entry.last_frame = current;
Ok(&mut entry.target)
}
pub fn invalidate(&mut self, layer_id: u64) {
if let Some(entry) = self.entries.get_mut(&layer_id) {
entry.target.mark_dirty();
entry.generation += 1;
}
}
pub fn invalidate_all(&mut self) {
for entry in self.entries.values_mut() {
entry.target.mark_dirty();
entry.generation += 1;
}
}
pub fn remove(&mut self, layer_id: u64) {
self.entries.remove(&layer_id);
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn current_frame(&self) -> u64 {
self.current_frame
}
fn evict_lru(&mut self) {
let lru_key = self
.entries
.iter()
.min_by_key(|(_, e)| e.last_frame)
.map(|(&k, _)| k);
if let Some(k) = lru_key {
self.entries.remove(&k);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn try_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::default();
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
force_fallback_adapter: false,
compatible_surface: None,
}))
.ok()?;
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("layer-cache test device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::downlevel_defaults(),
memory_hints: wgpu::MemoryHints::Performance,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
trace: wgpu::Trace::Off,
}))
.ok()
}
#[test]
fn layer_cache_empty_on_creation() {
let cache = LayerCache::new(8);
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
assert_eq!(cache.current_frame(), 0);
}
#[test]
fn layer_cache_get_or_create_and_get() {
let Some((device, _queue)) = try_device() else {
return;
};
let mut cache = LayerCache::new(4);
cache.begin_frame();
let target = cache
.get_or_create(&device, 42, 32, 32, 1)
.expect("create layer 42");
assert!(target.is_dirty(), "fresh layer must be dirty");
target.mark_clean();
assert_eq!(cache.len(), 1);
let t = cache.get(42).expect("layer 42 must exist");
assert!(!t.is_dirty(), "layer must be clean after mark_clean");
}
#[test]
fn layer_cache_invalidate_marks_dirty() {
let Some((device, _queue)) = try_device() else {
return;
};
let mut cache = LayerCache::new(4);
cache.begin_frame();
let target = cache.get_or_create(&device, 1, 16, 16, 1).expect("create");
target.mark_clean();
cache.invalidate(1);
let t = cache.get(1).expect("layer 1 must exist");
assert!(t.is_dirty(), "invalidated layer must be dirty");
}
#[test]
fn layer_cache_evicts_idle_layers() {
let Some((device, _queue)) = try_device() else {
return;
};
let mut cache = LayerCache::new(8);
cache.set_max_idle_frames(2);
cache.begin_frame(); cache.get_or_create(&device, 10, 16, 16, 1).expect("create");
cache.begin_frame(); cache.begin_frame(); cache.begin_frame();
assert!(
cache.get(10).is_none(),
"layer 10 must be evicted after {n} idle frames",
n = 3
);
}
#[test]
fn layer_cache_lru_eviction_at_capacity() {
let Some((device, _queue)) = try_device() else {
return;
};
let mut cache = LayerCache::new(2);
cache.begin_frame();
cache.get_or_create(&device, 1, 8, 8, 1).expect("layer 1");
cache.get_or_create(&device, 2, 8, 8, 1).expect("layer 2");
assert_eq!(cache.len(), 2);
cache.begin_frame();
let _ = cache.get(2);
cache.get_or_create(&device, 3, 8, 8, 1).expect("layer 3");
assert_eq!(cache.len(), 2, "cache should still hold 2 entries");
assert!(
cache.get(1).is_none(),
"layer 1 (LRU) must have been evicted"
);
assert!(cache.get(2).is_some(), "layer 2 must survive");
assert!(cache.get(3).is_some(), "layer 3 must be present");
}
#[test]
fn layer_cache_clear_removes_all() {
let Some((device, _queue)) = try_device() else {
return;
};
let mut cache = LayerCache::new(8);
cache.begin_frame();
cache.get_or_create(&device, 1, 8, 8, 1).expect("layer 1");
cache.get_or_create(&device, 2, 8, 8, 1).expect("layer 2");
assert_eq!(cache.len(), 2);
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn layer_cache_invalid_layer_id_returns_none() {
let mut cache = LayerCache::new(4);
cache.begin_frame();
assert!(cache.get(9999).is_none());
}
#[test]
fn layer_cache_invalidate_all() {
let Some((device, _queue)) = try_device() else {
return;
};
let mut cache = LayerCache::new(4);
cache.begin_frame();
for id in 1u64..=3 {
let t = cache.get_or_create(&device, id, 8, 8, 1).expect("create");
t.mark_clean();
}
cache.invalidate_all();
for id in 1u64..=3 {
let t = cache.get(id).expect("layer must exist");
assert!(
t.is_dirty(),
"all layers must be dirty after invalidate_all"
);
}
}
}