I am trying to use MTLSharedEvent
along with MTLSharedEventListener
to synchronize computation between GPU and CPU, as in example provided by Apple (https://developer.apple.com/documentation/metal/synchronization/synchronizing_events_between_a_gpu_and_the_cpu). Basically what I want to achieve is have work split into 3 parts executed in order, like so:
My problem is that eventListener
block is always called before command buffer is being scheduled for execution, which make my CPU task execute first in order.
To simplify the case, let’s use simple commands that fill MTLBuffer
with certain values (my original use case is more complicated, as using compute encoders with custom shaders, but behaves the same):
let device = MTLCreateSystemDefaultDevice()!
let queue = device.makeCommandQueue()!
let event = device.makeSharedEvent()!
let dispatchQueue = DispatchQueue(label: "myqueue")
let eventListener = MTLSharedEventListener(dispatchQueue: dispatchQueue)
let metalBuffer = device.makeBuffer(length: 2048, options: MTLResourceOptions.storageModeShared)!
let buffer = queue.makeCommandBuffer()!
NSLog("Start - signaled value: \(event.signaledValue)")
event.notify(eventListener, atValue: 1) { event, value in
// CPU work
let pointer = metalBuffer.contents().assumingMemoryBound(to: UInt8.self)
for i in 0..<512 {
(pointer + i).pointee = (pointer + i).pointee + 1;
}
NSLog("Event notification - signaled value: \(value), buffer status: \(buffer.status.rawValue)")
event.signaledValue = 2
}
// GPU work part 1
let encoder1 = buffer.makeBlitCommandEncoder()!
encoder1.fill(buffer: metalBuffer, range: .init(0...127), value: 22)
encoder1.endEncoding()
// signal with 1 to start CPU task
buffer.encodeSignalEvent(event, value: 1)
// wait for value >= 2 to proceed
buffer.encodeWaitForEvent(event, value: 2)
// GPU work part 2
let encoder2 = buffer.makeBlitCommandEncoder()!
encoder2.fill(buffer: metalBuffer, range: .init(128...511), value: 255)
encoder2.endEncoding()
buffer.addScheduledHandler { buffer in
NSLog("Buffer scheduled - signaled value: \(event.signaledValue)")
}
buffer.addCompletedHandler { buffer in
NSLog("Buffer completed - signaled value: \(event.signaledValue)")
}
buffer.commit()
buffer.waitUntilCompleted()
Output:
2022-01-09 23:46:08.774 Sync[76882:3531755] Metal GPU Frame Capture Enabled
2022-01-09 23:46:08.805 Sync[76882:3531755] Start - signaled value: 0
2022-01-09 23:46:08.808 Sync[76882:3531764] Event notification - signaled value: 1, buffer status: 2 (Commited)
2022-01-09 23:46:08.809 Sync[76882:3531763] Buffer scheduled - signaled value: 2
2022-01-09 23:46:08.809 Sync[76882:3531763] Buffer completed - signaled value: 2
As you can see eventListener
logs buffer status as .commited
.
What’s the matter here? Am I missing something?
System: macOS 12.0.1, Apple M1 Pro, Xcode 13.2.1
This is perfectly fine that command buffer is committed. In fact if it wouldn't be committed you'll never get to notify
block.
GPU and CPU runs in parallel. So when you use MTLEvent
you don't stop executing CPU code (all the Swift code actually). You just tell GPU in what order to execute GPU code.
So what's happening in your case:
commit()
. Before it GPU actually don't do anything. You just scheduled command to be performed on GPU but don't perform them.MTLEvent
. It performs part 1, then encodes value 1
to event, performs notify block, encodes value 2
, performs second GPU block.But again all the actual GPU work starts only when you call commit()
on command buffer. That's why buffer is already committed in notify block. Because it is performed after commit()
.