[C++&Vulkan] 加载Vulkan函数指针
众所周知,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封装了。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)