Vulkan Tutorial in C - 006 - App Initialization Part 2
create_shader_module function
VkShaderModule
create_shader_module(VulkanContext *vk, void *code, size_t size)
{
VkShaderModule result;
VkShaderModuleCreateInfo createInfo =
{
VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
NULL,
0,
size,
(u32 *)code
};
if (vkCreateShaderModule(vk->device, &createInfo, NULL,
&result) != VK_SUCCESS)
{
assert(!"Failed to create shader module!");
}
return result;
}
create_shader_module(VulkanContext *vk, void *code, size_t size)
{
VkShaderModule result;
VkShaderModuleCreateInfo createInfo =
{
VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
NULL,
0,
size,
(u32 *)code
};
if (vkCreateShaderModule(vk->device, &createInfo, NULL,
&result) != VK_SUCCESS)
{
assert(!"Failed to create shader module!");
}
return result;
}
Writing Shaders and Compiling Them to SPIR-V
In Vulkan, we can't pass shader source code directly to the creation functions. Instead, shaders must be precompiled into SPIR-V binary format. To do this, we use one of the tools included with the Vulkan SDK.For this tutorial, I've chosen GLSL as the shader source language since it's widely used and familiar to most developers. Below is the vertex shader we'll be using for this simple application:
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 triangle with counter-clockwise winding, it is actually clockwise in Vulkan.
This can be a source of confusion, so be aware that while it may appear as if our shader defines the triangle with counter-clockwise winding, it is actually clockwise in Vulkan.
#version 450
// Hardcoded triangle positions (NDC coordinates)
vec2 positions[3] = vec2[](
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.5)
);
void main()
{
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}
Note that I've included vertex data directly in the vertex shader. I made this choice to keep our Vulkan code as simple as possible. In this tutorial, we won't be creating vertex buffers at all.// Hardcoded triangle positions (NDC coordinates)
vec2 positions[3] = vec2[](
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.5)
);
void main()
{
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}
I know this might come as a bit of a surprise (or even a disappointment), but don't worry! I'll be writing follow-up tutorials where we'll cover vertex buffers, index buffers, textures, and much more.
For now, skipping vertex buffers makes things much easier. Later, once we have a working app, adding those features will be straightforward.
Here is the fragment shader:
#version 450
layout(location = 0) out vec4 outColor;
void main()
{
outColor = vec4(1.0, 0.0, 0.0, 1.0); // Red triangle
}
Save these shaders as shader.vert and shader.frag. I recommend placing them in a shaders folder inside your project root.layout(location = 0) out vec4 outColor;
void main()
{
outColor = vec4(1.0, 0.0, 0.0, 1.0); // Red triangle
}
Next, open a command prompt (a regular cmd is fine), navigate to the shaders folder, and run the following commands:
glslc shader.vert -o vert.spv
glslc shader.frag -o frag.spv
This will generate vert.spv and frag.spv, which are the SPIR-V binary files that we'll load in our code.
glslc shader.frag -o frag.spv
Load SPIR-V and Create Shader Modules
LoadedFile vertexShader = load_entire_file("../shaders/vert.spv");
assert(vertexShader.size > 0);
LoadedFile fragmentShader = load_entire_file("../shaders/frag.spv");
assert(fragmentShader.size > 0);
// Create shader modules from loaded binaries
VkShaderModule vertShaderModule =
create_shader_module(&vk, vertexShader.data, vertexShader.size);
VkShaderModule fragShaderModule =
create_shader_module(&vk, fragmentShader.data, fragmentShader.size);
assert(vertexShader.size > 0);
LoadedFile fragmentShader = load_entire_file("../shaders/frag.spv");
assert(fragmentShader.size > 0);
// Create shader modules from loaded binaries
VkShaderModule vertShaderModule =
create_shader_module(&vk, vertexShader.data, vertexShader.size);
VkShaderModule fragShaderModule =
create_shader_module(&vk, fragmentShader.data, fragmentShader.size);
When loading shaders, we assume a specific path relative to the app's binary location. This can be unreliable and may fail depending on how your Microsoft Visual Studio Working Directory is configured.
For this simple app, we'll stick with this approach, but keep in mind that a more robust solution is to build absolute paths programmatically based on the EXE location.
For now, just make sure your Working Directory is set to the bin folder.
For this simple app, we'll stick with this approach, but keep in mind that a more robust solution is to build absolute paths programmatically based on the EXE location.
For now, just make sure your Working Directory is set to the bin folder.
Define Shader Stage Create Info
After creating the shaders themselves, we need to define additional structures to integrate them into the graphics pipeline. Specifically, for each shader stage (e.g., vertex, fragment, geometry, etc.), we must create a VkPipelineShaderStageCreateInfo structure. This structure describes the shader module and the specific stage of the pipeline it will be used in.Once we’ve defined a VkPipelineShaderStageCreateInfo for each shader stage, we populate an array with these structures. This array is then passed to the graphics pipeline creation function (e.g., vkCreateGraphicsPipelines), allowing Vulkan to associate the shaders with their respective stages in the pipeline.
VkPipelineShaderStageCreateInfo vertShaderStageInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
NULL,
0,
VK_SHADER_STAGE_VERTEX_BIT,
vertShaderModule,
"main", // entry point
NULL // specialization info
};
VkPipelineShaderStageCreateInfo fragShaderStageInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
NULL,
0,
VK_SHADER_STAGE_FRAGMENT_BIT,
fragShaderModule,
"main", // entry point
NULL, // specialization info
};
VkPipelineShaderStageCreateInfo shaderStageInfo[] =
{
vertShaderStageInfo,
fragShaderStageInfo
};
{
VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
NULL,
0,
VK_SHADER_STAGE_VERTEX_BIT,
vertShaderModule,
"main", // entry point
NULL // specialization info
};
VkPipelineShaderStageCreateInfo fragShaderStageInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
NULL,
0,
VK_SHADER_STAGE_FRAGMENT_BIT,
fragShaderModule,
"main", // entry point
NULL, // specialization info
};
VkPipelineShaderStageCreateInfo shaderStageInfo[] =
{
vertShaderStageInfo,
fragShaderStageInfo
};
Define Vertex Input Create Info
VkPipelineVertexInputStateCreateInfo vertexInputStateInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
NULL,
0,
0, NULL, 0, NULL // No vertex buffers
};
VkPipelineInputAssemblyStateCreateInfo inputAssemblyStateInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
NULL,
0,
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
VK_FALSE // primitiveRestartEnable
};
{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
NULL,
0,
0, NULL, 0, NULL // No vertex buffers
};
VkPipelineInputAssemblyStateCreateInfo inputAssemblyStateInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
NULL,
0,
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
VK_FALSE // primitiveRestartEnable
};
Define Dynamic State Create Info
VkPipelineDynamicStateCreateInfo dynamicStateInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
NULL,
0,
0, NULL // No dynamic states
};
{
VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
NULL,
0,
0, NULL // No dynamic states
};
Define Viewport State Create Info
VkViewport viewport =
{
0, 0, // x, y
(f32)vk.swapchainExtents.width,
(f32)vk.swapchainExtents.height,
0, 0 // min, max depth
};
VkViewport viewports[] = { viewport };
VkRect2D scissor =
{
{0, 0}, // offset
vk.swapchainExtents
};
VkRect2D scissors[] = { scissor };
VkPipelineViewportStateCreateInfo viewportStateInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
NULL,
0,
array_count(viewports),
viewports,
array_count(scissors),
scissors
};
That wraps up Part 2 of the app initialization. Next, we'll move on to the final part of initialization, where we'll complete the graphics pipeline creation.{
0, 0, // x, y
(f32)vk.swapchainExtents.width,
(f32)vk.swapchainExtents.height,
0, 0 // min, max depth
};
VkViewport viewports[] = { viewport };
VkRect2D scissor =
{
{0, 0}, // offset
vk.swapchainExtents
};
VkRect2D scissors[] = { scissor };
VkPipelineViewportStateCreateInfo viewportStateInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
NULL,
0,
array_count(viewports),
viewports,
array_count(scissors),
scissors
};
Next
Comments
Post a Comment