Skip to content

Commit 841423a

Browse files
committed
Load shaders
1 parent 10faa5d commit 841423a

File tree

13 files changed

+347
-3
lines changed

13 files changed

+347
-3
lines changed

assets/shader.frag

408 Bytes
Binary file not shown.

assets/shader.vert

1.2 KB
Binary file not shown.

guide/src/SUMMARY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@
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)

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: 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+
```

guide/src/pipeline/shaders.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Shaders
2+
3+
Shaders work in NDC space: -1 to +1 for X and Y. We set up a triangle's coordinates and output that in the vertex shader save it to `src/glsl/shader.vert`:
4+
5+
```glsl
6+
#version 450 core
7+
8+
void main() {
9+
const vec2 positions[] = {
10+
vec2(-0.5, -0.5),
11+
vec2(0.5, -0.5),
12+
vec2(0.0, 0.5),
13+
};
14+
15+
const vec2 position = positions[gl_VertexIndex];
16+
17+
gl_Position = vec4(position, 0.0, 1.0);
18+
}
19+
```
20+
21+
The fragment shader just outputs white for now, in `src/glsl/shader.frag`:
22+
23+
```glsl
24+
#version 450 core
25+
26+
layout (location = 0) out vec4 out_color;
27+
28+
void main() {
29+
out_color = vec4(1.0);
30+
}
31+
```
32+
33+
Compile both shaders into `assets/`:
34+
35+
```
36+
glslc src/glsl/shader.vert -o assets/shader.vert
37+
glslc src/glsl/shader.frag -o assets/shader.frag
38+
```
39+
40+
## Shader Modules
41+
42+
SPIR-V modules are binary files with a stride/alignment of 4 bytes. The Vulkan API accepts a span of `std::uint32_t`s, so we need to load it into such a buffer (and _not_ `std::vector<std::byte>` or other 1-byte equivalents).
43+
44+
Add a new `class ShaderLoader`:
45+
46+
```cpp
47+
class ShaderLoader {
48+
public:
49+
explicit ShaderLoader(vk::Device const device) : m_device(device) {}
50+
51+
[[nodiscard]] auto load(fs::path const& path) -> vk::UniqueShaderModule;
52+
53+
private:
54+
vk::Device m_device{};
55+
std::vector<std::uint32_t> m_code{};
56+
};
57+
```
58+
59+
Implement `load()`:
60+
61+
```cpp
62+
auto ShaderLoader::load(fs::path const& path) -> vk::UniqueShaderModule {
63+
// open the file at the end, to get the total size.
64+
auto file = std::ifstream{path, std::ios::binary | std::ios::ate};
65+
if (!file.is_open()) {
66+
std::println(stderr, "Failed to open file: '{}'",
67+
path.generic_string());
68+
return {};
69+
}
70+
71+
auto const size = file.tellg();
72+
auto const usize = static_cast<std::uint64_t>(size);
73+
// file data must be uint32 aligned.
74+
if (usize % sizeof(std::uint32_t) != 0) {
75+
std::println(stderr, "Invalid SPIR-V size: {}", usize);
76+
return {};
77+
}
78+
79+
// seek to the beginning before reading.
80+
file.seekg({}, std::ios::beg);
81+
m_code.resize(usize / sizeof(std::uint32_t));
82+
void* data = m_code.data();
83+
file.read(static_cast<char*>(data), size);
84+
85+
auto shader_module_ci = vk::ShaderModuleCreateInfo{};
86+
shader_module_ci.setCode(m_code);
87+
return m_device.createShaderModuleUnique(shader_module_ci);
88+
}
89+
```
90+
91+
Add new members to `App`:
92+
93+
```cpp
94+
void create_pipeline();
95+
96+
[[nodiscard]] auto asset_path(std::string_view uri) const -> fs::path;
97+
```
98+
99+
Implement and call `create_pipeline()` before starting the main loop:
100+
101+
```cpp
102+
auto App::asset_path(std::string_view const uri) const -> fs::path {
103+
return m_assets_dir / uri;
104+
}
105+
106+
void App::create_pipeline() {
107+
auto shader_loader = ShaderLoader{*m_device};
108+
// we only need shader modules to create the pipeline, thus no need to store
109+
// them as members.
110+
auto const vertex = shader_loader.load(asset_path("shader.vert"));
111+
auto const fragment = shader_loader.load(asset_path("shader.frag"));
112+
if (!vertex || !fragment) {
113+
throw std::runtime_error{"Failed to load Shaders"};
114+
}
115+
std::println("[lvk] Shaders loaded");
116+
117+
// TODO
118+
}
119+
```

src/app.cpp

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <app.hpp>
2+
#include <shader_loader.hpp>
23
#include <cassert>
34
#include <chrono>
45
#include <print>
@@ -9,7 +10,28 @@ VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE
910
namespace lvk {
1011
using namespace std::chrono_literals;
1112

12-
void App::run() {
13+
namespace {
14+
[[nodiscard]] auto locate_assets_dir(std::string_view const in) -> fs::path {
15+
if (!in.empty()) {
16+
std::println("[lvk] Using custom assets directory: '{}'", in);
17+
return in;
18+
}
19+
// look for '<path>/assets/', starting from the working
20+
// directory and walking up the parent directory tree.
21+
static constexpr std::string_view dir_name_v{"assets"};
22+
for (auto path = fs::current_path();
23+
!path.empty() && path.has_parent_path(); path = path.parent_path()) {
24+
auto ret = path / dir_name_v;
25+
if (fs::is_directory(ret)) { return ret; }
26+
}
27+
std::println("[lvk] Warning: could not locate 'assets' directory");
28+
return fs::current_path();
29+
}
30+
} // namespace
31+
32+
void App::run(std::string_view const assets_dir) {
33+
m_assets_dir = locate_assets_dir(assets_dir);
34+
1335
create_window();
1436
create_instance();
1537
create_surface();
@@ -19,6 +41,8 @@ void App::run() {
1941
create_render_sync();
2042
create_imgui();
2143

44+
create_pipeline();
45+
2246
main_loop();
2347
}
2448

@@ -153,6 +177,24 @@ void App::create_imgui() {
153177
m_imgui.emplace(imgui_ci);
154178
}
155179

180+
auto App::asset_path(std::string_view const uri) const -> fs::path {
181+
return m_assets_dir / uri;
182+
}
183+
184+
void App::create_pipeline() {
185+
auto shader_loader = ShaderLoader{*m_device};
186+
// we only need shader modules to create the pipeline, thus no need to store
187+
// them as members.
188+
auto const vertex = shader_loader.load(asset_path("shader.vert"));
189+
auto const fragment = shader_loader.load(asset_path("shader.frag"));
190+
if (!vertex || !fragment) {
191+
throw std::runtime_error{"Failed to load Shaders"};
192+
}
193+
std::println("[lvk] Shaders loaded");
194+
195+
// TODO
196+
}
197+
156198
void App::main_loop() {
157199
while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) {
158200
glfwPollEvents();

src/app.hpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
#include <swapchain.hpp>
77
#include <vulkan/vulkan.hpp>
88
#include <window.hpp>
9+
#include <filesystem>
910

1011
namespace lvk {
12+
namespace fs = std::filesystem;
13+
1114
class App {
1215
public:
13-
void run();
16+
void run(std::string_view assets_dir);
1417

1518
private:
1619
struct RenderSync {
@@ -32,6 +35,9 @@ class App {
3235
void create_swapchain();
3336
void create_render_sync();
3437
void create_imgui();
38+
void create_pipeline();
39+
40+
[[nodiscard]] auto asset_path(std::string_view uri) const -> fs::path;
3541

3642
void main_loop();
3743

@@ -42,6 +48,8 @@ class App {
4248
void transition_for_present(vk::CommandBuffer command_buffer) const;
4349
void submit_and_present();
4450

51+
fs::path m_assets_dir{};
52+
4553
// the order of these RAII members is crucially important.
4654
glfw::Window m_window{};
4755
vk::UniqueInstance m_instance{};

src/glsl/shader.frag

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#version 450 core
2+
3+
layout (location = 0) out vec4 out_color;
4+
5+
void main() {
6+
out_color = vec4(1.0);
7+
}

src/glsl/shader.vert

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#version 450 core
2+
3+
void main() {
4+
const vec2 positions[] = {
5+
vec2(-0.5, -0.5),
6+
vec2(0.5, -0.5),
7+
vec2(0.0, 0.5),
8+
};
9+
10+
const vec2 position = positions[gl_VertexIndex];
11+
12+
gl_Position = vec4(position, 0.0, 1.0);
13+
}

0 commit comments

Comments
 (0)