[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.
This commit is contained in:
Jamie Nicol 2025-06-02 17:21:23 +01:00 committed by Jim Blandy
parent 0448b46033
commit e1ccb6632c
13 changed files with 1092 additions and 150 deletions

View File

@ -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

View File

@ -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<W: Write> 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<W: Write> 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<float4> plane0,")?;
writeln!(self.out, "{l1}Texture2D<float4> plane1,")?;
writeln!(self.out, "{l1}Texture2D<float4> 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<float4> plane0,")?;
writeln!(self.out, "{l1}Texture2D<float4> plane1,")?;
writeln!(self.out, "{l1}Texture2D<float4> 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<float4> tex, SamplerState samp, float2 coords) {{")?;
@ -291,7 +525,7 @@ impl<W: Write> 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<W: Write> 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";
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 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!(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<float4> plane0, Texture2D<float4> plane1, Texture2D<float4> 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 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 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 body
writeln!(self.out, "{{")?;
// 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, ")")?;
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!(),
};
// Write function body
writeln!(self.out, "{{")?;
// 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",
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!(),
};
(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)
// 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)
}
}
};
// 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}, ")?;
}
},
// write last parameter without comma and space for last parameter
write!(
self.out,
"{}.{}",
RETURN_VARIABLE_NAME,
COMPONENTS[number_of_params - 1]
)?;
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)?;
}
}
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]
)?;
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<W: Write> 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 } => {

View File

@ -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,
];

View File

@ -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<float4>`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<u32, OffsetsBindTarget>;
/// 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<float4>` 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<ExternalTextureBindingMap, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let vec = Vec::<ExternalTextureBindingMapSerialization>::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<crate::ResourceBinding, ExternalTextureBindTarget>;
/// 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<BindTarget>,
/// 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<BindTarget>,
/// 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<ExternalTextureBindTarget, EntryPointError> {
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),

View File

@ -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<crate::Expression>),
@ -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<crate::GlobalVariable>,
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<float4> {}: 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<crate::Function>,
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<crate::Function>,
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<float4> {}, Texture2D<float4> {}, Texture2D<float4> {}, {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, .. },
..

View File

@ -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,
}

View File

@ -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,

View File

@ -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 }

View File

@ -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<float4> tex_plane0_: register(t0);
Texture2D<float4> tex_plane1_: register(t1);
Texture2D<float4> 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<uint> nagaGroup0SamplerIndexArray : register(t0, space255);
static const SamplerState samp = nagaSamplerHeap[nagaGroup0SamplerIndexArray[0]];
float4 nagaTextureSampleBaseClampToEdge(
Texture2D<float4> plane0,
Texture2D<float4> plane1,
Texture2D<float4> 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<float4> plane0,
Texture2D<float4> plane1,
Texture2D<float4> 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<float4> plane0, Texture2D<float4> plane1, Texture2D<float4> plane2, NagaExternalTextureParams params) {
if (any(params.size)) {
return params.size;
} else {
uint2 ret;
plane0.GetDimensions(ret.x, ret.y);
return ret;
}
}
float4 test(Texture2D<float4> t_plane0_, Texture2D<float4> t_plane1_, Texture2D<float4> 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;
}

View File

@ -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",
),
],
)

View File

@ -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.

View File

@ -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,
},
})

View File

@ -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<L> {
/// 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],
}