---
title: "HLSL constant buffer packing rules"
url: https://recursiveai.org/oneshader/articles/hlsl-constant-buffer-packing-rules/
description: "How HLSL packs cbuffer members into 16-byte rows, where Direct3D and Vulkan layouts differ (arrays, matrices), and how to write a C# struct that matches."
date: 2026-10-26
author: OneShader team
---

# HLSL constant buffer packing rules

How HLSL packs cbuffer members into 16-byte rows, where Direct3D and Vulkan layouts differ (arrays, matrices), and how to write a C# struct that matches.

October 26, 2026 · OneShader team

HLSL packs a constant buffer (`cbuffer` or `ConstantBuffer<T>`) in 16-byte rows. Members fill a row in order, a member that would straddle a row boundary moves to the next row, every array element starts a new row, and every struct starts a new row. Those rules come from Direct3D. On Vulkan, DXC’s default layout follows them too, except for what comes after an array or a matrix, where a Vulkan buffer has gaps that a Direct3D buffer doesn’t. A C# struct has to repeat the layout of the API you upload to, byte for byte.

The offsets below aren’t worked out by hand. They are what DXC 1.9.0.5512 (Vulkan SDK 1.4.363.0) and [OneShader 1.2.0](https://www.nuget.org/packages/RecursiveAI.OneShader/1.2.0) wrote for one constant buffer that hits every rule.

## The rules

From Microsoft’s [packing rules for constant variables](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-packing-rules) (updated 31 January 2024):

1. **4-byte packing within 16-byte rows.** Rules “similar to performing a #pragma pack 4”, except that HLSL “packs data so that it does not cross a 16-byte boundary”.
2. **No straddling.** “Variables are packed into a given four-component vector until the variable will straddle a 4-vector boundary; the next variables will be bounced to the next four-component vector.”
3. **Structs start a row.** “Each structure forces the next variable to start on the next four-component vector.”
4. **Arrays aren’t packed.** “Every element in an array is stored in a four-component vector”, so a `float[3]` takes three rows. The last element’s row isn’t padded out, though: on Direct3D the next scalar can sit right after it.

## One buffer that hits every rule

*Packing.hlsl · HLSL*

```
struct Light { float3 Direction; float Intensity; };
 
cbuffer Scene : register(b0)
{
    float3   SunColor;     // fills x, y, z of row 0
    float    Exposure;     // packs into w of row 0
    float2   Jitter;       // row 1
    float3   FogColor;     // would straddle row 1, so moves to row 2
    float    Weights[3];   // each element starts a row: rows 3, 4, 5
    float    AfterArray;   // where this lands differs between D3D and Vulkan
    Light    Sun;          // a struct starts a new row
    float    AfterStruct;
    float3x3 Rotation;     // 3 rows of 16 bytes, column-major
    float    Last;
};
 
[shader("pixel")]
float4 main() : SV_Target
{
    return float4(SunColor * Exposure + FogColor * Jitter.x + Sun.Direction * Weights[2] + Rotation[0],
                  AfterArray + AfterStruct + Sun.Intensity + Last);
}
```

### Direct3D 12 offsets

DXC writes the layout it uses for DXIL as comments in the listing produced by `-Fc`:

*Terminal · DXC 1.9.0.5512*

```
dxc -T ps_6_0 -E main Packing.hlsl -Fc packing.dxil.txt
grep -E "Offset: " packing.dxil.txt
```

*Output*

```
;       float3 SunColor;                              ; Offset:    0
;       float Exposure;                               ; Offset:   12
;       float2 Jitter;                                ; Offset:   16
;       float3 FogColor;                              ; Offset:   32
;       float Weights[3];                             ; Offset:   48
;       float AfterArray;                             ; Offset:   84
;           float3 Direction;                         ; Offset:   96
;           float Intensity;                          ; Offset:  108
;       } Sun;                                        ; Offset:   96
;       float AfterStruct;                            ; Offset:  112
;       column_major float3x3 Rotation;               ; Offset:  128
;       float Last;                                   ; Offset:  172
;   } Scene;                                          ; Offset:    0 Size:   176
```

### Vulkan offsets

*Terminal · DXC 1.9.0.5512*

```
dxc -spirv -T ps_6_0 -E main Packing.hlsl -Fo packing.spv
spirv-dis packing.spv | grep -E "OpMemberDecorate %type_Scene [0-9]+ Offset"
```

*Output*

```
               OpMemberDecorate %type_Scene 0 Offset 0
               OpMemberDecorate %type_Scene 1 Offset 12
               OpMemberDecorate %type_Scene 2 Offset 16
               OpMemberDecorate %type_Scene 3 Offset 32
               OpMemberDecorate %type_Scene 4 Offset 48
               OpMemberDecorate %type_Scene 5 Offset 96
               OpMemberDecorate %type_Scene 6 Offset 112
               OpMemberDecorate %type_Scene 7 Offset 128
               OpMemberDecorate %type_Scene 8 Offset 144
               OpMemberDecorate %type_Scene 9 Offset 192
```

The members are numbered in declaration order. Here they are side by side:

| Member | Direct3D 12 | Vulkan (DXC default) | Why |
|---|---|---|---|
| `float3 SunColor` | 0 | 0 | Starts row 0 |
| `float Exposure` | 12 | 12 | Fits in the rest of row 0 |
| `float2 Jitter` | 16 | 16 | Row 1 |
| `float3 FogColor` | 32 | 32 | At 24 it would straddle into row 2, so it moves |
| `float Weights[3]` | 48 | 48 | One row per element: 48, 64, 80 |
| `float AfterArray` | **84** | **96** | Direct3D packs it after the last element; Vulkan rounds the array up to whole rows |
| `Light Sun` | 96 | 112 | A struct starts a row |
| `float AfterStruct` | 112 | 128 | The struct ends its row |
| `float3x3 Rotation` | 128 | 144 | Column-major: three columns, one row each |
| `float Last` | **172** | **192** | Same as arrays: Direct3D packs into the matrix’s last row |
| Size | 176 | 208 |  |

The two differences are the members right after an array and right after a matrix. Everything after them shifts. DXC’s [SPIR-V manual](https://github.com/microsoft/DirectXShaderCompiler/blob/main/docs/SPIR-V.rst) explains why: its default for constant buffers is “vector-relaxed OpenGL `std140`”, which satisfies Vulkan’s standard uniform buffer layout, and `std140` rounds arrays and matrices up to a whole number of 16-byte rows. The “vector-relaxed” part gives a vector its element’s alignment as long as it doesn’t straddle a row, as on Direct3D. In `cbuffer C { float A; float2 B; }`, both layouts put `B` at 4, where strict `std140` would put it at 8. So most buffers have the same layout on both APIs, until an array or a matrix is followed by something.

## Making Vulkan match Direct3D

DXC can use the Direct3D layout on Vulkan with `-fvk-use-dx-layout`, which, its manual notes, “requires `VK_EXT_scalar_block_layout` to be enabled on the application side”:

*Terminal · DXC 1.9.0.5512*

```
dxc -spirv -fvk-use-dx-layout -T ps_6_0 -E main Packing.hlsl -Fo packing.dx.spv
spirv-dis packing.dx.spv | grep -E "OpMemberDecorate %type_Scene [0-9]+ Offset"
```

*Output*

```
               OpMemberDecorate %type_Scene 0 Offset 0
               OpMemberDecorate %type_Scene 1 Offset 12
               OpMemberDecorate %type_Scene 2 Offset 16
               OpMemberDecorate %type_Scene 3 Offset 32
               OpMemberDecorate %type_Scene 4 Offset 48
               OpMemberDecorate %type_Scene 5 Offset 84
               OpMemberDecorate %type_Scene 6 Offset 96
               OpMemberDecorate %type_Scene 7 Offset 112
               OpMemberDecorate %type_Scene 8 Offset 128
               OpMemberDecorate %type_Scene 9 Offset 172
```

The offsets now equal the Direct3D ones. The validator agrees about the requirement: without scalar block layout the module is invalid, with it the module passes:

*Terminal · SPIRV-Tools v2026.4*

```
spirv-val --target-env vulkan1.2 packing.dx.spv
spirv-val --target-env vulkan1.2 --scalar-block-layout packing.dx.spv
```

*Output*

```
error: line 62: Structure member 5 at offset 84 overlaps previous member ending at offset 95. Instantiated via '5[%Scene]' in the Uniform storage class using extended layout rules.
This is may be allowed if you enable the uniformBufferStandardLayout feature (or use the --uniform-buffer-standard-layout command line flag)
  %type_Scene = OpTypeStruct %v3float %float %v2float %v3float %_arr_float_uint_3 %float %Light %float %mat3v3float %float
```

The other way round needs no device feature: order members so that no scalar follows an array or a matrix, or pad explicitly, and the layout is the same on both APIs. OneShader always uses DXC’s default Vulkan layout, for SPIR-V, Metal and WGSL alike, and rejects `-fvk-use-dx-layout`. Its reflection reports the same offsets as DXC’s SPIR-V above:

*Terminal · OneShader 1.2.0*

```
oneshader -T ps_6_0 -E main Packing.hlsl -Fo packing.osh.spv --reflect packing.json
jq -r '.resources[] | select(.name == "Scene") | .layout.members[] | "\(.offset)\t\(.name)"' packing.json
```

*Output*

```
0	SunColor
12	Exposure
16	Jitter
32	FogColor
48	Weights
96	AfterArray
112	Sun
128	AfterStruct
144	Rotation
192	Last
```

## A C# struct that matches

Write the struct with `LayoutKind.Explicit` and one `FieldOffset` per member. An array becomes one field per element, and a `float3x3` one `Vector3` per column, each at the start of its row:

*Program.cs · .NET 10*

```
using System.Numerics;
using System.Runtime.InteropServices;
 
Console.WriteLine($"Size {Marshal.SizeOf<SceneVulkan>()}");
foreach (var name in new[] { "Weights0", "AfterArray", "Sun", "AfterStruct", "Rotation0", "Last" })
    Console.WriteLine($"{name,-12} {Marshal.OffsetOf<SceneVulkan>(name)}");
 
// The Scene constant buffer as DXC lays it out for Vulkan (vector-relaxed std140).
[StructLayout(LayoutKind.Explicit, Size = 208)]
struct SceneVulkan
{
    [FieldOffset(0)]   public Vector3 SunColor;
    [FieldOffset(12)]  public float Exposure;
    [FieldOffset(16)]  public Vector2 Jitter;
    [FieldOffset(32)]  public Vector3 FogColor;
    [FieldOffset(48)]  public float Weights0;    // each array element starts a 16-byte row
    [FieldOffset(64)]  public float Weights1;
    [FieldOffset(80)]  public float Weights2;
    [FieldOffset(96)]  public float AfterArray;  // 84 on Direct3D
    [FieldOffset(112)] public Light Sun;
    [FieldOffset(128)] public float AfterStruct;
    [FieldOffset(144)] public Vector3 Rotation0; // float3x3, column-major: one column per row
    [FieldOffset(160)] public Vector3 Rotation1;
    [FieldOffset(176)] public Vector3 Rotation2;
    [FieldOffset(192)] public float Last;        // 172 on Direct3D
}
 
[StructLayout(LayoutKind.Explicit, Size = 16)]
struct Light
{
    [FieldOffset(0)]  public Vector3 Direction;
    [FieldOffset(12)] public float Intensity;
}
```

*Output*

```
Size 208
Weights0     48
AfterArray   96
Sun          112
AfterStruct  128
Rotation0    144
Last         192
```

Setting `Size = 208` matters as much as the offsets. A sequential C# struct would pack `AfterArray` right after `Weights2` and come out shorter than the buffer the shader declares. For Direct3D 12 the same struct would use 84 and 172 for the two marked fields, and later offsets would shift as in the table above.

### Let the build write it

Hand-written offsets break silently when someone edits the shader. With `RecursiveAI.OneShader.Build`, the struct is generated from the compiled shader on every `dotnet build`, so it can’t drift. Add `Packing.hlsl` to a project as `Shaders/Packing.hlsl` and list the fields of the generated `Packing.Scene`:

*Program.cs · RecursiveAI.OneShader.Build 1.2.0*

```
using System.Runtime.InteropServices;
using App.Shaders;
 
foreach (var field in typeof(Packing.Scene).GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic))
    Console.WriteLine($"{field.Name,-14} {Marshal.OffsetOf<Packing.Scene>(field.Name),4}  {field.FieldType.Name}");
Console.WriteLine($"Size {Marshal.SizeOf<Packing.Scene>()}");
```

*Output*

```
SunColor          0  Vector3
Exposure         12  Single
Jitter           16  Vector2
FogColor         32  Vector3
Weights_0        48  Single
Weights_1        64  Single
Weights_2        80  Single
AfterArray       96  Single
Sun             112  Light
AfterStruct     128  Single
_Rotation_c0    144  Vector3
_Rotation_c1    160  Vector3
_Rotation_c2    176  Vector3
Last            192  Single
Size 208
```

The generated struct has the same offsets and size as the reflection and as DXC’s SPIR-V. Vectors are `System.Numerics` types, arrays are one field per element (`Weights_0` to `Weights_2`), and the matrix is stored column by column behind a `Matrix4x4` property. See the [constant buffer structs recipe](https://recursiveai.org/oneshader/cookbook/cbuffer-packing-csharp-structs/) and [generated bindings](https://recursiveai.org/oneshader/docs/generated-bindings/).

## Other buffer types

- **`StructuredBuffer` and `RWStructuredBuffer`** use “vector-relaxed OpenGL `std430`” on Vulkan in DXC’s default, per its manual. Arrays there aren’t rounded to rows: in an element `struct S { float W[2]; float X; }`, `W` has a 4-byte stride and `X` is at 8, the same as the Direct3D listing, and the element stride is 12.
- **Push constants** (`[[vk::push_constant]]`) follow the structured-buffer rules, not the constant-buffer rules, in DXC’s output and OneShader’s ([push constants recipe](https://recursiveai.org/oneshader/cookbook/push-constants/)).
- **`packoffset`** places a member at a chosen offset on Direct3D. On Vulkan, DXC’s `[[vk::offset]]` does the same and “overrules `:packoffset`” ([SPIR-V manual](https://github.com/microsoft/DirectXShaderCompiler/blob/main/docs/SPIR-V.rst)).

## Common questions

### Why is my HLSL constant buffer bigger than my C# struct?

Because HLSL rounds structs, array elements and matrix columns to 16-byte rows and C# doesn’t. Check the offsets the compiler reports (the `-Fc` listing, `spirv-dis`, or reflection) against `Marshal.OffsetOf` for each field.

### Does a float4 always have to be 16-byte aligned?

In a constant buffer it can’t straddle a 16-byte row, so in practice it always starts a row. A `float3` also has to fit in one row, but a `float` can follow it in the same row.

### What about Metal and WebGPU?

Their own layout rules differ again. Core WGSL, for one, requires a 16-byte array stride in uniform buffers. OneShader keeps the Vulkan byte layout on all three targets, so one C# struct works everywhere, as the [HLSL to WGSL comparison](https://recursiveai.org/oneshader/articles/hlsl-to-wgsl-converter/) shows with this same buffer.

To generate matching structs for your own shaders, run `dotnet add package RecursiveAI.OneShader.Build` and follow [getting started](https://recursiveai.org/oneshader/docs/getting-started/).
