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 a lot new concepts and a lot code to write, since Vulkan is a 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 in nvk.
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
Let’s get straight into the code.
First create a index.mjs file and import node-vulkan:
Notice the Object.assign call. Since node-vulkan consists of hundreds of objects and functions, assigning them globally saves you a lot of 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 actually requires an extension called “VK_KHR_swapchain”, which allows us to create our own Swapchain. To get a list of the required extensions for our window, use:
This method gives us an array of strings, representing the minimum required extensions to bring our window to the screen.
Next create a VkInstance handle, which is the main connection between us and Vulkan. A “handle” is just a memory address, wrapped inside an Object.
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 the handle
- Then pass that handle and the settings into a “create” function
Make sure you get a good understanding of this pattern, it’s often used later!
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 may ask? A Physical Device is another handle, which in this case represents a given GPU in your hardware. 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 create:
This code creates an Array of VkPhysicalDevices, based on the number we gathered in physicalDeviceCount.$.
If you find the above code strange, the following code piece does the same but in a more verbose way:
We now do something weird. 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 just one handle, it takes an array of handles and instantiates each one in there.
The pattern we learned here is:
- Create an In-Out Object, making a Primitive referenceable
- 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 newly Array and the In-Out Object
Let’s print all GPU names which Vulkan found for us to the Console:
VkPhysicalDeviceProperties is an Object, which after being filled by Vulkan contains useful information about the GPU. To fill this Object, we pass it into vkGetPhysicalDeviceProperties, which makes Vulkan fill our Object with the given information.
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 pick the first device in the array.
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 by Vulkan.
We’ve made good progress so far! In the next Parts we will setup a Vulkan Queue, create our own Swapchain and write our first Shader program.