vk: Make ExpandResolve work with AlwaysResolveIntoZeroLevelAndLayer.

Previously, ExpandResolveTexture with two subpasses didn't work with
AlwaysResolveIntoZeroLevelAndLayer workaround.
Because this workaround modifies the resolve targets of the
BeginRenderPassCmd before it reaches the Vulkan backend.

This CL makes ExpandResolveTexture work with this workaround by
copying the resolve targets to the temporary resolve targets before
starting the render pass.

Also move all render pass workarounds into one single file.

Bug: 42240662
Change-Id: I5fce0b8fb607bbf1208c1b7c1f8040e4e7a9afeb
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/190820
Reviewed-by: Austin Eng <enga@chromium.org>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: Quyen Le <lehoangquyen@chromium.org>
diff --git a/src/dawn/native/BUILD.gn b/src/dawn/native/BUILD.gn
index f2ea39e..cab023e 100644
--- a/src/dawn/native/BUILD.gn
+++ b/src/dawn/native/BUILD.gn
@@ -344,6 +344,8 @@
     "RenderEncoderBase.h",
     "RenderPassEncoder.cpp",
     "RenderPassEncoder.h",
+    "RenderPassWorkaroundsHelper.cpp",
+    "RenderPassWorkaroundsHelper.h",
     "RenderPipeline.cpp",
     "RenderPipeline.h",
     "ResourceHeap.h",
diff --git a/src/dawn/native/CMakeLists.txt b/src/dawn/native/CMakeLists.txt
index 44714f3..ec833eb 100644
--- a/src/dawn/native/CMakeLists.txt
+++ b/src/dawn/native/CMakeLists.txt
@@ -195,6 +195,8 @@
     "RenderEncoderBase.h"
     "RenderPassEncoder.cpp"
     "RenderPassEncoder.h"
+    "RenderPassWorkaroundsHelper.cpp"
+    "RenderPassWorkaroundsHelper.h"
     "RenderPipeline.cpp"
     "RenderPipeline.h"
     "ResourceHeap.h"
diff --git a/src/dawn/native/CommandEncoder.cpp b/src/dawn/native/CommandEncoder.cpp
index f9eb82f..adcbedc 100644
--- a/src/dawn/native/CommandEncoder.cpp
+++ b/src/dawn/native/CommandEncoder.cpp
@@ -39,7 +39,6 @@
 #include "dawn/native/ApplyClearColorValueWithDrawHelper.h"
 #include "dawn/native/BindGroup.h"
 #include "dawn/native/BlitBufferToDepthStencil.h"
-#include "dawn/native/BlitColorToColorWithDraw.h"
 #include "dawn/native/BlitDepthToDepth.h"
 #include "dawn/native/BlitTextureToBuffer.h"
 #include "dawn/native/Buffer.h"
@@ -56,6 +55,7 @@
 #include "dawn/native/QuerySet.h"
 #include "dawn/native/Queue.h"
 #include "dawn/native/RenderPassEncoder.h"
+#include "dawn/native/RenderPassWorkaroundsHelper.h"
 #include "dawn/native/RenderPipeline.h"
 #include "dawn/native/ValidationUtils_autogen.h"
 #include "dawn/platform/DawnPlatform.h"
@@ -930,36 +930,6 @@
                                                 paramsBuffer.Get());
 }
 
-// Load resolve texture to MSAA attachment if needed.
-MaybeError ApplyExpandResolveTextureLoadOp(DeviceBase* device,
-                                           RenderPassEncoder* renderPassEncoder,
-                                           const RenderPassDescriptor* renderPassDescriptor) {
-    // If backend doesn't support textureLoad on resolve targets, then it should handle the load op
-    // internally.
-    if (!device->CanTextureLoadResolveTargetInTheSameRenderpass()) {
-        return {};
-    }
-
-    // Read implicit resolve texture in fragment shader and copy to the implicit MSAA attachment.
-    return ExpandResolveTextureWithDraw(device, renderPassEncoder, renderPassDescriptor);
-}
-
-// Tracks the temporary resolve attachments used when the AlwaysResolveIntoZeroLevelAndLayer toggle
-// is active so that the results can be copied from the temporary resolve attachment into the
-// intended target after the render pass is complete. Also used by the
-// ResolveMultipleAttachmentInSeparatePasses toggle to track resolves that need to be done in their
-// own passes.
-struct TemporaryResolveAttachment {
-    TemporaryResolveAttachment(Ref<TextureViewBase> src,
-                               Ref<TextureViewBase> dst,
-                               wgpu::StoreOp storeOp = wgpu::StoreOp::Store)
-        : copySrc(std::move(src)), copyDst(std::move(dst)), storeOp(storeOp) {}
-
-    Ref<TextureViewBase> copySrc;
-    Ref<TextureViewBase> copyDst;
-    wgpu::StoreOp storeOp;
-};
-
 bool ShouldUseTextureToBufferBlit(const DeviceBase* device,
                                   const Format& format,
                                   const Aspect& aspect) {
@@ -1203,8 +1173,6 @@
     RenderPassValidationState validationState(
         GetDevice()->IsToggleEnabled(Toggle::AllowUnsafeAPIs));
 
-    std::function<void()> passEndCallback = nullptr;
-
     // Lazy make error function to be called if we error and need to return an error encoder.
     auto MakeError = [&]() {
         return RenderPassEncoder::MakeError(device, this, &mEncodingContext,
@@ -1213,6 +1181,10 @@
 
     UnpackedPtr<RenderPassDescriptor> descriptor;
     ClearWithDrawHelper clearWithDrawHelper;
+    RenderPassWorkaroundsHelper renderpassWorkaroundsHelper;
+
+    std::function<void()> passEndCallback = nullptr;
+
     bool success = mEncodingContext.TryEncode(
         this,
         [&](CommandAllocator* allocator) -> MaybeError {
@@ -1223,6 +1195,7 @@
             DAWN_ASSERT(validationState.IsValidState());
 
             DAWN_TRY(clearWithDrawHelper.Initialize(this, *descriptor));
+            DAWN_TRY(renderpassWorkaroundsHelper.Initialize(this, *descriptor));
 
             mEncodingContext.WillBeginRenderPass();
             BeginRenderPassCmd* cmd =
@@ -1397,8 +1370,8 @@
                 }
             }
 
-            DAWN_TRY_ASSIGN(passEndCallback,
-                            ApplyRenderPassWorkarounds(device, &usageTracker, cmd));
+            DAWN_TRY(renderpassWorkaroundsHelper.ApplyOnPostEncoding(this, &usageTracker, cmd,
+                                                                     &passEndCallback));
 
             return {};
         },
@@ -1413,14 +1386,15 @@
         mEncodingContext.EnterPass(passEncoder.Get());
 
         auto error = [&]() -> MaybeError {
-            if (validationState.WillExpandResolveTexture()) {
-                DAWN_TRY(ApplyExpandResolveTextureLoadOp(device, passEncoder.Get(), *descriptor));
-            }
             // clearWithDrawHelper.Apply() applies clear with draw if clear_color_with_draw or
             // apply_clear_big_integer_color_value_with_draw toggle is enabled, and the render pass
             // attachments need to be cleared.
+            // TODO(341129591): move inside RenderPassWorkaroundsHelper.
             DAWN_TRY(clearWithDrawHelper.Apply(passEncoder.Get()));
 
+            DAWN_TRY(renderpassWorkaroundsHelper.ApplyOnRenderPassStart(passEncoder.Get(),
+                                                                        rawDescriptor));
+
             return {};
         }();
 
@@ -1434,176 +1408,6 @@
     return MakeError();
 }
 
-// This function handles render pass workarounds. Because some cases may require
-// multiple workarounds, it applies any workarounds one by one and calls itself
-// recursively to handle the next workaround if needed.
-ResultOrError<std::function<void()>> CommandEncoder::ApplyRenderPassWorkarounds(
-    DeviceBase* device,
-    RenderPassResourceUsageTracker* usageTracker,
-    BeginRenderPassCmd* cmd,
-    std::function<void()> passEndCallback) {
-    // dawn:1550
-    // Handle toggle ResolveMultipleAttachmentInSeparatePasses. This identifies passes where there
-    // are multiple MSAA color targets and at least one of them has a resolve target. If that's the
-    // case then the resolves are deferred by removing the resolve targets and forcing the storeOp
-    // to Store. After the pass has ended an new pass is recorded for each resolve target that
-    // resolves it separately.
-    if (device->IsToggleEnabled(Toggle::ResolveMultipleAttachmentInSeparatePasses) &&
-        cmd->attachmentState->GetColorAttachmentsMask().count() > 1) {
-        bool splitResolvesIntoSeparatePasses = false;
-
-        // This workaround needs to apply if there are multiple MSAA color targets (checked above)
-        // and at least one resolve target.
-        for (auto i : IterateBitSet(cmd->attachmentState->GetColorAttachmentsMask())) {
-            if (cmd->colorAttachments[i].resolveTarget.Get() != nullptr) {
-                splitResolvesIntoSeparatePasses = true;
-                break;
-            }
-        }
-
-        if (splitResolvesIntoSeparatePasses) {
-            std::vector<TemporaryResolveAttachment> temporaryResolveAttachments;
-
-            for (auto i : IterateBitSet(cmd->attachmentState->GetColorAttachmentsMask())) {
-                auto& attachmentInfo = cmd->colorAttachments[i];
-                TextureViewBase* resolveTarget = attachmentInfo.resolveTarget.Get();
-                if (resolveTarget != nullptr) {
-                    // Save the color and resolve targets together for an explicit resolve pass
-                    // after this one ends, then remove the resolve target from this pass and
-                    // force the storeOp to Store.
-                    temporaryResolveAttachments.emplace_back(attachmentInfo.view.Get(),
-                                                             resolveTarget, attachmentInfo.storeOp);
-                    attachmentInfo.storeOp = wgpu::StoreOp::Store;
-                    attachmentInfo.resolveTarget = nullptr;
-                }
-            }
-
-            // Check for other workarounds that need to be applied recursively.
-            return ApplyRenderPassWorkarounds(
-                device, usageTracker, cmd,
-                [this, passEndCallback = std::move(passEndCallback),
-                 temporaryResolveAttachments = std::move(temporaryResolveAttachments)]() -> void {
-                    // Called once the render pass has been ended.
-                    // Handles any separate resolve passes needed for the
-                    // ResolveMultipleAttachmentInSeparatePasses workaround immediately after the
-                    // render pass ends and before any additional commands are recorded.
-                    for (auto& deferredResolve : temporaryResolveAttachments) {
-                        RenderPassColorAttachment attachment = {};
-                        attachment.view = deferredResolve.copySrc.Get();
-                        attachment.resolveTarget = deferredResolve.copyDst.Get();
-                        attachment.loadOp = wgpu::LoadOp::Load;
-                        attachment.storeOp = deferredResolve.storeOp;
-
-                        RenderPassDescriptor resolvePass = {};
-                        resolvePass.colorAttachmentCount = 1;
-                        resolvePass.colorAttachments = &attachment;
-
-                        // Begin and end an empty render pass to force the resolve.
-                        Ref<RenderPassEncoder> encoder = this->BeginRenderPass(&resolvePass);
-                        encoder->End();
-                    }
-
-                    // If there were any other callbacks in the workaround stack, call the next one.
-                    if (passEndCallback) {
-                        passEndCallback();
-                    }
-                });
-        }
-    }
-
-    // dawn:56, dawn:1569
-    // Handle Toggle AlwaysResolveIntoZeroLevelAndLayer. This swaps out the given resolve attachment
-    // for a temporary one that has no layers or mip levels. The results are copied from the
-    // temporary attachment into the given attachment when the render pass ends. (Handled at the
-    // bottom of this branch)
-    if (device->IsToggleEnabled(Toggle::AlwaysResolveIntoZeroLevelAndLayer)) {
-        std::vector<TemporaryResolveAttachment> temporaryResolveAttachments;
-
-        for (auto index : IterateBitSet(cmd->attachmentState->GetColorAttachmentsMask())) {
-            TextureViewBase* resolveTarget = cmd->colorAttachments[index].resolveTarget.Get();
-
-            if (resolveTarget != nullptr && (resolveTarget->GetBaseMipLevel() != 0 ||
-                                             resolveTarget->GetBaseArrayLayer() != 0)) {
-                // Create a temporary texture to resolve into
-                // TODO(dawn:1618): Defer allocation of temporary textures till submit time.
-                TextureDescriptor descriptor = {};
-                descriptor.usage =
-                    wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
-                descriptor.format = resolveTarget->GetFormat().format;
-                descriptor.size = resolveTarget->GetSingleSubresourceVirtualSize();
-                descriptor.dimension = wgpu::TextureDimension::e2D;
-                descriptor.mipLevelCount = 1;
-
-                // We are creating new resources. Device must already be locked via
-                // APIBeginRenderPass -> ApplyRenderPassWorkarounds.
-                // TODO(crbug.com/dawn/1618): In future, all temp resources should be created at
-                // Command Submit time, so the locking would be removed from here at that point.
-                Ref<TextureBase> temporaryResolveTexture;
-                Ref<TextureViewBase> temporaryResolveView;
-                {
-                    DAWN_ASSERT(device->IsLockedByCurrentThreadIfNeeded());
-
-                    DAWN_TRY_ASSIGN(temporaryResolveTexture, device->CreateTexture(&descriptor));
-
-                    TextureViewDescriptor viewDescriptor = {};
-                    DAWN_TRY_ASSIGN(
-                        temporaryResolveView,
-                        device->CreateTextureView(temporaryResolveTexture.Get(), &viewDescriptor));
-                }
-
-                // Save the temporary and given render targets together for copying after
-                // the render pass ends.
-                temporaryResolveAttachments.emplace_back(temporaryResolveView, resolveTarget);
-
-                // Replace the given resolve attachment with the temporary one.
-                usageTracker->TextureViewUsedAs(temporaryResolveView.Get(),
-                                                wgpu::TextureUsage::RenderAttachment);
-                cmd->colorAttachments[index].resolveTarget = temporaryResolveView;
-            }
-        }
-
-        if (temporaryResolveAttachments.size()) {
-            // Check for other workarounds that need to be applied recursively.
-            return ApplyRenderPassWorkarounds(
-                device, usageTracker, cmd,
-                [this, passEndCallback = std::move(passEndCallback),
-                 temporaryResolveAttachments = std::move(temporaryResolveAttachments)]() -> void {
-                    // Called once the render pass has been ended.
-                    // Handle any copies needed for the AlwaysResolveIntoZeroLevelAndLayer
-                    // workaround immediately after the render pass ends and before any additional
-                    // commands are recorded.
-                    for (auto& copyTarget : temporaryResolveAttachments) {
-                        ImageCopyTexture srcImageCopyTexture = {};
-                        srcImageCopyTexture.texture = copyTarget.copySrc->GetTexture();
-                        srcImageCopyTexture.aspect = wgpu::TextureAspect::All;
-                        srcImageCopyTexture.mipLevel = 0;
-                        srcImageCopyTexture.origin = {0, 0, 0};
-
-                        ImageCopyTexture dstImageCopyTexture = {};
-                        dstImageCopyTexture.texture = copyTarget.copyDst->GetTexture();
-                        dstImageCopyTexture.aspect = wgpu::TextureAspect::All;
-                        dstImageCopyTexture.mipLevel = copyTarget.copyDst->GetBaseMipLevel();
-                        dstImageCopyTexture.origin = {0, 0,
-                                                      copyTarget.copyDst->GetBaseArrayLayer()};
-
-                        Extent3D extent3D = copyTarget.copySrc->GetSingleSubresourceVirtualSize();
-
-                        auto internalUsageScope = MakeInternalUsageScope();
-                        this->APICopyTextureToTexture(&srcImageCopyTexture, &dstImageCopyTexture,
-                                                      &extent3D);
-                    }
-
-                    // If there were any other callbacks in the workaround stack, call the next one.
-                    if (passEndCallback) {
-                        passEndCallback();
-                    }
-                });
-        }
-    }
-
-    return std::move(passEndCallback);
-}
-
 void CommandEncoder::APICopyBufferToBuffer(BufferBase* source,
                                            uint64_t sourceOffset,
                                            BufferBase* destination,
diff --git a/src/dawn/native/CommandEncoder.h b/src/dawn/native/CommandEncoder.h
index 60ae122..552e65a 100644
--- a/src/dawn/native/CommandEncoder.h
+++ b/src/dawn/native/CommandEncoder.h
@@ -136,12 +136,6 @@
 
     void DestroyImpl() override;
 
-    ResultOrError<std::function<void()>> ApplyRenderPassWorkarounds(
-        DeviceBase* device,
-        RenderPassResourceUsageTracker* usageTracker,
-        BeginRenderPassCmd* cmd,
-        std::function<void()> passEndCallback = nullptr);
-
     MaybeError ValidateFinish() const;
 
     EncodingContext mEncodingContext;
diff --git a/src/dawn/native/RenderPassWorkaroundsHelper.cpp b/src/dawn/native/RenderPassWorkaroundsHelper.cpp
new file mode 100644
index 0000000..3c7cee7
--- /dev/null
+++ b/src/dawn/native/RenderPassWorkaroundsHelper.cpp
@@ -0,0 +1,284 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// 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 "dawn/native/RenderPassWorkaroundsHelper.h"
+
+#include <utility>
+#include <vector>
+
+#include "absl/container/inlined_vector.h"
+#include "dawn/common/Assert.h"
+#include "dawn/common/BitSetIterator.h"
+#include "dawn/native/AttachmentState.h"
+#include "dawn/native/BlitColorToColorWithDraw.h"
+#include "dawn/native/CommandEncoder.h"
+#include "dawn/native/Commands.h"
+#include "dawn/native/Device.h"
+#include "dawn/native/RenderPassEncoder.h"
+#include "dawn/native/Texture.h"
+
+namespace dawn::native {
+
+namespace {
+// Tracks the temporary resolve attachments used when the AlwaysResolveIntoZeroLevelAndLayer toggle
+// is active so that the results can be copied from the temporary resolve attachment into the
+// intended target after the render pass is complete. Also used by the
+// ResolveMultipleAttachmentInSeparatePasses toggle to track resolves that need to be done in their
+// own passes.
+struct TemporaryResolveAttachment {
+    TemporaryResolveAttachment(Ref<TextureViewBase> src,
+                               Ref<TextureViewBase> dst,
+                               wgpu::StoreOp storeOp = wgpu::StoreOp::Store)
+        : copySrc(std::move(src)), copyDst(std::move(dst)), storeOp(storeOp) {}
+
+    Ref<TextureViewBase> copySrc;
+    Ref<TextureViewBase> copyDst;
+    wgpu::StoreOp storeOp;
+};
+
+void CopyTextureView(CommandEncoder* encoder, TextureViewBase* src, TextureViewBase* dst) {
+    ImageCopyTexture srcImageCopyTexture = {};
+    srcImageCopyTexture.texture = src->GetTexture();
+    srcImageCopyTexture.aspect = wgpu::TextureAspect::All;
+    srcImageCopyTexture.mipLevel = src->GetBaseMipLevel();
+    srcImageCopyTexture.origin = {0, 0, src->GetBaseArrayLayer()};
+
+    ImageCopyTexture dstImageCopyTexture = {};
+    dstImageCopyTexture.texture = dst->GetTexture();
+    dstImageCopyTexture.aspect = wgpu::TextureAspect::All;
+    dstImageCopyTexture.mipLevel = dst->GetBaseMipLevel();
+    dstImageCopyTexture.origin = {0, 0, dst->GetBaseArrayLayer()};
+
+    Extent3D extent3D = src->GetSingleSubresourceVirtualSize();
+
+    auto internalUsageScope = encoder->MakeInternalUsageScope();
+    encoder->APICopyTextureToTexture(&srcImageCopyTexture, &dstImageCopyTexture, &extent3D);
+}
+
+}  // namespace
+
+RenderPassWorkaroundsHelper::RenderPassWorkaroundsHelper() = default;
+RenderPassWorkaroundsHelper::~RenderPassWorkaroundsHelper() = default;
+
+MaybeError RenderPassWorkaroundsHelper::Initialize(
+    CommandEncoder* encoder,
+    const RenderPassDescriptor* renderPassDescriptor) {
+    DeviceBase* device = encoder->GetDevice();
+
+    // dawn:56, dawn:1569
+    // Handle Toggle AlwaysResolveIntoZeroLevelAndLayer. This swaps out the given resolve attachment
+    // for a temporary one that has no layers or mip levels. The results are copied from the
+    // temporary attachment into the given attachment when the render pass ends. (Handled in
+    // Apply())
+    if (device->IsToggleEnabled(Toggle::AlwaysResolveIntoZeroLevelAndLayer)) {
+        for (uint8_t i = 0; i < renderPassDescriptor->colorAttachmentCount; ++i) {
+            ColorAttachmentIndex colorIdx(i);
+            const auto& colorAttachment = renderPassDescriptor->colorAttachments[i];
+            TextureViewBase* resolveTarget = colorAttachment.resolveTarget;
+
+            if (resolveTarget != nullptr && (resolveTarget->GetBaseMipLevel() != 0 ||
+                                             resolveTarget->GetBaseArrayLayer() != 0)) {
+                DAWN_ASSERT(colorAttachment.view);
+                // Create a temporary texture to resolve into
+                // TODO(dawn:1618): Defer allocation of temporary textures till submit time.
+                TextureDescriptor descriptor = {};
+                descriptor.usage = wgpu::TextureUsage::RenderAttachment |
+                                   wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst |
+                                   wgpu::TextureUsage::TextureBinding;
+                descriptor.format = resolveTarget->GetFormat().format;
+                descriptor.size = resolveTarget->GetSingleSubresourceVirtualSize();
+                descriptor.dimension = wgpu::TextureDimension::e2D;
+                descriptor.mipLevelCount = 1;
+
+                // We are creating new resources. Device must already be locked via
+                // APIBeginRenderPass.
+                // TODO(crbug.com/dawn/1618): In future, all temp resources should be created at
+                // Command Submit time, so the locking would be removed from here at that point.
+                Ref<TextureBase> temporaryResolveTexture;
+                Ref<TextureViewBase> temporaryResolveView;
+                {
+                    DAWN_ASSERT(device->IsLockedByCurrentThreadIfNeeded());
+
+                    DAWN_TRY_ASSIGN(temporaryResolveTexture, device->CreateTexture(&descriptor));
+
+                    TextureViewDescriptor viewDescriptor = {};
+                    DAWN_TRY_ASSIGN(
+                        temporaryResolveView,
+                        device->CreateTextureView(temporaryResolveTexture.Get(), &viewDescriptor));
+
+                    if (colorAttachment.loadOp == wgpu::LoadOp::ExpandResolveTexture) {
+                        // Since we want to load the original resolve target, we need to copy it to
+                        // the temp resolve target.
+                        CopyTextureView(encoder, resolveTarget, temporaryResolveView.Get());
+                    }
+                }
+
+                mTempResolveTargets[colorIdx] = {std::move(temporaryResolveTexture),
+                                                 std::move(temporaryResolveView)};
+
+                mTempResolveTargetsMask.set(colorIdx);
+            }
+        }
+    }
+
+    return {};
+}
+
+MaybeError RenderPassWorkaroundsHelper::ApplyOnPostEncoding(
+    CommandEncoder* encoder,
+    RenderPassResourceUsageTracker* usageTracker,
+    BeginRenderPassCmd* cmd,
+    std::function<void()>* passEndCallbackOut) {
+    auto device = encoder->GetDevice();
+
+    // List of operations to perform on render pass end.
+    absl::InlinedVector<std::function<void()>, 1> passEndOperations;
+
+    // dawn:56, dawn:1569
+    // swap the resolve targets for the temp resolve textures.
+    if (mTempResolveTargetsMask.any()) {
+        std::vector<TemporaryResolveAttachment> temporaryResolveAttachments;
+
+        for (auto index : IterateBitSet(mTempResolveTargetsMask)) {
+            TextureViewBase* resolveTarget = cmd->colorAttachments[index].resolveTarget.Get();
+            TextureViewBase* temporaryResolveView = mTempResolveTargets[index].view.Get();
+
+            DAWN_ASSERT(resolveTarget);
+
+            // Save the temporary and given render targets together for copying after
+            // the render pass ends.
+            temporaryResolveAttachments.emplace_back(temporaryResolveView, resolveTarget);
+
+            // Replace the given resolve attachment with the temporary one.
+            usageTracker->TextureViewUsedAs(temporaryResolveView,
+                                            wgpu::TextureUsage::RenderAttachment);
+            cmd->colorAttachments[index].resolveTarget = temporaryResolveView;
+        }
+
+        passEndOperations.emplace_back([encoder, temporaryResolveAttachments = std::move(
+                                                     temporaryResolveAttachments)]() -> void {
+            // Called once the render pass has been ended.
+            // Handle any copies needed for the AlwaysResolveIntoZeroLevelAndLayer
+            // workaround immediately after the render pass ends and before any additional
+            // commands are recorded.
+            for (auto& copyTarget : temporaryResolveAttachments) {
+                CopyTextureView(encoder, copyTarget.copySrc.Get(), copyTarget.copyDst.Get());
+            }
+        });
+    }
+
+    // dawn:1550
+    // Handle toggle ResolveMultipleAttachmentInSeparatePasses. This identifies passes where there
+    // are multiple MSAA color targets and at least one of them has a resolve target. If that's the
+    // case then the resolves are deferred by removing the resolve targets and forcing the storeOp
+    // to Store. After the pass has ended an new pass is recorded for each resolve target that
+    // resolves it separately.
+    if (device->IsToggleEnabled(Toggle::ResolveMultipleAttachmentInSeparatePasses) &&
+        cmd->attachmentState->GetColorAttachmentsMask().count() > 1) {
+        bool splitResolvesIntoSeparatePasses = false;
+
+        // This workaround needs to apply if there are multiple MSAA color targets (checked above)
+        // and at least one resolve target.
+        for (auto i : IterateBitSet(cmd->attachmentState->GetColorAttachmentsMask())) {
+            if (cmd->colorAttachments[i].resolveTarget.Get() != nullptr) {
+                splitResolvesIntoSeparatePasses = true;
+                break;
+            }
+        }
+
+        if (splitResolvesIntoSeparatePasses) {
+            std::vector<TemporaryResolveAttachment> temporaryResolveAttachments;
+
+            for (auto i : IterateBitSet(cmd->attachmentState->GetColorAttachmentsMask())) {
+                auto& attachmentInfo = cmd->colorAttachments[i];
+                TextureViewBase* resolveTarget = attachmentInfo.resolveTarget.Get();
+                if (resolveTarget != nullptr) {
+                    // Save the color and resolve targets together for an explicit resolve pass
+                    // after this one ends, then remove the resolve target from this pass and
+                    // force the storeOp to Store.
+                    temporaryResolveAttachments.emplace_back(attachmentInfo.view.Get(),
+                                                             resolveTarget, attachmentInfo.storeOp);
+                    attachmentInfo.storeOp = wgpu::StoreOp::Store;
+                    attachmentInfo.resolveTarget = nullptr;
+                }
+            }
+
+            passEndOperations.emplace_back([encoder, temporaryResolveAttachments = std::move(
+                                                         temporaryResolveAttachments)]() -> void {
+                // Called once the render pass has been ended.
+                // Handles any separate resolve passes needed for the
+                // ResolveMultipleAttachmentInSeparatePasses workaround immediately after the
+                // render pass ends and before any additional commands are recorded.
+                for (auto& deferredResolve : temporaryResolveAttachments) {
+                    RenderPassColorAttachment attachment = {};
+                    attachment.view = deferredResolve.copySrc.Get();
+                    attachment.resolveTarget = deferredResolve.copyDst.Get();
+                    attachment.loadOp = wgpu::LoadOp::Load;
+                    attachment.storeOp = deferredResolve.storeOp;
+
+                    RenderPassDescriptor resolvePass = {};
+                    resolvePass.colorAttachmentCount = 1;
+                    resolvePass.colorAttachments = &attachment;
+
+                    // Begin and end an empty render pass to force the resolve.
+                    Ref<RenderPassEncoder> rpEncoder = encoder->BeginRenderPass(&resolvePass);
+                    rpEncoder->End();
+                }
+            });
+        }
+    }
+
+    mShouldApplyExpandResolveEmulation =
+        cmd->attachmentState->GetExpandResolveInfo().attachmentsToExpandResolve.any() &&
+        device->CanTextureLoadResolveTargetInTheSameRenderpass();
+
+    *passEndCallbackOut = [passEndOperations = std::move(passEndOperations)] {
+        // Apply the operations in reverse order.
+        // We copy the MSAA textures to temp resolve targets, then from temp resolve targets
+        // to actual resolve targets.
+        for (auto opIte = passEndOperations.rbegin(); opIte != passEndOperations.rend(); ++opIte) {
+            (*opIte)();
+        }
+    };
+
+    return {};
+}
+
+MaybeError RenderPassWorkaroundsHelper::ApplyOnRenderPassStart(RenderPassEncoder* rpEncoder,
+                                                               const RenderPassDescriptor* rpDesc) {
+    DeviceBase* device = rpEncoder->GetDevice();
+    if (mShouldApplyExpandResolveEmulation) {
+        // Perform ExpandResolveTexture load op's emulation after the render pass starts.
+        // Backend that doesn't support CanTextureLoadResolveTargetInTheSameRenderpass() can
+        // implement this load op internally.
+        DAWN_TRY(ExpandResolveTextureWithDraw(device, rpEncoder, rpDesc));
+    }
+
+    return {};
+}
+
+}  // namespace dawn::native
diff --git a/src/dawn/native/RenderPassWorkaroundsHelper.h b/src/dawn/native/RenderPassWorkaroundsHelper.h
new file mode 100644
index 0000000..adc6e1a
--- /dev/null
+++ b/src/dawn/native/RenderPassWorkaroundsHelper.h
@@ -0,0 +1,82 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// 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.
+
+#ifndef SRC_DAWN_NATIVE_RENDERPASSWORKAROUNDSHELPER_H_
+#define SRC_DAWN_NATIVE_RENDERPASSWORKAROUNDSHELPER_H_
+
+#include <functional>
+
+#include "dawn/common/NonMovable.h"
+#include "dawn/common/Ref.h"
+#include "dawn/common/ityp_array.h"
+#include "dawn/common/ityp_bitset.h"
+#include "dawn/native/Error.h"
+#include "dawn/native/IntegerTypes.h"
+
+namespace dawn::native {
+struct BeginRenderPassCmd;
+class CommandEncoder;
+class RenderPassEncoder;
+struct RenderPassDescriptor;
+class RenderPassResourceUsageTracker;
+class TextureBase;
+class TextureViewBase;
+
+// This class does several render pass' workarounds at various steps:
+// - When post processing encoded BeginRenderPassCmd.
+// - After render pass starts.
+class RenderPassWorkaroundsHelper : NonMovable {
+  public:
+    RenderPassWorkaroundsHelper();
+    ~RenderPassWorkaroundsHelper();
+
+    MaybeError Initialize(CommandEncoder* encoder,
+                          const RenderPassDescriptor* renderPassDescriptor);
+    MaybeError ApplyOnPostEncoding(CommandEncoder* encoder,
+                                   RenderPassResourceUsageTracker* usageTracker,
+                                   BeginRenderPassCmd* cmd,
+                                   std::function<void()>* passEndCallbackOut);
+
+    MaybeError ApplyOnRenderPassStart(RenderPassEncoder* rpEncoder,
+                                      const RenderPassDescriptor* rpDesc);
+
+  private:
+    struct TextureAndView {
+        Ref<TextureBase> texture;
+        Ref<TextureViewBase> view;
+    };
+    // Temporary resolve targets to replace the original resolve targets.
+    PerColorAttachment<TextureAndView> mTempResolveTargets;
+    ColorAttachmentMask mTempResolveTargetsMask;
+
+    // Should we apply ExpandResolveTexture step?
+    bool mShouldApplyExpandResolveEmulation = false;
+};
+
+}  // namespace dawn::native
+
+#endif
diff --git a/src/dawn/native/Texture.cpp b/src/dawn/native/Texture.cpp
index a467029..8cd9172 100644
--- a/src/dawn/native/Texture.cpp
+++ b/src/dawn/native/Texture.cpp
@@ -776,14 +776,14 @@
     GetObjectTrackingList()->Track(this);
 
     // dawn:1569: If a texture with multiple array layers or mip levels is specified as a
-    // texture attachment when this toggle is active, it needs to be given CopyDst usage
+    // texture attachment when this toggle is active, it needs to be given CopySrc | CopyDst usage
     // internally.
     bool applyAlwaysResolveIntoZeroLevelAndLayerToggle =
         device->IsToggleEnabled(Toggle::AlwaysResolveIntoZeroLevelAndLayer) &&
         (GetArrayLayers() > 1 || GetNumMipLevels() > 1) &&
         (GetInternalUsage() & wgpu::TextureUsage::RenderAttachment);
     if (applyAlwaysResolveIntoZeroLevelAndLayerToggle) {
-        AddInternalUsage(wgpu::TextureUsage::CopyDst);
+        AddInternalUsage(wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst);
     }
 
     if (mInternalUsage & wgpu::TextureUsage::CopyDst) {
diff --git a/src/dawn/tests/end2end/MultisampledRenderingTests.cpp b/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
index 03b6734..0d3c2b1 100644
--- a/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
+++ b/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
@@ -2087,6 +2087,58 @@
     VerifyResolveTarget(kGreen, singleSampledTexture2);
 }
 
+// Test rendering into a layer of a 2D array texture and load op=LoadOp::ExpandResolveTexture.
+TEST_P(DawnLoadResolveTextureTest, DrawThenLoad2DArrayTextureLayer) {
+    auto multiSampledTexture = CreateTextureForRenderAttachment(kColorFormat, 4, 1, 1,
+                                                                /*transientAttachment=*/false,
+                                                                /*supportsTextureBinding=*/false);
+    auto multiSampledTextureView = multiSampledTexture.CreateView();
+
+    auto singleSampledTexture = CreateTextureForRenderAttachment(
+        kColorFormat, 1, 1, /*arrayCount=*/2, /*transientAttachment=*/false,
+        /*supportsTextureBinding=*/true);
+    wgpu::TextureViewDescriptor resolveViewDescriptor2;
+    resolveViewDescriptor2.dimension = wgpu::TextureViewDimension::e2D;
+    resolveViewDescriptor2.format = kColorFormat;
+    resolveViewDescriptor2.baseArrayLayer = 1;
+    resolveViewDescriptor2.baseMipLevel = 0;
+    auto singleSampledTextureView = singleSampledTexture.CreateView(&resolveViewDescriptor2);
+
+    wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+    wgpu::RenderPipeline pipeline = CreateRenderPipelineWithOneOutputForTest(
+        /*testDepth=*/false, /*sampleMask=*/0xFFFFFFFF, /*alphaToCoverageEnabled=*/false,
+        /*flipTriangle=*/false, /*enableExpandResolveLoadOp=*/false);
+
+    constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
+
+    // In first render pass we draw a green triangle. StoreOp=Discard to discard the MSAA texture's
+    // content.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {multiSampledTextureView}, {singleSampledTextureView}, wgpu::LoadOp::Clear,
+            wgpu::LoadOp::Clear,
+            /*testDepth=*/false);
+        renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Discard;
+        EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, kGreen);
+    }
+
+    // In second render pass, we only use LoadOp::ExpandResolveTexture with no draw call.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {multiSampledTextureView}, {singleSampledTextureView},
+            wgpu::LoadOp::ExpandResolveTexture, wgpu::LoadOp::Load,
+            /*testDepth=*/false);
+        renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Discard;
+        wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
+        renderPassEncoder.End();
+    }
+
+    wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
+    queue.Submit(1, &commandBuffer);
+
+    VerifyResolveTarget(kGreen, singleSampledTexture, 0, 1);
+}
+
 DAWN_INSTANTIATE_TEST(MultisampledRenderingTest,
                       D3D11Backend(),
                       D3D12Backend(),