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_render::{
10    camera::ExtractedCamera,
11    diagnostic::RecordDiagnostics,
12    render_resource::*,
13    renderer::{RenderContext, ViewQuery},
14    view::{Msaa, ViewTarget},
15    Render, RenderApp, RenderSystems,
16};
17
18/// This enables "msaa writeback" support for the `core_2d` and `core_3d` pipelines, which can be enabled on cameras
19/// using [`bevy_camera::Camera::msaa_writeback`]. See the docs on that field for more information.
20#[derive(Default)]
21pub struct MsaaWritebackPlugin;
22
23impl Plugin for MsaaWritebackPlugin {
24    fn build(&self, app: &mut App) {
25        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
26            return;
27        };
28        render_app.add_systems(
29            Render,
30            prepare_msaa_writeback_pipelines.in_set(RenderSystems::Prepare),
31        );
32        render_app.add_systems(Core3d, msaa_writeback.before(Core3dSystems::MainPass));
33        render_app.add_systems(Core2d, msaa_writeback.before(Core2dSystems::MainPass));
34    }
35}
36
37pub(crate) fn msaa_writeback(
38    view: ViewQuery<(&ViewTarget, &MsaaWritebackBlitPipeline, &Msaa)>,
39    blit_pipeline: Res<BlitPipeline>,
40    pipeline_cache: Res<PipelineCache>,
41    mut ctx: RenderContext,
42) {
43    let (target, blit_pipeline_id, msaa) = view.into_inner();
44
45    if *msaa == Msaa::Off {
46        return;
47    }
48
49    let Some(pipeline) = pipeline_cache.get_render_pipeline(blit_pipeline_id.0) else {
50        return;
51    };
52
53    // The current "main texture" needs to be bound as an input resource, and we need the "other"
54    // unused target to be the "resolve target" for the MSAA write. Therefore this is the same
55    // as a post process write!
56    let post_process = target.post_process_write();
57
58    let pass_descriptor = RenderPassDescriptor {
59        label: Some("msaa_writeback"),
60        // The target's "resolve target" is the "destination" in post_process.
61        // We will indirectly write the results to the "destination" using
62        // the MSAA resolve step.
63        color_attachments: &[Some(RenderPassColorAttachment {
64            // If MSAA is enabled, then the sampled texture will always exist
65            view: target.sampled_main_texture_view().unwrap(),
66            depth_slice: None,
67            resolve_target: Some(post_process.destination),
68            ops: Operations {
69                load: LoadOp::Clear(LinearRgba::BLACK.into()),
70                store: StoreOp::Store,
71            },
72        })],
73        depth_stencil_attachment: None,
74        timestamp_writes: None,
75        occlusion_query_set: None,
76        multiview_mask: None,
77    };
78
79    let bind_group =
80        blit_pipeline.create_bind_group(ctx.render_device(), post_process.source, &pipeline_cache);
81
82    let diagnostics = ctx.diagnostic_recorder();
83    let diagnostics = diagnostics.as_deref();
84    let time_span = diagnostics.time_span(ctx.command_encoder(), "msaa_writeback");
85
86    {
87        let mut render_pass = ctx.command_encoder().begin_render_pass(&pass_descriptor);
88
89        render_pass.set_pipeline(pipeline);
90        render_pass.set_bind_group(0, &bind_group, &[]);
91        render_pass.draw(0..3, 0..1);
92    }
93
94    time_span.end(ctx.command_encoder());
95}
96
97#[derive(Component)]
98pub struct MsaaWritebackBlitPipeline(CachedRenderPipelineId);
99
100fn prepare_msaa_writeback_pipelines(
101    mut commands: Commands,
102    pipeline_cache: Res<PipelineCache>,
103    mut pipelines: ResMut<SpecializedRenderPipelines<BlitPipeline>>,
104    blit_pipeline: Res<BlitPipeline>,
105    view_targets: Query<(Entity, &ViewTarget, &ExtractedCamera, &Msaa)>,
106) {
107    for (entity, view_target, camera, msaa) in view_targets.iter() {
108        // Determine if we should do MSAA writeback based on the camera's setting
109        let should_writeback = match camera.msaa_writeback {
110            MsaaWriteback::Off => false,
111            // writeback is needed when the main pass must load existing content
112            // from the main texture, either because another camera already
113            // rendered to this target or because this camera preserves content across
114            // frames via load op load. otherwise we'd read from an ephemeral sampled
115            // texture that doesn't have the real content
116            MsaaWriteback::Auto => {
117                camera.sorted_camera_index_for_target > 0
118                    || matches!(camera.clear_color, ClearColorConfig::None)
119            }
120            MsaaWriteback::Always => true,
121        };
122
123        if msaa.samples() > 1 && should_writeback {
124            let key = BlitPipelineKey {
125                target_format: view_target.main_texture_format(),
126                samples: msaa.samples(),
127                blend_state: None,
128                source_space: None,
129            };
130
131            let pipeline = pipelines.specialize(&pipeline_cache, &blit_pipeline, key);
132            commands
133                .entity(entity)
134                .insert(MsaaWritebackBlitPipeline(pipeline));
135        } else {
136            // This isn't strictly necessary now, but if we move to retained render entity state I don't
137            // want this to silently break
138            commands
139                .entity(entity)
140                .remove::<MsaaWritebackBlitPipeline>();
141        }
142    }
143}