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).
dotnet add package RecursiveAI.OneShaderRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
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);
12Creates a per-vertex Vulkan binding with a 12-byte stride and one position attribute.
Unzip the example, 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.
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; }
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:
var vulkan = vertex.ToVulkan(VertexStepMode.Instance);The binding line becomes:
Binding 0: stride 20, InstanceUse a different Vulkan binding number. With the full program’s vertex, this prints the selected binding:
var input = vertex.ToVulkan( VertexStepMode.Vertex, binding: 2); Console.WriteLine(input.Bindings[0].Binding); Console.WriteLine(input.Attributes[0].Binding);
2 2
Related recipes
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 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. 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 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.