Skip to content

Commit 6440546

Browse files
authored
Expand FallbackImage to include a GpuImage for each possible TextureViewDimension (#6974)
# Objective Fixes #6920 ## Solution From the issue discussion: > From looking at the `AsBindGroup` derive macro implementation, the fallback image's `TextureView` is used when the binding's `Option<Handle<Image>>` is `None`. Because this relies on already having a view that matches the desired binding dimensions, I think the solution will require creating a separate `GpuImage` for each possible `TextureViewDimension`. --- ## Changelog Users can now rely on `FallbackImage` to work with a texture binding of any dimension.
1 parent 96b9b6c commit 6440546

File tree

6 files changed

+245
-19
lines changed

6 files changed

+245
-19
lines changed

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2052,6 +2052,13 @@ hidden = true
20522052
name = "window_resizing"
20532053
path = "examples/window/window_resizing.rs"
20542054

2055+
[[example]]
2056+
name = "fallback_image"
2057+
path = "examples/shader/fallback_image.rs"
2058+
2059+
[package.metadata.example.fallback_image]
2060+
hidden = true
2061+
20552062
[package.metadata.example.window_resizing]
20562063
name = "Window Resizing"
20572064
description = "Demonstrates resizing and responding to resizing a window"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#import bevy_pbr::mesh_view_bindings
2+
#import bevy_pbr::mesh_bindings
3+
4+
@group(1) @binding(0)
5+
var test_texture_1d: texture_1d<f32>;
6+
@group(1) @binding(1)
7+
var test_texture_1d_sampler: sampler;
8+
9+
@group(1) @binding(2)
10+
var test_texture_2d: texture_2d<f32>;
11+
@group(1) @binding(3)
12+
var test_texture_2d_sampler: sampler;
13+
14+
@group(1) @binding(4)
15+
var test_texture_2d_array: texture_2d_array<f32>;
16+
@group(1) @binding(5)
17+
var test_texture_2d_array_sampler: sampler;
18+
19+
@group(1) @binding(6)
20+
var test_texture_cube: texture_cube<f32>;
21+
@group(1) @binding(7)
22+
var test_texture_cube_sampler: sampler;
23+
24+
@group(1) @binding(8)
25+
var test_texture_cube_array: texture_cube_array<f32>;
26+
@group(1) @binding(9)
27+
var test_texture_cube_array_sampler: sampler;
28+
29+
@group(1) @binding(10)
30+
var test_texture_3d: texture_3d<f32>;
31+
@group(1) @binding(11)
32+
var test_texture_3d_sampler: sampler;
33+
34+
struct FragmentInput {
35+
#import bevy_pbr::mesh_vertex_output
36+
};
37+
38+
@fragment
39+
fn fragment(in: FragmentInput) {}

crates/bevy_render/macros/src/as_bind_group.rs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
117117

118118
// Read field-level attributes
119119
for field in fields.iter() {
120+
// Search ahead for texture attributes so we can use them with any
121+
// corresponding sampler attribute.
122+
let mut tex_attrs = None;
123+
for attr in &field.attrs {
124+
let Some(attr_ident) = attr.path().get_ident() else {
125+
continue;
126+
};
127+
if attr_ident == TEXTURE_ATTRIBUTE_NAME {
128+
let (_binding_index, nested_meta_items) = get_binding_nested_attr(attr)?;
129+
tex_attrs = Some(get_texture_attrs(nested_meta_items)?);
130+
}
131+
}
132+
120133
for attr in &field.attrs {
121134
let Some(attr_ident) = attr.path().get_ident() else {
122135
continue;
@@ -255,18 +268,20 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
255268
sample_type,
256269
multisampled,
257270
visibility,
258-
} = get_texture_attrs(nested_meta_items)?;
271+
} = tex_attrs.as_ref().unwrap();
259272

260273
let visibility =
261274
visibility.hygenic_quote(&quote! { #render_path::render_resource });
262275

276+
let fallback_image = get_fallback_image(&render_path, *dimension);
277+
263278
binding_impls.push(quote! {
264279
#render_path::render_resource::OwnedBindingResource::TextureView({
265280
let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into();
266281
if let Some(handle) = handle {
267282
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone()
268283
} else {
269-
fallback_image.texture_view.clone()
284+
#fallback_image.texture_view.clone()
270285
}
271286
})
272287
});
@@ -288,18 +303,24 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
288303
let SamplerAttrs {
289304
sampler_binding_type,
290305
visibility,
306+
..
291307
} = get_sampler_attrs(nested_meta_items)?;
308+
let TextureAttrs { dimension, .. } = tex_attrs
309+
.as_ref()
310+
.expect("sampler attribute must have matching texture attribute");
292311

293312
let visibility =
294313
visibility.hygenic_quote(&quote! { #render_path::render_resource });
295314

315+
let fallback_image = get_fallback_image(&render_path, *dimension);
316+
296317
binding_impls.push(quote! {
297318
#render_path::render_resource::OwnedBindingResource::Sampler({
298319
let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into();
299320
if let Some(handle) = handle {
300321
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.sampler.clone()
301322
} else {
302-
fallback_image.sampler.clone()
323+
#fallback_image.sampler.clone()
303324
}
304325
})
305326
});
@@ -457,6 +478,22 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
457478
}))
458479
}
459480

481+
fn get_fallback_image(
482+
render_path: &syn::Path,
483+
dimension: BindingTextureDimension,
484+
) -> proc_macro2::TokenStream {
485+
quote! {
486+
match #render_path::render_resource::#dimension {
487+
#render_path::render_resource::TextureViewDimension::D1 => &fallback_image.d1,
488+
#render_path::render_resource::TextureViewDimension::D2 => &fallback_image.d2,
489+
#render_path::render_resource::TextureViewDimension::D2Array => &fallback_image.d2_array,
490+
#render_path::render_resource::TextureViewDimension::Cube => &fallback_image.cube,
491+
#render_path::render_resource::TextureViewDimension::CubeArray => &fallback_image.cube_array,
492+
#render_path::render_resource::TextureViewDimension::D3 => &fallback_image.d3,
493+
}
494+
}
495+
}
496+
460497
/// Represents the arguments for the `uniform` binding attribute.
461498
///
462499
/// If parsed, represents an attribute
@@ -637,7 +674,7 @@ fn get_visibility_flag_value(meta: Meta) -> Result<ShaderStageVisibility> {
637674
Ok(ShaderStageVisibility::Flags(visibility))
638675
}
639676

640-
#[derive(Default)]
677+
#[derive(Clone, Copy, Default)]
641678
enum BindingTextureDimension {
642679
D1,
643680
#[default]

crates/bevy_render/src/texture/fallback_image.rs

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use bevy_ecs::{
66
};
77
use bevy_math::Vec2;
88
use bevy_utils::HashMap;
9-
use wgpu::{Extent3d, TextureDimension, TextureFormat};
9+
use wgpu::{Extent3d, TextureFormat};
1010

1111
use crate::{
1212
prelude::Image,
@@ -20,8 +20,21 @@ use crate::{
2020
///
2121
/// Defaults to a 1x1 fully opaque white texture, (1.0, 1.0, 1.0, 1.0) which makes multiplying
2222
/// it with other colors a no-op.
23-
#[derive(Resource, Deref)]
24-
pub struct FallbackImage(GpuImage);
23+
#[derive(Resource)]
24+
pub struct FallbackImage {
25+
/// Fallback image for [`TextureViewDimension::D1`].
26+
pub d1: GpuImage,
27+
/// Fallback image for [`TextureViewDimension::D2`].
28+
pub d2: GpuImage,
29+
/// Fallback image for [`TextureViewDimension::D2Array`].
30+
pub d2_array: GpuImage,
31+
/// Fallback image for [`TextureViewDimension::Cube`].
32+
pub cube: GpuImage,
33+
/// Fallback image for [`TextureViewDimension::CubeArray`].
34+
pub cube_array: GpuImage,
35+
/// Fallback image for [`TextureViewDimension::D3`].
36+
pub d3: GpuImage,
37+
}
2538

2639
/// A [`RenderApp`](crate::RenderApp) resource that contains a _zero-filled_ "fallback image",
2740
/// which can be used in place of [`FallbackImage`], when a fully transparent or black fallback
@@ -54,14 +67,18 @@ fn fallback_image_new(
5467
width: 1,
5568
height: 1,
5669
depth_or_array_layers: match dimension {
57-
TextureViewDimension::Cube => 6,
70+
TextureViewDimension::Cube | TextureViewDimension::CubeArray => 6,
5871
_ => 1,
5972
},
6073
};
6174

62-
let mut image = Image::new_fill(extents, TextureDimension::D2, &data, format);
75+
let image_dimension = dimension.compatible_texture_dimension();
76+
77+
let mut image = Image::new_fill(extents, image_dimension, &data, format);
6378
image.texture_descriptor.sample_count = samples;
64-
image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT;
79+
if image_dimension == TextureDimension::D2 {
80+
image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT;
81+
}
6582

6683
// We can't create textures with data when it's a depth texture or when using multiple samples
6784
let texture = if format.is_depth_stencil_format() || samples > 1 {
@@ -97,15 +114,62 @@ impl FromWorld for FallbackImage {
97114
let render_device = world.resource::<RenderDevice>();
98115
let render_queue = world.resource::<RenderQueue>();
99116
let default_sampler = world.resource::<DefaultImageSampler>();
100-
Self(fallback_image_new(
101-
render_device,
102-
render_queue,
103-
default_sampler,
104-
TextureFormat::bevy_default(),
105-
TextureViewDimension::D2,
106-
1,
107-
255,
108-
))
117+
Self {
118+
d1: fallback_image_new(
119+
render_device,
120+
render_queue,
121+
default_sampler,
122+
TextureFormat::bevy_default(),
123+
TextureViewDimension::D1,
124+
1,
125+
255,
126+
),
127+
d2: fallback_image_new(
128+
render_device,
129+
render_queue,
130+
default_sampler,
131+
TextureFormat::bevy_default(),
132+
TextureViewDimension::D2,
133+
1,
134+
255,
135+
),
136+
d2_array: fallback_image_new(
137+
render_device,
138+
render_queue,
139+
default_sampler,
140+
TextureFormat::bevy_default(),
141+
TextureViewDimension::D2Array,
142+
1,
143+
255,
144+
),
145+
cube: fallback_image_new(
146+
render_device,
147+
render_queue,
148+
default_sampler,
149+
TextureFormat::bevy_default(),
150+
TextureViewDimension::Cube,
151+
1,
152+
255,
153+
),
154+
cube_array: fallback_image_new(
155+
render_device,
156+
render_queue,
157+
default_sampler,
158+
TextureFormat::bevy_default(),
159+
TextureViewDimension::CubeArray,
160+
1,
161+
255,
162+
),
163+
d3: fallback_image_new(
164+
render_device,
165+
render_queue,
166+
default_sampler,
167+
TextureFormat::bevy_default(),
168+
TextureViewDimension::D3,
169+
1,
170+
255,
171+
),
172+
}
109173
}
110174
}
111175

examples/shader/fallback_image.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//! This example tests that all texture dimensions are supported by
2+
//! `FallbackImage`.
3+
//!
4+
//! When running this example, you should expect to see a window that only draws
5+
//! the clear color. The test material does not shade any geometry; this example
6+
//! only tests that the images are initialized and bound so that the app does
7+
//! not panic.
8+
use bevy::{
9+
prelude::*,
10+
reflect::{TypePath, TypeUuid},
11+
render::render_resource::{AsBindGroup, ShaderRef},
12+
};
13+
14+
fn main() {
15+
App::new()
16+
.add_plugins(DefaultPlugins)
17+
.add_plugin(MaterialPlugin::<FallbackTestMaterial>::default())
18+
.add_systems(Startup, setup)
19+
.run();
20+
}
21+
22+
fn setup(
23+
mut commands: Commands,
24+
mut meshes: ResMut<Assets<Mesh>>,
25+
mut materials: ResMut<Assets<FallbackTestMaterial>>,
26+
) {
27+
commands.spawn(MaterialMeshBundle {
28+
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
29+
material: materials.add(FallbackTestMaterial {
30+
image_1d: None,
31+
image_2d: None,
32+
image_2d_array: None,
33+
image_cube: None,
34+
image_cube_array: None,
35+
image_3d: None,
36+
}),
37+
..Default::default()
38+
});
39+
commands.spawn(Camera3dBundle {
40+
transform: Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::new(1.5, 0.0, 0.0), Vec3::Y),
41+
..Default::default()
42+
});
43+
}
44+
45+
#[derive(AsBindGroup, Debug, Clone, TypePath, TypeUuid)]
46+
#[uuid = "d4890167-0e16-4bfc-b812-434717f20409"]
47+
struct FallbackTestMaterial {
48+
#[texture(0, dimension = "1d")]
49+
#[sampler(1)]
50+
image_1d: Option<Handle<Image>>,
51+
52+
#[texture(2, dimension = "2d")]
53+
#[sampler(3)]
54+
image_2d: Option<Handle<Image>>,
55+
56+
#[texture(4, dimension = "2d_array")]
57+
#[sampler(5)]
58+
image_2d_array: Option<Handle<Image>>,
59+
60+
#[texture(6, dimension = "cube")]
61+
#[sampler(7)]
62+
image_cube: Option<Handle<Image>>,
63+
64+
#[texture(8, dimension = "cube_array")]
65+
#[sampler(9)]
66+
image_cube_array: Option<Handle<Image>>,
67+
68+
#[texture(10, dimension = "3d")]
69+
#[sampler(11)]
70+
image_3d: Option<Handle<Image>>,
71+
}
72+
73+
impl Material for FallbackTestMaterial {
74+
fn fragment_shader() -> ShaderRef {
75+
"shaders/fallback_image_test.wgsl".into()
76+
}
77+
}

examples/shader/texture_binding_array.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ impl AsBindGroup for BindlessMaterial {
110110
}
111111
}
112112

113+
let fallback_image = &fallback_image.d2;
114+
113115
let textures = vec![&fallback_image.texture_view; MAX_TEXTURE_COUNT];
114116

115117
// convert bevy's resource types to WGPU's references

0 commit comments

Comments
 (0)