Vulkan API Overview

After the initial shock of looking into immense complexity that is Vulkan, I decided to try do something with it before condemning the Microsoft-esque complexity and verbosity it presents.

You don't need to be a half-elf from space to use Vulkan, but it would help a lot if you could handle more than 7 things in your working memory at the same time. This API is just demanding for the user, and I can show you why. Lets zoom into what you must do to clear a screen.

Broad view

To clear a screen:

You have to obtain a swapchain for the screen.

Get image from that chain.

Submit a command buffer into a queue that clears the image with a color.

Finally you have to tell it to present the image in the swapchain.

That's simple and neat, right?

Synchronization and Cache Control

Synchronization of access to resources is the responsibility of the application in Vulkan. It gives you four primitives to handle it, and expects you to also do it yourself. Not synchronizing means you get garbage, crashes or performance bumps, depending on what the driver vendor likes to give you. For this use you get fences, semaphores and events.

Fence can be used to query whether the task you gave has been completed. You can give a fence for some commands, and then you may check with fence whether the command has been completed, or wait until it completes. The idea is that when fence goes up, the program can proceed doing something. You can lower the fence but can't raise it on the host.

Semaphores are used to coordinate operations between queues and between queue submissions in a single queue. The semaphore is used to tell whether the enqueued work can proceed. Host has no access to the semaphore state, but can pass semaphores to describe how the work should flow.

You also got to tell the graphics hardware in advance where it should put stuff next and where that stuff was. Cache misses on concurrent hardware are probably terrible, and previous memory access doesn't tell where the next will occur. Vulkan provides you with pipeline barriers so you can do this.

I don't understand every detail about pipeline barriers yet, but I see they're really important here. They are mandatory for correct execution of the program and nothing warns you whether you got it right or wrong.

Events are used to gauge progress through a sequence of commands executed on a queue. It's not clear to me how to use them yet, but the idea appears to be you can chain submitted commands with events and submit other commands in between to cover stalls in pipeline.

Initialization

The initialization of Vulkan is relatively easy. You create an instance for your application, enumerate devices, choose one among those that fits. Then you decide how many queues and which family of queues you want. Nvidia makes it easy for you and gives you just one family of queues that can do everything.

You use functions:

vkCreateInstance

vkEnumeratePhysicalDevices

vkGetPhysicalDeviceProperties

vkCreateDevice

vkGetDeviceQueue

If you're going to draw on the window, you have to choose the device and queue by on whether the device can draw to the window you have and whether you can present on the window from the queue.

Obtaining the swapchain

Depending on where you're rendering to, this is different. Your program needs set of images to draw. In minimum you need one backbuffer and one frontbuffer so the desktop compositor can display one image while you're drawing an another.

You obtain a surface from somewhere and then you create the swapchain based on what you've learned about the surface. In minimum you use next functions:

vkCreateSwapchainKHR

vkGetSwapchainImagesKHR

Preparing for drawing

To be able to allocate a command buffer later, you need to create a command pool that matches your queue.

vkCreateCommandPool

You need a command buffer that fills into the specific image you give it. If you're doing it properly, you may already be able to create and fill it up here. Up to you what to do.

Acquiring & Presenting the image

You aren't expected to just arbitrarily grab images from the swapchain and fill to them. You have to tell the swapchain that you're grabbing the next image. It gives you an index of image it wishes you use. Then once you're done with the image, you're being expected to present it. In minimum you use next functions:

vkAcquireNextImageKHR

vkQueuePresentKHR

The AcquireNextImage can signal a semaphore, whereas QueuePresent can wait on a semaphore. To submit in between, you use two semaphores. One to let AcquireNextImage signal to the submission to proceed, and one for submission to let the presentation to proceed.

You can pass a fence to your buffer submission, that you can use to determine whether the last frame was drawn and whether you can submit an another.

Filling up a command buffer

To get Vulkan to work, you have to fill up a command buffer to describe what you want to do, and then submit it to a queue.

vkAllocateCommandBuffers

vkBeginCommandBuffer

vkCmdPipelineBarrier

vkCmdClearColorImage

vkEndCommandBuffer

vkQueueSubmit

To clear an image you're presenting in a window, you first have to pass it a barrier command to plug the image into write transfer. Then you pass a command to clear the image. Finally you give another barrier command to make the image a presentation source again.

Then you submit.

Fin

If this sounds little to you, you have to understand that you'll pass 10 to 20 parameters to each command you use. Things are heavily coupled: You have to decide what you're doing with an image before you create one. Also you have to do something different if you have some little thing different.

I don't fundamentally like what I see, but I understand it's not the fault of who made the Vulkan. It merely raises a cloth from the front of graphics programmers. The hardware from the last 5 to 10 years is just this complex.