Creating Memory Objects#

Creating memory objects in Vulkan is relatively straightforward, although the form and structure is slightly different to other graphics APIs’.

A graphical application needs some way of storing data that will be used by the GPU during rendering. This is usually achieved by allocating areas of GPU memory and then loading data from the CPU to these buffers.

Buffers are different to Images. Buffers are more or less raw memory objects accessed linearly. Images are blocks of memory that are accessed through specialised hardware and are heavily optimised for use with visual information and especially the texture mapping technique.

In this example, three different types of memory objects are created and used:

  • Vertex buffer.

  • Uniform buffer.

  • Texture image.

Alongside these objects, the shaders also need some way of accessing the data stored in them. In Vulkan, this is handled with descriptors and descriptor sets.

The exact details about the specific uses of each memory object will be explained in their sections. This includes information about what they store and how they are created. The code examples will demonstrate how to create descriptor sets that provide access to both the uniform buffer and the texture image. A descriptor set is not required for the vertex buffer because there is a specific stage of the pipeline that deals with vertex input. This stage will be covered during pipeline initialisation.

Creating a Vertex Buffer#

A vertex buffer is a reserved section of GPU memory that specifically stores vertex data. In this example, each element of the vertex buffer has two pieces of information:

  • A set of 3D homogenous position co-ordinates in (x,y,z) to determine where the vertex is located in the 3D space.

  • A set of texture co-ordinates in (u,v) to tell the fragment shader where to sample the texture in order to determine how to colour all of the fragments within the primitive.

The vertex shader that was created in Creating the Shaders has these two vertex attributes as an input: a four element vector for the vertex’s position and a two element vector for the vertex’s texture co-ordinates. These are attributes and represent properties of the vertex that the shader can process. The application needs to create a vertex buffer to store the vertex data which will be fed into the shader’s vertex attribute variables.

In this specific example, the vertex data is hard-coded as only a simple triangle consisting of three vertices is being drawn. Usually, more complex applications would load this data in from a 3D model file. The actual memory object is created using a helper function createBuffer().

View initVertexBuffers() in context.

View createBuffer() in context.

void VulkanHelloAPI::initVertexBuffers()
{
    // Calculate the size of the vertex buffer to be passed to the vertex shader.
    appManager.vertexBuffer.size = sizeof(Vertex) * 3;

    // Set the values for the triangle's vertices.
    Vertex triangle[3];
    triangle[0] = { -0.5f, -0.288f, 0.0f, 1.0f, 0.0f, 0.0f };
    triangle[1] = { .5f, -.288f, 0.0f, 1.0f, 1.0f, 0.0f };
    triangle[2] = { 0.0f, .577f, 0.0f, 1.0f, 0.5f, 1.0f };

    // Create the buffer that will hold the data and be passed to the shaders.
    createBuffer(appManager.vertexBuffer, reinterpret_cast<uint8_t*>(triangle), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
}

void VulkanHelloAPI::createBuffer(BufferData& inBuffer, const uint8_t* inData, const VkBufferUsageFlags& inUsage)
{
    // Declare and populate a buffer creation info struct.
    // This tells the API the size of the buffer and how it is going to be used. Additionally, it specifies whether the
    // buffer is going to be accessed by multiple queue families at the same time and if so, what those queue families are.
    VkBufferCreateInfo bufferInfo = {};
    bufferInfo.flags = 0;
    bufferInfo.pNext = nullptr;
    bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
    bufferInfo.size = inBuffer.size;
    bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
    bufferInfo.usage = inUsage;
    bufferInfo.pQueueFamilyIndices = nullptr;
    bufferInfo.queueFamilyIndexCount = 0;

    // Create the buffer object itself.
    debugAssertFunctionResult(vk::CreateBuffer(appManager.device, &bufferInfo, nullptr, &inBuffer.buffer), "Buffer Creation");

    // Define a struct to hold the memory requirements for the buffer.
    VkMemoryRequirements memoryRequirments;

    // Extract the memory requirements for the buffer.
    vk::GetBufferMemoryRequirements(appManager.device, inBuffer.buffer, &memoryRequirments);

    // Populate an allocation info struct with the memory requirement size.
    VkMemoryAllocateInfo allocateInfo = {};
    allocateInfo.pNext = nullptr;
    allocateInfo.memoryTypeIndex = 0;
    allocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
    allocateInfo.allocationSize = memoryRequirments.size;

    // Check if the memory that is going to be used supports the necessary flags for the usage of the buffer.
    // In this case it needs to be "Host Coherent" in order to be able to map it. If it is not, find a compatible one.
    bool pass = getMemoryTypeFromProperties(appManager.deviceMemoryProperties, memoryRequirments.memoryTypeBits,
    VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, &(allocateInfo.memoryTypeIndex));
    if (pass)
    {
    // This pointer will be used to pass the data into the buffer.
    uint8_t* pData;

    // Allocate the memory necessary for the data.
    debugAssertFunctionResult(vk::AllocateMemory(appManager.device, &allocateInfo, nullptr, &(inBuffer.memory)), "Allocate Buffer Memory");

    // Save the data in the buffer struct.
    inBuffer.bufferInfo.range = memoryRequirments.size;
    inBuffer.bufferInfo.offset = 0;
    inBuffer.bufferInfo.buffer = inBuffer.buffer;

    VkMemoryPropertyFlags flags = appManager.deviceMemoryProperties.memoryTypes[allocateInfo.memoryTypeIndex].propertyFlags;
    inBuffer.memPropFlags = flags;

    if (inData != nullptr)
    {
    // Map data to the memory.
    // inBuffer.memory is the device memory handle.
    // memoryRequirments.size is the size of the memory to be mapped, in this case it is the entire buffer.
    // &pData is an output variable and will contain a pointer to the mapped data.
    debugAssertFunctionResult(vk::MapMemory(appManager.device, inBuffer.memory, 0, inBuffer.size, 0, reinterpret_cast<void**>(&pData)), "Map Buffer Memory");

    // Copy the data into the pointer mapped to the memory.
    memcpy(pData, inData, inBuffer.size);

    VkMappedMemoryRange mapMemRange = {
    VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,
    nullptr,
    inBuffer.memory,
    0,
    inBuffer.size,
    };

    // ONLY flush the memory if it does not support VK_MEMORY_PROPERTY_HOST_COHERENT_BIT.
    if (!(flags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) { vk::FlushMappedMemoryRanges(appManager.device, 1, &mapMemRange); }
    }

    // Associate the allocated memory with the previously created buffer.
    // Mapping and binding do not need to occur in a particular order. This step could just as well be performed before mapping
    // and populating.
    debugAssertFunctionResult(vk::BindBufferMemory(appManager.device, inBuffer.buffer, inBuffer.memory, 0), "Bind Buffer Memory");
    }
}

createBuffer() is a function used to create buffers. It is responsible for creating a buffer object, allocating the memory, mapping this memory, and copying the data into the buffer. The usage flag parameter determines the types of buffers supported.

These are steps that need to be taken when creating a buffer of any kind, and so in this example, a helper function is used to avoid repeating several lines of code.

createBuffer() takes the following steps:

  1. Create a VkBuffer object.

  2. Allocate a portion of memory which matches the buffer’s size requirements.

With the buffer created, it can be populated with initial data through the following steps:

  1. Bind the buffer object to this memory.

  2. Map the allocated memory.

  3. Copy the data into the allocated memory.

Creating a Uniform Buffer#

Uniform variables in shaders act as parameters that can be set by the developer. They will remain constant for all vertices or fragments during a render pass. A uniform buffer contains the data that the shader reads as these variables.

The transformation matrices are a product of the fixed orthographic projection matrix and a rotation matrix. The rotation matrix is calculated from a rotation value which is incremented every frame. The rotation matrix changes between frames and therefore the transformation matrix must be recalculated each time. This implies that the uniform buffers which are used to store the transformation matrices have to be dynamic because they are can be bound at different times. This is so that the same uniform buffer can be used for different frames by binding a different part of it.

In this and some other common cases, because the contents of the buffers need to change every frame, the number of uniform buffers required will be equal to the number of images in the swapchain. This is because each image corresponds to a different frame with a slightly different rotation value, and it is necessary for each buffer’s contents to remain undisturbed/constant/not be overwritten/valid until the frame is finished. Instead of creating several buffers, initUniformBuffers() creates one large dynamic uniform buffer with a slice allocated for each image. The data for each individual buffer can then be accessed by using an offset. The large buffer is created using the same createBuffer helper function defined above. View in context.

void VulkanHelloAPI::initUniformBuffers()
{
    // Vulkan requires that when updating a descriptor of type VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER or VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, the
    // offset specified is an integer multiple of the minimum required alignment in bytes for the physical device. This also applied to any dynamic alignments used.
    size_t minimumUboAlignment = static_cast<size_t>(appManager.deviceProperties.limits.minUniformBufferOffsetAlignment);

    // The dynamic buffers will be used as uniform buffers. These are later used with a descriptor of type VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC and VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER.
    VkBufferUsageFlags usageFlags = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;

    {
        // Using the minimum uniform buffer offset alignment, the minimum buffer slice size is calculated based on the size of the intended data, or more specifically
        // the size of the smallest chunk of data which may be mapped or updated as a whole.
        // In this case the size of the intended data is the size of a 4 by 4 matrix.
        size_t bufferDataSizePerSwapchain = sizeof(float) * 4 * 4;
        bufferDataSizePerSwapchain = static_cast<uint32_t>(getAlignedDataSize(bufferDataSizePerSwapchain, minimumUboAlignment));

        // Calculate the size of the dynamic uniform buffer.
        // This buffer will be updated on each frame and must therefore be multi-buffered to avoid issues with using partially updated data, or updating data already in use.
        // Rather than allocating multiple (swapchain) buffers, a larger buffer is allocated and a slice of this buffer will be used per swapchain. This works as
        // long as the buffer is created taking into account the minimum uniform buffer offset alignment.
        appManager.dynamicUniformBufferData.size = bufferDataSizePerSwapchain * appManager.swapChainImages.size();

        // Create the buffer, allocate the device memory, and attach the memory to the newly created buffer object.
        createBuffer(appManager.dynamicUniformBufferData, nullptr, usageFlags);
        appManager.dynamicUniformBufferData.bufferInfo.range = bufferDataSizePerSwapchain;

        // Note that only memory created with the memory property flag VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT can be mapped.
        // vkMapMemory retrieves a host virtual address pointer to a region of a mappable memory object.
        debugAssertFunctionResult(vk::MapMemory(appManager.device, appManager.dynamicUniformBufferData.memory, 0, appManager.dynamicUniformBufferData.size, 0,
        &appManager.dynamicUniformBufferData.mappedData),
        "Could not map the uniform buffer.");
    }
}

Creating a Texture Image#

The application needs a way of determining how to colour the fragments inside a triangle. A simple way of doing this is with texture mapping. Texture mapping is a graphics technique where the fragment shader samples a 2D image (an image used this way is called a texture) for the colour information at a particular pixel and then applies this colour to the corresponding fragment in the triangle.

The way that the texture maps onto the triangle is determined by the texture coordinates at each vertex. These are written to the vertex buffer along with the position coordinates. These texture coordinates refer to the locations on the 2D texture which should be sampled to determine the colour of the object at the position of the vertex. They also act as anchor points from which the texture co-ordinates of each of the remaining fragments in a triangle can be interpolated.

Vulkan offers a VkImage object which can hold the texel data and allow the fragment shader to access it.

The code example demonstrates the process to create an image object and upload texture data to it. The texture data is not being loaded from a file, but rather a custom function that generates a checkered pattern on-the-fly. The process would be the same regardless of the origin of the data. The code makes use of the same helper function for vertex buffers and uniform buffers. This procedure is fairly complex because you need to ensure the texture data is laid out correctly in GPU memory.

The initTexture() method performs multiple steps, sorted into two main categories:

  1. Creating the texture:

    1. Create the VkImage.

    2. Create the VkDeviceMemory object.

    3. Bind the memory to the image.

  2. Uploading the data to the texture:

    1. Create a staging buffer.

    2. Determine its memory requirements and create the backing memory as a VkDeviceMemory object.

    3. Map the staging buffer and copy the image data into it.

    4. Perform a copy from the staging buffer to the image using the vkCmdCopyBufferToImage command to transfer the data. This requires a command buffer and related objects.

The reason the data must be copied from a staging buffer is because a texture is stored in the GPU in an implementation-defined way, which may be different to its layout on disk/CPU. Thus, direct mapping of memory and writing of data is not correct, whereas vkCmdCopyBufferToImage() guarantees the correct translation or swizzling of the data. View in context.

void VulkanHelloAPI::initTexture()
{
    // Set the width and height of the texture image.
    appManager.texture.textureDimensions.height = 256;
    appManager.texture.textureDimensions.width = 256;
    appManager.texture.data.resize(appManager.texture.textureDimensions.width * appManager.texture.textureDimensions.height * 4);

    // This function generates a texture pattern on-the-fly into a block of CPU-side memory: appManager.texture.data.
    generateTexture();

    // The BufferData struct has been defined in this application to hold the necessary data for the staging buffer.
    BufferData stagingBufferData;
    stagingBufferData.size = appManager.texture.data.size();

    // Use the buffer creation function to generate a staging buffer. The VK_BUFFER_USAGE_TRANSFER_SRC_BIT flag is passed to specify that the buffer
    // is going to be used as the source buffer of a transfer command.
    createBuffer(stagingBufferData, appManager.texture.data.data(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT);

    // Create the image object.
    // The format is set to the most common format, R8G8B8_UNORM, 8-bits per channel, unsigned, and normalised.
    // Additionally, the dimensions of the image, the number of mipmap levels, the intended usage of the image, the number of samples per texel,
    // and whether this image can be accessed concurrently by multiple queue families are all also set here.
    // Some of the other parameters specified include the tiling and the initialLayout.
    // The tiling parameter determines the layout of texel blocks in memory. This should be set
    // to VK_IMAGE_TILING_OPTIMAL for images used as textures in shaders.
    // The initialLayout parameter is set to VK_IMAGE_LAYOUT_UNDEFINED but the layout will be transitioned
    // later using a barrier.
    VkImageCreateInfo imageInfo = {};
    imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
    imageInfo.imageType = VK_IMAGE_TYPE_2D;
    imageInfo.flags = 0;
    imageInfo.pNext = nullptr;
    imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
    imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
    imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
    imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
    imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
    imageInfo.extent = { appManager.texture.textureDimensions.width, appManager.texture.textureDimensions.height, 1 };
    imageInfo.mipLevels = 1;
    imageInfo.arrayLayers = 1;

    debugAssertFunctionResult(vk::CreateImage(appManager.device, &imageInfo, nullptr, &appManager.texture.image), "Texture Image Creation");

    // Get the memory allocation requirements for the image.
    // These are used to allocate memory for the image that has just been created.
    VkMemoryRequirements memoryRequirments;
    vk::GetImageMemoryRequirements(appManager.device, appManager.texture.image, &memoryRequirments);

    // Populate a memory allocation info struct with the memory requirements size for the image.
    VkMemoryAllocateInfo allocateInfo = {};
    allocateInfo.pNext = nullptr;
    allocateInfo.memoryTypeIndex = 0;
    allocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
    allocateInfo.allocationSize = memoryRequirments.size;

    // This helper function queries available memory types to find memory with the features that are suitable for a sampled
    // image. Device Local memory is the preferred choice.
    getMemoryTypeFromProperties(appManager.deviceMemoryProperties, memoryRequirments.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, &(allocateInfo.memoryTypeIndex));

    // Use all of this information to allocate memory with the correct features for the image and bind the memory to the texture buffer.
    debugAssertFunctionResult(vk::AllocateMemory(appManager.device, &allocateInfo, nullptr, &appManager.texture.memory), "Texture Image Memory Allocation");
    debugAssertFunctionResult(vk::BindImageMemory(appManager.device, appManager.texture.image, appManager.texture.memory, 0), "Texture Image Memory Binding");

    // Specify the region which should be copied from the texture. In this case it is the entire image, so
    // the texture width and height are passed as extents.
    VkBufferImageCopy copyRegion = {};
    copyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    copyRegion.imageSubresource.mipLevel = 0;
    copyRegion.imageSubresource.baseArrayLayer = 0;
    copyRegion.imageSubresource.layerCount = 1;
    copyRegion.imageExtent.width = static_cast<uint32_t>(appManager.texture.textureDimensions.width);
    copyRegion.imageExtent.height = static_cast<uint32_t>(appManager.texture.textureDimensions.height);
    copyRegion.imageExtent.depth = 1;
    copyRegion.bufferOffset = 0;

    // Allocate a command buffer from the command pool. This command buffer will be used to execute the copy operation.
    // The allocation info struct below specifies that a single primary command buffer needs
    // to be allocated. Primary command buffers can be contrasted with secondary command buffers
    // which cannot be submitted directly to queues but instead are executed as part of a primary command
    // buffer.
    // The command pool referenced here was created in initCommandPoolAndBuffer().
    VkCommandBuffer commandBuffer;

    VkCommandBufferAllocateInfo commandAllocateInfo = {};
    commandAllocateInfo.commandPool = appManager.commandPool;
    commandAllocateInfo.pNext = nullptr;
    commandAllocateInfo.commandBufferCount = 1;
    commandAllocateInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    commandAllocateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;

    debugAssertFunctionResult(vk::AllocateCommandBuffers(appManager.device, &commandAllocateInfo, &commandBuffer), "Allocate Command Buffers");

    // Begin recording the copy commands into the command buffer.
    VkCommandBufferBeginInfo commandBufferBeginInfo = {};
    commandBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    commandBufferBeginInfo.flags = 0;
    commandBufferBeginInfo.pNext = nullptr;
    commandBufferBeginInfo.pInheritanceInfo = nullptr;

    debugAssertFunctionResult(vk::BeginCommandBuffer(commandBuffer, &commandBufferBeginInfo), "Begin Image Copy to Staging Buffer Command Buffer Recording");

    // Specify the sub resource range of the image. In the case of this image, the parameters are default, with one mipmap level and layer,
    // because the image is very simple.
    VkImageSubresourceRange subResourceRange = {};
    subResourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    subResourceRange.baseMipLevel = 0;
    subResourceRange.levelCount = 1;
    subResourceRange.layerCount = 1;

    // A memory barrier needs to be created to make sure that the image layout is set up for a copy operation.
    // The barrier will transition the image layout from VK_IMAGE_LAYOUT_UNDEFINED to
    // VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL. This new layout is optimal for images which are the destination
    // of a transfer command.
    VkImageMemoryBarrier copyMemoryBarrier = {};
    copyMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    copyMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    copyMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    copyMemoryBarrier.image = appManager.texture.image;
    copyMemoryBarrier.subresourceRange = subResourceRange;
    copyMemoryBarrier.srcAccessMask = 0;
    copyMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

    // Use the pipeline barrier defined above.
    vk::CmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &copyMemoryBarrier);

    // Copy the staging buffer data to the image that was just created.
    vk::CmdCopyBufferToImage(commandBuffer, stagingBufferData.buffer, appManager.texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copyRegion);

    // Create a barrier to make sure that the image layout is shader read-only.
    // This barrier will transition the image layout from VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL to
    // VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL.
    VkImageMemoryBarrier layoutMemoryBarrier = {};
    layoutMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    layoutMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    layoutMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    layoutMemoryBarrier.image = appManager.texture.image;
    layoutMemoryBarrier.subresourceRange = subResourceRange;
    layoutMemoryBarrier.srcAccessMask = 0;
    layoutMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

    // Use a pipeline barrier to change the image layout to be optimised for reading by the shader.
    vk::CmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &layoutMemoryBarrier);

    // End the recording of the command buffer.
    debugAssertFunctionResult(vk::EndCommandBuffer(commandBuffer), "End Image Copy to Staging Buffer Command Buffer Recording");

    // Create a fence object which will signal when all of the commands in this command buffer have been completed.
    VkFence copyFence;
    VkFenceCreateInfo copyFenceInfo = {};
    copyFenceInfo.flags = 0;
    copyFenceInfo.pNext = nullptr;
    copyFenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;

    debugAssertFunctionResult(vk::CreateFence(appManager.device, &copyFenceInfo, nullptr, &copyFence), "Image Copy to Staging Buffer Fence Creation");

    // Finally, submit the command buffer to the graphics queue to get the GPU to perform the copy operations.
    // When submitting command buffers, it is possible to set wait and signal semaphores to control synchronisation. These
    // are not used here but they will be used later during rendering.
    VkSubmitInfo submitInfo = {};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitInfo.pNext = nullptr;
    submitInfo.pWaitDstStageMask = nullptr;
    submitInfo.waitSemaphoreCount = 0;
    submitInfo.pWaitSemaphores = nullptr;
    submitInfo.signalSemaphoreCount = 0;
    submitInfo.pSignalSemaphores = nullptr;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &commandBuffer;

    debugAssertFunctionResult(vk::QueueSubmit(appManager.graphicQueue, 1, &submitInfo, copyFence), "Submit Image Copy to Staging Buffer Command Buffer");

    // Wait for the fence to be signalled. This ensures the command buffer has finished executing.
    debugAssertFunctionResult(vk::WaitForFences(appManager.device, 1, &copyFence, VK_TRUE, FENCE_TIMEOUT), "Image Copy to Staging Buffer Fence Signal");

    // After the image is complete and all the texture data has been copied, an image view needs to be created to make sure
    // that the API can understand what the image is. For example, information can be provided on the format or view type.
    // The image parameters used here are the same as for the swapchain images created earlier.
    VkImageViewCreateInfo imageViewInfo = {};
    imageViewInfo.flags = 0;
    imageViewInfo.pNext = nullptr;
    imageViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
    imageViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
    imageViewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
    imageViewInfo.image = appManager.texture.image;
    imageViewInfo.subresourceRange.layerCount = 1;
    imageViewInfo.subresourceRange.levelCount = 1;
    imageViewInfo.subresourceRange.baseArrayLayer = 0;
    imageViewInfo.subresourceRange.baseMipLevel = 0;
    imageViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    imageViewInfo.components.r = VK_COMPONENT_SWIZZLE_R;
    imageViewInfo.components.g = VK_COMPONENT_SWIZZLE_G;
    imageViewInfo.components.b = VK_COMPONENT_SWIZZLE_B;
    imageViewInfo.components.a = VK_COMPONENT_SWIZZLE_A;

    debugAssertFunctionResult(vk::CreateImageView(appManager.device, &imageViewInfo, nullptr, &appManager.texture.view), "Texture Image View Creation");

    // Create a texture sampler.
    // The sampler will be needed to sample the texture data and pass
    // it to the fragment shader during the execution of the rendering phase.
    // The parameters specified below define any filtering or transformations which are applied before
    // passing the colour data to the fragment shader.
    // In this case, anisotropic filtering is turned off and if the fragment shader samples outside of the image co-ordinates
    // it will return the colour at the nearest edge of the image.
    VkSamplerCreateInfo samplerInfo = {};
    samplerInfo.flags = 0;
    samplerInfo.pNext = nullptr;
    samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
    samplerInfo.magFilter = VK_FILTER_LINEAR;
    samplerInfo.minFilter = VK_FILTER_LINEAR;
    samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
    samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
    samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
    samplerInfo.anisotropyEnable = VK_FALSE;
    samplerInfo.maxAnisotropy = 1.0f;
    samplerInfo.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
    samplerInfo.unnormalizedCoordinates = VK_FALSE;
    samplerInfo.compareEnable = VK_FALSE;
    samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
    samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
    samplerInfo.mipLodBias = 0.0f;
    samplerInfo.minLod = 0.0f;
    samplerInfo.maxLod = 5.0f;

    debugAssertFunctionResult(vk::CreateSampler(appManager.device, &samplerInfo, nullptr, &appManager.texture.sampler), "Texture Sampler Creation");

    // Clean up all the temporary data created for this operation.
    vk::DestroyFence(appManager.device, copyFence, nullptr);
    vk::FreeCommandBuffers(appManager.device, appManager.commandPool, 1, &commandBuffer);
    vk::FreeMemory(appManager.device, stagingBufferData.memory, nullptr);
    vk::DestroyBuffer(appManager.device, stagingBufferData.buffer, nullptr);
}

As mentioned, the texture is a simple checked pattern generated on-the-fly using generateTexture(), which is outlined below. View in context.

void VulkanHelloAPI::generateTexture()
{
    // This function will generate a checkered texture on the fly to be used on the triangle that is going
    // to be rendered and rotated on screen.
    for (uint16_t x = 0; x < appManager.texture.textureDimensions.width; ++x)
    {
        for (uint16_t y = 0; y < appManager.texture.textureDimensions.height; ++y)
        {
           float g = 0.3f;
           if (x % 128 < 64 && y % 128 < 64) { g = 1; }
           if (x % 128 >= 64 && y % 128 >= 64) { g = 1; }

           uint8_t* pixel = (static_cast<uint8_t*>(appManager.texture.data.data())) + (x * appManager.texture.textureDimensions.height * 4) + (y * 4);
           pixel[0] = static_cast<uint8_t>(100 * g);
           pixel[1] = static_cast<uint8_t>(80 * g);
           pixel[2] = static_cast<uint8_t>(70 * g);
           pixel[3] = 255;
        }
    }
}

Creating the Descriptor Sets#

At this point, the application has buffers and images it uses to store data, such as the transformation matrix and the texture image. It also has shaders that use this data to perform calculations to generate an output. Resource descriptors provide the means through which shaders can access this buffer data.

A descriptor, as the name implies, describes the data that is going to be passed. Descriptors hold information that helps with binding data to shaders and describe any information Vulkan may require before executing the shader. Descriptors are not passed individually; they are bundled together in sets and are opaque to the application. Descriptor sets hold handles to the resources that are going to be accessed. Similarly to command buffers, descriptor sets are allocated from pools.

The code example initDescriptorPoolAndSet() demonstrates the process of creating the descriptor pool and allocating descriptor sets from it. Two descriptor sets are needed for this application: a dynamic set for pointing to the dynamic uniform buffer containing multiple transformation matrices, and a static set for pointing to the buffer containing texture data. It is generally desired for applications to group resources in different descriptor sets based on their update frequency (for example, per frame, per application, or per material).

Before allocating a descriptor set, you must create a descriptor set layout. This object describes what type of resource the descriptor contains and which shader stages can access the resource. For the dynamic descriptor set, this will be a dynamic uniform buffer and the vertex shader stage. For the static descriptor set, this will be a combined image sampler and the fragment shader stage.

To create a descriptor set:

  1. Create a descriptor pool that is used to allocate the sets.

  2. Create a descriptor layout that defines how the descriptor is laid out.

  3. Allocate the descriptor set from the pool.

View in context.

void VulkanHelloAPI::initDescriptorPoolAndSet()
{
    // This is the size of the descriptor pool. This establishes how many descriptors are needed and their type.
    VkDescriptorPoolSize descriptorPoolSize[2];

    descriptorPoolSize[0].descriptorCount = 1;
    descriptorPoolSize[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;

    descriptorPoolSize[1].descriptorCount = 1;
    descriptorPoolSize[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;

    // This is the creation info struct for the descriptor pool.
    // This specifies the size of the pool
    // and the maximum number of descriptor sets that can be allocated out of it.
    // The VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT used here indicates that the descriptor
    // sets can return their allocated memory individually rather than all together.
    VkDescriptorPoolCreateInfo descriptorPoolInfo = {};
    descriptorPoolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
    descriptorPoolInfo.pNext = nullptr;
    descriptorPoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
    descriptorPoolInfo.poolSizeCount = 2;
    descriptorPoolInfo.pPoolSizes = descriptorPoolSize;
    descriptorPoolInfo.maxSets = 2;

    // Create the descriptor pool.
    debugAssertFunctionResult(vk::CreateDescriptorPool(appManager.device, &descriptorPoolInfo, nullptr, &appManager.descriptorPool), "Descriptor Pool Creation");
    {
     // Populate a descriptor layout binding struct. This defines the type of data that will be passed to the shader and the binding location in the shader stages.
     VkDescriptorSetLayoutBinding descriptorLayoutBinding;
     descriptorLayoutBinding.descriptorCount = 1;
     descriptorLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
     descriptorLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
     descriptorLayoutBinding.binding = 0;
     descriptorLayoutBinding.pImmutableSamplers = nullptr;

     // Populate an info struct for the creation of the descriptor set layout. The number of bindings previously created is passed in here.
     VkDescriptorSetLayoutCreateInfo descriptorLayoutInfo = {};
     descriptorLayoutInfo.flags = 0;
     descriptorLayoutInfo.pNext = nullptr;
     descriptorLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
     descriptorLayoutInfo.bindingCount = 1;
     descriptorLayoutInfo.pBindings = &descriptorLayoutBinding;

     // Create the descriptor set layout for the descriptor set which provides access to the texture data.
     debugAssertFunctionResult(
     vk::CreateDescriptorSetLayout(appManager.device, &descriptorLayoutInfo, nullptr, &appManager.staticDescriptorSetLayout), "Descriptor Set Layout Creation");
    }

    // The process is then repeated for the descriptor set layout of the uniform buffer descriptor set.
    {
        VkDescriptorSetLayoutBinding descriptorLayoutBinding;
        descriptorLayoutBinding.descriptorCount = 1;
        descriptorLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
        descriptorLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
        descriptorLayoutBinding.binding = 0;
        descriptorLayoutBinding.pImmutableSamplers = nullptr;

        // Create the descriptor set layout using the array of VkDescriptorSetLayoutBindings.
        VkDescriptorSetLayoutCreateInfo descriptorLayoutInfo = {};
        descriptorLayoutInfo.flags = 0;
        descriptorLayoutInfo.pNext = nullptr;
        descriptorLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
        descriptorLayoutInfo.bindingCount = 1;
        descriptorLayoutInfo.pBindings = &descriptorLayoutBinding;

        // Create the descriptor set layout for the uniform buffer descriptor set.
        debugAssertFunctionResult(
        vk::CreateDescriptorSetLayout(appManager.device, &descriptorLayoutInfo, nullptr, &appManager.dynamicDescriptorSetLayout), "Descriptor Set Layout Creation");
    }

    // Allocate the uniform buffer descriptor set from the descriptor pool.
    // This struct simply points to the layout of the uniform buffer descriptor set and also the descriptor pool created earlier.
    VkDescriptorSetAllocateInfo descriptorAllocateInfo = {};
    descriptorAllocateInfo.descriptorPool = appManager.descriptorPool;
    descriptorAllocateInfo.descriptorSetCount = 1;
    descriptorAllocateInfo.pNext = nullptr;
    descriptorAllocateInfo.pSetLayouts = &appManager.dynamicDescriptorSetLayout;
    descriptorAllocateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;

    debugAssertFunctionResult(vk::AllocateDescriptorSets(appManager.device, &descriptorAllocateInfo, &appManager.dynamicDescSet), "Descriptor Set Creation");

    // Allocate the texture image descriptor set.
    // The allocation struct variable is updated to point to the layout of the texture image descriptor set.
    descriptorAllocateInfo.pSetLayouts = &appManager.staticDescriptorSetLayout;
    debugAssertFunctionResult(vk::AllocateDescriptorSets(appManager.device, &descriptorAllocateInfo, &appManager.staticDescSet), "Descriptor Set Creation");

    // This information references the texture sampler that will be passed to the shaders by way of
    // the descriptor set. The sampler determines how the pixel data of the texture image will be
    // sampled and how it will be passed to the fragment shader. It also contains the actual image
    // object (via its image view) and the image layout.
    // This image layout is optimised for read-only access by shaders. The image was transitioned to
    // this layout using a memory barrier in initTexture().
    VkDescriptorImageInfo descriptorImageInfo = {};
    descriptorImageInfo.sampler = appManager.texture.sampler;
    descriptorImageInfo.imageView = appManager.texture.view;
    descriptorImageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

    // Update the descriptor sets with the actual objects, in this case the texture image and the uniform buffer.
    // These structs specify which descriptor sets are going to be updated and hold a pointer to the actual objects.
    VkWriteDescriptorSet descriptorSetWrite[2] = {};

    descriptorSetWrite[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    descriptorSetWrite[0].pNext = nullptr;
    descriptorSetWrite[0].dstSet = appManager.staticDescSet;
    descriptorSetWrite[0].descriptorCount = 1;
    descriptorSetWrite[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
    descriptorSetWrite[0].pImageInfo = &descriptorImageInfo; // Pass image object
    descriptorSetWrite[0].dstArrayElement = 0;
    descriptorSetWrite[0].dstBinding = 0;
    descriptorSetWrite[0].pBufferInfo = nullptr;
    descriptorSetWrite[0].pTexelBufferView = nullptr;

    descriptorSetWrite[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    descriptorSetWrite[1].pNext = nullptr;
    descriptorSetWrite[1].dstSet = appManager.dynamicDescSet;
    descriptorSetWrite[1].descriptorCount = 1;
    descriptorSetWrite[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
    descriptorSetWrite[1].pBufferInfo = &appManager.dynamicUniformBufferData.bufferInfo; // Pass uniform buffer to this function.
    descriptorSetWrite[1].dstArrayElement = 0;
    descriptorSetWrite[1].dstBinding = 0;

    vk::UpdateDescriptorSets(appManager.device, 2, descriptorSetWrite, 0, nullptr);
}