MTLSharedEventListener 块在命令缓冲区调度之前调用而不是在运行中
Posted
技术标签:
【中文标题】MTLSharedEventListener 块在命令缓冲区调度之前调用而不是在运行中【英文标题】:MTLSharedEventListener block called before command buffer scheduling and not in-flight 【发布时间】:2021-02-09 03:13:49 【问题描述】:我正在使用 MTLSharedEvent
偶尔将新信息从 CPU 传递到 GPU,方法是在共享事件注册的块内写入具有存储模式 .storageModeManaged
的 MTLBuffer
(使用 notify(_:atValue:block:)
方法MTLSharedEvent
,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()
预期的行为如下:
-
GPU 对托管缓冲区做了一些工作
有时,需要使用 CPU 上的新信息更新信息。在这个框架中,我们注册了一个要执行的工作块。我们在专用块中这样做是因为我们不能保证当主线程上的执行到达这一点时,GPU 不会同时读取或写入托管缓冲区。因此,当前简单地写入它是不安全的,并且必须确保 GPU 没有对这些数据做任何事情
当 GPU 调度执行此命令缓冲区时,在调用
encodeSignalEvent(_:value:)
之前执行的命令将被执行,然后在 GPU 上停止执行,直到块增加传递到块中的事件的 signaledValue
属性
当执行到达块时,我们可以安全地写入托管缓冲区,因为我们知道 CPU 对资源具有独占访问权。完成后,我们将恢复 GPU 的执行
问题在于,当 GPU 执行命令时,Metal 似乎没有调用块,而是 在 命令缓冲区甚至被调度。更糟糕的是,系统似乎在使用初始命令缓冲区(第一个命令缓冲区,在任何其他命令缓冲区之前)“工作”。
当我在 CPU 更新后我的场景消失后查看 GPU 帧捕获时,我第一次注意到这个问题,这就是我看到 GPU 到处都有NaN
s 的地方。然后,当我故意在后台调度队列中等待sleep(:_)
调用时,我遇到了这种奇怪的情况。完全正确,我的共享资源信号量(未显示,在命令缓冲区的完成块中发出信号并在主线程中等待)在将三个命令缓冲区提交到命令队列(三个是回收的数量共享MTLBuffer
s 持有场景统一数据等)。这表明此时第一个命令缓冲区尚未完成执行,此时 CPU 领先超过三帧,这与 sleep(_:)
行为一致。同样,不一致的是顺序:Metal 似乎在调度缓冲区之前就调用了块。此外,在随后的帧中,Metal 似乎并不关心 sharedEventListener
块花费这么长时间,并且即使在块运行时也会安排命令缓冲区执行,这会在几十帧后完成。
这种行为与我的预期完全不一致。这是怎么回事?
附: 可能有更好的方法来定期更新托管缓冲区,其内容主要是 在 GPU 上修改,但我还没有找到这样做的方法。对此主题的任何建议也将受到赞赏。当然,三重缓冲区系统可以工作,但它会浪费大量内存,因为托管缓冲区非常大(而由信号量管理的共享缓冲区非常小)
【问题讨论】:
逻辑看起来没问题,我唯一看到的是事件值应该是单调增长的,不能只从1切换到2,因为value
是“最小在调用通知处理程序之前需要发出信号的值。"
你是否有一个小样本可以重现这个?
【参考方案1】:
我想我有你的答案,但我不确定。
来自MTLSharedEvent doc entry
如果新值等于或大于它们正在等待的值,则允许运行等待事件的命令。同样,如果设置事件的值等于或大于他们正在等待的值,则会触发通知。
这意味着,如果您传递值 1
和 2
就像您在 sn-p 中显示的那样,if 只会工作一次,然后不会等待事件并且不会等待侦听器通知。
你必须确保你正在等待的值,然后信号每次都单调上升,所以你必须将它提高 1 或更多。
【讨论】:
我明白了。似乎我错误地假设缓冲区完成时该值会重置或其他什么。将值更改为单调增加确实会使未来的缓冲区等待,因此这是固定的。但是,该块似乎仍然被调用 before 缓冲区被安排执行,这不是我所期望的。最后,我似乎仍然有 NaN,即使我不做任何阅读或写作。也就是说,当我禁用未来帧的实际缓冲区写入代码(通过简单的 if 检查)时,我似乎仍然得到 NaNs 这可能暗示 GPU 方面有问题。我正在执行的更新更改了已调度线程的数量,因此可能会除以 0,而不是从 CPU 写入错误值或不告诉 GPU 有更新。我会调查的 您是否启用了金属验证?它应该抓住这样的案例。很难从一个小的 sn-p 看出发生了什么,但我也可以建议尝试捕获一个“帧”以查看那里出了什么问题。如果您没有展示任何内容,您可以使用MTLCaptureManager
在某些点开始和停止捕获。试试这篇文章:developer.apple.com/documentation/metal/…
事实证明,除了您解决的问题之外,还有一个奇怪的除以零,尽管有很好的值写入 GPU,但它给了我 NaN。捕获范围对那个指针很有帮助
没问题,一般来说,捕获有助于了解正在发生的事情,并且它可以帮助您捕获由于回放方式而导致的同步错误。以上是关于MTLSharedEventListener 块在命令缓冲区调度之前调用而不是在运行中的主要内容,如果未能解决你的问题,请参考以下文章