---
title: "Read shader reflection in C#: bindings, buffer layouts and vertex inputs"
url: https://recursiveai.org/oneshader/cookbook/spirv-reflection-csharp/
description: "Read what a compiled HLSL shader binds, where, in which stages, its buffer layouts and its vertex inputs, from C# or as JSON, and turn it into Vulkan…"
version: "1.2.0"
---

# Read shader reflection in C#: bindings, buffer layouts and vertex inputs

Read what a compiled HLSL shader binds, where, in which stages, its buffer layouts and its vertex inputs, from C# or as JSON, and turn it into Vulkan descriptor set layouts, with no native SPIRV-Reflect.

Every compile returns reflection next to the code: resources with their slots per target and their byte layouts, push-constant blocks, and each entry point’s inputs, outputs, used resources and thread-group size. It comes from the compiler, not from parsing the SPIR-V afterwards, and it’s the same model for all three targets.

## From C#

With `RecursiveAI.OneShader` 1.2.0 and the `Shaders/Sprite.hlsl` of [Getting started](https://recursiveai.org/oneshader/docs/getting-started/):

```csharp
using OneShader;
using OneShader.Layouts;
using OneShader.Layouts.Vulkan;
using OneShader.Reflection;

CompileResult result = ShaderCompiler.CompileFile("Shaders/Sprite.hlsl");
if (!result.Success) { foreach (var d in result.Diagnostics) Console.WriteLine(d); return; }
ShaderReflection reflection = result.Reflection!;

// Resources: kind, slot, stages, and the byte layout of buffers.
foreach (ResourceInfo r in reflection.Resources)
{
    TargetSlot slot = r.Slots.First(s => s.Target == CompileTarget.SpirV);
    Console.WriteLine($"{r.Name}: {r.Kind}, set {slot.Set} binding {slot.Binding}, stages {r.Stages}");
    if (r.Layout is { Kind: ReflectedTypeKind.Struct } layout)
        foreach (ReflectedMember m in layout.Members)
            Console.WriteLine($"    {m.Name} at {m.Offset}, {m.Type.Size} bytes");
}

// Entry points: stage, resources used, and vertex inputs with their formats.
foreach (EntryPointInfo e in reflection.EntryPoints)
{
    Console.WriteLine($"{e.Name}: {e.Stage}, uses {string.Join(", ", e.UsedResources)}");
    foreach (StageVariable v in e.Inputs.Where(v => v.Format is not null))
        Console.WriteLine($"    in location {v.Location}: {v.Semantic}{v.SemanticIndex} {v.Format}");
}

// From reflection to a Vulkan pipeline layout.
EntryLayout.TryCreate(reflection, "VSMain", out EntryLayout? vs, out _);
EntryLayout.TryCreate(reflection, "PSMain", out EntryLayout? ps, out _);
if (!PipelineLayout.TryCreate([vs!, ps!], out PipelineLayout? pipeline, out LayoutError? error))
{
    Console.WriteLine(error);
    return;
}
foreach (DescriptorSetLayoutInfo set in pipeline!.ToVulkan().SetLayouts)
    foreach (DescriptorSetLayoutBindingInfo b in set.Bindings)
        Console.WriteLine($"VkDescriptorSetLayoutBinding {{ binding = {b.Binding}, descriptorType = {(uint)b.DescriptorType}, descriptorCount = {b.DescriptorCount}, stageFlags = 0x{(uint)b.StageFlags:x} }}");
```

```text
Frame: ConstantBuffer, set 0 binding 0, stages Vertex
    ViewProjection at 0, 64 bytes
    Tint at 64, 16 bytes
Atlas: SampledTexture, set 0 binding 1, stages Pixel
AtlasSampler: Sampler, set 0 binding 2, stages Pixel
VSMain: Vertex, uses Frame
    in location 0: POSITION0 Float32x2
    in location 1: TEXCOORD0 Float32x2
    in location 2: COLOR0 Unorm8x4
PSMain: Pixel, uses Atlas, AtlasSampler
VkDescriptorSetLayoutBinding { binding = 0, descriptorType = 6, descriptorCount = 1, stageFlags = 0x1 }
VkDescriptorSetLayoutBinding { binding = 1, descriptorType = 2, descriptorCount = 1, stageFlags = 0x10 }
VkDescriptorSetLayoutBinding { binding = 2, descriptorType = 0, descriptorCount = 1, stageFlags = 0x10 }
```

`descriptorType` 6, 2 and 0 are `VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER`, `SAMPLED_IMAGE` and `SAMPLER`. `stageFlags` 0x1 and 0x10 are `VK_SHADER_STAGE_VERTEX_BIT` and `FRAGMENT_BIT`.

- `Slots` has one entry per compiled target. Vulkan and WGSL use the same set and binding, and the Metal slot is the argument index.
- `Layout` is the byte layout of a buffer, members, offsets, array and matrix strides included, equal for every target.
- `PipelineLayout.TryCreate` merges entry points that run together: a binding used by both gets the union of their stages, and a conflict comes back as a `LayoutError` value. Unused sets are empty, as Vulkan needs them.
- Projects that use `RecursiveAI.OneShader.Build` don’t need this at run time: the generated class has the same data as constants, and `Sprite.Reflection` holds the reflection it was generated from.

## As JSON from the command line

```sh
oneshader -T vs_6_0 -E VSMain --reflect sprite.vs.json Shaders/Sprite.hlsl -Fo sprite.vs.spv
```

The start of the entry point in `sprite.vs.json`:

```json
  "entryPoints": [
    {
      "name": "VSMain",
      "stage": "Vertex",
      "vertexStride": 20,
      "inputs": [
        {
          "name": "i.Position",
          "semantic": "POSITION",
          "semanticIndex": 0,
          "location": 0,
          "builtIn": "None",
          "scalar": "Float",
          "components": 2,
          "format": "Float32x2",
          "offset": 0,
          "interpolation": "Default"
        },
```

The JSON is schema version 1: keys in camelCase, enums by name, a fixed order, `\n` line ends and no machine paths, so the same compile gives the same bytes. Breaking changes raise `schemaVersion`.

The build task’s own reflection file has a different, line-based format, described in [Reflection file format](https://recursiveai.org/oneshader/docs/reflection-format/). To check a recompiled shader against the reflection the C# was built from, see [Reload shaders while the app runs](https://recursiveai.org/oneshader/cookbook/hot-reload-shaders/).
