From 9ccd0d94c14074a79040768a503ff743570192e7 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Sun, 30 Mar 2025 19:20:29 -0700 Subject: [PATCH 1/7] Pipeline Layout --- guide/src/SUMMARY.md | 2 + guide/src/descriptor_sets/README.md | 5 ++ guide/src/descriptor_sets/pipeline_layout.md | 81 ++++++++++++++++++++ src/app.cpp | 53 ++++++++++++- src/app.hpp | 11 ++- 5 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 guide/src/descriptor_sets/README.md create mode 100644 guide/src/descriptor_sets/pipeline_layout.md diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index a465269..3a89eb4 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -43,3 +43,5 @@ - [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) diff --git a/guide/src/descriptor_sets/README.md b/guide/src/descriptor_sets/README.md new file mode 100644 index 0000000..ecdea30 --- /dev/null +++ b/guide/src/descriptor_sets/README.md @@ -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). diff --git a/guide/src/descriptor_sets/pipeline_layout.md b/guide/src/descriptor_sets/pipeline_layout.md new file mode 100644 index 0000000..9e16f3e --- /dev/null +++ b/guide/src/descriptor_sets/pipeline_layout.md @@ -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 m_set_layouts{}; +std::vector 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{}; + 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 { + 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> m_descriptor_sets{}; + +// ... + +void App::create_descriptor_sets() { + for (auto& descriptor_sets : m_descriptor_sets) { + descriptor_sets = allocate_sets(); + } +} +``` diff --git a/src/app.cpp b/src/app.cpp index 3f5839e..32478b3 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -18,6 +18,12 @@ template return std::bit_cast>(t); } +constexpr auto layout_binding(std::uint32_t binding, + vk::DescriptorType const type) { + return vk::DescriptorSetLayoutBinding{ + binding, type, 1, vk::ShaderStageFlagBits::eAllGraphics}; +} + [[nodiscard]] auto locate_assets_dir() -> fs::path { // look for '/assets/', starting from the working // directory and walking up the parent directory tree. @@ -88,10 +94,12 @@ void App::run() { create_swapchain(); create_render_sync(); create_imgui(); + create_descriptor_pool(); + create_pipeline_layout(); create_shader(); create_cmd_block_pool(); - create_vertex_buffer(); + create_descriptor_sets(); main_loop(); } @@ -243,6 +251,36 @@ void App::create_allocator() { m_allocator = vma::create_allocator(*m_instance, m_gpu.device, *m_device); } +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); +} + +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{}; + 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); +} + void App::create_shader() { auto const vertex_spirv = to_spir_v(asset_path("shader.vert")); auto const fragment_spirv = to_spir_v(asset_path("shader.frag")); @@ -298,6 +336,12 @@ void App::create_vertex_buffer() { }; m_vbo = vma::create_device_buffer(buffer_ci, create_command_block(), total_bytes_v); + + +void App::create_descriptor_sets() { + for (auto& descriptor_sets : m_descriptor_sets) { + descriptor_sets = allocate_sets(); + } } auto App::asset_path(std::string_view const uri) const -> fs::path { @@ -308,6 +352,13 @@ auto App::create_command_block() const -> CommandBlock { return CommandBlock{*m_device, m_queue, *m_cmd_block_pool}; } +auto App::allocate_sets() const -> std::vector { + auto allocate_info = vk::DescriptorSetAllocateInfo{}; + allocate_info.setDescriptorPool(*m_descriptor_pool) + .setSetLayouts(m_set_layout_views); + return m_device->allocateDescriptorSets(allocate_info); +} + void App::main_loop() { while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) { glfwPollEvents(); diff --git a/src/app.hpp b/src/app.hpp index 5bd094d..6811dd4 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -38,12 +38,15 @@ class App { void create_render_sync(); void create_imgui(); void create_allocator(); + void create_descriptor_pool(); + void create_pipeline_layout(); void create_shader(); void create_cmd_block_pool(); - void create_vertex_buffer(); + void create_descriptor_sets(); [[nodiscard]] auto asset_path(std::string_view uri) const -> fs::path; [[nodiscard]] auto create_command_block() const -> CommandBlock; + [[nodiscard]] auto allocate_sets() const -> std::vector; void main_loop(); @@ -82,9 +85,15 @@ class App { std::optional m_imgui{}; + vk::UniqueDescriptorPool m_descriptor_pool{}; + std::vector m_set_layouts{}; + std::vector m_set_layout_views{}; + vk::UniquePipelineLayout m_pipeline_layout{}; + std::optional m_shader{}; vma::Buffer m_vbo{}; + Buffered> m_descriptor_sets{}; glm::ivec2 m_framebuffer_size{}; std::optional m_render_target{}; From 82d5dbd479cc3a916978aa44277f240650b4c8ff Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Sun, 30 Mar 2025 19:32:17 -0700 Subject: [PATCH 2/7] ShaderBuffer --- assets/shader.vert | Bin 1156 -> 1436 bytes guide/src/SUMMARY.md | 1 + guide/src/descriptor_sets/shader_buffer.md | 173 +++++++++++++++++++++ guide/src/descriptor_sets/view_ubo.png | Bin 0 -> 52258 bytes src/app.cpp | 48 +++++- src/app.hpp | 6 + src/glsl/shader.vert | 8 +- src/shader_buffer.cpp | 43 +++++ src/shader_buffer.hpp | 30 ++++ 9 files changed, 301 insertions(+), 8 deletions(-) create mode 100644 guide/src/descriptor_sets/shader_buffer.md create mode 100644 guide/src/descriptor_sets/view_ubo.png create mode 100644 src/shader_buffer.cpp create mode 100644 src/shader_buffer.hpp diff --git a/assets/shader.vert b/assets/shader.vert index ca41ab9d6f7b9492dd8230e282fa74e09a50c8ab..6236adb3617f30422576528cd800d8d7bcd643b4 100644 GIT binary patch literal 1436 zcmYk6T~E|N6ozMa>4G32A|EQQT^Ibq5HHl27&S&sz1Rc@3AfFr+n7m8yJ@?iUilaN zOa3ZvOnjfVv+j_?%=?~q&c~cWr#)CSre#*ls(EJWwPD&~jJR&K2g9@BQBjVMUc7vc zV$F0aA)0k_tvWC0_r=Jjfa|g?*^aCy>*%Mg{kJYGm}L_T!{GQh7=8|;$s{bw?@1gc zHjTrv&Eu%Fmod7fIh{?TWICP~cv8&g^o&n@qb$kt zMnj#WhVj(%p77TsJc;x3I4|RGf?{i)Grp{_rOi^YiW9b7lH)OBJ0O&-gIvxTl?*_^AV2|9#eZ?hE@e;*!T5&W`OrFOw eV*b({@Dmvtc;e9ip^W$UW#j>)`>Tpy%l-iOCD>aQXHqzf%GW1>zR-P}Z->p1E6<6-$XOXJMuY234e zG%4+4if(F5XY(W*^~ME(6yt9OCUBkfi=i!T;rc*BFWp$IJR15cMrE87Sut>2%k|l< znLsnIiNDX{+jQ_d9hB)GL9rE2OwK>kHj_@zE*x+3^3ML#1l*}|%&y#JX8TumSa$Oy zeao>)a-%GJ%LUpOd*ej)3|kd+m>I?~dj``(q`UObn?E%#F^4^HJckwz?>X+k(Ua$h z;n?-Es-oGE0-ktPQCmKgtx2UHlf&df;VF4I3_K&xo8Xfg`#!ioFP?hTN54?V@aVzJ z@ADrNQ_ttY>;sN^%ktEt{}p|Hk`HB^?=@agjDAyiRUSRumLm4&Ymmd}Yh9im$kP|N z`mI;*YzT)k>hccwi;M&2jjs;97$Gtkz bytes); + + [[nodiscard]] auto descriptor_info_at(std::size_t frame_index) const + -> vk::DescriptorBufferInfo; + + private: + struct Buffer { + vma::Buffer buffer{}; + vk::DeviceSize size{}; + }; + + void write_to(Buffer& out, std::span bytes) const; + + VmaAllocator m_allocator{}; + std::uint32_t m_queue_family{}; + vk::BufferUsageFlags m_usage{}; + Buffered m_buffers{}; +}; +``` + +The implementation is fairly straightforward, it reuses existing buffers if they are large enough, else recreates them before copying data. It also ensures buffers are always valid to be bound to descriptors. + +```cpp +ShaderBuffer::ShaderBuffer(VmaAllocator allocator, + std::uint32_t const queue_family, + vk::BufferUsageFlags const usage) + : m_allocator(allocator), m_queue_family(queue_family), m_usage(usage) { + // ensure buffers are created and can be bound after returning. + for (auto& buffer : m_buffers) { write_to(buffer, {}); } +} + +void ShaderBuffer::write_at(std::size_t const frame_index, + std::span bytes) { + write_to(m_buffers.at(frame_index), bytes); +} + +auto ShaderBuffer::descriptor_info_at(std::size_t const frame_index) const + -> vk::DescriptorBufferInfo { + auto const& buffer = m_buffers.at(frame_index); + auto ret = vk::DescriptorBufferInfo{}; + ret.setBuffer(buffer.buffer.get().buffer).setRange(buffer.size); + return ret; +} + +void ShaderBuffer::write_to(Buffer& out, + std::span bytes) const { + static constexpr auto blank_byte_v = std::array{std::byte{}}; + // fallback to an empty byte if bytes is empty. + if (bytes.empty()) { bytes = blank_byte_v; } + out.size = bytes.size(); + if (out.buffer.get().size < bytes.size()) { + // size is too small (or buffer doesn't exist yet), recreate buffer. + auto const buffer_ci = vma::BufferCreateInfo{ + .allocator = m_allocator, + .usage = m_usage, + .queue_family = m_queue_family, + }; + out.buffer = vma::create_buffer(buffer_ci, vma::BufferMemoryType::Host, + out.size); + } + std::memcpy(out.buffer.get().mapped, bytes.data(), bytes.size()); +} +``` + +Store a `ShaderBuffer` in `App` and rename `create_vertex_buffer()` to `create_shader_resources()`: + +```cpp +std::optional m_view_ubo{}; + +// ... +m_vbo = vma::create_device_buffer(buffer_ci, create_command_block(), + total_bytes_v); + +m_view_ubo.emplace(m_allocator.get(), m_gpu.queue_family, + vk::BufferUsageFlagBits::eUniformBuffer); +``` + +Add functions to update the view/projection matrices and bind the frame's descriptor sets: + +```cpp +void App::update_view() { + auto const half_size = 0.5f * glm::vec2{m_framebuffer_size}; + auto const mat_projection = + glm::ortho(-half_size.x, half_size.x, -half_size.y, half_size.y); + auto const bytes = + std::bit_cast>( + mat_projection); + m_view_ubo->write_at(m_frame_index, bytes); +} + +// ... +void App::bind_descriptor_sets(vk::CommandBuffer const command_buffer) const { + auto writes = std::array{}; + auto const& descriptor_sets = m_descriptor_sets.at(m_frame_index); + auto const set0 = descriptor_sets[0]; + auto write = vk::WriteDescriptorSet{}; + auto const view_ubo_info = m_view_ubo->descriptor_info_at(m_frame_index); + write.setBufferInfo(view_ubo_info) + .setDescriptorType(vk::DescriptorType::eUniformBuffer) + .setDescriptorCount(1) + .setDstSet(set0) + .setDstBinding(0); + writes[0] = write; + m_device->updateDescriptorSets(writes, {}); + + command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + *m_pipeline_layout, 0, descriptor_sets, + {}); +} +``` + +Add the descriptor set layouts to the Shader, call `update_view()` before `draw()`, and `bind_descriptor_sets()` in `draw()`: + +```cpp +auto const shader_ci = ShaderProgram::CreateInfo{ + .device = *m_device, + .vertex_spirv = vertex_spirv, + .fragment_spirv = fragment_spirv, + .vertex_input = vertex_input_v, + .set_layouts = m_set_layout_views, +}; + +// ... +inspect(); +update_view(); +draw(command_buffer); + +// ... +m_shader->bind(command_buffer, m_framebuffer_size); +bind_descriptor_sets(command_buffer); +// ... +``` + +Update the vertex shader to use the view UBO: + +```glsl +layout (set = 0, binding = 0) uniform View { + mat4 mat_vp; +}; + +// ... +void main() { + const vec4 world_pos = vec4(a_pos, 0.0, 1.0); + + out_color = a_color; + gl_Position = mat_vp * world_pos; +} +``` + +Since the projected space is now the framebuffer size instead of [-1, 1], update the vertex positions to be larger than 1 pixel: + +```cpp +static constexpr auto vertices_v = std::array{ + Vertex{.position = {-200.0f, -200.0f}, .color = {1.0f, 0.0f, 0.0f}}, + Vertex{.position = {200.0f, -200.0f}, .color = {0.0f, 1.0f, 0.0f}}, + Vertex{.position = {200.0f, 200.0f}, .color = {0.0f, 0.0f, 1.0f}}, + Vertex{.position = {-200.0f, 200.0f}, .color = {1.0f, 1.0f, 0.0f}}, +}; +``` + +![View UBO](./view_ubo.png) + +When such shader buffers are created and (more importantly) destroyed dynamically, they would need to store a `ScopedWaiter` to ensure all rendering with descriptor sets bound to them completes before destruction. Alternatively, the app can maintain a pool of scratch buffers (similar to small/dynamic vertex buffers) per virtual frame which get destroyed in a batch instead of individually. diff --git a/guide/src/descriptor_sets/view_ubo.png b/guide/src/descriptor_sets/view_ubo.png new file mode 100644 index 0000000000000000000000000000000000000000..0a56b53ceb2572223437a910ca035ce24e278a61 GIT binary patch literal 52258 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 VKKoT( +#include #include #include #include @@ -99,6 +100,7 @@ void App::run() { create_shader(); create_cmd_block_pool(); + create_shader_resources(); create_descriptor_sets(); main_loop(); @@ -294,7 +296,7 @@ void App::create_shader() { .vertex_spirv = vertex_spirv, .fragment_spirv = fragment_spirv, .vertex_input = vertex_input_v, - .set_layouts = {}, + .set_layouts = m_set_layout_views, }; m_shader.emplace(shader_ci); } @@ -309,13 +311,13 @@ void App::create_cmd_block_pool() { m_cmd_block_pool = m_device->createCommandPoolUnique(command_pool_ci); } -void App::create_vertex_buffer() { +void App::create_shader_resources() { // vertices of a quad. static constexpr auto vertices_v = std::array{ - Vertex{.position = {-0.5f, -0.5f}, .color = {1.0f, 0.0f, 0.0f}}, - Vertex{.position = {0.5f, -0.5f}, .color = {0.0f, 1.0f, 0.0f}}, - Vertex{.position = {0.5f, 0.5f}, .color = {0.0f, 0.0f, 1.0f}}, - Vertex{.position = {-0.5f, 0.5f}, .color = {1.0f, 1.0f, 0.0f}}, + Vertex{.position = {-200.0f, -200.0f}, .color = {1.0f, 0.0f, 0.0f}}, + Vertex{.position = {200.0f, -200.0f}, .color = {0.0f, 1.0f, 0.0f}}, + Vertex{.position = {200.0f, 200.0f}, .color = {0.0f, 0.0f, 1.0f}}, + Vertex{.position = {-200.0f, 200.0f}, .color = {1.0f, 1.0f, 0.0f}}, }; static constexpr auto indices_v = std::array{ 0u, 1u, 2u, 2u, 3u, 0u, @@ -337,6 +339,9 @@ void App::create_vertex_buffer() { m_vbo = vma::create_device_buffer(buffer_ci, create_command_block(), total_bytes_v); + m_view_ubo.emplace(m_allocator.get(), m_gpu.queue_family, + vk::BufferUsageFlagBits::eUniformBuffer); +} void App::create_descriptor_sets() { for (auto& descriptor_sets : m_descriptor_sets) { @@ -448,6 +453,7 @@ void App::render(vk::CommandBuffer const command_buffer) { command_buffer.beginRendering(rendering_info); inspect(); + update_view(); draw(command_buffer); command_buffer.endRendering(); @@ -531,8 +537,19 @@ void App::inspect() { ImGui::End(); } +void App::update_view() { + auto const half_size = 0.5f * glm::vec2{m_framebuffer_size}; + auto const mat_projection = + glm::ortho(-half_size.x, half_size.x, -half_size.y, half_size.y); + auto const bytes = + std::bit_cast>( + mat_projection); + m_view_ubo->write_at(m_frame_index, bytes); +} + void App::draw(vk::CommandBuffer const command_buffer) const { m_shader->bind(command_buffer, m_framebuffer_size); + bind_descriptor_sets(command_buffer); // single VBO at binding 0 at no offset. command_buffer.bindVertexBuffers(0, m_vbo.get().buffer, vk::DeviceSize{}); // u32 indices after offset of 4 vertices. @@ -541,4 +558,23 @@ void App::draw(vk::CommandBuffer const command_buffer) const { // m_vbo has 6 indices. command_buffer.drawIndexed(6, 1, 0, 0, 0); } + +void App::bind_descriptor_sets(vk::CommandBuffer const command_buffer) const { + auto writes = std::array{}; + auto const& descriptor_sets = m_descriptor_sets.at(m_frame_index); + auto const set0 = descriptor_sets[0]; + auto write = vk::WriteDescriptorSet{}; + auto const view_ubo_info = m_view_ubo->descriptor_info_at(m_frame_index); + write.setBufferInfo(view_ubo_info) + .setDescriptorType(vk::DescriptorType::eUniformBuffer) + .setDescriptorCount(1) + .setDstSet(set0) + .setDstBinding(0); + writes[0] = write; + m_device->updateDescriptorSets(writes, {}); + + command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + *m_pipeline_layout, 0, descriptor_sets, + {}); +} } // namespace lvk diff --git a/src/app.hpp b/src/app.hpp index 6811dd4..1529203 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,7 @@ class App { void create_pipeline_layout(); void create_shader(); void create_cmd_block_pool(); + void create_shader_resources(); void create_descriptor_sets(); [[nodiscard]] auto asset_path(std::string_view uri) const -> fs::path; @@ -59,9 +61,12 @@ class App { // ImGui code goes here. void inspect(); + void update_view(); // Issue draw calls here. void draw(vk::CommandBuffer command_buffer) const; + void bind_descriptor_sets(vk::CommandBuffer command_buffer) const; + fs::path m_assets_dir{}; // the order of these RAII members is crucially important. @@ -93,6 +98,7 @@ class App { std::optional m_shader{}; vma::Buffer m_vbo{}; + std::optional m_view_ubo{}; Buffered> m_descriptor_sets{}; glm::ivec2 m_framebuffer_size{}; diff --git a/src/glsl/shader.vert b/src/glsl/shader.vert index edebf18..efd23a7 100644 --- a/src/glsl/shader.vert +++ b/src/glsl/shader.vert @@ -3,11 +3,15 @@ layout (location = 0) in vec2 a_pos; layout (location = 1) in vec3 a_color; +layout (set = 0, binding = 0) uniform View { + mat4 mat_vp; +}; + layout (location = 0) out vec3 out_color; void main() { - const vec2 position = a_pos; + const vec4 world_pos = vec4(a_pos, 0.0, 1.0); out_color = a_color; - gl_Position = vec4(position, 0.0, 1.0); + gl_Position = mat_vp * world_pos; } diff --git a/src/shader_buffer.cpp b/src/shader_buffer.cpp new file mode 100644 index 0000000..773c78a --- /dev/null +++ b/src/shader_buffer.cpp @@ -0,0 +1,43 @@ +#include + +namespace lvk { +ShaderBuffer::ShaderBuffer(VmaAllocator allocator, + std::uint32_t const queue_family, + vk::BufferUsageFlags const usage) + : m_allocator(allocator), m_queue_family(queue_family), m_usage(usage) { + // ensure buffers are created and can be bound after returning. + for (auto& buffer : m_buffers) { write_to(buffer, {}); } +} + +void ShaderBuffer::write_at(std::size_t const frame_index, + std::span bytes) { + write_to(m_buffers.at(frame_index), bytes); +} + +auto ShaderBuffer::descriptor_info_at(std::size_t const frame_index) const + -> vk::DescriptorBufferInfo { + auto const& buffer = m_buffers.at(frame_index); + auto ret = vk::DescriptorBufferInfo{}; + ret.setBuffer(buffer.buffer.get().buffer).setRange(buffer.size); + return ret; +} + +void ShaderBuffer::write_to(Buffer& out, + std::span bytes) const { + static constexpr auto blank_byte_v = std::array{std::byte{}}; + // fallback to an empty byte if bytes is empty. + if (bytes.empty()) { bytes = blank_byte_v; } + out.size = bytes.size(); + if (out.buffer.get().size < bytes.size()) { + // size is too small (or buffer doesn't exist yet), recreate buffer. + auto const buffer_ci = vma::BufferCreateInfo{ + .allocator = m_allocator, + .usage = m_usage, + .queue_family = m_queue_family, + }; + out.buffer = vma::create_buffer(buffer_ci, vma::BufferMemoryType::Host, + out.size); + } + std::memcpy(out.buffer.get().mapped, bytes.data(), bytes.size()); +} +} // namespace lvk diff --git a/src/shader_buffer.hpp b/src/shader_buffer.hpp new file mode 100644 index 0000000..c46a761 --- /dev/null +++ b/src/shader_buffer.hpp @@ -0,0 +1,30 @@ +#pragma once +#include +#include +#include + +namespace lvk { +class ShaderBuffer { + public: + explicit ShaderBuffer(VmaAllocator allocator, std::uint32_t queue_family, + vk::BufferUsageFlags usage); + + void write_at(std::size_t frame_index, std::span bytes); + + [[nodiscard]] auto descriptor_info_at(std::size_t frame_index) const + -> vk::DescriptorBufferInfo; + + private: + struct Buffer { + vma::Buffer buffer{}; + vk::DeviceSize size{}; + }; + + void write_to(Buffer& out, std::span bytes) const; + + VmaAllocator m_allocator{}; + std::uint32_t m_queue_family{}; + vk::BufferUsageFlags m_usage{}; + Buffered m_buffers{}; +}; +} // namespace lvk From 2f3cafe603922486b31a19d0c2bd05f7311b8a33 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Sun, 30 Mar 2025 20:37:20 -0700 Subject: [PATCH 3/7] Texture --- assets/shader.frag | Bin 572 -> 852 bytes assets/shader.vert | Bin 1436 -> 1584 bytes guide/src/SUMMARY.md | 1 + guide/src/descriptor_sets/rgby_texture.png | Bin 0 -> 22654 bytes guide/src/descriptor_sets/texture.md | 235 +++++++++++++++++++++ src/app.cpp | 52 ++++- src/app.hpp | 2 + src/glsl/shader.frag | 5 +- src/glsl/shader.vert | 3 + src/texture.cpp | 51 +++++ src/texture.hpp | 45 ++++ src/vertex.hpp | 4 + 12 files changed, 391 insertions(+), 7 deletions(-) create mode 100644 guide/src/descriptor_sets/rgby_texture.png create mode 100644 guide/src/descriptor_sets/texture.md create mode 100644 src/texture.cpp create mode 100644 src/texture.hpp diff --git a/assets/shader.frag b/assets/shader.frag index 782c404c1bb50d695218f562a042a094c51bcc77..9face2b0e39143e74ecba2b91d840a468238e25b 100644 GIT binary patch delta 358 zcmYjNI|{-;6ntwwDw@J?Ork+eZ7B$%tw#_+5DSYGrc$x9(F25(wjRS{co7dEI3pYI z!LV=UP3CR(X})a7C`3gOB^7ZbOr3F15>w=d{Wea}N*SmEe!9AQmKf3oIqc42!RDin zlg;szzN66s0*3>DmGdEbr8z$c$+McuE*{bwU^W%tzj7X6WfKGX`qu1q KwWxg%J^{XQM;Fxq delta 81 zcmcb@wugn6nMs+Qfq{{Mn}L@>cp|T-Q$vu=o5$ z9^D4gJ-nf9c|aSvGz@@J~+s{-T= zTO0+kO`>3E$I&3OvxvB*N}Npw@pM=XXey#wyJ?!{ifUY@<9Ha1(ur(h=qa4zX5L>q z&4NK1r{gk)e#4UC4!sjIofSEFN0=jU%Xq?H;@~tIpGV^?`YI^4;IZ+g6PwvI5xbbj zGqaqxNp@zx>xJAYe9T6^53wD6w394M2GQKd#PLnzc+$c78BBgya4#X<}VJV0H@U`eUcWZz}$-v?t-vAK2}GS9njD{@~iOJZU#0 zdC0q;vVJHiLjZ-klKii910RupgNdxPURZe2DU?>W_N?%$9Q zi;tbN=eMdzethI-Z#xophL7EXUF}`j><1rv1HV@vyz!BQcQZHQdGC<~9!w99CDi+= bJK!f0V&KWcY@SNk4SA>oChm`3^Gfm;lQMB8 literal 1436 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 literal 0 HcmV?d00001 diff --git a/guide/src/descriptor_sets/texture.md b/guide/src/descriptor_sets/texture.md new file mode 100644 index 0000000..df66aed --- /dev/null +++ b/guide/src/descriptor_sets/texture.md @@ -0,0 +1,235 @@ +# Texture + +With a large part of the complexity wrapped away in `vma`, a `Texture` is just a combination of three things: + +1. Sampled Image +2. (Unique) Image View of above +3. (Unique) Sampler + +In `texture.hpp`, create a default sampler: + +```cpp +[[nodiscard]] constexpr auto create_sampler_ci(vk::SamplerAddressMode wrap, + vk::Filter filter) { + auto ret = vk::SamplerCreateInfo{}; + ret.setAddressModeU(wrap) + .setAddressModeV(wrap) + .setAddressModeW(wrap) + .setMinFilter(filter) + .setMagFilter(filter) + .setMaxLod(VK_LOD_CLAMP_NONE) + .setBorderColor(vk::BorderColor::eFloatTransparentBlack) + .setMipmapMode(vk::SamplerMipmapMode::eNearest); + return ret; +} + +constexpr auto sampler_ci_v = create_sampler_ci( + vk::SamplerAddressMode::eClampToEdge, vk::Filter::eLinear); +``` + +Define the Create Info and Texture types: + +```cpp +struct TextureCreateInfo { + vk::Device device; + VmaAllocator allocator; + std::uint32_t queue_family; + CommandBlock command_block; + Bitmap bitmap; + + vk::SamplerCreateInfo sampler{sampler_ci_v}; +}; + +class Texture { + public: + using CreateInfo = TextureCreateInfo; + + explicit Texture(CreateInfo create_info); + + [[nodiscard]] auto descriptor_info() const -> vk::DescriptorImageInfo; + + private: + vma::Image m_image{}; + vk::UniqueImageView m_view{}; + vk::UniqueSampler m_sampler{}; +}; +``` + +Add a fallback bitmap constant, and the implementation: + +```cpp +// 4-channels. +constexpr auto white_pixel_v = std::array{std::byte{0xff}, std::byte{0xff}, + std::byte{0xff}, std::byte{0xff}}; +// fallback bitmap. +constexpr auto white_bitmap_v = Bitmap{ + .bytes = white_pixel_v, + .size = {1, 1}, +}; + +// ... +Texture::Texture(CreateInfo create_info) { + if (create_info.bitmap.bytes.empty() || create_info.bitmap.size.x <= 0 || + create_info.bitmap.size.y <= 0) { + create_info.bitmap = white_bitmap_v; + } + + auto const image_ci = vma::ImageCreateInfo{ + .allocator = create_info.allocator, + .queue_family = create_info.queue_family, + }; + m_image = vma::create_sampled_image( + image_ci, std::move(create_info.command_block), create_info.bitmap); + + auto image_view_ci = vk::ImageViewCreateInfo{}; + auto subresource_range = vk::ImageSubresourceRange{}; + subresource_range.setAspectMask(vk::ImageAspectFlagBits::eColor) + .setLayerCount(1) + .setLevelCount(m_image.get().levels); + + image_view_ci.setImage(m_image.get().image) + .setViewType(vk::ImageViewType::e2D) + .setFormat(m_image.get().format) + .setSubresourceRange(subresource_range); + m_view = create_info.device.createImageViewUnique(image_view_ci); + + m_sampler = create_info.device.createSamplerUnique(create_info.sampler); +} + +auto Texture::descriptor_info() const -> vk::DescriptorImageInfo { + auto ret = vk::DescriptorImageInfo{}; + ret.setImageView(*m_view) + .setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal) + .setSampler(*m_sampler); + return ret; +} +``` + +To sample textures, `Vertex` will need a UV coordinate: + +```cpp +struct Vertex { + glm::vec2 position{}; + glm::vec3 color{1.0f}; + glm::vec2 uv{}; +}; + +// two vertex attributes: position at 0, color at 1. +constexpr auto vertex_attributes_v = std::array{ + // the format matches the type and layout of data: vec2 => 2x 32-bit floats. + vk::VertexInputAttributeDescription2EXT{0, 0, vk::Format::eR32G32Sfloat, + offsetof(Vertex, position)}, + // vec3 => 3x 32-bit floats + vk::VertexInputAttributeDescription2EXT{1, 0, vk::Format::eR32G32B32Sfloat, + offsetof(Vertex, color)}, + // vec2 => 2x 32-bit floats + vk::VertexInputAttributeDescription2EXT{2, 0, vk::Format::eR32G32Sfloat, + offsetof(Vertex, uv)}, +}; +``` + +Store a texture in `App` and create with the other shader resources: + +```cpp +std::optional m_texture{}; + +// ... +using Pixel = std::array; +static constexpr auto rgby_pixels_v = std::array{ + Pixel{std::byte{0xff}, {}, {}, std::byte{0xff}}, + Pixel{std::byte{}, std::byte{0xff}, {}, std::byte{0xff}}, + Pixel{std::byte{}, {}, std::byte{0xff}, std::byte{0xff}}, + Pixel{std::byte{0xff}, std::byte{0xff}, {}, std::byte{0xff}}, +}; +static constexpr auto rgby_bytes_v = + std::bit_cast>( + rgby_pixels_v); +static constexpr auto rgby_bitmap_v = Bitmap{ + .bytes = rgby_bytes_v, + .size = {2, 2}, +}; +auto texture_ci = Texture::CreateInfo{ + .device = *m_device, + .allocator = m_allocator.get(), + .queue_family = m_gpu.queue_family, + .command_block = create_command_block(), + .bitmap = rgby_bitmap_v, +}; +// use Nearest filtering instead of Linear (interpolation). +texture_ci.sampler.setMagFilter(vk::Filter::eNearest); +m_texture.emplace(std::move(texture_ci)); +``` + +Update the descriptor pool sizes to also contain Combined Image Samplers: + +```cpp +/// ... +vk::DescriptorPoolSize{vk::DescriptorType::eUniformBuffer, 2}, +vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2}, +``` + +Set up a new descriptor set (number 1) with a combined image sampler at binding 0. This could be added to binding 1 of set 0 as well, since we are not optimizing binding calls (eg binding set 0 only once for multiple draws): + +```cpp +static constexpr auto set_1_bindings_v = std::array{ + layout_binding(0, vk::DescriptorType::eCombinedImageSampler), +}; +auto set_layout_cis = std::array{}; +set_layout_cis[0].setBindings(set_0_bindings_v); +set_layout_cis[1].setBindings(set_1_bindings_v); +``` + +Remove the vertex colors and set the UVs for the quad. In Vulkan UV space is the same as GLFW window space: origin is at the top left, +X moves right, +Y moves down. + +```cpp +static constexpr auto vertices_v = std::array{ + Vertex{.position = {-200.0f, -200.0f}, .uv = {0.0f, 1.0f}}, + Vertex{.position = {200.0f, -200.0f}, .uv = {1.0f, 1.0f}}, + Vertex{.position = {200.0f, 200.0f}, .uv = {1.0f, 0.0f}}, + Vertex{.position = {-200.0f, 200.0f}, .uv = {0.0f, 0.0f}}, +}; +``` + +Finally, update the descriptor writes: + +```cpp +auto writes = std::array{}; +// ... +auto const set1 = descriptor_sets[1]; +auto const image_info = m_texture->descriptor_info(); +write.setImageInfo(image_info) + .setDescriptorType(vk::DescriptorType::eCombinedImageSampler) + .setDescriptorCount(1) + .setDstSet(set1) + .setDstBinding(0); +writes[1] = write; +``` + +Since set 1 is not N-buffered (because the Texture is "GPU const"), in this case the sets could also be updated once after texture creation instead of every frame. + +Add the UV vertex attribute the vertex shader and pass it to the fragment shader: + +```glsl +layout (location = 2) in vec2 a_uv; + +// ... +layout (location = 1) out vec2 out_uv; + +// ... +out_color = a_color; +out_uv = a_uv; +``` + +Add set 1 and the incoming UV coords to the fragment shader, combine the sampled texture color with the vertex color: + +```glsl +layout (set = 1, binding = 0) uniform sampler2D tex; + +// ... +layout (location = 1) in vec2 in_uv; + +// ... +out_color = vec4(in_color, 1.0) * texture(tex, in_uv); +``` + +![RGBY Texture](./rgby_texture.png) diff --git a/src/app.cpp b/src/app.cpp index 55ee13b..8f6f644 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -257,6 +257,7 @@ 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}, + vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2}, }; auto pool_ci = vk::DescriptorPoolCreateInfo{}; // allow 16 sets to be allocated from this pool. @@ -268,8 +269,12 @@ 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{}; + static constexpr auto set_1_bindings_v = std::array{ + layout_binding(0, vk::DescriptorType::eCombinedImageSampler), + }; + auto set_layout_cis = std::array{}; set_layout_cis[0].setBindings(set_0_bindings_v); + set_layout_cis[1].setBindings(set_1_bindings_v); for (auto const& set_layout_ci : set_layout_cis) { m_set_layouts.push_back( @@ -314,10 +319,10 @@ void App::create_cmd_block_pool() { void App::create_shader_resources() { // vertices of a quad. static constexpr auto vertices_v = std::array{ - Vertex{.position = {-200.0f, -200.0f}, .color = {1.0f, 0.0f, 0.0f}}, - Vertex{.position = {200.0f, -200.0f}, .color = {0.0f, 1.0f, 0.0f}}, - Vertex{.position = {200.0f, 200.0f}, .color = {0.0f, 0.0f, 1.0f}}, - Vertex{.position = {-200.0f, 200.0f}, .color = {1.0f, 1.0f, 0.0f}}, + Vertex{.position = {-200.0f, -200.0f}, .uv = {0.0f, 1.0f}}, + Vertex{.position = {200.0f, -200.0f}, .uv = {1.0f, 1.0f}}, + Vertex{.position = {200.0f, 200.0f}, .uv = {1.0f, 0.0f}}, + Vertex{.position = {-200.0f, 200.0f}, .uv = {0.0f, 0.0f}}, }; static constexpr auto indices_v = std::array{ 0u, 1u, 2u, 2u, 3u, 0u, @@ -341,6 +346,31 @@ void App::create_shader_resources() { m_view_ubo.emplace(m_allocator.get(), m_gpu.queue_family, vk::BufferUsageFlagBits::eUniformBuffer); + + using Pixel = std::array; + static constexpr auto rgby_pixels_v = std::array{ + Pixel{std::byte{0xff}, {}, {}, std::byte{0xff}}, + Pixel{std::byte{}, std::byte{0xff}, {}, std::byte{0xff}}, + Pixel{std::byte{}, {}, std::byte{0xff}, std::byte{0xff}}, + Pixel{std::byte{0xff}, std::byte{0xff}, {}, std::byte{0xff}}, + }; + static constexpr auto rgby_bytes_v = + std::bit_cast>( + rgby_pixels_v); + static constexpr auto rgby_bitmap_v = Bitmap{ + .bytes = rgby_bytes_v, + .size = {2, 2}, + }; + auto texture_ci = Texture::CreateInfo{ + .device = *m_device, + .allocator = m_allocator.get(), + .queue_family = m_gpu.queue_family, + .command_block = create_command_block(), + .bitmap = rgby_bitmap_v, + }; + // use Nearest filtering instead of Linear (interpolation). + texture_ci.sampler.setMagFilter(vk::Filter::eNearest); + m_texture.emplace(std::move(texture_ci)); } void App::create_descriptor_sets() { @@ -560,7 +590,7 @@ void App::draw(vk::CommandBuffer const command_buffer) const { } void App::bind_descriptor_sets(vk::CommandBuffer const command_buffer) const { - auto writes = std::array{}; + auto writes = std::array{}; auto const& descriptor_sets = m_descriptor_sets.at(m_frame_index); auto const set0 = descriptor_sets[0]; auto write = vk::WriteDescriptorSet{}; @@ -571,6 +601,16 @@ void App::bind_descriptor_sets(vk::CommandBuffer const command_buffer) const { .setDstSet(set0) .setDstBinding(0); writes[0] = write; + + auto const set1 = descriptor_sets[1]; + auto const image_info = m_texture->descriptor_info(); + write.setImageInfo(image_info) + .setDescriptorType(vk::DescriptorType::eCombinedImageSampler) + .setDescriptorCount(1) + .setDstSet(set1) + .setDstBinding(0); + writes[1] = write; + m_device->updateDescriptorSets(writes, {}); command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, diff --git a/src/app.hpp b/src/app.hpp index 1529203..1afe8c6 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -99,6 +100,7 @@ class App { vma::Buffer m_vbo{}; std::optional m_view_ubo{}; + std::optional m_texture{}; Buffered> m_descriptor_sets{}; glm::ivec2 m_framebuffer_size{}; diff --git a/src/glsl/shader.frag b/src/glsl/shader.frag index 93094d4..86be76b 100644 --- a/src/glsl/shader.frag +++ b/src/glsl/shader.frag @@ -1,9 +1,12 @@ #version 450 core +layout (set = 1, binding = 0) uniform sampler2D tex; + layout (location = 0) in vec3 in_color; +layout (location = 1) in vec2 in_uv; layout (location = 0) out vec4 out_color; void main() { - out_color = vec4(in_color, 1.0); + out_color = vec4(in_color, 1.0) * texture(tex, in_uv); } diff --git a/src/glsl/shader.vert b/src/glsl/shader.vert index efd23a7..4a117d5 100644 --- a/src/glsl/shader.vert +++ b/src/glsl/shader.vert @@ -2,16 +2,19 @@ layout (location = 0) in vec2 a_pos; layout (location = 1) in vec3 a_color; +layout (location = 2) in vec2 a_uv; layout (set = 0, binding = 0) uniform View { mat4 mat_vp; }; layout (location = 0) out vec3 out_color; +layout (location = 1) out vec2 out_uv; void main() { const vec4 world_pos = vec4(a_pos, 0.0, 1.0); out_color = a_color; + out_uv = a_uv; gl_Position = mat_vp * world_pos; } diff --git a/src/texture.cpp b/src/texture.cpp new file mode 100644 index 0000000..2c0b72d --- /dev/null +++ b/src/texture.cpp @@ -0,0 +1,51 @@ +#include +#include + +namespace lvk { +namespace { +// 4-channels. +constexpr auto white_pixel_v = std::array{std::byte{0xff}, std::byte{0xff}, + std::byte{0xff}, std::byte{0xff}}; +// fallback bitmap. +constexpr auto white_bitmap_v = Bitmap{ + .bytes = white_pixel_v, + .size = {1, 1}, +}; +} // namespace + +Texture::Texture(CreateInfo create_info) { + if (create_info.bitmap.bytes.empty() || create_info.bitmap.size.x <= 0 || + create_info.bitmap.size.y <= 0) { + create_info.bitmap = white_bitmap_v; + } + + auto const image_ci = vma::ImageCreateInfo{ + .allocator = create_info.allocator, + .queue_family = create_info.queue_family, + }; + m_image = vma::create_sampled_image( + image_ci, std::move(create_info.command_block), create_info.bitmap); + + auto image_view_ci = vk::ImageViewCreateInfo{}; + auto subresource_range = vk::ImageSubresourceRange{}; + subresource_range.setAspectMask(vk::ImageAspectFlagBits::eColor) + .setLayerCount(1) + .setLevelCount(m_image.get().levels); + + image_view_ci.setImage(m_image.get().image) + .setViewType(vk::ImageViewType::e2D) + .setFormat(m_image.get().format) + .setSubresourceRange(subresource_range); + m_view = create_info.device.createImageViewUnique(image_view_ci); + + m_sampler = create_info.device.createSamplerUnique(create_info.sampler); +} + +auto Texture::descriptor_info() const -> vk::DescriptorImageInfo { + auto ret = vk::DescriptorImageInfo{}; + ret.setImageView(*m_view) + .setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal) + .setSampler(*m_sampler); + return ret; +} +} // namespace lvk diff --git a/src/texture.hpp b/src/texture.hpp new file mode 100644 index 0000000..b5de188 --- /dev/null +++ b/src/texture.hpp @@ -0,0 +1,45 @@ +#pragma once +#include + +namespace lvk { +[[nodiscard]] constexpr auto create_sampler_ci(vk::SamplerAddressMode wrap, + vk::Filter filter) { + auto ret = vk::SamplerCreateInfo{}; + ret.setAddressModeU(wrap) + .setAddressModeV(wrap) + .setAddressModeW(wrap) + .setMinFilter(filter) + .setMagFilter(filter) + .setMaxLod(VK_LOD_CLAMP_NONE) + .setBorderColor(vk::BorderColor::eFloatTransparentBlack) + .setMipmapMode(vk::SamplerMipmapMode::eNearest); + return ret; +} + +constexpr auto sampler_ci_v = create_sampler_ci( + vk::SamplerAddressMode::eClampToEdge, vk::Filter::eLinear); + +struct TextureCreateInfo { + vk::Device device; + VmaAllocator allocator; + std::uint32_t queue_family; + CommandBlock command_block; + Bitmap bitmap; + + vk::SamplerCreateInfo sampler{sampler_ci_v}; +}; + +class Texture { + public: + using CreateInfo = TextureCreateInfo; + + explicit Texture(CreateInfo create_info); + + [[nodiscard]] auto descriptor_info() const -> vk::DescriptorImageInfo; + + private: + vma::Image m_image{}; + vk::UniqueImageView m_view{}; + vk::UniqueSampler m_sampler{}; +}; +} // namespace lvk diff --git a/src/vertex.hpp b/src/vertex.hpp index a8c003c..cefec31 100644 --- a/src/vertex.hpp +++ b/src/vertex.hpp @@ -7,6 +7,7 @@ namespace lvk { struct Vertex { glm::vec2 position{}; glm::vec3 color{1.0f}; + glm::vec2 uv{}; }; // two vertex attributes: position at 0, color at 1. @@ -17,6 +18,9 @@ constexpr auto vertex_attributes_v = std::array{ // vec3 => 3x 32-bit floats vk::VertexInputAttributeDescription2EXT{1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)}, + // vec2 => 2x 32-bit floats + vk::VertexInputAttributeDescription2EXT{2, 0, vk::Format::eR32G32Sfloat, + offsetof(Vertex, uv)}, }; // one vertex binding at location 0. From 21c56cd84e7f6ea912f8bb3ace28cb87ba8f8ac9 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Mon, 31 Mar 2025 16:56:17 -0700 Subject: [PATCH 4/7] Link to mip-mapping sample --- guide/src/descriptor_sets/texture.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/guide/src/descriptor_sets/texture.md b/guide/src/descriptor_sets/texture.md index df66aed..1b5f5c7 100644 --- a/guide/src/descriptor_sets/texture.md +++ b/guide/src/descriptor_sets/texture.md @@ -233,3 +233,15 @@ out_color = vec4(in_color, 1.0) * texture(tex, in_uv); ``` ![RGBY Texture](./rgby_texture.png) + +For generating mip-maps, follow the [sample in the Vulkan docs](https://docs.vulkan.org/samples/latest/samples/api/hpp_texture_mipmap_generation/README.html#_generating_the_mip_chain). The high-level steps are: + +1. Compute mip levels based on image size +1. Create an image with the desired mip levels +1. Copy the source data to the first mip level as usual +1. Transition the first mip level to TransferSrc +1. Iterate through all the remaining mip levels: + 1. Transition the current mip level to TransferDst + 1. Record an image blit operation from previous to current mip levels + 1. Transition the current mip level to TransferSrc +1. Transition all levels (entire image) to ShaderRead From 1e78e49744240a42a7bf32ef1d10291e02602465 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Mon, 31 Mar 2025 17:20:47 -0700 Subject: [PATCH 5/7] Add `Transform`, view matrix --- guide/src/SUMMARY.md | 1 + guide/src/descriptor_sets/view_matrix.md | 79 ++++++++++++++++++++++ guide/src/descriptor_sets/view_matrix.png | Bin 0 -> 27789 bytes src/app.cpp | 13 +++- src/app.hpp | 3 + src/transform.cpp | 39 +++++++++++ src/transform.hpp | 14 ++++ 7 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 guide/src/descriptor_sets/view_matrix.md create mode 100644 guide/src/descriptor_sets/view_matrix.png create mode 100644 src/transform.cpp create mode 100644 src/transform.hpp diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 5d52887..59306c2 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -47,3 +47,4 @@ - [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) diff --git a/guide/src/descriptor_sets/view_matrix.md b/guide/src/descriptor_sets/view_matrix.md new file mode 100644 index 0000000..e357cb1 --- /dev/null +++ b/guide/src/descriptor_sets/view_matrix.md @@ -0,0 +1,79 @@ +# View Matrix + +Integrating the view matrix will be quite simple and short. First, transformations for objects and cameras/views can be encapsulated into a single struct: + +```cpp +struct Transform { + glm::vec2 position{}; + float rotation{}; + glm::vec2 scale{1.0f}; + + [[nodiscard]] auto model_matrix() const -> glm::mat4; + [[nodiscard]] auto view_matrix() const -> glm::mat4; +}; +``` + +Extracting the common logic into a helper, both member functions can be implemented easily: + +```cpp +namespace { +struct Matrices { + glm::mat4 translation; + glm::mat4 orientation; + glm::mat4 scale; +}; + +[[nodiscard]] auto to_matrices(glm::vec2 const position, float rotation, + glm::vec2 const scale) -> Matrices { + static constexpr auto mat_v = glm::identity(); + static constexpr auto axis_v = glm::vec3{0.0f, 0.0f, 1.0f}; + return Matrices{ + .translation = glm::translate(mat_v, glm::vec3{position, 0.0f}), + .orientation = glm::rotate(mat_v, glm::radians(rotation), axis_v), + .scale = glm::scale(mat_v, glm::vec3{scale, 1.0f}), + }; +} +} // namespace + +auto Transform::model_matrix() const -> glm::mat4 { + auto const [t, r, s] = to_matrices(position, rotation, scale); + // right to left: scale first, then rotate, then translate. + return t * r * s; +} + +auto Transform::view_matrix() const -> glm::mat4 { + // view matrix is the inverse of the model matrix. + // instead, perform translation and rotation in reverse order and with + // negative values. or, use glm::lookAt(). + // scale is kept unchanged as the first transformation for + // "intuitive" scaling on cameras. + auto const [t, r, s] = to_matrices(-position, -rotation, scale); + return r * t * s; +} +``` + +Add a `Transform` member to `App` to represent the view/camera, inspect its members, and combine with the existing projection matrix: + +```cpp +Transform m_view_transform{}; + +// ... +ImGui::Separator(); +if (ImGui::TreeNode("View")) { + ImGui::DragFloat2("position", &m_view_transform.position.x); + ImGui::DragFloat("rotation", &m_view_transform.rotation); + ImGui::DragFloat2("scale", &m_view_transform.scale.x); + ImGui::TreePop(); +} + +// ... +auto const mat_view = m_view_transform.view_matrix(); +auto const mat_vp = mat_projection * mat_view; +auto const bytes = + std::bit_cast>(mat_vp); +m_view_ubo->write_at(m_frame_index, bytes); +``` + +Naturally, moving the view left moves everything else - currently only a single RGBY quad - to the _right_. + +![View Matrix](./view_matrix.png) diff --git a/guide/src/descriptor_sets/view_matrix.png b/guide/src/descriptor_sets/view_matrix.png new file mode 100644 index 0000000000000000000000000000000000000000..5070b917bf99f95c88bb3a99679970da7c55cff0 GIT binary patch literal 27789 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 literal 0 HcmV?d00001 diff --git a/src/app.cpp b/src/app.cpp index 8f6f644..16b2167 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -563,6 +563,14 @@ void App::inspect() { ImGui::DragFloat("line width", &m_shader->line_width, 0.25f, line_width_range[0], line_width_range[1]); } + + ImGui::Separator(); + if (ImGui::TreeNode("View")) { + ImGui::DragFloat2("position", &m_view_transform.position.x); + ImGui::DragFloat("rotation", &m_view_transform.rotation); + ImGui::DragFloat2("scale", &m_view_transform.scale.x, 0.1f); + ImGui::TreePop(); + } } ImGui::End(); } @@ -571,9 +579,10 @@ void App::update_view() { auto const half_size = 0.5f * glm::vec2{m_framebuffer_size}; auto const mat_projection = glm::ortho(-half_size.x, half_size.x, -half_size.y, half_size.y); + auto const mat_view = m_view_transform.view_matrix(); + auto const mat_vp = mat_projection * mat_view; auto const bytes = - std::bit_cast>( - mat_projection); + std::bit_cast>(mat_vp); m_view_ubo->write_at(m_frame_index, bytes); } diff --git a/src/app.hpp b/src/app.hpp index 1afe8c6..64b361d 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -107,6 +108,8 @@ class App { std::optional m_render_target{}; bool m_wireframe{}; + Transform m_view_transform{}; + // waiter must be the last member to ensure it blocks until device is idle // before other members get destroyed. ScopedWaiter m_waiter{}; diff --git a/src/transform.cpp b/src/transform.cpp new file mode 100644 index 0000000..f5a66be --- /dev/null +++ b/src/transform.cpp @@ -0,0 +1,39 @@ +#include +#include + +namespace lvk { +namespace { +struct Matrices { + glm::mat4 translation; + glm::mat4 orientation; + glm::mat4 scale; +}; + +[[nodiscard]] auto to_matrices(glm::vec2 const position, float rotation, + glm::vec2 const scale) -> Matrices { + static constexpr auto mat_v = glm::identity(); + static constexpr auto axis_v = glm::vec3{0.0f, 0.0f, 1.0f}; + return Matrices{ + .translation = glm::translate(mat_v, glm::vec3{position, 0.0f}), + .orientation = glm::rotate(mat_v, glm::radians(rotation), axis_v), + .scale = glm::scale(mat_v, glm::vec3{scale, 1.0f}), + }; +} +} // namespace + +auto Transform::model_matrix() const -> glm::mat4 { + auto const [t, r, s] = to_matrices(position, rotation, scale); + // right to left: scale first, then rotate, then translate. + return t * r * s; +} + +auto Transform::view_matrix() const -> glm::mat4 { + // view matrix is the inverse of the model matrix. + // instead, perform translation and rotation in reverse order and with + // negative values. or, use glm::lookAt(). + // scale is kept unchanged as the first transformation for + // "intuitive" scaling on cameras. + auto const [t, r, s] = to_matrices(-position, -rotation, scale); + return r * t * s; +} +} // namespace lvk diff --git a/src/transform.hpp b/src/transform.hpp new file mode 100644 index 0000000..f4507e5 --- /dev/null +++ b/src/transform.hpp @@ -0,0 +1,14 @@ +#pragma once +#include +#include + +namespace lvk { +struct Transform { + glm::vec2 position{}; + float rotation{}; + glm::vec2 scale{1.0f}; + + [[nodiscard]] auto model_matrix() const -> glm::mat4; + [[nodiscard]] auto view_matrix() const -> glm::mat4; +}; +} // namespace lvk From 2b2d4b27d9bafbc01b0fc4c389439699e2b44398 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Mon, 31 Mar 2025 17:54:31 -0700 Subject: [PATCH 6/7] Instanced rendering --- assets/shader.vert | Bin 1584 -> 2068 bytes guide/src/SUMMARY.md | 1 + .../descriptor_sets/instanced_rendering.md | 132 ++++++++++++++++++ .../descriptor_sets/instanced_rendering.png | Bin 0 -> 29662 bytes guide/src/descriptor_sets/view_matrix.md | 2 +- src/app.cpp | 55 +++++++- src/app.hpp | 6 +- src/glsl/shader.vert | 7 +- 8 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 guide/src/descriptor_sets/instanced_rendering.md create mode 100644 guide/src/descriptor_sets/instanced_rendering.png diff --git a/assets/shader.vert b/assets/shader.vert index c0886609f397d81e9a521c4f3e77d9f4803fca61..d91edd9bb92990747d1ab9e040fedd7be4e59dd4 100644 GIT binary patch literal 2068 zcmYk6Z%-3J5XKkUD+mH2@<#=1sUlj%e}E{W5;cXS2^bRIUYhn|E;;U+_FB|eeg(gh zpUO8TKEJ!$_O_YsJkQL|%+73E7_UwkGiIjEj5##vnm6NOjJQ&=*Bj@J?Oxp8-r0SQ z$E+zNj`+-(>xo{}_hr)$0XHS9l1<5$q$YVHc_k_6Gp_xQ4j0XosW;sE>1n<3#cg#u zZZG~3cy8c_p4;}jUMuzo9)6QKKR;{*{dV5Lkw-p1cuF}go3VST-O-0CPr_c@3|n4L zdMV?irhXzF)iw*E<(P@&9^V4j9y*w9ymh#-iRG?H9gx1*V5!7QG&H@F|$R zlrnYsPfIUJK1%AcF$eM-%ZAQ!B**%;SP#lEfdCa`P^p#qe7do|~v&hG?9I@coEyo$Sv^VKlbU5BN zvFUXhJDr2zuy=M2quz~XGwM|_nN5X*THKdeZ6wH;1w|)*MY+h0z9fzLl_ksaCq9^( zR`flo9S&U9_q7beujwrHqpwS6zF_=jv=blhAj8ZKZc%tnds#9jsb!cq!Q%Y9c5L`@ z9?Wdvu;0>7EOLXFwNod;J??1d9{6CttDUp(!~^p-;K+}!eHRwr6KBk?55&7K4IlbO zya$;MPdxBu#$kUbo!Hh6-pV-akEIjK)&s6(oUM@6joxIq|1LpS+#|(LAwx_$&=^q@vVBVAM>49|4k!MdY6`ps(g8wUq z-#%w~H~eSg3ua#Yroo58%y~h=jE__`^IeqiW_TCa@T0aR3AXpjLQJr=-IfjqrnhSn z=KNKa(2KGJj%B%ubU1o>ri!R(LsFJt<6ZC;ek%sHs)RiFPy_dXBPa8IBw^OrcoSe- z&r|8l6&v#gA1N;PeJ!EC+}GY`KjXpVej_2Dt>vwBeBg=0Ox{VDDRIaH#_ykQxG(t+ DW*U&+ literal 1584 zcmZ9M-EPxB5QR5Mla@kTO6d-Q$vu=o5$ z9^D4gJ-nf9c|aSvGz@@J~+s{-T= zTO0+kO`>3E$I&3OvxvB*N}Npw@pM=XXey#wyJ?!{ifUY@<9Ha1(ur(h=qa4zX5L>q z&4NK1r{gk)e#4UC4!sjIofSEFN0=jU%Xq?H;@~tIpGV^?`YI^4;IZ+g6PwvI5xbbj zGqaqxNp@zx>xJAYe9T6^53wD6w394M2GQKd#PLnzc+$c78BBgya4#X<}VJV0H@U`eUcWZz}$-v?t-vAK2}GS9njD{@~iOJZU#0 zdC0q;vVJHiLjZ-klKii910RupgNdxPURZe2DU?>W_N?%$9Q zi;tbN=eMdzethI-Z#xophL7EXUF}`j><1rv1HV@vyz!BQcQZHQdGC<~9!w99CDi+= bJK!f0V&KWcY@SNk4SA>oChm`3^Gfm;lQMB8 diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 59306c2..76da5fc 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -48,3 +48,4 @@ - [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) diff --git a/guide/src/descriptor_sets/instanced_rendering.md b/guide/src/descriptor_sets/instanced_rendering.md new file mode 100644 index 0000000..019d2e6 --- /dev/null +++ b/guide/src/descriptor_sets/instanced_rendering.md @@ -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 m_instance_data{}; // model matrices. +std::optional 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 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(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{}; +// ... +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(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) diff --git a/guide/src/descriptor_sets/instanced_rendering.png b/guide/src/descriptor_sets/instanced_rendering.png new file mode 100644 index 0000000000000000000000000000000000000000..766a49c59495dc3da7c6d038d4c6dff36fdd44b4 GIT binary patch literal 29662 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 literal 0 HcmV?d00001 diff --git a/guide/src/descriptor_sets/view_matrix.md b/guide/src/descriptor_sets/view_matrix.md index e357cb1..43cdc32 100644 --- a/guide/src/descriptor_sets/view_matrix.md +++ b/guide/src/descriptor_sets/view_matrix.md @@ -55,7 +55,7 @@ auto Transform::view_matrix() const -> glm::mat4 { Add a `Transform` member to `App` to represent the view/camera, inspect its members, and combine with the existing projection matrix: ```cpp -Transform m_view_transform{}; +Transform m_view_transform{}; // generates view matrix. // ... ImGui::Separator(); diff --git a/src/app.cpp b/src/app.cpp index 16b2167..5f2cdaa 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -258,6 +258,7 @@ void App::create_descriptor_pool() { // 2 uniform buffers, can be more if desired. vk::DescriptorPoolSize{vk::DescriptorType::eUniformBuffer, 2}, vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2}, + vk::DescriptorPoolSize{vk::DescriptorType::eStorageBuffer, 2}, }; auto pool_ci = vk::DescriptorPoolCreateInfo{}; // allow 16 sets to be allocated from this pool. @@ -271,6 +272,7 @@ void App::create_pipeline_layout() { }; static constexpr auto set_1_bindings_v = std::array{ layout_binding(0, vk::DescriptorType::eCombinedImageSampler), + layout_binding(1, vk::DescriptorType::eStorageBuffer), }; auto set_layout_cis = std::array{}; set_layout_cis[0].setBindings(set_0_bindings_v); @@ -347,6 +349,9 @@ void App::create_shader_resources() { m_view_ubo.emplace(m_allocator.get(), m_gpu.queue_family, vk::BufferUsageFlagBits::eUniformBuffer); + m_instance_ssbo.emplace(m_allocator.get(), m_gpu.queue_family, + vk::BufferUsageFlagBits::eStorageBuffer); + using Pixel = std::array; static constexpr auto rgby_pixels_v = std::array{ Pixel{std::byte{0xff}, {}, {}, std::byte{0xff}}, @@ -484,6 +489,7 @@ void App::render(vk::CommandBuffer const command_buffer) { command_buffer.beginRendering(rendering_info); inspect(); update_view(); + update_instances(); draw(command_buffer); command_buffer.endRendering(); @@ -564,11 +570,27 @@ void App::inspect() { line_width_range[0], line_width_range[1]); } + 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")) { - ImGui::DragFloat2("position", &m_view_transform.position.x); - ImGui::DragFloat("rotation", &m_view_transform.rotation); - ImGui::DragFloat2("scale", &m_view_transform.scale.x, 0.1f); + 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(); } } @@ -586,6 +608,20 @@ void App::update_view() { m_view_ubo->write_at(m_frame_index, bytes); } +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(data), span.size_bytes()}; + m_instance_ssbo->write_at(m_frame_index, bytes); +} + void App::draw(vk::CommandBuffer const command_buffer) const { m_shader->bind(command_buffer, m_framebuffer_size); bind_descriptor_sets(command_buffer); @@ -594,12 +630,13 @@ void App::draw(vk::CommandBuffer const command_buffer) const { // u32 indices after offset of 4 vertices. command_buffer.bindIndexBuffer(m_vbo.get().buffer, 4 * sizeof(Vertex), vk::IndexType::eUint32); + auto const instances = static_cast(m_instances.size()); // m_vbo has 6 indices. - command_buffer.drawIndexed(6, 1, 0, 0, 0); + command_buffer.drawIndexed(6, instances, 0, 0, 0); } void App::bind_descriptor_sets(vk::CommandBuffer const command_buffer) const { - auto writes = std::array{}; + auto writes = std::array{}; auto const& descriptor_sets = m_descriptor_sets.at(m_frame_index); auto const set0 = descriptor_sets[0]; auto write = vk::WriteDescriptorSet{}; @@ -619,6 +656,14 @@ void App::bind_descriptor_sets(vk::CommandBuffer const command_buffer) const { .setDstSet(set1) .setDstBinding(0); writes[1] = write; + 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; m_device->updateDescriptorSets(writes, {}); diff --git a/src/app.hpp b/src/app.hpp index 64b361d..198518c 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -64,6 +64,7 @@ class App { // ImGui code goes here. void inspect(); void update_view(); + void update_instances(); // Issue draw calls here. void draw(vk::CommandBuffer command_buffer) const; @@ -102,13 +103,16 @@ class App { vma::Buffer m_vbo{}; std::optional m_view_ubo{}; std::optional m_texture{}; + std::vector m_instance_data{}; // model matrices. + std::optional m_instance_ssbo{}; Buffered> m_descriptor_sets{}; glm::ivec2 m_framebuffer_size{}; std::optional m_render_target{}; bool m_wireframe{}; - Transform m_view_transform{}; + Transform m_view_transform{}; // generates view matrix. + std::array m_instances{}; // generates model matrices. // waiter must be the last member to ensure it blocks until device is idle // before other members get destroyed. diff --git a/src/glsl/shader.vert b/src/glsl/shader.vert index 4a117d5..adf7066 100644 --- a/src/glsl/shader.vert +++ b/src/glsl/shader.vert @@ -8,11 +8,16 @@ layout (set = 0, binding = 0) uniform View { mat4 mat_vp; }; +layout (set = 1, binding = 1) readonly buffer Instances { + mat4 mat_ms[]; +}; + layout (location = 0) out vec3 out_color; layout (location = 1) out vec2 out_uv; void main() { - const vec4 world_pos = vec4(a_pos, 0.0, 1.0); + const mat4 mat_m = mat_ms[gl_InstanceIndex]; + const vec4 world_pos = mat_m * vec4(a_pos, 0.0, 1.0); out_color = a_color; out_uv = a_uv; From cf3399eb46b908a432eeb7b0295b85b45afbdc4c Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Mon, 31 Mar 2025 21:49:10 -0700 Subject: [PATCH 7/7] Fixups --- guide/src/descriptor_sets/shader_buffer.md | 2 +- guide/src/descriptor_sets/texture.md | 6 +++--- src/texture.hpp | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/guide/src/descriptor_sets/shader_buffer.md b/guide/src/descriptor_sets/shader_buffer.md index 276357b..d2e03ea 100644 --- a/guide/src/descriptor_sets/shader_buffer.md +++ b/guide/src/descriptor_sets/shader_buffer.md @@ -6,7 +6,7 @@ Uniform and Storage buffers need to be N-buffered unless they are "GPU const", i class ShaderBuffer { public: explicit ShaderBuffer(VmaAllocator allocator, std::uint32_t queue_family, - vk::BufferUsageFlags usage); + vk::BufferUsageFlags usage); void write_at(std::size_t frame_index, std::span bytes); diff --git a/guide/src/descriptor_sets/texture.md b/guide/src/descriptor_sets/texture.md index 1b5f5c7..bac0b80 100644 --- a/guide/src/descriptor_sets/texture.md +++ b/guide/src/descriptor_sets/texture.md @@ -9,8 +9,8 @@ With a large part of the complexity wrapped away in `vma`, a `Texture` is just a In `texture.hpp`, create a default sampler: ```cpp -[[nodiscard]] constexpr auto create_sampler_ci(vk::SamplerAddressMode wrap, - vk::Filter filter) { +[[nodiscard]] constexpr auto +create_sampler_ci(vk::SamplerAddressMode const wrap, vk::Filter const filter) { auto ret = vk::SamplerCreateInfo{}; ret.setAddressModeU(wrap) .setAddressModeV(wrap) @@ -205,7 +205,7 @@ write.setImageInfo(image_info) writes[1] = write; ``` -Since set 1 is not N-buffered (because the Texture is "GPU const"), in this case the sets could also be updated once after texture creation instead of every frame. +Since the Texture is not N-buffered (because it is "GPU const"), in this case the sets could also be updated once after texture creation instead of every frame. Add the UV vertex attribute the vertex shader and pass it to the fragment shader: diff --git a/src/texture.hpp b/src/texture.hpp index b5de188..185f25b 100644 --- a/src/texture.hpp +++ b/src/texture.hpp @@ -2,8 +2,8 @@ #include namespace lvk { -[[nodiscard]] constexpr auto create_sampler_ci(vk::SamplerAddressMode wrap, - vk::Filter filter) { +[[nodiscard]] constexpr auto +create_sampler_ci(vk::SamplerAddressMode const wrap, vk::Filter const filter) { auto ret = vk::SamplerCreateInfo{}; ret.setAddressModeU(wrap) .setAddressModeV(wrap)