Vulkan Usage
Vulkan main objects and their use
vk.Instance
: The Vulkan context, used to access drivers.vk.PhysicalDevice
: A GPU. Used to query physical GPU details, like features, capabilities, memory size, etc.vk.Device
: The "logical" GPU context that you actually execute things on.vk.Buffer
: A chunk of GPU visible memory.vk.Image
: A texture you can write to and read from.vk.Pipeline
: Holds the state of the gpu needed to draw. For example: shaders, rasterization options, depth settings.vk.RenderPass
: Holds information about the images you are rendering into. All drawing commands have to be done inside a renderpass. Only used in legacy vkguidevk.FrameBuffer
: Holds the target images for a renderpass. Only used in legacy vkguidevk.CommandBuffer
: Encodes GPU commands. All execution that is performed on the GPU itself (not in the driver) has to be encoded in avk.CommandBuffer
.vk.Queue
: Execution "port" for commands. GPUs will have a set of queues with different properties. Some allow only graphics commands, others only allow memory commands, etc. Command buffers are executed by submitting them into a queue, which will copy the rendering commands onto the GPU for execution.vk.DescriptorSet
: Holds the binding information that connects shader inputs to data such asvk.Buffer
resources andvk.Image
textures. Think of it as a set of gpu-side pointers that you bind once.vk.SwapchainKHR
: Holds the images for the screen. It allows you to render things into a visible window. TheKHR
suffix shows that it comes from an extension, which in this case isVK_KHR_swapchain
vk.Semaphore
: Synchronizes GPU to GPU execution of commands. Used for syncing multiple command buffer submissions one after other.vk.Fence
: Synchronizes GPU to CPU execution of commands. Used to know if a command buffer has finished being executed on the GPU.
High level Vulkan application flow
Engine initialization
First, everything is initialized. To initialize Vulkan, you start by creating a vk.Instance
.
From the vk.Instance
, you query the list of vk.PhysicalDevice
handles available in the
machine. For example, if the computer had both a dedicated GPU and integrated graphics, there
would be a vk.PhysicalDevice
for each. After querying the limits and features of the
available vk.PhysicalDevice
handles, you create a vk.Device
from it. With a vk.Device
,
you then get vk.Queue
handles from it, allowing you to execute commands. Then you initialize
the vk.SwapchainKHR
. Alongside the vk.Queue
handles, you create vk.CommandPool
objects
that enable you to allocate command buffers from them.
Asset initialization
Once the core structures are initialized, you initialize the resources you need for whatever
you will be rendering. The materials are loaded, and you create a set of vk.Pipeline
objects
for the shader combinations and parameters needed to render the materials. For the meshes, you
upload their vertex data into vk.Buffer
resources, and you upload their textures into
vk.Image
resources, making sure that the images are in "readable" layout. You also create
vk.RenderPass
objects for whatever your main rendering passes you have. For example, there may
be a vk.RenderPass
for the main rendering, and another for a shadow pass. On a real engine,
much of this can be parallelized and done in background threads, especially since pipeline
creation can be quite expensive.
Render Loop
Now that everything is ready for rendering, you first ask the vk.SwapchainKHR
for an image to
render to. Then you allocate a vk.CommandBuffer
from a vk.CommandBufferPool
or reuse an
already allocated command buffer that has finished execution, and "start" the command buffer,
which allows you to write commands into it. Next, you begin rendering by starting a
vk.RenderPass
, this can be done with a normal vk.RenderPass
, or using dynamic rendering. The
render pass specifies that you are rendering to the image requested from swapchain. Then create
a loop where you bind a vk.Pipeline
, bind some vk.DescriptorSet
resources (for the shader
parameters), bind the vertex buffers, and then execute a draw call. Once you are finished with
the drawing for a pass, you end the vk.RenderPass
. If there is nothing more to render, you end
the vk.CommandBuffer
. Finally, you submit the command buffer into the queue for rendering.
This will begin execution of the commands in the command buffer on the gpu. If you want to
display the result of the rendering, you "present" the image you have rendered to to the
screen. Because the execution may not have finished yet, you use a semaphore to make the
presentation of the image to the screen wait until rendering is finished.
Pseudocode of a render-loop in Vulkan:
package vk_engine
import vk "vendor:vulkan"
// Ask the swapchain for the index of the swapchain image we can render onto
image_index := request_image(mySwapchain)
// Create a new command buffer
cmd := allocate_command_buffer()
// Initialize the command buffer
vk.BeginCommandBuffer(cmd, ...)
// Start a new renderpass with the image index from swapchain as target to render onto
// Each framebuffer refers to a image in the swapchain
vk.CmdBeginRenderPass(cmd, main_render_pass, framebuffers[image_index])
// Rendering all objects
for object in PassObjects {
// Bind the shaders and configuration used to render the object
vk.CmdBindPipeline(cmd, object.pipeline)
// Bind the vertex and index buffers for rendering the object
vk.CmdBindVertexBuffers(cmd, object.VertexBuffer,...)
vk.CmdBindIndexBuffer(cmd, object.IndexBuffer,...)
// Bind the descriptor sets for the object (shader inputs)
vk.CmdBindDescriptorSets(cmd, object.textureDescriptorSet)
vk.CmdBindDescriptorSets(cmd, object.parametersDescriptorSet)
// Execute drawing
vk.CmdDraw(cmd,...)
}
// Finalize the render pass and command buffer
vk.CmdEndRenderPass(cmd)
vk.EndCommandBuffer(cmd)
// Submit the command buffer to begin execution on GPU
vk.QueueSubmit(graphicsQueue, cmd, ...)
// Display the image we just rendered on the screen
// renderSemaphore makes sure the image isn't presented until `cmd` is finished executing
vk.QueuePresent(graphicsQueue, renderSemaphore)