Swift Metal计算着色器混合异常:高斯纹理仅显示左上象限
问题原因与修复方案
核心问题分析
你遇到的模糊纹理仅显示左上象限的问题,根源在于线程调度用了视图逻辑尺寸而非纹理实际像素尺寸,以及直接读写同一纹理的不安全操作:
- 线程数量不足:iPhone XR是Retina屏幕(缩放因子2),
view.bounds.size是逻辑坐标,而drawable.texture是实际像素尺寸(比如XR的纹理尺寸为828x1792,逻辑尺寸仅414x896)。用逻辑尺寸计算线程组数,导致仅调度了1/4的线程,只处理了纹理左上1/4区域。 - 纹理读写冲突:你将
drawable.texture同时绑定为输入和输出,Metal不允许在同一命令缓冲内对同一纹理进行读写,会引发未定义行为。
修复步骤
1. 修正线程调度逻辑
始终基于drawable.texture的实际像素尺寸计算线程数,替换原代码中view.bounds的相关逻辑:
// 替换原else分支的线程调度代码 let textureWidth = drawable.texture.width let textureHeight = drawable.texture.height // 使用8x8的线程组(比1x1效率更高) let threadGroupSize = MTLSize(width: 8, height: 8, depth: 1) // 计算线程组数,确保覆盖整个纹理(避免尺寸无法整除的情况) let numGroups = MTLSize( width: (textureWidth + threadGroupSize.width - 1) / threadGroupSize.width, height: (textureHeight + threadGroupSize.height - 1) / threadGroupSize.height, depth: 1 ) computeEncoder?.dispatchThreadgroups(numGroups, threadsPerThreadgroup: threadGroupSize)
或者用更简洁的dispatchThreads方式:
let threadsPerGroup = MTLSize(width: 8, height: 8, depth: 1) let threadsPerGrid = MTLSize(width: textureWidth, height: textureHeight, depth: 1) computeEncoder?.dispatchThreads(threadsPerGrid, threadsPerThreadgroup: threadsPerGroup)
2. 避免纹理读写冲突
创建临时纹理作为混合输出,不要直接修改drawable纹理:
// 混合前创建临时纹理 let descriptor = MTLTextureDescriptor() descriptor.width = drawable.texture.width descriptor.height = drawable.texture.height descriptor.pixelFormat = drawable.texture.pixelFormat descriptor.usage = [.shaderRead, .shaderWrite] guard let tempTexture = device.makeTexture(descriptor: descriptor) else { fatalError("无法创建临时纹理") } // 绑定纹理时用临时纹理作为输出 computeEncoder?.setTexture(tempTexture, index: 2) // 混合完成后,将临时纹理内容拷贝到drawable纹理 let blitEncoder = commandBuffer.makeBlitCommandEncoder() blitEncoder?.copy(from: tempTexture, to: drawable.texture) blitEncoder?.endEncoding()
3. 验证纹理尺寸一致性
确保模糊纹理self.debugImageA与drawable纹理尺寸完全匹配,可在绑定纹理前打印验证:
print("Drawable纹理尺寸: \(drawable.texture.width)x\(drawable.texture.height)") print("模糊纹理尺寸: \(self.debugImageA.width)x\(self.debugImageA.height)")
修正后的完整代码片段
// 完成3D场景渲染 commandEncoder.endEncoding() print("Before Texture: \(drawable.texture.width) \(drawable.texture.height)") print("模糊纹理尺寸: \(self.debugImageA.width) \(self.debugImageA.height)") // 创建临时纹理避免读写冲突 let descriptor = MTLTextureDescriptor() descriptor.width = drawable.texture.width descriptor.height = drawable.texture.height descriptor.pixelFormat = drawable.texture.pixelFormat descriptor.usage = [.shaderRead, .shaderWrite] guard let tempTexture = device.makeTexture(descriptor: descriptor) else { fatalError("无法创建临时纹理") } // 创建并配置计算着色器 guard let computeFunction = self.computeLibrary.makeFunction(name: "ComputeBlender"), let computePipelineState = try? device.makeComputePipelineState(function: computeFunction), let computeEncoder = commandBuffer.makeComputeCommandEncoder() else { fatalError("计算管线初始化失败") } computeEncoder.setComputePipelineState(computePipelineState) computeEncoder.setTexture(drawable.texture, index: 0) computeEncoder.setTexture(self.debugImageA, index: 1) computeEncoder.setTexture(tempTexture, index: 2) // 基于实际纹理尺寸调度线程 let textureWidth = drawable.texture.width let textureHeight = drawable.texture.height let threadGroupSize = MTLSize(width: 8, height: 8, depth: 1) let numGroups = MTLSize( width: (textureWidth + threadGroupSize.width - 1) / threadGroupSize.width, height: (textureHeight + threadGroupSize.height - 1) / threadGroupSize.height, depth: 1 ) computeEncoder.dispatchThreadgroups(numGroups, threadsPerThreadgroup: threadGroupSize) computeEncoder.endEncoding() // 将临时纹理内容拷贝到drawable if let blitEncoder = commandBuffer.makeBlitCommandEncoder() { blitEncoder.copy(from: tempTexture, to: drawable.texture) blitEncoder.endEncoding() } // 捕获调试纹理 self.debugImageB = drawable.texture // 呈现最终画面 commandBuffer.present(drawable) commandBuffer.commit() commandBuffer.waitUntilCompleted() print("After Texture: \(self.debugImageB.width) \(self.debugImageB.height)")
着色器说明
如果不需要颜色通道交换效果,可去掉着色器中result_color = float4(result_color.g, result_color.b, result_color.r, 1.0);这一行。
内容的提问来源于stack exchange,提问作者PhilBot
相关产品推荐
相关产品推荐

