mcpField
A
axiom-metal-migration-diag

axiom-metal-migration-diag

github.com/charleswiltgen开发1.2k163 次安装今天更新
代码仓库

关于 axiom-metal-migration-diag

用于诊断图形代码迁移到 Metal 后常见问题的开发技能,按症状组织排查方法,涵盖验证、着色器转换、GPU 捕获、资源绑定和性能分析。

主要功能

  1. 01启用 Metal API 和着色器验证
  2. 02诊断黑屏与渲染瑕疵
  3. 03查阅 GLSL 到 MSL 的着色器转换
  4. 04检查 GPU 帧和着色器执行
  5. 05分析 GPU 与 CPU 性能

适用场景

  1. 01调查迁移到 Metal 后出现的黑屏
  2. 02解决着色器编译错误
  3. 03修正颜色或坐标异常
  4. 04追踪 GPU 工作期间的崩溃与性能退化

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-metal-migration-diag

安装后在 Claude Code 中输入/axiom-metal-migration-diag确认已加载。

详细介绍

Metal 迁移诊断

系统地诊断常见的 Metal 移植问题。

何时使用此诊断技能

出现以下情况时,请使用此技能:

  • 移植到 Metal 后屏幕变黑
  • 着色器无法在 Metal 中编译
  • 颜色或坐标不正确
  • 性能比原来更差
  • 出现渲染瑕疵
  • App 在执行 GPU 工作时崩溃

必须先执行的步骤:启用 Metal 验证

耗时:设置需 30 秒,避免数小时盲目调试

开始任何调试之前,请启用 Metal 验证:

Xcode → Edit Scheme → Run → Diagnostics
✓ Metal API Validation
✓ Metal Shader Validation
✓ GPU Frame Capture (Metal)

大多数 Metal 错误都会产生清晰的验证错误。如果调试时尚未启用验证,请停止调试并先启用验证。

症状 1:黑屏

决策树
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
常见修复方法

缺少 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
}

函数名称错误:

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

缺少资源绑定:

// 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(...)

耗时:使用 GPU 帧捕获进行诊断需 5-10 分钟。没有工具而靠猜测需 1-4 小时。

症状 2:着色器编译错误

决策树
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)
常见转换
// 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);

耗时:使用转换表时,每个着色器需 2-5 分钟。否则,每个着色器需 15-30 分钟。

症状 3:颜色或坐标不正确

决策树
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
坐标系修复方法
// 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)
    ])
}

耗时:使用 GPU 帧捕获检查纹理需 5-10 分钟。否则需 1-2 小时。

症状 4:性能下降

决策树
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
三重缓冲模式
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
    }
}

耗时:使用 Metal System Trace 诊断需 15-30 分钟。靠猜测则需数小时。

症状 5:执行 GPU 工作时崩溃

决策树
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
资源生命周期修复方法
// 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()
    }
}

调试工具快速参考

GPU 帧捕获
Xcode → Debug → Capture GPU Frame (Cmd+Opt+Shift+G)

适用于:

  • 检查缓冲区内容
  • 查看中间纹理
  • 检查绘制调用顺序
  • 调试着色器变量值
  • 了解某项内容为何未渲染
Metal System Trace(Instruments)
Instruments → Metal System Trace template

适用于:

  • 分析 GPU/CPU 时间线
  • 查找同步停顿
  • 测量编码器/缓冲区开销
  • 找出瓶颈
着色器调试器
GPU Frame Capture → Select draw call → Debug button

适用于:

  • 单步执行着色器
  • 检查每个像素/顶点的变量值
  • 查找着色器中的逻辑错误
验证消息

大多数验证消息都会说明:

  • 出了什么问题
  • 涉及哪个资源/状态
  • 预期值是什么

请务必阅读完整消息——它通常会明确告诉你如何修复问题。

诊断清单

遇到问题时:

  • 是否已启用 Metal 验证?(大多数错误都会产生验证错误)
  • 是否可用 GPU 帧捕获?(可视化调试最快)
  • 控制台是否有错误消息?(请完整阅读)
  • 资源是否已绑定?(Metal 要求显式绑定)
  • 坐标是否正确?(Y 轴翻转、NDC Z 范围)
  • 是否已成功创建管线状态?(检查是否抛出错误)
  • Drawable 是否可用?(视图必须显示在屏幕上)

资源

WWDC:2019-00611、2020-10602、2020-10603

文档:/metal/debugging-metal-applications、/metal/gpu-capture

技能:axiom-metal-migration、axiom-metal-migration-ref


最后更新:2025-12-29 平台:iOS 12+、macOS 10.14+、tvOS 12+ 状态:全面的 Metal 移植诊断

评论

支持 Markdown