Support multiple attachments with LoadOp::ExpandResolveTexture.

Bug: dawn:1710
Change-Id: I426f422a4df16c3d336d8a357394c5605ca1a1e8
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/188280
Commit-Queue: Austin Eng <enga@chromium.org>
Commit-Queue: Quyen Le <lehoangquyen@chromium.org>
Reviewed-by: Austin Eng <enga@chromium.org>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Auto-Submit: Quyen Le <lehoangquyen@chromium.org>
diff --git a/docs/dawn/features/dawn_load_resolve_texture.md b/docs/dawn/features/dawn_load_resolve_texture.md
index 8cd0241..880f52d 100644
--- a/docs/dawn/features/dawn_load_resolve_texture.md
+++ b/docs/dawn/features/dawn_load_resolve_texture.md
@@ -67,5 +67,5 @@
  - If a resolve texture is used in a `wgpu::LoadOp::ExpandResolveTexture` operation, it must have `wgpu::TextureUsage::TextureBinding` usage.
  - If `wgpu::ColorTargetStateExpandResolveTextureDawn` chained struct is not included in a `wgpu::RenderPipelineDescriptor::FragmentState::ColorTargetState`  or if it is included but `enabled` boolean flag is false, then the result render pipeline cannot be used in a render pass using `ExpandResolveTexture` load op for that respective color attachment.
    - Similarly, a render pipeline created with `wgpu::ColorTargetStateExpandResolveTextureDawn`'s `enabled` flag = `true` won't be able to be used in render passes that don't use `ExpandResolveTexture` load op for the respective color attachment.
- - Currently only one color attachment is supported and the `ExpandResolveTexture` LoadOp only works on color attachment, this could be changed in future.
+ - Currently the `ExpandResolveTexture` LoadOp only works on color attachment, this could be changed in future.
  - The texture is not supported if it is not resolvable by WebGPU standard. This means this feature currently doesn't work with integer textures.
diff --git a/src/dawn/native/AttachmentState.cpp b/src/dawn/native/AttachmentState.cpp
index efd4206..75ac8dc 100644
--- a/src/dawn/native/AttachmentState.cpp
+++ b/src/dawn/native/AttachmentState.cpp
@@ -54,6 +54,7 @@
     mDepthStencilFormat = descriptor->depthStencilFormat;
 
     // TODO(dawn:1710): support MSAA render to single sampled in render bundles.
+    // TODO(dawn:1710): support LoadOp::ExpandResolveTexture in render bundles.
     // TODO(dawn:1704): support PLS in render bundles.
 
     SetContentHash(ComputeContentHash());
diff --git a/src/dawn/native/BlitColorToColorWithDraw.cpp b/src/dawn/native/BlitColorToColorWithDraw.cpp
index 3d65525..cd382db 100644
--- a/src/dawn/native/BlitColorToColorWithDraw.cpp
+++ b/src/dawn/native/BlitColorToColorWithDraw.cpp
@@ -27,7 +27,12 @@
 
 #include "dawn/native/BlitColorToColorWithDraw.h"
 
+#include <sstream>
+#include <string>
+
+#include "absl/container/inlined_vector.h"
 #include "dawn/common/Assert.h"
+#include "dawn/common/Enumerator.h"
 #include "dawn/common/HashUtils.h"
 #include "dawn/native/BindGroup.h"
 #include "dawn/native/CommandEncoder.h"
@@ -36,6 +41,7 @@
 #include "dawn/native/RenderPassEncoder.h"
 #include "dawn/native/RenderPipeline.h"
 #include "dawn/native/utils/WGPUHelpers.h"
+#include "dawn/native/webgpu_absl_format.h"
 
 namespace dawn::native {
 
@@ -54,34 +60,45 @@
 }
 )";
 
-constexpr char kBlitToFloatColorFS[] = R"(
-@group(0) @binding(0) var src_tex : texture_2d<f32>;
+std::string GenerateFS(const BlitColorToColorWithDrawPipelineKey& pipelineKey) {
+    std::ostringstream outputStructStream;
+    std::ostringstream assignOutputsStream;
+    std::ostringstream finalStream;
 
-@fragment fn blit_to_color(@builtin(position) position : vec4f) -> @location(0) vec4f {
-  return textureLoad(src_tex, vec2u(position.xy), 0);
+    for (auto i : IterateBitSet(pipelineKey.attachmentsToExpandResolve)) {
+        finalStream << absl::StrFormat("@group(0) @binding(%u) var srcTex%u : texture_2d<f32>;\n",
+                                       i, i);
+
+        outputStructStream << absl::StrFormat("@location(%u) output%u : vec4f,\n", i, i);
+
+        assignOutputsStream << absl::StrFormat(
+            "\toutputColor.output%u = textureLoad(srcTex%u, vec2u(position.xy), 0);\n", i, i);
+    }
+
+    finalStream << "struct OutputColor {\n" << outputStructStream.str() << "}\n" << std::endl;
+    finalStream << R"(
+@fragment fn blit_to_color(@builtin(position) position : vec4f) -> OutputColor {
+    var outputColor : OutputColor;
+)" << assignOutputsStream.str()
+                << R"(
+    return outputColor;
+})";
+
+    return finalStream.str();
 }
 
-)";
-
 ResultOrError<Ref<RenderPipelineBase>> GetOrCreateColorBlitPipeline(
     DeviceBase* device,
-    const Format& colorInternalFormat,
-    wgpu::TextureFormat depthStencilFormat,
-    uint32_t sampleCount) {
+    const BlitColorToColorWithDrawPipelineKey& pipelineKey,
+    uint8_t colorAttachmentCount) {
     InternalPipelineStore* store = device->GetInternalPipelineStore();
-    BlitColorToColorWithDrawPipelineKey pipelineKey;
-    pipelineKey.colorFormat = colorInternalFormat.format;
-    pipelineKey.depthStencilFormat = depthStencilFormat;
-    pipelineKey.sampleCount = sampleCount;
     {
-        auto it = store->msaaRenderToSingleSampledColorBlitPipelines.find(pipelineKey);
-        if (it != store->msaaRenderToSingleSampledColorBlitPipelines.end()) {
+        auto it = store->expandResolveTexturePipelines.find(pipelineKey);
+        if (it != store->expandResolveTexturePipelines.end()) {
             return it->second;
         }
     }
 
-    const auto& formatAspectInfo = colorInternalFormat.GetAspectInfo(Aspect::Color);
-
     // vertex shader's source.
     ShaderModuleWGSLDescriptor wgslDesc = {};
     ShaderModuleDescriptor shaderModuleDesc = {};
@@ -91,16 +108,9 @@
     Ref<ShaderModuleBase> vshaderModule;
     DAWN_TRY_ASSIGN(vshaderModule, device->CreateShaderModule(&shaderModuleDesc));
 
-    // fragment shader's source will depend on color format type.
-    switch (formatAspectInfo.baseType) {
-        case TextureComponentType::Float:
-            wgslDesc.code = kBlitToFloatColorFS;
-            break;
-        default:
-            // TODO(dawn:1710): blitting integer textures are not currently supported.
-            DAWN_UNREACHABLE();
-            break;
-    }
+    // fragment shader's source will depend on pipeline key.
+    std::string fsCode = GenerateFS(pipelineKey);
+    wgslDesc.code = fsCode.c_str();
     Ref<ShaderModuleBase> fshaderModule;
     DAWN_TRY_ASSIGN(fshaderModule, device->CreateShaderModule(&shaderModuleDesc));
 
@@ -108,15 +118,23 @@
     fragmentState.module = fshaderModule.Get();
     fragmentState.entryPoint = "blit_to_color";
 
-    // Color target state.
-    ColorTargetState colorTarget;
-    colorTarget.format = colorInternalFormat.format;
-
-    fragmentState.targetCount = 1;
-    fragmentState.targets = &colorTarget;
+    // Color target states.
+    PerColorAttachment<ColorTargetState> colorTargets = {};
     wgpu::ColorTargetStateExpandResolveTextureDawn msaaExpandResolveState;
     msaaExpandResolveState.enabled = true;
-    colorTarget.nextInChain = &msaaExpandResolveState;
+
+    for (auto [i, target] : Enumerate(colorTargets)) {
+        target.format = pipelineKey.colorTargetFormats[i];
+        // We shouldn't change the color targets that are not involved in.
+        if (pipelineKey.attachmentsToExpandResolve[i]) {
+            target.nextInChain = &msaaExpandResolveState;
+        } else {
+            target.writeMask = wgpu::ColorWriteMask::None;
+        }
+    }
+
+    fragmentState.targetCount = colorAttachmentCount;
+    fragmentState.targets = colorTargets.data();
 
     RenderPipelineDescriptor renderPipelineDesc = {};
     renderPipelineDesc.label = "blit_color_to_color";
@@ -126,8 +144,8 @@
 
     // Depth stencil state.
     DepthStencilState depthStencilState = {};
-    if (depthStencilFormat != wgpu::TextureFormat::Undefined) {
-        depthStencilState.format = depthStencilFormat;
+    if (pipelineKey.depthStencilFormat != wgpu::TextureFormat::Undefined) {
+        depthStencilState.format = pipelineKey.depthStencilFormat;
         depthStencilState.depthWriteEnabled = false;
         depthStencilState.depthCompare = wgpu::CompareFunction::Always;
 
@@ -135,19 +153,27 @@
     }
 
     // Multisample state.
-    DAWN_ASSERT(sampleCount > 1);
-    renderPipelineDesc.multisample.count = sampleCount;
+    DAWN_ASSERT(pipelineKey.sampleCount > 1);
+    renderPipelineDesc.multisample.count = pipelineKey.sampleCount;
 
     // Bind group layout.
+    absl::InlinedVector<BindGroupLayoutEntry, kMaxColorAttachments> bglEntries;
+    for (auto colorIdx : IterateBitSet(pipelineKey.attachmentsToExpandResolve)) {
+        bglEntries.push_back({});
+        auto& bglEntry = bglEntries.back();
+        bglEntry.binding = static_cast<uint8_t>(colorIdx);
+        bglEntry.visibility = wgpu::ShaderStage::Fragment;
+        bglEntry.texture.sampleType = kInternalResolveAttachmentSampleType;
+        bglEntry.texture.viewDimension = wgpu::TextureViewDimension::e2D;
+    }
+    BindGroupLayoutDescriptor bglDesc = {};
+    bglDesc.entries = bglEntries.data();
+    bglDesc.entryCount = bglEntries.size();
+
     Ref<BindGroupLayoutBase> bindGroupLayout;
     DAWN_TRY_ASSIGN(bindGroupLayout,
-                    utils::MakeBindGroupLayout(
-                        device,
-                        {
-                            {0, wgpu::ShaderStage::Fragment, kInternalResolveAttachmentSampleType,
-                             wgpu::TextureViewDimension::e2D},
-                        },
-                        /* allowInternalBinding */ true));
+                    device->CreateBindGroupLayout(&bglDesc, /* allowInternalBinding */ true));
+
     Ref<PipelineLayoutBase> pipelineLayout;
     DAWN_TRY_ASSIGN(pipelineLayout, utils::MakeBasicPipelineLayout(device, bindGroupLayout));
     renderPipelineDesc.layout = pipelineLayout.Get();
@@ -155,7 +181,7 @@
     Ref<RenderPipelineBase> pipeline;
     DAWN_TRY_ASSIGN(pipeline, device->CreateRenderPipeline(&renderPipelineDesc));
 
-    store->msaaRenderToSingleSampledColorBlitPipelines.emplace(pipelineKey, pipeline);
+    store->expandResolveTexturePipelines.emplace(pipelineKey, pipeline);
     return pipeline;
 }
 
@@ -167,41 +193,64 @@
     DAWN_ASSERT(device->IsLockedByCurrentThreadIfNeeded());
     DAWN_ASSERT(device->IsResolveTextureBlitWithDrawSupported());
 
-    // TODO(dawn:1710): support multiple attachments.
-    DAWN_ASSERT(renderPassDescriptor->colorAttachmentCount == 1);
+    BlitColorToColorWithDrawPipelineKey pipelineKey;
+    for (uint8_t i = 0; i < renderPassDescriptor->colorAttachmentCount; ++i) {
+        ColorAttachmentIndex colorIdx(i);
+        const auto& colorAttachment = renderPassDescriptor->colorAttachments[i];
+        TextureViewBase* view = colorAttachment.view;
+        if (!view) {
+            continue;
+        }
+        const Format& format = view->GetFormat();
+        TextureComponentType baseType = format.GetAspectInfo(Aspect::Color).baseType;
+        // TODO(dawn:1710): blitting integer textures are not currently supported.
+        DAWN_ASSERT(baseType == TextureComponentType::Float);
 
-    // The original color attachment of the render pass will be used as source.
-    TextureViewBase* src = renderPassDescriptor->colorAttachments[0].resolveTarget;
-    TextureViewBase* dst = renderPassDescriptor->colorAttachments[0].view;
+        if (colorAttachment.loadOp == wgpu::LoadOp::ExpandResolveTexture) {
+            DAWN_ASSERT(colorAttachment.resolveTarget->GetLayerCount() == 1u);
+            DAWN_ASSERT(colorAttachment.resolveTarget->GetDimension() ==
+                        wgpu::TextureViewDimension::e2D);
+            pipelineKey.attachmentsToExpandResolve.set(colorIdx);
+        }
 
-    TextureBase* dstTexture = dst->GetTexture();
+        pipelineKey.colorTargetFormats[colorIdx] = format.format;
+        pipelineKey.sampleCount = view->GetTexture()->GetSampleCount();
+    }
 
-    DAWN_ASSERT(src->GetLayerCount() == 1u);
-    DAWN_ASSERT(src->GetDimension() == wgpu::TextureViewDimension::e2D);
+    if (!pipelineKey.attachmentsToExpandResolve.any()) {
+        return {};
+    }
 
-    wgpu::TextureFormat depthStencilFormat = wgpu::TextureFormat::Undefined;
+    pipelineKey.depthStencilFormat = wgpu::TextureFormat::Undefined;
     if (renderPassDescriptor->depthStencilAttachment != nullptr) {
-        depthStencilFormat = renderPassDescriptor->depthStencilAttachment->view->GetFormat().format;
+        pipelineKey.depthStencilFormat =
+            renderPassDescriptor->depthStencilAttachment->view->GetFormat().format;
     }
 
     Ref<RenderPipelineBase> pipeline;
-    DAWN_TRY_ASSIGN(pipeline,
-                    GetOrCreateColorBlitPipeline(device, src->GetFormat(), depthStencilFormat,
-                                                 /*sampleCount=*/dstTexture->GetSampleCount()));
+    DAWN_TRY_ASSIGN(pipeline, GetOrCreateColorBlitPipeline(
+                                  device, pipelineKey, renderPassDescriptor->colorAttachmentCount));
 
     Ref<BindGroupLayoutBase> bgl;
     DAWN_TRY_ASSIGN(bgl, pipeline->GetBindGroupLayout(0));
 
     Ref<BindGroupBase> bindGroup;
     {
-        BindGroupEntry bgEntry = {};
-        bgEntry.binding = 0;
-        bgEntry.textureView = src;
+        absl::InlinedVector<BindGroupEntry, kMaxColorAttachments> bgEntries = {};
+
+        for (auto colorIdx : IterateBitSet(pipelineKey.attachmentsToExpandResolve)) {
+            uint8_t i = static_cast<uint8_t>(colorIdx);
+            const auto& colorAttachment = renderPassDescriptor->colorAttachments[i];
+            bgEntries.push_back({});
+            auto& bgEntry = bgEntries.back();
+            bgEntry.binding = i;
+            bgEntry.textureView = colorAttachment.resolveTarget;
+        }
 
         BindGroupDescriptor bgDesc = {};
         bgDesc.layout = bgl.Get();
-        bgDesc.entryCount = 1;
-        bgDesc.entries = &bgEntry;
+        bgDesc.entryCount = bgEntries.size();
+        bgDesc.entries = bgEntries.data();
         DAWN_TRY_ASSIGN(bindGroup, device->CreateBindGroup(&bgDesc, UsageValidationMode::Internal));
     }
 
@@ -217,7 +266,12 @@
     const BlitColorToColorWithDrawPipelineKey& key) const {
     size_t hash = 0;
 
-    HashCombine(&hash, key.colorFormat);
+    HashCombine(&hash, key.attachmentsToExpandResolve);
+
+    for (auto format : key.colorTargetFormats) {
+        HashCombine(&hash, format);
+    }
+
     HashCombine(&hash, key.depthStencilFormat);
     HashCombine(&hash, key.sampleCount);
 
@@ -227,8 +281,17 @@
 bool BlitColorToColorWithDrawPipelineKey::EqualityFunc::operator()(
     const BlitColorToColorWithDrawPipelineKey& a,
     const BlitColorToColorWithDrawPipelineKey& b) const {
-    return a.colorFormat == b.colorFormat && a.depthStencilFormat == b.depthStencilFormat &&
-           a.sampleCount == b.sampleCount;
+    if (a.attachmentsToExpandResolve != b.attachmentsToExpandResolve) {
+        return false;
+    }
+
+    for (auto [i, format] : Enumerate(a.colorTargetFormats)) {
+        if (format != b.colorTargetFormats[i]) {
+            return false;
+        }
+    }
+
+    return a.depthStencilFormat == b.depthStencilFormat && a.sampleCount == b.sampleCount;
 }
 
 }  // namespace dawn::native
diff --git a/src/dawn/native/BlitColorToColorWithDraw.h b/src/dawn/native/BlitColorToColorWithDraw.h
index 4d57914..d418e40 100644
--- a/src/dawn/native/BlitColorToColorWithDraw.h
+++ b/src/dawn/native/BlitColorToColorWithDraw.h
@@ -28,8 +28,13 @@
 #ifndef SRC_DAWN_NATIVE_BLITCOLORTOCOLORWITHDRAW_H_
 #define SRC_DAWN_NATIVE_BLITCOLORTOCOLORWITHDRAW_H_
 
+#include <bitset>
+
 #include "absl/container/flat_hash_map.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 {
 
@@ -39,8 +44,13 @@
 class TextureViewBase;
 
 struct BlitColorToColorWithDrawPipelineKey {
-    wgpu::TextureFormat colorFormat;
-    wgpu::TextureFormat depthStencilFormat;
+    BlitColorToColorWithDrawPipelineKey() {
+        colorTargetFormats.fill(wgpu::TextureFormat::Undefined);
+    }
+
+    PerColorAttachment<wgpu::TextureFormat> colorTargetFormats;
+    ColorAttachmentMask attachmentsToExpandResolve;
+    wgpu::TextureFormat depthStencilFormat = wgpu::TextureFormat::Undefined;
     uint32_t sampleCount = 1;
 
     struct HashFunc {
diff --git a/src/dawn/native/CommandEncoder.cpp b/src/dawn/native/CommandEncoder.cpp
index f7033ee..741ca27 100644
--- a/src/dawn/native/CommandEncoder.cpp
+++ b/src/dawn/native/CommandEncoder.cpp
@@ -813,12 +813,6 @@
     }
 
     if (validationState->WillExpandResolveTexture()) {
-        // TODO(dawn:1710): support multiple attachments.
-        DAWN_INVALID_IF(
-            descriptor->colorAttachmentCount != 1,
-            "colorAttachmentCount (%u) is not supported when the render pass has one attachment "
-            "with %s. (Currently) colorAttachmentCount = 1 is supported.",
-            descriptor->colorAttachmentCount, wgpu::LoadOp::ExpandResolveTexture);
         // TODO(dawn:1704): Consider supporting ExpandResolveTexture + PLS
         DAWN_INVALID_IF(pls != nullptr, "For now pixel local storage is invalid to use with %s.",
                         wgpu::LoadOp::ExpandResolveTexture);
@@ -935,12 +929,6 @@
 MaybeError ApplyExpandResolveTextureLoadOp(DeviceBase* device,
                                            RenderPassEncoder* renderPassEncoder,
                                            const RenderPassDescriptor* renderPassDescriptor) {
-    // TODO(dawn:1710): support multiple attachments.
-    DAWN_ASSERT(renderPassDescriptor->colorAttachmentCount == 1);
-    if (renderPassDescriptor->colorAttachments[0].loadOp != wgpu::LoadOp::ExpandResolveTexture) {
-        return {};
-    }
-
     // TODO(dawn:1710): support loading resolve texture on platforms that don't support reading
     // it in fragment shader such as vulkan.
     DAWN_ASSERT(device->IsResolveTextureBlitWithDrawSupported());
diff --git a/src/dawn/native/InternalPipelineStore.h b/src/dawn/native/InternalPipelineStore.h
index 316efe4..e45d116 100644
--- a/src/dawn/native/InternalPipelineStore.h
+++ b/src/dawn/native/InternalPipelineStore.h
@@ -103,7 +103,7 @@
 
     absl::flat_hash_map<wgpu::TextureFormat, Ref<RenderPipelineBase>> depthBlitPipelines;
 
-    BlitColorToColorWithDrawPipelinesCache msaaRenderToSingleSampledColorBlitPipelines;
+    BlitColorToColorWithDrawPipelinesCache expandResolveTexturePipelines;
 };
 
 }  // namespace dawn::native
diff --git a/src/dawn/tests/end2end/MultisampledRenderingTests.cpp b/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
index 15487d0..fefc26f 100644
--- a/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
+++ b/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
@@ -26,6 +26,7 @@
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 #include <algorithm>
+#include <bitset>
 #include <vector>
 
 #include "dawn/common/Assert.h"
@@ -36,6 +37,8 @@
 namespace dawn {
 namespace {
 
+using AttachmentMask = std::bitset<16>;
+
 class MultisampledRenderingTest : public DawnTest {
   protected:
     void SetUp() override {
@@ -94,14 +97,18 @@
 
         const char* fs = testDepth ? kFsOneOutputWithDepth : kFsOneOutputWithoutDepth;
 
+        AttachmentMask enableExpandResolveLoadOps;
+        enableExpandResolveLoadOps.set(0, enableExpandResolveLoadOp);
         return CreateRenderPipelineForTest(fs, 1, testDepth, sampleMask, alphaToCoverageEnabled,
-                                           flipTriangle, enableExpandResolveLoadOp);
+                                           flipTriangle, enableExpandResolveLoadOps);
     }
 
     wgpu::RenderPipeline CreateRenderPipelineWithTwoOutputsForTest(
         uint32_t sampleMask = 0xFFFFFFFF,
         bool alphaToCoverageEnabled = false,
-        bool enableExpandResolveLoadOp = false) {
+        bool depthTest = false,
+        bool enableExpandResolveLoadOpForColor0 = false,
+        bool enableExpandResolveLoadOpForColor1 = false) {
         const char* kFsTwoOutputs = R"(
             struct U {
                 color0 : vec4f,
@@ -121,9 +128,13 @@
                 return output;
             })";
 
-        return CreateRenderPipelineForTest(kFsTwoOutputs, 2, false, sampleMask,
+        AttachmentMask enableExpandResolveLoadOps;
+        enableExpandResolveLoadOps.set(0, enableExpandResolveLoadOpForColor0);
+        enableExpandResolveLoadOps.set(1, enableExpandResolveLoadOpForColor1);
+
+        return CreateRenderPipelineForTest(kFsTwoOutputs, 2, depthTest, sampleMask,
                                            alphaToCoverageEnabled, /*flipTriangle=*/false,
-                                           enableExpandResolveLoadOp);
+                                           enableExpandResolveLoadOps);
     }
 
     wgpu::RenderPipeline CreateRenderPipelineWithNonZeroLocationOutputForTest(
@@ -140,9 +151,11 @@
                 return uBuffer.color;
             })";
 
+        AttachmentMask enableExpandResolveLoadOps = {};
+        enableExpandResolveLoadOps.set(1, enableExpandResolveLoadOp);
         return CreateRenderPipelineForTest(kFsNonZeroLocationOutputs, 1, false, sampleMask,
                                            alphaToCoverageEnabled, /*flipTriangle=*/false,
-                                           enableExpandResolveLoadOp, 1);
+                                           enableExpandResolveLoadOps, 1);
     }
 
     wgpu::Texture CreateTextureForRenderAttachment(wgpu::TextureFormat format,
@@ -271,7 +284,7 @@
                                                      uint32_t sampleMask = 0xFFFFFFFF,
                                                      bool alphaToCoverageEnabled = false,
                                                      bool flipTriangle = false,
-                                                     bool enableExpandResolveLoadOp = false,
+                                                     AttachmentMask enableExpandResolveLoadOps = {},
                                                      uint32_t firstAttachmentLocation = 0) {
         utils::ComboRenderPipelineDescriptor pipelineDescriptor;
 
@@ -320,22 +333,21 @@
         pipelineDescriptor.multisample.alphaToCoverageEnabled = alphaToCoverageEnabled;
 
         pipelineDescriptor.cFragment.targetCount = numColorAttachments + firstAttachmentLocation;
+
+        wgpu::ColorTargetStateExpandResolveTextureDawn msaaExpandResolveDesc;
+        msaaExpandResolveDesc.enabled = true;
         for (uint32_t i = 0; i < numColorAttachments + firstAttachmentLocation; ++i) {
             if (i < firstAttachmentLocation) {
                 pipelineDescriptor.cTargets[i].writeMask = wgpu::ColorWriteMask::None;
                 pipelineDescriptor.cTargets[i].format = wgpu::TextureFormat::Undefined;
             } else {
                 pipelineDescriptor.cTargets[i].format = kColorFormat;
+                if (enableExpandResolveLoadOps.test(i)) {
+                    pipelineDescriptor.cTargets[i].nextInChain = &msaaExpandResolveDesc;
+                }
             }
         }
 
-        // TODO(dawn:1710): support multiple targets with ExpandResolveTexture load op.
-        wgpu::ColorTargetStateExpandResolveTextureDawn msaaExpandResolveDesc;
-        if (enableExpandResolveLoadOp) {
-            msaaExpandResolveDesc.enabled = true;
-            pipelineDescriptor.cTargets[0].nextInChain = &msaaExpandResolveDesc;
-        }
-
         wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
         return pipeline;
     }
@@ -1588,6 +1600,287 @@
     VerifyResolveTarget(kGreen, singleSampledTexture);
 }
 
+// Test using ExpandResolveTexture load op for non-zero indexed attachment.
+TEST_P(DawnLoadResolveTextureTest, DrawThenLoadNonZeroIndexedAttachment) {
+    auto multiSampledTexture = CreateTextureForRenderAttachment(
+        kColorFormat, 4, 1, 1,
+        /*transientAttachment=*/device.HasFeature(wgpu::FeatureName::TransientAttachments),
+        /*supportsTextureBinding=*/false);
+    auto multiSampledTextureView = multiSampledTexture.CreateView();
+
+    auto singleSampledTexture =
+        CreateTextureForRenderAttachment(kColorFormat, 1, 1, 1, /*transientAttachment=*/false,
+                                         /*supportsTextureBinding=*/true);
+    auto singleSampledTextureView = singleSampledTexture.CreateView();
+
+    wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+    wgpu::RenderPipeline pipeline = CreateRenderPipelineWithNonZeroLocationOutputForTest(
+        /*sampleMask=*/0xFFFFFFFF, /*alphaToCoverageEnabled=*/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(
+            {nullptr, multiSampledTextureView}, {nullptr, singleSampledTextureView},
+            wgpu::LoadOp::Clear, wgpu::LoadOp::Clear,
+            /*testDepth=*/false);
+        renderPass.cColorAttachments[1].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(
+            {nullptr, multiSampledTextureView}, {nullptr, singleSampledTextureView},
+            wgpu::LoadOp::ExpandResolveTexture, wgpu::LoadOp::Load,
+            /*testDepth=*/false);
+        renderPass.cColorAttachments[1].storeOp = wgpu::StoreOp::Discard;
+        wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
+        renderPassEncoder.End();
+    }
+
+    wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
+    queue.Submit(1, &commandBuffer);
+
+    VerifyResolveTarget(kGreen, singleSampledTexture);
+}
+
+// Test rendering into 2 attachments. The 1st attachment will use
+// LoadOp::ExpandResolveTexture.
+TEST_P(DawnLoadResolveTextureTest, TwoOutputsDrawThenLoadColor0) {
+    auto multiSampledTexture1 = CreateTextureForRenderAttachment(
+        kColorFormat, 4, 1, 1,
+        /*transientAttachment=*/device.HasFeature(wgpu::FeatureName::TransientAttachments),
+        /*supportsTextureBinding=*/false);
+    auto multiSampledTextureView1 = multiSampledTexture1.CreateView();
+
+    auto multiSampledTexture2 = CreateTextureForRenderAttachment(kColorFormat, 4, 1, 1,
+                                                                 /*transientAttachment=*/false,
+                                                                 /*supportsTextureBinding=*/false);
+    auto multiSampledTextureView2 = multiSampledTexture2.CreateView();
+
+    auto singleSampledTexture1 =
+        CreateTextureForRenderAttachment(kColorFormat, 1, 1, 1, /*transientAttachment=*/false,
+                                         /*supportsTextureBinding=*/true);
+    auto singleSampledTextureView1 = singleSampledTexture1.CreateView();
+
+    auto singleSampledTexture2 =
+        CreateTextureForRenderAttachment(kColorFormat, 1, 1, 1, /*transientAttachment=*/false,
+                                         /*supportsTextureBinding=*/true);
+    auto singleSampledTextureView2 = singleSampledTexture2.CreateView();
+
+    wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+    wgpu::RenderPipeline pipeline = CreateRenderPipelineWithTwoOutputsForTest(
+        /*sampleMask=*/0xFFFFFFFF, /*alphaToCoverageEnabled=*/false);
+
+    constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
+    constexpr wgpu::Color kRed = {0.8f, 0.0f, 0.0f, 0.8f};
+
+    // In first render pass:
+    // - we draw a red triangle to attachment0. StoreOp=Discard to discard the MSAA texture's
+    // content.
+    // - we draw a green triangle to attachment1. StoreOp=Store.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {multiSampledTextureView1, multiSampledTextureView2},
+            {singleSampledTextureView1, singleSampledTextureView2}, wgpu::LoadOp::Clear,
+            wgpu::LoadOp::Clear,
+            /*testDepth=*/false);
+        renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Discard;
+        renderPass.cColorAttachments[1].storeOp = wgpu::StoreOp::Store;
+
+        std::array<float, 8> kUniformData = {
+            static_cast<float>(kRed.r),   static_cast<float>(kRed.g),
+            static_cast<float>(kRed.b),   static_cast<float>(kRed.a),
+            static_cast<float>(kGreen.r), static_cast<float>(kGreen.g),
+            static_cast<float>(kGreen.b), static_cast<float>(kGreen.a)};
+        constexpr uint32_t kSize = sizeof(kUniformData);
+
+        EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, kUniformData.data(), kSize);
+    }
+
+    // In second render pass:
+    // - we only use LoadOp::ExpandResolveTexture for attachment0 with no draw call.
+    // - we only use LoadOp::Load for attachment1 with no draw call.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {multiSampledTextureView1, multiSampledTextureView2},
+            {singleSampledTextureView1, singleSampledTextureView2}, wgpu::LoadOp::Clear,
+            wgpu::LoadOp::Clear,
+            /*testDepth=*/false);
+        renderPass.cColorAttachments[0].loadOp = wgpu::LoadOp::ExpandResolveTexture;
+        renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Discard;
+        renderPass.cColorAttachments[1].loadOp = wgpu::LoadOp::Load;
+        renderPass.cColorAttachments[1].storeOp = wgpu::StoreOp::Discard;
+        wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
+        renderPassEncoder.End();
+    }
+
+    wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
+    queue.Submit(1, &commandBuffer);
+
+    VerifyResolveTarget(kRed, singleSampledTexture1);
+    VerifyResolveTarget(kGreen, singleSampledTexture2);
+}
+
+// Test rendering into 2 attachments. The 2nd attachment will use
+// LoadOp::ExpandResolveTexture.
+TEST_P(DawnLoadResolveTextureTest, TwoOutputsDrawThenLoadColor1) {
+    auto multiSampledTexture1 = CreateTextureForRenderAttachment(kColorFormat, 4, 1, 1,
+                                                                 /*transientAttachment=*/false,
+                                                                 /*supportsTextureBinding=*/false);
+    auto multiSampledTextureView1 = multiSampledTexture1.CreateView();
+
+    auto multiSampledTexture2 = CreateTextureForRenderAttachment(
+        kColorFormat, 4, 1, 1,
+        /*transientAttachment=*/device.HasFeature(wgpu::FeatureName::TransientAttachments),
+        /*supportsTextureBinding=*/false);
+    auto multiSampledTextureView2 = multiSampledTexture2.CreateView();
+
+    auto singleSampledTexture1 =
+        CreateTextureForRenderAttachment(kColorFormat, 1, 1, 1, /*transientAttachment=*/false,
+                                         /*supportsTextureBinding=*/true);
+    auto singleSampledTextureView1 = singleSampledTexture1.CreateView();
+
+    auto singleSampledTexture2 =
+        CreateTextureForRenderAttachment(kColorFormat, 1, 1, 1, /*transientAttachment=*/false,
+                                         /*supportsTextureBinding=*/true);
+    auto singleSampledTextureView2 = singleSampledTexture2.CreateView();
+
+    wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+    wgpu::RenderPipeline pipeline = CreateRenderPipelineWithTwoOutputsForTest(
+        /*sampleMask=*/0xFFFFFFFF, /*alphaToCoverageEnabled=*/false);
+
+    constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
+    constexpr wgpu::Color kRed = {0.8f, 0.0f, 0.0f, 0.8f};
+
+    // In first render pass:
+    // - we draw a red triangle to attachment0. StoreOp=Store.
+    // - we draw a green triangle to attachment1. StoreOp=Discard.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {multiSampledTextureView1, multiSampledTextureView2},
+            {singleSampledTextureView1, singleSampledTextureView2}, wgpu::LoadOp::Clear,
+            wgpu::LoadOp::Clear,
+            /*testDepth=*/false);
+        renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Store;
+        renderPass.cColorAttachments[1].storeOp = wgpu::StoreOp::Discard;
+
+        std::array<float, 8> kUniformData = {
+            static_cast<float>(kRed.r),   static_cast<float>(kRed.g),
+            static_cast<float>(kRed.b),   static_cast<float>(kRed.a),
+            static_cast<float>(kGreen.r), static_cast<float>(kGreen.g),
+            static_cast<float>(kGreen.b), static_cast<float>(kGreen.a)};
+        constexpr uint32_t kSize = sizeof(kUniformData);
+
+        EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, kUniformData.data(), kSize);
+    }
+
+    // In second render pass:
+    // - we only use LoadOp::Load for attachment0 with no draw call.
+    // - we only use LoadOp::ExpandResolveTexture for attachment1 with no draw call.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {multiSampledTextureView1, multiSampledTextureView2},
+            {singleSampledTextureView1, singleSampledTextureView2}, wgpu::LoadOp::Clear,
+            wgpu::LoadOp::Clear,
+            /*testDepth=*/false);
+        renderPass.cColorAttachments[0].loadOp = wgpu::LoadOp::Load;
+        renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Discard;
+        renderPass.cColorAttachments[1].loadOp = wgpu::LoadOp::ExpandResolveTexture;
+        renderPass.cColorAttachments[1].storeOp = wgpu::StoreOp::Discard;
+        wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
+        renderPassEncoder.End();
+    }
+
+    wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
+    queue.Submit(1, &commandBuffer);
+
+    VerifyResolveTarget(kRed, singleSampledTexture1);
+    VerifyResolveTarget(kGreen, singleSampledTexture2);
+}
+
+// Test rendering into 2 attachments. The both attachments will use
+// LoadOp::ExpandResolveTexture.
+TEST_P(DawnLoadResolveTextureTest, TwoOutputsDrawThenLoadColor0AndColor1) {
+    auto multiSampledTexture1 = CreateTextureForRenderAttachment(
+        kColorFormat, 4, 1, 1,
+        /*transientAttachment=*/device.HasFeature(wgpu::FeatureName::TransientAttachments),
+        /*supportsTextureBinding=*/false);
+    auto multiSampledTextureView1 = multiSampledTexture1.CreateView();
+
+    auto multiSampledTexture2 = CreateTextureForRenderAttachment(
+        kColorFormat, 4, 1, 1,
+        /*transientAttachment=*/device.HasFeature(wgpu::FeatureName::TransientAttachments),
+        /*supportsTextureBinding=*/false);
+    auto multiSampledTextureView2 = multiSampledTexture2.CreateView();
+
+    auto singleSampledTexture1 =
+        CreateTextureForRenderAttachment(kColorFormat, 1, 1, 1, /*transientAttachment=*/false,
+                                         /*supportsTextureBinding=*/true);
+    auto singleSampledTextureView1 = singleSampledTexture1.CreateView();
+
+    auto singleSampledTexture2 =
+        CreateTextureForRenderAttachment(kColorFormat, 1, 1, 1, /*transientAttachment=*/false,
+                                         /*supportsTextureBinding=*/true);
+    auto singleSampledTextureView2 = singleSampledTexture2.CreateView();
+
+    wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+    wgpu::RenderPipeline pipeline = CreateRenderPipelineWithTwoOutputsForTest(
+        /*sampleMask=*/0xFFFFFFFF, /*alphaToCoverageEnabled=*/false);
+
+    constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
+    constexpr wgpu::Color kRed = {0.8f, 0.0f, 0.0f, 0.8f};
+
+    // In first render pass:
+    // - we draw a red triangle to attachment0. StoreOp=Discard.
+    // - we draw a green triangle to attachment1. StoreOp=Discard.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {multiSampledTextureView1, multiSampledTextureView2},
+            {singleSampledTextureView1, singleSampledTextureView2}, wgpu::LoadOp::Clear,
+            wgpu::LoadOp::Clear,
+            /*testDepth=*/false);
+        renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Discard;
+        renderPass.cColorAttachments[1].storeOp = wgpu::StoreOp::Discard;
+
+        std::array<float, 8> kUniformData = {
+            static_cast<float>(kRed.r),   static_cast<float>(kRed.g),
+            static_cast<float>(kRed.b),   static_cast<float>(kRed.a),
+            static_cast<float>(kGreen.r), static_cast<float>(kGreen.g),
+            static_cast<float>(kGreen.b), static_cast<float>(kGreen.a)};
+        constexpr uint32_t kSize = sizeof(kUniformData);
+
+        EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, kUniformData.data(), kSize);
+    }
+
+    // In second render pass:
+    // - we only use LoadOp::ExpandResolveTexture for attachment0 with no draw call.
+    // - we only use LoadOp::ExpandResolveTexture for attachment1 with no draw call.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {multiSampledTextureView1, multiSampledTextureView2},
+            {singleSampledTextureView1, singleSampledTextureView2}, wgpu::LoadOp::Clear,
+            wgpu::LoadOp::Clear,
+            /*testDepth=*/false);
+        renderPass.cColorAttachments[0].loadOp = wgpu::LoadOp::ExpandResolveTexture;
+        renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Discard;
+        renderPass.cColorAttachments[1].loadOp = wgpu::LoadOp::ExpandResolveTexture;
+        renderPass.cColorAttachments[1].storeOp = wgpu::StoreOp::Discard;
+        wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
+        renderPassEncoder.End();
+    }
+
+    wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
+    queue.Submit(1, &commandBuffer);
+
+    VerifyResolveTarget(kRed, singleSampledTexture1);
+    VerifyResolveTarget(kGreen, singleSampledTexture2);
+}
+
 // Test ExpandResolveTexture load op rendering with depth test works correctly.
 TEST_P(DawnLoadResolveTextureTest, DrawWithDepthTest) {
     auto multiSampledTexture = CreateTextureForRenderAttachment(
@@ -1661,6 +1954,112 @@
     VerifyResolveTarget(kGreen, singleSampledTexture);
 }
 
+// Test ExpandResolveTexture load op rendering with depth test works correctly with
+// two outputs both use ExpandResolveTexture load op.
+TEST_P(DawnLoadResolveTextureTest, TwoOutputsDrawWithDepthTestColor0AndColor1) {
+    auto multiSampledTexture1 = CreateTextureForRenderAttachment(
+        kColorFormat, 4, 1, 1,
+        /*transientAttachment=*/device.HasFeature(wgpu::FeatureName::TransientAttachments),
+        /*supportsTextureBinding=*/false);
+    auto multiSampledTextureView1 = multiSampledTexture1.CreateView();
+
+    auto multiSampledTexture2 = CreateTextureForRenderAttachment(
+        kColorFormat, 4, 1, 1,
+        /*transientAttachment=*/device.HasFeature(wgpu::FeatureName::TransientAttachments),
+        /*supportsTextureBinding=*/false);
+    auto multiSampledTextureView2 = multiSampledTexture2.CreateView();
+
+    auto singleSampledTexture1 =
+        CreateTextureForRenderAttachment(kColorFormat, 1, 1, 1, /*transientAttachment=*/false,
+                                         /*supportsTextureBinding=*/true);
+    auto singleSampledTextureView1 = singleSampledTexture1.CreateView();
+
+    auto singleSampledTexture2 =
+        CreateTextureForRenderAttachment(kColorFormat, 1, 1, 1, /*transientAttachment=*/false,
+                                         /*supportsTextureBinding=*/true);
+    auto singleSampledTextureView2 = singleSampledTexture2.CreateView();
+
+    wgpu::RenderPipeline pipeline = CreateRenderPipelineWithTwoOutputsForTest(
+        /*sampleMask=*/0xFFFFFFFF, /*alphaToCoverageEnabled=*/false,
+        /*depthTest=*/true,
+        /*enableExpandResolveLoadOpForColor0=*/true,
+        /*enableExpandResolveLoadOpForColor1=*/true);
+
+    wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+
+    constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
+    constexpr wgpu::Color kRed = {0.8f, 0.0f, 0.0f, 0.8f};
+
+    // In first render pass we clear the render pass without drawing anything
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {multiSampledTextureView1, multiSampledTextureView2},
+            {singleSampledTextureView1, singleSampledTextureView2}, wgpu::LoadOp::Clear,
+            wgpu::LoadOp::Clear,
+            /*testDepth=*/true);
+        renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Discard;
+        renderPass.cColorAttachments[1].storeOp = wgpu::StoreOp::Discard;
+
+        wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
+        renderPassEncoder.End();
+    }
+
+    // In 2nd render pass:
+    // - we draw a red triangle to attachment0.
+    // - we draw a green triangle to attachment1.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {multiSampledTextureView1, multiSampledTextureView2},
+            {singleSampledTextureView1, singleSampledTextureView2},
+            wgpu::LoadOp::ExpandResolveTexture, wgpu::LoadOp::Clear,
+            /*testDepth=*/true);
+        renderPass.cColorAttachments[0].loadOp = wgpu::LoadOp::ExpandResolveTexture;
+        renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Discard;
+        renderPass.cColorAttachments[1].loadOp = wgpu::LoadOp::ExpandResolveTexture;
+        renderPass.cColorAttachments[1].storeOp = wgpu::StoreOp::Discard;
+
+        std::array<float, 8> kUniformData = {
+            static_cast<float>(kRed.r),   static_cast<float>(kRed.g),
+            static_cast<float>(kRed.b),   static_cast<float>(kRed.a),
+            static_cast<float>(kGreen.r), static_cast<float>(kGreen.g),
+            static_cast<float>(kGreen.b), static_cast<float>(kGreen.a)};
+        constexpr uint32_t kSize = sizeof(kUniformData);
+
+        EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, kUniformData.data(), kSize);
+    }
+
+    // In 3rd render pass:
+    // - we draw a green triangle to attachment0.
+    // - we draw a red triangle to attachment1.
+    // Both triangles should not pass the depth test.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {multiSampledTextureView1, multiSampledTextureView2},
+            {singleSampledTextureView1, singleSampledTextureView2},
+            wgpu::LoadOp::ExpandResolveTexture, wgpu::LoadOp::Load,
+            /*testDepth=*/true);
+        renderPass.cColorAttachments[0].loadOp = wgpu::LoadOp::ExpandResolveTexture;
+        renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Discard;
+        renderPass.cColorAttachments[1].loadOp = wgpu::LoadOp::ExpandResolveTexture;
+        renderPass.cColorAttachments[1].storeOp = wgpu::StoreOp::Discard;
+
+        std::array<float, 8> kUniformData = {
+            static_cast<float>(kGreen.r), static_cast<float>(kGreen.g),
+            static_cast<float>(kGreen.b), static_cast<float>(kGreen.a),
+            static_cast<float>(kRed.r),   static_cast<float>(kRed.g),
+            static_cast<float>(kRed.b),   static_cast<float>(kRed.a)};
+        constexpr uint32_t kSize = sizeof(kUniformData);
+
+        EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, kUniformData.data(), kSize);
+    }
+
+    wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
+    queue.Submit(1, &commandBuffer);
+
+    VerifyResolveTarget(kRed, singleSampledTexture1);
+    VerifyResolveTarget(kGreen, singleSampledTexture2);
+}
+
 DAWN_INSTANTIATE_TEST(MultisampledRenderingTest,
                       D3D11Backend(),
                       D3D12Backend(),
diff --git a/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp b/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp
index 13198c3..a40a8ed 100644
--- a/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp
@@ -1981,30 +1981,11 @@
     AssertBeginRenderPassError(&renderPass, testing::HasSubstr("does not support resolve"));
 }
 
-// LoadOp::ExpandResolveTexture can only be used in a render pass with single color attachment.
-// The LoadOp is NOT currently supported on depth/stencil attachment either.
-TEST_F(DawnLoadResolveTextureValidationTest, OnlyLoadingSingleColorAttachmentIsSupported) {
+// The LoadOp is NOT currently supported on depth/stencil attachment.
+TEST_F(DawnLoadResolveTextureValidationTest, OnlyLoadingColorAttachmentIsSupported) {
     auto multisampledColorTextureView = CreateMultisampledColorTextureView();
     auto resolveTarget = CreateCompatibleResolveTextureView();
 
-    // Error case: Use ExpandResolveTexture with multiple color attachments.
-    {
-        auto multisampledColorTextureView2 = CreateMultisampledColorTextureView();
-        auto resolveTarget2 = CreateCompatibleResolveTextureView();
-
-        auto renderPass = CreateMultisampledRenderPass();
-        renderPass.colorAttachmentCount = 2;
-        renderPass.cColorAttachments[0].view = multisampledColorTextureView;
-        renderPass.cColorAttachments[0].resolveTarget = resolveTarget;
-        renderPass.cColorAttachments[0].loadOp = wgpu::LoadOp::ExpandResolveTexture;
-
-        renderPass.cColorAttachments[1].view = multisampledColorTextureView2;
-        renderPass.cColorAttachments[1].resolveTarget = resolveTarget2;
-        renderPass.cColorAttachments[1].loadOp = wgpu::LoadOp::ExpandResolveTexture;
-
-        AssertBeginRenderPassError(&renderPass, testing::HasSubstr("colorAttachmentCount"));
-    }
-
     // Error case: Use ExpandResolveTexture on depth/stencil attachment.
     {
         // Create depth stencil texture with sample count = 4.
diff --git a/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp b/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
index 8da4cc7..b8a7494 100644
--- a/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
@@ -2336,6 +2336,13 @@
             @fragment fn main() -> @location(0) vec4f {
                 return textureLoad(src_tex, vec2u(0, 0), 0);
             })");
+
+        fsWithTextureToTarget1Module = utils::CreateShaderModule(device, R"(
+            @group(0) @binding(0) var src_tex : texture_2d<f32>;
+
+            @fragment fn main() -> @location(1) vec4f {
+                return textureLoad(src_tex, vec2u(0, 0), 0);
+            })");
     }
 
     WGPUDevice CreateTestDevice(dawn::native::Adapter dawnAdapter,
@@ -2359,6 +2366,7 @@
     static constexpr wgpu::TextureFormat kColorFormat = wgpu::TextureFormat::RGBA8Unorm;
 
     wgpu::ShaderModule fsWithTextureModule;
+    wgpu::ShaderModule fsWithTextureToTarget1Module;
 };
 
 // Test that creating and using a render pipeline with ColorTargetStateExpandResolveTextureDawn
@@ -2405,6 +2413,55 @@
     encoder.Finish();
 }
 
+// Test that creating and using a render pipeline with ColorTargetStateExpandResolveTextureDawn
+// chained struct in a non-zero indexed attachment should success.
+TEST_F(LoadResolveTexturePipelineDescriptorValidationTest, UseInNonZeroIndexedAttachment) {
+    constexpr uint32_t kSampleCount = 4;
+
+    auto msaaTexture = CreateTexture(wgpu::TextureUsage::RenderAttachment, kSampleCount);
+
+    // Create single sampled texture.
+    auto texture =
+        CreateTexture(wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding, 1);
+
+    // Create render pass (with ExpandResolveTexture load op).
+    utils::ComboRenderPassDescriptor renderPassDescriptor({nullptr, msaaTexture.CreateView()});
+    renderPassDescriptor.cColorAttachments[1].loadOp = wgpu::LoadOp::ExpandResolveTexture;
+    renderPassDescriptor.cColorAttachments[1].resolveTarget = texture.CreateView();
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    wgpu::RenderPassEncoder renderPass = encoder.BeginRenderPass(&renderPassDescriptor);
+
+    // Create render pipeline
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.vertex.module = vsModule;
+    pipelineDescriptor.cFragment.module = fsWithTextureToTarget1Module;
+    pipelineDescriptor.multisample.count = kSampleCount;
+    pipelineDescriptor.cFragment.targetCount = 2;
+    pipelineDescriptor.cTargets[0].format = wgpu::TextureFormat::Undefined;
+    pipelineDescriptor.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
+    pipelineDescriptor.cTargets[1].format = kColorFormat;
+    pipelineDescriptor.cTargets[1].writeMask = wgpu::ColorWriteMask::All;
+
+    wgpu::ColorTargetStateExpandResolveTextureDawn pipelineMSAAExpandResolveDesc;
+    pipelineMSAAExpandResolveDesc.enabled = true;
+    pipelineDescriptor.cTargets[1].nextInChain = &pipelineMSAAExpandResolveDesc;
+
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
+
+    // Input texture.
+    auto sampledTexture = CreateTexture(wgpu::TextureUsage::TextureBinding, 1);
+    wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+                                                     {{0, sampledTexture.CreateView()}});
+
+    renderPass.SetPipeline(pipeline);
+    renderPass.SetBindGroup(0, bindGroup);
+    renderPass.Draw(3);
+    renderPass.End();
+
+    encoder.Finish();
+}
+
 // If a render pipeline's MultisampleState contains ColorTargetStateExpandResolveTextureDawn
 // chained struct. Then its sampleCount must be > 1.
 TEST_F(LoadResolveTexturePipelineDescriptorValidationTest,