home / skills / charleswiltgen / axiom / axiom-metal-migration-ref

This skill helps you migrate OpenGL, DirectX, and GLSL shaders to Metal by mapping types, shaders, and pipelines for iOS and macOS.

npx playbooks add skill charleswiltgen/axiom --skill axiom-metal-migration-ref

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
19.6 KB
---
name: axiom-metal-migration-ref
description: Use when converting shaders or looking up API equivalents - GLSL to MSL, HLSL to MSL, GL/DirectX to Metal mappings, MTKView setup code
license: MIT
compatibility: [iOS 12+, macOS 10.14+, tvOS 12+]
metadata:
  version: "1.0.0"
---

# Metal Migration Reference

Complete reference for converting OpenGL/DirectX code to Metal.

## When to Use This Reference

Use this reference when:
- Converting GLSL shaders to Metal Shading Language (MSL)
- Converting HLSL shaders to MSL
- Looking up GL/D3D API equivalents in Metal
- Setting up MTKView or CAMetalLayer
- Building render pipelines
- Using Metal Shader Converter for DirectX

## Part 1: GLSL to MSL Conversion

### Type Mappings

| GLSL | MSL | Notes |
|------|-----|-------|
| `void` | `void` | |
| `bool` | `bool` | |
| `int` | `int` | 32-bit signed |
| `uint` | `uint` | 32-bit unsigned |
| `float` | `float` | 32-bit |
| `double` | N/A | Use `float` (no 64-bit float in MSL) |
| `vec2` | `float2` | |
| `vec3` | `float3` | |
| `vec4` | `float4` | |
| `ivec2` | `int2` | |
| `ivec3` | `int3` | |
| `ivec4` | `int4` | |
| `uvec2` | `uint2` | |
| `uvec3` | `uint3` | |
| `uvec4` | `uint4` | |
| `bvec2` | `bool2` | |
| `bvec3` | `bool3` | |
| `bvec4` | `bool4` | |
| `mat2` | `float2x2` | |
| `mat3` | `float3x3` | |
| `mat4` | `float4x4` | |
| `mat2x3` | `float2x3` | Columns x Rows |
| `mat3x4` | `float3x4` | |
| `sampler2D` | `texture2d<float>` + `sampler` | Separate in MSL |
| `sampler3D` | `texture3d<float>` + `sampler` | |
| `samplerCube` | `texturecube<float>` + `sampler` | |
| `sampler2DArray` | `texture2d_array<float>` + `sampler` | |
| `sampler2DShadow` | `depth2d<float>` + `sampler` | |

### Built-in Variable Mappings

| GLSL | MSL | Stage |
|------|-----|-------|
| `gl_Position` | Return `[[position]]` | Vertex |
| `gl_PointSize` | Return `[[point_size]]` | Vertex |
| `gl_VertexID` | `[[vertex_id]]` parameter | Vertex |
| `gl_InstanceID` | `[[instance_id]]` parameter | Vertex |
| `gl_FragCoord` | `[[position]]` parameter | Fragment |
| `gl_FrontFacing` | `[[front_facing]]` parameter | Fragment |
| `gl_PointCoord` | `[[point_coord]]` parameter | Fragment |
| `gl_FragDepth` | Return `[[depth(any)]]` | Fragment |
| `gl_SampleID` | `[[sample_id]]` parameter | Fragment |
| `gl_SamplePosition` | `[[sample_position]]` parameter | Fragment |

### Function Mappings

| GLSL | MSL | Notes |
|------|-----|-------|
| `texture(sampler, uv)` | `tex.sample(sampler, uv)` | Method on texture |
| `textureLod(sampler, uv, lod)` | `tex.sample(sampler, uv, level(lod))` | |
| `textureGrad(sampler, uv, ddx, ddy)` | `tex.sample(sampler, uv, gradient2d(ddx, ddy))` | |
| `texelFetch(sampler, coord, lod)` | `tex.read(coord, lod)` | Integer coords |
| `textureSize(sampler, lod)` | `tex.get_width(lod)`, `tex.get_height(lod)` | Separate calls |
| `dFdx(v)` | `dfdx(v)` | |
| `dFdy(v)` | `dfdy(v)` | |
| `fwidth(v)` | `fwidth(v)` | Same |
| `mix(a, b, t)` | `mix(a, b, t)` | Same |
| `clamp(v, lo, hi)` | `clamp(v, lo, hi)` | Same |
| `smoothstep(e0, e1, x)` | `smoothstep(e0, e1, x)` | Same |
| `step(edge, x)` | `step(edge, x)` | Same |
| `mod(x, y)` | `fmod(x, y)` | Different name |
| `fract(x)` | `fract(x)` | Same |
| `inversesqrt(x)` | `rsqrt(x)` | Different name |
| `atan(y, x)` | `atan2(y, x)` | Different name |

### Shader Structure Conversion

**GLSL Vertex Shader**:
```glsl
#version 300 es
precision highp float;

layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec2 aTexCoord;

uniform mat4 uModelViewProjection;

out vec2 vTexCoord;

void main() {
    gl_Position = uModelViewProjection * vec4(aPosition, 1.0);
    vTexCoord = aTexCoord;
}
```

**MSL Vertex Shader**:
```metal
#include <metal_stdlib>
using namespace metal;

struct VertexIn {
    float3 position [[attribute(0)]];
    float2 texCoord [[attribute(1)]];
};

struct VertexOut {
    float4 position [[position]];
    float2 texCoord;
};

struct Uniforms {
    float4x4 modelViewProjection;
};

vertex VertexOut vertexShader(
    VertexIn in [[stage_in]],
    constant Uniforms& uniforms [[buffer(1)]]
) {
    VertexOut out;
    out.position = uniforms.modelViewProjection * float4(in.position, 1.0);
    out.texCoord = in.texCoord;
    return out;
}
```

**GLSL Fragment Shader**:
```glsl
#version 300 es
precision highp float;

in vec2 vTexCoord;
uniform sampler2D uTexture;

out vec4 fragColor;

void main() {
    fragColor = texture(uTexture, vTexCoord);
}
```

**MSL Fragment Shader**:
```metal
fragment float4 fragmentShader(
    VertexOut in [[stage_in]],
    texture2d<float> tex [[texture(0)]],
    sampler samp [[sampler(0)]]
) {
    return tex.sample(samp, in.texCoord);
}
```

### Precision Qualifiers

GLSL precision qualifiers have no direct MSL equivalent — MSL uses explicit types:

| GLSL | MSL Equivalent |
|------|----------------|
| `lowp float` | `half` (16-bit) |
| `mediump float` | `half` (16-bit) |
| `highp float` | `float` (32-bit) |
| `lowp int` | `short` (16-bit) |
| `mediump int` | `short` (16-bit) |
| `highp int` | `int` (32-bit) |

### Buffer Alignment (Critical)

**GLSL/C assumes**:
- `vec3`: 12 bytes, any alignment
- `vec4`: 16 bytes

**MSL requires**:
- `float3`: 12 bytes storage, **16-byte aligned**
- `float4`: 16 bytes storage, 16-byte aligned

**Solution**: Use `simd` types in Swift for CPU-GPU shared structs:

```swift
import simd

struct Uniforms {
    var modelViewProjection: simd_float4x4  // Correct alignment
    var cameraPosition: simd_float3         // 16-byte aligned
    var padding: Float = 0                   // Explicit padding if needed
}
```

Or use packed types in MSL (slower):
```metal
struct VertexPacked {
    packed_float3 position;  // 12 bytes, no padding
    packed_float2 texCoord;  // 8 bytes
};
```

## Part 2: HLSL to MSL Conversion

### Type Mappings

| HLSL | MSL | Notes |
|------|-----|-------|
| `float` | `float` | |
| `float2` | `float2` | |
| `float3` | `float3` | |
| `float4` | `float4` | |
| `half` | `half` | |
| `int` | `int` | |
| `uint` | `uint` | |
| `bool` | `bool` | |
| `float2x2` | `float2x2` | |
| `float3x3` | `float3x3` | |
| `float4x4` | `float4x4` | |
| `Texture2D` | `texture2d<float>` | |
| `Texture3D` | `texture3d<float>` | |
| `TextureCube` | `texturecube<float>` | |
| `SamplerState` | `sampler` | |
| `RWTexture2D` | `texture2d<float, access::read_write>` | |
| `RWBuffer` | `device float* [[buffer(n)]]` | |
| `StructuredBuffer` | `constant T* [[buffer(n)]]` | |
| `RWStructuredBuffer` | `device T* [[buffer(n)]]` | |

### Semantic Mappings

| HLSL Semantic | MSL Attribute |
|---------------|---------------|
| `SV_Position` | `[[position]]` |
| `SV_Target0` | Return value / `[[color(0)]]` |
| `SV_Target1` | `[[color(1)]]` |
| `SV_Depth` | `[[depth(any)]]` |
| `SV_VertexID` | `[[vertex_id]]` |
| `SV_InstanceID` | `[[instance_id]]` |
| `SV_IsFrontFace` | `[[front_facing]]` |
| `SV_SampleIndex` | `[[sample_id]]` |
| `SV_PrimitiveID` | `[[primitive_id]]` |
| `SV_DispatchThreadID` | `[[thread_position_in_grid]]` |
| `SV_GroupThreadID` | `[[thread_position_in_threadgroup]]` |
| `SV_GroupID` | `[[threadgroup_position_in_grid]]` |
| `SV_GroupIndex` | `[[thread_index_in_threadgroup]]` |

### Function Mappings

| HLSL | MSL | Notes |
|------|-----|-------|
| `tex.Sample(samp, uv)` | `tex.sample(samp, uv)` | Lowercase |
| `tex.SampleLevel(samp, uv, lod)` | `tex.sample(samp, uv, level(lod))` | |
| `tex.SampleGrad(samp, uv, ddx, ddy)` | `tex.sample(samp, uv, gradient2d(ddx, ddy))` | |
| `tex.Load(coord)` | `tex.read(coord.xy, coord.z)` | Split coord |
| `mul(a, b)` | `a * b` | Operator |
| `saturate(x)` | `saturate(x)` | Same |
| `lerp(a, b, t)` | `mix(a, b, t)` | Different name |
| `frac(x)` | `fract(x)` | Different name |
| `ddx(v)` | `dfdx(v)` | Different name |
| `ddy(v)` | `dfdy(v)` | Different name |
| `clip(x)` | `if (x < 0) discard_fragment()` | Manual |
| `discard` | `discard_fragment()` | Function call |

### Metal Shader Converter (DirectX → Metal)

Apple's official tool for converting DXIL (compiled HLSL) to Metal libraries.

**Requirements**:
- macOS 13+ with Xcode 15+
- OR Windows 10+ with VS 2019+
- Target devices: Argument Buffers Tier 2 (macOS 14+, iOS 17+)

**Workflow**:

```bash
# Step 1: Compile HLSL to DXIL using DXC
dxc -T vs_6_0 -E MainVS -Fo vertex.dxil shader.hlsl
dxc -T ps_6_0 -E MainPS -Fo fragment.dxil shader.hlsl

# Step 2: Convert DXIL to Metal library
metal-shaderconverter vertex.dxil -o vertex.metallib
metal-shaderconverter fragment.dxil -o fragment.metallib

# Step 3: Load in Swift
let vertexLib = try device.makeLibrary(URL: vertexURL)
let fragmentLib = try device.makeLibrary(URL: fragmentURL)
```

**Key Options**:

| Option | Purpose |
|--------|---------|
| `-o <file>` | Output metallib path |
| `--minimum-gpu-family` | Target GPU family |
| `--minimum-os-build-version` | Minimum OS version |
| `--vertex-stage-in` | Separate vertex fetch function |
| `-dualSourceBlending` | Enable dual-source blending |

**Supported Shader Models**: SM 6.0 - 6.6 (with limitations on 6.6 features)

## Part 3: OpenGL API to Metal API

### View/Context Setup

| OpenGL | Metal |
|--------|-------|
| `NSOpenGLView` | `MTKView` |
| `GLKView` | `MTKView` |
| `EAGLContext` | `MTLDevice` + `MTLCommandQueue` |
| `CGLContextObj` | `MTLDevice` |

### Resource Creation

| OpenGL | Metal |
|--------|-------|
| `glGenBuffers` + `glBufferData` | `device.makeBuffer(bytes:length:options:)` |
| `glGenTextures` + `glTexImage2D` | `device.makeTexture(descriptor:)` + `texture.replace(region:...)` |
| `glGenFramebuffers` | `MTLRenderPassDescriptor` |
| `glGenVertexArrays` | `MTLVertexDescriptor` |
| `glCreateShader` + `glCompileShader` | Build-time compilation → `MTLLibrary` |
| `glCreateProgram` + `glLinkProgram` | `MTLRenderPipelineDescriptor` → `MTLRenderPipelineState` |

### State Management

| OpenGL | Metal |
|--------|-------|
| `glEnable(GL_DEPTH_TEST)` | `MTLDepthStencilDescriptor` → `MTLDepthStencilState` |
| `glDepthFunc(GL_LESS)` | `descriptor.depthCompareFunction = .less` |
| `glEnable(GL_BLEND)` | `pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true` |
| `glBlendFunc` | `sourceRGBBlendFactor`, `destinationRGBBlendFactor` |
| `glCullFace` | `encoder.setCullMode(.back)` |
| `glFrontFace` | `encoder.setFrontFacing(.counterClockwise)` |
| `glViewport` | `encoder.setViewport(MTLViewport(...))` |
| `glScissor` | `encoder.setScissorRect(MTLScissorRect(...))` |

### Draw Commands

| OpenGL | Metal |
|--------|-------|
| `glDrawArrays(mode, first, count)` | `encoder.drawPrimitives(type:vertexStart:vertexCount:)` |
| `glDrawElements(mode, count, type, indices)` | `encoder.drawIndexedPrimitives(type:indexCount:indexType:indexBuffer:indexBufferOffset:)` |
| `glDrawArraysInstanced` | `encoder.drawPrimitives(type:vertexStart:vertexCount:instanceCount:)` |
| `glDrawElementsInstanced` | `encoder.drawIndexedPrimitives(...instanceCount:)` |

### Primitive Types

| OpenGL | Metal |
|--------|-------|
| `GL_POINTS` | `.point` |
| `GL_LINES` | `.line` |
| `GL_LINE_STRIP` | `.lineStrip` |
| `GL_TRIANGLES` | `.triangle` |
| `GL_TRIANGLE_STRIP` | `.triangleStrip` |
| `GL_TRIANGLE_FAN` | N/A (decompose to triangles) |

## Part 4: Complete Setup Examples

### MTKView Setup (Recommended)

```swift
import MetalKit

class GameViewController: UIViewController {
    var metalView: MTKView!
    var renderer: Renderer!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Create Metal view
        guard let device = MTLCreateSystemDefaultDevice() else {
            fatalError("Metal not supported")
        }

        metalView = MTKView(frame: view.bounds, device: device)
        metalView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        metalView.colorPixelFormat = .bgra8Unorm
        metalView.depthStencilPixelFormat = .depth32Float
        metalView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1)
        metalView.preferredFramesPerSecond = 60
        view.addSubview(metalView)

        // Create renderer
        renderer = Renderer(metalView: metalView)
        metalView.delegate = renderer
    }
}

class Renderer: NSObject, MTKViewDelegate {
    let device: MTLDevice
    let commandQueue: MTLCommandQueue
    var pipelineState: MTLRenderPipelineState!
    var depthState: MTLDepthStencilState!
    var vertexBuffer: MTLBuffer!

    init(metalView: MTKView) {
        device = metalView.device!
        commandQueue = device.makeCommandQueue()!
        super.init()

        buildPipeline(metalView: metalView)
        buildDepthStencil()
        buildBuffers()
    }

    private func buildPipeline(metalView: MTKView) {
        let library = device.makeDefaultLibrary()!

        let descriptor = MTLRenderPipelineDescriptor()
        descriptor.vertexFunction = library.makeFunction(name: "vertexShader")
        descriptor.fragmentFunction = library.makeFunction(name: "fragmentShader")
        descriptor.colorAttachments[0].pixelFormat = metalView.colorPixelFormat
        descriptor.depthAttachmentPixelFormat = metalView.depthStencilPixelFormat

        // Vertex descriptor (matches shader's VertexIn struct)
        let vertexDescriptor = MTLVertexDescriptor()
        vertexDescriptor.attributes[0].format = .float3
        vertexDescriptor.attributes[0].offset = 0
        vertexDescriptor.attributes[0].bufferIndex = 0
        vertexDescriptor.attributes[1].format = .float2
        vertexDescriptor.attributes[1].offset = MemoryLayout<SIMD3<Float>>.stride
        vertexDescriptor.attributes[1].bufferIndex = 0
        vertexDescriptor.layouts[0].stride = MemoryLayout<Vertex>.stride
        descriptor.vertexDescriptor = vertexDescriptor

        pipelineState = try! device.makeRenderPipelineState(descriptor: descriptor)
    }

    private func buildDepthStencil() {
        let descriptor = MTLDepthStencilDescriptor()
        descriptor.depthCompareFunction = .less
        descriptor.isDepthWriteEnabled = true
        depthState = device.makeDepthStencilState(descriptor: descriptor)
    }

    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
        // Handle resize
    }

    func draw(in view: MTKView) {
        guard let drawable = view.currentDrawable,
              let descriptor = view.currentRenderPassDescriptor,
              let commandBuffer = commandQueue.makeCommandBuffer(),
              let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {
            return
        }

        encoder.setRenderPipelineState(pipelineState)
        encoder.setDepthStencilState(depthState)
        encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
        encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount)
        encoder.endEncoding()

        commandBuffer.present(drawable)
        commandBuffer.commit()
    }
}
```

### CAMetalLayer Setup (Custom Control)

```swift
import Metal
import QuartzCore

class MetalLayerView: UIView {
    var metalLayer: CAMetalLayer!
    var device: MTLDevice!
    var commandQueue: MTLCommandQueue!
    var displayLink: CADisplayLink?

    override class var layerClass: AnyClass { CAMetalLayer.self }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    private func setup() {
        device = MTLCreateSystemDefaultDevice()!
        commandQueue = device.makeCommandQueue()!

        metalLayer = layer as? CAMetalLayer
        metalLayer.device = device
        metalLayer.pixelFormat = .bgra8Unorm
        metalLayer.framebufferOnly = true

        displayLink = CADisplayLink(target: self, selector: #selector(render))
        displayLink?.add(to: .main, forMode: .common)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        metalLayer.drawableSize = CGSize(
            width: bounds.width * contentScaleFactor,
            height: bounds.height * contentScaleFactor
        )
    }

    @objc func render() {
        guard let drawable = metalLayer.nextDrawable(),
              let commandBuffer = commandQueue.makeCommandBuffer() else {
            return
        }

        let descriptor = MTLRenderPassDescriptor()
        descriptor.colorAttachments[0].texture = drawable.texture
        descriptor.colorAttachments[0].loadAction = .clear
        descriptor.colorAttachments[0].storeAction = .store
        descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1)

        guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {
            return
        }

        // Draw commands here
        encoder.endEncoding()

        commandBuffer.present(drawable)
        commandBuffer.commit()
    }
}
```

### Compute Shader Setup

```swift
class ComputeProcessor {
    let device: MTLDevice
    let commandQueue: MTLCommandQueue
    var computePipeline: MTLComputePipelineState!

    init() {
        device = MTLCreateSystemDefaultDevice()!
        commandQueue = device.makeCommandQueue()!

        let library = device.makeDefaultLibrary()!
        let function = library.makeFunction(name: "computeKernel")!
        computePipeline = try! device.makeComputePipelineState(function: function)
    }

    func process(input: MTLBuffer, output: MTLBuffer, count: Int) {
        let commandBuffer = commandQueue.makeCommandBuffer()!
        let encoder = commandBuffer.makeComputeCommandEncoder()!

        encoder.setComputePipelineState(computePipeline)
        encoder.setBuffer(input, offset: 0, index: 0)
        encoder.setBuffer(output, offset: 0, index: 1)

        let threadGroupSize = MTLSize(width: 256, height: 1, depth: 1)
        let threadGroups = MTLSize(
            width: (count + 255) / 256,
            height: 1,
            depth: 1
        )

        encoder.dispatchThreadgroups(threadGroups, threadsPerThreadgroup: threadGroupSize)
        encoder.endEncoding()

        commandBuffer.commit()
        commandBuffer.waitUntilCompleted()
    }
}
```

```metal
// Compute shader
kernel void computeKernel(
    device float* input [[buffer(0)]],
    device float* output [[buffer(1)]],
    uint id [[thread_position_in_grid]]
) {
    output[id] = input[id] * 2.0;
}
```

## Part 5: Storage Modes & Synchronization

### Buffer Storage Modes

| Mode | CPU Access | GPU Access | Use Case |
|------|------------|------------|----------|
| `.shared` | Read/Write | Read/Write | Small dynamic data, uniforms |
| `.private` | None | Read/Write | Static assets, render targets |
| `.managed` (macOS) | Read/Write | Read/Write | Large buffers with partial updates |

```swift
// Shared: CPU and GPU both access (iOS typical)
let uniformBuffer = device.makeBuffer(length: size, options: .storageModeShared)

// Private: GPU only (best for static geometry)
let vertexBuffer = device.makeBuffer(bytes: vertices, length: size, options: .storageModePrivate)

// Managed: Explicit sync (macOS)
#if os(macOS)
let buffer = device.makeBuffer(length: size, options: .storageModeManaged)
// After CPU write:
buffer.didModifyRange(0..<size)
#endif
```

### Texture Storage Modes

```swift
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
    pixelFormat: .rgba8Unorm,
    width: 1024,
    height: 1024,
    mipmapped: true
)

// For static textures (loaded once)
descriptor.storageMode = .private
descriptor.usage = [.shaderRead]

// For render targets
descriptor.storageMode = .private
descriptor.usage = [.renderTarget, .shaderRead]

// For CPU-readable (screenshots, readback)
descriptor.storageMode = .shared  // iOS
descriptor.storageMode = .managed  // macOS
descriptor.usage = [.shaderRead, .shaderWrite]
```

## Resources

**WWDC**: 2016-00602, 2018-00604, 2019-00611

**Docs**: /metal/migrating-opengl-code-to-metal, /metal/shader-converter, /metalkit/mtkview

**Skills**: axiom-metal-migration, axiom-metal-migration-diag

---

**Last Updated**: 2025-12-29
**Platforms**: iOS 12+, macOS 10.14+, tvOS 12+
**Status**: Complete shader conversion and API mapping reference

Overview

This skill is a compact Metal migration reference for converting GLSL/HLSL and OpenGL/DirectX code to Metal. It provides type and builtin mappings, shader structure examples, API equivalences, MTKView setup code, and guidance for the Metal Shader Converter workflow. Use it to speed up shader porting and to avoid common alignment and resource-binding pitfalls.

How this skill works

The skill inspects shader constructs and rendering patterns and supplies direct MSL equivalents: type maps, built‑in/semantic translations, texture/sample usage, and function name differences. It also offers concrete vertex/fragment shader examples, buffer alignment rules, and a step‑by‑step MTKView + renderer setup in Swift. For HLSL it shows DXIL→Metal conversion commands and key metal‑shaderconverter options.

When to use it

  • Porting GLSL shaders to Metal Shading Language (MSL).
  • Porting HLSL shaders or using DXIL → Metal converter workflows.
  • Replacing OpenGL/DirectX API calls with Metal equivalents.
  • Setting up MTKView, render pipelines, vertex descriptors, and depth states.
  • Debugging mismatched buffer alignment and CPU/GPU struct layouts.

Best practices

  • Prefer simd types in Swift for GPU-shared structs to guarantee 16-byte alignment.
  • Separate texture and sampler in MSL (texture2d<T> + sampler).
  • Use packed types only when necessary; they can be slower and harder to align.
  • Match vertex descriptor layouts exactly to shader attribute declarations.
  • Compile shaders into MTLLibrary at build time when possible for runtime stability.

Example use cases

  • Convert a GLSL ES 3.0 vertex/fragment pair to MSL with proper attribute and [[position]] mappings.
  • Translate HLSL semantics like SV_Position and SV_Target to MSL attributes and color indices.
  • Run DXC then metal-shaderconverter to produce metallib for iOS 17 / macOS 14 targets.
  • Replace glGenBuffers/glTexImage2D calls with device.makeBuffer and device.makeTexture calls.
  • Implement an MTKView-based renderer with pipeline state, depth stencil, and vertex buffers.

FAQ

Do GLSL precision qualifiers map directly to MSL?

No. MSL uses explicit types: use half for low/medium precision floats and float for high precision.

How do I handle vec3 alignment differences?

MSL requires 16-byte alignment for float3 in GPU buffers. Use simd_float3 plus padding or simd types (simd_float4) to satisfy alignment.