DEV Community

Kaustuv Pokharel
Kaustuv Pokharel

Posted on

VULKAN- SETUP Summary

Deep Dive into the Code Architecture for REVISING IN THE FUTURE, for personal use.

This code is designed to encapsulate the Vulkan setup process into a manageable and reusable structure, following object-oriented programming principles. This organization makes it easier to read, maintain, and expand the code as your Vulkan application grows in complexity. Here's a step-by-step breakdown of the class HelloTriangleApplication, its functions, and why each function exists.


The Class Structure

class HelloTriangleApplication {
public:
    void run() {
        initWindow();
        initVulkan();
        mainLoop();
        cleanup();
    }
Enter fullscreen mode Exit fullscreen mode

This is the main driver class for the Vulkan application. It encapsulates the entire lifecycle of your program:

  1. initWindow(): Initializes the GLFW window for displaying Vulkan's output.
  2. initVulkan(): Sets up all Vulkan resources, such as the instance, physical device, logical device, and queues.
  3. mainLoop(): The rendering loop, where you keep the application running and process user input.
  4. cleanup(): Cleans up all resources when the application exits.

This structure ensures that each part of the program has a clear, focused purpose.


Why use functions to break things up?

  1. Code clarity:

    • Instead of dumping all Vulkan setup code in main(), breaking it into functions like initVulkan and cleanup makes it more readable and easier to understand.
  2. Maintainability:

    • If you need to modify the Vulkan setup or rendering loop, you can do so in an isolated function without breaking the rest of the code.
  3. Reusability:

    • If you create another Vulkan project in the future, you can reuse some of these functions, like createInstance or createLogicalDevice, with little modification.


Public Method: run()

void run() {
    initWindow();
    initVulkan();
    mainLoop();
    cleanup();
}
Enter fullscreen mode Exit fullscreen mode

The run function is the entry point for the Vulkan application. It acts as the master controller, calling other functions in a logical sequence.

  1. initWindow():

    • Sets up the GLFW window for Vulkan rendering.
    • Ensures Vulkan can create a surface to draw on.
  2. initVulkan():

    • Handles the complex Vulkan initialization process:
      • Connect to Vulkan (createInstance).
      • Choose a GPU (pickPhysicalDevice).
      • Set up an interface to communicate with the GPU (createLogicalDevice).
  3. mainLoop():

    • Keeps the application running until the user closes the window.
    • Processes input and, eventually, issues rendering commands.
  4. cleanup():

    • Frees up memory and destroys Vulkan objects to prevent memory leaks.


Private Member Functions

These functions handle specific tasks within the Vulkan lifecycle. Let’s break them down one by one, discussing why they exist, how they’re implemented, and how they fit into the overall flow.


1. initWindow()

void initWindow() {
    glfwInit();

    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

    window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
}
Enter fullscreen mode Exit fullscreen mode

Purpose:

This function initializes the GLFW windowing system and creates a window that Vulkan can use for rendering. Vulkan itself doesn’t handle window creation—it relies on external libraries like GLFW.

Key Steps:

  1. glfwInit():

    • Initializes the GLFW library, which manages the window.
  2. glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API):

    • Tells GLFW not to create an OpenGL context, as Vulkan doesn’t need it.
  3. glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE):

    • Makes the window non-resizable to simplify Vulkan’s handling of swap chains.
  4. glfwCreateWindow(...):

    • Creates the actual window.

Why is it a separate function?

  • Window initialization is independent of Vulkan initialization.
  • It keeps the run() function clean and focused.


2. initVulkan()

void initVulkan() {
    createInstance();
    pickPhysicalDevice();
    createLogicalDevice();
}
Enter fullscreen mode Exit fullscreen mode

Purpose:

This function orchestrates all the Vulkan setup steps. It calls helper functions (createInstance, pickPhysicalDevice, etc.) to initialize Vulkan in a modular way.


Why is it structured like this?

  1. Vulkan initialization is complex and involves multiple steps.
  2. Each step has a specific purpose:
    • createInstance: Connects to Vulkan.
    • pickPhysicalDevice: Chooses a GPU.
    • createLogicalDevice: Sets up communication with the GPU.


3. createInstance()

void createInstance() {
    if (enableValidationLayers && !checkValidationLayerSupport()) {
        throw std::runtime_error("validation layers requested, but not available!");
    }

    VkApplicationInfo appInfo{};
    appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
    appInfo.pApplicationName = "Hello Triangle";
    appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
    appInfo.pEngineName = "No Engine";
    appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
    appInfo.apiVersion = VK_API_VERSION_1_0;

    VkInstanceCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
    createInfo.pApplicationInfo = &appInfo;

    uint32_t glfwExtensionCount = 0;
    const char** glfwExtensions;
    glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

    std::vector<const char*> requiredExtensions;

    for(uint32_t i = 0; i < glfwExtensionCount; i++) {
        requiredExtensions.emplace_back(glfwExtensions[i]);
    }

    createInfo.enabledExtensionCount = static_cast<uint32_t>(requiredExtensions.size());
    createInfo.ppEnabledExtensionNames = requiredExtensions.data();

    if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
        throw std::runtime_error("failed to create instance!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Purpose:

This function creates a Vulkan instance, which is the foundation for all Vulkan operations.

Key Details:

  • Validation Layers:

    • Debugging tools to catch errors during development.
    • checkValidationLayerSupport() ensures the requested validation layers are available.
  • VkApplicationInfo:

    • Provides basic information about the app (name, version, etc.).
    • Helps Vulkan optimize for specific apps.
  • Extensions:

    • Extensions allow Vulkan to interface with the operating system for windowing.

Why is it a separate function?

Creating an instance is a distinct step in Vulkan’s setup process, so it deserves its own function for clarity and modularity.



4. pickPhysicalDevice()

void pickPhysicalDevice() {
    uint32_t deviceCount = 0;
    vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);

    if (deviceCount == 0) {
        throw std::runtime_error("failed to find GPUs with Vulkan support!");
    }

    std::vector<VkPhysicalDevice> devices(deviceCount);
    vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());

    for (const auto& device : devices) {
        if (isDeviceSuitable(device)) {
            physicalDevice = device;
            break;
        }
    }

    if (physicalDevice == VK_NULL_HANDLE) {
        throw std::runtime_error("failed to find a suitable GPU!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Purpose:

This function selects a Vulkan-compatible GPU to use for rendering.

Key Details:

  1. vkEnumeratePhysicalDevices:

    • Retrieves a list of Vulkan-compatible GPUs.
  2. isDeviceSuitable():

    • Checks if a GPU supports the required features (e.g., graphics queues).

Why is it a separate function?

GPU selection is a key part of Vulkan initialization, and isolating it makes the code easier to read and debug.



5. createLogicalDevice()

void createLogicalDevice() {
    QueueFamilyIndices indices = findQueueFamilies(physicalDevice);

    VkDeviceQueueCreateInfo queueCreateInfo{};
    queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value();
    queueCreateInfo.queueCount = 1;

    float queuePriority = 1.0f;
    queueCreateInfo.pQueuePriorities = &queuePriority;

    VkPhysicalDeviceFeatures deviceFeatures{};

    VkDeviceCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;

    createInfo.pQueueCreateInfos = &queueCreateInfo;
    createInfo.queueCreateInfoCount = 1;

    createInfo.pEnabledFeatures = &deviceFeatures;

    createInfo.enabledExtensionCount = 0;

    if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) {
        throw std::runtime_error("failed to create logical device!");
    }

    vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue);
}
Enter fullscreen mode Exit fullscreen mode

Purpose:

This function creates a logical device to communicate with the GPU.


Key Details:

  • Queues:
    • GPUs support multiple queues for different tasks (graphics, compute, etc.).
    • This function sets up a queue for graphics commands.

Why is it a separate function?

Logical device creation is distinct from physical device selection, so separating it improves modularity and readability.


AWS GenAI LIVE image

How is generative AI increasing efficiency?

Join AWS GenAI LIVE! to find out how gen AI is reshaping productivity, streamlining processes, and driving innovation.

Learn more

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay