bevy_core_pipeline/
msaa_writeback.rs

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