use std::collections::HashMap;
use std::sync::{Mutex, PoisonError};
type TextureKey = (u32, u32, wgpu::TextureFormat, wgpu::TextureUsages);
pub(crate) struct TexturePool {
free: HashMap<TextureKey, Vec<wgpu::Texture>>,
alloc_count: usize,
}
impl TexturePool {
pub(crate) fn new() -> Self {
Self {
free: HashMap::new(),
alloc_count: 0,
}
}
pub(crate) fn acquire(
&mut self,
device: &wgpu::Device,
width: u32,
height: u32,
format: wgpu::TextureFormat,
usage: wgpu::TextureUsages,
) -> wgpu::Texture {
if let Some(texture) = self
.free
.get_mut(&(width, height, format, usage))
.and_then(Vec::pop)
{
return texture;
}
self.alloc_count += 1;
device.create_texture(&wgpu::TextureDescriptor {
label: Some("ffrender.pool.texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage,
view_formats: &[],
})
}
pub(crate) fn release(&mut self, key: TextureKey, texture: wgpu::Texture) {
self.free.entry(key).or_default().push(texture);
}
#[cfg(test)]
pub(crate) fn alloc_count(&self) -> usize {
self.alloc_count
}
}
fn lock(pool: &Mutex<TexturePool>) -> std::sync::MutexGuard<'_, TexturePool> {
pool.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) struct FrameScope<'a> {
pool: &'a Mutex<TexturePool>,
device: &'a wgpu::Device,
items: Vec<(TextureKey, wgpu::Texture)>,
}
impl<'a> FrameScope<'a> {
pub(crate) fn new(pool: &'a Mutex<TexturePool>, device: &'a wgpu::Device) -> Self {
Self {
pool,
device,
items: Vec::new(),
}
}
pub(crate) fn acquire(
&mut self,
width: u32,
height: u32,
format: wgpu::TextureFormat,
usage: wgpu::TextureUsages,
) -> usize {
let texture = lock(self.pool).acquire(self.device, width, height, format, usage);
self.items.push(((width, height, format, usage), texture));
self.items.len() - 1
}
pub(crate) fn get(&self, index: usize) -> &wgpu::Texture {
&self.items[index].1
}
pub(crate) fn take(mut self, index: usize) -> Option<wgpu::Texture> {
let items = std::mem::take(&mut self.items);
let mut pool = lock(self.pool);
let mut taken = None;
for (i, (key, texture)) in items.into_iter().enumerate() {
if i == index {
taken = Some(texture);
} else {
pool.release(key, texture);
}
}
taken
}
}
impl Drop for FrameScope<'_> {
fn drop(&mut self) {
let mut pool = lock(self.pool);
for (key, texture) in self.items.drain(..) {
pool.release(key, texture);
}
}
}
#[cfg(all(test, feature = "wgpu"))]
mod tests {
use super::TexturePool;
fn device() -> Option<wgpu::Device> {
match futures::executor::block_on(crate::context::RenderContext::init()) {
Ok(ctx) => Some(ctx.device),
Err(_) => None,
}
}
#[test]
fn frame_scope_take_should_not_return_taken_texture_to_pool() {
use std::sync::{Mutex, PoisonError};
let Some(device) = device() else {
return;
};
let pool = Mutex::new(TexturePool::new());
let format = wgpu::TextureFormat::Rgba8Unorm;
let usage = wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING;
{
let mut scope = super::FrameScope::new(&pool, &device);
let a = scope.acquire(16, 16, format, usage);
let _b = scope.acquire(16, 16, format, usage);
let taken = scope.take(a);
assert!(
taken.is_some(),
"take must return the texture at a valid index"
);
}
let mut guard = pool.lock().unwrap_or_else(PoisonError::into_inner);
let allocs = guard.alloc_count();
assert_eq!(allocs, 2, "the two scope acquires were pool misses");
let reused = guard.acquire(&device, 16, 16, format, usage);
assert_eq!(
guard.alloc_count(),
allocs,
"the released texture must be reused, not allocated"
);
let _fresh = guard.acquire(&device, 16, 16, format, usage);
assert_eq!(
guard.alloc_count(),
allocs + 1,
"the taken texture was not returned to the pool, so this acquire allocates"
);
drop(reused);
}
#[test]
fn texture_pool_should_reuse_a_released_texture_for_the_same_key() {
let Some(device) = device() else {
return;
};
let mut pool = TexturePool::new();
let key = (
16,
16,
wgpu::TextureFormat::Rgba8Unorm,
wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
);
let texture = pool.acquire(&device, key.0, key.1, key.2, key.3);
assert_eq!(pool.alloc_count(), 1, "the first acquire is a pool miss");
pool.release(key, texture);
let _reused = pool.acquire(&device, key.0, key.1, key.2, key.3);
assert_eq!(
pool.alloc_count(),
1,
"releasing then re-acquiring the same key must reuse, not allocate"
);
}
#[test]
fn texture_pool_should_key_formats_distinctly() {
let Some(device) = device() else {
return;
};
let mut pool = TexturePool::new();
let usage = wgpu::TextureUsages::COPY_DST;
let rgba8 = pool.acquire(&device, 16, 16, wgpu::TextureFormat::Rgba8Unorm, usage);
pool.release((16, 16, wgpu::TextureFormat::Rgba8Unorm, usage), rgba8);
let _rgba16 = pool.acquire(&device, 16, 16, wgpu::TextureFormat::Rgba16Float, usage);
assert_eq!(
pool.alloc_count(),
2,
"a distinct format must allocate its own texture"
);
}
}