Что может вызвать задержку повторяющихся вызовов функции draw () MetalKit MTKView

Я разрабатываю приложение Какао, используя Swift 4.0 MetalKit API для macOS 10.13. Все, о чем я здесь сообщаю, было сделано на моем MBPro 2015 года выпуска.

Я успешно реализовал MTKView, который очень хорошо отображает простую геометрию с небольшим количеством вершин (кубы, треугольники и т. Д.). Я реализовал камеру на основе перетаскивания мышью, которая вращается, обтекает и увеличивает. Вот скриншот экрана отладки xcode FPS, пока я вращаю куб:

введите описание изображения здесь

Однако, когда я пытаюсь загрузить набор данных, который содержит только ~ 1500 вершин (каждая из которых хранится как 7 x 32-битных чисел с плавающей запятой ... то есть: всего 42 кБ), я начинаю получать очень плохую задержку в FPS. Я покажу реализацию кода ниже. Вот скриншот (обратите внимание, что на этом изображении вид охватывает только несколько вершин, которые отображаются как большие точки):

введите описание изображения здесь

Вот моя реализация:

1) viewDidLoad ():

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 для вершин куба:

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, чтобы обеспечить эффективный доступ к данным со стороны графического процессора, поскольку процессору не требуется доступ к данным после загрузки буфера. Также обратите внимание, что я загружаю только 1/100 вершин в моем фактическом наборе данных, потому что вся ОС на моем компьютере начинает отставать, когда я пытаюсь загрузить его полностью (всего 4,2 МБ данных).

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.

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. Сначала я думал, что это было из-за того, что графический процессор не мог достаточно быстро получить доступ к памяти, потому что он был в режиме общего хранилища -> Я изменил набор данных MTLBuffer на режим частного хранилища. Это не решило проблему.

  2. Затем я подумал, что проблема связана с тем, что процессор слишком много времени тратит на мою функцию render (). Это могло быть связано с проблемой с BufferProvider или, может быть, потому что каким-то образом ЦП пытался каким-то образом повторно обрабатывать / перезагружать буфер вершин набора данных каждый кадр -> Чтобы это проверить, я использовал Time Profiler в инструментах xcode. К сожалению, похоже, что проблема в том, что приложение вызывает этот метод рендеринга (другими словами, метод draw () MTKView) очень редко. Вот несколько скриншотов:

введите описание изображения здесь

  • Всплеск ~ 10 секунд - это когда куб загружен.
  • Пики между ~ 25-35 секундами - это когда набор данных загружен.

введите описание изображения здесь

  • Это изображение (^) показывает активность в промежутке ~ 10-20 секунд сразу после загрузки куба. Это когда FPS на уровне ~ 60. Вы можете видеть, что основной поток тратит около 53 мсек на функцию render () в течение этих 10 секунд.

введите описание изображения здесь

  • На этом изображении (^) показана активность между ~ 40-50 секундами сразу после загрузки набора данных. Это когда FPS <10. Вы можете видеть, что основной поток тратит около 4 мсек на функцию render () в течение этих 10 секунд. Как видите, ни один из методов, которые обычно вызываются из этой функции, не вызывается (то есть: те, которые мы видим, вызываются, когда загружен только куб, предыдущее изображение). Следует отметить, что когда я загружаю набор данных, таймер профилировщика времени начинает прыгать (то есть: он останавливается на несколько секунд, а затем переходит к текущему времени ... повтор).

Так вот где я. Проблема, похоже, в том, что процессор каким-то образом перегружается этими 42 КБ данных ... рекурсивно. Я также провел тест с распределителем в инструментах xcode. Насколько я могу судить, никаких признаков утечки памяти (вы могли заметить, что многое из этого для меня в новинку).

Извините за запутанный пост, я надеюсь, что следить за ним не так уж и сложно. Заранее всем спасибо за помощь.

Изменить:

Вот мои шейдеры, на случай, если вы захотите их увидеть:

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]);
}

person Alexandre Sps    schedule 02.12.2017    source источник


Ответы (1)


Я думаю, что главная проблема в том, что вы говорите Металлу делать инстансы рисования, когда этого делать не следует. Эта строка:

rgbaVolumeRenderEncoder!.drawPrimitives(type: .point, vertexStart: 0, vertexCount: datasetVertexCount!, instanceCount: datasetVertexCount!)

сообщает Metal нарисовать datasetVertexCount! экземпляра каждой из datasetVertexCount! вершин. Работа графического процессора растет пропорционально квадрату количества вершин. Кроме того, поскольку вы не используете идентификатор экземпляра, например, для настройки положения вершины, все эти экземпляры идентичны и, следовательно, являются избыточными.

Думаю, то же самое относится и к этой строке:

cubeRenderEncoder!.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount!, instanceCount: self.cubeVertexCount!/3)

хотя непонятно, что такое self.cubeVertexCount! и растет ли он с vertexCount. В любом случае, поскольку кажется, что вы используете одно и то же состояние конвейера и, следовательно, те же шейдеры, которые не используют идентификатор экземпляра, это по-прежнему бесполезно и расточительно.

Другие вещи:

Почему вы используете MTLParallelRenderCommandEncoder, когда фактически не используете обеспечиваемый им параллелизм? Не делай этого.

Везде, где вы используете size метод MemoryLayout, вам почти наверняка следует использовать вместо него stride. А если вы вычисляете шаг составной структуры данных, не умножайте шаг одного элемента этой структуры на количество элементов. Оцените всю структуру данных.

person Ken Thomases    schedule 02.12.2017
comment
Спасибо, Кен, за проницательный ответ. Я рассмотрю различные аспекты, о которых вы упомянули, и сообщу об этом. - person Alexandre Sps; 02.12.2017
comment
Итак, 1) вы определенно правы насчет инстансового рисунка. Я плохо понимал, что это означает и как влияет на рендеринг. Теперь все стало намного плавнее. 2) Я согласен с вами в отношении использования MTLParallelCommandEncoder. Я добавил логику потока управления, чтобы параллельный кодировщик вызывался только тогда, когда это было необходимо. 3) Не могли бы вы привести пример реализации шага для всего массива? Я могу только заставить его правильно работать с отдельными элементами массива, что требует умножения на количество элементов ... Еще раз спасибо! - person Alexandre Sps; 04.12.2017
comment
Относительно 1): из того, что я теперь понимаю об инстансном рисовании, я в основном перегружал графический процессор, прося его отрендерить набор данныхVertexCount! экземпляры набора данныхVertexCount! вершины. В этом есть смысл. Однако знаете ли вы, почему окно отладки xcode FPS не зарегистрировало эту перегрузку графического процессора? Как вы можете видеть на втором снимке экрана в моем первоначальном вопросе, графический процессор практически бездействует, поэтому я не подумал рассматривать его как потенциальное узкое место в данном конкретном контексте. - person Alexandre Sps; 04.12.2017
comment
Извините, я не знаю, почему Xcode не отображает загрузку графического процессора. Что касается шага для массива, я тоже не уверен. Я вообще не использую Swift. Я лишь мельком знаком с этим здесь, на SO и тому подобном. Для массива имеет смысл умножать шаг отдельного элемента на количество элементов. Мое предостережение касалось структур. Например, шаг float3 не равен трехкратному шагу поплавка. - person Ken Thomases; 04.12.2017