axiom-metal-migration-ref

Metal Migration Reference

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "axiom-metal-migration-ref" with this command: npx skills add charleswiltgen/axiom/charleswiltgen-axiom-axiom-metal-migration-ref

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:

#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

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:

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

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:

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)

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&#x3C;SIMD3&#x3C;Float>>.stride
    vertexDescriptor.attributes[1].bufferIndex = 0
    vertexDescriptor.layouts[0].stride = MemoryLayout&#x3C;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

.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

// 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

axiom-vision

No summary provided by upstream source.

Repository SourceNeeds Review
General

axiom-swiftdata

No summary provided by upstream source.

Repository SourceNeeds Review
General

axiom-swiftui-26-ref

No summary provided by upstream source.

Repository SourceNeeds Review
General

axiom-swiftui-architecture

No summary provided by upstream source.

Repository SourceNeeds Review