Compile an HLSL vertex, pixel or compute shader to WGSL for WebGPU with OneShader, from the command line, from C# or during dotnet build, check it with naga, and see what WGSL can’t express.
OneShader writes WGSL from the same front end as its SPIR-V and Metal output, with the same bindings and the same buffer byte layout. The sample is Sprite.hlsl from Getting started.
From the command line
oneshader --target wgsl Sprite.hlsl -Fo sprite.wgsl naga sprite.VSMain.wgsl naga sprite.PSMain.wgsl
Without -T, every [shader("...")] entry point is compiled, one file each: sprite.VSMain.wgsl and sprite.PSMain.wgsl. naga 30.0.1 prints Validation successful for both. The start of the vertex shader:
struct VSMain_Output { @builtin(position) VSMain_Position: vec4<f32>, @location(0) VSMain_Uv: vec2<f32>, @location(1) VSMain_Color: vec4<f32>, } @group(0) @binding(0) var<uniform> Frame: array<vec4<u32>, 5>; ... @vertex fn VSMain(@location(0) in_VSMain_i_Position: vec2<f32>, @location(1) in_VSMain_i_Uv: vec2<f32>, @location(2) in_VSMain_i_Color: vec4<f32>) -> VSMain_Output {
- Bindings are
@group(set) @binding(binding), the same numbers as on Vulkan. - The constant buffer is raw 16-byte rows (
array<vec4<u32>, 5>for the 80-byteFrame), read with bitcasts at the offsets HLSL packing gives. So the bytes you upload are the same for WebGPU, Vulkan and Metal, and one generated C# struct serves all three. Structured and byte-address buffers arearray<u32>in the same way. - The entry function keeps the HLSL name, unless WGSL reserves it (
loopbecomesloop_).
From C# or with dotnet build
- At run time, add
CompileTarget.WgsltoCompileOptions.Targetsand read the text withresult.Find("PSMain", CompileTarget.Wgsl)!.AsText(). See Compile at run time. - In a build-package project, set
OneShaderTargetsset toSpirV;Wgsl. Each entry-point class then hasWgsl(UTF-8 bytes) andWgslEntryPoint, andPipeline.TryToWebGpu()gives the bind group layouts with WebGPU’s enum strings. See Write a compute shader for Vulkan and WebGPU.
What WGSL can’t express
A construct that WebGPU doesn’t have is a compile error, OSH5202, naming the target and what to do instead. For example, clip distances:
cbuffer Frame : register(b0) { float4x4 ViewProjection; float4 ClipPlane; }; struct VSOut { float4 Position : SV_Position; float Clip : SV_ClipDistance0; }; [shader("vertex")] VSOut VSMain(float3 p : POSITION) { VSOut o; o.Position = mul(ViewProjection, float4(p, 1)); o.Clip = dot(float4(p, 1), ClipPlane); return o; }
oneshader --target wgsl Clip.hlsl -Fo clip.wgslClip.hlsl(2,53): error OSH5202: SV_ClipDistance and SV_CullDistance need WebGPU's optional clip-distances feature, which this backend does not use; clip in the pixel shader with clip() or discard instead.The same file compiles for SPIR-V. In a build-package project the error only appears when WGSL is selected, which is why SPIR-V alone is the default.
The others, from the WGSL backend page: CalculateLevelOfDetail; wave intrinsics in vertex shaders; resource arrays (WebGPU has no binding arrays); Buffer<T> and RWBuffer<T>; append and consume buffers; atomics on textures; double, 64-bit integers and 16-bit integers (half works, as f16); integer and bool matrices; PrimitiveID, layer and viewport index; recursion; storage textures without a format, or read and written in a format other than r32float, r32uint or r32sint; bool stage variables; texel offsets on cube textures.
Good to know
- Specialization constants become
@id(N) overridedeclarations, set through the pipeline’sconstants. See Specialization constants. precisehas no WGSL equivalent: it compiles with warning OSH5205. See The precise keyword.- Wave intrinsics use WebGPU’s
subgroupsfeature in compute and fragment shaders. See Wave intrinsics. - Compare-exchange atomics produce WGSL that naga rejects at 1.2.0. See InterlockedAdd and float atomics.
- Clip-space Y points up in WebGPU and down in Vulkan, and the shader isn’t adjusted: flip it with the viewport.
- How it’s checked. Every corpus file either validates with naga or reports one of the errors above, and the repository runs the corpus in headless Chromium on a real WebGPU adapter against DXC’s SPIR-V on Vulkan (WebGPU execution parity).
DXC with naga compared with OneShader: HLSL to WGSL converter.