Skip to content

Commit e2ff9e3

Browse files
committed
Improve 0200-0390
1 parent c947bab commit e2ff9e3

11 files changed

+66
-61
lines changed

docs/md/02/00_vertexinput.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
## **顶点着色器**
1414

1515
首先我们需要改变顶点着色器的代码,不再包含硬编码的顶点数据。
16-
顶点数据将从外部获取,通过`in`关键字:
16+
顶点数据将从外部获取,通过 `in` 关键字:
1717

1818
```glsl
1919
#version 450
@@ -31,8 +31,8 @@ void main() {
3131

3232
`inPosition``inColor` 是顶点参数,这些数据将从管线的顶点输入获取。
3333

34-
我们之前提过,一个`location`只能放一个资源,所以我们的位置和颜色信息需要放在不同的`location`中。
35-
注意 `inPosition` 是“顶点输入->顶点着色器”,`fragColor`是“顶点着色器->片段着色器”,所以二者不冲突。
34+
我们之前提过,一个 `location` 只能放一个资源,所以我们的位置和颜色信息需要放在不同的 `location` 中。
35+
注意 `inPosition` 是“顶点输入->顶点着色器”, `fragColor` 是“顶点着色器->片段着色器”,所以二者不冲突。
3636

3737
特殊的是,通常一个槽位最多支持 16 字节,因此某些类型需要多个槽位。比如使用`dvec3`时,后一个变量的索引至少要高 2 :
3838

@@ -207,8 +207,9 @@ vertexInputInfo.setVertexAttributeDescriptions(attributeDescriptions);
207207
## **最后**
208208

209209
现在管线已准备好接受指定格式的顶点数据并将其传递给顶点着色器。
210+
但是如果你启用验证层并运行,会看到它提示没有绑定顶点缓冲。
210211

211-
但是如果你启用验证层并运行,会看到它提示没有绑定顶点缓冲。下一节我们将创建顶点缓冲并将数据移入,保证GPU可以正常访问它
212+
下一节我们将创建顶点缓冲并将数据移入,保证 GPU 可以正常访问它
212213

213214
> 如果你忘记重新编译着色器,可能没有报错。
214215

docs/md/02/01_vertexbuffer.md

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
缓冲(Buffer)在 Vulkan 中是一个用于存储任意数据的区域,它可以被显卡读取。
66
在本节中,我们将用它存储顶点数据。
77
与其他的 Vulkan 对象不同,缓冲并不会自动分配内存。
8-
我们在之前的章节中见识到了Vulkan API的可控性,内存管理也是其中之一。
8+
我们在之前的章节中见识到了 Vulkan API 的可控性,内存管理也是其中之一。
99

1010
> 本教程中有时叫它“缓冲区”,但多数时候直接称为“缓冲”,二者是一样的。
1111
1212
## **创建缓冲**
1313

14-
`initVulkan`中创建一个新的函数`createVertexBuffer`,放在`createCommandBuffers`之前
14+
创建一个新的函数 `createVertexBuffer` ,在 `createCommandBuffers` 之前调用
1515

1616
```cpp
1717
void initVulkan() {
@@ -36,19 +36,19 @@ void createVertexBuffer() {
3636
}
3737
```
3838

39-
然后创建我们熟悉的`CreateInfo`信息并填写它:
39+
然后创建我们熟悉的 `CreateInfo` 信息并填写它:
4040

4141
```cpp
4242
vk::BufferCreateInfo bufferInfo;
4343
```
4444

45-
首先填写了`size`参数,它定义了缓冲的总字节数,我们使用第一个元素的大小乘以总长度。
45+
首先填写了 `size` 参数,它定义了缓冲的总字节数,我们使用第一个元素的大小乘以总长度。
4646

4747
```cpp
4848
bufferInfo.size = sizeof(vertices[0]) * vertices.size();
4949
```
5050

51-
第二个参数是`usage`,它表示这些数据的用途,可以是多个位掩码的组合。
51+
第二个参数是 `usage` ,它表示这些数据的用途,可以是多个位掩码的组合。
5252
我们需要的是顶点缓冲,所以可以这样写:
5353

5454
```cpp
@@ -62,9 +62,9 @@ bufferInfo.usage = vk::BufferUsageFlagBits::eVertexBuffer;
6262
bufferInfo.sharingMode = vk::SharingMode::eExclusive;
6363
```
6464

65-
`flags`参数用于配置稀疏缓冲内存,暂时无需设置,保持默认即可。
65+
`flags` 参数用于配置稀疏缓冲内存,暂时无需设置,保持默认即可。
6666

67-
现在我们可以创建缓冲了,首先创建一个`vk::raii::Buffer`类型的`m_vertexBuffer`成员。
67+
现在可以创建缓冲了,首先创建一个 `vk::raii::Buffer` 类型的 `m_vertexBuffer` 成员。
6868
由于它不依赖交换链,且应该保证它在程序结束前都可用于渲染命令,我们可以把它放在交换链的上方:
6969

7070
```cpp
@@ -77,7 +77,7 @@ std::vector<vk::Image> m_swapChainImages;
7777
// ......
7878
```
7979
80-
然后在`createVertexBuffer`函数中创建它:
80+
然后在 `createVertexBuffer` 函数中创建它:
8181
8282
```cpp
8383
void createVertexBuffer() {
@@ -98,16 +98,16 @@ void createVertexBuffer() {
9898
vk::MemoryRequirements memRequirements = m_vertexBuffer.getMemoryRequirements();
9999
```
100100

101-
`vk::MemoryRequirements`结构体包含如下信息:
101+
`vk::MemoryRequirements` 结构体包含如下信息:
102102

103103
| 成员变量 | 含义 |
104104
|----------|------|
105-
| `size` | 需要的内存大小(字节),可能与`bufferInfo.size`不同 |
106-
| `alignment` | 内存对齐方式,取决于`bufferInfo.usage``bufferInfo.flags` |
105+
| `size` | 需要的内存大小(字节),可能与 `bufferInfo.size` 不同 |
106+
| `alignment` | 内存对齐方式,取决于 `bufferInfo.usage``bufferInfo.flags` |
107107
| `memoryTypeBits` | 适用于缓冲的内存类型的位字段 |
108108

109109
显卡可以分配不同类型的内存\(显存\),这些不同的内存类型可能有不同的操作或性能表现。
110-
我们需要根据需求寻找合适的内存类型,现在让我们创建新函数`findMemoryType`
110+
我们需要根据需求寻找合适的内存类型,现在让我们创建新函数 `findMemoryType`
111111

112112
```cpp
113113
uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) {
@@ -122,8 +122,8 @@ uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties)
122122
auto memProperties = m_physicalDevice.getMemoryProperties();
123123
```
124124

125-
`vk::PhysicalDeviceMemoryProperties`结构体中有两个数组 `memoryTypes``memoryHeaps`
126-
第二个数组“内存堆”是不同的内存资源,比如VRAM耗尽时专用于VRAM和RAM交换的空间。而不同的内存类型存在于这些堆中。
125+
`vk::PhysicalDeviceMemoryProperties` 结构体中有两个数组 `memoryTypes``memoryHeaps`
126+
第二个数组“内存堆”是不同的内存资源,比如 VRAM 耗尽时专用于 VRAM 和 RAM 交换的空间。而不同的内存类型存在于这些堆中。
127127

128128
作为初学者,我们现在只关心类型而不关心它来自哪个堆,但需要记得后者也会影响性能。
129129

@@ -143,7 +143,7 @@ return 0; // optional
143143
我们可以通过简单地迭代并检查相应的位是否为 `1` 来寻找合适的内存类型的索引。
144144
145145
我们还需要保证我们的顶点数据能够写入内存。
146-
`memoryTypes`数组由`vk::MemoryType`结构体组成,这些结构体指定每种内存类型的属性和对应的堆。
146+
`memoryTypes` 数组由 `vk::MemoryType` 结构体组成,这些结构体指定每种内存类型的属性和对应的堆。
147147
148148
现在修改循环以检查属性支持:
149149
@@ -158,7 +158,7 @@ for(uint32_t i = 0; i < memProperties.memoryTypeCount; ++i){
158158

159159
## **内存分配**
160160

161-
现在我们有办法找到正确的内存类型了,将这些信息都记入 `vk::MemoryAllocateInfo` 结构体中。
161+
现在我们可以找到正确的内存类型了,将这些信息都记入 `vk::MemoryAllocateInfo` 结构体:
162162

163163
```cpp
164164
vk::MemoryAllocateInfo allocInfo;
@@ -189,11 +189,11 @@ m_vertexBufferMemory = m_device.allocateMemory( allocInfo );
189189
m_vertexBuffer.bindMemory(m_vertexBufferMemory, 0);
190190
```
191191

192-
右边的`0`是内存区域内的偏移量,通过此变量可以让一块内存分成多个区域。此内存专为顶点缓冲分配,偏移量应为0
192+
右边的 `0` 是内存区域内的偏移量,通过此变量可以让一块内存分成多个区域。此内存专为顶点缓冲分配,偏移量应为 0
193193

194194
## **填充顶点缓冲**
195195

196-
现在是时候把顶点数据拷贝到缓冲中了我们使用`mapMemory`获取内存地址指针:
196+
现在是时候把顶点数据拷贝到缓冲中了我们使用`mapMemory`获取内存地址指针:
197197

198198
```cpp
199199
void* data;
@@ -216,21 +216,21 @@ m_vertexBufferMemory.unmapMemory();
216216
不幸的是,由于缓存或其他因素,驱动程序可能不会立即将数据复制到缓冲内存中,也可能对缓冲区的写入在映射的内存中尚不可见。
217217
有两种解决方案:
218218
219-
- 使用主机一致的内存堆,用`vk::MemoryPropertyFlagBits::eHostCoherent`标记。
219+
- 使用主机一致的内存堆,用 `vk::MemoryPropertyFlagBits::eHostCoherent` 标记。
220220
221-
- 写入映射的内存后立刻调用`flushMappedMemoryRanges`,并在映射的内存读取前调用`invalidateMappedMemoryRanges`。
221+
- 写入映射的内存后立刻调用 `flushMappedMemoryRanges` ,并在映射的内存读取前调用 `invalidateMappedMemoryRanges`
222222
223-
我们使用了第一种方式,在`findMemoryType`函数参数中添加了一致性标志位。
223+
我们使用了第一种方式,在 `findMemoryType` 函数参数中添加了一致性标志位。
224224
不过需要注意的是,这可能比显式刷新的性能略低一些,但我们将在下一章说明为什么这无关紧要。
225225
226-
刷新内存范围和使用一致性内存堆意味着驱动程序可以注意到我们写入了缓冲,但这不意味着在GPU上可见
227-
GPU上的数据转移是个隐含的过程,规范只[告诉我们](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-submission-host-writes),它保证在下次调用`queue.submit`时完成。
226+
刷新内存范围和使用一致性内存堆意味着驱动程序可以注意到我们写入了缓冲,但这不意味着在 GPU 上立即可见
227+
GPU 上的数据转移是个隐含的过程,规范只[告诉我们](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-submission-host-writes),它保证在下次调用 `queue.submit` 时完成。
228228
229229
## **绑定顶点缓冲**
230230
231231
剩下的就是在渲染操作期间绑定顶点缓冲,我们扩展`recordCommandBuffer`函数来执行此操作。
232232
233-
在`draw`语句上方添加一些内容,再修改`draw`语句:
233+
`draw` 语句上方添加一些内容,再修改 `draw` 语句:
234234
235235
```cpp
236236
// ......
@@ -243,8 +243,8 @@ commandBuffer.draw(static_cast<uint32_t>(vertices.size()), 1, 0, 0);
243243
// ......
244244
```
245245

246-
`bindVertexBuffers`的第一个参数指定顶点缓冲绑定点的偏移量。
247-
我们还修改了`draw`,传递缓冲中得到顶点数,而不是硬编码的`3`
246+
`bindVertexBuffers` 的第一个参数指定顶点缓冲绑定点的偏移量。
247+
我们还修改了 `draw` ,传递缓冲中得到顶点数,而不是硬编码的 `3`
248248

249249
## **测试**
250250

docs/md/02/02_stagingbuffer.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
对于显卡而言,最佳内存类型应该有`vk::MemoryPropertyFlagBits::eDeviceLocal`标志。
88
也就是设备本地的内存,实际是指显卡上的内存(显存),通常无法被CPU直接访问。
99

10-
容易理解,CPU访问内存更快、而GPU访问显存更快,交叉访问却比较麻烦,所以我们最好需要两个缓冲区:
10+
容易理解, CPU 访问主存更快、而 GPU 访问显存更快,交叉访问却比较麻烦,所以我们最好需要两个缓冲区:
1111

12-
1. 位于内存,像上一节一样,我们称其为暂存缓冲\(Staging Buffer\)
12+
1. 位于主存,像上一节一样,我们称其为暂存缓冲\(Staging Buffer\)
1313
2. 位于显存,是我们最终需要的顶点缓冲,类型是设备本地缓冲\(Device Local\)
1414

15-
我们的CPU向暂存缓冲写数据,然后通过某种方式将数据从暂存缓冲复制到最终缓冲,然后GPU从最终缓冲中读取(使用最终缓冲绑定顶点输入)。
15+
我们的 CPU 向暂存缓冲写数据,然后通过某种方式将数据从暂存缓冲复制到最终缓冲,然后 GPU 从最终缓冲中读取(使用最终缓冲绑定顶点输入)。
1616

1717
## **转移队列**
1818

@@ -139,7 +139,7 @@ void copyBuffer(vk::raii::Buffer& srcBuffer, vk::raii::Buffer& dstBuffer, vk::De
139139
```
140140
141141
内存传输操作需要使用命令缓冲,我们必须分配一个临时的命令缓冲用于命令的录制和提交。
142-
同时最好为这些临时命令缓冲创建一个独立的命令池,程序可以更好进行资源分配的优化,此时需要使用`vk::CommandBufferLevel::ePrimary`标记。
142+
同时最好为这些临时命令缓冲创建一个独立的命令池,程序可以更好进行资源分配的优化,此时需要使用 `vk::CommandBufferLevel::ePrimary` 标记。
143143
144144
```cpp
145145
void copyBuffer(vk::raii::Buffer& srcBuffer, vk::raii::Buffer& dstBuffer, vk::DeviceSize size) {
@@ -182,7 +182,7 @@ commandBuffer.end();
182182
```
183183

184184
与绘制命令不同,我们只希望它立刻执行内存传输命令。
185-
我们至少有两种方式等待内存传输完成,使用围栏`Fence`进行同步或直接`waitIdle`,这里使用后者:
185+
我们至少有两种方式等待内存传输完成,使用围栏 `Fence` 进行同步或直接 `waitIdle` ,这里使用后者:
186186

187187
```cpp
188188
vk::SubmitInfo submitInfo;
@@ -204,15 +204,18 @@ copyBuffer(stagingBuffer, m_vertexBuffer, bufferSize);
204204
205205
现在尝试运行程序,保证程序可以正常执行。
206206
207-
### **注意**
207+
---
208+
209+
## **注意**
208210
209-
在实际应用中,你不应该给每个缓冲都调用一次`allocateMemory`。
211+
在实际应用中,你不应该给每个缓冲都调用一次 `allocateMemory`
210212
同时为大量对象分配内存的正确方法是创建一个自定义分配器,该分配器通过使用我们在许多函数中看到的 `offset` 参数将单个资源拆分到许多不同的对象中。
211213
212214
你可以自己实现这样的分配器,也可以使用 [VulkanMemoryAllocator-Hpp](https://github.com/YaaZ/VulkanMemoryAllocator-Hpp) 这样的第三方库。
213215
214-
但是对于本教程,为每个资源使用单独的分配是可以的。因为我们的数据量很小,不会触发相关限制
216+
但是对于本教程,为每个资源使用单独的分配是可以的,因为我们的数据量很小
215217
218+
> 我们将在进阶章节详细讨论内存分配器内容。
216219
217220
---
218221

docs/md/02/10_indexbuffer.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ inline static const std::vector<uint16_t> indices = {
3636
};
3737
```
3838

39-
你可以使用`uint16_t``uint32_t`,我们这里使用前者,因为我们需要的索引数很少(不超过65535)。
39+
你可以使用 `uint16_t``uint32_t` ,我们这里使用前者,因为我们需要的索引数很少(不超过65535)。
4040

4141
和顶点缓冲一样,我们也需要索引缓冲和缓冲内存。现在添加两个成员变量:
4242

@@ -47,7 +47,7 @@ vk::raii::DeviceMemory m_indexBufferMemory{ nullptr };
4747
vk::raii::Buffer m_indexBuffer{ nullptr };
4848
```
4949
50-
然后创建一个`createIndexBuffer`函数,内容参考`createVertexBuffer`:
50+
然后创建一个 `createIndexBuffer` 函数,内容参考 `createVertexBuffer`
5151
5252
```cpp
5353
void initVulkan() {
@@ -87,7 +87,7 @@ void createIndexBuffer() {
8787
```
8888

8989
这个顶点缓冲的创建代码基本一致,我们将顶点缓冲的变量换成了索引缓冲的。
90-
唯一需要注意的是,创建索引缓冲时修改了`vk::BufferUsageFlagBits`,使用`eIndexBuffer`而不是`eVertexBuffer`
90+
唯一需要注意的是,创建索引缓冲时修改了 `vk::BufferUsageFlagBits` ,使用 `eIndexBuffer` 而不是 `eVertexBuffer`
9191

9292
## **使用索引缓冲**
9393

@@ -104,7 +104,7 @@ commandBuffer.bindIndexBuffer( m_indexBuffer, 0, vk::IndexType::eUint16 );
104104
我们略微调整了顶点缓冲的绑定代码,因为我们只有一个顶点缓冲。
105105
索引缓冲的第二个参数是偏移量,第三个参数是索引使用的类型。
106106
107-
仅仅绑定是不够的,我们需要使用`drawIndexed`代替`draw`。
107+
仅仅绑定是不够的,我们需要使用 `drawIndexed` 代替 `draw`
108108
109109
```cpp
110110
// commandBuffer.draw(static_cast<uint32_t>(vertices.size()), 1, 0, 0);
@@ -121,15 +121,14 @@ commandBuffer.drawIndexed(static_cast<uint32_t>(indices.size()), 1, 0, 0, 0);
121121

122122
![矩形](../../images/0210/indexed_rectangle.png)
123123

124-
你现在已经知道了如何使用索引缓冲节省内存,这非常重要,因为我们后面会导入复杂的3D模型。
125-
126-
> 你大概还发现了一点,顶点缓冲需要在管线创建时添加描述信息,但索引缓冲不需要。
127-
> 因为索引数据的内容足够简单,格式由 drawIndexed 调用时指定,你完全可以修改索引而不影响管线。
124+
你现在已经知道了如何使用索引缓冲节省内存,这非常重要,因为我们后面会导入复杂的 3D 模型。
128125

129126
---
130127

128+
## **注意**
129+
131130
前一章的末尾提到了你应该使用内存分配器,从单个内存分配多个资源。
132-
但实际上你还应该更进一步,[驱动程序开发者建议](https://developer.nvidia.com/vulkan-memory-management)你将多个缓冲区合并到一个`vk::Buffer`中,并在使用时通过偏移量区分不同的缓冲区。
131+
但实际上你还应该更进一步,[驱动程序开发者建议](https://developer.nvidia.com/vulkan-memory-management)你将多个缓冲区合并到一个 `vk::Buffer` 中,并在使用时通过偏移量区分不同的缓冲区。
133132
这样做对于缓存更加友好,因为数据更加紧凑。
134133

135134
如果多个资源在同一渲染操作期间未使用,甚至可以重用相同的内存块,当然前提是它们的数据已刷新。

docs/md/02/20_descriptor1.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -299,11 +299,9 @@ ubo.model = glm::rotate(
299299
);
300300
```
301301

302-
`glm::rotate` 函数接受现有变换、旋转角度和旋转轴作为参数。
303-
`glm::mat4(1.0f)` 构造函数返回一个单位矩阵。
304-
使用 `time * glm::radians(90.0f)` 的旋转角度实现了每秒旋转 90 度的目的。
302+
`glm::rotate` 函数接受现有变换、旋转角度和旋转轴作为参数,`glm::mat4(1.0f)` 构造函数返回一个单位矩阵,使用 `time * glm::radians(90.0f)` 的旋转角度实现了每秒旋转 90 度的目的。
305303

306-
对于视图变换,我决定从上方以 45 度角观察几何体。
304+
对于视图变换,我们可以从上方以 45 度角观察几何体。
307305
`glm::lookAt` 函数接受眼睛位置、中心位置和向上轴作为参数。
308306

309307
```cpp

docs/md/02/21_descriptor2.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,23 @@ void createDescriptorPool() {
2525
}
2626
```
2727

28-
首先,我们需要使用`vk::DescriptorPoolSize`结构体描述我们需要的描述符类型以及数量。
28+
首先,我们需要使用 `vk::DescriptorPoolSize` 结构体描述我们需要的描述符类型以及数量。
2929

3030
```cpp
3131
vk::DescriptorPoolSize poolSize;
3232
poolSize.type = vk::DescriptorType::eUniformBuffer;
3333
poolSize.descriptorCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
3434
```
3535

36-
我们会为每个帧分配一个描述符。上面的`poolSize`结构体会被`CreateInfo`引用:
36+
我们会为每个帧分配一个描述符。上面的 `poolSize` 结构体会被 `CreateInfo` 引用:
3737

3838
```cpp
3939
vk::DescriptorPoolCreateInfo poolInfo;
4040
poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet;
4141
poolInfo.setPoolSizes( poolSize );
4242
```
4343

44-
我们使用了RAII封装,必须指定`eFreeDescriptorSet`标志位,从而在描述符集合释放时将控制权交还给描述符池。
44+
我们使用了RAII封装,必须指定 `eFreeDescriptorSet` 标志位,从而在描述符集合释放时将控制权交还给描述符池。
4545

4646
除了可用的单种描述符的最大数量外,我们还需要指定可能分配的描述符集合的最大数量:
4747

@@ -52,7 +52,7 @@ poolInfo.maxSets = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
5252
> 假如 `descriptorCount` 是 4 ,而 `maxSets` 是 2 。
5353
> 那么你可以分配 2 个描述符集,每个集包含 2 个描述符;或只分配 1 个描述符集,它包含 1~4 个描述符。(描述符总数不超过4。)
5454
55-
`m_descriptorSetLayout`下方添加一个新的类成员来存储描述符池的句柄,并调用 `createDescriptorPool` 来创建它。
55+
`m_descriptorSetLayout` 下方添加一个新的类成员来存储描述符池的句柄,并调用 `createDescriptorPool` 来创建它。
5656

5757
```cpp
5858
vk::raii::DescriptorSetLayout m_descriptorSetLayout{ nullptr };
@@ -331,7 +331,9 @@ struct alignas(16) UniformBufferObject {
331331
然后,着色器可以像这样引用特定的描述符集合:
332332

333333
```glsl
334-
layout(set = 0, binding = 0) uniform UniformBufferObject { ... }
334+
// 通过 set 指定描述符集的索引,通过 binding 指定描述符绑定位
335+
layout(set = 0, binding = 0) uniform UniformBufferObject { ... }; // 帧数据
336+
layout(set = 1, binding = 0) uniform sampler2D tex; // 材质数据
335337
```
336338
337339
您可以使用此功能将每个对象变化的描述符和共享的描述符放入单独的描述符集合中。

0 commit comments

Comments
 (0)