Add ExternalTexture BindingType behind new Feature flag

Adds a new feature flag, `EXTERNAL_TEXTURE`, indicating device support
for our implementation of WebGPU's `GPUExternalTexture` [1] which will
land in upcoming patches. Conceptually this would make more sense as a
downlevel flag, as it is a core part of the WebGPU spec which we do not
yet support. We do not want, however, to cause applications to reject
adapters because we have not finished implementing this, so for now we
are making it an opt-in feature.

As an initial step towards supporting this feature, this patch adds a
new `BindingType` corresponding to WebGPU's
`GPUExternalTextureBindingLayout` [2]. This binding type dictates that
when creating a bind group the corresponding entry must be either an
external texture or a texture view with certain additional requirements
[3].

As of yet wgpu has no concept of an external texture (that will follow
in later patches) but for now this patch ensures that texture views
corresponding to an external texture binding type are validated
correctly. Note that as the feature flag is not yet supported on any
real backends, bind group layout creation will fail before getting the
chance to attempt to create a bind group. But in the added tests using
the noop backend we can see this validation taking place.

[1] https://www.w3.org/TR/webgpu/#gpuexternaltexture
[1] https://www.w3.org/TR/webgpu/#dictdef-gpuexternaltexturebindinglayout
[2] https://gpuweb.github.io/gpuweb/#bind-group-creation
This commit is contained in:
Jamie Nicol 2025-05-16 15:10:12 +01:00 committed by Jim Blandy
parent 111a95b822
commit 8cdbcc1755
14 changed files with 295 additions and 20 deletions

View File

@ -0,0 +1,162 @@
use wgpu::*;
use wgpu_test::{fail, valid};
/// Ensures a [`TextureView`] can be bound to a [`BindingType::ExternalTexture`]
/// resource binding.
#[test]
fn external_texture_binding_texture_view() {
let (device, _queue) = wgpu::Device::noop(&DeviceDescriptor {
required_features: Features::EXTERNAL_TEXTURE,
..Default::default()
});
let bgl = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: None,
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::ExternalTexture,
count: None,
}],
});
let texture_descriptor = TextureDescriptor {
label: None,
size: Extent3d {
width: 256,
height: 256,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING,
view_formats: &[],
};
let texture = device.create_texture(&texture_descriptor);
let view = texture.create_view(&TextureViewDescriptor::default());
valid(&device, || {
device.create_bind_group(&BindGroupDescriptor {
label: None,
layout: &bgl,
entries: &[BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&view),
}],
})
});
// Invalid usages (must include TEXTURE_BINDING)
let texture = device.create_texture(&TextureDescriptor {
usage: TextureUsages::STORAGE_BINDING,
..texture_descriptor
});
let view = texture.create_view(&TextureViewDescriptor::default());
fail(
&device,
|| {
device.create_bind_group(&BindGroupDescriptor {
label: None,
layout: &bgl,
entries: &[BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&view),
}],
})
},
Some("Usage flags TextureUsages(STORAGE_BINDING) of TextureView with '' label do not contain required usage flags TextureUsages(TEXTURE_BINDING"),
);
// Invalid dimension (must be D2)
let texture = device.create_texture(&TextureDescriptor {
dimension: TextureDimension::D3,
..texture_descriptor
});
let view = texture.create_view(&TextureViewDescriptor::default());
fail(
&device,
|| {
device.create_bind_group(&BindGroupDescriptor {
label: None,
layout: &bgl,
entries: &[BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&view),
}],
})
},
Some("Texture binding 0 expects dimension = D2, but given a view with dimension = D3"),
);
// Invalid mip_level_count (must be 1)
let texture = device.create_texture(&TextureDescriptor {
mip_level_count: 2,
..texture_descriptor
});
let view = texture.create_view(&TextureViewDescriptor::default());
fail(
&device,
|| {
device.create_bind_group(&BindGroupDescriptor {
label: None,
layout: &bgl,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&view),
},
],
})
},
Some("External texture bindings must have a single mip level, but given a view with mip_level_count = 2 at binding 0")
);
// Invalid format (must be Rgba8Unorm, Bgra8Unorm, or Rgba16float)
let texture = device.create_texture(&TextureDescriptor {
format: TextureFormat::Rgba8Uint,
..texture_descriptor
});
let view = texture.create_view(&TextureViewDescriptor::default());
fail(
&device,
|| {
device.create_bind_group(&BindGroupDescriptor {
label: None,
layout: &bgl,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&view),
},
],
})
},
Some("External texture bindings must have a format of `rgba8unorm`, `bgra8unorm`, or `rgba16float, but given a view with format = Rgba8Uint at binding 0")
);
// Invalid sample count (must be 1)
let texture = device.create_texture(&TextureDescriptor {
sample_count: 4,
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
..texture_descriptor
});
let view = texture.create_view(&TextureViewDescriptor::default());
fail(
&device,
|| {
device.create_bind_group(&BindGroupDescriptor {
label: None,
layout: &bgl,
entries: &[BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&view),
}],
})
},
Some("Texture binding 0 expects multisampled = false, but given a view with samples = 4"),
);
}

View File

@ -1,5 +1,6 @@
mod binding_arrays;
mod buffer;
mod buffer_slice;
mod external_texture;
mod instance;
mod texture;

View File

@ -173,6 +173,13 @@ pub enum CreateBindGroupError {
},
#[error("Storage texture bindings must have a single mip level, but given a view with mip_level_count = {mip_level_count:?} at binding {binding}")]
InvalidStorageTextureMipLevelCount { binding: u32, mip_level_count: u32 },
#[error("External texture bindings must have a single mip level, but given a view with mip_level_count = {mip_level_count:?} at binding {binding}")]
InvalidExternalTextureMipLevelCount { binding: u32, mip_level_count: u32 },
#[error("External texture bindings must have a format of `rgba8unorm`, `bgra8unorm`, or `rgba16float, but given a view with format = {format:?} at binding {binding}")]
InvalidExternalTextureFormat {
binding: u32,
format: wgt::TextureFormat,
},
#[error("Sampler binding {binding} expects comparison = {layout_cmp}, but given a sampler with comparison = {sampler_cmp}")]
WrongSamplerComparison {
binding: u32,
@ -382,6 +389,19 @@ impl BindingTypeMaxCountValidator {
wgt::BindingType::AccelerationStructure { .. } => {
self.acceleration_structures.add(binding.visibility, count);
}
wgt::BindingType::ExternalTexture => {
// https://www.w3.org/TR/webgpu/#gpuexternaltexture
// In order to account for many possible representations,
// the binding conservatively uses the following, for each
// external texture:
// * Three sampled textures for up to 3 planes
// * One additional sampled texture for a 3D LUT
// * One sampler to sample the LUT
// * One uniform buffer for metadata
self.sampled_textures.add(binding.visibility, count * 4);
self.samplers.add(binding.visibility, count);
self.uniform_buffers.add(binding.visibility, count);
}
}
}
}

View File

@ -2011,6 +2011,14 @@ impl Device {
)
}
Bt::AccelerationStructure { .. } => (None, WritableStorage::No),
Bt::ExternalTexture => {
self.require_features(wgt::Features::EXTERNAL_TEXTURE)
.map_err(|e| binding_model::CreateBindGroupLayoutError::Entry {
binding: entry.binding,
error: e.into(),
})?;
(None, WritableStorage::No)
}
};
// Validate the count parameter
@ -2761,6 +2769,41 @@ impl Device {
view.check_usage(wgt::TextureUsages::STORAGE_BINDING)?;
Ok(internal_use)
}
wgt::BindingType::ExternalTexture => {
if view.desc.dimension != TextureViewDimension::D2 {
return Err(Error::InvalidTextureDimension {
binding,
layout_dimension: TextureViewDimension::D2,
view_dimension: view.desc.dimension,
});
}
let mip_level_count = view.selector.mips.end - view.selector.mips.start;
if mip_level_count != 1 {
return Err(Error::InvalidExternalTextureMipLevelCount {
binding,
mip_level_count,
});
}
if view.desc.format != TextureFormat::Rgba8Unorm
&& view.desc.format != TextureFormat::Bgra8Unorm
&& view.desc.format != TextureFormat::Rgba16Float
{
return Err(Error::InvalidExternalTextureFormat {
binding,
format: view.desc.format,
});
}
if view.samples != 1 {
return Err(Error::InvalidTextureMultisample {
binding,
layout_multisampled: false,
view_samples: view.samples,
});
}
view.check_usage(wgt::TextureUsages::TEXTURE_BINDING)?;
Ok(wgt::TextureUses::RESOURCE)
}
_ => Err(Error::WrongBindingType {
binding,
actual: decl.ty,

View File

@ -36,6 +36,7 @@ pub enum BindingTypeName {
Texture,
Sampler,
AccelerationStructure,
ExternalTexture,
}
impl From<&ResourceType> for BindingTypeName {
@ -57,6 +58,7 @@ impl From<&BindingType> for BindingTypeName {
BindingType::StorageTexture { .. } => BindingTypeName::Texture,
BindingType::Sampler { .. } => BindingTypeName::Sampler,
BindingType::AccelerationStructure { .. } => BindingTypeName::AccelerationStructure,
BindingType::ExternalTexture => BindingTypeName::ExternalTexture,
}
}
}
@ -466,6 +468,7 @@ impl Resource {
let view_dimension = match entry.ty {
BindingType::Texture { view_dimension, .. }
| BindingType::StorageTexture { view_dimension, .. } => view_dimension,
BindingType::ExternalTexture => wgt::TextureViewDimension::D2,
_ => {
return Err(BindingError::WrongTextureViewDimension {
dim,
@ -1147,6 +1150,9 @@ impl Interface {
);
let texture_sample_type = match texture_layout.ty {
BindingType::Texture { sample_type, .. } => sample_type,
BindingType::ExternalTexture => {
wgt::TextureSampleType::Float { filterable: true }
}
_ => unreachable!(),
};

View File

@ -135,6 +135,7 @@ pub fn map_binding_type(ty: &wgt::BindingType) -> Direct3D12::D3D12_DESCRIPTOR_R
}
| Bt::StorageTexture { .. } => Direct3D12::D3D12_DESCRIPTOR_RANGE_TYPE_UAV,
Bt::AccelerationStructure { .. } => Direct3D12::D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
Bt::ExternalTexture => unimplemented!(),
}
}

View File

@ -785,6 +785,7 @@ impl crate::Device for super::Device {
| wgt::BindingType::StorageTexture { .. }
| wgt::BindingType::AccelerationStructure { .. } => num_views += count,
wgt::BindingType::Sampler { .. } => has_sampler_in_group = true,
wgt::BindingType::ExternalTexture => unimplemented!(),
}
}
@ -1550,6 +1551,7 @@ impl crate::Device for super::Device {
inner.stage.push(handle);
}
}
wgt::BindingType::ExternalTexture => unimplemented!(),
}
}

View File

@ -1203,6 +1203,7 @@ impl crate::Device for super::Device {
..
} => &mut num_storage_buffers,
wgt::BindingType::AccelerationStructure { .. } => unimplemented!(),
wgt::BindingType::ExternalTexture => unimplemented!(),
};
binding_to_slot[entry.binding as usize] = *counter;
@ -1313,6 +1314,7 @@ impl crate::Device for super::Device {
})
}
wgt::BindingType::AccelerationStructure { .. } => unimplemented!(),
wgt::BindingType::ExternalTexture => unimplemented!(),
};
contents.push(binding);
}

View File

@ -747,6 +747,7 @@ impl crate::Device for super::Device {
};
}
wgt::BindingType::AccelerationStructure { .. } => unimplemented!(),
wgt::BindingType::ExternalTexture => unimplemented!(),
}
}
@ -979,6 +980,7 @@ impl crate::Device for super::Device {
counter.textures += 1;
}
wgt::BindingType::AccelerationStructure { .. } => unimplemented!(),
wgt::BindingType::ExternalTexture => unimplemented!(),
}
}
}

View File

@ -764,6 +764,7 @@ pub fn map_binding_type(ty: wgt::BindingType) -> vk::DescriptorType {
wgt::BindingType::AccelerationStructure { .. } => {
vk::DescriptorType::ACCELERATION_STRUCTURE_KHR
}
wgt::BindingType::ExternalTexture => unimplemented!(),
}
}

View File

@ -1493,6 +1493,7 @@ impl crate::Device for super::Device {
wgt::BindingType::AccelerationStructure { .. } => {
desc_count.acceleration_structure += count;
}
wgt::BindingType::ExternalTexture => unimplemented!(),
}
}

View File

@ -997,6 +997,21 @@ bitflags_array! {
/// This is a native-only feature.
const EXPERIMENTAL_RAY_TRACING_ACCELERATION_STRUCTURE = 1 << 30;
/// Allows for the creation and usage of `ExternalTexture`s, and bind
/// group layouts containing external texture `BindingType`s.
///
/// Conceptually this should really be a [`crate::DownlevelFlags`] as
/// it corresponds to WebGPU's [`GPUExternalTexture`](
/// https://www.w3.org/TR/webgpu/#gpuexternaltexture).
/// However, the implementation is currently in-progress, and until it
/// is complete we do not want applications to ignore adapters due to
/// a missing downlevel flag, when they may not require this feature at
/// all.
///
/// Supported platforms:
/// - None
const EXTERNAL_TEXTURE = 1 << 31;
// Shader:
/// ***THIS IS EXPERIMENTAL:*** Features enabled by this may have
@ -1009,7 +1024,7 @@ bitflags_array! {
/// - Vulkan
///
/// This is a native-only feature.
const EXPERIMENTAL_RAY_QUERY = 1 << 31;
const EXPERIMENTAL_RAY_QUERY = 1 << 32;
/// Enables 64-bit floating point types in SPIR-V shaders.
///
/// Note: even when supported by GPU hardware, 64-bit floating point operations are
@ -1019,14 +1034,14 @@ bitflags_array! {
/// - Vulkan
///
/// This is a native only feature.
const SHADER_F64 = 1 << 32;
const SHADER_F64 = 1 << 33;
/// Allows shaders to use i16. Not currently supported in `naga`, only available through `spirv-passthrough`.
///
/// Supported platforms:
/// - Vulkan
///
/// This is a native only feature.
const SHADER_I16 = 1 << 33;
const SHADER_I16 = 1 << 34;
/// Enables `builtin(primitive_index)` in fragment shaders.
///
/// Note: enables geometry processing for pipelines using the builtin.
@ -1040,7 +1055,7 @@ bitflags_array! {
/// - OpenGL (some)
///
/// This is a native only feature.
const SHADER_PRIMITIVE_INDEX = 1 << 34;
const SHADER_PRIMITIVE_INDEX = 1 << 35;
/// Allows shaders to use the `early_depth_test` attribute.
///
/// The attribute is applied to the fragment shader entry point. It can be used in two
@ -1068,7 +1083,7 @@ bitflags_array! {
/// This is a native only feature.
///
/// [`EarlyDepthTest`]: https://docs.rs/naga/latest/naga/ir/enum.EarlyDepthTest.html
const SHADER_EARLY_DEPTH_TEST = 1 << 35;
const SHADER_EARLY_DEPTH_TEST = 1 << 36;
/// Allows shaders to use i64 and u64.
///
/// Supported platforms:
@ -1077,7 +1092,7 @@ bitflags_array! {
/// - Metal (with MSL 2.3+)
///
/// This is a native only feature.
const SHADER_INT64 = 1 << 36;
const SHADER_INT64 = 1 << 37;
/// Allows compute and fragment shaders to use the subgroup operation built-ins
///
/// Supported Platforms:
@ -1086,14 +1101,14 @@ bitflags_array! {
/// - Metal
///
/// This is a native only feature.
const SUBGROUP = 1 << 37;
const SUBGROUP = 1 << 38;
/// Allows vertex shaders to use the subgroup operation built-ins
///
/// Supported Platforms:
/// - Vulkan
///
/// This is a native only feature.
const SUBGROUP_VERTEX = 1 << 38;
const SUBGROUP_VERTEX = 1 << 39;
/// Allows shaders to use the subgroup barrier
///
/// Supported Platforms:
@ -1101,7 +1116,7 @@ bitflags_array! {
/// - Metal
///
/// This is a native only feature.
const SUBGROUP_BARRIER = 1 << 39;
const SUBGROUP_BARRIER = 1 << 40;
/// Allows the use of pipeline cache objects
///
/// Supported platforms:
@ -1110,7 +1125,7 @@ bitflags_array! {
/// Unimplemented Platforms:
/// - DX12
/// - Metal
const PIPELINE_CACHE = 1 << 40;
const PIPELINE_CACHE = 1 << 41;
/// Allows shaders to use i64 and u64 atomic min and max.
///
/// Supported platforms:
@ -1119,7 +1134,7 @@ bitflags_array! {
/// - Metal (with MSL 2.4+)
///
/// This is a native only feature.
const SHADER_INT64_ATOMIC_MIN_MAX = 1 << 41;
const SHADER_INT64_ATOMIC_MIN_MAX = 1 << 42;
/// Allows shaders to use all i64 and u64 atomic operations.
///
/// Supported platforms:
@ -1127,7 +1142,7 @@ bitflags_array! {
/// - DX12 (with SM 6.6+)
///
/// This is a native only feature.
const SHADER_INT64_ATOMIC_ALL_OPS = 1 << 42;
const SHADER_INT64_ATOMIC_ALL_OPS = 1 << 43;
/// Allows using the [VK_GOOGLE_display_timing] Vulkan extension.
///
/// This is used for frame pacing to reduce latency, and is generally only available on Android.
@ -1143,7 +1158,7 @@ bitflags_array! {
///
/// [VK_GOOGLE_display_timing]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_GOOGLE_display_timing.html
/// [`Surface::as_hal()`]: https://docs.rs/wgpu/latest/wgpu/struct.Surface.html#method.as_hal
const VULKAN_GOOGLE_DISPLAY_TIMING = 1 << 43;
const VULKAN_GOOGLE_DISPLAY_TIMING = 1 << 44;
/// Allows using the [VK_KHR_external_memory_win32] Vulkan extension.
///
@ -1153,7 +1168,7 @@ bitflags_array! {
/// This is a native only feature.
///
/// [VK_KHR_external_memory_win32]: https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_external_memory_win32.html
const VULKAN_EXTERNAL_MEMORY_WIN32 = 1 << 44;
const VULKAN_EXTERNAL_MEMORY_WIN32 = 1 << 45;
/// Enables R64Uint image atomic min and max.
///
@ -1163,7 +1178,7 @@ bitflags_array! {
/// - Metal (with MSL 3.1+)
///
/// This is a native only feature.
const TEXTURE_INT64_ATOMIC = 1 << 45;
const TEXTURE_INT64_ATOMIC = 1 << 46;
/// Allows uniform buffers to be bound as binding arrays.
///
@ -1180,7 +1195,7 @@ bitflags_array! {
/// - Vulkan 1.2+ (or VK_EXT_descriptor_indexing)'s `shaderUniformBufferArrayNonUniformIndexing` feature)
///
/// This is a native only feature.
const UNIFORM_BUFFER_BINDING_ARRAYS = 1 << 46;
const UNIFORM_BUFFER_BINDING_ARRAYS = 1 << 47;
/// Enables mesh shaders and task shaders in mesh shader pipelines.
///
@ -1192,7 +1207,7 @@ bitflags_array! {
/// - Metal
///
/// This is a native only feature.
const EXPERIMENTAL_MESH_SHADER = 1 << 47;
const EXPERIMENTAL_MESH_SHADER = 1 << 48;
/// ***THIS IS EXPERIMENTAL:*** Features enabled by this may have
/// major bugs in them and are expected to be subject to breaking changes, suggestions
@ -1207,7 +1222,7 @@ bitflags_array! {
/// This is a native only feature
///
/// [`AccelerationStructureFlags::ALLOW_RAY_HIT_VERTEX_RETURN`]: super::AccelerationStructureFlags::ALLOW_RAY_HIT_VERTEX_RETURN
const EXPERIMENTAL_RAY_HIT_VERTEX_RETURN = 1 << 48;
const EXPERIMENTAL_RAY_HIT_VERTEX_RETURN = 1 << 49;
/// Enables multiview in mesh shader pipelines
///
@ -1219,7 +1234,7 @@ bitflags_array! {
/// - Metal
///
/// This is a native only feature.
const EXPERIMENTAL_MESH_SHADER_MULTIVIEW = 1 << 49;
const EXPERIMENTAL_MESH_SHADER_MULTIVIEW = 1 << 50;
/// Allows usage of additional vertex formats in [BlasTriangleGeometrySizeDescriptor::vertex_format]
///
@ -1228,7 +1243,7 @@ bitflags_array! {
/// - DX12
///
/// [BlasTriangleGeometrySizeDescriptor::vertex_format]: super::BlasTriangleGeometrySizeDescriptor
const EXTENDED_ACCELERATION_STRUCTURE_VERTEX_FORMATS = 1 << 50;
const EXTENDED_ACCELERATION_STRUCTURE_VERTEX_FORMATS = 1 << 51;
}
/// Features that are not guaranteed to be supported.

View File

@ -6799,6 +6799,20 @@ pub enum BindingType {
/// If enabled requires [`Features::EXPERIMENTAL_RAY_HIT_VERTEX_RETURN`]
vertex_return: bool,
},
/// An external texture binding.
///
/// Example WGSL syntax:
/// ```rust,ignore
/// @group(0) @binding(0)
/// var t: texture_external;
/// ```
///
/// Corresponds to [WebGPU `GPUExternalTextureBindingLayout`](
/// https://gpuweb.github.io/gpuweb/#dictdef-gpuexternaltexturebindinglayout).
///
/// Requires [`Features::EXTERNAL_TEXTURE`]
ExternalTexture,
}
impl BindingType {

View File

@ -1942,6 +1942,11 @@ impl dispatch::DeviceInterface for WebDevice {
mapped_entry.set_storage_texture(&storage_texture);
}
wgt::BindingType::AccelerationStructure { .. } => todo!(),
wgt::BindingType::ExternalTexture => {
mapped_entry.set_external_texture(
&webgpu_sys::GpuExternalTextureBindingLayout::new(),
);
}
}
mapped_entry