Skip to content

Commit f48c117

Browse files
committed
[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.
1 parent 51a8f67 commit f48c117

File tree

11 files changed

+994
-126
lines changed

11 files changed

+994
-126
lines changed

naga-cli/src/bin/naga.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,7 @@ fn run() -> anyhow::Result<()> {
539539
let missing = match Path::new(path).extension().and_then(|ex| ex.to_str()) {
540540
Some("wgsl") => C::CLIP_DISTANCE | C::CULL_DISTANCE,
541541
Some("metal") => C::CULL_DISTANCE | C::TEXTURE_EXTERNAL,
542+
Some("hlsl") => C::empty(),
542543
_ => C::TEXTURE_EXTERNAL,
543544
};
544545
caps & !missing

naga/src/back/hlsl/help.rs

Lines changed: 374 additions & 91 deletions
Large diffs are not rendered by default.

naga/src/back/hlsl/keywords.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,7 @@ pub const RESERVED: &[&str] = &[
826826
super::writer::INSERT_BITS_FUNCTION,
827827
super::writer::SAMPLER_HEAP_VAR,
828828
super::writer::COMPARISON_SAMPLER_HEAP_VAR,
829+
super::writer::SAMPLE_EXTERNAL_TEXTURE_FUNCTION,
829830
super::writer::ABS_FUNCTION,
830831
super::writer::DIV_FUNCTION,
831832
super::writer::MOD_FUNCTION,
@@ -834,6 +835,7 @@ pub const RESERVED: &[&str] = &[
834835
super::writer::F2U32_FUNCTION,
835836
super::writer::F2I64_FUNCTION,
836837
super::writer::F2U64_FUNCTION,
838+
super::writer::IMAGE_LOAD_EXTERNAL_FUNCTION,
837839
super::writer::IMAGE_SAMPLE_BASE_CLAMP_TO_EDGE_FUNCTION,
838840
];
839841

naga/src/back/hlsl/mod.rs

Lines changed: 182 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,37 @@ index buffer for each bind group. This buffer is accessed in the shader to get t
106106
sampler index within the heap. See the wgpu_hal dx12 backend documentation for more
107107
information.
108108
109+
# External textures
110+
111+
Support for [`crate::ImageClass::External`] textures is implemented by lowering
112+
each external texture global variable to 3 `Texture2D<float4>`s, and a `cbuffer`
113+
of type `NagaExternalTextureParams`. This provides up to 3 planes of texture
114+
data (for example single planar RGBA, or separate Y, Cb, and Cr planes), and the
115+
parameters buffer containing information describing how to handle these
116+
correctly. The bind target to use for each of these globals is specified via
117+
[`Options::external_texture_binding_map`].
118+
119+
External textures are supported by WGSL's `textureDimensions()`,
120+
`textureLoad()`, and `textureSampleBaseClampToEdge()` built-in functions. These
121+
are implemented using helper functions. See the following functions for how
122+
these are generated:
123+
* `Writer::write_wrapped_image_query_function`
124+
* `Writer::write_wrapped_image_load_function`
125+
* `Writer::write_wrapped_image_sample_function`
126+
127+
Ideally the set of global variables could be wrapped in a single struct that
128+
could conveniently be passed around. But, alas, HLSL does not allow structs to
129+
have `Texture2D` members. Fortunately, however, external textures can only be
130+
used as arguments to either built-in or user-defined functions. We therefore
131+
expand any external texture function argument to four consecutive arguments (3
132+
textures and the params struct) when declaring user-defined functions, and
133+
ensure our built-in function implementations take the same arguments. Then,
134+
whenever we need to emit an external texture in `Writer::write_expr`, which
135+
fortunately can only ever be for a global variable or function argument, we
136+
simply emit the variable name of each of the three textures and the parameters
137+
struct in a comma-separated list. This won't win any awards for elegance, but
138+
it works for our purposes.
139+
109140
[hlsl]: https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl
110141
[ilov]: https://gpuweb.github.io/gpuweb/wgsl/#internal-value-layout
111142
[16bb]: https://github.com/microsoft/DirectXShaderCompiler/wiki/Buffer-Packing#constant-buffer-packing
@@ -126,6 +157,35 @@ use thiserror::Error;
126157

127158
use crate::{back, ir, proc};
128159

160+
/// Direct3D 12 binding information for a global variable.
161+
///
162+
/// This type provides the HLSL-specific information Naga needs to declare and
163+
/// access an HLSL global variable that cannot be derived from the `Module`
164+
/// itself.
165+
///
166+
/// An HLSL global variable declaration includes details that the Direct3D API
167+
/// will use to refer to it. For example:
168+
///
169+
/// RWByteAddressBuffer s_sasm : register(u0, space2);
170+
///
171+
/// This defines a global `s_sasm` that a Direct3D root signature would refer to
172+
/// as register `0` in register space `2` in a `UAV` descriptor range. Naga can
173+
/// infer the register's descriptor range type from the variable's address class
174+
/// (writable [`Storage`] variables are implemented by Direct3D Unordered Access
175+
/// Views, the `u` register type), but the register number and register space
176+
/// must be supplied by the user.
177+
///
178+
/// The [`back::hlsl::Options`] structure provides `BindTarget`s for various
179+
/// situations in which Naga may need to generate an HLSL global variable, like
180+
/// [`binding_map`] for Naga global variables, or [`push_constants_target`] for
181+
/// a module's sole [`PushConstant`] variable. See those fields' documentation
182+
/// for details.
183+
///
184+
/// [`Storage`]: crate::ir::AddressSpace::Storage
185+
/// [`back::hlsl::Options`]: Options
186+
/// [`binding_map`]: Options::binding_map
187+
/// [`push_constants_target`]: Options::push_constants_target
188+
/// [`PushConstant`]: crate::ir::AddressSpace::PushConstant
129189
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
130190
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
131191
#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
@@ -335,6 +395,62 @@ where
335395

336396
pub type DynamicStorageBufferOffsetsTargets = alloc::collections::BTreeMap<u32, OffsetsBindTarget>;
337397

398+
/// HLSL binding information for a Naga [`External`] image global variable.
399+
///
400+
/// See the module documentation's section on [External textures][mod] for details.
401+
///
402+
/// [`External`]: crate::ir::ImageClass::External
403+
/// [mod]: #external-textures
404+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
405+
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
406+
#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
407+
pub struct ExternalTextureBindTarget {
408+
/// HLSL binding information for the individual plane textures.
409+
///
410+
/// Each of these should refer to an HLSL `Texture2D<float4>` holding one
411+
/// plane of data for the external texture. The exact meaning of each plane
412+
/// varies at runtime depending on where the external texture's data
413+
/// originated.
414+
pub planes: [BindTarget; 3],
415+
416+
/// HLSL binding information for a buffer holding the sampling parameters.
417+
///
418+
/// This should refer to a cbuffer of type `NagaExternalTextureParams`, that
419+
/// the code Naga generates for `textureSampleBaseClampToEdge` consults to
420+
/// decide how to combine the data in [`planes`] to get the result required
421+
/// by the spec.
422+
///
423+
/// [`planes`]: Self::planes
424+
pub params: BindTarget,
425+
}
426+
427+
#[cfg(any(feature = "serialize", feature = "deserialize"))]
428+
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
429+
#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
430+
struct ExternalTextureBindingMapSerialization {
431+
resource_binding: crate::ResourceBinding,
432+
bind_target: ExternalTextureBindTarget,
433+
}
434+
435+
#[cfg(feature = "deserialize")]
436+
fn deserialize_external_texture_binding_map<'de, D>(
437+
deserializer: D,
438+
) -> Result<ExternalTextureBindingMap, D::Error>
439+
where
440+
D: serde::Deserializer<'de>,
441+
{
442+
use serde::Deserialize;
443+
444+
let vec = Vec::<ExternalTextureBindingMapSerialization>::deserialize(deserializer)?;
445+
let mut map = ExternalTextureBindingMap::default();
446+
for item in vec {
447+
map.insert(item.resource_binding, item.bind_target);
448+
}
449+
Ok(map)
450+
}
451+
pub type ExternalTextureBindingMap =
452+
alloc::collections::BTreeMap<crate::ResourceBinding, ExternalTextureBindTarget>;
453+
338454
/// Shorthand result used internally by the backend
339455
type BackendResult = Result<(), Error>;
340456

@@ -354,21 +470,47 @@ pub enum EntryPointError {
354470
pub struct Options {
355471
/// The hlsl shader model to be used
356472
pub shader_model: ShaderModel,
357-
/// Map of resources association to binding locations.
473+
474+
/// HLSL binding information for each Naga global variable.
475+
///
476+
/// This maps Naga [`GlobalVariable`]'s [`ResourceBinding`]s to a
477+
/// [`BindTarget`] specifying its register number and space, along with
478+
/// other details necessary to generate a full HLSL declaration for it,
479+
/// or to access its value.
480+
///
481+
/// This must provide a [`BindTarget`] for every [`GlobalVariable`] in the
482+
/// [`Module`] that has a [`binding`].
483+
///
484+
/// [`GlobalVariable`]: crate::ir::GlobalVariable
485+
/// [`ResourceBinding`]: crate::ir::ResourceBinding
486+
/// [`Module`]: crate::ir::Module
487+
/// [`binding`]: crate::ir::GlobalVariable::binding
358488
#[cfg_attr(
359489
feature = "deserialize",
360490
serde(deserialize_with = "deserialize_binding_map")
361491
)]
362492
pub binding_map: BindingMap,
493+
363494
/// Don't panic on missing bindings, instead generate any HLSL.
364495
pub fake_missing_bindings: bool,
365496
/// Add special constants to `SV_VertexIndex` and `SV_InstanceIndex`,
366497
/// to make them work like in Vulkan/Metal, with help of the host.
367498
pub special_constants_binding: Option<BindTarget>,
368-
/// Bind target of the push constant buffer
499+
500+
/// HLSL binding information for the [`PushConstant`] global, if present.
501+
///
502+
/// If a module contains a global in the [`PushConstant`] address space, the
503+
/// `dx12` backend stores its value directly in the root signature as a
504+
/// series of [`D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS`], whose binding
505+
/// information is given here.
506+
///
507+
/// [`PushConstant`]: crate::ir::AddressSpace::PushConstant
508+
/// [`D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS`]: https://learn.microsoft.com/en-us/windows/win32/api/d3d12/ne-d3d12-d3d12_root_parameter_type
369509
pub push_constants_target: Option<BindTarget>,
370-
/// Bind target of the sampler heap and comparison sampler heap.
510+
511+
/// HLSL binding information for the sampler heap and comparison sampler heap.
371512
pub sampler_heap_target: SamplerHeapBindTargets,
513+
372514
/// Mapping of each bind group's sampler index buffer to a bind target.
373515
#[cfg_attr(
374516
feature = "deserialize",
@@ -381,6 +523,18 @@ pub struct Options {
381523
serde(deserialize_with = "deserialize_storage_buffer_offsets")
382524
)]
383525
pub dynamic_storage_buffer_offsets_targets: DynamicStorageBufferOffsetsTargets,
526+
#[cfg_attr(
527+
feature = "deserialize",
528+
serde(deserialize_with = "deserialize_external_texture_binding_map")
529+
)]
530+
531+
/// HLSL binding information for [`External`] image global variables.
532+
///
533+
/// See [`ExternalTextureBindTarget`] for details.
534+
///
535+
/// [`External`]: crate::ir::ImageClass::External
536+
pub external_texture_binding_map: ExternalTextureBindingMap,
537+
384538
/// Should workgroup variables be zero initialized (by polyfilling)?
385539
pub zero_initialize_workgroup_memory: bool,
386540
/// Should we restrict indexing of vectors, matrices and arrays?
@@ -401,6 +555,7 @@ impl Default for Options {
401555
sampler_buffer_binding_map: alloc::collections::BTreeMap::default(),
402556
push_constants_target: None,
403557
dynamic_storage_buffer_offsets_targets: alloc::collections::BTreeMap::new(),
558+
external_texture_binding_map: ExternalTextureBindingMap::default(),
404559
zero_initialize_workgroup_memory: true,
405560
restrict_indexing: true,
406561
force_loop_bounding: true,
@@ -425,6 +580,29 @@ impl Options {
425580
None => Err(EntryPointError::MissingBinding(*res_binding)),
426581
}
427582
}
583+
584+
fn resolve_external_texture_resource_binding(
585+
&self,
586+
res_binding: &crate::ResourceBinding,
587+
) -> Result<ExternalTextureBindTarget, EntryPointError> {
588+
match self.external_texture_binding_map.get(res_binding) {
589+
Some(target) => Ok(*target),
590+
None if self.fake_missing_bindings => {
591+
let fake = BindTarget {
592+
space: res_binding.group as u8,
593+
register: res_binding.binding,
594+
binding_array_size: None,
595+
dynamic_storage_buffer_offsets_index: None,
596+
restrict_indexing: false,
597+
};
598+
Ok(ExternalTextureBindTarget {
599+
planes: [fake, fake, fake],
600+
params: fake,
601+
})
602+
}
603+
None => Err(EntryPointError::MissingBinding(*res_binding)),
604+
}
605+
}
428606
}
429607

430608
/// Reflection info for entry point names.
@@ -479,6 +657,7 @@ enum WrappedType {
479657
ArrayLength(help::WrappedArrayLength),
480658
ImageSample(help::WrappedImageSample),
481659
ImageQuery(help::WrappedImageQuery),
660+
ImageLoad(help::WrappedImageLoad),
482661
ImageLoadScalar(crate::Scalar),
483662
Constructor(help::WrappedConstructor),
484663
StructMatrixAccess(help::WrappedStructMatrixAccess),

0 commit comments

Comments
 (0)