首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >什么会导致对MetalKit MTKView的()函数的循环调用滞后?

什么会导致对MetalKit MTKView的()函数的循环调用滞后?
EN

Stack Overflow用户
提问于 2017-12-02 04:37:30
回答 1查看 904关注 0票数 1

我正在设计一个Cocoa应用程序,它使用用于MetalKit 10.13的快速4.0 macOS API。我在这里报告的一切都是在2015年的MBPro上完成的。

我已经成功地实现了一个MTKView,它很好地呈现简单的几何学和低顶点计数(立方体、三角形等)。我实现了一个基于鼠标拖动的相机,它可以旋转、散落和放大。以下是我旋转多维数据集时xcode FPS调试屏幕的屏幕截图:

但是,当我尝试加载一个数据集时,它只包含~1500个顶点(每个顶点存储为7x32bit浮点数).ie: 42 kB总数),我开始在FPS方面有一个非常严重的滞后。我将向您展示更低的代码实现。下面是一个屏幕截图(请注意,在这个图像上,视图只包含几个顶点,这些顶点被呈现为大点):

以下是我的实现:

1) viewDidLoad():

代码语言:javascript
复制
override func viewDidLoad() {

    super.viewDidLoad()

    // Initialization of the projection matrix and camera
    self.projectionMatrix = float4x4.makePerspectiveViewAngle(float4x4.degrees(toRad: 85.0),
                                      aspectRatio: Float(self.view.bounds.size.width / self.view.bounds.size.height),
                                      nearZ: 0.01, farZ: 100.0)
    self.vCam = ViewCamera()

    // Initialization of the MTLDevice
    metalView.device = MTLCreateSystemDefaultDevice()
    device = metalView.device
    metalView.colorPixelFormat = .bgra8Unorm

    // Initialization of the shader library
    let defaultLibrary = device.makeDefaultLibrary()!
    let fragmentProgram = defaultLibrary.makeFunction(name: "basic_fragment")
    let vertexProgram = defaultLibrary.makeFunction(name: "basic_vertex")

    // Initialization of the MTLRenderPipelineState
    let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
    pipelineStateDescriptor.vertexFunction = vertexProgram
    pipelineStateDescriptor.fragmentFunction = fragmentProgram
    pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
    pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)

    // Initialization of the MTLCommandQueue
    commandQueue = device.makeCommandQueue()

    // Initialization of Delegates and BufferProvider for View and Projection matrix MTLBuffer
    self.metalView.delegate = self
    self.metalView.eventDelegate = self
    self.bufferProvider = BufferProvider(device: device, inflightBuffersCount: 3, sizeOfUniformsBuffer: MemoryLayout<Float>.size * float4x4.numberOfElements() * 2)
}

2)加载立方体顶点的MTLBuffer:

代码语言:javascript
复制
private func makeCubeVertexBuffer() {

    let cube = Cube()
    let vertices = cube.verticesArray
    var vertexData = Array<Float>()
    for vertex in vertices{
        vertexData += vertex.floatBuffer()
    }
    VDataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0])
    self.vertexBuffer = device.makeBuffer(bytes: vertexData, length: VDataSize!, options: [])!
    self.vertexCount = vertices.count
}

3)数据集顶点的MTLBuffer加载。请注意,我将此缓冲区的存储模式显式声明为Private,以确保GPU高效地访问数据,因为一旦加载缓冲区,CPU就不需要访问数据。另外,请注意,我在实际数据集中只加载了1/100的顶点,因为当我试图完全加载它(只有4.2MB的数据)时,机器上的整个操作系统开始滞后。

代码语言:javascript
复制
public func loadDataset(datasetVolume: DatasetVolume) {

    // Load dataset vertices
    self.datasetVolume = datasetVolume
    self.datasetVertexCount = self.datasetVolume!.vertexCount/100
    let rgbaVertices = self.datasetVolume!.rgbaPixelVolume[0...(self.datasetVertexCount!-1)]
    var vertexData = Array<Float>()
    for vertex in rgbaVertices{
            vertexData += vertex.floatBuffer()
    }
    let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0])

    // Make two MTLBuffer's: One with Shared storage mode in which data is initially loaded, and a second one with Private storage mode
    self.datasetVertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: MTLResourceOptions.storageModeShared)
    self.datasetVertexBufferGPU = device.makeBuffer(length: dataSize, options: MTLResourceOptions.storageModePrivate)

    // Create a MTLCommandBuffer and blit the vertex data from the Shared MTLBuffer to the Private MTLBuffer
    let commandBuffer = self.commandQueue.makeCommandBuffer()
    let blitEncoder = commandBuffer!.makeBlitCommandEncoder()
    blitEncoder!.copy(from: self.datasetVertexBuffer!, sourceOffset: 0, to: self.datasetVertexBufferGPU!, destinationOffset: 0, size: dataSize)
    blitEncoder!.endEncoding()
    commandBuffer!.commit()

    // Clean up
    self.datasetLoaded = true
    self.datasetVertexBuffer = nil
}

4)最后,这里是呈现循环。同样,这是在使用MetalKit。

代码语言:javascript
复制
func draw(in view: MTKView) {
    render(view.currentDrawable)
}

private func render(_ drawable: CAMetalDrawable?) {
    guard let drawable = drawable else { return }

    // Make sure an MTLBuffer for the View and Projection matrices is available
    _ = self.bufferProvider?.availableResourcesSemaphore.wait(timeout: DispatchTime.distantFuture)

    // Initialize common RenderPassDescriptor
    let renderPassDescriptor = MTLRenderPassDescriptor()
    renderPassDescriptor.colorAttachments[0].texture = drawable.texture
    renderPassDescriptor.colorAttachments[0].loadAction = .clear
    renderPassDescriptor.colorAttachments[0].clearColor = Colors.White
    renderPassDescriptor.colorAttachments[0].storeAction = .store

    // Initialize a CommandBuffer and add a CompletedHandler to release an MTLBuffer from the BufferProvider once the GPU is done processing this command
    let commandBuffer = self.commandQueue.makeCommandBuffer()
    commandBuffer?.addCompletedHandler { (_) in
        self.bufferProvider?.availableResourcesSemaphore.signal()
    }

    // Update the View matrix and obtain an MTLBuffer for it and the projection matrix
    let camViewMatrix = self.vCam.getLookAtMatrix()
    let uniformBuffer = bufferProvider?.nextUniformsBuffer(projectionMatrix: projectionMatrix, camViewMatrix: camViewMatrix)

    // Initialize a MTLParallelRenderCommandEncoder
    let parallelEncoder = commandBuffer?.makeParallelRenderCommandEncoder(descriptor: renderPassDescriptor)

    // Create a CommandEncoder for the cube vertices if its data is loaded
    if self.cubeLoaded == true {
        let cubeRenderEncoder = parallelEncoder?.makeRenderCommandEncoder()
        cubeRenderEncoder!.setCullMode(MTLCullMode.front)
        cubeRenderEncoder!.setRenderPipelineState(pipelineState)
        cubeRenderEncoder!.setTriangleFillMode(MTLTriangleFillMode.fill)
        cubeRenderEncoder!.setVertexBuffer(self.cubeVertexBuffer, offset: 0, index: 0)
        cubeRenderEncoder!.setVertexBuffer(uniformBuffer, offset: 0, index: 1)
        cubeRenderEncoder!.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount!, instanceCount: self.cubeVertexCount!/3)
        cubeRenderEncoder!.endEncoding()
    }

    // Create a CommandEncoder for the dataset vertices if its data is loaded
    if self.datasetLoaded == true {
        let rgbaVolumeRenderEncoder = parallelEncoder?.makeRenderCommandEncoder()
        rgbaVolumeRenderEncoder!.setRenderPipelineState(pipelineState)
        rgbaVolumeRenderEncoder!.setVertexBuffer( self.datasetVertexBufferGPU!, offset: 0, index: 0)
        rgbaVolumeRenderEncoder!.setVertexBuffer(uniformBuffer, offset: 0, index: 1)
        rgbaVolumeRenderEncoder!.drawPrimitives(type: .point, vertexStart: 0, vertexCount: datasetVertexCount!, instanceCount: datasetVertexCount!)
        rgbaVolumeRenderEncoder!.endEncoding()
    }

    // End CommandBuffer encoding and commit task
    parallelEncoder!.endEncoding()
    commandBuffer!.present(drawable)
    commandBuffer!.commit()
}

好的,以下是我尝试找出造成滞后的原因时所经历的步骤,记住滞后效应与数据集的顶点缓冲区的大小成正比:

  1. 我最初认为这是由于GPU无法足够快地访问内存,因为它处于共享存储模式,所以->将dataset MTLBuffer更改为私有存储模式。这并没有解决问题。
  2. 然后,我认为问题是由于CPU花费了太多的时间在()函数上。这可能是由于BufferProvider出现了问题,或者可能是因为CPU试图以某种方式重新处理/重新加载数据集顶点缓冲区--每个帧-> --为了检查这一点,我在xcode的仪器中使用了Time Profiler。不幸的是,问题似乎是应用程序很少调用这个呈现方法(换句话说,MTKView的draw()方法)。以下是一些截图:

  • 10秒的峰值是当立方体被加载时。
  • 25-35秒之间的峰值是在加载数据集时。

  • 这个图像(^)显示了在加载多维数据集之后的10-20秒之间的活动。这是在FPS在60岁的时候。您可以看到,在这10秒内,主线程在render()函数中花费了大约53 10。

  • 此图像(^)显示数据集加载后大约40-50秒之间的活动。这是FPS < 10的时候,您可以看到主线程在这10秒内在render()函数中花费了大约4ms。正如您所看到的,通常在此函数中调用的方法中没有一个被调用(即,当只加载多维数据集时,我们可以看到调用的方法,前一个映像)。请注意,当我加载数据集时,时间分析器的计时器开始跳转(即:它停止几秒钟,然后跳转到当前时间.(重复)。

所以我现在就在这里问题似乎是CPU以某种方式超载了这42 kB的数据.递归地。我还用xcode的仪器中的分配器进行了测试。据我所知,没有内存泄漏的迹象(您可能已经注意到这对我来说是新的)。

不好意思,这篇文章太复杂了,我希望它不会太难理解。提前感谢你们的帮助。

编辑:

这是我的着色器,如果你想看的话:

代码语言:javascript
复制
struct VertexIn{
    packed_float3 position;
    packed_float4 color;
};

struct VertexOut{
    float4 position [[position]];  
    float4 color;
    float  size [[point_size]];
};

struct Uniforms{
    float4x4 cameraMatrix;
    float4x4 projectionMatrix;
};


vertex VertexOut basic_vertex(const device VertexIn* vertex_array [[ buffer(0) ]],
                              constant Uniforms&  uniforms    [[ buffer(1) ]],
                              unsigned int vid [[ vertex_id ]]) {

    float4x4 cam_Matrix = uniforms.cameraMatrix;
    float4x4 proj_Matrix = uniforms.projectionMatrix;

    VertexIn VertexIn = vertex_array[vid];

    VertexOut VertexOut;
    VertexOut.position = proj_Matrix * cam_Matrix * float4(VertexIn.position,1);
    VertexOut.color = VertexIn.color;
    VertexOut.size = 15;

    return VertexOut;
}

fragment half4 basic_fragment(VertexOut interpolated [[stage_in]]) {
    return half4(interpolated.color[0], interpolated.color[1], interpolated.color[2], interpolated.color[3]);
}
EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2017-12-02 11:49:15

我认为主要的问题是,你告诉金属做实例绘图,而你不应该这样做。这一行:

代码语言:javascript
复制
rgbaVolumeRenderEncoder!.drawPrimitives(type: .point, vertexStart: 0, vertexCount: datasetVertexCount!, instanceCount: datasetVertexCount!)

告诉金属绘制每个datasetVertexCount!顶点的datasetVertexCount!实例。GPU的工作随着顶点计数的平方而增长。另外,由于您没有使用实例ID来调整顶点位置,所以所有这些实例都是相同的,因此是多余的。

我认为这句话也适用于这一行:

代码语言:javascript
复制
cubeRenderEncoder!.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount!, instanceCount: self.cubeVertexCount!/3)

虽然还不清楚self.cubeVertexCount!是什么,也不清楚它是否与vertexCount一起增长。在任何情况下,由于您似乎使用相同的管道状态,因此相同的着色器没有使用实例ID,它仍然是无用和浪费的。

其他事情:

当您实际上没有使用MTLParallelRenderCommandEncoder启用的并行性时,为什么要使用它呢?别干那事。

无论您在哪里使用size方法MemoryLayout,几乎都应该使用stride。如果您正在计算复合数据结构的步长,那么不是取该结构中一个元素的步长乘以元素数吗?采取整个数据结构的步调。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/47604638

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档