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 56aae87920..f84c372498 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 0000000000..3cb17d0864 --- /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 adc8fa6327..18b1b6b610 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 233671a159..573bf0d5a0 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 ab3f68df20..71cfaf6d40 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 5b0fe9eaea..4faa1cd83f 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 0000000000..2f8122d193 --- /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 c91b5d4664..441f9f7775 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, - texture_format: view_target.out_texture_format(), + 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 pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key); + let key = BlitPipelineKey { + texture_format: view_target.out_texture_format(), + blend_state, + samples: 1, + }; + 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 44cf195f72..8e66f1eb07 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 58fb2df3c8..27687fc863 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 539383cd56..dcee0cba45 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 e1639a348f..91ea69d941 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 7bd7b07ce2..008c8aae3a 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, });