Use when porting OpenGL/DirectX to Metal - translation layer vs native rewrite decisions, migration planning, anti-patterns
/plugin marketplace add CharlesWiltgen/Axiom/plugin install axiom@axiom-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Porting OpenGL/OpenGL ES or DirectX code to Metal on Apple platforms.
Use this skill when:
❌ "Just use MetalANGLE and ship" — Translation layers add 10-30% overhead; fine for demos, not production
❌ "Convert shaders one-by-one without planning" — State management differs fundamentally; you'll rewrite twice
❌ "Keep the GL state machine mental model" — Metal is explicit; thinking GL causes subtle bugs
❌ "Port everything at once" — Phased migration catches issues early; big-bang migrations hide compounding bugs
❌ "Skip validation layer during development" — Metal validation catches 80% of porting bugs with clear messages
❌ "Worry about coordinate systems later" — Y-flip and NDC differences cause the most debugging time
❌ "Performance will be the same or better automatically" — Metal requires explicit optimization; naive ports can be slower
Starting a port to Metal?
│
├─ Need working demo in <1 week?
│ ├─ OpenGL ES source? → MetalANGLE (translation layer)
│ │ └─ Caveats: 10-30% overhead, ES 2/3 only, no compute
│ │
│ └─ Vulkan available? → MoltenVK
│ └─ Caveats: Vulkan complexity, indirect translation
│
├─ Production app with performance requirements?
│ └─ Native Metal rewrite (recommended)
│ ├─ Phased: Keep GL for reference, port module-by-module
│ └─ Full: Clean rewrite using Metal idioms from start
│
├─ DirectX/HLSL source?
│ └─ Metal Shader Converter (Apple tool)
│ └─ Converts DXIL bytecode → Metal library
│ └─ See metal-migration-ref for usage
│
└─ Hybrid approach?
└─ MetalANGLE for demo → Native Metal incrementally
└─ Best of both: fast validation, optimal end state
When to use: Validate feasibility, get stakeholder buy-in, prototype
// 1. Add MetalANGLE via SPM or CocoaPods
// GitHub: nicklockwood/MetalANGLE
// 2. Replace EAGLContext with MGLContext
import MetalANGLE
let context = MGLContext(api: kMGLRenderingAPIOpenGLES3)
MGLContext.setCurrent(context)
// 3. Replace GLKView with MGLKView
let glView = MGLKView(frame: bounds, context: context)
glView.delegate = self
glView.drawableDepthFormat = .format24
// 4. Existing GL code works unchanged
glClearColor(0, 0, 0, 1)
glClear(GL_COLOR_BUFFER_BIT)
// ... your existing GL rendering code
| Aspect | MetalANGLE | Native Metal |
|---|---|---|
| Time to demo | Hours | Days-weeks |
| Runtime overhead | 10-30% | Baseline |
| Shader changes | None | Full rewrite |
| Compute shaders | Not supported | Full support |
| Future-proof | Translation debt | Apple-recommended |
| Debugging | GL tools only | GPU Frame Capture |
| Thermal/battery | Higher | Optimizable |
MetalANGLE will NOT work if your code:
When to use: Production apps, performance-critical rendering, long-term maintenance
Phase 1: Abstraction Layer (1-2 weeks)
├─ Create renderer interface hiding GL/Metal specifics
├─ Keep GL implementation as reference
├─ Define clear boundaries: setup, resources, draw, present
└─ Validate abstraction with existing tests
Phase 2: Metal Backend (2-4 weeks)
├─ Implement Metal renderer behind same interface
├─ Convert shaders GLSL → MSL (use metal-migration-ref)
├─ Run GL and Metal side-by-side for visual diff
├─ GPU Frame Capture for debugging
└─ Milestone: Feature parity, visual match
Phase 3: Optimization (1-2 weeks)
├─ Remove abstraction overhead where it hurts
├─ Use Metal-specific features (argument buffers, indirect)
├─ Profile with Metal System Trace
├─ Tune for thermal envelope and battery
└─ Remove GL backend entirely
| Concept | OpenGL | Metal |
|---|---|---|
| State model | Implicit, mutable | Explicit, immutable PSO |
| Validation | At draw time | At PSO creation |
| Shader compilation | Runtime (JIT) | Build time (AOT) |
| Command submission | Implicit | Explicit command buffers |
| Resource binding | Global state | Per-encoder binding |
| Synchronization | Driver-managed | App-managed |
import MetalKit
class MetalRenderer: NSObject, MTKViewDelegate {
let device: MTLDevice
let commandQueue: MTLCommandQueue
var pipelineState: MTLRenderPipelineState!
init?(metalView: MTKView) {
guard let device = MTLCreateSystemDefaultDevice(),
let queue = device.makeCommandQueue() else {
return nil
}
self.device = device
self.commandQueue = queue
metalView.device = device
metalView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1)
metalView.depthStencilPixelFormat = .depth32Float
super.init()
metalView.delegate = self
buildPipeline(metalView: metalView)
}
private func buildPipeline(metalView: MTKView) {
let library = device.makeDefaultLibrary()!
let vertexFunction = library.makeFunction(name: "vertexShader")
let fragmentFunction = library.makeFunction(name: "fragmentShader")
let descriptor = MTLRenderPipelineDescriptor()
descriptor.vertexFunction = vertexFunction
descriptor.fragmentFunction = fragmentFunction
descriptor.colorAttachments[0].pixelFormat = metalView.colorPixelFormat
descriptor.depthAttachmentPixelFormat = metalView.depthStencilPixelFormat
// Pre-validated at creation, not at draw time
pipelineState = try! device.makeRenderPipelineState(descriptor: descriptor)
}
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)
// Bind resources explicitly - nothing persists between draws
encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
encoder.setFragmentTexture(texture, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount)
encoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
}
}
❌ BAD — Thinking in GL's implicit state:
// GL mental model: "set state, then draw"
glBindTexture(GL_TEXTURE_2D, texture)
glBindBuffer(GL_ARRAY_BUFFER, vbo)
glUseProgram(program)
glDrawArrays(GL_TRIANGLES, 0, vertexCount)
// State persists until changed — can draw again without rebinding
✅ GOOD — Metal's explicit model:
// Metal: encode everything explicitly per draw
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: rpd)!
encoder.setRenderPipelineState(pipelineState) // Always set
encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) // Always bind
encoder.setFragmentTexture(texture, index: 0) // Always bind
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: count)
encoder.endEncoding()
// Nothing persists — next encoder starts fresh
Time cost: 30-60 min debugging "why did my texture disappear" vs 2 min understanding the model upfront.
❌ BAD — Assuming GL coordinates work in Metal:
OpenGL:
- Origin: bottom-left
- Y-axis: up
- NDC Z range: [-1, 1]
- Texture origin: bottom-left
Metal:
- Origin: top-left
- Y-axis: down
- NDC Z range: [0, 1]
- Texture origin: top-left
✅ GOOD — Explicit coordinate handling:
// Option 1: Flip Y in vertex shader
vertex float4 vertexShader(VertexIn in [[stage_in]]) {
float4 pos = uniforms.mvp * float4(in.position, 1.0);
pos.y = -pos.y; // Flip Y for Metal's coordinate system
return pos;
}
// Option 2: Flip texture coordinates in fragment shader
fragment float4 fragmentShader(VertexOut in [[stage_in]],
texture2d<float> tex [[texture(0)]],
sampler samp [[sampler(0)]]) {
float2 uv = in.texCoord;
uv.y = 1.0 - uv.y; // Flip V for Metal's texture origin
return tex.sample(samp, uv);
}
// Option 3: Use MTKTextureLoader with origin option
let options: [MTKTextureLoader.Option: Any] = [
.origin: MTKTextureLoader.Origin.bottomLeft // Match GL convention
]
let texture = try textureLoader.newTexture(URL: url, options: options)
Time cost: 2-4 hours debugging "upside down" or "mirrored" rendering vs 5 min reading this pattern.
❌ BAD — Disabling validation for "performance":
// No validation — API misuse silently corrupts or crashes later
✅ GOOD — Always enable during development:
In Xcode: Edit Scheme → Run → Diagnostics
✓ Metal API Validation
✓ Metal Shader Validation
✓ GPU Frame Capture (Metal)
Time cost: Hours debugging silent corruption vs immediate error messages with call stacks.
❌ BAD — CPU and GPU fight over same buffer:
// Frame N: CPU writes to buffer
// Frame N: GPU reads from buffer
// Frame N+1: CPU writes again — RACE CONDITION
buffer.contents().copyMemory(from: data, byteCount: size)
✅ GOOD — Triple buffering with semaphore:
class TripleBufferedRenderer {
let inflightSemaphore = DispatchSemaphore(value: 3)
var buffers: [MTLBuffer] = []
var bufferIndex = 0
func draw(in view: MTKView) {
// Wait for a buffer to become available
inflightSemaphore.wait()
let buffer = buffers[bufferIndex]
// Safe to write — GPU finished with this buffer
buffer.contents().copyMemory(from: data, byteCount: size)
let commandBuffer = commandQueue.makeCommandBuffer()!
commandBuffer.addCompletedHandler { [weak self] _ in
self?.inflightSemaphore.signal() // Release buffer
}
// ... encode and commit
bufferIndex = (bufferIndex + 1) % 3
}
}
Time cost: Hours debugging intermittent visual glitches vs 15 min implementing triple buffering.
Situation: Deadline in 2 weeks. MetalANGLE demo works. PM says ship it.
Pressure: "We can optimize later. Users won't notice 20% overhead."
Why this fails:
Response template:
"MetalANGLE is viable for the demo milestone. For production, I recommend a 3-week buffer to implement native Metal for the render loop. This recovers the 20-30% overhead and eliminates deprecation risk. Can we scope the MVP to fewer visual effects to hit the deadline with native Metal?"
Situation: 50 GLSL shaders. Sprint is 2 weeks. Manager wants all converted.
Pressure: "They're just text files. How hard can shader conversion be?"
Why this fails:
Response template:
"Shader conversion requires visual validation, not just compilation. I can convert 10-15 shaders/week with confidence. For 50 shaders: (1) Prioritize by usage — convert the 10 most-used first, (2) Automate mappings — type conversions, boilerplate, (3) Parallel validation — run GL and Metal side-by-side. Realistic timeline: 4-5 weeks for full conversion with quality."
Situation: Developer says "I'll just use print statements to debug shaders."
Pressure: "GPU tools are overkill. I know what I'm doing."
Why this fails:
Response template:
"GPU Frame Capture is the only way to inspect shader variables, see intermediate textures, and understand GPU timing. It takes 30 seconds to capture a frame. Without it, shader debugging is 10x slower — you're guessing instead of observing."
Before starting any port:
After completing the port:
WWDC: 2016-00602, 2018-00604, 2019-00611
Docs: /metal/migrating-opengl-code-to-metal, /metal/shader-converter
Tools: MetalANGLE, MoltenVK
Skills: axiom-metal-migration-ref, axiom-metal-migration-diag
Last Updated: 2025-12-29 Platforms: iOS 12+, macOS 10.14+, tvOS 12+ Status: Production-ready Metal migration patterns
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.