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 added assets/shader.frag
Binary file not shown.
Binary file added 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 @@ -24,3 +24,9 @@
- [Dear ImGui](dear_imgui/README.md)
- [class DearImGui](dear_imgui/dear_imgui.md)
- [ImGui Integration](dear_imgui/imgui_integration.md)
- [Graphics Pipeline](pipeline/README.md)
- [Locating Assets](pipeline/locating_assets.md)
- [Shaders](pipeline/shaders.md)
- [Pipeline Creation](pipeline/pipeline_creation.md)
- [Drawing a Triangle](pipeline/drawing_triangle.md)
- [Switching Pipelines](pipeline/switching_pipelines.md)
14 changes: 14 additions & 0 deletions guide/src/pipeline/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Graphics Pipeline

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:

1. Input Assembly: vertex buffers are read here
1. Vertex Shader: shader is run for each vertex in the primitive
1. Early Fragment Tests (EFT): pre-shading tests
1. Fragment Shader: shader is run for each fragment
1. Late Fragment Tests (LFT): depth buffer is written here
1. Color Blending: transparency

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

We shall use a single Pipeline Layout that evolves over chapters.
138 changes: 138 additions & 0 deletions guide/src/pipeline/drawing_triangle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Drawing a Triangle

We shall create two pipelines: one for standard draws, one for wireframe draws. Add new `App` members:

```cpp
void create_pipeline_builder();
void create_pipelines();

// ...
std::optional<PipelineBuilder> m_pipeline_builder{};

vk::UniquePipelineLayout m_pipeline_layout{};
struct {
vk::UniquePipeline standard{};
vk::UniquePipeline wireframe{};
} m_pipelines{};
float m_line_width{1.0f};
bool m_wireframe{};
```

Implement and call `create_pipeline_builder()`:

```cpp
void App::create_pipeline_builder() {
auto const pipeline_builder_ci = PipelineBuilder::CreateInfo{
.device = *m_device,
.samples = vk::SampleCountFlagBits::e1,
.color_format = m_swapchain->get_format(),
};
m_pipeline_builder.emplace(pipeline_builder_ci);
}
```

Complete the implementation of `create_pipelines()`:

```cpp
// ...
m_pipeline_layout = m_device->createPipelineLayoutUnique({});

auto pipeline_state = PipelineState{
.vertex_shader = *vertex,
.fragment_shader = *fragment,
};
m_pipelines.standard =
m_pipeline_builder->build(*m_pipeline_layout, pipeline_state);
pipeline_state.polygon_mode = vk::PolygonMode::eLine;
m_pipelines.wireframe =
m_pipeline_builder->build(*m_pipeline_layout, pipeline_state);
if (!m_pipelines.standard || !m_pipelines.wireframe) {
throw std::runtime_error{"Failed to create Graphics Pipelines"};
}
```

Before `render()` grows to an unwieldy size, extract the higher level logic into two member functions:

```cpp
// ImGui code goes here.
void inspect();
// Issue draw calls here.
void draw(vk::Rect2D const& render_area,
vk::CommandBuffer command_buffer) const;

// ...
void App::inspect() {
ImGui::ShowDemoWindow();
// TODO
}

// ...
command_buffer.beginRendering(rendering_info);
inspect();
draw(render_area, command_buffer);
command_buffer.endRendering();
```

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:

```cpp
void App::draw(vk::Rect2D const& render_area,
vk::CommandBuffer const command_buffer) const {
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics,
*m_pipelines.standard);
// we are creating pipelines with dynamic viewport and scissor states.
// they must be set here after binding (before drawing).
auto viewport = vk::Viewport{};
// flip the viewport about the X-axis (negative height):
// https://www.saschawillems.de/blog/2019/03/29/flipping-the-vulkan-viewport/
viewport.setX(0.0f)
.setY(static_cast<float>(m_render_target->extent.height))
.setWidth(static_cast<float>(m_render_target->extent.width))
.setHeight(-viewport.y);
command_buffer.setViewport(0, viewport);
command_buffer.setScissor(0, render_area);
// current shader has hard-coded logic for 3 vertices.
command_buffer.draw(3, 1, 0, 0);
}
```

![White Triangle](./white_triangle.png)

Updating our shaders to use interpolated RGB on each vertex:

```glsl
// shader.vert

layout (location = 0) out vec3 out_color;

// ...
const vec3 colors[] = {
vec3(1.0, 0.0, 0.0),
vec3(0.0, 1.0, 0.0),
vec3(0.0, 0.0, 1.0),
};

// ...
out_color = colors[gl_VertexIndex];

// shader.frag

layout (location = 0) in vec3 in_color;

// ...
out_color = vec4(in_color, 1.0);
```

> Make sure to recompile both the SPIR-V shaders in assets/.

And a black clear color:

```cpp
// ...
.setClearValue(vk::ClearColorValue{0.0f, 0.0f, 0.0f, 1.0f});
```

Gives us the renowned Vulkan sRGB triangle:

![sRGB Triangle](./srgb_triangle.png)

83 changes: 83 additions & 0 deletions guide/src/pipeline/locating_assets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Locating Assets

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:

```
.
|-- assets/
|-- app
|-- build/
|-- app
|-- out/
|-- default/Release/
|-- app
|-- ubsan/Debug/
|-- app
```

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.

## Assets Directory

Add a member to `App` to store this path to `assets/`:

```cpp
namespace fs = std::filesystem;

// ...
fs::path m_assets_dir{};
```

Add a helper function to locate the assets dir, and assign `m_assets_dir` to its return value at the top of `run()`:

```cpp
[[nodiscard]] auto locate_assets_dir() -> fs::path {
// look for '<path>/assets/', starting from the working
// directory and walking up the parent directory tree.
static constexpr std::string_view dir_name_v{"assets"};
for (auto path = fs::current_path();
!path.empty() && path.has_parent_path(); path = path.parent_path()) {
auto ret = path / dir_name_v;
if (fs::is_directory(ret)) { return ret; }
}
std::println("[lvk] Warning: could not locate 'assets' directory");
return fs::current_path();
}

// ...
m_assets_dir = locate_assets_dir();
```

We can also support a command line argument to override this algorithm:

```cpp
// app.hpp
void run(std::string_view assets_dir);

// app.cpp
[[nodiscard]] auto locate_assets_dir(std::string_view const in) -> fs::path {
if (!in.empty()) {
std::println("[lvk] Using custom assets directory: '{}'", in);
return in;
}
// ...
}

// ...
void App::run(std::string_view const assets_dir) {
m_assets_dir = locate_assets_dir(assets_dir);
// ...
}

// main.cpp
auto assets_dir = std::string_view{};

// ...
if (arg == "-x" || arg == "--force-x11") {
glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
}
if (arg == "-a" || arg == "--assets") { assets_dir = arg; }

// ...
lvk::App{}.run(assets_dir);
```
Loading