teksilo_render/
image_manager.rs1use std::collections::HashMap;
15
16use crate::mipmap::build_mip_chain;
17
18#[derive(Default)]
25pub struct ImageManager {
26 images: HashMap<String, ImageEntry>,
27 #[cfg(debug_assertions)]
28 texture_count_warning: TextureCountWarning,
29}
30
31#[cfg(debug_assertions)]
33const TEXTURE_COUNT_WARNING_THRESHOLD: usize = 256;
34
35#[cfg(debug_assertions)]
39#[derive(Debug, Default)]
40struct TextureCountWarning {
41 warned: bool,
42}
43
44#[cfg(debug_assertions)]
45impl TextureCountWarning {
46 fn should_warn(&mut self, count: usize) -> bool {
48 if self.warned || count <= TEXTURE_COUNT_WARNING_THRESHOLD {
49 return false;
50 }
51 self.warned = true;
52 true
53 }
54}
55
56struct ImageEntry {
57 _texture: wgpu::Texture,
58 bind_group: wgpu::BindGroup,
59}
60
61impl ImageManager {
62 pub fn new() -> Self {
63 Self::default()
64 }
65
66 #[allow(clippy::too_many_arguments)]
69 pub fn register_image(
70 &mut self,
71 name: &str,
72 width: u32,
73 height: u32,
74 pixels: &[u8],
75 device: &wgpu::Device,
76 queue: &wgpu::Queue,
77 bind_group_layout: &wgpu::BindGroupLayout,
78 ) {
79 if width == 0 || height == 0 {
80 return;
81 }
82
83 let mips = build_mip_chain(pixels, width, height);
85
86 let texture = device.create_texture(&wgpu::TextureDescriptor {
87 label: Some("image_texture"),
88 size: wgpu::Extent3d {
89 width,
90 height,
91 depth_or_array_layers: 1,
92 },
93 mip_level_count: 1 + mips.len() as u32,
94 sample_count: 1,
95 dimension: wgpu::TextureDimension::D2,
96 format: wgpu::TextureFormat::Rgba8UnormSrgb,
97 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
98 view_formats: &[],
99 });
100
101 let upload = |level: u32, w: u32, h: u32, data: &[u8]| {
105 queue.write_texture(
106 wgpu::TexelCopyTextureInfo {
107 texture: &texture,
108 mip_level: level,
109 origin: wgpu::Origin3d::ZERO,
110 aspect: wgpu::TextureAspect::All,
111 },
112 data,
113 wgpu::TexelCopyBufferLayout {
114 offset: 0,
115 bytes_per_row: Some(w * 4),
116 rows_per_image: Some(h),
117 },
118 wgpu::Extent3d {
119 width: w,
120 height: h,
121 depth_or_array_layers: 1,
122 },
123 );
124 };
125 upload(0, width, height, pixels);
126 for (level, (w, h, data)) in mips.iter().enumerate() {
127 upload(level as u32 + 1, *w, *h, data);
128 }
129
130 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
131 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
137 mag_filter: wgpu::FilterMode::Linear,
138 min_filter: wgpu::FilterMode::Linear,
139 mipmap_filter: wgpu::MipmapFilterMode::Linear,
140 ..Default::default()
141 });
142
143 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
144 label: Some("image_bind_group"),
145 layout: bind_group_layout,
146 entries: &[
147 wgpu::BindGroupEntry {
148 binding: 0,
149 resource: wgpu::BindingResource::TextureView(&view),
150 },
151 wgpu::BindGroupEntry {
152 binding: 1,
153 resource: wgpu::BindingResource::Sampler(&sampler),
154 },
155 ],
156 });
157
158 self.images.insert(
159 name.to_string(),
160 ImageEntry {
161 _texture: texture,
162 bind_group,
163 },
164 );
165
166 #[cfg(debug_assertions)]
167 if self.texture_count_warning.should_warn(self.images.len()) {
168 eprintln!(
169 "teksilo-render: this window holds {} image textures and frees none before it \
170 closes, because every decoded RasterIcon and every ImageWidget::from_raw gets a \
171 texture of its own; decode each image once and clone the RasterIcon.",
172 self.images.len()
173 );
174 }
175 }
176
177 pub fn get_bind_group(&self, name: &str) -> Option<&wgpu::BindGroup> {
179 self.images.get(name).map(|e| &e.bind_group)
180 }
181
182 pub fn contains(&self, name: &str) -> bool {
184 self.images.contains_key(name)
185 }
186
187 pub fn remove(&mut self, name: &str) {
189 self.images.remove(name);
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn image_manager_new_is_empty() {
199 let mgr = ImageManager::new();
200 assert!(!mgr.contains("test"));
201 assert!(mgr.get_bind_group("test").is_none());
202 }
203
204 #[cfg(debug_assertions)]
205 #[test]
206 fn texture_count_warning_fires_once_past_the_threshold() {
207 let mut warning = TextureCountWarning::default();
208 for count in 0..=TEXTURE_COUNT_WARNING_THRESHOLD {
209 assert!(!warning.should_warn(count), "warned at {count}");
210 }
211 assert!(
212 warning.should_warn(TEXTURE_COUNT_WARNING_THRESHOLD + 1),
213 "must warn as the count first goes past the threshold"
214 );
215 for count in [
216 TEXTURE_COUNT_WARNING_THRESHOLD + 1,
217 TEXTURE_COUNT_WARNING_THRESHOLD + 2,
218 TEXTURE_COUNT_WARNING_THRESHOLD,
219 10 * TEXTURE_COUNT_WARNING_THRESHOLD,
220 ] {
221 assert!(
222 !warning.should_warn(count),
223 "warned a second time at {count}"
224 );
225 }
226 }
227}