From 2d4fc9bf47ad8623c43085ea220351ae1af00304 Mon Sep 17 00:00:00 2001 From: Rik Cabanier Date: Fri, 1 May 2026 20:24:47 -0700 Subject: [PATCH] Vulkan/Qualcomm: temp resolve textures from `AlwaysResolveIntoZeroLevelAndLayer` can accumulate and OOM On Qualcomm Vulkan, Dawn's non-zero-array-layer resolve workaround allocates per-pass temporary resolve textures, and their lifetime can extend longer than the submit that used them, causing avoidable memory growth and eventual OOM in workloads like WebXR. `AlwaysResolveIntoZeroLevelAndLayer` exists because Qualcomm Vulkan has a bug resolving into a non-zero mip/layer of an array texture: - `src/dawn/native/vulkan/PhysicalDeviceVk.cpp` - `src/dawn/native/Toggles.cpp` In that path, `RenderPassWorkaroundsHelper` allocates a temporary single-layer 2D resolve texture, resolves into it, and then copies into the real destination. The issue is that these temporary resolve textures can live longer than the submit that actually used them. In workloads that hit this path every frame, this causes unnecessary Vulkan image memory growth and can eventually OOM. I hit this with a WebXR/WebGPU workload on Quest where the XR color target is a 2-layer array texture and the right eye resolve targets `baseArrayLayer = 1`, which reliably triggers this workaround every frame. There is also a separate Qualcomm workaround for empty resolve passes (`VulkanAddWorkToEmptyResolvePass`), but that is not the memory issue here. The memory issue is the lifetime of the temporary resolve texture used by `AlwaysResolveIntoZeroLevelAndLayer`. Proposed fix: track these workaround textures on the command buffer and explicitly `Destroy()` them immediately after `vkQueueSubmit`, before the queue serial advances, so their fenced deletion is tied to the submit that used them rather than to later command-buffer/object teardown timing. --- src/dawn/native/CommandBuffer.cpp | 17 +++++++++++++++ src/dawn/native/CommandBuffer.h | 3 +++ src/dawn/native/CommandEncoder.cpp | 8 +++++++ src/dawn/native/CommandEncoder.h | 3 +++ .../native/RenderPassWorkaroundsHelper.cpp | 1 + src/dawn/native/vulkan/QueueVk.cpp | 21 +++++++++++++++++-- src/dawn/native/vulkan/QueueVk.h | 1 + 7 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/dawn/native/CommandBuffer.cpp b/src/dawn/native/CommandBuffer.cpp index fd7d73473b..ea4a620d43 100644 --- a/src/dawn/native/CommandBuffer.cpp +++ b/src/dawn/native/CommandBuffer.cpp @@ -25,6 +25,8 @@ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#include + #include "dawn/native/CommandBuffer.h" #include "dawn/native/Buffer.h" @@ -45,6 +47,7 @@ CommandBufferBase::CommandBufferBase(CommandEncoder* encoder, mCommands(encoder->AcquireCommands()), mResourceUsages(encoder->AcquireResourceUsages()), mIndirectDrawMetadata(encoder->AcquireIndirectDrawMetadata()), + mTemporaryTexturesForEarlyDestroy(encoder->AcquireTemporaryTexturesForEarlyDestroy()), mEncoderLabel(encoder->GetLabel()) { GetObjectTrackingList()->Track(this); } @@ -105,6 +108,20 @@ const ityp::vector& CommandBufferBase::GetIndir return mIndirectDrawMetadata; } +void CommandBufferBase::ExtractTemporaryTexturesForEarlyDestroy( + std::vector>* temporaryTexturesForEarlyDestroy) { + if (mTemporaryTexturesForEarlyDestroy.empty()) { + return; + } + + temporaryTexturesForEarlyDestroy->insert(temporaryTexturesForEarlyDestroy->end(), + std::make_move_iterator( + mTemporaryTexturesForEarlyDestroy.begin()), + std::make_move_iterator( + mTemporaryTexturesForEarlyDestroy.end())); + mTemporaryTexturesForEarlyDestroy.clear(); +} + CommandIterator* CommandBufferBase::GetCommandIteratorForTesting() { return &mCommands; } diff --git a/src/dawn/native/CommandBuffer.h b/src/dawn/native/CommandBuffer.h index 3f7be31940..6a1b81e0d1 100644 --- a/src/dawn/native/CommandBuffer.h +++ b/src/dawn/native/CommandBuffer.h @@ -68,6 +68,8 @@ class CommandBufferBase : public ApiObjectBase { const CommandBufferResourceUsage& GetResourceUsages() const; const ityp::vector& GetIndirectDrawMetadata(); + void ExtractTemporaryTexturesForEarlyDestroy( + std::vector>* temporaryTexturesForEarlyDestroy); CommandIterator* GetCommandIteratorForTesting(); @@ -81,6 +83,7 @@ class CommandBufferBase : public ApiObjectBase { CommandBufferResourceUsage mResourceUsages; ityp::vector mIndirectDrawMetadata; + std::vector> mTemporaryTexturesForEarlyDestroy; std::string mEncoderLabel; }; diff --git a/src/dawn/native/CommandEncoder.cpp b/src/dawn/native/CommandEncoder.cpp index 3f653c1568..525752656b 100644 --- a/src/dawn/native/CommandEncoder.cpp +++ b/src/dawn/native/CommandEncoder.cpp @@ -1278,10 +1278,18 @@ CommandIterator CommandEncoder::AcquireCommands() { return mEncodingContext.AcquireCommands(); } +std::vector> CommandEncoder::AcquireTemporaryTexturesForEarlyDestroy() { + return std::move(mTemporaryTexturesForEarlyDestroy); +} + void CommandEncoder::TrackUsedQuerySet(QuerySetBase* querySet) { mUsedQuerySets.insert(querySet); } +void CommandEncoder::TrackTemporaryTextureForEarlyDestroy(Ref texture) { + mTemporaryTexturesForEarlyDestroy.push_back(std::move(texture)); +} + ityp::vector CommandEncoder::AcquireIndirectDrawMetadata() { return mEncodingContext.AcquireIndirectDrawMetadata(); } diff --git a/src/dawn/native/CommandEncoder.h b/src/dawn/native/CommandEncoder.h index 0712e8a209..0e447a927b 100644 --- a/src/dawn/native/CommandEncoder.h +++ b/src/dawn/native/CommandEncoder.h @@ -62,8 +62,10 @@ class CommandEncoder final : public ApiObjectBase { CommandIterator AcquireCommands(); CommandBufferResourceUsage AcquireResourceUsages(); ityp::vector AcquireIndirectDrawMetadata(); + std::vector> AcquireTemporaryTexturesForEarlyDestroy(); void TrackUsedQuerySet(QuerySetBase* querySet); + void TrackTemporaryTextureForEarlyDestroy(Ref texture); // Dawn API ComputePassEncoder* APIBeginComputePass(const ComputePassDescriptor* descriptor); @@ -144,6 +146,7 @@ class CommandEncoder final : public ApiObjectBase { absl::flat_hash_set mTopLevelBuffers; absl::flat_hash_set mTopLevelTextures; absl::flat_hash_set mUsedQuerySets; + std::vector> mTemporaryTexturesForEarlyDestroy; uint64_t mDebugGroupStackSize = 0; diff --git a/src/dawn/native/RenderPassWorkaroundsHelper.cpp b/src/dawn/native/RenderPassWorkaroundsHelper.cpp index 4b4165b342..c047e04a73 100644 --- a/src/dawn/native/RenderPassWorkaroundsHelper.cpp +++ b/src/dawn/native/RenderPassWorkaroundsHelper.cpp @@ -151,6 +151,7 @@ MaybeError RenderPassWorkaroundsHelper::Initialize( DAWN_ASSERT(device->IsLockedByCurrentThreadIfNeeded()); DAWN_TRY_ASSIGN(temporaryResolveTexture, device->CreateTexture(&descriptor)); + encoder->TrackTemporaryTextureForEarlyDestroy(temporaryResolveTexture); TextureViewDescriptor viewDescriptor = {}; DAWN_TRY_ASSIGN( diff --git a/src/dawn/native/vulkan/QueueVk.cpp b/src/dawn/native/vulkan/QueueVk.cpp index 82d6d608f0..d971a840ad 100644 --- a/src/dawn/native/vulkan/QueueVk.cpp +++ b/src/dawn/native/vulkan/QueueVk.cpp @@ -103,13 +103,23 @@ MaybeError Queue::Initialize() { MaybeError Queue::SubmitImpl(uint32_t commandCount, CommandBufferBase* const* commands) { TRACE_EVENT_BEGIN0(GetDevice()->GetPlatform(), Recording, "CommandBufferVk::RecordCommands"); + DAWN_ASSERT(mPendingTexturesToDestroyOnSubmit.empty()); CommandRecordingContext* recordingContext = GetPendingRecordingContext(); for (uint32_t i = 0; i < commandCount; ++i) { - DAWN_TRY(ToBackend(commands[i])->RecordCommands(recordingContext)); + MaybeError recordResult = ToBackend(commands[i])->RecordCommands(recordingContext); + if (recordResult.IsError()) { + mPendingTexturesToDestroyOnSubmit.clear(); + return recordResult; + } + commands[i]->ExtractTemporaryTexturesForEarlyDestroy(&mPendingTexturesToDestroyOnSubmit); } TRACE_EVENT_END0(GetDevice()->GetPlatform(), Recording, "CommandBufferVk::RecordCommands"); - DAWN_TRY(SubmitPendingCommandsImpl()); + MaybeError submitResult = SubmitPendingCommandsImpl(); + if (submitResult.IsError()) { + mPendingTexturesToDestroyOnSubmit.clear(); + return submitResult; + } return {}; } @@ -346,6 +356,13 @@ MaybeError Queue::SubmitPendingCommandsImpl() { }); TRACE_EVENT_END0(device->GetPlatform(), Recording, "vkQueueSubmit"); + // Destroy temporary resolve textures while the pending serial still refers to this submit. + // Otherwise command-buffer teardown schedules their Vulkan memory release on the next serial. + for (auto& texture : mPendingTexturesToDestroyOnSubmit) { + texture->Destroy(); + } + mPendingTexturesToDestroyOnSubmit.clear(); + // Enqueue the semaphores before incrementing the serial, so that they can be deleted as // soon as the current submission is finished. for (VkSemaphore semaphore : mRecordingContext.waitSemaphores) { diff --git a/src/dawn/native/vulkan/QueueVk.h b/src/dawn/native/vulkan/QueueVk.h index 96a5b64766..0966374dc3 100644 --- a/src/dawn/native/vulkan/QueueVk.h +++ b/src/dawn/native/vulkan/QueueVk.h @@ -92,6 +92,7 @@ class Queue final : public QueueBase { MutexProtected> mUnusedCommands; // There is always a valid recording context stored in mRecordingContext CommandRecordingContext mRecordingContext; + std::vector> mPendingTexturesToDestroyOnSubmit; uint32_t mQueueFamily = 0; VkQueue mQueue = VK_NULL_HANDLE;