From 28441337bb4ecae0a71b145275734f8e073af5db Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Thu, 20 Feb 2025 21:55:36 -0800 Subject: [PATCH] Use global binding arrays for bindless resources. (#17898) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, Bevy's implementation of bindless resources is rather unusual: every binding in an object that implements `AsBindGroup` (most commonly, a material) becomes its own separate binding array in the shader. This is inefficient for two reasons: 1. If multiple materials reference the same texture or other resource, the reference to that resource will be duplicated many times. This increases `wgpu` validation overhead. 2. It creates many unused binding array slots. This increases `wgpu` and driver overhead and makes it easier to hit limits on APIs that `wgpu` currently imposes tight resource limits on, like Metal. This PR fixes these issues by switching Bevy to use the standard approach in GPU-driven renderers, in which resources are de-duplicated and passed as global arrays, one for each type of resource. Along the way, this patch introduces per-platform resource limits and bumps them from 16 resources per binding array to 64 resources per bind group on Metal and 2048 resources per bind group on other platforms. (Note that the number of resources per *binding array* isn't the same as the number of resources per *bind group*; as it currently stands, if all the PBR features are turned on, Bevy could pack as many as 496 resources into a single slab.) The limits have been increased because `wgpu` now has universal support for partially-bound binding arrays, which mean that we no longer need to fill the binding arrays with fallback resources on Direct3D 12. The `#[bindless(LIMIT)]` declaration when deriving `AsBindGroup` can now simply be written `#[bindless]` in order to have Bevy choose a default limit size for the current platform. Custom limits are still available with the new `#[bindless(limit(LIMIT))]` syntax: e.g. `#[bindless(limit(8))]`. The material bind group allocator has been completely rewritten. Now there are two allocators: one for bindless materials and one for non-bindless materials. The new non-bindless material allocator simply maintains a 1:1 mapping from material to bind group. The new bindless material allocator maintains a list of slabs and allocates materials into slabs on a first-fit basis. This unfortunately makes its performance O(number of resources per object * number of slabs), but the number of slabs is likely to be low, and it's planned to become even lower in the future with `wgpu` improvements. Resources are de-duplicated with in a slab and reference counted. So, for instance, if multiple materials refer to the same texture, that texture will exist only once in the appropriate binding array. To support these new features, this patch adds the concept of a *bindless descriptor* to the `AsBindGroup` trait. The bindless descriptor allows the material bind group allocator to probe the layout of the material, now that an array of `BindGroupLayoutEntry` records is insufficient to describe the group. The `#[derive(AsBindGroup)]` has been heavily modified to support the new features. The most important user-facing change to that macro is that the struct-level `uniform` attribute, `#[uniform(BINDING_NUMBER, StandardMaterial)]`, now reads `#[uniform(BINDLESS_INDEX, MATERIAL_UNIFORM_TYPE, binding_array(BINDING_NUMBER)]`, allowing the material to specify the binding number for the binding array that holds the uniform data. To make this patch simpler, I removed support for bindless `ExtendedMaterial`s, as well as field-level bindless uniform and storage buffers. I intend to add back support for these as a follow-up. Because they aren't in any released Bevy version yet, I figured this was OK. Finally, this patch updates `StandardMaterial` for the new bindless changes. Generally, code throughout the PBR shaders that looked like `base_color_texture[slot]` now looks like `bindless_2d_textures[material_indices[slot].base_color_texture]`. This patch fixes a system hang that I experienced on the [Caldera test] when running with `caldera --random-materials --texture-count 100`. The time per frame is around 19.75 ms, down from 154.2 ms in Bevy 0.14: a 7.8× speedup. [Caldera test]: https://github.com/DGriffin91/bevy_caldera_scene --- assets/shaders/bindless_material.wgsl | 20 +- crates/bevy_pbr/src/extended_material.rs | 60 +- crates/bevy_pbr/src/lib.rs | 3 +- crates/bevy_pbr/src/material.rs | 58 +- crates/bevy_pbr/src/material_bind_groups.rs | 2259 ++++++++++++----- .../src/meshlet/material_pipeline_prepare.rs | 4 +- crates/bevy_pbr/src/pbr_material.rs | 4 +- crates/bevy_pbr/src/prepass/mod.rs | 3 +- .../bevy_pbr/src/render/parallax_mapping.wgsl | 10 +- crates/bevy_pbr/src/render/pbr_bindings.wgsl | 92 +- crates/bevy_pbr/src/render/pbr_fragment.wgsl | 130 +- crates/bevy_pbr/src/render/pbr_prepass.wgsl | 12 +- .../src/render/pbr_prepass_functions.wgsl | 18 +- .../bevy_render/macros/src/as_bind_group.rs | 585 ++++- crates/bevy_render/src/bindless.wgsl | 37 + crates/bevy_render/src/lib.rs | 8 + .../src/render_resource/bind_group.rs | 128 +- .../src/render_resource/bindless.rs | 313 +++ crates/bevy_render/src/render_resource/mod.rs | 11 +- examples/shader/shader_material_bindless.rs | 28 +- 20 files changed, 2744 insertions(+), 1039 deletions(-) create mode 100644 crates/bevy_render/src/bindless.wgsl create mode 100644 crates/bevy_render/src/render_resource/bindless.rs 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()