Add bindless support back to ExtendedMaterial. (#18025)

PR #17898 disabled bindless support for `ExtendedMaterial`. This commit
adds it back. It also adds a new example, `extended_material_bindless`,
showing how to use it.
This commit is contained in:
Patrick Walton 2025-04-09 08:34:44 -07:00 committed by GitHub
parent 714b4a43d6
commit dc7c8f228f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 658 additions and 112 deletions

View File

@ -4317,3 +4317,14 @@ name = "`no_std` Compatible Library"
description = "Example library compatible with `std` and `no_std` targets"
category = "Embedded"
wasm = true
[[example]]
name = "extended_material_bindless"
path = "examples/shader/extended_material_bindless.rs"
doc-scrape-examples = true
[package.metadata.example.extended_material_bindless]
name = "Extended Bindless Material"
description = "Demonstrates bindless `ExtendedMaterial`"
category = "Shaders"
wasm = false

View File

@ -0,0 +1,107 @@
// The shader that goes with `extended_material_bindless.rs`.
//
// This code demonstrates how to write shaders that are compatible with both
// bindless and non-bindless mode. See the `#ifdef BINDLESS` blocks.
#import bevy_pbr::{
forward_io::{FragmentOutput, VertexOutput},
mesh_bindings::mesh,
pbr_fragment::pbr_input_from_standard_material,
pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing},
}
#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d}
#ifdef BINDLESS
#import bevy_pbr::pbr_bindings::{material_array, material_indices}
#else // BINDLESS
#import bevy_pbr::pbr_bindings::material
#endif // BINDLESS
// Stores the indices of the bindless resources in the bindless resource arrays,
// for the `ExampleBindlessExtension` fields.
struct ExampleBindlessExtendedMaterialIndices {
// The index of the `ExampleBindlessExtendedMaterial` data in
// `example_extended_material`.
material: u32,
// The index of the texture we're going to modulate the base color with in
// the `bindless_textures_2d` array.
modulate_texture: u32,
// The index of the sampler we're going to sample the modulated texture with
// in the `bindless_samplers_filtering` array.
modulate_texture_sampler: u32,
}
// Plain data associated with this example material.
struct ExampleBindlessExtendedMaterial {
// The color that we multiply the base color, base color texture, and
// modulated texture with.
modulate_color: vec4<f32>,
}
#ifdef BINDLESS
// The indices of the bindless resources in the bindless resource arrays, for
// the `ExampleBindlessExtension` fields.
@group(2) @binding(100) var<storage> example_extended_material_indices:
array<ExampleBindlessExtendedMaterialIndices>;
// An array that holds the `ExampleBindlessExtendedMaterial` plain old data,
// indexed by `ExampleBindlessExtendedMaterialIndices.material`.
@group(2) @binding(101) var<storage> example_extended_material:
array<ExampleBindlessExtendedMaterial>;
#else // BINDLESS
// In non-bindless mode, we simply use a uniform for the plain old data.
@group(2) @binding(50) var<uniform> example_extended_material: ExampleBindlessExtendedMaterial;
@group(2) @binding(51) var modulate_texture: texture_2d<f32>;
@group(2) @binding(52) var modulate_sampler: sampler;
#endif // BINDLESS
@fragment
fn fragment(
in: VertexOutput,
@builtin(front_facing) is_front: bool,
) -> FragmentOutput {
#ifdef BINDLESS
// Fetch the material slot. We'll use this in turn to fetch the bindless
// indices from `example_extended_material_indices`.
let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu;
#endif // BINDLESS
// Generate a `PbrInput` struct from the `StandardMaterial` bindings.
var pbr_input = pbr_input_from_standard_material(in, is_front);
// Calculate the UV for the texture we're about to sample.
#ifdef BINDLESS
let uv_transform = material_array[material_indices[slot].material].uv_transform;
#else // BINDLESS
let uv_transform = material.uv_transform;
#endif // BINDLESS
let uv = (uv_transform * vec3(in.uv, 1.0)).xy;
// Multiply the base color by the `modulate_texture` and `modulate_color`.
#ifdef BINDLESS
// Notice how we fetch the texture, sampler, and plain extended material
// data from the appropriate arrays.
pbr_input.material.base_color *= textureSample(
bindless_textures_2d[example_extended_material_indices[slot].modulate_texture],
bindless_samplers_filtering[
example_extended_material_indices[slot].modulate_texture_sampler
],
uv
) * example_extended_material[example_extended_material_indices[slot].material].modulate_color;
#else // BINDLESS
pbr_input.material.base_color *= textureSample(modulate_texture, modulate_sampler, uv) *
example_extended_material.modulate_color;
#endif // BINDLESS
var out: FragmentOutput;
// Apply lighting.
out.color = apply_pbr_lighting(pbr_input);
// Apply in-shader post processing (fog, alpha-premultiply, and also
// tonemapping, debanding if the camera is non-HDR). Note this does not
// include fullscreen postprocessing effects like bloom.
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
return out;
}

View File

@ -1,13 +1,16 @@
use alloc::borrow::Cow;
use bevy_asset::{Asset, Handle};
use bevy_ecs::system::SystemParamItem;
use bevy_platform_support::{collections::HashSet, hash::FixedHasher};
use bevy_reflect::{impl_type_path, Reflect};
use bevy_render::{
alpha::AlphaMode,
mesh::MeshVertexBufferLayoutRef,
render_resource::{
AsBindGroup, AsBindGroupError, BindGroupLayout, BindlessDescriptor,
BindlessSlabResourceLimit, RenderPipelineDescriptor, Shader, ShaderRef,
SpecializedMeshPipelineError, UnpreparedBindGroup,
AsBindGroup, AsBindGroupError, BindGroupLayout, BindGroupLayoutEntry, BindlessDescriptor,
BindlessResourceType, BindlessSlabResourceLimit, RenderPipelineDescriptor, Shader,
ShaderRef, SpecializedMeshPipelineError, UnpreparedBindGroup,
},
renderer::RenderDevice,
};
@ -156,11 +159,24 @@ impl<B: Material, E: MaterialExtension> AsBindGroup for ExtendedMaterial<B, E> {
type Param = (<B as AsBindGroup>::Param, <E as AsBindGroup>::Param);
fn bindless_slot_count() -> Option<BindlessSlabResourceLimit> {
// 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")
// We only enable bindless if both the base material and its extension
// are bindless. If we do enable bindless, we choose the smaller of the
// two slab size limits.
match (B::bindless_slot_count()?, E::bindless_slot_count()?) {
(BindlessSlabResourceLimit::Auto, BindlessSlabResourceLimit::Auto) => {
Some(BindlessSlabResourceLimit::Auto)
}
(BindlessSlabResourceLimit::Auto, BindlessSlabResourceLimit::Custom(limit))
| (BindlessSlabResourceLimit::Custom(limit), BindlessSlabResourceLimit::Auto) => {
Some(BindlessSlabResourceLimit::Custom(limit))
}
(
BindlessSlabResourceLimit::Custom(base_limit),
BindlessSlabResourceLimit::Custom(extended_limit),
) => Some(BindlessSlabResourceLimit::Custom(
base_limit.min(extended_limit),
)),
}
None
}
fn unprepared_bind_group(
@ -168,15 +184,28 @@ impl<B: Material, E: MaterialExtension> AsBindGroup for ExtendedMaterial<B, E> {
layout: &BindGroupLayout,
render_device: &RenderDevice,
(base_param, extended_param): &mut SystemParamItem<'_, '_, Self::Param>,
_: bool,
mut force_non_bindless: bool,
) -> Result<UnpreparedBindGroup<Self::Data>, AsBindGroupError> {
force_non_bindless = force_non_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, true)?;
let extended_bindgroup =
E::unprepared_bind_group(&self.extension, layout, render_device, extended_param, true)?;
} = B::unprepared_bind_group(
&self.base,
layout,
render_device,
base_param,
force_non_bindless,
)?;
let extended_bindgroup = E::unprepared_bind_group(
&self.extension,
layout,
render_device,
extended_param,
force_non_bindless,
)?;
bindings.extend(extended_bindgroup.bindings.0);
@ -188,23 +217,72 @@ impl<B: Material, E: MaterialExtension> AsBindGroup for ExtendedMaterial<B, E> {
fn bind_group_layout_entries(
render_device: &RenderDevice,
_: bool,
) -> Vec<bevy_render::render_resource::BindGroupLayoutEntry>
mut force_non_bindless: bool,
) -> Vec<BindGroupLayoutEntry>
where
Self: Sized,
{
// add together the bindings of the standard material and the user material
let mut entries = B::bind_group_layout_entries(render_device, true);
entries.extend(E::bind_group_layout_entries(render_device, true));
force_non_bindless = force_non_bindless || Self::bindless_slot_count().is_none();
// Add together the bindings of the standard material and the user
// material, skipping duplicate bindings. Duplicate bindings will occur
// when bindless mode is on, because of the common bindless resource
// arrays, and we need to eliminate the duplicates or `wgpu` will
// complain.
let mut entries = vec![];
let mut seen_bindings = HashSet::<_>::with_hasher(FixedHasher);
for entry in B::bind_group_layout_entries(render_device, force_non_bindless)
.into_iter()
.chain(E::bind_group_layout_entries(render_device, force_non_bindless).into_iter())
{
if seen_bindings.insert(entry.binding) {
entries.push(entry);
}
}
entries
}
fn bindless_descriptor() -> Option<BindlessDescriptor> {
if B::bindless_descriptor().is_some() && E::bindless_descriptor().is_some() {
panic!("Bindless extended materials are currently unsupported")
// We're going to combine the two bindless descriptors.
let base_bindless_descriptor = B::bindless_descriptor()?;
let extended_bindless_descriptor = E::bindless_descriptor()?;
// Combining the buffers and index tables is straightforward.
let mut buffers = base_bindless_descriptor.buffers.to_vec();
let mut index_tables = base_bindless_descriptor.index_tables.to_vec();
buffers.extend(extended_bindless_descriptor.buffers.iter().cloned());
index_tables.extend(extended_bindless_descriptor.index_tables.iter().cloned());
// Combining the resources is a little trickier because the resource
// array is indexed by bindless index, so we have to merge the two
// arrays, not just concatenate them.
let max_bindless_index = base_bindless_descriptor
.resources
.len()
.max(extended_bindless_descriptor.resources.len());
let mut resources = Vec::with_capacity(max_bindless_index);
for bindless_index in 0..max_bindless_index {
// In the event of a conflicting bindless index, we choose the
// base's binding.
match base_bindless_descriptor.resources.get(bindless_index) {
None | Some(&BindlessResourceType::None) => resources.push(
extended_bindless_descriptor
.resources
.get(bindless_index)
.copied()
.unwrap_or(BindlessResourceType::None),
),
Some(&resource_type) => resources.push(resource_type),
}
}
None
Some(BindlessDescriptor {
resources: Cow::Owned(resources),
buffers: Cow::Owned(buffers),
index_tables: Cow::Owned(index_tables),
})
}
}

View File

@ -4,7 +4,7 @@
//! allocator manages each bind group, assigning slots to materials as
//! appropriate.
use core::{iter, marker::PhantomData, mem};
use core::{cmp::Ordering, iter, marker::PhantomData, mem, ops::Range};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
@ -16,11 +16,11 @@ use bevy_reflect::{prelude::ReflectDefault, Reflect};
use bevy_render::{
render_resource::{
BindGroup, BindGroupEntry, BindGroupLayout, BindingNumber, BindingResource,
BindingResources, BindlessDescriptor, BindlessIndex, BindlessResourceType, Buffer,
BufferBinding, BufferDescriptor, BufferId, BufferInitDescriptor, BufferUsages,
CompareFunction, FilterMode, OwnedBindingResource, PreparedBindGroup, RawBufferVec,
Sampler, SamplerDescriptor, SamplerId, TextureView, TextureViewDimension, TextureViewId,
UnpreparedBindGroup, WgpuSampler, WgpuTextureView,
BindingResources, BindlessDescriptor, BindlessIndex, BindlessIndexTableDescriptor,
BindlessResourceType, Buffer, BufferBinding, BufferDescriptor, BufferId,
BufferInitDescriptor, BufferUsages, CompareFunction, FilterMode, OwnedBindingResource,
PreparedBindGroup, RawBufferVec, Sampler, SamplerDescriptor, SamplerId, TextureView,
TextureViewDimension, TextureViewId, UnpreparedBindGroup, WgpuSampler, WgpuTextureView,
},
renderer::{RenderDevice, RenderQueue},
settings::WgpuFeatures,
@ -89,11 +89,16 @@ where
/// regenerated.
bind_group: Option<BindGroup>,
/// A GPU-accessible buffer that holds the mapping from binding index to
/// The GPU-accessible buffers that hold the mapping from binding index to
/// bindless slot.
///
/// This is conventionally assigned to bind group binding 0.
bindless_index_table: MaterialBindlessIndexTable<M>,
/// This is conventionally assigned to bind group binding 0, but it can be
/// changed using the `#[bindless(index_table(binding(B)))]` attribute on
/// `AsBindGroup`.
///
/// Because the slab binary searches this table, the entries within must be
/// sorted by bindless index.
bindless_index_tables: Vec<MaterialBindlessIndexTable<M>>,
/// The binding arrays containing samplers.
samplers: HashMap<BindlessResourceType, MaterialBindlessBindingArray<Sampler>>,
@ -122,13 +127,25 @@ where
/// A GPU-accessible buffer that holds the mapping from binding index to
/// bindless slot.
///
/// This is conventionally assigned to bind group binding 0.
/// This is conventionally assigned to bind group binding 0, but it can be
/// changed by altering the [`Self::binding_number`], which corresponds to the
/// `#[bindless(index_table(binding(B)))]` attribute in `AsBindGroup`.
struct MaterialBindlessIndexTable<M>
where
M: Material,
{
/// The buffer containing the mappings.
buffer: RetainedRawBufferVec<u32>,
/// The range of bindless indices that this bindless index table covers.
///
/// If this range is M..N, then the field at index $i$ maps to bindless
/// index $i$ + M. The size of this table is N - M.
///
/// This corresponds to the `#[bindless(index_table(range(M..N)))]`
/// attribute in `AsBindGroup`.
index_range: Range<BindlessIndex>,
/// The binding number that this index table is assigned to in the shader.
binding_number: BindingNumber,
phantom: PhantomData<M>,
}
@ -601,29 +618,54 @@ where
M: Material,
{
/// Creates a new [`MaterialBindlessIndexTable`] for a single slab.
fn new(bindless_descriptor: &BindlessDescriptor) -> MaterialBindlessIndexTable<M> {
fn new(
bindless_index_table_descriptor: &BindlessIndexTableDescriptor,
) -> MaterialBindlessIndexTable<M> {
// Preallocate space for one bindings table, so that there will always be a buffer.
let mut buffer = RetainedRawBufferVec::new(BufferUsages::STORAGE);
for _ in 0..bindless_descriptor.resources.len() {
for _ in *bindless_index_table_descriptor.indices.start
..*bindless_index_table_descriptor.indices.end
{
buffer.push(0);
}
MaterialBindlessIndexTable {
buffer,
index_range: bindless_index_table_descriptor.indices.clone(),
binding_number: bindless_index_table_descriptor.binding_number,
phantom: PhantomData,
}
}
/// Returns the binding index table for a single material.
/// Returns the bindings in the binding index table.
///
/// 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();
/// If the current [`MaterialBindlessIndexTable::index_range`] is M..N, then
/// element *i* of the returned binding index table contains the slot of the
/// bindless resource with bindless index *i* + M.
fn get(&self, slot: MaterialBindGroupSlot) -> &[u32] {
let struct_size = *self.index_range.end as usize - *self.index_range.start as usize;
let start = struct_size * slot.0 as usize;
&self.buffer.values()[start..(start + struct_size)]
}
/// Returns a single binding from the binding index table.
fn get_binding(
&self,
slot: MaterialBindGroupSlot,
bindless_index: BindlessIndex,
) -> Option<u32> {
if bindless_index < self.index_range.start || bindless_index >= self.index_range.end {
return None;
}
self.get(slot)
.get((*bindless_index - *self.index_range.start) as usize)
.copied()
}
fn table_length(&self) -> u32 {
self.index_range.end.0 - self.index_range.start.0
}
/// Updates the binding index table for a single material.
///
/// The `allocated_resource_slots` map contains a mapping from the
@ -635,22 +677,37 @@ where
&mut self,
slot: MaterialBindGroupSlot,
allocated_resource_slots: &HashMap<BindlessIndex, u32>,
bindless_descriptor: &BindlessDescriptor,
) {
let table_len = bindless_descriptor.resources.len();
let table_len = self.table_length() as usize;
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);
if self.index_range.contains(&bindless_index) {
self.buffer.set(
*bindless_index + range.start as u32 - *self.index_range.start,
resource_slot,
);
}
}
// Mark the buffer as needing to be recreated, in case we grew it.
self.buffer.dirty = BufferDirtyState::NeedsReserve;
}
/// Returns the [`BindGroupEntry`] for the index table itself.
fn bind_group_entry(&self) -> BindGroupEntry {
BindGroupEntry {
binding: *self.binding_number,
resource: self
.buffer
.buffer()
.expect("Bindings buffer must exist")
.as_entire_binding(),
}
}
}
impl<T> RetainedRawBufferVec<T>
@ -743,11 +800,7 @@ where
) -> 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,
) {
match slab.try_allocate(unprepared_bind_group, self.slab_capacity) {
Ok(slot) => {
return MaterialBindingId {
group: MaterialBindGroupIndex(slab_index as u32),
@ -767,11 +820,7 @@ where
.slabs
.last_mut()
.expect("We just pushed a slab")
.try_allocate(
unprepared_bind_group,
&self.bindless_descriptor,
self.slab_capacity,
)
.try_allocate(unprepared_bind_group, self.slab_capacity)
else {
panic!("An allocation into an empty slab should always succeed")
};
@ -852,7 +901,6 @@ where
fn try_allocate(
&mut self,
unprepared_bind_group: UnpreparedBindGroup<M::Data>,
bindless_descriptor: &BindlessDescriptor,
slot_capacity: u32,
) -> Result<MaterialBindGroupSlot, UnpreparedBindGroup<M::Data>> {
// Locate pre-existing resources, and determine how many free slots we need.
@ -890,8 +938,9 @@ where
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);
for bindless_index_table in &mut self.bindless_index_tables {
bindless_index_table.set(slot, &allocated_resource_slots);
}
// Insert extra data.
if self.extra_data.len() < (*slot as usize + 1) {
@ -1103,13 +1152,17 @@ where
/// 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()
for (bindless_index, bindless_resource_type) in
bindless_descriptor.resources.iter().enumerate()
{
let bindless_index = BindlessIndex::from(bindless_index as u32);
let Some(bindless_index_table) = self.get_bindless_index_table(bindless_index) else {
continue;
};
let Some(bindless_binding) = bindless_index_table.get_binding(slot, bindless_index)
else {
continue;
};
// Free the binding. If the resource in question was anything other
// than a data buffer, then it has a reference count and
@ -1176,8 +1229,10 @@ where
bindless_descriptor: &BindlessDescriptor,
slab_capacity: u32,
) {
// Create the bindless index table buffer if needed.
self.bindless_index_table.buffer.prepare(render_device);
// Create the bindless index table buffers if needed.
for bindless_index_table in &mut self.bindless_index_tables {
bindless_index_table.buffer.prepare(render_device);
}
// Create any data buffers we were managing if necessary.
for data_buffer in self.data_buffers.values_mut() {
@ -1232,15 +1287,11 @@ where
required_binding_array_size,
);
let mut bind_group_entries = vec![BindGroupEntry {
binding: 0,
resource: self
.bindless_index_table
.buffer
.buffer()
.expect("Bindings buffer must exist")
.as_entire_binding(),
}];
let mut bind_group_entries: Vec<_> = self
.bindless_index_tables
.iter()
.map(|bindless_index_table| bindless_index_table.bind_group_entry())
.collect();
for &(&binding, ref binding_resource_array) in binding_resource_arrays.iter() {
bind_group_entries.push(BindGroupEntry {
@ -1283,9 +1334,11 @@ where
/// Currently, this consists of the bindless index table plus any data
/// buffers we're managing.
fn write_buffer(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) {
self.bindless_index_table
.buffer
.write(render_device, render_queue);
for bindless_index_table in &mut self.bindless_index_tables {
bindless_index_table
.buffer
.write(render_device, render_queue);
}
for data_buffer in self.data_buffers.values_mut() {
data_buffer.buffer.write(render_device, render_queue);
@ -1520,6 +1573,26 @@ where
.and_then(|data| data.as_ref())
.expect("Extra data not present")
}
/// Returns the bindless index table containing the given bindless index.
fn get_bindless_index_table(
&self,
bindless_index: BindlessIndex,
) -> Option<&MaterialBindlessIndexTable<M>> {
let table_index = self
.bindless_index_tables
.binary_search_by(|bindless_index_table| {
if bindless_index < bindless_index_table.index_range.start {
Ordering::Less
} else if bindless_index >= bindless_index_table.index_range.end {
Ordering::Greater
} else {
Ordering::Equal
}
})
.ok()?;
self.bindless_index_tables.get(table_index)
}
}
impl<R> MaterialBindlessBindingArray<R>
@ -1708,9 +1781,15 @@ where
}
}
let bindless_index_tables = bindless_descriptor
.index_tables
.iter()
.map(|bindless_index_table| MaterialBindlessIndexTable::new(bindless_index_table))
.collect();
MaterialBindlessSlab {
bind_group: None,
bindless_index_table: MaterialBindlessIndexTable::new(bindless_descriptor),
bindless_index_tables,
samplers,
textures,
buffers,

View File

@ -6,7 +6,7 @@ use syn::{
parenthesized,
parse::{Parse, ParseStream},
punctuated::Punctuated,
token::Comma,
token::{Comma, DotDot},
Data, DataStruct, Error, Fields, LitInt, LitStr, Meta, MetaList, Result,
};
@ -20,6 +20,9 @@ const BINDLESS_ATTRIBUTE_NAME: Symbol = Symbol("bindless");
const DATA_ATTRIBUTE_NAME: Symbol = Symbol("data");
const BINDING_ARRAY_MODIFIER_NAME: Symbol = Symbol("binding_array");
const LIMIT_MODIFIER_NAME: Symbol = Symbol("limit");
const INDEX_TABLE_MODIFIER_NAME: Symbol = Symbol("index_table");
const RANGE_MODIFIER_NAME: Symbol = Symbol("range");
const BINDING_MODIFIER_NAME: Symbol = Symbol("binding");
#[derive(Copy, Clone, Debug)]
enum BindingType {
@ -48,6 +51,12 @@ enum BindlessSlabResourceLimitAttr {
Limit(LitInt),
}
// The `bindless(index_table(range(M..N)))` attribute.
struct BindlessIndexTableRangeAttr {
start: LitInt,
end: LitInt,
}
pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
let manifest = BevyManifest::shared();
let render_path = manifest.get_path("bevy_render");
@ -65,6 +74,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
// 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;
let mut attr_bindless_index_table_range = None;
let mut attr_bindless_index_table_binding = None;
// `actual_bindless_slot_count` holds the actual number of bindless slots
// per bind group, taking into account whether the current platform supports
@ -88,28 +99,54 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
attr_prepared_data_ident = Some(prepared_data_ident);
}
} 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::Auto);
if let Meta::List(_) = attr.meta {
// Parse bindless features.
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(());
}
attr_bindless_count = Some(BindlessSlabResourceLimitAttr::Limit(lit));
return Ok(());
}
Err(Error::new_spanned(attr, "Expected `limit(N)`"))
})?;
}
_ => {}
if submeta.path.is_ident(&INDEX_TABLE_MODIFIER_NAME) {
submeta.parse_nested_meta(|subsubmeta| {
if subsubmeta.path.is_ident(&RANGE_MODIFIER_NAME) {
let content;
parenthesized!(content in subsubmeta.input);
let start: LitInt = content.parse()?;
content.parse::<DotDot>()?;
let end: LitInt = content.parse()?;
attr_bindless_index_table_range =
Some(BindlessIndexTableRangeAttr { start, end });
return Ok(());
}
if subsubmeta.path.is_ident(&BINDING_MODIFIER_NAME) {
let content;
parenthesized!(content in subsubmeta.input);
let lit: LitInt = content.parse()?;
attr_bindless_index_table_binding = Some(lit);
return Ok(());
}
Err(Error::new_spanned(
attr,
"Expected `range(M..N)` or `binding(N)`",
))
})?;
return Ok(());
}
Err(Error::new_spanned(
attr,
"Expected `limit` or `index_table`",
))
})?;
}
}
}
@ -881,6 +918,33 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
None => quote! { 0 },
};
// Calculate the actual bindless index table range, taking the
// `#[bindless(index_table(range(M..N)))]` attribute into account.
let bindless_index_table_range = match attr_bindless_index_table_range {
None => {
let resource_count = bindless_resource_types.len() as u32;
quote! {
#render_path::render_resource::BindlessIndex(0)..
#render_path::render_resource::BindlessIndex(#resource_count)
}
}
Some(BindlessIndexTableRangeAttr { start, end }) => {
quote! {
#render_path::render_resource::BindlessIndex(#start)..
#render_path::render_resource::BindlessIndex(#end)
}
}
};
// Calculate the actual binding number of the bindless index table, taking
// the `#[bindless(index_table(binding(B)))]` into account.
let bindless_index_table_binding_number = match attr_bindless_index_table_binding {
None => quote! { #render_path::render_resource::BindingNumber(0) },
Some(binding_number) => {
quote! { #render_path::render_resource::BindingNumber(#binding_number) }
}
};
// Calculate the actual number of bindless slots, taking hardware
// limitations into account.
let (bindless_slot_count, actual_bindless_slot_count_declaration, bindless_descriptor_syntax) =
@ -942,9 +1006,18 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
]> = ::std::sync::LazyLock::new(|| {
[#(#bindless_buffer_descriptors),*]
});
static INDEX_TABLES: &[
#render_path::render_resource::BindlessIndexTableDescriptor
] = &[
#render_path::render_resource::BindlessIndexTableDescriptor {
indices: #bindless_index_table_range,
binding_number: #bindless_index_table_binding_number,
}
];
Some(#render_path::render_resource::BindlessDescriptor {
resources: ::std::borrow::Cow::Borrowed(RESOURCES),
buffers: ::std::borrow::Cow::Borrowed(&*BUFFERS),
index_tables: ::std::borrow::Cow::Borrowed(&*INDEX_TABLES),
})
};
@ -964,8 +1037,6 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
),
};
let bindless_resource_count = bindless_resource_types.len() as u32;
Ok(TokenStream::from(quote! {
#(#field_struct_impls)*
@ -1011,10 +1082,13 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
let mut #bind_group_layout_entries = Vec::new();
match #actual_bindless_slot_count {
Some(bindless_slot_count) => {
let bindless_index_table_range = #bindless_index_table_range;
#bind_group_layout_entries.extend(
#render_path::render_resource::create_bindless_bind_group_layout_entries(
#bindless_resource_count,
bindless_index_table_range.end.0 -
bindless_index_table_range.start.0,
bindless_slot_count.into(),
#bindless_index_table_binding_number,
).into_iter()
);
#(#bindless_binding_layouts)*;

View File

@ -369,12 +369,12 @@ impl Deref for BindGroup {
/// 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.
/// * By default, 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)]`. Again by default, 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
@ -384,12 +384,30 @@ impl Deref for BindGroup {
/// 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 `index_table(range(M..N), binding(B))` declaration allows you to
/// customize the layout of the bindless index table. This is useful for
/// materials that are composed of multiple bind groups, such as
/// `ExtendedMaterial`. In such cases, there will be multiple bindless index
/// tables, so they can't both be assigned to binding 0 or their bindings will
/// conflict.
/// - The `binding(B)` attribute of the `index_table` attribute allows you to
/// customize the binding (`@binding(B)`, in the shader) at which the index
/// table will be bound.
/// - The `range(M, N)` attribute of the `index_table` attribute allows you to
/// change the mapping from the field index in the bindless index table to the
/// bindless index. Instead of the field at index $i$ being mapped to the
/// bindless index $i$, with the `range(M, N)` attribute the field at index
/// $i$ in the bindless index table is mapped to the bindless index $i$ + M.
/// The size of the index table will be set to N - M. Note that this may
/// result in the table being too small to contain all the bindless bindings.
/// * 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.
/// * See the `shaders/shader_material_bindless` example for an example of how
/// to use bindless mode. See the `shaders/extended_material_bindless` example
/// for a more exotic example of bindless mode that demonstrates the
/// `index_table` attribute.
/// * The following diagram illustrates how bindless mode works using a subset
/// of `StandardMaterial`:
///

View File

@ -1,7 +1,10 @@
//! Types and functions relating to bindless resources.
use alloc::borrow::Cow;
use core::num::{NonZeroU32, NonZeroU64};
use core::{
num::{NonZeroU32, NonZeroU64},
ops::Range,
};
use bevy_derive::{Deref, DerefMut};
use wgpu::{
@ -102,6 +105,11 @@ pub struct BindlessDescriptor {
///
/// The order of this array is irrelevant.
pub buffers: Cow<'static, [BindlessBufferDescriptor]>,
/// The [`BindlessIndexTableDescriptor`]s describing each bindless index
/// table.
///
/// This list must be sorted by the first bindless index.
pub index_tables: Cow<'static, [BindlessIndexTableDescriptor]>,
}
/// The type of potentially-bindless resource.
@ -165,7 +173,7 @@ pub enum BindlessResourceType {
/// `#[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)]
#[derive(Clone, Copy, Debug)]
pub struct BindlessBufferDescriptor {
/// The actual binding number of the buffer.
///
@ -185,6 +193,19 @@ pub struct BindlessBufferDescriptor {
pub size: Option<usize>,
}
/// Describes the layout of the bindless index table, which maps bindless
/// indices to indices within the binding arrays.
#[derive(Clone)]
pub struct BindlessIndexTableDescriptor {
/// The range of bindless indices that this descriptor covers.
pub indices: Range<BindlessIndex>,
/// The binding at which the index table itself will be bound.
///
/// By default, this is binding 0, but it can be changed with the
/// `#[bindless(index_table(binding(B)))]` attribute.
pub binding_number: BindingNumber,
}
/// The index of the actual binding in the bind group.
///
/// This is the value specified in WGSL as `@binding`.
@ -194,7 +215,7 @@ 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)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Hash, Debug, Deref, DerefMut)]
pub struct BindlessIndex(pub u32);
/// Creates the bind group layout entries common to all shaders that use
@ -204,8 +225,9 @@ pub struct BindlessIndex(pub u32);
/// `bindless_slab_resource_limit` specifies the resolved
/// [`BindlessSlabResourceLimit`] value.
pub fn create_bindless_bind_group_layout_entries(
bindless_resource_count: u32,
bindless_index_table_length: u32,
bindless_slab_resource_limit: u32,
bindless_index_table_binding_number: BindingNumber,
) -> Vec<BindGroupLayoutEntry> {
let bindless_slab_resource_limit =
NonZeroU32::new(bindless_slab_resource_limit).expect("Bindless slot count must be nonzero");
@ -219,10 +241,10 @@ pub fn create_bindless_bind_group_layout_entries(
// 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::<u32>() as u64),
NonZeroU64::new(bindless_index_table_length as u64 * size_of::<u32>() as u64),
)
.build(0, ShaderStages::all()),
// Continue with the common bindless buffers.
.build(*bindless_index_table_binding_number, ShaderStages::all()),
// Continue with the common bindless resource arrays.
sampler(SamplerBindingType::Filtering)
.count(bindless_slab_resource_limit)
.build(1, ShaderStages::all()),

View File

@ -453,6 +453,7 @@ Example | Description
[Custom Render Phase](../examples/shader/custom_render_phase.rs) | Shows how to make a complete render phase
[Custom Vertex Attribute](../examples/shader/custom_vertex_attribute.rs) | A shader that reads a mesh's custom vertex attribute
[Custom phase item](../examples/shader/custom_phase_item.rs) | Demonstrates how to enqueue custom draw commands in a render phase
[Extended Bindless Material](../examples/shader/extended_material_bindless.rs) | Demonstrates bindless `ExtendedMaterial`
[Extended Material](../examples/shader/extended_material.rs) | A custom shader that builds on the standard material
[GPU readback](../examples/shader/gpu_readback.rs) | A very simple compute shader that writes to a buffer that is read by the cpu
[Instancing](../examples/shader/custom_shader_instancing.rs) | A shader that renders a mesh multiple times in one draw call using low level rendering api

View File

@ -0,0 +1,156 @@
//! Demonstrates bindless `ExtendedMaterial`.
use std::f32::consts::FRAC_PI_2;
use bevy::{
color::palettes::{css::RED, tailwind::GRAY_600},
pbr::{ExtendedMaterial, MaterialExtension, MeshMaterial3d},
prelude::*,
render::{
mesh::{SphereKind, SphereMeshBuilder},
render_resource::{AsBindGroup, ShaderRef, ShaderType},
},
utils::default,
};
/// The path to the example material shader.
static SHADER_ASSET_PATH: &str = "shaders/extended_material_bindless.wgsl";
/// The example bindless material extension.
///
/// As usual for material extensions, we need to avoid conflicting with both the
/// binding numbers and bindless indices of the [`StandardMaterial`], so we
/// start both values at 100 and 50 respectively.
///
/// The `#[data(50, ExampleBindlessExtensionUniform, binding_array(101))]`
/// attribute specifies that the plain old data
/// [`ExampleBindlessExtensionUniform`] will be placed into an array with
/// binding 100 and will occupy index 50 in the
/// `ExampleBindlessExtendedMaterialIndices` structure. (See the shader for the
/// definition of that structure.) That corresponds to the following shader
/// declaration:
///
/// ```wgsl
/// @group(2) @binding(100) var<storage> example_extended_material_indices:
/// array<ExampleBindlessExtendedMaterialIndices>;
/// ```
///
/// The `#[bindless(index_table(range(50..53), binding(100)))]` attribute
/// specifies that this material extension should be bindless. The `range`
/// subattribute specifies that this material extension should have its own
/// index table covering bindings 50, 51, and 52. The `binding` subattribute
/// specifies that the extended material index table should be bound to binding
/// 100. This corresponds to the following shader declarations:
///
/// ```wgsl
/// struct ExampleBindlessExtendedMaterialIndices {
/// material: u32, // 50
/// modulate_texture: u32, // 51
/// modulate_texture_sampler: u32, // 52
/// }
///
/// @group(2) @binding(100) var<storage> example_extended_material_indices:
/// array<ExampleBindlessExtendedMaterialIndices>;
/// ```
///
/// We need to use the `index_table` subattribute because the
/// [`StandardMaterial`] bindless index table is bound to binding 0 by default.
/// Thus we need to specify a different binding so that our extended bindless
/// index table doesn't conflict.
#[derive(Asset, Clone, Reflect, AsBindGroup)]
#[data(50, ExampleBindlessExtensionUniform, binding_array(101))]
#[bindless(index_table(range(50..53), binding(100)))]
struct ExampleBindlessExtension {
/// The color we're going to multiply the base color with.
modulate_color: Color,
/// The image we're going to multiply the base color with.
#[texture(51)]
#[sampler(52)]
modulate_texture: Option<Handle<Image>>,
}
/// The GPU-side data structure specifying plain old data for the material
/// extension.
#[derive(Clone, Default, ShaderType)]
struct ExampleBindlessExtensionUniform {
/// The GPU representation of the color we're going to multiply the base
/// color with.
modulate_color: Vec4,
}
impl MaterialExtension for ExampleBindlessExtension {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
}
impl<'a> From<&'a ExampleBindlessExtension> for ExampleBindlessExtensionUniform {
fn from(material_extension: &'a ExampleBindlessExtension) -> Self {
// Convert the CPU `ExampleBindlessExtension` structure to its GPU
// format.
ExampleBindlessExtensionUniform {
modulate_color: LinearRgba::from(material_extension.modulate_color).to_vec4(),
}
}
}
/// The entry point.
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(MaterialPlugin::<
ExtendedMaterial<StandardMaterial, ExampleBindlessExtension>,
>::default())
.add_systems(Startup, setup)
.add_systems(Update, rotate_sphere)
.run();
}
/// Creates the scene.
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, ExampleBindlessExtension>>>,
) {
// Create a gray sphere, modulated with a red-tinted Bevy logo.
commands.spawn((
Mesh3d(meshes.add(SphereMeshBuilder::new(
1.0,
SphereKind::Uv {
sectors: 20,
stacks: 20,
},
))),
MeshMaterial3d(materials.add(ExtendedMaterial {
base: StandardMaterial {
base_color: GRAY_600.into(),
..default()
},
extension: ExampleBindlessExtension {
modulate_color: RED.into(),
modulate_texture: Some(asset_server.load("textures/uv_checker_bw.png")),
},
})),
Transform::from_xyz(0.0, 0.5, 0.0),
));
// Create a light.
commands.spawn((
DirectionalLight::default(),
Transform::from_xyz(1.0, 1.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y),
));
// Create a camera.
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
fn rotate_sphere(mut meshes: Query<&mut Transform, With<Mesh3d>>, time: Res<Time>) {
for mut transform in &mut meshes {
transform.rotation =
Quat::from_euler(EulerRot::YXZ, -time.elapsed_secs(), FRAC_PI_2 * 3.0, 0.0);
}
}