[wgpu-core] Allow creation of bind groups containing external textures

Adds a `BindingResource` variant for external textures. In core's
create_bind_group() implementation, allow binding either external
textures or texture views to `BindingType::ExternalTexture` layout
entries.

In either case, provide HAL with a `hal::ExternalTextureBinding`,
consisting of 3 `hal::TextureBinding`s and a `hal::BufferBinding`. In
the texture view case we use the device's default params buffer for
the buffer. When there are fewer than 3 planes we can simply repeat an
existing plane multiple times - the contents of the params buffer will
ensure the shader only accesses the correct number of planes anyway.

Track the view or external texture in `BindGroupStates` to ensure they
remain alive whilst required.

And finally, add the corresponding API to wgpu, with an implementation
for the wgpu-core backend.
This commit is contained in:
Jamie Nicol 2025-05-27 13:15:03 +01:00 committed by Jim Blandy
parent d263b1875d
commit 7087f0c01f
16 changed files with 274 additions and 36 deletions

View File

@ -21,13 +21,13 @@ use crate::{
device::{
bgl, Device, DeviceError, MissingDownlevelFlags, MissingFeatures, SHADER_STAGE_COUNT,
},
id::{BindGroupLayoutId, BufferId, SamplerId, TextureViewId, TlasId},
id::{BindGroupLayoutId, BufferId, ExternalTextureId, SamplerId, TextureViewId, TlasId},
init_tracker::{BufferInitTrackerAction, TextureInitTrackerAction},
pipeline::{ComputePipeline, RenderPipeline},
resource::{
Buffer, DestroyedResourceError, InvalidResourceError, Labeled, MissingBufferUsageError,
MissingTextureUsageError, RawResourceAccess, ResourceErrorIdent, Sampler, TextureView,
Tlas, TrackingData,
Buffer, DestroyedResourceError, ExternalTexture, InvalidResourceError, Labeled,
MissingBufferUsageError, MissingTextureUsageError, RawResourceAccess, ResourceErrorIdent,
Sampler, TextureView, Tlas, TrackingData,
},
resource_log,
snatch::{SnatchGuard, Snatchable},
@ -594,8 +594,14 @@ impl BindingTypeMaxCountValidator {
/// cbindgen:ignore
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BindGroupEntry<'a, B = BufferId, S = SamplerId, TV = TextureViewId, TLAS = TlasId>
where
pub struct BindGroupEntry<
'a,
B = BufferId,
S = SamplerId,
TV = TextureViewId,
TLAS = TlasId,
ET = ExternalTextureId,
> where
[BufferBinding<B>]: ToOwned,
[S]: ToOwned,
[TV]: ToOwned,
@ -608,15 +614,21 @@ where
pub binding: u32,
#[cfg_attr(
feature = "serde",
serde(bound(deserialize = "BindingResource<'a, B, S, TV, TLAS>: Deserialize<'de>"))
serde(bound(deserialize = "BindingResource<'a, B, S, TV, TLAS, ET>: Deserialize<'de>"))
)]
/// Resource to attach to the binding
pub resource: BindingResource<'a, B, S, TV, TLAS>,
pub resource: BindingResource<'a, B, S, TV, TLAS, ET>,
}
/// cbindgen:ignore
pub type ResolvedBindGroupEntry<'a> =
BindGroupEntry<'a, Arc<Buffer>, Arc<Sampler>, Arc<TextureView>, Arc<Tlas>>;
pub type ResolvedBindGroupEntry<'a> = BindGroupEntry<
'a,
Arc<Buffer>,
Arc<Sampler>,
Arc<TextureView>,
Arc<Tlas>,
Arc<ExternalTexture>,
>;
/// Describes a group of bindings and the resources to be bound.
#[derive(Clone, Debug)]
@ -628,6 +640,7 @@ pub struct BindGroupDescriptor<
S = SamplerId,
TV = TextureViewId,
TLAS = TlasId,
ET = ExternalTextureId,
> where
[BufferBinding<B>]: ToOwned,
[S]: ToOwned,
@ -635,8 +648,8 @@ pub struct BindGroupDescriptor<
<[BufferBinding<B>] as ToOwned>::Owned: fmt::Debug,
<[S] as ToOwned>::Owned: fmt::Debug,
<[TV] as ToOwned>::Owned: fmt::Debug,
[BindGroupEntry<'a, B, S, TV, TLAS>]: ToOwned,
<[BindGroupEntry<'a, B, S, TV, TLAS>] as ToOwned>::Owned: fmt::Debug,
[BindGroupEntry<'a, B, S, TV, TLAS, ET>]: ToOwned,
<[BindGroupEntry<'a, B, S, TV, TLAS, ET>] as ToOwned>::Owned: fmt::Debug,
{
/// Debug label of the bind group.
///
@ -647,11 +660,12 @@ pub struct BindGroupDescriptor<
#[cfg_attr(
feature = "serde",
serde(bound(
deserialize = "<[BindGroupEntry<'a, B, S, TV, TLAS>] as ToOwned>::Owned: Deserialize<'de>"
deserialize = "<[BindGroupEntry<'a, B, S, TV, TLAS, ET>] as ToOwned>::Owned: Deserialize<'de>"
))
)]
/// The resources to bind to this bind group.
pub entries: Cow<'a, [BindGroupEntry<'a, B, S, TV, TLAS>]>,
#[allow(clippy::type_complexity)]
pub entries: Cow<'a, [BindGroupEntry<'a, B, S, TV, TLAS, ET>]>,
}
/// cbindgen:ignore
@ -662,6 +676,7 @@ pub type ResolvedBindGroupDescriptor<'a> = BindGroupDescriptor<
Arc<Sampler>,
Arc<TextureView>,
Arc<Tlas>,
Arc<ExternalTexture>,
>;
/// Describes a [`BindGroupLayout`].
@ -1005,8 +1020,14 @@ pub type ResolvedBufferBinding = BufferBinding<Arc<Buffer>>;
// They're different enough that it doesn't make sense to share a common type
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BindingResource<'a, B = BufferId, S = SamplerId, TV = TextureViewId, TLAS = TlasId>
where
pub enum BindingResource<
'a,
B = BufferId,
S = SamplerId,
TV = TextureViewId,
TLAS = TlasId,
ET = ExternalTextureId,
> where
[BufferBinding<B>]: ToOwned,
[S]: ToOwned,
[TV]: ToOwned,
@ -1033,10 +1054,17 @@ where
)]
TextureViewArray(Cow<'a, [TV]>),
AccelerationStructure(TLAS),
ExternalTexture(ET),
}
pub type ResolvedBindingResource<'a> =
BindingResource<'a, Arc<Buffer>, Arc<Sampler>, Arc<TextureView>, Arc<Tlas>>;
pub type ResolvedBindingResource<'a> = BindingResource<
'a,
Arc<Buffer>,
Arc<Sampler>,
Arc<TextureView>,
Arc<Tlas>,
Arc<ExternalTexture>,
>;
#[derive(Clone, Debug, Error)]
#[non_exhaustive]

View File

@ -846,6 +846,7 @@ impl Global {
sampler_storage: &Storage<Fallible<resource::Sampler>>,
texture_view_storage: &Storage<Fallible<resource::TextureView>>,
tlas_storage: &Storage<Fallible<resource::Tlas>>,
external_texture_storage: &Storage<Fallible<resource::ExternalTexture>>,
) -> Result<ResolvedBindGroupEntry<'a>, binding_model::CreateBindGroupError>
{
let resolve_buffer = |bb: &BufferBinding| {
@ -877,6 +878,12 @@ impl Global {
.get()
.map_err(binding_model::CreateBindGroupError::from)
};
let resolve_external_texture = |id: &id::ExternalTextureId| {
external_texture_storage
.get(*id)
.get()
.map_err(binding_model::CreateBindGroupError::from)
};
let resource = match e.resource {
BindingResource::Buffer(ref buffer) => {
ResolvedBindingResource::Buffer(resolve_buffer(buffer)?)
@ -911,6 +918,9 @@ impl Global {
BindingResource::AccelerationStructure(ref tlas) => {
ResolvedBindingResource::AccelerationStructure(resolve_tlas(tlas)?)
}
BindingResource::ExternalTexture(ref et) => {
ResolvedBindingResource::ExternalTexture(resolve_external_texture(et)?)
}
};
Ok(ResolvedBindGroupEntry {
binding: e.binding,
@ -923,6 +933,7 @@ impl Global {
let texture_view_guard = hub.texture_views.read();
let sampler_guard = hub.samplers.read();
let tlas_guard = hub.tlas_s.read();
let external_texture_guard = hub.external_textures.read();
desc.entries
.iter()
.map(|e| {
@ -932,6 +943,7 @@ impl Global {
&sampler_guard,
&texture_view_guard,
&tlas_guard,
&external_texture_guard,
)
})
.collect::<Result<Vec<_>, _>>()

View File

@ -76,8 +76,8 @@ pub(crate) struct CommandIndices {
pub(crate) next_acceleration_structure_build_command_index: u64,
}
/// Parameters provided to shaders via a uniform buffer, describing an
/// ExternalTexture resource binding.
/// Parameters provided to shaders via a uniform buffer, describing a
/// [`binding_model::BindingResource::ExternalTexture`] resource binding.
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Zeroable, bytemuck::Pod)]
pub struct ExternalTextureParams {
@ -2612,6 +2612,121 @@ impl Device {
Ok(tlas.try_raw(snatch_guard)?)
}
fn create_external_texture_binding<'a>(
&'a self,
binding: u32,
decl: &wgt::BindGroupLayoutEntry,
external_texture: &'a Arc<ExternalTexture>,
used: &mut BindGroupStates,
snatch_guard: &'a SnatchGuard,
) -> Result<
hal::ExternalTextureBinding<'a, dyn hal::DynBuffer, dyn hal::DynTextureView>,
binding_model::CreateBindGroupError,
> {
use crate::binding_model::CreateBindGroupError as Error;
external_texture.same_device(self)?;
used.external_textures
.insert_single(external_texture.clone());
match decl.ty {
wgt::BindingType::ExternalTexture => {}
_ => {
return Err(Error::WrongBindingType {
binding,
actual: decl.ty,
expected: "ExternalTexture",
});
}
}
let planes = (0..3)
.map(|i| {
// We always need 3 bindings. If we have fewer than 3 planes
// just bind plane 0 multiple times. The shader will only
// sample from valid planes anyway.
let plane = external_texture
.planes
.get(i)
.unwrap_or(&external_texture.planes[0]);
let internal_use = wgt::TextureUses::RESOURCE;
used.views.insert_single(plane.clone(), internal_use);
let view = plane.try_raw(snatch_guard)?;
Ok(hal::TextureBinding {
view,
usage: internal_use,
})
})
// We can remove this intermediate Vec by using
// array::try_from_fn() above, once it stabilizes.
.collect::<Result<Vec<_>, Error>>()?;
let planes = planes.try_into().unwrap();
used.buffers
.insert_single(external_texture.params.clone(), wgt::BufferUses::UNIFORM);
let params = hal::BufferBinding {
buffer: external_texture.params.try_raw(snatch_guard)?,
offset: 0,
size: wgt::BufferSize::new(external_texture.params.size),
};
Ok(hal::ExternalTextureBinding { planes, params })
}
fn create_external_texture_binding_from_view<'a>(
&'a self,
binding: u32,
decl: &wgt::BindGroupLayoutEntry,
view: &'a Arc<TextureView>,
used: &mut BindGroupStates,
snatch_guard: &'a SnatchGuard,
) -> Result<
hal::ExternalTextureBinding<'a, dyn hal::DynBuffer, dyn hal::DynTextureView>,
binding_model::CreateBindGroupError,
> {
use crate::binding_model::CreateBindGroupError as Error;
view.same_device(self)?;
let internal_use = self.texture_use_parameters(binding, decl, view, "SampledTexture")?;
used.views.insert_single(view.clone(), internal_use);
match decl.ty {
wgt::BindingType::ExternalTexture => {}
_ => {
return Err(Error::WrongBindingType {
binding,
actual: decl.ty,
expected: "ExternalTexture",
});
}
}
// We need 3 bindings, so just repeat the same texture view 3 times.
let planes = [
hal::TextureBinding {
view: view.try_raw(snatch_guard)?,
usage: internal_use,
},
hal::TextureBinding {
view: view.try_raw(snatch_guard)?,
usage: internal_use,
},
hal::TextureBinding {
view: view.try_raw(snatch_guard)?,
usage: internal_use,
},
];
let params = hal::BufferBinding {
buffer: self.default_external_texture_params_buffer.as_ref(),
offset: 0,
size: None,
};
Ok(hal::ExternalTextureBinding { planes, params })
}
// This function expects the provided bind group layout to be resolved
// (not passing a duplicate) beforehand.
pub(crate) fn create_bind_group(
@ -2652,6 +2767,7 @@ impl Device {
let mut hal_samplers = Vec::new();
let mut hal_textures = Vec::new();
let mut hal_tlas_s = Vec::new();
let mut hal_external_textures = Vec::new();
let snatch_guard = self.snatchable_lock.read();
for entry in desc.entries.iter() {
let binding = entry.binding;
@ -2718,19 +2834,33 @@ impl Device {
(res_index, num_bindings)
}
Br::TextureView(ref view) => {
let tb = self.create_texture_binding(
binding,
decl,
view,
&mut used,
&mut used_texture_ranges,
&snatch_guard,
)?;
let res_index = hal_textures.len();
hal_textures.push(tb);
(res_index, 1)
}
Br::TextureView(ref view) => match decl.ty {
wgt::BindingType::ExternalTexture => {
let et = self.create_external_texture_binding_from_view(
binding,
decl,
view,
&mut used,
&snatch_guard,
)?;
let res_index = hal_external_textures.len();
hal_external_textures.push(et);
(res_index, 1)
}
_ => {
let tb = self.create_texture_binding(
binding,
decl,
view,
&mut used,
&mut used_texture_ranges,
&snatch_guard,
)?;
let res_index = hal_textures.len();
hal_textures.push(tb);
(res_index, 1)
}
},
Br::TextureViewArray(ref views) => {
let num_bindings = views.len();
Self::check_array_binding(self.features, decl.count, num_bindings)?;
@ -2758,6 +2888,18 @@ impl Device {
hal_tlas_s.push(tlas);
(res_index, 1)
}
Br::ExternalTexture(ref et) => {
let et = self.create_external_texture_binding(
binding,
decl,
et,
&mut used,
&snatch_guard,
)?;
let res_index = hal_external_textures.len();
hal_external_textures.push(et);
(res_index, 1)
}
};
hal_entries.push(hal::BindGroupEntry {
@ -2783,6 +2925,7 @@ impl Device {
samplers: &hal_samplers,
textures: &hal_textures,
acceleration_structures: &hal_tlas_s,
external_textures: &hal_external_textures,
};
let raw = unsafe { self.raw().create_bind_group(&hal_desc) }
.map_err(|e| self.handle_hal_error(e))?;

View File

@ -241,6 +241,7 @@ impl Dispatch {
samplers: &[],
textures: &[],
acceleration_structures: &[],
external_textures: &[],
};
let dst_bind_group = unsafe {
device
@ -284,6 +285,7 @@ impl Dispatch {
samplers: &[],
textures: &[],
acceleration_structures: &[],
external_textures: &[],
};
unsafe {
device

View File

@ -140,6 +140,7 @@ impl Draw {
samplers: &[],
textures: &[],
acceleration_structures: &[],
external_textures: &[],
};
unsafe {
device
@ -690,6 +691,7 @@ fn create_buffer_and_bind_group(
samplers: &[],
textures: &[],
acceleration_structures: &[],
external_textures: &[],
};
let bind_group = unsafe { device.create_bind_group(&bind_group_desc) }?;
Ok(BufferPoolEntry { buffer, bind_group })

View File

@ -1801,11 +1801,9 @@ pub type ExternalTextureDescriptor<'a> = wgt::ExternalTextureDescriptor<Label<'a
pub struct ExternalTexture {
pub(crate) device: Arc<Device>,
/// Between 1 and 3 (inclusive) planes of texture data.
#[allow(dead_code)]
pub(crate) planes: arrayvec::ArrayVec<Arc<TextureView>, 3>,
/// Buffer containing a [`crate::device::resource::ExternalTextureParams`]
/// describing the external texture.
#[allow(dead_code)]
pub(crate) params: Arc<Buffer>,
/// The `label` from the descriptor used to create the resource.
pub(crate) label: String,

View File

@ -289,6 +289,7 @@ impl TimestampNormalizer {
samplers: &[],
textures: &[],
acceleration_structures: &[],
external_textures: &[],
entries: &[hal::BindGroupEntry {
binding: 0,
resource_index: 0,

View File

@ -438,6 +438,7 @@ impl<T: ResourceUses> fmt::Display for InvalidUse<T> {
pub(crate) struct BindGroupStates {
pub buffers: BufferBindGroupState,
pub views: TextureViewBindGroupState,
pub external_textures: StatelessTracker<resource::ExternalTexture>,
pub samplers: StatelessTracker<resource::Sampler>,
pub acceleration_structures: StatelessTracker<resource::Tlas>,
}
@ -447,6 +448,7 @@ impl BindGroupStates {
Self {
buffers: BufferBindGroupState::new(),
views: TextureViewBindGroupState::new(),
external_textures: StatelessTracker::new(),
samplers: StatelessTracker::new(),
acceleration_structures: StatelessTracker::new(),
}

View File

@ -464,6 +464,7 @@ impl<A: hal::Api> Example<A> {
samplers: &[&sampler],
textures: &[texture_binding],
acceleration_structures: &[],
external_textures: &[],
entries: &[
hal::BindGroupEntry {
binding: 0,
@ -499,6 +500,7 @@ impl<A: hal::Api> Example<A> {
samplers: &[],
textures: &[],
acceleration_structures: &[],
external_textures: &[],
entries: &[hal::BindGroupEntry {
binding: 0,
resource_index: 0,

View File

@ -622,6 +622,7 @@ impl<A: hal::Api> Example<A> {
samplers: &[],
textures: &[texture_binding],
acceleration_structures: &[&tlas],
external_textures: &[],
entries: &[
hal::BindGroupEntry {
binding: 0,

View File

@ -345,6 +345,11 @@ impl<D: Device + DynResource> DynDevice for D {
.iter()
.map(|a| a.expect_downcast_ref())
.collect();
let external_textures: Vec<_> = desc
.external_textures
.iter()
.map(|et| et.clone().expect_downcast())
.collect();
let desc = BindGroupDescriptor {
label: desc.label.to_owned(),
@ -354,6 +359,7 @@ impl<D: Device + DynResource> DynDevice for D {
textures: &textures,
entries: desc.entries,
acceleration_structures: &acceleration_structures,
external_textures: &external_textures,
};
unsafe { D::create_bind_group(self, &desc) }

View File

@ -23,7 +23,8 @@ use wgt::WasmNotSendSync;
use crate::{
AccelerationStructureAABBs, AccelerationStructureEntries, AccelerationStructureInstances,
AccelerationStructureTriangleIndices, AccelerationStructureTriangleTransform,
AccelerationStructureTriangles, BufferBinding, ProgrammableStage, TextureBinding,
AccelerationStructureTriangles, BufferBinding, ExternalTextureBinding, ProgrammableStage,
TextureBinding,
};
/// Base trait for all resources, allows downcasting via [`Any`].
@ -143,6 +144,16 @@ impl<'a> TextureBinding<'a, dyn DynTextureView> {
}
}
impl<'a> ExternalTextureBinding<'a, dyn DynBuffer, dyn DynTextureView> {
pub fn expect_downcast<B: DynBuffer, T: DynTextureView>(
self,
) -> ExternalTextureBinding<'a, B, T> {
let planes = self.planes.map(|plane| plane.expect_downcast());
let params = self.params.expect_downcast();
ExternalTextureBinding { planes, params }
}
}
impl<'a> ProgrammableStage<'a, dyn DynShaderModule> {
fn expect_downcast<T: DynShaderModule>(self) -> ProgrammableStage<'a, T> {
ProgrammableStage {

View File

@ -2145,6 +2145,23 @@ impl<'a, T: DynTextureView + ?Sized> Clone for TextureBinding<'a, T> {
}
}
#[derive(Debug)]
pub struct ExternalTextureBinding<'a, B: DynBuffer + ?Sized, T: DynTextureView + ?Sized> {
pub planes: [TextureBinding<'a, T>; 3],
pub params: BufferBinding<'a, B>,
}
impl<'a, B: DynBuffer + ?Sized, T: DynTextureView + ?Sized> Clone
for ExternalTextureBinding<'a, B, T>
{
fn clone(&self) -> Self {
ExternalTextureBinding {
planes: self.planes.clone(),
params: self.params.clone(),
}
}
}
/// cbindgen:ignore
#[derive(Clone, Debug)]
pub struct BindGroupEntry {
@ -2178,6 +2195,7 @@ pub struct BindGroupDescriptor<
pub textures: &'a [TextureBinding<'a, T>],
pub entries: &'a [BindGroupEntry],
pub acceleration_structures: &'a [&'a A],
pub external_textures: &'a [ExternalTextureBinding<'a, B, T>],
}
#[derive(Clone, Debug)]

View File

@ -81,6 +81,12 @@ pub enum BindingResource<'a> {
/// built using `build_acceleration_structures` a validation error is generated otherwise this is a part of the
/// safety section of `build_acceleration_structures_unsafe_tlas` and so undefined behavior occurs.
AccelerationStructure(&'a Tlas),
/// Binding is backed by an external texture.
///
/// [`Features::EXTERNAL_TEXTURE`] must be supported to use this feature.
///
/// Corresponds to [`wgt::BindingType::ExternalTexture`].
ExternalTexture(&'a ExternalTexture),
}
#[cfg(send_sync)]
static_assertions::assert_impl_all!(BindingResource<'_>: Send, Sync);

View File

@ -2017,6 +2017,9 @@ impl dispatch::DeviceInterface for WebDevice {
crate::BindingResource::AccelerationStructure(_) => {
unimplemented!("Raytracing not implemented for web")
}
crate::BindingResource::ExternalTexture(_) => {
unimplemented!("ExternalTexture not implemented for web")
}
};
webgpu_sys::GpuBindGroupEntry::new(binding.binding, &mapped_resource)

View File

@ -1190,6 +1190,9 @@ impl dispatch::DeviceInterface for CoreDevice {
acceleration_structure.inner.as_core().id,
)
}
BindingResource::ExternalTexture(external_texture) => {
bm::BindingResource::ExternalTexture(external_texture.inner.as_core().id)
}
},
})
.collect::<Vec<_>>();