A Triangle rendered with Vulkan

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.

Vulkan, originally is a C API but there are bindings for various higher-level languages like Java, Rust and now also JavaScript.

In JavaScript you are currently limited to WebGL, which means you have to work with an outdated API, lacking support for many essential things such as Compute Shaders. Vulkan in contrast has all of it.

This tutorial will give you an introduction into Vulkan and how to render your first triangle, entirely in JavaScript.

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.

Prerequisites

This tutorial makes use of some recently added JavaScript language features, so here is a short explanation on what they are and why they are used.

BigInt

The BigInt Type is a new type that allows to represent 64-bit Numbers in JavaScript. This is important because Vulkan requires to work with memory addresses at some point (don’t be scared, it sounds harder than it is).

ArrayBuffer

ArrayBuffer is not really a new thing in JavaScript, but nvk adds two new methods to it:

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.

ES6 Modules

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.

Installation

Installing Vulkan for JavaScript is simple, create a project folder somewhere and run:

npm install nvk

You’re now ready to use Vulkan in JavaScript.

Code Reference

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” is an so called “In-Out” Object. In-Out Objects are used to pass Primitives by-reference, since JavaScript doesn’t bring this language feature.

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.$.

If you find the above code strange, the following snippet does the same but in more common JavaScript:

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.

Still curious?

We’ve made good progress so far! Next time, we will setup a Logical Device and write our first Shader program.