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