diff --git a/assets/shaders/bindless_material.wgsl b/assets/shaders/bindless_material.wgsl index 9d9d068d4c..3c91dcebaf 100644 --- a/assets/shaders/bindless_material.wgsl +++ b/assets/shaders/bindless_material.wgsl @@ -1,14 +1,22 @@ #import bevy_pbr::forward_io::VertexOutput #import bevy_pbr::mesh_bindings::mesh +#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d} struct Color { base_color: vec4, } +// This structure is a mapping from bindless index to the index in the +// appropriate slab +struct MaterialBindings { + material: u32, // 0 + color_texture: u32, // 1 + color_texture_sampler: u32, // 2 +} + #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; +@group(2) @binding(0) var materials: binding_array; +@group(2) @binding(10) var material_color: binding_array; #else // BINDLESS @group(2) @binding(0) var material_color: Color; @group(2) @binding(1) var material_color_texture: texture_2d; @@ -19,15 +27,15 @@ struct Color { fn fragment(in: VertexOutput) -> @location(0) vec4 { #ifdef BINDLESS let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; - let base_color = material_color[slot].base_color; + let base_color = material_color[materials[slot].material].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], + bindless_textures_2d[materials[slot].color_texture], + bindless_samplers_filtering[materials[slot].color_texture_sampler], #else // BINDLESS material_color_texture, material_color_sampler, diff --git a/crates/bevy_pbr/src/extended_material.rs b/crates/bevy_pbr/src/extended_material.rs index 17ea201561..6869043903 100644 --- a/crates/bevy_pbr/src/extended_material.rs +++ b/crates/bevy_pbr/src/extended_material.rs @@ -5,8 +5,9 @@ use bevy_render::{ alpha::AlphaMode, mesh::MeshVertexBufferLayoutRef, render_resource::{ - AsBindGroup, AsBindGroupError, BindGroupLayout, RenderPipelineDescriptor, Shader, - ShaderRef, SpecializedMeshPipelineError, UnpreparedBindGroup, + AsBindGroup, AsBindGroupError, BindGroupLayout, BindlessDescriptor, + BindlessSlabResourceLimit, RenderPipelineDescriptor, Shader, ShaderRef, + SpecializedMeshPipelineError, UnpreparedBindGroup, }, renderer::RenderDevice, }; @@ -153,13 +154,12 @@ impl AsBindGroup for ExtendedMaterial { type Data = (::Data, ::Data); type Param = (::Param, ::Param); - fn bindless_slot_count() -> Option { - match (B::bindless_slot_count(), E::bindless_slot_count()) { - (Some(base_bindless_slot_count), Some(extension_bindless_slot_count)) => { - Some(base_bindless_slot_count.min(extension_bindless_slot_count)) - } - _ => None, + fn bindless_slot_count() -> Option { + // For now, disable bindless in `ExtendedMaterial`. + if B::bindless_slot_count().is_some() && E::bindless_slot_count().is_some() { + panic!("Bindless extended materials are currently unsupported") } + None } fn unprepared_bind_group( @@ -167,30 +167,15 @@ impl AsBindGroup for ExtendedMaterial { layout: &BindGroupLayout, render_device: &RenderDevice, (base_param, extended_param): &mut SystemParamItem<'_, '_, Self::Param>, - mut force_no_bindless: bool, + _: bool, ) -> Result, AsBindGroupError> { - // Only allow bindless mode if both the base material and the extension - // support it. - force_no_bindless = force_no_bindless || Self::bindless_slot_count().is_none(); - // add together the bindings of the base material and the user material let UnpreparedBindGroup { mut bindings, data: base_data, - } = B::unprepared_bind_group( - &self.base, - layout, - render_device, - base_param, - force_no_bindless, - )?; - let extended_bindgroup = E::unprepared_bind_group( - &self.extension, - layout, - render_device, - extended_param, - force_no_bindless, - )?; + } = B::unprepared_bind_group(&self.base, layout, render_device, base_param, true)?; + let extended_bindgroup = + E::unprepared_bind_group(&self.extension, layout, render_device, extended_param, true)?; bindings.extend(extended_bindgroup.bindings.0); @@ -202,23 +187,24 @@ impl AsBindGroup for ExtendedMaterial { fn bind_group_layout_entries( render_device: &RenderDevice, - mut force_no_bindless: bool, + _: bool, ) -> Vec where Self: Sized, { - // Only allow bindless mode if both the base material and the extension - // support it. - force_no_bindless = force_no_bindless || Self::bindless_slot_count().is_none(); - // add together the bindings of the standard material and the user material - let mut entries = B::bind_group_layout_entries(render_device, force_no_bindless); - entries.extend(E::bind_group_layout_entries( - render_device, - force_no_bindless, - )); + let mut entries = B::bind_group_layout_entries(render_device, true); + entries.extend(E::bind_group_layout_entries(render_device, true)); entries } + + fn bindless_descriptor() -> Option { + if B::bindless_descriptor().is_some() && E::bindless_descriptor().is_some() { + panic!("Bindless extended materials are currently unsupported") + } + + None + } } impl Material for ExtendedMaterial { diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 2074008bb9..4914060d08 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -45,8 +45,6 @@ mod ssao; mod ssr; mod volumetric_fog; -use crate::material_bind_groups::FallbackBindlessResources; - use bevy_color::{Color, LinearRgba}; pub use atmosphere::*; @@ -59,6 +57,7 @@ pub use light::*; pub use light_probe::*; pub use lightmap::*; pub use material::*; +pub use material_bind_groups::*; pub use mesh_material::*; pub use parallax::*; pub use pbr_material::*; diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 944bd764e4..a1c31fe26b 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1,4 +1,6 @@ -use crate::material_bind_groups::{MaterialBindGroupAllocator, MaterialBindingId}; +use crate::material_bind_groups::{ + FallbackBindlessResources, MaterialBindGroupAllocator, MaterialBindingId, +}; #[cfg(feature = "meshlet")] use crate::meshlet::{ prepare_material_meshlet_meshes_main_opaque_pass, queue_material_meshlet_meshes, @@ -32,6 +34,7 @@ use bevy_platform_support::hash::FixedHasher; use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; use bevy_render::mesh::mark_3d_meshes_as_changed_if_their_assets_changed; +use bevy_render::renderer::RenderQueue; use bevy_render::{ batching::gpu_preprocessing::GpuPreprocessingSupport, extract_resource::ExtractResource, @@ -328,7 +331,11 @@ where ) .add_systems( Render, - prepare_material_bind_groups:: + ( + prepare_material_bind_groups::, + write_material_bind_group_buffers::, + ) + .chain() .in_set(RenderSet::PrepareBindGroups) .after(prepare_assets::>), ); @@ -509,7 +516,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), + bindless: material_uses_bindless_resources::(render_device), marker: PhantomData, } } @@ -560,7 +567,7 @@ impl RenderCommand

for SetMaterial else { return RenderCommandResult::Skip; }; - let Some(bind_group) = material_bind_group.get_bind_group() else { + let Some(bind_group) = material_bind_group.bind_group() else { return RenderCommandResult::Skip; }; pass.set_bind_group(I, bind_group, &[]); @@ -1234,11 +1241,6 @@ impl RenderAsset for PreparedMaterial { ref mut material_param, ): &mut SystemParamItem, ) -> Result> { - // Allocate a material binding ID if needed. - let material_binding_id = *render_material_bindings - .entry(material_id.into()) - .or_insert_with(|| bind_group_allocator.allocate()); - let draw_opaque_pbr = opaque_draw_functions.read().id::>(); let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::>(); let draw_transmissive_pbr = transmissive_draw_functions.read().id::>(); @@ -1304,10 +1306,15 @@ impl RenderAsset for PreparedMaterial { false, ) { Ok(unprepared) => { - bind_group_allocator.init(render_device, material_binding_id, unprepared); + let binding = *render_material_bindings + .entry(material_id.into()) + .or_insert_with(|| { + bind_group_allocator + .allocate_unprepared(unprepared, &pipeline.material_layout) + }); Ok(PreparedMaterial { - binding: material_binding_id, + binding, properties: MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), @@ -1339,11 +1346,9 @@ impl RenderAsset for PreparedMaterial { ) { Ok(prepared_bind_group) => { // Store the resulting bind group directly in the slot. - bind_group_allocator.init_custom( - material_binding_id, - prepared_bind_group.bind_group, - prepared_bind_group.data, - ); + let material_binding_id = + bind_group_allocator.allocate_prepared(prepared_bind_group); + render_material_bindings.insert(material_id.into(), material_binding_id); Ok(PreparedMaterial { binding: material_binding_id, @@ -1408,8 +1413,8 @@ impl From for MaterialBindGroupId { } } -/// A system that creates and/or recreates any bind groups that contain -/// materials that were modified this frame. +/// Creates and/or recreates any bind groups that contain materials that were +/// modified this frame. pub fn prepare_material_bind_groups( mut allocator: ResMut>, render_device: Res, @@ -1418,5 +1423,20 @@ pub fn prepare_material_bind_groups( ) where M: Material, { - allocator.prepare_bind_groups(&render_device, &fallback_image, &fallback_resources); + allocator.prepare_bind_groups(&render_device, &fallback_resources, &fallback_image); +} + +/// Uploads the contents of all buffers that the [`MaterialBindGroupAllocator`] +/// manages to the GPU. +/// +/// Non-bindless allocators don't currently manage any buffers, so this method +/// only has an effect for bindless allocators. +pub fn write_material_bind_group_buffers( + mut allocator: ResMut>, + render_device: Res, + render_queue: Res, +) where + M: Material, +{ + allocator.write_buffers(&render_device, &render_queue); } diff --git a/crates/bevy_pbr/src/material_bind_groups.rs b/crates/bevy_pbr/src/material_bind_groups.rs index 9066537a72..ea152d2272 100644 --- a/crates/bevy_pbr/src/material_bind_groups.rs +++ b/crates/bevy_pbr/src/material_bind_groups.rs @@ -1,137 +1,258 @@ //! Material bind group management for bindless resources. //! -//! In bindless mode, Bevy's renderer groups materials into small bind groups. -//! This allocator manages each bind group, assigning slots to materials as +//! In bindless mode, Bevy's renderer groups materials into bind groups. This +//! allocator manages each bind group, assigning slots to materials as //! appropriate. -use crate::Material; +use core::{marker::PhantomData, mem}; + use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ resource::Resource, world::{FromWorld, World}, }; -use bevy_platform_support::collections::HashMap; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_platform_support::collections::{HashMap, HashSet}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_render::{ render_resource::{ - BindGroup, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry, BindingResource, - BindingType, Buffer, BufferBinding, BufferInitDescriptor, BufferUsages, - OwnedBindingResource, Sampler, SamplerDescriptor, TextureViewDimension, - UnpreparedBindGroup, WgpuSampler, WgpuTextureView, + BindGroup, BindGroupEntry, BindGroupLayout, BindingNumber, BindingResource, + BindingResources, BindlessDescriptor, BindlessIndex, BindlessResourceType, Buffer, + BufferBinding, BufferDescriptor, BufferId, BufferUsages, CompareFunction, FilterMode, + OwnedBindingResource, PreparedBindGroup, RawBufferVec, Sampler, SamplerDescriptor, + SamplerId, TextureView, TextureViewDimension, TextureViewId, UnpreparedBindGroup, + WgpuSampler, WgpuTextureView, }, - renderer::RenderDevice, + renderer::{RenderDevice, RenderQueue}, texture::FallbackImage, }; use bevy_utils::default; -use core::{any, iter, marker::PhantomData, num::NonZero}; -use tracing::error; +use tracing::{error, trace}; -/// An object that creates and stores bind groups for a single material type. +use crate::Material; + +/// A resource that places materials into bind groups and tracks their +/// resources. /// -/// This object collects bindless materials into groups as appropriate and -/// assigns slots as materials are created. +/// Internally, Bevy has separate allocators for bindless and non-bindless +/// materials. This resource provides a common interface to the specific +/// allocator in use. #[derive(Resource)] -pub struct MaterialBindGroupAllocator +pub enum MaterialBindGroupAllocator where M: Material, { - /// The data that the allocator keeps about each bind group. - bind_groups: Vec>, + /// The allocator used when the material is bindless. + Bindless(Box>), + /// The allocator used when the material is non-bindless. + NonBindless(Box>), +} - /// Stores IDs of material bind groups that have at least one slot - /// available. - free_bind_groups: Vec, - - /// The layout for this bind group. +/// The allocator that places bindless materials into bind groups and tracks +/// their resources. +pub struct MaterialBindGroupBindlessAllocator +where + M: Material, +{ + /// The slabs, each of which contains a bind group. + slabs: Vec>, + /// The layout of the bind groups that we produce. bind_group_layout: BindGroupLayout, - - /// Dummy buffers that are assigned to unused slots. - fallback_buffers: MaterialFallbackBuffers, - - /// Whether this material is actually using bindless resources. + /// Information about the bindless resources in the material. /// - /// This takes the availability of bindless resources on this platform into - /// account. - bindless_enabled: bool, + /// We use this information to create and maintain bind groups. + bindless_descriptor: BindlessDescriptor, + /// Dummy buffers that we use to fill empty slots in buffer binding arrays. + /// + /// There's one fallback buffer for each buffer in the bind group, each + /// appropriately sized. Each buffer contains one uninitialized element of + /// the applicable type. + fallback_buffers: HashMap, + + /// The maximum number of resources that can be stored in a slab. + /// + /// This corresponds to `SLAB_CAPACITY` in the `#[bindless(SLAB_CAPACITY)]` + /// attribute, when deriving `AsBindGroup`. + slab_capacity: u32, +} + +/// A single bind group and the bookkeeping necessary to allocate into it. +pub struct MaterialBindlessSlab +where + M: Material, +{ + /// The current bind group, if it's up to date. + /// + /// If this is `None`, then the bind group is dirty and needs to be + /// regenerated. + bind_group: Option, + + /// A GPU-accessible buffer that holds the mapping from binding index to + /// bindless slot. + /// + /// This is conventionally assigned to bind group binding 0. + bindless_index_table: MaterialBindlessIndexTable, + + /// The binding arrays containing samplers. + samplers: HashMap>, + /// The binding arrays containing textures. + textures: HashMap>, + /// The binding arrays containing data buffers. + buffers: HashMap>, + + /// Holds extra CPU-accessible data that the material provides. + /// + /// Typically, this data is used for constructing the material key, for + /// pipeline specialization purposes. + extra_data: Vec>, + + /// A list of free slot IDs. + free_slots: Vec, + /// The total number of materials currently allocated in this slab. + live_allocation_count: u32, + /// The total number of resources currently allocated in the binding arrays. + allocated_resource_count: u32, +} + +/// A GPU-accessible buffer that holds the mapping from binding index to +/// bindless slot. +/// +/// This is conventionally assigned to bind group binding 0. +struct MaterialBindlessIndexTable +where + M: Material, +{ + /// The contents of the buffer. + buffer: RawBufferVec, + /// Whether the contents of the buffer have been uploaded to the GPU. + buffer_dirty: BufferDirtyState, phantom: PhantomData, } -/// Information that the allocator keeps about each bind group. -pub enum MaterialBindGroup +/// A single binding array for storing bindless resources and the bookkeeping +/// necessary to allocate into it. +struct MaterialBindlessBindingArray where - M: Material, + R: GetBindingResourceId, { - /// Information that the allocator keeps about each bind group with bindless - /// textures in use. - Bindless(MaterialBindlessBindGroup), - - /// Information that the allocator keeps about each bind group for which - /// bindless textures are not in use. - NonBindless(MaterialNonBindlessBindGroup), -} - -/// Information that the allocator keeps about each bind group with bindless -/// textures in use. -pub struct MaterialBindlessBindGroup -where - M: Material, -{ - /// The actual bind group. - pub bind_group: Option, - - /// The bind group data for each slot. + /// The number of the binding that we attach this binding array to. + binding_number: BindingNumber, + /// A mapping from bindless slot index to the resource stored in that slot, + /// if any. + bindings: Vec>>, + /// The type of resource stored in this binding array. + resource_type: BindlessResourceType, + /// Maps a resource ID to the slot in which it's stored. /// - /// This is `None` if the slot is unallocated and `Some` if the slot is - /// full. - unprepared_bind_groups: Vec>>, + /// This is essentially the inverse mapping of [`Self::bindings`]. + resource_to_slot: HashMap, + /// A list of free slots in [`Self::bindings`] that contain no binding. + free_slots: Vec, + /// The number of allocated objects in this binding array. + len: u32, +} - /// A bitfield that contains a 0 if the slot is free or a 1 if the slot is - /// full. +/// A single resource (sampler, texture, or buffer) in a binding array. +/// +/// Resources hold a reference count, which specifies the number of materials +/// currently allocated within the slab that refer to this resource. When the +/// reference count drops to zero, the resource is freed. +struct MaterialBindlessBinding +where + R: GetBindingResourceId, +{ + /// The sampler, texture, or buffer. + resource: R, + /// The number of materials currently allocated within the containing slab + /// that use this resource. + ref_count: u32, +} + +/// The allocator that stores bind groups for non-bindless materials. +pub struct MaterialBindGroupNonBindlessAllocator +where + M: Material, +{ + /// A mapping from [`MaterialBindGroupIndex`] to the bind group allocated in + /// each slot. + bind_groups: Vec>>, + /// The bind groups that are dirty and need to be prepared. /// - /// We keep this value so that we can quickly find the next free slot when - /// we go to allocate. - used_slot_bitmap: u32, + /// To prepare the bind groups, call + /// [`MaterialBindGroupAllocator::prepare_bind_groups`]. + to_prepare: HashSet, + /// A list of free bind group indices. + free_indices: Vec, + phantom: PhantomData, } -/// Information that the allocator keeps about each bind group for which -/// bindless textures are not in use. -/// -/// When a bindless texture isn't in use, bind groups and material instances are -/// in 1:1 correspondence, and therefore there's only a single slot for extra -/// material data here. -pub struct MaterialNonBindlessBindGroup +/// A single bind group that a [`MaterialBindGroupNonBindlessAllocator`] is +/// currently managing. +enum MaterialNonBindlessAllocatedBindGroup where M: Material, { - /// The single allocation in a non-bindless bind group. - allocation: MaterialNonBindlessBindGroupAllocation, + /// An unprepared bind group. + /// + /// The allocator prepares all outstanding unprepared bind groups when + /// [`MaterialBindGroupNonBindlessAllocator::prepare_bind_groups`] is + /// called. + Unprepared { + /// The unprepared bind group, including extra data. + bind_group: UnpreparedBindGroup, + /// The layout of that bind group. + layout: BindGroupLayout, + }, + /// A bind group that's already been prepared. + Prepared(PreparedBindGroup), } -/// The single allocation in a non-bindless bind group. -enum MaterialNonBindlessBindGroupAllocation -where - M: Material, -{ - /// The allocation is free. - Unallocated, - /// The allocation has been allocated, but not yet initialized. - Allocated, - /// The allocation is full and contains both a bind group and extra data. - Initialized(BindGroup, M::Data), +/// Dummy instances of various resources that we fill unused slots in binding +/// arrays with. +#[derive(Resource)] +pub struct FallbackBindlessResources { + /// A dummy filtering sampler. + filtering_sampler: Sampler, + /// A dummy non-filtering sampler. + non_filtering_sampler: Sampler, + /// A dummy comparison sampler. + comparison_sampler: Sampler, } -/// Where the GPU data for a material is located. +/// The `wgpu` ID of a single bindless or non-bindless resource. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +enum BindingResourceId { + /// A buffer. + Buffer(BufferId), + /// A texture view, with the given dimension. + TextureView(TextureViewDimension, TextureViewId), + /// A sampler. + Sampler(SamplerId), +} + +/// A temporary list of references to `wgpu` bindless resources. /// -/// In bindless mode, materials are gathered into bind groups, and the slot is -/// necessary to locate the material data within that group. If not in bindless -/// mode, bind groups and materials are in 1:1 correspondence, and the slot -/// index is always 0. +/// We need this because the `wgpu` bindless API takes a slice of references. +/// Thus we need to create intermediate vectors of bindless resources in order +/// to satisfy `wgpu`'s lifetime requirements. +enum BindingResourceArray<'a> { + /// A list of bindings. + Buffers(Vec>), + /// A list of texture views. + TextureViews(Vec<&'a WgpuTextureView>), + /// A list of samplers. + Samplers(Vec<&'a WgpuSampler>), +} + +/// The location of a material (either bindless or non-bindless) within the +/// slabs. #[derive(Clone, Copy, Debug, Default, Reflect)] pub struct MaterialBindingId { /// The index of the bind group (slab) where the GPU data is located. pub group: MaterialBindGroupIndex, /// The slot within that bind group. + /// + /// Non-bindless materials will always have a slot of 0. pub slot: MaterialBindGroupSlot, } @@ -139,7 +260,7 @@ pub struct MaterialBindingId { /// /// In bindless mode, each bind group contains multiple materials. In /// non-bindless mode, each bind group contains only one material. -#[derive(Clone, Copy, Debug, Default, Reflect, PartialEq, Deref, DerefMut)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Reflect, Deref, DerefMut)] #[reflect(Default)] pub struct MaterialBindGroupIndex(pub u32); @@ -155,611 +276,475 @@ impl From for MaterialBindGroupIndex { /// In bindless mode, this slot is needed to locate the material data in each /// bind group, since multiple materials are packed into a single slab. In /// non-bindless mode, this slot is always 0. -#[derive(Clone, Copy, Debug, Default, Reflect, Deref, DerefMut)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Reflect, Deref, DerefMut)] #[reflect(Default)] -pub struct MaterialBindGroupSlot(pub u16); +pub struct MaterialBindGroupSlot(pub u32); + +/// The CPU/GPU synchronization state of a buffer that we maintain. +/// +/// Currently, the only buffer that we maintain is the +/// [`MaterialBindlessIndexTable`]. +enum BufferDirtyState { + /// The buffer is currently synchronized between the CPU and GPU. + Clean, + /// The buffer hasn't been created yet. + NeedsReserve, + /// The buffer exists on both CPU and GPU, but the GPU data is out of date. + NeedsUpload, +} + +/// Information that describes a potential allocation of an +/// [`UnpreparedBindGroup`] into a slab. +struct BindlessAllocationCandidate { + /// A map that, for every resource in the [`UnpreparedBindGroup`] that + /// already existed in this slab, maps bindless index of that resource to + /// its slot in the appropriate binding array. + pre_existing_resources: HashMap, + /// Stores the number of free slots that are needed to satisfy this + /// allocation. + needed_free_slots: u32, +} + +/// A trait that allows fetching the [`BindingResourceId`] from a +/// [`BindlessResourceType`]. +/// +/// This is used when freeing bindless resources, in order to locate the IDs +/// assigned to each resource so that they can be removed from the appropriate +/// maps. +trait GetBindingResourceId { + /// Returns the [`BindingResourceId`] for this resource. + /// + /// `resource_type` specifies this resource's type. This is used for + /// textures, as a `wgpu` [`TextureView`] doesn't store enough information + /// itself to determine its dimension. + fn binding_resource_id(&self, resource_type: BindlessResourceType) -> BindingResourceId; +} + +/// The public interface to a slab, which represents a single bind group. +pub struct MaterialSlab<'a, M>(MaterialSlabImpl<'a, M>) +where + M: Material; + +/// The actual implementation of a material slab. +/// +/// This has bindless and non-bindless variants. +enum MaterialSlabImpl<'a, M> +where + M: Material, +{ + /// The implementation of the slab interface we use when the slab + /// is bindless. + Bindless(&'a MaterialBindlessSlab), + /// The implementation of the slab interface we use when the slab + /// is non-bindless. + NonBindless(MaterialNonBindlessSlab<'a, M>), +} + +/// A single bind group that the [`MaterialBindGroupNonBindlessAllocator`] +/// manages. +struct MaterialNonBindlessSlab<'a, M>(&'a PreparedBindGroup) +where + M: Material; impl From for MaterialBindGroupSlot { fn from(value: u32) -> Self { - MaterialBindGroupSlot(value as u16) + MaterialBindGroupSlot(value) } } impl From for u32 { fn from(value: MaterialBindGroupSlot) -> Self { - value.0 as u32 + value.0 } } -/// A temporary data structure that contains references to bindless resources. -/// -/// We need this because the `wgpu` bindless API takes a slice of references. -/// Thus we need to create intermediate vectors of bindless resources in order -/// to satisfy the lifetime requirements. -enum BindingResourceArray<'a> { - Buffers(Vec>), - TextureViews(TextureViewDimension, Vec<&'a WgpuTextureView>), - Samplers(Vec<&'a WgpuSampler>), +impl<'a> From<&'a OwnedBindingResource> for BindingResourceId { + fn from(value: &'a OwnedBindingResource) -> Self { + match *value { + OwnedBindingResource::Buffer(ref buffer) => BindingResourceId::Buffer(buffer.id()), + OwnedBindingResource::TextureView(ref texture_view_dimension, ref texture_view) => { + BindingResourceId::TextureView(*texture_view_dimension, texture_view.id()) + } + OwnedBindingResource::Sampler(_, ref sampler) => { + BindingResourceId::Sampler(sampler.id()) + } + } + } } -/// Contains dummy resources that we use to pad out bindless arrays. -/// -/// On DX12, every binding array slot must be filled, so we have to fill unused -/// slots. -#[derive(Resource)] -pub struct FallbackBindlessResources { - /// A dummy sampler that we fill unused slots in bindless sampler arrays - /// with. - fallback_sampler: Sampler, +impl GetBindingResourceId for Buffer { + fn binding_resource_id(&self, _: BindlessResourceType) -> BindingResourceId { + BindingResourceId::Buffer(self.id()) + } } -struct MaterialFallbackBuffers(HashMap); +impl GetBindingResourceId for Sampler { + fn binding_resource_id(&self, _: BindlessResourceType) -> BindingResourceId { + BindingResourceId::Sampler(self.id()) + } +} -/// The minimum byte size of each fallback buffer. -const MIN_BUFFER_SIZE: u64 = 16; +impl GetBindingResourceId for TextureView { + fn binding_resource_id(&self, resource_type: BindlessResourceType) -> BindingResourceId { + let texture_view_dimension = match resource_type { + BindlessResourceType::Texture1d => TextureViewDimension::D1, + BindlessResourceType::Texture2d => TextureViewDimension::D2, + BindlessResourceType::Texture2dArray => TextureViewDimension::D2Array, + BindlessResourceType::Texture3d => TextureViewDimension::D3, + BindlessResourceType::TextureCube => TextureViewDimension::Cube, + BindlessResourceType::TextureCubeArray => TextureViewDimension::CubeArray, + _ => panic!("Resource type is not a texture"), + }; + BindingResourceId::TextureView(texture_view_dimension, self.id()) + } +} impl MaterialBindGroupAllocator where M: Material, { - /// Creates or recreates any bind groups that were modified this frame. + /// Creates a new [`MaterialBindGroupAllocator`] managing the data for a + /// single material. + fn new(render_device: &RenderDevice) -> MaterialBindGroupAllocator { + if material_uses_bindless_resources::(render_device) { + MaterialBindGroupAllocator::Bindless(Box::new(MaterialBindGroupBindlessAllocator::new( + render_device, + ))) + } else { + MaterialBindGroupAllocator::NonBindless(Box::new( + MaterialBindGroupNonBindlessAllocator::new(), + )) + } + } + + /// Returns the slab with the given index, if one exists. + pub fn get(&self, group: MaterialBindGroupIndex) -> Option> { + match *self { + MaterialBindGroupAllocator::Bindless(ref bindless_allocator) => bindless_allocator + .get(group) + .map(|bindless_slab| MaterialSlab(MaterialSlabImpl::Bindless(bindless_slab))), + MaterialBindGroupAllocator::NonBindless(ref non_bindless_allocator) => { + non_bindless_allocator.get(group).map(|non_bindless_slab| { + MaterialSlab(MaterialSlabImpl::NonBindless(non_bindless_slab)) + }) + } + } + } + + /// Allocates an [`UnpreparedBindGroup`] and returns the resulting binding ID. + /// + /// This method should generally be preferred over + /// [`Self::allocate_prepared`], because this method supports both bindless + /// and non-bindless bind groups. Only use [`Self::allocate_prepared`] if + /// you need to prepare the bind group yourself. + pub fn allocate_unprepared( + &mut self, + unprepared_bind_group: UnpreparedBindGroup, + bind_group_layout: &BindGroupLayout, + ) -> MaterialBindingId { + match *self { + MaterialBindGroupAllocator::Bindless( + ref mut material_bind_group_bindless_allocator, + ) => material_bind_group_bindless_allocator.allocate_unprepared(unprepared_bind_group), + MaterialBindGroupAllocator::NonBindless( + ref mut material_bind_group_non_bindless_allocator, + ) => material_bind_group_non_bindless_allocator + .allocate_unprepared(unprepared_bind_group, (*bind_group_layout).clone()), + } + } + + /// Places a pre-prepared bind group into a slab. + /// + /// For bindless materials, the allocator internally manages the bind + /// groups, so calling this method will panic if this is a bindless + /// allocator. Only non-bindless allocators support this method. + /// + /// It's generally preferred to use [`Self::allocate_unprepared`], because + /// that method supports both bindless and non-bindless allocators. Only use + /// this method if you need to prepare the bind group yourself. + pub fn allocate_prepared( + &mut self, + prepared_bind_group: PreparedBindGroup, + ) -> MaterialBindingId { + match *self { + MaterialBindGroupAllocator::Bindless(_) => { + panic!( + "Bindless resources are incompatible with implementing `as_bind_group` \ + directly; implement `unprepared_bind_group` instead or disable bindless" + ) + } + MaterialBindGroupAllocator::NonBindless(ref mut non_bindless_allocator) => { + non_bindless_allocator.allocate_prepared(prepared_bind_group) + } + } + } + + /// Deallocates the material with the given binding ID. + /// + /// Any resources that are no longer referenced are removed from the slab. + pub fn free(&mut self, material_binding_id: MaterialBindingId) { + match *self { + MaterialBindGroupAllocator::Bindless( + ref mut material_bind_group_bindless_allocator, + ) => material_bind_group_bindless_allocator.free(material_binding_id), + MaterialBindGroupAllocator::NonBindless( + ref mut material_bind_group_non_bindless_allocator, + ) => material_bind_group_non_bindless_allocator.free(material_binding_id), + } + } + + /// Recreates any bind groups corresponding to slabs that have been modified + /// since last calling [`MaterialBindGroupAllocator::prepare_bind_groups`]. pub fn prepare_bind_groups( &mut self, render_device: &RenderDevice, + fallback_bindless_resources: &FallbackBindlessResources, fallback_image: &FallbackImage, - fallback_resources: &FallbackBindlessResources, ) { - for bind_group in &mut self.bind_groups { - bind_group.rebuild_bind_group_if_necessary( + match *self { + MaterialBindGroupAllocator::Bindless( + ref mut material_bind_group_bindless_allocator, + ) => material_bind_group_bindless_allocator.prepare_bind_groups( + render_device, + fallback_bindless_resources, + fallback_image, + ), + MaterialBindGroupAllocator::NonBindless( + ref mut material_bind_group_non_bindless_allocator, + ) => material_bind_group_non_bindless_allocator.prepare_bind_groups(render_device), + } + } + + /// Uploads the contents of all buffers that this + /// [`MaterialBindGroupAllocator`] manages to the GPU. + /// + /// Non-bindless allocators don't currently manage any buffers, so this + /// method only has an effect for bindless allocators. + pub fn write_buffers(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + match *self { + MaterialBindGroupAllocator::Bindless( + ref mut material_bind_group_bindless_allocator, + ) => material_bind_group_bindless_allocator.write_buffers(render_device, render_queue), + MaterialBindGroupAllocator::NonBindless(_) => { + // Not applicable. + } + } + } +} + +impl MaterialBindlessIndexTable +where + M: Material, +{ + /// Creates a new [`MaterialBindlessIndexTable`] for a single slab. + fn new(bindless_descriptor: &BindlessDescriptor) -> MaterialBindlessIndexTable { + // Preallocate space for one bindings table, so that there will always be a buffer. + let mut buffer = RawBufferVec::new(BufferUsages::STORAGE); + for _ in 0..bindless_descriptor.resources.len() { + buffer.push(0); + } + + MaterialBindlessIndexTable { + buffer, + buffer_dirty: BufferDirtyState::NeedsReserve, + phantom: PhantomData, + } + } + + /// Returns the binding index table for a single material. + /// + /// Element *i* of the returned binding index table contains the slot of the + /// bindless resource with bindless index *i*. + fn get(&self, slot: MaterialBindGroupSlot, bindless_descriptor: &BindlessDescriptor) -> &[u32] { + let struct_size = bindless_descriptor.resources.len(); + let start = struct_size * slot.0 as usize; + &self.buffer.values()[start..(start + struct_size)] + } + + /// Updates the binding index table for a single material. + /// + /// The `allocated_resource_slots` map contains a mapping from the + /// [`BindlessIndex`] of each resource that the material references to the + /// slot that that resource occupies in the appropriate binding array. This + /// method serializes that map into a binding index table that the shader + /// can read. + fn set( + &mut self, + slot: MaterialBindGroupSlot, + allocated_resource_slots: &HashMap, + bindless_descriptor: &BindlessDescriptor, + ) { + let table_len = bindless_descriptor.resources.len(); + let range = (slot.0 as usize * table_len)..((slot.0 as usize + 1) * table_len); + while self.buffer.len() < range.end { + self.buffer.push(0); + } + + for (&bindless_index, &resource_slot) in allocated_resource_slots { + self.buffer + .set(*bindless_index + range.start as u32, resource_slot); + } + + // Mark the buffer as needing to be recreated, in case we grew it. + self.buffer_dirty = BufferDirtyState::NeedsReserve; + } + + /// Creates the buffer that contains the bindless index table if necessary. + fn prepare_buffer(&mut self, render_device: &RenderDevice) { + match self.buffer_dirty { + BufferDirtyState::Clean | BufferDirtyState::NeedsUpload => {} + BufferDirtyState::NeedsReserve => { + let capacity = self.buffer.len(); + self.buffer.reserve(capacity, render_device); + self.buffer_dirty = BufferDirtyState::NeedsUpload; + } + } + } + + /// Writes the contents of the bindless index table buffer to GPU if + /// necessary. + fn write_buffer(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + match self.buffer_dirty { + BufferDirtyState::Clean => {} + BufferDirtyState::NeedsReserve | BufferDirtyState::NeedsUpload => { + self.buffer.write_buffer(render_device, render_queue); + self.buffer_dirty = BufferDirtyState::Clean; + } + } + } +} + +impl MaterialBindGroupBindlessAllocator +where + M: Material, +{ + /// Creates a new [`MaterialBindGroupBindlessAllocator`] managing the data + /// for a single bindless material. + fn new(render_device: &RenderDevice) -> MaterialBindGroupBindlessAllocator { + let bindless_descriptor = M::bindless_descriptor() + .expect("Non-bindless materials should use the non-bindless allocator"); + let fallback_buffers = bindless_descriptor + .buffers + .iter() + .map(|bindless_buffer_descriptor| { + ( + bindless_buffer_descriptor.bindless_index, + render_device.create_buffer(&BufferDescriptor { + label: Some("bindless fallback buffer"), + size: bindless_buffer_descriptor.size as u64, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + ) + }) + .collect(); + + MaterialBindGroupBindlessAllocator { + slabs: vec![], + bind_group_layout: M::bind_group_layout(render_device), + bindless_descriptor, + fallback_buffers, + slab_capacity: M::bindless_slot_count() + .expect("Non-bindless materials should use the non-bindless allocator") + .resolve(), + } + } + + /// Allocates the resources for a single material into a slab and returns + /// the resulting ID. + /// + /// The returned [`MaterialBindingId`] can later be used to fetch the slab + /// that was used. + /// + /// This function can't fail. If all slabs are full, then a new slab is + /// created, and the material is allocated into it. + fn allocate_unprepared( + &mut self, + mut unprepared_bind_group: UnpreparedBindGroup, + ) -> MaterialBindingId { + for (slab_index, slab) in self.slabs.iter_mut().enumerate() { + trace!("Trying to allocate in slab {}", slab_index); + match slab.try_allocate( + unprepared_bind_group, + &self.bindless_descriptor, + self.slab_capacity, + ) { + Ok(slot) => { + return MaterialBindingId { + group: MaterialBindGroupIndex(slab_index as u32), + slot, + }; + } + Err(bind_group) => unprepared_bind_group = bind_group, + } + } + + let group = MaterialBindGroupIndex(self.slabs.len() as u32); + self.slabs + .push(MaterialBindlessSlab::new(&self.bindless_descriptor)); + + // Allocate into the newly-pushed slab. + let Ok(slot) = self + .slabs + .last_mut() + .expect("We just pushed a slab") + .try_allocate( + unprepared_bind_group, + &self.bindless_descriptor, + self.slab_capacity, + ) + else { + panic!("An allocation into an empty slab should always succeed") + }; + + MaterialBindingId { group, slot } + } + + /// Deallocates the material with the given binding ID. + /// + /// Any resources that are no longer referenced are removed from the slab. + fn free(&mut self, material_binding_id: MaterialBindingId) { + self.slabs + .get_mut(material_binding_id.group.0 as usize) + .expect("Slab should exist") + .free(material_binding_id.slot, &self.bindless_descriptor); + } + + /// Returns the slab with the given bind group index. + /// + /// A [`MaterialBindGroupIndex`] can be fetched from a + /// [`MaterialBindingId`]. + fn get(&self, group: MaterialBindGroupIndex) -> Option<&MaterialBindlessSlab> { + self.slabs.get(group.0 as usize) + } + + /// Recreates any bind groups corresponding to slabs that have been modified + /// since last calling + /// [`MaterialBindGroupBindlessAllocator::prepare_bind_groups`]. + fn prepare_bind_groups( + &mut self, + render_device: &RenderDevice, + fallback_bindless_resources: &FallbackBindlessResources, + fallback_image: &FallbackImage, + ) { + for slab in &mut self.slabs { + slab.prepare( render_device, &self.bind_group_layout, - fallback_image, - fallback_resources, + fallback_bindless_resources, &self.fallback_buffers, + fallback_image, + &self.bindless_descriptor, ); } } - /// Returns the bind group with the given index, if it exists. - #[inline] - pub fn get(&self, index: MaterialBindGroupIndex) -> Option<&MaterialBindGroup> { - self.bind_groups.get(index.0 as usize) - } - - /// Allocates a new binding slot and returns its ID. - pub 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, - } - } - - /// Assigns an unprepared bind group to the group and slot specified in the - /// [`MaterialBindingId`]. - pub fn init( - &mut self, - render_device: &RenderDevice, - material_binding_id: MaterialBindingId, - unprepared_bind_group: UnpreparedBindGroup, - ) { - self.bind_groups[material_binding_id.group.0 as usize].init( - render_device, - &self.bind_group_layout, - material_binding_id.slot, - unprepared_bind_group, - ); - } - - /// Fills a slot directly with a custom bind group. + /// Writes any buffers that we're managing to the GPU. /// - /// This is only a meaningful operation for non-bindless bind groups. It's - /// rarely used, but see the `texture_binding_array` example for an example - /// demonstrating how this feature might see use in practice. - pub fn init_custom( - &mut self, - material_binding_id: MaterialBindingId, - bind_group: BindGroup, - bind_group_data: M::Data, - ) { - self.bind_groups[material_binding_id.group.0 as usize] - .init_custom(bind_group, bind_group_data); - } - - /// Marks the slot corresponding to the given [`MaterialBindingId`] as free. - pub 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 the group that this material belonged to was full, it now contains - // at least one free slot, so add the group to the `free_bind_groups` - // list. - 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, -{ - /// Creates a new material bind group. - fn new(bindless: bool) -> MaterialBindGroup { - if bindless { - MaterialBindGroup::Bindless(MaterialBindlessBindGroup::new()) - } else { - MaterialBindGroup::NonBindless(MaterialNonBindlessBindGroup::new()) - } - } - - /// Allocates a new binding slot and returns its ID. - fn allocate(&mut self) -> MaterialBindGroupSlot { - match *self { - MaterialBindGroup::Bindless(ref mut material_bindless_bind_group) => { - material_bindless_bind_group.allocate() - } - MaterialBindGroup::NonBindless(ref mut material_non_bindless_bind_group) => { - material_non_bindless_bind_group.allocate() - } - } - } - - /// Assigns an unprepared bind group to the group and slot specified in the - /// [`MaterialBindingId`]. - fn init( - &mut self, - render_device: &RenderDevice, - bind_group_layout: &BindGroupLayout, - slot: MaterialBindGroupSlot, - unprepared_bind_group: UnpreparedBindGroup, - ) { - match *self { - MaterialBindGroup::Bindless(ref mut material_bindless_bind_group) => { - material_bindless_bind_group.init( - render_device, - bind_group_layout, - slot, - unprepared_bind_group, - ); - } - MaterialBindGroup::NonBindless(ref mut material_non_bindless_bind_group) => { - material_non_bindless_bind_group.init( - render_device, - bind_group_layout, - slot, - unprepared_bind_group, - ); - } - } - } - - /// Fills a slot directly with a custom bind group. - /// - /// This is only a meaningful operation for non-bindless bind groups. It's - /// rarely used, but see the `texture_binding_array` example for an example - /// demonstrating how this feature might see use in practice. - fn init_custom(&mut self, bind_group: BindGroup, extra_data: M::Data) { - match *self { - MaterialBindGroup::Bindless(_) => { - error!("Custom bind groups aren't supported in bindless mode"); - } - MaterialBindGroup::NonBindless(ref mut material_non_bindless_bind_group) => { - material_non_bindless_bind_group.init_custom(bind_group, extra_data); - } - } - } - - /// Marks the slot corresponding to the given [`MaterialBindGroupSlot`] as - /// free. - fn free(&mut self, material_bind_group_slot: MaterialBindGroupSlot) { - match *self { - MaterialBindGroup::Bindless(ref mut material_bindless_bind_group) => { - material_bindless_bind_group.free(material_bind_group_slot); - } - MaterialBindGroup::NonBindless(ref mut material_non_bindless_bind_group) => { - material_non_bindless_bind_group.free(material_bind_group_slot); - } - } - } - - /// Returns the actual bind group, or `None` if it hasn't been created yet. - pub fn get_bind_group(&self) -> Option<&BindGroup> { - match *self { - MaterialBindGroup::Bindless(ref material_bindless_bind_group) => { - material_bindless_bind_group.get_bind_group() - } - MaterialBindGroup::NonBindless(ref material_non_bindless_bind_group) => { - material_non_bindless_bind_group.get_bind_group() - } - } - } - - /// Returns true if all the slots are full or false if at least one slot in - /// this bind group is free. - fn is_full(&self) -> bool { - match *self { - MaterialBindGroup::Bindless(ref material_bindless_bind_group) => { - material_bindless_bind_group.is_full() - } - MaterialBindGroup::NonBindless(ref material_non_bindless_bind_group) => { - material_non_bindless_bind_group.is_full() - } - } - } - - /// Recreates the bind group for this material bind group containing the - /// data for every material in it. - 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, - ) { - match *self { - MaterialBindGroup::Bindless(ref mut material_bindless_bind_group) => { - material_bindless_bind_group.rebuild_bind_group_if_necessary( - render_device, - bind_group_layout, - fallback_image, - fallback_bindless_resources, - fallback_buffers, - ); - } - MaterialBindGroup::NonBindless(_) => {} - } - } - - /// Returns the associated extra data for the material with the given slot. - pub fn get_extra_data(&self, slot: MaterialBindGroupSlot) -> &M::Data { - match *self { - MaterialBindGroup::Bindless(ref material_bindless_bind_group) => { - material_bindless_bind_group.get_extra_data(slot) - } - MaterialBindGroup::NonBindless(ref material_non_bindless_bind_group) => { - material_non_bindless_bind_group.get_extra_data(slot) - } - } - } -} - -impl MaterialBindlessBindGroup -where - M: Material, -{ - /// Returns a new bind group. - fn new() -> MaterialBindlessBindGroup { - let count = M::bindless_slot_count().unwrap_or(1); - - MaterialBindlessBindGroup { - bind_group: None, - unprepared_bind_groups: iter::repeat_with(|| None).take(count as usize).collect(), - used_slot_bitmap: 0, - } - } - - /// Allocates a new slot and returns its index. - /// - /// This bind group must not be full. - fn allocate(&mut self) -> MaterialBindGroupSlot { - debug_assert!(!self.is_full()); - - // Mark the slot as used. - let slot = self.used_slot_bitmap.trailing_ones(); - self.used_slot_bitmap |= 1 << slot; - - slot.into() - } - - /// Assigns the given unprepared bind group to the given slot. - fn init( - &mut self, - _: &RenderDevice, - _: &BindGroupLayout, - slot: MaterialBindGroupSlot, - unprepared_bind_group: UnpreparedBindGroup, - ) { - self.unprepared_bind_groups[slot.0 as usize] = Some(unprepared_bind_group); - - // Invalidate the cached bind group so that we rebuild it again. - self.bind_group = None; - } - - /// Marks the given slot as free. - fn free(&mut self, slot: MaterialBindGroupSlot) { - self.unprepared_bind_groups[slot.0 as usize] = None; - self.used_slot_bitmap &= !(1 << slot.0); - - // Invalidate the cached bind group so that we rebuild it again. - self.bind_group = None; - } - - /// Returns true if all the slots are full or false if at least one slot in - /// this bind group is free. - fn is_full(&self) -> bool { - self.used_slot_bitmap == (1 << (self.unprepared_bind_groups.len() as u32)) - 1 - } - - /// Returns the actual bind group, or `None` if it hasn't been created yet. - fn get_bind_group(&self) -> Option<&BindGroup> { - self.bind_group.as_ref() - } - - /// Recreates the bind group for this material bind group containing the - /// data for every material in it. - 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, - ) { - 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; - }; - - // Creates the intermediate binding resource vectors. - let Some(binding_resource_arrays) = self.recreate_binding_resource_arrays( - first_bind_group, - fallback_image, - fallback_bindless_resources, - fallback_buffers, - ) else { - return; - }; - - // Now build the actual resource arrays for `wgpu`. - 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)); - } - - /// Recreates the binding arrays for each material in this bind group. - fn recreate_binding_resource_arrays<'a>( - &'a self, - first_bind_group: &'a UnpreparedBindGroup, - fallback_image: &'a FallbackImage, - fallback_bindless_resources: &'a FallbackBindlessResources, - fallback_buffers: &'a MaterialFallbackBuffers, - ) -> Option)>> { - // Initialize the arrays. - 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 => { - // Push dummy resources for this slot. - 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) => { - // Push the resources for this slot. - // - // All materials in this group must have the same type of - // binding (buffer, texture view, sampler) in each bind - // group entry. - for ( - binding_index, - (&mut (binding, ref mut binding_resource_array), (_, binding_resource)), - ) in binding_resource_arrays - .iter_mut() - .zip(unprepared_bind_group.bindings.0.iter()) - .enumerate() - { - 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 for material \ - {} at bind group {}; can't combine bind \ - groups into a single bindless bind group!", - any::type_name::(), - binding_index, - ); - return None; - } - } - } - } - } - } - - Some(binding_resource_arrays) - } - - /// Returns the associated extra data for the material with the given slot. - fn get_extra_data(&self, slot: MaterialBindGroupSlot) -> &M::Data { - &self.unprepared_bind_groups[slot.0 as usize] - .as_ref() - .unwrap() - .data - } -} - -impl MaterialNonBindlessBindGroup -where - M: Material, -{ - /// Creates a new material bind group. - fn new() -> MaterialNonBindlessBindGroup { - MaterialNonBindlessBindGroup { - allocation: MaterialNonBindlessBindGroupAllocation::Unallocated, - } - } - - /// Allocates a new slot and returns its index. - /// - /// This bind group must not be full. - fn allocate(&mut self) -> MaterialBindGroupSlot { - debug_assert!(!self.is_full()); - self.allocation = MaterialNonBindlessBindGroupAllocation::Allocated; - MaterialBindGroupSlot(0) - } - - /// Assigns an unprepared bind group to the group and slot specified in the - /// [`MaterialBindingId`]. - /// - /// For non-bindless bind groups, we go ahead and create the bind group - /// immediately. - fn init( - &mut self, - render_device: &RenderDevice, - bind_group_layout: &BindGroupLayout, - _: MaterialBindGroupSlot, - unprepared_bind_group: UnpreparedBindGroup, - ) { - let entries = unprepared_bind_group - .bindings - .iter() - .map(|(index, binding)| BindGroupEntry { - binding: *index, - resource: binding.get_binding(), - }) - .collect::>(); - - self.allocation = MaterialNonBindlessBindGroupAllocation::Initialized( - render_device.create_bind_group(M::label(), bind_group_layout, &entries), - unprepared_bind_group.data, - ); - } - - /// Fills the slot directly with a custom bind group. - /// - /// This is only a meaningful operation for non-bindless bind groups. It's - /// rarely used, but see the `texture_binding_array` example for an example - /// demonstrating how this feature might see use in practice. - fn init_custom(&mut self, bind_group: BindGroup, extra_data: M::Data) { - self.allocation = - MaterialNonBindlessBindGroupAllocation::Initialized(bind_group, extra_data); - } - - /// Deletes the stored bind group. - fn free(&mut self, _: MaterialBindGroupSlot) { - self.allocation = MaterialNonBindlessBindGroupAllocation::Unallocated; - } - - /// Returns true if the slot is full or false if it's free. - fn is_full(&self) -> bool { - !matches!( - self.allocation, - MaterialNonBindlessBindGroupAllocation::Unallocated - ) - } - - /// Returns the actual bind group, or `None` if it hasn't been created yet. - fn get_bind_group(&self) -> Option<&BindGroup> { - match self.allocation { - MaterialNonBindlessBindGroupAllocation::Unallocated - | MaterialNonBindlessBindGroupAllocation::Allocated => None, - MaterialNonBindlessBindGroupAllocation::Initialized(ref bind_group, _) => { - Some(bind_group) - } - } - } - - /// Returns the associated extra data for the material. - fn get_extra_data(&self, _: MaterialBindGroupSlot) -> &M::Data { - match self.allocation { - MaterialNonBindlessBindGroupAllocation::Initialized(_, ref extra_data) => extra_data, - MaterialNonBindlessBindGroupAllocation::Unallocated - | MaterialNonBindlessBindGroupAllocation::Allocated => { - panic!("Bind group not initialized") - } + /// Currently, this only consists of the bindless index tables. + fn write_buffers(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + for slab in &mut self.slabs { + slab.write_buffer(render_device, render_queue); } } } @@ -769,20 +754,672 @@ where M: Material, { fn from_world(world: &mut World) -> Self { - // Create a new bind group allocator. let render_device = world.resource::(); - let bind_group_layout_entries = M::bind_group_layout_entries(render_device, false); - 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![], + MaterialBindGroupAllocator::new(render_device) + } +} + +impl MaterialBindlessSlab +where + M: Material, +{ + /// Attempts to allocate the given unprepared bind group in this slab. + /// + /// If the allocation succeeds, this method returns the slot that the + /// allocation was placed in. If the allocation fails because the slab was + /// full, this method returns the unprepared bind group back to the caller + /// so that it can try to allocate again. + fn try_allocate( + &mut self, + unprepared_bind_group: UnpreparedBindGroup, + bindless_descriptor: &BindlessDescriptor, + slot_capacity: u32, + ) -> Result> { + // Locate pre-existing resources, and determine how many free slots we need. + let Some(allocation_candidate) = self.check_allocation(&unprepared_bind_group) else { + return Err(unprepared_bind_group); + }; + + // Check to see if we have enough free space. + // + // As a special case, note that if *nothing* is allocated in this slab, + // then we always allow a material to be placed in it, regardless of the + // number of bindings the material has. This is so that, if the + // platform's maximum bindless count is set too low to hold even a + // single material, we can still place each material into a separate + // slab instead of failing outright. + if self.allocated_resource_count > 0 + && self.allocated_resource_count + allocation_candidate.needed_free_slots + > slot_capacity + { + trace!("Slab is full, can't allocate"); + return Err(unprepared_bind_group); + } + + // OK, we can allocate in this slab. Assign a slot ID. + let slot = self + .free_slots + .pop() + .unwrap_or(MaterialBindGroupSlot(self.live_allocation_count)); + + // Bump the live allocation count. + self.live_allocation_count += 1; + + // Insert the resources into the binding arrays. + let allocated_resource_slots = + self.insert_resources(unprepared_bind_group.bindings, allocation_candidate); + + // Serialize the allocated resource slots. + self.bindless_index_table + .set(slot, &allocated_resource_slots, bindless_descriptor); + + // Insert extra data. + if self.extra_data.len() < (*slot as usize + 1) { + self.extra_data.resize_with(*slot as usize + 1, || None); + } + self.extra_data[*slot as usize] = Some(unprepared_bind_group.data); + + // Invalidate the cached bind group. + self.bind_group = None; + + Ok(slot) + } + + /// Gathers the information needed to determine whether the given unprepared + /// bind group can be allocated in this slab. + fn check_allocation( + &self, + unprepared_bind_group: &UnpreparedBindGroup, + ) -> Option { + let mut allocation_candidate = BindlessAllocationCandidate { + pre_existing_resources: HashMap::default(), + needed_free_slots: 0, + }; + + for &(bindless_index, ref owned_binding_resource) in unprepared_bind_group.bindings.iter() { + let bindless_index = BindlessIndex(bindless_index); + match *owned_binding_resource { + OwnedBindingResource::Buffer(ref buffer) => { + let Some(binding_array) = self.buffers.get(&bindless_index) else { + error!( + "Binding array wasn't present for buffer at index {:?}", + bindless_index + ); + return None; + }; + match binding_array.find(BindingResourceId::Buffer(buffer.id())) { + Some(slot) => { + allocation_candidate + .pre_existing_resources + .insert(bindless_index, slot); + } + None => allocation_candidate.needed_free_slots += 1, + } + } + + OwnedBindingResource::TextureView(texture_view_dimension, ref texture_view) => { + let bindless_resource_type = BindlessResourceType::from(texture_view_dimension); + match self + .textures + .get(&bindless_resource_type) + .expect("Missing binding array for texture") + .find(BindingResourceId::TextureView( + texture_view_dimension, + texture_view.id(), + )) { + Some(slot) => { + allocation_candidate + .pre_existing_resources + .insert(bindless_index, slot); + } + None => { + allocation_candidate.needed_free_slots += 1; + } + } + } + + OwnedBindingResource::Sampler(sampler_binding_type, ref sampler) => { + let bindless_resource_type = BindlessResourceType::from(sampler_binding_type); + match self + .samplers + .get(&bindless_resource_type) + .expect("Missing binding array for sampler") + .find(BindingResourceId::Sampler(sampler.id())) + { + Some(slot) => { + allocation_candidate + .pre_existing_resources + .insert(bindless_index, slot); + } + None => { + allocation_candidate.needed_free_slots += 1; + } + } + } + } + } + + Some(allocation_candidate) + } + + /// Inserts the given [`BindingResources`] into this slab. + /// + /// Returns a table that maps the bindless index of each resource to its + /// slot in its binding array. + fn insert_resources( + &mut self, + mut binding_resources: BindingResources, + allocation_candidate: BindlessAllocationCandidate, + ) -> HashMap { + let mut allocated_resource_slots = HashMap::default(); + + for (bindless_index, owned_binding_resource) in binding_resources.drain(..) { + let bindless_index = BindlessIndex(bindless_index); + // If this is an other reference to an object we've already + // allocated, just bump its reference count. + if let Some(pre_existing_resource_slot) = allocation_candidate + .pre_existing_resources + .get(&bindless_index) + { + allocated_resource_slots.insert(bindless_index, *pre_existing_resource_slot); + + match owned_binding_resource { + OwnedBindingResource::Buffer(_) => { + self.buffers + .get_mut(&bindless_index) + .expect("Buffer binding array should exist") + .bindings + .get_mut(*pre_existing_resource_slot as usize) + .and_then(|binding| binding.as_mut()) + .expect("Slot should exist") + .ref_count += 1; + } + OwnedBindingResource::TextureView(texture_view_dimension, _) => { + let bindless_resource_type = + BindlessResourceType::from(texture_view_dimension); + self.textures + .get_mut(&bindless_resource_type) + .expect("Texture binding array should exist") + .bindings + .get_mut(*pre_existing_resource_slot as usize) + .and_then(|binding| binding.as_mut()) + .expect("Slot should exist") + .ref_count += 1; + } + OwnedBindingResource::Sampler(sampler_binding_type, _) => { + let bindless_resource_type = + BindlessResourceType::from(sampler_binding_type); + self.samplers + .get_mut(&bindless_resource_type) + .expect("Sampler binding array should exist") + .bindings + .get_mut(*pre_existing_resource_slot as usize) + .and_then(|binding| binding.as_mut()) + .expect("Slot should exist") + .ref_count += 1; + } + } + + continue; + } + + // Otherwise, we need to insert it anew. + let binding_resource_id = BindingResourceId::from(&owned_binding_resource); + match owned_binding_resource { + OwnedBindingResource::Buffer(buffer) => { + let slot = self + .buffers + .get_mut(&bindless_index) + .expect("Buffer binding array should exist") + .insert(binding_resource_id, buffer); + allocated_resource_slots.insert(bindless_index, slot); + } + OwnedBindingResource::TextureView(texture_view_dimension, texture_view) => { + let bindless_resource_type = BindlessResourceType::from(texture_view_dimension); + let slot = self + .textures + .get_mut(&bindless_resource_type) + .expect("Texture array should exist") + .insert(binding_resource_id, texture_view); + allocated_resource_slots.insert(bindless_index, slot); + } + OwnedBindingResource::Sampler(sampler_binding_type, sampler) => { + let bindless_resource_type = BindlessResourceType::from(sampler_binding_type); + let slot = self + .samplers + .get_mut(&bindless_resource_type) + .expect("Sampler should exist") + .insert(binding_resource_id, sampler); + allocated_resource_slots.insert(bindless_index, slot); + } + } + + // Bump the allocated resource count. + self.allocated_resource_count += 1; + } + + allocated_resource_slots + } + + /// Removes the material allocated in the given slot, with the given + /// descriptor, from this slab. + fn free(&mut self, slot: MaterialBindGroupSlot, bindless_descriptor: &BindlessDescriptor) { + // Loop through each binding. + for (bindless_index, (bindless_resource_type, &bindless_binding)) in bindless_descriptor + .resources + .iter() + .zip(self.bindless_index_table.get(slot, bindless_descriptor)) + .enumerate() + { + let bindless_index = BindlessIndex::from(bindless_index as u32); + + // Free the binding. + let resource_freed = match *bindless_resource_type { + BindlessResourceType::None => false, + BindlessResourceType::Buffer => self + .buffers + .get_mut(&bindless_index) + .expect("Buffer should exist with that bindless index") + .remove(bindless_binding), + BindlessResourceType::SamplerFiltering + | BindlessResourceType::SamplerNonFiltering + | BindlessResourceType::SamplerComparison => self + .samplers + .get_mut(bindless_resource_type) + .expect("Sampler array should exist") + .remove(bindless_binding), + BindlessResourceType::Texture1d + | BindlessResourceType::Texture2d + | BindlessResourceType::Texture2dArray + | BindlessResourceType::Texture3d + | BindlessResourceType::TextureCube + | BindlessResourceType::TextureCubeArray => self + .textures + .get_mut(bindless_resource_type) + .expect("Texture array should exist") + .remove(bindless_binding), + }; + + // If the slot is now free, decrement the allocated resource + // count. + if resource_freed { + self.allocated_resource_count -= 1; + } + } + + // Clear out the extra data. + self.extra_data[slot.0 as usize] = None; + + // Invalidate the cached bind group. + self.bind_group = None; + + // Release the slot ID. + self.free_slots.push(slot); + self.live_allocation_count -= 1; + } + + /// Recreates the bind group and bindless index table buffer if necessary. + fn prepare( + &mut self, + render_device: &RenderDevice, + bind_group_layout: &BindGroupLayout, + fallback_bindless_resources: &FallbackBindlessResources, + fallback_buffers: &HashMap, + fallback_image: &FallbackImage, + bindless_descriptor: &BindlessDescriptor, + ) { + // Create the bindless index table buffer if needed. + self.bindless_index_table.prepare_buffer(render_device); + + // Create the bind group if needed. + self.prepare_bind_group( + render_device, bind_group_layout, + fallback_bindless_resources, fallback_buffers, - bindless_enabled: material_uses_bindless_resources::(render_device), - phantom: PhantomData, + fallback_image, + bindless_descriptor, + ); + } + + /// Recreates the bind group if this slab has been changed since the last + /// time we created it. + fn prepare_bind_group( + &mut self, + render_device: &RenderDevice, + bind_group_layout: &BindGroupLayout, + fallback_bindless_resources: &FallbackBindlessResources, + fallback_buffers: &HashMap, + fallback_image: &FallbackImage, + bindless_descriptor: &BindlessDescriptor, + ) { + // If the bind group is clean, then do nothing. + if self.bind_group.is_some() { + return; + } + + let binding_resource_arrays = self.create_binding_resource_arrays( + fallback_bindless_resources, + fallback_buffers, + fallback_image, + bindless_descriptor, + ); + + let mut bind_group_entries = vec![BindGroupEntry { + binding: 0, + resource: self + .bindless_index_table + .buffer + .buffer() + .expect("Bindings buffer must exist") + .as_entire_binding(), + }]; + + for &(&binding, ref binding_resource_array) in binding_resource_arrays.iter() { + bind_group_entries.push(BindGroupEntry { + binding, + resource: match *binding_resource_array { + BindingResourceArray::Buffers(ref buffer_bindings) => { + BindingResource::BufferArray(&buffer_bindings[..]) + } + BindingResourceArray::TextureViews(ref texture_views) => { + BindingResource::TextureViewArray(&texture_views[..]) + } + BindingResourceArray::Samplers(ref samplers) => { + BindingResource::SamplerArray(&samplers[..]) + } + }, + }); + } + + self.bind_group = Some(render_device.create_bind_group( + M::label(), + bind_group_layout, + &bind_group_entries, + )); + } + + /// Writes any buffers that we're managing to the GPU. + /// + /// Currently, this only consists of the bindless index table. + fn write_buffer(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + self.bindless_index_table + .write_buffer(render_device, render_queue); + } + + /// Converts our binding arrays into binding resource arrays suitable for + /// passing to `wgpu`. + fn create_binding_resource_arrays<'a>( + &'a self, + fallback_bindless_resources: &'a FallbackBindlessResources, + fallback_buffers: &'a HashMap, + fallback_image: &'a FallbackImage, + bindless_descriptor: &'a BindlessDescriptor, + ) -> Vec<(&'a u32, BindingResourceArray<'a>)> { + let mut binding_resource_arrays = vec![]; + + // Build sampler bindings. + self.create_sampler_binding_resource_arrays( + &mut binding_resource_arrays, + fallback_bindless_resources, + ); + + // Build texture bindings. + self.create_texture_binding_resource_arrays(&mut binding_resource_arrays, fallback_image); + + // Build buffer bindings. + self.create_buffer_binding_resource_arrays( + &mut binding_resource_arrays, + fallback_buffers, + bindless_descriptor, + ); + + binding_resource_arrays + } + + /// Accumulates sampler binding arrays into binding resource arrays suitable + /// for passing to `wgpu`. + fn create_sampler_binding_resource_arrays<'a, 'b>( + &'a self, + binding_resource_arrays: &'b mut Vec<(&'a u32, BindingResourceArray<'a>)>, + fallback_bindless_resources: &'a FallbackBindlessResources, + ) { + // We have one binding resource array per sampler type. + for (bindless_resource_type, fallback_sampler) in [ + ( + BindlessResourceType::SamplerFiltering, + &fallback_bindless_resources.filtering_sampler, + ), + ( + BindlessResourceType::SamplerNonFiltering, + &fallback_bindless_resources.non_filtering_sampler, + ), + ( + BindlessResourceType::SamplerComparison, + &fallback_bindless_resources.comparison_sampler, + ), + ] { + match self.samplers.get(&bindless_resource_type) { + Some(sampler_bindless_binding_array) => { + let sampler_bindings = sampler_bindless_binding_array + .bindings + .iter() + .map(|maybe_bindless_binding| match *maybe_bindless_binding { + Some(ref bindless_binding) => &bindless_binding.resource, + None => &**fallback_sampler, + }) + .collect(); + binding_resource_arrays.push(( + &*sampler_bindless_binding_array.binding_number, + BindingResourceArray::Samplers(sampler_bindings), + )); + } + + None => { + // Fill with a single fallback sampler. + let binding_number = bindless_resource_type + .binding_number() + .expect("Sampler bindless resource type must have a binding number"); + + binding_resource_arrays.push(( + &**binding_number, + BindingResourceArray::Samplers(vec![&**fallback_sampler]), + )); + } + } + } + } + + /// Accumulates texture binding arrays into binding resource arrays suitable + /// for passing to `wgpu`. + fn create_texture_binding_resource_arrays<'a, 'b>( + &'a self, + binding_resource_arrays: &'b mut Vec<(&'a u32, BindingResourceArray<'a>)>, + fallback_image: &'a FallbackImage, + ) { + for (bindless_resource_type, fallback_image) in [ + (BindlessResourceType::Texture1d, &fallback_image.d1), + (BindlessResourceType::Texture2d, &fallback_image.d2), + ( + BindlessResourceType::Texture2dArray, + &fallback_image.d2_array, + ), + (BindlessResourceType::Texture3d, &fallback_image.d3), + (BindlessResourceType::TextureCube, &fallback_image.cube), + ( + BindlessResourceType::TextureCubeArray, + &fallback_image.cube_array, + ), + ] { + match self.textures.get(&bindless_resource_type) { + Some(texture_bindless_binding_array) => { + let texture_bindings = texture_bindless_binding_array + .bindings + .iter() + .map(|maybe_bindless_binding| match *maybe_bindless_binding { + Some(ref bindless_binding) => &*bindless_binding.resource, + None => &*fallback_image.texture_view, + }) + .collect(); + binding_resource_arrays.push(( + &*texture_bindless_binding_array.binding_number, + BindingResourceArray::TextureViews(texture_bindings), + )); + } + + None => { + // Fill with a single fallback image. + let binding_number = bindless_resource_type + .binding_number() + .expect("Texture bindless resource type must have a binding number"); + + binding_resource_arrays.push(( + binding_number, + BindingResourceArray::TextureViews(vec![&*fallback_image.texture_view]), + )); + } + } + } + } + + /// Accumulates buffer binding arrays into binding resource arrays suitable + /// for `wgpu`. + fn create_buffer_binding_resource_arrays<'a, 'b>( + &'a self, + binding_resource_arrays: &'b mut Vec<(&'a u32, BindingResourceArray<'a>)>, + fallback_buffers: &'a HashMap, + bindless_descriptor: &'a BindlessDescriptor, + ) { + for bindless_buffer_descriptor in bindless_descriptor.buffers.iter() { + let Some(buffer_bindless_binding_array) = + self.buffers.get(&bindless_buffer_descriptor.bindless_index) + else { + error!( + "Slab didn't contain a binding array for buffer binding {:?}, bindless {:?}", + bindless_buffer_descriptor.binding_number, + bindless_buffer_descriptor.bindless_index, + ); + continue; + }; + let buffer_bindings = buffer_bindless_binding_array + .bindings + .iter() + .map(|maybe_bindless_binding| { + let buffer = match *maybe_bindless_binding { + None => fallback_buffers + .get(&bindless_buffer_descriptor.bindless_index) + .expect("Fallback buffer should exist"), + Some(ref bindless_binding) => &bindless_binding.resource, + }; + BufferBinding { + buffer, + offset: 0, + size: None, + } + }) + .collect(); + binding_resource_arrays.push(( + &*buffer_bindless_binding_array.binding_number, + BindingResourceArray::Buffers(buffer_bindings), + )); + } + } + + /// Returns the [`BindGroup`] corresponding to this slab, if it's been + /// prepared. + fn bind_group(&self) -> Option<&BindGroup> { + self.bind_group.as_ref() + } + + /// Returns the extra data associated with this material. + fn get_extra_data(&self, slot: MaterialBindGroupSlot) -> &M::Data { + self.extra_data + .get(slot.0 as usize) + .and_then(|data| data.as_ref()) + .expect("Extra data not present") + } +} + +impl MaterialBindlessBindingArray +where + R: GetBindingResourceId, +{ + /// Creates a new [`MaterialBindlessBindingArray`] with the given binding + /// number, managing resources of the given type. + fn new( + binding_number: BindingNumber, + resource_type: BindlessResourceType, + ) -> MaterialBindlessBindingArray { + MaterialBindlessBindingArray { + binding_number, + bindings: vec![], + resource_type, + resource_to_slot: HashMap::default(), + free_slots: vec![], + len: 0, + } + } + + /// Returns the slot corresponding to the given resource, if that resource + /// is located in this binding array. + /// + /// If the resource isn't in this binding array, this method returns `None`. + fn find(&self, binding_resource_id: BindingResourceId) -> Option { + self.resource_to_slot.get(&binding_resource_id).copied() + } + + /// Inserts a bindless resource into a binding array and returns the index + /// of the slot it was inserted into. + fn insert(&mut self, binding_resource_id: BindingResourceId, resource: R) -> u32 { + let slot = self.free_slots.pop().unwrap_or(self.len); + self.resource_to_slot.insert(binding_resource_id, slot); + + if self.bindings.len() < slot as usize + 1 { + self.bindings.resize_with(slot as usize + 1, || None); + } + self.bindings[slot as usize] = Some(MaterialBindlessBinding::new(resource)); + + self.len += 1; + slot + } + + /// Removes a reference to an object from the slot. + /// + /// If the reference count dropped to 0 and the object was freed, this + /// method returns true. If the object was still referenced after removing + /// it, returns false. + fn remove(&mut self, slot: u32) -> bool { + let maybe_binding = &mut self.bindings[slot as usize]; + let binding = maybe_binding + .as_mut() + .expect("Attempted to free an already-freed binding"); + + binding.ref_count -= 1; + if binding.ref_count != 0 { + return false; + } + + let binding_resource_id = binding.resource.binding_resource_id(self.resource_type); + self.resource_to_slot.remove(&binding_resource_id); + + *maybe_binding = None; + self.free_slots.push(slot); + self.len -= 1; + true + } +} + +impl MaterialBindlessBinding +where + R: GetBindingResourceId, +{ + /// Creates a new [`MaterialBindlessBinding`] for a freshly-added resource. + /// + /// The reference count is initialized to 1. + fn new(resource: R) -> MaterialBindlessBinding { + MaterialBindlessBinding { + resource, + ref_count: 1, } } } @@ -796,54 +1433,274 @@ pub fn material_uses_bindless_resources(render_device: &RenderDevice) -> bool where M: Material, { - M::bindless_slot_count().is_some() && M::bindless_supported(render_device) + M::bindless_slot_count().is_some_and(|bindless_slot_count| { + M::bindless_supported(render_device) && bindless_slot_count.resolve() > 1 + }) +} + +impl MaterialBindlessSlab +where + M: Material, +{ + /// Creates a new [`MaterialBindlessSlab`] for a material with the given + /// bindless descriptor. + /// + /// We use this when no existing slab could hold a material to be allocated. + fn new(bindless_descriptor: &BindlessDescriptor) -> MaterialBindlessSlab { + let mut buffers = HashMap::default(); + let mut samplers = HashMap::default(); + let mut textures = HashMap::default(); + + for (bindless_index, bindless_resource_type) in + bindless_descriptor.resources.iter().enumerate() + { + let bindless_index = BindlessIndex(bindless_index as u32); + match *bindless_resource_type { + BindlessResourceType::None => {} + BindlessResourceType::Buffer => { + let binding_number = bindless_descriptor + .buffers + .iter() + .find(|bindless_buffer_descriptor| { + bindless_buffer_descriptor.bindless_index == bindless_index + }) + .expect( + "Bindless buffer descriptor matching that bindless index should be \ + present", + ) + .binding_number; + buffers.insert( + bindless_index, + MaterialBindlessBindingArray::new(binding_number, *bindless_resource_type), + ); + } + BindlessResourceType::SamplerFiltering + | BindlessResourceType::SamplerNonFiltering + | BindlessResourceType::SamplerComparison => { + samplers.insert( + *bindless_resource_type, + MaterialBindlessBindingArray::new( + *bindless_resource_type.binding_number().unwrap(), + *bindless_resource_type, + ), + ); + } + BindlessResourceType::Texture1d + | BindlessResourceType::Texture2d + | BindlessResourceType::Texture2dArray + | BindlessResourceType::Texture3d + | BindlessResourceType::TextureCube + | BindlessResourceType::TextureCubeArray => { + textures.insert( + *bindless_resource_type, + MaterialBindlessBindingArray::new( + *bindless_resource_type.binding_number().unwrap(), + *bindless_resource_type, + ), + ); + } + } + } + + MaterialBindlessSlab { + bind_group: None, + bindless_index_table: MaterialBindlessIndexTable::new(bindless_descriptor), + samplers, + textures, + buffers, + extra_data: vec![], + free_slots: vec![], + live_allocation_count: 0, + allocated_resource_count: 0, + } + } } 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"), + filtering_sampler: render_device.create_sampler(&SamplerDescriptor { + label: Some("fallback filtering sampler"), + ..default() + }), + non_filtering_sampler: render_device.create_sampler(&SamplerDescriptor { + label: Some("fallback non-filtering sampler"), + mag_filter: FilterMode::Nearest, + min_filter: FilterMode::Nearest, + mipmap_filter: FilterMode::Nearest, + ..default() + }), + comparison_sampler: render_device.create_sampler(&SamplerDescriptor { + label: Some("fallback comparison sampler"), + compare: Some(CompareFunction::Always), ..default() }), } } } -impl MaterialFallbackBuffers { - /// Creates a new set of fallback buffers containing dummy allocations. - /// - /// We populate unused bind group slots with these. - fn new( - render_device: &RenderDevice, - bind_group_layout_entries: &[BindGroupLayoutEntry], - ) -> MaterialFallbackBuffers { - let mut fallback_buffers = HashMap::default(); - for bind_group_layout_entry in bind_group_layout_entries { - // Create a dummy buffer of the appropriate size. - 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); +impl MaterialBindGroupNonBindlessAllocator +where + M: Material, +{ + /// Creates a new [`MaterialBindGroupNonBindlessAllocator`] managing the + /// bind groups for a single non-bindless material. + fn new() -> MaterialBindGroupNonBindlessAllocator { + MaterialBindGroupNonBindlessAllocator { + bind_groups: vec![], + to_prepare: HashSet::default(), + free_indices: vec![], + phantom: PhantomData, + } + } - 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, + /// Inserts a bind group, either unprepared or prepared, into this allocator + /// and returns a [`MaterialBindingId`]. + /// + /// The returned [`MaterialBindingId`] can later be used to fetch the bind + /// group. + fn allocate( + &mut self, + bind_group: MaterialNonBindlessAllocatedBindGroup, + ) -> MaterialBindingId { + let group_id = self + .free_indices + .pop() + .unwrap_or(MaterialBindGroupIndex(self.bind_groups.len() as u32)); + if self.bind_groups.len() < *group_id as usize + 1 { + self.bind_groups + .resize_with(*group_id as usize + 1, || None); + } + + if matches!( + bind_group, + MaterialNonBindlessAllocatedBindGroup::Unprepared { .. } + ) { + self.to_prepare.insert(group_id); + } + + self.bind_groups[*group_id as usize] = Some(bind_group); + + MaterialBindingId { + group: group_id, + slot: default(), + } + } + + /// Inserts an unprepared bind group into this allocator and returns a + /// [`MaterialBindingId`]. + fn allocate_unprepared( + &mut self, + unprepared_bind_group: UnpreparedBindGroup, + bind_group_layout: BindGroupLayout, + ) -> MaterialBindingId { + self.allocate(MaterialNonBindlessAllocatedBindGroup::Unprepared { + bind_group: unprepared_bind_group, + layout: bind_group_layout, + }) + } + + /// Inserts an prepared bind group into this allocator and returns a + /// [`MaterialBindingId`]. + fn allocate_prepared( + &mut self, + prepared_bind_group: PreparedBindGroup, + ) -> MaterialBindingId { + self.allocate(MaterialNonBindlessAllocatedBindGroup::Prepared( + prepared_bind_group, + )) + } + + /// Deallocates the bind group with the given binding ID. + fn free(&mut self, binding_id: MaterialBindingId) { + debug_assert_eq!(binding_id.slot, MaterialBindGroupSlot(0)); + debug_assert!(self.bind_groups[*binding_id.group as usize].is_none()); + self.bind_groups[*binding_id.group as usize] = None; + self.to_prepare.remove(&binding_id.group); + self.free_indices.push(binding_id.group); + } + + /// Returns a wrapper around the bind group with the given index. + fn get(&self, group: MaterialBindGroupIndex) -> Option> { + match self.bind_groups[group.0 as usize] { + Some(MaterialNonBindlessAllocatedBindGroup::Prepared(ref prepared_bind_group)) => { + Some(MaterialNonBindlessSlab(prepared_bind_group)) + } + Some(MaterialNonBindlessAllocatedBindGroup::Unprepared { .. }) | None => None, + } + } + + /// Prepares any as-yet unprepared bind groups that this allocator is + /// managing. + /// + /// Unprepared bind groups can be added to this allocator with + /// [`Self::allocate_unprepared`]. Such bind groups will defer being + /// prepared until the next time this method is called. + fn prepare_bind_groups(&mut self, render_device: &RenderDevice) { + for bind_group_index in mem::take(&mut self.to_prepare) { + let Some(MaterialNonBindlessAllocatedBindGroup::Unprepared { + bind_group: unprepared_bind_group, + layout: bind_group_layout, + }) = mem::take(&mut self.bind_groups[*bind_group_index as usize]) + else { + panic!("Allocation didn't exist or was already prepared"); + }; + + let entries: Vec<_> = unprepared_bind_group + .bindings + .iter() + .map(|(index, binding)| BindGroupEntry { + binding: *index, + resource: binding.get_binding(), + }) + .collect(); + + let bind_group = + render_device.create_bind_group(M::label(), &bind_group_layout, &entries); + + self.bind_groups[*bind_group_index as usize] = Some( + MaterialNonBindlessAllocatedBindGroup::Prepared(PreparedBindGroup { + bindings: unprepared_bind_group.bindings, + bind_group, + data: unprepared_bind_group.data, }), ); } - - MaterialFallbackBuffers(fallback_buffers) + } +} + +impl<'a, M> MaterialSlab<'a, M> +where + M: Material, +{ + /// Returns the extra data associated with this material. + /// + /// When deriving `AsBindGroup`, this data is given by the + /// `#[bind_group_data(DataType)]` attribute on the material structure. + pub fn get_extra_data(&self, slot: MaterialBindGroupSlot) -> &M::Data { + match self.0 { + MaterialSlabImpl::Bindless(material_bindless_slab) => { + material_bindless_slab.get_extra_data(slot) + } + MaterialSlabImpl::NonBindless(ref prepared_bind_group) => &prepared_bind_group.0.data, + } + } + + /// Returns the [`BindGroup`] corresponding to this slab, if it's been + /// prepared. + /// + /// You can prepare bind groups by calling + /// [`MaterialBindGroupAllocator::prepare_bind_groups`]. If the bind group + /// isn't ready, this method returns `None`. + pub fn bind_group(&self) -> Option<&'a BindGroup> { + match self.0 { + MaterialSlabImpl::Bindless(material_bindless_slab) => { + material_bindless_slab.bind_group() + } + MaterialSlabImpl::NonBindless(ref prepared_bind_group) => { + Some(&prepared_bind_group.0.bind_group) + } + } } } diff --git a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs index 0e71d5a9d9..2351c7756d 100644 --- a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs +++ b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs @@ -228,7 +228,7 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( else { continue; }; - let Some(bind_group) = material_bind_group.get_bind_group() else { + let Some(bind_group) = material_bind_group.bind_group() else { continue; }; @@ -399,7 +399,7 @@ pub fn prepare_material_meshlet_meshes_prepass( else { continue; }; - let Some(bind_group) = material_bind_group.get_bind_group() else { + let Some(bind_group) = material_bind_group.bind_group() else { continue; }; diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 48353eb67c..44d1b960df 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -30,8 +30,8 @@ pub enum UvChannel { /// May be created directly from a [`Color`] or an [`Image`]. #[derive(Asset, AsBindGroup, Reflect, Debug, Clone)] #[bind_group_data(StandardMaterialKey)] -#[uniform(0, StandardMaterialUniform)] -#[bindless(16)] +#[uniform(0, StandardMaterialUniform, binding_array(10))] +#[bindless] #[reflect(Default, Debug)] pub struct StandardMaterial { /// The color of the surface of the material before lighting. diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index c8902145c8..9b3420cbc2 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -46,7 +46,7 @@ use bevy_render::{ Extract, }; use bevy_transform::prelude::GlobalTransform; -use tracing::error; +use tracing::{error, warn}; #[cfg(feature = "meshlet")] use crate::meshlet::{ @@ -980,6 +980,7 @@ pub fn specialize_prepass_material_meshes( let Some(material_bind_group) = material_bind_group_allocator.get(material.binding.group) else { + warn!("Couldn't get bind group for material"); continue; }; let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { diff --git a/crates/bevy_pbr/src/render/parallax_mapping.wgsl b/crates/bevy_pbr/src/render/parallax_mapping.wgsl index 780b5c290a..9005734da5 100644 --- a/crates/bevy_pbr/src/render/parallax_mapping.wgsl +++ b/crates/bevy_pbr/src/render/parallax_mapping.wgsl @@ -1,10 +1,16 @@ #define_import_path bevy_pbr::parallax_mapping +#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d} + #import bevy_pbr::{ pbr_bindings::{depth_map_texture, depth_map_sampler}, mesh_bindings::mesh } +#ifdef BINDLESS +#import bevy_pbr::pbr_bindings::material_indices +#endif // BINDLESS + fn sample_depth_map(uv: vec2, material_bind_group_slot: u32) -> f32 { // We use `textureSampleLevel` over `textureSample` because the wgpu DX12 // backend (Fxc) panics when using "gradient instructions" inside a loop. @@ -18,8 +24,8 @@ fn sample_depth_map(uv: vec2, material_bind_group_slot: u32) -> f32 { // See https://stackoverflow.com/questions/56581141/direct3d11-gradient-instruction-used-in-a-loop-with-varying-iteration-forcing return textureSampleLevel( #ifdef BINDLESS - depth_map_texture[material_bind_group_slot], - depth_map_sampler[material_bind_group_slot], + bindless_textures_2d[material_indices[material_bind_group_slot].depth_map_texture], + bindless_samplers_filtering[material_indices[material_bind_group_slot].depth_map_sampler], #else // BINDLESS depth_map_texture, depth_map_sampler, diff --git a/crates/bevy_pbr/src/render/pbr_bindings.wgsl b/crates/bevy_pbr/src/render/pbr_bindings.wgsl index d6514acfa9..1e77cba985 100644 --- a/crates/bevy_pbr/src/render/pbr_bindings.wgsl +++ b/crates/bevy_pbr/src/render/pbr_bindings.wgsl @@ -3,20 +3,53 @@ #import bevy_pbr::pbr_types::StandardMaterial #ifdef BINDLESS -@group(2) @binding(0) var material: binding_array; -@group(2) @binding(1) var base_color_texture: binding_array, 16>; -@group(2) @binding(2) var base_color_sampler: binding_array; -@group(2) @binding(3) var emissive_texture: binding_array, 16>; -@group(2) @binding(4) var emissive_sampler: binding_array; -@group(2) @binding(5) var metallic_roughness_texture: binding_array, 16>; -@group(2) @binding(6) var metallic_roughness_sampler: binding_array; -@group(2) @binding(7) var occlusion_texture: binding_array, 16>; -@group(2) @binding(8) var occlusion_sampler: binding_array; -@group(2) @binding(9) var normal_map_texture: binding_array, 16>; -@group(2) @binding(10) var normal_map_sampler: binding_array; -@group(2) @binding(11) var depth_map_texture: binding_array, 16>; -@group(2) @binding(12) var depth_map_sampler: binding_array; +struct StandardMaterialBindings { + material: u32, // 0 + base_color_texture: u32, // 1 + base_color_sampler: u32, // 2 + emissive_texture: u32, // 3 + emissive_sampler: u32, // 4 + metallic_roughness_texture: u32, // 5 + metallic_roughness_sampler: u32, // 6 + occlusion_texture: u32, // 7 + occlusion_sampler: u32, // 8 + normal_map_texture: u32, // 9 + normal_map_sampler: u32, // 10 + depth_map_texture: u32, // 11 + depth_map_sampler: u32, // 12 +#ifdef PBR_ANISOTROPY_TEXTURE_SUPPORTED + anisotropy_texture: u32, // 13 + anisotropy_sampler: u32, // 14 +#endif // PBR_ANISOTROPY_TEXTURE_SUPPORTED +#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED + specular_transmission_texture: u32, // 15 + specular_transmission_sampler: u32, // 16 + thickness_texture: u32, // 17 + thickness_sampler: u32, // 18 + diffuse_transmission_texture: u32, // 19 + diffuse_transmission_sampler: u32, // 20 +#endif // PBR_TRANSMISSION_TEXTURES_SUPPORTED +#ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED + clearcoat_texture: u32, // 21 + clearcoat_sampler: u32, // 22 + clearcoat_roughness_texture: u32, // 23 + clearcoat_roughness_sampler: u32, // 24 + clearcoat_normal_texture: u32, // 25 + clearcoat_normal_sampler: u32, // 26 +#endif // PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED +#ifdef PBR_SPECULAR_TEXTURES_SUPPORTED + specular_texture: u32, // 27 + specular_sampler: u32, // 28 + specular_tint_texture: u32, // 29 + specular_tint_sampler: u32, // 30 +#endif // PBR_SPECULAR_TEXTURES_SUPPORTED +} + +@group(2) @binding(0) var material_indices: array; +@group(2) @binding(10) var material_array: binding_array; + #else // BINDLESS + @group(2) @binding(0) var material: StandardMaterial; @group(2) @binding(1) var base_color_texture: texture_2d; @group(2) @binding(2) var base_color_sampler: sampler; @@ -30,64 +63,35 @@ @group(2) @binding(10) var normal_map_sampler: sampler; @group(2) @binding(11) var depth_map_texture: texture_2d; @group(2) @binding(12) var depth_map_sampler: sampler; -#endif // BINDLESS #ifdef PBR_ANISOTROPY_TEXTURE_SUPPORTED -#ifdef BINDLESS -@group(2) @binding(13) var anisotropy_texture: binding_array, 16>; -@group(2) @binding(14) var anisotropy_sampler: binding_array; -#else // BINDLESS @group(2) @binding(13) var anisotropy_texture: texture_2d; @group(2) @binding(14) var anisotropy_sampler: sampler; -#endif // BINDLESS #endif // PBR_ANISOTROPY_TEXTURE_SUPPORTED #ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED -#ifdef BINDLESS -@group(2) @binding(15) var specular_transmission_texture: binding_array, 16>; -@group(2) @binding(16) var specular_transmission_sampler: binding_array; -@group(2) @binding(17) var thickness_texture: binding_array, 16>; -@group(2) @binding(18) var thickness_sampler: binding_array; -@group(2) @binding(19) var diffuse_transmission_texture: binding_array, 16>; -@group(2) @binding(20) var diffuse_transmission_sampler: binding_array; -#else // BINDLESS @group(2) @binding(15) var specular_transmission_texture: texture_2d; @group(2) @binding(16) var specular_transmission_sampler: sampler; @group(2) @binding(17) var thickness_texture: texture_2d; @group(2) @binding(18) var thickness_sampler: sampler; @group(2) @binding(19) var diffuse_transmission_texture: texture_2d; @group(2) @binding(20) var diffuse_transmission_sampler: sampler; -#endif // BINDLESS #endif // PBR_TRANSMISSION_TEXTURES_SUPPORTED #ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED -#ifdef BINDLESS -@group(2) @binding(21) var clearcoat_texture: binding_array, 16>; -@group(2) @binding(22) var clearcoat_sampler: binding_array; -@group(2) @binding(23) var clearcoat_roughness_texture: binding_array, 16>; -@group(2) @binding(24) var clearcoat_roughness_sampler: binding_array; -@group(2) @binding(25) var clearcoat_normal_texture: binding_array, 16>; -@group(2) @binding(26) var clearcoat_normal_sampler: binding_array; -#else // BINDLESS @group(2) @binding(21) var clearcoat_texture: texture_2d; @group(2) @binding(22) var clearcoat_sampler: sampler; @group(2) @binding(23) var clearcoat_roughness_texture: texture_2d; @group(2) @binding(24) var clearcoat_roughness_sampler: sampler; @group(2) @binding(25) var clearcoat_normal_texture: texture_2d; @group(2) @binding(26) var clearcoat_normal_sampler: sampler; -#endif // BINDLESS #endif // PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED #ifdef PBR_SPECULAR_TEXTURES_SUPPORTED -#ifdef BINDLESS -@group(2) @binding(27) var specular_texture: binding_array, 16>; -@group(2) @binding(28) var specular_sampler: binding_array; -@group(2) @binding(29) var specular_tint_texture: binding_array, 16>; -@group(2) @binding(30) var specular_tint_sampler: binding_array; -#else @group(2) @binding(27) var specular_texture: texture_2d; @group(2) @binding(28) var specular_sampler: sampler; @group(2) @binding(29) var specular_tint_texture: texture_2d; @group(2) @binding(30) var specular_tint_sampler: sampler; -#endif // BINDLESS #endif // PBR_SPECULAR_TEXTURES_SUPPORTED + +#endif // BINDLESS diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index 18bbf82e68..779546f8bd 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -1,5 +1,7 @@ #define_import_path bevy_pbr::pbr_fragment +#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d} + #import bevy_pbr::{ pbr_functions, pbr_functions::SampleBias, @@ -26,6 +28,10 @@ #import bevy_pbr::forward_io::VertexOutput #endif +#ifdef BINDLESS +#import bevy_pbr::pbr_bindings::material_indices +#endif // BINDLESS + // prepare a basic PbrInput from the vertex stage output, mesh binding and view binding fn pbr_input_from_vertex_output( in: VertexOutput, @@ -76,9 +82,10 @@ fn pbr_input_from_standard_material( let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - let flags = pbr_bindings::material[slot].flags; - let base_color = pbr_bindings::material[slot].base_color; - let deferred_lighting_pass_id = pbr_bindings::material[slot].deferred_lighting_pass_id; + let flags = pbr_bindings::material_array[material_indices[slot].material].flags; + let base_color = pbr_bindings::material_array[material_indices[slot].material].base_color; + let deferred_lighting_pass_id = + pbr_bindings::material_array[material_indices[slot].material].deferred_lighting_pass_id; #else // BINDLESS let flags = pbr_bindings::material.flags; let base_color = pbr_bindings::material.base_color; @@ -108,7 +115,7 @@ fn pbr_input_from_standard_material( #ifdef VERTEX_UVS #ifdef BINDLESS - let uv_transform = pbr_bindings::material[slot].uv_transform; + let uv_transform = pbr_bindings::material_array[material_indices[slot].material].uv_transform; #else // BINDLESS let uv_transform = pbr_bindings::material.uv_transform; #endif // BINDLESS @@ -137,9 +144,9 @@ fn pbr_input_from_standard_material( // TODO: Transforming UVs mean we need to apply derivative chain rule for meshlet mesh material pass uv = parallaxed_uv( #ifdef BINDLESS - pbr_bindings::material[slot].parallax_depth_scale, - pbr_bindings::material[slot].max_parallax_layer_count, - pbr_bindings::material[slot].max_relief_mapping_search_steps, + pbr_bindings::material_array[material_indices[slot].material].parallax_depth_scale, + pbr_bindings::material_array[material_indices[slot].material].max_parallax_layer_count, + pbr_bindings::material_array[material_indices[slot].material].max_relief_mapping_search_steps, #else // BINDLESS pbr_bindings::material.parallax_depth_scale, pbr_bindings::material.max_parallax_layer_count, @@ -158,9 +165,9 @@ fn pbr_input_from_standard_material( // TODO: Transforming UVs mean we need to apply derivative chain rule for meshlet mesh material pass uv_b = parallaxed_uv( #ifdef BINDLESS - pbr_bindings::material[slot].parallax_depth_scale, - pbr_bindings::material[slot].max_parallax_layer_count, - pbr_bindings::material[slot].max_relief_mapping_search_steps, + pbr_bindings::material_array[material_indices[slot].material].parallax_depth_scale, + pbr_bindings::material_array[material_indices[slot].material].max_parallax_layer_count, + pbr_bindings::material_array[material_indices[slot].material].max_relief_mapping_search_steps, #else // BINDLESS pbr_bindings::material.parallax_depth_scale, pbr_bindings::material.max_parallax_layer_count, @@ -187,8 +194,8 @@ fn pbr_input_from_standard_material( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::base_color_texture[slot], - pbr_bindings::base_color_sampler[slot], + bindless_textures_2d[material_indices[slot].base_color_texture], + bindless_samplers_filtering[material_indices[slot].base_color_sampler], #else // BINDLESS pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, @@ -214,7 +221,7 @@ fn pbr_input_from_standard_material( if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE { #ifdef BINDLESS - let alpha_cutoff = pbr_bindings::material[slot].alpha_cutoff; + let alpha_cutoff = pbr_bindings::material_array[material_indices[slot].material].alpha_cutoff; #else // BINDLESS let alpha_cutoff = pbr_bindings::material.alpha_cutoff; #endif // BINDLESS @@ -232,10 +239,13 @@ fn pbr_input_from_standard_material( // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) { #ifdef BINDLESS - pbr_input.material.ior = pbr_bindings::material[slot].ior; - pbr_input.material.attenuation_color = pbr_bindings::material[slot].attenuation_color; - pbr_input.material.attenuation_distance = pbr_bindings::material[slot].attenuation_distance; - pbr_input.material.alpha_cutoff = pbr_bindings::material[slot].alpha_cutoff; + pbr_input.material.ior = pbr_bindings::material_array[material_indices[slot].material].ior; + pbr_input.material.attenuation_color = + pbr_bindings::material_array[material_indices[slot].material].attenuation_color; + pbr_input.material.attenuation_distance = + pbr_bindings::material_array[material_indices[slot].material].attenuation_distance; + pbr_input.material.alpha_cutoff = + pbr_bindings::material_array[material_indices[slot].material].alpha_cutoff; #else // BINDLESS pbr_input.material.ior = pbr_bindings::material.ior; pbr_input.material.attenuation_color = pbr_bindings::material.attenuation_color; @@ -245,7 +255,8 @@ fn pbr_input_from_standard_material( // reflectance #ifdef BINDLESS - pbr_input.material.reflectance = pbr_bindings::material[slot].reflectance; + pbr_input.material.reflectance = + pbr_bindings::material_array[material_indices[slot].material].reflectance; #else // BINDLESS pbr_input.material.reflectance = pbr_bindings::material.reflectance; #endif // BINDLESS @@ -262,8 +273,8 @@ fn pbr_input_from_standard_material( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::specular_texture[slot], - pbr_bindings::specular_sampler[slot], + bindless_textures_2d[material_indices[slot].specular_texture], + bindless_samplers_filtering[material_indices[slot].specular_sampler], #else // BINDLESS pbr_bindings::specular_texture, pbr_bindings::specular_sampler, @@ -294,8 +305,8 @@ fn pbr_input_from_standard_material( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::specular_tint_texture[slot], - pbr_bindings::specular_tint_sampler[slot], + bindless_textures_2d[material_indices[slot].specular_tint_texture], + bindless_samplers_filtering[material_indices[slot].specular_tint_sampler], #else // BINDLESS pbr_bindings::specular_tint_texture, pbr_bindings::specular_tint_sampler, @@ -320,7 +331,7 @@ fn pbr_input_from_standard_material( // emissive #ifdef BINDLESS - var emissive: vec4 = pbr_bindings::material[slot].emissive; + var emissive: vec4 = pbr_bindings::material_array[material_indices[slot].material].emissive; #else // BINDLESS var emissive: vec4 = pbr_bindings::material.emissive; #endif // BINDLESS @@ -334,8 +345,8 @@ fn pbr_input_from_standard_material( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::emissive_texture[slot], - pbr_bindings::emissive_sampler[slot], + bindless_textures_2d[material_indices[slot].emissive_texture], + bindless_samplers_filtering[material_indices[slot].emissive_sampler], #else // BINDLESS pbr_bindings::emissive_texture, pbr_bindings::emissive_sampler, @@ -359,8 +370,8 @@ fn pbr_input_from_standard_material( // metallic and perceptual roughness #ifdef BINDLESS - var metallic: f32 = pbr_bindings::material[slot].metallic; - var perceptual_roughness: f32 = pbr_bindings::material[slot].perceptual_roughness; + var metallic: f32 = pbr_bindings::material_array[material_indices[slot].material].metallic; + var perceptual_roughness: f32 = pbr_bindings::material_array[material_indices[slot].material].perceptual_roughness; #else // BINDLESS var metallic: f32 = pbr_bindings::material.metallic; var perceptual_roughness: f32 = pbr_bindings::material.perceptual_roughness; @@ -376,8 +387,8 @@ fn pbr_input_from_standard_material( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::metallic_roughness_texture[slot], - pbr_bindings::metallic_roughness_sampler[slot], + bindless_textures_2d[material_indices[slot].metallic_roughness_texture], + bindless_samplers_filtering[material_indices[slot].metallic_roughness_sampler], #else // BINDLESS pbr_bindings::metallic_roughness_texture, pbr_bindings::metallic_roughness_sampler, @@ -404,7 +415,8 @@ fn pbr_input_from_standard_material( // Clearcoat factor #ifdef BINDLESS - pbr_input.material.clearcoat = pbr_bindings::material[slot].clearcoat; + pbr_input.material.clearcoat = + pbr_bindings::material_array[material_indices[slot].material].clearcoat; #else // BINDLESS pbr_input.material.clearcoat = pbr_bindings::material.clearcoat; #endif // BINDLESS @@ -419,8 +431,8 @@ fn pbr_input_from_standard_material( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::clearcoat_texture[slot], - pbr_bindings::clearcoat_sampler[slot], + bindless_textures_2d[material_indices[slot].clearcoat_texture], + bindless_samplers_filtering[material_indices[slot].clearcoat_sampler], #else // BINDLESS pbr_bindings::clearcoat_texture, pbr_bindings::clearcoat_sampler, @@ -444,7 +456,7 @@ fn pbr_input_from_standard_material( // Clearcoat roughness #ifdef BINDLESS pbr_input.material.clearcoat_perceptual_roughness = - pbr_bindings::material[slot].clearcoat_perceptual_roughness; + pbr_bindings::material_array[material_indices[slot].material].clearcoat_perceptual_roughness; #else // BINDLESS pbr_input.material.clearcoat_perceptual_roughness = pbr_bindings::material.clearcoat_perceptual_roughness; @@ -460,8 +472,8 @@ fn pbr_input_from_standard_material( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::clearcoat_roughness_texture[slot], - pbr_bindings::clearcoat_roughness_sampler[slot], + bindless_textures_2d[material_indices[slot].clearcoat_roughness_texture], + bindless_samplers_filtering[material_indices[slot].clearcoat_roughness_sampler], #else // BINDLESS pbr_bindings::clearcoat_roughness_texture, pbr_bindings::clearcoat_roughness_sampler, @@ -483,7 +495,7 @@ fn pbr_input_from_standard_material( #endif // VERTEX_UVS #ifdef BINDLESS - var specular_transmission: f32 = pbr_bindings::material[slot].specular_transmission; + var specular_transmission: f32 = pbr_bindings::material_array[slot].specular_transmission; #else // BINDLESS var specular_transmission: f32 = pbr_bindings::material.specular_transmission; #endif // BINDLESS @@ -498,8 +510,12 @@ fn pbr_input_from_standard_material( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::specular_transmission_texture[slot], - pbr_bindings::specular_transmission_sampler[slot], + bindless_textures_2d[ + material_indices[slot].specular_transmission_texture + ], + bindless_samplers_filtering[ + material_indices[slot].specular_transmission_sampler + ], #else // BINDLESS pbr_bindings::specular_transmission_texture, pbr_bindings::specular_transmission_sampler, @@ -522,7 +538,7 @@ fn pbr_input_from_standard_material( pbr_input.material.specular_transmission = specular_transmission; #ifdef BINDLESS - var thickness: f32 = pbr_bindings::material[slot].thickness; + var thickness: f32 = pbr_bindings::material_array[material_indices[slot].material].thickness; #else // BINDLESS var thickness: f32 = pbr_bindings::material.thickness; #endif // BINDLESS @@ -537,8 +553,8 @@ fn pbr_input_from_standard_material( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::thickness_texture[slot], - pbr_bindings::thickness_sampler[slot], + bindless_textures_2d[material_indices[slot].thickness_texture], + bindless_samplers_filtering[material_indices[slot].thickness_sampler], #else // BINDLESS pbr_bindings::thickness_texture, pbr_bindings::thickness_sampler, @@ -568,7 +584,8 @@ fn pbr_input_from_standard_material( pbr_input.material.thickness = thickness; #ifdef BINDLESS - var diffuse_transmission = pbr_bindings::material[slot].diffuse_transmission; + var diffuse_transmission = + pbr_bindings::material_array[material_indices[slot].material].diffuse_transmission; #else // BINDLESS var diffuse_transmission = pbr_bindings::material.diffuse_transmission; #endif // BINDLESS @@ -583,8 +600,8 @@ fn pbr_input_from_standard_material( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::diffuse_transmission_texture[slot], - pbr_bindings::diffuse_transmission_sampler[slot], + bindless_textures_2d[material_indices[slot].diffuse_transmission_texture], + bindless_samplers_filtering[material_indices[slot].diffuse_transmission_sampler], #else // BINDLESS pbr_bindings::diffuse_transmission_texture, pbr_bindings::diffuse_transmission_sampler, @@ -617,8 +634,8 @@ fn pbr_input_from_standard_material( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::occlusion_texture[slot], - pbr_bindings::occlusion_sampler[slot], + bindless_textures_2d[material_indices[slot].occlusion_texture], + bindless_samplers_filtering[material_indices[slot].occlusion_sampler], #else // BINDLESS pbr_bindings::occlusion_texture, pbr_bindings::occlusion_sampler, @@ -668,8 +685,8 @@ fn pbr_input_from_standard_material( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::normal_map_texture[slot], - pbr_bindings::normal_map_sampler[slot], + bindless_textures_2d[material_indices[slot].normal_map_texture], + bindless_samplers_filtering[material_indices[slot].normal_map_sampler], #else // BINDLESS pbr_bindings::normal_map_texture, pbr_bindings::normal_map_sampler, @@ -706,8 +723,8 @@ fn pbr_input_from_standard_material( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::clearcoat_normal_texture[slot], - pbr_bindings::clearcoat_normal_sampler[slot], + bindless_textures_2d[material_indices[slot].clearcoat_normal_texture], + bindless_samplers_filtering[material_indices[slot].clearcoat_normal_sampler], #else // BINDLESS pbr_bindings::clearcoat_normal_texture, pbr_bindings::clearcoat_normal_sampler, @@ -749,8 +766,10 @@ fn pbr_input_from_standard_material( #ifdef STANDARD_MATERIAL_ANISOTROPY #ifdef BINDLESS - var anisotropy_strength = pbr_bindings::material[slot].anisotropy_strength; - var anisotropy_direction = pbr_bindings::material[slot].anisotropy_rotation; + var anisotropy_strength = + pbr_bindings::material_array[material_indices[slot].material].anisotropy_strength; + var anisotropy_direction = + pbr_bindings::material_array[material_indices[slot].material].anisotropy_rotation; #else // BINDLESS var anisotropy_strength = pbr_bindings::material.anisotropy_strength; var anisotropy_direction = pbr_bindings::material.anisotropy_rotation; @@ -765,8 +784,8 @@ fn pbr_input_from_standard_material( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::anisotropy_texture[slot], - pbr_bindings::anisotropy_sampler[slot], + bindless_textures_2d[material_indices[slot].anisotropy_texture], + bindless_samplers_filtering[material_indices[slot].anisotropy_sampler], #else // BINDLESS pbr_bindings::anisotropy_texture, pbr_bindings::anisotropy_sampler, @@ -809,7 +828,8 @@ fn pbr_input_from_standard_material( #ifdef LIGHTMAP #ifdef BINDLESS - let lightmap_exposure = pbr_bindings::material[slot].lightmap_exposure; + let lightmap_exposure = + pbr_bindings::material_array[material_indices[slot].material].lightmap_exposure; #else // BINDLESS let lightmap_exposure = pbr_bindings::material.lightmap_exposure; #endif // BINDLESS diff --git a/crates/bevy_pbr/src/render/pbr_prepass.wgsl b/crates/bevy_pbr/src/render/pbr_prepass.wgsl index 0fd1c63b89..1d26f6390c 100644 --- a/crates/bevy_pbr/src/render/pbr_prepass.wgsl +++ b/crates/bevy_pbr/src/render/pbr_prepass.wgsl @@ -14,6 +14,10 @@ #import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output #endif +#ifdef BINDLESS +#import bevy_pbr::pbr_bindings::material_indices +#endif // BINDLESS + #ifdef PREPASS_FRAGMENT @fragment fn fragment( @@ -31,8 +35,8 @@ fn fragment( #ifdef BINDLESS let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; - let flags = pbr_bindings::material[slot].flags; - let uv_transform = pbr_bindings::material[slot].uv_transform; + let flags = pbr_bindings::material_array[material_indices[slot].material].flags; + let uv_transform = pbr_bindings::material_array[material_indices[slot].material].uv_transform; #else // BINDLESS let flags = pbr_bindings::material.flags; let uv_transform = pbr_bindings::material.uv_transform; @@ -93,8 +97,8 @@ fn fragment( textureSampleBias( #endif // MESHLET_MESH_MATERIAL_PASS #ifdef BINDLESS - pbr_bindings::normal_map_texture[slot], - pbr_bindings::normal_map_sampler[slot], + bindless_textures_2d[material_indices[slot].normal_map_texture], + bindless_samplers_filtering[material_indices[slot].normal_map_sampler], #else // BINDLESS pbr_bindings::normal_map_texture, pbr_bindings::normal_map_sampler, diff --git a/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl b/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl index e3391126aa..568f3821db 100644 --- a/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl @@ -1,5 +1,7 @@ #define_import_path bevy_pbr::pbr_prepass_functions +#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d} + #import bevy_pbr::{ prepass_io::VertexOutput, prepass_bindings::previous_view_uniforms, @@ -9,6 +11,10 @@ pbr_types, } +#ifdef BINDLESS +#import bevy_pbr::pbr_bindings::material_indices +#endif // BINDLESS + // Cutoff used for the premultiplied alpha modes BLEND, ADD, and ALPHA_TO_COVERAGE. const PREMULTIPLIED_ALPHA_CUTOFF = 0.05; @@ -18,7 +24,7 @@ fn prepass_alpha_discard(in: VertexOutput) { #ifdef MAY_DISCARD #ifdef BINDLESS let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; - var output_color: vec4 = pbr_bindings::material[slot].base_color; + var output_color: vec4 = pbr_bindings::material_array[material_indices[slot].material].base_color; #else // BINDLESS var output_color: vec4 = pbr_bindings::material.base_color; #endif // BINDLESS @@ -31,8 +37,8 @@ fn prepass_alpha_discard(in: VertexOutput) { #endif // STANDARD_MATERIAL_BASE_COLOR_UV_B #ifdef BINDLESS - let uv_transform = pbr_bindings::material[slot].uv_transform; - let flags = pbr_bindings::material[slot].flags; + let uv_transform = pbr_bindings::material_array[material_indices[slot].material].uv_transform; + let flags = pbr_bindings::material_array[material_indices[slot].material].flags; #else // BINDLESS let uv_transform = pbr_bindings::material.uv_transform; let flags = pbr_bindings::material.flags; @@ -42,8 +48,8 @@ fn prepass_alpha_discard(in: VertexOutput) { if (flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u { output_color = output_color * textureSampleBias( #ifdef BINDLESS - pbr_bindings::base_color_texture[slot], - pbr_bindings::base_color_sampler[slot], + bindless_textures_2d[material_indices[slot].base_color_texture], + bindless_samplers_filtering[material_indices[slot].base_color_sampler], #else // BINDLESS pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, @@ -57,7 +63,7 @@ fn prepass_alpha_discard(in: VertexOutput) { let alpha_mode = flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK { #ifdef BINDLESS - let alpha_cutoff = pbr_bindings::material[slot].alpha_cutoff; + let alpha_cutoff = pbr_bindings::material_array[material_indices[slot].material].alpha_cutoff; #else // BINDLESS let alpha_cutoff = pbr_bindings::material.alpha_cutoff; #endif // BINDLESS diff --git a/crates/bevy_render/macros/src/as_bind_group.rs b/crates/bevy_render/macros/src/as_bind_group.rs index d5ce58d46c..47cf8b7ce8 100644 --- a/crates/bevy_render/macros/src/as_bind_group.rs +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -3,10 +3,11 @@ use proc_macro::TokenStream; use proc_macro2::{Ident, Span}; use quote::{quote, ToTokens}; use syn::{ + parenthesized, parse::{Parse, ParseStream}, punctuated::Punctuated, token::Comma, - Data, DataStruct, Error, Fields, Lit, LitInt, LitStr, Meta, MetaList, Result, + Data, DataStruct, Error, Fields, LitInt, LitStr, Meta, MetaList, Result, }; const UNIFORM_ATTRIBUTE_NAME: Symbol = Symbol("uniform"); @@ -16,6 +17,8 @@ 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"); +const BINDING_ARRAY_MODIFIER_NAME: Symbol = Symbol("binding_array"); +const LIMIT_MODIFIER_NAME: Symbol = Symbol("limit"); #[derive(Copy, Clone, Debug)] enum BindingType { @@ -39,6 +42,11 @@ enum BindingState<'a> { }, } +enum BindlessSlabResourceLimitAttr { + Auto, + Limit(LitInt), +} + pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { let manifest = BevyManifest::shared(); let render_path = manifest.get_path("bevy_render"); @@ -48,14 +56,20 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { let mut binding_states: Vec = Vec::new(); let mut binding_impls = Vec::new(); - let mut binding_layouts = Vec::new(); + let mut bindless_binding_layouts = Vec::new(); + let mut non_bindless_binding_layouts = Vec::new(); + let mut bindless_resource_types = Vec::new(); + let mut bindless_buffer_descriptors = Vec::new(); let mut attr_prepared_data_ident = None; + // After the first attribute pass, this will be `None` if the object isn't + // bindless and `Some` if it is. let mut attr_bindless_count = None; // `actual_bindless_slot_count` holds the actual number of bindless slots // per bind group, taking into account whether the current platform supports // bindless resources. let actual_bindless_slot_count = Ident::new("actual_bindless_slot_count", Span::call_site()); + let bind_group_layout_entries = Ident::new("bind_group_layout_entries", Span::call_site()); // The `BufferBindingType` and corresponding `BufferUsages` used for // uniforms. We need this because bindless uniforms don't exist, so in @@ -63,7 +77,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { let uniform_binding_type = Ident::new("uniform_binding_type", Span::call_site()); let uniform_buffer_usages = Ident::new("uniform_buffer_usages", Span::call_site()); - // Read struct-level attributes + // Read struct-level attributes, first pass. for attr in &ast.attrs { if let Some(attr_ident) = attr.path().get_ident() { if attr_ident == BIND_GROUP_DATA_ATTRIBUTE_NAME { @@ -72,8 +86,43 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { { attr_prepared_data_ident = Some(prepared_data_ident); } - } else if attr_ident == UNIFORM_ATTRIBUTE_NAME { - let (binding_index, converted_shader_type) = get_uniform_binding_attr(attr)?; + } else if attr_ident == BINDLESS_ATTRIBUTE_NAME { + match attr.meta { + Meta::Path(_) => { + attr_bindless_count = Some(BindlessSlabResourceLimitAttr::Auto); + } + Meta::List(_) => { + // Parse bindless features. For now, the only one we + // support is `limit(N)`. + attr.parse_nested_meta(|submeta| { + if submeta.path.is_ident(&LIMIT_MODIFIER_NAME) { + let content; + parenthesized!(content in submeta.input); + let lit: LitInt = content.parse()?; + + attr_bindless_count = + Some(BindlessSlabResourceLimitAttr::Limit(lit)); + return Ok(()); + } + + Err(Error::new_spanned(attr, "Expected `limit(N)`")) + })?; + } + _ => {} + } + } + } + } + + // Read struct-level attributes, second pass. + for attr in &ast.attrs { + if let Some(attr_ident) = attr.path().get_ident() { + if attr_ident == UNIFORM_ATTRIBUTE_NAME { + let UniformBindingAttr { + binding_index, + converted_shader_type, + binding_array: binding_array_binding, + } = get_uniform_binding_attr(attr)?; binding_impls.push(quote! {{ use #render_path::render_resource::AsBindGroupShaderType; let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new()); @@ -91,30 +140,89 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { ) }}); - binding_layouts.push(quote!{ - #render_path::render_resource::BindGroupLayoutEntry { - binding: #binding_index, - visibility: #render_path::render_resource::ShaderStages::all(), - ty: #render_path::render_resource::BindingType::Buffer { - ty: #uniform_binding_type, - has_dynamic_offset: false, - min_binding_size: Some(<#converted_shader_type as #render_path::render_resource::ShaderType>::min_size()), - }, - count: #actual_bindless_slot_count, - } + // Push the binding layout. This depends on whether we're bindless or not. + + non_bindless_binding_layouts.push(quote!{ + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #render_path::render_resource::ShaderStages::all(), + ty: #render_path::render_resource::BindingType::Buffer { + ty: #uniform_binding_type, + has_dynamic_offset: false, + min_binding_size: Some(<#converted_shader_type as #render_path::render_resource::ShaderType>::min_size()), + }, + count: None, + } + ); }); + match binding_array_binding { + None => { + if attr_bindless_count.is_some() { + return Err(Error::new_spanned( + attr, + "Must specify `binding_array(...)` with `#[uniform]` if the \ + object is bindless", + )); + } + } + Some(binding_array_binding) => { + if attr_bindless_count.is_none() { + return Err(Error::new_spanned( + attr, + "`binding_array(...)` with `#[uniform]` requires the object to be \ + bindless", + )); + } + + bindless_binding_layouts.push(quote!{ + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_array_binding, + visibility: #render_path::render_resource::ShaderStages::all(), + ty: #render_path::render_resource::BindingType::Buffer { + ty: #uniform_binding_type, + has_dynamic_offset: false, + min_binding_size: Some(<#converted_shader_type as #render_path::render_resource::ShaderType>::min_size()), + }, + count: #actual_bindless_slot_count, + } + ); + }); + + bindless_buffer_descriptors.push(quote! { + #render_path::render_resource::BindlessBufferDescriptor { + // Note that, because this is bindless, *binding + // index* here refers to the index in the + // bindless index table (`bindless_index`), and + // the actual binding number is the *binding + // array binding*. + binding_number: #render_path::render_resource::BindingNumber( + #binding_array_binding + ), + bindless_index: + #render_path::render_resource::BindlessIndex(#binding_index), + size: <#converted_shader_type as + #render_path::render_resource::ShaderType>::min_size().get() as + usize, + } + }); + + add_bindless_resource_type( + &render_path, + &mut bindless_resource_types, + binding_index, + quote! { #render_path::render_resource::BindlessResourceType::Buffer }, + ); + } + } + let required_len = binding_index as usize + 1; if required_len > binding_states.len() { 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); - } } } } @@ -135,7 +243,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { // Count the number of sampler fields needed. We might have to disable // bindless if bindless arrays take the GPU over the maximum number of // samplers. - let mut sampler_binding_count = 0; + let mut sampler_binding_count: u32 = 0; // Read field-level attributes for field in fields { @@ -225,9 +333,25 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { match binding_type { BindingType::Uniform => { + if attr_bindless_count.is_some() { + return Err(Error::new_spanned( + attr, + "Only structure-level `#[uniform]` attributes are supported in \ + bindless mode", + )); + } + // uniform codegen is deferred to account for combined uniform bindings } + BindingType::Storage => { + if attr_bindless_count.is_some() { + return Err(Error::new_spanned( + attr, + "Storage buffers are unsupported in bindless mode", + )); + } + let StorageAttrs { visibility, read_only, @@ -259,20 +383,32 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { }); } - binding_layouts.push(quote! { - #render_path::render_resource::BindGroupLayoutEntry { - binding: #binding_index, - visibility: #visibility, - ty: #render_path::render_resource::BindingType::Buffer { - ty: #render_path::render_resource::BufferBindingType::Storage { read_only: #read_only }, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: #actual_bindless_slot_count, - } + // TODO: Support bindless buffers that aren't + // structure-level `#[uniform]` attributes. + non_bindless_binding_layouts.push(quote! { + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #visibility, + ty: #render_path::render_resource::BindingType::Buffer { + ty: #render_path::render_resource::BufferBindingType::Storage { read_only: #read_only }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: #actual_bindless_slot_count, + } + ); }); } + BindingType::StorageTexture => { + if attr_bindless_count.is_some() { + return Err(Error::new_spanned( + attr, + "Storage textures are unsupported in bindless mode", + )); + } + let StorageTextureAttrs { dimension, image_format, @@ -302,19 +438,22 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { ) }); - binding_layouts.push(quote! { - #render_path::render_resource::BindGroupLayoutEntry { - binding: #binding_index, - visibility: #visibility, - ty: #render_path::render_resource::BindingType::StorageTexture { - access: #render_path::render_resource::StorageTextureAccess::#access, - format: #render_path::render_resource::TextureFormat::#image_format, - view_dimension: #render_path::render_resource::#dimension, - }, - count: #actual_bindless_slot_count, - } + non_bindless_binding_layouts.push(quote! { + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #visibility, + ty: #render_path::render_resource::BindingType::StorageTexture { + access: #render_path::render_resource::StorageTextureAccess::#access, + format: #render_path::render_resource::TextureFormat::#image_format, + view_dimension: #render_path::render_resource::#dimension, + }, + count: #actual_bindless_slot_count, + } + ); }); } + BindingType::Texture => { let TextureAttrs { dimension, @@ -348,19 +487,64 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { sampler_binding_count += 1; - binding_layouts.push(quote! { - #render_path::render_resource::BindGroupLayoutEntry { - binding: #binding_index, - visibility: #visibility, - ty: #render_path::render_resource::BindingType::Texture { - multisampled: #multisampled, - sample_type: #render_path::render_resource::#sample_type, - view_dimension: #render_path::render_resource::#dimension, - }, - count: #actual_bindless_slot_count, - } + non_bindless_binding_layouts.push(quote! { + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #visibility, + ty: #render_path::render_resource::BindingType::Texture { + multisampled: #multisampled, + sample_type: #render_path::render_resource::#sample_type, + view_dimension: #render_path::render_resource::#dimension, + }, + count: #actual_bindless_slot_count, + } + ); }); + + let bindless_resource_type = match *dimension { + BindingTextureDimension::D1 => { + quote! { + #render_path::render_resource::BindlessResourceType::Texture1d + } + } + BindingTextureDimension::D2 => { + quote! { + #render_path::render_resource::BindlessResourceType::Texture2d + } + } + BindingTextureDimension::D2Array => { + quote! { + #render_path::render_resource::BindlessResourceType::Texture2dArray + } + } + BindingTextureDimension::Cube => { + quote! { + #render_path::render_resource::BindlessResourceType::TextureCube + } + } + BindingTextureDimension::CubeArray => { + quote! { + #render_path::render_resource::BindlessResourceType::TextureCubeArray + } + } + BindingTextureDimension::D3 => { + quote! { + #render_path::render_resource::BindlessResourceType::Texture3d + } + } + }; + + // Add the texture to the `BindlessResourceType` list in the + // bindless descriptor. + add_bindless_resource_type( + &render_path, + &mut bindless_resource_types, + binding_index, + bindless_resource_type, + ); } + BindingType::Sampler => { let SamplerAttrs { sampler_binding_type, @@ -394,7 +578,10 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { binding_impls.insert(0, quote! { ( #binding_index, - #render_path::render_resource::OwnedBindingResource::Sampler({ + #render_path::render_resource::OwnedBindingResource::Sampler( + // TODO: Support other types. + #render_path::render_resource::WgpuSamplerBindingType::Filtering, + { let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into(); if let Some(handle) = handle { let image = images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?; @@ -426,14 +613,29 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { sampler_binding_count += 1; - binding_layouts.push(quote!{ - #render_path::render_resource::BindGroupLayoutEntry { - binding: #binding_index, - visibility: #visibility, - ty: #render_path::render_resource::BindingType::Sampler(#render_path::render_resource::#sampler_binding_type), - count: #actual_bindless_slot_count, - } + non_bindless_binding_layouts.push(quote!{ + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #visibility, + ty: #render_path::render_resource::BindingType::Sampler(#render_path::render_resource::#sampler_binding_type), + count: #actual_bindless_slot_count, + } + ); }); + + // Add the sampler to the `BindlessResourceType` list in the + // bindless descriptor. + // + // TODO: Support other types of samplers. + add_bindless_resource_type( + &render_path, + &mut bindless_resource_types, + binding_index, + quote! { + #render_path::render_resource::BindlessResourceType::SamplerFiltering + }, + ); } } } @@ -495,17 +697,19 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { ) }}); - binding_layouts.push(quote!{ - #render_path::render_resource::BindGroupLayoutEntry { - binding: #binding_index, - visibility: #render_path::render_resource::ShaderStages::all(), - ty: #render_path::render_resource::BindingType::Buffer { - ty: #uniform_binding_type, - has_dynamic_offset: false, - min_binding_size: Some(<#field_ty as #render_path::render_resource::ShaderType>::min_size()), - }, - count: actual_bindless_slot_count, - } + non_bindless_binding_layouts.push(quote!{ + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #render_path::render_resource::ShaderStages::all(), + ty: #render_path::render_resource::BindingType::Buffer { + ty: #uniform_binding_type, + has_dynamic_offset: false, + min_binding_size: Some(<#field_ty as #render_path::render_resource::ShaderType>::min_size()), + }, + count: #actual_bindless_slot_count, + } + ); }); // multi-field uniform bindings for a given index require an intermediate struct to derive ShaderType } else { @@ -541,8 +745,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { ) }}); - binding_layouts.push(quote!{ - #render_path::render_resource::BindGroupLayoutEntry { + non_bindless_binding_layouts.push(quote!{ + #bind_group_layout_entries.push(#render_path::render_resource::BindGroupLayoutEntry { binding: #binding_index, visibility: #render_path::render_resource::ShaderStages::all(), ty: #render_path::render_resource::BindingType::Buffer { @@ -550,8 +754,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { has_dynamic_offset: false, min_binding_size: Some(<#uniform_struct_name as #render_path::render_resource::ShaderType>::min_size()), }, - count: actual_bindless_slot_count, - } + count: #actual_bindless_slot_count, + }); }); } } @@ -571,46 +775,100 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { // Calculate the number of samplers that we need, so that we don't go over // the limit on certain platforms. See // https://github.com/bevyengine/bevy/issues/16988. - let samplers_needed = match attr_bindless_count { - Some(Lit::Int(ref bindless_count)) => match bindless_count.base10_parse::() { - Ok(bindless_count) => sampler_binding_count * bindless_count, - Err(_) => 0, - }, - _ => 0, + let bindless_count_syntax = match attr_bindless_count { + Some(BindlessSlabResourceLimitAttr::Auto) => { + quote! { #render_path::render_resource::AUTO_BINDLESS_SLAB_RESOURCE_LIMIT } + } + Some(BindlessSlabResourceLimitAttr::Limit(ref count)) => { + quote! { #count } + } + None => quote! { 0 }, }; // Calculate the actual number of bindless slots, taking hardware // limitations into account. - let (bindless_slot_count, actual_bindless_slot_count_declaration) = match attr_bindless_count { - Some(bindless_count) => ( - quote! { - fn bindless_slot_count() -> Option { - Some(#bindless_count) - } - - fn bindless_supported(render_device: &#render_path::renderer::RenderDevice) -> bool { - 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_device.limits().max_samplers_per_shader_stage >= #samplers_needed - } - }, - quote! { - let #actual_bindless_slot_count = if Self::bindless_supported(render_device) && - !force_no_bindless { - ::core::num::NonZeroU32::new(#bindless_count) - } else { - None + let (bindless_slot_count, actual_bindless_slot_count_declaration, bindless_descriptor_syntax) = + match attr_bindless_count { + Some(ref bindless_count) => { + let bindless_supported_syntax = quote! { + fn bindless_supported( + render_device: &#render_path::renderer::RenderDevice + ) -> bool { + 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_device.limits().max_samplers_per_shader_stage >= + (#sampler_binding_count * #bindless_count_syntax) + } }; - }, - ), - None => ( - TokenStream::new().into(), - quote! { let #actual_bindless_slot_count: Option<::core::num::NonZeroU32> = None; }, - ), - }; + let actual_bindless_slot_count_declaration = quote! { + let #actual_bindless_slot_count = if Self::bindless_supported(render_device) && + !force_no_bindless { + ::core::num::NonZeroU32::new(#bindless_count_syntax) + } else { + None + }; + }; + let bindless_slot_count_declaration = match bindless_count { + BindlessSlabResourceLimitAttr::Auto => { + quote! { + fn bindless_slot_count() -> Option< + #render_path::render_resource::BindlessSlabResourceLimit + > { + Some(#render_path::render_resource::BindlessSlabResourceLimit::Auto) + } + } + } + BindlessSlabResourceLimitAttr::Limit(lit) => { + quote! { + fn bindless_slot_count() -> Option< + #render_path::render_resource::BindlessSlabResourceLimit + > { + Some(#render_path::render_resource::BindlessSlabResourceLimit::Custom(#lit)) + } + } + } + }; + + let bindless_buffer_descriptor_count = bindless_buffer_descriptors.len(); + + // We use `LazyLock` so that we can call `min_size`, which isn't + // a `const fn`. + let bindless_descriptor_syntax = quote! { + static RESOURCES: &[#render_path::render_resource::BindlessResourceType] = &[ + #(#bindless_resource_types),* + ]; + static BUFFERS: ::std::sync::LazyLock<[ + #render_path::render_resource::BindlessBufferDescriptor; + #bindless_buffer_descriptor_count + ]> = ::std::sync::LazyLock::new(|| { + [#(#bindless_buffer_descriptors),*] + }); + Some(#render_path::render_resource::BindlessDescriptor { + resources: ::std::borrow::Cow::Borrowed(RESOURCES), + buffers: ::std::borrow::Cow::Borrowed(&*BUFFERS), + }) + }; + + ( + quote! { + #bindless_slot_count_declaration + #bindless_supported_syntax + }, + actual_bindless_slot_count_declaration, + bindless_descriptor_syntax, + ) + } + None => ( + TokenStream::new().into(), + quote! { let #actual_bindless_slot_count: Option<::core::num::NonZeroU32> = None; }, + quote! { None }, + ), + }; + + let bindless_resource_count = bindless_resource_types.len() as u32; Ok(TokenStream::from(quote! { #(#field_struct_impls)* @@ -654,12 +912,54 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { #actual_bindless_slot_count_declaration #uniform_binding_type_declarations - vec![#(#binding_layouts,)*] + let mut #bind_group_layout_entries = Vec::new(); + match #actual_bindless_slot_count { + Some(bindless_slot_count) => { + #bind_group_layout_entries.extend( + #render_path::render_resource::create_bindless_bind_group_layout_entries( + #bindless_resource_count, + bindless_slot_count.into(), + ).into_iter() + ); + #(#bindless_binding_layouts)*; + } + None => { + #(#non_bindless_binding_layouts)*; + } + }; + #bind_group_layout_entries + } + + fn bindless_descriptor() -> Option<#render_path::render_resource::BindlessDescriptor> { + #bindless_descriptor_syntax } } })) } +/// Adds a bindless resource type to the `BindlessResourceType` array in the +/// bindless descriptor we're building up. +/// +/// See the `bevy_render::render_resource::bindless::BindlessResourceType` +/// documentation for more information. +fn add_bindless_resource_type( + render_path: &syn::Path, + bindless_resource_types: &mut Vec, + binding_index: u32, + bindless_resource_type: proc_macro2::TokenStream, +) { + // If we need to grow the array, pad the unused fields with + // `BindlessResourceType::None`. + if bindless_resource_types.len() < (binding_index as usize + 1) { + bindless_resource_types.resize_with(binding_index as usize + 1, || { + quote! { #render_path::render_resource::BindlessResourceType::None } + }); + } + + // Assign the `BindlessResourceType`. + bindless_resource_types[binding_index as usize] = bindless_resource_type; +} + fn get_fallback_image( render_path: &syn::Path, dimension: BindingTextureDimension, @@ -682,8 +982,21 @@ fn get_fallback_image( /// like `#[uniform(LitInt, Ident)]` struct UniformBindingMeta { lit_int: LitInt, - _comma: Comma, ident: Ident, + binding_array: Option, +} + +/// The parsed structure-level `#[uniform]` attribute. +/// +/// The corresponding syntax is `#[uniform(BINDING_INDEX, CONVERTED_SHADER_TYPE, +/// binding_array(BINDING_ARRAY)]`. +struct UniformBindingAttr { + /// The binding index. + binding_index: u32, + /// The uniform data type. + converted_shader_type: Ident, + /// The binding number of the binding array, if this is a bindless material. + binding_array: Option, } /// Represents the arguments for any general binding attribute. @@ -725,22 +1038,52 @@ impl Parse for BindingIndexOptions { } impl Parse for UniformBindingMeta { + // Parse syntax like `#[uniform(0, StandardMaterial, binding_array(10))]`. fn parse(input: ParseStream) -> Result { + let lit_int = input.parse()?; + input.parse::()?; + let ident = input.parse()?; + + // Look for a `binding_array(BINDING_NUMBER)` declaration. + let mut binding_array: Option = None; + if input.parse::().is_ok() { + if input + .parse::()? + .get_ident() + .is_none_or(|ident| *ident != BINDING_ARRAY_MODIFIER_NAME) + { + return Err(Error::new_spanned(ident, "Expected `binding_array`")); + } + let parser; + parenthesized!(parser in input); + binding_array = Some(parser.parse()?); + } + Ok(Self { - lit_int: input.parse()?, - _comma: input.parse()?, - ident: input.parse()?, + lit_int, + ident, + binding_array, }) } } -fn get_uniform_binding_attr(attr: &syn::Attribute) -> Result<(u32, Ident)> { +/// Parses a structure-level `#[uniform]` attribute (not a field-level +/// `#[uniform]` attribute). +fn get_uniform_binding_attr(attr: &syn::Attribute) -> Result { let uniform_binding_meta = attr.parse_args_with(UniformBindingMeta::parse)?; let binding_index = uniform_binding_meta.lit_int.base10_parse()?; let ident = uniform_binding_meta.ident; + let binding_array = match uniform_binding_meta.binding_array { + None => None, + Some(binding_array) => Some(binding_array.base10_parse()?), + }; - Ok((binding_index, ident)) + Ok(UniformBindingAttr { + binding_index, + converted_shader_type: ident, + binding_array, + }) } fn get_binding_nested_attr(attr: &syn::Attribute) -> Result<(u32, Vec)> { diff --git a/crates/bevy_render/src/bindless.wgsl b/crates/bevy_render/src/bindless.wgsl new file mode 100644 index 0000000000..05517a1746 --- /dev/null +++ b/crates/bevy_render/src/bindless.wgsl @@ -0,0 +1,37 @@ +// Defines the common arrays used to access bindless resources. +// +// This need to be kept up to date with the `BINDING_NUMBERS` table in +// `bindless.rs`. +// +// You access these by indexing into the bindless index table, and from there +// indexing into the appropriate binding array. For example, to access the base +// color texture of a `StandardMaterial` in bindless mode, write +// `bindless_textures_2d[materials[slot].base_color_texture]`, where +// `materials` is the bindless index table and `slot` is the index into that +// table (which can be found in the `Mesh`). + +#define_import_path bevy_render::bindless + +#ifdef BINDLESS + +// Binding 0 is the bindless index table. +// Filtering samplers. +@group(2) @binding(1) var bindless_samplers_filtering: binding_array; +// Non-filtering samplers (nearest neighbor). +@group(2) @binding(2) var bindless_samplers_non_filtering: binding_array; +// Comparison samplers (typically for shadow mapping). +@group(2) @binding(3) var bindless_samplers_comparison: binding_array; +// 1D textures. +@group(2) @binding(4) var bindless_textures_1d: binding_array>; +// 2D textures. +@group(2) @binding(5) var bindless_textures_2d: binding_array>; +// 2D array textures. +@group(2) @binding(6) var bindless_textures_2d_array: binding_array>; +// 3D textures. +@group(2) @binding(7) var bindless_textures_3d: binding_array>; +// Cubemap textures. +@group(2) @binding(8) var bindless_textures_cube: binding_array>; +// Cubemap array textures. +@group(2) @binding(9) var bindless_textures_cube_array: binding_array>; + +#endif // BINDLESS diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 76e1f1f0b7..4a91686dfb 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -289,6 +289,8 @@ pub const MATHS_SHADER_HANDLE: Handle = weak_handle!("d94d70d4-746d-49c4-bfc3-27d63f2acda0"); pub const COLOR_OPERATIONS_SHADER_HANDLE: Handle = weak_handle!("33a80b2f-aaf7-4c86-b828-e7ae83b72f1a"); +pub const BINDLESS_SHADER_HANDLE: Handle = + weak_handle!("13f1baaa-41bf-448e-929e-258f9307a522"); impl Plugin for RenderPlugin { /// Initializes the renderer, sets up the [`RenderSet`] and creates the rendering sub-app. @@ -434,6 +436,12 @@ impl Plugin for RenderPlugin { "color_operations.wgsl", Shader::from_wgsl ); + load_internal_asset!( + app, + BINDLESS_SHADER_HANDLE, + "bindless.wgsl", + Shader::from_wgsl + ); if let Some(future_render_resources) = app.world_mut().remove_resource::() { diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index 9b29e453a7..9fe835bb1b 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -12,7 +12,11 @@ pub use bevy_render_macros::AsBindGroup; use core::ops::Deref; use encase::ShaderType; use thiserror::Error; -use wgpu::{BindGroupEntry, BindGroupLayoutEntry, BindingResource, TextureViewDimension}; +use wgpu::{ + BindGroupEntry, BindGroupLayoutEntry, BindingResource, SamplerBindingType, TextureViewDimension, +}; + +use super::{BindlessDescriptor, BindlessSlabResourceLimit}; define_atomic_id!(BindGroupId); @@ -259,12 +263,33 @@ impl Deref for BindGroup { /// Some less common scenarios will require "struct-level" attributes. These are the currently supported struct-level attributes: /// ## `uniform(BINDING_INDEX, ConvertedShaderType)` /// -/// * This also creates a [`Buffer`] using [`ShaderType`] and binds it as a uniform, much -/// like the field-level `uniform` attribute. The difference is that the entire [`AsBindGroup`] value is converted to `ConvertedShaderType`, -/// which must implement [`ShaderType`], instead of a specific field implementing [`ShaderType`]. This is useful if more complicated conversion -/// logic is required. The conversion is done using the [`AsBindGroupShaderType`] trait, which is automatically implemented -/// if `&Self` implements [`Into`]. Only use [`AsBindGroupShaderType`] if access to resources like [`RenderAssets`] is -/// required. +/// * This also creates a [`Buffer`] using [`ShaderType`] and binds it as a +/// uniform, much like the field-level `uniform` attribute. The difference is +/// that the entire [`AsBindGroup`] value is converted to `ConvertedShaderType`, +/// which must implement [`ShaderType`], instead of a specific field +/// implementing [`ShaderType`]. This is useful if more complicated conversion +/// logic is required, or when using bindless mode (see below). The conversion +/// is done using the [`AsBindGroupShaderType`] trait, +/// which is automatically implemented if `&Self` implements +/// [`Into`]. Outside of bindless mode, only use +/// [`AsBindGroupShaderType`] if access to resources like +/// [`RenderAssets`] is required. +/// +/// * In bindless mode (see `bindless(COUNT)`), this attribute becomes +/// `uniform(BINDLESS_INDEX, ConvertedShaderType, +/// binding_array(BINDING_INDEX))`. The resulting uniform buffers will be +/// available in the shader as a binding array at the given `BINDING_INDEX`. The +/// `BINDLESS_INDEX` specifies the offset of the buffer in the bindless index +/// table. +/// +/// For example, suppose that the material slot is stored in a variable named +/// `slot`, the bindless index table is named `material_indices`, and that the +/// first field (index 0) of the bindless index table type is named +/// `material`. Then specifying `#[uniform(0, StandardMaterialUniform, +/// binding_array(10)]` will create a binding array buffer declared in the +/// shader as `var material_array: +/// binding_array` and accessible as +/// `material_array[material_indices[slot].material]`. /// /// ## `bind_group_data(DataType)` /// @@ -273,30 +298,77 @@ impl Deref for BindGroup { /// 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)` +/// ## `bindless` /// /// * 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_and_lightmap_bind_group_slot & -/// 0xffffu`. -/// * Bindless uniforms don't exist, so in bindless mode all uniforms and -/// uniform buffers are automatically replaced with read-only storage -/// buffers. -/// * 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. +/// supplies resources (textures, and samplers) to the shader. When bindless +/// resources are enabled, and the current platform supports them, Bevy will +/// allocate textures, and samplers into *binding arrays*, separated based on +/// type and will supply your shader with indices into those arrays. +/// * Bindless textures and samplers are placed into the appropriate global +/// array defined in `bevy_render::bindless` (`bindless.wgsl`). +/// * Bevy doesn't currently support bindless buffers, except for those created +/// with the `uniform(BINDLESS_INDEX, ConvertedShaderType, +/// binding_array(BINDING_INDEX))` attribute. If you need to include a buffer in +/// your object, and you can't create the data in that buffer with the `uniform` +/// attribute, consider a non-bindless object instead. /// * 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. +/// * In bindless mode, binding 0 becomes the *bindless index table*, which is +/// an array of structures, each of which contains as many fields of type `u32` +/// as the highest binding number in the structure annotated with +/// `#[derive(AsBindGroup)]`. The *i*th field of the bindless index table +/// contains the index of the resource with binding *i* within the appropriate +/// binding array. +/// * In the case of materials, the index of the applicable table within the +/// bindless index table list corresponding to the mesh currently being drawn +/// can be retrieved with +/// `mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu`. +/// * You can limit the size of the bindless slabs to N resources with the +/// `limit(N)` declaration. For example, `#[bindless(limit(16))]` ensures that +/// each slab will have no more than 16 total resources in it. If you don't +/// specify a limit, Bevy automatically picks a reasonable one for the current +/// platform. +/// * 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. /// * See the `shaders/shader_material_bindless` example for an example of /// how to use bindless mode. +/// * The following diagram illustrates how bindless mode works using a subset +/// of `StandardMaterial`: +/// +/// ```text +/// Shader Bindings Sampler Binding Array +/// +----+-----------------------------+ +-----------+-----------+-----+ +/// +---| 0 | material_indices | +->| sampler 0 | sampler 1 | ... | +/// | +----+-----------------------------+ | +-----------+-----------+-----+ +/// | | 1 | bindless_samplers_filtering +--+ ^ +/// | +----+-----------------------------+ +-------------------------------+ +/// | | .. | ... | | +/// | +----+-----------------------------+ Texture Binding Array | +/// | | 5 | bindless_textures_2d +--+ +-----------+-----------+-----+ | +/// | +----+-----------------------------+ +->| texture 0 | texture 1 | ... | | +/// | | .. | ... | +-----------+-----------+-----+ | +/// | +----+-----------------------------+ ^ | +/// | + 10 | material_array +--+ +---------------------------+ | +/// | +----+-----------------------------+ | | | +/// | | Buffer Binding Array | | +/// | | +----------+----------+-----+ | | +/// | +->| buffer 0 | buffer 1 | ... | | | +/// | Material Bindless Indices +----------+----------+-----+ | | +/// | +----+-----------------------------+ ^ | | +/// +-->| 0 | material +----------+ | | +/// +----+-----------------------------+ | | +/// | 1 | base_color_texture +---------------------------------------+ | +/// +----+-----------------------------+ | +/// | 2 | base_color_sampler +-------------------------------------------+ +/// +----+-----------------------------+ +/// | .. | ... | +/// +----+-----------------------------+ +/// ``` /// /// 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: @@ -364,7 +436,7 @@ pub trait AsBindGroup { /// Note that the *actual* slot count may be different from this value, due /// to platform limitations. For example, if bindless resources aren't /// supported on this platform, the actual slot count will be 1. - fn bindless_slot_count() -> Option { + fn bindless_slot_count() -> Option { None } @@ -451,6 +523,10 @@ pub trait AsBindGroup { ) -> Vec where Self: Sized; + + fn bindless_descriptor() -> Option { + None + } } /// An error that occurs during [`AsBindGroup::as_bind_group`] calls. @@ -490,7 +566,7 @@ pub struct BindingResources(pub Vec<(u32, OwnedBindingResource)>); pub enum OwnedBindingResource { Buffer(Buffer), TextureView(TextureViewDimension, TextureView), - Sampler(Sampler), + Sampler(SamplerBindingType, Sampler), } impl OwnedBindingResource { @@ -498,7 +574,7 @@ impl OwnedBindingResource { match self { OwnedBindingResource::Buffer(buffer) => buffer.as_entire_binding(), OwnedBindingResource::TextureView(_, view) => BindingResource::TextureView(view), - OwnedBindingResource::Sampler(sampler) => BindingResource::Sampler(sampler), + OwnedBindingResource::Sampler(_, sampler) => BindingResource::Sampler(sampler), } } } diff --git a/crates/bevy_render/src/render_resource/bindless.rs b/crates/bevy_render/src/render_resource/bindless.rs new file mode 100644 index 0000000000..d34de59e67 --- /dev/null +++ b/crates/bevy_render/src/render_resource/bindless.rs @@ -0,0 +1,313 @@ +//! Types and functions relating to bindless resources. + +use alloc::borrow::Cow; +use core::num::{NonZeroU32, NonZeroU64}; + +use bevy_derive::{Deref, DerefMut}; +use wgpu::{ + BindGroupLayoutEntry, SamplerBindingType, ShaderStages, TextureSampleType, TextureViewDimension, +}; + +use crate::render_resource::binding_types::storage_buffer_read_only_sized; + +use super::binding_types::{ + sampler, texture_1d, texture_2d, texture_2d_array, texture_3d, texture_cube, texture_cube_array, +}; + +/// The default value for the number of resources that can be stored in a slab +/// on this platform. +/// +/// See the documentation for [`BindlessSlabResourceLimit`] for more +/// information. +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub const AUTO_BINDLESS_SLAB_RESOURCE_LIMIT: u32 = 64; +/// The default value for the number of resources that can be stored in a slab +/// on this platform. +/// +/// See the documentation for [`BindlessSlabResourceLimit`] for more +/// information. +#[cfg(not(any(target_os = "macos", target_os = "ios")))] +pub const AUTO_BINDLESS_SLAB_RESOURCE_LIMIT: u32 = 2048; + +/// The binding numbers for the built-in binding arrays of each bindless +/// resource type. +/// +/// In the case of materials, the material allocator manages these binding +/// arrays. +/// +/// `bindless.wgsl` contains declarations of these arrays for use in your +/// shaders. If you change these, make sure to update that file as well. +pub static BINDING_NUMBERS: [(BindlessResourceType, BindingNumber); 9] = [ + (BindlessResourceType::SamplerFiltering, BindingNumber(1)), + (BindlessResourceType::SamplerNonFiltering, BindingNumber(2)), + (BindlessResourceType::SamplerComparison, BindingNumber(3)), + (BindlessResourceType::Texture1d, BindingNumber(4)), + (BindlessResourceType::Texture2d, BindingNumber(5)), + (BindlessResourceType::Texture2dArray, BindingNumber(6)), + (BindlessResourceType::Texture3d, BindingNumber(7)), + (BindlessResourceType::TextureCube, BindingNumber(8)), + (BindlessResourceType::TextureCubeArray, BindingNumber(9)), +]; + +/// The maximum number of resources that can be stored in a slab. +/// +/// This limit primarily exists in order to work around `wgpu` performance +/// problems involving large numbers of bindless resources. Also, some +/// platforms, such as Metal, currently enforce limits on the number of +/// resources in use. +/// +/// This corresponds to `LIMIT` in the `#[bindless(LIMIT)]` attribute when +/// deriving [`crate::render_resource::AsBindGroup`]. +#[derive(Clone, Copy, Default, PartialEq, Debug)] +pub enum BindlessSlabResourceLimit { + /// Allows the renderer to choose a reasonable value for the resource limit + /// based on the platform. + /// + /// This value has been tuned, so you should default to this value unless + /// you have special platform-specific considerations that prevent you from + /// using it. + #[default] + Auto, + + /// A custom value for the resource limit. + /// + /// Bevy will allocate no more than this number of resources in a slab, + /// unless exceeding this value is necessary in order to allocate at all + /// (i.e. unless the number of bindless resources in your bind group exceeds + /// this value), in which case Bevy can exceed it. + Custom(u32), +} + +/// Information about the bindless resources in this object. +/// +/// The material bind group allocator uses this descriptor in order to create +/// and maintain bind groups. The fields within this bindless descriptor are +/// [`Cow`]s in order to support both the common case in which the fields are +/// simply `static` constants and the more unusual case in which the fields are +/// dynamically generated efficiently. An example of the latter case is +/// `ExtendedMaterial`, which needs to assemble a bindless descriptor from those +/// of the base material and the material extension at runtime. +/// +/// This structure will only be present if this object is bindless. +pub struct BindlessDescriptor { + /// The bindless resource types that this object uses, in order of bindless + /// index. + /// + /// The resource assigned to binding index 0 will be at index 0, the + /// resource assigned to binding index will be at index 1 in this array, and + /// so on. Unused binding indices are set to [`BindlessResourceType::None`]. + pub resources: Cow<'static, [BindlessResourceType]>, + /// The [`BindlessBufferDescriptor`] for each bindless buffer that this + /// object uses. + /// + /// The order of this array is irrelevant. + pub buffers: Cow<'static, [BindlessBufferDescriptor]>, +} + +/// The type of potentially-bindless resource. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum BindlessResourceType { + /// No bindless resource. + /// + /// This is used as a placeholder to fill holes in the + /// [`BindlessDescriptor::resources`] list. + None, + /// A storage buffer. + Buffer, + /// A filtering sampler. + SamplerFiltering, + /// A non-filtering sampler (nearest neighbor). + SamplerNonFiltering, + /// A comparison sampler (typically used for shadow maps). + SamplerComparison, + /// A 1D texture. + Texture1d, + /// A 2D texture. + Texture2d, + /// A 2D texture array. + /// + /// Note that this differs from a binding array. 2D texture arrays must all + /// have the same size and format. + Texture2dArray, + /// A 3D texture. + Texture3d, + /// A cubemap texture. + TextureCube, + /// A cubemap texture array. + /// + /// Note that this differs from a binding array. Cubemap texture arrays must + /// all have the same size and format. + TextureCubeArray, +} + +/// Describes a bindless buffer. +/// +/// Unlike samplers and textures, each buffer in a bind group gets its own +/// unique bind group entry. That is, there isn't any `bindless_buffers` binding +/// array to go along with `bindless_textures_2d`, +/// `bindless_samplers_filtering`, etc. Therefore, this descriptor contains two +/// indices: the *binding number* and the *bindless index*. The binding number +/// is the `@binding` number used in the shader, while the bindless index is the +/// index of the buffer in the bindless index table (which is itself +/// conventionally bound to binding number 0). +/// +/// When declaring the buffer in a derived implementation +/// [`crate::render_resource::AsBindGroup`] with syntax like +/// `#[uniform(BINDLESS_INDEX, StandardMaterialUniform, +/// bindless(BINDING_NUMBER)]`, the bindless index is `BINDLESS_INDEX`, and the +/// binding number is `BINDING_NUMBER`. Note the order. +#[derive(Clone, Copy)] +pub struct BindlessBufferDescriptor { + /// The actual binding number of the buffer. + /// + /// This is declared with `@binding` in WGSL. When deriving + /// [`crate::render_resource::AsBindGroup`], this is the `BINDING_NUMBER` in + /// `#[uniform(BINDLESS_INDEX, StandardMaterialUniform, + /// bindless(BINDING_NUMBER)]`. + pub binding_number: BindingNumber, + /// The index of the buffer in the bindless index table. + /// + /// In the shader, this is the index into the table bound to binding 0. When + /// deriving [`crate::render_resource::AsBindGroup`], this is the + /// `BINDLESS_INDEX` in `#[uniform(BINDLESS_INDEX, StandardMaterialUniform, + /// bindless(BINDING_NUMBER)]`. + pub bindless_index: BindlessIndex, + /// The size of the buffer in bytes. + pub size: usize, +} + +/// The index of the actual binding in the bind group. +/// +/// This is the value specified in WGSL as `@binding`. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Deref, DerefMut)] +pub struct BindingNumber(pub u32); + +/// The index in the bindless index table. +/// +/// This table is conventionally bound to binding number 0. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Deref, DerefMut)] +pub struct BindlessIndex(pub u32); + +/// Creates the bind group layout entries common to all shaders that use +/// bindless bind groups. +/// +/// `bindless_resource_count` specifies the total number of bindless resources. +/// `bindless_slab_resource_limit` specifies the resolved +/// [`BindlessSlabResourceLimit`] value. +pub fn create_bindless_bind_group_layout_entries( + bindless_resource_count: u32, + bindless_slab_resource_limit: u32, +) -> Vec { + let bindless_slab_resource_limit = + NonZeroU32::new(bindless_slab_resource_limit).expect("Bindless slot count must be nonzero"); + + // The maximum size of a binding array is the + // `bindless_slab_resource_limit`, which would occur if all of the bindless + // resources were of the same type. So we create our binding arrays with + // that size. + + vec![ + // Start with the bindless index table, bound to binding number 0. + storage_buffer_read_only_sized( + false, + NonZeroU64::new(bindless_resource_count as u64 * size_of::() as u64), + ) + .build(0, ShaderStages::all()), + // Continue with the common bindless buffers. + sampler(SamplerBindingType::Filtering) + .count(bindless_slab_resource_limit) + .build(1, ShaderStages::all()), + sampler(SamplerBindingType::NonFiltering) + .count(bindless_slab_resource_limit) + .build(2, ShaderStages::all()), + sampler(SamplerBindingType::Comparison) + .count(bindless_slab_resource_limit) + .build(3, ShaderStages::all()), + texture_1d(TextureSampleType::Float { filterable: true }) + .count(bindless_slab_resource_limit) + .build(4, ShaderStages::all()), + texture_2d(TextureSampleType::Float { filterable: true }) + .count(bindless_slab_resource_limit) + .build(5, ShaderStages::all()), + texture_2d_array(TextureSampleType::Float { filterable: true }) + .count(bindless_slab_resource_limit) + .build(6, ShaderStages::all()), + texture_3d(TextureSampleType::Float { filterable: true }) + .count(bindless_slab_resource_limit) + .build(7, ShaderStages::all()), + texture_cube(TextureSampleType::Float { filterable: true }) + .count(bindless_slab_resource_limit) + .build(8, ShaderStages::all()), + texture_cube_array(TextureSampleType::Float { filterable: true }) + .count(bindless_slab_resource_limit) + .build(9, ShaderStages::all()), + ] +} + +impl BindlessSlabResourceLimit { + /// Determines the actual bindless slab resource limit on this platform. + pub fn resolve(&self) -> u32 { + match *self { + BindlessSlabResourceLimit::Auto => AUTO_BINDLESS_SLAB_RESOURCE_LIMIT, + BindlessSlabResourceLimit::Custom(limit) => limit, + } + } +} + +impl BindlessResourceType { + /// Returns the binding number for the common array of this resource type. + /// + /// For example, if you pass `BindlessResourceType::Texture2d`, this will + /// return 5, in order to match the `@group(2) @binding(5) var + /// bindless_textures_2d: binding_array>` declaration in + /// `bindless.wgsl`. + /// + /// Not all resource types have fixed binding numbers. If you call + /// [`Self::binding_number`] on such a resource type, it returns `None`. + /// + /// Note that this returns a static reference to the binding number, not the + /// binding number itself. This is to conform to an idiosyncratic API in + /// `wgpu` whereby binding numbers for binding arrays are taken by `&u32` + /// *reference*, not by `u32` value. + pub fn binding_number(&self) -> Option<&'static BindingNumber> { + match BINDING_NUMBERS.binary_search_by_key(self, |(key, _)| *key) { + Ok(binding_number) => Some(&BINDING_NUMBERS[binding_number].1), + Err(_) => None, + } + } +} + +impl From for BindlessResourceType { + fn from(texture_view_dimension: TextureViewDimension) -> Self { + match texture_view_dimension { + TextureViewDimension::D1 => BindlessResourceType::Texture1d, + TextureViewDimension::D2 => BindlessResourceType::Texture2d, + TextureViewDimension::D2Array => BindlessResourceType::Texture2dArray, + TextureViewDimension::Cube => BindlessResourceType::TextureCube, + TextureViewDimension::CubeArray => BindlessResourceType::TextureCubeArray, + TextureViewDimension::D3 => BindlessResourceType::Texture3d, + } + } +} + +impl From for BindlessResourceType { + fn from(sampler_binding_type: SamplerBindingType) -> Self { + match sampler_binding_type { + SamplerBindingType::Filtering => BindlessResourceType::SamplerFiltering, + SamplerBindingType::NonFiltering => BindlessResourceType::SamplerNonFiltering, + SamplerBindingType::Comparison => BindlessResourceType::SamplerComparison, + } + } +} + +impl From for BindlessIndex { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for BindingNumber { + fn from(value: u32) -> Self { + Self(value) + } +} diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index 3d9d9c6010..aab2fe5b6a 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -3,6 +3,7 @@ mod bind_group; mod bind_group_entries; mod bind_group_layout; mod bind_group_layout_entries; +mod bindless; mod buffer; mod buffer_vec; mod gpu_array_buffer; @@ -19,6 +20,7 @@ pub use bind_group::*; pub use bind_group_entries::*; pub use bind_group_layout::*; pub use bind_group_layout_entries::*; +pub use bindless::*; pub use buffer::*; pub use buffer_vec::*; pub use gpu_array_buffer::*; @@ -49,10 +51,11 @@ pub use wgpu::{ PrimitiveState, PrimitiveTopology, PushConstantRange, RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipelineDescriptor as RawRenderPipelineDescriptor, Sampler as WgpuSampler, - SamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, - ShaderStages, StencilFaceState, StencilOperation, StencilState, StorageTextureAccess, StoreOp, - TexelCopyBufferInfo, TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, - TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages, + SamplerBindingType, SamplerBindingType as WgpuSamplerBindingType, SamplerDescriptor, + ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, StencilFaceState, + StencilOperation, StencilState, StorageTextureAccess, StoreOp, TexelCopyBufferInfo, + TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, TextureDescriptor, + TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureView as WgpuTextureView, TextureViewDescriptor, TextureViewDimension, VertexAttribute, VertexBufferLayout as RawVertexBufferLayout, VertexFormat, VertexState as RawVertexState, VertexStepMode, COPY_BUFFER_ALIGNMENT, diff --git a/examples/shader/shader_material_bindless.rs b/examples/shader/shader_material_bindless.rs index 677082d4b7..7e6ff32af8 100644 --- a/examples/shader/shader_material_bindless.rs +++ b/examples/shader/shader_material_bindless.rs @@ -1,18 +1,18 @@ //! A material that uses bindless textures. use bevy::prelude::*; -use bevy::render::render_resource::{AsBindGroup, ShaderRef}; +use bevy::render::render_resource::{AsBindGroup, ShaderRef, ShaderType}; const SHADER_ASSET_PATH: &str = "shaders/bindless_material.wgsl"; -// `#[bindless(4)]` indicates that we want Bevy to group materials into bind -// groups of at most 4 materials each. +// `#[bindless(limit(4))]` indicates that we want Bevy to group materials into +// bind groups of at most 4 materials each. +// Note that we use the structure-level `#[uniform]` attribute to supply +// ordinary data to the shader. #[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] -#[bindless(4)] +#[uniform(0, BindlessMaterialUniform, binding_array(10))] +#[bindless(limit(4))] struct BindlessMaterial { - // This will be exposed to the shader as a binding array of 4 *storage* - // buffers (as bindless uniforms don't exist). - #[uniform(0)] color: LinearRgba, // This will be exposed to the shader as a binding array of 4 textures and a // binding array of 4 samplers. @@ -21,6 +21,20 @@ struct BindlessMaterial { color_texture: Option>, } +// This buffer will be presented to the shader as `@binding(10)`. +#[derive(ShaderType)] +struct BindlessMaterialUniform { + color: LinearRgba, +} + +impl<'a> From<&'a BindlessMaterial> for BindlessMaterialUniform { + fn from(material: &'a BindlessMaterial) -> Self { + BindlessMaterialUniform { + color: material.color, + } + } +} + // The entry point. fn main() { App::new()