Skip to content

Commit

Permalink
Overhaul Vulkan synchronization
Browse files Browse the repository at this point in the history
This stabilizes Vulkan on Android.

Also adds a "vkcore" backend, which tests vulkan without bonus features enabled.

Diffs=
adc240554 Overhaul Vulkan synchronization (#8166)

Co-authored-by: Chris Dalton <[email protected]>
  • Loading branch information
csmartdalton and csmartdalton committed Sep 18, 2024
1 parent 0f206ef commit 88dce8c
Show file tree
Hide file tree
Showing 24 changed files with 1,291 additions and 1,018 deletions.
2 changes: 1 addition & 1 deletion .rive_head
Original file line number Diff line number Diff line change
@@ -1 +1 @@
9dd20080cc462660396b8b24b9d3b1b72f53516b
adc2405543ea447b83fda212a0cd7cc4eafd4de9
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

#include "rive/renderer/render_context_impl.hpp"
#include "rive/renderer/vulkan/vulkan_context.hpp"
#include "rive/renderer/vulkan/vkutil_resource_pool.hpp"
#include <chrono>
#include <map>
#include <vulkan/vulkan.h>
#include <deque>

namespace rive::gpu
{
Expand All @@ -18,11 +18,20 @@ class TextureVulkanImpl;
class RenderTargetVulkan : public RenderTarget
{
public:
void setTargetTextureView(rcp<vkutil::TextureView> view)
void setTargetTextureView(rcp<vkutil::TextureView> view,
VulkanContext::TextureAccess targetLastAccess)
{
m_targetTextureView = std::move(view);
setTargetLastAccess(targetLastAccess);
}

void setTargetLastAccess(VulkanContext::TextureAccess lastAccess)
{
m_targetLastAccess = lastAccess;
}

const VulkanContext::TextureAccess& targetLastAccess() const { return m_targetLastAccess; }

bool targetViewContainsUsageFlag(VkImageUsageFlagBits bit) const
{
return m_targetTextureView ? static_cast<bool>(m_targetTextureView->usageFlags() & bit)
Expand All @@ -33,11 +42,11 @@ class RenderTargetVulkan : public RenderTarget

VkImage targetTexture() const { return m_targetTextureView->info().image; }

VkImageView targetTextureView() const { return *m_targetTextureView; }
vkutil::TextureView* targetTextureView() const { return m_targetTextureView.get(); }

VkImageView offscreenColorTextureView() const
vkutil::TextureView* offscreenColorTextureView() const
{
return m_offscreenColorTextureView ? *m_offscreenColorTextureView : nullptr;
return m_offscreenColorTextureView.get();
}

VkImage offscreenColorTexture() const { return *m_offscreenColorTexture; }
Expand All @@ -48,11 +57,11 @@ class RenderTargetVulkan : public RenderTarget

// getters that lazy load if needed.

VkImageView ensureOffscreenColorTextureView(VkCommandBuffer);
VkImageView ensureCoverageTextureView(VkCommandBuffer);
VkImageView ensureClipTextureView(VkCommandBuffer);
VkImageView ensureScratchColorTextureView(VkCommandBuffer);
VkImageView ensureCoverageAtomicTextureView(VkCommandBuffer);
vkutil::TextureView* ensureOffscreenColorTextureView();
vkutil::TextureView* ensureCoverageTextureView();
vkutil::TextureView* ensureClipTextureView();
vkutil::TextureView* ensureScratchColorTextureView();
vkutil::TextureView* ensureCoverageAtomicTextureView();

private:
friend class RenderContextVulkanImpl;
Expand All @@ -67,6 +76,7 @@ class RenderTargetVulkan : public RenderTarget
const rcp<VulkanContext> m_vk;
const VkFormat m_framebufferFormat;
rcp<vkutil::TextureView> m_targetTextureView;
VulkanContext::TextureAccess m_targetLastAccess;

rcp<vkutil::Texture> m_offscreenColorTexture; // Used when m_targetTextureView does not have
// VK_ACCESS_INPUT_ATTACHMENT_READ_BIT
Expand Down Expand Up @@ -125,7 +135,7 @@ class RenderContextVulkanImpl : public RenderContextImpl
{ \
return m_name.contentsAt(m_bufferRingIdx, mapSizeInBytes); \
} \
void unmap##Name() override { m_name.flushMappedContentsAt(m_bufferRingIdx); }
void unmap##Name() override { m_name.flushContentsAt(m_bufferRingIdx); }

#define IMPLEMENT_PLS_STRUCTURED_BUFFER(Name, m_name) \
void resize##Name(size_t sizeInBytes, gpu::StorageBufferStructure) override \
Expand All @@ -136,7 +146,7 @@ class RenderContextVulkanImpl : public RenderContextImpl
{ \
return m_name.contentsAt(m_bufferRingIdx, mapSizeInBytes); \
} \
void unmap##Name() override { m_name.flushMappedContentsAt(m_bufferRingIdx); }
void unmap##Name() override { m_name.flushContentsAt(m_bufferRingIdx); }

IMPLEMENT_PLS_BUFFER(FlushUniformBuffer, m_flushUniformBufferRing)
IMPLEMENT_PLS_BUFFER(ImageDrawUniformBuffer, m_imageDrawUniformBufferRing)
Expand All @@ -160,27 +170,26 @@ class RenderContextVulkanImpl : public RenderContextImpl
// The vkutil::RenderingResource base ensures this class stays alive until its
// command buffer finishes, at which point we free the allocated descriptor
// sets and return the VkDescriptor to the renderContext.
class DescriptorSetPool final : public vkutil::RenderingResource
class DescriptorSetPool final : public RefCnt<DescriptorSetPool>
{
public:
DescriptorSetPool(RenderContextVulkanImpl*);
~DescriptorSetPool() final;
DescriptorSetPool(rcp<VulkanContext>);
~DescriptorSetPool();

VkDescriptorSet allocateDescriptorSet(VkDescriptorSetLayout);
void freeDescriptorSets();
void reset();

private:
// Recycles this instance in the renderContext's m_descriptorSetPoolPool, if
// the renderContext still exists.
void onRefCntReachedZero() const;
friend class RefCnt<DescriptorSetPool>;
friend class vkutil::ResourcePool<DescriptorSetPool>;

void onRefCntReachedZero() const { m_pool->onResourceRefCntReachedZero(this); }

RenderContextVulkanImpl* const m_renderContextImpl;
const rcp<VulkanContext> m_vk;
rcp<vkutil::ResourcePool<DescriptorSetPool>> m_pool;
VkDescriptorPool m_vkDescriptorPool;
std::vector<VkDescriptorSet> m_descriptorSets;
};

rcp<DescriptorSetPool> makeDescriptorSetPool();

void flush(const FlushDescriptor&) override;

double secondsNow() const override
Expand Down Expand Up @@ -243,8 +252,8 @@ class RenderContextVulkanImpl : public RenderContextImpl
rcp<gpu::CommandBufferCompletionFence> m_frameCompletionFences[gpu::kBufferRingSize];
int m_bufferRingIdx = -1;

// Pool of DescriptorSetPools that have been fully released. These can be
// Pool of DescriptorSetPools that have been fully released. These will be
// recycled once their expirationFrameIdx is reached.
std::deque<vkutil::ZombieResource<DescriptorSetPool>> m_descriptorSetPoolPool;
rcp<vkutil::ResourcePool<DescriptorSetPool>> m_descriptorSetPoolPool;
};
} // namespace rive::gpu
30 changes: 10 additions & 20 deletions renderer/include/rive/renderer/vulkan/vkutil.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,15 @@ class Buffer : public RenderingResource
return m_contents;
}

void flushMappedContents(size_t updatedSizeInBytes);
// Calls through to vkFlushMappedMemoryRanges().
// Called after modifying contents() with the CPU. Makes those modifications
// available to the GPU.
void flushContents(size_t sizeInBytes = VK_WHOLE_SIZE);

// Calls through to vkInvalidateMappedMemoryRanges().
// Called after modifying the buffer with the GPU. Makes those modifications
// available to the CPU via contents().
void invalidateContents(size_t sizeInBytes = VK_WHOLE_SIZE);

private:
friend class ::rive::gpu::VulkanContext;
Expand All @@ -122,24 +130,6 @@ class Buffer : public RenderingResource
void* m_contents;
};

// RAII utility to call flushMappedContents() on a buffer when the class goes out
// of scope.
class ScopedBufferFlush
{
public:
ScopedBufferFlush(Buffer& buff, size_t mapSizeInBytes = VK_WHOLE_SIZE) :
m_buff(buff), m_mapSizeInBytes(mapSizeInBytes)
{}
~ScopedBufferFlush() { m_buff.flushMappedContents(m_mapSizeInBytes); }

operator void*() { return m_buff.contents(); }
template <typename T> T as() { return reinterpret_cast<T>(m_buff.contents()); }

private:
Buffer& m_buff;
const size_t m_mapSizeInBytes;
};

// Wraps a ring of VkBuffers so we can map one while other(s) are in-flight.
class BufferRing
{
Expand All @@ -158,7 +148,7 @@ class BufferRing
void setTargetSize(size_t size);
void synchronizeSizeAt(int bufferRingIdx);
void* contentsAt(int bufferRingIdx, size_t dirtySize = VK_WHOLE_SIZE);
void flushMappedContentsAt(int bufferRingIdx);
void flushContentsAt(int bufferRingIdx);

private:
size_t m_targetSize;
Expand Down
175 changes: 175 additions & 0 deletions renderer/include/rive/renderer/vulkan/vkutil_resource_pool.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* Copyright 2024 Rive
*/

#pragma once

#include "rive/renderer/gpu.hpp"
#include "rive/renderer/vulkan/vulkan_context.hpp"

namespace rive::gpu::vkutil
{
class CommandBuffer;
class Fence;
class Semaphore;

// Used by ResourcePool<T> to construct resources.
template <typename T> class ResourceFactory
{
public:
ResourceFactory(rcp<VulkanContext> vk) : m_vk(std::move(vk)) {}
VulkanContext* vulkanContext() const { return m_vk.get(); }
rcp<T> make() { return make_rcp<T>(m_vk); }

private:
rcp<VulkanContext> m_vk;
};

// ResourceFactory<> specialization for command buffers, which also require a
// VkCommandPool.
template <> class ResourceFactory<CommandBuffer>
{
public:
ResourceFactory(rcp<VulkanContext> vk, uint32_t queueFamilyIndex);
~ResourceFactory();

VulkanContext* vulkanContext() const { return m_vk.get(); }
rcp<CommandBuffer> make() { return make_rcp<CommandBuffer>(m_vk, m_vkCommandPool); }
operator VkCommandPool() const { return m_vkCommandPool; }

private:
rcp<VulkanContext> m_vk;
VkCommandPool m_vkCommandPool = VK_NULL_HANDLE;
};

// Manages a pool of recyclable Vulkan resources. When onRefCntReachedZero() is
// called on the resources owned by this pool, they are captured and recycled
// rather than deleted.
template <typename T, uint32_t MaxResourcesInPool = 64>
class ResourcePool : public RefCnt<ResourcePool<T>>
{
public:
template <typename... FactoryArgs>
ResourcePool(FactoryArgs&&... factoryArgs) :
m_factory(std::forward<FactoryArgs>(factoryArgs)...)
{}

~ResourcePool()
{
m_releasedResources.clear(); // Delete resources before freeing the factory.
}

rcp<T> make()
{
rcp<T> resource;
if (!m_releasedResources.empty() && m_factory.vulkanContext()->currentFrameIdx() >=
m_releasedResources.front().expirationFrameIdx)
{
resource = ref_rcp(m_releasedResources.front().resource.release());
m_releasedResources.pop_front();
resource->reset();
}
else
{
resource = m_factory.make();
}
resource->m_pool = ref_rcp(static_cast<ResourcePool<T>*>(this));
assert(resource->debugging_refcnt() == 1);
return resource;
}

void onResourceRefCntReachedZero(const T* resource)
{
auto mutableResource = const_cast<T*>(resource);
assert(mutableResource->debugging_refcnt() == 0);
assert(mutableResource->m_pool.get() == this);

if (m_releasedResources.size() < MaxResourcesInPool)
{
// Recycle the resource!
m_releasedResources.emplace_back(mutableResource,
m_factory.vulkanContext()->currentFrameIdx());
// Do this last in case it deletes our "this".
mutableResource->m_pool = nullptr;
return;
}

delete resource;
}

protected:
ResourceFactory<T> m_factory;

// Pool of Resources that have been fully released.
// These can be recycled once their expirationFrameIdx is reached.
std::deque<vkutil::ZombieResource<T>> m_releasedResources;
};

// VkCommandBuffer that lives in a ResourcePool.
class CommandBuffer : public RefCnt<CommandBuffer>
{
public:
CommandBuffer(rcp<VulkanContext>, VkCommandPool);
~CommandBuffer();

void reset();
operator VkCommandBuffer() const { return m_vkCommandBuffer; }
const VkCommandBuffer* vkCommandBufferAddressOf() const { return &m_vkCommandBuffer; }

private:
friend class RefCnt<CommandBuffer>;
friend class ResourcePool<CommandBuffer>;

void onRefCntReachedZero() const { m_pool->onResourceRefCntReachedZero(this); }

const rcp<VulkanContext> m_vk;
const VkCommandPool m_vkCommandPool;
rcp<ResourcePool<CommandBuffer>> m_pool;
VkCommandBuffer m_vkCommandBuffer;
};

// VkSemaphore wrapper that lives in a ResourcePool.
class Semaphore : public RefCnt<Semaphore>
{
public:
Semaphore(rcp<VulkanContext>);
~Semaphore();

void reset();

operator VkSemaphore() const { return m_vkSemaphore; }
const VkSemaphore* vkSemaphoreAddressOf() const { return &m_vkSemaphore; }

private:
friend class RefCnt<Semaphore>;
friend class ResourcePool<Semaphore>;

void onRefCntReachedZero() const { m_pool->onResourceRefCntReachedZero(this); }

const rcp<VulkanContext> m_vk;
rcp<ResourcePool<Semaphore>> m_pool;
VkSemaphore m_vkSemaphore;
};

// VkFence wrapper that lives in a ResourcePool.
class Fence : public CommandBufferCompletionFence
{
public:
Fence(rcp<VulkanContext>);
~Fence() override;

void reset();
void wait() override;

operator VkFence() const { return m_vkFence; }

private:
friend class ResourcePool<Fence>;

void onRefCntReachedZero() const override { m_pool->onResourceRefCntReachedZero(this); }

const rcp<VulkanContext> m_vk;
rcp<ResourcePool<Fence>> m_pool;
VkFence m_vkFence;
};
} // namespace rive::gpu::vkutil
Loading

0 comments on commit 88dce8c

Please sign in to comment.