From 80b1f8d1661f582801488a834f5c85b1b1c9ff99 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Mon, 11 Nov 2024 19:07:37 -0800 Subject: [PATCH 1/3] Add a bindless mode to `AsBindGroup`. This adds the infrastructure necessary for Bevy to support *bindless resources*. Bindless resources significantly improve rendering performance by reducing `wgpu` and driver overhead considerably. --- Cargo.toml | 11 + assets/shaders/bindless_material.wgsl | 38 ++ .../src/auto_exposure/compensation_curve.rs | 1 + crates/bevy_core_pipeline/src/core_3d/mod.rs | 12 +- crates/bevy_core_pipeline/src/prepass/mod.rs | 14 +- crates/bevy_gizmos/src/lib.rs | 3 +- crates/bevy_pbr/src/extended_material.rs | 7 +- crates/bevy_pbr/src/lib.rs | 6 +- crates/bevy_pbr/src/material.rs | 191 +++++--- crates/bevy_pbr/src/material_bind_groups.rs | 437 ++++++++++++++++++ crates/bevy_pbr/src/prepass/mod.rs | 22 +- crates/bevy_pbr/src/render/light.rs | 15 +- crates/bevy_pbr/src/render/mesh.rs | 89 +++- .../bevy_pbr/src/render/mesh_preprocess.wgsl | 5 +- crates/bevy_pbr/src/render/mesh_types.wgsl | 3 +- .../bevy_render/macros/src/as_bind_group.rs | 133 ++++-- crates/bevy_render/macros/src/lib.rs | 10 +- crates/bevy_render/src/mesh/mod.rs | 3 +- crates/bevy_render/src/render_asset.rs | 18 +- .../src/render_resource/bind_group.rs | 35 +- crates/bevy_render/src/render_resource/mod.rs | 14 +- crates/bevy_render/src/storage.rs | 3 +- .../bevy_render/src/texture/fallback_image.rs | 13 + crates/bevy_render/src/texture/gpu_image.rs | 2 + crates/bevy_sprite/src/mesh2d/material.rs | 11 +- .../src/render/ui_material_pipeline.rs | 3 +- examples/shader/custom_phase_item.rs | 2 +- examples/shader/shader_material_bindless.rs | 63 +++ examples/shader/specialized_mesh_pipeline.rs | 2 +- examples/shader/texture_binding_array.rs | 2 +- 30 files changed, 998 insertions(+), 170 deletions(-) create mode 100644 assets/shaders/bindless_material.wgsl create mode 100644 crates/bevy_pbr/src/material_bind_groups.rs create mode 100644 examples/shader/shader_material_bindless.rs diff --git a/Cargo.toml b/Cargo.toml index 307558e0bc7aa..db51125094467 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3811,6 +3811,17 @@ description = "Shows how to use animation clips to animate UI properties" category = "Animation" wasm = true +[[example]] +name = "shader_material_bindless" +path = "examples/shader/shader_material_bindless.rs" +doc-scrape-examples = true + +[package.metadata.example.shader_material_bindless] +name = "Material - Bindless" +description = "Demonstrates how to make materials that use bindless textures" +category = "Shaders" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/assets/shaders/bindless_material.wgsl b/assets/shaders/bindless_material.wgsl new file mode 100644 index 0000000000000..a8d42de19658a --- /dev/null +++ b/assets/shaders/bindless_material.wgsl @@ -0,0 +1,38 @@ +#import bevy_pbr::forward_io::VertexOutput +#import bevy_pbr::mesh_bindings::mesh + +struct Color { + base_color: vec4, +} + +#ifdef BINDLESS +@group(2) @binding(0) var material_color: binding_array; +@group(2) @binding(1) var material_color_texture: binding_array, 4>; +@group(2) @binding(2) var material_color_sampler: binding_array; +#else // BINDLESS +@group(2) @binding(0) var material_color: Color; +@group(2) @binding(1) var material_color_texture: texture_2d; +@group(2) @binding(2) var material_color_sampler: sampler; +#endif // BINDLESS + +@fragment +fn fragment(in: VertexOutput) -> @location(0) vec4 { +#ifdef BINDLESS + let slot = mesh[in.instance_index].material_bind_group_slot; + let base_color = material_color[slot].base_color; +#else // BINDLESS + let base_color = material_color.base_color; +#endif // BINDLESS + + return base_color * textureSampleLevel( +#ifdef BINDLESS + material_color_texture[slot], + material_color_sampler[slot], +#else // BINDLESS + material_color_texture, + material_color_sampler, +#endif // BINDLESS + in.uv, + 0.0 + ); +} diff --git a/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs b/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs index 3c64152063493..d984b4a836ea8 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs @@ -191,6 +191,7 @@ impl RenderAsset for GpuAutoExposureCompensationCurve { fn prepare_asset( source: Self::SourceAsset, + _: AssetId, (render_device, render_queue): &mut SystemParamItem, ) -> Result> { let texture = render_device.create_texture_with_data( diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 4a616226d150f..0dba6e8e09ff0 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -87,7 +87,7 @@ use bevy_render::{ ViewSortedRenderPhases, }, render_resource::{ - BindGroupId, CachedRenderPipelineId, Extent3d, FilterMode, Sampler, SamplerDescriptor, + CachedRenderPipelineId, Extent3d, FilterMode, Sampler, SamplerDescriptor, Texture, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView, }, renderer::RenderDevice, @@ -233,17 +233,17 @@ pub struct Opaque3dBinKey { /// The function used to draw. pub draw_function: DrawFunctionId, + /// The ID of a bind group specific to the material. + /// + /// In the case of PBR, this is the `MaterialBindGroupIndex`. + pub material_bind_group_index: Option, + /// The asset that this phase item is associated with. /// /// Normally, this is the ID of the mesh, but for non-mesh items it might be /// the ID of another type of asset. pub asset_id: UntypedAssetId, - /// The ID of a bind group specific to the material. - /// - /// In the case of PBR, this is the `MaterialBindGroupId`. - pub material_bind_group_id: Option, - /// The lightmap, if present. pub lightmap_image: Option>, } diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs index f8151a0e163c4..cc056baab0bfc 100644 --- a/crates/bevy_core_pipeline/src/prepass/mod.rs +++ b/crates/bevy_core_pipeline/src/prepass/mod.rs @@ -41,8 +41,8 @@ use bevy_render::{ PhaseItemExtraIndex, }, render_resource::{ - BindGroupId, CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, - Extent3d, ShaderType, TextureFormat, TextureView, + CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d, + ShaderType, TextureFormat, TextureView, }, texture::ColorAttachment, }; @@ -158,13 +158,13 @@ pub struct OpaqueNoLightmap3dBinKey { /// The function used to draw the mesh. pub draw_function: DrawFunctionId, - /// The ID of the asset. - pub asset_id: UntypedAssetId, - /// The ID of a bind group specific to the material. /// - /// In the case of PBR, this is the `MaterialBindGroupId`. - pub material_bind_group_id: Option, + /// In the case of PBR, this is the `MaterialBindGroupIndex`. + pub material_bind_group_index: Option, + + /// The ID of the asset. + pub asset_id: UntypedAssetId, } impl PhaseItem for Opaque3dPrepass { diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 620a3e3700e8c..e83a6a080c423 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -74,7 +74,7 @@ pub mod prelude { } use bevy_app::{App, FixedFirst, FixedLast, Last, Plugin, RunFixedMainLoop}; -use bevy_asset::{Asset, AssetApp, Assets, Handle}; +use bevy_asset::{Asset, AssetApp, AssetId, Assets, Handle}; use bevy_color::LinearRgba; use bevy_ecs::{ schedule::{IntoSystemConfigs, SystemSet}, @@ -520,6 +520,7 @@ impl RenderAsset for GpuLineGizmo { fn prepare_asset( gizmo: Self::SourceAsset, + _: AssetId, render_device: &mut SystemParamItem, ) -> Result> { let position_buffer_data = cast_slice(&gizmo.positions); diff --git a/crates/bevy_pbr/src/extended_material.rs b/crates/bevy_pbr/src/extended_material.rs index cef4784ad228c..ae8f9f710e0d2 100644 --- a/crates/bevy_pbr/src/extended_material.rs +++ b/crates/bevy_pbr/src/extended_material.rs @@ -17,6 +17,7 @@ pub struct MaterialExtensionPipeline { pub material_layout: BindGroupLayout, pub vertex_shader: Option>, pub fragment_shader: Option>, + pub bindless: bool, } pub struct MaterialExtensionKey { @@ -163,7 +164,7 @@ impl AsBindGroup for ExtendedMaterial { let extended_bindgroup = E::unprepared_bind_group(&self.extension, layout, render_device, extended_param)?; - bindings.extend(extended_bindgroup.bindings); + bindings.extend(extended_bindgroup.bindings.0); Ok(UnpreparedBindGroup { bindings, @@ -279,6 +280,7 @@ impl Material for ExtendedMaterial { material_layout, vertex_shader, fragment_shader, + bindless, .. } = pipeline.clone(); let base_pipeline = MaterialPipeline:: { @@ -286,6 +288,7 @@ impl Material for ExtendedMaterial { material_layout, vertex_shader, fragment_shader, + bindless, marker: Default::default(), }; let base_key = MaterialPipelineKey:: { @@ -300,6 +303,7 @@ impl Material for ExtendedMaterial { material_layout, vertex_shader, fragment_shader, + bindless, .. } = pipeline.clone(); @@ -309,6 +313,7 @@ impl Material for ExtendedMaterial { material_layout, vertex_shader, fragment_shader, + bindless, }, descriptor, layout, diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 7526f384e2fe5..3be5d6f2f9a8d 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -34,6 +34,7 @@ mod light; mod light_probe; mod lightmap; mod material; +mod material_bind_groups; mod mesh_material; mod parallax; mod pbr_material; @@ -43,6 +44,8 @@ mod ssao; mod ssr; mod volumetric_fog; +use crate::material_bind_groups::FallbackBindlessResources; + use bevy_color::{Color, LinearRgba}; use core::marker::PhantomData; @@ -474,7 +477,8 @@ impl Plugin for PbrPlugin { // Extract the required data from the main world render_app .init_resource::() - .init_resource::(); + .init_resource::() + .init_resource::(); } } diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 65d935226e7ab..f864d219c37f7 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1,4 +1,5 @@ use self::{irradiance_volume::IrradianceVolume, prelude::EnvironmentMapLight}; +use crate::material_bind_groups::{MaterialBindGroupAllocator, MaterialBindingId}; #[cfg(feature = "meshlet")] use crate::meshlet::{ prepare_material_meshlet_meshes_main_opaque_pass, queue_material_meshlet_meshes, @@ -20,11 +21,13 @@ use bevy_core_pipeline::{ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ prelude::*, - system::{lifetimeless::SRes, SystemParamItem}, + system::{ + lifetimeless::{SRes, SResMut}, + SystemParamItem, + }, }; use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; -use bevy_render::sync_world::MainEntityHashMap; use bevy_render::view::RenderVisibleEntities; use bevy_render::{ camera::TemporalJitter, @@ -37,13 +40,9 @@ use bevy_render::{ view::{ExtractedView, Msaa, RenderVisibilityRanges, ViewVisibility}, Extract, }; -use bevy_utils::tracing::error; -use core::{ - hash::Hash, - marker::PhantomData, - num::NonZero, - sync::atomic::{AtomicU32, Ordering}, -}; +use bevy_render::{sync_world::MainEntityHashMap, texture::FallbackImage}; +use bevy_utils::{hashbrown::hash_map::Entry, tracing::error}; +use core::{hash::Hash, marker::PhantomData}; /// Materials are used alongside [`MaterialPlugin`], [`Mesh3d`], and [`MeshMaterial3d`] /// to spawn entities that are rendered with a specific [`Material`] type. They serve as an easy to use high level @@ -283,12 +282,23 @@ where .add_render_command::>() .add_render_command::>() .init_resource::>>() - .add_systems(ExtractSchedule, extract_mesh_materials::) + .add_systems( + ExtractSchedule, + extract_mesh_materials:: + .before(extract_meshes_for_cpu_building) + .before(extract_meshes_for_gpu_building), + ) .add_systems( Render, queue_material_meshes:: .in_set(RenderSet::QueueMeshes) .after(prepare_assets::>), + ) + .add_systems( + Render, + prepare_material_bind_groups:: + .in_set(RenderSet::PrepareBindGroups) + .after(prepare_assets::>), ); if self.shadows_enabled { @@ -331,7 +341,9 @@ where fn finish(&self, app: &mut App) { if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.init_resource::>(); + render_app + .init_resource::>() + .init_resource::>(); } } } @@ -382,6 +394,7 @@ pub struct MaterialPipeline { pub material_layout: BindGroupLayout, pub vertex_shader: Option>, pub fragment_shader: Option>, + pub bindless: bool, pub marker: PhantomData, } @@ -392,6 +405,7 @@ impl Clone for MaterialPipeline { material_layout: self.material_layout.clone(), vertex_shader: self.vertex_shader.clone(), fragment_shader: self.fragment_shader.clone(), + bindless: self.bindless, marker: PhantomData, } } @@ -420,6 +434,15 @@ where descriptor.layout.insert(2, self.material_layout.clone()); M::specialize(self, &mut descriptor, layout, key)?; + + // Add a `BINDLESS` define. + if self.bindless { + descriptor.vertex.shader_defs.push("BINDLESS".into()); + if let Some(ref mut fragment) = descriptor.fragment { + fragment.shader_defs.push("BINDLESS".into()); + } + } + Ok(descriptor) } } @@ -442,6 +465,7 @@ impl FromWorld for MaterialPipeline { ShaderRef::Handle(handle) => Some(handle), ShaderRef::Path(path) => Some(asset_server.load(path)), }, + bindless: material_bind_groups::material_uses_bindless_resources::(render_device), marker: PhantomData, } } @@ -461,6 +485,7 @@ impl RenderCommand

for SetMaterial type Param = ( SRes>>, SRes>, + SRes>, ); type ViewQuery = (); type ItemQuery = (); @@ -470,11 +495,16 @@ impl RenderCommand

for SetMaterial item: &P, _view: (), _item_query: Option<()>, - (materials, material_instances): SystemParamItem<'w, '_, Self::Param>, + (materials, material_instances, material_bind_group_allocator): SystemParamItem< + 'w, + '_, + Self::Param, + >, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { let materials = materials.into_inner(); let material_instances = material_instances.into_inner(); + let material_bind_group_allocator = material_bind_group_allocator.into_inner(); let Some(material_asset_id) = material_instances.get(&item.main_entity()) else { return RenderCommandResult::Skip; @@ -482,7 +512,14 @@ impl RenderCommand

for SetMaterial let Some(material) = materials.get(*material_asset_id) else { return RenderCommandResult::Skip; }; - pass.set_bind_group(I, &material.bind_group, &[]); + let Some(material_bind_group) = material_bind_group_allocator.get(material.binding.group) + else { + return RenderCommandResult::Skip; + }; + let Some(bind_group) = material_bind_group.get_bind_group() else { + return RenderCommandResult::Skip; + }; + pass.set_bind_group(I, bind_group, &[]); RenderCommandResult::Success } } @@ -549,6 +586,8 @@ pub const fn screen_space_specular_transmission_pipeline_key( fn extract_mesh_materials( mut material_instances: ResMut>, + mut material_ids: ResMut, + mut material_bind_group_allocator: ResMut>, query: Extract)>>, ) { material_instances.clear(); @@ -556,6 +595,14 @@ fn extract_mesh_materials( for (entity, view_visibility, material) in &query { if view_visibility.get() { material_instances.insert(entity.into(), material.id()); + + let material_id = material.id().untyped(); + material_ids + .mesh_to_material + .insert(entity.into(), material_id); + if let Entry::Vacant(entry) = material_ids.material_to_binding.entry(material_id) { + entry.insert(material_bind_group_allocator.allocate()); + } } } } @@ -584,6 +631,7 @@ pub fn queue_material_meshes( render_material_instances: Res>, render_lightmaps: Res, render_visibility_ranges: Res, + material_bind_group_allocator: Res>, mut opaque_render_phases: ResMut>, mut alpha_mask_render_phases: ResMut>, mut transmissive_render_phases: ResMut>, @@ -739,6 +787,11 @@ pub fn queue_material_meshes( let Some(material) = render_materials.get(*material_asset_id) else { continue; }; + let Some(material_bind_group) = + material_bind_group_allocator.get(material.binding.group) + else { + continue; + }; let mut mesh_pipeline_key_bits = material.properties.mesh_pipeline_key_bits; mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key( @@ -782,7 +835,9 @@ pub fn queue_material_meshes( &material_pipeline, MaterialPipelineKey { mesh_key, - bind_group_data: material.key.clone(), + bind_group_data: material_bind_group + .get_extra_data(material.binding.slot) + .clone(), }, &mesh.layout, ); @@ -794,10 +849,6 @@ pub fn queue_material_meshes( } }; - mesh_instance - .material_bind_group_id - .set(material.get_bind_group_id()); - match mesh_key .intersection(MeshPipelineKey::BLEND_RESERVED_BITS | MeshPipelineKey::MAY_DISCARD) { @@ -818,7 +869,7 @@ pub fn queue_material_meshes( draw_function: draw_opaque_pbr, pipeline: pipeline_id, asset_id: mesh_instance.mesh_asset_id.into(), - material_bind_group_id: material.get_bind_group_id().0, + material_bind_group_index: Some(material.binding.group.0), lightmap_image, }; opaque_phase.add( @@ -846,7 +897,7 @@ pub fn queue_material_meshes( draw_function: draw_alpha_mask_pbr, pipeline: pipeline_id, asset_id: mesh_instance.mesh_asset_id.into(), - material_bind_group_id: material.get_bind_group_id().0, + material_bind_group_index: Some(material.binding.group.0), }; alpha_mask_phase.add( bin_key, @@ -945,11 +996,10 @@ pub struct MaterialProperties { } /// Data prepared for a [`Material`] instance. -pub struct PreparedMaterial { - pub bindings: Vec<(u32, OwnedBindingResource)>, - pub bind_group: BindGroup, - pub key: T::Data, +pub struct PreparedMaterial { + pub binding: MaterialBindingId, pub properties: MaterialProperties, + pub phantom: PhantomData, } impl RenderAsset for PreparedMaterial { @@ -959,15 +1009,36 @@ impl RenderAsset for PreparedMaterial { SRes, SRes>, SRes, + SRes, + SResMut>, M::Param, ); fn prepare_asset( material: Self::SourceAsset, - (render_device, pipeline, default_opaque_render_method, ref mut material_param): &mut SystemParamItem, + material_id: AssetId, + ( + render_device, + pipeline, + default_opaque_render_method, + mesh_material_ids, + ref mut bind_group_allocator, + ref mut material_param, + ): &mut SystemParamItem, ) -> Result> { - match material.as_bind_group(&pipeline.material_layout, render_device, material_param) { - Ok(prepared) => { + let Some(material_binding_id) = mesh_material_ids + .material_to_binding + .get(&material_id.untyped()) + else { + return Err(PrepareAssetError::RetryNextUpdate(material)); + }; + + match material.unprepared_bind_group( + &pipeline.material_layout, + render_device, + material_param, + ) { + Ok(unprepared) => { let method = match material.opaque_render_method() { OpaqueRendererMethod::Forward => OpaqueRendererMethod::Forward, OpaqueRendererMethod::Deferred => OpaqueRendererMethod::Deferred, @@ -979,10 +1050,10 @@ impl RenderAsset for PreparedMaterial { material.reads_view_transmission_texture(), ); + bind_group_allocator.init(*material_binding_id, unprepared); + Ok(PreparedMaterial { - bindings: prepared.bindings, - bind_group: prepared.bind_group, - key: prepared.data, + binding: *material_binding_id, properties: MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), @@ -991,6 +1062,7 @@ impl RenderAsset for PreparedMaterial { render_method: method, mesh_pipeline_key_bits, }, + phantom: PhantomData, }) } Err(AsBindGroupError::RetryNextUpdate) => { @@ -999,6 +1071,22 @@ impl RenderAsset for PreparedMaterial { Err(other) => Err(PrepareAssetError::AsBindGroupError(other)), } } + + fn finalize_asset( + asset_id: AssetId, + (_, _, _, mesh_material_ids, ref mut bind_group_allocator, _): &mut SystemParamItem< + Self::Param, + >, + ) { + let Some(material_binding_id) = mesh_material_ids + .material_to_binding + .get(&asset_id.untyped()) + else { + return; + }; + + bind_group_allocator.free(*material_binding_id); + } } #[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)] @@ -1016,38 +1104,13 @@ impl From for MaterialBindGroupId { } } -/// An atomic version of [`MaterialBindGroupId`] that can be read from and written to -/// safely from multiple threads. -#[derive(Default)] -pub struct AtomicMaterialBindGroupId(AtomicU32); - -impl AtomicMaterialBindGroupId { - /// Stores a value atomically. Uses [`Ordering::Relaxed`] so there is zero guarantee of ordering - /// relative to other operations. - /// - /// See also: [`AtomicU32::store`]. - pub fn set(&self, id: MaterialBindGroupId) { - let id = if let Some(id) = id.0 { - NonZero::::from(id).get() - } else { - 0 - }; - self.0.store(id, Ordering::Relaxed); - } - - /// Loads a value atomically. Uses [`Ordering::Relaxed`] so there is zero guarantee of ordering - /// relative to other operations. - /// - /// See also: [`AtomicU32::load`]. - pub fn get(&self) -> MaterialBindGroupId { - MaterialBindGroupId( - NonZero::::new(self.0.load(Ordering::Relaxed)).map(BindGroupId::from), - ) - } -} - -impl PreparedMaterial { - pub fn get_bind_group_id(&self) -> MaterialBindGroupId { - MaterialBindGroupId(Some(self.bind_group.id())) - } +pub fn prepare_material_bind_groups( + mut allocator: ResMut>, + render_device: Res, + fallback_image: Res, + fallback_resources: Res, +) where + M: Material, +{ + allocator.prepare_bind_groups(&render_device, &fallback_image, &fallback_resources); } diff --git a/crates/bevy_pbr/src/material_bind_groups.rs b/crates/bevy_pbr/src/material_bind_groups.rs new file mode 100644 index 0000000000000..ce0f1df8393eb --- /dev/null +++ b/crates/bevy_pbr/src/material_bind_groups.rs @@ -0,0 +1,437 @@ +//! Material bind group management. + +use crate::Material; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + system::Resource, + world::{FromWorld, World}, +}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + render_resource::{ + BindGroup, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry, BindingResource, + BindingType, Buffer, BufferBinding, BufferInitDescriptor, BufferUsages, + OwnedBindingResource, Sampler, SamplerDescriptor, TextureViewDimension, + UnpreparedBindGroup, WgpuSampler, WgpuTextureView, + }, + renderer::RenderDevice, + settings::WgpuFeatures, + texture::FallbackImage, +}; +use bevy_utils::{default, tracing::error, HashMap}; +use core::iter; +use core::marker::PhantomData; +use core::num::NonZero; + +/// An object that creates and stores bind groups for materials. +/// +/// This object groups bindless materials together as appropriate. +#[derive(Resource)] +pub struct MaterialBindGroupAllocator +where + M: Material, +{ + pub bind_groups: Vec>, + free_bind_groups: Vec, + bind_group_layout: BindGroupLayout, + fallback_buffers: MaterialFallbackBuffers, + bindless_enabled: bool, + phantom: PhantomData, +} + +pub struct MaterialBindGroup +where + M: Material, +{ + pub bind_group: Option, + unprepared_bind_groups: Vec>>, + used_slot_bitmap: u32, +} + +#[derive(Clone, Copy, Debug, Default, Reflect)] +pub struct MaterialBindingId { + pub group: MaterialBindGroupIndex, + pub slot: MaterialBindGroupSlot, +} + +#[derive(Clone, Copy, Debug, Default, Reflect, PartialEq, Deref, DerefMut)] +#[reflect(Default)] +pub struct MaterialBindGroupIndex(pub u32); + +impl From for MaterialBindGroupIndex { + fn from(value: u32) -> Self { + MaterialBindGroupIndex(value) + } +} + +#[derive(Clone, Copy, Debug, Default, Reflect, Deref, DerefMut)] +#[reflect(Default)] +pub struct MaterialBindGroupSlot(pub u32); + +impl From for MaterialBindGroupSlot { + fn from(value: u32) -> Self { + MaterialBindGroupSlot(value) + } +} + +enum BindingResourceArray<'a> { + Buffers(Vec>), + TextureViews(TextureViewDimension, Vec<&'a WgpuTextureView>), + Samplers(Vec<&'a WgpuSampler>), +} + +#[derive(Resource)] +pub struct FallbackBindlessResources { + fallback_sampler: Sampler, +} + +struct MaterialFallbackBuffers(HashMap); + +const MIN_BUFFER_SIZE: u64 = 16; + +impl MaterialBindGroupAllocator +where + M: Material, +{ + pub(crate) fn prepare_bind_groups( + &mut self, + render_device: &RenderDevice, + fallback_image: &FallbackImage, + fallback_resources: &FallbackBindlessResources, + ) { + for bind_group in &mut self.bind_groups { + bind_group.rebuild_bind_group_if_necessary( + render_device, + &self.bind_group_layout, + fallback_image, + fallback_resources, + &self.fallback_buffers, + self.bindless_enabled, + ); + } + } + + #[inline] + pub(crate) fn get(&self, index: MaterialBindGroupIndex) -> Option<&MaterialBindGroup> { + self.bind_groups.get(index.0 as usize) + } + + pub(crate) fn allocate(&mut self) -> MaterialBindingId { + let group_index = self.free_bind_groups.pop().unwrap_or_else(|| { + let group_index = self.bind_groups.len() as u32; + self.bind_groups + .push(MaterialBindGroup::new(self.bindless_enabled)); + group_index + }); + + let bind_group = &mut self.bind_groups[group_index as usize]; + let slot_index = bind_group.allocate(); + + if !bind_group.is_full() { + self.free_bind_groups.push(group_index); + } + + MaterialBindingId { + group: group_index.into(), + slot: slot_index, + } + } + + pub(crate) fn init( + &mut self, + material_binding_id: MaterialBindingId, + unprepared_bind_group: UnpreparedBindGroup, + ) { + self.bind_groups[material_binding_id.group.0 as usize] + .init(material_binding_id.slot, unprepared_bind_group); + } + + pub(crate) fn free(&mut self, material_binding_id: MaterialBindingId) { + let bind_group = &mut self.bind_groups[material_binding_id.group.0 as usize]; + let was_full = bind_group.is_full(); + + bind_group.free(material_binding_id.slot); + + if was_full { + debug_assert!(!self.free_bind_groups.contains(&material_binding_id.group.0)); + self.free_bind_groups.push(*material_binding_id.group); + } + } +} + +impl MaterialBindGroup +where + M: Material, +{ + fn new(bindless_enabled: bool) -> MaterialBindGroup { + let count = if !bindless_enabled { + 1 + } else { + M::BINDLESS_SLOT_COUNT.unwrap_or(1) + }; + + MaterialBindGroup { + bind_group: None, + unprepared_bind_groups: iter::repeat_with(|| None).take(count as usize).collect(), + used_slot_bitmap: 0, + } + } + + fn allocate(&mut self) -> MaterialBindGroupSlot { + debug_assert!(!self.is_full()); + + let slot = self.used_slot_bitmap.trailing_ones(); + self.used_slot_bitmap |= 1 << slot; + + slot.into() + } + + fn init( + &mut self, + slot: MaterialBindGroupSlot, + unprepared_bind_group: UnpreparedBindGroup, + ) { + self.unprepared_bind_groups[slot.0 as usize] = Some(unprepared_bind_group); + + // Invalidate cache. + self.bind_group = None; + } + + fn free(&mut self, slot: MaterialBindGroupSlot) { + self.unprepared_bind_groups[slot.0 as usize] = None; + self.used_slot_bitmap &= !(1 << slot.0); + + // Invalidate cache. + self.bind_group = None; + } + + fn is_full(&self) -> bool { + self.used_slot_bitmap == (1 << (self.unprepared_bind_groups.len() as u32)) - 1 + } + + pub fn get_bind_group(&self) -> Option<&BindGroup> { + self.bind_group.as_ref() + } + + fn rebuild_bind_group_if_necessary( + &mut self, + render_device: &RenderDevice, + bind_group_layout: &BindGroupLayout, + fallback_image: &FallbackImage, + fallback_bindless_resources: &FallbackBindlessResources, + fallback_buffers: &MaterialFallbackBuffers, + bindless_enabled: bool, + ) { + if self.bind_group.is_some() { + return; + } + + let Some(first_bind_group) = self + .unprepared_bind_groups + .iter() + .find_map(|slot| slot.as_ref()) + else { + return; + }; + + if !bindless_enabled { + let entries = first_bind_group + .bindings + .iter() + .map(|(index, binding)| BindGroupEntry { + binding: *index, + resource: binding.get_binding(), + }) + .collect::>(); + + self.bind_group = + Some(render_device.create_bind_group(M::label(), bind_group_layout, &entries)); + return; + } + + let mut binding_resource_arrays = first_bind_group + .bindings + .iter() + .map(|(index, binding)| match *binding { + OwnedBindingResource::Buffer(..) => (index, BindingResourceArray::Buffers(vec![])), + OwnedBindingResource::TextureView(dimension, _) => { + (index, BindingResourceArray::TextureViews(dimension, vec![])) + } + OwnedBindingResource::Sampler(..) => { + (index, BindingResourceArray::Samplers(vec![])) + } + }) + .collect::>(); + + for maybe_unprepared_bind_group in self.unprepared_bind_groups.iter() { + match *maybe_unprepared_bind_group { + None => { + for binding_resource_array in &mut binding_resource_arrays { + match *binding_resource_array { + (binding, BindingResourceArray::Buffers(ref mut vec)) => { + vec.push(BufferBinding { + buffer: &fallback_buffers.0[binding], + offset: 0, + size: None, + }); + } + ( + _, + BindingResourceArray::TextureViews(texture_dimension, ref mut vec), + ) => vec.push(&fallback_image.get(texture_dimension).texture_view), + (_, BindingResourceArray::Samplers(ref mut vec)) => { + vec.push(&fallback_bindless_resources.fallback_sampler); + } + } + } + } + + Some(ref unprepared_bind_group) => { + for (&mut (binding, ref mut binding_resource_array), (_, binding_resource)) in + binding_resource_arrays + .iter_mut() + .zip(unprepared_bind_group.bindings.0.iter()) + { + match (binding_resource_array, binding_resource) { + ( + &mut BindingResourceArray::Buffers(ref mut vec), + OwnedBindingResource::Buffer(buffer), + ) => match NonZero::new(buffer.size()) { + None => vec.push(BufferBinding { + buffer: &fallback_buffers.0[binding], + offset: 0, + size: None, + }), + Some(size) => vec.push(BufferBinding { + buffer, + offset: 0, + size: Some(size), + }), + }, + ( + &mut BindingResourceArray::TextureViews(_, ref mut vec), + OwnedBindingResource::TextureView(_, texture_view), + ) => vec.push(texture_view), + ( + &mut BindingResourceArray::Samplers(ref mut vec), + OwnedBindingResource::Sampler(sampler), + ) => vec.push(sampler), + _ => { + error!( + "Mismatched bind group layouts; can't combine bind groups \ + into a single bindless bind group!" + ); + return; + } + } + } + } + } + } + + let entries = binding_resource_arrays + .iter() + .map(|&(&binding, ref binding_resource_array)| BindGroupEntry { + binding, + resource: match *binding_resource_array { + BindingResourceArray::Buffers(ref vec) => { + BindingResource::BufferArray(&vec[..]) + } + BindingResourceArray::TextureViews(_, ref vec) => { + BindingResource::TextureViewArray(&vec[..]) + } + BindingResourceArray::Samplers(ref vec) => { + BindingResource::SamplerArray(&vec[..]) + } + }, + }) + .collect::>(); + + self.bind_group = + Some(render_device.create_bind_group(M::label(), bind_group_layout, &entries)); + } + + pub fn get_extra_data(&self, slot: MaterialBindGroupSlot) -> &M::Data { + &self.unprepared_bind_groups[slot.0 as usize] + .as_ref() + .unwrap() + .data + } +} + +impl FromWorld for MaterialBindGroupAllocator +where + M: Material, +{ + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let bind_group_layout_entries = M::bind_group_layout_entries(render_device); + let bind_group_layout = + render_device.create_bind_group_layout(M::label(), &bind_group_layout_entries); + let fallback_buffers = + MaterialFallbackBuffers::new(render_device, &bind_group_layout_entries); + MaterialBindGroupAllocator { + bind_groups: vec![], + free_bind_groups: vec![], + bind_group_layout, + fallback_buffers, + bindless_enabled: material_uses_bindless_resources::(render_device), + phantom: PhantomData, + } + } +} + +pub fn material_uses_bindless_resources(render_device: &RenderDevice) -> bool +where + M: Material, +{ + M::BINDLESS_SLOT_COUNT.is_some() + && render_device + .features() + .contains(WgpuFeatures::BUFFER_BINDING_ARRAY | WgpuFeatures::TEXTURE_BINDING_ARRAY) +} + +impl FromWorld for FallbackBindlessResources { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + FallbackBindlessResources { + fallback_sampler: render_device.create_sampler(&SamplerDescriptor { + label: Some("fallback sampler"), + ..default() + }), + } + } +} + +impl MaterialFallbackBuffers { + fn new( + render_device: &RenderDevice, + bind_group_layout_entries: &[BindGroupLayoutEntry], + ) -> MaterialFallbackBuffers { + let mut fallback_buffers = HashMap::new(); + for bind_group_layout_entry in bind_group_layout_entries { + let BindingType::Buffer { + min_binding_size, .. + } = bind_group_layout_entry.ty + else { + continue; + }; + let mut size: u64 = match min_binding_size { + None => 0, + Some(min_binding_size) => min_binding_size.into(), + }; + size = size.max(MIN_BUFFER_SIZE); + + fallback_buffers.insert( + bind_group_layout_entry.binding, + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("fallback buffer"), + contents: &vec![0; size as usize], + usage: BufferUsages::UNIFORM | BufferUsages::STORAGE, + }), + ); + } + + MaterialFallbackBuffers(fallback_buffers) + } +} diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 8bd10f9678984..b26a43b32290f 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -1,5 +1,6 @@ mod prepass_bindings; +use crate::material_bind_groups::MaterialBindGroupAllocator; use bevy_render::{ mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, render_resource::binding_types::uniform_buffer, @@ -696,6 +697,7 @@ pub fn queue_prepass_material_meshes( render_materials: Res>>, render_material_instances: Res>, render_lightmaps: Res, + material_bind_group_allocator: Res>, mut opaque_prepass_render_phases: ResMut>, mut alpha_mask_prepass_render_phases: ResMut>, mut opaque_deferred_render_phases: ResMut>, @@ -784,6 +786,11 @@ pub fn queue_prepass_material_meshes( let Some(material) = render_materials.get(*material_asset_id) else { continue; }; + let Some(material_bind_group) = + material_bind_group_allocator.get(material.binding.group) + else { + continue; + }; let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; @@ -852,7 +859,9 @@ pub fn queue_prepass_material_meshes( &prepass_pipeline, MaterialPipelineKey { mesh_key, - bind_group_data: material.key.clone(), + bind_group_data: material_bind_group + .get_extra_data(material.binding.slot) + .clone(), }, &mesh.layout, ); @@ -864,9 +873,6 @@ pub fn queue_prepass_material_meshes( } }; - mesh_instance - .material_bind_group_id - .set(material.get_bind_group_id()); match mesh_key .intersection(MeshPipelineKey::BLEND_RESERVED_BITS | MeshPipelineKey::MAY_DISCARD) { @@ -877,7 +883,7 @@ pub fn queue_prepass_material_meshes( draw_function: opaque_draw_deferred, pipeline: pipeline_id, asset_id: mesh_instance.mesh_asset_id.into(), - material_bind_group_id: material.get_bind_group_id().0, + material_bind_group_index: Some(material.binding.group.0), }, (*render_entity, *visible_entity), BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), @@ -888,7 +894,7 @@ pub fn queue_prepass_material_meshes( draw_function: opaque_draw_prepass, pipeline: pipeline_id, asset_id: mesh_instance.mesh_asset_id.into(), - material_bind_group_id: material.get_bind_group_id().0, + material_bind_group_index: Some(material.binding.group.0), }, (*render_entity, *visible_entity), BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), @@ -902,7 +908,7 @@ pub fn queue_prepass_material_meshes( pipeline: pipeline_id, draw_function: alpha_mask_draw_deferred, asset_id: mesh_instance.mesh_asset_id.into(), - material_bind_group_id: material.get_bind_group_id().0, + material_bind_group_index: Some(material.binding.group.0), }; alpha_mask_deferred_phase.as_mut().unwrap().add( bin_key, @@ -914,7 +920,7 @@ pub fn queue_prepass_material_meshes( pipeline: pipeline_id, draw_function: alpha_mask_draw_prepass, asset_id: mesh_instance.mesh_asset_id.into(), - material_bind_group_id: material.get_bind_group_id().0, + material_bind_group_index: Some(material.binding.group.0), }; alpha_mask_phase.add( bin_key, diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index c6ca3d0fb7991..2341ace341f91 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,3 +1,4 @@ +use crate::material_bind_groups::MaterialBindGroupAllocator; use crate::*; use bevy_asset::UntypedAssetId; use bevy_color::ColorToComponents; @@ -1495,6 +1496,7 @@ pub fn queue_shadows( render_mesh_instances: Res, render_materials: Res>>, render_material_instances: Res>, + material_bind_group_allocator: Res>, mut shadow_render_phases: ResMut>, mut pipelines: ResMut>>, pipeline_cache: Res, @@ -1567,6 +1569,11 @@ pub fn queue_shadows( let Some(material) = render_materials.get(*material_asset_id) else { continue; }; + let Some(material_bind_group) = + material_bind_group_allocator.get(material.binding.group) + else { + continue; + }; let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; @@ -1596,7 +1603,9 @@ pub fn queue_shadows( &prepass_pipeline, MaterialPipelineKey { mesh_key, - bind_group_data: material.key.clone(), + bind_group_data: material_bind_group + .get_extra_data(material.binding.slot) + .clone(), }, &mesh.layout, ); @@ -1609,10 +1618,6 @@ pub fn queue_shadows( } }; - mesh_instance - .material_bind_group_id - .set(material.get_bind_group_id()); - shadow_phase.add( ShadowBinKey { draw_function: draw_shadow_mesh, diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 06a1de09e2a6b..caac54549ac6c 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1,7 +1,8 @@ use core::mem::{self, size_of}; +use crate::material_bind_groups::{MaterialBindGroupIndex, MaterialBindGroupSlot}; use allocator::MeshAllocator; -use bevy_asset::{load_internal_asset, AssetId}; +use bevy_asset::{load_internal_asset, AssetId, UntypedAssetId}; use bevy_core_pipeline::{ core_3d::{AlphaMask3d, Opaque3d, Transmissive3d, Transparent3d, CORE_3D_DEPTH_FORMAT}, deferred::{AlphaMask3dDeferred, Opaque3dDeferred}, @@ -26,7 +27,7 @@ use bevy_render::{ camera::Camera, mesh::*, primitives::Aabb, - render_asset::RenderAssets, + render_asset::{ExtractAssetsSet, RenderAssets}, render_phase::{ BinnedRenderPhasePlugin, PhaseItem, RenderCommand, RenderCommandResult, SortedRenderPhasePlugin, TrackedRenderPass, @@ -45,6 +46,7 @@ use bevy_utils::{ tracing::{error, warn}, Entry, HashMap, Parallel, }; +use material_bind_groups::MaterialBindingId; use crate::{ render::{ @@ -153,6 +155,7 @@ impl Plugin for MeshRenderPlugin { .init_resource::() .init_resource::() .init_resource::() + .init_resource::() .add_systems( ExtractSchedule, ( @@ -200,7 +203,9 @@ impl Plugin for MeshRenderPlugin { .init_resource::() .add_systems( ExtractSchedule, - extract_meshes_for_gpu_building.in_set(ExtractMeshesSet), + extract_meshes_for_gpu_building + .in_set(ExtractMeshesSet) + .after(ExtractAssetsSet), ) .add_systems( Render, @@ -227,7 +232,9 @@ impl Plugin for MeshRenderPlugin { .insert_resource(cpu_batched_instance_buffer) .add_systems( ExtractSchedule, - extract_meshes_for_cpu_building.in_set(ExtractMeshesSet), + extract_meshes_for_cpu_building + .in_set(ExtractMeshesSet) + .after(ExtractAssetsSet), ) .add_systems( Render, @@ -298,12 +305,12 @@ pub struct MeshUniform { /// [`MeshAllocator`]). This value stores the offset of the first vertex in /// this mesh in that buffer. pub first_vertex_index: u32, + /// Index of the material inside the bind group data. + pub material_bind_group_slot: u32, /// Padding. pub pad_a: u32, /// Padding. pub pad_b: u32, - /// Padding. - pub pad_c: u32, } /// Information that has to be transferred from CPU to GPU in order to produce @@ -340,12 +347,12 @@ pub struct MeshInputUniform { /// [`MeshAllocator`]). This value stores the offset of the first vertex in /// this mesh in that buffer. pub first_vertex_index: u32, + /// Index of the material inside the bind group data. + pub material_bind_group_slot: u32, /// Padding. pub pad_a: u32, /// Padding. pub pad_b: u32, - /// Padding. - pub pad_c: u32, } /// Information about each mesh instance needed to cull it on GPU. @@ -375,6 +382,7 @@ impl MeshUniform { pub fn new( mesh_transforms: &MeshTransforms, first_vertex_index: u32, + material_bind_group_slot: MaterialBindGroupSlot, maybe_lightmap_uv_rect: Option, ) -> Self { let (local_from_world_transpose_a, local_from_world_transpose_b) = @@ -387,9 +395,9 @@ impl MeshUniform { local_from_world_transpose_b, flags: mesh_transforms.flags, first_vertex_index, + material_bind_group_slot: *material_bind_group_slot, pad_a: 0, pad_b: 0, - pad_c: 0, } } } @@ -505,10 +513,8 @@ pub struct RenderMeshInstanceGpu { pub struct RenderMeshInstanceShared { /// The [`AssetId`] of the mesh. pub mesh_asset_id: AssetId, - /// A slot for the material bind group ID. - /// - /// This is filled in during [`crate::material::queue_material_meshes`]. - pub material_bind_group_id: AtomicMaterialBindGroupId, + /// A slot for the material bind group index. + pub material_bindings_index: MaterialBindingId, /// Various flags. pub flags: RenderMeshInstanceFlags, } @@ -576,6 +582,7 @@ impl RenderMeshInstanceShared { fn from_components( previous_transform: Option<&PreviousGlobalTransform>, mesh: &Mesh3d, + material_bindings_index: MaterialBindingId, not_shadow_caster: bool, no_automatic_batching: bool, ) -> Self { @@ -593,7 +600,7 @@ impl RenderMeshInstanceShared { RenderMeshInstanceShared { mesh_asset_id: mesh.id(), flags: mesh_instance_flags, - material_bind_group_id: AtomicMaterialBindGroupId::default(), + material_bindings_index, } } @@ -603,7 +610,6 @@ impl RenderMeshInstanceShared { pub fn should_batch(&self) -> bool { self.flags .contains(RenderMeshInstanceFlags::AUTOMATIC_BATCHING) - && self.material_bind_group_id.get().is_some() } } @@ -630,6 +636,13 @@ pub struct RenderMeshInstancesCpu(MainEntityHashMap); #[derive(Default, Deref, DerefMut)] pub struct RenderMeshInstancesGpu(MainEntityHashMap); +/// FIXME: The fact that we need this is ugly. +#[derive(Resource, Default)] +pub struct RenderMeshMaterialIds { + pub(crate) mesh_to_material: MainEntityHashMap, + pub(crate) material_to_binding: HashMap, +} + impl RenderMeshInstances { /// Creates a new [`RenderMeshInstances`] instance. fn new(use_gpu_instance_buffer_builder: bool) -> RenderMeshInstances { @@ -793,9 +806,9 @@ impl RenderMeshInstanceGpuBuilder { None => u32::MAX, }, first_vertex_index, + material_bind_group_slot: *self.shared.material_bindings_index.slot, pad_a: 0, pad_b: 0, - pad_c: 0, }); // Record the [`RenderMeshInstance`]. @@ -870,6 +883,7 @@ pub struct ExtractMeshesSet; pub fn extract_meshes_for_cpu_building( mut render_mesh_instances: ResMut, render_visibility_ranges: Res, + mesh_material_ids: Res, mut render_mesh_instance_queues: Local>>, meshes_query: Extract< Query<( @@ -905,6 +919,19 @@ pub fn extract_meshes_for_cpu_building( return; } + let Some(mesh_material_asset_id) = mesh_material_ids + .mesh_to_material + .get(&MainEntity::from(entity)) + else { + return; + }; + let Some(mesh_material_binding_id) = mesh_material_ids + .material_to_binding + .get(mesh_material_asset_id) + else { + return; + }; + let mut lod_index = None; if visibility_range { lod_index = render_visibility_ranges.lod_index_for_entity(entity.into()); @@ -920,6 +947,7 @@ pub fn extract_meshes_for_cpu_building( let shared = RenderMeshInstanceShared::from_components( previous_transform, mesh, + *mesh_material_binding_id, not_shadow_caster, no_automatic_batching, ); @@ -967,6 +995,7 @@ pub fn extract_meshes_for_cpu_building( pub fn extract_meshes_for_gpu_building( mut render_mesh_instances: ResMut, render_visibility_ranges: Res, + mesh_material_ids: Res, mut render_mesh_instance_queues: ResMut, meshes_query: Extract< Query<( @@ -1021,6 +1050,19 @@ pub fn extract_meshes_for_gpu_building( return; } + let Some(mesh_material_asset_id) = mesh_material_ids + .mesh_to_material + .get(&MainEntity::from(entity)) + else { + return; + }; + let Some(mesh_material_binding_id) = mesh_material_ids + .material_to_binding + .get(mesh_material_asset_id) + else { + return; + }; + let mut lod_index = None; if visibility_range { lod_index = render_visibility_ranges.lod_index_for_entity(entity.into()); @@ -1036,6 +1078,7 @@ pub fn extract_meshes_for_gpu_building( let shared = RenderMeshInstanceShared::from_components( previous_transform, mesh, + *mesh_material_binding_id, not_shadow_caster, no_automatic_batching, ); @@ -1283,7 +1326,11 @@ impl GetBatchData for MeshPipeline { ); // The material bind group ID, the mesh ID, and the lightmap ID, // respectively. - type CompareData = (MaterialBindGroupId, AssetId, Option>); + type CompareData = ( + MaterialBindGroupIndex, + AssetId, + Option>, + ); type BufferData = MeshUniform; @@ -1306,14 +1353,17 @@ impl GetBatchData for MeshPipeline { }; let maybe_lightmap = lightmaps.render_lightmaps.get(&main_entity); + let material_bind_group_index = mesh_instance.material_bindings_index; + Some(( MeshUniform::new( &mesh_instance.transforms, first_vertex_index, + material_bind_group_index.slot, maybe_lightmap.map(|lightmap| lightmap.uv_rect), ), mesh_instance.should_batch().then_some(( - mesh_instance.material_bind_group_id.get(), + material_bind_group_index.group, mesh_instance.mesh_asset_id, maybe_lightmap.map(|lightmap| lightmap.image), )), @@ -1343,7 +1393,7 @@ impl GetFullBatchData for MeshPipeline { Some(( mesh_instance.current_uniform_index, mesh_instance.should_batch().then_some(( - mesh_instance.material_bind_group_id.get(), + mesh_instance.material_bindings_index.group, mesh_instance.mesh_asset_id, maybe_lightmap.map(|lightmap| lightmap.image), )), @@ -1371,6 +1421,7 @@ impl GetFullBatchData for MeshPipeline { Some(MeshUniform::new( &mesh_instance.transforms, first_vertex_index, + mesh_instance.material_bindings_index.slot, maybe_lightmap.map(|lightmap| lightmap.uv_rect), )) } diff --git a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl index 6a5a1fcf06e33..5132691930cad 100644 --- a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl +++ b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl @@ -23,9 +23,10 @@ struct MeshInput { // applicable. If not present, this is `u32::MAX`. previous_input_index: u32, first_vertex_index: u32, + // Index of the material inside the bind group data. + material_bind_group_slot: u32, pad_a: u32, pad_b: u32, - pad_c: u32, } // Information about each mesh instance needed to cull it on GPU. @@ -191,4 +192,6 @@ fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { output[mesh_output_index].flags = current_input[input_index].flags; output[mesh_output_index].lightmap_uv_rect = current_input[input_index].lightmap_uv_rect; output[mesh_output_index].first_vertex_index = current_input[input_index].first_vertex_index; + output[mesh_output_index].material_bind_group_slot = + current_input[input_index].material_bind_group_slot; } diff --git a/crates/bevy_pbr/src/render/mesh_types.wgsl b/crates/bevy_pbr/src/render/mesh_types.wgsl index 57576a3bb3805..7cf8cdf7ed512 100644 --- a/crates/bevy_pbr/src/render/mesh_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_types.wgsl @@ -17,9 +17,10 @@ struct Mesh { lightmap_uv_rect: vec2, // The index of the mesh's first vertex in the vertex buffer. first_vertex_index: u32, + // Index of the material inside the bind group data. + material_bind_group_slot: u32, pad_a: u32, pad_b: u32, - pad_c: u32, }; #ifdef SKINNED diff --git a/crates/bevy_render/macros/src/as_bind_group.rs b/crates/bevy_render/macros/src/as_bind_group.rs index 8f871bd0e00c6..6a16fd12b3d94 100644 --- a/crates/bevy_render/macros/src/as_bind_group.rs +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -6,7 +6,7 @@ use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, token::Comma, - Data, DataStruct, Error, Fields, LitInt, LitStr, Meta, MetaList, Result, + Data, DataStruct, Error, Fields, Lit, LitInt, LitStr, Meta, MetaList, Result, }; const UNIFORM_ATTRIBUTE_NAME: Symbol = Symbol("uniform"); @@ -15,6 +15,7 @@ const STORAGE_TEXTURE_ATTRIBUTE_NAME: Symbol = Symbol("storage_texture"); const SAMPLER_ATTRIBUTE_NAME: Symbol = Symbol("sampler"); const STORAGE_ATTRIBUTE_NAME: Symbol = Symbol("storage"); const BIND_GROUP_DATA_ATTRIBUTE_NAME: Symbol = Symbol("bind_group_data"); +const BINDLESS_ATTRIBUTE_NAME: Symbol = Symbol("bindless"); #[derive(Copy, Clone, Debug)] enum BindingType { @@ -49,6 +50,9 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { let mut binding_impls = Vec::new(); let mut binding_layouts = Vec::new(); let mut attr_prepared_data_ident = None; + let mut attr_bindless_count = None; + + let actual_bindless_slot_count = Ident::new("actual_bindless_slot_count", Span::call_site()); // Read struct-level attributes for attr in &ast.attrs { @@ -87,7 +91,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { has_dynamic_offset: false, min_binding_size: Some(<#converted_shader_type as #render_path::render_resource::ShaderType>::min_size()), }, - count: None, + count: #actual_bindless_slot_count, } }); @@ -96,6 +100,12 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { binding_states.resize(required_len, BindingState::Free); } binding_states[binding_index as usize] = BindingState::OccupiedConvertedUniform; + } else if attr_ident == BINDLESS_ATTRIBUTE_NAME { + if let Ok(count_lit) = + attr.parse_args_with(|input: ParseStream| input.parse::()) + { + attr_bindless_count = Some(count_lit); + } } } } @@ -244,7 +254,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { has_dynamic_offset: false, min_binding_size: None, }, - count: None, + count: #actual_bindless_slot_count, } }); } @@ -264,14 +274,17 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { // insert fallible texture-based entries at 0 so that if we fail here, we exit before allocating any buffers binding_impls.insert(0, quote! { ( #binding_index, - #render_path::render_resource::OwnedBindingResource::TextureView({ - let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into(); - if let Some(handle) = handle { - images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone() - } else { - #fallback_image.texture_view.clone() - } - }) + #render_path::render_resource::OwnedBindingResource::TextureView( + #dimension, + { + let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into(); + if let Some(handle) = handle { + images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone() + } else { + #fallback_image.texture_view.clone() + } + } + ) ) }); @@ -284,7 +297,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { format: #render_path::render_resource::TextureFormat::#image_format, view_dimension: #render_path::render_resource::#dimension, }, - count: None, + count: #actual_bindless_slot_count, } }); } @@ -305,14 +318,17 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { binding_impls.insert(0, quote! { ( #binding_index, - #render_path::render_resource::OwnedBindingResource::TextureView({ - let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into(); - if let Some(handle) = handle { - images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone() - } else { - #fallback_image.texture_view.clone() + #render_path::render_resource::OwnedBindingResource::TextureView( + #render_path::render_resource::#dimension, + { + let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into(); + if let Some(handle) = handle { + images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone() + } else { + #fallback_image.texture_view.clone() + } } - }) + ) ) }); @@ -325,7 +341,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { sample_type: #render_path::render_resource::#sample_type, view_dimension: #render_path::render_resource::#dimension, }, - count: None, + count: #actual_bindless_slot_count, } }); } @@ -397,7 +413,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { binding: #binding_index, visibility: #visibility, ty: #render_path::render_resource::BindingType::Sampler(#render_path::render_resource::#sampler_binding_type), - count: None, + count: #actual_bindless_slot_count, } }); } @@ -410,6 +426,40 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { let struct_name_literal = struct_name.to_string(); let struct_name_literal = struct_name_literal.as_str(); let mut field_struct_impls = Vec::new(); + + let uniform_binding_type = Ident::new("uniform_binding_type", Span::call_site()); + let uniform_buffer_usages = Ident::new("uniform_buffer_usages", Span::call_site()); + + let uniform_binding_type_declarations = match attr_bindless_count { + Some(_) => { + quote! { + let (#uniform_binding_type, #uniform_buffer_usages) = + if render_device.features().contains( + #render_path::settings::WgpuFeatures::BUFFER_BINDING_ARRAY | + #render_path::settings::WgpuFeatures::TEXTURE_BINDING_ARRAY + ) && render_device.limits().max_storage_buffers_per_shader_stage > 0 { + ( + #render_path::render_resource::BufferBindingType::Storage { read_only: true }, + #render_path::render_resource::BufferUsages::STORAGE, + ) + } else { + ( + #render_path::render_resource::BufferBindingType::Uniform, + #render_path::render_resource::BufferUsages::UNIFORM, + ) + }; + } + } + None => { + quote! { + let (#uniform_binding_type, #uniform_buffer_usages) = ( + #render_path::render_resource::BufferBindingType::Uniform, + #render_path::render_resource::BufferUsages::UNIFORM, + ); + } + } + }; + for (binding_index, binding_state) in binding_states.iter().enumerate() { let binding_index = binding_index as u32; if let BindingState::OccupiedMergeableUniform { uniform_fields } = binding_state { @@ -426,7 +476,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { #render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data( &#render_path::render_resource::BufferInitDescriptor { label: None, - usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM, + usage: #render_path::render_resource::BufferUsages::COPY_DST | #uniform_buffer_usages, contents: buffer.as_ref(), }, )) @@ -438,11 +488,11 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { binding: #binding_index, visibility: #render_path::render_resource::ShaderStages::all(), ty: #render_path::render_resource::BindingType::Buffer { - ty: #render_path::render_resource::BufferBindingType::Uniform, + ty: #uniform_binding_type, has_dynamic_offset: false, min_binding_size: Some(<#field_ty as #render_path::render_resource::ShaderType>::min_size()), }, - count: None, + count: actual_bindless_slot_count, } }); // multi-field uniform bindings for a given index require an intermediate struct to derive ShaderType @@ -472,7 +522,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { #render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data( &#render_path::render_resource::BufferInitDescriptor { label: None, - usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM, + usage: #render_path::render_resource::BufferUsages::COPY_DST | #uniform_buffer_usages, contents: buffer.as_ref(), }, )) @@ -484,11 +534,11 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { binding: #binding_index, visibility: #render_path::render_resource::ShaderStages::all(), ty: #render_path::render_resource::BindingType::Buffer { - ty: #render_path::render_resource::BufferBindingType::Uniform, + ty: #uniform_binding_type, has_dynamic_offset: false, min_binding_size: Some(<#uniform_struct_name as #render_path::render_resource::ShaderType>::min_size()), }, - count: None, + count: actual_bindless_slot_count, } }); } @@ -506,6 +556,26 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { (prepared_data.clone(), prepared_data) }; + let (bindless_slot_count, actual_bindless_slot_count_declaration) = match attr_bindless_count { + Some(bindless_count) => ( + quote! { const BINDLESS_SLOT_COUNT: Option = Some(#bindless_count); }, + quote! { + let #actual_bindless_slot_count = if render_device.features().contains( + #render_path::settings::WgpuFeatures::BUFFER_BINDING_ARRAY | + #render_path::settings::WgpuFeatures::TEXTURE_BINDING_ARRAY + ) && render_device.limits().max_storage_buffers_per_shader_stage > 0 { + ::core::num::NonZeroU32::new(#bindless_count) + } else { + None + }; + }, + ), + None => ( + TokenStream::new().into(), + quote! { let #actual_bindless_slot_count: Option<::core::num::NonZeroU32> = None; }, + ), + }; + Ok(TokenStream::from(quote! { #(#field_struct_impls)* @@ -518,6 +588,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { #ecs_path::system::lifetimeless::SRes<#render_path::render_asset::RenderAssets<#render_path::storage::GpuShaderStorageBuffer>>, ); + #bindless_slot_count + fn label() -> Option<&'static str> { Some(#struct_name_literal) } @@ -528,7 +600,9 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { render_device: &#render_path::renderer::RenderDevice, (images, fallback_image, storage_buffers): &mut #ecs_path::system::SystemParamItem<'_, '_, Self::Param>, ) -> Result<#render_path::render_resource::UnpreparedBindGroup, #render_path::render_resource::AsBindGroupError> { - let bindings = vec![#(#binding_impls,)*]; + #uniform_binding_type_declarations + + let bindings = #render_path::render_resource::BindingResources(vec![#(#binding_impls,)*]); Ok(#render_path::render_resource::UnpreparedBindGroup { bindings, @@ -537,6 +611,9 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { } fn bind_group_layout_entries(render_device: &#render_path::renderer::RenderDevice) -> Vec<#render_path::render_resource::BindGroupLayoutEntry> { + #actual_bindless_slot_count_declaration + #uniform_binding_type_declarations + vec![#(#binding_layouts,)*] } } diff --git a/crates/bevy_render/macros/src/lib.rs b/crates/bevy_render/macros/src/lib.rs index d7cb909477326..6ebcc43cd272d 100644 --- a/crates/bevy_render/macros/src/lib.rs +++ b/crates/bevy_render/macros/src/lib.rs @@ -57,7 +57,15 @@ pub fn derive_extract_component(input: TokenStream) -> TokenStream { #[proc_macro_derive( AsBindGroup, - attributes(uniform, storage_texture, texture, sampler, bind_group_data, storage) + attributes( + uniform, + storage_texture, + texture, + sampler, + bind_group_data, + storage, + bindless + ) )] pub fn derive_as_bind_group(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index 40f9617c50d15..7a7829e0f4ef1 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -13,7 +13,7 @@ use crate::{ }; use allocator::MeshAllocatorPlugin; use bevy_app::{App, Plugin, PostUpdate}; -use bevy_asset::{AssetApp, RenderAssetUsages}; +use bevy_asset::{AssetApp, AssetId, RenderAssetUsages}; use bevy_ecs::{ entity::Entity, query::{Changed, With}, @@ -169,6 +169,7 @@ impl RenderAsset for RenderMesh { /// Converts the extracted mesh into a [`RenderMesh`]. fn prepare_asset( mesh: Self::SourceAsset, + _: AssetId, (images, ref mut mesh_vertex_buffer_layouts): &mut SystemParamItem, ) -> Result> { let morph_targets = match mesh.morph_targets() { diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index 9fc802ed85503..cc126f3e62322 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -6,7 +6,7 @@ pub use bevy_asset::RenderAssetUsages; use bevy_asset::{Asset, AssetEvent, AssetId, Assets}; use bevy_ecs::{ prelude::{Commands, EventReader, IntoSystemConfigs, ResMut, Resource}, - schedule::SystemConfigs, + schedule::{SystemConfigs, SystemSet}, system::{StaticSystemParam, SystemParam, SystemParamItem, SystemState}, world::{FromWorld, Mut}, }; @@ -26,6 +26,9 @@ pub enum PrepareAssetError { AsBindGroupError(AsBindGroupError), } +#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] +pub struct ExtractAssetsSet; + /// Describes how an asset gets extracted and prepared for rendering. /// /// In the [`ExtractSchedule`] step the [`RenderAsset::SourceAsset`] is transferred @@ -61,8 +64,11 @@ pub trait RenderAsset: Send + Sync + 'static + Sized { /// ECS data may be accessed via `param`. fn prepare_asset( source_asset: Self::SourceAsset, + asset_id: AssetId, param: &mut SystemParamItem, ) -> Result>; + + fn finalize_asset(_: AssetId, _: &mut SystemParamItem) {} } /// This plugin extracts the changed assets from the "app world" into the "render world" @@ -99,7 +105,10 @@ impl Plugin .init_resource::>() .init_resource::>() .init_resource::>() - .add_systems(ExtractSchedule, extract_render_asset::); + .add_systems( + ExtractSchedule, + extract_render_asset::.in_set(ExtractAssetsSet), + ); AFTER::register_system( render_app, prepare_assets::.in_set(RenderSet::PrepareAssets), @@ -310,7 +319,7 @@ pub fn prepare_assets( 0 }; - match A::prepare_asset(extracted_asset, &mut param) { + match A::prepare_asset(extracted_asset, id, &mut param) { Ok(prepared_asset) => { render_assets.insert(id, prepared_asset); bpf.write_bytes(write_bytes); @@ -330,6 +339,7 @@ pub fn prepare_assets( for removed in extracted_assets.removed.drain() { render_assets.remove(removed); + A::finalize_asset(removed, &mut param); } for (id, extracted_asset) in extracted_assets.extracted.drain(..) { @@ -348,7 +358,7 @@ pub fn prepare_assets( 0 }; - match A::prepare_asset(extracted_asset, &mut param) { + match A::prepare_asset(extracted_asset, id, &mut param) { Ok(prepared_asset) => { render_assets.insert(id, prepared_asset); bpf.write_bytes(write_bytes); diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index 2753616aa0a4a..481ef867c81f7 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -7,12 +7,13 @@ use crate::{ texture::GpuImage, }; use alloc::sync::Arc; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::system::{SystemParam, SystemParamItem}; pub use bevy_render_macros::AsBindGroup; use core::ops::Deref; use derive_more::derive::{Display, Error}; use encase::ShaderType; -use wgpu::{BindGroupEntry, BindGroupLayoutEntry, BindingResource}; +use wgpu::{BindGroupEntry, BindGroupLayoutEntry, BindingResource, TextureViewDimension}; define_atomic_id!(BindGroupId); @@ -247,6 +248,25 @@ impl Deref for BindGroup { /// as [`AsBindGroup::Data`] as part of the [`AsBindGroup::as_bind_group`] call. This is useful if data needs to be stored alongside /// the generated bind group, such as a unique identifier for a material's bind group. The most common use case for this attribute /// is "shader pipeline specialization". See [`SpecializedRenderPipeline`](crate::render_resource::SpecializedRenderPipeline). +/// * `bindless(COUNT)` +/// * This switch enables *bindless resources*, which changes the way Bevy +/// supplies resources (uniforms, textures, and samplers) to the shader. +/// When bindless resources are enabled, and the current platform supports +/// them, instead of presenting a single instance of a resource to your +/// shader Bevy will instead present a *binding array* of `COUNT` elements. +/// In your shader, the index of the element of each binding array +/// corresponding to the mesh currently being drawn can be retrieved with +/// `mesh[in.instance_index].material_bind_group_slot`. +/// * The purpose of bindless mode is to improve performance by reducing +/// state changes. By grouping resources together into binding arrays, Bevy +/// doesn't have to modify GPU state as often, decreasing API and driver +/// overhead. +/// * If bindless mode is enabled, the `BINDLESS` definition will be +/// available. Because not all platforms support bindless resources, you +/// should check for the presence of this definition via `#ifdef` and fall +/// back to standard bindings if it isn't present. +/// * See the `shaders/shader_material_bindless` example for an example of +/// how to use bindless mode. /// /// The previous `CoolMaterial` example illustrating "combining multiple field-level uniform attributes with the same binding index" can /// also be equivalently represented with a single struct-level uniform attribute: @@ -307,6 +327,8 @@ pub trait AsBindGroup { type Param: SystemParam + 'static; + const BINDLESS_SLOT_COUNT: Option = None; + /// label fn label() -> Option<&'static str> { None @@ -379,24 +401,27 @@ pub enum AsBindGroupError { /// A prepared bind group returned as a result of [`AsBindGroup::as_bind_group`]. pub struct PreparedBindGroup { - pub bindings: Vec<(u32, OwnedBindingResource)>, + pub bindings: BindingResources, pub bind_group: BindGroup, pub data: T, } /// a map containing `OwnedBindingResource`s, keyed by the target binding index pub struct UnpreparedBindGroup { - pub bindings: Vec<(u32, OwnedBindingResource)>, + pub bindings: BindingResources, pub data: T, } +#[derive(Deref, DerefMut)] +pub struct BindingResources(pub Vec<(u32, OwnedBindingResource)>); + /// An owned binding resource of any type (ex: a [`Buffer`], [`TextureView`], etc). /// This is used by types like [`PreparedBindGroup`] to hold a single list of all /// render resources used by bindings. #[derive(Debug)] pub enum OwnedBindingResource { Buffer(Buffer), - TextureView(TextureView), + TextureView(TextureViewDimension, TextureView), Sampler(Sampler), } @@ -404,7 +429,7 @@ impl OwnedBindingResource { pub fn get_binding(&self) -> BindingResource { match self { OwnedBindingResource::Buffer(buffer) => buffer.as_entire_binding(), - OwnedBindingResource::TextureView(view) => BindingResource::TextureView(view), + OwnedBindingResource::TextureView(_, view) => BindingResource::TextureView(view), OwnedBindingResource::Sampler(sampler) => BindingResource::Sampler(sampler), } } diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index 068bf74fa06d8..27a08851122c6 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -49,13 +49,13 @@ pub use wgpu::{ PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, PushConstantRange, RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, - RenderPipelineDescriptor as RawRenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, - ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, StencilFaceState, - StencilOperation, StencilState, StorageTextureAccess, StoreOp, TextureAspect, - TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages, - TextureViewDescriptor, TextureViewDimension, VertexAttribute, - VertexBufferLayout as RawVertexBufferLayout, VertexFormat, VertexState as RawVertexState, - VertexStepMode, COPY_BUFFER_ALIGNMENT, + RenderPipelineDescriptor as RawRenderPipelineDescriptor, Sampler as WgpuSampler, + SamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, + ShaderStages, StencilFaceState, StencilOperation, StencilState, StorageTextureAccess, StoreOp, + TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, + TextureUsages, TextureView as WgpuTextureView, TextureViewDescriptor, TextureViewDimension, + VertexAttribute, VertexBufferLayout as RawVertexBufferLayout, VertexFormat, + VertexState as RawVertexState, VertexStepMode, COPY_BUFFER_ALIGNMENT, }; pub use crate::mesh::VertexBufferLayout; diff --git a/crates/bevy_render/src/storage.rs b/crates/bevy_render/src/storage.rs index bd5c3dba9fc38..7434f3999f3dc 100644 --- a/crates/bevy_render/src/storage.rs +++ b/crates/bevy_render/src/storage.rs @@ -4,7 +4,7 @@ use crate::{ renderer::RenderDevice, }; use bevy_app::{App, Plugin}; -use bevy_asset::{Asset, AssetApp}; +use bevy_asset::{Asset, AssetApp, AssetId}; use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_utils::default; @@ -114,6 +114,7 @@ impl RenderAsset for GpuShaderStorageBuffer { fn prepare_asset( source_asset: Self::SourceAsset, + _: AssetId, render_device: &mut SystemParamItem, ) -> Result> { match source_asset.data { diff --git a/crates/bevy_render/src/texture/fallback_image.rs b/crates/bevy_render/src/texture/fallback_image.rs index ee359f1d3f3bb..9ef7c04d2f1d0 100644 --- a/crates/bevy_render/src/texture/fallback_image.rs +++ b/crates/bevy_render/src/texture/fallback_image.rs @@ -34,6 +34,19 @@ pub struct FallbackImage { pub d3: GpuImage, } +impl FallbackImage { + pub fn get(&self, texture_dimension: TextureViewDimension) -> &GpuImage { + match texture_dimension { + TextureViewDimension::D1 => &self.d1, + TextureViewDimension::D2 => &self.d2, + TextureViewDimension::D2Array => &self.d2_array, + TextureViewDimension::Cube => &self.cube, + TextureViewDimension::CubeArray => &self.cube_array, + TextureViewDimension::D3 => &self.d3, + } + } +} + /// A [`RenderApp`](crate::RenderApp) resource that contains a _zero-filled_ "fallback image", /// which can be used in place of [`FallbackImage`], when a fully transparent or black fallback /// is required instead of fully opaque white. diff --git a/crates/bevy_render/src/texture/gpu_image.rs b/crates/bevy_render/src/texture/gpu_image.rs index 2e760054f3e20..7be547a4a119f 100644 --- a/crates/bevy_render/src/texture/gpu_image.rs +++ b/crates/bevy_render/src/texture/gpu_image.rs @@ -3,6 +3,7 @@ use crate::{ render_resource::{DefaultImageSampler, Sampler, Texture, TextureView}, renderer::{RenderDevice, RenderQueue}, }; +use bevy_asset::AssetId; use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; use bevy_image::{Image, ImageSampler}; use bevy_math::UVec2; @@ -41,6 +42,7 @@ impl RenderAsset for GpuImage { /// Converts the extracted image into a [`GpuImage`]. fn prepare_asset( image: Self::SourceAsset, + _: AssetId, (render_device, render_queue, default_sampler): &mut SystemParamItem, ) -> Result> { let texture = render_device.create_texture_with_data( diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 4eb064de206b4..5b27a7c658ebf 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -17,7 +17,6 @@ use bevy_ecs::{ }; use bevy_math::FloatOrd; use bevy_reflect::{prelude::ReflectDefault, Reflect}; -use bevy_render::sync_world::MainEntityHashMap; use bevy_render::view::RenderVisibleEntities; use bevy_render::{ mesh::{MeshVertexBufferLayoutRef, RenderMesh}, @@ -30,14 +29,15 @@ use bevy_render::{ ViewBinnedRenderPhases, ViewSortedRenderPhases, }, render_resource::{ - AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, - OwnedBindingResource, PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef, - SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, + AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, PipelineCache, + RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline, + SpecializedMeshPipelineError, SpecializedMeshPipelines, }, renderer::RenderDevice, view::{ExtractedView, InheritedVisibility, Msaa, ViewVisibility, Visibility}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; +use bevy_render::{render_resource::BindingResources, sync_world::MainEntityHashMap}; use bevy_transform::components::{GlobalTransform, Transform}; use bevy_utils::tracing::error; use core::{hash::Hash, marker::PhantomData}; @@ -630,7 +630,7 @@ pub struct Material2dProperties { /// Data prepared for a [`Material2d`] instance. pub struct PreparedMaterial2d { - pub bindings: Vec<(u32, OwnedBindingResource)>, + pub bindings: BindingResources, pub bind_group: BindGroup, pub key: T::Data, pub properties: Material2dProperties, @@ -649,6 +649,7 @@ impl RenderAsset for PreparedMaterial2d { fn prepare_asset( material: Self::SourceAsset, + _: AssetId, (render_device, pipeline, material_param): &mut SystemParamItem, ) -> Result> { match material.as_bind_group(&pipeline.material2d_layout, render_device, material_param) { diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 23be50063c401..4e6330457fa94 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -576,7 +576,7 @@ pub fn prepare_uimaterial_nodes( } pub struct PreparedUiMaterial { - pub bindings: Vec<(u32, OwnedBindingResource)>, + pub bindings: BindingResources, pub bind_group: BindGroup, pub key: T::Data, } @@ -588,6 +588,7 @@ impl RenderAsset for PreparedUiMaterial { fn prepare_asset( material: Self::SourceAsset, + _: AssetId, (render_device, pipeline, ref mut material_param): &mut SystemParamItem, ) -> Result> { match material.as_bind_group(&pipeline.ui_layout, render_device, material_param) { diff --git a/examples/shader/custom_phase_item.rs b/examples/shader/custom_phase_item.rs index 935204c397113..2dcf8dd83781a 100644 --- a/examples/shader/custom_phase_item.rs +++ b/examples/shader/custom_phase_item.rs @@ -273,7 +273,7 @@ fn queue_custom_phase_item( draw_function: draw_custom_phase_item, pipeline: pipeline_id, asset_id: AssetId::::invalid().untyped(), - material_bind_group_id: None, + material_bind_group_index: None, lightmap_image: None, }, entity, diff --git a/examples/shader/shader_material_bindless.rs b/examples/shader/shader_material_bindless.rs new file mode 100644 index 0000000000000..4baab25cd96b0 --- /dev/null +++ b/examples/shader/shader_material_bindless.rs @@ -0,0 +1,63 @@ +//! A material that uses bindless textures. + +use bevy::prelude::*; +use bevy_render::render_resource::{AsBindGroup, ShaderRef}; + +const SHADER_ASSET_PATH: &str = "shaders/bindless_material.wgsl"; + +#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] +#[bindless(4)] +struct BindlessMaterial { + #[uniform(0)] + color: LinearRgba, + #[texture(1)] + #[sampler(2)] + color_texture: Option>, +} + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + MaterialPlugin::::default(), + )) + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + asset_server: Res, +) { + commands.spawn(( + Mesh3d(meshes.add(Cuboid::default())), + MeshMaterial3d(materials.add(BindlessMaterial { + color: LinearRgba::BLUE, + color_texture: Some(asset_server.load("branding/bevy_logo_dark.png")), + })), + Transform::from_xyz(-2.0, 0.5, 0.0), + )); + + commands.spawn(( + Mesh3d(meshes.add(Cylinder::default())), + MeshMaterial3d(materials.add(BindlessMaterial { + color: LinearRgba::RED, + color_texture: Some(asset_server.load("branding/bevy_logo_light.png")), + })), + Transform::from_xyz(2.0, 0.5, 0.0), + )); + + // camera + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + )); +} + +impl Material for BindlessMaterial { + fn fragment_shader() -> ShaderRef { + SHADER_ASSET_PATH.into() + } +} diff --git a/examples/shader/specialized_mesh_pipeline.rs b/examples/shader/specialized_mesh_pipeline.rs index 2d30f605d1f9e..f6811f4ab8368 100644 --- a/examples/shader/specialized_mesh_pipeline.rs +++ b/examples/shader/specialized_mesh_pipeline.rs @@ -341,7 +341,7 @@ fn queue_custom_mesh_pipeline( // but you can use anything you like. Note that the asset ID need // not be the ID of a [`Mesh`]. asset_id: AssetId::::invalid().untyped(), - material_bind_group_id: None, + material_bind_group_index: None, lightmap_image: None, }, (render_entity, visible_entity), diff --git a/examples/shader/texture_binding_array.rs b/examples/shader/texture_binding_array.rs index 663401fab3460..5e1c2e7ed4dad 100644 --- a/examples/shader/texture_binding_array.rs +++ b/examples/shader/texture_binding_array.rs @@ -133,7 +133,7 @@ impl AsBindGroup for BindlessMaterial { ); Ok(PreparedBindGroup { - bindings: vec![], + bindings: BindingResources(vec![]), bind_group, data: (), }) From ed3272f4f8c20b81d8a8b3d53f02be3bbd624112 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Tue, 12 Nov 2024 20:53:30 -0800 Subject: [PATCH 2/3] Internal import police --- examples/shader/shader_material_bindless.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/shader/shader_material_bindless.rs b/examples/shader/shader_material_bindless.rs index 4baab25cd96b0..5619ca8da6668 100644 --- a/examples/shader/shader_material_bindless.rs +++ b/examples/shader/shader_material_bindless.rs @@ -1,7 +1,7 @@ //! A material that uses bindless textures. use bevy::prelude::*; -use bevy_render::render_resource::{AsBindGroup, ShaderRef}; +use bevy::render::render_resource::{AsBindGroup, ShaderRef}; const SHADER_ASSET_PATH: &str = "shaders/bindless_material.wgsl"; From ebcb094482b83e217098d2930929b6cdd0d94291 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Tue, 12 Nov 2024 21:08:26 -0800 Subject: [PATCH 3/3] Update `examples/README.md` --- examples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.md b/examples/README.md index 92bf3f188be1b..330d4a17a32c7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -426,6 +426,7 @@ Example | Description [Instancing](../examples/shader/automatic_instancing.rs) | Shows that multiple instances of a cube are automatically instanced in one draw call [Material](../examples/shader/shader_material.rs) | A shader and a material that uses it [Material](../examples/shader/shader_material_2d.rs) | A shader and a material that uses it on a 2d mesh +[Material - Bindless](../examples/shader/shader_material_bindless.rs) | Demonstrates how to make materials that use bindless textures [Material - GLSL](../examples/shader/shader_material_glsl.rs) | A shader that uses the GLSL shading language [Material - Screenspace Texture](../examples/shader/shader_material_screenspace_texture.rs) | A shader that samples a texture with view-independent UV coordinates [Material Prepass](../examples/shader/shader_prepass.rs) | A shader that uses the various textures generated by the prepass