[naga wgsl-in wgsl-out] WGSL support for texture_external texture type (#7822)

* [naga wgsl-in wgsl-out] WGSL support for texture_external texture type

Make wgsl-in correctly parse `texture_external` texture declarations,
and allow such textures to be used in `textureDimensions()`,
`textureSampleBaseClampToEdge()`, and `textureLoad()` function
calls. In IR these are represented by the `ImageClass::External` image
class, which is a 2D, non-multisampled, non-mipmapped, float-sampled
image.

Adds a new Capability `TEXTURE_EXTERNAL` and ensure validation rejects
shaders containing external textures if this capability flag is not
set. This capability is enabled for validation by wgpu devices which
support the `TEXTURE_EXTERNAL` feature (currently only when using the
noop backend), and by the Naga CLI when validating-only or when
outputting WGSL.

The WGSL backend can of course emit `ImageClass::External` images
directly as `texture_external` textures. Other backends are, for now,
unimplemented.

Lastly, we add a snapshot test covering all the valid uses of a
texture_external texture. These are:
  - As a global variable declaration
  - As an argument to the built-in functions `textureDimensions()`,
    `textureSampleBaseClampToEdge()`, and `textureLoad()`
  - As an argument to user-defined function declarations and calls.

We keep these in their own test so that we can control which targets
to run them against (currently WGSL and IR). When external textures
are supported by all Naga backends we can, if so inclined, integrate
these with existing texture tests.

* fixup! [naga wgsl-in wgsl-out] WGSL support for texture_external texture type

* fixup! [naga wgsl-in wgsl-out] WGSL support for texture_external texture type

---------

Co-authored-by: Jim Blandy <jimb@red-bean.com>
This commit is contained in:
Jamie Nicol
2025-07-22 22:38:32 +01:00
committed by GitHub
parent 4a00b62651
commit 43a4d53107
25 changed files with 774 additions and 7 deletions

View File

@@ -538,8 +538,8 @@ fn run() -> anyhow::Result<()> {
use naga::valid::Capabilities as C;
let missing = match Path::new(path).extension().and_then(|ex| ex.to_str()) {
Some("wgsl") => C::CLIP_DISTANCE | C::CULL_DISTANCE,
Some("metal") => C::CULL_DISTANCE,
_ => C::empty(),
Some("metal") => C::CULL_DISTANCE | C::TEXTURE_EXTERNAL,
_ => C::TEXTURE_EXTERNAL,
};
caps & !missing
});

View File

@@ -421,7 +421,8 @@ impl<W> Writer<'_, W> {
_ => {}
},
ImageClass::Sampled { multi: false, .. }
| ImageClass::Depth { multi: false } => {}
| ImageClass::Depth { multi: false }
| ImageClass::External => {}
}
}
_ => {}

View File

@@ -1176,6 +1176,7 @@ impl<'a, W: Write> Writer<'a, W> {
Ic::Depth { multi: true } => ("sampler", float, "MS", ""),
Ic::Depth { multi: false } => ("sampler", float, "", "Shadow"),
Ic::Storage { format, .. } => ("image", format.into(), "", ""),
Ic::External => unimplemented!(),
};
let precision = if self.options.version.is_es() {
@@ -3302,6 +3303,7 @@ impl<'a, W: Write> Writer<'a, W> {
write!(self.out, "imageSize(")?;
self.write_expr(image, ctx)?;
}
ImageClass::External => unimplemented!(),
}
write!(self.out, ")")?;
if components != 1 || self.options.version.is_es() {
@@ -3317,6 +3319,7 @@ impl<'a, W: Write> Writer<'a, W> {
let fun_name = match class {
ImageClass::Sampled { .. } | ImageClass::Depth { .. } => "textureSize",
ImageClass::Storage { .. } => "imageSize",
ImageClass::External => unimplemented!(),
};
write!(self.out, "{fun_name}(")?;
self.write_expr(image, ctx)?;
@@ -3336,6 +3339,7 @@ impl<'a, W: Write> Writer<'a, W> {
"textureSamples"
}
ImageClass::Storage { .. } => "imageSamples",
ImageClass::External => unimplemented!(),
};
write!(self.out, "{fun_name}(")?;
self.write_expr(image, ctx)?;
@@ -4618,6 +4622,7 @@ impl<'a, W: Write> Writer<'a, W> {
"WGSL `textureLoad` from depth textures is not supported in GLSL".to_string(),
))
}
crate::ImageClass::External => unimplemented!(),
};
// openGL es doesn't have 1D images so we need workaround it

View File

@@ -195,6 +195,7 @@ impl<W: Write> super::Writer<'_, W> {
let storage_format_str = format.to_hlsl_str();
write!(self.out, "<{storage_format_str}>")?
}
crate::ImageClass::External => unimplemented!(),
}
Ok(())
}
@@ -290,6 +291,7 @@ impl<W: Write> super::Writer<'_, W> {
crate::ImageClass::Depth { multi: false } => "Depth",
crate::ImageClass::Sampled { multi: false, .. } => "",
crate::ImageClass::Storage { .. } => "RW",
crate::ImageClass::External => unimplemented!(),
};
let arrayed_str = if query.arrayed { "Array" } else { "" };
let query_str = match query.query {
@@ -349,6 +351,7 @@ impl<W: Write> super::Writer<'_, W> {
let extra_coords = match wiq.class {
crate::ImageClass::Storage { .. } => 0,
crate::ImageClass::Sampled { .. } | crate::ImageClass::Depth { .. } => 1,
crate::ImageClass::External => unimplemented!(),
};
// GetDimensions Overloaded Methods

View File

@@ -321,6 +321,7 @@ impl Display for TypeContext<'_> {
};
("texture", "", format.into(), access)
}
crate::ImageClass::External => unimplemented!(),
};
let base_name = scalar.to_msl_name();
let array_str = if arrayed { "_array" } else { "" };
@@ -6637,6 +6638,7 @@ template <typename A>
"read-write textures".to_string(),
));
}
crate::ImageClass::External => unimplemented!(),
},
_ => {
return Err(Error::UnsupportedArrayOfType(base));

View File

@@ -118,6 +118,7 @@ impl Load {
crate::ImageClass::Depth { .. } | crate::ImageClass::Sampled { .. } => {
spirv::Op::ImageFetch
}
crate::ImageClass::External => unimplemented!(),
};
// `OpImageRead` and `OpImageFetch` instructions produce vec4<f32>

View File

@@ -276,6 +276,7 @@ impl LocalImageType {
flags: make_flags(false, ImageTypeFlags::empty()),
image_format: format.into(),
},
crate::ImageClass::External => unimplemented!(),
}
}
}

View File

@@ -1246,6 +1246,7 @@ impl Writer {
self.request_image_format_capabilities(format.into())?;
false
}
crate::ImageClass::External => unimplemented!(),
};
match dim {

View File

@@ -250,6 +250,9 @@ where
"texture_storage_{dim_str}{arrayed_str}<{format_str}{access_str}>"
)?;
}
Ic::External => {
write!(out, "texture_external")?;
}
}
}
TypeInner::Scalar(scalar) => {

View File

@@ -2138,6 +2138,7 @@ impl Frontend {
ImageClass::Depth { .. } => (true, false),
ImageClass::Storage { .. } => (false, true),
ImageClass::Sampled { .. } => (false, false),
ImageClass::External => unreachable!(),
};
let coordinate = match (image_size, coord_size) {
@@ -2259,6 +2260,7 @@ pub fn sampled_to_depth(
kind: ErrorKind::SemanticError("Not a texture".into()),
meta,
}),
ImageClass::External => unreachable!(),
},
_ => errors.push(Error {
kind: ErrorKind::SemanticError("Not a texture".into()),

View File

@@ -3587,9 +3587,12 @@ impl<'source, 'temp> Lowerer<'source, 'temp> {
self.expression_with_leaf_scalar(args.next()?, ir::Scalar::F32, ctx)?
}
// Sampling `Storage` textures isn't allowed at all. Let the
// validator report the error.
ir::ImageClass::Storage { .. } => self.expression(args.next()?, ctx)?,
// Sampling `External` textures with a specified level isn't
// allowed, and sampling `Storage` textures isn't allowed at
// all. Let the validator report the error.
ir::ImageClass::Storage { .. } | ir::ImageClass::External => {
self.expression(args.next()?, ctx)?
}
};
level = ir::SampleLevel::Exact(exact);
depth_ref = None;

View File

@@ -676,6 +676,7 @@ impl Parser {
| "texture_depth_cube"
| "texture_depth_cube_array"
| "texture_depth_multisampled_2d"
| "texture_external"
| "texture_storage_1d"
| "texture_storage_1d_array"
| "texture_storage_2d"
@@ -1867,6 +1868,11 @@ impl Parser {
arrayed: false,
class: crate::ImageClass::Depth { multi: true },
},
"texture_external" => ast::Type::Image {
dim: crate::ImageDimension::D2,
arrayed: false,
class: crate::ImageClass::External,
},
"texture_storage_1d" => {
let (format, access) = lexer.next_format_generic()?;
ast::Type::Image {

View File

@@ -652,6 +652,8 @@ pub enum ImageClass {
/// Multi-sampled depth image.
multi: bool,
},
/// External texture.
External,
/// Storage image.
Storage {
format: StorageFormat,

View File

@@ -383,6 +383,7 @@ impl super::ImageClass {
match self {
crate::ImageClass::Sampled { multi, .. } | crate::ImageClass::Depth { multi } => multi,
crate::ImageClass::Storage { .. } => false,
crate::ImageClass::External => false,
}
}
@@ -390,6 +391,7 @@ impl super::ImageClass {
match self {
crate::ImageClass::Sampled { multi, .. } | crate::ImageClass::Depth { multi } => !multi,
crate::ImageClass::Storage { .. } => false,
crate::ImageClass::External => false,
}
}

View File

@@ -512,6 +512,10 @@ impl<'a> ResolveContext<'a> {
scalar: format.into(),
size: crate::VectorSize::Quad,
},
crate::ImageClass::External => Ti::Vector {
scalar: crate::Scalar::F32,
size: crate::VectorSize::Quad,
},
}),
ref other => {
log::error!("Image type {other:?}");

View File

@@ -460,6 +460,7 @@ impl super::Validator {
kind: crate::ScalarKind::Uint | crate::ScalarKind::Sint,
multi: false,
} if gather.is_some() => false,
crate::ImageClass::External => false,
crate::ImageClass::Depth { multi: false } => true,
_ => return Err(ExpressionError::InvalidImageClass(class)),
};
@@ -551,7 +552,7 @@ impl super::Validator {
crate::ImageClass::Sampled {
kind: crate::ScalarKind::Float,
multi: false
}
} | crate::ImageClass::External
) {
return Err(ExpressionError::InvalidSampleClampCoordinateToEdge(
alloc::format!("image class `{class:?}`"),

View File

@@ -168,6 +168,8 @@ bitflags::bitflags! {
const RAY_HIT_VERTEX_POSITION = 1 << 25;
/// Support for 16-bit floating-point types.
const SHADER_FLOAT16 = 1 << 26;
/// Support for [`ImageClass::External`]
const TEXTURE_EXTERNAL = 1 << 27;
}
}

View File

@@ -732,6 +732,16 @@ impl super::Validator {
if arrayed && matches!(dim, crate::ImageDimension::Cube) {
self.require_type_capability(Capabilities::CUBE_ARRAY_TEXTURES)?;
}
if matches!(class, crate::ImageClass::External) {
if dim != crate::ImageDimension::D2 || arrayed {
return Err(TypeError::UnsupportedImageType {
dim,
arrayed,
class,
});
}
self.require_type_capability(Capabilities::TEXTURE_EXTERNAL)?;
}
TypeInfo::new(
TypeFlags::ARGUMENT | TypeFlags::CREATION_RESOLVED,
Alignment::ONE,

View File

@@ -0,0 +1,2 @@
god_mode = true
targets = "IR | WGSL"

View File

@@ -0,0 +1,27 @@
@group(0) @binding(0)
var tex: texture_external;
@group(0) @binding(1)
var samp: sampler;
fn test(t: texture_external) -> vec4<f32> {
var a = textureSampleBaseClampToEdge(t, samp, vec2(0.0f));
var b = textureLoad(t, vec2(0u));
var c = textureDimensions(t);
return a + b + vec2f(c).xyxy;
}
@fragment
fn fragment_main() -> @location(0) vec4<f32> {
return test(tex);
}
@vertex
fn vertex_main() -> @builtin(position) vec4<f32> {
return test(tex);
}
@compute @workgroup_size(1)
fn compute_main() {
test(tex);
}

View File

@@ -0,0 +1,320 @@
(
types: [
(
name: None,
inner: Image(
dim: D2,
arrayed: false,
class: External,
),
),
(
name: None,
inner: Sampler(
comparison: false,
),
),
(
name: None,
inner: Vector(
size: Quad,
scalar: (
kind: Float,
width: 4,
),
),
),
(
name: None,
inner: Vector(
size: Bi,
scalar: (
kind: Uint,
width: 4,
),
),
),
],
special_types: (
ray_desc: None,
ray_intersection: None,
ray_vertex_return: None,
predeclared_types: {},
),
constants: [],
overrides: [],
global_variables: [
(
name: Some("tex"),
space: Handle,
binding: Some((
group: 0,
binding: 0,
)),
ty: 0,
init: None,
),
(
name: Some("samp"),
space: Handle,
binding: Some((
group: 0,
binding: 1,
)),
ty: 1,
init: None,
),
],
global_expressions: [],
functions: [
(
name: Some("test"),
arguments: [
(
name: Some("t"),
ty: 0,
binding: None,
),
],
result: Some((
ty: 2,
binding: None,
)),
local_variables: [
(
name: Some("a"),
ty: 2,
init: None,
),
(
name: Some("b"),
ty: 2,
init: None,
),
(
name: Some("c"),
ty: 3,
init: None,
),
],
expressions: [
FunctionArgument(0),
GlobalVariable(1),
Literal(F32(0.0)),
Splat(
size: Bi,
value: 2,
),
ImageSample(
image: 0,
sampler: 1,
gather: None,
coordinate: 3,
array_index: None,
offset: None,
level: Zero,
depth_ref: None,
clamp_to_edge: true,
),
LocalVariable(0),
Literal(U32(0)),
Splat(
size: Bi,
value: 6,
),
ImageLoad(
image: 0,
coordinate: 7,
array_index: None,
sample: None,
level: None,
),
LocalVariable(1),
ImageQuery(
image: 0,
query: Size(
level: None,
),
),
LocalVariable(2),
Load(
pointer: 5,
),
Load(
pointer: 9,
),
Binary(
op: Add,
left: 12,
right: 13,
),
Load(
pointer: 11,
),
As(
expr: 15,
kind: Float,
convert: Some(4),
),
Swizzle(
size: Quad,
vector: 16,
pattern: (X, Y, X, Y),
),
Binary(
op: Add,
left: 14,
right: 17,
),
],
named_expressions: {
0: "t",
},
body: [
Emit((
start: 3,
end: 5,
)),
Store(
pointer: 5,
value: 4,
),
Emit((
start: 7,
end: 9,
)),
Store(
pointer: 9,
value: 8,
),
Emit((
start: 10,
end: 11,
)),
Store(
pointer: 11,
value: 10,
),
Emit((
start: 12,
end: 19,
)),
Return(
value: Some(18),
),
],
diagnostic_filter_leaf: None,
),
],
entry_points: [
(
name: "fragment_main",
stage: Fragment,
early_depth_test: None,
workgroup_size: (0, 0, 0),
workgroup_size_overrides: None,
function: (
name: Some("fragment_main"),
arguments: [],
result: Some((
ty: 2,
binding: Some(Location(
location: 0,
interpolation: Some(Perspective),
sampling: Some(Center),
blend_src: None,
)),
)),
local_variables: [],
expressions: [
GlobalVariable(0),
CallResult(0),
],
named_expressions: {},
body: [
Call(
function: 0,
arguments: [
0,
],
result: Some(1),
),
Return(
value: Some(1),
),
],
diagnostic_filter_leaf: None,
),
),
(
name: "vertex_main",
stage: Vertex,
early_depth_test: None,
workgroup_size: (0, 0, 0),
workgroup_size_overrides: None,
function: (
name: Some("vertex_main"),
arguments: [],
result: Some((
ty: 2,
binding: Some(BuiltIn(Position(
invariant: false,
))),
)),
local_variables: [],
expressions: [
GlobalVariable(0),
CallResult(0),
],
named_expressions: {},
body: [
Call(
function: 0,
arguments: [
0,
],
result: Some(1),
),
Return(
value: Some(1),
),
],
diagnostic_filter_leaf: None,
),
),
(
name: "compute_main",
stage: Compute,
early_depth_test: None,
workgroup_size: (1, 1, 1),
workgroup_size_overrides: None,
function: (
name: Some("compute_main"),
arguments: [],
result: None,
local_variables: [],
expressions: [
GlobalVariable(0),
CallResult(0),
],
named_expressions: {},
body: [
Call(
function: 0,
arguments: [
0,
],
result: Some(1),
),
Return(
value: None,
),
],
diagnostic_filter_leaf: None,
),
),
],
diagnostic_filters: [],
diagnostic_filter_leaf: None,
doc_comments: None,
)

View File

@@ -0,0 +1,320 @@
(
types: [
(
name: None,
inner: Image(
dim: D2,
arrayed: false,
class: External,
),
),
(
name: None,
inner: Sampler(
comparison: false,
),
),
(
name: None,
inner: Vector(
size: Quad,
scalar: (
kind: Float,
width: 4,
),
),
),
(
name: None,
inner: Vector(
size: Bi,
scalar: (
kind: Uint,
width: 4,
),
),
),
],
special_types: (
ray_desc: None,
ray_intersection: None,
ray_vertex_return: None,
predeclared_types: {},
),
constants: [],
overrides: [],
global_variables: [
(
name: Some("tex"),
space: Handle,
binding: Some((
group: 0,
binding: 0,
)),
ty: 0,
init: None,
),
(
name: Some("samp"),
space: Handle,
binding: Some((
group: 0,
binding: 1,
)),
ty: 1,
init: None,
),
],
global_expressions: [],
functions: [
(
name: Some("test"),
arguments: [
(
name: Some("t"),
ty: 0,
binding: None,
),
],
result: Some((
ty: 2,
binding: None,
)),
local_variables: [
(
name: Some("a"),
ty: 2,
init: None,
),
(
name: Some("b"),
ty: 2,
init: None,
),
(
name: Some("c"),
ty: 3,
init: None,
),
],
expressions: [
FunctionArgument(0),
GlobalVariable(1),
Literal(F32(0.0)),
Splat(
size: Bi,
value: 2,
),
ImageSample(
image: 0,
sampler: 1,
gather: None,
coordinate: 3,
array_index: None,
offset: None,
level: Zero,
depth_ref: None,
clamp_to_edge: true,
),
LocalVariable(0),
Literal(U32(0)),
Splat(
size: Bi,
value: 6,
),
ImageLoad(
image: 0,
coordinate: 7,
array_index: None,
sample: None,
level: None,
),
LocalVariable(1),
ImageQuery(
image: 0,
query: Size(
level: None,
),
),
LocalVariable(2),
Load(
pointer: 5,
),
Load(
pointer: 9,
),
Binary(
op: Add,
left: 12,
right: 13,
),
Load(
pointer: 11,
),
As(
expr: 15,
kind: Float,
convert: Some(4),
),
Swizzle(
size: Quad,
vector: 16,
pattern: (X, Y, X, Y),
),
Binary(
op: Add,
left: 14,
right: 17,
),
],
named_expressions: {
0: "t",
},
body: [
Emit((
start: 3,
end: 5,
)),
Store(
pointer: 5,
value: 4,
),
Emit((
start: 7,
end: 9,
)),
Store(
pointer: 9,
value: 8,
),
Emit((
start: 10,
end: 11,
)),
Store(
pointer: 11,
value: 10,
),
Emit((
start: 12,
end: 19,
)),
Return(
value: Some(18),
),
],
diagnostic_filter_leaf: None,
),
],
entry_points: [
(
name: "fragment_main",
stage: Fragment,
early_depth_test: None,
workgroup_size: (0, 0, 0),
workgroup_size_overrides: None,
function: (
name: Some("fragment_main"),
arguments: [],
result: Some((
ty: 2,
binding: Some(Location(
location: 0,
interpolation: Some(Perspective),
sampling: Some(Center),
blend_src: None,
)),
)),
local_variables: [],
expressions: [
GlobalVariable(0),
CallResult(0),
],
named_expressions: {},
body: [
Call(
function: 0,
arguments: [
0,
],
result: Some(1),
),
Return(
value: Some(1),
),
],
diagnostic_filter_leaf: None,
),
),
(
name: "vertex_main",
stage: Vertex,
early_depth_test: None,
workgroup_size: (0, 0, 0),
workgroup_size_overrides: None,
function: (
name: Some("vertex_main"),
arguments: [],
result: Some((
ty: 2,
binding: Some(BuiltIn(Position(
invariant: false,
))),
)),
local_variables: [],
expressions: [
GlobalVariable(0),
CallResult(0),
],
named_expressions: {},
body: [
Call(
function: 0,
arguments: [
0,
],
result: Some(1),
),
Return(
value: Some(1),
),
],
diagnostic_filter_leaf: None,
),
),
(
name: "compute_main",
stage: Compute,
early_depth_test: None,
workgroup_size: (1, 1, 1),
workgroup_size_overrides: None,
function: (
name: Some("compute_main"),
arguments: [],
result: None,
local_variables: [],
expressions: [
GlobalVariable(0),
CallResult(0),
],
named_expressions: {},
body: [
Call(
function: 0,
arguments: [
0,
],
result: Some(1),
),
Return(
value: None,
),
],
diagnostic_filter_leaf: None,
),
),
],
diagnostic_filters: [],
diagnostic_filter_leaf: None,
doc_comments: None,
)

View File

@@ -0,0 +1,39 @@
@group(0) @binding(0)
var tex: texture_external;
@group(0) @binding(1)
var samp: sampler;
fn test(t: texture_external) -> vec4<f32> {
var a: vec4<f32>;
var b: vec4<f32>;
var c: vec2<u32>;
let _e4 = textureSampleBaseClampToEdge(t, samp, vec2(0f));
a = _e4;
let _e8 = textureLoad(t, vec2(0u));
b = _e8;
let _e10 = textureDimensions(t);
c = _e10;
let _e12 = a;
let _e13 = b;
let _e15 = c;
return ((_e12 + _e13) + vec2<f32>(_e15).xyxy);
}
@fragment
fn fragment_main() -> @location(0) vec4<f32> {
let _e1 = test(tex);
return _e1;
}
@vertex
fn vertex_main() -> @builtin(position) vec4<f32> {
let _e1 = test(tex);
return _e1;
}
@compute @workgroup_size(1, 1, 1)
fn compute_main() {
let _e1 = test(tex);
return;
}

View File

@@ -494,6 +494,10 @@ pub fn create_validator(
Caps::RAY_HIT_VERTEX_POSITION,
features.intersects(wgt::Features::EXPERIMENTAL_RAY_HIT_VERTEX_RETURN),
);
caps.set(
Caps::TEXTURE_EXTERNAL,
features.intersects(wgt::Features::EXTERNAL_TEXTURE),
);
naga::valid::Validator::new(flags, caps)
}

View File

@@ -46,6 +46,10 @@ impl From<&ResourceType> for BindingTypeName {
fn from(ty: &ResourceType) -> BindingTypeName {
match ty {
ResourceType::Buffer { .. } => BindingTypeName::Buffer,
ResourceType::Texture {
class: naga::ImageClass::External,
..
} => BindingTypeName::ExternalTexture,
ResourceType::Texture { .. } => BindingTypeName::Texture,
ResourceType::Sampler { .. } => BindingTypeName::Sampler,
ResourceType::AccelerationStructure { .. } => BindingTypeName::AccelerationStructure,
@@ -594,6 +598,7 @@ impl Resource {
access: naga_access,
}
}
BindingType::ExternalTexture => naga::ImageClass::External,
_ => {
return Err(BindingError::WrongType {
binding: (&entry.ty).into(),
@@ -701,6 +706,7 @@ impl Resource {
f
},
},
naga::ImageClass::External => BindingType::ExternalTexture,
}
}
ResourceType::AccelerationStructure { vertex_return } => {