Я пишу представление для построения данных реального времени в 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 настолько непредсказуемо, но я чувствую, что эти две проблемы возникают из одной и той же основной проблемы.
Любая помощь будет очень признательна!
Мое впечатление заключалось в том, что в обращении находилось 3 ярлыка, а семафор был бы уверен, что я подожду, пока не будет доступно. Возможно, это не имеет значения, если это семафор или currentDrawable, которого я жду, что результат будет таким же. Но тогда в чем смысл семафора? Тем не менее, это ожидание оказывает значительное влияние на другие части моего кода, такие как таймеры в основной очереди. Вы говорите, что это ожидаемое поведение, и я должен найти обходные пути в остальной части моего кода? – vegather
Я использую сбрасываемый в настоящее время (...). Вызов currentRenderPassDescriptor эффективно вызывает currentDrawable в любом случае. Я играл с ним, и, похоже, это не имеет значения, если я вызываю currentDrawable непосредственно в текущем (...). В этом случае currentRenderPassDescriptor получит непредсказуемое поведение. – vegather
Вы нашли объяснение этому непредсказуемому поведению? – vtruant