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:
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:
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");
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:
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 aFieldOffsetper member and auint Sizeconstant. It’s blittable, soMemoryMarshal.AsBytesgives the bytes to copy into the buffer. - Arrays of scalars become one field per element (
Weights_0toWeights_3), each at the start of its own row, the 16-byte stride HLSL uses. - Matrices are a
System.Numerics.Matrix4x4property over four private column vectors, because HLSL’s default packing is column-major. Set it as a normalMatrix4x4: element (i, j) is HLSL’sM[i][j]. Arow_majormatrix is stored the wayMatrix4x4is, so it becomes a plainMatrix4x4field. 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_asserts on the size and offsets, compiled whenONESHADER_CHECK_LAYOUTis defined. - Overlapping
packoffsetmembers are an error (OSH0332) instead of invalid output. - Push constants follow different rules (a
float3may share a row with the next scalar). See Push constants on every target.
The full rules, structured buffers and resource arrays included, are in Generated bindings.
Direct3D and Vulkan offsets side by side: HLSL constant buffer packing rules.