Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Request WGPU Capabilities for Non-uniform Indexing #6995

Closed
wants to merge 12 commits into from
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,16 @@ description = "A shader that shows how to reuse the core bevy PBR shading functi
category = "Shaders"
wasm = true

[[example]]
name = "texture_binding_array"
path = "examples/shader/texture_binding_array.rs"

[package.metadata.example.texture_binding_array]
name = "Texture Binding Array (Bindless Textures)"
description = "A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures)."
category = "Shaders"
wasm = false

# Stress tests
[[package.metadata.category]]
name = "Stress Tests"
Expand Down
15 changes: 15 additions & 0 deletions assets/shaders/texture_binding_array.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@group(1) @binding(0)
var textures: binding_array<texture_2d<f32>>;
@group(1) @binding(1)
var samplers: binding_array<sampler>;

@fragment
fn fragment(
#import bevy_pbr::mesh_vertex_output
) -> @location(0) vec4<f32> {
// Select the texture to sample from using non-uniform uv coordinates
let coords = clamp(vec2<u32>(uv * 4.0), vec2<u32>(0u), vec2<u32>(3u));
let index = coords.y * 4u + coords.x;
let inner_uv = fract(uv * 4.0);
return textureSample(textures[index], samplers[index], inner_uv);
}
12 changes: 12 additions & 0 deletions crates/bevy_render/src/render_resource/shader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,18 @@ impl ProcessedShader {
Features::SHADER_PRIMITIVE_INDEX,
Capabilities::PRIMITIVE_INDEX,
),
(
Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when those features are not available? Should it just be user configurable instead of always asking for it?

Copy link
Contributor Author

@cryscan cryscan Dec 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to

pub fn get_module_descriptor(
&self,
features: Features,
) -> Result<ShaderModuleDescriptor, AsModuleDescriptorError> {
Ok(ShaderModuleDescriptor {
label: None,
source: match self {
ProcessedShader::Wgsl(source) => {
#[cfg(debug_assertions)]
// Parse and validate the shader early, so that (e.g. while hot reloading) we can
// display nicely formatted error messages instead of relying on just displaying the error string
// returned by wgpu upon creating the shader module.
let _ = self.reflect(features)?;
ShaderSource::Wgsl(source.clone())
}
ProcessedShader::Glsl(_source, _stage) => {
let reflection = self.reflect(features)?;
// TODO: it probably makes more sense to convert this to spirv, but as of writing
// this comment, naga's spirv conversion is broken
let wgsl = reflection.get_wgsl()?;
ShaderSource::Wgsl(wgsl.into())
}
ProcessedShader::SpirV(source) => make_spirv(source),
},
})
}
}
and the caller
.get_module_descriptor(render_device.features())
, only available features are passed in. If certain features are not available, the corresponding capabilities won't be requested, and there will be a validation error only if the features are actually used in the shader.

Capabilities::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
),
(
Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
Capabilities::SAMPLER_NON_UNIFORM_INDEXING,
Comment on lines +149 to +150
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the right feature->capability pair?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no Features::SAMPLER_NON_UNIFORM_INDEXING. This capability is required to have non-uniform indexed samplers, usually in pair with the usage of non-uniform indexed textures.

),
(
Features::UNIFORM_BUFFER_AND_STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING,
Capabilities::UNIFORM_BUFFER_AND_STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING,
),
];
let mut capabilities = Capabilities::empty();
for (feature, capability) in CAPABILITIES {
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ Example | Description
[Material Prepass](../examples/shader/shader_prepass.rs) | A shader that uses the depth texture generated in a prepass
[Post Processing](../examples/shader/post_processing.rs) | A custom post processing effect, using two cameras, with one reusing the render texture of the first one
[Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader)
[Texture Binding Array (Bindless Textures)](../examples/shader/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures).

## Stress Tests

Expand Down
165 changes: 165 additions & 0 deletions examples/shader/texture_binding_array.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//! A shader that binds several textures onto one
//! `binding_array<texture<f32>>` shader binding slot and sample non-uniformly.

use bevy::{
prelude::*,
reflect::TypeUuid,
render::{
render_asset::RenderAssets,
render_resource::{AsBindGroupError, PreparedBindGroup, *},
renderer::RenderDevice,
texture::FallbackImage,
},
};
use std::num::NonZeroU32;

fn main() {
App::new()
.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
.add_plugin(MaterialPlugin::<BindlessMaterial>::default())
.add_startup_system(setup)
.run();
}

const MAX_TEXTURE_COUNT: usize = 16;
const TILE_ID: [usize; 16] = [
19, 23, 4, 33, 12, 69, 30, 48, 10, 65, 40, 47, 57, 41, 44, 46,
];

fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<BindlessMaterial>>,
asset_server: Res<AssetServer>,
render_device: Res<RenderDevice>,
) {
// check if the device support the required feature
if !render_device
.features()
.contains(WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING)
{
error!(
"Render device doesn't support feature \
SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, \
which is required for texture binding arrays"
);
return;
}

commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(2.0, 2.0, 2.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
..Default::default()
});

// load 16 textures
let textures: Vec<_> = TILE_ID
.iter()
.map(|id| {
let path = format!("textures/rpg/tiles/generic-rpg-tile{:0>2}.png", id);
asset_server.load(path)
})
.collect();

// a cube with multiple textures
commands.spawn(MaterialMeshBundle {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we pls check the capabilities in the demo, (both to show how it's done, and what is required)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, will do!

mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
material: materials.add(BindlessMaterial { textures }),
..Default::default()
});
}

#[derive(Debug, Clone, TypeUuid)]
#[uuid = "8dd2b424-45a2-4a53-ac29-7ce356b2d5fe"]
struct BindlessMaterial {
textures: Vec<Handle<Image>>,
}

impl AsBindGroup for BindlessMaterial {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be great to get this into the derive macro. i don't think we need to block on that but lets add an issue at least when this is merged.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It's worth noting that AsBindGroup has a limitation that bind_group_layout() is a static function, and that's why there needs a constant MAX_TEXTURE_COUNT here.

type Data = ();

fn as_bind_group(
&self,
layout: &BindGroupLayout,
render_device: &RenderDevice,
image_assets: &RenderAssets<Image>,
fallback_image: &FallbackImage,
) -> Result<PreparedBindGroup<Self::Data>, AsBindGroupError> {
// retrieve the render resources from handles
let mut images = vec![];
for handle in self.textures.iter().take(MAX_TEXTURE_COUNT) {
match image_assets.get(handle) {
Some(image) => images.push(image),
None => return Err(AsBindGroupError::RetryNextUpdate),
}
}

let textures = vec![&fallback_image.texture_view; MAX_TEXTURE_COUNT];
let samplers = vec![&fallback_image.sampler; MAX_TEXTURE_COUNT];

// convert bevy's resource types to WGPU's references
let mut textures: Vec<_> = textures.into_iter().map(|texture| &**texture).collect();
let mut samplers: Vec<_> = samplers.into_iter().map(|sampler| &**sampler).collect();

// fill in up to the first `MAX_TEXTURE_COUNT` textures and samplers to the arrays
for (id, image) in images.into_iter().enumerate() {
textures[id] = &*image.texture_view;
samplers[id] = &*image.sampler;
}

let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
label: "bindless_material_bind_group".into(),
layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureViewArray(&textures[..]),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::SamplerArray(&samplers[..]),
},
],
});

Ok(PreparedBindGroup {
bindings: vec![],
bind_group,
data: (),
})
}

fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout
where
Self: Sized,
{
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: "bindless_material_layout".into(),
entries: &[
// @group(1) @binding(0) var textures: binding_array<texture_2d<f32>>;
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32),
},
// @group(1) @binding(1) var samplers: binding_array<sampler>;
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32),
},
],
})
}
}

impl Material for BindlessMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/texture_binding_array.wgsl".into()
}
}