wgpu_mipmap/backends/
recommended.rs

1use super::compute::*;
2use super::copy::*;
3use super::render::*;
4use crate::core::*;
5use log::{debug, warn};
6
7/// Generates mipmaps for textures with any usage using the compute, render, or copy backends.
8#[derive(Debug)]
9pub struct RecommendedMipmapGenerator {
10    render: RenderMipmapGenerator,
11    compute: ComputeMipmapGenerator,
12}
13
14/// A list of supported texture formats.
15const SUPPORTED_FORMATS: [wgpu::TextureFormat; 17] = {
16    use wgpu::TextureFormat;
17    [
18        TextureFormat::R8Unorm,
19        TextureFormat::R8Snorm,
20        TextureFormat::R16Float,
21        TextureFormat::Rg8Unorm,
22        TextureFormat::Rg8Snorm,
23        TextureFormat::R32Float,
24        TextureFormat::Rg16Float,
25        TextureFormat::Rgba8Unorm,
26        TextureFormat::Rgba8Snorm,
27        TextureFormat::Bgra8Unorm,
28        TextureFormat::Bgra8UnormSrgb,
29        TextureFormat::Rgba8UnormSrgb,
30        TextureFormat::Rgb10a2Unorm,
31        TextureFormat::Rg11b10Float,
32        TextureFormat::Rg32Float,
33        TextureFormat::Rgba16Float,
34        TextureFormat::Rgba32Float,
35    ]
36};
37
38impl RecommendedMipmapGenerator {
39    /// Creates a new `RecommendedMipmapGenerator`. Once created, it can be used repeatedly to
40    /// generate mipmaps for any texture with a supported format.
41    pub fn new(device: &wgpu::Device) -> Self {
42        Self::new_with_format_hints(device, &SUPPORTED_FORMATS)
43    }
44
45    /// Creates a new `RecommendedMipmapGenerator`. Once created, it can be used repeatedly to
46    /// generate mipmaps for any texture with format specified in `format_hints`.
47    pub fn new_with_format_hints(
48        device: &wgpu::Device,
49        format_hints: &[wgpu::TextureFormat],
50    ) -> Self {
51        for format in format_hints {
52            if !SUPPORTED_FORMATS.contains(&format) {
53                warn!("[RecommendedMipmapGenerator::new] No support for requested texture format {:?}", *format);
54                warn!("[RecommendedMipmapGenerator::new] Attempting to continue, but calls to generate may fail or produce unexpected results.");
55                continue;
56            }
57        }
58        let render = RenderMipmapGenerator::new_with_format_hints(device, format_hints);
59        let compute = ComputeMipmapGenerator::new_with_format_hints(device, format_hints);
60        Self { render, compute }
61    }
62}
63
64impl MipmapGenerator for RecommendedMipmapGenerator {
65    fn generate(
66        &self,
67        device: &wgpu::Device,
68        encoder: &mut wgpu::CommandEncoder,
69        texture: &wgpu::Texture,
70        texture_descriptor: &wgpu::TextureDescriptor,
71    ) -> Result<(), Error> {
72        // compute backend
73        match self
74            .compute
75            .generate(device, encoder, texture, texture_descriptor)
76        {
77            Err(e) => {
78                debug!("[RecommendedMipmapGenerator::generate] compute error {}.\n falling back to render backend.", e);
79            }
80            ok => return ok,
81        };
82        // render backend
83        match self
84            .render
85            .generate(device, encoder, texture, texture_descriptor)
86        {
87            Err(e) => {
88                debug!("[RecommendedMipmapGenerator::generate] render error {}.\n falling back to copy backend.", e);
89            }
90            ok => return ok,
91        };
92        // copy backend
93        match CopyMipmapGenerator::new(&self.render).generate(
94            device,
95            encoder,
96            texture,
97            texture_descriptor,
98        ) {
99            Err(e) => {
100                debug!("[RecommendedMipmapGenerator::generate] copy error {}.", e);
101            }
102            ok => return ok,
103        }
104        Err(Error::UnsupportedUsage(texture_descriptor.usage))
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::util::*;
112
113    fn init() {
114        let _ = env_logger::builder().is_test(true).try_init();
115    }
116
117    #[allow(dead_code)]
118    async fn generate_and_copy_to_cpu_recommended(
119        buffer: &[u8],
120        texture_descriptor: &wgpu::TextureDescriptor<'_>,
121    ) -> Result<Vec<MipBuffer>, Error> {
122        let (_instance, _adaptor, device, queue) = wgpu_setup().await;
123        let generator = crate::backends::RecommendedMipmapGenerator::new_with_format_hints(
124            &device,
125            &[texture_descriptor.format],
126        );
127        Ok(
128            generate_and_copy_to_cpu(&device, &queue, &generator, buffer, texture_descriptor)
129                .await?,
130        )
131    }
132    #[test]
133    fn checkerboard_r8_render() {
134        init();
135        // Generate texture data on the CPU
136        let size = 512;
137        let mip_level_count = 1 + (size as f32).log2() as u32;
138        let checkboard_size = 16;
139        let data = checkerboard_r8(size, size, checkboard_size);
140        // Create a texture
141        let format = wgpu::TextureFormat::R8Unorm;
142        let texture_extent = wgpu::Extent3d {
143            width: size,
144            height: size,
145            depth: 1,
146        };
147        let texture_descriptor = wgpu::TextureDescriptor {
148            size: texture_extent,
149            mip_level_count,
150            format,
151            sample_count: 1,
152            dimension: wgpu::TextureDimension::D2,
153            usage: crate::RenderMipmapGenerator::required_usage()
154                | wgpu::TextureUsage::COPY_DST
155                | wgpu::TextureUsage::COPY_SRC,
156            label: None,
157        };
158        dbg!(format);
159        futures::executor::block_on((|| async {
160            let mipmap_buffers = generate_and_copy_to_cpu_recommended(&data, &texture_descriptor)
161                .await
162                .unwrap();
163
164            assert!(mipmap_buffers.len() == mip_level_count as usize);
165
166            for mip in &mipmap_buffers {
167                assert!(
168                    mip.buffer.len()
169                        == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height
170                );
171            }
172            // The last mip map level should be 1x1 and the value is an average of 0 and 255
173            mipmap_buffers.last().map(|mip| {
174                let width = mip.dimensions.width;
175                let height = mip.dimensions.height;
176                let bpp = mip.dimensions.bytes_per_channel;
177                let data = &mip.buffer;
178                dbg!(data);
179                assert!(width == 1);
180                assert!(height == 1);
181                assert!(data.len() == width * height * bpp);
182                // The final result is implementation dependent
183                // but we expect the pixel to be a perfect
184                // blend of white and black, i.e 255 / 2 = 127.5
185                // Depending on the platform and underlying implementation,
186                // this might round up or down so check 127 and 128
187                assert!(data[0] == 127 || data[0] == 128);
188            });
189        })());
190    }
191
192    #[test]
193    fn checkerboard_rgba8_render() {
194        init();
195        // Generate texture data on the CPU
196        let size = 512;
197        let mip_level_count = 1 + (size as f32).log2() as u32;
198        let checkboard_size = 16;
199        let data = checkerboard_rgba8(size, size, checkboard_size);
200        // Create a texture
201        let format = wgpu::TextureFormat::Rgba8Unorm;
202        let texture_extent = wgpu::Extent3d {
203            width: size,
204            height: size,
205            depth: 1,
206        };
207        let texture_descriptor = wgpu::TextureDescriptor {
208            size: texture_extent,
209            mip_level_count,
210            format,
211            sample_count: 1,
212            dimension: wgpu::TextureDimension::D2,
213            usage: crate::RenderMipmapGenerator::required_usage()
214                | wgpu::TextureUsage::COPY_DST
215                | wgpu::TextureUsage::COPY_SRC,
216            label: None,
217        };
218        futures::executor::block_on((|| async {
219            let mipmap_buffers = generate_and_copy_to_cpu_recommended(&data, &texture_descriptor)
220                .await
221                .unwrap();
222            assert!(mipmap_buffers.len() == mip_level_count as usize);
223            for mip in &mipmap_buffers {
224                assert!(
225                    mip.buffer.len()
226                        == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height
227                );
228            }
229            // The last mip map level should be 1x1 and each of the 4 componenets per pixel
230            // should be the average of 0 and 255, but in sRGB color space
231            mipmap_buffers.last().map(|mip| {
232                let width = mip.dimensions.width;
233                let height = mip.dimensions.height;
234                let bpp = mip.dimensions.bytes_per_channel;
235                let data = &mip.buffer;
236                assert!(width == 1);
237                assert!(height == 1);
238                assert!(data.len() == width * height * bpp);
239                assert!(data[0] == 127 || data[0] == 128);
240                assert!(data[1] == 127 || data[1] == 128);
241                assert!(data[2] == 127 || data[2] == 128);
242                assert!(data[3] == 255);
243            });
244        })());
245    }
246
247    #[test]
248    fn checkerboard_srgba8_render() {
249        init();
250        // Generate texture data on the CPU
251        let size = 512;
252        let mip_level_count = 1 + (size as f32).log2() as u32;
253        let checkboard_size = 16;
254        let data = checkerboard_rgba8(size, size, checkboard_size);
255        // Create a texture
256        let format = wgpu::TextureFormat::Rgba8UnormSrgb;
257        let texture_extent = wgpu::Extent3d {
258            width: size,
259            height: size,
260            depth: 1,
261        };
262        let texture_descriptor = wgpu::TextureDescriptor {
263            size: texture_extent,
264            mip_level_count,
265            format,
266            sample_count: 1,
267            dimension: wgpu::TextureDimension::D2,
268            usage: crate::RenderMipmapGenerator::required_usage()
269                | wgpu::TextureUsage::COPY_DST
270                | wgpu::TextureUsage::COPY_SRC,
271            label: None,
272        };
273        futures::executor::block_on((|| async {
274            let mipmap_buffers = generate_and_copy_to_cpu_recommended(&data, &texture_descriptor)
275                .await
276                .unwrap();
277            assert!(mipmap_buffers.len() == mip_level_count as usize);
278            for mip in &mipmap_buffers {
279                assert!(
280                    mip.buffer.len()
281                        == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height
282                );
283            }
284            // The last mip map level should be 1x1 and each of the 4 componenets per pixel
285            // should be the average of 0 and 255, but in sRGB color space
286            mipmap_buffers.last().map(|mip| {
287                let width = mip.dimensions.width;
288                let height = mip.dimensions.height;
289                let bpp = mip.dimensions.bytes_per_channel;
290                let data = &mip.buffer;
291                assert!(width == 1);
292                assert!(height == 1);
293                assert!(data.len() == width * height * bpp);
294                // The final result is implementation dependent
295                // See https://entropymine.com/imageworsener/srgbformula/
296                // for how to convert between linear and srgb
297                // Where does 187 and 188 come from? Solve for x in:
298                // ((((x / 255 + 0.055) / 1.055)^2.4) * 255) == (255 / 2)
299                // -> x = 187.516155
300                assert!(data[0] == 187 || data[0] == 188);
301                assert!(data[1] == 187 || data[1] == 188);
302                assert!(data[2] == 187 || data[2] == 188);
303                assert!(data[3] == 255);
304            });
305        })());
306    }
307
308    #[test]
309    fn checkerboard_r8_compute() {
310        init();
311        // Generate texture data on the CPU
312        let size = 512;
313        let mip_level_count = 1 + (size as f32).log2() as u32;
314        let checkboard_size = 16;
315        let data = checkerboard_r8(size, size, checkboard_size);
316        // Create a texture
317        let format = wgpu::TextureFormat::R8Unorm;
318        let texture_extent = wgpu::Extent3d {
319            width: size,
320            height: size,
321            depth: 1,
322        };
323        let texture_descriptor = wgpu::TextureDescriptor {
324            size: texture_extent,
325            mip_level_count,
326            format,
327            sample_count: 1,
328            dimension: wgpu::TextureDimension::D2,
329            usage: crate::ComputeMipmapGenerator::required_usage()
330                | wgpu::TextureUsage::COPY_DST
331                | wgpu::TextureUsage::COPY_SRC,
332            label: None,
333        };
334        futures::executor::block_on((|| async {
335            let mipmap_buffers = generate_and_copy_to_cpu_recommended(&data, &texture_descriptor)
336                .await
337                .unwrap();
338
339            assert!(mipmap_buffers.len() == mip_level_count as usize);
340
341            for mip in &mipmap_buffers {
342                assert!(
343                    mip.buffer.len()
344                        == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height
345                );
346            }
347            // The last mip map level should be 1x1 and the value is an average of 0 and 255
348            mipmap_buffers.last().map(|mip| {
349                let width = mip.dimensions.width;
350                let height = mip.dimensions.height;
351                let bpp = mip.dimensions.bytes_per_channel;
352                let data = &mip.buffer;
353                assert!(width == 1);
354                assert!(height == 1);
355                assert!(data.len() == width * height * bpp);
356                // The final result is implementation dependent
357                // but we expect the pixel to be a perfect
358                // blend of white and black, i.e 255 / 2 = 127.5
359                // Depending on the platform and underlying implementation,
360                // this might round up or down so check 127 and 128
361                assert!(data[0] == 127 || data[0] == 128);
362            });
363        })());
364    }
365
366    #[test]
367    fn checkerboard_rgba8_compute() {
368        init();
369        // Generate texture data on the CPU
370        let size = 512;
371        let mip_level_count = 1 + (size as f32).log2() as u32;
372        let checkboard_size = 16;
373        let data = checkerboard_rgba8(size, size, checkboard_size);
374        // Create a texture
375        let format = wgpu::TextureFormat::Rgba8Unorm;
376        let texture_extent = wgpu::Extent3d {
377            width: size,
378            height: size,
379            depth: 1,
380        };
381        let texture_descriptor = wgpu::TextureDescriptor {
382            size: texture_extent,
383            mip_level_count,
384            format,
385            sample_count: 1,
386            dimension: wgpu::TextureDimension::D2,
387            usage: crate::ComputeMipmapGenerator::required_usage()
388                | wgpu::TextureUsage::COPY_DST
389                | wgpu::TextureUsage::COPY_SRC,
390            label: None,
391        };
392        futures::executor::block_on((|| async {
393            let mipmap_buffers = generate_and_copy_to_cpu_recommended(&data, &texture_descriptor)
394                .await
395                .unwrap();
396            assert!(mipmap_buffers.len() == mip_level_count as usize);
397            for mip in &mipmap_buffers {
398                assert!(
399                    mip.buffer.len()
400                        == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height
401                );
402            }
403            // The last mip map level should be 1x1 and each of the 4 componenets per pixel
404            // should be the average of 0 and 255
405            mipmap_buffers.last().map(|mip| {
406                let width = mip.dimensions.width;
407                let height = mip.dimensions.height;
408                let bpp = mip.dimensions.bytes_per_channel;
409                let data = &mip.buffer;
410                assert!(width == 1);
411                assert!(height == 1);
412                assert!(data.len() == width * height * bpp);
413                assert!(data[0] == 127 || data[0] == 128);
414                assert!(data[1] == 127 || data[1] == 128);
415                assert!(data[2] == 127 || data[2] == 128);
416                assert!(data[3] == 255);
417            });
418        })());
419    }
420
421    #[test]
422    fn checkerboard_srgba8_compute() {
423        init();
424        // Generate texture data on the CPU
425        let size = 512;
426        let mip_level_count = 1 + (size as f32).log2() as u32;
427        let checkboard_size = 16;
428        let data = checkerboard_rgba8(size, size, checkboard_size);
429        // Create a texture
430        let format = wgpu::TextureFormat::Rgba8UnormSrgb;
431        let texture_extent = wgpu::Extent3d {
432            width: size,
433            height: size,
434            depth: 1,
435        };
436        let texture_descriptor = wgpu::TextureDescriptor {
437            size: texture_extent,
438            mip_level_count,
439            format,
440            sample_count: 1,
441            dimension: wgpu::TextureDimension::D2,
442            usage: crate::ComputeMipmapGenerator::required_usage()
443                | wgpu::TextureUsage::COPY_DST
444                | wgpu::TextureUsage::COPY_SRC,
445            label: None,
446        };
447        futures::executor::block_on((|| async {
448            let mipmap_buffers = generate_and_copy_to_cpu_recommended(&data, &texture_descriptor)
449                .await
450                .unwrap();
451            assert!(mipmap_buffers.len() == mip_level_count as usize);
452            for mip in &mipmap_buffers {
453                assert!(
454                    mip.buffer.len()
455                        == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height
456                );
457            }
458            // The last mip map level should be 1x1 and each of the 4 componenets per pixel
459            // should be the average of 0 and 255, but in sRGB color space
460            mipmap_buffers.last().map(|mip| {
461                let width = mip.dimensions.width;
462                let height = mip.dimensions.height;
463                let bpp = mip.dimensions.bytes_per_channel;
464                let data = &mip.buffer;
465                assert!(width == 1);
466                assert!(height == 1);
467                assert!(data.len() == width * height * bpp);
468                // The final result is implementation dependent
469                // See https://entropymine.com/imageworsener/srgbformula/
470                // for how to convert between linear and srgb
471                // Where does 187 and 188 come from? Solve for x in:
472                // ((((x / 255 + 0.055) / 1.055)^2.4) * 255) == (255 / 2)
473                // -> x = 187.516155
474                dbg!(data);
475                assert!(data[0] == 187 || data[0] == 188);
476                assert!(data[1] == 187 || data[1] == 188);
477                assert!(data[2] == 187 || data[2] == 188);
478                assert!(data[3] == 255);
479            });
480        })());
481    }
482
483    #[test]
484    fn checkerboard_rgba32f_render() {
485        init();
486        // Generate texture data on the CPU
487        let size = 512;
488        let mip_level_count = 1 + (size as f32).log2() as u32;
489        let checkboard_size = 16;
490        let data = checkerboard_rgba32f(size, size, checkboard_size);
491        // Create a texture
492        let format = wgpu::TextureFormat::Rgba32Float;
493        let texture_extent = wgpu::Extent3d {
494            width: size,
495            height: size,
496            depth: 1,
497        };
498        let texture_descriptor = wgpu::TextureDescriptor {
499            size: texture_extent,
500            mip_level_count,
501            format,
502            sample_count: 1,
503            dimension: wgpu::TextureDimension::D2,
504            usage: crate::RenderMipmapGenerator::required_usage()
505                | wgpu::TextureUsage::COPY_DST
506                | wgpu::TextureUsage::COPY_SRC,
507            label: None,
508        };
509        futures::executor::block_on((|| async {
510            let mipmap_buffers = generate_and_copy_to_cpu_recommended(
511                bytemuck::cast_slice(&data),
512                &texture_descriptor,
513            )
514            .await
515            .unwrap();
516            assert!(mipmap_buffers.len() == mip_level_count as usize);
517            for mip in &mipmap_buffers {
518                assert!(
519                    mip.buffer.len()
520                        == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height
521                );
522            }
523            // The last mip map level should be 1x1 and each of the 4 componenets per pixel
524            // should be the average of 0.0 and 1.0
525            mipmap_buffers.last().map(|mip| {
526                let width = mip.dimensions.width;
527                let height = mip.dimensions.height;
528                let bpp = mip.dimensions.bytes_per_channel;
529                let data = &mip.buffer;
530                assert!(width == 1);
531                assert!(height == 1);
532                assert!(data.len() == width * height * bpp);
533                let data: &[f32] = bytemuck::try_cast_slice(data).unwrap();
534                dbg!(data);
535                assert!(data[0] == 0.5);
536                assert!(data[1] == 0.5);
537                assert!(data[2] == 0.5);
538                assert!(data[3] == 1.0);
539            });
540        })());
541    }
542
543    #[test]
544    fn checkerboard_rgba32f_compute() {
545        init();
546        // Generate texture data on the CPU
547        let size = 512;
548        let mip_level_count = 1 + (size as f32).log2() as u32;
549        let checkboard_size = 16;
550        let data = checkerboard_rgba32f(size, size, checkboard_size);
551        // Create a texture
552        let format = wgpu::TextureFormat::Rgba32Float;
553        let texture_extent = wgpu::Extent3d {
554            width: size,
555            height: size,
556            depth: 1,
557        };
558        let texture_descriptor = wgpu::TextureDescriptor {
559            size: texture_extent,
560            mip_level_count,
561            format,
562            sample_count: 1,
563            dimension: wgpu::TextureDimension::D2,
564            usage: crate::ComputeMipmapGenerator::required_usage()
565                | wgpu::TextureUsage::COPY_DST
566                | wgpu::TextureUsage::COPY_SRC,
567            label: None,
568        };
569        futures::executor::block_on((|| async {
570            let mipmap_buffers = generate_and_copy_to_cpu_recommended(
571                bytemuck::cast_slice(&data),
572                &texture_descriptor,
573            )
574            .await
575            .unwrap();
576            assert!(mipmap_buffers.len() == mip_level_count as usize);
577            for mip in &mipmap_buffers {
578                assert!(
579                    mip.buffer.len()
580                        == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height
581                );
582            }
583            // The last mip map level should be 1x1 and each of the 4 componenets per pixel
584            // should be the average of 0.0 and 1.0
585            mipmap_buffers.last().map(|mip| {
586                let width = mip.dimensions.width;
587                let height = mip.dimensions.height;
588                let bpp = mip.dimensions.bytes_per_channel;
589                let data = &mip.buffer;
590                assert!(width == 1);
591                assert!(height == 1);
592                assert!(data.len() == width * height * bpp);
593                let data: &[f32] = bytemuck::try_cast_slice(data).unwrap();
594                dbg!(data);
595                assert!(data[0] == 0.5);
596                assert!(data[1] == 0.5);
597                assert!(data[2] == 0.5);
598                assert!(data[3] == 1.0);
599            });
600        })());
601    }
602
603    #[test]
604    fn checkerboard_rgba8_copy() {
605        init();
606        // Generate texture data on the CPU
607        let size = 512;
608        let mip_level_count = 1 + (size as f32).log2() as u32;
609        let checkboard_size = 16;
610        let data = checkerboard_rgba8(size, size, checkboard_size);
611        // Create a texture
612        let format = wgpu::TextureFormat::Rgba8Unorm;
613        let texture_extent = wgpu::Extent3d {
614            width: size,
615            height: size,
616            depth: 1,
617        };
618        let texture_descriptor = wgpu::TextureDescriptor {
619            size: texture_extent,
620            mip_level_count,
621            format,
622            sample_count: 1,
623            dimension: wgpu::TextureDimension::D2,
624            usage: crate::CopyMipmapGenerator::required_usage()
625                | wgpu::TextureUsage::COPY_SRC
626                | wgpu::TextureUsage::COPY_DST,
627            label: None,
628        };
629        futures::executor::block_on((|| async {
630            let mipmap_buffers = generate_and_copy_to_cpu_recommended(&data, &texture_descriptor)
631                .await
632                .unwrap();
633            assert!(mipmap_buffers.len() == mip_level_count as usize);
634            for mip in &mipmap_buffers {
635                assert!(
636                    mip.buffer.len()
637                        == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height
638                );
639            }
640            // The last mip map level should be 1x1 and each of the 4 componenets per pixel
641            // should be the average of 0 and 255
642            mipmap_buffers.last().map(|mip| {
643                let width = mip.dimensions.width;
644                let height = mip.dimensions.height;
645                let bpp = mip.dimensions.bytes_per_channel;
646                let data = &mip.buffer;
647                dbg!(data);
648                assert!(width == 1);
649                assert!(height == 1);
650                assert!(data.len() == width * height * bpp);
651                assert!(data[0] == 127 || data[0] == 128);
652                assert!(data[1] == 127 || data[1] == 128);
653                assert!(data[2] == 127 || data[2] == 128);
654                assert!(data[3] == 255);
655            });
656        })());
657    }
658}