diff --git a/assets/shader.frag b/assets/shader.frag new file mode 100644 index 0000000..782c404 Binary files /dev/null and b/assets/shader.frag differ diff --git a/assets/shader.vert b/assets/shader.vert new file mode 100644 index 0000000..1d47401 Binary files /dev/null and b/assets/shader.vert differ diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index f04d2c3..d86d249 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -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) diff --git a/guide/src/pipeline/README.md b/guide/src/pipeline/README.md new file mode 100644 index 0000000..75f8821 --- /dev/null +++ b/guide/src/pipeline/README.md @@ -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. diff --git a/guide/src/pipeline/drawing_triangle.md b/guide/src/pipeline/drawing_triangle.md new file mode 100644 index 0000000..e4c22ad --- /dev/null +++ b/guide/src/pipeline/drawing_triangle.md @@ -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 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(m_render_target->extent.height)) + .setWidth(static_cast(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) + diff --git a/guide/src/pipeline/locating_assets.md b/guide/src/pipeline/locating_assets.md new file mode 100644 index 0000000..1dfaaeb --- /dev/null +++ b/guide/src/pipeline/locating_assets.md @@ -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 '/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); +``` diff --git a/guide/src/pipeline/pipeline_creation.md b/guide/src/pipeline/pipeline_creation.md new file mode 100644 index 0000000..d1c44b2 --- /dev/null +++ b/guide/src/pipeline/pipeline_creation.md @@ -0,0 +1,202 @@ +# Pipeline Creation + +To constrain ourselves to a subset of pipeline state to worry about, create a custom `struct PipelineState`: + +```cpp +// bit flags for various binary Pipeline States. +struct PipelineFlag { + enum : std::uint8_t { + None = 0, + AlphaBlend = 1 << 0, // turn on alpha blending. + DepthTest = 1 << 1, // turn on depth write and test. + }; +}; + +// specification of a unique Graphics Pipeline. +struct PipelineState { + using Flag = PipelineFlag; + + [[nodiscard]] static constexpr auto default_flags() -> std::uint8_t { + return Flag::AlphaBlend | Flag::DepthTest; + } + + vk::ShaderModule vertex_shader; // required. + vk::ShaderModule fragment_shader; // required. + + std::span vertex_attributes{}; + std::span vertex_bindings{}; + + vk::PrimitiveTopology topology{vk::PrimitiveTopology::eTriangleList}; + vk::PolygonMode polygon_mode{vk::PolygonMode::eFill}; + vk::CullModeFlags cull_mode{vk::CullModeFlagBits::eNone}; + vk::CompareOp depth_compare{vk::CompareOp::eLess}; + std::uint8_t flags{default_flags()}; +}; +``` + +Encapsulate the exhausting process of building a pipeline into its own class: + +```cpp +struct PipelineBuilderCreateInfo { + vk::Device device{}; + vk::SampleCountFlagBits samples{}; + vk::Format color_format{}; + vk::Format depth_format{}; +}; + +class PipelineBuilder { + public: + using CreateInfo = PipelineBuilderCreateInfo; + + explicit PipelineBuilder(CreateInfo const& create_info); + + [[nodiscard]] auto build(vk::PipelineLayout layout, + PipelineState const& state) const + -> vk::UniquePipeline; + + private: + CreateInfo m_info{}; +}; +``` + +Before implementing `build()`, add some helper functions/constants, starting with the viewport and dynamic states: + +```cpp +// single viewport and scissor. +constexpr auto viewport_state_v = + vk::PipelineViewportStateCreateInfo({}, 1, {}, 1); + +// these dynamic states are guaranteed to be available. +constexpr auto dynamic_states_v = std::array{ + vk::DynamicState::eViewport, + vk::DynamicState::eScissor, + vk::DynamicState::eLineWidth, +}; +``` + +The shader stages: + +```cpp +[[nodiscard]] constexpr auto +create_shader_stages(vk::ShaderModule const vertex, + vk::ShaderModule const fragment) { + // set vertex (0) and fragment (1) shader stages. + auto ret = std::array{}; + ret[0] + .setStage(vk::ShaderStageFlagBits::eVertex) + .setPName("main") + .setModule(vertex); + ret[1] + .setStage(vk::ShaderStageFlagBits::eFragment) + .setPName("main") + .setModule(fragment); + return ret; +} +``` + +The depth/stencil state: + +```cpp +[[nodiscard]] constexpr auto +create_depth_stencil_state(std::uint8_t flags, + vk::CompareOp const depth_compare) { + auto ret = vk::PipelineDepthStencilStateCreateInfo{}; + auto const depth_test = + (flags & PipelineFlag::DepthTest) == PipelineFlag::DepthTest; + ret.setDepthTestEnable(depth_test ? vk::True : vk::False) + .setDepthCompareOp(depth_compare); + return ret; +} +``` + +And a color blend attachment: + +```cpp +[[nodiscard]] constexpr auto +create_color_blend_attachment(std::uint8_t const flags) { + auto ret = vk::PipelineColorBlendAttachmentState{}; + auto const alpha_blend = + (flags & PipelineFlag::AlphaBlend) == PipelineFlag::AlphaBlend; + using CCF = vk::ColorComponentFlagBits; + ret.setColorWriteMask(CCF::eR | CCF::eG | CCF::eB | CCF::eA) + .setBlendEnable(alpha_blend ? vk::True : vk::False) + // standard alpha blending: + // (alpha * src) + (1 - alpha) * dst + .setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) + .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha) + .setColorBlendOp(vk::BlendOp::eAdd) + .setSrcAlphaBlendFactor(vk::BlendFactor::eOne) + .setDstAlphaBlendFactor(vk::BlendFactor::eZero) + .setAlphaBlendOp(vk::BlendOp::eAdd); + return ret; +} +``` + +Now we can implement `build()`: + +```cpp +auto PipelineBuilder::build(vk::PipelineLayout const layout, + PipelineState const& state) const + -> vk::UniquePipeline { + auto const shader_stage_ci = + create_shader_stages(state.vertex_shader, state.fragment_shader); + + auto vertex_input_ci = vk::PipelineVertexInputStateCreateInfo{}; + vertex_input_ci.setVertexAttributeDescriptions(state.vertex_attributes) + .setVertexBindingDescriptions(state.vertex_bindings); + + auto multisample_state_ci = vk::PipelineMultisampleStateCreateInfo{}; + multisample_state_ci.setRasterizationSamples(m_info.samples) + .setSampleShadingEnable(vk::False); + + auto const input_assembly_ci = + vk::PipelineInputAssemblyStateCreateInfo{{}, state.topology}; + + auto rasterization_state_ci = vk::PipelineRasterizationStateCreateInfo{}; + rasterization_state_ci.setPolygonMode(state.polygon_mode) + .setCullMode(state.cull_mode); + + auto const depth_stencil_state_ci = + create_depth_stencil_state(state.flags, state.depth_compare); + + auto const color_blend_attachment = + create_color_blend_attachment(state.flags); + auto color_blend_state_ci = vk::PipelineColorBlendStateCreateInfo{}; + color_blend_state_ci.setAttachments(color_blend_attachment); + + auto dynamic_state_ci = vk::PipelineDynamicStateCreateInfo{}; + dynamic_state_ci.setDynamicStates(dynamic_states_v); + + // Dynamic Rendering requires passing this in the pNext chain. + auto rendering_ci = vk::PipelineRenderingCreateInfo{}; + // could be a depth-only pass, argument is span-like (notice the plural + // `Formats()`), only set if not Undefined. + if (m_info.color_format != vk::Format::eUndefined) { + rendering_ci.setColorAttachmentFormats(m_info.color_format); + } + // single depth attachment format, ok to set to Undefined. + rendering_ci.setDepthAttachmentFormat(m_info.depth_format); + + auto pipeline_ci = vk::GraphicsPipelineCreateInfo{}; + pipeline_ci.setLayout(layout) + .setStages(shader_stage_ci) + .setPVertexInputState(&vertex_input_ci) + .setPViewportState(&viewport_state_v) + .setPMultisampleState(&multisample_state_ci) + .setPInputAssemblyState(&input_assembly_ci) + .setPRasterizationState(&rasterization_state_ci) + .setPDepthStencilState(&depth_stencil_state_ci) + .setPColorBlendState(&color_blend_state_ci) + .setPDynamicState(&dynamic_state_ci) + .setPNext(&rendering_ci); + + auto ret = vk::Pipeline{}; + // use non-throwing API. + if (m_info.device.createGraphicsPipelines({}, 1, &pipeline_ci, {}, &ret) != + vk::Result::eSuccess) { + return {}; + } + + return vk::UniquePipeline{ret, m_info.device}; +} +``` diff --git a/guide/src/pipeline/shaders.md b/guide/src/pipeline/shaders.md new file mode 100644 index 0000000..dfc0571 --- /dev/null +++ b/guide/src/pipeline/shaders.md @@ -0,0 +1,121 @@ +# Shaders + +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`: + +```glsl +#version 450 core + +void main() { + const vec2 positions[] = { + vec2(-0.5, -0.5), + vec2(0.5, -0.5), + vec2(0.0, 0.5), + }; + + const vec2 position = positions[gl_VertexIndex]; + + gl_Position = vec4(position, 0.0, 1.0); +} +``` + +The fragment shader just outputs white for now, in `src/glsl/shader.frag`: + +```glsl +#version 450 core + +layout (location = 0) out vec4 out_color; + +void main() { + out_color = vec4(1.0); +} +``` + +Compile both shaders into `assets/`: + +``` +glslc src/glsl/shader.vert -o assets/shader.vert +glslc src/glsl/shader.frag -o assets/shader.frag +``` + +> glslc is part of the Vulkan SDK. + +## Shader Modules + +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` or other 1-byte equivalents). + +Add a new `class ShaderLoader`: + +```cpp +class ShaderLoader { + public: + explicit ShaderLoader(vk::Device const device) : m_device(device) {} + + [[nodiscard]] auto load(fs::path const& path) -> vk::UniqueShaderModule; + + private: + vk::Device m_device{}; + std::vector m_code{}; +}; +``` + +Implement `load()`: + +```cpp +auto ShaderLoader::load(fs::path const& path) -> vk::UniqueShaderModule { + // open the file at the end, to get the total size. + auto file = std::ifstream{path, std::ios::binary | std::ios::ate}; + if (!file.is_open()) { + std::println(stderr, "Failed to open file: '{}'", + path.generic_string()); + return {}; + } + + auto const size = file.tellg(); + auto const usize = static_cast(size); + // file data must be uint32 aligned. + if (usize % sizeof(std::uint32_t) != 0) { + std::println(stderr, "Invalid SPIR-V size: {}", usize); + return {}; + } + + // seek to the beginning before reading. + file.seekg({}, std::ios::beg); + m_code.resize(usize / sizeof(std::uint32_t)); + void* data = m_code.data(); + file.read(static_cast(data), size); + + auto shader_module_ci = vk::ShaderModuleCreateInfo{}; + shader_module_ci.setCode(m_code); + return m_device.createShaderModuleUnique(shader_module_ci); +} +``` + +Add new members to `App`: + +```cpp +void create_pipelines(); + +[[nodiscard]] auto asset_path(std::string_view uri) const -> fs::path; +``` + +Add code to load shaders in `create_pipelines()` and call it before starting the main loop: + +```cpp +void App::create_pipelines() { + auto shader_loader = ShaderLoader{*m_device}; + // we only need shader modules to create the pipelines, thus no need to + // store them as members. + auto const vertex = shader_loader.load(asset_path("shader.vert")); + auto const fragment = shader_loader.load(asset_path("shader.frag")); + if (!vertex || !fragment) { + throw std::runtime_error{"Failed to load Shaders"}; + } + std::println("[lvk] Shaders loaded"); + + // TODO +} + +auto App::asset_path(std::string_view const uri) const -> fs::path { + return m_assets_dir / uri; +} +``` diff --git a/guide/src/pipeline/srgb_triangle.png b/guide/src/pipeline/srgb_triangle.png new file mode 100644 index 0000000..a160a61 Binary files /dev/null and b/guide/src/pipeline/srgb_triangle.png differ diff --git a/guide/src/pipeline/srgb_triangle_wireframe.png b/guide/src/pipeline/srgb_triangle_wireframe.png new file mode 100644 index 0000000..92b9c6b Binary files /dev/null and b/guide/src/pipeline/srgb_triangle_wireframe.png differ diff --git a/guide/src/pipeline/switching_pipelines.md b/guide/src/pipeline/switching_pipelines.md new file mode 100644 index 0000000..93be2e0 --- /dev/null +++ b/guide/src/pipeline/switching_pipelines.md @@ -0,0 +1,39 @@ +# Switching Pipelines + +We can use an ImGui window to inspect / tweak some pipeline state: + +```cpp +ImGui::ShowDemoWindow(); + +ImGui::SetNextWindowSize({200.0f, 100.0f}, ImGuiCond_Once); +if (ImGui::Begin("Inspect")) { + ImGui::Checkbox("wireframe", &m_wireframe); + if (m_wireframe) { + auto const& line_width_range = + m_gpu.properties.limits.lineWidthRange; + ImGui::SetNextItemWidth(100.0f); + ImGui::DragFloat("line width", &m_line_width, 0.25f, + line_width_range[0], line_width_range[1]); + } +} +ImGui::End(); +``` + +Modify `draw()` to use the selected pipeline, and also set the line width: + +```cpp +auto const pipeline = + m_wireframe ? *m_pipelines.wireframe : *m_pipelines.standard; +command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); + +// ... +// line width is also a dynamic state in our pipelines, but setting it is +// not required (defaults to 1.0f or the minimum reported limit). +command_buffer.setLineWidth(m_line_width); +``` + +And that's it. + +![sRGB Triangle (wireframe)](./srgb_triangle_wireframe.png) + +In a system with dynamic pipeline creation, the first frame where `m_wireframe == true` will trigger the complex pipeline building step. This can cause stutters, especially if many different shaders/pipelines are involved in a short period of "experience" time. This is why many modern games/engines have the dreaded "Building/Compiling Shaders" screen, where all the pipelines are being built and cached. diff --git a/guide/src/pipeline/white_triangle.png b/guide/src/pipeline/white_triangle.png new file mode 100644 index 0000000..e8de477 Binary files /dev/null and b/guide/src/pipeline/white_triangle.png differ diff --git a/src/app.cpp b/src/app.cpp index 16583f5..879f00f 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -9,7 +10,28 @@ VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE namespace lvk { using namespace std::chrono_literals; -void App::run() { +namespace { +[[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; + } + // look for '/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(); +} +} // namespace + +void App::run(std::string_view const assets_dir) { + m_assets_dir = locate_assets_dir(assets_dir); + create_window(); create_instance(); create_surface(); @@ -18,6 +40,9 @@ void App::run() { create_swapchain(); create_render_sync(); create_imgui(); + create_pipeline_builder(); + + create_pipelines(); main_loop(); } @@ -153,6 +178,46 @@ void App::create_imgui() { m_imgui.emplace(imgui_ci); } +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); +} + +void App::create_pipelines() { + auto shader_loader = ShaderLoader{*m_device}; + // we only need shader modules to create the pipelines, thus no need to + // store them as members. + auto const vertex = shader_loader.load(asset_path("shader.vert")); + auto const fragment = shader_loader.load(asset_path("shader.frag")); + if (!vertex || !fragment) { + throw std::runtime_error{"Failed to load Shaders"}; + } + std::println("[lvk] Shaders loaded"); + + 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"}; + } +} + +auto App::asset_path(std::string_view const uri) const -> fs::path { + return m_assets_dir / uri; +} + void App::main_loop() { while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) { glfwPollEvents(); @@ -230,7 +295,7 @@ void App::render(vk::CommandBuffer const command_buffer) { .setLoadOp(vk::AttachmentLoadOp::eClear) .setStoreOp(vk::AttachmentStoreOp::eStore) // temporarily red. - .setClearValue(vk::ClearColorValue{1.0f, 0.0f, 0.0f, 1.0f}); + .setClearValue(vk::ClearColorValue{0.0f, 0.0f, 0.0f, 1.0f}); auto rendering_info = vk::RenderingInfo{}; auto const render_area = vk::Rect2D{vk::Offset2D{}, m_render_target->extent}; @@ -239,8 +304,8 @@ void App::render(vk::CommandBuffer const command_buffer) { .setLayerCount(1); command_buffer.beginRendering(rendering_info); - ImGui::ShowDemoWindow(); - // draw stuff here. + inspect(); + draw(render_area, command_buffer); command_buffer.endRendering(); m_imgui->end_frame(); @@ -296,4 +361,44 @@ void App::submit_and_present() { m_swapchain->recreate(m_framebuffer_size); } } + +void App::inspect() { + ImGui::ShowDemoWindow(); + + ImGui::SetNextWindowSize({200.0f, 100.0f}, ImGuiCond_Once); + if (ImGui::Begin("Inspect")) { + ImGui::Checkbox("wireframe", &m_wireframe); + if (m_wireframe) { + auto const& line_width_range = + m_gpu.properties.limits.lineWidthRange; + ImGui::SetNextItemWidth(100.0f); + ImGui::DragFloat("line width", &m_line_width, 0.25f, + line_width_range[0], line_width_range[1]); + } + } + ImGui::End(); +} + +void App::draw(vk::Rect2D const& render_area, + vk::CommandBuffer const command_buffer) const { + auto const pipeline = + m_wireframe ? *m_pipelines.wireframe : *m_pipelines.standard; + command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); + // 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(m_render_target->extent.height)) + .setWidth(static_cast(m_render_target->extent.width)) + .setHeight(-viewport.y); + command_buffer.setViewport(0, viewport); + command_buffer.setScissor(0, render_area); + // line width is also a dynamic state in our pipelines, but setting it is + // not required (defaults to 1.0f). + command_buffer.setLineWidth(m_line_width); + // current shader has hard-coded logic for 3 vertices. + command_buffer.draw(3, 1, 0, 0); +} } // namespace lvk diff --git a/src/app.hpp b/src/app.hpp index 788f44b..9b479d8 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -1,16 +1,19 @@ #pragma once #include #include +#include #include #include #include -#include #include +#include namespace lvk { +namespace fs = std::filesystem; + class App { public: - void run(); + void run(std::string_view assets_dir); private: struct RenderSync { @@ -32,6 +35,10 @@ class App { void create_swapchain(); void create_render_sync(); void create_imgui(); + void create_pipeline_builder(); + void create_pipelines(); + + [[nodiscard]] auto asset_path(std::string_view uri) const -> fs::path; void main_loop(); @@ -42,6 +49,14 @@ class App { void transition_for_present(vk::CommandBuffer command_buffer) const; void submit_and_present(); + // ImGui code goes here. + void inspect(); + // Issue draw calls here. + void draw(vk::Rect2D const& render_area, + vk::CommandBuffer command_buffer) const; + + fs::path m_assets_dir{}; + // the order of these RAII members is crucially important. glfw::Window m_window{}; vk::UniqueInstance m_instance{}; @@ -59,6 +74,15 @@ class App { std::size_t m_frame_index{}; std::optional m_imgui{}; + std::optional 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{}; glm::ivec2 m_framebuffer_size{}; std::optional m_render_target{}; diff --git a/src/glsl/shader.frag b/src/glsl/shader.frag new file mode 100644 index 0000000..93094d4 --- /dev/null +++ b/src/glsl/shader.frag @@ -0,0 +1,9 @@ +#version 450 core + +layout (location = 0) in vec3 in_color; + +layout (location = 0) out vec4 out_color; + +void main() { + out_color = vec4(in_color, 1.0); +} diff --git a/src/glsl/shader.vert b/src/glsl/shader.vert new file mode 100644 index 0000000..8efde56 --- /dev/null +++ b/src/glsl/shader.vert @@ -0,0 +1,22 @@ +#version 450 core + +layout (location = 0) out vec3 out_color; + +void main() { + const vec2 positions[] = { + vec2(-0.5, -0.5), + vec2(0.5, -0.5), + vec2(0.0, 0.5), + }; + + const vec3 colors[] = { + vec3(1.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(0.0, 0.0, 1.0), + }; + + const vec2 position = positions[gl_VertexIndex]; + + out_color = colors[gl_VertexIndex]; + gl_Position = vec4(position, 0.0, 1.0); +} diff --git a/src/main.cpp b/src/main.cpp index 238dd5c..ffeaee6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ auto main(int argc, char** argv) -> int { try { + auto assets_dir = std::string_view{}; // skip the first argument. auto args = std::span{argv, static_cast(argc)}.subspan(1); while (!args.empty()) { @@ -12,9 +13,10 @@ auto main(int argc, char** argv) -> int { if (arg == "-x" || arg == "--force-x11") { glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11); } + if (arg == "-a" || arg == "--assets") { assets_dir = arg; } args = args.subspan(1); } - lvk::App{}.run(); + lvk::App{}.run(assets_dir); } catch (std::exception const& e) { std::println(stderr, "PANIC: {}", e.what()); return EXIT_FAILURE; diff --git a/src/pipeline_builder.cpp b/src/pipeline_builder.cpp new file mode 100644 index 0000000..0e76862 --- /dev/null +++ b/src/pipeline_builder.cpp @@ -0,0 +1,131 @@ +#include +#include + +namespace lvk { +namespace { +// single viewport and scissor. +constexpr auto viewport_state_v = + vk::PipelineViewportStateCreateInfo({}, 1, {}, 1); + +// these dynamic states are guaranteed to be available. +constexpr auto dynamic_states_v = std::array{ + vk::DynamicState::eViewport, + vk::DynamicState::eScissor, + vk::DynamicState::eLineWidth, +}; + +[[nodiscard]] auto create_shader_stages(vk::ShaderModule const vertex, + vk::ShaderModule const fragment) { + // set vertex (0) and fragment (1) shader stages. + auto ret = std::array{}; + ret[0] + .setStage(vk::ShaderStageFlagBits::eVertex) + .setPName("main") + .setModule(vertex); + ret[1] + .setStage(vk::ShaderStageFlagBits::eFragment) + .setPName("main") + .setModule(fragment); + return ret; +} + +[[nodiscard]] constexpr auto +create_depth_stencil_state(std::uint8_t flags, + vk::CompareOp const depth_compare) { + auto ret = vk::PipelineDepthStencilStateCreateInfo{}; + auto const depth_test = + (flags & PipelineFlag::DepthTest) == PipelineFlag::DepthTest; + ret.setDepthTestEnable(depth_test ? vk::True : vk::False) + .setDepthCompareOp(depth_compare); + return ret; +} + +[[nodiscard]] constexpr auto +create_color_blend_attachment(std::uint8_t const flags) { + auto ret = vk::PipelineColorBlendAttachmentState{}; + auto const alpha_blend = + (flags & PipelineFlag::AlphaBlend) == PipelineFlag::AlphaBlend; + using CCF = vk::ColorComponentFlagBits; + ret.setColorWriteMask(CCF::eR | CCF::eG | CCF::eB | CCF::eA) + .setBlendEnable(alpha_blend ? vk::True : vk::False) + // standard alpha blending: + // (alpha * src) + (1 - alpha) * dst + .setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) + .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha) + .setColorBlendOp(vk::BlendOp::eAdd) + .setSrcAlphaBlendFactor(vk::BlendFactor::eOne) + .setDstAlphaBlendFactor(vk::BlendFactor::eZero) + .setAlphaBlendOp(vk::BlendOp::eAdd); + return ret; +} +} // namespace + +PipelineBuilder::PipelineBuilder(CreateInfo const& create_info) + : m_info(create_info) {} + +auto PipelineBuilder::build(vk::PipelineLayout const layout, + PipelineState const& state) const + -> vk::UniquePipeline { + auto const shader_stage_ci = + create_shader_stages(state.vertex_shader, state.fragment_shader); + + auto vertex_input_ci = vk::PipelineVertexInputStateCreateInfo{}; + vertex_input_ci.setVertexAttributeDescriptions(state.vertex_attributes) + .setVertexBindingDescriptions(state.vertex_bindings); + + auto multisample_state_ci = vk::PipelineMultisampleStateCreateInfo{}; + multisample_state_ci.setRasterizationSamples(m_info.samples) + .setSampleShadingEnable(vk::False); + + auto const input_assembly_ci = + vk::PipelineInputAssemblyStateCreateInfo{{}, state.topology}; + + auto rasterization_state_ci = vk::PipelineRasterizationStateCreateInfo{}; + rasterization_state_ci.setPolygonMode(state.polygon_mode) + .setCullMode(state.cull_mode); + + auto const depth_stencil_state_ci = + create_depth_stencil_state(state.flags, state.depth_compare); + + auto const color_blend_attachment = + create_color_blend_attachment(state.flags); + auto color_blend_state_ci = vk::PipelineColorBlendStateCreateInfo{}; + color_blend_state_ci.setAttachments(color_blend_attachment); + + auto dynamic_state_ci = vk::PipelineDynamicStateCreateInfo{}; + dynamic_state_ci.setDynamicStates(dynamic_states_v); + + // Dynamic Rendering requires passing this in the pNext chain. + auto rendering_ci = vk::PipelineRenderingCreateInfo{}; + // could be a depth-only pass, argument is span-like (notice the plural + // `Formats()`), only set if not Undefined. + if (m_info.color_format != vk::Format::eUndefined) { + rendering_ci.setColorAttachmentFormats(m_info.color_format); + } + // single depth attachment format, ok to set to Undefined. + rendering_ci.setDepthAttachmentFormat(m_info.depth_format); + + auto pipeline_ci = vk::GraphicsPipelineCreateInfo{}; + pipeline_ci.setLayout(layout) + .setStages(shader_stage_ci) + .setPVertexInputState(&vertex_input_ci) + .setPViewportState(&viewport_state_v) + .setPMultisampleState(&multisample_state_ci) + .setPInputAssemblyState(&input_assembly_ci) + .setPRasterizationState(&rasterization_state_ci) + .setPDepthStencilState(&depth_stencil_state_ci) + .setPColorBlendState(&color_blend_state_ci) + .setPDynamicState(&dynamic_state_ci) + .setPNext(&rendering_ci); + + auto ret = vk::Pipeline{}; + // use non-throwing API. + if (m_info.device.createGraphicsPipelines({}, 1, &pipeline_ci, {}, &ret) != + vk::Result::eSuccess) { + std::println(stderr, "[lvk] Failed to create Graphics Pipeline"); + return {}; + } + + return vk::UniquePipeline{ret, m_info.device}; +} +} // namespace lvk diff --git a/src/pipeline_builder.hpp b/src/pipeline_builder.hpp new file mode 100644 index 0000000..02e8afc --- /dev/null +++ b/src/pipeline_builder.hpp @@ -0,0 +1,25 @@ +#pragma once +#include + +namespace lvk { +struct PipelineBuilderCreateInfo { + vk::Device device{}; + vk::SampleCountFlagBits samples{}; + vk::Format color_format{}; + vk::Format depth_format{}; +}; + +class PipelineBuilder { + public: + using CreateInfo = PipelineBuilderCreateInfo; + + explicit PipelineBuilder(CreateInfo const& create_info); + + [[nodiscard]] auto build(vk::PipelineLayout layout, + PipelineState const& state) const + -> vk::UniquePipeline; + + private: + CreateInfo m_info{}; +}; +} // namespace lvk diff --git a/src/pipeline_state.hpp b/src/pipeline_state.hpp new file mode 100644 index 0000000..ed70b83 --- /dev/null +++ b/src/pipeline_state.hpp @@ -0,0 +1,36 @@ +#pragma once +#include +#include +#include + +namespace lvk { +// bit flags for various binary Pipeline States. +struct PipelineFlag { + enum : std::uint8_t { + None = 0, + AlphaBlend = 1 << 0, // turn on alpha blending. + DepthTest = 1 << 1, // turn on depth write and test. + }; +}; + +// specification of a unique Graphics Pipeline. +struct PipelineState { + using Flag = PipelineFlag; + + [[nodiscard]] static constexpr auto default_flags() -> std::uint8_t { + return Flag::AlphaBlend | Flag::DepthTest; + } + + vk::ShaderModule vertex_shader; // required. + vk::ShaderModule fragment_shader; // required. + + std::span vertex_attributes{}; + std::span vertex_bindings{}; + + vk::PrimitiveTopology topology{vk::PrimitiveTopology::eTriangleList}; + vk::PolygonMode polygon_mode{vk::PolygonMode::eFill}; + vk::CullModeFlags cull_mode{vk::CullModeFlagBits::eNone}; + vk::CompareOp depth_compare{vk::CompareOp::eLess}; + std::uint8_t flags{default_flags()}; +}; +} // namespace lvk diff --git a/src/shader_loader.cpp b/src/shader_loader.cpp new file mode 100644 index 0000000..39eb8c9 --- /dev/null +++ b/src/shader_loader.cpp @@ -0,0 +1,33 @@ +#include +#include +#include + +namespace lvk { +auto ShaderLoader::load(fs::path const& path) -> vk::UniqueShaderModule { + // open the file at the end, to get the total size. + auto file = std::ifstream{path, std::ios::binary | std::ios::ate}; + if (!file.is_open()) { + std::println(stderr, "Failed to open file: '{}'", + path.generic_string()); + return {}; + } + + auto const size = file.tellg(); + auto const usize = static_cast(size); + // file data must be uint32 aligned. + if (usize % sizeof(std::uint32_t) != 0) { + std::println(stderr, "Invalid SPIR-V size: {}", usize); + return {}; + } + + // seek to the beginning before reading. + file.seekg({}, std::ios::beg); + m_code.resize(usize / sizeof(std::uint32_t)); + void* data = m_code.data(); + file.read(static_cast(data), size); + + auto shader_module_ci = vk::ShaderModuleCreateInfo{}; + shader_module_ci.setCode(m_code); + return m_device.createShaderModuleUnique(shader_module_ci); +} +} // namespace lvk diff --git a/src/shader_loader.hpp b/src/shader_loader.hpp new file mode 100644 index 0000000..77f5b04 --- /dev/null +++ b/src/shader_loader.hpp @@ -0,0 +1,20 @@ +#pragma once +#include +#include +#include +#include + +namespace lvk { +namespace fs = std::filesystem; + +class ShaderLoader { + public: + explicit ShaderLoader(vk::Device const device) : m_device(device) {} + + [[nodiscard]] auto load(fs::path const& path) -> vk::UniqueShaderModule; + + private: + vk::Device m_device{}; + std::vector m_code{}; +}; +} // namespace lvk