---
title: "Match HLSL constant buffers with C# structs"
url: https://recursiveai.org/oneshader/cookbook/cbuffer-packing-csharp-structs/
description: "Get a C# struct whose field offsets equal an HLSL constant buffer’s packing, 16-byte rows, arrays and matrices included, so you can upload its bytes…"
version: "1.2.0"
---

# Match HLSL constant buffers with C# structs

Get a C# struct whose field offsets equal an HLSL constant buffer’s packing, 16-byte rows, arrays and matrices included, so you can upload its bytes without hand-written padding.

HLSL packs constant buffers in 16-byte rows. A vector can’t cross a row boundary, and each array element starts a new row. A hand-written C# struct has to repeat those rules exactly, and a mistake shows up as wrong values on screen. With `RecursiveAI.OneShader.Build`, the struct is generated from the compiled shader on every build, so it can’t drift from it.

`Shaders/Lighting.hlsl`:

```hlsl
cbuffer Light : register(b0)
{
    float3 LightDirection;   // 12 bytes, and Intensity fits in the same 16-byte row
    float  Intensity;
    float2 ShadowBias;
    float3 Ambient;          // would cross a 16-byte row after ShadowBias, so it starts the next one
    float  Weights[4];       // each element of an array starts a new row
    float4x4 LightViewProjection;
};

[shader("pixel")]
float4 PSMain(float3 normal : NORMAL) : SV_Target
{
    float l = saturate(dot(normal, -LightDirection)) * Intensity;
    float w = Weights[0] + Weights[1] + Weights[2] + Weights[3];
    return float4(Ambient + l * w + ShadowBias.x, LightViewProjection[0][0]);
}
```

`Program.cs`, in a project that references `RecursiveAI.OneShader.Build` 1.2.0:

```csharp
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using Packing.Shaders;

// The generated struct has the shader's offsets, so its bytes can be uploaded as they are.
Console.WriteLine($"Light.Size = {Lighting.Light.Size}, Marshal.SizeOf = {Marshal.SizeOf<Lighting.Light>()}");
foreach (FieldInfo field in typeof(Lighting.Light).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
    Console.WriteLine($"{field.Name,-24} {field.FieldType.Name,-8} offset {Marshal.OffsetOf<Lighting.Light>(field.Name)}");

var light = new Lighting.Light
{
    LightDirection = Vector3.Normalize(new Vector3(0, -1, -1)),
    Intensity = 2.5f,
    Weights_0 = 0.25f, Weights_1 = 0.25f, Weights_2 = 0.25f, Weights_3 = 0.25f,
    LightViewProjection = Matrix4x4.Identity,   // element (i, j) is HLSL's LightViewProjection[i][j]
};
ReadOnlySpan<byte> bytes = MemoryMarshal.AsBytes(new ReadOnlySpan<Lighting.Light>(in light));
Console.WriteLine($"{bytes.Length} bytes to upload");
```

```text
Light.Size = 176, Marshal.SizeOf = 176
LightDirection           Vector3  offset 0
Intensity                Single   offset 12
ShadowBias               Vector2  offset 16
Ambient                  Vector3  offset 32
Weights_0                Single   offset 48
Weights_1                Single   offset 64
Weights_2                Single   offset 80
Weights_3                Single   offset 96
_LightViewProjection_c0  Vector4  offset 112
_LightViewProjection_c1  Vector4  offset 128
_LightViewProjection_c2  Vector4  offset 144
_LightViewProjection_c3  Vector4  offset 160
176 bytes to upload
```

DXC agrees. Its SPIR-V for the same file (`dxc -T ps_6_0 -E PSMain -spirv`) declares:

```text
               OpDecorate %_arr_float_uint_4 ArrayStride 16
               OpMemberDecorate %type_Light 0 Offset 0
               OpMemberDecorate %type_Light 1 Offset 12
               OpMemberDecorate %type_Light 2 Offset 16
               OpMemberDecorate %type_Light 3 Offset 32
               OpMemberDecorate %type_Light 4 Offset 48
               OpMemberDecorate %type_Light 5 Offset 112
```

## What the generator does

- **Explicit layout.** The struct is `[StructLayout(LayoutKind.Explicit)]` with a `FieldOffset` per member and a `uint Size` constant. It’s blittable, so `MemoryMarshal.AsBytes` gives the bytes to copy into the buffer.
- **Arrays of scalars** become one field per element (`Weights_0` to `Weights_3`), each at the start of its own row, the 16-byte stride HLSL uses.
- **Matrices** are a `System.Numerics.Matrix4x4` property over four private column vectors, because HLSL’s default packing is column-major. Set it as a normal `Matrix4x4`: element (i, j) is HLSL’s `M[i][j]`. A `row_major` matrix is stored the way `Matrix4x4` is, so it becomes a plain `Matrix4x4` field. Either way the C# code is the same.
- **The same bytes for every target.** SPIR-V, Metal and WGSL use one byte layout, so one struct serves all three. The MSL output carries `static_assert`s on the size and offsets, compiled when `ONESHADER_CHECK_LAYOUT` is defined.
- **Overlapping `packoffset` members** are an error (OSH0332) instead of invalid output.
- **Push constants** follow different rules (a `float3` may share a row with the next scalar). See [Push constants on every target](https://recursiveai.org/oneshader/cookbook/push-constants/).

The full rules, structured buffers and resource arrays included, are in [Generated bindings](https://recursiveai.org/oneshader/docs/generated-bindings/).

Direct3D and Vulkan offsets side by side: [HLSL constant buffer packing rules](https://recursiveai.org/oneshader/articles/hlsl-constant-buffer-packing-rules/).
