Skillshub axiom-metal-migration-ref
Use when converting shaders or looking up API equivalents - GLSL to MSL, HLSL to MSL, GL/DirectX to Metal mappings, MTKView setup code
git clone https://github.com/ComeOnOliver/skillshub
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/CharlesWiltgen/Axiom/axiom-metal-migration-ref" ~/.claude/skills/comeonoliver-skillshub-axiom-metal-migration-ref && rm -rf "$T"
skills/CharlesWiltgen/Axiom/axiom-metal-migration-ref/SKILL.mdMetal 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 |
| | |
| | |
| | Integer coords |
| , | Separate calls |
| | |
| | |
| | Same |
| | Same |
| | Same |
| | Same |
| | Same |
| | Different name |
| | Same |
| | Different name |
| | Different name |
Shader Structure Conversion
GLSL Vertex Shader:
#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:
#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:
#version 300 es precision highp float; in vec2 vTexCoord; uniform sampler2D uTexture; out vec4 fragColor; void main() { fragColor = texture(uTexture, vTexCoord); }
MSL Fragment Shader:
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 alignmentvec3
: 16 bytesvec4
MSL requires:
: 12 bytes storage, 16-byte alignedfloat3
: 16 bytes storage, 16-byte alignedfloat4
Solution: Use
simd types in Swift for CPU-GPU shared structs:
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):
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 |
|---|---|---|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
Semantic Mappings
| HLSL Semantic | MSL Attribute |
|---|---|
| |
| Return value / |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
Function Mappings
| HLSL | MSL | Notes |
|---|---|---|
| | Lowercase |
| | |
| | |
| | Split coord |
| | Operator |
| | Same |
| | Different name |
| | Different name |
| | Different name |
| | Different name |
| | 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:
# 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 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
| OpenGL | Metal |
|---|---|
| |
| |
| + |
| |
Resource Creation
| OpenGL | Metal |
|---|---|
+ | |
+ | + |
| |
| |
+ | Build-time compilation → |
+ | → |
State Management
| OpenGL | Metal |
|---|---|
| → |
| |
| |
| , |
| |
| |
| |
| |
Draw Commands
| OpenGL | Metal |
|---|---|
| |
| |
| |
| |
Primitive Types
| OpenGL | Metal |
|---|---|
| |
| |
| |
| |
| |
| N/A (decompose to triangles) |
Part 4: Complete Setup Examples
MTKView Setup (Recommended)
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)
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
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() } }
// 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 |
// 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
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