axiom-metal-migration

安装量: 117
排名: #7307

安装

npx skills add https://github.com/charleswiltgen/axiom --skill axiom-metal-migration
Metal Migration
Porting OpenGL/OpenGL ES or DirectX code to Metal on Apple platforms.
When to Use This Skill
Use this skill when:
Porting an OpenGL/OpenGL ES codebase to iOS/macOS
Porting a DirectX codebase to Apple platforms
Deciding between translation layer (MetalANGLE) vs native rewrite
Planning a phased migration strategy
Evaluating effort vs performance tradeoffs
Red Flags
❌ "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
Migration Strategy Decision Tree
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
Pattern 1: Translation Layer (Quick Demo Path)
When to use
Validate feasibility, get stakeholder buy-in, prototype
MetalANGLE Setup (OpenGL ES → Metal)
// 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
Tradeoffs Table
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
When MetalANGLE Fails
MetalANGLE will NOT work if your code:
Uses OpenGL ES extensions not in core ES 2/3
Relies on compute shaders (GL_COMPUTE_SHADER)
Requires precise GL state machine semantics
Needs performance within 10% of native
Targets visionOS (no translation layer support)
Pattern 2: Native Metal Rewrite (Production Path)
When to use
Production apps, performance-critical rendering, long-term maintenance Phased Migration Strategy 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 GLSL to Metal Shading Language (MSL) Conversion GLSL MSL Notes attribute / varying [[stage_in]] struct Vertex attributes via struct uniform [[buffer(N)]] parameter Explicit binding index gl_Position Return float4 from vertex Vertex function return value gl_FragColor Return float4 from fragment Fragment function return value texture2D(tex, uv) tex.sample(sampler, uv) Separate sampler object vec2/3/4 float2/3/4 Type names differ mat4 float4x4 Matrix types differ mix() mix() Same name precision mediump float (not needed) Metal infers precision

version 300 es

include

Different preamble Example conversion: // GLSL vertex shader

version 300 es uniform mat4 u_mvp ; in vec3 a_position ; in vec2 a_texCoord ; out vec2 v_texCoord ; void main ( ) { v_texCoord = a_texCoord ; gl_Position = u_mvp * vec4 ( a_position , 1.0 ) ; } // Equivalent MSL vertex shader

include

using namespace metal; struct VertexIn { float3 position [[attribute(0)]]; float2 texCoord [[attribute(1)]]; }; struct VertexOut { float4 position [[position]]; float2 texCoord; }; struct Uniforms { float4x4 mvp; }; vertex VertexOut vertexShader(VertexIn in [[stage_in]], constant Uniforms &uniforms [[buffer(1)]]) { VertexOut out; out.texCoord = in.texCoord; out.position = uniforms.mvp * float4(in.position, 1.0); return out; } Key differences to watch: GLSL globals → MSL function parameters with [[attribute]] qualifiers Implicit uniform binding → explicit [[buffer(N)]] indices sampler2D combines texture+sampler → Metal separates texture2d and sampler GLSL preprocessor → Metal uses C++

include

and
using namespace metal
Core Architecture Differences
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
MTKView Setup (Native Metal)
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
(
)
}
}
Common Migration Anti-Patterns
Anti-Pattern 1: Keeping GL State Machine Mentality
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.
Anti-Pattern 2: Ignoring Coordinate System Differences
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 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.
Anti-Pattern 3: No Validation Layer During Development
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.
Anti-Pattern 4: Single Buffer Without Synchronization
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.
Pressure Scenarios
Scenario 1: "Just Ship with MetalANGLE"
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
:
Translation overhead compounds with complex scenes (visualizers, games)
No compute shader support limits future features
Technical debt grows — team learns MetalANGLE quirks, not Metal
Apple deprecation risk (OpenGL ES deprecated since iOS 12)
Battery/thermal complaints from users
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?"
Scenario 2: "Port All Shaders This Sprint"
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
:
GLSL → MSL isn't 1:1 (precision qualifiers, built-ins, sampling)
Each shader needs visual validation, not just compilation
Complex shaders need performance profiling
Bugs compound — broken shader A masks broken shader B
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."
Scenario 3: "We Don't Need GPU Frame Capture"
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
:
Print statements don't work in shaders
Visual bugs require seeing intermediate render targets
Performance issues require GPU timeline analysis
Metal validation errors need call stack context
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."
Pre-Migration Checklist
Before starting any port:
Inventory shaders
Count GLSL/HLSL files, complexity (LOC, features used)
Identify extensions
Which GL extensions does the code use? Metal equivalents?
Audit state management
How stateful is the renderer? Global state count?
Check compute usage
Any GL compute shaders? GPGPU? (MetalANGLE won't help)
Profile baseline
FPS, frame time, memory, thermal on reference platform
Define success criteria
Target FPS, memory budget, thermal envelope
Set up A/B testing
Can you run GL and Metal side-by-side for validation?
Enable validation
Metal API Validation, Shader Validation, Frame Capture
Post-Migration Checklist
After completing the port:
Visual parity
Side-by-side screenshots match reference
Performance parity or better
Frame time ≤ GL baseline
No validation errors
Clean run with Metal validation enabled
Thermal acceptable
Device doesn't throttle during normal use
Memory stable
No leaks over extended use
All code paths tested
Edge cases, error states, resize/rotate
Resources
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
返回排行榜