Skip to main content

bevy_post_process/
msaa_writeback.rs

1use bevy_app::{App, Plugin};
2use bevy_camera::{ClearColorConfig, MsaaWriteback};
3use bevy_color::LinearRgba;
4use bevy_core_pipeline::{
5    blit::{BlitPipeline, BlitPipelineKey},
6    schedule::{Core2d, Core2dSystems, Core3d, Core3dSystems},
7};
8use bevy_ecs::prelude::*;
9use bevy_platform::collections::HashMap;
10use bevy_render::{
11    camera::ExtractedCamera,
12    diagnostic::RecordDiagnostics,
13    render_resource::*,
14    renderer::{RenderContext, ViewQuery},
15    view::{Msaa, ViewTarget},
16    Render, RenderApp, RenderSystems,
17};
18
19/// This enables "msaa writeback" support for the `core_2d` and `core_3d` pipelines, which can be enabled on cameras
20/// using [`bevy_camera::Camera::msaa_writeback`]. See the docs on that field for more information.
21#[derive(#[automatically_derived]impl ::core::default::Default for MsaaWritebackPlugin
    {
    #[inline]
    fn default() -> Self { MsaaWritebackPlugin {} }
}Default)]
22pub struct MsaaWritebackPlugin;
23
24impl Plugin for MsaaWritebackPlugin {
25    fn build(&self, app: &mut App) {
26        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
27            return;
28        };
29        render_app.add_systems(
30            Render,
31            prepare_msaa_writeback_pipelines.in_set(RenderSystems::Prepare),
32        );
33        render_app.add_systems(Core3d, msaa_writeback.before(Core3dSystems::MainPass));
34        render_app.add_systems(Core2d, msaa_writeback.before(Core2dSystems::MainPass));
35    }
36}
37
38pub(crate) fn msaa_writeback(
39    view: ViewQuery<(&ViewTarget, &MsaaWritebackBlitPipeline, &Msaa)>,
40    blit_pipeline: Res<BlitPipeline>,
41    pipeline_cache: Res<PipelineCache>,
42    mut ctx: RenderContext,
43) {
44    let (target, blit_pipeline_id, msaa) = view.into_inner();
45
46    if *msaa == Msaa::Off {
47        return;
48    }
49
50    let Some(pipeline) = pipeline_cache.get_render_pipeline(blit_pipeline_id.0) else {
51        return;
52    };
53
54    // The current "main texture" needs to be bound as an input resource, and we need the "other"
55    // unused target to be the "resolve target" for the MSAA write. Therefore this is the same
56    // as a post process write!
57    let post_process = target.post_process_write();
58
59    let pass_descriptor = RenderPassDescriptor {
60        label: Some("msaa_writeback"),
61        // The target's "resolve target" is the "destination" in post_process.
62        // We will indirectly write the results to the "destination" using
63        // the MSAA resolve step.
64        color_attachments: &[Some(RenderPassColorAttachment {
65            // If MSAA is enabled, then the sampled texture will always exist
66            view: target.sampled_main_texture_view().unwrap(),
67            depth_slice: None,
68            resolve_target: Some(post_process.destination),
69            ops: Operations {
70                load: LoadOp::Clear(LinearRgba::BLACK.into()),
71                store: StoreOp::Store,
72            },
73        })],
74        depth_stencil_attachment: None,
75        timestamp_writes: None,
76        occlusion_query_set: None,
77        multiview_mask: None,
78    };
79
80    let bind_group =
81        blit_pipeline.create_bind_group(ctx.render_device(), post_process.source, &pipeline_cache);
82
83    let diagnostics = ctx.diagnostic_recorder();
84    let diagnostics = diagnostics.as_deref();
85    let time_span = diagnostics.time_span(ctx.command_encoder(), "msaa_writeback");
86
87    {
88        let mut render_pass = ctx.command_encoder().begin_render_pass(&pass_descriptor);
89
90        render_pass.set_pipeline(pipeline);
91        render_pass.set_bind_group(0, &bind_group, &[]);
92        render_pass.draw(0..3, 0..1);
93    }
94
95    time_span.end(ctx.command_encoder());
96}
97
98#[derive(impl bevy_ecs::component::Component for MsaaWritebackBlitPipeline where
    Self: ::core::marker::Send + ::core::marker::Sync + 'static {
    const STORAGE_TYPE: bevy_ecs::component::StorageType =
        bevy_ecs::component::StorageType::Table;
    type Mutability = bevy_ecs::component::Mutable;
    fn register_required_components(_requiree:
            bevy_ecs::component::ComponentId,
        required_components:
            &mut bevy_ecs::component::RequiredComponentsRegistrator) {}
    fn clone_behavior() -> bevy_ecs::component::ComponentCloneBehavior {
        use bevy_ecs::component::{
            DefaultCloneBehaviorBase, DefaultCloneBehaviorViaClone,
        };
        (&&&bevy_ecs::component::DefaultCloneBehaviorSpecialization::<Self>::default()).default_clone_behavior()
    }
    fn relationship_accessor()
        ->
            ::core::option::Option<bevy_ecs::relationship::ComponentRelationshipAccessor<Self>> {
        ::core::option::Option::None
    }
}Component)]
99pub struct MsaaWritebackBlitPipeline(CachedRenderPipelineId);
100
101fn prepare_msaa_writeback_pipelines(
102    mut commands: Commands,
103    pipeline_cache: Res<PipelineCache>,
104    mut pipelines: ResMut<SpecializedRenderPipelines<BlitPipeline>>,
105    blit_pipeline: Res<BlitPipeline>,
106    view_targets: Query<(Entity, &ViewTarget, &ExtractedCamera, &Msaa)>,
107) {
108    // the lowest sorted camera index rendering into each main texture. cameras on one render
109    // target can still end up with different main textures, since Hdr and SDR cameras use
110    // different texture formats, so this is keyed by texture rather than target
111    let mut first_writer_indices = <HashMap<TextureId, usize>>::default();
112    for (_, view_target, camera, _) in view_targets.iter() {
113        first_writer_indices
114            .entry(view_target.main_texture().id())
115            .and_modify(|first| *first = (*first).min(camera.sorted_camera_index_for_target))
116            .or_insert(camera.sorted_camera_index_for_target);
117    }
118
119    for (entity, view_target, camera, msaa) in view_targets.iter() {
120        // Determine if we should do MSAA writeback based on the camera's setting
121        let should_writeback = match camera.msaa_writeback {
122            MsaaWriteback::Off => false,
123            // writeback is needed when the main pass must load existing content
124            // from the main texture, either because a fullscreen camera composites
125            // over what another camera already rendered into this main texture or
126            // because this camera preserves content across frames via load op load.
127            // otherwise we'd read from an ephemeral sampled texture that doesn't have
128            // the real content
129            MsaaWriteback::Auto => {
130                #[allow(non_exhaustive_omitted_patterns)] match camera.clear_color {
    ClearColorConfig::None => true,
    _ => false,
}matches!(camera.clear_color, ClearColorConfig::None)
131                    || (camera.viewport.is_none()
132                        && first_writer_indices[&view_target.main_texture().id()]
133                            < camera.sorted_camera_index_for_target)
134            }
135            MsaaWriteback::Always => true,
136        };
137
138        if msaa.samples() > 1 && should_writeback {
139            let key = BlitPipelineKey {
140                target_format: view_target.main_texture_format(),
141                samples: msaa.samples(),
142                blend_state: None,
143                source_space: None,
144            };
145
146            let pipeline = pipelines.specialize(&pipeline_cache, &blit_pipeline, key);
147            commands
148                .entity(entity)
149                .insert(MsaaWritebackBlitPipeline(pipeline));
150        } else {
151            // This isn't strictly necessary now, but if we move to retained render entity state I don't
152            // want this to silently break
153            commands
154                .entity(entity)
155                .remove::<MsaaWritebackBlitPipeline>();
156        }
157    }
158}