axiom-metal-migration-diag
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-metal-migration-diag
Agent 安装分布
Skill 文档
Metal Migration Diagnostics
Systematic diagnosis for common Metal porting issues.
When to Use This Diagnostic Skill
Use this skill when:
- Screen is black after porting to Metal
- Shaders fail to compile in Metal
- Colors or coordinates are wrong
- Performance is worse than the original
- Rendering artifacts appear
- App crashes during GPU work
Mandatory First Step: Enable Metal Validation
Time cost: 30 seconds setup vs hours of blind debugging
Before ANY debugging, enable Metal validation:
Xcode â Edit Scheme â Run â Diagnostics
â Metal API Validation
â Metal Shader Validation
â GPU Frame Capture (Metal)
Most Metal bugs produce clear validation errors. If you’re debugging without validation enabled, stop and enable it first.
Symptom 1: Black Screen
Decision Tree
Black screen after porting
â
ââ Are there Metal validation errors in console?
â ââ YES â Fix validation errors first (see below)
â
ââ Is the render pass descriptor valid?
â ââ Check: view.currentRenderPassDescriptor != nil
â ââ Check: drawable = view.currentDrawable != nil
â ââ FIX: Ensure MTKView.device is set, view is on screen
â
ââ Is the pipeline state created?
â ââ Check: makeRenderPipelineState doesn't throw
â ââ FIX: Check shader function names match library
â
ââ Are draw calls being issued?
â ââ Add: encoder.label = "Main Pass" for frame capture
â ââ DEBUG: GPU Frame Capture â verify draw calls appear
â
ââ Are resources bound?
â ââ Check: setVertexBuffer, setFragmentTexture called
â ââ FIX: Metal requires explicit binding every frame
â
ââ Is the vertex data correct?
â ââ DEBUG: GPU Frame Capture â inspect vertex buffer
â ââ FIX: Check buffer offsets, vertex count
â
ââ Are coordinates in Metal's range?
â ââ Metal NDC: X [-1,1], Y [-1,1], Z [0,1]
â ââ OpenGL NDC: X [-1,1], Y [-1,1], Z [-1,1]
â ââ FIX: Adjust projection matrix or vertex shader
â
ââ Is clear color set?
ââ Default clear color is (0,0,0,0) â transparent black
ââ FIX: Set view.clearColor or renderPassDescriptor.colorAttachments[0].clearColor
Common Fixes
Missing Drawable:
// BAD: Drawing before view is ready
override func viewDidLoad() {
draw() // metalView.currentDrawable is nil
}
// GOOD: Wait for delegate callback
func draw(in view: MTKView) {
guard let drawable = view.currentDrawable else { return }
// Safe to draw
}
Wrong Function Names:
// BAD: Function name doesn't match .metal file
descriptor.vertexFunction = library.makeFunction(name: "vertexMain")
// .metal file has: vertex VertexOut vertexShader(...)
// GOOD: Names must match exactly
descriptor.vertexFunction = library.makeFunction(name: "vertexShader")
Missing Resource Binding:
// BAD: Assumed state persists like OpenGL
encoder.setRenderPipelineState(pso)
encoder.drawPrimitives(...) // No buffers bound!
// GOOD: Bind everything explicitly
encoder.setRenderPipelineState(pso)
encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
encoder.setVertexBytes(&uniforms, length: uniformsSize, index: 1)
encoder.setFragmentTexture(texture, index: 0)
encoder.drawPrimitives(...)
Time cost: GPU Frame Capture diagnosis: 5-10 min. Guessing without tools: 1-4 hours.
Symptom 2: Shader Compilation Errors
Decision Tree
Shader fails to compile
â
ââ "Use of undeclared identifier"
â ââ Check: #include <metal_stdlib>
â ââ Check: using namespace metal;
â ââ FIX: Standard functions need metal_stdlib
â
ââ "No matching function for call to 'texture'"
â ââ GLSL texture() â MSL tex.sample(sampler, uv)
â FIX: Texture sampling is a method, needs sampler
â
ââ "Invalid type 'vec4'"
â ââ GLSL vec4 â MSL float4
â FIX: See type mapping table in metal-migration-ref
â
ââ "No matching constructor"
â ââ GLSL: vec4(vec3, float) works
â ââ MSL: float4(float3, float) works
â ââ Check: Argument types match exactly
â
ââ "Attribute index out of range"
â ââ Check: [[attribute(N)]] matches vertex descriptor
â ââ FIX: vertexDescriptor.attributes[N] must be configured
â
ââ "Buffer binding index out of range"
â ââ Check: [[buffer(N)]] where N < 31
â ââ FIX: Metal has max 31 buffer bindings per stage
â
ââ "Cannot convert value of type"
ââ MSL is stricter than GLSL about implicit conversions
ââ FIX: Add explicit casts: float(intValue), int(floatValue)
Common Conversions
// GLSL
vec4 color = texture(sampler2D, uv);
// MSL â texture and sampler are separate
float4 color = tex.sample(samp, uv);
// GLSL â mod() for floats
float x = mod(y, z);
// MSL â fmod() for floats
float x = fmod(y, z);
// GLSL â atan(y, x)
float angle = atan(y, x);
// MSL â atan2(y, x)
float angle = atan2(y, x);
// GLSL â inversesqrt
float invSqrt = inversesqrt(x);
// MSL â rsqrt
float invSqrt = rsqrt(x);
Time cost: With conversion table: 2-5 min per shader. Without: 15-30 min per shader.
Symptom 3: Wrong Colors or Coordinates
Decision Tree
Rendering looks wrong
â
ââ Image is upside down
â ââ Cause: Metal Y-axis is opposite OpenGL
â ââ FIX (vertex shader): pos.y = -pos.y
â ââ FIX (texture load): MTKTextureLoader .origin: .bottomLeft
â ââ FIX (UV): uv.y = 1.0 - uv.y in fragment shader
â
ââ Image is mirrored
â ââ Cause: Winding order or cull mode wrong
â ââ FIX: encoder.setFrontFacing(.counterClockwise)
â ââ FIX: encoder.setCullMode(.back) or .none to test
â
ââ Colors are swapped (red/blue)
â ââ Cause: Pixel format mismatch
â ââ Check: .bgra8Unorm vs .rgba8Unorm
â ââ FIX: Match texture pixel format to data format
â
ââ Colors are washed out / too bright
â ââ Cause: sRGB vs linear color space
â ââ Check: Using .bgra8Unorm_srgb for sRGB textures?
â ââ FIX: Use _srgb format variants for gamma-correct rendering
â
ââ Depth fighting / z-fighting
â ââ Cause: NDC Z range difference
â ââ OpenGL: Z in [-1, 1]
â ââ Metal: Z in [0, 1]
â ââ FIX: Adjust projection matrix for Metal's Z range
â
ââ Objects clipped incorrectly
â ââ Cause: Near/far plane or viewport
â ââ Check: Viewport size matches drawable size
â ââ FIX: encoder.setViewport(MTLViewport(...))
â
ââ Transparency wrong
ââ Cause: Blend state not configured
ââ FIX: pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
ââ FIX: Set sourceRGBBlendFactor, destinationRGBBlendFactor
Coordinate System Fix
// Fix projection matrix for Metal's Z range [0, 1]
func metalPerspectiveProjection(fovY: Float, aspect: Float, near: Float, far: Float) -> simd_float4x4 {
let yScale = 1.0 / tan(fovY * 0.5)
let xScale = yScale / aspect
let zRange = far - near
return simd_float4x4(rows: [
SIMD4<Float>(xScale, 0, 0, 0),
SIMD4<Float>(0, yScale, 0, 0),
SIMD4<Float>(0, 0, far / zRange, 1), // Metal: [0, 1]
SIMD4<Float>(0, 0, -near * far / zRange, 0)
])
}
Time cost: With GPU Frame Capture texture inspection: 5-10 min. Without: 1-2 hours.
Symptom 4: Performance Regression
Decision Tree
Performance worse than OpenGL
â
ââ Enabling validation?
â ââ Validation adds ~30% overhead
â FIX: Disable for release builds, keep for debug
â
ââ Creating resources every frame?
â ââ BAD: device.makeBuffer() in draw()
â ââ FIX: Create buffers once, reuse with triple buffering
â
ââ Creating pipeline state every frame?
â ââ BAD: makeRenderPipelineState() in draw()
â ââ FIX: Create PSO once at init, store as property
â
ââ Too many draw calls?
â ââ DEBUG: GPU Frame Capture â count draw calls
â ââ FIX: Batch geometry, use instancing, indirect draws
â
ââ GPU-CPU sync stalls?
â ââ DEBUG: Metal System Trace â look for stalls
â ââ Cause: waitUntilCompleted() blocks CPU
â ââ FIX: Triple buffering with semaphore
â
ââ Inefficient buffer updates?
â ââ BAD: Recreating buffer to update
â ââ FIX: buffer.contents().copyMemory() for dynamic data
â
ââ Wrong storage mode?
â ââ .shared: Good for small dynamic data
â ââ .private: Good for static GPU-only data
â ââ FIX: Use .private for geometry that doesn't change
â
ââ Missing Metal-specific optimizations?
ââ Argument buffers reduce binding overhead
ââ Indirect draws reduce CPU work
ââ See WWDC sessions on Metal optimization
Triple Buffering Pattern
class TripleBufferedRenderer {
static let maxInflightFrames = 3
let inflightSemaphore = DispatchSemaphore(value: maxInflightFrames)
var uniformBuffers: [MTLBuffer] = []
var currentBufferIndex = 0
init(device: MTLDevice) {
for _ in 0..<Self.maxInflightFrames {
let buffer = device.makeBuffer(length: uniformsSize, options: .storageModeShared)!
uniformBuffers.append(buffer)
}
}
func draw(in view: MTKView) {
// Wait for a buffer to be available
inflightSemaphore.wait()
let buffer = uniformBuffers[currentBufferIndex]
// Safe to write â GPU is done with this buffer
memcpy(buffer.contents(), &uniforms, uniformsSize)
let commandBuffer = commandQueue.makeCommandBuffer()!
// Signal when GPU is done
commandBuffer.addCompletedHandler { [weak self] _ in
self?.inflightSemaphore.signal()
}
// ... encode and commit
currentBufferIndex = (currentBufferIndex + 1) % Self.maxInflightFrames
}
}
Time cost: Metal System Trace diagnosis: 15-30 min. Guessing: hours.
Symptom 5: Crashes During GPU Work
Decision Tree
App crashes during rendering
â
ââ EXC_BAD_ACCESS in Metal framework
â ââ Cause: Accessing released resource
â ââ Check: Buffer/texture retained during GPU use
â ââ FIX: Keep strong references until command buffer completes
â
ââ "Execution of the command buffer was aborted"
â ââ Cause: GPU timeout (>10 sec on iOS)
â ââ Check: Infinite loop in shader?
â ââ FIX: Add early exit conditions, reduce work
â
ââ "-[MTLDebugRenderCommandEncoder validateDrawCallWithArray:...]"
â ââ Cause: Validation caught misuse
â ââ FIX: Read the validation message â it tells you exactly what's wrong
â
ââ "Fragment shader writes to non-existent render target"
â ââ Cause: Shader returns color but no color attachment
â ââ FIX: Configure colorAttachments[0].pixelFormat
â
ââ Crash in shader (SIGABRT)
â ââ Cause: Out-of-bounds buffer access
â ââ DEBUG: Enable shader validation
â ââ FIX: Check array bounds, buffer sizes
â
ââ Device disconnected / GPU restart
ââ Cause: Severe GPU hang
ââ Check: Infinite loop or massive overdraw
ââ FIX: Simplify shader, reduce draw complexity
Resource Lifetime Fix
// BAD: Buffer released before GPU finishes
func draw(in view: MTKView) {
let buffer = device.makeBuffer(...) // Created here
encoder.setVertexBuffer(buffer, ...)
commandBuffer.commit()
// buffer released at end of scope â GPU still using it!
}
// GOOD: Keep reference until completion
class Renderer {
var currentBuffer: MTLBuffer? // Strong reference
func draw(in view: MTKView) {
currentBuffer = device.makeBuffer(...)
encoder.setVertexBuffer(currentBuffer!, ...)
commandBuffer.addCompletedHandler { [weak self] _ in
// Safe to release now
self?.currentBuffer = nil
}
commandBuffer.commit()
}
}
Debugging Tools Quick Reference
GPU Frame Capture
Xcode â Debug â Capture GPU Frame (Cmd+Opt+Shift+G)
Use for:
- Inspecting buffer contents
- Viewing intermediate textures
- Checking draw call sequence
- Debugging shader variable values
- Understanding why something isn’t rendering
Metal System Trace (Instruments)
Instruments â Metal System Trace template
Use for:
- GPU/CPU timeline analysis
- Finding synchronization stalls
- Measuring encoder/buffer overhead
- Identifying bottlenecks
Shader Debugger
GPU Frame Capture â Select draw call â Debug button
Use for:
- Step through shader execution
- Inspect variable values per pixel/vertex
- Find logic errors in shaders
Validation Messages
Most validation messages include:
- What went wrong
- Which resource/state
- What the expected value was
Always read the full message â it usually tells you exactly how to fix the problem.
Diagnostic Checklist
When something doesn’t work:
- Metal validation enabled? (Most bugs produce validation errors)
- GPU Frame Capture available? (Visual debugging is fastest)
- Console error messages? (Read them fully)
- Resources bound? (Metal requires explicit binding)
- Coordinates correct? (Y-flip, NDC Z range)
- Pipeline state created successfully? (Check for throw)
- Drawable available? (View must be on screen)
Resources
WWDC: 2019-00611, 2020-10602, 2020-10603
Docs: /metal/debugging-metal-applications, /metal/gpu-capture
Skills: axiom-metal-migration, axiom-metal-migration-ref
Last Updated: 2025-12-29 Platforms: iOS 12+, macOS 10.14+, tvOS 12+ Status: Comprehensive Metal porting diagnostics