you can’t combine async compute shader with async readback. they are mutually exclusive. the reason is unknown to me.
there are two approaches:
-
- use a non-blocking call to
CommandBuffer.RequestAsyncReadbackIntoNativeArray.
-
only the callback is available to discover end of readback
-
conversely, it doesn’t give you a AsyncGPUReadbackRequest to poll the end with AsyncGPUReadbackRequest.done
-
use a non-blocking call to Graphics.ExecuteCommandBuffer that freezes the GPU, because it executes in the graphical pipeline
-
- create and store a fence with
CommandBuffer.CreateAsyncGraphicsFence
-
use GraphicsFence.passed to poll end of execution.
-
the dumbest form of polling is inside the MonoBehaviour.Update event
-
I think coroutines can be used to poll? I don’t really care
-
if you are multi-threading, the easiest and simplest way to poll is inside UnitySynchronizationContext. however, this requires complete and very deep understanding of TAP
-
use a non-blocking call to Graphics.ExecuteCommandBufferAsync
-
only the blocking call ComputeBuffer.GetData is available for reading
I support both approaches. call SystemInfo.supportsAsyncCompute to branch your code. keep the boolean value in a variable, since this call is expensive.
since my compute shader is extremely expensive and my GPU will freeze anyway because it is using the graphical pipeline, I don’t bother with async readback and I just call ComputeBuffer.GetData in both cases
in my project, I have to dispatch the compute shader from a thread which is not the “main thread”. sorry for the incomplete code, but I have a secrecy agreement.
the UnitySynchronizationContext hack (only for multi-threaded situations!):
put this in some MonoBehaviour:
public class [...] : MonoBehaviour
{
private void Start()
{
// Keep this in a "global" variable for later use
Ucs = SynchronizationContext.Current;
}
}
call this inside some thread. Task.Wait is blocking! so if the compute shader is expensive, calling this from the “main thread” would freeze it! this entire hack is unnecessary if calling from the “main thread”, just use MonoBehaviour.Update or Unity coroutines!
new Func<Task>(async () =>
{
SynchronizationContext.SetSynchronizationContext(Ucs);
await Task.Yield();
bool isAsync = SystemInfo.supportsAsyncCompute;
Assert.IsTrue(SystemInfo.supportsComputeShaders);
CommandBuffer command = new();
command.SetExecutionFlags(isAsync ? CommandBufferExecutionFlags.AsyncCompute : CommandBufferExecutionFlags.None);
[...]
command.DispatchCompute([...]);
if (isAsync)
{
Assert.IsTrue(SystemInfo.supportsGraphicsFence);
GraphicsFence fence = command.CreateAsyncGraphicsFence();
Graphics.ExecuteCommandBufferAsync(command, ComputeQueueType.Background);
while (!fence.passed)
{
await Task.Yield();
}
}
else
{
Graphics.ExecuteCommandBuffer(command);
}
[...].GetData([...]);
command.Dispose();
[...]
})().Wait();
notice I take care to name everything blocking/non-blocking. all the API has “async” slapped into their names, but they are not actually “async” at all, they are all just non-blocking with polling or callback notification. it was very confusing for me in the beginning. moreover, none of the API integrates with any of Microsoft’s official async paradigms.