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: &BindGroupLayout,
106 render_device: &RenderDevice,
107 (image_assets, fallback_image): &mut SystemParamItem<'_, '_, Self::Param>,
108 ) -> Result<PreparedBindGroup, AsBindGroupError> {
109 let mut images = vec![];
111 for handle in self.textures.iter().take(MAX_TEXTURE_COUNT) {
112 match image_assets.get(handle) {
113 Some(image) => images.push(image),
114 None => return Err(AsBindGroupError::RetryNextUpdate),
115 }
116 }
117
118 let fallback_image = &fallback_image.d2;
119
120 let textures = vec![&fallback_image.texture_view; MAX_TEXTURE_COUNT];
121
122 let mut textures: Vec<_> = textures.into_iter().map(|texture| &**texture).collect();
124
125 for (id, image) in images.into_iter().enumerate() {
127 textures[id] = &*image.texture_view;
128 }
129
130 let bind_group = render_device.create_bind_group(
131 "bindless_material_bind_group",
132 layout,
133 &BindGroupEntries::sequential((&textures[..], &fallback_image.sampler)),
134 );
135
136 Ok(PreparedBindGroup {
137 bindings: BindingResources(vec![]),
138 bind_group,
139 })
140 }
141
142 fn bind_group_data(&self) -> Self::Data {}
143
144 fn unprepared_bind_group(
145 &self,
146 _layout: &BindGroupLayout,
147 _render_device: &RenderDevice,
148 _param: &mut SystemParamItem<'_, '_, Self::Param>,
149 _force_no_bindless: bool,
150 ) -> Result<UnpreparedBindGroup, AsBindGroupError> {
151 Err(AsBindGroupError::CreateBindGroupDirectly)
156 }
157
158 fn bind_group_layout_entries(_: &RenderDevice, _: bool) -> Vec<BindGroupLayoutEntry>
159 where
160 Self: Sized,
161 {
162 BindGroupLayoutEntries::with_indices(
163 ShaderStages::FRAGMENT,
165 (
166 (
170 0,
171 texture_2d(TextureSampleType::Float { filterable: true })
172 .count(NonZero::<u32>::new(MAX_TEXTURE_COUNT as u32).unwrap()),
173 ),
174 (1, sampler(SamplerBindingType::Filtering)),
189 ),
190 )
191 .to_vec()
192 }
193}
194
195impl Material for BindlessMaterial {
196 fn fragment_shader() -> ShaderRef {
197 SHADER_ASSET_PATH.into()
198 }
199}