GPU-TESTS: every WGSL kernel parses and validates in the test run, without a GPU

The kernels were only compiled when a device was created, so a module that doesn't build (a
function defined twice after an automatic merge, a type error) passed CI on machines without a
GPU and failed on users' machines. naga (re-exported by wgpu) now parses and validates every
module as it is handed to the device: the finish, blur, resize, map, mask and geometry modules
with their generated constants and bindings, the denoise convolution for every block size the
planner picks, the pooling kernel and the face detector's two. A test checks that a duplicate
definition is caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
OneSlips
2026-10-26 01:59:34 +10:00
co-authored by Claude Opus 5.5
parent 03c88c166c
commit 7c9b6d88e5
4 changed files with 60 additions and 0 deletions
+11
View File
@@ -763,6 +763,17 @@ pub fn groups1(n: usize) -> [u32; 3] {
#[cfg(test)]
mod tests {
/// Every kernel module, as it is handed to the device (the generated constants, the bindings, `common.wgsl`, the
/// module), parses and validates without a GPU.
#[test]
fn every_kernel_module_validates_without_a_gpu() {
let consts = super::constants();
for m in super::MODULES {
let name = m.entries.first().copied().unwrap_or("?");
assert_eq!(crate::validate_wgsl(name, &super::module_source(m, &consts)), Ok(()));
}
}
#[test]
fn limits_override_parses() {
assert_eq!(super::parse_limits("webgpu"), Some((128 << 20, 256 << 20)));
+7
View File
@@ -817,6 +817,13 @@ static PER_CONV: AtomicBool = AtomicBool::new(false);
mod tests {
use super::*;
/// The face detector's kernels validate without a GPU.
#[test]
fn the_face_kernels_validate_without_a_gpu() {
assert_eq!(crate::validate_wgsl("faces_conv", include_str!("wgsl/faces_conv.wgsl")), Ok(()));
assert_eq!(crate::validate_wgsl("faces_ops", include_str!("wgsl/faces_ops.wgsl")), Ok(()));
}
fn gpu() -> Option<Arc<FaceGpu>> {
match accelerator() {
Ok(a) => Some(a),
+23
View File
@@ -550,10 +550,33 @@ pub fn try_render(
}
}
/// Tests: parse and validate a WGSL module with naga, as wgpu does before it hands a shader to the driver. Needs no
/// GPU, so kernels that don't build (a duplicate definition after a merge, a type error) fail every test run, not only
/// the GPU ones.
#[cfg(test)]
pub(crate) fn validate_wgsl(name: &str, src: &str) -> Result<(), String> {
use wgpu::naga;
let module = naga::front::wgsl::parse_str(src).map_err(|e| format!("{name}: {}", e.emit_to_string(src)))?;
naga::valid::Validator::new(naga::valid::ValidationFlags::all(), naga::valid::Capabilities::all())
.validate(&module)
.map(|_| ())
.map_err(|e| format!("{name}: {}", e.emit_to_string(src)))
}
#[cfg(test)]
mod tests {
use lightcraft_pipeline::{OutputDepth, Overlay};
/// The helper refuses what a driver would: a function defined twice (what an automatic merge of two branches
/// touching the same kernel can leave).
#[test]
fn wgsl_validation_catches_a_duplicate_definition() {
let ok = "fn f() -> f32 { return 1.0; }\n@compute @workgroup_size(1) fn main() { _ = f(); }\n";
assert_eq!(super::validate_wgsl("ok", ok), Ok(()));
let twice = format!("fn f() -> f32 {{ return 2.0; }}\n{ok}");
assert!(super::validate_wgsl("twice", &twice).is_err());
}
// Given a request the 8-bit kernel can't draw (high bit depth, a soft proof, Visualize HDR),
// the GPU doesn't take it; the loupe and diagnostic overlays it can draw stay on the GPU
#[test]
+19
View File
@@ -881,6 +881,25 @@ impl TileRunner for NetRunner {
#[cfg(test)]
mod tests {
use super::*;
/// The denoise kernels validate without a GPU: the pooling kernel, and the convolution for every block size the
/// planner picks for these layer widths and channel counts.
#[test]
fn the_network_kernels_validate_without_a_gpu() {
assert_eq!(crate::validate_wgsl("nn_pool", include_str!("wgsl/nn_pool.wgsl")), Ok(()));
let mut sizes: Vec<(u32, u32, u32)> = Vec::new();
for n in [1usize, 3, 8, 16, 24, 32, 48, 64, 96, 128, 256, 512] {
for cin in [1usize, 3, 4, 8, 16, 32, 48, 64, 128, 256, 512] {
let b = blocks(n, cin);
if !sizes.contains(&b) {
sizes.push(b);
}
}
}
for (bm, bn, bk) in sizes {
assert_eq!(crate::validate_wgsl(&format!("nn_conv {bm}x{bn}x{bk}"), &conv_source(bm, bn, bk)), Ok(()));
}
}
use lightcraft_denoise::{onnx, reference, synthetic};
fn net(tile: u64, ch: u64, depth: usize, seed: u64) -> Net {