Learn_Vulkan01_重要对象浅析
前面一篇文章是Learn_Vulkan00_第一个三角形,渲染出来了第一个彩色三角形。学习新事物的是一个很枯燥的过程,所以我们应该每个阶段想办法获得一些成就感,比如上一篇的三角形,先用最快的方式得到效果,能看到效果就能给我们带来进一步的学习动力。
1 概述
上一个案例中用到的对象我只是列了一个列表,简单描述了各个对象的作用。但描述还是较为简短,在进入更复杂的Vulkan程序之前,还是想把几个重要对象梳理一遍。
- 层和扩展
- 实例
- 窗口表面
- 物理设备和队列族
- 逻辑设备和队列
- 交换链
说明:本文中的代码就是上篇文章中的代码,编程环境是Windows,如果要在Linux/MacOS中运行,还需要修改配置文件。
2 对象描述
2.1 层和扩展
在Vulkan程序开发中,开发者可以查询当前环境下的Vulkan驱动有哪些扩展和功能,这些细节交给开发者,以实现程序最大优化。
层(Layer
),如下图所示,在Vulkan API调用过程中,会检查已开启的层并将其注入到执行链中。这种调用方式类似于装饰器设计模式,Vulkan API最后通过驱动完成任务,也就是图中的ICD(不同厂商对图形API所做的驱动文件)。比如我们可以根据需求开启Validation Layer,在真正的函数调用前后截取一些信息,这样就可以实现我们的日志抓取。
图中虚线框标识不走该层。
- 扩展(
extension
),扩展提供给我们一些额外的功能或特性,在Vulkan API种,以KHR/EXT
结尾的都是扩展。我们可以先查询支持的扩展列表,然后在创建实例或设备时添加我们需要的扩展。
在上一篇文章种,比如VkSurfaceKHR
、VkSwapchainKHR
和VkDebugReportCallbackEXT
就是扩展对象,因为在不同系统平台下,我们需要不同的Surface
。
VK_KHR_surface
VK_KHR_win32_surface
另外,在应用开发阶段我们可以开启验证层(Validation Layer),方便调试,然后在发布产品前将这些层关闭,防止生产环境下因验证而产生不必要的性能消耗。
使用
层和扩展的使用分为三个步骤:
- 维护我们要检查的层和扩展,我们准备开启最常用的验证层。
const char *validationInstanceLayers[] = { "VK_LAYER_KHRONOS_validation" };
- 使用
vkEnumeratexxxLayerProperties
或vkEnumeratexxxExtensionProperties
查询当前环境支持的层和扩展;
uint32_t availableLayerCount;
CALL_VK(vkEnumerateInstanceLayerProperties(&availableLayerCount, nullptr));
VkLayerProperties availableLayerProps[availableLayerCount];
CALL_VK(vkEnumerateInstanceLayerProperties(&availableLayerCount, availableLayerProps));
- 判断层/扩展,并将其赋值给实例或设备的
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
对象中都有enabledLayerCount
、ppEnabledLayerNames
、enabledExtensionCount
和ppEnabledExtensionNames
四个属性,将我们最终需要的层和扩展赋值即可。
2.2 实例、窗口表面、设备和队列
下图是实例、物理设备(一般就是GPU)、逻辑设备、队列族和队列之间的关系。
简要描述:
-
使用Vulkan渲染,我们需要先查询当前环境下支持的层和扩展,对应我们需要哪些额外功能,然后将层/扩展名传递给Vulkan API完成实例创建。接着创建一个实例(VkInstance),每个应用程序应只有这么一个实例,接下来的所有Vulkan API调用都基于该实例。
-
完成实例创建后,我们应根据不同系统平台创建一个窗口表面(Surface),以建立应用程序和显示之间的桥梁,这一步骤针对每个平台会有点不一致,具体在第3节详细描述。
-
接下来,我们需要选择一个物理设备或者数个物理设备使用。那我们怎么选呢?需要哪些条件得到最佳的物理设备?其实可以从以下几个方面考虑:
- 这个物理设备是否支持步骤2创建的Surface,这个比较重要,如果不支持我们的Surface,那接下来渲染都会受到影响。
- 这个物理设备队列族是否支持图形渲染管线或者计算渲染管线?这个需要根据我们后续的渲染管线配置来选择。
- 检查物理设备的描述符(后续有案例,也就是Uniform变量)范围、最大的顶点属性参数量、最大视图数量、最大颜色附件数量等等属性。
- 还可以检查物理设备的内存参数,比如内存类型数量、内存大小等。
所以根据具体的需求,可以多个维度检查物理设备,最终得到最适合我们应用的物理设备就是好设备~
这样的话,也就符合我们上图中关于物理设备的描述了(图中右半部分),我们环境中可能会有多个物理设备,每个物理设备可能有多个队列族,每种队列族中有多个队列。
-
物理设备选好后,我们还可以根据实际情况检查设备的层和扩展。
-
我们在步骤3中已经有队列族索引了,这里我们再配置好队列数量,然后给到创建逻辑设备的API就可以。所以这里不一样的是,队列是不单独创建的,而是在创建逻辑设备是传入,然后逻辑设备创建完成后才可以取到具体队列。
2.2.1 创建实例
- 构建实例级别的层和扩展。
- 构建
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
,一个类型标识。
以上三个参数在很多地方都会看到。
-
pApplicationName
和applicationVersion
是配置应用程序的信息,这部分自由指定即可,版本号可以用VK_MAKE_VERSION()
完成构建。 -
pEngineName
和engineVersion
是配置引擎信息,这部分自由指定即可。 -
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_surface
、VK_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窗口的HWND
、HINSTANCE
等参数。
类似的,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 选取物理设备
在使用的时候分为以下步骤:
- 查出来所有的物理设备;
- 查询每个设备的基本属性;
- 查询每个设备的队列族;
- 查询指定设备的内存属性。
对于查询所有物理设备,我们应该比较熟悉了,接下来就是查询每个物理设备属性。
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
具体的队列配置信息。enabledLayerCount
、ppEnabledLayerNames
、enabledExtensionCount
和ppEnabledExtensionNames
还是我们熟悉的层/扩展配置。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 交换链
交换链是渲染结果和窗口之间的传输媒介,应用程序为从交换链中获取图像进行绘制,然后将其返回给交换链,等待屏幕显示完成。
如图,我们在创建交换链的时候需要指定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_SRGB
、VK_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_KHR
或VK_PRESENT_MODE_FIFO_KHR
模式,但现在很多驱动还未支持这些模式,这种情况就只能使用VK_PRESENT_MODE_IMMEDIATE_KHR
模式了。
了解完上边的这些参数之后,我们实现一个函数querySwapchainParam()
,用以查询最适配的表面参数。
void querySwapchainParam(VkPhysicalDevice physicalDev, VkSurfaceKHR surface, SwapchainParam *out_swapchainParam){
}
- 查询表面支持
vkGetPhysicalDeviceSurfaceCapabilitiesKHR
。
//capabilities
CALL_VK(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDev, surface, &out_swapchainParam->capabilities));
- 选择最佳的表面格式
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];
}
- 选择最佳的呈现模式
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
- Author: xingchen
- Link: http://www.adiosy.com/posts/learn_vulkan/learn_vulkan01_%E9%87%8D%E8%A6%81%E5%AF%B9%E8%B1%A1%E6%B5%85%E6%9E%90.html
- License: This work is under a 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. Kindly fulfill the requirements of the aforementioned License when adapting or creating a derivative of this work.