Understanding synchronization primitives in Vulkan

Synchronization is key to bringing order and discipline into asynchronous system. It not only improves resource utilization, it also benefits from parallelism by reducing CPU and GPU idle time.

Vulkan offers the following four types of synchronization primitive for concurrent execution:

  • Fences: Offer synchronization between the host and device
  • Semaphores: Synchronize between and within queues
  • Events: Between queue submissions
  • Barriers: Within a command buffer between commands

In this section, we will learn about synchronization primitives and understand their API specification. The drawing object example that we implemented in this chapter makes use of semaphores to synchronize swapchain image writing. In the next chapter, we will learn to draw textures and implement fence to synchronize the host and device.

Fences

When a host submits a command in a queue, it gets scheduled for device processing. Sometimes it may require to know the status of command execution on the GPU in order to control the execution of the next batch, to ensure that it never overlaps with the previous batch of commands, which may produce undefined results or a situation that causes resource access violation.

Fence provides synchronization between the host and the GPU; using this, an application instructs the host to wait until a certain submitted operation is completed. This way, the GPU can be prevented from piling up more operations into the command queues:

Creating the fence object: The fence object can be created using the vkCreateFence()API.

VkResult vkCreateFence( 
    VkDevice                                    device, 
    const VkFenceCreateInfo*                    pCreateInfo, 
    const VkAllocationCallbacks*                pAllocator, 
    VkFence*                                    pFence); 

Let's take a look at the different fields used in this API and their respective descriptions:

Parameter

Description

device

This is the logical device object, which will be used to create the fence object.

pCreateInfo

This is a pointer to an array of the VkFenceCreateInfo control structure.

pAllocator

This controls the host memory allocation. You can refer to Host memory, Chapter 5, Command Buffer and Memory Management in Vulkan for more information.

pFence

This is the handle of the created fence object.

The vkCreateFence() API takes VkFenceCreateInfo containing metadata, which is used to control the creation of the fence objects. Following is the syntax of this control structure:

typedef struct VkFenceCreateInfo { 
    VkStructureType       sType; 
    const void*           pNext; 
    VkFenceCreateFlags    flags; 
} VkFenceCreateInfo; 

It has three fields: the first field sType indicates the type information of this structure, which must be VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; the second field pNext is not in use and must be NULL; the last parameter is VkFenceCreateFlagBits (see the following code snippet), which indicates whether the created fence object will be in a signaled or nonsignaled state. The signal state is specified using VK_FENCE_CREATE_SIGNALED_BIT.

typedef enum VkFenceCreateFlagBits { 
    VK_FENCE_CREATE_SIGNALED_BIT = 0x00000001, 
} VkFenceCreateFlagBits; 

Waiting on the fence object: Once a valid fence object is created, the host can inject this into a command and wait for it using the vkWaitForFences()API until it is not processed by the device. The device signals the fence object as soon as it processes the associated command, allowing the host to unblock the waiting state. Following is the API syntax:

VkResult vkWaitForFences( 
    VkDevice                   device, 
    uint32_t                   fenceCount, 
    const VkFence*             pFences, 
    VkBool32                   waitAll, 
    uint64_t                   timeout); 

The vkWaitForFences API takes the following parameters:

Parameters

Description

device

This is the logical device object (VkDevice) that will be used to destroy the fence object.

fenceCount

This is number of the fence object that needs to be destroyed.

PFences

This is an array of fence objects handles that needs to be destroyed. The array size must be equal to fenceCount.

waitAll

The block can be unblocked using this Boolean flag. When this flag value is:

  • VK_TRUE: It indicates that all the pFences must be signaled in order to successfully unblock the waiting.
  • VK_FALSE: At least one fence object in the pFences array must be signaled for successfully unblocking the wait state.

timeOut

This is the time-out period, specified (in nano seconds), which will be used to unblock the wait state if the fence object never got signaled. This field guarantees that the system never falls into an infinite blocking state that will bring the application to a halt.

Destroying the fence object: Once the fence is used and no longer required, it can be destroyed using the vkDestroyFence()API; this API takes three parameters—the first parameter device is the logical device that will be used to destroy the fence object, which is indicated by the second parameter called fence; the last parameter (pAllocator) manages host memory deallocation:

void vkDestroyFence( 
    VkDevice                     device, 
    VkFence                      fence, 
    const VkAllocationCallbacks* pAllocator); 

Resetting the fence object: The application can also preserve the created fence object and reuse it by resetting them using vkResetFences(). This API takes three parameters as an input—the first parameter indicates the logical device to be used to reset the given fence objects. The number of fence objects that need to be reset is pointed by the second parameter called fenceCount. The last parameter pFences is a pointer of the array of fence objects that will be reset by this API. The following is the syntax of this API:

VkResult vkResetFences( 
    VkDevice          device, 
    uint32_t          fenceCount, 
    const VkFence*    pFences); 

Let's move to the next synchronization primitive: semaphores.

Semaphores

Semaphores give the flexibility to achieve synchronization at the queue level; they are used to synchronize one or more queues. A Semaphore has two states: signaled and unsignaled. Signaled semaphores are specified in the queue submission command vkQueueSubmit(); it blocks the rest of the batch until the semaphores are not unsignaled by the device. A created semaphore is visible across multiple queues. If two or more queue submission commands are waiting upon the same semaphore, then only one will receive the signaled state; others may continue to wait, ensuring atomicity.

Creating semaphore object: Semaphores are created using the vkCreateSemaphore() API; the following is the syntax of this API:

VkResult vkCreateSemaphore( 
   VkDevice                      device, 
    const VkSemaphoreCreateInfo* pCreateInfo, 
    const VkAllocationCallbacks* pAllocator, 
    VkSemaphore*                 pSemaphore); 

The vkCreateSemaphore API takes the following parameters:

Parameter

Description

device

This is the logical device object, which will be used to create the semaphore object.

pCreateInfo

This is the pointer to an array of the VkSemaphoreCreateInfo control structure.

pAllocator

This controls host memory allocation. You can refer to Host memory, Chapter 5, Command Buffer and Memory Management in Vulkan for more information.

pSemaphore

This is the handle of the created semaphore object.

The VkSemaphoreCreateInfo type structure has three parameters—the first parameter sType indicates the type information of this control structure; the second parameter is pNext, which could be a valid pointer to an extension-specific structure or could be NULL. The last parameter is a flag value (flags), which is currently not being used and is reserved for future purposes. The following is the syntax of this structure:

typedef struct VkSemaphoreCreateInfo { 
    VkStructureType           sType; 
    const void*               pNext; 
    VkSemaphoreCreateFlags    flags; 
} VkSemaphoreCreateInfo; 

Destroying a semaphore: The created semaphore is destroyed using vkDestroySemaphore() as declared in the following syntax. This API takes three parameters. The first parameter device is the logical device that will destroy the semaphore object specified in the second parameter (semaphore). The last parameter (pAllocator) manages host memory deallocation:

void vkDestroySemaphore( 
    VkDevice                      device, 
    VkSemaphore                   semaphore, 
    const VkAllocationCallbacks*  pAllocator); 

Note

In this chapter, we used semaphores to ensure that a given swapchain image is only being used if it is not read by the presentation engine, in other words, when the presentation has finished reading the swapchain image and is ready to render it. For this, we created a semaphore object and passed it into vkAcquireNextImageKHR(); this API signals the semaphore when the image is ready to render. This signaled semaphore is next passed to vkQueueSubmit() using the VkSubmitInfo control structure; this ensures that drawing commands must only be drawn to the presentation image when it is not being used. The vkQueueSubmit() unsignals the semaphore, unblocking the next command (vkQueuePresentKHR) to be executed; this command renders the image to the output display.

Events

Events controls fine-grained synchronization and can exist in both signaled and unsignaled states. It allows synchronization of work within a single command buffer or sequence of command buffers submitted to a queue. Both the host and device can signal or reset the events. Similarly, both can wait on the event object; however, the device is only allowed to wait at some specific pipeline stage within the pipeline. You will learn more as we will proceed through the API specification:

Creating the event object: The event can be created using vkCreateEvent() API. This API accepts three parameters; the syntax is provided as follows:

VkResult vkCreateEvent( 
    VkDevice                      device, 
    const VkEventCreateInfo*      pCreateInfo, 
    const VkAllocationCallbacks*  pAllocator, 
    VkEvent*                      pEvent); 

The vkCreateEvent API takes four parameters as described in the following table:

Parameter

Description

device

This is the logical device object, which will be used to create the event object.

pCreateInfo

Thhis is the pointer to an array of VkEventCreateInfo control structures.

pAllocator

This controls host memory allocation. You can refer to Host memory, Chapter 5, Command Buffer and Memory Management in Vulkan for more information.

pSemaphore

This is the handle of the created event object.

The VkEventCreateInfo structure has three parameters: the first parameter (sType) describes the type information of this create info data structure; it must be VK_STRUCTURE_TYPE_EVENT_CREATE_INFO. The second parameter is pNext; this could be a valid pointer to an extension-specific structure or could be NULL. The last parameter is flag value (flags), which is currently not being used and reserved for future purposes. Following is the syntax of this structure:

typedef struct VkEventCreateInfo { 
    VkStructureType       sType; 
    const void*           pNext; 
    VkEventCreateFlags    flags; 
}   VkEventCreateInfo; 

Destroying the event object: The created event is destroyed using vkDestroyEvent()when the event is no longer in use. This API takes three parameters—the first parameter device is the logical device that will destroy the event object specified by the second parameter (semaphore). The last parameter (pAllocator) manages host memory deallocation.

void vkDestroyEvent( 
    VkDevice                     device, 
    VkEvent                      event, 
    const VkAllocationCallbacks* pAllocator); 

Querying the event status: The event can be queried to check whether it is in the signaled or nonsignaled state. This is done using the vkGetEventStatus()API; the first parameter (device) of this API is the logical device (VkDevice) that owns the event object; the second parameter (event) is the event handle whose status is being queried. The following is the syntax of this API:

VkResult vkGetEventStatus(   
    VkDevice      device, 
    VkEvent       event); 

This API returns VK_EVENT_SET, which indicates the event is signaled; for an unsignaled event, it returns VK_EVENT_RESET.

Setting and resetting events: Events can be set using vkSetEvent() and vkResetEvent(). Both APIs accept the same input parameters as described above for the vkGetEventStatus() API; for more information, please refer to vkSetEvent and vkResetEvent syntax:

vkSetEvent API

vkResetEvent API

VkResult vkCmdSetEvent(
VkDevice device,
VkEvent  event);

VkResult vkSetEvent(
VkDevice device,
VkEvent  event);

Signaling and unsignaling an event from a device: An event can be updated to set or reset on the device using command buffers. The vkCmdSetEvent()and vkCmdResetEvent()APIs are used to signal and unsignal the events, respectively; following is the syntax of these APIs:

vkCmdSetEvent API

vkCmdResetEvent API

VkResult vkCmdSetEvent(
VkCommandBuffer
commandBuffer,
VkEvent
event,
VkPipelineStageFlags
stageMask);

VkResult vkCmdResetEvent(
VkCommandBuffer
commandBuffer,
VkEvent
event,
VkPipelineStageFlags
stageMask);

Both APIs accept the following three parameters: the first parameter (commandBuffer) specifies the command buffer in which this command will be recorded. The second parameter (event) indicates the handle of the event object, which needs to be signaled or unsignaled. The last parameter (stageMask) is the VkPipelineStageFlags pipeline stage, indicating the point at which the event's state will be updated.

Waiting on event objects: One or more event objects can be waited upon to signal using the vkCmdWaitEvents()API. The following is the syntax of this API:

void vkCmdWaitEvents( 
    VkCommandBuffer               commandBuffer, 
    uint32_t                      eventCount, 
    const VkEvent*                pEvents, 
    VkPipelineStageFlags          srcStageMask, 
    VkPipelineStageFlags          dstStageMask, 
    uint32_t                      memoryBarrierCount, 
    const VkMemoryBarrier*        pMemoryBarriers, 
    uint32_t                      bufferMemoryBarrierCount, 
    const VkBufferMemoryBarrier*  pBufferMemoryBarriers, 
    uint32_t                      imageMemoryBarrierCount, 
    const VkImageMemoryBarrier*   pImageMemoryBarriers); 

The fields and a description of each parameter follow:

Parameter

Description

commandBuffer

This is the command buffer object into which this command will be captured or recorded.

eventCount

This is the number of event objects to be waited upon.

pEvents

This is the array of the VkEvent objects; the size of the array must be equal to eventCount.

srcStageMask

This is the bitwise mask field that specifies the pipeline stages that will signal the event objects specified in the pEvents array.

dstStageMask

This is the bitwise mask field that specifies the pipeline stage at which the waiting should be performed.

memoryBarrierCount

This refers to the number of memory barriers.

pMemoryBarriers

This is the VkBufferMemoryBarreir object array that has the number of elements equal to memoryBarrierCount.

bufferMemoryBarrierCount

This refers to the number of buffer memory barriers.

pBufferMemoryBarriers

This refers to the VkMemoryBarreir object array that has the number of elements equal to bufferMemoryBarrierCount.

imageMemoryBarrierCount

This refers to the number of image type memory barriers.

pImageMemoryBarriers

This refers to the VkImageMemoryBarrier object array that has the number of elements equal to imageMemoryBarrierCount.

Note

Barrier have already been discussed and implemented in Chapter 6, Allocating Image Resources and Building a Swapchain with WSI; for more information, please refer to the Image layout transition with memory barriers section.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.200.3