Ch1-3 创建VK实例与逻辑设备

从本节开始,我们将正式接触Vulkan SDK,开始书写Vulkan代码。

指定所需的层和扩展

首先来获取实例级别扩展。之前在Ch1-1书写了InitializeWindow(...),现在其中,在glfwWindowHint(GLFW_RESIZABLE, isResizable);后加入以下代码:

uint32_t extensionCount = 0;
const char** extensionNames;
extensionNames = glfwGetRequiredInstanceExtensions(&extensionCount);
if (!extensionNames) {
    std::cout << std::format("[ InitializeWindow ]\nVulkan is not available on this machine!\n");
    glfwTerminate();
    return false;
}
for (size_t i = 0; i < extensionCount; i++)
    graphicsBase::Base().AddInstanceExtension(extensionNames[i]);

const char** glfwGetRequiredInstanceExtensions(...) 的参数说明

uint32_t* count

若执行成功,将所需扩展的数量写入到*count

  • glfwGetRequiredInstanceExtensions(...)获取平台所需的扩展,若执行成功,返回一个指针,指向一个由所需扩展的名称为元素的数组,失败则返回nullptr,并意味着此设备不支持Vulkan。

Important

注意,你不需要手动 delete extensionNames,GLFW的注释及官方文档中写到:
The returned array is allocated and freed by GLFW. You should not free it yourself. It is guaranteed to be valid only until the library is terminated.

在Windows上取得的扩展名称有两个,"VK_KHR_surface""VK_KHR_win32_surface"。 如果明知运行环境是Windows,那么也可以直接push而不需要调用glfwGetRequiredInstanceExtensions(...):

graphicsBase::Base().AddInstanceExtension("VK_KHR_surface");
graphicsBase::Base().AddInstanceExtension("VK_KHR_win32_surface");

接着是设备级别的扩展,所需的只有"VK_KHR_swapchain"

graphicsBase::Base().AddDeviceExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
  • 因为编译器会检查符号是否已定义,vulkan_core.h中提供了VK_KHR_SWAPCHAIN_EXTENSION_NAME宏来避免你写错"VK_KHR_swapchain"vulkan_core.h中也提供了与"VK_KHR_surface"相应的宏VK_KHR_SURFACE_EXTENSION_NAMEvulkan_win32.h中提供了与"VK_KHR_win32_surface"相应的宏VK_KHR_WIN32_SURFACE_EXTENSION_NAME

除此之外仅需要验证层及debug messenger的扩展,来填充前一节中定义在VKBase.h中的graphicsBase::CreateInstance(...):

VkResult CreateInstance(VkInstanceCreateFlags flags = 0) {
#ifndef NDEBUG
    AddInstanceLayer("VK_LAYER_KHRONOS_validation");
    AddInstanceExtension(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
#endif
    /*待后续填充*/
}
  • #ifndef NDEBUG是照标准库中的assert.h学的写法,NDEBUG是C标准库中的宏。

  • vulkan_core.h提供了VK_EXT_DEBUG_UTILS_EXTENSION_NAME宏来避免你写错写错"VK_EXT_debug_utils"

这段代码说明仅在编译选项为DEBUG时,在instanceLayersinstanceExtensions尾部加上所需的名称。

使用Vulkan的最新版本

如果你想使用Vulkan的最新版本,须在创建Vulkan实例前,用vkEnumerateInstanceVersion(...)取得取得当前运行环境所支持的最新Vulkan版本,然而,Vulkan1.0版本中是不支持这个函数的(尽管你下的SDK是新版本,程序在用户电脑上运行时连接的dll可能是Vulkan1.0的),所以要先用vkGetInstanceProcAddr(...)尝试取得该函数的指针,若返回非空指针,则说明可以执行该函数且Vulkan版本至少为1.1,否则说明当前运行环境中Vulkan的最高版本为1.0。

PFN_vkVoidFunction VKAPI_CALL vkGetInstanceProcAddr(...) 的参数说明

VkInstance instance

Vulkan实例的handle,若要取得的函数指针不依赖Vulkan实例,可为VK_NULL_HANDLE

const char* pName

所要取得的函数的名称

  • 若无法取得相应名称的函数,返回nullptr

填充UseLatestApiVersion():

VkResult UseLatestApiVersion() {
    if (vkGetInstanceProcAddr(VK_NULL_HANDLE, "vkEnumerateInstanceVersion"))
        return vkEnumerateInstanceVersion(&apiVersion);
    return VK_SUCCESS;
}

创建一个Vulkan实例

几乎创建任何Vulkan对象都得填写一个创建信息结构体,对于VkInstance,这个结构体叫VkInstanceCreateInfo

struct VkInstanceCreateInfo 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkInstanceCreateFlags flags

const VkApplicationInfo* pApplicationInfo

指向描述本程序相关信息的结构体

uint32_t enabledLayerCount

所需额外开启的实例级别层数

const char* const* ppEnabledLayerNames

指向由所需开启的层的名称构成的数组(同一名称可以重复出现)

uint32_t enabledExtensionCount

所需额外开启的实例级别扩展数

const char* const* ppEnabledExtensionNames

指向由所需开启的扩展的名称构成的数组(同一名称可以重复出现)

  • Vulkan的各种创建信息结构体中皆有个pNext,这是为了可扩展性设置的,它指向一些Vulkan1.0后的版本或与特定厂商的扩展相关的结构体。今后如无必要不做特意说明,其值填写为nullptr

  • flags也是个很常见的东西,它被用于指定一些特殊要求,有些创建信息中的flags可以填写内容,有些目前没有任何可用的flag bit。这里flags只有一个与扩展相关的可用的bit。今后如无必要亦不做特意说明,其值填写为0。

于是来看一下这个VkApplicationInfo

struct VkApplicationInfo 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_APPLICATION_INFO

const void* pNext

无用,必须为nullptr

const void* pApplicationName

应用程序的名称

uint32_t applicationVersion

应用程序的版本号

const void* pEngineName

引擎的名称

uint32_t engineVersion

引擎的版本号

uint32_t apiVersion

VulkanAPI的版本号,必填

这个结构体很水,你可以用它描述你的应用程序名称和版本号,如果你写的程序是某种引擎,比如游戏引擎,可以描述其引擎名称和版本号,随你喜欢即可,毕竟这对程序的运行不构成任何影响,可以直接省略。
重要的是apiVersion,这个参数决定了所用的VulkanAPI版本号。版本号总是能有三个部分,你可以用VK_MAKE_VERSION(major, minor, patch)计算版本号。apiVersion不关心patch版本号,所以请直接使用宏VK_API_VERSION_1_0VK_API_VERSION_1_1VK_API_VERSION_1_2VK_API_VERSION_1_3

填写了以上两个结构体后,使用vkCreateInstance(...)来创建Vulkan实例:

VkResult VKAPI_CALL vkCreateInstance(...) 的参数说明

const VkInstanceCreateInfo* pCreateInfo

指向VkInstance的创建信息

const VkAllocationCallbacks* pAllocator

如有必要,指向描述自定义内存分配方式的结构体

VkInstance* pInstance

若执行成功,将Vulkan实例的handle写入*pInstance

  • pAlloctar在本教程中一概不用,其值填写为nullptr

  • 若函数执行成功,返回值只可能是VK_SUCCESS,其值为0。若函数在成功时除了VK_SUCCESS外没有其他可能的返回值,今后不再赘述。

于是继续填充graphicsBase::CreateInstance(...),最后整个函数如下:

VkResult CreateInstance(VkInstanceCreateFlags flags = 0) {
#ifndef NDEBUG
    AddInstanceLayer("VK_LAYER_KHRONOS_validation");
    AddInstanceExtension(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
#endif
    VkApplicationInfo applicatianInfo = {
        .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
        .apiVersion = apiVersion
    };
    VkInstanceCreateInfo instanceCreateInfo = {
        .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
        .flags = flags,
        .pApplicationInfo = &applicatianInfo,
        .enabledLayerCount = uint32_t(instanceLayers.size()),
        .ppEnabledLayerNames = instanceLayers.data(),
        .enabledExtensionCount = uint32_t(instanceExtensions.size()),
        .ppEnabledExtensionNames = instanceExtensions.data()
    };
    if (VkResult result = vkCreateInstance(&instanceCreateInfo, nullptr, &instance)) {
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to create a vulkan instance!\nError code: {}\n", int32_t(result));
        return result;
    }
    //成功创建Vulkan实例后,输出Vulkan版本
    std::cout << std::format(
        "Vulkan API Version: {}.{}.{}\n",
        VK_VERSION_MAJOR(apiVersion),
        VK_VERSION_MINOR(apiVersion),
        VK_VERSION_PATCH(apiVersion));
#ifndef NDEBUG
    //创建完Vulkan实例后紧接着创建debug messenger
    CreateDebugMessenger();
#endif
    return VK_SUCCESS;
}
  • if (VkResult result = vkCreateInstance(&instanceCreateInfo, nullptr, &instance))
    以上这种写法不太直观,如果你比较介意的话,写成:
    if (VkResult result = vkCreateInstance(&instanceCreateInfo, nullptr, &instance); result != VK_SUCCESS)

  • 这里我在聚合初始化中使用了指派符,对于C语言式的结构体(所有成员为public且没有构造函数),没有被指派的成员被零初始化。

  • 如果初始化失败,把result返回出去。错误处理在函数外部进行。

之后是在InitializeWindow(...)中调用graphicsBase::Base().CreateInstance(...),相应代码我会汇总在本节最后。

检查层和实例级别扩展是否可用

入门教程中的所有层和扩展都是必须的,不过这里先准备好相应的函数,以在创建Vulkan实例失败时检查是否可以去掉一些非必要的层或扩展。

检查层是否可用

vkEnumerateInstanceLayerProperties(...)取得可用层的列表。

VkResult VKAPI_CALL vkEnumerateInstanceLayerProperties(...) 的参数说明

uint32_t* pPropertyCount

若pProperties为nullptr,则将可用层的数量返回到*pPropertyCount,否则由*pPropertyCount指定所需获取的VkLayerProperties的数量

VkLayerProperties* pProperties

若pProperties非nullptr,则将*pPropertyCount个可用层的VkLayerProperties返回到*pProperties

  • 若传入的*pPropertyCount等于所有可用层的总数,函数成功时返回VK_SUCCESS,若传入的*pPropertyCount小于可用层总数,返回VK_INCOMPLETE

填充graphicsBase::CheckInstanceLayers(...),首先取得可用层的总数:

uint32_t layerCount;
std::vector<VkLayerProperties> availableLayers;
if (VkResult result = vkEnumerateInstanceLayerProperties(&layerCount, nullptr)) {
    std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get the count of instance layers!\n");
    return result;
}

接着,若layerCount为0,则说明没有任何可用层(虽然没这个可能),否则嵌套循环逐个比较字符串,若没有找到某个层的名称,将layersToCheck中对应的字符串指针设置为nullptr

if (layerCount) {
    availableLayers.resize(layerCount);
    if (VkResult result = vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data())) {
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to enumerate instance layer properties!\nError code: {}\n", int32_t(result));
        return result;
    }
    for (auto& i : layersToCheck) {
        bool found = false;
        for (auto& j : availableLayers)
            if (!strcmp(i, j.layerName)) {
                found = true;
                break;
            }
        if (!found)
            i = nullptr;
    }
}
else
    for (auto& i : layersToCheck)
        i = nullptr;
//一切顺利则返回VK_SUCCESS
return VK_SUCCESS;
  • 两次调用vkEnumerateInstanceLayerProperties(...),第一次取得layerCount,第二次取得layerCount个VkLayerProperties,因此第二次调用该函数时若执行成功,必然只可能返回VK_SUCCESS,于是这里就只需要验证result是否为VK_SUCCESS而不需要验证是否为VK_INCOMPLETE了。

检查实例级别扩展是否可用

vkEnumerateInstanceExtensionProperties(...)取得可用扩展的列表:

VkResult VKAPI_CALL vkEnumerateInstanceExtensionProperties(...) 的参数说明

const char* pLayerName

因有些扩展隶属于特定的层,在此填入层的名称,若为nullptr,则取得所有默认提供或隐式开启的层的扩展

uint32_t* pPropertyCount

若pProperties为nullptr,则将可用扩展的数量返回到*pPropertyCount,否则由*pPropertyCount指定所需获取的VkExtensionProperties的数量

VkExtensionProperties* pProperties

若pProperties非nullptr,则将*pPropertyCount个可用扩展的VkExtensionProperties返回到*pProperties

用几乎换汤不换药的写法填充graphicsBase::CheckInstanceExtensions(...):

VkResult CheckInstanceExtensions(std::span<const char*> extensionsToCheck, const char* layerName) const {
    uint32_t extensionCount;
    std::vector<VkExtensionProperties> availableExtensions;
    if (VkResult result = vkEnumerateInstanceExtensionProperties(layerName, &extensionCount, nullptr)) {
        layerName ?
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get the count of instance extensions!\nLayer name:{}\n", layerName) :
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get the count of instance extensions!\n");
        return result;
    }
    if (extensionCount) {
        availableExtensions.resize(extensionCount);
        if (VkResult result = vkEnumerateInstanceExtensionProperties(layerName, &extensionCount, availableExtensions.data())) {
            std::cout << std::format("[ graphicsBase ] ERROR\nFailed to enumerate instance extension properties!\nError code: {}\n", int32_t(result));
            return result;
        }
        for (auto& i : extensionsToCheck) {
            bool found = false;
            for (auto& j : availableExtensions)
                if (!strcmp(i, j.extensionName)) {
                    found = true;
                    break;
                }
            if (!found)
                i = nullptr;
        }
    }
    else
        for (auto& i : extensionsToCheck)
            i = nullptr;
    return VK_SUCCESS;
}

创建Debug Messenger

Vulkan中,扩展相关的函数,若非设备特定,大都通过vkGetInstanceProcAddr(...)来获取(虽然在vulkan_core.h里有定义,但vulkan-1.lib里没有其binary code),创建debug messenger也不例外:

PFN_vkCreateDebugUtilsMessengerEXT vkCreateDebugUtilsMessenger =
        reinterpret_cast<PFN_vkCreateDebugUtilsMessengerEXT>(vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT"));

VkResult VKAPI_CALL vkCreateDebugUtilsMessengerEXT(...) 的参数说明

VkInstance instance

Vulkan实例的handle

const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo

指向VkDebugUtilsMessengerEXT的创建信息

const VkAllocationCallbacks* pAllocator

如有必要,指向描述自定义内存分配方式的结构体

VkDebugUtilsMessengerEXT* pMessenger

若执行成功,将debug messenger的handle写入*pMessenger

于是来看一下这个VkDebugUtilsMessengerCreateInfoEXT

struct VkDebugUtilsMessengerCreateInfoEXT 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkDebugUtilsMessengerCreateFlagsEXT flags

VkDebugUtilsMessageSeverityFlagsEXT messageSeverity

表明所需获取debug信息的严重性

VkDebugUtilsMessageTypeFlagsEXT messageType

表明需要获取哪些类型的debug信息

PFN_vkDebugUtilsMessengerCallbackEXT pfnUserCallback

产生debug信息后,所调用自定义回调函数的指针

void* pUserData

提供一个地址,之后如有需要,可以让自定义回调函数将数据存入该地址

  • 请自行在VS中将光标移动到VkDebugUtilsMessengerCreateFlagsEXTVkDebugUtilsMessageSeverityFlagsEXT,然后按F12来查询他们有哪些flag bit。

  • 如果错误发生在程序能正常渲染的情况下,那么你可能希望把debug信息渲染到窗口画面中,而不是即刻输出到控制台,这就是pUserData的存在意义。

自定义回调函数的类型声明如下:

typedef VkBool32 (VKAPI_PTR PFN_vkDebugUtilsMessengerCallbackEXT) 的参数说明

VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity

所获取到的debug信息的严重性

VkDebugUtilsMessengerCreateInfoEXT messageTypes

所获取到的debug信息的类型

const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData

所获取到的相关回调信息,包含debug信息、涉及到的队列、命令缓冲区、Vulkan对象

void* pUserData

对应VkDebugUtilsMessengerCreateInfoEXT中的pUserData

于是剧本流程很清晰了,填充graphicsBase::CreateDebugMessenger(...),首先定义这个自定义回调函数:

static PFN_vkDebugUtilsMessengerCallbackEXT DebugUtilsMessengerCallback = [](
    VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
    VkDebugUtilsMessageTypeFlagsEXT messageTypes,
    const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
    void* pUserData)->VkBool32 {
        std::cout << std::format("{}\n\n", pCallbackData->pMessage);
        return VK_FALSE;
};

我让这个lambda表达式除了输出debug信息到控制台外啥都不干。然后来填写VkDebugUtilsMessengerCreateInfoEXT

VkDebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfo = {
    .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
    .messageSeverity =
        VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
        VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
    .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,
    .pfnUserCallback = DebugUtilsMessengerCallback
};
  • 我对messageSeverity设置了VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXTVK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,对messageType设置了所有可用的bit,这意味着将获取所有警告和错误信息。

这之后就是获取并调用vkCreateDebugUtilsMessengerEXT(...),整个CreateDebugMessenger(...)如下:

VkResult CreateDebugMessenger() {
    static PFN_vkDebugUtilsMessengerCallbackEXT DebugUtilsMessengerCallback = [](
        VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
        VkDebugUtilsMessageTypeFlagsEXT messageTypes,
        const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
        void* pUserData)->VkBool32 {
            std::cout << std::format("{}\n\n", pCallbackData->pMessage);
            return VK_FALSE;
    };
    VkDebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfo = {
        .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
        .messageSeverity =
            VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
            VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
        .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,
        .pfnUserCallback = DebugUtilsMessengerCallback
    };
    PFN_vkCreateDebugUtilsMessengerEXT vkCreateDebugUtilsMessenger =
        reinterpret_cast<PFN_vkCreateDebugUtilsMessengerEXT>(vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT"));
    if (vkCreateDebugUtilsMessenger) {
        VkResult result = vkCreateDebugUtilsMessenger(instance, &debugUtilsMessengerCreateInfo, nullptr, &debugMessenger);
        if (result)
            std::cout << std::format("[ graphicsBase ] ERROR\nFailed to create a debug messenger!\nError code: {}\n", int32_t(result));
        return result;
    }
    std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get the function pointer of vkCreateDebugUtilsMessengerEXT!\n");
    return VK_RESULT_MAX_ENUM;
}
  • 虽然在正确书写函数名字符串的情况下应当不可能,如果无法取得vkCreateDebugUtilsMessengerEXT(...),返回VK_RESULT_MAX_ENUM,其值为INT32_MAX。这并不是个有效的错误代码,正是因此才使用这个数值,一来因为VkResult的枚举项中没有任何符合该种情况的返回值,二来这个数值不会跟任何Vulkan函数的返回值重合。注意,不要使用VK_ERROR_UNKNOWN,一些函数在失败时能返回这个数值。因没有合适的错误代码而返回VK_RESULT_MAX_ENUM的情况在这套教程中还会出现很多次。

Note

若你需要获取Vulkan实例创建过程中的debug信息:
VkInstanceCreateInfo::pNext(或其pNext链中的其它结构体的pNext成员)指向一个VkDebugUtilsMessengerCreateInfoEXT结构体,如此一来相当于创建一个只在Vulkan实例的创建过程中起作用的、临时的debug messenger。

创建Window Surface

glfwCreateWindowSurface(...)为GLFW窗口创建window surface。

VkResult** glfwCreateWindowSurface(...) 的参数说明

VkInstance instance

Vulkan实例的handle

GLFWwindow* window

窗口的指针

const VkAllocationCallbacks* allocator

一个指向描述自定义内存分配方式的结构体的指针

VkSurfaceKHR surface

Window surface的handle

于是在InitializeWindow(...)中,在之前所写的创建窗口的代码后加入以下代码:

VkSurfaceKHR surface = VK_NULL_HANDLE;
if (VkResult result = glfwCreateWindowSurface(graphicsBase::Base().Instance(), pWindow, nullptr, &surface)) {
    std::cout << std::format("[ InitializeWindow ] ERROR\nFailed to create a window surface!\nError code: {}\n", int32_t(result));
    glfwTerminate();
    return false;
}
graphicsBase::Base().Surface(surface);

获取并选择物理设备

获取物理设备列表

vkEnumeratePhysicalDevices(...)获取物理设备列表:

VkResult VKAPI_CALL vkEnumeratePhysicalDevices(...) 的参数说明

VkInstance instance

Vulkan实例的handle

uint32_t* pPhysicalDeviceCount

若pPhysicalDevices为nullptr,则将物理设备的数量返回到*pPhysicalDeviceCount,否则由*pPhysicalDeviceCount指定所需获取的VkPhysicalDevice的数量

VkPhysicalDevice* pPhysicalDevices

若pPhysicalDevices非nullptr,则将*pPhysicalDeviceCount个物理设备的VkPhysicalDevice返回到*pPhysicalDevices

从函数参数可以看出,获取物理设备列表的方法,相比获取实例级别层和扩展的方法可谓完全换汤不换药,填充graphicsBase::GetPhysicalDevices():

VkResult GetPhysicalDevices() {
    uint32_t deviceCount;
    if (VkResult result = vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr)) {
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get the count of physical devices!\nError code: {}\n", int32_t(result));
        return result;
    }
    if (!deviceCount)
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to find any physical device supports vulkan!\n"),
        abort();
    availablePhysicalDevices.resize(deviceCount);
    VkResult result = vkEnumeratePhysicalDevices(instance, &deviceCount, availablePhysicalDevices.data());
    if (result)
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to enumerate physical devices!\nError code: {}\n", int32_t(result));
    return result;
}
  • 要是一个物理设备都没有,显然程序跑不了,直接abort()。

Note

如果vkEnumeratePhysicalDevices(...)只给你返回一个设备,尝试更新所有显卡的驱动(虽然不清楚具体原因,我在刚开始写这个教程的时候遇到过这个问题,但是如今已经没这个问题了)。
如果无论如何都只能查找到一张显卡,你可以去独立显卡的控制面板(比如Nvidia控制面板)里指定使用独显来运行程序。

检查并取得所需的队列族索引

vkGetPhysicalDeviceQueueFamilyProperties(...)取得物理设备所具有的队列族的列表:

void VKAPI_CALL vkGetPhysicalDeviceQueueFamilyProperties(...) 的参数说明

VkPhysicalDevice physicalDevice

物理设备的handle

uint32_t* pQueueFamilyPropertyCount

若pQueueFamilyProperties为nullptr,则将队列族的数量返回到*pQueueFamilyPropertyCount,否则由*pQueueFamilyPropertyCount指定所需获取的VkQueueFamilyProperties的数量

VkQueueFamilyProperties* pQueueFamilyProperties

若pQueueFamilyProperties非nullptr,则将*pQueueFamilyPropertyCount个物理设备的VkQueueFamilyProperties返回到*pQueueFamilyProperties

于是填充graphicsBase::GetQueueFamilyIndices(...),依旧是换汤不换药的写法:

VkResult GetQueueFamilyIndices(VkPhysicalDevice physicalDevice, bool enableGraphicsQueue, bool enableComputeQueue, uint32_t (&queueFamilyIndices)[3]) {
    uint32_t queueFamilyCount = 0;
    vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, nullptr);
    if (!queueFamilyCount)
        return VK_RESULT_MAX_ENUM;
    std::vector<VkQueueFamilyProperties> queueFamilyPropertieses(queueFamilyCount);
    vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, queueFamilyPropertieses.data());
    /*待后续填充*/
}

VkQueueFamilyProperties::queueFlags与VkQueueFlagBits的枚举项做位与,即可确定队列族支持的操作类型。

于是遍历所有队列族,从中找出符合条件的队列族的索引。
遍历过程中将所找到的队列族索引存到queueFamilyIndices中(传入这个引用变量的原因见后文的DeterminePhysicalDevice(...)),仅在所需的队列族被全部找齐时,覆写graphicsBase成员变量中的queueFamilyIndex_graphicsqueueFamilyIndex_presentationqueueFamilyIndex_compute

auto& [ig, ip, ic] = queueFamilyIndices;//分别对应图形、呈现、计算
ig = ip = ic = VK_QUEUE_FAMILY_IGNORED;
//遍历所有队列族的索引
for (uint32_t i = 0; i < queueFamilyCount; i++) {
    /*待后续填充*/
}
//若任何需要被取得的队列族索引尚未被取得,则函数执行失败
if (ig == VK_QUEUE_FAMILY_IGNORED && enableGraphicsQueue ||
    ip == VK_QUEUE_FAMILY_IGNORED && surface ||
    ic == VK_QUEUE_FAMILY_IGNORED && enableComputeQueue)
    return VK_RESULT_MAX_ENUM;
//函数执行成功时,将所取得的队列族索引写入到成员变量
queueFamilyIndex_graphics = ig;
queueFamilyIndex_presentation = ip;
queueFamilyIndex_compute = ic;
return VK_SUCCESS;
  • 显然,用不着的队列族索引对应的成员变量会被覆写为VK_QUEUE_FAMILY_IGNORED

继续填充graphicsBase::GetQueueFamilyIndices(...),整个函数如下:

VkResult GetQueueFamilyIndices(VkPhysicalDevice physicalDevice, bool enableGraphicsQueue, bool enableComputeQueue, uint32_t (&queueFamilyIndices)[3]) {
    uint32_t queueFamilyCount = 0;
    vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, nullptr);
    if (!queueFamilyCount)
        return VK_RESULT_MAX_ENUM;
    std::vector<VkQueueFamilyProperties> queueFamilyPropertieses(queueFamilyCount);
    vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, queueFamilyPropertieses.data());
    auto& [ig, ip, ic] = queueFamilyIndices;
    ig = ip = ic = VK_QUEUE_FAMILY_IGNORED;
    for (uint32_t i = 0; i < queueFamilyCount; i++) {
        //这三个VkBool32变量指示是否可获取(指应该被获取且能获取)相应队列族索引
        VkBool32
            //只在enableGraphicsQueue为true时获取支持图形操作的队列族的索引
            supportGraphics = enableGraphicsQueue && queueFamilyPropertieses[i].queueFlags & VK_QUEUE_GRAPHICS_BIT,
            supportPresentation = false,
            //只在enableComputeQueue为true时获取支持计算的队列族的索引
            supportCompute = enableComputeQueue && queueFamilyPropertieses[i].queueFlags & VK_QUEUE_COMPUTE_BIT;
            //只在创建了window surface时获取支持呈现的队列族的索引
        if (surface)
            if (VkResult result = vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice, i, surface, &supportPresentation)) {
                std::cout << std::format("[ graphicsBase ] ERROR\nFailed to determine if the queue family supports presentation!\nError code: {}\n", int32_t(result));
                return result;
            }
        //若某队列族同时支持图形操作和计算
        if (supportGraphics && supportCompute) {
            //若需要呈现,最好是三个队列族索引全部相同
            if (supportPresentation) {
                ig = ip = ic = i;
                break;
            }
            //除非ig和ic都已取得且相同,否则将它们的值覆写为i,以确保两个队列族索引相同
            if (ig != ic ||
                ig == VK_QUEUE_FAMILY_IGNORED)
                ig = ic = i;
            //如果不需要呈现,那么已经可以break了
            if (!surface)
                break;
        }
        //若任何一个队列族索引可以被取得但尚未被取得,将其值覆写为i
        if (supportGraphics &&
            ig == VK_QUEUE_FAMILY_IGNORED)
            ig = i;
        if (supportPresentation &&
            ip == VK_QUEUE_FAMILY_IGNORED)
            ip = i;
        if (supportCompute &&
            ic == VK_QUEUE_FAMILY_IGNORED)
            ic = i;
    }
    if (ig == VK_QUEUE_FAMILY_IGNORED && enableGraphicsQueue ||
        ip == VK_QUEUE_FAMILY_IGNORED && surface ||
        ic == VK_QUEUE_FAMILY_IGNORED && enableComputeQueue)
        return VK_RESULT_MAX_ENUM;
    queueFamilyIndex_graphics = ig;
    queueFamilyIndex_presentation = ip;
    queueFamilyIndex_compute = ic;
    return VK_SUCCESS;
}
  • vkGetPhysicalDeviceSurfaceSupportKHR(...)查询队列族是否支持将图像呈现到该surface,各个参数含义应该一目了然,在此不做赘述。

  • 当图像或缓冲区要被另一个队列族的队列使用时,可能需要做资源的队列族所有权转移(所谓可能是因为并非所有硬件都有此要求),所以尽量使用相同的队列族来省去这一麻烦。而优先确保图形和计算的队列族相同,是因为无论程序多么复杂,资源的所有权在图形和呈现队列之间转移的情况至多只会在渲染循环中发生,但在图形和计算队列之间转移的情况可能发生在所有你想做间接渲染(此处姑且不解释)的场合,而且图形和计算队列不同的话,计算和图形命令也不得不在各自的命令缓冲区中完成(而不是在同一个命令缓冲区中),即,图形和计算的列族不同,比图形和呈现的队列族不同更费事。

(如果你觉得上述代码已经够啰嗦了的话,往下拉直到看见粗体字)
然后填充graphicsBase::DeterminePhysicalDevice(...),在该函数中对GetQueueFamilyIndices(...)做进一步包装,为每个物理设备保存一份所需队列族索引的组合:

VkResult DeterminePhysicalDevice(uint32_t deviceIndex = 0, bool enableGraphicsQueue = true, bool enableComputeQueue = true) {
    //定义一个特殊值用于标记一个队列族索引已被找过但未找到
    static constexpr uint32_t notFound = INT32_MAX;//== VK_QUEUE_FAMILY_IGNORED & INT32_MAX
    //定义队列族索引组合的结构体
    struct queueFamilyIndexCombination {
        uint32_t graphics = VK_QUEUE_FAMILY_IGNORED;
        uint32_t presentation = VK_QUEUE_FAMILY_IGNORED;
        uint32_t compute = VK_QUEUE_FAMILY_IGNORED;
    };
    //queueFamilyIndexCombinations用于为各个物理设备保存一份队列族索引组合
    static std::vector<queueFamilyIndexCombination> queueFamilyIndexCombinations(availablePhysicalDevices.size());
    auto& [ig, ip, ic] = queueFamilyIndexCombinations[deviceIndex];

    //如果有任何队列族索引已被找过但未找到,返回VK_RESULT_MAX_ENUM
    if (ig == notFound && enableGraphicsQueue ||
        ip == notFound && surface ||
        ic == notFound && enableComputeQueue)
        return VK_RESULT_MAX_ENUM;

    //如果有任何队列族索引应被获取但还未被找过
    if (ig == VK_QUEUE_FAMILY_IGNORED && enableGraphicsQueue ||
        ip == VK_QUEUE_FAMILY_IGNORED && surface ||
        ic == VK_QUEUE_FAMILY_IGNORED && enableComputeQueue) {
        uint32_t indices[3];
        VkResult result = GetQueueFamilyIndices(availablePhysicalDevices[deviceIndex], enableGraphicsQueue, enableComputeQueue, indices);
        //若GetQueueFamilyIndices(...)返回VK_SUCCESS或VK_RESULT_MAX_ENUM(vkGetPhysicalDeviceSurfaceSupportKHR(...)执行成功但没找齐所需队列族),
        //说明对所需队列族索引已有结论,保存结果到queueFamilyIndexCombinations[deviceIndex]中相应变量
        //应被获取的索引若仍为VK_QUEUE_FAMILY_IGNORED,说明未找到相应队列族,VK_QUEUE_FAMILY_IGNORED(~0u)与INT32_MAX做位与得到的数值等于notFound
        if (result == VK_SUCCESS ||
            result == VK_RESULT_MAX_ENUM) {
            if (enableGraphicsQueue)
                ig = indices[0] & INT32_MAX;
            if (surface)
                ip = indices[1] & INT32_MAX;
            if (enableComputeQueue)
                ic = indices[2] & INT32_MAX;
        }
        //如果GetQueueFamilyIndices(...)执行失败,return
        if (result)
            return result;
    }

    //若以上两个if分支皆不执行,则说明所需的队列族索引皆已被获取,从queueFamilyIndexCombinations[deviceIndex]中取得索引
    else {
        queueFamilyIndex_graphics = enableGraphicsQueue ? ig : VK_QUEUE_FAMILY_IGNORED;
        queueFamilyIndex_presentation = surface ? ip : VK_QUEUE_FAMILY_IGNORED;
        queueFamilyIndex_compute = enableComputeQueue ? ic : VK_QUEUE_FAMILY_IGNORED;
    }
    physicalDevice = availablePhysicalDevices[deviceIndex];
    return VK_SUCCESS;
}
  • 我打算之后在graphicsBase::CreateDevice(...)中会判断队列族索引是否为VK_QUEUE_FAMILY_IGNORED以确定是否要创建相应队列,因此这里应当将不需要的队列族索引覆写为VK_QUEUE_FAMILY_IGNORED

上述代码会这么啰嗦,是因为严格按照Vulkan标准而言,无法断言是否一定能找到一个同时支持图形、计算、呈现的队列族。
如果你程序的目标平台一定能跑DirectX12的图形程序(能装Win11的电脑皆在此列,嘛不是老古董电脑就没问题),那么你可以大胆假定一定会有这么一个队列族,以上两个函数就可以简化为:

VkResult GetQueueFamilyIndices(VkPhysicalDevice physicalDevice) {
    uint32_t queueFamilyCount = 0;
    vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, nullptr);
    if (!queueFamilyCount)
        return VK_RESULT_MAX_ENUM;
    std::vector<VkQueueFamilyProperties> queueFamilyPropertieses(queueFamilyCount);
    vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, queueFamilyPropertieses.data());
    for (uint32_t i = 0; i < queueFamilyCount; i++) {
        VkBool32
            supportGraphics = queueFamilyPropertieses[i].queueFlags & VK_QUEUE_GRAPHICS_BIT,
            supportPresentation = false,
            supportCompute = queueFamilyPropertieses[i].queueFlags & VK_QUEUE_COMPUTE_BIT;
        if (surface)
            if (VkResult result = vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice, i, surface, &supportPresentation)) {
                std::cout << std::format("[ graphicsBase ] ERROR\nFailed to determine if the queue family supports presentation!\nError code: {}\n", int32_t(result));
                return result;
            }
        if (supportGraphics && supportCompute &&
            (!surface || supportPresentation)) {//短路执行,需要呈现的时候才需要判断supportPresentation
            queueFamilyIndex_graphics = queueFamilyIndex_compute = i;
            if (surface)
                queueFamilyIndex_presentation = i;
            return VK_SUCCESS;
        }
    }
    return VK_RESULT_MAX_ENUM;
}
VkResult DeterminePhysicalDevice(uint32_t deviceIndex = 0) {
    //定义一个特殊值用于标记一个队列族索引已被找过但未找到
    static constexpr uint32_t notFound = INT32_MAX;//== VK_QUEUE_FAMILY_IGNORED & INT32_MAX
    //queueFamilyIndices用于为各个物理设备保存一份队列族索引
    static std::vector<uint32_t> queueFamilyIndices(availablePhysicalDevices.size());

    //如果队列族索引已被找过但未找到,返回VK_RESULT_MAX_ENUM
    if (queueFamilyIndices[deviceIndex] == notFound)
        return VK_RESULT_MAX_ENUM;

    //如果队列族索引应被获取但还未被找过
    if (queueFamilyIndices[deviceIndex] == VK_QUEUE_FAMILY_IGNORED) {
        VkResult result = GetQueueFamilyIndices(availablePhysicalDevices[deviceIndex]);
        //若GetQueueFamilyIndices(...)返回VK_SUCCESS或VK_RESULT_MAX_ENUM(vkGetPhysicalDeviceSurfaceSupportKHR(...)执行成功但没找齐所需队列族),
        //说明对所需队列族索引已有结论,保存结果到queueFamilyIndexCombinations[deviceIndex]中相应变量
        if (result) {
            if (result == VK_RESULT_MAX_ENUM)
                queueFamilyIndices[deviceIndex] = notFound;
            return result;
        }
        else
            queueFamilyIndices[deviceIndex] = queueFamilyIndex_graphics;
    }

    //若以上两个if分支皆不执行,则说明队列族索引已被获取,从queueFamilyIndices[deviceIndex]取得索引
    else {
        queueFamilyIndex_graphics = queueFamilyIndex_compute = queueFamilyIndices[deviceIndex];
        queueFamilyIndex_presentation = surface ? queueFamilyIndices[deviceIndex] : VK_QUEUE_FAMILY_IGNORED;
    }
    physicalDevice = availablePhysicalDevices[deviceIndex];
    return VK_SUCCESS;
}

创建逻辑设备

首先来看一下逻辑设备的创建信息:

struct VkDeviceCreateInfo 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkDeviceCreateFlags flags

uint32_t queueCreateInfoCount

队列的创建信息的个数

const VkDeviceQueueCreateInfo* pQueueCreateInfos

指向由队列的创建信息构成的数组

uint32_t enabledLayerCount

(已弃用,无视之)所需额外开启的设备级别层数

const char* const* ppEnabledLayerNames

(已弃用,无视之)指向由所需开启的层的名称构成的数组

uint32_t enabledExtensionCount

所需额外开启的设备级别扩展数

const char* const* ppEnabledExtensionNames

指向由所需开启的扩展的名称构成的数组(同一名称可以重复出现)

const VkPhysicalDeviceFeatures* pEnabledFeatures

指向一个VkPhysicalDeviceFeatures结构体,指明需要开启哪些特性

队列的创建信息:

struct VkDeviceQueueCreateInfo 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkDeviceQueueCreateFlags flags

uint32_t queueFamilyIndex

队列族索引

uint32_t queueCount

在该队列族索引下,所须创建的队列个数

const float* pQueuePriorities

指向浮点数的数组,用于指定各个队列的优先级,浮点数范围在0到1之间,1最大

  • 从Vulkan1.1开始可以将flags指定为VK_DEVICE_QUEUE_CREATE_PROTECTED_BIT,说明该队列受保护。虽然本套教程不会对Vulkan1.0以上版本的功能做过分展开,由于受保护内存(protected memory)相关的说明在官方标准中很常见(尽管通常不会被使用),这里还是做一下简单提要。Vulkan的设备内存对主机(指CPU一侧)可以是可见的,这就导致有能力的程序员可以通过一些手段,使得一个程序读写其他程序的设备内存,而受保护设备内存则被严格限制只能被设备写入,受保护队列则是能执行受保护操作的队列,这里头还有一些很麻烦的读写守则,这里不做展开,有兴趣请自行参考官方标准。受保护设备内存的主要应用场景是防止从内存中读取DRM内容(也就是防止盗版加密图像啦!DLsite和Fanza的新版电子书阅读器有这种特性),游戏程序员是用不着的。

于是乎,首先在graphicsBase::CreateDevice(...)中为所需的队列族填写相应的VkDeviceQueueCreateInfo,并记录所需队列的个数:

VkResult CreateDevice(VkDeviceCreateFlags flags = 0) {
    float queuePriority = 1.f;
    VkDeviceQueueCreateInfo queueCreateInfos[3] = {
        {
            .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
            .queueCount = 1,
            .pQueuePriorities = &queuePriority },
        {
            .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
            .queueCount = 1,
            .pQueuePriorities = &queuePriority },
        {
            .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
            .queueCount = 1,
            .pQueuePriorities = &queuePriority } };
    uint32_t queueCreateInfoCount = 0;
    if (queueFamilyIndex_graphics != VK_QUEUE_FAMILY_IGNORED)
        queueCreateInfos[queueCreateInfoCount++].queueFamilyIndex = queueFamilyIndex_graphics;
    if (queueFamilyIndex_presentation != VK_QUEUE_FAMILY_IGNORED &&
        queueFamilyIndex_presentation != queueFamilyIndex_graphics)
        queueCreateInfos[queueCreateInfoCount++].queueFamilyIndex = queueFamilyIndex_presentation;
    if (queueFamilyIndex_compute != VK_QUEUE_FAMILY_IGNORED &&
        queueFamilyIndex_compute != queueFamilyIndex_graphics &&
        queueFamilyIndex_compute != queueFamilyIndex_presentation)
        queueCreateInfos[queueCreateInfoCount++].queueFamilyIndex = queueFamilyIndex_compute;
    /*待后续填充*/
}
  • 因为优先级如何影响队列是实现特定的,出于保险,我对优先级一概使用了1.f。

  • 显然,以上代码说明创建信息的个数由所需队列的个数决定。

接着用vkGetPhysicalDeviceFeatures(...)获取物理设备特性:

VkPhysicalDeviceFeatures physicalDeviceFeatures;
vkGetPhysicalDeviceFeatures(physicalDevice, &physicalDeviceFeatures);

填写VkDeviceCreateInfo

VkDeviceCreateInfo deviceCreateInfo = {
    .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
    .flags = flags,
    .queueCreateInfoCount = queueCreateInfoCount,
    .pQueueCreateInfos = queueCreateInfos,
    .enabledExtensionCount = uint32_t(deviceExtensions.size()),
    .ppEnabledExtensionNames = deviceExtensions.data(),
    .pEnabledFeatures = &physicalDeviceFeatures
};
  • 这里不修改获取到的物理设备特性列表,即开启所有能开的特性,这并不会减慢程序初始化速度或在实时渲染中产生额外负担。

接着调用vkCreateDevice(...)来创建逻辑设备:

if (VkResult result = vkCreateDevice(physicalDevice, &deviceCreateInfo, nullptr, &device)) {
    std::cout << std::format("[ graphicsBase ] ERROR\nFailed to create a vulkan logical device!\nError code: {}\n", int32_t(result));
    return result;
}

接着用vkGetDeviceQueue(...)取得队列:

void VKAPI_CALL vkGetDeviceQueue(...) 的参数说明

VkDevice device

逻辑设备的handle

uint32_t queueFamilyIndex

队列族索引

uint32_t queueIndex

所需获取的队列在其所属队列族中的索引

VkQueue* pQueue

所接收队列的handle被写入*pQueue

由于之前在填写队列的创建信息时,指定了每个队列族中只创建一个队列,所以获取队列时,参数queueIndex为0:

if (queueFamilyIndex_graphics != VK_QUEUE_FAMILY_IGNORED)
    vkGetDeviceQueue(device, queueFamilyIndex_graphics, 0, &queue_graphics);
if (queueFamilyIndex_presentation != VK_QUEUE_FAMILY_IGNORED)
    vkGetDeviceQueue(device, queueFamilyIndex_presentation, 0, &queue_presentation);
if (queueFamilyIndex_compute != VK_QUEUE_FAMILY_IGNORED)
    vkGetDeviceQueue(device, queueFamilyIndex_compute, 0, &queue_compute);
  • 很显然,如果之前所取得的队列族索引queueFamilyIndex_graphicsqueueFamilyIndex_presentationqueueFamilyIndex_compute中有相同的,那么这里获取到的各个队列中也会有相同的。

成功创建逻辑设备后,已没有必要变更物理设备,于是用vkGetPhysicalDeviceProperties(...)获取物理设备属性,用vkGetPhysicalDeviceMemoryProperties(...)获取物理设备内存属性,整个graphicsBase::CreateDevice()函数如下:

VkResult CreateDevice(VkDeviceCreateFlags flags = 0) {
    float queuePriority = 1.f;
    VkDeviceQueueCreateInfo queueCreateInfos[3] = {
        {
            .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
            .queueCount = 1,
            .pQueuePriorities = &queuePriority },
        {
            .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
            .queueCount = 1,
            .pQueuePriorities = &queuePriority },
        {
            .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
            .queueCount = 1,
            .pQueuePriorities = &queuePriority } };
    uint32_t queueCreateInfoCount = 0;
    if (queueFamilyIndex_graphics != VK_QUEUE_FAMILY_IGNORED)
        queueCreateInfos[queueCreateInfoCount++].queueFamilyIndex = queueFamilyIndex_graphics;
    if (queueFamilyIndex_presentation != VK_QUEUE_FAMILY_IGNORED &&
        queueFamilyIndex_presentation != queueFamilyIndex_graphics)
        queueCreateInfos[queueCreateInfoCount++].queueFamilyIndex = queueFamilyIndex_presentation;
    if (queueFamilyIndex_compute != VK_QUEUE_FAMILY_IGNORED &&
        queueFamilyIndex_compute != queueFamilyIndex_graphics &&
        queueFamilyIndex_compute != queueFamilyIndex_presentation)
        queueCreateInfos[queueCreateInfoCount++].queueFamilyIndex = queueFamilyIndex_compute;
    VkPhysicalDeviceFeatures physicalDeviceFeatures;
    vkGetPhysicalDeviceFeatures(physicalDevice, &physicalDeviceFeatures);
    VkDeviceCreateInfo deviceCreateInfo = {
        .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
        .flags = flags,
        .queueCreateInfoCount = queueCreateInfoCount,
        .pQueueCreateInfos = queueCreateInfos,
        .enabledExtensionCount = uint32_t(deviceExtensions.size()),
        .ppEnabledExtensionNames = deviceExtensions.data(),
        .pEnabledFeatures = &physicalDeviceFeatures
    };
    if (VkResult result = vkCreateDevice(physicalDevice, &deviceCreateInfo, nullptr, &device)) {
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to create a vulkan logical device!\nError code: {}\n", int32_t(result));
        return result;
    }
    if (queueFamilyIndex_graphics != VK_QUEUE_FAMILY_IGNORED)
        vkGetDeviceQueue(device, queueFamilyIndex_graphics, 0, &queue_graphics);
    if (queueFamilyIndex_presentation != VK_QUEUE_FAMILY_IGNORED)
        vkGetDeviceQueue(device, queueFamilyIndex_presentation, 0, &queue_presentation);
    if (queueFamilyIndex_compute != VK_QUEUE_FAMILY_IGNORED)
        vkGetDeviceQueue(device, queueFamilyIndex_compute, 0, &queue_compute);
    vkGetPhysicalDeviceProperties(physicalDevice, &physicalDeviceProperties);
    vkGetPhysicalDeviceMemoryProperties(physicalDevice, &physicalDeviceMemoryProperties);
    //输出所用的物理设备名称
    std::cout << std::format("Renderer: {}\n", physicalDeviceProperties.deviceName);
    /*待Ch1-4填充*/
    return VK_SUCCESS;
}
  • physicalDeviceProperties.deviceName是物理设备的名称,这里将物理设备名称输出到控制台,以便之后测试。

汇总并测试

InitializeWindow(...)中按顺序调用本节中填充的函数:

bool InitializeWindow(VkExtent2D size, bool fullScreen = false, bool isResizable = true, bool limitFrameRate = true) {
    //using命名空间
    using namespace vulkan;

    if (!glfwInit()) {
        std::cout << std::format("[ InitializeWindow ] ERROR\nFailed to initialize GLFW!\n");
        return false;
    }
    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    glfwWindowHint(GLFW_RESIZABLE, isResizable);
    pMonitor = glfwGetPrimaryMonitor();
    const GLFWvidmode* pMode = glfwGetVideoMode(pMonitor);
    pWindow = fullScreen ?
        glfwCreateWindow(size.width, size.height, windowTitle, pMonitor, nullptr) :
        glfwCreateWindow(size.width, size.height, windowTitle, nullptr, nullptr);
    if (!pWindow) {
        std::cout << std::format("[ InitializeWindow ]\nFailed to create a glfw window!\n");
        glfwTerminate();
        return false;
    }

    //本节新增--------------------------------
#ifdef _WIN32
    graphicsBase::Base().AddInstanceExtension(VK_KHR_SURFACE_EXTENSION_NAME);
    graphicsBase::Base().AddInstanceExtension(VK_KHR_WIN32_SURFACE_EXTENSION_NAME);
#else
    uint32_t extensionCount = 0;
    const char** extensionNames;
    extensionNames = glfwGetRequiredInstanceExtensions(&extensionCount);
    if (!extensionNames) {
        std::cout << std::format("[ InitializeWindow ]\nVulkan is not available on this machine!\n");
        glfwTerminate();
        return false;
    }
    for (size_t i = 0; i < extensionCount; i++)
        graphicsBase::Base().AddInstanceExtension(extensionNames[i]);
#endif
    graphicsBase::Base().AddDeviceExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
    //在创建window surface前创建Vulkan实例
    graphicsBase::Base().UseLatestApiVersion()
    if (graphicsBase::Base().CreateInstance())
        return false;

    //创建window surface
    VkSurfaceKHR surface = VK_NULL_HANDLE;
    if (VkResult result = glfwCreateWindowSurface(graphicsBase::Base().Instance(), pWindow, nullptr, &surface)) {
        std::cout << std::format("[ InitializeWindow ] ERROR\nFailed to create a window surface!\nError code: {}\n", int32_t(result));
        glfwTerminate();
        return false;
    }
    graphicsBase::Base().Surface(surface);

    //通过用||操作符短路执行来省去几行
    if (//获取物理设备,并使用列表中的第一个物理设备,这里不考虑以下任意函数失败后更换物理设备的情况
        graphicsBase::Base().GetPhysicalDevices() ||
        //一个true一个false,暂时不需要计算用的队列
        graphicsBase::Base().DeterminePhysicalDevice(0, true, false) ||
        //创建逻辑设备
        graphicsBase::Base().CreateDevice())
        return false;
    //----------------------------------------

    /*待Ch1-4填充*/
    return true;
}
  • 显然,因为VK_SUCCESS为0,对多个返回VkResult的函数表达式做逻辑或意味着需要一路执行这堆表达式,尽管不够直观,(因为个人比较喜欢)这种利用短路执行的写法在本套教程中还会出现很多次。

主函数中不做改变,执行程序,如果控制台中成功输出了物理设备名称且没有错误信息,那么便是一切正常。