Vulkan on Android 4 — Model Rendering Part 3

Jin-Long Wu
4 min readOct 27, 2018

--

Photo by Annie Spratt on Unsplash

In this post, we are going to talk about how to build an image from pixel data.

Image

Images represent multidimensional arrays of data. In Vulkan, they are represented by VkImage handles.

Like buffers, an image is usually accompanied by memory(VkDeviceMemory) and an image view(VkImageView). However, in the last post, we didn’t talk about buffer view(VkBufferView) just because we do not need them right now. A VkImage is more like an image id than the image itself as we access the data by VkImageView and the data is stored in VkDeviceMemory. Also, any operations on an image need a VkDevice.

It takes a similar procedure to build an image as a buffer does: create a texture(in a derived class), allocate memory for it, bind the memory to the image, and additionally, create the image view. GenerateMipmap is kind of complicated so we’ll leave it to an independent post.

We define stb definitions in here as I said in part 1. AllocateTexture takes an already-created image as an argument and queries its memory requirements with vkGetImageMemoryRequirements. Like we did in buffer creation we also query memory type index with MapMemoryTypeToIndex so that we can allocate memory for the image with vkAllocateMemory.

CreateImageView takes a TextureAttribs struct declared in texture.h. We specify the image on which the image view based, the image format, which aspect(s) of the image are included in the view, first mipmap level, the number of mipmap levels (starting from 0 in this case), the first array layer, and the number of array layers. With all of those information, we create an image view with vkCreateImageView.

CreateTexture2D takes a TextureAttribs struct as an argument whose fields except sRGB are filled depending on the texture we load from disk or memory. The candidate formats are based on channelsPerPixel and sRGB. However, my testing device does not support formats for three channels.

FindDeviceSupportedFormat iterates VkFormats to check linearTilingFeatures (if tiling is VK_IMAGE_TILING_LINEAR) or optimalTilingFeatures(if tiling is VK_IMAGE_TILING_OPTIMAL) masks of VkFormatProperties retrieved by vkGetPhysicalDeviceFormatProperties contain the VkFormatFeatureFlags we request.

Usually, we use VK_IMAGE_TILING_OTIMAL for better performance and support. Only when we want to manipulate pixels directly on the image we use VK_IMAGE_TILING_LINEAR.

The texture will be used for texture mapping and as the destination of pixels transfer, so we want its format to be VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT and its usage flag to be the bitwise-or of VK_IMAGE_USAGE_SAMPLED_BIT and VK_IMAGE_USAGE_TRANSFER_DST_BIT.

Finally, we create VkImageCreateInfo with ImageCreateInfo and feed it to vkCreateImage to create the image.

BuildTexture2D copy the pixel data to the buffer and copy the buffer data to the image. It is faster than it seems to be than copy pixels one by one to the image which requires that image tiling to be VK_IMAGE_TILING_LINEAR.

To go into detail, first, we create a buffer whose job is storing the pixel data which will be transferred to the image. Therefore, we should indicate our purpose by specifying VK_BUFFER_USAGE_SRC_BIT.

Next, we need to specify VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT in order for CPU to access the buffer memory and specify VK_MEMORY_PROPERTY_HOST_COHERENT_BIT in order for the memory content of CPU and GPU staying synchronized.

Buffer::Map retrieves a host virtual address pointer to a region of a mappable memory object and store it into Buffer::mapped pointer so we can copy the pixels. We should unmap it immediately once we are done with it due to the limited resource of the memory which is visible to both the CPU and GPU. Here don’t forget to free the pixel data since we’ve uploaded the pixels to the buffer.

The next four lines are self-explanatory, they create the texture, allocate memory for it, bind the memory with the texture, and create derived image view.

Before copying, we specify a VkImageMemoryBarrier that no memory access takes effect on stage VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT when the image is in the layout VK_IMAGE_LAYOUT_UNDEFINED and any write access to an image in a copy operation takes effect on stage VK_PIPELINE_STAGE_TRANSFER_BIT when the layout of the image is in VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, and record it with PipelineBarrier. And, we can record a command that copies the buffer data to the image with vkCmdCopyBufferToImage, and submit the whole command buffer to the device queue.

Here is a missing part, the texture will be used for texture mapping, that is to say, it will be sampled by a shader. So we need to transition the image layout from VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL after the copy. In fact, the final step is within mipmaps generation which we ignore for this moment. So if we are not intended to generate mipmaps we must complement this missing part.

--

--