Commit 9f2a8613 by Jamie Madill Committed by Commit Bot

Vulkan: Implement a RenderPass cache.

This cache replaces the RenderPass-per-Framebuffer approach. Although the concepts of a RenderPass are closely associated with rendering to a Framebuffer, there can be multiple RenderPasses used with a single FBO, especially considering the nature of Load and Store operations. This code will then lend itself to the implementation of the deferred RenderPasses, which are created on flush. These RenderPasses won't be owned by a Framebuffer. Bug: angleproject:2264 Change-Id: I4dce07c302118f7e05f5225e2a3b0569ad1e52bf Reviewed-on: https://chromium-review.googlesource.com/789534Reviewed-by: 's avatarFrank Henigman <fjhenigman@chromium.org> Reviewed-by: 's avatarJamie Madill <jmadill@chromium.org> Commit-Queue: Jamie Madill <jmadill@chromium.org>
parent e218f15f
...@@ -278,8 +278,10 @@ gl::Error ContextVk::initPipeline(const gl::Context *context) ...@@ -278,8 +278,10 @@ gl::Error ContextVk::initPipeline(const gl::Context *context)
mCurrentInputAssemblyState.topology = gl_vk::GetPrimitiveTopology(mCurrentDrawMode); mCurrentInputAssemblyState.topology = gl_vk::GetPrimitiveTopology(mCurrentDrawMode);
const vk::RenderPassDesc &desc = vkFBO->getRenderPassDesc(context);
vk::RenderPass *renderPass = nullptr; vk::RenderPass *renderPass = nullptr;
ANGLE_TRY_RESULT(vkFBO->getRenderPass(context, device), renderPass); ANGLE_TRY(mRenderer->getCompatibleRenderPass(desc, &renderPass));
ASSERT(renderPass && renderPass->valid()); ASSERT(renderPass && renderPass->valid());
const vk::PipelineLayout &pipelineLayout = programVk->getPipelineLayout(); const vk::PipelineLayout &pipelineLayout = programVk->getPipelineLayout();
......
...@@ -81,12 +81,12 @@ FramebufferVk *FramebufferVk::CreateDefaultFBO(const gl::FramebufferState &state ...@@ -81,12 +81,12 @@ FramebufferVk *FramebufferVk::CreateDefaultFBO(const gl::FramebufferState &state
} }
FramebufferVk::FramebufferVk(const gl::FramebufferState &state) FramebufferVk::FramebufferVk(const gl::FramebufferState &state)
: FramebufferImpl(state), mBackbuffer(nullptr), mRenderPass(), mFramebuffer() : FramebufferImpl(state), mBackbuffer(nullptr), mRenderPassDesc(), mFramebuffer()
{ {
} }
FramebufferVk::FramebufferVk(const gl::FramebufferState &state, WindowSurfaceVk *backbuffer) FramebufferVk::FramebufferVk(const gl::FramebufferState &state, WindowSurfaceVk *backbuffer)
: FramebufferImpl(state), mBackbuffer(backbuffer), mRenderPass(), mFramebuffer() : FramebufferImpl(state), mBackbuffer(backbuffer), mRenderPassDesc(), mFramebuffer()
{ {
} }
...@@ -98,7 +98,6 @@ void FramebufferVk::destroy(const gl::Context *context) ...@@ -98,7 +98,6 @@ void FramebufferVk::destroy(const gl::Context *context)
{ {
RendererVk *renderer = vk::GetImpl(context)->getRenderer(); RendererVk *renderer = vk::GetImpl(context)->getRenderer();
renderer->releaseResource(*this, &mRenderPass);
renderer->releaseResource(*this, &mFramebuffer); renderer->releaseResource(*this, &mFramebuffer);
} }
...@@ -106,7 +105,6 @@ void FramebufferVk::destroyDefault(const egl::Display *display) ...@@ -106,7 +105,6 @@ void FramebufferVk::destroyDefault(const egl::Display *display)
{ {
VkDevice device = vk::GetImpl(display)->getRenderer()->getDevice(); VkDevice device = vk::GetImpl(display)->getRenderer()->getDevice();
mRenderPass.destroy(device);
mFramebuffer.destroy(device); mFramebuffer.destroy(device);
} }
...@@ -363,7 +361,7 @@ void FramebufferVk::syncState(const gl::Context *context, ...@@ -363,7 +361,7 @@ void FramebufferVk::syncState(const gl::Context *context,
ASSERT(dirtyBits.any()); ASSERT(dirtyBits.any());
// TODO(jmadill): Smarter update. // TODO(jmadill): Smarter update.
renderer->releaseResource(*this, &mRenderPass); mRenderPassDesc.reset();
renderer->releaseResource(*this, &mFramebuffer); renderer->releaseResource(*this, &mFramebuffer);
renderer->onReleaseRenderPass(this); renderer->onReleaseRenderPass(this);
...@@ -371,12 +369,11 @@ void FramebufferVk::syncState(const gl::Context *context, ...@@ -371,12 +369,11 @@ void FramebufferVk::syncState(const gl::Context *context,
contextVk->invalidateCurrentPipeline(); contextVk->invalidateCurrentPipeline();
} }
gl::ErrorOrResult<vk::RenderPass *> FramebufferVk::getRenderPass(const gl::Context *context, const vk::RenderPassDesc &FramebufferVk::getRenderPassDesc(const gl::Context *context)
VkDevice device)
{ {
if (mRenderPass.valid()) if (mRenderPassDesc.valid())
{ {
return &mRenderPass; return mRenderPassDesc.value();
} }
vk::RenderPassDesc desc; vk::RenderPassDesc desc;
...@@ -388,7 +385,7 @@ gl::ErrorOrResult<vk::RenderPass *> FramebufferVk::getRenderPass(const gl::Conte ...@@ -388,7 +385,7 @@ gl::ErrorOrResult<vk::RenderPass *> FramebufferVk::getRenderPass(const gl::Conte
if (colorAttachment.isAttached()) if (colorAttachment.isAttached())
{ {
RenderTargetVk *renderTarget = nullptr; RenderTargetVk *renderTarget = nullptr;
ANGLE_TRY(colorAttachment.getRenderTarget(context, &renderTarget)); ANGLE_SWALLOW_ERR(colorAttachment.getRenderTarget(context, &renderTarget));
VkAttachmentDescription *colorDesc = desc.nextColorAttachment(); VkAttachmentDescription *colorDesc = desc.nextColorAttachment();
...@@ -415,7 +412,7 @@ gl::ErrorOrResult<vk::RenderPass *> FramebufferVk::getRenderPass(const gl::Conte ...@@ -415,7 +412,7 @@ gl::ErrorOrResult<vk::RenderPass *> FramebufferVk::getRenderPass(const gl::Conte
if (depthStencilAttachment && depthStencilAttachment->isAttached()) if (depthStencilAttachment && depthStencilAttachment->isAttached())
{ {
RenderTargetVk *renderTarget = nullptr; RenderTargetVk *renderTarget = nullptr;
ANGLE_TRY(depthStencilAttachment->getRenderTarget(context, &renderTarget)); ANGLE_SWALLOW_ERR(depthStencilAttachment->getRenderTarget(context, &renderTarget));
VkAttachmentDescription *depthStencilDesc = desc.nextDepthStencilAttachment(); VkAttachmentDescription *depthStencilDesc = desc.nextDepthStencilAttachment();
...@@ -430,13 +427,12 @@ gl::ErrorOrResult<vk::RenderPass *> FramebufferVk::getRenderPass(const gl::Conte ...@@ -430,13 +427,12 @@ gl::ErrorOrResult<vk::RenderPass *> FramebufferVk::getRenderPass(const gl::Conte
depthStencilDesc->finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; depthStencilDesc->finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
} }
ANGLE_TRY(vk::InitializeRenderPassFromDesc(device, desc, &mRenderPass)); mRenderPassDesc = desc;
return mRenderPassDesc.value();
return &mRenderPass;
} }
gl::ErrorOrResult<vk::Framebuffer *> FramebufferVk::getFramebuffer(const gl::Context *context, gl::ErrorOrResult<vk::Framebuffer *> FramebufferVk::getFramebuffer(const gl::Context *context,
VkDevice device) RendererVk *rendererVk)
{ {
// If we've already created our cached Framebuffer, return it. // If we've already created our cached Framebuffer, return it.
if (mFramebuffer.valid()) if (mFramebuffer.valid())
...@@ -444,10 +440,13 @@ gl::ErrorOrResult<vk::Framebuffer *> FramebufferVk::getFramebuffer(const gl::Con ...@@ -444,10 +440,13 @@ gl::ErrorOrResult<vk::Framebuffer *> FramebufferVk::getFramebuffer(const gl::Con
return &mFramebuffer; return &mFramebuffer;
} }
const vk::RenderPassDesc &desc = getRenderPassDesc(context);
vk::RenderPass *renderPass = nullptr; vk::RenderPass *renderPass = nullptr;
ANGLE_TRY_RESULT(getRenderPass(context, device), renderPass); ANGLE_TRY(rendererVk->getCompatibleRenderPass(desc, &renderPass));
// If we've a Framebuffer provided by a Surface (default FBO/backbuffer), query it. // If we've a Framebuffer provided by a Surface (default FBO/backbuffer), query it.
VkDevice device = rendererVk->getDevice();
if (mBackbuffer) if (mBackbuffer)
{ {
return mBackbuffer->getCurrentFramebuffer(device, *renderPass); return mBackbuffer->getCurrentFramebuffer(device, *renderPass);
...@@ -490,7 +489,7 @@ gl::ErrorOrResult<vk::Framebuffer *> FramebufferVk::getFramebuffer(const gl::Con ...@@ -490,7 +489,7 @@ gl::ErrorOrResult<vk::Framebuffer *> FramebufferVk::getFramebuffer(const gl::Con
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.pNext = nullptr; framebufferInfo.pNext = nullptr;
framebufferInfo.flags = 0; framebufferInfo.flags = 0;
framebufferInfo.renderPass = mRenderPass.getHandle(); framebufferInfo.renderPass = renderPass->getHandle();
framebufferInfo.attachmentCount = static_cast<uint32_t>(attachments.size()); framebufferInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
framebufferInfo.pAttachments = attachments.data(); framebufferInfo.pAttachments = attachments.data();
framebufferInfo.width = static_cast<uint32_t>(attachmentsSize.width); framebufferInfo.width = static_cast<uint32_t>(attachmentsSize.width);
...@@ -509,7 +508,7 @@ gl::Error FramebufferVk::getSamplePosition(size_t index, GLfloat *xy) const ...@@ -509,7 +508,7 @@ gl::Error FramebufferVk::getSamplePosition(size_t index, GLfloat *xy) const
} }
gl::Error FramebufferVk::beginRenderPass(const gl::Context *context, gl::Error FramebufferVk::beginRenderPass(const gl::Context *context,
VkDevice device, RendererVk *rendererVk,
vk::CommandBuffer *commandBuffer, vk::CommandBuffer *commandBuffer,
Serial queueSerial) Serial queueSerial)
{ {
...@@ -533,11 +532,13 @@ gl::Error FramebufferVk::beginRenderPass(const gl::Context *context, ...@@ -533,11 +532,13 @@ gl::Error FramebufferVk::beginRenderPass(const gl::Context *context,
} }
vk::Framebuffer *framebuffer = nullptr; vk::Framebuffer *framebuffer = nullptr;
ANGLE_TRY_RESULT(getFramebuffer(context, device), framebuffer); ANGLE_TRY_RESULT(getFramebuffer(context, rendererVk), framebuffer);
ASSERT(framebuffer && framebuffer->valid()); ASSERT(framebuffer && framebuffer->valid());
const vk::RenderPassDesc &desc = getRenderPassDesc(context);
vk::RenderPass *renderPass = nullptr; vk::RenderPass *renderPass = nullptr;
ANGLE_TRY_RESULT(getRenderPass(context, device), renderPass); ANGLE_TRY(rendererVk->getMatchingRenderPass(desc, &renderPass));
ASSERT(renderPass && renderPass->valid()); ASSERT(renderPass && renderPass->valid());
// TODO(jmadill): Proper clear value implementation. // TODO(jmadill): Proper clear value implementation.
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
namespace rx namespace rx
{ {
class RendererVk;
class RenderTargetVk; class RenderTargetVk;
class WindowSurfaceVk; class WindowSurfaceVk;
...@@ -84,22 +85,22 @@ class FramebufferVk : public FramebufferImpl, public ResourceVk ...@@ -84,22 +85,22 @@ class FramebufferVk : public FramebufferImpl, public ResourceVk
gl::Error getSamplePosition(size_t index, GLfloat *xy) const override; gl::Error getSamplePosition(size_t index, GLfloat *xy) const override;
gl::Error beginRenderPass(const gl::Context *context, gl::Error beginRenderPass(const gl::Context *context,
VkDevice device, RendererVk *rendererVk,
vk::CommandBuffer *commandBuffer, vk::CommandBuffer *commandBuffer,
Serial queueSerial); Serial queueSerial);
gl::ErrorOrResult<vk::RenderPass *> getRenderPass(const gl::Context *context, VkDevice device); const vk::RenderPassDesc &getRenderPassDesc(const gl::Context *context);
private: private:
FramebufferVk(const gl::FramebufferState &state); FramebufferVk(const gl::FramebufferState &state);
FramebufferVk(const gl::FramebufferState &state, WindowSurfaceVk *backbuffer); FramebufferVk(const gl::FramebufferState &state, WindowSurfaceVk *backbuffer);
gl::ErrorOrResult<vk::Framebuffer *> getFramebuffer(const gl::Context *context, gl::ErrorOrResult<vk::Framebuffer *> getFramebuffer(const gl::Context *context,
VkDevice device); RendererVk *rendererVk);
WindowSurfaceVk *mBackbuffer; WindowSurfaceVk *mBackbuffer;
vk::RenderPass mRenderPass; Optional<vk::RenderPassDesc> mRenderPassDesc;
vk::Framebuffer mFramebuffer; vk::Framebuffer mFramebuffer;
}; };
......
...@@ -84,6 +84,63 @@ VkBool32 VKAPI_CALL DebugReportCallback(VkDebugReportFlagsEXT flags, ...@@ -84,6 +84,63 @@ VkBool32 VKAPI_CALL DebugReportCallback(VkDebugReportFlagsEXT flags,
} // anonymous namespace } // anonymous namespace
// RenderPassCache implementation.
RenderPassCache::RenderPassCache()
{
}
RenderPassCache::~RenderPassCache()
{
ASSERT(mPayload.empty());
}
void RenderPassCache::destroy(VkDevice device)
{
for (auto &renderPassIt : mPayload)
{
renderPassIt.second.get().destroy(device);
}
mPayload.clear();
}
vk::Error RenderPassCache::getCompatibleRenderPass(VkDevice device,
Serial serial,
const vk::RenderPassDesc &desc,
vk::RenderPass **renderPassOut)
{
// TODO(jmadill): Return compatible RenderPass when possible.
return getMatchingRenderPass(device, serial, desc, renderPassOut);
}
vk::Error RenderPassCache::getMatchingRenderPass(VkDevice device,
Serial serial,
const vk::RenderPassDesc &desc,
vk::RenderPass **renderPassOut)
{
auto it = mPayload.find(desc);
if (it != mPayload.end())
{
// Update the serial before we return.
// TODO(jmadill): Could possibly use an MRU cache here.
it->second.updateSerial(serial);
*renderPassOut = &it->second.get();
return vk::NoError();
}
vk::RenderPass newRenderPass;
ANGLE_TRY(vk::InitializeRenderPassFromDesc(device, desc, &newRenderPass));
vk::RenderPassAndSerial withSerial(std::move(newRenderPass), serial);
auto insertPos = mPayload.emplace(desc, std::move(withSerial));
*renderPassOut = &insertPos.first->second.get();
// TODO(jmadill): Trim cache, and pre-populate with the most common RPs on startup.
return vk::NoError();
}
// RendererVk implementation.
RendererVk::RendererVk() RendererVk::RendererVk()
: mCapsInitialized(false), : mCapsInitialized(false),
mInstance(VK_NULL_HANDLE), mInstance(VK_NULL_HANDLE),
...@@ -112,6 +169,8 @@ RendererVk::~RendererVk() ...@@ -112,6 +169,8 @@ RendererVk::~RendererVk()
} }
} }
mRenderPassCache.destroy(mDevice);
if (mGlslangWrapper) if (mGlslangWrapper)
{ {
GlslangWrapper::ReleaseReference(); GlslangWrapper::ReleaseReference();
...@@ -798,8 +857,7 @@ gl::Error RendererVk::ensureInRenderPass(const gl::Context *context, Framebuffer ...@@ -798,8 +857,7 @@ gl::Error RendererVk::ensureInRenderPass(const gl::Context *context, Framebuffer
{ {
endRenderPass(); endRenderPass();
} }
ANGLE_TRY( ANGLE_TRY(framebufferVk->beginRenderPass(context, this, &mCommandBuffer, mCurrentQueueSerial));
framebufferVk->beginRenderPass(context, mDevice, &mCommandBuffer, mCurrentQueueSerial));
mCurrentRenderPassFramebuffer = framebufferVk; mCurrentRenderPassFramebuffer = framebufferVk;
return gl::NoError(); return gl::NoError();
} }
...@@ -832,4 +890,18 @@ bool RendererVk::isSerialInUse(Serial serial) ...@@ -832,4 +890,18 @@ bool RendererVk::isSerialInUse(Serial serial)
return serial > mLastCompletedQueueSerial; return serial > mLastCompletedQueueSerial;
} }
vk::Error RendererVk::getCompatibleRenderPass(const vk::RenderPassDesc &desc,
vk::RenderPass **renderPassOut)
{
return mRenderPassCache.getCompatibleRenderPass(mDevice, mCurrentQueueSerial, desc,
renderPassOut);
}
vk::Error RendererVk::getMatchingRenderPass(const vk::RenderPassDesc &desc,
vk::RenderPass **renderPassOut)
{
return mRenderPassCache.getMatchingRenderPass(mDevice, mCurrentQueueSerial, desc,
renderPassOut);
}
} // namespace rx } // namespace rx
...@@ -33,6 +33,28 @@ namespace vk ...@@ -33,6 +33,28 @@ namespace vk
struct Format; struct Format;
} }
// TODO(jmadill): Add cache trimming.
class RenderPassCache
{
public:
RenderPassCache();
~RenderPassCache();
void destroy(VkDevice device);
vk::Error getCompatibleRenderPass(VkDevice device,
Serial serial,
const vk::RenderPassDesc &desc,
vk::RenderPass **renderPassOut);
vk::Error getMatchingRenderPass(VkDevice device,
Serial serial,
const vk::RenderPassDesc &desc,
vk::RenderPass **renderPassOut);
private:
std::unordered_map<vk::RenderPassDesc, vk::RenderPassAndSerial> mPayload;
};
class RendererVk : angle::NonCopyable class RendererVk : angle::NonCopyable
{ {
public: public:
...@@ -115,6 +137,10 @@ class RendererVk : angle::NonCopyable ...@@ -115,6 +137,10 @@ class RendererVk : angle::NonCopyable
return mFormatTable[internalFormat]; return mFormatTable[internalFormat];
} }
vk::Error getCompatibleRenderPass(const vk::RenderPassDesc &desc,
vk::RenderPass **renderPassOut);
vk::Error getMatchingRenderPass(const vk::RenderPassDesc &desc, vk::RenderPass **renderPassOut);
private: private:
void ensureCapsInitialized() const; void ensureCapsInitialized() const;
void generateCaps(gl::Caps *outCaps, void generateCaps(gl::Caps *outCaps,
...@@ -157,6 +183,8 @@ class RendererVk : angle::NonCopyable ...@@ -157,6 +183,8 @@ class RendererVk : angle::NonCopyable
// TODO(jmadill): Don't keep a single renderpass in the Renderer. // TODO(jmadill): Don't keep a single renderpass in the Renderer.
FramebufferVk *mCurrentRenderPassFramebuffer; FramebufferVk *mCurrentRenderPassFramebuffer;
RenderPassCache mRenderPassCache;
}; };
} // namespace rx } // namespace rx
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
#include "renderervk_utils.h" #include "renderervk_utils.h"
#include "libANGLE/SizedMRUCache.h"
#include "libANGLE/renderer/vulkan/ContextVk.h" #include "libANGLE/renderer/vulkan/ContextVk.h"
#include "libANGLE/renderer/vulkan/RendererVk.h" #include "libANGLE/renderer/vulkan/RendererVk.h"
...@@ -1287,6 +1288,19 @@ VkAttachmentDescription *RenderPassDesc::nextDepthStencilAttachment() ...@@ -1287,6 +1288,19 @@ VkAttachmentDescription *RenderPassDesc::nextDepthStencilAttachment()
return &attachmentDescs[depthStencilAttachmentCount++]; return &attachmentDescs[depthStencilAttachmentCount++];
} }
size_t RenderPassDesc::hash() const
{
return angle::ComputeGenericHash(*this);
}
bool RenderPassDesc::operator==(const RenderPassDesc &other) const
{
return colorAttachmentCount == other.colorAttachmentCount &&
depthStencilAttachmentCount == other.depthStencilAttachmentCount &&
(memcmp(attachmentDescs.data(), other.attachmentDescs.data(),
sizeof(VkAttachmentDescription) * attachmentDescs.size()) == 0);
}
uint32_t RenderPassDesc::attachmentCount() const uint32_t RenderPassDesc::attachmentCount() const
{ {
return (colorAttachmentCount + depthStencilAttachmentCount); return (colorAttachmentCount + depthStencilAttachmentCount);
......
...@@ -624,6 +624,11 @@ class ObjectAndSerial final : angle::NonCopyable ...@@ -624,6 +624,11 @@ class ObjectAndSerial final : angle::NonCopyable
} }
Serial queueSerial() const { return mQueueSerial; } Serial queueSerial() const { return mQueueSerial; }
void updateSerial(Serial newSerial)
{
ASSERT(newSerial >= mQueueSerial);
mQueueSerial = newSerial;
}
const ObjT &get() const { return mObject; } const ObjT &get() const { return mObject; }
ObjT &get() { return mObject; } ObjT &get() { return mObject; }
...@@ -667,6 +672,7 @@ class CommandBufferAndState : public vk::CommandBuffer ...@@ -667,6 +672,7 @@ class CommandBufferAndState : public vk::CommandBuffer
using CommandBufferAndSerial = ObjectAndSerial<CommandBufferAndState>; using CommandBufferAndSerial = ObjectAndSerial<CommandBufferAndState>;
using FenceAndSerial = ObjectAndSerial<Fence>; using FenceAndSerial = ObjectAndSerial<Fence>;
using RenderPassAndSerial = ObjectAndSerial<RenderPass>;
struct RenderPassDesc final struct RenderPassDesc final
{ {
...@@ -680,6 +686,9 @@ struct RenderPassDesc final ...@@ -680,6 +686,9 @@ struct RenderPassDesc final
VkAttachmentDescription *nextDepthStencilAttachment(); VkAttachmentDescription *nextDepthStencilAttachment();
uint32_t attachmentCount() const; uint32_t attachmentCount() const;
size_t hash() const;
bool operator==(const RenderPassDesc &other) const;
// Fully padded out, with no bools, to avoid any undefined behaviour. // Fully padded out, with no bools, to avoid any undefined behaviour.
uint32_t colorAttachmentCount; uint32_t colorAttachmentCount;
uint32_t depthStencilAttachmentCount; uint32_t depthStencilAttachmentCount;
...@@ -717,4 +726,14 @@ VkFrontFace GetFrontFace(GLenum frontFace); ...@@ -717,4 +726,14 @@ VkFrontFace GetFrontFace(GLenum frontFace);
std::ostream &operator<<(std::ostream &stream, const rx::vk::Error &error); std::ostream &operator<<(std::ostream &stream, const rx::vk::Error &error);
// Introduce a std::hash for a RenderPassDesc
namespace std
{
template <>
struct hash<rx::vk::RenderPassDesc>
{
size_t operator()(const rx::vk::RenderPassDesc &key) const { return key.hash(); }
};
} // namespace std
#endif // LIBANGLE_RENDERER_VULKAN_RENDERERVK_UTILS_H_ #endif // LIBANGLE_RENDERER_VULKAN_RENDERERVK_UTILS_H_
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment