MTLSharedEventListener 块在命令缓冲区调度之前调用,而不是在运行中

MTLSharedEventListener block called before command buffer scheduling and not in-flight

我正在使用 MTLSharedEvent 偶尔将新信息从 CPU 传递到 GPU,方法是在已注册的块中写入具有存储模式 .storageModeManagedMTLBuffer通过共享事件(使用 MTLSharedEventnotify(_:atValue:block:) 方法,MTLSharedEventListener 配置为在后台调度队列上通知)。该过程看起来像这样:

let device = MTLCreateSystemDefaultDevice()!
let synchronizationQueue = DispatchQueue(label: "com.myproject.synchronization")

let sharedEvent = device.makeSharedEvent()!
let sharedEventListener = MTLSharedEventListener(dispatchQueue: synchronizationQueue)

// Updated only occasionally on the CPU (on user interaction). Mostly written to 
// on the GPU
let managedBuffer = device.makeBuffer(length: 10, options: .storageModeManaged)!

var doExtra = true

func computeSomething(commandBuffer: MTLCommandBuffer) {
    
    // Do work on the GPU every frame
   
    // After writing to the buffer on the GPU, synchronize the buffer (required)
    let blitToSynchronize = commandBuffer.makeBlitCommandEncoder()!
    blitToSynchronize.synchronize(resource: managedBuffer)
    blitToSynchronize.endEncoding()
    
    // Occassionally, add extra information on the GPU
    if doExtraWork {
        
        // Register a block to write into the buffer
        sharedEvent.notify(sharedEventListener, atValue: 1) { event, value in
            
            // Safely write into the buffer. Make sure we call `didModifyRange(_:)` after
            
            // Update the counter
            event.signaledValue = 2
        }
        
        commandBuffer.encodeSignalEvent(sharedEvent, value: 1)
        commandBuffer.encodeWaitForEvent(sharedEvent, value: 2)
    }
    
    // Commit the work
    commandBuffer.commit()
}

预期行为如下:

  1. GPU 使用托管缓冲区做一些工作
  2. 有时,需要用 CPU 上的新信息更新信息。在这个框架中,我们注册了一个要执行的工作块。我们在专用块中执行此操作,因为我们无法保证当主线程上的执行达到这一点时,GPU 不会同时读取或写入托管缓冲区。因此,目前简单地写入它是不安全的,并且必须确保 GPU 没有对这些数据做任何事情
  3. 当 GPU 安排执行此命令缓冲区时,执行 encodeSignalEvent(_:value:) 调用之前执行的命令,然后 GPU 上的执行停止,直到块递增 signaledValue 属性传递到块中的事件
  4. 当执行到达块时,我们可以安全地写入托管缓冲区,因为我们知道 CPU 对资源具有独占访问权限。完成后,我们恢复执行 GPU

问题是,当 GPU 执行命令时,Metal 似乎没有调用块,而是 命令缓冲区甚至被调度之前。更糟糕的是,系统似乎与初始命令缓冲区“一起工作”(第一个命令缓冲区,在任何其他命令缓冲区被调度之前)。

我第一次注意到这个问题是在我的场景在 CPU 更新后消失后查看 GPU 帧捕获时,这是我看到 GPU 到处都是 NaN 的地方地方。然后我 运行 进入了这种 st运行ge 情况,当时我故意在后台调度队列上等待 sleep(:_) 调用。非常正确,我的共享资源信号量(未显示,在命令缓冲区的完成块中发出信号并在主线程中等待)在将三个命令缓冲区提交到命令队列(三个是回收的数量)后达到 -1 的值共享 MTLBuffers 持有场景统一数据等)。这表明第一个命令缓冲区在 CPU 提前三帧以上时尚未完成执行,这与 sleep(_:) 行为一致。同样,顺序不一致:Metal 似乎甚至在调度缓冲区之前就调用了块。此外,在随后的帧中,Metal 似乎并不关心 sharedEventListener 块花费了这么长时间,并且即使块是 运行 也会安排命令缓冲区执行,这会在几十帧后完成.

这种行为与我的预期完全不符。这是怎么回事?

P.S。 可能有更好的方法来定期更新内容大部分为 在 GPU 上修改,但我还没有找到这样做的方法。也感谢有关此主题的任何建议。当然,三重缓冲区系统 可以 工作,但它会浪费大量内存,因为托管缓冲区非常大(而信号量管理的共享缓冲区非常小)

我想我已经找到了答案,但我不确定。

来自MTLSharedEvent doc entry

Commands waiting on the event are allowed to run if the new value is equal to or greater than the value for which they are waiting. Similarly, setting the event's value triggers notifications if the value is equal to or greater than the value for which they are waiting.

这意味着,如果您传递值 12 就像您在代码段中显示的那样,如果只会工作一次,那么事件将不会被等待和听众不会收到通知。

你必须确保你等待的值和信号每次都单调上升,所以你必须将它增加 1 或更多。