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.
This commit is contained in:
Duncan 2023-06-19 15:56:25 -07:00 committed by GitHub
parent 96b9b6c8ad
commit 64405469a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 245 additions and 19 deletions

View file

@ -2052,6 +2052,13 @@ hidden = true
name = "window_resizing"
path = "examples/window/window_resizing.rs"
[[example]]
name = "fallback_image"
path = "examples/shader/fallback_image.rs"
[package.metadata.example.fallback_image]
hidden = true
[package.metadata.example.window_resizing]
name = "Window Resizing"
description = "Demonstrates resizing and responding to resizing a window"

View file

@ -0,0 +1,39 @@
#import bevy_pbr::mesh_view_bindings
#import bevy_pbr::mesh_bindings
@group(1) @binding(0)
var test_texture_1d: texture_1d<f32>;
@group(1) @binding(1)
var test_texture_1d_sampler: sampler;
@group(1) @binding(2)
var test_texture_2d: texture_2d<f32>;
@group(1) @binding(3)
var test_texture_2d_sampler: sampler;
@group(1) @binding(4)
var test_texture_2d_array: texture_2d_array<f32>;
@group(1) @binding(5)
var test_texture_2d_array_sampler: sampler;
@group(1) @binding(6)
var test_texture_cube: texture_cube<f32>;
@group(1) @binding(7)
var test_texture_cube_sampler: sampler;
@group(1) @binding(8)
var test_texture_cube_array: texture_cube_array<f32>;
@group(1) @binding(9)
var test_texture_cube_array_sampler: sampler;
@group(1) @binding(10)
var test_texture_3d: texture_3d<f32>;
@group(1) @binding(11)
var test_texture_3d_sampler: sampler;
struct FragmentInput {
#import bevy_pbr::mesh_vertex_output
};
@fragment
fn fragment(in: FragmentInput) {}

View file

@ -117,6 +117,19 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
// Read field-level attributes
for field in fields.iter() {
// Search ahead for texture attributes so we can use them with any
// corresponding sampler attribute.
let mut tex_attrs = None;
for attr in &field.attrs {
let Some(attr_ident) = attr.path().get_ident() else {
continue;
};
if attr_ident == TEXTURE_ATTRIBUTE_NAME {
let (_binding_index, nested_meta_items) = get_binding_nested_attr(attr)?;
tex_attrs = Some(get_texture_attrs(nested_meta_items)?);
}
}
for attr in &field.attrs {
let Some(attr_ident) = attr.path().get_ident() else {
continue;
@ -255,18 +268,20 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
sample_type,
multisampled,
visibility,
} = get_texture_attrs(nested_meta_items)?;
} = tex_attrs.as_ref().unwrap();
let visibility =
visibility.hygenic_quote(&quote! { #render_path::render_resource });
let fallback_image = get_fallback_image(&render_path, *dimension);
binding_impls.push(quote! {
#render_path::render_resource::OwnedBindingResource::TextureView({
let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into();
if let Some(handle) = handle {
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone()
} else {
fallback_image.texture_view.clone()
#fallback_image.texture_view.clone()
}
})
});
@ -288,18 +303,24 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
let SamplerAttrs {
sampler_binding_type,
visibility,
..
} = get_sampler_attrs(nested_meta_items)?;
let TextureAttrs { dimension, .. } = tex_attrs
.as_ref()
.expect("sampler attribute must have matching texture attribute");
let visibility =
visibility.hygenic_quote(&quote! { #render_path::render_resource });
let fallback_image = get_fallback_image(&render_path, *dimension);
binding_impls.push(quote! {
#render_path::render_resource::OwnedBindingResource::Sampler({
let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into();
if let Some(handle) = handle {
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.sampler.clone()
} else {
fallback_image.sampler.clone()
#fallback_image.sampler.clone()
}
})
});
@ -457,6 +478,22 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
}))
}
fn get_fallback_image(
render_path: &syn::Path,
dimension: BindingTextureDimension,
) -> proc_macro2::TokenStream {
quote! {
match #render_path::render_resource::#dimension {
#render_path::render_resource::TextureViewDimension::D1 => &fallback_image.d1,
#render_path::render_resource::TextureViewDimension::D2 => &fallback_image.d2,
#render_path::render_resource::TextureViewDimension::D2Array => &fallback_image.d2_array,
#render_path::render_resource::TextureViewDimension::Cube => &fallback_image.cube,
#render_path::render_resource::TextureViewDimension::CubeArray => &fallback_image.cube_array,
#render_path::render_resource::TextureViewDimension::D3 => &fallback_image.d3,
}
}
}
/// Represents the arguments for the `uniform` binding attribute.
///
/// If parsed, represents an attribute
@ -637,7 +674,7 @@ fn get_visibility_flag_value(meta: Meta) -> Result<ShaderStageVisibility> {
Ok(ShaderStageVisibility::Flags(visibility))
}
#[derive(Default)]
#[derive(Clone, Copy, Default)]
enum BindingTextureDimension {
D1,
#[default]

View file

@ -6,7 +6,7 @@ use bevy_ecs::{
};
use bevy_math::Vec2;
use bevy_utils::HashMap;
use wgpu::{Extent3d, TextureDimension, TextureFormat};
use wgpu::{Extent3d, TextureFormat};
use crate::{
prelude::Image,
@ -20,8 +20,21 @@ use crate::{
///
/// Defaults to a 1x1 fully opaque white texture, (1.0, 1.0, 1.0, 1.0) which makes multiplying
/// it with other colors a no-op.
#[derive(Resource, Deref)]
pub struct FallbackImage(GpuImage);
#[derive(Resource)]
pub struct FallbackImage {
/// Fallback image for [`TextureViewDimension::D1`].
pub d1: GpuImage,
/// Fallback image for [`TextureViewDimension::D2`].
pub d2: GpuImage,
/// Fallback image for [`TextureViewDimension::D2Array`].
pub d2_array: GpuImage,
/// Fallback image for [`TextureViewDimension::Cube`].
pub cube: GpuImage,
/// Fallback image for [`TextureViewDimension::CubeArray`].
pub cube_array: GpuImage,
/// Fallback image for [`TextureViewDimension::D3`].
pub d3: GpuImage,
}
/// A [`RenderApp`](crate::RenderApp) resource that contains a _zero-filled_ "fallback image",
/// which can be used in place of [`FallbackImage`], when a fully transparent or black fallback
@ -54,14 +67,18 @@ fn fallback_image_new(
width: 1,
height: 1,
depth_or_array_layers: match dimension {
TextureViewDimension::Cube => 6,
TextureViewDimension::Cube | TextureViewDimension::CubeArray => 6,
_ => 1,
},
};
let mut image = Image::new_fill(extents, TextureDimension::D2, &data, format);
let image_dimension = dimension.compatible_texture_dimension();
let mut image = Image::new_fill(extents, image_dimension, &data, format);
image.texture_descriptor.sample_count = samples;
image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT;
if image_dimension == TextureDimension::D2 {
image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT;
}
// We can't create textures with data when it's a depth texture or when using multiple samples
let texture = if format.is_depth_stencil_format() || samples > 1 {
@ -97,15 +114,62 @@ impl FromWorld for FallbackImage {
let render_device = world.resource::<RenderDevice>();
let render_queue = world.resource::<RenderQueue>();
let default_sampler = world.resource::<DefaultImageSampler>();
Self(fallback_image_new(
render_device,
render_queue,
default_sampler,
TextureFormat::bevy_default(),
TextureViewDimension::D2,
1,
255,
))
Self {
d1: fallback_image_new(
render_device,
render_queue,
default_sampler,
TextureFormat::bevy_default(),
TextureViewDimension::D1,
1,
255,
),
d2: fallback_image_new(
render_device,
render_queue,
default_sampler,
TextureFormat::bevy_default(),
TextureViewDimension::D2,
1,
255,
),
d2_array: fallback_image_new(
render_device,
render_queue,
default_sampler,
TextureFormat::bevy_default(),
TextureViewDimension::D2Array,
1,
255,
),
cube: fallback_image_new(
render_device,
render_queue,
default_sampler,
TextureFormat::bevy_default(),
TextureViewDimension::Cube,
1,
255,
),
cube_array: fallback_image_new(
render_device,
render_queue,
default_sampler,
TextureFormat::bevy_default(),
TextureViewDimension::CubeArray,
1,
255,
),
d3: fallback_image_new(
render_device,
render_queue,
default_sampler,
TextureFormat::bevy_default(),
TextureViewDimension::D3,
1,
255,
),
}
}
}

View file

@ -0,0 +1,77 @@
//! This example tests that all texture dimensions are supported by
//! `FallbackImage`.
//!
//! When running this example, you should expect to see a window that only draws
//! the clear color. The test material does not shade any geometry; this example
//! only tests that the images are initialized and bound so that the app does
//! not panic.
use bevy::{
prelude::*,
reflect::{TypePath, TypeUuid},
render::render_resource::{AsBindGroup, ShaderRef},
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(MaterialPlugin::<FallbackTestMaterial>::default())
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<FallbackTestMaterial>>,
) {
commands.spawn(MaterialMeshBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
material: materials.add(FallbackTestMaterial {
image_1d: None,
image_2d: None,
image_2d_array: None,
image_cube: None,
image_cube_array: None,
image_3d: None,
}),
..Default::default()
});
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::new(1.5, 0.0, 0.0), Vec3::Y),
..Default::default()
});
}
#[derive(AsBindGroup, Debug, Clone, TypePath, TypeUuid)]
#[uuid = "d4890167-0e16-4bfc-b812-434717f20409"]
struct FallbackTestMaterial {
#[texture(0, dimension = "1d")]
#[sampler(1)]
image_1d: Option<Handle<Image>>,
#[texture(2, dimension = "2d")]
#[sampler(3)]
image_2d: Option<Handle<Image>>,
#[texture(4, dimension = "2d_array")]
#[sampler(5)]
image_2d_array: Option<Handle<Image>>,
#[texture(6, dimension = "cube")]
#[sampler(7)]
image_cube: Option<Handle<Image>>,
#[texture(8, dimension = "cube_array")]
#[sampler(9)]
image_cube_array: Option<Handle<Image>>,
#[texture(10, dimension = "3d")]
#[sampler(11)]
image_3d: Option<Handle<Image>>,
}
impl Material for FallbackTestMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/fallback_image_test.wgsl".into()
}
}

View file

@ -110,6 +110,8 @@ impl AsBindGroup for BindlessMaterial {
}
}
let fallback_image = &fallback_image.d2;
let textures = vec![&fallback_image.texture_view; MAX_TEXTURE_COUNT];
// convert bevy's resource types to WGPU's references