Program Listing for File VulkanDevice.cpp

Program Listing for File VulkanDevice.cpp#

Return to documentation for file (Src/GraphicsEngineVulkan/vulkan_base/VulkanDevice.cpp)

module;

#include "renderer/SwapChainDetails.hpp"
#include <cstdint>
#include <cstring>

#include "common/Utilities.hpp"
#include "spdlog/spdlog.h"
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <limits>
#include <set>
#include <string>
#include <vulkan/vulkan.hpp>

module kataglyphis.vulkan.device;

namespace {
constexpr int DEVICE_TYPE_SCORE_DISCRETE = 10000;
constexpr int DEVICE_TYPE_SCORE_INTEGRATED = 1000;
constexpr int DEVICE_TYPE_SCORE_VIRTUAL = 100;
constexpr int DEVICE_TYPE_SCORE_CPU = 10;

enum class GpuSelectionMode : std::uint8_t { Auto, Dedicated, Integrated };

auto readGpuSelectionFromEnvironment() -> std::string
{
#if defined(_WIN32)
    std::size_t required_size = 0;
    if (getenv_s(&required_size, nullptr, 0, "KATAGLYPHIS_VK_GPU") != 0 || required_size == 0) { return ""; }

    std::string value(required_size, '\0');
    if (getenv_s(&required_size, value.data(), value.size(), "KATAGLYPHIS_VK_GPU") != 0 || required_size == 0) {
        return "";
    }

    value.resize(required_size - 1);
    return value;
#else
    const char *value = std::getenv("KATAGLYPHIS_VK_GPU");
    if (value == nullptr) { return ""; }
    return std::string(value);
#endif
}

auto parseGpuSelectionMode() -> GpuSelectionMode
{
    std::string mode = readGpuSelectionFromEnvironment();
    if (mode.empty()) { return GpuSelectionMode::Auto; }

    std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char character) {
        return static_cast<char>(std::tolower(character));
    });

    if (mode == "dedicated") { return GpuSelectionMode::Dedicated; }
    if (mode == "integrated") { return GpuSelectionMode::Integrated; }

    return GpuSelectionMode::Auto;
}

auto gpuSelectionModeToString(GpuSelectionMode mode) -> const char *
{
    switch (mode) {
    case GpuSelectionMode::Dedicated:
        return "dedicated";
    case GpuSelectionMode::Integrated:
        return "integrated";
    case GpuSelectionMode::Auto:
    default:
        return "auto";
    }
}

auto matchesSelectionMode(const vk::PhysicalDeviceProperties &properties, GpuSelectionMode mode) -> bool
{
    switch (mode) {
    case GpuSelectionMode::Dedicated:
        return properties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu;
    case GpuSelectionMode::Integrated:
        return properties.deviceType == vk::PhysicalDeviceType::eIntegratedGpu;
    case GpuSelectionMode::Auto:
    default:
        return true;
    }
}

auto scorePhysicalDevice(const vk::PhysicalDeviceProperties &properties) -> int
{
    int score = 0;

    switch (properties.deviceType) {
    case vk::PhysicalDeviceType::eDiscreteGpu:
        score += DEVICE_TYPE_SCORE_DISCRETE;
        break;
    case vk::PhysicalDeviceType::eIntegratedGpu:
        score += DEVICE_TYPE_SCORE_INTEGRATED;
        break;
    case vk::PhysicalDeviceType::eVirtualGpu:
        score += DEVICE_TYPE_SCORE_VIRTUAL;
        break;
    case vk::PhysicalDeviceType::eCpu:
        score += DEVICE_TYPE_SCORE_CPU;
        break;
    default:
        break;
    }

    score += static_cast<int>(properties.limits.maxImageDimension2D);
    return score;
}

auto deviceTypeToString(vk::PhysicalDeviceType type) -> const char *
{
    switch (type) {
    case vk::PhysicalDeviceType::eDiscreteGpu:
        return "Discrete GPU";
    case vk::PhysicalDeviceType::eIntegratedGpu:
        return "Integrated GPU";
    case vk::PhysicalDeviceType::eVirtualGpu:
        return "Virtual GPU";
    case vk::PhysicalDeviceType::eCpu:
        return "CPU";
    default:
        return "Other";
    }
}
}// namespace

Kataglyphis::VulkanDevice::VulkanDevice(VulkanInstance *instance, vk::SurfaceKHR *surface)
  : instance(instance), surface(surface)
{


    get_physical_device();
    create_logical_device();
}

auto Kataglyphis::VulkanDevice::getSwapchainDetails() -> Kataglyphis::VulkanRendererInternals::SwapChainDetails
{
    return getSwapchainDetails(physical_device);
}

void Kataglyphis::VulkanDevice::cleanUp() { logical_device.destroy(); }

Kataglyphis::VulkanDevice::~VulkanDevice() = default;

auto Kataglyphis::VulkanDevice::getQueueFamilies() -> Kataglyphis::VulkanRendererInternals::QueueFamilyIndices
{
    Kataglyphis::VulkanRendererInternals::QueueFamilyIndices indices{};

    std::vector<vk::QueueFamilyProperties> queue_family_list = physical_device.getQueueFamilyProperties();

    // Go through each queue family and check if it has at least 1 of required
    // types we need to keep track th eindex by our own
    uint32_t index = 0;
    for (const auto &queue_family : queue_family_list) {
        // first check if queue family has at least 1 queue in that family
        // Queue can be multiple types defined through bitfield. Need to bitwise AND
        // with vk::QueueFlagBits to check if has required  type
        if (queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) {
            indices.graphics_family = static_cast<int>(index);// if queue family valid, than get index
        }

        if (queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) {
            indices.compute_family = static_cast<int>(index);
        }

        // check if queue family suppports presentation
        vk::Bool32 presentation_support = physical_device.getSurfaceSupportKHR(index, *surface).value;
        // check if queue is presentation type (can be both graphics and
        // presentation)
        if (queue_family.queueCount > 0 && presentation_support) {
            indices.presentation_family = static_cast<int>(index);
        }

        // check if queue family indices are in a valid state
        if (indices.is_valid()) { break; }

        index++;
    }

    return indices;
}

void Kataglyphis::VulkanDevice::get_physical_device()
{
    const GpuSelectionMode selection_mode = parseGpuSelectionMode();

    // Enumerate physical devices the vkInstance can access
    std::vector<vk::PhysicalDevice> device_list = instance->getVulkanInstance().enumeratePhysicalDevices().value;

    // if no devices available, then none support of Vulkan
    if (device_list.empty()) { spdlog::error("Can not find GPU's that support Vulkan Instance!"); }

    int best_device_score = std::numeric_limits<int>::min();
    int best_device_score_fallback = std::numeric_limits<int>::min();
    vk::PhysicalDevice fallback_device{};
    vk::PhysicalDeviceProperties fallback_properties{};

    for (const auto &device : device_list) {
        if (!check_device_suitable(device)) { continue; }

        vk::PhysicalDeviceProperties candidate_properties = device.getProperties();

        const int candidate_score = scorePhysicalDevice(candidate_properties);
        if (candidate_score > best_device_score_fallback) {
            best_device_score_fallback = candidate_score;
            fallback_device = device;
            fallback_properties = candidate_properties;
        }

        if (!matchesSelectionMode(candidate_properties, selection_mode)) { continue; }

        if (candidate_score > best_device_score) {
            best_device_score = candidate_score;
            physical_device = device;
            device_properties = candidate_properties;
        }
    }

    if (!physical_device && fallback_device) {
        physical_device = fallback_device;
        device_properties = fallback_properties;
        spdlog::default_logger_raw()->log(spdlog::level::warn,
          std::string("No suitable Vulkan GPU matching selection mode '") + gpuSelectionModeToString(selection_mode)
            + "' found. Falling back to auto device selection.");
    }

    if (!physical_device) {
        spdlog::critical("Failed to find a suitable Vulkan physical device.");
        std::abort();
    }

    // get properties of our new device
    device_properties = physical_device.getProperties();
    spdlog::default_logger_raw()->log(spdlog::level::info,
      std::string("Selected Vulkan physical device: ") + device_properties.deviceName.data() + " ("
        + deviceTypeToString(device_properties.deviceType) + ")");
    spdlog::default_logger_raw()->log(
      spdlog::level::info, std::string("Vulkan GPU selection mode: ") + gpuSelectionModeToString(selection_mode));
}

void Kataglyphis::VulkanDevice::create_logical_device()
{
    // get the queue family indices for the chosen physical device
    Kataglyphis::VulkanRendererInternals::QueueFamilyIndices const indices = getQueueFamilies();

    // vector for queue creation information and set for family indices
    std::vector<vk::DeviceQueueCreateInfo> queue_create_infos;
    std::set<int> const queue_family_indices = {
        indices.graphics_family, indices.presentation_family, indices.compute_family
    };
    std::vector<float> queue_priorities(queue_family_indices.size(), 1.0F);
    queue_create_infos.reserve(queue_family_indices.size());

    // Queue the logical device needs to create and info to do so (only 1 for now,
    // will add more later!)
    std::size_t priority_index = 0;
    for (int const queue_family_index : queue_family_indices) {
        vk::DeviceQueueCreateInfo queue_create_info{};
        queue_create_info.queueFamilyIndex =
          static_cast<uint32_t>(queue_family_index);// the index of the family to create a queue from
        queue_create_info.queueCount = 1;// number of queues to create
        queue_create_info.pQueuePriorities =
          &queue_priorities[priority_index];// Vulkan needs to know how to handle multiple queues, so
                                            // decide priority (1 = highest)

        queue_create_infos.push_back(queue_create_info);
        ++priority_index;
    }

    vk::PhysicalDeviceVulkan13Features available_features13{};
    available_features13.pNext = nullptr;

    vk::PhysicalDeviceVulkan12Features available_features12{};
    available_features12.pNext = &available_features13;

    vk::PhysicalDeviceFeatures2 available_features2{};
    available_features2.pNext = &available_features12;
    physical_device.getFeatures2(&available_features2);

    // --ENABLE RAY TRACING PIPELINE
    vk::PhysicalDeviceRayTracingPipelineFeaturesKHR ray_tracing_pipeline_features{};
    ray_tracing_pipeline_features.pNext = nullptr;
    ray_tracing_pipeline_features.rayTracingPipeline = false;

    // -- ENABLE ACCELERATION STRUCTURES
    vk::PhysicalDeviceAccelerationStructureFeaturesKHR acceleration_structure_features{};
    acceleration_structure_features.pNext = &ray_tracing_pipeline_features;
    acceleration_structure_features.accelerationStructure = false;
    acceleration_structure_features.accelerationStructureCaptureReplay = false;
    acceleration_structure_features.accelerationStructureIndirectBuild = false;
    acceleration_structure_features.accelerationStructureHostCommands = false;
    acceleration_structure_features.descriptorBindingAccelerationStructureUpdateAfterBind = false;

    vk::PhysicalDeviceVulkan13Features features13{};
    features13.maintenance4 = false;
    features13.robustImageAccess = false;
    features13.inlineUniformBlock = false;
    features13.descriptorBindingInlineUniformBlockUpdateAfterBind = false;
    features13.pipelineCreationCacheControl = false;
    features13.privateData = false;
    features13.shaderDemoteToHelperInvocation = false;
    features13.shaderTerminateInvocation = false;
    features13.subgroupSizeControl = false;
    features13.computeFullSubgroups = false;
    features13.synchronization2 = false;
    features13.textureCompressionASTC_HDR = false;
    features13.shaderZeroInitializeWorkgroupMemory = false;
    features13.dynamicRendering = false;
    features13.shaderIntegerDotProduct = false;
    features13.pNext = &acceleration_structure_features;

    vk::PhysicalDeviceRayQueryFeaturesKHR rayQueryFeature{};
    rayQueryFeature.pNext = &features13;
    rayQueryFeature.rayQuery = false;

    vk::PhysicalDeviceRayQueryFeaturesKHR availableRayQueryFeature{};

    vk::PhysicalDeviceRayTracingPipelineFeaturesKHR availableRayTracingPipelineFeatures{};
    availableRayTracingPipelineFeatures.pNext = &availableRayQueryFeature;

    vk::PhysicalDeviceAccelerationStructureFeaturesKHR availableAccelerationStructureFeatures{};
    availableAccelerationStructureFeatures.pNext = &availableRayTracingPipelineFeatures;

    vk::PhysicalDeviceFeatures2 availableRayTracingFeatures2{};
    availableRayTracingFeatures2.pNext = &availableAccelerationStructureFeatures;
    physical_device.getFeatures2(&availableRayTracingFeatures2);

    vk::PhysicalDeviceVulkan12Features features12{};
    features12.pNext = nullptr;
    features12.bufferDeviceAddress = available_features12.bufferDeviceAddress;
    features12.scalarBlockLayout = available_features12.scalarBlockLayout;
    features12.descriptorIndexing = available_features12.descriptorIndexing;
    features12.runtimeDescriptorArray = available_features12.runtimeDescriptorArray;
    features12.shaderSampledImageArrayNonUniformIndexing =
      available_features12.shaderSampledImageArrayNonUniformIndexing;

    vk::PhysicalDeviceFeatures2 features2{};
    features2.pNext = nullptr;
    features2.features.samplerAnisotropy = true;
    features2.features.shaderInt64 = true;
    features2.features.geometryShader = true;
    features2.features.fragmentStoresAndAtomics = true;
    features2.features.logicOp = true;
    features2.features.robustBufferAccess = available_features2.features.robustBufferAccess;

    // -- PREPARE FOR HAVING MORE EXTENSION BECAUSE WE NEED RAYTRACING
    // CAPABILITIES
    std::vector<const char *> extensions(device_extensions);

    // Query available extensions for the physical device
    std::vector<vk::ExtensionProperties> availableExtensions =
      physical_device.enumerateDeviceExtensionProperties().value;

    // Helper function to check if an extension is supported
    auto isExtensionSupported = [&availableExtensions](const char *extensionName) -> bool {
        for (const auto &ext : availableExtensions) {
            if (strcmp(ext.extensionName, extensionName) == 0) { return true; }
        }
        return false;
    };

    const bool hasBufferDeviceAddressFeature = available_features12.bufferDeviceAddress == VK_TRUE;
    deviceSupportsBufferDeviceAddress = hasBufferDeviceAddressFeature;
    const bool hasRequiredDescriptorIndexingFeatures =
      available_features12.descriptorIndexing == VK_TRUE && available_features12.runtimeDescriptorArray == VK_TRUE
      && available_features12.shaderSampledImageArrayNonUniformIndexing == VK_TRUE;

    spdlog::default_logger_raw()->log(spdlog::level::info,
      std::string("Feature support: bufferDeviceAddress=") + (hasBufferDeviceAddressFeature ? "true" : "false")
        + ", descriptorIndexing=" + (available_features12.descriptorIndexing == VK_TRUE ? "true" : "false")
        + ", runtimeDescriptorArray=" + (available_features12.runtimeDescriptorArray == VK_TRUE ? "true" : "false")
        + ", sampledImageArrayNonUniformIndexing="
        + (available_features12.shaderSampledImageArrayNonUniformIndexing == VK_TRUE ? "true" : "false")
        + ", robustBufferAccess=" + (available_features2.features.robustBufferAccess == VK_TRUE ? "true" : "false"));

    for (const char *extensionName : device_extensions_for_raytracing) {
        if (!isExtensionSupported(extensionName)) {
            deviceSupportsHardwareAcceleratedRRT = false;
            spdlog::default_logger_raw()->log(
              spdlog::level::info, std::string("Required extension not supported: ") + extensionName);
        }
    }

    if (deviceSupportsHardwareAcceleratedRRT && !hasRequiredDescriptorIndexingFeatures) {
        deviceSupportsHardwareAcceleratedRRT = false;
        spdlog::info(
          "Required Vulkan 1.2 descriptor indexing features are not supported; disabling hardware ray tracing path.");
    }

    if (deviceSupportsHardwareAcceleratedRRT && !hasBufferDeviceAddressFeature) {
        deviceSupportsHardwareAcceleratedRRT = false;
        spdlog::info("bufferDeviceAddress feature is not supported; disabling hardware ray tracing path.");
    }

    const bool hasMaintenance4Feature = available_features13.maintenance4 == VK_TRUE;

    const bool hasRequiredRayTracingFeatures = availableAccelerationStructureFeatures.accelerationStructure == VK_TRUE
                                               && availableRayTracingPipelineFeatures.rayTracingPipeline == VK_TRUE
                                               && availableRayQueryFeature.rayQuery == VK_TRUE;

    if (deviceSupportsHardwareAcceleratedRRT && !hasRequiredRayTracingFeatures) {
        deviceSupportsHardwareAcceleratedRRT = false;
        spdlog::default_logger_raw()->log(spdlog::level::info,
          std::string("Required ray tracing features are not fully supported (accelerationStructure=")
            + (availableAccelerationStructureFeatures.accelerationStructure == VK_TRUE ? "true" : "false")
            + ", rayTracingPipeline="
            + (availableRayTracingPipelineFeatures.rayTracingPipeline == VK_TRUE ? "true" : "false")
            + ", rayQuery=" + (availableRayQueryFeature.rayQuery == VK_TRUE ? "true" : "false")
            + "); disabling hardware ray tracing path.");
    }

    if (deviceSupportsHardwareAcceleratedRRT && !hasMaintenance4Feature) {
        deviceSupportsHardwareAcceleratedRRT = false;
        spdlog::default_logger_raw()->log(spdlog::level::info,
          "Vulkan 1.3 maintenance4 feature is not supported; disabling hardware ray tracing/path tracing path.");
    }

    if (deviceSupportsHardwareAcceleratedRRT) {
        // COPY ALL NECESSARY EXTENSIONS FOR RAYTRACING TO THE EXTENSION
        extensions.insert(
          extensions.begin(), device_extensions_for_raytracing.begin(), device_extensions_for_raytracing.end());

        features12.bufferDeviceAddress = true;
        features12.descriptorIndexing = true;
        features12.runtimeDescriptorArray = true;
        features12.shaderSampledImageArrayNonUniformIndexing = true;
        features13.maintenance4 = true;

        acceleration_structure_features.accelerationStructure = true;
        acceleration_structure_features.accelerationStructureCaptureReplay = false;
        ray_tracing_pipeline_features.rayTracingPipeline = true;
        rayQueryFeature.rayQuery = true;
        features12.pNext = &rayQueryFeature;
    }

    if (!deviceSupportsHardwareAcceleratedRRT && !hasBufferDeviceAddressFeature) {
        spdlog::info("bufferDeviceAddress feature is not supported; related shader capabilities may be unavailable.");
    }

    if (features2.features.robustBufferAccess == VK_TRUE) {
        spdlog::info("Enabling robustBufferAccess for additional GPU memory access safety.");
    } else {
        spdlog::info("robustBufferAccess is not supported on this device.");
    }

    features2.pNext = &features12;

    // information to create logical device (sometimes called "device")
    vk::DeviceCreateInfo device_create_info{};
    device_create_info.queueCreateInfoCount =
      static_cast<uint32_t>(queue_create_infos.size());// number of queue create infos
    device_create_info.pQueueCreateInfos = queue_create_infos.data();// list of queue create infos so device can
                                                                     // create required queues
    device_create_info.enabledExtensionCount =
      static_cast<uint32_t>(extensions.size());// number of enabled logical device extensions
    device_create_info.ppEnabledExtensionNames = extensions.data();// list of enabled logical device extensions
    device_create_info.flags = {};
    device_create_info.pEnabledFeatures = nullptr;
    device_create_info.pNext = &features2;

    // create logical device for the given physical device
    vk::Result const result = physical_device.createDevice(&device_create_info, nullptr, &logical_device);
    ASSERT_VULKAN(static_cast<VkResult>(result), "Failed to create a logical device!");
    if (result != vk::Result::eSuccess || !logical_device) {
        spdlog::critical("Unable to continue without a valid Vulkan logical device.");
        std::abort();
    }

    VULKAN_HPP_DEFAULT_DISPATCHER.init(logical_device);

    //  Queues are created at the same time as the device...
    // So we want handle to queues
    // From given logical device of given queue family, of given queue index (0
    // since only one queue), place reference in given vk::Queue
    graphics_queue = logical_device.getQueue(static_cast<uint32_t>(indices.graphics_family), 0);
    presentation_queue = logical_device.getQueue(static_cast<uint32_t>(indices.presentation_family), 0);
    compute_queue = logical_device.getQueue(static_cast<uint32_t>(indices.compute_family), 0);
}

auto Kataglyphis::VulkanDevice::getQueueFamilies(vk::PhysicalDevice selectedPhysicalDevice)
  -> Kataglyphis::VulkanRendererInternals::QueueFamilyIndices
{
    Kataglyphis::VulkanRendererInternals::QueueFamilyIndices indices{};

    std::vector<vk::QueueFamilyProperties> queue_family_list = selectedPhysicalDevice.getQueueFamilyProperties();

    // Go through each queue family and check if it has at least 1 of required
    // types we need to keep track th eindex by our own
    uint32_t index = 0;
    for (const auto &queue_family : queue_family_list) {
        // first check if queue family has at least 1 queue in that family
        // Queue can be multiple types defined through bitfield. Need to bitwise AND
        // with vk::QueueFlagBits to check if has required  type
        if (queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) {
            indices.graphics_family = static_cast<int>(index);// if queue family valid, than get index
        }

        if (queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) {
            indices.compute_family = static_cast<int>(index);
        }

        // check if queue family suppports presentation
        vk::Bool32 presentation_support = selectedPhysicalDevice.getSurfaceSupportKHR(index, *surface).value;
        // check if queue is presentation type (can be both graphics and
        // presentation)
        if (queue_family.queueCount > 0 && presentation_support) {
            indices.presentation_family = static_cast<int>(index);
        }

        // check if queue family indices are in a valid state
        if (indices.is_valid()) { break; }

        index++;
    }

    return indices;
}

auto Kataglyphis::VulkanDevice::getSwapchainDetails(vk::PhysicalDevice device)
  -> Kataglyphis::VulkanRendererInternals::SwapChainDetails
{
    Kataglyphis::VulkanRendererInternals::SwapChainDetails swapchain_details{};
    // get the surface capabilities for the given surface on the given physical
    // device
    swapchain_details.surface_capabilities = device.getSurfaceCapabilitiesKHR(*surface).value;

    // get list of formats
    swapchain_details.formats = device.getSurfaceFormatsKHR(*surface).value;

    // get list of presentation modes
    swapchain_details.presentation_mode = device.getSurfacePresentModesKHR(*surface).value;

    return swapchain_details;
}

auto Kataglyphis::VulkanDevice::check_device_suitable(vk::PhysicalDevice device) -> bool
{
    vk::PhysicalDeviceFeatures device_features = device.getFeatures();

    Kataglyphis::VulkanRendererInternals::QueueFamilyIndices indices = getQueueFamilies(device);

    bool const extensions_supported = check_device_extension_support(device);

    bool swap_chain_valid = false;

    if (extensions_supported) {
        Kataglyphis::VulkanRendererInternals::SwapChainDetails const swap_chain_details = getSwapchainDetails(device);
        swap_chain_valid = !swap_chain_details.presentation_mode.empty() && !swap_chain_details.formats.empty();
    }

    return indices.is_valid() && extensions_supported && swap_chain_valid && device_features.samplerAnisotropy;
}

auto Kataglyphis::VulkanDevice::check_device_extension_support(vk::PhysicalDevice device) -> bool
{
    std::vector<vk::ExtensionProperties> extensions = device.enumerateDeviceExtensionProperties().value;

    if (extensions.empty()) { return false; }

    for (const auto &device_extension : device_extensions) {
        bool has_extension = false;

        for (const auto &extension : extensions) {
            if (strcmp(device_extension, extension.extensionName) == 0) {
                has_extension = true;
                break;
            }
        }

        if (!has_extension) { return false; }
    }

    return true;
}

auto Kataglyphis::VulkanDevice::getBufferDeviceAddress(const vk::BufferDeviceAddressInfo &info) const
  -> vk::DeviceAddress
{
    return VULKAN_HPP_DEFAULT_DISPATCHER.vkGetBufferDeviceAddress(
      static_cast<VkDevice>(logical_device), reinterpret_cast<const VkBufferDeviceAddressInfo *>(&info));
}