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:
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} }}");
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.
Slotshas one entry per compiled target. Vulkan and WGSL use the same set and binding, and the Metal slot is the argument index.Layoutis the byte layout of a buffer, members, offsets, array and matrix strides included, equal for every target.PipelineLayout.TryCreatemerges entry points that run together: a binding used by both gets the union of their stages, and a conflict comes back as aLayoutErrorvalue. Unused sets are empty, as Vulkan needs them.- Projects that use
RecursiveAI.OneShader.Builddon’t need this at run time: the generated class has the same data as constants, andSprite.Reflectionholds the reflection it was generated from.
As JSON from the command line
oneshader -T vs_6_0 -E VSMain --reflect sprite.vs.json Shaders/Sprite.hlsl -Fo sprite.vs.spvThe start of the entry point in sprite.vs.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. To check a recompiled shader against the reflection the C# was built from, see Reload shaders while the app runs.