【问题标题】:Unpredictable return time for currentDrawablecurrentDrawable 的不可预测的返回时间
【发布时间】:2017-02-05 01:12:30
【问题描述】:

我正在编写一个视图来在 Metal 中绘制实时数据。我正在使用点图元绘制样本,并对顶点和统一数据进行三重缓冲。我遇到的问题是调用 currentDrawable 返回所需的时间似乎是不可预测的。就好像有时没有准备好可绘制对象,我必须等待一整帧才能有一个可用。通常 currentDrawable 返回的时间约为 0.07 毫秒(这与我预期的差不多),但其他时候是整整 1/60 秒。这会导致整个主线程阻塞,也就是说至少不是很理想。

我在 iPhone 6S Plus 和 iPad Air 上看到了这个问题。我还没有在 Mac 上看到这种行为(我有一个带有 AMD 460 GPU 的 2016 MPB)。我的猜测是,这在某种程度上与 iOS 设备中的 GPU 基于 TBDR 的事实有关。我不认为我受到带宽限制,因为无论我绘制多少样本,我都会得到完全相同的行为。

为了说明这个问题,我编写了一个绘制静态正弦波的最小示例。这是一个简化的示例,因为我通常会将样本 memcpy 到当前的 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 在 commandBuffer 的 completionHandler 中递增,就在信号量发出信号之前(这对我来说很有意义)。当我有那条线时,我会得到一种奇怪的抖动效果,几乎就像帧缓冲区被无序显示一样。将这条线移到绘图函数的底部解决了这个问题,尽管它对我来说没有多大意义。我不确定这是否与 currentDrawable 的返回时间如此不可预测有关,但我感觉这两个问题来自同一个潜在问题。

任何帮助将不胜感激!

【问题讨论】:

    标签: ios swift drawable point metal


    【解决方案1】:

    [T]调用 currentDrawable 返回所需的时间似乎是不可预测的。就好像有时没有准备好可绘制对象,我必须等待一整帧才能有一个可用。

    嗯,是的。这是明确记录的。来自Metal Programming Guide

    重要提示:只有一小部分可绘制资源,因此较长的帧渲染时间可能会暂时耗尽这些资源并导致 nextDrawable 方法调用阻塞其 CPU 线程,直到该方法完全的。为避免代价高昂的 CPU 停顿,调用CAMetalLayer 对象的nextDrawable 方法之前,执行所有不需要可绘制资源的每帧操作。

    来自docs for CAMetalLayer.nextDrawable()

    调用此方法会阻塞当前 CPU 线程,直到有新的可绘制对象可用。只有一小部分可绘制资源,因此较长的 GPU 帧时间可能会暂时耗尽这些资源,迫使此调用阻塞,直到 GPU 渲染完成。为获得最佳结果,请将您的 nextDrawable() 调用安排得相对于其他每帧 CPU 工作尽可能晚。

    除此之外,您的代码有些奇怪。您正在请求currentDrawable,但您没有对它做任何事情。 currentRenderPassDescriptor 自动配置为使用currentDrawable 的纹理。那么,如果您自己不请求currentDrawable,会发生什么?

    【讨论】:

    • 我的印象是有 3 个可绘制对象在流通,信号量会确保我等到一个可用。可能是我正在等待的信号量或 currentDrawable 都没有关系,最终结果是相同的。但是,信号量的意义何在?尽管如此,这种等待对我的代码的其他部分产生了重大影响,例如主队列上的计时器。你是说这是预期的行为,我应该在我的其余代码中找到解决方法吗?
    • 我确实在 present(...) 中使用了 drawable。无论如何,调用 currentRenderPassDescriptor 都会有效地调用 currentDrawable。我玩过它,如果我直接在 present(...) 中调用 currentDrawable 似乎并不重要。在这种情况下 currentRenderPassDescriptor 将获得不可预测的行为。
    • 您找到解释这种不可预知行为的原因了吗?
    【解决方案2】:

    我通过添加一个@autoreleasepool 解决了这个问题,这样drawable 就在我完成后立即发布。

    注意这里@autoreleasepool 的使用:

    https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/Drawables.html

    【讨论】:

      猜你喜欢
      • 2011-11-30
      • 2015-08-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多