1use bevy::{
5 ecs::system::{lifetimeless::SRes, SystemParamItem},
6 prelude::*,
7 reflect::TypePath,
8 render::{
9 material_bind_groups::FallbackBuffer,
10 render_asset::RenderAssets,
11 render_resource::{
12 binding_types::{sampler, texture_2d},
13 *,
14 },
15 renderer::RenderDevice,
16 storage::GpuShaderBuffer,
17 texture::{FallbackImage, GpuImage},
18 RenderApp, RenderStartup,
19 },
20 shader::ShaderRef,
21};
22use std::{num::NonZero, process::exit};
23
24const SHADER_ASSET_PATH: &str = "shaders/texture_binding_array.wesl";
26
27fn main() {
28 let mut app = App::new();
29 app.add_plugins((
30 DefaultPlugins.set(ImagePlugin::default_nearest()),
31 GpuFeatureSupportChecker,
32 MaterialPlugin::<BindlessMaterial>::default(),
33 ))
34 .add_systems(Startup, setup)
35 .run();
36}
37
38const MAX_TEXTURE_COUNT: usize = 16;
39const TILE_ID: [usize; 16] = [
40 19, 23, 4, 33, 12, 69, 30, 48, 10, 65, 40, 47, 57, 41, 44, 46,
41];
42
43struct GpuFeatureSupportChecker;
44
45impl Plugin for GpuFeatureSupportChecker {
46 fn build(&self, app: &mut App) {
47 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
48 return;
49 };
50
51 render_app.add_systems(RenderStartup, verify_required_features);
52 }
53}
54
55fn setup(
56 mut commands: Commands,
57 mut meshes: ResMut<Assets<Mesh>>,
58 mut materials: ResMut<Assets<BindlessMaterial>>,
59 asset_server: Res<AssetServer>,
60) {
61 commands.spawn((
62 Camera3d::default(),
63 Transform::from_xyz(2.0, 2.0, 2.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
64 ));
65
66 let textures: Vec<_> = TILE_ID
68 .iter()
69 .map(|id| asset_server.load(format!("textures/rpg/tiles/generic-rpg-tile{id:0>2}.png")))
70 .collect();
71
72 commands.spawn((
74 Mesh3d(meshes.add(Cuboid::default())),
75 MeshMaterial3d(materials.add(BindlessMaterial { textures })),
76 ));
77}
78
79fn verify_required_features(render_device: Res<RenderDevice>) {
80 if !render_device
83 .features()
84 .contains(WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING)
85 {
86 error!(
87 "Render device doesn't support feature \
88SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, \
89which is required for texture binding arrays"
90 );
91 exit(1);
92 }
93}
94
95#[derive(Asset, TypePath, Debug, Clone)]
96struct BindlessMaterial {
97 textures: Vec<Handle<Image>>,
98}
99
100impl AsBindGroup for BindlessMaterial {
101 type Data = ();
102
103 type Param = (SRes<RenderAssets<GpuImage>>, SRes<FallbackImage>);
104
105 fn as_bind_group(
106 &self,
107 layout: &BindGroupLayoutDescriptor,
108 render_device: &RenderDevice,
109 pipeline_cache: &PipelineCache,
110 _: &FallbackBuffer,
111 shader_buffer_assets: &RenderAssets<GpuShaderBuffer>,
112 (image_assets, fallback_image): &mut SystemParamItem<'_, '_, Self::Param>,
113 ) -> Result<PreparedBindGroup, AsBindGroupError> {
114 let _ = shader_buffer_assets;
115 let mut images = vec![];
117 for handle in self.textures.iter().take(MAX_TEXTURE_COUNT) {
118 match image_assets.get(handle) {
119 Some(image) => images.push(image),
120 None => return Err(AsBindGroupError::RetryNextUpdate),
121 }
122 }
123
124 let fallback_image = &fallback_image.d2;
125
126 let textures = vec![&fallback_image.texture_view; MAX_TEXTURE_COUNT];
127
128 let mut textures: Vec<_> = textures.into_iter().map(|texture| &**texture).collect();
130
131 for (id, image) in images.into_iter().enumerate() {
133 textures[id] = &*image.texture_view;
134 }
135
136 let bind_group = render_device.create_bind_group(
137 Self::label(),
138 &pipeline_cache.get_bind_group_layout(layout),
139 &BindGroupEntries::sequential((&textures[..], &fallback_image.sampler)),
140 );
141
142 Ok(PreparedBindGroup {
143 bindings: BindingResources(vec![]),
144 bind_group,
145 })
146 }
147
148 fn bind_group_data(&self) -> Self::Data {}
149
150 fn build_bind_group(
151 &self,
152 _layout: &BindGroupLayout,
153 _render_device: &RenderDevice,
154 _param: &mut SystemParamItem<'_, '_, Self::Param>,
155 _force_no_bindless: bool,
156 _output: &mut BindGroupBuilder,
157 ) -> Result<(), AsBindGroupError> {
158 Err(AsBindGroupError::CreateBindGroupDirectly)
163 }
164
165 fn bind_group_layout_entries(_: &RenderDevice, _: bool) -> Vec<BindGroupLayoutEntry>
166 where
167 Self: Sized,
168 {
169 BindGroupLayoutEntries::with_indices(
170 ShaderStages::FRAGMENT,
172 (
173 (
177 0,
178 texture_2d(TextureSampleType::Float { filterable: true })
179 .count(NonZero::<u32>::new(MAX_TEXTURE_COUNT as u32).unwrap()),
180 ),
181 (1, sampler(SamplerBindingType::Filtering)),
196 ),
197 )
198 .to_vec()
199 }
200
201 fn label() -> &'static str {
202 "bindless_material_bind_group"
203 }
204}
205
206impl Material for BindlessMaterial {
207 fn fragment_shader() -> ShaderRef {
208 SHADER_ASSET_PATH.into()
209 }
210}