Loading...
Loading...
Compare original and translation side by side
Xcode → Edit Scheme → Run → Diagnostics
✓ Metal API Validation
✓ Metal Shader Validation
✓ GPU Frame Capture (Metal)Xcode → Edit Scheme → Run → Diagnostics
✓ Metal API Validation
✓ Metal Shader Validation
✓ GPU Frame Capture (Metal)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// 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(...)// 错误:视图就绪前进行绘制
override func viewDidLoad() {
draw() // metalView.currentDrawable 为nil
}
// 正确:等待代理回调
func draw(in view: MTKView) {
guard let drawable = view.currentDrawable else { return }
// 可以安全绘制
}// 错误:函数名称与.metal文件中的不匹配
descriptor.vertexFunction = library.makeFunction(name: "vertexMain")
// .metal文件中的内容:vertex VertexOut vertexShader(...)
// 正确:名称必须完全匹配
descriptor.vertexFunction = library.makeFunction(name: "vertexShader")// 错误:假设状态像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(...)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)// 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);// 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);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// 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)
])
}// 针对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)
])
}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优化的会话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
}
}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
}
}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挂起
├─ 检查:是否有无限循环或大量过绘制
└─ 修复:简化着色器,降低绘制复杂度// 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完成前释放缓冲区
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()
}
}Xcode → Debug → Capture GPU Frame (Cmd+Opt+Shift+G)Xcode → Debug → Capture GPU Frame (Cmd+Opt+Shift+G)Instruments → Metal System Trace templateInstruments → Metal System Trace 模板GPU Frame Capture → Select draw call → Debug buttonGPU Frame Capture → 选择绘制调用 → 调试按钮