1 概述

前面一篇文章是Learn_Vulkan00_第一个三角形,渲染出来了第一个彩色三角形。学习新事物的是一个很枯燥的过程,所以我们应该每个阶段想办法获得一些成就感,比如上一篇的三角形,先用最快的方式得到效果,能看到效果就能给我们带来进一步的学习动力。

上一个案例中用到的对象我只是列了一个列表,简单描述了各个对象的作用。但描述还是较为简短,在进入更复杂的Vulkan程序之前,还是想把几个重要对象梳理一遍。

  • 层和扩展
  • 实例
  • 窗口表面
  • 物理设备和队列族
  • 逻辑设备和队列
  • 交换链

说明:本文中的代码就是上篇文章中的代码,编程环境是Windows,如果要在Linux/MacOS中运行,还需要修改配置文件。

2 对象描述

2.1 层和扩展

在Vulkan程序开发中,开发者可以查询当前环境下的Vulkan驱动有哪些扩展和功能,这些细节交给开发者,以实现程序最大优化。

层(Layer),如下图所示,在Vulkan API调用过程中,会检查已开启的层并将其注入到执行链中。这种调用方式类似于装饰器设计模式,Vulkan API最后通过驱动完成任务,也就是图中的ICD(不同厂商对图形API所做的驱动文件)。比如我们可以根据需求开启Validation Layer,在真正的函数调用前后截取一些信息,这样就可以实现我们的日志抓取。

图中虚线框标识不走该层。

image-20230615155051528

  • 扩展(extension),扩展提供给我们一些额外的功能或特性,在Vulkan API种,以KHR/EXT结尾的都是扩展。我们可以先查询支持的扩展列表,然后在创建实例或设备时添加我们需要的扩展。

在上一篇文章种,比如VkSurfaceKHR VkSwapchainKHRVkDebugReportCallbackEXT 就是扩展对象,因为在不同系统平台下,我们需要不同的Surface

VK_KHR_surface
VK_KHR_win32_surface

另外,在应用开发阶段我们可以开启验证层(Validation Layer),方便调试,然后在发布产品前将这些层关闭,防止生产环境下因验证而产生不必要的性能消耗。

使用

层和扩展的使用分为三个步骤:

  1. 维护我们要检查的层和扩展,我们准备开启最常用的验证层。
const char *validationInstanceLayers[] = { "VK_LAYER_KHRONOS_validation" };
  1. 使用vkEnumeratexxxLayerPropertiesvkEnumeratexxxExtensionProperties查询当前环境支持的层和扩展;
uint32_t availableLayerCount;
CALL_VK(vkEnumerateInstanceLayerProperties(&availableLayerCount, nullptr));
VkLayerProperties availableLayerProps[availableLayerCount];
CALL_VK(vkEnumerateInstanceLayerProperties(&availableLayerCount, availableLayerProps));
  1. 判断层/扩展,并将其赋值给实例或设备的CreateInfo对象中。
for(uint32_t i = 0; i < ARRAY_SIZE(validationInstanceLayers); i++){
    bool isFound = false;
    for(uint32_t j = 0; j < availableLayerCount; j++){
        if (strcmp(validationInstanceLayers[i], availableLayerProps[j].layerName) == 0 ) {
            isFound = true;
            break;
        }
    }

    if(!isFound){
        Error("Can not find %s instance layer!", validationInstanceLayers[i]);
        return;
    }
}

在赋值时,无论实例还是设备,在CreateInfo对象中都有enabledLayerCountppEnabledLayerNamesenabledExtensionCountppEnabledExtensionNames四个属性,将我们最终需要的层和扩展赋值即可。

2.2 实例、窗口表面、设备和队列

下图是实例、物理设备(一般就是GPU)、逻辑设备、队列族和队列之间的关系。

image-20230615163929731

简要描述:

  1. 使用Vulkan渲染,我们需要先查询当前环境下支持的层和扩展,对应我们需要哪些额外功能,然后将层/扩展名传递给Vulkan API完成实例创建。接着创建一个实例(VkInstance),每个应用程序应只有这么一个实例,接下来的所有Vulkan API调用都基于该实例。

  2. 完成实例创建后,我们应根据不同系统平台创建一个窗口表面(Surface),以建立应用程序和显示之间的桥梁,这一步骤针对每个平台会有点不一致,具体在第3节详细描述。

  3. 接下来,我们需要选择一个物理设备或者数个物理设备使用。那我们怎么选呢?需要哪些条件得到最佳的物理设备?其实可以从以下几个方面考虑:

  • 这个物理设备是否支持步骤2创建的Surface,这个比较重要,如果不支持我们的Surface,那接下来渲染都会受到影响。
  • 这个物理设备队列族是否支持图形渲染管线或者计算渲染管线?这个需要根据我们后续的渲染管线配置来选择。
  • 检查物理设备的描述符(后续有案例,也就是Uniform变量)范围、最大的顶点属性参数量、最大视图数量、最大颜色附件数量等等属性。
  • 还可以检查物理设备的内存参数,比如内存类型数量、内存大小等。

所以根据具体的需求,可以多个维度检查物理设备,最终得到最适合我们应用的物理设备就是好设备~

这样的话,也就符合我们上图中关于物理设备的描述了(图中右半部分),我们环境中可能会有多个物理设备,每个物理设备可能有多个队列族,每种队列族中有多个队列

  1. 物理设备选好后,我们还可以根据实际情况检查设备的层和扩展。

  2. 我们在步骤3中已经有队列族索引了,这里我们再配置好队列数量,然后给到创建逻辑设备的API就可以。所以这里不一样的是,队列是不单独创建的,而是在创建逻辑设备是传入,然后逻辑设备创建完成后才可以取到具体队列。

2.2.1 创建实例

  1. 构建实例级别的层和扩展。
  2. 构建VkInstanceCreateInfo对象。

ApplicationInfo中。

typedef struct VkApplicationInfo {
    VkStructureType    sType;
    const void*        pNext;
    const char*        pApplicationName;
    uint32_t           applicationVersion;
    const char*        pEngineName;
    uint32_t           engineVersion;
    uint32_t           apiVersion;
} VkApplicationInfo;

//use
VkApplicationInfo application = {
        .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
        .pNext = nullptr,
        .pApplicationName = APP_NAME,
        .applicationVersion = VK_MAKE_VERSION(1, 0, 0),
        .pEngineName = APP_NAME,
        .engineVersion = VK_MAKE_VERSION(1, 0, 0),
        .apiVersion = VK_VERSION_1_0
};
  • sType,Vulkan的结构体枚举类型,基本上都是" VK_STRUCTURE_TYPE"+XXX+“INFO”。

  • pNext,在不同对象中使用会不太一样,它指向的是一个链式结构。

  • flags,一个类型标识。

以上三个参数在很多地方都会看到。

  • pApplicationNameapplicationVersion是配置应用程序的信息,这部分自由指定即可,版本号可以用VK_MAKE_VERSION()完成构建。

  • pEngineNameengineVersion是配置引擎信息,这部分自由指定即可。

  • apiVersion是Vulkan API的版本号,可选值都是在vulkan_core.h中定义的宏,都是VK_VERSION开头。

接下来就是VkInstanceCreateInfo

typedef struct VkInstanceCreateInfo {
    VkStructureType             sType;
    const void*                 pNext;
    VkInstanceCreateFlags       flags;
    const VkApplicationInfo*    pApplicationInfo;
    uint32_t                    enabledLayerCount;
    const char* const*          ppEnabledLayerNames;
    uint32_t                    enabledExtensionCount;
    const char* const*          ppEnabledExtensionNames;
} VkInstanceCreateInfo;

对于VkInstanceCreateInfo,我们会发现其实核心就是三个:应用程序信息、层和扩展。我们把应用程序信息指针赋值给它,层和合成信息则提供数量和名称即可。

这里需要注意的是,我们开启了VK_LAYER_KHRONOS_validation验证层后,这里还需要配置VkDebugReportCallbackCreateInfoEXT对象,完成错误回调配置。

VkBool32 debugReportCallback(VkDebugReportFlagsEXT msgFlags, VkDebugReportObjectTypeEXT objType, uint64_t srcObject,
                             size_t location, int32_t msgCode, const char *pLayerPrefix, const char *pMsg, void *pUserData){
    if(msgFlags == VK_DEBUG_REPORT_ERROR_BIT_EXT){
        Error("%s: [%s] Code %d : %s", "Error", pLayerPrefix, msgCode, pMsg);
    } else if(msgFlags == VK_DEBUG_REPORT_WARNING_BIT_EXT) {
        Warn("%s: [%s] Code %d : %s", "Warning", pLayerPrefix, msgCode, pMsg);
    } else {
        Print("%s: [%s] Code %d : %s", "Info", pLayerPrefix, msgCode, pMsg);
    }
    return VK_FALSE;
}
if(bValidate){
    VkDebugReportCallbackCreateInfoEXT debugReport = {
            .sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT,
            .pNext = nullptr,
            .flags = VK_DEBUG_REPORT_ERROR_BIT_EXT |				//错误信息
                     VK_DEBUG_REPORT_WARNING_BIT_EXT |				//警告信息
                     VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT,	//性能警告信息
            .pfnCallback = (PFN_vkDebugReportCallbackEXT) debugReportCallback,
            .pUserData = nullptr,
    };

    createInfo.pNext = &debugReport;
}

//use
VkInstanceCreateInfo createInfo = {
        .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
        .pNext = nullptr,
        .flags = 0,
        .pApplicationInfo = &application,
        .enabledLayerCount = enabledLayerCount,
        .ppEnabledLayerNames = enabledLayerCount == 0 ? nullptr : enabledLayerNames,
        .enabledExtensionCount = enableExtensionCount,
        .ppEnabledExtensionNames = enableExtensionCount == 0 ? nullptr : enableExtensionNames
};
CALL_VK(vkCreateInstance(&createInfo, VK_ALLOC, out_instance));

最后调用vkCreateInstance()完成实例创建。

2.2.2 创建窗口表面Surface

还记得上文提到的VK_KHR_surfaceVK_KHR_win32_surface这两个扩展吗?因为不是所有的图形卡都支持窗口显示的,比如服务器。所以surface属于一个扩展。

因为Vulkan是平台无关性API,并不是将渲染结果直接呈现给屏幕,而是需要Surface来作为Vulkan和窗体之间的连接(也称为窗口集成WSI)。在Windows平台上,如果我们使用GLFW作为窗口系统,会省下很多事,因为GLFW也支持Windows、Linux和Macos,而且集成了Vulkan中的Surface创建流程。所以可以利用glfwCreateWindowSurface完成Surface创建:

//TODO GLFW的一些初始化逻辑
...
...
window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, APP_NAME, nullptr, nullptr);
if(!window){
    throw std::runtime_error("Can not create glfw window");
}

Print("GLFW Vulkan Support: %d", glfwVulkanSupported());
glfwMakeContextCurrent(window);

if(glfwCreateWindowSurface(vk.instance, window, VK_ALLOC, &vk.surface) != VK_SUCCESS){
    throw std::runtime_error("Can not creat surface.");
}

GLFW帮助我们处理了很多平台差异性,比如在Windows平台需要使用vkCreateWin32SurfaceKHR()来完成窗口创建。

// Windows
typedef struct VkWin32SurfaceCreateInfoKHR {
    VkStructureType                 sType;
    const void*                     pNext;
    VkWin32SurfaceCreateFlagsKHR    flags;
    HINSTANCE                       hinstance;
    HWND                            hwnd;
} VkWin32SurfaceCreateInfoKHR;

可以看到,这里就是需要我们传入Windows窗口的HWNDHINSTANCE等参数。

类似的,Linux中,如果是X11界面窗口系统,则是通过vkCreateXcbSurfaceKHR()完成了Surface创建。

注意,如果要在Linux或者其他环境运行,还需要更改宏定义、扩展配置等。

//Linux Xcb
typedef struct VkXcbSurfaceCreateInfoKHR {
    VkStructureType               sType;
    const void*                   pNext;
    VkXcbSurfaceCreateFlagsKHR    flags;
    xcb_connection_t*             connection;
    xcb_window_t                  window;
} VkXcbSurfaceCreateInfoKHR;

那在安卓系统中呢?

VkAndroidSurfaceCreateInfoKHR createInfo {
    .sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR,
    .pNext = nullptr,
    .flags = 0,
    .window = app->nativeWindow   // ====> 核心就是这个参数。
};
CALL_VK(vkCreateAndroidSurfaceKHR(vkInstance, &createInfo, nullptr, &vkSurface));

2.2.3 选取物理设备

在使用的时候分为以下步骤:

  1. 查出来所有的物理设备;
  2. 查询每个设备的基本属性;
  3. 查询每个设备的队列族;
  4. 查询指定设备的内存属性。

对于查询所有物理设备,我们应该比较熟悉了,接下来就是查询每个物理设备属性。

uint32_t physicalDevCount;
CALL_VK(vkEnumeratePhysicalDevices(instance, &physicalDevCount, nullptr));
VkPhysicalDevice physicalDevs[physicalDevCount];
CALL_VK(vkEnumeratePhysicalDevices(instance, &physicalDevCount, physicalDevs));

//选取的物理设备和队列索引
VkPhysicalDevice selectedPhysicalDev = nullptr;
uint16_t workQueueIndex;
uint16_t presentQueueIndex;
for(int i = 0; i < physicalDevCount; i++){
    VkPhysicalDeviceProperties devProps;
    vkGetPhysicalDeviceProperties(physicalDevs[i], &devProps);

    ...
}

物理设备属性中,我们可以分析下每个字段。

typedef struct VkPhysicalDeviceProperties {
    uint32_t                            apiVersion;
    uint32_t                            driverVersion;
    uint32_t                            vendorID;
    uint32_t                            deviceID;
    VkPhysicalDeviceType                deviceType;
    char                                deviceName[VK_MAX_PHYSICAL_DEVICE_NAME_SIZE];
    uint8_t                             pipelineCacheUUID[VK_UUID_SIZE];
    VkPhysicalDeviceLimits              limits;
    VkPhysicalDeviceSparseProperties    sparseProperties;
} VkPhysicalDeviceProperties;
  • apiVersion 物理设备API的版本号。

    可以看到,Vulkan中如果要设置版本号,就用VK_MAKE_VERSION()这个宏,如果要解析版本号,就用 VK_VERSION_MAJOR()VK_VERSION_MINOR()VK_VERSION_PATCH()这三个宏。

  • driverVersion 驱动版本号。

  • vendorID 供应商ID标识。

  • deviceID 设备ID标识。

  • deviceType 可能是以下几种。

    • VK_PHYSICAL_DEVICE_TYPE_CPU CPU设备。
    • VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU 集成显卡。
    • VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU 独立显卡。
    • VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU 虚拟显卡。
  • deviceName 设备名称。

  • pipelineCacheUUID pipeline cache唯一标识。

  • limits 设备的限制属性,比如支持的最大贴图数量、最大描述符范围等等。

  • sparseProperties 设备的离散绑定信息。

以下是我的环境输出,可以看到有两个物理设备,一个是集显,一个是独显。

---------------------------------
Physical Devices:
---------------------------------
Device Name          : Intel(R) Iris(R) Xe Graphics
Device Type          : integrated GPU
Vendor ID            : 0x8086
Device ID            : 0x9A49
Driver Version       : 0.405.50
API Version          : 1.3.238
---------------------------------
Device Name          : NVIDIA GeForce MX450
Device Type          : discrete GPU
Vendor ID            : 0x10DE
Device ID            : 0x1F97
Driver Version       : 516.216.0
API Version          : 1.3.205

判断物理设备是否支持当前窗口表面Surface。

VkBool32 surfaceSupport;
vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevs[i], i, surface, &surfaceSupport);
Print("Surface Support: %d", surfaceSupport);

该API直接返回true/false。

查询队列族。

uint32_t queueFamilyPropCount;
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevs[i], &queueFamilyPropCount, nullptr);
VkQueueFamilyProperties queueFamilyProps[queueFamilyPropCount];
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevs[i], &queueFamilyPropCount, queueFamilyProps);

获得的VkQueueFamilyProperties数据结构如下:

typedef struct VkQueueFamilyProperties {
    VkQueueFlags    queueFlags;
    uint32_t        queueCount;
    uint32_t        timestampValidBits;
    VkExtent3D      minImageTransferGranularity;
} VkQueueFamilyProperties;

这里主要识别队列类型的就是VkQueueFlags,我们可以发现有以下这几种队列族类型。

typedef enum VkQueueFlagBits {
    VK_QUEUE_GRAPHICS_BIT = 0x00000001,
    VK_QUEUE_COMPUTE_BIT = 0x00000002,
    VK_QUEUE_TRANSFER_BIT = 0x00000004,
    VK_QUEUE_SPARSE_BINDING_BIT = 0x00000008,
    VK_QUEUE_PROTECTED_BIT = 0x00000010,
    VK_QUEUE_VIDEO_DECODE_BIT_KHR = 0x00000020,
#ifdef VK_ENABLE_BETA_EXTENSIONS
    VK_QUEUE_VIDEO_ENCODE_BIT_KHR = 0x00000040,
#endif
    VK_QUEUE_OPTICAL_FLOW_BIT_NV = 0x00000100,
    VK_QUEUE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkQueueFlagBits;
  • GRAPHICS 支持图形管线;
  • COMPUTE 支持计算管线;
  • TRANSFER 用以传输,例如复制缓冲区和图像内容;
  • SPARSE_BINDING离散绑定,用作离散资源时(暂时没用到过,以后补充);

queueCount是当前队列族内的队列数量。

timestampValidBits表示当从这个队列里面获取时间戳时,多少位有效。如果这个值为0,表示不支持时间戳。如果不是0,保证至少32位。

于是我们就可以用下面的方法判断是否是我们想要的队列族类型。

if(queueFamilyProps[j].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
    selectedPhysicalDev = physicalDevs[i];
    workQueueIndex = j;
    presentQueueIndex = j;
    break;
}

接下来,在后续申请Buffer或者复制时会用到物理设备的内存类型,可以通过vkGetPhysicalDeviceMemoryProperties()获得。该函数返回以下结构体。

typedef struct VkPhysicalDeviceMemoryProperties {
    uint32_t        memoryTypeCount;
    VkMemoryType    memoryTypes[VK_MAX_MEMORY_TYPES];
    uint32_t        memoryHeapCount;
    VkMemoryHeap    memoryHeaps[VK_MAX_MEMORY_HEAPS];
} VkPhysicalDeviceMemoryProperties;

从结构体我们可以得到该设备的内存类型和内存大小。

2.2.4 创建逻辑设备和获取队列

我们在上文已经知道,层和扩展在实例和设备级别都可以配置。所以我们可以跟创建实例时一致,先查询目前环境支持的层和扩展列表。对设备级别层面,我们用到比较多的扩展就是交换链、几何着色器支持等。

所以我们先建立好要判断的扩展列表。

const DriverFeature validationDeviceExtensions[] = {
        {VK_KHR_SWAPCHAIN_EXTENSION_NAME, false, true}		// 这个宏也就是:VK_KHR_swapchain
};

然后判断是否支持。

uint32_t availableDeviceExtensionCount;
CALL_VK(vkEnumerateDeviceExtensionProperties(out_deviceInfo->physicalDev, nullptr, &availableDeviceExtensionCount, nullptr));
VkExtensionProperties availableDeviceExtensions[availableDeviceExtensionCount];
CALL_VK(vkEnumerateDeviceExtensionProperties(out_deviceInfo->physicalDev, nullptr, &availableDeviceExtensionCount, availableDeviceExtensions));
const char *enableDeviceExtensions[32];
uint32_t enableDeviceExtensionCount;
checkFeatures("Device Extensions", true, true, validationDeviceExtensions, ARRAY_SIZE(validationDeviceExtensions), availableDeviceExtensions, availableDeviceExtensionCount, enableDeviceExtensions, &enableDeviceExtensionCount);

接下来我们需要配置逻辑设备的队列信息,也就是VkDeviceQueueCreateInfo对象。

typedef struct VkDeviceQueueCreateInfo {
    VkStructureType             sType;
    const void*                 pNext;
    VkDeviceQueueCreateFlags    flags;
    uint32_t                    queueFamilyIndex;
    uint32_t                    queueCount;
    const float*                pQueuePriorities;
} VkDeviceQueueCreateInfo;

//use
float queuePriority = 1.0f;
VkDeviceQueueCreateInfo queueCreateInfo = {
        .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
        .pNext = nullptr,
        .flags = 0,
        .queueFamilyIndex = out_queueInfo->workQueueIndex,
        .queueCount = 1,
        .pQueuePriorities = &queuePriority, //队列优先级
};
  • queueFamilyIndex 队列族索引,实在选取物理设备时记录下来的队列族索引。
  • queueCount 队列数量,也就是跟逻辑设备关联创建的队列数量。这里在后续获取队列时需要用到。
  • pQueuePriorities 队列优先级,当创建多个队列时,指定各队列的优先级(范围0.0~1.0)。

接下来就是构造逻辑设备创建要用的CreateInfo

typedef struct VkDeviceCreateInfo {
    VkStructureType                    sType;
    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;
} VkDeviceCreateInfo;

//use
VkDeviceCreateInfo deviceCreateInfo = {
        .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
        .pNext = nullptr,
        .flags = 0,
        .queueCreateInfoCount = 1,
        .pQueueCreateInfos = &queueCreateInfo,
        .enabledLayerCount = 0,
        .ppEnabledLayerNames = nullptr,
        .enabledExtensionCount = enableDeviceExtensionCount,
        .ppEnabledExtensionNames = enableDeviceExtensionCount == 0 ? nullptr : enableDeviceExtensions
};
  • queueCreateInfoCount 配置的VkDeviceQueueCreateInfo有多少个,注意不是队列数量。我们这里是一个。
  • pQueueCreateInfos 具体的队列配置信息。
  • enabledLayerCountppEnabledLayerNamesenabledExtensionCountppEnabledExtensionNames还是我们熟悉的层/扩展配置。
  • pEnabledFeatures 启用/停用物理设备中功能,这个需要根据具体程序要求配置。

接着我们可以把逻辑设备中刚刚创建的队列指针拿出来,后续提交命令缓冲直接使用即可。

vkGetDeviceQueue(out_deviceInfo->device, out_queueInfo->workQueueIndex, 0, &out_queueInfo->queue);
VKAPI_ATTR void VKAPI_CALL vkGetDeviceQueue(
    VkDevice                                    device,
    uint32_t                                    queueFamilyIndex,
    uint32_t                                    queueIndex,
    VkQueue*                                    pQueue);

在这里我们需要一个queueIndex参数,这就是为什么上文要注意队列数量的原因。

2.3 交换链

交换链是渲染结果和窗口之间的传输媒介,应用程序为从交换链中获取图像进行绘制,然后将其返回给交换链,等待屏幕显示完成。

image-20230615170514402

如图,我们在创建交换链的时候需要指定Surface,以及交换链内图像的大小、格式、用途等参数。创建完交换链后,渲染时由逻辑设备取得交换链内的图像,执行渲染后返回给交换链,最终完成提交显示。

2.3.1 表面参数适配

首先我们看一下交换链的创建流程,为了得到适配参数,我们需要通过Surface查询图片的格式、颜色空间、呈现模式等参数,为了方便,我这里将其称为SwapchainParam

struct SwapchainParam{
    VkSurfaceCapabilitiesKHR capabilities;
    VkSurfaceFormatKHR format;
    VkPresentModeKHR presentMode;
};
  • capabilities 表面支持能力,包含最小图片数量(minImageCount)、最大图片数量(maxImageCount)、当前尺寸范围(currentExtent)、最小尺寸范围(minImageExtent)、最大尺寸范围(maxImageExtent)等参数。
  • format 表面格式,包含格式和色彩空间两个参数。通过VkFormat枚举来看,Vulkan内定义的格式非常繁多,不过很多是RGB颜色的色彩通道和类型,比如VK_FORMAT_B8G8R8A8_SRGBVK_FORMAT_R8G8B8_SRGB
  • presentMode 呈现模式,该参数对于交换链是非常重要的,其指定了呈现到屏幕上的条件。有以下几种类型:
    • VK_PRESENT_MODE_IMMEDIATE_KHR 应用程序提交的图像会被立即呈现到屏幕,因为没有缓冲和等待的概念,该模式可能会导致撕裂(不过可以配合Vsync信号来解决这个问题)。
    • VK_PRESENT_MODE_FIFO_KHR 交换链被看作是一个队列,当屏幕刷新时,会从队列前面中获取图像。并且应用程序渲染完成的图像,会被放到队列后面,等待渲染。如果队列满了的话,应用程序需要等待。这种模式和游戏中的垂直同步机制类似。
    • VK_PRESENT_MODE_FIFO_RELAXED_KHR 与第二种模式略微不同,如果应用程序没有及时渲染图像,即队列空了,此时不会等待下一个Vsync信号,而是直接传输图像,这样可能导致撕裂。
    • VK_PRESENT_MODE_MAILBOX_KHR 该模式是第二种模式的变种,在队列满了的情况下,会选择旧的图像代替新的图像,从而达到不阻塞应用程序的作用。这种模式经常用作三级缓冲,和标准的垂直同步双缓冲相比,它可以有效避免延迟带来的撕裂效果。

我们一般优先选择VK_PRESENT_MODE_MAILBOX_KHRVK_PRESENT_MODE_FIFO_KHR模式,但现在很多驱动还未支持这些模式,这种情况就只能使用VK_PRESENT_MODE_IMMEDIATE_KHR模式了。

了解完上边的这些参数之后,我们实现一个函数querySwapchainParam(),用以查询最适配的表面参数。

void querySwapchainParam(VkPhysicalDevice physicalDev, VkSurfaceKHR surface, SwapchainParam *out_swapchainParam){
 	   
}
  1. 查询表面支持vkGetPhysicalDeviceSurfaceCapabilitiesKHR
//capabilities
CALL_VK(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDev, surface, &out_swapchainParam->capabilities));
  1. 选择最佳的表面格式vkGetPhysicalDeviceSurfaceFormatsKHR

我们这里尽可能选择VK_FORMAT_B8G8R8A8_UNORM/VK_COLORSPACE_SRGB_NONLINEAR_KHR,如果表面格式是VK_FORMAT_UNDEFINED,则表示我们可以自定义,所以直接返回我们想要的格式即可。最后如果实在找不到我们想要的格式,就返回某一个支持格式。

uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDev, surface, &formatCount, nullptr);
VkSurfaceFormatKHR formats[formatCount];
vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDev, surface, &formatCount, formats);

 bool bFindFormat = false;
if(formatCount == 1 && formats[0].format == VK_FORMAT_UNDEFINED){
    out_swapchainParam->format = {VK_FORMAT_B8G8R8A8_UNORM, VK_COLORSPACE_SRGB_NONLINEAR_KHR};
    bFindFormat = true;
}  else {
    for (const auto &item: formats) {
        if(item.format == VK_FORMAT_B8G8R8A8_UNORM && item.colorSpace == VK_COLORSPACE_SRGB_NONLINEAR_KHR){
            out_swapchainParam->format = item;
            bFindFormat = true;
            break;
        }
    }
}
if(!bFindFormat){
    out_swapchainParam->format = formats[0];
}
  1. 选择最佳的呈现模式vkGetPhysicalDeviceSurfacePresentModesKHR

类似的,如果支持显示模式VK_PRESENT_MODE_MAILBOX_KHR,我们优先使用,不行就VK_PRESENT_MODE_FIFO_KHR,如果都不支持这两种格式,则使用VK_PRESENT_MODE_IMMEDIATE_KHR

uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDev, surface, &presentModeCount, nullptr);
VkPresentModeKHR presentModes[presentModeCount];
vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDev, surface, &presentModeCount, presentModes);

bool bFoundPrentMode = false;
for (const auto &item: presentModes) {
    if(VK_PRESENT_MODE_MAILBOX_KHR == item){
        out_swapchainParam->presentMode = item;
        bFoundPrentMode = true;
    } else if(VK_PRESENT_MODE_FIFO_KHR == item){
        out_swapchainParam->presentMode = item;
        bFoundPrentMode = true;
    }
}
if(!bFoundPrentMode){
    out_swapchainParam->presentMode = VK_PRESENT_MODE_IMMEDIATE_KHR;
}

3 总结

本文描述了使用Vulkan渲染中需要配置的几个重要对象。因为Vulkan的很多细节交给了开发者,所以在编写Vulkan程序时,会发现很多工作都是在各种配置。除了本文提到外,还有一些重要的对象,比如渲染流程、渲染管线、帧缓冲、命令缓冲和同步对象等,这些在具体使用了再详细描述。

4 参考

Vulkan Tutorial: https://vulkan-tutorial.com

Vulkan-Guide: https://github.com/KhronosGroup/Vulkan-Guide