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:
- The upload buffer memory will have to be CPU-visible
- The texture memory will have to be GPU-local, meaning optimal runtime performance
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:
- Transition the resources
- The staging buffer is transitioned from the former
HOST_WRITE
toTRANSFER_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 toTRANSFER_DST_OPTIMAL
in order to ensure proper copyability
- The staging buffer is transitioned from the former
- A command is enqueued to transfer the mipmap data from the staging buffer to the image resource memory
- The image is transitioned for
SHADER_READ
andINPUT_ATTACHMENT_READ
access, as well as aSHADER_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 },
},
}
);