Compile HLSL vertex, pixel and compute shaders to Metal Shading Language with OneShader, and get from C# the function names, the buffer, texture and sampler index of every resource, and the MTLVertexDescriptor data.
OneShader writes one MSL source file per entry point, for Metal Shading Language 2.4 or newer (macOS 12 and iOS 15), from the same front end as its SPIR-V and with the same buffer byte layout. It writes MSL directly from its own IR, without going through SPIR-V and SPIRV-Cross.
With dotnet build
The project references RecursiveAI.OneShader.Build 1.2.0 with OneShaderTargets set to Msl and contains the Shaders/Sprite.hlsl of Getting started. Program.cs:
using OneShader.Layouts.Metal; using Metal.Shaders; // The MSL source and the function name to pass to newFunction(name:). File.WriteAllBytes("sprite.vs.metal", Sprite.VSMain.Msl.ToArray()); File.WriteAllBytes("sprite.ps.metal", Sprite.PSMain.Msl.ToArray()); Console.WriteLine($"vertex function {Sprite.VSMain.MslFunctionName}, fragment function {Sprite.PSMain.MslFunctionName}"); // The argument index of each resource, per stage: setVertexBuffer, setFragmentTexture, ... if (Sprite.Pipeline.TryToMetal(out MetalPipelineLayout? metal, out var error)) foreach (MetalStageLayout stage in metal!.Stages) foreach (MetalArgument arg in stage.Arguments) Console.WriteLine($"{stage.Stage}: {arg.Name} at [[{arg.Kind.ToString().ToLowerInvariant()}({arg.Index})]]"); else Console.WriteLine(error); // MTLVertexDescriptor data for the interleaved vertex buffer. MetalVertexDescriptor vd = Sprite.VSMain.Layout.Vertex!.ToMetal(Sprite.VSMain.MslVertexBufferIndex); Console.WriteLine($"vertex buffer {vd.BufferIndex}, stride {vd.Stride}, {vd.StepFunction}"); foreach (MetalVertexAttribute a in vd.Attributes) Console.WriteLine($" attributes[{a.AttributeIndex}]: {a.Format} ({(uint)a.Format}) offset {a.Offset}");
vertex function VSMain, fragment function PSMain Vertex: Frame at [[buffer(0)]] Pixel: Atlas at [[texture(0)]] Pixel: AtlasSampler at [[sampler(0)]] vertex buffer 30, stride 20, PerVertex attributes[0]: Float2 (29) offset 0 attributes[1]: Float2 (29) offset 8 attributes[2]: UChar4Normalized (9) offset 16
The vertex function in sprite.vs.metal:
vertex VSMain_out VSMain( VSMain_in stage_in [[stage_in]], constant Frame_1_mem& Frame [[buffer(0)]])
- Argument indices are counted per class (buffers, textures, samplers) in declaration order, so
Atlasis texture 0 although its Vulkan binding is 1.MetalStageLayout.TryFind(set, binding)goes from a Vulkan-style slot to the Metal index. - The vertex buffer is bound at index 30 by default, away from the resource buffers. The numbers after the formats are
MTLVertexFormatvalues. - The
Framestruct generated in C# has the bytesconstant Frame_1_mem&expects, the same as on Vulkan. The MSL file carriesstatic_asserts on its size and offsets, compiled whenONESHADER_CHECK_LAYOUTis defined.
From the command line
oneshader -T ps_6_0 -E main --target msl Tint.hlsl -Fo tint.metalMSL reserves main, so the fragment function is main0:
fragment main0_out main0(MslFunctionName in generated code, and MslBackend.EntryFunctionName at run time, give the name to pass to newFunction(name:).
Limits in 1.2.0
From the MSL backend page, these are compile errors on Metal: double; 64-bit integer vectors in buffers; 64-bit atomics other than unsigned InterlockedMax/InterlockedMin whose result isn’t used; SV_CullDistance; GetDimensions on structured buffers (Metal can’t query their length); atomics on textures; WaveMatch and WaveMultiPrefix*; the wave lane index and count in vertex shaders; integer and bool matrices; unbounded resource arrays; jumps out of several nested loops at once; comparison sampling on 1D and 3D textures; texel offsets on cube and 1D textures; overlapping packoffset members.
With 16-bit types enabled, half, short and ushort are Metal’s native types, and long and ulong are native 64-bit integers. See 16-bit and 64-bit types.
And differences to know:
- Clip-space Y points up in Metal and down in Vulkan. The shader isn’t adjusted, so draw with a viewport of negative height, as the repository’s Metal runner does.
- Fast math is Metal’s default, so NaN and infinity handling isn’t guaranteed unless the host turns it off.
preciseis honored either way (The precise keyword). - Function constants. Specialization constants become
[[function_constant(N)]], and a function that declares any must be created withmakeFunction(name:constantValues:), even to get the defaults. See Specialization constants. - Signed integer overflow is undefined in MSL, where HLSL wraps.
- Checking. The samples here ran on Linux, where Apple’s compiler isn’t available, so their
.metalfiles weren’t compiled with it. The repository compiles every corpus file withxcrun metaland runs the corpus on a Metal GPU against DXC’s SPIR-V on Vulkan.
SPIRV-Cross, Metal Shader Converter and OneShader compared: HLSL to Metal converter.