Skip to content

Commit b6e95a7

Browse files
authored
Descriptor sets, view UBO, instance SSBO, Texture (#16)
* Pipeline Layout * ShaderBuffer * Texture * Link to mip-mapping sample * Add `Transform`, view matrix * Instanced rendering * Fixups
1 parent 811b904 commit b6e95a7

24 files changed

+1181
-12
lines changed

assets/shader.frag

280 Bytes
Binary file not shown.

assets/shader.vert

912 Bytes
Binary file not shown.

guide/src/SUMMARY.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,9 @@
4343
- [Command Block](memory/command_block.md)
4444
- [Device Buffers](memory/device_buffers.md)
4545
- [Images](memory/images.md)
46+
- [Descriptor Sets](descriptor_sets/README.md)
47+
- [Pipeline Layout](descriptor_sets/pipeline_layout.md)
48+
- [Shader Buffer](descriptor_sets/shader_buffer.md)
49+
- [Texture](descriptor_sets/texture.md)
50+
- [View Matrix](descriptor_sets/view_matrix.md)
51+
- [Instanced Rendering](descriptor_sets/instanced_rendering.md)

guide/src/descriptor_sets/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Descriptor Sets
2+
3+
[Vulkan Descriptor](https://docs.vulkan.org/guide/latest/mapping_data_to_shaders.html#descriptors)s are essentially typed pointers to resources that shaders can use, eg uniform/storage buffers or combined image samplers (textures with samplers). A Descriptor Set is a collection of descriptors at various **bindings** that is bound together as an atomic unit. Shaders can declare input based on these set and binding numbers, and any sets the shader uses must have been updated and bound before drawing. A Descriptor Set Layout is a description of a collection of descriptor sets associated with a particular set number, usually describing all the sets in a shader. Descriptor sets are allocated using a Descriptor Pool and the desired set layout(s).
4+
5+
Structuring set layouts and managing descriptor sets are complex topics with many viable approaches, each with their pros and cons. Some robust ones are described in this [page](https://docs.vulkan.org/samples/latest/samples/performance/descriptor_management/README.html). 2D frameworks - and even simple/basic 3D ones - can simply allocate and update sets every frame, as described in the docs as the "simplest approach". Here's an [extremely detailed](https://zeux.io/2020/02/27/writing-an-efficient-vulkan-renderer/) - albeit a bit dated now - post by Arseny on the subject. A more modern approach, namely "bindless" or Descriptor Indexing, is described in the official docs [here](https://docs.vulkan.org/samples/latest/samples/extensions/descriptor_indexing/README.html).
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Instanced Rendering
2+
3+
When multiple copies of a drawable object are desired, one option is to use instanced rendering. The basic idea is to store per-instance data in a uniform/storage buffer and index into it in the vertex shader. We shall represent one model matrix per instance, feel free to add more data like an overall tint (color) that gets multiplied to the existing output color in the fragment shader. This will be bound to a Storage Buffer (SSBO), which can be "unbounded" in the shader (size is determined during invocation).
4+
5+
Store the SSBO and a buffer for instance matrices:
6+
7+
```cpp
8+
std::vector<glm::mat4> m_instance_data{}; // model matrices.
9+
std::optional<ShaderBuffer> m_instance_ssbo{};
10+
```
11+
12+
Add two `Transform`s as the source of rendering instances, and a function to update the matrices:
13+
14+
```cpp
15+
void update_instances();
16+
17+
// ...
18+
std::array<Transform, 2> m_instances{}; // generates model matrices.
19+
20+
// ...
21+
void App::update_instances() {
22+
m_instance_data.clear();
23+
m_instance_data.reserve(m_instances.size());
24+
for (auto const& transform : m_instances) {
25+
m_instance_data.push_back(transform.model_matrix());
26+
}
27+
// can't use bit_cast anymore, reinterpret data as a byte array instead.
28+
auto const span = std::span{m_instance_data};
29+
void* data = span.data();
30+
auto const bytes =
31+
std::span{static_cast<std::byte const*>(data), span.size_bytes()};
32+
m_instance_ssbo->write_at(m_frame_index, bytes);
33+
}
34+
```
35+
36+
Update the descriptor pool to also provide storage buffers:
37+
38+
```cpp
39+
// ...
40+
vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2},
41+
vk::DescriptorPoolSize{vk::DescriptorType::eStorageBuffer, 2},
42+
```
43+
44+
This time add a new binding to set 1 (instead of a new set):
45+
46+
```cpp
47+
static constexpr auto set_1_bindings_v = std::array{
48+
layout_binding(0, vk::DescriptorType::eCombinedImageSampler),
49+
layout_binding(1, vk::DescriptorType::eStorageBuffer),
50+
};
51+
```
52+
53+
Create the instance SSBO after the view UBO:
54+
55+
```cpp
56+
m_instance_ssbo.emplace(m_allocator.get(), m_gpu.queue_family,
57+
vk::BufferUsageFlagBits::eStorageBuffer);
58+
```
59+
60+
Call `update_instances()` after `update_view()`:
61+
62+
```cpp
63+
// ...
64+
update_view();
65+
update_instances();
66+
```
67+
68+
Extract transform inspection into a lambda and inspect each instance transform too:
69+
70+
```cpp
71+
static auto const inspect_transform = [](Transform& out) {
72+
ImGui::DragFloat2("position", &out.position.x);
73+
ImGui::DragFloat("rotation", &out.rotation);
74+
ImGui::DragFloat2("scale", &out.scale.x, 0.1f);
75+
};
76+
77+
ImGui::Separator();
78+
if (ImGui::TreeNode("View")) {
79+
inspect_transform(m_view_transform);
80+
ImGui::TreePop();
81+
}
82+
83+
ImGui::Separator();
84+
if (ImGui::TreeNode("Instances")) {
85+
for (std::size_t i = 0; i < m_instances.size(); ++i) {
86+
auto const label = std::to_string(i);
87+
if (ImGui::TreeNode(label.c_str())) {
88+
inspect_transform(m_instances.at(i));
89+
ImGui::TreePop();
90+
}
91+
}
92+
ImGui::TreePop();
93+
}
94+
```
95+
96+
Add another descriptor write for the SSBO:
97+
98+
```cpp
99+
auto writes = std::array<vk::WriteDescriptorSet, 3>{};
100+
// ...
101+
auto const instance_ssbo_info =
102+
m_instance_ssbo->descriptor_info_at(m_frame_index);
103+
write.setBufferInfo(instance_ssbo_info)
104+
.setDescriptorType(vk::DescriptorType::eStorageBuffer)
105+
.setDescriptorCount(1)
106+
.setDstSet(set1)
107+
.setDstBinding(1);
108+
writes[2] = write;
109+
```
110+
111+
Finally, change the instance count in the draw call:
112+
113+
```cpp
114+
auto const instances = static_cast<std::uint32_t>(m_instances.size());
115+
// m_vbo has 6 indices.
116+
command_buffer.drawIndexed(6, instances, 0, 0, 0);
117+
```
118+
119+
Update the vertex shader to incorporate the instance model matrix:
120+
121+
```glsl
122+
// ...
123+
layout (set = 1, binding = 1) readonly buffer Instances {
124+
mat4 mat_ms[];
125+
};
126+
127+
// ...
128+
const mat4 mat_m = mat_ms[gl_InstanceIndex];
129+
const vec4 world_pos = mat_m * vec4(a_pos, 0.0, 1.0);
130+
```
131+
132+
![Instanced Rendering](./instanced_rendering.png)
Loading
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Pipeline Layout
2+
3+
A [Vulkan Pipeline Layout](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineLayout.html) represents a sequence of descriptor sets (and push constants) associated with a shader program. Even when using Shader Objects, a Pipeline Layout is needed to utilize descriptor sets.
4+
5+
Starting with the layout of a single descriptor set containing a uniform buffer to set the view/projection matrices in, store a descriptor pool in `App` and create it before the shader:
6+
7+
```cpp
8+
vk::UniqueDescriptorPool m_descriptor_pool{};
9+
10+
// ...
11+
void App::create_descriptor_pool() {
12+
static constexpr auto pool_sizes_v = std::array{
13+
// 2 uniform buffers, can be more if desired.
14+
vk::DescriptorPoolSize{vk::DescriptorType::eUniformBuffer, 2},
15+
};
16+
auto pool_ci = vk::DescriptorPoolCreateInfo{};
17+
// allow 16 sets to be allocated from this pool.
18+
pool_ci.setPoolSizes(pool_sizes_v).setMaxSets(16);
19+
m_descriptor_pool = m_device->createDescriptorPoolUnique(pool_ci);
20+
}
21+
```
22+
23+
Add new members to `App` to store the set layouts and pipeline layout. `m_set_layout_views` is just a copy of the descriptor set layout handles in a contiguous vector:
24+
25+
```cpp
26+
std::vector<vk::UniqueDescriptorSetLayout> m_set_layouts{};
27+
std::vector<vk::DescriptorSetLayout> m_set_layout_views{};
28+
vk::UniquePipelineLayout m_pipeline_layout{};
29+
30+
// ...
31+
constexpr auto layout_binding(std::uint32_t binding,
32+
vk::DescriptorType const type) {
33+
return vk::DescriptorSetLayoutBinding{
34+
binding, type, 1, vk::ShaderStageFlagBits::eAllGraphics};
35+
}
36+
37+
// ...
38+
void App::create_pipeline_layout() {
39+
static constexpr auto set_0_bindings_v = std::array{
40+
layout_binding(0, vk::DescriptorType::eUniformBuffer),
41+
};
42+
auto set_layout_cis = std::array<vk::DescriptorSetLayoutCreateInfo, 1>{};
43+
set_layout_cis[0].setBindings(set_0_bindings_v);
44+
45+
for (auto const& set_layout_ci : set_layout_cis) {
46+
m_set_layouts.push_back(
47+
m_device->createDescriptorSetLayoutUnique(set_layout_ci));
48+
m_set_layout_views.push_back(*m_set_layouts.back());
49+
}
50+
51+
auto pipeline_layout_ci = vk::PipelineLayoutCreateInfo{};
52+
pipeline_layout_ci.setSetLayouts(m_set_layout_views);
53+
m_pipeline_layout =
54+
m_device->createPipelineLayoutUnique(pipeline_layout_ci);
55+
}
56+
```
57+
58+
Add a helper function that allocates a set of descriptor sets for the entire layout:
59+
60+
```cpp
61+
auto App::allocate_sets() const -> std::vector<vk::DescriptorSet> {
62+
auto allocate_info = vk::DescriptorSetAllocateInfo{};
63+
allocate_info.setDescriptorPool(*m_descriptor_pool)
64+
.setSetLayouts(m_set_layout_views);
65+
return m_device->allocateDescriptorSets(allocate_info);
66+
}
67+
```
68+
69+
Store a Buffered copy of descriptor sets for one drawable object:
70+
71+
```cpp
72+
Buffered<std::vector<vk::DescriptorSet>> m_descriptor_sets{};
73+
74+
// ...
75+
76+
void App::create_descriptor_sets() {
77+
for (auto& descriptor_sets : m_descriptor_sets) {
78+
descriptor_sets = allocate_sets();
79+
}
80+
}
81+
```
22.1 KB
Loading

0 commit comments

Comments
 (0)