---
title: "Convert HLSL to WGSL for WebGPU"
url: https://recursiveai.org/oneshader/cookbook/hlsl-to-wgsl/
description: "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…"
version: "1.2.0"
---

# Convert HLSL to WGSL for WebGPU

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](https://recursiveai.org/oneshader/docs/getting-started/).

## From the command line

```sh
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:

```text
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-byte `Frame`), 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 are `array<u32>` in the same way.
- **The entry function** keeps the HLSL name, unless WGSL reserves it (`loop` becomes `loop_`).

## From C# or with dotnet build

- At run time, add `CompileTarget.Wgsl` to `CompileOptions.Targets` and read the text with `result.Find("PSMain", CompileTarget.Wgsl)!.AsText()`. See [Compile at run time](https://recursiveai.org/oneshader/docs/getting-started/#compile-at-run-time).
- In a build-package project, set `OneShaderTargets` set to `SpirV;Wgsl`. Each entry-point class then has `Wgsl` (UTF-8 bytes) and `WgslEntryPoint`, and `Pipeline.TryToWebGpu()` gives the bind group layouts with WebGPU’s enum strings. See [Write a compute shader for Vulkan and WebGPU](https://recursiveai.org/oneshader/cookbook/hlsl-compute-shader/).

## 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:

```hlsl
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;
}
```

```sh
oneshader --target wgsl Clip.hlsl -Fo clip.wgsl
```

```text
Clip.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](https://recursiveai.org/oneshader/docs/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) override` declarations, set through the pipeline’s `constants`. See [Specialization constants](https://recursiveai.org/oneshader/cookbook/specialization-constants/).
- **`precise`** has no WGSL equivalent: it compiles with warning OSH5205. See [The precise keyword](https://recursiveai.org/oneshader/cookbook/hlsl-precise-keyword/).
- **Wave intrinsics** use WebGPU’s `subgroups` feature in compute and fragment shaders. See [Wave intrinsics](https://recursiveai.org/oneshader/cookbook/hlsl-wave-intrinsics/).
- **Compare-exchange atomics** produce WGSL that naga rejects at 1.2.0. See [InterlockedAdd and float atomics](https://recursiveai.org/oneshader/cookbook/hlsl-interlockedadd/).
- **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](https://recursiveai.org/oneshader/docs/webgpu-parity/)).

DXC with naga compared with OneShader: [HLSL to WGSL converter](https://recursiveai.org/oneshader/articles/hlsl-to-wgsl-converter/).
