From b5d562361b63a2db172e1b54b46f9bcd8b0ef163 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Wed, 26 Mar 2025 22:33:06 -0700 Subject: [PATCH 01/10] Shader Objects --- src/app.cpp | 53 +++++++++------- src/app.hpp | 7 ++- src/shader_program.cpp | 140 +++++++++++++++++++++++++++++++++++++++++ src/shader_program.hpp | 66 +++++++++++++++++++ 4 files changed, 240 insertions(+), 26 deletions(-) create mode 100644 src/shader_program.cpp create mode 100644 src/shader_program.hpp diff --git a/src/app.cpp b/src/app.cpp index 32e6d9e..69b74d8 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -40,6 +40,7 @@ void App::run(std::string_view const assets_dir) { create_swapchain(); create_render_sync(); create_imgui(); + create_shader(); create_pipeline_builder(); create_pipelines(); @@ -106,11 +107,16 @@ void App::create_device() { // and later device_ci.pNext => sync_feature. // this is 'pNext chaining'. sync_feature.setPNext(&dynamic_rendering_feature); + auto shader_object_feature = + vk::PhysicalDeviceShaderObjectFeaturesEXT{vk::True}; + dynamic_rendering_feature.setPNext(&shader_object_feature); auto device_ci = vk::DeviceCreateInfo{}; // we only need one device extension: Swapchain. - static constexpr auto extensions_v = - std::array{VK_KHR_SWAPCHAIN_EXTENSION_NAME}; + static constexpr auto extensions_v = std::array{ + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + "VK_EXT_shader_object", + }; device_ci.setPEnabledExtensionNames(extensions_v) .setQueueCreateInfos(queue_ci) .setPEnabledFeatures(&enabled_features) @@ -178,6 +184,19 @@ void App::create_imgui() { m_imgui.emplace(imgui_ci); } +void App::create_shader() { + auto const vertex_spirv = + to_spir_v(asset_path("shader.vert").string().c_str()); + auto const fragment_spirv = + to_spir_v(asset_path("shader.frag").string().c_str()); + auto const shader_ci = ShaderProgram::CreateInfo{ + .device = *m_device, + .vertex_spirv = vertex_spirv, + .fragment_spirv = fragment_spirv, + }; + m_shader.emplace(shader_ci); +} + void App::create_pipeline_builder() { auto const pipeline_builder_ci = PipelineBuilder::CreateInfo{ .device = *m_device, @@ -307,7 +326,7 @@ void App::render(vk::CommandBuffer const command_buffer) { command_buffer.beginRendering(rendering_info); inspect(); - draw(render_area, command_buffer); + draw(command_buffer); command_buffer.endRendering(); m_imgui->end_frame(); @@ -375,37 +394,23 @@ void App::inspect() { ImGui::SetNextWindowSize({200.0f, 100.0f}, ImGuiCond_Once); if (ImGui::Begin("Inspect")) { - ImGui::Checkbox("wireframe", &m_wireframe); + if (ImGui::Checkbox("wireframe", &m_wireframe)) { + m_shader->polygon_mode = + m_wireframe ? vk::PolygonMode::eLine : vk::PolygonMode::eFill; + } 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, + ImGui::DragFloat("line width", &m_shader->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); +void App::draw(vk::CommandBuffer const command_buffer) const { + m_shader->bind(command_buffer, m_framebuffer_size); // current shader has hard-coded logic for 3 vertices. command_buffer.draw(3, 1, 0, 0); } diff --git a/src/app.hpp b/src/app.hpp index 4982b2c..efd0d80 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ class App { void create_swapchain(); void create_render_sync(); void create_imgui(); + void create_shader(); void create_pipeline_builder(); void create_pipelines(); @@ -52,8 +54,7 @@ class App { // ImGui code goes here. void inspect(); // Issue draw calls here. - void draw(vk::Rect2D const& render_area, - vk::CommandBuffer command_buffer) const; + void draw(vk::CommandBuffer command_buffer) const; fs::path m_assets_dir{}; @@ -84,6 +85,8 @@ class App { float m_line_width{1.0f}; bool m_wireframe{}; + std::optional m_shader{}; + glm::ivec2 m_framebuffer_size{}; std::optional m_render_target{}; diff --git a/src/shader_program.cpp b/src/shader_program.cpp new file mode 100644 index 0000000..b240e05 --- /dev/null +++ b/src/shader_program.cpp @@ -0,0 +1,140 @@ +#include +#include +#include +#include + +namespace lvk { +namespace { +constexpr auto to_vkbool(bool const value) { + return value ? vk::True : vk::False; +} +} // namespace + +ShaderProgram::ShaderProgram(CreateInfo const& create_info) + : m_device(create_info.device) { + static auto const create_shader_ci = + [](std::span spirv) { + auto ret = vk::ShaderCreateInfoEXT{}; + ret.setCodeSize(spirv.size_bytes()) + .setPCode(spirv.data()) + // set common parameters. + .setCodeType(vk::ShaderCodeTypeEXT::eSpirv) + .setPName("main"); + return ret; + }; + + auto shader_cis = std::array{ + create_shader_ci(create_info.vertex_spirv), + create_shader_ci(create_info.fragment_spirv), + }; + shader_cis[0] + .setStage(vk::ShaderStageFlagBits::eVertex) + .setNextStage(vk::ShaderStageFlagBits::eFragment); + shader_cis[1].setStage(vk::ShaderStageFlagBits::eFragment); + + auto result = m_device.createShadersEXTUnique(shader_cis); + if (result.result != vk::Result::eSuccess) { + throw std::runtime_error{"Failed to create Shader Objects"}; + } + m_shaders = std::move(result.value); +} + +void ShaderProgram::bind(vk::CommandBuffer const command_buffer, + glm::ivec2 const framebuffer_size) const { + set_viewport_scissor(command_buffer, framebuffer_size); + set_static_states(command_buffer); + set_common_states(command_buffer); + set_vertex_states(command_buffer); + set_fragment_states(command_buffer); + bind_shaders(command_buffer); +} + +void ShaderProgram::set_viewport_scissor(vk::CommandBuffer const command_buffer, + glm::ivec2 const framebuffer_size) { + auto const fsize = glm::vec2{framebuffer_size}; + 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(fsize.y).setWidth(fsize.x).setHeight(-fsize.y); + command_buffer.setViewportWithCount(viewport); + + auto const usize = glm::uvec2{framebuffer_size}; + auto const scissor = + vk::Rect2D{vk::Offset2D{}, vk::Extent2D{usize.x, usize.y}}; + command_buffer.setScissorWithCount(scissor); +} + +void ShaderProgram::set_static_states(vk::CommandBuffer const command_buffer) { + command_buffer.setRasterizerDiscardEnable(vk::False); + command_buffer.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1); + command_buffer.setSampleMaskEXT(vk::SampleCountFlagBits::e1, 0xff); + command_buffer.setAlphaToCoverageEnableEXT(vk::False); + command_buffer.setCullMode(vk::CullModeFlagBits::eNone); + command_buffer.setFrontFace(vk::FrontFace::eCounterClockwise); + command_buffer.setDepthBiasEnable(vk::False); + command_buffer.setStencilTestEnable(vk::False); + command_buffer.setPrimitiveRestartEnable(vk::False); + command_buffer.setColorWriteMaskEXT(0, ~vk::ColorComponentFlags{}); +} + +void ShaderProgram::set_common_states( + vk::CommandBuffer const command_buffer) const { + auto const depth_test = to_vkbool((flags & DepthTest) == DepthTest); + command_buffer.setDepthWriteEnable(depth_test); + command_buffer.setDepthTestEnable(depth_test); + command_buffer.setDepthCompareOp(depth_compare_op); + command_buffer.setPolygonModeEXT(polygon_mode); + command_buffer.setLineWidth(line_width); +} + +void ShaderProgram::set_vertex_states( + vk::CommandBuffer const command_buffer) const { + command_buffer.setVertexInputEXT(m_vertex_input.bindings, + m_vertex_input.attributes); + command_buffer.setPrimitiveTopology(topology); +} + +void ShaderProgram::set_fragment_states( + vk::CommandBuffer const command_buffer) const { + auto const alpha_blend = to_vkbool((flags & AlphaBlend) == AlphaBlend); + command_buffer.setColorBlendEnableEXT(0, alpha_blend); + command_buffer.setColorBlendEquationEXT(0, color_blend_equation); +} + +void ShaderProgram::bind_shaders(vk::CommandBuffer const command_buffer) const { + static constexpr auto stages_v = std::array{ + vk::ShaderStageFlagBits::eVertex, + vk::ShaderStageFlagBits::eFragment, + }; + auto const shaders = std::array{ + *m_shaders[0], + *m_shaders[1], + }; + command_buffer.bindShadersEXT(stages_v, shaders); +} +} // namespace lvk + +auto lvk::to_spir_v(char const* path) -> std::vector { + // 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); + 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); + auto ret = std::vector{}; + ret.resize(usize / sizeof(std::uint32_t)); + void* data = ret.data(); + file.read(static_cast(data), size); + return ret; +} diff --git a/src/shader_program.hpp b/src/shader_program.hpp new file mode 100644 index 0000000..25d8df2 --- /dev/null +++ b/src/shader_program.hpp @@ -0,0 +1,66 @@ +#pragma once +#include +#include + +namespace lvk { +struct ShaderVertexInput { + std::span attributes{}; + std::span bindings{}; +}; + +struct ShaderProgramCreateInfo { + vk::Device device; + std::span vertex_spirv; + std::span fragment_spirv; + ShaderVertexInput vertex_input{}; +}; + +class ShaderProgram { + public: + // bit flags for various binary states. + enum : std::uint8_t { + None = 0, + AlphaBlend = 1 << 0, // turn on alpha blending. + DepthTest = 1 << 1, // turn on depth write and test. + }; + + static constexpr auto color_blend_equation_v = [] { + auto ret = vk::ColorBlendEquationEXT{}; + ret.setColorBlendOp(vk::BlendOp::eAdd) + .setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) + .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha); + return ret; + }(); + + static constexpr auto flags_v = AlphaBlend | DepthTest; + + using CreateInfo = ShaderProgramCreateInfo; + + explicit ShaderProgram(CreateInfo const& create_info); + + void bind(vk::CommandBuffer command_buffer, + glm::ivec2 framebuffer_size) const; + + vk::PrimitiveTopology topology{vk::PrimitiveTopology::eTriangleList}; + vk::PolygonMode polygon_mode{vk::PolygonMode::eFill}; + float line_width{1.0f}; + vk::ColorBlendEquationEXT color_blend_equation{color_blend_equation_v}; + vk::CompareOp depth_compare_op{vk::CompareOp::eLessOrEqual}; + std::uint8_t flags{flags_v}; + + private: + static void set_viewport_scissor(vk::CommandBuffer command_buffer, + glm::ivec2 framebuffer_size); + static void set_static_states(vk::CommandBuffer command_buffer); + void set_common_states(vk::CommandBuffer command_buffer) const; + void set_vertex_states(vk::CommandBuffer command_buffer) const; + void set_fragment_states(vk::CommandBuffer command_buffer) const; + void bind_shaders(vk::CommandBuffer command_buffer) const; + + vk::Device m_device{}; + ShaderVertexInput m_vertex_input{}; + std::vector m_shaders{}; +}; + +[[nodiscard]] auto to_spir_v(char const* path) -> std::vector; +} // namespace lvk From 58fe82322cd27dfe9ff4d68e41af117f1bca2a4f Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Wed, 26 Mar 2025 23:27:20 -0700 Subject: [PATCH 02/10] Remove pipeline code and section --- guide/src/SUMMARY.md | 11 +- guide/src/pipeline/README.md | 14 - guide/src/pipeline/drawing_triangle.md | 138 --------- guide/src/pipeline/pipeline_creation.md | 202 ------------- guide/src/pipeline/shaders.md | 121 -------- guide/src/pipeline/switching_pipelines.md | 39 --- guide/src/shader_objects/README.md | 5 + guide/src/shader_objects/drawing_triangle.md | 121 ++++++++ guide/src/shader_objects/glsl_to_spir_v.md | 71 +++++ .../locating_assets.md | 0 guide/src/shader_objects/shader_program.md | 267 ++++++++++++++++++ .../srgb_triangle.png | Bin .../srgb_triangle_wireframe.png | Bin .../white_triangle.png | Bin src/app.cpp | 72 ++--- src/app.hpp | 13 +- src/pipeline_builder.cpp | 131 --------- src/pipeline_builder.hpp | 25 -- src/pipeline_state.hpp | 36 --- src/shader_loader.cpp | 33 --- src/shader_loader.hpp | 20 -- src/shader_program.cpp | 32 +-- src/shader_program.hpp | 11 +- 23 files changed, 508 insertions(+), 854 deletions(-) delete mode 100644 guide/src/pipeline/README.md delete mode 100644 guide/src/pipeline/drawing_triangle.md delete mode 100644 guide/src/pipeline/pipeline_creation.md delete mode 100644 guide/src/pipeline/shaders.md delete mode 100644 guide/src/pipeline/switching_pipelines.md create mode 100644 guide/src/shader_objects/README.md create mode 100644 guide/src/shader_objects/drawing_triangle.md create mode 100644 guide/src/shader_objects/glsl_to_spir_v.md rename guide/src/{pipeline => shader_objects}/locating_assets.md (100%) create mode 100644 guide/src/shader_objects/shader_program.md rename guide/src/{pipeline => shader_objects}/srgb_triangle.png (100%) rename guide/src/{pipeline => shader_objects}/srgb_triangle_wireframe.png (100%) rename guide/src/{pipeline => shader_objects}/white_triangle.png (100%) delete mode 100644 src/pipeline_builder.cpp delete mode 100644 src/pipeline_builder.hpp delete mode 100644 src/pipeline_state.hpp delete mode 100644 src/shader_loader.cpp delete mode 100644 src/shader_loader.hpp diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index d86d249..53ad568 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -24,9 +24,8 @@ - [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) +- [Shader Objects](shader_objects/README.md) + - [Locating Assets](shader_objects/locating_assets.md) + - [Shader Program](shader_objects/shader_program.md) + - [GLSL to SPIR-V](shader_objects/glsl_to_spir_v.md) + - [Drawing a Triangle](shader_objects/drawing_triangle.md) diff --git a/guide/src/pipeline/README.md b/guide/src/pipeline/README.md deleted file mode 100644 index 75f8821..0000000 --- a/guide/src/pipeline/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# 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 deleted file mode 100644 index e4c22ad..0000000 --- a/guide/src/pipeline/drawing_triangle.md +++ /dev/null @@ -1,138 +0,0 @@ -# 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/pipeline_creation.md b/guide/src/pipeline/pipeline_creation.md deleted file mode 100644 index d1c44b2..0000000 --- a/guide/src/pipeline/pipeline_creation.md +++ /dev/null @@ -1,202 +0,0 @@ -# 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 deleted file mode 100644 index dfc0571..0000000 --- a/guide/src/pipeline/shaders.md +++ /dev/null @@ -1,121 +0,0 @@ -# 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/switching_pipelines.md b/guide/src/pipeline/switching_pipelines.md deleted file mode 100644 index 93be2e0..0000000 --- a/guide/src/pipeline/switching_pipelines.md +++ /dev/null @@ -1,39 +0,0 @@ -# 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/shader_objects/README.md b/guide/src/shader_objects/README.md new file mode 100644 index 0000000..88e169b --- /dev/null +++ b/guide/src/shader_objects/README.md @@ -0,0 +1,5 @@ +# Shader Objects + +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. There is however an extension called [`VK_EXT_shader_object`](https://www.khronos.org/blog/you-can-use-vulkan-without-pipelines-today) which enables avoiding graphics pipelines entirely. Almost all pipeline state becomes dynamic, ie set at draw time, and the only Vulkan handles to own are `ShaderEXT` objects. For a comprehensive guide, check out the [Vulkan Sample from Khronos](https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/extensions/shader_object). + +Vulkan requires shader code to be provided as SPIR-V (IR). We shall use `glslc` (part of the Vulkan SDK) to compile GLSL to SPIR-V manually when required. diff --git a/guide/src/shader_objects/drawing_triangle.md b/guide/src/shader_objects/drawing_triangle.md new file mode 100644 index 0000000..bd8f737 --- /dev/null +++ b/guide/src/shader_objects/drawing_triangle.md @@ -0,0 +1,121 @@ +# Drawing a Triangle + +Add a `ShaderProgram` to `App` and its create function: + +```cpp +void create_shader(); + +// ... +std::optional m_shader{}; +``` + +Implement and call `create_shader()`: + +```cpp +void App::create_shader() { + auto const vertex_spirv = to_spir_v(asset_path("shader.vert")); + auto const fragment_spirv = to_spir_v(asset_path("shader.frag")); + auto const shader_ci = ShaderProgram::CreateInfo{ + .device = *m_device, + .vertex_spirv = vertex_spirv, + .fragment_spirv = fragment_spirv, + }; + m_shader.emplace(shader_ci); +} +``` + +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::CommandBuffer command_buffer) const; + +// ... +void App::inspect() { + ImGui::ShowDemoWindow(); + // TODO +} + +// ... +command_buffer.beginRendering(rendering_info); +inspect(); +draw(command_buffer); +command_buffer.endRendering(); +``` + +We can now bind the shader 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::CommandBuffer const command_buffer) const { + m_shader->bind(command_buffer, m_framebuffer_size); + // 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) + +## Modifying Dynamic State + +We can use an ImGui window to inspect / tweak some pipeline state: + +```cpp +ImGui::SetNextWindowSize({200.0f, 100.0f}, ImGuiCond_Once); +if (ImGui::Begin("Inspect")) { + if (ImGui::Checkbox("wireframe", &m_wireframe)) { + m_shader->polygon_mode = + m_wireframe ? vk::PolygonMode::eLine : vk::PolygonMode::eFill; + } + if (m_wireframe) { + auto const& line_width_range = + m_gpu.properties.limits.lineWidthRange; + ImGui::SetNextItemWidth(100.0f); + ImGui::DragFloat("line width", &m_shader->line_width, 0.25f, + line_width_range[0], line_width_range[1]); + } +} +ImGui::End(); +``` + +![sRGB Triangle (wireframe)](./srgb_triangle_wireframe.png) + diff --git a/guide/src/shader_objects/glsl_to_spir_v.md b/guide/src/shader_objects/glsl_to_spir_v.md new file mode 100644 index 0000000..7ed5b2b --- /dev/null +++ b/guide/src/shader_objects/glsl_to_spir_v.md @@ -0,0 +1,71 @@ +# GLSL to SPIR-V + +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. + +## Loading SPIR-V + +SPIR-V shaders are binary files with a stride/alignment of 4 bytes. As we have seen, 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 helper function in `app.cpp`: + +```cpp +[[nodiscard]] auto to_spir_v(fs::path const& path) + -> std::vector { + // 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()) { + throw std::runtime_error{ + std::format("Failed to open file: '{}'", path.generic_string())}; + } + + auto const size = file.tellg(); + auto const usize = static_cast(size); + // file data must be uint32 aligned. + if (usize % sizeof(std::uint32_t) != 0) { + throw std::runtime_error{std::format("Invalid SPIR-V size: {}", usize)}; + } + + // seek to the beginning before reading. + file.seekg({}, std::ios::beg); + auto ret = std::vector{}; + ret.resize(usize / sizeof(std::uint32_t)); + void* data = ret.data(); + file.read(static_cast(data), size); + return ret; +} +``` diff --git a/guide/src/pipeline/locating_assets.md b/guide/src/shader_objects/locating_assets.md similarity index 100% rename from guide/src/pipeline/locating_assets.md rename to guide/src/shader_objects/locating_assets.md diff --git a/guide/src/shader_objects/shader_program.md b/guide/src/shader_objects/shader_program.md new file mode 100644 index 0000000..2e61575 --- /dev/null +++ b/guide/src/shader_objects/shader_program.md @@ -0,0 +1,267 @@ +# Shader Program + +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). We will encapsulate both vertex and fragment shaders into a single `ShaderProgram`, which will also bind the shaders before a draw, and expose/set various dynamic states. + +In `shader_program.hpp`, first add a `ShaderProgramCreateInfo` struct: + +```cpp +struct ShaderProgramCreateInfo { + vk::Device device; + std::span vertex_spirv; + std::span fragment_spirv; +}; +``` + +Start with a skeleton definition: + +```cpp +class ShaderProgram { + public: + using CreateInfo = ShaderProgramCreateInfo; + + explicit ShaderProgram(CreateInfo const& create_info); + + private: + std::vector m_shaders{}; + + ScopedWaiter m_waiter{}; +}; +``` + +The definition of the constructor is fairly straightforward: + +```cpp +ShaderProgram::ShaderProgram(CreateInfo const& create_info) { + static auto const create_shader_ci = + [](std::span spirv) { + auto ret = vk::ShaderCreateInfoEXT{}; + ret.setCodeSize(spirv.size_bytes()) + .setPCode(spirv.data()) + // set common parameters. + .setCodeType(vk::ShaderCodeTypeEXT::eSpirv) + .setPName("main"); + return ret; + }; + + auto shader_cis = std::array{ + create_shader_ci(create_info.vertex_spirv), + create_shader_ci(create_info.fragment_spirv), + }; + shader_cis[0] + .setStage(vk::ShaderStageFlagBits::eVertex) + .setNextStage(vk::ShaderStageFlagBits::eFragment); + shader_cis[1].setStage(vk::ShaderStageFlagBits::eFragment); + + auto result = create_info.device.createShadersEXTUnique(shader_cis); + if (result.result != vk::Result::eSuccess) { + throw std::runtime_error{"Failed to create Shader Objects"}; + } + m_shaders = std::move(result.value); + m_waiter = create_info.device; +} +``` + +Expose some dynamic states via public members: + +```cpp +static constexpr auto color_blend_equation_v = [] { + auto ret = vk::ColorBlendEquationEXT{}; + ret.setColorBlendOp(vk::BlendOp::eAdd) + // standard alpha blending: + // (alpha * src) + (1 - alpha) * dst + .setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) + .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha); + return ret; +}(); + +// ... +vk::PrimitiveTopology topology{vk::PrimitiveTopology::eTriangleList}; +vk::PolygonMode polygon_mode{vk::PolygonMode::eFill}; +float line_width{1.0f}; +vk::ColorBlendEquationEXT color_blend_equation{color_blend_equation_v}; +vk::CompareOp depth_compare_op{vk::CompareOp::eLessOrEqual}; +``` + +Encapsulate booleans into bit flags: + +```cpp +// bit flags for various binary states. +enum : std::uint8_t { + None = 0, + AlphaBlend = 1 << 0, // turn on alpha blending. + DepthTest = 1 << 1, // turn on depth write and test. +}; + +// ... +static constexpr auto flags_v = AlphaBlend | DepthTest; + +// ... +std::uint8_t flags{flags_v}; +``` + +There is one more piece of pipeline state needed: vertex input. We will consider this to be constant per shader and take it in the constructor: + +```cpp +// shader_program.hpp + +// vertex attributes and bindings. +struct ShaderVertexInput { + std::span attributes{}; + std::span bindings{}; +}; + +struct ShaderProgramCreateInfo { + // ... + ShaderVertexInput vertex_input{}; +}; + +class ShaderProgram { + // ... + ShaderVertexInput m_vertex_input{}; + std::vector m_shaders{}; + // ... +}; + +// shader_program.cpp +ShaderProgram::ShaderProgram(CreateInfo const& create_info) + : m_vertex_input(create_info.vertex_input) { + // ... +} +``` + +The API to bind will take the command buffer and the framebuffer size (to set the viewport and scissor): + +```cpp +void bind(vk::CommandBuffer command_buffer, + glm::ivec2 framebuffer_size) const; +``` + +Add helper member functions and implement `bind()` by calling them in succession: + +```cpp +static void set_viewport_scissor(vk::CommandBuffer command_buffer, + glm::ivec2 framebuffer); +static void set_static_states(vk::CommandBuffer command_buffer); +void set_common_states(vk::CommandBuffer command_buffer) const; +void set_vertex_states(vk::CommandBuffer command_buffer) const; +void set_fragment_states(vk::CommandBuffer command_buffer) const; +void bind_shaders(vk::CommandBuffer command_buffer) const; + +// ... +void ShaderProgram::bind(vk::CommandBuffer const command_buffer, + glm::ivec2 const framebuffer_size) const { + set_viewport_scissor(command_buffer, framebuffer_size); + set_static_states(command_buffer); + set_common_states(command_buffer); + set_vertex_states(command_buffer); + set_fragment_states(command_buffer); + bind_shaders(command_buffer); +} +``` + +Implementations are long but straightforward: + +```cpp +namespace { +constexpr auto to_vkbool(bool const value) { + return value ? vk::True : vk::False; +} +} // namespace + +// ... +void ShaderProgram::set_viewport_scissor(vk::CommandBuffer const command_buffer, + glm::ivec2 const framebuffer_size) { + auto const fsize = glm::vec2{framebuffer_size}; + 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(fsize.y).setWidth(fsize.x).setHeight(-fsize.y); + command_buffer.setViewportWithCount(viewport); + + auto const usize = glm::uvec2{framebuffer_size}; + auto const scissor = + vk::Rect2D{vk::Offset2D{}, vk::Extent2D{usize.x, usize.y}}; + command_buffer.setScissorWithCount(scissor); +} + +void ShaderProgram::set_static_states(vk::CommandBuffer const command_buffer) { + command_buffer.setRasterizerDiscardEnable(vk::False); + command_buffer.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1); + command_buffer.setSampleMaskEXT(vk::SampleCountFlagBits::e1, 0xff); + command_buffer.setAlphaToCoverageEnableEXT(vk::False); + command_buffer.setCullMode(vk::CullModeFlagBits::eNone); + command_buffer.setFrontFace(vk::FrontFace::eCounterClockwise); + command_buffer.setDepthBiasEnable(vk::False); + command_buffer.setStencilTestEnable(vk::False); + command_buffer.setPrimitiveRestartEnable(vk::False); + command_buffer.setColorWriteMaskEXT(0, ~vk::ColorComponentFlags{}); +} + +void ShaderProgram::set_common_states( + vk::CommandBuffer const command_buffer) const { + auto const depth_test = to_vkbool((flags & DepthTest) == DepthTest); + command_buffer.setDepthWriteEnable(depth_test); + command_buffer.setDepthTestEnable(depth_test); + command_buffer.setDepthCompareOp(depth_compare_op); + command_buffer.setPolygonModeEXT(polygon_mode); + command_buffer.setLineWidth(line_width); +} + +void ShaderProgram::set_vertex_states( + vk::CommandBuffer const command_buffer) const { + command_buffer.setVertexInputEXT(m_vertex_input.bindings, + m_vertex_input.attributes); + command_buffer.setPrimitiveTopology(topology); +} + +void ShaderProgram::set_fragment_states( + vk::CommandBuffer const command_buffer) const { + auto const alpha_blend = to_vkbool((flags & AlphaBlend) == AlphaBlend); + command_buffer.setColorBlendEnableEXT(0, alpha_blend); + command_buffer.setColorBlendEquationEXT(0, color_blend_equation); +} + +void ShaderProgram::bind_shaders(vk::CommandBuffer const command_buffer) const { + static constexpr auto stages_v = std::array{ + vk::ShaderStageFlagBits::eVertex, + vk::ShaderStageFlagBits::eFragment, + }; + auto const shaders = std::array{ + *m_shaders[0], + *m_shaders[1], + }; + command_buffer.bindShadersEXT(stages_v, shaders); +} +``` + +## TODO: MOVE + +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/shader_objects/srgb_triangle.png similarity index 100% rename from guide/src/pipeline/srgb_triangle.png rename to guide/src/shader_objects/srgb_triangle.png diff --git a/guide/src/pipeline/srgb_triangle_wireframe.png b/guide/src/shader_objects/srgb_triangle_wireframe.png similarity index 100% rename from guide/src/pipeline/srgb_triangle_wireframe.png rename to guide/src/shader_objects/srgb_triangle_wireframe.png diff --git a/guide/src/pipeline/white_triangle.png b/guide/src/shader_objects/white_triangle.png similarity index 100% rename from guide/src/pipeline/white_triangle.png rename to guide/src/shader_objects/white_triangle.png diff --git a/src/app.cpp b/src/app.cpp index 69b74d8..a49b340 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1,7 +1,7 @@ #include -#include #include #include +#include #include #include @@ -27,6 +27,31 @@ namespace { std::println("[lvk] Warning: could not locate 'assets' directory"); return fs::current_path(); } + +[[nodiscard]] auto to_spir_v(fs::path const& path) + -> std::vector { + // 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()) { + throw std::runtime_error{ + std::format("Failed to open file: '{}'", path.generic_string())}; + } + + auto const size = file.tellg(); + auto const usize = static_cast(size); + // file data must be uint32 aligned. + if (usize % sizeof(std::uint32_t) != 0) { + throw std::runtime_error{std::format("Invalid SPIR-V size: {}", usize)}; + } + + // seek to the beginning before reading. + file.seekg({}, std::ios::beg); + auto ret = std::vector{}; + ret.resize(usize / sizeof(std::uint32_t)); + void* data = ret.data(); + file.read(static_cast(data), size); + return ret; +} } // namespace void App::run(std::string_view const assets_dir) { @@ -41,9 +66,6 @@ void App::run(std::string_view const assets_dir) { create_render_sync(); create_imgui(); create_shader(); - create_pipeline_builder(); - - create_pipelines(); main_loop(); } @@ -185,10 +207,8 @@ void App::create_imgui() { } void App::create_shader() { - auto const vertex_spirv = - to_spir_v(asset_path("shader.vert").string().c_str()); - auto const fragment_spirv = - to_spir_v(asset_path("shader.frag").string().c_str()); + auto const vertex_spirv = to_spir_v(asset_path("shader.vert")); + auto const fragment_spirv = to_spir_v(asset_path("shader.frag")); auto const shader_ci = ShaderProgram::CreateInfo{ .device = *m_device, .vertex_spirv = vertex_spirv, @@ -197,42 +217,6 @@ void App::create_shader() { m_shader.emplace(shader_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; } diff --git a/src/app.hpp b/src/app.hpp index efd0d80..95e50bf 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -1,7 +1,6 @@ #pragma once #include #include -#include #include #include #include @@ -37,8 +36,6 @@ class App { void create_render_sync(); void create_imgui(); void create_shader(); - void create_pipeline_builder(); - void create_pipelines(); [[nodiscard]] auto asset_path(std::string_view uri) const -> fs::path; @@ -75,20 +72,12 @@ 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{}; std::optional m_shader{}; glm::ivec2 m_framebuffer_size{}; std::optional m_render_target{}; + bool m_wireframe{}; // waiter must be the last member to ensure it blocks until device is idle // before other members get destroyed. diff --git a/src/pipeline_builder.cpp b/src/pipeline_builder.cpp deleted file mode 100644 index 0e76862..0000000 --- a/src/pipeline_builder.cpp +++ /dev/null @@ -1,131 +0,0 @@ -#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 deleted file mode 100644 index 02e8afc..0000000 --- a/src/pipeline_builder.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#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 deleted file mode 100644 index ed70b83..0000000 --- a/src/pipeline_state.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#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 deleted file mode 100644 index 39eb8c9..0000000 --- a/src/shader_loader.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#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 deleted file mode 100644 index 77f5b04..0000000 --- a/src/shader_loader.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#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 diff --git a/src/shader_program.cpp b/src/shader_program.cpp index b240e05..bc762b9 100644 --- a/src/shader_program.cpp +++ b/src/shader_program.cpp @@ -1,6 +1,4 @@ #include -#include -#include #include namespace lvk { @@ -11,7 +9,7 @@ constexpr auto to_vkbool(bool const value) { } // namespace ShaderProgram::ShaderProgram(CreateInfo const& create_info) - : m_device(create_info.device) { + : m_vertex_input(create_info.vertex_input) { static auto const create_shader_ci = [](std::span spirv) { auto ret = vk::ShaderCreateInfoEXT{}; @@ -32,11 +30,12 @@ ShaderProgram::ShaderProgram(CreateInfo const& create_info) .setNextStage(vk::ShaderStageFlagBits::eFragment); shader_cis[1].setStage(vk::ShaderStageFlagBits::eFragment); - auto result = m_device.createShadersEXTUnique(shader_cis); + auto result = create_info.device.createShadersEXTUnique(shader_cis); if (result.result != vk::Result::eSuccess) { throw std::runtime_error{"Failed to create Shader Objects"}; } m_shaders = std::move(result.value); + m_waiter = create_info.device; } void ShaderProgram::bind(vk::CommandBuffer const command_buffer, @@ -113,28 +112,3 @@ void ShaderProgram::bind_shaders(vk::CommandBuffer const command_buffer) const { command_buffer.bindShadersEXT(stages_v, shaders); } } // namespace lvk - -auto lvk::to_spir_v(char const* path) -> std::vector { - // 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); - 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); - auto ret = std::vector{}; - ret.resize(usize / sizeof(std::uint32_t)); - void* data = ret.data(); - file.read(static_cast(data), size); - return ret; -} diff --git a/src/shader_program.hpp b/src/shader_program.hpp index 25d8df2..f16a72f 100644 --- a/src/shader_program.hpp +++ b/src/shader_program.hpp @@ -1,8 +1,10 @@ #pragma once +#include #include #include namespace lvk { +// vertex attributes and bindings. struct ShaderVertexInput { std::span attributes{}; std::span bindings{}; @@ -27,6 +29,8 @@ class ShaderProgram { static constexpr auto color_blend_equation_v = [] { auto ret = vk::ColorBlendEquationEXT{}; ret.setColorBlendOp(vk::BlendOp::eAdd) + // standard alpha blending: + // (alpha * src) + (1 - alpha) * dst .setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha); return ret; @@ -50,17 +54,16 @@ class ShaderProgram { private: static void set_viewport_scissor(vk::CommandBuffer command_buffer, - glm::ivec2 framebuffer_size); + glm::ivec2 framebuffer); static void set_static_states(vk::CommandBuffer command_buffer); void set_common_states(vk::CommandBuffer command_buffer) const; void set_vertex_states(vk::CommandBuffer command_buffer) const; void set_fragment_states(vk::CommandBuffer command_buffer) const; void bind_shaders(vk::CommandBuffer command_buffer) const; - vk::Device m_device{}; ShaderVertexInput m_vertex_input{}; std::vector m_shaders{}; -}; -[[nodiscard]] auto to_spir_v(char const* path) -> std::vector; + ScopedWaiter m_waiter{}; +}; } // namespace lvk From 159f4e2c8d8784515f7d2675f827277a26d6c4d0 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Wed, 26 Mar 2025 23:44:23 -0700 Subject: [PATCH 03/10] Fixup docs --- guide/src/shader_objects/drawing_triangle.md | 9 +++- guide/src/shader_objects/shader_program.md | 49 +++++++------------- src/app.cpp | 2 +- 3 files changed, 25 insertions(+), 35 deletions(-) diff --git a/guide/src/shader_objects/drawing_triangle.md b/guide/src/shader_objects/drawing_triangle.md index bd8f737..a1cb089 100644 --- a/guide/src/shader_objects/drawing_triangle.md +++ b/guide/src/shader_objects/drawing_triangle.md @@ -3,13 +3,16 @@ Add a `ShaderProgram` to `App` and its create function: ```cpp +[[nodiscard]] auto asset_path(std::string_view uri) const -> fs::path; + +// ... void create_shader(); // ... std::optional m_shader{}; ``` -Implement and call `create_shader()`: +Implement and call `create_shader()` (and `asset_path()`): ```cpp void App::create_shader() { @@ -22,6 +25,10 @@ void App::create_shader() { }; m_shader.emplace(shader_ci); } + +auto App::asset_path(std::string_view const uri) const -> fs::path { + return m_assets_dir / uri; +} ``` Before `render()` grows to an unwieldy size, extract the higher level logic into two member functions: diff --git a/guide/src/shader_objects/shader_program.md b/guide/src/shader_objects/shader_program.md index 2e61575..dc42703 100644 --- a/guide/src/shader_objects/shader_program.md +++ b/guide/src/shader_objects/shader_program.md @@ -1,6 +1,21 @@ # Shader Program -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). We will encapsulate both vertex and fragment shaders into a single `ShaderProgram`, which will also bind the shaders before a draw, and expose/set various dynamic states. +To use Shader Objects we need to enable the corresponding feature and extension during device creation: + +```cpp +auto shader_object_feature = + vk::PhysicalDeviceShaderObjectFeaturesEXT{vk::True}; +dynamic_rendering_feature.setPNext(&shader_object_feature); + +// ... +// we need two device extensions: Swapchain and Shader Object. +static constexpr auto extensions_v = std::array{ + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + "VK_EXT_shader_object", +}; +``` + +We will encapsulate both vertex and fragment shaders into a single `ShaderProgram`, which will also bind the shaders before a draw, and expose/set various dynamic states. In `shader_program.hpp`, first add a `ShaderProgramCreateInfo` struct: @@ -233,35 +248,3 @@ void ShaderProgram::bind_shaders(vk::CommandBuffer const command_buffer) const { command_buffer.bindShadersEXT(stages_v, shaders); } ``` - -## TODO: MOVE - -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/src/app.cpp b/src/app.cpp index a49b340..822e664 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -134,7 +134,7 @@ void App::create_device() { dynamic_rendering_feature.setPNext(&shader_object_feature); auto device_ci = vk::DeviceCreateInfo{}; - // we only need one device extension: Swapchain. + // we need two device extensions: Swapchain and Shader Object. static constexpr auto extensions_v = std::array{ VK_KHR_SWAPCHAIN_EXTENSION_NAME, "VK_EXT_shader_object", From 50bb8eca54286c118bdc9764556c163ac9b08994 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Thu, 27 Mar 2025 19:04:53 -0700 Subject: [PATCH 04/10] Add `VK_LAYER_KHRONOS_shader_object` --- guide/src/shader_objects/shader_program.md | 20 ++++++++++++++++++++ src/app.cpp | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/guide/src/shader_objects/shader_program.md b/guide/src/shader_objects/shader_program.md index dc42703..18fedba 100644 --- a/guide/src/shader_objects/shader_program.md +++ b/guide/src/shader_objects/shader_program.md @@ -15,6 +15,26 @@ static constexpr auto extensions_v = std::array{ }; ``` +## Emulation Layer + +It's possible device creation now fails because the driver or physical device does not support `VK_EXT_shader_object` (especially likely with Intel). Vulkan SDK provides a layer that implements this extension: [`VK_LAYER_KHRONOS_shader_object`](https://github.com/KhronosGroup/Vulkan-ExtensionLayer/blob/main/docs/shader_object_layer.md). Adding this layer to the Instance Create Info should unblock usage of this feature: + +```cpp +// ... +// add the Shader Object emulation layer. +static constexpr auto layers_v = std::array{ + "VK_LAYER_KHRONOS_shader_object", +}; +instance_ci.setPEnabledLayerNames(layers_v); + +m_instance = vk::createInstanceUnique(instance_ci); +// ... +``` + +> This layer _is not_ part of standard Vulkan driver installs, you must package the layer with the application for it to run on environments without Vulkan SDK / Vulkan Configurator. Read more [here](https://docs.vulkan.org/samples/latest/samples/extensions/shader_object/README.html#_emulation_layer). + +## `class ShaderObject` + We will encapsulate both vertex and fragment shaders into a single `ShaderProgram`, which will also bind the shaders before a draw, and expose/set various dynamic states. In `shader_program.hpp`, first add a `ShaderProgramCreateInfo` struct: diff --git a/src/app.cpp b/src/app.cpp index 822e664..9529331 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -91,6 +91,12 @@ void App::create_instance() { instance_ci.setPApplicationInfo(&app_info).setPEnabledExtensionNames( extensions); + // add the Shader Object emulation layer. + static constexpr auto layers_v = std::array{ + "VK_LAYER_KHRONOS_shader_object", + }; + instance_ci.setPEnabledLayerNames(layers_v); + m_instance = vk::createInstanceUnique(instance_ci); // initialize the dispatcher against the created Instance. VULKAN_HPP_DEFAULT_DISPATCHER.init(*m_instance); From 1244d6ab00f8efe1f7405cd7e0af327f74e76fc2 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Thu, 27 Mar 2025 19:12:21 -0700 Subject: [PATCH 05/10] Use warning --- guide/src/shader_objects/drawing_triangle.md | 2 +- guide/src/shader_objects/shader_program.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/guide/src/shader_objects/drawing_triangle.md b/guide/src/shader_objects/drawing_triangle.md index a1cb089..3282244 100644 --- a/guide/src/shader_objects/drawing_triangle.md +++ b/guide/src/shader_objects/drawing_triangle.md @@ -64,7 +64,7 @@ void App::draw(vk::CommandBuffer const command_buffer) const { ![White Triangle](./white_triangle.png) -Updating our shaders to use interpolated RGB on each vertex: +Updating the shaders to use interpolated RGB on each vertex: ```glsl // shader.vert diff --git a/guide/src/shader_objects/shader_program.md b/guide/src/shader_objects/shader_program.md index 18fedba..8fea1ca 100644 --- a/guide/src/shader_objects/shader_program.md +++ b/guide/src/shader_objects/shader_program.md @@ -31,7 +31,9 @@ m_instance = vk::createInstanceUnique(instance_ci); // ... ``` -> This layer _is not_ part of standard Vulkan driver installs, you must package the layer with the application for it to run on environments without Vulkan SDK / Vulkan Configurator. Read more [here](https://docs.vulkan.org/samples/latest/samples/extensions/shader_object/README.html#_emulation_layer). +
+This layer is not part of standard Vulkan driver installs, you must package the layer with the application for it to run on environments without Vulkan SDK / Vulkan Configurator. Read more here. +
## `class ShaderObject` From e7a0adb360d9cfdc8d3644da18b1b6b000b97306 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Thu, 27 Mar 2025 19:45:44 -0700 Subject: [PATCH 06/10] Add set layouts to shader ci --- guide/src/shader_objects/drawing_triangle.md | 2 ++ guide/src/shader_objects/shader_program.md | 6 ++++-- src/app.cpp | 2 ++ src/shader_program.cpp | 5 +++-- src/shader_program.hpp | 3 ++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/guide/src/shader_objects/drawing_triangle.md b/guide/src/shader_objects/drawing_triangle.md index 3282244..ecb757f 100644 --- a/guide/src/shader_objects/drawing_triangle.md +++ b/guide/src/shader_objects/drawing_triangle.md @@ -22,6 +22,8 @@ void App::create_shader() { .device = *m_device, .vertex_spirv = vertex_spirv, .fragment_spirv = fragment_spirv, + .vertex_input = {}, + .set_layouts = {}, }; m_shader.emplace(shader_ci); } diff --git a/guide/src/shader_objects/shader_program.md b/guide/src/shader_objects/shader_program.md index 8fea1ca..40abb0a 100644 --- a/guide/src/shader_objects/shader_program.md +++ b/guide/src/shader_objects/shader_program.md @@ -46,6 +46,7 @@ struct ShaderProgramCreateInfo { vk::Device device; std::span vertex_spirv; std::span fragment_spirv; + std::span set_layouts; }; ``` @@ -69,12 +70,13 @@ The definition of the constructor is fairly straightforward: ```cpp ShaderProgram::ShaderProgram(CreateInfo const& create_info) { - static auto const create_shader_ci = - [](std::span spirv) { + auto const create_shader_ci = + [&create_info](std::span spirv) { auto ret = vk::ShaderCreateInfoEXT{}; ret.setCodeSize(spirv.size_bytes()) .setPCode(spirv.data()) // set common parameters. + .setSetLayouts(create_info.set_layouts) .setCodeType(vk::ShaderCodeTypeEXT::eSpirv) .setPName("main"); return ret; diff --git a/src/app.cpp b/src/app.cpp index 9529331..61a14dc 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -219,6 +219,8 @@ void App::create_shader() { .device = *m_device, .vertex_spirv = vertex_spirv, .fragment_spirv = fragment_spirv, + .vertex_input = {}, + .set_layouts = {}, }; m_shader.emplace(shader_ci); } diff --git a/src/shader_program.cpp b/src/shader_program.cpp index bc762b9..a2a8b5a 100644 --- a/src/shader_program.cpp +++ b/src/shader_program.cpp @@ -10,12 +10,13 @@ constexpr auto to_vkbool(bool const value) { ShaderProgram::ShaderProgram(CreateInfo const& create_info) : m_vertex_input(create_info.vertex_input) { - static auto const create_shader_ci = - [](std::span spirv) { + auto const create_shader_ci = + [&create_info](std::span spirv) { auto ret = vk::ShaderCreateInfoEXT{}; ret.setCodeSize(spirv.size_bytes()) .setPCode(spirv.data()) // set common parameters. + .setSetLayouts(create_info.set_layouts) .setCodeType(vk::ShaderCodeTypeEXT::eSpirv) .setPName("main"); return ret; diff --git a/src/shader_program.hpp b/src/shader_program.hpp index f16a72f..204e25f 100644 --- a/src/shader_program.hpp +++ b/src/shader_program.hpp @@ -14,7 +14,8 @@ struct ShaderProgramCreateInfo { vk::Device device; std::span vertex_spirv; std::span fragment_spirv; - ShaderVertexInput vertex_input{}; + ShaderVertexInput vertex_input; + std::span set_layouts; }; class ShaderProgram { From 49c56195030b59f7913b03d02bcfd3b044c37da7 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Thu, 27 Mar 2025 19:51:02 -0700 Subject: [PATCH 07/10] Remove incorrect assets arg --- guide/src/shader_objects/locating_assets.md | 34 --------------------- src/app.cpp | 10 ++---- src/app.hpp | 2 +- src/main.cpp | 4 +-- 4 files changed, 5 insertions(+), 45 deletions(-) diff --git a/guide/src/shader_objects/locating_assets.md b/guide/src/shader_objects/locating_assets.md index 1dfaaeb..bbe0361 100644 --- a/guide/src/shader_objects/locating_assets.md +++ b/guide/src/shader_objects/locating_assets.md @@ -47,37 +47,3 @@ Add a helper function to locate the assets dir, and assign `m_assets_dir` to its // ... 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/src/app.cpp b/src/app.cpp index 61a14dc..b137951 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -11,11 +11,7 @@ namespace lvk { using namespace std::chrono_literals; 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; - } +[[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"}; @@ -54,8 +50,8 @@ namespace { } } // namespace -void App::run(std::string_view const assets_dir) { - m_assets_dir = locate_assets_dir(assets_dir); +void App::run() { + m_assets_dir = locate_assets_dir(); create_window(); create_instance(); diff --git a/src/app.hpp b/src/app.hpp index 95e50bf..7f5d47a 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -13,7 +13,7 @@ namespace fs = std::filesystem; class App { public: - void run(std::string_view assets_dir); + void run(); private: struct RenderSync { diff --git a/src/main.cpp b/src/main.cpp index ffeaee6..238dd5c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,6 @@ 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()) { @@ -13,10 +12,9 @@ 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(assets_dir); + lvk::App{}.run(); } catch (std::exception const& e) { std::println(stderr, "PANIC: {}", e.what()); return EXIT_FAILURE; From 0605ba72985133bf5242402bbeae198786c6a487 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Thu, 27 Mar 2025 20:09:07 -0700 Subject: [PATCH 08/10] Only add available layers --- guide/src/shader_objects/shader_program.md | 27 ++++++++++++++++++++++ src/app.cpp | 22 +++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/guide/src/shader_objects/shader_program.md b/guide/src/shader_objects/shader_program.md index 40abb0a..96236b3 100644 --- a/guide/src/shader_objects/shader_program.md +++ b/guide/src/shader_objects/shader_program.md @@ -35,6 +35,33 @@ m_instance = vk::createInstanceUnique(instance_ci); This layer is not part of standard Vulkan driver installs, you must package the layer with the application for it to run on environments without Vulkan SDK / Vulkan Configurator. Read more here. +Since desired layers may not be available, we can set up a defensive check: + +```cpp +[[nodiscard]] auto get_layers(std::span desired) + -> std::vector { + auto ret = std::vector{}; + ret.reserve(desired.size()); + auto const available = vk::enumerateInstanceLayerProperties(); + for (char const* layer : desired) { + auto const pred = [layer = std::string_view{layer}]( + vk::LayerProperties const& properties) { + return properties.layerName == layer; + }; + if (std::ranges::find_if(available, pred) == available.end()) { + std::println("[lvk] [WARNING] Vulkan Layer '{}' not found", layer); + continue; + } + ret.push_back(layer); + } + return ret; +} + +// ... +auto const layers = get_layers(layers_v); +instance_ci.setPEnabledLayerNames(layers); +``` + ## `class ShaderObject` We will encapsulate both vertex and fragment shaders into a single `ShaderProgram`, which will also bind the shaders before a draw, and expose/set various dynamic states. diff --git a/src/app.cpp b/src/app.cpp index b137951..c0ce98c 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -24,6 +24,25 @@ namespace { return fs::current_path(); } +[[nodiscard]] auto get_layers(std::span desired) + -> std::vector { + auto ret = std::vector{}; + ret.reserve(desired.size()); + auto const available = vk::enumerateInstanceLayerProperties(); + for (char const* layer : desired) { + auto const pred = [layer = std::string_view{layer}]( + vk::LayerProperties const& properties) { + return properties.layerName == layer; + }; + if (std::ranges::find_if(available, pred) == available.end()) { + std::println("[lvk] [WARNING] Vulkan Layer '{}' not found", layer); + continue; + } + ret.push_back(layer); + } + return ret; +} + [[nodiscard]] auto to_spir_v(fs::path const& path) -> std::vector { // open the file at the end, to get the total size. @@ -91,7 +110,8 @@ void App::create_instance() { static constexpr auto layers_v = std::array{ "VK_LAYER_KHRONOS_shader_object", }; - instance_ci.setPEnabledLayerNames(layers_v); + auto const layers = get_layers(layers_v); + instance_ci.setPEnabledLayerNames(layers); m_instance = vk::createInstanceUnique(instance_ci); // initialize the dispatcher against the created Instance. From afc1a36ee0651d6d47b033d4fa568646d23cf3c9 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Thu, 27 Mar 2025 21:05:38 -0700 Subject: [PATCH 09/10] Add page on using pipelines --- guide/src/SUMMARY.md | 1 + guide/src/shader_objects/pipelines.md | 254 +++++++++++++++++++++ guide/src/shader_objects/shader_program.md | 5 +- 3 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 guide/src/shader_objects/pipelines.md diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 53ad568..590b41d 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -29,3 +29,4 @@ - [Shader Program](shader_objects/shader_program.md) - [GLSL to SPIR-V](shader_objects/glsl_to_spir_v.md) - [Drawing a Triangle](shader_objects/drawing_triangle.md) + - [Graphics Pipelines](shader_objects/pipelines.md) diff --git a/guide/src/shader_objects/pipelines.md b/guide/src/shader_objects/pipelines.md new file mode 100644 index 0000000..a9e23b0 --- /dev/null +++ b/guide/src/shader_objects/pipelines.md @@ -0,0 +1,254 @@ +# Graphics Pipelines + +This page describes the usage of Graphics Pipelines _instead of_ Shader Objects. While the guide assumes Shader Object usage, not much should change in the rest of the code if you instead choose to use Graphics Pipelines. A notable exception is the setup of Descriptor Set Layouts: with pipelines it needs to be specified as part of the Pipeline Layout, whereas with Shader Objects it is part of each ShaderEXT's CreateInfo. + +## Pipeline State + +Most dynamic state with Shader Objects is static with pipelines: specified at pipeline creation time. Pipelines also require additional parameters, like attachment formats and sample count: these will be considered constant and stored in the builder later. Expose a subset of dynamic states through a struct: + +```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 building pipelines into a 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) + : m_info(create_info) {} + + [[nodiscard]] auto build(vk::PipelineLayout layout, + PipelineState const& state) const + -> vk::UniquePipeline; + + private: + CreateInfo m_info{}; +}; +``` + +The implementation is quite verbose, splitting it into multiple functions helps a bit: + +```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, +}; + +[[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; +} + +// ... +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}; +} +``` + +`App` will need to store a builder, a Pipeline Layout, and the Pipeline(s): + +```cpp +std::optional m_pipeline_builder{}; +vk::UniquePipelineLayout m_pipeline_layout{}; +vk::UniquePipeline m_pipeline{}; + +// ... +void create_pipeline() { + auto const vertex_spirv = to_spir_v(asset_path("shader.vert")); + auto const fragment_spirv = to_spir_v(asset_path("shader.frag")); + if (vertex_spirv.empty() || fragment_spirv.empty()) { + throw std::runtime_error{"Failed to load shaders"}; + } + + auto pipeline_layout_ci = vk::PipelineLayoutCreateInfo{}; + pipeline_layout_ci.setSetLayouts({}); + m_pipeline_layout = + m_device->createPipelineLayoutUnique(pipeline_layout_ci); + + 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); + + auto vertex_ci = vk::ShaderModuleCreateInfo{}; + vertex_ci.setCode(vertex_spirv); + auto fragment_ci = vk::ShaderModuleCreateInfo{}; + fragment_ci.setCode(fragment_spirv); + + auto const vertex_shader = + m_device->createShaderModuleUnique(vertex_ci); + auto const fragment_shader = + m_device->createShaderModuleUnique(fragment_ci); + auto const pipeline_state = PipelineState{ + .vertex_shader = *vertex_shader, + .fragment_shader = *fragment_shader, + }; + m_pipeline = + m_pipeline_builder->build(*m_pipeline_layout, pipeline_state); +} +``` + +Finally, `App::draw()`: + +```cpp +void draw(vk::CommandBuffer const command_buffer) const { + command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, + *m_pipeline); + auto viewport = vk::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, vk::Rect2D{{}, m_render_target->extent}); + command_buffer.draw(3, 1, 0, 0); +} +``` diff --git a/guide/src/shader_objects/shader_program.md b/guide/src/shader_objects/shader_program.md index 96236b3..dbd6a82 100644 --- a/guide/src/shader_objects/shader_program.md +++ b/guide/src/shader_objects/shader_program.md @@ -77,6 +77,8 @@ struct ShaderProgramCreateInfo { }; ``` +> Descriptor Sets and their Layouts will be covered later. + Start with a skeleton definition: ```cpp @@ -178,7 +180,8 @@ struct ShaderVertexInput { struct ShaderProgramCreateInfo { // ... - ShaderVertexInput vertex_input{}; + ShaderVertexInput vertex_input{}; + // ... }; class ShaderProgram { From c7e361ce0270ca0edce2d1e2ee7eaf952c08f17c Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Thu, 27 Mar 2025 21:22:53 -0700 Subject: [PATCH 10/10] Update guide pages --- guide/src/README.md | 7 ++++++- guide/src/SUMMARY.md | 3 +++ guide/src/getting_started/README.md | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/guide/src/README.md b/guide/src/README.md index 7948476..966f383 100644 --- a/guide/src/README.md +++ b/guide/src/README.md @@ -25,4 +25,9 @@ Some examples of what this guide _does not_ focus on: - GPU-driven rendering - Real-time graphics from ground-up -- Considerations for tiled GPUs (eg mobile devices / Android) \ No newline at end of file +- Considerations for tiled GPUs (eg mobile devices / Android) + +## Source + +The source code for the project (as well as this guide) is located in [this repository](https://github.com/cpp-gamedev/learn-vulkan). A `section/*` branch intends to reflect the state of the code at the end of a particular section of the guide. Bugfixes / changes are generally backported, but there may be some divergence from the current state of the code (ie, in `main`). The source of the guide itself is only up-to-date on `main`, changes are not backported. + diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 590b41d..6c9363b 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -16,6 +16,9 @@ - [Vulkan Device](initialization/device.md) - [Scoped Waiter](initialization/scoped_waiter.md) - [Swapchain](initialization/swapchain.md) + +# Hello Triangle + - [Rendering](rendering/README.md) - [Swapchain Loop](rendering/swapchain_loop.md) - [Render Sync](rendering/render_sync.md) diff --git a/guide/src/getting_started/README.md b/guide/src/getting_started/README.md index f2f4541..9c1a639 100644 --- a/guide/src/getting_started/README.md +++ b/guide/src/getting_started/README.md @@ -2,7 +2,7 @@ Vulkan is platform agnostic, which is one of the main reasons for its verbosity: it has to account for a wide range of implementations in its API. We shall be constraining our approach to Windows and Linux (x64 or aarch64), and focusing on discrete GPUs, enabing us to sidestep quite a bit of that verbosity. Vulkan 1.3 is widely supported by the target desktop platforms and reasonably recent graphics cards. -> _This doesn't mean that eg an integrated graphics chip will not be supported, it will just not be particularly designed/optimized for._ +> This doesn't mean that eg an integrated graphics chip will not be supported, it will just not be particularly designed/optimized for. ## Technical Requirements @@ -31,4 +31,4 @@ The project uses a "Build the World" approach, enabling usage of sanitizers, rep 1. While Vulkan is a C API, it offers an official C++ wrapper library with many quality-of-life features. This guide almost exclusively uses that, except at the boundaries of other C libraries that themselves use the C API (eg GLFW and VMA). 1. [Vulkan Memory Allocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator/) for dealing with Vulkan memory heaps 1. [GLM](https://github.com/g-truc/glm) for GLSL-like linear algebra in C++ -1. [Dear ImGui](https://github.com/ocornut/imgui) for UI \ No newline at end of file +1. [Dear ImGui](https://github.com/ocornut/imgui) for UI