Skip to content

Commit 9d074c1

Browse files
committed
GPU Vulkan Backend: Remove unnecessary semaphores and rely on barriers
1 parent 34045c1 commit 9d074c1

2 files changed

Lines changed: 40 additions & 90 deletions

File tree

‎GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx‎

Lines changed: 39 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -558,19 +558,11 @@ void GPUDisplayBackendVulkan::createSemaphoresAndFences()
558558
vk::FenceCreateInfo fenceInfo{};
559559
fenceInfo.flags = vk::FenceCreateFlagBits::eSignaled;
560560
mImageAvailableSemaphore.resize(mFramesInFlight);
561-
mRenderFinishedSemaphore.resize(mFramesInFlight);
562-
mTextFinishedSemaphore.resize(mFramesInFlight);
563-
mMixFinishedSemaphore.resize(mFramesInFlight);
564-
mDownsampleFinishedSemaphore.resize(mFramesInFlight);
565561
mPresentSemaphore.resize(mImageCount);
566562
mInFlightFence.resize(mFramesInFlight);
567563
mImageFence.resize(mImageCount);
568564
for (uint32_t i = 0; i < mFramesInFlight; i++) {
569565
mImageAvailableSemaphore[i] = mDevice.createSemaphore(semaphoreInfo, nullptr);
570-
mRenderFinishedSemaphore[i] = mDevice.createSemaphore(semaphoreInfo, nullptr);
571-
mTextFinishedSemaphore[i] = mDevice.createSemaphore(semaphoreInfo, nullptr);
572-
mMixFinishedSemaphore[i] = mDevice.createSemaphore(semaphoreInfo, nullptr);
573-
mDownsampleFinishedSemaphore[i] = mDevice.createSemaphore(semaphoreInfo, nullptr);
574566
mInFlightFence[i] = mDevice.createFence(fenceInfo, nullptr);
575567
}
576568
for (uint32_t i = 0; i < mImageCount; i++) {
@@ -584,10 +576,6 @@ void GPUDisplayBackendVulkan::createSemaphoresAndFences()
584576
void GPUDisplayBackendVulkan::clearSemaphoresAndFences()
585577
{
586578
clearVector(mImageAvailableSemaphore, [&](auto& x) { mDevice.destroySemaphore(x, nullptr); });
587-
clearVector(mRenderFinishedSemaphore, [&](auto& x) { mDevice.destroySemaphore(x, nullptr); });
588-
clearVector(mTextFinishedSemaphore, [&](auto& x) { mDevice.destroySemaphore(x, nullptr); });
589-
clearVector(mMixFinishedSemaphore, [&](auto& x) { mDevice.destroySemaphore(x, nullptr); });
590-
clearVector(mDownsampleFinishedSemaphore, [&](auto& x) { mDevice.destroySemaphore(x, nullptr); });
591579
clearVector(mPresentSemaphore, [&](auto& x) { mDevice.destroySemaphore(x, nullptr); });
592580
clearVector(mInFlightFence, [&](auto& x) { mDevice.destroyFence(x, nullptr); });
593581
mDevice.destroyFence(mSingleCommitFence, nullptr);
@@ -861,22 +849,25 @@ void GPUDisplayBackendVulkan::createOffscreenBuffers(bool forScreenshot, bool fo
861849
colorAttachmentRef.attachment = nAttachments++;
862850
colorAttachmentRef.layout = vk::ImageLayout::eColorAttachmentOptimal;
863851
vk::AttachmentReference depthAttachmentRef{};
864-
// depthAttachmentRef.attachment // below
865852
depthAttachmentRef.layout = vk::ImageLayout::eDepthStencilAttachmentOptimal;
866853
vk::AttachmentReference colorAttachmentResolveRef{};
867-
// colorAttachmentResolveRef.attachment // below
868854
colorAttachmentResolveRef.layout = vk::ImageLayout::eColorAttachmentOptimal;
869855
vk::SubpassDescription subpass{};
870856
subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics;
871857
subpass.colorAttachmentCount = 1;
872858
subpass.pColorAttachments = &colorAttachmentRef;
873-
vk::SubpassDependency dependency{};
874-
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
875-
dependency.dstSubpass = 0;
876-
dependency.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests;
877-
dependency.srcAccessMask = {};
878-
dependency.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests;
879-
dependency.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentWrite;
859+
std::array<vk::SubpassDependency, 2> dependencies{};
860+
dependencies[0].srcSubpass = dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
861+
dependencies[0].dstSubpass = 0;
862+
dependencies[0].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests | vk::PipelineStageFlagBits::eTransfer | vk::PipelineStageFlagBits::eFragmentShader; // Includes eColorAttachmentOutput, the wait stage of the image acquire semaphore
863+
dependencies[0].srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentWrite | vk::AccessFlagBits::eTransferWrite;
864+
dependencies[0].dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests;
865+
dependencies[0].dstAccessMask = vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite;
866+
dependencies[1].srcSubpass = 0;
867+
dependencies[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eLateFragmentTests;
868+
dependencies[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentWrite;
869+
dependencies[1].dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eTransfer | vk::PipelineStageFlagBits::eFragmentShader;
870+
dependencies[1].dstAccessMask = vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eTransferRead | vk::AccessFlagBits::eShaderRead;
880871

881872
std::vector<vk::AttachmentDescription> attachments = {colorAttachment};
882873
if (mZActive) {
@@ -895,8 +886,8 @@ void GPUDisplayBackendVulkan::createOffscreenBuffers(bool forScreenshot, bool fo
895886
renderPassInfo.pAttachments = attachments.data();
896887
renderPassInfo.subpassCount = 1;
897888
renderPassInfo.pSubpasses = &subpass;
898-
renderPassInfo.dependencyCount = 1;
899-
renderPassInfo.pDependencies = &dependency;
889+
renderPassInfo.dependencyCount = dependencies.size();
890+
renderPassInfo.pDependencies = dependencies.data();
900891
mRenderPass = mDevice.createRenderPass(renderPassInfo, nullptr);
901892

902893
const uint32_t imageCountWithMixImages = mImageCount * (mMixingSupported ? 2 : 1);
@@ -928,11 +919,6 @@ void GPUDisplayBackendVulkan::createOffscreenBuffers(bool forScreenshot, bool fo
928919
renderPassInfo.pAttachments = &colorAttachment;
929920
subpass.pDepthStencilAttachment = nullptr;
930921
subpass.pResolveAttachments = nullptr;
931-
if (mFramebuffersText.size()) {
932-
dependency.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; // Remove early fragment test
933-
dependency.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
934-
dependency.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; // Remove depth/stencil dependencies
935-
}
936922
colorAttachment.loadOp = vk::AttachmentLoadOp::eLoad; // Don't clear the frame buffer
937923
colorAttachment.initialLayout = vk::ImageLayout::ePresentSrcKHR; // Initial layout is not undefined after 1st pass
938924
colorAttachment.samples = vk::SampleCountFlagBits::e1; // No MSAA for Text
@@ -944,11 +930,6 @@ void GPUDisplayBackendVulkan::createOffscreenBuffers(bool forScreenshot, bool fo
944930
colorAttachment.initialLayout = vk::ImageLayout::eColorAttachmentOptimal;
945931
colorAttachment.finalLayout = mDownsampleFSAA ? vk::ImageLayout::eColorAttachmentOptimal : vk::ImageLayout::ePresentSrcKHR;
946932
}
947-
if (mFramebuffersTexture.size()) {
948-
dependency.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; // Remove early fragment test
949-
dependency.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
950-
dependency.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; // Remove depth/stencil dependencies
951-
}
952933
mRenderPassTexture = mDevice.createRenderPass(renderPassInfo, nullptr);
953934
}
954935

@@ -1553,6 +1534,7 @@ void GPUDisplayBackendVulkan::prepareDraw(const hmm_mat4& proj, const hmm_mat4&
15531534
CHKERR(mDevice.resetFences(1, &mInFlightFence[mCurrentFrame]));
15541535

15551536
mHasDrawnText = false;
1537+
mFrameCommandBuffers.clear();
15561538
const hmm_mat4 modelViewProj = proj * view;
15571539
writeToBuffer(mUniformBuffersMat[0][mCurrentImageIndex], sizeof(modelViewProj), &modelViewProj);
15581540
}
@@ -1562,7 +1544,6 @@ void GPUDisplayBackendVulkan::prepareDraw(const hmm_mat4& proj, const hmm_mat4&
15621544
if (!mCommandBufferUpToDate[mCurrentImageIndex]) {
15631545
startFillCommandBuffer(mCurrentCommandBuffer, mCurrentImageIndex, toMixBuffer);
15641546
}
1565-
mSubmitQueue.clear();
15661547
}
15671548

15681549
void GPUDisplayBackendVulkan::finishDraw(bool doScreenshot, bool toMixBuffer, float includeMixImage)
@@ -1577,73 +1558,46 @@ void GPUDisplayBackendVulkan::finishDraw(bool doScreenshot, bool toMixBuffer, fl
15771558

15781559
void GPUDisplayBackendVulkan::finishFrame(bool doScreenshot, bool toMixBuffer, float includeMixImage)
15791560
{
1580-
vk::Semaphore* stageFinishedSemaphore = &mRenderFinishedSemaphore[mCurrentFrame];
1581-
1582-
vk::SubmitInfo submitInfo{};
1583-
vk::PipelineStageFlags waitStages[] = {vk::PipelineStageFlagBits::eColorAttachmentOutput};
1584-
submitInfo.pWaitSemaphores = includeMixImage > 0.f ? &mRenderFinishedSemaphore[mCurrentFrame] : &mImageAvailableSemaphore[mCurrentFrame];
1585-
submitInfo.waitSemaphoreCount = 1;
1586-
submitInfo.pWaitDstStageMask = waitStages;
1587-
submitInfo.commandBufferCount = 1;
1588-
submitInfo.pCommandBuffers = &mCurrentCommandBuffer;
1589-
submitInfo.signalSemaphoreCount = 1;
1590-
submitInfo.pSignalSemaphores = stageFinishedSemaphore;
1591-
mSubmitQueue.emplace_back(submitInfo);
1592-
1561+
mFrameCommandBuffers.emplace_back(mCurrentCommandBuffer);
15931562
if (!toMixBuffer) {
15941563
if (includeMixImage > 0.f) {
15951564
mixImages(mCommandBuffersTexture[mCurrentImageIndex], includeMixImage);
1596-
submitInfo.pWaitSemaphores = stageFinishedSemaphore;
1597-
waitStages[0] = {vk::PipelineStageFlagBits::eColorAttachmentOutput};
1598-
submitInfo.waitSemaphoreCount = 1;
1599-
submitInfo.pCommandBuffers = &mCommandBuffersTexture[mCurrentImageIndex];
1600-
stageFinishedSemaphore = &mMixFinishedSemaphore[mCurrentFrame];
1601-
submitInfo.pSignalSemaphores = stageFinishedSemaphore;
1602-
mSubmitQueue.emplace_back(submitInfo);
1565+
mFrameCommandBuffers.emplace_back(mCommandBuffersTexture[mCurrentImageIndex]);
16031566
}
1604-
16051567
if (mDownsampleFSAA) {
16061568
downsampleToFramebuffer(mCommandBuffersDownsample[mCurrentImageIndex]);
1607-
submitInfo.pCommandBuffers = &mCommandBuffersDownsample[mCurrentImageIndex];
1608-
submitInfo.pWaitSemaphores = stageFinishedSemaphore;
1609-
waitStages[0] = {vk::PipelineStageFlagBits::eTransfer};
1610-
submitInfo.waitSemaphoreCount = 1;
1611-
stageFinishedSemaphore = &mDownsampleFinishedSemaphore[mCurrentFrame];
1612-
submitInfo.pSignalSemaphores = stageFinishedSemaphore;
1613-
mSubmitQueue.emplace_back(submitInfo);
1569+
mFrameCommandBuffers.emplace_back(mCommandBuffersDownsample[mCurrentImageIndex]);
16141570
}
16151571

1572+
vk::PipelineStageFlags waitStage = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eTransfer;
1573+
vk::SubmitInfo submitInfo{};
1574+
submitInfo.waitSemaphoreCount = 1;
1575+
submitInfo.pWaitSemaphores = &mImageAvailableSemaphore[mCurrentFrame];
1576+
submitInfo.pWaitDstStageMask = &waitStage;
16161577
if (doScreenshot) {
1617-
CHKERR(mGraphicsQueue.submit(mSubmitQueue.size(), mSubmitQueue.data(), VkFence(VK_NULL_HANDLE)));
1578+
submitInfo.commandBufferCount = mFrameCommandBuffers.size();
1579+
submitInfo.pCommandBuffers = mFrameCommandBuffers.data();
1580+
CHKERR(mGraphicsQueue.submit(1, &submitInfo, VkFence(VK_NULL_HANDLE)));
16181581
mDevice.waitIdle();
16191582
if (mDisplay->cfgR().screenshotScaleFactor != 1) {
16201583
readImageToPixels(mDownsampleImages[mCurrentImageIndex].image, vk::ImageLayout::eColorAttachmentOptimal, mScreenshotPixels);
16211584
} else {
16221585
readImageToPixels(mSwapChainImages[mCurrentImageIndex], vk::ImageLayout::ePresentSrcKHR, mScreenshotPixels);
16231586
}
1624-
mSubmitQueue.clear();
1587+
mFrameCommandBuffers.clear();
1588+
submitInfo.waitSemaphoreCount = 0;
16251589
}
1626-
16271590
if (mHasDrawnText) {
1628-
submitInfo.pWaitSemaphores = stageFinishedSemaphore;
1629-
waitStages[0] = {vk::PipelineStageFlagBits::eColorAttachmentOutput};
1630-
submitInfo.waitSemaphoreCount = 1;
1631-
submitInfo.pCommandBuffers = &mCommandBuffersText[mCurrentImageIndex];
1632-
stageFinishedSemaphore = &mTextFinishedSemaphore[mCurrentFrame];
1633-
submitInfo.pSignalSemaphores = stageFinishedSemaphore;
1634-
mSubmitQueue.emplace_back(submitInfo);
1635-
}
1636-
1637-
if (mSubmitQueue.size()) {
1638-
vk::SubmitInfo& submitInfoLast = mSubmitQueue.back();
1639-
vk::PipelineStageFlags presentWaitStage = vk::PipelineStageFlagBits::eAllCommands; // Last commit signals to present
1640-
submitInfoLast.pWaitDstStageMask = &presentWaitStage;
1641-
submitInfoLast.pSignalSemaphores = &mPresentSemaphore[mCurrentImageIndex];
1642-
CHKERR(mGraphicsQueue.submit(mSubmitQueue.size(), mSubmitQueue.data(), mInFlightFence[mCurrentFrame]));
1591+
mFrameCommandBuffers.emplace_back(mCommandBuffersText[mCurrentImageIndex]);
16431592
}
1593+
submitInfo.commandBufferCount = mFrameCommandBuffers.size();
1594+
submitInfo.pCommandBuffers = mFrameCommandBuffers.data();
1595+
submitInfo.signalSemaphoreCount = 1;
1596+
submitInfo.pSignalSemaphores = &mPresentSemaphore[mCurrentImageIndex];
1597+
CHKERR(mGraphicsQueue.submit(1, &submitInfo, mInFlightFence[mCurrentFrame]));
16441598

16451599
vk::PresentInfoKHR presentInfo{};
1646-
presentInfo.waitSemaphoreCount = mSubmitQueue.size() ? 1 : 0;
1600+
presentInfo.waitSemaphoreCount = 1;
16471601
presentInfo.pWaitSemaphores = &mPresentSemaphore[mCurrentImageIndex];
16481602
presentInfo.swapchainCount = 1;
16491603
presentInfo.pSwapchains = &mSwapChain;
@@ -1666,7 +1620,7 @@ void GPUDisplayBackendVulkan::downsampleToFramebuffer(vk::CommandBuffer& command
16661620
commandBuffer.begin(beginInfo);
16671621

16681622
cmdImageMemoryBarrier(commandBuffer, mSwapChainImages[mCurrentImageIndex], {}, vk::AccessFlagBits::eTransferWrite, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer);
1669-
cmdImageMemoryBarrier(commandBuffer, mDownsampleImages[mCurrentImageIndex].image, vk::AccessFlagBits::eMemoryRead, vk::AccessFlagBits::eTransferRead, vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eTransferSrcOptimal, vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer);
1623+
cmdImageMemoryBarrier(commandBuffer, mDownsampleImages[mCurrentImageIndex].image, vk::AccessFlagBits::eColorAttachmentWrite, vk::AccessFlagBits::eTransferRead, vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eTransferSrcOptimal, vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eTransfer);
16701624

16711625
vk::Offset3D blitSizeSrc;
16721626
blitSizeSrc.x = mRenderWidth;
@@ -1685,8 +1639,8 @@ void GPUDisplayBackendVulkan::downsampleToFramebuffer(vk::CommandBuffer& command
16851639
imageBlitRegion.dstOffsets[1] = blitSizeDst;
16861640
commandBuffer.blitImage(mDownsampleImages[mCurrentImageIndex].image, vk::ImageLayout::eTransferSrcOptimal, mSwapChainImages[mCurrentImageIndex], vk::ImageLayout::eTransferDstOptimal, 1, &imageBlitRegion, vk::Filter::eLinear);
16871641

1688-
cmdImageMemoryBarrier(commandBuffer, mSwapChainImages[mCurrentImageIndex], vk::AccessFlagBits::eTransferWrite, vk::AccessFlagBits::eMemoryRead, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::ePresentSrcKHR, vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer);
1689-
cmdImageMemoryBarrier(commandBuffer, mDownsampleImages[mCurrentImageIndex].image, vk::AccessFlagBits::eTransferRead, vk::AccessFlagBits::eMemoryRead, vk::ImageLayout::eUndefined, vk::ImageLayout::eColorAttachmentOptimal, vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer);
1642+
cmdImageMemoryBarrier(commandBuffer, mSwapChainImages[mCurrentImageIndex], vk::AccessFlagBits::eTransferWrite, vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::ePresentSrcKHR, vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eColorAttachmentOutput); // Next: text pass, or present (ordered by the present semaphore)
1643+
cmdImageMemoryBarrier(commandBuffer, mDownsampleImages[mCurrentImageIndex].image, {}, vk::AccessFlagBits::eColorAttachmentWrite, vk::ImageLayout::eUndefined, vk::ImageLayout::eColorAttachmentOptimal, vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eColorAttachmentOutput); // Next frame renders into it again
16901644

16911645
commandBuffer.end();
16921646
}
@@ -1753,7 +1707,7 @@ void GPUDisplayBackendVulkan::mixImages(vk::CommandBuffer commandBuffer, float m
17531707

17541708
vk::Image& image = mDownsampleFSAA ? mDownsampleImages[mCurrentImageIndex + mImageCount].image : mMixImages[mCurrentImageIndex].image;
17551709
vk::ImageLayout srcLayout = mDownsampleFSAA ? vk::ImageLayout::eColorAttachmentOptimal : vk::ImageLayout::ePresentSrcKHR;
1756-
cmdImageMemoryBarrier(commandBuffer, image, {}, vk::AccessFlagBits::eMemoryRead, srcLayout, vk::ImageLayout::eShaderReadOnlyOptimal, vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eFragmentShader);
1710+
cmdImageMemoryBarrier(commandBuffer, image, vk::AccessFlagBits::eColorAttachmentWrite, vk::AccessFlagBits::eShaderRead, srcLayout, vk::ImageLayout::eShaderReadOnlyOptimal, vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eFragmentShader);
17571711

17581712
vk::RenderPassBeginInfo renderPassInfo{};
17591713
renderPassInfo.renderPass = mRenderPassTexture;

‎GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,9 @@ class GPUDisplayBackendVulkan : public GPUDisplayBackend
175175
std::vector<vk::CommandBuffer> mCommandBuffersText;
176176
std::vector<vk::CommandBuffer> mCommandBuffersTexture;
177177
std::vector<vk::CommandBuffer> mCommandBuffersMix;
178-
std::vector<vk::SubmitInfo> mSubmitQueue;
178+
std::vector<vk::CommandBuffer> mFrameCommandBuffers; // All command buffers of the current frame, submitted as one batch, ordered by render pass dependencies and barriers
179179
std::vector<bool> mCommandBufferUpToDate;
180180
std::vector<vk::Semaphore> mImageAvailableSemaphore;
181-
std::vector<vk::Semaphore> mRenderFinishedSemaphore;
182-
std::vector<vk::Semaphore> mTextFinishedSemaphore;
183-
std::vector<vk::Semaphore> mMixFinishedSemaphore;
184-
std::vector<vk::Semaphore> mDownsampleFinishedSemaphore;
185181
std::vector<vk::Semaphore> mPresentSemaphore;
186182
std::vector<vk::Fence> mInFlightFence;
187183
std::vector<vk::Fence> mImageFence;

0 commit comments

Comments
 (0)