use crate::cache::{LruCache, MaxSize, WithFactory};
use crate::draw::texture::{ColorFormat, Texture};
use crate::ecs::Entity;
use crate::types::Fixed;
use core::cell::RefCell;
#[derive(Clone, Copy, Debug)]
pub struct OffscreenRender {
pub scale: Fixed,
}
impl Default for OffscreenRender {
fn default() -> Self {
Self::new()
}
}
impl OffscreenRender {
pub const fn new() -> Self {
Self { scale: Fixed::ONE }
}
pub const fn with_scale(scale: Fixed) -> Self {
Self { scale }
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct OffscreenGeneration(pub u32);
#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
pub(crate) struct BufferKey {
pub entity: Entity,
pub w: u16,
pub h: u16,
pub format: ColorFormat,
pub generation: u32,
}
pub struct OffscreenBufferPool {
pub(crate) cache:
RefCell<WithFactory<LruCache<BufferKey, RefCell<Texture<'static>>>, BufferCtor>>,
}
pub(crate) type BufferCtor = fn(&BufferKey) -> Result<RefCell<Texture<'static>>, BufferAllocError>;
#[derive(Debug)]
pub struct BufferAllocError;
fn make_buffer(k: &BufferKey) -> Result<RefCell<Texture<'static>>, BufferAllocError> {
Ok(RefCell::new(Texture::owned(k.w, k.h, k.format)))
}
impl OffscreenBufferPool {
pub fn with_budget(budget_bytes: usize) -> Self {
let cache = LruCache::builder()
.max_size(MaxSize::Bytes(budget_bytes))
.name("widget/offscreen")
.build();
Self {
cache: RefCell::new(WithFactory::new(cache, make_buffer as BufferCtor)),
}
}
}
impl Default for OffscreenBufferPool {
fn default() -> Self {
Self::with_budget(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_entity(id: u32) -> Entity {
Entity { id, generation: 0 }
}
#[test]
fn pool_creates_with_byte_budget() {
let pool = OffscreenBufferPool::with_budget(64 * 1024);
assert_eq!(pool.cache.borrow().cache().len(), 0);
}
#[test]
fn pool_default_disables_cache() {
let pool = OffscreenBufferPool::default();
let key = BufferKey {
entity: dummy_entity(1),
w: 32,
h: 32,
format: ColorFormat::RGBA8888,
generation: 0,
};
let handle = pool
.cache
.borrow_mut()
.entry(key)
.or_insert()
.expect("ctor still runs even when the budget is zero");
assert!(
handle.is_invalid(),
"Bytes(0) must hand back a detached handle"
);
assert_eq!(pool.cache.borrow().cache().len(), 0);
}
#[test]
fn buffer_key_distinguishes_entities() {
let k1 = BufferKey {
entity: dummy_entity(1),
w: 40,
h: 24,
format: ColorFormat::RGBA8888,
generation: 0,
};
let k2 = BufferKey {
entity: dummy_entity(2),
w: 40,
h: 24,
format: ColorFormat::RGBA8888,
generation: 0,
};
assert_ne!(k1, k2);
}
#[test]
fn buffer_key_distinguishes_generations() {
let k1 = BufferKey {
entity: dummy_entity(1),
w: 40,
h: 24,
format: ColorFormat::RGBA8888,
generation: 0,
};
let k2 = BufferKey {
generation: 1,
..k1
};
assert_ne!(k1, k2);
}
#[test]
fn pool_or_insert_creates_buffer_at_requested_size() {
let pool = OffscreenBufferPool::with_budget(64 * 1024);
let key = BufferKey {
entity: dummy_entity(1),
w: 40,
h: 24,
format: ColorFormat::RGBA8888,
generation: 0,
};
let handle = pool
.cache
.borrow_mut()
.entry(key)
.or_insert()
.expect("alloc");
assert!(!handle.is_invalid());
let tex = handle.borrow();
assert_eq!(tex.width, 40);
assert_eq!(tex.height, 24);
assert_eq!(tex.format, ColorFormat::RGBA8888);
}
#[test]
fn pool_or_insert_hits_same_key() {
let pool = OffscreenBufferPool::with_budget(64 * 1024);
let key = BufferKey {
entity: dummy_entity(1),
w: 40,
h: 24,
format: ColorFormat::RGBA8888,
generation: 0,
};
let _h1 = pool
.cache
.borrow_mut()
.entry(key)
.or_insert()
.expect("first");
let stats_after_first = *pool.cache.borrow().cache().stats();
let _h2 = pool
.cache
.borrow_mut()
.entry(key)
.or_insert()
.expect("second");
let stats_after_second = *pool.cache.borrow().cache().stats();
assert_eq!(stats_after_second.miss_count, stats_after_first.miss_count);
assert_eq!(
stats_after_second.hit_count,
stats_after_first.hit_count + 1
);
}
#[test]
fn pool_or_insert_misses_after_generation_bump() {
let pool = OffscreenBufferPool::with_budget(64 * 1024);
let key0 = BufferKey {
entity: dummy_entity(1),
w: 40,
h: 24,
format: ColorFormat::RGBA8888,
generation: 0,
};
let _h0 = pool
.cache
.borrow_mut()
.entry(key0)
.or_insert()
.expect("gen 0");
let key1 = BufferKey {
generation: 1,
..key0
};
let stats_before = *pool.cache.borrow().cache().stats();
let _h1 = pool
.cache
.borrow_mut()
.entry(key1)
.or_insert()
.expect("gen 1");
let stats_after = *pool.cache.borrow().cache().stats();
assert_eq!(stats_after.miss_count, stats_before.miss_count + 1);
}
#[test]
fn pool_byte_budget_evicts_lru_when_total_exceeds_limit() {
let pool = OffscreenBufferPool::with_budget(8 * 1024);
let key = |id, g| BufferKey {
entity: dummy_entity(id),
w: 40,
h: 24,
format: ColorFormat::RGBA8888,
generation: g,
};
let _h1 = pool.cache.borrow_mut().entry(key(1, 0)).or_insert();
let _h2 = pool.cache.borrow_mut().entry(key(2, 0)).or_insert();
let _ = pool.cache.borrow_mut().acquire(&key(1, 0));
let _h3 = pool.cache.borrow_mut().entry(key(3, 0)).or_insert();
let cache = pool.cache.borrow();
assert_eq!(cache.cache().len(), 2);
assert!(cache.cache().current_size() <= 8 * 1024);
assert_eq!(cache.cache().stats().evict_count, 1);
}
#[test]
fn pool_oversized_entry_returns_invalid_handle_without_growing_cache() {
let pool = OffscreenBufferPool::with_budget(4 * 1024);
let key = BufferKey {
entity: dummy_entity(1),
w: 200,
h: 200,
format: ColorFormat::RGBA8888,
generation: 0,
};
let handle = pool
.cache
.borrow_mut()
.entry(key)
.or_insert()
.expect("ctor still runs even when entry won't fit");
assert!(
handle.is_invalid(),
"oversized entry must come back detached"
);
assert_eq!(pool.cache.borrow().cache().len(), 0);
}
#[test]
fn offscreen_render_default_is_full_scale() {
let off = OffscreenRender::default();
assert_eq!(off.scale, Fixed::ONE);
}
#[test]
fn offscreen_render_with_scale() {
let off = OffscreenRender::with_scale(Fixed::ONE / 2);
assert_eq!(off.scale, Fixed::ONE / 2);
}
}