Skip to main content

bevy_core_pipeline/mip_generation/
mod.rs

1//! Downsampling of textures to produce mipmap levels.
2//!
3//! This module implements variations on the [AMD FidelityFX single-pass
4//! downsampling] shader. It's used for generating mipmaps for textures
5//! ([`MipGenerationJobs`]) and for creating hierarchical Z-buffers (the
6//! [`experimental::depth`] module).
7//!
8//! See the documentation for [`MipGenerationJobs`] and [`experimental::depth`]
9//! for more information.
10//!
11//! [AMD FidelityFX single-pass downsampling]: https://gpuopen.com/fidelityfx-spd/
12
13use crate::core_3d::prepare_core_3d_depth_textures;
14use crate::deferred::node::early_deferred_prepass;
15use crate::mip_generation::experimental::depth::{
16    self, early_downsample_depth, late_downsample_depth, DownsampleDepthPipeline,
17    DownsampleDepthPipelines,
18};
19use crate::prepass::node::late_prepass;
20use crate::schedule::{Core3d, Core3dSystems};
21
22use bevy_app::{App, Plugin};
23use bevy_asset::{embedded_asset, load_embedded_asset, AssetId, Assets, Handle};
24use bevy_derive::{Deref, DerefMut};
25use bevy_ecs::{
26    prelude::resource_exists,
27    resource::Resource,
28    schedule::IntoScheduleConfigs as _,
29    system::{Res, ResMut},
30    world::{FromWorld, World},
31};
32use bevy_image::Image;
33use bevy_log::error;
34use bevy_math::{vec2, Vec2};
35use bevy_platform::collections::{hash_map::Entry, HashMap, HashSet};
36use bevy_render::{
37    diagnostic::RecordDiagnostics as _,
38    render_asset::RenderAssets,
39    render_resource::{
40        binding_types::{sampler, texture_2d, texture_storage_2d, uniform_buffer},
41        BindGroup, BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries,
42        CachedComputePipelineId, ComputePassDescriptor, ComputePipelineDescriptor, Extent3d,
43        FilterMode, MipmapFilterMode, PipelineCache, Sampler, SamplerBindingType,
44        SamplerDescriptor, ShaderStages, ShaderType, SpecializedComputePipelines,
45        StorageTextureAccess, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat,
46        TextureFormatFeatureFlags, TextureUsages, TextureView, TextureViewDescriptor,
47        TextureViewDimension, UniformBuffer,
48    },
49    renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue},
50    settings::WgpuFeatures,
51    texture::GpuImage,
52    RenderStartup,
53};
54use bevy_render::{GpuResourceAppExt, Render, RenderApp, RenderSystems};
55use bevy_shader::{Shader, ShaderDefVal};
56use bevy_utils::default;
57
58pub mod experimental;
59
60/// A resource that stores the shaders that perform downsampling.
61#[derive(Clone, Resource)]
62pub struct DownsampleShaders {
63    /// The experimental shader that downsamples depth
64    /// (`downsample_depth.wgsl`).
65    pub depth: Handle<Shader>,
66    /// The shaders that perform downsampling of color textures.
67    ///
68    /// This table maps a [`TextureFormat`] to the shader that performs
69    /// downsampling for textures in that format.
70    pub general: HashMap<TextureFormat, Handle<Shader>>,
71}
72
73// The number of storage textures required to combine the bind groups in the
74// downsampling shader.
75const REQUIRED_STORAGE_TEXTURES: u32 = 12;
76
77/// All texture formats that we can perform downsampling for.
78///
79/// This is a list of pairs, each of which consists of the [`TextureFormat`] and
80/// the WGSL name for that texture format.
81///
82/// The comprehensive list of WGSL names for texture formats can be found in
83/// [the relevant section of the WGSL specification].
84///
85/// [the relevant section of the WGSL specification]:
86/// https://www.w3.org/TR/WGSL/#texel-formats
87static TEXTURE_FORMATS: [(TextureFormat, &str); 40] = [
88    (TextureFormat::Rgba8Unorm, "rgba8unorm"),
89    (TextureFormat::Rgba8Snorm, "rgba8snorm"),
90    (TextureFormat::Rgba8Uint, "rgba8uint"),
91    (TextureFormat::Rgba8Sint, "rgba8sint"),
92    (TextureFormat::Rgba16Unorm, "rgba16unorm"),
93    (TextureFormat::Rgba16Snorm, "rgba16snorm"),
94    (TextureFormat::Rgba16Uint, "rgba16uint"),
95    (TextureFormat::Rgba16Sint, "rgba16sint"),
96    (TextureFormat::Rgba16Float, "rgba16float"),
97    (TextureFormat::Rg8Unorm, "rg8unorm"),
98    (TextureFormat::Rg8Snorm, "rg8snorm"),
99    (TextureFormat::Rg8Uint, "rg8uint"),
100    (TextureFormat::Rg8Sint, "rg8sint"),
101    (TextureFormat::Rg16Unorm, "rg16unorm"),
102    (TextureFormat::Rg16Snorm, "rg16snorm"),
103    (TextureFormat::Rg16Uint, "rg16uint"),
104    (TextureFormat::Rg16Sint, "rg16sint"),
105    (TextureFormat::Rg16Float, "rg16float"),
106    (TextureFormat::R32Uint, "r32uint"),
107    (TextureFormat::R32Sint, "r32sint"),
108    (TextureFormat::R32Float, "r32float"),
109    (TextureFormat::Rg32Uint, "rg32uint"),
110    (TextureFormat::Rg32Sint, "rg32sint"),
111    (TextureFormat::Rg32Float, "rg32float"),
112    (TextureFormat::Rgba32Uint, "rgba32uint"),
113    (TextureFormat::Rgba32Sint, "rgba32sint"),
114    (TextureFormat::Rgba32Float, "rgba32float"),
115    (TextureFormat::Bgra8Unorm, "bgra8unorm"),
116    (TextureFormat::R8Unorm, "r8unorm"),
117    (TextureFormat::R8Snorm, "r8snorm"),
118    (TextureFormat::R8Uint, "r8uint"),
119    (TextureFormat::R8Sint, "r8sint"),
120    (TextureFormat::R16Unorm, "r16unorm"),
121    (TextureFormat::R16Snorm, "r16snorm"),
122    (TextureFormat::R16Uint, "r16uint"),
123    (TextureFormat::R16Sint, "r16sint"),
124    (TextureFormat::R16Float, "r16float"),
125    (TextureFormat::Rgb10a2Unorm, "rgb10a2unorm"),
126    (TextureFormat::Rgb10a2Uint, "rgb10a2uint"),
127    (TextureFormat::Rg11b10Ufloat, "rg11b10ufloat"),
128];
129
130/// A render-world resource that stores a list of [`Image`]s that will have
131/// mipmaps generated for them.
132///
133/// You can add images to this list via the [`MipGenerationJobs::add`] method,
134/// in the render world. Note that this, by itself, isn't enough to generate
135/// the mipmaps; you must also add a [`generate_mips_for_phase`] system to the render schedule.
136///
137/// This resource exists only in the render world, not the main world.
138/// Therefore, you typically want to place images in this resource in a system
139/// that runs in the [`bevy_render::ExtractSchedule`] of the
140/// [`bevy_render::RenderApp`].
141///
142/// See `dynamic_mip_generation` for an example of usage.
143#[derive(Resource, Default, Deref, DerefMut)]
144pub struct MipGenerationJobs(pub HashMap<MipGenerationPhaseId, MipGenerationPhase>);
145
146impl MipGenerationJobs {
147    /// Schedules the generation of mipmaps for an image.
148    ///
149    /// Mipmaps will be generated during the execution of the
150    /// [`generate_mips_for_phase`] system corresponding to the [`MipGenerationPhaseId`].
151    /// Note that, by default, Bevy doesn't automatically add any such system to
152    /// the render schedule; it's up to you to manually add that system.
153    pub fn add(&mut self, phase: MipGenerationPhaseId, image: impl Into<AssetId<Image>>) {
154        self.entry(phase).or_default().push(image.into());
155    }
156}
157
158/// The list of [`Image`]s that will have mipmaps generated for them during a
159/// specific phase.
160///
161/// The [`MipGenerationJobs`] resource stores one of these lists per mipmap
162/// generation phase.
163///
164/// To add images to this list, use [`MipGenerationJobs::add`] in a render app
165/// system.
166#[derive(Default, Deref, DerefMut)]
167pub struct MipGenerationPhase(pub Vec<AssetId<Image>>);
168
169/// Identifies a *phase* during which mipmaps will be generated for an image.
170///
171/// Sometimes, mipmaps must be generated at a specific time during the rendering
172/// process. This typically occurs when a camera renders to the image and then
173/// the image is sampled later in the frame as a second camera renders the
174/// scene. In this case, the mipmaps must be generated after the first camera
175/// renders to the image rendered to but before the second camera's rendering
176/// samples the image. To express these kinds of dependencies, you group images
177/// into *phases* and schedule systems that call [`generate_mips_for_phase`]
178/// targeting each phase at the appropriate time.
179///
180/// Each phase has an ID, which is an arbitrary 32-bit integer. You may specify
181/// any value you wish as a phase ID, so long as the system that calls
182/// [`generate_mips_for_phase`] uses the same ID.
183#[derive(Clone, Copy, PartialEq, Eq, Hash)]
184pub struct MipGenerationPhaseId(pub u32);
185
186/// Stores all render pipelines and bind groups associated with the mipmap
187/// generation shader.
188///
189/// The `prepare_mip_generator_pipelines` system populates this resource lazily
190/// as new textures are scheduled.
191#[derive(Resource, Default)]
192pub struct MipGenerationPipelines {
193    /// The pipeline for each texture format.
194    ///
195    /// Note that pipelines can be shared among all images that use a single
196    /// texture format.
197    pipelines: HashMap<TextureFormat, MipGenerationTextureFormatPipelines>,
198
199    /// The bind group for each image.
200    ///
201    /// These are cached from frame to frame if the same image needs mips
202    /// generated for it on immediately-consecutive frames.
203    bind_groups: HashMap<AssetId<Image>, MipGenerationJobBindGroups>,
204}
205
206/// The compute pipelines and bind group layouts for the single-pass
207/// downsampling shader for a single texture format.
208///
209/// Note that, despite the name, the single-pass downsampling shader has two
210/// passes, not one. This is because WGSL doesn't presently support
211/// globally-coherent buffers; the only way to have a synchronization point is
212/// to issue a second dispatch.
213struct MipGenerationTextureFormatPipelines {
214    /// The bind group layout for the first pass of the downsampling shader.
215    downsampling_bind_group_layout_pass_1: BindGroupLayoutDescriptor,
216    /// The bind group layout for the second pass of the downsampling shader.
217    downsampling_bind_group_layout_pass_2: BindGroupLayoutDescriptor,
218    /// The compute pipeline for the first pass of the downsampling shader.
219    downsampling_pipeline_pass_1: CachedComputePipelineId,
220    /// The compute pipeline for the second pass of the downsampling shader.
221    downsampling_pipeline_pass_2: CachedComputePipelineId,
222}
223
224/// Bind groups for the downsampling shader associated with a single texture.
225struct MipGenerationJobBindGroups {
226    /// The bind group for the first downsampling compute pass.
227    downsampling_bind_group_pass_1: BindGroup,
228    /// The bind group for the second downsampling compute pass.
229    downsampling_bind_group_pass_2: BindGroup,
230}
231
232/// Constants for the single-pass downsampling shader generated on the CPU and
233/// read on the GPU.
234///
235/// These constants are stored within a uniform buffer. There's one such uniform
236/// buffer per image.
237#[derive(Clone, Copy, ShaderType)]
238#[repr(C)]
239pub struct DownsamplingConstants {
240    /// The number of mip levels that this image possesses.
241    pub mips: u32,
242    /// The reciprocal of the size of the first mipmap level for this texture.
243    pub inverse_input_size: Vec2,
244    /// Padding.
245    pub _padding: u32,
246}
247
248/// A plugin that allows Bevy to repeatedly downsample textures to create
249/// mipmaps.
250///
251/// Generation of mipmaps happens on the GPU.
252pub struct MipGenerationPlugin;
253
254impl Plugin for MipGenerationPlugin {
255    fn build(&self, app: &mut App) {
256        embedded_asset!(app, "experimental/downsample_depth.wgsl");
257        embedded_asset!(app, "downsample.wgsl");
258
259        let depth_shader = load_embedded_asset!(app, "experimental/downsample_depth.wgsl");
260
261        // We don't have string-valued shader definitions in `naga_oil`, so we
262        // use a text-pasting hack. The `downsample.wgsl` shader is eagerly
263        // specialized for each texture format by replacing `##TEXTURE_FORMAT##`
264        // with each possible format.
265        // When we have WESL, we should probably revisit this.
266        let mut shader_assets = app.world_mut().resource_mut::<Assets<Shader>>();
267        let shader_template_source = include_str!("downsample.wgsl");
268        let general_shaders: HashMap<_, _> = TEXTURE_FORMATS
269            .iter()
270            .map(|(target_format, identifier)| {
271                let shader_source =
272                    shader_template_source.replace("##TEXTURE_FORMAT##", identifier);
273                (
274                    *target_format,
275                    shader_assets.add(Shader::from_wgsl(shader_source, "downsample.wgsl")),
276                )
277            })
278            .collect();
279
280        let downsample_shaders = DownsampleShaders {
281            depth: depth_shader,
282            general: general_shaders,
283        };
284        app.insert_resource(downsample_shaders.clone());
285
286        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
287            return;
288        };
289
290        render_app
291            .init_gpu_resource::<SpecializedComputePipelines<DownsampleDepthPipeline>>()
292            .init_resource::<MipGenerationJobs>()
293            .init_resource::<MipGenerationPipelines>()
294            .insert_resource(downsample_shaders)
295            .add_systems(RenderStartup, depth::init_depth_pyramid_dummy_texture)
296            .add_systems(
297                Core3d,
298                (
299                    early_downsample_depth
300                        .after(early_deferred_prepass)
301                        .before(late_prepass),
302                    late_downsample_depth.in_set(Core3dSystems::PostProcess),
303                ),
304            )
305            .add_systems(
306                Render,
307                depth::create_downsample_depth_pipelines.in_set(RenderSystems::Prepare),
308            )
309            .add_systems(
310                Render,
311                (
312                    depth::prepare_view_depth_pyramids,
313                    depth::prepare_downsample_depth_view_bind_groups,
314                )
315                    .chain()
316                    .in_set(RenderSystems::PrepareResources)
317                    .run_if(resource_exists::<DownsampleDepthPipelines>)
318                    .after(prepare_core_3d_depth_textures),
319            )
320            .add_systems(
321                Render,
322                prepare_mip_generator_pipelines.in_set(RenderSystems::PrepareResources),
323            )
324            .add_systems(
325                Render,
326                reset_mip_generation_jobs.in_set(RenderSystems::Cleanup),
327            );
328    }
329
330    fn finish(&self, app: &mut App) {
331        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
332            return;
333        };
334
335        // This needs to be done here so that we have access to the
336        // `RenderDevice`.
337        render_app.init_gpu_resource::<MipGenerationResources>();
338    }
339}
340
341/// Global GPU resources that the mip generation pipelines use.
342///
343/// At the moment, the only such resource is a texture sampler.
344#[derive(Resource)]
345struct MipGenerationResources {
346    /// The texture sampler that the single-pass downsampling pipelines use to
347    /// sample the source texture.
348    sampler: Sampler,
349}
350
351impl FromWorld for MipGenerationResources {
352    fn from_world(world: &mut World) -> Self {
353        let render_device = world.resource_mut::<RenderDevice>();
354        MipGenerationResources {
355            sampler: render_device.create_sampler(&SamplerDescriptor {
356                label: Some("mip generation sampler"),
357                mag_filter: FilterMode::Linear,
358                min_filter: FilterMode::Linear,
359                mipmap_filter: MipmapFilterMode::Nearest,
360                ..default()
361            }),
362        }
363    }
364}
365
366/// Generates mipmaps for all images in a [`MipGenerationPhaseId`].
367///
368/// This function should be called from within a render system to generate
369/// mipmaps for all images that have been enqueued for the specified phase.
370/// The phased nature of mipmap generation allows precise control over the time
371/// when mipmaps are generated for each image. Your system should be ordered
372/// so that the mipmaps will be generated after any passes that *write* to the
373/// images in question but before any shaders that *read* from those images
374/// execute.
375///
376/// See `dynamic_mip_generation` for an example of use.
377pub fn generate_mips_for_phase(
378    phase_id: MipGenerationPhaseId,
379    mip_generation_jobs: &MipGenerationJobs,
380    pipeline_cache: &PipelineCache,
381    mip_generation_bind_groups: &MipGenerationPipelines,
382    gpu_images: &RenderAssets<GpuImage>,
383    ctx: &mut RenderContext,
384) {
385    let Some(mip_generation_phase) = mip_generation_jobs.get(&phase_id) else {
386        return;
387    };
388    if mip_generation_phase.is_empty() {
389        // Quickly bail out if there's nothing to do.
390        return;
391    }
392
393    let diagnostics = ctx.diagnostic_recorder();
394    let diagnostics = diagnostics.as_deref();
395
396    for mip_generation_job in mip_generation_phase.iter() {
397        let Some(gpu_image) = gpu_images.get(*mip_generation_job) else {
398            continue;
399        };
400        let Some(mip_generation_job_bind_groups) = mip_generation_bind_groups
401            .bind_groups
402            .get(mip_generation_job)
403        else {
404            continue;
405        };
406        let Some(mip_generation_pipelines) = mip_generation_bind_groups
407            .pipelines
408            .get(&gpu_image.texture_descriptor.format)
409        else {
410            continue;
411        };
412
413        // Fetch the mip generation pipelines.
414        let (Some(mip_generation_pipeline_pass_1), Some(mip_generation_pipeline_pass_2)) = (
415            pipeline_cache
416                .get_compute_pipeline(mip_generation_pipelines.downsampling_pipeline_pass_1),
417            pipeline_cache
418                .get_compute_pipeline(mip_generation_pipelines.downsampling_pipeline_pass_2),
419        ) else {
420            continue;
421        };
422
423        // Perform the first downsampling pass.
424        {
425            let mut compute_pass_1 =
426                ctx.command_encoder()
427                    .begin_compute_pass(&ComputePassDescriptor {
428                        label: Some("mip generation pass 1"),
429                        timestamp_writes: None,
430                    });
431            let pass_span = diagnostics.pass_span(&mut compute_pass_1, "mip generation pass 1");
432            compute_pass_1.set_pipeline(mip_generation_pipeline_pass_1);
433            compute_pass_1.set_bind_group(
434                0,
435                &mip_generation_job_bind_groups.downsampling_bind_group_pass_1,
436                &[],
437            );
438            compute_pass_1.dispatch_workgroups(
439                gpu_image.texture_descriptor.size.width.div_ceil(64),
440                gpu_image.texture_descriptor.size.height.div_ceil(64),
441                1,
442            );
443            pass_span.end(&mut compute_pass_1);
444        }
445
446        // Perform the second downsampling pass.
447        {
448            let mut compute_pass_2 =
449                ctx.command_encoder()
450                    .begin_compute_pass(&ComputePassDescriptor {
451                        label: Some("mip generation pass 2"),
452                        timestamp_writes: None,
453                    });
454            let pass_span = diagnostics.pass_span(&mut compute_pass_2, "mip generation pass 2");
455            compute_pass_2.set_pipeline(mip_generation_pipeline_pass_2);
456            compute_pass_2.set_bind_group(
457                0,
458                &mip_generation_job_bind_groups.downsampling_bind_group_pass_2,
459                &[],
460            );
461            compute_pass_2.dispatch_workgroups(
462                gpu_image.texture_descriptor.size.width.div_ceil(256),
463                gpu_image.texture_descriptor.size.height.div_ceil(256),
464                1,
465            );
466            pass_span.end(&mut compute_pass_2);
467        }
468    }
469}
470
471/// Creates all bind group layouts, bind groups, and pipelines for all mipmap
472/// generation jobs that have been enqueued this frame.
473///
474/// Bind group layouts, bind groups, and pipelines are all cached for images
475/// that are being processed every frame.
476fn prepare_mip_generator_pipelines(
477    mip_generation_bind_groups: ResMut<MipGenerationPipelines>,
478    mip_generation_resources: Res<MipGenerationResources>,
479    mip_generation_jobs: Res<MipGenerationJobs>,
480    pipeline_cache: Res<PipelineCache>,
481    gpu_images: Res<RenderAssets<GpuImage>>,
482    downsample_shaders: Res<DownsampleShaders>,
483    render_adapter: Res<RenderAdapter>,
484    render_device: Res<RenderDevice>,
485    render_queue: Res<RenderQueue>,
486) {
487    let mip_generation_pipelines = mip_generation_bind_groups.into_inner();
488
489    // Check to see whether we can combine downsampling bind groups on this
490    // hardware and driver.
491    let combine_downsampling_bind_groups =
492        can_combine_downsampling_bind_groups(&render_adapter, &render_device);
493
494    // Make a record of all jobs that we saw so that we can expire cached bind
495    // groups at the end of this process.
496    let mut all_source_images = HashSet::new();
497
498    for mip_generation_phase in mip_generation_jobs.values() {
499        for mip_generation_job in mip_generation_phase.iter() {
500            let Some(gpu_image) = gpu_images.get(*mip_generation_job) else {
501                continue;
502            };
503
504            // Note this job.
505            all_source_images.insert(mip_generation_job);
506
507            // Create pipelines for this texture format if necessary. We have at
508            // most one pipeline per texture format, regardless of the number of
509            // jobs that use that texture format that there are.
510            let Some(pipelines) = get_or_create_mip_generation_pipelines(
511                &render_device,
512                &pipeline_cache,
513                &downsample_shaders,
514                &mut mip_generation_pipelines.pipelines,
515                gpu_image.texture_descriptor.format,
516                mip_generation_job,
517                combine_downsampling_bind_groups,
518            ) else {
519                continue;
520            };
521
522            // Create bind groups for the job if necessary.
523
524            let Entry::Vacant(vacant_entry) = mip_generation_pipelines
525                .bind_groups
526                .entry(*mip_generation_job)
527            else {
528                continue;
529            };
530
531            let downsampling_constants_buffer =
532                create_downsampling_constants_buffer(&render_device, &render_queue, gpu_image);
533
534            let (downsampling_bind_group_pass_1, downsampling_bind_group_pass_2) =
535                create_downsampling_bind_groups(
536                    &render_device,
537                    &pipeline_cache,
538                    &mip_generation_resources,
539                    &downsampling_constants_buffer,
540                    pipelines,
541                    gpu_image,
542                    combine_downsampling_bind_groups,
543                );
544
545            vacant_entry.insert(MipGenerationJobBindGroups {
546                downsampling_bind_group_pass_1,
547                downsampling_bind_group_pass_2,
548            });
549        }
550    }
551
552    // Expire all bind groups for jobs that we didn't see this frame.
553    //
554    // Note that this logic ensures that we don't recreate bind groups for
555    // images that are updated every frame.
556    mip_generation_pipelines
557        .bind_groups
558        .retain(|asset_id, _| all_source_images.contains(asset_id));
559}
560
561/// Returns the [`MipGenerationTextureFormatPipelines`] for a single texture
562/// format, creating it if necessary.
563///
564/// The [`MipGenerationTextureFormatPipelines`] that this function returns
565/// contains both the bind group layouts and pipelines for all invocations of
566/// the single-pass downsampling shader. Note that all images that share a
567/// texture format can share the same [`MipGenerationTextureFormatPipelines`]
568/// instance.
569fn get_or_create_mip_generation_pipelines<'a>(
570    render_device: &RenderDevice,
571    pipeline_cache: &PipelineCache,
572    downsample_shaders: &DownsampleShaders,
573    mip_generation_pipelines: &'a mut HashMap<TextureFormat, MipGenerationTextureFormatPipelines>,
574    target_format: TextureFormat,
575    mip_generation_job: &AssetId<Image>,
576    combine_downsampling_bind_groups: bool,
577) -> Option<&'a MipGenerationTextureFormatPipelines> {
578    match mip_generation_pipelines.entry(target_format) {
579        Entry::Vacant(vacant_entry) => {
580            let Some(downsample_shader) = downsample_shaders.general.get(&target_format) else {
581                error!(
582                    "Attempted to generate mips for texture {:?} with format {:?}, but no \
583                     downsample shader was available for that texture format",
584                    mip_generation_job, target_format
585                );
586                return None;
587            };
588
589            let (downsampling_bind_group_layout_pass_1, downsampling_bind_group_layout_pass_2) =
590                create_downsampling_bind_group_layouts(
591                    target_format,
592                    combine_downsampling_bind_groups,
593                );
594
595            let (downsampling_pipeline_pass_1, downsampling_pipeline_pass_2) =
596                create_downsampling_pipelines(
597                    render_device,
598                    pipeline_cache,
599                    &downsampling_bind_group_layout_pass_1,
600                    &downsampling_bind_group_layout_pass_2,
601                    downsample_shader,
602                    target_format,
603                    combine_downsampling_bind_groups,
604                );
605
606            Some(vacant_entry.insert(MipGenerationTextureFormatPipelines {
607                downsampling_bind_group_layout_pass_1,
608                downsampling_bind_group_layout_pass_2,
609                downsampling_pipeline_pass_1,
610                downsampling_pipeline_pass_2,
611            }))
612        }
613
614        Entry::Occupied(occupied_entry) => Some(occupied_entry.into_mut()),
615    }
616}
617
618/// Creates the [`BindGroupLayoutDescriptor`]s for the single-pass downsampling
619/// shader for a single texture format.
620fn create_downsampling_bind_group_layouts(
621    target_format: TextureFormat,
622    combine_downsampling_bind_groups: bool,
623) -> (BindGroupLayoutDescriptor, BindGroupLayoutDescriptor) {
624    let texture_sample_type = target_format.sample_type(None, None).expect(
625        "Depth and multisample texture formats shouldn't have mip generation shaders to begin with",
626    );
627    let mips_storage = texture_storage_2d(target_format, StorageTextureAccess::WriteOnly);
628
629    if combine_downsampling_bind_groups {
630        let bind_group_layout_descriptor = BindGroupLayoutDescriptor::new(
631            "combined mip generation bind group layout",
632            &BindGroupLayoutEntries::sequential(
633                ShaderStages::COMPUTE,
634                (
635                    sampler(SamplerBindingType::Filtering),
636                    uniform_buffer::<DownsamplingConstants>(false),
637                    texture_2d(texture_sample_type),
638                    mips_storage, // 1
639                    mips_storage, // 2
640                    mips_storage, // 3
641                    mips_storage, // 4
642                    mips_storage, // 5
643                    texture_storage_2d(target_format, StorageTextureAccess::ReadWrite), // 6
644                    mips_storage, // 7
645                    mips_storage, // 8
646                    mips_storage, // 9
647                    mips_storage, // 10
648                    mips_storage, // 11
649                    mips_storage, // 12
650                ),
651            ),
652        );
653        return (
654            bind_group_layout_descriptor.clone(),
655            bind_group_layout_descriptor,
656        );
657    }
658
659    // If we got here, we use a split layout. The first pass outputs mip levels
660    // [0, 6]; the second pass outputs mip levels [7, 12].
661
662    let bind_group_layout_descriptor_pass_1 = BindGroupLayoutDescriptor::new(
663        "mip generation bind group layout, pass 1",
664        &BindGroupLayoutEntries::sequential(
665            ShaderStages::COMPUTE,
666            (
667                sampler(SamplerBindingType::Filtering),
668                uniform_buffer::<DownsamplingConstants>(false),
669                // Input mip 0
670                texture_2d(texture_sample_type),
671                mips_storage, // 1
672                mips_storage, // 2
673                mips_storage, // 3
674                mips_storage, // 4
675                mips_storage, // 5
676                mips_storage, // 6
677            ),
678        ),
679    );
680
681    let bind_group_layout_descriptor_pass_2 = BindGroupLayoutDescriptor::new(
682        "mip generation bind group layout, pass 2",
683        &BindGroupLayoutEntries::sequential(
684            ShaderStages::COMPUTE,
685            (
686                sampler(SamplerBindingType::Filtering),
687                uniform_buffer::<DownsamplingConstants>(false),
688                // Input mip 6
689                texture_2d(texture_sample_type),
690                mips_storage, // 7
691                mips_storage, // 8
692                mips_storage, // 9
693                mips_storage, // 10
694                mips_storage, // 11
695                mips_storage, // 12
696            ),
697        ),
698    );
699
700    (
701        bind_group_layout_descriptor_pass_1,
702        bind_group_layout_descriptor_pass_2,
703    )
704}
705
706/// Creates the bind groups for the single-pass downsampling shader associated
707/// with a single texture.
708///
709/// Depending on whether bind groups can be combined on this platform, this
710/// returns either two copies of a single bind group or two separate bind
711/// groups.
712fn create_downsampling_bind_groups(
713    render_device: &RenderDevice,
714    pipeline_cache: &PipelineCache,
715    mip_generation_resources: &MipGenerationResources,
716    downsampling_constants_buffer: &UniformBuffer<DownsamplingConstants>,
717    pipelines: &MipGenerationTextureFormatPipelines,
718    gpu_image: &GpuImage,
719    combine_downsampling_bind_groups: bool,
720) -> (BindGroup, BindGroup) {
721    let input_texture_view_pass_1 = gpu_image.texture.create_view(&TextureViewDescriptor {
722        label: Some("mip generation input texture view, pass 1"),
723        format: Some(gpu_image.texture.format()),
724        dimension: Some(TextureViewDimension::D2),
725        base_mip_level: 0,
726        mip_level_count: Some(1),
727        ..default()
728    });
729
730    // If we can combine downsampling bind groups on this platform, we only need
731    // one bind group.
732    if combine_downsampling_bind_groups {
733        let bind_group = render_device.create_bind_group(
734            Some("combined mip generation bind group"),
735            &pipeline_cache.get_bind_group_layout(&pipelines.downsampling_bind_group_layout_pass_1),
736            &BindGroupEntries::sequential((
737                &mip_generation_resources.sampler,
738                downsampling_constants_buffer,
739                &input_texture_view_pass_1,
740                &get_mip_storage_view(render_device, gpu_image, 1),
741                &get_mip_storage_view(render_device, gpu_image, 2),
742                &get_mip_storage_view(render_device, gpu_image, 3),
743                &get_mip_storage_view(render_device, gpu_image, 4),
744                &get_mip_storage_view(render_device, gpu_image, 5),
745                &get_mip_storage_view(render_device, gpu_image, 6),
746                &get_mip_storage_view(render_device, gpu_image, 7),
747                &get_mip_storage_view(render_device, gpu_image, 8),
748                &get_mip_storage_view(render_device, gpu_image, 9),
749                &get_mip_storage_view(render_device, gpu_image, 10),
750                &get_mip_storage_view(render_device, gpu_image, 11),
751                &get_mip_storage_view(render_device, gpu_image, 12),
752            )),
753        );
754        return (bind_group.clone(), bind_group);
755    }
756
757    // Otherwise, create two separate bind groups.
758
759    let input_texture_view_pass_2 = gpu_image.texture.create_view(&TextureViewDescriptor {
760        label: Some("mip generation input texture view, pass 2"),
761        format: Some(gpu_image.texture.format()),
762        dimension: Some(TextureViewDimension::D2),
763        base_mip_level: gpu_image.texture_descriptor.mip_level_count.min(6),
764        mip_level_count: Some(1),
765        ..default()
766    });
767
768    let bind_group_pass_1 = render_device.create_bind_group(
769        "mip generation bind group, pass 1",
770        &pipeline_cache.get_bind_group_layout(&pipelines.downsampling_bind_group_layout_pass_1),
771        &BindGroupEntries::sequential((
772            &mip_generation_resources.sampler,
773            downsampling_constants_buffer,
774            &input_texture_view_pass_1,
775            &get_mip_storage_view(render_device, gpu_image, 1),
776            &get_mip_storage_view(render_device, gpu_image, 2),
777            &get_mip_storage_view(render_device, gpu_image, 3),
778            &get_mip_storage_view(render_device, gpu_image, 4),
779            &get_mip_storage_view(render_device, gpu_image, 5),
780            &get_mip_storage_view(render_device, gpu_image, 6),
781        )),
782    );
783    let bind_group_pass_2 = render_device.create_bind_group(
784        "mip generation bind group, pass 2",
785        &pipeline_cache.get_bind_group_layout(&pipelines.downsampling_bind_group_layout_pass_2),
786        &BindGroupEntries::sequential((
787            &mip_generation_resources.sampler,
788            downsampling_constants_buffer,
789            &input_texture_view_pass_2,
790            &get_mip_storage_view(render_device, gpu_image, 7),
791            &get_mip_storage_view(render_device, gpu_image, 8),
792            &get_mip_storage_view(render_device, gpu_image, 9),
793            &get_mip_storage_view(render_device, gpu_image, 10),
794            &get_mip_storage_view(render_device, gpu_image, 11),
795            &get_mip_storage_view(render_device, gpu_image, 12),
796        )),
797    );
798
799    (bind_group_pass_1, bind_group_pass_2)
800}
801
802/// Creates the single-pass downsampling compute pipelines that perform
803/// downsampling on textures with a specific texture format.
804///
805/// Depending on whether the current platform can combine downsampling bind
806/// groups, this will either return two copies of the same pipeline or two
807/// different pipelines.
808fn create_downsampling_pipelines(
809    render_device: &RenderDevice,
810    pipeline_cache: &PipelineCache,
811    downsampling_bind_group_layout_pass_1: &BindGroupLayoutDescriptor,
812    downsampling_bind_group_layout_pass_2: &BindGroupLayoutDescriptor,
813    downsample_shader: &Handle<Shader>,
814    target_format: TextureFormat,
815    combine_downsampling_bind_groups: bool,
816) -> (CachedComputePipelineId, CachedComputePipelineId) {
817    let mut downsampling_shader_defs = vec![];
818    if render_device.features().contains(WgpuFeatures::SUBGROUP) {
819        downsampling_shader_defs.push(ShaderDefVal::Int("SUBGROUP_SUPPORT".into(), 1));
820    }
821    if combine_downsampling_bind_groups {
822        downsampling_shader_defs.push(ShaderDefVal::Int("COMBINE_BIND_GROUP".into(), 1));
823    }
824
825    let mut downsampling_first_shader_defs = downsampling_shader_defs.clone();
826    let mut downsampling_second_shader_defs = downsampling_shader_defs.clone();
827    if !combine_downsampling_bind_groups {
828        downsampling_first_shader_defs.push(ShaderDefVal::Int("FIRST_PASS".into(), 1));
829        downsampling_second_shader_defs.push(ShaderDefVal::Int("SECOND_PASS".into(), 1));
830    }
831
832    // Create the pipeline for the first pass, corresponding to mip levels [0,
833    // 6].
834    let downsampling_first_pipeline =
835        pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
836            label: Some(format!("mip generation pipeline, pass 1 ({:?})", target_format).into()),
837            layout: vec![downsampling_bind_group_layout_pass_1.clone()],
838            immediate_size: 0,
839            shader: downsample_shader.clone(),
840            shader_defs: downsampling_first_shader_defs,
841            entry_point: Some("downsample_first".into()),
842            zero_initialize_workgroup_memory: false,
843        });
844
845    // Create the pipeline for the second pass, corresponding to mip levels [7,
846    // 12].
847    let downsampling_second_pipeline =
848        pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
849            label: Some(format!("mip generation pipeline, pass 2 ({:?})", target_format).into()),
850            layout: vec![downsampling_bind_group_layout_pass_2.clone()],
851            immediate_size: 0,
852            shader: downsample_shader.clone(),
853            shader_defs: downsampling_second_shader_defs,
854            entry_point: Some("downsample_second".into()),
855            zero_initialize_workgroup_memory: false,
856        });
857
858    (downsampling_first_pipeline, downsampling_second_pipeline)
859}
860
861/// Creates the uniform buffer containing the [`DownsamplingConstants`] for a
862/// single texture.
863fn create_downsampling_constants_buffer(
864    render_device: &RenderDevice,
865    render_queue: &RenderQueue,
866    gpu_image: &GpuImage,
867) -> UniformBuffer<DownsamplingConstants> {
868    let downsampling_constants = DownsamplingConstants {
869        mips: gpu_image.texture_descriptor.mip_level_count,
870        inverse_input_size: vec2(
871            1.0 / gpu_image.texture_descriptor.size.width as f32,
872            1.0 / gpu_image.texture_descriptor.size.height as f32,
873        ),
874        _padding: 0,
875    };
876
877    let mut downsampling_constants_buffer = UniformBuffer::from(downsampling_constants);
878    downsampling_constants_buffer.write_buffer(render_device, render_queue);
879    downsampling_constants_buffer
880}
881
882/// Returns a view of the given mipmap level of a texture, suitable for
883/// attachment as a texture storage binding.
884fn get_mip_storage_view(
885    render_device: &RenderDevice,
886    gpu_image: &GpuImage,
887    level: u32,
888) -> TextureView {
889    // If `level` represents an actual mip level of the image, return a view to
890    // it.
891    if level < gpu_image.texture_descriptor.mip_level_count {
892        return gpu_image.texture.create_view(&TextureViewDescriptor {
893            label: Some(&*format!(
894                "mip downsampling storage view {}/{}",
895                level, gpu_image.texture_descriptor.mip_level_count
896            )),
897            format: Some(gpu_image.texture_descriptor.format),
898            dimension: Some(TextureViewDimension::D2),
899            aspect: TextureAspect::All,
900            base_mip_level: level,
901            mip_level_count: Some(1),
902            base_array_layer: 0,
903            array_layer_count: Some(1),
904            usage: Some(TextureUsages::STORAGE_BINDING),
905        });
906    }
907
908    // Otherwise, create a dummy texture and return a view to that.
909
910    let dummy_texture = render_device.create_texture(&TextureDescriptor {
911        label: Some(&*format!(
912            "mip downsampling dummy storage view {}/{}",
913            level, gpu_image.texture_descriptor.mip_level_count
914        )),
915        size: Extent3d {
916            width: 1,
917            height: 1,
918            depth_or_array_layers: 1,
919        },
920        mip_level_count: 1,
921        sample_count: 1,
922        dimension: TextureDimension::D2,
923        format: gpu_image.texture_descriptor.format,
924        usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
925        view_formats: &[],
926    });
927
928    dummy_texture.create_view(&TextureViewDescriptor::default())
929}
930
931/// A system that clears out the [`MipGenerationJobs`] resource in preparation
932/// for a new frame.
933fn reset_mip_generation_jobs(mut mip_generation_jobs: ResMut<MipGenerationJobs>) {
934    mip_generation_jobs.clear();
935}
936
937/// Returns true if the current platform can use a single bind group for
938/// single-pass downsampling.
939///
940/// If this platform must use two separate bind groups, one for each pass, this
941/// function returns false.
942pub fn can_combine_downsampling_bind_groups(
943    render_adapter: &RenderAdapter,
944    render_device: &RenderDevice,
945) -> bool {
946    // Determine whether we can use a single, large bind group for all mip outputs
947    let storage_texture_limit = render_device.limits().max_storage_textures_per_shader_stage;
948
949    // Determine whether we can read and write to the same rgba16f storage texture
950    let read_write_support = render_adapter
951        .get_texture_format_features(TextureFormat::Rgba16Float)
952        .flags
953        .contains(TextureFormatFeatureFlags::STORAGE_READ_WRITE);
954
955    // Combine the bind group and use read-write storage if it is supported
956    storage_texture_limit >= REQUIRED_STORAGE_TEXTURES && read_write_support
957}