Update DawnLoadResolveTexture's render pass compatibility spec

Because Vulkan's render pass compatibility requires that if the
render passes have more than one subpass then:
- All attachments (color, input, resolve, depth/stencil) **must** be the
same.
We need to know that a render pipeline's color target has any resolve
target or not in order to query the appropriate render pass.

Previously, `ColorTargetStateExpandResolveTextureDawn` only told us
whether a color target uses `ExpandResolveTexture` load op or not. But
it didn't tell us whether the color target has a resolve target but
uses another type of load op (e.g. `Load`). Consequencely this only
enabled us to query a vulkan render pass with correct input attachments,
but not resolve attachments.

This CL updates the DawnLoadResolveTexture's spec:
- If `ColorTargetStateExpandResolveTextureDawn` is chained to a render
pipeline's ColorTargetState. Then it is implicitly implied that a
resolve target is present at that location.
  - If the struct's enabled flag is true then it's understood that the
  respective color target does use `ExpandResolveTexture` load op.
- If `ColorTargetStateExpandResolveTextureDawn` is not chained. Then
it is implied that the corresponding color target doesn't have resolve
texture.

Bug: dawn:1710
Change-Id: I23115930dafdaf506995eaafa9bd4d0200e1f278
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/189980
Reviewed-by: Austin Eng <enga@chromium.org>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: 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 880f52d..7da8a3f 100644
--- a/docs/dawn/features/dawn_load_resolve_texture.md
+++ b/docs/dawn/features/dawn_load_resolve_texture.md
@@ -65,7 +65,12 @@
 
 Notes:
  - 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.
+ - The `wgpu::ColorTargetStateExpandResolveTextureDawn` chained struct controls the compatibility between a render pipeline and a render pass:
+  - If the chained struct is not included in any `wgpu::RenderPipelineDescriptor::FragmentState::ColorTargetState` then the render pipeline **can only** be used on any render pass not using any `ExpandResolveTexture` load op. Whether the render pass has any resolve target doesn't matter.
+  - If the chained struct is included in some color targets but **none** of their `enabled` boolean flags are true, then it's the same as above case.
+  - If at least one included chained struct has `enabled` = true, then the compatibility's requirements are stricter:
+    - If render pipeline's color target `i` has `wgpu::ColorTargetStateExpandResolveTextureDawn.enabled` = true, then the compatible render pass **must** have `ExpandResolveTexture` load op on attachment `i`.
+    - If render pipeline's color target `i` has `wgpu::ColorTargetStateExpandResolveTextureDawn.enabled` = false, then the compatible render pass's attachment `i` must have a resolve target and its load op **must not** be `ExpandResolveTexture`.
+    - If render pipeline's color target `i` has no `wgpu::ColorTargetStateExpandResolveTextureDawn` included, then the compatible render pass's attachment `i` **must not** have any resolve target.
  - 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 75ac8dc..6432976 100644
--- a/src/dawn/native/AttachmentState.cpp
+++ b/src/dawn/native/AttachmentState.cpp
@@ -78,11 +78,26 @@
                 UnpackedPtr<ColorTargetState> unpackedTarget = Unpack(&target);
                 if (auto* expandResolveState =
                         unpackedTarget.Get<ColorTargetStateExpandResolveTextureDawn>()) {
-                    mAttachmentsToExpandResolve.set(i, expandResolveState->enabled);
+                    mExpandResolveInfo.attachmentsToExpandResolve.set(i,
+                                                                      expandResolveState->enabled);
+                    // The presence of ColorTargetStateExpandResolveTextureDawn implies that
+                    // this color target has a resolve target. Doesn't matter `enabled` is true or
+                    // not.
+                    mExpandResolveInfo.resolveTargetsMask.set(i);
                 }
             }
         }
     }
+    if (!mExpandResolveInfo.attachmentsToExpandResolve.any()) {
+        // If render pipeline doesn't have any color target using ExpandResolveTexture load op then
+        // ignore resolve targets. This is to relax compatibility requirement for common cases
+        // where ExpandResolveTexture is not used.
+        mExpandResolveInfo.resolveTargetsMask.reset();
+    }
+
+    DAWN_ASSERT(IsSubset(mExpandResolveInfo.attachmentsToExpandResolve,
+                         mExpandResolveInfo.resolveTargetsMask));
+
     if (descriptor->depthStencil != nullptr) {
         mDepthStencilFormat = descriptor->depthStencil->format;
     }
@@ -124,8 +139,9 @@
         }
 
         if (colorAttachment.loadOp == wgpu::LoadOp::ExpandResolveTexture) {
-            mAttachmentsToExpandResolve.set(i);
+            mExpandResolveInfo.attachmentsToExpandResolve.set(i);
         }
+        mExpandResolveInfo.resolveTargetsMask.set(i, colorAttachment.resolveTarget);
     }
 
     // Gather the depth-stencil information.
@@ -139,6 +155,15 @@
         }
     }
 
+    if (!mExpandResolveInfo.attachmentsToExpandResolve.any()) {
+        // If render pass doesn't have any color attachment using ExpandResolveTexture load op then
+        // ignore resolve targets. This is to relax compatibility requirement for common cases
+        // where ExpandResolveTexture is not used.
+        mExpandResolveInfo.resolveTargetsMask.reset();
+    }
+    DAWN_ASSERT(IsSubset(mExpandResolveInfo.attachmentsToExpandResolve,
+                         mExpandResolveInfo.resolveTargetsMask));
+
     // Gather the PLS information.
     if (auto* pls = descriptor.Get<RenderPassPixelLocalStorage>()) {
         mHasPLS = true;
@@ -167,9 +192,11 @@
     mColorFormats = blueprint.mColorFormats;
     mDepthStencilFormat = blueprint.mDepthStencilFormat;
     mSampleCount = blueprint.mSampleCount;
-    mAttachmentsToExpandResolve = blueprint.mAttachmentsToExpandResolve;
+    mExpandResolveInfo = blueprint.mExpandResolveInfo;
     mHasPLS = blueprint.mHasPLS;
     mStorageAttachmentSlots = blueprint.mStorageAttachmentSlots;
+    DAWN_ASSERT(IsSubset(mExpandResolveInfo.attachmentsToExpandResolve,
+                         mExpandResolveInfo.resolveTargetsMask));
     SetContentHash(blueprint.GetContentHash());
 }
 
@@ -202,8 +229,13 @@
         return false;
     }
 
-    // Both attachment state must either enable MSAA render to single sampled or disable it.
-    if (a->mAttachmentsToExpandResolve != b->mAttachmentsToExpandResolve) {
+    // Both attachment state must have the same `ExpandResolveTexture` load ops.
+    if (a->mExpandResolveInfo.attachmentsToExpandResolve !=
+        b->mExpandResolveInfo.attachmentsToExpandResolve) {
+        return false;
+    }
+
+    if (a->mExpandResolveInfo.resolveTargetsMask != b->mExpandResolveInfo.resolveTargetsMask) {
         return false;
     }
 
@@ -238,8 +270,9 @@
     // Hash sample count
     HashCombine(&hash, mSampleCount);
 
-    // Hash MSAA render to single sampled flag
-    HashCombine(&hash, mAttachmentsToExpandResolve);
+    // Hash expand resolve load op bits
+    HashCombine(&hash, mExpandResolveInfo.attachmentsToExpandResolve);
+    HashCombine(&hash, mExpandResolveInfo.resolveTargetsMask);
 
     // Hash the PLS state
     HashCombine(&hash, mHasPLS);
@@ -272,8 +305,8 @@
     return mSampleCount;
 }
 
-ColorAttachmentMask AttachmentState::GetExpandResolveUsingAttachmentsMask() const {
-    return mAttachmentsToExpandResolve;
+const AttachmentState::ExpandResolveInfo& AttachmentState::GetExpandResolveInfo() const {
+    return mExpandResolveInfo;
 }
 
 bool AttachmentState::HasPixelLocalStorage() const {
diff --git a/src/dawn/native/AttachmentState.h b/src/dawn/native/AttachmentState.h
index a320818..4dcbf98 100644
--- a/src/dawn/native/AttachmentState.h
+++ b/src/dawn/native/AttachmentState.h
@@ -50,6 +50,14 @@
                               public CachedObject,
                               public ContentLessObjectCacheable<AttachmentState> {
   public:
+    struct ExpandResolveInfo {
+        // Mask indicates which attachments use `ExpandResolveTexture` load op.
+        ColorAttachmentMask attachmentsToExpandResolve;
+        // Mask indicates which attachments have resolve target. Only enabled if
+        // attachmentsToExpandResolve has any bit set.
+        ColorAttachmentMask resolveTargetsMask;
+    };
+
     // Note: Descriptors must be validated before the AttachmentState is constructed.
     explicit AttachmentState(DeviceBase* device, const RenderBundleEncoderDescriptor* descriptor);
     explicit AttachmentState(DeviceBase* device,
@@ -66,7 +74,7 @@
     bool HasDepthStencilAttachment() const;
     wgpu::TextureFormat GetDepthStencilFormat() const;
     uint32_t GetSampleCount() const;
-    ColorAttachmentMask GetExpandResolveUsingAttachmentsMask() const;
+    const ExpandResolveInfo& GetExpandResolveInfo() const;
     bool HasPixelLocalStorage() const;
     const std::vector<wgpu::TextureFormat>& GetStorageAttachmentSlots() const;
     std::vector<ColorAttachmentIndex> ComputeStorageAttachmentPackingInColorAttachments() const;
@@ -86,7 +94,8 @@
     wgpu::TextureFormat mDepthStencilFormat = wgpu::TextureFormat::Undefined;
     uint32_t mSampleCount = 0;
 
-    ColorAttachmentMask mAttachmentsToExpandResolve;
+    ExpandResolveInfo mExpandResolveInfo;
+
     bool mHasPLS = false;
     std::vector<wgpu::TextureFormat> mStorageAttachmentSlots;
 };
diff --git a/src/dawn/native/BlitColorToColorWithDraw.cpp b/src/dawn/native/BlitColorToColorWithDraw.cpp
index cd382db..9c78f70 100644
--- a/src/dawn/native/BlitColorToColorWithDraw.cpp
+++ b/src/dawn/native/BlitColorToColorWithDraw.cpp
@@ -120,14 +120,19 @@
 
     // Color target states.
     PerColorAttachment<ColorTargetState> colorTargets = {};
-    wgpu::ColorTargetStateExpandResolveTextureDawn msaaExpandResolveState;
-    msaaExpandResolveState.enabled = true;
+    PerColorAttachment<wgpu::ColorTargetStateExpandResolveTextureDawn> msaaExpandResolveStates;
 
     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;
+        if (pipelineKey.resolveTargetsMask[i]) {
+            target.nextInChain = &msaaExpandResolveStates[i];
+            msaaExpandResolveStates[i].enabled = pipelineKey.attachmentsToExpandResolve[i];
+            if (msaaExpandResolveStates[i].enabled) {
+                target.writeMask = wgpu::ColorWriteMask::All;
+            } else {
+                target.writeMask = wgpu::ColorWriteMask::None;
+            }
         } else {
             target.writeMask = wgpu::ColorWriteMask::None;
         }
@@ -212,6 +217,7 @@
                         wgpu::TextureViewDimension::e2D);
             pipelineKey.attachmentsToExpandResolve.set(colorIdx);
         }
+        pipelineKey.resolveTargetsMask.set(colorIdx, colorAttachment.resolveTarget);
 
         pipelineKey.colorTargetFormats[colorIdx] = format.format;
         pipelineKey.sampleCount = view->GetTexture()->GetSampleCount();
@@ -267,6 +273,7 @@
     size_t hash = 0;
 
     HashCombine(&hash, key.attachmentsToExpandResolve);
+    HashCombine(&hash, key.resolveTargetsMask);
 
     for (auto format : key.colorTargetFormats) {
         HashCombine(&hash, format);
@@ -284,6 +291,9 @@
     if (a.attachmentsToExpandResolve != b.attachmentsToExpandResolve) {
         return false;
     }
+    if (a.resolveTargetsMask != b.resolveTargetsMask) {
+        return false;
+    }
 
     for (auto [i, format] : Enumerate(a.colorTargetFormats)) {
         if (format != b.colorTargetFormats[i]) {
diff --git a/src/dawn/native/BlitColorToColorWithDraw.h b/src/dawn/native/BlitColorToColorWithDraw.h
index d418e40..ac5463a 100644
--- a/src/dawn/native/BlitColorToColorWithDraw.h
+++ b/src/dawn/native/BlitColorToColorWithDraw.h
@@ -50,6 +50,7 @@
 
     PerColorAttachment<wgpu::TextureFormat> colorTargetFormats;
     ColorAttachmentMask attachmentsToExpandResolve;
+    ColorAttachmentMask resolveTargetsMask;
     wgpu::TextureFormat depthStencilFormat = wgpu::TextureFormat::Undefined;
     uint32_t sampleCount = 1;
 
diff --git a/src/dawn/native/webgpu_absl_format.cpp b/src/dawn/native/webgpu_absl_format.cpp
index 4b78122..90bc866 100644
--- a/src/dawn/native/webgpu_absl_format.cpp
+++ b/src/dawn/native/webgpu_absl_format.cpp
@@ -336,16 +336,20 @@
         }
 
         while (nextColorIndex < i) {
-            s->Append(absl::StrFormat("{format: %s}, ", wgpu::TextureFormat::Undefined));
+            s->Append(absl::StrFormat("%d={format: %s}, ", nextColorIndex,
+                                      wgpu::TextureFormat::Undefined));
             nextColorIndex++;
             needsComma = false;
         }
 
-        s->Append(absl::StrFormat("{format:%s", value->GetColorAttachmentFormat(i)));
+        s->Append(absl::StrFormat("%d={format:%s", i, value->GetColorAttachmentFormat(i)));
 
-        if (value->GetDevice()->HasFeature(Feature::DawnLoadResolveTexture)) {
-            s->Append(absl::StrFormat(", expandResolveTexture:%v",
-                                      value->GetExpandResolveUsingAttachmentsMask().test(i)));
+        if (value->GetDevice()->HasFeature(Feature::DawnLoadResolveTexture) &&
+            value->GetExpandResolveInfo().attachmentsToExpandResolve.any()) {
+            s->Append(
+                absl::StrFormat(", resolve:%v, expandResolve:%v",
+                                value->GetExpandResolveInfo().resolveTargetsMask.test(i),
+                                value->GetExpandResolveInfo().attachmentsToExpandResolve.test(i)));
         }
         s->Append("}");
 
diff --git a/src/dawn/tests/end2end/MultisampledRenderingTests.cpp b/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
index fefc26f..e72e23a 100644
--- a/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
+++ b/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
@@ -26,7 +26,7 @@
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 #include <algorithm>
-#include <bitset>
+#include <array>
 #include <vector>
 
 #include "dawn/common/Assert.h"
@@ -37,7 +37,12 @@
 namespace dawn {
 namespace {
 
-using AttachmentMask = std::bitset<16>;
+enum class PipelineMultisampleLoadOp {
+    Ignore,
+    ExpandResolveTarget,
+    HasResolveTargetButLoadMultisampled,
+};
+using PipelineMultisampleLoadOps = std::array<PipelineMultisampleLoadOp, 16>;
 
 class MultisampledRenderingTest : public DawnTest {
   protected:
@@ -97,18 +102,20 @@
 
         const char* fs = testDepth ? kFsOneOutputWithDepth : kFsOneOutputWithoutDepth;
 
-        AttachmentMask enableExpandResolveLoadOps;
-        enableExpandResolveLoadOps.set(0, enableExpandResolveLoadOp);
+        PipelineMultisampleLoadOps multisampleLoadOps{};
+        if (enableExpandResolveLoadOp) {
+            multisampleLoadOps[0] = PipelineMultisampleLoadOp::ExpandResolveTarget;
+        }
         return CreateRenderPipelineForTest(fs, 1, testDepth, sampleMask, alphaToCoverageEnabled,
-                                           flipTriangle, enableExpandResolveLoadOps);
+                                           flipTriangle, multisampleLoadOps);
     }
 
     wgpu::RenderPipeline CreateRenderPipelineWithTwoOutputsForTest(
         uint32_t sampleMask = 0xFFFFFFFF,
         bool alphaToCoverageEnabled = false,
         bool depthTest = false,
-        bool enableExpandResolveLoadOpForColor0 = false,
-        bool enableExpandResolveLoadOpForColor1 = false) {
+        PipelineMultisampleLoadOp loadOpForColor0 = PipelineMultisampleLoadOp::Ignore,
+        PipelineMultisampleLoadOp loadOpForColor1 = PipelineMultisampleLoadOp::Ignore) {
         const char* kFsTwoOutputs = R"(
             struct U {
                 color0 : vec4f,
@@ -128,13 +135,13 @@
                 return output;
             })";
 
-        AttachmentMask enableExpandResolveLoadOps;
-        enableExpandResolveLoadOps.set(0, enableExpandResolveLoadOpForColor0);
-        enableExpandResolveLoadOps.set(1, enableExpandResolveLoadOpForColor1);
+        PipelineMultisampleLoadOps multisampleLoadOps{};
+        multisampleLoadOps[0] = loadOpForColor0;
+        multisampleLoadOps[1] = loadOpForColor1;
 
         return CreateRenderPipelineForTest(kFsTwoOutputs, 2, depthTest, sampleMask,
                                            alphaToCoverageEnabled, /*flipTriangle=*/false,
-                                           enableExpandResolveLoadOps);
+                                           multisampleLoadOps);
     }
 
     wgpu::RenderPipeline CreateRenderPipelineWithNonZeroLocationOutputForTest(
@@ -151,11 +158,13 @@
                 return uBuffer.color;
             })";
 
-        AttachmentMask enableExpandResolveLoadOps = {};
-        enableExpandResolveLoadOps.set(1, enableExpandResolveLoadOp);
+        PipelineMultisampleLoadOps multisampleLoadOps{};
+        if (enableExpandResolveLoadOp) {
+            multisampleLoadOps[1] = PipelineMultisampleLoadOp::ExpandResolveTarget;
+        }
         return CreateRenderPipelineForTest(kFsNonZeroLocationOutputs, 1, false, sampleMask,
                                            alphaToCoverageEnabled, /*flipTriangle=*/false,
-                                           enableExpandResolveLoadOps, 1);
+                                           multisampleLoadOps, 1);
     }
 
     wgpu::Texture CreateTextureForRenderAttachment(wgpu::TextureFormat format,
@@ -278,14 +287,15 @@
     wgpu::Texture mDepthStencilTexture;
     wgpu::TextureView mDepthStencilView;
 
-    wgpu::RenderPipeline CreateRenderPipelineForTest(const char* fs,
-                                                     uint32_t numColorAttachments,
-                                                     bool hasDepthStencilAttachment,
-                                                     uint32_t sampleMask = 0xFFFFFFFF,
-                                                     bool alphaToCoverageEnabled = false,
-                                                     bool flipTriangle = false,
-                                                     AttachmentMask enableExpandResolveLoadOps = {},
-                                                     uint32_t firstAttachmentLocation = 0) {
+    wgpu::RenderPipeline CreateRenderPipelineForTest(
+        const char* fs,
+        uint32_t numColorAttachments,
+        bool hasDepthStencilAttachment,
+        uint32_t sampleMask = 0xFFFFFFFF,
+        bool alphaToCoverageEnabled = false,
+        bool flipTriangle = false,
+        PipelineMultisampleLoadOps multisampleLoadOps = {},
+        uint32_t firstAttachmentLocation = 0) {
         utils::ComboRenderPipelineDescriptor pipelineDescriptor;
 
         // Draw a bottom-right triangle. In standard 4xMSAA pattern, for the pixels on diagonal,
@@ -334,16 +344,17 @@
 
         pipelineDescriptor.cFragment.targetCount = numColorAttachments + firstAttachmentLocation;
 
-        wgpu::ColorTargetStateExpandResolveTextureDawn msaaExpandResolveDesc;
-        msaaExpandResolveDesc.enabled = true;
+        std::array<wgpu::ColorTargetStateExpandResolveTextureDawn, 16> msaaExpandResolveDescs;
         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;
+                if (multisampleLoadOps[i] != PipelineMultisampleLoadOp::Ignore) {
+                    msaaExpandResolveDescs[i].enabled =
+                        multisampleLoadOps[i] == PipelineMultisampleLoadOp::ExpandResolveTarget;
+                    pipelineDescriptor.cTargets[i].nextInChain = &msaaExpandResolveDescs[i];
                 }
             }
         }
@@ -1982,8 +1993,8 @@
     wgpu::RenderPipeline pipeline = CreateRenderPipelineWithTwoOutputsForTest(
         /*sampleMask=*/0xFFFFFFFF, /*alphaToCoverageEnabled=*/false,
         /*depthTest=*/true,
-        /*enableExpandResolveLoadOpForColor0=*/true,
-        /*enableExpandResolveLoadOpForColor1=*/true);
+        /*loadOpForColor0=*/PipelineMultisampleLoadOp::ExpandResolveTarget,
+        /*loadOpForColor1=*/PipelineMultisampleLoadOp::ExpandResolveTarget);
 
     wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
 
diff --git a/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp b/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
index fb26be8..c83130c 100644
--- a/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
@@ -2329,6 +2329,19 @@
             @fragment fn main() -> @location(1) vec4f {
                 return textureLoad(src_tex, vec2u(0, 0), 0);
             })");
+
+        fs2TargetsModule = utils::CreateShaderModule(device, R"(
+            struct FragmentOut {
+                @location(0) color0 : vec4f,
+                @location(1) color1 : vec4f,
+            }
+
+            @fragment fn main() -> FragmentOut {
+                var output : FragmentOut;
+                output.color0 = vec4f(0.85);
+                output.color1 = output.color0 * 0.5;
+                return output;
+            })");
     }
 
     std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
@@ -2349,6 +2362,7 @@
 
     wgpu::ShaderModule fsWithTextureModule;
     wgpu::ShaderModule fsWithTextureToTarget1Module;
+    wgpu::ShaderModule fs2TargetsModule;
 };
 
 // Test that creating and using a render pipeline with ColorTargetStateExpandResolveTextureDawn
@@ -2529,6 +2543,172 @@
     ASSERT_DEVICE_ERROR(encoder.Finish());
 }
 
+// Test that the following scenario works:
+// - Render pipeline and render pass both have two color outputs with resolve targets.
+// - Only second output uses ExpandResolveTexture.
+TEST_F(LoadResolveTexturePipelineDescriptorValidationTest,
+       RenderPipelineAndRenderPassTwoOuputsLoadColor1) {
+    constexpr uint32_t kSampleCount = 4;
+
+    auto msaaTexture1 = CreateTexture(wgpu::TextureUsage::RenderAttachment, kSampleCount);
+    auto msaaTexture2 = CreateTexture(wgpu::TextureUsage::RenderAttachment, kSampleCount);
+
+    // Create single sampled textures.
+    auto texture1 =
+        CreateTexture(wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding, 1);
+    auto texture2 =
+        CreateTexture(wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding, 1);
+
+    // Create render pass with two outputs. Second one uses ExpandResolveTexture.
+    utils::ComboRenderPassDescriptor renderPassDescriptor(
+        {msaaTexture1.CreateView(), msaaTexture2.CreateView()});
+    renderPassDescriptor.cColorAttachments[0].loadOp = wgpu::LoadOp::Load;
+    renderPassDescriptor.cColorAttachments[0].resolveTarget = texture1.CreateView();
+    renderPassDescriptor.cColorAttachments[1].loadOp = wgpu::LoadOp::ExpandResolveTexture;
+    renderPassDescriptor.cColorAttachments[1].resolveTarget = texture2.CreateView();
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    wgpu::RenderPassEncoder renderPass = encoder.BeginRenderPass(&renderPassDescriptor);
+
+    // Create render pipeline with two chained ColorTargetStateExpandResolveTextureDawn.
+    // The first one's enabled flag = true.
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.vertex.module = vsModule;
+    pipelineDescriptor.cFragment.module = fs2TargetsModule;
+    pipelineDescriptor.multisample.count = kSampleCount;
+
+    wgpu::ColorTargetStateExpandResolveTextureDawn pipelineMSAAExpandResolveDesc[2];
+    pipelineMSAAExpandResolveDesc[0].enabled = false;
+    pipelineDescriptor.cTargets[0].format = kColorFormat;
+    pipelineDescriptor.cTargets[0].nextInChain = &pipelineMSAAExpandResolveDesc[0];
+    pipelineMSAAExpandResolveDesc[1].enabled = true;
+    pipelineDescriptor.cTargets[1].format = kColorFormat;
+    pipelineDescriptor.cTargets[1].nextInChain = &pipelineMSAAExpandResolveDesc[1];
+    pipelineDescriptor.cFragment.targetCount = 2;
+
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
+    renderPass.SetPipeline(pipeline);
+    renderPass.End();
+
+    encoder.Finish();
+}
+
+// Test that the following render pipeline and render pass are incompatible.
+// - Render pass:
+//   - colorAttachments[0]
+//     - has resolveTarget.
+//     - loadOp = ExpandResolveTexture.
+//   - colorAttachments[1]
+//     - has resolveTarget.
+//     - loadOp = Load.
+// - Render pipeline:
+//   - colorTargets[0].nextInChain contains ColorTargetStateExpandResolveTextureDawn.
+//     - ColorTargetStateExpandResolveTextureDawn.enabled = true.
+//   - colorTargets[1].nextInChain = null.
+// They are incompatible by spec's requirements.
+TEST_F(LoadResolveTexturePipelineDescriptorValidationTest,
+       RenderPipelineAndRenderPassMismatchResolveTargetsError) {
+    constexpr uint32_t kSampleCount = 4;
+
+    auto msaaTexture1 = CreateTexture(wgpu::TextureUsage::RenderAttachment, kSampleCount);
+    auto msaaTexture2 = CreateTexture(wgpu::TextureUsage::RenderAttachment, kSampleCount);
+
+    // Create single sampled textures.
+    auto texture1 =
+        CreateTexture(wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding, 1);
+    auto texture2 =
+        CreateTexture(wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding, 1);
+
+    // Create render pass with two resolve targets. First one uses ExpandResolveTexture.
+    utils::ComboRenderPassDescriptor renderPassDescriptor(
+        {msaaTexture1.CreateView(), msaaTexture2.CreateView()});
+    renderPassDescriptor.cColorAttachments[0].loadOp = wgpu::LoadOp::ExpandResolveTexture;
+    renderPassDescriptor.cColorAttachments[0].resolveTarget = texture1.CreateView();
+    renderPassDescriptor.cColorAttachments[1].loadOp = wgpu::LoadOp::Load;
+    renderPassDescriptor.cColorAttachments[1].resolveTarget = texture2.CreateView();
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    wgpu::RenderPassEncoder renderPass = encoder.BeginRenderPass(&renderPassDescriptor);
+
+    // Create render pipeline (without ColorTargetStateExpandResolveTextureDawn chained to
+    // colorTargets[1])
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.vertex.module = vsModule;
+    pipelineDescriptor.cFragment.module = fs2TargetsModule;
+    pipelineDescriptor.multisample.count = kSampleCount;
+
+    wgpu::ColorTargetStateExpandResolveTextureDawn pipelineMSAAExpandResolveDesc;
+    pipelineMSAAExpandResolveDesc.enabled = true;
+    pipelineDescriptor.cTargets[0].format = kColorFormat;
+    pipelineDescriptor.cTargets[0].nextInChain = &pipelineMSAAExpandResolveDesc;
+    pipelineDescriptor.cTargets[1].format = kColorFormat;
+    pipelineDescriptor.cFragment.targetCount = 2;
+
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
+    renderPass.SetPipeline(pipeline);
+    renderPass.End();
+
+    ASSERT_DEVICE_ERROR(encoder.Finish(), testing::HasSubstr("not compatible"));
+}
+
+// Test that the following render pipeline and render pass are incompatible.
+// - Render pass:
+//   - colorAttachments[0]
+//     - has resolveTarget.
+//     - loadOp = ExpandResolveTexture.
+//   - colorAttachments[1]
+//     - has **no** resolveTarget.
+// - Render pipeline:
+//   - colorTargets[0].nextInChain = ColorTargetStateExpandResolveTextureDawn.
+//     - ColorTargetStateExpandResolveTextureDawn.enabled = true.
+//   - colorTargets[1].nextInChain = ColorTargetStateExpandResolveTextureDawn.
+//     - ColorTargetStateExpandResolveTextureDawn.enabled = false.
+// They are incompatible by spec's requirements.
+TEST_F(LoadResolveTexturePipelineDescriptorValidationTest,
+       RenderPipelineAndRenderPassMismatchResolveTargetsError2) {
+    constexpr uint32_t kSampleCount = 4;
+
+    auto msaaTexture1 = CreateTexture(wgpu::TextureUsage::RenderAttachment, kSampleCount);
+    auto msaaTexture2 = CreateTexture(wgpu::TextureUsage::RenderAttachment, kSampleCount);
+
+    // Create single sampled textures.
+    auto texture1 =
+        CreateTexture(wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding, 1);
+
+    // Create render pass with two outputs but one resolve target. First one uses
+    // ExpandResolveTexture.
+    utils::ComboRenderPassDescriptor renderPassDescriptor(
+        {msaaTexture1.CreateView(), msaaTexture2.CreateView()});
+    renderPassDescriptor.cColorAttachments[0].loadOp = wgpu::LoadOp::ExpandResolveTexture;
+    renderPassDescriptor.cColorAttachments[0].resolveTarget = texture1.CreateView();
+    renderPassDescriptor.cColorAttachments[1].loadOp = wgpu::LoadOp::Load;
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    wgpu::RenderPassEncoder renderPass = encoder.BeginRenderPass(&renderPassDescriptor);
+
+    // Create render pipeline with two chained ColorTargetStateExpandResolveTextureDawn.
+    // The first one's enabled flag = true.
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.vertex.module = vsModule;
+    pipelineDescriptor.cFragment.module = fs2TargetsModule;
+    pipelineDescriptor.multisample.count = kSampleCount;
+
+    wgpu::ColorTargetStateExpandResolveTextureDawn pipelineMSAAExpandResolveDesc[2];
+    pipelineMSAAExpandResolveDesc[0].enabled = true;
+    pipelineDescriptor.cTargets[0].format = kColorFormat;
+    pipelineDescriptor.cTargets[0].nextInChain = &pipelineMSAAExpandResolveDesc[0];
+    pipelineMSAAExpandResolveDesc[1].enabled = false;
+    pipelineDescriptor.cTargets[1].format = kColorFormat;
+    pipelineDescriptor.cTargets[1].nextInChain = &pipelineMSAAExpandResolveDesc[1];
+    pipelineDescriptor.cFragment.targetCount = 2;
+
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
+    renderPass.SetPipeline(pipeline);
+    renderPass.End();
+
+    ASSERT_DEVICE_ERROR(encoder.Finish(), testing::HasSubstr("not compatible"));
+}
+
 // Bind resolve attachment in a ExpandResolveTexture render pass as texture should result
 // in error.
 TEST_F(LoadResolveTexturePipelineDescriptorValidationTest, BindColorAttachmentAsTextureError) {