目录
0 基本代码
1 Instance
2 验证层
3 物理设备和队列系列
4 逻辑设备和队列
0 基本代码
首先包括LunarG SDK的Vulkan头,它提供了函数、结构和枚举。stdexcept'和
iostream’头文件被包括在内,用于报告和传播错误
- 函数将被
initVulkan
函数调用 - 进入主循环,开始渲染帧mainLoop
- 一旦窗口关闭,
mainLoop
返回,取消分配在cleanup
函数中使用的资源 - 如果在执行过程中发生任何致命的错误,将抛出一个
std::runtime_error
异常
#include <vulkan/vulkan.h>
#include <iostream>
#include <stdexcept>
#include <cstdlib>
class HelloTriangleApplication {
public:
void run() {
initVulkan();
mainLoop();
cleanup();
}
private:
void initVulkan() {
}
void mainLoop() {
}
void cleanup() {
}
};
int main() {
HelloTriangleApplication app;
try {
app.run();
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
之后的每一节都会增加一个新的函数,该函数将从
initVulkan
中调用,并在cleanup
中为需要在最后释放的私有类成员增加一个或多个新的Vulkan对象。-显式销毁Vulkan对象要么用vkCreateXXX这样的函数直接创建,要么通过vkAllocateXXX这样的函数分配给另一个对象。在确保一个对象不再被用于任何地方后,你需要用对应的vkDestroyXXX和vkFreeXXX销毁它。
pAllocator:
这是一个可选的参数,允许你指定自定义内存分配器的回调。
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
void run()
{
initWindow();
initVulkan();
mainLoop();
cleanup();
}
void initWindow()
{
glfwInit();
//初始化GLFW库。
//GLFW最初被设计为创建一个OpenGL上下文,告诉它不要使用下面的调用创建OpenGL上下文
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
//初始化该窗口
window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
}
void mainLoop()
{
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
}
}
void cleanup()
{
glfwDestroyWindow(window);
glfwTerminate();
}
GLFW将包括它自己的定义并自动加载Vulkan头。 为了保持应用程序的运行,直到错误发生或窗口关闭,我们需要在mainLoop
函数中添加一个事件循环,它循环并检查事件,如按下X按钮,直到窗口被用户关闭。一旦窗口被关闭,我们需要通过销毁它和终止GLFW本身来清理资源。
1 Instance
Vulkan API
使用vkInstance
对象来存储所有每个应用的状态。应用程序必须在执行任何其他Vulkan
操作之前创建一个Vulkan
实例,基本的Vulkan
架构看起来是这样的:
创建一个instance来初始化Vulkan库,实例是你的应用程序和Vulkan库之间的连接,创建它需要向驱动指定一些关于你的应用程序的细节。
void initVulkan()
{
createInstance();
}
void createInstance() {
//填写关于我们应用程序的一些信息
VkApplicationInfo appInfo{};
//sType成员中明确指定类型
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Hello Triangle";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;
//我们要使用哪些全局扩展和验证层
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
//Vulkan是一个与平台无关的API,这意味着你需要一个扩展来与窗口系统对接。
//GLFW有一个方便的内置函数,可以返回它所需要的扩展,我们可以将其传递给结构.
uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
createInfo.enabledExtensionCount = glfwExtensionCount;
createInfo.ppEnabledExtensionNames = glfwExtensions;
//启用全局验证层
createInfo.enabledLayerCount = 0;
//已经指定了Vulkan创建实例所需要的一切,我们最终可以执行vkCreateInstance调用:
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
//几乎所有的Vulkan函数都会返回一个VkResult类型的值,这个值要么是VK_SUCCESS
//要么是一个错误代码
throw std::runtime_error("failed to create instance!");
}
}
Vulkan中对象创建函数参数遵循的一般模式是。
- 指向带有创建信息的结构的指针
- 指向自定义分配器回调的指针,在本教程中总是
nullptr
。- 指向存储新对象句柄的变量的指针
为了在创建实例之前检索支持的扩展列表,有一个vkEnumerateInstanceExtensionProperties函数。它需要一个存储扩展数量的变量指针和一个VkExtensionProperties的数组来存储扩展的细节。它还需要一个可选的第一个参数,允许我们通过一个特定的验证层来过滤扩展,我们现在将忽略这个参数。
//首先需要知道有多少个
uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
//分配一个数组来保存扩展的细节
std::vector<VkExtensionProperties> extensions(extensionCount);
//查询扩展的详细信息
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());
//每个VkExtensionProperties结构包含一个扩展的名称和版本
std::cout << "available extensions:\n";
for (const auto& extension : extensions) {
std::cout << '\t' << extension.extensionName << '\n';
}
VkInstance应该只在程序退出前销毁。它可以在cleanup
中用vkDestroyInstance函数销毁。vkDestroyInstance函数的参数是直接的。
2 验证层
Vulkan要求你对你所做的一切都要非常明确
Vulkan为此引入了一个被称为验证层的优雅系统。验证层是可选的组件,它与Vulkan函数调用挂钩以应用额外的操作。验证层中的常见操作是。
- 对照规范检查参数值,以发现误用情况
- 跟踪对象的创建和销毁以发现资源泄漏
- 通过跟踪调用来源的线程来检查线程的安全性
- 将每个调用及其参数记录到标准输出中
- 跟踪Vulkan调用以进行分析和回放
VkResult vkCreateInstance(
const VkInstanceCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkInstance* instance) {
if (pCreateInfo == nullptr || instance == nullptr) {
log("Null pointer passed to required parameter!");
return VK_ERROR_INITIALIZATION_FAILED;
}
return real_vkCreateInstance(pCreateInfo, pAllocator, instance);
}
验证层可以自由堆叠, Vulkan没有内置任何验证层,但LunarG Vulkan SDK提供了一套不错的验证层,可以检查常见的错误。只有当验证层被安装到系统上时,才能使用它们。
- 首先在程序中添加两个配置变量,以指定要启用的层和是否启用它们。
- 添加一个新的函数
checkValidationLayerSupport
来检查所有请求的图层是否可用 - 检查
validationLayers
中的所有图层是否存在于availableLayers
列表中 - 最后,修改VkInstanceCreateInfo结构实例,以包括验证层名称(如果它们被启用)
const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;
const std::vector<const char*> validationLayers = {
"VK_LAYER_KHRONOS_validation"
};
#ifdef NDEBUG
//NDEBUG宏是C++标准的一部分,意味着 “非调试”。
const bool enableValidationLayers = false;
#else
const bool enableValidationLayers = true;
#endif
bool checkValidationLayerSupport() {
uint32_t layerCount;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
std::vector<VkLayerProperties> availableLayers(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
//检查validationLayers中的所有图层是否存在于availableLayers列表中
for (const char* layerName : validationLayers) {
bool layerFound = false;
for (const auto& layerProperties : availableLayers) {
if (strcmp(layerName, layerProperties.layerName) == 0) {
layerFound = true;
break;
}
}
if (!layerFound) {
return false;
}
}
return true;
}
消息回调使用VK_EXT_debug_utils
扩展来设置一个带有回调的调试信使。
- 首先创建一个
getRequiredExtensions
函数,它将根据是否启用验证层来返回所需的扩展列表
std::vector<const char*> getRequiredExtensions() {
uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
std::vector<const char*> extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);
if (enableValidationLayers) {
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
//使用了VK_EXT_DEBUG_UTILS_EXTENSION_NAME宏,它等于字面字符串 VK_EXT_debug_utils
}
return extensions;
}
现在让我们看看调试回调函数是什么样子的。用PFN_vkDebugUtilsMessengerCallbackEXT
的原型添加一个新的静态成员函数,叫做debugCallback
。VKAPI_ATTR
和VKAPI_CALL
确保该函数具有正确的签名,以便Vulkan调用它。
static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
//pMessage: 调试信息是一个空尾的字符串
//pObjects: 与该消息相关的Vulkan对象句柄的数组
//objectCount: 数组中对象的数量
//最后,pUserData参数包含一个在设置回调时指定的指针,允许你向它传递你自己的数据。
VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
VkDebugUtilsMessageTypeFlagsEXT messageType,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
void* pUserData) {
std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl;
return VK_FALSE;//回调返回一个布尔值,表明触发验证层消息的Vulkan调用是否应该被终止。
}
//true,那么该调用将被中止
第一个参数指定消息的严重性,它是以下标志之一。
vk_debug_utils_message_severity_verbose_bit_ext
: 诊断消息vk_debug_utils_message_severity_info_bit_ext
: 像创建资源的信息消息vk_debug_utils_message_severity_warning_bit_ext
: 关于不一定是错误的行为的消息,但很可能是您的应用程序中的一个错误。vk_debug_utils_message_severity_error_bit_ext
: 关于无效行为的信息,可能导致崩溃
这个枚举的值是这样设置的,你可以使用比较操作来检查一个消息与某个严重程度相比是否相等或更坏,例如:
if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
// Message is important enough to show
}
我们需要在一个结构中填写关于信使及其回调的细节:这个结构应该被传递给vkCreateDebugUtilsMessengerEXT
函数来创建VkDebugUtilsMessengerEXT
对象。不幸的是,由于这个函数是一个扩展函数,它不会被自动加载。我们必须自己使用vkGetInstanceProcAddr来查找它的地址。
VkDebugUtilsMessengerCreateInfoEXT createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
//回调被调用的严重程度类型
createInfo.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;
//消息类型
createInfo.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;
//回调函数的指针。你可以选择传递一个指向pUserData字段的指针
createInfo.pfnUserCallback = debugCallback;
createInfo.pUserData = nullptr; // Optional
//这个结构应该被传递给vkCreateDebugUtilsMessengerEXT函数来创建VkDebugUtilsMessengerEXT对象
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;
}
}
VkDebugUtilsMessengerEXT
对象也需要通过调用vkDestroyDebugUtilsMessengerEXT
来清理。与vkCreateDebugUtilsMessengerEXT
类似,该函数需要明确加载。在CreateDebugUtilsMessengerEXT
下面创建另一个代理函数。
void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) {
auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");
if (func != nullptr) {
func(instance, debugMessenger, pAllocator);
}
}
现在让我们故意犯一个错误,看看验证层的作用。暂时删除cleanup
函数中对DestroyDebugUtilsMessengerEXT
的调用,然后运行你的程序。一旦它退出,你应该看到类似这样的东西。
3 物理设备和队列系列
在通过VkInstance初始化Vulkan库后,我们需要在系统中寻找并选择一个支持我们所需功能的显卡。事实上,我们可以选择任何数量的显卡并同时使用它们,但在本教程中,我们将坚持使用第一个适合我们需求的显卡。
void initVulkan() {
createInstance();
setupDebugMessenger();
pickPhysicalDevice();
}
//我们最终选择的显卡将被存储在一个VkPhysicalDevice句柄中,它被作为一个新的类成员添加
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
void pickPhysicalDevice() {
//开始时只查询数字
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
//如果有0台设备支持Vulkan,那么就没有必要再继续下去了
if (deviceCount == 0) {
throw std::runtime_error("failed to find GPUs with Vulkan support!");
}
//分配一个数组来保存所有的VkPhysicalDevice句柄
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());
for (const auto& device : devices)
{
if (isDeviceSuitable(device))
{
physicalDevice = device;
break;
}
}
if (physicalDevice == VK_NULL_HANDLE)
{
throw std::runtime_error("failed to find a suitable GPU!");
}
}
//每一个进行评估,并检查它们是否适合于我们想要执行的操作
bool isDeviceSuitable(VkPhysicalDevice device) {
return true;
}
为了评估一个设备的适用性,我们可以从查询一些细节开始。基本的设备属性,如名称、类型和支持的Vulkan版本,可以使用vkGetPhysicalDeviceProperties进行查询。
VkPhysicalDeviceProperties deviceProperties;
vkGetPhysicalDeviceProperties(device, &deviceProperties);
纹理压缩、64位浮点和多视口渲染(对VR有用)等可选特性的支持可以用vkGetPhysicalDeviceFeatures查询:
VkPhysicalDeviceFeatures deviceFeatures;
vkGetPhysicalDeviceFeatures(device, &deviceFeatures);
假设我们认为我们的应用程序只适用于支持几何着色器的专用图形卡。那么 isDeviceSuitable
函数会是这样的:
bool isDeviceSuitable(VkPhysicalDevice device) {
VkPhysicalDeviceProperties deviceProperties;
VkPhysicalDeviceFeatures deviceFeatures;
vkGetPhysicalDeviceProperties(device, &deviceProperties);
vkGetPhysicalDeviceFeatures(device, &deviceFeatures);
return deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU &&
deviceFeatures.geometryShader;
}
Queue families
之前已经简单地提到过,Vulkan中几乎所有的操作,从绘图到上传纹理,都需要将命令提交给一个队列。有不同类型的队列,它们来自不同的队列家族,每个队列家族只允许一个子集的命令。例如,可能有一个队列家族只允许处理计算命令,或者一个只允许内存传输相关的命令。
现在我们只想寻找支持图形命令的队列,所以这个函数可以是这样的:
struct QueueFamilyIndices {
//std::optional是一个包装器,在你给它赋值之前不包含任何值。
//在任何时候,你都可以通过调用其has_value()成员函数来查询它是否包含一个值
std::optional<uint32_t> graphicsFamily;
bool isComplete() {
return graphicsFamily.has_value();
}
};
//但是如果一个队列家族不可用呢?我们可以在findQueueFamilies中抛出一个异常
QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
QueueFamilyIndices indices;
// Assign index to queue families that could be found
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);
std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data());
int i = 0;
for (const auto& queueFamily : queueFamilies)
{
if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT)
{
indices.graphicsFamily = i;
}
i++;
}
return indices;
}
//在isDeviceSuitable函数中使用它作为检查
bool isDeviceSuitable(VkPhysicalDevice device) {
QueueFamilyIndices indices = findQueueFamilies(device);
return indices.isComplete();
}
4 逻辑设备和队列
Vulkan 逻辑设备与队列,在选择要使用的物理设备之后,我们需要设置一个逻辑设备用于交互。逻辑设备创建过程与instance创建过程类似,也需要描述我们需要使用的功能。因为我们已经查询过哪些队列簇可用,在这里需要进一步为逻辑设备创建具体类型的命令队列。如果有不同的需求,也可以基于同一个物理设备创建多个逻辑设备。
VkDevice device;//存储逻辑设备句柄。
添加一个createLogicalDevice
函数,从initVulkan
中调用。
void createLogicalDevice()
{
//要的单个队列家族的队列数量
QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
VkDeviceQueueCreateInfo queueCreateInfo{};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value();
queueCreateInfo.queueCount = 1;
//Vulkan让你为队列分配优先级,以影响命令缓冲区执行的调度
float queuePriority = 1.0f;
queueCreateInfo.pQueuePriorities = &queuePriority;
//查询支持的特性
VkPhysicalDeviceFeatures deviceFeatures{};
//首先添加指向队列创建信息和设备特征结构的指针
createInfo.pQueueCreateInfos = &queueCreateInfo;
createInfo.queueCreateInfoCount = 1;
//填写主VkDeviceCreateInfo结构
VkDeviceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();
createInfo.pEnabledFeatures = &deviceFeatures;
createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
if (enableValidationLayers) {
createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validationLayers.data();
}
else {
createInfo.enabledLayerCount = 0;
}
//实例化这个逻辑设备
if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) {
throw std::runtime_error("failed to create logical device!");
}
//存储图形队列的句柄
//vkGetDeviceQueue函数来检索每个队列家族的队列柄
vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue);
vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);
}