blob: f62d8a885c7326937189b9c98df9202487aab1c7 [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
{
static VkStencilOp vk_stencil_op(StencilOp op)
{
switch (op)
{
case StencilOp::keep:
return VK_STENCIL_OP_KEEP;
case StencilOp::replace:
return VK_STENCIL_OP_REPLACE;
case StencilOp::zero:
return VK_STENCIL_OP_ZERO;
case StencilOp::decrClamp:
return VK_STENCIL_OP_DECREMENT_AND_CLAMP;
case StencilOp::incrWrap:
return VK_STENCIL_OP_INCREMENT_AND_WRAP;
case StencilOp::decrWrap:
return VK_STENCIL_OP_DECREMENT_AND_WRAP;
}
RIVE_UNREACHABLE();
}
static VkCompareOp vk_compare_op(gpu::StencilCompareOp op)
{
switch (op)
{
case gpu::StencilCompareOp::less:
return VK_COMPARE_OP_LESS;
case gpu::StencilCompareOp::equal:
return VK_COMPARE_OP_EQUAL;
case gpu::StencilCompareOp::lessOrEqual:
return VK_COMPARE_OP_LESS_OR_EQUAL;
case gpu::StencilCompareOp::notEqual:
return VK_COMPARE_OP_NOT_EQUAL;
case gpu::StencilCompareOp::always:
return VK_COMPARE_OP_ALWAYS;
}
RIVE_UNREACHABLE();
}
static VkCullModeFlags vk_cull_mode(CullFace cullFace)
{
switch (cullFace)
{
case CullFace::none:
return VK_CULL_MODE_NONE;
case CullFace::clockwise:
return VK_CULL_MODE_FRONT_BIT;
case CullFace::counterclockwise:
return VK_CULL_MODE_BACK_BIT;
}
RIVE_UNREACHABLE();
}
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);
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::atlasBlit:
case gpu::DrawType::imageRect:
case gpu::DrawType::imageMesh:
case gpu::DrawType::msaaStrokes:
case gpu::DrawType::msaaMidpointFanBorrowedCoverage:
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[SPECIALIZATION_COUNT] = {
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::borrowedCoveragePass),
enums::is_flag_set(props.shaderMiscFlags,
gpu::ShaderMiscFlags::nestedClipUpdateOnly),
pipelineManager->vendorID(),
};
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(BORROWED_COVERAGE_PASS_SPECIALIZATION_IDX == 9);
static_assert(NESTED_CLIP_UPDATE_ONLY_IDX == 10);
static_assert(VULKAN_VENDOR_ID_SPECIALIZATION_IDX == 11);
static_assert(SPECIALIZATION_COUNT == 12);
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 = vk_cull_mode(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 = vk_stencil_op(pipelineState.stencilFrontOps.failOp),
.passOp = vk_stencil_op(pipelineState.stencilFrontOps.passOp),
.depthFailOp =
vk_stencil_op(pipelineState.stencilFrontOps.depthFailOp),
.compareOp = vk_compare_op(pipelineState.stencilFrontOps.compareOp),
.compareMask = pipelineState.stencilCompareMask,
.writeMask = pipelineState.stencilWriteMask,
.reference = pipelineState.stencilReference,
};
depthStencilState.back =
!pipelineState.stencilDoubleSided
? depthStencilState.front
: VkStencilOpState{
.failOp =
vk_stencil_op(pipelineState.stencilBackOps.failOp),
.passOp =
vk_stencil_op(pipelineState.stencilBackOps.passOp),
.depthFailOp = vk_stencil_op(
pipelineState.stencilBackOps.depthFailOp),
.compareOp =
vk_compare_op(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,
};
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 = &layout::DYNAMIC_VIEWPORT_SCISSOR,
.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::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::atlasBlit:
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