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::{
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,
BufferBinding, BufferDescriptor, BufferId, BufferInitDescriptor, BufferUsages,
CompareFunction, FilterMode, OwnedBindingResource, PreparedBindGroup, RawBufferVec,
Sampler, SamplerDescriptor, SamplerId, TextureView, TextureViewDimension, TextureViewId,
UnpreparedBindGroup, WgpuSampler, WgpuTextureView,
},
renderer::{RenderDevice, RenderQueue},
texture::FallbackImage,
};
use bevy_utils::default;
use bytemuck::Pod;
use tracing::{error, trace};
use crate::Material;
@ -97,8 +98,11 @@ where
samplers: HashMap<BindlessResourceType, MaterialBindlessBindingArray<Sampler>>,
/// The binding arrays containing textures.
textures: HashMap<BindlessResourceType, MaterialBindlessBindingArray<TextureView>>,
/// The binding arrays containing data buffers.
/// The binding arrays containing buffers.
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.
///
@ -122,10 +126,8 @@ struct MaterialBindlessIndexTable<M>
where
M: Material,
{
/// The contents of the buffer.
buffer: RawBufferVec<u32>,
/// Whether the contents of the buffer have been uploaded to the GPU.
buffer_dirty: BufferDirtyState,
/// The buffer containing the mappings.
buffer: RetainedRawBufferVec<u32>,
phantom: PhantomData<M>,
}
@ -204,7 +206,11 @@ where
layout: BindGroupLayout,
},
/// 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
@ -228,6 +234,11 @@ enum BindingResourceId {
TextureView(TextureViewDimension, TextureViewId),
/// A sampler.
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.
@ -352,6 +363,44 @@ where
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 {
fn from(value: u32) -> Self {
MaterialBindGroupSlot(value)
@ -368,6 +417,7 @@ impl<'a> From<&'a OwnedBindingResource> for BindingResourceId {
fn from(value: &'a OwnedBindingResource) -> Self {
match *value {
OwnedBindingResource::Buffer(ref buffer) => BindingResourceId::Buffer(buffer.id()),
OwnedBindingResource::Data(_) => BindingResourceId::DataBuffer,
OwnedBindingResource::TextureView(ref texture_view_dimension, ref texture_view) => {
BindingResourceId::TextureView(*texture_view_dimension, texture_view.id())
}
@ -545,14 +595,13 @@ where
/// Creates a new [`MaterialBindlessIndexTable`] for a single slab.
fn new(bindless_descriptor: &BindlessDescriptor) -> MaterialBindlessIndexTable<M> {
// 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() {
buffer.push(0);
}
MaterialBindlessIndexTable {
buffer,
buffer_dirty: BufferDirtyState::NeedsReserve,
phantom: PhantomData,
}
}
@ -592,29 +641,42 @@ where
}
// 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.
fn prepare_buffer(&mut self, render_device: &RenderDevice) {
match self.buffer_dirty {
/// Recreates the GPU backing buffer if needed.
fn prepare(&mut self, render_device: &RenderDevice) {
match self.dirty {
BufferDirtyState::Clean | BufferDirtyState::NeedsUpload => {}
BufferDirtyState::NeedsReserve => {
let capacity = self.buffer.len();
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
/// necessary.
fn write_buffer(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) {
match self.buffer_dirty {
/// Writes the current contents of the buffer to the GPU if necessary.
fn write(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) {
match self.dirty {
BufferDirtyState::Clean => {}
BufferDirtyState::NeedsReserve | BufferDirtyState::NeedsUpload => {
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) => {
let bindless_resource_type = BindlessResourceType::from(texture_view_dimension);
match self
@ -940,6 +1006,11 @@ where
.expect("Slot should exist")
.ref_count += 1;
}
OwnedBindingResource::Data(_) => {
panic!("Data buffers can't be deduplicated")
}
OwnedBindingResource::TextureView(texture_view_dimension, _) => {
let bindless_resource_type =
BindlessResourceType::from(texture_view_dimension);
@ -952,6 +1023,7 @@ where
.expect("Slot should exist")
.ref_count += 1;
}
OwnedBindingResource::Sampler(sampler_binding_type, _) => {
let bindless_resource_type =
BindlessResourceType::from(sampler_binding_type);
@ -980,6 +1052,14 @@ where
.insert(binding_resource_id, buffer);
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) => {
let bindless_resource_type = BindlessResourceType::from(texture_view_dimension);
let slot = self
@ -1019,14 +1099,23 @@ where
{
let bindless_index = BindlessIndex::from(bindless_index as u32);
// Free the binding.
let resource_freed = match *bindless_resource_type {
// Free the binding. If the resource in question was anything other
// 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::Buffer => self
.buffers
.get_mut(&bindless_index)
.expect("Buffer should exist with that bindless index")
.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::SamplerNonFiltering
| BindlessResourceType::SamplerComparison => self
@ -1048,7 +1137,7 @@ where
// If the slot is now free, decrement the allocated resource
// count.
if resource_freed {
if decrement_allocated_resource_count {
self.allocated_resource_count -= 1;
}
}
@ -1075,7 +1164,12 @@ where
bindless_descriptor: &BindlessDescriptor,
) {
// 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.
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(
M::label(),
bind_group_layout,
@ -1147,10 +1253,16 @@ where
/// 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) {
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
@ -1300,11 +1412,9 @@ where
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,
);
// This is OK, because index buffers are present in
// `BindlessDescriptor::buffers` but not in
// `BindlessDescriptor::resources`.
continue;
};
let buffer_bindings = buffer_bindless_binding_array
@ -1456,6 +1566,7 @@ where
let mut buffers = HashMap::default();
let mut samplers = HashMap::default();
let mut textures = HashMap::default();
let mut data_buffers = HashMap::default();
for (bindless_index, bindless_resource_type) in
bindless_descriptor.resources.iter().enumerate()
@ -1480,6 +1591,26 @@ where
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::SamplerNonFiltering
| BindlessResourceType::SamplerComparison => {
@ -1514,6 +1645,7 @@ where
samplers,
textures,
buffers,
data_buffers,
extra_data: vec![],
free_slots: vec![],
live_allocation_count: 0,
@ -1613,9 +1745,10 @@ where
&mut self,
prepared_bind_group: PreparedBindGroup<M::Data>,
) -> MaterialBindingId {
self.allocate(MaterialNonBindlessAllocatedBindGroup::Prepared(
prepared_bind_group,
))
self.allocate(MaterialNonBindlessAllocatedBindGroup::Prepared {
bind_group: prepared_bind_group,
uniform_buffers: vec![],
})
}
/// Deallocates the bind group with the given binding ID.
@ -1632,8 +1765,8 @@ where
self.bind_groups[group.0 as usize]
.as_ref()
.map(|bind_group| match bind_group {
MaterialNonBindlessAllocatedBindGroup::Prepared(prepared_bind_group) => {
MaterialNonBindlessSlab::Prepared(prepared_bind_group)
MaterialNonBindlessAllocatedBindGroup::Prepared { bind_group, .. } => {
MaterialNonBindlessSlab::Prepared(bind_group)
}
MaterialNonBindlessAllocatedBindGroup::Unprepared { bind_group, .. } => {
MaterialNonBindlessSlab::Unprepared(bind_group)
@ -1657,25 +1790,58 @@ where
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();
// Pack any `Data` into uniform buffers.
let mut uniform_buffers = vec![];
for (index, binding) in unprepared_bind_group.bindings.iter() {
let OwnedBindingResource::Data(ref owned_data) = *binding else {
continue;
};
let label = format!("material uniform data {}", *index);
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 =
render_device.create_bind_group(M::label(), &bind_group_layout, &entries);
// Create bind group 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(
MaterialNonBindlessAllocatedBindGroup::Prepared(PreparedBindGroup {
bindings: unprepared_bind_group.bindings,
bind_group,
data: unprepared_bind_group.data,
}),
// Create the bind group.
let bind_group = render_device.create_bind_group(
M::label(),
&bind_group_layout,
&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`].
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
#[bind_group_data(StandardMaterialKey)]
#[uniform(0, StandardMaterialUniform, binding_array(10))]
#[data(0, StandardMaterialUniform, binding_array(10))]
#[bindless]
#[reflect(Default, Debug)]
pub struct StandardMaterial {

View File

@ -46,7 +46,7 @@ struct 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

View File

@ -17,6 +17,7 @@ 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 DATA_ATTRIBUTE_NAME: Symbol = Symbol("data");
const BINDING_ARRAY_MODIFIER_NAME: Symbol = Symbol("binding_array");
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.
for attr in &ast.attrs {
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 {
binding_type,
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());
let converted: #converted_shader_type = self.as_bind_group_shader_type(&images);
buffer.write(&converted).unwrap();
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #uniform_buffer_usages,
contents: buffer.as_ref(),
},
))
)
}});
match binding_type {
UniformBindingAttrType::Uniform => {
binding_impls.push(quote! {{
use #render_path::render_resource::AsBindGroupShaderType;
let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new());
let converted: #converted_shader_type = self.as_bind_group_shader_type(&images);
buffer.write(&converted).unwrap();
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
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!{
#bind_group_layout_entries.push(
@ -157,66 +230,23 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
);
});
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",
));
}
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,
}
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() {
@ -986,11 +1016,13 @@ struct UniformBindingMeta {
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,
/// binding_array(BINDING_ARRAY)]`.
/// binding_array(BINDING_ARRAY)]`, optionally replacing `uniform` with `data`.
struct UniformBindingAttr {
/// Whether the declaration is `#[uniform]` or `#[data]`.
binding_type: UniformBindingAttrType,
/// The binding index.
binding_index: u32,
/// The uniform data type.
@ -999,6 +1031,17 @@ struct UniformBindingAttr {
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.
///
/// If parsed, represents an attribute
@ -1070,6 +1113,11 @@ impl Parse for UniformBindingMeta {
/// Parses a structure-level `#[uniform]` attribute (not a field-level
/// `#[uniform]` attribute).
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 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 {
binding_type: if attr_ident == UNIFORM_ATTRIBUTE_NAME {
UniformBindingAttrType::Uniform
} else {
UniformBindingAttrType::Data
},
binding_index,
converted_shader_type: ident,
binding_array,

View File

@ -60,7 +60,8 @@ pub fn derive_extract_component(input: TokenStream) -> TokenStream {
sampler,
bind_group_data,
storage,
bindless
bindless,
data
)
)]
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
/// `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)`
///
/// * The [`AsBindGroup`] type will be converted to some `DataType` using [`Into<DataType>`] and stored
@ -567,14 +616,29 @@ pub enum OwnedBindingResource {
Buffer(Buffer),
TextureView(TextureViewDimension, TextureView),
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 {
/// 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 {
match self {
OwnedBindingResource::Buffer(buffer) => buffer.as_entire_binding(),
OwnedBindingResource::TextureView(_, view) => BindingResource::TextureView(view),
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
/// all have the same size and format.
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.