Vulkan
|
Vulkan is a modern 3D graphics API, very comparable to Direct3D 12.
These APIs are much more multithreading friendly, and do this by offering lower level control of the hardware to applications than previous generation APIs like Open GL or Direct3D 11. In contrast to these APIs, Vulkan avoids global state, state is localised to a command buffer.
Closer mapping to the graphics hardware is able to achieve better performance, at the cost of application complexity. However, this maps to reducing work done by device drivers (and thus, driver complexity).
The existence of projects like ANGLE and DXVK, which implement OpenGL and Direct3D 8-11 respectively, on top of Vulkan, further makes Vulkan an attractive API to target.
Structure of a Vulkan Application
Note that this is not meant to be a tutorial on how to use Vulkan! Rather, it's useful to understand the typical flow of an application.
vkCreateInstance - Creates an application instance. This is the closest thing that Vulkan has to a global state. However, what's passed in is a list of required extensions, and some information about the application (name, required API version...).
vkEnumerateInstanceExtensionProperties can enumerate for optional extensions.
Now with this initial phase of initialization out of the way, the application can now enumerate graphics adapters.
vkEnumeratePhysicalDevices - Returns a count, and optionally, fills an array of, VkPhysicalDevice
handles.
vkGetPhysicalDeviceQueueFamilyProperties - Enumerates the queue families supported by a physical device. An application may well need different queues for Compute vs 3D. This is how it finds out.
Finally, the application is able to create a logical device object.
vkCreateDevice. This takes a physical device to use, a set of queue creation descriptions, and optionally a set of host memory allocator callbacks - allowing e.g. pool allocators instead of default heap.
Render Pipeline
The application now starts to build a render pipeline. There are numerous stages involved here!
// Provided by VK_VERSION_1_0
typedef struct VkComputePipelineCreateInfo {
VkStructureType sType;
const void* pNext;
VkPipelineCreateFlags flags;
VkPipelineShaderStageCreateInfo stage;
VkPipelineLayout layout;
VkPipeline basePipelineHandle;
int32_t basePipelineIndex;
} VkComputePipelineCreateInfo;
// Provided by VK_VERSION_1_0
typedef struct VkGraphicsPipelineCreateInfo {
VkStructureType sType;
const void* pNext;
VkPipelineCreateFlags flags;
uint32_t stageCount;
const VkPipelineShaderStageCreateInfo* pStages;
const VkPipelineVertexInputStateCreateInfo* pVertexInputState;
const VkPipelineInputAssemblyStateCreateInfo* pInputAssemblyState;
const VkPipelineTessellationStateCreateInfo* pTessellationState;
const VkPipelineViewportStateCreateInfo* pViewportState;
const VkPipelineRasterizationStateCreateInfo* pRasterizationState;
const VkPipelineMultisampleStateCreateInfo* pMultisampleState;
const VkPipelineDepthStencilStateCreateInfo* pDepthStencilState;
const VkPipelineColorBlendStateCreateInfo* pColorBlendState;
const VkPipelineDynamicStateCreateInfo* pDynamicState;
VkPipelineLayout layout;
VkRenderPass renderPass;
uint32_t subpass;
VkPipeline basePipelineHandle;
int32_t basePipelineIndex;
} VkGraphicsPipelineCreateInfo;
Note the basePipeline fields allow optimisations when deriving from a similar pipeline, they can reasonably be ignored here.
Shaders
An application will create shaders. These are specified in SPIR-V bytecode.
vkCreateShaderModule - Creates a shader module from an array of SPIR-V bytecode. Note that this is simply a loading operation, it's a thin, opaque, wrapper.
Once shader modules are loaded, they can be specified in a VkPipelineShaderStageCreateInfo . Note there can be multiple entry points to a shader, shaders can be linked...
Vertex Input
The format of the vertex buffer is specified. This is how the application will feed raw vertex data to the shaders.
Input Assembly
An initial processing step, where the toplogogy is specified. The vertex buffer may be individual triangles, triangle strips, points, lines... The input assembly builds the basic mesh based on this.
The rest of the pipleine is similar.
Pipleline Layout
vkCreatePipelineLayout - This is where uniforms (cbuffer in HLSL) are specified. A relatively small buffer that effectively paramaterises the shader. For instance, the view transform can be passed to the vertex shader this way.
Render Pass
vkCreateRenderPass - Determines how the result is rendered to a framebuffer.
After all that work, we reach vkCreateGraphicsPipelines
Framebuffer
vkCreateFramebuffer creates a framebuffer, which is pretty straightforward. It can be used as a destination for a graphics pipeline.
Command Queues
Now we're getting somewhere - and this section maps closely to the graphics hardware itself. First, a command pool is created - vkCreateCommandPool
vkAllocateCommandBuffers - Allocate command buffers from the pool.
vkBeginCommandBuffer - Starts a sequence of commands in a buffer.
vkCmdBeginRenderPass
vkCmdBindPipeline
vkCmdDraw
vkCmdEndRenderPass
vkEndCommandBuffer
vkQueueSubmit - submits the recorded command queue asynchronously.
Synchronisation
All synchronisation is explicit, there are semaphores for sync between GPU processes, and fences for the host CPU.