Vulkan Engine - Part 1

When it comes to computer science, computer graphics has always been one of my favorite topics. Initially I started off with playing around in OpenGL, but as soon as I discovered what Vulkan was, I had to try it.

Vulkan is a low-level graphics API developed by the Khronos group. It’s a tough API to get down and is not recommended for beginners. However, once you get the basics down, it’s extremely satisfying to watch the stuff being rendered by your program.

Setting up development environment

Installing the Vulkan API is extremely simple and is slightly different depending on the operating system you’re using. You can download your required version from the LunarG website and follow the instructions from there.

Next to the Vulkan SDK, we need a build tool! Lately I have been using Bazel instead of CMake as my primary build tool. After having to use it for work, it kind of stuck with me and now I like it more than CMake. There are some slight disadvantages to using Bazel as it’s not as widely used as CMake, but follow along and I’ll show you how to overcome those problems. In this blog I will only demonstrate how to integrate it with Bazel, but there are plenty other great guides describing how to build Vulkan-based applications with other build tools.

Initializing the Bazel project

Now the fun begins! We can finally start developing our application. We need some small boilerplate in order to integrate the Vulkan SDK into our project, but don’t worry, it’s pretty simple!

So first, in the root folder of your project, create a file called WORKSPACE with the following content.

# The name of our project.
workspace(
    name = "vulkan-engine"
)

# Load the bazel HTTP and GIT tools.
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")

# Clone the GIT repository containg the bazel rules for Vulkan.
git_repository(
    name = "rules_vulkan",
    remote = "https://github.com/jadarve/rules_vulkan.git",
    tag = "v0.0.3"
)

load("@rules_vulkan//vulkan:repositories.bzl", "vulkan_repositories")
vulkan_repositories("/path/to/where/you/installed/vulkan_sdk/")

Afterwards, create a file called BUILD, also in the root folder.

config_setting (
    name = "linux",
    constraint_values = [
        "@platforms//os:linux"
    ],
    visibility = ["//visibility:public"]
)

config_setting (
    name = "windows",
    constraint_values = [
        "@platforms//os:windows"
    ],
    visibility = ["//visibility:public"]
)

config_setting (
    name = "macos",
    constraint_values = [
        "@platforms//os:macos"
    ],
    visibility = ["//visibility:public"]
)

The BUILD file contains several configuration settings for different operating systems as they each have a slightly different way of loading the Vulkan SDK.

Now we can start writing some code! First create a folder main and create a BUILD file there as well. The BUILD files are the same principle as CMakeLists.txt files and describes specific build rules for the contents of that folder. This file should have the following content.

load("@rules_cc//cc:defs.bzl", "cc_binary")

cc_binary(
    name = "main",
    srcs = ["main.cc"],
    deps = select({
        "//:windows": [
            "@vulkan_windows//:vulkan_cc_library",
        ],
        "//:macos": [
            "@vulkan_macos//:vulkan_cc_library",
        ],
        "//conditions:default": [],
    }),
)

The cc_binary is what will create our (as the name already specifies) binary, in other words, the executable. All source files go in the srcs argument, headers in hdrs and dependencies in deps.

Now we can create our first source file to test that our Vulkan installation actually works. Create a file called main.cc in the main folder with the following contents.

#include <iostream>
#include <string>
#include <vulkan/vulkan.hpp>

int main(int argc, char **argv) {
  const vk::ApplicationInfo appInfo = vk::ApplicationInfo()
                                          .setPApplicationName("vulkan-engine")
                                          .setApplicationVersion(0)
                                          .setEngineVersion(0)
                                          .setPEngineName("vulkan-engine");

  const vk::InstanceCreateInfo instanceInfo =
      vk::InstanceCreateInfo().setPApplicationInfo(&appInfo);

  vk::Instance instance;
  vk::Result result = vk::createInstance(&instanceInfo, nullptr, &instance);
  if (result != vk::Result::eSuccess) {
    std::cerr << "Failed to create Vulkan instance." << std::endl;
    exit(-1);
  }

  auto extensions = vk::enumerateInstanceExtensionProperties();
  for (const auto &ext : extensions) {
    std::cout << ext.extensionName << std::endl;
  }

  return EXIT_SUCCESS;
}

Try to build and run the project by running bazel run //main:main from the root folder of your project. The bazel run command automatically builds (and runs) the specified build target. You should get the following output (or something similar depending on the operating system you run it from).

> VK_KHR_device_group_creation
> VK_KHR_external_fence_capabilities
> VK_KHR_external_memory_capabilities
> VK_KHR_external_semaphore_capabilities
> VK_KHR_get_physical_device_properties2
> VK_KHR_get_surface_capabilities2
> VK_KHR_surface
> VK_EXT_debug_report
> VK_EXT_debug_utils
> VK_EXT_metal_surface
> VK_EXT_swapchain_colorspace
> VK_MVK_macos_surface

So what did we actually write? This is the easiest way of verifying your current setup is working. Basically, we create a Vulkan instance and ask it to print out its supported extensions. In the next part we will integrate this with GLFW.

Written on December 20, 2021