From 5a297d7903d9ddb20051bd346a72aa77a7dc78b2 Mon Sep 17 00:00:00 2001 From: Dusty DeWeese Date: Tue, 12 Apr 2022 19:27:30 +0000 Subject: [PATCH] Reuse texture when resolving multiple passes (#3552) # Objective Fixes https://github.com/bevyengine/bevy/issues/3499 ## Solution Uses a `HashMap` from `RenderTarget` to sampled textures when preparing `ViewTarget`s to ensure that two passes with the same render target get sampled to the same texture. This builds on and depends on https://github.com/bevyengine/bevy/pull/3412, so this will be a draft PR until #3412 is merged. All changes for this PR are in the last commit. --- Cargo.toml | 4 + crates/bevy_core_pipeline/src/lib.rs | 49 ++-- crates/bevy_core_pipeline/src/main_pass_3d.rs | 7 +- .../bevy_render/src/texture/texture_cache.rs | 1 + crates/bevy_render/src/view/mod.rs | 38 +-- examples/3d/two_passes.rs | 217 ++++++++++++++++++ examples/README.md | 1 + 7 files changed, 280 insertions(+), 37 deletions(-) create mode 100644 examples/3d/two_passes.rs diff --git a/Cargo.toml b/Cargo.toml index adddf0273f..8ed279b567 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -216,6 +216,10 @@ path = "examples/3d/texture.rs" name = "render_to_texture" path = "examples/3d/render_to_texture.rs" +[[example]] +name = "two_passes" +path = "examples/3d/two_passes.rs" + [[example]] name = "update_gltf_scene" path = "examples/3d/update_gltf_scene.rs" diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 8fa6be9f29..e9d0c9b8b9 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -23,7 +23,7 @@ use bevy_app::{App, Plugin}; use bevy_core::FloatOrd; use bevy_ecs::prelude::*; use bevy_render::{ - camera::{ActiveCamera, Camera2d, Camera3d, RenderTarget}, + camera::{ActiveCamera, Camera2d, Camera3d, ExtractedCamera, RenderTarget}, color::Color, render_graph::{EmptyNode, RenderGraph, SlotInfo, SlotType}, render_phase::{ @@ -390,7 +390,7 @@ pub fn prepare_core_views_system( msaa: Res, render_device: Res, views_3d: Query< - (Entity, &ExtractedView), + (Entity, &ExtractedView, Option<&ExtractedCamera>), ( With>, With>, @@ -398,24 +398,35 @@ pub fn prepare_core_views_system( ), >, ) { - for (entity, view) in views_3d.iter() { - let cached_texture = texture_cache.get( - &render_device, - TextureDescriptor { - label: Some("view_depth_texture"), - size: Extent3d { - depth_or_array_layers: 1, - width: view.width as u32, - height: view.height as u32, + let mut textures = HashMap::default(); + for (entity, view, camera) in views_3d.iter() { + let mut get_cached_texture = || { + texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("view_depth_texture"), + size: Extent3d { + depth_or_array_layers: 1, + width: view.width as u32, + height: view.height as u32, + }, + mip_level_count: 1, + sample_count: msaa.samples, + dimension: TextureDimension::D2, + format: TextureFormat::Depth32Float, /* PERF: vulkan docs recommend using 24 + * bit depth for better performance */ + usage: TextureUsages::RENDER_ATTACHMENT, }, - mip_level_count: 1, - sample_count: msaa.samples, - dimension: TextureDimension::D2, - format: TextureFormat::Depth32Float, /* PERF: vulkan docs recommend using 24 - * bit depth for better performance */ - usage: TextureUsages::RENDER_ATTACHMENT, - }, - ); + ) + }; + let cached_texture = if let Some(camera) = camera { + textures + .entry(camera.target.clone()) + .or_insert_with(get_cached_texture) + .clone() + } else { + get_cached_texture() + }; commands.entity(entity).insert(ViewDepthTexture { texture: cached_texture.texture, view: cached_texture.default_view, diff --git a/crates/bevy_core_pipeline/src/main_pass_3d.rs b/crates/bevy_core_pipeline/src/main_pass_3d.rs index 242a0596be..28841e8acb 100644 --- a/crates/bevy_core_pipeline/src/main_pass_3d.rs +++ b/crates/bevy_core_pipeline/src/main_pass_3d.rs @@ -142,12 +142,15 @@ impl Node for MainPass3dNode { })], depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { view: &depth.view, - // NOTE: For the transparent pass we load the depth buffer but do not write to it. + // NOTE: For the transparent pass we load the depth buffer. There should be no + // need to write to it, but store is set to `true` as a workaround for issue #3776, + // https://github.com/bevyengine/bevy/issues/3776 + // so that wgpu does not clear the depth buffer. // As the opaque and alpha mask passes run first, opaque meshes can occlude // transparent ones. depth_ops: Some(Operations { load: LoadOp::Load, - store: false, + store: true, }), stencil_ops: None, }), diff --git a/crates/bevy_render/src/texture/texture_cache.rs b/crates/bevy_render/src/texture/texture_cache.rs index 43cac472c1..d92878774a 100644 --- a/crates/bevy_render/src/texture/texture_cache.rs +++ b/crates/bevy_render/src/texture/texture_cache.rs @@ -18,6 +18,7 @@ struct CachedTextureMeta { /// A cached GPU [`Texture`] with corresponding [`TextureView`]. /// This is useful for textures that are created repeatedly (each frame) in the rendering process /// to reduce the amount of GPU memory allocations. +#[derive(Clone)] pub struct CachedTexture { pub texture: Texture, pub default_view: TextureView, diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index ba712a3194..e6760974b7 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -21,6 +21,7 @@ use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; use bevy_math::{Mat4, Vec3}; use bevy_transform::components::GlobalTransform; +use bevy_utils::HashMap; pub struct ViewPlugin; @@ -181,26 +182,31 @@ fn prepare_view_targets( mut texture_cache: ResMut, cameras: Query<(Entity, &ExtractedCamera)>, ) { + let mut sampled_textures = HashMap::default(); for (entity, camera) in cameras.iter() { if let Some(size) = camera.physical_size { if let Some(texture_view) = camera.target.get_texture_view(&windows, &images) { let sampled_target = if msaa.samples > 1 { - let sampled_texture = texture_cache.get( - &render_device, - TextureDescriptor { - label: Some("sampled_color_attachment_texture"), - size: Extent3d { - width: size.x, - height: size.y, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: msaa.samples, - dimension: TextureDimension::D2, - format: TextureFormat::bevy_default(), - usage: TextureUsages::RENDER_ATTACHMENT, - }, - ); + let sampled_texture = sampled_textures + .entry(camera.target.clone()) + .or_insert_with(|| { + texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("sampled_color_attachment_texture"), + size: Extent3d { + width: size.x, + height: size.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: msaa.samples, + dimension: TextureDimension::D2, + format: TextureFormat::bevy_default(), + usage: TextureUsages::RENDER_ATTACHMENT, + }, + ) + }); Some(sampled_texture.default_view.clone()) } else { None diff --git a/examples/3d/two_passes.rs b/examples/3d/two_passes.rs new file mode 100644 index 0000000000..f9e9fb8eb0 --- /dev/null +++ b/examples/3d/two_passes.rs @@ -0,0 +1,217 @@ +use bevy::{ + core_pipeline::{draw_3d_graph, node, AlphaMask3d, Opaque3d, Transparent3d}, + prelude::*, + render::{ + camera::{ActiveCamera, Camera, CameraTypePlugin, RenderTarget}, + render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext, SlotValue}, + render_phase::RenderPhase, + renderer::RenderContext, + view::RenderLayers, + RenderApp, RenderStage, + }, + window::WindowId, +}; + +// The name of the final node of the first pass. +pub const FIRST_PASS_DRIVER: &str = "first_pass_driver"; + +// Marks the camera that determines the view rendered in the first pass. +#[derive(Component, Default)] +struct FirstPassCamera; + +fn main() { + let mut app = App::new(); + app.insert_resource(Msaa { samples: 4 }) + .add_plugins(DefaultPlugins) + .add_plugin(CameraTypePlugin::::default()) + .add_startup_system(setup) + .add_system(cube_rotator_system) + .add_system(rotator_system) + .add_system(toggle_msaa); + + let render_app = app.sub_app_mut(RenderApp); + let driver = FirstPassCameraDriver::new(&mut render_app.world); + + // This will add 3D render phases for the new camera. + render_app.add_system_to_stage(RenderStage::Extract, extract_first_pass_camera_phases); + + let mut graph = render_app.world.get_resource_mut::().unwrap(); + + // Add a node for the first pass. + graph.add_node(FIRST_PASS_DRIVER, driver); + + // The first pass's dependencies include those of the main pass. + graph + .add_node_edge(node::MAIN_PASS_DEPENDENCIES, FIRST_PASS_DRIVER) + .unwrap(); + + // Insert the first pass node: CLEAR_PASS_DRIVER -> FIRST_PASS_DRIVER -> MAIN_PASS_DRIVER + graph + .add_node_edge(node::CLEAR_PASS_DRIVER, FIRST_PASS_DRIVER) + .unwrap(); + graph + .add_node_edge(FIRST_PASS_DRIVER, node::MAIN_PASS_DRIVER) + .unwrap(); + app.run(); +} + +// Add 3D render phases for FirstPassCamera. +fn extract_first_pass_camera_phases( + mut commands: Commands, + active: Res>, +) { + if let Some(entity) = active.get() { + commands.get_or_spawn(entity).insert_bundle(( + RenderPhase::::default(), + RenderPhase::::default(), + RenderPhase::::default(), + )); + } +} +// A node for the first pass camera that runs draw_3d_graph with this camera. +struct FirstPassCameraDriver { + query: QueryState>, +} + +impl FirstPassCameraDriver { + pub fn new(render_world: &mut World) -> Self { + Self { + query: QueryState::new(render_world), + } + } +} + +impl Node for FirstPassCameraDriver { + fn update(&mut self, world: &mut World) { + self.query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + _render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + for camera in self.query.iter_manual(world) { + graph.run_sub_graph(draw_3d_graph::NAME, vec![SlotValue::Entity(camera)])?; + } + Ok(()) + } +} + +// Marks the first pass cube. +#[derive(Component)] +struct FirstPassCube; + +// Marks the main pass cube. +#[derive(Component)] +struct MainPassCube; + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let cube_handle = meshes.add(Mesh::from(shape::Cube { size: 4.0 })); + let cube_material_handle = materials.add(StandardMaterial { + base_color: Color::GREEN, + reflectance: 0.02, + unlit: false, + ..Default::default() + }); + + let split = 2.0; + + // This specifies the layer used for the first pass, which will be attached to the first pass camera and cube. + let first_pass_layer = RenderLayers::layer(1); + + // The first pass cube. + commands + .spawn_bundle(PbrBundle { + mesh: cube_handle, + material: cube_material_handle, + transform: Transform::from_translation(Vec3::new(-split, 0.0, 1.0)), + ..Default::default() + }) + .insert(FirstPassCube) + .insert(first_pass_layer); + + // Light + // NOTE: Currently lights are shared between passes - see https://github.com/bevyengine/bevy/issues/3462 + commands.spawn_bundle(PointLightBundle { + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)), + ..Default::default() + }); + + // First pass camera + commands + .spawn_bundle(PerspectiveCameraBundle:: { + camera: Camera { + target: RenderTarget::Window(WindowId::primary()), + ..Default::default() + }, + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 15.0)) + .looking_at(Vec3::default(), Vec3::Y), + ..PerspectiveCameraBundle::new() + }) + .insert(first_pass_layer); + + let cube_size = 4.0; + let cube_handle = meshes.add(Mesh::from(shape::Box::new(cube_size, cube_size, cube_size))); + + let material_handle = materials.add(StandardMaterial { + base_color: Color::RED, + reflectance: 0.02, + unlit: false, + ..Default::default() + }); + + // Main pass cube. + commands + .spawn_bundle(PbrBundle { + mesh: cube_handle, + material: material_handle, + transform: Transform { + translation: Vec3::new(split, 0.0, -4.5), + rotation: Quat::from_rotation_x(-std::f32::consts::PI / 5.0), + ..Default::default() + }, + ..Default::default() + }) + .insert(MainPassCube); + + // The main pass camera. + commands.spawn_bundle(PerspectiveCameraBundle { + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 15.0)) + .looking_at(Vec3::default(), Vec3::Y), + ..Default::default() + }); +} + +/// Rotates the inner cube (first pass) +fn rotator_system(time: Res