Add a bindless mode to AsBindGroup. (#16368)

This patch adds the infrastructure necessary for Bevy to support
*bindless resources*, by adding a new `#[bindless]` attribute to
`AsBindGroup`.

Classically, only a single texture (or sampler, or buffer) can be
attached to each shader binding. This means that switching materials
requires breaking a batch and issuing a new drawcall, even if the mesh
is otherwise identical. This adds significant overhead not only in the
driver but also in `wgpu`, as switching bind groups increases the amount
of validation work that `wgpu` must do.

*Bindless resources* are the typical solution to this problem. Instead
of switching bindings between each texture, the renderer instead
supplies a large *array* of all textures in the scene up front, and the
material contains an index into that array. This pattern is repeated for
buffers and samplers as well. The renderer now no longer needs to switch
binding descriptor sets while drawing the scene.

Unfortunately, as things currently stand, this approach won't quite work
for Bevy. Two aspects of `wgpu` conspire to make this ideal approach
unacceptably slow:

1. In the DX12 backend, all binding arrays (bindless resources) must
have a constant size declared in the shader, and all textures in an
array must be bound to actual textures. Changing the size requires a
recompile.

2. Changing even one texture incurs revalidation of all textures, a
process that takes time that's linear in the total size of the binding
array.

This means that declaring a large array of textures big enough to
encompass the entire scene is presently unacceptably slow. For example,
if you declare 4096 textures, then `wgpu` will have to revalidate all
4096 textures if even a single one changes. This process can take
multiple frames.

To work around this problem, this PR groups bindless resources into
small *slabs* and maintains a free list for each. The size of each slab
for the bindless arrays associated with a material is specified via the
`#[bindless(N)]` attribute. For instance, consider the following
declaration:

```rust
#[derive(AsBindGroup)]
#[bindless(16)]
struct MyMaterial {
    #[buffer(0)]
    color: Vec4,
    #[texture(1)]
    #[sampler(2)]
    diffuse: Handle<Image>,
}
```

The `#[bindless(N)]` attribute specifies that, if bindless arrays are
supported on the current platform, each resource becomes a binding array
of N instances of that resource. So, for `MyMaterial` above, the `color`
attribute is exposed to the shader as `binding_array<vec4<f32>, 16>`,
the `diffuse` texture is exposed to the shader as
`binding_array<texture_2d<f32>, 16>`, and the `diffuse` sampler is
exposed to the shader as `binding_array<sampler, 16>`. Inside the
material's vertex and fragment shaders, the applicable index is
available via the `material_bind_group_slot` field of the `Mesh`
structure. So, for instance, you can access the current color like so:

```wgsl
// `uniform` binding arrays are a non-sequitur, so `uniform` is automatically promoted
// to `storage` in bindless mode.
@group(2) @binding(0) var<storage> material_color: binding_array<Color, 4>;
...
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
    let color = material_color[mesh[in.instance_index].material_bind_group_slot];
    ...
}
```

Note that portable shader code can't guarantee that the current platform
supports bindless textures. Indeed, bindless mode is only available in
Vulkan and DX12. The `BINDLESS` shader definition is available for your
use to determine whether you're on a bindless platform or not. Thus a
portable version of the shader above would look like:

```wgsl
#ifdef BINDLESS
@group(2) @binding(0) var<storage> material_color: binding_array<Color, 4>;
#else // BINDLESS
@group(2) @binding(0) var<uniform> material_color: Color;
#endif // BINDLESS
...
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
#ifdef BINDLESS
    let color = material_color[mesh[in.instance_index].material_bind_group_slot];
#else // BINDLESS
    let color = material_color;
#endif // BINDLESS
    ...
}
```

Importantly, this PR *doesn't* update `StandardMaterial` to be bindless.
So, for example, `scene_viewer` will currently not run any faster. I
intend to update `StandardMaterial` to use bindless mode in a follow-up
patch.

A new example, `shaders/shader_material_bindless`, has been added to
demonstrate how to use this new feature.

Here's a Tracy profile of `submit_graph_commands` of this patch and an
additional patch (not submitted yet) that makes `StandardMaterial` use
bindless. Red is those patches; yellow is `main`. The scene was Bistro
Exterior with a hack that forces all textures to opaque. You can see a
1.47x mean speedup.
![Screenshot 2024-11-12
161713](https://github.com/user-attachments/assets/4334b362-42c8-4d64-9cfb-6835f019b95c)

## Migration Guide

* `RenderAssets::prepare_asset` now takes an `AssetId` parameter.
* Bin keys now have Bevy-specific material bind group indices instead of
`wgpu` material bind group IDs, as part of the bindless change. Use the
new `MaterialBindGroupAllocator` to map from bind group index to bind
group ID.
This commit is contained in:
Patrick Walton 2024-12-03 10:00:34 -08:00 committed by GitHub
parent bfb2ff16e8
commit 5adf831b42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1248 additions and 180 deletions

View File

@ -3824,6 +3824,17 @@ description = "Shows how to use animation clips to animate UI properties"
category = "Animation"
wasm = true
[[example]]
name = "shader_material_bindless"
path = "examples/shader/shader_material_bindless.rs"
doc-scrape-examples = true
[package.metadata.example.shader_material_bindless]
name = "Material - Bindless"
description = "Demonstrates how to make materials that use bindless textures"
category = "Shaders"
wasm = true
[profile.wasm-release]
inherits = "release"
opt-level = "z"

View File

@ -0,0 +1,38 @@
#import bevy_pbr::forward_io::VertexOutput
#import bevy_pbr::mesh_bindings::mesh
struct Color {
base_color: vec4<f32>,
}
#ifdef BINDLESS
@group(2) @binding(0) var<storage> material_color: binding_array<Color, 4>;
@group(2) @binding(1) var material_color_texture: binding_array<texture_2d<f32>, 4>;
@group(2) @binding(2) var material_color_sampler: binding_array<sampler, 4>;
#else // BINDLESS
@group(2) @binding(0) var<uniform> material_color: Color;
@group(2) @binding(1) var material_color_texture: texture_2d<f32>;
@group(2) @binding(2) var material_color_sampler: sampler;
#endif // BINDLESS
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
#ifdef BINDLESS
let slot = mesh[in.instance_index].material_bind_group_slot;
let base_color = material_color[slot].base_color;
#else // BINDLESS
let base_color = material_color.base_color;
#endif // BINDLESS
return base_color * textureSampleLevel(
#ifdef BINDLESS
material_color_texture[slot],
material_color_sampler[slot],
#else // BINDLESS
material_color_texture,
material_color_sampler,
#endif // BINDLESS
in.uv,
0.0
);
}

View File

@ -191,6 +191,7 @@ impl RenderAsset for GpuAutoExposureCompensationCurve {
fn prepare_asset(
source: Self::SourceAsset,
_: AssetId<Self::SourceAsset>,
(render_device, render_queue): &mut SystemParamItem<Self::Param>,
) -> Result<Self, bevy_render::render_asset::PrepareAssetError<Self::SourceAsset>> {
let texture = render_device.create_texture_with_data(

View File

@ -87,8 +87,8 @@ use bevy_render::{
ViewSortedRenderPhases,
},
render_resource::{
BindGroupId, CachedRenderPipelineId, Extent3d, FilterMode, Sampler, SamplerDescriptor,
Texture, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView,
CachedRenderPipelineId, Extent3d, FilterMode, Sampler, SamplerDescriptor, Texture,
TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView,
},
renderer::RenderDevice,
sync_world::RenderEntity,
@ -233,17 +233,17 @@ pub struct Opaque3dBinKey {
/// The function used to draw.
pub draw_function: DrawFunctionId,
/// The ID of a bind group specific to the material instance.
///
/// In the case of PBR, this is the `MaterialBindGroupIndex`.
pub material_bind_group_index: Option<u32>,
/// The asset that this phase item is associated with.
///
/// Normally, this is the ID of the mesh, but for non-mesh items it might be
/// the ID of another type of asset.
pub asset_id: UntypedAssetId,
/// The ID of a bind group specific to the material.
///
/// In the case of PBR, this is the `MaterialBindGroupId`.
pub material_bind_group_id: Option<BindGroupId>,
/// The lightmap, if present.
pub lightmap_image: Option<AssetId<Image>>,
}

View File

@ -41,8 +41,8 @@ use bevy_render::{
PhaseItemExtraIndex,
},
render_resource::{
BindGroupId, CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer,
Extent3d, ShaderType, TextureFormat, TextureView,
CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d,
ShaderType, TextureFormat, TextureView,
},
texture::ColorAttachment,
};
@ -158,13 +158,13 @@ pub struct OpaqueNoLightmap3dBinKey {
/// The function used to draw the mesh.
pub draw_function: DrawFunctionId,
/// The ID of the asset.
pub asset_id: UntypedAssetId,
/// The ID of a bind group specific to the material.
///
/// In the case of PBR, this is the `MaterialBindGroupId`.
pub material_bind_group_id: Option<BindGroupId>,
/// In the case of PBR, this is the `MaterialBindGroupIndex`.
pub material_bind_group_index: Option<u32>,
/// The ID of the asset.
pub asset_id: UntypedAssetId,
}
impl PhaseItem for Opaque3dPrepass {

View File

@ -74,7 +74,7 @@ pub mod prelude {
}
use bevy_app::{App, FixedFirst, FixedLast, Last, Plugin, RunFixedMainLoop};
use bevy_asset::{Asset, AssetApp, Assets, Handle};
use bevy_asset::{Asset, AssetApp, AssetId, Assets, Handle};
use bevy_color::LinearRgba;
use bevy_ecs::{
schedule::{IntoSystemConfigs, SystemSet},
@ -520,6 +520,7 @@ impl RenderAsset for GpuLineGizmo {
fn prepare_asset(
gizmo: Self::SourceAsset,
_: AssetId<Self::SourceAsset>,
render_device: &mut SystemParamItem<Self::Param>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
let position_buffer_data = cast_slice(&gizmo.positions);

View File

@ -17,6 +17,7 @@ pub struct MaterialExtensionPipeline {
pub material_layout: BindGroupLayout,
pub vertex_shader: Option<Handle<Shader>>,
pub fragment_shader: Option<Handle<Shader>>,
pub bindless: bool,
}
pub struct MaterialExtensionKey<E: MaterialExtension> {
@ -163,7 +164,7 @@ impl<B: Material, E: MaterialExtension> AsBindGroup for ExtendedMaterial<B, E> {
let extended_bindgroup =
E::unprepared_bind_group(&self.extension, layout, render_device, extended_param)?;
bindings.extend(extended_bindgroup.bindings);
bindings.extend(extended_bindgroup.bindings.0);
Ok(UnpreparedBindGroup {
bindings,
@ -279,6 +280,7 @@ impl<B: Material, E: MaterialExtension> Material for ExtendedMaterial<B, E> {
material_layout,
vertex_shader,
fragment_shader,
bindless,
..
} = pipeline.clone();
let base_pipeline = MaterialPipeline::<B> {
@ -286,6 +288,7 @@ impl<B: Material, E: MaterialExtension> Material for ExtendedMaterial<B, E> {
material_layout,
vertex_shader,
fragment_shader,
bindless,
marker: Default::default(),
};
let base_key = MaterialPipelineKey::<B> {
@ -300,6 +303,7 @@ impl<B: Material, E: MaterialExtension> Material for ExtendedMaterial<B, E> {
material_layout,
vertex_shader,
fragment_shader,
bindless,
..
} = pipeline.clone();
@ -309,6 +313,7 @@ impl<B: Material, E: MaterialExtension> Material for ExtendedMaterial<B, E> {
material_layout,
vertex_shader,
fragment_shader,
bindless,
},
descriptor,
layout,

View File

@ -34,6 +34,7 @@ mod light;
mod light_probe;
mod lightmap;
mod material;
mod material_bind_groups;
mod mesh_material;
mod parallax;
mod pbr_material;
@ -43,6 +44,8 @@ mod ssao;
mod ssr;
mod volumetric_fog;
use crate::material_bind_groups::FallbackBindlessResources;
use bevy_color::{Color, LinearRgba};
use core::marker::PhantomData;
@ -474,7 +477,8 @@ impl Plugin for PbrPlugin {
// Extract the required data from the main world
render_app
.init_resource::<ShadowSamplers>()
.init_resource::<GlobalClusterableObjectMeta>();
.init_resource::<GlobalClusterableObjectMeta>()
.init_resource::<FallbackBindlessResources>();
}
}

View File

@ -1,4 +1,5 @@
use self::{irradiance_volume::IrradianceVolume, prelude::EnvironmentMapLight};
use crate::material_bind_groups::{MaterialBindGroupAllocator, MaterialBindingId};
#[cfg(feature = "meshlet")]
use crate::meshlet::{
prepare_material_meshlet_meshes_main_opaque_pass, queue_material_meshlet_meshes,
@ -20,11 +21,13 @@ use bevy_core_pipeline::{
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
prelude::*,
system::{lifetimeless::SRes, SystemParamItem},
system::{
lifetimeless::{SRes, SResMut},
SystemParamItem,
},
};
use bevy_reflect::std_traits::ReflectDefault;
use bevy_reflect::Reflect;
use bevy_render::sync_world::MainEntityHashMap;
use bevy_render::view::RenderVisibleEntities;
use bevy_render::{
camera::TemporalJitter,
@ -37,13 +40,9 @@ use bevy_render::{
view::{ExtractedView, Msaa, RenderVisibilityRanges, ViewVisibility},
Extract,
};
use bevy_utils::tracing::error;
use core::{
hash::Hash,
marker::PhantomData,
num::NonZero,
sync::atomic::{AtomicU32, Ordering},
};
use bevy_render::{sync_world::MainEntityHashMap, texture::FallbackImage};
use bevy_utils::{hashbrown::hash_map::Entry, tracing::error};
use core::{hash::Hash, marker::PhantomData};
/// Materials are used alongside [`MaterialPlugin`], [`Mesh3d`], and [`MeshMaterial3d`]
/// to spawn entities that are rendered with a specific [`Material`] type. They serve as an easy to use high level
@ -283,12 +282,23 @@ where
.add_render_command::<Opaque3d, DrawMaterial<M>>()
.add_render_command::<AlphaMask3d, DrawMaterial<M>>()
.init_resource::<SpecializedMeshPipelines<MaterialPipeline<M>>>()
.add_systems(ExtractSchedule, extract_mesh_materials::<M>)
.add_systems(
ExtractSchedule,
extract_mesh_materials::<M>
.before(extract_meshes_for_cpu_building)
.before(extract_meshes_for_gpu_building),
)
.add_systems(
Render,
queue_material_meshes::<M>
.in_set(RenderSet::QueueMeshes)
.after(prepare_assets::<PreparedMaterial<M>>),
)
.add_systems(
Render,
prepare_material_bind_groups::<M>
.in_set(RenderSet::PrepareBindGroups)
.after(prepare_assets::<PreparedMaterial<M>>),
);
if self.shadows_enabled {
@ -331,7 +341,9 @@ where
fn finish(&self, app: &mut App) {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<MaterialPipeline<M>>();
render_app
.init_resource::<MaterialPipeline<M>>()
.init_resource::<MaterialBindGroupAllocator<M>>();
}
}
}
@ -382,6 +394,9 @@ pub struct MaterialPipeline<M: Material> {
pub material_layout: BindGroupLayout,
pub vertex_shader: Option<Handle<Shader>>,
pub fragment_shader: Option<Handle<Shader>>,
/// Whether this material *actually* uses bindless resources, taking the
/// platform support (or lack thereof) of bindless resources into account.
pub bindless: bool,
pub marker: PhantomData<M>,
}
@ -392,6 +407,7 @@ impl<M: Material> Clone for MaterialPipeline<M> {
material_layout: self.material_layout.clone(),
vertex_shader: self.vertex_shader.clone(),
fragment_shader: self.fragment_shader.clone(),
bindless: self.bindless,
marker: PhantomData,
}
}
@ -420,6 +436,15 @@ where
descriptor.layout.insert(2, self.material_layout.clone());
M::specialize(self, &mut descriptor, layout, key)?;
// If bindless mode is on, add a `BINDLESS` define.
if self.bindless {
descriptor.vertex.shader_defs.push("BINDLESS".into());
if let Some(ref mut fragment) = descriptor.fragment {
fragment.shader_defs.push("BINDLESS".into());
}
}
Ok(descriptor)
}
}
@ -442,6 +467,7 @@ impl<M: Material> FromWorld for MaterialPipeline<M> {
ShaderRef::Handle(handle) => Some(handle),
ShaderRef::Path(path) => Some(asset_server.load(path)),
},
bindless: material_bind_groups::material_uses_bindless_resources::<M>(render_device),
marker: PhantomData,
}
}
@ -461,6 +487,7 @@ impl<P: PhaseItem, M: Material, const I: usize> RenderCommand<P> for SetMaterial
type Param = (
SRes<RenderAssets<PreparedMaterial<M>>>,
SRes<RenderMaterialInstances<M>>,
SRes<MaterialBindGroupAllocator<M>>,
);
type ViewQuery = ();
type ItemQuery = ();
@ -470,11 +497,16 @@ impl<P: PhaseItem, M: Material, const I: usize> RenderCommand<P> for SetMaterial
item: &P,
_view: (),
_item_query: Option<()>,
(materials, material_instances): SystemParamItem<'w, '_, Self::Param>,
(materials, material_instances, material_bind_group_allocator): SystemParamItem<
'w,
'_,
Self::Param,
>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let materials = materials.into_inner();
let material_instances = material_instances.into_inner();
let material_bind_group_allocator = material_bind_group_allocator.into_inner();
let Some(material_asset_id) = material_instances.get(&item.main_entity()) else {
return RenderCommandResult::Skip;
@ -482,7 +514,14 @@ impl<P: PhaseItem, M: Material, const I: usize> RenderCommand<P> for SetMaterial
let Some(material) = materials.get(*material_asset_id) else {
return RenderCommandResult::Skip;
};
pass.set_bind_group(I, &material.bind_group, &[]);
let Some(material_bind_group) = material_bind_group_allocator.get(material.binding.group)
else {
return RenderCommandResult::Skip;
};
let Some(bind_group) = material_bind_group.get_bind_group() else {
return RenderCommandResult::Skip;
};
pass.set_bind_group(I, bind_group, &[]);
RenderCommandResult::Success
}
}
@ -549,6 +588,8 @@ pub const fn screen_space_specular_transmission_pipeline_key(
fn extract_mesh_materials<M: Material>(
mut material_instances: ResMut<RenderMaterialInstances<M>>,
mut material_ids: ResMut<RenderMeshMaterialIds>,
mut material_bind_group_allocator: ResMut<MaterialBindGroupAllocator<M>>,
query: Extract<Query<(Entity, &ViewVisibility, &MeshMaterial3d<M>)>>,
) {
material_instances.clear();
@ -556,6 +597,15 @@ fn extract_mesh_materials<M: Material>(
for (entity, view_visibility, material) in &query {
if view_visibility.get() {
material_instances.insert(entity.into(), material.id());
// Allocate a slot for this material in the bind group.
let material_id = material.id().untyped();
material_ids
.mesh_to_material
.insert(entity.into(), material_id);
if let Entry::Vacant(entry) = material_ids.material_to_binding.entry(material_id) {
entry.insert(material_bind_group_allocator.allocate());
}
}
}
}
@ -584,6 +634,7 @@ pub fn queue_material_meshes<M: Material>(
render_material_instances: Res<RenderMaterialInstances<M>>,
render_lightmaps: Res<RenderLightmaps>,
render_visibility_ranges: Res<RenderVisibilityRanges>,
material_bind_group_allocator: Res<MaterialBindGroupAllocator<M>>,
mut opaque_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3d>>,
mut alpha_mask_render_phases: ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
mut transmissive_render_phases: ResMut<ViewSortedRenderPhases<Transmissive3d>>,
@ -739,6 +790,11 @@ pub fn queue_material_meshes<M: Material>(
let Some(material) = render_materials.get(*material_asset_id) else {
continue;
};
let Some(material_bind_group) =
material_bind_group_allocator.get(material.binding.group)
else {
continue;
};
let mut mesh_pipeline_key_bits = material.properties.mesh_pipeline_key_bits;
mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key(
@ -782,7 +838,9 @@ pub fn queue_material_meshes<M: Material>(
&material_pipeline,
MaterialPipelineKey {
mesh_key,
bind_group_data: material.key.clone(),
bind_group_data: material_bind_group
.get_extra_data(material.binding.slot)
.clone(),
},
&mesh.layout,
);
@ -794,10 +852,6 @@ pub fn queue_material_meshes<M: Material>(
}
};
mesh_instance
.material_bind_group_id
.set(material.get_bind_group_id());
match mesh_key
.intersection(MeshPipelineKey::BLEND_RESERVED_BITS | MeshPipelineKey::MAY_DISCARD)
{
@ -818,7 +872,7 @@ pub fn queue_material_meshes<M: Material>(
draw_function: draw_opaque_pbr,
pipeline: pipeline_id,
asset_id: mesh_instance.mesh_asset_id.into(),
material_bind_group_id: material.get_bind_group_id().0,
material_bind_group_index: Some(material.binding.group.0),
lightmap_image,
};
opaque_phase.add(
@ -846,7 +900,7 @@ pub fn queue_material_meshes<M: Material>(
draw_function: draw_alpha_mask_pbr,
pipeline: pipeline_id,
asset_id: mesh_instance.mesh_asset_id.into(),
material_bind_group_id: material.get_bind_group_id().0,
material_bind_group_index: Some(material.binding.group.0),
};
alpha_mask_phase.add(
bin_key,
@ -945,11 +999,10 @@ pub struct MaterialProperties {
}
/// Data prepared for a [`Material`] instance.
pub struct PreparedMaterial<T: Material> {
pub bindings: Vec<(u32, OwnedBindingResource)>,
pub bind_group: BindGroup,
pub key: T::Data,
pub struct PreparedMaterial<M: Material> {
pub binding: MaterialBindingId,
pub properties: MaterialProperties,
pub phantom: PhantomData<M>,
}
impl<M: Material> RenderAsset for PreparedMaterial<M> {
@ -959,15 +1012,38 @@ impl<M: Material> RenderAsset for PreparedMaterial<M> {
SRes<RenderDevice>,
SRes<MaterialPipeline<M>>,
SRes<DefaultOpaqueRendererMethod>,
SRes<RenderMeshMaterialIds>,
SResMut<MaterialBindGroupAllocator<M>>,
M::Param,
);
fn prepare_asset(
material: Self::SourceAsset,
(render_device, pipeline, default_opaque_render_method, ref mut material_param): &mut SystemParamItem<Self::Param>,
material_id: AssetId<Self::SourceAsset>,
(
render_device,
pipeline,
default_opaque_render_method,
mesh_material_ids,
ref mut bind_group_allocator,
ref mut material_param,
): &mut SystemParamItem<Self::Param>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
match material.as_bind_group(&pipeline.material_layout, render_device, material_param) {
Ok(prepared) => {
// Fetch the material binding ID, so that we can write it in to the
// `PreparedMaterial`.
let Some(material_binding_id) = mesh_material_ids
.material_to_binding
.get(&material_id.untyped())
else {
return Err(PrepareAssetError::RetryNextUpdate(material));
};
match material.unprepared_bind_group(
&pipeline.material_layout,
render_device,
material_param,
) {
Ok(unprepared) => {
let method = match material.opaque_render_method() {
OpaqueRendererMethod::Forward => OpaqueRendererMethod::Forward,
OpaqueRendererMethod::Deferred => OpaqueRendererMethod::Deferred,
@ -979,10 +1055,10 @@ impl<M: Material> RenderAsset for PreparedMaterial<M> {
material.reads_view_transmission_texture(),
);
bind_group_allocator.init(*material_binding_id, unprepared);
Ok(PreparedMaterial {
bindings: prepared.bindings,
bind_group: prepared.bind_group,
key: prepared.data,
binding: *material_binding_id,
properties: MaterialProperties {
alpha_mode: material.alpha_mode(),
depth_bias: material.depth_bias(),
@ -991,6 +1067,7 @@ impl<M: Material> RenderAsset for PreparedMaterial<M> {
render_method: method,
mesh_pipeline_key_bits,
},
phantom: PhantomData,
})
}
Err(AsBindGroupError::RetryNextUpdate) => {
@ -999,6 +1076,24 @@ impl<M: Material> RenderAsset for PreparedMaterial<M> {
Err(other) => Err(PrepareAssetError::AsBindGroupError(other)),
}
}
fn unload_asset(
asset_id: AssetId<Self::SourceAsset>,
(_, _, _, mesh_material_ids, ref mut bind_group_allocator, _): &mut SystemParamItem<
Self::Param,
>,
) {
// Mark this material's slot in the binding array as free.
let Some(material_binding_id) = mesh_material_ids
.material_to_binding
.get(&asset_id.untyped())
else {
return;
};
bind_group_allocator.free(*material_binding_id);
}
}
#[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)]
@ -1016,38 +1111,15 @@ impl From<BindGroup> for MaterialBindGroupId {
}
}
/// An atomic version of [`MaterialBindGroupId`] that can be read from and written to
/// safely from multiple threads.
#[derive(Default)]
pub struct AtomicMaterialBindGroupId(AtomicU32);
impl AtomicMaterialBindGroupId {
/// Stores a value atomically. Uses [`Ordering::Relaxed`] so there is zero guarantee of ordering
/// relative to other operations.
///
/// See also: [`AtomicU32::store`].
pub fn set(&self, id: MaterialBindGroupId) {
let id = if let Some(id) = id.0 {
NonZero::<u32>::from(id).get()
} else {
0
};
self.0.store(id, Ordering::Relaxed);
}
/// Loads a value atomically. Uses [`Ordering::Relaxed`] so there is zero guarantee of ordering
/// relative to other operations.
///
/// See also: [`AtomicU32::load`].
pub fn get(&self) -> MaterialBindGroupId {
MaterialBindGroupId(
NonZero::<u32>::new(self.0.load(Ordering::Relaxed)).map(BindGroupId::from),
)
}
}
impl<T: Material> PreparedMaterial<T> {
pub fn get_bind_group_id(&self) -> MaterialBindGroupId {
MaterialBindGroupId(Some(self.bind_group.id()))
}
/// A system that creates and/or recreates any bind groups that contain
/// materials that were modified this frame.
pub fn prepare_material_bind_groups<M>(
mut allocator: ResMut<MaterialBindGroupAllocator<M>>,
render_device: Res<RenderDevice>,
fallback_image: Res<FallbackImage>,
fallback_resources: Res<FallbackBindlessResources>,
) where
M: Material,
{
allocator.prepare_bind_groups(&render_device, &fallback_image, &fallback_resources);
}

View File

@ -0,0 +1,569 @@
//! Material bind group management for bindless resources.
//!
//! In bindless mode, Bevy's renderer groups materials into small bind groups.
//! This allocator manages each bind group, assigning slots to materials as
//! appropriate.
use crate::Material;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
system::Resource,
world::{FromWorld, World},
};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
render_resource::{
BindGroup, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry, BindingResource,
BindingType, Buffer, BufferBinding, BufferInitDescriptor, BufferUsages,
OwnedBindingResource, Sampler, SamplerDescriptor, TextureViewDimension,
UnpreparedBindGroup, WgpuSampler, WgpuTextureView,
},
renderer::RenderDevice,
settings::WgpuFeatures,
texture::FallbackImage,
};
use bevy_utils::{default, tracing::error, HashMap};
use core::any;
use core::iter;
use core::marker::PhantomData;
use core::num::NonZero;
/// An object that creates and stores bind groups for a single material type.
///
/// This object collects bindless materials into groups as appropriate and
/// assigns slots as materials are created.
#[derive(Resource)]
pub struct MaterialBindGroupAllocator<M>
where
M: Material,
{
/// The data that the allocator keeps about each bind group.
pub bind_groups: Vec<MaterialBindGroup<M>>,
/// Stores IDs of material bind groups that have at least one slot
/// available.
free_bind_groups: Vec<u32>,
/// The layout for this bind group.
bind_group_layout: BindGroupLayout,
/// Dummy buffers that are assigned to unused slots.
fallback_buffers: MaterialFallbackBuffers,
/// Whether this material is actually using bindless resources.
///
/// This takes the availability of bindless resources on this platform into
/// account.
bindless_enabled: bool,
phantom: PhantomData<M>,
}
/// Information that the allocator keeps about each bind group.
pub struct MaterialBindGroup<M>
where
M: Material,
{
/// The actual bind group.
pub bind_group: Option<BindGroup>,
/// The bind group data for each slot.
///
/// This is `None` if the slot is unallocated and `Some` if the slot is
/// full.
unprepared_bind_groups: Vec<Option<UnpreparedBindGroup<M::Data>>>,
/// A bitfield that contains a 0 if the slot is free or a 1 if the slot is
/// full.
///
/// We keep this value so that we can quickly find the next free slot when
/// we go to allocate.
used_slot_bitmap: u32,
}
/// Where the GPU data for a material is located.
///
/// In bindless mode, materials are gathered into bind groups, and the slot is
/// necessary to locate the material data within that group. If not in bindless
/// mode, bind groups and materials are in 1:1 correspondence, and the slot
/// index is always 0.
#[derive(Clone, Copy, Debug, Default, Reflect)]
pub struct MaterialBindingId {
/// The index of the bind group (slab) where the GPU data is located.
pub group: MaterialBindGroupIndex,
/// The slot within that bind group.
pub slot: MaterialBindGroupSlot,
}
/// The index of each material bind group.
///
/// In bindless mode, each bind group contains multiple materials. In
/// non-bindless mode, each bind group contains only one material.
#[derive(Clone, Copy, Debug, Default, Reflect, PartialEq, Deref, DerefMut)]
#[reflect(Default)]
pub struct MaterialBindGroupIndex(pub u32);
impl From<u32> for MaterialBindGroupIndex {
fn from(value: u32) -> Self {
MaterialBindGroupIndex(value)
}
}
/// The index of the slot containing material data within each material bind
/// group.
///
/// In bindless mode, this slot is needed to locate the material data in each
/// bind group, since multiple materials are packed into a single slab. In
/// non-bindless mode, this slot is always 0.
#[derive(Clone, Copy, Debug, Default, Reflect, Deref, DerefMut)]
#[reflect(Default)]
pub struct MaterialBindGroupSlot(pub u32);
impl From<u32> for MaterialBindGroupSlot {
fn from(value: u32) -> Self {
MaterialBindGroupSlot(value)
}
}
/// A temporary data structure that contains references to bindless resources.
///
/// We need this because the `wgpu` bindless API takes a slice of references.
/// Thus we need to create intermediate vectors of bindless resources in order
/// to satisfy the lifetime requirements.
enum BindingResourceArray<'a> {
Buffers(Vec<BufferBinding<'a>>),
TextureViews(TextureViewDimension, Vec<&'a WgpuTextureView>),
Samplers(Vec<&'a WgpuSampler>),
}
/// Contains dummy resources that we use to pad out bindless arrays.
///
/// On DX12, every binding array slot must be filled, so we have to fill unused
/// slots.
#[derive(Resource)]
pub struct FallbackBindlessResources {
/// A dummy sampler that we fill unused slots in bindless sampler arrays
/// with.
fallback_sampler: Sampler,
}
struct MaterialFallbackBuffers(HashMap<u32, Buffer>);
/// The minimum byte size of each fallback buffer.
const MIN_BUFFER_SIZE: u64 = 16;
impl<M> MaterialBindGroupAllocator<M>
where
M: Material,
{
/// Creates or recreates any bind groups that were modified this frame.
pub(crate) fn prepare_bind_groups(
&mut self,
render_device: &RenderDevice,
fallback_image: &FallbackImage,
fallback_resources: &FallbackBindlessResources,
) {
for bind_group in &mut self.bind_groups {
bind_group.rebuild_bind_group_if_necessary(
render_device,
&self.bind_group_layout,
fallback_image,
fallback_resources,
&self.fallback_buffers,
self.bindless_enabled,
);
}
}
/// Returns the bind group with the given index, if it exists.
#[inline]
pub(crate) fn get(&self, index: MaterialBindGroupIndex) -> Option<&MaterialBindGroup<M>> {
self.bind_groups.get(index.0 as usize)
}
/// Allocates a new binding slot and returns its ID.
pub(crate) fn allocate(&mut self) -> MaterialBindingId {
let group_index = self.free_bind_groups.pop().unwrap_or_else(|| {
let group_index = self.bind_groups.len() as u32;
self.bind_groups
.push(MaterialBindGroup::new(self.bindless_enabled));
group_index
});
let bind_group = &mut self.bind_groups[group_index as usize];
let slot_index = bind_group.allocate();
if !bind_group.is_full() {
self.free_bind_groups.push(group_index);
}
MaterialBindingId {
group: group_index.into(),
slot: slot_index,
}
}
/// Assigns an unprepared bind group to the group and slot specified in the
/// [`MaterialBindingId`].
pub(crate) fn init(
&mut self,
material_binding_id: MaterialBindingId,
unprepared_bind_group: UnpreparedBindGroup<M::Data>,
) {
self.bind_groups[material_binding_id.group.0 as usize]
.init(material_binding_id.slot, unprepared_bind_group);
}
/// Marks the slot corresponding to the given [`MaterialBindingId`] as free.
pub(crate) fn free(&mut self, material_binding_id: MaterialBindingId) {
let bind_group = &mut self.bind_groups[material_binding_id.group.0 as usize];
let was_full = bind_group.is_full();
bind_group.free(material_binding_id.slot);
// If the group that this material belonged to was full, it now contains
// at least one free slot, so add the group to the `free_bind_groups`
// list.
if was_full {
debug_assert!(!self.free_bind_groups.contains(&material_binding_id.group.0));
self.free_bind_groups.push(*material_binding_id.group);
}
}
}
impl<M> MaterialBindGroup<M>
where
M: Material,
{
/// Returns a new bind group.
fn new(bindless_enabled: bool) -> MaterialBindGroup<M> {
let count = if !bindless_enabled {
1
} else {
M::BINDLESS_SLOT_COUNT.unwrap_or(1)
};
MaterialBindGroup {
bind_group: None,
unprepared_bind_groups: iter::repeat_with(|| None).take(count as usize).collect(),
used_slot_bitmap: 0,
}
}
/// Allocates a new slot and returns its index.
///
/// This bind group must not be full.
fn allocate(&mut self) -> MaterialBindGroupSlot {
debug_assert!(!self.is_full());
// Mark the slot as used.
let slot = self.used_slot_bitmap.trailing_ones();
self.used_slot_bitmap |= 1 << slot;
slot.into()
}
/// Assigns the given unprepared bind group to the given slot.
fn init(
&mut self,
slot: MaterialBindGroupSlot,
unprepared_bind_group: UnpreparedBindGroup<M::Data>,
) {
self.unprepared_bind_groups[slot.0 as usize] = Some(unprepared_bind_group);
// Invalidate the cached bind group so that we rebuild it again.
self.bind_group = None;
}
/// Marks the given slot as free.
fn free(&mut self, slot: MaterialBindGroupSlot) {
self.unprepared_bind_groups[slot.0 as usize] = None;
self.used_slot_bitmap &= !(1 << slot.0);
// Invalidate the cached bind group so that we rebuild it again.
self.bind_group = None;
}
/// Returns true if all the slots are full or false if at least one slot in
/// this bind group is free.
fn is_full(&self) -> bool {
self.used_slot_bitmap == (1 << (self.unprepared_bind_groups.len() as u32)) - 1
}
/// Returns the actual bind group, or `None` if it hasn't been created yet.
pub fn get_bind_group(&self) -> Option<&BindGroup> {
self.bind_group.as_ref()
}
/// Recreates the bind group for this material bind group containing the
/// data for every material in it.
fn rebuild_bind_group_if_necessary(
&mut self,
render_device: &RenderDevice,
bind_group_layout: &BindGroupLayout,
fallback_image: &FallbackImage,
fallback_bindless_resources: &FallbackBindlessResources,
fallback_buffers: &MaterialFallbackBuffers,
bindless_enabled: bool,
) {
if self.bind_group.is_some() {
return;
}
let Some(first_bind_group) = self
.unprepared_bind_groups
.iter()
.find_map(|slot| slot.as_ref())
else {
return;
};
// If bindless isn't enabled, create a trivial bind group containing
// only one material's worth of data.
if !bindless_enabled {
let entries = first_bind_group
.bindings
.iter()
.map(|(index, binding)| BindGroupEntry {
binding: *index,
resource: binding.get_binding(),
})
.collect::<Vec<_>>();
self.bind_group =
Some(render_device.create_bind_group(M::label(), bind_group_layout, &entries));
return;
}
// Creates the intermediate binding resource vectors.
let Some(binding_resource_arrays) = self.recreate_binding_resource_arrays(
first_bind_group,
fallback_image,
fallback_bindless_resources,
fallback_buffers,
) else {
return;
};
// Now build the actual resource arrays for `wgpu`.
let entries = binding_resource_arrays
.iter()
.map(|&(&binding, ref binding_resource_array)| BindGroupEntry {
binding,
resource: match *binding_resource_array {
BindingResourceArray::Buffers(ref vec) => {
BindingResource::BufferArray(&vec[..])
}
BindingResourceArray::TextureViews(_, ref vec) => {
BindingResource::TextureViewArray(&vec[..])
}
BindingResourceArray::Samplers(ref vec) => {
BindingResource::SamplerArray(&vec[..])
}
},
})
.collect::<Vec<_>>();
self.bind_group =
Some(render_device.create_bind_group(M::label(), bind_group_layout, &entries));
}
/// Recreates the binding arrays for each material in this bind group.
fn recreate_binding_resource_arrays<'a>(
&'a self,
first_bind_group: &'a UnpreparedBindGroup<M::Data>,
fallback_image: &'a FallbackImage,
fallback_bindless_resources: &'a FallbackBindlessResources,
fallback_buffers: &'a MaterialFallbackBuffers,
) -> Option<Vec<(&'a u32, BindingResourceArray<'a>)>> {
// Initialize the arrays.
let mut binding_resource_arrays = first_bind_group
.bindings
.iter()
.map(|(index, binding)| match *binding {
OwnedBindingResource::Buffer(..) => (index, BindingResourceArray::Buffers(vec![])),
OwnedBindingResource::TextureView(dimension, _) => {
(index, BindingResourceArray::TextureViews(dimension, vec![]))
}
OwnedBindingResource::Sampler(..) => {
(index, BindingResourceArray::Samplers(vec![]))
}
})
.collect::<Vec<_>>();
for maybe_unprepared_bind_group in self.unprepared_bind_groups.iter() {
match *maybe_unprepared_bind_group {
None => {
// Push dummy resources for this slot.
for binding_resource_array in &mut binding_resource_arrays {
match *binding_resource_array {
(binding, BindingResourceArray::Buffers(ref mut vec)) => {
vec.push(BufferBinding {
buffer: &fallback_buffers.0[binding],
offset: 0,
size: None,
});
}
(
_,
BindingResourceArray::TextureViews(texture_dimension, ref mut vec),
) => vec.push(&fallback_image.get(texture_dimension).texture_view),
(_, BindingResourceArray::Samplers(ref mut vec)) => {
vec.push(&fallback_bindless_resources.fallback_sampler);
}
}
}
}
Some(ref unprepared_bind_group) => {
// Push the resources for this slot.
//
// All materials in this group must have the same type of
// binding (buffer, texture view, sampler) in each bind
// group entry.
for (
binding_index,
(&mut (binding, ref mut binding_resource_array), (_, binding_resource)),
) in binding_resource_arrays
.iter_mut()
.zip(unprepared_bind_group.bindings.0.iter())
.enumerate()
{
match (binding_resource_array, binding_resource) {
(
&mut BindingResourceArray::Buffers(ref mut vec),
OwnedBindingResource::Buffer(buffer),
) => match NonZero::new(buffer.size()) {
None => vec.push(BufferBinding {
buffer: &fallback_buffers.0[binding],
offset: 0,
size: None,
}),
Some(size) => vec.push(BufferBinding {
buffer,
offset: 0,
size: Some(size),
}),
},
(
&mut BindingResourceArray::TextureViews(_, ref mut vec),
OwnedBindingResource::TextureView(_, texture_view),
) => vec.push(texture_view),
(
&mut BindingResourceArray::Samplers(ref mut vec),
OwnedBindingResource::Sampler(sampler),
) => vec.push(sampler),
_ => {
error!(
"Mismatched bind group layouts for material \
{} at bind group {}; can't combine bind \
groups into a single bindless bind group!",
any::type_name::<M>(),
binding_index,
);
return None;
}
}
}
}
}
}
Some(binding_resource_arrays)
}
/// Returns the associated extra data for the material with the given slot.
pub fn get_extra_data(&self, slot: MaterialBindGroupSlot) -> &M::Data {
&self.unprepared_bind_groups[slot.0 as usize]
.as_ref()
.unwrap()
.data
}
}
impl<M> FromWorld for MaterialBindGroupAllocator<M>
where
M: Material,
{
fn from_world(world: &mut World) -> Self {
// Create a new bind group allocator.
let render_device = world.resource::<RenderDevice>();
let bind_group_layout_entries = M::bind_group_layout_entries(render_device);
let bind_group_layout =
render_device.create_bind_group_layout(M::label(), &bind_group_layout_entries);
let fallback_buffers =
MaterialFallbackBuffers::new(render_device, &bind_group_layout_entries);
MaterialBindGroupAllocator {
bind_groups: vec![],
free_bind_groups: vec![],
bind_group_layout,
fallback_buffers,
bindless_enabled: material_uses_bindless_resources::<M>(render_device),
phantom: PhantomData,
}
}
}
/// Returns true if the material will *actually* use bindless resources or false
/// if it won't.
///
/// This takes the platform support (or lack thereof) for bindless resources
/// into account.
pub fn material_uses_bindless_resources<M>(render_device: &RenderDevice) -> bool
where
M: Material,
{
M::BINDLESS_SLOT_COUNT.is_some()
&& render_device
.features()
.contains(WgpuFeatures::BUFFER_BINDING_ARRAY | WgpuFeatures::TEXTURE_BINDING_ARRAY)
}
impl FromWorld for FallbackBindlessResources {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
FallbackBindlessResources {
fallback_sampler: render_device.create_sampler(&SamplerDescriptor {
label: Some("fallback sampler"),
..default()
}),
}
}
}
impl MaterialFallbackBuffers {
/// Creates a new set of fallback buffers containing dummy allocations.
///
/// We populate unused bind group slots with these.
fn new(
render_device: &RenderDevice,
bind_group_layout_entries: &[BindGroupLayoutEntry],
) -> MaterialFallbackBuffers {
let mut fallback_buffers = HashMap::new();
for bind_group_layout_entry in bind_group_layout_entries {
// Create a dummy buffer of the appropriate size.
let BindingType::Buffer {
min_binding_size, ..
} = bind_group_layout_entry.ty
else {
continue;
};
let mut size: u64 = match min_binding_size {
None => 0,
Some(min_binding_size) => min_binding_size.into(),
};
size = size.max(MIN_BUFFER_SIZE);
fallback_buffers.insert(
bind_group_layout_entry.binding,
render_device.create_buffer_with_data(&BufferInitDescriptor {
label: Some("fallback buffer"),
contents: &vec![0; size as usize],
usage: BufferUsages::UNIFORM | BufferUsages::STORAGE,
}),
);
}
MaterialFallbackBuffers(fallback_buffers)
}
}

View File

@ -1,7 +1,7 @@
use super::{meshlet_mesh_manager::MeshletMeshManager, MeshletMesh, MeshletMesh3d};
use crate::{
Material, MeshFlags, MeshTransforms, MeshUniform, NotShadowCaster, NotShadowReceiver,
PreviousGlobalTransform, RenderMaterialInstances,
PreviousGlobalTransform, RenderMaterialInstances, RenderMeshMaterialIds,
};
use bevy_asset::{AssetEvent, AssetServer, Assets, UntypedAssetId};
use bevy_ecs::{
@ -89,6 +89,7 @@ impl InstanceManager {
transform: &GlobalTransform,
previous_transform: Option<&PreviousGlobalTransform>,
render_layers: Option<&RenderLayers>,
mesh_material_ids: &RenderMeshMaterialIds,
not_shadow_receiver: bool,
not_shadow_caster: bool,
) {
@ -108,7 +109,18 @@ impl InstanceManager {
previous_world_from_local: (&previous_transform).into(),
flags: flags.bits(),
};
let mesh_uniform = MeshUniform::new(&transforms, 0, None);
let Some(mesh_material_asset_id) = mesh_material_ids.mesh_to_material.get(&instance) else {
return;
};
let Some(mesh_material_binding_id) = mesh_material_ids
.material_to_binding
.get(mesh_material_asset_id)
else {
return;
};
let mesh_uniform = MeshUniform::new(&transforms, 0, mesh_material_binding_id.slot, None);
// Append instance data
self.instances.push((
@ -170,6 +182,7 @@ pub fn extract_meshlet_mesh_entities(
mut instance_manager: ResMut<InstanceManager>,
// TODO: Replace main_world and system_state when Extract<ResMut<Assets<MeshletMesh>>> is possible
mut main_world: ResMut<MainWorld>,
mesh_material_ids: Res<RenderMeshMaterialIds>,
mut system_state: Local<
Option<
SystemState<(
@ -238,6 +251,7 @@ pub fn extract_meshlet_mesh_entities(
transform,
previous_transform,
render_layers,
&mesh_material_ids,
not_shadow_receiver,
not_shadow_caster,
);

View File

@ -2,7 +2,10 @@ use super::{
instance_manager::InstanceManager, resource_manager::ResourceManager,
MESHLET_MESH_MATERIAL_SHADER_HANDLE,
};
use crate::{environment_map::EnvironmentMapLight, irradiance_volume::IrradianceVolume, *};
use crate::{
environment_map::EnvironmentMapLight, irradiance_volume::IrradianceVolume,
material_bind_groups::MaterialBindGroupAllocator, *,
};
use bevy_asset::AssetServer;
use bevy_core_pipeline::{
core_3d::Camera3d,
@ -36,6 +39,7 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass<M: Material>(
mesh_pipeline: Res<MeshPipeline>,
render_materials: Res<RenderAssets<PreparedMaterial<M>>>,
render_material_instances: Res<RenderMaterialInstances<M>>,
material_bind_group_allocator: Res<MaterialBindGroupAllocator<M>>,
asset_server: Res<AssetServer>,
mut mesh_vertex_buffer_layouts: ResMut<MeshVertexBufferLayouts>,
mut views: Query<
@ -145,6 +149,11 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass<M: Material>(
let Some(material) = render_materials.get(*material_id) else {
continue;
};
let Some(material_bind_group) =
material_bind_group_allocator.get(material.binding.group)
else {
continue;
};
if material.properties.render_method != OpaqueRendererMethod::Forward
|| material.properties.alpha_mode != AlphaMode::Opaque
@ -156,7 +165,9 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass<M: Material>(
let Ok(material_pipeline_descriptor) = material_pipeline.specialize(
MaterialPipelineKey {
mesh_key: view_key,
bind_group_data: material.key.clone(),
bind_group_data: material_bind_group
.get_extra_data(material.binding.slot)
.clone(),
},
fake_vertex_buffer_layout,
) else {
@ -208,7 +219,17 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass<M: Material>(
let pipeline_id = *cache.entry(view_key).or_insert_with(|| {
pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone())
});
materials.push((material_id, pipeline_id, material.bind_group.clone()));
let Some(material_bind_group) =
material_bind_group_allocator.get(material.binding.group)
else {
continue;
};
let Some(bind_group) = material_bind_group.get_bind_group() else {
continue;
};
materials.push((material_id, pipeline_id, (*bind_group).clone()));
}
}
}
@ -235,6 +256,7 @@ pub fn prepare_material_meshlet_meshes_prepass<M: Material>(
render_materials: Res<RenderAssets<PreparedMaterial<M>>>,
render_material_instances: Res<RenderMaterialInstances<M>>,
mut mesh_vertex_buffer_layouts: ResMut<MeshVertexBufferLayouts>,
material_bind_group_allocator: Res<MaterialBindGroupAllocator<M>>,
asset_server: Res<AssetServer>,
mut views: Query<
(
@ -273,6 +295,11 @@ pub fn prepare_material_meshlet_meshes_prepass<M: Material>(
let Some(material) = render_materials.get(*material_id) else {
continue;
};
let Some(material_bind_group) =
material_bind_group_allocator.get(material.binding.group)
else {
continue;
};
if material.properties.alpha_mode != AlphaMode::Opaque
|| material.properties.reads_view_transmission_texture
@ -293,7 +320,9 @@ pub fn prepare_material_meshlet_meshes_prepass<M: Material>(
let Ok(material_pipeline_descriptor) = prepass_pipeline.specialize(
MaterialPipelineKey {
mesh_key: view_key,
bind_group_data: material.key.clone(),
bind_group_data: material_bind_group
.get_extra_data(material.binding.slot)
.clone(),
},
fake_vertex_buffer_layout,
) else {
@ -363,7 +392,16 @@ pub fn prepare_material_meshlet_meshes_prepass<M: Material>(
pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone())
});
let item = (material_id, pipeline_id, material.bind_group.clone());
let Some(material_bind_group) =
material_bind_group_allocator.get(material.binding.group)
else {
continue;
};
let Some(bind_group) = material_bind_group.get_bind_group() else {
continue;
};
let item = (material_id, pipeline_id, (*bind_group).clone());
if view_key.contains(MeshPipelineKey::DEFERRED_PREPASS) {
deferred_materials.push(item);
} else {

View File

@ -1,5 +1,6 @@
mod prepass_bindings;
use crate::material_bind_groups::MaterialBindGroupAllocator;
use bevy_render::{
mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh},
render_resource::binding_types::uniform_buffer,
@ -712,6 +713,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
render_materials: Res<RenderAssets<PreparedMaterial<M>>>,
render_material_instances: Res<RenderMaterialInstances<M>>,
render_lightmaps: Res<RenderLightmaps>,
material_bind_group_allocator: Res<MaterialBindGroupAllocator<M>>,
mut opaque_prepass_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3dPrepass>>,
mut alpha_mask_prepass_render_phases: ResMut<ViewBinnedRenderPhases<AlphaMask3dPrepass>>,
mut opaque_deferred_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3dDeferred>>,
@ -800,6 +802,11 @@ pub fn queue_prepass_material_meshes<M: Material>(
let Some(material) = render_materials.get(*material_asset_id) else {
continue;
};
let Some(material_bind_group) =
material_bind_group_allocator.get(material.binding.group)
else {
continue;
};
let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
continue;
};
@ -868,7 +875,9 @@ pub fn queue_prepass_material_meshes<M: Material>(
&prepass_pipeline,
MaterialPipelineKey {
mesh_key,
bind_group_data: material.key.clone(),
bind_group_data: material_bind_group
.get_extra_data(material.binding.slot)
.clone(),
},
&mesh.layout,
);
@ -880,9 +889,6 @@ pub fn queue_prepass_material_meshes<M: Material>(
}
};
mesh_instance
.material_bind_group_id
.set(material.get_bind_group_id());
match mesh_key
.intersection(MeshPipelineKey::BLEND_RESERVED_BITS | MeshPipelineKey::MAY_DISCARD)
{
@ -893,7 +899,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
draw_function: opaque_draw_deferred,
pipeline: pipeline_id,
asset_id: mesh_instance.mesh_asset_id.into(),
material_bind_group_id: material.get_bind_group_id().0,
material_bind_group_index: Some(material.binding.group.0),
},
(*render_entity, *visible_entity),
BinnedRenderPhaseType::mesh(mesh_instance.should_batch()),
@ -904,7 +910,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
draw_function: opaque_draw_prepass,
pipeline: pipeline_id,
asset_id: mesh_instance.mesh_asset_id.into(),
material_bind_group_id: material.get_bind_group_id().0,
material_bind_group_index: Some(material.binding.group.0),
},
(*render_entity, *visible_entity),
BinnedRenderPhaseType::mesh(mesh_instance.should_batch()),
@ -918,7 +924,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
pipeline: pipeline_id,
draw_function: alpha_mask_draw_deferred,
asset_id: mesh_instance.mesh_asset_id.into(),
material_bind_group_id: material.get_bind_group_id().0,
material_bind_group_index: Some(material.binding.group.0),
};
alpha_mask_deferred_phase.as_mut().unwrap().add(
bin_key,
@ -930,7 +936,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
pipeline: pipeline_id,
draw_function: alpha_mask_draw_prepass,
asset_id: mesh_instance.mesh_asset_id.into(),
material_bind_group_id: material.get_bind_group_id().0,
material_bind_group_index: Some(material.binding.group.0),
};
alpha_mask_phase.add(
bin_key,

View File

@ -1,3 +1,4 @@
use crate::material_bind_groups::MaterialBindGroupAllocator;
use crate::*;
use bevy_asset::UntypedAssetId;
use bevy_color::ColorToComponents;
@ -1504,6 +1505,7 @@ pub fn queue_shadows<M: Material>(
render_mesh_instances: Res<RenderMeshInstances>,
render_materials: Res<RenderAssets<PreparedMaterial<M>>>,
render_material_instances: Res<RenderMaterialInstances<M>>,
material_bind_group_allocator: Res<MaterialBindGroupAllocator<M>>,
mut shadow_render_phases: ResMut<ViewBinnedRenderPhases<Shadow>>,
mut pipelines: ResMut<SpecializedMeshPipelines<PrepassPipeline<M>>>,
pipeline_cache: Res<PipelineCache>,
@ -1576,6 +1578,11 @@ pub fn queue_shadows<M: Material>(
let Some(material) = render_materials.get(*material_asset_id) else {
continue;
};
let Some(material_bind_group) =
material_bind_group_allocator.get(material.binding.group)
else {
continue;
};
let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
continue;
};
@ -1605,7 +1612,9 @@ pub fn queue_shadows<M: Material>(
&prepass_pipeline,
MaterialPipelineKey {
mesh_key,
bind_group_data: material.key.clone(),
bind_group_data: material_bind_group
.get_extra_data(material.binding.slot)
.clone(),
},
&mesh.layout,
);
@ -1618,10 +1627,6 @@ pub fn queue_shadows<M: Material>(
}
};
mesh_instance
.material_bind_group_id
.set(material.get_bind_group_id());
shadow_phase.add(
ShadowBinKey {
draw_function: draw_shadow_mesh,

View File

@ -1,7 +1,8 @@
use core::mem::{self, size_of};
use crate::material_bind_groups::{MaterialBindGroupIndex, MaterialBindGroupSlot};
use allocator::MeshAllocator;
use bevy_asset::{load_internal_asset, AssetId};
use bevy_asset::{load_internal_asset, AssetId, UntypedAssetId};
use bevy_core_pipeline::{
core_3d::{AlphaMask3d, Opaque3d, Transmissive3d, Transparent3d, CORE_3D_DEPTH_FORMAT},
deferred::{AlphaMask3dDeferred, Opaque3dDeferred},
@ -26,7 +27,7 @@ use bevy_render::{
camera::Camera,
mesh::*,
primitives::Aabb,
render_asset::RenderAssets,
render_asset::{ExtractAssetsSet, RenderAssets},
render_phase::{
BinnedRenderPhasePlugin, PhaseItem, RenderCommand, RenderCommandResult,
SortedRenderPhasePlugin, TrackedRenderPass,
@ -45,6 +46,7 @@ use bevy_utils::{
tracing::{error, warn},
Entry, HashMap, Parallel,
};
use material_bind_groups::MaterialBindingId;
use crate::{
render::{
@ -153,6 +155,7 @@ impl Plugin for MeshRenderPlugin {
.init_resource::<MorphUniforms>()
.init_resource::<MorphIndices>()
.init_resource::<MeshCullingDataBuffer>()
.init_resource::<RenderMeshMaterialIds>()
.add_systems(
ExtractSchedule,
(
@ -200,7 +203,9 @@ impl Plugin for MeshRenderPlugin {
.init_resource::<RenderMeshInstanceGpuQueues>()
.add_systems(
ExtractSchedule,
extract_meshes_for_gpu_building.in_set(ExtractMeshesSet),
extract_meshes_for_gpu_building
.in_set(ExtractMeshesSet)
.after(ExtractAssetsSet),
)
.add_systems(
Render,
@ -227,7 +232,9 @@ impl Plugin for MeshRenderPlugin {
.insert_resource(cpu_batched_instance_buffer)
.add_systems(
ExtractSchedule,
extract_meshes_for_cpu_building.in_set(ExtractMeshesSet),
extract_meshes_for_cpu_building
.in_set(ExtractMeshesSet)
.after(ExtractAssetsSet),
)
.add_systems(
Render,
@ -298,12 +305,12 @@ pub struct MeshUniform {
/// [`MeshAllocator`]). This value stores the offset of the first vertex in
/// this mesh in that buffer.
pub first_vertex_index: u32,
/// Index of the material inside the bind group data.
pub material_bind_group_slot: u32,
/// Padding.
pub pad_a: u32,
/// Padding.
pub pad_b: u32,
/// Padding.
pub pad_c: u32,
}
/// Information that has to be transferred from CPU to GPU in order to produce
@ -340,12 +347,12 @@ pub struct MeshInputUniform {
/// [`MeshAllocator`]). This value stores the offset of the first vertex in
/// this mesh in that buffer.
pub first_vertex_index: u32,
/// Index of the material inside the bind group data.
pub material_bind_group_slot: u32,
/// Padding.
pub pad_a: u32,
/// Padding.
pub pad_b: u32,
/// Padding.
pub pad_c: u32,
}
/// Information about each mesh instance needed to cull it on GPU.
@ -375,6 +382,7 @@ impl MeshUniform {
pub fn new(
mesh_transforms: &MeshTransforms,
first_vertex_index: u32,
material_bind_group_slot: MaterialBindGroupSlot,
maybe_lightmap_uv_rect: Option<Rect>,
) -> Self {
let (local_from_world_transpose_a, local_from_world_transpose_b) =
@ -387,9 +395,9 @@ impl MeshUniform {
local_from_world_transpose_b,
flags: mesh_transforms.flags,
first_vertex_index,
material_bind_group_slot: *material_bind_group_slot,
pad_a: 0,
pad_b: 0,
pad_c: 0,
}
}
}
@ -505,10 +513,8 @@ pub struct RenderMeshInstanceGpu {
pub struct RenderMeshInstanceShared {
/// The [`AssetId`] of the mesh.
pub mesh_asset_id: AssetId<Mesh>,
/// A slot for the material bind group ID.
///
/// This is filled in during [`crate::material::queue_material_meshes`].
pub material_bind_group_id: AtomicMaterialBindGroupId,
/// A slot for the material bind group index.
pub material_bindings_index: MaterialBindingId,
/// Various flags.
pub flags: RenderMeshInstanceFlags,
}
@ -576,6 +582,7 @@ impl RenderMeshInstanceShared {
fn from_components(
previous_transform: Option<&PreviousGlobalTransform>,
mesh: &Mesh3d,
material_bindings_index: MaterialBindingId,
not_shadow_caster: bool,
no_automatic_batching: bool,
) -> Self {
@ -593,7 +600,7 @@ impl RenderMeshInstanceShared {
RenderMeshInstanceShared {
mesh_asset_id: mesh.id(),
flags: mesh_instance_flags,
material_bind_group_id: AtomicMaterialBindGroupId::default(),
material_bindings_index,
}
}
@ -603,7 +610,6 @@ impl RenderMeshInstanceShared {
pub fn should_batch(&self) -> bool {
self.flags
.contains(RenderMeshInstanceFlags::AUTOMATIC_BATCHING)
&& self.material_bind_group_id.get().is_some()
}
}
@ -630,6 +636,17 @@ pub struct RenderMeshInstancesCpu(MainEntityHashMap<RenderMeshInstanceCpu>);
#[derive(Default, Deref, DerefMut)]
pub struct RenderMeshInstancesGpu(MainEntityHashMap<RenderMeshInstanceGpu>);
/// Maps each mesh instance to the material ID, and allocated binding ID,
/// associated with that mesh instance.
#[derive(Resource, Default)]
pub struct RenderMeshMaterialIds {
/// Maps the mesh instance to the material ID.
pub(crate) mesh_to_material: MainEntityHashMap<UntypedAssetId>,
/// Maps the material ID to the binding ID, which describes the location of
/// that material bind group data in memory.
pub(crate) material_to_binding: HashMap<UntypedAssetId, MaterialBindingId>,
}
impl RenderMeshInstances {
/// Creates a new [`RenderMeshInstances`] instance.
fn new(use_gpu_instance_buffer_builder: bool) -> RenderMeshInstances {
@ -793,9 +810,9 @@ impl RenderMeshInstanceGpuBuilder {
None => u32::MAX,
},
first_vertex_index,
material_bind_group_slot: *self.shared.material_bindings_index.slot,
pad_a: 0,
pad_b: 0,
pad_c: 0,
});
// Record the [`RenderMeshInstance`].
@ -870,6 +887,7 @@ pub struct ExtractMeshesSet;
pub fn extract_meshes_for_cpu_building(
mut render_mesh_instances: ResMut<RenderMeshInstances>,
render_visibility_ranges: Res<RenderVisibilityRanges>,
mesh_material_ids: Res<RenderMeshMaterialIds>,
mut render_mesh_instance_queues: Local<Parallel<Vec<(Entity, RenderMeshInstanceCpu)>>>,
meshes_query: Extract<
Query<(
@ -905,6 +923,19 @@ pub fn extract_meshes_for_cpu_building(
return;
}
let Some(mesh_material_asset_id) = mesh_material_ids
.mesh_to_material
.get(&MainEntity::from(entity))
else {
return;
};
let Some(mesh_material_binding_id) = mesh_material_ids
.material_to_binding
.get(mesh_material_asset_id)
else {
return;
};
let mut lod_index = None;
if visibility_range {
lod_index = render_visibility_ranges.lod_index_for_entity(entity.into());
@ -920,6 +951,7 @@ pub fn extract_meshes_for_cpu_building(
let shared = RenderMeshInstanceShared::from_components(
previous_transform,
mesh,
*mesh_material_binding_id,
not_shadow_caster,
no_automatic_batching,
);
@ -967,6 +999,7 @@ pub fn extract_meshes_for_cpu_building(
pub fn extract_meshes_for_gpu_building(
mut render_mesh_instances: ResMut<RenderMeshInstances>,
render_visibility_ranges: Res<RenderVisibilityRanges>,
mesh_material_ids: Res<RenderMeshMaterialIds>,
mut render_mesh_instance_queues: ResMut<RenderMeshInstanceGpuQueues>,
meshes_query: Extract<
Query<(
@ -1021,6 +1054,19 @@ pub fn extract_meshes_for_gpu_building(
return;
}
let Some(mesh_material_asset_id) = mesh_material_ids
.mesh_to_material
.get(&MainEntity::from(entity))
else {
return;
};
let Some(mesh_material_binding_id) = mesh_material_ids
.material_to_binding
.get(mesh_material_asset_id)
else {
return;
};
let mut lod_index = None;
if visibility_range {
lod_index = render_visibility_ranges.lod_index_for_entity(entity.into());
@ -1036,6 +1082,7 @@ pub fn extract_meshes_for_gpu_building(
let shared = RenderMeshInstanceShared::from_components(
previous_transform,
mesh,
*mesh_material_binding_id,
not_shadow_caster,
no_automatic_batching,
);
@ -1283,7 +1330,11 @@ impl GetBatchData for MeshPipeline {
);
// The material bind group ID, the mesh ID, and the lightmap ID,
// respectively.
type CompareData = (MaterialBindGroupId, AssetId<Mesh>, Option<AssetId<Image>>);
type CompareData = (
MaterialBindGroupIndex,
AssetId<Mesh>,
Option<AssetId<Image>>,
);
type BufferData = MeshUniform;
@ -1306,14 +1357,17 @@ impl GetBatchData for MeshPipeline {
};
let maybe_lightmap = lightmaps.render_lightmaps.get(&main_entity);
let material_bind_group_index = mesh_instance.material_bindings_index;
Some((
MeshUniform::new(
&mesh_instance.transforms,
first_vertex_index,
material_bind_group_index.slot,
maybe_lightmap.map(|lightmap| lightmap.uv_rect),
),
mesh_instance.should_batch().then_some((
mesh_instance.material_bind_group_id.get(),
material_bind_group_index.group,
mesh_instance.mesh_asset_id,
maybe_lightmap.map(|lightmap| lightmap.image),
)),
@ -1343,7 +1397,7 @@ impl GetFullBatchData for MeshPipeline {
Some((
mesh_instance.current_uniform_index,
mesh_instance.should_batch().then_some((
mesh_instance.material_bind_group_id.get(),
mesh_instance.material_bindings_index.group,
mesh_instance.mesh_asset_id,
maybe_lightmap.map(|lightmap| lightmap.image),
)),
@ -1371,6 +1425,7 @@ impl GetFullBatchData for MeshPipeline {
Some(MeshUniform::new(
&mesh_instance.transforms,
first_vertex_index,
mesh_instance.material_bindings_index.slot,
maybe_lightmap.map(|lightmap| lightmap.uv_rect),
))
}

View File

@ -23,9 +23,10 @@ struct MeshInput {
// applicable. If not present, this is `u32::MAX`.
previous_input_index: u32,
first_vertex_index: u32,
// Index of the material inside the bind group data.
material_bind_group_slot: u32,
pad_a: u32,
pad_b: u32,
pad_c: u32,
}
// Information about each mesh instance needed to cull it on GPU.
@ -191,4 +192,6 @@ fn main(@builtin(global_invocation_id) global_invocation_id: vec3<u32>) {
output[mesh_output_index].flags = current_input[input_index].flags;
output[mesh_output_index].lightmap_uv_rect = current_input[input_index].lightmap_uv_rect;
output[mesh_output_index].first_vertex_index = current_input[input_index].first_vertex_index;
output[mesh_output_index].material_bind_group_slot =
current_input[input_index].material_bind_group_slot;
}

View File

@ -17,9 +17,10 @@ struct Mesh {
lightmap_uv_rect: vec2<u32>,
// The index of the mesh's first vertex in the vertex buffer.
first_vertex_index: u32,
// Index of the material inside the bind group data.
material_bind_group_slot: u32,
pad_a: u32,
pad_b: u32,
pad_c: u32,
};
#ifdef SKINNED

View File

@ -6,7 +6,7 @@ use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
token::Comma,
Data, DataStruct, Error, Fields, LitInt, LitStr, Meta, MetaList, Result,
Data, DataStruct, Error, Fields, Lit, LitInt, LitStr, Meta, MetaList, Result,
};
const UNIFORM_ATTRIBUTE_NAME: Symbol = Symbol("uniform");
@ -15,6 +15,7 @@ const STORAGE_TEXTURE_ATTRIBUTE_NAME: Symbol = Symbol("storage_texture");
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");
#[derive(Copy, Clone, Debug)]
enum BindingType {
@ -49,6 +50,18 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
let mut binding_impls = Vec::new();
let mut binding_layouts = Vec::new();
let mut attr_prepared_data_ident = None;
let mut attr_bindless_count = None;
// `actual_bindless_slot_count` holds the actual number of bindless slots
// per bind group, taking into account whether the current platform supports
// bindless resources.
let actual_bindless_slot_count = Ident::new("actual_bindless_slot_count", Span::call_site());
// The `BufferBindingType` and corresponding `BufferUsages` used for
// uniforms. We need this because bindless uniforms don't exist, so in
// bindless mode we must promote uniforms to storage buffers.
let uniform_binding_type = Ident::new("uniform_binding_type", Span::call_site());
let uniform_buffer_usages = Ident::new("uniform_buffer_usages", Span::call_site());
// Read struct-level attributes
for attr in &ast.attrs {
@ -71,7 +84,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
usage: #uniform_buffer_usages,
contents: buffer.as_ref(),
},
))
@ -83,11 +96,11 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
binding: #binding_index,
visibility: #render_path::render_resource::ShaderStages::all(),
ty: #render_path::render_resource::BindingType::Buffer {
ty: #render_path::render_resource::BufferBindingType::Uniform,
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,
count: #actual_bindless_slot_count,
}
});
@ -96,6 +109,12 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
binding_states.resize(required_len, BindingState::Free);
}
binding_states[binding_index as usize] = BindingState::OccupiedConvertedUniform;
} else if attr_ident == BINDLESS_ATTRIBUTE_NAME {
if let Ok(count_lit) =
attr.parse_args_with(|input: ParseStream| input.parse::<Lit>())
{
attr_bindless_count = Some(count_lit);
}
}
}
}
@ -244,7 +263,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
count: #actual_bindless_slot_count,
}
});
}
@ -264,14 +283,17 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
// insert fallible texture-based entries at 0 so that if we fail here, we exit before allocating any buffers
binding_impls.insert(0, quote! {
( #binding_index,
#render_path::render_resource::OwnedBindingResource::TextureView({
let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into();
if let Some(handle) = handle {
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone()
} else {
#fallback_image.texture_view.clone()
}
})
#render_path::render_resource::OwnedBindingResource::TextureView(
#dimension,
{
let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into();
if let Some(handle) = handle {
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone()
} else {
#fallback_image.texture_view.clone()
}
}
)
)
});
@ -284,7 +306,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
format: #render_path::render_resource::TextureFormat::#image_format,
view_dimension: #render_path::render_resource::#dimension,
},
count: None,
count: #actual_bindless_slot_count,
}
});
}
@ -305,14 +327,17 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
binding_impls.insert(0, quote! {
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::TextureView({
let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into();
if let Some(handle) = handle {
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone()
} else {
#fallback_image.texture_view.clone()
#render_path::render_resource::OwnedBindingResource::TextureView(
#render_path::render_resource::#dimension,
{
let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into();
if let Some(handle) = handle {
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone()
} else {
#fallback_image.texture_view.clone()
}
}
})
)
)
});
@ -325,7 +350,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
sample_type: #render_path::render_resource::#sample_type,
view_dimension: #render_path::render_resource::#dimension,
},
count: None,
count: #actual_bindless_slot_count,
}
});
}
@ -397,7 +422,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
binding: #binding_index,
visibility: #visibility,
ty: #render_path::render_resource::BindingType::Sampler(#render_path::render_resource::#sampler_binding_type),
count: None,
count: #actual_bindless_slot_count,
}
});
}
@ -410,6 +435,37 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
let struct_name_literal = struct_name.to_string();
let struct_name_literal = struct_name_literal.as_str();
let mut field_struct_impls = Vec::new();
let uniform_binding_type_declarations = match attr_bindless_count {
Some(_) => {
quote! {
let (#uniform_binding_type, #uniform_buffer_usages) =
if render_device.features().contains(
#render_path::settings::WgpuFeatures::BUFFER_BINDING_ARRAY |
#render_path::settings::WgpuFeatures::TEXTURE_BINDING_ARRAY
) && render_device.limits().max_storage_buffers_per_shader_stage > 0 {
(
#render_path::render_resource::BufferBindingType::Storage { read_only: true },
#render_path::render_resource::BufferUsages::STORAGE,
)
} else {
(
#render_path::render_resource::BufferBindingType::Uniform,
#render_path::render_resource::BufferUsages::UNIFORM,
)
};
}
}
None => {
quote! {
let (#uniform_binding_type, #uniform_buffer_usages) = (
#render_path::render_resource::BufferBindingType::Uniform,
#render_path::render_resource::BufferUsages::UNIFORM,
);
}
}
};
for (binding_index, binding_state) in binding_states.iter().enumerate() {
let binding_index = binding_index as u32;
if let BindingState::OccupiedMergeableUniform { uniform_fields } = binding_state {
@ -426,7 +482,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #uniform_buffer_usages,
contents: buffer.as_ref(),
},
))
@ -438,11 +494,11 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
binding: #binding_index,
visibility: #render_path::render_resource::ShaderStages::all(),
ty: #render_path::render_resource::BindingType::Buffer {
ty: #render_path::render_resource::BufferBindingType::Uniform,
ty: #uniform_binding_type,
has_dynamic_offset: false,
min_binding_size: Some(<#field_ty as #render_path::render_resource::ShaderType>::min_size()),
},
count: None,
count: actual_bindless_slot_count,
}
});
// multi-field uniform bindings for a given index require an intermediate struct to derive ShaderType
@ -472,7 +528,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #uniform_buffer_usages,
contents: buffer.as_ref(),
},
))
@ -484,11 +540,11 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
binding: #binding_index,
visibility: #render_path::render_resource::ShaderStages::all(),
ty: #render_path::render_resource::BindingType::Buffer {
ty: #render_path::render_resource::BufferBindingType::Uniform,
ty: #uniform_binding_type,
has_dynamic_offset: false,
min_binding_size: Some(<#uniform_struct_name as #render_path::render_resource::ShaderType>::min_size()),
},
count: None,
count: actual_bindless_slot_count,
}
});
}
@ -506,6 +562,28 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
(prepared_data.clone(), prepared_data)
};
// Calculate the actual number of bindless slots, taking hardware
// limitations into account.
let (bindless_slot_count, actual_bindless_slot_count_declaration) = match attr_bindless_count {
Some(bindless_count) => (
quote! { const BINDLESS_SLOT_COUNT: Option<u32> = Some(#bindless_count); },
quote! {
let #actual_bindless_slot_count = if render_device.features().contains(
#render_path::settings::WgpuFeatures::BUFFER_BINDING_ARRAY |
#render_path::settings::WgpuFeatures::TEXTURE_BINDING_ARRAY
) && render_device.limits().max_storage_buffers_per_shader_stage > 0 {
::core::num::NonZeroU32::new(#bindless_count)
} else {
None
};
},
),
None => (
TokenStream::new().into(),
quote! { let #actual_bindless_slot_count: Option<::core::num::NonZeroU32> = None; },
),
};
Ok(TokenStream::from(quote! {
#(#field_struct_impls)*
@ -518,6 +596,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
#ecs_path::system::lifetimeless::SRes<#render_path::render_asset::RenderAssets<#render_path::storage::GpuShaderStorageBuffer>>,
);
#bindless_slot_count
fn label() -> Option<&'static str> {
Some(#struct_name_literal)
}
@ -528,7 +608,9 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
render_device: &#render_path::renderer::RenderDevice,
(images, fallback_image, storage_buffers): &mut #ecs_path::system::SystemParamItem<'_, '_, Self::Param>,
) -> Result<#render_path::render_resource::UnpreparedBindGroup<Self::Data>, #render_path::render_resource::AsBindGroupError> {
let bindings = vec![#(#binding_impls,)*];
#uniform_binding_type_declarations
let bindings = #render_path::render_resource::BindingResources(vec![#(#binding_impls,)*]);
Ok(#render_path::render_resource::UnpreparedBindGroup {
bindings,
@ -537,6 +619,9 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
}
fn bind_group_layout_entries(render_device: &#render_path::renderer::RenderDevice) -> Vec<#render_path::render_resource::BindGroupLayoutEntry> {
#actual_bindless_slot_count_declaration
#uniform_binding_type_declarations
vec![#(#binding_layouts,)*]
}
}

View File

@ -57,7 +57,15 @@ pub fn derive_extract_component(input: TokenStream) -> TokenStream {
#[proc_macro_derive(
AsBindGroup,
attributes(uniform, storage_texture, texture, sampler, bind_group_data, storage)
attributes(
uniform,
storage_texture,
texture,
sampler,
bind_group_data,
storage,
bindless
)
)]
pub fn derive_as_bind_group(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);

View File

@ -13,7 +13,7 @@ use crate::{
};
use allocator::MeshAllocatorPlugin;
use bevy_app::{App, Plugin, PostUpdate};
use bevy_asset::{AssetApp, RenderAssetUsages};
use bevy_asset::{AssetApp, AssetId, RenderAssetUsages};
use bevy_ecs::{
entity::Entity,
query::{Changed, With},
@ -169,6 +169,7 @@ impl RenderAsset for RenderMesh {
/// Converts the extracted mesh into a [`RenderMesh`].
fn prepare_asset(
mesh: Self::SourceAsset,
_: AssetId<Self::SourceAsset>,
(images, ref mut mesh_vertex_buffer_layouts): &mut SystemParamItem<Self::Param>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
let morph_targets = match mesh.morph_targets() {

View File

@ -6,7 +6,7 @@ pub use bevy_asset::RenderAssetUsages;
use bevy_asset::{Asset, AssetEvent, AssetId, Assets};
use bevy_ecs::{
prelude::{Commands, EventReader, IntoSystemConfigs, ResMut, Resource},
schedule::SystemConfigs,
schedule::{SystemConfigs, SystemSet},
system::{StaticSystemParam, SystemParam, SystemParamItem, SystemState},
world::{FromWorld, Mut},
};
@ -26,6 +26,10 @@ pub enum PrepareAssetError<E: Send + Sync + 'static> {
AsBindGroupError(AsBindGroupError),
}
/// The system set during which we extract modified assets to the render world.
#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]
pub struct ExtractAssetsSet;
/// Describes how an asset gets extracted and prepared for rendering.
///
/// In the [`ExtractSchedule`] step the [`RenderAsset::SourceAsset`] is transferred
@ -61,8 +65,21 @@ pub trait RenderAsset: Send + Sync + 'static + Sized {
/// ECS data may be accessed via `param`.
fn prepare_asset(
source_asset: Self::SourceAsset,
asset_id: AssetId<Self::SourceAsset>,
param: &mut SystemParamItem<Self::Param>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>>;
/// Called whenever the [`RenderAsset::SourceAsset`] has been removed.
///
/// You can implement this method if you need to access ECS data (via
/// `_param`) in order to perform cleanup tasks when the asset is removed.
///
/// The default implementation does nothing.
fn unload_asset(
_source_asset: AssetId<Self::SourceAsset>,
_param: &mut SystemParamItem<Self::Param>,
) {
}
}
/// This plugin extracts the changed assets from the "app world" into the "render world"
@ -99,7 +116,10 @@ impl<A: RenderAsset, AFTER: RenderAssetDependency + 'static> Plugin
.init_resource::<ExtractedAssets<A>>()
.init_resource::<RenderAssets<A>>()
.init_resource::<PrepareNextFrameAssets<A>>()
.add_systems(ExtractSchedule, extract_render_asset::<A>);
.add_systems(
ExtractSchedule,
extract_render_asset::<A>.in_set(ExtractAssetsSet),
);
AFTER::register_system(
render_app,
prepare_assets::<A>.in_set(RenderSet::PrepareAssets),
@ -310,7 +330,7 @@ pub fn prepare_assets<A: RenderAsset>(
0
};
match A::prepare_asset(extracted_asset, &mut param) {
match A::prepare_asset(extracted_asset, id, &mut param) {
Ok(prepared_asset) => {
render_assets.insert(id, prepared_asset);
bpf.write_bytes(write_bytes);
@ -330,6 +350,7 @@ pub fn prepare_assets<A: RenderAsset>(
for removed in extracted_assets.removed.drain() {
render_assets.remove(removed);
A::unload_asset(removed, &mut param);
}
for (id, extracted_asset) in extracted_assets.extracted.drain(..) {
@ -348,7 +369,7 @@ pub fn prepare_assets<A: RenderAsset>(
0
};
match A::prepare_asset(extracted_asset, &mut param) {
match A::prepare_asset(extracted_asset, id, &mut param) {
Ok(prepared_asset) => {
render_assets.insert(id, prepared_asset);
bpf.write_bytes(write_bytes);

View File

@ -7,12 +7,13 @@ use crate::{
texture::GpuImage,
};
use alloc::sync::Arc;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::system::{SystemParam, SystemParamItem};
pub use bevy_render_macros::AsBindGroup;
use core::ops::Deref;
use derive_more::derive::{Display, Error};
use encase::ShaderType;
use wgpu::{BindGroupEntry, BindGroupLayoutEntry, BindingResource};
use wgpu::{BindGroupEntry, BindGroupLayoutEntry, BindingResource, TextureViewDimension};
define_atomic_id!(BindGroupId);
@ -247,6 +248,28 @@ impl Deref for BindGroup {
/// as [`AsBindGroup::Data`] as part of the [`AsBindGroup::as_bind_group`] call. This is useful if data needs to be stored alongside
/// the generated bind group, such as a unique identifier for a material's bind group. The most common use case for this attribute
/// is "shader pipeline specialization". See [`SpecializedRenderPipeline`](crate::render_resource::SpecializedRenderPipeline).
/// * `bindless(COUNT)`
/// * This switch enables *bindless resources*, which changes the way Bevy
/// supplies resources (uniforms, textures, and samplers) to the shader.
/// When bindless resources are enabled, and the current platform supports
/// them, instead of presenting a single instance of a resource to your
/// shader Bevy will instead present a *binding array* of `COUNT` elements.
/// In your shader, the index of the element of each binding array
/// corresponding to the mesh currently being drawn can be retrieved with
/// `mesh[in.instance_index].material_bind_group_slot`.
/// * Bindless uniforms don't exist, so in bindless mode all uniforms and
/// uniform buffers are automatically replaced with read-only storage
/// buffers.
/// * The purpose of bindless mode is to improve performance by reducing
/// state changes. By grouping resources together into binding arrays, Bevy
/// doesn't have to modify GPU state as often, decreasing API and driver
/// overhead.
/// * If bindless mode is enabled, the `BINDLESS` definition will be
/// available. Because not all platforms support bindless resources, you
/// should check for the presence of this definition via `#ifdef` and fall
/// back to standard bindings if it isn't present.
/// * See the `shaders/shader_material_bindless` example for an example of
/// how to use bindless mode.
///
/// The previous `CoolMaterial` example illustrating "combining multiple field-level uniform attributes with the same binding index" can
/// also be equivalently represented with a single struct-level uniform attribute:
@ -307,6 +330,15 @@ pub trait AsBindGroup {
type Param: SystemParam + 'static;
/// The number of slots per bind group, if bindless mode is enabled.
///
/// If this bind group doesn't use bindless, then this will be `None`.
///
/// Note that the *actual* slot count may be different from this value, due
/// to platform limitations. For example, if bindless resources aren't
/// supported on this platform, the actual slot count will be 1.
const BINDLESS_SLOT_COUNT: Option<u32> = None;
/// label
fn label() -> Option<&'static str> {
None
@ -379,24 +411,29 @@ pub enum AsBindGroupError {
/// A prepared bind group returned as a result of [`AsBindGroup::as_bind_group`].
pub struct PreparedBindGroup<T> {
pub bindings: Vec<(u32, OwnedBindingResource)>,
pub bindings: BindingResources,
pub bind_group: BindGroup,
pub data: T,
}
/// a map containing `OwnedBindingResource`s, keyed by the target binding index
pub struct UnpreparedBindGroup<T> {
pub bindings: Vec<(u32, OwnedBindingResource)>,
pub bindings: BindingResources,
pub data: T,
}
/// A pair of binding index and binding resource, used as part of
/// [`PreparedBindGroup`] and [`UnpreparedBindGroup`].
#[derive(Deref, DerefMut)]
pub struct BindingResources(pub Vec<(u32, OwnedBindingResource)>);
/// An owned binding resource of any type (ex: a [`Buffer`], [`TextureView`], etc).
/// This is used by types like [`PreparedBindGroup`] to hold a single list of all
/// render resources used by bindings.
#[derive(Debug)]
pub enum OwnedBindingResource {
Buffer(Buffer),
TextureView(TextureView),
TextureView(TextureViewDimension, TextureView),
Sampler(Sampler),
}
@ -404,7 +441,7 @@ impl OwnedBindingResource {
pub fn get_binding(&self) -> BindingResource {
match self {
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),
}
}

View File

@ -49,13 +49,13 @@ pub use wgpu::{
PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, PolygonMode,
PrimitiveState, PrimitiveTopology, PushConstantRange, RenderPassColorAttachment,
RenderPassDepthStencilAttachment, RenderPassDescriptor,
RenderPipelineDescriptor as RawRenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor,
ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, StencilFaceState,
StencilOperation, StencilState, StorageTextureAccess, StoreOp, TextureAspect,
TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages,
TextureViewDescriptor, TextureViewDimension, VertexAttribute,
VertexBufferLayout as RawVertexBufferLayout, VertexFormat, VertexState as RawVertexState,
VertexStepMode, COPY_BUFFER_ALIGNMENT,
RenderPipelineDescriptor as RawRenderPipelineDescriptor, Sampler as WgpuSampler,
SamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource,
ShaderStages, StencilFaceState, StencilOperation, StencilState, StorageTextureAccess, StoreOp,
TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType,
TextureUsages, TextureView as WgpuTextureView, TextureViewDescriptor, TextureViewDimension,
VertexAttribute, VertexBufferLayout as RawVertexBufferLayout, VertexFormat,
VertexState as RawVertexState, VertexStepMode, COPY_BUFFER_ALIGNMENT,
};
pub use crate::mesh::VertexBufferLayout;

View File

@ -4,7 +4,7 @@ use crate::{
renderer::RenderDevice,
};
use bevy_app::{App, Plugin};
use bevy_asset::{Asset, AssetApp};
use bevy_asset::{Asset, AssetApp, AssetId};
use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem};
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use bevy_utils::default;
@ -114,6 +114,7 @@ impl RenderAsset for GpuShaderStorageBuffer {
fn prepare_asset(
source_asset: Self::SourceAsset,
_: AssetId<Self::SourceAsset>,
render_device: &mut SystemParamItem<Self::Param>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
match source_asset.data {

View File

@ -34,6 +34,20 @@ pub struct FallbackImage {
pub d3: GpuImage,
}
impl FallbackImage {
/// Returns the appropriate fallback image for the given texture dimension.
pub fn get(&self, texture_dimension: TextureViewDimension) -> &GpuImage {
match texture_dimension {
TextureViewDimension::D1 => &self.d1,
TextureViewDimension::D2 => &self.d2,
TextureViewDimension::D2Array => &self.d2_array,
TextureViewDimension::Cube => &self.cube,
TextureViewDimension::CubeArray => &self.cube_array,
TextureViewDimension::D3 => &self.d3,
}
}
}
/// A [`RenderApp`](crate::RenderApp) resource that contains a _zero-filled_ "fallback image",
/// which can be used in place of [`FallbackImage`], when a fully transparent or black fallback
/// is required instead of fully opaque white.

View File

@ -3,6 +3,7 @@ use crate::{
render_resource::{DefaultImageSampler, Sampler, Texture, TextureView},
renderer::{RenderDevice, RenderQueue},
};
use bevy_asset::AssetId;
use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem};
use bevy_image::{Image, ImageSampler};
use bevy_math::UVec2;
@ -41,6 +42,7 @@ impl RenderAsset for GpuImage {
/// Converts the extracted image into a [`GpuImage`].
fn prepare_asset(
image: Self::SourceAsset,
_: AssetId<Self::SourceAsset>,
(render_device, render_queue, default_sampler): &mut SystemParamItem<Self::Param>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
let texture = render_device.create_texture_with_data(

View File

@ -17,7 +17,6 @@ use bevy_ecs::{
};
use bevy_math::FloatOrd;
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use bevy_render::sync_world::MainEntityHashMap;
use bevy_render::view::RenderVisibleEntities;
use bevy_render::{
mesh::{MeshVertexBufferLayoutRef, RenderMesh},
@ -30,14 +29,15 @@ use bevy_render::{
ViewBinnedRenderPhases, ViewSortedRenderPhases,
},
render_resource::{
AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout,
OwnedBindingResource, PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef,
SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines,
AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, PipelineCache,
RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline,
SpecializedMeshPipelineError, SpecializedMeshPipelines,
},
renderer::RenderDevice,
view::{ExtractedView, InheritedVisibility, Msaa, ViewVisibility, Visibility},
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_render::{render_resource::BindingResources, sync_world::MainEntityHashMap};
use bevy_transform::components::{GlobalTransform, Transform};
use bevy_utils::tracing::error;
use core::{hash::Hash, marker::PhantomData};
@ -630,7 +630,7 @@ pub struct Material2dProperties {
/// Data prepared for a [`Material2d`] instance.
pub struct PreparedMaterial2d<T: Material2d> {
pub bindings: Vec<(u32, OwnedBindingResource)>,
pub bindings: BindingResources,
pub bind_group: BindGroup,
pub key: T::Data,
pub properties: Material2dProperties,
@ -649,6 +649,7 @@ impl<M: Material2d> RenderAsset for PreparedMaterial2d<M> {
fn prepare_asset(
material: Self::SourceAsset,
_: AssetId<Self::SourceAsset>,
(render_device, pipeline, material_param): &mut SystemParamItem<Self::Param>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
match material.as_bind_group(&pipeline.material2d_layout, render_device, material_param) {

View File

@ -576,7 +576,7 @@ pub fn prepare_uimaterial_nodes<M: UiMaterial>(
}
pub struct PreparedUiMaterial<T: UiMaterial> {
pub bindings: Vec<(u32, OwnedBindingResource)>,
pub bindings: BindingResources,
pub bind_group: BindGroup,
pub key: T::Data,
}
@ -588,6 +588,7 @@ impl<M: UiMaterial> RenderAsset for PreparedUiMaterial<M> {
fn prepare_asset(
material: Self::SourceAsset,
_: AssetId<Self::SourceAsset>,
(render_device, pipeline, ref mut material_param): &mut SystemParamItem<Self::Param>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
match material.as_bind_group(&pipeline.ui_layout, render_device, material_param) {

View File

@ -426,6 +426,7 @@ Example | Description
[Instancing](../examples/shader/automatic_instancing.rs) | Shows that multiple instances of a cube are automatically instanced in one draw call
[Material](../examples/shader/shader_material.rs) | A shader and a material that uses it
[Material](../examples/shader/shader_material_2d.rs) | A shader and a material that uses it on a 2d mesh
[Material - Bindless](../examples/shader/shader_material_bindless.rs) | Demonstrates how to make materials that use bindless textures
[Material - GLSL](../examples/shader/shader_material_glsl.rs) | A shader that uses the GLSL shading language
[Material - Screenspace Texture](../examples/shader/shader_material_screenspace_texture.rs) | A shader that samples a texture with view-independent UV coordinates
[Material Prepass](../examples/shader/shader_prepass.rs) | A shader that uses the various textures generated by the prepass

View File

@ -273,7 +273,7 @@ fn queue_custom_phase_item(
draw_function: draw_custom_phase_item,
pipeline: pipeline_id,
asset_id: AssetId::<Mesh>::invalid().untyped(),
material_bind_group_id: None,
material_bind_group_index: None,
lightmap_image: None,
},
entity,

View File

@ -0,0 +1,73 @@
//! A material that uses bindless textures.
use bevy::prelude::*;
use bevy::render::render_resource::{AsBindGroup, ShaderRef};
const SHADER_ASSET_PATH: &str = "shaders/bindless_material.wgsl";
// `#[bindless(4)]` indicates that we want Bevy to group materials into bind
// groups of at most 4 materials each.
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
#[bindless(4)]
struct BindlessMaterial {
// This will be exposed to the shader as a binding array of 4 *storage*
// buffers (as bindless uniforms don't exist).
#[uniform(0)]
color: LinearRgba,
// This will be exposed to the shader as a binding array of 4 textures and a
// binding array of 4 samplers.
#[texture(1)]
#[sampler(2)]
color_texture: Option<Handle<Image>>,
}
// The entry point.
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
MaterialPlugin::<BindlessMaterial>::default(),
))
.add_systems(Startup, setup)
.run();
}
// Creates a simple scene.
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<BindlessMaterial>>,
asset_server: Res<AssetServer>,
) {
// Add a cube with a blue tinted texture.
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(BindlessMaterial {
color: LinearRgba::BLUE,
color_texture: Some(asset_server.load("branding/bevy_logo_dark.png")),
})),
Transform::from_xyz(-2.0, 0.5, 0.0),
));
// Add a cylinder with a red tinted texture.
commands.spawn((
Mesh3d(meshes.add(Cylinder::default())),
MeshMaterial3d(materials.add(BindlessMaterial {
color: LinearRgba::RED,
color_texture: Some(asset_server.load("branding/bevy_logo_light.png")),
})),
Transform::from_xyz(2.0, 0.5, 0.0),
));
// Add a camera.
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
impl Material for BindlessMaterial {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
}

View File

@ -341,7 +341,7 @@ fn queue_custom_mesh_pipeline(
// but you can use anything you like. Note that the asset ID need
// not be the ID of a [`Mesh`].
asset_id: AssetId::<Mesh>::invalid().untyped(),
material_bind_group_id: None,
material_bind_group_index: None,
lightmap_image: None,
},
(render_entity, visible_entity),

View File

@ -133,7 +133,7 @@ impl AsBindGroup for BindlessMaterial {
);
Ok(PreparedBindGroup {
bindings: vec![],
bindings: BindingResources(vec![]),
bind_group,
data: (),
})