---
title: "Compile HLSL to Metal Shading Language"
url: https://recursiveai.org/oneshader/cookbook/hlsl-to-metal/
description: "Compile HLSL vertex, pixel and compute shaders to Metal Shading Language with OneShader, and get from C# the function names, the buffer, texture and…"
version: "1.2.0"
---

# Compile HLSL to Metal Shading Language

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](https://recursiveai.org/oneshader/docs/getting-started/). `Program.cs`:

```csharp
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}");
```

```text
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`:

```text
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 `Atlas` is 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 `MTLVertexFormat` values.
- **The `Frame` struct** generated in C# has the bytes `constant Frame_1_mem&` expects, the same as on Vulkan. The MSL file carries `static_assert`s on its size and offsets, compiled when `ONESHADER_CHECK_LAYOUT` is defined.

## From the command line

```sh
oneshader -T ps_6_0 -E main --target msl Tint.hlsl -Fo tint.metal
```

MSL reserves `main`, so the fragment function is `main0`:

```text
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](https://recursiveai.org/oneshader/docs/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](https://recursiveai.org/oneshader/cookbook/hlsl-16-bit-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. `precise` is honored either way ([The precise keyword](https://recursiveai.org/oneshader/cookbook/hlsl-precise-keyword/)).
- **Function constants.** Specialization constants become `[[function_constant(N)]]`, and a function that declares any must be created with `makeFunction(name:constantValues:)`, even to get the defaults. See [Specialization constants](https://recursiveai.org/oneshader/cookbook/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 `.metal` files weren’t compiled with it. The repository compiles every corpus file with `xcrun metal` and 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](https://recursiveai.org/oneshader/articles/hlsl-to-metal-converter/).
