blob: 17921e9e6b3862ef26a02c322f6e60e14f2f8528 [file]
/*
* Copyright 2025 Rive
*/
#include "rive/math/bitwise.hpp"
#include "rive/renderer/vulkan/render_context_vulkan_impl.hpp"
#include "rive/renderer/vulkan/vulkan_context.hpp"
#include "rive/renderer/stack_vector.hpp"
#include "shaders/constants.glsl"
#include "common_layouts.hpp"
#include "draw_pipeline_layout_vulkan.hpp"
#include "pipeline_manager_vulkan.hpp"
#include "render_pass_vulkan.hpp"
namespace rive::gpu
{
constexpr static VkBlendOp vk_blend_op(gpu::BlendEquation equation)
{
switch (equation)
{
case gpu::BlendEquation::none:
case gpu::BlendEquation::srcOver:
case gpu::BlendEquation::plus:
return VK_BLEND_OP_ADD;
case gpu::BlendEquation::min:
return VK_BLEND_OP_MIN;
case gpu::BlendEquation::max:
return VK_BLEND_OP_MAX;
case gpu::BlendEquation::screen:
case gpu::BlendEquation::overlay:
case gpu::BlendEquation::darken:
case gpu::BlendEquation::lighten:
case gpu::BlendEquation::colorDodge:
case gpu::BlendEquation::colorBurn:
case gpu::BlendEquation::hardLight:
case gpu::BlendEquation::softLight:
case gpu::BlendEquation::difference:
case gpu::BlendEquation::exclusion:
case gpu::BlendEquation::multiply:
case gpu::BlendEquation::hue:
case gpu::BlendEquation::saturation:
case gpu::BlendEquation::color:
case gpu::BlendEquation::luminosity:
break;
}
RIVE_UNREACHABLE();
}
constexpr static VkBlendFactor vk_dst_blend_factor(gpu::BlendEquation equation)
{
switch (equation)
{
case gpu::BlendEquation::none:
return VK_BLEND_FACTOR_ZERO;
case gpu::BlendEquation::srcOver:
return VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
case gpu::BlendEquation::plus:
case gpu::BlendEquation::min:
case gpu::BlendEquation::max:
return VK_BLEND_FACTOR_ONE;
case gpu::BlendEquation::screen:
case gpu::BlendEquation::overlay:
case gpu::BlendEquation::darken:
case gpu::BlendEquation::lighten:
case gpu::BlendEquation::colorDodge:
case gpu::BlendEquation::colorBurn:
case gpu::BlendEquation::hardLight:
case gpu::BlendEquation::softLight:
case gpu::BlendEquation::difference:
case gpu::BlendEquation::exclusion:
case gpu::BlendEquation::multiply:
case gpu::BlendEquation::hue:
case gpu::BlendEquation::saturation:
case gpu::BlendEquation::color:
case gpu::BlendEquation::luminosity:
break;
}
RIVE_UNREACHABLE();
}
uint64_t DrawPipelineVulkan::PipelineProps::createKey(
const PlatformFeatures& platformFeatures) const
{
uint64_t key = gpu::pipeline_unique_key(
drawType,
shaderFeatures,
interlockMode,
shaderMiscFlags,
drawContents,
enums::is_flag_set(renderPassOptions,
RenderPassOptionsVulkan::fixedFunctionColorOutput),
blendMode,
platformFeatures);
const uint64_t renderPassKeyNoInterlockMode =
RenderPassVulkan::KeyNoInterlockMode(renderPassOptions,
renderTargetFormat,
colorLoadAction);
key = math::add_bits_to_key(
key,
renderPassKeyNoInterlockMode,
RenderPassVulkan::KEY_NO_INTERLOCK_MODE_BIT_COUNT);
key =
math::add_bits_to_key(key, uint64_t(drawPipelineOptions), OPTION_COUNT);
// pipeline_unique_key() keys on baked depth/stencil/cull/color state, so a
// dynamic-state pipeline collides with the static pipelines that share its
// state (e.g. msaaDynamicMidpointFans vs. msaaMidpointFans). They need
// distinct pipelines -- one uses the dynamic-state layout, the other bakes
// -- so fold the layout choice into the key.
key = math::add_bits_to_key(
key,
uint64_t(vkutil::hasPipelineDynamicState(drawType)),
1);
return key;
}
uint32_t subpass_index(gpu::DrawType drawType,
gpu::LoadAction colorLoadAction,
gpu::InterlockMode interlockMode,
gpu::ShaderMiscFlags shaderMiscFlags)
{
if (interlockMode == gpu::InterlockMode::clockwiseAtomic)
{
// In clockwiseAtomic mode, borrowed coverage is rendered in a separate
// subpass prior to the main one.
return enums::is_flag_set(shaderMiscFlags,
gpu::ShaderMiscFlags::borrowedCoveragePass)
? 0
: 1;
}
const uint32_t mainSubpassIdx =
(interlockMode == gpu::InterlockMode::msaa &&
colorLoadAction == gpu::LoadAction::preserveRenderTarget)
? 1
: 0;
switch (drawType)
{
case gpu::DrawType::renderPassInitialize:
assert(mainSubpassIdx == 1);
return 0;
case gpu::DrawType::midpointFanPatches:
case gpu::DrawType::midpointFanCenterAAPatches:
case gpu::DrawType::outerCurvePatches:
case gpu::DrawType::interiorTriangulation:
case gpu::DrawType::featherAtlasBlit:
case gpu::DrawType::imageRect:
case gpu::DrawType::imageMesh:
case gpu::DrawType::msaaStrokes:
case gpu::DrawType::msaaMidpointFanBorrowedCoverage:
case gpu::DrawType::msaaDynamicMidpointFans:
case gpu::DrawType::msaaMidpointFans:
case gpu::DrawType::msaaMidpointFanStencilReset:
case gpu::DrawType::msaaMidpointFanPathsStencil:
case gpu::DrawType::msaaMidpointFanPathsCover:
case gpu::DrawType::msaaOuterCubics:
case gpu::DrawType::clipReset:
return mainSubpassIdx;
case gpu::DrawType::renderPassResolve:
return mainSubpassIdx + 1;
}
RIVE_UNREACHABLE();
}
DrawPipelineVulkan::DrawPipelineVulkan(
PipelineManagerVulkan* pipelineManager,
const DrawPipelineLayoutVulkan& pipelineLayout,
const PipelineProps& props,
VkRenderPass vkRenderPass,
const PlatformFeatures& platformFeatures
#ifdef WITH_RIVE_TOOLS
,
SynthesizedFailureType synthesizedFailureType
#endif
) :
m_vk(ref_rcp(pipelineManager->vulkanContext()))
{
#ifdef WITH_RIVE_TOOLS
if (synthesizedFailureType == SynthesizedFailureType::pipelineCreation)
{
return;
}
#endif
const bool pipelineWriteOnlyRenderTarget =
enums::is_flag_set(props.renderPassOptions,
RenderPassOptionsVulkan::fixedFunctionColorOutput);
const gpu::PipelineState pipelineState =
get_pipeline_state(props.drawType,
props.interlockMode,
props.shaderMiscFlags,
props.drawContents,
pipelineWriteOnlyRenderTarget,
props.blendMode,
platformFeatures);
const gpu::InterlockMode interlockMode = pipelineLayout.interlockMode();
uint32_t subpassIndex = subpass_index(props.drawType,
props.colorLoadAction,
interlockMode,
props.shaderMiscFlags);
auto& vertShader =
pipelineManager->getVertexShaderSynchronous(props.drawType,
props.shaderFeatures,
interlockMode);
if (vertShader.module() == VK_NULL_HANDLE)
{
return;
}
auto& fragShader =
pipelineManager->getFragmentShaderSynchronous(props.drawType,
props.shaderFeatures,
interlockMode,
props.shaderMiscFlags);
if (fragShader.module() == VK_NULL_HANDLE)
{
return;
}
uint32_t shaderPermutationFlags[] = {
enums::is_flag_set(props.shaderFeatures,
gpu::ShaderFeatures::ENABLE_CLIPPING),
enums::is_flag_set(props.shaderFeatures,
gpu::ShaderFeatures::ENABLE_CLIP_RECT),
enums::is_flag_set(props.shaderFeatures,
gpu::ShaderFeatures::ENABLE_ADVANCED_BLEND),
enums::is_flag_set(props.shaderFeatures,
gpu::ShaderFeatures::ENABLE_FEATHER),
enums::is_flag_set(props.shaderFeatures,
gpu::ShaderFeatures::ENABLE_EVEN_ODD),
enums::is_flag_set(props.shaderFeatures,
gpu::ShaderFeatures::ENABLE_NESTED_CLIPPING),
enums::is_flag_set(props.shaderFeatures,
gpu::ShaderFeatures::ENABLE_HSL_BLEND_MODES),
enums::is_flag_set(props.shaderFeatures,
gpu::ShaderFeatures::ENABLE_DITHER),
enums::is_flag_set(props.shaderMiscFlags,
gpu::ShaderMiscFlags::clockwiseFill),
enums::is_flag_set(props.shaderMiscFlags,
gpu::ShaderMiscFlags::nestedClipUpdateOnly),
enums::is_flag_set(props.shaderMiscFlags,
gpu::ShaderMiscFlags::borrowedCoveragePass),
enums::is_flag_set(
props.shaderMiscFlags,
gpu::ShaderMiscFlags::emulateDynamicColorWriteDisable),
enums::is_flag_set(props.shaderMiscFlags,
gpu::ShaderMiscFlags::storeColorClear),
enums::is_flag_set(props.shaderMiscFlags,
gpu::ShaderMiscFlags::loadColorFromDstTexture),
pipelineManager->vendorID() == vkutil::vendors::ARM,
};
static_assert(std::size(shaderPermutationFlags) == SPECIALIZATION_COUNT);
static_assert(CLIPPING_SPECIALIZATION_IDX == 0);
static_assert(CLIP_RECT_SPECIALIZATION_IDX == 1);
static_assert(ADVANCED_BLEND_SPECIALIZATION_IDX == 2);
static_assert(FEATHER_SPECIALIZATION_IDX == 3);
static_assert(EVEN_ODD_SPECIALIZATION_IDX == 4);
static_assert(NESTED_CLIPPING_SPECIALIZATION_IDX == 5);
static_assert(HSL_BLEND_MODES_SPECIALIZATION_IDX == 6);
static_assert(DITHER_SPECIALIZATION_IDX == 7);
static_assert(CLOCKWISE_FILL_SPECIALIZATION_IDX == 8);
static_assert(NESTED_CLIP_UPDATE_ONLY_SPECIALIZATION_IDX == 9);
static_assert(BORROWED_COVERAGE_PASS_SPECIALIZATION_IDX == 10);
static_assert(EMULATE_DYNAMIC_COLOR_WRITE_DISABLE_SPECIALIZATION_IDX == 11);
static_assert(STORE_COLOR_CLEAR_SPECIALIZATION_IDX == 12);
static_assert(LOAD_COLOR_FROM_DST_TEXTURE_SPECIALIZATION_IDX == 13);
static_assert(VULKAN_VENDOR_ARM_SPECIALIZATION_IDX == 14);
static_assert(SPECIALIZATION_COUNT == 15);
VkSpecializationMapEntry permutationMapEntries[SPECIALIZATION_COUNT];
for (uint32_t i = 0; i < SPECIALIZATION_COUNT; ++i)
{
permutationMapEntries[i] = {
.constantID = i,
.offset = i * static_cast<uint32_t>(sizeof(uint32_t)),
.size = sizeof(uint32_t),
};
}
VkSpecializationInfo specializationInfo = {
.mapEntryCount = SPECIALIZATION_COUNT,
.pMapEntries = permutationMapEntries,
.dataSize = sizeof(shaderPermutationFlags),
.pData = &shaderPermutationFlags,
};
VkPipelineShaderStageCreateInfo stages[] = {
{
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_VERTEX_BIT,
.module = vertShader.module(),
.pName = "main",
.pSpecializationInfo = &specializationInfo,
},
{
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_FRAGMENT_BIT,
.module = fragShader.module(),
.pName = "main",
.pSpecializationInfo = &specializationInfo,
},
};
VkPipelineRasterizationStateCreateInfo
pipelineRasterizationStateCreateInfo = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
.polygonMode = enums::is_flag_set(props.drawPipelineOptions,
Options::wireframe)
? VK_POLYGON_MODE_LINE
: VK_POLYGON_MODE_FILL,
.cullMode = vkutil::vkCullMode(pipelineState.cullFace),
.frontFace = VK_FRONT_FACE_CLOCKWISE,
.lineWidth = 1.0,
};
gpu::BlendEquation blendEquation = pipelineState.blendEquation;
bool colorWriteEnabled = pipelineState.colorWriteEnabled;
if (interlockMode == gpu::InterlockMode::rasterOrdering ||
interlockMode == gpu::InterlockMode::atomics ||
(interlockMode == gpu::InterlockMode::clockwiseAtomic &&
!enums::is_flag_set(props.shaderMiscFlags,
gpu::ShaderMiscFlags::borrowedCoveragePass)))
{
// The pipeline state gets generated under the assumption that pixel
// local storage can still be written when colorWriteEnabled is false.
// So when Vulkan implements PLS via color attachments, we need to
// override the colorWriteEnabled state.
colorWriteEnabled = true;
}
if (interlockMode == gpu::InterlockMode::atomics &&
!enums::is_flag_set(props.shaderMiscFlags,
gpu::ShaderMiscFlags::coalescedResolveAndTransfer))
{
// Vulkan deviates from the other renderers by enabling src-over
// blending for PLS planes in atomic mode.
//
// Advanced blend modes are handled by rearranging the math such that
// the correct color isn't reached until *AFTER* this blend state is
// applied. This primarily benefits us by hinting to the hardware that
// it doesn't need to read or write anything when a == 0, but it also
// saves flops by offloading the blend work onto the ROP blending unit.
//
// Clip also has blend enabled, which allows us to preserve both clip
// and color contents by just emitting a=0 (instead of loading and
// re-emitting the current value) when a PLS plane needs to remain
// unchanged during a fragment invocation.
blendEquation = gpu::BlendEquation::srcOver;
}
#ifndef NDEBUG
else if (enums::is_flag_set(
props.shaderMiscFlags,
gpu::ShaderMiscFlags::coalescedResolveAndTransfer))
{
assert(interlockMode == gpu::InterlockMode::atomics);
assert(blendEquation == gpu::BlendEquation::none);
}
#endif
StackVector<VkPipelineColorBlendAttachmentState, PLS_PLANE_COUNT>
blendStates;
blendStates.push_back_n(
pipelineLayout.colorAttachmentCount(subpassIndex,
pipelineLayout.renderPassOptions()),
{
.blendEnable = blendEquation != gpu::BlendEquation::none,
.srcColorBlendFactor = VK_BLEND_FACTOR_ONE,
.dstColorBlendFactor = vk_dst_blend_factor(blendEquation),
.colorBlendOp = vk_blend_op(blendEquation),
.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE,
.dstAlphaBlendFactor = vk_dst_blend_factor(blendEquation),
.alphaBlendOp = vk_blend_op(blendEquation),
.colorWriteMask = colorWriteEnabled ? vkutil::kColorWriteMaskRGBA
: vkutil::kColorWriteMaskNone,
});
if (m_vk->features.independentBlend &&
interlockMode == gpu::InterlockMode::clockwiseAtomic)
{
// Since we support independentBlend, it will hopefully give us a perf
// boost to disable color writes to clip/color when not being updated.
// Otherwise, we will rely on the shader emitting no-op values for the
// non-updated planes.
if (enums::is_flag_set(props.shaderMiscFlags,
gpu::ShaderMiscFlags::clipUpdateOnly))
{
blendStates[COLOR_PLANE_IDX].colorWriteMask =
vkutil::kColorWriteMaskNone;
}
else if (props.drawType != gpu::DrawType::renderPassInitialize)
{
assert(props.drawType != gpu::DrawType::clipReset);
assert(!enums::is_flag_set(
props.shaderMiscFlags,
gpu::ShaderMiscFlags::nestedClipUpdateOnly));
blendStates[CLIP_PLANE_IDX].colorWriteMask =
vkutil::kColorWriteMaskNone;
}
}
VkPipelineColorBlendStateCreateInfo pipelineColorBlendStateCreateInfo = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
.attachmentCount = blendStates.size(),
.pAttachments = blendStates.data(),
};
if (interlockMode == gpu::InterlockMode::rasterOrdering &&
m_vk->features.rasterizationOrderColorAttachmentAccess)
{
pipelineColorBlendStateCreateInfo.flags |=
VK_PIPELINE_COLOR_BLEND_STATE_CREATE_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_BIT_EXT;
}
VkPipelineDepthStencilStateCreateInfo depthStencilState = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO,
.depthTestEnable = pipelineState.depthTestEnabled,
.depthWriteEnable = pipelineState.depthWriteEnabled,
.depthCompareOp = VK_COMPARE_OP_LESS,
.depthBoundsTestEnable = VK_FALSE,
.stencilTestEnable = pipelineState.stencilTestEnabled,
.minDepthBounds = gpu::DEPTH_MIN,
.maxDepthBounds = gpu::DEPTH_MAX,
};
if (pipelineState.stencilTestEnabled)
{
depthStencilState.front = {
.failOp = vkutil::vkStencilOp(
pipelineState.stencilFrontOps.stencilFailOp),
.passOp = vkutil::vkStencilOp(
pipelineState.stencilFrontOps.depthStencilPassOp),
.depthFailOp =
vkutil::vkStencilOp(pipelineState.stencilFrontOps.depthFailOp),
.compareOp =
vkutil::vkCompareOp(pipelineState.stencilFrontOps.compareOp),
.compareMask = pipelineState.stencilCompareMask,
.writeMask = pipelineState.stencilWriteMask,
.reference = pipelineState.stencilReference,
};
depthStencilState.back =
!pipelineState.stencilDoubleSided
? depthStencilState.front
: VkStencilOpState{
.failOp = vkutil::vkStencilOp(
pipelineState.stencilBackOps.stencilFailOp),
.passOp = vkutil::vkStencilOp(
pipelineState.stencilBackOps.depthStencilPassOp),
.depthFailOp = vkutil::vkStencilOp(
pipelineState.stencilBackOps.depthFailOp),
.compareOp = vkutil::vkCompareOp(
pipelineState.stencilBackOps.compareOp),
.compareMask = pipelineState.stencilCompareMask,
.writeMask = pipelineState.stencilWriteMask,
.reference = pipelineState.stencilReference,
};
}
VkPipelineMultisampleStateCreateInfo msaaState = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
.rasterizationSamples =
(interlockMode == gpu::InterlockMode::msaa &&
props.drawType != gpu::DrawType::renderPassResolve)
? VK_SAMPLE_COUNT_4_BIT
: VK_SAMPLE_COUNT_1_BIT,
};
// Everything bakes except viewport and scissor, unless this pipeline
// switches its constituent passes with dynamic state.
StackVector<VkDynamicState, 8> dynamicStates;
dynamicStates.push_back(VK_DYNAMIC_STATE_VIEWPORT);
dynamicStates.push_back(VK_DYNAMIC_STATE_SCISSOR);
if (vkutil::hasPipelineDynamicState(props.drawType))
{
// Dynamic state is currently only used for multi-pass path draws, which
// toggle depth-write, stencil, cull, and color-write per pass.
// NOTE: depthCompareOp stays baked at LESS and stencilReference is a
// constant 0x80, so neither is listed here.
dynamicStates.push_back(VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE);
dynamicStates.push_back(VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK);
dynamicStates.push_back(VK_DYNAMIC_STATE_STENCIL_WRITE_MASK);
dynamicStates.push_back(VK_DYNAMIC_STATE_STENCIL_OP);
dynamicStates.push_back(VK_DYNAMIC_STATE_CULL_MODE);
// NOTE: if VK_EXT_color_write_enable is NOT supported, the shader will
// emulate it. See ShaderMiscFlags::emulateDynamicColorWriteDisable.
if (m_vk->features.colorWriteEnable)
{
dynamicStates.push_back(VK_DYNAMIC_STATE_COLOR_WRITE_ENABLE_EXT);
}
}
const VkPipelineDynamicStateCreateInfo dynamicState = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
.dynamicStateCount = dynamicStates.size(),
.pDynamicStates = dynamicStates.data(),
};
VkGraphicsPipelineCreateInfo pipelineCreateInfo = {
.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
.stageCount = 2,
.pStages = stages,
.pViewportState = &layout::SINGLE_VIEWPORT,
.pRasterizationState = &pipelineRasterizationStateCreateInfo,
.pMultisampleState = &msaaState,
.pDepthStencilState = interlockMode == gpu::InterlockMode::msaa
? &depthStencilState
: nullptr,
.pColorBlendState = &pipelineColorBlendStateCreateInfo,
.pDynamicState = &dynamicState,
.layout = *pipelineLayout,
.renderPass = vkRenderPass,
.subpass = subpassIndex,
};
switch (props.drawType)
{
case DrawType::midpointFanPatches:
case DrawType::midpointFanCenterAAPatches:
case DrawType::outerCurvePatches:
case DrawType::msaaOuterCubics:
case DrawType::msaaStrokes:
case DrawType::msaaMidpointFanBorrowedCoverage:
case DrawType::msaaDynamicMidpointFans:
case DrawType::msaaMidpointFans:
case DrawType::msaaMidpointFanStencilReset:
case DrawType::msaaMidpointFanPathsStencil:
case DrawType::msaaMidpointFanPathsCover:
pipelineCreateInfo.pVertexInputState =
&layout::PATH_VERTEX_INPUT_STATE;
pipelineCreateInfo.pInputAssemblyState =
&layout::INPUT_ASSEMBLY_TRIANGLE_LIST;
break;
case DrawType::clipReset:
case DrawType::interiorTriangulation:
case DrawType::featherAtlasBlit:
pipelineCreateInfo.pVertexInputState =
&layout::INTERIOR_TRI_VERTEX_INPUT_STATE;
pipelineCreateInfo.pInputAssemblyState =
&layout::INPUT_ASSEMBLY_TRIANGLE_LIST;
break;
case DrawType::imageRect:
pipelineCreateInfo.pVertexInputState =
&layout::IMAGE_RECT_VERTEX_INPUT_STATE;
pipelineCreateInfo.pInputAssemblyState =
&layout::INPUT_ASSEMBLY_TRIANGLE_LIST;
break;
case DrawType::imageMesh:
pipelineCreateInfo.pVertexInputState =
&layout::IMAGE_MESH_VERTEX_INPUT_STATE;
pipelineCreateInfo.pInputAssemblyState =
&layout::INPUT_ASSEMBLY_TRIANGLE_LIST;
break;
case DrawType::renderPassResolve:
case DrawType::renderPassInitialize:
pipelineCreateInfo.pVertexInputState =
&layout::EMPTY_VERTEX_INPUT_STATE;
pipelineCreateInfo.pInputAssemblyState =
&layout::INPUT_ASSEMBLY_TRIANGLE_STRIP;
break;
}
if (m_vk->CreateGraphicsPipelines(m_vk->device,
VK_NULL_HANDLE,
1,
&pipelineCreateInfo,
nullptr,
&m_vkPipeline) != VK_SUCCESS)
{
m_vkPipeline = VK_NULL_HANDLE;
}
}
DrawPipelineVulkan::~DrawPipelineVulkan()
{
m_vk->DestroyPipeline(m_vk->device, m_vkPipeline, nullptr);
}
} // namespace rive::gpu