veg*_*her 6 point drawable ios swift metal
我正在编写一个视图来绘制Metal中的实时数据.我正在使用点基元绘制样本,并且我对顶点和统一数据进行三重缓冲.我遇到的问题是调用currentDrawable返回的时间似乎是不可预测的.这几乎就好像有时没有准备就绪,我必须等待一整帧才能获得.通常,currentDrawable返回的时间约为0.07毫秒(这与我的期望大致相同),但有时则是整整1/60秒.这导致整个主线程被阻塞,也就是说至少不是非常期望的.
我在iPhone 6S Plus和iPad Air上看到了这个问题.我还没有看到这种行为是Mac(我有一台带有AMD 460 GPU的2016 MPB).我的猜测是,这与iOS设备中的GPU基于TBDR的事实有某种关系.我不认为我的带宽有限,因为无论我绘制的样本数量多少或几乎没有,我都会得到完全相同的行为.
为了说明这个问题,我写了一个绘制静态正弦波的最小例子.这是一个简化的例子,因为我通常会将样本记忆到当前的vertexBuffer中,就像我对制服一样.这就是为什么我对顶点数据和制服进行三重缓冲.尽管如此,它仍然足以说明问题.只需将此视图设置为故事板中的基本视图,然后运行即可.在某些运行中,它工作得很好.其他时候currentDrawable以16.67 ms的返回时间开始,然后在几秒钟后跳转到0.07 ms,然后一段时间后回到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
}
}
Run Code Online (Sandbox Code Playgroud)
着色器
#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);
}
Run Code Online (Sandbox Code Playgroud)
可能与此相关:在我看过的所有示例中,inflightBufferSemaphore在commandBuffer的completionHandler中递增,就在信号量发出信号之前(这对我来说很有意义).当我有那条线时,我得到一个奇怪的抖动效果,几乎就像帧缓冲区被无序显示一样.将此行移动到绘制函数的底部可以解决问题,尽管它对我来说没有多大意义.我不确定这是否与currentDrawable的返回时间如此不可预测有关,但我感觉这两个问题正在从相同的潜在问题中出现.
任何帮助将非常感谢!
\n\n\n[T]调用 currentDrawable 返回所需的时间似乎是不可预测的。它\xe2\x80\x99s几乎就像有时没有准备好可绘制的东西一样,我必须等待一整帧才能有一个可用。
\n
嗯,是的。这是明确记录的。来自Metal 编程指南:
\n\n\n\n\n重要提示:只有一小部分可绘制资源,因此较长的帧渲染时间可能会暂时耗尽这些资源,并导致方法
\nnextDrawable调用阻塞其 CPU 线程,直到该方法完成。为了避免昂贵的 CPU 停顿,请在调用对象的方法之前执行所有不需要可绘制资源的每帧操作。nextDrawableCAMetalLayer
从文档中CAMetalLayer.nextDrawable():
\n\n\n调用此方法会阻塞当前 CPU 线程,直到有新的可绘制对象可用。只有一小部分可绘制资源,因此较长的 GPU 帧时间可能会暂时耗尽这些资源,迫使此调用阻塞,直到 GPU 渲染完成。为了获得最佳结果,
\nnextDrawable()相对于其他每帧 CPU 工作,尽可能晚地安排您的调用。
除此之外,您的代码还有一些奇怪的地方。您正在请求currentDrawable,但您没有用它做任何事情。自动配置currentRenderPassDescriptor为使用 的纹理currentDrawable。那么,如果您根本不要求currentDrawable自己,会发生什么?