下载 [PDF 1 MB]
Github 示例代码链接
目录
教程 2: 交换链 — 集成 Vulkan 和操作系统
欢迎观看第 2 节 Vulkan 教程。 在第 1 节教程中,我们介绍了 Vulkan 基本设置:功能加载、实例创建、选择物理设备和队列,以及逻辑设备创建。 现在您一定希望绘制一些图像! 很遗憾,我们需要等到下一节。 为什么? 因为如果我们绘图,肯定希望能够看见它。 与 OpenGL* 类似,我们必须将 Vulkan 管道与操作系统提供的应用和 API 相集成。 不过很遗憾,使用 Vulkan 时,这项任务并不简单。 正如其他精简 API 一样,这样做是有目的的 — 实现高性能和灵活性。
那么如何集成 Vulkan 和应用窗口? 它与 OpenGL 之间有何不同? 在(Microsoft Windows* 上的)OpenGL 中,我们获取与应用窗口相关的设备环境 (Device Context)。 使用时,我们需要定义“如何”在屏幕上演示图像,用“哪种”格式在应用窗口上绘制,以及应支持哪些功能。 这可通过像素格式完成。 大多数时候,我们可通过 24 位深度缓冲区和双缓冲支持(这样我们可以在“隐藏的”后台缓冲区绘图,并图像完成后在屏幕上演示图像 — 交换前后缓冲区),创建 32 位色彩平面。 只有完成这些准备工作后,才能创建并激活渲染环境 (Rendering Context)。 在 OpenGL 中,所有渲染直接进入默认的后台缓冲区。
在 Vulkan 中没有默认帧缓冲区。 我们可以创建一个不显示任何内容的应用。 这种方法非常有效。 但是如果希望显示内容,我们可以创建支持渲染的缓冲区集。 这些缓冲区及其属性(与 Direct3D* 类似)称为交换链。 交换链可包含许多图像。 如果要显示其中一些图像,我们不必“交换”(顾名思义)图像,而是演示这些图像,这意味着我们要将它们返回至演示引擎。 因此在 OpenGL 中,首先需要定义平面格式并建立它与窗口(至少在 Windows 上)的相关性,然后创建渲染环境。 在 Vulkan 中,我们首先创建实例、设备,然后创建交换链。 但有意思的是,在一些情况下,需要破坏该交换链并重新创建它。 在工作流程中, 从头开始!
请求交换链扩展
在 Vulkan 中,交换链就是一种扩展。 为什么? 我们希望在应用窗口中的屏幕中显示图像,这不是很明显吗?
并不明显。 Vulkan 可用于多种用途,包括执行数学运算、提升物理计算速度,以及处理视频流。 这些行为结果无需显示在常用显示器上,这就是核心 API 适用于操作系统的原因(与 OpenGL 类似)。
如果想创建游戏,并将渲染后的图像显示在显示器上,您可以(并应该)使用交换链。 但我们说交换链是扩展的原因还有第二个。 每种操作系统显示图像的方式都各不相同。 供您渲染图像的平面可能以不同的方式实施,格式不同,在操作系统的表现方式也不相同 — 没有一种通用的方法。 因此在 Vulkan 中,交换链还必须依赖编写应用所针对的操作系统。
在 Vulkan 中,交换链被视作一种扩展,原因如下:它可提供与操作系统特定代码相集成的渲染对象(在 OpenGL 中为 FBO 等缓冲区或图像)。 这是核心 Vulkan(独立于平台)所无法完成的。因此如果交换链创建和用法是一种扩展,那么我们需要在实例和设备创建过程中请求扩展。 我们必须在两个层面(至少在大多数操作系统(包括 Windows 和 Linux*)上)启用扩展,才能创建和使用交换链。 这意味着我们必须回到第 1 节教程并进行更改,以请求与交换链有关的相应扩展。 如果特定实例和设备不支持这些扩展,将无法创建该实例和/或设备。 当然我们还能采用其他方法显示图像,比如获取针对缓冲区(纹理)的内存指示器(映射它),或将数据拷贝至操作系统获取的窗口的平面指示器。 该流程(尽管并不困难)不在本教程讨论范围内。 但幸运的是,交换链似乎与 OpenGL 的核心扩展类似:并不在核心规范内,也不要求实施,但所有硬件厂商都会实施。 我认为所有硬件厂商都希望证明,他们支持 Vulkan,而且这样能够显著提升屏幕上显示的游戏的性能。 而且还有一点能够支持该理论,即交换链扩展可集成至主要的“核心”vulkan.h 标头。
如果支持交换链,实际上会涉及到三种扩展:两种来源于实例层,一种来源于设备层。 从逻辑上来说,这些扩展将不同的功能分开来。 第一种是在实例层定义的 VK_KHR_surface扩展。 它可描述“平面”对象,即应用窗口的逻辑表现形式。 该扩展支持我们查看平面的不同参数(功能、支持的格式、大小),并查询特定物理设备是否支持交换链(更确切的说,特定队列家族是否支持在特定平面上演示图像)。 这些信息非常实用,因为我们不想选择物理设备并尝试通过它创建逻辑设备,来了解它是否支持交换链。 该扩展还可定义破环此类平面的方法。
第二种实例层扩展依赖于操作系统:在 Windows 操作系统家族中称为 VK_KHR_win32_surface,在 Linux 中称为VK_KHR_xlib_surface或 VK_KHR_xcb_surface。 该扩展支持我们创建在特定操作系统中展现应用窗口(并使用特定于操作系统的参数)的平面。
检查是否支持实例扩展
启用这两种实例层扩展之前,需要查看它们是否可用或受到支持。 我们一直在讨论实例扩展,还未创建过任何实例。 为确定 Vulkan 实例是否支持这些扩展,我们使用全局级函数 vkEnumerateInstanceExtensionProperties()。 它列举所有可用实例通用扩展,第一个参数是否为 null,或实例层扩展(似乎层级也可以有扩展),是否将第一个参数设置为任意特定层级的名称。 我们对层级不感兴趣,因此将第一个参数设为 null。 我们重新调用该函数两次。 在第一次调用中,我们希望获取支持的扩展总数,因此将第三个参数保留为 null。 接下来我们为所有扩展准备存储,并用第三个指向已分配存储再次调用该函数。
uint32_t extensions_count = 0;
if( (vkEnumerateInstanceExtensionProperties( nullptr, &extensions_count, nullptr ) != VK_SUCCESS) ||
(extensions_count == 0) ) {
std::cout << "Error occurred during instance extensions enumeration!"<< std::endl;
return false;
}
std::vector<VkExtensionProperties> available_extensions( extensions_count );
if( vkEnumerateInstanceExtensionProperties( nullptr, &extensions_count, &available_extensions[0] ) != VK_SUCCESS ) {
std::cout << "Error occurred during instance extensions enumeration!"<< std::endl;
return false;
}
std::vector<const char*> extensions = {
VK_KHR_SURFACE_EXTENSION_NAME,
#if defined(VK_USE_PLATFORM_WIN32_KHR)
VK_KHR_WIN32_SURFACE_EXTENSION_NAME
#elif defined(VK_USE_PLATFORM_XCB_KHR)
VK_KHR_XCB_SURFACE_EXTENSION_NAME
#elif defined(VK_USE_PLATFORM_XLIB_KHR)
VK_KHR_XLIB_SURFACE_EXTENSION_NAME
#endif
};
for( size_t i = 0; i < extensions.size(); ++i ) {
if( !CheckExtensionAvailability( extensions[i], available_extensions ) ) {
std::cout << "Could not find instance extension named \""<< extensions[i] << "\"!"<< std::endl;
return false;
}
}
1. Tutorial02.cpp,函数 CreateInstance()
我们可以为较少数量的扩展准备一个位置,然后vkEnumerateInstanceExtensionProperties()会返回 VK_INCOMPLETE,以让我们知道我们没有获取所有扩展。
我们的阵列现在布满了所有可用(支持的)实例层扩展。 已分配空间的各要素均包含扩展名称及其版本。 第二个参数可能不常使用,但可帮助我们检查硬件是否支持特定版本的扩展。 例如,我们可能对部分特定扩展感兴趣,并为此下载了包含许多标头文件的 SDK。 每个标头文件都有自己的版本,与该查询返回的值相对应。 如果供应用执行的硬件支持旧版本扩展(不是我们下载了 SDK 的扩展),它可能不支持我们通过该特定扩展所使用的所有功能。 因此,有时验证版本非常实用,但对交换链来说无所谓 — 至少现在是这样。
现在我们可以搜索所有返回的扩展,检查该列表是否包含我们要寻找的扩展。 这里我使用两个方便的定义,分别为 VK_KHR_SURFACE_EXTENSION_NAME 和 VK_KHR_????_SURFACE_EXTENSION_NAME。 它们在 Vulkan 标头文件中定义,并包含扩展名称,因此我们无需拷贝或记住它们。 我们只需使用代码中的定义,如果出现错误,编译器会告诉我们。 我希望所有扩展都带有类似的定义。
第二个定义带有一个。 这两个提到的定义都位于 vulkan.h 标头文件中。 但第二个不是特定于操作系统定义,且 vulkan.h 标头独立于操作系统吗? 两个问题都是对的,而且非常有效。 vulkan.h 文件独立于操作系统,且包含特定于操作系统的扩展的定义。 但这些均位于 #ifdef … #endif 预处理器指令之中。 如果想“启用”它们,需要在项目的某个地方添加相应的预处理器指令。 对 Windows 系统来说,需要添加 VK_USE_PLATFORM_WIN32_KHR 字符串。 在 Linux 上,根据我们是否希望使用 X11 或 XCB 库,需要添加 VK_USE_PLATFORM_XCB_KHR 或 VK_USE_PLATFORM_XLIB_KHR。 在提供的示例项目中,这些定义通过 CMakeLists.txt 文件默认添加。
但回到源代码。 CheckExtensionAvailability() 函数具有哪些功能? 它循环所有可用扩展,并将它们的名称与所提供扩展的名称进行对比。 如果发现匹配,将返回“真”值。
for( size_t i = 0; i < available_extensions.size(); ++i ) {
if( strcmp( available_extensions[i].extensionName, extension_name ) == 0 ) {
return true;
}
}
return false;
2.Tutorial02.cpp,函数 CheckExtensionAvailability()
启用实例层扩展
我们已经验证了这两种扩展均受支持。 实例层扩展在实例创建中请求(启用) — 我们创建包含应启用的扩展列表的实例。 以下代码负责完成这一步骤:
VkApplicationInfo application_info = {
VK_STRUCTURE_TYPE_APPLICATION_INFO, // VkStructureType sType
nullptr, // const void *pNext"API without Secrets: Introduction to Vulkan", // const char *pApplicationName
VK_MAKE_VERSION( 1, 0, 0 ), // uint32_t applicationVersion"Vulkan Tutorial by Intel", // const char *pEngineName
VK_MAKE_VERSION( 1, 0, 0 ), // uint32_t engineVersion
VK_API_VERSION // uint32_t apiVersion
};
VkInstanceCreateInfo instance_create_info = {
VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, // VkStructureType sType
nullptr, // const void *pNext
0, // VkInstanceCreateFlags flags
&application_info, // const VkApplicationInfo *pApplicationInfo
0, // uint32_t enabledLayerCount
nullptr, // const char * const *ppEnabledLayerNames
static_cast<uint32_t>(extensions.size()), // uint32_t enabledExtensionCount&extensions[0] // const char * const *ppEnabledExtensionNames
};
if( vkCreateInstance( &instance_create_info, nullptr, &Vulkan.Instance ) != VK_SUCCESS ) {
std::cout << "Could not create Vulkan instance!"<< std::endl;
return false;
}
return true;
3.Tutorial02.cpp,函数 CreateInstance()
该代码与 Tutorial01.cpp 文件中的 CreateInstance()函数类似。 要请求实例层扩展,必须为阵列准备我们希望启用的所有扩展的名称。 此处我们使用包含 “const char*” 要素,以及以定义形式提及的扩展名称的标准矢量。
在教程 1 中,我们声明零扩展,并将阵列地址的 nullptr 放在 VkInstanceCreateInfo 结构中。 这次我们必须提供阵列(包含请求扩展的名称)的第一个要素的地址。 而且还必须指定阵列所包含的要素数量(因此我们选择使用矢量:如果在未来版本中需要添加或删除扩展,该矢量的大小也会相应更改)。 接下来我们调用 vkCreateInstance()函数。 如果不返回 VK_SUCCESS,表示(在本教程中)不支持这些扩展。 如果成功返回,我们可像之前一样加载实例层函数,但这次还需加载一些其他特定于扩展的函数。
这些函数附带了部分其他的函数。 而且因为它是实例层函数,因此必须将它们添加至实例层函数集(以便在有相应函数时能够适时加载)。 在本示例中,必须将以下函数添加至打包在 VK_INSTANCE_LEVEL_FUNCTION() 宏中的 ListOfFunctions.inl,如下所示:
// From extensions
#if defined(USE_SWAPCHAIN_EXTENSIONS)
VK_INSTANCE_LEVEL_FUNCTION( vkDestroySurfaceKHR )
VK_INSTANCE_LEVEL_FUNCTION( vkGetPhysicalDeviceSurfaceSupportKHR )
VK_INSTANCE_LEVEL_FUNCTION( vkGetPhysicalDeviceSurfaceCapabilitiesKHR )
VK_INSTANCE_LEVEL_FUNCTION( vkGetPhysicalDeviceSurfaceFormatsKHR )
VK_INSTANCE_LEVEL_FUNCTION( vkGetPhysicalDeviceSurfacePresentModesKHR )
#if defined(VK_USE_PLATFORM_WIN32_KHR)
VK_INSTANCE_LEVEL_FUNCTION( vkCreateWin32SurfaceKHR )
#elif defined(VK_USE_PLATFORM_XCB_KHR)
VK_INSTANCE_LEVEL_FUNCTION( vkCreateXcbSurfaceKHR )
#elif defined(VK_USE_PLATFORM_XLIB_KHR)
VK_INSTANCE_LEVEL_FUNCTION( vkCreateXlibSurfaceKHR )
#endif
#endif
4.ListOfFunctions.inl
还有一件事情: 我已将所有与交换链相关的函数打包在其他 #ifdef … #endif 配对中,这要求定义 USE_SWAPCHAIN_EXTENSIONS 预处理器指令。 我已完成这步,这样便能够按照教程 1 中的说明操作。 没有它,第一个应用(因为它使用相同的标头文件)将尝试加载所有函数。 但我们没有启用教程 1 中的交换链扩展,因此此操作会失败,应用也将在没有完全初始化 Vulkan 的情况下关闭。 如果没有启用特定扩展,它的函数将不可用。
创建演示平面
我们借助两个启用的扩展创建了 Vulkan 实例。 我们通过核心 Vulkan 规范和启用的扩展加载了实例层函数(得益于宏,这一过程自动完成)。 为创建平面,我们编写了如下代码:
#if defined(VK_USE_PLATFORM_WIN32_KHR)
VkWin32SurfaceCreateInfoKHR surface_create_info = {
VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR, // VkStructureType sType
nullptr, // const void *pNext
0, // VkWin32SurfaceCreateFlagsKHR flags
Window.Instance, // HINSTANCE hinstance
Window.Handle // HWND hwnd
};
if( vkCreateWin32SurfaceKHR( Vulkan.Instance, &surface_create_info, nullptr, &Vulkan.PresentationSurface ) == VK_SUCCESS ) {
return true;
}
#elif defined(VK_USE_PLATFORM_XCB_KHR)
VkXcbSurfaceCreateInfoKHR surface_create_info = {
VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR, // VkStructureType sType
nullptr, // const void *pNext
0, // VkXcbSurfaceCreateFlagsKHR flags
Window.Connection, // xcb_connection_t* connection
Window.Handle // xcb_window_t window
};
if( vkCreateXcbSurfaceKHR( Vulkan.Instance, &surface_create_info, nullptr, &Vulkan.PresentationSurface ) == VK_SUCCESS ) {
return true;
}
#elif defined(VK_USE_PLATFORM_XLIB_KHR)
VkXlibSurfaceCreateInfoKHR surface_create_info = {
VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR, // VkStructureType sType
nullptr, // const void *pNext
0, // VkXlibSurfaceCreateFlagsKHR flags
Window.DisplayPtr, // Display *dpy
Window.Handle // Window window
};
if( vkCreateXlibSurfaceKHR( Vulkan.Instance, &surface_create_info, nullptr, &Vulkan.PresentationSurface ) == VK_SUCCESS ) {
return true;
}
#endif
std::cout << "Could not create presentation surface!"<< std::endl;
return false;
5.Tutorial02.cpp,函数 CreatePresentationSurface()
为创建演示平面,我们调用 vkCreate????SurfaceKHR()函数,以接受 Vulkan 实例(借助启用的平面扩展)、特定于操作系统的结构指示器、可选内存分配处理函数的指示器,以及供保存已创建平面的句柄的变量指示器。
特定于操作系统的结构称为 Vk????SurfaceCreateInfoKHR并包含以下字段:
- sType – 标准结构类型,应相当于 VK_STRUCTURE_TYPE_????_SURFACE_CREATE_INFO_KHR(其中 ???? 可以是 WIN32、XCB、XLIB 或其他)
- pNext – 其他部分结构的标准指示器
- flags – 留作将来使用的参数。
- hinstance/connection/dpy – 第一个特定于操作系统的参数
- hwnd/window – 应用窗口的句柄(同样特定于操作系统)
检查是否支持设备扩展
我们已经创建了实例和平面。 下一步是创建逻辑设备。 但我们希望创建支持交换链的设备。 因此我们还需检查特定物理设备是否支持交换链扩展 — 设备层扩展。 该扩展称为 VK_KHR_swapchain,可定义交换链的实际支持、实施和用法。
要检查特定物理设备支持哪些扩展,我们必须创建与为实例层扩展准备的代码类似的代码。 这次我们只使用 vkEnumerateDeviceExtensionProperties()函数。 它的运行模式与查询实例扩展的函数大体相同。 唯一的区别是它提取第一个参数中的附加物理设备句柄。 该函数的代码类似于以下示例。 在我们的示例源代码中,它是 CheckPhysicalDeviceProperties() 函数的一部分。
uint32_t extensions_count = 0;
if( (vkEnumerateDeviceExtensionProperties( physical_device, nullptr, &extensions_count, nullptr ) != VK_SUCCESS) ||
(extensions_count == 0) ) {
std::cout << "Error occurred during physical device "<< physical_device << " extensions enumeration!"<< std::endl;
return false;
}
std::vector<VkExtensionProperties> available_extensions( extensions_count );
if( vkEnumerateDeviceExtensionProperties( physical_device, nullptr, &extensions_count, &available_extensions[0] ) != VK_SUCCESS ) {
std::cout << "Error occurred during physical device "<< physical_device << " extensions enumeration!"<< std::endl;
return false;
}
std::vector<const char*> device_extensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
for( size_t i = 0; i < device_extensions.size(); ++i ) {
if( !CheckExtensionAvailability( device_extensions[i], available_extensions ) ) {
std::cout << "Physical device "<< physical_device << " doesn't support extension named \""<< device_extensions[i] << "\"!"<< std::endl;
return false;
}
}
6.Tutorial02.cpp,函数 CheckPhysicalDeviceProperties()
我们首先请求特定物理设备上可用的所有扩展的数量。 接下来获取它们的名称并检查设备层交换链扩展。 如果没有,那没有必要进一步查看设备的属性、特性和队列家族的属性,因为特定设备根本不支持交换链。
检查是否支持演示至特定平面
现在我们回过头来看 CreateDevice() 函数。 创建完实例后,我们在教程 1 中循环了所有可用物理设备并查询了它们的属性。 根据这些属性,我们选择了我们所希望使用的设备和希望请求的队列家族。 该查询在循环所有可用物理设备的过程中完成。 既然我们希望使用交换链,那么必须对 CheckPhysicalDeviceProperties() 函数(通过 CreateDevice() 函数在所提到的循环中调用)进行更改,如下所示:
uint32_t selected_graphics_queue_family_index = UINT32_MAX;
uint32_t selected_present_queue_family_index = UINT32_MAX;
for( uint32_t i = 0; i < num_devices; ++i ) {
if( CheckPhysicalDeviceProperties( physical_devices[i], selected_graphics_queue_family_index, selected_present_queue_family_index ) ) {
Vulkan.PhysicalDevice = physical_devices[i];
}
}
7.Tutorial02.cpp,函数 CreateDevice()
唯一的更改是添加了另外一个变量,它将包含支持交换链的队列家族的索引(更精确的图像演示)。 遗憾的是,仅检查是否支持交换链扩展远远不够,因为演示支持是一种队列家族属性。 物理设备可能支持交换链,但这不表示其所有队列家族都支持交换链。 我们是否针对需要另一队列或队列家族来显示图像? 我们能不能仅使用我们在教程 1 中选择的图形队列? 大多数时候,一个队列家族就能满足需求。 这意味着所选择的队列家族将支持图形操作和演示。 但遗憾的是,单个队列家族中还有可能存在不支持图形和演示的设备。 在 Vulkan 中,我们需要灵活应对任何情况。
vkGetPhysicalDeviceSurfaceSupportKHR()函数用于查看特定物理设备的特定队列家族是否支持交换链,或更准确地说,是否支持在特定平面上演示图像。 因此我们才需要提前创建平面。
假设我们已经检查了特定物理设备是否显示交换链扩展,并查询了一些特定物理设备支持的队列家族。 我们还请求了所有队列家族的属性。 现在可以检查特定队列家族是否支持在平面(窗口)上演示图像。
uint32_t graphics_queue_family_index = UINT32_MAX;
uint32_t present_queue_family_index = UINT32_MAX;
for( uint32_t i = 0; i < queue_families_count; ++i ) {
vkGetPhysicalDeviceSurfaceSupportKHR( physical_device, i, Vulkan.PresentationSurface, &queue_present_support[i] );
if( (queue_family_properties[i].queueCount > 0) &&
(queue_family_properties[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) ) {
// Select first queue that supports graphics
if( graphics_queue_family_index == UINT32_MAX ) {
graphics_queue_family_index = i;
}
// If there is queue that supports both graphics and present - prefer it
if( queue_present_support[i] ) {
selected_graphics_queue_family_index = i;
selected_present_queue_family_index = i;
return true;
}
}
}
// We don't have queue that supports both graphics and present so we have to use separate queues
for( uint32_t i = 0; i < queue_families_count; ++i ) {
if( queue_present_support[i] ) {
present_queue_family_index = i;
break;
}
}
// If this device doesn't support queues with graphics and present capabilities don't use it
if( (graphics_queue_family_index == UINT32_MAX) ||
(present_queue_family_index == UINT32_MAX) ) {
std::cout << "Could not find queue family with required properties on physical device "<< physical_device << "!"<< std::endl;
return false;
}
selected_graphics_queue_family_index = graphics_queue_family_index;
selected_present_queue_family_index = present_queue_family_index;
return true;
8.Tutorial02.cpp,函数 CheckPhysicalDeviceProperties()
这里我们要迭代所有可用的队列家族。 在每次循环迭代中,我们调用一个负责查看特定队列家族是否支持演示图像的函数。 vkGetPhysicalDeviceSurfaceSupportKHR()要求我们提供物理设备句柄、欲检查的队列家族,以及希望渲染(演示图像)的平面句柄。 如果支持,特定地址中将保存 VK_TRUE;否则将保存 VK_FALSE。
现在我们具有所有可用队列家族的属性。 我们知道哪种队列家族支持图形操作,哪种支持图像演示。 在本教程示例中,我更偏向于支持这两种功能的队列家族。 找到一个后,保存该队列家族的索引,并立即退出 CheckPhysicalDeviceProperties() 函数。 如果没有这种队列家族,我将使用支持图形的第一个队列家族和支持图像演示的第一个队列家族。 只有这样才能使该函数包含“成功”返回代码。
高级场景可能搜索所有可用设备,并尝试寻找包含支持图形操作和演示的队列家族的设备。 不过我还想象了没有设备支持这两种操作的场景。 那么我们必须用一台设备进行图形运算(类似于老式的“图形加速器”),用另一台设备在屏幕(连接“加速器”和显示器)上演示结果。 遗憾的是,在这种情况下,我们必须使用 Vulkan Runtime 的“通用”Vulkan 函数,或者需要保存适用于每台设备(设备实施 Vulkan 函数的方式各不相同 )的设备层函数。 但我们希望这种场景不要经常出现。
借助启用的交换链扩展创建设备
现在我们回到 CreateDevice() 函数。 我们发现了支持图形和演示操作,但在单个队列家族中不一定支持的物理设备。 现在我们需要创建一台逻辑设备。
std::vector<VkDeviceQueueCreateInfo> queue_create_infos;
std::vector<float> queue_priorities = { 1.0f };
queue_create_infos.push_back( {
VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, // VkStructureType sType
nullptr, // const void *pNext
0, // VkDeviceQueueCreateFlags flags
selected_graphics_queue_family_index, // uint32_t queueFamilyIndex
static_cast<uint32_t>(queue_priorities.size()), // uint32_t queueCount&queue_priorities[0] // const float *pQueuePriorities
} );
if( selected_graphics_queue_family_index != selected_present_queue_family_index ) {
queue_create_infos.push_back( {
VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, // VkStructureType sType
nullptr, // const void *pNext
0, // VkDeviceQueueCreateFlags flags
selected_present_queue_family_index, // uint32_t queueFamilyIndex
static_cast<uint32_t>(queue_priorities.size()), // uint32_t queueCount&queue_priorities[0] // const float *pQueuePriorities
} );
}
std::vector<const char*> extensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
VkDeviceCreateInfo device_create_info = {
VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, // VkStructureType sType
nullptr, // const void *pNext
0, // VkDeviceCreateFlags flags
static_cast<uint32_t>(queue_create_infos.size()), // uint32_t queueCreateInfoCount&queue_create_infos[0], // const VkDeviceQueueCreateInfo *pQueueCreateInfos
0, // uint32_t enabledLayerCount
nullptr, // const char * const *ppEnabledLayerNames
static_cast<uint32_t>(extensions.size()), // uint32_t enabledExtensionCount&extensions[0], // const char * const *ppEnabledExtensionNames
nullptr // const VkPhysicalDeviceFeatures *pEnabledFeatures
};
if( vkCreateDevice( Vulkan.PhysicalDevice, &device_create_info, nullptr, &Vulkan.Device ) != VK_SUCCESS ) {
std::cout << "Could not create Vulkan device!"<< std::endl;
return false;
}
Vulkan.GraphicsQueueFamilyIndex = selected_graphics_queue_family_index;
Vulkan.PresentQueueFamilyIndex = selected_present_queue_family_index;
return true;
9.Tutorial02.cpp,函数 CreateDevice()
像之前一样,我们需要填充 VkDeviceCreateInfo 类型的变量。 为此,我们需要声明队列家族和欲启用的队列数量。 我们通过包含 VkDeviceQueueCreateInfo 要素的独立阵列的指示器来完成这一步骤。 此处我声明一个矢量,并添加一个要素,定义支持图形操作的队列家族的某个队列。 使用矢量的原因是,如果不支持图形和演示操作,我们将需要定义两个单独的队列家族。 如果单个家族支持这两种操作,我们仅需定义一个成员,并声明仅需一个家族。 如果图形和演示家族的索引不同,我们需要声明面向矢量和 VkDeviceQueueCreateInfo 要素的其他成员。 在这种情况下,VkDeviceCreateInfo 结构必须提供这两个不同家族的信息。 因此矢量将再次派上用场(通过其 size() 成员函数)。
但我们还未完成设备创建。 我们需要请求第三个与交换链相关的扩展 — 设备层“VK_KHR_swapchain”扩展。 如前所述,该扩展定义交换链的实际支持、实施和用法。
请求该扩展时,与实例层一样,我们定义一个阵列(或矢量),其中我们欲启用的所有设备层扩展的名称。 我们提供该阵列第一个要素的地址,以及希望使用的扩展数量。 该扩展还以 #define VK_KHR_SWAPCHAIN_EXTENSION_NAME 的形式包含其名称定义。 我们可以在阵列(矢量)中使用该扩展,无需担心任何 typo 问题。
第三个扩展包含用于实际创建、破坏、或管理交换链的其他函数。 使用之前,我们需要将指示器加载至这些函数。 它们来自于设备层,因此我们可以使用 VK_DEVICE_LEVEL_FUNCTION() 宏将它们放在 ListOfFunctions.inl 文件中。
// From extensions
#if defined(USE_SWAPCHAIN_EXTENSIONS)
VK_DEVICE_LEVEL_FUNCTION( vkCreateSwapchainKHR )
VK_DEVICE_LEVEL_FUNCTION( vkDestroySwapchainKHR )
VK_DEVICE_LEVEL_FUNCTION( vkGetSwapchainImagesKHR )
VK_DEVICE_LEVEL_FUNCTION( vkAcquireNextImageKHR )
VK_DEVICE_LEVEL_FUNCTION( vkQueuePresentKHR )
#endif
10.ListOfFunctions.inl
你可再次看到,我们正在检查是否定义了 USE_SWAPCHAIN_EXTENSIONS 预处理器指令。 我只在启用交换链扩展的项目中定义。
由于我们创建了逻辑设备,所以需要接收图形队列和演示队列(如果分开)的句柄。 为方便起见,我使用两个单独的队列变量,但它们都包含相同的句柄。
加载设备层函数后,我们读取请求的队列句柄。 以下是相应的代码:
vkGetDeviceQueue( Vulkan.Device, Vulkan.GraphicsQueueFamilyIndex, 0, &Vulkan.GraphicsQueue );
vkGetDeviceQueue( Vulkan.Device, Vulkan.PresentQueueFamilyIndex, 0, &Vulkan.PresentQueue );
return true;
11.Tutorial02.cpp,函数 GetDeviceQueue()
创建旗语 (semaphore)
创建和使用交换链之前的最后一个步骤是创建旗语。 旗语指用于队列同步化的对象。 其中包含信号旗语和无信号旗语。 如果部分操作已完成,其中一个队列将发出旗语信号(将状态从无信号改成信号),另一队列将等待该旗语直至其变成信号旗语。 之后,队列重新执行通过命令缓冲区提交的操作。
VkSemaphoreCreateInfo semaphore_create_info = {
VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, // VkStructureType sType
nullptr, // const void* pNext
0 // VkSemaphoreCreateFlags flags
};
if( (vkCreateSemaphore( Vulkan.Device, &semaphore_create_info, nullptr, &Vulkan.ImageAvailableSemaphore ) != VK_SUCCESS) ||
(vkCreateSemaphore( Vulkan.Device, &semaphore_create_info, nullptr, &Vulkan.RenderingFinishedSemaphore ) != VK_SUCCESS) ) {
std::cout << "Could not create semaphores!"<< std::endl;
return false;
}
return true;
12.Tutorial02.cpp,函数 CreateSemaphores()
我们调用 vkCreateSemaphore()函数,以创建旗语。 它要求提供包含三个字段的创建信息:
- sType – 标准结构类型,在此示例中必须设置为 VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO。
- pNext – 留作将来使用的标准参数。
- flags – 另一留作将来使用的参数,必须是 0。
旗语在绘制过程中(如果希望提高精确性,在演示过程中)使用。 稍后将详细介绍。
创建交换链
我们启用了交换链支持,但在屏幕上进行渲染之前,必须首先创建交换链,以获取图像,进行渲染(或在渲染至其他图像时进行拷贝)。
为创建交换链,我们调用 vkCreateSwapchainKHR()函数。 它要求我们提供类型变量 VkSwapchainCreateInfoKHR 的地址,告知驱动程序将创建的交换链的属性。 为使用相应的值填充该结构,我们必须确定特定硬件和平台上的内容。 为此我们查询平台或窗口有关可用性和兼容不同特性的属性,即支持的图像格式或演示模式(如何在屏幕上演示图像)。 因此在创建交换链之前,我们必须查看特定平台的内容,以及如何创建交换链。
获取平面功能
首先必须查询平面功能。 为此,我们调用 vkGetPhysicalDeviceSurfaceCapabilitiesKHR()函数,如下所示:
VkSurfaceCapabilitiesKHR surface_capabilities;
if( vkGetPhysicalDeviceSurfaceCapabilitiesKHR( Vulkan.PhysicalDevice, Vulkan.PresentationSurface, &surface_capabilities ) != VK_SUCCESS ) {
std::cout << "Could not check presentation surface capabilities!"<< std::endl;
return false;
}
13.Tutorial02.cpp,函数 CreateSwapChain()
获取的功能包含有关交换链支持范围(限制)的重要信息,即图像的最大数和最小数、图像的最小尺寸和最大尺寸,或支持的转换格式(有些平台可能要求在演示图像之前首先进行图像转换)。
获取支持的平面格式
下一步我们需要查询支持的平面格式。 并非所有平台都兼容常见的图像格式,比如非线性 32 位 RGBA。 有些平台没有任何偏好,但有些仅支持少数几种格式。 我们仅选择其中一种适用于交换链的可用格式,否则创建将会失败。
要查询平面格式,我们必须调用 vkGetPhysicalDeviceSurfaceFormatsKHR()函数。 我们可以像往常一样调用两次:第一次获取支持格式的数量,第二次获取阵列(为达到该目的而准备)中的支持格式。 可通过以下命令来实现:
uint32_t formats_count;
if( (vkGetPhysicalDeviceSurfaceFormatsKHR( Vulkan.PhysicalDevice, Vulkan.PresentationSurface, &formats_count, nullptr ) != VK_SUCCESS) ||
(formats_count == 0) ) {
std::cout << "Error occurred during presentation surface formats enumeration!"<< std::endl;
return false;
}
std::vector<VkSurfaceFormatKHR> surface_formats( formats_count );
if( vkGetPhysicalDeviceSurfaceFormatsKHR( Vulkan.PhysicalDevice, Vulkan.PresentationSurface, &formats_count, &surface_formats[0] ) != VK_SUCCESS ) {
std::cout << "Error occurred during presentation surface formats enumeration!"<< std::endl;
return false;
}
14.Tutorial02.cpp,函数 CreateSwapChain()
获取支持的演示模式
我们还应请求可用的演示模式,告知我们如何在屏幕上演示(显示)图像。 演示模式定义应用是等待垂直同步,还是在可用时立即显示图像(可能会造成图像撕裂)。 稍后我将介绍不同的演示模式。
为查询特定平台支持的演示模式,我们调用 vkGetPhysicalDeviceSurfacePresentModesKHR()函数。 我们可以创建与以下内容类似的代码:
uint32_t present_modes_count;
if( (vkGetPhysicalDeviceSurfacePresentModesKHR( Vulkan.PhysicalDevice, Vulkan.PresentationSurface, &present_modes_count, nullptr ) != VK_SUCCESS) ||
(present_modes_count == 0) ) {
std::cout << "Error occurred during presentation surface present modes enumeration!"<< std::endl;
return false;
}
std::vector<VkPresentModeKHR> present_modes( present_modes_count );
if( vkGetPhysicalDeviceSurfacePresentModesKHR( Vulkan.PhysicalDevice, Vulkan.PresentationSurface, &present_modes_count, &present_modes[0] ) != VK_SUCCESS ) {
std::cout << "Error occurred during presentation surface present modes enumeration!"<< std::endl;
return false;
}
15.Tutorial02.cpp,函数 CreateSwapChain()
现在我们获取了所有相关数据,可帮助我们为创建交换链准备相应的数值。
选择交换链图像数量
交换链包含多个图像。 获取多个图像(通常超过一个)可帮助演示引擎正常运行,即一个图像在屏幕上演示,另一个图像等待查询下一垂直同步,而第三个图像用于应用渲染。
应用可能会请求更多图像。 如果希望,它可以一次使用多个图像,例如,为视频流编码时,第四个图像是关键帧,应用需要它来准备剩下的三个帧。 这种用法将决定在交换链中自动创建的图像数量:应用一次要求处理多少个图像,以及演示引擎要求多少个图像才能正常运行。
但我们必须确保请求的交换链图像数量不少于所需图像的最小值,也不超过支持图像的最大值(如果存在这种数量限制条件)。 而且,图像过多会要求使用更大的内存。 另一方面,图像过少,会导致应用中出现停顿(稍后详细介绍)。
交换链正常运行以及应用进行渲染所需的图像数量由平面功能定义。 以下部分代码可检查图像数量是否在允许的最小值和最大值之间:
// Set of images defined in a swap chain may not always be available for application to render to:
// One may be displayed and one may wait in a queue to be presented
// If application wants to use more images at the same time it must ask for more images
uint32_t image_count = surface_capabilities.minImageCount + 1;
if( (surface_capabilities.maxImageCount > 0) &&
(image_count > surface_capabilities.maxImageCount) ) {
image_count = surface_capabilities.maxImageCount;
}
return image_count;
16.Tutorial02.cpp,函数 GetSwapChainNumImages()
平面功能结构中的 minImageCount 值提供交换链正常运行所需的最少图像。 此处我们选择比要求多一个的图像数量,并检查是否请求太多。 多出的一个图像可用于类似三次缓冲的演示模式(如果可用)。 在高级场景中,我们还要求保存希望(一次)同时使用的图像数量。 我们想要编码之前提到过的视频流,因此需要一个关键帧(每第四个图像帧)和其他三个图像。 但交换链不允许应用一次操作四个图像 — 只能操作三个。 我们必须了解这种情况,因为我们仅通过关键帧准备了两个帧,然后我们需要释放它们(让其返回至演示引擎)并获取最后第三个非关键帧。 这一点稍后会更加清晰。
选择交换链图像格式
选择图像格式取决于我们希望执行的处理/渲染类型,即如果我们想混合应用窗口和桌面内容,可能需要一个阿尔法值。 我们还必须知道哪种色域可用,以及我们是否在线性或 sRGB 色域上操作。
平台支持的格式-色域配对数量各不相同。 如果希望使用特定数量,必须确保它们可用。
// If the list contains only one entry with undefined format
// it means that there are no preferred surface formats and any can be chosen
if( (surface_formats.size() == 1) &&
(surface_formats[0].format == VK_FORMAT_UNDEFINED) ) {
return{ VK_FORMAT_R8G8B8A8_UNORM, VK_COLORSPACE_SRGB_NONLINEAR_KHR };
}
// Check if list contains most widely used R8 G8 B8 A8 format
// with nonlinear color space
for( VkSurfaceFormatKHR &surface_format : surface_formats ) {
if( surface_format.format == VK_FORMAT_R8G8B8A8_UNORM ) {
return surface_format;
}
}
// Return the first format from the list
return surface_formats[0];
17.Tutorial02.cpp,函数 GetSwapChainFormat()
之前我们请求了放在阵列中的支持格式(在本示例中为矢量)。 如果该阵列仅包含一个值和一个未定义的格式,该平台将没有任何偏好。 我们可以使用任何图像格式。
在其他情况下,我们只能使用一种可用的格式。 这里我正在查找任意(线性或非线性)32 位 RGBA 格式。 如果可用,就可以选择。 如果没有这种格式,我将使用列表中的任意一种格式(希望第一个是最好的,同时也是精度最高的格式)。
选择交换链图像大小
交换链图像的大小通常与窗口大小相同。 我们可以选择其他大小,但必须符合图像大小限制。 符合当前应用窗口大小的图像大小以 “currentExtent” 成员的形式在平面功能结构中提供。
值得注意的是,特定值“-1”表示应用窗口大小由交换链大小决定,因此我们选择所希望的任意尺寸。 但必须确保所选尺寸不小于,也不大于定义的最小限值和最大限值。
选择交换链大小可能(通常)如下所示:
// Special value of surface extent is width == height == -1
// If this is so we define the size by ourselves but it must fit within defined confines
if( surface_capabilities.currentExtent.width == -1 ) {
VkExtent2D swap_chain_extent = { 640, 480 };
if( swap_chain_extent.width < surface_capabilities.minImageExtent.width ) {
swap_chain_extent.width = surface_capabilities.minImageExtent.width;
}
if( swap_chain_extent.height < surface_capabilities.minImageExtent.height ) {
swap_chain_extent.height = surface_capabilities.minImageExtent.height;
}
if( swap_chain_extent.width > surface_capabilities.maxImageExtent.width ) {
swap_chain_extent.width = surface_capabilities.maxImageExtent.width;
}
if( swap_chain_extent.height > surface_capabilities.maxImageExtent.height ) {
swap_chain_extent.height = surface_capabilities.maxImageExtent.height;
}
return swap_chain_extent;
}
// Most of the cases we define size of the swap_chain images equal to current window's size
return surface_capabilities.currentExtent;
18.Tutorial02.cpp,函数 GetSwapChainExtent()
选择交换链用法标记
用法标记定义如何在 Vulkan 中使用特定图像。 如果想对图像进行抽样(使用内部着色器),必须将它创建成“sampled”用法。 如果图像用作深度渲染对象,必须将它创建成“depth and stencil”用法。 没有“启用”相应用法的图像不能用于特定目的,否则不会定义此类操作结果。
对(大多数情况下)希望渲染其图像的交换链(用作渲染对象)来说,必须用 VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT enum 指定“color attachment”用法。 在 Vulkan 中,这种用法通常适用于交换链,因此通常在不进行其他检查的情况下对其进行设置。 但对于其他用法来说,我们必须确保它受到支持 — 可以通过平面功能结构的“supportedUsageFlags”成员来完成。
// Color attachment flag must always be supported
// We can define other usage flags but we always need to check if they are supported
if( surface_capabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT ) {
return VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
}
std::cout << "VK_IMAGE_USAGE_TRANSFER_DST image usage is not supported by the swap chain!"<< std::endl<< "Supported swap chain's image usages include:"<< std::endl<< (surface_capabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT ? " VK_IMAGE_USAGE_TRANSFER_SRC\n" : "")<< (surface_capabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT ? " VK_IMAGE_USAGE_TRANSFER_DST\n" : "")<< (surface_capabilities.supportedUsageFlags & VK_IMAGE_USAGE_SAMPLED_BIT ? " VK_IMAGE_USAGE_SAMPLED\n" : "")<< (surface_capabilities.supportedUsageFlags & VK_IMAGE_USAGE_STORAGE_BIT ? " VK_IMAGE_USAGE_STORAGE\n" : "")<< (surface_capabilities.supportedUsageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT ? " VK_IMAGE_USAGE_COLOR_ATTACHMENT\n" : "")<< (surface_capabilities.supportedUsageFlags & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT ? " VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT\n" : "")<< (surface_capabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT ? " VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT\n" : "")<< (surface_capabilities.supportedUsageFlags & VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT ? " VK_IMAGE_USAGE_INPUT_ATTACHMENT" : "")<< std::endl;
return static_cast<VkImageUsageFlags>(-1);
19.Tutorial02.cpp,函数 GetSwapChainUsageFlags()
在本示例中,我们定义图像清晰操作所需的其他“transfer destination”用法。
选择预转换
在有些平台上,我们可能希望对图像进行转换。 这种情况通常发生在朝向某一方位(而非默认方位)的平板电脑上。 在交换链创建期间,必须在图像演示之前指定应用于图像的转换方式。 当然,我们可以仅使用支持的转换,位于获取的平面功能的“supportedTransforms”成员之中。
如果选择的预转换不是(平面功能中的)当前转换,演示引擎将使用所选择的转换方式。 在部分平台上,这一操作可能会造成性能下降(可能不明显,但需要注意)。 在示例代码中,我不想进行任何转换,但必须检查是否支持转换。 如果不支持,我仅使用当前使用的相同转换。
// Sometimes images must be transformed before they are presented (i.e. due to device's orienation
// being other than default orientation)
// If the specified transform is other than current transform, presentation engine will transform image
// during presentation operation; this operation may hit performance on some platforms
// Here we don't want any transformations to occur so if the identity transform is supported use it
// otherwise just use the same transform as current transform
if( surface_capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR ) {
return VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
} else {
return surface_capabilities.currentTransform;
}
20.Tutorial02.cpp,函数 GetSwapChainTransform()
选择演示模式
演示模式决定图像在演示引擎内部处理以及在屏幕上显示的方式。 过去,从头到尾仅显示单个缓冲区。 如果在上面绘图,绘制操作(整个图像创建流程)是可视的。
双缓冲的推出,可防止查看绘制操作:一个图像显示,第二个用于渲染。 在演示过程中,第二个图像的内容拷贝至第一个图像(之前),或(之后)两个图像互换(还记得 OpenGL 应用中使用的 SwapBuffers() 函数吗?),这意味着它们的指示器已经互换。
撕裂是图像显示遇到的另一个问题,我们想避免这一问题,因此推出了等待垂直回扫信号的功能。 但等待又引发了另一问题:输入延迟。 因此双缓冲换成了三次缓冲,我们可以轮流绘制进两个后台缓冲区,而且在垂直同步期间,最新的一个用于演示。
这就是演示的目的所在:如何处理这些问题、如何在屏幕上演示图像,以及是否希望使用垂直同步。
目前主要有四种演示模式:
- IMMEDIATE。 演示请求立即采用,可能会出现撕裂问题(取决于每秒帧速率)。 在内部,演示引擎不使用任何保持交换链图像的队列。
- FIFO。 该模式与 OpenGL 的缓冲交换类似,交换间隔设为 1。 图像仅在垂直回扫期间显示(替换当前显示的图像),因此不会出现撕裂。 在内部,演示引擎使用带有“numSwapchainImages – 1”要素的队列。 演示请求附在队列的末尾。 回扫期间,队列开头的图像替换当前显示的图像,适用于应用。 如果所有图像都在队列中,那么应用必须等待垂直同步释放当前显示的图像。 只有这样,它才适用于渲染图像的应用和程序。 该模式必须始终适用于所有支持交换链扩展的 Vulkan 实施。
- FIFO RELAXED。 该模式与 FIFO 类似,但当图像的显示时间超过回扫时间时,将会立即释放,不等待另一垂直同步信号(因此如果渲染帧的频率低于屏幕刷新率,可能会出现撕裂问题)。
- MAILBOX。 在我看来,该模式与之前提到的三次缓冲最为相似。 图像仅在垂直回扫期间显示,不会出现图像撕裂问题。 但在内部,演示引擎使用仅带有一个要素的队列。 一个图像显示,另一个图像在队列中等待。 如果应用希望演示另一图像,它将不附在队列末尾,而是替换等待的那一个图像。 因此队列中总有最新生成的图像。 如果图像超过两个,就可以使用这种行为。 如果是两个图像,那么 MAILBOX 模式与 FIFO 类似(因为我们需要等待释放显示的图像,没有能够与队列中等待的图像进行互换的“空白”图像)。
使用哪种演示模式取决于欲执行的操作类型。如果想解码和播放电影,我们希望以相应的顺序显示所有的帧。 因此在我看来,FIFO 模式是最佳选择。 但如果我们创建游戏,通常希望显示最新生成的帧。 在这种情况下,我建议使用 MAILBOX,因为该模式不会出现撕裂问题,而且其输入延迟最低。 显示最新生成的图像,应用不需要等待垂直同步。 但是为了实现这种行为,必须创建至少三个图像,而且并不总支持这种模式。
FIFO 模式始终可用,并且要求支持两个图像,但导致应用需等待垂直同步(无论请求多少交换链图像)。 即时模式的速度最快 据我所知,它也要求两个图像,但不会让应用等待显示器刷新率。 它的劣势是会造成图像撕裂。 如何选择在于您,像往常一样,我们必须确保所选的演示模式受到支持。
之前我们查询了可用演示模式,因此现在必须寻找最能满足我们需求的模式。 以下是用于寻找 MAILBOX 模式的代码:
// FIFO present mode is always available
// MAILBOX is the lowest latency V-Sync enabled mode (something like triple-buffering) so use it if available
for( VkPresentModeKHR &present_mode : present_modes ) {
if( present_mode == VK_PRESENT_MODE_MAILBOX_KHR ) {
return present_mode;
}
}
for( VkPresentModeKHR &present_mode : present_modes ) {
if( present_mode == VK_PRESENT_MODE_FIFO_KHR ) {
return present_mode;
}
}
std::cout << "FIFO present mode is not supported by the swap chain!"<< std::endl;
return static_cast<VkPresentModeKHR>(-1);
21.Tutorial02.cpp,函数 GetSwapChainPresentMode()
创建交换链
现在我们拥有创建交换链需要的所有数据。 我们定义了所有所需的值,并确保它们满足特定平台的限制条件。
uint32_t desired_number_of_images = GetSwapChainNumImages( surface_capabilities );
VkSurfaceFormatKHR desired_format = GetSwapChainFormat( surface_formats );
VkExtent2D desired_extent = GetSwapChainExtent( surface_capabilities );
VkImageUsageFlags desired_usage = GetSwapChainUsageFlags( surface_capabilities );
VkSurfaceTransformFlagBitsKHR desired_transform = GetSwapChainTransform( surface_capabilities );
VkPresentModeKHR desired_present_mode = GetSwapChainPresentMode( present_modes );
VkSwapchainKHR old_swap_chain = Vulkan.SwapChain;
if( static_cast<int>(desired_usage) == -1 ) {
return false;
}
if( static_cast<int>(desired_present_mode) == -1 ) {
return false;
}
VkSwapchainCreateInfoKHR swap_chain_create_info = {
VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, // VkStructureType sType
nullptr, // const void *pNext
0, // VkSwapchainCreateFlagsKHR flags
Vulkan.PresentationSurface, // VkSurfaceKHR surface
desired_number_of_images, // uint32_t minImageCount
desired_format.format, // VkFormat imageFormat
desired_format.colorSpace, // VkColorSpaceKHR imageColorSpace
desired_extent, // VkExtent2D imageExtent
1, // uint32_t imageArrayLayers
desired_usage, // VkImageUsageFlags imageUsage
VK_SHARING_MODE_EXCLUSIVE, // VkSharingMode imageSharingMode
0, // uint32_t queueFamilyIndexCount
nullptr, // const uint32_t *pQueueFamilyIndices
desired_transform, // VkSurfaceTransformFlagBitsKHR preTransform
VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, // VkCompositeAlphaFlagBitsKHR compositeAlpha
desired_present_mode, // VkPresentModeKHR presentMode
VK_TRUE, // VkBool32 clipped
old_swap_chain // VkSwapchainKHR oldSwapchain
};
if( vkCreateSwapchainKHR( Vulkan.Device, &swap_chain_create_info, nullptr, &Vulkan.SwapChain ) != VK_SUCCESS ) {
std::cout << "Could not create swap chain!"<< std::endl;
return false;
}
if( old_swap_chain != VK_NULL_HANDLE ) {
vkDestroySwapchainKHR( Vulkan.Device, old_swap_chain, nullptr );
}
return true;
22.Tutorial02.cpp,函数 CreateSwapChain()
在本代码示例中,一开始我们收集了之前介绍的所有必要数据。 接下来创建类型 VkSwapchainCreateInfoKHR 的变量。 它包括以下成员:
- sType – 标准结构类型,此处必须为 VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR。
- pNext – 留作将来使用的指示器(用于指向此次扩展的部分扩展)。
- flags – 留作将来使用的值,目前必须设为 0。
- surface – 已创建的表示窗口系统(我们的应用窗口)的平面的句柄。
- minImageCount – 应用为交换链请求的图像的最小数量(必须满足提供的限制条件)。
- imageFormat –应用针对交换链图像选择的格式;必须是受支持的平面格式。
- imageColorSpace – 交换链图像的色域;仅列举的格式-色域配对的值可用于 imageFormat 和 imageColorSpace(不能使用一个配对的格式和另一配对的色域)。
- imageExtent – 以像素形式定义的交换链图像的大小(尺寸);必须满足提供的限制条件。
- imageArrayLayers – 定义交换链图像的层数(即视图);该数值通常为 1,但如果我们希望创建多视图或立体(立体 3D)图,可以将其设为更大的值。
- imageUsage – 定义应用如何使用图像;可包含仅支持用法的数值;通常支持 color attachment 用法。
- imageSharingMode – 在多个队列引用图像时描述图像共享模式(稍后将详细介绍)。
- queueFamilyIndexCount – 供引用交换链图像的不同队列家族数量;该参数仅在使用 VK_SHARING_MODE_CONCURRENT 共享模式时重要。
- pQueueFamilyIndices – 包含队列家族(将引用交换链图像)的索引阵列;必须至少包含 queueFamilyIndexCount 个要素,而且因为在 queueFamilyIndexCount 中,该参数仅在使用 VK_SHARING_MODE_CONCURRENT 共享模式时重要。
- preTransform – 在演示图像时应用于交换链图像的转换;必须是支持的数值之一。
- compositeAlpha – 该参数用于指示平面(图像)如何与相同窗口系统上的其他平面进行合成(混合?);该数值必须是平面功能中返回的值(位),但看起来似乎始终支持不透明合成(没有混合,阿尔法忽略)(因为大多数游戏希望使用这种模式)。
- presentMode – 交换链将使用的演示模式;只能选择支持的模式。
- clipped – 连接像素所有权;一般来说,如果应用不希望读取交换链图像(比如 ReadPixels()),应设为 VK_TRUE,因为它将支持部分平台使用更好的演示方法;在特定场景下使用 VK_FALSE 值(我了解更多信息后再来介绍这些场景)。
- oldSwapchain – 如果重新创建交换链,该参数将定义将之前的交换链替换为新创建的交换链。
那么使用这种共享模式会出现什么问题? 队列可以引用 Vulkan 中的图像。 这意味着我们可以创建命令来使用这些图像。 这些命令保存在命令缓冲区内,而这些缓冲区将提交至队列。 队列属于不同的队列家族。 Vulkan 要求我们声明队列家族的数量,以及其中哪些家族将通过提交至命令缓冲区中的命令引用这些图像。
如果希望,我们可以一次引用不同队列家族的图像。 在这种情况下,我们必须提供“并发”共享模式。 但这(可能)要求我们自己管理图像数据的一致性,即我们必须同步不同的阵列,同时确保图像中的数据合理而且不会出现任何不利影响 — 部分队列正在读取图像的数据,而其他队列还未完成写入。
我们可以不指定这些队列家族,只告诉 Vulkan 一次仅一个队列家族(一个家族的队列)引用图像。 这并不意味着其他队列不会引用这些图像。 仅意味着它们不会一次性同时进行。 因此如果我们希望引用一个家族的图像,然后引用另一家族的图像,那么必须明确告诉 Vulkan: “我的图像可在该队列家族内部使用,但从现在起其他家族(这个)将引用它。” 使用图像内存壁垒能够完成这种过渡。 当一次只有一个队列家族使用特定图像时,可以使用“专有”共享模式。
如果不满足这些要求,将出现未定义行为,而且我们无法信赖图像内容。
在本示例中,我们使用一个队列,因此不必指定“并发”共享模式,并使相关参数(queueFamilyCount 和 pQueueFamilyIndices)保持空白(或 null 或零值)状态。
现在我们可以调用 vkCreateSwapchainKHR()函数创建交换链并查看该操作是否成功。 之后(如果我们重新创建交换链,表示这不是第一次创建)我们应该毁坏之前的交换链。 稍后将对此进行介绍。
图像演示
现在我们有包含多个图像的交换链。 为将这些图像用作渲染对象,我们可以获取用交换链创建的所有图像的句柄,但不允许那样使用。 交换链图像属于交换链。 这意味着应用不能直接使用这些图像,必须发出请求。 还意味着图像由平台和交换链(而非应用)一起创建和毁坏。
因此如果应用希望渲染交换链图像或以其他的方式使用,必须首先通过请求交换链以访问该图像。 如果交换链要求等待,那么我们必须等待。 应用使用完图像后,应通过演示“返回”该图像。 如果忘记将图像返回至交换链,图像会很快用完,屏幕上将什么也不显示。
应用还可以请求一次访问更多图像,但它们必须处于可用状态。 获取访问可能要求等待。 在一些极端情况下,交换链中的图像不够,而应用希望访问多个图像,或者如果我们忘记将图像返回至交换链,应用甚至可能会无限期等待下去。
倘若(通常)至少有两个图像,我们还需要等待,这听起来似乎很奇怪,但这是有原因的。 并不是所有图像都可用于应用,因为演示引擎也要使用图像。 通常显示一个图像。 演示引擎可能需要使用其他图像,才能保持正常运行。 所以我们不能使用它们,因为这可能会在一定程度上阻碍演示引擎的运行。 我们不知道其内部机制和算法,也不知道供应用执行的操作系统的要求。 因此图像的可用性取决于多个因素:内部实施、操作系统、已创建图像数量、应用希望单次并以所选演示模式使用的图像数量(从本教程的角度来说,这是最重要的因素)。
在即时模式中,通常演示一个图像。 其他图像(至少一个)可用于应用。 当应用发布演示请求(“返回”图像),显示的图像将替换为新图像。 因此,如果创建两个图像,应用一次仅可使用一个图像。 如果应用请求其他图像,则必须“返回”之前的图像。 如果希望一次使用两个图像,则必须创建包含多个图像的交换链,否则将一直处于等待状态。 在即时模式中,如果我们请求多个图像,应用将一次请求(拥有)“imageCount – 1”个图像。
在 FIFO 模式中,显示一个图像,其余图像放在 FIFO 队列中。 该队列的长度通常等于“imageCount – 1”。 一开始,所有图像可能都可用于应用(因为队列是空的,没有任何图像)。 当应用演示图像(将其“返回”至交换链)时,该图像将附在队列末尾。 因此,队列变满后,应用需要等待其他图像,直至垂直回扫阶段释放出所显示的图像。 图像的显示顺序通常与应用的演示顺序相同。 如果出现垂直同步信号,该队列的第一个图像将替换显示的图像。 之前显示的图像(释放的图像)可用于应用,因为它成了未使用的图像(不演示,也不在队列中等待)。 如果所有的图像都在列队中,应用将等待下一个回扫期以访问其他图像。 如果渲染时间长于刷新率,应用将不需要等待。 如果有多个图像,该行为不会变化。 内部交换链队列通常包含“imageCount – 1”个要素。
当前最后一个可用的模式是 MAILBOX。 如前所述,该模式与“传统”三次缓冲最为类似。 通常显示一个图像。 第二个图像在单要素队列中等待(通常仅有容纳一个要素的位置)。 其他图像可用于应用。 应用演示图像时,该图像将替换在队列中等待的那个图像。 队列中的图像仅在回扫期间显示,但应用无需等待下一个图像(如果有超过两个图像)。 只有两个图像时,MAILBOX 模式的运行方式与 FIFO 模式相同 — 应用必须等待垂直同步信号以获取下一个图像。 但如果有至少三个图像,它可立即获取由“演示”图像(队列中等待的图像)替换的那个图像。 这就是我所请求的图像比最小值多一个的原因。 如果 MAILBOX 模式可用,我希望以与三次缓冲相同的方式使用该模式(可能第一件事是检查哪种模式可用,然后根据所选的演示模式选择交换链图像的数量)。
我希望这些示例可帮助您了解,如果应用希望使用任意图像时,为什么必须请求图像。 在 Vulkan 中,我们只能执行允许和要求的行为 — 不能太少,也不能太多。
uint32_t image_index;
VkResult result = vkAcquireNextImageKHR( Vulkan.Device, Vulkan.SwapChain, UINT64_MAX, Vulkan.ImageAvailableSemaphore, VK_NULL_HANDLE, &image_index );
switch( result ) {
case VK_SUCCESS:
case VK_SUBOPTIMAL_KHR:
break;
case VK_ERROR_OUT_OF_DATE_KHR:
return OnWindowSizeChanged();
default:
std::cout << "Problem occurred during swap chain image acquisition!"<< std::endl;
return false;
}
23.Tutorial02.cpp,函数 Draw()
为获取图像,我们必须调用 vkAcquireNextImageKHR()函数。 在调用期间,我们必须指定(除像其他函数中的设备句柄外)交换链,以通过其使用图像、超时 (timeout)、旗语和围栏对象。 如果成功,函数将图像索引保存在我们提供地址的变量中。 为何保存索引,而不保存图像(句柄)本身? 这种行为非常方便(即在“预处理”阶段,当我们希望为渲染准备尽可能多的数据时,这样在典型帧渲染期间就不会浪费时间),不过这点我们稍后讨论。 只需记住,我们可以查看交换链中创建了哪些图像(需得到允许才能使用)。 进行这种查询时,会提供图像阵列。 而且 vkAcquireNextImageKHR()函数将索引保存在该阵列中。
我们必须指定超时,因为有时候图像不能立即使用。 未得到允许就尝试使用图像会导致未定义行为。 指定超时可为演示引擎提供反应时间。 如果需要,可以等待下一垂直回扫期,而且我们提供时间。 因此如果没超过指定时间,该函数将会中断。 我们可提供最大可用值,因此该函数可能会无限期中断下去。 如果我们提供的超时为 0,该函数会立即返回。 如果调用时图像可用,将会立即提供图像。 如果没有可用图像,将会返回错误,提示图像尚未准备就绪。
获取图像后,我们可以任意使用该图像。 通过保存在命令缓冲区的命令,可以处理或引用图像。 我们可以事先准备命令缓冲区(以节省渲染的处理过程)并在此使用或提交。 或者,我们还可以现在准备命令,完成后进行提交。 在 Vulkan 中,创建命令缓冲区并将其提交至队列是支持设备执行操作的唯一方式。
命令缓冲区提交至队列后,所有命令开始处理。 但如果没有得到允许,队列不能使用图像,我们之前创建的旗语用于内部队列同步 — 队列开始处理引用特定图形的命令之前,应等待旗语(直到获得信号)。 但这种等待不会中断应用。 用于访问交换链图像的同步机制有两种: (1) 超时 — 可能会中断应用,但不会中止队列处理;(2) 旗语 — 不会中断应用,但会中断所选的队列。
现在我们(从理论上来说)了解了如何(通过命令缓冲区)进行渲染。 现在我们想像一下,我们正在命令缓冲区内部提交,部分渲染操作已在执行。 但开始处理之前,我们应让队列(执行渲染的地方)等待。 这一过程在提交操作中完成。
VkPipelineStageFlags wait_dst_stage_mask = VK_PIPELINE_STAGE_TRANSFER_BIT;
VkSubmitInfo submit_info = {
VK_STRUCTURE_TYPE_SUBMIT_INFO, // VkStructureType sType
nullptr, // const void *pNext
1, // uint32_t waitSemaphoreCount&Vulkan.ImageAvailableSemaphore, // const VkSemaphore *pWaitSemaphores&wait_dst_stage_mask, // const VkPipelineStageFlags *pWaitDstStageMask;
1, // uint32_t commandBufferCount&Vulkan.PresentQueueCmdBuffers[image_index], // const VkCommandBuffer *pCommandBuffers
1, // uint32_t signalSemaphoreCount&Vulkan.RenderingFinishedSemaphore // const VkSemaphore *pSignalSemaphores
};
if( vkQueueSubmit( Vulkan.PresentQueue, 1, &submit_info, VK_NULL_HANDLE ) != VK_SUCCESS ) {
return false;
}
24.Tutorial02.cpp,函数 Draw()
首先我们准备一个结构,其中包含欲提交至队列的操作类型的信息。 这可通过 VkSubmitInfo 结构来完成。 它包含以下字段:
- sType – 标准结构类型;此处必须设置为 VK_STRUCTURE_TYPE_SUBMIT_INFO。
- pNext – 留作将来使用的标准指示器。
- waitSemaphoreCount – 我们希望队列在开始处理命令缓冲区的命令之前所等待的旗语数量。
- pWaitSemaphores – 包含队列应等待的旗语句柄的阵列指示器;该阵列必须包含至少 waitSemaphoreCount 个要素。
- pWaitDstStageMask – 要素数量与 pWaitSemaphores 阵列相同的阵列指示器;它描述每个(相应)旗语等待将出现时所处的管道阶段;在本示例中,该队列可在开始使用交换链的图像之前执行部分操作,因此不会中断所有操作;该队列可开始处理部分绘制命令,而且必须等待管道进入使用图像的阶段。
- commandBufferCount – 提交以待执行的命令缓冲区数量。
- pCommandBuffers – 包含命令缓冲区句柄的阵列(必须包含至少 commandBufferCount 个要素)的指示器。
- signalSemaphoreCount – 我们希望队列在处理完所有提交的命令缓冲区后发出信号的旗语数量。
- pSignalSemaphores – 至少包含 signalSemaphoreCount 个要素和旗语句柄的阵列指示器;队列处理完在该提交信息内提交的命令后,将向这些旗语发出信号。
在本示例中,我们告诉队列仅等待一个旗语,其信号由演示引擎在队列安全开始处理引用交换链图像的命令时发出。
而且,我们仅提交一个简单的命令缓冲区。 该缓冲区是之前已经准备好的(稍后将介绍如何准备)。 它仅清空获取的图像。 但这已经足够我们查看在应用窗口中选择的颜色,以及交换链是否正常运行。
在上述代码中,命令缓冲区安排在阵列(更准确的说是矢量)中。 为简化提交相应命令缓冲区(引用当前获取的图像)的流程,我为每个交换链图像准备了一个单独的命令缓冲区。 此处可使用 vkAcquireNextImageKHR()函数提供的图像索引。 (在类似场景中)使用图像句柄要求创建地图,以将句柄转换成特定命令缓冲区或索引。 另一方面,仅选择一个特定阵列要素时,我们可使用范数。 因此该函数为我们提供的是索引,而非图像句柄。
提交命令缓冲区后,所有处理过程在后台“硬件”上开始进行。 下一步我们希望演示渲染的图像。 演示表示我们希望显示图像并将其“交还”至交换链。 用于完成这一步骤的代码如下所示:
VkPresentInfoKHR present_info = {
VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, // VkStructureType sType
nullptr, // const void *pNext
1, // uint32_t waitSemaphoreCount&Vulkan.RenderingFinishedSemaphore, // const VkSemaphore *pWaitSemaphores
1, // uint32_t swapchainCount&Vulkan.SwapChain, // const VkSwapchainKHR *pSwapchains&image_index, // const uint32_t *pImageIndices
nullptr // VkResult *pResults
};
result = vkQueuePresentKHR( Vulkan.PresentQueue, &present_info );
switch( result ) {
case VK_SUCCESS:
break;
case VK_ERROR_OUT_OF_DATE_KHR:
case VK_SUBOPTIMAL_KHR:
return OnWindowSizeChanged();
default:
std::cout << "Problem occurred during image presentation!"<< std::endl;
return false;
}
return true;
25.Tutorial02.cpp,函数 Draw()
通过调用 vkQueuePresentKHR()函数演示图像。 这种感觉像是,提交缓冲区只进行一项操作:演示。
要演示图像,我们必须指定演示多少个图像,以及哪个交换链的哪些图像。 我们可一次性演示多个交换链的图像(即演示给多个窗口),但一次只可演示一个交换链的一个图像。 我们通过 VkPresentInfoKHR 结构提供这类信息,该结构包含以下字段:
- sType – 标准结构类型;此处必须设置为 VK_STRUCTURE_TYPE_PRESENT_INFO_KHR。
- pNext – 留作将来使用的参数。
- waitSemaphoreCount – 演示图像之前希望队列等待的旗语数量。
- pWaitSemaphores – 包含队列应等待的旗语句柄的阵列指示器;该阵列必须包含至少 waitSemaphoreCount 个要素。
- swapchainCount – 我们希望演示其图像的交换链数量。
- pSwapchains – 带有 swapchainCount 个要素,包含我们希望演示其图像的所有交换链的句柄的阵列;单个交换链仅在此阵列中出现一次。
- imageIndices – 带有 swapchainCount 个要素,包含我们希望演示的图像的索引的阵列;阵列中的每个要素都对应 pSwapchains 阵列中的一个交换链;图像索引是阵列中每个交换链图像的索引(请参阅下一节)。
- pResults – 包含至少 swapchainCount 个要素的阵列的指示器;该参数为可选项,可设置为 null,但如果我们提供此阵列,演示操作的结果将按照交换链分别保存在各要素中;整个函数返回的单个值与所有交换链的最差结果值相同。
该结构已准备好,现在我们可以用它来演示图像。 在本示例中,我只演示一个交换链的一个图像。
通过调用 vkQueue…()函数执行(或提交)的每次操作都附在待处理队列的末尾。 按照提交的顺序依次处理各项操作。 我们在提交其他命令缓冲区后开始演示图像。 因此演示队列在处理完所有命令缓冲区后才开始演示图像。 这样可确保图像将在我们使用(渲染)后演示,而且内容正确的图像将显示在屏幕上。 但在本示例中,我们向同一个队列 PresentQueue 提交绘制(清空)操作和演示操作。 我们只执行允许在演示队列上执行的简单操作。
如果想在队列上执行与演示操作不同的绘制操作,我们需要同步这些队列。 也可通过旗语完成,因此我们创建了两个旗语(本示例可能不需要使用第二个旗语,因为我们使用同一个队列渲染和演示图像,我是想展示如何正确地完成这一步骤)。
第一个旗语用于演示引擎告诉队列,它可以安全使用(引用/渲染)图像。 第二个旗语供我们使用。 图像操作(渲染)完成后向该旗语发出信号。 提交信息结构有一个名为 pSignalSemaphores 的字段。 它是旗语句柄阵列,在处理完所有提交的命令缓冲区后收到信号。 因此我们需要让第二个队列等待这第二个旗语。 我们将第二个旗语的句柄保存在 VkPresentInfoKHR 结构的 pWaitSemaphores 字段中。 这样由于有第二个旗语,我们提交演示操作的队列将等待特定图像渲染完成。
好了,就是这样。 我们使用 Vulkan 显示了第一个图像!
查看在交换链中创建的图像
之前我提到过交换链的图像索引。 在本代码实例中,我将对此具体介绍。
uint32_t image_count = 0;
if( (vkGetSwapchainImagesKHR( Vulkan.Device, Vulkan.SwapChain, &image_count, nullptr ) != VK_SUCCESS) ||
(image_count == 0) ) {
std::cout << "Could not get the number of swap chain images!"<< std::endl;
return false;
}
std::vector<VkImage> swap_chain_images( image_count );
if( vkGetSwapchainImagesKHR( Vulkan.Device, Vulkan.SwapChain, &image_count, &swap_chain_images[0] ) != VK_SUCCESS ) {
std::cout << "Could not get swap chain images!"<< std::endl;
return false;
}
2 6. -
本代码示例是虚构出来,用于查看交换链中创建了多少以及哪些图像的函数的一个片段。 通过传统“两次调用”完成,这次使用的是 vkGetSwapchainImagesKHR()函数。 首先我们调用该函数,其最后一个参数设为 null。 这样交换链中已创建图像数量将保存在“image_count”变量中,并且我们知道需要为所有图像的句柄准备多少存储。 第二次调用该函数时,我们在通过最后一个参数提供地址的阵列中取得句柄。
现在我们知道交换链正在使用的所有图像。 对 vkAcquireNextImageKHR()函数和 VkPresentInfoKHR 结构来说,我引用的索引都是该阵列(通过 vkGetSwapchainImagesKHR()函数返回的阵列)中的索引。 它称为交换链可演示图像的阵列。 在有交换链的情况下,如果函数希望提供或返回索引,该索引将是这一阵列中的图像索引。
重新创建交换链
之前我提到过多次,我们必须重新创建交换链,而且我还说过,之前的交换链必须毁坏。 vkAcquireNextImageKHR()和 vkQueuePresentKHR()函数返回的结果有时会导致调用 OnWindowSizeChanged()函数。 该函数可重新创建交换链。
有时交换链会过期。 这意味着平面、平台或应用窗口的属性已发生了变化,当前交换链无法再使用。 最明显的(但可惜不是太好)示例是窗口大小的更改。 我们不能创建交换链图像,也不能改变大小。 唯一的可能就是毁坏并重新创建交换链。 还有一些我们仍能使用交换链的情况,但可能不再适用于为其创建的平面。
vkAcquireNextImageKHR()和 vkQueuePresentKHR()函数的返回代码会通知这些情况。
返回 VK_SUBOPTIMAL_KHR 值时,我们仍然可以将当前交换链用于演示。 它仍然可以使用,但不处于最佳状态(即色彩精度有所降低)。 如果有可能,建议重新创建交换链。 其中一个很好的示例就是执行性能严苛型渲染的时候,而且获取图像后,我们得知图像并不处于最佳状态。 我们不希望浪费这一处理过程,用户需要花很长时间等待另一个帧。 我们仅演示该图像并抓住机会重新创建交换链。
返回 VK_ERROR_OUT_OF_DATE_KHR 时,我们不能使用当前的交换链,必须立即重新创建。 我们不能使用当前交换链演示图像;该操作将失败。 我们必须尽快重新创建交换链。
我说过,关于平面属性更改,之后应重新创建交换链,更改窗口大小是最明显的(但不是最好的)的示例。 在这种情况下,我们应该重新创建交换链,但不会有之前提到的返回代码告知我们这一情况。 我们应该自己使用特定于操作系统的代码,监控窗口大小的变化。 而且这就是为什么在我们的资源中,该函数的名称为 OnWindowSizeChanged。 只要窗口大小发生变化,就要调用该函数。 但由于该函数仅重新创建交换链(和命令缓冲区),此处可调用相同的函数。
重新创建的方法与创建时相同。 有一个结构成员,我们在其中提供应被新交换链替换的交换链。 但创建新交换链后,我们必须毁坏之前的交换链。
快速了解命令缓冲区
现在您已了解了许多关于交换链的信息,但还有一点需要了解。 为解释这一点,我简要展示一下如何准备绘制命令。 关于交换链,最重要的一点是连接绘制和准备命令缓冲区。 我仅介绍如何清空图像,但这足以查看我们的交换链是否正常运行。
在教程 1 中,我介绍了队列和队列家族。 如果想在设备上执行命令,需通过命令缓冲区将它们提交至队列。 换句话说,命令封装在命令缓冲区内。 缓冲区提交至队列后,设备开始处理记录其中的命令。 还记得 OpenGL 的绘制列表吗? 我们可准备命令列表,以便以绘制命令列表的形式绘制几何图形。 Vulkan 中的情况类似,但更加灵活和高级。
创建命令缓冲区内存池
命令缓冲区需要一部分存储保存命令。 如果要为命令提供空间,我们可创建一个池,缓冲区可向该池分配内存。 我们不必指定空间量 — 缓冲区建立(记录)时动态分配。
请记住,命令缓冲区只能提交至相应的队列家族,只有兼容特定家族的操作类型才能提交至特定队列。 此外,命令缓冲区本身不连接任何队列或队列家族,但缓冲区分配内存的内存池连接。 因此每个从特定池获取内存的命令缓冲区只能提交至相应队列家族的队列 — 通过其(内部)创建内存池的家族。 如果通过特定家族创建多个队列,我们可将命令缓冲区提交至任意队列;此时家族索引最为重要。
VkCommandPoolCreateInfo cmd_pool_create_info = {
VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, // VkStructureType sType
nullptr, // const void* pNext
0, // VkCommandPoolCreateFlags flags
Vulkan.PresentQueueFamilyIndex // uint32_t queueFamilyIndex
};
if( vkCreateCommandPool( Vulkan.Device, &cmd_pool_create_info, nullptr, &Vulkan.PresentQueueCmdPool ) != VK_SUCCESS ) {
std::cout << "Could not create a command pool!"<< std::endl;
return false;
}
27.Tutorial02.cpp,函数 CreateCommandBuffers()
为创建命令缓冲区池,我们调用 vkCreateCommandPool()函数。 它要求我们提供结构类型 VkCommandPoolCreateInfo 的(地址)变量。 它包含以下成员:
- sType – 常用结构类型,此处必须相当于 VK_STRUCTURE_TYPE_CMD_POOL_CREATE_INFO。
- pNext – 留作将来使用的指示器。
- flags – 留作将来使用的值。
- queueFamilyIndex – (为其创建池的队列家族的索引。
对测试应用来说,我们仅使用演示家族的一个队列,因此应该使用它的索引。 现在我们调用 vkCreateCommandPool()函数并查看是否成功。 如果成功,命令池的句柄将保存在我们之前提供了地址的变量中。
分配命令缓冲区
接下来,我们需要分配命令缓冲区。 命令缓冲区不通过常见的方式创建,而是从池中分配。 从池对象获取内存的其他对象也分配(池自己创建)。 因此 vkCreate…() 和 vkAllocate…() 函数的名字相互分开。
如前所述,我分配一个以上的命令缓冲区 — 分别对应绘制命令引用的每个交换链图像。 因此每次获取交换链图像时,就可以提交/使用相应的命令缓冲区。
uint32_t image_count = 0;
if( (vkGetSwapchainImagesKHR( Vulkan.Device, Vulkan.SwapChain, &image_count, nullptr ) != VK_SUCCESS) ||
(image_count == 0) ) {
std::cout << "Could not get the number of swap chain images!"<< std::endl;
return false;
}
Vulkan.PresentQueueCmdBuffers.resize( image_count );
VkCommandBufferAllocateInfo cmd_buffer_allocate_info = {
VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, // VkStructureType sType
nullptr, // const void* pNext
Vulkan.PresentQueueCmdPool, // VkCommandPool commandPool
VK_COMMAND_BUFFER_LEVEL_PRIMARY, // VkCommandBufferLevel level
image_count // uint32_t bufferCount
};
if( vkAllocateCommandBuffers( Vulkan.Device, &cmd_buffer_allocate_info, &Vulkan.PresentQueueCmdBuffers[0] ) != VK_SUCCESS ) {
std::cout << "Could not allocate command buffers!"<< std::endl;
return false;
}
if( !RecordCommandBuffers() ) {
std::cout << "Could not record command buffers!"<< std::endl;
return false;
}
return true;
28.Tutorial02.cpp,函数 CreateCommandBuffers()
首先我们需要知道创建了多少个交换链图像(一个交换链创建的图像可超过我们指定的数量)。 这一点之前已有介绍。 我们调用 vkGetSwapchainImagesKHR()函数,其中最后一个参数设为 null。 现在我们不需要图像句柄,只需要它们的总数。 然后我们为相应数量的命令缓冲区准备一个阵列(矢量),然后我们可以创建相应数量的命令缓冲区。 为实施该步骤,我们调用 vkAllocateCommandBuffers()函数。 它要求我们准备类型 VkCommandBufferAllocateInfo 的结构化变量,其中包含以下字段:
- sType – 结构类型,此时应为 VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO。
- pNext – 留作将来使用的正态参数。
- commandPool – 供缓冲区在命令记录期间分配内存的命令池。
- level – 命令缓冲区的类型(级别)。 包含两个级别:主要和次要。 次要命令缓冲区仅通过主要命令缓冲区引用(使用)。 因为我们没有其他缓冲区,因此这里我们需要创建主要缓冲区。
- bufferCount – 我们希望一次性创建的命令缓冲区数量。
调用 vkAllocateCommandBuffers()函数后,需要查看缓冲器创建是否成功。 如果是,我们分配命令缓冲区,并准备记录一些(简单的)命令。
记录命令缓冲区
命令记录是我们在 Vulkan 中执行的最重要的操作。 记录本身要求我们提供许多信息。 信息越多,绘制命令越复杂。
(在本教程中)记录命令缓冲区要求以下变量:
uint32_t image_count = static_cast<uint32_t>(Vulkan.PresentQueueCmdBuffers.size());
std::vector<VkImage> swap_chain_images( image_count );
if( vkGetSwapchainImagesKHR( Vulkan.Device, Vulkan.SwapChain, &image_count, &swap_chain_images[0] ) != VK_SUCCESS ) {
std::cout << "Could not get swap chain images!"<< std::endl;
return false;
}
VkCommandBufferBeginInfo cmd_buffer_begin_info = {
VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, // VkStructureType sType
nullptr, // const void *pNext
VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT, // VkCommandBufferUsageFlags flags
nullptr // const VkCommandBufferInheritanceInfo *pInheritanceInfo
};
VkClearColorValue clear_color = {
{ 1.0f, 0.8f, 0.4f, 0.0f }
};
VkImageSubresourceRange image_subresource_range = {
VK_IMAGE_ASPECT_COLOR_BIT, // VkImageAspectFlags aspectMask
0, // uint32_t baseMipLevel
1, // uint32_t levelCount
0, // uint32_t baseArrayLayer
1 // uint32_t layerCount
};
29.Tutorial02.cpp,函数 RecordCommandBuffers()
首先我们获取所有交换链图像的句柄,用于绘制命令(我们仅将其清空至一种颜色,不过我们将要使用它们)。 我们知道了图像的数量,因此不必再次请求。 调用 vkGetSwapchainImagesKHR()函数后,图像句柄保存在矢量中。
接下来我们需要准备结构化类型 VkCommandBufferBeginInfo 的变量。 它包含较多典型渲染场景(比如渲染通道)所需的信息。 这里不进行这些操作,因此我们将几乎全部参数都设为 0 或 null。 但为清楚起见,该结构包含以下字段:
- sType – 标准类型,此次必须设为 VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO。
- pNext – 留作将来使用的指示器,保留为 null。
- flags – 定义命令缓冲区首选用法的参数。
- pInheritanceInfo – 指示另一用于较多典型渲染场景的结构的参数。
命令缓冲区收集命令。 为将命令保存在命令缓冲区中,我们将它们记录下来。 以上结构提供一些必要的信息,供驱动程序准备和优化记录流程。
在 Vulkan 中,命令缓冲区分为主要缓冲区和次要缓冲区两种。 主要命令缓冲区是与绘制列表类似的常用命令缓冲区。 它们是独立的“个体”,(仅)提交至队列。 次要命令缓冲区还保存也可保存命令(我们也记录它们),但主要通过主要命令缓冲区引用(我们从主要命令缓冲区中调用次要命令缓冲区,就像从另一绘制列表调用 OpenGL 的绘制列表)。 不能将次要命令缓冲区直接提交至队列。
在下一节中,我们将详细介绍这些信息。
在这一简单示例中,我们希望用单个值清空图像。 因此接下来设置用于清空的颜色。 您可以选择任意值。 我使用淡橘色。
上述代码中的最后一个变量指定了我们执行操作的图像部分。 图像仅包含一个 mipmap 层和一个阵列层(没有立体缓冲区等)。 我们相应地设置 VkImageSubresourceRange 结构中的值。 该结构包含以下字段:
- aspectMask – 将图像用作颜色渲染对象(它们有“颜色”格式)时,根据图像格式,指定此处的“color aspect”。
- baseMipLevel – 将访问(修改)的第一个 mipmap 层。
- levelCount – 执行操作的 mipmap 层(包括基础层)数量。
- baseArrayLayer – 将访问(修改)的第一个阵列层。
- arraySize – 执行操作的层级(包括基础层)数量。
我们准备记录一些缓冲区。
图像布局和布局过渡
(类型 VkImageSubresourceRange的)上述代码示例所需的最后一个变量指定执行操作的图像部分。 在本课程中我们仅清空图像。 但我们还需执行资源过渡。 还记得创建交换链之前为交换链图像选择用法的代码吗? 图像可用于不同的目的。 它们可用作渲染对象、在着色器中取样的纹理,或用于拷贝/blit 操作(数据传输)的数据源。 在为不同类型的操作创建图像时,我们必须指定不同的用法标记。 我们可以指定更多用法标记(如果支持;“color attachment”用法通常可用于交换链)。 但我们要做的不仅仅是指定图像用法。根据操作类型,图像可能以不同的方式分配,或在内存中呈现不同的布局。 每种图像操作类型可能与不同的“图像布局”有关。 我们可以支持所有操作支持的通用布局,但可能无法提供最佳性能。 对特定用法来说,我们应使用专用布局。
如果创建图像时考虑了不同的用法,并希望执行不同的操作,那么在执行每种操作之前,必须改变图像的当前布局。 为此,我们必须将当前局部过渡至兼容待执行操作的另一种布局。
我们创建的图像(通常)以未定义布局的形式创建,因此如果希望使用该图像,我们必须将其过渡至另一种布局。 但交换链创建的图像有 VK_IMAGE_LAYOUT_PRESENT_SOURCE_KHR 布局。 顾名思义,该布局针对演示引擎使用(演示)(即在屏幕上显示)的图像而设计。 因此,如果我们向在交换链图像上执行部分操作,需要将它们的布局更改成兼容所需操作的布局。 处理完图像(渲染图像)后,我们需要将布局过渡回 VK_IMAGE_LAYOUT_PRESENT_SOURCE_KHR。 否则演示引擎将无法使用这些引擎,而且会出现未定义行为。
进行布局过渡时,可使用图像内存壁垒。 我们用它们指定(当前)即将淘汰的旧布局和即将过渡到的新布局。 旧布局必须为当前或未定义的布局。 如果旧布局指定为未定义布局,那么在过渡过程中必须丢弃图像内容。 这样有助于驱动程序执行优化。 如果想保存图像内容,那么必须将布局指定为当前布局。
上述代码示例中类型 VkImageSubresourceRange 的最后一个变量也可用于图像过渡。 它可定义哪“部分”图像将改变布局,而且在准备图像内存壁垒时需要该变量。
记录命令缓冲区
最后一步是为每个交换链图像记录一个命令缓冲区。 我们希望将图像清空至任意一种颜色。 但首先需要改变图像布局,并在完成后恢复布局。 此处完成这一步的代码如下:
for( uint32_t i = 0; i < image_count; ++i ) {
VkImageMemoryBarrier barrier_from_present_to_clear = {
VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, // VkStructureType sType
nullptr, // const void *pNext
VK_ACCESS_MEMORY_READ_BIT, // VkAccessFlags srcAccessMask
VK_ACCESS_TRANSFER_WRITE_BIT, // VkAccessFlags dstAccessMask
VK_IMAGE_LAYOUT_UNDEFINED, // VkImageLayout oldLayout
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, // VkImageLayout newLayout
Vulkan.PresentQueueFamilyIndex, // uint32_t srcQueueFamilyIndex
Vulkan.PresentQueueFamilyIndex, // uint32_t dstQueueFamilyIndex
swap_chain_images[i], // VkImage image
image_subresource_range // VkImageSubresourceRange subresourceRange
};
VkImageMemoryBarrier barrier_from_clear_to_present = {
VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, // VkStructureType sType
nullptr, // const void *pNext
VK_ACCESS_TRANSFER_WRITE_BIT, // VkAccessFlags srcAccessMask
VK_ACCESS_MEMORY_READ_BIT, // VkAccessFlags dstAccessMask
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, // VkImageLayout oldLayout
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, // VkImageLayout newLayout
Vulkan.PresentQueueFamilyIndex, // uint32_t srcQueueFamilyIndex
Vulkan.PresentQueueFamilyIndex, // uint32_t dstQueueFamilyIndex
swap_chain_images[i], // VkImage image
image_subresource_range // VkImageSubresourceRange subresourceRange
};
vkBeginCommandBuffer( Vulkan.PresentQueueCmdBuffers[i], &cmd_buffer_begin_info );
vkCmdPipelineBarrier( Vulkan.PresentQueueCmdBuffers[i], VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier_from_present_to_clear );
vkCmdClearColorImage( Vulkan.PresentQueueCmdBuffers[i], swap_chain_images[i], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear_color, 1, &image_subresource_range );
vkCmdPipelineBarrier( Vulkan.PresentQueueCmdBuffers[i], VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier_from_clear_to_present );
if( vkEndCommandBuffer( Vulkan.PresentQueueCmdBuffers[i] ) != VK_SUCCESS ) {
std::cout << "Could not record command buffers!"<< std::endl;
return false;
}
}
return true;
30.Tutorial02.cpp,函数 RecordCommandBuffers()
该代码放在循环内。 我们为每个交换链图像记录一个命令缓冲区。 因此我们需要大量图像。 这里也需要图像句柄。 我们需要在图像清空期间为图像内存壁垒指定这些句柄。 不过大家回忆一下,我之前说过,必须得到允许,获取交换链图像后,才能使用该图像。 没错,但我们这里不使用这些图像。 我们仅准备命令。 将操作(命令缓冲区)提交至队列时执行用法。 这里我们仅告知 Vulkan,未来拿这张图片这样做,然后那样做......等等。 这样,在我们开始主渲染循环之前,做尽可能多的准备工作,从而在真实渲染迁建避免切换、ifs、跳跃和其他分支。 在现实生活中这一场景并不简单,但我希望能够通过示例解释清除。
在上述代码中,我们首先准备两个图像内存壁垒。 内存壁垒用于改变图像中的三个不同元素。 从教程的角度来看,现在感兴趣的只有布局,但我们需要适当设置所有字段。 为设置内存壁垒,我们需要准备类型变量 VkImageMemoryBarrier,其中包含以下字段:
- sType – 标准类型,此处必须设置为 VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER。
- pNext – 保留为 null 状态,目前暂不使用的指示器。
- srcAccessMask – 壁垒前在图像上完成的内存操作类型。
- dstAccessMask – 壁垒后进行的内存操作类型。
- oldLayout – 即将转换出的布局;并始终相当于当前布局(在本示例中面向第一个壁垒,应为VK_IMAGE_LAYOUT_PRESENT_SOURCE_KHR)。或者我们还可以使用未定义布局,支持驱动程序执行部分优化,但图像内容可能会被丢弃。 既然我们不需要内容,那么我们可以使用此处的未定义布局。
- newLayout – 兼容我们将在壁垒后执行的操作的布局;我们希望清空图像;为此我们需要指定 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL 布局。 我们应一直使用特定的专用布局。
- srcQueueFamilyIndex – 之前引用图像的队列家族索引。
- dstQueueFamilyIndex – 将在壁垒后引用图像的队列的家族索引(指我之前所述的交换链共享模式)。
- image – 图像句柄。
- subresourceRange – 描述我们希望执行过渡的图像部分的结构;也就是之前代码示例的最后一个变量。
关于访问掩码和家族索引,需注意几点。 在本示例中,在第一个壁垒前和第二个壁垒后,仅演示引擎可访问图像。 演示引擎仅读取图像(不更改图像),因为我们将第一个壁垒中的 srcAccessMask 和第二个壁垒中的 dstAccessMask 设置为 VK_ACCESS_MEMORY_READ_BIT。 这表示以图像相关的图像为只读模式(在第一个壁垒前和第二个壁垒后,不更改能图像内容)。 在命令缓冲区中,我们仅清空图像。 该操作属于所谓的“转移”操作。 因此我们将第一个壁垒的 dstAccessMask 字段和第二个壁垒中的 srcAccessMask 字段设为 VK_ACCESS_TRANSFER_WRITE_BIT。
我不详细介绍队列家族索引,但如果用于图形操作的队列和演示相同,srcQueueFamilyIndex 和 dstQueueFamilyIndex 将相同,且硬件不会对图像访问队列的行为作出任何更改。 但请记住,我们已经指定,一次只有一个队列可以访问/使用图像。 因此如果队列不同,我们会将“所有权”更改通知给此处的硬件,即现在有不同的队列访问图像。 而且您此刻需要这些信息,以对壁垒进行相应的设置。
我们需要创建两个壁垒:一个将布局从“present source”(或未定义)改为“transfer dst”。 该壁垒用在命令缓冲区的开头,如果之前的演示引擎使用过图像,我们现在希望使用并更改它。 第二个壁垒用于当我们使用完图像并将其交还给交换链时,将布局恢复成“present source”。 该壁垒在命令缓冲区末尾设置。
现在我们已准备好通过调用 vkBeginCommandBuffer()函数开始记录命令。 我们提供命令缓冲区句柄和类型变量 VkCommandBufferBeginInfo 的地址,并开始进行。 接下来设置壁垒,以更改图像布局。 我们调用函数,它包含多个参数,但在本示例中,相关的参数只有第一个(命令缓冲区句柄)和最后两个:阵列要素数量和包含类型 VkImageMemoryBarrier 的变量地址的阵列的第一个要素的指示器。 该阵列的要素描述图像、其组成部分,以及应进行的过渡类型。 我们可以在壁垒后安全执行任何有关交换链图像,且兼容已过渡图像的布局的操作。 通用布局兼容所有操作,但性能(可能)有所下降。
在本示例中,我们仅清空图像,因此调用 vkCmdClearColorImage()函数。 它提取命令缓冲区句柄、图像句柄、图像的当前布局、带有清晰色彩值的变量的指示器、子资源数量(最后一个参数的数量的要素数量),以及类型 VkImageSubresourceRange 的变量的指示器。 最后阵列中的要素指定我们希望清空的图像部分(如果不想,我们可以不清空图像的所有 mipmap 或阵列层)。
在记录会话的结尾部分,我们设置另一个壁垒,将图像布局过渡回“present source”布局。 它是唯一一个兼容演示引擎执行的演示操作的布局。
现在我们调用 vkEndCommandBuffer()函数,通知我们已结束记录命令缓冲区。 如果在记录期间出错,该函数将通过返回值向我们通知出现错误。 如果出现错误,我们将无法使用命令缓冲器,并需要重新记录。 如果一切正常,我们将可以使用命令缓冲区,只需将缓冲区提交至阵列,就可告诉设备执行保存在其中的操作。
教程 2 执行
在本示例中,如果一切正常,我们将看到一个显示淡橙色的窗口。 窗口内容将如下所示:
清空
现在您已知道如何创建交换链、在窗口中显示图像,并在设备上执行简单的操作。 我们已创建命令缓冲区、记录它们,并在屏幕上显示它们。 但在关闭应用之前,我们需要清空所使用过的资源。 在本教程中我们将清空过程分成两个函数: 第一个函数仅清空(毁坏)间重新创建交换链时(即应用窗口改变大小后)应重新创建的资源。
if( Vulkan.Device != VK_NULL_HANDLE ) {
vkDeviceWaitIdle( Vulkan.Device );
if( (Vulkan.PresentQueueCmdBuffers.size() > 0) && (Vulkan.PresentQueueCmdBuffers[0] != VK_NULL_HANDLE) ) {
vkFreeCommandBuffers( Vulkan.Device, Vulkan.PresentQueueCmdPool, static_cast<uint32_t>(Vulkan.PresentQueueCmdBuffers.size()), &Vulkan.PresentQueueCmdBuffers[0] );
Vulkan.PresentQueueCmdBuffers.clear();
}
if( Vulkan.PresentQueueCmdPool != VK_NULL_HANDLE ) {
vkDestroyCommandPool( Vulkan.Device, Vulkan.PresentQueueCmdPool, nullptr );
Vulkan.PresentQueueCmdPool = VK_NULL_HANDLE;
}
}
31.Tutorial02.cpp,Clear()
首先我们必须确保设备阵列上没有执行操作(不能毁坏当前已处理命令所使用的资源)。 我们通过调用 vkDeviceWaitIdle()函数进行检查。 直到所有操作完成后才中止。
接下来我们释放所有分配的命令缓冲区。 事实上,这里并不一定需要执行此操作。 毁坏命令池会暗中释放所有从特定池分配的命令缓冲区。 但我希望展示如何明确释放命令缓冲区。 接下来毁坏命令池。
以下代码负责毁坏本教程中创建的所有资源:
Clear();
if( Vulkan.Device != VK_NULL_HANDLE ) {
vkDeviceWaitIdle( Vulkan.Device );
if( Vulkan.ImageAvailableSemaphore != VK_NULL_HANDLE ) {
vkDestroySemaphore( Vulkan.Device, Vulkan.ImageAvailableSemaphore, nullptr );
}
if( Vulkan.RenderingFinishedSemaphore != VK_NULL_HANDLE ) {
vkDestroySemaphore( Vulkan.Device, Vulkan.RenderingFinishedSemaphore, nullptr );
}
if( Vulkan.SwapChain != VK_NULL_HANDLE ) {
vkDestroySwapchainKHR( Vulkan.Device, Vulkan.SwapChain, nullptr );
}
vkDestroyDevice( Vulkan.Device, nullptr );
}
if( Vulkan.PresentationSurface != VK_NULL_HANDLE ) {
vkDestroySurfaceKHR( Vulkan.Instance, Vulkan.PresentationSurface, nullptr );
}
if( Vulkan.Instance != VK_NULL_HANDLE ) {
vkDestroyInstance( Vulkan.Instance, nullptr );
}
if( VulkanLibrary ) {
#if defined(VK_USE_PLATFORM_WIN32_KHR)
FreeLibrary( VulkanLibrary );
#elif defined(VK_USE_PLATFORM_XCB_KHR) || defined(VK_USE_PLATFORM_XLIB_KHR)
dlclose( VulkanLibrary );
#endif
}
32.Tutorial02.cpp,destructor
首先我们毁坏旗语(请记住,它们在使用的过程中不能毁坏,即队列等待特定旗语时)。 然后毁坏交换链。 与交换链一同创建的图像会自动毁坏,因此无需(也不允许)我们手动进行。 接下来毁坏设备。 我们还需毁坏代表应用窗口的平面。 最后,毁坏 Vulkan,并卸载图形驱动程序的动态库。 执行每步操作之前,我们还需检查是否相应地创建了特定资源。 不能毁坏不是相应创建的资源。
结论
在本教程中,您学习了如何在屏幕上显示用 Vulkan API 创建的图像。 步骤如下: 首先启用相应的实例层扩展。 接下来创建应用窗口的 Vulkan 表现形式(称为平面)。 然后选择带有家族阵列(支持演示并创建设备)的设备(不要忘记启用设备层扩展!)
之后创建交换链。 为此我们首先获取描述平面的参数集,然后选择适用于交换链创建的值。 这些值必须满足平面支持的限制条件。
为在屏幕上进行绘制,我们学习了如何创建和记录命令缓冲区,还包括图像使用内存壁垒(管道壁垒)所进行的布局过渡。 我们清空图像,以看到所选颜色显示在屏幕上。
我们还学习了如何在屏幕上演示特定图像,包括获取图像、提交命令缓冲区,以及演示流程。
声明
本文件不构成对任何知识产权的授权,包括明示的、暗示的,也无论是基于禁止反言的原则或其他。
英特尔明确拒绝所有明确或隐含的担保,包括但不限于对于适销性、特定用途适用性和不侵犯任何权利的隐含担保,以及任何对于履约习惯、交易习惯或贸易惯例的担保。
本文包含尚处于开发阶段的产品、服务和/或流程的信息。 此处提供的信息可随时改变而毋需通知。 联系您的英特尔代表,了解最新的预测、时间表、规格和路线图。
本文件所描述的产品和服务可能包含使其与宣称的规格不符的设计缺陷或失误。 英特尔提供最新的勘误表备索。
如欲获取本文提及的带订购编号的文档副本,可致电 1-800-548-4725,或访问 www.intel.com/design/literature.htm。
该示例源代码根据英特尔示例源代码许可协议发布。
英特尔和 Intel 标识是英特尔在美国和/或其他国家的商标。
*其他的名称和品牌可能是其他所有者的资产。
英特尔公司 © 2016 年版权所有。