Commit f9dd2c15 by Jamie Madill Committed by Commit Bot

Vulkan: Accumulate Buffer barriers.

Uses an unordered_map in the CommandBufferHelper to track buffer reads and writes. Buffer barriers are tracked specially in the CommandBufferHelper class as a barrier we execute immediately when we execute the commands into the primary. So when we run into an incompatible buffer access we must start a new command buffer. The rules for an incompatible access are: - when we are reading a buffer, any prior write in the same command buffer is incompatible. - when we are writing a buffer, any prior read or write in the same command buffer is incopatible. Also adds a regression test using a new performance counter. Bug: angleproject:4429 Change-Id: I393a4ed87314f955eb998940b877ba76ea15a7b8 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/2334091Reviewed-by: 's avatarTim Van Patten <timvp@google.com> Reviewed-by: 's avatarShahbaz Youssefi <syoussefi@chromium.org> Reviewed-by: 's avatarCharlie Lao <cclao@google.com> Commit-Queue: Jamie Madill <jmadill@chromium.org>
parent 18dd0c28
...@@ -252,31 +252,40 @@ angle::Result BufferVk::copySubData(const gl::Context *context, ...@@ -252,31 +252,40 @@ angle::Result BufferVk::copySubData(const gl::Context *context,
{ {
ASSERT(mBuffer && mBuffer->valid()); ASSERT(mBuffer && mBuffer->valid());
ContextVk *contextVk = vk::GetImpl(context); ContextVk *contextVk = vk::GetImpl(context);
auto *sourceBuffer = GetAs<BufferVk>(source); BufferVk *sourceVk = GetAs<BufferVk>(source);
ASSERT(sourceBuffer->getBuffer().valid()); vk::BufferHelper &sourceBuffer = sourceVk->getBuffer();
ASSERT(sourceBuffer.valid());
// If the shadow buffer is enabled for the destination buffer then // If the shadow buffer is enabled for the destination buffer then
// we need to update that as well. This will require us to complete // we need to update that as well. This will require us to complete
// all recorded and in-flight commands involving the source buffer. // all recorded and in-flight commands involving the source buffer.
if (mShadowBuffer.valid()) if (mShadowBuffer.valid())
{ {
ANGLE_TRY(sourceBuffer->getBuffer().waitForIdle(contextVk)); ANGLE_TRY(sourceBuffer.waitForIdle(contextVk));
// Update the shadow buffer // Update the shadow buffer
uint8_t *srcPtr; uint8_t *srcPtr;
ANGLE_TRY(sourceBuffer->getBuffer().mapWithOffset(contextVk, &srcPtr, sourceOffset)); ANGLE_TRY(sourceBuffer.mapWithOffset(contextVk, &srcPtr, sourceOffset));
updateShadowBuffer(srcPtr, size, destOffset); updateShadowBuffer(srcPtr, size, destOffset);
// Unmap the source buffer // Unmap the source buffer
sourceBuffer->getBuffer().unmap(contextVk->getRenderer()); sourceBuffer.unmap(contextVk->getRenderer());
} }
vk::CommandBuffer *commandBuffer = nullptr; vk::CommandBuffer *commandBuffer = nullptr;
ANGLE_TRY(contextVk->onBufferTransferRead(&sourceBuffer->getBuffer())); // Check for self-dependency.
ANGLE_TRY(contextVk->onBufferTransferWrite(mBuffer)); if (sourceBuffer.getBufferSerial() == mBuffer->getBufferSerial())
{
ANGLE_TRY(contextVk->onBufferSelfCopy(mBuffer));
}
else
{
ANGLE_TRY(contextVk->onBufferTransferRead(&sourceBuffer));
ANGLE_TRY(contextVk->onBufferTransferWrite(mBuffer));
}
ANGLE_TRY(contextVk->endRenderPassAndGetCommandBuffer(&commandBuffer)); ANGLE_TRY(contextVk->endRenderPassAndGetCommandBuffer(&commandBuffer));
// Enqueue a copy command on the GPU. // Enqueue a copy command on the GPU.
...@@ -284,8 +293,7 @@ angle::Result BufferVk::copySubData(const gl::Context *context, ...@@ -284,8 +293,7 @@ angle::Result BufferVk::copySubData(const gl::Context *context,
static_cast<VkDeviceSize>(destOffset), static_cast<VkDeviceSize>(destOffset),
static_cast<VkDeviceSize>(size)}; static_cast<VkDeviceSize>(size)};
commandBuffer->copyBuffer(sourceBuffer->getBuffer().getBuffer(), mBuffer->getBuffer(), 1, commandBuffer->copyBuffer(sourceBuffer.getBuffer(), mBuffer->getBuffer(), 1, &copyRegion);
&copyRegion);
// The new destination buffer data may require a conversion for the next draw, so mark it dirty. // The new destination buffer data may require a conversion for the next draw, so mark it dirty.
onDataChanged(); onDataChanged();
......
...@@ -1521,7 +1521,8 @@ angle::Result ContextVk::handleDirtyGraphicsTransformFeedbackBuffersEmulation( ...@@ -1521,7 +1521,8 @@ angle::Result ContextVk::handleDirtyGraphicsTransformFeedbackBuffersEmulation(
vk::BufferHelper *bufferHelper = bufferHelpers[bufferIndex]; vk::BufferHelper *bufferHelper = bufferHelpers[bufferIndex];
ASSERT(bufferHelper); ASSERT(bufferHelper);
mRenderPassCommands->bufferWrite(&mResourceUseList, VK_ACCESS_SHADER_WRITE_BIT, mRenderPassCommands->bufferWrite(&mResourceUseList, VK_ACCESS_SHADER_WRITE_BIT,
vk::PipelineStage::VertexShader, bufferHelper); vk::PipelineStage::VertexShader,
vk::BufferAliasingMode::Disallowed, bufferHelper);
} }
// TODO(http://anglebug.com/3570): Need to update to handle Program Pipelines // TODO(http://anglebug.com/3570): Need to update to handle Program Pipelines
...@@ -1556,9 +1557,9 @@ angle::Result ContextVk::handleDirtyGraphicsTransformFeedbackBuffersExtension( ...@@ -1556,9 +1557,9 @@ angle::Result ContextVk::handleDirtyGraphicsTransformFeedbackBuffersExtension(
{ {
vk::BufferHelper *bufferHelper = bufferHelpers[bufferIndex]; vk::BufferHelper *bufferHelper = bufferHelpers[bufferIndex];
ASSERT(bufferHelper); ASSERT(bufferHelper);
mRenderPassCommands->bufferWrite(&mResourceUseList, mRenderPassCommands->bufferWrite(
VK_ACCESS_TRANSFORM_FEEDBACK_WRITE_BIT_EXT, &mResourceUseList, VK_ACCESS_TRANSFORM_FEEDBACK_WRITE_BIT_EXT,
vk::PipelineStage::TransformFeedback, bufferHelper); vk::PipelineStage::TransformFeedback, vk::BufferAliasingMode::Disallowed, bufferHelper);
} }
const gl::TransformFeedbackBuffersArray<VkBuffer> &bufferHandles = const gl::TransformFeedbackBuffersArray<VkBuffer> &bufferHandles =
...@@ -3385,7 +3386,9 @@ angle::Result ContextVk::onBeginTransformFeedback( ...@@ -3385,7 +3386,9 @@ angle::Result ContextVk::onBeginTransformFeedback(
for (size_t bufferIndex = 0; bufferIndex < bufferCount; ++bufferIndex) for (size_t bufferIndex = 0; bufferIndex < bufferCount; ++bufferIndex)
{ {
if (mCurrentTransformFeedbackBuffers.count(buffers[bufferIndex]) != 0) const vk::BufferHelper *buffer = buffers[bufferIndex];
if (mCurrentTransformFeedbackBuffers.count(buffer) != 0 ||
mRenderPassCommands->usesBuffer(*buffer))
{ {
ANGLE_TRY(endRenderPass()); ANGLE_TRY(endRenderPass());
break; break;
...@@ -4322,7 +4325,8 @@ angle::Result ContextVk::onBufferRead(VkAccessFlags readAccessType, ...@@ -4322,7 +4325,8 @@ angle::Result ContextVk::onBufferRead(VkAccessFlags readAccessType,
ANGLE_TRY(endRenderPass()); ANGLE_TRY(endRenderPass());
if (!buffer->canAccumulateRead(this, readAccessType)) // A current write access means we need to start a new command buffer.
if (mOutsideRenderPassCommands->usesBufferForWrite(*buffer))
{ {
ANGLE_TRY(flushOutsideRenderPassCommands()); ANGLE_TRY(flushOutsideRenderPassCommands());
} }
...@@ -4340,12 +4344,14 @@ angle::Result ContextVk::onBufferWrite(VkAccessFlags writeAccessType, ...@@ -4340,12 +4344,14 @@ angle::Result ContextVk::onBufferWrite(VkAccessFlags writeAccessType,
ANGLE_TRY(endRenderPass()); ANGLE_TRY(endRenderPass());
if (!buffer->canAccumulateWrite(this, writeAccessType)) // Any current access means we need to start a new command buffer.
if (mOutsideRenderPassCommands->usesBuffer(*buffer))
{ {
ANGLE_TRY(flushOutsideRenderPassCommands()); ANGLE_TRY(flushOutsideRenderPassCommands());
} }
mOutsideRenderPassCommands->bufferWrite(&mResourceUseList, writeAccessType, writeStage, buffer); mOutsideRenderPassCommands->bufferWrite(&mResourceUseList, writeAccessType, writeStage,
vk::BufferAliasingMode::Disallowed, buffer);
return angle::Result::Continue; return angle::Result::Continue;
} }
......
...@@ -494,6 +494,11 @@ class ContextVk : public ContextImpl, public vk::Context ...@@ -494,6 +494,11 @@ class ContextVk : public ContextImpl, public vk::Context
{ {
return onBufferWrite(VK_ACCESS_TRANSFER_WRITE_BIT, vk::PipelineStage::Transfer, buffer); return onBufferWrite(VK_ACCESS_TRANSFER_WRITE_BIT, vk::PipelineStage::Transfer, buffer);
} }
angle::Result onBufferSelfCopy(vk::BufferHelper *buffer)
{
return onBufferWrite(VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
vk::PipelineStage::Transfer, buffer);
}
angle::Result onBufferComputeShaderRead(vk::BufferHelper *buffer) angle::Result onBufferComputeShaderRead(vk::BufferHelper *buffer)
{ {
return onBufferRead(VK_ACCESS_SHADER_READ_BIT, vk::PipelineStage::ComputeShader, buffer); return onBufferRead(VK_ACCESS_SHADER_READ_BIT, vk::PipelineStage::ComputeShader, buffer);
......
...@@ -985,7 +985,8 @@ void ProgramExecutableVk::updateBuffersDescriptorSet(ContextVk *contextVk, ...@@ -985,7 +985,8 @@ void ProgramExecutableVk::updateBuffersDescriptorSet(ContextVk *contextVk,
// We set the SHADER_READ_BIT to be conservative. // We set the SHADER_READ_BIT to be conservative.
VkAccessFlags accessFlags = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; VkAccessFlags accessFlags = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
commandBufferHelper->bufferWrite(resourceUseList, accessFlags, commandBufferHelper->bufferWrite(resourceUseList, accessFlags,
kPipelineStageShaderMap[shaderType], &bufferHelper); kPipelineStageShaderMap[shaderType],
vk::BufferAliasingMode::Allowed, &bufferHelper);
} }
else else
{ {
...@@ -1053,9 +1054,9 @@ void ProgramExecutableVk::updateAtomicCounterBuffersDescriptorSet( ...@@ -1053,9 +1054,9 @@ void ProgramExecutableVk::updateAtomicCounterBuffersDescriptorSet(
&writeInfo); &writeInfo);
// We set SHADER_READ_BIT to be conservative. // We set SHADER_READ_BIT to be conservative.
commandBufferHelper->bufferWrite(resourceUseList, commandBufferHelper->bufferWrite(
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, resourceUseList, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
kPipelineStageShaderMap[shaderType], &bufferHelper); kPipelineStageShaderMap[shaderType], vk::BufferAliasingMode::Allowed, &bufferHelper);
writtenBindings.set(binding); writtenBindings.set(binding);
} }
......
...@@ -557,6 +557,8 @@ CommandBufferHelper::~CommandBufferHelper() ...@@ -557,6 +557,8 @@ CommandBufferHelper::~CommandBufferHelper()
void CommandBufferHelper::initialize(bool isRenderPassCommandBuffer, bool mergeBarriers) void CommandBufferHelper::initialize(bool isRenderPassCommandBuffer, bool mergeBarriers)
{ {
ASSERT(mUsedBuffers.empty());
mAllocator.initialize(kDefaultPoolAllocatorPageSize, 1); mAllocator.initialize(kDefaultPoolAllocatorPageSize, 1);
// Push a scope into the pool allocator so we can easily free and re-init on reset() // Push a scope into the pool allocator so we can easily free and re-init on reset()
mAllocator.push(); mAllocator.push();
...@@ -565,6 +567,17 @@ void CommandBufferHelper::initialize(bool isRenderPassCommandBuffer, bool mergeB ...@@ -565,6 +567,17 @@ void CommandBufferHelper::initialize(bool isRenderPassCommandBuffer, bool mergeB
mMergeBarriers = mergeBarriers; mMergeBarriers = mergeBarriers;
} }
bool CommandBufferHelper::usesBuffer(const BufferHelper &buffer) const
{
return mUsedBuffers.count(buffer.getBufferSerial()) > 0;
}
bool CommandBufferHelper::usesBufferForWrite(const BufferHelper &buffer) const
{
auto iter = mUsedBuffers.find(buffer.getBufferSerial());
return iter != mUsedBuffers.end() && iter->second == BufferAccess::Write;
}
void CommandBufferHelper::bufferRead(vk::ResourceUseList *resourceUseList, void CommandBufferHelper::bufferRead(vk::ResourceUseList *resourceUseList,
VkAccessFlags readAccessType, VkAccessFlags readAccessType,
vk::PipelineStage readStage, vk::PipelineStage readStage,
...@@ -576,11 +589,15 @@ void CommandBufferHelper::bufferRead(vk::ResourceUseList *resourceUseList, ...@@ -576,11 +589,15 @@ void CommandBufferHelper::bufferRead(vk::ResourceUseList *resourceUseList,
{ {
mPipelineBarrierMask.set(readStage); mPipelineBarrierMask.set(readStage);
} }
ASSERT(!usesBufferForWrite(*buffer));
mUsedBuffers[buffer->getBufferSerial()] = BufferAccess::Read;
} }
void CommandBufferHelper::bufferWrite(vk::ResourceUseList *resourceUseList, void CommandBufferHelper::bufferWrite(vk::ResourceUseList *resourceUseList,
VkAccessFlags writeAccessType, VkAccessFlags writeAccessType,
vk::PipelineStage writeStage, vk::PipelineStage writeStage,
BufferAliasingMode aliasingMode,
vk::BufferHelper *buffer) vk::BufferHelper *buffer)
{ {
buffer->retain(resourceUseList); buffer->retain(resourceUseList);
...@@ -589,6 +606,16 @@ void CommandBufferHelper::bufferWrite(vk::ResourceUseList *resourceUseList, ...@@ -589,6 +606,16 @@ void CommandBufferHelper::bufferWrite(vk::ResourceUseList *resourceUseList,
{ {
mPipelineBarrierMask.set(writeStage); mPipelineBarrierMask.set(writeStage);
} }
// Storage buffers are special. They can alias one another in a shader.
// We support aliasing by not tracking storage buffers. This works well with the GL API
// because storage buffers are required to be externally synchronized.
// Compute / XFB emulation buffers are not allowed to alias.
if (aliasingMode == BufferAliasingMode::Disallowed)
{
ASSERT(!usesBuffer(*buffer));
mUsedBuffers[buffer->getBufferSerial()] = BufferAccess::Write;
}
} }
void CommandBufferHelper::imageRead(vk::ResourceUseList *resourceUseList, void CommandBufferHelper::imageRead(vk::ResourceUseList *resourceUseList,
...@@ -884,6 +911,8 @@ void CommandBufferHelper::reset() ...@@ -884,6 +911,8 @@ void CommandBufferHelper::reset()
mAllocator.pop(); mAllocator.pop();
mAllocator.push(); mAllocator.push();
mCommandBuffer.reset(); mCommandBuffer.reset();
mUsedBuffers.clear();
if (mIsRenderPassCommandBuffer) if (mIsRenderPassCommandBuffer)
{ {
mRenderPassStarted = false; mRenderPassStarted = false;
...@@ -2490,22 +2519,6 @@ bool BufferHelper::isReleasedToExternal() const ...@@ -2490,22 +2519,6 @@ bool BufferHelper::isReleasedToExternal() const
#endif #endif
} }
bool BufferHelper::canAccumulateRead(ContextVk *contextVk, VkAccessFlags readAccessType)
{
// We only need to start a new command buffer when we need a new barrier.
// For simplicity's sake for now we always start a new command buffer.
// TODO(jmadill): Re-use the command buffer. http://anglebug.com/4429
return false;
}
bool BufferHelper::canAccumulateWrite(ContextVk *contextVk, VkAccessFlags writeAccessType)
{
// We only need to start a new command buffer when we need a new barrier.
// For simplicity's sake for now we always start a new command buffer.
// TODO(jmadill): Re-use the command buffer. http://anglebug.com/4429
return false;
}
bool BufferHelper::updateReadBarrier(VkAccessFlags readAccessType, bool BufferHelper::updateReadBarrier(VkAccessFlags readAccessType,
VkPipelineStageFlags readStage, VkPipelineStageFlags readStage,
PipelineBarrier *barrier) PipelineBarrier *barrier)
......
...@@ -798,10 +798,6 @@ class BufferHelper final : public Resource ...@@ -798,10 +798,6 @@ class BufferHelper final : public Resource
// Returns true if the image is owned by an external API or instance. // Returns true if the image is owned by an external API or instance.
bool isReleasedToExternal() const; bool isReleasedToExternal() const;
// Currently always returns false. Should be smarter about accumulation.
bool canAccumulateRead(ContextVk *contextVk, VkAccessFlags readAccessType);
bool canAccumulateWrite(ContextVk *contextVk, VkAccessFlags writeAccessType);
bool updateReadBarrier(VkAccessFlags readAccessType, bool updateReadBarrier(VkAccessFlags readAccessType,
VkPipelineStageFlags readStage, VkPipelineStageFlags readStage,
PipelineBarrier *barrier); PipelineBarrier *barrier);
...@@ -835,6 +831,18 @@ class BufferHelper final : public Resource ...@@ -835,6 +831,18 @@ class BufferHelper final : public Resource
BufferSerial mSerial; BufferSerial mSerial;
}; };
enum class BufferAccess
{
Read,
Write,
};
enum class BufferAliasingMode
{
Allowed,
Disallowed,
};
// CommandBufferHelper (CBH) class wraps ANGLE's custom command buffer // CommandBufferHelper (CBH) class wraps ANGLE's custom command buffer
// class, SecondaryCommandBuffer. This provides a way to temporarily // class, SecondaryCommandBuffer. This provides a way to temporarily
// store Vulkan commands that be can submitted in-line to a primary // store Vulkan commands that be can submitted in-line to a primary
...@@ -859,6 +867,7 @@ struct CommandBufferHelper : angle::NonCopyable ...@@ -859,6 +867,7 @@ struct CommandBufferHelper : angle::NonCopyable
void bufferWrite(vk::ResourceUseList *resourceUseList, void bufferWrite(vk::ResourceUseList *resourceUseList,
VkAccessFlags writeAccessType, VkAccessFlags writeAccessType,
vk::PipelineStage writeStage, vk::PipelineStage writeStage,
BufferAliasingMode aliasingMode,
vk::BufferHelper *buffer); vk::BufferHelper *buffer);
void imageRead(vk::ResourceUseList *resourceUseList, void imageRead(vk::ResourceUseList *resourceUseList,
...@@ -960,6 +969,9 @@ struct CommandBufferHelper : angle::NonCopyable ...@@ -960,6 +969,9 @@ struct CommandBufferHelper : angle::NonCopyable
return mFramebuffer.getHandle(); return mFramebuffer.getHandle();
} }
bool usesBuffer(const BufferHelper &buffer) const;
bool usesBufferForWrite(const BufferHelper &buffer) const;
// Dumping the command stream is disabled by default. // Dumping the command stream is disabled by default.
static constexpr bool kEnableCommandStreamDiagnostics = false; static constexpr bool kEnableCommandStreamDiagnostics = false;
...@@ -997,7 +1009,11 @@ struct CommandBufferHelper : angle::NonCopyable ...@@ -997,7 +1009,11 @@ struct CommandBufferHelper : angle::NonCopyable
bool mDepthTestEverEnabled; bool mDepthTestEverEnabled;
bool mStencilTestEverEnabled; bool mStencilTestEverEnabled;
uint32_t mDepthStencilAttachmentIndex; uint32_t mDepthStencilAttachmentIndex;
// Tracks resources used in the command buffer.
std::unordered_map<BufferSerial, BufferAccess> mUsedBuffers;
}; };
static constexpr uint32_t kInvalidAttachmentIndex = -1; static constexpr uint32_t kInvalidAttachmentIndex = -1;
// Imagine an image going through a few layout transitions: // Imagine an image going through a few layout transitions:
......
...@@ -140,6 +140,50 @@ TEST_P(VulkanPerformanceCounterTest, ChangingMaxLevelHitsDescriptorCache) ...@@ -140,6 +140,50 @@ TEST_P(VulkanPerformanceCounterTest, ChangingMaxLevelHitsDescriptorCache)
EXPECT_EQ(expectedWriteDescriptorSetCount, actualWriteDescriptorSetCount); EXPECT_EQ(expectedWriteDescriptorSetCount, actualWriteDescriptorSetCount);
} }
// Tests that two glCopyBufferSubData commands can share a barrier.
TEST_P(VulkanPerformanceCounterTest, IndependentBufferCopiesShareSingleBarrier)
{
constexpr GLint srcDataA[] = {1, 2, 3, 4};
constexpr GLint srcDataB[] = {5, 6, 7, 8};
// Step 1: Set up four buffers for two copies.
GLBuffer srcA;
glBindBuffer(GL_COPY_READ_BUFFER, srcA);
glBufferData(GL_COPY_READ_BUFFER, sizeof(srcDataA), srcDataA, GL_STATIC_COPY);
GLBuffer dstA;
glBindBuffer(GL_COPY_WRITE_BUFFER, dstA);
glBufferData(GL_COPY_WRITE_BUFFER, sizeof(srcDataA[0]) * 2, nullptr, GL_STATIC_COPY);
GLBuffer srcB;
glBindBuffer(GL_COPY_READ_BUFFER, srcB);
glBufferData(GL_COPY_READ_BUFFER, sizeof(srcDataB), srcDataB, GL_STATIC_COPY);
GLBuffer dstB;
glBindBuffer(GL_COPY_WRITE_BUFFER, dstB);
glBufferData(GL_COPY_WRITE_BUFFER, sizeof(srcDataB[0]) * 2, nullptr, GL_STATIC_COPY);
// We expect that ANGLE generate zero additional command buffers.
const rx::vk::PerfCounters &counters = hackANGLE();
uint32_t expectedFlushCount = counters.flushedOutsideRenderPassCommandBuffers;
// Step 2: Do the two copies.
glBindBuffer(GL_COPY_READ_BUFFER, srcA);
glBindBuffer(GL_COPY_WRITE_BUFFER, dstA);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, sizeof(srcDataB[0]), 0,
sizeof(srcDataA[0]) * 2);
glBindBuffer(GL_COPY_READ_BUFFER, srcB);
glBindBuffer(GL_COPY_WRITE_BUFFER, dstB);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, sizeof(srcDataB[0]), 0,
sizeof(srcDataB[0]) * 2);
ASSERT_GL_NO_ERROR();
uint32_t actualFlushCount = counters.flushedOutsideRenderPassCommandBuffers;
EXPECT_EQ(expectedFlushCount, actualFlushCount);
}
ANGLE_INSTANTIATE_TEST(VulkanPerformanceCounterTest, ES3_VULKAN()); ANGLE_INSTANTIATE_TEST(VulkanPerformanceCounterTest, ES3_VULKAN());
} // anonymous namespace } // anonymous namespace
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