diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 0e5a07b870e83..235bfb90f984e 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -5,7 +5,7 @@ mod main_transparent_pass_3d_node; pub mod graph { use bevy_render::render_graph::{RenderLabel, RenderSubGraph}; - #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderSubGraph)] + #[derive(Debug, Hash, PartialEq, Eq, Clone, Default, RenderSubGraph)] pub struct Core3d; pub mod input { diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 79163cb648134..ae54ec6886fbb 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -84,7 +84,6 @@ pub mod prelude { mesh_material::MeshMaterial3d, parallax::ParallaxMappingMethod, pbr_material::StandardMaterial, - ssao::ScreenSpaceAmbientOcclusionPlugin, }; } @@ -143,6 +142,7 @@ use bevy_render::{ Extent3d, TextureDataOrder, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, }, + render_task::RenderTaskPlugin, sync_component::SyncComponentPlugin, ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems, }; @@ -229,7 +229,7 @@ impl Plugin for PbrPlugin { debug_flags: self.debug_flags, ..Default::default() }, - ScreenSpaceAmbientOcclusionPlugin, + RenderTaskPlugin::::default(), FogPlugin, ExtractResourcePlugin::::default(), SyncComponentPlugin::::default(), diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index 81aa5aa4aed65..b58d57bffe10b 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -1,103 +1,38 @@ use crate::NodePbr; -use bevy_app::{App, Plugin}; -use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; -use bevy_camera::{Camera, Camera3d}; +use bevy_app::{App, SubApp}; +use bevy_asset::{embedded_asset, load_embedded_asset}; use bevy_core_pipeline::{ core_3d::graph::{Core3d, Node3d}, prepass::{DepthPrepass, NormalPrepass, ViewPrepassTextures}, }; use bevy_ecs::{ prelude::{Component, Entity}, - query::{Has, QueryItem, With}, + query::Has, reflect::ReflectComponent, resource::Resource, - schedule::IntoScheduleConfigs, - system::{Commands, Query, Res, ResMut}, world::{FromWorld, World}, }; use bevy_image::ToExtents; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ camera::{ExtractedCamera, TemporalJitter}, - diagnostic::RecordDiagnostics, extract_component::ExtractComponent, - globals::{GlobalsBuffer, GlobalsUniform}, - render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, - render_resource::{ - binding_types::{ - sampler, texture_2d, texture_depth_2d, texture_storage_2d, uniform_buffer, + globals::GlobalsBuffer, + render_graph::{IntoRenderNodeArray, RenderLabel}, + render_resource::*, + render_task::{ + bind::{ + DynamicUniformBuffer, SampledTexture, SamplerFiltering, SamplerNonFiltering, + StorageTextureWriteOnly, UniformBuffer, }, - *, + RenderTask, RenderTaskContext, }, - renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue}, - sync_component::SyncComponentPlugin, - sync_world::RenderEntity, - texture::{CachedTexture, TextureCache}, - view::{Msaa, ViewUniform, ViewUniformOffset, ViewUniforms}, - Extract, ExtractSchedule, Render, RenderApp, RenderSystems, + renderer::{RenderAdapter, RenderDevice, RenderQueue}, + view::{ViewUniformOffset, ViewUniforms}, }; -use bevy_shader::{load_shader_library, Shader, ShaderDefVal}; +use bevy_shader::{load_shader_library, ShaderDefVal}; use bevy_utils::prelude::default; use core::mem; -use tracing::{error, warn}; - -/// Plugin for screen space ambient occlusion. -pub struct ScreenSpaceAmbientOcclusionPlugin; - -impl Plugin for ScreenSpaceAmbientOcclusionPlugin { - fn build(&self, app: &mut App) { - load_shader_library!(app, "ssao_utils.wgsl"); - - embedded_asset!(app, "preprocess_depth.wgsl"); - embedded_asset!(app, "ssao.wgsl"); - embedded_asset!(app, "spatial_denoise.wgsl"); - - app.add_plugins(SyncComponentPlugin::::default()); - } - - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - - if render_app - .world() - .resource::() - .limits() - .max_storage_textures_per_shader_stage - < 5 - { - warn!("ScreenSpaceAmbientOcclusionPlugin not loaded. GPU lacks support: Limits::max_storage_textures_per_shader_stage is less than 5."); - return; - } - - render_app - .init_resource::() - .init_resource::>() - .add_systems(ExtractSchedule, extract_ssao_settings) - .add_systems( - Render, - ( - prepare_ssao_pipelines.in_set(RenderSystems::Prepare), - prepare_ssao_textures.in_set(RenderSystems::PrepareResources), - prepare_ssao_bind_groups.in_set(RenderSystems::PrepareBindGroups), - ), - ) - .add_render_graph_node::>( - Core3d, - NodePbr::ScreenSpaceAmbientOcclusion, - ) - .add_render_graph_edges( - Core3d, - ( - // END_PRE_PASSES -> SCREEN_SPACE_AMBIENT_OCCLUSION -> MAIN_PASS - Node3d::EndPrepasses, - NodePbr::ScreenSpaceAmbientOcclusion, - Node3d::StartMainPass, - ), - ); - } -} /// Component to apply screen space ambient occlusion to a 3d camera. /// @@ -110,7 +45,7 @@ impl Plugin for ScreenSpaceAmbientOcclusionPlugin { /// /// # Usage Notes /// -/// Requires that you add [`ScreenSpaceAmbientOcclusionPlugin`] to your app. +/// Requires that you add `RenderTaskPlugin` to your app. /// /// It strongly recommended that you use SSAO in conjunction with /// TAA (`TemporalAntiAliasing`). @@ -157,7 +92,7 @@ pub enum ScreenSpaceAmbientOcclusionQualityLevel { } impl ScreenSpaceAmbientOcclusionQualityLevel { - fn sample_counts(&self) -> (u32, u32) { + fn sample_counts(&self) -> (i32, i32) { match self { Self::Low => (1, 2), // 4 spp (1 * (2 * 2)), plus optional temporal samples Self::Medium => (2, 2), // 8 spp (2 * (2 * 2)), plus optional temporal samples @@ -166,166 +101,235 @@ impl ScreenSpaceAmbientOcclusionQualityLevel { Self::Custom { slice_count: slices, samples_per_slice_side, - } => (*slices, *samples_per_slice_side), + } => (*slices as i32, *samples_per_slice_side as i32), } } } -#[derive(Default)] -struct SsaoNode {} +impl RenderTask for ScreenSpaceAmbientOcclusion { + type RenderNodeSubGraph = Core3d; + + fn render_node_label() -> impl RenderLabel { + NodePbr::ScreenSpaceAmbientOcclusion + } + + fn render_node_ordering() -> impl IntoRenderNodeArray { + ( + Node3d::EndPrepasses, + Self::render_node_label(), + Node3d::StartMainPass, + ) + } + + const REQUIRED_LIMITS: WgpuLimits = WgpuLimits { + max_storage_textures_per_shader_stage: 5, + ..WgpuLimits::downlevel_webgl2_defaults() + }; -impl ViewNode for SsaoNode { - type ViewQuery = ( - &'static ExtractedCamera, - &'static SsaoPipelineId, - &'static SsaoBindGroups, - &'static ViewUniformOffset, - ); + fn plugin_app_build(app: &mut App) { + load_shader_library!(app, "ssao_utils.wgsl"); - fn run( + embedded_asset!(app, "preprocess_depth.wgsl"); + embedded_asset!(app, "ssao.wgsl"); + embedded_asset!(app, "spatial_denoise.wgsl"); + } + + fn plugin_render_app_build(render_app: &mut SubApp) { + render_app.init_resource::(); + } + + fn encode_commands( &self, - _graph: &mut RenderGraphContext, - render_context: &mut RenderContext, - (camera, pipeline_id, bind_groups, view_uniform_offset): QueryItem, + mut ctx: RenderTaskContext, + camera_entity: Entity, world: &World, - ) -> Result<(), NodeRunError> { - let pipelines = world.resource::(); - let pipeline_cache = world.resource::(); - let ( - Some(camera_size), - Some(preprocess_depth_pipeline), - Some(spatial_denoise_pipeline), - Some(ssao_pipeline), - ) = ( - camera.physical_viewport_size, - pipeline_cache.get_compute_pipeline(pipelines.preprocess_depth_pipeline), - pipeline_cache.get_compute_pipeline(pipelines.spatial_denoise_pipeline), - pipeline_cache.get_compute_pipeline(pipeline_id.0), - ) - else { - return Ok(()); + ) -> Option<()> { + let (camera, prepass_textures, view_uniform_offset, has_temporal_jitter) = + world.entity(camera_entity).get_components::<( + &ExtractedCamera, + &ViewPrepassTextures, + &ViewUniformOffset, + Has, + )>()?; + let render_adapter = world.get_resource::()?; + let view_uniforms = world.get_resource::()?.uniforms.buffer()?; + let global_uniforms = world.get_resource::()?.buffer.buffer()?; + let static_resources = world.get_resource::()?; + let view_uniform_offset = view_uniform_offset.offset; + + let camera_size = camera.physical_viewport_size?.to_extents(); + let depth_format = get_depth_format(render_adapter); + let (slice_count, samples_per_slice_side) = self.quality_level.sample_counts(); + + // TODO: Helpers/builder pattern for texture descriptor creation + let preprocessed_depth_texture = ctx.texture(TextureDescriptor { + label: Some("ssao_preprocessed_depth_texture"), + size: camera_size, + mip_level_count: 5, + sample_count: 1, + dimension: TextureDimension::D2, + format: depth_format, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let preprocessed_depth_texture_view = |mip_level| -> TextureView { + let texture_view_descriptor = TextureViewDescriptor { + label: Some("ssao_preprocessed_depth_texture_mip_view"), + base_mip_level: mip_level, + format: Some(depth_format), + dimension: Some(TextureViewDimension::D2), + mip_level_count: Some(1), + ..default() + }; + + preprocessed_depth_texture + .texture() + .create_view(&texture_view_descriptor) + .into() }; - let diagnostics = render_context.diagnostic_recorder(); - - let command_encoder = render_context.command_encoder(); - command_encoder.push_debug_group("ssao"); - let time_span = diagnostics.time_span(command_encoder, "ssao"); - - { - let mut preprocess_depth_pass = - command_encoder.begin_compute_pass(&ComputePassDescriptor { - label: Some("ssao_preprocess_depth"), - timestamp_writes: None, - }); - preprocess_depth_pass.set_pipeline(preprocess_depth_pipeline); - preprocess_depth_pass.set_bind_group(0, &bind_groups.preprocess_depth_bind_group, &[]); - preprocess_depth_pass.set_bind_group( - 1, - &bind_groups.common_bind_group, - &[view_uniform_offset.offset], - ); - preprocess_depth_pass.dispatch_workgroups( - camera_size.x.div_ceil(16), - camera_size.y.div_ceil(16), - 1, - ); - } + let ssao_noisy_texture = ctx.texture(TextureDescriptor { + label: Some("ssao_noisy_texture"), + size: camera_size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: depth_format, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); - { - let mut ssao_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { - label: Some("ssao"), - timestamp_writes: None, - }); - ssao_pass.set_pipeline(ssao_pipeline); - ssao_pass.set_bind_group(0, &bind_groups.ssao_bind_group, &[]); - ssao_pass.set_bind_group( - 1, - &bind_groups.common_bind_group, - &[view_uniform_offset.offset], - ); - ssao_pass.dispatch_workgroups(camera_size.x.div_ceil(8), camera_size.y.div_ceil(8), 1); - } + // TODO: How does prepare_mesh_view_bind_groups() get access to this texture? + // Might need to create this one specifically outside of RenderTask + let ssao_texture = ctx.texture(TextureDescriptor { + label: Some("ssao_texture"), + size: camera_size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: depth_format, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); - { - let mut spatial_denoise_pass = - command_encoder.begin_compute_pass(&ComputePassDescriptor { - label: Some("ssao_spatial_denoise"), - timestamp_writes: None, - }); - spatial_denoise_pass.set_pipeline(spatial_denoise_pipeline); - spatial_denoise_pass.set_bind_group(0, &bind_groups.spatial_denoise_bind_group, &[]); - spatial_denoise_pass.set_bind_group( - 1, - &bind_groups.common_bind_group, - &[view_uniform_offset.offset], - ); - spatial_denoise_pass.dispatch_workgroups( - camera_size.x.div_ceil(8), - camera_size.y.div_ceil(8), - 1, - ); - } + let depth_differences_texture = ctx.texture(TextureDescriptor { + label: Some("ssao_depth_differences_texture"), + size: camera_size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R32Uint, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); - time_span.end(command_encoder); - command_encoder.pop_debug_group(); - Ok(()) + let render_device = world.get_resource::()?; + let thickness_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("thickness_buffer"), + contents: &self.constant_object_thickness.to_le_bytes(), + usage: BufferUsages::UNIFORM, + }); + + let common_resources = ( + ( + SamplerNonFiltering(&static_resources.point_clamp_sampler), + SamplerFiltering(&static_resources.linear_clamp_sampler), + DynamicUniformBuffer(view_uniforms), + ), + [view_uniform_offset].as_slice(), + ); + + ctx.compute_pass("preprocess_depth") + .shader(load_embedded_asset!(world, "preprocess_depth.wgsl")) + .shader_def_if("USE_R16FLOAT", depth_format == TextureFormat::R16Float) + .bind_resources(( + SampledTexture(prepass_textures.depth_view()?), + StorageTextureWriteOnly(&preprocessed_depth_texture_view(0)), + StorageTextureWriteOnly(&preprocessed_depth_texture_view(1)), + StorageTextureWriteOnly(&preprocessed_depth_texture_view(2)), + StorageTextureWriteOnly(&preprocessed_depth_texture_view(3)), + StorageTextureWriteOnly(&preprocessed_depth_texture_view(4)), + )) + .bind_resources_with_dynamic_offsets(common_resources) + .dispatch_2d( + camera_size.width.div_ceil(16), + camera_size.height.div_ceil(16), + )?; + + ctx.compute_pass("ssao") + .shader(load_embedded_asset!(world, "ssao.wgsl")) + .shader_def_if("USE_R16FLOAT", depth_format == TextureFormat::R16Float) + .shader_def_if("TEMPORAL_JITTER", has_temporal_jitter) + .shader_def(ShaderDefVal::Int("SLICE_COUNT".to_owned(), slice_count)) + .shader_def(ShaderDefVal::Int( + // TODO: Better API for making ShaderDefVals + "SAMPLES_PER_SLICE_SIDE".to_owned(), + samples_per_slice_side, + )) + .bind_resources(( + SampledTexture(&preprocessed_depth_texture), + SampledTexture(prepass_textures.normal_view()?), + SampledTexture(&static_resources.hilbert_index_lut), + StorageTextureWriteOnly(&ssao_noisy_texture), + StorageTextureWriteOnly(&depth_differences_texture), + UniformBuffer(global_uniforms), + UniformBuffer(&thickness_buffer), + )) + .bind_resources_with_dynamic_offsets(common_resources) + .dispatch_2d( + camera_size.width.div_ceil(8), + camera_size.height.div_ceil(8), + )?; + + ctx.compute_pass("ssao_spatial_denoise") + .shader(load_embedded_asset!(world, "spatial_denoise.wgsl")) + .shader_def_if("USE_R16FLOAT", depth_format == TextureFormat::R16Float) + .bind_resources(( + SampledTexture(&ssao_noisy_texture), + SampledTexture(&depth_differences_texture), + StorageTextureWriteOnly(&ssao_texture), + )) + .bind_resources_with_dynamic_offsets(common_resources) + .dispatch_2d( + camera_size.width.div_ceil(8), // TODO: Helpers for this kind of dispatch? + camera_size.height.div_ceil(8), + )?; + + Some(()) } } #[derive(Resource)] -struct SsaoPipelines { - preprocess_depth_pipeline: CachedComputePipelineId, - spatial_denoise_pipeline: CachedComputePipelineId, - - common_bind_group_layout: BindGroupLayoutDescriptor, - preprocess_depth_bind_group_layout: BindGroupLayoutDescriptor, - ssao_bind_group_layout: BindGroupLayoutDescriptor, - spatial_denoise_bind_group_layout: BindGroupLayoutDescriptor, - +struct SsaoStaticResources { hilbert_index_lut: TextureView, point_clamp_sampler: Sampler, linear_clamp_sampler: Sampler, - - shader: Handle, - depth_format: TextureFormat, } -impl FromWorld for SsaoPipelines { +impl FromWorld for SsaoStaticResources { fn from_world(world: &mut World) -> Self { let render_device = world.resource::(); let render_queue = world.resource::(); - let pipeline_cache = world.resource::(); - - // Detect the depth format support - let render_adapter = world.resource::(); - let depth_format = if render_adapter - .get_texture_format_features(TextureFormat::R16Float) - .allowed_usages - .contains(TextureUsages::STORAGE_BINDING) - { - TextureFormat::R16Float - } else { - TextureFormat::R32Float - }; + let texture_descriptor = TextureDescriptor { + label: Some("ssao_hilbert_index_lut"), + size: Extent3d { + width: HILBERT_WIDTH as u32, + height: HILBERT_WIDTH as u32, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R16Uint, + usage: TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }; let hilbert_index_lut = render_device .create_texture_with_data( render_queue, - &(TextureDescriptor { - label: Some("ssao_hilbert_index_lut"), - size: Extent3d { - width: HILBERT_WIDTH as u32, - height: HILBERT_WIDTH as u32, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::R16Uint, - usage: TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }), + &texture_descriptor, TextureDataOrder::default(), bytemuck::cast_slice(&generate_hilbert_index_lut()), ) @@ -339,6 +343,7 @@ impl FromWorld for SsaoPipelines { address_mode_v: AddressMode::ClampToEdge, ..Default::default() }); + let linear_clamp_sampler = render_device.create_sampler(&SamplerDescriptor { min_filter: FilterMode::Linear, mag_filter: FilterMode::Linear, @@ -348,399 +353,14 @@ impl FromWorld for SsaoPipelines { ..Default::default() }); - let common_bind_group_layout = BindGroupLayoutDescriptor::new( - "ssao_common_bind_group_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::COMPUTE, - ( - sampler(SamplerBindingType::NonFiltering), - sampler(SamplerBindingType::Filtering), - uniform_buffer::(true), - ), - ), - ); - - let preprocess_depth_bind_group_layout = BindGroupLayoutDescriptor::new( - "ssao_preprocess_depth_bind_group_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::COMPUTE, - ( - texture_depth_2d(), - texture_storage_2d(depth_format, StorageTextureAccess::WriteOnly), - texture_storage_2d(depth_format, StorageTextureAccess::WriteOnly), - texture_storage_2d(depth_format, StorageTextureAccess::WriteOnly), - texture_storage_2d(depth_format, StorageTextureAccess::WriteOnly), - texture_storage_2d(depth_format, StorageTextureAccess::WriteOnly), - ), - ), - ); - - let ssao_bind_group_layout = BindGroupLayoutDescriptor::new( - "ssao_ssao_bind_group_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::COMPUTE, - ( - texture_2d(TextureSampleType::Float { filterable: true }), - texture_2d(TextureSampleType::Float { filterable: false }), - texture_2d(TextureSampleType::Uint), - texture_storage_2d(depth_format, StorageTextureAccess::WriteOnly), - texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly), - uniform_buffer::(false), - uniform_buffer::(false), - ), - ), - ); - - let spatial_denoise_bind_group_layout = BindGroupLayoutDescriptor::new( - "ssao_spatial_denoise_bind_group_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::COMPUTE, - ( - texture_2d(TextureSampleType::Float { filterable: false }), - texture_2d(TextureSampleType::Uint), - texture_storage_2d(depth_format, StorageTextureAccess::WriteOnly), - ), - ), - ); - - let mut shader_defs = Vec::new(); - if depth_format == TextureFormat::R16Float { - shader_defs.push("USE_R16FLOAT".into()); - } - - let preprocess_depth_pipeline = - pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { - label: Some("ssao_preprocess_depth_pipeline".into()), - layout: vec![ - preprocess_depth_bind_group_layout.clone(), - common_bind_group_layout.clone(), - ], - shader: load_embedded_asset!(world, "preprocess_depth.wgsl"), - shader_defs: shader_defs.clone(), - ..default() - }); - - let spatial_denoise_pipeline = - pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { - label: Some("ssao_spatial_denoise_pipeline".into()), - layout: vec![ - spatial_denoise_bind_group_layout.clone(), - common_bind_group_layout.clone(), - ], - shader: load_embedded_asset!(world, "spatial_denoise.wgsl"), - shader_defs, - ..default() - }); - Self { - preprocess_depth_pipeline, - spatial_denoise_pipeline, - - common_bind_group_layout, - preprocess_depth_bind_group_layout, - ssao_bind_group_layout, - spatial_denoise_bind_group_layout, - hilbert_index_lut, point_clamp_sampler, linear_clamp_sampler, - - shader: load_embedded_asset!(world, "ssao.wgsl"), - depth_format, - } - } -} - -#[derive(PartialEq, Eq, Hash, Clone)] -struct SsaoPipelineKey { - quality_level: ScreenSpaceAmbientOcclusionQualityLevel, - temporal_jitter: bool, -} - -impl SpecializedComputePipeline for SsaoPipelines { - type Key = SsaoPipelineKey; - - fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor { - let (slice_count, samples_per_slice_side) = key.quality_level.sample_counts(); - - let mut shader_defs = vec![ - ShaderDefVal::Int("SLICE_COUNT".to_string(), slice_count as i32), - ShaderDefVal::Int( - "SAMPLES_PER_SLICE_SIDE".to_string(), - samples_per_slice_side as i32, - ), - ]; - - if key.temporal_jitter { - shader_defs.push("TEMPORAL_JITTER".into()); - } - - if self.depth_format == TextureFormat::R16Float { - shader_defs.push("USE_R16FLOAT".into()); - } - - ComputePipelineDescriptor { - label: Some("ssao_ssao_pipeline".into()), - layout: vec![ - self.ssao_bind_group_layout.clone(), - self.common_bind_group_layout.clone(), - ], - shader: self.shader.clone(), - shader_defs, - ..default() } } } -fn extract_ssao_settings( - mut commands: Commands, - cameras: Extract< - Query< - (RenderEntity, &Camera, &ScreenSpaceAmbientOcclusion, &Msaa), - (With, With, With), - >, - >, -) { - for (entity, camera, ssao_settings, msaa) in &cameras { - if *msaa != Msaa::Off { - error!( - "SSAO is being used which requires Msaa::Off, but Msaa is currently set to Msaa::{:?}", - *msaa - ); - return; - } - let mut entity_commands = commands - .get_entity(entity) - .expect("SSAO entity wasn't synced."); - if camera.is_active { - entity_commands.insert(ssao_settings.clone()); - } else { - entity_commands.remove::(); - } - } -} - -#[derive(Component)] -pub struct ScreenSpaceAmbientOcclusionResources { - preprocessed_depth_texture: CachedTexture, - ssao_noisy_texture: CachedTexture, // Pre-spatially denoised texture - pub screen_space_ambient_occlusion_texture: CachedTexture, // Spatially denoised texture - depth_differences_texture: CachedTexture, - thickness_buffer: Buffer, -} - -fn prepare_ssao_textures( - mut commands: Commands, - mut texture_cache: ResMut, - render_device: Res, - pipelines: Res, - views: Query<(Entity, &ExtractedCamera, &ScreenSpaceAmbientOcclusion)>, -) { - for (entity, camera, ssao_settings) in &views { - let Some(physical_viewport_size) = camera.physical_viewport_size else { - continue; - }; - let size = physical_viewport_size.to_extents(); - - let preprocessed_depth_texture = texture_cache.get( - &render_device, - TextureDescriptor { - label: Some("ssao_preprocessed_depth_texture"), - size, - mip_level_count: 5, - sample_count: 1, - dimension: TextureDimension::D2, - format: pipelines.depth_format, - usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - ); - - let ssao_noisy_texture = texture_cache.get( - &render_device, - TextureDescriptor { - label: Some("ssao_noisy_texture"), - size, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: pipelines.depth_format, - usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - ); - - let ssao_texture = texture_cache.get( - &render_device, - TextureDescriptor { - label: Some("ssao_texture"), - size, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: pipelines.depth_format, - usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - ); - - let depth_differences_texture = texture_cache.get( - &render_device, - TextureDescriptor { - label: Some("ssao_depth_differences_texture"), - size, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::R32Uint, - usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - ); - - let thickness_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { - label: Some("thickness_buffer"), - contents: &ssao_settings.constant_object_thickness.to_le_bytes(), - usage: BufferUsages::UNIFORM, - }); - - commands - .entity(entity) - .insert(ScreenSpaceAmbientOcclusionResources { - preprocessed_depth_texture, - ssao_noisy_texture, - screen_space_ambient_occlusion_texture: ssao_texture, - depth_differences_texture, - thickness_buffer, - }); - } -} - -#[derive(Component)] -struct SsaoPipelineId(CachedComputePipelineId); - -fn prepare_ssao_pipelines( - mut commands: Commands, - pipeline_cache: Res, - mut pipelines: ResMut>, - pipeline: Res, - views: Query<(Entity, &ScreenSpaceAmbientOcclusion, Has)>, -) { - for (entity, ssao_settings, temporal_jitter) in &views { - let pipeline_id = pipelines.specialize( - &pipeline_cache, - &pipeline, - SsaoPipelineKey { - quality_level: ssao_settings.quality_level, - temporal_jitter, - }, - ); - - commands.entity(entity).insert(SsaoPipelineId(pipeline_id)); - } -} - -#[derive(Component)] -struct SsaoBindGroups { - common_bind_group: BindGroup, - preprocess_depth_bind_group: BindGroup, - ssao_bind_group: BindGroup, - spatial_denoise_bind_group: BindGroup, -} - -fn prepare_ssao_bind_groups( - mut commands: Commands, - render_device: Res, - pipelines: Res, - view_uniforms: Res, - global_uniforms: Res, - pipeline_cache: Res, - views: Query<( - Entity, - &ScreenSpaceAmbientOcclusionResources, - &ViewPrepassTextures, - )>, -) { - let (Some(view_uniforms), Some(globals_uniforms)) = ( - view_uniforms.uniforms.binding(), - global_uniforms.buffer.binding(), - ) else { - return; - }; - - for (entity, ssao_resources, prepass_textures) in &views { - let common_bind_group = render_device.create_bind_group( - "ssao_common_bind_group", - &pipeline_cache.get_bind_group_layout(&pipelines.common_bind_group_layout), - &BindGroupEntries::sequential(( - &pipelines.point_clamp_sampler, - &pipelines.linear_clamp_sampler, - view_uniforms.clone(), - )), - ); - - let create_depth_view = |mip_level| { - ssao_resources - .preprocessed_depth_texture - .texture - .create_view(&TextureViewDescriptor { - label: Some("ssao_preprocessed_depth_texture_mip_view"), - base_mip_level: mip_level, - format: Some(pipelines.depth_format), - dimension: Some(TextureViewDimension::D2), - mip_level_count: Some(1), - ..default() - }) - }; - - let preprocess_depth_bind_group = render_device.create_bind_group( - "ssao_preprocess_depth_bind_group", - &pipeline_cache.get_bind_group_layout(&pipelines.preprocess_depth_bind_group_layout), - &BindGroupEntries::sequential(( - prepass_textures.depth_view().unwrap(), - &create_depth_view(0), - &create_depth_view(1), - &create_depth_view(2), - &create_depth_view(3), - &create_depth_view(4), - )), - ); - - let ssao_bind_group = render_device.create_bind_group( - "ssao_ssao_bind_group", - &pipeline_cache.get_bind_group_layout(&pipelines.ssao_bind_group_layout), - &BindGroupEntries::sequential(( - &ssao_resources.preprocessed_depth_texture.default_view, - prepass_textures.normal_view().unwrap(), - &pipelines.hilbert_index_lut, - &ssao_resources.ssao_noisy_texture.default_view, - &ssao_resources.depth_differences_texture.default_view, - globals_uniforms.clone(), - ssao_resources.thickness_buffer.as_entire_binding(), - )), - ); - - let spatial_denoise_bind_group = render_device.create_bind_group( - "ssao_spatial_denoise_bind_group", - &pipeline_cache.get_bind_group_layout(&pipelines.spatial_denoise_bind_group_layout), - &BindGroupEntries::sequential(( - &ssao_resources.ssao_noisy_texture.default_view, - &ssao_resources.depth_differences_texture.default_view, - &ssao_resources - .screen_space_ambient_occlusion_texture - .default_view, - )), - ); - - commands.entity(entity).insert(SsaoBindGroups { - common_bind_group, - preprocess_depth_bind_group, - ssao_bind_group, - spatial_denoise_bind_group, - }); - } -} - fn generate_hilbert_index_lut() -> [[u16; 64]; 64] { use core::array::from_fn; from_fn(|x| from_fn(|y| hilbert_index(x as u16, y as u16))) @@ -771,3 +391,15 @@ fn hilbert_index(mut x: u16, mut y: u16) -> u16 { index } + +fn get_depth_format(render_adapter: &RenderAdapter) -> TextureFormat { + if render_adapter + .get_texture_format_features(TextureFormat::R16Float) + .allowed_usages + .contains(TextureUsages::STORAGE_BINDING) + { + TextureFormat::R16Float + } else { + TextureFormat::R32Float + } +} diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 11b6ceb63bce4..a9cbee6c33682 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -55,6 +55,7 @@ pub mod render_asset; pub mod render_graph; pub mod render_phase; pub mod render_resource; +pub mod render_task; pub mod renderer; pub mod settings; pub mod storage; diff --git a/crates/bevy_render/src/render_graph/app.rs b/crates/bevy_render/src/render_graph/app.rs index 879f28fe54015..879d2b038cde3 100644 --- a/crates/bevy_render/src/render_graph/app.rs +++ b/crates/bevy_render/src/render_graph/app.rs @@ -17,10 +17,10 @@ pub trait RenderGraphExt { node_label: impl RenderLabel, ) -> &mut Self; /// Automatically add the required node edges based on the given ordering - fn add_render_graph_edges( + fn add_render_graph_edges( &mut self, sub_graph: impl RenderSubGraph, - edges: impl IntoRenderNodeArray, + edges: impl IntoRenderNodeArray, ) -> &mut Self; /// Add node edge to the specified graph @@ -54,10 +54,10 @@ impl RenderGraphExt for World { } #[track_caller] - fn add_render_graph_edges( + fn add_render_graph_edges( &mut self, sub_graph: impl RenderSubGraph, - edges: impl IntoRenderNodeArray, + edges: impl IntoRenderNodeArray, ) -> &mut Self { let sub_graph = sub_graph.intern(); let mut render_graph = self.get_resource_mut::().expect( @@ -123,10 +123,10 @@ impl RenderGraphExt for SubApp { } #[track_caller] - fn add_render_graph_edges( + fn add_render_graph_edges( &mut self, sub_graph: impl RenderSubGraph, - edges: impl IntoRenderNodeArray, + edges: impl IntoRenderNodeArray, ) -> &mut Self { World::add_render_graph_edges(self.world_mut(), sub_graph, edges); self @@ -158,10 +158,11 @@ impl RenderGraphExt for App { self } - fn add_render_graph_edges( + #[track_caller] + fn add_render_graph_edges( &mut self, sub_graph: impl RenderSubGraph, - edges: impl IntoRenderNodeArray, + edges: impl IntoRenderNodeArray, ) -> &mut Self { World::add_render_graph_edges(self.world_mut(), sub_graph, edges); self diff --git a/crates/bevy_render/src/render_graph/graph.rs b/crates/bevy_render/src/render_graph/graph.rs index b7f8328610054..1254e97b36dae 100644 --- a/crates/bevy_render/src/render_graph/graph.rs +++ b/crates/bevy_render/src/render_graph/graph.rs @@ -146,7 +146,7 @@ impl RenderGraph { /// Defining an edge that already exists is not considered an error with this api. /// It simply won't create a new edge. #[track_caller] - pub fn add_node_edges(&mut self, edges: impl IntoRenderNodeArray) { + pub fn add_node_edges(&mut self, edges: impl IntoRenderNodeArray) { for window in edges.into_array().windows(2) { let [a, b] = window else { break; diff --git a/crates/bevy_render/src/render_graph/node.rs b/crates/bevy_render/src/render_graph/node.rs index 4355892487c00..b82a0b248a6de 100644 --- a/crates/bevy_render/src/render_graph/node.rs +++ b/crates/bevy_render/src/render_graph/node.rs @@ -34,18 +34,18 @@ define_label!( /// A shorthand for `Interned`. pub type InternedRenderLabel = Interned; -pub trait IntoRenderNodeArray { - fn into_array(self) -> [InternedRenderLabel; N]; +pub trait IntoRenderNodeArray { + fn into_array(self) -> Vec; } macro_rules! impl_render_label_tuples { ($N: expr, $(#[$meta:meta])* $(($T: ident, $I: ident)),*) => { $(#[$meta])* - impl<$($T: RenderLabel),*> IntoRenderNodeArray<$N> for ($($T,)*) { + impl<$($T: RenderLabel),*> IntoRenderNodeArray for ($($T,)*) { #[inline] - fn into_array(self) -> [InternedRenderLabel; $N] { + fn into_array(self) -> Vec { let ($($I,)*) = self; - [$($I.intern(), )*] + vec![$($I.intern(), )*] } } } diff --git a/crates/bevy_render/src/render_resource/bind_group_entries.rs b/crates/bevy_render/src/render_resource/bind_group_entries.rs index 274aa111434f6..50216783aaf22 100644 --- a/crates/bevy_render/src/render_resource/bind_group_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_entries.rs @@ -182,6 +182,13 @@ impl<'a> IntoBinding<'a> for BindingResource<'a> { } } +impl<'a> IntoBinding<'a> for &'a super::Buffer { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + self.as_entire_binding() + } +} + impl<'a> IntoBinding<'a> for wgpu::BufferBinding<'a> { #[inline] fn into_binding(self) -> BindingResource<'a> { diff --git a/crates/bevy_render/src/render_resource/pipeline.rs b/crates/bevy_render/src/render_resource/pipeline.rs index f3976f68af916..385d5789f6b6a 100644 --- a/crates/bevy_render/src/render_resource/pipeline.rs +++ b/crates/bevy_render/src/render_resource/pipeline.rs @@ -104,7 +104,7 @@ impl BindGroupLayoutDescriptor { } /// Describes a render (graphics) pipeline. -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Eq, Default, Hash)] pub struct RenderPipelineDescriptor { /// Debug label of the pipeline. This will show up in graphics debuggers for easy identification. pub label: Option>, @@ -142,7 +142,7 @@ impl RenderPipelineDescriptor { } } -#[derive(Clone, Debug, Eq, PartialEq, Default)] +#[derive(Clone, Debug, Eq, PartialEq, Default, Hash)] pub struct VertexState { /// The compiled shader module for this stage. pub shader: Handle, @@ -155,7 +155,7 @@ pub struct VertexState { } /// Describes the fragment process in a render pipeline. -#[derive(Clone, Debug, PartialEq, Eq, Default)] +#[derive(Clone, Debug, PartialEq, Eq, Default, Hash)] pub struct FragmentState { /// The compiled shader module for this stage. pub shader: Handle, @@ -174,7 +174,7 @@ impl FragmentState { } /// Describes a compute pipeline. -#[derive(Clone, Debug, PartialEq, Eq, Default)] +#[derive(Clone, Debug, PartialEq, Eq, Default, Hash)] pub struct ComputePipelineDescriptor { pub label: Option>, pub layout: Vec, diff --git a/crates/bevy_render/src/render_task/bind.rs b/crates/bevy_render/src/render_task/bind.rs new file mode 100644 index 0000000000000..f5710c96aca76 --- /dev/null +++ b/crates/bevy_render/src/render_task/bind.rs @@ -0,0 +1,230 @@ +use crate::render_resource::{ + BindGroupLayoutEntryBuilder, Buffer, IntoBindGroupLayoutEntryBuilder, IntoBinding, TextureView, +}; +use bevy_derive::Deref; +use wgpu::{ + BindingResource, BindingType, BufferBindingType, SamplerBindingType, StorageTextureAccess, + TextureViewDimension, +}; + +/// Corresponds to `var my_buffer: T` in a WGSL shader. +#[derive(Clone, Deref)] +pub struct StorageBufferReadOnly<'a>(pub &'a Buffer); + +/// Corresponds to `var my_buffer: T` in a WGSL shader. +#[derive(Clone, Deref)] +pub struct StorageBufferReadWrite<'a>(pub &'a Buffer); + +/// Corresponds to `var my_buffer: T` in a WGSL shader. +#[derive(Clone, Deref)] +pub struct UniformBuffer<'a>(pub &'a Buffer); + +/// Corresponds to `var my_buffer: T` in a WGSL shader. +#[derive(Clone, Deref)] +pub struct DynamicUniformBuffer<'a>(pub &'a Buffer); + +/// Corresponds to `var my_texture: texture_2d` or `var my_texture: texture_depth_2d` in a WGSL shader. +#[derive(Clone, Deref)] +pub struct SampledTexture<'a>(pub &'a TextureView); + +/// Corresponds to `var my_texture: texture_storage_2d` in a WGSL shader. +#[derive(Clone, Deref)] +pub struct StorageTextureReadWrite<'a>(pub &'a TextureView); + +/// Corresponds to `var my_texture: texture_storage_2d` in a WGSL shader. +#[derive(Clone, Deref)] +pub struct StorageTextureWriteOnly<'a>(pub &'a TextureView); + +/// Corresponds to `var my_texture: texture_storage_2d` in a WGSL shader. +#[derive(Clone, Deref)] +pub struct StorageTextureReadOnly<'a>(pub &'a TextureView); + +/// Corresponds to `var my_texture: texture_storage_2d` in a WGSL shader. +#[derive(Clone, Deref)] +pub struct StorageTextureAtomic<'a>(pub &'a TextureView); + +/// Corresponds to `var my_sampler: sampler` in a WGSL shader. +#[derive(Clone, Deref)] +pub struct SamplerNonFiltering<'a>(pub &'a crate::render_resource::Sampler); + +/// Corresponds to `var my_sampler: sampler` in a WGSL shader. +#[derive(Clone, Deref)] +pub struct SamplerFiltering<'a>(pub &'a crate::render_resource::Sampler); + +impl<'a> IntoBinding<'a> for StorageBufferReadOnly<'a> { + fn into_binding(self) -> BindingResource<'a> { + self.0.as_entire_binding() + } +} + +impl<'a> IntoBindGroupLayoutEntryBuilder for StorageBufferReadOnly<'a> { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { + BindingType::Buffer { + ty: BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + } + .into_bind_group_layout_entry_builder() + } +} + +impl<'a> IntoBinding<'a> for StorageBufferReadWrite<'a> { + fn into_binding(self) -> BindingResource<'a> { + self.0.as_entire_binding() + } +} + +impl<'a> IntoBindGroupLayoutEntryBuilder for StorageBufferReadWrite<'a> { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { + BindingType::Buffer { + ty: BufferBindingType::Storage { read_only: false }, + has_dynamic_offset: false, + min_binding_size: None, + } + .into_bind_group_layout_entry_builder() + } +} + +impl<'a> IntoBinding<'a> for UniformBuffer<'a> { + fn into_binding(self) -> BindingResource<'a> { + self.0.as_entire_binding() + } +} + +impl<'a> IntoBindGroupLayoutEntryBuilder for UniformBuffer<'a> { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { + BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + } + .into_bind_group_layout_entry_builder() + } +} + +impl<'a> IntoBinding<'a> for DynamicUniformBuffer<'a> { + fn into_binding(self) -> BindingResource<'a> { + self.0.as_entire_binding() + } +} + +impl<'a> IntoBindGroupLayoutEntryBuilder for DynamicUniformBuffer<'a> { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { + BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: None, + } + .into_bind_group_layout_entry_builder() + } +} + +impl<'a> IntoBinding<'a> for SampledTexture<'a> { + fn into_binding(self) -> BindingResource<'a> { + BindingResource::TextureView(self.0) + } +} + +impl<'a> IntoBindGroupLayoutEntryBuilder for SampledTexture<'a> { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { + BindingType::Texture { + sample_type: self.texture().format().sample_type(None, None).unwrap(), + view_dimension: TextureViewDimension::D2, + multisampled: self.0.texture().sample_count() > 1, + } + .into_bind_group_layout_entry_builder() + } +} + +impl<'a> IntoBinding<'a> for StorageTextureReadWrite<'a> { + fn into_binding(self) -> BindingResource<'a> { + BindingResource::TextureView(self.0) + } +} + +impl<'a> IntoBindGroupLayoutEntryBuilder for StorageTextureReadWrite<'a> { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { + BindingType::StorageTexture { + access: StorageTextureAccess::ReadWrite, + format: self.0.texture().format(), + view_dimension: TextureViewDimension::D2, + } + .into_bind_group_layout_entry_builder() + } +} + +impl<'a> IntoBinding<'a> for StorageTextureWriteOnly<'a> { + fn into_binding(self) -> BindingResource<'a> { + BindingResource::TextureView(self.0) + } +} + +impl<'a> IntoBindGroupLayoutEntryBuilder for StorageTextureWriteOnly<'a> { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { + BindingType::StorageTexture { + access: StorageTextureAccess::WriteOnly, + format: self.0.texture().format(), + view_dimension: TextureViewDimension::D2, + } + .into_bind_group_layout_entry_builder() + } +} + +impl<'a> IntoBinding<'a> for StorageTextureReadOnly<'a> { + fn into_binding(self) -> BindingResource<'a> { + BindingResource::TextureView(self.0) + } +} + +impl<'a> IntoBindGroupLayoutEntryBuilder for StorageTextureReadOnly<'a> { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { + BindingType::StorageTexture { + access: StorageTextureAccess::ReadOnly, + format: self.0.texture().format(), + view_dimension: TextureViewDimension::D2, + } + .into_bind_group_layout_entry_builder() + } +} + +impl<'a> IntoBinding<'a> for StorageTextureAtomic<'a> { + fn into_binding(self) -> BindingResource<'a> { + BindingResource::TextureView(self.0) + } +} + +impl<'a> IntoBindGroupLayoutEntryBuilder for StorageTextureAtomic<'a> { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { + BindingType::StorageTexture { + access: StorageTextureAccess::Atomic, + format: self.0.texture().format(), + view_dimension: TextureViewDimension::D2, + } + .into_bind_group_layout_entry_builder() + } +} + +impl<'a> IntoBinding<'a> for SamplerNonFiltering<'a> { + fn into_binding(self) -> BindingResource<'a> { + BindingResource::Sampler(self.0) + } +} + +impl<'a> IntoBindGroupLayoutEntryBuilder for SamplerNonFiltering<'a> { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { + BindingType::Sampler(SamplerBindingType::NonFiltering) + .into_bind_group_layout_entry_builder() + } +} + +impl<'a> IntoBinding<'a> for SamplerFiltering<'a> { + fn into_binding(self) -> BindingResource<'a> { + BindingResource::Sampler(self.0) + } +} + +impl<'a> IntoBindGroupLayoutEntryBuilder for SamplerFiltering<'a> { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { + BindingType::Sampler(SamplerBindingType::Filtering).into_bind_group_layout_entry_builder() + } +} diff --git a/crates/bevy_render/src/render_task/compute_builder.rs b/crates/bevy_render/src/render_task/compute_builder.rs new file mode 100644 index 0000000000000..4d8d4122d1057 --- /dev/null +++ b/crates/bevy_render/src/render_task/compute_builder.rs @@ -0,0 +1,215 @@ +use super::resource_cache::ResourceCache; +use crate::{ + render_resource::{ + BindGroup, BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries, Buffer, + ComputePipelineDescriptor, IntoBindGroupLayoutEntryBuilderArray, IntoBindingArray, + }, + renderer::RenderDevice, + PipelineCache as PipelineCompiler, +}; +use bevy_asset::Handle; +use bevy_shader::{Shader, ShaderDefVal}; +use bytemuck::NoUninit; +use std::borrow::Cow; +use wgpu::{BindGroupDescriptor, ComputePass, DynamicOffset, PushConstantRange, ShaderStages}; + +pub struct ComputeCommandBuilder<'a> { + pass: &'a mut ComputePass<'static>, + pass_name: &'a str, + shader: Handle, + entry_point: Option<&'static str>, + shader_defs: Vec, + push_constants: Option<&'a [u8]>, + bind_groups: Vec<(Option, &'a [DynamicOffset])>, + bind_group_layouts: Vec, + resource_cache: &'a mut ResourceCache, + pipeline_compiler: &'a PipelineCompiler, + render_device: &'a RenderDevice, +} + +impl<'a> ComputeCommandBuilder<'a> { + pub fn new( + pass: &'a mut ComputePass<'static>, + pass_name: &'a str, + resource_cache: &'a mut ResourceCache, + pipeline_compiler: &'a PipelineCompiler, + render_device: &'a RenderDevice, + ) -> Self { + Self { + pass, + pass_name, + shader: Handle::default(), + entry_point: None, + shader_defs: Vec::new(), + push_constants: None, + bind_groups: Vec::new(), + bind_group_layouts: Vec::new(), + resource_cache, + pipeline_compiler, + render_device, + } + } + + pub fn shader(mut self, shader: Handle) -> Self { + self.shader = shader; + self + } + + pub fn entry_point(mut self, entry_point: &'static str) -> Self { + self.entry_point = Some(entry_point); + self + } + + pub fn shader_def(mut self, shader_def: impl Into) -> Self { + self.shader_defs.push(shader_def.into()); + self + } + + pub fn shader_def_if(mut self, shader_def: impl Into, condition: bool) -> Self { + if condition { + self.shader_defs.push(shader_def.into()); + } + self + } + + pub fn push_constants(mut self, push_constants: &'a [T]) -> Self { + self.push_constants = Some(bytemuck::cast_slice(push_constants)); + self + } + + pub fn bind_resources<'b, const N: usize>( + self, + resources: impl IntoBindingArray<'b, N> + IntoBindGroupLayoutEntryBuilderArray + Clone, + ) -> Self { + self.bind_resources_with_dynamic_offsets((resources, &[])) + } + + pub fn bind_resources_with_dynamic_offsets<'b, const N: usize>( + mut self, + (resources, dynamic_offsets): ( + impl IntoBindingArray<'b, N> + IntoBindGroupLayoutEntryBuilderArray + Clone, + &'a [DynamicOffset], + ), + ) -> Self { + let layout_descriptor = BindGroupLayoutDescriptor::new( + self.pass_name.to_owned(), + &BindGroupLayoutEntries::sequential(ShaderStages::COMPUTE, resources.clone()), + ); + + let descriptor = BindGroupDescriptor { + label: Some(self.pass_name), + layout: &self + .pipeline_compiler + .get_bind_group_layout(&layout_descriptor), + entries: &BindGroupEntries::sequential(resources), + }; + + // TODO + // self.bind_groups.push(Some( + // self.resource_cache + // .get_or_create_bind_group(descriptor, self.render_device), + // )); + self.bind_groups.push(( + Some( + self.render_device + .wgpu_device() + .create_bind_group(&descriptor) + .into(), + ), + dynamic_offsets, + )); + + self.bind_group_layouts.push(layout_descriptor); + + self + } + + pub fn bind_group( + mut self, + bind_group: impl Into>, + layout: BindGroupLayoutDescriptor, + ) -> Self { + self.bind_groups.push((bind_group.into(), &[])); + self.bind_group_layouts.push(layout); + self + } + + pub fn bind_group_with_dynamic_offsets( + mut self, + bind_group: BindGroup, + dynamic_offsets: &'a [DynamicOffset], + layout: BindGroupLayoutDescriptor, + ) -> Self { + self.bind_groups.push((Some(bind_group), dynamic_offsets)); + self.bind_group_layouts.push(layout); + self + } + + #[must_use] + pub fn dispatch_1d(mut self, x: u32) -> Option { + self.setup_state()?; + self.pass.dispatch_workgroups(x, 1, 1); + Some(self) + } + + #[must_use] + pub fn dispatch_2d(mut self, x: u32, y: u32) -> Option { + self.setup_state()?; + self.pass.dispatch_workgroups(x, y, 1); + Some(self) + } + + #[must_use] + pub fn dispatch_3d(mut self, x: u32, y: u32, z: u32) -> Option { + self.setup_state()?; + self.pass.dispatch_workgroups(x, y, z); + Some(self) + } + + #[must_use] + pub fn dispatch_indirect(mut self, buffer: &Buffer) -> Option { + self.setup_state()?; + self.pass.dispatch_workgroups_indirect(buffer, 0); + Some(self) + } + + #[must_use] + fn setup_state(&mut self) -> Option<()> { + let push_constant_ranges = self + .push_constants + .map(|pc| { + vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..(pc.len() as u32), + }] + }) + .unwrap_or_default(); + + let pipeline = self.resource_cache.get_or_compile_compute_pipeline( + ComputePipelineDescriptor { + label: Some(self.pass_name.to_owned().into()), + layout: self.bind_group_layouts.clone(), + push_constant_ranges, + shader: self.shader.clone(), + shader_defs: self.shader_defs.clone(), + entry_point: self.entry_point.map(Cow::from), + zero_initialize_workgroup_memory: false, + }, + self.pipeline_compiler, + )?; + + self.pass.set_pipeline(&pipeline); // TODO: Only set if changed + + if let Some(push_constants) = self.push_constants { + self.pass.set_push_constants(0, push_constants); // TODO: Only set if pipeline changed + } + + for (i, (bind_group, dynamic_offsets)) in self.bind_groups.iter().enumerate() { + // TODO: Only set if changed + self.pass + .set_bind_group(i as u32, bind_group.as_deref(), dynamic_offsets); + } + + Some(()) + } +} diff --git a/crates/bevy_render/src/render_task/mod.rs b/crates/bevy_render/src/render_task/mod.rs new file mode 100644 index 0000000000000..1c368ceb06cee --- /dev/null +++ b/crates/bevy_render/src/render_task/mod.rs @@ -0,0 +1,192 @@ +//! Low-level API for custom rendering tasks (graphics or compute). +//! +//! See [`RenderTask`] for more details. + +/// Types for binding buffers or textures to your render passes. +pub mod bind; + +mod compute_builder; +mod node; +mod plugin; +mod resource_cache; + +pub use compute_builder::ComputeCommandBuilder; +pub use node::RenderTaskContext; +pub use plugin::RenderTaskPlugin; + +use crate::{ + extract_component::ExtractComponent, + render_graph::{IntoRenderNodeArray, RenderLabel, RenderSubGraph}, + settings::{WgpuFeatures, WgpuLimits}, +}; +use bevy_app::{App, SubApp}; +use bevy_ecs::{component::Component, entity::Entity, world::World}; + +/// Low-level API for custom rendering tasks (graphics or compute). +/// +/// # Introduction +/// +/// [`RenderTask`] is a low-level API for adding custom rendering work to your Bevy app. +/// +/// It provides some convenient helpers to reduce CPU-code boilerplate of common [`bevy_render`] API usages within Bevy. +/// +/// # Use cases +/// +/// Bevy provides several different APIs, depending on how deeply you want to customize rendering: +/// - **High level** - `Material` - Intended for artists writing shaders to customize the visual appearance of a `Mesh`. +/// - **Mid level** - [`crate::render_phase::PhaseItem`] and [`crate::render_phase::RenderCommand`] - Intended for rendering engineers to customize how specific entities render, beyond Bevy's default of issuing a draw call with a vertex and index buffer. +/// - **Low level** - [`RenderTask`] (this trait, and [`bevy_render`] in general) - Intended for rendering engineers to add completely from-scratch rendering tasks associated with a `Camera`, e.g. a compute shader-based weather simulation. +/// - **Lowest level** - Writing your own renderer on top of bevy_mesh, bevy_camera, etc, without using [`bevy_render`] / [`wgpu`]. +/// +/// # What this trait does +/// +/// This trait wraps several common pieces of functionality for creating a new rendering feature: +/// * Checking that the user's GPU supports the required [`WgpuFeatures`] and [`WgpuLimits`] to use the feature. +/// * Setting up a [`crate::render_graph::Node`]. +/// * Syncing (extracting) a camera component in the main world to the render world. +/// * Creating and caching textures, buffers, bind groups, and pipelines. +/// * Binding resources and encoding draw/dispatch commands in render/compute passes. +/// * Adding profiling spans to passes. +/// +/// # Usage +/// +/// ## 1) Define your camera component +/// In Bevy, almost all rendering work is driven by cameras. +/// +/// To add a new rendering task to your app, write a new component for a camera: +/// +/// ```rust +/// #[derive(Component, ExtractComponent)] +/// struct MyRenderingFeature { /* ... */ } +/// +/// impl RenderTask for MyRenderingFeature { +/// // ... +/// } +/// ``` +/// +/// ## 2) (Optional) Set required GPU features and limits +/// Tasks can optionally require certain GPU features and limits in order to run. +/// +/// If the defaults (no required features, [`WgpuLimits::downlevel_webgl2_defaults()`]) are sufficient for your task, you may skip this step. +/// +/// ```rust +/// const REQUIRED_FEATURES: WgpuFeatures = WgpuFeatures::SHADER_F64; +/// const REQUIRED_LIMITS: WgpuLimits = WgpuLimits { max_sampled_textures_per_shader_stage: 32, ..WgpuLimits::downlevel_webgl2_defaults() }; +/// ``` +/// +/// ## 3) Setup a render node +/// Render nodes control the order that each piece of rendering work runs in, relative to other rendering work. +/// +/// ```rust +/// #[derive(RenderLabel, Default)] +/// struct MyRenderingFeatureNode; +/// +/// type RenderNodeSubGraph = Core3d; // Run as part of the Core3d render graph +/// +/// fn render_node_label() -> impl RenderLabel { +/// MyRenderingFeatureNode +/// } +/// +/// fn render_node_ordering() -> impl IntoRenderNodeArray { +/// ( +/// Node3d::EndPrepasses, +/// Self::render_node_label(), // Run sometime after the end of the prepass rendering, and before the end of the main pass rendering +/// Node3d::EndMainPass, +/// ) +/// } +/// ``` +/// +/// ## 4) (Optional) Add additional plugin setup code +/// If you need additional resources, systems, etc as part of your plugin, you can add them like this: +/// +/// ```rust +/// fn plugin_app_build(app: &mut App) { +/// app.insert_resource(/* ... */); +/// } +/// +/// fn plugin_render_app_build(render_app: &mut SubApp) { +/// render_app.add_systems(Render, /* ... */); +/// } +/// ``` +/// +/// ## 5) Encode commands +/// With the setup out of the way, you can now define the actual render work your task will do. +/// +/// Create resources and encode render commands as follows: +/// +/// ```rust +/// fn encode_commands(&self, mut ctx: RenderTaskContext, camera_entity: Entity, world: &World) -> Option<(); { +/// let (component_a, component_b) = world +/// .entity(entity) +/// .get_components::<(&ComponentA, &ComponentB)>()?; +/// +/// let resource = world.get_resource::()?; +/// +/// if self.foo { +/// // ... +/// } +/// +/// let texture = ctx.texture(TextureDescriptor { /* ... */ }); +/// let buffer = ctx.buffer(BufferDescriptor { /* ... */ }); +/// +/// ctx.compute_pass("my_pass") +/// .shader(load_embedded_asset!(world, "my_shader.wgsl")) +/// .bind_resources(( +/// SampledTexture(&texture), +/// StorageTextureReadWrite(&buffer), +/// )) +/// .dispatch_2d(10, 20)?; +/// +/// Some(()) +/// } +/// ``` +/// +/// ## 6) Use the plugin +/// Finally, you can add the task plugin to your app, and use it with your camera: +/// +/// ```rust +/// app.add_plugins(RenderTaskPlugin::::default()); +/// +/// commands.spawn(( +/// Camera3d::default(), +/// MyRenderingFeature::new(), +/// )); +/// ``` +pub trait RenderTask: Component + ExtractComponent { + /// What render graph the task should run it. + type RenderNodeSubGraph: RenderSubGraph + Default; + + /// Render node label for the task. + fn render_node_label() -> impl RenderLabel; + + /// Ordering to run render nodes in. + fn render_node_ordering() -> impl IntoRenderNodeArray; + + /// Required GPU features for the task. + /// + /// Defaults to [`WgpuFeatures::empty()`]. + const REQUIRED_FEATURES: WgpuFeatures = WgpuFeatures::empty(); + + /// Required GPU limits for the task. + /// + /// Defaults to [`WgpuLimits::downlevel_webgl2_defaults()`]. + const REQUIRED_LIMITS: WgpuLimits = WgpuLimits::downlevel_webgl2_defaults(); + + /// Optional additional plugin setup for the main app. + #[expect(unused_variables)] + fn plugin_app_build(app: &mut App) {} + + /// Optional additional plugin setup for the render app. + #[expect(unused_variables)] + fn plugin_render_app_build(render_app: &mut SubApp) {} + + /// Function to encode render commands for the task. + /// + /// This is where you create textures, run shaders, etc. + fn encode_commands( + &self, + ctx: RenderTaskContext, + camera_entity: Entity, + world: &World, + ) -> Option<()>; +} diff --git a/crates/bevy_render/src/render_task/node.rs b/crates/bevy_render/src/render_task/node.rs new file mode 100644 index 0000000000000..5298eb91c07dc --- /dev/null +++ b/crates/bevy_render/src/render_task/node.rs @@ -0,0 +1,121 @@ +use super::{compute_builder::ComputeCommandBuilder, resource_cache::ResourceCache, RenderTask}; +use crate::{ + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{Buffer, TextureView}, + renderer::{RenderContext, RenderDevice}, + PipelineCache as PipelineCompiler, +}; +use bevy_ecs::{ + entity::Entity, + query::QueryItem, + world::{FromWorld, World}, +}; +use std::{ + marker::PhantomData, + sync::{Arc, Mutex}, +}; +use wgpu::{ + BufferDescriptor, CommandEncoder, CommandEncoderDescriptor, ComputePass, ComputePassDescriptor, + TextureDescriptor, +}; + +// TODO: Profiling spans + +#[derive(FromWorld)] +pub struct RenderTaskNode { + resource_cache: Arc>, + _phantom_data: PhantomData, +} + +impl ViewNode for RenderTaskNode { + type ViewQuery = (&'static T, Entity); + + fn run<'w>( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + (task, camera_entity): QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let resource_cache = Arc::clone(&self.resource_cache); + + render_context.add_command_buffer_generation_task(move |render_device| { + let mut command_encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some(std::any::type_name::()), + }); + + let context = RenderTaskContext { + camera_entity, + command_encoder: &mut command_encoder, + compute_pass: None, + resource_cache: &mut resource_cache.lock().unwrap(), + pipeline_compiler: world.resource::(), + render_device: &render_device, + }; + + task.encode_commands(context, camera_entity, world); + + command_encoder.finish() + }); + + Ok(()) + } +} + +/// Create resources and start render and compute passes as part of a [`RenderTask`]. +pub struct RenderTaskContext<'a> { + camera_entity: Entity, + command_encoder: &'a mut CommandEncoder, + compute_pass: Option>, + resource_cache: &'a mut ResourceCache, + pipeline_compiler: &'a PipelineCompiler, + render_device: &'a RenderDevice, +} + +impl<'a> RenderTaskContext<'a> { + /// Create a texture belonging to the camera entity. + /// + /// The texture will be cached across frames, and will not be recreated when this function is next called, + /// so long as the descriptor is kept the same. + pub fn texture(&mut self, descriptor: TextureDescriptor<'static>) -> TextureView { + self.resource_cache.get_or_create_texture( + descriptor, + self.camera_entity, + self.render_device, + ) + } + + /// Create a buffer belonging to the camera entity. + /// + /// The buffer will be cached across frames, and will not be recreated when this function is next called, + /// so long as the descriptor is kept the same. + pub fn buffer(&mut self, descriptor: BufferDescriptor<'static>) -> Buffer { + self.resource_cache + .get_or_create_buffer(descriptor, self.camera_entity, self.render_device) + } + + /// Begin a new render pass. + pub fn render_pass(&mut self) { + todo!() + } + + /// Begin a new compute pass. + pub fn compute_pass<'b>(&'b mut self, pass_name: &'b str) -> ComputeCommandBuilder<'b> { + if self.compute_pass.is_none() { + self.compute_pass = Some( + self.command_encoder + .begin_compute_pass(&ComputePassDescriptor::default()) + .forget_lifetime(), + ); + } + + ComputeCommandBuilder::new( + self.compute_pass.as_mut().unwrap(), + pass_name, + self.resource_cache, + self.pipeline_compiler, + self.render_device, + ) + } +} diff --git a/crates/bevy_render/src/render_task/plugin.rs b/crates/bevy_render/src/render_task/plugin.rs new file mode 100644 index 0000000000000..5d70fbd83f6cc --- /dev/null +++ b/crates/bevy_render/src/render_task/plugin.rs @@ -0,0 +1,73 @@ +use super::{node::RenderTaskNode, RenderTask}; +use crate::{ + extract_component::ExtractComponentPlugin, + render_graph::{RenderGraphExt, ViewNodeRunner}, + renderer::RenderDevice, + RenderApp, +}; +use bevy_app::{App, Plugin}; +use std::marker::PhantomData; +use tracing::warn; + +/// Plugin to setup a [`RenderTask`]. +/// +/// Make sure to add this to your app: `app.add_plugins(RenderTaskPlugin::::default())`. +#[derive(Default)] +pub struct RenderTaskPlugin(PhantomData); + +impl Plugin for RenderTaskPlugin { + fn build(&self, _app: &mut App) {} + + fn finish(&self, app: &mut App) { + // Get render app + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + let render_device = render_app.world().resource::(); + + // Check features + let features = render_device.features(); + if !features.contains(T::REQUIRED_FEATURES) { + warn!( + "{} not loaded. GPU lacks support for required features: {:?}.", + std::any::type_name::(), + T::REQUIRED_FEATURES.difference(features) + ); + return; + } + + // Check limits + let mut should_exit = false; + let fail_fn = |limit_name, required_limit_value, _| { + warn!( + "{} not loaded. GPU lacks support for required limits: {}={}.", + std::any::type_name::(), + limit_name, + required_limit_value + ); + should_exit = true; + }; + T::REQUIRED_LIMITS.check_limits_with_fail_fn(&render_device.limits(), true, fail_fn); + if should_exit { + return; + } + + // Setup app + app.add_plugins(ExtractComponentPlugin::::default()); + T::plugin_app_build(app); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + // Setup render app + render_app + .add_render_graph_node::>>( + T::RenderNodeSubGraph::default(), + T::render_node_label(), + ) + .add_render_graph_edges(T::RenderNodeSubGraph::default(), T::render_node_ordering()); + + T::plugin_render_app_build(render_app); + } +} diff --git a/crates/bevy_render/src/render_task/resource_cache.rs b/crates/bevy_render/src/render_task/resource_cache.rs new file mode 100644 index 0000000000000..4cf91cbfa223b --- /dev/null +++ b/crates/bevy_render/src/render_task/resource_cache.rs @@ -0,0 +1,89 @@ +use crate::{ + render_resource::{ + BindGroup, Buffer, CachedComputePipelineId, CachedRenderPipelineId, ComputePipeline, + ComputePipelineDescriptor, RenderPipeline, RenderPipelineDescriptor, TextureView, + }, + renderer::RenderDevice, + PipelineCache as PipelineCompiler, +}; +use bevy_ecs::entity::Entity; +use std::collections::HashMap; +use wgpu::{BindGroupDescriptor, BufferDescriptor, TextureDescriptor, TextureViewDescriptor}; + +// TODO: Garbage collect old resources +#[derive(Default)] +pub struct ResourceCache { + textures: HashMap<(Entity, TextureDescriptor<'static>), TextureView>, + buffers: HashMap<(Entity, BufferDescriptor<'static>), Buffer>, + _bind_groups: HashMap, BindGroup>, + compute_pipelines: HashMap, + render_pipelines: HashMap, +} + +impl ResourceCache { + pub fn get_or_create_texture( + &mut self, + descriptor: TextureDescriptor<'static>, + entity: Entity, + render_device: &RenderDevice, + ) -> TextureView { + self.textures + .entry((entity, descriptor.clone())) + .or_insert_with(|| { + render_device + .create_texture(&descriptor) + .create_view(&TextureViewDescriptor::default()) + }) + .clone() + } + + pub fn get_or_create_buffer( + &mut self, + descriptor: BufferDescriptor<'static>, + entity: Entity, + render_device: &RenderDevice, + ) -> Buffer { + self.buffers + .entry((entity, descriptor.clone())) + .or_insert_with(|| render_device.create_buffer(&descriptor)) + .clone() + } + + pub fn get_or_create_bind_group( + &mut self, + _descriptor: BindGroupDescriptor<'static>, + _render_device: &RenderDevice, + ) -> BindGroup { + todo!() + // self.bind_groups + // .entry(descriptor.clone()) + // .or_insert_with(|| render_device.wgpu_device().create_bind_group(&descriptor)) + // .clone() + } + + pub fn get_or_compile_compute_pipeline( + &mut self, + descriptor: ComputePipelineDescriptor, + pipeline_compiler: &PipelineCompiler, + ) -> Option { + let pipeline_id = *self + .compute_pipelines + .entry(descriptor.clone()) + .or_insert_with(|| pipeline_compiler.queue_compute_pipeline(descriptor)); + + pipeline_compiler.get_compute_pipeline(pipeline_id).cloned() + } + + pub fn get_or_compile_render_pipeline( + &mut self, + descriptor: RenderPipelineDescriptor, + pipeline_compiler: &PipelineCompiler, + ) -> Option { + let pipeline_id = *self + .render_pipelines + .entry(descriptor.clone()) + .or_insert_with(|| pipeline_compiler.queue_render_pipeline(descriptor)); + + pipeline_compiler.get_render_pipeline(pipeline_id).cloned() + } +}