Skip to content

Commit 82d5dbd

Browse files
committed
ShaderBuffer
1 parent 9ccd0d9 commit 82d5dbd

File tree

9 files changed

+301
-8
lines changed

9 files changed

+301
-8
lines changed

assets/shader.vert

280 Bytes
Binary file not shown.

guide/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@
4545
- [Images](memory/images.md)
4646
- [Descriptor Sets](descriptor_sets/README.md)
4747
- [Pipeline Layout](descriptor_sets/pipeline_layout.md)
48+
- [Shader Buffer](descriptor_sets/shader_buffer.md)
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Shader Buffer
2+
3+
Uniform and Storage buffers need to be N-buffered unless they are "GPU const", ie contents do not change after creation. Encapsulate a `vma::Buffer` per virtual frame in a `ShaderBuffer`:
4+
5+
```cpp
6+
class ShaderBuffer {
7+
public:
8+
explicit ShaderBuffer(VmaAllocator allocator, std::uint32_t queue_family,
9+
vk::BufferUsageFlags usage);
10+
11+
void write_at(std::size_t frame_index, std::span<std::byte const> bytes);
12+
13+
[[nodiscard]] auto descriptor_info_at(std::size_t frame_index) const
14+
-> vk::DescriptorBufferInfo;
15+
16+
private:
17+
struct Buffer {
18+
vma::Buffer buffer{};
19+
vk::DeviceSize size{};
20+
};
21+
22+
void write_to(Buffer& out, std::span<std::byte const> bytes) const;
23+
24+
VmaAllocator m_allocator{};
25+
std::uint32_t m_queue_family{};
26+
vk::BufferUsageFlags m_usage{};
27+
Buffered<Buffer> m_buffers{};
28+
};
29+
```
30+
31+
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.
32+
33+
```cpp
34+
ShaderBuffer::ShaderBuffer(VmaAllocator allocator,
35+
std::uint32_t const queue_family,
36+
vk::BufferUsageFlags const usage)
37+
: m_allocator(allocator), m_queue_family(queue_family), m_usage(usage) {
38+
// ensure buffers are created and can be bound after returning.
39+
for (auto& buffer : m_buffers) { write_to(buffer, {}); }
40+
}
41+
42+
void ShaderBuffer::write_at(std::size_t const frame_index,
43+
std::span<std::byte const> bytes) {
44+
write_to(m_buffers.at(frame_index), bytes);
45+
}
46+
47+
auto ShaderBuffer::descriptor_info_at(std::size_t const frame_index) const
48+
-> vk::DescriptorBufferInfo {
49+
auto const& buffer = m_buffers.at(frame_index);
50+
auto ret = vk::DescriptorBufferInfo{};
51+
ret.setBuffer(buffer.buffer.get().buffer).setRange(buffer.size);
52+
return ret;
53+
}
54+
55+
void ShaderBuffer::write_to(Buffer& out,
56+
std::span<std::byte const> bytes) const {
57+
static constexpr auto blank_byte_v = std::array{std::byte{}};
58+
// fallback to an empty byte if bytes is empty.
59+
if (bytes.empty()) { bytes = blank_byte_v; }
60+
out.size = bytes.size();
61+
if (out.buffer.get().size < bytes.size()) {
62+
// size is too small (or buffer doesn't exist yet), recreate buffer.
63+
auto const buffer_ci = vma::BufferCreateInfo{
64+
.allocator = m_allocator,
65+
.usage = m_usage,
66+
.queue_family = m_queue_family,
67+
};
68+
out.buffer = vma::create_buffer(buffer_ci, vma::BufferMemoryType::Host,
69+
out.size);
70+
}
71+
std::memcpy(out.buffer.get().mapped, bytes.data(), bytes.size());
72+
}
73+
```
74+
75+
Store a `ShaderBuffer` in `App` and rename `create_vertex_buffer()` to `create_shader_resources()`:
76+
77+
```cpp
78+
std::optional<ShaderBuffer> m_view_ubo{};
79+
80+
// ...
81+
m_vbo = vma::create_device_buffer(buffer_ci, create_command_block(),
82+
total_bytes_v);
83+
84+
m_view_ubo.emplace(m_allocator.get(), m_gpu.queue_family,
85+
vk::BufferUsageFlagBits::eUniformBuffer);
86+
```
87+
88+
Add functions to update the view/projection matrices and bind the frame's descriptor sets:
89+
90+
```cpp
91+
void App::update_view() {
92+
auto const half_size = 0.5f * glm::vec2{m_framebuffer_size};
93+
auto const mat_projection =
94+
glm::ortho(-half_size.x, half_size.x, -half_size.y, half_size.y);
95+
auto const bytes =
96+
std::bit_cast<std::array<std::byte, sizeof(mat_projection)>>(
97+
mat_projection);
98+
m_view_ubo->write_at(m_frame_index, bytes);
99+
}
100+
101+
// ...
102+
void App::bind_descriptor_sets(vk::CommandBuffer const command_buffer) const {
103+
auto writes = std::array<vk::WriteDescriptorSet, 1>{};
104+
auto const& descriptor_sets = m_descriptor_sets.at(m_frame_index);
105+
auto const set0 = descriptor_sets[0];
106+
auto write = vk::WriteDescriptorSet{};
107+
auto const view_ubo_info = m_view_ubo->descriptor_info_at(m_frame_index);
108+
write.setBufferInfo(view_ubo_info)
109+
.setDescriptorType(vk::DescriptorType::eUniformBuffer)
110+
.setDescriptorCount(1)
111+
.setDstSet(set0)
112+
.setDstBinding(0);
113+
writes[0] = write;
114+
m_device->updateDescriptorSets(writes, {});
115+
116+
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
117+
*m_pipeline_layout, 0, descriptor_sets,
118+
{});
119+
}
120+
```
121+
122+
Add the descriptor set layouts to the Shader, call `update_view()` before `draw()`, and `bind_descriptor_sets()` in `draw()`:
123+
124+
```cpp
125+
auto const shader_ci = ShaderProgram::CreateInfo{
126+
.device = *m_device,
127+
.vertex_spirv = vertex_spirv,
128+
.fragment_spirv = fragment_spirv,
129+
.vertex_input = vertex_input_v,
130+
.set_layouts = m_set_layout_views,
131+
};
132+
133+
// ...
134+
inspect();
135+
update_view();
136+
draw(command_buffer);
137+
138+
// ...
139+
m_shader->bind(command_buffer, m_framebuffer_size);
140+
bind_descriptor_sets(command_buffer);
141+
// ...
142+
```
143+
144+
Update the vertex shader to use the view UBO:
145+
146+
```glsl
147+
layout (set = 0, binding = 0) uniform View {
148+
mat4 mat_vp;
149+
};
150+
151+
// ...
152+
void main() {
153+
const vec4 world_pos = vec4(a_pos, 0.0, 1.0);
154+
155+
out_color = a_color;
156+
gl_Position = mat_vp * world_pos;
157+
}
158+
```
159+
160+
Since the projected space is now the framebuffer size instead of [-1, 1], update the vertex positions to be larger than 1 pixel:
161+
162+
```cpp
163+
static constexpr auto vertices_v = std::array{
164+
Vertex{.position = {-200.0f, -200.0f}, .color = {1.0f, 0.0f, 0.0f}},
165+
Vertex{.position = {200.0f, -200.0f}, .color = {0.0f, 1.0f, 0.0f}},
166+
Vertex{.position = {200.0f, 200.0f}, .color = {0.0f, 0.0f, 1.0f}},
167+
Vertex{.position = {-200.0f, 200.0f}, .color = {1.0f, 1.0f, 0.0f}},
168+
};
169+
```
170+
171+
![View UBO](./view_ubo.png)
172+
173+
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.
51 KB
Loading

src/app.cpp

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <app.hpp>
2+
#include <glm/gtc/matrix_transform.hpp>
23
#include <vertex.hpp>
34
#include <bit>
45
#include <cassert>
@@ -99,6 +100,7 @@ void App::run() {
99100
create_shader();
100101
create_cmd_block_pool();
101102

103+
create_shader_resources();
102104
create_descriptor_sets();
103105

104106
main_loop();
@@ -294,7 +296,7 @@ void App::create_shader() {
294296
.vertex_spirv = vertex_spirv,
295297
.fragment_spirv = fragment_spirv,
296298
.vertex_input = vertex_input_v,
297-
.set_layouts = {},
299+
.set_layouts = m_set_layout_views,
298300
};
299301
m_shader.emplace(shader_ci);
300302
}
@@ -309,13 +311,13 @@ void App::create_cmd_block_pool() {
309311
m_cmd_block_pool = m_device->createCommandPoolUnique(command_pool_ci);
310312
}
311313

312-
void App::create_vertex_buffer() {
314+
void App::create_shader_resources() {
313315
// vertices of a quad.
314316
static constexpr auto vertices_v = std::array{
315-
Vertex{.position = {-0.5f, -0.5f}, .color = {1.0f, 0.0f, 0.0f}},
316-
Vertex{.position = {0.5f, -0.5f}, .color = {0.0f, 1.0f, 0.0f}},
317-
Vertex{.position = {0.5f, 0.5f}, .color = {0.0f, 0.0f, 1.0f}},
318-
Vertex{.position = {-0.5f, 0.5f}, .color = {1.0f, 1.0f, 0.0f}},
317+
Vertex{.position = {-200.0f, -200.0f}, .color = {1.0f, 0.0f, 0.0f}},
318+
Vertex{.position = {200.0f, -200.0f}, .color = {0.0f, 1.0f, 0.0f}},
319+
Vertex{.position = {200.0f, 200.0f}, .color = {0.0f, 0.0f, 1.0f}},
320+
Vertex{.position = {-200.0f, 200.0f}, .color = {1.0f, 1.0f, 0.0f}},
319321
};
320322
static constexpr auto indices_v = std::array{
321323
0u, 1u, 2u, 2u, 3u, 0u,
@@ -337,6 +339,9 @@ void App::create_vertex_buffer() {
337339
m_vbo = vma::create_device_buffer(buffer_ci, create_command_block(),
338340
total_bytes_v);
339341

342+
m_view_ubo.emplace(m_allocator.get(), m_gpu.queue_family,
343+
vk::BufferUsageFlagBits::eUniformBuffer);
344+
}
340345

341346
void App::create_descriptor_sets() {
342347
for (auto& descriptor_sets : m_descriptor_sets) {
@@ -448,6 +453,7 @@ void App::render(vk::CommandBuffer const command_buffer) {
448453

449454
command_buffer.beginRendering(rendering_info);
450455
inspect();
456+
update_view();
451457
draw(command_buffer);
452458
command_buffer.endRendering();
453459

@@ -531,8 +537,19 @@ void App::inspect() {
531537
ImGui::End();
532538
}
533539

540+
void App::update_view() {
541+
auto const half_size = 0.5f * glm::vec2{m_framebuffer_size};
542+
auto const mat_projection =
543+
glm::ortho(-half_size.x, half_size.x, -half_size.y, half_size.y);
544+
auto const bytes =
545+
std::bit_cast<std::array<std::byte, sizeof(mat_projection)>>(
546+
mat_projection);
547+
m_view_ubo->write_at(m_frame_index, bytes);
548+
}
549+
534550
void App::draw(vk::CommandBuffer const command_buffer) const {
535551
m_shader->bind(command_buffer, m_framebuffer_size);
552+
bind_descriptor_sets(command_buffer);
536553
// single VBO at binding 0 at no offset.
537554
command_buffer.bindVertexBuffers(0, m_vbo.get().buffer, vk::DeviceSize{});
538555
// u32 indices after offset of 4 vertices.
@@ -541,4 +558,23 @@ void App::draw(vk::CommandBuffer const command_buffer) const {
541558
// m_vbo has 6 indices.
542559
command_buffer.drawIndexed(6, 1, 0, 0, 0);
543560
}
561+
562+
void App::bind_descriptor_sets(vk::CommandBuffer const command_buffer) const {
563+
auto writes = std::array<vk::WriteDescriptorSet, 1>{};
564+
auto const& descriptor_sets = m_descriptor_sets.at(m_frame_index);
565+
auto const set0 = descriptor_sets[0];
566+
auto write = vk::WriteDescriptorSet{};
567+
auto const view_ubo_info = m_view_ubo->descriptor_info_at(m_frame_index);
568+
write.setBufferInfo(view_ubo_info)
569+
.setDescriptorType(vk::DescriptorType::eUniformBuffer)
570+
.setDescriptorCount(1)
571+
.setDstSet(set0)
572+
.setDstBinding(0);
573+
writes[0] = write;
574+
m_device->updateDescriptorSets(writes, {});
575+
576+
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
577+
*m_pipeline_layout, 0, descriptor_sets,
578+
{});
579+
}
544580
} // namespace lvk

src/app.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <gpu.hpp>
55
#include <resource_buffering.hpp>
66
#include <scoped_waiter.hpp>
7+
#include <shader_buffer.hpp>
78
#include <shader_program.hpp>
89
#include <swapchain.hpp>
910
#include <vma.hpp>
@@ -42,6 +43,7 @@ class App {
4243
void create_pipeline_layout();
4344
void create_shader();
4445
void create_cmd_block_pool();
46+
void create_shader_resources();
4547
void create_descriptor_sets();
4648

4749
[[nodiscard]] auto asset_path(std::string_view uri) const -> fs::path;
@@ -59,9 +61,12 @@ class App {
5961

6062
// ImGui code goes here.
6163
void inspect();
64+
void update_view();
6265
// Issue draw calls here.
6366
void draw(vk::CommandBuffer command_buffer) const;
6467

68+
void bind_descriptor_sets(vk::CommandBuffer command_buffer) const;
69+
6570
fs::path m_assets_dir{};
6671

6772
// the order of these RAII members is crucially important.
@@ -93,6 +98,7 @@ class App {
9398
std::optional<ShaderProgram> m_shader{};
9499

95100
vma::Buffer m_vbo{};
101+
std::optional<ShaderBuffer> m_view_ubo{};
96102
Buffered<std::vector<vk::DescriptorSet>> m_descriptor_sets{};
97103

98104
glm::ivec2 m_framebuffer_size{};

src/glsl/shader.vert

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
layout (location = 0) in vec2 a_pos;
44
layout (location = 1) in vec3 a_color;
55

6+
layout (set = 0, binding = 0) uniform View {
7+
mat4 mat_vp;
8+
};
9+
610
layout (location = 0) out vec3 out_color;
711

812
void main() {
9-
const vec2 position = a_pos;
13+
const vec4 world_pos = vec4(a_pos, 0.0, 1.0);
1014

1115
out_color = a_color;
12-
gl_Position = vec4(position, 0.0, 1.0);
16+
gl_Position = mat_vp * world_pos;
1317
}

src/shader_buffer.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#include <shader_buffer.hpp>
2+
3+
namespace lvk {
4+
ShaderBuffer::ShaderBuffer(VmaAllocator allocator,
5+
std::uint32_t const queue_family,
6+
vk::BufferUsageFlags const usage)
7+
: m_allocator(allocator), m_queue_family(queue_family), m_usage(usage) {
8+
// ensure buffers are created and can be bound after returning.
9+
for (auto& buffer : m_buffers) { write_to(buffer, {}); }
10+
}
11+
12+
void ShaderBuffer::write_at(std::size_t const frame_index,
13+
std::span<std::byte const> bytes) {
14+
write_to(m_buffers.at(frame_index), bytes);
15+
}
16+
17+
auto ShaderBuffer::descriptor_info_at(std::size_t const frame_index) const
18+
-> vk::DescriptorBufferInfo {
19+
auto const& buffer = m_buffers.at(frame_index);
20+
auto ret = vk::DescriptorBufferInfo{};
21+
ret.setBuffer(buffer.buffer.get().buffer).setRange(buffer.size);
22+
return ret;
23+
}
24+
25+
void ShaderBuffer::write_to(Buffer& out,
26+
std::span<std::byte const> bytes) const {
27+
static constexpr auto blank_byte_v = std::array{std::byte{}};
28+
// fallback to an empty byte if bytes is empty.
29+
if (bytes.empty()) { bytes = blank_byte_v; }
30+
out.size = bytes.size();
31+
if (out.buffer.get().size < bytes.size()) {
32+
// size is too small (or buffer doesn't exist yet), recreate buffer.
33+
auto const buffer_ci = vma::BufferCreateInfo{
34+
.allocator = m_allocator,
35+
.usage = m_usage,
36+
.queue_family = m_queue_family,
37+
};
38+
out.buffer = vma::create_buffer(buffer_ci, vma::BufferMemoryType::Host,
39+
out.size);
40+
}
41+
std::memcpy(out.buffer.get().mapped, bytes.data(), bytes.size());
42+
}
43+
} // namespace lvk

0 commit comments

Comments
 (0)