axiom-metal-migration-diag

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Metal Migration Diagnostics

Metal移植诊断

Systematic diagnosis for common Metal porting issues.
针对常见Metal移植问题的系统化诊断方案。

When to Use This Diagnostic Skill

何时使用该诊断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
在以下场景使用该Skill:
  • 移植到Metal后出现黑屏
  • Metal环境下着色器编译失败
  • 颜色或坐标显示异常
  • 性能比原版本差
  • 出现渲染瑕疵
  • GPU工作期间应用崩溃

Mandatory First Step: Enable Metal Validation

强制第一步:启用Metal验证

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.
时间成本:30秒设置 vs 数小时盲调
在进行任何调试之前,请启用Metal验证:
Xcode → Edit Scheme → Run → Diagnostics
✓ Metal API Validation
✓ Metal Shader Validation
✓ GPU Frame Capture (Metal)
大多数Metal bug会产生明确的验证错误。如果您在未启用验证的情况下调试,请立即停止并先启用它

Symptom 1: Black Screen

症状1:黑屏

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
移植后出现黑屏
├─ 控制台中是否有Metal验证错误?
│   └─ 是 → 优先修复验证错误(见下文)
├─ 渲染通道描述符是否有效?
│   ├─ 检查:view.currentRenderPassDescriptor != nil
│   ├─ 检查:drawable = view.currentDrawable != nil
│   └─ 修复:确保MTKView.device已设置,视图处于显示状态
├─ 管线状态是否已创建?
│   ├─ 检查:makeRenderPipelineState未抛出异常
│   └─ 修复:检查着色器函数名称与库中的名称匹配
├─ 是否已发起绘制调用?
│   ├─ 添加:encoder.label = "Main Pass" 以进行帧捕获
│   └─ 调试:通过GPU Frame Capture → 验证绘制调用是否存在
├─ 资源是否已绑定?
│   ├─ 检查:是否调用了setVertexBuffer、setFragmentTexture
│   └─ 修复:Metal要求每帧都显式绑定资源
├─ 顶点数据是否正确?
│   ├─ 调试:通过GPU Frame Capture → 检查顶点缓冲区
│   └─ 修复:检查缓冲区偏移量、顶点数量
├─ 坐标是否在Metal的范围内?
│   ├─ Metal NDC:X [-1,1], Y [-1,1], Z [0,1]
│   ├─ OpenGL NDC:X [-1,1], Y [-1,1], Z [-1,1]
│   └─ 修复:调整投影矩阵或顶点着色器
└─ 是否设置了清除颜色?
    ├─ 默认清除颜色为(0,0,0,0) —— 透明黑色
    └─ 修复:设置view.clearColor或renderPassDescriptor.colorAttachments[0].clearColor

Common Fixes

常见修复方案

Missing Drawable:
swift
// 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:
swift
// 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:
swift
// 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.
缺少Drawable:
swift
// 错误:视图就绪前进行绘制
override func viewDidLoad() {
    draw()  // metalView.currentDrawable 为nil
}

// 正确:等待代理回调
func draw(in view: MTKView) {
    guard let drawable = view.currentDrawable else { return }
    // 可以安全绘制
}
函数名称不匹配:
swift
// 错误:函数名称与.metal文件中的不匹配
descriptor.vertexFunction = library.makeFunction(name: "vertexMain")
// .metal文件中的内容:vertex VertexOut vertexShader(...)

// 正确:名称必须完全匹配
descriptor.vertexFunction = library.makeFunction(name: "vertexShader")
缺少资源绑定:
swift
// 错误:假设状态像OpenGL一样持续存在
encoder.setRenderPipelineState(pso)
encoder.drawPrimitives(...)  // 未绑定任何缓冲区!

// 正确:显式绑定所有资源
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 Frame Capture诊断:5-10分钟。无工具盲猜:1-4小时。

Symptom 2: Shader Compilation Errors

症状2:着色器编译错误

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)
着色器编译失败
├─ "Use of undeclared identifier"
│   ├─ 检查:是否包含#include <metal_stdlib>
│   ├─ 检查:是否添加using namespace metal;
│   └─ 修复:标准函数需要metal_stdlib
├─ "No matching function for call to 'texture'"
│   └─ GLSL的texture() → MSL的tex.sample(sampler, uv)
│       修复:纹理采样是方法,需要采样器
├─ "Invalid type 'vec4'"
│   └─ GLSL的vec4 → MSL的float4
│       修复:参考metal-migration-ref中的类型映射表
├─ "No matching constructor"
│   ├─ GLSL:vec4(vec3, float) 可用
│   ├─ MSL:float4(float3, float) 可用
│   └─ 检查:参数类型完全匹配
├─ "Attribute index out of range"
│   ├─ 检查:[[attribute(N)]] 与顶点描述符匹配
│   └─ 修复:必须配置vertexDescriptor.attributes[N]
├─ "Buffer binding index out of range"
│   ├─ 检查:[[buffer(N)]] 中的N < 31
│   └─ 修复:Metal每个阶段最多支持31个缓冲区绑定
└─ "Cannot convert value of type"
    ├─ MSL在隐式转换方面比GLSL更严格
    └─ 修复:添加显式转换:float(intValue), int(floatValue)

Common Conversions

常见转换示例

metal
// 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.
metal
// GLSL
vec4 color = texture(sampler2D, uv);

// MSL —— 纹理和采样器是分离的
float4 color = tex.sample(samp, uv);

// GLSL —— 浮点数的mod()
float x = mod(y, z);

// MSL —— 浮点数的fmod()
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分钟。

Symptom 3: Wrong Colors or Coordinates

症状3:颜色或坐标异常

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
渲染效果异常
├─ 图像倒置
│   ├─ 原因:Metal的Y轴与OpenGL相反
│   ├─ 修复(顶点着色器):pos.y = -pos.y
│   ├─ 修复(纹理加载):MTKTextureLoader的.origin: .bottomLeft
│   └─ 修复(UV坐标):在片段着色器中设置uv.y = 1.0 - uv.y
├─ 图像镜像
│   ├─ 原因:缠绕顺序或剔除模式错误
│   ├─ 修复:encoder.setFrontFacing(.counterClockwise)
│   └─ 修复:设置encoder.setCullMode(.back) 或 .none 进行测试
├─ 颜色交换(红/蓝)
│   ├─ 原因:像素格式不匹配
│   ├─ 检查:.bgra8Unorm vs .rgba8Unorm
│   └─ 修复:使纹理像素格式与数据格式匹配
├─ 颜色褪色/过亮
│   ├─ 原因:sRGB与线性色彩空间不匹配
│   ├─ 检查:是否为sRGB纹理使用.bgra8Unorm_srgb?
│   └─ 修复:使用_srgb格式变体以实现伽马校正渲染
├─ 深度冲突/Z-fighting
│   ├─ 原因:NDC Z范围不同
│   ├─ OpenGL:Z ∈ [-1, 1]
│   ├─ Metal:Z ∈ [0, 1]
│   └─ 修复:调整投影矩阵以适配Metal的Z范围
├─ 对象裁剪异常
│   ├─ 原因:近/远平面或视口设置错误
│   ├─ 检查:视口大小与Drawable大小匹配
│   └─ 修复:调用encoder.setViewport(MTLViewport(...))
└─ 透明度异常
    ├─ 原因:混合状态未配置
    ├─ 修复:pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
    └─ 修复:设置sourceRGBBlendFactor、destinationRGBBlendFactor

Coordinate System Fix

坐标系修复示例

swift
// 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.
swift
// 针对Metal的Z范围[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 Frame Capture检查纹理:5-10分钟。无工具:1-2小时。

Symptom 4: Performance Regression

症状4:性能退化

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
性能比OpenGL差
├─ 是否启用了验证?
│   └─ 验证会增加约30%的开销
│       修复:发布版本禁用验证,调试版本保留
├─ 是否每帧都创建资源?
│   ├─ 错误:在draw()中调用device.makeBuffer()
│   └─ 修复:一次性创建缓冲区,通过三重缓冲复用
├─ 是否每帧都创建管线状态?
│   ├─ 错误:在draw()中调用makeRenderPipelineState()
│   └─ 修复:在初始化时一次性创建PSO,作为属性存储
├─ 绘制调用过多?
│   ├─ 调试:通过GPU Frame Capture → 统计绘制调用数量
│   └─ 修复:批处理几何体,使用实例化、间接绘制
├─ 是否存在GPU-CPU同步停顿?
│   ├─ 调试:通过Metal System Trace → 查找停顿点
│   ├─ 原因:waitUntilCompleted() 阻塞CPU
│   └─ 修复:使用信号量实现三重缓冲
├─ 缓冲区更新效率低下?
│   ├─ 错误:重建缓冲区以更新数据
│   └─ 修复:使用buffer.contents().copyMemory() 更新动态数据
├─ 存储模式错误?
│   ├─ .shared:适合小型动态数据
│   ├─ .private:适合静态GPU专属数据
│   └─ 修复:对不变化的几何体使用.private模式
└─ 缺少Metal特定优化?
    ├─ 参数缓冲区可减少绑定开销
    ├─ 间接绘制可减少CPU工作量
    └─ 参考WWDC中关于Metal优化的会话

Triple Buffering Pattern

三重缓冲模式示例

swift
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.
swift
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) {
        // 等待可用缓冲区
        inflightSemaphore.wait()

        let buffer = uniformBuffers[currentBufferIndex]
        // 可以安全写入 —— GPU已完成该缓冲区的使用
        memcpy(buffer.contents(), &uniforms, uniformsSize)

        let commandBuffer = commandQueue.makeCommandBuffer()!

        // GPU完成时发送信号
        commandBuffer.addCompletedHandler { [weak self] _ in
            self?.inflightSemaphore.signal()
        }

        // ... 编码并提交

        currentBufferIndex = (currentBufferIndex + 1) % Self.maxInflightFrames
    }
}
时间成本:使用Metal System Trace诊断:15-30分钟。盲猜:数小时。

Symptom 5: Crashes During GPU Work

症状5:GPU工作期间崩溃

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
渲染期间应用崩溃
├─ Metal框架中出现EXC_BAD_ACCESS
│   ├─ 原因:访问已释放的资源
│   ├─ 检查:GPU使用期间缓冲区/纹理是否被保留
│   └─ 修复:在命令缓冲区完成前保持强引用
├─ "Execution of the command buffer was aborted"
│   ├─ 原因:GPU超时(iOS上超过10秒)
│   ├─ 检查:着色器中是否有无限循环?
│   └─ 修复:添加提前退出条件,减少工作量
├─ "-[MTLDebugRenderCommandEncoder validateDrawCallWithArray:...]"
│   ├─ 原因:验证捕获到使用错误
│   └─ 修复:阅读验证消息 —— 它会明确告知问题所在
├─ "Fragment shader writes to non-existent render target"
│   ├─ 原因:着色器返回颜色但无颜色附件
│   └─ 修复:配置colorAttachments[0].pixelFormat
├─ 着色器中崩溃(SIGABRT)
│   ├─ 原因:缓冲区越界访问
│   ├─ 调试:启用着色器验证
│   └─ 修复:检查数组边界、缓冲区大小
└─ 设备断开/GPU重启
    ├─ 原因:严重GPU挂起
    ├─ 检查:是否有无限循环或大量过绘制
    └─ 修复:简化着色器,降低绘制复杂度

Resource Lifetime Fix

资源生命周期修复示例

swift
// 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()
    }
}
swift
// 错误:GPU完成前释放缓冲区
func draw(in view: MTKView) {
    let buffer = device.makeBuffer(...) 
    encoder.setVertexBuffer(buffer, ...)
    commandBuffer.commit()
    // 缓冲区在作用域结束时释放 —— GPU仍在使用它!
}

// 正确:保留引用直到完成
class Renderer {
    var currentBuffer: MTLBuffer?  // 强引用

    func draw(in view: MTKView) {
        currentBuffer = device.makeBuffer(...)
        encoder.setVertexBuffer(currentBuffer!, ...)
        commandBuffer.addCompletedHandler { [weak self] _ in
            // 现在可以安全释放
            self?.currentBuffer = nil
        }
        commandBuffer.commit()
    }
}

Debugging Tools Quick Reference

调试工具快速参考

GPU Frame Capture

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
Xcode → Debug → Capture GPU Frame (Cmd+Opt+Shift+G)
适用场景:
  • 检查缓冲区内容
  • 查看中间纹理
  • 检查绘制调用序列
  • 调试着色器变量值
  • 排查渲染失败原因

Metal System Trace (Instruments)

Metal System Trace(Instruments)

Instruments → Metal System Trace template
Use for:
  • GPU/CPU timeline analysis
  • Finding synchronization stalls
  • Measuring encoder/buffer overhead
  • Identifying bottlenecks
Instruments → Metal System Trace 模板
适用场景:
  • GPU/CPU时间线分析
  • 查找同步停顿
  • 测量编码器/缓冲区开销
  • 识别性能瓶颈

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
GPU Frame Capture → 选择绘制调用 → 调试按钮
适用场景:
  • 单步执行着色器
  • 检查每个像素/顶点的变量值
  • 查找着色器中的逻辑错误

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)
出现问题时,请检查:
  • 是否启用了Metal验证?(大多数bug会产生验证错误)
  • 是否可以使用GPU Frame Capture?(可视化调试最快)
  • 控制台是否有错误消息?(完整阅读)
  • 资源是否已绑定?(Metal要求显式绑定)
  • 坐标是否正确?(Y轴翻转、NDC Z范围)
  • 管线状态是否创建成功?(检查是否抛出异常)
  • Drawable是否可用?(视图必须在屏幕上)

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
WWDC:2019-00611, 2020-10602, 2020-10603
文档:/metal/debugging-metal-applications, /metal/gpu-capture
Skills:axiom-metal-migration, axiom-metal-migration-ref

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