Shader Program
To use Shader Objects we need to enable the corresponding feature and extension during device creation:
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",
};
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
. Adding this layer to the Instance Create Info should unblock usage of this feature:
// ...
// 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);
// ...
Since desired layers may not be available, we can set up a defensive check:
[[nodiscard]] auto get_layers(std::span<char const* const> desired)
-> std::vector<char const*> {
auto ret = std::vector<char const*>{};
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 ShaderProgram
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:
struct ShaderProgramCreateInfo {
vk::Device device;
std::span<std::uint32_t const> vertex_spirv;
std::span<std::uint32_t const> fragment_spirv;
std::span<vk::DescriptorSetLayout const> set_layouts;
};
Descriptor Sets and their Layouts will be covered later.
Start with a skeleton definition:
class ShaderProgram {
public:
using CreateInfo = ShaderProgramCreateInfo;
explicit ShaderProgram(CreateInfo const& create_info);
private:
std::vector<vk::UniqueShaderEXT> m_shaders{};
ScopedWaiter m_waiter{};
};
The definition of the constructor is fairly straightforward:
ShaderProgram::ShaderProgram(CreateInfo const& create_info) {
auto const create_shader_ci =
[&create_info](std::span<std::uint32_t const> 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;
};
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:
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:
// 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 store it in the constructor:
// shader_program.hpp
// vertex attributes and bindings.
struct ShaderVertexInput {
std::span<vk::VertexInputAttributeDescription2EXT const> attributes{};
std::span<vk::VertexInputBindingDescription2EXT const> bindings{};
};
struct ShaderProgramCreateInfo {
// ...
ShaderVertexInput vertex_input{};
// ...
};
class ShaderProgram {
// ...
ShaderVertexInput m_vertex_input{};
std::vector<vk::UniqueShaderEXT> 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):
void bind(vk::CommandBuffer command_buffer,
glm::ivec2 framebuffer_size) const;
Add helper member functions and implement bind()
by calling them in succession:
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:
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);
}