Create images

The process of creating textures in Vulkan is similar to the creation of a depth buffer, with a number of very important extra steps.

As we want to transfer pixel data to the API rather than render to the texture for later use, we will need to fill the texture with data. In order to do so, we will require an upload buffer to store our pixel data in on top of the image itself. This buffer will need to accessible to the CPU in order to be able to transfer the data.

First we will create both the upload buffer and the image. Note that, while the image needs a size in pixels, the buffer will need a size in bytes. So the total size of the pixel buffer is calculated first.

uint64_t pixelSize = 0;
for ( uint32_t i = 0; i < imageDesc->mipCount; i++ )
    pixelSize += imageDesc->mips[i].width * imageDesc->mips[i].height * sizeof ( uint32_t );

VkImage image;
VkBuffer stagingBuffer;
result = vkCreateBuffer (
    renderer->device,
    &(VkBufferCreateInfo){
        .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
        .pNext = NULL,
        .flags = 0,
        .size  = pixelSize,
        .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
    },
    NULL,
    &stagingBuffer
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1, "vkCreateImage failed (0x%08X)", (uint32_t)result);

result = vkCreateImage (
    renderer->device,
    &(VkImageCreateInfo){
        .sType         = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
        .pNext         = NULL,
        .flags         = 0,
        .imageType     = VK_IMAGE_TYPE_2D,
        .format        = VK_FORMAT_R8G8B8A8_UNORM,
        .extent        = { .width = imageDesc->width, .height = imageDesc->height, .depth = 1 },
        .mipLevels     = imageDesc->mipCount,
        .arrayLayers   = 1,
        .samples       = VK_SAMPLE_COUNT_1_BIT,
        .tiling        = VK_IMAGE_TILING_OPTIMAL,
        .usage         = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
        .initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED,
    },
    NULL,
    &image
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1, "vkCreateImage failed (0x%08X)", (uint32_t)result);

With the main resources themselves created, we will still have to assign memory to these objects in order to use them. For this, we will need to take note of two important points:

  1. The upload buffer memory will have to be CPU-visible
  2. The texture memory will have to be GPU-local, meaning optimal runtime performance

For this reason, while we handled allocation and binding before, we are now also checking for the propertyFlags field to verify properties VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT and VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT for the respective resources.

VkMemoryRequirements imageMemoryRequirements, stagingMemoryRequirements;
vkGetImageMemoryRequirements ( renderer->device, image, &imageMemoryRequirements );
vkGetBufferMemoryRequirements ( renderer->device, stagingBuffer, &stagingMemoryRequirements );

uint32_t imageTypeIndex = 0xFFFFFFFF, stagingTypeIndex = 0xFFFFFFFF;
for ( uint32_t i = 0; i < memoryProperties.memoryTypeCount; i++ )
{
    if ( imageMemoryRequirements.memoryTypeBits & (1 << i)
        && (memoryProperties.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) )
    {
        imageTypeIndex = i;
        break;
    }
}
for ( uint32_t i = 0; i < memoryProperties.memoryTypeCount; i++ )
{
    if ( (stagingMemoryRequirements.memoryTypeBits & (1 << i))
        && (memoryProperties.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) )
    {
        stagingTypeIndex = i;
        break;
    }
}
if ( imageTypeIndex == 0xFFFFFFFF || stagingTypeIndex == 0xFFFFFFFF )
    RETURN_ERROR(-1, "No compatible memory type found for image");

VkDeviceMemory imageDeviceMemory, stagingDeviceMemory;
result = vkAllocateMemory (
    renderer->device,
    &(VkMemoryAllocateInfo){
        .sType           = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
        .pNext           = NULL,
        .allocationSize  = imageMemoryRequirements.size,
        .memoryTypeIndex = imageTypeIndex,
    },
    NULL,
    &imageDeviceMemory
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1, "vkAllocateMemory failed (0x%08X)", (uint32_t)result);

result = vkAllocateMemory (
    renderer->device,
    &(VkMemoryAllocateInfo){
        .sType           = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
        .pNext           = NULL,
        .allocationSize  = stagingMemoryRequirements.size,
        .memoryTypeIndex = stagingTypeIndex,
    },
    NULL,
    &stagingDeviceMemory
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1, "vkAllocateMemory failed (0x%08X)", (uint32_t)result);

result = vkBindImageMemory (
    renderer->device,
    image,
    imageDeviceMemory,
    0
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1, "vkBindImageMemory failed (0x%08X)", (uint32_t)result);

result = vkBindBufferMemory (
    renderer->device,
    stagingBuffer,
    stagingDeviceMemory,
    0
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1, "vkBindImageMemory failed (0x%08X)", (uint32_t)result);

With the memory properly allocated and bound, we have to populate the upload buffer with data.

uint8_t* dst = NULL;
result = vkMapMemory (
    renderer->device,
    stagingDeviceMemory,
    0,
    pixelSize,
    0,
    &dst
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1, "vkMapMemory failed (0x%08X)", (uint32_t)result);
memcpy ( dst, pixelData, pixelSize );
vkUnmapMemory ( renderer->device, stagingDeviceMemory );

It might be important to point out here that the memory is mapped, not the resource. As such, when you have a resource you intend to update at any time during the runtime of your application, the memory of the object will have to be in an easily accessible location.

We then perform the following effective operations:

  1. Transition the resources
    • The staging buffer is transitioned from the former HOST_WRITE to TRANSFER_READ state, as the CPU is no longer going to access it, but the resource is going to be the source of a copy operation
    • The image is transitioned from the initial state to the TRANSFER_WRITE state, and the layout transitioned to TRANSFER_DST_OPTIMAL in order to ensure proper copyability
  2. A command is enqueued to transfer the mipmap data from the staging buffer to the image resource memory
  3. The image is transitioned for SHADER_READ and INPUT_ATTACHMENT_READ access, as well as a SHADER_READ_ONLY_OPTIMAL layout. This to ensure optimal layout for sampling the texture in a shader.


vkCmdPipelineBarrier (
    preloaderCommandBuffer,
    VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
    VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
    0,
    0,
    NULL,
    1,
    (VkBufferMemoryBarrier[1]){
        [0] = {
            .sType         = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
            .pNext         = NULL,
            .srcAccessMask = VK_ACCESS_HOST_WRITE_BIT,
            .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT,
            .buffer        = stagingBuffer,
            .offset        = 0,
            .size          = pixelSize,
        },
    },
    1,
    (VkImageMemoryBarrier[1]){
        [0] = {
            .sType               = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
            .pNext               = NULL,
            .srcAccessMask       = VK_ACCESS_HOST_WRITE_BIT,
            .dstAccessMask       = VK_ACCESS_TRANSFER_WRITE_BIT,
            .oldLayout           = VK_IMAGE_LAYOUT_PREINITIALIZED,
            .newLayout           = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
            .image               = image,
            .subresourceRange    = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .levelCount = imageDesc->mipCount, .layerCount = 1 },
        },
    }
);
VkBufferImageCopy mipCopies[16];
for ( uint32_t mip = 0; mip < imageDesc->mipCount; mip++ )
{
    mipCopies[mip] = (VkBufferImageCopy){
        .bufferOffset      = imageDesc->mips[mip].offset,
        .bufferRowLength   = imageDesc->mips[mip].width,
        .bufferImageHeight = imageDesc->mips[mip].height,
        .imageSubresource  = {
            .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
            .mipLevel   = mip,
            .layerCount = 1,
        },
        .imageOffset       = { 0, 0, 0 },
        .imageExtent       = { imageDesc->mips[mip].width, imageDesc->mips[mip].height, 1 },
    };
}

vkCmdCopyBufferToImage (
    preloaderCommandBuffer,
    stagingBuffer,
    image,
    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
    imageDesc->mipCount,
    mipCopies
);

vkCmdPipelineBarrier (
    preloaderCommandBuffer,
    VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
    VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
    0,
    0,
    NULL,
    0,
    NULL,
    1,
    (VkImageMemoryBarrier[1]){
        [0] = {
            .sType               = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
            .pNext               = NULL,
            .srcAccessMask       = VK_ACCESS_TRANSFER_WRITE_BIT,
            .dstAccessMask       = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_INPUT_ATTACHMENT_READ_BIT,
            .oldLayout           = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
            .newLayout           = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
            .image               = image,
            .subresourceRange    = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .levelCount = imageDesc->mipCount, .layerCount = 1 },
        },
    }
);