1
\$\begingroup\$

I've been following the ever popular 'vulkan-tutorial.com' guide on Vulkan and the result is this program which draws a multicolored triangle. I'm interested in finding out how to make my code more concise and easier to manage, what data/methods REALLY need to be class members or can be local to a function, etc.

The project is structured as three CMake projects, building one as a library (liboceanlight) which contains most of the code, one as the entry point/consumer of that library (oceanlight), and another as the global top-level project:

There are quite a few files here so bear with me.

oceanlight/src/oceanlight.cc

#include <iostream>
#include <vector>
#include <exception>
#include <config.h>
#include <liboceanlight/lol_engine.hpp>
#include <liboceanlight/lol_window.hpp>
#include <liboceanlight/lol_debug_messenger.hpp>
#include "args.hpp"

int main(int argc, char** argv)
{
    try
    {
        oceanlight::args args;
        args.parse(argc, argv);

        if (args.should_exit())
        {
            return EXIT_SUCCESS;
        }

        static liboceanlight::engine engine;
        liboceanlight::window window(args.width, args.height);
        engine.init(window);
        engine.run(window);
    }

    catch (const std::exception& e)
    {
        std::cerr << e.what() << std::endl;
    }

    catch (...)
    {
        std::cerr << "Error: unknown error" << std::endl;
        throw;
    }

    return EXIT_SUCCESS;
}

oceanlight/include/args.hpp

#ifndef OCEANLIGHT_ARGS_HPP_INCLUDED
#define OCEANLIGHT_ARGS_HPP_INCLUDED
#include <iostream>
namespace oceanlight
{
    class args
    {
        bool exit_flag;

        public:
        int width, height;
        args() : width(640), height(480), exit_flag(false) {};
        void parse(int, char**);
        bool should_exit();
    };
}
#endif /* OCEANLIGHT_ARGS_HPP_INCLUDED */

oceanlight/src/args.cc

#include <iostream>
#include <cxxopts.hpp>
#include <liboceanlight/lol_debug_messenger.hpp>
#include <config.h>
#include "args.hpp"

void oceanlight::args::parse(int argc, char** argv)
{
    try
    {
        cxxopts::Options op("oceanlight", "Chase your star");
        op.add_options()("h,help", "Produce help message");
        op.add_options()("v,version", "Print version information");
        op.add_options()("x,width", "Window width", cxxopts::value<int>());
        op.add_options()("y,height", "Window height", cxxopts::value<int>());
        auto result {op.parse(argc, argv)};

        if (result.count("help"))
        {
            std::cout << op.help();
            exit_flag = true;
            return;
        }

        if (result.count("version"))
        {
            std::cout << PROJECT_NAME " Ver. " PROJECT_VER "\n"
                      << liboceanlight::version_string() << std::endl;
            exit_flag = true;
            return;
        }

        if (result.count("width"))
            width = result["width"].as<int>();

        if (result.count("height"))
            height = result["height"].as<int>();
    }

    catch (std::exception& e)
    {
        std::cerr << "Error: " << e.what() << std::endl;
        throw;
    }

    catch (...)
    {
        std::cerr << "Error: unknown error" << std::endl;
        throw;
    }
}

bool oceanlight::args::should_exit()
{
    return exit_flag;
}

liboceanlight/include/liboceanlight/lol_engine.hpp

#ifndef LIBOCEANLIGHT_ENGINE_HPP_INCLUDED
#define LIBOCEANLIGHT_ENGINE_HPP_INCLUDED
#include "vulkan/vulkan_core.h"
#include <iostream>
#include <string>
#include <vector>
#include <optional>
#include <liboceanlight/lol_debug_messenger.hpp>
#include <liboceanlight/lol_glfw_key_callback.hpp>
#include <liboceanlight/lol_glfw_err_callback.hpp>
#include <vulkan/vulkan.h>
#include <GLFW/glfw3.h>
#include <config.h>
#include <liboceanlight/lol_window.hpp>

struct queue_family_indices_struct
{
    std::optional<uint32_t> graphics_queue_family;
    std::optional<uint32_t> presentation_queue_family;

    bool is_complete()
    {
        return graphics_queue_family.has_value() &&
               presentation_queue_family.has_value();
    }
};

struct swap_chain_support_details
{
    VkSurfaceCapabilitiesKHR capabilities;
    std::vector<VkSurfaceFormatKHR> formats;
    std::vector<VkPresentModeKHR> present_modes;
};

namespace liboceanlight
{
    class engine
    {
        VkInstance vulkan_instance {nullptr};
        VkDevice logical_device {nullptr};
        VkPhysicalDevice physical_device {nullptr};
        VkQueue graphics_queue {nullptr};
        VkQueue present_queue {nullptr};
        VkSurfaceKHR window_surface {nullptr};
        VkSwapchainKHR swap_chain {nullptr};
        VkDebugUtilsMessengerEXT debug_utils_messenger {nullptr};
        queue_family_indices_struct queue_family_indices {};
        const std::vector<const char*> device_extensions {
            VK_KHR_SWAPCHAIN_EXTENSION_NAME};
        swap_chain_support_details swap_details {};
        VkExtent2D swap_chain_extent {};
        VkFormat swap_chain_image_format;
        std::vector<VkImage> swap_chain_images;
        std::vector<VkImageView> swap_chain_image_views;
        VkRenderPass render_pass {nullptr};
        VkPipelineLayout pipeline_layout {nullptr};
        VkPipeline graphics_pipeline;
        std::vector<VkFramebuffer> swap_chain_frame_buffers;
        VkCommandPool command_pool {nullptr};
        VkCommandBuffer command_buffer {nullptr};
        VkSemaphore image_available_semaphore {nullptr};
        VkSemaphore rendering_finished_semaphore {nullptr};
        VkFence in_flight_fence {nullptr};

#ifdef NDEBUG
        const bool validation_layers_enabled {false};
#else
        const bool validation_layers_enabled {true};
#endif

        public:
        engine()
        {
            glfwSetErrorCallback(lol_glfw_error_callback);

            int rv = glfwInit();
            if (!rv)
            {
                throw std::runtime_error("Failed to initialize glfw");
            }
        }

        ~engine()
        {
            vkDestroySemaphore(logical_device,
                               image_available_semaphore,
                               nullptr);

            vkDestroySemaphore(logical_device,
                               rendering_finished_semaphore,
                               nullptr);
                               
            vkDestroyFence(logical_device, in_flight_fence, nullptr);
            vkDestroyCommandPool(logical_device, command_pool, nullptr);

            for (auto framebuffer : swap_chain_frame_buffers)
            {
                vkDestroyFramebuffer(logical_device, framebuffer, nullptr);
            }

            vkDestroyPipeline(logical_device, graphics_pipeline, nullptr);
            vkDestroyPipelineLayout(logical_device, pipeline_layout, nullptr);
            vkDestroyRenderPass(logical_device, render_pass, nullptr);

            for (auto image_view : swap_chain_image_views)
            {
                vkDestroyImageView(logical_device, image_view, nullptr);
            }

            vkDestroySwapchainKHR(logical_device, swap_chain, nullptr);

            if (window_surface)
            {
                vkDestroySurfaceKHR(vulkan_instance, window_surface, nullptr);
            }

            vkDestroyDevice(logical_device, nullptr);

            if (debug_utils_messenger)
            {
                DestroyDebugUtilsMessengerEXT(vulkan_instance,
                                              debug_utils_messenger,
                                              nullptr);
            }

            vkDestroyInstance(vulkan_instance, nullptr);
            glfwTerminate();
        }

        void init(liboceanlight::window&);
        VkInstance create_vulkan_instance();
        void run(liboceanlight::window&);
        VkPhysicalDevice pick_physical_device();
        queue_family_indices_struct find_queue_families();
        bool device_is_suitable(queue_family_indices_struct&,
                                VkPhysicalDevice&,
                                VkSurfaceKHR&,
                                const std::vector<const char*>&);
        VkSwapchainKHR create_swap_chain(liboceanlight::window&,
                                         swap_chain_support_details&);
        std::vector<VkImageView> create_image_views();
        void create_graphics_pipeline();
        VkShaderModule create_shader_module(const std::vector<char>&);
        void create_render_pass();
        void create_framebuffers();
        void create_command_pool();
        void create_command_buffer();
        void record_command_buffer(VkCommandBuffer&, uint32_t);
        void draw_frame();
        void create_sync_objects();
    };
} /* namespace liboceanlight */

VkPhysicalDevice pick_physical_device(VkInstance&,
                                      queue_family_indices_struct&);

VkDevice create_logical_device(VkPhysicalDevice&,
                               queue_family_indices_struct&,
                               const std::vector<const char*>&);

bool check_layer_support(const std::vector<const char*>&);
bool check_extension_support(const std::vector<const char*>&);
void setup_instance_layers(VkInstanceCreateInfo&, std::vector<const char*>&);
void setup_instance_extensions(VkInstanceCreateInfo&,
                               std::vector<const char*>&);
void setup_dbg_utils_msngr(std::vector<const char*>&,
                           VkDebugUtilsMessengerCreateInfoEXT&,
                           VkInstanceCreateInfo&);

uint32_t rate_device_suitability(const VkPhysicalDevice&);
VkApplicationInfo populate_instance_app_info(void);
VkInstanceCreateInfo populate_instance_create_info(VkApplicationInfo&);
VkDeviceQueueCreateInfo populate_queue_create_info(uint32_t&);

VkDeviceCreateInfo populate_device_create_info(
    VkPhysicalDeviceFeatures&,
    std::vector<VkDeviceQueueCreateInfo>&,
    const std::vector<const char*>&);

std::vector<const char*> get_required_instance_extensions(void);
struct swap_chain_support_details get_swap_chain_support_details(
    VkPhysicalDevice&,
    VkSurfaceKHR&);
VkSurfaceFormatKHR choose_swap_surface_format(
    const std::vector<VkSurfaceFormatKHR>&);
#endif /* LIBOCEANLIGHT_ENGINE_HPP_INCLUDED */

liboceanlight/src/lol_engine.cc

#include <algorithm>
#include <iostream>
#include <iterator>
#include <limits>
#include <ostream>
#include <stdexcept>
#include <cstdint>
#include <stdint.h>
#include <string.h>
#include <vector>
#include <map>
#include <optional>
#include <set>
#include <cstring>
#include <vulkan/vulkan.h>
#include <GLFW/glfw3.h>
#include <liboceanlight/lol_engine.hpp>
#include <liboceanlight/lol_debug_messenger.hpp>
#include <liboceanlight/lol_window.hpp>
#include <liboceanlight/lol_utility.hpp>
#include <config.h>

void liboceanlight::engine::run(liboceanlight::window& window)
{
    while (!window.should_close())
    {
        glfwWaitEvents();
        draw_frame();
    }

    vkDeviceWaitIdle(logical_device);
}

void liboceanlight::engine::draw_frame()
{
    vkWaitForFences(logical_device, 1, &in_flight_fence, VK_TRUE, UINT64_MAX);
    vkResetFences(logical_device, 1, &in_flight_fence);

    uint32_t image_index;
    vkAcquireNextImageKHR(logical_device,
                          swap_chain,
                          UINT64_MAX,
                          image_available_semaphore,
                          VK_NULL_HANDLE,
                          &image_index);

    vkResetCommandBuffer(command_buffer, 0);
    record_command_buffer(command_buffer, image_index);

    VkSubmitInfo submit_info {};
    submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;

    VkSemaphore wait_semaphores[] = {image_available_semaphore};
    VkPipelineStageFlags wait_stages[] = {
        VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
    submit_info.waitSemaphoreCount = 1;
    submit_info.pWaitSemaphores = wait_semaphores;
    submit_info.pWaitDstStageMask = wait_stages;
    submit_info.commandBufferCount = 1;
    submit_info.pCommandBuffers = &command_buffer;

    VkSemaphore signal_semaphores[] = {rendering_finished_semaphore};
    submit_info.signalSemaphoreCount = 1;
    submit_info.pSignalSemaphores = signal_semaphores;

    auto rv = vkQueueSubmit(graphics_queue, 1, &submit_info, in_flight_fence);

    if (rv != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to submit command buffer");
    }

    VkPresentInfoKHR present_info {};
    present_info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
    present_info.waitSemaphoreCount = 1;
    present_info.pWaitSemaphores = signal_semaphores;

    VkSwapchainKHR swap_chains[] = {swap_chain};
    present_info.swapchainCount = 1;
    present_info.pSwapchains = swap_chains;
    present_info.pImageIndices = &image_index;
    present_info.pResults = nullptr;

    vkQueuePresentKHR(graphics_queue, &present_info);
}

void liboceanlight::engine::init(liboceanlight::window& window)
{
    vulkan_instance = create_vulkan_instance();
    window_surface = window.create_window_surface(vulkan_instance);
    physical_device = pick_physical_device();
    queue_family_indices = find_queue_families();

    if (!device_is_suitable(queue_family_indices,
                            physical_device,
                            window_surface,
                            device_extensions))
    {
        throw std::runtime_error("Device lacks required queue family.");
    }

    logical_device = create_logical_device(physical_device,
                                           queue_family_indices,
                                           device_extensions);

    vkGetDeviceQueue(logical_device,
                     queue_family_indices.graphics_queue_family.value(),
                     0,
                     &graphics_queue);

    if (!graphics_queue)
    {
        throw std::runtime_error("Could not get device queue handle.");
    }

    vkGetDeviceQueue(logical_device,
                     queue_family_indices.presentation_queue_family.value(),
                     0,
                     &present_queue);

    if (!present_queue)
    {
        throw std::runtime_error("Could not get presentation queue handle.");
    }

    swap_chain = create_swap_chain(window, swap_details);
    swap_chain_image_views = create_image_views();
    create_render_pass();
    create_graphics_pipeline();
    create_framebuffers();
    create_command_pool();
    create_command_buffer();
    create_sync_objects();
}

/* Create the Vulkan instance */
VkInstance liboceanlight::engine::create_vulkan_instance()
{
    /* Optional - VkApplicationInfo is used for game-specific optimizations by
     * hardware vendors (IHVs) */
    VkApplicationInfo instance_app_info;
    instance_app_info = populate_instance_app_info();

    /* Create and populate VkInstanceCreateInfo, used by VkCreateInstance to
     * set basic parameters of the vulkan instance. We will fill in more as we
     * go until passing to VkCreateInstance */
    VkInstanceCreateInfo instance_create_info;
    instance_create_info = populate_instance_create_info(instance_app_info);

    /* Get the instance extensions required by the windowing API */
    std::vector<const char*> extensions = get_required_instance_extensions();
    std::vector<const char*> layers {};

    /* If validation layers are enabled, check if they are supported. If they
     * are, then set them up, along with the debug utils messenger */
    bool layers_supported {false}, extensions_supported {false};
    VkDebugUtilsMessengerCreateInfoEXT dbg_utils_msngr_create_info {};
    if (validation_layers_enabled)
    {
        layers.push_back("VK_LAYER_KHRONOS_validation");
        layers_supported = check_layer_support(layers);
        if (layers_supported)
        {
            setup_dbg_utils_msngr(extensions,
                                  dbg_utils_msngr_create_info,
                                  instance_create_info);
        }
        else
        {
            layers.pop_back();
        }
    }

    /* Set up our instance layers */
    setup_instance_layers(instance_create_info, layers);

    /* Check if all of our instance extensions are supported. If they are not,
     * we have a problem */
    extensions_supported = check_extension_support(extensions);
    if (extensions_supported)
    {
        setup_instance_extensions(instance_create_info, extensions);
    }
    else
    {
        std::runtime_error("Missing some required instance extensions.");
    }

    /* Create our instance */
    VkInstance instance {nullptr};
    VkResult rv = vkCreateInstance(&instance_create_info, nullptr, &instance);

    if (rv != VK_SUCCESS || !instance)
    {
        throw std::runtime_error("Failed to create Vulkan instance.");
    }

    /* In vulkan, the debug utils messenger can be created twice. Once before
     * instance creation, and once after. This is so we can print debug
     * messages relevant to the instance creation itself. We've already used
     * the "pre-instance" debug messenger by setting pNext in our struct, so
     * now we will create the "post-instance" debug messenger, which will take
     * over from here on out */
    if (layers_supported)
    {
        rv = CreateDebugUtilsMessengerEXT(instance,
                                          &dbg_utils_msngr_create_info,
                                          nullptr,
                                          &debug_utils_messenger);

        if (rv != VK_SUCCESS)
        {
            throw std::runtime_error("Failed to set up debug messenger.");
        }
    }

    return instance;
}

VkPhysicalDevice liboceanlight::engine::pick_physical_device()
{
    uint32_t device_count {0};
    VkPhysicalDevice physical_device {VK_NULL_HANDLE};
    vkEnumeratePhysicalDevices(vulkan_instance, &device_count, nullptr);

    if (device_count == 0)
    {
        throw std::runtime_error("Failed to find supported device");
    }

    std::vector<VkPhysicalDevice> physical_devices(device_count);
    vkEnumeratePhysicalDevices(vulkan_instance,
                               &device_count,
                               physical_devices.data());

    std::multimap<uint32_t, VkPhysicalDevice> device_candidates;
    for (const auto& device : physical_devices)
    {
        uint32_t score = rate_device_suitability(device);
        device_candidates.insert(std::make_pair(score, device));

        /* Multimap is sorted, so rbegin()->first contains highest score */
        if (device_candidates.rbegin()->first > 0)
        {
            physical_device = device_candidates.rbegin()->second;
        }
        else
        {
            throw std::runtime_error("Failed to find suitable device.");
        }
    }

    if (physical_device == VK_NULL_HANDLE)
    {
        throw std::runtime_error("Found devices, but none suitable.");
    }

    return physical_device;
}

VkDevice create_logical_device(
    VkPhysicalDevice& physical_device,
    queue_family_indices_struct& indices,
    const std::vector<const char*>& device_extensions)
{
    VkDevice logical_device {nullptr};

    std::set<uint32_t> unique_queue_families {
        indices.graphics_queue_family.value(),
        indices.presentation_queue_family.value()};
    std::vector<VkDeviceQueueCreateInfo> queue_create_infos;
    for (uint32_t queue_family : unique_queue_families)
    {
        queue_create_infos.push_back(populate_queue_create_info(queue_family));
    }

    VkPhysicalDeviceFeatures features {};
    VkDeviceCreateInfo device_create_info;
    device_create_info = populate_device_create_info(features,
                                                     queue_create_infos,
                                                     device_extensions);

    VkResult rv;
    rv = vkCreateDevice(physical_device,
                        &device_create_info,
                        nullptr,
                        &logical_device);

    if (rv != VK_SUCCESS)
    {
        throw std::runtime_error("Logical device creation failed");
    }

    return logical_device;
}

struct swap_chain_support_details get_swap_chain_support_details(
    VkPhysicalDevice& device,
    VkSurfaceKHR& surface)
{
    swap_chain_support_details details;
    vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device,
                                              surface,
                                              &details.capabilities);

    uint32_t format_count;
    vkGetPhysicalDeviceSurfaceFormatsKHR(device,
                                         surface,
                                         &format_count,
                                         nullptr);

    if (format_count == 0)
    {
        throw std::runtime_error(
            "Could not get physical device surface formats");
    }

    details.formats.resize(format_count);
    vkGetPhysicalDeviceSurfaceFormatsKHR(device,
                                         surface,
                                         &format_count,
                                         details.formats.data());

    uint32_t present_mode_count;
    vkGetPhysicalDeviceSurfacePresentModesKHR(device,
                                              surface,
                                              &present_mode_count,
                                              nullptr);

    if (present_mode_count == 0)
    {
        throw std::runtime_error(
            "Could not get physical device present modes");
    }

    details.present_modes.resize(present_mode_count);
    vkGetPhysicalDeviceSurfacePresentModesKHR(device,
                                              surface,
                                              &present_mode_count,
                                              details.present_modes.data());

    return details;
}

VkSurfaceFormatKHR choose_swap_surface_format(
    const std::vector<VkSurfaceFormatKHR>& available_formats)
{
    for (const auto& available_format : available_formats)
    {
        if (available_format.format == VK_FORMAT_B8G8R8A8_SRGB &&
            available_format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
        {
            return available_format;
        }
    }

    return available_formats[0];
}

VkPresentModeKHR choose_swap_present_mode(
    const std::vector<VkPresentModeKHR>& available_present_modes)
{
    for (const auto& available_present_mode : available_present_modes)
    {
        if (available_present_mode == VK_PRESENT_MODE_MAILBOX_KHR)
        {
            return available_present_mode;
        }
    }

    return VK_PRESENT_MODE_FIFO_KHR;
}

VkExtent2D liboceanlight::window::choose_swap_extent(
    const VkSurfaceCapabilitiesKHR& capabilities)
{
    if (capabilities.currentExtent.width !=
        std::numeric_limits<uint32_t>::max())
    {
        return capabilities.currentExtent;
    }
    else
    {
        int width, height;
        glfwGetFramebufferSize(window_pointer, &width, &height);

        VkExtent2D actual_extent = {static_cast<uint32_t>(width),
                                    static_cast<uint32_t>(height)};

        actual_extent.width = std::clamp(actual_extent.width,
                                         capabilities.minImageExtent.width,
                                         capabilities.maxImageExtent.width);

        actual_extent.height = std::clamp(actual_extent.height,
                                          capabilities.minImageExtent.height,
                                          capabilities.maxImageExtent.height);

        return actual_extent;
    }
}

VkSwapchainKHR liboceanlight::engine::create_swap_chain(
    liboceanlight::window& window,
    swap_chain_support_details& details)
{
    VkSurfaceFormatKHR surface_format = choose_swap_surface_format(
        swap_details.formats);
    VkPresentModeKHR present_mode = choose_swap_present_mode(
        swap_details.present_modes);
    swap_chain_extent = window.choose_swap_extent(swap_details.capabilities);
    swap_chain_image_format = surface_format.format;

    uint32_t min_image_count = swap_details.capabilities.minImageCount;
    uint32_t image_count = min_image_count + 1;
    uint32_t max_image_count = swap_details.capabilities.maxImageCount;

    if (max_image_count > 0 && image_count > max_image_count)
    {
        image_count = max_image_count;
    }

    VkSwapchainCreateInfoKHR create_info {};
    create_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
    create_info.surface = window_surface;
    create_info.minImageCount = image_count;
    create_info.imageFormat = swap_chain_image_format;
    create_info.imageColorSpace = surface_format.colorSpace;
    create_info.imageExtent = swap_chain_extent;
    create_info.imageArrayLayers = 1;
    create_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;

    uint32_t indices[] = {
        queue_family_indices.graphics_queue_family.value(),
        queue_family_indices.presentation_queue_family.value()};

    if (queue_family_indices.graphics_queue_family !=
        queue_family_indices.presentation_queue_family)
    {
        create_info.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
        create_info.queueFamilyIndexCount = 2;
        create_info.pQueueFamilyIndices = indices;
    }
    else
    {
        create_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
        create_info.queueFamilyIndexCount = 0;
        create_info.pQueueFamilyIndices = nullptr;
    }

    create_info.preTransform = swap_details.capabilities.currentTransform;
    create_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
    create_info.presentMode = present_mode;
    create_info.clipped = VK_TRUE;
    create_info.oldSwapchain = VK_NULL_HANDLE;

    VkSwapchainKHR sw {nullptr};

    if (vkCreateSwapchainKHR(logical_device, &create_info, nullptr, &sw) !=
        VK_SUCCESS)
    {
        std::runtime_error("Failed to create swap chain");
    }

    vkGetSwapchainImagesKHR(logical_device, sw, &image_count, nullptr);
    swap_chain_images.resize(image_count);
    vkGetSwapchainImagesKHR(logical_device,
                            sw,
                            &image_count,
                            swap_chain_images.data());

    return sw;
}

std::vector<VkImageView> liboceanlight::engine::create_image_views()
{
    std::vector<VkImageView> image_views;
    image_views.resize(swap_chain_images.size());

    for (size_t i {0}; i < swap_chain_images.size(); ++i)
    {
        VkImageViewCreateInfo create_info {};
        create_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
        create_info.image = swap_chain_images[i];
        create_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
        create_info.format = swap_chain_image_format;
        create_info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
        create_info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
        create_info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
        create_info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
        create_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
        create_info.subresourceRange.baseMipLevel = 0;
        create_info.subresourceRange.levelCount = 1;
        create_info.subresourceRange.baseArrayLayer = 0;
        create_info.subresourceRange.layerCount = 1;

        if (vkCreateImageView(logical_device,
                              &create_info,
                              nullptr,
                              &image_views[i]) != VK_SUCCESS)
        {
            throw std::runtime_error("Failed to create image views.");
        }
    }

    return image_views;
}

void liboceanlight::engine::create_graphics_pipeline()
{
    auto vertex_shader_code = read_file(SHADER_PATH "vertex_shader.spv");
    auto fragment_shader_code = read_file(SHADER_PATH "fragment_shader.spv");

    VkShaderModule vertex_shader = create_shader_module(vertex_shader_code);
    VkShaderModule fragment_shader = create_shader_module(
        fragment_shader_code);

    VkPipelineShaderStageCreateInfo vertex_shader_stage_create_info {};
    vertex_shader_stage_create_info.sType =
        VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
    vertex_shader_stage_create_info.stage = VK_SHADER_STAGE_VERTEX_BIT;
    vertex_shader_stage_create_info.module = vertex_shader;
    vertex_shader_stage_create_info.pName = "main";

    VkPipelineShaderStageCreateInfo fragment_shader_stage_create_info {};
    fragment_shader_stage_create_info.sType =
        VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
    fragment_shader_stage_create_info.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
    fragment_shader_stage_create_info.module = fragment_shader;
    fragment_shader_stage_create_info.pName = "main";

    VkPipelineShaderStageCreateInfo shader_stages[] = {
        vertex_shader_stage_create_info,
        fragment_shader_stage_create_info};

    VkPipelineVertexInputStateCreateInfo vertex_input_create_info {};
    vertex_input_create_info.sType =
        VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
    vertex_input_create_info.vertexBindingDescriptionCount = 0;
    vertex_input_create_info.pVertexBindingDescriptions = nullptr;
    vertex_input_create_info.vertexAttributeDescriptionCount = 0;
    vertex_input_create_info.pVertexAttributeDescriptions = nullptr;

    VkPipelineInputAssemblyStateCreateInfo input_assembly_create_info {};
    input_assembly_create_info.sType =
        VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
    input_assembly_create_info.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
    input_assembly_create_info.primitiveRestartEnable = VK_FALSE;

    VkViewport viewport {};
    viewport.x = 0.0f;
    viewport.y = 0.0f;
    viewport.width = (float)swap_chain_extent.width;
    viewport.height = (float)swap_chain_extent.height;
    viewport.minDepth = 0.0f;
    viewport.maxDepth = 1.0f;

    VkRect2D scissor {};
    scissor.offset = {0, 0};
    scissor.extent = swap_chain_extent;

    std::vector<VkDynamicState> dynamic_states = {VK_DYNAMIC_STATE_VIEWPORT,
                                                  VK_DYNAMIC_STATE_SCISSOR};

    VkPipelineDynamicStateCreateInfo dynamic_state_create_info {};
    dynamic_state_create_info.sType =
        VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
    dynamic_state_create_info.dynamicStateCount = static_cast<uint32_t>(
        dynamic_states.size());
    dynamic_state_create_info.pDynamicStates = dynamic_states.data();

    VkPipelineViewportStateCreateInfo viewport_state_create_info {};
    viewport_state_create_info.sType =
        VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
    viewport_state_create_info.viewportCount = 1;
    viewport_state_create_info.scissorCount = 1;

    VkPipelineRasterizationStateCreateInfo rasterizer_state_create_info {};
    rasterizer_state_create_info.sType =
        VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
    rasterizer_state_create_info.depthClampEnable = VK_FALSE;
    rasterizer_state_create_info.rasterizerDiscardEnable = VK_FALSE;
    rasterizer_state_create_info.polygonMode = VK_POLYGON_MODE_FILL;
    rasterizer_state_create_info.lineWidth = 1.0f;
    rasterizer_state_create_info.cullMode = VK_CULL_MODE_BACK_BIT;
    rasterizer_state_create_info.frontFace = VK_FRONT_FACE_CLOCKWISE;
    rasterizer_state_create_info.depthBiasEnable = VK_FALSE;
    rasterizer_state_create_info.depthBiasConstantFactor = 0.0f;
    rasterizer_state_create_info.depthBiasClamp = 0.0f;
    rasterizer_state_create_info.depthBiasSlopeFactor = 0.0f;

    VkPipelineMultisampleStateCreateInfo multisampling_state_create_info {};
    multisampling_state_create_info.sType =
        VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
    multisampling_state_create_info.sampleShadingEnable = VK_FALSE;
    multisampling_state_create_info.rasterizationSamples =
        VK_SAMPLE_COUNT_1_BIT;
    multisampling_state_create_info.minSampleShading = 1.0f;
    multisampling_state_create_info.pSampleMask = nullptr;
    multisampling_state_create_info.alphaToCoverageEnable = VK_FALSE;
    multisampling_state_create_info.alphaToOneEnable = VK_FALSE;

    VkPipelineColorBlendAttachmentState
        color_blend_attachment_state_create_info {};
    color_blend_attachment_state_create_info.colorWriteMask =
        VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
        VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
    color_blend_attachment_state_create_info.blendEnable = VK_FALSE;
    color_blend_attachment_state_create_info.srcColorBlendFactor =
        VK_BLEND_FACTOR_ONE;
    color_blend_attachment_state_create_info.dstColorBlendFactor =
        VK_BLEND_FACTOR_ZERO;
    color_blend_attachment_state_create_info.colorBlendOp = VK_BLEND_OP_ADD;
    color_blend_attachment_state_create_info.srcAlphaBlendFactor =
        VK_BLEND_FACTOR_ONE;
    color_blend_attachment_state_create_info.dstAlphaBlendFactor =
        VK_BLEND_FACTOR_ZERO;
    color_blend_attachment_state_create_info.alphaBlendOp = VK_BLEND_OP_ADD;

    VkPipelineColorBlendStateCreateInfo color_blend_state_create_info {};
    color_blend_state_create_info.sType =
        VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
    color_blend_state_create_info.logicOpEnable = VK_FALSE;
    color_blend_state_create_info.logicOp = VK_LOGIC_OP_COPY;
    color_blend_state_create_info.attachmentCount = 1;
    color_blend_state_create_info.pAttachments =
        &color_blend_attachment_state_create_info;
    color_blend_state_create_info.blendConstants[0] = 0.0f;
    color_blend_state_create_info.blendConstants[1] = 0.0f;
    color_blend_state_create_info.blendConstants[2] = 0.0f;
    color_blend_state_create_info.blendConstants[3] = 0.0f;

    VkPipelineLayoutCreateInfo pipeline_layout_create_info {};
    pipeline_layout_create_info.sType =
        VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
    pipeline_layout_create_info.setLayoutCount = 0;
    pipeline_layout_create_info.pSetLayouts = nullptr;
    pipeline_layout_create_info.pushConstantRangeCount = 0;
    pipeline_layout_create_info.pPushConstantRanges = nullptr;

    auto rv = vkCreatePipelineLayout(logical_device,
                                     &pipeline_layout_create_info,
                                     nullptr,
                                     &pipeline_layout);

    if (rv != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to create pipeline layout");
    }

    VkGraphicsPipelineCreateInfo graphics_pipeline_create_info {};
    graphics_pipeline_create_info.sType =
        VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
    graphics_pipeline_create_info.stageCount = 2;
    graphics_pipeline_create_info.pStages = shader_stages;
    graphics_pipeline_create_info.pVertexInputState =
        &vertex_input_create_info;
    graphics_pipeline_create_info.pInputAssemblyState =
        &input_assembly_create_info;
    graphics_pipeline_create_info.pViewportState = &viewport_state_create_info;
    graphics_pipeline_create_info.pRasterizationState =
        &rasterizer_state_create_info;
    graphics_pipeline_create_info.pMultisampleState =
        &multisampling_state_create_info;
    graphics_pipeline_create_info.pDepthStencilState = nullptr;
    graphics_pipeline_create_info.pColorBlendState =
        &color_blend_state_create_info;
    graphics_pipeline_create_info.pDynamicState = &dynamic_state_create_info;
    graphics_pipeline_create_info.layout = pipeline_layout;
    graphics_pipeline_create_info.renderPass = render_pass;
    graphics_pipeline_create_info.subpass = 0;
    graphics_pipeline_create_info.basePipelineHandle = VK_NULL_HANDLE;
    graphics_pipeline_create_info.basePipelineIndex = -1;

    rv = vkCreateGraphicsPipelines(logical_device,
                                   VK_NULL_HANDLE,
                                   1,
                                   &graphics_pipeline_create_info,
                                   NULL,
                                   &graphics_pipeline);

    if (rv != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to create graphics pipeline");
    }

    vkDestroyShaderModule(logical_device, vertex_shader, nullptr);
    vkDestroyShaderModule(logical_device, fragment_shader, nullptr);
}

void liboceanlight::engine::create_render_pass()
{
    VkAttachmentDescription color_attachment_description {};
    color_attachment_description.format = swap_chain_image_format;
    color_attachment_description.samples = VK_SAMPLE_COUNT_1_BIT;
    color_attachment_description.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
    color_attachment_description.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
    color_attachment_description.stencilLoadOp =
        VK_ATTACHMENT_LOAD_OP_DONT_CARE;
    color_attachment_description.stencilStoreOp =
        VK_ATTACHMENT_STORE_OP_DONT_CARE;
    color_attachment_description.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    color_attachment_description.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

    VkAttachmentReference color_attachment_reference {};
    color_attachment_reference.attachment = 0;
    color_attachment_reference.layout =
        VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

    VkSubpassDescription subpass_description {};
    subpass_description.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
    subpass_description.colorAttachmentCount = 1;
    subpass_description.pColorAttachments = &color_attachment_reference;

    VkSubpassDependency subpass_dependency {};
    subpass_dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
    subpass_dependency.dstSubpass = 0;
    subpass_dependency.srcStageMask =
        VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    subpass_dependency.dstStageMask =
        VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    subpass_dependency.srcAccessMask = 0;
    subpass_dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

    VkRenderPassCreateInfo render_pass_create_info {};
    render_pass_create_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
    render_pass_create_info.attachmentCount = 1;
    render_pass_create_info.pAttachments = &color_attachment_description;
    render_pass_create_info.subpassCount = 1;
    render_pass_create_info.pSubpasses = &subpass_description;
    render_pass_create_info.dependencyCount = 1;
    render_pass_create_info.pDependencies = &subpass_dependency;

    auto rv = vkCreateRenderPass(logical_device,
                                 &render_pass_create_info,
                                 nullptr,
                                 &render_pass);

    if (rv != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to create render pass");
    }
}

void liboceanlight::engine::create_framebuffers()
{
    swap_chain_frame_buffers.resize(swap_chain_image_views.size());

    for (size_t i {0}; i < swap_chain_image_views.size(); ++i)
    {
        VkImageView attachments[] = {swap_chain_image_views[i]};

        VkFramebufferCreateInfo frame_buffer_create_info {};
        frame_buffer_create_info.sType =
            VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
        frame_buffer_create_info.renderPass = render_pass;
        frame_buffer_create_info.attachmentCount = 1;
        frame_buffer_create_info.pAttachments = attachments;
        frame_buffer_create_info.width = swap_chain_extent.width;
        frame_buffer_create_info.height = swap_chain_extent.height;
        frame_buffer_create_info.layers = 1;

        auto rv = vkCreateFramebuffer(logical_device,
                                      &frame_buffer_create_info,
                                      nullptr,
                                      &swap_chain_frame_buffers[i]);

        if (rv != VK_SUCCESS)
        {
            throw std::runtime_error("Failed to create image frambuffers");
        }
    }
}

void liboceanlight::engine::create_command_pool()
{
    VkCommandPoolCreateInfo command_pool_create_info {};
    command_pool_create_info.sType =
        VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
    command_pool_create_info.flags =
        VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
    command_pool_create_info.queueFamilyIndex =
        queue_family_indices.graphics_queue_family.value();

    auto rv = vkCreateCommandPool(logical_device,
                                  &command_pool_create_info,
                                  nullptr,
                                  &command_pool);

    if (rv != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to create command pool");
    }
}

void liboceanlight::engine::create_command_buffer()
{
    VkCommandBufferAllocateInfo command_buffer_allocate_create_info {};
    command_buffer_allocate_create_info.sType =
        VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    command_buffer_allocate_create_info.commandPool = command_pool;
    command_buffer_allocate_create_info.level =
        VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    command_buffer_allocate_create_info.commandBufferCount = 1;

    auto rv = vkAllocateCommandBuffers(logical_device,
                                       &command_buffer_allocate_create_info,
                                       &command_buffer);

    if (rv != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to allocate command buffers");
    }
}

void liboceanlight::engine::record_command_buffer(
    VkCommandBuffer& command_buffer,
    uint32_t image_index)
{
    VkCommandBufferBeginInfo command_buffer_begin_info {};
    command_buffer_begin_info.sType =
        VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    command_buffer_begin_info.flags = 0;
    command_buffer_begin_info.pInheritanceInfo = nullptr;

    auto rv = vkBeginCommandBuffer(command_buffer, &command_buffer_begin_info);

    if (rv != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to record command buffer");
    }

    VkRenderPassBeginInfo render_pass_begin_info {};
    render_pass_begin_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
    render_pass_begin_info.renderPass = render_pass;
    render_pass_begin_info.framebuffer = swap_chain_frame_buffers[image_index];
    render_pass_begin_info.renderArea.offset = {0, 0};
    render_pass_begin_info.renderArea.extent = swap_chain_extent;

    VkClearValue clear_value = {{{0.0f, 0.0f, 0.0f, 1.0f}}};
    render_pass_begin_info.clearValueCount = 1;
    render_pass_begin_info.pClearValues = &clear_value;

    vkCmdBeginRenderPass(command_buffer,
                         &render_pass_begin_info,
                         VK_SUBPASS_CONTENTS_INLINE);

    vkCmdBindPipeline(command_buffer,
                      VK_PIPELINE_BIND_POINT_GRAPHICS,
                      graphics_pipeline);

    VkViewport viewport {};
    viewport.x = 0.0f;
    viewport.y = 0.0f;
    viewport.width = static_cast<float>(swap_chain_extent.width);
    viewport.height = static_cast<float>(swap_chain_extent.height);
    viewport.minDepth = 0.0f;
    viewport.maxDepth = 1.0f;
    vkCmdSetViewport(command_buffer, 0, 1, &viewport);

    VkRect2D scissor {};
    scissor.offset = {0, 0};
    scissor.extent = swap_chain_extent;
    vkCmdSetScissor(command_buffer, 0, 1, &scissor);

    vkCmdDraw(command_buffer, 3, 1, 0, 0);
    vkCmdEndRenderPass(command_buffer);

    if (vkEndCommandBuffer(command_buffer) != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to end command buffer");
    }
}

void liboceanlight::engine::create_sync_objects()
{
    VkSemaphoreCreateInfo semaphore_create_info {};
    semaphore_create_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;

    VkFenceCreateInfo fence_create_info {};
    fence_create_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
    fence_create_info.flags = VK_FENCE_CREATE_SIGNALED_BIT;

    auto rv1 = vkCreateSemaphore(logical_device,
                                 &semaphore_create_info,
                                 nullptr,
                                 &image_available_semaphore);

    auto rv2 = vkCreateSemaphore(logical_device,
                                 &semaphore_create_info,
                                 nullptr,
                                 &rendering_finished_semaphore);

    auto rv3 = vkCreateFence(logical_device,
                             &fence_create_info,
                             nullptr,
                             &in_flight_fence);

    if (rv1 != VK_SUCCESS || rv2 != VK_SUCCESS || rv3 != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to create synchronization objects");
    }
}

VkShaderModule liboceanlight::engine::create_shader_module(
    const std::vector<char>& shader_code)
{
    VkShaderModuleCreateInfo create_info {};
    create_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
    create_info.codeSize = shader_code.size();
    create_info.pCode = reinterpret_cast<const uint32_t*>(shader_code.data());

    VkShaderModule shader_module;
    auto rv = vkCreateShaderModule(logical_device,
                                   &create_info,
                                   nullptr,
                                   &shader_module);
    if (rv != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to create shader module");
    }

    return shader_module;
}

uint32_t rate_device_suitability(const VkPhysicalDevice& physical_device)
{
    uint32_t score {0};

    VkPhysicalDeviceProperties device_properties;
    vkGetPhysicalDeviceProperties(physical_device, &device_properties);

    VkPhysicalDeviceFeatures device_features;
    vkGetPhysicalDeviceFeatures(physical_device, &device_features);

    if (!device_features.geometryShader)
    {
        return 0;
    }

    if (device_properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)
    {
        score += 100;
    }

    score += device_properties.limits.maxImageDimension2D;

    return score;
}

queue_family_indices_struct liboceanlight::engine::find_queue_families()
{
    queue_family_indices_struct indices;
    VkBool32 present_support {false};
    uint32_t queue_family_count {0};
    vkGetPhysicalDeviceQueueFamilyProperties(physical_device,
                                             &queue_family_count,
                                             nullptr);

    std::vector<VkQueueFamilyProperties> queue_families(queue_family_count);
    vkGetPhysicalDeviceQueueFamilyProperties(physical_device,
                                             &queue_family_count,
                                             queue_families.data());

    std::cout << "Queue families are as follows:\n";

    VkResult rv;
    int i {0};
    for (const auto& queue_family : queue_families)
    {
        std::cout << "Queue Count: " << queue_family.queueCount << "\n"
                  << "Queue Type: "
                  << queue_flags_to_string(queue_family.queueFlags) << "\n";

        if (queue_family.queueFlags & VK_QUEUE_GRAPHICS_BIT)
        {
            indices.graphics_queue_family = i;
        }

        rv = vkGetPhysicalDeviceSurfaceSupportKHR(physical_device,
                                                  i,
                                                  window_surface,
                                                  &present_support);

        if (rv != VK_SUCCESS)
        {
            throw std::runtime_error("Could not determine surface support");
        }

        if (present_support)
        {
            indices.presentation_queue_family = i;
        }

        ++i;
    }

    return indices;
}

bool check_device_extension_support(
    VkPhysicalDevice& device,
    const std::vector<const char*>& device_extensions)
{
    uint32_t extension_count;
    vkEnumerateDeviceExtensionProperties(device,
                                         nullptr,
                                         &extension_count,
                                         nullptr);

    std::vector<VkExtensionProperties> extension_properties(extension_count);
    vkEnumerateDeviceExtensionProperties(device,
                                         nullptr,
                                         &extension_count,
                                         extension_properties.data());

    std::set<std::string> required_extensions(device_extensions.begin(),
                                              device_extensions.end());

    for (const auto& extension : extension_properties)
    {
        required_extensions.erase(extension.extensionName);
    }

    return required_extensions.empty();
}

bool liboceanlight::engine::device_is_suitable(
    queue_family_indices_struct& indices,
    VkPhysicalDevice& device,
    VkSurfaceKHR& window_surface,
    const std::vector<const char*>& device_extensions)
{
    bool extensions_supported = check_device_extension_support(
        device,
        device_extensions);

    bool swap_chain_adequate = false;
    if (extensions_supported)
    {
        swap_details = get_swap_chain_support_details(device, window_surface);

        swap_chain_adequate = !swap_details.formats.empty() &&
                              !swap_details.present_modes.empty();
    }

    return indices.graphics_queue_family.has_value() &&
           indices.presentation_queue_family.has_value() &&
           extensions_supported && swap_chain_adequate;
}

void setup_dbg_utils_msngr(
    std::vector<const char*>& extensions,
    VkDebugUtilsMessengerCreateInfoEXT& dbg_utils_msngr_create_info,
    VkInstanceCreateInfo& instance_create_info)
{
    extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
    dbg_utils_msngr_create_info = populate_dbg_utils_msngr_create_info();
    instance_create_info.pNext =
        (VkDebugUtilsMessengerCreateInfoEXT*)&dbg_utils_msngr_create_info;
}

void setup_instance_layers(VkInstanceCreateInfo& c_info,
                           std::vector<const char*>& layers)
{
    if (layers.size() > 0)
    {
        c_info.enabledLayerCount = static_cast<uint32_t>(layers.size());
        c_info.ppEnabledLayerNames = layers.data();
    }
}

void setup_instance_extensions(VkInstanceCreateInfo& c_info,
                               std::vector<const char*>& extensions)
{
    c_info.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
    c_info.ppEnabledExtensionNames = extensions.data();
}

bool check_layer_support(const std::vector<const char*>& layers)
{
    uint32_t layer_count {0};
    vkEnumerateInstanceLayerProperties(&layer_count, nullptr);

    std::vector<VkLayerProperties> layer_properties(layer_count);
    vkEnumerateInstanceLayerProperties(&layer_count, layer_properties.data());

    int layers_found {0};
    bool layer_found, all_layers_found {false};
    for (const char* layer : layers)
    {
        layer_found = false;

        for (const auto& i : layer_properties)
        {
            if (strcmp(layer, i.layerName) == 0)
            {
                layer_found = true;
                ++layers_found;
                std::cout << "Layer Found: " << layer << "\n";
                break;
            }
        }

        if (!layer_found)
        {
            std::cerr << "Layer Not Found: " << layer << "\n";
        }
    }

    if (layers_found == layers.size())
    {
        all_layers_found = true;
        std::cout << "ALL INSTANCE LAYERS FOUND\n";
    }

    return all_layers_found;
}

bool check_extension_support(const std::vector<const char*>& extensions)
{
    uint32_t extension_count {0};
    vkEnumerateInstanceExtensionProperties(nullptr, &extension_count, nullptr);

    std::vector<VkExtensionProperties> extension_properties(extension_count);
    vkEnumerateInstanceExtensionProperties(nullptr,
                                           &extension_count,
                                           extension_properties.data());

    int extensions_found {0};
    bool extension_found, all_extensions_found {false};
    for (const char* extension : extensions)
    {
        extension_found = false;

        for (const auto& i : extension_properties)
        {
            if (strcmp(extension, i.extensionName) == 0)
            {
                extension_found = true;
                ++extensions_found;
                std::cout << "Extension Found: " << extension << "\n";
            }
        }

        if (!extension_found)
        {
            std::cerr << "Extension Not Found: " << extension << "\n";
        }
    }

    if (extensions_found == extensions.size())
    {
        all_extensions_found = true;
        std::cout << "ALL INSTANCE EXTENSIONS FOUND\n";
    }

    return all_extensions_found;
}

std::vector<const char*> get_required_instance_extensions()
{
    uint32_t count {0};
    const char** extensions = glfwGetRequiredInstanceExtensions(&count);
    std::vector<const char*> extensions_vec(extensions, extensions + count);
    return extensions_vec;
}

VkInstanceCreateInfo populate_instance_create_info(VkApplicationInfo& app_info)
{
    VkInstanceCreateInfo create_info {};
    create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
    create_info.pApplicationInfo = &app_info;
    create_info.pNext = nullptr;
    return create_info;
}

VkApplicationInfo populate_instance_app_info()
{
    VkApplicationInfo app_info {};
    app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
    app_info.pApplicationName = PROJECT_NAME;
    app_info.pEngineName = PROJECT_NAME;
    app_info.apiVersion = VK_API_VERSION_1_3;
    app_info.pNext = nullptr;

    app_info.applicationVersion = VK_MAKE_VERSION(PROJECT_VER_MAJOR,
                                                  PROJECT_VER_MINOR,
                                                  PROJECT_VER_PATCH);

    app_info.engineVersion = VK_MAKE_VERSION(PROJECT_VER_MAJOR,
                                             PROJECT_VER_MINOR,
                                             PROJECT_VER_PATCH);

    return app_info;
}

VkDeviceQueueCreateInfo populate_queue_create_info(uint32_t& queue_family)
{
    VkDeviceQueueCreateInfo create_info {};
    create_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    create_info.queueFamilyIndex = queue_family;
    create_info.queueCount = 1;
    float queue_priority = 1.0f;
    create_info.pQueuePriorities = &queue_priority;
    return create_info;
}

VkDeviceCreateInfo populate_device_create_info(
    VkPhysicalDeviceFeatures& features,
    std::vector<VkDeviceQueueCreateInfo>& queue_create_infos,
    const std::vector<const char*>& device_extensions)
{
    VkDeviceCreateInfo create_info {};
    create_info.queueCreateInfoCount = static_cast<uint32_t>(
        queue_create_infos.size());
    create_info.pQueueCreateInfos = queue_create_infos.data();
    create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
    create_info.pEnabledFeatures = &features;
    create_info.enabledExtensionCount = static_cast<uint32_t>(
        device_extensions.size());
    create_info.ppEnabledExtensionNames = device_extensions.data();

    return create_info;
}

liboceanlight/include/liboceanlight/lol_window.hpp

#ifndef LIBOCEANLIGHT_WINDOW_HPP_INCLUDED
#define LIBOCEANLIGHT_WINDOW_HPP_INCLUDED
#include <string>
#include <GLFW/glfw3.h>
#include <vulkan/vulkan.h>

namespace liboceanlight
{
    class window
    {
        int width, height;
        const std::string window_name {"Oceanlight"};
        GLFWwindow* window_pointer {nullptr};

        public:
        window(int w, int h);
        ~window();

        int should_close();
        VkSurfaceKHR create_window_surface(VkInstance&);
        VkExtent2D choose_swap_extent(const VkSurfaceCapabilitiesKHR&);
    };
}
#endif /* LIBOCEANLIGHT_WINDOW_HPP_INCLUDED */

liboceanlight/src/lol_window.cc

#include <string>
#include <stdexcept>
#include <vulkan/vulkan.h>
#include <GLFW/glfw3.h>
#include <liboceanlight/lol_window.hpp>
#include <liboceanlight/lol_glfw_key_callback.hpp>
#include <liboceanlight/lol_glfw_err_callback.hpp>

namespace liboceanlight
{
    window::window(int w, int h) : width(w), height(h)
    {
        glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
        glfwWindowHint(GLFW_VISIBLE, GLFW_TRUE);
        glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

        window_pointer = glfwCreateWindow(width,
                                          height,
                                          window_name.c_str(),
                                          nullptr,
                                          nullptr);

        if (window_pointer == NULL)
        {
            throw std::runtime_error("Could not create window");
        }
        
        glfwSetKeyCallback(window_pointer, lol_glfw_key_callback);
        glfwSetErrorCallback(lol_glfw_error_callback);
    }

    window::~window()
    {
        glfwDestroyWindow(window_pointer);
    }

    VkSurfaceKHR window::create_window_surface(VkInstance& instance)
    {
        VkSurfaceKHR surface {nullptr};
        VkResult rv = glfwCreateWindowSurface(instance,
                                              window_pointer,
                                              nullptr,
                                              &surface);

        if (rv != VK_SUCCESS)
        {
            throw std::runtime_error("Could not create window surface.");
        }

        return surface;
    }

    int window::should_close()
    {
        return glfwWindowShouldClose(window_pointer);
    }
}

liboceanlight/include/liboceanlight/lol_debug_messenger.hpp

#ifndef LIBOCEANLIGHT_DEBUG_MESSENGER_HPP_INCLUDED
#define LIBOCEANLIGHT_DEBUG_MESSENGER_HPP_INCLUDED
#include <iostream>
#include <vulkan/vulkan.h>
#include <string>
#include <vector>

namespace liboceanlight
{
    std::string version_string(void);
}

bool check_vldn_layer_support(const std::vector<const char*>&);
VkApplicationInfo populate_vulkan_application_info();
VkInstanceCreateInfo populate_vulkan_create_info(VkApplicationInfo*);
VkDebugUtilsMessengerCreateInfoEXT populate_dbg_utils_msngr_create_info();

VkResult CreateDebugUtilsMessengerEXT(
    VkInstance,
    const VkDebugUtilsMessengerCreateInfoEXT*,
    const VkAllocationCallbacks*,
    VkDebugUtilsMessengerEXT*);

void DestroyDebugUtilsMessengerEXT(VkInstance,
                                   VkDebugUtilsMessengerEXT,
                                   const VkAllocationCallbacks*);

VKAPI_ATTR VkBool32 VKAPI_CALL debug_utils_messenger_callback(
    VkDebugUtilsMessageSeverityFlagBitsEXT severity,
    VkDebugUtilsMessageTypeFlagsEXT type,
    const VkDebugUtilsMessengerCallbackDataEXT* callback_data,
    void* user_data);

#endif /* LIBOCEANLIGHT_DEBUG_MESSENGER_HPP_INCLUDED */

liboceanlight/src/lol_debug_messenger.cc

#include <iostream>
#include <vector>
#include <vulkan/vulkan.h>
#include <liboceanlight/lol_engine.hpp>
#include <liboceanlight/lol_debug_messenger.hpp>
#include <config.h>

VKAPI_ATTR VkBool32 VKAPI_CALL debug_utils_messenger_callback(
    VkDebugUtilsMessageSeverityFlagBitsEXT severity,
    VkDebugUtilsMessageTypeFlagsEXT type,
    const VkDebugUtilsMessengerCallbackDataEXT* callback_data,
    void* user_data)
{
    std::cout << callback_data->pMessage << "\n";

    if (severity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT)
    {
        throw std::runtime_error(
            "Vulkan validation layer detected a fatal error");
    }

    return VK_FALSE;
}

VkDebugUtilsMessengerCreateInfoEXT populate_dbg_utils_msngr_create_info()
{
    VkDebugUtilsMessengerCreateInfoEXT debug_utils_messenger_create_info {};
    debug_utils_messenger_create_info.sType =
        VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
    debug_utils_messenger_create_info.messageSeverity =
        VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT |
        VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
        VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
    debug_utils_messenger_create_info.messageType =
        VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
        VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
        VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
    debug_utils_messenger_create_info.pfnUserCallback =
        debug_utils_messenger_callback;
    debug_utils_messenger_create_info.pUserData = nullptr; // Optional
    return debug_utils_messenger_create_info;
}

VkResult CreateDebugUtilsMessengerEXT(
    VkInstance instance,
    const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo,
    const VkAllocationCallbacks* pAllocator,
    VkDebugUtilsMessengerEXT* pDebugMessenger)
{
    auto func = (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
        instance,
        "vkCreateDebugUtilsMessengerEXT");

    if (func != nullptr)
    {
        return func(instance, pCreateInfo, pAllocator, pDebugMessenger);
    }
    else
    {
        return VK_ERROR_EXTENSION_NOT_PRESENT;
    }
}

void DestroyDebugUtilsMessengerEXT(VkInstance instance,
                                   VkDebugUtilsMessengerEXT debugMessenger,
                                   const VkAllocationCallbacks* pAllocator)
{
    auto func = (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
        instance,
        "vkDestroyDebugUtilsMessengerEXT");

    if (func != nullptr)
    {
        func(instance, debugMessenger, pAllocator);
    }
}

liboceanlight/include/liboceanlight/lol_utility.hpp

#ifndef LOL_UTILITY_HPP_INCLUDED
#define LOL_UTILITY_HPP_INCLUDED
#include <vulkan/vulkan_core.h>
#include <string>
#include <vector>
namespace liboceanlight
{
    std::string queue_flags_to_string(const VkQueueFlags&);
    std::vector<char> read_file(const std::string&);
    int test_func(int, int);
}
#endif /* LOL_UTILITY_HPP_INCLUDED */

liboceanlight/src/lol_utility.cc

#include <initializer_list>
#include <iostream>
#include <fstream>
#include <map>
#include <string>
#include <sstream>
#include <vector>
#include <vulkan/vulkan_core.h>
#include <liboceanlight/lol_utility.hpp>

std::string liboceanlight::queue_flags_to_string(const VkQueueFlags& flags)
{
    std::stringstream formatted;

    std::map<unsigned int, std::string> flagbits
    {
        {VK_QUEUE_GRAPHICS_BIT, "Graphics"},
        {VK_QUEUE_COMPUTE_BIT, "Compute"},
        {VK_QUEUE_TRANSFER_BIT, "Transfer"},
        {VK_QUEUE_SPARSE_BINDING_BIT, "Sparsebinding"},
        {VK_QUEUE_PROTECTED_BIT, "Protected"},
        {VK_QUEUE_VIDEO_DECODE_BIT_KHR, "Video Decode"},
    #ifdef VK_ENABLE_BETA_EXTENSIONS
        {VK_QUEUE_VIDEO_ENCODE , "Video Encode"},
    #endif
        {VK_QUEUE_OPTICAL_FLOW_BIT_NV, "Optical Flow"}
    };
    
    for (const auto& flagbit : flagbits)
    {
        if (flags & flagbit.first)
            formatted << "|" << flagbit.second;
    }

    return formatted.str() + "|";
}

std::vector<char> liboceanlight::read_file(const std::string& filename)
{
    std::ifstream file(filename, std::ios::ate | std::ios::binary);

    if (!file.is_open())
    {
        throw std::runtime_error("Failed to open file " + filename);
    }

    size_t filesize = (size_t) file.tellg();
    std::vector<char> buffer(filesize);
    file.seekg(0);
    file.read(buffer.data(), filesize);
    file.close();

    return buffer;
}

liboceanlight/include/liboceanlight/lol_glfw_err_callback.hpp

#ifndef LIBOCEANLIGHT_GLFW_ERR_CALLBACK_HPP_INCLUDED
#define LIBOCEANLIGHT_GLFW_ERR_CALLBACK_HPP_INCLUDED
#include <GLFW/glfw3.h>
void lol_glfw_error_callback(int, const char*);
#endif /* LIBOCEANLIGHT_GLFW_ERR_CALLBACK_HPP_INCLUDED */

liboceanlight/src/lol_glfw_err_callback.cc

#include <iostream>

void lol_glfw_error_callback(int error_code, const char* description)
{
    std::cerr << "Error " << error_code << ": " << description << "\n";
}

liboceanlight/include/liboceanlight/lol_glfw_key_callback.hpp

#ifndef LIBOCEANLIGHT_GLFW_KEY_CALLBACK_HPP_INCLUDED
#define LIBOCEANLIGHT_GLFW_KEY_CALLBACK_HPP_INCLUDED
#include <GLFW/glfw3.h>
void lol_glfw_key_callback(GLFWwindow*, int, int, int, int);
#endif /* LIBOCEANLIGHT_GLFW_KEY_CALLBACK_HPP_INCLUDED */

liboceanlight/include/liboceanlight/lol_version.hpp

#ifndef LIBOCEANLIGHT_VERSION_HPP_INCLUDED
#define LIBOCEANLIGHT_VERSION_HPP_INCLUDED
#include <string>
namespace liboceanlight
{
    std::string version_string();
} /* namespace liboceanlight */
#endif /* LIBOCEANLIGHT_VERSION_HPP_INCLUDED */

liboceanlight/src/lol_version.cc

#include <string>
#include <config.h>
#include <liboceanlight/lol_version.hpp>

std::string liboceanlight::version_string()
{
    return PROJECT_NAME " Ver. " PROJECT_VER;
}

Overall I hope you'll be as nitpicky as possible with this code. Both in style and the structure of the application or lack thereof. Eventually I want to move towards a data oriented design approach and so any feedback towards that end is appreciated.

Thank you for your patience.

\$\endgroup\$
1

1 Answer 1

5
\$\begingroup\$

Given the amount of code, I can’t do a detailed review. I can’t even really do a holistic design review; that would require days of studying the code. lol_engine.cc alone deserves its own review. (I also don’t do CMake, so I can’t comment on that stuff.) So I’ll offer a scattershot review; some broad strokes, with a few highlights.

Rule of five/zero issues

A couple of your classes are buggy, if not outright dangerous. liboceanlight::engine and liboceanlight::window both violate the rule of five/zero. They require destructors, therefore they require all the special operations to be either defined or deleted.

Let’s focus on liboceanlight::window, since that’s simpler. Because you’ve declared a destructor, you’ve suppressed the move ops… which is good! Unfortunately, you haven’t also suppressed the copy ops, which is bad. I could easily do:

{
    auto window = liboceanlight::window{width, height};

    auto oops = window;

    // glfwDestroyWindow() called twice with the same handle
}

One option is to simply disable all moving and copying. But that makes classes really annoying to use. For example, a user may want to do complicated window construction in a helper function, and then return the window. If the window is not movable, this becomes difficult, if not impossible.

A better option is to disable copying, but allow moving, and make moving safe. For example:

class window
{
    int width;
    int height;
    std::string window_name = "Oceanlight";
    GLFWwindow* window_pointer = nullptr;

public:
    window(int w, int h);
    ~window();

    // disable copying:
    window(window const&) = delete;
    auto operator=(window const&) -> window& = delete;

    // allow moving (and make it noexcept if you can!)
    window(window&&) noexcept;
    auto operator=(window&&) noexcept -> window&;

    // ... other member functions ...
};

window::window(int w, int h)
    : width{w}
    , height{h}
{
    // ... same as what you already have ...
}

window::~window()
{
    if (window_pointer)                     // <-- note
        glfwDestroyWindow(window_pointer);
}

window::window(window&& other) noexcept
    : width{other.width}
    , height{other.height}
    , window_name{std::move(other.window_name)}
    , window_pointer{other.window_pointer}
{
    other.window_pointer = nullptr; // this is important!
}

auto window::operator=(window&& other) noexcept -> window&
{
    width = other.width;
    height = other.height;
    window_name = std::move(other.window_name);
    window_pointer = other.window_pointer;

    other.window_pointer = nullptr; // this is important!

    return *this;
}

You can do the same thing for the engine class, but obviously it would be much more complicated.

main()

There is a peculiar problem with the way you’ve structured main(). It’s basically this:

int main(int argc, char** argv)
{
    try
    {
        // ...
    }
    catch (const std::exception& e)
    {
        // ...
    }
    catch (...)
    {
        // ...
        throw;
    }

    return EXIT_SUCCESS;
}

So, basically, if a std::exception or any derived type is thrown, it gets caught, an error message gets printed, and then… the program reports that it completed successfully. That’s odd that it reports success even for an error.

If any other type is thrown—which may even include exception types of some other library—the program prints an error message and then… crashes. (Technically, it calls std::terminate() and then (by default) std::abort()… but std::abort() is very bad in C++, so calling it a crash is fair.)

Let me offer an alternative design, using a Lippincott function:

// can be defined elsewhere, away from main(), if you like
auto handle_error()
{
    if (not std::current_exception())
    {
        // This is a logic error; it means handle_error() was called when
        // there was no error. You should report/log this.
        return EXIT_FAILURE;
    }

    try
    {
        throw;
    }
    catch (std::exception const& e)
    {
        std::cerr << "Error: " << e.what() << "\n"; // note: don't use std::endl
    }
    catch (...)
    {
        std::cerr << "Error: unknown error\n";
    }

    return EXIT_FAILURE;
}

auto main(int argc, char* argv[]) -> int
{
    try
    {
        // ...
    }
    catch (...)
    {
        return handle_error();
    }
}

This pulls your error handling away from the happy path, so that main() is now almost entirely functional program code, without error handling noise. (You could even put handle_error() in a completely different file.)

Major engine design weirdness

So, this is, in essence, the core of your program:

static liboceanlight::engine engine;
liboceanlight::window window(args.width, args.height);
engine.init(window);
engine.run(window);

Now, I don’t see the point of making the engine a function static variable. That’s a little weird.

More importantly, though… you initialize the engine (by construction)… then create a window… then… initialize the engine again. Something has gone awry here.

init() functions are a code smell. They require the programmer to do something manually that could and should be done automatically. With your current design, if someone does:

auto engine = liboceanlight::engine{};
auto window = liboceanlight::window{args.width, args.height};
engine.run(window);

… they’ll (probably) see a window pop up, and then get a magnificent crash on the first call to engine::draw_frame().

Now, I think I understand why you did it this way. If I recall, to setup Vulkan, you first need to initialize Vulkan itself, but then you need a window surface before you can start selecting your rendering device. However, this is the kind of low-level stuff you should be papering over, so users don’t need to worry about it.

Indeed, your classes are a spaghetti mess of mixed responsibilities. engine initializes Vulkan… but it also handles creating the window surface… except the window also handles creating the window surface… but the engine creates the swap chain for the window… none of it makes any logical sense.

I would advise stopping, taking a step back, and rethinking everything… and this time, think about interfaces first… not functionality. Think about what your engine’s code should look like, ideally. I think the code I wrote above—the one that initializes the engine, creates the window, then does run()—is what I’d like to write. I mean, even better would be if the engine created the window automatically, but I get that it’s better to have control.

However, if you do want to allow more control by having more steps before the main loop starts, you should use techniques to enforce that that everything that needs to be done gets done, and that it gets done in the correct order. There is no one right way to do this; there are thousands of possible designs you could use, all with pros and cons. Here are just a few techniques:

  1. Use member functions of existing types.

    For example, if the window must be created after the engine, then you could do:

    auto engine = liboceanlight::engine{};
    auto window = engine.create_window(/*...*/);
    

    Now there is no possible way to create a window without creating the engine first. (Well, I mean, it’s still possible. Anything is possible in C++, if you are willing to write pathological code. But you only need to care about Murphy, not Machiavelli.)

  2. Pass required prerequisites as arguments.

    Again, if the window must be created after the engine, you can do:

    auto engine = liboceanlight::engine{};
    auto window = liboceanlight::window{engine, /*...*/};
    

    Again, you can’t create the window without creating the engine first.

  3. Use more types if necessary.

    So, let’s say it’s really necessary to do further initialization after creating the window. Instead of doing what you’ve done, where it is possible to forget to call init() (or call it multiple times), you could use an additional type that makes the secondary initialization impossible to miss:

    auto engine = liboceanlight::engine{};
    auto window = liboceanlight::window{engine, /*...*/};
    auto init = liboceanlight::init{engine, window, /*...*/};
    init.run();
    

    As you can see, now you can’t call run() without first doing the secondary initialization.

The basic point is that you need to carefully think about the structure of your code: what classes you’ll need, which will handle what responsibility, how they’ll interact, and so on. This is the kind of complexity that requires using a whiteboard or UML or something like that to plan to out. Making a game engine is not a simple task.

Handling command line arguments

So, you have a class to parse command line arguments, and you use it basically like this:

auto main(int argc, char* argv[]) -> int
{
    try
    {
        oceanlight::args args;
        args.parse(argc, argv);

        if (args.should_exit())
        {
            return EXIT_SUCCESS;
        }

        // ...
    }
    catch (...)
    {
        return handle_error();
    }
}

Now, I wonder if it makes sense for the arguments object to be the thing to decide whether or not the whole program should exit. To me, it would make more sense for that to be decided in main().

Also, you’ve duplicated your error handling. If you give a bad argument, you get an error message… then another error message. There is no need for the try-catch stuff in the parse function. Indeed, this is another case of mixed responsibilities. The parse() function should have one job: parsing the command line arguments. Not deciding what to do with them.

I would also recommend considering getting your program configuration from more than just command line arguments. For example, maybe you would first load arguments from a standard package defaults file… then from a user configuration file… and then from the command line… and whatever is defined at each step overrides any previous setting.

So would have a class for your program arguments, and you would default construct it to get basic program defaults. Then you would pass it to

// In the "args" unit:

class program_arguments_error : public std::runtime_error { /* ... */ };

struct program_arguments
{
    int width  = 640;
    int height = 480;
    // Note: Don't write `int width, height;`. Put each variable on its own line.
};

auto parse_command_line(int argc, char* argv[]) -> program_arguments;

auto write_help_message(std::ostream&) -> void;
auto write_version_message(std::ostream&) -> void;

// In the main unit:

auto load_configuration(int argc, char* argv[])
{
    auto args = program_arguments{};

    args = read_config_file("/usr/share/my-game/default-config", args);
    args = read_config_file("${HOME}/.local/share/my-game/config", args);
    args = parse_command_line(argc, argv, args);

    return args;
}

auto handle_error()
{
    // ...

    try
    {
        throw;
    }
    catch (program_arguments_error const& e)
    {
        std::cerr << "Invalid program arguments: " << e.what() << "\n";
        write_help_message(std::cerr);
    }
    // ...

    return EXIT_FAILURE;
}

auto main(int argc, char* argv[]) -> int
{
    try
    {
        auto const args = load_configuration(argc, argv);

        if (args.help_requested)
        {
            write_help_message(std::cout);
            return EXIT_SUCCESS;
        }
        if (args.version_requested)
        {
            write_version_message(std::cout);
            return EXIT_SUCCESS;
        }

        // ...
    }
    catch (...)
    {
        return handle_error();
    }
}

You really need to use RAII more intelligently

I know I said I wouldn’t dig into lol_engine.cc, but there is one thing I really wanted to point out, and the problem is easiest to illustrate in the engine.

So let’s look at main() again, where the main game code is:

static liboceanlight::engine engine;
liboceanlight::window window(args.width, args.height);
engine.init(window);
engine.run(window);

Okay, so, what happens if the window construction fails? The window constructor might throw a std::runtime_error if glfwCreateWindow() fails. Then what?

Well, you’ll immediately exit the try block in main, and—eventually (because, again, why is the engine a static?)—the destructor for engine gets called.

And what does engine’s destructor do? Well, for starters, it does this:

~engine()
{
    vkDestroySemaphore(logical_device,
                       image_available_semaphore,
                       nullptr);

    vkDestroySemaphore(logical_device,
                       rendering_finished_semaphore,
                       nullptr);
                       
    vkDestroyFence(logical_device, in_flight_fence, nullptr);
    vkDestroyCommandPool(logical_device, command_pool, nullptr);

    // ...

Now, riddle me this: Given that logical_device and image_available_semaphore are both initialized to nullptr… what happens when you call vkDestroySemaphore() with all nullptrs?

Welp, according to the specification, calling it with a null semaphore is okay… but:

device must be a valid VkDevice handle

Oops.

So, the very first line of your destructor triggers undefined behaviour. And, spoiler alert, so does pretty much every other Vulkan call.

Now, part of the problem here is that you’re using an init() function, which an anti-pattern, rather than doing the initialization in the constructor, as you should be doing. When initialization fails in the constructor, the destructor is not called. This is why.

But that’s only part of the problem, because even if you moved all of the stuff from init() into a constructor, you still have issues.

Why? Well, let’s look at init():

void liboceanlight::engine::init(liboceanlight::window& window)
{
    vulkan_instance = create_vulkan_instance();
    window_surface = window.create_window_surface(vulkan_instance);
    physical_device = pick_physical_device();
    queue_family_indices = find_queue_families();

    // ...

Now, let’s pretend that this is actually the constructor for engine. So, if the create_window_surface() call fails, and throws, physical_device will still be nullptr… but no big deal, because the destructor won’t be called, so all those destroy functions won’t be called with a null device. This is good!

However…

The vulkan_instance was successfully initialized. It needs to be destroyed. But the destructor is not going to be called. This is bad.

Also, the engine constructor actually also initializes GLFW. That, too, needs to be cleaned up.

What you need is, theoretically, something like this:

engine::engine()
{
    if (auto rv = glfwInit(); !rv)
        throw std::runtime_error("Failed to initialize glfw");

    try
    {
        vulkan_instance = create_vulkan_instance();

        try
        {
            window_surface = window.create_window_surface(vulkan_instance);

            try
            {
                physical_device = pick_physical_device();
                queue_family_indices = find_queue_families();

                if (!device_is_suitable(queue_family_indices,
                                        physical_device,
                                        window_surface,
                                        device_extensions))
                {
                    throw std::runtime_error("Device lacks required queue family.");
                }

                logical_device = create_logical_device(physical_device,
                                                       queue_family_indices,
                                                       device_extensions);

                try
                {
                    // ... and so on...
                }
                catch (...)
                {
                    vkDestroyDevice(logical_device, nullptr);
                    throw;
                }
            }
            catch (...)
            {
                vkDestroySurfaceKHR(vulkan_instance, window_surface, nullptr);
                throw;
            }
        }
        catch (...)
        {
            vkDestroyInstance(vulkan_instance, nullptr);
            throw;
        }
    }
    catch (...)
    {
        glfwTerminate();
        throw;
    }
}

But that’s just ghastly. Please, for the love of all holy, don’t ever write code like that.

One way to fix the above code would be to make RAII types for everything. So, instead of raw Vulkan types:

class engine
{
    VkInstance vulkan_instance {nullptr};
    VkDevice logical_device {nullptr};
    VkPhysicalDevice physical_device {nullptr};
    VkQueue graphics_queue {nullptr};
    VkQueue present_queue {nullptr};
    VkSurfaceKHR window_surface {nullptr};

    // ... etc. ...

You would use your own “smart” types:

class engine
{
    glfw::instance_t glfw_instance;
    vulkan::instance_t vulkan_instance;
    vulkan::device_t logical_device;
    vulkan::physical_device_t physical_device;
    vulkan::surface_t window_surface;

    // ... etc. ...

Each of these “smart” types would default initialize in the “null” state, which can be harmlessly destructed, but then you can assign something to them that will be automatically destroyed in their destructor. (Note also that you’d need a “smart” handle for GLFW.) You could also make them smart enough to be moved correctly. If you do this, then engine won’t even need a destructor, and you won’t need to manually delete the copy ops, or implement the move ops. Everything will “just work”.

Except… no.

Because this pattern is usually ideal… except in the case where each of those things needs to be constructed in a specific order. In fact, I’ve consciously left a bug in place above. If the objects are destroyed in reverse order, then the window surface will be destroyed before the logical device. (Will that be a problem? ¯_ (ツ)_/¯ Check the spec, if you care. The point I’m trying to make is that this pattern is brittle, and potentially buggy.)

There are a number of ways you could still use this pattern, and fix/avoid the problems. But I have a better suggestion.

The library fundamentals v3 TS has a type called std::experimental::scope_fail. As the “TS” and “experimental” imply, it’s not yet standard C++ even as of C++23… though it still might make C++26. Even if not, there are tons of implementations out there; I’ve made one myself.

With it, your constructor would become:

engine::engine()
{
    if (auto rv = glfwInit(); !rv)
        throw std::runtime_error("Failed to initialize glfw");
    auto const glfw_exit = std::experimental::scope_fail([] { glfwTerminate(); });

    vulkan_instance = create_vulkan_instance();
    auto const vk_inst_exit = std::experimental::scope_fail([&vulkan_instance] { vkDestroyInstance(vulkan_instance, nullptr); });

    window_surface = window.create_window_surface(vulkan_instance);
    auto const surf_exit = std::experimental::scope_fail([&] { vkDestroySurfaceKHR(vulkan_instance, window_surface, nullptr); });

    physical_device = pick_physical_device();
    queue_family_indices = find_queue_families();

    if (!device_is_suitable(queue_family_indices,
                            physical_device,
                            window_surface,
                            device_extensions))
    {
        throw std::runtime_error("Device lacks required queue family.");
    }

    logical_device = create_logical_device(physical_device,
                                           queue_family_indices,
                                           device_extensions);
    auto vk_dev_exit = std::experimental::scope_fail([&] { vkDestroyDevice(logical_device, nullptr); });

    // ... and so on...
}

Basically, every time you init something that needs cleaning up, immediately follow it with a scope_fail that does the cleanup. If nothing fails, then the constructor as a whole succeeds, and all is good; after that, the destructor will handle cleanup properly, no worries. But if anything fails, then anything that preceded the failure will be cleaned up.

This is still ugly, and lots of manual work and boilerplate, but… you’re using a C library. This is how it goes.

Utility issues

You have two utility functions (I mean, let’s ignore test_func()). The first stringizes a VkQueueFlags.

Now, I’m not sure it’s necessary to pass a flags type by const&… it’s a flags type, after all; it’s just an int. (Technically, a std::uint32_t.) But the real problem here is the use of std::map.

std::map is a beast; expensive and heavyweight. If you need it, you need it. But… you don’t need it. (Even if you did, you’d probably want to use a std::unordered_map most of the time.)

All you want to do is make a map of some compile-time constants. (You don’t even really need to do that; you could simply write a bunch of ifs, but I agree that’s not elegant.) You could just do:

namespace liboceanlight {

namespace {

constexpr auto flag_bits_to_string = std::to_array<std::tuple<VkQueueFlagBits, std::string_view>>({
    {VK_QUEUE_GRAPHICS_BIT, "Graphics"},
    {VK_QUEUE_COMPUTE_BIT, "Compute"},
    {VK_QUEUE_TRANSFER_BIT, "Transfer"},
    {VK_QUEUE_SPARSE_BINDING_BIT, "Sparsebinding"},
    {VK_QUEUE_PROTECTED_BIT, "Protected"},
    {VK_QUEUE_VIDEO_DECODE_BIT_KHR, "Video Decode"},
#ifdef VK_ENABLE_BETA_EXTENSIONS
    {VK_QUEUE_VIDEO_ENCODE , "Video Encode"},
#endif
    {VK_QUEUE_OPTICAL_FLOW_BIT_NV, "Optical Flow"}
});

} // anonymous namespace

auto queue_flags_to_string(VkQueueFlags flags) -> std::string
{
    return std::accumulate(
        std::ranges::begin(flag_bits_to_string),
        std::ranges::end(flag_bits_to_string),
        std::string{},
        [flags] (auto formatted, auto&& flag_info)
        {
            if (flags & std::get<VkQueueFlagBits>(flag_info))
            {
                formatted += "|";
                formatted += std::get<std::string_view>(flag_info);
            }

            return formatted;
        })
        + "|";
}

} // namespace liboceanlight

Already this will probably be an order of magnitude faster than what you’ve got.

But you could do even better if, instead of constructing a string and returning it, you used a manipulator. See, for example std::put_time(). You could write a put_queue_flags() function, and use the same way as your current function:

std::cout << "Queue Count: " << queue_family.queueCount << "\n"
          << "Queue Type: "
          << put_queue_flags(queue_family.queueFlags) << "\n";

… except it would construct the result directly in the output stream’s buffer. No need for allocating a temporary string.

All told, that should make queue_flags_to_string() at least a hundred times faster and, more importantly, much, much less likely to fail.

(Incidentally, std::set is also super heavyweight, which makes check_device_extension_support() quite expensive. This is not as big a deal, because it should only be called once in the whole game (unlike queue_flags_to_string(), which may be called multiple times in debug output), but still, you might consider ways to avoid needing a set. One option would be to simply sort the extensions vectors, and then use std::ranges::includes().)

Now, as for read_file(), I’m afraid you have a bug there. Unfortunately, the bug is very, very complex and subtle; years ago I wrote a blog post about it that was one of my most popular, but unfortunately that blog is long offline. I’ll have to rewrite it someday; a lot of stuff has changed, but some key problems remain.

I won’t rewrite the blog post here, but suffice it to say that this:

auto file = std::ifstream{filename, std::ios::ate | std::ios::binary};
auto filesize = file.tellg();

… does not work. No, not even if you use binary mode. It will probably work on most systems… but not all.

There is simply no safe and simple way to read a file into a string (or vector<char>) using only standard C++. The best you can do is use a loop, and read it chunk-by-chunk. You could simply expand and contract the vector as needed:

auto read_file(std::string const& filename) -> std::vector<char>
{
    auto file = std::ifstream{filename, std::ios::ate | std::ios::binary};
    file.exceptions(std::ios_base::failbit | std::ios_base::badbit);

    auto buffer = std::vector<char>{};

    while (not file.eof())
    {
        auto n = buffer.size();
        buffer.resize(n + 1024);

        file.read(buffer.data() + n, 1024);
        buffer.resize(n + file.gcount());
    }

    return buffer;
}

… but for a huge file, that could mean a lot of reallocating and shuffling bytes around.

If you’re expecting huge files, then you might want to do something like make a vector of vectors, and read each chunk into a vector, and only after you’ve reached the end, combine the vectors. (And as an optimization, if you have only a single vector of data, return that directory.)

Inconvenient, yes, I know. But unless you don’t care that what you have now is not portably safe (as in, it works on all the platforms you care about), then, this is the way.

(If you’re willing to abandon standard C++, then there are a ton of safe ways to do it simply and cleanly. But of course, that creates portability problems.)

Miscellaneous stuff

  • You declare version_string() in lol_version.hpp, but also in lol_debug_messenger.hpp.

  • Is it really necessary to return a string object for the version string? One that has to be constructed, complete with memory allocations? Because this is basically a compile-time constant. You could just return a char const* or a std::string_view or (the best option) a type that represents a NUL-terminated static string. Do you even need a function at all? You could just use an inline constant.

  • Speaking of lol_debug_messenger.hpp, half the functions declared there don’t even exist. Others, like debug_utils_messenger_callback(), do exist, but don’t need to be visible outside of lol_debug_messenger.cc.

  • As @Lozminda mentioned, indenting on namespaces is a little silly. It gets even sillier when you use more and better namespaces. I tend to use a main indi namespace for all my stuff, then an inline versioning namespace, and then a more precise namespace, like algorithm, and then sometimes even more levels of namespace for complex stuff. So namespace indi { inline namespace v1 { namespace whatever { and then maybe more, but with even just that, I’d waste 12 characters of indent for just nothing. (Okay, in C++20 you can now write namespace indi::inline v1::whatever {, but still.)

  • Following from the above, prefixing the namespace to every single function is also a little silly. It makes everything harder to read. If you don’t indent your namespaces, then there is no benefit to not simply using a namespace block.

  • Headers are on the way out, but you’re probably stuck using them regardless, because Vulkan is a C library. In any case, as a general rule, you should always organize your headers the same way. For example, most files include <string> before <vulkan/vulkan.h>… but not lol_debug_messenger.hpp. Mixing the order of headers can have bizarre effects. (As can not always including the same headers, but, hey, headers suck, so, what can you do?)

  • Never do this: int width, height; (That’s from window, but it also happens elsewhere, like in choose_swap_extent().) One variable declaration per line, please. This is even one of the core guidelines

  • This is unwise:

    VkResult rv;
    
    // ...
    
    // ... bunch of code ...
    
    // ...
    
    rv = vkGetPhysicalDeviceSurfaceSupportKHR(physical_device,
                                              i,
                                              window_surface,
                                              &present_support);
    
    if (rv != VK_SUCCESS)
    {
        throw std::runtime_error("Could not determine surface support");
    }
    

    Unless you can’t possibly avoid it (which is often the case with C APIs like Vulkan, though not here), don’t declare variables unless you’re actually using them. This why I and other C++ trainers teach the “Always auto (AA)” style (formerly “Almost Always auto (AAA)”); it forces better practices like declaring a variable right where it’s needed. If you did auto rv, you couldn’t declare it 10 or so lines before it’s needed.

    Also, there is a new form of if that includes an initializer, just for this kind of thing:

    if (auto rv = vkGetPhysicalDeviceSurfaceSupportKHR(physical_device, i,window_surface, &present_support); rv != VK_SUCCESS)
        throw std::runtime_error{"Could not determine surface support"};
    

    rv is scoped to the if, which is safer, and easier (because now you can reuse the name).

  • Also on the topic of modern C++, in a couple places, you pass around things like const std::vector<const char*>& (like in check_device_extension_support()). A better way these days would be to use std::span<char const*>.

  • Also also on the topic of modern C++, you use a lot of raw for loops, which is a code smell. Granted, almost all of them are modern range-for loops… but that’s still bad. At least some of them appear to be more complicated algorithms. The one in choose_swap_surface_format(), for instance, appears to actually be a find operation. Using proper, named algorithms not only makes your code safer, it makes it clearer.

  • Generally, I would suggest keeping the fact that the engine is using Vulkan as an implementation detail. When I look at oceanlight.cc, I see no sign of Vulkan… just liboceanlight, which is great!

    However, everywhere else, the fact that liboceanlight::engine is using Vulkan is leaked all over the place. The header lol_engine.hpp includes <vulkan/vulkan.h>, so anything that includes lol_engine.hpp gets all the Vulkan crap, too.

    What you should do is use something like the pImpl idiom to hide all the details of the engine. There should be basically no sign in the engine class interface that it’s using Vulkan under the hood. When and if you do need to expose something, disguise it with a liboceanlight-specific typedef.

    For example:

    // lol_engine.hpp ----------------------------------------------
    
    #ifndef LIBOCEANLIGHT_ENGINE_HPP_INCLUDED
    #define LIBOCEANLIGHT_ENGINE_HPP_INCLUDED
    
    #include <memory>   // for unique_ptr
    
    #include <liboceanlight/lol_window.hpp>
    
    namespace liboceanlight {
    
    class engine
    {
        class impl;
        std::unique_ptr<impl> _impl;
    
    public:
        engine();
    
        ~engine();
    
        // movable
        engine(engine&&) noexcept;
        auto operator=(engine&&) noexcept -> engine&;
    
        // non-copyable
        engine(engine const&) = delete;
        auto operator=(engine const&) -> engine& = delete;
    
        auto run(window&) -> void;
    };
    
    } // namespace liboceanlight
    
    #endif // include guard
    
    // NOTE: This header file is *extremely* light. It only includes <memory>
    // and the window header. You may need a few other headers in some cases,
    // if your class interface needs strings or paths or iostreams or whatever,
    // but still... a lightweight header makes for much faster compile times,
    // and the more stuff you can put in the implementation file and *NOT* the
    // header, the less often you will have to recompile everything when you make
    // a change.
    
    // lol_engine.cc -----------------------------------------------
    
    #include <liboceanlight/lol_engine.hpp>
    
    namespace liboceanlight {
    
    // You can put all your utility functions here, either in an anonymous
    // namespace, or as member functions of impl.
    
    class engine::impl
    {
        VkInstance vulkan_instance = nullptr;
        VkDevice logical_device = nullptr;
        VkPhysicalDevice physical_device = nullptr;
        // etc. ...
    
    public:
        impl()
        {
            // basically your engine constructor and init goes here
        }
    
        ~impl()
        {
            // basically your engine destructor goes here
        }
    
        auto run(window& w) -> void
        {
            // basically run() goes here
        }
    };
    
    engine::engine()
        : _impl{std::make_unique<impl>()}
    {
    }
    
    engine::~engine() = default;
    
    // defaulted move ops
    engine::engine(engine&&) noexcept = default;
    auto engine::operator=(engine&&) noexcept -> engine& = default;
    
    auto engine::run(window& w) -> void
    {
        impl->run(w);
    }
    
    } // namespace liboceanlight
    

    This kind of design isolates components, making for fewer clashes, and faster compiles. There should be no practical efficiency cost either, because the engine is a very high-level object. You’re basically paying the cost of two or three extra pointer indirections for the run of the entire game start to finish. I think you can afford that. (But don’t use the pImpl idiom for low-level, high-performance stuff, like vertex classes and such.)

That’s about all that comes to mind. I hope some of it helps!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.