---
title: "Build a vertex input layout from HLSL in C#"
url: https://recursiveai.org/oneshader/cookbook/build-vertex-input-layout/
description: "Reflect an HLSL vertex shader in C#, create Vulkan binding and attribute descriptions, and check stride and offsets against a C# vertex struct."
version: "1.7.1"
---

# Build a vertex input layout from HLSL in C#

**OneShader** is a free, open-source HLSL compiler library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneShader) · [GitHub](https://github.com/recursive-ai-oss/oneshader-csharp) · [Docs](https://recursiveai.org/oneshader/docs/) · [Download example (.zip)](https://recursiveai.org/oneshader/cookbook/build-vertex-input-layout/build-vertex-input-layout.zip)

Describe how a vertex shader reads buffer fields, without creating a GPU device. This example returns Vulkan descriptions; OneShader 1.7.1 also provides Metal (`ToMetal`) and WebGPU (`ToWebGpu`) vertex layouts, but no D3D input-layout conversion ([layout API](https://recursiveai.org/oneshader/docs/compiler/#pipeline-layouts)).

```shell
dotnet add package RecursiveAI.OneShader
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using OneShader;
using OneShader.Layouts;

var result = ShaderCompiler.Compile(
    "[shader(\"vertex\")] float4 V(" +
    "float3 p : POSITION) : SV_Position " +
    "{ return float4(p, 1); }", "vertex.hlsl");
if (!result.Success) throw new Exception(
    string.Join("\n", result.Diagnostics));
if (!EntryLayout.TryCreate(
    result.Reflection!, "V",
    out var entry, out var error))
    throw new Exception(error!.ToString());
var input = entry!.Vertex!.ToVulkan(
    VertexStepMode.Vertex);
Console.WriteLine(input.Bindings[0].Stride);
```

```text
12
```

Creates a per-vertex Vulkan binding with a 12-byte stride and one position attribute.

Unzip [the example](https://recursiveai.org/oneshader/cookbook/build-vertex-input-layout/build-vertex-input-layout.zip), then `dotnet run`.

## The full program

This shader reads a `float3` position and `float2` UV. It uses both fields so both remain part of the input. The C# struct uses `Vector3` and `Vector2`; the program checks its actual size and field offsets against reflection.

```csharp
using OneShader;
using OneShader.Layouts;
using System.Numerics;
using System.Runtime.InteropServices;

const string source = """
[shader("vertex")]
float4 VSMain(float3 position : POSITION,
              float2 uv : TEXCOORD0) : SV_Position
{
    return float4(position.xy + uv, position.z, 1);
}
""";
var result = ShaderCompiler.Compile(source, "vertex.hlsl");
foreach (var diagnostic in result.Diagnostics)
    Console.WriteLine(diagnostic);
if (!result.Success) return 1;
if (!EntryLayout.TryCreate(result.Reflection!, "VSMain", out var entry, out var error))
{
    Console.WriteLine(error);
    return 1;
}
var vertex = entry!.Vertex!;
var vulkan = vertex.ToVulkan(VertexStepMode.Vertex);
foreach (var binding in vulkan.Bindings)
    Console.WriteLine($"Binding {binding.Binding}: stride {binding.Stride}, {binding.InputRate}");
foreach (var attribute in vulkan.Attributes)
    Console.WriteLine($"Location {attribute.Location}: {attribute.Format}, offset {attribute.Offset}");
int size = Marshal.SizeOf<Vertex>();
int positionOffset = (int)Marshal.OffsetOf<Vertex>(nameof(Vertex.Position));
int uvOffset = (int)Marshal.OffsetOf<Vertex>(nameof(Vertex.Uv));
Console.WriteLine($"C# Vertex: size {size}, position {positionOffset}, uv {uvOffset}");
if (vertex.Stride != size || vertex.Attributes[0].Offset != positionOffset || vertex.Attributes[1].Offset != uvOffset)
    throw new InvalidOperationException("C# and shader layouts differ.");
return 0;

[StructLayout(LayoutKind.Sequential)]
struct Vertex
{
    public Vector3 Position;
    public Vector2 Uv;
}
```

```text
Binding 0: stride 20, Vertex
Location 0: R32G32B32Sfloat, offset 0
Location 1: R32G32Sfloat, offset 12
C# Vertex: size 20, position 0, uv 12
```

Compilation errors return diagnostics; an unsuccessful `EntryLayout.TryCreate` returns a layout error, and this example stops rather than using an invalid layout.

## Variations

**Advance per instance instead of per vertex.** Replace the `ToVulkan` line in the full program:

```csharp
var vulkan = vertex.ToVulkan(VertexStepMode.Instance);
```

The binding line becomes:

```text
Binding 0: stride 20, Instance
```

**Use a different Vulkan binding number.** With the full program’s `vertex`, this prints the selected binding:

```csharp
var input = vertex.ToVulkan(
    VertexStepMode.Vertex, binding: 2);
Console.WriteLine(input.Bindings[0].Binding);
Console.WriteLine(input.Attributes[0].Binding);
```

```text
2
2
```

## Related recipes

- [Reflect shader resources in C#](https://recursiveai.org/oneshader/cookbook/spirv-reflection-csharp/)
- [Union pipeline layouts](https://recursiveai.org/oneshader/cookbook/union-pipeline-layouts/)
- [Generate C# shader bindings](https://recursiveai.org/oneshader/cookbook/generate-csharp-shader-bindings/)

---

## The short answer

Compile the vertex entry point, pass its `ShaderReflection` to `EntryLayout.TryCreate`, and call `entry.Vertex.ToVulkan`. The returned data contains binding descriptions and attribute descriptions. Pass `VertexStepMode.Instance` explicitly when the buffer should advance per instance; use `Vertex` for ordinary per-vertex data.

This creates descriptions, not Vulkan handles. OneShader does not create a device, upload the vertices or draw anything in this recipe. The [pipeline layout guide](https://recursiveai.org/oneshader/docs/compiler/#pipeline-layouts) covers reflection-based and generated layouts.

## How do I get an HLSL vertex input layout from reflection in C#?

Mark a function with `[shader("vertex")]` and compile it. On success, select that same function name when calling `EntryLayout.TryCreate`. Selecting a missing entry point is an error even when the source itself compiles. The returned entry layout describes the chosen stage’s resources and vertex input; use its `Vertex` property for this task.

The example’s shader declares position first and UV second. Reflection yields locations 0 and 1, offsets 0 and 12, and stride 20. Without explicit locations, these scalar/vector inputs receive consecutive locations in declaration order.

`Marshal.SizeOf<Vertex>()` measures the struct size, and `Marshal.OffsetOf<Vertex>()` measures where each field starts. The full program checks these against the reflected stride and attribute offsets before continuing. A struct that merely has fields with familiar names is not enough: the bytes that reach the vertex buffer must match the attribute descriptions.

These are vertex-buffer rules, not constant-buffer packing rules. In this example, `Vector3` occupies the first 12 bytes and `Vector2` starts immediately afterward. Copying a constant-buffer layout assumption into this vertex representation would give the wrong UV offset. Keep the layout check with the struct if you later add a field or change its representation.

## What are stride and offset in Vulkan vertex input?

Stride is the byte distance between consecutive elements of a binding. Attribute offset is the byte distance from one element’s start to a particular field. In the full example, the next element starts 20 bytes after the current one, while its UV begins 12 bytes after the element starts.

A binding description also carries the input rate. An attribute description carries a location, the binding it reads, a numeric format and its field offset. These roles are described in the [Vulkan vertex input guide](https://docs.vulkan.org/guide/latest/vertex_input_data_processing.html). Do not confuse an attribute’s field offset with the separate offset you pass when binding a buffer.

`ToVulkan(VertexStepMode.Instance)` makes the entire returned binding advance per instance. It does not infer that policy from HLSL semantics, split fields into separate buffers, or create instance data. If you have both a per-vertex mesh buffer and a per-instance buffer, use the library’s [vertex stream layout API](https://recursiveai.org/oneshader/docs/compiler/) to describe separate streams rather than marking one interleaved buffer as both.

Changing the `binding` argument changes the binding number on the binding and its attributes together, as the variation demonstrates. Formats remain `R32G32B32Sfloat` for the position and `R32G32Sfloat` for the UV in the full example. Before creating a graphics pipeline, carry over the complete descriptions: locations alone do not tell Vulkan how to interpret the bytes.
