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:
parent
a1717331e4
commit
5d7a60592d
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user