Optimal tiling is implemented through the staging buffer. First, a buffer resource object is created and stored with the raw image data contents. Next, the buffer resource data contents are copied to a newly created image object using the buffer-to-image copy command. The buffer-to-image copy command (vkCmdCopyBufferToImage
) copies the buffer memory contents to the image memory.
In this section, we will implement the image resources using optimal tiling. In order to create an image resource with optimal tiling our user defined function VulkanRenderer::createTextureOptimal()
can be used. This function takes parameters in the same way as the createTextureLinear()
function:
void VulkanRenderer::createTextureOptimal(const char* filename, TextureData *texture, VkImageUsageFlags imageUsageFlags, VkFormat format);
Let's understand and implement these functions step by step.
Load the image file and retrieve its dimensions and the mipmap-level information:
// Load the image gli::texture2D image2D(gli::load(filename)); assert(!image2D.empty()); // Get the image dimensions texture->textureWidth = uint32_t(image2D[0].dimensions().x); texture->textureHeight = uint32_t(image2D[0].dimensions().y); // Get number of mip-map levels texture->mipMapLevels = uint32_t(image2D.levels());
The created image object does not have any device memory backing. In this step, we will allocate the physical device memory and bind it with the created texture->image
. For more information on memory allocation and the binding process, refer to the Memory allocation and binding image resources section in Chapter 6, Allocating Image Resources and Building a Swapchain with WSI:
// Create a staging buffer resource states using. // Indicate it be the source of the transfer command. // .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT VkBufferCreateInfo bufferCreateInfo = {}; bufferCreateInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufferCreateInfo.size = image2D.size(); bufferCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; bufferCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; // Get the buffer memory requirements for the staging buffer VkMemoryRequirements memRqrmnt; VkDeviceMemory devMemory; vkGetBufferMemoryRequirements(deviceObj->device, buffer, &memRqrmnt); VkMemoryAllocateInfo memAllocInfo = {}; memAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; memAllocInfo.pNext = NULL; memAllocInfo.allocationSize = 0; memAllocInfo.memoryTypeIndex = 0; memAllocInfo.allocationSize = memRqrmnt.size; // Determine the type of memory required for // the host-visible buffer deviceObj->memoryTypeFromProperties(memRqrmnt.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, &memAllocInfo.memoryTypeIndex); // Allocate the memory for host-visible buffer objects - error = vkAllocateMemory(deviceObj->device, &memAllocInfo, nullptr, &devMemory); assert(!error); // Bind the host-visible buffer with allocated device memory - error=vkBindBufferMemory(deviceObj->device,buffer,devMemory,0); assert(!error);
Use vkMapMemory()
and populate the raw contents of the loaded image into the buffer object's device memory. Once mapped, use vkUnmapMemory()
to complete the process of uploading data from the host to the device memory:
// Populate the raw image data into the device memory
uint8_t *data;
error = vkMapMemory(deviceObj->device, devMemory, 0,
memRqrmnt.size, 0, (void **)&data);
assert(!error);
memcpy(data, image2D.data(), image2D.size());
vkUnmapMemory(deviceObj->device, devMemory);
The image's create info object (VkImageCreateInfo
) must be created using tiling (.tiling
) options as optimal tiling (VK_IMAGE_TILING_OPTIMAL
). In addition, the image's usage
flag must be set with VK_IMAGE_USAGE_TRANSFER_DST_BIT
, making it a destination for the copy commands to transfer data contents to texture->image
from the buffer
object:
// Create image info with optimal tiling // support (.tiling = VK_IMAGE_TILING_OPTIMAL) VkImageCreateInfo imageCreateInfo = {}; imageCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; imageCreateInfo.pNext = NULL; imageCreateInfo.imageType = VK_IMAGE_TYPE_2D; imageCreateInfo.format = format; imageCreateInfo.mipLevels = texture->mipMapLevels; imageCreateInfo.arrayLayers = 1; imageCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageCreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; imageCreateInfo.extent = { texture->textureWidth, texture->textureHeight, 1 }; imageCreateInfo.usage = imageUsageFlags; // Set image object with VK_IMAGE_USAGE_TRANSFER_DST_BIT if // not set already. This allows to copy the source VkBuffer // object (with VK_IMAGE_USAGE_TRANSFER_DST_BIT) contents // into this image object memory(destination). if (!(imageCreateInfo.usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT)){ imageCreateInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; } error = vkCreateImage(deviceObj->device, &imageCreateInfo, nullptr, &texture->image); assert(!error);
Allocate the physical memory backing and bind it with the created texture->image
. For more information on memory allocation and the binding process, refer to the Memory allocation and binding image resources section in Chapter 6, Allocating Image Resources and Building a Swapchain with WSI:
// Get the image memory requirements vkGetImageMemoryRequirements(deviceObj->device, texture->image, &memRqrmnt); // Set the allocation size equal to the buffer allocation memAllocInfo.allocationSize = memRqrmnt.size; // Determine the type of memory required with the help of memory properties deviceObj->memoryTypeFromProperties(memRqrmnt.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, &memAllocInfo.memoryTypeIndex); // Allocate the physical memory on the GPU error = vkAllocateMemory(deviceObj->device, &memAllocInfo, nullptr, &texture->mem); assert(!error); // Bound the physical memory with the created image object error = vkBindImageMemory(deviceObj->device, texture->image, texture->mem, 0); assert(!error);
The image resource objects are created using the command buffer object, cmdTexture
, defined in the VulkanRenderer
class. Allocate the command buffer to set the image layout and start recording the command buffer:
// Command buffer allocation and recording begins
CommandBufferMgr::allocCommandBuffer(&deviceObj->device,
cmdPool, &cmdTexture);
CommandBufferMgr::beginCommandBuffer(cmdTexture);
Set the image layout (VkImageLayout
) to be VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
since the data contents will be copied from the staging buffer object (source) to the image object (destination). For more information on the setImageLayout()
function, refer to the Set the image layout with memory barriers section in Chapter 6, Allocating Image Resources and Building a Swapchain with WSI:
VkImageSubresourceRange subresourceRange = {}; subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; subresourceRange.baseMipLevel = 0; subresourceRange.levelCount = texture->mipMapLevels; subresourceRange.layerCount = 1; // Set the image layout to be // VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL // since it is destination for copying buffer // into image using vkCmdCopyBufferToImage - setImageLayout(texture->image, VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_LAYOUT_UNDEFINED,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, (VkAccessFlagBits)0, subresourceRange, cmdTexture);
Create buffer image copy regions for the image object and its subresource mipmaps. Use the copy command to transfer the buffer object's (buffer
) device memory contents to the image object's (texture->image
) memory contents:
// List contain buffer image copy for each mipLevel std::vector<VkBufferImageCopy> bufferImgCopyList; uint32_t bufferOffset = 0; // Iterater through each mip level and set buffer image copy for (uint32_t i = 0; i < texture->mipMapLevels; i++) { VkBufferImageCopy bufImgCopyItem = {}; bufImgCopyItem.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; bufImgCopyItem.imageSubresource.mipLevel = i; bufImgCopyItem.imageSubresource.layerCount = 1; bufImgCopyItem.imageSubresource.baseArrayLayer = 0; bufImgCopyItem.imageExtent.width = uint32_t(image2D[i].dimensions().x); bufImgCopyItem.imageExtent.height = uint32_t(image2D[i].dimensions().y); bufImgCopyItem.imageExtent.depth = 1; bufImgCopyItem.bufferOffset = bufferOffset; bufferImgCopyList.push_back(bufImgCopyItem); // adjust buffer offset bufferOffset += uint32_t(image2D[i].size()); } // Copy the staging buffer memory data containing the // staged raw data(with mip levels) into the image object vkCmdCopyBufferToImage(cmdTexture, buffer, texture->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, uint32_t(bufferImgCopyList.size()), bufferImgCopyList.data());
For more information on the copy commands, please refer to our next section, Understanding the copy commands.
Set the image layout, indicating the new layout to be optimal tiling compatible. The underlying implementation uses this flag and chooses a suitable technique to lay out the image contents in an optimal manner:
// Advised to change the image layout to shader read // after staged buffer copied into image memory texture->imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; setImageLayout(texture->image, VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, texture->imageLayout, subresourceRange, cmdTexture);
Finalize the command buffer recording and submit it to the graphics queue:
// Submit command buffer containing copy // and image layout commands CommandBufferMgr::endCommandBuffer(cmdTexture); // Create a fence object to ensure that the command // buffer is executed, coping our staged raw data // from the buffers to image memory with // respective image layout and attributes into consideration VkFence fence; VkFenceCreateInfo fenceCI = {}; fenceCI.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; fenceCI.flags = 0; error = vkCreateFence(deviceObj->device, &fenceCI, nullptr, &fence); assert(!error); VkSubmitInfo submitInfo = {}; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.pNext = NULL; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &cmdTexture; CommandBufferMgr::submitCommandBuffer(deviceObj->queue, &cmdTexture, &submitInfo, fence); error = vkWaitForFences(deviceObj->device, 1, &fence, VK_TRUE, 10000000000); assert(!error); vkDestroyFence(deviceObj->device, fence, nullptr); // destroy the allocated resoureces vkFreeMemory(deviceObj->device, devMemory, nullptr); vkDestroyBuffer(deviceObj->device, buffer, nullptr);
Add a fence as a synchronization primitive to ensure the image layout is prepared successfully before it could utilize the image. Release the fence object once the fence is signaled. In case the fence fails to signal, then the wait command vkWaitForFences()
waits for a maximum of 10 seconds to ensure the system never halts or goes into an infinite wait condition.
For more information on fences, refer to the Understanding synchronization primitives in Vulkan section in Chapter 9, Drawing Objects.
Create an image sampler with linear filtering for minification (minFilter
) and magnification (magFilter
) and also enable anisotropy filtering:
// Create sampler VkSamplerCreateInfo samplerCI = {}; samplerCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; samplerCI.pNext = NULL; samplerCI.magFilter = VK_FILTER_LINEAR; samplerCI.minFilter = VK_FILTER_LINEAR; samplerCI.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; samplerCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerCI.mipLodBias = 0.0f; if (deviceObj->deviceFeatures.samplerAnisotropy == VK_TRUE) { samplerCI.anisotropyEnable = VK_TRUE; samplerCI.maxAnisotropy = 8; } else { samplerCI.anisotropyEnable = VK_FALSE; samplerCI.maxAnisotropy = 1; } samplerCI.compareOp = VK_COMPARE_OP_NEVER; samplerCI.minLod = 0.0f; samplerCI.maxLod = (float)texture->mipMapLevels; samplerCI.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; samplerCI.unnormalizedCoordinates = VK_FALSE; error = vkCreateSampler(deviceObj->device, &samplerCI, nullptr, &texture->sampler); assert(!error); // Specify the sampler in the texture's descsImgInfo texture->descsImgInfo.sampler = texture->sampler;
Create the image view and store it in the local TextureData
object's texture
:
// Create image view to allow shader to // access the texture information VkImageViewCreateInfo viewCI = {}; viewCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; viewCI.pNext = NULL; viewCI.image = VK_NULL_HANDLE; viewCI.viewType = VK_IMAGE_VIEW_TYPE_2D; viewCI.format = format; viewCI.components.r = VK_COMPONENT_SWIZZLE_R; viewCI.components.g = VK_COMPONENT_SWIZZLE_G; viewCI.components.b = VK_COMPONENT_SWIZZLE_B; viewCI.components.a = VK_COMPONENT_SWIZZLE_A; viewCI.subresourceRange = subresourceRange; viewCI.subresourceRange.levelCount = texture->mipMapLevels; // Optimal tiling supports mip map levels very well set it. viewCI.image = texture->image; error = vkCreateImageView(deviceObj->device, &viewCI, NULL, &texture->view); assert(!error); // Fill descriptor image info that can be // used for setting up descriptor sets texture->descsImgInfo.imageView = texture->view;
3.135.187.210