From 58277832b275e68196531fcc67826102c65a9fff Mon Sep 17 00:00:00 2001 From: Jamie Nicol Date: Wed, 16 Apr 2025 16:54:18 +0100 Subject: [PATCH 1/6] [naga wgsl-in wgsl-out] WGSL support for texture_external texture type Make wgsl-in correctly parse `texture_external` texture declarations, and allow such textures to be used in `textureDimensions()`, `textureSampleBaseClampToEdge()`, and `textureLoad()` function calls. In IR these are represented by the `ImageClass::External` image class, which is a 2D, non-multisampled, non-mipmapped, float-sampled image. Adds a new Capability `TEXTURE_EXTERNAL` and ensure validation rejects shaders containing external textures if this capability flag is not set. This capability is enabled for validation by wgpu devices which support the `TEXTURE_EXTERNAL` feature (currently only when using the noop backend), and by the Naga CLI when validating-only or when outputting WGSL. The WGSL backend can of course emit `ImageClass::External` images directly as `texture_external` textures. Other backends are, for now, unimplemented. Lastly, we add a snapshot test covering all the valid uses of a texture_external texture. These are: - As a global variable declaration - As an argument to the built-in functions `textureDimensions()`, `textureSampleBaseClampToEdge()`, and `textureLoad()` - As an argument to user-defined function declarations and calls. We keep these in their own test so that we can control which targets to run them against (currently WGSL and IR). When external textures are supported by all Naga backends we can, if so inclined, integrate these with existing texture tests. --- naga-cli/src/bin/naga.rs | 4 +- naga/src/back/glsl/features.rs | 3 +- naga/src/back/glsl/mod.rs | 5 + naga/src/back/hlsl/help.rs | 3 + naga/src/back/msl/writer.rs | 2 + naga/src/back/spv/image.rs | 1 + naga/src/back/spv/mod.rs | 1 + naga/src/back/spv/writer.rs | 1 + naga/src/common/wgsl/types.rs | 3 + naga/src/front/glsl/builtins.rs | 2 + naga/src/front/wgsl/lower/mod.rs | 9 +- naga/src/front/wgsl/parse/mod.rs | 6 + naga/src/ir/mod.rs | 2 + naga/src/proc/mod.rs | 2 + naga/src/proc/typifier.rs | 4 + naga/src/valid/expression.rs | 3 +- naga/src/valid/mod.rs | 2 + naga/src/valid/type.rs | 3 + naga/tests/in/wgsl/texture-external.toml | 2 + naga/tests/in/wgsl/texture-external.wgsl | 17 ++ .../out/ir/wgsl-texture-external.compact.ron | 253 ++++++++++++++++++ naga/tests/out/ir/wgsl-texture-external.ron | 253 ++++++++++++++++++ .../tests/out/wgsl/wgsl-texture-external.wgsl | 27 ++ wgpu-core/src/device/mod.rs | 4 + wgpu-core/src/validation.rs | 6 + 25 files changed, 611 insertions(+), 7 deletions(-) create mode 100644 naga/tests/in/wgsl/texture-external.toml create mode 100644 naga/tests/in/wgsl/texture-external.wgsl create mode 100644 naga/tests/out/ir/wgsl-texture-external.compact.ron create mode 100644 naga/tests/out/ir/wgsl-texture-external.ron create mode 100644 naga/tests/out/wgsl/wgsl-texture-external.wgsl diff --git a/naga-cli/src/bin/naga.rs b/naga-cli/src/bin/naga.rs index a1e2b4a0bd1..495c418f6e8 100644 --- a/naga-cli/src/bin/naga.rs +++ b/naga-cli/src/bin/naga.rs @@ -508,8 +508,8 @@ fn run() -> anyhow::Result<()> { use naga::valid::Capabilities as C; let missing = match Path::new(path).extension().and_then(|ex| ex.to_str()) { Some("wgsl") => C::CLIP_DISTANCE | C::CULL_DISTANCE, - Some("metal") => C::CULL_DISTANCE, - _ => C::empty(), + Some("metal") => C::CULL_DISTANCE | C::TEXTURE_EXTERNAL, + _ => C::TEXTURE_EXTERNAL, }; caps & !missing }); diff --git a/naga/src/back/glsl/features.rs b/naga/src/back/glsl/features.rs index 1eb3df65f38..a6dfe4e3100 100644 --- a/naga/src/back/glsl/features.rs +++ b/naga/src/back/glsl/features.rs @@ -421,7 +421,8 @@ impl Writer<'_, W> { _ => {} }, ImageClass::Sampled { multi: false, .. } - | ImageClass::Depth { multi: false } => {} + | ImageClass::Depth { multi: false } + | ImageClass::External => {} } } _ => {} diff --git a/naga/src/back/glsl/mod.rs b/naga/src/back/glsl/mod.rs index ee0daa89c6c..d4ade3830e6 100644 --- a/naga/src/back/glsl/mod.rs +++ b/naga/src/back/glsl/mod.rs @@ -1176,6 +1176,7 @@ impl<'a, W: Write> Writer<'a, W> { Ic::Depth { multi: true } => ("sampler", float, "MS", ""), Ic::Depth { multi: false } => ("sampler", float, "", "Shadow"), Ic::Storage { format, .. } => ("image", format.into(), "", ""), + Ic::External => unimplemented!(), }; let precision = if self.options.version.is_es() { @@ -3302,6 +3303,7 @@ impl<'a, W: Write> Writer<'a, W> { write!(self.out, "imageSize(")?; self.write_expr(image, ctx)?; } + ImageClass::External => unimplemented!(), } write!(self.out, ")")?; if components != 1 || self.options.version.is_es() { @@ -3317,6 +3319,7 @@ impl<'a, W: Write> Writer<'a, W> { let fun_name = match class { ImageClass::Sampled { .. } | ImageClass::Depth { .. } => "textureSize", ImageClass::Storage { .. } => "imageSize", + ImageClass::External => unimplemented!(), }; write!(self.out, "{fun_name}(")?; self.write_expr(image, ctx)?; @@ -3336,6 +3339,7 @@ impl<'a, W: Write> Writer<'a, W> { "textureSamples" } ImageClass::Storage { .. } => "imageSamples", + ImageClass::External => unimplemented!(), }; write!(self.out, "{fun_name}(")?; self.write_expr(image, ctx)?; @@ -4618,6 +4622,7 @@ impl<'a, W: Write> Writer<'a, W> { "WGSL `textureLoad` from depth textures is not supported in GLSL".to_string(), )) } + crate::ImageClass::External => unimplemented!(), }; // openGL es doesn't have 1D images so we need workaround it diff --git a/naga/src/back/hlsl/help.rs b/naga/src/back/hlsl/help.rs index f5c9d4b3b97..bea917547b5 100644 --- a/naga/src/back/hlsl/help.rs +++ b/naga/src/back/hlsl/help.rs @@ -195,6 +195,7 @@ impl super::Writer<'_, W> { let storage_format_str = format.to_hlsl_str(); write!(self.out, "<{storage_format_str}>")? } + crate::ImageClass::External => unimplemented!(), } Ok(()) } @@ -290,6 +291,7 @@ impl super::Writer<'_, W> { crate::ImageClass::Depth { multi: false } => "Depth", crate::ImageClass::Sampled { multi: false, .. } => "", crate::ImageClass::Storage { .. } => "RW", + crate::ImageClass::External => unimplemented!(), }; let arrayed_str = if query.arrayed { "Array" } else { "" }; let query_str = match query.query { @@ -349,6 +351,7 @@ impl super::Writer<'_, W> { let extra_coords = match wiq.class { crate::ImageClass::Storage { .. } => 0, crate::ImageClass::Sampled { .. } | crate::ImageClass::Depth { .. } => 1, + crate::ImageClass::External => unimplemented!(), }; // GetDimensions Overloaded Methods diff --git a/naga/src/back/msl/writer.rs b/naga/src/back/msl/writer.rs index 3cb1ab2c39b..1ec87c9be6a 100644 --- a/naga/src/back/msl/writer.rs +++ b/naga/src/back/msl/writer.rs @@ -321,6 +321,7 @@ impl Display for TypeContext<'_> { }; ("texture", "", format.into(), access) } + crate::ImageClass::External => unimplemented!(), }; let base_name = scalar.to_msl_name(); let array_str = if arrayed { "_array" } else { "" }; @@ -6637,6 +6638,7 @@ template "read-write textures".to_string(), )); } + crate::ImageClass::External => unimplemented!(), }, _ => { return Err(Error::UnsupportedArrayOfType(base)); diff --git a/naga/src/back/spv/image.rs b/naga/src/back/spv/image.rs index f485014530e..6c3313992c9 100644 --- a/naga/src/back/spv/image.rs +++ b/naga/src/back/spv/image.rs @@ -118,6 +118,7 @@ impl Load { crate::ImageClass::Depth { .. } | crate::ImageClass::Sampled { .. } => { spirv::Op::ImageFetch } + crate::ImageClass::External => unimplemented!(), }; // `OpImageRead` and `OpImageFetch` instructions produce vec4 diff --git a/naga/src/back/spv/mod.rs b/naga/src/back/spv/mod.rs index 986bee57d31..5bddb87cf79 100644 --- a/naga/src/back/spv/mod.rs +++ b/naga/src/back/spv/mod.rs @@ -275,6 +275,7 @@ impl LocalImageType { flags: make_flags(false, ImageTypeFlags::empty()), image_format: format.into(), }, + crate::ImageClass::External => unimplemented!(), } } } diff --git a/naga/src/back/spv/writer.rs b/naga/src/back/spv/writer.rs index 9f4422345b5..09b1deb8b41 100644 --- a/naga/src/back/spv/writer.rs +++ b/naga/src/back/spv/writer.rs @@ -1249,6 +1249,7 @@ impl Writer { self.request_image_format_capabilities(format.into())?; false } + crate::ImageClass::External => unimplemented!(), }; match dim { diff --git a/naga/src/common/wgsl/types.rs b/naga/src/common/wgsl/types.rs index c118feeace7..7588b9aafba 100644 --- a/naga/src/common/wgsl/types.rs +++ b/naga/src/common/wgsl/types.rs @@ -250,6 +250,9 @@ where "texture_storage_{dim_str}{arrayed_str}<{format_str}{access_str}>" )?; } + Ic::External => { + write!(out, "texture_external")?; + } } } TypeInner::Scalar(scalar) => { diff --git a/naga/src/front/glsl/builtins.rs b/naga/src/front/glsl/builtins.rs index 6dcddda44e7..3d7588d27ba 100644 --- a/naga/src/front/glsl/builtins.rs +++ b/naga/src/front/glsl/builtins.rs @@ -2138,6 +2138,7 @@ impl Frontend { ImageClass::Depth { .. } => (true, false), ImageClass::Storage { .. } => (false, true), ImageClass::Sampled { .. } => (false, false), + ImageClass::External => unreachable!(), }; let coordinate = match (image_size, coord_size) { @@ -2259,6 +2260,7 @@ pub fn sampled_to_depth( kind: ErrorKind::SemanticError("Not a texture".into()), meta, }), + ImageClass::External => unreachable!(), }, _ => errors.push(Error { kind: ErrorKind::SemanticError("Not a texture".into()), diff --git a/naga/src/front/wgsl/lower/mod.rs b/naga/src/front/wgsl/lower/mod.rs index 1f994d753f0..3f4fe4a12cb 100644 --- a/naga/src/front/wgsl/lower/mod.rs +++ b/naga/src/front/wgsl/lower/mod.rs @@ -3587,9 +3587,12 @@ impl<'source, 'temp> Lowerer<'source, 'temp> { self.expression_with_leaf_scalar(args.next()?, ir::Scalar::F32, ctx)? } - // Sampling `Storage` textures isn't allowed at all. Let the - // validator report the error. - ir::ImageClass::Storage { .. } => self.expression(args.next()?, ctx)?, + // Sampling `External` textures with a specified level isn't + // allowed, and sampling `Storage` textures isn't allowed at + // all. Let the validator report the error. + ir::ImageClass::Storage { .. } | ir::ImageClass::External => { + self.expression(args.next()?, ctx)? + } }; level = ir::SampleLevel::Exact(exact); depth_ref = None; diff --git a/naga/src/front/wgsl/parse/mod.rs b/naga/src/front/wgsl/parse/mod.rs index 892930b4301..cf4dd4d4bb6 100644 --- a/naga/src/front/wgsl/parse/mod.rs +++ b/naga/src/front/wgsl/parse/mod.rs @@ -676,6 +676,7 @@ impl Parser { | "texture_depth_cube" | "texture_depth_cube_array" | "texture_depth_multisampled_2d" + | "texture_external" | "texture_storage_1d" | "texture_storage_1d_array" | "texture_storage_2d" @@ -1867,6 +1868,11 @@ impl Parser { arrayed: false, class: crate::ImageClass::Depth { multi: true }, }, + "texture_external" => ast::Type::Image { + dim: crate::ImageDimension::D2, + arrayed: false, + class: crate::ImageClass::External, + }, "texture_storage_1d" => { let (format, access) = lexer.next_format_generic()?; ast::Type::Image { diff --git a/naga/src/ir/mod.rs b/naga/src/ir/mod.rs index 943dbc2458b..35080bf3292 100644 --- a/naga/src/ir/mod.rs +++ b/naga/src/ir/mod.rs @@ -640,6 +640,8 @@ pub enum ImageClass { /// Multi-sampled depth image. multi: bool, }, + /// External texture. + External, /// Storage image. Storage { format: StorageFormat, diff --git a/naga/src/proc/mod.rs b/naga/src/proc/mod.rs index 0843e709b5d..f2584c64b3d 100644 --- a/naga/src/proc/mod.rs +++ b/naga/src/proc/mod.rs @@ -383,6 +383,7 @@ impl super::ImageClass { match self { crate::ImageClass::Sampled { multi, .. } | crate::ImageClass::Depth { multi } => multi, crate::ImageClass::Storage { .. } => false, + crate::ImageClass::External => false, } } @@ -390,6 +391,7 @@ impl super::ImageClass { match self { crate::ImageClass::Sampled { multi, .. } | crate::ImageClass::Depth { multi } => !multi, crate::ImageClass::Storage { .. } => false, + crate::ImageClass::External => false, } } diff --git a/naga/src/proc/typifier.rs b/naga/src/proc/typifier.rs index f7d1ea48390..8f48a69c041 100644 --- a/naga/src/proc/typifier.rs +++ b/naga/src/proc/typifier.rs @@ -512,6 +512,10 @@ impl<'a> ResolveContext<'a> { scalar: format.into(), size: crate::VectorSize::Quad, }, + crate::ImageClass::External => Ti::Vector { + scalar: crate::Scalar::F32, + size: crate::VectorSize::Quad, + }, }), ref other => { log::error!("Image type {:?}", other); diff --git a/naga/src/valid/expression.rs b/naga/src/valid/expression.rs index 8b73486b1fe..d2d67eb4923 100644 --- a/naga/src/valid/expression.rs +++ b/naga/src/valid/expression.rs @@ -460,6 +460,7 @@ impl super::Validator { kind: crate::ScalarKind::Uint | crate::ScalarKind::Sint, multi: false, } if gather.is_some() => false, + crate::ImageClass::External => false, crate::ImageClass::Depth { multi: false } => true, _ => return Err(ExpressionError::InvalidImageClass(class)), }; @@ -551,7 +552,7 @@ impl super::Validator { crate::ImageClass::Sampled { kind: crate::ScalarKind::Float, multi: false - } + } | crate::ImageClass::External ) { return Err(ExpressionError::InvalidSampleClampCoordinateToEdge( alloc::format!("image class `{class:?}`"), diff --git a/naga/src/valid/mod.rs b/naga/src/valid/mod.rs index aef6a241646..2ec44a54ec9 100644 --- a/naga/src/valid/mod.rs +++ b/naga/src/valid/mod.rs @@ -165,6 +165,8 @@ bitflags::bitflags! { const RAY_HIT_VERTEX_POSITION = 1 << 25; /// Support for 16-bit floating-point types. const SHADER_FLOAT16 = 1 << 26; + /// Support for [`ImageClass::External`] + const TEXTURE_EXTERNAL = 1 << 27; } } diff --git a/naga/src/valid/type.rs b/naga/src/valid/type.rs index b3ae13b7d4a..43323a08f37 100644 --- a/naga/src/valid/type.rs +++ b/naga/src/valid/type.rs @@ -732,6 +732,9 @@ impl super::Validator { if arrayed && matches!(dim, crate::ImageDimension::Cube) { self.require_type_capability(Capabilities::CUBE_ARRAY_TEXTURES)?; } + if matches!(class, crate::ImageClass::External) { + self.require_type_capability(Capabilities::TEXTURE_EXTERNAL)?; + } TypeInfo::new( TypeFlags::ARGUMENT | TypeFlags::CREATION_RESOLVED, Alignment::ONE, diff --git a/naga/tests/in/wgsl/texture-external.toml b/naga/tests/in/wgsl/texture-external.toml new file mode 100644 index 00000000000..f8f46d4223e --- /dev/null +++ b/naga/tests/in/wgsl/texture-external.toml @@ -0,0 +1,2 @@ +god_mode = true +targets = "IR | WGSL" diff --git a/naga/tests/in/wgsl/texture-external.wgsl b/naga/tests/in/wgsl/texture-external.wgsl new file mode 100644 index 00000000000..6295980a447 --- /dev/null +++ b/naga/tests/in/wgsl/texture-external.wgsl @@ -0,0 +1,17 @@ +@group(0) @binding(0) +var tex: texture_external; +@group(0) @binding(1) +var samp: sampler; + +fn test(t: texture_external) -> vec4 { + var a = textureSampleBaseClampToEdge(t, samp, vec2(0.0f)); + var b = textureLoad(t, vec2(0u)); + var c = textureDimensions(t); + + return a + b + vec2f(c).xyxy; +} + +@fragment +fn main() -> @location(0) vec4 { + return test(tex); +} diff --git a/naga/tests/out/ir/wgsl-texture-external.compact.ron b/naga/tests/out/ir/wgsl-texture-external.compact.ron new file mode 100644 index 00000000000..432298419d4 --- /dev/null +++ b/naga/tests/out/ir/wgsl-texture-external.compact.ron @@ -0,0 +1,253 @@ +( + types: [ + ( + name: None, + inner: Image( + dim: D2, + arrayed: false, + class: External, + ), + ), + ( + name: None, + inner: Sampler( + comparison: false, + ), + ), + ( + name: None, + inner: Vector( + size: Quad, + scalar: ( + kind: Float, + width: 4, + ), + ), + ), + ( + name: None, + inner: Vector( + size: Bi, + scalar: ( + kind: Uint, + width: 4, + ), + ), + ), + ], + special_types: ( + ray_desc: None, + ray_intersection: None, + ray_vertex_return: None, + predeclared_types: {}, + ), + constants: [], + overrides: [], + global_variables: [ + ( + name: Some("tex"), + space: Handle, + binding: Some(( + group: 0, + binding: 0, + )), + ty: 0, + init: None, + ), + ( + name: Some("samp"), + space: Handle, + binding: Some(( + group: 0, + binding: 1, + )), + ty: 1, + init: None, + ), + ], + global_expressions: [], + functions: [ + ( + name: Some("test"), + arguments: [ + ( + name: Some("t"), + ty: 0, + binding: None, + ), + ], + result: Some(( + ty: 2, + binding: None, + )), + local_variables: [ + ( + name: Some("a"), + ty: 2, + init: None, + ), + ( + name: Some("b"), + ty: 2, + init: None, + ), + ( + name: Some("c"), + ty: 3, + init: None, + ), + ], + expressions: [ + FunctionArgument(0), + GlobalVariable(1), + Literal(F32(0.0)), + Splat( + size: Bi, + value: 2, + ), + ImageSample( + image: 0, + sampler: 1, + gather: None, + coordinate: 3, + array_index: None, + offset: None, + level: Zero, + depth_ref: None, + clamp_to_edge: true, + ), + LocalVariable(0), + Literal(U32(0)), + Splat( + size: Bi, + value: 6, + ), + ImageLoad( + image: 0, + coordinate: 7, + array_index: None, + sample: None, + level: None, + ), + LocalVariable(1), + ImageQuery( + image: 0, + query: Size( + level: None, + ), + ), + LocalVariable(2), + Load( + pointer: 5, + ), + Load( + pointer: 9, + ), + Binary( + op: Add, + left: 12, + right: 13, + ), + Load( + pointer: 11, + ), + As( + expr: 15, + kind: Float, + convert: Some(4), + ), + Swizzle( + size: Quad, + vector: 16, + pattern: (X, Y, X, Y), + ), + Binary( + op: Add, + left: 14, + right: 17, + ), + ], + named_expressions: { + 0: "t", + }, + body: [ + Emit(( + start: 3, + end: 5, + )), + Store( + pointer: 5, + value: 4, + ), + Emit(( + start: 7, + end: 9, + )), + Store( + pointer: 9, + value: 8, + ), + Emit(( + start: 10, + end: 11, + )), + Store( + pointer: 11, + value: 10, + ), + Emit(( + start: 12, + end: 19, + )), + Return( + value: Some(18), + ), + ], + diagnostic_filter_leaf: None, + ), + ], + entry_points: [ + ( + name: "main", + stage: Fragment, + early_depth_test: None, + workgroup_size: (0, 0, 0), + workgroup_size_overrides: None, + function: ( + name: Some("main"), + arguments: [], + result: Some(( + ty: 2, + binding: Some(Location( + location: 0, + interpolation: Some(Perspective), + sampling: Some(Center), + blend_src: None, + )), + )), + local_variables: [], + expressions: [ + GlobalVariable(0), + CallResult(0), + ], + named_expressions: {}, + body: [ + Call( + function: 0, + arguments: [ + 0, + ], + result: Some(1), + ), + Return( + value: Some(1), + ), + ], + diagnostic_filter_leaf: None, + ), + ), + ], + diagnostic_filters: [], + diagnostic_filter_leaf: None, + doc_comments: None, +) \ No newline at end of file diff --git a/naga/tests/out/ir/wgsl-texture-external.ron b/naga/tests/out/ir/wgsl-texture-external.ron new file mode 100644 index 00000000000..432298419d4 --- /dev/null +++ b/naga/tests/out/ir/wgsl-texture-external.ron @@ -0,0 +1,253 @@ +( + types: [ + ( + name: None, + inner: Image( + dim: D2, + arrayed: false, + class: External, + ), + ), + ( + name: None, + inner: Sampler( + comparison: false, + ), + ), + ( + name: None, + inner: Vector( + size: Quad, + scalar: ( + kind: Float, + width: 4, + ), + ), + ), + ( + name: None, + inner: Vector( + size: Bi, + scalar: ( + kind: Uint, + width: 4, + ), + ), + ), + ], + special_types: ( + ray_desc: None, + ray_intersection: None, + ray_vertex_return: None, + predeclared_types: {}, + ), + constants: [], + overrides: [], + global_variables: [ + ( + name: Some("tex"), + space: Handle, + binding: Some(( + group: 0, + binding: 0, + )), + ty: 0, + init: None, + ), + ( + name: Some("samp"), + space: Handle, + binding: Some(( + group: 0, + binding: 1, + )), + ty: 1, + init: None, + ), + ], + global_expressions: [], + functions: [ + ( + name: Some("test"), + arguments: [ + ( + name: Some("t"), + ty: 0, + binding: None, + ), + ], + result: Some(( + ty: 2, + binding: None, + )), + local_variables: [ + ( + name: Some("a"), + ty: 2, + init: None, + ), + ( + name: Some("b"), + ty: 2, + init: None, + ), + ( + name: Some("c"), + ty: 3, + init: None, + ), + ], + expressions: [ + FunctionArgument(0), + GlobalVariable(1), + Literal(F32(0.0)), + Splat( + size: Bi, + value: 2, + ), + ImageSample( + image: 0, + sampler: 1, + gather: None, + coordinate: 3, + array_index: None, + offset: None, + level: Zero, + depth_ref: None, + clamp_to_edge: true, + ), + LocalVariable(0), + Literal(U32(0)), + Splat( + size: Bi, + value: 6, + ), + ImageLoad( + image: 0, + coordinate: 7, + array_index: None, + sample: None, + level: None, + ), + LocalVariable(1), + ImageQuery( + image: 0, + query: Size( + level: None, + ), + ), + LocalVariable(2), + Load( + pointer: 5, + ), + Load( + pointer: 9, + ), + Binary( + op: Add, + left: 12, + right: 13, + ), + Load( + pointer: 11, + ), + As( + expr: 15, + kind: Float, + convert: Some(4), + ), + Swizzle( + size: Quad, + vector: 16, + pattern: (X, Y, X, Y), + ), + Binary( + op: Add, + left: 14, + right: 17, + ), + ], + named_expressions: { + 0: "t", + }, + body: [ + Emit(( + start: 3, + end: 5, + )), + Store( + pointer: 5, + value: 4, + ), + Emit(( + start: 7, + end: 9, + )), + Store( + pointer: 9, + value: 8, + ), + Emit(( + start: 10, + end: 11, + )), + Store( + pointer: 11, + value: 10, + ), + Emit(( + start: 12, + end: 19, + )), + Return( + value: Some(18), + ), + ], + diagnostic_filter_leaf: None, + ), + ], + entry_points: [ + ( + name: "main", + stage: Fragment, + early_depth_test: None, + workgroup_size: (0, 0, 0), + workgroup_size_overrides: None, + function: ( + name: Some("main"), + arguments: [], + result: Some(( + ty: 2, + binding: Some(Location( + location: 0, + interpolation: Some(Perspective), + sampling: Some(Center), + blend_src: None, + )), + )), + local_variables: [], + expressions: [ + GlobalVariable(0), + CallResult(0), + ], + named_expressions: {}, + body: [ + Call( + function: 0, + arguments: [ + 0, + ], + result: Some(1), + ), + Return( + value: Some(1), + ), + ], + diagnostic_filter_leaf: None, + ), + ), + ], + diagnostic_filters: [], + diagnostic_filter_leaf: None, + doc_comments: None, +) \ No newline at end of file diff --git a/naga/tests/out/wgsl/wgsl-texture-external.wgsl b/naga/tests/out/wgsl/wgsl-texture-external.wgsl new file mode 100644 index 00000000000..e0e62f5d736 --- /dev/null +++ b/naga/tests/out/wgsl/wgsl-texture-external.wgsl @@ -0,0 +1,27 @@ +@group(0) @binding(0) +var tex: texture_external; +@group(0) @binding(1) +var samp: sampler; + +fn test(t: texture_external) -> vec4 { + var a: vec4; + var b: vec4; + var c: vec2; + + let _e4 = textureSampleBaseClampToEdge(t, samp, vec2(0f)); + a = _e4; + let _e8 = textureLoad(t, vec2(0u)); + b = _e8; + let _e10 = textureDimensions(t); + c = _e10; + let _e12 = a; + let _e13 = b; + let _e15 = c; + return ((_e12 + _e13) + vec2(_e15).xyxy); +} + +@fragment +fn main() -> @location(0) vec4 { + let _e1 = test(tex); + return _e1; +} diff --git a/wgpu-core/src/device/mod.rs b/wgpu-core/src/device/mod.rs index 42ebffdaaa9..16a2ecb0b6f 100644 --- a/wgpu-core/src/device/mod.rs +++ b/wgpu-core/src/device/mod.rs @@ -489,6 +489,10 @@ pub fn create_validator( Caps::RAY_HIT_VERTEX_POSITION, features.intersects(wgt::Features::EXPERIMENTAL_RAY_HIT_VERTEX_RETURN), ); + caps.set( + Caps::TEXTURE_EXTERNAL, + features.intersects(wgt::Features::EXTERNAL_TEXTURE), + ); naga::valid::Validator::new(flags, caps) } diff --git a/wgpu-core/src/validation.rs b/wgpu-core/src/validation.rs index a7e1d4f1d00..0bc2e892920 100644 --- a/wgpu-core/src/validation.rs +++ b/wgpu-core/src/validation.rs @@ -43,6 +43,10 @@ impl From<&ResourceType> for BindingTypeName { fn from(ty: &ResourceType) -> BindingTypeName { match ty { ResourceType::Buffer { .. } => BindingTypeName::Buffer, + ResourceType::Texture { + class: naga::ImageClass::External, + .. + } => BindingTypeName::ExternalTexture, ResourceType::Texture { .. } => BindingTypeName::Texture, ResourceType::Sampler { .. } => BindingTypeName::Sampler, ResourceType::AccelerationStructure { .. } => BindingTypeName::AccelerationStructure, @@ -548,6 +552,7 @@ impl Resource { access: naga_access, } } + BindingType::ExternalTexture => naga::ImageClass::External, _ => { return Err(BindingError::WrongType { binding: (&entry.ty).into(), @@ -655,6 +660,7 @@ impl Resource { f }, }, + naga::ImageClass::External => BindingType::ExternalTexture, } } ResourceType::AccelerationStructure { vertex_return } => { From 94b759a4a0ac55ea548bd984c524b45aa1d0752e Mon Sep 17 00:00:00 2001 From: Jamie Nicol Date: Tue, 27 May 2025 13:15:03 +0100 Subject: [PATCH 2/6] [wgpu-core] Add ExternalTexture type to wgpu-core `ExternalTexture` will form the basis of wgpu's implementation of WebGPU's `GPUExternalTexture`. [1] The application will be responsible for creating `Texture`(s) and `TextureView`(s) from the external texture source and managing their lifecycle. It may have a single RGBA texture, or it may have multiple textures for separate Y and Cb/Cr planes. It can then create an external texture by calling `create_external_texture()`, providing the texture views and a descriptor. The descriptor provides the following required information: * Whether the texture data is RGBA, or multiplanar or interleaved YCbCr. * The purpoted size of the external texture, which may not match the actual size of the underlying textures. * A matrix for converting from YCbCr to RGBA, if required. * A transform to apply to texture sample coordinates, allowing for rotation and crop rects. The external texture stores a reference to the provided texture views, and additionally owns a `Buffer`. This buffer holds data of the type `ExternalTextureParams`, and will be provided as a uniform buffer to shaders containing external textures. This contains information that will be required by the shaders to handle external textures correctly. Note that attempting to create an external texture will fail unless the `Feature::EXTERNAL_TEXTURE` feature is enabled, which as of yet is not supported by any HAL backends. Additionally add the relevant API to wgpu, implemented for the wgpu-core backend. The web and custom backends are unimplemented. [1] https://www.w3.org/TR/webgpu/#gpuexternaltexture --- .../standalone/custom_backend/src/custom.rs | 8 ++ player/src/lib.rs | 10 ++ wgpu-core/Cargo.toml | 13 +- wgpu-core/src/device/global.rs | 69 +++++++++ wgpu-core/src/device/resource.rs | 134 +++++++++++++++++- wgpu-core/src/device/trace.rs | 6 + wgpu-core/src/hub.rs | 7 +- wgpu-core/src/id.rs | 1 + wgpu-core/src/resource.rs | 69 +++++++++ wgpu-core/src/track/mod.rs | 2 + wgpu-types/src/lib.rs | 67 +++++++++ wgpu/src/api/device.rs | 14 ++ wgpu/src/api/external_texture.rs | 24 ++++ wgpu/src/api/mod.rs | 2 + wgpu/src/backend/custom.rs | 1 + wgpu/src/backend/webgpu.rs | 23 +++ wgpu/src/backend/wgpu_core.rs | 45 ++++++ wgpu/src/dispatch.rs | 7 + wgpu/src/lib.rs | 28 ++-- 19 files changed, 503 insertions(+), 27 deletions(-) create mode 100644 wgpu/src/api/external_texture.rs diff --git a/examples/standalone/custom_backend/src/custom.rs b/examples/standalone/custom_backend/src/custom.rs index ec57413ece8..8ca7df80bdb 100644 --- a/examples/standalone/custom_backend/src/custom.rs +++ b/examples/standalone/custom_backend/src/custom.rs @@ -184,6 +184,14 @@ impl DeviceInterface for CustomDevice { unimplemented!() } + fn create_external_texture( + &self, + _desc: &wgpu::ExternalTextureDescriptor<'_>, + _planes: &[&wgpu::TextureView], + ) -> wgpu::custom::DispatchExternalTexture { + unimplemented!() + } + fn create_blas( &self, _desc: &wgpu::CreateBlasDescriptor<'_>, diff --git a/player/src/lib.rs b/player/src/lib.rs index 16e31947149..ae885bfffa9 100644 --- a/player/src/lib.rs +++ b/player/src/lib.rs @@ -237,6 +237,16 @@ impl GlobalPlay for wgc::global::Global { Action::DestroyTextureView(id) => { self.texture_view_drop(id).unwrap(); } + Action::CreateExternalTexture { id, desc, planes } => { + let (_, error) = + self.device_create_external_texture(device, &desc, &planes, Some(id)); + if let Some(e) = error { + panic!("{e}"); + } + } + Action::DestroyExternalTexture(id) => { + self.external_texture_drop(id); + } Action::CreateSampler(id, desc) => { let (_, error) = self.device_create_sampler(device, &desc, Some(id)); if let Some(e) = error { diff --git a/wgpu-core/Cargo.toml b/wgpu-core/Cargo.toml index ee131dc9f82..d4772efa2df 100644 --- a/wgpu-core/Cargo.toml +++ b/wgpu-core/Cargo.toml @@ -72,14 +72,7 @@ observe_locks = ["std", "dep:ron", "serde/serde_derive"] serde = ["dep:serde", "wgpu-types/serde", "arrayvec/serde", "hashbrown/serde"] ## Enable API tracing. -trace = [ - "serde", - "std", - "dep:ron", - "naga/serialize", - "wgpu-types/trace", - "dep:bytemuck", -] +trace = ["serde", "std", "dep:ron", "naga/serialize", "wgpu-types/trace"] ## Enable API replaying replay = ["serde", "naga/deserialize"] @@ -100,7 +93,7 @@ wgsl = ["naga/wgsl-in"] glsl = ["naga/glsl-in"] ## Enable `ShaderModuleSource::SpirV` -spirv = ["naga/spv-in", "dep:bytemuck"] +spirv = ["naga/spv-in"] #! ### Other # -------------------------------------------------------------------- @@ -180,7 +173,7 @@ arrayvec.workspace = true bit-vec.workspace = true bit-set.workspace = true bitflags.workspace = true -bytemuck = { workspace = true, optional = true } +bytemuck.workspace = true document-features.workspace = true hashbrown.workspace = true indexmap.workspace = true diff --git a/wgpu-core/src/device/global.rs b/wgpu-core/src/device/global.rs index 5e6e4fc463e..834c5a63358 100644 --- a/wgpu-core/src/device/global.rs +++ b/wgpu-core/src/device/global.rs @@ -510,6 +510,75 @@ impl Global { Ok(()) } + pub fn device_create_external_texture( + &self, + device_id: DeviceId, + desc: &resource::ExternalTextureDescriptor, + planes: &[id::TextureViewId], + id_in: Option, + ) -> ( + id::ExternalTextureId, + Option, + ) { + profiling::scope!("Device::create_external_texture"); + + let hub = &self.hub; + + let fid = hub.external_textures.prepare(id_in); + + let error = 'error: { + let device = self.hub.devices.get(device_id); + + #[cfg(feature = "trace")] + if let Some(ref mut trace) = *device.trace.lock() { + let planes = Box::from(planes); + trace.add(trace::Action::CreateExternalTexture { + id: fid.id(), + desc: desc.clone(), + planes, + }); + } + + let planes = planes + .iter() + .map(|plane_id| self.hub.texture_views.get(*plane_id).get()) + .collect::, _>>(); + let planes = match planes { + Ok(planes) => planes, + Err(error) => break 'error error.into(), + }; + + let external_texture = match device.create_external_texture(desc, &planes) { + Ok(external_texture) => external_texture, + Err(error) => break 'error error, + }; + + let id = fid.assign(Fallible::Valid(external_texture)); + api_log!("Device::create_external_texture({desc:?}) -> {id:?}"); + + return (id, None); + }; + + let id = fid.assign(Fallible::Invalid(Arc::new(desc.label.to_string()))); + (id, Some(error)) + } + + pub fn external_texture_drop(&self, external_texture_id: id::ExternalTextureId) { + profiling::scope!("ExternalTexture::drop"); + api_log!("ExternalTexture::drop {external_texture_id:?}"); + + let hub = &self.hub; + + let _external_texture = hub.external_textures.remove(external_texture_id); + + #[cfg(feature = "trace")] + if let Ok(external_texture) = _external_texture.get() { + if let Some(t) = external_texture.device.trace.lock().as_mut() { + t.add(trace::Action::DestroyExternalTexture(external_texture_id)); + } + } + } + pub fn device_create_sampler( &self, device_id: DeviceId, diff --git a/wgpu-core/src/device/resource.rs b/wgpu-core/src/device/resource.rs index 53e9586da64..727b1aae10f 100644 --- a/wgpu-core/src/device/resource.rs +++ b/wgpu-core/src/device/resource.rs @@ -40,8 +40,9 @@ use crate::{ pipeline, pool::ResourcePool, resource::{ - self, AccelerationStructure, Buffer, Fallible, Labeled, ParentDevice, QuerySet, Sampler, - StagingBuffer, Texture, TextureView, TextureViewNotRenderableReason, Tlas, TrackingData, + self, AccelerationStructure, Buffer, ExternalTexture, Fallible, Labeled, ParentDevice, + QuerySet, Sampler, StagingBuffer, Texture, TextureView, TextureViewNotRenderableReason, + Tlas, TrackingData, }, resource_log, snatch::{SnatchGuard, SnatchLock, Snatchable}, @@ -74,6 +75,40 @@ 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. +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Zeroable, bytemuck::Pod)] +pub struct ExternalTextureParams { + /// 4x4 column-major matrix with which to convert sampled YCbCr values + /// to RGBA. + /// This is ignored when `num_planes` is 1. + pub yuv_conversion_matrix: [f32; 16], + /// 3x2 column-major matrix with which to multiply texture coordinates + /// prior to sampling from the external texture. + pub sample_transform: [f32; 6], + /// Size of the external texture. This value should be returned by size + /// queries in shader code. Note that this may not match the dimensions of + /// the underlying texture(s). A value of [0, 0] indicates that the actual + /// size of plane 0 should be used. + pub size: [u32; 2], + /// Number of planes. 1 indicates a single RGBA plane. 2 indicates a Y + /// plane and an interleaved CbCr plane. 3 indicates separate Y, Cb, and Cr + /// planes. + pub num_planes: u32, +} + +impl ExternalTextureParams { + pub fn from_desc(desc: &wgt::ExternalTextureDescriptor) -> Self { + Self { + yuv_conversion_matrix: desc.yuv_conversion_matrix, + size: [desc.width, desc.height], + sample_transform: desc.sample_transform, + num_planes: desc.num_planes() as u32, + } + } +} + /// Structure describing a logical device. Some members are internally mutable, /// stored behind mutexes. pub struct Device { @@ -1543,6 +1578,101 @@ impl Device { Ok(view) } + pub(crate) fn create_external_texture( + self: &Arc, + desc: &resource::ExternalTextureDescriptor, + planes: &[Arc], + ) -> Result, resource::CreateExternalTextureError> { + use resource::CreateExternalTextureError; + self.require_features(wgt::Features::EXTERNAL_TEXTURE)?; + self.check_is_valid()?; + + if desc.num_planes() != planes.len() { + return Err(CreateExternalTextureError::IncorrectPlaneCount { + format: desc.format, + expected: desc.num_planes(), + provided: planes.len(), + }); + } + + let planes = planes + .iter() + .enumerate() + .map(|(i, plane)| { + if plane.samples != 1 { + return Err(CreateExternalTextureError::InvalidPlaneMultisample( + plane.samples, + )); + } + + let sample_type = plane + .desc + .format + .sample_type(Some(plane.desc.range.aspect), Some(self.features)) + .unwrap(); + if !matches!(sample_type, TextureSampleType::Float { filterable: true }) { + return Err(CreateExternalTextureError::InvalidPlaneSampleType { + format: plane.desc.format, + sample_type, + }); + } + + if plane.desc.dimension != TextureViewDimension::D2 { + return Err(CreateExternalTextureError::InvalidPlaneDimension( + plane.desc.dimension, + )); + } + + let expected_components = match desc.format { + wgt::ExternalTextureFormat::Rgba => 4, + wgt::ExternalTextureFormat::Nv12 => match i { + 0 => 1, + 1 => 2, + _ => unreachable!(), + }, + wgt::ExternalTextureFormat::Yu12 => 1, + }; + if plane.desc.format.components() != expected_components { + return Err(CreateExternalTextureError::InvalidPlaneFormat { + format: desc.format, + plane: i, + expected: expected_components, + provided: plane.desc.format, + }); + } + + plane.check_usage(wgt::TextureUsages::TEXTURE_BINDING)?; + Ok(plane.clone()) + }) + .collect::>()?; + + let params_data = ExternalTextureParams::from_desc(desc); + let label = desc.label.as_ref().map(|l| alloc::format!("{l} params")); + let params_desc = resource::BufferDescriptor { + label: label.map(Cow::Owned), + size: size_of_val(¶ms_data) as wgt::BufferAddress, + usage: wgt::BufferUsages::UNIFORM | wgt::BufferUsages::COPY_DST, + mapped_at_creation: false, + }; + let params = self.create_buffer(¶ms_desc)?; + self.get_queue().unwrap().write_buffer( + Fallible::Valid(params.clone()), + 0, + bytemuck::bytes_of(¶ms_data), + )?; + + let external_texture = ExternalTexture { + device: self.clone(), + planes, + params, + label: desc.label.to_string(), + tracking_data: TrackingData::new(self.tracker_indices.external_textures.clone()), + }; + let external_texture = Arc::new(external_texture); + + Ok(external_texture) + } + pub(crate) fn create_sampler( self: &Arc, desc: &resource::SamplerDescriptor, diff --git a/wgpu-core/src/device/trace.rs b/wgpu-core/src/device/trace.rs index 9291ba9f4c6..668158a08a5 100644 --- a/wgpu-core/src/device/trace.rs +++ b/wgpu-core/src/device/trace.rs @@ -58,6 +58,12 @@ pub enum Action<'a> { desc: crate::resource::TextureViewDescriptor<'a>, }, DestroyTextureView(id::TextureViewId), + CreateExternalTexture { + id: id::ExternalTextureId, + desc: crate::resource::ExternalTextureDescriptor<'a>, + planes: alloc::boxed::Box<[id::TextureViewId]>, + }, + DestroyExternalTexture(id::ExternalTextureId), CreateSampler(id::SamplerId, crate::resource::SamplerDescriptor<'a>), DestroySampler(id::SamplerId), GetSurfaceTexture { diff --git a/wgpu-core/src/hub.rs b/wgpu-core/src/hub.rs index f52ac006a84..8dbad492d95 100644 --- a/wgpu-core/src/hub.rs +++ b/wgpu-core/src/hub.rs @@ -111,7 +111,8 @@ use crate::{ pipeline::{ComputePipeline, PipelineCache, RenderPipeline, ShaderModule}, registry::{Registry, RegistryReport}, resource::{ - Blas, Buffer, Fallible, QuerySet, Sampler, StagingBuffer, Texture, TextureView, Tlas, + Blas, Buffer, ExternalTexture, Fallible, QuerySet, Sampler, StagingBuffer, Texture, + TextureView, Tlas, }, }; @@ -133,6 +134,7 @@ pub struct HubReport { pub buffers: RegistryReport, pub textures: RegistryReport, pub texture_views: RegistryReport, + pub external_textures: RegistryReport, pub samplers: RegistryReport, } @@ -181,6 +183,7 @@ pub struct Hub { pub(crate) staging_buffers: Registry, pub(crate) textures: Registry>, pub(crate) texture_views: Registry>, + pub(crate) external_textures: Registry>, pub(crate) samplers: Registry>, pub(crate) blas_s: Registry>, pub(crate) tlas_s: Registry>, @@ -206,6 +209,7 @@ impl Hub { staging_buffers: Registry::new(), textures: Registry::new(), texture_views: Registry::new(), + external_textures: Registry::new(), samplers: Registry::new(), blas_s: Registry::new(), tlas_s: Registry::new(), @@ -230,6 +234,7 @@ impl Hub { buffers: self.buffers.generate_report(), textures: self.textures.generate_report(), texture_views: self.texture_views.generate_report(), + external_textures: self.external_textures.generate_report(), samplers: self.samplers.generate_report(), } } diff --git a/wgpu-core/src/id.rs b/wgpu-core/src/id.rs index 8858e1f8966..965f4d2a3c2 100644 --- a/wgpu-core/src/id.rs +++ b/wgpu-core/src/id.rs @@ -245,6 +245,7 @@ ids! { pub type StagingBufferId StagingBuffer; pub type TextureViewId TextureView; pub type TextureId Texture; + pub type ExternalTextureId ExternalTexture; pub type SamplerId Sampler; pub type BindGroupLayoutId BindGroupLayout; pub type PipelineLayoutId PipelineLayout; diff --git a/wgpu-core/src/resource.rs b/wgpu-core/src/resource.rs index 46be1c1e689..3aa4f886be6 100644 --- a/wgpu-core/src/resource.rs +++ b/wgpu-core/src/resource.rs @@ -1805,6 +1805,75 @@ crate::impl_parent_device!(TextureView); crate::impl_storage_item!(TextureView); crate::impl_trackable!(TextureView); +pub type ExternalTextureDescriptor<'a> = wgt::ExternalTextureDescriptor>; + +#[derive(Debug)] +pub struct ExternalTexture { + pub(crate) device: Arc, + /// Between 1 and 3 (inclusive) planes of texture data. + #[allow(dead_code)] + pub(crate) planes: arrayvec::ArrayVec, 3>, + /// Buffer containing a [`crate::device::resource::ExternalTextureParams`] + /// describing the external texture. + #[allow(dead_code)] + pub(crate) params: Arc, + /// The `label` from the descriptor used to create the resource. + pub(crate) label: String, + pub(crate) tracking_data: TrackingData, +} + +#[derive(Clone, Debug, Error)] +#[non_exhaustive] +pub enum CreateExternalTextureError { + #[error(transparent)] + Device(#[from] DeviceError), + #[error(transparent)] + MissingFeatures(#[from] MissingFeatures), + #[error(transparent)] + InvalidResource(#[from] InvalidResourceError), + #[error(transparent)] + CreateBuffer(#[from] CreateBufferError), + #[error(transparent)] + QueueWrite(#[from] queue::QueueWriteError), + #[error("External texture format {format:?} expects {expected} planes, but given {provided}")] + IncorrectPlaneCount { + format: wgt::ExternalTextureFormat, + expected: usize, + provided: usize, + }, + #[error("External texture planes cannot be multisampled, but given view with samples = {0}")] + InvalidPlaneMultisample(u32), + #[error("External texture planes expect a filterable float sample type, but given view with format {format:?} (sample type {sample_type:?})")] + InvalidPlaneSampleType { + format: wgt::TextureFormat, + sample_type: wgt::TextureSampleType, + }, + #[error("External texture planes expect 2D dimension, but given view with dimension = {0:?}")] + InvalidPlaneDimension(wgt::TextureViewDimension), + #[error(transparent)] + MissingTextureUsage(#[from] MissingTextureUsageError), + #[error("External texture format {format:?} plane {plane} expects format with {expected} components but given view with format {provided:?} ({} components)", + provided.components())] + InvalidPlaneFormat { + format: wgt::ExternalTextureFormat, + plane: usize, + expected: u8, + provided: wgt::TextureFormat, + }, +} + +impl Drop for ExternalTexture { + fn drop(&mut self) { + resource_log!("Destroy raw {}", self.error_ident()); + } +} + +crate::impl_resource_type!(ExternalTexture); +crate::impl_labeled!(ExternalTexture); +crate::impl_parent_device!(ExternalTexture); +crate::impl_storage_item!(ExternalTexture); +crate::impl_trackable!(ExternalTexture); + /// Describes a [`Sampler`] #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/wgpu-core/src/track/mod.rs b/wgpu-core/src/track/mod.rs index a878c5f39b6..6f74db8ae84 100644 --- a/wgpu-core/src/track/mod.rs +++ b/wgpu-core/src/track/mod.rs @@ -222,6 +222,7 @@ pub(crate) struct TrackerIndexAllocators { pub buffers: Arc, pub textures: Arc, pub texture_views: Arc, + pub external_textures: Arc, pub samplers: Arc, pub bind_groups: Arc, pub compute_pipelines: Arc, @@ -238,6 +239,7 @@ impl TrackerIndexAllocators { buffers: Arc::new(SharedTrackerIndexAllocator::new()), textures: Arc::new(SharedTrackerIndexAllocator::new()), texture_views: Arc::new(SharedTrackerIndexAllocator::new()), + external_textures: Arc::new(SharedTrackerIndexAllocator::new()), samplers: Arc::new(SharedTrackerIndexAllocator::new()), bind_groups: Arc::new(SharedTrackerIndexAllocator::new()), compute_pipelines: Arc::new(SharedTrackerIndexAllocator::new()), diff --git a/wgpu-types/src/lib.rs b/wgpu-types/src/lib.rs index 3ba3bbe2683..9bf754a778a 100644 --- a/wgpu-types/src/lib.rs +++ b/wgpu-types/src/lib.rs @@ -6131,6 +6131,73 @@ impl TextureDescriptor { } } +/// Format of an `ExternalTexture`. This indicates the number of underlying +/// planes used by the `ExternalTexture` as well as each plane's format. +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ExternalTextureFormat { + /// Single [`TextureFormat::Rgba8Unorm`] or [`TextureFormat::Bgra8Unorm`] format plane. + Rgba, + /// [`TextureFormat::R8Unorm`] Y plane, and [`TextureFormat::Rg8Unorm`] + /// interleaved CbCr plane. + Nv12, + /// Separate [`TextureFormat::R8Unorm`] Y, Cb, and Cr planes. + Yu12, +} + +/// Describes an [`ExternalTexture`](../wgpu/struct.ExternalTexture.html). +/// +/// Corresponds to [WebGPU `GPUExternalTextureDescriptor`]( +/// https://gpuweb.github.io/gpuweb/#dictdef-gpuexternaltexturedescriptor). +#[repr(C)] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ExternalTextureDescriptor { + /// Debug label of the external texture. This will show up in graphics + /// debuggers for easy identification. + pub label: L, + /// Width of the external texture. Note that both this and `height` may + /// not match the dimensions of the underlying texture(s). This could be + /// due to a crop rect or rotation. + pub width: u32, + /// Height of the external texture. + pub height: u32, + /// Format of the external texture. + pub format: ExternalTextureFormat, + /// 4x4 column-major matrix with which to convert sampled YCbCr values + /// to RGBA. + /// This is ignored when `format` is [`ExternalTextureFormat::Rgba`]. + pub yuv_conversion_matrix: [f32; 16], + /// 3x2 column-major matrix with which to multiply texture coordinates + /// prior to sampling from the external texture. + pub sample_transform: [f32; 6], +} + +impl ExternalTextureDescriptor { + /// Takes a closure and maps the label of the external texture descriptor into another. + #[must_use] + pub fn map_label(&self, fun: impl FnOnce(&L) -> K) -> ExternalTextureDescriptor { + ExternalTextureDescriptor { + label: fun(&self.label), + width: self.width, + height: self.height, + format: self.format, + yuv_conversion_matrix: self.yuv_conversion_matrix, + sample_transform: self.sample_transform, + } + } + + /// The number of underlying planes used by the external texture. + pub fn num_planes(&self) -> usize { + match self.format { + ExternalTextureFormat::Rgba => 1, + ExternalTextureFormat::Nv12 => 2, + ExternalTextureFormat::Yu12 => 3, + } + } +} + /// Describes a `Sampler`. /// /// For use with `Device::create_sampler`. diff --git a/wgpu/src/api/device.rs b/wgpu/src/api/device.rs index 99ed5071df9..68d25e09586 100644 --- a/wgpu/src/api/device.rs +++ b/wgpu/src/api/device.rs @@ -315,6 +315,20 @@ impl Device { } } + /// Creates a new [`ExternalTexture`]. + #[must_use] + pub fn create_external_texture( + &self, + desc: &ExternalTextureDescriptor<'_>, + planes: &[&TextureView], + ) -> ExternalTexture { + let external_texture = self.inner.create_external_texture(desc, planes); + + ExternalTexture { + inner: external_texture, + } + } + /// Creates a [`Buffer`] from a wgpu-hal Buffer. /// /// # Safety diff --git a/wgpu/src/api/external_texture.rs b/wgpu/src/api/external_texture.rs new file mode 100644 index 00000000000..faba59d11c9 --- /dev/null +++ b/wgpu/src/api/external_texture.rs @@ -0,0 +1,24 @@ +use crate::*; + +/// Handle to an external texture on the GPU. +/// +/// It can be created with [`Device::create_external_texture`]. +/// +/// Corresponds to [WebGPU `GPUExternalTexture`](https://gpuweb.github.io/gpuweb/#gpuexternaltexture). +#[derive(Debug, Clone)] +pub struct ExternalTexture { + pub(crate) inner: dispatch::DispatchExternalTexture, +} +#[cfg(send_sync)] +static_assertions::assert_impl_all!(ExternalTexture: Send, Sync); + +crate::cmp::impl_eq_ord_hash_proxy!(ExternalTexture => .inner); + +/// Describes an [`ExternalTexture`]. +/// +/// For use with [`Device::create_external_texture`]. +/// +/// Corresponds to [WebGPU `GPUExternalTextureDescriptor`]( +/// https://gpuweb.github.io/gpuweb/#dictdef-gpuexternaltexturedescriptor). +pub type ExternalTextureDescriptor<'a> = wgt::ExternalTextureDescriptor>; +static_assertions::assert_impl_all!(ExternalTextureDescriptor<'_>: Send, Sync); diff --git a/wgpu/src/api/mod.rs b/wgpu/src/api/mod.rs index c4804d8b61d..8178d2be576 100644 --- a/wgpu/src/api/mod.rs +++ b/wgpu/src/api/mod.rs @@ -32,6 +32,7 @@ mod common_pipeline; mod compute_pass; mod compute_pipeline; mod device; +mod external_texture; mod instance; mod pipeline_cache; mod pipeline_layout; @@ -60,6 +61,7 @@ pub use common_pipeline::*; pub use compute_pass::*; pub use compute_pipeline::*; pub use device::*; +pub use external_texture::*; pub use instance::*; pub use pipeline_cache::*; pub use pipeline_layout::*; diff --git a/wgpu/src/backend/custom.rs b/wgpu/src/backend/custom.rs index f82e1150b28..767281b5c6a 100644 --- a/wgpu/src/backend/custom.rs +++ b/wgpu/src/backend/custom.rs @@ -79,6 +79,7 @@ dyn_type!(pub ref struct DynTextureView(dyn TextureViewInterface)); dyn_type!(pub ref struct DynSampler(dyn SamplerInterface)); dyn_type!(pub ref struct DynBuffer(dyn BufferInterface)); dyn_type!(pub ref struct DynTexture(dyn TextureInterface)); +dyn_type!(pub ref struct DynExternalTexture(dyn ExternalTextureInterface)); dyn_type!(pub ref struct DynBlas(dyn BlasInterface)); dyn_type!(pub ref struct DynTlas(dyn TlasInterface)); dyn_type!(pub ref struct DynQuerySet(dyn QuerySetInterface)); diff --git a/wgpu/src/backend/webgpu.rs b/wgpu/src/backend/webgpu.rs index 12662e7c010..8d8dae95e1c 100644 --- a/wgpu/src/backend/webgpu.rs +++ b/wgpu/src/backend/webgpu.rs @@ -1259,6 +1259,12 @@ pub struct WebTexture { ident: crate::cmp::Identifier, } +#[derive(Debug)] +pub struct WebExternalTexture { + /// Unique identifier for this ExternalTexture. + ident: crate::cmp::Identifier, +} + #[derive(Debug)] pub struct WebBlas { /// Unique identifier for this Blas. @@ -1392,6 +1398,7 @@ impl_send_sync!(WebTextureView); impl_send_sync!(WebSampler); impl_send_sync!(WebBuffer); impl_send_sync!(WebTexture); +impl_send_sync!(WebExternalTexture); impl_send_sync!(WebBlas); impl_send_sync!(WebTlas); impl_send_sync!(WebQuerySet); @@ -1421,6 +1428,7 @@ crate::cmp::impl_eq_ord_hash_proxy!(WebTextureView => .ident); crate::cmp::impl_eq_ord_hash_proxy!(WebSampler => .ident); crate::cmp::impl_eq_ord_hash_proxy!(WebBuffer => .ident); crate::cmp::impl_eq_ord_hash_proxy!(WebTexture => .ident); +crate::cmp::impl_eq_ord_hash_proxy!(WebExternalTexture => .ident); crate::cmp::impl_eq_ord_hash_proxy!(WebBlas => .ident); crate::cmp::impl_eq_ord_hash_proxy!(WebTlas => .ident); crate::cmp::impl_eq_ord_hash_proxy!(WebQuerySet => .ident); @@ -2243,6 +2251,14 @@ impl dispatch::DeviceInterface for WebDevice { .into() } + fn create_external_texture( + &self, + _desc: &crate::ExternalTextureDescriptor<'_>, + _planes: &[&crate::TextureView], + ) -> dispatch::DispatchExternalTexture { + unimplemented!("ExternalTexture not implemented for web"); + } + fn create_blas( &self, _desc: &crate::CreateBlasDescriptor<'_>, @@ -2746,6 +2762,13 @@ impl Drop for WebTexture { } } +impl dispatch::ExternalTextureInterface for WebExternalTexture {} +impl Drop for WebExternalTexture { + fn drop(&mut self) { + unimplemented!("ExternalTexture not implemented for web"); + } +} + impl dispatch::BlasInterface for WebBlas { fn prepare_compact_async(&self, _callback: BlasCompactCallback) { unimplemented!("Raytracing not implemented for web") diff --git a/wgpu/src/backend/wgpu_core.rs b/wgpu/src/backend/wgpu_core.rs index b44db4dba1f..dd36bba182f 100644 --- a/wgpu/src/backend/wgpu_core.rs +++ b/wgpu/src/backend/wgpu_core.rs @@ -556,6 +556,12 @@ pub struct CoreTextureView { id: wgc::id::TextureViewId, } +#[derive(Debug)] +pub struct CoreExternalTexture { + pub(crate) context: ContextWgpuCore, + id: wgc::id::ExternalTextureId, +} + #[derive(Debug)] pub struct CoreSampler { pub(crate) context: ContextWgpuCore, @@ -789,6 +795,7 @@ crate::cmp::impl_eq_ord_hash_proxy!(CoreTextureView => .id); crate::cmp::impl_eq_ord_hash_proxy!(CoreSampler => .id); crate::cmp::impl_eq_ord_hash_proxy!(CoreBuffer => .id); crate::cmp::impl_eq_ord_hash_proxy!(CoreTexture => .id); +crate::cmp::impl_eq_ord_hash_proxy!(CoreExternalTexture => .id); crate::cmp::impl_eq_ord_hash_proxy!(CoreBlas => .id); crate::cmp::impl_eq_ord_hash_proxy!(CoreTlas => .id); crate::cmp::impl_eq_ord_hash_proxy!(CoreQuerySet => .id); @@ -1533,6 +1540,36 @@ impl dispatch::DeviceInterface for CoreDevice { .into() } + fn create_external_texture( + &self, + desc: &crate::ExternalTextureDescriptor<'_>, + planes: &[&crate::TextureView], + ) -> dispatch::DispatchExternalTexture { + let wgt_desc = desc.map_label(|l| l.map(Borrowed)); + let planes = planes + .iter() + .map(|plane| plane.inner.as_core().id) + .collect::>(); + let (id, error) = self + .context + .0 + .device_create_external_texture(self.id, &wgt_desc, &planes, None); + if let Some(cause) = error { + self.context.handle_error( + &self.error_sink, + cause, + desc.label, + "Device::create_external_texture", + ); + } + + CoreExternalTexture { + context: self.context.clone(), + id, + } + .into() + } + fn create_blas( &self, desc: &crate::CreateBlasDescriptor<'_>, @@ -2000,6 +2037,14 @@ impl Drop for CoreTextureView { } } +impl dispatch::ExternalTextureInterface for CoreExternalTexture {} + +impl Drop for CoreExternalTexture { + fn drop(&mut self) { + self.context.0.external_texture_drop(self.id); + } +} + impl dispatch::SamplerInterface for CoreSampler {} impl Drop for CoreSampler { diff --git a/wgpu/src/dispatch.rs b/wgpu/src/dispatch.rs index 558e48a1126..b21efe938a9 100644 --- a/wgpu/src/dispatch.rs +++ b/wgpu/src/dispatch.rs @@ -157,6 +157,11 @@ pub trait DeviceInterface: CommonTraits { ) -> DispatchPipelineCache; fn create_buffer(&self, desc: &crate::BufferDescriptor<'_>) -> DispatchBuffer; fn create_texture(&self, desc: &crate::TextureDescriptor<'_>) -> DispatchTexture; + fn create_external_texture( + &self, + desc: &crate::ExternalTextureDescriptor<'_>, + planes: &[&crate::TextureView], + ) -> DispatchExternalTexture; fn create_blas( &self, desc: &crate::CreateBlasDescriptor<'_>, @@ -257,6 +262,7 @@ pub trait TextureInterface: CommonTraits { fn destroy(&self); } +pub trait ExternalTextureInterface: CommonTraits {} pub trait BlasInterface: CommonTraits { fn prepare_compact_async(&self, callback: BlasCompactCallback); fn ready_for_compaction(&self) -> bool; @@ -840,6 +846,7 @@ dispatch_types! {ref type DispatchTextureView: TextureViewInterface = CoreTextur dispatch_types! {ref type DispatchSampler: SamplerInterface = CoreSampler, WebSampler, DynSampler} dispatch_types! {ref type DispatchBuffer: BufferInterface = CoreBuffer, WebBuffer, DynBuffer} dispatch_types! {ref type DispatchTexture: TextureInterface = CoreTexture, WebTexture, DynTexture} +dispatch_types! {ref type DispatchExternalTexture: ExternalTextureInterface = CoreExternalTexture, WebExternalTexture, DynExternalTexture} dispatch_types! {ref type DispatchBlas: BlasInterface = CoreBlas, WebBlas, DynBlas} dispatch_types! {ref type DispatchTlas: TlasInterface = CoreTlas, WebTlas, DynTlas} dispatch_types! {ref type DispatchQuerySet: QuerySetInterface = CoreQuerySet, WebQuerySet, DynQuerySet} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index c72eba64832..9ba11ba1242 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -76,20 +76,20 @@ pub use wgt::{ CommandBufferDescriptor, CompareFunction, CompositeAlphaMode, CopyExternalImageDestInfo, CoreCounters, DepthBiasState, DepthStencilState, DeviceLostReason, DeviceType, DownlevelCapabilities, DownlevelFlags, DownlevelLimits, Dx12BackendOptions, Dx12Compiler, - DxcShaderModel, DynamicOffset, Extent3d, Face, Features, FeaturesWGPU, FeaturesWebGPU, - FilterMode, FrontFace, GlBackendOptions, GlFenceBehavior, Gles3MinorVersion, HalCounters, - ImageSubresourceRange, IndexFormat, InstanceDescriptor, InstanceFlags, InternalCounters, - Limits, MemoryBudgetThresholds, MemoryHints, MultisampleState, NoopBackendOptions, Origin2d, - Origin3d, PipelineStatisticsTypes, PollError, PollStatus, PolygonMode, PowerPreference, - PredefinedColorSpace, PresentMode, PresentationTimestamp, PrimitiveState, PrimitiveTopology, - PushConstantRange, QueryType, RenderBundleDepthStencil, RequestAdapterError, - SamplerBindingType, SamplerBorderColor, ShaderLocation, ShaderModel, ShaderRuntimeChecks, - ShaderStages, StencilFaceState, StencilOperation, StencilState, StorageTextureAccess, - SurfaceCapabilities, SurfaceStatus, TexelCopyBufferLayout, TextureAspect, TextureDimension, - TextureFormat, TextureFormatFeatureFlags, TextureFormatFeatures, TextureSampleType, - TextureTransition, TextureUsages, TextureUses, TextureViewDimension, Trace, VertexAttribute, - VertexFormat, VertexStepMode, WasmNotSend, WasmNotSendSync, WasmNotSync, COPY_BUFFER_ALIGNMENT, - COPY_BYTES_PER_ROW_ALIGNMENT, MAP_ALIGNMENT, PUSH_CONSTANT_ALIGNMENT, + DxcShaderModel, DynamicOffset, Extent3d, ExternalTextureFormat, Face, Features, FeaturesWGPU, + FeaturesWebGPU, FilterMode, FrontFace, GlBackendOptions, GlFenceBehavior, Gles3MinorVersion, + HalCounters, ImageSubresourceRange, IndexFormat, InstanceDescriptor, InstanceFlags, + InternalCounters, Limits, MemoryBudgetThresholds, MemoryHints, MultisampleState, + NoopBackendOptions, Origin2d, Origin3d, PipelineStatisticsTypes, PollError, PollStatus, + PolygonMode, PowerPreference, PredefinedColorSpace, PresentMode, PresentationTimestamp, + PrimitiveState, PrimitiveTopology, PushConstantRange, QueryType, RenderBundleDepthStencil, + RequestAdapterError, SamplerBindingType, SamplerBorderColor, ShaderLocation, ShaderModel, + ShaderRuntimeChecks, ShaderStages, StencilFaceState, StencilOperation, StencilState, + StorageTextureAccess, SurfaceCapabilities, SurfaceStatus, TexelCopyBufferLayout, TextureAspect, + TextureDimension, TextureFormat, TextureFormatFeatureFlags, TextureFormatFeatures, + TextureSampleType, TextureTransition, TextureUsages, TextureUses, TextureViewDimension, Trace, + VertexAttribute, VertexFormat, VertexStepMode, WasmNotSend, WasmNotSendSync, WasmNotSync, + COPY_BUFFER_ALIGNMENT, COPY_BYTES_PER_ROW_ALIGNMENT, MAP_ALIGNMENT, PUSH_CONSTANT_ALIGNMENT, QUERY_RESOLVE_BUFFER_ALIGNMENT, QUERY_SET_MAX_QUERIES, QUERY_SIZE, VERTEX_STRIDE_ALIGNMENT, }; // wasm-only types, we try to keep as many types non-platform From 7a2067a1f443ce0fbafeebb1d397c9555980be60 Mon Sep 17 00:00:00 2001 From: Jamie Nicol Date: Tue, 27 May 2025 13:15:03 +0100 Subject: [PATCH 3/6] [wgpu-core] Create default ExternalTextureParams buffer In upcoming patches, wgpu will allowing the creation of bind groups with either `TextureView`s or `ExternalTexture`s bound to a `BindingType::ExternalTexture` bind group layout entry. Wgpu-hal and the Naga-generated shaders must be able to handle both of these cases. For external textures they will be provided a uniform buffer containing the external texture's `ExternalTextureParams`. For the texture view case, we must therefore provide the same. To do this, we create a single buffer per device which can be shared between all texture views. We initialize it with the required values in Device::late_init_resources_with_queue(). We know that texture views must have a single RGBA plane, with no rotation or crop-rect. The only thing that can vary between them is their size. We will therefore use the value of [0, 0] in the params buffer to indicate to the shader that it should query the actual texture's size rather than using the value provided in the buffer. --- wgpu-core/src/device/resource.rs | 51 +++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/wgpu-core/src/device/resource.rs b/wgpu-core/src/device/resource.rs index 727b1aae10f..fd957527c5a 100644 --- a/wgpu-core/src/device/resource.rs +++ b/wgpu-core/src/device/resource.rs @@ -173,6 +173,10 @@ pub struct Device { // Optional so that we can late-initialize this after the queue is created. pub(crate) timestamp_normalizer: OnceCellOrLock, + /// Uniform buffer containing [`ExternalTextureParams`] with values such + /// that a [`TextureView`] bound to a [`wgt::BindingType::ExternalTexture`] + /// binding point will be rendered correctly. + pub(crate) default_external_texture_params_buffer: std::sync::OnceLock>, // needs to be dropped last #[cfg(feature = "trace")] pub(crate) trace: Mutex>, @@ -314,6 +318,7 @@ impl Device { adapter: adapter.clone(), queue: OnceCellOrLock::new(), zero_buffer: ManuallyDrop::new(zero_buffer), + default_external_texture_params_buffer: std::sync::OnceLock::new(), label: desc.label.to_string(), command_allocator, command_indices: RwLock::new( @@ -364,7 +369,7 @@ impl Device { }) } - pub fn late_init_resources_with_queue(&self) -> Result<(), RequestDeviceError> { + pub fn late_init_resources_with_queue(self: &Arc) -> Result<(), RequestDeviceError> { let queue = self.get_queue().unwrap(); let timestamp_normalizer = crate::timestamp_normalization::TimestampNormalizer::new( @@ -376,6 +381,50 @@ impl Device { .set(timestamp_normalizer) .unwrap_or_else(|_| panic!("Called late_init_resources_with_queue twice")); + let params_data = ExternalTextureParams { + #[rustfmt::skip] + yuv_conversion_matrix: [ + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0, + ], + size: [0, 0], + #[rustfmt::skip] + sample_transform: [ + 1.0, 0.0, + 0.0, 1.0, + 0.0, 0.0 + ], + num_planes: 1, + }; + let params_buffer = self + .create_buffer(&resource::BufferDescriptor { + label: Some(Cow::Borrowed( + "(wgpu internal) default external texture params buffer", + )), + size: size_of_val(¶ms_data) as wgt::BufferAddress, + usage: wgt::BufferUsages::UNIFORM | wgt::BufferUsages::COPY_DST, + mapped_at_creation: false, + }) + .map_err(|err| match err { + resource::CreateBufferError::Device(device) => RequestDeviceError::Device(device), + _ => unreachable!("Error creating default external texture params buffer: {err:?}"), + })?; + queue + .write_buffer( + Fallible::Valid(params_buffer.clone()), + 0, + bytemuck::bytes_of(¶ms_data), + ) + .map_err(|err| match err { + super::queue::QueueWriteError::Queue(device) => RequestDeviceError::Device(device), + _ => unreachable!("Error writing default external texture params buffer: {err:?}"), + })?; + self.default_external_texture_params_buffer + .set(params_buffer) + .unwrap_or_else(|_| panic!("Called late_init_resources_with_queue twice")); + Ok(()) } From 451678911edda87e72e9013c8d6c3bb3bbf729bb Mon Sep 17 00:00:00 2001 From: Jamie Nicol Date: Tue, 27 May 2025 13:15:03 +0100 Subject: [PATCH 4/6] [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. --- wgpu-core/src/binding_model.rs | 63 +++++-- wgpu-core/src/device/global.rs | 12 ++ wgpu-core/src/device/resource.rs | 177 ++++++++++++++++-- wgpu-core/src/indirect_validation/dispatch.rs | 2 + wgpu-core/src/indirect_validation/draw.rs | 2 + wgpu-core/src/resource.rs | 2 - wgpu-core/src/timestamp_normalization/mod.rs | 1 + wgpu-core/src/track/mod.rs | 2 + wgpu-hal/examples/halmark/main.rs | 2 + wgpu-hal/examples/ray-traced-triangle/main.rs | 1 + wgpu-hal/src/dynamic/device.rs | 6 + wgpu-hal/src/dynamic/mod.rs | 13 +- wgpu-hal/src/lib.rs | 18 ++ wgpu/src/api/bind_group.rs | 6 + wgpu/src/backend/webgpu.rs | 3 + wgpu/src/backend/wgpu_core.rs | 3 + 16 files changed, 278 insertions(+), 35 deletions(-) diff --git a/wgpu-core/src/binding_model.rs b/wgpu-core/src/binding_model.rs index 6d99ef2f177..791d9eb194d 100644 --- a/wgpu-core/src/binding_model.rs +++ b/wgpu-core/src/binding_model.rs @@ -19,12 +19,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, ResourceErrorIdent, Sampler, TextureView, Tlas, TrackingData, + Buffer, DestroyedResourceError, ExternalTexture, InvalidResourceError, Labeled, + MissingBufferUsageError, MissingTextureUsageError, ResourceErrorIdent, Sampler, + TextureView, Tlas, TrackingData, }, resource_log, snatch::{SnatchGuard, Snatchable}, @@ -492,8 +493,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]: ToOwned, [S]: ToOwned, [TV]: ToOwned, @@ -506,15 +513,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, Arc, Arc, Arc>; +pub type ResolvedBindGroupEntry<'a> = BindGroupEntry< + 'a, + Arc, + Arc, + Arc, + Arc, + Arc, +>; /// Describes a group of bindings and the resources to be bound. #[derive(Clone, Debug)] @@ -526,6 +539,7 @@ pub struct BindGroupDescriptor< S = SamplerId, TV = TextureViewId, TLAS = TlasId, + ET = ExternalTextureId, > where [BufferBinding]: ToOwned, [S]: ToOwned, @@ -533,8 +547,8 @@ pub struct BindGroupDescriptor< <[BufferBinding] 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. /// @@ -545,11 +559,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 @@ -560,6 +575,7 @@ pub type ResolvedBindGroupDescriptor<'a> = BindGroupDescriptor< Arc, Arc, Arc, + Arc, >; /// Describes a [`BindGroupLayout`]. @@ -881,8 +897,14 @@ pub type ResolvedBufferBinding = BufferBinding>; // 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]: ToOwned, [S]: ToOwned, [TV]: ToOwned, @@ -909,10 +931,17 @@ where )] TextureViewArray(Cow<'a, [TV]>), AccelerationStructure(TLAS), + ExternalTexture(ET), } -pub type ResolvedBindingResource<'a> = - BindingResource<'a, Arc, Arc, Arc, Arc>; +pub type ResolvedBindingResource<'a> = BindingResource< + 'a, + Arc, + Arc, + Arc, + Arc, + Arc, +>; #[derive(Clone, Debug, Error)] #[non_exhaustive] diff --git a/wgpu-core/src/device/global.rs b/wgpu-core/src/device/global.rs index 834c5a63358..e348fb95219 100644 --- a/wgpu-core/src/device/global.rs +++ b/wgpu-core/src/device/global.rs @@ -802,6 +802,7 @@ impl Global { sampler_storage: &Storage>, texture_view_storage: &Storage>, tlas_storage: &Storage>, + external_texture_storage: &Storage>, ) -> Result, binding_model::CreateBindGroupError> { let resolve_buffer = |bb: &BufferBinding| { @@ -833,6 +834,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)?) @@ -867,6 +874,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, @@ -879,6 +889,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| { @@ -888,6 +899,7 @@ impl Global { &sampler_guard, &texture_view_guard, &tlas_guard, + &external_texture_guard, ) }) .collect::, _>>() diff --git a/wgpu-core/src/device/resource.rs b/wgpu-core/src/device/resource.rs index fd957527c5a..8ef39225443 100644 --- a/wgpu-core/src/device/resource.rs +++ b/wgpu-core/src/device/resource.rs @@ -75,8 +75,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 { @@ -2562,6 +2562,125 @@ 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, + 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::, 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, + 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 + .get() + .expect("Default external texture params buffer should have been initialized") + .try_raw(snatch_guard)?, + 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( @@ -2602,6 +2721,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; @@ -2668,19 +2788,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)?; @@ -2708,6 +2842,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 { @@ -2733,6 +2879,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))?; diff --git a/wgpu-core/src/indirect_validation/dispatch.rs b/wgpu-core/src/indirect_validation/dispatch.rs index 00e3798e9ba..6e2911c1a46 100644 --- a/wgpu-core/src/indirect_validation/dispatch.rs +++ b/wgpu-core/src/indirect_validation/dispatch.rs @@ -240,6 +240,7 @@ impl Dispatch { samplers: &[], textures: &[], acceleration_structures: &[], + external_textures: &[], }; let dst_bind_group = unsafe { device @@ -286,6 +287,7 @@ impl Dispatch { samplers: &[], textures: &[], acceleration_structures: &[], + external_textures: &[], }; unsafe { device diff --git a/wgpu-core/src/indirect_validation/draw.rs b/wgpu-core/src/indirect_validation/draw.rs index d88acb8d60d..cb7e69dbb68 100644 --- a/wgpu-core/src/indirect_validation/draw.rs +++ b/wgpu-core/src/indirect_validation/draw.rs @@ -143,6 +143,7 @@ impl Draw { samplers: &[], textures: &[], acceleration_structures: &[], + external_textures: &[], }; unsafe { device @@ -692,6 +693,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 }) diff --git a/wgpu-core/src/resource.rs b/wgpu-core/src/resource.rs index 3aa4f886be6..f756ed6c9d2 100644 --- a/wgpu-core/src/resource.rs +++ b/wgpu-core/src/resource.rs @@ -1811,11 +1811,9 @@ pub type ExternalTextureDescriptor<'a> = wgt::ExternalTextureDescriptor, /// Between 1 and 3 (inclusive) planes of texture data. - #[allow(dead_code)] pub(crate) planes: arrayvec::ArrayVec, 3>, /// Buffer containing a [`crate::device::resource::ExternalTextureParams`] /// describing the external texture. - #[allow(dead_code)] pub(crate) params: Arc, /// The `label` from the descriptor used to create the resource. pub(crate) label: String, diff --git a/wgpu-core/src/timestamp_normalization/mod.rs b/wgpu-core/src/timestamp_normalization/mod.rs index dd4d466235c..fd0f87aaf78 100644 --- a/wgpu-core/src/timestamp_normalization/mod.rs +++ b/wgpu-core/src/timestamp_normalization/mod.rs @@ -290,6 +290,7 @@ impl TimestampNormalizer { samplers: &[], textures: &[], acceleration_structures: &[], + external_textures: &[], entries: &[hal::BindGroupEntry { binding: 0, resource_index: 0, diff --git a/wgpu-core/src/track/mod.rs b/wgpu-core/src/track/mod.rs index 6f74db8ae84..638727a56e8 100644 --- a/wgpu-core/src/track/mod.rs +++ b/wgpu-core/src/track/mod.rs @@ -429,6 +429,7 @@ impl fmt::Display for InvalidUse { pub(crate) struct BindGroupStates { pub buffers: BufferBindGroupState, pub views: TextureViewBindGroupState, + pub external_textures: StatelessTracker, pub samplers: StatelessTracker, pub acceleration_structures: StatelessTracker, } @@ -438,6 +439,7 @@ impl BindGroupStates { Self { buffers: BufferBindGroupState::new(), views: TextureViewBindGroupState::new(), + external_textures: StatelessTracker::new(), samplers: StatelessTracker::new(), acceleration_structures: StatelessTracker::new(), } diff --git a/wgpu-hal/examples/halmark/main.rs b/wgpu-hal/examples/halmark/main.rs index 75f3bc2fb9a..032dd911a97 100644 --- a/wgpu-hal/examples/halmark/main.rs +++ b/wgpu-hal/examples/halmark/main.rs @@ -461,6 +461,7 @@ impl Example { samplers: &[&sampler], textures: &[texture_binding], acceleration_structures: &[], + external_textures: &[], entries: &[ hal::BindGroupEntry { binding: 0, @@ -495,6 +496,7 @@ impl Example { samplers: &[], textures: &[], acceleration_structures: &[], + external_textures: &[], entries: &[hal::BindGroupEntry { binding: 0, resource_index: 0, diff --git a/wgpu-hal/examples/ray-traced-triangle/main.rs b/wgpu-hal/examples/ray-traced-triangle/main.rs index a8d3a77b916..eb0494c4997 100644 --- a/wgpu-hal/examples/ray-traced-triangle/main.rs +++ b/wgpu-hal/examples/ray-traced-triangle/main.rs @@ -619,6 +619,7 @@ impl Example { samplers: &[], textures: &[texture_binding], acceleration_structures: &[&tlas], + external_textures: &[], entries: &[ hal::BindGroupEntry { binding: 0, diff --git a/wgpu-hal/src/dynamic/device.rs b/wgpu-hal/src/dynamic/device.rs index fd7c10f254d..de66b1619f6 100644 --- a/wgpu-hal/src/dynamic/device.rs +++ b/wgpu-hal/src/dynamic/device.rs @@ -345,6 +345,11 @@ impl 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 DynDevice for D { textures: &textures, entries: desc.entries, acceleration_structures: &acceleration_structures, + external_textures: &external_textures, }; unsafe { D::create_bind_group(self, &desc) } diff --git a/wgpu-hal/src/dynamic/mod.rs b/wgpu-hal/src/dynamic/mod.rs index a8dbae94ee0..85d8ca00450 100644 --- a/wgpu-hal/src/dynamic/mod.rs +++ b/wgpu-hal/src/dynamic/mod.rs @@ -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( + 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(self) -> ProgrammableStage<'a, T> { ProgrammableStage { diff --git a/wgpu-hal/src/lib.rs b/wgpu-hal/src/lib.rs index 4184c8c6f1b..a2f7efc715d 100644 --- a/wgpu-hal/src/lib.rs +++ b/wgpu-hal/src/lib.rs @@ -2034,6 +2034,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 { @@ -2067,6 +2084,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)] diff --git a/wgpu/src/api/bind_group.rs b/wgpu/src/api/bind_group.rs index 2f4ae007ff6..8471fca53fd 100644 --- a/wgpu/src/api/bind_group.rs +++ b/wgpu/src/api/bind_group.rs @@ -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); diff --git a/wgpu/src/backend/webgpu.rs b/wgpu/src/backend/webgpu.rs index 8d8dae95e1c..b3b1c1067c6 100644 --- a/wgpu/src/backend/webgpu.rs +++ b/wgpu/src/backend/webgpu.rs @@ -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) diff --git a/wgpu/src/backend/wgpu_core.rs b/wgpu/src/backend/wgpu_core.rs index dd36bba182f..f7f96579a6d 100644 --- a/wgpu/src/backend/wgpu_core.rs +++ b/wgpu/src/backend/wgpu_core.rs @@ -1251,6 +1251,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::>(); From ec8b51a938a620d3998fe96f9bb0025286fed051 Mon Sep 17 00:00:00 2001 From: Jamie Nicol Date: Mon, 2 Jun 2025 10:31:47 +0100 Subject: [PATCH 5/6] [wgpu] Add external texture validation tests Adds validation tests using the noop backend covering creation of external textures, and creation of bind groups containing external textures. --- .../wgpu-validation/api/external_texture.rs | 480 ++++++++++++++++++ 1 file changed, 480 insertions(+) diff --git a/tests/tests/wgpu-validation/api/external_texture.rs b/tests/tests/wgpu-validation/api/external_texture.rs index ece0e060f4f..ba33f2e495b 100644 --- a/tests/tests/wgpu-validation/api/external_texture.rs +++ b/tests/tests/wgpu-validation/api/external_texture.rs @@ -1,6 +1,351 @@ use wgpu::*; use wgpu_test::{fail, valid}; +/// Ensures an [`ExternalTexture`] can be created from a valid descriptor and planes, +/// but appropriate errors are returned for invalid descriptors and planes. +#[test] +fn create_external_texture() { + let (device, _queue) = wgpu::Device::noop(&DeviceDescriptor { + required_features: Features::EXTERNAL_TEXTURE, + ..Default::default() + }); + + let texture_descriptor = TextureDescriptor { + label: None, + size: Extent3d { + width: 512, + height: 512, + 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 r_texture = device.create_texture(&TextureDescriptor { + format: TextureFormat::R8Unorm, + ..texture_descriptor + }); + let r_view = r_texture.create_view(&TextureViewDescriptor::default()); + let rg_texture = device.create_texture(&TextureDescriptor { + format: TextureFormat::Rg8Unorm, + ..texture_descriptor + }); + let rg_view = rg_texture.create_view(&TextureViewDescriptor::default()); + let rgba_texture = device.create_texture(&TextureDescriptor { + format: TextureFormat::Rgba8Unorm, + ..texture_descriptor + }); + let rgba_view = rgba_texture.create_view(&TextureViewDescriptor::default()); + + let _ = valid(&device, || { + device.create_external_texture( + &ExternalTextureDescriptor { + format: ExternalTextureFormat::Rgba, + label: None, + width: r_texture.width(), + height: r_texture.height(), + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }, + &[&rgba_view], + ) + }); + let _ = valid(&device, || { + device.create_external_texture( + &ExternalTextureDescriptor { + format: ExternalTextureFormat::Nv12, + label: None, + width: r_texture.width(), + height: r_texture.height(), + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }, + &[&r_view, &rg_view], + ) + }); + let _ = valid(&device, || { + device.create_external_texture( + &ExternalTextureDescriptor { + format: ExternalTextureFormat::Yu12, + label: None, + width: r_texture.width(), + height: r_texture.height(), + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }, + &[&r_view, &r_view, &r_view], + ) + }); + + // Wrong number of planes for format + let _ = fail( + &device, + || { + device.create_external_texture( + &ExternalTextureDescriptor { + format: ExternalTextureFormat::Rgba, + label: None, + width: r_texture.width(), + height: r_texture.height(), + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }, + &[&r_view, &r_view], + ) + }, + Some("External texture format Rgba expects 1 planes, but given 2"), + ); + let _ = fail( + &device, + || { + device.create_external_texture( + &ExternalTextureDescriptor { + format: ExternalTextureFormat::Nv12, + label: None, + width: r_texture.width(), + height: r_texture.height(), + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }, + &[&r_view], + ) + }, + Some("External texture format Nv12 expects 2 planes, but given 1"), + ); + let _ = fail( + &device, + || { + device.create_external_texture( + &ExternalTextureDescriptor { + format: ExternalTextureFormat::Yu12, + label: None, + width: r_texture.width(), + height: r_texture.height(), + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }, + &[&r_view, &r_view], + ) + }, + Some("External texture format Yu12 expects 3 planes, but given 2"), + ); + + // Wrong plane formats + let _ = fail( + &device, + || { + device.create_external_texture( + &ExternalTextureDescriptor { + format: ExternalTextureFormat::Rgba, + label: None, + width: r_texture.width(), + height: r_texture.height(), + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }, + &[&r_view], + ) + }, + Some("External texture format Rgba plane 0 expects format with 4 components but given view with format R8Unorm (1 components)"), + ); + let _ = fail( + &device, + || { + device.create_external_texture( + &ExternalTextureDescriptor { + format: ExternalTextureFormat::Nv12, + label: None, + width: r_texture.width(), + height: r_texture.height(), + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }, + &[&r_view, &rgba_view], + ) + }, + Some("External texture format Nv12 plane 1 expects format with 2 components but given view with format Rgba8Unorm (4 components)"), + ); + let _ = fail( + &device, + || { + device.create_external_texture( + &ExternalTextureDescriptor { + format: ExternalTextureFormat::Yu12, + label: None, + width: r_texture.width(), + height: r_texture.height(), + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }, + &[&r_view, &rg_view, &r_view], + ) + }, + Some("External texture format Yu12 plane 1 expects format with 1 components but given view with format Rg8Unorm (2 components)"), + ); + + // Wrong sample type + let uint_texture = device.create_texture(&TextureDescriptor { + format: TextureFormat::Rgba8Uint, + ..texture_descriptor + }); + let uint_view = uint_texture.create_view(&TextureViewDescriptor::default()); + let _ = fail( + &device, + || { + device.create_external_texture( + &ExternalTextureDescriptor { + format: ExternalTextureFormat::Rgba, + label: None, + width: uint_texture.width(), + height: uint_texture.height(), + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }, + &[&uint_view], + ) + }, + Some("External texture planes expect a filterable float sample type, but given view with format Rgba8Uint (sample type Uint)"), + ); + + // Wrong texture dimension + let d3_texture = device.create_texture(&TextureDescriptor { + dimension: TextureDimension::D3, + ..texture_descriptor + }); + let d3_view = d3_texture.create_view(&TextureViewDescriptor::default()); + let _ = fail( + &device, + || { + device.create_external_texture( + &ExternalTextureDescriptor { + format: ExternalTextureFormat::Rgba, + label: None, + width: d3_texture.width(), + height: d3_texture.height(), + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }, + &[&d3_view], + ) + }, + Some("External texture planes expect 2D dimension, but given view with dimension = D3"), + ); + + // Multisampled + let multisampled_texture = device.create_texture(&TextureDescriptor { + sample_count: 4, + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + ..texture_descriptor + }); + let multisampled_view = multisampled_texture.create_view(&TextureViewDescriptor::default()); + let _ = fail( + &device, + || { + device.create_external_texture( + &ExternalTextureDescriptor { + format: ExternalTextureFormat::Rgba, + label: None, + width: multisampled_texture.width(), + height: multisampled_texture.height(), + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }, + &[&multisampled_view], + ) + }, + Some("External texture planes cannot be multisampled, but given view with samples = 4"), + ); + + // Missing TEXTURE_BINDING + let non_binding_texture = device.create_texture(&TextureDescriptor { + usage: TextureUsages::STORAGE_BINDING, + ..texture_descriptor + }); + let non_binding_view = non_binding_texture.create_view(&TextureViewDescriptor::default()); + let _ = fail( + &device, + || { + device.create_external_texture( + &ExternalTextureDescriptor { + format: ExternalTextureFormat::Rgba, + label: None, + width: non_binding_texture.width(), + height: non_binding_texture.height(), + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }, + &[&non_binding_view], + ) + }, + Some("Usage flags TextureUsages(STORAGE_BINDING) of TextureView with '' label do not contain required usage flags TextureUsages(TEXTURE_BINDING)"), + ); +} + +/// Ensures an [`ExternalTexture`] can be bound to a [`BindingType::ExternalTexture`] +/// resource binding. +#[test] +fn external_texture_binding() { + let (device, _queue) = wgpu::Device::noop(&DeviceDescriptor { + required_features: Features::EXTERNAL_TEXTURE, + ..Default::default() + }); + + let bgl = valid(&device, || { + 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 external_texture_descriptor = ExternalTextureDescriptor { + label: None, + width: texture_descriptor.size.width, + height: texture_descriptor.size.height, + format: ExternalTextureFormat::Rgba, + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }; + + valid(&device, || { + let texture = device.create_texture(&texture_descriptor); + let view = texture.create_view(&TextureViewDescriptor::default()); + let external_texture = + device.create_external_texture(&external_texture_descriptor, &[&view]); + + device.create_bind_group(&BindGroupDescriptor { + label: None, + layout: &bgl, + entries: &[BindGroupEntry { + binding: 0, + resource: BindingResource::ExternalTexture(&external_texture), + }], + }) + }); +} + /// Ensures a [`TextureView`] can be bound to a [`BindingType::ExternalTexture`] /// resource binding. #[test] @@ -160,3 +505,138 @@ fn external_texture_binding_texture_view() { Some("Texture binding 0 expects multisampled = false, but given a view with samples = 4"), ); } + +/// Ensures that submitting a command buffer referencing an external texture, any of +/// whose plane textures have already been destroyed, results in an error. +#[test] +fn destroyed_external_texture_plane() { + let (device, queue) = wgpu::Device::noop(&DeviceDescriptor { + required_features: Features::EXTERNAL_TEXTURE, + ..Default::default() + }); + + let target_texture = device.create_texture(&TextureDescriptor { + label: None, + size: Extent3d { + width: 512, + height: 512, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8Unorm, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + let target_view = target_texture.create_view(&TextureViewDescriptor::default()); + + let plane_texture = device.create_texture(&TextureDescriptor { + label: Some("External texture plane"), + size: Extent3d { + width: 512, + height: 512, + 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 plane_view = plane_texture.create_view(&TextureViewDescriptor::default()); + + let external_texture = device.create_external_texture( + &ExternalTextureDescriptor { + format: ExternalTextureFormat::Rgba, + label: None, + width: plane_texture.width(), + height: plane_texture.height(), + yuv_conversion_matrix: [0.0; 16], + sample_transform: [0.0; 6], + }, + &[&plane_view], + ); + + let module = device.create_shader_module(ShaderModuleDescriptor { + label: None, + source: ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + " +@group(0) @binding(0) +var tex: texture_external; +@vertex fn vert_main() -> @builtin(position) vec4 { return vec4(0); } +@fragment fn frag_main() -> @location(0) vec4 { return textureLoad(tex, vec2(0)); }", + )), + }); + + let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { + label: None, + layout: None, + vertex: VertexState { + module: &module, + entry_point: None, + compilation_options: PipelineCompilationOptions::default(), + buffers: &[], + }, + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState::default(), + fragment: Some(FragmentState { + module: &module, + entry_point: None, + compilation_options: PipelineCompilationOptions::default(), + targets: &[Some(ColorTargetState { + format: target_texture.format(), + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + multiview: None, + cache: None, + }); + + let bind_group = device.create_bind_group(&BindGroupDescriptor { + label: None, + layout: &pipeline.get_bind_group_layout(0), + entries: &[BindGroupEntry { + binding: 0, + resource: BindingResource::ExternalTexture(&external_texture), + }], + }); + + let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: None }); + let mut pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: None, + color_attachments: &[Some(RenderPassColorAttachment { + view: &target_view, + depth_slice: None, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + pass.set_pipeline(&pipeline); + pass.set_bind_group(0, &bind_group, &[]); + pass.draw(0..0, 0..0); + drop(pass); + + plane_texture.destroy(); + + fail( + &device, + || queue.submit([encoder.finish()]), + Some("Texture with 'External texture plane' label has been destroyed"), + ); +} From e2375e4845d44c58ad162c197f1e1fa8fef3759c Mon Sep 17 00:00:00 2001 From: Jamie Nicol Date: Mon, 23 Jun 2025 14:35:24 +0100 Subject: [PATCH 6/6] fixup! [wgpu-core] Create default ExternalTextureParams buffer --- wgpu-core/src/device/resource.rs | 133 ++++++++++++++++++++----------- 1 file changed, 87 insertions(+), 46 deletions(-) diff --git a/wgpu-core/src/device/resource.rs b/wgpu-core/src/device/resource.rs index 8ef39225443..025cbae22a0 100644 --- a/wgpu-core/src/device/resource.rs +++ b/wgpu-core/src/device/resource.rs @@ -175,8 +175,9 @@ pub struct Device { OnceCellOrLock, /// Uniform buffer containing [`ExternalTextureParams`] with values such /// that a [`TextureView`] bound to a [`wgt::BindingType::ExternalTexture`] - /// binding point will be rendered correctly. - pub(crate) default_external_texture_params_buffer: std::sync::OnceLock>, + /// binding point will be rendered correctly. Intended to be used as the + /// [`hal::ExternalTextureBinding::params`] field. + pub(crate) default_external_texture_params_buffer: ManuallyDrop>, // needs to be dropped last #[cfg(feature = "trace")] pub(crate) trace: Mutex>, @@ -204,6 +205,10 @@ impl Drop for Device { // SAFETY: We are in the Drop impl and we don't use self.zero_buffer anymore after this point. let zero_buffer = unsafe { ManuallyDrop::take(&mut self.zero_buffer) }; + // SAFETY: We are in the Drop impl and we don't use + // self.default_external_texture_params_buffer anymore after this point. + let default_external_texture_params_buffer = + unsafe { ManuallyDrop::take(&mut self.default_external_texture_params_buffer) }; // SAFETY: We are in the Drop impl and we don't use self.fence anymore after this point. let fence = unsafe { ManuallyDrop::take(&mut self.fence.write()) }; if let Some(indirect_validation) = self.indirect_validation.take() { @@ -214,6 +219,8 @@ impl Drop for Device { } unsafe { self.raw.destroy_buffer(zero_buffer); + self.raw + .destroy_buffer(default_external_texture_params_buffer); self.raw.destroy_fence(fence); } } @@ -293,6 +300,19 @@ impl Device { } .map_err(DeviceError::from_hal)?; + let default_external_texture_params_buffer = unsafe { + raw_device.create_buffer(&hal::BufferDescriptor { + label: hal_label( + Some("(wgpu internal) default external texture params buffer"), + instance_flags, + ), + size: size_of::() as _, + usage: wgt::BufferUses::COPY_DST | wgt::BufferUses::UNIFORM, + memory_flags: hal::MemoryFlags::empty(), + }) + } + .map_err(DeviceError::from_hal)?; + let alignments = adapter.raw.capabilities.alignments.clone(); let downlevel = adapter.raw.capabilities.downlevel.clone(); @@ -318,7 +338,9 @@ impl Device { adapter: adapter.clone(), queue: OnceCellOrLock::new(), zero_buffer: ManuallyDrop::new(zero_buffer), - default_external_texture_params_buffer: std::sync::OnceLock::new(), + default_external_texture_params_buffer: ManuallyDrop::new( + default_external_texture_params_buffer, + ), label: desc.label.to_string(), command_allocator, command_indices: RwLock::new( @@ -369,19 +391,12 @@ impl Device { }) } - pub fn late_init_resources_with_queue(self: &Arc) -> Result<(), RequestDeviceError> { - let queue = self.get_queue().unwrap(); - - let timestamp_normalizer = crate::timestamp_normalization::TimestampNormalizer::new( - self, - queue.get_timestamp_period(), - )?; - - self.timestamp_normalizer - .set(timestamp_normalizer) - .unwrap_or_else(|_| panic!("Called late_init_resources_with_queue twice")); - - let params_data = ExternalTextureParams { + /// Initializes [`Device::default_external_texture_params_buffer`] with + /// required values such that a [`TextureView`] bound to a + /// [`wgt::BindingType::ExternalTexture`] binding point will be rendered + /// correctly. + fn init_default_external_texture_params_buffer(self: &Arc) -> Result<(), DeviceError> { + let data = ExternalTextureParams { #[rustfmt::skip] yuv_conversion_matrix: [ 1.0, 0.0, 0.0, 0.0, @@ -398,33 +413,63 @@ impl Device { ], num_planes: 1, }; - let params_buffer = self - .create_buffer(&resource::BufferDescriptor { - label: Some(Cow::Borrowed( - "(wgpu internal) default external texture params buffer", - )), - size: size_of_val(¶ms_data) as wgt::BufferAddress, - usage: wgt::BufferUsages::UNIFORM | wgt::BufferUsages::COPY_DST, - mapped_at_creation: false, - }) - .map_err(|err| match err { - resource::CreateBufferError::Device(device) => RequestDeviceError::Device(device), - _ => unreachable!("Error creating default external texture params buffer: {err:?}"), - })?; - queue - .write_buffer( - Fallible::Valid(params_buffer.clone()), - 0, - bytemuck::bytes_of(¶ms_data), - ) - .map_err(|err| match err { - super::queue::QueueWriteError::Queue(device) => RequestDeviceError::Device(device), - _ => unreachable!("Error writing default external texture params buffer: {err:?}"), - })?; - self.default_external_texture_params_buffer - .set(params_buffer) + let mut staging_buffer = + StagingBuffer::new(self, wgt::BufferSize::new(size_of_val(&data) as _).unwrap())?; + staging_buffer.write(bytemuck::bytes_of(&data)); + let staging_buffer = staging_buffer.flush(); + + let params_buffer = self.default_external_texture_params_buffer.as_ref(); + let queue = self.get_queue().unwrap(); + let mut pending_writes = queue.pending_writes.lock(); + + unsafe { + pending_writes + .command_encoder + .transition_buffers(&[hal::BufferBarrier { + buffer: params_buffer, + usage: hal::StateTransition { + from: wgt::BufferUses::MAP_WRITE, + to: wgt::BufferUses::COPY_SRC, + }, + }]); + pending_writes.command_encoder.copy_buffer_to_buffer( + staging_buffer.raw(), + params_buffer, + &[hal::BufferCopy { + src_offset: 0, + dst_offset: 0, + size: staging_buffer.size, + }], + ); + pending_writes.consume(staging_buffer); + pending_writes + .command_encoder + .transition_buffers(&[hal::BufferBarrier { + buffer: params_buffer, + usage: hal::StateTransition { + from: wgt::BufferUses::COPY_DST, + to: wgt::BufferUses::UNIFORM, + }, + }]); + } + + Ok(()) + } + + pub fn late_init_resources_with_queue(self: &Arc) -> Result<(), RequestDeviceError> { + let queue = self.get_queue().unwrap(); + + let timestamp_normalizer = crate::timestamp_normalization::TimestampNormalizer::new( + self, + queue.get_timestamp_period(), + )?; + + self.timestamp_normalizer + .set(timestamp_normalizer) .unwrap_or_else(|_| panic!("Called late_init_resources_with_queue twice")); + self.init_default_external_texture_params_buffer()?; + Ok(()) } @@ -2669,11 +2714,7 @@ impl Device { }, ]; let params = hal::BufferBinding { - buffer: self - .default_external_texture_params_buffer - .get() - .expect("Default external texture params buffer should have been initialized") - .try_raw(snatch_guard)?, + buffer: self.default_external_texture_params_buffer.as_ref(), offset: 0, size: None, };