Vulkan API Overview: Rendering primitives
Previous Vulkan API Overview went over what went to clearing a screen.
You may have thought that was bad, and it still gets crazier. The maddening way you have to specify everything in Vulkan may remind you of playing Dungeons and Dragons... As long you didn't play with a premade character.
This time we zoom over what's required to draw with primitive shapes on the screen. Points, lines or triangles - the brushes of the space age.
Broad zoom
Like before, you need to obtain a swapchain so you have something to draw to. You have to prepare queues so you can submit your command buffers. You also have to:
- Allocate memory from your graphics device to hold your input for the renderer and upload it there.
- Create a render pass and a matching framebuffer.
- Create a descriptor set layout
- Allocate a descriptor set
- Build up a graphics pipeline for rendering.
- Fill and submit a command buffer that binds everything above and issues the draw calls. Don't forget to provide the necessary pipeline barriers.
Validation layers
It would be inevitable that you end up with programs that only work on the machine you developed it on, if you didn't knew these few things about the validation layers.
Vulkan has in-built system to attach layers between the API and the driver. Aside anyone can create such layers themselves, you get free validation layers from LunarG when you download their SDK. These layers help you catch issues in your use of the API, but there are little hurdles.
First, the layers do not write out debug messages unless they are configured to
do so. Just activating the validation layers doesn't tell you much unless you
configure them to log. Example of such config is bundled along the SDK and can
be found from the config/
-directory.
Second, these validation layers do not catch every incorrect use of the API. I don't know if they will ever catch everything. It's good idea to keep reading specs even if you were sure about how things work.
Image views and samplers
Image view provide an interpretation for the image. It tells:
- Whether it's a color, depth or stencil buffer
- Which layers are rendered in the image.
- What kind of dimensions are interpreted for the image data.
Additionally if you read from an image, for example to use one as a texture, you are going to need a sampler. The sampler tells how texture coordinates sample into color values from the image.
Image views are created by passing a record to the:
- vkCreateImageView
The resulting objects, like many other Vulkan objects, seem quite much immutable. All you can do to them is to use or destroy them.
Render pass and framebuffers
Render pass represent a rendering layer. You could imagine it's like a canvas that is being drawn except that it consists of many images. It describes how the framebuffer is drawn during rendering.
The render passes may consist of several subpasses. The idea is that you can use the results of the earlier pass as an input attachment to the remaining passes, such that they actually become like several layers in photo manipulation software.
After you have a render pass, you can group together the image views of the images you're going to render to into a framebuffer. The images need to match in the dimensions with the framebuffer object.
To prepare these objects, you use their associated Create -functions:
- vkCreateRenderPass
- vkCreateFramebuffer
Memory
Images and data buffers in vulkan require you to allocate the right kind of memory to hold them. After you've allocated the memory, you have to fill it with your data.
You get several heaps you can allocate from, and you should decide yourself which object you store where. You usually have a heap you can memory map, so uploading the contents to the graphics card isn't itself an issue.
- vkGetPhysicalDeviceMemoryProperties
- vkAllocateMemory
- vkMapMemory
- vkUnmapMemory
When you choose the heap, note that you can't access every heap from the host. Some heaps require that you issue commands that write the data there indirectly.
Every resource you create is initially created as virtual allocation. The resources that need to be allocated have a function that tells their memory requirements:
- vkCreateBuffer
- vkGetBufferMemoryRequirements
You may know prior how much memory you're going to need for the allocation, but the numbers should match anyway. The important thing here is that the resource is required to be aligned into the memory.
Here's how you can align an offset:
x + (a - x % a) % a
Having to align or control allocation of GPU memory should be nothing really new for graphics programmers. Whenever you've maintained image atlases or otherwise controlled how single buffer is shared between models, you've had to manage memory.
Once you've decided where to position the buffer, you can bind a memory representation for them with single function:
- vkBindBufferMemory
Resource Descriptors
Resource descriptors represent all the variables that your shaders need, yet aren't provided in the vertex buffers. Rather than binding every descriptor one-by-one they are grouped together into sets.
Descriptor set layout tell which descriptors are grouped together.
descriptor sets actually contain references to resources. You allocate these from descriptor pools. The descriptor sets are instantiated from the layouts and their contents may be updated to contain different references.
Since you can bind more than one descriptor set into one pipeline. You also have Pipeline layouts to describe which kind of descriptor sets the pipeline is using.
Pipeline layouts also describe non-memory-backed push constants that must be filled up in the command buffer.
Related functions:
- vkCreateDescriptorSetLayout
- vkCreateDescriptorPool
- vkAllocateDescriptorSets
- vkUpdateDescriptorSets
- vkCreatePipelineLayout
Graphics Pipeline
Graphics pipeline represents how the renderer itself is configured. It represents the few remaining things that were still fixed-function in graphics pipelines when Vulkan was designed.
Pipeline can be derived from an existing pipeline, but if you don't have an earlier pipeline, you have to create one from scratch. There are massive amount of details that need to be filled up:
- pStages*stageCount - Here you provide all the shader modules you want your pipeline to use. In vulkan loading the shader modules is the simplest thing. It used to be complicated on OpenGL, but now the modules themselves are compiled and validated outside your application.
- pVertexInputState - Tells how your vertex data is formatted into attributes.
- pInputAssemblyState - Tells how vertices are used. Mainly what kind of primitives are assembled from them.
- pViewportState - The viewport and scissors for rendering.
- pRasterizationState - Kind of miscellaneous things that did not fit elsewhere.
- pMultisampleState - Control to multisampling. You mostly fill this with zero if you don't use it, but you need it anyway.
- pColorBlendState - Whether and how blending functions work. This is also mostly filled with zero if you don't use anything in it.
- layout - pipeline layout for your pipeline
- renderPass - render pass that your pipeline is supposed to be compatible with.
These fields are optional:
- pTessellationState - If you're doing things with tessellation shaders.
- pDepthStencilState - If you have a depth or stencil attachment.
- pDynamicState - If you want to change some things dynamically in the command buffer, rather than have them fixed down in the pipeline.
Overall getting to the point that you can do something else than clear a screen requires you to set lot of state. Pipeline may very well be the last thing you set up, even if it only needs to know about render pass and descriptor set layouts.
Related functions:
- vkCreateShaderModule
- vkCreatePipelineCache
- vkCreateGraphicsPipelines
Command buffer
Finally, if you're lucky and not ready to stab yourself with blunt objects. You get to fill up the command buffer. At this point there is relatively little remaining left to call:
- vkCmdBeginRenderPass
- vkCmdBindPipeline
- vkCmdBindDescriptorSets
- vkCmdBindVertexBuffers
- vkCmdDraw
- vkEndRenderPass
Every call you preserve at rendering loop can be used for drawing stuff, so your program runs faster more things you can do at initialization. I guess optimally you would only submit your command buffers in the rendering loop.
Fin
The complexity present in Vulkan is partilly due how it throws the whole history of graphics cards through the users throat at once. You got to handle it all somehow before you can even render a single triangle.
After you've went through it all, there are really few sections in the specification you don't have to read through when starting. Actually there are just 4 of them. When beginning Vulkan, you don't need to read about:
16.
Queries21.
Tessellation22.
Geometry shading28.
Sparse resources
And you got 36 sections in the spec.