众所周知,Vulkan需要加载函数指针,一般而言,为了简便,会直接链接vulkan-1.dll,但是这太简单了,所以来整一下动态加载函数指针。

方案一:使用volk

首先需要引入volk的头文件。

#define VOLK_IMPLEMENTATION
#include <volk.h>

需要注意的是,volk.c会引入volk.h,而定义了VOLK_IMPLEMENTATION的volk.h会引入volk.c,这会导致递归,所以volk.h会undef VOLK_IMPLEMENTATION。

接下来为了和Vulkan-Hpp相适配,需要定义一个结构体Volk*Dispatcher,以便引入getVkHeaderVersion。(Vulkan-Hpp会调用此函数)

struct VolkInstanceDispatcher : public VolkInstanceTable
{
#ifndef NDEBUG
    static size_t getVkHeaderVersion() { return VOLK_HEADER_VERSION; }
#endif
};

struct VolkDeviceDispatcher : public VolkDeviceTable
{
#ifndef NDEBUG
    static size_t getVkHeaderVersion() { return VOLK_HEADER_VERSION; }
#endif
};

值得一提的是,这两个结构体都与volk二进制兼容,可以通过以下方式检验:

static_assert(std::is_trivial_v<VolkInstanceDispatcher>);
static_assert(std::is_standard_layout_v<VolkInstanceDispatcher>);
static_assert(sizeof(VolkInstanceDispatcher) == sizeof(VolkInstanceTable));

static_assert(std::is_trivial_v<VolkDeviceDispatcher>);
static_assert(std::is_standard_layout_v<VolkDeviceDispatcher>);
static_assert(sizeof(VolkDeviceDispatcher) == sizeof(VolkDeviceTable));

接下来初始化volk,加载动态链接库并加载基础函数指针

if (volkInitialize() != VK_SUCCESS)
    throw std::runtime_error("Failed to initialize volk");

创建实例

    VkApplicationInfo appInfo{};
    appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
    appInfo.apiVersion = VK_API_VERSION_1_0;
    VkInstanceCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
    createInfo.pApplicationInfo = &appInfo;

    VkInstance instance;
    if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS)
        throw std::runtime_error("Failed to create instance");

然后通过volk加载函数指针即可

    auto dispatcher = std::make_unique<VolkInstanceDispatcher>();
    volkLoadInstanceTable(dispatcher.get(), instance);

最后创建一个智能指针

    vk::UniqueHandle<vk::Instance, VolkInstanceDispatcher> uniqueHandle{
        vk::Instance{instance},
        vk::detail::ObjectDestroy<vk::detail::NoParent, VolkInstanceDispatcher>{
            nullptr,
            *dispatcher,
        },
    };

这样就完成了第一个函数:createInstance

创建逻辑设备

与创建实例一样,先创建一个逻辑设备

    VkDeviceCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;

    VkDevice device;
    if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS)
        throw std::runtime_error("Failed to create device");

然后加载函数指针

    auto dispatcher = std::make_unique<VolkDeviceDispatcher>();
    volkLoadDeviceTable(dispatcher.get(), device);

最后创建智能指针

    vk::UniqueHandle<vk::Device, VolkDeviceDispatcher> uniqueHandle{
        vk::Device{device},
        vk::detail::ObjectDestroy<vk::detail::NoParent, VolkDeviceDispatcher>{
            nullptr,
            *dispatcher,
        },
    };

这样就完成了第二个函数createDevice

但是这里需要注意的是,此时全局vkCreateDevice已经被替换成了函数指针变量(而非函数),并且没有被volk加载(事实上是被加载进了VolkInstanceDispatcher而非全局),所以需要手动传入PFN_vkCreateDevice vkCreateDevice。

最终代码

#include <iostream>
#include <type_traits>
#include <tuple>
#include <memory>
#if !defined(VOLK_SOURCE) && !defined(VOLK_IMPLEMENTATION)
#define VOLK_IMPLEMENTATION
#endif
#include <volk.h>
#include <vulkan/vulkan.hpp>

struct VolkInstanceDispatcher : public VolkInstanceTable
{
#ifndef NDEBUG
    static size_t getVkHeaderVersion() { return VOLK_HEADER_VERSION; }
#endif
};

static_assert(std::is_trivial_v<VolkInstanceDispatcher>);
static_assert(std::is_standard_layout_v<VolkInstanceDispatcher>);
static_assert(sizeof(VolkInstanceDispatcher) == sizeof(VolkInstanceTable));

std::tuple<vk::UniqueHandle<vk::Instance, VolkInstanceDispatcher>, std::unique_ptr<VolkInstanceDispatcher>> createInstance()
{
    VkApplicationInfo appInfo{};
    appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
    appInfo.apiVersion = VK_API_VERSION_1_0;
    VkInstanceCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
    createInfo.pApplicationInfo = &appInfo;

    VkInstance instance;
    if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS)
        throw std::runtime_error("Failed to create instance");

    auto dispatcher = std::make_unique<VolkInstanceDispatcher>();
    volkLoadInstanceTable(dispatcher.get(), instance);

    vk::UniqueHandle<vk::Instance, VolkInstanceDispatcher> uniqueHandle{
        vk::Instance{instance},
        vk::detail::ObjectDestroy<vk::detail::NoParent, VolkInstanceDispatcher>{
            nullptr,
            *dispatcher,
        },
    };

    return std::make_tuple(
        std::move(uniqueHandle),
        std::move(dispatcher));
}

struct VolkDeviceDispatcher : public VolkDeviceTable
{
#ifndef NDEBUG
    static size_t getVkHeaderVersion() { return VOLK_HEADER_VERSION; }
#endif
};

static_assert(std::is_trivial_v<VolkDeviceDispatcher>);
static_assert(std::is_standard_layout_v<VolkDeviceDispatcher>);
static_assert(sizeof(VolkDeviceDispatcher) == sizeof(VolkDeviceTable));

std::tuple<vk::UniqueHandle<vk::Device, VolkDeviceDispatcher>, std::unique_ptr<VolkDeviceDispatcher>> createDevice(PFN_vkCreateDevice vkCreateDevice, VkPhysicalDevice physicalDevice)
{
    VkDeviceCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;

    VkDevice device;
    if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS)
        throw std::runtime_error("Failed to create device");

    auto dispatcher = std::make_unique<VolkDeviceDispatcher>();
    volkLoadDeviceTable(dispatcher.get(), device);

    vk::UniqueHandle<vk::Device, VolkDeviceDispatcher> uniqueHandle{
        vk::Device{device},
        vk::detail::ObjectDestroy<vk::detail::NoParent, VolkDeviceDispatcher>{
            nullptr,
            *dispatcher,
        },
    };

    return std::make_tuple(
        std::move(uniqueHandle),
        std::move(dispatcher));
}

int main()
{
    if (volkInitialize() != VK_SUCCESS)
        throw std::runtime_error("Failed to initialize volk");

    auto [instance, instanceDispatcher] = createInstance();
    std::cout << "Succeed to create instance: " << instance.get() << "\n"
              << "vkDestroyInstance: " << std::addressof(instanceDispatcher->vkDestroyInstance) << "\n";

    auto physicalDevices = instance->enumeratePhysicalDevices(*instanceDispatcher);
    auto [device, deviceDispatcher] = createDevice(instanceDispatcher->vkCreateDevice, physicalDevices[0]);
    std::cout << "Succeed to create device: " << device.get() << "\n"
              << "vkDestroyDevice: " << std::addressof(deviceDispatcher->vkDestroyDevice) << "\n";

    // volkFinalize();

    return 0;
}

方案二:使用vk::detail::DispatchLoaderDynamic

首先初始化vk::detail::DispatchLoaderDynamic,它会自动加载基础函数

    auto dispatcher = std::make_unique<vk::detail::DispatchLoaderDynamic>();
    dispatcher->init();

因为vk::detail::DispatchLoaderDynamic同时包含实例级函数与设备级函数,所以它会自动加载所有函数

    auto instance = vk::createInstance(createInfo, nullptr, *dispatcher);
    dispatcher->init(*uniqueHandle);

事实上理论上通过vkGetInstanceProcAddr和vkGetDeviceProcAddr获取的设备级函数是有区别的,如果可以的话推荐用通过vkGetDeviceProcAddr获取的函数

虽说可以用void vk::detail::DispatchLoaderDynamic::init(Device deviceCpp) VULKAN_HPP_NOEXCEPT加载设备级函数,但是这样就是对设备级函数加载了两遍,所以我并不推荐方案二。

完整代码如下,与方案一大同小异

#include <iostream>
#include <tuple>
#include <memory>
#define VK_NO_PROTOTYPES
#include <vulkan/vulkan.hpp>

std::tuple<vk::UniqueInstance, std::unique_ptr<vk::detail::DispatchLoaderDynamic>> createInstance()
{
    vk::ApplicationInfo appInfo{};
    appInfo.apiVersion = VK_API_VERSION_1_0;
    vk::InstanceCreateInfo createInfo{};
    createInfo.pApplicationInfo = &appInfo;

    auto dispatcher = std::make_unique<vk::detail::DispatchLoaderDynamic>();
    dispatcher->init();

    auto instance = vk::createInstance(createInfo, nullptr, *dispatcher);
    dispatcher->init(instance);

    vk::UniqueHandle<vk::Instance, vk::detail::DispatchLoaderDynamic> uniqueHandle{
        instance,
        vk::detail::ObjectDestroy<vk::detail::NoParent, vk::detail::DispatchLoaderDynamic>{
            nullptr,
            *dispatcher,
        },
    };

    return std::make_tuple(
        std::move(uniqueHandle),
        std::move(dispatcher));
}

int main()
{
    auto [instance, dispatcher] = createInstance();
    std::cout << "Succeed to create instance: " << instance.get() << "\n"
              << "vkDestroyInstance: " << std::addressof(dispatcher->vkDestroyInstance) << "\n";

    return 0;
}

方案三:使用vulkan_raii.hpp

基础使用

使用vulkan_raii.hpp是最简单的

vk::raii::Context与volkInitialize和vk::detail::DispatchLoaderDynamic::init的作用一样,提供全局基础函数。

之后仅需调用vk::raii::Context::createInstance就能创建一个vk::raii::Instance。

枚举物理设备后仅需调用vk::raii::PhysicalDevice::createDevice就能创建一个逻辑设备。

完整代码如下:

#include <iostream>
#define VK_NO_PROTOTYPES
#include <vulkan/vulkan_raii.hpp>

vk::raii::Instance createInstance(const vk::raii::Context &context)
{
    vk::ApplicationInfo appInfo{};
    appInfo.apiVersion = VK_API_VERSION_1_0;
    vk::InstanceCreateInfo createInfo{};
    createInfo.pApplicationInfo = &appInfo;

    return vk::raii::Instance{context, createInfo};
}

vk::raii::Device createDevice(const vk::raii::PhysicalDevice &physicalDevice)
{
    vk::DeviceCreateInfo createInfo{};
    return vk::raii::Device{physicalDevice, createInfo};
}

int main()
{
    vk::raii::Context context;
    vk::raii::Instance instance = createInstance(context);
    std::cout << "Succeed to create instance: " << *instance << "\n"
              << "vkDestroyInstance: " << std::addressof(instance.getDispatcher()->vkDestroyInstance) << "\n";

    auto physicalDevices = instance.enumeratePhysicalDevices();
    vk::raii::Device device = createDevice(physicalDevices[0]);
    std::cout << "Succeed to create device: " << *device << "\n"
              << "vkDestroyDevice: " << std::addressof(device.getDispatcher()->vkDestroyDevice) << "\n";

    return 0;
}

额外开销

但是需要注意的是,所有的(绝大部分)raii类都额外储存了父级资源对象、一个调度器指针、一个自定义分配器回调指针,这会有额外的24字节开销。

以vk::raii::Image为例,其内部是这样的

      VULKAN_HPP_NAMESPACE::Device     m_device     = {};
      VULKAN_HPP_NAMESPACE::Image      m_image      = {};
      const AllocationCallbacks *      m_allocator  = {};
      detail::DeviceDispatcher const * m_dispatcher = nullptr;

这么设计的原因如下:

1.函数调用时需要传入其对应父级资源句柄,在VkImage中是VkDevice

2.销毁资源时需要传递其分配回调

3.调用函数时,由于不同实例级/设备级函数不通用,所以需要指定调度器

这就是为什么vk::raii::Image可以直接调用bindMemory,而vk::Image必须通过vk::Device::bindImageMemory调用。

结论

如果需要最小内存开销,可以选择方案一。

如果能接受额外开销,可以选择方案三。

如果需要方案三的快捷又不接受额外开销,那只能自己写一个RAII封装了。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐