From 5e80886ca8b2f251cb5eaa451ee417e52c8e6de7 Mon Sep 17 00:00:00 2001 From: Charlotte McElwain Date: Fri, 17 May 2024 18:26:59 -0700 Subject: [PATCH 1/6] Allow mix of hdr and non-hdr cameras to same render target Changes: - Track whether an output texture has been written to yet and only clear it on the first write. - Use `ClearColorConfig` on `CameraOutputMode` instead of a raw `LoadOp`. - Track whether a output texture has been seen when specializing the upscaling pipeline and use alpha blending for extra cameras rendering to that texture that do not specify an explicit blend mode. Fixes #6754 --- .../bevy_core_pipeline/src/upscaling/mod.rs | 19 ++++++++- .../bevy_core_pipeline/src/upscaling/node.rs | 32 +++++++-------- crates/bevy_render/src/camera/camera.rs | 11 +++-- crates/bevy_render/src/camera/clear_color.rs | 2 +- .../src/texture/texture_attachment.rs | 40 ++++++++++++++++++- crates/bevy_render/src/view/mod.rs | 37 ++++++++++++----- 6 files changed, 105 insertions(+), 36 deletions(-) diff --git a/crates/bevy_core_pipeline/src/upscaling/mod.rs b/crates/bevy_core_pipeline/src/upscaling/mod.rs index 6220843883ac8..9a25336356654 100644 --- a/crates/bevy_core_pipeline/src/upscaling/mod.rs +++ b/crates/bevy_core_pipeline/src/upscaling/mod.rs @@ -4,6 +4,7 @@ use bevy_ecs::prelude::*; use bevy_render::camera::{CameraOutputMode, ExtractedCamera}; use bevy_render::view::ViewTarget; use bevy_render::{render_resource::*, Render, RenderApp, RenderSet}; +use bevy_utils::HashSet; mod node; @@ -32,16 +33,32 @@ fn prepare_view_upscaling_pipelines( blit_pipeline: Res, view_targets: Query<(Entity, &ViewTarget, Option<&ExtractedCamera>)>, ) { + let mut output_textures = HashSet::new(); for (entity, view_target, camera) in view_targets.iter() { + let out_texture_id = view_target.out_texture().id(); let blend_state = if let Some(ExtractedCamera { output_mode: CameraOutputMode::Write { blend_state, .. }, .. }) = camera { - *blend_state + match *blend_state { + None => { + // If we've already seen this output for a camera and it doesn't have a output blend + // mode configured, default to alpha blend so that we don't accidentally overwrite + // the output texture + if output_textures.contains(&out_texture_id) { + Some(BlendState::ALPHA_BLENDING) + } else { + None + } + } + _ => *blend_state, + } } else { None }; + output_textures.insert(out_texture_id); + let key = BlitPipelineKey { texture_format: view_target.out_texture_format(), blend_state, diff --git a/crates/bevy_core_pipeline/src/upscaling/node.rs b/crates/bevy_core_pipeline/src/upscaling/node.rs index 59232f51c05f4..c0a703b208a8e 100644 --- a/crates/bevy_core_pipeline/src/upscaling/node.rs +++ b/crates/bevy_core_pipeline/src/upscaling/node.rs @@ -1,11 +1,11 @@ use crate::{blit::BlitPipeline, upscaling::ViewUpscalingPipeline}; use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_render::camera::{ClearColor, ClearColorConfig}; use bevy_render::{ camera::{CameraOutputMode, ExtractedCamera}, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_resource::{ - BindGroup, BindGroupEntries, LoadOp, Operations, PipelineCache, RenderPassColorAttachment, - RenderPassDescriptor, StoreOp, TextureViewId, + BindGroup, BindGroupEntries, PipelineCache, RenderPassDescriptor, TextureViewId, }, renderer::RenderContext, view::ViewTarget, @@ -33,19 +33,22 @@ impl ViewNode for UpscalingNode { ) -> Result<(), NodeRunError> { let pipeline_cache = world.get_resource::().unwrap(); let blit_pipeline = world.get_resource::().unwrap(); + let clear_color_global = world.get_resource::().unwrap(); - let color_attachment_load_op = if let Some(camera) = camera { + let clear_color = if let Some(camera) = camera { match camera.output_mode { - CameraOutputMode::Write { - color_attachment_load_op, - .. - } => color_attachment_load_op, + CameraOutputMode::Write { clear_color, .. } => clear_color, CameraOutputMode::Skip => return Ok(()), } } else { - LoadOp::Clear(Default::default()) + ClearColorConfig::Default }; - + let clear_color = match clear_color { + ClearColorConfig::Default => Some(clear_color_global.0), + ClearColorConfig::Custom(color) => Some(color), + ClearColorConfig::None => None, + }; + let converted_clear_color = clear_color.map(|color| color.into()); let upscaled_texture = target.main_texture_view(); let mut cached_bind_group = self.cached_texture_bind_group.lock().unwrap(); @@ -69,14 +72,9 @@ impl ViewNode for UpscalingNode { let pass_descriptor = RenderPassDescriptor { label: Some("upscaling_pass"), - color_attachments: &[Some(RenderPassColorAttachment { - view: target.out_texture(), - resolve_target: None, - ops: Operations { - load: color_attachment_load_op, - store: StoreOp::Store, - }, - })], + color_attachments: &[Some( + target.out_texture_color_attachment(converted_clear_color), + )], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index d0d9a4a6e1cd1..7a3defd68c879 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -35,7 +35,7 @@ use bevy_window::{ WindowScaleFactorChanged, }; use std::ops::Range; -use wgpu::{BlendState, LoadOp, TextureFormat, TextureUsages}; +use wgpu::{BlendState, TextureFormat, TextureUsages}; use super::{ClearColorConfig, Projection}; @@ -488,9 +488,8 @@ pub enum CameraOutputMode { 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: LoadOp, + /// The clear color operation to perform on the final render target texture. + clear_color: ClearColorConfig, }, /// 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 @@ -505,7 +504,7 @@ impl Default for CameraOutputMode { fn default() -> Self { CameraOutputMode::Write { blend_state: None, - color_attachment_load_op: LoadOp::Clear(Default::default()), + clear_color: ClearColorConfig::Default, } } } @@ -897,7 +896,7 @@ pub fn extract_cameras( order: camera.order, output_mode: camera.output_mode, msaa_writeback: camera.msaa_writeback, - clear_color: camera.clear_color.clone(), + clear_color: camera.clear_color, // this will be set in sort_cameras sorted_camera_index_for_target: 0, exposure: exposure diff --git a/crates/bevy_render/src/camera/clear_color.rs b/crates/bevy_render/src/camera/clear_color.rs index e986b3d30b29a..63f291aff4117 100644 --- a/crates/bevy_render/src/camera/clear_color.rs +++ b/crates/bevy_render/src/camera/clear_color.rs @@ -6,7 +6,7 @@ use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; /// For a camera, specifies the color used to clear the viewport before rendering. -#[derive(Reflect, Serialize, Deserialize, Clone, Debug, Default)] +#[derive(Reflect, Serialize, Deserialize, Copy, Clone, Debug, Default)] #[reflect(Serialize, Deserialize, Default)] pub enum ClearColorConfig { /// The clear color is taken from the world's [`ClearColor`] resource. diff --git a/crates/bevy_render/src/texture/texture_attachment.rs b/crates/bevy_render/src/texture/texture_attachment.rs index 220b9c5e0316c..1d6b3ba33c292 100644 --- a/crates/bevy_render/src/texture/texture_attachment.rs +++ b/crates/bevy_render/src/texture/texture_attachment.rs @@ -1,5 +1,5 @@ use super::CachedTexture; -use crate::render_resource::TextureView; +use crate::render_resource::{TextureFormat, TextureView}; use bevy_color::LinearRgba; use std::sync::{ atomic::{AtomicBool, Ordering}, @@ -120,3 +120,41 @@ impl DepthAttachment { } } } + +/// A wrapper for a [`TextureView`] that is used as a [`RenderPassColorAttachment`] for a view +/// targets final output texture. +#[derive(Clone)] +pub struct OutputColorAttachment { + pub view: TextureView, + pub format: TextureFormat, + is_first_call: Arc, +} + +impl OutputColorAttachment { + pub fn new(view: TextureView, format: TextureFormat) -> Self { + Self { + view, + format, + is_first_call: Arc::new(AtomicBool::new(true)), + } + } + + /// Get this texture view as an attachment. The attachment will be cleared with a value of + /// the provided `clear_color` if this is the first time calling this function, otherwise it + /// will be loaded. + pub fn get_attachment(&self, clear_color: Option) -> RenderPassColorAttachment { + let first_call = self.is_first_call.fetch_and(false, Ordering::SeqCst); + + RenderPassColorAttachment { + view: &self.view, + resolve_target: None, + ops: Operations { + load: match (clear_color, first_call) { + (Some(clear_color), true) => LoadOp::Clear(clear_color.into()), + (None, _) | (Some(_), false) => LoadOp::Load, + }, + store: StoreOp::Store, + }, + } + } +} diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 0a6dcc5e0020c..da30a2ccf07da 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -19,10 +19,12 @@ use crate::{ renderer::{RenderDevice, RenderQueue}, texture::{ BevyDefault, CachedTexture, ColorAttachment, DepthAttachment, GpuImage, TextureCache, + OutputColorAttachment }, Render, RenderApp, RenderSet, }; use bevy_app::{App, Plugin}; +use bevy_color::LinearRgba; use bevy_ecs::prelude::*; use bevy_math::{mat3, vec2, vec3, Mat3, Mat4, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -451,8 +453,7 @@ pub struct ViewTarget { /// 0 represents `main_textures.a`, 1 represents `main_textures.b` /// This is shared across view targets with the same render target main_texture: Arc, - out_texture: TextureView, - out_texture_format: TextureFormat, + out_texture: OutputColorAttachment, } pub struct PostProcessWrite<'a> { @@ -644,13 +645,20 @@ impl ViewTarget { /// The final texture this view will render to. #[inline] pub fn out_texture(&self) -> &TextureView { - &self.out_texture + &self.out_texture.view + } + + pub fn out_texture_color_attachment( + &self, + clear_color: Option, + ) -> RenderPassColorAttachment { + self.out_texture.get_attachment(clear_color) } /// The format of the final texture this view will render to #[inline] pub fn out_texture_format(&self) -> TextureFormat { - self.out_texture_format + self.out_texture.format } /// This will start a new "post process write", which assumes that the caller @@ -802,12 +810,22 @@ pub fn prepare_view_targets( manual_texture_views: Res, ) { let mut textures = HashMap::default(); + let mut output_textures = HashMap::default(); for (entity, camera, view, texture_usage) in cameras.iter() { if let (Some(target_size), Some(target)) = (camera.physical_target_size, &camera.target) { - if let (Some(out_texture_view), Some(out_texture_format)) = ( - target.get_texture_view(&windows, &images, &manual_texture_views), - target.get_texture_format(&windows, &images, &manual_texture_views), - ) { + if let Some(out_texture) = output_textures.entry(target.clone()).or_insert_with(|| { + if let (Some(out_texture_view), Some(out_texture_format)) = ( + target.get_texture_view(&windows, &images, &manual_texture_views), + target.get_texture_format(&windows, &images, &manual_texture_views), + ) { + Some(OutputColorAttachment::new( + out_texture_view.clone(), + out_texture_format.add_srgb_suffix(), + )) + } else { + None + } + }) { let size = Extent3d { width: target_size.x, height: target_size.y, @@ -891,8 +909,7 @@ pub fn prepare_view_targets( main_texture: main_textures.main_texture.clone(), main_textures, main_texture_format, - out_texture: out_texture_view.clone(), - out_texture_format: out_texture_format.add_srgb_suffix(), + out_texture: out_texture.clone(), }); } } From 8bdaffcee3f4e174d5bc8e5a16868a5758ff790f Mon Sep 17 00:00:00 2001 From: Charlotte McElwain Date: Fri, 17 May 2024 18:43:11 -0700 Subject: [PATCH 2/6] fmt. --- crates/bevy_render/src/view/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index da30a2ccf07da..20e669ec1be19 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -18,8 +18,8 @@ use crate::{ render_resource::{DynamicUniformBuffer, ShaderType, Texture, TextureView}, renderer::{RenderDevice, RenderQueue}, texture::{ - BevyDefault, CachedTexture, ColorAttachment, DepthAttachment, GpuImage, TextureCache, - OutputColorAttachment + BevyDefault, CachedTexture, ColorAttachment, DepthAttachment, GpuImage, + OutputColorAttachment, TextureCache, }, Render, RenderApp, RenderSet, }; From 19b3d4c97298ddb36468316d69c12545290e5f95 Mon Sep 17 00:00:00 2001 From: Charlotte McElwain Date: Tue, 21 May 2024 12:38:55 -0700 Subject: [PATCH 3/6] Track hdr on ExtractedCamera in order to properly determine when to trigger MSAA writeback. --- crates/bevy_render/src/camera/camera.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 7a3defd68c879..51f459273bad5 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -823,6 +823,7 @@ pub struct ExtractedCamera { pub clear_color: ClearColorConfig, pub sorted_camera_index_for_target: usize, pub exposure: f32, + pub hdr: bool, } pub fn extract_cameras( @@ -902,6 +903,7 @@ pub fn extract_cameras( exposure: exposure .map(|e| e.exposure()) .unwrap_or_else(|| Exposure::default().exposure()), + hdr: camera.hdr, }, ExtractedView { projection: camera.projection_matrix(), @@ -953,6 +955,7 @@ pub struct SortedCamera { pub entity: Entity, pub order: isize, pub target: Option, + pub hdr: bool, } pub fn sort_cameras( @@ -965,6 +968,7 @@ pub fn sort_cameras( entity, order: camera.order, target: camera.target.clone(), + hdr: camera.hdr, }); } // sort by order and ensure within an order, RenderTargets of the same type are packed together @@ -985,7 +989,7 @@ pub fn sort_cameras( } } if let Some(target) = &sorted_camera.target { - let count = target_counts.entry(target.clone()).or_insert(0usize); + let count = target_counts.entry((target.clone(), sorted_camera.hdr)).or_insert(0usize); let (_, mut camera) = cameras.get_mut(sorted_camera.entity).unwrap(); camera.sorted_camera_index_for_target = *count; *count += 1; From 18640c76d49598d536db80a23accd72af693d5fe Mon Sep 17 00:00:00 2001 From: Charlotte McElwain Date: Tue, 21 May 2024 12:46:31 -0700 Subject: [PATCH 4/6] fmt. --- crates/bevy_render/src/camera/camera.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 51f459273bad5..95c8ca82bdd5b 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -989,7 +989,9 @@ pub fn sort_cameras( } } if let Some(target) = &sorted_camera.target { - let count = target_counts.entry((target.clone(), sorted_camera.hdr)).or_insert(0usize); + let count = target_counts + .entry((target.clone(), sorted_camera.hdr)) + .or_insert(0usize); let (_, mut camera) = cameras.get_mut(sorted_camera.entity).unwrap(); camera.sorted_camera_index_for_target = *count; *count += 1; From 543ca5d471f4ba6df9cdbb335b84705df22766e5 Mon Sep 17 00:00:00 2001 From: Charlotte McElwain Date: Sun, 2 Jun 2024 09:49:43 -0700 Subject: [PATCH 5/6] Fix doc. --- crates/bevy_render/src/texture/texture_attachment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_render/src/texture/texture_attachment.rs b/crates/bevy_render/src/texture/texture_attachment.rs index 1d6b3ba33c292..6681ba9916211 100644 --- a/crates/bevy_render/src/texture/texture_attachment.rs +++ b/crates/bevy_render/src/texture/texture_attachment.rs @@ -122,7 +122,7 @@ impl DepthAttachment { } /// A wrapper for a [`TextureView`] that is used as a [`RenderPassColorAttachment`] for a view -/// targets final output texture. +/// target's final output texture. #[derive(Clone)] pub struct OutputColorAttachment { pub view: TextureView, From 82a3e4a2572471fd7966fb67d98ca51c5c6e3ce7 Mon Sep 17 00:00:00 2001 From: Charlotte McElwain Date: Sun, 2 Jun 2024 09:58:59 -0700 Subject: [PATCH 6/6] Tersify view creation. --- crates/bevy_render/src/view/mod.rs | 184 ++++++++++++++--------------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 20e669ec1be19..966bd889425aa 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -812,106 +812,106 @@ pub fn prepare_view_targets( let mut textures = HashMap::default(); let mut output_textures = HashMap::default(); for (entity, camera, view, texture_usage) in cameras.iter() { - if let (Some(target_size), Some(target)) = (camera.physical_target_size, &camera.target) { - if let Some(out_texture) = output_textures.entry(target.clone()).or_insert_with(|| { - if let (Some(out_texture_view), Some(out_texture_format)) = ( - target.get_texture_view(&windows, &images, &manual_texture_views), - target.get_texture_format(&windows, &images, &manual_texture_views), - ) { - Some(OutputColorAttachment::new( - out_texture_view.clone(), - out_texture_format.add_srgb_suffix(), - )) - } else { - None - } - }) { - let size = Extent3d { - width: target_size.x, - height: target_size.y, - depth_or_array_layers: 1, - }; + let (Some(target_size), Some(target)) = (camera.physical_target_size, &camera.target) + else { + continue; + }; - let main_texture_format = if view.hdr { - ViewTarget::TEXTURE_FORMAT_HDR - } else { - TextureFormat::bevy_default() - }; + let Some(out_texture) = output_textures.entry(target.clone()).or_insert_with(|| { + target + .get_texture_view(&windows, &images, &manual_texture_views) + .zip(target.get_texture_format(&windows, &images, &manual_texture_views)) + .map(|(view, format)| { + OutputColorAttachment::new(view.clone(), format.add_srgb_suffix()) + }) + }) else { + continue; + }; - let clear_color = match camera.clear_color { - ClearColorConfig::Custom(color) => Some(color), - ClearColorConfig::None => None, - _ => Some(clear_color_global.0), - }; + let size = Extent3d { + width: target_size.x, + height: target_size.y, + depth_or_array_layers: 1, + }; + + let main_texture_format = if view.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }; + + let clear_color = match camera.clear_color { + ClearColorConfig::Custom(color) => Some(color), + ClearColorConfig::None => None, + _ => Some(clear_color_global.0), + }; - let (a, b, sampled, main_texture) = textures - .entry((camera.target.clone(), view.hdr)) - .or_insert_with(|| { - let descriptor = TextureDescriptor { - label: None, + let (a, b, sampled, main_texture) = textures + .entry((camera.target.clone(), view.hdr)) + .or_insert_with(|| { + let descriptor = TextureDescriptor { + label: None, + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: main_texture_format, + usage: texture_usage.0, + view_formats: match main_texture_format { + TextureFormat::Bgra8Unorm => &[TextureFormat::Bgra8UnormSrgb], + TextureFormat::Rgba8Unorm => &[TextureFormat::Rgba8UnormSrgb], + _ => &[], + }, + }; + let a = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("main_texture_a"), + ..descriptor + }, + ); + let b = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("main_texture_b"), + ..descriptor + }, + ); + let sampled = if msaa.samples() > 1 { + let sampled = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("main_texture_sampled"), size, mip_level_count: 1, - sample_count: 1, + sample_count: msaa.samples(), dimension: TextureDimension::D2, format: main_texture_format, - usage: texture_usage.0, - view_formats: match main_texture_format { - TextureFormat::Bgra8Unorm => &[TextureFormat::Bgra8UnormSrgb], - TextureFormat::Rgba8Unorm => &[TextureFormat::Rgba8UnormSrgb], - _ => &[], - }, - }; - let a = texture_cache.get( - &render_device, - TextureDescriptor { - label: Some("main_texture_a"), - ..descriptor - }, - ); - let b = texture_cache.get( - &render_device, - TextureDescriptor { - label: Some("main_texture_b"), - ..descriptor - }, - ); - let sampled = if msaa.samples() > 1 { - let sampled = texture_cache.get( - &render_device, - TextureDescriptor { - label: Some("main_texture_sampled"), - size, - mip_level_count: 1, - sample_count: msaa.samples(), - dimension: TextureDimension::D2, - format: main_texture_format, - usage: TextureUsages::RENDER_ATTACHMENT, - view_formats: descriptor.view_formats, - }, - ); - Some(sampled) - } else { - None - }; - let main_texture = Arc::new(AtomicUsize::new(0)); - (a, b, sampled, main_texture) - }); - - let converted_clear_color = clear_color.map(|color| color.into()); - - let main_textures = MainTargetTextures { - a: ColorAttachment::new(a.clone(), sampled.clone(), converted_clear_color), - b: ColorAttachment::new(b.clone(), sampled.clone(), converted_clear_color), - main_texture: main_texture.clone(), + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: descriptor.view_formats, + }, + ); + Some(sampled) + } else { + None }; + let main_texture = Arc::new(AtomicUsize::new(0)); + (a, b, sampled, main_texture) + }); - commands.entity(entity).insert(ViewTarget { - main_texture: main_textures.main_texture.clone(), - main_textures, - main_texture_format, - out_texture: out_texture.clone(), - }); - } - } + let converted_clear_color = clear_color.map(|color| color.into()); + + let main_textures = MainTargetTextures { + a: ColorAttachment::new(a.clone(), sampled.clone(), converted_clear_color), + b: ColorAttachment::new(b.clone(), sampled.clone(), converted_clear_color), + main_texture: main_texture.clone(), + }; + + commands.entity(entity).insert(ViewTarget { + main_texture: main_textures.main_texture.clone(), + main_textures, + main_texture_format, + out_texture: out_texture.clone(), + }); } }