Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified assets/shader.frag
Binary file not shown.
Binary file modified assets/shader.vert
Binary file not shown.
6 changes: 6 additions & 0 deletions guide/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,9 @@
- [Command Block](memory/command_block.md)
- [Device Buffers](memory/device_buffers.md)
- [Images](memory/images.md)
- [Descriptor Sets](descriptor_sets/README.md)
- [Pipeline Layout](descriptor_sets/pipeline_layout.md)
- [Shader Buffer](descriptor_sets/shader_buffer.md)
- [Texture](descriptor_sets/texture.md)
- [View Matrix](descriptor_sets/view_matrix.md)
- [Instanced Rendering](descriptor_sets/instanced_rendering.md)
5 changes: 5 additions & 0 deletions guide/src/descriptor_sets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Descriptor Sets

[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).

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).
132 changes: 132 additions & 0 deletions guide/src/descriptor_sets/instanced_rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Instanced Rendering

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).

Store the SSBO and a buffer for instance matrices:

```cpp
std::vector<glm::mat4> m_instance_data{}; // model matrices.
std::optional<ShaderBuffer> m_instance_ssbo{};
```

Add two `Transform`s as the source of rendering instances, and a function to update the matrices:

```cpp
void update_instances();

// ...
std::array<Transform, 2> m_instances{}; // generates model matrices.

// ...
void App::update_instances() {
m_instance_data.clear();
m_instance_data.reserve(m_instances.size());
for (auto const& transform : m_instances) {
m_instance_data.push_back(transform.model_matrix());
}
// can't use bit_cast anymore, reinterpret data as a byte array instead.
auto const span = std::span{m_instance_data};
void* data = span.data();
auto const bytes =
std::span{static_cast<std::byte const*>(data), span.size_bytes()};
m_instance_ssbo->write_at(m_frame_index, bytes);
}
```

Update the descriptor pool to also provide storage buffers:

```cpp
// ...
vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2},
vk::DescriptorPoolSize{vk::DescriptorType::eStorageBuffer, 2},
```

This time add a new binding to set 1 (instead of a new set):

```cpp
static constexpr auto set_1_bindings_v = std::array{
layout_binding(0, vk::DescriptorType::eCombinedImageSampler),
layout_binding(1, vk::DescriptorType::eStorageBuffer),
};
```

Create the instance SSBO after the view UBO:

```cpp
m_instance_ssbo.emplace(m_allocator.get(), m_gpu.queue_family,
vk::BufferUsageFlagBits::eStorageBuffer);
```

Call `update_instances()` after `update_view()`:

```cpp
// ...
update_view();
update_instances();
```

Extract transform inspection into a lambda and inspect each instance transform too:

```cpp
static auto const inspect_transform = [](Transform& out) {
ImGui::DragFloat2("position", &out.position.x);
ImGui::DragFloat("rotation", &out.rotation);
ImGui::DragFloat2("scale", &out.scale.x, 0.1f);
};

ImGui::Separator();
if (ImGui::TreeNode("View")) {
inspect_transform(m_view_transform);
ImGui::TreePop();
}

ImGui::Separator();
if (ImGui::TreeNode("Instances")) {
for (std::size_t i = 0; i < m_instances.size(); ++i) {
auto const label = std::to_string(i);
if (ImGui::TreeNode(label.c_str())) {
inspect_transform(m_instances.at(i));
ImGui::TreePop();
}
}
ImGui::TreePop();
}
```

Add another descriptor write for the SSBO:

```cpp
auto writes = std::array<vk::WriteDescriptorSet, 3>{};
// ...
auto const instance_ssbo_info =
m_instance_ssbo->descriptor_info_at(m_frame_index);
write.setBufferInfo(instance_ssbo_info)
.setDescriptorType(vk::DescriptorType::eStorageBuffer)
.setDescriptorCount(1)
.setDstSet(set1)
.setDstBinding(1);
writes[2] = write;
```

Finally, change the instance count in the draw call:

```cpp
auto const instances = static_cast<std::uint32_t>(m_instances.size());
// m_vbo has 6 indices.
command_buffer.drawIndexed(6, instances, 0, 0, 0);
```

Update the vertex shader to incorporate the instance model matrix:

```glsl
// ...
layout (set = 1, binding = 1) readonly buffer Instances {
mat4 mat_ms[];
};

// ...
const mat4 mat_m = mat_ms[gl_InstanceIndex];
const vec4 world_pos = mat_m * vec4(a_pos, 0.0, 1.0);
```

![Instanced Rendering](./instanced_rendering.png)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions guide/src/descriptor_sets/pipeline_layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Pipeline Layout

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.

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:

```cpp
vk::UniqueDescriptorPool m_descriptor_pool{};

// ...
void App::create_descriptor_pool() {
static constexpr auto pool_sizes_v = std::array{
// 2 uniform buffers, can be more if desired.
vk::DescriptorPoolSize{vk::DescriptorType::eUniformBuffer, 2},
};
auto pool_ci = vk::DescriptorPoolCreateInfo{};
// allow 16 sets to be allocated from this pool.
pool_ci.setPoolSizes(pool_sizes_v).setMaxSets(16);
m_descriptor_pool = m_device->createDescriptorPoolUnique(pool_ci);
}
```

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:

```cpp
std::vector<vk::UniqueDescriptorSetLayout> m_set_layouts{};
std::vector<vk::DescriptorSetLayout> m_set_layout_views{};
vk::UniquePipelineLayout m_pipeline_layout{};

// ...
constexpr auto layout_binding(std::uint32_t binding,
vk::DescriptorType const type) {
return vk::DescriptorSetLayoutBinding{
binding, type, 1, vk::ShaderStageFlagBits::eAllGraphics};
}

// ...
void App::create_pipeline_layout() {
static constexpr auto set_0_bindings_v = std::array{
layout_binding(0, vk::DescriptorType::eUniformBuffer),
};
auto set_layout_cis = std::array<vk::DescriptorSetLayoutCreateInfo, 1>{};
set_layout_cis[0].setBindings(set_0_bindings_v);

for (auto const& set_layout_ci : set_layout_cis) {
m_set_layouts.push_back(
m_device->createDescriptorSetLayoutUnique(set_layout_ci));
m_set_layout_views.push_back(*m_set_layouts.back());
}

auto pipeline_layout_ci = vk::PipelineLayoutCreateInfo{};
pipeline_layout_ci.setSetLayouts(m_set_layout_views);
m_pipeline_layout =
m_device->createPipelineLayoutUnique(pipeline_layout_ci);
}
```

Add a helper function that allocates a set of descriptor sets for the entire layout:

```cpp
auto App::allocate_sets() const -> std::vector<vk::DescriptorSet> {
auto allocate_info = vk::DescriptorSetAllocateInfo{};
allocate_info.setDescriptorPool(*m_descriptor_pool)
.setSetLayouts(m_set_layout_views);
return m_device->allocateDescriptorSets(allocate_info);
}
```

Store a Buffered copy of descriptor sets for one drawable object:

```cpp
Buffered<std::vector<vk::DescriptorSet>> m_descriptor_sets{};

// ...

void App::create_descriptor_sets() {
for (auto& descriptor_sets : m_descriptor_sets) {
descriptor_sets = allocate_sets();
}
}
```
Binary file added guide/src/descriptor_sets/rgby_texture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading