blob: dd2c6992ae5af2aae67231e613ed87c9e0943889 [file]
/*
* Copyright 2025 Rive
*/
#include "rive/math/bitwise.hpp"
#include "rive/renderer/vulkan/render_context_vulkan_impl.hpp"
#include "shaders/constants.glsl"
#include "draw_pipeline_layout_vulkan.hpp"
#include "pipeline_manager_vulkan.hpp"
namespace rive::gpu
{
static VkSamplerMipmapMode vk_sampler_mipmap_mode(rive::ImageFilter option)
{
switch (option)
{
case rive::ImageFilter::nearest:
case rive::ImageFilter::bilinear:
return VK_SAMPLER_MIPMAP_MODE_NEAREST;
}
RIVE_UNREACHABLE();
}
static VkSamplerAddressMode vk_sampler_address_mode(rive::ImageWrap option)
{
switch (option)
{
case rive::ImageWrap::clamp:
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
case rive::ImageWrap::repeat:
return VK_SAMPLER_ADDRESS_MODE_REPEAT;
case rive::ImageWrap::mirror:
return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
}
RIVE_UNREACHABLE();
}
static VkFilter vk_filter(rive::ImageFilter option)
{
switch (option)
{
case rive::ImageFilter::bilinear:
return VK_FILTER_LINEAR;
case rive::ImageFilter::nearest:
return VK_FILTER_NEAREST;
}
RIVE_UNREACHABLE();
}
PipelineManagerVulkan::PipelineManagerVulkan(rcp<VulkanContext> vk,
ShaderCompilationMode mode,
VkImageView nullTextureView) :
Super(mode), m_vk(std::move(vk)), m_atlasFormat(VK_FORMAT_R16_SFLOAT)
{
// Create the immutable samplers.
VkSamplerCreateInfo linearSamplerCreateInfo = {
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
.magFilter = VK_FILTER_LINEAR,
.minFilter = VK_FILTER_LINEAR,
.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.minLod = 0,
.maxLod = 0,
};
VK_CHECK(m_vk->CreateSampler(m_vk->device,
&linearSamplerCreateInfo,
nullptr,
&m_linearSampler));
for (size_t i = 0; i < ImageSampler::MAX_SAMPLER_PERMUTATIONS; ++i)
{
ImageWrap wrapX = ImageSampler::GetWrapXOptionFromKey(i);
ImageWrap wrapY = ImageSampler::GetWrapYOptionFromKey(i);
ImageFilter filter = ImageSampler::GetFilterOptionFromKey(i);
VkFilter minMagFilter = vk_filter(filter);
VkSamplerCreateInfo samplerCreateInfo = {
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
.magFilter = minMagFilter,
.minFilter = minMagFilter,
.mipmapMode = vk_sampler_mipmap_mode(filter),
.addressModeU = vk_sampler_address_mode(wrapX),
.addressModeV = vk_sampler_address_mode(wrapY),
.minLod = 0,
.maxLod = VK_LOD_CLAMP_NONE,
};
VK_CHECK(m_vk->CreateSampler(m_vk->device,
&samplerCreateInfo,
nullptr,
m_imageSamplers + i));
}
// All pipelines share the same perFlush bindings.
VkDescriptorSetLayoutBinding perFlushLayoutBindings[] = {
{
.binding = FLUSH_UNIFORM_BUFFER_IDX,
.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
.descriptorCount = 1,
.stageFlags =
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
},
{
.binding = IMAGE_DRAW_UNIFORM_BUFFER_IDX,
.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,
.descriptorCount = 1,
.stageFlags =
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
},
{
.binding = PATH_BUFFER_IDX,
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
},
{
.binding = PAINT_BUFFER_IDX,
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.descriptorCount = 1,
.stageFlags =
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
},
{
.binding = PAINT_AUX_BUFFER_IDX,
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.descriptorCount = 1,
.stageFlags =
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
},
{
.binding = CONTOUR_BUFFER_IDX,
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
},
{
.binding = COVERAGE_BUFFER_IDX,
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
},
{
.binding = TESS_VERTEX_TEXTURE_IDX,
.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
.descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
},
{
.binding = GRAD_TEXTURE_IDX,
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
.descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
.pImmutableSamplers = &m_linearSampler,
},
{
.binding = FEATHER_TEXTURE_IDX,
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
.descriptorCount = 1,
.stageFlags =
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
.pImmutableSamplers = &m_linearSampler,
},
{
.binding = ATLAS_TEXTURE_IDX,
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
.descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
.pImmutableSamplers = &m_linearSampler,
},
};
VkDescriptorSetLayoutCreateInfo perFlushLayoutInfo = {
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.bindingCount = std::size(perFlushLayoutBindings),
.pBindings = perFlushLayoutBindings,
};
VK_CHECK(m_vk->CreateDescriptorSetLayout(m_vk->device,
&perFlushLayoutInfo,
nullptr,
&m_perFlushDescriptorSetLayout));
// The imageTexture gets updated with every draw that uses it.
VkDescriptorSetLayoutBinding perDrawLayoutBindings[] = {
{
.binding = IMAGE_TEXTURE_IDX,
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
.descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
},
};
VkDescriptorSetLayoutCreateInfo perDrawLayoutInfo = {
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.bindingCount = std::size(perDrawLayoutBindings),
.pBindings = perDrawLayoutBindings,
};
VK_CHECK(m_vk->CreateDescriptorSetLayout(m_vk->device,
&perDrawLayoutInfo,
nullptr,
&m_perDrawDescriptorSetLayout));
// For when a set isn't used at all by a shader.
VkDescriptorSetLayoutCreateInfo emptyLayoutInfo = {
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.bindingCount = 0,
};
VK_CHECK(m_vk->CreateDescriptorSetLayout(m_vk->device,
&emptyLayoutInfo,
nullptr,
&m_emptyDescriptorSetLayout));
// Create static descriptor sets.
VkDescriptorPoolSize staticDescriptorPoolSizes[] = {
{
.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
.descriptorCount = 1, // m_nullImageTexture
},
};
VkDescriptorPoolCreateInfo staticDescriptorPoolCreateInfo = {
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT,
.maxSets = 2,
.poolSizeCount = std::size(staticDescriptorPoolSizes),
.pPoolSizes = staticDescriptorPoolSizes,
};
VK_CHECK(m_vk->CreateDescriptorPool(m_vk->device,
&staticDescriptorPoolCreateInfo,
nullptr,
&m_staticDescriptorPool));
// Create a descriptor set to bind m_nullImageTexture when there is no image
// paint.
VkDescriptorSetAllocateInfo nullImageDescriptorSetInfo = {
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
.descriptorPool = m_staticDescriptorPool,
.descriptorSetCount = 1,
.pSetLayouts = &m_perDrawDescriptorSetLayout,
};
VK_CHECK(m_vk->AllocateDescriptorSets(m_vk->device,
&nullImageDescriptorSetInfo,
&m_nullImageDescriptorSet));
m_vk->updateImageDescriptorSets(
m_nullImageDescriptorSet,
{
.dstBinding = IMAGE_TEXTURE_IDX,
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
},
{{
.sampler = m_imageSamplers[ImageSampler::LINEAR_CLAMP_SAMPLER_KEY],
.imageView = nullTextureView,
.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
}});
}
PipelineManagerVulkan::~PipelineManagerVulkan()
{
shutdownBackgroundThread();
m_vk->DestroyDescriptorSetLayout(m_vk->device,
m_perFlushDescriptorSetLayout,
nullptr);
m_vk->DestroyDescriptorSetLayout(m_vk->device,
m_perDrawDescriptorSetLayout,
nullptr);
m_vk->DestroyDescriptorSetLayout(m_vk->device,
m_emptyDescriptorSetLayout,
nullptr);
m_vk->DestroyDescriptorPool(m_vk->device, m_staticDescriptorPool, nullptr);
for (VkSampler sampler : m_imageSamplers)
{
m_vk->DestroySampler(m_vk->device, sampler, nullptr);
}
m_vk->DestroySampler(m_vk->device, m_linearSampler, nullptr);
}
DrawPipelineLayoutVulkan& PipelineManagerVulkan::
getDrawPipelineLayoutSynchronous(InterlockMode interlockMode,
RenderPassOptionsVulkan renderPassOptions)
{
// Mask off the options that don't affect layout.
renderPassOptions &= RENDER_PASS_OPTIONS_LAYOUT_MASK;
const uint32_t key =
(static_cast<uint32_t>(interlockMode) << RENDER_PASS_OPTION_COUNT) |
static_cast<uint32_t>(renderPassOptions);
// Make sure our key doesn't overflow 32 bits.
assert(key >> RENDER_PASS_OPTION_COUNT ==
static_cast<uint32_t>(interlockMode));
return getSharedObjectSynchronous(key, m_drawPipelineLayouts, [&]() {
return std::make_unique<DrawPipelineLayoutVulkan>(this,
interlockMode,
renderPassOptions);
});
}
std::unique_ptr<DrawShaderVulkan> PipelineManagerVulkan::createVertexShader(
DrawType drawType,
ShaderFeatures shaderFeatures,
InterlockMode interlockMode)
{
return std::make_unique<DrawShaderVulkan>(DrawShaderVulkan::Type::vertex,
vulkanContext(),
drawType,
shaderFeatures,
interlockMode,
ShaderMiscFlags::none);
}
std::unique_ptr<DrawShaderVulkan> PipelineManagerVulkan::createFragmentShader(
DrawType drawType,
ShaderFeatures shaderFeatures,
InterlockMode interlockMode,
ShaderMiscFlags miscFlags)
{
return std::make_unique<DrawShaderVulkan>(DrawShaderVulkan::Type::fragment,
vulkanContext(),
drawType,
shaderFeatures,
interlockMode,
miscFlags);
}
RenderPassVulkan& PipelineManagerVulkan::getRenderPassSynchronous(
InterlockMode interlockMode,
RenderPassOptionsVulkan renderPassOptions,
VkFormat renderTargetFormat,
LoadAction colorLoadAction)
{
const uint32_t key = RenderPassVulkan::Key(interlockMode,
renderPassOptions,
renderTargetFormat,
colorLoadAction);
return getSharedObjectSynchronous(key, m_renderPasses, [&]() {
return std::make_unique<RenderPassVulkan>(this,
interlockMode,
renderPassOptions,
renderTargetFormat,
colorLoadAction);
});
}
std::unique_ptr<DrawPipelineVulkan> PipelineManagerVulkan::createPipeline(
PipelineCreateType createType,
uint64_t key,
const PipelineProps& props,
const PlatformFeatures& platformFeatures)
{
if (createType == PipelineCreateType::async)
{
this->queueBackgroundJob(key, props, platformFeatures);
return nullptr;
}
auto& renderPass = getRenderPassSynchronous(props.interlockMode,
props.renderPassOptions,
props.renderTargetFormat,
props.colorLoadAction);
assert(createType == PipelineCreateType::sync);
return std::make_unique<DrawPipelineVulkan>(
this,
*renderPass.drawPipelineLayout(),
props,
renderPass,
platformFeatures
#ifdef WITH_RIVE_TOOLS
,
props.synthesizedFailureType
#endif
);
}
PipelineStatus PipelineManagerVulkan::getPipelineStatus(
const DrawPipelineVulkan& pipeline) const
{
return (VkPipeline(pipeline) == VK_NULL_HANDLE) ? PipelineStatus::errored
: PipelineStatus::ready;
}
void PipelineManagerVulkan::clearCacheInternal()
{
m_drawPipelineLayouts.clear();
m_renderPasses.clear();
}
static Span<const BlendMode> get_relevant_blend_modes_for_pipeline_creation(
InterlockMode interlockMode,
DrawType drawType,
ShaderMiscFlags miscFlags,
DrawContents drawContents,
const PlatformFeatures& platformFeatures)
{
constexpr BlendMode SRC_OVER_ONLY[] = {BlendMode::srcOver};
switch (interlockMode)
{
case InterlockMode::rasterOrdering:
case InterlockMode::atomics:
case InterlockMode::clockwise:
case InterlockMode::clockwiseAtomic:
return make_span(SRC_OVER_ONLY);
case InterlockMode::msaa:
// If this assert ever fires (i.e. if we ever support GPU fixed-
// function advanced blend in Vulkan), we'll need to return a list
// of all blend modes instead of just srcOver.
assert(
enums::is_flag_set(drawContents, DrawContents::opaquePaint) ||
!platformFeatures.supportsBlendAdvancedKHR);
return make_span(SRC_OVER_ONLY);
}
RIVE_UNREACHABLE();
}
void PipelineManagerVulkan::forEachUbershaderPermutation(
InterlockMode interlockMode,
VkFormat renderTargetFormat,
VkImageUsageFlags renderTargetUsage,
LoadAction colorLoadAction,
const PlatformFeatures& platformFeatures,
const std::function<bool(const PipelineProps&)>& func)
{
ForEachUbershaderPermutation(
interlockMode,
platformFeatures,
[&](DrawType drawType,
ShaderFeatures shaderFeatures,
ShaderMiscFlags shaderMiscFlags) {
PipelineProps props{
.drawType = drawType,
.shaderFeatures = shaderFeatures,
.interlockMode = interlockMode,
.shaderMiscFlags = shaderMiscFlags,
.drawPipelineOptions = DrawPipelineVulkan::Options::none,
.renderTargetFormat = renderTargetFormat,
.colorLoadAction = colorLoadAction,
};
// only MSAA has draw contents options that are relevant to pipeline
// creation
const auto validDrawContents =
(interlockMode == InterlockMode::msaa)
? DRAW_CONTENTS_FOR_MSAA_PIPELINE_STATE
: DrawContents::none;
RenderPassOptionsVulkan fixedPassOptions =
RenderPassOptionsVulkan::none;
if (interlockMode != InterlockMode::clockwiseAtomic &&
interlockMode != InterlockMode::msaa &&
enums::is_flag_set(shaderMiscFlags,
ShaderMiscFlags::fixedFunctionColorOutput))
{
// In non-clockwiseAtomic mode, the render pass
// fixedFunctionColorOutput should match the one in
// shaderMiscFlags
fixedPassOptions |=
RenderPassOptionsVulkan::fixedFunctionColorOutput;
}
RenderPassOptionsVulkan validPassOptions =
RenderPassOptionsVulkan::none;
switch (interlockMode)
{
case InterlockMode::rasterOrdering:
validPassOptions |=
RenderPassOptionsVulkan::manuallyResolved |
RenderPassOptionsVulkan::rasterOrderingInterruptible |
RenderPassOptionsVulkan::rasterOrderingResume;
break;
case InterlockMode::atomics:
if (!(renderTargetUsage &
VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT) &&
!enums::is_flag_set(
shaderMiscFlags,
ShaderMiscFlags::fixedFunctionColorOutput))
{
fixedPassOptions |= RenderPassOptionsVulkan::
atomicCoalescedResolveAndTransfer;
}
break;
case InterlockMode::clockwise:
// no additional options
break;
case InterlockMode::clockwiseAtomic:
// Clockwise atomic render passes are allowed to (not) have
// this flag even if the shader has it specified (a shader
// is allowed to say "I don't read from the framebuffer"
// even if something else in the pipleine does)
if (enums::is_flag_set(
shaderMiscFlags,
ShaderMiscFlags::fixedFunctionColorOutput))
{
validPassOptions |=
RenderPassOptionsVulkan::fixedFunctionColorOutput;
}
break;
case InterlockMode::msaa:
validPassOptions |=
RenderPassOptionsVulkan::manuallyResolved |
RenderPassOptionsVulkan::msaaSeedFromOffscreenTexture;
if (enums::is_flag_set(
shaderMiscFlags,
ShaderMiscFlags::fixedFunctionColorOutput))
{
// Like clockwiseAtomic, msaa render passes are allowed
// to not have this flag even if a specific shader
// specifies it.
validPassOptions |=
RenderPassOptionsVulkan::fixedFunctionColorOutput;
}
break;
}
for (auto drawContents :
math::iterate_bit_combinations_in_mask(validDrawContents))
{
const auto stencilInfo =
get_stencil_info(interlockMode, drawType, drawContents);
if (!stencilInfo.areDrawContentsValid)
{
continue;
}
props.drawContents = drawContents;
for (auto variableRenderPassOptions :
math::iterate_bit_combinations_in_mask(validPassOptions))
{
props.renderPassOptions =
variableRenderPassOptions | fixedPassOptions;
if (enums::is_flag_set(
props.renderPassOptions,
RenderPassOptionsVulkan::manuallyResolved) &&
enums::any_flag_set(
props.renderPassOptions,
RenderPassOptionsVulkan::fixedFunctionColorOutput |
RenderPassOptionsVulkan::
rasterOrderingInterruptible))
{
// manuallyResolved and these other flags are mutually
// exclusive
continue;
}
if (enums::is_flag_set(
props.renderPassOptions,
RenderPassOptionsVulkan::
atomicCoalescedResolveAndTransfer))
{
if (enums::is_flag_set(props.renderPassOptions,
RenderPassOptionsVulkan::
fixedFunctionColorOutput))
{
// atomicCoalescedResolveAndTransfer should never be
// set when fixedFunctionColorOutput is set.
continue;
}
if (drawType == DrawType::renderPassResolve &&
!enums::is_flag_set(
props.shaderMiscFlags,
ShaderMiscFlags::coalescedResolveAndTransfer))
{
// ShaderMiscFlags::coalescedResolveAndTransfer will
// never be set if atomicCoalescedResolveAndTransfer
// is not set on the render pass
continue;
}
}
auto blendModes =
get_relevant_blend_modes_for_pipeline_creation(
interlockMode,
drawType,
shaderMiscFlags,
props.drawContents,
platformFeatures);
for (auto blendMode : blendModes)
{
props.blendMode = blendMode;
if (!func(props))
{
return false;
}
}
}
}
return true;
});
}
#if !defined(NDEBUG)
bool PipelineManagerVulkan::isValidUbershaderPipelineProps(
const PipelineProps& props,
const PlatformFeatures& platformFeatures)
{
bool found = false;
auto curKey = props.createKey(platformFeatures);
forEachUbershaderPermutation(
props.interlockMode,
props.renderTargetFormat,
enums::is_flag_set(
props.renderPassOptions,
RenderPassOptionsVulkan::atomicCoalescedResolveAndTransfer)
? VkImageUsageFlagBits(0)
: VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT,
props.colorLoadAction,
platformFeatures,
[&found, curKey, &platformFeatures](const PipelineProps& validProps) {
auto testKey = validProps.createKey(platformFeatures);
if (testKey == curKey)
{
found = true;
}
return !found; // Keep iterating if not found.
});
return found;
}
#endif
void PipelineManagerVulkan::queueUbershaderPipelineCreation(
InterlockMode interlockMode,
VkFormat renderTargetFormat,
VkImageUsageFlags renderTargetUsage,
LoadAction colorLoadAction,
const PlatformFeatures& platformFeatures)
{
forEachUbershaderPermutation(
interlockMode,
renderTargetFormat,
renderTargetUsage,
colorLoadAction,
platformFeatures,
[this, &platformFeatures](const PipelineProps& props) {
queuePipelineIfNotFound(props, platformFeatures);
return true;
});
}
} // namespace rive::gpu