Skip to content

Commit 39884df

Browse files
authored
Graphics Pipelines (#9)
* Load shaders * WIP * sRGB triangle * Split up pipeline creation, update docs * Fix constexpr error * Log error on pipeline creation failure * Cleanup, wireframe
1 parent 8a9d69f commit 39884df

22 files changed

+1017
-7
lines changed

assets/shader.frag

572 Bytes
Binary file not shown.

assets/shader.vert

1.54 KB
Binary file not shown.

guide/src/SUMMARY.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,9 @@
2424
- [Dear ImGui](dear_imgui/README.md)
2525
- [class DearImGui](dear_imgui/dear_imgui.md)
2626
- [ImGui Integration](dear_imgui/imgui_integration.md)
27+
- [Graphics Pipeline](pipeline/README.md)
28+
- [Locating Assets](pipeline/locating_assets.md)
29+
- [Shaders](pipeline/shaders.md)
30+
- [Pipeline Creation](pipeline/pipeline_creation.md)
31+
- [Drawing a Triangle](pipeline/drawing_triangle.md)
32+
- [Switching Pipelines](pipeline/switching_pipelines.md)

guide/src/pipeline/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Graphics Pipeline
2+
3+
A [Vulkan Graphics Pipeline](https://docs.vulkan.org/spec/latest/chapters/pipelines.html) is a large object that encompasses the entire graphics pipeline. It consists of many stages - all this happens during a single `draw()` call. We again constrain ourselves to caring about a small subset:
4+
5+
1. Input Assembly: vertex buffers are read here
6+
1. Vertex Shader: shader is run for each vertex in the primitive
7+
1. Early Fragment Tests (EFT): pre-shading tests
8+
1. Fragment Shader: shader is run for each fragment
9+
1. Late Fragment Tests (LFT): depth buffer is written here
10+
1. Color Blending: transparency
11+
12+
A Graphics Pipeline's specification broadly includes configuration of the vertex attributes and fixed state (baked into the pipeline), dynamic states (must be set at draw-time), shader code (SPIR-V), and its layout (Descriptor Sets and Push Constants). Creation of a pipeline is verbose _and_ expensive, most engines use some sort of "hash and cache" approach to optimize reuse of existing pipelines. The Descriptor Set Layouts of a shader program need to be explicitly specified, engines can either dictate a static layout or use runtime reflection via [SPIR-V Cross](https://github.com/KhronosGroup/SPIRV-Cross).
13+
14+
We shall use a single Pipeline Layout that evolves over chapters.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Drawing a Triangle
2+
3+
We shall create two pipelines: one for standard draws, one for wireframe draws. Add new `App` members:
4+
5+
```cpp
6+
void create_pipeline_builder();
7+
void create_pipelines();
8+
9+
// ...
10+
std::optional<PipelineBuilder> m_pipeline_builder{};
11+
12+
vk::UniquePipelineLayout m_pipeline_layout{};
13+
struct {
14+
vk::UniquePipeline standard{};
15+
vk::UniquePipeline wireframe{};
16+
} m_pipelines{};
17+
float m_line_width{1.0f};
18+
bool m_wireframe{};
19+
```
20+
21+
Implement and call `create_pipeline_builder()`:
22+
23+
```cpp
24+
void App::create_pipeline_builder() {
25+
auto const pipeline_builder_ci = PipelineBuilder::CreateInfo{
26+
.device = *m_device,
27+
.samples = vk::SampleCountFlagBits::e1,
28+
.color_format = m_swapchain->get_format(),
29+
};
30+
m_pipeline_builder.emplace(pipeline_builder_ci);
31+
}
32+
```
33+
34+
Complete the implementation of `create_pipelines()`:
35+
36+
```cpp
37+
// ...
38+
m_pipeline_layout = m_device->createPipelineLayoutUnique({});
39+
40+
auto pipeline_state = PipelineState{
41+
.vertex_shader = *vertex,
42+
.fragment_shader = *fragment,
43+
};
44+
m_pipelines.standard =
45+
m_pipeline_builder->build(*m_pipeline_layout, pipeline_state);
46+
pipeline_state.polygon_mode = vk::PolygonMode::eLine;
47+
m_pipelines.wireframe =
48+
m_pipeline_builder->build(*m_pipeline_layout, pipeline_state);
49+
if (!m_pipelines.standard || !m_pipelines.wireframe) {
50+
throw std::runtime_error{"Failed to create Graphics Pipelines"};
51+
}
52+
```
53+
54+
Before `render()` grows to an unwieldy size, extract the higher level logic into two member functions:
55+
56+
```cpp
57+
// ImGui code goes here.
58+
void inspect();
59+
// Issue draw calls here.
60+
void draw(vk::Rect2D const& render_area,
61+
vk::CommandBuffer command_buffer) const;
62+
63+
// ...
64+
void App::inspect() {
65+
ImGui::ShowDemoWindow();
66+
// TODO
67+
}
68+
69+
// ...
70+
command_buffer.beginRendering(rendering_info);
71+
inspect();
72+
draw(render_area, command_buffer);
73+
command_buffer.endRendering();
74+
```
75+
76+
We can now bind a pipeline and use it to draw the triangle in the shader. Making `draw()` `const` forces us to ensure no `App` state is changed:
77+
78+
```cpp
79+
void App::draw(vk::Rect2D const& render_area,
80+
vk::CommandBuffer const command_buffer) const {
81+
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics,
82+
*m_pipelines.standard);
83+
// we are creating pipelines with dynamic viewport and scissor states.
84+
// they must be set here after binding (before drawing).
85+
auto viewport = vk::Viewport{};
86+
// flip the viewport about the X-axis (negative height):
87+
// https://www.saschawillems.de/blog/2019/03/29/flipping-the-vulkan-viewport/
88+
viewport.setX(0.0f)
89+
.setY(static_cast<float>(m_render_target->extent.height))
90+
.setWidth(static_cast<float>(m_render_target->extent.width))
91+
.setHeight(-viewport.y);
92+
command_buffer.setViewport(0, viewport);
93+
command_buffer.setScissor(0, render_area);
94+
// current shader has hard-coded logic for 3 vertices.
95+
command_buffer.draw(3, 1, 0, 0);
96+
}
97+
```
98+
99+
![White Triangle](./white_triangle.png)
100+
101+
Updating our shaders to use interpolated RGB on each vertex:
102+
103+
```glsl
104+
// shader.vert
105+
106+
layout (location = 0) out vec3 out_color;
107+
108+
// ...
109+
const vec3 colors[] = {
110+
vec3(1.0, 0.0, 0.0),
111+
vec3(0.0, 1.0, 0.0),
112+
vec3(0.0, 0.0, 1.0),
113+
};
114+
115+
// ...
116+
out_color = colors[gl_VertexIndex];
117+
118+
// shader.frag
119+
120+
layout (location = 0) in vec3 in_color;
121+
122+
// ...
123+
out_color = vec4(in_color, 1.0);
124+
```
125+
126+
> Make sure to recompile both the SPIR-V shaders in assets/.
127+
128+
And a black clear color:
129+
130+
```cpp
131+
// ...
132+
.setClearValue(vk::ClearColorValue{0.0f, 0.0f, 0.0f, 1.0f});
133+
```
134+
135+
Gives us the renowned Vulkan sRGB triangle:
136+
137+
![sRGB Triangle](./srgb_triangle.png)
138+

guide/src/pipeline/locating_assets.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Locating Assets
2+
3+
Before we can use shaders (and thus graphics pipelines), we need to load them as asset/data files. To do that correctly, first the asset directory needs to be located. There are a few ways to go about this, we will use the approach of looking for a particular subdirectory, starting from the working directory and walking up the parent directory tree. This enables `app` in any project/build subdirectory to locate `assets/` in the various examples below:
4+
5+
```
6+
.
7+
|-- assets/
8+
|-- app
9+
|-- build/
10+
|-- app
11+
|-- out/
12+
|-- default/Release/
13+
|-- app
14+
|-- ubsan/Debug/
15+
|-- app
16+
```
17+
18+
In a release package you would want to use the path to the executable instead (and probably not perform an "upfind" walk), the working directory could be anywhere whereas assets shipped with the package will be in the vicinity of the executable.
19+
20+
## Assets Directory
21+
22+
Add a member to `App` to store this path to `assets/`:
23+
24+
```cpp
25+
namespace fs = std::filesystem;
26+
27+
// ...
28+
fs::path m_assets_dir{};
29+
```
30+
31+
Add a helper function to locate the assets dir, and assign `m_assets_dir` to its return value at the top of `run()`:
32+
33+
```cpp
34+
[[nodiscard]] auto locate_assets_dir() -> fs::path {
35+
// look for '<path>/assets/', starting from the working
36+
// directory and walking up the parent directory tree.
37+
static constexpr std::string_view dir_name_v{"assets"};
38+
for (auto path = fs::current_path();
39+
!path.empty() && path.has_parent_path(); path = path.parent_path()) {
40+
auto ret = path / dir_name_v;
41+
if (fs::is_directory(ret)) { return ret; }
42+
}
43+
std::println("[lvk] Warning: could not locate 'assets' directory");
44+
return fs::current_path();
45+
}
46+
47+
// ...
48+
m_assets_dir = locate_assets_dir();
49+
```
50+
51+
We can also support a command line argument to override this algorithm:
52+
53+
```cpp
54+
// app.hpp
55+
void run(std::string_view assets_dir);
56+
57+
// app.cpp
58+
[[nodiscard]] auto locate_assets_dir(std::string_view const in) -> fs::path {
59+
if (!in.empty()) {
60+
std::println("[lvk] Using custom assets directory: '{}'", in);
61+
return in;
62+
}
63+
// ...
64+
}
65+
66+
// ...
67+
void App::run(std::string_view const assets_dir) {
68+
m_assets_dir = locate_assets_dir(assets_dir);
69+
// ...
70+
}
71+
72+
// main.cpp
73+
auto assets_dir = std::string_view{};
74+
75+
// ...
76+
if (arg == "-x" || arg == "--force-x11") {
77+
glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
78+
}
79+
if (arg == "-a" || arg == "--assets") { assets_dir = arg; }
80+
81+
// ...
82+
lvk::App{}.run(assets_dir);
83+
```

0 commit comments

Comments
 (0)