1use std::collections::HashMap;
2use std::sync::Weak;
3
4use valo_dl::{BlendMode, ColorFilter, Filter, Image, ImageInner, MipmapMode, Sampling, TileMode};
5
6#[derive(Clone, Copy, Debug)]
8pub struct ImageDesc {
9 pub size: [u32; 2],
11 pub premultiplied: bool,
16 pub mips: bool,
20}
21
22impl Default for ImageDesc {
23 fn default() -> Self {
24 Self {
25 size: [0, 0],
26 premultiplied: false,
27 mips: true,
28 }
29 }
30}
31
32pub struct ImageStore {
37 device: wgpu::Device,
38 queue: wgpu::Queue,
39 samplers: HashMap<Sampling, wgpu::Sampler>,
40 binds: HashMap<(u64, Sampling), (Weak<ImageInner>, wgpu::BindGroup)>,
41 filtered: HashMap<(u64, ColorFilterKey), FilteredImage>,
42 frame: u64,
43 mips: MipGenerator,
44}
45
46pub const IMAGE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
48
49impl ImageStore {
50 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
52 Self {
53 device: device.clone(),
54 queue: queue.clone(),
55 samplers: HashMap::new(),
56 binds: HashMap::new(),
57 filtered: HashMap::new(),
58 frame: 0,
59 mips: MipGenerator::new(device),
60 }
61 }
62
63 pub fn filtered_image(&mut self, image: &Image, filter: ColorFilter) -> (Image, bool) {
68 self.sweep_if_crowded();
69 let key = (image.id(), ColorFilterKey::from(filter));
70 if let Some(entry) = self.filtered.get_mut(&key) {
71 entry.last_used = self.frame;
72 return (entry.image.clone(), false);
73 }
74 let texture = self.create_image_texture(image.size(), 1);
75 let filtered = Image::from_texture(texture, image.size(), 1);
76 self.filtered.insert(
77 key,
78 FilteredImage {
79 source: image.downgrade(),
80 image: filtered.clone(),
81 last_used: self.frame,
82 },
83 );
84 (filtered, true)
85 }
86
87 pub fn end_frame(&mut self) {
92 let current = self.frame;
93 self.frame += 1;
94 let before = self.filtered.len();
95 self.filtered
96 .retain(|_, entry| entry.last_used >= current && entry.source.strong_count() > 0);
97 if self.filtered.len() != before {
98 self.binds.retain(|_, (weak, _)| weak.strong_count() > 0);
101 }
102 }
103
104 pub fn upload(&mut self, desc: ImageDesc, pixels: &[u8]) -> Image {
110 let [w, h] = desc.size;
111 assert_eq!(pixels.len(), (w * h * 4) as usize, "RGBA8 pixel count");
112 let premul = premultiplied_pixels(desc.premultiplied, pixels);
113 let mip_levels = if desc.mips {
114 full_mip_count(desc.size)
115 } else {
116 1
117 };
118 let texture = self.create_image_texture(desc.size, mip_levels);
119 self.write_level_zero(&texture, desc.size, &premul);
120 if mip_levels > 1 {
121 self.mips
122 .generate(&self.device, &self.queue, &texture, desc.size, mip_levels);
123 }
124 Image::from_texture(texture, desc.size, mip_levels)
125 }
126
127 pub fn finish_external(
133 &mut self,
134 texture: wgpu::Texture,
135 size: [u32; 2],
136 mip_levels: u32,
137 ) -> Image {
138 if mip_levels > 1 {
139 self.mips
140 .generate(&self.device, &self.queue, &texture, size, mip_levels);
141 }
142 Image::from_texture(texture, size, mip_levels)
143 }
144
145 pub fn regenerate_mips(&mut self, image: &Image) {
149 if image.mip_levels() > 1 {
150 self.mips.generate(
151 &self.device,
152 &self.queue,
153 image.texture(),
154 image.size(),
155 image.mip_levels(),
156 );
157 }
158 }
159
160 pub fn create_image_texture(&self, size: [u32; 2], mip_levels: u32) -> wgpu::Texture {
165 self.device.create_texture(&wgpu::TextureDescriptor {
166 label: Some("valo.image"),
167 size: wgpu::Extent3d {
168 width: size[0],
169 height: size[1],
170 depth_or_array_layers: 1,
171 },
172 mip_level_count: mip_levels,
173 sample_count: 1,
174 dimension: wgpu::TextureDimension::D2,
175 format: IMAGE_FORMAT,
176 usage: wgpu::TextureUsages::TEXTURE_BINDING
178 | wgpu::TextureUsages::COPY_DST
179 | wgpu::TextureUsages::RENDER_ATTACHMENT,
180 view_formats: &[],
181 })
182 }
183
184 pub fn bind_group(
186 &mut self,
187 texture_layout: &wgpu::BindGroupLayout,
188 image: &Image,
189 sampling: Sampling,
190 ) -> wgpu::BindGroup {
191 self.sweep_if_crowded();
192 let key = (image.id(), sampling);
193 if let Some((_, bind)) = self.binds.get(&key) {
194 return bind.clone();
195 }
196 let sampler = self.sampler(sampling).clone();
197 let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
198 label: Some("valo.image"),
199 layout: texture_layout,
200 entries: &[
201 wgpu::BindGroupEntry {
202 binding: 0,
203 resource: wgpu::BindingResource::TextureView(image.view()),
204 },
205 wgpu::BindGroupEntry {
206 binding: 1,
207 resource: wgpu::BindingResource::Sampler(&sampler),
208 },
209 ],
210 });
211 self.binds.insert(key, (image.downgrade(), bind.clone()));
212 bind
213 }
214
215 fn sampler(&mut self, sampling: Sampling) -> &wgpu::Sampler {
216 self.samplers.entry(sampling).or_insert_with(|| {
217 let filter = match sampling.filter {
218 Filter::Linear => wgpu::FilterMode::Linear,
219 Filter::Nearest => wgpu::FilterMode::Nearest,
220 };
221 let mip_filter = match sampling.mipmap {
222 MipmapMode::Linear => wgpu::MipmapFilterMode::Linear,
223 MipmapMode::None | MipmapMode::Nearest => wgpu::MipmapFilterMode::Nearest,
224 };
225 let max_lod = match sampling.mipmap {
229 MipmapMode::None => 0.0,
230 _ => 32.0,
231 };
232 self.device.create_sampler(&wgpu::SamplerDescriptor {
233 label: Some("valo.image"),
234 address_mode_u: address_mode(sampling.tile_x),
235 address_mode_v: address_mode(sampling.tile_y),
236 mag_filter: filter,
237 min_filter: filter,
238 mipmap_filter: mip_filter,
239 lod_max_clamp: max_lod,
240 ..Default::default()
241 })
242 })
243 }
244
245 fn write_level_zero(&self, texture: &wgpu::Texture, size: [u32; 2], premul: &[u8]) {
246 self.queue.write_texture(
247 wgpu::TexelCopyTextureInfo {
248 texture,
249 mip_level: 0,
250 origin: wgpu::Origin3d::ZERO,
251 aspect: wgpu::TextureAspect::All,
252 },
253 premul,
254 wgpu::TexelCopyBufferLayout {
255 offset: 0,
256 bytes_per_row: Some(size[0] * 4),
257 rows_per_image: None,
258 },
259 wgpu::Extent3d {
260 width: size[0],
261 height: size[1],
262 depth_or_array_layers: 1,
263 },
264 );
265 }
266
267 fn sweep_if_crowded(&mut self) {
270 if self.binds.len() > 256 {
271 self.binds.retain(|_, (weak, _)| weak.strong_count() > 0);
272 }
273 }
274}
275
276struct FilteredImage {
277 source: Weak<ImageInner>,
278 image: Image,
279 last_used: u64,
280}
281
282#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
283enum ColorFilterKey {
284 Matrix([u32; 20]),
285 Blend([u32; 4], BlendMode),
286}
287
288impl From<ColorFilter> for ColorFilterKey {
289 fn from(filter: ColorFilter) -> Self {
290 match filter {
291 ColorFilter::Matrix(matrix) => Self::Matrix(matrix.map(f32::to_bits)),
292 ColorFilter::Blend(color, mode) => Self::Blend(
293 [
294 color.r.to_bits(),
295 color.g.to_bits(),
296 color.b.to_bits(),
297 color.a.to_bits(),
298 ],
299 mode,
300 ),
301 }
302 }
303}
304
305fn address_mode(tile: TileMode) -> wgpu::AddressMode {
306 match tile {
307 TileMode::Clamp | TileMode::Decal => wgpu::AddressMode::ClampToEdge,
312 TileMode::Repeat => wgpu::AddressMode::Repeat,
313 TileMode::Mirror => wgpu::AddressMode::MirrorRepeat,
314 }
315}
316
317fn premultiplied_pixels(already: bool, pixels: &[u8]) -> std::borrow::Cow<'_, [u8]> {
318 if already {
319 return std::borrow::Cow::Borrowed(pixels);
320 }
321 let mut out = pixels.to_vec();
322 for px in out.chunks_exact_mut(4) {
323 let a = px[3] as u32;
324 px[0] = ((px[0] as u32 * a) / 255) as u8;
325 px[1] = ((px[1] as u32 * a) / 255) as u8;
326 px[2] = ((px[2] as u32 * a) / 255) as u8;
327 }
328 std::borrow::Cow::Owned(out)
329}
330
331fn full_mip_count(size: [u32; 2]) -> u32 {
332 32 - size[0].max(size[1]).max(1).leading_zeros()
333}
334
335struct MipGenerator {
338 pipeline: wgpu::RenderPipeline,
339 layout: wgpu::BindGroupLayout,
340 sampler: wgpu::Sampler,
341}
342
343const MIP_SHADER: &str = r#"
344struct VsOut { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32> };
345@vertex fn vs(@builtin(vertex_index) vi: u32) -> VsOut {
346 // Fullscreen triangle.
347 let xy = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
348 var out: VsOut;
349 out.pos = vec4<f32>(xy * 2.0 - 1.0, 0.0, 1.0);
350 out.uv = vec2<f32>(xy.x, 1.0 - xy.y);
351 return out;
352}
353@group(0) @binding(0) var t: texture_2d<f32>;
354@group(0) @binding(1) var s: sampler;
355@fragment fn fs(in: VsOut) -> @location(0) vec4<f32> {
356 return textureSample(t, s, in.uv);
357}
358"#;
359
360impl MipGenerator {
361 fn new(device: &wgpu::Device) -> Self {
362 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
363 label: Some("valo.mips"),
364 source: wgpu::ShaderSource::Wgsl(MIP_SHADER.into()),
365 });
366 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
367 label: Some("valo.mips"),
368 entries: &[
369 wgpu::BindGroupLayoutEntry {
370 binding: 0,
371 visibility: wgpu::ShaderStages::FRAGMENT,
372 ty: wgpu::BindingType::Texture {
373 sample_type: wgpu::TextureSampleType::Float { filterable: true },
374 view_dimension: wgpu::TextureViewDimension::D2,
375 multisampled: false,
376 },
377 count: None,
378 },
379 wgpu::BindGroupLayoutEntry {
380 binding: 1,
381 visibility: wgpu::ShaderStages::FRAGMENT,
382 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
383 count: None,
384 },
385 ],
386 });
387 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
388 label: Some("valo.mips"),
389 bind_group_layouts: &[Some(&layout)],
390 immediate_size: 0,
391 });
392 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
393 label: Some("valo.mips"),
394 layout: Some(&pipeline_layout),
395 vertex: wgpu::VertexState {
396 module: &shader,
397 entry_point: Some("vs"),
398 compilation_options: Default::default(),
399 buffers: &[],
400 },
401 fragment: Some(wgpu::FragmentState {
402 module: &shader,
403 entry_point: Some("fs"),
404 compilation_options: Default::default(),
405 targets: &[Some(wgpu::ColorTargetState {
406 format: IMAGE_FORMAT,
407 blend: None,
408 write_mask: wgpu::ColorWrites::ALL,
409 })],
410 }),
411 primitive: wgpu::PrimitiveState::default(),
412 depth_stencil: None,
413 multisample: wgpu::MultisampleState::default(),
414 multiview_mask: None,
415 cache: None,
416 });
417 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
418 label: Some("valo.mips"),
419 mag_filter: wgpu::FilterMode::Linear,
420 min_filter: wgpu::FilterMode::Linear,
421 ..Default::default()
422 });
423 Self {
424 pipeline,
425 layout,
426 sampler,
427 }
428 }
429
430 fn generate(
431 &self,
432 device: &wgpu::Device,
433 queue: &wgpu::Queue,
434 texture: &wgpu::Texture,
435 _size: [u32; 2],
436 mip_levels: u32,
437 ) {
438 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
439 label: Some("valo.mips"),
440 });
441 for level in 1..mip_levels {
442 let (src, dst) = (
443 self.level_view(texture, level - 1),
444 self.level_view(texture, level),
445 );
446 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
447 label: Some("valo.mips"),
448 layout: &self.layout,
449 entries: &[
450 wgpu::BindGroupEntry {
451 binding: 0,
452 resource: wgpu::BindingResource::TextureView(&src),
453 },
454 wgpu::BindGroupEntry {
455 binding: 1,
456 resource: wgpu::BindingResource::Sampler(&self.sampler),
457 },
458 ],
459 });
460 self.blit_level(&mut encoder, &dst, &bind);
461 }
462 queue.submit(std::iter::once(encoder.finish()));
463 }
464
465 fn level_view(&self, texture: &wgpu::Texture, level: u32) -> wgpu::TextureView {
466 texture.create_view(&wgpu::TextureViewDescriptor {
467 base_mip_level: level,
468 mip_level_count: Some(1),
469 ..Default::default()
470 })
471 }
472
473 fn blit_level(
474 &self,
475 encoder: &mut wgpu::CommandEncoder,
476 dst: &wgpu::TextureView,
477 bind: &wgpu::BindGroup,
478 ) {
479 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
480 label: Some("valo.mips"),
481 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
482 view: dst,
483 depth_slice: None,
484 resolve_target: None,
485 ops: wgpu::Operations {
486 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
487 store: wgpu::StoreOp::Store,
488 },
489 })],
490 depth_stencil_attachment: None,
491 timestamp_writes: None,
492 occlusion_query_set: None,
493 multiview_mask: None,
494 });
495 pass.set_pipeline(&self.pipeline);
496 pass.set_bind_group(0, bind, &[]);
497 pass.draw(0..3, 0..1);
498 }
499}
500
501impl ImageStore {
502 pub(crate) fn report(&self) -> crate::PoolReport {
505 let mut seen = std::collections::HashSet::new();
506 let mut bytes = 0u64;
507 for (weak, _) in self.binds.values() {
508 let Some(inner) = weak.upgrade() else {
509 continue;
510 };
511 if !seen.insert(inner.id) {
512 continue;
513 }
514 let base = inner.size[0] as u64 * inner.size[1] as u64 * 4;
515 bytes += if inner.mip_levels > 1 {
516 base * 4 / 3
517 } else {
518 base
519 };
520 }
521 crate::PoolReport {
522 count: seen.len() as u32,
523 bytes,
524 }
525 }
526}