diff --git a/config.json b/config.json index 35920b42..e94e3a9c 100644 --- a/config.json +++ b/config.json @@ -33,7 +33,7 @@ "files": ["README.md", "build_ebook.py","daux.patch",".gitignore"], "folders": ["ebook"] }, - "languages": {"en": "English", "fr": "Français"}, + "languages": {"en": "English", "fr": "Français", "zh": "中文"}, "language": "en", "processor": "VulkanLinkProcessor" } diff --git a/zh/00_Introduction.md b/zh/00_Introduction.md new file mode 100644 index 00000000..e7a56cd3 --- /dev/null +++ b/zh/00_Introduction.md @@ -0,0 +1,91 @@ +--- +title: 背景介绍 +--- + +## 关于本教程 + +本教程将帮助读者学习使用 [Vulkan](https://www.khronos.org/vulkan/) 的基本知识。Vulkan 是一种新近的用于图形和计算的应用程序接口 (API), +由 [Khronos 组织](https://www.khronos.org/)(以OpenGL闻名)推出,其为现代显卡提供了很好的抽象。相比于其它已有的 API 如 +[OpenGL](https://zh.wikipedia.org/wiki/OpenGL) 和 [Direct3D](https://zh.wikipedia.org/wiki/Direct3D),Vulkan 能让你更明确地 +表述程序要做什么,从而带来性能提升,减少因为驱动程序导致的意外行为。Vulkan 背后的理念和 +[Direct3D 12](https://zh.wikipedia.org/wiki/Direct3D#Direct3D_12) 以及 [Metal](https://zh.wikipedia.org/wiki/Metal_(API)) +相似,但 Vulkan 具有全面跨平台的优点,使你能够同时为 Windows,Linux 和 Android 进行开发。 + +然而,为了享受这些好处所支付的代价是,开发者不得不面对巨量繁复的 API,亲自从头处理与图形编程有关的每个细节,包括初始化帧缓冲,管理缓冲区和纹理 +对象内存的内存分配。图形驱动需要插手之处大大减少,这也意味着开发者不得不在程序代码中做更多工作来保证程序行为正确。 + +在这里要说的是,Vulkan 并不适合所有人。它的目标是热衷于高性能计算机图形学,并且愿意在这方面投入精力的程序员。如果相比于计算机图形学,你对游戏开 +发更感兴趣,那么您可能希望继续使用 OpenGL 或 Direct3D,它们在短期内不会被 Vulkan 取代。另一种选择是使用图形引擎,例如 +[虚幻引擎](https://en.wikipedia.org/wiki/Unreal_Engine#Unreal_Engine_4) +或 [Unity](https://en.wikipedia.org/wiki/Unity_(game_engine)),它们在可以底层使用 Vulkan 而对开发者提供更高层次的开发接口。 + +了解完以上内容,让我们看看学习本教程需要哪些准备: + +* 兼容 Vulkan 的显卡和驱动 ([NVIDIA](https://developer.nvidia.com/vulkan-driver), + [AMD](http://www.amd.com/en-us/innovations/software-technologies/technologies-gaming/vulkan), + [Intel](https://software.intel.com/en-us/blogs/2016/03/14/new-intel-vulkan-beta-1540204404-graphics-driver-for-windows-78110-1540), + [Apple Silicon(也叫 Apple M1)](https://www.phoronix.com/scan.php?page=news_item&px=Apple-Silicon-Vulkan-MoltenVK)) +* C++ 经验(熟悉 RAII 和初始化列表) +* 支持 C++17 的编译器(Visual Studio 2017+,GCC 7+ 或 Clang 5+) +* 3D 计算机图形学经验 + +本教程不会假定读者了解 OpenGL 或 Direct3D 的相关概念,但仍要求具备 3D 计算机图形学的基础知识。例如,本教程不会解释透视投影背后的数学原理。 +[这本在线书籍](https://paroj.github.io/gltut/)很好地介绍了计算机图形学的概念。其他一些优秀的计算机图形学资源包括: + +* [用一个周末实现光线追踪(Ray tracing in one weekend)](https://github.com/RayTracing/raytracing.github.io) +* [基于物理的渲染(Physically Based Rendering book)](http://www.pbr-book.org/) +* Vulkan 被应用于开源图形引擎 [Quake](https://github.com/Novum/vkQuake) 和 + [DOOM 3](https://github.com/DustinHLand/vkDOOM3) 中 + +如果你想,你可以使用 C 语言而不是 C++,但这样你就需要使用不同的线性代数库,而且要自己组织代码结构。本教程将使用类和 RAII 等 C++ 特性来组织 +逻辑,管理资源生命周期。还有两个为 Rust 开发者提供的本教程的替代版本:[基于 Vulkano 的教程](https://github.com/bwasty/vulkan-tutorial-rs), +[基于 Vulkanalia 的教程](https://vk.7dg.tech/)。 + +为了让使用其他编程语言的开发者更容易理解,并获得一些使用基本 API 的经验,我们将使用原始的 C API 编写 Vulkan。但是,如果你使用C++,那你可能更 +喜欢使用更新的 [Vulkan-Hpp](https://github.com/KhronosGroup/Vulkan-Hpp) 绑定,它封装了一些费力的工作,还能防止几类错误。 + +## 电子书 + +如果你想阅读本教程的电子书,可以在这里下载 EPUB 或 PDF 版本: + + +* [EPUB](https://vulkan-tutorial.com/resources/vulkan_tutorial_en.epub) +* [PDF](https://vulkan-tutorial.com/resources/vulkan_tutorial_en.pdf) + +## 教程结构 + +我们首先会概述 Vulkan 如何工作,要怎么在屏幕上绘制我们的第一个三角形。在你了解每一小步在整个流程中的作用后,你对它们的理解会更深刻。接下来我们 +搭建开发环境,包括 [Vulkan SDK](https://lunarg.com/vulkan-sdk/),用于线性代数操作的 [GLM](http://glm.g-truc.net/) 和用于创建窗口 +的 [GLFW](http://www.glfw.org/)。本教程将说明如何设置上述依赖,在 Windows 上使用 Visual Studio,而在 Ubuntu 上使用 GCC。 + +然后我们将实现要渲染你的第一个三角形所必需的 Vulkan 程序的所有基本组件。每章将大致遵循以下结构: + +* 介绍一个新概念以及使用它的目的 +* 使用与之相关的 API 调用将其集成到程序中 +* 将其部分地抽象为辅助函数 + +虽然每章都承接上一章的内容,但仍可以将每章作为介绍相应 Vulkan 特性的独立文章阅读。这意味着本教程也可以作为参考手册使用。所有的 Vulkan 函数和 +类型都超链接到了 Vulkan 规范中相应位置,你可以点击链接来了解更多。Vulkan 是非常新近的 API,所以其规范中也可能有些许不足。鼓励大家去 +[科纳斯组织的代码仓库](https://github.com/KhronosGroup/Vulkan-Docs) 进行反馈。 + +如前所述,为了让开发者能最大限度地控制图形硬件, Vulkan 的接口相当冗长,带有许多参数。这导致像创建纹理这样的基本操作每次都需要重复大量步骤。 +因此本教程中我们将自己创建一系列辅助函数。 + +每章末尾将附上截止该章用到的完整代码的链接。如果对代码结构有疑问,或者想要和自己的代码比较来解决其中的错误,可以参考本教程所附代码。所有代码文件 +都已在来自多个厂商的显卡上验证过正确性。还有每章后的评论区,可以在此询问与相应章节主题相关的任何问题。为了便于我们帮助你,提问时请指明你的开发环境, +驱动程序版本,源代码,预期的行为和实际行为。 + +本教程欢迎来自社区的踊跃参与。Vulkan 仍然是非常新近的接口,最佳实践尚未完全得到归纳。如果你对本教程和网站本身有任何类型的反馈,请不要犹豫,向 +[GitHub 仓库](https://github.com/Overv/VulkanTutorial)提交 issue 或 pull request。你可以 *watch* 该存储库,这样当教程更新时就能收 +到通知。 + +经历了用 Vulkan 在屏幕上画出第一个三角形的“仪式”后,我们会开始扩展简单的画三角形的程序,包括进行线性变换,加载纹理和三维模型。 + +如果你曾经使用过图形接口进行开发,你应该明白在第一个几何图元显示在屏幕上之前有许多步骤要进行。Vulkan 里有许多初始化步骤,但你将发现每个独立的 +步骤都是易懂且必需的。要记住,一旦你能够画出一个看似无聊的三角形,绘制完整的带纹理的 3D 模型并不需要花费太多的额外工作,在此之外的每一步都会 +更有价值。 + +如果你在学习本教程的过程中遇到了任何问题,首先查看“常见问题”页面,看看是否已经列出了你遇到的问题及其解决方案。如果在这之后仍然卡住,请在相关章节 +的评论部分寻求帮助吧。 + +准备好深入研究高性能图形 API 的未来了吗?[让我们开始吧!](!zh/Overview) diff --git a/zh/01_Overview.md b/zh/01_Overview.md new file mode 100644 index 00000000..234973d1 --- /dev/null +++ b/zh/01_Overview.md @@ -0,0 +1,174 @@ +--- +title: 概述 +--- + +本章首先介绍 Vulkan 以及它所解决的问题。然后我们会看一下为了画出第一个三角形所需要的步骤。这可以让你纵观全局并且理清后续每一章在整个过程中的位置。 +我们将会以展示 Vulkan API 的结构以及它们的一般使用模式作结。 + +## Vulkan 的起源 + +和以前的那些图形 API 一样, Vulkan 也被设计成了跨平台的 [GPU](https://zh.wikipedia.org/zh-cn/%E5%9B%BE%E5%BD%A2%E5%A4%84%E7%90%86%E5%99%A8) +抽象。那些 API 基本都存在的问题是,在设计它们的时代,图形硬件大多被限制在可配置的固定功能上。程序员必须以标准格式提供顶点数据,并在光照和阴影选项 +方面受制于 GPU 制造商。 + +随着显卡架构的日益成熟,他们开始提供越来越多的可编程功能,这些新功能被集成在原有的 API 中。这导致了与理想状态相比不够合理的抽象。并且在显卡驱动 +中,许多时候只能通过猜测程序员的意图来将之实现于现代图形架构。这就是驱动要经常更新来为游戏提供更好的显示性能的原因,这有时能大幅提高性能。出于这 +种复杂性,应用开发者们也得处理各种供应商的显卡之间的不一致,比如[着色器](https://zh.wikipedia.org/wiki/%E7%9D%80%E8%89%B2%E5%99%A8)的 +语法。除了这些新功能之外,在过去的十年中涌现出许多拥有强劲显卡的移动设备。这些移动设备的 GPU 根据功耗和空间需求的不同有着不同的架构。一个有代表 +性的例子是[基于图块渲染](https://zh.wikipedia.org/wiki/%E5%9F%BA%E4%BA%8E%E5%9B%BE%E5%9D%97%E6%B8%B2%E6%9F%93),通过赋予程序员 +更大的控制权来获得更好的性能。另一个来自于旧时代 API 的限制在于有限的多线程支持,这往往是造成 CPU 端性能瓶颈的原因。 + +为了解决这些问题,Vulkan 从头按照现代图形架构设计,提供更加详细的 API 给开发者使其能更明确地声明自己的意图,大大减少了驱动程序的开销,并且允许 +多线程并行创建和提交指令。它将着色器程序通过编译器编译成标准化的字节码,减少了编译着色器时的不一致性。最后,它认同现代显卡的通用处理能力,将计算 +和图形功能统一到了同一个 API 中。 + +## 画一个三角形需要分几步 + +我们现在来看看在一个行为良好的 Vulkan 程序中渲染一个三角形所需要的所有步骤的概述。此处介绍的所有概念在接下来的章节中会详细说明。此处只是为了让 +你一览整个流程,把每个单独部分之间联系起来。 + +### 第一步 实例和物理设备选择 + +Vulkan 应用程序以用 `VkInstance`(实例)来建立 Vulkan API 开始。创建一个实例则需要描述你的应用程序和你想使用的 API 扩展。创建实例之后,可 +以查询支持 Vulkan 的硬件设备并且选择一个或多个 `VkPhysicalDevice`(物理设备)来使用。你可以查询设备的属性(比如显存大小和设备能力)来选择所 +需设备,例如同时可用集成显卡和独立显卡时,你可能倾向于使用独立显卡。 + +### 第二步 逻辑设备和队列家族 + +在选择了合适的硬件设备之后,你需要创建一个 `VkDevice`(逻辑设备),你可以在其中更具体地描述你想使用哪些 `VkPhysicalDeviceFeatures`(物理 +设备特性),比如多视口渲染以及 64 位浮点数。同时,你也需要指明你想使用的队列家族(queue families)。绝大多数 Vulkan 操作,比如绘图命令和内 +存操作,都是通过提交到 `VkQueue`(队列)来异步执行的。队列(queue)从队列家族中分配,每个队列家族支持一组特定的操作。例如,可能存在不同的队列 +家族进行图形、计算和内存传输操作。队列家族的可用性也可以成为在选择物理显卡时的一个影响因素。可能存在一些支持 Vulkan 却不提供任何图形功能的设备, +不过目前所有支持 Vulkan 的显卡都广泛支持我们所感兴趣的所有队列操作。 + +### 第三步 窗口表面和交换链 + +除非你只想离屏渲染,你会需要一个窗口来显示渲染的图像。窗口可以使用原生平台 API 或者像是 [GLFW](http://www.glfw.org/) 以及 [SDL](https://www.libsdl.org/) +之类的图形库来创建。在此教程中我们选用 GLFW,下一章会详细讲解。 + +我们还需要另外两个组件来把图像渲染到窗口上:一个表面(`VkSurfaceKHR`, surface)和一个交换链(`VkSwapchainKHR`, swap chain)。注意, +`KHR` 后缀说明这些对象是 Vulkan 扩展(extension)的一部分。Vulkan API 本身是完全平台无关的,因此我们必须使用标准化的 WSI(Window System +Interface,窗口系统接口)扩展来与窗口管理器进行交互。表面是对要渲染的窗口的跨平台的抽象,它通常需要传入一个原生窗口的句柄来实例化,比如 +Windows 上的 `HWND`。幸运的是,GLFW 库内置了一个函数来帮我们处理平台相关的细节。 + +交换链是渲染目标的集合。它的基本作用是确保现在正在渲染的图像与现在显示在屏幕上的图像不是同一个。这可以确保显示出来的图像是完整的。每当我们想要绘 +制一个帧的时候,我们需要向交换链请求一个图像来进行渲染。当我们完成绘制之后,把这个图像返还到交换链,之后某个时刻,图像被显示到屏幕上。渲染目标的 +数量以及将渲染完成的图像显示到屏幕上的条件由显示模式(present mode)决定。常见的渲染模式有双缓冲(vsync,垂直同步)和三缓冲。在创建交换链那一 +章我们再详细讨论这些。 + +在一些平台上可以使用 `VK_KHR_display` 和 `VK_KHR_display_swapchain` 扩展直接渲染到显示器上,而无需与任何窗口管理器交互。例如,你可以通 +过这些扩展创建一个代表整个屏幕的表面,来实现你自己的窗口管理器。 + +### 第四步 图像视图和帧缓冲 + +为了在从交换链中请求到的图像上进行绘制,我们需要用 `VkImageView`(图像视图)和 `VkFramebuffer`(帧缓冲)把它包装起来。图像视图引用了图像中 +要被使用的特定部分,而帧缓冲则引用一些图像视图并把它们当作颜色、深度和模板目标使用。因为在一个交换链中可能有多个不同的图像,所以我们提前为每一个 +图像创建一个图像视图和一个帧缓冲,并且在绘制时选择合适的那个。 + +### 第五步 Render pass + +Vulkan 中的 render pass(渲染流程)描述了在渲染操作时要使用的图像类型、图像的使用方式以及处理图像的内容的方式。在我们最初绘制三角形的应用中, +我们要告诉 Vulkan,我们将会使用一个图像作为颜色目标,并且我们想要在绘制之前把它清除为纯色。Render pass 只描述图像的类型,绑定图像对象是通过 +`VkFramebuffer` 完成的。 + +### 第六步 图形管线 + +Vulkan 中的图形管线可以通过 `VkPipeline`(管线)对象来建立。它描述了显卡的一些可配置部分(译注:不可编程部分),比如视口大小以及深度缓冲操作 +等,而可编程部分则使用 `VkShaderModule`(着色器模块)对象来描述。`VkShaderModule` 对象使用着色器的字节码来创建。驱动还需要知道管线中的哪些 +渲染目标会被使用,需要我们用 render pass 来说明。 + +Vulkan 与现有的其它 API 之间最明显的区别就是,图形管线的几乎所有配置项都需要提前设置好。这意味着如果你想切换到另一个着色器或者稍微改变一下顶点 +数据的布局,你都需要重新创建整个图形管线。这意味着,你需要针对你在渲染操作时的具体情况提前创建许多 `VkPipeline` 对象,以满足渲染操作所需的所有 +不同组合。只有很少的一些基本配置可以动态更改,比如视口大小和清屏颜色等。所有的状态都必须被明确地描述,例如,没有默认的颜色混合模式。 + +好消息是,就像提前编译(ahead-of-time compilation)与即时编译(just-in-time compilation)的区别那样,驱动有更多的优化机会,并且运行时性 +能将会更加具有可预测性,因为大量的状态更改,如切换不同的图形管线,是非常明确的。 + +### 第七步 命令池和命令缓冲 + +如前所述,在 Vulkan 中,许多我们想要执行的操作,比如绘制操作,都需要被提交到一个队列中去。这些操作在提交之前需要先被记录到一个 +`VkCommandBuffer`(命令缓冲)中。命令缓冲由 `VkCommandPool`(命令池)分配,每个命令池与一个特定的队列家族相关联。为了画出一个三角形,我们 +需要在命令缓冲里记录下列操作: + +* 开始 render pass +* 绑定图形渲染管线 +* 画 3 个顶点 +* 结束渲染过程 + +因为帧缓冲中的图像取决于交换链具体会给我们哪一个,我们需要为每一个可能使用的图像记录一个命令缓冲,然后在绘制的时候选择合适的那个。另外一种可行的 +方法是每一帧都重新记录一次命令缓冲,但是这种方法效率不高。 + +### 第八步 主循环 + +现在绘制命令已经被包装到了命令缓冲里,那主循环就十分简单明了了。首先我们通过 `vkAcquireNextImageKHR` 来从交换链中获得一个图像,之后为这个图 +像选择合适的命令缓冲并且用 `vkQueueSubmit` 来执行。最后,我们用 `vkQueuePresentKHR` 把这个图像返回到交换链中以供显示。 + +提交到队列中的操作是异步执行的。因此我们需要使用像信号量等这样的同步对象来保证执行顺序正确。绘图命令缓冲必须被设置为等到获取图像之后再执行,否则 +可能导致我们开始渲染一个正在被读取以在屏幕上显示的图像。`vkQueuePresentKHR` 函数反过来又需要等待渲染完成,为此我们需要第二个信号量,并且在渲 +染完成后发出信号。 + +### 小结 + +以上这些简单的讲解应该让你在绘制第一个三角形前对所需要做的工作有了基本的认识。通常一个真正实用的程序包含了更多的步骤,比如分配顶点缓冲,创建 +uniform 缓冲以及上传纹理图像等,这些将会在后面的章节讲解。我们先从一个简单的例子开始,因为 Vulkan 的学习曲线非常陡峭。我们作了一点小弊,把顶点 +坐标硬编码到了顶点着色器里而不是使用顶点缓冲,因为管理顶点缓冲需要先对命令缓冲有一定的了解。 + +所以长话短说,画出第一个三角形需要: + +* 创建一个 `VkInstance` +* 选择一个受支持的显卡(`VkPhysicalDevice`) +* 为绘制和显示创建一个 `VkDevice` 和 `VkQueue` +* 创建一个窗口,窗口表面和交换链 +* 把交换链里的图像包装到 `VkImageView` 里面 +* 创建一个 render pass 来指定渲染目标及其用途 +* 为渲染过程创建帧缓冲 +* 建立图形渲染管线 +* 为交换链中每个可用的图像分配命令缓冲,并在其中记录绘制命令 +* 获取图像,提交正确的渲染命令缓冲,再把图像返还到交换链中,通过这样的方式绘制帧 + +步骤有很多,但是在接下来的章节中,每一步的目标都会变得非常简单而清晰。如果你对某一步在整个程序中的作用有疑惑,你应该回来参考本章。 + +## API 概念 + +本小节将简要概述 Vulkan API 的基本结构。 + +### 编码约定 + +Vulkan 中所有的函数、枚举类型和结构体都定义在了 `vulkan.h` 头文件中,这个文件包含在了 LunarG 开发的 [Vulkan SDK](https://lunarg.com/vulkan-sdk/) +里。下一章我们将会介绍如何安装这个SDK。 + +函数以小写的 `vk` 开头,枚举类型和结构体以 `Vk` 开头,枚举值则以 `VK_` 开头。这套 API 非常依赖结构体作为函数参数。举个例子,对象通常以这种 +形式创建: + +```c++ +VkXXXCreateInfo createInfo = {}; +createInfo.sType = VK_STRUCTURE_TYPE_XXX_CREATE_INFO; +createInfo.pNext = nullptr; +createInfo.foo = ...; +createInfo.bar = ...; + +VkXXX object; +if (vkCreateXXX(&createInfo, nullptr, &object) != VK_SUCCESS) { + std::cerr << "failed to create object" << std::endl; + return false; +} +``` + +Vulkan 中许多结构体要求你在 `sType` 成员中明确指定结构体类型。`pNext` 成员可以是一个指向扩展结构的指针,在此教程中它将被永远置为 `nullptr`。 +创建或销毁一个对象的函数会有一个 `VkAllocationCallbacks` 参数,允许你为驱动内存使用一个自定义的分配器,它在此教程中也将永远被置为 `nullptr`。 + +几乎所有函数的返回值都是一个 `VkResult` 的枚举类型,它要么是 `VK_SUCCESS`(成功),要么是一个错误代码。Vulkan 规范说明了每个函数会返回什么 +错误代码以及它们的含义。 + +### 验证层 + +就像之前说过的, Vulkan 被设计为高性能,低驱动开销的 API 。因此它默认的错误检查和调试能力非常有限。当你做错了什么的时候,驱动程序常常是直接崩 +溃而不是返回一个错误代码——或者更糟糕的是,在你的显卡上跑得起来,在别的显卡上就完全不行了。 + +Vulkan 允许你通过被称作*验证层*(validation layers)的特性来启用广泛的检查功能。验证层是一些可以被插入到 API 与显卡驱动之间的代码片段,可以 +用来运行额外的函数参数检查或者追踪内存管理问题。它的优点是你可以在开发的时候启用验证层,然后在发布应用时完全禁用它来做到零开销。每个人都可以编写 +自己的验证层,不过 LunarG 的 Vulkan SDK 提供了一套标准的验证层,我们将在教程中用到它们。为了从验证层接收调试信息,你还需要注册一个回调函数。 + +因为 Vulkan 中每个操作都非常明确,并且验证层的检查十分宽泛,所以相比于 OpenGL 和 Direct3D 更容易找到使你黑屏的原因! + +在我们开始写代码之还剩一步,那就是[配置开发环境](!zh/Development_environment)。 diff --git a/zh/03_Drawing_a_triangle/00_Setup/00_Base_code.md b/zh/03_Drawing_a_triangle/00_Setup/00_Base_code.md new file mode 100644 index 00000000..ab99941a --- /dev/null +++ b/zh/03_Drawing_a_triangle/00_Setup/00_Base_code.md @@ -0,0 +1,181 @@ +--- +title: 基本代码 +--- + +## 通用结构 + +在上一章你已经使用正确的配置创建了一个 Vulkan 项目,并且已经用一些简单的代码测试过了。在这一章我们会用下面的代码从头开始: + +```c++ +#include + +#include +#include +#include + +class HelloTriangleApplication { +public: + void run() { + initVulkan(); + mainLoop(); + cleanup(); + } + +private: + void initVulkan() { + + } + + void mainLoop() { + + } + + void cleanup() { + + } +}; + +int main() { + HelloTriangleApplication app; + + try { + app.run(); + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} +``` + +首先我们从 LunarG SDK 中引入 Vulkan 的头文件,这个头文件提供了函数、结构体和枚举类型。`stdexcept` 和 `iostream` 头文件用来报告和输出 +错误。`functional` 头文件为资源管理部分提供 lambda 函数支持,`cstdlib` 头文件提供 `EXIT_SUCCESS` 和 `EXIT_FAILURE` 宏定义。 + +程序本身被包装在了一个类里,我们把 Vulkan 对象存储成这个类的私有成员,并且添加成员函数来初始化它们,这些成员函数会被 `initVulkan` 函数调用。 +当准备工作都做好了之后,我们进入主循环开始渲染每一帧。我们会用一个循环来填充 `mainLoop` 函数,它会一直循环到窗口被关闭为止。一旦窗口被关闭, +`mainLoop` 返回,我们将确保在 `cleanup` 函数中释放所有用过的资源。 + +如果在运行过程中有发生了任何致命错误,我们会抛出 `std::runtime_error` 异常并给出异常描述信息,这个异常描述信息会被传递到 `main` 函数,然后 +被输出到命令行。为了同时处理各式标准异常,我们捕获更为一般的 `std::exception`。很快就会有一个关于错误处理的例子,我们会检查我们需要的扩展是否 +受支持。 + +粗略地说,此后的每一章我们都会添加一个会被 `initVulkan` 函数调用的新函数以及对应的 Vulkan 对象作为私有类成员,成为私有成员的新 Vulkan 对象 +需要在程序末尾通过 `cleanup` 函数释放。 + +## 资源管理 + +就像通过 `malloc` 申请到的每一块内存都必须通过 `free` 函数释放一样,每个 Vulkan 对象在当我们不需要它的时候都需要被显式销毁。现代 C++中可以 +通过 [RAII](https://zh.wikipedia.org/wiki/RAII) 机制或 `` 头文件提供的智能指针来进行自动资源管理。但是在此教程中,我选择显式 +地分配和回收 Vulkan 对象。毕竟 Vulkan 的卖点就在于显式地进行每一个操作从而避免出错,所以最好明确对象的生命周期来学习 API 如何工作。 + +在学习此教程之后,你可以通过各种方式实现自动资源管理,例如写一个 C++ 类,在构造函数中产生并持有 Vulkan 对象,在析构函数中释放它们;或者也可以 +给 `std::unique_ptr` 或 `std::shared_ptr` 提供自定义删除器,具体使用哪种智能指针取决于你的所有权管理策略。在大型 Vulkan 程序中很推荐 +使用 RAII,但是为了学习的目的,知道幕后发生了什么总是好的。 + +Vulkan 对象要么是直接用形如 `vkCreateXXX` 的函数直接创建的,要么是通过形如 `vkAllocateXXX` 的函数从另一个对象分配的。当你确定一个对象不再 +被任何地方所使用的时候。你需要使用相应的 `vkDestroyXXX` 和 `vkFreeXXX` 来销毁它。这些函数的参数通常因对象的类型不同而不同,不过有一个参数是 +它们公有的:`pAllocator`。这是一个可选的参数,允许你为自定义的内存分配器指定回调函数。在此教程中我们将忽略这个参数并一直传一个 `nullptr` 作为 +参数。 + +## 集成 GLFW + +如果你只想离屏渲染的话,Vulkan 在不创建窗口的情况下也能工作良好,但是事实上显示出点什么东西会更让人兴奋!首先删掉 +`#include ` 这一行,换成: + +```c++ +#define GLFW_INCLUDE_VULKAN +#include +``` + +这样,GLFW 会使用它自己的定义并且自动加载 Vulkan 头文件。添加一个 `initWindow` 函数并且在 `run` 函数中第一个调用它。我们会用这个函数初始 +化 GLFW 并创建一个窗口。 + +```c++ +void run() { + initWindow(); + initVulkan(); + mainLoop(); + cleanup(); +} + +private: + void initWindow() { + + } +``` + +在 `initWindow` 函数中第一个调用的应该是 `glfwInit()`,这个函数初始化 GLFW 库。因为 GLFW 原本是为创建 OpenGL 上下文设计的,所以我们接下 +来需要调用函数告诉 GLFW 不要创建 OpenGL 上下文: + +```c++ +glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); +``` + +允许窗口调整大小会产生许多额外的问题,这一点日后再谈,现在先通过调用另一个 window hint 调用禁用掉: + +```c++ +glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); +``` + +现在可以创建真正的窗口了。添加一个 `GLFWwindow* window;` 私有成员变量来保存一个 GLFW 窗口的引用,然后用以下函数初始化它: + +```c++ +window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr); +``` + +前三个参数知名了窗口的长度、宽度和标题。第四个参数是可选的,允许你指定一个显示器来显示这个窗口。最后一个参数只与 OpenGL 有关。 + +比起硬编码,使用常量来表示长度和宽度显然更好,因为一会儿我们还要用到这些值好几次。我在 `HelloTriangleApplication` 类的定义里加入了如下几行: + +```c++ +const uint32_t WIDTH = 800; +const uint32_t HEIGHT = 600; +``` + +然后把创建窗口的代码改成这样 + +```cpp +window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); +``` + +现在 `initWindow` 函数看起来应该长这样: + +```c++ +void initWindow() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + + window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); +} +``` + +为了能让这个程序在不发生错误或者关闭窗口的情况下一直运行下去,我们需要在 `mainLoop` 函数中添加如下所示的事件循环: + +```c++ +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + } +} +``` + +这段代码的意思应该不言自明。它是一个循环,每次循环都会检查事件,比如某按钮有没有被按下,一直循环到窗口被用户关闭为止。我们过之后还要在这个循环里 +调用绘制单个帧的函数。 + +一旦窗口被关闭,我们需要销毁资源并退出 GLFW,把资源清理干净。这就是我们最初的 `cleanup` 代码: + +```c++ +void cleanup() { + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +现在运行这个程序,你应该会看到一个标题为 `Vulkan` 的窗口,它会一直显示着,除非你把它关掉,程序也因此结束。现在我们有了一个 Vulkan 程序的框架, +让我们[创建第一个 Vulkan 对象吧](zn/Drawing_a_triangle/Setup/Instance)! + +[C++代码](https://vulkan-tutorial.com/code/00_base_code.cpp) diff --git a/zh/03_Drawing_a_triangle/00_Setup/01_Instance.md b/zh/03_Drawing_a_triangle/00_Setup/01_Instance.md new file mode 100644 index 00000000..a61645bb --- /dev/null +++ b/zh/03_Drawing_a_triangle/00_Setup/01_Instance.md @@ -0,0 +1,188 @@ +--- +title: 实例 +--- + +## 创建一个实例 + +一切开始于创建一个*实例*(instance)来初始化 Vulkan 库。实例是连接 Vulkan 库和你的程序之间的桥梁,创建实例还涉及到向驱动指定你的应用程序的 +一些细节。 + +添加一个 `createInstance` 函数,然后在 `initVulkan` 函数中调用它。 + +```c++ +void initVulkan() { + createInstance(); +} +``` + +再在类中添加一个数据成员,用来保存实例的句柄: + +```c++ +private: +VkInstance instance; +``` + +现在,为了创建实例,我们首先需要用我们程序的一些信息去填充一个结构体。从技术上来说,这些信息是可有可无的,但是它们或许能够提供一些信息给驱动,以 +使驱动针对我们的特定程序进行优化(例如,它使用了一个具有某些特殊行为的知名图形引擎)。这个结构体叫做 `VkApplicationInfo`: + +```c++ +void createInstance() { + VkApplicationInfo appInfo{}; + appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + appInfo.pApplicationName = "Hello Triangle"; + appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.pEngineName = "No Engine"; + appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.apiVersion = VK_API_VERSION_1_0; +} +``` + +就像之前提到过的那样,Vulkan 中的许多结构体需要你在 `sType` 成员中显式指定类型。这个结构体也是众多拥有 `pNext` 成员的结构体之一,这个成员在 +将来可以指向扩展信息。我们现在执行默认初始化,所以此处置为 `nullptr`(空指针)。 + +Vulkan 中的许多信息都通过结构体来传递,而不是函数参数。我们还需要填充另一个结构体来为创建实例提供足够多的信息。接下来的这个结构体是必需的,它告 +知 Vulkan 驱动我们要使用哪些全局的扩展以及验证层。“全局”意味着它们将在整个程序中生效,而不是某个特定的设备,接下来的几章里我们会说明这个问题。 + +```c++ +VkInstanceCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; +createInfo.pApplicationInfo = &appInfo; +``` + +前两个参数的意思非常明显。接下来的两个成员会指定我们想用的全局扩展。就像我们在概述那章里提到过的,Vulkan 是一套平台无关的 API,这意味着你需要 +一个扩展与窗口系统(window system)来交互。GLFW 已经集成了一个好用的内置函数,它返回 GLFW 需要的 Vulkan 扩展,我们可以直接把它传给 +Vulkan API: + +```c++ +uint32_t glfwExtensionCount = 0; +const char** glfwExtensions; + +glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + +createInfo.enabledExtensionCount = glfwExtensionCount; +createInfo.ppEnabledExtensionNames = glfwExtensions; +``` + +最后两个成员指定哪些全局验证层将会被启用。我们会在下一章深入讨论验证层,这里先暂时留空。 + +```c++ +createInfo.enabledLayerCount = 0; +``` + +我们已经指定了初始化 Vulkan 实例需要的所有信息,现在终于可以调用 `vkCreateInstance` 函数了: + +```c++ +VkResult result = vkCreateInstance(&createInfo, nullptr, &instance); +``` + +如你所见,Vulkan 中创建对象的函数,其参数通常是这样的: + +* 指向创建信息(creation info)的指针 +* 指向自定义分配器回调函数的指针,此教程中永远被置为 `nullptr` +* 指向保存了要被创建的对象的句柄变量的指针 + +如果一切运行良好,那么创建好的实例的句柄就被保存在 `VkInstance` 类型的成员变量中了。几乎每一个 Vulkan 函数的返回值都是 `VkResult` 类型的, +它要么是 `VK_SUCCESS`,要么是一个错误代码。如果要检查实例是否被成功创建,我们不需要保存这个返回结果,只需要检查一下返回值就行了: + +```c++ +if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); +} +``` + +现在运行这个程序以确定实例创建成功。 + +## 遭遇 VK_ERROR_INCOMPATIBLE_DRIVER + +如果在 MacOS 上使用最新的 MoltenVK SDK,你可能从 `vkCreateInstance` 得到 `VK_ERROR_INCOMPATIBLE_DRIVER` 返回值。根据 +[Vulkan SDK 的入门指南](https://vulkan.lunarg.com/doc/sdk/1.3.216.0/mac/getting_started.html),从 1.3.216 版本开始, +`VK_KHR_PORTABILITY_subset` 扩展必须被启用。 + +为了解决这个错误,首先添加 `VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR` 标志位到 `VkInstanceCreateInfo` 结构体的 `flags` +成员,然后添加 `VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME` 到实例的启用扩展列表。 + +典型的代码应该像这样: + +```c++ +... + +std::vector requiredExtensions; + +for(uint32_t i = 0; i < glfwExtensionCount; i++) { + requiredExtensions.emplace_back(glfwExtensions[i]); +} + +requiredExtensions.emplace_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + +createInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + +createInfo.enabledExtensionCount = (uint32_t) requiredExtensions.size(); +createInfo.ppEnabledExtensionNames = requiredExtensions.data(); + +if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); +} +``` + +## 检查插件是否受支持 + +如果你看过 `vkCreateInstance` 的文档,你就会看到有一个错误代码是 `VK_ERROR_EXTENSION_NOT_PRESENT`。我们可以简单地指定我们想用的扩展, +如果返回了这个错误码就直接终止程序。如果要检查那些必要的扩展,例如窗口系统接口(window system interface, WSI),这么做还有点道理,但如果我 +们要检查那些可选的功能呢? + +为了在创建实例之前得到所有受支持的扩展列表,可以用 `vkEnumerateInstanceExtensionProperties` 函数。它需要两个指针变量,一个指向受支持的扩 +展数量,另一个指向一个 `VkExtensionProperties` 类型的、存储着扩展的细节的数组。它的第一个参数是可选的,允许我们使用一个特殊的验证层来选择扩 +展,我们现在先忽略它。 + +为了分配那个存储着扩展的细节的数组,我们需要先知道扩展的数量。你可以通过把最后一个参数留空的方式来只请求扩展的数量: + +```c++ +uint32_t extensionCount = 0; +vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); +``` + +现在来分配一个数组,保存扩展的细节(引入头文件 `#include `): + +```c++ +std::vector extensions(extensionCount); +``` + +最后我们就可以查询扩展的细节了: + +```c++ +vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data()); +``` + +每个 `VkExtensionProperties` 结构体都包含着扩展的名称和版本。我们可以通过一个简单的循环来输出它们(`\t` 是一个制表符,用来缩进): + +```c++ +std::cout << "available extensions:\n"; + +for (const auto& extension : extensions) { + std::cout << '\t' << extension.extensionName << '\n'; +} +``` + +如果你想输出 Vulkan 支持的详细信息,你可以把这段代码加到 `createInstance` 函数里。留一个课后练习,尝试创建一个函数,检查 +`glfwGetRequiredInstanceExtensions` 函数返回的所有扩展是不是都在受支持的扩展列表里。 + +## 清理 + +`VkInstance` 只应该在程序退出之前被销毁。可以在 `cleanup` 函数中用 `vkDestroyInstance` 函数销毁它: + +```c++ +void cleanup() { + vkDestroyInstance(instance, nullptr); + + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +`vkDestroyInstance` 函数的参数非常直接了当,就像在上一章里提过的那样,Vulkan 中的分配器和回收器都有一个可选的回调函数参数,这个参数被我们设 +置为 `nullptr` 以忽略它。在随后的章节中,我们创建的所有其它 Vulkan 资源都会在实例被销毁之前回收。 + +在创建了实例之后、开始进行更复杂的步骤之前,是时候看看我们的[验证层](!zh/Drawing_a_triangle/Setup/Validation_layers)来评估调试选项了。 + +[C++ 代码](https://vulkan-tutorial.com/code/01_instance_creation.cpp) diff --git a/zh/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md b/zh/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md new file mode 100644 index 00000000..db8d8228 --- /dev/null +++ b/zh/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md @@ -0,0 +1,412 @@ +--- +title: 校验层 +--- + +## 校验层是什么? + +Vulkan API 基于最小化驱动负担的思想设计,这个目标的一个体现形式就是,在默认情况下,这套 API 中的错误检查十分有限。哪怕是一点小问题,比如枚举 +值传错了,或者在必需参数上传了一个空指针,通常都不会显式暴露出来,而只是简单地崩溃或者产生未定义行为。Vulkan 要求你在使用时显式设置每样东西,这 +很容易导致许多小毛病的发生:比如使用了新的 GPU 特性却没有在创建逻辑设备的时候请求它。 + +然而,这并不意味着不能给这套 API 加上错误检查。Vulkan 使用了一个非常优雅的系统来进行错误检查,这就是*校验层*(*validation layers*)。校验 +层是可选的,它们能在你调用 Vulkan 函数时插入钩子来执行额外的操作。一般来说,校验层有以下用途: + +* 根据规范检测参数值,以避免误用 +* 追踪对象的创建和析构过程,以发现资源泄露 +* 追踪调用被发起的源头线程,以检查线程安全性 +* 把每个调用及其参数都记录在标准输出上 +* 追踪 Vulkan 函数的调用,以进行性能分析和重放 + +以下是诊断校验层(diagnostics validation layer)中一个函数的例子,用来说明其实现大概是什么样子: + +```c++ +VkResult vkCreateInstance( + const VkInstanceCreateInfo* pCreateInfo, + const VkAllocationCallbacks* pAllocator, + VkInstance* instance) { + + if (pCreateInfo == nullptr || instance == nullptr) { + log("Null pointer passed to required parameter!"); + return VK_ERROR_INITIALIZATION_FAILED; + } + + return real_vkCreateInstance(pCreateInfo, pAllocator, instance); +} + +``` + +这些校验层可以自由地组合起来,以实现你感兴趣的所有调试功能。你可以简单地在调试时开启校验层,然后在发布时彻底关掉校验层,这样两全其美。 + +Vulkan 没有任何内置的校验层,但是 LunarG Vulkan SDK 提供了一套校验层来检查普遍会犯的错误。这些校验层是完全 +[开源](https://github.com/KhronosGroup/Vulkan-ValidationLayers)的,所以你可以查看它们检查哪些错误类型,也可以向其贡献代码。使用校验 +层是避免你的应用程序因不小心依赖未定义行为而在不同的驱动上出错的最佳方式。 + +校验层只有在安装在系统上之后才能使用。例如,LunarG 校验层只能在装了 Vulkan SDK 的电脑上使用。 + +Vulkan 中曾经有两种不同类型的校验层:实例校验层和基于特定设备的校验层。与之对应的想法是,实例层只检查与全局 Vulkan 对象,例如与 instance 有 +关的调用;而基于特定设备的校验层则只检查与某种特定 GPU 有关的调用。基于特定设备的校验层现在已经被弃用,这意味着实例校验层可以作用于所有 Vulkan +调用。规范文档仍然推荐你同时在设备层面启用校验层以提高兼容性,这是某些实现所需要的。我们将简单地在逻辑设备层面启用一些和实例层面相同的校验层, +我们[之后](!zh/Drawing_a_triangle/Setup/Logical_device_and_queues)再讨论这个。 + +## 使用校验层 + +在这一节我们会看看如何启用一个 Vulkan SDK 提供的标准诊断层。和扩展一样,校验层也需要通过指定名字的方式启用。有用的校验策略都集成在了 SDK 提供 +的 `VK_LAYER_KHRONOS_validation` 层中。 + +首先在程序里加两个配置变量来指定要启用的层,以及是否启用它们。我选择基于是否开启调试模式来设置这个值。`NDEBUG` 宏是 C++ 标准的一部分,代表着 +“没有进行调试”(no debug)。 + +```c++ +const int WIDTH = 800; +const int HEIGHT = 600; + +const std::vector validationLayers = { + "VK_LAYER_KHRONOS_validation" +}; + +#ifdef NDEBUG + const bool enableValidationLayers = false; +#else + const bool enableValidationLayers = true; +#endif +``` + +我们添加了一个新函数 `checkValidationLayerSupport` 来检查是否所有被请求的层都可用。首先使用 `vkEnumerateInstanceLayerProperties` +函数列出所有可用的层。这个函数的使用方式与之前讲解创建实例的时候使用的 `vkEnumerateInstanceExtensionProperties` 函数相同。 + +```c++ +bool checkValidationLayerSupport() { + uint32_t layerCount; + vkEnumerateInstanceLayerProperties(&layerCount, nullptr); + + std::vector availableLayers(layerCount); + vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data()); + + return false; +} +``` + +接下来,检查 `validationLayers` 中的层是否都存在于 `availableLayers` 列表里。你可能需要引入 `` 头文件来使用 `strcmp`。 + +```c++ +for (const char* layerName : validationLayers) { + bool layerFound = false; + + for (const auto& layerProperties : availableLayers) { + if (strcmp(layerName, layerProperties.layerName) == 0) { + layerFound = true; + break; + } + } + + if (!layerFound) { + return false; + } +} + +return true; +``` + +现在我们可以在 `createInstance` 中使用这个函数了: + +```c++ +void createInstance() { + if (enableValidationLayers && !checkValidationLayerSupport()) { + throw std::runtime_error("validation layers requested, but not available!"); + } + + ... +} +``` + +现在以调试模式运行这个程序并且确保没有任何错误。如果出错了,请参阅“常见问题”页面。 + +最后,修改 `VkInstanceCreateInfo` 结构体实例,在启用校验层时包含相应的校验层名: + +```c++ +if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); +} else { + createInfo.enabledLayerCount = 0; +} +``` + +如果检查成功了,那么 `vkCreateInstance` 不应该返回 `VK_ERROR_LAYER_NOT_PRESENT` 错误,不过你应该运行一下程序来确保这点。 + +## 信息回调函数 + +校验层默认向标准输出打印调试信息,但我们亦可以在我们的程序中提供回调函数来自己处理这些调试信息。这还能使我们决定想看到哪些种类的哪些消息,毕竟不 +是所有消息都代表必要的(致命的)错误。如果你不想现在就这么做,你可以跳过本章的剩余部分。 + +为了设置回调函数,处理信息及有关详情,我们需要使用 `VK_EXT_debug_utils` 扩展来设置一个调试信使(debug messenger)。 + +首先我们创建一个 `getRequiredExtensions` 函数,这个函数将根据启用的校验层返回我们需要的插件列表: + +```c++ +std::vector getRequiredExtensions() { + uint32_t glfwExtensionCount = 0; + const char** glfwExtensions; + glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + + std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); + + if (enableValidationLayers) { + extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + } + + return extensions; +} +``` + +由 GLFW 指定的扩展总是需要启用,而调试信使的扩展是有条件地启用的。注意此处我使用了 `VK_EXT_DEBUG_UTILS_EXTENSION_NAME` 宏,它等同 +于字符串字面量 "VK_EXT_debug_utils"。使用这个宏可以让你避免打错字。 + +现在我们可以在 `createInstance` 里面使用这个函数了: + +```c++ +auto extensions = getRequiredExtensions(); +createInfo.enabledExtensionCount = static_cast(extensions.size()); +createInfo.ppEnabledExtensionNames = extensions.data(); +``` + +运行程序并且确保没有收到 `VK_ERROR_EXTENSION_NOT_PRESENT` 错误。我们实际上不用检查这个插件是否存在,因为校验层可用这件事本身暗示了它的存 +在。 + +现在我们来看看回调函数应该长什么样。加入一个名为 `debugCallback` 的新的静态成员函数并且使用 `PFN_vkDebugUtilsMessengerCallbackEXT` 函 +数原型。`VKAPI_ATTR` 和 `VKAPI_CALL` 确保了这个函数拥有正确的修饰符,以使 Vulkan 能够调用它。 + +```c++ +static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( + VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + VkDebugUtilsMessageTypeFlagsEXT messageType, + const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, + void* pUserData) { + + std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl; + + return VK_FALSE; +} +``` + +第一个参数指明了消息的严重性,其值是下列值之一: + +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT`:诊断消息 +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT`:信息性消息,例如一个资源被创建 +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT`:有关此消息的行为不一定是一个错误,但很有可能是应用程序中的一个 bug(警告) +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT`:有关此消息的行为是非法的,并且可能导致程序崩溃(错误) + +枚举值被设置为递增的,这样就可以用比较运算符来检查一条消息是否比某个严重程度更严重,例如: + +```c++ +if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + // 消息足够严重,需要被显示 +} +``` + +`messageType` 参数可以是以下值: + +- `VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT`:发生了一个与规范或性能无关的事件 +- `VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT`:发生了违反规范的行为或者有可能发生的错误 +- `VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT`:潜在的会使 Vulkan 性能劣化的使用方式 + +`pCallbackData` 参数指向 `VkDebugUtilsMessengerCallbackDataEXT` 类型的结构体,其中包含了这个信息的细节,其中最重要的成员有: + +- `pMessage`:调试信息,是一个空字符结尾字符串 +- `pObjects`:有关此消息的 Vulkan 对象句柄数组 +- `objectCount`:数组中的对象数量 + +最后,`pUserData` 参数包含了一个在设置回调函数时指定的指针,允许你传入自己的数据。 + +回调函数返回一个布尔值指示当校验层消息被 Vulkan 函数调用触发时是否应该退出程序。如果回调函数返回了真值,这个调用就会中止,并返回 +`VK_ERROR_VALIDATION_FAILED_EXT` 错误代码。这通常只用于测试校验层本身,因此你应该始终返回 `VK_FALSE`。 + +现在只剩下告知 Vulkan 有关这个回调函数的信息。说起来或许会有些令人惊讶,就连 Vulkan 中的调试回调函数也由一个需要显式创建和销毁的句柄来管理。 +这种回调函数是*调试信使*的一部分,并且你可以根据需要想设置多少个就设置多少个。在 `instance` 下方添加一个类成员来保存这个句柄: + +```c++ +VkDebugUtilsMessengerEXT debugMessenger; +``` + +现在添加一个 `setupDebugMessenger` 函数,然后在 `initVulkan` 中的 `createInstance` 之后调用: + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); +} + +void setupDebugMessenger() { + if (!enableValidationLayers) return; + +} +``` + +我们需要用这个信使极其回调函数的详细信息来填充一个结构体: + +```c++ +VkDebugUtilsMessengerCreateInfoEXT createInfo = {}; +createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; +createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; +createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; +createInfo.pfnUserCallback = debugCallback; +createInfo.pUserData = nullptr; // 可选的 +``` + +`messageSeverity` 字段允许你指定你的回调函数在何种严重等级下被触发。我在此指定了除 `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT` +以外的所有等级来接收所有可能的错误信息,并忽略更详细的一般调试信息。 + +类似地,`messageType` 字段允许你过滤回调函数的消息类型。我在这里简单地开启了所有类型,你可以关闭那些对你来说没什么用的。 + +最后,`pfnUserCallback` 指定了回调函数的指针。你可以给 `pUserData` 传递一个指针,这个指针会通过 `pUserData` 参数传递到回调函数中。比如 +你可以用它来传递 `HelloTriangleApplication` 类的指针。 + +注意,配置校验层消息和调试回调函数还有很多不同的方法,不过这里给出的是一个很适合入门的方法。关于其它方法,参阅这份 +[扩展规范](https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VK_EXT_debug_utils)以获取更多信息。 + +这个结构体应该被传递到 `vkCreateDebugUtilsMessengerEXT` 函数中来创建 `VkDebugUtilsMessengerEXT` 对象。不幸的是,因为这个函数是一个扩 +展函数,所以它不会被自动加载。我们必须自己用 `vkGetInstanceProcAddr` 函数来查找它的地址。我们要创建一个我们自己的钩子函数,帮助我们在幕后完 +成这一切。我在 `HelloTriangleApplication` 类定义之前添加了这个函数: + +```c++ +VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) { + auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT"); + if (func != nullptr) { + return func(instance, pCreateInfo, pAllocator, pDebugMessenger); + } else { + return VK_ERROR_EXTENSION_NOT_PRESENT; + } +} +``` + +如果这个函数没有被加载,`vkGetInstanceProcAddr` 函数则返回 `nullptr`。现在,如果该函数可用,我们就可以调用这个函数来创建这个扩展对象了: + +```c++ +if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { + throw std::runtime_error("failed to set up debug messenger!"); +} +``` + +倒数第二个参数依然是那个被我们设置成 `nullptr` 的可选的分配器回调函数,其余的参数含义都很明了。由于调试信使与我们的特定的 Vulkan 实例极其验 +证层相关联,实例需要被显式设置为第一个参数。一会你还会看到这种模式用在其它*子*对象上。 + +`VkDebugUtilsMessengerEXT` 对象还需要使用 `vkDestroyDebugUtilsMessengerEXT` 函数来清除。与 `vkCreateDebugUtilsMessengerEXT` +类似,这个函数需要被显式加载。 + +在 `CreateDebugUtilsMessengerEXT` 下面添加另外一个钩子函数: + +```c++ +void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) { + auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT"); + if (func != nullptr) { + func(instance, debugMessenger, pAllocator); + } +} +``` + +确保这个函数是一个静态成员函数,或者是一个在类外面的函数。然后我们可以在 `cleanup` 函数中调用它: + +```c++ +void cleanup() { + if (enableValidationLayers) { + DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr); + } + + vkDestroyInstance(instance, nullptr); + + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +## 调试实例的创建和销毁 + +尽管我们已经用校验层增加了调试功能,但我们仍然没有覆盖所有内容。调用 `vkCreateDebugUtilsMessengerEXT` 的前提是成功创建一个合法的实例,而 +`vkDestroyDebugUtilsMessengerEXT` 必须在实例被销毁前调用。这使我们目前无法调试调用 `vkCreateInstance` 和 `vkDestroyInstance` 过程 +中的问题。 + +然而,若你仔细阅读了[扩展文档](https://github.com/KhronosGroup/Vulkan-Docs/blob/main/appendices/VK_EXT_debug_utils.adoc#examples), +你将发现,有一种方法可以专门为这两个函数调用创建调试组件信使。只需要简单地将 `VkInstanceCreateInfo` 的 `pNext` 成员设置为一个指向 +`VkDebugUtilsMessengerCreateInfoEXT` 结构体的指针即可。首先将填充信使的创建信息的过程提取到一个单独的函数中: + +```c++ +void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) { + createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; + createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; + createInfo.pfnUserCallback = debugCallback; +} + +... + +void setupDebugMessenger() { + if (!enableValidationLayers) return; + + VkDebugUtilsMessengerCreateInfoEXT createInfo; + populateDebugMessengerCreateInfo(createInfo); + + if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { + throw std::runtime_error("failed to set up debug messenger!"); + } +} +``` + +现在我们可以在 `createInstance` 函数中重用这段代码: + +```c++ +void createInstance() { + ... + + VkInstanceCreateInfo createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + createInfo.pApplicationInfo = &appInfo; + + ... + + VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; + if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); + + populateDebugMessengerCreateInfo(debugCreateInfo); + createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*) &debugCreateInfo; + } else { + createInfo.enabledLayerCount = 0; + + createInfo.pNext = nullptr; + } + + if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); + } +} +``` + +变量 `debugCreateInfo` 放在了 if 语句外面来确保它不会在 `vkCreateInstance` 调用被销毁。用这种方法创建的额外的调试信使会自动在 +`vkCreateInstance` 和 `vkDestroyInstance` 中被使用,并在之后被清理。 + +## 测试 + +现在,让我们故意犯一个错误,看看校验层是如何工作的。暂时移除 `cleanup` 函数中调用 `DestroyDebugUtilsMessengerEXT` 的代码并运行你的程序。 +当程序退出后你应该看到类似下图的输出: + +![](/images/validation_layer_test.png) + +> 如果你没有看到任何消息,[检查你的安装配置](https://vulkan.lunarg.com/doc/view/1.2.131.1/windows/getting_started.html#user-content-verify-the-installation)。 + +如果你想看到哪个调用触发了消息,你可以在消息回调函数里打一个断点,然后看看堆栈跟踪。 + +## 配置 + +除了在 `VkDebugUtilsMessengerCreateInfoEXT` 结构体中指定标志之外,还有很多设置校验层行为的方法,浏览 Vulkan SDK 中的 `Config` 目录, +`vk_layer_settings.txt` 文件解释了如何设置这些校验层。 + +要为你的应用程序设置校验层,把这个文件复制到你工程的 `Debug` 和 `Release` 文件夹里然后照着上面的说明来设置你想要的行为。然而,在此教程的余下 +部分,我假设你用的是默认设置。 + +在此教程中,我会故意犯几个错误来让你看看校验层对于捕获这些错误有多大的帮助,并且告诉你清楚地知道你在用 Vulkan 做什么有多重要。现在是时候看看 +[系统中的 Vulkan 设备](!zh/Drawing_a_triangle/Setup/Physical_devices_and_queue_families)了。 + +[C++ 代码](/code/02_validation_layers.cpp) diff --git a/zh/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md b/zh/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md new file mode 100644 index 00000000..33f841c3 --- /dev/null +++ b/zh/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md @@ -0,0 +1,326 @@ +--- +title: 物理设备与队列族 +--- + +## 选择一个物理设备 + +通过 VkInstance 初始化了 Vulkan 的库之后,我们需要在系统中选择一个支持我们需要的功能的显卡。事实上,我们可以同时选择并使用 +任意数量的显卡,但是在这个教程里,我们会专注于第一个满足我们需要的显卡。 + +我们会添加一个函数 `pickPhysicalDevice`,并且在 `initVulkan` 函数中使用它。 + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + pickPhysicalDevice(); +} + +void pickPhysicalDevice() { + +} +``` + +我们最终选择使用的显卡会被添加为一个 VkPhysicalDevice 句柄的成员变量。在 VkInstance 被销毁的时候,这个对象也会被销毁,所以 +我们不需要在 cleanup 里面对它进行清理。 + +```c++ +VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; +``` + +列出显卡的方式和列出插件的方式很相似。它也是先查询数量。 + +```c++ +uint32_t deviceCount = 0; +vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr); +``` + +如果没有支持 Vulkan 的显卡,那么就不需要再搜寻下去了。 + +```c++ +if (deviceCount == 0) { + throw std::runtime_error("failed to find GPUs with Vulkan support!"); +} +``` + +除此之外我们可以分配一个数组来容纳所有的 VkPhysicalDevice 句柄。 + +```c++ +std::vector devices(deviceCount); +vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()); +``` + +因为显卡之间存在不同之处,现在我们需要去对每一个显卡评估是否合适用于我们要进行的操作。为此,我们将引入一个新的函数: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + return true; +} +``` + +然后调用这个函数去检查是否有显卡满足我们的要求。 + +```c++ +for (const auto& device : devices) { + if (isDeviceSuitable(device)) { + physicalDevice = device; + break; + } +} + +if (physicalDevice == VK_NULL_HANDLE) { + throw std::runtime_error("failed to find a suitable GPU!"); +} +``` + +下一部分会介绍我们在 `isDeviceSuitable` 要检查的第一个要求。在后面的章节中,我们开始使用更多 Vulkan 的功能的时候,我们也会 +扩展这个函数来包含更多的检查。 + +## 检查设备的基础兼容性 + +为了评估一个设备的兼容性,我们可以从查询设备的一些细节开始。设备的基础信息比如说设备的名称,类型和支持的 Vulkan 版本可以通过 `vkGetPhysicalDeviceProperties` 查询到。 + +```c++ +VkPhysicalDeviceProperties deviceProperties; +vkGetPhysicalDeviceProperties(device, &deviceProperties); +``` + +通过 `vkGetPhysicalDeviceFeatures`,可以查询是否支持一些可选的功能,比如说纹理压缩,64 位浮点型和多视口渲染(较多用于 VR)。 + +```c++ +VkPhysicalDeviceFeatures deviceFeatures; +vkGetPhysicalDeviceFeatures(device, &deviceFeatures); +``` + +更多能从设备中查到的细节,比如设备内存和队列族(参见下一部分),将在之后讨论。 + +例如说,如果我们的应用程序只有在支持几何着色器的显卡中可以使用,那么 `isDeviceSuitable` 函数就应该是这样: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + VkPhysicalDeviceProperties deviceProperties; + VkPhysicalDeviceFeatures deviceFeatures; + vkGetPhysicalDeviceProperties(device, &deviceProperties); + vkGetPhysicalDeviceFeatures(device, &deviceFeatures); + + return deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU && + deviceFeatures.geometryShader; +} +``` + +除了只检查是否设备是否是第一个,你也可以给每个设备打分,然后选择最高分的那个。这样你就可以找到一个最合适的显卡。但是如果只有 +集成 GPU 可以用,那就退回到集成 GPU。你可以写成类似于下面这个样子: + +```c++ +#include + +... + +void pickPhysicalDevice() { + ... + + // 使用一个有序映射来自动将候选的设备按照分数从小到大排序 + std::multimap candidates; + + for (const auto& device : devices) { + int score = rateDeviceSuitability(device); + candidates.insert(std::make_pair(score, device)); + } + + // 检查最佳的候选显卡是否可用 + if (candidates.rbegin()->first > 0) { + physicalDevice = candidates.rbegin()->second; + } else { + throw std::runtime_error("failed to find a suitable GPU!"); + } +} + +int rateDeviceSuitability(VkPhysicalDevice device) { + ... + + int score = 0; + + // 独立显卡有非常大的性能优势 + if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { + score += 1000; + } + + // 不影响渲染质量时能存储的最多的纹理的数量 + score += deviceProperties.limits.maxImageDimension2D; + + // 应用程序必须要求有几何着色器 + if (!deviceFeatures.geometryShader) { + return 0; + } + + return score; +} +``` + +对于这个教程来说,你不需要去完全照着这样去实现。这只是给你一个思路去设计你选择显卡的过程。当然你也可以直接显示所有显卡的名字 +然后让用户去做选择。 + +因为我们只是入门,支持 Vulkan 就是所有我们需要的了。所以我们可以选择任意一个 GPU: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + return true; +} +``` + +在下一部分中,我们会讨论我们第一个需要去检查的特性。 + +## 队列族 (Queue Family) + +在之前我们简单提到过,在 Vulkan 中,几乎所有的操作,所有从绘制到上传纹理的部分,都需要将命令上传给一个队列。从不同的 +*队列族*(queue families)中会有不同类型的队列,并且每个队列族只允许一个命令子集。比如说,可能会有一个队列族只允许处理计算 +命令,或者一个队列族只允许有关于内存转储的指令。 + +我们需要去检查哪一个队列族是设备支持的,并且其中哪一个支持我们想要使用的指令。我们可以添加一个新函数 `findQueueFamilies` 来 +查找所有我们需要的队列族。 + +现在我们要去查找一个支持图形指令的队列族。这个函数可能是这个样子: + +```c++ +uint32_t findQueueFamilies(VkPhysicalDevice device) { + // 查找图形队列族的逻辑 +} +``` + +但是,在后面的一个章节中我们需要去查找另外一个队列。所以为将来做准备,更好的做法是把索引存储在一个结构体中: + +```c++ +struct QueueFamilyIndices { + uint32_t graphicsFamily; +}; + +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + // 查找队列族索引并填充结构体的逻辑 + return indices; +} +``` + +但是如果队列族不能使用怎么办?我们可以在 `findQueueFamilies` 中抛出一个异常,但是这个函数不是一个处理设备兼容性的地方。比如 +说,我们可能更*倾向于*一个有专用的传输队列族的设备,而不是需要它。所以我们需要判断一个特定的队列族是否存在。 + +因为任何一个 `uint32_t` 都有可能是一个可用的队列族(包括 `0`),所以似乎并不能使用一个特定的数字来代表一个队列族不存在。幸运的 +是,C++17 引进了一个数据结构来区分值存在或不存在的情况: + +```c++ +#include + +... + +std::optional graphicsFamily; + +std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // false + +graphicsFamily = 0; + +std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // true +``` + +`std::optional` 是一个直到你给它赋值之前不去存储任何值的封装。你可以通过调用它的成员函数 `has_value()` 来查询它是否储存着一个 +值。这样我们就可以将函数改为: + +```c++ +#include + +... + +struct QueueFamilyIndices { + std::optional graphicsFamily; +}; + +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + // 将 index 赋值为找到的队列族 + return indices; +} +``` + +现在我们就可以去实际地去实现 `findQueueFamilies`: + +```c++ +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + + ... + + return indices; +} +``` + +和往常一样,列出队列族的过程需要使用到 `vkGetPhysicalDeviceQueueFamilyProperties`: + +```c++ +uint32_t queueFamilyCount = 0; +vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr); + +std::vector queueFamilies(queueFamilyCount); +vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data()); +``` + +VkQueueFamilyProperties 结构体储存了一些关于队列族的细节信息,包括支持的操作的类型,和这个队列族可以创建的队列数量。我们需要 +找到至少一个支持 `VK_QUEUE_GRAPHICS_BIT` 的队列族。 + +```c++ +int i = 0; +for (const auto& queueFamily : queueFamilies) { + if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) { + indices.graphicsFamily = i; + } + + i++; +} +``` + +现在我们有了这个查找队列族的函数,我们可以在 `isDeviceSuitable` 中使用它来确保设备可以处理我们想要使用的指令: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + QueueFamilyIndices indices = findQueueFamilies(device); + + return indices.graphicsFamily.has_value(); +} +``` + +为了便捷性,我们可以在结构体里面添加一个用来检查的函数: + +```c++ +struct QueueFamilyIndices { + std::optional graphicsFamily; + + bool isComplete() { + return graphicsFamily.has_value(); + } +}; + +... + +bool isDeviceSuitable(VkPhysicalDevice device) { + QueueFamilyIndices indices = findQueueFamilies(device); + + return indices.isComplete(); +} +``` + +现在我们就可以在 `findQueueFamilies` 中使用它来提前退出循环: + +```c++ +for (const auto& queueFamily : queueFamilies) { + ... + + if (indices.isComplete()) { + break; + } + + i++; +} +``` + +很好!这些就是我们目前需要的所有的查找合适物理设备的东西了!下一步就是 +[创建一个逻辑设备](!zh/Drawing_a_triangle/Setup/Logical_device_and_queues)与它交互。 + +[C++ code](/code/03_physical_device_selection.cpp) diff --git a/zh/03_Drawing_a_triangle/00_Setup/index.md b/zh/03_Drawing_a_triangle/00_Setup/index.md new file mode 100644 index 00000000..06368f68 --- /dev/null +++ b/zh/03_Drawing_a_triangle/00_Setup/index.md @@ -0,0 +1,4 @@ +--- +title: 引子 +ignore: true +--- diff --git a/zh/03_Drawing_a_triangle/index.md b/zh/03_Drawing_a_triangle/index.md new file mode 100644 index 00000000..8124572f --- /dev/null +++ b/zh/03_Drawing_a_triangle/index.md @@ -0,0 +1,4 @@ +--- +title: 画一个三角形 +ignore: true +--- diff --git a/zh/91_About_Chinese_translation.md b/zh/91_About_Chinese_translation.md new file mode 100644 index 00000000..cfd0d5d3 --- /dev/null +++ b/zh/91_About_Chinese_translation.md @@ -0,0 +1,43 @@ +--- +title: 中文翻译说明 +--- + +本书源代码托管于 GitHub([地址](https://github.com/Overv/VulkanTutorial)),使用经过修改的文档生成器 daux.io +([官方网站](https://daux.io),[修改版仓库](https://github.com/Overv/daux.io))来渲染网页,使用 Markdwon 格式。贡献翻译者需要了解 +daux.io 的基本使用规则和其支持的 Markdwon 语法。 + +本书的中文翻译基于 [daux.io 的多语言支持](https://daux.io/Features/Multilanguage.html),翻译文本放置在 `zh` 文件夹下,文件名保持与英 +文版本一致,在 [Front Matter](https://daux.io/Features/Front_Matter.html) 中使用 `title` 属性设置中文标题。 + +## Markdown 写法要求 + +中文翻译文本应当符合[中文排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines/blob/master/README.zh-Hans.md), +有几项额外要求: + +- 使用弯引号; +- 中文文本的行内链接的两侧不用额外添加空格,但若链接文本开头或结尾是英文数字等,需在相应位置添加空格; +- 每行不应超过 120 个半角字符长度,除非是代码,URL,标点禁则等必要情形。 + +行长度限制可用文本编辑器的辅助标尺来提示,如 VSCode 可在其 settings.json 中设置 `"editor.rulers": [120]` 在 120 半角字符行宽的位置显 +示一条竖线。 + +注意,文本以空行分段,没有首行缩进。 + +文章会经过预处理器处理,Vulkan 类型和函数名称会自动加上行内代码和指向相应页面的超链接,如 VkResult。中文翻译不应加上超链接,是否给原文没有行 +内代码格式的类型和函数名等加上行内代码格式则随意,最后显示效果正确即可。 + +## 术语翻译 + +参考 [OpenGL 3.3 教程的翻译术语对照](https://github.com/cybercser/OpenGL_3_3_Tutorial_Translation/blob/master/%E7%BF%BB%E8%AF%91%E6%9C%AF%E8%AF%AD%E5%AF%B9%E7%85%A7.md): + +- 以[《游戏引擎架构》中英词汇索引表](https://www.cnblogs.com/miloyip/p/GameEngineArchitectureIndex.html)为参考标准; +- 某些术语可以保留英文原文,如使用“uniform buffer”或“uniform 缓冲”而非“统一缓冲区”,“统一的缓冲区”; +- 在第一次出现相应术语时,用括号注明英文原文,如果是保留英文原文的术语,则注明中文翻译。 + +需要保留英文原文的术语: + +- 所有具体的 Vulkan API 的类型、枚举、函数、结构体成员名等。 +- render pass:渲染流程,渲染步骤 + + +