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 |
|---|
| | |
| | |
| | 32-bit signed |
| | 32-bit unsigned |
| | 32-bit |
| N/A | Use (no 64-bit float in MSL) |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | Columns x Rows |
| | |
| + | Separate in MSL |
| + | |
| + | |
| + | |
| + | |
Built-in Variable Mappings
| GLSL | MSL | Stage |
|---|
| Return | Vertex |
| Return | Vertex |
| parameter | Vertex |
| parameter | Vertex |
| parameter | Fragment |
| parameter | Fragment |
| parameter | Fragment |
| Return | Fragment |
| parameter | Fragment |
| parameter | Fragment |
Function Mappings
| GLSL | MSL | Notes |
|---|
| | 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)
| | Integer coords |
textureSize(sampler, lod)
| , | Separate calls |
| | |
| | |
| | Same |
| | Same |
| | Same |
| | Same |
| | Same |
| | Different name |
| | Same |
| | Different name |
| | 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 |
|---|
| (16-bit) |
| (16-bit) |
| (32-bit) |
| (16-bit) |
| (16-bit) |
| (32-bit) |
Buffer Alignment (Critical)
GLSL/C assumes:
- : 12 bytes, any alignment
- : 16 bytes
MSL requires:
- : 12 bytes storage, 16-byte aligned
- : 16 bytes storage, 16-byte aligned
Solution: Use
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 |
|---|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| texture2d<float, access::read_write>
| |
| device float* [[buffer(n)]]
| |
| constant T* [[buffer(n)]]
| |
| | |
Semantic Mappings
| HLSL Semantic | MSL Attribute |
|---|
| |
| Return value / |
| |
| |
| |
| |
| |
| |
| |
| [[thread_position_in_grid]]
|
| [[thread_position_in_threadgroup]]
|
| [[threadgroup_position_in_grid]]
|
| [[thread_index_in_threadgroup]]
|
Function Mappings
| HLSL | MSL | Notes |
|---|
| | 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.read(coord.xy, coord.z)
| Split coord |
| | Operator |
| | Same |
| | Different name |
| | Different name |
| | Different name |
| | Different name |
| if (x < 0) discard_fragment()
| Manual |
| | 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 |
|---|
| Output metallib path |
| Target GPU family |
--minimum-os-build-version
| Minimum OS version |
| Separate vertex fetch function |
| 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
Resource Creation
| OpenGL | Metal |
|---|
| + | device.makeBuffer(bytes:length:options:)
|
| + | device.makeTexture(descriptor:)
+ texture.replace(region:...)
|
| |
| |
| + | Build-time compilation → |
| + | MTLRenderPipelineDescriptor
→ |
State Management
| OpenGL | Metal |
|---|
| MTLDepthStencilDescriptor
→ |
| descriptor.depthCompareFunction = .less
|
| pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
|
| , destinationRGBBlendFactor
|
| encoder.setCullMode(.back)
|
| encoder.setFrontFacing(.counterClockwise)
|
| encoder.setViewport(MTLViewport(...))
|
| 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:)
|
| encoder.drawPrimitives(type:vertexStart:vertexCount:instanceCount:)
|
| encoder.drawIndexedPrimitives(...instanceCount:)
|
Primitive Types
| OpenGL | Metal |
|---|
| |
| |
| |
| |
| |
| 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 |
|---|
| Read/Write | Read/Write | Small dynamic data, uniforms |
| None | Read/Write | Static assets, render targets |
| (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