Synchronization on the GPU.
Just like for CPU code, you have to ensure that buffers and images are not accessed mutably by multiple GPU queues simultaneously and that they are not accessed mutably by the CPU and by the GPU simultaneously.
This safety is enforced at runtime by vulkano but it is not magic and you will require some knowledge if you want to avoid errors.
Whenever you ask the GPU to start an operation by using a function of the vulkano library (for
example executing a command buffer), this function will return a future. A future is an
object that implements the
GpuFuture trait and that represents the
point in time when this operation is over.
No function in vulkano immediately sends an operation to the GPU (with the exception of some
unsafe low-level functions). Instead they return a future that is in the pending state. Before
the GPU actually starts doing anything, you have to flush the future by calling the
method or one of its derivatives.
Futures serve several roles:
- Futures can be used to build dependencies between operations and makes it possible to ask that an operation starts only after a previous operation is finished.
- Submitting an operation to the GPU is a costly operation. By chaining multiple operations with futures you will submit them all at once instead of one by one, thereby reducing this cost.
- Futures keep alive the resources and objects used by the GPU so that they don’t get destroyed while they are still in use.
The last point means that you should keep futures alive in your program for as long as their corresponding operation is potentially still being executed by the GPU. Dropping a future earlier will block the current thread (after flushing, if necessary) until the GPU has finished the operation, which is usually not what you want.
If you write a function that submits an operation to the GPU in your program, you are encouraged to let this function return the corresponding future and let the caller handle it. This way the caller will be able to chain multiple futures together and decide when it wants to keep the future alive or drop it.
Respecting the order of operations on the GPU is important, as it is what proves vulkano that what you are doing is indeed safe. For example if you submit two operations that modify the same buffer, then you need to execute one after the other instead of submitting them independently. Failing to do so would mean that these two operations could potentially execute simultaneously on the GPU, which would be unsafe.
This is done by calling one of the methods of the
GpuFuture trait. For example calling
prev_future.then_execute(command_buffer) takes ownership of
prev_future and will make sure
to only start executing
command_buffer after the moment corresponding to
happens. The object returned by the
then_execute function is itself a future that corresponds
to the moment when the execution of
When you want to perform an operation after another operation on two different queues, you
must put a semaphore between them. Failure to do so would result in a runtime error.
Adding a semaphore is a simple as replacing
Note: A common use-case is using a transfer queue (ie. a queue that is only capable of performing transfer operations) to write data to a buffer, then read that data from the rendering queue.
What happens when you do so is that the first queue will execute the first set of operations
prev_future in the example), then put a semaphore in the signalled state.
Meanwhile the second queue blocks (if necessary) until that same semaphore gets signalled, and
then only will execute the second set of operations.
Since you want to avoid blocking the second queue as much as possible, you probably want to
flush the operation to the first queue as soon as possible. This can easily be done by calling
then_signal_semaphore_and_flush() instead of
then_signal_semaphore() method is appropriate when you perform an operation in one queue,
and want to see the result in another queue. However in some situations you want to start
multiple operations on several different queues.
TODO: this is not yet implemented
Fence is an object that is used to signal the CPU when an operation on the GPU is finished.
Signalling a fence is done by calling
then_signal_fence() on a future. Just like semaphores,
you are encouraged to use
Signalling a fence is kind of a “terminator” to a chain of futures.
TODO: lots of problems with how to use fences TODO: talk about fence + semaphore simultaneously TODO: talk about using fences to clean up
Used to block the GPU execution until an event on the CPU occurs.
A fence is used to know when a command buffer submission has finished its execution.
Represents a fence being signaled after a previous event.
Two futures joined into one.
A dummy future that represents “now”.
The full specification of memory access by the pipeline for a particular resource.
Used to provide synchronization between command buffers during their execution.
Represents a semaphore being signaled after a previous event.
Error that can happen when checking whether we have access to a resource.
Access to a resource was denied.
Error that can be returned when waiting on a fence.
Error that can happen when creating a graphics pipeline.
Declares in which queue(s) a resource can be used.
Declares in which queue(s) a resource can be used.
Represents an event that will happen on the GPU in the future.
Builds a future that represents “now”.