Vulkan Tutorial in C - 010 - Adding Textures Part 2

Right after the app initialization section where we defined the shader stage create infos, we'll create the descriptor system. Locate the part of the code where it says "Define Shader Stage Create Info" and, after that, begin adding the following code.

Create the Descriptor Set Layout

VkDescriptorSetLayoutBinding descSetLayoutBinding =
{
    0, // binding
    VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
    1, // descriptorCount
    VK_SHADER_STAGE_FRAGMENT_BIT,
    NULL // pImmutableSamplers
};

VkDescriptorSetLayoutCreateInfo descSetLayoutInfo =
{
    VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
    NULL,
    0,
    1, // bindingCount
    &descSetLayoutBinding
};

if (vkCreateDescriptorSetLayout(vk.device, &descSetLayoutInfo, NULL,
                                &descSetLayout) != VK_SUCCESS)
{
    assert(!"Failed to create descriptor set layout!");
}

Create the Descriptor Pool

VkDescriptorPoolSize descPoolSize =
{
    VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
    1 // descriptorCount
};

VkDescriptorPoolSize descPoolSizes[] = { descPoolSize };

VkDescriptorPoolCreateInfo descPoolInfo =
{
    VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
    NULL,
    0,
    1, // maxSets
    array_count(descPoolSizes),
    descPoolSizes
};

if (vkCreateDescriptorPool(vk.device, &descPoolInfo, NULL,
                           &descPool) != VK_SUCCESS)
{
    assert(!"Failed to create descriptor pool!");
}

Allocate the Descriptor Set

VkDescriptorSetLayout descSetLayouts[] = { descSetLayout };
    
VkDescriptorSetAllocateInfo descSetAllocInfo =
{
    VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
    NULL,
    descPool,
    array_count(descSetLayouts),
    descSetLayouts
};

if (vkAllocateDescriptorSets(vk.device, &descSetAllocInfo,
                             &descSet) != VK_SUCCESS)
{
    assert(!"Failed to allocate descriptor set!");
}

Define Texture Data

Loading texture data into C code is beyond the scope of this tutorial. If you're unsure how to do it, I recommend using the stb_image.h library by Sean Barrett. It's easy to use, well-made, and widely used, so you'll find many tutorials on how to work with it.

In this tutorial, I'll be using a simple 2x2 texture with red and semi-transparent black pixels in a checkerboard pattern that I defined myself:
u32 texData[] =
{
    0xCC000000, 0xFF0000FF,
    0xFF0000FF, 0xCC000000
};

VkDeviceSize texDataSize = sizeof(texData);

Create the Staging Buffer

VkBuffer texStagingBuffer;
VkDeviceMemory texStagingBufferMemory;

VkBufferCreateInfo textureStagingBufferInfo =
{
    VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
    NULL,
    0,
    texDataSize,
    VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
    VK_SHARING_MODE_EXCLUSIVE,
    0, NULL // queue families ignored
};

if (vkCreateBuffer(vk.device, &textureStagingBufferInfo, NULL,
                   &texStagingBuffer) != VK_SUCCESS)
{
    assert(!"Failed to create texture staging buffer!");
}

Allocate Memory for the Staging Buffer

{
    VkMemoryRequirements memRequirements = { 0 };
    vkGetBufferMemoryRequirements(vk.device, texStagingBuffer,
                                  &memRequirements);
    
    u32 memoryTypeIndex =
        vk_find_memory_type(&vk, memRequirements.memoryTypeBits,
                            VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                            VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);

    VkMemoryAllocateInfo memAllocInfo =
    {
        VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
        NULL,
        memRequirements.size,
        memoryTypeIndex
    };
    
    if (vkAllocateMemory(vk.device, &memAllocInfo, NULL,
                         &texStagingBufferMemory) != VK_SUCCESS)
    {
        assert(!"Failed to allocate staging buffer memory!");
    }
}
Note that I wrapped this bit of code in its own scope block to avoid leaking memAllocInfo. I'll reuse this name later, which is the only reason I did this.

Bind the buffer to the allocated memory

vkBindBufferMemory(vk.device, texStagingBuffer,
                   texStagingBufferMemory, 0);

Map the Buffer Memory and Copy Data into it

void *mappedData = 0;
vkMapMemory(vk.device, texStagingBufferMemory, 0, texDataSize, 0,
            &mappedData);

memcpy(mappedData, texData, texDataSize);

vkUnmapMemory(vk.device, texStagingBufferMemory);

Create Texture Image

VkExtent3D imageExtent =
{
    2, // width
    2, // height
    1 // depth
};

VkImageCreateInfo imageInfo =
{
    VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
    NULL,
    0,
    VK_IMAGE_TYPE_2D,
    VK_FORMAT_R8G8B8A8_SRGB,
    imageExtent,
    1, // mipLevels
    1, // arrayLayers
    VK_SAMPLE_COUNT_1_BIT,
    VK_IMAGE_TILING_OPTIMAL,
    VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
    VK_SHARING_MODE_EXCLUSIVE,
    0, NULL, // queue families ignored
    VK_IMAGE_LAYOUT_UNDEFINED
};

if (vkCreateImage(vk.device, &imageInfo, NULL,
                  &texImage) != VK_SUCCESS)
{
    assert(!"Failed to create image");
}

Allocate Memory for the Texture Image

{
    VkMemoryRequirements memRequirements = { 0 };
    vkGetImageMemoryRequirements(vk.device, texImage,
                                 &memRequirements);
    
    VkMemoryAllocateInfo memAllocInfo =
    {
        VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
        NULL,
        memRequirements.size,
        vk_find_memory_type(&vk, memRequirements.memoryTypeBits,
                            VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)
    };
    
    if (vkAllocateMemory(vk.device, &memAllocInfo, NULL,
                         &texImageMemory) != VK_SUCCESS)
    {
        assert(!"Failed to allocate texture image memory!");
    }

    // Bind the image to the allocated memory
    vkBindImageMemory(vk.device, texImage, texImageMemory, 0);
}

Begin Single Time Command Buffer

VkCommandBuffer texCommandBuffer = vk_begin_single_time_commands(&vk);

Define the Subresource Range

VkImageSubresourceRange subResRange =
{
    VK_IMAGE_ASPECT_COLOR_BIT,
    0, // baseMipLevel
    1, // levelCount
    0, // baseArrayLayer
    1, // layerCount
};

Change Image Layout to Transfer using a barrier

{
    VkImageMemoryBarrier barrier =
    {
        VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
        NULL,
        0, // srcAccessMask
        VK_ACCESS_TRANSFER_WRITE_BIT, // dstAccessMask
        VK_IMAGE_LAYOUT_UNDEFINED, // oldLayout
        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, // newLayout
        VK_QUEUE_FAMILY_IGNORED, // srcQueueFamilyIndex
        VK_QUEUE_FAMILY_IGNORED, // dstQueueFamilyIndex
        texImage,
        subResRange
    };
    
    VkImageMemoryBarrier barriers[] = { barrier };
    
    vkCmdPipelineBarrier(texCommandBuffer,
                         VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
                         VK_PIPELINE_STAGE_TRANSFER_BIT,
                         0, 0, NULL, 0, NULL,
                         array_count(barriers),
                         barriers);
}

Copy texture data from Staging Buffer to Image

VkImageSubresourceLayers subResLayers =
{
    VK_IMAGE_ASPECT_COLOR_BIT,
    0, // mipLevel
    0, // baseArrayLayer
    1 // layerCount
};

VkBufferImageCopy imageCopy =
{
    0, // bufferOfsset
    0, // bufferRowLength
    0, // bufferImageHeight
    subResLayers,
    {0, 0, 0},
    imageExtent
};

vkCmdCopyBufferToImage(texCommandBuffer,
                       texStagingBuffer,
                       texImage,
                       VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                       1,
                       &imageCopy);

Change Image Layout to Shader Read using a barrier

{
    VkImageMemoryBarrier barrier =
    {
        VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
        NULL,
        VK_ACCESS_TRANSFER_WRITE_BIT, // srcAccessMask
        VK_ACCESS_SHADER_READ_BIT, // dstAccessMask
        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, // oldLayout
        VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, // newLayout
        VK_QUEUE_FAMILY_IGNORED, // srcQueueFamilyIndex
        VK_QUEUE_FAMILY_IGNORED, // dstQueueFamilyIndex
        texImage,
        subResRange
    };
    
    vkCmdPipelineBarrier(texCommandBuffer,
                         VK_PIPELINE_STAGE_TRANSFER_BIT,
                         VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
                         0, 0, NULL, 0, NULL, 1, &barrier);
}

End and Execute Single Time Command Buffer

vk_end_single_time_commands(&vk, texCommandBuffer);

Destroy Staging Buffer and Free its Memory

vkDestroyBuffer(vk.device, texStagingBuffer, NULL);
vkFreeMemory(vk.device, texStagingBufferMemory, NULL);

Create Texture Image View

texImageView = vk_create_image_view(&vk, texImage,
                                    VK_FORMAT_R8G8B8A8_SRGB);

Create Texture Image Sampler

VkSamplerCreateInfo samplerInfo =
{
    VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
    NULL,
    0,
    VK_FILTER_NEAREST,
    VK_FILTER_NEAREST,
    VK_SAMPLER_MIPMAP_MODE_LINEAR,
    VK_SAMPLER_ADDRESS_MODE_REPEAT,
    VK_SAMPLER_ADDRESS_MODE_REPEAT,
    VK_SAMPLER_ADDRESS_MODE_REPEAT,
    0.0f, // mipLodBias
    VK_FALSE, // anisotropyEnable
    16.0f, // maxAnisotropy
    VK_FALSE, // compareEnabled
    VK_COMPARE_OP_ALWAYS,
    0.0f, // minLod
    VK_LOD_CLAMP_NONE, // maxLod
    VK_BORDER_COLOR_INT_OPAQUE_BLACK,
    VK_FALSE // unnormalizedCoordinates
};

if (vkCreateSampler(vk.device, &samplerInfo, NULL,
                    &texSampler) != VK_SUCCESS)
{
    assert(!"Failed to create texture sampler!");
}

Update Descriptor Set

VkDescriptorImageInfo descImageInfo =
{
    texSampler,
    texImageView,
    VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
};

VkWriteDescriptorSet writeDescSet =
{
    VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
    NULL,
    descSet,
    0, // dstBinding
    0, // dstArrayElement
    1, // descriptorCount
    VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
    &descImageInfo,
    NULL, // pBufferInfo
    NULL // pTexelBufferView
};

vkUpdateDescriptorSets(vk.device, 1, &writeDescSet, 0, NULL);
And that's the end of the initialization. We've set up everything needed for the texture to work. Now, let's update the color blend attachment to use alpha blending:
VkPipelineColorBlendAttachmentState colorBlendAttachment =
{
    VK_TRUE, // blendEnable
    VK_BLEND_FACTOR_SRC_ALPHA,
    VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
    VK_BLEND_OP_ADD,
    VK_BLEND_FACTOR_SRC_ALPHA,
    VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
    VK_BLEND_OP_ADD,
    colorWriteMask,
};
We also need to update the graphics pipeline layout. Where we previously had no descriptor set layouts, we'll now add our descriptor set layout array:
VkPipelineLayoutCreateInfo pipelineLayoutInfo =
{
    VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
    NULL,
    0,
    array_count(descSetLayouts),
    descSetLayouts,
    0, NULL // (no push constant ranges)
};
The very last thing we need to do is bind the descriptor set right after binding our graphics pipeline and modify the draw function to draw 6 vertices instead of 3:
// Bind descriptor set
// Bind descriptor set
VkDescriptorSet descSets[] = { descSet };
vkCmdBindDescriptorSets(graphicsCommandBuffer,
                        VK_PIPELINE_BIND_POINT_GRAPHICS,
                        pipelineLayout, 0,
                        array_count(descSets),
                        descSets,
                        0, NULL);

// Draw 6 vertices (2 triangles)
vkCmdDraw(graphicsCommandBuffer, 6, 1, 0, 0);
And there we have it! We're now drawing a quad with a checkerboard texture, including transparency. This involved a lot of steps, and unlike the first tutorial, I had to guide you on where to change or add new code, so there are more opportunities for mistakes, or for me to not be clear enough.

If you encounter any issues, please let me know in the comments. You can also find the full source code for where we’re at here, so you can compare it with your code if it's not working.

In the next tutorials, we’ll move on to adding vertex and index buffers to the app. See you there!

Next

Comments

Popular Posts