Add F16_IN_F32 downlevel flag for pack/unpack/quantize f16 (#8130)

Although the operation of these functions is defined in terms of f16
semantics, the input/output types are not f16, and they are generally
available even when native `f16` support is not. But in at least one
case, they are only available with `f16` support, so add a new downlevel
flag that is cleared when these functions are not available.

Add some infrastructure to simplify testing of missing
capabilities/extensions, and add tests for a few more kinds of f16
usage.

Co-authored-by: Erich Gubler <erichdongubler@gmail.com>
This commit is contained in:
Andy Leiserson 2025-08-22 10:05:44 -07:00
parent 1689c56ee3
commit 4a7b6abb19
15 changed files with 345 additions and 109 deletions

View File

@ -110,6 +110,7 @@ By @cwfitzgerald in [#8162](https://github.com/gfx-rs/wgpu/pull/8162).
By @kpreid in [#8011](https://github.com/gfx-rs/wgpu/pull/8011).
- Make a compacted hal acceleration structure inherit a label from the base BLAS. By @Vecvec in [#8103](https://github.com/gfx-rs/wgpu/pull/8103).
- The limits requested for a device must now satisfy `min_subgroup_size <= max_subgroup_size`. By @andyleiserson in [#8085](https://github.com/gfx-rs/wgpu/pull/8085).
- Require new `F16_IN_F32` downlevel flag for `quantizeToF16`, `pack2x16float`, and `unpack2x16float` in WGSL input. By @aleiserson in [#8130](https://github.com/gfx-rs/wgpu/pull/8130).
#### naga

View File

@ -1060,6 +1060,20 @@ impl super::Validator {
arg2,
arg3,
} => {
if matches!(
fun,
crate::MathFunction::QuantizeToF16
| crate::MathFunction::Pack2x16float
| crate::MathFunction::Unpack2x16float
) && !self
.capabilities
.contains(crate::valid::Capabilities::SHADER_FLOAT16_IN_FLOAT32)
{
return Err(ExpressionError::MissingCapabilities(
crate::valid::Capabilities::SHADER_FLOAT16_IN_FLOAT32,
));
}
let actuals: &[_] = match (arg1, arg2, arg3) {
(None, None, None) => &[arg],
(Some(arg1), None, None) => &[arg, arg1],

View File

@ -170,6 +170,27 @@ bitflags::bitflags! {
const SHADER_FLOAT16 = 1 << 26;
/// Support for [`ImageClass::External`]
const TEXTURE_EXTERNAL = 1 << 27;
/// Support for `quantizeToF16`, `pack2x16float`, and `unpack2x16float`, which store
/// `f16`-precision values in `f32`s.
const SHADER_FLOAT16_IN_FLOAT32 = 1 << 28;
}
}
impl Capabilities {
/// Returns the extension corresponding to this capability, if there is one.
///
/// This is used by integration tests.
#[cfg(feature = "wgsl-in")]
#[doc(hidden)]
pub const fn extension(&self) -> Option<crate::front::wgsl::ImplementedEnableExtension> {
use crate::front::wgsl::ImplementedEnableExtension as Ext;
match *self {
Self::DUAL_SOURCE_BLENDING => Some(Ext::DualSourceBlending),
// NOTE: `SHADER_FLOAT16_IN_FLOAT32` _does not_ require the `f16` extension
Self::SHADER_FLOAT16 => Some(Ext::F16),
Self::CLIP_DISTANCE => Some(Ext::ClipDistances),
_ => None,
}
}
}

View File

@ -0,0 +1 @@
god_mode = true # requires F16_IN_F32

View File

@ -1,3 +1,4 @@
god_mode = true # requires F16_IN_F32
targets = "METAL"
[msl]

View File

@ -1,3 +1,5 @@
god_mode = true # requires F16_IN_F32
[msl]
fake_missing_bindings = false
lang_version = [1, 2]

View File

@ -0,0 +1 @@
god_mode = true # requires F16_IN_F32

View File

@ -1,3 +1,7 @@
//! Tests of the module validator.
//!
//! There are also some validation tests in [`wgsl_errors`](super::wgsl_errors).
#![allow(
// We need to investigate these.
clippy::result_large_err

View File

@ -1,6 +1,10 @@
/*!
Tests for the WGSL front end.
*/
//! Tests for the WGSL front end.
//!
//! This file also contains some tests of the module validator. In some cases,
//! the validator and the frontend both raise an error, and it is easier to
//! have both tests in one place. In other cases, it might be more appropriate
//! for the validator tests to be in the `validation` test suite.
#![cfg(feature = "wgsl-in")]
#![allow(
// We need to investigate these.
@ -950,50 +954,6 @@ fn matrix_constructor_inferred() {
);
}
#[test]
fn float16_requires_enable() {
check(
r#"
const a: f16 = 1.0;
"#,
r#"error: the `f16` enable extension is not enabled
wgsl:2:22
2 const a: f16 = 1.0;
^^^ the `f16` "Enable Extension" is needed for this functionality, but it is not currently enabled.
= note: You can enable this extension by adding `enable f16;` at the top of the shader, before any other items.
"#,
);
check(
r#"
const a = 1.0h;
"#,
r#"error: the `f16` enable extension is not enabled
wgsl:2:23
2 const a = 1.0h;
^^^^ the `f16` "Enable Extension" is needed for this functionality, but it is not currently enabled.
= note: You can enable this extension by adding `enable f16;` at the top of the shader, before any other items.
"#,
);
}
#[test]
fn multiple_enables_valid() {
check_success(
r#"
enable f16;
enable f16;
const a: f16 = 1.0h;
"#,
);
}
/// Check the result of validating a WGSL program against a pattern.
///
/// Unless you are generating code programmatically, the
@ -1042,6 +1002,125 @@ macro_rules! check_one_validation {
}
}
/// Test validation of required extensions and capabilities.
///
/// This tests that the shader is rejected either if the required extension is
/// not declared in an `enable` directive, or if the validator is configured
/// without the required capability.
///
/// For the first case, we use the supplied test case source verbatim (which
/// should not include the `enable` directive), and check for a parse error
/// matching the expected error message text. For the second case, we add the
/// `enable` directive to the supplied test case, and check for a validation
/// error matching the expected pattern.
///
/// The WGSL frontend is not the only way of producing Naga IR, and the
/// validator must reject an invalid module however produced. So it is important
/// that the validator check for missing capabilities. Checking missing
/// extensions in the frontend as well can produce better error messages or
/// simplify implementation of the frontend by eliminating some cases of invalid
/// programs earlier.
///
/// Multiple capabilities can be specified in the macro argument in the case
/// where any one of them grants access to a feature (e.g. `SUBGROUP` and
/// `SUBGROUP_BARRIER` for `subgroupBarrier`). When passing multiple capabilities,
/// all of the passed capabilities must be covered by the same enable-extension.
///
/// NOTE: The only reason we don't use a function for this is because we need to syntactically
/// re-use `$val_err_pat`.
macro_rules! check_extension_validation {
( $caps:expr, $source:expr, $parse_err:expr, $val_err_pat:pat ) => {
let caps = $caps;
let source = $source;
let mut ext = None;
for cap in caps.iter() {
match cap.extension() {
Some(this_ext) if ext.is_none() => ext = Some(this_ext),
Some(this_ext) if ext.is_some_and(|ext| ext != this_ext) => {
panic!(
concat!(
"the capabilities {:?} in `check_extension_validation` ",
"are not all covered by the same extension ",
"(found both {:?} and {:?})",
),
caps, ext, this_ext,
);
}
_ => {}
}
}
let Some(ext) = ext else {
panic!(
concat!(
"None of the capabilities {:?} in `check_extension_validation` ",
"are associated with an extension. ",
"Use `check_validation!` to check validator behavior ",
"when there isn't a corresponding parse error.",
),
caps
);
};
let directive = format!(
"enable {};",
naga::front::wgsl::EnableExtension::Implemented(ext).to_ident()
);
assert!(
!source.contains(&directive),
"test case for `check_extension_validation!` should not contain the enable directive",
);
// First check, for the expected WGSL parse error when extension is not enabled
check(&source, $parse_err);
let source_with_enable = format!("{directive}\n{source}");
let module = match naga::front::wgsl::parse_str(&source_with_enable) {
Ok(module) => module,
Err(err) => {
eprintln!("WGSL parse failed:");
panic!("{}", err.emit_to_string(source));
}
};
// Second check, for the expected validation error when the capability is not present
let error = naga::valid::Validator::new(naga::valid::ValidationFlags::all(), !caps)
.validate(&module)
.map_err(|e| e.into_inner()); // TODO: Add tests for spans, too?
#[allow(clippy::redundant_pattern_matching)]
if !matches!(&error, $val_err_pat) {
eprintln!(
concat!(
"validation error without {:?} does not match pattern:\n",
"source code: {}\n",
"\n",
"actual result:\n",
"{:#?}\n",
"\n",
"expected match for pattern:\n",
"{}",
),
caps,
&source,
error,
stringify!($val_err_pat)
);
panic!("validation error does not match pattern");
}
// Also check that when multiple capabililiites can enable a feature,
// any one of them is sufficient.
if !caps.bits().is_power_of_two() {
for cap in caps.iter() {
let res = naga::valid::Validator::new(naga::valid::ValidationFlags::all(), cap)
.validate(&module);
match res {
Ok(_) => {}
Err(err) => panic!("Module did not validate with only {cap:?}: {err:?}"),
}
}
}
};
}
macro_rules! check_validation {
// We want to support an optional guard expression after the pattern, so
// that we can check values we can't match against, like strings.
@ -1100,12 +1179,136 @@ fn int64_capability() {
}
#[test]
fn float16_capability() {
check_validation! {
"enable f16; var input: f16;",
"enable f16; var input: vec2<f16>;":
fn multiple_enables_valid() {
check_success(
r#"
enable f16;
enable f16;
const a: f16 = 1.0h;
"#,
);
}
#[test]
fn float16_capability_and_enable() {
// A zero value expression
check_extension_validation! {
Capabilities::SHADER_FLOAT16,
r#"fn foo() {
let a = f16();
}
"#,
r#"error: the `f16` enable extension is not enabled
wgsl:2:21
2 let a = f16();
^^^ the `f16` "Enable Extension" is needed for this functionality, but it is not currently enabled.
= note: You can enable this extension by adding `enable f16;` at the top of the shader, before any other items.
"#,
Err(naga::valid::ValidationError::Type {
source: naga::valid::TypeError::WidthError(naga::valid::WidthError::MissingCapability {flag: "FLOAT16",..}),
source: naga::valid::TypeError::WidthError(naga::valid::WidthError::MissingCapability { flag: "FLOAT16", .. }),
..
})
}
// Literals
check_extension_validation! {
Capabilities::SHADER_FLOAT16,
r#"fn foo() {
let a = f16(1);
}
"#,
r#"error: the `f16` enable extension is not enabled
wgsl:2:21
2 let a = f16(1);
^^^ the `f16` "Enable Extension" is needed for this functionality, but it is not currently enabled.
= note: You can enable this extension by adding `enable f16;` at the top of the shader, before any other items.
"#,
Err(naga::valid::ValidationError::Function {
source: naga::valid::FunctionError::Expression {
source: naga::valid::ExpressionError::Literal(
naga::valid::LiteralError::Width(
naga::valid::WidthError::MissingCapability { flag: "FLOAT16", .. }
)
),
..
},
..
})
}
check_extension_validation! {
Capabilities::SHADER_FLOAT16,
r#"
const a = 1.0h;
"#,
r#"error: the `f16` enable extension is not enabled
wgsl:2:23
2 const a = 1.0h;
^^^^ the `f16` "Enable Extension" is needed for this functionality, but it is not currently enabled.
= note: You can enable this extension by adding `enable f16;` at the top of the shader, before any other items.
"#,
Err(naga::valid::ValidationError::Type {
source: naga::valid::TypeError::WidthError(naga::valid::WidthError::MissingCapability { flag: "FLOAT16", .. }),
..
})
}
// `f16`-typed declarations
check_extension_validation! {
Capabilities::SHADER_FLOAT16,
r#"
const a: f16 = 1.0;
"#,
r#"error: the `f16` enable extension is not enabled
wgsl:2:22
2 const a: f16 = 1.0;
^^^ the `f16` "Enable Extension" is needed for this functionality, but it is not currently enabled.
= note: You can enable this extension by adding `enable f16;` at the top of the shader, before any other items.
"#,
Err(naga::valid::ValidationError::Type {
source: naga::valid::TypeError::WidthError(naga::valid::WidthError::MissingCapability { flag: "FLOAT16", .. }),
..
})
}
check_extension_validation! {
Capabilities::SHADER_FLOAT16,
"var input: f16;",
r#"error: the `f16` enable extension is not enabled
wgsl:1:12
1 var input: f16;
^^^ the `f16` "Enable Extension" is needed for this functionality, but it is not currently enabled.
= note: You can enable this extension by adding `enable f16;` at the top of the shader, before any other items.
"#,
Err(naga::valid::ValidationError::Type {
source: naga::valid::TypeError::WidthError(naga::valid::WidthError::MissingCapability { flag: "FLOAT16", .. }),
..
})
}
// Functions that operate on `f16`-precision values stored in `f32`s.
check_validation! {
"fn foo() -> f32 { return quantizeToF16(1.0f); }",
"fn foo() -> u32 { return pack2x16float(vec2(1.0f, 2.0f)); }",
"fn foo() -> vec2<f32> { return unpack2x16float(0x7c007c00); }":
Err(naga::valid::ValidationError::Function {
source: naga::valid::FunctionError::Expression {
source: naga::valid::ExpressionError::MissingCapabilities(Capabilities::SHADER_FLOAT16_IN_FLOAT32),
..
},
..
})
}
@ -1669,37 +1872,16 @@ fn missing_bindings2() {
#[test]
fn invalid_blend_src() {
// Missing capability.
check_validation! {
// Missing capability or enable directive
check_extension_validation! {
Capabilities::DUAL_SOURCE_BLENDING,
"
enable dual_source_blending;
struct FragmentOutput {
@location(0) @blend_src(0) output0: vec4<f32>,
@location(0) @blend_src(1) output1: vec4<f32>,
}
@fragment
fn main() -> FragmentOutput { return FragmentOutput(vec4(0.0), vec4(1.0)); }
":
Err(
naga::valid::ValidationError::EntryPoint {
stage: naga::ShaderStage::Fragment,
source: naga::valid::EntryPointError::Result(
naga::valid::VaryingError::UnsupportedCapability(Capabilities::DUAL_SOURCE_BLENDING),
),
..
},
)
}
// Missing enable directive.
// Note that this is a parsing error, not a validation error.
check("
struct FragmentOutput {
@location(0) @blend_src(0) output0: vec4<f32>,
@location(0) @blend_src(1) output1: vec4<f32>,
}
@fragment
fn main(@builtin(position) position: vec4<f32>) -> FragmentOutput { return FragmentOutput(vec4(0.0), vec4(0.0)); }
",
r###"error: the `dual_source_blending` enable extension is not enabled
wgsl:3:27
@ -1710,7 +1892,16 @@ fn invalid_blend_src() {
= note: You can enable this extension by adding `enable dual_source_blending;` at the top of the shader, before any other items.
"###,
);
Err(
naga::valid::ValidationError::EntryPoint {
stage: naga::ShaderStage::Fragment,
source: naga::valid::EntryPointError::Result(
naga::valid::VaryingError::UnsupportedCapability(Capabilities::DUAL_SOURCE_BLENDING),
),
..
},
)
}
// Using blend_src on an input.
check_validation! {
@ -3677,35 +3868,9 @@ fn subgroup_invalid_broadcast() {
#[test]
fn invalid_clip_distances() {
// Missing capability.
check_validation! {
r#"
enable clip_distances;
struct VertexOutput {
@builtin(position) pos: vec4f,
@builtin(clip_distances) clip_distances: array<f32, 8>,
}
@vertex
fn vs_main() -> VertexOutput {
var out: VertexOutput;
return out;
}
"#:
Err(
naga::valid::ValidationError::EntryPoint {
stage: naga::ShaderStage::Vertex,
source: naga::valid::EntryPointError::Result(
naga::valid::VaryingError::UnsupportedCapability(Capabilities::CLIP_DISTANCE),
),
..
},
)
}
// Missing enable directive.
// Note that this is a parsing error, not a validation error.
check(
// Missing capability or enable directive
check_extension_validation! {
Capabilities::CLIP_DISTANCE,
r#"
@vertex
fn vs_main() -> @builtin(clip_distances) array<f32, 8> {
@ -3722,7 +3887,14 @@ fn invalid_clip_distances() {
= note: You can enable this extension by adding `enable clip_distances;` at the top of the shader, before any other items.
"###,
);
Err(naga::valid::ValidationError::EntryPoint {
stage: naga::ShaderStage::Vertex,
source: naga::valid::EntryPointError::Result(
naga::valid::VaryingError::UnsupportedCapability(Capabilities::CLIP_DISTANCE)
),
..
})
}
// Maximum clip distances exceeded
check_validation! {

View File

@ -412,6 +412,10 @@ pub fn create_validator(
Caps::SHADER_FLOAT16,
features.contains(wgt::Features::SHADER_F16),
);
caps.set(
Caps::SHADER_FLOAT16_IN_FLOAT32,
downlevel.contains(wgt::DownlevelFlags::SHADER_F16_IN_F32),
);
caps.set(
Caps::PRIMITIVE_INDEX,
features.contains(wgt::Features::SHADER_PRIMITIVE_INDEX),

View File

@ -1677,7 +1677,7 @@ impl super::Instance {
},
backend: wgt::Backend::Vulkan,
};
let (available_features, downlevel_flags) =
let (available_features, mut downlevel_flags) =
phd_features.to_wgpu(&self.shared.raw, phd, &phd_capabilities);
let mut workarounds = super::Workarounds::empty();
{
@ -1693,6 +1693,15 @@ impl super::Instance {
);
};
if info.driver_info.contains("Mesa ") {
// The `F16_IN_F32` instructions do not normally require native `F16` support, but on
// Mesa, they do.
downlevel_flags.set(
wgt::DownlevelFlags::SHADER_F16_IN_F32,
available_features.contains(wgt::Features::SHADER_F16),
);
}
if let Some(driver) = phd_capabilities.driver {
if driver.conformance_version.major == 0 {
if driver.driver_id == vk::DriverId::MOLTENVK {

View File

@ -1227,7 +1227,7 @@ bitflags_array! {
/// Features that are not guaranteed to be supported.
///
/// These are part of the webgpu standard. For all features see [`Features`]
/// These are part of the WebGPU standard. For all features, see [`Features`].
///
/// If you want to use a feature, you need to first verify that the adapter supports
/// the feature. If the adapter does not support the feature, requesting a device with it enabled

View File

@ -1323,6 +1323,12 @@ bitflags::bitflags! {
/// Not Supported by:
/// - GL ES / WebGL
const NONBLOCKING_QUERY_RESOLVE = 1 << 22;
/// Allows shaders to use `quantizeToF16`, `pack2x16float`, and `unpack2x16float`, which
/// operate on `f16`-precision values stored in `f32`s.
///
/// Not supported by Vulkan on Mesa when [`Features::SHADER_F16`] is absent.
const SHADER_F16_IN_F32 = 1 << 23;
}
}