Vulkan Tutorial in C - 009 - Adding Textures

This marks the beginning of the second app, where we add textures to the first app. You can find the full source code here.
The first thing we'll do is rename commandPool to graphicsCommandPool and move it inside the VulkanContext struct:
typedef struct
{
    HWND window;
    VkInstance instance;
    VkSurfaceKHR surface;
    VkPhysicalDevice physicalDevice;
    VkDevice device;
    u32 graphicsAndPresentQueueFamily;
    VkQueue graphicsAndPresentQueue;
    VkSwapchainKHR swapchain;
    VkFormat swapchainImageFormat;
    VkImage swapchainImages[2];
    VkImageView swapchainImageViews[2];
    VkExtent2D swapchainExtents;
    
    VkCommandPool graphicsCommandPool;
    
} VulkanContext;
To avoid confusion and make the code less error-prone, let's rename commandBuffer to graphicsCommandBuffer:
VkRenderPass renderPass;
VkFramebuffer swapchainFramebuffers[2];
VkSemaphore imageAvailableSemaphore;
VkSemaphore renderFinishedSemaphore;
VkPipelineLayout pipelineLayout;
VkPipeline graphicsPipeline;
VkFence frameFence;

VkCommandBuffer graphicsCommandBuffer;

Texture-related Vulkan Objects

To keep things simple, let's place all important texture-related Vulkan objects right next to it:
// Descriptor System
VkDescriptorSetLayout descSetLayout;
VkDescriptorPool descPool;
VkDescriptorSet descSet;

// Texture
VkImage texImage;
VkDeviceMemory texImageMemory;
VkImageView texImageView;
VkSampler texSampler;
Since we renamed commandPool to graphicsCommandPool and commandBuffer to graphicsCommandBuffer, make sure to update the entire code accordingly. It's a simple find-and-replace task.

Later, we'll write functions that need to use graphicsCommandPool to allocate new command buffers, which is why we're moving it into the struct. Technically, we should create a separate command pool for those extra command buffers, but for simplicity, we won’t.

Create Image View function

Next, we'll create a helper function for creating image views since we'll be creating more than one:
VkImageView
vk_create_image_view(VulkanContext *vk, VkImage image, VkFormat format)
{
    VkImageView imageView;
    
    VkComponentMapping swizzle =
    {
        VK_COMPONENT_SWIZZLE_IDENTITY,
        VK_COMPONENT_SWIZZLE_IDENTITY,
        VK_COMPONENT_SWIZZLE_IDENTITY,
        VK_COMPONENT_SWIZZLE_IDENTITY
    };
    
    VkImageSubresourceRange subRange =
    {
        VK_IMAGE_ASPECT_COLOR_BIT,
        0, // baseMipLevel
        1, // levelCount
        0, // baseArrayLayer
        1 // layerCount
    };
    
    VkImageViewCreateInfo viewInfo =
    {
        VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
        NULL,
        0,
        image,
        VK_IMAGE_VIEW_TYPE_2D,
        format,
        swizzle,
        subRange
    };
    
    // Create the image view
    vkCreateImageView(vk->device, &viewInfo, NULL,
                      &imageView);
    
    assert(imageView);
    
    return imageView;
}
Previously, we were writing this code in place during swapchain creation, but now that we have a helper function, we'll replace that with a function call:
// For each swapchain image
for (u32 i = 0; i < imageCount; i++)
{
    assert(vk.swapchainImages[i]);
    
    vk.swapchainImageViews[i] =
        vk_create_image_view(&vk, vk.swapchainImages[i],
                             vk.swapchainImageFormat);
    
    assert(vk.swapchainImageViews[i]);
}

Find Memory Type function

Next, we'll create a helper function to find the memory type index we need. This function takes the required memory type and characteristics as parameters. We need this because GPUs have different kinds of memory, mainly differing in whether they are accessible to the CPU.
u32
vk_find_memory_type(VulkanContext *vk, u32 typeFilter,
                    VkMemoryPropertyFlags memPropFlags)
{
    VkPhysicalDeviceMemoryProperties memProperties = { 0 };
    vkGetPhysicalDeviceMemoryProperties(vk->physicalDevice, &memProperties);
    
    u32 memoryTypeIndex = UINT32_MAX;
    
    for (u32 i = 0; i < memProperties.memoryTypeCount; i++)
    {
        bool hasMemoryType = typeFilter & (1 << i);
        
        u32 propFlags = memProperties.memoryTypes[i].propertyFlags;
        bool propsMatch = (propFlags & memPropFlags) == memPropFlags;
        
        if (hasMemoryType && propsMatch)
        {
            memoryTypeIndex = i;
            break;
        }
    }
    
    assert(memoryTypeIndex != UINT32_MAX);
    
    return memoryTypeIndex;
}

Memory Types and Staging Buffer

Memory that is not CPU-accessible is faster, but we can't modify it directly from the CPU. To work around this, we create temporary buffers using CPU-visible memory so we can write to them. Then, on the GPU, we copy data from these buffers to GPU-only memory. This process is known as staging, and the temporary buffer is called a staging buffer.

To copy data from one buffer to another on the GPU, we need to use command buffers. While we could technically reuse the same command buffer for this operation, it's easier to create a single-use command buffer. This approach simplifies synchronization since the buffer is used once and then discarded.

So, let's create two helper functions to streamline this process: vk_begin_single_time_commands and vk_end_single_time_commands. This is a common practice in Vulkan:
VkCommandBuffer
vk_begin_single_time_commands(VulkanContext *vk)
{
    VkCommandBuffer commandBuffer;
    
    VkCommandBufferAllocateInfo allocInfo =
    {
        VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
        NULL,
        vk->graphicsCommandPool, // TODO: change to transfer command pool
        VK_COMMAND_BUFFER_LEVEL_PRIMARY,
        1
    };
    
    vkAllocateCommandBuffers(vk->device, &allocInfo,
                             &commandBuffer);
    
    VkCommandBufferBeginInfo beginInfo =
    {
        VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
        NULL,
        VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
        NULL
    };
    
    vkBeginCommandBuffer(commandBuffer, &beginInfo);
    
    return commandBuffer;
}

void
vk_end_single_time_commands(VulkanContext *vk, VkCommandBuffer commandBuffer)
{
    vkEndCommandBuffer(commandBuffer);
    
    VkSubmitInfo submitInfo = {0};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &commandBuffer;
    
    vkQueueSubmit(vk->graphicsAndPresentQueue, 1, &submitInfo, VK_NULL_HANDLE);
    vkQueueWaitIdle(vk->graphicsAndPresentQueue); // Wait for execution to finish
    
    // TODO: change to transfer command pool
    vkFreeCommandBuffers(vk->device, vk->graphicsCommandPool, 1,
                         &commandBuffer);
}

Update Shaders

As the final step in this post, let's update the shaders. We'll add UVs to the vertex shader and modify it to draw two triangles forming a quad instead of a single triangle:
#version 450

layout(location = 0) out vec2 outUV;

void main()
{
    vec2 positions[6] = vec2[](
        vec2(-0.5, -0.5),
        vec2(+0.5, -0.5),
        vec2(+0.5, +0.5),

        vec2(-0.5, -0.5),
        vec2(+0.5, +0.5),
        vec2(-0.5, +0.5)
    );

    vec2 uvs[6] = vec2[](
        vec2(0, 0),
        vec2(1, 0),
        vec2(1, 1),

        vec2(0, 0),
        vec2(1, 1),
        vec2(0, 1)
    );

    gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
    outUV = uvs[gl_VertexIndex];
}
In OpenGL/D3D11, the NDC coordinates have their origin at the bottom-left corner. However, this is not the case in Vulkan. In Vulkan, the NDC coordinates have their origin at the top-left. This means that the clockwise winding is reversed compared to OpenGL/D3D11.

This can be a source of confusion, so be aware that while it may appear as if our shader defines the triangles with counter-clockwise winding, it is actually clockwise in Vulkan.
In the fragment shader, we'll add a sampler2D to render our texture instead of just displaying a solid red color like before:
#version 450

layout(location = 0) in vec2 inUV;
layout(set = 0, binding = 0) uniform sampler2D texSampler;

layout(location = 0) out vec4 outColor;

void main()
{
    outColor = texture(texSampler, inUV);
}
Remember to recompile the shaders. In the next post, we'll create the descriptor system objects and move on to creating the actual texture objects.

Next

Comments

Popular Posts