Add a new #[data] attribute to AsBindGroup that allows packing data for multiple materials into a single array. (#17965)

Currently, the structure-level `#[uniform]` attribute of `AsBindGroup`
creates a binding array of individual buffers, each of which contains
data for a single material. A more efficient approach would be to
provide a single buffer with an array containing all of the data for all
materials in the bind group. Because `StandardMaterial` uses
`#[uniform]`, this can be notably inefficient with large numbers of
materials.

This patch introduces a new attribute on `AsBindGroup`, `#[data]`, which
works identically to `#[uniform]` except that it concatenates all the
data into a single buffer that the material bind group allocator itself
manages. It also converts `StandardMaterial` to use this new
functionality. This effectively provides the "material data in arrays"
feature.
This commit is contained in:
Patrick Walton 2025-02-24 13:38:55 -08:00 committed by GitHub
parent a1717331e4
commit 5d7a60592d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 483 additions and 136 deletions

View File

@ -17,15 +17,16 @@ use bevy_render::{
render_resource::{ render_resource::{
BindGroup, BindGroupEntry, BindGroupLayout, BindingNumber, BindingResource, BindGroup, BindGroupEntry, BindGroupLayout, BindingNumber, BindingResource,
BindingResources, BindlessDescriptor, BindlessIndex, BindlessResourceType, Buffer, BindingResources, BindlessDescriptor, BindlessIndex, BindlessResourceType, Buffer,
BufferBinding, BufferDescriptor, BufferId, BufferUsages, CompareFunction, FilterMode, BufferBinding, BufferDescriptor, BufferId, BufferInitDescriptor, BufferUsages,
OwnedBindingResource, PreparedBindGroup, RawBufferVec, Sampler, SamplerDescriptor, CompareFunction, FilterMode, OwnedBindingResource, PreparedBindGroup, RawBufferVec,
SamplerId, TextureView, TextureViewDimension, TextureViewId, UnpreparedBindGroup, Sampler, SamplerDescriptor, SamplerId, TextureView, TextureViewDimension, TextureViewId,
WgpuSampler, WgpuTextureView, UnpreparedBindGroup, WgpuSampler, WgpuTextureView,
}, },
renderer::{RenderDevice, RenderQueue}, renderer::{RenderDevice, RenderQueue},
texture::FallbackImage, texture::FallbackImage,
}; };
use bevy_utils::default; use bevy_utils::default;
use bytemuck::Pod;
use tracing::{error, trace}; use tracing::{error, trace};
use crate::Material; use crate::Material;
@ -97,8 +98,11 @@ where
samplers: HashMap<BindlessResourceType, MaterialBindlessBindingArray<Sampler>>, samplers: HashMap<BindlessResourceType, MaterialBindlessBindingArray<Sampler>>,
/// The binding arrays containing textures. /// The binding arrays containing textures.
textures: HashMap<BindlessResourceType, MaterialBindlessBindingArray<TextureView>>, textures: HashMap<BindlessResourceType, MaterialBindlessBindingArray<TextureView>>,
/// The binding arrays containing data buffers. /// The binding arrays containing buffers.
buffers: HashMap<BindlessIndex, MaterialBindlessBindingArray<Buffer>>, buffers: HashMap<BindlessIndex, MaterialBindlessBindingArray<Buffer>>,
/// The buffers that contain plain old data (i.e. the structure-level
/// `#[data]` attribute of `AsBindGroup`).
data_buffers: HashMap<BindlessIndex, MaterialDataBuffer>,
/// Holds extra CPU-accessible data that the material provides. /// Holds extra CPU-accessible data that the material provides.
/// ///
@ -122,10 +126,8 @@ struct MaterialBindlessIndexTable<M>
where where
M: Material, M: Material,
{ {
/// The contents of the buffer. /// The buffer containing the mappings.
buffer: RawBufferVec<u32>, buffer: RetainedRawBufferVec<u32>,
/// Whether the contents of the buffer have been uploaded to the GPU.
buffer_dirty: BufferDirtyState,
phantom: PhantomData<M>, phantom: PhantomData<M>,
} }
@ -204,7 +206,11 @@ where
layout: BindGroupLayout, layout: BindGroupLayout,
}, },
/// A bind group that's already been prepared. /// A bind group that's already been prepared.
Prepared(PreparedBindGroup<M::Data>), Prepared {
bind_group: PreparedBindGroup<M::Data>,
#[expect(dead_code, reason = "These buffers are only referenced by bind groups")]
uniform_buffers: Vec<Buffer>,
},
} }
/// Dummy instances of various resources that we fill unused slots in binding /// Dummy instances of various resources that we fill unused slots in binding
@ -228,6 +234,11 @@ enum BindingResourceId {
TextureView(TextureViewDimension, TextureViewId), TextureView(TextureViewDimension, TextureViewId),
/// A sampler. /// A sampler.
Sampler(SamplerId), Sampler(SamplerId),
/// A buffer containing plain old data.
///
/// This corresponds to the `#[data]` structure-level attribute on
/// `AsBindGroup`.
DataBuffer,
} }
/// A temporary list of references to `wgpu` bindless resources. /// A temporary list of references to `wgpu` bindless resources.
@ -352,6 +363,44 @@ where
Unprepared(&'a UnpreparedBindGroup<M::Data>), Unprepared(&'a UnpreparedBindGroup<M::Data>),
} }
/// Manages an array of untyped plain old data on GPU and allocates individual
/// slots within that array.
///
/// This supports the `#[data]` attribute of `AsBindGroup`.
struct MaterialDataBuffer {
/// The number of the binding that we attach this storage buffer to.
binding_number: BindingNumber,
/// The actual data.
///
/// Note that this is untyped (`u8`); the actual aligned size of each
/// element is given by [`Self::aligned_element_size`];
buffer: RetainedRawBufferVec<u8>,
/// The size of each element in the buffer, including padding and alignment
/// if any.
aligned_element_size: u32,
/// A list of free slots within the buffer.
free_slots: Vec<u32>,
/// The actual number of slots that have been allocated.
len: u32,
}
/// A buffer containing plain old data, already packed into the appropriate GPU
/// format, and that can be updated incrementally.
///
/// This structure exists in order to encapsulate the lazy update
/// ([`BufferDirtyState`]) logic in a single place.
#[derive(Deref, DerefMut)]
struct RetainedRawBufferVec<T>
where
T: Pod,
{
/// The contents of the buffer.
#[deref]
buffer: RawBufferVec<T>,
/// Whether the contents of the buffer have been uploaded to the GPU.
dirty: BufferDirtyState,
}
impl From<u32> for MaterialBindGroupSlot { impl From<u32> for MaterialBindGroupSlot {
fn from(value: u32) -> Self { fn from(value: u32) -> Self {
MaterialBindGroupSlot(value) MaterialBindGroupSlot(value)
@ -368,6 +417,7 @@ impl<'a> From<&'a OwnedBindingResource> for BindingResourceId {
fn from(value: &'a OwnedBindingResource) -> Self { fn from(value: &'a OwnedBindingResource) -> Self {
match *value { match *value {
OwnedBindingResource::Buffer(ref buffer) => BindingResourceId::Buffer(buffer.id()), OwnedBindingResource::Buffer(ref buffer) => BindingResourceId::Buffer(buffer.id()),
OwnedBindingResource::Data(_) => BindingResourceId::DataBuffer,
OwnedBindingResource::TextureView(ref texture_view_dimension, ref texture_view) => { OwnedBindingResource::TextureView(ref texture_view_dimension, ref texture_view) => {
BindingResourceId::TextureView(*texture_view_dimension, texture_view.id()) BindingResourceId::TextureView(*texture_view_dimension, texture_view.id())
} }
@ -545,14 +595,13 @@ where
/// Creates a new [`MaterialBindlessIndexTable`] for a single slab. /// Creates a new [`MaterialBindlessIndexTable`] for a single slab.
fn new(bindless_descriptor: &BindlessDescriptor) -> MaterialBindlessIndexTable<M> { fn new(bindless_descriptor: &BindlessDescriptor) -> MaterialBindlessIndexTable<M> {
// Preallocate space for one bindings table, so that there will always be a buffer. // Preallocate space for one bindings table, so that there will always be a buffer.
let mut buffer = RawBufferVec::new(BufferUsages::STORAGE); let mut buffer = RetainedRawBufferVec::new(BufferUsages::STORAGE);
for _ in 0..bindless_descriptor.resources.len() { for _ in 0..bindless_descriptor.resources.len() {
buffer.push(0); buffer.push(0);
} }
MaterialBindlessIndexTable { MaterialBindlessIndexTable {
buffer, buffer,
buffer_dirty: BufferDirtyState::NeedsReserve,
phantom: PhantomData, phantom: PhantomData,
} }
} }
@ -592,29 +641,42 @@ where
} }
// Mark the buffer as needing to be recreated, in case we grew it. // Mark the buffer as needing to be recreated, in case we grew it.
self.buffer_dirty = BufferDirtyState::NeedsReserve; self.buffer.dirty = BufferDirtyState::NeedsReserve;
}
}
impl<T> RetainedRawBufferVec<T>
where
T: Pod,
{
/// Creates a new empty [`RetainedRawBufferVec`] supporting the given
/// [`BufferUsages`].
fn new(buffer_usages: BufferUsages) -> RetainedRawBufferVec<T> {
RetainedRawBufferVec {
buffer: RawBufferVec::new(buffer_usages),
dirty: BufferDirtyState::NeedsUpload,
}
} }
/// Creates the buffer that contains the bindless index table if necessary. /// Recreates the GPU backing buffer if needed.
fn prepare_buffer(&mut self, render_device: &RenderDevice) { fn prepare(&mut self, render_device: &RenderDevice) {
match self.buffer_dirty { match self.dirty {
BufferDirtyState::Clean | BufferDirtyState::NeedsUpload => {} BufferDirtyState::Clean | BufferDirtyState::NeedsUpload => {}
BufferDirtyState::NeedsReserve => { BufferDirtyState::NeedsReserve => {
let capacity = self.buffer.len(); let capacity = self.buffer.len();
self.buffer.reserve(capacity, render_device); self.buffer.reserve(capacity, render_device);
self.buffer_dirty = BufferDirtyState::NeedsUpload; self.dirty = BufferDirtyState::NeedsUpload;
} }
} }
} }
/// Writes the contents of the bindless index table buffer to GPU if /// Writes the current contents of the buffer to the GPU if necessary.
/// necessary. fn write(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) {
fn write_buffer(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { match self.dirty {
match self.buffer_dirty {
BufferDirtyState::Clean => {} BufferDirtyState::Clean => {}
BufferDirtyState::NeedsReserve | BufferDirtyState::NeedsUpload => { BufferDirtyState::NeedsReserve | BufferDirtyState::NeedsUpload => {
self.buffer.write_buffer(render_device, render_queue); self.buffer.write_buffer(render_device, render_queue);
self.buffer_dirty = BufferDirtyState::Clean; self.dirty = BufferDirtyState::Clean;
} }
} }
} }
@ -863,6 +925,10 @@ where
} }
} }
OwnedBindingResource::Data(_) => {
// The size of a data buffer is unlimited.
}
OwnedBindingResource::TextureView(texture_view_dimension, ref texture_view) => { OwnedBindingResource::TextureView(texture_view_dimension, ref texture_view) => {
let bindless_resource_type = BindlessResourceType::from(texture_view_dimension); let bindless_resource_type = BindlessResourceType::from(texture_view_dimension);
match self match self
@ -940,6 +1006,11 @@ where
.expect("Slot should exist") .expect("Slot should exist")
.ref_count += 1; .ref_count += 1;
} }
OwnedBindingResource::Data(_) => {
panic!("Data buffers can't be deduplicated")
}
OwnedBindingResource::TextureView(texture_view_dimension, _) => { OwnedBindingResource::TextureView(texture_view_dimension, _) => {
let bindless_resource_type = let bindless_resource_type =
BindlessResourceType::from(texture_view_dimension); BindlessResourceType::from(texture_view_dimension);
@ -952,6 +1023,7 @@ where
.expect("Slot should exist") .expect("Slot should exist")
.ref_count += 1; .ref_count += 1;
} }
OwnedBindingResource::Sampler(sampler_binding_type, _) => { OwnedBindingResource::Sampler(sampler_binding_type, _) => {
let bindless_resource_type = let bindless_resource_type =
BindlessResourceType::from(sampler_binding_type); BindlessResourceType::from(sampler_binding_type);
@ -980,6 +1052,14 @@ where
.insert(binding_resource_id, buffer); .insert(binding_resource_id, buffer);
allocated_resource_slots.insert(bindless_index, slot); allocated_resource_slots.insert(bindless_index, slot);
} }
OwnedBindingResource::Data(data) => {
let slot = self
.data_buffers
.get_mut(&bindless_index)
.expect("Data buffer binding array should exist")
.insert(&data);
allocated_resource_slots.insert(bindless_index, slot);
}
OwnedBindingResource::TextureView(texture_view_dimension, texture_view) => { OwnedBindingResource::TextureView(texture_view_dimension, texture_view) => {
let bindless_resource_type = BindlessResourceType::from(texture_view_dimension); let bindless_resource_type = BindlessResourceType::from(texture_view_dimension);
let slot = self let slot = self
@ -1019,14 +1099,23 @@ where
{ {
let bindless_index = BindlessIndex::from(bindless_index as u32); let bindless_index = BindlessIndex::from(bindless_index as u32);
// Free the binding. // Free the binding. If the resource in question was anything other
let resource_freed = match *bindless_resource_type { // than a data buffer, then it has a reference count and
// consequently we need to decrement it.
let decrement_allocated_resource_count = match *bindless_resource_type {
BindlessResourceType::None => false, BindlessResourceType::None => false,
BindlessResourceType::Buffer => self BindlessResourceType::Buffer => self
.buffers .buffers
.get_mut(&bindless_index) .get_mut(&bindless_index)
.expect("Buffer should exist with that bindless index") .expect("Buffer should exist with that bindless index")
.remove(bindless_binding), .remove(bindless_binding),
BindlessResourceType::DataBuffer => {
self.data_buffers
.get_mut(&bindless_index)
.expect("Data buffer should exist with that bindless index")
.remove(bindless_binding);
false
}
BindlessResourceType::SamplerFiltering BindlessResourceType::SamplerFiltering
| BindlessResourceType::SamplerNonFiltering | BindlessResourceType::SamplerNonFiltering
| BindlessResourceType::SamplerComparison => self | BindlessResourceType::SamplerComparison => self
@ -1048,7 +1137,7 @@ where
// If the slot is now free, decrement the allocated resource // If the slot is now free, decrement the allocated resource
// count. // count.
if resource_freed { if decrement_allocated_resource_count {
self.allocated_resource_count -= 1; self.allocated_resource_count -= 1;
} }
} }
@ -1075,7 +1164,12 @@ where
bindless_descriptor: &BindlessDescriptor, bindless_descriptor: &BindlessDescriptor,
) { ) {
// Create the bindless index table buffer if needed. // Create the bindless index table buffer if needed.
self.bindless_index_table.prepare_buffer(render_device); self.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() {
data_buffer.buffer.prepare(render_device);
}
// Create the bind group if needed. // Create the bind group if needed.
self.prepare_bind_group( self.prepare_bind_group(
@ -1138,6 +1232,18 @@ where
}); });
} }
// Create bind group entries for any data buffers we're managing.
for data_buffer in self.data_buffers.values() {
bind_group_entries.push(BindGroupEntry {
binding: *data_buffer.binding_number,
resource: data_buffer
.buffer
.buffer()
.expect("Backing data buffer must have been uploaded by now")
.as_entire_binding(),
});
}
self.bind_group = Some(render_device.create_bind_group( self.bind_group = Some(render_device.create_bind_group(
M::label(), M::label(),
bind_group_layout, bind_group_layout,
@ -1147,10 +1253,16 @@ where
/// Writes any buffers that we're managing to the GPU. /// Writes any buffers that we're managing to the GPU.
/// ///
/// Currently, this only consists of the bindless index table. /// 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) { fn write_buffer(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) {
self.bindless_index_table self.bindless_index_table
.write_buffer(render_device, render_queue); .buffer
.write(render_device, render_queue);
for data_buffer in self.data_buffers.values_mut() {
data_buffer.buffer.write(render_device, render_queue);
}
} }
/// Converts our binding arrays into binding resource arrays suitable for /// Converts our binding arrays into binding resource arrays suitable for
@ -1300,11 +1412,9 @@ where
let Some(buffer_bindless_binding_array) = let Some(buffer_bindless_binding_array) =
self.buffers.get(&bindless_buffer_descriptor.bindless_index) self.buffers.get(&bindless_buffer_descriptor.bindless_index)
else { else {
error!( // This is OK, because index buffers are present in
"Slab didn't contain a binding array for buffer binding {:?}, bindless {:?}", // `BindlessDescriptor::buffers` but not in
bindless_buffer_descriptor.binding_number, // `BindlessDescriptor::resources`.
bindless_buffer_descriptor.bindless_index,
);
continue; continue;
}; };
let buffer_bindings = buffer_bindless_binding_array let buffer_bindings = buffer_bindless_binding_array
@ -1456,6 +1566,7 @@ where
let mut buffers = HashMap::default(); let mut buffers = HashMap::default();
let mut samplers = HashMap::default(); let mut samplers = HashMap::default();
let mut textures = HashMap::default(); let mut textures = HashMap::default();
let mut data_buffers = HashMap::default();
for (bindless_index, bindless_resource_type) in for (bindless_index, bindless_resource_type) in
bindless_descriptor.resources.iter().enumerate() bindless_descriptor.resources.iter().enumerate()
@ -1480,6 +1591,26 @@ where
MaterialBindlessBindingArray::new(binding_number, *bindless_resource_type), MaterialBindlessBindingArray::new(binding_number, *bindless_resource_type),
); );
} }
BindlessResourceType::DataBuffer => {
// Copy the data in.
let buffer_descriptor = 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",
);
data_buffers.insert(
bindless_index,
MaterialDataBuffer::new(
buffer_descriptor.binding_number,
buffer_descriptor.size as u32,
),
);
}
BindlessResourceType::SamplerFiltering BindlessResourceType::SamplerFiltering
| BindlessResourceType::SamplerNonFiltering | BindlessResourceType::SamplerNonFiltering
| BindlessResourceType::SamplerComparison => { | BindlessResourceType::SamplerComparison => {
@ -1514,6 +1645,7 @@ where
samplers, samplers,
textures, textures,
buffers, buffers,
data_buffers,
extra_data: vec![], extra_data: vec![],
free_slots: vec![], free_slots: vec![],
live_allocation_count: 0, live_allocation_count: 0,
@ -1613,9 +1745,10 @@ where
&mut self, &mut self,
prepared_bind_group: PreparedBindGroup<M::Data>, prepared_bind_group: PreparedBindGroup<M::Data>,
) -> MaterialBindingId { ) -> MaterialBindingId {
self.allocate(MaterialNonBindlessAllocatedBindGroup::Prepared( self.allocate(MaterialNonBindlessAllocatedBindGroup::Prepared {
prepared_bind_group, bind_group: prepared_bind_group,
)) uniform_buffers: vec![],
})
} }
/// Deallocates the bind group with the given binding ID. /// Deallocates the bind group with the given binding ID.
@ -1632,8 +1765,8 @@ where
self.bind_groups[group.0 as usize] self.bind_groups[group.0 as usize]
.as_ref() .as_ref()
.map(|bind_group| match bind_group { .map(|bind_group| match bind_group {
MaterialNonBindlessAllocatedBindGroup::Prepared(prepared_bind_group) => { MaterialNonBindlessAllocatedBindGroup::Prepared { bind_group, .. } => {
MaterialNonBindlessSlab::Prepared(prepared_bind_group) MaterialNonBindlessSlab::Prepared(bind_group)
} }
MaterialNonBindlessAllocatedBindGroup::Unprepared { bind_group, .. } => { MaterialNonBindlessAllocatedBindGroup::Unprepared { bind_group, .. } => {
MaterialNonBindlessSlab::Unprepared(bind_group) MaterialNonBindlessSlab::Unprepared(bind_group)
@ -1657,25 +1790,58 @@ where
panic!("Allocation didn't exist or was already prepared"); panic!("Allocation didn't exist or was already prepared");
}; };
let entries: Vec<_> = unprepared_bind_group // Pack any `Data` into uniform buffers.
.bindings let mut uniform_buffers = vec![];
.iter() for (index, binding) in unprepared_bind_group.bindings.iter() {
.map(|(index, binding)| BindGroupEntry { let OwnedBindingResource::Data(ref owned_data) = *binding else {
binding: *index, continue;
resource: binding.get_binding(), };
}) let label = format!("material uniform data {}", *index);
.collect(); let uniform_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
label: Some(&label),
contents: &owned_data.0,
usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM,
});
uniform_buffers.push(uniform_buffer);
}
let bind_group = // Create bind group entries.
render_device.create_bind_group(M::label(), &bind_group_layout, &entries); let mut bind_group_entries = vec![];
let mut uniform_buffers_iter = uniform_buffers.iter();
for (index, binding) in unprepared_bind_group.bindings.iter() {
match *binding {
OwnedBindingResource::Data(_) => {
bind_group_entries.push(BindGroupEntry {
binding: *index,
resource: uniform_buffers_iter
.next()
.expect("We should have created uniform buffers for each `Data`")
.as_entire_binding(),
});
}
_ => bind_group_entries.push(BindGroupEntry {
binding: *index,
resource: binding.get_binding(),
}),
}
}
self.bind_groups[*bind_group_index as usize] = Some( // Create the bind group.
MaterialNonBindlessAllocatedBindGroup::Prepared(PreparedBindGroup { let bind_group = render_device.create_bind_group(
bindings: unprepared_bind_group.bindings, M::label(),
bind_group, &bind_group_layout,
data: unprepared_bind_group.data, &bind_group_entries,
}),
); );
self.bind_groups[*bind_group_index as usize] =
Some(MaterialNonBindlessAllocatedBindGroup::Prepared {
bind_group: PreparedBindGroup {
bindings: unprepared_bind_group.bindings,
bind_group,
data: unprepared_bind_group.data,
},
uniform_buffers,
});
} }
} }
} }
@ -1720,3 +1886,57 @@ where
} }
} }
} }
impl MaterialDataBuffer {
/// Creates a new [`MaterialDataBuffer`] managing a buffer of elements of
/// size `aligned_element_size` that will be bound to the given binding
/// number.
fn new(binding_number: BindingNumber, aligned_element_size: u32) -> MaterialDataBuffer {
MaterialDataBuffer {
binding_number,
buffer: RetainedRawBufferVec::new(BufferUsages::STORAGE),
aligned_element_size,
free_slots: vec![],
len: 0,
}
}
/// Allocates a slot for a new piece of data, copies the data into that
/// slot, and returns the slot ID.
///
/// The size of the piece of data supplied to this method must equal the
/// [`Self::aligned_element_size`] provided to [`MaterialDataBuffer::new`].
fn insert(&mut self, data: &[u8]) -> u32 {
// Make the the data is of the right length.
debug_assert_eq!(data.len(), self.aligned_element_size as usize);
// Grab a slot.
let slot = self.free_slots.pop().unwrap_or(self.len);
// Calculate the range we're going to copy to.
let start = slot as usize * self.aligned_element_size as usize;
let end = (slot as usize + 1) * self.aligned_element_size as usize;
// Resize the buffer if necessary.
if self.buffer.len() < end {
self.buffer.reserve_internal(end);
}
while self.buffer.values().len() < end {
self.buffer.push(0);
}
// Copy in the data.
self.buffer.values_mut()[start..end].copy_from_slice(data);
// Mark the buffer dirty, and finish up.
self.len += 1;
self.buffer.dirty = BufferDirtyState::NeedsReserve;
slot
}
/// Marks the given slot as free.
fn remove(&mut self, slot: u32) {
self.free_slots.push(slot);
self.len -= 1;
}
}

View File

@ -30,7 +30,7 @@ pub enum UvChannel {
/// May be created directly from a [`Color`] or an [`Image`]. /// May be created directly from a [`Color`] or an [`Image`].
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)] #[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
#[bind_group_data(StandardMaterialKey)] #[bind_group_data(StandardMaterialKey)]
#[uniform(0, StandardMaterialUniform, binding_array(10))] #[data(0, StandardMaterialUniform, binding_array(10))]
#[bindless] #[bindless]
#[reflect(Default, Debug)] #[reflect(Default, Debug)]
pub struct StandardMaterial { pub struct StandardMaterial {

View File

@ -46,7 +46,7 @@ struct StandardMaterialBindings {
} }
@group(2) @binding(0) var<storage> material_indices: array<StandardMaterialBindings>; @group(2) @binding(0) var<storage> material_indices: array<StandardMaterialBindings>;
@group(2) @binding(10) var<storage> material_array: binding_array<StandardMaterial>; @group(2) @binding(10) var<storage> material_array: array<StandardMaterial>;
#else // BINDLESS #else // BINDLESS

View File

@ -17,6 +17,7 @@ const SAMPLER_ATTRIBUTE_NAME: Symbol = Symbol("sampler");
const STORAGE_ATTRIBUTE_NAME: Symbol = Symbol("storage"); const STORAGE_ATTRIBUTE_NAME: Symbol = Symbol("storage");
const BIND_GROUP_DATA_ATTRIBUTE_NAME: Symbol = Symbol("bind_group_data"); const BIND_GROUP_DATA_ATTRIBUTE_NAME: Symbol = Symbol("bind_group_data");
const BINDLESS_ATTRIBUTE_NAME: Symbol = Symbol("bindless"); const BINDLESS_ATTRIBUTE_NAME: Symbol = Symbol("bindless");
const DATA_ATTRIBUTE_NAME: Symbol = Symbol("data");
const BINDING_ARRAY_MODIFIER_NAME: Symbol = Symbol("binding_array"); const BINDING_ARRAY_MODIFIER_NAME: Symbol = Symbol("binding_array");
const LIMIT_MODIFIER_NAME: Symbol = Symbol("limit"); const LIMIT_MODIFIER_NAME: Symbol = Symbol("limit");
@ -117,30 +118,102 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
// Read struct-level attributes, second pass. // Read struct-level attributes, second pass.
for attr in &ast.attrs { for attr in &ast.attrs {
if let Some(attr_ident) = attr.path().get_ident() { if let Some(attr_ident) = attr.path().get_ident() {
if attr_ident == UNIFORM_ATTRIBUTE_NAME { if attr_ident == UNIFORM_ATTRIBUTE_NAME || attr_ident == DATA_ATTRIBUTE_NAME {
let UniformBindingAttr { let UniformBindingAttr {
binding_type,
binding_index, binding_index,
converted_shader_type, converted_shader_type,
binding_array: binding_array_binding, binding_array: binding_array_binding,
} = get_uniform_binding_attr(attr)?; } = get_uniform_binding_attr(attr)?;
binding_impls.push(quote! {{ match binding_type {
use #render_path::render_resource::AsBindGroupShaderType; UniformBindingAttrType::Uniform => {
let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new()); binding_impls.push(quote! {{
let converted: #converted_shader_type = self.as_bind_group_shader_type(&images); use #render_path::render_resource::AsBindGroupShaderType;
buffer.write(&converted).unwrap(); let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new());
( let converted: #converted_shader_type = self.as_bind_group_shader_type(&images);
#binding_index, buffer.write(&converted).unwrap();
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data( (
&#render_path::render_resource::BufferInitDescriptor { #binding_index,
label: None, #render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
usage: #uniform_buffer_usages, &#render_path::render_resource::BufferInitDescriptor {
contents: buffer.as_ref(), label: None,
}, usage: #uniform_buffer_usages,
)) contents: buffer.as_ref(),
) },
}}); ))
)
}});
// Push the binding layout. This depends on whether we're bindless or not. match (&binding_array_binding, &attr_bindless_count) {
(&None, &Some(_)) => {
return Err(Error::new_spanned(
attr,
"Must specify `binding_array(...)` with `#[uniform]` if the \
object is bindless",
));
}
(&Some(_), &None) => {
return Err(Error::new_spanned(
attr,
"`binding_array(...)` with `#[uniform]` requires the object to \
be bindless",
));
}
_ => {}
}
}
UniformBindingAttrType::Data => {
binding_impls.push(quote! {{
use #render_path::render_resource::AsBindGroupShaderType;
use #render_path::render_resource::encase::{ShaderType, internal::WriteInto};
let mut buffer: Vec<u8> = Vec::new();
let converted: #converted_shader_type = self.as_bind_group_shader_type(&images);
converted.write_into(
&mut #render_path::render_resource::encase::internal::Writer::new(
&converted,
&mut buffer,
0,
).unwrap(),
);
let min_size = <#converted_shader_type as #render_path::render_resource::ShaderType>::min_size().get() as usize;
while buffer.len() < min_size {
buffer.push(0);
}
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::Data(
#render_path::render_resource::OwnedData(buffer)
)
)
}});
let binding_array_binding = binding_array_binding.unwrap_or(0);
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: None,
}
)
});
add_bindless_resource_type(
&render_path,
&mut bindless_resource_types,
binding_index,
quote! { #render_path::render_resource::BindlessResourceType::DataBuffer },
);
}
}
// Push the non-bindless binding layout.
non_bindless_binding_layouts.push(quote!{ non_bindless_binding_layouts.push(quote!{
#bind_group_layout_entries.push( #bind_group_layout_entries.push(
@ -157,66 +230,23 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
); );
}); });
match binding_array_binding { bindless_buffer_descriptors.push(quote! {
None => { #render_path::render_resource::BindlessBufferDescriptor {
if attr_bindless_count.is_some() { // Note that, because this is bindless, *binding
return Err(Error::new_spanned( // index* here refers to the index in the
attr, // bindless index table (`bindless_index`), and
"Must specify `binding_array(...)` with `#[uniform]` if the \ // the actual binding number is the *binding
object is bindless", // 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,
} }
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; let required_len = binding_index as usize + 1;
if required_len > binding_states.len() { if required_len > binding_states.len() {
@ -986,11 +1016,13 @@ struct UniformBindingMeta {
binding_array: Option<LitInt>, binding_array: Option<LitInt>,
} }
/// The parsed structure-level `#[uniform]` attribute. /// The parsed structure-level `#[uniform]` or `#[data]` attribute.
/// ///
/// The corresponding syntax is `#[uniform(BINDING_INDEX, CONVERTED_SHADER_TYPE, /// The corresponding syntax is `#[uniform(BINDING_INDEX, CONVERTED_SHADER_TYPE,
/// binding_array(BINDING_ARRAY)]`. /// binding_array(BINDING_ARRAY)]`, optionally replacing `uniform` with `data`.
struct UniformBindingAttr { struct UniformBindingAttr {
/// Whether the declaration is `#[uniform]` or `#[data]`.
binding_type: UniformBindingAttrType,
/// The binding index. /// The binding index.
binding_index: u32, binding_index: u32,
/// The uniform data type. /// The uniform data type.
@ -999,6 +1031,17 @@ struct UniformBindingAttr {
binding_array: Option<u32>, binding_array: Option<u32>,
} }
/// Whether a structure-level shader type declaration is `#[uniform]` or
/// `#[data]`.
enum UniformBindingAttrType {
/// `#[uniform]`: i.e. in bindless mode, we need a separate buffer per data
/// instance.
Uniform,
/// `#[data]`: i.e. in bindless mode, we concatenate all instance data into
/// a single buffer.
Data,
}
/// Represents the arguments for any general binding attribute. /// Represents the arguments for any general binding attribute.
/// ///
/// If parsed, represents an attribute /// If parsed, represents an attribute
@ -1070,6 +1113,11 @@ impl Parse for UniformBindingMeta {
/// Parses a structure-level `#[uniform]` attribute (not a field-level /// Parses a structure-level `#[uniform]` attribute (not a field-level
/// `#[uniform]` attribute). /// `#[uniform]` attribute).
fn get_uniform_binding_attr(attr: &syn::Attribute) -> Result<UniformBindingAttr> { fn get_uniform_binding_attr(attr: &syn::Attribute) -> Result<UniformBindingAttr> {
let attr_ident = attr
.path()
.get_ident()
.expect("Shouldn't be here if we didn't have an attribute");
let uniform_binding_meta = attr.parse_args_with(UniformBindingMeta::parse)?; let uniform_binding_meta = attr.parse_args_with(UniformBindingMeta::parse)?;
let binding_index = uniform_binding_meta.lit_int.base10_parse()?; let binding_index = uniform_binding_meta.lit_int.base10_parse()?;
@ -1080,6 +1128,11 @@ fn get_uniform_binding_attr(attr: &syn::Attribute) -> Result<UniformBindingAttr>
}; };
Ok(UniformBindingAttr { Ok(UniformBindingAttr {
binding_type: if attr_ident == UNIFORM_ATTRIBUTE_NAME {
UniformBindingAttrType::Uniform
} else {
UniformBindingAttrType::Data
},
binding_index, binding_index,
converted_shader_type: ident, converted_shader_type: ident,
binding_array, binding_array,

View File

@ -60,7 +60,8 @@ pub fn derive_extract_component(input: TokenStream) -> TokenStream {
sampler, sampler,
bind_group_data, bind_group_data,
storage, storage,
bindless bindless,
data
) )
)] )]
pub fn derive_as_bind_group(input: TokenStream) -> TokenStream { pub fn derive_as_bind_group(input: TokenStream) -> TokenStream {

View File

@ -291,6 +291,55 @@ impl Deref for BindGroup {
/// binding_array<StandardMaterialUniform>` and accessible as /// binding_array<StandardMaterialUniform>` and accessible as
/// `material_array[material_indices[slot].material]`. /// `material_array[material_indices[slot].material]`.
/// ///
/// ## `data(BINDING_INDEX, ConvertedShaderType, binding_array(BINDING_INDEX))`
///
/// * This is very similar to `uniform(BINDING_INDEX, ConvertedShaderType,
/// binding_array(BINDING_INDEX)` and in fact is identical if bindless mode
/// isn't being used. The difference is that, in bindless mode, the `data`
/// attribute produces a single buffer containing an array, not an array of
/// buffers. For example, suppose you had the following declaration:
///
/// ```ignore
/// #[uniform(0, StandardMaterialUniform, binding_array(10))]
/// struct StandardMaterial { ... }
/// ```
///
/// In bindless mode, this will produce a binding matching the following WGSL
/// declaration:
///
/// ```wgsl
/// @group(2) @binding(10) var<storage> material_array: binding_array<StandardMaterial>;
/// ```
///
/// On the other hand, if you write this declaration:
///
/// ```ignore
/// #[data(0, StandardMaterialUniform, binding_array(10))]
/// struct StandardMaterial { ... }
/// ```
///
/// Then Bevy produces a binding that matches this WGSL declaration instead:
///
/// ```wgsl
/// @group(2) @binding(10) var<storage> material_array: array<StandardMaterial>;
/// ```
///
/// * Just as with the structure-level `uniform` attribute, Bevy converts the
/// entire [`AsBindGroup`] to `ConvertedShaderType`, using the
/// [`AsBindGroupShaderType<ConvertedShaderType>`] trait.
///
/// * In non-bindless mode, the structure-level `data` attribute is the same as
/// the structure-level `uniform` attribute and produces a single uniform buffer
/// in the shader. The above example would result in a binding that looks like
/// this in WGSL in non-bindless mode:
///
/// ```wgsl
/// @group(2) @binding(0) var<uniform> material: StandardMaterial;
/// ```
///
/// * For efficiency reasons, `data` is generally preferred over `uniform`
/// unless you need to place your data in individual buffers.
///
/// ## `bind_group_data(DataType)` /// ## `bind_group_data(DataType)`
/// ///
/// * The [`AsBindGroup`] type will be converted to some `DataType` using [`Into<DataType>`] and stored /// * The [`AsBindGroup`] type will be converted to some `DataType` using [`Into<DataType>`] and stored
@ -567,14 +616,29 @@ pub enum OwnedBindingResource {
Buffer(Buffer), Buffer(Buffer),
TextureView(TextureViewDimension, TextureView), TextureView(TextureViewDimension, TextureView),
Sampler(SamplerBindingType, Sampler), Sampler(SamplerBindingType, Sampler),
Data(OwnedData),
} }
/// Data that will be copied into a GPU buffer.
///
/// This corresponds to the `#[data]` attribute in `AsBindGroup`.
#[derive(Debug, Deref, DerefMut)]
pub struct OwnedData(pub Vec<u8>);
impl OwnedBindingResource { impl OwnedBindingResource {
/// Creates a [`BindingResource`] reference to this
/// [`OwnedBindingResource`].
///
/// Note that this operation panics if passed a
/// [`OwnedBindingResource::Data`], because [`OwnedData`] doesn't itself
/// correspond to any binding and instead requires the
/// `MaterialBindGroupAllocator` to pack it into a buffer.
pub fn get_binding(&self) -> BindingResource { pub fn get_binding(&self) -> BindingResource {
match self { match self {
OwnedBindingResource::Buffer(buffer) => buffer.as_entire_binding(), OwnedBindingResource::Buffer(buffer) => buffer.as_entire_binding(),
OwnedBindingResource::TextureView(_, view) => BindingResource::TextureView(view), OwnedBindingResource::TextureView(_, view) => BindingResource::TextureView(view),
OwnedBindingResource::Sampler(_, sampler) => BindingResource::Sampler(sampler), OwnedBindingResource::Sampler(_, sampler) => BindingResource::Sampler(sampler),
OwnedBindingResource::Data(_) => panic!("`OwnedData` has no binding resource"),
} }
} }
} }

View File

@ -138,6 +138,15 @@ pub enum BindlessResourceType {
/// Note that this differs from a binding array. Cubemap texture arrays must /// Note that this differs from a binding array. Cubemap texture arrays must
/// all have the same size and format. /// all have the same size and format.
TextureCubeArray, TextureCubeArray,
/// Multiple instances of plain old data concatenated into a single buffer.
///
/// This corresponds to the `#[data]` declaration in
/// [`crate::render_resource::AsBindGroup`].
///
/// Note that this resource doesn't itself map to a GPU-level binding
/// resource and instead depends on the `MaterialBindGroupAllocator` to
/// create a binding resource for it.
DataBuffer,
} }
/// Describes a bindless buffer. /// Describes a bindless buffer.