use crate::cache::{Handle, LruCache, MaxSize, WithFactory};
use crate::draw::texture::{ColorFormat, Texture};
use crate::ecs::{Entity, World};
use crate::types::Fixed;
use core::cell::{Ref, 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(Clone, Copy, Debug)]
pub struct WidgetTextureRef(pub Entity);
#[derive(Clone, Copy, Debug)]
pub struct OffscreenAutoAdded;
#[derive(Clone, Copy, Debug)]
pub struct OffscreenAlphaMode {
pub clear_transparent: bool,
}
impl OffscreenAlphaMode {
pub const fn clear_transparent() -> Self {
Self {
clear_transparent: true,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct WidgetTextureRefPrevGen(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) last_format: core::cell::Cell<Option<ColorFormat>>,
}
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)),
last_format: core::cell::Cell::new(None),
}
}
}
impl Default for OffscreenBufferPool {
fn default() -> Self {
Self::with_budget(0)
}
}
#[derive(Clone)]
pub struct TextureSnapshot {
handle: Handle<RefCell<Texture<'static>>>,
}
impl TextureSnapshot {
pub fn borrow(&self) -> Ref<'_, Texture<'static>> {
self.handle.get().borrow()
}
}
fn texture_for(world: &World, entity: Entity, generation_offset: i64) -> Option<TextureSnapshot> {
let pool = world.resource::<OffscreenBufferPool>()?;
let format = pool.last_format.get()?;
let off = world.get::<OffscreenRender>(entity)?;
let rect = world.get::<super::ComputedRect>(entity)?.0;
let scale = off.scale.max(Fixed::ONE / 8);
let buf_w_f = Fixed::from_int(rect.w.to_int().max(1)) * scale;
let buf_h_f = Fixed::from_int(rect.h.to_int().max(1)) * scale;
let w = buf_w_f.to_int().max(1).min(u16::MAX as i32) as u16;
let h = buf_h_f.to_int().max(1).min(u16::MAX as i32) as u16;
let g_now = world
.get::<OffscreenGeneration>(entity)
.map(|g| g.0)
.unwrap_or(0);
let g = if generation_offset >= 0 {
g_now.checked_add(generation_offset as u32)?
} else {
g_now.checked_sub((-generation_offset) as u32)?
};
let key = BufferKey {
entity,
w,
h,
format,
generation: g,
};
let handle = pool.cache.borrow_mut().acquire(&key)?;
Some(TextureSnapshot { handle })
}
pub trait WidgetTextureAccess {
fn texture_of(&self, entity: Entity) -> Option<TextureSnapshot>;
fn prev_texture_of(&self, entity: Entity) -> Option<TextureSnapshot>;
}
impl WidgetTextureAccess for World {
fn texture_of(&self, entity: Entity) -> Option<TextureSnapshot> {
texture_for(self, entity, 0)
}
fn prev_texture_of(&self, entity: Entity) -> Option<TextureSnapshot> {
texture_for(self, entity, -1)
}
}
#[crate::system(order = PRE_RENDER, expect = [WidgetTextureRef, OffscreenAutoAdded])]
pub fn maintain_widget_texture_refs(world: &mut World) {
use alloc::vec::Vec;
use hashbrown::HashMap;
let any_ref = world.query::<WidgetTextureRef>().iter().next().is_some();
let any_auto = world.query::<OffscreenAutoAdded>().iter().next().is_some();
if !any_ref && !any_auto {
return;
}
let mut counts: HashMap<Entity, u32> = HashMap::new();
for (_e, r) in world.query::<WidgetTextureRef>().iter() {
*counts.entry(r.0).or_insert(0) += 1;
}
let mut to_add: Vec<Entity> = Vec::new();
for (&source, &n) in &counts {
if n > 0 && world.get::<OffscreenRender>(source).is_none() {
to_add.push(source);
}
}
for source in to_add {
world.insert(source, OffscreenRender::default());
world.insert(source, OffscreenAutoAdded);
}
let auto_entries: Vec<Entity> = world
.query::<OffscreenAutoAdded>()
.iter()
.map(|(e, _)| e)
.collect();
for source in auto_entries {
if counts.get(&source).copied().unwrap_or(0) == 0
&& world.get::<WidgetTextureRef>(source).is_none()
{
world.remove::<OffscreenRender>(source);
world.remove::<OffscreenAutoAdded>(source);
}
}
let pairs: Vec<(Entity, Entity)> = world
.query::<WidgetTextureRef>()
.iter()
.map(|(e, r)| (e, r.0))
.collect();
for (consumer, source) in pairs {
let g_now = world
.get::<OffscreenGeneration>(source)
.map(|g| g.0)
.unwrap_or(0);
let g_prev = world
.get::<WidgetTextureRefPrevGen>(consumer)
.map(|g| g.0)
.unwrap_or(0);
let source_dirty = world.get::<super::dirty::Dirty>(source).is_some();
let source_transform = world
.get::<crate::components::WidgetTransform>(source)
.copied();
if g_now != g_prev || source_dirty {
world.insert(consumer, super::dirty::Dirty);
world.insert(consumer, WidgetTextureRefPrevGen(g_now));
if let Some(tf) = source_transform {
world.insert(consumer, tf);
} else {
world.remove::<crate::components::WidgetTransform>(consumer);
}
}
}
}
#[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);
}
fn run_refs(world: &mut World) {
super::maintain_widget_texture_refs(world);
}
#[test]
fn first_ref_auto_adds_offscreen_render() {
let mut world = World::default();
let source = world.spawn();
let consumer = world.spawn();
world.insert(consumer, WidgetTextureRef(source));
run_refs(&mut world);
assert!(world.get::<OffscreenRender>(source).is_some());
assert!(world.get::<OffscreenAutoAdded>(source).is_some());
}
#[test]
fn last_ref_dropped_auto_removes_offscreen_render() {
let mut world = World::default();
let source = world.spawn();
let c1 = world.spawn();
let c2 = world.spawn();
world.insert(c1, WidgetTextureRef(source));
world.insert(c2, WidgetTextureRef(source));
run_refs(&mut world);
assert!(world.get::<OffscreenRender>(source).is_some());
world.remove::<WidgetTextureRef>(c1);
run_refs(&mut world);
assert!(world.get::<OffscreenRender>(source).is_some());
world.remove::<WidgetTextureRef>(c2);
run_refs(&mut world);
assert!(world.get::<OffscreenRender>(source).is_none());
assert!(world.get::<OffscreenAutoAdded>(source).is_none());
}
#[test]
fn user_explicit_offscreen_render_is_never_removed() {
let mut world = World::default();
let source = world.spawn();
world.insert(source, OffscreenRender::default());
let consumer = world.spawn();
world.insert(consumer, WidgetTextureRef(source));
run_refs(&mut world);
assert!(world.get::<OffscreenAutoAdded>(source).is_none());
world.remove::<WidgetTextureRef>(consumer);
run_refs(&mut world);
assert!(world.get::<OffscreenRender>(source).is_some());
}
}