From ad84bec26e649b73e13e8c0317b02500bf103d93 Mon Sep 17 00:00:00 2001 From: Jamie Nicol Date: Mon, 2 Jun 2025 17:21:23 +0100 Subject: [PATCH 1/4] [naga] Generate special type for external texture params buffer During wgsl lowering, if we encounter an external texture type then generate the `ExternalTextureParams` struct. This will be required by most Naga backends to implement external textures. This type is not actually used by wgsl-in or the IR. However, generating it in Naga IR ensures tricky details such as member alignment are handled for us. wgsl-out must ensure it does *not* generate code for this type, as it handles external textures natively. --- naga/src/back/wgsl/writer.rs | 20 +++- naga/src/compact/mod.rs | 12 ++ naga/src/front/type_gen.rs | 94 ++++++++++++++++ naga/src/front/wgsl/lower/mod.rs | 28 ++++- naga/src/ir/mod.rs | 22 ++++ naga/tests/out/ir/spv-fetch_depth.compact.ron | 1 + naga/tests/out/ir/spv-fetch_depth.ron | 1 + naga/tests/out/ir/spv-shadow.compact.ron | 1 + naga/tests/out/ir/spv-shadow.ron | 1 + .../out/ir/spv-spec-constants.compact.ron | 1 + naga/tests/out/ir/spv-spec-constants.ron | 1 + naga/tests/out/ir/wgsl-access.compact.ron | 1 + naga/tests/out/ir/wgsl-access.ron | 1 + naga/tests/out/ir/wgsl-collatz.compact.ron | 1 + naga/tests/out/ir/wgsl-collatz.ron | 1 + .../out/ir/wgsl-const_assert.compact.ron | 1 + naga/tests/out/ir/wgsl-const_assert.ron | 1 + .../out/ir/wgsl-diagnostic-filter.compact.ron | 1 + naga/tests/out/ir/wgsl-diagnostic-filter.ron | 1 + .../out/ir/wgsl-index-by-value.compact.ron | 1 + naga/tests/out/ir/wgsl-index-by-value.ron | 1 + .../tests/out/ir/wgsl-local-const.compact.ron | 1 + naga/tests/out/ir/wgsl-local-const.ron | 1 + naga/tests/out/ir/wgsl-must-use.compact.ron | 1 + naga/tests/out/ir/wgsl-must-use.ron | 1 + ...ides-atomicCompareExchangeWeak.compact.ron | 1 + ...sl-overrides-atomicCompareExchangeWeak.ron | 1 + .../ir/wgsl-overrides-ray-query.compact.ron | 1 + .../tests/out/ir/wgsl-overrides-ray-query.ron | 1 + naga/tests/out/ir/wgsl-overrides.compact.ron | 1 + naga/tests/out/ir/wgsl-overrides.ron | 1 + .../out/ir/wgsl-storage-textures.compact.ron | 1 + naga/tests/out/ir/wgsl-storage-textures.ron | 1 + ...l-template-list-trailing-comma.compact.ron | 1 + .../ir/wgsl-template-list-trailing-comma.ron | 1 + .../out/ir/wgsl-texture-external.compact.ron | 106 ++++++++++++++---- naga/tests/out/ir/wgsl-texture-external.ron | 106 ++++++++++++++---- .../ir/wgsl-types_with_comments.compact.ron | 1 + .../tests/out/ir/wgsl-types_with_comments.ron | 1 + wgpu-core/src/device/resource.rs | 13 ++- 40 files changed, 386 insertions(+), 47 deletions(-) diff --git a/naga/src/back/wgsl/writer.rs b/naga/src/back/wgsl/writer.rs index 58c1b734f09..dd2696c17f7 100644 --- a/naga/src/back/wgsl/writer.rs +++ b/naga/src/back/wgsl/writer.rs @@ -109,12 +109,28 @@ impl Writer { self.required_polyfills.clear(); } - fn is_builtin_wgsl_struct(&self, module: &Module, handle: Handle) -> bool { + /// Determine if `ty` is the Naga IR presentation of a WGSL builtin type. + /// + /// Return true if `ty` refers to the Naga IR form of a WGSL builtin type + /// like `__atomic_compare_exchange_result`. + /// + /// Even though the module may use the type, the WGSL backend should avoid + /// emitting a definition for it, since it is [predeclared] in WGSL. + /// + /// This also covers types like [`NagaExternalTextureParams`], which other + /// backends use to lower WGSL constructs like external textures to their + /// implementations. WGSL can express these directly, so the types need not + /// be emitted. + /// + /// [predeclared]: https://www.w3.org/TR/WGSL/#predeclared + /// [`NagaExternalTextureParams`]: crate::ir::SpecialTypes::external_texture_params + fn is_builtin_wgsl_struct(&self, module: &Module, ty: Handle) -> bool { module .special_types .predeclared_types .values() - .any(|t| *t == handle) + .any(|t| *t == ty) + || Some(ty) == module.special_types.external_texture_params } pub fn write(&mut self, module: &Module, info: &valid::ModuleInfo) -> BackendResult { diff --git a/naga/src/compact/mod.rs b/naga/src/compact/mod.rs index fe4296be844..7fbb5c532f2 100644 --- a/naga/src/compact/mod.rs +++ b/naga/src/compact/mod.rs @@ -380,6 +380,7 @@ impl<'module> ModuleTracer<'module> { ref ray_intersection, ref ray_vertex_return, ref predeclared_types, + ref external_texture_params, } = *special_types; if let Some(ray_desc) = *ray_desc { @@ -391,6 +392,12 @@ impl<'module> ModuleTracer<'module> { if let Some(ray_vertex_return) = *ray_vertex_return { self.types_used.insert(ray_vertex_return); } + // The `external_texture_params` type is generated purely as a + // convenience to the backends. While it will never actually be used in + // the IR, it must be marked as used so that it survives compaction. + if let Some(external_texture_params) = *external_texture_params { + self.types_used.insert(external_texture_params); + } for (_, &handle) in predeclared_types { self.types_used.insert(handle); } @@ -532,6 +539,7 @@ impl ModuleMap { ref mut ray_intersection, ref mut ray_vertex_return, ref mut predeclared_types, + ref mut external_texture_params, } = *special; if let Some(ref mut ray_desc) = *ray_desc { @@ -545,6 +553,10 @@ impl ModuleMap { self.types.adjust(ray_vertex_return); } + if let Some(ref mut external_texture_params) = *external_texture_params { + self.types.adjust(external_texture_params); + } + for handle in predeclared_types.values_mut() { self.types.adjust(handle); } diff --git a/naga/src/front/type_gen.rs b/naga/src/front/type_gen.rs index 9a01b637d5a..d58561796bd 100644 --- a/naga/src/front/type_gen.rs +++ b/naga/src/front/type_gen.rs @@ -276,6 +276,100 @@ impl crate::Module { handle } + /// Generate [`SpecialTypes::external_texture_params`]. + /// + /// [`SpecialTypes::external_texture_params`]: crate::ir::SpecialTypes::external_texture_params + pub fn generate_external_texture_params_type(&mut self) -> Handle { + if let Some(handle) = self.special_types.external_texture_params { + return handle; + } + + let ty_u32 = self.types.insert( + crate::Type { + name: None, + inner: crate::TypeInner::Scalar(crate::Scalar::U32), + }, + Span::UNDEFINED, + ); + let ty_vec2u = self.types.insert( + crate::Type { + name: None, + inner: crate::TypeInner::Vector { + size: crate::VectorSize::Bi, + scalar: crate::Scalar::U32, + }, + }, + Span::UNDEFINED, + ); + let ty_mat3x2f = self.types.insert( + crate::Type { + name: None, + inner: crate::TypeInner::Matrix { + columns: crate::VectorSize::Tri, + rows: crate::VectorSize::Bi, + scalar: crate::Scalar::F32, + }, + }, + Span::UNDEFINED, + ); + let ty_mat4x4f = self.types.insert( + crate::Type { + name: None, + inner: crate::TypeInner::Matrix { + columns: crate::VectorSize::Quad, + rows: crate::VectorSize::Quad, + scalar: crate::Scalar::F32, + }, + }, + Span::UNDEFINED, + ); + + let handle = self.types.insert( + crate::Type { + name: Some("NagaExternalTextureParams".to_string()), + inner: crate::TypeInner::Struct { + members: vec![ + crate::StructMember { + name: Some("yuv_conversion_matrix".to_string()), + ty: ty_mat4x4f, + binding: None, + offset: 0, + }, + crate::StructMember { + name: Some("sample_transform".to_string()), + ty: ty_mat3x2f, + binding: None, + offset: 64, + }, + crate::StructMember { + name: Some("load_transform".to_string()), + ty: ty_mat3x2f, + binding: None, + offset: 88, + }, + crate::StructMember { + name: Some("size".to_string()), + ty: ty_vec2u, + binding: None, + offset: 112, + }, + crate::StructMember { + name: Some("num_planes".to_string()), + ty: ty_u32, + binding: None, + offset: 120, + }, + ], + span: 128, + }, + }, + Span::UNDEFINED, + ); + + self.special_types.external_texture_params = Some(handle); + handle + } + /// Populate this module's [`SpecialTypes::predeclared_types`] type and return the handle. /// /// [`SpecialTypes::predeclared_types`]: crate::SpecialTypes::predeclared_types diff --git a/naga/src/front/wgsl/lower/mod.rs b/naga/src/front/wgsl/lower/mod.rs index c8545ca654a..3b6e7806c32 100644 --- a/naga/src/front/wgsl/lower/mod.rs +++ b/naga/src/front/wgsl/lower/mod.rs @@ -3983,11 +3983,29 @@ impl<'source, 'temp> Lowerer<'source, 'temp> { dim, arrayed, class, - } => ir::TypeInner::Image { - dim, - arrayed, - class, - }, + } => { + if class == crate::ImageClass::External { + // Other than the WGSL backend, every backend that supports + // external textures does so by lowering them to a set of + // ordinary textures and some parameters saying how to + // sample from them. We don't know which backend will + // consume the `Module` we're building, but in case it's not + // WGSL, populate `SpecialTypes::external_texture_params` + // with the type the backend will use for the parameter + // buffer. + // + // This is *not* the type we are lowering here: that's an + // ordinary `TypeInner::Image`. But the fact we are + // lowering a `texture_external` implies the backends may + // need `SpecialTypes::external_texture_params` too. + ctx.module.generate_external_texture_params_type(); + } + ir::TypeInner::Image { + dim, + arrayed, + class, + } + } ast::Type::Sampler { comparison } => ir::TypeInner::Sampler { comparison }, ast::Type::AccelerationStructure { vertex_return } => { ir::TypeInner::AccelerationStructure { vertex_return } diff --git a/naga/src/ir/mod.rs b/naga/src/ir/mod.rs index 0101954849f..1744ff92fcd 100644 --- a/naga/src/ir/mod.rs +++ b/naga/src/ir/mod.rs @@ -2346,6 +2346,28 @@ pub struct SpecialTypes { /// Call [`Module::generate_vertex_return_type`] pub ray_vertex_return: Option>, + /// Struct containing parameters required by some backends to emit code for + /// [`ImageClass::External`] textures. + /// + /// See `wgpu_core::device::resource::ExternalTextureParams` for the + /// documentation of each field. + /// + /// In WGSL, this type would be: + /// + /// ```ignore + /// struct NagaExternalTextureParams { // align size offset + /// yuv_conversion_matrix: mat4x4, // 16 64 0 + /// sample_transform: mat3x2, // 8 24 64 + /// load_transform: mat3x2, // 8 24 88 + /// size: vec2, // 8 8 112 + /// num_planes: u32, // 4 4 120 + /// } // whole struct: 16 128 + /// ``` + /// + /// Call [`Module::generate_external_texture_params_type`] to populate this + /// if needed and return the handle. + pub external_texture_params: Option>, + /// Types for predeclared wgsl types instantiated on demand. /// /// Call [`Module::generate_predeclared_type`] to populate this if diff --git a/naga/tests/out/ir/spv-fetch_depth.compact.ron b/naga/tests/out/ir/spv-fetch_depth.compact.ron index 024f918022c..c2b7b9b5f6e 100644 --- a/naga/tests/out/ir/spv-fetch_depth.compact.ron +++ b/naga/tests/out/ir/spv-fetch_depth.compact.ron @@ -67,6 +67,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [ diff --git a/naga/tests/out/ir/spv-fetch_depth.ron b/naga/tests/out/ir/spv-fetch_depth.ron index 8daa29847ff..dd019d62aa8 100644 --- a/naga/tests/out/ir/spv-fetch_depth.ron +++ b/naga/tests/out/ir/spv-fetch_depth.ron @@ -130,6 +130,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [ diff --git a/naga/tests/out/ir/spv-shadow.compact.ron b/naga/tests/out/ir/spv-shadow.compact.ron index 7f4e6df5c1a..04547e651ce 100644 --- a/naga/tests/out/ir/spv-shadow.compact.ron +++ b/naga/tests/out/ir/spv-shadow.compact.ron @@ -155,6 +155,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [ diff --git a/naga/tests/out/ir/spv-shadow.ron b/naga/tests/out/ir/spv-shadow.ron index 35a9e48befc..0babda31f58 100644 --- a/naga/tests/out/ir/spv-shadow.ron +++ b/naga/tests/out/ir/spv-shadow.ron @@ -278,6 +278,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [ diff --git a/naga/tests/out/ir/spv-spec-constants.compact.ron b/naga/tests/out/ir/spv-spec-constants.compact.ron index 698a8072584..a07dd0aca04 100644 --- a/naga/tests/out/ir/spv-spec-constants.compact.ron +++ b/naga/tests/out/ir/spv-spec-constants.compact.ron @@ -171,6 +171,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [ diff --git a/naga/tests/out/ir/spv-spec-constants.ron b/naga/tests/out/ir/spv-spec-constants.ron index 5afd71ed657..643d3c7303c 100644 --- a/naga/tests/out/ir/spv-spec-constants.ron +++ b/naga/tests/out/ir/spv-spec-constants.ron @@ -262,6 +262,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [ diff --git a/naga/tests/out/ir/wgsl-access.compact.ron b/naga/tests/out/ir/wgsl-access.compact.ron index 5e15ff84caf..fff8d0bcf9b 100644 --- a/naga/tests/out/ir/wgsl-access.compact.ron +++ b/naga/tests/out/ir/wgsl-access.compact.ron @@ -421,6 +421,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-access.ron b/naga/tests/out/ir/wgsl-access.ron index 5e15ff84caf..fff8d0bcf9b 100644 --- a/naga/tests/out/ir/wgsl-access.ron +++ b/naga/tests/out/ir/wgsl-access.ron @@ -421,6 +421,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-collatz.compact.ron b/naga/tests/out/ir/wgsl-collatz.compact.ron index ce0d6d8c89b..30168b26292 100644 --- a/naga/tests/out/ir/wgsl-collatz.compact.ron +++ b/naga/tests/out/ir/wgsl-collatz.compact.ron @@ -44,6 +44,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-collatz.ron b/naga/tests/out/ir/wgsl-collatz.ron index ce0d6d8c89b..30168b26292 100644 --- a/naga/tests/out/ir/wgsl-collatz.ron +++ b/naga/tests/out/ir/wgsl-collatz.ron @@ -44,6 +44,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-const_assert.compact.ron b/naga/tests/out/ir/wgsl-const_assert.compact.ron index c10c9f97d32..4d77a57494d 100644 --- a/naga/tests/out/ir/wgsl-const_assert.compact.ron +++ b/naga/tests/out/ir/wgsl-const_assert.compact.ron @@ -4,6 +4,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-const_assert.ron b/naga/tests/out/ir/wgsl-const_assert.ron index c10c9f97d32..4d77a57494d 100644 --- a/naga/tests/out/ir/wgsl-const_assert.ron +++ b/naga/tests/out/ir/wgsl-const_assert.ron @@ -4,6 +4,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-diagnostic-filter.compact.ron b/naga/tests/out/ir/wgsl-diagnostic-filter.compact.ron index dc4d2defdbd..10f533f1059 100644 --- a/naga/tests/out/ir/wgsl-diagnostic-filter.compact.ron +++ b/naga/tests/out/ir/wgsl-diagnostic-filter.compact.ron @@ -4,6 +4,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-diagnostic-filter.ron b/naga/tests/out/ir/wgsl-diagnostic-filter.ron index dc4d2defdbd..10f533f1059 100644 --- a/naga/tests/out/ir/wgsl-diagnostic-filter.ron +++ b/naga/tests/out/ir/wgsl-diagnostic-filter.ron @@ -4,6 +4,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-index-by-value.compact.ron b/naga/tests/out/ir/wgsl-index-by-value.compact.ron index 8f3de4f3e2e..31a60105410 100644 --- a/naga/tests/out/ir/wgsl-index-by-value.compact.ron +++ b/naga/tests/out/ir/wgsl-index-by-value.compact.ron @@ -81,6 +81,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-index-by-value.ron b/naga/tests/out/ir/wgsl-index-by-value.ron index 8f3de4f3e2e..31a60105410 100644 --- a/naga/tests/out/ir/wgsl-index-by-value.ron +++ b/naga/tests/out/ir/wgsl-index-by-value.ron @@ -81,6 +81,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-local-const.compact.ron b/naga/tests/out/ir/wgsl-local-const.compact.ron index 8bce0bb0084..b7deb55aca5 100644 --- a/naga/tests/out/ir/wgsl-local-const.compact.ron +++ b/naga/tests/out/ir/wgsl-local-const.compact.ron @@ -26,6 +26,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [ diff --git a/naga/tests/out/ir/wgsl-local-const.ron b/naga/tests/out/ir/wgsl-local-const.ron index 8bce0bb0084..b7deb55aca5 100644 --- a/naga/tests/out/ir/wgsl-local-const.ron +++ b/naga/tests/out/ir/wgsl-local-const.ron @@ -26,6 +26,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [ diff --git a/naga/tests/out/ir/wgsl-must-use.compact.ron b/naga/tests/out/ir/wgsl-must-use.compact.ron index 0534e736397..4d148b9061d 100644 --- a/naga/tests/out/ir/wgsl-must-use.compact.ron +++ b/naga/tests/out/ir/wgsl-must-use.compact.ron @@ -12,6 +12,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-must-use.ron b/naga/tests/out/ir/wgsl-must-use.ron index 0534e736397..4d148b9061d 100644 --- a/naga/tests/out/ir/wgsl-must-use.ron +++ b/naga/tests/out/ir/wgsl-must-use.ron @@ -12,6 +12,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-overrides-atomicCompareExchangeWeak.compact.ron b/naga/tests/out/ir/wgsl-overrides-atomicCompareExchangeWeak.compact.ron index ac45d5bad6a..1e568ebbc3d 100644 --- a/naga/tests/out/ir/wgsl-overrides-atomicCompareExchangeWeak.compact.ron +++ b/naga/tests/out/ir/wgsl-overrides-atomicCompareExchangeWeak.compact.ron @@ -53,6 +53,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: { AtomicCompareExchangeWeakResult(( kind: Uint, diff --git a/naga/tests/out/ir/wgsl-overrides-atomicCompareExchangeWeak.ron b/naga/tests/out/ir/wgsl-overrides-atomicCompareExchangeWeak.ron index ac45d5bad6a..1e568ebbc3d 100644 --- a/naga/tests/out/ir/wgsl-overrides-atomicCompareExchangeWeak.ron +++ b/naga/tests/out/ir/wgsl-overrides-atomicCompareExchangeWeak.ron @@ -53,6 +53,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: { AtomicCompareExchangeWeakResult(( kind: Uint, diff --git a/naga/tests/out/ir/wgsl-overrides-ray-query.compact.ron b/naga/tests/out/ir/wgsl-overrides-ray-query.compact.ron index 0848ed2b73c..649ba28d476 100644 --- a/naga/tests/out/ir/wgsl-overrides-ray-query.compact.ron +++ b/naga/tests/out/ir/wgsl-overrides-ray-query.compact.ron @@ -85,6 +85,7 @@ ray_desc: Some(5), ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-overrides-ray-query.ron b/naga/tests/out/ir/wgsl-overrides-ray-query.ron index 0848ed2b73c..649ba28d476 100644 --- a/naga/tests/out/ir/wgsl-overrides-ray-query.ron +++ b/naga/tests/out/ir/wgsl-overrides-ray-query.ron @@ -85,6 +85,7 @@ ray_desc: Some(5), ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-overrides.compact.ron b/naga/tests/out/ir/wgsl-overrides.compact.ron index 4c40ef32de7..eca51561070 100644 --- a/naga/tests/out/ir/wgsl-overrides.compact.ron +++ b/naga/tests/out/ir/wgsl-overrides.compact.ron @@ -26,6 +26,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-overrides.ron b/naga/tests/out/ir/wgsl-overrides.ron index 4c40ef32de7..eca51561070 100644 --- a/naga/tests/out/ir/wgsl-overrides.ron +++ b/naga/tests/out/ir/wgsl-overrides.ron @@ -26,6 +26,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-storage-textures.compact.ron b/naga/tests/out/ir/wgsl-storage-textures.compact.ron index 4f1325da561..eb70e3badd2 100644 --- a/naga/tests/out/ir/wgsl-storage-textures.compact.ron +++ b/naga/tests/out/ir/wgsl-storage-textures.compact.ron @@ -71,6 +71,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-storage-textures.ron b/naga/tests/out/ir/wgsl-storage-textures.ron index 4f1325da561..eb70e3badd2 100644 --- a/naga/tests/out/ir/wgsl-storage-textures.ron +++ b/naga/tests/out/ir/wgsl-storage-textures.ron @@ -71,6 +71,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-template-list-trailing-comma.compact.ron b/naga/tests/out/ir/wgsl-template-list-trailing-comma.compact.ron index 67ec4ffb52d..bfcd83f7f35 100644 --- a/naga/tests/out/ir/wgsl-template-list-trailing-comma.compact.ron +++ b/naga/tests/out/ir/wgsl-template-list-trailing-comma.compact.ron @@ -28,6 +28,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-template-list-trailing-comma.ron b/naga/tests/out/ir/wgsl-template-list-trailing-comma.ron index 67ec4ffb52d..bfcd83f7f35 100644 --- a/naga/tests/out/ir/wgsl-template-list-trailing-comma.ron +++ b/naga/tests/out/ir/wgsl-template-list-trailing-comma.ron @@ -28,6 +28,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [], diff --git a/naga/tests/out/ir/wgsl-texture-external.compact.ron b/naga/tests/out/ir/wgsl-texture-external.compact.ron index 4c0876618da..2bceb18a51f 100644 --- a/naga/tests/out/ir/wgsl-texture-external.compact.ron +++ b/naga/tests/out/ir/wgsl-texture-external.compact.ron @@ -2,34 +2,101 @@ types: [ ( name: None, - inner: Image( - dim: D2, - arrayed: false, - class: External, + inner: Scalar(( + kind: Uint, + width: 4, + )), + ), + ( + name: None, + inner: Vector( + size: Bi, + scalar: ( + kind: Uint, + width: 4, + ), ), ), ( name: None, - inner: Sampler( - comparison: false, + inner: Matrix( + columns: Tri, + rows: Bi, + scalar: ( + kind: Float, + width: 4, + ), ), ), ( name: None, - inner: Vector( - size: Quad, + inner: Matrix( + columns: Quad, + rows: Quad, scalar: ( kind: Float, width: 4, ), ), ), + ( + name: Some("NagaExternalTextureParams"), + inner: Struct( + members: [ + ( + name: Some("yuv_conversion_matrix"), + ty: 3, + binding: None, + offset: 0, + ), + ( + name: Some("sample_transform"), + ty: 2, + binding: None, + offset: 64, + ), + ( + name: Some("load_transform"), + ty: 2, + binding: None, + offset: 88, + ), + ( + name: Some("size"), + ty: 1, + binding: None, + offset: 112, + ), + ( + name: Some("num_planes"), + ty: 0, + binding: None, + offset: 120, + ), + ], + span: 128, + ), + ), + ( + name: None, + inner: Image( + dim: D2, + arrayed: false, + class: External, + ), + ), + ( + name: None, + inner: Sampler( + comparison: false, + ), + ), ( name: None, inner: Vector( - size: Bi, + size: Quad, scalar: ( - kind: Uint, + kind: Float, width: 4, ), ), @@ -39,6 +106,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: Some(4), predeclared_types: {}, ), constants: [], @@ -51,7 +119,7 @@ group: 0, binding: 0, )), - ty: 0, + ty: 5, init: None, ), ( @@ -61,7 +129,7 @@ group: 0, binding: 1, )), - ty: 1, + ty: 6, init: None, ), ], @@ -72,28 +140,28 @@ arguments: [ ( name: Some("t"), - ty: 0, + ty: 5, binding: None, ), ], result: Some(( - ty: 2, + ty: 7, binding: None, )), local_variables: [ ( name: Some("a"), - ty: 2, + ty: 7, init: None, ), ( name: Some("b"), - ty: 2, + ty: 7, init: None, ), ( name: Some("c"), - ty: 3, + ty: 1, init: None, ), ], @@ -217,7 +285,7 @@ name: Some("fragment_main"), arguments: [], result: Some(( - ty: 2, + ty: 7, binding: Some(Location( location: 0, interpolation: Some(Perspective), @@ -256,7 +324,7 @@ name: Some("vertex_main"), arguments: [], result: Some(( - ty: 2, + ty: 7, binding: Some(BuiltIn(Position( invariant: false, ))), diff --git a/naga/tests/out/ir/wgsl-texture-external.ron b/naga/tests/out/ir/wgsl-texture-external.ron index 4c0876618da..2bceb18a51f 100644 --- a/naga/tests/out/ir/wgsl-texture-external.ron +++ b/naga/tests/out/ir/wgsl-texture-external.ron @@ -2,34 +2,101 @@ types: [ ( name: None, - inner: Image( - dim: D2, - arrayed: false, - class: External, + inner: Scalar(( + kind: Uint, + width: 4, + )), + ), + ( + name: None, + inner: Vector( + size: Bi, + scalar: ( + kind: Uint, + width: 4, + ), ), ), ( name: None, - inner: Sampler( - comparison: false, + inner: Matrix( + columns: Tri, + rows: Bi, + scalar: ( + kind: Float, + width: 4, + ), ), ), ( name: None, - inner: Vector( - size: Quad, + inner: Matrix( + columns: Quad, + rows: Quad, scalar: ( kind: Float, width: 4, ), ), ), + ( + name: Some("NagaExternalTextureParams"), + inner: Struct( + members: [ + ( + name: Some("yuv_conversion_matrix"), + ty: 3, + binding: None, + offset: 0, + ), + ( + name: Some("sample_transform"), + ty: 2, + binding: None, + offset: 64, + ), + ( + name: Some("load_transform"), + ty: 2, + binding: None, + offset: 88, + ), + ( + name: Some("size"), + ty: 1, + binding: None, + offset: 112, + ), + ( + name: Some("num_planes"), + ty: 0, + binding: None, + offset: 120, + ), + ], + span: 128, + ), + ), + ( + name: None, + inner: Image( + dim: D2, + arrayed: false, + class: External, + ), + ), + ( + name: None, + inner: Sampler( + comparison: false, + ), + ), ( name: None, inner: Vector( - size: Bi, + size: Quad, scalar: ( - kind: Uint, + kind: Float, width: 4, ), ), @@ -39,6 +106,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: Some(4), predeclared_types: {}, ), constants: [], @@ -51,7 +119,7 @@ group: 0, binding: 0, )), - ty: 0, + ty: 5, init: None, ), ( @@ -61,7 +129,7 @@ group: 0, binding: 1, )), - ty: 1, + ty: 6, init: None, ), ], @@ -72,28 +140,28 @@ arguments: [ ( name: Some("t"), - ty: 0, + ty: 5, binding: None, ), ], result: Some(( - ty: 2, + ty: 7, binding: None, )), local_variables: [ ( name: Some("a"), - ty: 2, + ty: 7, init: None, ), ( name: Some("b"), - ty: 2, + ty: 7, init: None, ), ( name: Some("c"), - ty: 3, + ty: 1, init: None, ), ], @@ -217,7 +285,7 @@ name: Some("fragment_main"), arguments: [], result: Some(( - ty: 2, + ty: 7, binding: Some(Location( location: 0, interpolation: Some(Perspective), @@ -256,7 +324,7 @@ name: Some("vertex_main"), arguments: [], result: Some(( - ty: 2, + ty: 7, binding: Some(BuiltIn(Position( invariant: false, ))), diff --git a/naga/tests/out/ir/wgsl-types_with_comments.compact.ron b/naga/tests/out/ir/wgsl-types_with_comments.compact.ron index dde1bd367b0..1139fab1976 100644 --- a/naga/tests/out/ir/wgsl-types_with_comments.compact.ron +++ b/naga/tests/out/ir/wgsl-types_with_comments.compact.ron @@ -37,6 +37,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [ diff --git a/naga/tests/out/ir/wgsl-types_with_comments.ron b/naga/tests/out/ir/wgsl-types_with_comments.ron index 3a23c49c4f2..a1761c17c8e 100644 --- a/naga/tests/out/ir/wgsl-types_with_comments.ron +++ b/naga/tests/out/ir/wgsl-types_with_comments.ron @@ -62,6 +62,7 @@ ray_desc: None, ray_intersection: None, ray_vertex_return: None, + external_texture_params: None, predeclared_types: {}, ), constants: [ diff --git a/wgpu-core/src/device/resource.rs b/wgpu-core/src/device/resource.rs index ed03fce774b..3f212f80eef 100644 --- a/wgpu-core/src/device/resource.rs +++ b/wgpu-core/src/device/resource.rs @@ -85,9 +85,18 @@ pub struct ExternalTextureParams { /// 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. + /// 3x2 column-major matrix with which to multiply normalized texture + /// coordinates prior to sampling from the external texture. This may + /// scale, translate, flip, and rotate in 90-degree increments, but the + /// result of transforming the rectangle (0,0)..(1,1) must be an + /// axis-aligned rectangle that falls within the bounds of (0,0)..(1,1). pub sample_transform: [f32; 6], + /// 3x2 column-major matrix with which to multiply unnormalized texture + /// coordinates prior to loading from the external texture. This may scale, + /// translate, flip, and rotate in 90-degree increments, but the result of + /// transforming the rectangle (0,0)..(texture_size - 1) must be an + /// axis-aligned rectangle that falls within the bounds of + /// (0,0)..(texture_size - 1). pub load_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 From 5f0c8fe1b1072dac5d41a7c7c6f616d1a2bae6a7 Mon Sep 17 00:00:00 2001 From: Jamie Nicol Date: Mon, 2 Jun 2025 17:21:23 +0100 Subject: [PATCH 2/4] [naga] Reserve names for each plane and params buffer of external texture Adds new `NameKey` variants `ExternalTextureGlobalVariable` and `ExternalTextureFunctionArgument`, like their non-external-texture cousins but additionally keyed by either being a specific plane index or params buffer. For each external texture global variable or function argument reserve additional names for 3 planes and the params buffer. For Naga backends which must represent external textures as multiple variables/arguments, this will allow them to uniquely name each one. --- naga/src/back/mod.rs | 25 +++++++++++++ naga/src/proc/mod.rs | 2 +- naga/src/proc/namer.rs | 83 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/naga/src/back/mod.rs b/naga/src/back/mod.rs index d7b14475bff..0d13d63dd9b 100644 --- a/naga/src/back/mod.rs +++ b/naga/src/back/mod.rs @@ -196,6 +196,31 @@ impl FunctionCtx<'_> { } } + /// Helper method that generates a [`NameKey`](crate::proc::NameKey) for an external texture + /// function argument. + /// + /// # Panics + /// - If the function arguments are less or equal to `arg` + /// - If `self.ty` is not `FunctionType::Function`. + pub const fn external_texture_argument_key( + &self, + arg: u32, + external_texture_key: crate::proc::ExternalTextureNameKey, + ) -> crate::proc::NameKey { + match self.ty { + FunctionType::Function(handle) => { + crate::proc::NameKey::ExternalTextureFunctionArgument( + handle, + arg, + external_texture_key, + ) + } + FunctionType::EntryPoint(_) => { + panic!("External textures cannot be used as arguments to entry points") + } + } + } + /// Returns true if the given expression points to a fixed-function pipeline input. pub fn is_fixed_function_input( &self, diff --git a/naga/src/proc/mod.rs b/naga/src/proc/mod.rs index f2584c64b3d..413e49c1eed 100644 --- a/naga/src/proc/mod.rs +++ b/naga/src/proc/mod.rs @@ -18,7 +18,7 @@ pub use constant_evaluator::{ pub use emitter::Emitter; pub use index::{BoundsCheckPolicies, BoundsCheckPolicy, IndexableLength, IndexableLengthError}; pub use layouter::{Alignment, LayoutError, LayoutErrorInner, Layouter, TypeLayout}; -pub use namer::{EntryPointIndex, NameKey, Namer}; +pub use namer::{EntryPointIndex, ExternalTextureNameKey, NameKey, Namer}; pub use overloads::{Conclusion, MissingSpecialType, OverloadSet, Rule}; pub use terminator::ensure_block_returns; use thiserror::Error; diff --git a/naga/src/proc/namer.rs b/naga/src/proc/namer.rs index 504617dd1d3..0b812ff036a 100644 --- a/naga/src/proc/namer.rs +++ b/naga/src/proc/namer.rs @@ -15,6 +15,40 @@ use crate::{arena::Handle, FastHashMap, FastHashSet}; pub type EntryPointIndex = u16; const SEPARATOR: char = '_'; +/// A component of a lowered external texture. +/// +/// Whereas the WGSL backend implements [`ImageClass::External`] +/// images directly, most other Naga backends lower them to a +/// collection of ordinary textures that represent individual planes +/// (as received from a video decoder, perhaps), together with a +/// struct of parameters saying how they should be cropped, sampled, +/// and color-converted. +/// +/// This lowering means that individual globals and function +/// parameters in Naga IR must be split out by the backends into +/// collections of globals and parameters of simpler types. +/// +/// A value of this enum serves as a name key for one specific +/// component in the lowered representation of an external texture. +/// That is, these keys are for variables/parameters that do not exist +/// in the Naga IR, only in its lowered form. +/// +/// [`ImageClass::External`]: crate::ir::ImageClass::External +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum ExternalTextureNameKey { + Plane(usize), + Params, +} + +impl ExternalTextureNameKey { + const ALL: &[(&str, ExternalTextureNameKey)] = &[ + ("_plane0", ExternalTextureNameKey::Plane(0)), + ("_plane1", ExternalTextureNameKey::Plane(1)), + ("_plane2", ExternalTextureNameKey::Plane(2)), + ("_params", ExternalTextureNameKey::Params), + ]; +} + #[derive(Debug, Eq, Hash, PartialEq)] pub enum NameKey { Constant(Handle), @@ -37,6 +71,17 @@ pub enum NameKey { /// Entry point version of `FunctionOobLocal`. EntryPointOobLocal(EntryPointIndex, Handle), + + /// A global variable holding a component of a lowered external texture. + /// + /// See [`ExternalTextureNameKey`] for details. + ExternalTextureGlobalVariable(Handle, ExternalTextureNameKey), + + /// A function argument holding a component of a lowered external + /// texture. + /// + /// See [`ExternalTextureNameKey`] for details. + ExternalTextureFunctionArgument(Handle, u32, ExternalTextureNameKey), } /// This processor assigns names to all the things in a module @@ -272,6 +317,27 @@ impl Namer { for (index, arg) in fun.arguments.iter().enumerate() { let name = self.call_or(&arg.name, "param"); output.insert(NameKey::FunctionArgument(fun_handle, index as u32), name); + + if matches!( + module.types[arg.ty].inner, + crate::TypeInner::Image { + class: crate::ImageClass::External, + .. + } + ) { + let base = arg.name.as_deref().unwrap_or("param"); + for &(suffix, ext_key) in ExternalTextureNameKey::ALL { + let name = self.call(&format!("{base}_{suffix}")); + output.insert( + NameKey::ExternalTextureFunctionArgument( + fun_handle, + index as u32, + ext_key, + ), + name, + ); + } + } } for (handle, var) in fun.local_variables.iter() { let name = self.call_or(&var.name, "local"); @@ -282,6 +348,23 @@ impl Namer { for (handle, var) in module.global_variables.iter() { let name = self.call_or(&var.name, "global"); output.insert(NameKey::GlobalVariable(handle), name); + + if matches!( + module.types[var.ty].inner, + crate::TypeInner::Image { + class: crate::ImageClass::External, + .. + } + ) { + let base = var.name.as_deref().unwrap_or("global"); + for &(suffix, ext_key) in ExternalTextureNameKey::ALL { + let name = self.call(&format!("{base}_{suffix}")); + output.insert( + NameKey::ExternalTextureGlobalVariable(handle, ext_key), + name, + ); + } + } } for (handle, constant) in module.constants.iter() { From 32583929dcc0c2c07410ecbbdc2a2078db918bc8 Mon Sep 17 00:00:00 2001 From: Jamie Nicol Date: Mon, 2 Jun 2025 17:21:23 +0100 Subject: [PATCH 3/4] [naga hlsl-out] Implement external texture support This adds HLSL backend support for `ImageClass::External` (ie WGSL's `external_texture` texture type). For each external texture global variable in the IR, we declare 3 `Texture2D` globals as well as a `cbuffer` for the params. The additional bindings required by these are found in the newly added `external_texture_binding_map`. Unique names for each can be obtained using `NameKey::ExternalTextureGlobalVariable`. For functions that contain ImageQuery::Size, ImageLoad, or ImageSample expressions for external textures, ensure we have generated wrapper functions for those expressions. When emitting code for the expressions themselves, simply insert a call to the wrapper function. For size queries, we return the value provided in the params struct. If that value is [0, 0] then we query the size of the plane 0 texture and return that. For load and sample, we sample the textures based on the number of planes specified in the params struct. If there is more than one plane we additionally perform YUV to RGB conversion using the provided matrix. Unfortunately HLSL does not allow structs to contain textures, meaning we are unable to wrap the 3 textures and params struct variables in a single variable that can be passed around. For our wrapper functions we therefore ensure they take the three textures and the params as consecutive arguments. Likewise, when declaring user-defined functions with external texture arguments, we expand the single external texture argument into 4 consecutive arguments. (Using NameKey::ExternalTextureFunctionArgument to ensure unique names for each.) Thankfully external textures can only be used as either global variables or function arguments. This means we only have to handle the `Expression::GlobalVariable` and `Expression::FunctionArgument` cases of `write_expr()`. Since in both cases we know the external texture can only be an argument to either a user-defined function or one of our wrapper functions, we can simply emit the names of the variables for each three textures and the params struct in a comma-separated list. --- naga-cli/src/bin/naga.rs | 1 + naga/src/back/hlsl/help.rs | 468 ++++++++++++++---- naga/src/back/hlsl/keywords.rs | 2 + naga/src/back/hlsl/mod.rs | 185 ++++++- naga/src/back/hlsl/writer.rs | 265 ++++++++-- naga/src/ir/mod.rs | 14 + naga/src/valid/type.rs | 9 + naga/tests/in/wgsl/texture-external.toml | 15 +- .../tests/out/hlsl/wgsl-texture-external.hlsl | 143 ++++++ naga/tests/out/hlsl/wgsl-texture-external.ron | 20 + wgpu-core/src/device/resource.rs | 59 ++- wgpu-hal/src/dx12/device.rs | 1 + wgpu-types/src/lib.rs | 56 ++- 13 files changed, 1090 insertions(+), 148 deletions(-) create mode 100644 naga/tests/out/hlsl/wgsl-texture-external.hlsl create mode 100644 naga/tests/out/hlsl/wgsl-texture-external.ron diff --git a/naga-cli/src/bin/naga.rs b/naga-cli/src/bin/naga.rs index b018058613c..44369e9df7d 100644 --- a/naga-cli/src/bin/naga.rs +++ b/naga-cli/src/bin/naga.rs @@ -539,6 +539,7 @@ fn run() -> anyhow::Result<()> { 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::TEXTURE_EXTERNAL, + Some("hlsl") => C::empty(), _ => C::TEXTURE_EXTERNAL, }; caps & !missing diff --git a/naga/src/back/hlsl/help.rs b/naga/src/back/hlsl/help.rs index bea917547b5..70c0a692a33 100644 --- a/naga/src/back/hlsl/help.rs +++ b/naga/src/back/hlsl/help.rs @@ -33,8 +33,8 @@ use super::{ super::FunctionCtx, writer::{ ABS_FUNCTION, DIV_FUNCTION, EXTRACT_BITS_FUNCTION, F2I32_FUNCTION, F2I64_FUNCTION, - F2U32_FUNCTION, F2U64_FUNCTION, IMAGE_SAMPLE_BASE_CLAMP_TO_EDGE_FUNCTION, - INSERT_BITS_FUNCTION, MOD_FUNCTION, NEG_FUNCTION, + F2U32_FUNCTION, F2U64_FUNCTION, IMAGE_LOAD_EXTERNAL_FUNCTION, + IMAGE_SAMPLE_BASE_CLAMP_TO_EDGE_FUNCTION, INSERT_BITS_FUNCTION, MOD_FUNCTION, NEG_FUNCTION, }, BackendResult, WrappedType, }; @@ -45,8 +45,14 @@ pub(super) struct WrappedArrayLength { pub(super) writable: bool, } +#[derive(Clone, Copy, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)] +pub(super) struct WrappedImageLoad { + pub(super) class: crate::ImageClass, +} + #[derive(Clone, Copy, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)] pub(super) struct WrappedImageSample { + pub(super) class: crate::ImageClass, pub(super) clamp_to_edge: bool, } @@ -195,7 +201,11 @@ impl super::Writer<'_, W> { let storage_format_str = format.to_hlsl_str(); write!(self.out, "<{storage_format_str}>")? } - crate::ImageClass::External => unimplemented!(), + crate::ImageClass::External => { + unreachable!( + "external images should be handled by `write_global_external_texture`" + ); + } } Ok(()) } @@ -254,12 +264,236 @@ impl super::Writer<'_, W> { Ok(()) } + pub(super) fn write_wrapped_image_load_function( + &mut self, + module: &crate::Module, + load: WrappedImageLoad, + ) -> BackendResult { + match load { + WrappedImageLoad { + class: crate::ImageClass::External, + } => { + let l1 = crate::back::Level(1); + let l2 = l1.next(); + let l3 = l2.next(); + let params_ty_name = &self.names + [&NameKey::Type(module.special_types.external_texture_params.unwrap())]; + writeln!(self.out, "float4 {IMAGE_LOAD_EXTERNAL_FUNCTION}(")?; + writeln!(self.out, "{l1}Texture2D plane0,")?; + writeln!(self.out, "{l1}Texture2D plane1,")?; + writeln!(self.out, "{l1}Texture2D plane2,")?; + writeln!(self.out, "{l1}{params_ty_name} params,")?; + writeln!(self.out, "{l1}uint2 coords)")?; + writeln!(self.out, "{{")?; + writeln!(self.out, "{l1}uint2 plane0_size;")?; + writeln!( + self.out, + "{l1}plane0.GetDimensions(plane0_size.x, plane0_size.y);" + )?; + // Clamp coords to provided size of external texture to prevent OOB read. + // If params.size is zero then clamp to the actual size of the texture. + writeln!( + self.out, + "{l1}uint2 cropped_size = any(params.size) ? params.size : plane0_size;" + )?; + writeln!(self.out, "{l1}coords = min(coords, cropped_size - 1);")?; + + // Apply load transformation. We declare our matrices as row_major in + // HLSL, therefore we must reverse the order of this multiplication + writeln!(self.out, "{l1}float3x2 load_transform = float3x2(")?; + writeln!(self.out, "{l2}params.load_transform_0,")?; + writeln!(self.out, "{l2}params.load_transform_1,")?; + writeln!(self.out, "{l2}params.load_transform_2")?; + writeln!(self.out, "{l1});")?; + writeln!(self.out, "{l1}uint2 plane0_coords = uint2(round(mul(float3(coords, 1.0), load_transform)));")?; + writeln!(self.out, "{l1}if (params.num_planes == 1u) {{")?; + // For single plane, simply read from plane0 + writeln!( + self.out, + "{l2}return plane0.Load(uint3(plane0_coords, 0u));" + )?; + writeln!(self.out, "{l1}}} else {{")?; + + // Chroma planes may be subsampled so we must scale the coords accordingly. + writeln!(self.out, "{l2}uint2 plane1_size;")?; + writeln!( + self.out, + "{l2}plane1.GetDimensions(plane1_size.x, plane1_size.y);" + )?; + writeln!(self.out, "{l2}uint2 plane1_coords = uint2(floor(float2(plane0_coords) * float2(plane1_size) / float2(plane0_size)));")?; + + // For multi-plane, read the Y value from plane 0 + writeln!( + self.out, + "{l2}float y = plane0.Load(uint3(plane0_coords, 0u)).x;" + )?; + + writeln!(self.out, "{l2}float2 uv;")?; + writeln!(self.out, "{l2}if (params.num_planes == 2u) {{")?; + // Read UV from interleaved plane 1 + writeln!( + self.out, + "{l3}uv = plane1.Load(uint3(plane1_coords, 0u)).xy;" + )?; + writeln!(self.out, "{l2}}} else {{")?; + // Read U and V from planes 1 and 2 respectively + writeln!(self.out, "{l3}uint2 plane2_size;")?; + writeln!( + self.out, + "{l3}plane2.GetDimensions(plane2_size.x, plane2_size.y);" + )?; + writeln!(self.out, "{l3}uint2 plane2_coords = uint2(floor(float2(plane0_coords) * float2(plane2_size) / float2(plane0_size)));")?; + writeln!(self.out, "{l3}uv = float2(plane1.Load(uint3(plane1_coords, 0u)).x, plane2.Load(uint3(plane2_coords, 0u)).x);")?; + writeln!(self.out, "{l2}}}")?; + + // Convert from YUV to RGB. We declare our matrices as row_major in HLSL, + // therefore we must reverse the order of this multiplication + writeln!( + self.out, + "{l2}return mul(float4(y, uv, 1.0), params.yuv_conversion_matrix);" + )?; + + writeln!(self.out, "{l1}}}")?; + writeln!(self.out, "}}")?; + writeln!(self.out)?; + } + _ => {} + } + + Ok(()) + } + pub(super) fn write_wrapped_image_sample_function( &mut self, + module: &crate::Module, sample: WrappedImageSample, ) -> BackendResult { match sample { WrappedImageSample { + class: crate::ImageClass::External, + clamp_to_edge: true, + } => { + let l1 = crate::back::Level(1); + let l2 = l1.next(); + let l3 = l2.next(); + let params_ty_name = &self.names + [&NameKey::Type(module.special_types.external_texture_params.unwrap())]; + writeln!( + self.out, + "float4 {IMAGE_SAMPLE_BASE_CLAMP_TO_EDGE_FUNCTION}(" + )?; + writeln!(self.out, "{l1}Texture2D plane0,")?; + writeln!(self.out, "{l1}Texture2D plane1,")?; + writeln!(self.out, "{l1}Texture2D plane2,")?; + writeln!(self.out, "{l1}{params_ty_name} params,")?; + writeln!(self.out, "{l1}SamplerState samp,")?; + writeln!(self.out, "{l1}float2 coords)")?; + writeln!(self.out, "{{")?; + writeln!(self.out, "{l1}float2 plane0_size;")?; + writeln!( + self.out, + "{l1}plane0.GetDimensions(plane0_size.x, plane0_size.y);" + )?; + writeln!(self.out, "{l1}float3x2 sample_transform = float3x2(")?; + writeln!(self.out, "{l2}params.sample_transform_0,")?; + writeln!(self.out, "{l2}params.sample_transform_1,")?; + writeln!(self.out, "{l2}params.sample_transform_2")?; + writeln!(self.out, "{l1});")?; + // Apply sample transformation. We declare our matrices as row_major in + // HLSL, therefore we must reverse the order of this multiplication + writeln!( + self.out, + "{l1}coords = mul(float3(coords, 1.0), sample_transform);" + )?; + // Calculate the sample bounds. The purported size of the texture + // (params.size) is irrelevant here as we are dealing with normalized + // coordinates. Usually we would clamp to (0,0)..(1,1). However, we must + // apply the sample transformation to that, also bearing in mind that it + // may contain a flip on either axis. We calculate and adjust for the + // half-texel separately for each plane as it depends on the actual + // texture size which may vary between planes. + writeln!( + self.out, + "{l1}float2 bounds_min = mul(float3(0.0, 0.0, 1.0), sample_transform);" + )?; + writeln!( + self.out, + "{l1}float2 bounds_max = mul(float3(1.0, 1.0, 1.0), sample_transform);" + )?; + writeln!(self.out, "{l1}float4 bounds = float4(min(bounds_min, bounds_max), max(bounds_min, bounds_max));")?; + writeln!( + self.out, + "{l1}float2 plane0_half_texel = float2(0.5, 0.5) / plane0_size;" + )?; + writeln!( + self.out, + "{l1}float2 plane0_coords = clamp(coords, bounds.xy + plane0_half_texel, bounds.zw - plane0_half_texel);" + )?; + writeln!(self.out, "{l1}if (params.num_planes == 1u) {{")?; + // For single plane, simply sample from plane0 + writeln!( + self.out, + "{l2}return plane0.SampleLevel(samp, plane0_coords, 0.0f);" + )?; + writeln!(self.out, "{l1}}} else {{")?; + + writeln!(self.out, "{l2}float2 plane1_size;")?; + writeln!( + self.out, + "{l2}plane1.GetDimensions(plane1_size.x, plane1_size.y);" + )?; + writeln!( + self.out, + "{l2}float2 plane1_half_texel = float2(0.5, 0.5) / plane1_size;" + )?; + writeln!( + self.out, + "{l2}float2 plane1_coords = clamp(coords, bounds.xy + plane1_half_texel, bounds.zw - plane1_half_texel);" + )?; + + // For multi-plane, sample the Y value from plane 0 + writeln!( + self.out, + "{l2}float y = plane0.SampleLevel(samp, plane0_coords, 0.0f).x;" + )?; + writeln!(self.out, "{l2}float2 uv;")?; + writeln!(self.out, "{l2}if (params.num_planes == 2u) {{")?; + // Sample UV from interleaved plane 1 + writeln!( + self.out, + "{l3}uv = plane1.SampleLevel(samp, plane1_coords, 0.0f).xy;" + )?; + writeln!(self.out, "{l2}}} else {{")?; + // Sample U and V from planes 1 and 2 respectively + writeln!(self.out, "{l3}float2 plane2_size;")?; + writeln!( + self.out, + "{l3}plane2.GetDimensions(plane2_size.x, plane2_size.y);" + )?; + writeln!( + self.out, + "{l3}float2 plane2_half_texel = float2(0.5, 0.5) / plane2_size;" + )?; + writeln!(self.out, "{l3}float2 plane2_coords = clamp(coords, bounds.xy + plane2_half_texel, bounds.zw - plane2_half_texel);")?; + writeln!(self.out, "{l3}uv = float2(plane1.SampleLevel(samp, plane1_coords, 0.0f).x, plane2.SampleLevel(samp, plane2_coords, 0.0f).x);")?; + writeln!(self.out, "{l2}}}")?; + + // Convert from YUV to RGB. We declare our matrices as row_major in HLSL, + // therefore we must reverse the order of this multiplication + writeln!( + self.out, + "{l2}return mul(float4(y, uv, 1.0), params.yuv_conversion_matrix);" + )?; + writeln!(self.out, "{l1}}}")?; + writeln!(self.out, "}}")?; + writeln!(self.out)?; + } + WrappedImageSample { + class: + crate::ImageClass::Sampled { + kind: ScalarKind::Float, + multi: false, + }, clamp_to_edge: true, } => { writeln!(self.out, "float4 {IMAGE_SAMPLE_BASE_CLAMP_TO_EDGE_FUNCTION}(Texture2D tex, SamplerState samp, float2 coords) {{")?; @@ -291,7 +525,7 @@ impl super::Writer<'_, W> { crate::ImageClass::Depth { multi: false } => "Depth", crate::ImageClass::Sampled { multi: false, .. } => "", crate::ImageClass::Storage { .. } => "RW", - crate::ImageClass::External => unimplemented!(), + crate::ImageClass::External => "External", }; let arrayed_str = if query.arrayed { "Array" } else { "" }; let query_str = match query.query { @@ -322,102 +556,133 @@ impl super::Writer<'_, W> { ImageDimension as IDim, }; - const ARGUMENT_VARIABLE_NAME: &str = "tex"; - const RETURN_VARIABLE_NAME: &str = "ret"; - const MIP_LEVEL_PARAM: &str = "mip_level"; - - // Write function return type and name - let ret_ty = func_ctx.resolve_type(expr_handle, &module.types); - self.write_value_type(module, ret_ty)?; - write!(self.out, " ")?; - self.write_wrapped_image_query_function_name(wiq)?; + match wiq.class { + crate::ImageClass::External => { + if wiq.query != ImageQuery::Size { + return Err(super::Error::Custom( + "External images only support `Size` queries".into(), + )); + } - // Write function parameters - write!(self.out, "(")?; - // Texture always first parameter - self.write_image_type(wiq.dim, wiq.arrayed, wiq.class)?; - write!(self.out, " {ARGUMENT_VARIABLE_NAME}")?; - // Mipmap is a second parameter if exists - if let ImageQuery::SizeLevel = wiq.query { - write!(self.out, ", uint {MIP_LEVEL_PARAM}")?; - } - writeln!(self.out, ")")?; + write!(self.out, "uint2 ")?; + self.write_wrapped_image_query_function_name(wiq)?; + let params_name = &self.names + [&NameKey::Type(module.special_types.external_texture_params.unwrap())]; + // Only plane0 and params are used by this implementation, but it's easier to + // always take all of them as arguments so that we can unconditionally expand an + // external texture expression each of its parts. + writeln!(self.out, "(Texture2D plane0, Texture2D plane1, Texture2D plane2, {params_name} params) {{")?; + let l1 = crate::back::Level(1); + let l2 = l1.next(); + writeln!(self.out, "{l1}if (any(params.size)) {{")?; + writeln!(self.out, "{l2}return params.size;")?; + writeln!(self.out, "{l1}}} else {{")?; + // params.size == (0, 0) indicates to query and return plane 0's actual size + writeln!(self.out, "{l2}uint2 ret;")?; + writeln!(self.out, "{l2}plane0.GetDimensions(ret.x, ret.y);")?; + writeln!(self.out, "{l2}return ret;")?; + writeln!(self.out, "{l1}}}")?; + writeln!(self.out, "}}")?; + writeln!(self.out)?; + } + _ => { + const ARGUMENT_VARIABLE_NAME: &str = "tex"; + const RETURN_VARIABLE_NAME: &str = "ret"; + const MIP_LEVEL_PARAM: &str = "mip_level"; + + // Write function return type and name + let ret_ty = func_ctx.resolve_type(expr_handle, &module.types); + self.write_value_type(module, ret_ty)?; + write!(self.out, " ")?; + self.write_wrapped_image_query_function_name(wiq)?; + + // Write function parameters + write!(self.out, "(")?; + // Texture always first parameter + self.write_image_type(wiq.dim, wiq.arrayed, wiq.class)?; + write!(self.out, " {ARGUMENT_VARIABLE_NAME}")?; + // Mipmap is a second parameter if exists + if let ImageQuery::SizeLevel = wiq.query { + write!(self.out, ", uint {MIP_LEVEL_PARAM}")?; + } + writeln!(self.out, ")")?; - // Write function body - writeln!(self.out, "{{")?; + // Write function body + writeln!(self.out, "{{")?; - let array_coords = usize::from(wiq.arrayed); - // extra parameter is the mip level count or the sample count - let extra_coords = match wiq.class { - crate::ImageClass::Storage { .. } => 0, - crate::ImageClass::Sampled { .. } | crate::ImageClass::Depth { .. } => 1, - crate::ImageClass::External => unimplemented!(), - }; + let array_coords = usize::from(wiq.arrayed); + // extra parameter is the mip level count or the sample count + let extra_coords = match wiq.class { + crate::ImageClass::Storage { .. } => 0, + crate::ImageClass::Sampled { .. } | crate::ImageClass::Depth { .. } => 1, + crate::ImageClass::External => unreachable!(), + }; - // GetDimensions Overloaded Methods - // https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-to-getdimensions#overloaded-methods - let (ret_swizzle, number_of_params) = match wiq.query { - ImageQuery::Size | ImageQuery::SizeLevel => { - let ret = match wiq.dim { - IDim::D1 => "x", - IDim::D2 => "xy", - IDim::D3 => "xyz", - IDim::Cube => "xy", + // GetDimensions Overloaded Methods + // https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-to-getdimensions#overloaded-methods + let (ret_swizzle, number_of_params) = match wiq.query { + ImageQuery::Size | ImageQuery::SizeLevel => { + let ret = match wiq.dim { + IDim::D1 => "x", + IDim::D2 => "xy", + IDim::D3 => "xyz", + IDim::Cube => "xy", + }; + (ret, ret.len() + array_coords + extra_coords) + } + ImageQuery::NumLevels | ImageQuery::NumSamples | ImageQuery::NumLayers => { + if wiq.arrayed || wiq.dim == IDim::D3 { + ("w", 4) + } else { + ("z", 3) + } + } }; - (ret, ret.len() + array_coords + extra_coords) - } - ImageQuery::NumLevels | ImageQuery::NumSamples | ImageQuery::NumLayers => { - if wiq.arrayed || wiq.dim == IDim::D3 { - ("w", 4) - } else { - ("z", 3) - } - } - }; - // Write `GetDimensions` function. - writeln!(self.out, "{INDENT}uint4 {RETURN_VARIABLE_NAME};")?; - write!(self.out, "{INDENT}{ARGUMENT_VARIABLE_NAME}.GetDimensions(")?; - match wiq.query { - ImageQuery::SizeLevel => { - write!(self.out, "{MIP_LEVEL_PARAM}, ")?; - } - _ => match wiq.class { - crate::ImageClass::Sampled { multi: true, .. } - | crate::ImageClass::Depth { multi: true } - | crate::ImageClass::Storage { .. } => {} - _ => { - // Write zero mipmap level for supported types - write!(self.out, "0, ")?; + // Write `GetDimensions` function. + writeln!(self.out, "{INDENT}uint4 {RETURN_VARIABLE_NAME};")?; + write!(self.out, "{INDENT}{ARGUMENT_VARIABLE_NAME}.GetDimensions(")?; + match wiq.query { + ImageQuery::SizeLevel => { + write!(self.out, "{MIP_LEVEL_PARAM}, ")?; + } + _ => match wiq.class { + crate::ImageClass::Sampled { multi: true, .. } + | crate::ImageClass::Depth { multi: true } + | crate::ImageClass::Storage { .. } => {} + _ => { + // Write zero mipmap level for supported types + write!(self.out, "0, ")?; + } + }, } - }, - } - for component in COMPONENTS[..number_of_params - 1].iter() { - write!(self.out, "{RETURN_VARIABLE_NAME}.{component}, ")?; - } + for component in COMPONENTS[..number_of_params - 1].iter() { + write!(self.out, "{RETURN_VARIABLE_NAME}.{component}, ")?; + } - // write last parameter without comma and space for last parameter - write!( - self.out, - "{}.{}", - RETURN_VARIABLE_NAME, - COMPONENTS[number_of_params - 1] - )?; + // write last parameter without comma and space for last parameter + write!( + self.out, + "{}.{}", + RETURN_VARIABLE_NAME, + COMPONENTS[number_of_params - 1] + )?; - writeln!(self.out, ");")?; + writeln!(self.out, ");")?; - // Write return value - writeln!( - self.out, - "{INDENT}return {RETURN_VARIABLE_NAME}.{ret_swizzle};" - )?; - - // End of function body - writeln!(self.out, "}}")?; - // Write extra new line - writeln!(self.out)?; + // Write return value + writeln!( + self.out, + "{INDENT}return {RETURN_VARIABLE_NAME}.{ret_swizzle};" + )?; + // End of function body + writeln!(self.out, "}}")?; + // Write extra new line + writeln!(self.out)?; + } + } Ok(()) } @@ -1557,10 +1822,31 @@ impl super::Writer<'_, W> { self.write_wrapped_array_length_function(wal)?; } } - crate::Expression::ImageSample { clamp_to_edge, .. } => { - let wrapped = WrappedImageSample { clamp_to_edge }; + crate::Expression::ImageLoad { image, .. } => { + let class = match *func_ctx.resolve_type(image, &module.types) { + crate::TypeInner::Image { class, .. } => class, + _ => unreachable!(), + }; + let wrapped = WrappedImageLoad { class }; + if self.wrapped.insert(WrappedType::ImageLoad(wrapped)) { + self.write_wrapped_image_load_function(module, wrapped)?; + } + } + crate::Expression::ImageSample { + image, + clamp_to_edge, + .. + } => { + let class = match *func_ctx.resolve_type(image, &module.types) { + crate::TypeInner::Image { class, .. } => class, + _ => unreachable!(), + }; + let wrapped = WrappedImageSample { + class, + clamp_to_edge, + }; if self.wrapped.insert(WrappedType::ImageSample(wrapped)) { - self.write_wrapped_image_sample_function(wrapped)?; + self.write_wrapped_image_sample_function(module, wrapped)?; } } crate::Expression::ImageQuery { image, query } => { diff --git a/naga/src/back/hlsl/keywords.rs b/naga/src/back/hlsl/keywords.rs index f38d47483f7..7b2ab839d4c 100644 --- a/naga/src/back/hlsl/keywords.rs +++ b/naga/src/back/hlsl/keywords.rs @@ -826,6 +826,7 @@ pub const RESERVED: &[&str] = &[ super::writer::INSERT_BITS_FUNCTION, super::writer::SAMPLER_HEAP_VAR, super::writer::COMPARISON_SAMPLER_HEAP_VAR, + super::writer::SAMPLE_EXTERNAL_TEXTURE_FUNCTION, super::writer::ABS_FUNCTION, super::writer::DIV_FUNCTION, super::writer::MOD_FUNCTION, @@ -834,6 +835,7 @@ pub const RESERVED: &[&str] = &[ super::writer::F2U32_FUNCTION, super::writer::F2I64_FUNCTION, super::writer::F2U64_FUNCTION, + super::writer::IMAGE_LOAD_EXTERNAL_FUNCTION, super::writer::IMAGE_SAMPLE_BASE_CLAMP_TO_EDGE_FUNCTION, ]; diff --git a/naga/src/back/hlsl/mod.rs b/naga/src/back/hlsl/mod.rs index 7f553980402..8df06cf1323 100644 --- a/naga/src/back/hlsl/mod.rs +++ b/naga/src/back/hlsl/mod.rs @@ -106,6 +106,37 @@ index buffer for each bind group. This buffer is accessed in the shader to get t sampler index within the heap. See the wgpu_hal dx12 backend documentation for more information. +# External textures + +Support for [`crate::ImageClass::External`] textures is implemented by lowering +each external texture global variable to 3 `Texture2D`s, and a `cbuffer` +of type `NagaExternalTextureParams`. This provides up to 3 planes of texture +data (for example single planar RGBA, or separate Y, Cb, and Cr planes), and the +parameters buffer containing information describing how to handle these +correctly. The bind target to use for each of these globals is specified via +[`Options::external_texture_binding_map`]. + +External textures are supported by WGSL's `textureDimensions()`, +`textureLoad()`, and `textureSampleBaseClampToEdge()` built-in functions. These +are implemented using helper functions. See the following functions for how +these are generated: + * `Writer::write_wrapped_image_query_function` + * `Writer::write_wrapped_image_load_function` + * `Writer::write_wrapped_image_sample_function` + +Ideally the set of global variables could be wrapped in a single struct that +could conveniently be passed around. But, alas, HLSL does not allow structs to +have `Texture2D` members. Fortunately, however, external textures can only be +used as arguments to either built-in or user-defined functions. We therefore +expand any external texture function argument to four consecutive arguments (3 +textures and the params struct) when declaring user-defined functions, and +ensure our built-in function implementations take the same arguments. Then, +whenever we need to emit an external texture in `Writer::write_expr`, which +fortunately can only ever be for a global variable or function argument, we +simply emit the variable name of each of the three textures and the parameters +struct in a comma-separated list. This won't win any awards for elegance, but +it works for our purposes. + [hlsl]: https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl [ilov]: https://gpuweb.github.io/gpuweb/wgsl/#internal-value-layout [16bb]: https://github.com/microsoft/DirectXShaderCompiler/wiki/Buffer-Packing#constant-buffer-packing @@ -126,6 +157,35 @@ use thiserror::Error; use crate::{back, ir, proc}; +/// Direct3D 12 binding information for a global variable. +/// +/// This type provides the HLSL-specific information Naga needs to declare and +/// access an HLSL global variable that cannot be derived from the `Module` +/// itself. +/// +/// An HLSL global variable declaration includes details that the Direct3D API +/// will use to refer to it. For example: +/// +/// RWByteAddressBuffer s_sasm : register(u0, space2); +/// +/// This defines a global `s_sasm` that a Direct3D root signature would refer to +/// as register `0` in register space `2` in a `UAV` descriptor range. Naga can +/// infer the register's descriptor range type from the variable's address class +/// (writable [`Storage`] variables are implemented by Direct3D Unordered Access +/// Views, the `u` register type), but the register number and register space +/// must be supplied by the user. +/// +/// The [`back::hlsl::Options`] structure provides `BindTarget`s for various +/// situations in which Naga may need to generate an HLSL global variable, like +/// [`binding_map`] for Naga global variables, or [`push_constants_target`] for +/// a module's sole [`PushConstant`] variable. See those fields' documentation +/// for details. +/// +/// [`Storage`]: crate::ir::AddressSpace::Storage +/// [`back::hlsl::Options`]: Options +/// [`binding_map`]: Options::binding_map +/// [`push_constants_target`]: Options::push_constants_target +/// [`PushConstant`]: crate::ir::AddressSpace::PushConstant #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serialize", derive(serde::Serialize))] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] @@ -335,6 +395,62 @@ where pub type DynamicStorageBufferOffsetsTargets = alloc::collections::BTreeMap; +/// HLSL binding information for a Naga [`External`] image global variable. +/// +/// See the module documentation's section on [External textures][mod] for details. +/// +/// [`External`]: crate::ir::ImageClass::External +/// [mod]: #external-textures +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize))] +#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] +pub struct ExternalTextureBindTarget { + /// HLSL binding information for the individual plane textures. + /// + /// Each of these should refer to an HLSL `Texture2D` holding one + /// plane of data for the external texture. The exact meaning of each plane + /// varies at runtime depending on where the external texture's data + /// originated. + pub planes: [BindTarget; 3], + + /// HLSL binding information for a buffer holding the sampling parameters. + /// + /// This should refer to a cbuffer of type `NagaExternalTextureParams`, that + /// the code Naga generates for `textureSampleBaseClampToEdge` consults to + /// decide how to combine the data in [`planes`] to get the result required + /// by the spec. + /// + /// [`planes`]: Self::planes + pub params: BindTarget, +} + +#[cfg(any(feature = "serialize", feature = "deserialize"))] +#[cfg_attr(feature = "serialize", derive(serde::Serialize))] +#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] +struct ExternalTextureBindingMapSerialization { + resource_binding: crate::ResourceBinding, + bind_target: ExternalTextureBindTarget, +} + +#[cfg(feature = "deserialize")] +fn deserialize_external_texture_binding_map<'de, D>( + deserializer: D, +) -> Result +where + D: serde::Deserializer<'de>, +{ + use serde::Deserialize; + + let vec = Vec::::deserialize(deserializer)?; + let mut map = ExternalTextureBindingMap::default(); + for item in vec { + map.insert(item.resource_binding, item.bind_target); + } + Ok(map) +} +pub type ExternalTextureBindingMap = + alloc::collections::BTreeMap; + /// Shorthand result used internally by the backend type BackendResult = Result<(), Error>; @@ -354,21 +470,47 @@ pub enum EntryPointError { pub struct Options { /// The hlsl shader model to be used pub shader_model: ShaderModel, - /// Map of resources association to binding locations. + + /// HLSL binding information for each Naga global variable. + /// + /// This maps Naga [`GlobalVariable`]'s [`ResourceBinding`]s to a + /// [`BindTarget`] specifying its register number and space, along with + /// other details necessary to generate a full HLSL declaration for it, + /// or to access its value. + /// + /// This must provide a [`BindTarget`] for every [`GlobalVariable`] in the + /// [`Module`] that has a [`binding`]. + /// + /// [`GlobalVariable`]: crate::ir::GlobalVariable + /// [`ResourceBinding`]: crate::ir::ResourceBinding + /// [`Module`]: crate::ir::Module + /// [`binding`]: crate::ir::GlobalVariable::binding #[cfg_attr( feature = "deserialize", serde(deserialize_with = "deserialize_binding_map") )] pub binding_map: BindingMap, + /// Don't panic on missing bindings, instead generate any HLSL. pub fake_missing_bindings: bool, /// Add special constants to `SV_VertexIndex` and `SV_InstanceIndex`, /// to make them work like in Vulkan/Metal, with help of the host. pub special_constants_binding: Option, - /// Bind target of the push constant buffer + + /// HLSL binding information for the [`PushConstant`] global, if present. + /// + /// If a module contains a global in the [`PushConstant`] address space, the + /// `dx12` backend stores its value directly in the root signature as a + /// series of [`D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS`], whose binding + /// information is given here. + /// + /// [`PushConstant`]: crate::ir::AddressSpace::PushConstant + /// [`D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS`]: https://learn.microsoft.com/en-us/windows/win32/api/d3d12/ne-d3d12-d3d12_root_parameter_type pub push_constants_target: Option, - /// Bind target of the sampler heap and comparison sampler heap. + + /// HLSL binding information for the sampler heap and comparison sampler heap. pub sampler_heap_target: SamplerHeapBindTargets, + /// Mapping of each bind group's sampler index buffer to a bind target. #[cfg_attr( feature = "deserialize", @@ -381,6 +523,18 @@ pub struct Options { serde(deserialize_with = "deserialize_storage_buffer_offsets") )] pub dynamic_storage_buffer_offsets_targets: DynamicStorageBufferOffsetsTargets, + #[cfg_attr( + feature = "deserialize", + serde(deserialize_with = "deserialize_external_texture_binding_map") + )] + + /// HLSL binding information for [`External`] image global variables. + /// + /// See [`ExternalTextureBindTarget`] for details. + /// + /// [`External`]: crate::ir::ImageClass::External + pub external_texture_binding_map: ExternalTextureBindingMap, + /// Should workgroup variables be zero initialized (by polyfilling)? pub zero_initialize_workgroup_memory: bool, /// Should we restrict indexing of vectors, matrices and arrays? @@ -401,6 +555,7 @@ impl Default for Options { sampler_buffer_binding_map: alloc::collections::BTreeMap::default(), push_constants_target: None, dynamic_storage_buffer_offsets_targets: alloc::collections::BTreeMap::new(), + external_texture_binding_map: ExternalTextureBindingMap::default(), zero_initialize_workgroup_memory: true, restrict_indexing: true, force_loop_bounding: true, @@ -425,6 +580,29 @@ impl Options { None => Err(EntryPointError::MissingBinding(*res_binding)), } } + + fn resolve_external_texture_resource_binding( + &self, + res_binding: &crate::ResourceBinding, + ) -> Result { + match self.external_texture_binding_map.get(res_binding) { + Some(target) => Ok(*target), + None if self.fake_missing_bindings => { + let fake = BindTarget { + space: res_binding.group as u8, + register: res_binding.binding, + binding_array_size: None, + dynamic_storage_buffer_offsets_index: None, + restrict_indexing: false, + }; + Ok(ExternalTextureBindTarget { + planes: [fake, fake, fake], + params: fake, + }) + } + None => Err(EntryPointError::MissingBinding(*res_binding)), + } + } } /// Reflection info for entry point names. @@ -479,6 +657,7 @@ enum WrappedType { ArrayLength(help::WrappedArrayLength), ImageSample(help::WrappedImageSample), ImageQuery(help::WrappedImageQuery), + ImageLoad(help::WrappedImageLoad), ImageLoadScalar(crate::Scalar), Constructor(help::WrappedConstructor), StructMatrixAccess(help::WrappedStructMatrixAccess), diff --git a/naga/src/back/hlsl/writer.rs b/naga/src/back/hlsl/writer.rs index 0639623fd8b..357b8597521 100644 --- a/naga/src/back/hlsl/writer.rs +++ b/naga/src/back/hlsl/writer.rs @@ -17,7 +17,7 @@ use super::{ use crate::{ back::{self, get_entry_points, Baked}, common, - proc::{self, index, NameKey}, + proc::{self, index, ExternalTextureNameKey, NameKey}, valid, Handle, Module, RayQueryFunction, Scalar, ScalarKind, ShaderStage, TypeInner, }; @@ -34,6 +34,7 @@ pub(crate) const EXTRACT_BITS_FUNCTION: &str = "naga_extractBits"; pub(crate) const INSERT_BITS_FUNCTION: &str = "naga_insertBits"; pub(crate) const SAMPLER_HEAP_VAR: &str = "nagaSamplerHeap"; pub(crate) const COMPARISON_SAMPLER_HEAP_VAR: &str = "nagaComparisonSamplerHeap"; +pub(crate) const SAMPLE_EXTERNAL_TEXTURE_FUNCTION: &str = "nagaSampleExternalTexture"; pub(crate) const ABS_FUNCTION: &str = "naga_abs"; pub(crate) const DIV_FUNCTION: &str = "naga_div"; pub(crate) const MOD_FUNCTION: &str = "naga_mod"; @@ -44,6 +45,7 @@ pub(crate) const F2I64_FUNCTION: &str = "naga_f2i64"; pub(crate) const F2U64_FUNCTION: &str = "naga_f2u64"; pub(crate) const IMAGE_SAMPLE_BASE_CLAMP_TO_EDGE_FUNCTION: &str = "nagaTextureSampleBaseClampToEdge"; +pub(crate) const IMAGE_LOAD_EXTERNAL_FUNCTION: &str = "nagaTextureLoadExternal"; enum Index { Expression(Handle), @@ -431,6 +433,10 @@ impl<'a, W: fmt::Write> super::Writer<'a, W> { .find(|&(var_handle, var)| match var.binding { Some(ref binding) if !info[var_handle].is_empty() => { self.options.resolve_resource_binding(binding).is_err() + && self + .options + .resolve_external_texture_resource_binding(binding) + .is_err() } _ => false, }) @@ -473,8 +479,14 @@ impl<'a, W: fmt::Write> super::Writer<'a, W> { match var.binding { Some(ref binding) if !info[var_handle].is_empty() => { if let Err(err) = self.options.resolve_resource_binding(binding) { - ep_error = Some(err); - break; + if self + .options + .resolve_external_texture_resource_binding(binding) + .is_err() + { + ep_error = Some(err); + break; + } } } _ => {} @@ -904,6 +916,25 @@ impl<'a, W: fmt::Write> super::Writer<'a, W> { let global = &module.global_variables[handle]; let inner = &module.types[global.ty].inner; + let handle_ty = match *inner { + TypeInner::BindingArray { ref base, .. } => &module.types[*base].inner, + _ => inner, + }; + + // External textures are handled entirely differently, so defer entirely to that method. + // We do so prior to calling resolve_resource_binding() below, as we even need to resolve + // their bindings separately. + let is_external_texture = matches!( + *handle_ty, + TypeInner::Image { + class: crate::ImageClass::External, + .. + } + ); + if is_external_texture { + return self.write_global_external_texture(module, handle, global); + } + if let Some(ref binding) = global.binding { if let Err(err) = self.options.resolve_resource_binding(binding) { log::info!( @@ -916,11 +947,6 @@ impl<'a, W: fmt::Write> super::Writer<'a, W> { } } - let handle_ty = match *inner { - TypeInner::BindingArray { ref base, .. } => &module.types[*base].inner, - _ => inner, - }; - // Samplers are handled entirely differently, so defer entirely to that method. let is_sampler = matches!(*handle_ty, TypeInner::Sampler { .. }); @@ -1133,6 +1159,70 @@ impl<'a, W: fmt::Write> super::Writer<'a, W> { Ok(()) } + /// Write the declarations for an external texture global variable. + /// These are emitted as multiple global variables: Three `Texture2D`s + /// (one for each plane) and a parameters cbuffer. + fn write_global_external_texture( + &mut self, + module: &Module, + handle: Handle, + global: &crate::GlobalVariable, + ) -> BackendResult { + let res_binding = global + .binding + .as_ref() + .expect("External texture global variables must have a resource binding"); + let ext_tex_bindings = match self + .options + .resolve_external_texture_resource_binding(res_binding) + { + Ok(bindings) => bindings, + Err(err) => { + log::info!( + "Skipping global {:?} (name {:?}) for being inaccessible: {}", + handle, + global.name, + err, + ); + return Ok(()); + } + }; + + let mut write_plane = |bt: &super::BindTarget, name| -> BackendResult { + write!( + self.out, + "Texture2D {}: register(t{}", + name, bt.register + )?; + if bt.space != 0 { + write!(self.out, ", space{}", bt.space)?; + } + writeln!(self.out, ");")?; + Ok(()) + }; + for (i, bt) in ext_tex_bindings.planes.iter().enumerate() { + let plane_name = &self.names + [&NameKey::ExternalTextureGlobalVariable(handle, ExternalTextureNameKey::Plane(i))]; + write_plane(bt, plane_name)?; + } + + let params_name = &self.names + [&NameKey::ExternalTextureGlobalVariable(handle, ExternalTextureNameKey::Params)]; + let params_ty_name = + &self.names[&NameKey::Type(module.special_types.external_texture_params.unwrap())]; + write!( + self.out, + "cbuffer {}: register(b{}", + params_name, ext_tex_bindings.params.register + )?; + if ext_tex_bindings.params.space != 0 { + write!(self.out, ", space{}", ext_tex_bindings.params.space)?; + } + writeln!(self.out, ") {{ {params_ty_name} {params_name}; }};")?; + + Ok(()) + } + /// Helper method used to write global constants /// /// # Notes @@ -1485,26 +1575,8 @@ impl<'a, W: fmt::Write> super::Writer<'a, W> { if index != 0 { write!(self.out, ", ")?; } - // Write argument type - let arg_ty = match module.types[arg.ty].inner { - // pointers in function arguments are expected and resolve to `inout` - TypeInner::Pointer { base, .. } => { - //TODO: can we narrow this down to just `in` when possible? - write!(self.out, "inout ")?; - base - } - _ => arg.ty, - }; - self.write_type(module, arg_ty)?; - - let argument_name = - &self.names[&NameKey::FunctionArgument(handle, index as u32)]; - // Write argument name. Space is important. - write!(self.out, " {argument_name}")?; - if let TypeInner::Array { base, size, .. } = module.types[arg_ty].inner { - self.write_array_size(module, base, size)?; - } + self.write_function_argument(module, handle, arg, index)?; } } back::FunctionType::EntryPoint(ep_index) => { @@ -1618,6 +1690,74 @@ impl<'a, W: fmt::Write> super::Writer<'a, W> { Ok(()) } + fn write_function_argument( + &mut self, + module: &Module, + handle: Handle, + arg: &crate::FunctionArgument, + index: usize, + ) -> BackendResult { + // External texture arguments must be expanded into separate + // arguments for each plane and the params buffer. + if let TypeInner::Image { + class: crate::ImageClass::External, + .. + } = module.types[arg.ty].inner + { + return self.write_function_external_texture_argument(module, handle, index); + } + + // Write argument type + let arg_ty = match module.types[arg.ty].inner { + // pointers in function arguments are expected and resolve to `inout` + TypeInner::Pointer { base, .. } => { + //TODO: can we narrow this down to just `in` when possible? + write!(self.out, "inout ")?; + base + } + _ => arg.ty, + }; + self.write_type(module, arg_ty)?; + + let argument_name = &self.names[&NameKey::FunctionArgument(handle, index as u32)]; + + // Write argument name. Space is important. + write!(self.out, " {argument_name}")?; + if let TypeInner::Array { base, size, .. } = module.types[arg_ty].inner { + self.write_array_size(module, base, size)?; + } + + Ok(()) + } + + fn write_function_external_texture_argument( + &mut self, + module: &Module, + handle: Handle, + index: usize, + ) -> BackendResult { + let plane_names = [0, 1, 2].map(|i| { + &self.names[&NameKey::ExternalTextureFunctionArgument( + handle, + index as u32, + ExternalTextureNameKey::Plane(i), + )] + }); + let params_name = &self.names[&NameKey::ExternalTextureFunctionArgument( + handle, + index as u32, + ExternalTextureNameKey::Params, + )]; + let params_ty_name = + &self.names[&NameKey::Type(module.special_types.external_texture_params.unwrap())]; + write!( + self.out, + "Texture2D {}, Texture2D {}, Texture2D {}, {params_ty_name} {params_name}", + plane_names[0], plane_names[1], plane_names[2], + )?; + Ok(()) + } + fn need_workgroup_variables_initialization( &mut self, func_ctx: &back::FunctionCtx, @@ -3117,9 +3257,34 @@ impl<'a, W: fmt::Write> super::Writer<'a, W> { } } Expression::FunctionArgument(pos) => { - let key = func_ctx.argument_key(pos); - let name = &self.names[&key]; - write!(self.out, "{name}")?; + let ty = func_ctx.resolve_type(expr, &module.types); + + // We know that any external texture function argument has been expanded into + // separate consecutive arguments for each plane and the parameters buffer. And we + // also know that external textures can only ever be used as an argument to another + // function. Therefore we can simply emit each of the expanded arguments in a + // consecutive comma-separated list. + if let TypeInner::Image { + class: crate::ImageClass::External, + .. + } = *ty + { + let plane_names = [0, 1, 2].map(|i| { + &self.names[&func_ctx + .external_texture_argument_key(pos, ExternalTextureNameKey::Plane(i))] + }); + let params_name = &self.names[&func_ctx + .external_texture_argument_key(pos, ExternalTextureNameKey::Params)]; + write!( + self.out, + "{}, {}, {}, {}", + plane_names[0], plane_names[1], plane_names[2], params_name + )?; + } else { + let key = func_ctx.argument_key(pos); + let name = &self.names[&key]; + write!(self.out, "{name}")?; + } } Expression::ImageSample { coordinate, @@ -3282,7 +3447,34 @@ impl<'a, W: fmt::Write> super::Writer<'a, W> { let is_storage_space = matches!(global_variable.space, crate::AddressSpace::Storage { .. }); - if !is_binding_array_of_samplers && !is_storage_space { + // Our external texture global variable has been expanded into multiple + // global variables, one for each plane and the parameters buffer. + // External textures can only ever be used as arguments to a function + // call, and we know that an external texture argument to any function + // will have been expanded to separate consecutive arguments for each + // plane and the parameters buffer. Therefore we can simply emit each of + // the expanded global variables in a consecutive comma-separated list. + if let TypeInner::Image { + class: crate::ImageClass::External, + .. + } = *ty + { + let plane_names = [0, 1, 2].map(|i| { + &self.names[&NameKey::ExternalTextureGlobalVariable( + handle, + ExternalTextureNameKey::Plane(i), + )] + }); + let params_name = &self.names[&NameKey::ExternalTextureGlobalVariable( + handle, + ExternalTextureNameKey::Params, + )]; + write!( + self.out, + "{}, {}, {}, {}", + plane_names[0], plane_names[1], plane_names[2], params_name + )?; + } else if !is_binding_array_of_samplers && !is_storage_space { let name = &self.names[&NameKey::GlobalVariable(handle)]; write!(self.out, "{name}")?; } @@ -4113,6 +4305,17 @@ impl<'a, W: fmt::Write> super::Writer<'a, W> { ) -> Result<(), Error> { let mut wrapping_type = None; match *func_ctx.resolve_type(image, &module.types) { + TypeInner::Image { + class: crate::ImageClass::External, + .. + } => { + write!(self.out, "{IMAGE_LOAD_EXTERNAL_FUNCTION}(")?; + self.write_expr(module, image, func_ctx)?; + write!(self.out, ", ")?; + self.write_expr(module, coordinate, func_ctx)?; + write!(self.out, ")")?; + return Ok(()); + } TypeInner::Image { class: crate::ImageClass::Storage { format, .. }, .. diff --git a/naga/src/ir/mod.rs b/naga/src/ir/mod.rs index 1744ff92fcd..50cc89a5b4c 100644 --- a/naga/src/ir/mod.rs +++ b/naga/src/ir/mod.rs @@ -347,7 +347,21 @@ pub enum AddressSpace { Storage { access: StorageAccess }, /// Opaque handles, such as samplers and images. Handle, + /// Push constants. + /// + /// A [`Module`] may contain at most one [`GlobalVariable`] in + /// this address space. Its contents are provided not by a buffer + /// but by `SetPushConstant` pass commands, allowing the CPU to + /// establish different values for each draw/dispatch. + /// + /// `PushConstant` variables may not contain `f16` values, even if + /// the [`SHADER_FLOAT16`] capability is enabled. + /// + /// Backends generally place tight limits on the size of + /// `PushConstant` variables. + /// + /// [`SHADER_FLOAT16`]: crate::valid::Capabilities::SHADER_FLOAT16 PushConstant, } diff --git a/naga/src/valid/type.rs b/naga/src/valid/type.rs index 095fd7f882b..8aa3c04f207 100644 --- a/naga/src/valid/type.rs +++ b/naga/src/valid/type.rs @@ -261,6 +261,15 @@ impl super::Validator { } } + /// Check whether `scalar` is a permitted scalar width. + /// + /// If `scalar` is not a width allowed by the selected [`Capabilities`], + /// return an error explaining why. + /// + /// If `scalar` is allowed, return a [`PushConstantCompatibility`] result + /// that says whether `scalar` is allowed specifically in push constants. + /// + /// [`Capabilities`]: crate::valid::Capabilities pub(super) const fn check_width( &self, scalar: crate::Scalar, diff --git a/naga/tests/in/wgsl/texture-external.toml b/naga/tests/in/wgsl/texture-external.toml index f8f46d4223e..3ea75be88d2 100644 --- a/naga/tests/in/wgsl/texture-external.toml +++ b/naga/tests/in/wgsl/texture-external.toml @@ -1,2 +1,15 @@ god_mode = true -targets = "IR | WGSL" +targets = "HLSL | IR | WGSL" + +[[hlsl.binding_map]] +resource_binding = { group = 0, binding = 1 } +bind_target = { register = 0, space = 0 } + +[[hlsl.external_texture_binding_map]] +resource_binding = { group = 0, binding = 0 } +bind_target.planes = [ + { space = 0, register = 0 }, + { space = 0, register = 1 }, + { space = 0, register = 2 }, +] +bind_target.params = { space = 0, register = 3 } diff --git a/naga/tests/out/hlsl/wgsl-texture-external.hlsl b/naga/tests/out/hlsl/wgsl-texture-external.hlsl new file mode 100644 index 00000000000..517d6a724a8 --- /dev/null +++ b/naga/tests/out/hlsl/wgsl-texture-external.hlsl @@ -0,0 +1,143 @@ +struct NagaExternalTextureParams { + row_major float4x4 yuv_conversion_matrix; + float2 sample_transform_0; float2 sample_transform_1; float2 sample_transform_2; + float2 load_transform_0; float2 load_transform_1; float2 load_transform_2; + uint2 size; + uint num_planes; + int _end_pad_0; +}; + +Texture2D tex_plane0_: register(t0); +Texture2D tex_plane1_: register(t1); +Texture2D tex_plane2_: register(t2); +cbuffer tex_params: register(b3) { NagaExternalTextureParams tex_params; }; +SamplerState nagaSamplerHeap[2048]: register(s0, space0); +SamplerComparisonState nagaComparisonSamplerHeap[2048]: register(s0, space1); +StructuredBuffer nagaGroup0SamplerIndexArray : register(t0, space255); +static const SamplerState samp = nagaSamplerHeap[nagaGroup0SamplerIndexArray[0]]; + +float4 nagaTextureSampleBaseClampToEdge( + Texture2D plane0, + Texture2D plane1, + Texture2D plane2, + NagaExternalTextureParams params, + SamplerState samp, + float2 coords) +{ + float2 plane0_size; + plane0.GetDimensions(plane0_size.x, plane0_size.y); + float3x2 sample_transform = float3x2( + params.sample_transform_0, + params.sample_transform_1, + params.sample_transform_2 + ); + coords = mul(float3(coords, 1.0), sample_transform); + float2 bounds_min = mul(float3(0.0, 0.0, 1.0), sample_transform); + float2 bounds_max = mul(float3(1.0, 1.0, 1.0), sample_transform); + float4 bounds = float4(min(bounds_min, bounds_max), max(bounds_min, bounds_max)); + float2 plane0_half_texel = float2(0.5, 0.5) / plane0_size; + float2 plane0_coords = clamp(coords, bounds.xy + plane0_half_texel, bounds.zw - plane0_half_texel); + if (params.num_planes == 1u) { + return plane0.SampleLevel(samp, plane0_coords, 0.0f); + } else { + float2 plane1_size; + plane1.GetDimensions(plane1_size.x, plane1_size.y); + float2 plane1_half_texel = float2(0.5, 0.5) / plane1_size; + float2 plane1_coords = clamp(coords, bounds.xy + plane1_half_texel, bounds.zw - plane1_half_texel); + float y = plane0.SampleLevel(samp, plane0_coords, 0.0f).x; + float2 uv; + if (params.num_planes == 2u) { + uv = plane1.SampleLevel(samp, plane1_coords, 0.0f).xy; + } else { + float2 plane2_size; + plane2.GetDimensions(plane2_size.x, plane2_size.y); + float2 plane2_half_texel = float2(0.5, 0.5) / plane2_size; + float2 plane2_coords = clamp(coords, bounds.xy + plane2_half_texel, bounds.zw - plane2_half_texel); + uv = float2(plane1.SampleLevel(samp, plane1_coords, 0.0f).x, plane2.SampleLevel(samp, plane2_coords, 0.0f).x); + } + return mul(float4(y, uv, 1.0), params.yuv_conversion_matrix); + } +} + +float4 nagaTextureLoadExternal( + Texture2D plane0, + Texture2D plane1, + Texture2D plane2, + NagaExternalTextureParams params, + uint2 coords) +{ + uint2 plane0_size; + plane0.GetDimensions(plane0_size.x, plane0_size.y); + uint2 cropped_size = any(params.size) ? params.size : plane0_size; + coords = min(coords, cropped_size - 1); + float3x2 load_transform = float3x2( + params.load_transform_0, + params.load_transform_1, + params.load_transform_2 + ); + uint2 plane0_coords = uint2(round(mul(float3(coords, 1.0), load_transform))); + if (params.num_planes == 1u) { + return plane0.Load(uint3(plane0_coords, 0u)); + } else { + uint2 plane1_size; + plane1.GetDimensions(plane1_size.x, plane1_size.y); + uint2 plane1_coords = uint2(floor(float2(plane0_coords) * float2(plane1_size) / float2(plane0_size))); + float y = plane0.Load(uint3(plane0_coords, 0u)).x; + float2 uv; + if (params.num_planes == 2u) { + uv = plane1.Load(uint3(plane1_coords, 0u)).xy; + } else { + uint2 plane2_size; + plane2.GetDimensions(plane2_size.x, plane2_size.y); + uint2 plane2_coords = uint2(floor(float2(plane0_coords) * float2(plane2_size) / float2(plane0_size))); + uv = float2(plane1.Load(uint3(plane1_coords, 0u)).x, plane2.Load(uint3(plane2_coords, 0u)).x); + } + return mul(float4(y, uv, 1.0), params.yuv_conversion_matrix); + } +} + +uint2 NagaExternalDimensions2D(Texture2D plane0, Texture2D plane1, Texture2D plane2, NagaExternalTextureParams params) { + if (any(params.size)) { + return params.size; + } else { + uint2 ret; + plane0.GetDimensions(ret.x, ret.y); + return ret; + } +} + +float4 test(Texture2D t_plane0_, Texture2D t_plane1_, Texture2D t_plane2_, NagaExternalTextureParams t_params) +{ + float4 a = (float4)0; + float4 b = (float4)0; + uint2 c = (uint2)0; + + float4 _e4 = nagaTextureSampleBaseClampToEdge(t_plane0_, t_plane1_, t_plane2_, t_params, samp, (0.0).xx); + a = _e4; + float4 _e8 = nagaTextureLoadExternal(t_plane0_, t_plane1_, t_plane2_, t_params, (0u).xx); + b = _e8; + c = NagaExternalDimensions2D(t_plane0_, t_plane1_, t_plane2_, t_params); + float4 _e12 = a; + float4 _e13 = b; + uint2 _e15 = c; + return ((_e12 + _e13) + float2(_e15).xyxy); +} + +float4 fragment_main() : SV_Target0 +{ + const float4 _e1 = test(tex_plane0_, tex_plane1_, tex_plane2_, tex_params); + return _e1; +} + +float4 vertex_main() : SV_Position +{ + const float4 _e1 = test(tex_plane0_, tex_plane1_, tex_plane2_, tex_params); + return _e1; +} + +[numthreads(1, 1, 1)] +void compute_main() +{ + const float4 _e1 = test(tex_plane0_, tex_plane1_, tex_plane2_, tex_params); + return; +} diff --git a/naga/tests/out/hlsl/wgsl-texture-external.ron b/naga/tests/out/hlsl/wgsl-texture-external.ron new file mode 100644 index 00000000000..23afa21e1f5 --- /dev/null +++ b/naga/tests/out/hlsl/wgsl-texture-external.ron @@ -0,0 +1,20 @@ +( + vertex:[ + ( + entry_point:"vertex_main", + target_profile:"vs_5_1", + ), + ], + fragment:[ + ( + entry_point:"fragment_main", + target_profile:"ps_5_1", + ), + ], + compute:[ + ( + entry_point:"compute_main", + target_profile:"cs_5_1", + ), + ], +) diff --git a/wgpu-core/src/device/resource.rs b/wgpu-core/src/device/resource.rs index 3f212f80eef..5fe0de4134c 100644 --- a/wgpu-core/src/device/resource.rs +++ b/wgpu-core/src/device/resource.rs @@ -83,26 +83,55 @@ pub(crate) struct CommandIndices { 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 normalized texture - /// coordinates prior to sampling from the external texture. This may - /// scale, translate, flip, and rotate in 90-degree increments, but the - /// result of transforming the rectangle (0,0)..(1,1) must be an - /// axis-aligned rectangle that falls within the bounds of (0,0)..(1,1). + + /// Transform to apply to [`ImageSample`] coordinates. + /// + /// This is a 3x2 column-major matrix representing an affine transform from + /// normalized texture coordinates to the normalized coordinates that should + /// be sampled from the external texture's underlying plane(s). + /// + /// This transform may scale, translate, flip, and rotate in 90-degree + /// increments, but the result of transforming the rectangle (0,0)..(1,1) + /// must be an axis-aligned rectangle that falls within the bounds of + /// (0,0)..(1,1). + /// + /// [`ImageSample`]: naga::ir::Expression::ImageSample pub sample_transform: [f32; 6], - /// 3x2 column-major matrix with which to multiply unnormalized texture - /// coordinates prior to loading from the external texture. This may scale, - /// translate, flip, and rotate in 90-degree increments, but the result of - /// transforming the rectangle (0,0)..(texture_size - 1) must be an - /// axis-aligned rectangle that falls within the bounds of - /// (0,0)..(texture_size - 1). + + /// Transform to apply to [`ImageLoad`] coordinates. + /// + /// This is a 3x2 column-major matrix representing an affine transform from + /// non-normalized texel coordinates to the non-normalized coordinates of + /// the texel that should be loaded from the external texture's underlying + /// plane 0. For planes 1 and 2, if present, plane 0's coordinates are + /// scaled according to the textures' relative sizes. + /// + /// This transform may scale, translate, flip, and rotate in 90-degree + /// increments, but the result of transforming the rectangle (0,0)..[`size`] + /// must be an axis-aligned rectangle that falls within the bounds of + /// (0,0)..[`size`]. + /// + /// [`ImageLoad`]: naga::ir::Expression::ImageLoad + /// [`size`]: Self::size pub load_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. + + /// Size of the external texture. + /// + /// This is the value that should be returned by size queries in shader + /// code; it does not necessarily match the dimensions of the underlying + /// texture(s). As a special case, if this is `[0, 0]`, the actual size of + /// plane 0 should be used instead. + /// + /// This must be consistent with [`sample_transform`]: it should be the size + /// in texels of the rectangle covered by the square (0,0)..(1,1) after + /// [`sample_transform`] has been applied to it. + /// + /// [`sample_transform`]: Self::sample_transform 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. diff --git a/wgpu-hal/src/dx12/device.rs b/wgpu-hal/src/dx12/device.rs index 5d3a6ac8d45..d8bdd2b1d5f 100644 --- a/wgpu-hal/src/dx12/device.rs +++ b/wgpu-hal/src/dx12/device.rs @@ -1388,6 +1388,7 @@ impl crate::Device for super::Device { restrict_indexing: true, sampler_heap_target, sampler_buffer_binding_map, + external_texture_binding_map: hlsl::ExternalTextureBindingMap::default(), force_loop_bounding: true, }, }) diff --git a/wgpu-types/src/lib.rs b/wgpu-types/src/lib.rs index 6252e5c6778..d70fd3803cc 100644 --- a/wgpu-types/src/lib.rs +++ b/wgpu-types/src/lib.rs @@ -6285,6 +6285,20 @@ pub enum ExternalTextureFormat { /// Describes an [`ExternalTexture`](../wgpu/struct.ExternalTexture.html). /// +/// Note that [`width`] and [`height`] are the values that should be returned by +/// size queries in shader code; they do not necessarily match the dimensions of +/// the underlying plane texture(s). As a special case, if `(width, height)` is +/// `(0, 0)`, the actual size of the first underlying plane should be used instead. +/// +/// The size given by [`width`] and [`height`] must be consistent with +/// [`sample_transform`]: they should be the size in texels of the rectangle +/// covered by the square (0,0)..(1,1) after [`sample_transform`] has been applied +/// to it. +/// +/// [`width`]: Self::width +/// [`height`]: Self::height +/// [`sample_transform`]: Self::sample_transform +/// /// Corresponds to [WebGPU `GPUExternalTextureDescriptor`]( /// https://gpuweb.github.io/gpuweb/#dictdef-gpuexternaltexturedescriptor). #[repr(C)] @@ -6294,23 +6308,51 @@ 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. + + /// Width of the external texture. 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 normalized texture - /// coordinates prior to sampling from the external texture. + + /// Transform to apply to [`ImageSample`] coordinates. + /// + /// This is a 3x2 column-major matrix representing an affine transform from + /// normalized texture coordinates to the normalized coordinates that should + /// be sampled from the external texture's underlying plane(s). + /// + /// This transform may scale, translate, flip, and rotate in 90-degree + /// increments, but the result of transforming the rectangle (0,0)..(1,1) + /// must be an axis-aligned rectangle that falls within the bounds of + /// (0,0)..(1,1). + /// + /// [`ImageSample`]: https://docs.rs/naga/latest/naga/ir/enum.Expression.html#variant.ImageSample pub sample_transform: [f32; 6], - /// 3x2 column-major matrix with which to multiply unnormalized texture - /// coordinates prior to loading from the external texture. + + /// Transform to apply to [`ImageLoad`] coordinates. + /// + /// This is a 3x2 column-major matrix representing an affine transform from + /// non-normalized texel coordinates to the non-normalized coordinates of + /// the texel that should be loaded from the external texture's underlying + /// plane 0. For planes 1 and 2, if present, plane 0's coordinates are + /// scaled according to the textures' relative sizes. + /// + /// This transform may scale, translate, flip, and rotate in 90-degree + /// increments, but the result of transforming the rectangle (0,0)..([`width`], + /// [`height`]) must be an axis-aligned rectangle that falls within the bounds + /// of (0,0)..([`width`], [`height`]). + /// + /// [`ImageLoad`]: https://docs.rs/naga/latest/naga/ir/enum.Expression.html#variant.ImageLoad + /// [`width`]: Self::width + /// [`height`]: Self::height pub load_transform: [f32; 6], } From bb7a4e05aa2c16ce88170c563ddcb7496647afc0 Mon Sep 17 00:00:00 2001 From: Jamie Nicol Date: Wed, 30 Jul 2025 15:32:28 +0100 Subject: [PATCH 4/4] [naga] Have validation reject shaders containing binding arrays of external textures For simplicity's sake our initial implementation of external textures will not support binding arrays of external textures. We should therefore reject any shaders which use them during validation. Their implementation will be tracked in #8027. naga/src/valid/type.rs JJ: JJ: Lines starting with "JJ:" (like this one) will be removed. --- naga/src/valid/type.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/naga/src/valid/type.rs b/naga/src/valid/type.rs index 8aa3c04f207..e8b83ff08f3 100644 --- a/naga/src/valid/type.rs +++ b/naga/src/valid/type.rs @@ -133,6 +133,8 @@ pub enum TypeError { InvalidDynamicArray(String, Handle), #[error("The base handle {0:?} has to be a struct")] BindingArrayBaseTypeNotStruct(Handle), + #[error("Binding arrays of external textures are not yet supported")] + BindingArrayBaseExternalTextures, #[error("Structure member[{index}] at {offset} overlaps the previous member")] MemberOverlap { index: u32, offset: u32 }, #[error( @@ -803,6 +805,17 @@ impl super::Validator { _ => return Err(TypeError::BindingArrayBaseTypeNotStruct(base)), }; } + if matches!( + gctx.types[base].inner, + crate::TypeInner::Image { + class: crate::ImageClass::External, + .. + } + ) { + // Binding arrays of external textures are not yet supported. + // https://github.com/gfx-rs/wgpu/issues/8027 + return Err(TypeError::BindingArrayBaseExternalTextures); + } if !base_info.flags.contains(TypeFlags::CREATION_RESOLVED) { return Err(TypeError::InvalidData(base));