2017-02-05 22 views
2

Я пишу представление для построения данных реального времени в Metal. Я рисую образцы с помощью примитивов точек, и я тройной буферизую как вершины, так и равномерные данные. Проблема, с которой я сталкиваюсь, заключается в том, что время, необходимое для вызова currentDrawable для возврата, кажется непредсказуемым. Это почти так, как будто иногда нет готовых чертежей, и я должен ждать целый кадр, чтобы он стал доступен. Обычно время для currentDrawable для возврата равно ~ 0,07 мс (это примерно то, что я ожидаю), но в других случаях это полный 1/60 с. Это заставляет весь основной поток блокировать, что, как минимум, не очень желательно.Непредсказуемое время возврата для currentDrawable

Я вижу эту проблему на iPhone 6S Plus и iPad Air. Я еще не видел такого поведения Mac (у меня есть MPI 2016 с AMD 460 GPU). Я предполагаю, что это как-то связано с тем, что графические процессоры на устройствах iOS основаны на TBDR. Я не думаю, что у меня ограниченная пропускная способность, потому что я получаю то же самое поведение независимо от того, сколько или сколько образцов я рисую.

Чтобы проиллюстрировать проблему, я написал минимальный пример, который рисует статическую синусоидальную волну. Это упрощенный пример, поскольку я обычно имел memcpy'ed образцы в текущий vertexBuffer, как и я с униформой. Вот почему я тройной буферизацией данных вершин, а также униформы. Однако этого достаточно, чтобы проиллюстрировать проблему. Просто установите это представление в качестве основного вида в раскадровке и запустите. На некоторых трассах это работает отлично. В других случаях currentDrawable запускается с обратным временем 16.67 мс, а затем через несколько секунд переходит в 0.07 мс, а затем через некоторое время возвращается к 16.67. Похоже, что с 16.67 по 0.07 если вы повернуть устройство по какой-то причине.

MTKView Подкласс

import MetalKit 

let N = 500 

class MetalGraph: MTKView { 
    typealias Vertex = Int32 

    struct Uniforms { 
     var offset: UInt32 
     var numSamples: UInt32 
    } 

    // Data 
    var uniforms = Uniforms(offset: 0, numSamples: UInt32(N)) 

    // Buffers 
    var vertexBuffers = [MTLBuffer]() 
    var uniformBuffers = [MTLBuffer]() 
    var inflightBufferSemaphore = DispatchSemaphore(value: 3) 
    var inflightBufferIndex = 0 

    // Metal State 
    var commandQueue: MTLCommandQueue! 
    var pipeline: MTLRenderPipelineState! 


    // Setup 

    override func awakeFromNib() { 
     super.awakeFromNib() 

     device = MTLCreateSystemDefaultDevice() 
     commandQueue = device?.makeCommandQueue() 
     colorPixelFormat = .bgra8Unorm 

     setupPipeline() 
     setupBuffers() 
    } 

    func setupPipeline() { 
     let library = device?.newDefaultLibrary() 

     let descriptor = MTLRenderPipelineDescriptor() 
     descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm 
     descriptor.vertexFunction = library?.makeFunction(name: "vertexFunction") 
     descriptor.fragmentFunction = library?.makeFunction(name: "fragmentFunction") 

     pipeline = try! device?.makeRenderPipelineState(descriptor: descriptor) 
    } 

    func setupBuffers() { 
     // Produces a dummy sine wave with N samples, 2 periods, with a range of [0, 1000] 
     let vertices: [Vertex] = (0..<N).map { 
      let periods = 2.0 
      let scaled = Double($0)/(Double(N)-1) * periods * 2 * .pi 
      let value = (sin(scaled) + 1) * 500 // Transform from range [-1, 1] to [0, 1000] 
      return Vertex(value) 
     } 

     let vertexBytes = MemoryLayout<Vertex>.size * vertices.count 
     let uniformBytes = MemoryLayout<Uniforms>.size 

     for _ in 0..<3 { 
      vertexBuffers .append(device!.makeBuffer(bytes: vertices, length: vertexBytes)) 
      uniformBuffers.append(device!.makeBuffer(bytes: &uniforms, length: uniformBytes)) 
     } 
    } 



    // Drawing 

    func updateUniformBuffers() { 
     uniforms.offset = (uniforms.offset + 1) % UInt32(N) 

     memcpy(
      uniformBuffers[inflightBufferIndex].contents(), 
      &uniforms, 
      MemoryLayout<Uniforms>.size 
     ) 
    } 

    override func draw(_ rect: CGRect) { 
     _ = inflightBufferSemaphore.wait(timeout: .distantFuture) 

     updateUniformBuffers() 

     let start = CACurrentMediaTime() 
     guard let drawable = currentDrawable else { return } 
     print(String(format: "Grab Drawable: %.3f ms", (CACurrentMediaTime() - start) * 1000)) 

     guard let passDescriptor = currentRenderPassDescriptor else { return } 

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

     let commandBuffer = commandQueue.makeCommandBuffer() 

     let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: passDescriptor) 
     encoder.setRenderPipelineState(pipeline) 
     encoder.setVertexBuffer(vertexBuffers[inflightBufferIndex], offset: 0, at: 0) 
     encoder.setVertexBuffer(uniformBuffers[inflightBufferIndex], offset: 0, at: 1) 
     encoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: N) 
     encoder.endEncoding() 

     commandBuffer.addCompletedHandler { _ in 
      self.inflightBufferSemaphore.signal() 
     } 
     commandBuffer.present(drawable) 
     commandBuffer.commit() 

     inflightBufferIndex = (inflightBufferIndex + 1) % 3 
    } 
} 

шейдеры

#include <metal_stdlib> 
using namespace metal; 

struct VertexIn { 
    int32_t value; 
}; 

struct VertexOut { 
    float4 pos [[position]]; 
    float pointSize [[point_size]]; 
}; 

struct Uniforms { 
    uint32_t offset; 
    uint32_t numSamples; 
}; 

vertex VertexOut vertexFunction(device VertexIn *vertices [[buffer(0)]], 
           constant Uniforms *uniforms [[buffer(1)]], 
           uint vid [[vertex_id]]) 
{ 
    // I'm using the vertex index to evenly spread the 
    // samples out in the x direction 
    float xIndex = float((vid + (uniforms->numSamples - uniforms->offset)) % uniforms->numSamples); 
    float x = (float(xIndex)/float(uniforms->numSamples - 1)) * 2.0f - 1.0f; 

    // Transforming the values from the range [0, 1000] to [-1, 1] 
    float y = (float)vertices[vid].value/500.0f - 1.0f ; 

    VertexOut vOut; 
    vOut.pos = {x, y, 1, 1}; 
    vOut.pointSize = 3; 

    return vOut; 
} 

fragment half4 fragmentFunction() { 
    return half4(1, 1, 1, 1); 
} 

Возможно, связанные с этим: Во всех примерах, которые я видел, inflightBufferSemaphore увеличивается в completionHandler в commandBuffer, всего до того, как семафор сигнал (что имеет смысл для меня). Когда у меня есть эта строка, я получаю странный дрожащий эффект, почти как если бы фреймбуферы отображались не по порядку. Перемещение этой строки в нижней части функции ничьи устраняет проблему, хотя для меня это не имеет большого смысла. Я не уверен, что это связано с тем, что время возврата currentDrawable настолько непредсказуемо, но я чувствую, что эти две проблемы возникают из одной и той же основной проблемы.

Любая помощь будет очень признательна!

ответ

1

[T] Время, которое требуется для вызова currentDrawable для возврата, кажется непредсказуемым. Это почти так, как будто иногда нет готовых чертежей, и я должен ждать целый кадр, чтобы он стал доступен.

Ум, да. Это явно задокументировано. Из Metal Programming Guide:

Важно: Есть только небольшой набор рисуемых ресурсов, поэтому время рендеринга долго кадра может временно истощить эти ресурсы и вызвать nextDrawable вызов метода с блокировать его CPU нити, пока метод не является завершено. Чтобы избежать дорогостоящих киосков CPU, выполните все операции с кадрами, которые не нуждаются в ресурсе до, вызывающего метод nextDrawable объекта CAMetalLayer.

Из docs for CAMetalLayer.nextDrawable():

Вызов этого метода блокирует текущий поток CPU, пока новый рисуют объект не доступен. Есть только небольшой набор ресурса, который можно использовать, поэтому длительное время кадра графического процессора может временно вывести эти ресурсы, заставляя этот вызов блокироваться до тех пор, пока рендеринг графического процессора не будет завершен. Для достижения наилучших результатов планируйте свой вызов nextDrawable() как можно позже по сравнению с другой работой центрального процессора.

Помимо этого, в вашем коде есть что-то странное. Вы запрашиваете currentDrawable, но вы ничего с этим не делаете. currentRenderPassDescriptor автоматически настроен на использование текстуры currentDrawable. Итак, что произойдет, если вы просто не запросите у себя currentDrawable?

+0

Мое впечатление заключалось в том, что в обращении находилось 3 ярлыка, а семафор был бы уверен, что я подожду, пока не будет доступно. Возможно, это не имеет значения, если это семафор или currentDrawable, которого я жду, что результат будет таким же. Но тогда в чем смысл семафора? Тем не менее, это ожидание оказывает значительное влияние на другие части моего кода, такие как таймеры в основной очереди. Вы говорите, что это ожидаемое поведение, и я должен найти обходные пути в остальной части моего кода? – vegather

+0

Я использую сбрасываемый в настоящее время (...). Вызов currentRenderPassDescriptor эффективно вызывает currentDrawable в любом случае. Я играл с ним, и, похоже, это не имеет значения, если я вызываю currentDrawable непосредственно в текущем (...). В этом случае currentRenderPassDescriptor получит непредсказуемое поведение. – vegather

+0

Вы нашли объяснение этому непредсказуемому поведению? – vtruant