Metal学习(4) - 通过使用一个资源的多个实例来避免CPU和GPU工作之间的停顿

353 阅读4分钟

ShaderType.h

#include <simd/simd.h>

typedef enum ShaderVertexInputIndex
{
    ShaderVertexInputIndexVertices     = 0,
    ShaderVertexInputIndexViewportSize = 1,
} ShaderVertexInputIndex;

typedef struct
{
    vector_float2 position;
    vector_float4 color;
} ShaderVertex;

Shader.metal

#include <metal_stdlib>
using namespace metal;

#include "ShaderType.h"

struct RasterizerData
{
    float4 position [[position]];
    float4 color;
};

vertex RasterizerData vertexShader(const uint vertexID [[vertex_id]],
                                   const device ShaderVertex *vertices [[buffer(ShaderVertexInputIndexVertices)]],
                                   constant vector_uint2 *viewportSizePointer [[buffer(ShaderVertexInputIndexViewportSize)]])
{
    RasterizerData out;
    vector_float2 pixelSpacePosition = vertices[vertexID].position.xy;
    vector_float2 viewportSize = vector_float2(*viewportSizePointer);
    out.position = vector_float4(0.0, 0.0, 0.0, 1.0);
    out.position.xy = pixelSpacePosition / (viewportSize / 2.0);
    out.color = vertices[vertexID].color;
    return out;
}


fragment float4 fragmentShader(RasterizerData in [[stage_in]])
{
    return in.color;
}

MetalRender.swift

import UIKit
import MetalKit
import simd

/// 缓冲帧的最大数量
private let MaxFramesInFlight: Int = 3
/// 场景中三角形的数量
private let TrianglesCount = 50
/// 变化限度
private let Limit = 100

class MetalRender: NSObject {

    // 向设备传递命令的命令队列
    private var commandQueue: MTLCommandQueue?
    private var pipelineState: MTLRenderPipelineState?
    private var viewportSize: vector_uint2 = vector_uint2(x: 1, y: 1)
    private var triangleVertexs: [ShaderVertex] = []
    private var vertexBuffers: [MTLBuffer] = []
    private var currentBuffer: Int = 0
    private var wavePosition: Float = 0
    private var refreshCount: Int = 0
    private var iSCumulative: Bool = true

    private let inFlightSemaphore = DispatchSemaphore(value: MaxFramesInFlight)
    private let colors: [vector_float4] = [vector_float4(1.0, 0.0, 0.0, 1.0),
                                           vector_float4(0.0, 1.0, 0.0, 1.0),
                                           vector_float4(0.0, 0.0, 1.0, 1.0),
                                           vector_float4(1.0, 0.0, 1.0, 1.0),
                                           vector_float4(0.0, 1.0, 1.0, 1.0),
                                           vector_float4(1.0, 1.0, 0.0, 1.0),
    ]

    private override init() {
        super.init()
    }

    convenience init(_ view: MTKView) {
        self.init()

        let device = view.device

        let defaultLibrary = device?.makeDefaultLibrary()
        let vertexFunction = defaultLibrary?.makeFunction(name: "vertexShader")
        let fragmentFunction = defaultLibrary?.makeFunction(name: "fragmentShader")  

        let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
        pipelineStateDescriptor.label = "Simple Pipeline"
        pipelineStateDescriptor.vertexFunction = vertexFunction
        pipelineStateDescriptor.fragmentFunction = fragmentFunction
        pipelineStateDescriptor.colorAttachments[0].pixelFormat = view.colorPixelFormat
        // 指定缓冲区在被绑定到绘制或分派中执行计算或渲染管道之间是否会被修改。
        pipelineStateDescriptor.vertexBuffers[Int(ShaderVertexInputIndexVertices.rawValue)].mutability = .immutable

        do {
            pipelineState = try device?.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
        }catch {
            print(error)
        }

        commandQueue = device?.makeCommandQueue()

        /// 初始化三角形数据
        triangleVertexs = generateTriangles()
        /// 创建buffer
        let bufferLength = MaxFramesInFlight * TrianglesCount * 3 * MemoryLayout<ShaderVertex>.size
        for i in 0..<MaxFramesInFlight {
            let buffer = device?.makeBuffer(length: bufferLength)
            buffer?.label = "Vertex Buffer \(i)"
            vertexBuffers.append(buffer!)
        }
    }
}

// MARK: - private

private extension MetalRender {

    /// 初始化三角形
    func generateTriangles() -> [ShaderVertex] {
        let horizontalSpacing: Float = 16
        let colorsCount = colors.count
        var triangles: [ShaderVertex] = []
        for t in 0..<(TrianglesCount*3) {
            let x = (-0.5*Float(TrianglesCount)+Float(t))*horizontalSpacing
            let color = colors[t%colorsCount]

            var triangle0 = ShaderVertex()
            triangle0.position = vector_float2(x-32, -32)
            triangle0.color = color

            var triangle1 = ShaderVertex()
            triangle1.position = vector_float2(x, 32)
            triangle1.color = color

            var triangle2 = ShaderVertex()
            triangle2.position = vector_float2(x+32, -32)
            triangle2.color = color
            
            triangles.append(triangle0)
            triangles.append(triangle1)
            triangles.append(triangle2)
        }
        return triangles
    }

    /// 更新顶点数据
    func updateState() {
        let waveSpeed: Float = 0.05
        if refreshCount > Limit {
            iSCumulative = false
        }else if refreshCount < -Limit {
            iSCumulative = true
        }
        if iSCumulative {
            wavePosition += waveSpeed
            refreshCount += 1
        }else {
            wavePosition -= waveSpeed
            refreshCount -= 1
        }
        // 获取buffer的指针
        let bufferPointer = vertexBuffers[currentBuffer].contents()
        for i in 0..<(TrianglesCount*3) {
            var vertex = triangleVertexs[i]
            vertex.position = vector_float2(vertex.position.x , vertex.position.y+wavePosition*Float(i))
            // 向buffer写入值
            bufferPointer.advanced(by: i*MemoryLayout<ShaderVertex>.size).storeBytes(of: vertex, as: ShaderVertex.self)
        }
    }
}

// MARK: - MTKViewDelegate

extension MetalRender: MTKViewDelegate {
    // 每当视图改变方向或调整大小时调用
    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
        viewportSize.x = UInt32(size.width)
        viewportSize.y = UInt32(size.height)
        
        triangleVertexs = generateTriangles()
    }

    // 当视图需要渲染帧时调
    func draw(in view: MTKView) {

        inFlightSemaphore.wait()
        currentBuffer = (currentBuffer + 1)%MaxFramesInFlight

        updateState()

        guard pipelineState != nil else { return }

        let commandBuffer = commandQueue?.makeCommandBuffer()
        commandBuffer?.label = "MyCommand"
        if let renderPassDescriptor = view.currentRenderPassDescriptor {
            let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
            renderEncoder?.label = "MyRenderEncoder"
            renderEncoder?.setViewport(MTLViewport(originX: 0, originY: 0, width: Double(viewportSize.x), height: Double(viewportSize.y), znear: -1, zfar: 1))
            renderEncoder?.setRenderPipelineState(pipelineState!)
            
           // 使用buffer传值
           renderEncoder?.setVertexBuffer(vertexBuffers[currentBuffer], offset: 0, index: Int(ShaderVertexInputIndexVertices.rawValue))
           
            renderEncoder?.setVertexBytes(&viewportSize, length: MemoryLayout<vector_uint2>.size, index: Int(ShaderVertexInputIndexViewportSize.rawValue))
            renderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: TrianglesCount*3)
            renderEncoder?.endEncoding()
            if let drawable = view.currentDrawable {
                commandBuffer?.present(drawable)
            }
        }
        commandBuffer?.addCompletedHandler({ buffer in
            self.inFlightSemaphore.signal()
        })
        commandBuffer?.commit()
    }
}

顶点数据存储在CPU和GPU共享的缓冲区中。CPU将数据写入缓冲区,GPU读取数据。
资源共享在处理器之间创建了数据依赖关系:

  • CPU必须先完成对资源的写操作,GPU才能读取资源。
  • 如果GPU在CPU写之前读取该资源,则会读取未定义的资源数据。
  • CPU写时,GPU读取资源,会读取错误的资源数据。 这些数据依赖会导致CPU和GPU之间的处理器停滞;每个处理器在开始自己的工作之前必须等待其他处理器完成它的工作。

image.png

由于CPU和GPU是单独的处理器,所以可以通过使用一个资源的多个实例使它们同时工作。每一帧,着色器提供相同的参数,但这并不意味着必须引用相同的资源对象。相反,可以创建一个资源的多个实例池,并在每次渲染时使用一个不同的实例。
如下图所示,在GPU读取第n帧的数据的同时,CPU可以进行n+1帧的数据实例的写入。这样就节省了等待时间。

image.png 实例池中每一份实例的创建都有相应的内存消耗,那么实例池中存放几份实例比较好?

Core Animation提供了优化的可显示资源,通常被称为绘图,你可以渲染内容并在屏幕上显示它。Drawables是高效但昂贵的系统资源,所以Core Animation限制了你可以同时在应用中使用的Drawables的数量。默认的限制是3,但你可以通过maximumDrawableCount属性将其设置为2(2和3是唯一支持的值)。

所以,最大存放3份就好了,没必要更多。
缓冲区实例每次都创建新的是昂贵的也是浪费的,因为是逐帧渲染的,所以我们可以把实例池中的实例看作一个FIFO队列来使用。每当使用到最后一个实例了,就再从第一个开始使用,不断的进行数据覆盖。

管理CPU和GPU工作速率
因为CPU、GPU不同步,那么如果是GPU耗时的渲染,有可能CPU写入数据过快,在GPU正使用的实例中,重新覆盖了数据。
为了防止这种情况发生,需要限制CPU的写入,因为是类似队列的使用实例,所以GPU的commandBuffer执行完成肯定是有序的。我们不必再记录顺序,可以通过信号量开控制,让信号量等于实例池数量,每当commandBuffer提交渲染,信号量-1,commandBuffer渲染完成,信号量+1,当信号量为0的时候,哪怕CPU执行快,也需要等待。这样就不会改动到GPU正在使用的数据了。

Metal可以优化不可变缓冲区的性能,但不能优化可变缓冲区的性能。为了获得最好的结果,尽可能多地使用不可变缓冲区。

ipelineStateDescriptor.vertexBuffers[Int(ShaderVertexInputIndexVertices.rawValue)].mutability = .immutable