/*
 * Copyright 2023 Rive
 */

#include "rive/renderer/vulkan/render_context_vulkan_impl.hpp"

#include "vulkan_shaders.hpp"
#include "rive/renderer/stack_vector.hpp"
#include "rive/renderer/texture.hpp"
#include "rive/renderer/rive_render_buffer.hpp"
#include "rive/renderer/vulkan/render_target_vulkan.hpp"
#include "shaders/constants.glsl"
#include "common_layouts.hpp"
#include "draw_pipeline_vulkan.hpp"
#include "draw_pipeline_layout_vulkan.hpp"
#include "draw_shader_vulkan.hpp"
#include "pipeline_manager_vulkan.hpp"
#include "render_pass_vulkan.hpp"
#include "instance_chunker.hpp"
#include <sstream>

namespace rive::gpu
{
using PLSBackingType = PipelineManagerVulkan::PLSBackingType;

static VkBufferUsageFlagBits render_buffer_usage_flags(
    RenderBufferType renderBufferType)
{
    switch (renderBufferType)
    {
        case RenderBufferType::index:
            return VK_BUFFER_USAGE_INDEX_BUFFER_BIT;
        case RenderBufferType::vertex:
            return VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
    }
    RIVE_UNREACHABLE();
}

class RenderBufferVulkanImpl
    : public LITE_RTTI_OVERRIDE(RiveRenderBuffer, RenderBufferVulkanImpl)
{
public:
    RenderBufferVulkanImpl(rcp<VulkanContext> vk,
                           RenderBufferType renderBufferType,
                           RenderBufferFlags renderBufferFlags,
                           size_t sizeInBytes) :
        lite_rtti_override(renderBufferType, renderBufferFlags, sizeInBytes),
        m_bufferPool(make_rcp<vkutil::BufferPool>(
            std::move(vk),
            render_buffer_usage_flags(renderBufferType),
            sizeInBytes))
    {}

    vkutil::Buffer* currentBuffer() { return m_currentBuffer.get(); }

protected:
    void* onMap() override
    {
        m_bufferPool->recycle(std::move(m_currentBuffer));
        m_currentBuffer = m_bufferPool->acquire();
        return m_currentBuffer->contents();
    }

    void onUnmap() override { m_currentBuffer->flushContents(); }

private:
    rcp<vkutil::BufferPool> m_bufferPool;
    rcp<vkutil::Buffer> m_currentBuffer;
};

rcp<RenderBuffer> RenderContextVulkanImpl::makeRenderBuffer(
    RenderBufferType type,
    RenderBufferFlags flags,
    size_t sizeInBytes)
{
    return make_rcp<RenderBufferVulkanImpl>(m_vk, type, flags, sizeInBytes);
}

rcp<Texture> RenderContextVulkanImpl::makeImageTexture(
    uint32_t width,
    uint32_t height,
    uint32_t mipLevelCount,
    const uint8_t imageDataRGBAPremul[])
{
    auto texture = m_vk->makeTexture2D(
        {
            .format = VK_FORMAT_R8G8B8A8_UNORM,
            .extent = {width, height},
            .mipLevels = mipLevelCount,
        },
        "RenderContext imageTexture");
    texture->scheduleUpload(imageDataRGBAPremul, height * width * 4);
    return texture;
}

// Common base class for a pipeline that renders a texture resource at the
// beginning of a flush, which is then read during the main draw pass.
class RenderContextVulkanImpl::ResourceTexturePipeline
{
public:
    ResourceTexturePipeline(rcp<VulkanContext> vk,
                            VkFormat format,
                            VkAttachmentLoadOp loadOp,
                            VkPipelineStageFlags resourceConsumptionStage,
                            const char* label,
                            const DriverWorkarounds& workarounds) :
        m_vk(std::move(vk))
    {
        const VkAttachmentDescription attachment = {
            .format = format,
            .samples = VK_SAMPLE_COUNT_1_BIT,
            .loadOp = loadOp,
            .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
            .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
            .finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
        };

        const VkSubpassDependency dependencies[] = {
            {
                .srcSubpass = VK_SUBPASS_EXTERNAL,
                .dstSubpass = 0,
                .srcStageMask = resourceConsumptionStage,
                .dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
                .srcAccessMask = VK_ACCESS_SHADER_READ_BIT,
                .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
            },
            {
                .srcSubpass = 0,
                .dstSubpass = VK_SUBPASS_EXTERNAL,
                .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
                .dstStageMask = resourceConsumptionStage,
                .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                .dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
            },
        };

        const VkRenderPassCreateInfo renderPassCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
            .attachmentCount = 1,
            .pAttachments = &attachment,
            .subpassCount = 1,
            .pSubpasses = &layout::SINGLE_ATTACHMENT_SUBPASS,
            .dependencyCount = std::size(dependencies),
            .pDependencies = dependencies,
        };

        VK_CHECK(m_vk->CreateRenderPass(m_vk->device,
                                        &renderPassCreateInfo,
                                        nullptr,
                                        &m_renderPass));

        const std::string renderPassLabel =
            (std::ostringstream() << label << " RenderPass").str();
        m_vk->setDebugNameIfEnabled(uint64_t(m_renderPass),
                                    VK_OBJECT_TYPE_RENDER_PASS,
                                    renderPassLabel.c_str());

        if (workarounds.needsInterruptibleRenderPasses())
        {
            // We are running on a device that is known to crash if a render
            // pass is too complex. Create another render pass that is designed
            // to resume atlas rendering in the event that the previous render
            // pass had to be interrupted (in order to work around the crash).
            auto resumingAttachment = attachment;
            resumingAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
            resumingAttachment.initialLayout =
                VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

            VkRenderPassCreateInfo resumingRenderPassCreateInfo =
                renderPassCreateInfo;
            resumingRenderPassCreateInfo.pAttachments = &resumingAttachment;

            VK_CHECK(m_vk->CreateRenderPass(m_vk->device,
                                            &resumingRenderPassCreateInfo,
                                            nullptr,
                                            &m_resumingRenderPass));

            const std::string resumingRenderPassLabel =
                (std::ostringstream() << label << " RESUME RenderPass").str();
            m_vk->setDebugNameIfEnabled(uint64_t(m_resumingRenderPass),
                                        VK_OBJECT_TYPE_RENDER_PASS,
                                        resumingRenderPassLabel.c_str());
        }
    }

    virtual ~ResourceTexturePipeline()
    {
        m_vk->DestroyRenderPass(m_vk->device, m_renderPass, nullptr);
        if (m_resumingRenderPass != VK_NULL_HANDLE)
        {
            m_vk->DestroyRenderPass(m_vk->device,
                                    m_resumingRenderPass,
                                    nullptr);
        }
    }

    VkRenderPass renderPass() const { return m_renderPass; }

    void beginRenderPass(VkCommandBuffer commandBuffer,
                         VkRect2D renderArea,
                         VkFramebuffer framebuffer)
    {
        beginRenderPass(commandBuffer,
                        renderArea,
                        framebuffer,
                        RenderPassType::primary);
    }

    // Some early Android tilers are known to crash when a render pass is too
    // complex. This is a mechanism to interrupt and begin a new render pass on
    // affected devices after a pre-set complexity is reached.
    void interruptRenderPassIfNeeded(VkCommandBuffer commandBuffer,
                                     VkRect2D renderArea,
                                     VkFramebuffer framebuffer,
                                     uint32_t nextInstanceCount,
                                     const DriverWorkarounds& workarounds)
    {
        assert(m_instanceCountInCurrentRenderPass <=
               workarounds.maxInstancesPerRenderPass);
        assert(nextInstanceCount <= workarounds.maxInstancesPerRenderPass);

        if (m_instanceCountInCurrentRenderPass + nextInstanceCount >
            workarounds.maxInstancesPerRenderPass)
        {
            m_vk->CmdEndRenderPass(commandBuffer);

            // We don't need to bind new pipelines, even though we changed
            // the render pass, because Vulkan allows for pipelines to be
            // used interchangeably with "compatible" render passes.
            beginRenderPass(commandBuffer,
                            renderArea,
                            framebuffer,
                            RenderPassType::resume);
        }
        m_instanceCountInCurrentRenderPass += nextInstanceCount;
    };

protected:
    const rcp<VulkanContext> m_vk;

private:
    enum class RenderPassType
    {
        primary,
        resume,
    };

    void beginRenderPass(VkCommandBuffer commandBuffer,
                         VkRect2D renderArea,
                         VkFramebuffer framebuffer,
                         RenderPassType renderPassType)
    {
        constexpr static VkClearValue CLEAR_ZERO = {};

        const VkRenderPass renderPass =
            (renderPassType == RenderPassType::primary) ? m_renderPass
                                                        : m_resumingRenderPass;
        assert(renderPass != VK_NULL_HANDLE);

        VkRenderPassBeginInfo renderPassBeginInfo = {
            .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
            .renderPass = renderPass,
            .framebuffer = framebuffer,
            .renderArea = renderArea,
            .clearValueCount = 1,
            .pClearValues = &CLEAR_ZERO,
        };

        m_vk->CmdBeginRenderPass(commandBuffer,
                                 &renderPassBeginInfo,
                                 VK_SUBPASS_CONTENTS_INLINE);

        m_instanceCountInCurrentRenderPass = 0;
    }

    VkRenderPass m_renderPass;
    VkRenderPass m_resumingRenderPass = VK_NULL_HANDLE;
    uint32_t m_instanceCountInCurrentRenderPass;
};

// Renders color ramps to the gradient texture.
class RenderContextVulkanImpl::ColorRampPipeline
    : public ResourceTexturePipeline
{
public:
    ColorRampPipeline(PipelineManagerVulkan* pipelineManager,
                      const DriverWorkarounds& workarounds) :
        ResourceTexturePipeline(ref_rcp(pipelineManager->vulkanContext()),
                                VK_FORMAT_R8G8B8A8_UNORM,
                                VK_ATTACHMENT_LOAD_OP_DONT_CARE,
                                VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
                                "ColorRamp",
                                workarounds)
    {
        VkDescriptorSetLayout perFlushDescriptorSetLayout =
            pipelineManager->perFlushDescriptorSetLayout();
        VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
            .setLayoutCount = 1,
            .pSetLayouts = &perFlushDescriptorSetLayout,
        };

        VK_CHECK(m_vk->CreatePipelineLayout(m_vk->device,
                                            &pipelineLayoutCreateInfo,
                                            nullptr,
                                            &m_pipelineLayout));

        VkShaderModuleCreateInfo shaderModuleCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
            .codeSize = spirv::color_ramp_vert.size_bytes(),
            .pCode = spirv::color_ramp_vert.data(),
        };

        VkShaderModule vertexShader;
        VK_CHECK(m_vk->CreateShaderModule(m_vk->device,
                                          &shaderModuleCreateInfo,
                                          nullptr,
                                          &vertexShader));

        shaderModuleCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
            .codeSize = spirv::color_ramp_frag.size_bytes(),
            .pCode = spirv::color_ramp_frag.data(),
        };

        VkShaderModule fragmentShader;
        VK_CHECK(m_vk->CreateShaderModule(m_vk->device,
                                          &shaderModuleCreateInfo,
                                          nullptr,
                                          &fragmentShader));

        VkPipelineShaderStageCreateInfo stages[] = {
            {
                .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
                .stage = VK_SHADER_STAGE_VERTEX_BIT,
                .module = vertexShader,
                .pName = "main",
            },
            {
                .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
                .stage = VK_SHADER_STAGE_FRAGMENT_BIT,
                .module = fragmentShader,
                .pName = "main",
            },
        };

        VkVertexInputBindingDescription vertexInputBindingDescription = {
            .binding = 0,
            .stride = sizeof(gpu::GradientSpan),
            .inputRate = VK_VERTEX_INPUT_RATE_INSTANCE,
        };

        VkVertexInputAttributeDescription vertexAttributeDescription = {
            .location = 0,
            .binding = 0,
            .format = VK_FORMAT_R32G32B32A32_UINT,
        };

        VkPipelineVertexInputStateCreateInfo
            pipelineVertexInputStateCreateInfo = {
                .sType =
                    VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
                .vertexBindingDescriptionCount = 1,
                .pVertexBindingDescriptions = &vertexInputBindingDescription,
                .vertexAttributeDescriptionCount = 1,
                .pVertexAttributeDescriptions = &vertexAttributeDescription,
            };

        VkGraphicsPipelineCreateInfo pipelineCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
            .stageCount = 2,
            .pStages = stages,
            .pVertexInputState = &pipelineVertexInputStateCreateInfo,
            .pInputAssemblyState = &layout::INPUT_ASSEMBLY_TRIANGLE_STRIP,
            .pViewportState = &layout::SINGLE_VIEWPORT,
            .pRasterizationState = &layout::RASTER_STATE_CULL_BACK_CCW,
            .pMultisampleState = &layout::MSAA_DISABLED,
            .pColorBlendState = &layout::SINGLE_ATTACHMENT_BLEND_DISABLED,
            .pDynamicState = &layout::DYNAMIC_VIEWPORT_SCISSOR,
            .layout = m_pipelineLayout,
            .renderPass = renderPass(),
        };

        VK_CHECK(m_vk->CreateGraphicsPipelines(m_vk->device,
                                               VK_NULL_HANDLE,
                                               1,
                                               &pipelineCreateInfo,
                                               nullptr,
                                               &m_renderPipeline));
        m_vk->setDebugNameIfEnabled(uint64_t(m_renderPipeline),
                                    VK_OBJECT_TYPE_PIPELINE,
                                    "Color Ramp Pipeline");

        m_vk->DestroyShaderModule(m_vk->device, vertexShader, nullptr);
        m_vk->DestroyShaderModule(m_vk->device, fragmentShader, nullptr);
    }

    ~ColorRampPipeline()
    {
        m_vk->DestroyPipelineLayout(m_vk->device, m_pipelineLayout, nullptr);
        m_vk->DestroyPipeline(m_vk->device, m_renderPipeline, nullptr);
    }

    VkPipelineLayout pipelineLayout() const { return m_pipelineLayout; }
    VkPipeline renderPipeline() const { return m_renderPipeline; }

private:
    VkPipelineLayout m_pipelineLayout;
    VkPipeline m_renderPipeline;
};

// Renders tessellated vertices to the tessellation texture.
class RenderContextVulkanImpl::TessellatePipeline
    : public ResourceTexturePipeline
{
public:
    TessellatePipeline(PipelineManagerVulkan* pipelineManager,
                       const DriverWorkarounds& workarounds) :
        ResourceTexturePipeline(ref_rcp(pipelineManager->vulkanContext()),
                                VK_FORMAT_R32G32B32A32_UINT,
                                VK_ATTACHMENT_LOAD_OP_DONT_CARE,
                                VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,
                                "Tessellate",
                                workarounds)
    {
        VkDescriptorSetLayout pipelineDescriptorSetLayouts[] = {
            pipelineManager->perFlushDescriptorSetLayout(),
            pipelineManager->emptyDescriptorSetLayout(),
            pipelineManager->immutableSamplerDescriptorSetLayout(),
        };
        static_assert(PER_FLUSH_BINDINGS_SET == 0);
        static_assert(IMMUTABLE_SAMPLER_BINDINGS_SET == 2);

        VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
            .setLayoutCount = std::size(pipelineDescriptorSetLayouts),
            .pSetLayouts = pipelineDescriptorSetLayouts,
        };

        VK_CHECK(m_vk->CreatePipelineLayout(m_vk->device,
                                            &pipelineLayoutCreateInfo,
                                            nullptr,
                                            &m_pipelineLayout));

        VkShaderModuleCreateInfo shaderModuleCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
            .codeSize = spirv::tessellate_vert.size_bytes(),
            .pCode = spirv::tessellate_vert.data(),
        };

        VkShaderModule vertexShader;
        VK_CHECK(m_vk->CreateShaderModule(m_vk->device,
                                          &shaderModuleCreateInfo,
                                          nullptr,
                                          &vertexShader));

        shaderModuleCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
            .codeSize = spirv::tessellate_frag.size_bytes(),
            .pCode = spirv::tessellate_frag.data(),
        };

        VkShaderModule fragmentShader;
        VK_CHECK(m_vk->CreateShaderModule(m_vk->device,
                                          &shaderModuleCreateInfo,
                                          nullptr,
                                          &fragmentShader));

        VkPipelineShaderStageCreateInfo stages[] = {
            {
                .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
                .stage = VK_SHADER_STAGE_VERTEX_BIT,
                .module = vertexShader,
                .pName = "main",
            },
            {
                .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
                .stage = VK_SHADER_STAGE_FRAGMENT_BIT,
                .module = fragmentShader,
                .pName = "main",
            },
        };

        VkVertexInputBindingDescription vertexInputBindingDescription = {
            .binding = 0,
            .stride = sizeof(gpu::TessVertexSpan),
            .inputRate = VK_VERTEX_INPUT_RATE_INSTANCE,
        };

        VkVertexInputAttributeDescription vertexAttributeDescriptions[] = {
            {
                .location = 0,
                .binding = 0,
                .format = VK_FORMAT_R32G32B32A32_SFLOAT,
                .offset = 0,
            },
            {
                .location = 1,
                .binding = 0,
                .format = VK_FORMAT_R32G32B32A32_SFLOAT,
                .offset = 4 * sizeof(float),
            },
            {
                .location = 2,
                .binding = 0,
                .format = VK_FORMAT_R32G32B32A32_SFLOAT,
                .offset = 8 * sizeof(float),
            },
            {
                .location = 3,
                .binding = 0,
                .format = VK_FORMAT_R32G32B32A32_UINT,
                .offset = 12 * sizeof(float),
            },
        };

        VkPipelineVertexInputStateCreateInfo
            pipelineVertexInputStateCreateInfo = {
                .sType =
                    VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
                .vertexBindingDescriptionCount = 1,
                .pVertexBindingDescriptions = &vertexInputBindingDescription,
                .vertexAttributeDescriptionCount = 4,
                .pVertexAttributeDescriptions = vertexAttributeDescriptions,
            };

        VkGraphicsPipelineCreateInfo pipelineCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
            .stageCount = 2,
            .pStages = stages,
            .pVertexInputState = &pipelineVertexInputStateCreateInfo,
            .pInputAssemblyState = &layout::INPUT_ASSEMBLY_TRIANGLE_LIST,
            .pViewportState = &layout::SINGLE_VIEWPORT,
            .pRasterizationState = &layout::RASTER_STATE_CULL_BACK_CCW,
            .pMultisampleState = &layout::MSAA_DISABLED,
            .pColorBlendState = &layout::SINGLE_ATTACHMENT_BLEND_DISABLED,
            .pDynamicState = &layout::DYNAMIC_VIEWPORT_SCISSOR,
            .layout = m_pipelineLayout,
            .renderPass = renderPass(),
        };

        VK_CHECK(m_vk->CreateGraphicsPipelines(m_vk->device,
                                               VK_NULL_HANDLE,
                                               1,
                                               &pipelineCreateInfo,
                                               nullptr,
                                               &m_renderPipeline));
        m_vk->setDebugNameIfEnabled(uint64_t(m_renderPipeline),
                                    VK_OBJECT_TYPE_PIPELINE,
                                    "Tesselation Pipeline");

        m_vk->DestroyShaderModule(m_vk->device, vertexShader, nullptr);
        m_vk->DestroyShaderModule(m_vk->device, fragmentShader, nullptr);
    }

    ~TessellatePipeline() override
    {
        m_vk->DestroyPipelineLayout(m_vk->device, m_pipelineLayout, nullptr);
        m_vk->DestroyPipeline(m_vk->device, m_renderPipeline, nullptr);
    }

    VkPipelineLayout pipelineLayout() const { return m_pipelineLayout; }
    VkPipeline renderPipeline() const { return m_renderPipeline; }

private:
    VkPipelineLayout m_pipelineLayout;
    VkPipeline m_renderPipeline;
};

// Renders feathers to the atlas.
class RenderContextVulkanImpl::AtlasPipeline : public ResourceTexturePipeline
{
public:
    AtlasPipeline(PipelineManagerVulkan* pipelineManager,
                  const DriverWorkarounds& workarounds) :
        ResourceTexturePipeline(ref_rcp(pipelineManager->vulkanContext()),
                                pipelineManager->atlasFormat(),
                                VK_ATTACHMENT_LOAD_OP_CLEAR,
                                VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
                                "Atlas",
                                workarounds)
    {
        VkDescriptorSetLayout pipelineDescriptorSetLayouts[] = {
            pipelineManager->perFlushDescriptorSetLayout(),
            pipelineManager->emptyDescriptorSetLayout(),
            pipelineManager->immutableSamplerDescriptorSetLayout(),
        };
        static_assert(PER_FLUSH_BINDINGS_SET == 0);
        static_assert(IMMUTABLE_SAMPLER_BINDINGS_SET == 2);

        VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
            .setLayoutCount = std::size(pipelineDescriptorSetLayouts),
            .pSetLayouts = pipelineDescriptorSetLayouts,
        };

        VK_CHECK(m_vk->CreatePipelineLayout(m_vk->device,
                                            &pipelineLayoutCreateInfo,
                                            nullptr,
                                            &m_pipelineLayout));

        VkShaderModuleCreateInfo shaderModuleCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
            .codeSize = spirv::render_atlas_vert.size_bytes(),
            .pCode = spirv::render_atlas_vert.data(),
        };

        VkShaderModule vertexShader;
        VK_CHECK(m_vk->CreateShaderModule(m_vk->device,
                                          &shaderModuleCreateInfo,
                                          nullptr,
                                          &vertexShader));

        shaderModuleCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
            .codeSize = spirv::render_atlas_fill_frag.size_bytes(),
            .pCode = spirv::render_atlas_fill_frag.data(),
        };

        VkShaderModule fragmentFillShader;
        VK_CHECK(m_vk->CreateShaderModule(m_vk->device,
                                          &shaderModuleCreateInfo,
                                          nullptr,
                                          &fragmentFillShader));

        shaderModuleCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
            .codeSize = spirv::render_atlas_stroke_frag.size_bytes(),
            .pCode = spirv::render_atlas_stroke_frag.data(),
        };

        VkShaderModule fragmentStrokeShader;
        VK_CHECK(m_vk->CreateShaderModule(m_vk->device,
                                          &shaderModuleCreateInfo,
                                          VK_NULL_HANDLE,
                                          &fragmentStrokeShader));

        VkPipelineShaderStageCreateInfo stages[] = {
            {
                .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
                .stage = VK_SHADER_STAGE_VERTEX_BIT,
                .module = vertexShader,
                .pName = "main",
            },
            {
                .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
                .stage = VK_SHADER_STAGE_FRAGMENT_BIT,
                // Set for individual fill/stroke pipelines.
                .module = VK_NULL_HANDLE,
                .pName = "main",
            },
        };

        VkPipelineColorBlendAttachmentState blendState =
            VkPipelineColorBlendAttachmentState{
                .blendEnable = VK_TRUE,
                .srcColorBlendFactor = VK_BLEND_FACTOR_ONE,
                .dstColorBlendFactor = VK_BLEND_FACTOR_ONE,
                .colorWriteMask = VK_COLOR_COMPONENT_R_BIT,
            };
        VkPipelineColorBlendStateCreateInfo blendStateCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
            .attachmentCount = 1u,
            .pAttachments = &blendState,
        };

        VkGraphicsPipelineCreateInfo pipelineCreateInfo = {
            .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
            .stageCount = std::size(stages),
            .pStages = stages,
            .pVertexInputState = &layout::PATH_VERTEX_INPUT_STATE,
            .pInputAssemblyState = &layout::INPUT_ASSEMBLY_TRIANGLE_LIST,
            .pViewportState = &layout::SINGLE_VIEWPORT,
            .pRasterizationState = &layout::RASTER_STATE_CULL_BACK_CW,
            .pMultisampleState = &layout::MSAA_DISABLED,
            .pColorBlendState = &blendStateCreateInfo,
            .pDynamicState = &layout::DYNAMIC_VIEWPORT_SCISSOR,
            .layout = m_pipelineLayout,
            .renderPass = renderPass(),
        };

        stages[1].module = fragmentFillShader;
        blendState.colorBlendOp = VK_BLEND_OP_ADD;
        VK_CHECK(m_vk->CreateGraphicsPipelines(m_vk->device,
                                               VK_NULL_HANDLE,
                                               1,
                                               &pipelineCreateInfo,
                                               nullptr,
                                               &m_fillPipeline));
        m_vk->setDebugNameIfEnabled(uint64_t(m_fillPipeline),
                                    VK_OBJECT_TYPE_PIPELINE,
                                    "Atlas Fill Pipeline");

        stages[1].module = fragmentStrokeShader;
        blendState.colorBlendOp = VK_BLEND_OP_MAX;
        VK_CHECK(m_vk->CreateGraphicsPipelines(m_vk->device,
                                               VK_NULL_HANDLE,
                                               1,
                                               &pipelineCreateInfo,
                                               nullptr,
                                               &m_strokePipeline));
        m_vk->setDebugNameIfEnabled(uint64_t(m_strokePipeline),
                                    VK_OBJECT_TYPE_PIPELINE,
                                    "Atlas Stroke Pipeline");

        m_vk->DestroyShaderModule(m_vk->device, vertexShader, nullptr);
        m_vk->DestroyShaderModule(m_vk->device, fragmentFillShader, nullptr);
        m_vk->DestroyShaderModule(m_vk->device, fragmentStrokeShader, nullptr);
    }

    ~AtlasPipeline() override
    {
        m_vk->DestroyPipelineLayout(m_vk->device, m_pipelineLayout, nullptr);
        m_vk->DestroyPipeline(m_vk->device, m_fillPipeline, nullptr);
        m_vk->DestroyPipeline(m_vk->device, m_strokePipeline, nullptr);
    }

    VkPipelineLayout pipelineLayout() const { return m_pipelineLayout; }
    VkPipeline fillPipeline() const { return m_fillPipeline; }
    VkPipeline strokePipeline() const { return m_strokePipeline; }

private:
    VkPipelineLayout m_pipelineLayout;
    VkPipeline m_fillPipeline;
    VkPipeline m_strokePipeline;
};

RenderContextVulkanImpl::RenderContextVulkanImpl(
    rcp<VulkanContext> vk,
    const ContextOptions& contextOptions) :
    m_vk(std::move(vk)),
    m_workarounds({
        .maxInstancesPerRenderPass =
            (m_vk->physicalDeviceProperties().apiVersion < VK_API_VERSION_1_3 &&
             (m_vk->physicalDeviceProperties().vendorID == VULKAN_VENDOR_ARM ||
              m_vk->physicalDeviceProperties().vendorID ==
                  VULKAN_VENDOR_IMG_TEC))
                // Early Mali and PowerVR devices are known to crash when a
                // single render pass is too complex.
                ? (1u << 13) - 1u
                : UINT32_MAX,
        // Early Xclipse drivers struggle with our manual msaa resolve, so we
        // always do automatic fullscreen resolves on that GPU family.
        .avoidManualMSAAResolves =
            m_vk->physicalDeviceProperties().vendorID == VULKAN_VENDOR_SAMSUNG,
        // Some Android drivers (some Android 12 and earlier Adreno drivers)
        // have issues with having both a self-dependency for dst reads and
        // resolve attachments. For now we just always manually resolve these
        // render passes that use advanced blend on Qualcomm.
        .needsManualMSAAResolveAfterDstRead =
            m_vk->physicalDeviceProperties().vendorID == VULKAN_VENDOR_QUALCOMM,
    }),
    m_flushUniformBufferPool(m_vk, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT),
    m_imageDrawUniformBufferPool(m_vk, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT),
    m_pathBufferPool(m_vk, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT),
    m_paintBufferPool(m_vk, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT),
    m_paintAuxBufferPool(m_vk, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT),
    m_contourBufferPool(m_vk, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT),
    m_gradSpanBufferPool(m_vk, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT),
    m_tessSpanBufferPool(m_vk, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT),
    m_triangleBufferPool(m_vk, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT),
    m_descriptorSetPoolPool(make_rcp<DescriptorSetPoolPool>(m_vk))
{
    const auto& physicalDeviceProps = m_vk->physicalDeviceProperties();

    m_platformFeatures.supportsRasterOrderingMode =
        !contextOptions.forceAtomicMode &&
        m_vk->features.rasterizationOrderColorAttachmentAccess;
#ifdef RIVE_ANDROID
    m_platformFeatures.supportsAtomicMode =
        m_vk->features.fragmentStoresAndAtomics &&
        // For now, disable gpu::InterlockMode::atomics on Android unless
        // explicitly requested. We will focus on stabilizing MSAA first, and
        // then roll this mode back in.
        contextOptions.forceAtomicMode;
#else
    m_platformFeatures.supportsAtomicMode =
        m_vk->features.fragmentStoresAndAtomics;
    m_platformFeatures.supportsClockwiseMode =
        m_platformFeatures.supportsClockwiseFixedFunctionMode =
            m_vk->features.fragmentShaderPixelInterlock &&
            !contextOptions.forceAtomicMode &&
            // TODO: Before we can support rasterOrdering and clockwise at the
            // same time, we need to figure out barriers between transient PLS
            // attachments being bound as both input attachments and storage
            // textures. (Probably by using
            // VK_EXT_rasterization_order_attachment_access whenever possible in
            // clockwise mode.)
            !m_platformFeatures.supportsRasterOrderingMode;
#endif
    m_platformFeatures.supportsClockwiseAtomicMode =
        m_platformFeatures.supportsAtomicMode;
    m_platformFeatures.supportsClipPlanes =
        m_vk->features.shaderClipDistance &&
        // The Vulkan spec mandates that the minimum value for maxClipDistances
        // is 8, but we might as well make this >= 4 check to be more clear
        // about how we're using it.
        physicalDeviceProps.limits.maxClipDistances >= 4;
    m_platformFeatures.clipSpaceBottomUp = false;
    m_platformFeatures.framebufferBottomUp = false;
    // Vulkan can't load color from a different texture into the transient MSAA
    // texture. We need to draw the previous renderTarget contents into it
    // manually when LoadAction::preserveRenderTarget is specified.
    m_platformFeatures.msaaColorPreserveNeedsDraw = true;
    m_platformFeatures.maxTextureSize =
        physicalDeviceProps.limits.maxImageDimension2D;
    m_platformFeatures.maxCoverageBufferLength =
        std::min(physicalDeviceProps.limits.maxStorageBufferRange, 1u << 28) /
        sizeof(uint32_t);

    switch (physicalDeviceProps.vendorID)
    {
        case VULKAN_VENDOR_QUALCOMM:
            // Qualcomm advertises EXT_rasterization_order_attachment_access,
            // but it's slow. Use atomics instead on this platform.
            m_platformFeatures.supportsRasterOrderingMode = false;
            // Pixel4 struggles with fine-grained fp16 path IDs.
            m_platformFeatures.pathIDGranularity = 2;
            break;

        case VULKAN_VENDOR_ARM:
            // Raster ordering is known to work on ARM hardware, even on old
            // drivers without EXT_rasterization_order_attachment_access, as
            // long as you define a subpass self-dependency.
            m_platformFeatures.supportsRasterOrderingMode =
                !contextOptions.forceAtomicMode;
            break;

        case VULKAN_VENDOR_IMG_TEC:
            // Raster ordering is known to work on IMG hardware, even without
            // EXT_rasterization_order_attachment_access, as long as you define
            // a subpass self-dependency.
            // IMG just can't expose the extension because they do _not_
            // guarantee raster ordering across samples, which is required by
            // the extension, but irrelevant to Rive.
            // THAT BEING SAID: while Google Chrome relies on implicit raster
            // ordering on all IMG devices, it has only been observed to work
            // with Rive on Vulkan 1.3 contexts (PowerVR Rogue GE9215 -- driver
            // 1.555, and PowerVR D-Series DXT-48-1536 (Pixel 10) -- driver
            // 1.602).
            m_platformFeatures.supportsRasterOrderingMode =
                !contextOptions.forceAtomicMode &&
                m_vk->features.apiVersion >= VK_API_VERSION_1_3;
            break;
    }
}

void RenderContextVulkanImpl::initGPUObjects(
    ShaderCompilationMode shaderCompilationMode)
{
    // Bound when there is not an image paint.
    constexpr static uint8_t black[] = {0, 0, 0, 1};
    m_nullImageTexture = m_vk->makeTexture2D(
        {
            .format = VK_FORMAT_R8G8B8A8_UNORM,
            .extent = {1, 1},
        },
        "null image texture");
    m_nullImageTexture->scheduleUpload(black, sizeof(black));

    if (strstr(m_vk->physicalDeviceProperties().deviceName, "Adreno (TM) 8") !=
        nullptr)
    {
        // The Adreno 8s (at least on the Galaxy S25) have a strange
        // synchronization issue around our tesselation texture, where the
        // barriers appear to not work properly (leading to tesselation texture
        // corruption, even across frames).
        // We can do a blit to a 1x1 texture, however, which seems to make the
        // synchronization play nice.
        m_tesselationSyncIssueWorkaroundTexture = m_vk->makeTexture2D(
            {
                .format = VK_FORMAT_R8G8B8A8_UINT,
                .extent = {1, 1},
                .usage = VK_IMAGE_USAGE_SAMPLED_BIT |
                         VK_IMAGE_USAGE_TRANSFER_DST_BIT,
            },
            "tesselation sync bug workaround texture");
    }

    m_pipelineManager = std::make_unique<PipelineManagerVulkan>(
        m_vk,
        shaderCompilationMode,
        m_nullImageTexture->vkImageView());

    // The pipelines reference our vulkan objects. Delete them first.
    m_colorRampPipeline =
        std::make_unique<ColorRampPipeline>(m_pipelineManager.get(),
                                            m_workarounds);
    m_tessellatePipeline =
        std::make_unique<TessellatePipeline>(m_pipelineManager.get(),
                                             m_workarounds);
    m_atlasPipeline =
        std::make_unique<AtlasPipeline>(m_pipelineManager.get(), m_workarounds);

    // Determine usage flags for transient PLS backing textures.
    m_plsTransientUsageFlags = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
                               VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT;
    if (m_platformFeatures.supportsClockwiseMode)
    {
        // When we support the interlock, PLS backings can be storage textures.
        m_plsTransientUsageFlags |= VK_IMAGE_USAGE_STORAGE_BIT |
                                    // For vkCmdClearColorImage.
                                    VK_IMAGE_USAGE_TRANSFER_DST_BIT;
    }
    else if (!m_workarounds.needsInterruptibleRenderPasses())
    {
        // Otherwise, and as long as we don't have to build interruptible render
        // passes, PLS backings are transient input attachments.
        m_plsTransientUsageFlags |= VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT;
    }

    // Emulate the feather texture1d array as a 2d texture until we add
    // texture1d support in Vulkan.
    uint16_t featherTextureData[gpu::GAUSSIAN_TABLE_SIZE *
                                FEATHER_TEXTURE_1D_ARRAY_LENGTH];
    memcpy(featherTextureData,
           gpu::g_gaussianIntegralTableF16,
           sizeof(gpu::g_gaussianIntegralTableF16));
    memcpy(featherTextureData + gpu::GAUSSIAN_TABLE_SIZE,
           gpu::g_inverseGaussianIntegralTableF16,
           sizeof(gpu::g_inverseGaussianIntegralTableF16));
    static_assert(FEATHER_FUNCTION_ARRAY_INDEX == 0);
    static_assert(FEATHER_INVERSE_FUNCTION_ARRAY_INDEX == 1);
    m_featherTexture = m_vk->makeTexture2D(
        {
            .format = VK_FORMAT_R16_SFLOAT,
            .extent =
                {
                    .width = gpu::GAUSSIAN_TABLE_SIZE,
                    .height = FEATHER_TEXTURE_1D_ARRAY_LENGTH,
                },
        },
        "feather texture");
    m_featherTexture->scheduleUpload(featherTextureData,
                                     sizeof(featherTextureData));

    m_tessSpanIndexBuffer = m_vk->makeBuffer(
        {
            .size = sizeof(gpu::kTessSpanIndices),
            .usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
        },
        vkutil::Mappability::writeOnly);
    memcpy(m_tessSpanIndexBuffer->contents(),
           gpu::kTessSpanIndices,
           sizeof(gpu::kTessSpanIndices));
    m_tessSpanIndexBuffer->flushContents();

    m_pathPatchVertexBuffer = m_vk->makeBuffer(
        {
            .size = kPatchVertexBufferCount * sizeof(gpu::PatchVertex),
            .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
        },
        vkutil::Mappability::writeOnly);
    m_pathPatchIndexBuffer = m_vk->makeBuffer(
        {
            .size = kPatchIndexBufferCount * sizeof(uint16_t),
            .usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
        },
        vkutil::Mappability::writeOnly);
    gpu::GeneratePatchBufferData(
        reinterpret_cast<PatchVertex*>(m_pathPatchVertexBuffer->contents()),
        reinterpret_cast<uint16_t*>(m_pathPatchIndexBuffer->contents()));
    m_pathPatchVertexBuffer->flushContents();
    m_pathPatchIndexBuffer->flushContents();

    m_imageRectVertexBuffer = m_vk->makeBuffer(
        {
            .size = sizeof(gpu::kImageRectVertices),
            .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
        },
        vkutil::Mappability::writeOnly);
    memcpy(m_imageRectVertexBuffer->contents(),
           gpu::kImageRectVertices,
           sizeof(gpu::kImageRectVertices));
    m_imageRectVertexBuffer->flushContents();
    m_imageRectIndexBuffer = m_vk->makeBuffer(
        {
            .size = sizeof(gpu::kImageRectIndices),
            .usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
        },
        vkutil::Mappability::writeOnly);
    memcpy(m_imageRectIndexBuffer->contents(),
           gpu::kImageRectIndices,
           sizeof(gpu::kImageRectIndices));
    m_imageRectIndexBuffer->flushContents();
}

RenderContextVulkanImpl::~RenderContextVulkanImpl()
{
    // These should all have gotten recycled at the end of the last frame.
    assert(m_flushUniformBuffer == nullptr);
    assert(m_imageDrawUniformBuffer == nullptr);
    assert(m_pathBuffer == nullptr);
    assert(m_paintBuffer == nullptr);
    assert(m_paintAuxBuffer == nullptr);
    assert(m_contourBuffer == nullptr);
    assert(m_gradSpanBuffer == nullptr);
    assert(m_tessSpanBuffer == nullptr);
    assert(m_triangleBuffer == nullptr);

    // Tell the context we are entering our shutdown cycle. After this point,
    // all resources will be deleted immediately upon their refCount reaching
    // zero, as opposed to being kept alive for in-flight command buffers.
    m_vk->shutdown();
}

void RenderContextVulkanImpl::resizeGradientTexture(uint32_t width,
                                                    uint32_t height)
{
    width = std::max(width, 1u);
    height = std::max(height, 1u);

    m_gradTexture = m_vk->makeTexture2D(
        {
            .format = VK_FORMAT_R8G8B8A8_UNORM,
            .extent = {width, height},
            .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
                     VK_IMAGE_USAGE_SAMPLED_BIT,
        },
        "gradient texture");

    m_gradTextureFramebuffer = m_vk->makeFramebuffer({
        .renderPass = m_colorRampPipeline->renderPass(),
        .attachmentCount = 1,
        .pAttachments = m_gradTexture->vkImageViewAddressOf(),
        .width = width,
        .height = height,
        .layers = 1,
    });
}

void RenderContextVulkanImpl::resizeTessellationTexture(uint32_t width,
                                                        uint32_t height)
{
    width = std::max(width, 1u);
    height = std::max(height, 1u);

    VkImageUsageFlags usage =
        VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;

    // If we are doing the Adreno synchronization workaround we also need the
    // TRANSFER_SRC bit
    if (m_tesselationSyncIssueWorkaroundTexture != nullptr)
    {
        usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
    }

    m_tessTexture = m_vk->makeTexture2D(
        {
            .format = VK_FORMAT_R32G32B32A32_UINT,
            .extent = {width, height},
            .usage = usage,
        },
        "tesselation texture");

    m_tessTextureFramebuffer = m_vk->makeFramebuffer({
        .renderPass = m_tessellatePipeline->renderPass(),
        .attachmentCount = 1,
        .pAttachments = m_tessTexture->vkImageViewAddressOf(),
        .width = width,
        .height = height,
        .layers = 1,
    });
}

void RenderContextVulkanImpl::resizeAtlasTexture(uint32_t width,
                                                 uint32_t height)
{
    width = std::max(width, 1u);
    height = std::max(height, 1u);

    m_atlasTexture = m_vk->makeTexture2D(
        {
            .format = m_pipelineManager->atlasFormat(),
            .extent = {width, height},
            .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
                     VK_IMAGE_USAGE_SAMPLED_BIT,
        },
        "atlas texture");

    m_atlasFramebuffer = m_vk->makeFramebuffer({
        .renderPass = m_atlasPipeline->renderPass(),
        .attachmentCount = 1,
        .pAttachments = m_atlasTexture->vkImageViewAddressOf(),
        .width = width,
        .height = height,
        .layers = 1,
    });
}

void RenderContextVulkanImpl::resizeTransientPLSBacking(uint32_t width,
                                                        uint32_t height,
                                                        uint32_t planeCount)
{
    // Erase the backings and allocate them lazily. Our Vulkan backend needs
    // different allocations based on interlock mode and other factors.
    m_plsExtent = {width, height, 1};
    m_plsTransientPlaneCount = planeCount;
    m_plsTransientImageArray.reset();
    m_plsTransientCoverageView.reset();
    m_plsTransientClipView.reset();
    m_plsTransientScratchColorTexture.reset();
    m_plsOffscreenColorTexture.reset();
}

void RenderContextVulkanImpl::resizeAtomicCoverageBacking(uint32_t width,
                                                          uint32_t height)
{
    m_plsAtomicCoverageTexture.reset();

    if (width != 0 && height != 0)
    {
        m_plsAtomicCoverageTexture = m_vk->makeTexture2D(
            {
                .format = VK_FORMAT_R32_UINT,
                .extent = {width, height},
                .usage =
                    VK_IMAGE_USAGE_STORAGE_BIT |
                    VK_IMAGE_USAGE_TRANSFER_DST_BIT, // For vkCmdClearColorImage
            },
            "atomic coverage backing");
    }
}

void RenderContextVulkanImpl::resizeCoverageBuffer(size_t sizeInBytes)
{
    if (sizeInBytes == 0)
    {
        m_coverageBuffer = nullptr;
    }
    else
    {
        m_coverageBuffer = m_vk->makeBuffer(
            {
                .size = sizeInBytes,
                .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
                         VK_BUFFER_USAGE_TRANSFER_DST_BIT,
            },
            vkutil::Mappability::none);
    }
}

vkutil::Image* RenderContextVulkanImpl::plsTransientImageArray()
{
    assert(m_plsExtent.width != 0);
    assert(m_plsExtent.height != 0);
    assert(m_plsExtent.depth == 1);
    assert(m_plsTransientPlaneCount != 0);

    if (m_plsTransientImageArray == nullptr)
    {
        m_plsTransientImageArray = m_vk->makeImage(
            {
                .imageType = VK_IMAGE_TYPE_2D,
                .format = VK_FORMAT_R32_UINT,
                .extent = m_plsExtent,
                .arrayLayers = std::min(m_plsTransientPlaneCount, 2u),
                .usage = m_plsTransientUsageFlags,
            },
            "plsTransientImageArray");
    }

    return m_plsTransientImageArray.get();
}

vkutil::ImageView* RenderContextVulkanImpl::plsTransientCoverageView()
{
    if (m_plsTransientCoverageView == nullptr)
    {
        m_plsTransientCoverageView = m_vk->makeImageView(
            ref_rcp(plsTransientImageArray()),
            {
                .viewType = VK_IMAGE_VIEW_TYPE_2D,
                .format = VK_FORMAT_R32_UINT,
                .subresourceRange =
                    {
                        .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
                        .levelCount = 1,
                        .baseArrayLayer = 0,
                        .layerCount = 1,
                    },
            },
            "plsTransientCoverageView");
    }

    return m_plsTransientCoverageView.get();
}

vkutil::ImageView* RenderContextVulkanImpl::plsTransientClipView()
{
    if (m_plsTransientClipView == nullptr)
    {
        assert(m_plsTransientPlaneCount != 0);
        if (m_plsTransientPlaneCount == 1)
        {
            // When planeCount is 1, the shaders are guaranteed to only use
            // 1 single plane in an entire render pass, so we can just alias
            // the coverage & clip views to each other to keep the bindings and
            // validation happy.
            m_plsTransientClipView = ref_rcp(plsTransientCoverageView());
        }
        else
        {
            m_plsTransientClipView = m_vk->makeImageView(
                ref_rcp(plsTransientImageArray()),
                {
                    .viewType = VK_IMAGE_VIEW_TYPE_2D,
                    .format = VK_FORMAT_R32_UINT,
                    .subresourceRange =
                        {
                            .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
                            .levelCount = 1,
                            .baseArrayLayer = 1,
                            .layerCount = 1,
                        },
                },
                "plsTransientClipView");
        }
    }

    return m_plsTransientClipView.get();
}

vkutil::Texture2D* RenderContextVulkanImpl::plsTransientScratchColorTexture()
{
    assert(m_plsExtent.width != 0);
    assert(m_plsExtent.height != 0);
    assert(m_plsExtent.depth == 1);
    assert(m_plsTransientPlaneCount != 0);

    if (m_plsTransientScratchColorTexture == nullptr)
    {
        m_plsTransientScratchColorTexture = m_vk->makeTexture2D(
            {
                .format = VK_FORMAT_R8G8B8A8_UNORM,
                .extent = m_plsExtent,
                .usage = m_plsTransientUsageFlags,
            },
            "plsTransientScratchColorTexture");
    }

    return m_plsTransientScratchColorTexture.get();
}

vkutil::Texture2D* RenderContextVulkanImpl::accessPLSOffscreenColorTexture(
    VkCommandBuffer commandBuffer,
    const vkutil::ImageAccess& dstAccess,
    vkutil::ImageAccessAction imageAccessAction)
{
    assert(m_plsExtent.width != 0);
    assert(m_plsExtent.height != 0);
    assert(m_plsExtent.depth == 1);
    assert(m_plsTransientPlaneCount != 0);

    if (m_plsOffscreenColorTexture == nullptr)
    {
        m_plsOffscreenColorTexture = m_vk->makeTexture2D(
            {
                .format = VK_FORMAT_R8G8B8A8_UNORM,
                .extent = m_plsExtent,
                .usage = (m_plsTransientUsageFlags |
                          // For copying back to the main render target.
                          VK_IMAGE_USAGE_TRANSFER_SRC_BIT) &
                         // This texture is never transient.
                         ~VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT,
            },
            "PLSOffscreenColorTexture");
    }

    m_plsOffscreenColorTexture->barrier(commandBuffer,
                                        dstAccess,
                                        imageAccessAction);

    return m_plsOffscreenColorTexture.get();
}

vkutil::Texture2D* RenderContextVulkanImpl::clearPLSOffscreenColorTexture(
    VkCommandBuffer commandBuffer,
    ColorInt clearColor,
    const vkutil::ImageAccess& dstAccessAfterClear)
{
    m_vk->clearColorImage(
        commandBuffer,
        clearColor,
        accessPLSOffscreenColorTexture(
            commandBuffer,
            {
                .pipelineStages = VK_PIPELINE_STAGE_TRANSFER_BIT,
                .accessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
                .layout = VK_IMAGE_LAYOUT_GENERAL,
            },
            vkutil::ImageAccessAction::invalidateContents)
            ->vkImage(),
        VK_IMAGE_LAYOUT_GENERAL);

    return accessPLSOffscreenColorTexture(commandBuffer, dstAccessAfterClear);
}

vkutil::Texture2D* RenderContextVulkanImpl::
    copyRenderTargetToPLSOffscreenColorTexture(
        VkCommandBuffer commandBuffer,
        RenderTargetVulkan* renderTarget,
        const IAABB& copyBounds,
        const vkutil::ImageAccess& dstAccessAfterCopy)
{
    m_vk->blitSubRect(commandBuffer,
                      renderTarget->accessTargetImage(
                          commandBuffer,
                          {
                              .pipelineStages = VK_PIPELINE_STAGE_TRANSFER_BIT,
                              .accessMask = VK_ACCESS_TRANSFER_READ_BIT,
                              .layout = VK_IMAGE_LAYOUT_GENERAL,
                          }),
                      VK_IMAGE_LAYOUT_GENERAL,
                      accessPLSOffscreenColorTexture(
                          commandBuffer,
                          {
                              .pipelineStages = VK_PIPELINE_STAGE_TRANSFER_BIT,
                              .accessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
                              .layout = VK_IMAGE_LAYOUT_GENERAL,
                          },
                          vkutil::ImageAccessAction::invalidateContents)
                          ->vkImage(),
                      VK_IMAGE_LAYOUT_GENERAL,
                      copyBounds);

    return accessPLSOffscreenColorTexture(commandBuffer, dstAccessAfterCopy);
}

bool RenderContextVulkanImpl::wantsManualRenderPassResolve(
    gpu::InterlockMode interlockMode,
    const RenderTarget* renderTarget,
    const IAABB& renderTargetUpdateBounds,
    gpu::DrawContents combinedDrawContents) const
{
    if (interlockMode == gpu::InterlockMode::rasterOrdering &&
        !m_workarounds.needsInterruptibleRenderPasses())
    {
#ifndef __APPLE__
        // If the render target doesn't support input attachment usage, we will
        // render to an offscreen texture that does. Add a resolve operation at
        // the end of the render pass that transfers the offscreen data back to
        // the main render target. On tilers, this saves the memory bandwidth of
        // a fullscreen copy.
        // NOTE: The manual resolve doesn't seem to work on MoltenVK, so don't
        // do it on Apple.
        auto renderTargetVulkan =
            static_cast<const RenderTargetVulkan*>(renderTarget);
        return !(renderTargetVulkan->targetUsageFlags() &
                 VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT);
#endif
    }
    if (interlockMode == gpu::InterlockMode::msaa &&
        !m_workarounds.avoidManualMSAAResolves)
    {
        if (!renderTargetUpdateBounds.contains(renderTarget->bounds()))
        {
            // Do manual resolves after partial updates because automatic
            // resolves only support fullscreen.
            // TODO: Identify when and if this is actually better than just
            // taking the hit of an automatic fullscreen resolve.
            return true;
        }
        if (m_workarounds.needsManualMSAAResolveAfterDstRead &&
            (combinedDrawContents & gpu::DrawContents::advancedBlend))
        {
            return true;
        }
    }
    return false;
}

void RenderContextVulkanImpl::prepareToFlush(uint64_t nextFrameNumber,
                                             uint64_t safeFrameNumber)
{
    // These should all have gotten recycled at the end of the last frame.
    assert(m_flushUniformBuffer == nullptr);
    assert(m_imageDrawUniformBuffer == nullptr);
    assert(m_pathBuffer == nullptr);
    assert(m_paintBuffer == nullptr);
    assert(m_paintAuxBuffer == nullptr);
    assert(m_contourBuffer == nullptr);
    assert(m_gradSpanBuffer == nullptr);
    assert(m_tessSpanBuffer == nullptr);
    assert(m_triangleBuffer == nullptr);

    // Advance the context frame and delete resources that are no longer
    // referenced by in-flight command buffers.
    m_vk->advanceFrameNumber(nextFrameNumber, safeFrameNumber);

    // Acquire buffers for the flush.
    m_flushUniformBuffer = m_flushUniformBufferPool.acquire();
    m_imageDrawUniformBuffer = m_imageDrawUniformBufferPool.acquire();
    m_pathBuffer = m_pathBufferPool.acquire();
    m_paintBuffer = m_paintBufferPool.acquire();
    m_paintAuxBuffer = m_paintAuxBufferPool.acquire();
    m_contourBuffer = m_contourBufferPool.acquire();
    m_gradSpanBuffer = m_gradSpanBufferPool.acquire();
    m_tessSpanBuffer = m_tessSpanBufferPool.acquire();
    m_triangleBuffer = m_triangleBufferPool.acquire();
}

namespace descriptor_pool_limits
{
constexpr static uint32_t kMaxUniformUpdates = 3;
constexpr static uint32_t kMaxDynamicUniformUpdates = 1;
constexpr static uint32_t kMaxImageTextureUpdates = 256;
constexpr static uint32_t kMaxSampledImageUpdates =
    4 + kMaxImageTextureUpdates; // tess + grad + feather + atlas + images
constexpr static uint32_t kMaxStorageImageUpdates =
    4; // color/coverage/clip/scratch in clockwise mode.
constexpr static uint32_t kMaxStorageBufferUpdates =
    7 + // path/paint/uniform buffers
    1;  // coverage buffer in clockwiseAtomic mode
constexpr static uint32_t kMaxDescriptorSets = 3 + kMaxImageTextureUpdates;
} // namespace descriptor_pool_limits

RenderContextVulkanImpl::DescriptorSetPool::DescriptorSetPool(
    rcp<VulkanContext> vulkanContext) :
    vkutil::Resource(std::move(vulkanContext))
{
    VkDescriptorPoolSize descriptorPoolSizes[] = {
        {
            .type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
            .descriptorCount = descriptor_pool_limits::kMaxUniformUpdates,
        },
        {
            .type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,
            .descriptorCount =
                descriptor_pool_limits::kMaxDynamicUniformUpdates,
        },
        {
            .type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
            .descriptorCount = descriptor_pool_limits::kMaxSampledImageUpdates,
        },
        {
            .type = VK_DESCRIPTOR_TYPE_SAMPLER,
            .descriptorCount = descriptor_pool_limits::kMaxImageTextureUpdates,
        },
        {
            .type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
            .descriptorCount = descriptor_pool_limits::kMaxStorageImageUpdates,
        },
        {
            .type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
            .descriptorCount = descriptor_pool_limits::kMaxStorageBufferUpdates,
        },
        {
            .type = VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT,
            .descriptorCount = 4,
        },
    };

    VkDescriptorPoolCreateInfo descriptorPoolCreateInfo = {
        .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
        .flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT,
        .maxSets = descriptor_pool_limits::kMaxDescriptorSets,
        .poolSizeCount = std::size(descriptorPoolSizes),
        .pPoolSizes = descriptorPoolSizes,
    };

    VK_CHECK(vk()->CreateDescriptorPool(vk()->device,
                                        &descriptorPoolCreateInfo,
                                        nullptr,
                                        &m_vkDescriptorPool));
}

RenderContextVulkanImpl::DescriptorSetPool::~DescriptorSetPool()
{
    vk()->DestroyDescriptorPool(vk()->device, m_vkDescriptorPool, nullptr);
}

VkDescriptorSet RenderContextVulkanImpl::DescriptorSetPool::
    allocateDescriptorSet(VkDescriptorSetLayout layout)
{
    VkDescriptorSetAllocateInfo descriptorSetAllocateInfo = {
        .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
        .descriptorPool = m_vkDescriptorPool,
        .descriptorSetCount = 1,
        .pSetLayouts = &layout,
    };

    VkDescriptorSet descriptorSet;
    VK_CHECK(vk()->AllocateDescriptorSets(vk()->device,
                                          &descriptorSetAllocateInfo,
                                          &descriptorSet));

    return descriptorSet;
}

void RenderContextVulkanImpl::DescriptorSetPool::reset()
{
    vk()->ResetDescriptorPool(vk()->device, m_vkDescriptorPool, 0);
}

rcp<RenderContextVulkanImpl::DescriptorSetPool> RenderContextVulkanImpl::
    DescriptorSetPoolPool::acquire()
{
    auto descriptorSetPool =
        static_rcp_cast<DescriptorSetPool>(GPUResourcePool::acquire());
    if (descriptorSetPool == nullptr)
    {
        descriptorSetPool = make_rcp<DescriptorSetPool>(
            static_rcp_cast<VulkanContext>(m_manager));
    }
    else
    {
        descriptorSetPool->reset();
    }
    return descriptorSetPool;
}

const DrawPipelineLayoutVulkan& RenderContextVulkanImpl::beginDrawRenderPass(
    const FlushDescriptor& desc,
    RenderPassOptionsVulkan renderPassOptions,
    const IAABB& drawBounds,
    VkImageView colorImageView,
    VkImageView msaaColorSeedImageView,
    VkImageView msaaResolveImageView)
{
    const auto commandBuffer =
        reinterpret_cast<VkCommandBuffer>(desc.externalCommandBuffer);
    auto* const renderTarget =
        static_cast<RenderTargetVulkan*>(desc.renderTarget);

    RenderPassVulkan& renderPass = m_pipelineManager->getRenderPassSynchronous(
        desc.interlockMode,
        renderPassOptions,
        renderTarget->framebufferFormat(),
        desc.colorLoadAction);

    const DrawPipelineLayoutVulkan& pipelineLayout =
        *renderPass.drawPipelineLayout();

    // Create the framebuffer.
    StackVector<VkImageView, layout::MAX_RENDER_PASS_ATTACHMENTS>
        framebufferViews;
    StackVector<VkClearValue, layout::MAX_RENDER_PASS_ATTACHMENTS> clearValues;
    if (m_pipelineManager->plsBackingType(desc.interlockMode) ==
            PLSBackingType::inputAttachment ||
        (pipelineLayout.renderPassOptions() &
         RenderPassOptionsVulkan::fixedFunctionColorOutput))
    {
        assert(framebufferViews.size() == COLOR_PLANE_IDX);
        framebufferViews.push_back(colorImageView);
        clearValues.push_back(
            {.color = vkutil::color_clear_rgba32f(desc.colorClearValue)});
    }
    if (desc.interlockMode == gpu::InterlockMode::rasterOrdering)
    {
        assert(framebufferViews.size() == CLIP_PLANE_IDX);
        framebufferViews.push_back(*plsTransientClipView());
        clearValues.push_back({});

        assert(framebufferViews.size() == SCRATCH_COLOR_PLANE_IDX);
        framebufferViews.push_back(
            plsTransientScratchColorTexture()->vkImageView());
        clearValues.push_back({});

        assert(framebufferViews.size() == COVERAGE_PLANE_IDX);
        framebufferViews.push_back(*plsTransientCoverageView());
        clearValues.push_back(
            {.color = vkutil::color_clear_r32ui(desc.coverageClearValue)});

        if (renderPassOptions & RenderPassOptionsVulkan::manuallyResolved)
        {
            // The render pass will transfer the color data back into the
            // renderTarget at the end.
            assert(framebufferViews.size() == PLS_PLANE_COUNT);
            framebufferViews.push_back(renderTarget->accessTargetImageView(
                commandBuffer,
                {
                    .pipelineStages =
                        VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
                    .accessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                    .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
                },
                vkutil::ImageAccessAction::invalidateContents));
            clearValues.push_back({});
        }
    }
    else if (desc.interlockMode == gpu::InterlockMode::atomics)
    {
        assert(framebufferViews.size() == CLIP_PLANE_IDX);
        framebufferViews.push_back(
            plsTransientScratchColorTexture()->vkImageView());
        clearValues.push_back({});

        if (pipelineLayout.renderPassOptions() &
            RenderPassOptionsVulkan::atomicCoalescedResolveAndTransfer)
        {
            assert(framebufferViews.size() == COALESCED_ATOMIC_RESOLVE_IDX);
            framebufferViews.push_back(renderTarget->accessTargetImageView(
                commandBuffer,
                {
                    .pipelineStages =
                        VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
                    .accessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                    .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
                },
                drawBounds.contains(
                    IAABB{0,
                          0,
                          static_cast<int32_t>(renderTarget->width()),
                          static_cast<int32_t>(renderTarget->height())})
                    ? vkutil::ImageAccessAction::invalidateContents
                    : vkutil::ImageAccessAction::preserveContents));
            clearValues.push_back({});
        }
    }
    else if (desc.interlockMode == gpu::InterlockMode::msaa)
    {
        assert(framebufferViews.size() == MSAA_DEPTH_STENCIL_IDX);
        framebufferViews.push_back(
            renderTarget->msaaDepthStencilTexture()->vkImageView());
        clearValues.push_back(
            {.depthStencil = {desc.depthClearValue, desc.stencilClearValue}});

        assert(framebufferViews.size() == MSAA_RESOLVE_IDX);
        framebufferViews.push_back(msaaResolveImageView);
        clearValues.push_back({});

        if (pipelineLayout.renderPassOptions() &
            RenderPassOptionsVulkan::msaaSeedFromOffscreenTexture)
        {
            assert(desc.colorLoadAction ==
                   gpu::LoadAction::preserveRenderTarget);
            assert(msaaColorSeedImageView != VK_NULL_HANDLE);
            assert(msaaColorSeedImageView != msaaResolveImageView);
            assert(framebufferViews.size() == MSAA_COLOR_SEED_IDX);
            framebufferViews.push_back(msaaColorSeedImageView);
            clearValues.push_back({});
        }
    }

    rcp<vkutil::Framebuffer> framebuffer = m_vk->makeFramebuffer({
        .renderPass = renderPass,
        .attachmentCount = framebufferViews.size(),
        .pAttachments = framebufferViews.data(),
        .width = static_cast<uint32_t>(renderTarget->width()),
        .height = static_cast<uint32_t>(renderTarget->height()),
        .layers = 1,
    });

    VkRect2D renderArea = {
        .offset = {drawBounds.left, drawBounds.top},
        .extent = {static_cast<uint32_t>(drawBounds.width()),
                   static_cast<uint32_t>(drawBounds.height())},
    };

    assert(clearValues.size() == framebufferViews.size());
    VkRenderPassBeginInfo renderPassBeginInfo = {
        .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
        .renderPass = renderPass,
        .framebuffer = *framebuffer,
        .renderArea = renderArea,
        .clearValueCount = clearValues.size(),
        .pClearValues = clearValues.data(),
    };

    m_vk->CmdBeginRenderPass(commandBuffer,
                             &renderPassBeginInfo,
                             VK_SUBPASS_CONTENTS_INLINE);

    m_vk->CmdSetViewport(
        commandBuffer,
        0,
        1,
        vkutil::ViewportFromRect2D(
            {.extent = {renderTarget->width(), renderTarget->height()}}));

    m_vk->CmdSetScissor(commandBuffer, 0, 1, &renderArea);

    return pipelineLayout;
}

void RenderContextVulkanImpl::flush(const FlushDescriptor& desc)
{
    constexpr static VkDeviceSize ZERO_OFFSET[1] = {0};
    constexpr static uint32_t ZERO_OFFSET_32[1] = {0};

    auto* const renderTarget =
        static_cast<RenderTargetVulkan*>(desc.renderTarget);

    IAABB drawBounds = desc.renderTargetUpdateBounds;
    if (drawBounds.empty())
    {
        return;
    }

    auto renderPassOptions = RenderPassOptionsVulkan::none;
    if (desc.fixedFunctionColorOutput)
    {
        // In the case of Vulkan, fixedFunctionColorOutput means the color
        // buffer will never be bound as an input attachment.
        renderPassOptions |= RenderPassOptionsVulkan::fixedFunctionColorOutput;
    }
    if (desc.manuallyResolved)
    {
        // The drawList ends with a batch of type of type
        // DrawType::renderPassResolve, and the render pass needs to be set up
        // to handle manual resolving.
        renderPassOptions |= RenderPassOptionsVulkan::manuallyResolved;
    }
    else if (desc.interlockMode == gpu::InterlockMode::msaa)
    {
        // Vulkan does not support partial MSAA resolves when using resolve
        // attachments.
        drawBounds = renderTarget->bounds();
    }
    // Vulkan builtin MSAA resolves don't support partial drawBounds.
    assert(desc.interlockMode != gpu::InterlockMode::msaa ||
           desc.manuallyResolved || drawBounds == renderTarget->bounds());

    const auto commandBuffer =
        reinterpret_cast<VkCommandBuffer>(desc.externalCommandBuffer);
    rcp<DescriptorSetPool> descriptorSetPool =
        m_descriptorSetPoolPool->acquire();

    m_featherTexture->prepareForVertexOrFragmentShaderRead(commandBuffer);
    m_nullImageTexture->prepareForFragmentShaderRead(commandBuffer);

    uint32_t pendingTessPatchCount = 0;
    for (const DrawBatch& batch : *desc.drawList)
    {
        // Apply pending image texture updates.
        if (auto imageTextureVulkan =
                static_cast<vkutil::Texture2D*>(batch.imageTexture))
        {
            imageTextureVulkan->prepareForFragmentShaderRead(commandBuffer);
        }

        // Count up the complexity in tessellation patches of this flush.
        switch (batch.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:
                pendingTessPatchCount += batch.elementCount;
                break;
            case DrawType::msaaStencilClipReset:
            case DrawType::interiorTriangulation:
            case DrawType::atlasBlit:
            case DrawType::imageRect:
            case DrawType::imageMesh:
            case DrawType::renderPassInitialize:
            case DrawType::renderPassResolve:
                break;
        }
    }
    if (desc.interlockMode == gpu::InterlockMode::rasterOrdering &&
        pendingTessPatchCount > m_workarounds.maxInstancesPerRenderPass)
    {
        // Some early Android tilers are known to crash when a render pass is
        // too complex. Make this render pass interruptible so we can break it
        // up and avoid the crash.
        assert(!(m_plsTransientUsageFlags &
                 VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT));
        // Manually resolved render passes aren't currently compatible with
        // interruptions.
        assert(!desc.manuallyResolved);
        renderPassOptions |=
            RenderPassOptionsVulkan::rasterOrderingInterruptible;
    }

    // Create a per-flush descriptor set.
    VkDescriptorSet perFlushDescriptorSet =
        descriptorSetPool->allocateDescriptorSet(
            m_pipelineManager->perFlushDescriptorSetLayout());

    m_vk->updateBufferDescriptorSets(
        perFlushDescriptorSet,
        {
            .dstBinding = FLUSH_UNIFORM_BUFFER_IDX,
            .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
        },
        {{
            .buffer = *m_flushUniformBuffer,
            .offset = desc.flushUniformDataOffsetInBytes,
            .range = sizeof(gpu::FlushUniforms),
        }});

    m_vk->updateBufferDescriptorSets(
        perFlushDescriptorSet,
        {
            .dstBinding = IMAGE_DRAW_UNIFORM_BUFFER_IDX,
            .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,
        },
        {{
            .buffer = *m_imageDrawUniformBuffer,
            .offset = 0,
            .range = sizeof(gpu::ImageDrawUniforms),
        }});

    m_vk->updateBufferDescriptorSets(
        perFlushDescriptorSet,
        {
            .dstBinding = PATH_BUFFER_IDX,
            .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
        },
        {{
            .buffer = *m_pathBuffer,
            .offset = desc.firstPath * sizeof(gpu::PathData),
            .range = VK_WHOLE_SIZE,
        }});

    m_vk->updateBufferDescriptorSets(
        perFlushDescriptorSet,
        {
            .dstBinding = PAINT_BUFFER_IDX,
            .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
        },
        {{
            .buffer = *m_paintBuffer,
            .offset = desc.firstPaint * sizeof(gpu::PaintData),
            .range = VK_WHOLE_SIZE,
        }});

    // NOTE: This technically could be part of the above call (passing two
    // buffers instead of one to set them both at once), but there is a bug on
    // some Adreno devices where the second one does not get applied properly
    // (all reads from PaintAuxBuffer end up reading 0s), so instead we'll do it
    // as a separate call.
    m_vk->updateBufferDescriptorSets(
        perFlushDescriptorSet,
        {
            .dstBinding = PAINT_AUX_BUFFER_IDX,
            .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
        },
        {{
            .buffer = *m_paintAuxBuffer,
            .offset = desc.firstPaintAux * sizeof(gpu::PaintAuxData),
            .range = VK_WHOLE_SIZE,
        }});
    static_assert(PAINT_AUX_BUFFER_IDX == PAINT_BUFFER_IDX + 1);

    m_vk->updateBufferDescriptorSets(
        perFlushDescriptorSet,
        {
            .dstBinding = CONTOUR_BUFFER_IDX,
            .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
        },
        {{
            .buffer = *m_contourBuffer,
            .offset = desc.firstContour * sizeof(gpu::ContourData),
            .range = VK_WHOLE_SIZE,
        }});

    if (desc.interlockMode == gpu::InterlockMode::clockwiseAtomic &&
        m_coverageBuffer != nullptr)
    {
        m_vk->updateBufferDescriptorSets(
            perFlushDescriptorSet,
            {
                .dstBinding = COVERAGE_BUFFER_IDX,
                .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
            },
            {{
                .buffer = *m_coverageBuffer,
                .offset = 0,
                .range = VK_WHOLE_SIZE,
            }});
    }

    m_vk->updateImageDescriptorSets(
        perFlushDescriptorSet,
        {
            .dstBinding = TESS_VERTEX_TEXTURE_IDX,
            .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
        },
        {{
            .imageView = m_tessTexture->vkImageView(),
            .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
        }});

    m_vk->updateImageDescriptorSets(
        perFlushDescriptorSet,
        {
            .dstBinding = GRAD_TEXTURE_IDX,
            .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
        },
        {{
            .imageView = m_gradTexture->vkImageView(),
            .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
        }});

    m_vk->updateImageDescriptorSets(
        perFlushDescriptorSet,
        {
            .dstBinding = FEATHER_TEXTURE_IDX,
            .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
        },
        {{
            .imageView = m_featherTexture->vkImageView(),
            .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
        }});

    m_vk->updateImageDescriptorSets(
        perFlushDescriptorSet,
        {
            .dstBinding = ATLAS_TEXTURE_IDX,
            .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
        },
        {{
            .imageView = m_atlasTexture->vkImageView(),
            .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
        }});

    // Render the complex color ramps to the gradient texture.
    if (desc.gradSpanCount > 0)
    {
        VkRect2D renderArea = {
            .extent = {gpu::kGradTextureWidth, desc.gradDataHeight},
        };

        m_colorRampPipeline->beginRenderPass(commandBuffer,
                                             renderArea,
                                             *m_gradTextureFramebuffer);

        m_vk->CmdSetViewport(commandBuffer,
                             0,
                             1,
                             vkutil::ViewportFromRect2D(renderArea));

        m_vk->CmdSetScissor(commandBuffer, 0, 1, &renderArea);

        VkBuffer gradSpanBuffer = *m_gradSpanBuffer;
        VkDeviceSize gradSpanOffset =
            desc.firstGradSpan * sizeof(gpu::GradientSpan);
        m_vk->CmdBindVertexBuffers(commandBuffer,
                                   0,
                                   1,
                                   &gradSpanBuffer,
                                   &gradSpanOffset);

        m_vk->CmdBindDescriptorSets(commandBuffer,
                                    VK_PIPELINE_BIND_POINT_GRAPHICS,
                                    m_colorRampPipeline->pipelineLayout(),
                                    PER_FLUSH_BINDINGS_SET,
                                    1,
                                    &perFlushDescriptorSet,
                                    1,
                                    ZERO_OFFSET_32);

        m_vk->CmdBindPipeline(commandBuffer,
                              VK_PIPELINE_BIND_POINT_GRAPHICS,
                              m_colorRampPipeline->renderPipeline());

        for (auto [chunkInstanceCount, chunkFirstInstance] :
             InstanceChunker(desc.gradSpanCount,
                             0,
                             m_workarounds.maxInstancesPerRenderPass))

        {
            m_colorRampPipeline->interruptRenderPassIfNeeded(
                commandBuffer,
                renderArea,
                *m_gradTextureFramebuffer,
                chunkInstanceCount,
                m_workarounds);
            m_vk->CmdDraw(commandBuffer,
                          gpu::GRAD_SPAN_TRI_STRIP_VERTEX_COUNT,
                          chunkInstanceCount,
                          0,
                          chunkFirstInstance);
        }

        m_vk->CmdEndRenderPass(commandBuffer);

        // The render pass transitioned the gradient texture to
        // VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL.
        m_gradTexture->lastAccess().layout =
            VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    }
    else
    {
        // The above render pass has the barriers in it, but if it was not run,
        // we still are going to bind it as READ_ONLY_OPTIMAL so need to
        // transition it
        // TODO: Perhaps we should have a "null" texture that we can bind for
        // cases like this where we need to bind all the textures but know it's
        // not needed
        m_gradTexture->barrier(
            commandBuffer,
            {
                .pipelineStages = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
                .accessMask = VK_ACCESS_SHADER_READ_BIT,
                .layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
            });
    }
    VkDescriptorSet immutableSamplerDescriptorSet =
        m_pipelineManager->immutableSamplerDescriptorSet();

    // Tessellate all curves into vertices in the tessellation texture.
    if (desc.tessVertexSpanCount > 0)
    {
        // Don't render new vertices until the previous flush has finished using
        // the tessellation texture.
        // TODO: What this barrier does is also part of the tessellation
        // renderpass, and should be handled automatically, but on early PowerVR
        // devices (Reno 3 Plus, Vivo Y21) tesselation is still incorrect
        // without this explicit barrier. Figure out why.
        m_tessTexture->barrier(
            commandBuffer,
            {
                .pipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
                .accessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
            },
            vkutil::ImageAccessAction::invalidateContents);

        const VkRect2D tessellateArea = {
            .extent = {gpu::kTessTextureWidth, desc.tessDataHeight},
        };

        m_tessellatePipeline->beginRenderPass(commandBuffer,
                                              tessellateArea,
                                              *m_tessTextureFramebuffer);

        m_vk->CmdSetViewport(commandBuffer,
                             0,
                             1,
                             vkutil::ViewportFromRect2D(tessellateArea));

        m_vk->CmdSetScissor(commandBuffer, 0, 1, &tessellateArea);

        VkBuffer tessBuffer = *m_tessSpanBuffer;
        VkDeviceSize tessOffset =
            desc.firstTessVertexSpan * sizeof(gpu::TessVertexSpan);
        m_vk->CmdBindVertexBuffers(commandBuffer,
                                   0,
                                   1,
                                   &tessBuffer,
                                   &tessOffset);

        m_vk->CmdBindIndexBuffer(commandBuffer,
                                 *m_tessSpanIndexBuffer,
                                 0,
                                 VK_INDEX_TYPE_UINT16);

        m_vk->CmdBindDescriptorSets(commandBuffer,
                                    VK_PIPELINE_BIND_POINT_GRAPHICS,
                                    m_tessellatePipeline->pipelineLayout(),
                                    PER_FLUSH_BINDINGS_SET,
                                    1,
                                    &perFlushDescriptorSet,
                                    1,
                                    ZERO_OFFSET_32);
        m_vk->CmdBindDescriptorSets(commandBuffer,
                                    VK_PIPELINE_BIND_POINT_GRAPHICS,
                                    m_tessellatePipeline->pipelineLayout(),
                                    IMMUTABLE_SAMPLER_BINDINGS_SET,
                                    1,
                                    &immutableSamplerDescriptorSet,
                                    0,
                                    nullptr);

        m_vk->CmdBindPipeline(commandBuffer,
                              VK_PIPELINE_BIND_POINT_GRAPHICS,
                              m_tessellatePipeline->renderPipeline());

        for (auto [chunkInstanceCount, chunkFirstInstance] :
             InstanceChunker(desc.tessVertexSpanCount,
                             0,
                             m_workarounds.maxInstancesPerRenderPass))

        {
            m_tessellatePipeline->interruptRenderPassIfNeeded(
                commandBuffer,
                tessellateArea,
                *m_tessTextureFramebuffer,
                chunkInstanceCount,
                m_workarounds);
            m_vk->CmdDrawIndexed(commandBuffer,
                                 std::size(gpu::kTessSpanIndices),
                                 chunkInstanceCount,
                                 0,
                                 0,
                                 chunkFirstInstance);
        }

        m_vk->CmdEndRenderPass(commandBuffer);

        // The render pass transitioned the tessellation texture to
        // VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL.
        m_tessTexture->lastAccess().layout =
            VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

        if (m_tesselationSyncIssueWorkaroundTexture != nullptr)
        {
            // On the Adreno 8xx series drivers we've encountered, there is some
            // sort of synchronization issue with the tesselation texture that
            // causes the barriers to not work, and it ends up being corrupted.
            // However, if we first just blit it to an offscreen texture (just a
            // 1x1 texture), the render corruption goes away.
            m_tessTexture->barrier(
                commandBuffer,
                {
                    .pipelineStages = VK_PIPELINE_STAGE_TRANSFER_BIT,
                    .accessMask = VK_ACCESS_TRANSFER_READ_BIT,
                    .layout = VK_IMAGE_LAYOUT_GENERAL,
                });

            // We need this transition but really only one time (if we haven't
            // done so already)
            if (m_tesselationSyncIssueWorkaroundTexture->lastAccess().layout !=
                VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
            {
                m_tesselationSyncIssueWorkaroundTexture->barrier(
                    commandBuffer,
                    {
                        .pipelineStages = VK_PIPELINE_STAGE_TRANSFER_BIT,
                        .accessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
                        .layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                    },
                    vkutil::ImageAccessAction::invalidateContents);
            }

            m_vk->blitSubRect(
                commandBuffer,
                m_tessTexture->vkImage(),
                VK_IMAGE_LAYOUT_GENERAL,
                m_tesselationSyncIssueWorkaroundTexture->vkImage(),
                VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                IAABB::MakeWH(
                    m_tesselationSyncIssueWorkaroundTexture->width(),
                    m_tesselationSyncIssueWorkaroundTexture->height()));

            // NOTE: Technically there should be a barrier after this blit to
            // prevent a write-after-write hazard. However, we don't use this
            // texture at all and thus don't care if we overwrite it, so there
            // is intentionally no barrier here...but you will get a failure on
            // this texture if you enable synchronization validation on a device
            // with the workaround enabled.
        }
    }

    // Ensure the tessellation texture has finished rendering before the path
    // vertex shaders read it.
    // TODO: Similar to the barrier on the way into rendering the tesselation
    // texture, this barrier should already be covered by the tesselation
    // renderpass but fails on early PowerVR devices.
    m_tessTexture->barrier(
        commandBuffer,
        {
            .pipelineStages = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,
            .accessMask = VK_ACCESS_SHADER_READ_BIT,
            .layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
        });

    // Render the atlas if we have any offscreen feathers.
    if ((desc.atlasFillBatchCount | desc.atlasStrokeBatchCount) != 0)
    {
        VkRect2D renderArea = {
            .extent = {desc.atlasContentWidth, desc.atlasContentHeight},
        };

        // Begin the render pass before binding buffers or updating descriptor
        // sets. It's valid Vulkan to do these tasks in any order, but Adreno
        // 730, 740, and 840 appreciate it when we begin the render pass first.
        m_atlasPipeline->beginRenderPass(commandBuffer,
                                         renderArea,
                                         *m_atlasFramebuffer);

        m_vk->CmdSetViewport(commandBuffer,
                             0,
                             1,
                             vkutil::ViewportFromRect2D(renderArea));

        m_vk->CmdBindVertexBuffers(commandBuffer,
                                   0,
                                   1,
                                   m_pathPatchVertexBuffer->vkBufferAddressOf(),
                                   ZERO_OFFSET);
        m_vk->CmdBindIndexBuffer(commandBuffer,
                                 *m_pathPatchIndexBuffer,
                                 0,
                                 VK_INDEX_TYPE_UINT16);
        m_vk->CmdBindDescriptorSets(commandBuffer,
                                    VK_PIPELINE_BIND_POINT_GRAPHICS,
                                    m_atlasPipeline->pipelineLayout(),
                                    PER_FLUSH_BINDINGS_SET,
                                    1,
                                    &perFlushDescriptorSet,
                                    1,
                                    ZERO_OFFSET_32);
        m_vk->CmdBindDescriptorSets(commandBuffer,
                                    VK_PIPELINE_BIND_POINT_GRAPHICS,
                                    m_atlasPipeline->pipelineLayout(),
                                    IMMUTABLE_SAMPLER_BINDINGS_SET,
                                    1,
                                    &immutableSamplerDescriptorSet,
                                    0,
                                    nullptr);

        if (desc.atlasFillBatchCount != 0)
        {
            m_vk->CmdBindPipeline(commandBuffer,
                                  VK_PIPELINE_BIND_POINT_GRAPHICS,
                                  m_atlasPipeline->fillPipeline());
            for (size_t i = 0; i < desc.atlasFillBatchCount; ++i)
            {
                const gpu::AtlasDrawBatch& fillBatch = desc.atlasFillBatches[i];
                VkRect2D scissor = {
                    .offset = {fillBatch.scissor.left, fillBatch.scissor.top},
                    .extent = {fillBatch.scissor.width(),
                               fillBatch.scissor.height()},
                };
                m_vk->CmdSetScissor(commandBuffer, 0, 1, &scissor);
                for (auto [chunkPatchCount, chunkFirstPatch] :
                     InstanceChunker(fillBatch.patchCount,
                                     fillBatch.basePatch,
                                     m_workarounds.maxInstancesPerRenderPass))

                {
                    m_atlasPipeline->interruptRenderPassIfNeeded(
                        commandBuffer,
                        renderArea,
                        *m_atlasFramebuffer,
                        chunkPatchCount,
                        m_workarounds);
                    m_vk->CmdDrawIndexed(
                        commandBuffer,
                        gpu::kMidpointFanCenterAAPatchIndexCount,
                        chunkPatchCount,
                        gpu::kMidpointFanCenterAAPatchBaseIndex,
                        0,
                        chunkFirstPatch);
                }
            }
        }

        if (desc.atlasStrokeBatchCount != 0)
        {
            m_vk->CmdBindPipeline(commandBuffer,
                                  VK_PIPELINE_BIND_POINT_GRAPHICS,
                                  m_atlasPipeline->strokePipeline());
            for (size_t i = 0; i < desc.atlasStrokeBatchCount; ++i)
            {
                const gpu::AtlasDrawBatch& strokeBatch =
                    desc.atlasStrokeBatches[i];
                VkRect2D scissor = {
                    .offset = {strokeBatch.scissor.left,
                               strokeBatch.scissor.top},
                    .extent = {strokeBatch.scissor.width(),
                               strokeBatch.scissor.height()},
                };
                m_vk->CmdSetScissor(commandBuffer, 0, 1, &scissor);
                for (auto [chunkPatchCount, chunkFirstPatch] :
                     InstanceChunker(strokeBatch.patchCount,
                                     strokeBatch.basePatch,
                                     m_workarounds.maxInstancesPerRenderPass))

                {
                    m_atlasPipeline->interruptRenderPassIfNeeded(
                        commandBuffer,
                        renderArea,
                        *m_atlasFramebuffer,
                        chunkPatchCount,
                        m_workarounds);
                    m_vk->CmdDrawIndexed(commandBuffer,
                                         gpu::kMidpointFanPatchBorderIndexCount,
                                         chunkPatchCount,
                                         gpu::kMidpointFanPatchBaseIndex,
                                         0,
                                         chunkFirstPatch);
                }
            }
        }

        m_vk->CmdEndRenderPass(commandBuffer);

        // The render pass transitioned the atlas texture to
        // VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL.
        m_atlasTexture->lastAccess().layout =
            VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    }

    // Ensures any previous accesses to a color attachment complete before we
    // begin rendering.
    const vkutil::ImageAccess colorLoadAccess = {
        // "Load" operations always occur in
        // VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT.
        .pipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        .accessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
        .layout = (renderPassOptions &
                   RenderPassOptionsVulkan::fixedFunctionColorOutput)
                      ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
                      : VK_IMAGE_LAYOUT_GENERAL,
    };

    const bool renderAreaIsFullTarget = drawBounds.contains(
        IAABB{0,
              0,
              static_cast<int32_t>(renderTarget->width()),
              static_cast<int32_t>(renderTarget->height())});

    const vkutil::ImageAccessAction targetAccessAction =
        renderAreaIsFullTarget &&
                desc.colorLoadAction != gpu::LoadAction::preserveRenderTarget
            ? vkutil::ImageAccessAction::invalidateContents
            : vkutil::ImageAccessAction::preserveContents;

    const PLSBackingType plsBackingType =
        m_pipelineManager->plsBackingType(desc.interlockMode);

    VkImageView colorImageView = VK_NULL_HANDLE;
    bool colorAttachmentIsOffscreen = false;

    VkImageView msaaResolveImageView = VK_NULL_HANDLE;
    VkImageView msaaColorSeedImageView = VK_NULL_HANDLE;

    if (desc.interlockMode == gpu::InterlockMode::msaa)
    {
        colorImageView = renderTarget->msaaColorTexture()->vkImageView();

#if 0
        // TODO: Some early Qualcomm devices struggle when seeding from and
        // resolving to the same texture, even if we implement the MSAA resolve
        // manually. For now, always copy out the render target to a separate
        // texture when there's a preserve, but we should investigate this
        // further.
        if (desc.colorLoadAction == gpu::LoadAction::preserveRenderTarget &&
            (renderTarget->targetUsageFlags() &
             VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT))
        {
            // We can seed from, and resolve to the the same texture.
            msaaColorSeedImageView = msaaResolveImageView =
                renderTarget->accessTargetImageView(
                    commandBuffer,
                    {
                        // Apply a barrier for reading from this texture as an
                        // input attachment.
                        // vkCmdNextSubpass() will handle the barrier between
                        // reading this texture and resolving to it.
                        .pipelineStages = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
                        .accessMask = VK_ACCESS_INPUT_ATTACHMENT_READ_BIT,
                        .layout = VK_IMAGE_LAYOUT_GENERAL,
                    });
        }
        else
#endif
        {
            if (desc.colorLoadAction == gpu::LoadAction::preserveRenderTarget)
            {
                // We have to seed the MSAA attachment from a separate texture
                // because the render target doesn't support being bound as an
                // input attachment.
                msaaColorSeedImageView =
                    renderTarget
                        ->copyTargetImageToOffscreenColorTexture(
                            commandBuffer,
                            {
                                .pipelineStages =
                                    VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
                                .accessMask =
                                    VK_ACCESS_INPUT_ATTACHMENT_READ_BIT,
                                .layout = VK_IMAGE_LAYOUT_GENERAL,
                            },
                            drawBounds)
                        ->vkImageView();
                renderPassOptions |=
                    RenderPassOptionsVulkan::msaaSeedFromOffscreenTexture;
            }
            msaaResolveImageView = renderTarget->accessTargetImageView(
                commandBuffer,
                {
                    .pipelineStages =
                        VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
                    .accessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                    .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
                },
                renderAreaIsFullTarget
                    ? vkutil::ImageAccessAction::invalidateContents
                    : vkutil::ImageAccessAction::preserveContents);
        }
    }
    else if ((renderPassOptions &
              RenderPassOptionsVulkan::fixedFunctionColorOutput) ||
             ((desc.interlockMode == gpu::InterlockMode::rasterOrdering ||
               desc.interlockMode == gpu::InterlockMode::atomics) &&
              (renderTarget->targetUsageFlags() &
               VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT)))
    {
        // We can render directly to the render target.
        colorImageView =
            renderTarget->accessTargetImageView(commandBuffer,
                                                colorLoadAccess,
                                                targetAccessAction);
    }
    else if (plsBackingType == PLSBackingType::storageTexture)
    {
        constexpr static vkutil::ImageAccess PLS_STORAGE_TEXTURE_ACCESS = {
            .pipelineStages = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
            .accessMask =
                VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
            .layout = VK_IMAGE_LAYOUT_GENERAL,
        };

        if ((renderTarget->targetUsageFlags() & VK_IMAGE_USAGE_STORAGE_BIT) &&
            renderTarget->framebufferFormat() == VK_FORMAT_R8G8B8A8_UNORM)
        {
            // We can bind the renderTarget as a storage texture directly to the
            // color plane_.
            if (desc.colorLoadAction == gpu::LoadAction::clear)
            {
                colorImageView = renderTarget->clearTargetImageView(
                    commandBuffer,
                    desc.colorClearValue,
                    PLS_STORAGE_TEXTURE_ACCESS);
            }
            else
            {
                colorImageView = renderTarget->accessTargetImageView(
                    commandBuffer,
                    PLS_STORAGE_TEXTURE_ACCESS,
                    targetAccessAction);
            }
        }
        else
        {
            // We have to bind a separate texture to the color plane.
            switch (desc.colorLoadAction)
            {
                case gpu::LoadAction::clear:
                    colorImageView = clearPLSOffscreenColorTexture(
                                         commandBuffer,
                                         desc.colorClearValue,
                                         PLS_STORAGE_TEXTURE_ACCESS)
                                         ->vkImageView();
                    break;
                case gpu::LoadAction::preserveRenderTarget:
                    // Preserve the target texture by copying its contents into
                    // our offscreen color texture.
                    colorImageView = copyRenderTargetToPLSOffscreenColorTexture(
                                         commandBuffer,
                                         renderTarget,
                                         drawBounds,
                                         PLS_STORAGE_TEXTURE_ACCESS)
                                         ->vkImageView();
                    break;
                case gpu::LoadAction::dontCare:
                    colorImageView =
                        accessPLSOffscreenColorTexture(
                            commandBuffer,
                            PLS_STORAGE_TEXTURE_ACCESS,
                            vkutil::ImageAccessAction::invalidateContents)
                            ->vkImageView();
                    break;
            }
            colorAttachmentIsOffscreen = true;
        }
    }
    else
    {
        // The renderTarget doesn't support input attachments, so we have to
        // attach a separate texture to the framebuffer for color.
        if (desc.colorLoadAction == gpu::LoadAction::preserveRenderTarget)
        {
            // Preserve the target texture by copying its contents into our
            // offscreen color texture.
            colorImageView =
                renderTarget
                    ->copyTargetImageToOffscreenColorTexture(commandBuffer,
                                                             colorLoadAccess,
                                                             drawBounds)
                    ->vkImageView();
        }
        else
        {
            colorImageView =
                renderTarget
                    ->accessOffscreenColorTexture(
                        commandBuffer,
                        colorLoadAccess,
                        vkutil::ImageAccessAction::invalidateContents)
                    ->vkImageView();
        }
        if (desc.interlockMode == gpu::InterlockMode::atomics)
        {
            renderPassOptions |=
                RenderPassOptionsVulkan::atomicCoalescedResolveAndTransfer;
        }
        colorAttachmentIsOffscreen = true;
    }

    const bool usesStorageTextures =
        plsBackingType == PLSBackingType::storageTexture ||
        desc.interlockMode == gpu::InterlockMode::atomics;
    if (usesStorageTextures)
    {
        // Clear the PLS planes that are bound as storage textures.
        const VkImage storageImageToClear =
            (desc.interlockMode == gpu::InterlockMode::atomics)
                ? m_plsAtomicCoverageTexture->vkImage()
                : *plsTransientImageArray();

        VkPipelineStageFlags srcStages = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
        VkPipelineStageFlags dstStages = VK_PIPELINE_STAGE_TRANSFER_BIT;
        StackVector<VkImageMemoryBarrier, 2> barriers;

        // Don't clear the storageImageToClear until shaders in previous flushes
        // have finished using it.
        // NOTE: This currently only works becauses we never support PLS as
        // storage texture (i.e., clockwise) and rasterOrdering at the same
        // time. If that changes, we will need to consider that the
        // plsTransientImageArray may have been bound previously as input
        // attachments.
        assert(plsBackingType == PLSBackingType::inputAttachment ||
               !m_platformFeatures.supportsRasterOrderingMode);
        barriers.push_back({
            .srcAccessMask =
                VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
            .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
            .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
            .newLayout = VK_IMAGE_LAYOUT_GENERAL,
            .image = storageImageToClear,
        });

        // The scratch color texture may also need a barrier.
        // NOTE: atomic mode uses the scratch color texture for clipping because
        // of its RGBA format.
        const bool usesScratchColorTexture =
            desc.interlockMode == gpu::InterlockMode::atomics ||
            !(renderPassOptions &
              RenderPassOptionsVulkan::fixedFunctionColorOutput);
        if (usesScratchColorTexture)
        {
            // Don't use the scratch color texture until shaders in previous
            // render passes have finished using it.
            // NOTE: The scratch texture may have been bound as an input
            // attachment OR a storage texture.
            const VkAccessFlags storageTextureAccess =
                m_platformFeatures.supportsClockwiseMode
                    ? VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT
                    : 0;
            srcStages |= VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
            dstStages |= VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
            barriers.push_back({
                .srcAccessMask =
                    VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | storageTextureAccess,
                .dstAccessMask =
                    VK_ACCESS_INPUT_ATTACHMENT_READ_BIT | storageTextureAccess,
                .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
                .newLayout = VK_IMAGE_LAYOUT_GENERAL,
                .image = plsTransientScratchColorTexture()->vkImage(),
            });
        }

        m_vk->imageMemoryBarriers(commandBuffer,
                                  srcStages,
                                  dstStages,
                                  0,
                                  barriers.size(),
                                  barriers.data());

        // Clear the entire storageImageToClear, even if we aren't going to use
        // the whole thing. There is a future world where we may want to
        // consider a "renderPassInitialize" draw to clear only the
        // renderTargetUpdateBounds, but in most cases, a full clear will almost
        // definitely be faster due to hardware optimizations.
        if (desc.interlockMode == gpu::InterlockMode::atomics)
        {
            const VkClearColorValue coverageClearValue =
                vkutil::color_clear_r32ui(desc.coverageClearValue);

            const VkImageSubresourceRange clearRange = {
                .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
                .levelCount = 1,
                .layerCount = 1,
            };

            m_vk->CmdClearColorImage(commandBuffer,
                                     m_plsAtomicCoverageTexture->vkImage(),
                                     VK_IMAGE_LAYOUT_GENERAL,
                                     &coverageClearValue,
                                     1,
                                     &clearRange);
        }
        else
        {
            assert(desc.coverageClearValue == 0);
            const VkClearColorValue zeroClearValue = {};

            const VkImageSubresourceRange transientClearRange = {
                .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
                .levelCount = 1,
                .baseArrayLayer = 0,
                .layerCount = (desc.combinedShaderFeatures &
                               gpu::ShaderFeatures::ENABLE_CLIPPING)
                                  ? 2u  // coverage and clip.
                                  : 1u, // coverage only.
            };

            m_vk->CmdClearColorImage(commandBuffer,
                                     *plsTransientImageArray(),
                                     VK_IMAGE_LAYOUT_GENERAL,
                                     &zeroClearValue,
                                     1,
                                     &transientClearRange);
        }

        // Don't use the storageImageToClear in shaders until the clear
        // finishes.
        m_vk->imageMemoryBarrier(
            commandBuffer,
            VK_PIPELINE_STAGE_TRANSFER_BIT,
            VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
            0,
            {
                .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
                .dstAccessMask =
                    VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
                .oldLayout = VK_IMAGE_LAYOUT_GENERAL,
                .newLayout = VK_IMAGE_LAYOUT_GENERAL,
                .image = storageImageToClear,
            });
    }
    else
    {
        // Any color writes in prior frames should already be complete before
        // the following load operations. NOTE: This currently only works
        // because we never support PLS as storage texture (i.e. clockwise) and
        // rasterOrdering at the same time. If that changes, we will need to
        // consider that the plsTransientImageArray may have been bound
        // previously as storage textures.
        assert(desc.interlockMode != gpu::InterlockMode::rasterOrdering ||
               !m_platformFeatures.supportsClockwiseMode);
    }

    if (desc.interlockMode == gpu::InterlockMode::clockwiseAtomic)
    {
        VkPipelineStageFlags lastCoverageBufferStage =
            VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
        VkAccessFlags lastCoverageBufferAccess = VK_ACCESS_SHADER_WRITE_BIT;

        if (desc.needsCoverageBufferClear)
        {
            assert(m_coverageBuffer != nullptr);

            // Don't clear the coverage buffer until shaders in the previous
            // flush have finished accessing it.
            m_vk->bufferMemoryBarrier(
                commandBuffer,
                lastCoverageBufferStage,
                VK_PIPELINE_STAGE_TRANSFER_BIT,
                0,
                {
                    .srcAccessMask = lastCoverageBufferAccess,
                    .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
                    .buffer = *m_coverageBuffer,
                });

            m_vk->CmdFillBuffer(commandBuffer,
                                *m_coverageBuffer,
                                0,
                                m_coverageBuffer->info().size,
                                0);

            lastCoverageBufferStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
            lastCoverageBufferAccess = VK_ACCESS_TRANSFER_WRITE_BIT;
        }

        if (m_coverageBuffer != nullptr)
        {
            // Don't use the coverage buffer until prior clears/accesses
            // have completed.
            m_vk->bufferMemoryBarrier(
                commandBuffer,
                lastCoverageBufferStage,
                VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
                0,
                {
                    .srcAccessMask = lastCoverageBufferAccess,
                    .dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
                    .buffer = *m_coverageBuffer,
                });
        }
    }

    // Begin the render pass before binding buffers or updating descriptor sets.
    // It's valid Vulkan to do these tasks in any order, but Adreno 730, 740,
    // and 840 appreciate it when we begin the render pass first.
    const DrawPipelineLayoutVulkan& pipelineLayout =
        beginDrawRenderPass(desc,
                            renderPassOptions,
                            drawBounds,
                            colorImageView,
                            msaaColorSeedImageView,
                            msaaResolveImageView);

    // Some early Android tilers are known to crash when a render pass is too
    // complex. This is a mechanism to interrupt and begin a new render pass on
    // affected devices after a pre-set complexity is reached.
    uint32_t patchCountInCurrentDrawPass = 0;
    auto interruptDrawPassIfNeeded = [&](uint32_t nextTessPatchCount) {
        assert(nextTessPatchCount <= m_workarounds.maxInstancesPerRenderPass);
        if (desc.interlockMode == gpu::InterlockMode::rasterOrdering &&
            patchCountInCurrentDrawPass + nextTessPatchCount >
                m_workarounds.maxInstancesPerRenderPass)
        {
            assert(renderPassOptions &
                   RenderPassOptionsVulkan::rasterOrderingInterruptible);
            // Manually resolved render passes aren't currently compatible with
            // interruptions.
            assert(!(renderPassOptions &
                     RenderPassOptionsVulkan::manuallyResolved));
            m_vk->CmdEndRenderPass(commandBuffer);

            auto resumingDesc = desc;
            resumingDesc.colorLoadAction =
                gpu::LoadAction::preserveRenderTarget;
            renderPassOptions |= RenderPassOptionsVulkan::rasterOrderingResume;
            if (pendingTessPatchCount <=
                m_workarounds.maxInstancesPerRenderPass)
            {
                renderPassOptions &=
                    ~RenderPassOptionsVulkan::rasterOrderingInterruptible;
            }
            const DrawPipelineLayoutVulkan& resumingLayout RIVE_MAYBE_UNUSED =
                beginDrawRenderPass(resumingDesc,
                                    renderPassOptions,
                                    drawBounds,
                                    colorImageView,
                                    msaaColorSeedImageView,
                                    msaaResolveImageView);
            // We don't need to bind new pipelines, even though we changed the
            // render pass, because Vulkan allows for pipelines to be used
            // interchangeably with "compatible" render passes.

            // The renderPassOptions dealing with interrupting a render pass
            // don't affect the layout. We count on the new render pass having
            // the same VkPipelineLayout so we don't have to update any
            // descriptor sets after the interruption.
            assert(&resumingLayout == &pipelineLayout);

            patchCountInCurrentDrawPass = 0;
        }
        patchCountInCurrentDrawPass += nextTessPatchCount;
        pendingTessPatchCount -= nextTessPatchCount;
    };

    // Update the PLS input attachment descriptor sets.
    VkDescriptorSet inputAttachmentDescriptorSet = VK_NULL_HANDLE;
    if (pipelineLayout.plsLayout() != VK_NULL_HANDLE)
    {
        const VkDescriptorType plsDescriptorType =
            (plsBackingType ==
             PipelineManagerVulkan::PLSBackingType::storageTexture)
                ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE
                : VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT;
        inputAttachmentDescriptorSet = descriptorSetPool->allocateDescriptorSet(
            pipelineLayout.plsLayout());

        if (!(renderPassOptions &
              RenderPassOptionsVulkan::fixedFunctionColorOutput))
        {
            m_vk->updateImageDescriptorSets(
                inputAttachmentDescriptorSet,
                {
                    .dstBinding = COLOR_PLANE_IDX,
                    .descriptorType = plsDescriptorType,
                },
                {{
                    .imageView = colorImageView,
                    .imageLayout = VK_IMAGE_LAYOUT_GENERAL,
                }});
        }

        if (desc.interlockMode == gpu::InterlockMode::rasterOrdering ||
            desc.interlockMode == gpu::InterlockMode::atomics ||
            desc.interlockMode == gpu::InterlockMode::clockwise)
        {
            m_vk->updateImageDescriptorSets(
                inputAttachmentDescriptorSet,
                {
                    .dstBinding = CLIP_PLANE_IDX,
                    .descriptorType = plsDescriptorType,
                },
                {{
                    .imageView =
                        (desc.interlockMode == gpu::InterlockMode::atomics)
                            ? plsTransientScratchColorTexture()->vkImageView()
                            : *plsTransientClipView(),
                    .imageLayout = VK_IMAGE_LAYOUT_GENERAL,
                }});

            if (desc.interlockMode == gpu::InterlockMode::rasterOrdering ||
                (desc.interlockMode == gpu::InterlockMode::clockwise &&
                 !(renderPassOptions &
                   RenderPassOptionsVulkan::fixedFunctionColorOutput)))
            {
                m_vk->updateImageDescriptorSets(
                    inputAttachmentDescriptorSet,
                    {
                        .dstBinding = SCRATCH_COLOR_PLANE_IDX,
                        .descriptorType = plsDescriptorType,
                    },
                    {{
                        .imageView =
                            plsTransientScratchColorTexture()->vkImageView(),
                        .imageLayout = VK_IMAGE_LAYOUT_GENERAL,
                    }});
            }

            m_vk->updateImageDescriptorSets(
                inputAttachmentDescriptorSet,
                {
                    .dstBinding = COVERAGE_PLANE_IDX,
                    .descriptorType =
                        (desc.interlockMode == gpu::InterlockMode::atomics)
                            ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE
                            : plsDescriptorType,
                },
                {{
                    .imageView =
                        (desc.interlockMode == gpu::InterlockMode::atomics)
                            ? m_plsAtomicCoverageTexture->vkImageView()
                            : *plsTransientCoverageView(),
                    .imageLayout = VK_IMAGE_LAYOUT_GENERAL,
                }});
        }

        if (msaaColorSeedImageView != VK_NULL_HANDLE)
        {
            assert(desc.interlockMode == gpu::InterlockMode::msaa &&
                   desc.colorLoadAction ==
                       gpu::LoadAction::preserveRenderTarget);
            m_vk->updateImageDescriptorSets(
                inputAttachmentDescriptorSet,
                {
                    .dstBinding = MSAA_COLOR_SEED_IDX,
                    .descriptorType = VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT,
                },
                {{
                    .imageView = msaaColorSeedImageView,
                    .imageLayout = VK_IMAGE_LAYOUT_GENERAL,
                }});
        }
    }

    // Bind the descriptor sets for this draw pass.
    // (The imageTexture and imageDraw dynamic uniform offsets might have to
    // update between draws, but this is otherwise all we need to bind!)
    VkDescriptorSet drawDescriptorSets[] = {
        perFlushDescriptorSet,
        m_pipelineManager->nullImageDescriptorSet(),
        m_pipelineManager->immutableSamplerDescriptorSet(),
        inputAttachmentDescriptorSet,
    };
    static_assert(PER_FLUSH_BINDINGS_SET == 0);
    static_assert(PER_DRAW_BINDINGS_SET == 1);
    static_assert(IMMUTABLE_SAMPLER_BINDINGS_SET == 2);
    static_assert(PLS_TEXTURE_BINDINGS_SET == 3);
    static_assert(BINDINGS_SET_COUNT == 4);

    m_vk->CmdBindDescriptorSets(commandBuffer,
                                VK_PIPELINE_BIND_POINT_GRAPHICS,
                                *pipelineLayout,
                                PER_FLUSH_BINDINGS_SET,
                                pipelineLayout.plsLayout() != VK_NULL_HANDLE
                                    ? BINDINGS_SET_COUNT
                                    : BINDINGS_SET_COUNT - 1,
                                drawDescriptorSets,
                                1,
                                ZERO_OFFSET_32);

    // Execute the DrawList.
    uint32_t imageTextureUpdateCount = 0;
    for (const DrawBatch& batch : *desc.drawList)
    {
        assert(batch.elementCount > 0);
        const DrawType drawType = batch.drawType;

        if (batch.imageTexture != nullptr)
        {
            // Update the imageTexture binding and the dynamic offset into the
            // imageDraw uniform buffer.
            auto imageTexture =
                static_cast<vkutil::Texture2D*>(batch.imageTexture);
            VkDescriptorSet imageDescriptorSet =
                imageTexture->getCachedDescriptorSet(m_vk->currentFrameNumber(),
                                                     batch.imageSampler);
            if (imageDescriptorSet == VK_NULL_HANDLE)
            {
                // Update the image's "texture binding" descriptor set. (These
                // expire every frame, so we need to make a new one each frame.)
                if (imageTextureUpdateCount >=
                    descriptor_pool_limits::kMaxImageTextureUpdates)
                {
                    // We ran out of room for image texture updates. Allocate a
                    // new pool.
                    m_descriptorSetPoolPool->recycle(
                        std::move(descriptorSetPool));
                    descriptorSetPool = m_descriptorSetPoolPool->acquire();
                    imageTextureUpdateCount = 0;
                }

                imageDescriptorSet = descriptorSetPool->allocateDescriptorSet(
                    m_pipelineManager->perDrawDescriptorSetLayout());

                m_vk->updateImageDescriptorSets(
                    imageDescriptorSet,
                    {
                        .dstBinding = IMAGE_TEXTURE_IDX,
                        .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
                    },
                    {{
                        .imageView = imageTexture->vkImageView(),
                        .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
                    }});

                m_vk->updateImageDescriptorSets(
                    imageDescriptorSet,
                    {
                        .dstBinding = IMAGE_SAMPLER_IDX,
                        .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER,
                    },
                    {{
                        .sampler = m_pipelineManager->imageSampler(
                            batch.imageSampler.asKey()),
                    }});

                ++imageTextureUpdateCount;
                imageTexture->updateCachedDescriptorSet(
                    imageDescriptorSet,
                    m_vk->currentFrameNumber(),
                    batch.imageSampler);
            }

            VkDescriptorSet imageDescriptorSets[] = {
                perFlushDescriptorSet, // Dynamic offset to imageDraw uniforms.
                imageDescriptorSet,    // imageTexture.
            };
            static_assert(PER_DRAW_BINDINGS_SET == PER_FLUSH_BINDINGS_SET + 1);

            m_vk->CmdBindDescriptorSets(commandBuffer,
                                        VK_PIPELINE_BIND_POINT_GRAPHICS,
                                        *pipelineLayout,
                                        PER_FLUSH_BINDINGS_SET,
                                        std::size(imageDescriptorSets),
                                        imageDescriptorSets,
                                        1,
                                        &batch.imageDrawDataOffset);
        }

        // Setup the pipeline for this specific drawType and shaderFeatures.
        gpu::ShaderFeatures shaderFeatures =
            desc.interlockMode == gpu::InterlockMode::atomics
                ? desc.combinedShaderFeatures
                : batch.shaderFeatures;

        auto shaderMiscFlags = batch.shaderMiscFlags;
        if ((renderPassOptions &
             RenderPassOptionsVulkan::atomicCoalescedResolveAndTransfer) &&
            drawType == gpu::DrawType::renderPassResolve)
        {
            assert(desc.interlockMode == gpu::InterlockMode::atomics);
            shaderMiscFlags |=
                gpu::ShaderMiscFlags::coalescedResolveAndTransfer;
        }

        auto drawPipelineOptions = DrawPipelineVulkan::Options::none;
        if (desc.wireframe && m_vk->features.fillModeNonSolid)
        {
            drawPipelineOptions |= DrawPipelineVulkan::Options::wireframe;
        }

        gpu::PipelineState pipelineState;
        gpu::get_pipeline_state(batch,
                                desc,
                                m_platformFeatures,
                                &pipelineState);

        if (batch.barriers & (gpu::BarrierFlags::plsAtomicPreResolve |
                              gpu::BarrierFlags::msaaPostInit |
                              gpu::BarrierFlags::preManualResolve))
        {
            // vkCmdNextSubpass() supersedes the pipeline barrier we would
            // insert for plsAtomic | dstBlend. So if those flags are also in
            // the barrier, we can just call vkCmdNextSubpass() and skip
            // vkCmdPipelineBarrier().
            assert(!(batch.barriers &
                     ~(gpu::BarrierFlags::plsAtomicPreResolve |
                       gpu::BarrierFlags::msaaPostInit |
                       gpu::BarrierFlags::preManualResolve |
                       BarrierFlags::plsAtomic | BarrierFlags::dstBlend |
                       BarrierFlags::drawBatchBreak)));
            m_vk->CmdNextSubpass(commandBuffer, VK_SUBPASS_CONTENTS_INLINE);
        }
        else if (batch.barriers &
                 (BarrierFlags::plsAtomic | BarrierFlags::dstBlend))
        {
            // Wait for color attachment writes to complete before we read the
            // input attachments again.
            assert(desc.interlockMode == gpu::InterlockMode::atomics ||
                   desc.interlockMode == gpu::InterlockMode::msaa);
            assert(drawType != gpu::DrawType::renderPassResolve);
            assert(!(batch.barriers &
                     ~(BarrierFlags::plsAtomic | BarrierFlags::dstBlend |
                       BarrierFlags::drawBatchBreak)));
            m_vk->memoryBarrier(
                commandBuffer,
                VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
                VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
                VK_DEPENDENCY_BY_REGION_BIT,
                {
                    // TODO: We should add SHADER_READ/SHADER_WRITE flags for
                    // the coverage buffer as well, but ironically, adding those
                    // causes artifacts on Qualcomm. Leave them out for now
                    // unless we find a case where we don't work without them.
                    .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                    .dstAccessMask = VK_ACCESS_INPUT_ATTACHMENT_READ_BIT,
                });
        }
        else if (batch.barriers & BarrierFlags::clockwiseBorrowedCoverage)
        {
            // Wait for prior fragment shaders to finish updating the coverage
            // buffer before we read it again.
            assert(desc.interlockMode == gpu::InterlockMode::clockwiseAtomic);
            assert(
                !(batch.barriers & ~(BarrierFlags::clockwiseBorrowedCoverage |
                                     BarrierFlags::drawBatchBreak)));
            m_vk->memoryBarrier(commandBuffer,
                                VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
                                VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
                                VK_DEPENDENCY_BY_REGION_BIT,
                                {
                                    .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
                                    .dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
                                });
        }

        const DrawPipelineVulkan* drawPipeline =
            m_pipelineManager->tryGetPipeline(
                {
                    .drawType = drawType,
                    .shaderFeatures = shaderFeatures,
                    .interlockMode = desc.interlockMode,
                    .shaderMiscFlags = shaderMiscFlags,
                    .pipelineState = pipelineState,
                    .drawPipelineOptions = drawPipelineOptions,
                    .renderPassOptions = renderPassOptions,
                    .renderTargetFormat = renderTarget->framebufferFormat(),
                    .colorLoadAction = desc.colorLoadAction,
#ifdef WITH_RIVE_TOOLS
                    .synthesizedFailureType = desc.synthesizedFailureType,
#endif
                },
                m_platformFeatures);

        if (drawPipeline != nullptr)
        {
            m_vk->CmdBindPipeline(commandBuffer,
                                  VK_PIPELINE_BIND_POINT_GRAPHICS,
                                  *drawPipeline);
        }

        switch (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:
            {
                // Draw patches that connect the tessellation vertices.
                m_vk->CmdBindVertexBuffers(
                    commandBuffer,
                    0,
                    1,
                    m_pathPatchVertexBuffer->vkBufferAddressOf(),
                    ZERO_OFFSET);
                m_vk->CmdBindIndexBuffer(commandBuffer,
                                         *m_pathPatchIndexBuffer,
                                         0,
                                         VK_INDEX_TYPE_UINT16);
                for (auto [chunkPatchCount, chunkFirstPatch] :
                     InstanceChunker(batch.elementCount,
                                     batch.baseElement,
                                     m_workarounds.maxInstancesPerRenderPass))
                {
                    interruptDrawPassIfNeeded(chunkPatchCount);
                    if (drawPipeline != nullptr)
                    {
                        m_vk->CmdDrawIndexed(commandBuffer,
                                             gpu::PatchIndexCount(drawType),
                                             chunkPatchCount,
                                             gpu::PatchBaseIndex(drawType),
                                             0,
                                             chunkFirstPatch);
                    }
                }
                break;
            }

            case DrawType::msaaStencilClipReset:
            case DrawType::interiorTriangulation:
            case DrawType::atlasBlit:
            {
                VkBuffer buffer = *m_triangleBuffer;
                m_vk->CmdBindVertexBuffers(commandBuffer,
                                           0,
                                           1,
                                           &buffer,
                                           ZERO_OFFSET);
                if (drawPipeline != nullptr)
                {
                    m_vk->CmdDraw(commandBuffer,
                                  batch.elementCount,
                                  1,
                                  batch.baseElement,
                                  0);
                }
                break;
            }

            case DrawType::imageRect:
            {
                assert(desc.interlockMode == gpu::InterlockMode::atomics);
                m_vk->CmdBindVertexBuffers(
                    commandBuffer,
                    0,
                    1,
                    m_imageRectVertexBuffer->vkBufferAddressOf(),
                    ZERO_OFFSET);
                m_vk->CmdBindIndexBuffer(commandBuffer,
                                         *m_imageRectIndexBuffer,
                                         0,
                                         VK_INDEX_TYPE_UINT16);
                if (drawPipeline != nullptr)
                {
                    m_vk->CmdDrawIndexed(commandBuffer,
                                         std::size(gpu::kImageRectIndices),
                                         1,
                                         batch.baseElement,
                                         0,
                                         0);
                }
                break;
            }

            case DrawType::imageMesh:
            {
                LITE_RTTI_CAST_OR_BREAK(vertexBuffer,
                                        RenderBufferVulkanImpl*,
                                        batch.vertexBuffer);
                LITE_RTTI_CAST_OR_BREAK(uvBuffer,
                                        RenderBufferVulkanImpl*,
                                        batch.uvBuffer);
                LITE_RTTI_CAST_OR_BREAK(indexBuffer,
                                        RenderBufferVulkanImpl*,
                                        batch.indexBuffer);
                m_vk->CmdBindVertexBuffers(
                    commandBuffer,
                    0,
                    1,
                    vertexBuffer->currentBuffer()->vkBufferAddressOf(),
                    ZERO_OFFSET);
                m_vk->CmdBindVertexBuffers(
                    commandBuffer,
                    1,
                    1,
                    uvBuffer->currentBuffer()->vkBufferAddressOf(),
                    ZERO_OFFSET);
                m_vk->CmdBindIndexBuffer(commandBuffer,
                                         *indexBuffer->currentBuffer(),
                                         0,
                                         VK_INDEX_TYPE_UINT16);
                if (drawPipeline != nullptr)
                {
                    m_vk->CmdDrawIndexed(commandBuffer,
                                         batch.elementCount,
                                         1,
                                         batch.baseElement,
                                         0,
                                         0);
                }
                break;
            }

            case DrawType::renderPassInitialize:
            case DrawType::renderPassResolve:
            {
                if (drawPipeline != nullptr)
                {
                    m_vk->CmdDraw(commandBuffer, 4, 1, 0, 0);
                }
                break;
            }
        }
    }

    assert(pendingTessPatchCount == 0);
    m_vk->CmdEndRenderPass(commandBuffer);

    // If the color attachment is offscreen and wasn't resolved already, copy
    // it back to the main renderTarget.
    if (colorAttachmentIsOffscreen &&
        !(renderPassOptions &
          (RenderPassOptionsVulkan::manuallyResolved |
           RenderPassOptionsVulkan::atomicCoalescedResolveAndTransfer)))
    {
#ifndef __APPLE__
        // In general, rasterOrdering flushes should not need this copy because
        // they transfer from offscreen back to the rendreTarget as part of the
        // draw pass (the one exception being interruptible render passes, which
        // aren't currently compatible with manual resolves).
        // NOTE: The manual resolve doesn't seem to work on MoltenVK, so don't
        // do it on Apple.
        assert(desc.interlockMode != gpu::InterlockMode::rasterOrdering ||
               m_workarounds.needsInterruptibleRenderPasses());
#endif

        // Atomic flushes don't need this copy either because we always use
        // "atomicCoalescedResolveAndTransfer" when the color attachment is
        // offscreen.
        assert(desc.interlockMode != gpu::InterlockMode::atomics);

        // MSAA never needs this copy. It handles resolves differently.
        assert(desc.interlockMode != gpu::InterlockMode::msaa);

        constexpr static vkutil::ImageAccess ACCESS_COPY_FROM = {
            .pipelineStages = VK_PIPELINE_STAGE_TRANSFER_BIT,
            .accessMask = VK_ACCESS_TRANSFER_READ_BIT,
            .layout = VK_IMAGE_LAYOUT_GENERAL,
        };

        m_vk->blitSubRect(
            commandBuffer,
            ((plsBackingType == PLSBackingType::storageTexture)
                 ? accessPLSOffscreenColorTexture(commandBuffer,
                                                  ACCESS_COPY_FROM)
                 : renderTarget->accessOffscreenColorTexture(commandBuffer,
                                                             ACCESS_COPY_FROM))
                ->vkImage(),
            ACCESS_COPY_FROM.layout,
            renderTarget->accessTargetImage(
                commandBuffer,
                {
                    .pipelineStages = VK_PIPELINE_STAGE_TRANSFER_BIT,
                    .accessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
                    .layout = VK_IMAGE_LAYOUT_GENERAL,
                },
                vkutil::ImageAccessAction::invalidateContents),
            VK_IMAGE_LAYOUT_GENERAL,
            drawBounds);
    }

    m_descriptorSetPoolPool->recycle(std::move(descriptorSetPool));
}

void RenderContextVulkanImpl::postFlush(const RenderContext::FlushResources&)
{
    // Recycle buffers.
    m_flushUniformBufferPool.recycle(std::move(m_flushUniformBuffer));
    m_imageDrawUniformBufferPool.recycle(std::move(m_imageDrawUniformBuffer));
    m_pathBufferPool.recycle(std::move(m_pathBuffer));
    m_paintBufferPool.recycle(std::move(m_paintBuffer));
    m_paintAuxBufferPool.recycle(std::move(m_paintAuxBuffer));
    m_contourBufferPool.recycle(std::move(m_contourBuffer));
    m_gradSpanBufferPool.recycle(std::move(m_gradSpanBuffer));
    m_tessSpanBufferPool.recycle(std::move(m_tessSpanBuffer));
    m_triangleBufferPool.recycle(std::move(m_triangleBuffer));
}

void RenderContextVulkanImpl::hotloadShaders(
    rive::Span<const uint32_t> spirvData)
{
    m_pipelineManager->clearCache();
    spirv::hotload_shaders(spirvData);

    // Delete and replace old shaders
    m_colorRampPipeline =
        std::make_unique<ColorRampPipeline>(m_pipelineManager.get(),
                                            m_workarounds);
    m_tessellatePipeline =
        std::make_unique<TessellatePipeline>(m_pipelineManager.get(),
                                             m_workarounds);
    m_atlasPipeline =
        std::make_unique<AtlasPipeline>(m_pipelineManager.get(), m_workarounds);
}

std::unique_ptr<RenderContext> RenderContextVulkanImpl::MakeContext(
    VkInstance instance,
    VkPhysicalDevice physicalDevice,
    VkDevice device,
    const VulkanFeatures& features,
    PFN_vkGetInstanceProcAddr pfnvkGetInstanceProcAddr,
    const ContextOptions& contextOptions)
{
    rcp<VulkanContext> vk = make_rcp<VulkanContext>(instance,
                                                    physicalDevice,
                                                    device,
                                                    features,
                                                    pfnvkGetInstanceProcAddr);

    if (vk->physicalDeviceProperties().apiVersion < VK_API_VERSION_1_1)
    {
        fprintf(
            stderr,
            "ERROR: Rive Vulkan renderer requires a driver that supports at least Vulkan 1.1.\n");
        return nullptr;
    }

    if (vk->physicalDeviceProperties().vendorID == VULKAN_VENDOR_IMG_TEC &&
        vk->physicalDeviceProperties().apiVersion < VK_API_VERSION_1_3)
    {
        fprintf(
            stderr,
            "ERROR: Rive Vulkan renderer requires a driver that supports at least Vulkan 1.3 on PowerVR chipsets.\n");
        return nullptr;
    }

    std::unique_ptr<RenderContextVulkanImpl> impl(
        new RenderContextVulkanImpl(std::move(vk), contextOptions));

    if (contextOptions.forceAtomicMode &&
        !impl->platformFeatures().supportsAtomicMode)
    {
        fprintf(
            stderr,
            "ERROR: Requested \"atomic\" mode but Vulkan does not support fragmentStoresAndAtomics on this platform.\n");
        return nullptr;
    }

    impl->initGPUObjects(contextOptions.shaderCompilationMode);
    return std::make_unique<RenderContext>(std::move(impl));
}
} // namespace rive::gpu
