应用Compute Shader后MTKTexture渲染MTKView的缩放与比例异常问题
我是iOS和Metal的新手,最近尝试用Metal Compute Shader处理摄像头采集的视频帧并展示给用户,但遇到了显示异常的问题:输出画面里有堆叠的处理后帧副本,部分帧被裁切,而且无法完全填满屏幕。
我已经定位到几个可能影响问题的变量:
- 启动的线程组数量
- MTKView的drawableSize
- Metal着色器中的采样ID
我尝试调整这些变量,但没得到理想结果。以下是我的相关代码和当前的问题表现:
初始化MTKView的代码
func initMetalView() { metalView = MTKView(frame: view.frame, device: metalDevice) metalView.delegate = self metalView.framebufferOnly = false metalView.colorPixelFormat = .bgra8Unorm metalView.autoResizeDrawable = false metalView.drawableSize = CGSize(width: 1920, height: 1080) metalView.layer.transform = CATransform3DMakeRotation(CGFloat(Float.pi),0.0,1.0,0.0) view.insertSubview(metalView, at: 0) }
AVCaptureVideoDataOutputSampleBufferDelegate扩展(CMSampleBuffer转MTLTexture)
extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate { func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { // sample buffer -> image buffer -> CoreVideo metal texture -> MTL texture guard let cvImageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { fatalError("can't get image buffer") } var textureCache: CVMetalTextureCache? guard CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, metalDevice, nil, &textureCache) == kCVReturnSuccess else { fatalError("cant create texture cache") } let width = CVPixelBufferGetWidth(cvImageBuffer) let height = CVPixelBufferGetHeight(cvImageBuffer) var imageTexture: CVMetalTexture? let result = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache!, cvImageBuffer, nil, MTLPixelFormat.bgra8Unorm, width, height, 0, &imageTexture) guard let unwrappedImageTexture = imageTexture, result == kCVReturnSuccess else { fatalError("failed to create texture from image") } inputTexture = CVMetalTextureGetTexture(unwrappedImageTexture) } }
MTKViewDelegate扩展(应用着色器并显示)
extension ViewController: MTKViewDelegate { func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { } func draw(in view: MTKView) { guard let inputTexture = inputTexture, let commandQueue = commandQueue, let commandBuffer = commandQueue.makeCommandBuffer(), let encoder = commandBuffer.makeComputeCommandEncoder(), let pipelineState = pipelineState else { return } encoder.setComputePipelineState(pipelineState) encoder.setTextures([metalView.currentDrawable!.texture, inputTexture], range: 0..<2) encoder.dispatchThreadgroups(MTLSizeMake(inputTexture.width/16, inputTexture.height/16, 1), threadsPerThreadgroup: threadsPerBlock) // inputTexture w:1920, h:1080 encoder.endEncoding() commandBuffer.present(metalView.currentDrawable!) commandBuffer.commit() } }
Metal Compute Shader代码
#include <metal_stdlib> using namespace metal; kernel void blacky (texture2d<float, access::write> outTexture [[texture(0)]], texture2d<float, access::read> inTexture [[texture(1)]], uint2 id [[thread_position_in_grid]]) { uint2 flipped_id = uint2(id.y, id.x); float3 val = inTexture.read(flipped_id).rgb; float g = (val.r + val.g + val.b)/3.0; float4 out = float4(g, g, g, 1); outTexture.write(out.rgba, id); }
当前问题表现
输出画面存在堆叠的处理后帧副本,部分帧被裁切且无法完全填满屏幕。
问题分析与解决方案
作为Metal开发的老手,我帮你梳理下几个关键问题点,以及对应的修复步骤:
1. 线程组调度不匹配,导致部分像素未被覆盖
你的线程组计算是inputTexture.width/16和inputTexture.height/16,但如果纹理的宽高不是16的整数倍,会有边缘像素没有被处理到。正确的做法是向上取整,确保所有像素都被覆盖:
let threadgroupWidth = 16 let threadgroupHeight = 16 let threadgroupsPerGrid = MTLSize( width: (inputTexture.width + threadgroupWidth - 1) / threadgroupWidth, height: (inputTexture.height + threadgroupHeight - 1) / threadgroupHeight, depth: 1 ) encoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: MTLSizeMake(threadgroupWidth, threadgroupHeight, 1))
同时要确保threadsPerBlock是MTLSize(width:16, height:16, depth:1),和这里的计算对应。
2. 纹理坐标翻转与尺寸不匹配问题
你的着色器里用了uint2 flipped_id = uint2(id.y, id.x);,这相当于把纹理的宽高互换了,但摄像头的输入纹理尺寸是1920x1080,而MTKView的drawableSize也是1920x1080,这样翻转后会导致画面拉伸、裁切,因为原本的宽高比被破坏了。
另外,摄像头采集的帧通常是上下颠倒的,你需要修正坐标而非互换宽高:
// 垂直翻转坐标,适配摄像头输出的方向 uint2 texCoord = uint2(id.x, inTexture.get_height() - 1 - id.y); float3 val = inTexture.read(texCoord).rgb;
同时在captureOutput方法里设置摄像头连接的方向,匹配UI方向:
connection.videoOrientation = .portrait // 根据你的设备实际方向调整
3. MTKView的drawableSize与实际帧尺寸不匹配
你硬编码了drawableSize = CGSize(width: 1920, height: 1080),但如果设备屏幕的尺寸或缩放因子不同,会导致画面无法填满屏幕。应该让drawableSize匹配MTKView的实际尺寸(考虑Retina屏的缩放):
metalView.autoResizeDrawable = true // 打开自动调整,让drawableSize跟随view尺寸变化 // 或者手动设置(适合固定尺寸场景): metalView.drawableSize = metalView.bounds.size.applying(CGAffineTransform(scaleX: UIScreen.main.scale, y: UIScreen.main.scale))
4. 纹理缓存重复创建的问题
在captureOutput方法里,你每次都创建新的CVMetalTextureCache,这不仅低效,还可能导致纹理管理异常。应该把textureCache作为类的属性初始化一次:
class ViewController: UIViewController { var textureCache: CVMetalTextureCache! // ...其他属性 override func viewDidLoad() { super.viewDidLoad() // 初始化textureCache guard CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, metalDevice, nil, &textureCache) == kCVReturnSuccess else { fatalError("Failed to create texture cache") } // ...其他初始化逻辑 } }
然后在captureOutput里直接使用这个属性即可。
5. 线程安全问题
inputTexture被多个线程访问(摄像头回调在后台线程,draw(in:)在主线程),需要添加线程同步,确保赋值操作在主线程执行:
// 在captureOutput里赋值时切换到主线程 DispatchQueue.main.async { self.inputTexture = CVMetalTextureGetTexture(unwrappedImageTexture) }
按照这些步骤修复后,应该能解决画面堆叠、裁切和无法填满屏幕的问题。
内容的提问来源于stack exchange,提问作者itsmeavinash

