Quantcast
Channel: 英特尔开发人员专区文章
Viewing all 154 articles
Browse latest View live

英特尔® 实感™ 摄像头 SR300 简介

$
0
0

简介

英特尔® 实感™ 摄像头 SR300是我们最新推出的前置摄像头。 相比于第一代英特尔® 实感™ 摄像头 F200,SR300 新增了多项新特性和改进功能。 SR300 将摄像头的景深范围扩大至 1.5 米,而且为动态运动捕获功能提高了深度数据的质量、降低了能耗,并增强了中间件的质量和稳定性。 凭借每秒帧速率 (FPS) 高达 30 的 1080p 全高清视频图像质量,或 FPS 高达 60 的 720p 高清视频图像质量,SR300 型号可提供改进的 Skype* 支持。 SR300 支持传统英特尔® 实感™ 摄像头 F200 应用和 RGB 使用。 英特尔® 实感™ SDK 针对 SR 300 摄像头新增了全新的 3D 光标模式、改进的背景分割和 3D 对象扫描功能。 文章英特尔® 实感™ 前置摄像头 SR300 与 F200 对比介绍了 SR300 和 F200 型号之间的不同,以及迁移至 SR300 所能带来的优势。


图 1:SR300 摄像头型号。

SR300 摄像头的外形尺寸大约为 110 x 12.6 x 3.8–4.1 毫米,重约 9.4 克。 这种尺寸和重量有利于固定在移动平台盖或台式机显示器上,以及提供稳定的视频输出。 2016 年,SR300 将能够内置于各种外形的设备,包括 PC、一体机、笔记本电脑和 2 合 1 设备。 SR300 摄像头可使用面向 Windows* 的英特尔实感 SDK 或librealsense软件。 新增 SR 300 支持的 SDK 版本是 2016 R1 或更高版本。

新特性与改进功能

新特性
  • 光标模式
  • 人物追踪
改进功能
  • 范围更大,横向速度更快
  • 提高弱光条件下进行捕获后的色质,并改进了面向 3D 扫描的 RGB 纹理
  • 提高颜色和景深数据流的同步性
  • 降低功耗

请参阅英特尔® 实感™ 前置摄像头 SR300 与 F200 对比,了解更多有关快速 VGA 以及更多新特性与改进功能的信息。

计划为 SR300 摄像头新增的其他英特尔® 实感™ SDK 特性

未来版本的英特尔® 实感™ SDK 将进行更新,并具备多项出色的新特性: 自动范围切换、高动态范围 (HDR) 模式和置信图。

2016 年下半年计划推出的新特性

自动范围切换

自动范围切换可提高图像质量,尤其是近距离范围的图像质量。 它可控制近距离范围的激光增益和远距离范围的曝光。

高动态范围 (HDR) 模式

高动态范围 (HDR) 模式是一种用来扩大图像动态范围的技巧。 动态范围是图像中的明暗比率。 HDR 模式启用后,图像重现时将包含更多细节。 HDR 模式非常适用于弱光或背光条件,并支持帧速率变化较大的应用。

HDR 模式启用后,图像可反映更多有关普通头发和带强光效果的头发细节:


图 2:反映更多头发细节。


图 3:改进带强光效果的头发。

HDR 模式可解决场景混乱问题,比如黑色前景在黑色背景之上,从而显著改进背景分割 (BGS) 功能。 HDR 仅适用于 BGS,而且一开始可能有时无法用作其他中间件。 发布未来版英特尔实感 SDK 时将会公布更多信息。


图 4:黑色头发在黑色背景上。

置信图

置信图特性将提供 0-15 范围内与深度图相关的置信度。 尽管全范围有利于 blob 分割、边缘检测和边缘空白填充,但 0–4 的小范围有助于提高景深准确度。

SR300 目标用途

  • 全手骨骼追踪和手势识别
  • 光标模式
  • 头部追踪
  • 3D 分割和背景移除
  • 景深增强型增强现实
  • 语音命令与控制
  • 3D 扫描:面部和小型物体
  • 面部识别

摄像头应用

动态 BSG

用户站在摄像头前面时,用户分割模块可以遮住背景,这样您可以将用户的面部放在新的背景中。 该模块将集成至视频会议应用。 HDR 模式启用后,SR300 型号将提供高质量掩盖功能,并显著提高弱光条件下的色质。

3D 扫描

SR300 型号显著提高了弱光条件下的色质,从而显著改进了 RGB 纹理,可用于网格以创建比 F200 型号更具吸引力的可视化。 前置摄像头可扫描用户的面部,也可扫描小型的物体。 不过,相比于 F200 型号,SR300 能够以 50 FPS 的速度将捕获范围扩大至 70 厘米,同时捕获更多细节。 您可以使用英特尔实感 SDK 创建 3D 扫描,然后使用 Sketchfab* 在 Facebook* 上共享扫描图。 如欲了解更多有关 Sketchfab 的信息,请访问在应用中登录 Sketchfab Sketchfab 集成。 3D 扫描模块将集成至 AAA 游戏,以捕获最终用户的面部扫描图,并将其用于游戏人物。

手势识别 — 光标模式(仅在 SR300 中可用)

手部模块主要支持三种追踪模式:光标模式、四肢模式和全手模式。 仅 SR300 摄像头提供光标模式这项新特性。 该模式在手上返回单个点,支持精确的响应式追踪和手势。 光标模式适用于在高稳定性手势充足情况下进行更快、更轻、更精确的手部追踪。 光标模式包含手部追踪动作和点击手势。 相比于全手模式,它可将范围和速度提高两倍,同时不会产生延迟,且能耗非常低。


图 1:光标模式。

双阵列麦克风

英特尔实感摄像头 SR300 有一个包含两个麦克风的麦克风阵列,以向客户端系统提供音频输入。 使用两个麦克风,可在嘈杂环境下提高语音模块的稳定性。

英特尔® 实感™ 摄像头 SR300 详情

 SR300 摄像头
范围**0.2 - 1.2 米,室内和间接阳光照射;
景深/IR640x480 分辨率 (60 FPS)
彩色摄像头**高达 1080p (30 FPS),720p (60 FPS)
景深摄像头**高达 640x480 60 FPS(快速 VGA,VGA),HVGA 110 FPS
IR 摄像头**高达 640x480 200 FPS
主板接口USB 3.0, 5V, GND
开发人员套件尺寸**110 毫米 x 12.6 毫米 x 3.8–4.1 毫米
重量**9.4 克
所需操作系统Microsoft Windows* 10 64 位 RTM
语言C++, C#, Visual Basic*, Java*, JavaScript*

DCM 驱动程序

SR300 摄像头要求安装英特尔® 实感™ 景深摄像头管理器 (DCM) 3.x。 截至本文撰写之时,面向 SR300 的黄金版 DCM 为 DCM 3.0.24.59748,其更新将在 Windows 10 Update 中发布。 请访问英特尔实感 SDK 下载页面,下载最新 DCM。 如欲了解更多有关 DCM 的信息,请前往英特尔实感摄像头和 DCM 概述

固件更新

英特尔实感摄像头支持 DCM 驱动程序提供的固件更新。 如果要求固件更新,DCM 驱动程序将提示用户,用户必须接受固件更新才能进行下一步。

硬件要求

如欲支持英特尔实感摄像头所需的带宽,客户端系统需要安装已通电的 USB 3.0 端口。 SR300 摄像头要求 Windows 10 和第六代智能英特尔® 酷睿™ 处理器或更高版本。 如欲了解系统要求以及 SR300 和 F200 支持的操作系统的详细信息,请访问购买开发人员套件页面。

总结

本文概述了最新版及未来版英特尔实感 SDK 为前置英特尔实感摄像头 SR300 提供的新特性。 请点击此处,下载最新英特尔实感 SDK。 如欲订购新摄像头,请访问 http://click.intel.com/intel-realsense-developer-kit.html

实用参考资料

以下是对英特尔实感 DCM 和 SDK 有用的参考资料,包括版本说明和软件下载及更新指南。

关于作者

Nancy Le 是英特尔公司软件与服务事业部的一名软件工程师,主要负责英特尔® 凌动™ 处理器扩展支持项目。

**距离均为大概数字。


英特尔® 实感™ 前置摄像头 SR300 和 F200 的比较

$
0
0

简介

SR300 是支持 Microsoft Windows 10 操作系统的第二代英特尔® 实感™ 前置摄像头。 与 F200 摄像头型号相似,SR300 使用编码光深技术,在更小范围内创建高质量的 3D 深度视频流。 SR300 摄像头的组件包括红外激光投影系统、高速 VGA 红外摄像头和具备集成 ISP 的 200 万像素彩色摄像头。 SR300 使用高速 VGA 深度模式替代了 F200 使用的本机 VGA 深度模式。 此新型深度模式降低了曝光时间,并且可以捕捉到高达 2 米/秒的动态动作。 该摄像头可向客户端提供同步颜色、深度和 IR 视频流数据,能够支持实现全新的平台使用方式。 摄像头深度解决方案的有效范围为室内 0.2 米至 1.2 米。


图 1:SR300 摄像头型号。

SR300 可以使用适用于 Windows 操作系统的英特尔® 实感™ SDK。 支持 SR300 的版本为 SDK 2015 R5 或更新版本。 SR300 将在 2016 年推出,能够集成至个人电脑、一体机、笔记本电脑和 2 合 1 设备等多种外形中。 相较 F200 型号,SR300 型号增加了新特性,并在以下方面做了很大改善:

  • 支持新型手部追踪光标模式
  • 支持新型人物追踪模式
  • 增大了范围和横向速度
  • 改善了暗光拍摄的色彩质量,同时改善了 3D 扫描的 RGB 纹理
  • 改善了色彩与深度流的同步
  • 降低了功耗

产品亮点

SR300

F200

方向

前置

前置

技术

编码光;高速 VGA 60 帧

编码光;本机 VGA 60 帧

彩色摄像头

多至 1080p 30 帧、720p 60 帧

多至 1080p 30 帧

SDK

SDK 2015 R5 或更新版本

SDK R2 或更新版本

DCM 版本

DCM 3.0.24.51819*

DCM 1.4.27.41994*

操作系统

Windows 10 64 位 RTM

Windows 10 64 位 RTM、 Windows 8 64 位

范围

室内:20 – 120 厘米

室内:20 – 120 厘米

* 截至 2016 年 2 月 19 日

仅 SR300 支持的新特性

光标模式

SR300 摄像头的突出特点便是光标模式。 这种跟踪模式会返回手部单点,确保能够实现准确和响应迅捷的 3D 光标点跟踪,并支持基本手势。 相比全手动模式,光标模式可将功耗和性能改善超过 50%,同时不会增加延时,也无需校准。 此外,它还将范围扩大至 85 厘米,而且手部动作追踪速度高达 2 米/秒。 光标模式包括点击手势,使用食指模仿鼠标点击。


图 2:点击手势。

人物追踪

SR300 型号的另一新增特性是人物追踪。 后置摄像头 R200 也支持人物追踪功能,但在 F200 中不可用。 人物追踪支持实时 3D 肢体动作追踪。 它包含三大主要追踪模式:肢体动作模式、关节骨架模式以及面部识别模式。

  • 肢体动作模式: 定位身体、头部和身体轮廓。
  • 关节骨架模式: 使用 2D 和 3D 数据返回身体关节位置。
  • 面部识别模式: 将当前面部图像与已记录的用户数据库对比,识别用户身份。

人物追踪

SR300

F200

检测

50 - 250 厘米

不适用

追踪

50 - 550 厘米

不适用

骨架

50 - 200 厘米

不适用

增大了范围和横向速度

SR300 摄像头引入了被称为高速 VGA 的新型深度模式。 它能够捕获 HVGA 格式帧,并在传输到客户端前,采用插值方式,将帧转变为 VGA 格式。 这一新型深度模式降低了场景曝光时间,并且允许手部动作速度达到 2 米/秒,而本机 VGA F200 最高仅支持 0.75 米/秒。 相对 F200 型号,SR300 型号在范围上也做出了很大改善。 使用手部追踪,SR300 范围可以达到 85 厘米,而 F200 仅能达到 60 厘米。手部分割范围从 F200 型号的 100 厘米达到了 SR300 的 110 厘米。

手部追踪模式

SR300

F200

光标模式 — 常规

20 - 120 厘米(2 米/秒)

不适用

光标模式 — 易操作模式

20 - 80 厘米(1 米/秒 — 2 米/秒)

不适用

追踪

20 - 85 厘米(1.5 米/秒)

20 - 60 厘米(0.75 米/秒)

手势

20 - 85 厘米(1.5 米/秒)

20 - 60 厘米(0.75 米/秒)

分割

20 - 120 厘米(1 米/秒)

20 - 100 厘米(1 米/秒)

面部识别范围从 F200 的 80 厘米增至 SR300 型号的 150 厘米。

面部追踪模式

SR300

F200

检测

30 - 100 厘米

25 - 100 厘米

界标

30 - 100 厘米

30 - 100 厘米

识别

30 - 150 厘米

30 - 80 厘米

表情

30 - 100 厘米

30 - 100 厘米

情绪

30 - 60 厘米

30 - 60 厘米

姿势

30 - 100 厘米

30 - 100 厘米

SR300 型号改善了 RGB 纹理坐标,并且实现了更加细微的 3D 扫描。 3D 扫描范围增至 70 厘米,同时扫描更加细微。 点追踪的速度增至 2 米/秒,并且在 SR300 型号中范围增至 150 米/秒。

其他追踪模式

SR300

F200

3D 扫描

25 - 70 厘米

25 - 54 厘米

点追踪

20 - 150 厘米(2 米/秒)

30 - 85 厘米(1.5 米/秒)

SR300 的深度范围改善了 50% 至 60%。 在 80 厘米范围内,SR300 和 F200 摄像头均能清晰检测到的手部。 当范围超出 120 厘米时,SR300 仍然能捕捉到手部图像,而 F200 则根本无法捕捉到图像。


图 3:SR300 与 F200 深度范围。

改善暗光拍摄的色彩质量以及改善 3D 扫描的 RGB 纹理

仅 SR300 型号新增了自动曝光特性。 曝光补偿特性使暗光或高对比度条件下拍摄的图像可以达到更好的色彩质量。 暗光条件下的色流帧速可能略低于自动曝光条件下的色流帧速。

功能

SR300

F200

色彩 EV 补偿控制

改善了色彩与深度流的同步

F200 型号仅支持相同帧速下运行的多个深度和色彩应用。 SR300 支持同一整数区间内运行的不同帧速的多个深度和色彩应用,并且保持时间轴同步。 这样软件可以在无需开始或停止视频流的情况下在不同帧速间切换。

摄像头时间轴同步

SR300

F200

同步相同帧速的不同视频流类型

同步不同帧速的不同视频流

降低功耗

SR300 摄像头型号设计了附加省电模式,可以在较低帧速下进行操作。 这使得图像系统在降低摄像头功耗的情况下,仍然可以捕捉清晰图像。 借助省电模式,即便在系统待机的情况下,SR300 也可以自主处理场景。

后向兼容 F200 应用

即便在系统待机的情况下,SR300 也可以自主处理场景。 后向兼容 F200 应用 英特尔实感景深摄像头管理器 (DCM) 3.x 使 SR300 兼具 F200 摄像头的功能,从而能够后向兼容为 F200 摄像头所设计的应用。 DCM 能够模拟 F200 摄像头的功能,使得现有的 SDK 应用可以直接在 SR300 型号上运行。 SDK R5 2015 或更新版本支持 SR300 特性。

当从使用低于 SDK R5 2015 版本的 SDK 编译的应用接收视频流时,DCM 将自动激活兼容模式,并通过 F200 管道替代 SR300 管道发出请求。 大多数应用无需进行配置即可直接在新型 SR300 上使用。

红外兼容性

SR300 支持 10 位本机红外数据格式,而 F200 支持 8 位本机红外数据格式。 DCM 驱动程序通过移除或填充两位数据达到其兼容性,进而符合所要求的红外数据大小。

物理接口

F200 和 SR300 的主板和电缆设计完全一致。 F200 的电缆插头可以插入 SR300 的插座。 因此,F200 的电缆可用于 SR300 摄像头。 两种型号均要求满载功率的 USB 3.0。

SDK API

大多数的 SDK API 可以在 SR300 和 F200 之间共享,甚至某些情况下可以在 R200 上使用,并且 SDK 模块会根据运行时搜索到的摄像头型号提供恰当的用户界面。 类似地,不会调用特殊解决方案或像素格式的简单色彩和深度流,无需修改即可直接使用。

通过使用 SenseManager 读取源流,无需代码变更和硬编码便可获取视频流解决方案、帧速以及像素格式。

以上自动转换取决于摄像头型号,对于每个应用来说运行时检查摄像头型号和配置至关重要。 参见 SDK 文件中安装程序选项。

DCM

截止发稿日,SR300 DCM 的标准版本为 DCM 3.0.24.59748,更新将通过 Windows Update 提供。 请访问 https://software.intel.com/zh-cn/intel-realsense-sdk/download获取最新版本的 DCM。 点击英特尔® 实感™ 摄像头与 DCM 概览,获取更多有关 DCM 的信息。

摄像头类型

SR300

F200

R200

DCM 安装程序版本

3 倍

1 倍

2 倍

硬件要求

为满足英特尔实感摄像头的宽带需求,客户端需具有 USB 3 端口。 如欲了解有关 SR300 和 F200 系统要求和支持的操作系统的详细信息,请访问 https://software.intel.com/zh-cn/RealSense/Devkit/

总结

本文总结了英特尔实感前置 3D 摄像头 SR300 相比 F200 的新增特性和增强功能。 这些新特性在 SDK 2015 R5 和 DCM 3.0.24.51819 或更新版本中可用。 此新型摄像头可通过 http://click.intel.com/realsense.html订购。

参考资料

以下搜集了一些英特尔® 实感™ DCM 和 SDK 的有用信息,包括版本说明、如何下载和更新软件。

关于作者

Nancy Le 是英特尔公司软件与服务事业部的软件工程师,负责英特尔® 凌动™ 处理器规模扩展项目。

英特尔® 基于硬件的安全技术为生物识别应用带来独特优势第 1 部分

$
0
0

下载 PDF [PDF 1 MB]

目录

生物识别为何更好
生物识别工作原理
攻击模式
基于英特尔® 硬件的安全技术如何提高生物识别的安全性

  1. 采用英特尔® Software Guard Extensions 技术的可信执行环境
  2. 内存保护方案与虚拟机扩展
  3. 采用英特尔® 身份保护技术与一次性密码的多重因素身份认证

参考资料

生物识别为何更好

“用户名/密码”安全模式多年来一直用于用户身份验证。 人们需要验证他们是否为某项服务的授权用户时(常见的流程是登录进计算机或在线服务,比如社交媒体或在线银行),输入用户名和密码即可。 这种安全模式具有比较明显的缺点,原因如下:

  1. “123456”、“hello” 等简单密码容易被蛮力攻击或字典攻击破解。
  2. 复杂密码不便记忆。
  3. 许多人可能使用同一密码登录多个网站。
  4. 如果忘记密码,提供其他身份验证信息后可重新设置密码。

图 1. 密码登录方案

图 1.密码登录方案

为了提升密码安全性及用户体验,越来越多的服务提供商开始使用生物识别技术。 借助这项技术,人们再也无需记住密码。 而是使用语音、面部、指纹或虹膜进行身份识别。 生物识别因素与传统用户名/密码安全模式有所不同:

  • 生物测定技术可用来衍生复杂的长密码,从而提高安全性,抵御蛮力攻击。
  • 生物测定技术要求生物识别应用开发人员提供更强大的安全保护,因为生物信息是人体的一部分,无法轻易改变。 如果生物信息被盗,用户难以撤销其生物密码。 攻击者可以使用盗取的生物特征复制假体,以在将来登录用户注册的其他账户时,将其用于生物验证。
  • 部分生物特征,比如面部和语音,有着较高的错误接受率。 因此,生物识别系统通常使用多生物因素验证,以提高识别准确率。
  • 有些生物特征可被复制,比如录制的语音、打印的面部照片,或明胶制作的指纹。 因此必须将生命检测模块添加至生物识别系统,以确定生物信息是来源于真实人体,还是副本。

生物识别工作原理

生物识别应用的基本流程包含五个步骤:

  1. 通过 I/O 端口连接的感应器收集生物信息。
  2. 特定设备驱动程序控制输出数据的格式和速度。 数据通过驱动程序进行处理,以满足 Ring-0 层的操作系统要求,然后发送至在 Ring-3 运行的生物验证应用。
  3. 应用获取数据后,进行一些预处理工作,并提取数据的特征点。
  4. 接下来,提取的特征点发送至模式匹配程序,并与数据库中已注册的生物模式进行对比。
  5. 如果与注册模式匹配,匹配程序发送 MATCH 消息,UI 流程将显示用户准确登录,并向用户显示相应的关键内容。

图 2. 生物识别程序流程图

图 2.生物识别程序流程图

攻击模式

在生物验证系统中,攻击者最看重的数据是用户的生物模式。 该模式可能是从传感器收集的原始数据,从内存提取的特征点集,也可能是数据库中所保存的已注册生物模式。

一般来说,如果生物识别应用没有设计适当的安全保护,攻击者可能通过 rootkit 或恶意软件展开进行时攻击,进而从内存中检索原始数据或特征点集。 如果注册模板保存在设备的本地存储,攻击者还可能发动离线攻击,获取已注册的生物模式。 此外,攻击者还会从处理器与感应器之间的数据总线寻找数据流,或使用用户附近的摄像头或麦克风获取面部照片或语音样本等生物数据,以在将来重演攻击。

图 3. 生物识别应用可能遭致的攻击。

图 3.生物识别应用可能遭致的攻击。

在生物识别服务开发人员看来,应用设计原则应提供端到端保护,以确保用户隐私的安全。 具体包括:

  • 提供可信运行环境,确保应用代码段的完整性。
  • 防止包含生物模式的内存区被其他应用访问。
  • 如果敏感数据位于内存/本地存储(或在其他应用或网络服务器之间交换机密数据),通过强有力的加密方法保护这些数据。

基于英特尔® 硬件的安全技术如何提高生物识别的安全性

英特尔平台提供各种基于硬件的安全技术,以满足生物验证应用的安全要求。

1. 采用英特尔® Software Guard Extensions 技术的可信执行环境

由于其出色的安全性,生物识别技术的应用领域越来越广泛。 因为该技术基于每个人的独有特征 — 面部、语音、指纹、虹膜,所以个人的身份不容易被盗。 生物识别技术取代了传统密码验证,还可提供良好的用户体验。

不过,由于生物识别技术广泛应用于各种消费类设备,平台的多样性和开放性也带来了潜在的安全威胁。 开发人员需要认真考虑的一种威胁是,如何确保生物识别功能在不同终端设备上的运行安全。 尤其需要考虑:

  • 如何在终端设备上安全地运行生物取样/建模/匹配算法
  • 如何在终端设备上安全地保存生物数据模板
  • 如何创建终端设备与生物特征云数据库之间的安全通道链路,以完成云验证和其他操作

开发人员可依靠可信执行环境 (TEE) 技术构建有效的增强型解决方案。

什么是 TEE?

TEE 是与富执行环境 (REE) 完全隔离的单独可信执行环境。

根据全球平台 TEE 系统架构规范1,在最高层级,TEE 是具有以下特征的一种环境:

  • TEE 中的所有代码执行都具备可信的真实性和完整性。
  • 其他资产也作为机密受到保护。
    • TEE 应阻挡一切已知的远程和软件攻击,以及各种外部硬件攻击。
  • 防止资产和代码通过调试和测试特性受到未授权追踪和控制。

英特尔® Software Guard Extensions 技术简介

英特尔® Software Guard Extensions(英特尔® SGX)支持软件开发人员在 PC 开放平台上开发和部署安全应用。 它是一套添加至英特尔® 架构的新指令和内存访问变化。

英特尔® SGX 的运行方法是,分配代码和数据驻留、受硬件保护的内存。 受保护的内存区称为安全区 (enclave)。 安全区内存中的数据只能被同样位于该安全区内存空间中的代码访问。 安全区代码可通过特殊指令调用。 安全区能够以 Windows* DLL 的形式构建和加载。

图 4. 嵌入至进程的受保护执行环境。

图 4. 嵌入至进程的受保护执行环境。

支持英特尔® SGX 技术的应用作为不可信部分以及遵循英特尔® SGX 设计框架2的可信部分来构建。 运行时,该应用调用英特尔® SGX 特殊指令来创建可放入可信内存中的安全区。 调用可信函数时,代码在安全区内运行,而且相关数据仅在该安全区内以清晰文本的形式展示。 任何针对该数据的外部访问都会被拒绝。 可信函数返回后,安全区数据仍然位于可信内存中。

图 5. 支持英特尔® Software Guard Extensions 技术的应用执行流程。

图 5.支持英特尔® Software Guard Extensions 技术的应用执行流程。

这项英特尔® 技术旨在为机密信息提供高级保护。 得益于英特尔® SGX 技术,应用可以保障机密信息的安全。 在应用中,敏感数据获得了可靠的保护。 攻击面或 TCB(可信计算基)降至最低,只剩下应用本身和处理器。 即使是破坏操作系统/VMM、BIOS、驱动程序的恶意软件,也无法盗取应用机密。

图 6. 借助英特尔® Software Guard Extensions 技术降低攻击面。

图 6.借助英特尔® Software Guard Extensions 技术降低攻击面。

英特尔® Software Guard Extensions 技术如何增强生物识别功能?

探讨生物识别安全解决方案建议之前,我们应了解该流程中的哪些因素应加以保护:

  • 在应用中,无论是静态还是动态,用户的私有生物特征数据都应仔细处理。
  • 生物操作算法(包括抽样、建模和匹配)应防止遭到病毒和恶意软件的攻击。 输出结果数据不应被篡改。

我们建议使用图 7 中所列出的架构。

图 7. 英特尔® Software Guard Extensions 技术助力增强生物识别功能

图 7.英特尔® Software Guard Extensions 技术助力增强生物识别功能

生物抽样/建模/匹配算法托管在英特尔® SGX 安全区内 — 客户端的可信部分,并负责处理生物特征数据。 其运行时的机密性和完整性得到了可靠保障。 这种类型的算法通常属于软件实施。 常见软件实施可能在运行时被病毒和恶意软件所篡改。 但在这种架构下,在运行时,受保护部分加载至安全区,其代码和数据将在该包安全区内进行测算。 应用的代码和数据加载至安全区后,可防止所有外部软件访问。 因此,生物操作算法是可信的。 除安全特性外,安全区环境还提供出色的可扩展性和性能,支持开放平台的主 CPU 上的执行。 因此,这非常有利于性能敏感型场景,尤其是生物识别功能。

英特尔® SGX 技术还提供一项功能,即为安全区的机密信息加密并保护其完整性,使其能够保存在安全区之外,比如磁盘上,以便应用将来重新使用该数据。 使用硬件衍生的密封密钥,可将数据密封起来,与安全区隔离。 该密封密钥是 CPU 和特定安全区环境所独有的。 结合其他服务,比如英特尔® SGX 平台软件提供的 Monotonic Counter 或 Trusted Time,该解决方案可用来抵御病毒攻击技巧。 Monotonic Counter 用于实施回放保护策略,而 Trusted Time 用于执行基于时间的策略。 两种均以密封数据的形式。 安全区负责通过其选择的算法执行加密,换言之,开发人员可以根据其系统安全要求选择任意一种加密框架。 因此我们可以确保用户的私有生物特征数据仅在安全区内处理,并确保其原始数据不会被暴露给安全区外的不可信部分。

有时,客户端生物识别功能需要连接至远程后端服务器,以在云数据库中(而非本地)进行验证。 使用英特尔® SGX 验证功能,客户端验证模块可借助远程服务器验证客户端平台和用户的生物特征数据。 验证是演示某软件已在平台上实现实例化的流程。 在英特尔® SGX 中,借助该机制,其他一方知道相应的软件在支持平台的安全区内安全运行,从而可以安枕无忧。

首先,该模块生成有关客户端身份(通过 CPU 连接至平台)的可验证报告3。 报告还包含有关用户运行生物识别会话的信息。 服务器验证该报告,以确保它正与通过英特尔® SGX 而受支持的设备进行通信。 客户端和服务器达成一次性供应协议,以使用英特尔® SGX 密封功能将应用机密信息安全密封至客户端平台。

这些机密信息只能由密封它的应用来解封,用于将来创建与服务器的安全会话,无需定期验证客户端平台的身份。 这类机密信息可以是 salt、加密密钥、策略、证书等。之后,生物特征数据和验证结果可通过客户端和服务器之间的安全信道进行发送。

2. 内存保护方案与虚拟机扩展

动态数据攻击是最常见的攻击方法之一。 rootkit 和恶意软件使用这种方法连接特定函数,并在运行时收集/修改数据。 如果进行生物识别,恶意代码可获取通过感应器捕获的生物数据和内存中的注册用户生物模板。

传统基于软件的内存保护的缺点

传统基于软件的保护机制并不可靠。 保护代码和恶意代码都有相同的运行特权(ring-0 或 ring-3)。 因此恶意软件可轻易使保护代码失去保护功能。

图 8. 攻击会损坏保护模块,访问敏感数据缓冲区。

图 8.攻击会损坏保护模块,访问敏感数据缓冲区。

基于虚拟机扩展的内存保护

虚拟机扩展 (VMX) 是一套支持处理器硬件虚拟化的指令4。 其基本工作原理是:

  • 忽略基本的 CPU 操作(比如加载/保存)、分支和 ALU 操作
  • 监控(诱捕)特权指令,比如 MMU 操纵、I/O 指令或更新 TLB。
  • 如果执行特权指令,中断执行并将 CPU 设为 VMX 根模式,以供将来处理。

下表显示了硬件/操作系统/应用与 VMM 模式启用/禁用之间的关系。

图 9. 虚拟机扩展模式开启/关闭时针对系统调用的不同响应。

图 9.虚拟机扩展模式开启/关闭时针对系统调用的不同响应。

通过使用 VMX 的基于硬件的陷阱功能,基于硬件虚拟化的内存保护机制能够以更安全、快速的方法保护内存5。其基本原理是在操作系统和硬件之间插入一块基于 VMM 的内存监控模块。 加载应用时,为可信代码区和数据区构建内存映射表。 映射表构建完成后,只要有内存访问,VMM 就可以诱捕它,然后通过预构建的表对比内存访问指令地址 (EIP) 和内存访问。 然后内存保护模块确认其是否为合法访问,并申请相应的进程。

3. 采用英特尔® 身份保护技术与一次性密码的多重因素身份认证

身份盗窃是当前全球个人和企业越来越关注的问题。 随着黑客不断发明新方法来获取用户名和密码,急需部署安全且简单易用的解决方案。 黑客不断发明新方法来获取用户名和密码。 如果您是一名消费者或日常计算机用户,英特尔® 身份保护技术(英特尔® IPT)可提供功能强大的技巧,帮助您将物理设备链接至您所使用的支持英特尔® IPT 的在线账户,从而避免身份被盗的威胁。

双因素身份验证通常采用一次性密码 (OTP),将用户了解的某些信息(用户名和密码)和用户拥有的某些设备(通常是能按需生成仅短期有效的六位数令牌或密钥卡)相结合。

如果英特尔® IPT 采用 OTP 6,将通过嵌入式处理器,每隔 30 秒生成一个唯一的一次性六位数,它能够防止篡改,并独立于操作系统运行。 由于证书在芯片组内部受到可靠保护,因此不会被恶意软件破坏,也不会从设备中删除。

图 10. 基于一次性密码验证的英特尔® 身份保护技术在客户端与服务器之间的工作流程。

图 10. 基于一次性密码验证的英特尔® 身份保护技术在客户端与服务器之间的工作流程。

如果您的企业已在使用双因素身份验证,那么您对令牌可用性和物流的相关问题就不会感到陌生。 采用 OTP 的英特尔® IPT 是一款内置硬件令牌(安全厂商的选择),无需单独的物理令牌,从而简化了双因素 VPN 登录流程,实现了几乎无延迟的无缝体验。

借助基于英特尔® 处理器的设备上的英特尔® IPT(采用 OTP),英特尔可为网站、金融机构和网络服务提供一个可信硬件根(基于英特尔处理器的独有设备的证明),从而使恶意软件无法登录帐号。 采用 OTP 的英特尔® IPT 支持系统提供其他身份保护和交易验证方法,供多因素身份验证解决方案使用。

关于作者

Jianjun Gu 是英特尔软件和解决方案事业部 (SSG) 开发人员关系部门移动支持团队的一名资深应用工程师。 他专注于企业应用的安全性和可管理性。

Zhihao Yu 在英特尔软件和解决方案事业部 (SSG) 开发人员关系部门担任应用工程师,致力于为基于英特尔® 平台的安全支付解决方案提供支持的英特尔® TEE 技术。

Liang Zhang 现任英特尔软件和解决方案事业部 (SSG) 开发人员关系部门的应用工程师,负责为基于英特尔® 平台的企业应用和物联网开发人员提供支持。

第 2 部分链接

参考资料

1 TEE 系统架构 v1.0: http://www.globalplatform.org/specificationsdevice.asp
2英特尔® Software Guard Extensions(英特尔® SGX),ISCA 2015 英特尔® SGX 教程幻灯片https://software.intel.com/sites/default/files/332680-002.pdf
3使用创新指令创建可信软件解决方案:https://software.intel.com/zh-cn/articles/using-innovative-instructions-to-create-trustworthy-software-solutions
4英特尔® 64 和 IA-32 架构软件开发人员手册:http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html
5 Ravi Sahita and Uday Savagaonkar. “Towards a Virtualization-enabled Framework for Information Traceability (VFIT).” In Insider Attack and Cyber Security Volume 39 of the series Advances in Information Security, pp 113-132, Springer, 2008.
6英特尔® 身份保护技术(英特尔® IPT): http://ipt.intel.com/Home
7英特尔® AES-NI 和英特尔® 安全密钥指令简介:https://software.intel.com/zh-cn/node/256280
8英特尔® 实感™ 技术:http://www.intel.com/content/www/us/en/architecture-and-technology/realsense-overview.html

最小化: 2016 年,智能设备游戏如何向简单演变

$
0
0

革命性 CGI 和虚拟现实具有开创性意义,但极简游戏的开发充满了挑战。

视觉技术突飞猛进:游戏能够完美呈现各种复杂画面,图像比以往更为饱满和逼真。然而,游戏领域有一部分人固执地奉行极简主义,原因是适合智能手机和平板电脑。

游戏业巨头不断投入巨资进行概念设计,但在 2016 年,重要的开发人员更可能减少游戏开发数量,同时提高画面效果和游戏设置1。 在更小、更轻便的屏幕上,移动游戏会带来不同的用户体验;一些游戏公司总是追求最复杂、最震撼的画面效果和动作;独立游戏开发人员深知,清晰、直观的图形和概念仍然可以吸引全球玩家。

2016 年初,Coherent Labs 在一篇博文中预测道,“极简的用户界面设计和更少的操作步骤将继续成为本年移动游戏的主要发展趋势。 在打造卓越用户体验和帮助玩家畅享游戏(无需查看冗长说明)方面,丰富色彩也将发挥重要作用。”下面,我们通过两个典型示例说明小公司如何稳步推出了极简游戏应用。

Super Finger Fighter

在色彩方面,该应用呼应了用户要求。 这款老少皆宜的游戏极其简单,巴西伯南布哥的 BigHut Games 开发人员为该游戏设计了令人震撼的画面效果。 Super Finger Fighter 时尚、直观、养眼,没有复杂或过于绚丽的视效,适合讲各种语言的玩家畅玩。 请点击此处下载:https://appshowcase.intel.com/en-us/android/super-finger-fighter

WaveRun

这款应用的简单性几乎无出其右。 波兰开发公司 SoftKiwi 打造的这款游戏既简洁又精致。 一位玩家评论道:“这不是一款游戏,而是一部杰作!” 本质上,玩家就像电线中的电荷一样,需要来回切换以避免障碍。 尽管目标明确,但该游戏具有陡峭的学习曲线。 该创意团队对概念设计进行了最大限度的简化,因此这款游戏打破了常规,让人玩起来既费劲又兴奋。 您可点击此处查看游戏规格:https://appshowcase.intel.com/en-us/android/waverun

“无所不能的”技术将继续令人惊叹,而在内心身处,用户希望游戏能够尽量简单,这也是这些应用能够广受瞩目的原因。 2016 年,深知直观游戏体验乃众望所归及简单性对于智能手机和平板电脑等设备至关重要的游戏公司有望获得巨大成功。 上述所有游戏都展现了简单之美,而且都支持基于英特尔® 凌动™ 处理器的 Android* 平板电脑。

参考资料:

  1. http://coherent-labs.com/blog/mobile-game-ui-trends-in-2016/

创造力的状态: 5 款可激发无限新创意的游戏应用

$
0
0

每个人都具有独创性,试玩各种游戏有助于释放想象力。

我们常常将创意过程视为一种沉思,若足够幸运,灵感之蝶便会停留在我们的肩膀上。 我们还会将创意能力视为某些人的天赋异禀,如 Georgia O’Keefes 和达芬奇等。

这是创意能力最神奇的特征之一,即它是某些人的专属“天赋”。 当然,有些人拥有与生俱来的天分,但这并非成就创新者的唯一途径。

在孩童时代,我们多数人都会绘画、玩泥巴、扮演角色等。但在达到某一年龄时,除非真得下定决心,我们一般会自认为(更糟的是,别人告诉我们)我们“缺乏禀赋”,而且深信不疑。 这等于宣告了我们“创造力”生命的终结。

>每个人都会在不经意间获得一些灵感,但非凡灵感的获得就像任何其他技能一样,需要针对性练习和努力。 成功人士的秘诀之一就是会想法设法激发其大脑的不同部位。 以量子物理学的研究人员为例,在遇到难题时,他们会先放松一下,例如拿起大提琴把弄一番。

事实上,正如 Elizabeth Gilbert 最近在接受有关其新作《Big Magic》的采访时所说:“有时,工作难以开展下去,这时我们可以做一些放松大脑的事情,以激发创意。”1

所幸,许多休闲创意活动都可以刺激大脑的不同部位。 玩乐是激发创意的重要方式,我们对于玩乐和尝试的成果非常期待。练习可以增强创意能力。

相比其他人,移动游戏应用开发人员对此有更深入的理解。正因如此,许多有趣、令人兴奋的游戏都融入了策略和模拟因素。 我们的选择以及对结果的好奇心能够激发我们的兴趣。 下面介绍五款应用,它们针对基于英特尔® 凌动™ 处理器的 Android* 平板电脑进行了优化,旨在通过不同的思维过程增强大脑的不同功能,点燃创意的火花。

  1. ClockStone 的 Bridge Constructor是工程师的梦想。 它支持用户在震撼的 3D 环境中虚拟构建大型建筑。 为确保成功,用户需要考虑各种实际因素,如预算、材料、各种车辆的有效载荷等。 详情请点击:https://appshowcase.intel.com/en-us/android/bridge-constructor-free

  2. Battle Empire: Sparkling City 的 Roman Wars 为用户提供了军事战略实践和帝国建造的游戏体验,让用户领略灿烂的古代文明。 罗马人因打造了前卫基础设施而闻名天下,部分建筑至今仍留存于世。 您的遗产幸免于敌人的袭击了吗? 亲自了解:https://appshowcase.intel.com/en-us/android/battle-empire-roman-wars

  3. 身处大城市的您可能向往简单的生活。 GIANTS Software 的 Farming Simulator 16正可满足这种期望。您想要种植玉米吗? 驾驶逼真的大型干草打捆机或拖拉机? 您甚至可以销售货物,也有机会爬梯子,但不会沾染任何污垢。https://appshowcase.intel.com/en-us/android/farming-simulator-16

  4. 您可以畅玩 Contra Labs 的《No Hero – Renaissance》,这是一款综合型叙事、platformer 和谜题游戏 。 故事情节非常丰富且具有神秘色彩,因此在通过各种关卡时,用户会发现该游戏极具吸引力。https://appshowcase.intel.com/en-us/android/no-hero-renaissance

  5. astragon Software 的 Construction Simulator 2014具有真实的 LIEBHERR、MAN 和 STILL 重型设备,您必须使用这些设备建造房屋和商业建筑。该游戏也采用了 3D 技术,可为您详细呈现大型建设项目的整个过程。 更多信息请点击该链接: https://appshowcase.intel.com/zh-cn/android/node/9081

尝试一些与日常生活完全无关的活动有助于您消除障碍。 与其他许多活动不同,创意项目具有其独特的周期,了解何时尝试不同和有趣的活动非常有必要。 现在的游戏具有很多便利性,例如可供随时随地玩乐、支持玩家掌握进度且具有可激发玩家兴趣的关卡。

 

模拟和刺激: 轻松保持活跃思维,击败冬季忧郁症

$
0
0

在新的一年,人们似乎总是会制定各种健身计划,但最新的科学研究表明,大脑也同样重要。

在这个时节,万物萧条,天空昏暗,寒气逼人,即使是最精力充沛的人也会无精打采。 日光和维生素 D 的缺乏使得人们很容易昏昏沉沉。随着新年氛围逐渐散去,我们光顾健身房的热情也渐渐消失(今年尤其如此)。 相比以往,思维和身体更为息息相关,对于该话题人们总是乐此不疲。

我们可以采取一些非传统的方法提振思维,为身体注入能量。因而,活跃的思维具有积极作用。 思考专家们在保持饱满精神方面的真知灼见。

  1. 专注力。在生活节奏加快的 21 世纪,保持专注力似乎更难,许多亿万富翁投入重金学习冥想技巧。 如果您从未实施过专注力练习,那么进行这种练习可能存在一定挑战性。但是,即使每天练习 10 分钟也可大幅增强科学家所说的“神经可塑性”,或大脑在受到新刺激时的反应和学习能力。1专注力研究先驱 John Kabat-Zinn 博士将直接注意力的基本原则应用于慢性病和疼痛管理,而专注力练习处于更为基础的层面,可帮助缓解压力这一情绪失调的重要元凶。

  2. 玩视频游戏。没错,大量证据表明,某些视频游戏有助于增强脑力和战略性思维。 玛丽女王生物化学科学学院 (Queen Mary’s School of Biological and Chemical Sciences) 的研究发现,玩实时战略游戏的测试可帮助提高决策灵活性,并全面提升“认知灵活性”,首席作者将其称为“人工智能的基石,而非静态特质,但可通过游戏等趣味学习工具进行培养和完善”。2

  3. 制定战略。刺进和改进大脑活力的另一个方法是制定战略。 Farming Simulator 16 (https://appshowcase.intel.com/zh-cn/android/farming-simulator-16) 是一款精致美观的应用,可帮助用户扩张其农业帝国。 该游戏要求玩家根据市场需求、状况和资源,并逐步根据经验作出决策。 作为奖励,该游戏支持您在多玩家模式下与朋友一起操作并开动重型机器。

  4. 调整状态。您每天可通过各种简短轻松的小活动调整大脑状态。例如,使用另一只手刷牙,与人共舞,聆听不同类型的音乐,根据记忆绘制地图等。

    研究人员使用迷宫研究大脑功能的原因之一在于,迷宫可帮助他们衡量空间记忆3,一个有效的途径是通过 mobadu 将 3D 迷宫下载到 Android* 设备中。 该游戏可帮助您刺激大脑部位的活动,如执行功能、空间学习和记忆。 在几分钟的规定时间内走出迷宫能够让大脑处于非常活跃的状态,而虚拟 3D 环境能够逼真地呈现各种物体,带来身临其境的体验。 请点击以下链接立即下载该应用:https://appshowcase.intel.com/zh-cn/android/node/9044

大脑训练尝试的优点在于,几乎所有新活动都可建立并增强新的神经连接。 至此,我们得出的重大结论是:尝试新事物。 请立即尝试新活动,激发大脑活力。 告别一成不变的生活,尝试一些新鲜事物,您会发现小小的刺激能够改变您的整个人生观。

参考资料:

  1. http://www.ncbi.nlm.nih.gov/pmc/articles/PMC3303604/
  2. http://www.medicaldaily.com/video-games-improve-decision-making-brain-power-real-time-strategy-games-starcraft-can-boost-253251
  3. http://www.jove.com/science-education/5418/spatial-memory-testing-using-mazes

游戏开发中的中间件

$
0
0

下载 PDF [PDF: 251KB]

middleware中间件在软件开发中有多种不同的意义。 但是在游戏开发中,可以从两种方式来考虑中间件:一种是内核和用户体验之间的软件;另一种更为重要,是添加服务、特性和功能以改进游戏并简化游戏开发的软件。 无论您在寻找一个完整的游戏引擎以便在游戏中充分发挥您的创意,还是在寻找一款高效、易用的视频编解码器来部署全动态视频,本列表都可以帮助您找到最佳的中间件,帮助您开发面向英特尔® 架构的游戏。

游戏引擎

游戏引擎通常用来封装渲染、物理、声效、输入、网络和人工智能。 如果您不准备构建自己的引擎,则可以使用商用版本。 下面的游戏引擎面向英特尔® 硬件进行了充分优化,可确保游戏流畅运行,无论您选择任何英特尔® 平台进行开发均不例外。

引擎描述英特尔资源

Unreal* Engine 4

Unreal Engine 4 为一些目前最具视觉冲击力的游戏提供支持,且易于学习。 蓝图视觉脚本支持无编程经验的人员轻松使用,或者您也可使用传统的方式和 C++。 Unreal 支持在基于英特尔® 处理器的 PC 和 Android* 设备上进行游戏跨平台开发。

https://software.intel.com/zh-cn/articles/Unreal-Engine-4-with-x86-Support

Unity* 5

Unity 5 易于学习,支持 Unity Script 和 C# 两种编程支持。 Unity 支持在基于英特尔® 处理器的 PC 和 Android* 设备上进行游戏跨平台开发。

https://software.intel.com/zh-cn/articles/unity

Cocos2d-x

Cocos2d-X 是一款开源游戏引擎,支持在基于英特尔® 处理器的 PC 和设备上进行 2D 游戏跨平台开发。 Cocos2d-x 支持 C++、JavaScript* 和 LUA,允许开发人员在所有平台上使用相同的代码。

https://software.intel.com/zh-cn/articles/creating-multi-platform-games-with-cocos2d-x

Marmalade

Marmalade 专为一次写入,多处执行的引擎而设计。 开发人员既可访问低级平台特性以管理内存和访问文件,也可使用 C++ 或 Objective-C* 编写游戏脚本。 Marmalade 支持在基于英特尔® 处理器的 PC 和 Android* 设备上进行游戏跨平台开发。

https://software.intel.com/zh-cn/android/articles/marmalade-c-and-shiva3d-a-game-engine-guide-for-android-x86

libGDX

libGDX 是一款跨平台开源游戏开发框架,主要面向 Windows*、Linux*、OS X*、iOS*、Android 和 Blackberry* 平台和支持 WebGL 的浏览器。 它支持多种 Java* 虚拟机语言。

https://software.intel.com/zh-cn/android/articles/preparing-libgdx-to-natively-support-intel-x86-cpus-running-android

优化工具

英特尔可为游戏分析和优化提供多种工具。 您是否有游戏片段导致帧绘制时间较长? 您是否想要针对多核性能优化代码? 英特尔的优化工具可以帮助您释放英特尔硬件的全部性能。

英特尔优化工具描述英特尔资源

图形性能分析器 (GPA)

GPA 是一套强大、灵活的工具,支持游戏开发人员充分利用其游戏平台中的性能,包括(但不限于)英特尔® 酷睿™ 处理器、英特尔® 高清显卡以及运行 Android 的基于英特尔处理器的平板电脑。

https://software.intel.com/zh-cn/gpa/faq

英特尔® VTune™ 放大器

英特尔® Vtune™ 放大器支持您轻松查看线程性能、可扩展性、带宽、高速缓存等。 借助该工具,分析能够更快、更简单,因为 VTune 放大器了解常见的线程模式,并能够从更高、更易理解的层面来展示信息。

https://software.intel.com/zh-cn/get-started-with-vtune

英特尔® 编译器工具

英特尔® 编辑器工具生成的代码可充分发挥英特尔处理器的能力。

https://software.intel.com/zh-cn/compiler_15.0_ug_c

英特尔® 线程构建模块(英特尔® TBB)

借助英特尔 TBB,您可以轻松编写 C++ 并行程序。 这些并行程序可充分利用多核性能,可移植、可组合,并且能够适应未来的扩展需求。

https://software.intel.com/zh-cn/android/articles/android-tutorial-writing-a-multithreaded-application-using-intel-threading-building-blocks

其他工具

使用附加工具可以进一步提升游戏的专业化程度。 使用高效的细节层次技术 (LOD) 生成逼真蔬菜,谱一曲莫扎特般的大师级作品,或使用逼真的阴影和照明改进全局照明。 如果您想要进一步提升游戏技术带来的效果,可以考虑使用以下工具。

音频

描述

Wwise*能够轻松集成至多个游戏引擎,并能够轻松部署至多个平台的多线程优质音频。

FMOD*

FMOD 是针对游戏开发和声效部署提供的一套工具。 FMOD Studio 是一款为游戏制作声效的音频创建工具,FMOD Ex 是一款声音播放引擎,跨平台兼容并支持多种引擎,包括 Unity、Unreal、Cocos2d 和 Havok*。

照明

描述

Beast*

Autodesk Beast 可提供优质的全球照明,从物理上模仿正确的实时照明。

GUI

描述

Scaleform*

Autodesk Scaleform 可创建轻量且功能丰富的菜单系统。 Scaleform 支持多线程渲染,易于部署且支持 DirectX* 12。

其他

描述

Bink* 2

Bink 是一款视频编码器,包含一个独立的库,无需安装任何软件。 Bink 支持多核 CPU(如第六代智能英特尔处理器),能够流畅播放游戏视频。

SpeedTree*

SpeedTree 可使用 LOD 为您的游戏生成逼真的树。 SpeedTree 支持按实例和按顶点生成色彩,可减少游戏所需的资产数量以及英特尔高清显卡所需的渲染器优化。

Umbra

Umbra 是一款多核优化的遮挡剔除中间件,兼容 Unity 和 Unreal 引擎的集成支持。

Simplygon*

Simplygon 可自动生成新 LOD,在生成过程中可根据不同 LOD 的需求智能减少模型中的多边形数量。

反馈

我们非常重视您的反馈! 如果您希望看到该列表中添加其他的中间件,请随时评论。 在下方的评论部分分享您使用的中间件截图。 

没有任何秘密的 API: Vulkan* 简介第 1 部分:序言

$
0
0

下载 [PDF 736 KB]

Github 示例代码链接


请前往:没有任何秘密的 API: Vulkan* 简介第 0 部分:前言


目录

教程 1:Vulkan* – 序言

我们从不显示任何内容的简单应用开始。 因为教程太长,因此本文不展示完整的源代码(以及窗口、渲染循环等)。 大家可以访问 https://github.com/GameTechDev/IntroductionToVulkan,在提供的示例中获取包含完整源代码的示例项目。 此处我仅展示与 Vulkan 相关的部分代码。 在应用中使用 Vulkan API 的方法有多种:

  1. 可以动态加载驱动程序的库来提供 Vulkan API 实施,并自己获取它提供的函数指示器。
  2. 可以使用 Vulkan SDK 并链接至提供的 Vulkan Runtime (Vulkan Loader) 静态库。
  3. 可以使用 Vulkan SDK,在运行时动态加载 Vulkan Loader 库,并通过它加载函数指示器。

不建议使用第一种方法。 硬件厂商可以任意修改它们的驱动程序,从而可能影响与特定应用的兼容性。 甚至还会破坏应用,并要求开发人员编写支持 Vulkan 的应用,以覆写部分代码。 这就是为什么最好使用部分抽象层的原因。

建议使用 Vulkan SDK 的 Vulkan Loader。 它能够提供更多配置功能和更高的灵活性,无需修改 Vulkan 应用源代码。 有关灵活性的一个示例是层。 Vulkan API 要求开发人员创建严格遵守 API 使用规则的应用。 如果出现错误,驱动程序几乎不会提供反馈,仅报告部分严重且重大的错误(比如内存不足)。 因为使用该方法,所以 API 本身能够尽可能的小、快。 但如果我们希望获得更多有关哪些地方出错的信息,那么必须启用调试/验证层。 不同的层级用途各不相同,比如内存使用、相应参数传递、对象寿命检查等等。 这些层级都会降低应用的性能,但会为我们提供更多信息。

我们还需选择是静态链接至 Vulkan Loader ,还是动态加载并在运行时由我们自己获取函数指示器。 选择哪一种只是个人喜好问题。 本文将重点介绍第三种使用 Vulkan 的访问,从 Vulkan Runtime 库动态加载函数指示器。 该方法与我们希望在 Windows* 系统上使用 OpenGL* 时的做法类似,采用该方法时,默认实施仅提供部分基础函数。 剩下的函数必须使用 wglGetProcAddress() 或标准窗口 GetProcAddress() 函数动态加载。 这就是创建 GLEW 或 GL3W 等 wrangler 库的对象。

加载 Vulkan Runtime 库并获取导出函数指示器

在本教程中,我们将逐步介绍如何自己获取 Vulkan 函数指示器。 我们从 Vulkan Runtime 库 (Vulkan Loader) 加载这些指示器,该运行时库应与支持 Vulkan 的显卡驱动程序一同安装。 面向 Vulkan 的动态库 (Vulkan Loader) 在 Windows* 和 Linux* 上分别命名为 vulkan-1.dll 和 libvulkan.so。

从现在起,我引用第一个教程的源代码,重点为 Tutorial01.cpp 文件。 因此在应用的初始化代码中,我们需要使用如下代码加载 Vulkan 库:

#if defined(VK_USE_PLATFORM_WIN32_KHR)
VulkanLibrary = LoadLibrary( "vulkan-1.dll" );
#elif defined(VK_USE_PLATFORM_XCB_KHR) || defined(VK_USE_PLATFORM_XLIB_KHR)
VulkanLibrary = dlopen( "libvulkan.so", RTLD_NOW );
#endif

if( VulkanLibrary == nullptr ) {
  std::cout << "Could not load Vulkan library!"<< std::endl;
  return false;
}
return true;

1.Tutorial01.cpp, function LoadVulkanLibrary()

VulkanLibrary 是 Windows 中类型 HMODULE 的变量,或者是 Linux 中 void* 的变量。 如果加载函数的库返回的值不是 0,那么我们可以加载所有导出的函数。 Vulkan 库和 Vulkan 实施(不同厂商的驱动程序)都要求仅展示一个可通过操作系统拥有的标准技巧(比如之前提到的 Windows 中的 GetProcAddress() 或 Linux 中的 dlsym())加载的函数。 Vulkan API 的其他函数可能也能通过该方法获取,但无法保证(甚至不建议)。 必须导出的一个函数是 vkGetInstanceProcAddr()

该函数用于加载其他所有 Vulkan 函数。 为减轻获取所有 Vulkan API 函数地址的工作负担,最便利的方法是将它们的名称放在宏中。 这样我们不用在多个位置(比如定义、声明或加载)复制函数名称,并且能够将它们放在一个标头文件中。 这种单一文件日后也可通过 #include 指令用于不同的用途。 我们可以像下面这样声明导出的函数:

#if !defined(VK_EXPORTED_FUNCTION)
#define VK_EXPORTED_FUNCTION( fun )
#endif

VK_EXPORTED_FUNCTION( vkGetInstanceProcAddr )

#undef VK_EXPORTED_FUNCTION

2.ListOfFunctions.inl

现在我们定义能够展示 Vulkan API 的函数的变量。 这可通过以下命令来实现:

#include "vulkan.h"

namespace ApiWithoutSecrets {

#define VK_EXPORTED_FUNCTION( fun ) PFN_##fun fun;
#define VK_GLOBAL_LEVEL_FUNCTION( fun ) PFN_##fun fun;
#define VK_INSTANCE_LEVEL_FUNCTION( fun ) PFN_##fun fun;
#define VK_DEVICE_LEVEL_FUNCTION( fun ) PFN_##fun fun;

#include "ListOfFunctions.inl"

}

3.VulkanFunctions.cpp

这里我们首先包含 vulkan.h 文件,它正式提供给希望在应用中使用 Vulkan API 的开发人员。 该文件与 OpenGL 库中的 gl.h 文件类似。 它定义开发 Vulkan 应用时所必须的所有枚举、结构、类型和函数类型。 接下来定义来自各“级”(稍后将具体介绍这些级)的函数的宏。 函数定义要求提供函数类型和函数名称。 幸运的是,Vulkan 中的函数类型可从函数名称轻松派生出来。 例如,vkGetInstanceProcAddr()函数的类型定义如下:

typedef PFN_vkVoidFunction (VKAPI_PTR *PFN_vkGetInstanceProcAddr)(VkInstance instance, const char* pName);

4.Vulkan.h

展示该函数的变量定义如下:

PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr;

这是 VulkanFunctions.cpp 文件的宏进行扩展的目标。 它们提取函数名称(隐藏在“fun”参数中)并在开头部分添加“PFN_”。 然后,宏在类型后面放置一个空格,并在之后添加函数名称和分号。 函数“粘贴”至符合 #include “ListOfFunctions.inl” 指令的文件。

但我们必须牢记,如果希望自己定义 Vulkan 函数的原型,那么必须定义 VK_NO_PROTOTYPES 预处理器指令。 默认情况下,vulkan.h 标头文件包含所有函数的定义。 这将有助于静态链接至 Vulkan Runtime。 因此当我们添加自己的定义时,将会出现编译错误,声明特定变量(面向函数指示器)已定义多次(因为我们打破了“一种定义”规则)。 我们可以使用之前提到的预处理器宏禁用 vulkan.h 文件的定义。

同样,我们需要声明 VulkanFunctions.cpp 文件中定义的变量,以便它们显示在代码的其他部分。 这可通过相同的方法完成,但“extern”需要放在各函数的前面。 比较 VulkanFunctions.h 文件。

现在我们有了可用来保存从 Vulkan 库中获取的函数地址的变量。 为了只加载一个导出的函数,我们使用以下代码:

#if defined(VK_USE_PLATFORM_WIN32_KHR)
#define LoadProcAddress GetProcAddress
#elif defined(VK_USE_PLATFORM_XCB_KHR) || defined(VK_USE_PLATFORM_XLIB_KHR)
#define LoadProcAddress dlsym
#endif

#define VK_EXPORTED_FUNCTION( fun )                                                   \
if( !(fun = (PFN_##fun)LoadProcAddress( VulkanLibrary, #fun )) ) {                \
  std::cout << "Could not load exported function: "<< #fun << "!"<< std::endl;  \
  return false;                                                                   \
}

#include "ListOfFunctions.inl"

return true;

5.Tutorial01.cpp, function LoadExportedEntryPoints()

宏从“fun”参数中提取函数名称,将其转化成字符串(带 #)并从 VulkanLibrary 中获取它的地址。 地址可通过(Windows 上的) GetProcAddress() 或(Linux 上的) dlsym() 获取,并保存在 fun 呈现的变量中。 如果操作失败而且库没有显示函数,我们通过打印相应信息并返回假值来报告该问题。 宏在通过 ListOfFunctions.inl 包含的行上运行。 这样我们就不用多次写入函数名称。

由于我们有主函数加载程序,因此可以加载剩下的 Vulkan API 程序。 它们可分为三类:

  • 全局级函数。 支持创建 Vulkan 实例。
  • 实例级函数。 检查可用的支持 Vulkan 的硬件以及显示的 Vulkan 特性。
  • 设备级函数。 负责执行通常在 3D 应用中完成的工作(比如绘制)。

我们从获取全局级的实例创建函数开始。

获取全局级函数指示器

在创建 Vulkan 实例之前,我们必须获取支持创建工作的函数地址。 以下是函数列表:

  • vkCreateInstance
  • vkEnumerateInstanceExtensionProperties
  • vkEnumerateInstanceLayerProperties

最重要的函数是 vkCreateInstance(),它支持我们创建“Vulkan 实例”。 从应用视角来看,Vulkan 实例相当于 OpenGL 的渲染环境。 它保存按照应用状态(Vulkan 中没有全局状态),比如启用的实例级层和扩展功能。 其他两种函数支持我们检查有哪些实例层和实例扩展功能可用。 验证层可根据它们调试的功能分成实例级和设备级。 Vulkan 中的扩展功能与 OpenGL 中的扩展功能类似:展示核心规范不需要的附加功能,而且并非所有硬件厂商都会实施这些功能。 扩展功能(比如层)也可分为实例级扩展功能和设备级扩展功能,不同层级的扩展必须单独启用。 在 OpenGL 中,所有扩展功能(通常)都在创建的环境中提供;使用 Vulkan 时,必须在它们展示的功能能够使用之前启用它们。

我们调用函数 vkGetInstanceProcAddr()获取实例级程序的地址。 它提取两个参数:实例和函数名称。 我们还没有实例,因此第一个参数为“null”。 这就是为什么这些函数有时调用 null 实例或非实例级函数的原因。 通过 vkGetInstanceProcAddr()函数获取的第二个参数是我们希望获取地址的程序名称。 我们可以只加载没有实例的全局级函数。 不能加载其他第一个参数中未提供实例句柄的函数。

加载全局级函数的代码如下所示:

#define VK_GLOBAL_LEVEL_FUNCTION( fun )                                                   \
if( !(fun = (PFN_##fun)vkGetInstanceProcAddr( nullptr, #fun )) ) {                    \
  std::cout << "Could not load global level function: "<< #fun << "!"<< std::endl;  \
  return false;                                                                       \
}

#include "ListOfFunctions.inl"

return true;

6.Tutorial01.cpp, function LoadGlobalLevelEntryPoints()

该代码与用于加载导出函数(库展示的 vkGetInstanceProcAddr())的代码之间唯一不同点在于,我们不使用操作系统提供的函数(比如 GetProcAddress()),而是调用第一个参数设为 null 的 vkGetInstanceProcAddr()

如果您按照本教程自己编写代码,务必将包含在合理命名的宏中的全局级函数添加至 ListOfFunctions.inl 标头文件:

#if !defined(VK_GLOBAL_LEVEL_FUNCTION)
#define VK_GLOBAL_LEVEL_FUNCTION( fun )
#endif

VK_GLOBAL_LEVEL_FUNCTION( vkCreateInstance )
VK_GLOBAL_LEVEL_FUNCTION( vkEnumerateInstanceExtensionProperties )
VK_GLOBAL_LEVEL_FUNCTION( vkEnumerateInstanceLayerProperties )

#undef VK_GLOBAL_LEVEL_FUNCTION

7.ListOfFunctions.inl

创建 Vulkan 实例

加载全局级函数后,现在我们可以创建 Vulkan 实例。 可以通过调用拥有三个参数的 vkCreateInstance()函数完成。

  • 第一个参数包含有关应用、请求的 Vulkan 版本,以及我们希望启用的实例级层和扩展功能的信息。 这都可以通过结构完成(结构在 Vulkan 中非常普遍)。
  • 第二个参数为结构指示器提供与内存分配相关的函数列表。 它们可用于调试,但该特性是可选的,而且我们可以依赖内置的内存分配方法。
  • 第三个参数是我们希望保存 Vulkan 实例句柄的变量地址。 在 Vulkan API 中,操作结果通常保存在我们提供地址的变量中。 返回值仅用于一些通过/未通过通知。 以下是有关实例创建的完整源代码:
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
  0,                                              // uint32_t                   enabledExtensionCount
  nullptr                                         // 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;

8.Tutorial01.cpp, function CreateInstance()

大部分 Vulkan 结构的开头是描述结构类型的字段。 参数通过指示器提供给函数,以避免复制较大的内存块。 有时在结构内部,也会将指示器提供给其他结构。 对于需要知道它应读取多少字节以及成员如何对齐的驱动程序来说,通常会提供结构类型。 那么这些参数到底有什么意义?

  • sType – 结构类型。 在这种情况下,它通过提供 VK_STRUCTURE_TYPE_APPLICATION_INFO 的值,通知驱动程序我们将提供有关实例创建的信息。
  • pNext – 未来版本的 Vulkan API 可能会提供有关实例创建的其他信息,该参数用于此目的。 目前它留作将来使用。
  • flags – 另一个留作将来使用的参数;目前必须设为 0。
  • pApplicationInfo – 包含应用信息(比如名称、版本、所需 Vulkan API 版本等)的另一结构的地址。
  • enabledLayerCount – 定义我们希望启用的实例级验证层的数量。
  • ppEnabledLayerNames – 包含我们希望启用的层级名称的 enabledLayerCount 要素阵列。
  • enabledExtensionCount – 我们希望启用的实例级扩展功能数量。
  • ppEnabledExtensionNames – 与层级一样,该参数必须指向至少包含我们希望使用的实例级扩展功能的名称的 enabledExtensionCount 要素的阵列。

大部分参数都可以设为 null 或 0。 最重要的一个参数(除结构类型信息外)是指向类型变量 VkApplicationInfo 的参数。 因此指定实例创建信息之前,我们还需要指定描述应用的另一变量。 该变量包含应用名称、正在使用的引擎名称,或所需的 Vulkan API 版本(与 OpenGL 版本类似,如果驱动程序不支持该版本,将无法创建实例)。 该信息可能对驱动程序非常有用。 请记住,一些显卡厂商会提供专门用于特定用途(比如特定游戏)的驱动程序。 如果显卡厂商知道引擎游戏使用哪种显卡,将可以优化驱动程序的行为,从而加快游戏的速度。 该应用信息结构可用来实现这一目的。 VkApplicationInfo 结构的参数包括:

  • sType – 结构类型。 此处为 VK_STRUCTURE_TYPE_APPLICATION_INFO,即有关应用的信息。
  • pNext – 留作将来使用。
  • pApplicationName – 应用名称。
  • applicationVersion – 应用版本,使用 Vulkan 宏创建版本非常方便。 它包括主要版本和次要版本,并将数字合成一个 32 位的值。
  • pEngineName – 应用使用的引擎名称。
  • engineVersion – 我们在应用中使用的引擎版本。
  • apiVersion – 我们希望使用的 Vulkan API 版本。 最好提供包含时在 Vulkan 标头中定义的版本,这就是我们为什么使用在 vulkan.h 标头文件中查找的 VK_API_VERSION。

定义了这两个结构后,现在我们可以调用 vkCreateInstance()函数并检查实例是否已创建。 如果创建成功,实例句柄将保存在我们提供地址的变量中,并返回 VK_SUCCESS (0!)。

获取实例级函数指示器

我们已经创建了一个 Vulkan 实例。 接下来是获取函数指示器,以便创建逻辑设备,它可视作物理设备上的用户视图。 计算机上可能安装了许多支持 Vulkan 的不同设备。 每台设备都具备不同的特性、功能和性能,或者支持的功能也各不相同。 如果希望使用 Vulkan,那么必须指定用来执行操作的设备。 可以使用不同用途的设备(比如一台用于渲染 3D 图形、一台用于物理计算,另一台用于媒体解码)。 必须检查有多少设备,其中哪些可用,具备哪些功能,以及支持哪些操作。 这可通过实例级函数来完成。 我们使用之前用过的 vkGetInstanceProcAddr()函数来获取这些函数的地址。 但这次要提供句柄,才能创建 Vulkan 实例。

使用 vkGetInstanceProcAddr()函数加载每个 Vulkan 程序,且 Vulkan 实例句柄带有部分权衡。 将 Vulkan 用于数据处理时,必须创建一台逻辑设备并获取设备级函数。 但在运行应用的计算机上可能有许多支持 Vulkan 的设备。 确定使用哪台设备取决于前面提到的逻辑设备。 但 vkGetInstanceProcAddr()无法认出逻辑设备,因为没有相应的参数。 使用该函数获取设备级程序时,事实上我们获取的是简单“jump”函数的地址。 这些函数提取逻辑设备的句柄,并跳至相应实施(为特定设备实施的函数)。 此次跳跃产生的开销是可以避免的。 建议使用其他函数单独加载每台设备的程序。 但仍然需要使用 vkGetInstanceProcAddr()函数加载支持创建此类逻辑设备的函数。

部分实例级函数包括:

  • vkEnumeratePhysicalDevices
  • vkGetPhysicalDeviceProperties
  • vkGetPhysicalDeviceFeatures
  • vkGetPhysicalDeviceQueueFamilyProperties
  • vkCreateDevice
  • vkGetDeviceProcAddr
  • vkDestroyInstance

这些函数是本教程用于创建逻辑设备所必需的。 但扩展功能也提供一些其他的实例级函数。 通过示例解决方案的源代码形成的标头文件中的列表将展开。 用于加载所有函数的源代码为:

#define VK_INSTANCE_LEVEL_FUNCTION( fun )                                                   \
if( !(fun = (PFN_##fun)vkGetInstanceProcAddr( Vulkan.Instance, #fun )) ) {              \
  std::cout << "Could not load instance level function: "<< #fun << "!"<< std::endl;  \
  return false;                                                                         \
}

#include "ListOfFunctions.inl"

return true;

9.Tutorial01.cpp, function LoadInstanceLevelEntryPoints()

用于加载实例级函数的代码与加载全局级函数的代码大体上相同。 我们只需将 vkGetInstanceProcAddr()函数的第一个参数从 null 改成创建 Vulkan 实例句柄。 当然我们还可以在实例级函数上运行,因此现在我们重新定义 VK_INSTANCE_LEVEL_FUNCTION() 宏,而非 VK_GLOBAL_LEVEL_FUNCTION() 宏。 我们还需定义实例级的函数。 像之前一样,最好通过共享标头中收集的包含宏的名称列表来完成,例如:

#if !defined(VK_INSTANCE_LEVEL_FUNCTION)
#define VK_INSTANCE_LEVEL_FUNCTION( fun )
#endif

VK_INSTANCE_LEVEL_FUNCTION( vkDestroyInstance )
VK_INSTANCE_LEVEL_FUNCTION( vkEnumeratePhysicalDevices )
VK_INSTANCE_LEVEL_FUNCTION( vkGetPhysicalDeviceProperties )
VK_INSTANCE_LEVEL_FUNCTION( vkGetPhysicalDeviceFeatures )
VK_INSTANCE_LEVEL_FUNCTION( vkGetPhysicalDeviceQueueFamilyProperties )
VK_INSTANCE_LEVEL_FUNCTION( vkCreateDevice )
VK_INSTANCE_LEVEL_FUNCTION( vkGetDeviceProcAddr )
VK_INSTANCE_LEVEL_FUNCTION( vkEnumerateDeviceExtensionProperties )

#undef VK_INSTANCE_LEVEL_FUNCTION

10.ListOfFunctions.inl

实例级函数在物理设备上运行。 在 Vulkan 中可以看“物理设备”到和“逻辑设备”(简单称为设备)。 顾名思义,物理设备指安装在计算机上、运行支持 Vulkan 且能够执行 Vulkan 命令的应用的物理显卡(或其他硬件组件)。 如前所述,此类设备可以显示并实施不同(可选)Vulkan 特性,具备不同的功能(比如总内存,或能够处理不同大小的缓冲区对象),或提供不同的扩展功能。 此类硬件可以是专用(独立)显卡,也可以是内置(集成)于主处理器中的附加芯片。 甚至还可以是 CPU 本身。 实例级函数支持我们检查所有参数。 检查后,必须(根据检查结果和需求)决定使用哪台物理设备。 我们还希望使用多台设备(这也是可能的),但这种场景目前太过高级。 因此如果希望发挥物理设备的能力,那么我们必须创建一台逻辑设备,以呈现我们在应用中的选择(以及启用的层、扩展功能、特性等)。 创建设备(并获取队列)后,我们准备使用 Vulkan,方法与创建渲染环境后准备使用 OpenGL 的方法相同。

创建逻辑设备

创建逻辑设备之前,必须首先进行检查,看看系统中有多少可供执行应用的物理设备。 接下来获取所有可用物理设备的句柄:

uint32_t num_devices = 0;
if( (vkEnumeratePhysicalDevices( Vulkan.Instance, &num_devices, nullptr ) != VK_SUCCESS) ||
    (num_devices == 0) ) {
  std::cout << "Error occurred during physical devices enumeration!"<< std::endl;
  return false;
}

std::vector<VkPhysicalDevice> physical_devices( num_devices );
if( vkEnumeratePhysicalDevices( Vulkan.Instance, &num_devices, &physical_devices[0] ) != VK_SUCCESS ) {
  std::cout << "Error occurred during physical devices enumeration!"<< std::endl;
  return false;
}

11.Tutorial01.cpp, function CreateDevice()

如要检查有多少可用设备,可以调用 vkEnumeratePhysicalDevices()函数。 调用两次,第一次调用时将最后一个参数设为 null。 这样驱动程序会知道我们仅要求知道可用物理设备的数量。 该数量将保存在我们在第二个参数中提供地址的变量中。

知道有多少可用物理设备后,我们可以准备保存它们的句柄。 我使用矢量,因此无需担心内存分配和取消分配问题。 再次调用 vkEnumeratePhysicalDevices()时,所有参数不等于 null,我们将获取在最后一个参数中提供地址的阵列中的物理设备的句柄。 该阵列的大小可能与第一次调用后的返回值不同,但必须与在第二个参数中定义的要素数量相同。

例如,有 4 台可用物理设备,但我们只对第 1 台感兴趣。 因此在第一次调用后,在 num_devices中设置一个为 4 的值。 这样我们将知道这里有任意兼容 Vulkan 的设备,然后继续。 我们将该值覆写成 1,因为无论有多少设备,我们只希望使用 1 台设备。 第二次调用后,我们将仅获取一个物理设备句柄。

提供的设备数量将由枚举的物理设备数量所取代(当然不会大于我们提供的值)。 例如,我们不希望两次调用这个函数。 我们的应用支持多达 10 台设备,并且我们提供该值和 10 要素静态阵列指示器。 驱动程序通常返回实际枚举的设备数量。 如果没有设备,我们提供的变量地址中将保存 0。 如果有这种设备,我们也会知道。 我们无法告知是否有超过 10 台设备。

由于我们有所有兼容 Vulkan 的物理设备的句柄,现在可以检查各设备的属性。 在示例代码中,这一过程在循环中完成:

VkPhysicalDevice selected_physical_device = VK_NULL_HANDLE;
uint32_t selected_queue_family_index = UINT32_MAX;
for( uint32_t i = 0; i < num_devices; ++i ) {
  if( CheckPhysicalDeviceProperties( physical_devices[i], selected_queue_family_index ) ) {
    selected_physical_device = physical_devices[i];
  }
}

12.Tutorial01.cpp, function CreateDevice()

设备属性

我创建了 CheckPhysicalDeviceProperties() 函数。 它提取物理设备的句柄,并检查特定设备是否具备足够的功能供应用正常运行。 如果是,返回真值,并将队列家族索引保存在第二个参数中提供在变量中。 队列和队列家族将在后续章节中介绍。

以下是 CheckPhysicalDeviceProperties() 函数的前半部分:

VkPhysicalDeviceProperties device_properties;
VkPhysicalDeviceFeatures   device_features;

vkGetPhysicalDeviceProperties( physical_device, &device_properties );
vkGetPhysicalDeviceFeatures( physical_device, &device_features );

uint32_t major_version = VK_VERSION_MAJOR( device_properties.apiVersion );
uint32_t minor_version = VK_VERSION_MINOR( device_properties.apiVersion );
uint32_t patch_version = VK_VERSION_PATCH( device_properties.apiVersion );

if( (major_version < 1) &&
    (device_properties.limits.maxImageDimension2D < 4096) ) {
  std::cout << "Physical device "<< physical_device << " doesn't support required parameters!"<< std::endl;
  return false;
}

13.Tutorial01.cpp, function CheckPhysicalDeviceProperties()

在函数的开头,查询物理设备的属性和特性。 属性包含的字段有:支持的 Vulkan API 版本、设备名称和类型(集成或专用/独立 GPU)、厂商 ID 和限制。 限制描述如何创建大纹理、anti-aliasing 中支持多少实例、或者特定着色器阶段可以使用多少缓冲区。

设备特性

特性是与扩展功能类似的附加硬件功能。 驱动程序也许没有必要支持这些,而且默认情况下不启用。 特性包含多个项目,比如几何体和镶嵌着色器多个视口、逻辑运算,或其他纹理压缩格式。 如果特定物理设备支持任意特性,那么我们将能够在逻辑设备创建期间启用该特性。 在 Vulkan 中默认不启用特性。 但 Vulkan 规范指出,部分特性可能会对性能(比如稳定性)造成影响。

查询硬件信息和功能后,我提供了一个有关如何使用这些查询的小示例。 我“保留” VK_MAKE_VERSION 宏并检索主要版本和次要版本,并修改了设备属性 apiVersion 字段的版本。 检查它是否高于我希望使用的版本,还检查我能否创建特定大小的 2D 纹理。 在本示例中,我没有使用任何特性,但如果希望使用特性(比如几何体着色器),必须检查它是否支持,并且在逻辑设备创建过程中必须(明确)启用它。 这就是我们为什么需要创建逻辑设备,不直接使用物理设备。 逻辑设备代表物理设备以及我们为其启用的所有特性和扩展功能。

检查物理设备的功能的下一部分 - 队列 - 需要另作解释。

队列、队列家族和命令缓冲区

如果我们希望处理数据(比如通过顶点数据和顶点属性绘制 3D 场景),要调用传递至驱动程序的 Vulkan 函数。 这些函数不直接传递,因为将每个请求单独向下发送至通信总线的效率非常低。 最好是将它们集中起来,分组传递。 在 OpenGL中,驱动程序自动完成该过程,用户是看不见的。 OpenGL API 调用在缓冲区中排队,如果该缓冲区已满(或我们请求刷新),整个缓冲区会传递至硬件以作处理。 在 Vulkan 中,该机制对用户是直接可见的,更重要的是,用户必须为命令专门创建并管理缓冲区。 这些是(方便)调用的命令缓冲区。

指令缓冲区(作为整个对象)被传递至硬件,以通过队列来执行。 然而,这些缓冲区包含不同的操作类型,比如图形命令(用于譬如在典型 3D 游戏中生成和显示图像)或计算命令(用于处理数据)。 特定命令类型可能由专用硬件处理,因此队列也可分成不同类型。 在 Vulkan 中,这些队列类型是调用的家族。 每个队列家族都可支持不同的操作类型。 因此我们还必须检查特定物理设备是否支持我们希望执行的操作类型。 另外,我们还可以在一台设备上执行一类操作,在另一台设备上执行另一类操作,但需要检查它的可行性。 这类检查由 CheckPhysicalDeviceProperties() 函数的后半部分完成:

uint32_t queue_families_count = 0;
vkGetPhysicalDeviceQueueFamilyProperties( physical_device, &queue_families_count, nullptr );
if( queue_families_count == 0 ) {
  std::cout << "Physical device "<< physical_device << " doesn't have any queue families!"<< std::endl;
  return false;
}

std::vector<VkQueueFamilyProperties> queue_family_properties( queue_families_count );
vkGetPhysicalDeviceQueueFamilyProperties( physical_device, &queue_families_count, &queue_family_properties[0] );
for( uint32_t i = 0; i < queue_families_count; ++i ) {
  if( (queue_family_properties[i].queueCount > 0) &&
      (queue_family_properties[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) ) {
    queue_family_index = i;
    return true;
  }
}

std::cout << "Could not find queue family with required properties on physical device "<< physical_device << "!"<< std::endl;
return false;

14.Tutorial01.cpp, function CheckPhysicalDeviceProperties()

我们必须首先检查特定物理设备中有多少可用的队列家族。 其检查方式与枚举物理设备类似。 首先我们调用 vkGetPhysicalDeviceQueueFamilyProperties(),其最后一个参数设为 null。 这样在“queue_count”中,将保存不同列队家族的可变数量。 接下来为该数量的队列家族的属性准备一个位置(如果想这样做 - 其机制与枚举物理设备类似)。 然后再次调用函数,各队列家族的属性将保存在提供的阵列中。

各队列家族的属性包含队列标记、家族中可用队列的数量、时间邮戳支持和图像传输粒度。 现在,最重要的部分是家族中的队列数量和标记。 (位字段)标记定义特定队列家族支持的操作类型(可能支持多种)。 它可以是图形、计算、传输(复制等内存操作),或(针对百万纹理等稀疏资源的)稀疏绑定操作。 未来可能出现其他类型的操作。

在本示例中,我们检查图形操作支持,如果找到该支持,那么就可以使用特定物理设备。 请记住,我们还需牢记指定的家族索引。 选择物理设备后,我们可以创建将在应用其他部分代表该设备的逻辑设备,如下例所示:

if( selected_physical_device == VK_NULL_HANDLE ) {
  std::cout << "Could not select physical device based on the chosen properties!"<< std::endl;
  return false;
}

std::vector<float> queue_priorities = { 1.0f };

VkDeviceQueueCreateInfo queue_create_info = {
  VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,     // VkStructureType              sType
  nullptr,                                        // const void                  *pNext
  0,                                              // VkDeviceQueueCreateFlags     flags
  selected_queue_family_index,                    // uint32_t                     queueFamilyIndex
  static_cast<uint32_t>(queue_priorities.size()), // uint32_t                     queueCount&queue_priorities[0]                            // const float                 *pQueuePriorities
};

VkDeviceCreateInfo device_create_info = {
  VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,           // VkStructureType                    sType
  nullptr,                                        // const void                        *pNext
  0,                                              // VkDeviceCreateFlags                flags
  1,                                              // uint32_t                           queueCreateInfoCount
  &queue_create_info,                             // const VkDeviceQueueCreateInfo     *pQueueCreateInfos
  0,                                              // uint32_t                           enabledLayerCount
  nullptr,                                        // const char * const                *ppEnabledLayerNames
  0,                                              // uint32_t                           enabledExtensionCount
  nullptr,                                        // const char * const                *ppEnabledExtensionNames
  nullptr                                         // const VkPhysicalDeviceFeatures    *pEnabledFeatures
};

if( vkCreateDevice( selected_physical_device, &device_create_info, nullptr, &Vulkan.Device ) != VK_SUCCESS ) {
  std::cout << "Could not create Vulkan device!"<< std::endl;
  return false;
}

Vulkan.QueueFamilyIndex = selected_queue_family_index;
return true;

15.Tutorial01.cpp, function CreateDevice()

首先确保退出设备特性循环后,我们找到了可满足需求的设备。 然后通过调用 vkCreateDevice()创建逻辑设备。 它提取物理设备的句柄和包含创建设备所需的信息的结构地址。 该结构的类型为 VkDeviceCreateInfo 并包含以下字段:

  • sType – 所提供结构的标准类型,此处的 VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO 表示我们为设备创建提供参数。
  • pNext – 指向扩展特定结构的参数;此外我们设为 null。
  • flags – 另一留作将来使用的参数,必须是 0。
  • queueCreateInfoCount – 不同队列家族的数量,我们通过它创建队列和设备。
  • pQueueCreateInfos – queueCreateInfoCount 要素(指定我们希望创建的队列)阵列的指示器。
  • enabledLayerCount – 待启用的设备级验证层数量。
  • ppEnabledLayerNames – 包含待启用设备级的 enabledLayerCount 名称的阵列的指示器。
  • enabledExtensionCount – 为设备启用的扩展功能数量。
  • ppEnabledExtensionNames – 包含 enabledExtensionCount 要素的指示器;各要素必须包含应该启用的扩展功能的名称。
  • pEnabledFeatures – 结构指示器(表示为该设备启用的其他特性)(请参阅“设备”部分)。

特性(如前所述)是默认禁用的附加硬件功能。 如果希望启用所有可用特性,不能简单用 ones 填充该结构。 如果部分特性不支持,设备创建将失败。 相反,我们应传递调用 vkGetPhysicalDeviceFeatures()时填充的结构。 这是启用所有支持特性的最简单的方法。 如果我们仅对部分特定特性感兴趣,那么查询面向可用特性的驱动程序,并清空所有不需要的字段。 如果不希望使用任何附加特性,可以清空该结构(用 0 填充),或为该参数传递一个 null 指示器(如本例所示)。

队列与设备一同自动创建。 如要指定希望启用的队列类型,需要提供其他 VkDeviceQueueCreateInfo 结构阵列。 该阵列必须包含 queueCreateInfoCount 要素。 该阵列中的每个要素都必须引用不同的队列家族;我们仅引用一次特定队列家族。

VkDeviceQueueCreateInfo 结构包含以下字段:

  • sType – 结构类型,此处 VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO 表示它为队列创建信息。
  • pNext – 为扩展功能预留的指示器。
  • flags – 留作将来使用的值。
  • queueFamilyIndex – 队列家族(通过它创建队列)的索引。
  • queueCount – 我们希望在特定队列家族中启用的队列数量(希望通过该家族使用的队列数量)以及 pQueuePriorities 阵列中的要素数量。
  • pQueuePriorities – 包含浮点值(描述通过该家族在各队列中执行的操作的优先级)的阵列。

如前所述,包含 VkDeviceQueueCreateInfo 要素的阵列中的各要素必须描述一个不同的队列家族。 索引是一个数,必须小于 vkGetPhysicalDeviceQueueFamilyProperties()函数提供的值(必须小于可用队列家族的数量)。 在本示例中,我们仅对一个队列家族中的一个队列感兴趣。 因此我们必须记住该队列家族索引。 它用于此处。 如果想准备一个更为复杂的场景,还应记住各家族的列队数量,因为各家族可能支持不同数量的队列。 而且创建的队列不能超过特定家族中的可用队列!

值得注意的一点是,不同队列家族可能有类似(甚至相同)的属性),这意味着它们可能支持类似的操作类型,即支持图形操作的队列家族不止一个。 此外,各家族可能包含不同数量的队列。

我们还必须向各队列分配一个浮点值(从 0.0 到 1.0,包括这两个值)。 我们为特定队列提供的值越大(相对于分配至其他队列的值),特定队列处理命令的时间越长(相对于其他队列)。 但这种关系并不绝对。 优先级也不会影响执行顺序。 它只是一个提示。

优先级仅在单台设备上有关系。 如果在多台设备上执行操作,优先级会影响各台设备(而非两台设备之间)的处理时间。 带有特定值的队列的重要性可能仅高于相同设备上优先级较低的队列。 不同设备的队列独立对待。 一旦我们填充了这些结构并调用 vkCreateDevice(),如果成功,创建的逻辑设备将保存在我们提供地址的变量中(在本示例中称为 VulkanDevice)。 如果该函数失败,将返回与 VK_SUCCESS 相反的值。

获取设备级函数指示器

我们创建了一台逻辑设备。 现在可以用它加载设备级函数。 正如我之前提到的在真实场景中,将有多家硬件厂商在单台计算机上为我们提供 Vulkan 实施的情况。 OpenGL 中现在就出现了这种情况。 许多计算机都有主要用于游戏的专用/独立显卡,但也有内置于处理器的英特尔显卡(当然也能用于游戏)。 因此未来将有更多设备支持 Vulkan。 而且借助 Vulkan。我们可以将处理分成任意硬件。 是否还记得什么时候出现了专门用于物理处理的扩展卡? 或者往前回顾,带有附加显卡“加速器”的正常“2D”卡(是否还记得 Voodoo 卡)? Vulkan 已准备好应对这种场景。

那么,如果有多台设备,我们该怎么处理设备级函数? 我们可以加载通用程序。 这可通过 vkGetInstanceProcAddr()函数来完成。 它返回派遣函数的地址,执行根据提供的逻辑设备句柄跳至相应实施的行为。 但我们可以通过分别加载各逻辑设备的函数,避免这种开销。 使用这种方法时,必须记住,只能基于供加载该函数的设备来调用特定函数。 因此如果在应用中使用较多设备,必须从各设备加载函数。 这并不是特别困难。 而且,尽管这样会导致保存较多函数(并根据供加载的设备对它们进行分组),但我们可以避免抽象层,并节约一部分处理器时间。 加载函数的方式与加载导出函数、全局级函数和实例级函数的方式类似:

#define VK_DEVICE_LEVEL_FUNCTION( fun )                                                   \
if( !(fun = (PFN_##fun)vkGetDeviceProcAddr( Vulkan.Device, #fun )) ) {                \
  std::cout << "Could not load device level function: "<< #fun << "!"<< std::endl;  \
  return false;                                                                       \
}

#include "ListOfFunctions.inl"

return true;

16.Tutorial01.cpp, function LoadDeviceLevelEntryPoints()

这次我们使用 vkGetDeviceProcAddr()函数和逻辑设备句柄。 设备级函数放在共享标头中。 这次它们包含在 VK_DEVICE_LEVEL_FUNCTION() 宏中,如下所示:

#if !defined(VK_DEVICE_LEVEL_FUNCTION)
#define VK_DEVICE_LEVEL_FUNCTION( fun )
#endif

VK_DEVICE_LEVEL_FUNCTION( vkGetDeviceQueue )
VK_DEVICE_LEVEL_FUNCTION( vkDestroyDevice )
VK_DEVICE_LEVEL_FUNCTION( vkDeviceWaitIdle )

#undef VK_DEVICE_LEVEL_FUNCTION

17.ListOfFunctions.inl

所有函数都不是来自于导出、全局或实例级,而是来自设备级。 第一个参数会造成另一个区别:对于设备级函数,列表中的第一个参数只能是类型 VkDevice、VkQueue 或 VkCommandBuffer。 在接下来的教程中,如果出现新的函数,必须添加至 ListOfFunctions.inl 并进一步添加至 VK_DEVICE_LEVEL_FUNCTION 部分(有一些明显的例外情况,比如扩展功能)。

检索队列

创建设备后,我们需要能够为数据处理提交部分命令的队列。 队列通过逻辑设备自动创建,但为了使用这些队列,我们必须特别要求队列句柄。 这可通过 vkGetDeviceQueue()完成,如下所示:

vkGetDeviceQueue( Vulkan.Device, Vulkan.QueueFamilyIndex, 0, &Vulkan.Queue );

18.Tutorial01.cpp, function GetDeviceQueue()

如要检索队列句柄,必须提供用于获取队列的逻辑设备。 还需要队列家族索引,该索引必须是我们在逻辑设备创建期间提供的索引之一(不能创建其他队列或使用我们没有请求的家族的队列)。 最后一个参数是来自特定家族的队列索引;它必须小于从特定家族请求的队列总数。 例如,如果设备支持 3 号家族的 5 个队列,而我们希望该家族提供 2 个队列,那么队列索引必须小于 2。 对于我们希望检索的各个队列来说,必须调用该函数并进行单独查询。 如果函数调用成功,请求队列的句柄会保存在我们在最后一个参数中提供地址的变量中。 从这时起,希望(使用命令缓冲区)执行的所有工作都可提交至获取的队列中以供处理。

Tutorial01 执行

如前所述,本教程提供的示例无法演示所有内容。 不过我们了解了足够多的信息。 那么,我们如何知道一切是否进展顺利? 如果出现正常的应用窗口,控制台/终端没有打印任何内容,表示 Vulkan 设置成功。 从下一教程开始,操作结果将显示在屏幕上。

清空

我们还需牢记的一点是:清空和释放资源。 必须以特定的顺序(通常与创建顺序相反)进行清空。

应用关闭后,操作系统应释放内存及其他所有相关资源。 这应包含 Vulkan;驱动程序通常清空没有引用的资源。 遗憾的是,这种清空没有以相应的顺序执行,因此可能会导致应用在关闭过程中崩溃。 最佳实践是自己执行清理。 以下是释放在第一个教程中创建的资源所需的示例代码:

if( Vulkan.Device != VK_NULL_HANDLE ) {
  vkDeviceWaitIdle( Vulkan.Device );
  vkDestroyDevice( Vulkan.Device, 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
}

19.Tutorial01.cpp, destructor

我们应该经常检查,看是否已创建任何特定资源。 没有逻辑设备就没有设备级函数指示器,也就无法调用相应的资源清理函数。 同样,如果没有实例,就无法获取 vkDestroyInstance()函数的指示器。 一般来说,我们不能释放没有创建的资源。

必须确保对象在删除之前,没有经过设备的使用。 因此有一个等候函数,等特定设备的所有队列上的处理过程完成之后才进行拦截。 接下来,我们使用 vkDestroyDevice()函数破坏逻辑设备。 与此相关的所有队列都会自动破坏掉,然后破坏实例。 在这之后我们就能够释放(或卸载)供获取所有函数的 Vulkan 库。

结论

本教程介绍了如何在应用中为使用 Vulkan 做准备。 首先“连接”Vulkan Runtime 库,并从中加载全局级函数。 然后创建 Vulkan 实例并加载实例级函数。 之后检查哪些物理设备可用,以及它们具备的特性、属性和功能。 接下来创建逻辑设备,并描述必须与设备一同创建的队列及其数量。 然后使用新创建的逻辑设备句柄检索设备级函数。 另外需要做的一件事是检索供我们提交待执行工作的队列。


请前往:没有任何秘密的 API:Vulkan* 简介第 2 部分:交换链


声明

本文件不构成对任何知识产权的授权,包括明示的、暗示的,也无论是基于禁止反言的原则或其他。

英特尔明确拒绝所有明确或隐含的担保,包括但不限于对于适销性、特定用途适用性和不侵犯任何权利的隐含担保,以及任何对于履约习惯、交易习惯或贸易惯例的担保。

本文包含尚处于开发阶段的产品、服务和/或流程的信息。 此处提供的信息可随时改变而毋需通知。 联系您的英特尔代表,了解最新的预测、时间表、规格和路线图。

本文件所描述的产品和服务可能包含使其与宣称的规格不符的设计缺陷或失误。 这些缺陷或失误已收录于勘误表中,可索取获得。

如欲获取本文提及的带订购编号的文档副本,可致电 1-800-548-4725,或访问 www.intel.com/design/literature.htm

该示例源代码根据英特尔示例源代码许可协议发布。

英特尔和 Intel 标识是英特尔在美国和/或其他国家的商标。

*其他的名称和品牌可能是其他所有者的资产。

英特尔公司 © 2016 年版权所有。


没有任何秘密的 API: Vulkan* 简介第 2 部分: 交换链

$
0
0

下载  [PDF 1 MB]

Github 示例代码链接


请前往: 没有任何秘密的 API: Vulkan* 简介第 1 部分: 序言


目录

教程 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_surfaceVK_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 表现形式(称为平面)。 然后选择带有家族阵列(支持演示并创建设备)的设备(不要忘记启用设备层扩展!)

之后创建交换链。 为此我们首先获取描述平面的参数集,然后选择适用于交换链创建的值。 这些值必须满足平面支持的限制条件。

为在屏幕上进行绘制,我们学习了如何创建和记录命令缓冲区,还包括图像使用内存壁垒(管道壁垒)所进行的布局过渡。 我们清空图像,以看到所选颜色显示在屏幕上。

我们还学习了如何在屏幕上演示特定图像,包括获取图像、提交命令缓冲区,以及演示流程。


请前往: 没有任何秘密的 API: Vulkan* 简介第 3 部分: 第一个三角形


声明

本文件不构成对任何知识产权的授权,包括明示的、暗示的,也无论是基于禁止反言的原则或其他。

英特尔明确拒绝所有明确或隐含的担保,包括但不限于对于适销性、特定用途适用性和不侵犯任何权利的隐含担保,以及任何对于履约习惯、交易习惯或贸易惯例的担保。

本文包含尚处于开发阶段的产品、服务和/或流程的信息。 此处提供的信息可随时改变而毋需通知。 联系您的英特尔代表,了解最新的预测、时间表、规格和路线图。

本文件所描述的产品和服务可能包含使其与宣称的规格不符的设计缺陷或失误。 英特尔提供最新的勘误表备索。

如欲获取本文提及的带订购编号的文档副本,可致电 1-800-548-4725,或访问 www.intel.com/design/literature.htm

该示例源代码根据英特尔示例源代码许可协议发布。

英特尔和 Intel 标识是英特尔在美国和/或其他国家的商标。

*其他的名称和品牌可能是其他所有者的资产。

英特尔公司 © 2016 年版权所有。

没有任何秘密的 API: Vulkan* 简介第 3 部分: 第一个三角形

$
0
0

下载  [PDF 885 KB]

Github 示例代码链接


请前往: 没有任何秘密的 API: Vulkan* 简介第 2 部分: 交换链


目录

教程 3: 第一个三角形 — 图形管道和绘制

在本教程中我们将最后在屏幕上绘制一些图形。 简单的三角形就是 Vulkan 生成一个比较好的的“图像”。

一般来说,图形管道和绘制操作要求 Vulkan 做许多准备工作(以在许多结构中填充复杂字段的形式)。 我们在很多方面都有可能犯错误,而且在 Vulkan 中,即使简单的错误也会造成应用无法按预期运行、显示空白屏幕,而且我们无法得知到达哪里出现了错误。 在这种情况下,验证层可为我们提供帮助。 但我不希望深入探讨 Vulkan API 的细节。 因此,我尽可能准备小型、简单的代码。

这样我们创建的应用就可以按照预期正常运行并显示简单的三角形,但它还使用不建议使用、不灵活,而且可能不高效(尽管正确)的机制。 我不想讨论不建议使用的情况,但它可显著简化教程,并支持我们仅专注于所需的最小 API 用法集。 只要遇到了“有争议的”功能,我都会指出来。 在下一教程中,我将介绍一些绘制三角形的推荐方法。

要绘制第一个简单的三角形,我们需创建渲染通道、帧缓冲器和图形管道。 当然也需要命令缓冲器,但我们已对其有所了解。 我们将创建简单的 GLSL 着色器,并将其编译成 Khronos 的 SPIR*-V 语言 — Vulkan(官方)理解着色器的(目前)唯一形式。

如果您的电脑屏幕没有显示任何内容,请尝试尽可能地简化代码,或者回到教程 2。 检查命令缓冲区是否按预期仅清空了图形行为,而且图形清空的颜色是否显示在屏幕上。 如果是,请通过本教程修改代码并添加一些部分。 如果不是 VK_SUCCESS,请检查每个返回值。 如果这些方法没有用,请等待本教程的验证层。

关于源代码示例

为方便本教程及其随后的教程,我更换了示例项目。 之前教程中介绍的 Vulkan 准备阶段放在单独文件(标头和源)的“VulkanCommon”类。 面向特定教程的类负责演示特定教程中介绍的主题、承袭“VulkanCommon”类并访问(所需)的 Vulkan,比如设备或交换链。 这样我们可以重新使用 Vulkan 创建代码,并准备仅专注于已演示主题的较小的类。 之前章节的代码能够正常运行,因此比较容易找出潜在错误。

我还为部分实用程序函数添加了独立的文件集。 此处我们将通过二进制文件读取 SPIR-V 着色器,因此我添加了一个函数,可检查二进制文件内容的加载。 它位于 Tools.cpp 和 Tools.h 文件。

创建渲染通道

为在屏幕上进行绘制,我们需要一个图形管道。 但现在创建这一管道需要其他结构的指示器,其中还可能需要另外其他结构的指示器。 因此我们从渲染通道开始。

什么是渲染通道? 常见图片可为我们提供一个用于许多常用渲染技巧(比如延迟着色)的“逻辑”渲染通道。 该技巧包含许多子通道。 第一个子通道使用填充 G-Buffer 的着色器绘制几何图形:将漫射颜色保存在一种纹理中,将标准矢量保存在另一纹理中、将亮度保存在另一纹理中,而将深度(位置)保存在另一纹理中。 接下来是各个光源,执行的绘制包括读取数据(标准矢量、亮度、深度/位置)、计算照明,并将其保存在另一纹理中。 最后一个通道整合照明数据和漫射颜色。 这只是有关延期着色的(粗略)解释,但它介绍了渲染通道 — 执行部分绘制操作所需的数据集:将数据保存在纹理中,并从其他纹理读取数据。

在 Vulkan 中,渲染通道代表(或描述)执行绘制操作所需的帧缓冲区附件(图像)集,以及排列绘制操作的子通道集合。 它是一种收集所有颜色、深度与模板附件,以及操作的构造,对它们进行修改后,驱动程序无需自己推断这种信息,从而为部分 GPU 提供了重要的优化机会。 子通道包含使用(或多或少)相同附件的绘制操作。 每种绘制操作都从部分输入附件读取数据,并将数据渲染至其他(颜色、深度、模板)附件。 渲染通道还描述这些附件之间的相关性:我们在一个子通道中渲染纹理,而在另一子通道中该纹理将用作数据源(即通过其进行采样)。 所有这些数据都可帮助图形硬件优化绘制操作。

为在 Vulkan 中创建渲染通道,我们调用 vkCreateRenderPass()函数,它要求具有结构指示器,该结构描述所有涉及渲染的附件和所有形成渲染通道的子通道。 像往常一样,使用的附件和子通道越多,所需的包含相应字段结构的阵列要素越多。 在这一简单示例中,我们仅通过单个通道绘制到单个纹理(颜色附件)。

渲染通道附件描述

VkAttachmentDescription attachment_descriptions[] = {
  {
    0,                                   // VkAttachmentDescriptionFlags   flags
    GetSwapChain().Format,               // VkFormat                       format
    VK_SAMPLE_COUNT_1_BIT,               // VkSampleCountFlagBits          samples
    VK_ATTACHMENT_LOAD_OP_CLEAR,         // VkAttachmentLoadOp             loadOp
    VK_ATTACHMENT_STORE_OP_STORE,        // VkAttachmentStoreOp            storeOp
    VK_ATTACHMENT_LOAD_OP_DONT_CARE,     // VkAttachmentLoadOp             stencilLoadOp
    VK_ATTACHMENT_STORE_OP_DONT_CARE,    // VkAttachmentStoreOp            stencilStoreOp
    VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,     // VkImageLayout                  initialLayout;
    VK_IMAGE_LAYOUT_PRESENT_SRC_KHR      // VkImageLayout                  finalLayout
  }
};

1.Tutorial03.cpp,函数 reateRenderPass()

为创建渲染通道,我们首先准备一个阵列,其中包含描述各个附件(无论附件类型)及其在渲染通道中的用法的要素。 该阵列要素的类型为 VkAttachmentDescription 并包含以下字段:

  • flags –描述附件的其他属性。 目前仅别名标记可用,它告知驱动程序附件与其他附件共享相同物理内存;这种情况不适用此处,因此我们将该参数设置为零。
  • format –用于附件的图像格式;此处我们直接渲染至交换链,因此需要采用这种格式。
  • samples – 图像样本数量;我们此处不使用任何多点采用,因此仅使用一个样本。
  • loadOp – 指定如何处理渲染通道开头的图像内容,是希望清空、保存,还是不管这些内容(我们将覆写这些内容)。 此处我们希望将图像清空至指定的值。 该参数还表示深度/模板图像的深度部分。
  • storeOp –告知驱动程序如何处理渲染通道后的图像内容(最后一次使用图像的子通道之后)。 此处我们希望在渲染通道后保存图像内容,因为我们想在屏幕上显示这些内容。 该参数还表示深度/模板图像的深度部分。
  • stencilLoadOp – 与 loadOp 相同,但面向深度/模板图像的模板部分;对颜色附件来说它已被忽略。
  • stencilStoreOp – 与 storeOp 相同,但面向深度/模板图像的模板部分;对颜色附件来说该参数已被忽略。
  • initialLayout – 渲染通道启动时特定附件将呈现的布局(应用为布局图像提供的内容)。
  • finalLayout – 渲染通道结束后驱动程序自动将特定图像过渡至的布局。

还需要一些其他信息亿完成加载和保存操作,以及初始和最终布局。

加载选项指渲染通道开头的附件内容。 该操作描述图形硬件如何处理附件:清空、在现有内容上操作(不触碰内容),或者不管它们,因为应用打算覆写这些内容。 这样硬件将有机会优化内存操作。 例如,如果我们希望覆写这些内容,硬件将不会打扰这些内容,而且如果速度加快,可能为附件分配所有新内存。

保存选项,顾名思义,用在渲染通道结尾部分,告知硬件我们是希望在渲染通道后使用附件内容,还是不关心并有可能舍弃这些内容。 在一些场景中(舍弃这些内容时)它将支持硬件在临时快速内存中创建图像,因为图像将仅在渲染通道期间“活跃”,而且实施操作可能会节省一些内存带宽,以避免在不需要的时候回写数据。

如果附件有深度格式(并可能有模板组件),加载和保存选项仅表示深度组件。 如果出现模板,将以模板加载和保存选项描述的方式处理模板值。 模板选项与颜色附件无关。

我们交换链教程中介绍过,布局指图像内部内存的安排形式。 图像数据整理后,相邻“图像像素”也是内存中的邻居,这样图像用作数据源时(即在纹理采样期间),可提高缓存命中率(加快内存读取速度)。 如果图像用作绘制操作的对象,并不一定要执行高速缓存,而且可能以完全不同的方法来整理用于该图像的内存。 图像可能呈现线性布局(支持 CPU 读取或填充图像的内存内容),也可能呈现最佳布局(面向性能优化,但依然依赖硬件/厂商)。 因此一些硬件可能针对一些操作类型有特定的内存组织形式;其他硬件可能适用于任何操作类型。 部分内存布局可能更适合预期的图像“用法”。 或从另一角度来说,部分用法可能要求特定的内存布局。 同时也存在一种通用布局,兼容所有操作类型。 但从性能的角度来说,最好设置符合预期图像用法的布局,而且应用负责将布局过渡告知驱动程序。

可使用图像内存壁垒更改图像布局。 我们在交换链教程中这样做过,首先将布局演示源(演示引擎使用的图像)改成转移目标(希望使用特定颜色清空图像)。 但布局与图像内存壁垒不同,也可由渲染通道内的硬件自动更改。 如果我们指定不同的初始布局、子通道布局(稍后介绍)和最终布局,硬件将在适当时自动过渡。

初始布局将应用为特定附件“提供”(或“保留”)的布局告知硬件。 图像在渲染通道开头开始呈现这种布局(在本示例中,我们从演示引擎获取图像,因此图像呈现“演示源”布局)。 渲染通道的每个子通道都使用不同的布局,子通道之间的硬件自动进行过渡。 最终布局是特定附件将在渲染通道结尾时(渲染通道完成后)(自动)过渡至的布局。

必须为将用于渲染通道的每个附件准备这类信息。 图形硬件收到此类信息后,可能会在渲染通道期间优化操作和内存,以实现最佳性能。

子通道描述

VkAttachmentReference color_attachment_references[] = {
  {
    0,                                          // uint32_t                       attachment
    VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL    // VkImageLayout                  layout
  }
};

VkSubpassDescription subpass_descriptions[] = {
  {
    0,                                          // VkSubpassDescriptionFlags      flags
    VK_PIPELINE_BIND_POINT_GRAPHICS,            // VkPipelineBindPoint            pipelineBindPoint
    0,                                          // uint32_t                       inputAttachmentCount
    nullptr,                                    // const VkAttachmentReference   *pInputAttachments
    1,                                          // uint32_t                       colorAttachmentCount
    color_attachment_references,                // const VkAttachmentReference   *pColorAttachments
    nullptr,                                    // const VkAttachmentReference   *pResolveAttachments
    nullptr,                                    // const VkAttachmentReference   *pDepthStencilAttachment
    0,                                          // uint32_t                       preserveAttachmentCount
    nullptr                                     // const uint32_t*                pPreserveAttachments
  }
};

2.Tutorial03.cpp,函数 CreateRenderPass()

接下来我们指定渲染通道将包含的子通道描述。 该步骤可通过VkSubpassDescription 结构完成,其中包含以下字段:

  • flags – 留作将来使用的参数。
  • pipelineBindPoint – 供子通道使用的管道类型(图形或计算)。 当然我们的示例使用图形管道。
  • inputAttachmentCount – pInputAttachments 阵列中的要素数量。
  • pInputAttachments – 包含描述哪些附件用作输入,并可从内部着色器读取的要素阵列。 此处我们不使用任何输入附件,因此将其设为 0。
  • colorAttachmentCount – pColorAttachments 和 pResolveAttachments 阵列中的要素数量。
  • pColorAttachments – 描述(指示)将用作颜色渲染对象(渲染图像)的附件阵列。
  • pResolveAttachments – 与颜色附件紧密相连的阵列。 该阵列的每个要素都分别对应颜色附件阵列中的一个要素;此类颜色附件将分解成特定分解附件(如果相同索引中的分解附件或整个指示器不是 null)。 该参数为可选项,而且可设为 null。
  • pDepthStencilAttachment – 将用于深度(和/或模板)数据的附件描述。 我们这里不使用深度信息,因此将其设为 null。
  • preserveAttachmentCount – pPreserveAttachments 阵列中的要素数量。
  • pPreserveAttachments – 描述将被保留的附件阵列。 如有多个子通道,并非所有通道都使用所有附件。 如果子通道不使用其中的附件,但在后续的通道中需要它们,那么我们必须在这里指定这些附件。

pInputAttachments、pColorAttachments、pResolveAttachments、pPreserveAttachments 和 pDepthStencilAttachment 参数的类型均为 VkAttachmentReference。 该结构仅包含两个字段:

  • attachment – VkRenderPassCreateInfo 的 attachment_descriptions 阵列索引。
  • layout – 附件在特定子通道期间请求(所需)的布局。 在特定通道之前,硬件将帮助自动过渡至提供的布局。

该结果包含 VkRenderPassCreateInfo 的 attachment_descriptions 阵列的参考信息(索引)。 创建渲染通道时,我们必须提供有关用于渲染通道期间的所有附件的描述。 之前创建 attachment_descriptions 阵列时,我们已在“渲染通道附件描述”部分准备了该描述。 现在它仅包含一个要素,但在高级场景中将有多个附件。 因此这种所有渲染通道附件的“通用”集合将用作参考点。 在子通道描述中,当填充 pColorAttachments 或 pDepthStencilAttachment members 成员时,我们提供这种“通用”集合的索引,像这样:从渲染通道附件提取第一个附件,并将其用作颜色附件。 该阵列的第二个附件将用于深度数据。

整个渲染通道与其子通道是独立的,因为子通道可能以不同的方式使用多个附件,即我们在一个子通道中渲染颜色附件,而在下一个子通道中读取该附件。 这样我们准备用于整个渲染通道的附件列表,同时可以指定每个附件在通道中的使用方式。 由于各子通道可能以自己独有的方式使用特定附件,因此我们必须为各子通道指定每个图像的布局。

因此指定所有子通道(包含 VkSubpassDescription 类型要素的阵列)之前,必须为用于各子通道的附件创建引用。 这就是创建 color_attachment_references 变量的目的所在。 编写纹理渲染教程时,该用法会变得更加明显。

渲染通道创建

现在我们有创建渲染通道需要的所有数据。

vkRenderPassCreateInfo render_pass_create_info = {
  VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,    // VkStructureType                sType
  nullptr,                                      // const void                    *pNext
  0,                                            // VkRenderPassCreateFlags        flags
  1,                                            // uint32_t                       attachmentCount
  attachment_descriptions,                      // const VkAttachmentDescription *pAttachments
  1,                                            // uint32_t                       subpassCount
  subpass_descriptions,                         // const VkSubpassDescription    *pSubpasses
  0,                                            // uint32_t                       dependencyCount
  nullptr                                       // const VkSubpassDependency     *pDependencies
};

if( vkCreateRenderPass( GetDevice(), &render_pass_create_info, nullptr, &Vulkan.RenderPass ) != VK_SUCCESS ) {
  printf( "Could not create render pass!\n" );
  return false;
}

return true;

3.Tutorial03.cpp,函数 CreateRenderPass()

我们首先填充 VkRenderPassCreateInfo 结构,其中包含以下字段:

  • sType – 结构类型(此处为 VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO)。
  • pNext – 目前不使用的参数。
  • flags – 留作将来使用的参数。
  • attachmentCount – 整个渲染通道(此处仅一个)期间使用的不同附件(pAttachments 阵列中的要素)数量。
  • pAttachments – 指定所有用于渲染通道的附件阵列。
  • subpassCount – 渲染通道包含的子通道数量(以及 pSubpasses 阵列(本示例中仅一个)中的要素数量)。
  • pSubpasses – 包含所有子通道描述的阵列。
  • dependencyCount – pDependencies 阵列中的要素数量(此处为 0)。
  • pDependencies – 描述子通道配对之间相关性的阵列。 我们的子通道不多,因此不存在相关性(此处设为 null)。

相关性描述图形管道的哪些部分以怎样的方式使用内存资源。 每个子通道使用资源的方式都各不相同。 资源布局不仅仅定义它们如何使用资源。 部分子通道可能渲染图像或通过着色器图像保存数据。 其他子通道可能不使用图像,也可能在不同的图像管道阶段(即顶点或碎片)读取图像。

该信息可帮助驱动程序优化自动布局过渡,更常见的是优化子通道之间的壁垒。 仅在顶点着色器中写入图像时,等待碎片着色器执行(当前以已用图像的形式)的意义不大。 执行完所有顶点操作后,图像立即更改布局和内存访问类型,部分图形硬件甚至会开始执行后续操作(引用或读取特定图像),无需等待完成特定子通道的其他命令。 现在只需记住,相关性对性能非常重要。

现在我们已经准备了创建渲染通道需要的所有信息,可以安全地调用 vkCreateRenderPass()函数。

创建帧缓冲器

我们创建了渲染通道。 它描述渲染通道期间使用的所有附件和子通道。 但这种描述非常抽象。 我们指定了所有附件(本示例中仅一个)的格式,并描述了子通道(同样只有一个)如何使用附件。 但我们没有指定使用哪些附件,换句话说,哪些图像将用作这些附件。 这一过程将通过帧缓冲器完成。

帧缓冲器描述供渲染通道操作的特定图像。 在 OpenGL*中,帧缓冲器是我们将渲染的纹理(附件)集。 在 Vulkan 中,该术语的意义更加广泛。 它描述渲染通道期间使用的所有纹理(附件),不仅包括即将渲染的图像(颜色和深度/模板附件),还包括用作数据源的图像(输入附件)。

渲染通道和帧缓冲器的分开为我们提高了灵活性。 特定渲染通道可用于不同的帧缓冲器,特定帧缓冲器也可用于不同的渲染通道,如果它们相互兼容,表示它们能在具有相同类型和用法的图像上以相同的方式操作。

创建帧缓冲器之前,我们必须为每个用作帧缓冲器和渲染通道附件的图像创建图像视图。 在 Vulkan 中,不仅在有帧缓冲器的情况下,一般情况下我们都不操作图像本身。 不能直接访问图像。 为此,我们使用图像视图。 图像视图代表图像,它们“包装”图像并提供其他(元)数据。

创建图像视图

在该简单应用中,我们想直接渲染交换链图像。 我们创建了包含多个图像的交换链,因此必须为每个图像创建图像视图。

const std::vector<VkImage> &swap_chain_images = GetSwapChain().Images;
Vulkan.FramebufferObjects.resize( swap_chain_images.size() );

for( size_t i = 0; i < swap_chain_images.size(); ++i ) {
  VkImageViewCreateInfo image_view_create_info = {
    VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,   // VkStructureType                sType
    nullptr,                                    // const void                    *pNext
    0,                                          // VkImageViewCreateFlags         flags
    swap_chain_images[i],                       // VkImage                        image
    VK_IMAGE_VIEW_TYPE_2D,                      // VkImageViewType                viewType
    GetSwapChain().Format,                      // VkFormat                       format
    {                                           // VkComponentMapping             components
      VK_COMPONENT_SWIZZLE_IDENTITY,              // VkComponentSwizzle             r
      VK_COMPONENT_SWIZZLE_IDENTITY,              // VkComponentSwizzle             g
      VK_COMPONENT_SWIZZLE_IDENTITY,              // VkComponentSwizzle             b
      VK_COMPONENT_SWIZZLE_IDENTITY               // VkComponentSwizzle             a
    },
    {                                           // VkImageSubresourceRange        subresourceRange
      VK_IMAGE_ASPECT_COLOR_BIT,                  // VkImageAspectFlags             aspectMask
      0,                                          // uint32_t                       baseMipLevel
      1,                                          // uint32_t                       levelCount
      0,                                          // uint32_t                       baseArrayLayer
      1                                           // uint32_t                       layerCount
    }
  };

  if( vkCreateImageView( GetDevice(), &image_view_create_info, nullptr, &Vulkan.FramebufferObjects[i].ImageView ) != VK_SUCCESS ) {
    printf( "Could not create image view for framebuffer!\n" );
    return false;
  }

4.Tutorial03.cpp, function CreateFramebuffers()

为创建图像视图,必须首先创建类型变量 VkImageViewCreateInfo。 它包含以下字段:

  • sType – 结构类型,此处应设为 VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO。
  • pNext – 通常设为 null 的参数。
  • flags – 留作将来使用的参数。
  • image – 为其创建图像视图的图像的句柄。
  • viewType – 希望创建的视图类型。 视图类型必须兼容相应的图像。  (即我们可以为包含多个阵列层的图像创建 2D 视图,也可为包含 6 个层级的 2D 图像创建 CUBE 视图)。
  • format – 图像视图的格式;必须兼容图像的格式,不能为相同的格式(即可以是不同的格式,但没像素的位数必须相同)。
  • components – 将图像组件映射到通过纹理操作返回到着色器中的顶点。 这仅适用于读取操作(采样),但既然我们将图像用作颜色附件(渲染图像),那么必须设置身份映射(R 组件为 R,G -> G 等等)或仅使用“身份”值 (VK_COMPONENT_SWIZZLE_IDENTITY)。
  • subresourceRange – 描述视图可访问的 mipmap 层和阵列层集。 如果对图像进行 mipmap 处理,我们可以指定希望渲染的特定 mipmap 层(如果有渲染对象,必须精确指定某个阵列层的某个 mipmap 层)。

大家可以看这里,我们获取所有交换链图像的句柄,并在循环内引用它们。 这样我们填充创建图像视图所需的结构,这样我们前往 vkCreateImageView()函数。 每个与交换链一起创建的图像都进行这样的处理。

指定帧缓冲器参数

现在我们可以创建帧缓冲器。 为此我们调用 vkCreateFramebuffer()函数。

VkFramebufferCreateInfo framebuffer_create_info = {
    VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,  // VkStructureType                sType
    nullptr,                                    // const void                    *pNext
    0,                                          // VkFramebufferCreateFlags       flags
    Vulkan.RenderPass,                          // VkRenderPass                   renderPass
    1,                                          // uint32_t                       attachmentCount&Vulkan.FramebufferObjects[i].ImageView,    // const VkImageView             *pAttachments
    300,                                        // uint32_t                       width
    300,                                        // uint32_t                       height
    1                                           // uint32_t                       layers
  };

  if( vkCreateFramebuffer( GetDevice(), &framebuffer_create_info, nullptr, &Vulkan.FramebufferObjects[i].Handle ) != VK_SUCCESS ) {
    printf( "Could not create a framebuffer!\n" );
    return false;
  }
}
return true;

5.Tutorial03.cpp,函数 CreateFramebuffers()

vkCreateFramebuffer() 函数要求我们提供类型变量 VkFramebufferCreateInfo 的指示器,因此我们必须首先准备。 它包含以下字段:

  • sType – 结构类型,此处设为 VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO。
  • pNext – 大多数时候设为 null 的参数。
  • flags – 留作将来使用的参数。
  • renderPass – 帧缓冲器将兼容的渲染通道。
  • attachmentCount – 帧缓冲器中的附件(pAttachments 阵列中要素)数量。
  • pAttachments – 图像视图阵列,表示用于帧缓冲器和渲染通道的所有附件。 阵列中的要素(图像视图)对应渲染通道中的附件。
  • width – 帧缓冲器的宽度。
  • height – 帧缓冲器的高度。
  • layers – 帧缓冲器中的层数(OpenGL 借助几何图形着色器进行分层渲染,可将层级指定为将渲染哪些通过特定多边形实现光栅化的碎片)。

帧缓冲器指定哪些图像用作供渲染通道操作的附件。 可以说,它可将图像(图像视图)过渡成特定附件。 为帧缓冲器指定的图像数量必须等于为其创建帧缓冲器的渲染通道中的附件数量。 而且,每个 pAttachments 阵列的要素直接对应渲染通道描述结构中的附件。 渲染通道和帧缓冲器紧密相连,因此我们必须在帧缓冲器创建期间指定渲染通道。 不过,我们不仅可以将帧缓冲器用于指定的渲染通道,还可用于兼容某一指定通道的渲染通道。 一般来说,兼容的渲染通道必须有相同数量的附件,而且对应附件的格式和样本数量必须相同。 但图像布局(初始布局、最终布局,以及面向各子通道的布局)可能各不相同,也不涉及渲染通道兼容性。

完成创建并填充 VkFramebufferCreateInfo 结构后,我们调用 vkCreateFramebuffer() 函数。

上述代码在循环中执行。 帧缓冲器引用图像视图。 此处图像视图针对各交换链图像创建。 因此我们要为交换链图像及其视图创建帧缓冲器。 我们这样做的目的是为了简化渲染循环中调用的代码。 在正常、真实的场景中,我们不(可能)为交换链图像创建帧缓冲器。 我假设了一种更好的解决方法,即渲染单个图像(纹理),然后使用命令缓冲区将该图像的渲染结构拷贝至特定交换链图像。 这样我们就只有三个连接至交换链的简单命令缓冲区。 其他渲染命令独立于交换链,因此更加易于维护。

创建图形管道

现在我们准备好创建图形管道。 管道是逐个处理数据的阶段集合。 Vulkan 中目前有计算管道和图形管道。 计算管道支持我们执行计算工作,比如对游戏中的对象执行物理计算。 图形管道用于绘制操作。

OpenGL 中有多个可编程阶段(顶点、镶嵌、碎片着色器等)和一些固定功能阶段(光栅器、深度测试、混合等)。 Vulkan 中的情况相似。 有些阶段比较类似(如果不相同)。 但整个管道的状态聚集在一个整体对象中。 OpenGL 支持我们随时更改影响渲染操作的状态,我们(大部分时候)可以独立更改各阶段的参数。 我们可以设置着色器程序、深度测试、混合,以及希望的各种状态,然后还可以渲染一些对象。 接下来我们可以仅更改一小部分状态,并渲染另一对象。 在 Vulkan 中不能执行这类操作(可以说管道具有“免疫力”)。 我们必须准备整个状态,设置管道阶段的参数,并将它们分成管道对象组。 对我来说,一开始这是最令人震惊的信息。 我不能随时更改着色器程序? 为什么?

最简单有效的解释是,因为这种状态会改变性能影响。 更改整个管道的单个状态可能导致图形硬件执行状态、错误检查等多项后台操作。 不同的硬件厂商可能(并通常)以不同的方式实施此功能。 如果在不同的图形硬件上执行,这样会导致应用以不同的方式执行(意味着会不可预测地影响性能)。 因此对开发人员来说,能够随时更改是一项非常方便的功能。 但遗憾的是,硬件因此会不太方便。

所以在 Vulkan 中,整个管道的状态聚集在一个单个对象中。 创建管道对象时执行所有相关的状态和错误检查。 如果出现问题(比如管道的不同部分设置的不兼容),管道对象创建将失败。 但我们提前了解了这点。 驱动程序无需担心,可以放心地使用毁坏的管道。 它会立即告诉我们这一问题。 但在真正使用期间,在应用的性能关键部分,一切都要正确设置和使用。

这种方法的缺点是,如果以不同的方式(一些不透明、一些半透明,一些启用深度测试等等)绘制对象,我们必须创建多个管道对象,管道对象的多个变量。 遗憾的是,由于着色器不同,不得不创建不同的管道对象。 如果想使用不同的着色器绘制对象,还必须创建多个管道对象,逐个整合着色器程序。 着色器还连接至整个管道状态。 它们使用不同的资源(比如纹理和缓冲区)、渲染不同的颜色附件,并读取不同的附件(可能是之前已渲染过的)。 必须初始化、准备并正确设置这些连接。 我们知道我们的目的,但驱动程序不知道。 因此由我们(而非驱动程序)进行是符合逻辑的最好办法。 一般来说这种方法比较有意义。

开始管道创建流程时,先从着色器开始。

创建着色器模块

创建图形管道要求我们以结构或结构阵列的形式准备大量数据。 第一个数据是所有着色器阶段和着色器程序(在渲染期间用于绑定的特定图形管道)的集合。

在 OpenGL 中我们用 GLSL 编写着色器。 它们经过编译,然后直接链接至应用中的着色器程序。 我们可以在应用中随时使用或停止着色器程序。

而 Vulkan 仅接收着色器的二进制形式 — 一种称为 SPIR-V 的中间语言。不能像在 OpenGL 中那样提供 GLSL 代码。 但有一种官方的独立编译器能够将用 GLSL 编写的着色器转换成二进制 SPIR-V 语言。 为使用该编译器,我们必须离线操作。 准备 SPIR-V 汇编后,我们可以通过它创建着色器模块。 然后将模块合成 VkPipelineShaderStageCreateInfo 结构阵列,从而与其他参数一起用于创建图形管道。

以下代码可通过包含二进制 SPIR-V 的指定文件创建着色器模块。

const std::vector<char> code = Tools::GetBinaryFileContents( filename );
if( code.size() == 0 ) {
  return Tools::AutoDeleter<VkShaderModule, PFN_vkDestroyShaderModule>();
}

VkShaderModuleCreateInfo shader_module_create_info = {
  VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,    // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  0,                                              // VkShaderModuleCreateFlags      flags
  code.size(),                                    // size_t                         codeSize
  reinterpret_cast<const uint32_t*>(&code[0])     // const uint32_t                *pCode
};

VkShaderModule shader_module;
if( vkCreateShaderModule( GetDevice(), &shader_module_create_info, nullptr, &shader_module ) != VK_SUCCESS ) {
  printf( "Could not create shader module from a %s file!\n", filename );
  return Tools::AutoDeleter<VkShaderModule, PFN_vkDestroyShaderModule>();
}

return Tools::AutoDeleter<VkShaderModule, PFN_vkDestroyShaderModule>( shader_module, vkDestroyShaderModule, GetDevice() );

6.Tutorial03.cpp,函数 CreateShaderModule()

首先准备包含以下字段的 VkShaderModuleCreateInfo 结构:

  • sType – 结构类型,本示例中设为 VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO。
  • pNext – 目前不使用的指示器。
  • flags – 留作将来使用的参数。
  • codeSize – 传递至 pCode 参数中的代码大小(字节)。
  • pCode –包含源代码(二进制 SPIR-V 汇编)的阵列指示器。

为获取文件内容,我准备了一个简单的效用函数 GetBinaryFileContents(),可读取指定文件的所有内容。 它以字符矢量的形式返回内容。

准备该结构后,我们调用 vkCreateShaderModule()函数并检查一切是否正常运行。

Tools 命名空间的 AutoDeleter<> 类是一种帮助类,可包装特定的 Vulkan 对象句柄,并提取用于删除该对象的函数。 该类与智能指示器类似,可在对象(智能指示器)超出范围时删除分配的内存。 AutoDeleter<> 可提取特定对象的句柄,并在这类对象的类型超出范围时运用提供的函数删除该对象。

template<class T, class F>
class AutoDeleter {
public:
  AutoDeleter() :
    Object( VK_NULL_HANDLE ),
    Deleter( nullptr ),
    Device( VK_NULL_HANDLE ) {
  }

  AutoDeleter( T object, F deleter, VkDevice device ) :
    Object( object ),
    Deleter( deleter ),
    Device( device ) {
  }

  AutoDeleter( AutoDeleter&& other ) {
    *this = std::move( other );
  }

  ~AutoDeleter() {
    if( (Object != VK_NULL_HANDLE) && (Deleter != nullptr) && (Device != VK_NULL_HANDLE) ) {
      Deleter( Device, Object, nullptr );
    }
  }

  AutoDeleter& operator=( AutoDeleter&& other ) {
    if( this != &other ) {
      Object = other.Object;
      Deleter = other.Deleter;
      Device = other.Device;
      other.Object = VK_NULL_HANDLE;
    }
    return *this;
  }

  T Get() {
    return Object;
  }

  bool operator !() const {
    return Object == VK_NULL_HANDLE;
  }

private:
  AutoDeleter( const AutoDeleter& );
  AutoDeleter& operator=( const AutoDeleter& );
  T         Object;
  F         Deleter;
  VkDevice  Device;
};

7.Tools.h

为何我们如此费力地处理一个简单对象? 着色器模块是创建图形管道所需的对象之一。 但创建管道后,我就不再需要这些着色器模块。 将它们保留下来有时会非常方便,因为我们可能需要创建其他类似的管道。 但在本示例中,创建完图形管道后,我们需要安全地毁坏它们。 通过调用 vkDestroyShaderModule()函数毁坏着色器模块。 但在本示例中,我们需要在多个地方(多个“ifs”中和整个函数末尾)调用该函数。 因为我们不想忘记哪里需要调用该函数,同时不想出现内存泄漏情况,因此为了方便我准备了这个简单的类。 现在我们不需要记住删除创建的着色器模块,因为它会自动删除。

准备着色器阶段描述

知道如何创建和毁坏着色器模块后,现在我们可以创建着色器阶段数据,以组成图形管道。 正如我所写的,描述哪些着色器阶段应在绑定特定图形管道时处于活跃状态的数据,其形式是包含类型 VkPipelineShaderStageCreateInfo 的要素阵列。 以下代码可创建着色器模块并准备此类阵列:

Tools::AutoDeleter<VkShaderModule, PFN_vkDestroyShaderModule> vertex_shader_module = CreateShaderModule( "Data03/vert.spv" );
Tools::AutoDeleter<VkShaderModule, PFN_vkDestroyShaderModule> fragment_shader_module = CreateShaderModule( "Data03/frag.spv" );

if( !vertex_shader_module || !fragment_shader_module ) {
  return false;
}

std::vector<VkPipelineShaderStageCreateInfo> shader_stage_create_infos = {
  // Vertex shader
  {
    VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,        // VkStructureType                                sType
    nullptr,                                                    // const void                                    *pNext
    0,                                                          // VkPipelineShaderStageCreateFlags               flags
    VK_SHADER_STAGE_VERTEX_BIT,                                 // VkShaderStageFlagBits                          stage
    vertex_shader_module.Get(),                                 // VkShaderModule                                 module"main",                                                     // const char                                    *pName
    nullptr                                                     // const VkSpecializationInfo                    *pSpecializationInfo
  },
  // Fragment shader
  {
    VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,        // VkStructureType                                sType
    nullptr,                                                    // const void                                    *pNext
    0,                                                          // VkPipelineShaderStageCreateFlags               flags
    VK_SHADER_STAGE_FRAGMENT_BIT,                               // VkShaderStageFlagBits                          stage
    fragment_shader_module.Get(),                               // VkShaderModule                                 module"main",                                                     // const char                                    *pName
    nullptr                                                     // const VkSpecializationInfo                    *pSpecializationInfo
  }
};

8.Tutorial03.cpp,函数 CreatePipeline()

一开始我们创建两个面向顶点和碎片阶段的着色器模块。 用之前所述的函数创建。 如果出现错误,我们从 CreatePipeline()函数返回,创建的模块将通过包装程序类和提供的删除函数自动删除。

面向着色器模块的代码从包含二进制 SPIR-V 汇编的文件中读取。 这些文件由应用“glslangValidator”生成。 它是一种通过 Vulkan SDK 正式发布的工具,设计目的是为了验证 GLSL 着色器。 但“glslangValidator”也具备编译或将GLSL 着色器转换为 SPIR-V 二进制文件的功能。 官方 SDK 网站提供了有关该用法命令行的完整解释。 我用以下命令生成用于本教程的 SPIR-V 着色器:

glslangValidator.exe -V -H shader.vert > vert.spv.txt

glslangValidator.exe -V -H shader.frag > frag.spv.txt

“glslangValidator”提取指定文件并通过该文件生成 SPIR-V 文件。 输入文件的扩展文件(顶点着色器的“.vert”、几何图形着色器的“.geom”等)将自动检测着色器阶段的类型。 生成文件的名称可以指定,也可以默认为“<stage>.spv”形式。 因此在本示例中将生成“vert.spv”和“frag.spv”文件。

SPIR-V 文件为二进制格式,不容易读取和分析 — 但也有可能。 使用“-H”选项时,“glslangValidator”以易于读取的形式输出 SPIR-V。 这种形式打印为标准输出,因此我使用“> *.spv.txt”重定向运算符。

以下内容是为顶点阶段生成 SPIR-V 汇编的“shader.vert”文件内容:

#version 400

void main() {
    vec2 pos[3] = vec2[3]( vec2(-0.7, 0.7), vec2(0.7, 0.7), vec2(0.0, -0.7) );
    gl_Position = vec4( pos[gl_VertexIndex], 0.0, 1.0 );
}

9.shader.vert

大家看,我对用于渲染三角形的所有顶点位置进行了硬编码。 通过特定于 Vulkan 的“gl_VertexIndex”内置变量为它们编入了索引。 在最简单的场景中,当(此时)使用非索引绘制命令时,该数值从绘制命令的“firstVertex”参数的数值(在提供的示例中为 0)开始。

我之前写过这一具有争议的部分 — 这种方法可接受,也有效,但不太便于维护,也支持我们跳过创建图形管道所需的“结构填充”部分。 我选择使用这种方法,是为了尽可能地缩短和简化本教程。 在下一教程中,我将演示一种更常用的顶点数量绘制方法,类似于在 OpenGL 中使用顶点阵列和索引。

以下是“shader.frag”文件的碎片着色器的源代码,用于生成面向碎片阶段的 SPIRV-V 汇编:

#version 400

layout(location = 0) out vec4 out_Color;

void main() {
  out_Color = vec4( 0.0, 0.4, 1.0, 1.0 );
}

10.shader.frag

在 Vulkan 着色器中(当从 GLSL 转换成 SPIR-V 时),需要使用布局限定符。 这里我们指定哪些输出(颜色)附件保存碎片着色器生成的颜色值。 因为我们仅使用一个附件,所以必须指定第一个可用位置(零)。

现在了解如何为使用 Vulkan 的应用准备着色器后,就可以进行下一步了。 创建两个着色器模块后,检查这些操作是否成功。 如果成功,我们可以开始准备着色器阶段描述,以继续创建图形管道。

我们需要为每个启用的着色器阶段准备 VkPipelineShaderStageCreateInfo 结构实例。 这些结构阵列及其要素数量一起用于图形管道创建信息结构(提供给创建图形管道的函数)。 VkPipelineShaderStageCreateInfo 结构包含以下字段:

  • sType – 我们所准备的结构类型,此处必须等于 VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 留作将来使用的参数。
  • stage – 我们描述的着色器阶段类型(比如顶点、镶嵌控制等)。
  • module – 着色器模块句柄,包含用于特定阶段的着色器。
  • pName – 提供的着色器的切入点名称。
  • pSpecializationInfo – VkSpecializationInfo 结构指示器,留作现在使用,设为 null。

创建图形管道时,我们不创建太多 (Vulkan) 对象。 大部分数据只用此类结构的形式展示。

准备顶点输入描述

现在我们必须提供用于绘制的输入数据描述。 这类似于 OpenGL 的顶点数据:属性、组件数量、供数据提取的缓冲区、数据步长,或步进率。 当然,在 Vulkan 中准备这类数据的方式不同,但大体意思相同。 幸运的是,在本教程中由于顶点数据已硬编码成顶点着色器,因此我们几乎可以完全跳过这一步骤,并用 null 和零填充 VkPipelineVertexInputStateCreateInfo:

VkPipelineVertexInputStateCreateInfo vertex_input_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,    // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineVertexInputStateCreateFlags          flags;
  0,                                                            // uint32_t                                       vertexBindingDescriptionCount
  nullptr,                                                      // const VkVertexInputBindingDescription         *pVertexBindingDescriptions
  0,                                                            // uint32_t                                       vertexAttributeDescriptionCount
  nullptr                                                       // const VkVertexInputAttributeDescription       *pVertexAttributeDescriptions
};

11. Tutorial03.cpp,函数 CreatePipeline()

为清晰起见,以下为 VkPipelineVertexInputStateCreateInfo 结构的成员描述:

  • sType – 结构类型,此处为 VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO。
  • pNext – 特定于扩展的结构指示器。
  • flags – 留作将来使用的参数。
  • vertexBindingDescriptionCount – pVertexBindingDescriptions 阵列中的要素数量。
  • pVertexBindingDescriptions – 包含描述输入顶点数据(步长和步进率)的要素阵列。
  • vertexAttributeDescriptionCount – pVertexAttributeDescriptions 阵列中的要素数量。
  • pVertexAttributeDescriptions – 包含描述顶点属性(位置、格式、位移)的要素阵列。

准备输入汇编描述

下一步骤要求描述如何将顶点汇编成基元。 和 OpenGL 一样,我们必须指定欲使用的拓扑:点、线、三角形、三角扇等。

VkPipelineInputAssemblyStateCreateInfo input_assembly_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,  // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineInputAssemblyStateCreateFlags        flags
  VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,                          // VkPrimitiveTopology                            topology
  VK_FALSE                                                      // VkBool32                                       primitiveRestartEnable
};

12.Tutorial03.cpp,函数 CreatePipeline()

我们通过 VkPipelineInputAssemblyStateCreateInfo 结构完成该步骤,其中包含以下成员:

  • sType – 结构类型,此处设为 VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO。
  • pNext – 目前不使用的指示器。
  • flags – 留作将来使用的参数。
  • topology – 描述如何组织顶点以形成基元的参数。
  • primitiveRestartEnable – 告知特定索引值(执行索引绘制时)是否重新开始汇编特定基元的参数。

准备视口描述

我们已处理完输入数据。 现在必须指定输出数据的形式,图形管道连接碎片(比如光栅化、窗口(视口)、深度测试等)的所有部分。 这里必须准备的第一个数据集为视口状态,以指定我希望绘制哪部分图形(或纹理,或窗口)。

VkViewport viewport = {
  0.0f,                                                         // float                                          x
  0.0f,                                                         // float                                          y
  300.0f,                                                       // float                                          width
  300.0f,                                                       // float                                          height
  0.0f,                                                         // float                                          minDepth
  1.0f                                                          // float                                          maxDepth
};

VkRect2D scissor = {
  {                                                             // VkOffset2D                                     offset
    0,                                                            // int32_t                                        x
    0                                                             // int32_t                                        y
  },
  {                                                             // VkExtent2D                                     extent
    300,                                                          // int32_t                                        width
    300                                                           // int32_t                                        height
  }
};

VkPipelineViewportStateCreateInfo viewport_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,        // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineViewportStateCreateFlags             flags
  1,                                                            // uint32_t                                       viewportCount
  &viewport,                                                    // const VkViewport                              *pViewports
  1,                                                            // uint32_t                                       scissorCount&scissor                                                      // const VkRect2D                                *pScissors
};

13.Tutorial03.cpp,函数 CreatePipeline()

在本示例中,用法很简单:仅将视口坐标设置为预定义的值。 不用检查待渲染的交换链图像的大小。 但请记住,在真实生产应用中,必须执行这一操作,因为规范规定,视口的尺寸不能超过待渲染的附件尺寸。

为指定视口参数,我们填充包含以下字段的 VkViewport 结构:

  • x – 视口左侧。
  • y – 视口上侧。
  • width – 视口的宽度。
  • height – 视口的高度。
  • minDepth – 用于深度计算的最小深度值。
  • maxDepth – 用于深度计算的最大深度值。

指定视口坐标时,请记住,起点与 OpenGL 中的不同。 此处我们指定视口的左上角(而非左下角)。

另外一点值得注意的是,minDepth 和 maxDepth 值必须位于 0.0 - 1.0(包含 1.0)之间,但 maxDepth 可以小于 minDepth。 这样会以“相反”的顺序计算深度。

接下来必须指定用于 scissor 测试的参数。 scissor 测试与 OpenGL 类似,将碎片生成仅限制在指定的矩形区域。 但在 Vulkan 中,scissor 测试始终处于启用状态,无法关闭。 我们仅提供与为视口提供的相似的值。 尝试更改这些值,看看对生成的图像产生怎样的影响。

scissor 测试没有专用的结构。 为提供用于该测试的数据,我们填充 VkRect2D 结构,其中包含两个类似的结构成员。 第一个是 VkOffset2D,包含以下成员:

  • x – 用于 scissor 测试的矩形区域的左侧
  • y – 矩形区域的上侧

第二个成员的类型为 VkExtent2D,并包含以下字段:

  • width – scissor 矩形区域的宽度
  • height – scissor 区域的高度

一般来说,通过 VkRect2D 结构为 scissor 测试提供的数据与为视口准备的数据在意义上相似。

为视口和 scissor 测试准备数据后,我们最后可以填充用于创建管道的结构。 该结构称为 VkPipelineViewportStateCreateInfo,并包含以下字段:

  • sType – 结构类型,此处为 VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 留作将来使用的参数。
  • attachmentCount – pViewports 阵列中的要素数量。
  • pViewports – 描述绑定特定管道时所使用的视口参数的要素阵列。
  • scissorCount – pScissors 阵列中的要素数量。
  • pScissors – 描述针对各视口的 scissor 测试参数的要素阵列。

请记住,viewportCount 和 scissorCount 参数必须相等。 我们还允许指定更多视口,但之后必须启用 multiViewport 特性。

准备光栅化状态描述

图形管道创建的下一部分适用于光栅化状态。 我们必须指定如何对多边形进行光栅化(改成碎片),是希望为整个多边形生成碎片,还是仅为边缘生成碎片(多边形模式),或者希望看到多边形的正面或背面,还是同时看到这两面(背面剔除)。 我们还可以提供深度偏差参数,或指明是否希望启用深度夹紧 (depth clamp)。 整个状态将封装至 VkPipelineRasterizationStateCreateInfo。 它包含以下成员:

  • sType – 结构类型,本示例中为 VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 留作将来使用的参数。
  • depthClampEnable – 描述是希望将光栅化基元的深度值夹在视锥上(真值),还是希望进行正常裁剪(假值)的参数。
  • rasterizerDiscardEnable – 禁用碎片生成(在光栅化关闭碎片着色器之前舍弃基元)。
  • polygonMode – 控制如何为特定基元生成碎片(三角形模式):为整个三角形生成,仅为其边缘生成,或是仅为其顶点生成。
  • cullMode – 选择用于剔除的三角形面(如果启用)。
  • frontFace – 选择将哪个面视作正面(取决于缠绕顺序)。
  • depthBiasEnable – 启用或禁用偏置碎片的深度值。
  • depthBiasConstantFactor – 启用偏置时添加至碎片深度值的常数因子。
  • depthBiasClamp – 适用于碎片深度的最大(或最小)偏差值。
  • depthBiasSlopeFactor – 启用偏置时在深度计算期间适用于碎片斜度的因子。
  • lineWidth – 光栅化线条的宽度。

以下源代码负责设置本示例中的光栅化状态:

VkPipelineRasterizationStateCreateInfo rasterization_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,   // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineRasterizationStateCreateFlags        flags
  VK_FALSE,                                                     // VkBool32                                       depthClampEnable
  VK_FALSE,                                                     // VkBool32                                       rasterizerDiscardEnable
  VK_POLYGON_MODE_FILL,                                         // VkPolygonMode                                  polygonMode
  VK_CULL_MODE_BACK_BIT,                                        // VkCullModeFlags                                cullMode
  VK_FRONT_FACE_COUNTER_CLOCKWISE,                              // VkFrontFace                                    frontFace
  VK_FALSE,                                                     // VkBool32                                       depthBiasEnable
  0.0f,                                                         // float                                          depthBiasConstantFactor
  0.0f,                                                         // float                                          depthBiasClamp
  0.0f,                                                         // float                                          depthBiasSlopeFactor
  1.0f                                                          // float                                          lineWidth
};

14.Tutorial03.cpp,函数 CreatePipeline()

在本教程中,我们禁用尽可能多的参数,以简化流程、代码和渲染操作。 这里的重要参数可设置适用于多边形光栅化、背面剔除,以及类似于 OpenGL 的逆时针正面的(典型)填充模式。 深度偏置和夹紧也处于禁用状态(要启用深度夹紧,我们首先需要在逻辑设备创建期间启用专用特定;同样,对多边形模式来说,我们也必须进行相同的操作,而非“填充”)。

设置多点采样状态描述

在 Vulkan 中,创建图形管道时,我们还必须指定与多点采用相关的状态。 该步骤可使用 VkPipelineMultisampleStateCreateInfo 结构来完成。 它包含以下成员:

  • sType – 结构类型,此处为 VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 留作将来使用的参数。
  • rasterizationSamples – 用于光栅化的每像素样本数量。
  • sampleShadingEnable – 指定是按照样本着色(启用),还是按照碎片着色(禁用)的参数。
  • minSampleShading – 指定应用于特定碎片着色期间的独有样本位置的最少数量。
  • pSampleMask – 静态覆盖范围样本掩码的阵列指示器;可设为 null。
  • alphaToCoverageEnable – 控制碎片的阿尔法值是否用于覆盖范围计算。
  • alphaToOneEnable – 控制是否替换碎片的阿尔法值。

在本示例中,我希望最大限度减少问题的发生,因此将参数设为通常禁用多点采样的值 — 每特定像素仅一个样本,其他参数均处于关闭状态。 请记住,如果我们希望启用样本着色或将阿尔法设为 1,还必须分别启用两个特性。 以下是用于准备 VkPipelineMultisampleStateCreateInfo 结构的源代码:

VkPipelineMultisampleStateCreateInfo multisample_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,     // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineMultisampleStateCreateFlags          flags
  VK_SAMPLE_COUNT_1_BIT,                                        // VkSampleCountFlagBits                          rasterizationSamples
  VK_FALSE,                                                     // VkBool32                                       sampleShadingEnable
  1.0f,                                                         // float                                          minSampleShading
  nullptr,                                                      // const VkSampleMask                            *pSampleMask
  VK_FALSE,                                                     // VkBool32                                       alphaToCoverageEnable
  VK_FALSE                                                      // VkBool32                                       alphaToOneEnable
};

15.Tutorial03.cpp,函数 CreatePipeline()

设置混合状态描述

在创建图形管道期间,我们另外还需准备混合状态(它还包括逻辑操作)。

VkPipelineColorBlendAttachmentState color_blend_attachment_state = {
  VK_FALSE,                                                     // VkBool32                                       blendEnable
  VK_BLEND_FACTOR_ONE,                                          // VkBlendFactor                                  srcColorBlendFactor
  VK_BLEND_FACTOR_ZERO,                                         // VkBlendFactor                                  dstColorBlendFactor
  VK_BLEND_OP_ADD,                                              // VkBlendOp                                      colorBlendOp
  VK_BLEND_FACTOR_ONE,                                          // VkBlendFactor                                  srcAlphaBlendFactor
  VK_BLEND_FACTOR_ZERO,                                         // VkBlendFactor                                  dstAlphaBlendFactor
  VK_BLEND_OP_ADD,                                              // VkBlendOp                                      alphaBlendOp
  VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |         // VkColorComponentFlags                          colorWriteMask
  VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT
};

VkPipelineColorBlendStateCreateInfo color_blend_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,     // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineColorBlendStateCreateFlags           flags
  VK_FALSE,                                                     // VkBool32                                       logicOpEnable
  VK_LOGIC_OP_COPY,                                             // VkLogicOp                                      logicOp
  1,                                                            // uint32_t                                       attachmentCount
  &color_blend_attachment_state,                                // const VkPipelineColorBlendAttachmentState     *pAttachments
  { 0.0f, 0.0f, 0.0f, 0.0f }                                    // float                                          blendConstants[4]
};

16.Tutorial03.cpp,函数 CreatePipeline()

VkPipelineColorBlendStateCreateInfo 结构可用于设置最终颜色操作。 它包含以下字段:

  • sType – 结构类型,在本示例中设置为 VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO。
  • pNext – 留作将来特定用于扩展的指示器。
  • flags – 同样留作将来使用的参数。
  • logicOpEnable – 指示是否希望启用有关像素的逻辑操作。
  • logicOp – 预执行的逻辑操作类型(比如拷贝、清空等)。
  • attachmentCount – pAttachments 阵列中的要素数量。
  • pAttachments – 包含颜色附件状态参数的阵列,其中这些颜色附件用于子通道,以便其绑定特定图形管道。
  • blendConstants – 包含四个要素以及用于混合操作的颜色值的阵列(使用专用混合因子时)。

attachmentCount 和 pAttachments 需要更多信息。 如果想执行绘制操作,我们需要设置参数,其中最重要的是图形管道、渲染通道和帧缓冲器。 显卡需要知道绘制的方式(描述渲染状态、着色器、测试等的图形管道)和绘制的位置(渲染通道提供通用设置;帧缓冲器指定使用哪些图像)。 我之前说过,渲染通道指定如何排列操作、具有哪些相关性、何时渲染特定附件,以及何时读取相同附件。 这些阶段以子通道的形式执行。 我们可以(但不是必须)为每项绘制操作启用/使用不同的管道。 我们进行绘制时,必须记住要将绘制成附件集。 该集合在渲染通道中定义,其中描述所有的颜色、输入和深度附件(帧缓冲器仅指定哪些图像用于这些附件)。 就混合状态而言,我们可以指定是否希望启用混合。 这一操作通过 pAttachments 阵列来完成。 每个要素都必须对应渲染通道中定义的颜色附件。 因此 pAttachments 阵列中 attachmentCount 要素的值必须等于渲染通道中定义的颜色附件数量。

还有一个限制条件。 在默认情况下,pAttachments 阵列中的所有要素都必须包含相同的值,必须以相同的方式指定,而且必须相同。 默认情况下,执行混合(和颜色掩码)的方式与所有附件相同。 为何是一个阵列? 为什么只需指定一个值? 因为有一项特性可以支持我们执行为每个活跃的颜色附件独立、独特的混合。 如果在创建设备期间启用独立混合,我们可以为各颜色附件提供不同的值。

pAttachments 阵列要素的类型为 VkPipelineColorBlendAttachmentState。 该结构包含以下成员:

  • blendEnable – 指示是否希望启用混合。
  • srcColorBlendFactor – 面向源(入站)碎片颜色的混合因子。
  • dstColorBlendFactor – 面向目标颜色的混合因子(保存在帧缓冲器中,位置与入站碎片相同)。
  • colorBlendOp – 待执行操作的类型(乘法、加法等)。
  • srcAlphaBlendFactor – 面向源(入站)碎片阿尔法值的混合因子。
  • dstAlphaBlendFactor – 面向目标阿尔法值的混合因子(保存在帧缓冲器中)。
  • alphaBlendOp – 面向阿尔法混合执行的操作类型。
  • colorWriteMask – 选择(启用)编写哪个 RGBA 组件的位掩码。

在本示例中,我们禁用混合操作,这样其他所有参数都将处于不相关状态。 除 colorWriteMask 外,我们选择编写所有组件,但您可以自由检查该参数变成其他 R、G、B、A 组合后,将会发生什么。

创建管道布局

创建管道之前,我们需要做的最后一件事是创建相应的管道布局。 管道布局描述管道可访问的所有资源。 在本示例中,我们必须指定着色器将使用多少纹理,以及哪些着色器阶段将访问它们。 当然还会涉及到其他资源。 除着色器阶段外,我们还必须描述资源的类型(纹理、缓冲区)、总数量,以及布局。 该布局可以比作 OpenGL 的活跃纹理和 shader uniform。 在 OpenGL 中,我们将纹理绑定至所需的纹理图像单元,而且不为 shader uniform 提供纹理句柄,而是提供纹理图像单元(绑定实际纹理)的 ID(我们提供与特定纹理相关的单元编号)。

Vulkan 中的情况类似。 我们创建某种内存布局形式:首先是两个缓冲区,接下来是三个纹理和一个图像。 这种内存“结构”称为集,这些集的集合将提供给管道。 在着色器中,我们使用这些集(布局)中的内存“位置”访问指定的资源。 这可通过布局 (set = X, binding = Y) 分类符来完成,也可以解释为:从 Y 内存位置和 X 集提取资源。

管道布局可视作着色器阶段和着色器资源之间的交互,因为它提取这些资源组,并描述如何收集并向管道提供这些资源。

该流程比较复杂,我计划为其另外编写一节教程。 这里我们不使用其他资源,因为我展示的是关于创建“空”管道布局的示例。

VkPipelineLayoutCreateInfo layout_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,  // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  0,                                              // VkPipelineLayoutCreateFlags    flags
  0,                                              // uint32_t                       setLayoutCount
  nullptr,                                        // const VkDescriptorSetLayout   *pSetLayouts
  0,                                              // uint32_t                       pushConstantRangeCount
  nullptr                                         // const VkPushConstantRange     *pPushConstantRanges
};

VkPipelineLayout pipeline_layout;
if( vkCreatePipelineLayout( GetDevice(), &layout_create_info, nullptr, &pipeline_layout ) != VK_SUCCESS ) {
  printf( "Could not create pipeline layout!\n" );
  return Tools::AutoDeleter<VkPipelineLayout, PFN_vkDestroyPipelineLayout>();
}

return Tools::AutoDeleter<VkPipelineLayout, PFN_vkDestroyPipelineLayout>( pipeline_layout, vkDestroyPipelineLayout, GetDevice() );

17.Tutorial03.cpp,函数 CreatePipelineLayout()

为创建管道布局,必须首先创建类型变量 VkPipelineLayoutCreateInfo。 它包含以下字段:

  • sType – 结构类型,本示例中为 VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO。
  • pNext – 为扩展功能预留的参数。
  • flags – 留作将来使用的参数。
  • setLayoutCount – 本布局中包含的描述符集的数量。
  • pSetLayouts – 包含描述符布局描述的阵列指示器。
  • pushConstantRangeCount – push constant 范围(稍后介绍)的数量。
  • pPushConstantRanges – 描述(特定管道中)着色器中使用的所有 push constant 范围的阵列。

在本示例中,我们创建“空”布局,因此几乎所有字段都设置为 null 或零。

我们在这里不使用 push constant,不过这一概念值得介绍。 Vulkan 中的 push constant 支持我们修改用于着色器的常量变量的数据。 这里为 push constant 预留了少量的特殊内存。 我们通过 Vulkan 命令(而非内存更新)更新其数值,push constant 值的更新速度预计快于正常内存写入。

如上述示例所示,我还会将管道布局包装至“AutoDeleter”对象。 管道创建、描述符集绑定(启用/激活着色器与着色器资源之间的交互),以及 push constant 设置期间都需要管道布局。 除管道创建外,本教程不执行任何其他操作。 因此在这里,创建管道后,不再需要使用布局。 为避免内存泄漏,离开创建图形管道的函数后,我立即使用了该帮助类毁坏布局。

创建图形管道

现在我们准备了创建图形管道需要的所有资源。 以下代码可帮助完成这一操作:

Tools::AutoDeleter<VkPipelineLayout, PFN_vkDestroyPipelineLayout> pipeline_layout = CreatePipelineLayout();
if( !pipeline_layout ) {
  return false;
}

VkGraphicsPipelineCreateInfo pipeline_create_info = {
  VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,              // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineCreateFlags                          flags
  static_cast<uint32_t>(shader_stage_create_infos.size()),      // uint32_t                                       stageCount&shader_stage_create_infos[0],                                // const VkPipelineShaderStageCreateInfo         *pStages&vertex_input_state_create_info,                              // const VkPipelineVertexInputStateCreateInfo    *pVertexInputState;&input_assembly_state_create_info,                            // const VkPipelineInputAssemblyStateCreateInfo  *pInputAssemblyState
  nullptr,                                                      // const VkPipelineTessellationStateCreateInfo   *pTessellationState&viewport_state_create_info,                                  // const VkPipelineViewportStateCreateInfo       *pViewportState&rasterization_state_create_info,                             // const VkPipelineRasterizationStateCreateInfo  *pRasterizationState&multisample_state_create_info,                               // const VkPipelineMultisampleStateCreateInfo    *pMultisampleState
  nullptr,                                                      // const VkPipelineDepthStencilStateCreateInfo   *pDepthStencilState&color_blend_state_create_info,                               // const VkPipelineColorBlendStateCreateInfo     *pColorBlendState
  nullptr,                                                      // const VkPipelineDynamicStateCreateInfo        *pDynamicState
  pipeline_layout.Get(),                                        // VkPipelineLayout                               layout
  Vulkan.RenderPass,                                            // VkRenderPass                                   renderPass
  0,                                                            // uint32_t                                       subpass
  VK_NULL_HANDLE,                                               // VkPipeline                                     basePipelineHandle
  -1                                                            // int32_t                                        basePipelineIndex
};

if( vkCreateGraphicsPipelines( GetDevice(), VK_NULL_HANDLE, 1, &pipeline_create_info, nullptr, &Vulkan.GraphicsPipeline ) != VK_SUCCESS ) {
  printf( "Could not create graphics pipeline!\n" );
  return false;
}
return true;

18.Tutorial03.cpp,函数 CreatePipeline()

我们首先创建封装在“AutoDeleter” 对象中的管道布局。 接下来填充 VkGraphicsPipelineCreateInfo 类型的结构 。 它包含多个字段。 以下简要介绍它们:

  • sType – 结构类型,此处为 VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO。
  • pNext – 留作将来用于扩展的参数。
  • flags – 这次该参数不留做将来使用,而是用于控制如何创建管道:是创建衍生管道(如果承袭另一管道),还是支持从该管道创建衍生管道。 我们还可以禁用优化,这样可缩短创建管道的时间。
  • stageCount – pStages 参数中描述的阶段数据;必须大于 0。
  • pStages – 包含活跃着色器阶段(用着色器模块创建而成)描述的阵列;各阶段必须具有唯一性(指定特定阶段的数量不能超过一次)。 还必须展示一个顶点阶段。
  • pVertexInputState – 包含顶点输入状态描述的变量指示器。
  • pInputAssemblyState – 包含输入汇编描述的变量指示器。
  • pTessellationState – 镶嵌阶段描述指示器;如果镶嵌处于禁用状态,则可设置为 null。
  • pViewportState – 指定视口参数的变量指示器;如果光栅化处于禁用状态,则可设置为 null。
  • pRasterizationState – 指定光栅化行为的变量指示器。
  • pMultisampleState – 定义多点采样的变量指示器;如果镶嵌处于禁用状态,则可设置为 null。
  • pDepthStencilState – 深度/模板参数描述指示器;在两种情况下可设置为 null:光栅化处于禁用状态,或者我们不在渲染通道中使用深度/模板附件。
  • pColorBlendState – 包含颜色混合/写入掩码状态的变量指示器;也在两种情况下可设置为 null:光栅化处于禁用状态,或者我们不在渲染通道中使用任何颜色附件。
  • pDynamicState – 指定可动态设置哪部分图形管道的变量指示器;如果认为整个状态是静止状态(仅通过该创建信息结构定义),可设置为 null。
  • layout – 管道布局对象的句柄,该对象描述在着色器中访问的资源。
  • renderPass – 渲染通道对象句柄;管道可用于任何兼容已提供通道的渲染通道。
  • subpass – 供使用管道的子通道编号(索引)。
  • basePipelineHandle – 支持衍生该管道的管道句柄。
  • basePipelineIndex – 支持衍生该管道的管道索引。

创建新管道时,我们可以承袭其他管道的部分参数。 这意味着两个管道存在共同之处。 比较好的示例是着色器代码。 我们不指定哪些字段是相同的,但从其他管道继承的通用信息可显著加快管道创建速度。 但为什么有两个字段指示“父”管道? 我们不能使用两个 — 一次仅使用一个。 我们使用句柄时,表示“父”管道已创建完成,我们正从提供句柄的管道衍生新管道。 但管道创建函数支持我们一次创建多个管道。 使用第二个参数“父”管道索引可以帮助我们用相同的调用方式创建“父”管道和“子”管道。 我们仅指定图形管道创建信息结构阵列,而且该阵列提供给管道创建函数。 因此“basePipelineIndex”是该阵列中管道创建信息的索引。 我们只需记住,“父”管道在该阵列中必须先创建(索引必须小),而且必须通过“allow derivatives”标记集创建。

在本示例中,我们创建整体处于静止状态的管道(“pDynamicState”参数设为 null)。 但什么是静止状态? 为支持部分灵活性和减少管道对象的创建数量,我们引入了动态状态。 我们可以通过“pDynamicState”参数定义可通过其他 Vulkan 命令动态设置哪部分图形管道,以及在管道创建期间将哪部分一次设置为静态。 动态状态包括视口、线条宽度、混合常量等参数,或部分模板参数。 如果我们指定特定状态为动态,那么忽略管道创建信息结构中与该状态有关的参数。 在渲染期间,必须使用适当的命令设置特定状态,因为该状态的初始值可能没有定义。

因此经过这项繁重的准备工作后,我们可以创建图形管道。 这一过程通过调用 vkCreateGraphicsPipelines()函数完成,提取管道创建信息结构的指示器阵列。 如果进展顺利,该函数将返回 VK_SUCCESS,图形管道句柄并保存在我们提供了地址的变量中。 现在我们可以开始进行绘制。

准备绘制命令

之前的教程介绍过命令缓冲区概念。 这里我简要介绍使用其中的哪些,以及如何使用。

命令缓冲区是 GPU 命令的容器。 如果想在设备上执行某项任务,我们可以通过命令缓冲区来完成。 这表示我们必须准备处理数据(即在屏幕上绘制图形)的命令集,并将这些命令记录在命令缓冲区中。 然后将整个缓冲区提交至设备的队列。 这种提交操作将告诉设备:我希望你现在替我执行一些任务。

为记录命令,我们必须首先分配命令缓冲区。 它们通过命令池进行分配,可视作内存块。 如果命令缓冲区希望变大(因为我们记录了许多复杂命令),它可以增长,并使用命令池(进行分配)的其他内存。 因此我们首先必须创建命令池。

创建命令池

创建命令池非常简单,如下所示:

VkCommandPoolCreateInfo cmd_pool_create_info = {
  VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,     // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  0,                                              // VkCommandPoolCreateFlags       flags
  queue_family_index                              // uint32_t                       queueFamilyIndex
};

if( vkCreateCommandPool( GetDevice(), &cmd_pool_create_info, nullptr, pool ) != VK_SUCCESS ) {
  return false;
}
return true;

19.Tutorial03.cpp,函数 CreateCommandPool()

首先准备类型变量 VkCommandPoolCreateInfo。 它包含以下字段:

  • sType – 标准结构类型,此处设置为 VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 指示命令池及其分配的命令缓冲区的使用场景;即我们告知驱动程序,从该命令池分配的命令缓冲区将存在较短时间;如果没有特定用法,可将其设置为零。
  • queueFamilyIndex – 队列家族(我们为其创建命令池)索引。

请记住,从特定命令池分配的命令缓冲区只能提交至命令池创建期间指定的队列家族的队列。

要创建命令池,我们只需调用 vkCreateCommandPool()函数。

分配命令缓冲区

现在我们准备好命令池后,可以通过它分配命令缓冲区。

VkCommandBufferAllocateInfo command_buffer_allocate_info = {
  VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  pool,                                           // VkCommandPool                  commandPool
  VK_COMMAND_BUFFER_LEVEL_PRIMARY,                // VkCommandBufferLevel           level
  count                                           // uint32_t                       bufferCount
};

if( vkAllocateCommandBuffers( GetDevice(), &command_buffer_allocate_info, command_buffers ) != VK_SUCCESS ) {
  return false;
}
return true;

20.Tutorial03.cpp,函数 AllocateCommandBuffers()

为分配命令缓冲区,我们指定一个结构类型变量。 这次的类型为 VkCommandBufferAllocateInfo,其中包含以下三个成员:

  • sType – 结构类型;此处为 VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • commandPool – 我们希望命令缓冲区从中提取内存的池。
  • level – 命令缓冲区级别;包含两个级别:主缓冲区和次缓冲区;此时我们仅有兴趣使用主命令缓冲区。
  • bufferCount – 希望分配的命令缓冲区数量。

为分配命令缓冲区,我们调用 vkAllocateCommandBuffers()函数并检查是否调用成功。 通过一次函数调用可以一次性分配多个缓冲区。

我准备了一个简单的缓冲区分配函数,以向大家展示如果包装 Vulkan 函数,以方便使用。 以下是两个此类包装程序函数的用法,分别用于创建命令池和分配命令缓冲区。

if( !CreateCommandPool( GetGraphicsQueue().FamilyIndex, &Vulkan.GraphicsCommandPool ) ) {
  printf( "Could not create command pool!\n" );
  return false;
}

uint32_t image_count = static_cast<uint32_t>(GetSwapChain().Images.size());
Vulkan.GraphicsCommandBuffers.resize( image_count, VK_NULL_HANDLE );

if( !AllocateCommandBuffers( Vulkan.GraphicsCommandPool, image_count, &Vulkan.GraphicsCommandBuffers[0] ) ) {
  printf( "Could not allocate command buffers!\n" );
  return false;
}
return true;

21.Tutorial03.cpp,函数 CreateCommandBuffers()

大家看,我们正在为显卡队列家族索引创建命令池。 所有图像状态过渡和绘制操作都将在显卡队列上执行。 演示操作在另一队列上执行(如果演示队列与显卡队列不同),但执行该操作时不需要使用命令缓冲区。

而且我们还为各交换链图像分配命令缓冲区。 这里我们提取图像数量,并将其提供给简单的“wrapper”函数,以便分配命令缓冲区。

记录命令缓冲区

从命令池分配到命令缓冲区后,最后我们可以记录在屏幕上进行绘制的操作。 首先必须准备执行记录操作所需的数据集。 一部分数据与所有命令缓冲区相同,另一部分引用特定的交换链图像。 以下是独立于交换链图像的代码:

VkCommandBufferBeginInfo graphics_commandd_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
};

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
};

VkClearValue clear_value = {
  { 1.0f, 0.8f, 0.4f, 0.0f },                     // VkClearColorValue              color
};

const std::vector<VkImage>& swap_chain_images = GetSwapChain().Images;

22.Tutorial03.cpp,函数 RecordCommandBuffers()

执行命令缓冲区记录类似于 OpenGL 的绘制列表,其中我们通过调用 glNewList() 函数开始记录列表。 接下来准备绘制命令集,然后关闭列表并停止记录 (glEndList())。 因此我们首先要做的是准备类型变量 VkCommandBufferBeginInfo。 它用于开始记录命令缓冲区的时候,告知驱动程序有关命令缓冲区的类型、内容和用法等信息。 该类型变量包含以下成员:

  • sType – 标准结构类型,此处设置为 VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 描述预期用法的参数(即我们是否想仅提交一次,并毁坏/重置该命令缓冲区,或者是否可以在之前的提交完成之前再次提交该缓冲区)。
  • pInheritanceInfo – 仅在记录次命令缓冲区时所使用的参数。

接下来描述为其设置图像内存壁垒的图像区域或部分。 此处我们设置壁垒以指定不同家族的列队将引用特定图像。 这一操作通过类型变量 VkImageSubresourceRange 完成,该变量包含以下成员:

  • aspectMask – 描述图像的”类型“,是否用于颜色、深度或模板数据。
  • baseMipLevel – 供我们执行操作的第一个 mipmap 层的编号。
  • levelCount – 供我们执行操作的 mipmap 层(包括基础层)数量。
  • baseArrayLayer – 参与操作的图像的第一个阵列层的编号。
  • layerCount – 将进行修改的层级(包括基础层)数量。

接下来设置面向图像的清空值。 进行绘制之前需要清空图像。 在之前的教程中,我们自己明确执行这一操作。 这里图像作为渲染通道附件加载操作的一部分进行清空。 我们要设置成“clear”,必须指定图像需清空的颜色。 这一操作通过类型变量 VkClearValue(其中我们提供了 R、G、B、A 四个值)完成。

到目前为止,我们创建的变量均独立于图像本身,因此我们在循环前完成了指定行为。 现在我们开始记录命令缓冲区:

for( size_t i = 0; i < Vulkan.GraphicsCommandBuffers.size(); ++i ) {
  vkBeginCommandBuffer( Vulkan.GraphicsCommandBuffers[i], &graphics_commandd_buffer_begin_info );

  if( GetPresentQueue().Handle != GetGraphicsQueue().Handle ) {
    VkImageMemoryBarrier barrier_from_present_to_draw = {
      VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,     // VkStructureType                sType
      nullptr,                                    // const void                    *pNext
      VK_ACCESS_MEMORY_READ_BIT,                  // VkAccessFlags                  srcAccessMask
      VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,       // VkAccessFlags                  dstAccessMask
      VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,            // VkImageLayout                  oldLayout
      VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,            // VkImageLayout                  newLayout
      GetPresentQueue().FamilyIndex,              // uint32_t                       srcQueueFamilyIndex
      GetGraphicsQueue().FamilyIndex,             // uint32_t                       dstQueueFamilyIndex
      swap_chain_images[i],                       // VkImage                        image
      image_subresource_range                     // VkImageSubresourceRange        subresourceRange
    };
    vkCmdPipelineBarrier( Vulkan.GraphicsCommandBuffers[i], VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier_from_present_to_draw );
  }

  VkRenderPassBeginInfo render_pass_begin_info = {
    VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,     // VkStructureType                sType
    nullptr,                                      // const void                    *pNext
    Vulkan.RenderPass,                            // VkRenderPass                   renderPass
    Vulkan.FramebufferObjects[i].Handle,          // VkFramebuffer                  framebuffer
    {                                             // VkRect2D                       renderArea
      {                                           // VkOffset2D                     offset
        0,                                          // int32_t                        x
        0                                           // int32_t                        y
      },
      {                                           // VkExtent2D                     extent
        300,                                        // int32_t                        width
        300,                                        // int32_t                        height
      }
    },
    1,                                            // uint32_t                       clearValueCount
    &clear_value                                  // const VkClearValue            *pClearValues
  };

  vkCmdBeginRenderPass( Vulkan.GraphicsCommandBuffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE );

  vkCmdBindPipeline( Vulkan.GraphicsCommandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, Vulkan.GraphicsPipeline );

  vkCmdDraw( Vulkan.GraphicsCommandBuffers[i], 3, 1, 0, 0 );

  vkCmdEndRenderPass( Vulkan.GraphicsCommandBuffers[i] );

  if( GetGraphicsQueue().Handle != GetPresentQueue().Handle ) {
    VkImageMemoryBarrier barrier_from_draw_to_present = {
      VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,       // VkStructureType              sType
      nullptr,                                      // const void                  *pNext
      VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,         // VkAccessFlags                srcAccessMask
      VK_ACCESS_MEMORY_READ_BIT,                    // VkAccessFlags                dstAccessMask
      VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,              // VkImageLayout                oldLayout
      VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,              // VkImageLayout                newLayout
      GetGraphicsQueue().FamilyIndex,               // uint32_t                     srcQueueFamilyIndex
      GetPresentQueue( ).FamilyIndex,               // uint32_t                     dstQueueFamilyIndex
      swap_chain_images[i],                         // VkImage                      image
      image_subresource_range                       // VkImageSubresourceRange      subresourceRange
    };
    vkCmdPipelineBarrier( Vulkan.GraphicsCommandBuffers[i], VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier_from_draw_to_present );
  }

  if( vkEndCommandBuffer( Vulkan.GraphicsCommandBuffers[i] ) != VK_SUCCESS ) {
    printf( "Could not record command buffer!\n" );
    return false;
  }
}
return true;

23.Tutorial03.cpp,函数 RecordCommandBuffers()

通过调用 vkBeginCommandBuffer()函数开始记录命令缓冲区。 开始时设置一个壁垒,告知驱动程序之前某个家族的队列引用了特定图像,但现在不同家族的队列将引用该图像(这么做的原因是在交换链创建期间,我们指定了专用共享模式)。 该壁垒仅在显卡队列不同于演示队列时使用。 该步骤可通过调用 vkCmdPipelineBarrier()函数进行。 我们必须指定何时将壁垒放在管道中 (VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT),以及如何设置该壁垒。 通过 VkImageMemoryBarrier 结构准备壁垒参数:

  • sType – 结构类型,此处设置为 VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER。
  • pNext – 为扩展功能预留的指示器。
  • srcAccessMask – 壁垒前执行的与特定图像有关的内存操作类型。
  • dstAccessMask – 连接至特定图像,在壁垒之后执行的内存操作类型。
  • oldLayout – 当前图像内存布局。
  • newLayout – 壁垒之后应该拥有的内存布局图像。
  • srcQueueFamilyIndex – 在壁垒之前引用图像的队列家族索引。
  • dstQueueFamilyIndex – 在壁垒之后将通过其引用图像的队列家族索引。
  • image – 图像本身的句柄。
  • subresourceRange – 我们希望进行过渡的图像部分。

在本示例中,我们不更改图像的布局,原因有两点: (1) 壁垒可以不用设置(如果图形和演示队列相同),(2) 布局过渡将作为渲染通道操作自动进行(在第一个 — 唯一 — 子通道开头)。

接下来启动渲染通道。 我们调用 vkCmdBeginRenderPass()函数,而且必须为该函数提供 VkRenderPassBeginInfo 类型变量指示器。 它包含以下成员:

  • sType – 标准结构类型。 在本示例中,必须将其设置为 VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO 的值。
  • pNext – 留作将来使用的指示器。
  • renderPass – 我们希望启动的渲染通道的句柄。
  • framebuffer – 帧缓冲器的句柄,指定在该渲染通道中用作附件的图像。
  • renderArea – 受渲染通道内执行的操作所影响的图形区域。 它指定左上角(通过抵消成员 (offset member) 的 x 和 y 参数),以及渲染区域的宽度和高度(通过扩展成员 (extent member))。
  • clearValueCount – pClearValues 阵列中的要素数量。
  • pClearValues – 有关各附件的清空值阵列。

指定渲染通道的渲染区域时,必须确保渲染操作不会修改该区域外的像素。 这只是给驱动程序的提示,但可以优化其行为。 如果不使用相应的 scissor 测试将操作限制在提供的区域内,该区域外的像素可能变成未定义像素(不能依靠内容)。 而且,我们不能指定大于帧缓冲区尺寸的渲染区域(超出帧缓冲器)。

就 pClearValues 而言,它必须包含各渲染通道附件的要素。 loadOp 设置为清空时,每个要素均指定特定附件必须清空的颜色。 对于 loadOp 不是清空的附件来说,将忽略提供给它们的值。 但不能为阵列提供数量较少的要素。

我们已经开始创建命令缓冲区、设置壁垒(如有必要),并启动渲染通道。 启动渲染通道时,我们还将启动其第一个子通道。 我们可以通过调用 vkCmdNextSubpass()函数切换至下一个子通道。 执行这些操作期间,可能会出现布局过渡和清空操作。 清空操作在首先使用(引用)图像的子通道内进行。 如果子通道布局与之前的通道或(如果是第一个子通道或第一个引用图像时)初始布局(渲染通道之前的布局)不同,将出现布局过渡。 因此在本示例中,启动渲染通道时,交换链图像的布局将从“presentation source”布局自动变成 “color attachment optimal”布局。

现在我们绑定图形管道。 该步骤可通过调用 vkCmdBindPipeline()函数完成。 这样可“激活”所有着色器程序(类似于 glUseProgram()函数,并设置必要的测试、混合操作等。

绑定管道后,我们可以通过调用 vkCmdDraw()函数,进行最终的绘制操作。 在本函数中,我们指定希望绘制的顶点数量(3 个)、应绘制的实例数量(仅 1 个),以及第一个顶点和第一个实例的索引编号(均为 0)。

接下来调用 vkCmdEndRenderPass()函数,结束特定的渲染通道。 这里,如果为渲染通道指定的最终布局与引用特定图像的最后一个子通道所使用的布局不同,所有最终布局都将进行过渡。

之后将设置壁垒,其中我们告知驱动程序,显卡队列已使用完特定图像,而且从现在开始演示队列将使用该图像。 仅在显卡队列和演示队列不同的情况下,再次执行该步骤。 在壁垒之后,我们停止为特定图像记录命令缓冲区。 所有这些操作都会为每个交换链图像重复一次。

绘制

绘制函数与教程 2 中的 Draw()函数相同。 我们获取图形索引、提交相应的命令缓冲区,并演示图像。 使用旗语的方式与之前的相同:一个旗语用于获取图像,并告知显卡队列等待可用的图像。 第二个命令缓冲区用于指示显卡队列上的绘制操作是否已经完成。 演示图像之前,演示队列需等待该旗语。 以下是 Draw()函数的源代码:

VkSemaphore image_available_semaphore = GetImageAvailableSemaphore();
VkSemaphore rendering_finished_semaphore = GetRenderingFinishedSemaphore();
VkSwapchainKHR swap_chain = GetSwapChain().Handle;
uint32_t image_index;

VkResult result = vkAcquireNextImageKHR( GetDevice(), swap_chain, UINT64_MAX, image_available_semaphore, 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:
    printf( "Problem occurred during swap chain image acquisition!\n" );
    return false;
}

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
  &image_available_semaphore,                   // const VkSemaphore           *pWaitSemaphores&wait_dst_stage_mask,                         // const VkPipelineStageFlags  *pWaitDstStageMask;
  1,                                            // uint32_t                     commandBufferCount&Vulkan.GraphicsCommandBuffers[image_index],  // const VkCommandBuffer       *pCommandBuffers
  1,                                            // uint32_t                     signalSemaphoreCount&rendering_finished_semaphore                 // const VkSemaphore           *pSignalSemaphores
};

if( vkQueueSubmit( GetGraphicsQueue().Handle, 1, &submit_info, VK_NULL_HANDLE ) != VK_SUCCESS ) {
  return false;
}

VkPresentInfoKHR present_info = {
  VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,           // VkStructureType              sType
  nullptr,                                      // const void                  *pNext
  1,                                            // uint32_t                     waitSemaphoreCount
  &rendering_finished_semaphore,                // const VkSemaphore           *pWaitSemaphores
  1,                                            // uint32_t                     swapchainCount&swap_chain,                                  // const VkSwapchainKHR        *pSwapchains&image_index,                                 // const uint32_t              *pImageIndices
  nullptr                                       // VkResult                    *pResults
};
result = vkQueuePresentKHR( GetPresentQueue().Handle, &present_info );

switch( result ) {
  case VK_SUCCESS:
    break;
  case VK_ERROR_OUT_OF_DATE_KHR:
  case VK_SUBOPTIMAL_KHR:
    return OnWindowSizeChanged();
  default:
    printf( "Problem occurred during image presentation!\n" );
    return false;
}

return true;

24.Tutorial03.cpp,函数 Draw()

教程 3 执行

在本教程中我们执行了“真正的”绘制操作。 简单的三角形似乎没有太大的说服力,但对于 Vulkan 创建的第一个图像来说,它是一个良好的开端。 该三角形如下所示:

如果您想知道图像中为什么会出现黑色部分,原因如下: 为简化整个代码,我们创建了大小固定(宽度和高度均为 300 像素)的帧缓冲区。 但窗口尺寸(和交换链图像的尺寸)可能大于 300 x 300 像素。 超出帧缓冲区尺寸的图像部分没有被应用清空和修改。 这可能包含部分“人为因素”,因为供驱动程序分配交换链图像的内存之前可能用于其他目的,可能包含一些数据。 正确的行为是创建大小与交换链图像相同的帧缓冲区,并在窗口尺寸大小变化后重新创建。 但如果在橙色/金色背景上渲染蓝色三角形,表示该代码可正常运行。

清空

本教程结束之前,我们最后需要学习的是如何使用在本教程中创建的资源。 释放资源所需的代码已在之前章节中创建,这里不再叙述。 只需查看 VulkanCommon.cpp 文件。 以下代码可毁坏特定于本章节的资源:

if( GetDevice() != VK_NULL_HANDLE ) {
  vkDeviceWaitIdle( GetDevice() );

  if( (Vulkan.GraphicsCommandBuffers.size() > 0) && (Vulkan.GraphicsCommandBuffers[0] != VK_NULL_HANDLE) ) {
    vkFreeCommandBuffers( GetDevice(), Vulkan.GraphicsCommandPool, static_cast<uint32_t>(Vulkan.GraphicsCommandBuffers.size()), &Vulkan.GraphicsCommandBuffers[0] );
    Vulkan.GraphicsCommandBuffers.clear();
  }

  if( Vulkan.GraphicsCommandPool != VK_NULL_HANDLE ) {
    vkDestroyCommandPool( GetDevice(), Vulkan.GraphicsCommandPool, nullptr );
    Vulkan.GraphicsCommandPool = VK_NULL_HANDLE;
  }

  if( Vulkan.GraphicsPipeline != VK_NULL_HANDLE ) {
    vkDestroyPipeline( GetDevice(), Vulkan.GraphicsPipeline, nullptr );
    Vulkan.GraphicsPipeline = VK_NULL_HANDLE;
  }

  if( Vulkan.RenderPass != VK_NULL_HANDLE ) {
    vkDestroyRenderPass( GetDevice(), Vulkan.RenderPass, nullptr );
    Vulkan.RenderPass = VK_NULL_HANDLE;
  }

  for( size_t i = 0; i < Vulkan.FramebufferObjects.size(); ++i ) {
    if( Vulkan.FramebufferObjects[i].Handle != VK_NULL_HANDLE ) {
      vkDestroyFramebuffer( GetDevice(), Vulkan.FramebufferObjects[i].Handle, nullptr );
      Vulkan.FramebufferObjects[i].Handle = VK_NULL_HANDLE;
    }

    if( Vulkan.FramebufferObjects[i].ImageView != VK_NULL_HANDLE ) {
      vkDestroyImageView( GetDevice(), Vulkan.FramebufferObjects[i].ImageView, nullptr );
      Vulkan.FramebufferObjects[i].ImageView = VK_NULL_HANDLE;
    }
  }
  Vulkan.FramebufferObjects.clear();
}

25.Tutorial03.cpp,函数 ChildClear()

像往常一样,首先检查是否有设备。 如果没有设备,就没有资源。 接下来等待设备空闲下来,并删除所有已创建的资源。 我们首先通过调用 vkFreeCommandBuffers()函数,开始删除命令缓冲区。 接下来通过 vkDestroyCommandPool()函数毁坏命令池,然后破坏图形管道。 该步骤可通过 vkDestroyPipeline()函数完成。 然后调用 vkDestroyRenderPass()函数,以释放渲染通道的句柄。 最后删除与交换链图像相关的所有帧缓冲区和图像视图。

破坏对象之前,首先检查是否创建了特定资源。 如果没有,我们则跳过破环资源这一流程。

结论

在本教程中,我们创建了包含一个子通道的渲染通道。 接下来创建交换链图像图像视图和帧缓冲区。 其中一个最重要的部分是创建图形管道,因为这一过程要求我们准备大量数据。 我们需要创建着色器模块,并描述应该绑定图形管道时处于活跃状态的着色器阶段。 需要准备与输入顶点、布局,以及将其汇编成拓扑等相关的信息。 还需要准备视口、光栅化、多点采样和颜色混合信息。 然后创建简单的管道布局,之后才能创建管道。 接下来我们创建了命令池,并为各交换链图像分配了命令缓冲区。 记录在命令缓冲区中的操作涉及设置图像内存壁垒、启动渲染通道、绑定图形通道,以及绘制。 接下来结束渲染通道,并设置另一图像内存壁垒。 执行绘制的方式与之前教程 (2) 中所述的相同。

在接下来的教程中,我们将学习顶点属性、图像和缓冲区等相关知识。


请前往: 没有任何秘密的 API: Vulkan* 简介第 4 部分: 顶点属性


声明

本文件不构成对任何知识产权的授权,包括明示的、暗示的,也无论是基于禁止反言的原则或其他。

英特尔明确拒绝所有明确或隐含的担保,包括但不限于对于适销性、特定用途适用性和不侵犯任何权利的隐含担保,以及任何对于履约习惯、交易习惯或贸易惯例的担保。

本文包含尚处于开发阶段的产品、服务和/或流程的信息。 此处提供的信息可随时改变而毋需通知。 联系您的英特尔代表,了解最新的预测、时间表、规格和路线图。

本文件所描述的产品和服务可能包含使其与宣称的规格不符的设计缺陷或失误。 英特尔提供最新的勘误表备索。

如欲获取本文提及的带订购编号的文档副本,可致电 1-800-548-4725,或访问 www.intel.com/design/literature.htm

该示例源代码根据英特尔示例源代码许可协议发布。

英特尔和 Intel 标识是英特尔在美国和/或其他国家的商标。

*其他的名称和品牌可能是其他所有者的资产。

英特尔公司 © 2016 年版权所有。

没有任何秘密的 API: Vulkan* 简介第 4 部分

$
0
0

下载  [PDF 890KB]

Github 示例代码链接


请前往: 没有任何秘密的 API: Vulkan* 简介第 3 部分: 第一个三角形


目录

教程 4: 顶点属性 – 缓冲区、图像和栅栏

教程 4: 顶点属性 – 缓冲区、图像和栅栏

我们在之前的教程中学些了一些基本知识。 教程篇幅比较长而且(我希望)介绍得比较详细。 这是因为 Vulkan* API 的学习曲线非常陡峭。 而且大家看,即使是准备最简单的应用,我们也需要了解大量的知识。

但现在我们已经打好了基础。 因此本教程篇幅会短一些,并重点介绍与 Vulkan API 相关的一些话题。 本节我们将通过从顶点缓冲区提供顶点属性,介绍绘制任意几何图形的推荐方法。 由于本课程的代码与“03 – 第一个三角形”教程的代码类似,因此我仅介绍与之不同的部分。

并介绍另外一种整理渲染代码的方法。 之前我们在主渲染循环前记录了命令缓冲区。 但在实际情况中,每帧动画都各不相同,因此我们无法预先记录所有渲染命令。 我们应该尽可能地晚一些记录和提交命令缓冲区,以最大限度地降低输入延迟,并获取最新的输入数据。 我们将正好在提交至队列之前记录命令缓冲区。 不过,一个命令缓冲区远远不够。 必须等到显卡处理完提交的命令缓冲区后,才记录相同的命令缓冲区。 此时将通过栅栏发出信号。 但每一帧都等待栅栏实在非常浪费时间,因此我们需要更多的命令缓冲区,以便交换使用。 命令缓冲区越多,所需的栅栏越多,情况也将越复杂。 本教程将展示如何组织代码,尽可能地实现代码的轻松维护性、灵活性和快速性。

指定渲染通道相关性

我们从创建渲染通道开始,方法与之前教程中所介绍的相同。 但这次我们还需要提供其他信息。 渲染通道描述渲染资源(图像/附件)的内部组织形式,使用方法,以及在渲染流程中的变化。 通过创建图像内存壁垒,可明确更改图像的布局。 如果指定了合适的渲染通道描述(初始布局、子通道布局和最终图像布局),这种操作也可以隐式地进行。 我们首选隐式过渡,因为驱动程序可以更好地执行此类过渡。

在本教程的这一部分,与之前相同,我们将初始和最终图像布局指定为“transfer src”,而将渲染通道指定为“color attachment optimal”子通道布局。 但之前的教程缺乏其他重要信息,尤其是如何使用图像(即执行哪些与图像相关的操作),以及何时使用图像(渲染管道的哪些部分使用图像)。 此类信息可在图像内存壁垒和渲染通道描述中指定。 创建图像内存壁垒时,我们指定与特定图像相关的操作类型(壁垒之前和之后的内存访问类型),而且我们还指定何时放置壁垒(壁垒之前和之后使用图像的管道阶段)。

创建渲染通道并为其提供描述时,通过子通道相关性指定相同的信息。 其他数据对驱动程序也至关重要,可更好地准备隐式壁垒。 以下源代码可创建渲染通道并准备子通道相关性。

std::vector<VkSubpassDependency> dependencies = {
  {
    VK_SUBPASS_EXTERNAL,                            // uint32_t                       srcSubpass
    0,                                              // uint32_t                       dstSubpass
    VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,           // VkPipelineStageFlags           srcStageMask
    VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,  // VkPipelineStageFlags           dstStageMask
    VK_ACCESS_MEMORY_READ_BIT,                      // VkAccessFlags                  srcAccessMask
    VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,           // VkAccessFlags                  dstAccessMask
    VK_DEPENDENCY_BY_REGION_BIT                     // VkDependencyFlags              dependencyFlags
  },
  {
    0,                                              // uint32_t                       srcSubpass
    VK_SUBPASS_EXTERNAL,                            // uint32_t                       dstSubpass
    VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,  // VkPipelineStageFlags           srcStageMask
    VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,           // VkPipelineStageFlags           dstStageMask
    VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,           // VkAccessFlags                  srcAccessMask
    VK_ACCESS_MEMORY_READ_BIT,                      // VkAccessFlags                  dstAccessMask
    VK_DEPENDENCY_BY_REGION_BIT                     // VkDependencyFlags              dependencyFlags
  }
};

VkRenderPassCreateInfo render_pass_create_info = {
  VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,        // VkStructureType                sType
  nullptr,                                          // const void                    *pNext
  0,                                                // VkRenderPassCreateFlags        flags
  1,                                                // uint32_t                       attachmentCount
  attachment_descriptions,                          // const VkAttachmentDescription *pAttachments
  1,                                                // uint32_t                       subpassCount
  subpass_descriptions,                             // const VkSubpassDescription    *pSubpasses
  static_cast<uint32_t>(dependencies.size()),       // uint32_t                       dependencyCount&dependencies[0]                                  // const VkSubpassDependency     *pDependencies
};

if( vkCreateRenderPass( GetDevice(), &render_pass_create_info, nullptr, &Vulkan.RenderPass ) != VK_SUCCESS ) {
  std::cout << "Could not create render pass!"<< std::endl;
  return false;
}

1.Tutorial04.cpp,函数 CreateRenderPass()

子通道相关性描述不同子通道之间的相关性。 当附件以特定方式用于特定子通道(例如渲染)时,会以另一种方式用于另一个子通道(取样),因此我们可创建内存壁垒或提供子通道相关性,以描述附件在这两个子通道中的预期用法。 当然,我们推荐使用后一种选项,因为驱动程序能够(通常)以最佳的方式准备壁垒。 而且代码本身也会得到完善 — 了解代码所需的一切都收集在一个位置,一个对象中。

在我们的简单示例中,我们仅有一个子通道,但我们指定了两种相关性。 因为我们能够(而且应该)指定渲染通道(通过提供特定子通道的编号)与其外侧操作(通过提供 VK_SUBPASS_EXTERNAL 值)之间的相关性。 这里我们为渲染通道与其唯一的子通道之前执行的操作之间的颜色附件提供相关性。 第二种相关性针对子通道内和渲染通道之后执行的操作定义。

我们将讨论哪些操作。 我们仅使用一个附件,即从演示引擎(交换链)获取的图像。 演示引擎将图像用作可演示数据源。 它仅显示一个图像。 因此涉及图像的唯一操作是在“present src”布局的图像上进行“内存读取”。 该操作不会在任何正常的管道阶段中进行,而在“管道底部”阶段再现。

在渲染通道内部唯一的子通道(索引为 0)中,我们将渲染用作颜色附件的图像。 因此在该图像上进行的操作为“颜色附件写入”,在“颜色附件输入”管道阶段中(碎片着色器之后)执行。 演示并将图像返回至演示引擎后,会再次将该图像用作数据源。 因此在本示例中,渲染通道之后的操作与之前的一样: “内存读取”。

我们通过 VkSubpassDependency 成员阵列指定该数据。 创建渲染通道和 VkRenderPassCreateInfo 结构时,我们(通过 dependencyCount 成员)指定相关性阵列中的要素数量,并(通过 pDependencies)提供第一个要素的地址。 在之前的教程中,我们将这两个字段设置为 0 和 nullptr。 VkSubpassDependency 结构包含以下字段:

  • srcSubpass – 第一个(前面)子通道的索引或 VK_SUBPASS_EXTERNAL(如果希望指示子通道与渲染通道外的操作之间的相关性)。
  • dstSubpass – 第二个(后面)子通道的索引(或 VK_SUBPASS_EXTERNAL)。
  • srcStageMask – 之前(src 子通道中)使用特定附件的管道阶段。
  • dstStageMask – 之后(dst 子通道中)将使用特定附件的管道阶段。
  • srcAccessMask – src 子通道中或渲染通道前所发生的内存操作类型。
  • dstAccessMask – dst 子通道中或渲染通道后所发生的内存操作类型。
  • dependencyFlags – 描述相关性类型(区域)的标记。

图形管道创建

现在我们将创建图形管道对象。 (我们应创建面向交换链图像的帧缓冲器,不过这一步骤在命令缓冲区记录期间进行)。 我们不想渲染在着色器中进行过硬编码的几何图形, 而是想绘制任意数量的顶点,并提供其他属性(不仅仅是顶点位置)。 我们首先应该做什么?

编写着色器

首先查看用 GLSL 代码编写的顶点着色器:

#version 450

layout(location = 0) in vec4 i_Position;
layout(location = 1) in vec4 i_Color;

out gl_PerVertex
{
  vec4 gl_Position;
};

layout(location = 0) out vec4 v_Color;

void main() {
    gl_Position = i_Position;
    v_Color = i_Color;
}

2.shader.vert

尽管比教程 03 的复杂,但该着色器非常简单。

我们指定两个输入属性(named i_Position 和 i_Color)。 在 Vulkan中,所有属性必须有一个位置布局限定符。 在 Vulkan API 中指定顶点属性描述时,属性名称不重要,重要的是它们的索引/位置。 在 OpenGL* 中,我们可请求特定名称的属性位置。 在 Vulkan 中不能这样做。 位置布局限定符是唯一的方法。

接下来我们重新声明着色器中的 gl_PerVertex 模块。 Vulkan 使用着色器 I/O 模块,所以我们应该重新声明 gl_PerVertex 以明确指定该模块使用哪些成员。 如果没有指定,将使用默认定义。 但我们必须记住该默认定义 contains gl_ClipDistance[],它要求我们启用特性 shaderClipDistance(而且在 Vulkan 中,不能使用创建设备期间没有启用的特性,或可能无法正常运行的应用)。 这里我们仅使用 gl_Position 成员,因此不要求启用该特性。

然后我们指定一个与变量 v_Color 不同的附加输入,以保存顶点颜色。 在主函数中,我们将应用提供的值拷贝至相应的输入变量:position to gl_Position 和 color to v_Color。

现在查看碎片着色器,以了解如何使用属性。

#version 450

layout(location = 0) in vec4 v_Color;

layout(location = 0) out vec4 o_Color;

void main() {
  o_Color = v_Color;
}

3.shader.frag

在碎片着色器中,将与变量 v_Color 不同的输入仅拷贝至输出变量 o_Color。 两个变量都有位置布局说明符。 在顶点着色器中变量 v_Color 的位置与输出变量相同,因此它将包含定点之间插值替换的颜色值。

着色器能够以与之前相同的方式转换成 SPIR-V 汇编。 这一步骤可通过以下命令完成:

glslangValidator.exe -V -H shader.vert > vert.spv.txt

glslangValidator.exe -V -H shader.frag > frag.spv.txt

因此现在,了解哪些是我们希望在着色器中使用的属性后,我们将可以创建相应的图形管道。

顶点属性指定

在本教程中,我们将对顶点输入状态创建进行最重要的改进,为此我们指定类型变量 VkPipelineVertexInputStateCreateInfo。 在该变量中我们提供结构指示器,定义顶点输入数据类型,以及属性的数量和布局。

我们希望使用两个属性:顶点位置和顶点颜色,前者由四个浮点组件组成,后者由四个浮点值组成。 我们以交错属性布局的形式将所有顶点数据放在缓冲器中。 这表示我们将依次放置第一个顶点的位置,相同顶点的颜色,第二个顶点的位置,第二个顶点的颜色,第三个顶点的位置和颜色,依此类推。 我们借助以下代码完成这种指定:

std::vector<VkVertexInputBindingDescription> vertex_binding_descriptions = {
  {
    0,                                                          // uint32_t                                       binding
    sizeof(VertexData),                                         // uint32_t                                       stride
    VK_VERTEX_INPUT_RATE_VERTEX                                 // VkVertexInputRate                              inputRate
  }
};

std::vector<VkVertexInputAttributeDescription> vertex_attribute_descriptions = {
  {
    0,                                                          // uint32_t                                       location
    vertex_binding_descriptions[0].binding,                     // uint32_t                                       binding
    VK_FORMAT_R32G32B32A32_SFLOAT,                              // VkFormat                                       format
    offsetof(struct VertexData, x)                              // uint32_t                                       offset
  },
  {
    1,                                                          // uint32_t                                       location
    vertex_binding_descriptions[0].binding,                     // uint32_t                                       binding
    VK_FORMAT_R32G32B32A32_SFLOAT,                              // VkFormat                                       format
    offsetof( struct VertexData, r )                            // uint32_t                                       offset
  }
};

VkPipelineVertexInputStateCreateInfo vertex_input_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,    // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineVertexInputStateCreateFlags          flags;
  static_cast<uint32_t>(vertex_binding_descriptions.size()),    // uint32_t                                       vertexBindingDescriptionCount&vertex_binding_descriptions[0],                              // const VkVertexInputBindingDescription         *pVertexBindingDescriptions
  static_cast<uint32_t>(vertex_attribute_descriptions.size()),  // uint32_t                                       vertexAttributeDescriptionCount&vertex_attribute_descriptions[0]                             // const VkVertexInputAttributeDescription       *pVertexAttributeDescriptions
};

4.Tutorial04.cpp,函数 CreatePipeline()

首先通过 VkVertexInputBindingDescription 指定顶点数据绑定(通用内存信息)。 它包含以下字段:

  • binding – 与顶点数据相关的绑定索引。
  • stride – 两个连续要素(两个相邻顶点的相同属性)之间的间隔(字节)。
  • inputRate – 定义如何使用数据,是按照顶点还是按照实例使用。

步长和 inputRate 字段不言而喻。 绑定成员可能还要求提供其他信息。 创建顶点缓冲区时,在执行渲染操作之前我们将其绑定至所选的插槽。 插槽编号(索引)就是这种绑定,此处我们描述该插槽中的数据如何与内存对齐,以及如何使用数据(按顶点或实例)。 不同的顶点缓冲区可绑定至不同的绑定。 而且每个绑定都可放在内存中的不同位置。

接下来定义所有顶点属性。 我们必须指定各属性的位置(索引)(与着色器源代码相同,以位置布局限定符的形式)、数据源(从哪个绑定读取数据)、格式(数据类型和组件数量),以及查找特定属性数据的偏移(从特定顶点数据的开头,而非所有顶点数据的开头)。 这种情况与 OpenGL 几乎相同,我们创建顶点缓冲区对象(VBO,可视作等同于“绑定”),并使用 glVertexAttribPointer() 函数(通过该函数指定属性索引(位置)、大小和类型(组件数量和格式)、步长和偏移)定义属性。 可通过 VkVertexInputAttributeDescription 结构提供这类信息。 它包含以下字段:

  • location – 属性索引,与着色器源代码中由位置布局说明符定义的相同。
  • binding – 供数据读取的插槽编号(与 OpenGL 中的 VBO 等数据源),与 VkVertexInputBindingDescription 结构和 vkCmdBindVertexBuffers()函数(稍后介绍)中的绑定相同。
  • format – 数据类型和每个属性的组件数量。
  • offset – 特定属性数据的开头。

准备好后,我们可以通过填充类型变量 VkPipelineVertexInputStateCreateInfo 准备顶点输入状态描述,该变量包含以下字段:

  • sType – 结构类型,此处应等于 VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。 目前将该数值设为 null。
  • flags – 留作将来使用的参数。
  • vertexBindingDescriptionCount – pVertexBindingDescriptions 阵列中的要素数量。
  • pVertexBindingDescriptions – 描述为特定管道(支持读取所有属性的缓冲区)定义的所有绑定的阵列。
  • vertexAttributeDescriptionCount – pVertexAttributeDescriptions 阵列中的要素数量。
  • pVertexAttributeDescriptions – 指定所有顶点属性的要素阵列。

它包含创建管道期间的顶点属性指定。 如要使用它们,我们必须创建顶点缓冲区,并在发布渲染命令之前将其绑定至命令缓冲区。

输入汇编状态指定

之前我们使用三角形条拓扑绘制了一个简单的三角形。 现在我们绘制一个四边形,通过定义四个顶点(而非两个三角形和六个顶点)绘制起来非常方便。 为此我们必须使用三角形条带拓扑。 我们通过 VkPipelineInputAssemblyStateCreateInfo 结构定义该拓扑,其中包含以下成员:

  • sType – 结构类型,此处等于 VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 留作将来使用的参数。
  • topology – 用于绘制顶点的拓扑(比如三角扇、带、条)。
  • primitiveRestartEnable – 该参数定义是否希望使用特定顶点索引值重新开始汇编基元。

以下简单代码可用于定义三角条带拓扑:

VkPipelineInputAssemblyStateCreateInfo input_assembly_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,  // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineInputAssemblyStateCreateFlags        flags
  VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP,                         // VkPrimitiveTopology                            topology
  VK_FALSE                                                      // VkBool32                                       primitiveRestartEnable
};

5.Tutorial04.cpp,函数 CreatePipeline()

视口状态指定

本教程引进了另外一个变化。 之前为了简单起见,我们对视口和 scissor 测试参数进行了硬编码,可惜导致图像总保持相同的大小,无论应用窗口多大。 这次我们不通过 VkPipelineViewportStateCreateInfo 结构指定这些数值, 而是使用动态。 以下代码负责定义静态视口状态参数:

VkPipelineViewportStateCreateInfo viewport_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,        // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineViewportStateCreateFlags             flags
  1,                                                            // uint32_t                                       viewportCount
  nullptr,                                                      // const VkViewport                              *pViewports
  1,                                                            // uint32_t                                       scissorCount
  nullptr                                                       // const VkRect2D                                *pScissors
};

6.Tutorial04.cpp,函数 CreatePipeline()

定义静态视口参数的结构包含以下成员:

  • sType – 结构类型,此处为 VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO。
  • pNext – 为特定于扩展的参数预留的指示器。
  • flags – 留作将来使用的参数。
  • viewportCount – 视口数量。
  • pViewports – 定义静态视口参数的结构指示器。
  • scissorCount – scissor 矩形的数量(数值必须与 viewportCount 参数相同)。
  • pScissors – 定义视口的静态 scissor 测试参数的 2D 矩形阵列指示器。

如果希望通过动态定义视口和 scissor 参数,则无需填充 pViewports 和 pScissors 成员。 因此在上述示例中将其设置为 null。 但我们必须定义视口和 scissor 测试矩形的数量。 通常通过 VkPipelineViewportStateCreateInfo 结构指定这些值,无论是否希望使用动态或静态视口和 scissor 状态。

动态指定

创建管道时,我们可以指定哪部分始终保持静态 — 在管道创建期间通过结构定义,哪部分保持动态 — 在命令缓冲区记录期间通过调用相应的函数来指定。 这可帮助我们减少仅在细节方面有所差异(比如线条宽度、混合常量、模板参数或之前提到的视口大小)的管道对象的数量。 以下代码用于定义管道应保持动态的部分:

std::vector<VkDynamicState> dynamic_states = {
  VK_DYNAMIC_STATE_VIEWPORT,
  VK_DYNAMIC_STATE_SCISSOR,
};

VkPipelineDynamicStateCreateInfo dynamic_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,         // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineDynamicStateCreateFlags              flags
  static_cast<uint32_t>(dynamic_states.size()),                 // uint32_t                                       dynamicStateCount&dynamic_states[0]                                            // const VkDynamicState                          *pDynamicStates
};

7.Tutorial04.cpp,函数 CreatePipeline()

该步骤通过类型结构 VkPipelineDynamicStateCreateInfo 完成,其中包含以下字段:

  • sType – 定义特定结构类型的参数,此处等于 VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO。
  • pNext – 为扩展功能预留的参数。
  • flags – 留作将来使用的参数。
  • dynamicStateCount – pDynamicStates 阵列中的要素数量。
  • pDynamicStates – 包含 enum 的阵列,指定将哪部分管道标记为动态。 该阵列的要素类型为 VkDynamicState。

管道对象创建

定义完图形管道的所有必要参数后,将可以开始创建管道对象。 以下代码可帮助完成这一操作:

VkGraphicsPipelineCreateInfo pipeline_create_info = {
  VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,              // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineCreateFlags                          flags
  static_cast<uint32_t>(shader_stage_create_infos.size()),      // uint32_t                                       stageCount&shader_stage_create_infos[0],                                // const VkPipelineShaderStageCreateInfo         *pStages&vertex_input_state_create_info,                              // const VkPipelineVertexInputStateCreateInfo    *pVertexInputState;&input_assembly_state_create_info,                            // const VkPipelineInputAssemblyStateCreateInfo  *pInputAssemblyState
  nullptr,                                                      // const VkPipelineTessellationStateCreateInfo   *pTessellationState&viewport_state_create_info,                                  // const VkPipelineViewportStateCreateInfo       *pViewportState&rasterization_state_create_info,                             // const VkPipelineRasterizationStateCreateInfo  *pRasterizationState&multisample_state_create_info,                               // const VkPipelineMultisampleStateCreateInfo    *pMultisampleState
  nullptr,                                                      // const VkPipelineDepthStencilStateCreateInfo   *pDepthStencilState&color_blend_state_create_info,                               // const VkPipelineColorBlendStateCreateInfo     *pColorBlendState&dynamic_state_create_info,                                   // const VkPipelineDynamicStateCreateInfo        *pDynamicState
  pipeline_layout.Get(),                                        // VkPipelineLayout                               layout
  Vulkan.RenderPass,                                            // VkRenderPass                                   renderPass
  0,                                                            // uint32_t                                       subpass
  VK_NULL_HANDLE,                                               // VkPipeline                                     basePipelineHandle
  -1                                                            // int32_t                                        basePipelineIndex
};

if( vkCreateGraphicsPipelines( GetDevice(), VK_NULL_HANDLE, 1, &pipeline_create_info, nullptr, &Vulkan.GraphicsPipeline ) != VK_SUCCESS ) {
  std::cout << "Could not create graphics pipeline!"<< std::endl;
  return false;
}
return true;

8.Tutorial04.cpp,函数 CreatePipeline()

最重要的变量(包含对所有管道参数的引用)的类型为 VkGraphicsPipelineCreateInfo。 与之前教程相比,唯一的变化是添加了 pDynamicState 参数,以指出 VkPipelineDynamicStateCreateInfo 结构的类型,如上所示。 每个指定为动态的管道状态均在命令缓冲区记录期间通过相应的函数调用进行设置。

通过调用 vkCreateGraphicsPipelines()函数创建管道对象。

顶点缓冲区创建

如要使用顶点属性,除了在创建管道期间指定它们外,还需准备包含所有这些属性数据的缓冲区。 我们将从该缓冲区读取属性值并将其提供给顶点着色器。

在 Vulkan 中,缓冲区和图像创建包含至少两个阶段: 首先创建对象本身。 然后,我们需要创建内存对象,该对象之后将绑定至缓冲区(或图像)。 缓冲区将从该内存对象中提取存储空间。 这种方法有助于我们指定针对内存的其他参数,并通过更多细节对其进行控制。

我们调用 vkCreateBuffer()创建(通用)缓冲区对象。 它从其他参数中接受类型变量 VkBufferCreateInfo 的指示器,以定义已创建缓冲区的参数。 以下代码负责创建用于顶点属性数据源的缓冲区:

VertexData vertex_data[] = {
  {
    -0.7f, -0.7f, 0.0f, 1.0f,
    1.0f, 0.0f, 0.0f, 0.0f
  },
  {
    -0.7f, 0.7f, 0.0f, 1.0f,
    0.0f, 1.0f, 0.0f, 0.0f
  },
  {
    0.7f, -0.7f, 0.0f, 1.0f,
    0.0f, 0.0f, 1.0f, 0.0f
  },
  {
    0.7f, 0.7f, 0.0f, 1.0f,
    0.3f, 0.3f, 0.3f, 0.0f
  }
};

Vulkan.VertexBuffer.Size = sizeof(vertex_data);

VkBufferCreateInfo buffer_create_info = {
  VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,             // VkStructureType        sType
  nullptr,                                          // const void            *pNext
  0,                                                // VkBufferCreateFlags    flags
  Vulkan.VertexBuffer.Size,                         // VkDeviceSize           size
  VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,                // VkBufferUsageFlags     usage
  VK_SHARING_MODE_EXCLUSIVE,                        // VkSharingMode          sharingMode
  0,                                                // uint32_t               queueFamilyIndexCount
  nullptr                                           // const uint32_t        *pQueueFamilyIndices
};

if( vkCreateBuffer( GetDevice(), &buffer_create_info, nullptr, &Vulkan.VertexBuffer.Handle ) != VK_SUCCESS ) {
  std::cout << "Could not create a vertex buffer!"<< std::endl;
  return false;
}

9.Tutorial04.cpp,函数 CreateVertexBuffer()

我们在 CreateVertexBuffer() 函数开头定义了大量用于位置和颜色属性的数值。 首先为第一个顶点定义四个位置组件,然后为相同的顶点定义四个颜色组件,之后为第二个顶点定义四个有关位置属性的组件,然后为相同顶点定义四个颜色值,之后依次为第三个和第四个顶点定义位置和颜色值。 阵列大小用于定义缓冲区大小。 但请记住,内部显卡驱动程序要求缓冲区的存储大于应用请求的大小。

接下来定义 VkBufferCreateInfo 类型变量。 该结构包含以下字段:

  • sType – 结构类型,应设置为 VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO 。
  • pNext – 为扩展功能预留的参数。
  • flags – 定义其他创建参数的参数。 此处它支持创建通过稀疏内存备份的缓冲区(类似于宏纹理)。 我们不想使用稀疏内存,因此将该参数设为 0。
  • size – 缓冲区的大小(字节)。
  • usage – 定义将来打算如何使用该缓冲区的参数。 我们可以指定为将该缓冲区用作统一缓冲区、索引缓冲区、传输(拷贝)操作数据源等。 这里我们打算将该缓冲区用作顶点缓冲区。 请记住,我们不能将该缓冲区用于缓冲器创建期间未定义的目的。
  • sharingMode – 共享模式,类似于交换链图像,定义特定缓冲区能否供多个队列同时访问(并发共享模式),还是仅供单个队列访问(专有共享模式)。 如果指定为并发共享模式,那么必须提供所有将访问缓冲区的队列的索引。 如果希望定义为专有共享模式,我们仍然可以在不同队列中引用该缓冲区,但一次仅引用一个。 如果希望在不同的队列中使用缓冲区(提交将该缓冲区引用至另一队列的命令),我们需要指定缓冲区内存壁垒,以将缓冲区的所有权移交至另一队列。
  • queueFamilyIndexCount – pQueueFamilyIndices 阵列中的队列索引数量(仅指定为并发共享模式时)。
  • pQueueFamilyIndices – 包含所有队列(将引用缓冲区)的索引阵列(仅指定为并发共享模式时)。

为创建缓冲区,我们必须调用 vkCreateBuffer()函数。

缓冲区内存分配

我们接下来创建内存对象,以备份缓冲区存储。

VkMemoryRequirements buffer_memory_requirements;
vkGetBufferMemoryRequirements( GetDevice(), buffer, &buffer_memory_requirements );

VkPhysicalDeviceMemoryProperties memory_properties;
vkGetPhysicalDeviceMemoryProperties( GetPhysicalDevice(), &memory_properties );

for( uint32_t i = 0; i < memory_properties.memoryTypeCount; ++i ) {
  if( (buffer_memory_requirements.memoryTypeBits & (1 << i)) &&
    (memory_properties.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) ) {

    VkMemoryAllocateInfo memory_allocate_info = {
      VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,     // VkStructureType                        sType
      nullptr,                                    // const void                            *pNext
      buffer_memory_requirements.size,            // VkDeviceSize                           allocationSize
      i                                           // uint32_t                               memoryTypeIndex
    };

    if( vkAllocateMemory( GetDevice(), &memory_allocate_info, nullptr, memory ) == VK_SUCCESS ) {
      return true;
    }
  }
}
return false;

10.Tutorial04.cpp,函数 AllocateBufferMemory()

首先必须检查创建缓冲区需满足哪些内存要求。 为此我们调用 vkGetBufferMemoryRequirements()函数。 它将供内存创建的参数保存在我们在最后一个参数中提供了地址的变量中。 该变量的类型必须为 VkMemoryRequirements,并包含与所需大小、内存对齐,以及支持内存类型等相关的信息。 内存类型有哪些?

每种设备都可拥有并展示不同的内存类型 — 属性不同的尺寸堆。 一种内存类型可能是设备位于 GDDR 芯片上的本地内存(因此速度极快)。 另一种可能是显卡和 CPU 均可见的共享内存。 显卡和应用都可以访问该内存,但这种内存的速度比(仅供显卡访问的)设备本地内存慢。

为查看可用的内存堆和类型,我们需要调用 vkGetPhysicalDeviceMemoryProperties()函数,将相关内存信息保存在类型变量 VkPhysicalDeviceMemoryProperties 中。 它包含以下信息:

  • memoryHeapCount – 特定设备展示的内存堆数量。
  • memoryHeaps – 内存堆阵列。 每个堆代表大小和属性各不相同的内存。
  • memoryTypeCount – 特定设备展示的内存类型数量。
  • memoryTypes – 内存类型阵列。 每个要素描述特定的内存属性,并包含拥有这些特殊属性的内存堆索引。

为特定缓冲区分配内存之前,我们需要查看哪种内存类型满足缓冲区的内存要求。 如果还有其他特定的需求,我们也需要检查。 为此,我们迭代所有可用的内存类型。 缓冲区内存要求中有一个称为 memoryTypeBits 的字段,如果在该字段中设置特定索引的位,表示我们可以为特定缓冲区分配该索引代表的类型的内存。 但必须记住,尽管始终有一种内存类型满足缓冲区的内存要求,但可能不支持其他特定需求。 在这种情况下,我们需要查看另一种内存类型,或更改其他要求。

这里我们的其他要求指内存需要对主机是可见的, 表示应用可以映射并访问该内存 — 读写数据。 这种内存的速度通常比设备本地内存慢,但这样我们可以轻松为顶点属性上传数据。 接下来的教程将介绍如何使用设备本地内存,以提升性能。

幸运的是,主机可见要求非常普遍,因此我们能够轻松找到一种内存类型既能满足缓冲区的内存需求,也具备主机可见性。 然后我们准备类型变量 VkMemoryAllocateInfo,并填充其中的字段:

  • sType – 结构类型,此处设置为 VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • allocationSize – 要求分配的最小内存。
  • memoryTypeIndex – 希望用于已创建内存对象的内存类型索引。 该索引的其中一位在缓冲区内存要求中设置(值为 1)。

填充该结构后,我们调用 vkAllocateMemory()并检查内存对象分配是否成功。

绑定缓冲区内存

创建完内存对象后,必须将其绑定至缓冲区。 如果不绑定,缓冲区中将没有存储空间,我们将无法保存任何数据。

if( !AllocateBufferMemory( Vulkan.VertexBuffer.Handle, &Vulkan.VertexBuffer.Memory ) ) {
  std::cout << "Could not allocate memory for a vertex buffer!"<< std::endl;
  return false;
}

if( vkBindBufferMemory( GetDevice(), Vulkan.VertexBuffer.Handle, Vulkan.VertexBuffer.Memory, 0 ) != VK_SUCCESS ) {
  std::cout << "Could not bind memory for a vertex buffer!"<< std::endl;
  return false;
}

11.Tutorial04.cpp,函数 CreateVertexBuffer()

函数 AllocateBufferMemory() 可分配内存对象, 之前已介绍过。 创建内存对象时,我们通过调用 vkBindBufferMemory()函数,将其绑定至缓冲区。 在调用期间,我们必须指定缓冲区句柄、内存对象句柄和偏移。 偏移非常重要,需要我们另加介绍。

查询缓冲区内存要求时,我们获取了有关所需大小、内存类型和对齐方面的信息。 不同的缓冲区用法要求不同的内存对齐。 内存对象的开头(偏移为 0)满足所有对齐要求。 这表示所有内存对象将在满足所有不同用法要求的地址创建。 因此偏移指定为 0 时,我们完全不用担心。

但我们可以创建更大的内存对象,并将其用作多个缓冲区(或图像)的存储空间。 事实上我们建议使用这种方法。 创建大型内存对象表示我们只需创建较少的内存对象。 这有利于驱动程序跟踪较少数量的对象。 出于操作系统要求和安全措施,驱动程序必须跟踪内存对象。 大型内存对象不会造成较大的内存碎片问题。 最后,我们还应分配较多的内存数量,保持对象的相似性,以提高缓存命中率,从而提升应用性能。

但当我们分配大型内存对象并将其绑定至多个缓冲区(或图像)时,并非所有对象都能绑定在 0 偏移的位置。 只有一种可以如此,其他必须绑定得远一些,在第一个缓冲区(或图像)使用的空间之后。 因此第二个,以及绑定至相同内存对象的缓冲区的偏移必须满足查询报告的对齐要求。 而且我们必须牢记这点。 因此对齐成员至关重要。

创建缓冲区,并为其分配和绑定内存时,我们可以用顶点属性数据填充该缓冲区。

上传顶点数据

我们创建了缓冲区,并绑定了主机可见的内存。 这表示我们可以映射该内存、获取该内存的指示器,并使用该指示器将数据从应用拷贝至该缓冲区(与 OpenGL 的 glBufferData() 函数类似):

void *vertex_buffer_memory_pointer;
if( vkMapMemory( GetDevice(), Vulkan.VertexBuffer.Memory, 0, Vulkan.VertexBuffer.Size, 0, &vertex_buffer_memory_pointer ) != VK_SUCCESS ) {
  std::cout << "Could not map memory and upload data to a vertex buffer!"<< std::endl;
  return false;
}

memcpy( vertex_buffer_memory_pointer, vertex_data, Vulkan.VertexBuffer.Size );

VkMappedMemoryRange flush_range = {
  VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,            // VkStructureType        sType
  nullptr,                                          // const void            *pNext
  Vulkan.VertexBuffer.Memory,                       // VkDeviceMemory         memory
  0,                                                // VkDeviceSize           offset
  VK_WHOLE_SIZE                                     // VkDeviceSize           size
};
vkFlushMappedMemoryRanges( GetDevice(), 1, &flush_range );

vkUnmapMemory( GetDevice(), Vulkan.VertexBuffer.Memory );

return true;

12.Tutorial04.cpp,函数 CreateVertexBuffer()

我们调用 vkMapMemory()函数映射内存。 在该调用中,必须指定我们希望映射哪个内存对象以及访问区域。 区域表示内存对象的存储与大小开头的偏移。 调用成功后我们获取指示器。 我们可以使用该指示器将数据从应用拷贝至提供的内存地址。 这里我们从包含顶点位置和颜色的阵列拷贝顶点数据。

内存拷贝操作之后,取消内存映射之前(无需取消内存映射,可以保留指示器,不会影响性能),我们需要告知驱动程序我们的操作修改了哪部分内存。 该操作称为 flushing。 尽管如此,我们指定所有内存范围,以支持应用将数据拷贝至该范围。 范围无需具备连续性。 通过包含以下字段的 VkMappedMemoryRange 要素阵列可定义该范围:

  • sType – 结构类型,此处等于 VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE。
  • pNext – 为扩展功能预留的指示器。
  • memory – 已映射并修改的内存对象句柄。
  • offset – 特定范围开始的偏移(从特定内存对象的存储开头)。
  • size – 受影响区域的大小(字节)。 如果从偏移到结尾的整个内存均受影响,我们可以使用 VK_WHOLE_SIZE 的特定值。

定义应闪存的所有内存范围时,我们可调用 vkFlushMappedMemoryRanges()函数。 之后,驱动程序将知道哪些部分已修改,并重新加载它们(即刷新高速缓存)。 重新加载通常在壁垒上执行。 修改缓冲区后,我们应设置缓冲区内存壁垒,告知驱动程序部分操作对缓冲区造成了影响,应进行刷新。 不过幸运的是,在本示例中,驱动程序隐式地将壁垒放在命令缓冲区(引用特定缓冲区且不要求其他操作)的提交操作上。 现在我们可以在渲染命令记录期间使用该缓冲区。

渲染资源创建

现在必须准备命令缓冲区记录所需的资源。 我们在之前的教程中为每个交换链图像记录了一个静态命令缓冲区。 这里我们将重新整理渲染代码。 我们仍然展示一个比较简单的静态场景,不过此处介绍的方法可以用于展示动态场景的真实情况。

要想有效地记录命令缓冲区并将其提交至队列,我们需要四类资源:命令缓冲区、旗语、栅栏和帧缓冲器。 旗语我们之前介绍过,用于内部队列同步。 而栅栏支持应用检查是否出现了特定情况,例如命令缓冲区提交至队列后是否已执行完。 如有必要,应用可以等待栅栏,直到收到信号。 一般来说,旗语用于同步队列 (GPU),栅栏用于同步应用 (CPU)。

如要渲染一帧简单的动画,我们(至少)需要一个命令缓冲区,两个旗语 — 一个用于获取交换链图像(图像可用的旗语),另一个用于发出可进行演示的信号(渲染已完成的旗语) —,一个栅栏和一个帧缓冲器。 栅栏稍后将用于检查我们是否重新记录了特定命令缓冲区。 我们将保留部分渲染资源,以调用虚拟帧虚拟帧(包含一个命令缓冲区、两个旗语、一个栅栏和一个帧缓冲器)的数量与交换链图像数量无关。

渲染算法进展如下: 我们将渲染命令记录至第一个虚拟帧,然后将其提交至队列。 然后记录另一帧(命令缓冲区)并将其提交至队列。 直到记录并提交完所有虚拟帧。 这时我们通过提取并再次重新记录最之前(最早提交)的命令缓冲区,开始重复使用帧。 然后使用另一命令缓冲区,依此类推。

此时栅栏登场。我们不允许记录已提交至队列,但在队列中的没有执行完的命令缓冲区。 在记录命令缓冲区期间,我们可以使用“simultaneous use”标记,以记录或重新提交之前已提交过的命令缓冲区。 不过这样会影响性能。 最好的方法是使用栅栏,检查命令缓冲区是否不能再使用。 如果显卡仍然在处理命令缓冲区,我们可以等待与特定命令缓冲区相关的栅栏,或将这额外的时间用于其他目的,比如改进 AI 计算,并在这之后再次检查以查看栅栏是否收到了信号。

我们应准备多少虚拟帧? 一个远远不够。 记录并提交单个命令缓冲区时,我们会立即等待,直到能够重新记录该缓冲区。 这样会导致 CPU 和 GPU 浪费时间。 GPU 的速度通常更快,因此等待 CPU 会造成 GPU 等待的时间更长。 我们应该保持 GPU 的繁忙状态。 因此我们创建了 Vulkan 等瘦 API。 使用两个虚拟帧将会显著提升性能,因为这样会大大缩短 CPU 和 GPU 的等待时间。 添加第三个虚拟帧能够进一步提升性能,但提升空间不大。 使用四组以上渲染资源没有太大意义,因为其性能提升可忽略不计(当然这取决于已渲染场景的复杂程度和类似 CPU 的物理组件或 AI 所执行的计算)。 增加虚拟帧数量时,会提高输入延迟,因为我们在 CPU 背后演示的帧为 1-3 个。 因此两个或三个虚拟帧似乎是最合理的组合,能够使性能、内存使用和输入延迟之间达到最佳动平衡。

您可能想知道虚拟帧的数量为何与交换链图像数量无关。 这种方法会影响应用的行为。 创建交换链时,我们请求所需图像的最小数量,但驱动程序允许创建更多图像。 因此不同的硬件厂商可能实施提供不同数量交换链图像的驱动程序,甚至要求相同(演示模式和最少图像数量)时也如此。 建立虚拟帧数量与交换链数量的相关性后,应用将在一个显卡上仅使用两个虚拟帧,而在另一显卡上使用四个虚拟帧。 这样会影响性能和之前提到的输入延迟。 因此我们不希望出现这种行为。 通过保持固定数量的虚拟帧,我们可以控制渲染算法,并对其进行调优以满足需求,即渲染时间与 AI 或物理计算之间的平衡。

命令池创建

分配命令缓冲区之前,我们首先需要创建一个命令池。

VkCommandPoolCreateInfo cmd_pool_create_info = {
  VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,       // VkStructureType                sType
  nullptr,                                          // const void                    *pNext
  VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT | // VkCommandPoolCreateFlags       flags
  VK_COMMAND_POOL_CREATE_TRANSIENT_BIT,
  queue_family_index                                // uint32_t                       queueFamilyIndex
};

if( vkCreateCommandPool( GetDevice(), &cmd_pool_create_info, nullptr, pool ) != VK_SUCCESS ) {
  return false;
}
return true;

13.Tutorial04.cpp,函数 CreateCommandPool()

通过调用 vkCreateCommandPool()创建命令池,其中要求我们提供类型变量 VkCommandPoolCreateInfo 的指示器。 与之前的教程相比,代码基本保持不变。 但这次添加两个其他的标记以创建命令池:

  • VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT – 表示通过命令池分配的命令缓冲区可单独重新设置。 正常来说,如果没有这一标记,我们将无法多次重新记录相同的命令缓冲区。 它必须首先重新设置。 而且从命令池创建的命令缓冲区只能一起重新设置。 指定该标记可支持我们单独重新设置命令缓冲区,而且(甚至更好)通过调用 vkBeginCommandBuffer()函数隐式地完成这一操作。
  • VK_COMMAND_POOL_CREATE_TRANSIENT_BIT – 该标记告知驱动程序通过该命令池分配的命令缓冲区将仅短时间内存在,需要经常重新记录和重新设置。 该信息可帮助优化命令缓冲区分配并以更好地方式执行。

命令缓冲区分配

命令缓冲区分配与之前的相同。

for( size_t i = 0; i < Vulkan.RenderingResources.size(); ++i ) {
  if( !AllocateCommandBuffers( Vulkan.CommandPool, 1, &Vulkan.RenderingResources[i].CommandBuffer ) ) {
    std::cout << "Could not allocate command buffer!"<< std::endl;
    return false;
  }
}
return true;

14.Tutorial04.cpp,函数 CreateCommandBuffers()

唯一的变化是命令缓冲区收集在渲染资源矢量中。 每个渲染资源结构均包含一个命令缓冲区、图像可用旗语、渲染已完成旗语、一个栅栏和一个帧缓冲器。 命令缓冲区循环分配。 渲染资源矢量中的要素数量可随意选择。 在本教程中,该数量为 3。

旗语创建

负责创建旗语的代码非常简单,与之前的相同:

VkSemaphoreCreateInfo semaphore_create_info = {
  VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,      // VkStructureType          sType
  nullptr,                                      // const void*              pNext
  0                                             // VkSemaphoreCreateFlags   flags
};

for( size_t i = 0; i < Vulkan.RenderingResources.size(); ++i ) {
  if( (vkCreateSemaphore( GetDevice(), &semaphore_create_info, nullptr, &Vulkan.RenderingResources[i].ImageAvailableSemaphore ) != VK_SUCCESS) ||
    (vkCreateSemaphore( GetDevice(), &semaphore_create_info, nullptr, &Vulkan.RenderingResources[i].FinishedRenderingSemaphore ) != VK_SUCCESS) ) {
    std::cout << "Could not create semaphores!"<< std::endl;
    return false;
  }
}
return true;

15.Tutorial04.cpp,函数 CreateSemaphores()

栅栏创建

以下代码负责创建栅栏对象:

VkFenceCreateInfo fence_create_info = {
  VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,              // VkStructureType                sType
  nullptr,                                          // const void                    *pNext
  VK_FENCE_CREATE_SIGNALED_BIT                      // VkFenceCreateFlags             flags
};

for( size_t i = 0; i < Vulkan.RenderingResources.size(); ++i ) {
  if( vkCreateFence( GetDevice(), &fence_create_info, nullptr, &Vulkan.RenderingResources[i].Fence ) != VK_SUCCESS ) {
    std::cout << "Could not create a fence!"<< std::endl;
    return false;
  }
}
return true;

16.Tutorial04.cpp,函数 CreateFences()

我们调用 vkCreateFence()函数,以创建栅栏对象。 它从其他参数中接受类型变量 VkFenceCreateInfo 的指示器,其中包含以下成员:

  • sType – 结构类型。 此处应设置为 VK_STRUCTURE_TYPE_FENCE_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 目前该参数支持创建已收到信号的栅栏。

栅栏包含两种状态:收到信号和未收到信号。 该应用检查特定栅栏是否处于收到信号状态,否则将等待栅栏收到信号。 提交至队列的所有操作均处理完后,由 GPU 发出信号。 提交命令缓冲区时,我们可以提供一个栅栏,该栅栏将在队列执行完提交操作发布的所有命令后收到信号。 栅栏收到信号后,将由应用负责将其重新设置为未收到信号状态。

为何创建收到信号的栅栏? 渲染算法将命令记录至第一个命令缓冲区,然后是第二个命令缓冲器,之后是第三个,然后(队列中的执行结束后)再次记录至第一个命令缓冲区。 我们使用栅栏检查能否再次记录特定命令缓冲区。 那么第一次记录会怎样呢? 我们不希望第一次命令缓冲区记录和接下来的记录操作为不同的代码路径。 因此第一次发布命令缓冲区记录时,我们还检查栅栏是否已收到信号。 但因为我们没有提交特定命令缓冲区,因此与此相关的栅栏在执行完成后无法变成收到信号状态。 因此需要以已收到信号状态创建栅栏。 这样第一次记录时我们无需等待它变成已收到信号状态(因为它已经是已收到信号状态),但检查之后,我们要重新设置并立即前往记录代码。 之后我们提交命令缓冲区并提供相同的栅栏,这样在完成操作后将收到队列发来的信号。 下一次当我们希望将渲染命令记录至相同的命令缓冲区时,我们可以执行同样的操作:等待栅栏,重新设置栅栏,然后开始记录命令缓冲区。

绘制

现在我们准备记录渲染操作。 我们将正好在提交至队列之前记录命令缓冲区。 记录并提交一个命令缓冲区,然后记录并提交下一个命令缓冲区,然后记录并提交另一个。 在这之后我们提取第一个命令缓冲区,检查是否可用,并记录并将其提交至队列。

static size_t           resource_index = 0;
RenderingResourcesData ¤t_rendering_resource = Vulkan.RenderingResources[resource_index];
VkSwapchainKHR          swap_chain = GetSwapChain().Handle;
uint32_t                image_index;

resource_index = (resource_index + 1) % VulkanTutorial04Parameters::ResourcesCount;

if( vkWaitForFences( GetDevice(), 1, ¤t_rendering_resource.Fence, VK_FALSE, 1000000000 ) != VK_SUCCESS ) {
  std::cout << "Waiting for fence takes too long!"<< std::endl;
  return false;
}
vkResetFences( GetDevice(), 1, ¤t_rendering_resource.Fence );

VkResult result = vkAcquireNextImageKHR( GetDevice(), swap_chain, UINT64_MAX, current_rendering_resource.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;
}

if( !PrepareFrame( current_rendering_resource.CommandBuffer, GetSwapChain().Images[image_index], current_rendering_resource.Framebuffer ) ) {
  return false;
}

VkPipelineStageFlags wait_dst_stage_mask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSubmitInfo submit_info = {
  VK_STRUCTURE_TYPE_SUBMIT_INFO,                          // VkStructureType              sType
  nullptr,                                                // const void                  *pNext
  1,                                                      // uint32_t                     waitSemaphoreCount
  ¤t_rendering_resource.ImageAvailableSemaphore,    // const VkSemaphore           *pWaitSemaphores
  &wait_dst_stage_mask,                                   // const VkPipelineStageFlags  *pWaitDstStageMask;
  1,                                                      // uint32_t                     commandBufferCount¤t_rendering_resource.CommandBuffer,              // const VkCommandBuffer       *pCommandBuffers
  1,                                                      // uint32_t                     signalSemaphoreCount
  ¤t_rendering_resource.FinishedRenderingSemaphore  // const VkSemaphore           *pSignalSemaphores
};

if( vkQueueSubmit( GetGraphicsQueue().Handle, 1, &submit_info, current_rendering_resource.Fence ) != VK_SUCCESS ) {
  return false;
}

VkPresentInfoKHR present_info = {
  VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,                     // VkStructureType              sType
  nullptr,                                                // const void                  *pNext
  1,                                                      // uint32_t                     waitSemaphoreCount
  ¤t_rendering_resource.FinishedRenderingSemaphore, // const VkSemaphore           *pWaitSemaphores
  1,                                                      // uint32_t                     swapchainCount
  &swap_chain,                                            // const VkSwapchainKHR        *pSwapchains&image_index,                                           // const uint32_t              *pImageIndices
  nullptr                                                 // VkResult                    *pResults
};
result = vkQueuePresentKHR( GetPresentQueue().Handle, &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;

17.Tutorial04.cpp,函数 Draw()

首先提取最近使用的渲染资源。 然后等待与该组相关的栅栏收到信号。 如果收到了信号,表示我们可以安全提取并记录命令缓冲区。 不过它还表示我们可以提取用于获取并演示在特定命令缓冲区中引用的旗语。 不能将同一个旗语用于不同的目的或两项不同的提交操作,必须等待之前的提交操作完成。 栅栏可防止我们修改命令缓冲区和旗语。 而且大家会看到,帧缓冲器也是如此。

栅栏完成后,我们重新设置栅栏并执行与正常绘制相关的操作:获取图像,记录渲染已获取图像的操作,提交命令缓冲区,并演示图像。

然后提取另一渲染资源集并执行相同的操作。 由于保留了三组渲染资源,三个虚拟帧,我们可缩短等待栅栏收到信号的时间。

记录命令缓冲区

负责记录命令缓冲区的函数很长。 此时会更长,因为我们使用顶点缓冲区和动态视口及 scissor 测试。 而且我们还创建临时帧缓冲器!

帧缓冲器的创建非常简单、快速。 同时保留帧缓冲器对象和交换链意味着,需要重新创建交换链时,我们需要重新创建这些对象。 如果渲染算法复杂,我们将有多个图像以及与之相关的帧缓冲器。 如果这些图像的大小必须与交换链图像的大小相同,那么我们需要重新创建所有图像(以纳入潜在的大小变化)。 因此最好按照需求创建帧缓冲器,这样也更方便。 这样它们的大小将始终符合要求。 帧缓冲器在面向特定图像创建的图像视图上运行。 交换链重新创建时,旧的图像将无效并消失。 因此我们必须重新创建图像视图和帧缓冲器。

在“03-第一个三角形”教程中,我们有大小固定的帧缓冲器,而且需要与交换链同时重新创建。 现在我们的帧缓冲器对象在每个虚拟帧资源组中。 记录命令缓冲器之前,我们要为将渲染的图像创建帧缓冲器,其大小与图像相同。 这样当我们重新创建交换链时,将立即调整下一帧的大小,而且新交换链图像的句柄及其图像视图将用于创建帧缓冲器。

记录使用渲染通道和帧缓冲器对象的命令缓冲区时,在队列处理命令缓冲区期间,帧缓冲器必须始终保持有效。 创建新的帧缓冲器时,命令提交至队列的操作完成后我们才开始破坏它。 不过由于我们使用栅栏,而且等待与特定命令缓冲区相关的栅栏,因为能够确保安全地破坏帧缓冲器。 然后我们创建新的帧缓冲器,以纳入潜在的大小和图像句柄变化。

if( framebuffer != VK_NULL_HANDLE ) {
  vkDestroyFramebuffer( GetDevice(), framebuffer, nullptr );
}

VkFramebufferCreateInfo framebuffer_create_info = {
  VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,      // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  0,                                              // VkFramebufferCreateFlags       flags
  Vulkan.RenderPass,                              // VkRenderPass                   renderPass
  1,                                              // uint32_t                       attachmentCount
  &image_view,                                    // const VkImageView             *pAttachments
  GetSwapChain().Extent.width,                    // uint32_t                       width
  GetSwapChain().Extent.height,                   // uint32_t                       height
  1                                               // uint32_t                       layers
};

if( vkCreateFramebuffer( GetDevice(), &framebuffer_create_info, nullptr, &framebuffer ) != VK_SUCCESS ) {
  std::cout << "Could not create a framebuffer!"<< std::endl;
  return false;
}

return true;

18.Tutorial04.cpp,函数 CreateFramebuffer()

创建帧缓冲器时,我们提取当前的交换链扩展,以及已获取交换链图像的图像视图。

接下来开始记录命令缓冲区:

if( !CreateFramebuffer( framebuffer, image_parameters.View ) ) {
  return false;
}

VkCommandBufferBeginInfo command_buffer_begin_info = {
  VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,        // VkStructureType                        sType
  nullptr,                                            // const void                            *pNext
  VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,        // VkCommandBufferUsageFlags              flags
  nullptr                                             // const VkCommandBufferInheritanceInfo  *pInheritanceInfo
};

vkBeginCommandBuffer( command_buffer, &command_buffer_begin_info );

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
};

if( GetPresentQueue().Handle != GetGraphicsQueue().Handle ) {
  VkImageMemoryBarrier barrier_from_present_to_draw = {
    VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,           // VkStructureType                        sType
    nullptr,                                          // const void                            *pNext
    VK_ACCESS_MEMORY_READ_BIT,                        // VkAccessFlags                          srcAccessMask
    VK_ACCESS_MEMORY_READ_BIT,                        // VkAccessFlags                          dstAccessMask
    VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,                  // VkImageLayout                          oldLayout
    VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,                  // VkImageLayout                          newLayout
    GetPresentQueue().FamilyIndex,                    // uint32_t                               srcQueueFamilyIndex
    GetGraphicsQueue().FamilyIndex,                   // uint32_t                               dstQueueFamilyIndex
    image_parameters.Handle,                          // VkImage                                image
    image_subresource_range                           // VkImageSubresourceRange                subresourceRange
  };
  vkCmdPipelineBarrier( command_buffer, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier_from_present_to_draw );
}

VkClearValue clear_value = {
  { 1.0f, 0.8f, 0.4f, 0.0f },                         // VkClearColorValue                      color
};

VkRenderPassBeginInfo render_pass_begin_info = {
  VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,           // VkStructureType                        sType
  nullptr,                                            // const void                            *pNext
  Vulkan.RenderPass,                                  // VkRenderPass                           renderPass
  framebuffer,                                        // VkFramebuffer                          framebuffer
  {                                                   // VkRect2D                               renderArea
    {                                                 // VkOffset2D                             offset
      0,                                                // int32_t                                x
      0                                                 // int32_t                                y
    },
    GetSwapChain().Extent,                            // VkExtent2D                             extent;
  },
  1,                                                  // uint32_t                               clearValueCount
  &clear_value                                        // const VkClearValue                    *pClearValues
};

vkCmdBeginRenderPass( command_buffer, &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE );

19.Tutorial04.cpp,函数 PrepareFrame()

首先定义类型变量 VkCommandBufferBeginInfo,并指定命令缓冲区只能提交一次。 指定 VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT 标记时,不能多次提交特定命令缓冲区。 每次提交后,必须重新设置。 但记录操作重新设置它的原因是 VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT 标记用于命令池创建。

接下来我们定义有关图像内存壁垒的子资源范围。 交换链图像布局过渡在渲染通道中隐式执行,但如果显卡队列和演示队列不同,则必须手动执行队列过渡。

然后启动包含临时帧缓冲器对象的渲染通道。

vkCmdBindPipeline( command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, Vulkan.GraphicsPipeline );

VkViewport viewport = {
  0.0f,                                               // float                                  x
  0.0f,                                               // float                                  y
  static_cast<float>(GetSwapChain().Extent.width),    // float                                  width
  static_cast<float>(GetSwapChain().Extent.height),   // float                                  height
  0.0f,                                               // float                                  minDepth
  1.0f                                                // float                                  maxDepth
};

VkRect2D scissor = {
  {                                                   // VkOffset2D                             offset
    0,                                                  // int32_t                                x
    0                                                   // int32_t                                y
  },
  {                                                   // VkExtent2D                             extent
    GetSwapChain().Extent.width,                        // uint32_t                               width
    GetSwapChain().Extent.height                        // uint32_t                               height
  }
};

vkCmdSetViewport( command_buffer, 0, 1, &viewport );
vkCmdSetScissor( command_buffer, 0, 1, &scissor );

VkDeviceSize offset = 0;
vkCmdBindVertexBuffers( command_buffer, 0, 1, &Vulkan.VertexBuffer.Handle, &offset );

vkCmdDraw( command_buffer, 4, 1, 0, 0 );

vkCmdEndRenderPass( command_buffer );

if( GetGraphicsQueue().Handle != GetPresentQueue().Handle ) {
  VkImageMemoryBarrier barrier_from_draw_to_present = {
    VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,           // VkStructureType                        sType
    nullptr,                                          // const void                            *pNext
    VK_ACCESS_MEMORY_READ_BIT,                        // VkAccessFlags                          srcAccessMask
    VK_ACCESS_MEMORY_READ_BIT,                        // VkAccessFlags                          dstAccessMask
    VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,                  // VkImageLayout                          oldLayout
    VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,                  // VkImageLayout                          newLayout
    GetGraphicsQueue().FamilyIndex,                   // uint32_t                               srcQueueFamilyIndex
    GetPresentQueue().FamilyIndex,                    // uint32_t                               dstQueueFamilyIndex
    image_parameters.Handle,                          // VkImage                                image
    image_subresource_range                           // VkImageSubresourceRange                subresourceRange
  };
  vkCmdPipelineBarrier( command_buffer, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier_from_draw_to_present );
}

if( vkEndCommandBuffer( command_buffer ) != VK_SUCCESS ) {
  std::cout << "Could not record command buffer!"<< std::endl;
  return false;
}
return true;

20.Tutorial04.cpp,函数 PrepareFrame()

接下来我们绑定图像管道。 它包含两个标记为动态的状态:视口和 scissor 测试。 因此我们准备能够定义视口和 scissor 测试参数的结构。 通过调用 vkCmdSetViewport()函数设置动态视口。 通过调用 vkCmdSetScissor()函数设置动态 scissor 测试。 这样图像管道可用于渲染大小不同的图像。

进行绘制之前的最后一件事是绑定相应的顶点缓冲区,为顶点属性提供缓冲区数据。 此操作通过调用 vkCmdBindVertexBuffers()函数完成。 我们指定一个绑定号码(哪个顶点属性集从该缓冲区提取数据)、一个缓冲区句柄指示器(或较多句柄,如果希望绑定多个绑定的缓冲区),以及偏移。 偏移指定应从缓冲区的较远部分提取有关顶点属性的数据。 但指定的偏移不能大于相应缓冲区(该缓冲区未绑定内存对象)的范围。

现在我们已经指定了全部所需要素:帧缓冲器、视口和 scissor 测试,以及顶点缓冲区。 我们可以绘制几何图形、完成渲染通道,并结束命令缓冲区。

教程 04 执行

以下是渲染操作的结果:

我们渲染了一个四边形,各个角落的颜色均不相同。 尝试更改窗口的大小;之前的三角形始终保持相同的大小,仅应用窗口右侧和底部的黑色方框变大或变小。 现在,由于是动态视口,因此四边形将随着窗口的变化而变大或变小。

清空

完成渲染后,关闭应用之前,我们需要破坏所有资源。 以下代码负责完成此项操作:

if( GetDevice() != VK_NULL_HANDLE ) {
  vkDeviceWaitIdle( GetDevice() );

  for( size_t i = 0; i < Vulkan.RenderingResources.size(); ++i ) {
    if( Vulkan.RenderingResources[i].Framebuffer != VK_NULL_HANDLE ) {
      vkDestroyFramebuffer( GetDevice(), Vulkan.RenderingResources[i].Framebuffer, nullptr );
    }
    if( Vulkan.RenderingResources[i].CommandBuffer != VK_NULL_HANDLE ) {
      vkFreeCommandBuffers( GetDevice(), Vulkan.CommandPool, 1, &Vulkan.RenderingResources[i].CommandBuffer );
    }
    if( Vulkan.RenderingResources[i].ImageAvailableSemaphore != VK_NULL_HANDLE ) {
      vkDestroySemaphore( GetDevice(), Vulkan.RenderingResources[i].ImageAvailableSemaphore, nullptr );
    }
    if( Vulkan.RenderingResources[i].FinishedRenderingSemaphore != VK_NULL_HANDLE ) {
      vkDestroySemaphore( GetDevice(), Vulkan.RenderingResources[i].FinishedRenderingSemaphore, nullptr );
    }
    if( Vulkan.RenderingResources[i].Fence != VK_NULL_HANDLE ) {
      vkDestroyFence( GetDevice(), Vulkan.RenderingResources[i].Fence, nullptr );
    }
  }

  if( Vulkan.CommandPool != VK_NULL_HANDLE ) {
    vkDestroyCommandPool( GetDevice(), Vulkan.CommandPool, nullptr );
    Vulkan.CommandPool = VK_NULL_HANDLE;
  }

  if( Vulkan.VertexBuffer.Handle != VK_NULL_HANDLE ) {
    vkDestroyBuffer( GetDevice(), Vulkan.VertexBuffer.Handle, nullptr );
    Vulkan.VertexBuffer.Handle = VK_NULL_HANDLE;
  }

  if( Vulkan.VertexBuffer.Memory != VK_NULL_HANDLE ) {
    vkFreeMemory( GetDevice(), Vulkan.VertexBuffer.Memory, nullptr );
    Vulkan.VertexBuffer.Memory = VK_NULL_HANDLE;
  }

  if( Vulkan.GraphicsPipeline != VK_NULL_HANDLE ) {
    vkDestroyPipeline( GetDevice(), Vulkan.GraphicsPipeline, nullptr );
    Vulkan.GraphicsPipeline = VK_NULL_HANDLE;
  }

  if( Vulkan.RenderPass != VK_NULL_HANDLE ) {
    vkDestroyRenderPass( GetDevice(), Vulkan.RenderPass, nullptr );
    Vulkan.RenderPass = VK_NULL_HANDLE;
  }
}

21.Tutorial04.cpp,destructor

设备处理完所有提交至阵列的命令后,我们开始破坏所有资源。 资源破坏以相反的顺序进行。 首先破坏所有渲染资源:帧缓冲器、命令缓冲区、旗语和栅栏。 栅栏通过调用 vkDestroyFence()函数破坏。 然后破坏命令池。 之后通过调用 vkDestroyBuffer()函数和 vkFreeMemory()函数分别破坏缓冲区和空闲内存对象。 最后破坏管道对象和渲染通道。

结论

本教程的编写以“03-第一个三角形”教程为基础。 我们通过在图像管道中使用顶点属性,并在记录命令缓冲区期间绑定顶点缓冲区,以此改进渲染过程。 我们介绍了顶点属性的数量和布局, 针对视口和 scissors 测试引入了动态管道状态, 并学习了如何创建缓冲区和内存对象,以及如何相互绑定。 我们还映射了内存,并将数据从 CPU 上传至 GPU。

我们创建了渲染资源集,以高效记录和发布渲染命令。 这些资源包括命令缓冲器、旗语、栅栏和帧缓冲器。 我们学习了如何使用栅栏,如何设置动态管道状态的值,以及如何在记录命令缓冲区期间绑定顶点缓冲区(顶点属性数据源)。

下节教程将介绍分期资源。 它们是用于在 CPU 和 GPU 之间拷贝数据的中间缓冲区。 这样应用无需映射用于渲染的缓冲区(或图像),它们也不必绑定至设备的本地(快速)内存。

没有任何秘密的 API:Vulkan* 简介第 0 部分:前言

$
0
0

下载  [PDF 456K]

Github 示例代码链接

关于作者

我成为软件开发人员已有超过 9 年的时间。 我最感兴趣的领域是图形编程,大部分工作主要涉及 3D 图形。 我在 OpenGL* 和着色语言(主要是 GLSL 和 Cg)方面拥有丰富的经验。三年来我还一直致力于开发 Unity* 软件。 我也曾有机会投身于涉及头盔式显示器(比如 Oculus Rift*) 或类似 CAVE 系统的 VR 项目。

最近,我正与英特尔的团队成员一起准备验证工具,为被称为 Vulkan 的新兴 API 提供显卡驱动程序支持。 图形编程接口及其使用方法对我来说非常新鲜。 在了解这些内容的时候我突然想到,我可以同时准备有关使用 Vulkan 编写应用的教程。 我可以像那些了解 OpenGL 并希望“迁移”至其后续产品的人一样,分享我的想法和经验。

关于 Vulkan

Vulkan 被视作是 OpenGL 的后续产品。 它是一种多平台 API,可支持开发人员准备游戏、CAD 工具、性能基准测试等高性能图形应用。 它可在不同的操作系统(比如 Windows*、Linux* 或 Android*)上使用。 Vulkan 由科纳斯组织创建和维护。 Vulkan 与 OpenGL 之间还有其他相似之处,包括图形管道阶段、OpenGL 着色器(一定程度上),或命名。

但也存在许多差异,但这进一步验证了新 API 的必要性。 20 多年来,OpenGL 一直处于不断变化之中。 自 90 年代以来,计算机行业发生了巨大的变化,尤其是显卡架构领域。 OpenGL 库非常适用,但仅依靠添加新功能以匹配新显卡功能并不能解决一切问题。 有时需要完全重新设计。 因此创建出了 Vulkan。

Vulkan 基于 Mantle* — 第一个全新的低级别图形 API。 Mantle 由 AMD 开发而成,专为 Radeon 卡架构而设计。 尽管是第一个公开发布的 API,但使用 Mantle 的游戏和基准测试均显著提升了性能。 后来陆续发布了其他低级别 API,比如 Microsoft 的 DirectX* 12、Apple 的 Metal*,以及现在的 Vulkan。

传统图形 API 和全新低级别 API 之间有何区别? OpenGL 等高级别 API 使用起来非常简单。 开发人员只需声明操作内容和操作方式,剩下的都由驱动程序来完成。 驱动程序检查开发人员是否正确使用 API 调用、是否传递了正确的参数,以及是否充分准备了状态。 如果出现问题,将提供反馈。 为实现其易用性,许多任务必须由驱动程序在“后台”执行。

在低级别 API 中,开发人员需要负责完成大部分任务。 他们需要符合严格的编程和使用规则,还必须编写大量代码。 但这种做法是合理的。 开发人员知道他们的操作内容和希望实现的目的。 但驱动程序不知道,因此使用传统 API 时,驱动程序必须完成更多工作,以便程序正常运行。 采用 Vulkan 等 API 可避免这些额外的工作。 因此 DirectX 12、Metal 或 Vulkan 也被称为精简驱动程序/精简 API。 大部分时候它们仅将用户请求传输至硬件,仅提供硬件的精简抽象层。 为显著提升性能,驱动程序几乎不执行任何操作。

低级别 API 要求应用完成更多工作。 但这种工作是不可避免的, 必须要有人去完成。 因此由开发人员去完成更加合理,因为他们知道如何将工作分成独立的线程,图像何时成为渲染对象(颜色附件)或用作纹理/采样器等等。 开发人员知道管道处于何种状态,或哪些顶点属性变化的更频繁。 这样有助于提高显卡硬件的使用效率。 最重要的原因是它行之有效。 我们能够观察到显著的性能提升。

但“能够”一词非常重要。 它要求完成其他工作,但同时也是一种合适的方法。 在有一些场景中,我们将观察到,OpenGL 和 Vulkan 之间在性能方面没有任何差别。 如果不需要多线程化,或应用不是 CPU 密集型(渲染的场景不太复杂),使用 OpenGL 即可,而且使用 Vulkan 不会实现性能提升(但可能会降低功耗,这对移动设备至关重要)。 但如果我们想最大限度地发挥图形硬件的功能,Vulkan 将是最佳选择。

主要显卡引擎迟早会支持部分(如果不是所有)新的低级别 API。 如果希望使用 Vulkan 或其他 API,无需从头进行编写。 但通常最好对“深层”信息有所了解,因此我准备这一教程。

源代码说明

我是 Windows 开发人员 如果有选择,我选择编写面向 Windows 的应用。 因为我在其他操作系统方面没有任何经验。 但 Vulkan 是多平台 API,而且我希望展示它可用于不同的操作系统。 因此我们准备了一个示例项目,可在 Windows 和 Linux 上编译和执行。

关于本教程的源代码,请访问:

https://github.com/GameTechDev/IntroductionToVulkan

我曾尝试编写尽可能简单的代码示例,而且代码中不会掺杂不必要的“#ifdefs”。 但有时不可避免(比如在窗口创建和管理过程中),因此我们决定将代码分成几个小部分:

  • Tutorial文件,是这里最重要的一部分。 与 Vulkan 相关的所有代码都可放置在该文件中。 每节课都放在一个标头/源配对中。
  • OperatingSystem标头和源文件,包含依赖于操作系统的代码部分,比如窗口创建、消息处理和渲染循环。 这些文件包含面向 Linux 和 Windows 的代码,不过我试着尽可能地保持统一。
  • main.cpp文件,每节课的起点。 由于它使用自定义 Window 类,因此不包含任何特定于操作系统的代码。
  • VulkanCommon标头/源文件,包含面向从教程 3 之后各教程的基本课程。 该类基本上重复教程 1 和 2 — 创建 Vulkan 实例和渲染图像和其他所需的资源,以在屏幕上显示图像。 我提取了这一准备代码,因此其他章节的代码可以仅专注于所介绍的主题。
  • 工具,包含其他实用程序函数和类,比如读取二进制文件内容的函数,或用于自动破坏对象的包装程序类。

每个章节的代码都放置在单独的文件夹中。 有时可包含其他数据目录,其中放置了用于某特定章节的资源,比如着色器或纹理。 数据文件夹应拷贝至包含可执行文件的相同目录。 默认情况下可执行文件将编译成构建文件夹。

没错。 编译和构建文件夹。 由于示例项目可在 Windows 和 Linux 上轻松维护,因此我决定使用 CMakeLists.txt 文件和 CMake 工具。 Windows 上有一个 build.bat 文件,可创建 Visual Studio* 解决方案 — (默认情况下)Microsoft Visual Studio 2013 需要编译 Windows 上的代码。 我在 Linux 上提供了一个 build.sh 脚本,可使用 make 编译代码,但使用 Qt 等工具也可轻松打开 CMakeLists.txt。当然还需要 CMake。

生成解决方案与项目文件,而且可执行文件将编译至构建文件夹。 该文件夹也是默认的工作目录,因此数据文件夹应拷贝至该目录,以便课程正常运行。 执行过程中如果出现问题,cmd/terminal 中将“打印”其他信息。 如果出现问题,将通过命令行/终端运行课程,或检查控制台/终端窗口,以查看是否显示了消息。

我希望这些说明能够帮助大家了解并跟上 Vulkan 教程的节奏。 现在我们来重点学习 Vulkan!


请前往: 没有任何秘密的 API: Vulkan* 简介第 1 部分: 序言


声明

本文件不构成对任何知识产权的授权,包括明示的、暗示的,也无论是基于禁止反言的原则或其他。

英特尔明确拒绝所有明确或隐含的担保,包括但不限于对于适销性、特定用途适用性和不侵犯任何权利的隐含担保,以及任何对于履约习惯、交易习惯或贸易惯例的担保。

本文包含尚处于开发阶段的产品、服务和/或流程的信息。 此处提供的信息可随时改变而毋需通知。 联系您的英特尔代表,了解最新的预测、时间表、规格和路线图。

本文件所描述的产品和服务可能包含使其与宣称的规格不符的设计缺陷或失误。 英特尔提供最新的勘误表备索。

如欲获取本文提及的带订购编号的文档副本,可致电 1-800-548-4725,或访问 www.intel.com/design/literature.htm

该示例源代码根据英特尔示例源代码许可协议发布。

英特尔和 Intel 标识是英特尔在美国和/或其他国家的商标。

*其他的名称和品牌可能是其他所有者的资产。

英特尔公司 © 2016 年版权所有。

整理您的数据和代码: 数据和布局 - 第 2 部分

$
0
0

这两篇关于性能和内存的文章介绍了一些基本概念,用于指导开发人员更好地改善软件性能。为实现此目标,文章内容重点阐述了内存和数据布局方面的注意事项。第 1 部分介绍了寄存器使用以及覆盖或阻塞算法,以改善数据重用情况。文章从考虑数据布局以提供通用并行处理能力(与线程共享内存编程)开始,然后还考虑了基于 MPI 的分布式计算。本文扩展了在实现并行处理能力时需考虑的概念,包括矢量化(单指令多数据 SIMD)、共享内存并行处理(线程化)和分布式内存计算等。最后,文章考虑了数据布局结构阵列 (AOS) 以及阵列结构 (SOA) 数据布局。

第 1 部分强调的基本性能原则是:在寄存器或缓存中重新利用数据然后再将其去除。在本文中强调的性能原则为:在最常用数据的地方放置数据,以连续访问模式放置数据,并避免数据冲突。

 

与线程共享内存编程

让我们从考虑与线程共享内存编程开始。全部线程都共享进程中的相同内存。有许多常用的线程模型。最为闻名的是 Posix* 线程和 Windows* 线程。正确创建和管理线程中涉及的工作容易出错。涉及大量模块和大型开发团队的现代软件让线程的并行编程极易出错。在此过程中开发团队需要开发数个程序包,以简化线程创建、管理和充分利用并行线程。最常用的两个模型为 OpenMP* 和英特尔® 线程构建模块。第三个线程模型英特尔® Cilk™ Plus 还未达到与 OpenMP 和线程构建模块相似的采用级别。所有这些线程模型形成线程池,该线程池被重新用于每个并行操作或并行区域。OpenMP 的优势在于可通过使用指令,逐步提高并行处理能力。OpenMP 指令通常可添加至现有软件,并仅需对每一步骤的过程进行最少的代码变更。它允许使用线程运行时库来管理大量线程维护工作,可显著简化线程软件的开发。同时它还可以为所有代码开发人员提供一个可沿用的一致线程模型,减少一些常见线程错误的可能性,并提供由专注于线程优化的开发人员制作的最优线程化运行时库。

简介段落中提及的基本并行原则为将数据置于使用该数据之处,并避免移动数据。在线程化编程中,默认模型是在进程中全局共享数据,并可由所有线程访问数据。有关线程的简介文章强调了通过将 OpenMP 应用至循环 (Fortran*) 或用于循环 (C) 来开始线程的便利性。当在两到四核上运行时,这些方法通常能够带来速度提升。这些方法会经常地扩展至 64 条线程或更多。但很多时候也不会进行此类扩展。在不进行扩展的一些情况中,主要是因为它们尊虚了良好的数据分解计划。这要求为良好的并行代码设计一个架构。

在代码调用堆栈的较高级别利用并行处理能力非常重要,而不是局限于由开发人员或软件工具确定的并行机会。当开发人员认识到可并行操作任务或数据时,依据埃坶德尔定律考虑这些问题: “在进行这点之前,我是否可以开始更高级别的并行运算? 如果我这样做,增大我代码的并行区域是否会带来更好的可扩展性?”

仔细考虑数据的放置以及必须通过消息共享什么数据。数据被置于最常使用的地方,然后根据需要发送至其他系统。对于以网格表示的应用程序,或具有特定分区的物理域,MPI 软件中常见的做法是围绕子网格或子域添加一行“虚拟”单元。虚拟单元用于存储 MPI 进程发送的数据的值,该进程会更新这些单元。通常虚拟单元不会用在线程化软件中,但是正如您沿着用于消息通过的分区最大程度减少边缘的长度一样,需要使用共享内存为线程最大程度减少分区的边缘。这样可最大程度减少对于线程锁(或关键部分)或关系到缓存所有权的缓存使用代价的需求。

大型多路系统共享全局内存地址空间,但通常具有非均匀的内存访问 (NUMA) 时间。和位于最靠近运行代码的插槽的内存条中的数据相比,最靠近另一插槽的内存条中的数据进行检索所需的时间更长,或者延迟更久。对于靠近的内存的访问延迟更短。

. Latency memory access, showing relative time to access data

图 1. 延迟内存访问,显示访问数据的相对时间。

如果一个线程分配并初始化数据,则通常会将该数据置于最靠近线程分配和初始化正在其上运行的插槽的内存条(图 1)。您可通过每个线程分配以及先引用其将主要使用的内存来改善性能。这通常足以确保内存最靠近线程在其上运行的插槽。一旦创建了线程,并且线程处于活动状态,操作系统通常会将线程留在相同插槽上。有时明确将线程绑定至核心以防止线程迁移较为有利。当数据具有特定模式时,实用的做法是将线程的亲缘性分配、绑定或设置到特定核心以匹配该模式。英特尔 OpenMP 运行时库(英特尔® Parallel Studio XE 2016 的一部分)提供了明确的映射属性,这些属性经过证明可用于英特尔® 至强融核™ 协处理器。

这些类型包括紧凑、分散和平衡。 

  • 紧凑属性将连续或相邻的线程分配至单核上的系统性多线程 (SMT),以将线程分配至其他核心。这在线程和连续编号(相邻)的线程共享数据的地方非常重要。
  • 分散亲缘性功能将线程分配至每个核心,然后再回到初始核心以在 SMT 上安排更多线程。
  • 平衡亲缘性功能以平衡的方式将连续或相邻 ID 的线程分配至相同的核心。如果期望根据英特尔 16.0 C++ 编译器文档优化线程亲缘性,在开始亲缘性时建议进行平衡。平衡的亲缘性设置仅可用于英特尔® 至强融核™ 产品系列。它并非一般 CPU 的有效选项。当利用了至强融核平台上的所有 SMT 时,平衡以及紧凑属性的效果相同。如果在至强融核平台上只利用了某些 SMT,紧凑方法将填补第一批内核上的所有 SMT 并在最后适当保留某些内核。

花些一些时间将线程数据靠近使用它的地方放置很重要。就和数据布局对于 MPI 程序很重要一样,这可能也对线程化软件很重要。  

在内存和数据布局方面,需要考虑两个较短的项目。这些是相对易于解决的部分,但是可能有很大影响。第一个是错误共享,第二个是数据对齐。和线程化软件相关的性能问题之一是错误共享。所运算的每个线程数据均为独立状态。它们之间没有共享,但是会共享包含两个数据点的高速缓存行。正因如此,将其称为错误共享或错误数据共享;虽然它们没有共享数据,但是性能行为表现和已经共享一样。

我们假设每个线程递增自身计数器,但是计数器处于一维阵列中。每个线程递增其自身的计数器。要递增其计数器,内核必须拥有高速缓存行。例如,插槽 0 上的线程 A 获得高速缓存行的所有权并递增 iCount[A] 。同时插槽 1 上的线程 A+1 递增 iCount[A+1],要实现这些操作,插槽 1 上的内核获得高速缓存行的所有权,并且线程 A+1 更新其值。由于高速缓存行中的值改变,使得插槽 0 上处理器的高速缓存行无效。在下次迭代时,插槽 0 中的处理器获得来自插槽 0 的高速缓存行的所有权,并修改 iCount[A] 中的值,该值继而让插槽 1 中的高速缓存行无效。当插槽 1 上的线程准备好写入时,循环重复。受到高速缓存行无效、重获控制以及同步至有性能影响的内存的影响,需要使用大量循环来保持缓存一致性。

对此的最佳解决方案并非让缓存无效。例如,在循环的入口,每个线程可读取其计数并将其存储在其堆栈上的本地变量中(读取不会让缓存无效)。当工作完成时,线程可将该本地值复制回最初的位置(参见图 2)。另一个备选方案是填补数据,使数据主要由其自身高速缓存行中的特定线程使用。

int iCount[nThreads] ;
      .
      .
      .
      for (some interval){
       //some work . . .
       iCount[myThreadId]++ // may result in false sharing
     }

Not invalidating the cache

int iCount[nThreads*16] ;// memory padding to avoid false sharing
      .
      .
      .
      for (some interval){
       //some work . . .
       iCount[myThreadId*16]++ //no false sharing, unused memory
     }

No false sharing, unused memory

int iCount[nThreads] ; // make temporary local copy

      .
      .
      .
      // every thread creates its own local variable local_count
      int local_Count = iCount[myThreadID] ;
      for (some interval){
       //some work . . .
       local_Count++ ; //no false sharing
     }
     iCount[myThreadId] = local_Count ; //preserve values
     // potential false sharing at the end,
     // but outside of inner work loop much improved
     // better just preserve local_Count for each thread

图 2.

相同的错误共享也可能在分配给相邻内存位置的标量上发生。这是如下面代码段所示的最后一种情况:

int data1, data2 ; // data1 and data2 may be placed in memory
                   //such that false sharing could occur
declspec(align(64)) int data3;  // data3 and data4 will be
declspec(align(64)) int data4;  // on separate cache lines,
                                // no false sharing

如果开发人员从一开始就设计并行处理,并最小化共享数据使用,通常要避免错误共享。 如果您的线程化软件没有很好地扩展,尽管有大量独立的工作在持续进行并且有少量障碍(互斥器、关键部分),检查错误共享也非常重要。

 

数据对齐

当以 SIMD 方式(AVX512、AVX、SSE4 等)运算的数据在高速缓存行边界上对齐时 , 软件性能最佳。数据访问未对齐的代价根据处理器系列而有所不同。英特尔® 至强融核™ 协处理器对于数据对齐尤其敏感。  在英特尔至强融核平台上,数据对齐至关重要。该差异在其他英特尔® 至强® 平台上不是很明显,但是当数据和高速缓存行边界对齐时,性能也能够得到显著的改进。因此建议软件开发人员务必在 64 字节边界上对齐数据。在 Linux* 和 Mac OS X* 上,这可通过英特尔编译器选项完成,没有源代码更改,只需使用以下命令行选项: /align:rec64byte。    

对于 C 语言中的动态分配的内存,malloc()可由 _mm_alloc(datasize,64)取代。当使用了 _mm_alloc()时,应当使用 _mm_free()取代 free()。专门针对数据对齐的完整文章位于以下网址:https://software.intel.com/zh-cn/articles/data-alignment-to-assist-vectorization。 

另外也请查看编译器文档。为了展现数据对齐的影响,我们创建了两个相同大小的矩阵,并且两个矩阵都运行该系列第 1 部分中使用的阻塞矩阵多层代码。 对于第一种情况,对齐了矩阵 A,对于第二种情况,特意将矩阵 A 偏移 24 个字节(3 的倍数),在将英特尔 16.0 编译器用于大小从 1200x1200 到 4000x4000 的矩阵的情况下,性能减少 56% 至 63%。在该系列的第 1 部分,我显示了一个表格,其中有使用了不同编译器的循环排序的性能,当一个矩阵偏移时使用英特尔编译器不再有任何性能优势。建议开发人员就数据对齐和可用的选项查看其编译器文档,从而当数据对齐时,编译器能够最有效地利用该信息。用于为偏离高速缓存行的的矩阵评估性能的代码嵌入在第 1 部分的代码中 - 该试验的代码位于:https://github.com/drmackay/samplematrixcode

编译器文档也有详细信息。

为了展现数据对齐的效果,我们创建了两个相同大小的矩阵,并且两个矩阵都运行第 1 部分中使用的阻塞矩阵多层代码。我们对齐了第一矩阵 A,第二个矩阵特意被偏移 24 个字节(3 的倍数),在将英特尔 16.0 编译器用于大小从 1200x1200 到 4000x4000 的矩阵的情况下,性能减少 56% 至 63%。

 

结构阵列对比阵列结构

处理器在内存连续流入时性能更佳。这在高速缓存行的每个元素移入 SIMD 寄存器时很有效, 前提是连续高速缓存行也以有序的方式载入了处理器预取。在结构阵列中,数据可采用类似以下形式的布局:

struct {
   uint r, g, b, w ; // a possible 2D color rgb pixel layout
} MyAoS[N] ;

在该布局中,连续陈列出 rgb 值。如果软件跨彩色平面处理数据,则整个结构可能被拉入缓存,但是每次仅使用一个值,例如 g。如果数据存储在阵列结构中,布局可能类似以下形式:

struct {
   uint r[N] ;
   uint g[N] ;
   uint b[N] ;
   uint w[N] ;
} MySoA ;

如果数据以阵列结构组织,并且软件运算所有 g 值(也可以是 r 或 b),当将高速缓存行引入高速缓存时,可能会在运算中使用整个高速缓存行。数据被更有效地载入 SIMD 寄存器,效率和性能显著改善。在许多情况下,软件开发人员用时间在实际操作中临时将数据移入要运算的阵列结构,然后根据需要将其复制回原位。在可行时,最好避免该额外的复制操作,因为这会占用执行时间。

英特尔 (Vectorization) Advisor 2016“内存访问模式”(MAP) 分析确定了具有连续(“单位步幅”)、非连续并且“非常规”的访问模式的循环:

“步幅分配”列提供关于每个模式在给定源循环中发生的频率的汇总统计数据。在上图中,条形图左边的三分之二为蓝色,表示连续访问模式,而右边的三分之一为红色,其表示非连续内存访问。对于具有纯 AoS 模式的代码,Advisor 也可自动获取特定“建议”以执行 AoS -> SoA 转换。 

在 Advisor MAP 中访问模式以及更为一般的内存位置分析得到简化,具体方法为额外提供内存“占用空间”指标,并将每个“步幅”(即访问模式)诊断映射至特定 C++ 或 Fortran* 对象/阵列名称。有关英特尔 Advisor 的详细信息,请访问

https://software.intel.com/zh-cn/get-started-with-advisor https://software.intel.com/zh-cn/intel-advisor-xe

阵列结构和结构阵列数据布局关系到许多图形程序以及 nbody(例如分子动态),或任何时间数据/属性(例如质量、位置、速度、电量),并且可与点或特定主体关联。通常,阵列结构更加有效,并且具有更高性能。

从英特尔编译器 2016 Update 1 开始,通过引入英特尔® SIMD 数据布局模板(英特尔® SDLT),AoS -> SoA 转换变得更简单。借助 SDLT,可以采用下面的方式方便地重新定义 AoS 容器:

SDLT_PRIMITIVE(Point3s, x, y, z)
sdlt::soa1d_container<Point3s> inputDataSet(count);  

从而可通过 SoA 方式访问 Point3s 实例。在此处阅读有关 SDLT 的更多信息。

几篇专论文章专门阐述了 AoS 对比 SoA 的主题。读者可通过下面的链接查看:

https://software.intel.com/zh-cn/articles/a-case-study-comparing-aos-arrays-of-structures-and-soa-structures-of-arrays-data-layouts

https://software.intel.com/zh-cn/articles/how-to-manipulate-data-structure-to-optimize-memory-use-on-32-bit-intel-architecture
http://stackoverflow.com/questions/17924705/structure-of-arrays-vs-array-of-structures-in-cuda

尽管大多数情况下,阵列结构匹配该模式并提供最佳性能,但也存在少数情况,其中数据参考和使用更紧密地匹配结构阵列布局,并且在该情况下结构阵列可提供更好的性能。

 

总结

下面总结了数据布局和性能方面要遵守的基本原则。将代码结构化以最小化数据移动。在数据处于寄存器或高速缓存中时重新使用它;这也有助于最小化数据移动。循环分块有助于最小化数据移动。对于具有 2D 或 3D 布局的软件尤其如此。考虑并行化布局,包括如何为并行计算分配任务和数据。良好的域分解做法有利于消息传送 (MPI) 和共享内存编程。阵列结构移动的数据通常比结构阵列更少,并且效果更佳。避免错误共享,并创建真正的本地变量或提供填充,从而使每个线程在不同的高速缓存行中引用值。最后,将数据对齐设置为在高速缓存行上开始。 

完整的代码可在以下网址下载: https://github.com/drmackay/samplematrixcode

如果您错过了第 1 部分,可在此处找到它。

您可应用这些技巧并了解代码性能如何得到改善。

基于 Windows® 10 的英特尔® 内存保护扩展:教程

$
0
0

简介

自第六代智能英特尔® 酷睿™ 处理器起,英特尔公司开始推出英特尔® 内存保护扩展(英特尔® MPX),它是一种针对指令集架构的全新扩展,旨在通过帮助抵御缓冲区溢出攻击,增强软件安全性。 在本文中,我们将介绍缓冲区溢出,并提供详细的步骤指导应用开发人员如何保护 Windows® 10 上的应用免受缓冲区溢出攻击。 英特尔 MPX 适用于传统桌面应用和 Universal Windows Platform* 应用。

前提条件

为运行本文提供的示例,您需要准备以下软硬件:

  • 采用第六代智能英特尔® 酷睿™ 处理器和 Microsoft Windows 10 操作系统(2015 年 11 月更新版或更高版本,首选 Windows 10 版本 1607)的计算机(台式机、笔记本电脑或其他外形的计算机)
  • 在 UEFI 中启用英特尔 MPX(如果该选项可用)
  • 安装正确的英特尔 MPX 驱动程序
  • Microsoft Visual Studio* 2015(更新 1 或更高版本 IDE;首选 Visual Studio 2015 更新 3)

缓冲区溢出

从本质上来说,C/C++ 代码更容易遭受缓冲区溢出。 例如,在以下代码中,main() 中的字符串操作函数 “strcpy” 将会导致程序遭受缓冲区溢出攻击的风险。

#include "stdafx.h"
#include <iostream>
#include <time.h>
#include <stdlib.h>

using namespace std;

void GenRandomUname(char* uname_string, const int uname_len)
{
	srand(time(NULL));
	for (int i = 0; i < uname_len; i++)
	{
		uname_string[i] = (rand() % ('9' - '0' + 1)) + '0';
	}
	uname_string[uname_len] = '\0';
}

int main(int argnum, char** args)
{
	char user_name[16];
	GenRandomUname(user_name, 15);
	cout << "random gentd user name: "<< user_name << endl;

	char config[10] = { '\0' };
	strcpy(config, args[1]);

	cout << "config mem addr: "<< &config << endl;
	cout << "user_name mem addr: "<< &user_name << endl;

	if (0 == strcmp("ROOT", user_name))
	{
		cout << "Buffer Overflow Attacked!"<< endl;
		cout << "Uname changed to: "<< user_name << endl;
	}
	else
	{
		cout << "Uname OK: "<< user_name << endl;
	}
	return 0;
}

为了提高准确性,如果我们以 C++ 控制台应用的形式编译并运行以上示例,将 CUSM_CFG 当作参数,那么该程序将正常运行,并且控制台将显示以下输出:

Figure 1 Buffer Overflow

但如果我们重新运行该程序,将 CUSTOM_CONFIGUREROOT 当作参数,输出将“出乎意料”,且控制台显示以下消息:

Figure 2 Buffer Overflow

这一简单示例说明了缓冲区溢出攻击的工作原理。 出现意外输出的原因是 strcpy 函数调用无法检查目标数组的联系。 尽管编译器通常为数组提供额外字节以达到内存对齐,但如果源数组足够长,还是会发生缓冲区溢出。 在这种情况下,程序的一部分运行时内存布局将如下所示(结果可能因编译器或编译选项的不同而有所差异):

Figure 3

英特尔内存保护扩展

借助英特尔 MPX,只需为 Visual Studio C++ 编译器添加编译选项 /d2MPX,就可避免缓冲区溢出这一安全问题。

Figure 4

借助英特尔 MPX 选项进行编译后,该程序将能够抵御缓冲区溢出攻击。 如果我们尝试运行重新进行编译,且包含 CUSTOM_CONFIGUREROOT 参数的程序,运行时例外情况将会增加,并导致程序退出。

Figure 5

我们来深入了解一下生成的汇编代码,看看英特尔 MPX 对该程序有何作用。 从结果来看,原始指令中插入了许多有关英特尔 MPX 的指令,以检测运行时的缓冲区溢出。

Figure 6

现在我们来详细了解一下有关英特尔 MXP 的指令:

bndmk: 在界限寄存器 (%bnd0) 中创建 LowerBound (LB) 和 UpperBound (UB),如上述代码快照所示。
bndmov: 从内存中获取(上下)界限信息并将其放在界限寄存器中。
bndcl: 根据上述代码快照中的参数 (%rax) 检查下界限。
bndcu: 根据上述代码快照中的参数 (%rax) 检查上界限。

故障排除

如果 MPX 无法正常工作。

  1. 复查 CPU、操作系统和 Visual Studio 2015 的版本。 将 PC 启动至 UEFI 设置,检查是否有英特尔 MPX 开关,如有必要打开开关。
  2. 确认英特尔 MPX 驱动程序安装正确并在 Windows* Device Manager 中正常运行。 Figure 7
  3. 检查编译的可执行文件是否包含有关英特尔 MPX 的指令。 插入一个断点,然后运行程序。 如果命中断点,右击鼠标,然后单击Go To Disassembly。 将显示一个新的窗口以供查看汇编代码。

Figure 8

结论

英特尔 MPX 是一款全新的硬件解决方案,有助于抵御缓冲区溢出攻击。 从应用开发人员的角度来看,相比于 AddressSanitizer (https://code.google.com/p/address-sanitizer/) 等软件解决方案,英特尔 MPX 拥有多项优势,包括:

  • 检测指针点在对象之外,但仍然指向有效内存。
  • 英特尔 MPX 更加灵活,可用于许多模块,但不影响其他模块。
  • 与传统代码的兼容性更高,适用于用英特尔 MPX 控制的代码。
  • 由于特殊的指令编码,因此仍然可以发布单一版本的二进制。 在不支持的硬件或操作系统上,与英特尔 MPX 相关的指令将以 NOP(无操作)的形式执行。

在第六代智能英特尔® 酷睿™ 处理器和 Windows 10 上,只需添加编译器选项,即可受益于面向应用的英特尔 MPX,从而帮助增强应用安全性,且丝毫不影响应用的后向兼容性。

相关文章

英特尔® 内存保护扩展启用指南:

https://software.intel.com/zh-cn/articles/intel-memory-protection-extensions-enabling-guide

参考资料

[1] AddressSanitizer: https://code.google.com/p/address-sanitizer/

关于作者

Fanjiang Pei 是软件和解决方案事业部 (SSG) 开发人员关系部门客户端计算移动支持团队的一名应用工程师, 负责为英特尔 MPX、英特尔® Software Guard 扩展等英特尔安全技术提供支持。

推出英特尔® Software Guard Extensions 教程系列

$
0
0

今天,我们将推出一个包含多个部分的教程系列,旨在帮助软件开发人员了解如何将英特尔® Software Guard Extensions(英特尔® SGX)集成至他们的应用中。 本系列概述了构建英特尔 SGX 应用时软件开发周期的各个阶段,从应用设计到开发、测试、封装和部署。 尽管单独的代码示例和单篇文章非常重要,深入了解在单个应用中支持英特尔 SGX 可帮助开发人员全面掌握如何在实际应用中采用该技术。

本教程将包含多个部分,目前计划发表 12 篇文章(准确数字尚未确定),每篇文章阐述一个特定主题。 虽然准确的时间表尚未制定,但系列的每个部分每 2-3 周* 应会发表一次,内容涉及以下阶段:

  1. 概念和设计
  2. 应用开发和英特尔 SGX 集成
  3. 验证和测试
  4. 封装和部署
  5. 处理

本系列相关部分将附带源代码,源代码将通过英特尔示例源代码许可分发。 然而,源代码在数周之内还不会发布。 教程的第一部分将介绍英特尔 SGX 应用开发初步阶段的基本要素。

目标

在本系列结束时,开发人员将知道如何:

  • 识别应用的机密
  • 应用安全区 (enclave) 设计原则
  • 在安全区使用值得信赖的库
  • 在应用中构建双代码路径支持(为没有英特尔 SGX 功能的平台提供传统支持)
  • 使用英特尔 SGX 调试程序。
  • 创建英特尔 SGX 应用安装程序包

示例应用

在本系列发表过程中,我们将开发一款基础密码管理程序。 最终应用产品并非要追求商用可行性,而是提供足以满足有效安全实践的功能。 本应用虽然简单,但足以满足本教程的例示要求。

您需要的组件

源代码发布时希望使用它的开发人员将需要使用下列组件:

硬件要求

硬件硬性要求备注
具备英特尔® 安全密钥技术的英特尔® 处理器是                            密码管理程序将广泛使用英特尔安全密钥技术提供的数字随机数生成器。 访问 http://ark.intel.com,查找具有英特尔安全密钥技术支持的具体处理器型号。
带有支持英特尔® Software Guard Extensions(英特尔® SGX)的 BIOS 的第六代智能英特尔® 酷睿™ 处理器否                           为充分利用本教程,您需要支持英特尔 SGX 的处理器,但应用开发可在较小的系统上实施,且英特尔 SGX 应用可在英特尔 SDK 附带的仿真器中运行。

 

软件要求

这些软件要求基于当前公开发布的英特尔 SGX 软件开发人员套件 (SDK)。 随着 SDK 版本的更新换代,要求可能有所变化。

更新于 2016 年 7 月 11 日: SDK 要求已更新至 1.6。 这也迫使 Microsoft Visual Studio* 版本更新至 2013。

软件硬性
要求
备注
英特尔® Software Guard Extensions(英特尔® SGX)SDK v1.6是                 需用于开发英特尔 SGX 应用。
Microsoft Visual Studio* 2013 专业版是                 需用于 SDK。 每版 SDK 都与特定版本的 Visual Studio 相关联,以支持向导、开发人员工具和各种集成组件。
面向 Windows* 的英特尔® Parallel Studio XE 2013 专业版 否                 

推荐使用,但并非英特尔 SGX 开发工作的必备工具。

敬请关注

本系列概述了构建英特尔 SGX 应用时软件开发周期的各个阶段,从应用设计到开发、测试、封装和部署。 本教程将阐述相关概念和设计,应用开发和英特尔 SGX 集成、验证、测试、封装和部署以及处理。

我们为即将发布本系列感到激动不已,期待您加入我们!

入门

本系列第一部分,英特尔® Software Guard Extensions 教程系列: 第一部分,英特尔® SGX 基础,概述了该技术,并为教程的后续内容奠定了基础。

本系列第二部分,英特尔® Software Guard Extensions 教程系列: 第二部分,应用设计描述了我们将开发的应用(简单的密码管理程序)的高级规格。

本系列第三部分,英特尔® Software Guard Extensions 教程系列: 第三部分,为英特尔® SGX 而设计讨论如何为英特尔 SGX 设计应用。 

本系列第四部分,英特尔® Software Guard Extensions 教程系列:第四部分,安全区设计,开始在安全区进行开发工作。

本系列第五部分, 英特尔® Software Guard Extensions 教程系列:第五部分,安全区开发,完成第一版安全区。

本系列第六部分,英特尔® Software Guard Extensions 教程系列:第六部分,双核路径,确保我们的应用能够在具有和不具有英特尔 SGX 支持的主机上运行。

* 注: 由于人员减少,发布周期可能会延长至平均 3-4 周。 我们仍然重视本教程系列的发表,但不希望为迎合我们最初制定的较为紧凑的时间表而降低文章的质量。 由此造成任何不便,我们深感抱歉。


英特尔® Software Guard Extensions 教程系列:第一部分,英特尔® SGX 基础

$
0
0

英特尔® Software Guard Extensions(英特尔® SGX)教程系列的第一部分仅对该技术进行了简单概述。 更多详情,请参阅英特尔 Software Guard Extensions SDK提供的相关资料。 在文章介绍英特尔® Software Guard Extensions 教程系列中查找本系列所有教程列表。

了解英特尔® Software Guard Extensions 技术

软件应用通常需涉及诸如密码、账号、财务信息、加密秘钥和健康档案等私人信息。 这些敏感数据只能由指定接收人访问。 按英特尔 SGX 术语来讲,这些隐私信息被称为应用机密。

操作系统的任务对计算机系统实施安全策略,以避免这些机密信息无意间暴露给其他用户和应用。 操作系统会阻止用户访问其他用户的文件(除非访问已获得明确许可),阻止应用访问其它应用的内存,阻止未授权用户访问操作系统资源(除非通过受到严格控制的界面进行访问)。 应用通常还会采用数据加密等其它安全保护措施,以确保发送给存储器或者通过网络连接而发送的数据不会被第三方访问到——即使是在操作系统和硬件发盗用的情况下。

尽管有这些措施提供保护,但大部分计算机系统仍然面临着一项重大安全隐患:虽然有很多安保措施可保护应用免受其它应用入侵,保护操作系统免受未授权用户访问,但是几乎没有一种措施可保护应用免受拥有更高权限的处理器的入侵,包括操作系统本身。 获取管理权限的恶意软件可不受限制地访问所有系统资源以及运行在系统上的所有应用。 复杂的恶意软件可以锁定应用的保护方案为目标进行攻击,提取加密秘钥,甚至直接从内存提取机密数据。

为对这些机密信息提供高级别的保护,同时抵御恶意软件的攻击,英特尔设计了英特尔 SGX。 英特尔 SGX 是一套 CPU 指令,可支持应用创建安全区: 应用地址空间中受保护的区域,它可确保数据的机密性和完整性——即便有获取权限的恶意软件存在。 安全区代码可通过专用指令启用,并被构建和加载成 Windows* 动态链接库 (DLL) 文件。

英特尔 SGX 可减少应用的攻击面。 图 1 显示了借助英特尔 SGX 安全区和不借助英特尔 SGX 安全区时,攻击面的显著差异。

Attack-surface areas with and without Intel® Software Guard Extensions enclaves.

图 1借助和不借助英特尔® Software Guard Extensions 安全区时的攻击面。

英特尔 Software Guard Extensions 技术如何确保数据安全

英特尔 SGX 可针对已知的硬件和软件攻击提供以下保护措施:

  • 安全区内存不可从安全区外读写,无论当前的权限是何种级别,CPU 处于何种模式。
  • 产品安全区不能通过软件或硬件调试器来调试。 (可创建具有以下调试属性的安全区:该调试属性支持专用调试器,即英特尔 SGX 调试器像标准调试器那样对其内容进行查看。 此措施旨在为软件开发周期提供辅助)。
  • 安全区环境不能通过传统函数调用、转移、注册操作或堆栈操作进入。 调用安全区函数的唯一途径是完成可执行多道保护验证程序的新指令。
  • 安全区内存采用具有回放保护功能的行业标准加密算法进行加密。 访问内存或将 DRAM 模块连接至另一系统只会产生加密数据(见图 2)。
  • 内存加密秘钥会随着电源周期(例如,启动时或者从睡眠和休眠状态进行恢复时)随机更改。 该秘钥存储在 CPU 中且不可访问。
  • 安全区中的隔离数据只能通过共享安全区的代码访问。

受保护的内存在大小上存在硬限值,该限值由系统 BIOS 设定,通常为 64 MB 和 128 MB。 有些系统提供商可能会将此限值制定成其 BIOS 设置内的可配置选项。 内存中可同时保留 5 到 20 个安全区,这取决于每个安全区的大小。

How Intel® Software Guard Extensions helps secure enclave data in protected applications.

图 2英特尔® Software Guard Extensions 如何确保受保护应用中安全区数据的安全。

设计注意事项

包含英特尔 SGX 技术的应用设计要求将应用分成两个部分(见图 3):

  • 可信部分。它指的是安全区。 可信代码中的代码是访问应用机密的代码。 一款应用可以拥有一个以上可信部分/安全区。
  • 不可信部分。它包括应用的剩余部分及其所有模块。 需要指出的是,从安全区的角度来看,操作系统和虚拟机显示器都被看做不可信部分。

可信部分应尽量保持最小,仅限于需要最高等级保护的数据以及必须直接作用于其上的操作。 具有复杂界面的大型安全区不仅仅会消耗更多受保护内存:还会产生更大攻击面。

安全区还应使可信-不可信部分交互程度保持最低。 虽然安全区可离开受保护内存区域,在不可信部分(通过专用指令)调用函数,但对依赖性进行限制将会针对攻击对安全区进行加固。

Intel® Software Guard Extensions application execution flow.

图 3英特尔® Software Guard Extensions 应用执行流程。

认证

在英特尔 SGX 架构中,认证指的是以下流程,即证明在平台上建立了特定安全区。 有两种认证机制:

  • 本地认证 适用于同一平台上两个安全区进行相互认证时。
  • 远程认证 适用于某一安全区获取远程提供商的信任时。

本地认证

当应用拥有一个以上需要相互协作才能完成任务的安全区时,或者两款应用必须在安全区之间进行数据通信时,本地认证非常有用。 每个安全区都须对另一安全区进行验证,以确认双方都是可信安全区。 一旦完成认证,它们就会建立受保护会话,采用 ECDH Key Exchange 共享会话秘钥。 该会话秘钥可用于对必须在这两个安全区之间进行共享的数据进行加密。

因为一个安全区不能对另一个安全区的受保护内存空间进行访问——即使它们运行在同一应用中,所以必须将所有指针解除引用至其值和副本,且必须将完整的数据集从一个安全区封送至另一安全区。

远程认证

借助远程认证,英特尔 SGX 软件和平台硬件的组合可用于生成评价,评价会被发送至第三方服务器以建立信任。 该软件包括应用安全区,以及评价安全区 (QE) 和配置安全区 (PvE),后两者皆由英特尔提供。 该认证硬件是英特尔 SGX 所支持的 CPU。 将该软件信息摘要与来自该硬件的平台唯一非对称秘钥相组合以生成评价,再通过已认证渠道将评价发送至远程服务器。 如果远程服务器确认安全区得到了正确实例化且运行在真正支持英特尔 SGX 的处理器上,远程服务器就会立即信任该安全区并选择通过已认证渠道向其提供机密信息。

密封数据

密封数据是对数据进行加密,以便可在不泄露其内容的前提下将数据写至不可信内存或对其进行存储。 稍后,该安全区可将该数据读回并进行解封(解密)。 加密秘钥由内部按需推导,不会暴露给安全区。

有两种数据密封方法:

  • 安全区标识。本方法可生成一个所述安全区所独有的秘钥。
  • 密封标识。本方法可生成一个基于安全区密封授权方标识的秘钥。 相同签名授权方的多个安全区可推导出相同秘钥。

密封至安全区标识

密封至安全区标识时,秘钥对于密封数据的具体安全区是独一无二的,该安全区会影响其签名的任何更改都会产生新秘钥。 借助该方法,使用一版安全区密封的数据不可由其他版本的安全区访问,因此该方法的一个副作用是,密封的数据不可迁移至较新版本的应用及其安全区中。 它专为密封的旧数据不应被新版应用使用的应用而设计。

密封至密封标识

密封至密封标识时,来自同一授权方的多个安全区可透明地对彼此的数据进行密封和解封。 这样,来自一版安全区的数据可迁移至另一版,或在同一软件厂商的多个应用中进行共享。

如果需要防止旧版软件和安全区访问较新应用版本密封的数据,授权方可在对安全区进行签名时写下软件版本编号 (SVN)。 比指定 SVN 更旧的安全区版本将不可推导出密封密钥,因此不可对数据进行解封。

我们将如何在本教程中使用英特尔 Software Guard Extensions 技术

我们描述了英特尔 SGX 的三大要素:安全区、认证和密封。 对于本教程,我们将重点实施安全区,因为安全区是英特尔 SGX 的核心。 如果不先建立安全区,您不可进行认证或密封。 这还有助于确保教程保持可管理大小。

即将推出

本教程系列第二部分,英特尔® Software Guard Extensions 教程系列: 第二部分,应用设计,将聚焦我们将为英特尔 SGX 构建和支持的密码管理程序应用。 我们将谈及设计要求、限制和用户界面。 敬请关注!

 在文章介绍英特尔® Software Guard Extensions 教程系列中查找本系列所有教程列表。

 

英特尔® Software Guard Extensions 教程系列:第二部分,应用设计

$
0
0

英特尔® Software Guard Extensions(英特尔® SGX)教程系列的第二部分介绍了我们将开发的应用(简单的密码管理程序)的高级规格。 全新的设计工作支持我们从一开始专为英特尔 SGX 而构建。 这意味着,除说明应用要求,我们还将分析英特尔 SGX 设计决策和整个应用架构会如何相互影响。

阅读本系列第一部分教程,或介绍英特尔® Software Guard Extensions 教程系列的文章中查找所有已发表教程的列表。

密码管理程序一览

多数人可能熟悉密码管理程序及其功能,但在详细了解应用设计之前有必要回顾其基本信息。

密码管理程序的主要目标是:

  • 减少最终用户需要记住的密码数量。
  • 帮助最终用户创建安全性更高的密码 —— 相比其通常选择的密码。
  • 确保针对每个账户使用不同密码具有实用性。

密码管理日益成为互联网用户的问题。多年来,多项研究在努力对这一问题进行量化。 Microsoft 于 2007 年发布的一项调查—— 距离本文写作时接近 10 年 —— 估计,普通人拥有 25 个需要密码的账户。 2014 年,Dashlane 估计其美国用户平均拥有 130 个账户,而其全球用户所拥有账户的平均数量为 90。 问题并未就此结束:用户都不善于挑选“好”密码(众所周知),经常在多个站点重复使用相同密码,这已经导致了一些备受关注的攻击事件。 这些问题基本可归为两类:黑客工具难以猜透的密码通常也不易于用户记住,以及密码数量的增多让问题更加复杂,因为用户需要花费更多心思记住哪个密码与哪个账户相关联。

借助密码管理程序,您只需记住一个安全性极高的密码,便可访问密码数据库。 通过了密码管理程序的身份验证之后,您便可查找您存储的任何密码,并根据需要将其复制粘贴在身份验证字段中。 当然,密码管理程序的主要弱点恰恰在于密码数据库本身:由于包含所有的用户密码,因此它对于攻击者极具诱惑力。 因为这个原因,密码数据库需使用安全性较高的加密技术进行加密,这样解密其中的数据就需要用户的主密码。

我们制作本教程的目标是帮助构建一个简单的密码管理程序 —— 它能够在遵守良好安全实践的情况下提供与商用产品相同的核心功能,并将其用作针对英特尔 SGX 进行设计的学习工具。 教程密码管理程序(我们称之为“支持英特尔® Software Guard Extensions 的教程密码管理程序”,名字虽然有点长,但具有描述性)并非要用作商用产品,因此不会、也没必要包含所有保护功能。

基本的应用要求

一些基本的应用要求将可帮助缩小应用的范围,以便我们专注于英特尔 SGX 集成,而非应用设计和开发的细节。 再次强调,我们的目标不是创建商用产品,支持英特尔 SGX 的教程密码管理程序无需在多个操作系统或所有可能的 CPU 架构上运行。 我们只需要一个合理的起点。

为此,我们的基本应用要求是:

第一个要求可能有点奇怪,因为本教程系列旨在说明英特尔 SGX 应用开发,但实际的应用需要考虑传统的安装基础。 对于一些应用而言,将执行范围限制于支持英特尔 SGX 的平台的范围内可能比较合适,但对于教程密码管理程序,我们将放宽要求。 支持英特尔 SGX 的平台将支持强化的执行环境,但不支持的平台仍可正常运行。 这种使用方法适用于密码管理程序。使用密码管理程序时,用户可能需要将其密码数据库与其他较旧的系统进行同步。 它还是实施双代码路径的学习机会。

第二个要求是访问非英特尔 SGX 代码路径中的某些密码算法以及我们需要的一些库。 64 位要求通过确保原生 64 位类型的访问简化了应用开发,而且提升了某些针对 64 位代码而优化的密码算法的性能。

第三个要求是访问非英特尔 SGX 代码路径中的 RDRAND 指令。 这可极大简化随机数生成,确保对高质量熵源的访问。 支持 RDSEED 指令的系统也可利用它。 (有关 RDRAND 和 RDSEED 指令的信息,请访问英特尔® 数字随机数生成器软件实施指南)。

第四个要求是尽量减少开发人员(和最终用户)需要的软件数量。 无需下载和安装第三方库、框架、应用或实用程序。 然而,该要求有一个不利的副作用:没有第三方框架,我们只可使用四个选项创建用户界面。 这些选项包括:

  • Win32 API
  • Microsoft 基础类 (MFC)
  • Windows Presentation Foundation (WPF)
  • Windows Forms

前两个选项使用原生/非托管代码实施,后两个需要使用 .NET*。

用户界面框架

对于教程密码管理程序,我们在 C# 中使用 Windows Presentation Foundation 开发 GUI。 该设计决策会影响我们的下列要求:

为何使用 WPF? 主要是因为它可简化 UI 设计,并会带来我们实际上需要的复杂性。 具体而言,通过使用 .NET 框架,我们可以讨论将托管代码、特别是高级语言与安全区代码进行混合。 不过,请注意,WPF 和 Windows Forms 都可选择,两种环境都能有效运行。

您可能记得,安全区必须使用原生 C 或 C++ 代码编写,与安全区交互的桥函数必须为原生 C(而非 C++)函数。 尽管 Win32 API 和 MFC 支持使用完全原生的 C/C++ 代码开发密码管理程序,但这两种方法不会对希望学习英特尔 SGX 应用开发的人员造成负担。 借助基于托管代码的 GUI,我们不仅可获得集成设计工具的优势,而且可讨论一些可为英特尔 SGX 应用开发人员带来潜在价值的功能。 简言之,您并非要学习 MFC 或原始 Win32,但可能希望了解如何将 .NET 粘合至安全区。

为将托管和非托管代码连接起来,我们将使用 C++/CLI(针对通用语言基础设施而修改的 C++)。 它可大幅简化数据封送,而且因其便捷性和易用性被许多开发人员形容为 IJW(“确实有效”)。

图 1原生和 C# 英特尔® Software Guard Extensions 应用的最小组件结构。

图 1 所示为英特尔 SGX 应用从原生代码迁移至 C# 时其最小组件结构受到的影响。 在完全原生的应用中,应用层可直接与安全区 DLL 交互,因为安全区桥函数可纳入应用的可执行文件。 然而,在混合模式应用中,安全区桥函数需要与托管代码块相隔离,因为它们需要是完全原生的代码。 另一方面,C# 应用不可直接与桥函数交互,在 C++/CLI 中这意味着创建另一个媒介:在托管 C# 应用和原生安全区桥 DLL 之间封送数据的 DLL。

密码库要求

密码数据库或我们所称的密码库是密码管理程序的核心。 这种加密文件将保存最终用户的账户信息和密码。 我们教程应用的基本要求是:

密码库应可移植的要求意味着我们应在将密码库文件复制到另一台计算机后仍可访问其内容,无论它们是否支持英特尔 SGX。 换言之,用户体验应该一致:密码管理程序应该无缝运行(当然,前提是系统满足基础硬件和操作系统要求)。

加密静态密码库意味着密码库文件在未使用时应被加密。 密码库至少须在磁盘上进行加密(若没有可移植性要求,我们有望使用英特尔 SGX 的封闭特性满足加密要求),且若无必要,在内存中不应处于未加密状态。

经过验证的加密可确保加密的密码库在加密之后未被修改。 它还可帮助我们方便地验证用户的密码:如果解密密钥不正确,在对验证标签进行验证时解密会失败。 这样,我们无需检测解密数据是否正确。

密码

从多方面考虑,任何账户信息都是敏感信息,它可准确告知攻击者将攻击目标锁定哪些登录名和站点,不过密码无疑是密码库最重要的要素。 确定攻击目标账户后就可发起攻击。 为此,我们将介绍有关密码库中所存储密码的更多要求:

嵌入加密功能。 每个用户账户的密码存储在密码库时经过了加密,整个密码库在写入磁盘时进行了加密。 该方法支持我们在密码库解密后避免密码被曝光。 对密码库进行全面解密可帮助用户浏览其账户详情,但以这种方式清晰显示所有密码文本则并不合适。

账户密码只在用户请求查看它时被解密。 这可防止密码在内存中和用户显示屏上被曝光。

密码算法

确定加密需求后,我们需选择特定的密码算法。根据我们的现有应用要求,相关选项受到一些严格限制。 该教程密码管理程序必须在英特尔 SGX 和非英特尔 SGX 平台上提供无缝用户体验,不可依赖第三方库。 这意味着我们需要选择一种算法以及支持的密钥和验证标签大小,这对于 Windows CNG API 和英特尔 SGX 可信密码库都一样。 实际上,我们只有一个选项: 高级加密标准-Galois 计算器模式 (AES-GCM),带有 128 位密钥。 这可以说是该应用中可使用的最佳加密模式,尤其是因为 128 位 GCM 的有效验证标签强度小于 128 位,但它足以我们的目的。 请记住:我们的目标不是创建一款商用产品,而是提供一款实用的英特尔 SGX 开发学习工具。

选择 GCM 时,我们同时做出了一些其他的设计决策,包括 IV 长度(12 字节最适合该算法)和验证标签。

加密密钥和用户身份验证

选择加密方法后,我们可将关注点转向加密密钥和用户身份验证。 用户如何在密码管理程序中进行身份验证并解锁密码库?

简单的方法是直接使用密钥推导函数 (KDF) 从用户密码中推导加密密钥。 不过,这个简单的方法虽然有效,却存在一个缺点:如果用户修改了其密码,加密密钥也会随之改变。 因此,我们将采用更为常见的做法:对加密密钥进行加密。

在该方法中,主加密密钥使用高质量熵源随机生成,绝不会变化。 用户密码用于推导辅助加密密钥,后者用户对主密钥进行加密。 这种方法具备一些重要优势:

  • 当用户密码改变时,数据无需再次加密
  • 加密密钥绝不会改变,因此理论上它能以十六进制表示法进行记录,锁定在物理安全的位置。 即使当用户忘记其密码时,数据仍可解密。 由于密钥绝不会改变,因此它只需记录一次。
  • 理论上,这种方法满足用户访问数据的需求绰绰有余。 每位用户可使用自己的密码加密主密钥的副本。

所有这些措施并非都需要用于教程密码管理程序或与其相关,但构成了一种有效的安全实践。

主密钥被称为密码库密钥,通过用户密码推导的辅助密钥被称为总密钥。 用户可输入密码进行身份验证,密码管理程序可从中推导出总密钥。 如果总密钥成功解密密码库密钥,用户则通过了身份验证,密码库被解密。 如果密码不正确,密码库密钥解密失败,因此密码库不可解密。

最后一个要求,即根据 SHA-256 构建 KDF,源于我们发现的一项限制,即哈希算法同时支持 Windows CNG API 和英特尔 SGX 可信密码库。

帐户详情

最后一个高级要求与在密码库中实际存储什么内容相关。 对于该教程,我们将力求简单。 图 2 所示为主 UI 屏幕的早期模型。

图 2:教程密码管理程序主屏幕的早期模型

最后一个要求与简化代码相关。 通过固定密码库中存储账户的数量,我们可更轻松地确定密码库大小的上限。 这在我们开始设计安全区时至关重要。 当然,实际的密码管理程序不具备这一优势,但它可满足本教程的目的。

即将推出

本教程第三部分,我们将详细探讨如何针对英特尔 SGX 设计教程密码管理程序。 我们将确定机密(应用的这一部分应包含在安全区内)、安全区如何与核心应用交互以及安全区如何影响对象模型。 敬请关注!

阅读本系列第一部分教程英特尔® Software Guard Extensions 教程系列: 第一部分,英特尔® SGX 基础,或在介绍英特尔® Software Guard Extensions 教程系列文章中查找所有已发表教程的列表。

 

将英特尔® 实感™ SDK 面部扫描映射为3D 头部模型

$
0
0

下载示例代码

下载 PDF [1.08 MB]

本代码示例支持用户使用前置英特尔® 实感™ 摄像头扫描面部,将其投影至可定制头部网格,并为其添加后处理效果。 它被称为将英特尔® 实感™ 摄像头扫描运用于 3D 网格,是对之前的代码示例所进行的一次扩展。 该示例添加了多项特性和技巧,可显著提高最终头部结果的质量,并提供一系列后期映射效果。

本代码示例添加了以下特性:

  • 参数化头部。用户可使用滑块塑造头部外形。 例如,用户可改变头部、耳朵、下巴等部位的宽度。
  • 标准化网格拓扑。映射算法可用于支持标准化拓扑的头部网格,并在映射完成后保留各顶点的上下文, 从而为对头部进行动画制作、改进混合效果和后期映射效果奠定基础。
  • 色彩和外形后期处理。映射阶段完成后,可采用形态目标和其他色彩混合,对最终结果进行定制。
  • 头发。新创建了许多适合基础头部模型的头发模型。 使用自定义算法可调整头发几何体,使其符合用户所选的头部外形。

该示例的最终结果适用于各种应用。 开发人员将这些工具运用到应用程序之中,可支持其用户扫描面部并对面部特征进行定制。 它还可用于生成游戏角色内容。 也可添加其他形态目标,以扩大所创建的角色范围。


图 1: 从左至右: (a) 英特尔® 实感™ SDK 扫描模块所返回的面部网格,(b) 映射至头部模型的扫描面部,(c) 通过 ogre 形态目标调整后的头部模型几何体,以及 (d) 经过调整并着色后的头部。

本文将详细介绍如何使用代码示例的特性创建扫描、修改面部几何体并添加效果。 以下视频将为您详细介绍这些步骤。

使用英特尔® 实感™ 创建可定制头部网格

使用示例

本示例包含两个可执行文件: ReleaseDX.exe(支持面部扫描)和 ReleaseDX_NORS.exe(仅支持映射之前扫描的面部)。 这两个可执行文件均要求 64 位 Visual Studio* 2013 运行时(点击此处下载)。 ReleaseDX.exe 要求安装英特尔® 实感™ SDK 运行时 2016 R1 (8.0.24.6528),可点击此处此处下载。

运行 ReleaseDX.exe 即可开始面部扫描流程。 满足头部定位提示后,按下 Begin Scan 按钮,缓慢转动头部,然后点击 End Scan。 为获得最佳效果,请摘掉帽子或眼镜,将头发拨至耳后,并在光线好的区域进行扫描。

扫描完成或加载现有扫描后,可使用 UI 中的以下类型定制结果:

  • 面部扫描。调整偏航、倾斜、旋转和 z 位移,以调整面部扫描的方向和位置。
  • 头部塑形。使用提供的滑块改变头部模型的形状, 使创建的头部与扫描人物的头部相符。 外形调整可在后期处理阶段进行。
  • 混合。使用色彩控制选择两种最适合肤色的颜色。 第一种颜色为基础色,第二种用于调整色调。
  • 后期头部塑形。映射流程结束后,可对头部外形进行调整。 在本阶段中,您可以改变体质指数、将自己变成 orge,放大双眼等等。
  • 后期混合。映射完成后,选择任意色彩效果并将其运用于整个头部。 这些色彩效果不会影响嘴唇或眼睛。 可以使用这些效果调整头部的色调/饱和度/亮度。

调试类包含多种选项,用于显示面部映射管道的不同部分。

本示例支持将最终头部和头发导出至 .OBJ 文件,以便加载至其他应用。


图 2:代码示例截图,显示用于定制头部的一部分选项。

艺术资产

下文将简要介绍本代码示例所使用以及本文所引用的艺术资产。 所有纹理资产均面向基础头部模型的 UV 坐标创作。 所有纹理资产均可用于管道混合阶段(位移控制图除外,它用于几何体阶段)。

  • 基础头部网格。 供扫描面部网格使用的基础头部网格。
  • 头部特征数据。基础头部网格的特征,符合英特尔实感 SDK 提供给各面部扫描的特征。
  • 位移控制图。控制基础头部网格的哪些顶点因生成的面部位移图而发生偏移。
  • 色彩控制图。 控制面部色彩与头部色彩的混合。
  • 特征图。为最后生成的漫反射图提供头部纹理的灰度图。
  • 皮肤图在后期混合阶段中用于防止色彩效果影响眼睛和嘴唇。
  • 色彩传递图。控制用户选择的两种色彩之间的混合。
  • 特征网格。用于将头部顶点移至与面部图投影中的相应位置。
  • 头部形态目标。形态目标集合,面部映射至头部之前和之后均可用于基础头部外形。
  • 头发模型。可供用户选择的头发模型集合。

面部映射管道

面部映射管道可分成四个阶段。

  1. 位移和色彩图 渲染面部扫描几何体,以创建在后续阶段中映射至头部的位移和色彩图。
  2. 几何体。 本阶段主要调整头部网格的位置和法线。 可使用位移图的值通过头部模型凸出面部扫描的外形。
  3. 混合。 将头部和面部色彩通道混合在一起,输出可映射至头部模型 UV 的单一色彩图。
  4. 头发几何体。 重新映射头发顶点位置,以纳入几何体阶段中对头部外形所做的修改。

面部位移和色彩图阶段

在该阶段中,使用正交投影矩阵渲染扫描面部网格,以创建深度图和色彩图。 面部扫描特征位置可投影到这些贴图上,并在几何体阶段中用于投影至头部网格。


图 3: 位移图阶段创建的色彩和位移图。 黄色圆点表示英特尔® 实感™ SDK 提供并投影至 2D 贴图空间的特征。

几何体阶段。

该阶段主要调整基础头部网格外形,并将位移图印在头上。 进行上述操作时必须维持各面部顶点的上下文;头部网格中鼻尖的顶点将会移动,以便面部置换顶点时,该顶点仍在鼻尖上。 维持这种上下文有利于控制与基础头部网格相关的信息,从而保持最终的头部网格。

下文将详细介绍有关本流程的信息。 这些高级步骤包括:

  1. 将基础头部网格投射至特征网格。 这一步骤将关联头部顶点和特征网格上的单个三角形、重心坐标以及沿该三角形法线的偏移。
  2. 使用形态目标调整头部顶点。
  3. 计算映射矩阵以便将 2D 位移/色彩图投影至头部网格,并使用该矩阵计算各顶点的纹理坐标。
  4. 使用面部特征数据调整特征网格。
  5. 使用步骤 1 中的投影数据根据调整后的特征网格移动头部顶点。
  6. 使用位移图的值沿着 z 轴置换头部顶点。
  7. 运用后期处理形态目标

构建参数头部

各种头部空间均适用于形态调整系统。 每种形状空间负责雕刻头部的一小部分(例如,其中一个目标仅控制下巴宽度, 而另外一项体质指数 (BMI) 几乎可以改变整个头部外形。) 美术师创作的头部外形都包含相同数量的顶点,必须匹配基础头部外形中相应的顶点数量。


图 4:借助形态目标实施的参数头部。

通过编译各顶点的三角点位置,可将美术师创作的头部外形转换成形态目标。 三角点位置表示基础头部网格的顶点与其相关目标外形顶点之间的差异或变化。 添加各顶点的三角点位置,然后乘以标量值,即可运用形态目标。 标量 0 不会产生影响,标量 1 可运用精确的目标外形。 大于 1 的标量可放大目标外形,负数标量可反转目标外形,产生极其有趣的效果。

该示例展示了部分复合形态目标,支持单个滑块作用于多个形态目标。 部分滑块作用于 0-1 的权重,其他滑块支持该范围之外的数值。

无论是面部映射至头部网格之前还是之后,都可使用这些形态调整技巧。

创建位移和色彩图投影矩阵

位移/色彩图投影将头部模型顶点位置正交投影至之前创建的位移和色彩图的 UV 坐标。 关于该流程的更多详情请参阅之前的相关示例文本。

面部调整几何体

之前的示例要求基础头部网格拥有适用于面部区域的相对密集的顶点网格。 它置换这些顶点以匹配扫描网格的外形。 但它不按照功能识别这些顶点(例如嘴角的顶点)。 在本示例版本中,基础头部网格的密集程度较低,顶点契合面部扫描,从而保留了顶点功能。 例如,眼周围的顶点可随扫描眼睛的位置移动。

英特尔实感 SDK 在面部扫描过程中提供大量的已知面部特征。 创作的基础头部网格提供匹配的特征。 特征网格可用于面部之间的映射。 每个重要特征对应一个顶点,从而形成相对较大的三角形以便对面部区域进行划分。 我们确定在何处将基础头部网格顶点投影至特征网格,以计算其在扫描面部的相应位置。

在这一过程中,头部顶点投影至特征网格,特征网格根据基础头部网格和面部特征数据进行调整,顶点位置重新投影至头部。 最后,将位移图应用于各面部区域顶点的 z 组件,从而突出扫描面部的外形。 位移控制图可确保仅移动面部顶点,以及受影响和未受影响顶点之间的平稳过渡。

将顶点投影至特征网格与头发调整阶段的投影类似。


图 5:包含明显特征的面部色彩图(左)和包含明显头部特征的基础头部网格(右)。


图 6: 与头部网格(左)顶部重叠的特征网格。 请注意内层顶点均排列于头部特征的上方。 根据面部特征信息(右)调整的特征网格。


图 7:重新投影并置换顶点位置(左)后包含特征网格的头部重叠。 注意嘴唇如何上移。 重新投影、置换和使用面部色彩图(右)之后的头部。

混合阶段

混合阶段负责混合面部扫描色彩数据和美术师创作的头部纹理,从而生成面向头部 UV 坐标创建的最终漫反射纹理。

色彩传递图用于计算用户选择的两种头部色彩之间的插值。 最终颜色乘以头部细节图,即可生成最终的头部颜色。 然后使用该色彩控制图混合头部颜色和面部颜色,从而平稳过渡这两种颜色。

确定色彩后,我们可以选择施加部分后处理混合效果。 该示例支持彩色和色彩调整效果。 彩色效果提取混合后的最终色彩的亮度,然后施加特定于用户的色调、饱和度和其他亮度。 色彩调整与其类似,不同之处在于它调整(而非覆盖)现有的色调、饱和度和亮度。 两种效果均支持色彩控制图控制的这两种色彩/调整。 这些效果使用皮肤图掩膜,以便眼睛和嘴唇的颜色保持不变。

所有混合操作均在 GPU 上完成。 如欲获取着色器,请访问 Media/MyAssets/Shader/SculptFace.fx。


图 8: 用于混合用户选择的两种色彩(左)的色彩传递图,和将纹理添加至头部的特征图(右)。


图 9:色彩控制图(左)负责控制头部颜色与面部颜色的混合。 该混合流程创建可映射至头部网格 UV 坐标(右)的色彩纹理。

头发几何体阶段

仅扫描面部不能提供头发信息。 应用需要提供头发,才能创建完整的头部。 该示例仅包含几个选择,只能展示技术功能,无法提供完整的解决方案。 功能齐全的应用包含多种选择和变体。

该示例支持更改头部外形,因此也可支持改变头发外形,从而以使两者之间完美匹配。 一种可能性是创建头发形态目标,以匹配头部形态目标。 但这需要创建大量的艺术资产,因此该示例选择随头部外形的变化以编程的方式调整头发形状。


图 10:在基础头部网格(左)上渲染的基础头发。 在调整后的头部外形上渲染的调整后的头发(中和右)。

找到与基础头部模型相关的头发顶点,然后将顶点移至最终头部外形中的相同的头部相对位置,即可完成头发匹配过程。 具体来说,每个头发顶点都与基础头部网格上的三角形、重心坐标和沿该三角形法线的偏移相关。

可以首先将头发顶点映射至基础头部网格的三角形,从而迭代每个头部网格三角形,然后使用三角形的法线将顶点投影至三角形的平面,并查看顶点是否位于三角形内部。 但这种方法会造成头发顶点可能无法映射至三角形的情况。 更好的方法是沿着顶点的法线正向投影基础头部网格三角形,直到它与头发顶点交叉。 如果头发顶点能够映射至多个头部网格三角形(因为头部网格为非凸面体),那么可以选择沿三角形法线的头发顶点最近的三角形。


图 11:简化后的头部网格和头发顶点。


图 12:单个三角形沿顶点法线突出,直到包含头发顶点。


图 13:为新的头部网格外形计算新的头部顶点位置。 按照相同的重心坐标查找,但与三角形的新顶点位置和法线相关。


图 14:将三角形投影至顶点 — 数学。

图 14 显示了用于将三角形投影至顶点的矢量,如以下 4 个视图所示。

  • 黄色三角形为原始头部三角形。
  • 灰色三角形为投影至头发顶点的黄色三角形。
  • 蓝色线条表示顶点法线(非标准化)。
  • 粉色线条从头发顶点延伸至黄色三角形上的点(注意其中一个顶点用作平面上的点)。
  • 绿色线条显示沿着三角形法线从顶点到平面(包含三角形)的最短距离。 投影三角形的顶点位置的计算方法为:首先计算最近距离 d(从头发顶点到包含三角形的平面)。

Nt = 三角形法线
Ns = 表面法线(即从顶点法线插值替换)
Vh = 头发顶点
P = 平面上的顶点(即黄色三角形的一个顶点)
Px= 交叉位置。 三角形上的位置与从头发顶点(沿着表面法线)延伸出的线条交叉
at = 2x 投影三角形区域
d =从头发顶点到包含头部三角形的平面之间的最短距离
l = 从头发顶点到交叉点的距离

从头发顶点到包含头部三角形的平面之间的最短距离为投影三角形提供距离。

d = Nt(Vh-P)

各投影顶点的位置 v’i ,其中 Ni Vi 的法线,即

Vi =Vi + dNi/(NtNi )

头发顶点的重心坐标(与投影三角形相关)为三角形的总面积。 运用交叉乘积使三角形的面积增加一倍。

at = |(V1 - V0) x (V2-V0)|

三个重心坐标 a、b、c 为

a = |(V2 - V1) x (Vh-V1)|/at

b = |(V0 - V2) x (Vh-V2)|/at

c = |(V1 - V0) x (Vh-V0)|/at

顶点位于三角形内部,如果 a、b、c 大于 0,那么 a、b、c 的总和应小于 1。 注意该顶点位于表面法线之上。 重心坐标提供通过三条顶点法线插值交替的表面法线。 它们同样提供三角形上的点(也沿着这条线分布(即交叉点)。

Ns = aN0 + bN1 + cN2

交叉点为

Px = aV0 + bV1 + cV2

保存从头发顶点到 Px的距离 (l)。 头部变形后,头发顶点将移动至与新头部外形上的三角形保持这段距离。

l = |(Px - Vh)|

逆向进行该流程,以确定与新头部外形相对的头发顶点位置。 交叉点的重心坐标(之前根据基础头部网格所计算的)可用于计算头发顶点位置和新头部外形的法线。

N’s = aN0 + bN1 + cN2

Px = aV0 + bV1 + cV2

新的头发顶点位置为

Vh = Px + lNs

注意,该方法仅移动顶点, 取法检查其他交叉点。 在实际应用中,该方法能够取得良好的效果,但也存在局限性。 例如,极端头部外形会使头发顶点/三角形穿过头发。

另外需要注意的一点是,头发顶点可位于头部三角形的任意一侧。 这样有助于美术师将部分头发顶点推进至头部。 可缩短该距离,从而最大限度地降低相关顶点和三角形位于头部另一侧的几率。

致谢

用于基础头部网格、形态目标、纹理和部分头发模型的资产均由 3DModelForge (http://3dmodelforge.com) 创建。

其他头发由 Liquid Development (http://www.liquiddevelopment.com/) 创建。

Doug McNabb、Jeff Williams、Dave Bookout 和 Chris Kirkpatrick 为该示例提供了其他帮助。

在英特尔® 实感™ 应用中实施用户体验指南

$
0
0

下载示例应用 ›

简介

用户体验 (UX) 指南可帮助在应用中实施英特尔® 实感™ 技术。 然而,想要实现这些指南的可视化却非常困难,原因有以下四点: (a) 在应用设计阶段,没有用于最终用户测试的原型,但必须在无法触摸的环境中解析最终用户交互,(b) 应用可用于不同外形的设备,比如笔记本电脑和一体机,用于交互的视野 (FOV) 和用户位置各不相同,(c) 所使用的彩色摄像头和景深摄像头的保真度和 FOV 各不相同,(d) 不同的英特尔® 实感™ SDK 模式有不同的 UX 要求。 因此关键要有一个实时反馈机制来判断这种影响。 本文将介绍专为支持开发人员使用英特尔® 实感™ 应用而开发的应用,以帮助开发人员查看和了解 UX 要求,并在代码中实施这些指南。 本文可供下载适用于该应用的源代码。

应用

该应用仅适用于前置摄像头。 本应用范围内包含 F200SR300两种摄像头。 在该应用中,这两种摄像头之间可无缝切换。 如果使用 F200 摄像头,该应用将基于 Windows* 8 或 Windows® 10 运行。 如果使用 SR300 摄像头,该应用将要求安装 Windows 10。

该应用中有两个窗口。 一个窗口提供实时摄像头馈入,以支持用户进行交互。 该部分还提供可视指标,模拟您将在应用中提供的可视反馈。 在以下场景中,我们将调出已经实施的可视反馈。 另一个窗口提供实施特定 UX 场景所需的代码片段。 下文将逐一介绍各个场景。 WPF 是一种开发框架。

英特尔实感 SDK版本: Build 8.0.24.6528

英特尔® 实感™ 景深摄像头管理器 (DCM) (F200) 版本: 1.4.27.52404 版

英特尔® 实感™ 景深摄像头管理器 (SR300) 版本: 3.1.25.2599 版

该应用预计适用于更多 SDK 版本,但仅上述版本通过了验证。

场景

普通场景

景深和 RGB 分辨率

RGB 和景深摄像头支持不同的分辨率,而且宽高比也各不相同。 不同的模式对这些摄像头也有不同的分辨率要求。

以下截图显示了使用多种模式时 SDK 文档所规定的流分辨率和帧速率:

UX 问题:

如何知道哪部分屏幕用于 3D 交互,哪部分用于放置 UI? 当最终用户超出交互区域后,如何通过可视反馈或音频反馈将这种情况告诉他们?

实施:

该应用使用 SDK API(下文将会提到)获取适用于各模式的色彩和景深分辨率数据,并在色彩图上绘制深度图,以展示叠加区域。 在摄像头馈入窗口中,查看表示色彩图和深度图重叠区域的黄色边界。这就是可视反馈。 从 UX 的角度来说,现在您可以观看屏幕,确定用于 FOV 3D 交互的屏幕区域,以及用于放置 UI 元素的区域。 在第一栏中选择不同的模式以及可用的色彩和景深分辨率进行试验,了解从 RGB 至景深的映射对预期用途的影响。 以下截图显示了重叠区域如何随输入的变化而变化的示例。

使用景深和色彩分辨率时的示例:

试验用户在不同的色彩和景深分辨率之间进行切换时映射所发生的变化。 并选择其他使用景深和 RGB 的模式,查看色彩和景深分辨率支持列表的变化。

仅使用景深分辨率时的示例:

使用手部骨骼跟踪时可动手试验该示例。 该用例无需彩色摄像头,不过您可以切换各种可用的景深分辨率,查看屏幕映射的变化。

仅使用色彩分辨率时的示例:

如果应用只能使用面部识别,您只需面部边界框即可,因此 2D 功能足以满足这一需求。 但是如果您需要 78 项特征,那么需切换至 3D 示例。

示例应用可在本文中下载,它介绍了在应用实施该切换流程所需的代码。 重点来说,为不同模式创建景深和色彩分辨率迭代列表所需的两个 API 分别为 PXCMDevice.QueryCaptureProfile (int i) 和 PXCMVideoModule.QueryCaptureProfile()。 不过,为了便于查看两张图的重叠,还需要使用 Projection 界面。 我们知道,每个像素都有一个相关的色彩和景深值。 在本示例中,为使景深图和色彩图重叠,我们仅选择一个景深值。 为实施此景深值,该应用使用 blob 模块。 此方法使用离摄像头最近的 blob(说你的手),并映射该 blob 的中心(屏幕中的蓝绿色点)。 然后将该像素的景深值用作单个景深值,以将景深图映射在色彩图上。

最佳光照

英特尔实感 SDK 不直接提供 API 来识别摄像头运行时环境中的光照条件。 如果光照条件较差,会导致色彩数据中出现许多噪音。

UX 问题:

在该应用中,最好为用户提供可视反馈,提示用户移到光照条件理想的环境下。 在该应用中,观察摄像头馈入如何在屏幕上显示当前亮度值。

实施:

该应用使用 RGB 值并使用对数平均亮度来确定光照条件。 有关如何使用对数平均亮度的更多信息,请访问此处

以下公式用于确定像素的对数平均亮度:

L = 0.27R + 0.67G + 0.06B;

数值范围为 0-1,其中 0 表示光线极暗,1 表示光线明亮。 本示例不定义阀值,因为这需要开发人员进行试验。 影响亮度值的因素包括背光、黑色衣服(可导致许多像素给出接近于 0 的等级,从而降低平均值),室外和室内光照条件等等。

因为每帧数据中的像素都要执行这种计算,因此它属于计算密集型操作。 该应用展示了如何使用 GPU 实施这种计算,以达到最佳性能。

原始数据流

英特尔实感 SDK 提供 API 捕捉色彩和景深数据流。 不过在有些情况下,可能需要捕捉原始数据流,以执行低层计算。 英特尔实感 SDK 提供 C++ API 和 .NET 包装程序。 这意味着包含图像的内存位于未托管内存中,因此 在 WPF 中显示图像时并不处于最佳状态。

解决该问题的方法是使用 PXCMImage.ToBitmap() API 创建围绕图像数据包装的未托管 HBITMAP,并使用 System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap() 将数据拷贝至托管堆中,然后包装 WPF BitmapSource 对象。

UX 问题:

采用上述方法的问题是在 CPU 上完成 YUY2-> RGB 转换后,我们需将图像数据从未托管内存拷贝至托管内存。 这样会大幅降低该流程的速度,还会导致数据丢失和抖动的出现。

实施:

该应用展示了另外一种实施办法,即使用 Microsoft .NET 框架版本 3 的 Service Pack 1 中的 Direct3D* Image Source,使 WPF 中包含任意的 DirectX* 9 平面。 我们使用未托管 DirectX 库来进行色彩转换,从而支持在 GPU 上显示。 该方法还支持通过像素着色器处理 GPU 加速后的图像,从而满足自定义控制需求(例如处理深度图像数据)。 以下截图显示了自定义着色器所显示的原始色彩、IR 和景深流,以及深度图像。

面部识别

英特尔实感 SDK 中最常用的一种模式是面部模块。 该模块支持识别 FOV 中的四个人,还可为每张脸提供 78 个特征点。 使用这些数据点时,可以在应用中集成面部识别实施。 Windows 10 操作系统中的 Windows Hello* 可使用这些特征模版,以识别登录用户。 更多关于 Windows Hello 工作原理的信息,请访问此处。 在此应用中,我们重点关注有关该模块的 UX 问题,以及如何提供可视反馈来纠正最终用户的交互,从而提升 UX。

UX 问题:

最大的 UX 挑战是最终用户不知道摄像头的 FOV 在哪里。 他们可能完全处于视锥范围之外,或离计算机太远,导致超出范围。 英特尔实感 SDK 会提供多项告警来捕捉这些场景。 但关键是如何向超出 FOV 范围的最终用户提供可视反馈。 在本应用中,如果最终用户处于 FOV 以及允许的范围内,将会提供一个绿色的边界框,表示用户处于交互区域内。 您可以将头移到计算机边缘或更远的地方来进行试验,此时你会看到,一旦摄像头丢失面部数据,会马上出现一个红色边界框。

实施:

英特尔实感 SDK 提供以下告警,以便有效处理用户错误: ALERT_FACE_OUT_OF_FOV、ALERT_FACE_OCCLUDED、ALERT_FACE_LOST。 更多关于告警的信息,请参阅 PXCMFaceModule。 该应用使用简单的 ViewModel 架构捕捉错误,并在 XAML 中针对错误采取行动。

沉浸式协作

设想一下您正在照相亭设置中尝试获取一张背景分割后的图像。 正如深度和 RGB 场景部分所述,每种英特尔实感模式的范围都不相同。 因此如何为最终用户提示最佳的 3D 摄像头范围,以便他们将自己放在合适的 FOV 中?

UX 问题:

与面部识别场景一样,关键是如何向在该范围中移动的最终用户提供可视反馈。 在本应用中,请注意,该滑块可为摄像头 FOV 设定最佳范围,从而支持 3D 分割(绿色部分)。 如要确定最小范围,看将左侧滑块移动至带有摄像头图片的那端。 注意像素如何变成白色。 另外,如果想确定最大的最佳范围,可向右移动右侧滑块。 如果超出最佳点,像素将变成红色。 两个滑块之间的范围则为支持图像分割的最佳范围。

花一秒钟时间看看最后一张图像。 您将注意到,使用 BGS 时会出现另一个 UX 问题。 向背景(此案例中为椅子)移动时,3D 分割模块将通过前景和背景对象创建一个 blob。 如果背景为黑色,而且您穿着黑色衬衫,也会出现这种情况。 识别像素不均匀的深度非常困难。 本应用不解决该场景问题,但我们想提出这一 UX 挑战以引起大家的注意。

实施:

3D 分割模块提供告警以处理 UX 场景。 我们此处实施的部分重要告警包括: ALERT_USER_IN_RANGE、ALERT_USER_TOO_CLOSE 和 ALERT_USER_TOO_FAR。 该应用实施这些告警,为用户提供色素沉淀和纹理反馈,以告知用户离得太近或太远。

3D 扫描

前置摄像头的 3D 扫描模块支持扫描面部和小物体。 在该应用中,我将通过面部扫描示例展示部分 UX 挑战,以及如何实施代码以添加可视反馈和音频反馈。

UX 问题:

如要获得良好的扫描效果,其中面临的一个主要挑战是如何检测扫描区域。 通常,扫描开始后的几秒钟会锁定这一过程。 以下截图显示了为获得良好扫描效果所需的摄像头区域。

如果用户无法确定准确的扫描区域,扫描模块将无法正常运行。 以下是无法正常运行的示例场景: 扫描面部时,用户要求面对摄像头以便摄像头检测到面部,然后‘缓慢地’从左至右转动头部。 因此观看屏幕时,当用户处于摄像头 FOV 内,必须以边界框的形式为用户提供有关面部的可视反馈。 请注意,扫描开始之间,我们才需要这种反馈。 扫描开始后,用户从左至右转动头部,无法看到屏幕,因此这种可视反馈毫无用处。 在示例应用中,我们同时创建了可视反馈和音频反馈,以辅助该场景。

实施:

PXCM3DScan 模块包含以下告警: ALERT_IN_RANGE、ALERT_TOO_CLOSE、ALERT_TOO_FAR、ALERT_TRACKING 和 ALERT_TRACKING_LOST。 在该应用中,我可同时捕捉这些告警,并按照需要提供可视反馈或音频反馈。 以下截图显示应用正在捕捉告警和提供反馈。

扫描开始之前以及扫描过程中的可视反馈:

请注意在此示例中,我们不展示如何保存和渲染网格。 关于如何在应用中实施 3D 扫描的具体信息,请参阅 SDK API 文档

总结

在应用中使用英特尔实感技术会遇到诸多 UX 挑战,包括如何理解非触摸反馈,以及最终用户如何使用和理解这项技术。 通过实时解释部分 UX 挑战和有助于解决这些挑战的代码片段,我们希望该应用可帮助开发人员和 UI 设计人员更好地了解英特尔实感技术。

更多资源

面向英特尔® 实感™ 技术设计应用 — 包含 Windows* 示例的用户体验指南

关于英特尔® 实感™ 摄像头(前置)的 UX 最佳实践 — 技术技巧

英特尔® Software Guard Extensions 教程系列: 第三部分,英特尔® SGX 设计

$
0
0

英特尔® Software Guard Extensions(英特尔® SGX )教程系列第三部分将介绍如何将英特尔 SGX 融入应用设计之中。 我们将运用第一部分中回顾的部分概念,以对第二部分的示例应用 Tutorial Password Manager 进行高级设计。 我们将描述该应用的整体结构以及英特尔 SGX 对它的影响,并创建一个类模型来帮助我们设计和集成安全区。

文章英特尔® Software Guard Extensions 教程系列简介列举了所有已经发布的教程。

尽管我们不对安全区或安全区界面进行编码,但该系列的安装部分仍然会提供源代码。 非英特尔 SGX 版本的应用内核(不包含用户界面)可供下载。 它包含一个小型测试程序,一个用 C# 编写的控制台应用,以及一个示例密码仓库文件。

专为安全区设计

以下是我们设计面向英特尔 SGX 的 Tutorial Password Manager 时所采用的通用方法:

  1. 识别应用机密。
  2. 识别这些机密的提供者和使用者。
  3. 确定安全区边界。
  4. 定制安全区的应用组件。

识别应用机密

设计英特尔 SGX 应用的第一步是识别应用机密。

机密表示不应被其他人知晓或看到的事情。 因此,机密只供其用户或目标应用访问,绝对不能展示给其他用户或应用,无论其拥有多高的权限级别。 机密包含财务信息、病历、个人可识别信息、身份数据、授权的媒体内容、密码和加密密钥。

Tutorial Password Manager 中包含多个可即刻识别为机密的条目,如表 1 所示。


秘密


用户帐号密码

用户帐号登录信息

用户的主密码或口令

密码仓库的主密钥

帐号数据库的加密密钥


表 1:初步的应用机密列表。


这些选项比较明显,但我们可以将所有用户帐号信息(不仅仅是登录信息)都包含在内,以扩展该列表。 修改后的列表如表 2 所示


秘密


用户帐号密码

用户帐号登录信息

用户的主密码或口令

密码仓库的主密钥

帐号数据库的加密密钥


表 2: 修改后的应用机密列表。

即使没有显示密码,帐号信息(比如服务名称和 URL)对攻击者来说也非常重要。 在密码管理器中显示此类数据会为不怀好意的攻击者提供重要提示。 有了此类数据,他们可以选择通过社交工程或密码重置攻击来对服务本身发动攻击,以访问所有者的帐号,因为他们完全知道谁是他们的攻击目标。

识别这些机密的提供者和使用者。

识别应用机密后,接下来是确定它们的来源和目的地。

在当前的英特尔 SGX 版本中,安全区代码未经加密,任何能够访问该应用文件的人都能对其进行拆卸和观察。 根据定义,可供观察的事情不能成为机密,即机密不能静态地编译成安全区代码。 应用机密必须来源于安全区外部,在运行时加载至安全区内。 在英特尔 SGX 术语表中,这称之为将机密供应至安全区。

当机密来源于可信计算基 (TCB) 外部的组件时,必须最大限度地降低暴露给非信任代码的几率。 (远程验证之所以成为英特尔 SGX 的重要组件,主要是因为它支持服务提供者建立与英特尔 SGX 应用的可信关系,然后衍生出加密密钥用于将加密机密供应至仅能通过客户端系统上的可信安全区解密的应用。) 从安全区导出机密时同样需要谨慎小心。 一般来说,如果没有事先在安全区内进行加密,应用机密不能发送至非信任代码。

对 Tutorial Password Manager 应用而言遗憾的是,我们确实需要将机密发送至安全区以及从安全区发出机密,而且这些机密必须在某个点以明文形式存在。 最终用户通过键盘或触摸屏输入帐号信息和密码,并在将来根据需要重新调用。 他们的帐号密码需要显示在屏幕上,甚至根据请求拷贝至 Windows* 剪切板。 只有满足这些核心要求,密码管理器应用才能发挥作用。

这意味着我们无法完全消除攻击面:只能最大限度地缩小攻击面,并且需要实施规避策略来处理机密以纯本文形式位于安全区外部的情况。

机密

来源

目的地

用户帐号密码

用户输入*

密码仓库文件

用户界面*

剪切板*

密码仓库文件

用户帐号信息

用户输入*

密码仓库文件

用户界面*

密码仓库文件

用户的主密码或口令

用户输入

密钥衍生功能

密码仓库的主密钥

密钥衍生功能

数据密钥加密

帐号数据库的加密密钥

随机生成

密码仓库文件

密码仓库加密

密码仓库文件

表 3: 应用机密及其来源与目的地。 星号 (*) 表示可能存在安全风险。

表 3 新增了 Tutorial Password Manager 中机密的来源和目的地。 星号 (*) 表示可能存在的问题 — 机密可能会暴露给非信任代码。

确定安全区边界

识别机密后,接下来应该确定安全区的边界。 首先通过应用的核心组件查看机密的数据流。 安全区边界应该:

  • 包含少量对应用机密起作用的关键组件。
  • 如果可行,完全包含多个机密。
  • 最大限度地减少与非信任代码的交互以及对它的依赖。

Tutorial Password Manager 应用的数据流和所选安全区边界如图 1 所示。

Figure 1

图 1:面向 Tutorial Password Manager 中的机密的数据流。

此处用圆圈表示应用机密:蓝色圆圈表示应用执行期间在某个点以(未加密)纯文本形式存在的机密,绿色圆圈表示经过应用加密的机密。 安全区边界围绕加密和解密路径、密钥衍生功能 (KDF) 和随机数生成器绘制。 此举可为我们执行以下几项任务:

  1. 在安全区内生成数据库/仓库密钥,用于加密部分应用机密(帐号信息和密码),而且在安全区外部不以明文形式发送该密钥。
  2. 通过用户口令在安全区内衍生出主密钥,并将其用于加/解密数据库/仓库密钥进行。 主密钥是临时的,不会以任何形式在安全区外部发送。
  3. 数据库/仓库密钥、帐号信息和帐号密码在安全区内通过非信任代码(见 #1 和 #2)不可见的加密密钥完成加密。

遗憾的是,通过安全区边界的未加密机密存在一些我们无法避免的问题。 在 Tutorial Password Manager 执行过程中的某个点,用户需要通过键盘输入密码,或将密码拷贝至 Windows 剪切板。 这些非安全通道无法放在安全区内部,而且是执行应用所必需的操作。 如果决定在托管代码库顶端构建应用,该问题会变得非常严重。

保护安全区外部的机密

我们没有完备的解决方案来保护安全区外部的未加密机密,只能采取规避策略来缩小攻击面。 最好地方法是最大限度地缩短信息以易受影响的形式所存在的时间。

以下一般性建议可帮助处理非信任代码中的敏感数据:

  • 使用完敏感数据后零填充数据缓冲区。 务必使用确保不会由编译器进行优化的函数,比如 SecureZeroMemory (Windows) 和 memzero_explicit (Linux)。
  • 不要使用 C++ 标准模板库 (STL) 容器保存敏感数据。 STL 容器有自己的内存管理方法,对象删除后,难以确保分配给对象的内存经过安全擦除。 (使用自定义分配程序,可解决部分容器所存在的这种问题。)
  • 处理 .NET 等托管代码或具有自动内存分配功能的语言时,使用专为保留安全数据所设计的存储类型。 其他存储类型依赖垃圾回收器和即时编译,可能无法按照需求进行清空和释放。
  • 如果必须将数据放在剪切板上,请务必尽快清空。 尤其是应用退出后,不要让数据留在剪切板上。

对 Tutorial Password Manager 项目而言,我们必须使用原生代码和托管代码。 在原生代码中,我们将分配 wchar_tchar 缓冲区,并在释放之前,使用 SecureZeroMemory 将其擦除干净。 在托管代码空间中,我们将使用 .NET’s SecureString 类。

将 SecureString 发送至未托管代码时,我们将使用 System::Runtime::InteropServices 的帮助程序函数封送数据。 

using namespace System::Runtime::InteropServices;

LPWSTR PasswordManagerCore::M_SecureString_to_LPWSTR(SecureString ^ss)
{
	IntPtr wsp= IntPtr::Zero;

	if (!ss) return NULL;

	wsp = Marshal::SecureStringToGlobalAllocUnicode(ss);
	return (wchar_t *) wsp.ToPointer();
}

可以使用两种方法将数据封送至其他方向,即从原生代码封送至托管代码。 如果 SecureString 对象已经存在,我们将使用 Clear AppendChar 方法通过 wchar_t字符串设置新的数值。

password->Clear();
for (int i = 0; i < wpass_len; ++i) password->AppendChar(wpass[i]);

如果创建新的 SecureString 对象,我们将使用构建程序通过现有 wchar_t字符串创建 SecureString

try {
	name = gcnew SecureString(wname, (int) wcslen(wname));
	login = gcnew SecureString(wlogin, (int) wcslen(wlogin));
	url = gcnew SecureString(wurl, (int) wcslen(wurl));
}
catch (...) {
	rv = NL_STATUS_ALLOC;
}

密码管理器还支持将密码传输至 Windows 剪切板。 剪切板是不安全的存储空间,其他用户都能够访问该剪切板,因此 Microsoft 建议不要将敏感数据放在剪切板上。 不过密码管理器旨在支持用户创建不需要记住,且安全性较强的密码。 它还支持创建长密码,其中包含随机生成,难以手动输入的字符。 尽管存在风险,但剪切板能够提供诸多便利。

为缓解风险,我们需要采取其他防范措施。 首先确保应用退出时已清空剪切板。 原生对象中的析构函数可帮助完成清空。

PasswordManagerCoreNative::~PasswordManagerCoreNative(void)
{
	if (!OpenClipboard(NULL)) return;
	EmptyClipboard();
	CloseClipboard();
}

我们还可设置剪切板计时器。 密码拷贝至剪切板时,设置 15 秒的计时器,到时间后执行相关函数清空剪切板。 如果计时器已经运行,表示旧密码到期之前剪切板放置了新密码,该计时器将取消并由新计时器取代。

void PasswordManagerCoreNative::start_clipboard_timer()
{
	// Use the default Timer Queue

	// Stop any existing timer
	if (timer != NULL) DeleteTimerQueueTimer(NULL, timer, NULL);

	// Start a new timer
	if (!CreateTimerQueueTimer(&timer, NULL, (WAITORTIMERCALLBACK)clear_clipboard_proc,
		NULL, CLIPBOARD_CLEAR_SECS * 1000, 0, 0)) return;
}

static void CALLBACK clear_clipboard_proc(PVOID param, BOOLEAN fired)
{
	if (!OpenClipboard(NULL)) return;
	EmptyClipboard();
	CloseClipboard();
}

定制安全区的应用组件

识别机密并绘制完安全区边界后,现在可以构建应用,同时将安全区纳入帐之号。 安全区内的操作内容存在诸多限制,而且这些限制条件将规定放在安全区内的组件,安全区外的组件,以及移植现有应用时需要一分为二的组件。

影响 Tutorial Password Manager 最大的限制条件是安全区不能执行任何 I/O 操作。 安全区不能通过键盘读取,也不能写入至显示器,因此所有机密(密码和账号信息)必须封送至安全区内,或者从安全区封送出来。 而且,它不能读取或写入仓库文件:解析仓库文件的组件必须与执行物理 I/O 操作的组件分开。 这意味着我们穿过安全区边界所封送的内容除了机密外,还有文件内容。

 Class diagram for the Tutorial Password Manager.

图 2:Tutorial Password Manager 类图。

图 2 显示了应用内核(包括用户界面)的基本类图,包括用作机密来源和目的地的类。 请注意,在该图中,为简单起见,PasswordManagerCore 类被视作必须与 GUI 交互的机密的来源和目的地。 表 4 对各个类及其用途进行了简要介绍。

类型

功能

PasswordManagerCore

托管

与 C# 图形用户界面 (GUI) 交互,并将数据封送至本地层。

PasswordManagerCoreNative

原生,非信任         

与托管 PasswordManagerCore 类交互。 同时负责在 Unicode 和多字节字符数据之间进行转换。

VaultFile

托管

读写仓库文件。

Vault

原生,安全区

将密码仓库数据保存在 AccountRecord成员中。 在读取过程中对仓库文件进行反序列化处理,并重新实现其序列化以供写入。

AccountRecord

原生,安全区

将各个账号的账号信息和密码保存在用户的密码仓库中。

Crypto

原生,安全区

执行加密功能。

DRNG

原生,安全区

随机数生成器接口。

表 4:类描述。

请注意,我们需要将仓库文件处理分成两部分:一部分执行物理 I/O 操作,另一部分保存读取并解析后的内容。 还需新增序列化和反序列化方法来处理作为机密中间来源和目的地的 Vault 对象。 所有这些操作都必不可少,因为 VaultFile 类完全不了解仓库文件的结构,且要求访问安全区内部的加密函数。

连接 PasswordManagerCoreNative 类和 Vault 类时我们还绘制了一条虚线。 大家可能还记得我们在第二部分说过,安全区只能链接至 C 函数。 有两种 C++ 类相互之间不能通信:必须使用表示为 Bridge Functions 框的中介。

非英特尔® Software Guard Extensions 代码路径

英特尔 SGX 代码路径如图 2 中的示意图所示。 PasswordManagerCoreNative 类不能直接链接至 Vault 类,因为后者位于安全区内。 不过非英特尔 SGX 代码路径中不存在这种限制: PasswordManagerCoreNative 可直接包含 Vault类的成员。 这是我们面向非英特尔 SGX 代码路径进行应用设计时所能走的唯一捷径。为了简化安全区集成,非安全区代码路径仍然会将仓库处理隔离在 Vault VaultFile 类中。

两种路径之间的另外一个不同点是英特尔 SGX 路径中的加密函数将来源于英特尔 SGX SDK。 非英特尔 SGX 代码路径不能使用这些函数,因为它们将利用 Microsoft 的 Cryptography API: Next Generation* (CNG)。 这意味着我们需要保持 Crypto 类有两个不同的副本:一个在安全区内使用,一个在非信任空间中使用。 (其他的类也是如此;第 5 部分将予以介绍。)

示例代码

如简介部分所述,本部分将提供示例代码以供下载。 随附档案包含进行安全区集成之前,用于 Tutorial Password Manager 核心 DLL 的源代码。 换言之,它是非英特尔 SGX 版本的应用内核。 这里不提供用户界面,但包含用 C# 编写,并通过一系列测试操作运行的基础测试应用。 它执行两个测试套件,一个创建新仓库文件并在此基础上执行各种操作,另一个处理通过源分发模式所包含的参考仓库文件。 编写时,测试应用预计测试仓库位于 Documents 文件夹中,不过您可以根据需要在 TestSetup 类中进行更改。

按照本教程简介部分所述要求,源代码在 Microsoft Visual Studio* Professional 2013 中开发。 此时不需要英特尔 SGX SDK,但需要系统支持英特尔® 数据保护技术(基于安全密钥)。

即将推出

教程第四部分将介绍如何开发安全区和桥接函数。 敬请关注!

在文章英特尔® Software Guard Extensions 教程系列简介中查找已发布的教程列表。

Viewing all 154 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>