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, 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::{
41            sampler, texture_2d, texture_2d_array, texture_storage_2d, texture_storage_2d_array,
42            uniform_buffer,
43        },
44        BindGroup, BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries,
45        CachedComputePipelineId, ComputePassDescriptor, ComputePipelineDescriptor, Extent3d,
46        FilterMode, MipmapFilterMode, PipelineCache, Sampler, SamplerBindingType,
47        SamplerDescriptor, ShaderStages, ShaderType, SpecializedComputePipeline,
48        SpecializedComputePipelines, StorageTextureAccess, TextureAspect, TextureDescriptor,
49        TextureDimension, TextureFormat, TextureFormatFeatureFlags, TextureUsages, TextureView,
50        TextureViewDescriptor, TextureViewDimension, UniformBuffer,
51    },
52    renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue},
53    settings::WgpuFeatures,
54    texture::GpuImage,
55    RenderStartup,
56};
57use bevy_render::{GpuResourceAppExt, Render, RenderApp, RenderSystems};
58use bevy_shader::{Shader, ShaderDefVal};
59use bevy_utils::default;
60
61pub mod experimental;
62
63/// A resource that stores the shaders that perform downsampling.
64#[derive(Clone, Resource)]
65pub struct DownsampleShaders {
66    /// The experimental shader that downsamples depth
67    /// (`downsample_depth.wesl`).
68    pub depth: Handle<Shader>,
69    /// The shader that performs downsampling of color textures
70    /// (`downsample.wesl`).
71    ///
72    /// [`DownsamplePipeline`] specializes it for a texture format.
73    pub general: Handle<Shader>,
74}
75
76/// The single-pass downsampling compute pipeline for color textures.
77///
78/// Specialize it with [`SpecializedComputePipelines`] and a
79/// [`DownsamplePipelineKey`], once per [`DownsamplePass`].
80#[derive(Resource)]
81pub struct DownsamplePipeline {
82    /// The `downsample.wesl` shader.
83    shader: Handle<Shader>,
84    /// Whether the device supports subgroup operations.
85    subgroup_support: bool,
86}
87
88impl DownsamplePipeline {
89    /// Returns true if the shader can downsample textures in `format`.
90    pub fn supports_texture_format(format: TextureFormat) -> bool {
91        texture_format_shader_def(format).is_some()
92    }
93}
94
95impl FromWorld for DownsamplePipeline {
96    fn from_world(world: &mut World) -> Self {
97        DownsamplePipeline {
98            shader: world.resource::<DownsampleShaders>().general.clone(),
99            subgroup_support: world
100                .resource::<RenderDevice>()
101                .features()
102                .contains(WgpuFeatures::SUBGROUP),
103        }
104    }
105}
106
107/// Selects a variant of the [`DownsamplePipeline`].
108#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
109pub struct DownsamplePipelineKey {
110    /// The format of the texture to downsample.
111    ///
112    /// Must be a format that [`DownsamplePipeline::supports_texture_format`]
113    /// accepts.
114    pub texture_format: TextureFormat,
115    /// True if the texture is a 2D array texture, such as a cubemap.
116    pub array_texture: bool,
117    /// True if both passes share a single bind group.
118    ///
119    /// See [`can_combine_downsampling_bind_groups`].
120    pub combine_bind_groups: bool,
121    /// The pass that this pipeline runs.
122    pub pass: DownsamplePass,
123}
124
125/// One of the two dispatches of the single-pass downsampling shader.
126///
127/// Note that, despite the name, the single-pass downsampling shader has two
128/// passes, not one. This is because WGSL doesn't presently support
129/// globally-coherent buffers; the only way to have a synchronization point is
130/// to issue a second dispatch.
131#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
132pub enum DownsamplePass {
133    /// Produces mip levels 1 to 6.
134    First,
135    /// Produces mip levels 7 to 12.
136    Second,
137}
138
139impl SpecializedComputePipeline for DownsamplePipeline {
140    type Key = DownsamplePipelineKey;
141
142    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
143        let mut shader_defs = vec![
144            texture_format_shader_def(key.texture_format).unwrap_or_else(|| {
145                panic!(
146                    "The downsample shader doesn't support texture format {:?}",
147                    key.texture_format
148                )
149            }),
150            ShaderDefVal::Bool("ARRAY_TEXTURE".into(), key.array_texture),
151        ];
152        if self.subgroup_support {
153            shader_defs.push("SUBGROUP_SUPPORT".into());
154        }
155        shader_defs.push(
156            match (key.combine_bind_groups, key.pass) {
157                (true, _) => "COMBINE_BIND_GROUP",
158                (false, DownsamplePass::First) => "SPLIT_BIND_GROUP_FIRST",
159                (false, DownsamplePass::Second) => "SPLIT_BIND_GROUP_SECOND",
160            }
161            .into(),
162        );
163
164        let (pass_number, entry_point) = match key.pass {
165            DownsamplePass::First => (1, "downsample_first"),
166            DownsamplePass::Second => (2, "downsample_second"),
167        };
168
169        ComputePipelineDescriptor {
170            label: Some(
171                format!(
172                    "mip generation pipeline, pass {pass_number} ({:?})",
173                    key.texture_format
174                )
175                .into(),
176            ),
177            layout: vec![key.bind_group_layout()],
178            shader: self.shader.clone(),
179            shader_defs,
180            entry_point: Some(entry_point.into()),
181            ..default()
182        }
183    }
184}
185
186// The number of storage textures required to combine the bind groups in the
187// downsampling shader.
188const REQUIRED_STORAGE_TEXTURES: u32 = 12;
189
190/// Returns the shader def that selects the output format of `downsample.wesl`,
191/// or `None` if the format isn't supported.
192fn texture_format_shader_def(format: TextureFormat) -> Option<ShaderDefVal> {
193    let name = match format {
194        TextureFormat::Rgba8Unorm => "TEXTURE_FORMAT_RGBA8UNORM",
195        TextureFormat::Rgba8Snorm => "TEXTURE_FORMAT_RGBA8SNORM",
196        TextureFormat::Rgba16Unorm => "TEXTURE_FORMAT_RGBA16UNORM",
197        TextureFormat::Rgba16Snorm => "TEXTURE_FORMAT_RGBA16SNORM",
198        TextureFormat::Rgba16Float => "TEXTURE_FORMAT_RGBA16FLOAT",
199        TextureFormat::Rg8Unorm => "TEXTURE_FORMAT_RG8UNORM",
200        TextureFormat::Rg8Snorm => "TEXTURE_FORMAT_RG8SNORM",
201        TextureFormat::Rg16Unorm => "TEXTURE_FORMAT_RG16UNORM",
202        TextureFormat::Rg16Snorm => "TEXTURE_FORMAT_RG16SNORM",
203        TextureFormat::Rg16Float => "TEXTURE_FORMAT_RG16FLOAT",
204        TextureFormat::R32Float => "TEXTURE_FORMAT_R32FLOAT",
205        TextureFormat::Rg32Float => "TEXTURE_FORMAT_RG32FLOAT",
206        TextureFormat::Rgba32Float => "TEXTURE_FORMAT_RGBA32FLOAT",
207        TextureFormat::Bgra8Unorm => "TEXTURE_FORMAT_BGRA8UNORM",
208        TextureFormat::R8Unorm => "TEXTURE_FORMAT_R8UNORM",
209        TextureFormat::R8Snorm => "TEXTURE_FORMAT_R8SNORM",
210        TextureFormat::R16Unorm => "TEXTURE_FORMAT_R16UNORM",
211        TextureFormat::R16Snorm => "TEXTURE_FORMAT_R16SNORM",
212        TextureFormat::R16Float => "TEXTURE_FORMAT_R16FLOAT",
213        TextureFormat::Rgb10a2Unorm => "TEXTURE_FORMAT_RGB10A2UNORM",
214        TextureFormat::Rg11b10Ufloat => "TEXTURE_FORMAT_RG11B10UFLOAT",
215        _ => return None,
216    };
217    Some(name.into())
218}
219
220/// A render-world resource that stores a list of [`Image`]s that will have
221/// mipmaps generated for them.
222///
223/// You can add images to this list via the [`MipGenerationJobs::add`] method,
224/// in the render world. Note that this, by itself, isn't enough to generate
225/// the mipmaps; you must also add a [`generate_mips_for_phase`] system to the render schedule.
226///
227/// This resource exists only in the render world, not the main world.
228/// Therefore, you typically want to place images in this resource in a system
229/// that runs in the [`bevy_render::ExtractSchedule`] of the
230/// [`bevy_render::RenderApp`].
231///
232/// See `dynamic_mip_generation` for an example of usage.
233#[derive(Resource, Default, Deref, DerefMut)]
234pub struct MipGenerationJobs(pub HashMap<MipGenerationPhaseId, MipGenerationPhase>);
235
236impl MipGenerationJobs {
237    /// Schedules the generation of mipmaps for an image.
238    ///
239    /// Mipmaps will be generated during the execution of the
240    /// [`generate_mips_for_phase`] system corresponding to the [`MipGenerationPhaseId`].
241    /// Note that, by default, Bevy doesn't automatically add any such system to
242    /// the render schedule; it's up to you to manually add that system.
243    pub fn add(&mut self, phase: MipGenerationPhaseId, image: impl Into<AssetId<Image>>) {
244        self.entry(phase).or_default().push(image.into());
245    }
246}
247
248/// The list of [`Image`]s that will have mipmaps generated for them during a
249/// specific phase.
250///
251/// The [`MipGenerationJobs`] resource stores one of these lists per mipmap
252/// generation phase.
253///
254/// To add images to this list, use [`MipGenerationJobs::add`] in a render app
255/// system.
256#[derive(Default, Deref, DerefMut)]
257pub struct MipGenerationPhase(pub Vec<AssetId<Image>>);
258
259/// Identifies a *phase* during which mipmaps will be generated for an image.
260///
261/// Sometimes, mipmaps must be generated at a specific time during the rendering
262/// process. This typically occurs when a camera renders to the image and then
263/// the image is sampled later in the frame as a second camera renders the
264/// scene. In this case, the mipmaps must be generated after the first camera
265/// renders to the image rendered to but before the second camera's rendering
266/// samples the image. To express these kinds of dependencies, you group images
267/// into *phases* and schedule systems that call [`generate_mips_for_phase`]
268/// targeting each phase at the appropriate time.
269///
270/// Each phase has an ID, which is an arbitrary 32-bit integer. You may specify
271/// any value you wish as a phase ID, so long as the system that calls
272/// [`generate_mips_for_phase`] uses the same ID.
273#[derive(Clone, Copy, PartialEq, Eq, Hash)]
274pub struct MipGenerationPhaseId(pub u32);
275
276/// Stores the pipelines and bind groups for each image that has mipmaps
277/// generated for it.
278///
279/// The `prepare_mip_generator_pipelines` system adds an entry when an image is
280/// first scheduled and removes it on the first frame the image isn't.
281#[derive(Resource, Default)]
282pub struct MipGenerationPipelines {
283    jobs: HashMap<AssetId<Image>, MipGenerationJobResources>,
284}
285
286/// Pipelines and bind groups for the downsampling shader associated with a
287/// single texture.
288struct MipGenerationJobResources {
289    downsampling_pipeline_pass_1: CachedComputePipelineId,
290    downsampling_pipeline_pass_2: CachedComputePipelineId,
291    downsampling_bind_group_pass_1: BindGroup,
292    downsampling_bind_group_pass_2: BindGroup,
293}
294
295/// Constants for the single-pass downsampling shader generated on the CPU and
296/// read on the GPU.
297///
298/// These constants are stored within a uniform buffer. There's one such uniform
299/// buffer per image.
300#[derive(Clone, Copy, ShaderType)]
301#[repr(C)]
302pub struct DownsamplingConstants {
303    /// The number of mip levels that this image possesses.
304    pub mips: u32,
305    /// The reciprocal of the size of the first mipmap level for this texture.
306    pub inverse_input_size: Vec2,
307    /// Padding.
308    pub _padding: u32,
309}
310
311/// A plugin that allows Bevy to repeatedly downsample textures to create
312/// mipmaps.
313///
314/// Generation of mipmaps happens on the GPU.
315pub struct MipGenerationPlugin;
316
317impl Plugin for MipGenerationPlugin {
318    fn build(&self, app: &mut App) {
319        embedded_asset!(app, "experimental/downsample_depth.wesl");
320        embedded_asset!(app, "downsample.wesl");
321
322        let depth_shader = load_embedded_asset!(app, "experimental/downsample_depth.wesl");
323
324        let downsample_shaders = DownsampleShaders {
325            depth: depth_shader,
326            general: load_embedded_asset!(app, "downsample.wesl"),
327        };
328        app.insert_resource(downsample_shaders.clone());
329
330        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
331            return;
332        };
333
334        render_app
335            .init_gpu_resource::<SpecializedComputePipelines<DownsampleDepthPipeline>>()
336            .init_gpu_resource::<DownsamplePipeline>()
337            .init_gpu_resource::<SpecializedComputePipelines<DownsamplePipeline>>()
338            .init_resource::<MipGenerationJobs>()
339            .init_gpu_resource::<MipGenerationPipelines>()
340            .init_gpu_resource::<MipGenerationResources>()
341            .insert_resource(downsample_shaders)
342            .add_systems(RenderStartup, depth::init_depth_pyramid_dummy_texture)
343            .add_systems(
344                Core3d,
345                (
346                    early_downsample_depth
347                        .after(early_deferred_prepass)
348                        .before(late_prepass),
349                    late_downsample_depth.in_set(Core3dSystems::PostProcess),
350                ),
351            )
352            .add_systems(
353                Render,
354                depth::create_downsample_depth_pipelines.in_set(RenderSystems::Prepare),
355            )
356            .add_systems(
357                Render,
358                (
359                    depth::prepare_view_depth_pyramids,
360                    depth::prepare_downsample_depth_view_bind_groups,
361                )
362                    .chain()
363                    .in_set(RenderSystems::PrepareResources)
364                    .run_if(resource_exists::<DownsampleDepthPipelines>)
365                    .after(prepare_core_3d_depth_textures),
366            )
367            .add_systems(
368                Render,
369                prepare_mip_generator_pipelines.in_set(RenderSystems::PrepareResources),
370            )
371            .add_systems(
372                Render,
373                reset_mip_generation_jobs.in_set(RenderSystems::Cleanup),
374            );
375    }
376}
377
378/// Global GPU resources that the mip generation pipelines use.
379///
380/// At the moment, the only such resource is a texture sampler.
381#[derive(Resource)]
382struct MipGenerationResources {
383    /// The texture sampler that the single-pass downsampling pipelines use to
384    /// sample the source texture.
385    sampler: Sampler,
386}
387
388impl FromWorld for MipGenerationResources {
389    fn from_world(world: &mut World) -> Self {
390        let render_device = world.resource_mut::<RenderDevice>();
391        MipGenerationResources {
392            sampler: render_device.create_sampler(&SamplerDescriptor {
393                label: Some("mip generation sampler"),
394                mag_filter: FilterMode::Linear,
395                min_filter: FilterMode::Linear,
396                mipmap_filter: MipmapFilterMode::Nearest,
397                ..default()
398            }),
399        }
400    }
401}
402
403/// Generates mipmaps for all images in a [`MipGenerationPhaseId`].
404///
405/// This function should be called from within a render system to generate
406/// mipmaps for all images that have been enqueued for the specified phase.
407/// The phased nature of mipmap generation allows precise control over the time
408/// when mipmaps are generated for each image. Your system should be ordered
409/// so that the mipmaps will be generated after any passes that *write* to the
410/// images in question but before any shaders that *read* from those images
411/// execute.
412///
413/// See `dynamic_mip_generation` for an example of use.
414pub fn generate_mips_for_phase(
415    phase_id: MipGenerationPhaseId,
416    mip_generation_jobs: &MipGenerationJobs,
417    pipeline_cache: &PipelineCache,
418    mip_generation_pipelines: &MipGenerationPipelines,
419    gpu_images: &RenderAssets<GpuImage>,
420    ctx: &mut RenderContext,
421) {
422    let Some(mip_generation_phase) = mip_generation_jobs.get(&phase_id) else {
423        return;
424    };
425    if mip_generation_phase.is_empty() {
426        // Quickly bail out if there's nothing to do.
427        return;
428    }
429
430    let diagnostics = ctx.diagnostic_recorder();
431    let diagnostics = diagnostics.as_deref();
432
433    for mip_generation_job in mip_generation_phase.iter() {
434        let Some(gpu_image) = gpu_images.get(*mip_generation_job) else {
435            continue;
436        };
437        let Some(job_resources) = mip_generation_pipelines.jobs.get(mip_generation_job) else {
438            continue;
439        };
440
441        // Fetch the mip generation pipelines.
442        let (Some(mip_generation_pipeline_pass_1), Some(mip_generation_pipeline_pass_2)) = (
443            pipeline_cache.get_compute_pipeline(job_resources.downsampling_pipeline_pass_1),
444            pipeline_cache.get_compute_pipeline(job_resources.downsampling_pipeline_pass_2),
445        ) else {
446            continue;
447        };
448
449        // Perform the first downsampling pass.
450        {
451            let mut compute_pass_1 =
452                ctx.command_encoder()
453                    .begin_compute_pass(&ComputePassDescriptor {
454                        label: Some("mip generation pass 1"),
455                        timestamp_writes: None,
456                    });
457            let pass_span = diagnostics.pass_span(&mut compute_pass_1, "mip generation pass 1");
458            compute_pass_1.set_pipeline(mip_generation_pipeline_pass_1);
459            compute_pass_1.set_bind_group(0, &job_resources.downsampling_bind_group_pass_1, &[]);
460            compute_pass_1.dispatch_workgroups(
461                gpu_image.texture_descriptor.size.width.div_ceil(64),
462                gpu_image.texture_descriptor.size.height.div_ceil(64),
463                1,
464            );
465            pass_span.end(&mut compute_pass_1);
466        }
467
468        // Perform the second downsampling pass.
469        {
470            let mut compute_pass_2 =
471                ctx.command_encoder()
472                    .begin_compute_pass(&ComputePassDescriptor {
473                        label: Some("mip generation pass 2"),
474                        timestamp_writes: None,
475                    });
476            let pass_span = diagnostics.pass_span(&mut compute_pass_2, "mip generation pass 2");
477            compute_pass_2.set_pipeline(mip_generation_pipeline_pass_2);
478            compute_pass_2.set_bind_group(0, &job_resources.downsampling_bind_group_pass_2, &[]);
479            compute_pass_2.dispatch_workgroups(
480                gpu_image.texture_descriptor.size.width.div_ceil(256),
481                gpu_image.texture_descriptor.size.height.div_ceil(256),
482                1,
483            );
484            pass_span.end(&mut compute_pass_2);
485        }
486    }
487}
488
489/// Creates all bind group layouts, bind groups, and pipelines for all mipmap
490/// generation jobs that have been enqueued this frame.
491///
492/// Bind group layouts, bind groups, and pipelines are all cached for images
493/// that are being processed every frame.
494fn prepare_mip_generator_pipelines(
495    mip_generation_pipelines: ResMut<MipGenerationPipelines>,
496    mip_generation_resources: Res<MipGenerationResources>,
497    mip_generation_jobs: Res<MipGenerationJobs>,
498    pipeline_cache: Res<PipelineCache>,
499    mut specialized_pipelines: ResMut<SpecializedComputePipelines<DownsamplePipeline>>,
500    downsample_pipeline: Res<DownsamplePipeline>,
501    gpu_images: Res<RenderAssets<GpuImage>>,
502    render_adapter: Res<RenderAdapter>,
503    render_device: Res<RenderDevice>,
504    render_queue: Res<RenderQueue>,
505) {
506    let mip_generation_pipelines = mip_generation_pipelines.into_inner();
507
508    // Check to see whether we can combine downsampling bind groups on this
509    // hardware and driver.
510    let combine_downsampling_bind_groups =
511        can_combine_downsampling_bind_groups(&render_adapter, &render_device);
512
513    // Make a record of all jobs that we saw so that we can expire cached bind
514    // groups at the end of this process.
515    let mut all_source_images = HashSet::new();
516
517    for mip_generation_phase in mip_generation_jobs.values() {
518        for mip_generation_job in mip_generation_phase.iter() {
519            let Some(gpu_image) = gpu_images.get(*mip_generation_job) else {
520                continue;
521            };
522
523            // Note this job.
524            all_source_images.insert(mip_generation_job);
525
526            // Create pipelines and bind groups for the job if it's new.
527            let Entry::Vacant(vacant_entry) =
528                mip_generation_pipelines.jobs.entry(*mip_generation_job)
529            else {
530                continue;
531            };
532
533            let target_format = gpu_image.texture_descriptor.format;
534            if !DownsamplePipeline::supports_texture_format(target_format) {
535                error!(
536                    "Attempted to generate mips for texture {:?} with format {:?}, but no \
537                     downsample shader was available for that texture format",
538                    mip_generation_job, target_format
539                );
540                continue;
541            }
542
543            let key_pass_1 = DownsamplePipelineKey {
544                texture_format: target_format,
545                array_texture: false,
546                combine_bind_groups: combine_downsampling_bind_groups,
547                pass: DownsamplePass::First,
548            };
549            let key_pass_2 = DownsamplePipelineKey {
550                pass: DownsamplePass::Second,
551                ..key_pass_1
552            };
553
554            let downsampling_constants_buffer =
555                create_downsampling_constants_buffer(&render_device, &render_queue, gpu_image);
556
557            let (downsampling_bind_group_pass_1, downsampling_bind_group_pass_2) =
558                create_downsampling_bind_groups(
559                    &render_device,
560                    &pipeline_cache,
561                    &mip_generation_resources,
562                    &downsampling_constants_buffer,
563                    gpu_image,
564                    key_pass_1,
565                    key_pass_2,
566                );
567
568            vacant_entry.insert(MipGenerationJobResources {
569                downsampling_pipeline_pass_1: specialized_pipelines.specialize(
570                    &pipeline_cache,
571                    &downsample_pipeline,
572                    key_pass_1,
573                ),
574                downsampling_pipeline_pass_2: specialized_pipelines.specialize(
575                    &pipeline_cache,
576                    &downsample_pipeline,
577                    key_pass_2,
578                ),
579                downsampling_bind_group_pass_1,
580                downsampling_bind_group_pass_2,
581            });
582        }
583    }
584
585    // Expire all bind groups for jobs that we didn't see this frame.
586    //
587    // Note that this logic ensures that we don't recreate bind groups for
588    // images that are updated every frame.
589    mip_generation_pipelines
590        .jobs
591        .retain(|asset_id, _| all_source_images.contains(asset_id));
592}
593
594impl DownsamplePipelineKey {
595    /// Returns the [`BindGroupLayoutDescriptor`] for the pipeline that this
596    /// key selects.
597    pub fn bind_group_layout(&self) -> BindGroupLayoutDescriptor {
598        let texture_sample_type = self.texture_format.sample_type(None, None).expect(
599        "Depth and multisample texture formats shouldn't have mip generation shaders to begin with",
600    );
601        let source_texture = if self.array_texture {
602            texture_2d_array(texture_sample_type)
603        } else {
604            texture_2d(texture_sample_type)
605        };
606        let storage = |access| {
607            if self.array_texture {
608                texture_storage_2d_array(self.texture_format, access)
609            } else {
610                texture_storage_2d(self.texture_format, access)
611            }
612        };
613        let mips_storage = storage(StorageTextureAccess::WriteOnly);
614
615        if self.combine_bind_groups {
616            let mip6_storage = storage(StorageTextureAccess::ReadWrite);
617            return BindGroupLayoutDescriptor::new(
618                "combined mip generation bind group layout",
619                &BindGroupLayoutEntries::sequential(
620                    ShaderStages::COMPUTE,
621                    (
622                        sampler(SamplerBindingType::Filtering),
623                        uniform_buffer::<DownsamplingConstants>(false),
624                        source_texture,
625                        mips_storage, // 1
626                        mips_storage, // 2
627                        mips_storage, // 3
628                        mips_storage, // 4
629                        mips_storage, // 5
630                        mip6_storage, // 6
631                        mips_storage, // 7
632                        mips_storage, // 8
633                        mips_storage, // 9
634                        mips_storage, // 10
635                        mips_storage, // 11
636                        mips_storage, // 12
637                    ),
638                ),
639            );
640        }
641
642        let label = match self.pass {
643            DownsamplePass::First => "mip generation bind group layout, pass 1",
644            DownsamplePass::Second => "mip generation bind group layout, pass 2",
645        };
646        BindGroupLayoutDescriptor::new(
647            label,
648            &BindGroupLayoutEntries::sequential(
649                ShaderStages::COMPUTE,
650                (
651                    sampler(SamplerBindingType::Filtering),
652                    uniform_buffer::<DownsamplingConstants>(false),
653                    source_texture, // input mip
654                    mips_storage,   // output mip 1
655                    mips_storage,   // output mip 2
656                    mips_storage,   // output mip 3
657                    mips_storage,   // output mip 4
658                    mips_storage,   // output mip 5
659                    mips_storage,   // output mip 6
660                ),
661            ),
662        )
663    }
664}
665
666/// Creates the bind groups for the single-pass downsampling shader associated
667/// with a single texture.
668///
669/// Depending on whether bind groups can be combined on this platform, this
670/// returns either two copies of a single bind group or two separate bind
671/// groups.
672fn create_downsampling_bind_groups(
673    render_device: &RenderDevice,
674    pipeline_cache: &PipelineCache,
675    mip_generation_resources: &MipGenerationResources,
676    downsampling_constants_buffer: &UniformBuffer<DownsamplingConstants>,
677    gpu_image: &GpuImage,
678    key_pass_1: DownsamplePipelineKey,
679    key_pass_2: DownsamplePipelineKey,
680) -> (BindGroup, BindGroup) {
681    let input_texture_view_pass_1 = gpu_image.texture.create_view(&TextureViewDescriptor {
682        label: Some("mip generation input texture view, pass 1"),
683        format: Some(gpu_image.texture.format()),
684        dimension: Some(TextureViewDimension::D2),
685        base_mip_level: 0,
686        mip_level_count: Some(1),
687        ..default()
688    });
689
690    // If we can combine downsampling bind groups on this platform, we only need
691    // one bind group.
692    if key_pass_1.combine_bind_groups {
693        let bind_group = render_device.create_bind_group(
694            Some("combined mip generation bind group"),
695            &pipeline_cache.get_bind_group_layout(&key_pass_1.bind_group_layout()),
696            &BindGroupEntries::sequential((
697                &mip_generation_resources.sampler,
698                downsampling_constants_buffer,
699                &input_texture_view_pass_1,
700                &get_mip_storage_view(render_device, gpu_image, 1),
701                &get_mip_storage_view(render_device, gpu_image, 2),
702                &get_mip_storage_view(render_device, gpu_image, 3),
703                &get_mip_storage_view(render_device, gpu_image, 4),
704                &get_mip_storage_view(render_device, gpu_image, 5),
705                &get_mip_storage_view(render_device, gpu_image, 6),
706                &get_mip_storage_view(render_device, gpu_image, 7),
707                &get_mip_storage_view(render_device, gpu_image, 8),
708                &get_mip_storage_view(render_device, gpu_image, 9),
709                &get_mip_storage_view(render_device, gpu_image, 10),
710                &get_mip_storage_view(render_device, gpu_image, 11),
711                &get_mip_storage_view(render_device, gpu_image, 12),
712            )),
713        );
714        return (bind_group.clone(), bind_group);
715    }
716
717    // Otherwise, create two separate bind groups.
718
719    let input_texture_view_pass_2 = gpu_image.texture.create_view(&TextureViewDescriptor {
720        label: Some("mip generation input texture view, pass 2"),
721        format: Some(gpu_image.texture.format()),
722        dimension: Some(TextureViewDimension::D2),
723        base_mip_level: gpu_image.texture_descriptor.mip_level_count.min(6),
724        mip_level_count: Some(1),
725        ..default()
726    });
727
728    let bind_group_pass_1 = render_device.create_bind_group(
729        "mip generation bind group, pass 1",
730        &pipeline_cache.get_bind_group_layout(&key_pass_1.bind_group_layout()),
731        &BindGroupEntries::sequential((
732            &mip_generation_resources.sampler,
733            downsampling_constants_buffer,
734            &input_texture_view_pass_1,
735            &get_mip_storage_view(render_device, gpu_image, 1),
736            &get_mip_storage_view(render_device, gpu_image, 2),
737            &get_mip_storage_view(render_device, gpu_image, 3),
738            &get_mip_storage_view(render_device, gpu_image, 4),
739            &get_mip_storage_view(render_device, gpu_image, 5),
740            &get_mip_storage_view(render_device, gpu_image, 6),
741        )),
742    );
743    let bind_group_pass_2 = render_device.create_bind_group(
744        "mip generation bind group, pass 2",
745        &pipeline_cache.get_bind_group_layout(&key_pass_2.bind_group_layout()),
746        &BindGroupEntries::sequential((
747            &mip_generation_resources.sampler,
748            downsampling_constants_buffer,
749            &input_texture_view_pass_2,
750            &get_mip_storage_view(render_device, gpu_image, 7),
751            &get_mip_storage_view(render_device, gpu_image, 8),
752            &get_mip_storage_view(render_device, gpu_image, 9),
753            &get_mip_storage_view(render_device, gpu_image, 10),
754            &get_mip_storage_view(render_device, gpu_image, 11),
755            &get_mip_storage_view(render_device, gpu_image, 12),
756        )),
757    );
758
759    (bind_group_pass_1, bind_group_pass_2)
760}
761
762/// Creates the uniform buffer containing the [`DownsamplingConstants`] for a
763/// single texture.
764fn create_downsampling_constants_buffer(
765    render_device: &RenderDevice,
766    render_queue: &RenderQueue,
767    gpu_image: &GpuImage,
768) -> UniformBuffer<DownsamplingConstants> {
769    let downsampling_constants = DownsamplingConstants {
770        mips: gpu_image.texture_descriptor.mip_level_count,
771        inverse_input_size: vec2(
772            1.0 / gpu_image.texture_descriptor.size.width as f32,
773            1.0 / gpu_image.texture_descriptor.size.height as f32,
774        ),
775        _padding: 0,
776    };
777
778    let mut downsampling_constants_buffer = UniformBuffer::from(downsampling_constants);
779    downsampling_constants_buffer.write_buffer(render_device, render_queue);
780    downsampling_constants_buffer
781}
782
783/// Returns a view of the given mipmap level of a texture, suitable for
784/// attachment as a texture storage binding.
785fn get_mip_storage_view(
786    render_device: &RenderDevice,
787    gpu_image: &GpuImage,
788    level: u32,
789) -> TextureView {
790    // If `level` represents an actual mip level of the image, return a view to
791    // it.
792    if level < gpu_image.texture_descriptor.mip_level_count {
793        return gpu_image.texture.create_view(&TextureViewDescriptor {
794            label: Some(&*format!(
795                "mip downsampling storage view {}/{}",
796                level, gpu_image.texture_descriptor.mip_level_count
797            )),
798            format: Some(gpu_image.texture_descriptor.format),
799            dimension: Some(TextureViewDimension::D2),
800            aspect: TextureAspect::All,
801            base_mip_level: level,
802            mip_level_count: Some(1),
803            base_array_layer: 0,
804            array_layer_count: Some(1),
805            usage: Some(TextureUsages::STORAGE_BINDING),
806        });
807    }
808
809    // Otherwise, create a dummy texture and return a view to that.
810
811    let dummy_texture = render_device.create_texture(&TextureDescriptor {
812        label: Some(&*format!(
813            "mip downsampling dummy storage view {}/{}",
814            level, gpu_image.texture_descriptor.mip_level_count
815        )),
816        size: Extent3d {
817            width: 1,
818            height: 1,
819            depth_or_array_layers: 1,
820        },
821        mip_level_count: 1,
822        sample_count: 1,
823        dimension: TextureDimension::D2,
824        format: gpu_image.texture_descriptor.format,
825        usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
826        view_formats: &[],
827    });
828
829    dummy_texture.create_view(&TextureViewDescriptor::default())
830}
831
832/// A system that clears out the [`MipGenerationJobs`] resource in preparation
833/// for a new frame.
834fn reset_mip_generation_jobs(mut mip_generation_jobs: ResMut<MipGenerationJobs>) {
835    mip_generation_jobs.clear();
836}
837
838/// Returns true if the current platform can use a single bind group for
839/// single-pass downsampling.
840///
841/// If this platform must use two separate bind groups, one for each pass, this
842/// function returns false.
843pub fn can_combine_downsampling_bind_groups(
844    render_adapter: &RenderAdapter,
845    render_device: &RenderDevice,
846) -> bool {
847    // Determine whether we can use a single, large bind group for all mip outputs
848    let storage_texture_limit = render_device.limits().max_storage_textures_per_shader_stage;
849
850    // Determine whether we can read and write to the same rgba16f storage texture
851    let read_write_support = render_adapter
852        .get_texture_format_features(TextureFormat::Rgba16Float)
853        .flags
854        .contains(TextureFormatFeatureFlags::STORAGE_READ_WRITE);
855
856    // Combine the bind group and use read-write storage if it is supported
857    storage_texture_limit >= REQUIRED_STORAGE_TEXTURES && read_write_support
858}