wgpu_mipmap/backends/
render.rs

1use crate::core::*;
2use crate::util::get_mip_extent;
3use log::warn;
4use std::collections::HashMap;
5
6/// Generates mipmaps for textures with output attachment usage.
7#[derive(Debug)]
8pub struct RenderMipmapGenerator {
9    sampler: wgpu::Sampler,
10    layout_cache: HashMap<wgpu::TextureComponentType, wgpu::BindGroupLayout>,
11    pipeline_cache: HashMap<wgpu::TextureFormat, wgpu::RenderPipeline>,
12}
13
14impl RenderMipmapGenerator {
15    /// Returns the texture usage `RenderMipmapGenerator` requires for mipmap generation.
16    pub fn required_usage() -> wgpu::TextureUsage {
17        wgpu::TextureUsage::OUTPUT_ATTACHMENT | wgpu::TextureUsage::SAMPLED
18    }
19
20    /// Creates a new `RenderMipmapGenerator`. Once created, it can be used repeatedly to
21    /// generate mipmaps for any texture with format specified in `format_hints`.
22    pub fn new_with_format_hints(
23        device: &wgpu::Device,
24        format_hints: &[wgpu::TextureFormat],
25    ) -> Self {
26        // A sampler for box filter with clamp to edge behavior
27        // In practice, the final result may be implementation dependent
28        // - [Vulkan](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#textures-texel-linear-filtering)
29        // - [Metal](https://developer.apple.com/documentation/metal/mtlsamplerminmagfilter/linear)
30        // - [DX12](https://docs.microsoft.com/en-us/windows/win32/api/d3d12/ne-d3d12-d3d12_filter)
31        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
32            label: Some(&"wgpu-mipmap-sampler"),
33            address_mode_u: wgpu::AddressMode::ClampToEdge,
34            address_mode_v: wgpu::AddressMode::ClampToEdge,
35            address_mode_w: wgpu::AddressMode::ClampToEdge,
36            mag_filter: wgpu::FilterMode::Linear,
37            min_filter: wgpu::FilterMode::Nearest,
38            mipmap_filter: wgpu::FilterMode::Nearest,
39            ..Default::default()
40        });
41
42        let render_layout_cache = {
43            let mut layout_cache = HashMap::new();
44            // For now, we only cache a bind group layout for floating-point textures
45            for component_type in &[wgpu::TextureComponentType::Float] {
46                let bind_group_layout =
47                    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
48                        label: Some(&format!("wgpu-mipmap-bg-layout-{:?}", component_type)),
49                        entries: &[
50                            wgpu::BindGroupLayoutEntry {
51                                binding: 0,
52                                visibility: wgpu::ShaderStage::FRAGMENT,
53                                ty: wgpu::BindingType::SampledTexture {
54                                    dimension: wgpu::TextureViewDimension::D2,
55                                    component_type: *component_type,
56                                    multisampled: false,
57                                },
58                                count: None,
59                            },
60                            wgpu::BindGroupLayoutEntry {
61                                binding: 1,
62                                visibility: wgpu::ShaderStage::FRAGMENT,
63                                ty: wgpu::BindingType::Sampler { comparison: false },
64                                count: None,
65                            },
66                        ],
67                    });
68                layout_cache.insert(*component_type, bind_group_layout);
69            }
70            layout_cache
71        };
72
73        let render_pipeline_cache = {
74            let mut pipeline_cache = HashMap::new();
75            let vertex_module = device.create_shader_module(wgpu::util::make_spirv(
76                include_bytes!("shaders/triangle.vert.spv"),
77            ));
78            let box_filter = device.create_shader_module(wgpu::util::make_spirv(include_bytes!(
79                "shaders/box.frag.spv"
80            )));
81            for format in format_hints {
82                let fragment_module = &box_filter;
83
84                let component_type = wgpu::TextureComponentType::from(*format);
85                if let Some(bind_group_layout) = render_layout_cache.get(&component_type) {
86                    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
87                        label: None,
88                        bind_group_layouts: &[bind_group_layout],
89                        push_constant_ranges: &[],
90                    });
91                    let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
92                        label: Some(&format!("wgpu-mipmap-render-pipeline-{:?}", format)),
93                        layout: Some(&layout),
94                        vertex_stage: wgpu::ProgrammableStageDescriptor {
95                            module: &vertex_module,
96                            entry_point: "main",
97                        },
98                        fragment_stage: Some(wgpu::ProgrammableStageDescriptor {
99                            module: &fragment_module,
100                            entry_point: "main",
101                        }),
102                        rasterization_state: Some(wgpu::RasterizationStateDescriptor {
103                            front_face: wgpu::FrontFace::Ccw,
104                            cull_mode: wgpu::CullMode::Back,
105                            ..Default::default()
106                        }),
107                        primitive_topology: wgpu::PrimitiveTopology::TriangleList,
108                        color_states: &[(*format).into()],
109                        depth_stencil_state: None,
110                        vertex_state: wgpu::VertexStateDescriptor {
111                            index_format: wgpu::IndexFormat::Uint16,
112                            vertex_buffers: &[],
113                        },
114                        sample_count: 1,
115                        sample_mask: !0,
116                        alpha_to_coverage_enabled: false,
117                    });
118                    pipeline_cache.insert(*format, pipeline);
119                } else {
120                    warn!(
121                        "RenderMipmapGenerator does not support requested format {:?}",
122                        format
123                    );
124                    continue;
125                }
126            }
127            pipeline_cache
128        };
129
130        Self {
131            sampler,
132            layout_cache: render_layout_cache,
133            pipeline_cache: render_pipeline_cache,
134        }
135    }
136
137    /// Generate mipmaps from level 0 of `src_texture` to
138    /// levels `dst_mip_offset..dst_texture_descriptor.mip_level_count`
139    // of `dst_texture`.
140    #[allow(clippy::too_many_arguments)]
141    pub(crate) fn generate_src_dst(
142        &self,
143        device: &wgpu::Device,
144        encoder: &mut wgpu::CommandEncoder,
145        src_texture: &wgpu::Texture,
146        dst_texture: &wgpu::Texture,
147        src_texture_descriptor: &wgpu::TextureDescriptor,
148        dst_texture_descriptor: &wgpu::TextureDescriptor,
149        dst_mip_offset: u32,
150    ) -> Result<(), Error> {
151        let src_format = src_texture_descriptor.format;
152        let src_mip_count = src_texture_descriptor.mip_level_count;
153        let src_ext = src_texture_descriptor.size;
154        let src_dim = src_texture_descriptor.dimension;
155        let src_usage = src_texture_descriptor.usage;
156        let src_next_mip_ext = get_mip_extent(&src_ext, 1);
157
158        let dst_format = dst_texture_descriptor.format;
159        let dst_mip_count = dst_texture_descriptor.mip_level_count;
160        let dst_ext = dst_texture_descriptor.size;
161        let dst_dim = dst_texture_descriptor.dimension;
162        let dst_usage = dst_texture_descriptor.usage;
163        // invariants that we expect callers to uphold
164        if src_format != dst_format {
165            dbg!(src_texture_descriptor);
166            dbg!(dst_texture_descriptor);
167            panic!("src and dst texture formats must be equal");
168        }
169        if src_dim != dst_dim {
170            dbg!(src_texture_descriptor);
171            dbg!(dst_texture_descriptor);
172            panic!("src and dst texture dimensions must be eqaul");
173        }
174        if !((src_mip_count == dst_mip_count && src_ext == dst_ext)
175            || (src_next_mip_ext == dst_ext))
176        {
177            dbg!(src_texture_descriptor);
178            dbg!(dst_texture_descriptor);
179            panic!("src and dst texture extents must match or dst must be half the size of src");
180        }
181
182        if src_dim != wgpu::TextureDimension::D2 {
183            return Err(Error::UnsupportedDimension(src_dim));
184        }
185        // src texture must be sampled
186        if !src_usage.contains(wgpu::TextureUsage::SAMPLED) {
187            return Err(Error::UnsupportedUsage(src_usage));
188        }
189        // dst texture must be sampled and output attachment
190        if !dst_usage.contains(Self::required_usage()) {
191            return Err(Error::UnsupportedUsage(dst_usage));
192        }
193        let format = src_format;
194        let pipeline = self
195            .pipeline_cache
196            .get(&format)
197            .ok_or(Error::UnknownFormat(format))?;
198        let component_type = wgpu::TextureComponentType::from(format);
199        let layout = self
200            .layout_cache
201            .get(&component_type)
202            .ok_or(Error::UnknownFormat(format))?;
203        let views = (0..src_mip_count)
204            .map(|mip_level| {
205                // The first view is mip level 0 of the src texture
206                // Subsequent views are for the dst_texture
207                let (texture, base_mip_level) = if mip_level == 0 {
208                    (src_texture, 0)
209                } else {
210                    (dst_texture, mip_level - dst_mip_offset)
211                };
212                texture.create_view(&wgpu::TextureViewDescriptor {
213                    label: None,
214                    format: None,
215                    dimension: None,
216                    aspect: wgpu::TextureAspect::All,
217                    base_mip_level,
218                    level_count: std::num::NonZeroU32::new(1),
219                    array_layer_count: None,
220                    base_array_layer: 0,
221                })
222            })
223            .collect::<Vec<_>>();
224        for mip in 1..src_mip_count as usize {
225            let src_view = &views[mip - 1];
226            let dst_view = &views[mip];
227            let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
228                label: None,
229                layout,
230                entries: &[
231                    wgpu::BindGroupEntry {
232                        binding: 0,
233                        resource: wgpu::BindingResource::TextureView(&src_view),
234                    },
235                    wgpu::BindGroupEntry {
236                        binding: 1,
237                        resource: wgpu::BindingResource::Sampler(&self.sampler),
238                    },
239                ],
240            });
241            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
242                color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor {
243                    attachment: &dst_view,
244                    resolve_target: None,
245                    ops: wgpu::Operations {
246                        load: wgpu::LoadOp::Load,
247                        store: true,
248                    },
249                }],
250                depth_stencil_attachment: None,
251            });
252            pass.set_pipeline(pipeline);
253            pass.set_bind_group(0, &bind_group, &[]);
254            pass.draw(0..3, 0..1);
255        }
256        Ok(())
257    }
258}
259
260impl MipmapGenerator for RenderMipmapGenerator {
261    fn generate(
262        &self,
263        device: &wgpu::Device,
264        encoder: &mut wgpu::CommandEncoder,
265        texture: &wgpu::Texture,
266        texture_descriptor: &wgpu::TextureDescriptor,
267    ) -> Result<(), Error> {
268        self.generate_src_dst(
269            device,
270            encoder,
271            &texture,
272            &texture,
273            &texture_descriptor,
274            &texture_descriptor,
275            0,
276        )
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::util::*;
284
285    fn init() {
286        let _ = env_logger::builder().is_test(true).try_init();
287    }
288
289    #[allow(dead_code)]
290    async fn generate_and_copy_to_cpu_render(
291        buffer: &[u8],
292        texture_descriptor: &wgpu::TextureDescriptor<'_>,
293    ) -> Result<Vec<MipBuffer>, Error> {
294        let (_instance, _adaptor, device, queue) = wgpu_setup().await;
295        let generator = crate::backends::RenderMipmapGenerator::new_with_format_hints(
296            &device,
297            &[texture_descriptor.format],
298        );
299        Ok(
300            generate_and_copy_to_cpu(&device, &queue, &generator, buffer, texture_descriptor)
301                .await?,
302        )
303    }
304
305    async fn generate_test(texture_descriptor: &wgpu::TextureDescriptor<'_>) -> Result<(), Error> {
306        let (_instance, _adapter, device, _queue) = wgpu_setup().await;
307        let generator =
308            RenderMipmapGenerator::new_with_format_hints(&device, &[texture_descriptor.format]);
309        let texture = device.create_texture(&texture_descriptor);
310        let mut encoder = device.create_command_encoder(&Default::default());
311        generator.generate(&device, &mut encoder, &texture, &texture_descriptor)
312    }
313
314    #[test]
315    fn sanity_check() {
316        init();
317        // Generate texture data on the CPU
318        let size = 512;
319        let mip_level_count = 1 + (size as f32).log2() as u32;
320        // Create a texture
321        let format = wgpu::TextureFormat::R8Unorm;
322        let texture_extent = wgpu::Extent3d {
323            width: size,
324            height: size,
325            depth: 1,
326        };
327        let texture_descriptor = wgpu::TextureDescriptor {
328            size: texture_extent,
329            mip_level_count,
330            format,
331            sample_count: 1,
332            dimension: wgpu::TextureDimension::D2,
333            usage: RenderMipmapGenerator::required_usage(),
334            label: None,
335        };
336        futures::executor::block_on((|| async {
337            let res = generate_test(&texture_descriptor).await;
338            assert!(res.is_ok());
339        })());
340    }
341
342    #[test]
343    fn unsupported_usage() {
344        init();
345        // Generate texture data on the CPU
346        let size = 512;
347        let mip_level_count = 1 + (size as f32).log2() as u32;
348        // Create a texture
349        let format = wgpu::TextureFormat::R8Unorm;
350        let texture_extent = wgpu::Extent3d {
351            width: size,
352            height: size,
353            depth: 1,
354        };
355        let texture_descriptor = wgpu::TextureDescriptor {
356            size: texture_extent,
357            mip_level_count,
358            format,
359            sample_count: 1,
360            dimension: wgpu::TextureDimension::D2,
361            usage: wgpu::TextureUsage::empty(),
362            label: None,
363        };
364        futures::executor::block_on((|| async {
365            let res = generate_test(&texture_descriptor).await;
366            assert!(res.is_err());
367            assert!(res.err() == Some(Error::UnsupportedUsage(wgpu::TextureUsage::empty())));
368        })());
369    }
370
371    #[test]
372    fn unknown_format() {
373        init();
374        // Generate texture data on the CPU
375        let size = 512;
376        let mip_level_count = 1 + (size as f32).log2() as u32;
377        // Create a texture
378        let format = wgpu::TextureFormat::Rgba8Sint;
379        let texture_extent = wgpu::Extent3d {
380            width: size,
381            height: size,
382            depth: 1,
383        };
384        let texture_descriptor = wgpu::TextureDescriptor {
385            size: texture_extent,
386            mip_level_count,
387            format,
388            sample_count: 1,
389            dimension: wgpu::TextureDimension::D2,
390            usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::OUTPUT_ATTACHMENT,
391            label: None,
392        };
393        futures::executor::block_on((|| async {
394            let res = generate_test(&texture_descriptor).await;
395            assert!(res.is_err());
396            assert!(res.err() == Some(Error::UnknownFormat(wgpu::TextureFormat::Rgba8Sint)));
397        })());
398    }
399}