bevy_post_process/
msaa_writeback.rs

1use bevy_app::{App, Plugin};
2use bevy_camera::MsaaWriteback;
3use bevy_color::LinearRgba;
4use bevy_core_pipeline::{
5    blit::{BlitPipeline, BlitPipelineKey},
6    core_2d::graph::{Core2d, Node2d},
7    core_3d::graph::{Core3d, Node3d},
8};
9use bevy_ecs::{prelude::*, query::QueryItem};
10use bevy_render::{
11    camera::ExtractedCamera,
12    diagnostic::RecordDiagnostics,
13    render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner},
14    render_resource::*,
15    renderer::RenderContext,
16    view::{Msaa, ViewTarget},
17    Render, RenderApp, RenderSystems,
18};
19
20/// This enables "msaa writeback" support for the `core_2d` and `core_3d` pipelines, which can be enabled on cameras
21/// using [`bevy_camera::Camera::msaa_writeback`]. See the docs on that field for more information.
22#[derive(Default)]
23pub struct MsaaWritebackPlugin;
24
25impl Plugin for MsaaWritebackPlugin {
26    fn build(&self, app: &mut App) {
27        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
28            return;
29        };
30        render_app.add_systems(
31            Render,
32            prepare_msaa_writeback_pipelines.in_set(RenderSystems::Prepare),
33        );
34        {
35            render_app
36                .add_render_graph_node::<ViewNodeRunner<MsaaWritebackNode>>(
37                    Core2d,
38                    Node2d::MsaaWriteback,
39                )
40                .add_render_graph_edge(Core2d, Node2d::MsaaWriteback, Node2d::StartMainPass);
41        }
42        {
43            render_app
44                .add_render_graph_node::<ViewNodeRunner<MsaaWritebackNode>>(
45                    Core3d,
46                    Node3d::MsaaWriteback,
47                )
48                .add_render_graph_edge(Core3d, Node3d::MsaaWriteback, Node3d::StartMainPass);
49        }
50    }
51}
52
53#[derive(Default)]
54pub struct MsaaWritebackNode;
55
56impl ViewNode for MsaaWritebackNode {
57    type ViewQuery = (
58        &'static ViewTarget,
59        &'static MsaaWritebackBlitPipeline,
60        &'static Msaa,
61    );
62
63    fn run<'w>(
64        &self,
65        _graph: &mut RenderGraphContext,
66        render_context: &mut RenderContext<'w>,
67        (target, blit_pipeline_id, msaa): QueryItem<'w, '_, Self::ViewQuery>,
68        world: &'w World,
69    ) -> Result<(), NodeRunError> {
70        if *msaa == Msaa::Off {
71            return Ok(());
72        }
73
74        let blit_pipeline = world.resource::<BlitPipeline>();
75        let pipeline_cache = world.resource::<PipelineCache>();
76        let Some(pipeline) = pipeline_cache.get_render_pipeline(blit_pipeline_id.0) else {
77            return Ok(());
78        };
79
80        let diagnostics = render_context.diagnostic_recorder();
81
82        // The current "main texture" needs to be bound as an input resource, and we need the "other"
83        // unused target to be the "resolve target" for the MSAA write. Therefore this is the same
84        // as a post process write!
85        let post_process = target.post_process_write();
86
87        let pass_descriptor = RenderPassDescriptor {
88            label: Some("msaa_writeback"),
89            // The target's "resolve target" is the "destination" in post_process.
90            // We will indirectly write the results to the "destination" using
91            // the MSAA resolve step.
92            color_attachments: &[Some(RenderPassColorAttachment {
93                // If MSAA is enabled, then the sampled texture will always exist
94                view: target.sampled_main_texture_view().unwrap(),
95                depth_slice: None,
96                resolve_target: Some(post_process.destination),
97                ops: Operations {
98                    load: LoadOp::Clear(LinearRgba::BLACK.into()),
99                    store: StoreOp::Store,
100                },
101            })],
102            depth_stencil_attachment: None,
103            timestamp_writes: None,
104            occlusion_query_set: None,
105        };
106
107        let bind_group = blit_pipeline.create_bind_group(
108            render_context.render_device(),
109            post_process.source,
110            pipeline_cache,
111        );
112
113        let mut render_pass = render_context
114            .command_encoder()
115            .begin_render_pass(&pass_descriptor);
116        let pass_span = diagnostics.pass_span(&mut render_pass, "msaa_writeback");
117
118        render_pass.set_pipeline(pipeline);
119        render_pass.set_bind_group(0, &bind_group, &[]);
120        render_pass.draw(0..3, 0..1);
121
122        pass_span.end(&mut render_pass);
123
124        Ok(())
125    }
126}
127
128#[derive(Component)]
129pub struct MsaaWritebackBlitPipeline(CachedRenderPipelineId);
130
131fn prepare_msaa_writeback_pipelines(
132    mut commands: Commands,
133    pipeline_cache: Res<PipelineCache>,
134    mut pipelines: ResMut<SpecializedRenderPipelines<BlitPipeline>>,
135    blit_pipeline: Res<BlitPipeline>,
136    view_targets: Query<(Entity, &ViewTarget, &ExtractedCamera, &Msaa)>,
137) {
138    for (entity, view_target, camera, msaa) in view_targets.iter() {
139        // Determine if we should do MSAA writeback based on the camera's setting
140        let should_writeback = match camera.msaa_writeback {
141            MsaaWriteback::Off => false,
142            MsaaWriteback::Auto => camera.sorted_camera_index_for_target > 0,
143            MsaaWriteback::Always => true,
144        };
145
146        if msaa.samples() > 1 && should_writeback {
147            let key = BlitPipelineKey {
148                texture_format: view_target.main_texture_format(),
149                samples: msaa.samples(),
150                blend_state: None,
151            };
152
153            let pipeline = pipelines.specialize(&pipeline_cache, &blit_pipeline, key);
154            commands
155                .entity(entity)
156                .insert(MsaaWritebackBlitPipeline(pipeline));
157        } else {
158            // This isn't strictly necessary now, but if we move to retained render entity state I don't
159            // want this to silently break
160            commands
161                .entity(entity)
162                .remove::<MsaaWritebackBlitPipeline>();
163        }
164    }
165}