Series of articles developed by Vulkan:

  1. Invader Vulkan mobile Development (I) of this life prelife
  1. Vulkan Mobile Development (II) : Understanding the rendering process
  1. Instance & Device & Queue for Vulkan mobile development

This article continues learning about the component in Vulkan: the Command Buffer.

In the previous article, we have created three components, Instance, Device and Queue, and learned that Queue is used as a bridge to communicate with physical devices. The specific communication process requires command-buffer, which is a collection of several commands. We submit the command-buffer to the Queue before it is processed by the physical device GPU.

The Command – the Pool components

Before creating a Command-Buffer, create a Command-Pool component to allocate the Command-Buffer from the Command-Pool.

Create a VkXXXXCreateInfo structure, and see the official documentation for the definition of each parameter.

    // Create a command-pool component
    VkCommandPool command_pool;
    VkCommandPoolCreateInfo poolCreateInfo = {};
    poolCreateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
    // We can see that command-pool is associated with Queue
    poolCreateInfo.queueFamilyIndex = info.graphics_queue_family_index;
    // Identifies some behavior of the command buffer
    poolCreateInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
    // The call to create the function
    vkCreateCommandPool(info.device, &poolCreateInfo, nullptr, &command_pool);
Copy the code

There are a few parameters to note:

  1. queueFamilyIndexThe parameter is createQueueThe one I chosequeueFlagsVK_QUEUE_GRAPHICS_BITIndex fromCommand-PoolIs assigned toCommand-BufferMust be submitted to the sameQueueIn the.
  2. flagsThere are the following options, specified separatelyCommand-BufferDifferent characteristics of:
typedef enum VkCommandPoolCreateFlagBits {
    VK_COMMAND_POOL_CREATE_TRANSIENT_BIT = 0x00000001,
    VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT = 0x00000002,
    VK_COMMAND_POOL_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkCommandPoolCreateFlagBits;
Copy the code
  • VK_COMMAND_POOL_CREATE_TRANSIENT_BIT

    • According to theCommand-BufferIs short-lived and can be reset or released at short notice
  • VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT

    • Says from theCommand-PoolIn the distribution ofCommand-BufferCan be achieved byvkResetCommandBufferorvkBeginCommandBufferMethod, cannot be called if the identity bit is not setvkResetCommandBufferMethod to reset.

The Command – Buffer components

Next comes from the Command – Command – Buffer Pool in distribution, through VkCommandBufferAllocateInfo function.

You first need a VkCommandBufferAllocateInfo structure said distribution of the information you need.

typedef struct VkCommandBufferAllocateInfo {
    VkStructureType         sType;
    const void*             pNext;
    VkCommandPool           commandPool;    // Corresponds to the command-pool created above
    VkCommandBufferLevel    level;
    uint32_t                commandBufferCount; // Number of created items
} VkCommandBufferAllocateInfo;
Copy the code

Here is another parameter to note:

  • VkCommandBufferLevelThe specifiedCommand-BufferThe level of.

The following levels are available:

typedef enum VkCommandBufferLevel {
    VK_COMMAND_BUFFER_LEVEL_PRIMARY = 0,
    VK_COMMAND_BUFFER_LEVEL_SECONDARY = 1,
    VK_COMMAND_BUFFER_LEVEL_BEGIN_RANGE = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
    VK_COMMAND_BUFFER_LEVEL_END_RANGE = VK_COMMAND_BUFFER_LEVEL_SECONDARY,
    VK_COMMAND_BUFFER_LEVEL_RANGE_SIZE = (VK_COMMAND_BUFFER_LEVEL_SECONDARY - VK_COMMAND_BUFFER_LEVEL_PRIMARY + 1),
    VK_COMMAND_BUFFER_LEVEL_MAX_ENUM = 0x7FFFFFFF
} VkCommandBufferLevel;
Copy the code

In general, VK_COMMAND_BUFFER_LEVEL_PRIMARY is fine.

The specific creation code is as follows:

    VkCommandBuffer commandBuffer[2];
    VkCommandBufferAllocateInfo command_buffer_allocate_info{};
    command_buffer_allocate_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    command_buffer_allocate_info.commandPool = command_pool;
    command_buffer_allocate_info.commandBufferCount = 2;
    command_buffer_allocate_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    vkAllocateCommandBuffers(info.device, &command_buffer_allocate_info, commandBuffer);
Copy the code

The life cycle of the command-buffer

After creating the command-buffer, take a look at its life cycle as shown below:

  • The Initial state

When the command-buffer is created, it is initialized. From this state, you can achieve a Recording state, in addition, if reset, will return to the state.

  • Recording state

Call the vkBeginCommandBuffer method from the Initial state to this state. Once in that state, a series of method logging commands such as vkCmd* can be invoked.

  • The Executable state

The vkEndCommandBuffer method is called from a Recording state to a Recording state in which a CommandBuffer can be committed or reset.

  • Pending state

This state occurs when the Command-Buffer is committed to the Queue. The physical device may be processing recorded commands in this state, so do not change the Command Buffer at this time. When the processing is complete, the Command Buffer may return to the Executable state or Invalid state.

  • Invalid state

Some operations put the Command-Buffer into this state, where it can only be reset or released.

The recording and submission of command-buffer

Now you can try logging some commands to the Queue. The command logging process looks like this:

Between the vkBeginCommandBuffer and vkEndCommandBuffer methods, you can record the commands related to rendering. Here, you can create the commit directly without thinking about the intermediate process.

The begin stage

        VkCommandBufferBeginInfo beginInfo = {};
        beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
        beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
        vkBeginCommandBuffer(commandBuffer[0], &beginInfo);
Copy the code

First, you need to create a VkCommandBufferBeginInfo structure to represent the information starting from the CommandBuffer.

The argument to note here is flags, which indicates the purpose of the command-buffer,

typedef enum VkCommandBufferUsageFlagBits {
    VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT = 0x00000001,
    VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT = 0x00000002,
    VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT = 0x00000004,
    VK_COMMAND_BUFFER_USAGE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkCommandBufferUsageFlagBits;
Copy the code
  • VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT
    • Indicates that the command-buffer is committed only once, then reset and re-logged each time it is committed

End stage

A direct call to the vkEndCommandBuffer method terminates the recording, at which point it can be committed.

    vkEndCommandBuffer(commandBuffer[0]);
Copy the code

Buffer to submit

The Command-Buffer is submitted to the Queue using the vkQueueSubmit method.

Again, we need to create a VkSubmitInfo structure:

typedef struct VkSubmitInfo {
    VkStructureType                sType;
    const void*                    pNext;
    uint32_t                       waitSemaphoreCount;  // The number of Semaphore waiting
    const VkSemaphore*             pWaitSemaphores;     // Wait for pointer to Semaphore array
    const VkPipelineStageFlags*    pWaitDstStageMask;       // At which stage to wait
    uint32_t                       commandBufferCount;  // The number of Command buffers submitted
    const VkCommandBuffer*         pCommandBuffers;      // Specifies the Command-Buffer array pointer
    uint32_t                       signalSemaphoreCount;    // Number of Semaphore notifications after execution
    const VkSemaphore*             pSignalSemaphores;       // Semaphore array pointer notified after execution
} VkSubmitInfo;
Copy the code

It has a large number of parameters and involves the synchronization relationship between Command-Buffer. This section is briefly described here and discussed in detail later.

As shown below, There are four mechanisms in Vulkan to ensure synchronization: Semaphore, Fences, Event and Barrier.

A few words about Semaphore and Fence.

  • Semaphore

    • SemaphoreThe role of is mainly used toQueueIn the submissionCommand-BufferTo achieve synchronization. For example, someoneCommand-Buffer-BOne phase of execution requires waiting for anotherCommand-Buffer-AResults after successful execution, andCommand-Buffer-CAt some stage you have to waitCommand-Buffer-BIs executed as a result, then it should be usedSemaphoreMechanism to achieve synchronization;
    • At this timeCommand-Buffer-BSubmitted to theQueueYou need twoVkSemaphor, one that means it needs to waitSemaphoreAnd specify at which stage to wait; One is when it’s doneSemaphore.
  • Fence

    • FenceIs used to ensure synchronization between physical devices and applications, for example, toQueueIn the filedCommand-BufferAfter that, the actual execution is handed over to the physical device, which is an asynchronous process that the application uses if it wants to wait for the execution to finishFenceMechanism.

Semaphore and Fence have something in common, but are used differently, as shown in the figure below.

Semaphore and Fence are created as follows, not much different from the usual Vulkan object creation call:

    / / create a Semaphore
    VkSemaphore imageAcquiredSemaphore;
    VkSemaphoreCreateInfo semaphoreCreateInfo = {};
    semaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
    vkCreateSemaphore(info.device, &semaphoreCreateInfo, nullptr, &imageAcquiredSemaphore);

    / / create a Fence
    VkFence drawFence;
    VkFenceCreateInfo fenceCreateInfo = {};
    fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
    Signaled state indicates the state of the Fence, and if it is not set or 0 indicates unsignaled state
    fence_info.flags = 0; 
    vkCreateFence(info.device, &fenceCreateInfo, nullptr, &drawFence);
Copy the code

Nullptr = nullPTR = nullPTR = nullPTR = nullPTR = nullPTR = nullPTR = nullPTR

    // Simple submission process
    // Start recording
    VkCommandBufferBeginInfo beginInfo1 = {};
    beginInfo1.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    beginInfo1.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
    vkBeginCommandBuffer(commandBuffer[0], &beginInfo1);

    // omit the intermediate vkCmdXXXX series methods
    // End the record
    vkEndCommandBuffer(commandBuffer[0]);

    VkSubmitInfo submitInfo1 = {};
    submitInfo1.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    // pWaitSemaphores and pSignalSemaphores are not set, just commit
    submitInfo1.commandBufferCount = 1;
    submitInfo1.pCommandBuffers = &commandBuffer[0];

    // Note that the last parameter is temporarily set to VK_NULL_HANDLE, or it can be set to Fence for synchronization
    vkQueueSubmit(info.queue.1, &submitInfo1, VK_NULL_HANDLE);
Copy the code

This completes the command-buffer commit to Queue, leaving out Semaphores and Fences synchronization. You can also add them.

The last argument in vkQueueSubmit is set to VK_NULL_HANDLE, which is a method set to NULL in Vulkan (which is actually an integer 0), as well as the Fence, Semaphore: indicates that we wait for the command-buffer to terminate in a Queue. Although a command-buffer can also terminate in a Semaphore, the two methods are used in different scenarios.

Back to the creation of a Fence, there is a flags parameter that indicates the state of the Fence, which has the following two states:

  • signaled state
    • If the flags parameter is VK_FENCE_CREATE_SIGNALED_BIT, it indicates that the flags is in this state after creation.
  • unsignaled state
    • Default status.

When the last argument to vkQueueSubmit is passed to the Fence, the Fence waits for the command-buffer execution to end.

// wait fence to enter the signaled state on the host
// Wrong use of waitForFences because it is not a blocking method
// VkResult res = vkWaitForFences(info.device, 1, &fence, VK_TRUE, UINT64_MAX);
    VkResult res;
    do {
        res = vkWaitForFences(info.device, 1, &fence, VK_TRUE, UINT64_MAX);
    } while (res == VK_TIMEOUT);
Copy the code

Waiting on the Fence to go into signaled State, the call is placed on a while loop. Waiting on the Fence are released from the while loop and wait for the result to become invalid. VK_SUCCESS is returned only when the result meets the requirements.

Signaled from Unsignaled State to Wait State after the execution of the Command Buffer ends, the Fence parameter passed in triggers the vkWaitForFences call ending the loop.

This is how a Fence is used, and for an example of synchronization between command-buffers via Semaphore, see a subsequent article.

conclusion

This article focuses on the use and submission of command-buffer and covers some synchronization mechanisms for Vulkan.

Specific operations related to rendering should be recorded between Command and Buffer, and then submitted to Queue after recording, so that GPU can perform specific operations. Of course, the specific execution is an asynchronous process, requiring the synchronization mechanism.

Both Semaphore and Fence can be synchronized, but in different scenarios.

Welcome to pay attention to wechat public number: [paper talk], get the latest article push ~~~