You probably heard of Vulkan, the new low-level Graphics and Compute API by Khronos. After a long OpenGL era, time has come for a new API.
Before you start this tutorial, you should prepare for many new concepts to learn and a lot code to write, since Vulkan is a low-level and very verbose API. Vulkan is targeted at advanced graphics programmers, so it’s from advantage if you already have some knowledge about graphics programming before you start this tutorial.
- ArrayBuffer.prototype.getAddress: This allows to take the address of an ArrayBuffer which is represented using a BigInt (as described above)
- ArrayBuffer.fromAddress: This method takes two parameters. The 1st parameter is the memory address which you want the ArrayBuffer to start “reading” from. The 2nd parameter is the byteLength of the ArrayBuffer
You will later see why these two methods play an important role.
nvk itself was written using ES6 modules (using the --experimental-modules flag and .mjs file extension) and I recommend you to do the same for this project.
npm install nvk
The code of this tutorial is available on Github here
#1 Setup Phase
Let’s get straight into the code.
First create a index.mjs file and import nvk:
Notice the Object.assign call. Since nvk consists of hundreds of objects and functions, assigning them globally saves you a lot typing later.
Next, create a window:
VulkanWindow creates a native Window which you can use to render things on your screen.
Just like WebGL, Vulkan has extensions which can be enabled when available. Our window already requires extensions, for example “VK_KHR_swapchain”, which allows us to later create our own Swapchain. To get a list of the required extensions for window creation, you can use this shortcut:
This method returns an array of strings, representing the minimum required extensions to bring our window to the screen.
#2 Creating an Instance
Next create a VkInstance handle, which is the main connection between us and Vulkan.
A “handle” is internally just a memory address, wrapped inside an Object and represents different things in Vulkan - more on that later.
We can specify some properties how our VkInstance object will behave, for example add some extensions to it. To do so, we create an so called “createInfo” Object. To setup VkInstance, we need two new Objects: VkApplicationInfo and the VkInstanceCreateInfo.
VkApplicationInfo.apiVersion let’s us specify the Vulkan version to use.
VkInstanceCreateInfo takes our VkApplicationInfo as a member and allows to specify extensions.
To tell Vulkan to use and create this handle, we have to pass it into a function called vkCreateInstance:
This now activates our VkInstance with the settings we’ve put into VkInstanceCreateInfo.
The code pattern you’ve seen in this Phase is very common in Vulkan:
- You create a handle
- Create and fill a “createInfo” Object to specify settings for that handle
- Then pass that handle, together with the settings into a “create” function
Make sure you get a good understanding of this pattern, it’s often used later!
#3 Retrieve available GPUs
We now have to choose a GPU which we want to use with Vulkan. You will also learn another Vulkan code pattern in this Phase.
There is a lot going on here!
First, what’s a “Physical Device” you might ask? A Physical Device is just another handle, which in this case represents a given GPU on your device. You may want to think of it as just “the connection between you and a GPU”.
The variable physicalDeviceCount gets passed into the vkEnumeratePhysicalDevices function.
After the call to vkEnumeratePhysicalDevices, physicalDeviceCount.$ changed to the amount of available GPUs in your hardware. For example if you have 2 GPUs in your Computer, physicalDeviceCount.$ equals “2” after this call.
We now have the amount of GPUs available on our hardware. Remember the VkInstance handle? There is a handle for a GPU as well called VkPhysicalDevice which we now have to instantiate:
This code creates an Array of VkPhysicalDevices, based on the number we got in physicalDeviceCount.$.
We now do something that seems strange at first. We call the vkEnumeratePhysicalDevices function a second time, but this time the 3rd parameter is not null anymore. Instead we pass in the physicalDevices Array we just created:
This function does the same thing like vkCreateInstance, but instead of taking only one handle, it takes an array of handles and fills each handle in there.
The pattern we learned here is:
- Create an In-Out Object (making a Primitive referenceable) to get the amount of handles that we have to instantiate
- Call the enumeration function to update our In-Out Object
- Create and fill an Array based on the In-Out Object’s “$” value
- Call the enumeration function a second time, but this time with our Array of VkPhysicalDevices and the In-Out Object
#4 Print GPU Device Names
Let’s print all GPU names which Vulkan found to the Console:
VkPhysicalDeviceProperties is an Object, which after being filled by Vulkan contains useful information about the GPU. To fill this Object, we just pass it into vkGetPhysicalDeviceProperties, together with our VkPhysicalDevice.
#5 Picking a GPU
Picking the right GPU is an important step, as you want to use a GPU that supports all the stuff you intend it to use it for. But to keep things simple, we will just take the first device from the list of GPU devices.
#6 Creating a Surface
To make our window renderable, let’s tell Vulkan to create a Surface on it:
Vulkan requires us to make at least one call to vkGetPhysicalDeviceSurfaceCapabilitiesKHR, so Vulkan has information about the surface capabilities.
Now we ask Vulkan to give us information about which present modes are available:
Present modes represent how things get updated on your screen (V-Sync).
Notice the variable presentModes, which contains an Int32Array of VkPresentModeKHR enum values, which are the supported present modes, filled in by Vulkan after the 2nd call.
We’ve made good progress so far! Next time, we will setup a Logical Device and write our first Shader program.