2015-12-02 81 views
5

我想创建一个使用金属的程序游戏,并且我正在使用基于八叉树的块方法来实现细节级别。金属块渲染

我正在使用的方法涉及CPU为地形创建八叉树节点,然后使用计算着色器在GPU上创建其网格。该网格存储在块对象中的顶点缓冲区和索引缓冲区中用于渲染。

所有这些似乎工作得很好,但是当涉及到渲染块时,我很早就遇到了性能问题。目前,我收集一系列块来绘制,然后将其提交给我的渲染器,这会创建一个MTLParallelRenderCommandEncoder,然后为每个块创建一个MTLRenderCommandEncoder,然后将其提交给GPU。

通过它的外观大约50%的CPU时间花费在为每个块创建MTLRenderCommandEncoder上。目前,我只是为每个块创建一个简单的8顶点立方体网格,并且我有一个4x4x4的块大小,我在这些早期阶段下降到大约50fps。 (在现实中,它似乎只能有最多63 MTLRenderCommandEncoder每个MTLParallelRenderCommandEncoder,所以它不是完全型4x4x4)

我读过的MTLParallelRenderCommandEncoder点是在一个单独的线程创建每个MTLRenderCommandEncoder,但我没有太多的运气,让这个工作。此外,多线程不会绕过63个块的上限被渲染为最大值。

我觉得以某种方式将每个块的顶点和索引缓冲区合并为一个或两个较大的缓冲区以提交会有所帮助,但我不知道如何在没有大量调用的情况下如何执行此操作,以及这是否会改善效率。

这里是我的代码,需要的节点数组中,并提请他们:

func drawNodes(nodes: [OctreeNode], inView view: AHMetalView){ 
    // For control of several rotating buffers 
    dispatch_semaphore_wait(displaySemaphore, DISPATCH_TIME_FOREVER) 

    makeDepthTexture() 

    updateUniformsForView(view, duration: view.frameDuration) 
    let commandBuffer = commandQueue.commandBuffer() 


    let optDrawable = layer.nextDrawable() 

    guard let drawable = optDrawable else{ 
    return 
    } 

    let passDescriptor = MTLRenderPassDescriptor() 

    passDescriptor.colorAttachments[0].texture = drawable.texture 
    passDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.2, 0.2, 0.2, 1) 
    passDescriptor.colorAttachments[0].storeAction = .Store 
    passDescriptor.colorAttachments[0].loadAction = .Clear 

    passDescriptor.depthAttachment.texture = depthTexture 
    passDescriptor.depthAttachment.clearDepth = 1 
    passDescriptor.depthAttachment.loadAction = .Clear 
    passDescriptor.depthAttachment.storeAction = .Store 

    let parallelRenderPass = commandBuffer.parallelRenderCommandEncoderWithDescriptor(passDescriptor) 

    // Currently 63 nodes as a maximum 
    for node in nodes{ 
    // This line is taking up around 50% of the CPU time 
    let renderPass = parallelRenderPass.renderCommandEncoder() 

    renderPass.setRenderPipelineState(renderPipelineState) 
    renderPass.setDepthStencilState(depthStencilState) 
    renderPass.setFrontFacingWinding(.CounterClockwise) 
    renderPass.setCullMode(.Back) 

    let uniformBufferOffset = sizeof(AHUniforms) * uniformBufferIndex 

    renderPass.setVertexBuffer(node.vertexBuffer, offset: 0, atIndex: 0) 
    renderPass.setVertexBuffer(uniformBuffer, offset: uniformBufferOffset, atIndex: 1) 

    renderPass.setTriangleFillMode(.Lines) 

    renderPass.drawIndexedPrimitives(.Triangle, indexCount: AHMaxIndicesPerChunk, indexType: AHIndexType, indexBuffer: node.indexBuffer, indexBufferOffset: 0) 

    renderPass.endEncoding() 
    } 
    parallelRenderPass.endEncoding() 

    commandBuffer.presentDrawable(drawable) 

    commandBuffer.addCompletedHandler { (commandBuffer) -> Void in 
    self.uniformBufferIndex = (self.uniformBufferIndex + 1) % AHInFlightBufferCount 
    dispatch_semaphore_signal(self.displaySemaphore) 
    } 

    commandBuffer.commit() 
} 

回答

5

您注意:

我读过的MTLParallelRenderCommandEncoder的一点是在创建每个MTLRenderCommandEncoder一个独立的线...

而你是正确的。你在做什么是按顺序创建,编码和结束命令编码器 - 这里没有什么并行的,所以MTLParallelRenderCommandEncoder没有为你做任何事情。如果您消除了并行编码器,并在每次通过for循环时创建了renderCommandEncoderWithDescriptor(_:)编码器,那么您的性能大致相同......也就是说,由于创建开销,您仍然会遇到同样的性能问题所有这些编码器。

所以,如果你要顺序编码,只需重新使用相同的编码器。此外,您应尽可能多地重复使用其他共享状态。这里有一个快速传球,在可能的重构(未经测试):

let passDescriptor = MTLRenderPassDescriptor() 

// call this once before your render loop 
func setup() { 
    makeDepthTexture() 

    passDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.2, 0.2, 0.2, 1) 
    passDescriptor.colorAttachments[0].storeAction = .Store 
    passDescriptor.colorAttachments[0].loadAction = .Clear 

    passDescriptor.depthAttachment.texture = depthTexture 
    passDescriptor.depthAttachment.clearDepth = 1 
    passDescriptor.depthAttachment.loadAction = .Clear 
    passDescriptor.depthAttachment.storeAction = .Store 

    // set up render pipeline state and depthStencil state 
} 

func drawNodes(nodes: [OctreeNode], inView view: AHMetalView) { 

    updateUniformsForView(view, duration: view.frameDuration) 

    // Set up completed handler ahead of time 
    let commandBuffer = commandQueue.commandBuffer() 
    commandBuffer.addCompletedHandler { _ in // unused parameter 
     self.uniformBufferIndex = (self.uniformBufferIndex + 1) % AHInFlightBufferCount 
     dispatch_semaphore_signal(self.displaySemaphore) 
    } 

    // Semaphore should be tied to drawable acquisition 
    dispatch_semaphore_wait(displaySemaphore, DISPATCH_TIME_FOREVER) 
    guard let drawable = layer.nextDrawable() 
     else { return } 

    // Set up the one part of the pass descriptor that changes per-frame 
    passDescriptor.colorAttachments[0].texture = drawable.texture 

    // Get one render pass descriptor and reuse it 
    let renderPass = commandBuffer.renderCommandEncoderWithDescriptor(passDescriptor) 
    renderPass.setTriangleFillMode(.Lines) 
    renderPass.setRenderPipelineState(renderPipelineState) 
    renderPass.setDepthStencilState(depthStencilState) 

    for node in nodes { 
     // Update offsets and draw 
     let uniformBufferOffset = sizeof(AHUniforms) * uniformBufferIndex 
     renderPass.setVertexBuffer(node.vertexBuffer, offset: 0, atIndex: 0) 
     renderPass.setVertexBuffer(uniformBuffer, offset: uniformBufferOffset, atIndex: 1) 
     renderPass.drawIndexedPrimitives(.Triangle, indexCount: AHMaxIndicesPerChunk, indexType: AHIndexType, indexBuffer: node.indexBuffer, indexBufferOffset: 0) 

    } 
    renderPass.endEncoding() 

    commandBuffer.presentDrawable(drawable) 
    commandBuffer.commit() 
} 

然后,仪器配置文件,看看你可能有什么,如果有的话,进一步的性能问题。这里有一个很好的WWDC 2015 session,它展示了几个常见的“陷阱”,如何在剖析中诊断它们,以及如何解决它们。

+0

这是一些很棒的建议,就像魅力一样。在16-18ms内CPU的帧数为3-4ms左右。非常感谢,我仍然在学习如何使用金属,并且我的大部分代码都是从Metal by Example中剥离出来的,它是基础教程。你真的帮了,谢谢。 –

+0

另外,出于某种原因,我在我的脑海中有一个渲染命令编码器 - >一个绘制基元调用:S –

+0

*“我的头脑中有一个渲染命令编码器 - >一个绘制原语调用”* ...这就是在幼稚使用OpenGL的情况下实际发生的情况 - 每次绘制调用都可能隐含地变成昂贵的GPU状态编译和上传。但金属使事情更加明确。如果看起来你是多余而低效的,你可能是。 :) – rickster