use std::collections::HashMap;
use crate::caps::TierCaps;
use crate::texture::{ColorAttachment, Texture, TextureDesc};
pub const SIZE_QUANTUM: u32 = 256;
pub const DEFAULT_MAX_UNUSED_FRAMES: u64 = 60;
pub trait TextureAllocator {
type Texture;
type View;
fn allocate_texture(&self, desc: &TextureDesc) -> (Self::Texture, Self::View);
}
impl TextureAllocator for wgpu::Device {
type Texture = wgpu::Texture;
type View = wgpu::TextureView;
fn allocate_texture(&self, desc: &TextureDesc) -> (wgpu::Texture, wgpu::TextureView) {
let texture = self.create_texture(&wgpu::TextureDescriptor {
label: desc.label.as_deref(),
size: wgpu::Extent3d {
width: desc.width,
height: desc.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: desc.sample_count(),
dimension: wgpu::TextureDimension::D2,
format: desc.format,
usage: desc.usage,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
(texture, view)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PoolKey {
pub width: u32,
pub height: u32,
pub format: wgpu::TextureFormat,
pub usage: wgpu::TextureUsages,
pub sample_count: u32,
}
#[derive(Clone, Debug)]
pub struct PooledTexture<T = wgpu::Texture, V = wgpu::TextureView> {
texture: Texture<T, V>,
key: PoolKey,
last_used_frame: u64,
requested: (u32, u32),
}
impl<T, V> PooledTexture<T, V> {
pub fn texture(&self) -> &Texture<T, V> {
&self.texture
}
pub fn view(&self) -> &V {
self.texture.view()
}
pub fn size(&self) -> (u32, u32) {
self.texture.size()
}
pub fn requested_size(&self) -> (u32, u32) {
self.requested
}
pub fn key(&self) -> PoolKey {
self.key
}
pub fn last_used_frame(&self) -> u64 {
self.last_used_frame
}
}
impl PooledTexture<wgpu::Texture, wgpu::TextureView> {
pub fn color_attachment(&self, clear: wgpu::Color) -> ColorAttachment<'_> {
ColorAttachment {
view: self.texture.view(),
load: wgpu::LoadOp::Clear(clear),
store: wgpu::StoreOp::Discard,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PoolStats {
pub created: u64,
pub reused: u64,
pub evicted: u64,
pub in_use: u64,
pub free: usize,
pub keys: usize,
}
#[derive(Debug)]
pub struct TexturePool<T = wgpu::Texture, V = wgpu::TextureView> {
free: HashMap<PoolKey, Vec<PooledTexture<T, V>>>,
max_dimension: u32,
transient_saves_memory: bool,
max_unused_frames: u64,
created: u64,
reused: u64,
evicted: u64,
in_use: u64,
}
impl<T, V> TexturePool<T, V> {
pub fn new(caps: &TierCaps) -> Self {
Self::with_max_unused_frames(caps, DEFAULT_MAX_UNUSED_FRAMES)
}
pub fn with_max_unused_frames(caps: &TierCaps, max_unused_frames: u64) -> Self {
Self {
free: HashMap::new(),
max_dimension: caps.max_texture_dimension_2d,
transient_saves_memory: caps.transient_saves_memory,
max_unused_frames,
created: 0,
reused: 0,
evicted: 0,
in_use: 0,
}
}
pub fn key_for(&self, desc: &TextureDesc) -> PoolKey {
let (width, height) = quantize_extent(desc.width, desc.height, self.max_dimension);
PoolKey {
width,
height,
format: desc.format,
usage: effective_usage(desc.usage, self.transient_saves_memory),
sample_count: desc.sample_count(),
}
}
pub fn acquire<A>(
&mut self,
allocator: &A,
desc: &TextureDesc,
frame: u64,
) -> PooledTexture<T, V>
where
A: TextureAllocator<Texture = T, View = V>,
{
let key = self.key_for(desc);
let requested = (desc.width, desc.height);
self.in_use += 1;
if let Some(mut entry) = self.free.get_mut(&key).and_then(Vec::pop) {
if self.free.get(&key).is_some_and(Vec::is_empty) {
self.free.remove(&key);
}
entry.last_used_frame = frame;
entry.requested = requested;
self.reused += 1;
return entry;
}
let pooled_desc = TextureDesc {
width: key.width,
height: key.height,
format: key.format,
usage: key.usage,
label: desc.label.clone(),
};
let (texture, view) = allocator.allocate_texture(&pooled_desc);
self.created += 1;
PooledTexture {
texture: Texture::new(texture, view, pooled_desc),
key,
last_used_frame: frame,
requested,
}
}
pub fn release(&mut self, texture: PooledTexture<T, V>) {
self.in_use = self.in_use.saturating_sub(1);
self.free.entry(texture.key).or_default().push(texture);
}
pub fn age(&mut self, frame: u64) {
let max_unused = self.max_unused_frames;
let mut evicted = 0u64;
self.free.retain(|_, entries| {
let before = entries.len();
entries.retain(|entry| !entry_expired(entry.last_used_frame, frame, max_unused));
evicted += (before - entries.len()) as u64;
!entries.is_empty()
});
self.evicted += evicted;
}
pub fn stats(&self) -> PoolStats {
PoolStats {
created: self.created,
reused: self.reused,
evicted: self.evicted,
in_use: self.in_use,
free: self.free.values().map(Vec::len).sum(),
keys: self.free.len(),
}
}
pub fn max_unused_frames(&self) -> u64 {
self.max_unused_frames
}
}
pub fn quantize_extent(width: u32, height: u32, max_dimension: u32) -> (u32, u32) {
(
quantize_dimension(width, max_dimension),
quantize_dimension(height, max_dimension),
)
}
fn quantize_dimension(value: u32, max_dimension: u32) -> u32 {
value
.checked_next_multiple_of(SIZE_QUANTUM)
.unwrap_or(value)
.min(max_dimension)
.max(value)
}
pub fn effective_usage(
usage: wgpu::TextureUsages,
transient_saves_memory: bool,
) -> wgpu::TextureUsages {
if transient_saves_memory && usage == wgpu::TextureUsages::RENDER_ATTACHMENT {
usage | wgpu::TextureUsages::TRANSIENT_ATTACHMENT
} else {
usage
}
}
fn entry_expired(last_used: u64, frame: u64, max_unused: u64) -> bool {
frame.saturating_sub(last_used) > max_unused
}
#[cfg(test)]
mod tests {
use super::*;
use crate::caps::DownlevelProfile;
#[test]
fn quantization_rounds_up_to_the_next_multiple() {
assert_eq!(quantize_dimension(1, 8192), 256);
assert_eq!(quantize_dimension(255, 8192), 256);
assert_eq!(quantize_dimension(256, 8192), 256);
assert_eq!(quantize_dimension(257, 8192), 512);
assert_eq!(quantize_dimension(800, 8192), 1024);
assert_eq!(quantize_dimension(2880, 8192), 3072);
assert_eq!(quantize_dimension(5120, 8192), 5120);
}
#[test]
fn quantization_never_exceeds_the_adapter_ceiling() {
assert_eq!(quantize_dimension(2000, 2000), 2000);
assert_eq!(quantize_dimension(4096, 2048), 4096);
}
#[test]
fn quantization_passes_a_zero_extent_through() {
assert_eq!(quantize_extent(0, 0, 8192), (0, 0));
}
#[test]
fn transient_is_added_only_for_a_write_only_attachment() {
let attachment = wgpu::TextureUsages::RENDER_ATTACHMENT;
assert_eq!(
effective_usage(attachment, true),
attachment | wgpu::TextureUsages::TRANSIENT_ATTACHMENT
);
assert_eq!(effective_usage(attachment, false), attachment);
let sampled = attachment | wgpu::TextureUsages::TEXTURE_BINDING;
assert_eq!(effective_usage(sampled, true), sampled);
assert_eq!(effective_usage(sampled, false), sampled);
let copied = attachment | wgpu::TextureUsages::COPY_SRC;
assert_eq!(effective_usage(copied, true), copied);
}
#[test]
fn expiry_boundary_matches_the_compositor_precedent() {
assert!(!entry_expired(7, 7, 60));
assert!(!entry_expired(7, 67, 60));
assert!(entry_expired(7, 68, 60));
assert!(!entry_expired(9, 3, 60));
}
#[test]
fn key_folds_quantization_and_transient_policy_together() {
let mut caps = TierCaps::fake(DownlevelProfile::Full);
caps.transient_saves_memory = true;
let pool: TexturePool<u32, u32> = TexturePool::new(&caps);
let desc = TextureDesc {
width: 900,
height: 700,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
label: None,
};
let key = pool.key_for(&desc);
assert_eq!((key.width, key.height), (1024, 768));
assert!(
key.usage
.contains(wgpu::TextureUsages::TRANSIENT_ATTACHMENT)
);
assert_eq!(key.sample_count, 1);
}
#[test]
fn stats_start_empty() {
let caps = TierCaps::fake(DownlevelProfile::Full);
let pool: TexturePool<u32, u32> = TexturePool::new(&caps);
assert_eq!(pool.stats(), PoolStats::default());
assert_eq!(pool.max_unused_frames(), DEFAULT_MAX_UNUSED_FRAMES);
}
}