macOS Metal中CAMetalLayer的nextDrawable方法性能异常缓慢
Metal渲染性能瓶颈优化方案
问题背景
基于macOS Metal编写的渲染代码(在@autoreleasepool块中调用),预期可处理数百万三角形,但实际仅支持数千量级。当三角形数量达30000时,nextDrawable调用耗时0.4秒,性能严重不达预期。相关代码如下:
setupEncoder方法
- (void)setupEncoder:(nonnull MTKView *)view { id<MTLCommandBuffer> commandBuffer = nil; id<CAMetalDrawable> drawable = nil; MTLRenderPassDescriptor *renderPass = nil; id<MTLRenderPipelineState> pipeline = nil; MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor; if (renderPassDescriptor == nil) { return; } commandBuffer = [_commandQueue commandBuffer]; // VERY SLOW drawable = [_metalLayer nextDrawable]; // --------- if (drawable == nil) { return; } id<MTLTexture> framebufferTexture = drawable.texture; renderPass = [MTLRenderPassDescriptor renderPassDescriptor]; renderPass.colorAttachments[0].clearColor = self.clearColor; renderPass.colorAttachments[0].storeAction = MTLStoreActionStore; renderPass.colorAttachments[0].loadAction = MTLLoadActionClear; renderPass.colorAttachments[0].texture = framebufferTexture; renderPass.depthAttachment = _depthAttachmentDescriptor; pipeline = [self.device newRenderPipelineStateWithDescriptor:_renderPipelineDescriptor error:NULL]; id<MTLRenderCommandEncoder> commandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPass]; [commandEncoder setRenderPipelineState:pipeline]; // Set up the depth buffer [commandEncoder setDepthStencilState:_depthStencilState]; [commandEncoder setFrontFacingWinding:MTLWindingCounterClockwise]; [commandEncoder setCullMode:MTLCullModeBack]; // Draw root node [self drawKSCNNode:commandEncoder node:scene.rootNode uniforms:uniformSet startTranslation:SCNVector3Make(0, 0, 0) startRotation:SCNVector4Make(0, 1, 0, 0) startScale:SCNVector3Make(1, 1, 1)]; [commandEncoder endEncoding]; // Get the drawable that will be presented at the end of the frame // id<MTLDrawable> drawable = view.currentDrawable; // Request that the drawable texture be presented by the windowing system once drawing is done if (commandBuffer != nil && drawable != nil) { [commandBuffer presentDrawable:drawable]; [commandBuffer commit]; } }
drawKSCNNode方法
- (void)drawKSCNNode:(id<MTLRenderCommandEncoder>)commandEncoder node:(KSCNNode *)node uniforms:(float *)uniforms startTranslation:(SCNVector3)startTranslation startRotation:(SCNVector4)startRotation startScale:(SCNVector3)startScale { if (node.geometry != nil) { if (node.geometry.vertices != nil && node.geometry.colors != nil) { //NSLog(@"DRAWING %@, %ld vertices", node.name, node.geometry.vertexCount); uniformSet[8] = node.position.x; uniformSet[9] = node.position.y; uniformSet[10] = node.position.z; // Update uniforms for this node memcpy(_uniforms.contents, uniformSet, sizeof(float) * 45); [commandEncoder setVertexBuffer:[node.geometry.vertices getBuffer] offset:0 atIndex:0]; [commandEncoder setVertexBuffer:[node.geometry.colors getBuffer] offset:0 atIndex:1]; [commandEncoder setVertexBuffer:_uniforms offset:0 atIndex:2]; [commandEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:node.geometry.vertexCount instanceCount:node.geometry.vertexCount / 3]; } } for (int j = 0; j < [node.children count]; j++) { KSCNNode * child = [node.children objectAtIndex:j]; [self drawKSCNNode:commandEncoder node:child uniforms:uniforms startTranslation:startTranslation startRotation:startRotation startScale:startScale]; } }
渲染循环设置
- (void)step:(CADisplayLink *)sender { [self drawInMTKView:self]; } - (void)createDisplayLink { CADisplayLink *displayLink = [self displayLinkWithTarget:self selector:@selector(step:)]; [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; [displayLink setPreferredFrameRateRange:CAFrameRateRangeMake(30, 60, 60)]; } - (void)drawInMTKView:(nonnull MTKView *)view { // Wah wah .... @autoreleasepool { [self setupEncoder:view]; } }
核心优化方案
1. 预创建并复用渲染管线状态
当前每帧调用newRenderPipelineStateWithDescriptor创建管线状态,这是CPU开销极大的操作。管线状态只需初始化一次(如在视图初始化或setup方法中),之后每帧直接复用:
// 初始化阶段执行 - (void)setupRenderPipeline { NSError *error = nil; _pipelineState = [self.device newRenderPipelineStateWithDescriptor:_renderPipelineDescriptor error:&error]; if (!_pipelineState) { NSLog(@"管线状态创建失败: %@", error); } } // setupEncoder中直接复用 // 移除原有的pipeline创建代码,替换为: if (!_pipelineState) return; [commandEncoder setRenderPipelineState:_pipelineState];
2. 修复Render Pass Descriptor使用逻辑
代码中获取了view.currentRenderPassDescriptor却未使用,反而手动创建新的Descriptor,这会导致配置不匹配及额外开销。直接复用MTKView提供的Descriptor即可:
// 移除手动创建renderPass的代码,替换为: renderPass = view.currentRenderPassDescriptor; if (renderPass == nil) return; // 仅修改需要自定义的属性(如clearColor) renderPass.colorAttachments[0].clearColor = self.clearColor;
3. 优化Drawable获取方式
使用view.currentDrawable替代[_metalLayer nextDrawable],MTKView已内置Drawable生命周期管理,可避免手动调用导致的CPU-GPU同步阻塞:
drawable = view.currentDrawable;
4. 修正实例化渲染参数并减少Draw Call
当前drawPrimitives的instanceCount设置错误(设为三角形数量而非实例数量),会导致几何重复渲染,GPU负载暴增。同时合并静态几何、使用实例化渲染减少Draw Call:
// 修正instanceCount为1(单个实例渲染) [commandEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:node.geometry.vertexCount instanceCount:1]; // 进阶优化:将多个静态节点的几何合并为单个Buffer,减少setVertexBuffer和drawPrimitives调用次数
5. 改用MTKView原生渲染循环
移除自定义的CADisplayLink,直接实现MTKView的代理方法,Metal框架会自动优化渲染同步逻辑:
// 遵循MTKViewDelegate协议 - (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size { // 处理尺寸变更逻辑 } - (void)drawInMTKView:(MTKView *)view { @autoreleasepool { [self setupEncoder:view]; } }
内容的提问来源于stack exchange,提问作者Jonathan Claassens
相关产品推荐
相关产品推荐

