easy_wgpu/
mipmap.rs

1//mostly from
2// https://github.com/jshrake/wgpu-mipmap/blob/main/src/backends/render.rs
3
4use crate::utils::get_mip_extent;
5use std::collections::HashMap;
6use thiserror::Error;
7use wgpu::{
8    util::make_spirv, AddressMode, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry,
9    BindingResource, BindingType, CommandEncoder, Device, FilterMode, FragmentState, FrontFace, LoadOp, MultisampleState, Operations,
10    PipelineLayoutDescriptor, PrimitiveState, RenderPassDescriptor, RenderPipeline, RenderPipelineDescriptor, Sampler, SamplerDescriptor,
11    ShaderModuleDescriptor, Texture, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages,
12    TextureViewDescriptor, TextureViewDimension, VertexState,
13};
14
15/// Generates mipmaps for textures with output attachment usage.
16#[derive(Debug)]
17pub struct RenderMipmapGenerator {
18    sampler: Sampler,
19    layout_cache: HashMap<TextureSampleType, BindGroupLayout>,
20    pipeline_cache: HashMap<TextureFormat, RenderPipeline>,
21}
22
23#[allow(clippy::match_same_arms)]
24fn to_sample_type(format: TextureFormat) -> TextureSampleType {
25    match format {
26        TextureFormat::R8Uint
27        | TextureFormat::R16Uint
28        | TextureFormat::Rg8Uint
29        | TextureFormat::R32Uint
30        | TextureFormat::Rg16Uint
31        | TextureFormat::Rgba8Uint
32        | TextureFormat::Rg32Uint
33        | TextureFormat::Rgba16Uint
34        | TextureFormat::Rgba32Uint => TextureSampleType::Uint,
35
36        TextureFormat::R8Sint
37        | TextureFormat::R16Sint
38        | TextureFormat::Rg8Sint
39        | TextureFormat::R32Sint
40        | TextureFormat::Rg16Sint
41        | TextureFormat::Rgba8Sint
42        | TextureFormat::Rg32Sint
43        | TextureFormat::Rgba16Sint
44        | TextureFormat::Rgba32Sint => TextureSampleType::Sint,
45
46        TextureFormat::R8Unorm
47        | TextureFormat::R8Snorm
48        | TextureFormat::R16Float
49        | TextureFormat::Rg8Unorm
50        | TextureFormat::Rg8Snorm
51        | TextureFormat::R32Float
52        | TextureFormat::Rg16Float
53        | TextureFormat::Rgba8Unorm
54        | TextureFormat::Rgba8UnormSrgb
55        | TextureFormat::Rgba8Snorm
56        | TextureFormat::Bgra8Unorm
57        | TextureFormat::Bgra8UnormSrgb
58        | TextureFormat::Rgb10a2Unorm
59        | TextureFormat::Rg11b10Float
60        | TextureFormat::Rg32Float
61        | TextureFormat::Rgba16Float
62        | TextureFormat::Rgba32Float
63        | TextureFormat::Depth32Float
64        | TextureFormat::Depth24Plus
65        | TextureFormat::Depth24PlusStencil8
66        | TextureFormat::Bc1RgbaUnorm
67        | TextureFormat::Bc1RgbaUnormSrgb
68        | TextureFormat::Bc2RgbaUnorm
69        | TextureFormat::Bc2RgbaUnormSrgb
70        | TextureFormat::Bc3RgbaUnorm
71        | TextureFormat::Bc3RgbaUnormSrgb
72        | TextureFormat::Bc4RUnorm
73        | TextureFormat::Bc4RSnorm
74        | TextureFormat::Bc5RgUnorm
75        | TextureFormat::Bc5RgSnorm
76        | TextureFormat::Bc6hRgbUfloat
77        // | TextureFormat::Bc6hRgbSfloat
78        | TextureFormat::Bc7RgbaUnorm
79        | TextureFormat::Bc7RgbaUnormSrgb
80        // | TextureFormat::Etc2RgbUnorm
81        // | TextureFormat::Etc2RgbUnormSrgb
82        // | TextureFormat::Etc2RgbA1Unorm
83        // | TextureFormat::Etc2RgbA1UnormSrgb
84        // | TextureFormat::Etc2RgbA8Unorm
85        // | TextureFormat::Etc2RgbA8UnormSrgb
86        // | TextureFormat::EacRUnorm
87        // | TextureFormat::EacRSnorm
88        // | TextureFormat::EtcRgUnorm
89        // | TextureFormat::EtcRgSnorm
90        // | TextureFormat::Astc4x4RgbaUnorm
91        // | TextureFormat::Astc4x4RgbaUnormSrgb
92        // | TextureFormat::Astc5x4RgbaUnorm
93        // | TextureFormat::Astc5x4RgbaUnormSrgb
94        // | TextureFormat::Astc5x5RgbaUnorm
95        // | TextureFormat::Astc5x5RgbaUnormSrgb
96        // | TextureFormat::Astc6x5RgbaUnorm
97        // | TextureFormat::Astc6x5RgbaUnormSrgb
98        // | TextureFormat::Astc6x6RgbaUnorm
99        // | TextureFormat::Astc6x6RgbaUnormSrgb
100        // | TextureFormat::Astc8x5RgbaUnorm
101        // | TextureFormat::Astc8x5RgbaUnormSrgb
102        // | TextureFormat::Astc8x6RgbaUnorm
103        // | TextureFormat::Astc8x6RgbaUnormSrgb
104        // | TextureFormat::Astc10x5RgbaUnorm
105        // | TextureFormat::Astc10x5RgbaUnormSrgb
106        // | TextureFormat::Astc10x6RgbaUnorm
107        // | TextureFormat::Astc10x6RgbaUnormSrgb
108        // | TextureFormat::Astc8x8RgbaUnorm
109        // | TextureFormat::Astc8x8RgbaUnormSrgb
110        // | TextureFormat::Astc10x8RgbaUnorm
111        // | TextureFormat::Astc10x8RgbaUnormSrgb
112        // | TextureFormat::Astc10x10RgbaUnorm
113        // | TextureFormat::Astc10x10RgbaUnormSrgb
114        // | TextureFormat::Astc12x10RgbaUnorm
115        // | TextureFormat::Astc12x10RgbaUnormSrgb
116        // | TextureFormat::Astc12x12RgbaUnorm
117        // | TextureFormat::Astc12x12RgbaUnormSrgb
118         => TextureSampleType::Float { filterable: true },
119
120        _ => TextureSampleType::Float { filterable: true },
121    }
122}
123
124impl RenderMipmapGenerator {
125    /// Returns the texture usage `RenderMipmapGenerator` requires for mipmap
126    /// generation.
127    pub fn required_usage() -> TextureUsages {
128        TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING
129    }
130
131    /// Creates a new `RenderMipmapGenerator`. Once created, it can be used
132    /// repeatedly to generate mipmaps for any texture with format specified
133    /// in `format_hints`.
134    #[allow(clippy::too_many_lines)]
135    pub fn new_with_format_hints(device: &Device, format_hints: &[TextureFormat]) -> Self {
136        // A sampler for box filter with clamp to edge behavior
137        // In practice, the final result may be implementation dependent
138        // - [Vulkan](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#textures-texel-linear-filtering)
139        // - [Metal](https://developer.apple.com/documentation/metal/mtlsamplerminmagfilter/linear)
140        // - [DX12](https://docs.microsoft.com/en-us/windows/win32/api/d3d12/ne-d3d12-d3d12_filter)
141        let sampler = device.create_sampler(&SamplerDescriptor {
142            label: Some("wgpu-mipmap-sampler"),
143            address_mode_u: AddressMode::ClampToEdge,
144            address_mode_v: AddressMode::ClampToEdge,
145            address_mode_w: AddressMode::ClampToEdge,
146            mag_filter: FilterMode::Linear,
147            min_filter: FilterMode::Nearest,
148            mipmap_filter: FilterMode::Nearest,
149            ..Default::default()
150        });
151
152        let render_layout_cache = {
153            let mut layout_cache = HashMap::new();
154            // For now, we only cache a bind group layout for floating-point textures
155            #[allow(clippy::single_element_loop)]
156            for &sample_type in &[TextureSampleType::Float { filterable: true }] {
157                let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
158                    label: Some(&format!("wgpu-mipmap-bg-layout-{sample_type:?}")),
159                    entries: &[
160                        BindGroupLayoutEntry {
161                            binding: 0,
162                            visibility: wgpu::ShaderStages::FRAGMENT,
163                            ty: BindingType::Texture {
164                                view_dimension: TextureViewDimension::D2,
165                                sample_type,
166                                multisampled: false,
167                            },
168                            count: None,
169                        },
170                        BindGroupLayoutEntry {
171                            binding: 1,
172                            visibility: wgpu::ShaderStages::FRAGMENT,
173                            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
174                            count: None,
175                        },
176                    ],
177                });
178                layout_cache.insert(sample_type, bind_group_layout);
179            }
180            layout_cache
181        };
182
183        let render_pipeline_cache = {
184            let mut pipeline_cache = HashMap::new();
185            let vertex_module = device.create_shader_module(ShaderModuleDescriptor {
186                label: None,
187                source: make_spirv(include_bytes!("../shaders/triangle.vert.spv")),
188            });
189            let box_filter = device.create_shader_module(ShaderModuleDescriptor {
190                label: None,
191                source: make_spirv(include_bytes!("../shaders/box.frag.spv")),
192            });
193            for format in format_hints {
194                let fragment_module = &box_filter;
195
196                let sample_type = to_sample_type(*format);
197                if let Some(bind_group_layout) = render_layout_cache.get(&sample_type) {
198                    let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
199                        label: None,
200                        bind_group_layouts: &[bind_group_layout],
201                        push_constant_ranges: &[],
202                    });
203                    let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
204                        label: Some(&format!("wgpu-mipmap-render-pipeline-{format:?}")),
205                        layout: Some(&layout),
206                        vertex: VertexState {
207                            module: &vertex_module,
208                            entry_point: "main",
209                            buffers: &[],
210                            compilation_options: wgpu::PipelineCompilationOptions::default(),
211                        },
212                        primitive: PrimitiveState {
213                            topology: wgpu::PrimitiveTopology::TriangleList,
214                            front_face: FrontFace::Ccw,
215                            cull_mode: Some(wgpu::Face::Back),
216                            ..Default::default()
217                        },
218                        depth_stencil: None,
219                        multisample: MultisampleState {
220                            count: 1,
221                            mask: !0,
222                            alpha_to_coverage_enabled: false,
223                        },
224                        fragment: Some(FragmentState {
225                            module: fragment_module,
226                            entry_point: "main",
227                            targets: &[Some(wgpu::ColorTargetState {
228                                format: *format,
229                                blend: None,
230                                write_mask: wgpu::ColorWrites::ALL,
231                            })],
232                            compilation_options: wgpu::PipelineCompilationOptions::default(),
233                        }),
234                        multiview: None,
235                        cache: None,
236                    });
237                    pipeline_cache.insert(*format, pipeline);
238                } else {
239                    log::warn!("RenderMipmapGenerator does not support requested format {:?}", format);
240                    continue;
241                }
242            }
243            pipeline_cache
244        };
245
246        Self {
247            sampler,
248            layout_cache: render_layout_cache,
249            pipeline_cache: render_pipeline_cache,
250        }
251    }
252
253    /// Generate mipmaps from level 0 of `src_texture` to
254    /// levels `dst_mip_offset..dst_texture_descriptor.mip_level_count`
255    // of `dst_texture`.
256    #[allow(clippy::too_many_arguments)]
257    #[allow(clippy::too_many_lines)]
258    pub(crate) fn generate_src_dst(
259        &self,
260        device: &Device,
261        encoder: &mut CommandEncoder,
262        src_texture: &Texture,
263        dst_texture: &Texture,
264        src_texture_descriptor: &TextureDescriptor,
265        dst_texture_descriptor: &TextureDescriptor,
266        dst_mip_offset: u32,
267    ) -> Result<(), Error> {
268        let src_format = src_texture_descriptor.format;
269        let src_mip_count = src_texture_descriptor.mip_level_count;
270        let src_ext = src_texture_descriptor.size;
271        let src_dim = src_texture_descriptor.dimension;
272        let src_usage = src_texture_descriptor.usage;
273        let src_next_mip_ext = get_mip_extent(&src_ext, 1);
274
275        let dst_format = dst_texture_descriptor.format;
276        let dst_mip_count = dst_texture_descriptor.mip_level_count;
277        let dst_ext = dst_texture_descriptor.size;
278        let dst_dim = dst_texture_descriptor.dimension;
279        let dst_usage = dst_texture_descriptor.usage;
280        // invariants that we expect callers to uphold
281        if src_format != dst_format {
282            dbg!(src_texture_descriptor);
283            dbg!(dst_texture_descriptor);
284            panic!("src and dst texture formats must be equal");
285        }
286        if src_dim != dst_dim {
287            dbg!(src_texture_descriptor);
288            dbg!(dst_texture_descriptor);
289            panic!("src and dst texture dimensions must be eqaul");
290        }
291        if !((src_mip_count == dst_mip_count && src_ext == dst_ext) || (src_next_mip_ext == dst_ext)) {
292            dbg!(src_texture_descriptor);
293            dbg!(dst_texture_descriptor);
294            panic!("src and dst texture extents must match or dst must be half the size of src");
295        }
296
297        if src_dim != TextureDimension::D2 {
298            return Err(Error::UnsupportedDimension(src_dim));
299        }
300        // src texture must be sampled
301        if !src_usage.contains(TextureUsages::TEXTURE_BINDING) {
302            return Err(Error::UnsupportedUsage(src_usage));
303        }
304        // dst texture must be sampled and output attachment
305        if !dst_usage.contains(Self::required_usage()) {
306            return Err(Error::UnsupportedUsage(dst_usage));
307        }
308        let format = src_format;
309        let pipeline = self.pipeline_cache.get(&format).ok_or(Error::UnknownFormat(format))?;
310        let sample_type = to_sample_type(format);
311        let layout = self.layout_cache.get(&sample_type).ok_or(Error::UnknownFormat(format))?;
312        let views = (0..src_mip_count)
313            .map(|mip_level| {
314                // The first view is mip level 0 of the src texture
315                // Subsequent views are for the dst_texture
316                let (texture, base_mip_level) = if mip_level == 0 {
317                    (src_texture, 0)
318                } else {
319                    (dst_texture, mip_level - dst_mip_offset)
320                };
321                texture.create_view(&TextureViewDescriptor {
322                    label: None,
323                    format: None,
324                    dimension: None,
325                    aspect: TextureAspect::All,
326                    base_mip_level,
327                    mip_level_count: Some(1),
328                    array_layer_count: None,
329                    base_array_layer: 0,
330                })
331            })
332            .collect::<Vec<_>>();
333        for mip in 1..src_mip_count as usize {
334            let src_view = &views[mip - 1];
335            let dst_view = &views[mip];
336            let bind_group = device.create_bind_group(&BindGroupDescriptor {
337                label: None,
338                layout,
339                entries: &[
340                    BindGroupEntry {
341                        binding: 0,
342                        resource: BindingResource::TextureView(src_view),
343                    },
344                    BindGroupEntry {
345                        binding: 1,
346                        resource: BindingResource::Sampler(&self.sampler),
347                    },
348                ],
349            });
350            let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
351                label: None,
352                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
353                    view: dst_view,
354                    resolve_target: None,
355                    ops: Operations {
356                        load: LoadOp::Load,
357                        store: wgpu::StoreOp::Store,
358                    },
359                })],
360                depth_stencil_attachment: None,
361                timestamp_writes: None,
362                occlusion_query_set: None,
363            });
364            pass.set_pipeline(pipeline);
365            pass.set_bind_group(0, &bind_group, &[]);
366            pass.draw(0..3, 0..1);
367        }
368        Ok(())
369    }
370}
371
372impl RenderMipmapGenerator {
373    /// # Errors
374    ///
375    /// Will return `Err` if the texture cannot be mipmapped
376    pub fn generate(
377        &self,
378        device: &Device,
379        encoder: &mut CommandEncoder,
380        texture: &Texture,
381        texture_descriptor: &TextureDescriptor,
382    ) -> Result<(), Error> {
383        self.generate_src_dst(device, encoder, texture, texture, texture_descriptor, texture_descriptor, 0)
384    }
385}
386
387/// An error that occurred during mipmap generation.
388#[derive(Debug, Error, PartialEq, Eq)]
389pub enum Error {
390    #[error("Unsupported texture usage `{0:?}`.\nYour texture usage must contain one of: 1. TextureUsage::STORAGE, 2. TextureUsage::OUTPUT_ATTACHMENT | TextureUsage::SAMPLED, 3. TextureUsage::COPY_SRC | TextureUsage::COPY_DST")]
391    UnsupportedUsage(wgpu::TextureUsages),
392    #[error("Unsupported texture dimension `{0:?}. You texture dimension must be TextureDimension::D2`")]
393    UnsupportedDimension(wgpu::TextureDimension),
394    #[error("Unsupported texture format `{0:?}`. Try using the render backend.")]
395    UnsupportedFormat(wgpu::TextureFormat),
396    #[error("Unsupported texture size. Texture size must be a power of 2.")]
397    NpotTexture,
398    #[error("Unknown texture format `{0:?}`.\nDid you mean to specify it in `MipmapGeneratorDescriptor::formats`?")]
399    UnknownFormat(wgpu::TextureFormat),
400}