Recursive AI

Push constants on Vulkan, Metal and WebGPU

All oneshader recipes

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:

C#
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. For ordinary constant buffers, see Match constant buffers with C# structs.