You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Metal绘图应用中环形缓冲未提升FPS的问题排查

基于Metal的绘图应用渲染性能优化问题

我正在开发一款基于Metal的绘图应用,和其他绘图应用不同,这款应用将所有笔画存储为数据模型而非仅保存绘制后的位图,以此支持用户用橡皮擦工具移除单一笔画而不影响其他笔画。但擦除笔画时需要重新渲染剩余笔画,因此渲染函数需尽可能高效,目前遇到了严重的性能问题:

用CADisplayLink循环调用初始渲染函数,渲染4000笔画时,缓冲区长度达到4000000,应用FPS仅为12,响应性极差。

初始渲染实现

func renderStroke(strokes: [Stroke]) {
    let commandBuffer = commandQueue?.makeCommandBuffer()
    let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderer.renderPassDescriptor)
    commandEncoder?.setRenderPipelineState(pipelineState)

    var allPoints = [MetalStrokePoint]()

    for stroke in strokes {
        let pointsAlongPath = stroke.cachedPointsAlongPath
        allPoints.append(contentsOf: pointsAlongPath)
    }

    let pointsAlongPathBuffer = sharedDevice?.makeBuffer(bytes: allPoints, length: MemoryLayout<MetalStrokePoint>.stride * allPoints.count, options: .cpuCacheModeWriteCombined)

    if let pointsAlongPathBuffer {
        commandEncoder?.setVertexBuffer(pointsAlongPathBuffer, offset: 0, index: 0)
        commandEncoder?.setVertexBuffer(renderer.uniformBuffer, offset: 0, index: 1)
        commandEncoder?.setVertexBuffer(renderer.transformBuffer, offset: 0, index: 2)

        commandEncoder?.setFragmentTexture(self.stampTexture, index: 0)

        commandEncoder?.drawPrimitives(type: .point, vertexStart: 0, vertexCount: pointsAlongPath.count)
    }

    renderer?.commitCommandBufer()
}

环形缓冲优化后的实现

改用环形缓冲复用缓冲区后,CPU占用率降低了5%,但FPS并未提升,优化后的代码如下:

func renderStroke(strokes: [Stroke]) {
    // Wait for a free ring buffer
    renderer?.ringBufferSemaphore?.wait()

    let commandBuffer = commandQueue?.makeCommandBuffer()
    let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderer.renderPassDescriptor)
    commandEncoder?.setRenderPipelineState(pipelineState)

    var allPoints = [MetalStrokePoint]()

    for stroke in strokes {
        let pointsAlongPath = stroke.cachedPointsAlongPath
        allPoints.append(contentsOf: pointsAlongPath)
    }

    // Reuse ring buffer to avoid creating new buffer
    let ringBuffer = renderer.ringBuffers.getCurrentRingBuffer()

    let ringBufferContents = ringBuffer.contents().bindMemory(to: MetalStrokePoint.self, capacity: allPoints.count)

    for (pointIndex, point) in allPoints.enumerated() {
        ringBufferContents[pointIndex] = point
    }

    commandEncoder?.setVertexBuffer(ringBuffer, offset: 0, index: 0)
    commandEncoder?.setVertexBuffer(renderer.uniformBuffer, offset: 0, index: 1)
    commandEncoder?.setVertexBuffer(renderer.transformBuffer, offset: 0, index: 2)

    commandEncoder?.setFragmentTexture(self.stampTexture, index: 0)

    commandEncoder?.drawPrimitives(type: .point, vertexStart: 0, vertexCount: pointsAlongPath.count)

    // Signal to free current ring buffer
    renderer.commandBuffer?.addCompletedHandler({ commandBuffer in
        renderer.ringBufferSemaphore?.signal()
    })

    renderer?.commitCommandBufer()
}

请问是我实现有误,还是环形缓冲在此场景下无法帮助提升FPS?


问题分析与优化建议

首先,环形缓冲的作用是减少CPU端频繁创建缓冲区的内存开销,但你的核心性能瓶颈不在内存分配,而在数据拷贝和GPU绘制压力,同时代码里存在明显错误:

1. 实现中的错误修正

  • 绘制点数错误:优化后的代码里drawPrimitives的vertexCount用了pointsAlongPath.count(单条笔画的点数),应该改为allPoints.count,否则实际绘制的点数远低于需求,这会导致渲染异常,必须先修正。
  • 信号量释放对象错误:addCompletedHandler应该绑定当前创建的commandBuffer,而非renderer.commandBuffer,否则信号量无法正确释放,可能导致后续渲染阻塞。修正后代码应为:
commandBuffer?.addCompletedHandler { _ in
    renderer.ringBufferSemaphore?.signal()
}

2. 环形缓冲无法提升FPS的原因

环形缓冲解决的是CPU侧内存分配的开销,但你的主要性能瓶颈在两处:

  • CPU端的数据拷贝开销:每次渲染都要把所有笔画的点拼接成allPoints数组,再逐点拷贝到环形缓冲,400万点的拷贝是巨大的CPU消耗。
  • GPU端的绘制压力:一次性绘制400万点,GPU的顶点处理和片元填充压力极大,这才是FPS无法提升的核心原因。

3. 针对性优化方案

  • 避免CPU端数组拼接:不要将所有点合并到一个数组,而是为每条笔画提前分配Metal顶点缓冲区(将cachedPointsAlongPath直接存储为MTLBuffer),渲染时循环调用drawPrimitives绘制单条笔画,彻底消除CPU端的大规模内存拷贝。
  • 启用实例化绘制:如果所有笔画的渲染逻辑一致,将笔画的公共属性(颜色、宽度等)存入实例缓冲区,通过实例化绘制减少Draw Call数量,降低CPU命令提交开销。
  • 离屏渲染缓存静态内容:对于不会被修改的笔画,渲染到离屏纹理(MTLTexture),后续只需绘制该纹理;只有当用户擦除笔画时,再重新生成离屏纹理,避免重复渲染所有笔画。
  • 减少顶点数量:检查cachedPointsAlongPath的采样密度,根据屏幕分辨率和笔画长度做优化,剔除不必要的顶点。
  • 优化渲染管线:确保渲染管线状态(pipelineState)启用了提前深度剔除等优化,简化片元着色器逻辑,减少GPU计算压力。

内容的提问来源于stack exchange,提问作者Brew Master

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 15:20:37