From 852787ec61e03c640886687e89bcc8da25dcf79d Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Wed, 1 Mar 2023 20:35:13 +0000 Subject: [PATCH] Camera Output Modes, MSAA Writeback, and BlitPipeline (#7671) # Objective Alternative to #7490. I wrote all of the code in this PR, but I have added @robtfm as co-author on commits that build on ideas from #7490. I would not have been able to solve these problems on my own without much more time investment and I'm largely just rephrasing the ideas from that PR. Fixes #7435 Fixes #7361 Fixes #5721 ## Solution This implements the solution I [outlined here](https://github.com/bevyengine/bevy/pull/7490#issuecomment-1426580633). * Adds "msaa writeback" as an explicit "msaa camera feature" and default to msaa_writeback: true for each camera. If this is true, a camera has MSAA enabled, and it isn't the first camera for the target, add a writeback before the main pass for that camera. * Adds a CameraOutputMode, which can be used to configure if (and how) the results of a camera's rendering will be written to the final RenderTarget output texture (via the upscaling node). The `blend_state` and `color_attachment_load_op` are now configurable, giving much more control over how a camera will write to the output texture. * Made cameras with the same target share the same main_texture tracker by using `Arc`, which ensures continuity across cameras. This was previously broken / could produce weird results in some cases. `ViewTarget::main_texture()` is now correct in every context. * Added a new generic / specializable BlitPipeline, which the new MsaaWritebackNode uses internally. The UpscalingPipelineNode now uses BlitPipeline instead of its own pipeline. We might ultimately need to fork this back out if we choose to add more configurability to the upscaling, but for now this will save on binary size by not embedding the same shader twice. * Moved the "camera sorting" logic from the camera driver node to its own system. The results are now stored in the `SortedCameras` resource, which can be used anywhere in the renderer. MSAA writeback makes use of this. --- ## Changelog - Added `Camera::msaa_writeback` which can enable and disable msaa writeback. - Added specializable `BlitPipeline` and ported the upscaling node to use this. - Added SortedCameras, exposing information that was previously internal to the camera driver node. - Made cameras with the same target share the same main_texture tracker, which ensures continuity across cameras. --- .../upscaling.wgsl => blit/blit.wgsl} | 8 +- crates/bevy_core_pipeline/src/blit/mod.rs | 104 ++++++++++ crates/bevy_core_pipeline/src/bloom/mod.rs | 6 +- crates/bevy_core_pipeline/src/core_2d/mod.rs | 1 + crates/bevy_core_pipeline/src/core_3d/mod.rs | 1 + crates/bevy_core_pipeline/src/lib.rs | 6 + .../bevy_core_pipeline/src/msaa_writeback.rs | 181 ++++++++++++++++++ .../bevy_core_pipeline/src/upscaling/mod.rs | 119 ++---------- .../bevy_core_pipeline/src/upscaling/node.rs | 36 +++- crates/bevy_render/src/camera/camera.rs | 114 ++++++++++- .../src/camera/camera_driver_node.rs | 49 ++--- crates/bevy_render/src/camera/mod.rs | 8 +- crates/bevy_render/src/view/mod.rs | 14 +- 13 files changed, 487 insertions(+), 160 deletions(-) rename crates/bevy_core_pipeline/src/{upscaling/upscaling.wgsl => blit/blit.wgsl} (54%) create mode 100644 crates/bevy_core_pipeline/src/blit/mod.rs create mode 100644 crates/bevy_core_pipeline/src/msaa_writeback.rs diff --git a/crates/bevy_core_pipeline/src/upscaling/upscaling.wgsl b/crates/bevy_core_pipeline/src/blit/blit.wgsl similarity index 54% rename from crates/bevy_core_pipeline/src/upscaling/upscaling.wgsl rename to crates/bevy_core_pipeline/src/blit/blit.wgsl index 56aae879208659..f84c3724987ac1 100644 --- a/crates/bevy_core_pipeline/src/upscaling/upscaling.wgsl +++ b/crates/bevy_core_pipeline/src/blit/blit.wgsl @@ -1,13 +1,11 @@ #import bevy_core_pipeline::fullscreen_vertex_shader @group(0) @binding(0) -var hdr_texture: texture_2d; +var in_texture: texture_2d; @group(0) @binding(1) -var hdr_sampler: sampler; +var in_sampler: sampler; @fragment fn fs_main(in: FullscreenVertexOutput) -> @location(0) vec4 { - let hdr_color = textureSample(hdr_texture, hdr_sampler, in.uv); - - return hdr_color; + return textureSample(in_texture, in_sampler, in.uv); } diff --git a/crates/bevy_core_pipeline/src/blit/mod.rs b/crates/bevy_core_pipeline/src/blit/mod.rs new file mode 100644 index 00000000000000..3cb17d0864db46 --- /dev/null +++ b/crates/bevy_core_pipeline/src/blit/mod.rs @@ -0,0 +1,104 @@ +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, HandleUntyped}; +use bevy_ecs::prelude::*; +use bevy_reflect::TypeUuid; +use bevy_render::{render_resource::*, renderer::RenderDevice, RenderApp}; + +use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; + +pub const BLIT_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2312396983770133547); + +/// Adds support for specialized "blit pipelines", which can be used to write one texture to another. +pub struct BlitPlugin; + +impl Plugin for BlitPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!(app, BLIT_SHADER_HANDLE, "blit.wgsl", Shader::from_wgsl); + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return + }; + + render_app + .init_resource::() + .init_resource::>(); + } +} + +#[derive(Resource)] +pub struct BlitPipeline { + pub texture_bind_group: BindGroupLayout, + pub sampler: Sampler, +} + +impl FromWorld for BlitPipeline { + fn from_world(render_world: &mut World) -> Self { + let render_device = render_world.resource::(); + + let texture_bind_group = + render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("blit_bind_group_layout"), + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: false }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::NonFiltering), + count: None, + }, + ], + }); + + let sampler = render_device.create_sampler(&SamplerDescriptor::default()); + + BlitPipeline { + texture_bind_group, + sampler, + } + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +pub struct BlitPipelineKey { + pub texture_format: TextureFormat, + pub blend_state: Option, + pub samples: u32, +} + +impl SpecializedRenderPipeline for BlitPipeline { + type Key = BlitPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + RenderPipelineDescriptor { + label: Some("blit pipeline".into()), + layout: vec![self.texture_bind_group.clone()], + vertex: fullscreen_shader_vertex_state(), + fragment: Some(FragmentState { + shader: BLIT_SHADER_HANDLE.typed(), + shader_defs: vec![], + entry_point: "fs_main".into(), + targets: vec![Some(ColorTargetState { + format: key.texture_format, + blend: key.blend_state, + write_mask: ColorWrites::ALL, + })], + }), + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState { + count: key.samples, + ..Default::default() + }, + push_constant_ranges: Vec::new(), + } + } +} diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index adc8fa632726a9..18b1b6b6108551 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -513,7 +513,11 @@ impl FromWorld for BloomPipelines { dst_factor: BlendFactor::One, operation: BlendOperation::Add, }, - alpha: BlendComponent::REPLACE, + alpha: BlendComponent { + src_factor: BlendFactor::One, + dst_factor: BlendFactor::One, + operation: BlendOperation::Max, + }, }), write_mask: ColorWrites::ALL, })], diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 233671a159ab0b..573bf0d5a0f700 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -7,6 +7,7 @@ pub mod graph { pub const VIEW_ENTITY: &str = "view_entity"; } pub mod node { + pub const MSAA_WRITEBACK: &str = "msaa_writeback"; pub const MAIN_PASS: &str = "main_pass"; pub const BLOOM: &str = "bloom"; pub const TONEMAPPING: &str = "tonemapping"; diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index ab3f68df209043..71cfaf6d402f3a 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -7,6 +7,7 @@ pub mod graph { pub const VIEW_ENTITY: &str = "view_entity"; } pub mod node { + pub const MSAA_WRITEBACK: &str = "msaa_writeback"; pub const PREPASS: &str = "prepass"; pub const MAIN_PASS: &str = "main_pass"; pub const BLOOM: &str = "bloom"; diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 5b0fe9eaea21cc..4faa1cd83f7f9e 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -1,9 +1,11 @@ +pub mod blit; pub mod bloom; pub mod clear_color; pub mod core_2d; pub mod core_3d; pub mod fullscreen_vertex_shader; pub mod fxaa; +pub mod msaa_writeback; pub mod prepass; pub mod tonemapping; pub mod upscaling; @@ -18,12 +20,14 @@ pub mod prelude { } use crate::{ + blit::BlitPlugin, bloom::BloomPlugin, clear_color::{ClearColor, ClearColorConfig}, core_2d::Core2dPlugin, core_3d::Core3dPlugin, fullscreen_vertex_shader::FULLSCREEN_SHADER_HANDLE, fxaa::FxaaPlugin, + msaa_writeback::MsaaWritebackPlugin, prepass::{DepthPrepass, NormalPrepass}, tonemapping::TonemappingPlugin, upscaling::UpscalingPlugin, @@ -52,6 +56,8 @@ impl Plugin for CorePipelinePlugin { .add_plugin(ExtractResourcePlugin::::default()) .add_plugin(Core2dPlugin) .add_plugin(Core3dPlugin) + .add_plugin(BlitPlugin) + .add_plugin(MsaaWritebackPlugin) .add_plugin(TonemappingPlugin) .add_plugin(UpscalingPlugin) .add_plugin(BloomPlugin) diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs new file mode 100644 index 00000000000000..2f8122d193d4c4 --- /dev/null +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -0,0 +1,181 @@ +use crate::blit::{BlitPipeline, BlitPipelineKey}; +use bevy_app::{App, Plugin}; +use bevy_ecs::prelude::*; +use bevy_render::{ + camera::ExtractedCamera, + render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext, SlotInfo, SlotType}, + renderer::RenderContext, + view::{Msaa, ViewTarget}, + RenderSet, +}; +use bevy_render::{render_resource::*, RenderApp}; + +/// This enables "msaa writeback" support for the `core_2d` and `core_3d` pipelines, which can be enabled on cameras +/// using [`bevy_render::camera::Camera::msaa_writeback`]. See the docs on that field for more information. +pub struct MsaaWritebackPlugin; + +impl Plugin for MsaaWritebackPlugin { + fn build(&self, app: &mut App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return + }; + + render_app.add_system(queue_msaa_writeback_pipelines.in_set(RenderSet::Queue)); + let msaa_writeback_2d = MsaaWritebackNode::new(&mut render_app.world); + let msaa_writeback_3d = MsaaWritebackNode::new(&mut render_app.world); + let mut graph = render_app.world.resource_mut::(); + if let Some(core_2d) = graph.get_sub_graph_mut(crate::core_2d::graph::NAME) { + let input_node = core_2d.input_node().id; + core_2d.add_node( + crate::core_2d::graph::node::MSAA_WRITEBACK, + msaa_writeback_2d, + ); + core_2d.add_node_edge( + crate::core_2d::graph::node::MSAA_WRITEBACK, + crate::core_2d::graph::node::MAIN_PASS, + ); + core_2d.add_slot_edge( + input_node, + crate::core_2d::graph::input::VIEW_ENTITY, + crate::core_2d::graph::node::MSAA_WRITEBACK, + MsaaWritebackNode::IN_VIEW, + ); + } + + if let Some(core_3d) = graph.get_sub_graph_mut(crate::core_3d::graph::NAME) { + let input_node = core_3d.input_node().id; + core_3d.add_node( + crate::core_3d::graph::node::MSAA_WRITEBACK, + msaa_writeback_3d, + ); + core_3d.add_node_edge( + crate::core_3d::graph::node::MSAA_WRITEBACK, + crate::core_3d::graph::node::MAIN_PASS, + ); + core_3d.add_slot_edge( + input_node, + crate::core_3d::graph::input::VIEW_ENTITY, + crate::core_3d::graph::node::MSAA_WRITEBACK, + MsaaWritebackNode::IN_VIEW, + ); + } + } +} + +pub struct MsaaWritebackNode { + cameras: QueryState<(&'static ViewTarget, &'static MsaaWritebackBlitPipeline)>, +} + +impl MsaaWritebackNode { + pub const IN_VIEW: &'static str = "view"; + + pub fn new(world: &mut World) -> Self { + Self { + cameras: world.query(), + } + } +} + +impl Node for MsaaWritebackNode { + fn input(&self) -> Vec { + vec![SlotInfo::new(Self::IN_VIEW, SlotType::Entity)] + } + fn update(&mut self, world: &mut World) { + self.cameras.update_archetypes(world); + } + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + if let Ok((target, blit_pipeline_id)) = self.cameras.get_manual(world, view_entity) { + let blit_pipeline = world.resource::(); + let pipeline_cache = world.resource::(); + let pipeline = pipeline_cache + .get_render_pipeline(blit_pipeline_id.0) + .unwrap(); + + // The current "main texture" needs to be bound as an input resource, and we need the "other" + // unused target to be the "resolve target" for the MSAA write. Therefore this is the same + // as a post process write! + let post_process = target.post_process_write(); + + let pass_descriptor = RenderPassDescriptor { + label: Some("msaa_writeback"), + // The target's "resolve target" is the "destination" in post_process + // We will indirectly write the results to the "destination" using + // the MSAA resolve step. + color_attachments: &[Some(target.get_color_attachment(Operations { + load: LoadOp::Clear(Default::default()), + store: true, + }))], + depth_stencil_attachment: None, + }; + + let bind_group = + render_context + .render_device() + .create_bind_group(&BindGroupDescriptor { + label: None, + layout: &blit_pipeline.texture_bind_group, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(post_process.source), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&blit_pipeline.sampler), + }, + ], + }); + + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); + + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group(0, &bind_group, &[]); + render_pass.draw(0..3, 0..1); + } + Ok(()) + } +} + +#[derive(Component)] +pub struct MsaaWritebackBlitPipeline(CachedRenderPipelineId); + +fn queue_msaa_writeback_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + blit_pipeline: Res, + view_targets: Query<(Entity, &ViewTarget, &ExtractedCamera)>, + msaa: Res, +) { + for (entity, view_target, camera) in view_targets.iter() { + // only do writeback if writeback is enabled for the camera and this isn't the first camera in the target, + // as there is nothing to write back for the first camera. + if msaa.samples() > 1 && camera.msaa_writeback && camera.sorted_camera_index_for_target > 0 + { + let key = BlitPipelineKey { + texture_format: view_target.main_texture_format(), + samples: msaa.samples(), + blend_state: None, + }; + + let pipeline = pipelines.specialize(&pipeline_cache, &blit_pipeline, key); + commands + .entity(entity) + .insert(MsaaWritebackBlitPipeline(pipeline)); + } else { + // This isn't strictly necessary now, but if we move to retained render entity state I don't + // want this to silently break + commands + .entity(entity) + .remove::(); + } + } +} diff --git a/crates/bevy_core_pipeline/src/upscaling/mod.rs b/crates/bevy_core_pipeline/src/upscaling/mod.rs index c91b5d4664052f..441f9f77758c56 100644 --- a/crates/bevy_core_pipeline/src/upscaling/mod.rs +++ b/crates/bevy_core_pipeline/src/upscaling/mod.rs @@ -1,9 +1,7 @@ -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; +use crate::blit::{BlitPipeline, BlitPipelineKey}; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, HandleUntyped}; use bevy_ecs::prelude::*; -use bevy_reflect::TypeUuid; -use bevy_render::renderer::RenderDevice; +use bevy_render::camera::{CameraOutputMode, ExtractedCamera}; use bevy_render::view::ViewTarget; use bevy_render::{render_resource::*, RenderApp, RenderSet}; @@ -11,99 +9,12 @@ mod node; pub use node::UpscalingNode; -const UPSCALING_SHADER_HANDLE: HandleUntyped = - HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 14589267395627146578); - pub struct UpscalingPlugin; impl Plugin for UpscalingPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - UPSCALING_SHADER_HANDLE, - "upscaling.wgsl", - Shader::from_wgsl - ); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { - render_app - .init_resource::() - .init_resource::>() - .add_system(queue_view_upscaling_pipelines.in_set(RenderSet::Queue)); - } - } -} - -#[derive(Resource)] -pub struct UpscalingPipeline { - texture_bind_group: BindGroupLayout, -} - -impl FromWorld for UpscalingPipeline { - fn from_world(render_world: &mut World) -> Self { - let render_device = render_world.resource::(); - - let texture_bind_group = - render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - label: Some("upscaling_texture_bind_group_layout"), - entries: &[ - BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Texture { - sample_type: TextureSampleType::Float { filterable: false }, - view_dimension: TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - BindGroupLayoutEntry { - binding: 1, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::NonFiltering), - count: None, - }, - ], - }); - - UpscalingPipeline { texture_bind_group } - } -} - -#[derive(PartialEq, Eq, Hash, Clone, Copy)] -pub enum UpscalingMode { - Filtering, - Nearest, -} - -#[derive(PartialEq, Eq, Hash, Clone, Copy)] -pub struct UpscalingPipelineKey { - upscaling_mode: UpscalingMode, - texture_format: TextureFormat, -} - -impl SpecializedRenderPipeline for UpscalingPipeline { - type Key = UpscalingPipelineKey; - - fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { - RenderPipelineDescriptor { - label: Some("upscaling pipeline".into()), - layout: vec![self.texture_bind_group.clone()], - vertex: fullscreen_shader_vertex_state(), - fragment: Some(FragmentState { - shader: UPSCALING_SHADER_HANDLE.typed(), - shader_defs: vec![], - entry_point: "fs_main".into(), - targets: vec![Some(ColorTargetState { - format: key.texture_format, - blend: None, - write_mask: ColorWrites::ALL, - })], - }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: Vec::new(), + render_app.add_system(queue_view_upscaling_pipelines.in_set(RenderSet::Queue)); } } } @@ -114,16 +25,26 @@ pub struct ViewUpscalingPipeline(CachedRenderPipelineId); fn queue_view_upscaling_pipelines( mut commands: Commands, pipeline_cache: Res, - mut pipelines: ResMut>, - upscaling_pipeline: Res, - view_targets: Query<(Entity, &ViewTarget)>, + mut pipelines: ResMut>, + blit_pipeline: Res, + view_targets: Query<(Entity, &ViewTarget, Option<&ExtractedCamera>)>, ) { - for (entity, view_target) in view_targets.iter() { - let key = UpscalingPipelineKey { - upscaling_mode: UpscalingMode::Filtering, + for (entity, view_target, camera) in view_targets.iter() { + let blend_state = if let Some(ExtractedCamera { + output_mode: CameraOutputMode::Write { blend_state, .. }, + .. + }) = camera + { + *blend_state + } else { + None + }; + let key = BlitPipelineKey { texture_format: view_target.out_texture_format(), + blend_state, + samples: 1, }; - let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key); + let pipeline = pipelines.specialize(&pipeline_cache, &blit_pipeline, key); commands .entity(entity) diff --git a/crates/bevy_core_pipeline/src/upscaling/node.rs b/crates/bevy_core_pipeline/src/upscaling/node.rs index 44cf195f724ca6..8e66f1eb07f065 100644 --- a/crates/bevy_core_pipeline/src/upscaling/node.rs +++ b/crates/bevy_core_pipeline/src/upscaling/node.rs @@ -1,8 +1,8 @@ -use std::sync::Mutex; - +use crate::{blit::BlitPipeline, upscaling::ViewUpscalingPipeline}; use bevy_ecs::prelude::*; use bevy_ecs::query::QueryState; use bevy_render::{ + camera::{CameraOutputMode, ExtractedCamera}, render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, render_resource::{ BindGroup, BindGroupDescriptor, BindGroupEntry, BindingResource, LoadOp, Operations, @@ -12,11 +12,17 @@ use bevy_render::{ renderer::RenderContext, view::{ExtractedView, ViewTarget}, }; - -use super::{UpscalingPipeline, ViewUpscalingPipeline}; +use std::sync::Mutex; pub struct UpscalingNode { - query: QueryState<(&'static ViewTarget, &'static ViewUpscalingPipeline), With>, + query: QueryState< + ( + &'static ViewTarget, + &'static ViewUpscalingPipeline, + Option<&'static ExtractedCamera>, + ), + With, + >, cached_texture_bind_group: Mutex>, } @@ -49,13 +55,25 @@ impl Node for UpscalingNode { let view_entity = graph.get_input_entity(Self::IN_VIEW)?; let pipeline_cache = world.get_resource::().unwrap(); - let upscaling_pipeline = world.get_resource::().unwrap(); + let blit_pipeline = world.get_resource::().unwrap(); - let (target, upscaling_target) = match self.query.get_manual(world, view_entity) { + let (target, upscaling_target, camera) = match self.query.get_manual(world, view_entity) { Ok(query) => query, Err(_) => return Ok(()), }; + let color_attachment_load_op = if let Some(camera) = camera { + match camera.output_mode { + CameraOutputMode::Write { + color_attachment_load_op, + .. + } => color_attachment_load_op, + CameraOutputMode::Skip => return Ok(()), + } + } else { + LoadOp::Clear(Default::default()) + }; + let upscaled_texture = target.main_texture(); let mut cached_bind_group = self.cached_texture_bind_group.lock().unwrap(); @@ -71,7 +89,7 @@ impl Node for UpscalingNode { .render_device() .create_bind_group(&BindGroupDescriptor { label: None, - layout: &upscaling_pipeline.texture_bind_group, + layout: &blit_pipeline.texture_bind_group, entries: &[ BindGroupEntry { binding: 0, @@ -100,7 +118,7 @@ impl Node for UpscalingNode { view: target.out_texture(), resolve_target: None, ops: Operations { - load: LoadOp::Clear(Default::default()), + load: color_attachment_load_op, store: true, }, })], diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 58fb2df3c878a3..27687fc863a7f6 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -15,19 +15,20 @@ use bevy_ecs::{ event::EventReader, prelude::With, reflect::ReflectComponent, - system::{Commands, Query, Res}, + system::{Commands, Query, Res, ResMut, Resource}, }; +use bevy_log::warn; use bevy_math::{Mat4, Ray, UVec2, UVec4, Vec2, Vec3}; use bevy_reflect::prelude::*; use bevy_reflect::FromReflect; use bevy_transform::components::GlobalTransform; -use bevy_utils::HashSet; +use bevy_utils::{HashMap, HashSet}; use bevy_window::{ NormalizedWindowRef, PrimaryWindow, Window, WindowCreated, WindowRef, WindowResized, }; use std::{borrow::Cow, ops::Range}; -use wgpu::{Extent3d, TextureFormat}; +use wgpu::{BlendState, Extent3d, LoadOp, TextureFormat}; /// Render viewport configuration for the [`Camera`] component. /// @@ -107,6 +108,15 @@ pub struct Camera { /// See for details. // TODO: resolve the issues mentioned in the doc comment above, then remove the warning. pub hdr: bool, + // todo: reflect this when #6042 lands + /// The [`CameraOutputMode`] for this camera. + #[reflect(ignore)] + pub output_mode: CameraOutputMode, + /// If this is enabled, a previous camera exists that shares this camera's render target, and this camera has MSAA enabled, then the previous camera's + /// outputs will be written to the intermediate multi-sampled render target textures for this camera. This enables cameras with MSAA enabled to + /// "write their results on top" of previous camera results, and include them as a part of their render results. This is enabled by default to ensure + /// cameras with MSAA enabled layer their results in the same way as cameras without MSAA enabled by default. + pub msaa_writeback: bool, } impl Default for Camera { @@ -117,7 +127,9 @@ impl Default for Camera { viewport: None, computed: Default::default(), target: Default::default(), + output_mode: Default::default(), hdr: false, + msaa_writeback: true, } } } @@ -308,6 +320,35 @@ impl Camera { } } +/// Control how this camera outputs once rendering is completed. +#[derive(Debug, Clone, Copy)] +pub enum CameraOutputMode { + /// Writes the camera output to configured render target. + Write { + /// The blend state that will be used by the pipeline that writes the intermediate render textures to the final render target texture. + blend_state: Option, + /// The color attachment load operation that will be used by the pipeline that writes the intermediate render textures to the final render + /// target texture. + color_attachment_load_op: wgpu::LoadOp, + }, + /// Skips writing the camera output to the configured render target. The output will remain in the + /// Render Target's "intermediate" textures, which a camera with a higher order should write to the render target + /// using [`CameraOutputMode::Write`]. The "skip" mode can easily prevent render results from being displayed, or cause + /// them to be lost. Only use this if you know what you are doing! + /// In camera setups with multiple active cameras rendering to the same RenderTarget, the Skip mode can be used to remove + /// unnecessary / redundant writes to the final output texture, removing unnecessary render passes. + Skip, +} + +impl Default for CameraOutputMode { + fn default() -> Self { + CameraOutputMode::Write { + blend_state: None, + color_attachment_load_op: LoadOp::Clear(Default::default()), + } + } +} + /// Configures the [`RenderGraph`](crate::render_graph::RenderGraph) name assigned to be run for a given [`Camera`] entity. #[derive(Component, Deref, DerefMut, Reflect, Default)] #[reflect(Component)] @@ -519,6 +560,9 @@ pub struct ExtractedCamera { pub viewport: Option, pub render_graph: Cow<'static, str>, pub order: isize, + pub output_mode: CameraOutputMode, + pub msaa_writeback: bool, + pub sorted_camera_index_for_target: usize, } pub fn extract_cameras( @@ -560,6 +604,10 @@ pub fn extract_cameras( physical_target_size: Some(target_size), render_graph: camera_render_graph.0.clone(), order: camera.order, + output_mode: camera.output_mode, + msaa_writeback: camera.msaa_writeback, + // this will be set in sort_cameras + sorted_camera_index_for_target: 0, }, ExtractedView { projection: camera.projection_matrix(), @@ -579,3 +627,63 @@ pub fn extract_cameras( } } } + +/// Cameras sorted by their order field. This is updated in the [`sort_cameras`] system. +#[derive(Resource, Default)] +pub struct SortedCameras(pub Vec); + +pub struct SortedCamera { + pub entity: Entity, + pub order: isize, + pub target: Option, +} + +pub fn sort_cameras( + mut sorted_cameras: ResMut, + mut cameras: Query<(Entity, &mut ExtractedCamera)>, +) { + sorted_cameras.0.clear(); + for (entity, camera) in cameras.iter() { + sorted_cameras.0.push(SortedCamera { + entity, + order: camera.order, + target: camera.target.clone(), + }); + } + // sort by order and ensure within an order, RenderTargets of the same type are packed together + sorted_cameras + .0 + .sort_by(|c1, c2| match c1.order.cmp(&c2.order) { + std::cmp::Ordering::Equal => c1.target.cmp(&c2.target), + ord => ord, + }); + let mut previous_order_target = None; + let mut ambiguities = HashSet::new(); + let mut target_counts = HashMap::new(); + for sorted_camera in &mut sorted_cameras.0 { + let new_order_target = (sorted_camera.order, sorted_camera.target.clone()); + if let Some(previous_order_target) = previous_order_target { + if previous_order_target == new_order_target { + ambiguities.insert(new_order_target.clone()); + } + } + if let Some(target) = &sorted_camera.target { + let count = target_counts.entry(target.clone()).or_insert(0usize); + let (_, mut camera) = cameras.get_mut(sorted_camera.entity).unwrap(); + camera.sorted_camera_index_for_target = *count; + *count += 1; + } + previous_order_target = Some(new_order_target); + } + + if !ambiguities.is_empty() { + warn!( + "Camera order ambiguities detected for active cameras with the following priorities: {:?}. \ + To fix this, ensure there is exactly one Camera entity spawned with a given order for a given RenderTarget. \ + Ambiguities should be resolved because either (1) multiple active cameras were spawned accidentally, which will \ + result in rendering multiple instances of the scene or (2) for cases where multiple active cameras is intentional, \ + ambiguities could result in unpredictable render results.", + ambiguities + ); + } +} diff --git a/crates/bevy_render/src/camera/camera_driver_node.rs b/crates/bevy_render/src/camera/camera_driver_node.rs index 539383cd56e4a7..dcee0cba450457 100644 --- a/crates/bevy_render/src/camera/camera_driver_node.rs +++ b/crates/bevy_render/src/camera/camera_driver_node.rs @@ -1,15 +1,15 @@ use crate::{ - camera::{ExtractedCamera, NormalizedRenderTarget}, + camera::{ExtractedCamera, NormalizedRenderTarget, SortedCameras}, render_graph::{Node, NodeRunError, RenderGraphContext, SlotValue}, renderer::RenderContext, view::ExtractedWindows, }; -use bevy_ecs::{entity::Entity, prelude::QueryState, world::World}; -use bevy_utils::{tracing::warn, HashSet}; +use bevy_ecs::{prelude::QueryState, world::World}; +use bevy_utils::HashSet; use wgpu::{LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor}; pub struct CameraDriverNode { - cameras: QueryState<(Entity, &'static ExtractedCamera)>, + cameras: QueryState<&'static ExtractedCamera>, } impl CameraDriverNode { @@ -30,47 +30,20 @@ impl Node for CameraDriverNode { render_context: &mut RenderContext, world: &World, ) -> Result<(), NodeRunError> { - let mut sorted_cameras = self - .cameras - .iter_manual(world) - .map(|(e, c)| (e, c.order, c.target.clone())) - .collect::>(); - // sort by order and ensure within an order, RenderTargets of the same type are packed together - sorted_cameras.sort_by(|(_, p1, t1), (_, p2, t2)| match p1.cmp(p2) { - std::cmp::Ordering::Equal => t1.cmp(t2), - ord => ord, - }); + let sorted_cameras = world.resource::(); let mut camera_windows = HashSet::new(); - let mut previous_order_target = None; - let mut ambiguities = HashSet::new(); - for (entity, order, target) in sorted_cameras { - let new_order_target = (order, target); - if let Some(previous_order_target) = previous_order_target { - if previous_order_target == new_order_target { - ambiguities.insert(new_order_target.clone()); - } - } - previous_order_target = Some(new_order_target); - if let Ok((_, camera)) = self.cameras.get_manual(world, entity) { + for sorted_camera in &sorted_cameras.0 { + if let Ok(camera) = self.cameras.get_manual(world, sorted_camera.entity) { if let Some(NormalizedRenderTarget::Window(window_ref)) = camera.target { camera_windows.insert(window_ref.entity()); } - graph - .run_sub_graph(camera.render_graph.clone(), vec![SlotValue::Entity(entity)])?; + graph.run_sub_graph( + camera.render_graph.clone(), + vec![SlotValue::Entity(sorted_camera.entity)], + )?; } } - if !ambiguities.is_empty() { - warn!( - "Camera order ambiguities detected for active cameras with the following priorities: {:?}. \ - To fix this, ensure there is exactly one Camera entity spawned with a given order for a given RenderTarget. \ - Ambiguities should be resolved because either (1) multiple active cameras were spawned accidentally, which will \ - result in rendering multiple instances of the scene or (2) for cases where multiple active cameras is intentional, \ - ambiguities could result in unpredictable render results.", - ambiguities - ); - } - // wgpu (and some backends) require doing work for swap chains if you call `get_current_texture()` and `present()` // This ensures that Bevy doesn't crash, even when there are no cameras (and therefore no work submitted). for (id, window) in world.resource::().iter() { diff --git a/crates/bevy_render/src/camera/mod.rs b/crates/bevy_render/src/camera/mod.rs index e1639a348f9917..91ea69d9417c76 100644 --- a/crates/bevy_render/src/camera/mod.rs +++ b/crates/bevy_render/src/camera/mod.rs @@ -7,8 +7,9 @@ pub use camera::*; pub use camera_driver_node::*; pub use projection::*; -use crate::{render_graph::RenderGraph, ExtractSchedule, RenderApp}; +use crate::{render_graph::RenderGraph, ExtractSchedule, RenderApp, RenderSet}; use bevy_app::{App, IntoSystemAppConfig, Plugin}; +use bevy_ecs::schedule::IntoSystemConfig; #[derive(Default)] pub struct CameraPlugin; @@ -26,7 +27,10 @@ impl Plugin for CameraPlugin { .add_plugin(CameraProjectionPlugin::::default()); if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.add_system(extract_cameras.in_schedule(ExtractSchedule)); + render_app + .init_resource::() + .add_system(extract_cameras.in_schedule(ExtractSchedule)) + .add_system(sort_cameras.in_set(RenderSet::Prepare)); let camera_driver_node = CameraDriverNode::new(&mut render_app.world); let mut render_graph = render_app.world.resource_mut::(); render_graph.add_node(crate::main_graph::node::CAMERA_DRIVER, camera_driver_node); diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 7bd7b07ce2b3df..008c8aae3a8549 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -22,7 +22,10 @@ use bevy_math::{Mat4, UVec4, Vec3, Vec4}; use bevy_reflect::{Reflect, TypeUuid}; use bevy_transform::components::GlobalTransform; use bevy_utils::HashMap; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; use wgpu::{ Color, Extent3d, Operations, RenderPassColorAttachment, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, @@ -180,7 +183,8 @@ pub struct ViewTarget { main_textures: MainTargetTextures, main_texture_format: TextureFormat, /// 0 represents `main_textures.a`, 1 represents `main_textures.b` - main_texture: AtomicUsize, + /// This is shared across view targets with the same render target + main_texture: Arc, out_texture: TextureView, out_texture_format: TextureFormat, } @@ -341,6 +345,9 @@ struct MainTargetTextures { a: TextureView, b: TextureView, sampled: Option, + /// 0 represents `main_textures.a`, 1 represents `main_textures.b` + /// This is shared across view targets with the same render target + main_texture: Arc, } #[allow(clippy::too_many_arguments)] @@ -423,13 +430,14 @@ fn prepare_view_targets( ) .default_view }), + main_texture: Arc::new(AtomicUsize::new(0)), } }); commands.entity(entity).insert(ViewTarget { main_textures: main_textures.clone(), main_texture_format, - main_texture: AtomicUsize::new(0), + main_texture: main_textures.main_texture.clone(), out_texture: out_texture_view.clone(), out_texture_format, });