diff --git a/naga-cli/src/bin/naga.rs b/naga-cli/src/bin/naga.rs
index b018058613..44369e9df7 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 bea917547b..83d3a71816 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,9 @@ 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 +262,233 @@ 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 = params.size != 0 ? 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, "{l2}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 taking in to account the transform,
+                // bearing in mind that it may contain a flip on either axis. We must
+                // calculate and adjust for the half-texel separately for each plane as
+                // it depends on the 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 +520,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 +551,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";
-
-        // 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<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 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}, ")?;
-        }
 
-        // write last parameter without comma and space for last parameter
-        write!(
-            self.out,
-            "{}.{}",
-            RETURN_VARIABLE_NAME,
-            COMPONENTS[number_of_params - 1]
-        )?;
+                for component in COMPONENTS[..number_of_params - 1].iter() {
+                    write!(self.out, "{RETURN_VARIABLE_NAME}.{component}, ")?;
+                }
 
-        writeln!(self.out, ");")?;
+                // write last parameter without comma and space for last parameter
+                write!(
+                    self.out,
+                    "{}.{}",
+                    RETURN_VARIABLE_NAME,
+                    COMPONENTS[number_of_params - 1]
+                )?;
 
-        // Write return value
-        writeln!(
-            self.out,
-            "{INDENT}return {RETURN_VARIABLE_NAME}.{ret_swizzle};"
-        )?;
+                writeln!(self.out, ");")?;
 
-        // 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 +1817,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 } => {
diff --git a/naga/src/back/hlsl/keywords.rs b/naga/src/back/hlsl/keywords.rs
index f38d47483f..7b2ab839d4 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 7f55398040..0c9dd4bf6e 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<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
@@ -335,6 +366,41 @@ where
 
 pub type DynamicStorageBufferOffsetsTargets = alloc::collections::BTreeMap<u32, OffsetsBindTarget>;
 
+#[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 {
+    pub planes: [BindTarget; 3],
+    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>;
 
@@ -381,6 +447,11 @@ 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")
+    )]
+    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 +472,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 +497,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 +574,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 2f98b1fbfe..1cd388d5e1 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<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,25 +1575,51 @@ 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)];
+                    // 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
+                    {
+                        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} {}",
+                            plane_names[0], plane_names[1], plane_names[2], params_name)?;
+                    } else {
+                        // 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)?;
+                        // 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)?;
+                        }
                     }
                 }
             }
@@ -3117,9 +3233,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 +3423,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 +4281,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/back/mod.rs b/naga/src/back/mod.rs
index d7b14475bf..0d13d63dd9 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/back/wgsl/writer.rs b/naga/src/back/wgsl/writer.rs
index 58c1b734f0..dd2696c17f 100644
--- a/naga/src/back/wgsl/writer.rs
+++ b/naga/src/back/wgsl/writer.rs
@@ -109,12 +109,28 @@ impl<W: Write> Writer<W> {
         self.required_polyfills.clear();
     }
 
-    fn is_builtin_wgsl_struct(&self, module: &Module, handle: Handle<crate::Type>) -> 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<crate::Type>) -> 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 fe4296be84..7fbb5c532f 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 9a01b637d5..d58561796b 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<crate::Type> {
+        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 c8545ca654..3b6e7806c3 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 0101954849..f21992ab03 100644
--- a/naga/src/ir/mod.rs
+++ b/naga/src/ir/mod.rs
@@ -2346,6 +2346,25 @@ pub struct SpecialTypes {
     /// Call [`Module::generate_vertex_return_type`]
     pub ray_vertex_return: Option<Handle<Type>>,
 
+    /// Struct containing parameters required by some backends to emit code for
+    /// [`ImageClass::External`] textures.
+    ///
+    /// In WGSL, this type would be:
+    ///
+    /// ```ignore
+    /// struct NagaExternalTextureParams {      // align size offset
+    ///     yuv_conversion_matrix: mat4x4<f32>, //    16   64      0
+    ///     sample_transform: mat3x2<f32>,      //     8   24     64
+    ///     load_transform: mat3x2<f32>,        //     8   24     88
+    ///     size: vec2<u32>,                    //     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<Handle<Type>>,
+
     /// Types for predeclared wgsl types instantiated on demand.
     ///
     /// Call [`Module::generate_predeclared_type`] to populate this if
diff --git a/naga/src/proc/mod.rs b/naga/src/proc/mod.rs
index f2584c64b3..413e49c1ee 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 504617dd1d..0b812ff036 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<crate::Constant>),
@@ -37,6 +71,17 @@ pub enum NameKey {
 
     /// Entry point version of `FunctionOobLocal`.
     EntryPointOobLocal(EntryPointIndex, Handle<crate::Type>),
+
+    /// A global variable holding a component of a lowered external texture.
+    ///
+    /// See [`ExternalTextureNameKey`] for details.
+    ExternalTextureGlobalVariable(Handle<crate::GlobalVariable>, ExternalTextureNameKey),
+
+    /// A function argument holding a component of a lowered external
+    /// texture.
+    ///
+    /// See [`ExternalTextureNameKey`] for details.
+    ExternalTextureFunctionArgument(Handle<crate::Function>, 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() {
diff --git a/naga/tests/in/wgsl/texture-external.toml b/naga/tests/in/wgsl/texture-external.toml
index f8f46d4223..3ea75be88d 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 0000000000..baf8141521
--- /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<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 = params.size != 0 ? 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;
+}
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 0000000000..23afa21e1f
--- /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/naga/tests/out/ir/spv-fetch_depth.compact.ron b/naga/tests/out/ir/spv-fetch_depth.compact.ron
index 024f918022..c2b7b9b5f6 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 8daa29847f..dd019d62aa 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 7f4e6df5c1..04547e651c 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 35a9e48bef..0babda31f5 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 698a807258..a07dd0aca0 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 5afd71ed65..643d3c7303 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 5e15ff84ca..fff8d0bcf9 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 5e15ff84ca..fff8d0bcf9 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 ce0d6d8c89..30168b2629 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 ce0d6d8c89..30168b2629 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 c10c9f97d3..4d77a57494 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 c10c9f97d3..4d77a57494 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 dc4d2defdb..10f533f105 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 dc4d2defdb..10f533f105 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 8f3de4f3e2..31a6010541 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 8f3de4f3e2..31a6010541 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 8bce0bb008..b7deb55aca 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 8bce0bb008..b7deb55aca 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 0534e73639..4d148b9061 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 0534e73639..4d148b9061 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 ac45d5bad6..1e568ebbc3 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 ac45d5bad6..1e568ebbc3 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 0848ed2b73..649ba28d47 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 0848ed2b73..649ba28d47 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 4c40ef32de..eca5156107 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 4c40ef32de..eca5156107 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 4f1325da56..eb70e3badd 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 4f1325da56..eb70e3badd 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 67ec4ffb52..bfcd83f7f3 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 67ec4ffb52..bfcd83f7f3 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 4c0876618d..2bceb18a51 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 4c0876618d..2bceb18a51 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 dde1bd367b..1139fab197 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 3a23c49c4f..a1761c17c8 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-hal/src/dx12/device.rs b/wgpu-hal/src/dx12/device.rs
index 7d245e8ce3..fb0a39528b 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,
             },
         })