| /* |
| * Copyright 2025 Rive |
| */ |
| |
| #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_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(); |
| } |
| |
| static VkBlendOp vk_blend_op(gpu::BlendEquation equation) |
| { |
| switch (equation) |
| { |
| case gpu::BlendEquation::none: |
| case gpu::BlendEquation::srcOver: |
| return VK_BLEND_OP_ADD; |
| case gpu::BlendEquation::plus: |
| return VK_BLEND_OP_ADD; |
| case gpu::BlendEquation::max: |
| return VK_BLEND_OP_MAX; |
| case gpu::BlendEquation::screen: |
| return VK_BLEND_OP_SCREEN_EXT; |
| case gpu::BlendEquation::overlay: |
| return VK_BLEND_OP_OVERLAY_EXT; |
| case gpu::BlendEquation::darken: |
| return VK_BLEND_OP_DARKEN_EXT; |
| case gpu::BlendEquation::lighten: |
| return VK_BLEND_OP_LIGHTEN_EXT; |
| case gpu::BlendEquation::colorDodge: |
| return VK_BLEND_OP_COLORDODGE_EXT; |
| case gpu::BlendEquation::colorBurn: |
| return VK_BLEND_OP_COLORBURN_EXT; |
| case gpu::BlendEquation::hardLight: |
| return VK_BLEND_OP_HARDLIGHT_EXT; |
| case gpu::BlendEquation::softLight: |
| return VK_BLEND_OP_SOFTLIGHT_EXT; |
| case gpu::BlendEquation::difference: |
| return VK_BLEND_OP_DIFFERENCE_EXT; |
| case gpu::BlendEquation::exclusion: |
| return VK_BLEND_OP_EXCLUSION_EXT; |
| case gpu::BlendEquation::multiply: |
| return VK_BLEND_OP_MULTIPLY_EXT; |
| case gpu::BlendEquation::hue: |
| return VK_BLEND_OP_HSL_HUE_EXT; |
| case gpu::BlendEquation::saturation: |
| return VK_BLEND_OP_HSL_SATURATION_EXT; |
| case gpu::BlendEquation::color: |
| return VK_BLEND_OP_HSL_COLOR_EXT; |
| case gpu::BlendEquation::luminosity: |
| return VK_BLEND_OP_HSL_LUMINOSITY_EXT; |
| } |
| RIVE_UNREACHABLE(); |
| } |
| |
| uint64_t DrawPipelineVulkan::PipelineProps::createKey() const |
| { |
| uint64_t key = gpu::ShaderUniqueKey(drawType, |
| shaderFeatures, |
| interlockMode, |
| shaderMiscFlags); |
| |
| const uint32_t renderPassKey = RenderPassVulkan::Key(interlockMode, |
| pipelineLayoutOptions, |
| renderTargetFormat, |
| colorLoadAction); |
| |
| const uint32_t pipelineStateKey = pipelineState.uniqueKey; |
| assert(key << OPTION_COUNT >> OPTION_COUNT == key); |
| key = (key << OPTION_COUNT) | static_cast<uint32_t>(drawPipelineOptions); |
| |
| assert(key << gpu::PipelineState::UNIQUE_KEY_BIT_COUNT >> |
| gpu::PipelineState::UNIQUE_KEY_BIT_COUNT == |
| key); |
| assert(pipelineStateKey < 1 << gpu::PipelineState::UNIQUE_KEY_BIT_COUNT); |
| key = (key << gpu::PipelineState::UNIQUE_KEY_BIT_COUNT) | pipelineStateKey; |
| |
| assert(key << RenderPassVulkan::KEY_BIT_COUNT >> |
| RenderPassVulkan::KEY_BIT_COUNT == |
| key); |
| assert(renderPassKey < 1 << RenderPassVulkan::KEY_BIT_COUNT); |
| key = (key << RenderPassVulkan::KEY_BIT_COUNT) | renderPassKey; |
| |
| return key; |
| } |
| |
| uint32_t subpass_index(gpu::DrawType drawType, |
| gpu::LoadAction colorLoadAction, |
| gpu::InterlockMode interlockMode) |
| { |
| 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::msaaStencilClipReset: |
| return mainSubpassIdx; |
| case gpu::DrawType::renderPassResolve: |
| return mainSubpassIdx + 1; |
| } |
| |
| RIVE_UNREACHABLE(); |
| } |
| |
| DrawPipelineVulkan::DrawPipelineVulkan( |
| PipelineManagerVulkan* pipelineManager, |
| const DrawPipelineLayoutVulkan& pipelineLayout, |
| const PipelineProps& props, |
| VkRenderPass vkRenderPass |
| #ifdef WITH_RIVE_TOOLS |
| , |
| SynthesizedFailureType synthesizedFailureType |
| #endif |
| |
| ) : |
| m_vk(ref_rcp(pipelineManager->vulkanContext())) |
| { |
| #ifdef WITH_RIVE_TOOLS |
| if (synthesizedFailureType == SynthesizedFailureType::pipelineCreation) |
| { |
| return; |
| } |
| #endif |
| |
| const gpu::PipelineState& pipelineState = props.pipelineState; |
| const gpu::InterlockMode interlockMode = pipelineLayout.interlockMode(); |
| uint32_t subpassIndex = |
| subpass_index(props.drawType, props.colorLoadAction, interlockMode); |
| |
| 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] = { |
| props.shaderFeatures & gpu::ShaderFeatures::ENABLE_CLIPPING, |
| props.shaderFeatures & gpu::ShaderFeatures::ENABLE_CLIP_RECT, |
| props.shaderFeatures & gpu::ShaderFeatures::ENABLE_ADVANCED_BLEND, |
| props.shaderFeatures & gpu::ShaderFeatures::ENABLE_FEATHER, |
| props.shaderFeatures & gpu::ShaderFeatures::ENABLE_EVEN_ODD, |
| props.shaderFeatures & gpu::ShaderFeatures::ENABLE_NESTED_CLIPPING, |
| props.shaderFeatures & gpu::ShaderFeatures::ENABLE_HSL_BLEND_MODES, |
| props.shaderMiscFlags & gpu::ShaderMiscFlags::clockwiseFill, |
| props.shaderMiscFlags & gpu::ShaderMiscFlags::borrowedCoveragePrepass, |
| 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(CLOCKWISE_FILL_SPECIALIZATION_IDX == 7); |
| static_assert(BORROWED_COVERAGE_PREPASS_SPECIALIZATION_IDX == 8); |
| static_assert(VULKAN_VENDOR_ID_SPECIALIZATION_IDX == 9); |
| static_assert(SPECIALIZATION_COUNT == 10); |
| |
| 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 = (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 blendEquationPremultiplied = true; |
| bool colorWriteEnabled = pipelineState.colorWriteEnabled; |
| if (interlockMode == gpu::InterlockMode::atomics && |
| !(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; |
| // Image draws use premultiplied src-over so they can blend the image |
| // with the previous paint together, out of order. (Premultiplied |
| // src-over is associative.) |
| // |
| // Otherwise we save 3 flops and let the ROP multiply alpha into the |
| // color when it does the blending. |
| blendEquationPremultiplied = gpu::DrawTypeIsImageDraw(props.drawType); |
| } |
| #ifndef NDEBUG |
| else if (props.shaderMiscFlags & |
| gpu::ShaderMiscFlags::coalescedResolveAndTransfer) |
| { |
| assert(interlockMode == gpu::InterlockMode::atomics); |
| assert(blendEquation == gpu::BlendEquation::none); |
| } |
| #endif |
| else if (interlockMode == gpu::InterlockMode::clockwiseAtomic) |
| { |
| // Clockwise mode is still an experimental Vulkan-only feature. |
| // Override the pipeline blend state. |
| if (props.shaderMiscFlags & |
| gpu::ShaderMiscFlags::borrowedCoveragePrepass) |
| { |
| // Borrowed coverage clockwise draws only update the coverage buffer |
| // (which is not a render target attachment). |
| blendEquation = gpu::BlendEquation::none; |
| colorWriteEnabled = false; |
| } |
| else |
| { |
| // Forward coverage clockwise draws are all unmultiplied src-over. |
| blendEquation = gpu::BlendEquation::srcOver; |
| blendEquationPremultiplied = false; |
| } |
| } |
| |
| StackVector<VkPipelineColorBlendAttachmentState, PLS_PLANE_COUNT> |
| blendStates; |
| blendStates.push_back_n( |
| pipelineLayout.colorAttachmentCount(subpassIndex), |
| { |
| .blendEnable = blendEquation != gpu::BlendEquation::none, |
| .srcColorBlendFactor = blendEquationPremultiplied |
| ? VK_BLEND_FACTOR_ONE |
| : VK_BLEND_FACTOR_SRC_ALPHA, |
| .dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, |
| .colorBlendOp = vk_blend_op(pipelineState.blendEquation), |
| .srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE, |
| .dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, |
| .alphaBlendOp = vk_blend_op(pipelineState.blendEquation), |
| .colorWriteMask = colorWriteEnabled ? vkutil::kColorWriteMaskRGBA |
| : 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 |
| ? 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::msaaStencilClipReset: |
| 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 |