[tint][resolver]: Require @blend_src on all or none of the IO

Fixed: dawn:2380
Change-Id: I106719ad6e2a6f8dd75d9a8b00cc29f54595f3b6
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/172420
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
diff --git a/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc b/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
index 67a3aab..ab8677e 100644
--- a/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
+++ b/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
@@ -215,6 +215,49 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
+TEST_F(DualSourceBlendingExtensionTests, MixedBlendSrcAndNonBlendSrcOnLocationZero_Struct) {
+    // struct S {
+    //   @location(0) a : vec4<f32>,
+    //   @location(0) @blend_src(1) b : vec4<f32>,
+    // };
+    Structure("S", Vector{
+                       Member("a", ty.vec4<f32>(),
+                              Vector{
+                                  Location(Source{{12, 34}}, 0_a),
+                              }),
+                       Member("b", ty.vec4<f32>(),
+                              Vector{Location(0_a), BlendSrc(Source{{56, 78}}, 1_a)}),
+                   });
+
+    EXPECT_TRUE(r()->Resolve());
+}
+
+TEST_F(DualSourceBlendingExtensionTests,
+       MixedBlendSrcAndNonBlendSrcOnLocationZero_StructUsedInEntryPoint) {
+    // struct S {
+    //   @location(0) a : vec4<f32>,
+    //   @location(0) @blend_src(1) b : vec4<f32>,
+    // };
+    // fn F() -> S { return S(); }
+    Structure("S", Vector{
+                       Member("a", ty.vec4<f32>(),
+                              Vector{
+                                  Location(Source{{12, 34}}, 0_a),
+                              }),
+                       Member("b", ty.vec4<f32>(),
+                              Vector{Location(0_a), BlendSrc(Source{{56, 78}}, 1_a)}),
+                   });
+    Func("F", Empty, ty("S"), Vector{Return(Call("S"))},
+         Vector{Stage(ast::PipelineStage::kFragment)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: use of @blend_src requires all the output @location attributes of the entry point to be paired with a @blend_src attribute
+56:78 note: use of @blend_src here
+note: while analyzing entry point 'F')");
+}
+
 class DualSourceBlendingExtensionTestWithParams : public ResolverTestWithParam<int> {
   public:
     DualSourceBlendingExtensionTestWithParams() {
@@ -224,7 +267,7 @@
 
 // Rendering to multiple render targets while using dual source blending should fail.
 TEST_P(DualSourceBlendingExtensionTestWithParams,
-       MultipleRenderTargetsNotAllowed_BlendSrcThenNonZeroLocation) {
+       MultipleRenderTargetsNotAllowed_BlendSrcAndNoBlendSrc) {
     // struct S {
     //   @location(0) @blend_src(0) a : vec4<f32>,
     //   @location(0) @blend_src(1) b : vec4<f32>,
@@ -241,9 +284,10 @@
          Vector{Stage(ast::PipelineStage::kFragment)});
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(1:2 error: pipeline cannot use both non-zero @blend_src and non-zero @location
-3:4 note: non-zero @location declared here
+    EXPECT_EQ(
+        r()->error(),
+        R"(3:4 error: use of @blend_src requires all the output @location attributes of the entry point to be paired with a @blend_src attribute
+1:2 note: use of @blend_src here
 note: while analyzing entry point 'F')");
 }
 
@@ -265,9 +309,10 @@
          Vector{Stage(ast::PipelineStage::kFragment)});
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(3:4 error: pipeline cannot use both non-zero @blend_src and non-zero @location
-1:2 note: non-zero @location declared here
+    EXPECT_EQ(
+        r()->error(),
+        R"(1:2 error: use of @blend_src requires all the output @location attributes of the entry point to be paired with a @blend_src attribute
+note: use of @blend_src here
 5:6 note: while analyzing entry point 'F')");
 }
 
diff --git a/src/tint/lang/wgsl/resolver/validator.cc b/src/tint/lang/wgsl/resolver/validator.cc
index 907a3c4..0201bb4 100644
--- a/src/tint/lang/wgsl/resolver/validator.cc
+++ b/src/tint/lang/wgsl/resolver/validator.cc
@@ -1149,7 +1149,8 @@
     Hashset<core::BuiltinValue, 4> builtins;
     Hashset<std::pair<uint32_t, uint32_t>, 8> locations_and_blend_srcs;
     const ast::LocationAttribute* first_nonzero_location = nullptr;
-    const ast::BlendSrcAttribute* first_nonzero_blend_src = nullptr;
+    const ast::BlendSrcAttribute* first_blend_src = nullptr;
+    const ast::LocationAttribute* first_location_without_blend_src = nullptr;
     Hashset<uint32_t, 4> colors;
     enum class ParamOrRetType {
         kParameter,
@@ -1311,16 +1312,28 @@
                 }
             }
 
+            if (blend_src_attribute) {
+                first_blend_src = blend_src_attribute;
+            } else if (location_attribute) {
+                first_location_without_blend_src = location_attribute;
+            }
+
+            if (first_blend_src && first_location_without_blend_src) {
+                AddError(
+                    "use of @blend_src requires all the output @location attributes of the entry "
+                    "point to be paired with a @blend_src attribute",
+                    first_location_without_blend_src->source);
+                AddNote("use of @blend_src here", first_blend_src->source);
+                return false;
+            }
+
             if (location_attribute) {
                 if (!first_nonzero_location && location > 0u) {
                     first_nonzero_location = location_attribute;
                 }
-                if (!first_nonzero_blend_src && blend_src > 0u) {
-                    first_nonzero_blend_src = blend_src_attribute;
-                }
-                if (first_nonzero_location && first_nonzero_blend_src) {
-                    AddError("pipeline cannot use both non-zero @blend_src and non-zero @location",
-                             first_nonzero_blend_src->source);
+                if (first_nonzero_location && first_blend_src) {
+                    AddError("pipeline cannot use both a @blend_src and non-zero @location",
+                             first_blend_src->source);
                     AddNote("non-zero @location declared here", first_nonzero_location->source);
                     return false;
                 }
@@ -1416,7 +1429,8 @@
     builtins.Clear();
     locations_and_blend_srcs.Clear();
     first_nonzero_location = nullptr;
-    first_nonzero_blend_src = nullptr;
+    first_blend_src = nullptr;
+    first_location_without_blend_src = nullptr;
 
     if (!func->ReturnType()->Is<core::type::Void>()) {
         if (!validate_entry_point_attributes(decl->return_type_attributes, func->ReturnType(),
@@ -2226,7 +2240,7 @@
         return false;
     }
 
-    Hashset<std::pair<uint32_t, uint32_t>, 8> locations_and_blend_srcs;
+    Hashset<std::pair<uint32_t, std::optional<uint32_t>>, 8> locations_and_blend_srcs;
     Hashset<uint32_t, 4> colors;
     for (auto* member : str->Members()) {
         if (auto* r = member->Type()->As<sem::Array>()) {
@@ -2330,17 +2344,16 @@
             return false;
         }
 
-        // Ensure all locations and index pairs are unique
+        // Ensure all locations and optional blend_src pairs are unique
         if (location_attribute) {
             uint32_t location = member->Attributes().location.value();
-            uint32_t blend_src = member->Attributes().blend_src.value_or(0);
+            std::optional<uint32_t> blend_src = member->Attributes().blend_src;
 
-            std::pair<uint32_t, uint32_t> location_and_blend_src(location, blend_src);
-            if (!locations_and_blend_srcs.Add(location_and_blend_src)) {
+            if (!locations_and_blend_srcs.Add(std::make_pair(location, blend_src))) {
                 StringStream err;
                 err << "@location(" << location << ") ";
-                if (blend_src_attribute) {
-                    err << "@blend_src(" << blend_src << ") ";
+                if (blend_src) {
+                    err << "@blend_src(" << blend_src.value() << ") ";
                 }
                 err << "appears multiple times";
                 AddError(err.str(), location_attribute->source);