---
title: "Push constants on Vulkan, Metal and WebGPU"
url: https://recursiveai.org/oneshader/cookbook/push-constants/
description: "Declare an HLSL push-constant block once and get the C# struct plus where each API takes its bytes: a Vulkan push-constant range, a Metal setVertexBytes…"
version: "1.2.0"
---

# Push constants on Vulkan, Metal and WebGPU

Declare an HLSL push-constant block once and get the C# struct plus where each API takes its bytes: a Vulkan push-constant range, a Metal `setVertexBytes` index, and a WebGPU uniform buffer, since WebGPU has no push constants.

A struct-typed global or `ConstantBuffer<T>` marked `[[vk::push_constant]]` is a push-constant block, not a descriptor. `Shaders/Mesh.hlsl`:

```hlsl
struct PerDraw { float4x4 Model; float4 Tint; uint Index; };
[[vk::push_constant]] ConstantBuffer<PerDraw> Draw;

cbuffer Camera : register(b0) { float4x4 ViewProjection; };

struct VSOut { float4 Position : SV_Position; float4 Color : COLOR0; };

[shader("vertex")]
VSOut VSMain(float3 position : POSITION)
{
    VSOut o;
    o.Position = mul(ViewProjection, mul(Draw.Model, float4(position, 1)));
    o.Color = Draw.Tint;
    return o;
}

[shader("pixel")]
float4 PSMain(VSOut i) : SV_Target { return i.Color; }
```

The project references `RecursiveAI.OneShader.Build` 1.2.0 with `OneShaderTargets` set to `SpirV;Msl;Wgsl`. `Program.cs`:

```csharp
using System.Numerics;
using System.Runtime.InteropServices;
using Push.Shaders;

var draw = new Mesh.PushConstants
{
    Model = Matrix4x4.CreateTranslation(0, 1, 0),
    Tint = new Vector4(1, 0.5f, 0, 1),
    Index = 7,
};
ReadOnlySpan<byte> bytes = MemoryMarshal.AsBytes(new ReadOnlySpan<Mesh.PushConstants>(in draw));

Console.WriteLine($"Push constants: {Mesh.PushConstants.Size} bytes, {bytes.Length} in the struct");
Console.WriteLine($"Vulkan: vkCmdPushConstants(offset 0, size {Mesh.Bindings.PushConstants.Size}, stages {Mesh.Bindings.PushConstants.Stages})");
Console.WriteLine($"Metal:  setVertexBytes at buffer index {Mesh.Bindings.PushConstants.MslIndex}");
Console.WriteLine($"WebGPU: uniform buffer at @group({Mesh.Bindings.PushConstants.WgslGroup}) @binding({Mesh.Bindings.PushConstants.WgslBinding})");
Console.WriteLine($"Camera: set {Mesh.Bindings.Camera.Set}, binding {Mesh.Bindings.Camera.Binding}, Metal buffer {Mesh.Bindings.Camera.MslIndex}");
```

```text
Push constants: 96 bytes, 96 in the struct
Vulkan: vkCmdPushConstants(offset 0, size 96, stages Vertex)
Metal:  setVertexBytes at buffer index 0
WebGPU: uniform buffer at @group(0) @binding(1)
Camera: set 0, binding 0, Metal buffer 1
```

The SPIR-V of both entry points passes `spirv-val`, and the WGSL passes naga.

## How each API gets the bytes

- **Vulkan:** `vkCmdPushConstants(cmd, layout, stages, 0, Size, &data)`, where `stages` is `Bindings.PushConstants.Stages` cast to `VkShaderStageFlags`, with a push-constant range at offset 0, `Size` bytes, those stages, in the pipeline layout. `Stages` is only the stages that read the block: here the vertex shader, because the pixel shader doesn’t touch `Draw`.
- **Metal:** the block is a `constant T&` argument. Pass the struct’s bytes with `setVertexBytes` / `setFragmentBytes` (or `setBytes` for compute) at `MslIndex`. It takes the next buffer index in declaration order, so `Camera` moves to buffer 1.
- **WebGPU:** a uniform buffer at `@group(WgslGroup) @binding(WgslBinding)`, at least `Size` rounded up to 16 bytes. Write the bytes into it before each draw. Its binding is the one after the highest binding of the highest group the shader uses, so it never collides with the shader’s own.
- **One struct for all three.** The members follow the std430-style rules DXC uses for push constants (a `float3` may share a 16-byte row with the next scalar), not the constant-buffer rules, and the same bytes are valid on every target.

## Limits

- Vulkan only guarantees 128 bytes of push constants. A larger block compiles with warning OSH0334.
- A file may declare several blocks as long as no entry point reads more than one (OSH0333). With several, each struct is named `<Name>PushConstants`.
- The block isn’t in `Bindings.All`, because it isn’t a descriptor. `Pipeline.ToVulkan()` lists it in `PushConstantRanges`, and `TryToWebGpu()` adds the stand-in uniform buffer to its bind group.

The rules are in [Generated bindings](https://recursiveai.org/oneshader/docs/generated-bindings/). For ordinary constant buffers, see [Match constant buffers with C# structs](https://recursiveai.org/oneshader/cookbook/cbuffer-packing-csharp-structs/).
