use std::collections::HashMap;
use crate::mipmap::build_mip_chain;
#[derive(Default)]
pub struct ImageManager {
images: HashMap<String, ImageEntry>,
#[cfg(debug_assertions)]
texture_count_warning: TextureCountWarning,
}
#[cfg(debug_assertions)]
const TEXTURE_COUNT_WARNING_THRESHOLD: usize = 256;
#[cfg(debug_assertions)]
#[derive(Debug, Default)]
struct TextureCountWarning {
warned: bool,
}
#[cfg(debug_assertions)]
impl TextureCountWarning {
fn should_warn(&mut self, count: usize) -> bool {
if self.warned || count <= TEXTURE_COUNT_WARNING_THRESHOLD {
return false;
}
self.warned = true;
true
}
}
struct ImageEntry {
_texture: wgpu::Texture,
bind_group: wgpu::BindGroup,
}
impl ImageManager {
pub fn new() -> Self {
Self::default()
}
#[allow(clippy::too_many_arguments)]
pub fn register_image(
&mut self,
name: &str,
width: u32,
height: u32,
pixels: &[u8],
device: &wgpu::Device,
queue: &wgpu::Queue,
bind_group_layout: &wgpu::BindGroupLayout,
) {
if width == 0 || height == 0 {
return;
}
let mips = build_mip_chain(pixels, width, height);
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("image_texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1 + mips.len() as u32,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let upload = |level: u32, w: u32, h: u32, data: &[u8]| {
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: level,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(w * 4),
rows_per_image: Some(h),
},
wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
);
};
upload(0, width, height, pixels);
for (level, (w, h, data)) in mips.iter().enumerate() {
upload(level as u32 + 1, *w, *h, data);
}
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("image_bind_group"),
layout: bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
self.images.insert(
name.to_string(),
ImageEntry {
_texture: texture,
bind_group,
},
);
#[cfg(debug_assertions)]
if self.texture_count_warning.should_warn(self.images.len()) {
eprintln!(
"teksilo-render: this window holds {} image textures and frees none before it \
closes, because every decoded RasterIcon and every ImageWidget::from_raw gets a \
texture of its own; decode each image once and clone the RasterIcon.",
self.images.len()
);
}
}
pub fn get_bind_group(&self, name: &str) -> Option<&wgpu::BindGroup> {
self.images.get(name).map(|e| &e.bind_group)
}
pub fn contains(&self, name: &str) -> bool {
self.images.contains_key(name)
}
pub fn remove(&mut self, name: &str) {
self.images.remove(name);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn image_manager_new_is_empty() {
let mgr = ImageManager::new();
assert!(!mgr.contains("test"));
assert!(mgr.get_bind_group("test").is_none());
}
#[cfg(debug_assertions)]
#[test]
fn texture_count_warning_fires_once_past_the_threshold() {
let mut warning = TextureCountWarning::default();
for count in 0..=TEXTURE_COUNT_WARNING_THRESHOLD {
assert!(!warning.should_warn(count), "warned at {count}");
}
assert!(
warning.should_warn(TEXTURE_COUNT_WARNING_THRESHOLD + 1),
"must warn as the count first goes past the threshold"
);
for count in [
TEXTURE_COUNT_WARNING_THRESHOLD + 1,
TEXTURE_COUNT_WARNING_THRESHOLD + 2,
TEXTURE_COUNT_WARNING_THRESHOLD,
10 * TEXTURE_COUNT_WARNING_THRESHOLD,
] {
assert!(
!warning.should_warn(count),
"warned a second time at {count}"
);
}
}
}