|
| 1 | +# Graphics Pipelines |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +## Pipeline State |
| 6 | + |
| 7 | +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: |
| 8 | + |
| 9 | +```cpp |
| 10 | +// bit flags for various binary Pipeline States. |
| 11 | +struct PipelineFlag { |
| 12 | + enum : std::uint8_t { |
| 13 | + None = 0, |
| 14 | + AlphaBlend = 1 << 0, // turn on alpha blending. |
| 15 | + DepthTest = 1 << 1, // turn on depth write and test. |
| 16 | + }; |
| 17 | +}; |
| 18 | + |
| 19 | +// specification of a unique Graphics Pipeline. |
| 20 | +struct PipelineState { |
| 21 | + using Flag = PipelineFlag; |
| 22 | + |
| 23 | + [[nodiscard]] static constexpr auto default_flags() -> std::uint8_t { |
| 24 | + return Flag::AlphaBlend | Flag::DepthTest; |
| 25 | + } |
| 26 | + |
| 27 | + vk::ShaderModule vertex_shader; // required. |
| 28 | + vk::ShaderModule fragment_shader; // required. |
| 29 | + |
| 30 | + std::span<vk::VertexInputAttributeDescription const> vertex_attributes{}; |
| 31 | + std::span<vk::VertexInputBindingDescription const> vertex_bindings{}; |
| 32 | + |
| 33 | + vk::PrimitiveTopology topology{vk::PrimitiveTopology::eTriangleList}; |
| 34 | + vk::PolygonMode polygon_mode{vk::PolygonMode::eFill}; |
| 35 | + vk::CullModeFlags cull_mode{vk::CullModeFlagBits::eNone}; |
| 36 | + vk::CompareOp depth_compare{vk::CompareOp::eLess}; |
| 37 | + std::uint8_t flags{default_flags()}; |
| 38 | +}; |
| 39 | +``` |
| 40 | +
|
| 41 | +Encapsulate building pipelines into a class: |
| 42 | +
|
| 43 | +```cpp |
| 44 | +struct PipelineBuilderCreateInfo { |
| 45 | + vk::Device device{}; |
| 46 | + vk::SampleCountFlagBits samples{}; |
| 47 | + vk::Format color_format{}; |
| 48 | + vk::Format depth_format{}; |
| 49 | +}; |
| 50 | +
|
| 51 | +class PipelineBuilder { |
| 52 | + public: |
| 53 | + using CreateInfo = PipelineBuilderCreateInfo; |
| 54 | +
|
| 55 | + explicit PipelineBuilder(CreateInfo const& create_info) |
| 56 | + : m_info(create_info) {} |
| 57 | +
|
| 58 | + [[nodiscard]] auto build(vk::PipelineLayout layout, |
| 59 | + PipelineState const& state) const |
| 60 | + -> vk::UniquePipeline; |
| 61 | +
|
| 62 | + private: |
| 63 | + CreateInfo m_info{}; |
| 64 | +}; |
| 65 | +``` |
| 66 | + |
| 67 | +The implementation is quite verbose, splitting it into multiple functions helps a bit: |
| 68 | + |
| 69 | +```cpp |
| 70 | +// single viewport and scissor. |
| 71 | +constexpr auto viewport_state_v = |
| 72 | + vk::PipelineViewportStateCreateInfo({}, 1, {}, 1); |
| 73 | + |
| 74 | +// these dynamic states are guaranteed to be available. |
| 75 | +constexpr auto dynamic_states_v = std::array{ |
| 76 | + vk::DynamicState::eViewport, |
| 77 | + vk::DynamicState::eScissor, |
| 78 | + vk::DynamicState::eLineWidth, |
| 79 | +}; |
| 80 | + |
| 81 | +[[nodiscard]] auto create_shader_stages(vk::ShaderModule const vertex, |
| 82 | + vk::ShaderModule const fragment) { |
| 83 | + // set vertex (0) and fragment (1) shader stages. |
| 84 | + auto ret = std::array<vk::PipelineShaderStageCreateInfo, 2>{}; |
| 85 | + ret[0] |
| 86 | + .setStage(vk::ShaderStageFlagBits::eVertex) |
| 87 | + .setPName("main") |
| 88 | + .setModule(vertex); |
| 89 | + ret[1] |
| 90 | + .setStage(vk::ShaderStageFlagBits::eFragment) |
| 91 | + .setPName("main") |
| 92 | + .setModule(fragment); |
| 93 | + return ret; |
| 94 | +} |
| 95 | + |
| 96 | +[[nodiscard]] constexpr auto |
| 97 | +create_depth_stencil_state(std::uint8_t flags, |
| 98 | + vk::CompareOp const depth_compare) { |
| 99 | + auto ret = vk::PipelineDepthStencilStateCreateInfo{}; |
| 100 | + auto const depth_test = |
| 101 | + (flags & PipelineFlag::DepthTest) == PipelineFlag::DepthTest; |
| 102 | + ret.setDepthTestEnable(depth_test ? vk::True : vk::False) |
| 103 | + .setDepthCompareOp(depth_compare); |
| 104 | + return ret; |
| 105 | +} |
| 106 | + |
| 107 | +[[nodiscard]] constexpr auto |
| 108 | +create_color_blend_attachment(std::uint8_t const flags) { |
| 109 | + auto ret = vk::PipelineColorBlendAttachmentState{}; |
| 110 | + auto const alpha_blend = |
| 111 | + (flags & PipelineFlag::AlphaBlend) == PipelineFlag::AlphaBlend; |
| 112 | + using CCF = vk::ColorComponentFlagBits; |
| 113 | + ret.setColorWriteMask(CCF::eR | CCF::eG | CCF::eB | CCF::eA) |
| 114 | + .setBlendEnable(alpha_blend ? vk::True : vk::False) |
| 115 | + // standard alpha blending: |
| 116 | + // (alpha * src) + (1 - alpha) * dst |
| 117 | + .setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) |
| 118 | + .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha) |
| 119 | + .setColorBlendOp(vk::BlendOp::eAdd) |
| 120 | + .setSrcAlphaBlendFactor(vk::BlendFactor::eOne) |
| 121 | + .setDstAlphaBlendFactor(vk::BlendFactor::eZero) |
| 122 | + .setAlphaBlendOp(vk::BlendOp::eAdd); |
| 123 | + return ret; |
| 124 | +} |
| 125 | + |
| 126 | +// ... |
| 127 | +auto PipelineBuilder::build(vk::PipelineLayout const layout, |
| 128 | + PipelineState const& state) const |
| 129 | + -> vk::UniquePipeline { |
| 130 | + auto const shader_stage_ci = |
| 131 | + create_shader_stages(state.vertex_shader, state.fragment_shader); |
| 132 | + |
| 133 | + auto vertex_input_ci = vk::PipelineVertexInputStateCreateInfo{}; |
| 134 | + vertex_input_ci.setVertexAttributeDescriptions(state.vertex_attributes) |
| 135 | + .setVertexBindingDescriptions(state.vertex_bindings); |
| 136 | + |
| 137 | + auto multisample_state_ci = vk::PipelineMultisampleStateCreateInfo{}; |
| 138 | + multisample_state_ci.setRasterizationSamples(m_info.samples) |
| 139 | + .setSampleShadingEnable(vk::False); |
| 140 | + |
| 141 | + auto const input_assembly_ci = |
| 142 | + vk::PipelineInputAssemblyStateCreateInfo{{}, state.topology}; |
| 143 | + |
| 144 | + auto rasterization_state_ci = vk::PipelineRasterizationStateCreateInfo{}; |
| 145 | + rasterization_state_ci.setPolygonMode(state.polygon_mode) |
| 146 | + .setCullMode(state.cull_mode); |
| 147 | + |
| 148 | + auto const depth_stencil_state_ci = |
| 149 | + create_depth_stencil_state(state.flags, state.depth_compare); |
| 150 | + |
| 151 | + auto const color_blend_attachment = |
| 152 | + create_color_blend_attachment(state.flags); |
| 153 | + auto color_blend_state_ci = vk::PipelineColorBlendStateCreateInfo{}; |
| 154 | + color_blend_state_ci.setAttachments(color_blend_attachment); |
| 155 | + |
| 156 | + auto dynamic_state_ci = vk::PipelineDynamicStateCreateInfo{}; |
| 157 | + dynamic_state_ci.setDynamicStates(dynamic_states_v); |
| 158 | + |
| 159 | + // Dynamic Rendering requires passing this in the pNext chain. |
| 160 | + auto rendering_ci = vk::PipelineRenderingCreateInfo{}; |
| 161 | + // could be a depth-only pass, argument is span-like (notice the plural |
| 162 | + // `Formats()`), only set if not Undefined. |
| 163 | + if (m_info.color_format != vk::Format::eUndefined) { |
| 164 | + rendering_ci.setColorAttachmentFormats(m_info.color_format); |
| 165 | + } |
| 166 | + // single depth attachment format, ok to set to Undefined. |
| 167 | + rendering_ci.setDepthAttachmentFormat(m_info.depth_format); |
| 168 | + |
| 169 | + auto pipeline_ci = vk::GraphicsPipelineCreateInfo{}; |
| 170 | + pipeline_ci.setLayout(layout) |
| 171 | + .setStages(shader_stage_ci) |
| 172 | + .setPVertexInputState(&vertex_input_ci) |
| 173 | + .setPViewportState(&viewport_state_v) |
| 174 | + .setPMultisampleState(&multisample_state_ci) |
| 175 | + .setPInputAssemblyState(&input_assembly_ci) |
| 176 | + .setPRasterizationState(&rasterization_state_ci) |
| 177 | + .setPDepthStencilState(&depth_stencil_state_ci) |
| 178 | + .setPColorBlendState(&color_blend_state_ci) |
| 179 | + .setPDynamicState(&dynamic_state_ci) |
| 180 | + .setPNext(&rendering_ci); |
| 181 | + |
| 182 | + auto ret = vk::Pipeline{}; |
| 183 | + // use non-throwing API. |
| 184 | + if (m_info.device.createGraphicsPipelines({}, 1, &pipeline_ci, {}, &ret) != |
| 185 | + vk::Result::eSuccess) { |
| 186 | + std::println(stderr, "[lvk] Failed to create Graphics Pipeline"); |
| 187 | + return {}; |
| 188 | + } |
| 189 | + |
| 190 | + return vk::UniquePipeline{ret, m_info.device}; |
| 191 | +} |
| 192 | +``` |
| 193 | +
|
| 194 | +`App` will need to store a builder, a Pipeline Layout, and the Pipeline(s): |
| 195 | +
|
| 196 | +```cpp |
| 197 | +std::optional<PipelineBuilder> m_pipeline_builder{}; |
| 198 | +vk::UniquePipelineLayout m_pipeline_layout{}; |
| 199 | +vk::UniquePipeline m_pipeline{}; |
| 200 | +
|
| 201 | +// ... |
| 202 | +void create_pipeline() { |
| 203 | + auto const vertex_spirv = to_spir_v(asset_path("shader.vert")); |
| 204 | + auto const fragment_spirv = to_spir_v(asset_path("shader.frag")); |
| 205 | + if (vertex_spirv.empty() || fragment_spirv.empty()) { |
| 206 | + throw std::runtime_error{"Failed to load shaders"}; |
| 207 | + } |
| 208 | +
|
| 209 | + auto pipeline_layout_ci = vk::PipelineLayoutCreateInfo{}; |
| 210 | + pipeline_layout_ci.setSetLayouts({}); |
| 211 | + m_pipeline_layout = |
| 212 | + m_device->createPipelineLayoutUnique(pipeline_layout_ci); |
| 213 | +
|
| 214 | + auto const pipeline_builder_ci = PipelineBuilder::CreateInfo{ |
| 215 | + .device = *m_device, |
| 216 | + .samples = vk::SampleCountFlagBits::e1, |
| 217 | + .color_format = m_swapchain->get_format(), |
| 218 | + }; |
| 219 | + m_pipeline_builder.emplace(pipeline_builder_ci); |
| 220 | +
|
| 221 | + auto vertex_ci = vk::ShaderModuleCreateInfo{}; |
| 222 | + vertex_ci.setCode(vertex_spirv); |
| 223 | + auto fragment_ci = vk::ShaderModuleCreateInfo{}; |
| 224 | + fragment_ci.setCode(fragment_spirv); |
| 225 | +
|
| 226 | + auto const vertex_shader = |
| 227 | + m_device->createShaderModuleUnique(vertex_ci); |
| 228 | + auto const fragment_shader = |
| 229 | + m_device->createShaderModuleUnique(fragment_ci); |
| 230 | + auto const pipeline_state = PipelineState{ |
| 231 | + .vertex_shader = *vertex_shader, |
| 232 | + .fragment_shader = *fragment_shader, |
| 233 | + }; |
| 234 | + m_pipeline = |
| 235 | + m_pipeline_builder->build(*m_pipeline_layout, pipeline_state); |
| 236 | +} |
| 237 | +``` |
| 238 | + |
| 239 | +Finally, `App::draw()`: |
| 240 | + |
| 241 | +```cpp |
| 242 | +void draw(vk::CommandBuffer const command_buffer) const { |
| 243 | + command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, |
| 244 | + *m_pipeline); |
| 245 | + auto viewport = vk::Viewport{}; |
| 246 | + viewport.setX(0.0f) |
| 247 | + .setY(static_cast<float>(m_render_target->extent.height)) |
| 248 | + .setWidth(static_cast<float>(m_render_target->extent.width)) |
| 249 | + .setHeight(-viewport.y); |
| 250 | + command_buffer.setViewport(0, viewport); |
| 251 | + command_buffer.setScissor(0, vk::Rect2D{{}, m_render_target->extent}); |
| 252 | + command_buffer.draw(3, 1, 0, 0); |
| 253 | +} |
| 254 | +``` |
0 commit comments