[tint] Resolve ColorAttribute

Bug: tint:2085
Change-Id: I85a008fca5aa33c9a672551bac398173e48e1b70
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/159923
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: James Price <jrprice@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/lang/core/ir/transform/shader_io.cc b/src/tint/lang/core/ir/transform/shader_io.cc
index 368dcf0..d1790d4 100644
--- a/src/tint/lang/core/ir/transform/shader_io.cc
+++ b/src/tint/lang/core/ir/transform/shader_io.cc
@@ -122,6 +122,7 @@
                                    core::type::StructMemberAttributes{
                                        /* location */ {},
                                        /* index */ {},
+                                       /* color */ {},
                                        /* builtin */ core::BuiltinValue::kPointSize,
                                        /* interpolation */ {},
                                        /* invariant */ false,
diff --git a/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc b/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc
index e16f35e..0f63af0 100644
--- a/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc
+++ b/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc
@@ -1494,6 +1494,7 @@
                                         core::type::StructMemberAttributes{
                                             /* location */ {},
                                             /* index */ {},
+                                            /* color */ {},
                                             /* builtin */ core::BuiltinValue::kGlobalInvocationId,
                                             /* interpolation */ {},
                                             /* invariant */ false,
@@ -1505,6 +1506,7 @@
                                         core::type::StructMemberAttributes{
                                             /* location */ {},
                                             /* index */ {},
+                                            /* color */ {},
                                             /* builtin */ core::BuiltinValue::kLocalInvocationIndex,
                                             /* interpolation */ {},
                                             /* invariant */ false,
diff --git a/src/tint/lang/core/type/struct.h b/src/tint/lang/core/type/struct.h
index f4ff396..228c8b8 100644
--- a/src/tint/lang/core/type/struct.h
+++ b/src/tint/lang/core/type/struct.h
@@ -206,6 +206,8 @@
     std::optional<uint32_t> location;
     /// The value of a `@index` attribute
     std::optional<uint32_t> index;
+    /// The value of a `@color` attribute
+    std::optional<uint32_t> color;
     /// The value of a `@builtin` attribute
     std::optional<core::BuiltinValue> builtin;
     /// The values of a `@interpolate` attribute
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
index 6ef2020..230cb58 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
@@ -1969,7 +1969,7 @@
             TINT_ICE() << "missing binding attributes for entry point parameter";
             return kInvalidBindingIndex;
         }
-        auto* param_sem = builder_.Sem().Get<sem::Parameter>(param);
+        auto* param_sem = builder_.Sem().Get(param);
         auto bp = param_sem->Attributes().binding_point;
         if (TINT_UNLIKELY(bp->group != 0)) {
             TINT_ICE() << "encountered non-zero resource group index (use BindingRemapper to fix)";
@@ -2838,6 +2838,10 @@
             }
         }
 
+        if (auto color = attributes.color) {
+            out << " [[color(" + std::to_string(color.value()) + ")]]";
+        }
+
         if (auto interpolation = attributes.interpolation) {
             auto name = InterpolationToAttribute(interpolation->type, interpolation->sampling);
             if (name.empty()) {
diff --git a/src/tint/lang/spirv/writer/function_test.cc b/src/tint/lang/spirv/writer/function_test.cc
index fc66154..85904d4 100644
--- a/src/tint/lang/spirv/writer/function_test.cc
+++ b/src/tint/lang/spirv/writer/function_test.cc
@@ -355,6 +355,7 @@
                                                       core::type::StructMemberAttributes{
                                                           /* location */ 0u,
                                                           /* index */ 0u,
+                                                          /* color */ std::nullopt,
                                                           /* builtin */ std::nullopt,
                                                           /* interpolation */ std::nullopt,
                                                           /* invariant */ false,
@@ -366,6 +367,7 @@
                                                       core::type::StructMemberAttributes{
                                                           /* location */ 0u,
                                                           /* index */ 1u,
+                                                          /* color */ std::nullopt,
                                                           /* builtin */ std::nullopt,
                                                           /* interpolation */ std::nullopt,
                                                           /* invariant */ false,
diff --git a/src/tint/lang/spirv/writer/raise/shader_io_test.cc b/src/tint/lang/spirv/writer/raise/shader_io_test.cc
index edbde41..e630557 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io_test.cc
+++ b/src/tint/lang/spirv/writer/raise/shader_io_test.cc
@@ -154,6 +154,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ std::nullopt,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ core::BuiltinValue::kFrontFacing,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ false,
@@ -165,6 +166,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ std::nullopt,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ core::BuiltinValue::kPosition,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ true,
@@ -176,6 +178,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ 0u,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ std::nullopt,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ false,
@@ -187,6 +190,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ 1u,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ std::nullopt,
                                          /* interpolation */
                                          core::Interpolation{
@@ -302,6 +306,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ std::nullopt,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ core::BuiltinValue::kPosition,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ true,
@@ -313,6 +318,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ 0u,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ std::nullopt,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ false,
@@ -514,6 +520,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ std::nullopt,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ core::BuiltinValue::kPosition,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ true,
@@ -525,6 +532,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ 0u,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ std::nullopt,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ false,
@@ -536,6 +544,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ 1u,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ std::nullopt,
                                          /* interpolation */
                                          core::Interpolation{
@@ -621,6 +630,7 @@
                                                      core::type::StructMemberAttributes{
                                                          /* location */ 0u,
                                                          /* index */ 0u,
+                                                         /* color */ std::nullopt,
                                                          /* builtin */ std::nullopt,
                                                          /* interpolation */ std::nullopt,
                                                          /* invariant */ false,
@@ -632,6 +642,7 @@
                                                      core::type::StructMemberAttributes{
                                                          /* location */ 0u,
                                                          /* index */ 1u,
+                                                         /* color */ std::nullopt,
                                                          /* builtin */ std::nullopt,
                                                          /* interpolation */ std::nullopt,
                                                          /* invariant */ false,
@@ -707,6 +718,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ std::nullopt,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ core::BuiltinValue::kPosition,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ false,
@@ -718,6 +730,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ 0u,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ std::nullopt,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ false,
@@ -846,6 +859,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ std::nullopt,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ core::BuiltinValue::kPosition,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ false,
@@ -857,6 +871,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ 0u,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ std::nullopt,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ false,
@@ -939,6 +954,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ 0u,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ std::nullopt,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ false,
@@ -950,6 +966,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ std::nullopt,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ core::BuiltinValue::kSampleMask,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ false,
@@ -1033,6 +1050,7 @@
                                                        core::type::StructMemberAttributes{
                                                            /* location */ 1u,
                                                            /* index */ std::nullopt,
+                                                           /* color */ std::nullopt,
                                                            /* builtin */ std::nullopt,
                                                            /* interpolation */
                                                            core::Interpolation{
@@ -1180,6 +1198,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ 0u,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ std::nullopt,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ false,
@@ -1191,6 +1210,7 @@
                                      core::type::StructMemberAttributes{
                                          /* location */ std::nullopt,
                                          /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
                                          /* builtin */ core::BuiltinValue::kFragDepth,
                                          /* interpolation */ std::nullopt,
                                          /* invariant */ false,
diff --git a/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc b/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
index 0ebf6fa..b3e4aa6 100644
--- a/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
+++ b/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
@@ -793,7 +793,7 @@
             LocationInfo info;
             info.expr = [this, func_var] { return b.Expr(func_var); };
 
-            auto* sem = src.Sem().Get<sem::Parameter>(param);
+            auto* sem = src.Sem().Get(param);
             info.type = sem->Type();
 
             if (TINT_UNLIKELY(!sem->Attributes().location.has_value())) {
diff --git a/src/tint/lang/wgsl/resolver/BUILD.bazel b/src/tint/lang/wgsl/resolver/BUILD.bazel
index 1d19962..4187995 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.bazel
+++ b/src/tint/lang/wgsl/resolver/BUILD.bazel
@@ -118,6 +118,7 @@
     "evaluation_stage_test.cc",
     "expression_kind_test.cc",
     "f16_extension_test.cc",
+    "framebuffer_fetch_extension_test.cc",
     "function_validation_test.cc",
     "host_shareable_validation_test.cc",
     "increment_decrement_validation_test.cc",
diff --git a/src/tint/lang/wgsl/resolver/BUILD.cmake b/src/tint/lang/wgsl/resolver/BUILD.cmake
index 4ffe57c..745d508 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.cmake
+++ b/src/tint/lang/wgsl/resolver/BUILD.cmake
@@ -116,6 +116,7 @@
   lang/wgsl/resolver/evaluation_stage_test.cc
   lang/wgsl/resolver/expression_kind_test.cc
   lang/wgsl/resolver/f16_extension_test.cc
+  lang/wgsl/resolver/framebuffer_fetch_extension_test.cc
   lang/wgsl/resolver/function_validation_test.cc
   lang/wgsl/resolver/host_shareable_validation_test.cc
   lang/wgsl/resolver/increment_decrement_validation_test.cc
diff --git a/src/tint/lang/wgsl/resolver/BUILD.gn b/src/tint/lang/wgsl/resolver/BUILD.gn
index 28b1f3f..f5e4097 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.gn
+++ b/src/tint/lang/wgsl/resolver/BUILD.gn
@@ -118,6 +118,7 @@
       "evaluation_stage_test.cc",
       "expression_kind_test.cc",
       "f16_extension_test.cc",
+      "framebuffer_fetch_extension_test.cc",
       "function_validation_test.cc",
       "host_shareable_validation_test.cc",
       "increment_decrement_validation_test.cc",
diff --git a/src/tint/lang/wgsl/resolver/attribute_validation_test.cc b/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
index d88daa5..7d59187 100644
--- a/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
@@ -60,6 +60,7 @@
     kAlign,
     kBinding,
     kBuiltinPosition,
+    kColor,
     kDiagnostic,
     kGroup,
     kId,
@@ -82,6 +83,8 @@
             return o << "@binding";
         case AttributeKind::kBuiltinPosition:
             return o << "@builtin(position)";
+        case AttributeKind::kColor:
+            return o << "@color";
         case AttributeKind::kDiagnostic:
             return o << "@diagnostic";
         case AttributeKind::kGroup:
@@ -144,6 +147,10 @@
                 "1:2 error: @builtin is not valid for " + thing,
             },
             TestParams{
+                {AttributeKind::kColor},
+                "1:2 error: @color is not valid for " + thing,
+            },
+            TestParams{
                 {AttributeKind::kDiagnostic},
                 Pass,
             },
@@ -215,6 +222,8 @@
             return builder.Binding(source, 1_a);
         case AttributeKind::kBuiltinPosition:
             return builder.Builtin(source, core::BuiltinValue::kPosition);
+        case AttributeKind::kColor:
+            return builder.Color(source, 2_a);
         case AttributeKind::kDiagnostic:
             return builder.DiagnosticAttribute(source, wgsl::DiagnosticSeverity::kInfo, "chromium",
                                                "unreachable_code");
@@ -250,8 +259,15 @@
 
 struct TestWithParams : ResolverTestWithParam<TestParams> {
     void EnableExtensionIfNecessary(AttributeKind attribute) {
-        if (attribute == AttributeKind::kIndex) {
-            Enable(wgsl::Extension::kChromiumInternalDualSourceBlending);
+        switch (attribute) {
+            case AttributeKind::kColor:
+                Enable(wgsl::Extension::kChromiumExperimentalFramebufferFetch);
+                break;
+            case AttributeKind::kIndex:
+                Enable(wgsl::Extension::kChromiumInternalDualSourceBlending);
+                break;
+            default:
+                break;
         }
     }
 
@@ -310,6 +326,10 @@
             R"(1:2 error: @builtin is not valid for functions)",
         },
         TestParams{
+            {AttributeKind::kColor},
+            R"(1:2 error: @color is not valid for functions)",
+        },
+        TestParams{
             {AttributeKind::kDiagnostic},
             Pass,
         },
@@ -390,6 +410,10 @@
                                  R"(1:2 error: @builtin is not valid for functions)",
                              },
                              TestParams{
+                                 {AttributeKind::kColor},
+                                 R"(1:2 error: @color is not valid for functions)",
+                             },
+                             TestParams{
                                  {AttributeKind::kDiagnostic},
                                  Pass,
                              },
@@ -477,6 +501,10 @@
             R"(1:2 error: @builtin is not valid for non-entry point function parameters)",
         },
         TestParams{
+            {AttributeKind::kColor},
+            R"(1:2 error: @color is not valid for function parameters)",
+        },
+        TestParams{
             {AttributeKind::kDiagnostic},
             R"(1:2 error: @diagnostic is not valid for function parameters)",
         },
@@ -558,6 +586,10 @@
             R"(1:2 error: @builtin is not valid for non-entry point function return types)",
         },
         TestParams{
+            {AttributeKind::kColor},
+            R"(1:2 error: @color is not valid for non-entry point function return types)",
+        },
+        TestParams{
             {AttributeKind::kDiagnostic},
             R"(1:2 error: @diagnostic is not valid for non-entry point function return types)",
         },
@@ -644,6 +676,10 @@
             R"(1:2 error: @builtin(position) cannot be used for compute shader input)",
         },
         TestParams{
+            {AttributeKind::kColor},
+            R"(1:2 error: @color can only be used for fragment shader input)",
+        },
+        TestParams{
             {AttributeKind::kDiagnostic},
             R"(1:2 error: @diagnostic is not valid for function parameters)",
         },
@@ -724,6 +760,10 @@
             Pass,
         },
         TestParams{
+            {AttributeKind::kColor},
+            Pass,
+        },
+        TestParams{
             {AttributeKind::kDiagnostic},
             R"(1:2 error: @diagnostic is not valid for function parameters)",
         },
@@ -823,6 +863,10 @@
             R"(1:2 error: @builtin(position) cannot be used for vertex shader input)",
         },
         TestParams{
+            {AttributeKind::kColor},
+            R"(1:2 error: @color can only be used for fragment shader input)",
+        },
+        TestParams{
             {AttributeKind::kDiagnostic},
             R"(1:2 error: @diagnostic is not valid for function parameters)",
         },
@@ -924,6 +968,10 @@
             R"(1:2 error: @builtin(position) cannot be used for compute shader output)",
         },
         TestParams{
+            {AttributeKind::kColor},
+            R"(1:2 error: @color is not valid for entry point return types)",
+        },
+        TestParams{
             {AttributeKind::kDiagnostic},
             R"(1:2 error: @diagnostic is not valid for entry point return types)",
         },
@@ -1006,6 +1054,10 @@
             R"(1:2 error: @builtin(position) cannot be used for fragment shader output)",
         },
         TestParams{
+            {AttributeKind::kColor},
+            R"(1:2 error: @color is not valid for entry point return types)",
+        },
+        TestParams{
             {AttributeKind::kDiagnostic},
             R"(1:2 error: @diagnostic is not valid for entry point return types)",
         },
@@ -1114,6 +1166,10 @@
             Pass,
         },
         TestParams{
+            {AttributeKind::kColor},
+            R"(1:2 error: @color is not valid for entry point return types)",
+        },
+        TestParams{
             {AttributeKind::kDiagnostic},
             R"(1:2 error: @diagnostic is not valid for entry point return types)",
         },
@@ -1239,6 +1295,10 @@
             R"(1:2 error: @diagnostic is not valid for struct declarations)",
         },
         TestParams{
+            {AttributeKind::kColor},
+            R"(1:2 error: @color is not valid for struct declarations)",
+        },
+        TestParams{
             {AttributeKind::kGroup},
             R"(1:2 error: @group is not valid for struct declarations)",
         },
@@ -1314,6 +1374,10 @@
                                  Pass,
                              },
                              TestParams{
+                                 {AttributeKind::kColor},
+                                 Pass,
+                             },
+                             TestParams{
                                  {AttributeKind::kDiagnostic},
                                  R"(1:2 error: @diagnostic is not valid for struct members)",
                              },
diff --git a/src/tint/lang/wgsl/resolver/dependency_graph.cc b/src/tint/lang/wgsl/resolver/dependency_graph.cc
index dda9251..bb07be0 100644
--- a/src/tint/lang/wgsl/resolver/dependency_graph.cc
+++ b/src/tint/lang/wgsl/resolver/dependency_graph.cc
@@ -40,6 +40,7 @@
 #include "src/tint/lang/wgsl/ast/break_if_statement.h"
 #include "src/tint/lang/wgsl/ast/break_statement.h"
 #include "src/tint/lang/wgsl/ast/call_statement.h"
+#include "src/tint/lang/wgsl/ast/color_attribute.h"
 #include "src/tint/lang/wgsl/ast/compound_assignment_statement.h"
 #include "src/tint/lang/wgsl/ast/const.h"
 #include "src/tint/lang/wgsl/ast/continue_statement.h"
@@ -386,6 +387,7 @@
             attr,  //
             [&](const ast::BindingAttribute* binding) { TraverseExpression(binding->expr); },
             [&](const ast::BuiltinAttribute* builtin) { TraverseExpression(builtin->builtin); },
+            [&](const ast::ColorAttribute* color) { TraverseExpression(color->expr); },
             [&](const ast::GroupAttribute* group) { TraverseExpression(group->expr); },
             [&](const ast::IdAttribute* id) { TraverseExpression(id->expr); },
             [&](const ast::IndexAttribute* index) { TraverseExpression(index->expr); },
diff --git a/src/tint/lang/wgsl/resolver/dependency_graph_test.cc b/src/tint/lang/wgsl/resolver/dependency_graph_test.cc
index 05a83d4..360ffda 100644
--- a/src/tint/lang/wgsl/resolver/dependency_graph_test.cc
+++ b/src/tint/lang/wgsl/resolver/dependency_graph_test.cc
@@ -1692,6 +1692,7 @@
              Param(Sym(), T,
                    Vector{
                        Location(V),  // Parameter attributes
+                       Color(V),
                        Builtin(V),
                        Interpolate(V),
                        Interpolate(V, V),
diff --git a/src/tint/lang/wgsl/resolver/framebuffer_fetch_extension_test.cc b/src/tint/lang/wgsl/resolver/framebuffer_fetch_extension_test.cc
new file mode 100644
index 0000000..a9c6576
--- /dev/null
+++ b/src/tint/lang/wgsl/resolver/framebuffer_fetch_extension_test.cc
@@ -0,0 +1,256 @@
+// Copyright 2023 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 "src/tint/lang/wgsl/resolver/resolver.h"
+#include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
+
+#include "gmock/gmock.h"
+
+namespace tint::resolver {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+using FramebufferFetchExtensionTest = ResolverTest;
+
+TEST_F(FramebufferFetchExtensionTest, ColorParamUsedWithExtension) {
+    // enable chromium_experimental_framebuffer_fetch;
+    // fn f(@color(2) p : vec4<f32>) {}
+
+    Enable(Source{{12, 34}}, wgsl::Extension::kChromiumExperimentalFramebufferFetch);
+
+    auto* ast_param = Param("p", ty.vec4<f32>(), Vector{Color(2_a)});
+    Func("f", Vector{ast_param}, ty.void_(), Empty, Vector{Stage(ast::PipelineStage::kFragment)});
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem_param = Sem().Get(ast_param);
+    ASSERT_NE(sem_param, nullptr);
+    EXPECT_EQ(sem_param->Attributes().color, 2u);
+}
+
+TEST_F(FramebufferFetchExtensionTest, ColorParamUsedWithoutExtension) {
+    // enable chromium_experimental_framebuffer_fetch;
+    // struct S {
+    //   @color(2) c : vec4<f32>,
+    // }
+
+    Func("f", Vector{Param("p", ty.vec4<f32>(), Vector{Color(Source{{12, 34}}, 2_a)})}, ty.void_(),
+         Empty, Vector{Stage(ast::PipelineStage::kFragment)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: use of @color requires enabling extension 'chromium_experimental_framebuffer_fetch')");
+}
+
+TEST_F(FramebufferFetchExtensionTest, ColorMemberUsedWithExtension) {
+    // enable chromium_experimental_framebuffer_fetch;
+    // fn f(@color(2) p : vec4<f32>) {}
+
+    Enable(Source{{12, 34}}, wgsl::Extension::kChromiumExperimentalFramebufferFetch);
+
+    auto* ast_member = Member("c", ty.vec4<f32>(), Vector{Color(2_a)});
+    Structure("S", Vector{ast_member});
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem_member = Sem().Get(ast_member);
+    ASSERT_NE(sem_member, nullptr);
+    EXPECT_EQ(sem_member->Attributes().color, 2u);
+}
+
+TEST_F(FramebufferFetchExtensionTest, ColorMemberUsedWithoutExtension) {
+    // enable chromium_experimental_framebuffer_fetch;
+    // fn f(@color(2) p : vec4<f32>) {}
+
+    Structure("S", Vector{Member("c", ty.vec4<f32>(), Vector{Color(Source{{12, 34}}, 2_a)})});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: use of @color requires enabling extension 'chromium_experimental_framebuffer_fetch')");
+}
+
+TEST_F(FramebufferFetchExtensionTest, DuplicateColorParams) {
+    // enable chromium_experimental_framebuffer_fetch;
+    // fn f(@color(1) a : vec4<f32>, @color(2) b : vec4<f32>, @color(1) c : vec4<f32>) {}
+
+    Enable(Source{{12, 34}}, wgsl::Extension::kChromiumExperimentalFramebufferFetch);
+
+    Func("f",
+         Vector{
+             Param("a", ty.vec4<f32>(), Vector{Color(1_a)}),
+             Param("b", ty.vec4<f32>(), Vector{Color(2_a)}),
+             Param("c", ty.vec4<f32>(), Vector{Color(Source{{1, 2}}, 1_a)}),
+         },
+         ty.void_(), Empty, Vector{Stage(ast::PipelineStage::kFragment)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), R"(1:2 error: @color(1) appears multiple times)");
+}
+
+TEST_F(FramebufferFetchExtensionTest, DuplicateColorStruct) {
+    // enable chromium_experimental_framebuffer_fetch;
+    // struct S {
+    //   @color(1) a : vec4<f32>,
+    //   @color(2) b : vec4<f32>,
+    //   @color(1) c : vec4<f32>,
+    // }
+    // fn f(s : S) {}
+
+    Enable(Source{{12, 34}}, wgsl::Extension::kChromiumExperimentalFramebufferFetch);
+
+    Structure("S", Vector{
+                       Member("a", ty.vec4<f32>(), Vector{Color(1_a)}),
+                       Member("b", ty.vec4<f32>(), Vector{Color(2_a)}),
+                       Member("c", ty.vec4<f32>(), Vector{Color(Source{{1, 2}}, 1_a)}),
+                   });
+
+    Func("f", Vector{Param("s", ty("S"))}, ty.void_(), Empty,
+         Vector{Stage(ast::PipelineStage::kFragment)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), R"(1:2 error: @color(1) appears multiple times)");
+}
+
+TEST_F(FramebufferFetchExtensionTest, DuplicateColorParamAndStruct) {
+    // enable chromium_experimental_framebuffer_fetch;
+    // struct S {
+    //   @color(1) b : vec4<f32>,
+    //   @color(2) c : vec4<f32>,
+    // }
+    // fn f(@color(2) a : vec4<f32>, s : S, @color(3) d : vec4<f32>) {}
+
+    Enable(Source{{12, 34}}, wgsl::Extension::kChromiumExperimentalFramebufferFetch);
+
+    Structure("S", Vector{
+                       Member("b", ty.vec4<f32>(), Vector{Color(1_a)}),
+                       Member("c", ty.vec4<f32>(), Vector{Color(Source{{1, 2}}, 2_a)}),
+                   });
+
+    Func("f",
+         Vector{
+             Param("a", ty.vec4<f32>(), Vector{Color(2_a)}),
+             Param("s", ty("S")),
+             Param("d", ty.vec4<f32>(), Vector{Color(3_a)}),
+         },
+         ty.void_(), Empty, Vector{Stage(ast::PipelineStage::kFragment)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), R"(1:2 error: @color(2) appears multiple times
+note: while analyzing entry point 'f')");
+}
+
+namespace type_tests {
+struct Case {
+    builder::ast_type_func_ptr type;
+    std::string name;
+    bool pass;
+};
+
+static std::ostream& operator<<(std::ostream& o, const Case& c) {
+    return o << c.name;
+}
+
+template <typename T>
+Case Pass() {
+    return Case{builder::DataType<T>::AST, builder::DataType<T>::Name(), true};
+}
+
+template <typename T>
+Case Fail() {
+    return Case{builder::DataType<T>::AST, builder::DataType<T>::Name(), false};
+}
+
+using FramebufferFetchExtensionTest_Types = ResolverTestWithParam<Case>;
+
+TEST_P(FramebufferFetchExtensionTest_Types, Param) {
+    // enable chromium_experimental_framebuffer_fetch;
+    // fn f(@color(1) a : <type>) {}
+
+    Enable(wgsl::Extension::kChromiumExperimentalFramebufferFetch);
+
+    Func("f",
+         Vector{Param(Source{{12, 34}}, "p", GetParam().type(*this),
+                      Vector{Color(Source{{56, 78}}, 2_a)})},
+         ty.void_(), Empty, Vector{Stage(ast::PipelineStage::kFragment)});
+
+    if (GetParam().pass) {
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+    } else {
+        EXPECT_FALSE(r()->Resolve());
+        auto expected =
+            ReplaceAll(R"(12:34 error: cannot apply @color to declaration of type '$TYPE'
+56:78 note: @color must only be applied to declarations of numeric scalar or numeric vector type)",
+                       "$TYPE", GetParam().name);
+        EXPECT_EQ(r()->error(), expected);
+    }
+}
+
+TEST_P(FramebufferFetchExtensionTest_Types, Struct) {
+    // struct S {
+    //   @color(2) c : <type>,
+    // }
+
+    Enable(wgsl::Extension::kChromiumExperimentalFramebufferFetch);
+
+    Structure("S", Vector{
+                       Member(Source{{12, 34}}, "c", GetParam().type(*this),
+                              Vector{Color(Source{{56, 78}}, 2_a)}),
+                   });
+
+    if (GetParam().pass) {
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+    } else {
+        EXPECT_FALSE(r()->Resolve());
+        auto expected =
+            ReplaceAll(R"(12:34 error: cannot apply @color to declaration of type '$TYPE'
+56:78 note: @color must only be applied to declarations of numeric scalar or numeric vector type)",
+                       "$TYPE", GetParam().name);
+        EXPECT_EQ(r()->error(), expected);
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(Valid,
+                         FramebufferFetchExtensionTest_Types,
+                         testing::Values(Pass<i32>(),
+                                         Pass<u32>(),
+                                         Pass<f32>(),
+                                         Pass<vec2<f32>>(),
+                                         Pass<vec3<i32>>(),
+                                         Pass<vec4<u32>>()));
+
+INSTANTIATE_TEST_SUITE_P(Invalid,
+                         FramebufferFetchExtensionTest_Types,
+                         testing::Values(Fail<bool>(), Fail<array<u32, 4>>()));
+
+}  // namespace type_tests
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/lang/wgsl/resolver/pixel_local_extension_test.cc b/src/tint/lang/wgsl/resolver/pixel_local_extension_test.cc
index c70c6d5..eceb5ae 100644
--- a/src/tint/lang/wgsl/resolver/pixel_local_extension_test.cc
+++ b/src/tint/lang/wgsl/resolver/pixel_local_extension_test.cc
@@ -38,6 +38,20 @@
 
 using ResolverPixelLocalExtensionTest = ResolverTest;
 
+TEST_F(ResolverPixelLocalExtensionTest, UseWithFramebufferFetch) {
+    // enable chromium_experimental_pixel_local;
+    // enable chromium_experimental_framebuffer_fetch;
+
+    Enable(Source{{12, 34}}, wgsl::Extension::kChromiumExperimentalPixelLocal);
+    Enable(Source{{56, 78}}, wgsl::Extension::kChromiumExperimentalFramebufferFetch);
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: extension 'chromium_experimental_pixel_local' cannot be used with extension 'chromium_experimental_framebuffer_fetch'
+56:78 note: 'chromium_experimental_framebuffer_fetch' enabled here)");
+}
+
 TEST_F(ResolverPixelLocalExtensionTest, AddressSpaceUsedWithExtension) {
     // enable chromium_experimental_pixel_local;
     // struct S { a : i32 }
@@ -308,6 +322,7 @@
 using ResolverPixelLocalExtensionTest_Types = ResolverTestWithParam<Case>;
 
 TEST_P(ResolverPixelLocalExtensionTest_Types, Direct) {
+    // enable chromium_experimental_pixel_local;
     // var<pixel_local> v : <type>;
 
     Enable(wgsl::Extension::kChromiumExperimentalPixelLocal);
@@ -319,6 +334,7 @@
 }
 
 TEST_P(ResolverPixelLocalExtensionTest_Types, Struct) {
+    // enable chromium_experimental_pixel_local;
     // struct S {
     //   a : i32,
     //   m : <type>,
diff --git a/src/tint/lang/wgsl/resolver/resolver.cc b/src/tint/lang/wgsl/resolver/resolver.cc
index d5a0e93..aa54ccf 100644
--- a/src/tint/lang/wgsl/resolver/resolver.cc
+++ b/src/tint/lang/wgsl/resolver/resolver.cc
@@ -160,6 +160,10 @@
         return false;
     }
 
+    if (!validator_.Enables(b.AST().Enables())) {
+        return false;
+    }
+
     // Create the semantic module. Don't be tempted to std::move() these, they're used below.
     auto* mod = b.create<sem::Module>(dependencies_.ordered_globals, enabled_extensions_);
     ApplyDiagnosticSeverities(mod);
@@ -640,6 +644,17 @@
                     global->Attributes().index = value.Get();
                     return kSuccess;
                 },
+                [&](const ast::ColorAttribute* attr) {
+                    if (!has_io_address_space) {
+                        return kInvalid;
+                    }
+                    auto value = ColorAttribute(attr);
+                    if (!value) {
+                        return kErrored;
+                    }
+                    global->Attributes().color = value.Get();
+                    return kSuccess;
+                },
                 [&](const ast::BuiltinAttribute* attr) {
                     if (!has_io_address_space) {
                         return kInvalid;
@@ -723,6 +738,14 @@
                     sem->Attributes().location = value.Get();
                     return true;
                 },
+                [&](const ast::ColorAttribute* attr) {
+                    auto value = ColorAttribute(attr);
+                    if (TINT_UNLIKELY(!value)) {
+                        return false;
+                    }
+                    sem->Attributes().color = value.Get();
+                    return true;
+                },
                 [&](const ast::BuiltinAttribute* attr) -> bool { return BuiltinAttribute(attr); },
                 [&](const ast::InvariantAttribute* attr) -> bool {
                     return InvariantAttribute(attr);
@@ -3709,6 +3732,29 @@
     return static_cast<uint32_t>(value);
 }
 
+tint::Result<uint32_t> Resolver::ColorAttribute(const ast::ColorAttribute* attr) {
+    ExprEvalStageConstraint constraint{core::EvaluationStage::kConstant, "@color value"};
+    TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
+    auto* materialized = Materialize(ValueExpression(attr->expr));
+    if (!materialized) {
+        return Failure{};
+    }
+
+    if (!materialized->Type()->IsAnyOf<core::type::I32, core::type::U32>()) {
+        AddError("@color must be an i32 or u32 value", attr->source);
+        return Failure{};
+    }
+
+    auto const_value = materialized->ConstantValue();
+    auto value = const_value->ValueAs<AInt>();
+    if (value < 0) {
+        AddError("@color value must be non-negative", attr->source);
+        return Failure{};
+    }
+
+    return static_cast<uint32_t>(value);
+}
 tint::Result<uint32_t> Resolver::IndexAttribute(const ast::IndexAttribute* attr) {
     ExprEvalStageConstraint constraint{core::EvaluationStage::kConstant, "@index value"};
     TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
@@ -4342,6 +4388,14 @@
                     attributes.index = value.Get();
                     return true;
                 },
+                [&](const ast::ColorAttribute* attr) {
+                    auto value = ColorAttribute(attr);
+                    if (!value) {
+                        return false;
+                    }
+                    attributes.color = value.Get();
+                    return true;
+                },
                 [&](const ast::BuiltinAttribute* attr) {
                     auto value = BuiltinAttribute(attr);
                     if (!value) {
diff --git a/src/tint/lang/wgsl/resolver/resolver.h b/src/tint/lang/wgsl/resolver/resolver.h
index c489465..344c5af 100644
--- a/src/tint/lang/wgsl/resolver/resolver.h
+++ b/src/tint/lang/wgsl/resolver/resolver.h
@@ -419,6 +419,10 @@
     /// @returns the location value on success.
     tint::Result<uint32_t> LocationAttribute(const ast::LocationAttribute* attr);
 
+    /// Resolves the `@color` attribute @p attr
+    /// @returns the color value on success.
+    tint::Result<uint32_t> ColorAttribute(const ast::ColorAttribute* attr);
+
     /// Resolves the `@index` attribute @p attr
     /// @returns the index value on success.
     tint::Result<uint32_t> IndexAttribute(const ast::IndexAttribute* attr);
diff --git a/src/tint/lang/wgsl/resolver/uniformity.cc b/src/tint/lang/wgsl/resolver/uniformity.cc
index 08259f7..0cea0be 100644
--- a/src/tint/lang/wgsl/resolver/uniformity.cc
+++ b/src/tint/lang/wgsl/resolver/uniformity.cc
@@ -210,7 +210,7 @@
         for (size_t i = 0; i < func->params.Length(); i++) {
             auto* param = func->params[i];
             auto param_name = param->name->symbol.Name();
-            auto* sem = b.Sem().Get<sem::Parameter>(param);
+            auto* sem = b.Sem().Get(param);
             parameters[i].sem = sem;
 
             parameters[i].value = CreateNode({"param_", param_name});
@@ -543,7 +543,7 @@
             // we do not skip the `i==j` case.
             for (size_t j = 0; j < func->params.Length(); j++) {
                 auto tag = get_param_tag(reachable, j);
-                auto* source_param = sem_.Get<sem::Parameter>(func->params[j]);
+                auto* source_param = sem_.Get(func->params[j]);
                 if (tag == ParameterTag::ParameterContentsRequiredToBeUniform) {
                     param_info.ptr_output_source_param_contents.Push(source_param);
                 } else if (tag == ParameterTag::ParameterValueRequiredToBeUniform) {
diff --git a/src/tint/lang/wgsl/resolver/validator.cc b/src/tint/lang/wgsl/resolver/validator.cc
index 67ba42d..5046bd1 100644
--- a/src/tint/lang/wgsl/resolver/validator.cc
+++ b/src/tint/lang/wgsl/resolver/validator.cc
@@ -291,6 +291,40 @@
     return nullptr;
 }
 
+bool Validator::Enables(VectorRef<const ast::Enable*> enables) const {
+    auto source_of = [&](wgsl::Extension ext) {
+        for (auto* enable : enables) {
+            for (auto* extension : enable->extensions) {
+                if (extension->name == ext) {
+                    return extension->source;
+                }
+            }
+        }
+        return Source{};
+    };
+
+    // List of extensions that cannot be used together.
+    std::pair<wgsl::Extension, wgsl::Extension> incompatible[] = {
+        {
+            wgsl::Extension::kChromiumExperimentalPixelLocal,
+            wgsl::Extension::kChromiumExperimentalFramebufferFetch,
+        },
+    };
+
+    for (auto pair : incompatible) {
+        if (enabled_extensions_.Contains(pair.first) && enabled_extensions_.Contains(pair.second)) {
+            std::string a{ToString(pair.first)};
+            std::string b{ToString(pair.second)};
+            AddError("extension '" + a + "' cannot be used with extension '" + b + "'",
+                     source_of(pair.first));
+            AddNote("'" + b + "' enabled here", source_of(pair.second));
+            return false;
+        }
+    }
+
+    return true;
+}
+
 bool Validator::Atomic(const ast::TemplatedIdentifier* a, const core::type::Atomic* s) const {
     // https://gpuweb.github.io/gpuweb/wgsl/#atomic-types
     // T must be either u32 or i32.
@@ -1116,6 +1150,7 @@
     Hashset<std::pair<uint32_t, uint32_t>, 8> locations_and_indices;
     const ast::LocationAttribute* first_nonzero_location = nullptr;
     const ast::IndexAttribute* first_nonzero_index = nullptr;
+    Hashset<uint32_t, 4> colors;
     enum class ParamOrRetType {
         kParameter,
         kReturnType,
@@ -1127,11 +1162,13 @@
                                                      ParamOrRetType param_or_ret,
                                                      bool is_struct_member,
                                                      std::optional<uint32_t> location,
-                                                     std::optional<uint32_t> index) {
+                                                     std::optional<uint32_t> index,
+                                                     std::optional<uint32_t> color) {
         // Scan attributes for pipeline IO attributes.
         // Check for overlap with attributes that have been seen previously.
         const ast::Attribute* pipeline_io_attribute = nullptr;
         const ast::LocationAttribute* location_attribute = nullptr;
+        const ast::ColorAttribute* color_attribute = nullptr;
         const ast::IndexAttribute* index_attribute = nullptr;
         const ast::InterpolateAttribute* interpolate_attribute = nullptr;
         const ast::InvariantAttribute* invariant_attribute = nullptr;
@@ -1184,8 +1221,33 @@
                 },
                 [&](const ast::IndexAttribute* index_attr) {
                     index_attribute = index_attr;
+
+                    if (TINT_UNLIKELY(!index.has_value())) {
+                        TINT_ICE() << "@index has no value";
+                        return false;
+                    }
+
                     return IndexAttribute(index_attr, stage);
                 },
+                [&](const ast::ColorAttribute* col_attr) {
+                    color_attribute = col_attr;
+                    if (pipeline_io_attribute) {
+                        AddError("multiple entry point IO attributes", attr->source);
+                        AddNote("previously consumed " + AttrToStr(pipeline_io_attribute),
+                                pipeline_io_attribute->source);
+                        return false;
+                    }
+                    pipeline_io_attribute = attr;
+
+                    bool is_input = param_or_ret == ParamOrRetType::kParameter;
+
+                    if (TINT_UNLIKELY(!color.has_value())) {
+                        TINT_ICE() << "@color has no value";
+                        return false;
+                    }
+
+                    return ColorAttribute(col_attr, ty, stage, source, is_input);
+                },
                 [&](const ast::InterpolateAttribute* interpolate) {
                     interpolate_attribute = interpolate;
                     return InterpolateAttribute(interpolate, ty, stage);
@@ -1276,6 +1338,13 @@
                 }
             }
 
+            if (color_attribute && !colors.Add(color.value())) {
+                StringStream err;
+                err << "@color(" << color.value() << ") appears multiple times";
+                AddError(err.str(), color_attribute->source);
+                return false;
+            }
+
             if (interpolate_attribute) {
                 if (!pipeline_io_attribute ||
                     !pipeline_io_attribute->Is<ast::LocationAttribute>()) {
@@ -1304,39 +1373,39 @@
     };
 
     // Outer lambda for validating the entry point attributes for a type.
-    auto validate_entry_point_attributes = [&](VectorRef<const ast::Attribute*> attrs,
-                                               const core::type::Type* ty, Source source,
-                                               ParamOrRetType param_or_ret,
-                                               std::optional<uint32_t> location,
-                                               std::optional<uint32_t> index) {
-        if (!validate_entry_point_attributes_inner(attrs, ty, source, param_or_ret,
-                                                   /*is_struct_member*/ false, location, index)) {
-            return false;
-        }
+    auto validate_entry_point_attributes =
+        [&](VectorRef<const ast::Attribute*> attrs, const core::type::Type* ty, Source source,
+            ParamOrRetType param_or_ret, std::optional<uint32_t> location,
+            std::optional<uint32_t> index, std::optional<uint32_t> color) {
+            if (!validate_entry_point_attributes_inner(attrs, ty, source, param_or_ret,
+                                                       /*is_struct_member*/ false, location, index,
+                                                       color)) {
+                return false;
+            }
 
-        if (auto* str = ty->As<sem::Struct>()) {
-            for (auto* member : str->Members()) {
-                if (!validate_entry_point_attributes_inner(
-                        member->Declaration()->attributes, member->Type(),
-                        member->Declaration()->source, param_or_ret,
-                        /*is_struct_member*/ true, member->Attributes().location,
-                        member->Attributes().index)) {
-                    AddNote("while analyzing entry point '" + decl->name->symbol.Name() + "'",
-                            decl->source);
-                    return false;
+            if (auto* str = ty->As<sem::Struct>()) {
+                for (auto* member : str->Members()) {
+                    if (!validate_entry_point_attributes_inner(
+                            member->Declaration()->attributes, member->Type(),
+                            member->Declaration()->source, param_or_ret,
+                            /*is_struct_member*/ true, member->Attributes().location,
+                            member->Attributes().index, member->Attributes().color)) {
+                        AddNote("while analyzing entry point '" + decl->name->symbol.Name() + "'",
+                                decl->source);
+                        return false;
+                    }
                 }
             }
-        }
 
-        return true;
-    };
+            return true;
+        };
 
     for (auto* param : func->Parameters()) {
         auto* param_decl = param->Declaration();
         auto& attrs = param->Attributes();
         if (!validate_entry_point_attributes(param_decl->attributes, param->Type(),
                                              param_decl->source, ParamOrRetType::kParameter,
-                                             attrs.location, attrs.index)) {
+                                             attrs.location, attrs.index, attrs.color)) {
             return false;
         }
     }
@@ -1349,7 +1418,8 @@
     if (!func->ReturnType()->Is<core::type::Void>()) {
         if (!validate_entry_point_attributes(decl->return_type_attributes, func->ReturnType(),
                                              decl->source, ParamOrRetType::kReturnType,
-                                             func->ReturnLocation(), func->ReturnIndex())) {
+                                             func->ReturnLocation(), func->ReturnIndex(),
+                                             /* color */ std::nullopt)) {
             return false;
         }
     }
@@ -2154,6 +2224,7 @@
     }
 
     Hashset<std::pair<uint32_t, uint32_t>, 8> locations_and_indices;
+    Hashset<uint32_t, 4> colors;
     for (auto* member : str->Members()) {
         if (auto* r = member->Type()->As<sem::Array>()) {
             if (r->Count()->Is<core::type::RuntimeArrayCount>()) {
@@ -2178,6 +2249,7 @@
         auto has_position = false;
         const ast::IndexAttribute* index_attribute = nullptr;
         const ast::LocationAttribute* location_attribute = nullptr;
+        const ast::ColorAttribute* color_attribute = nullptr;
         const ast::InvariantAttribute* invariant_attribute = nullptr;
         const ast::InterpolateAttribute* interpolate_attribute = nullptr;
         for (auto* attr : member->Declaration()->attributes) {
@@ -2197,6 +2269,11 @@
                     index_attribute = index;
                     return IndexAttribute(index, stage);
                 },
+                [&](const ast::ColorAttribute* color) {
+                    color_attribute = color;
+                    return ColorAttribute(color, member->Type(), stage,
+                                          member->Declaration()->source);
+                },
                 [&](const ast::BuiltinAttribute* builtin_attr) {
                     if (!BuiltinAttribute(builtin_attr, member->Type(), stage,
                                           /* is_input */ false)) {
@@ -2266,6 +2343,16 @@
                 return false;
             }
         }
+
+        if (color_attribute) {
+            uint32_t color = member->Attributes().color.value();
+            if (!colors.Add(color)) {
+                StringStream err;
+                err << "@color(" << color << ") appears multiple times";
+                AddError(err.str(), color_attribute->source);
+                return false;
+            }
+        }
     }
 
     return true;
@@ -2293,6 +2380,38 @@
     return true;
 }
 
+bool Validator::ColorAttribute(const ast::ColorAttribute* attr,
+                               const core::type::Type* type,
+                               ast::PipelineStage stage,
+                               const Source& source,
+                               const std::optional<bool> is_input) const {
+    if (!enabled_extensions_.Contains(wgsl::Extension::kChromiumExperimentalFramebufferFetch)) {
+        AddError(
+            "use of @color requires enabling extension 'chromium_experimental_framebuffer_fetch'",
+            attr->source);
+        return false;
+    }
+
+    bool is_stage_non_fragment =
+        stage != ast::PipelineStage::kNone && stage != ast::PipelineStage::kFragment;
+    bool is_output = !is_input.value_or(true);
+    if (is_stage_non_fragment || is_output) {
+        AddError("@color can only be used for fragment shader input", attr->source);
+        return false;
+    }
+
+    if (!type->is_numeric_scalar_or_vector()) {
+        std::string invalid_type = sem_.TypeNameOf(type);
+        AddError("cannot apply @color to declaration of type '" + invalid_type + "'", source);
+        AddNote(
+            "@color must only be applied to declarations of numeric scalar or numeric vector type",
+            attr->source);
+        return false;
+    }
+
+    return true;
+}
+
 bool Validator::IndexAttribute(const ast::IndexAttribute* attr,
                                ast::PipelineStage stage,
                                const std::optional<bool> is_input) const {
diff --git a/src/tint/lang/wgsl/resolver/validator.h b/src/tint/lang/wgsl/resolver/validator.h
index 03466ee..8bf5af3 100644
--- a/src/tint/lang/wgsl/resolver/validator.h
+++ b/src/tint/lang/wgsl/resolver/validator.h
@@ -168,6 +168,11 @@
     /// @returns true if the given type is host-shareable
     bool IsHostShareable(const core::type::Type* type) const;
 
+    /// Validates the enabled extensions
+    /// @param enables the extension enables
+    /// @returns true on success, false otherwise.
+    bool Enables(VectorRef<const ast::Enable*> enables) const;
+
     /// Validates pipeline stages
     /// @param entry_points the entry points to the module
     /// @returns true on success, false otherwise.
@@ -351,6 +356,20 @@
                            const ast::PipelineStage stage,
                            const Source& source) const;
 
+    /// Validates a color attribute
+    /// @param attr the color attribute to validate
+    /// @param type the variable type
+    /// @param stage the current pipeline stage
+    /// @param source the source of declaration using the attribute
+    /// @param is_input true if is an input variable, false if output variable, std::nullopt is
+    /// unknown.
+    /// @returns true on success, false otherwise.
+    bool ColorAttribute(const ast::ColorAttribute* attr,
+                        const core::type::Type* type,
+                        ast::PipelineStage stage,
+                        const Source& source,
+                        const std::optional<bool> is_input = std::nullopt) const;
+
     /// Validates a index attribute
     /// @param index_attr the index attribute to validate
     /// @param stage the current pipeline stage
diff --git a/src/tint/lang/wgsl/resolver/variable_test.cc b/src/tint/lang/wgsl/resolver/variable_test.cc
index c87c4df..d3d9f10 100644
--- a/src/tint/lang/wgsl/resolver/variable_test.cc
+++ b/src/tint/lang/wgsl/resolver/variable_test.cc
@@ -700,7 +700,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* param = Sem().Get<sem::Parameter>(p);
+    auto* param = Sem().Get(p);
     auto* local = Sem().Get<sem::LocalVariable>(l);
 
     ASSERT_NE(param, nullptr);
@@ -898,7 +898,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* param = Sem().Get<sem::Parameter>(p);
+    auto* param = Sem().Get(p);
     auto* local = Sem().Get<sem::LocalVariable>(c);
 
     ASSERT_NE(param, nullptr);
@@ -1222,7 +1222,7 @@
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
     auto* func = Sem().Get(f);
-    auto* param = Sem().Get<sem::Parameter>(p);
+    auto* param = Sem().Get(p);
 
     ASSERT_NE(func, nullptr);
     ASSERT_NE(param, nullptr);
@@ -1243,7 +1243,7 @@
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
     auto* global = Sem().Get(g);
-    auto* param = Sem().Get<sem::Parameter>(p);
+    auto* param = Sem().Get(p);
 
     ASSERT_NE(global, nullptr);
     ASSERT_NE(param, nullptr);
@@ -1264,7 +1264,7 @@
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
     auto* global = Sem().Get(g);
-    auto* param = Sem().Get<sem::Parameter>(p);
+    auto* param = Sem().Get(p);
 
     ASSERT_NE(global, nullptr);
     ASSERT_NE(param, nullptr);
@@ -1285,7 +1285,7 @@
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
     auto* alias = Sem().Get(a);
-    auto* param = Sem().Get<sem::Parameter>(p);
+    auto* param = Sem().Get(p);
 
     ASSERT_NE(alias, nullptr);
     ASSERT_NE(param, nullptr);
diff --git a/src/tint/lang/wgsl/sem/type_mappings.h b/src/tint/lang/wgsl/sem/type_mappings.h
index dcee79b..fed219f 100644
--- a/src/tint/lang/wgsl/sem/type_mappings.h
+++ b/src/tint/lang/wgsl/sem/type_mappings.h
@@ -50,6 +50,7 @@
 class LiteralExpression;
 class Node;
 class Override;
+class Parameter;
 class PhonyExpression;
 class Statement;
 class Struct;
@@ -71,6 +72,7 @@
 class GlobalVariable;
 class IfStatement;
 class Node;
+class Parameter;
 class Statement;
 class Struct;
 class StructMember;
@@ -100,6 +102,7 @@
     Function* operator()(ast::Function*);
     GlobalVariable* operator()(ast::Override*);
     IfStatement* operator()(ast::IfStatement*);
+    Parameter* operator()(ast::Parameter*);
     Statement* operator()(ast::Statement*);
     Struct* operator()(ast::Struct*);
     StructMember* operator()(ast::StructMember*);
diff --git a/src/tint/lang/wgsl/sem/variable.h b/src/tint/lang/wgsl/sem/variable.h
index 3ee24f3..9809fcb 100644
--- a/src/tint/lang/wgsl/sem/variable.h
+++ b/src/tint/lang/wgsl/sem/variable.h
@@ -165,6 +165,10 @@
     /// @note a GlobalVariable generally doesn't have a `index` in WGSL, as it isn't allowed by
     /// the spec. The location maybe attached by transforms such as CanonicalizeEntryPointIO.
     std::optional<uint32_t> index;
+    /// The `color` attribute value for the variable, if set
+    /// @note a GlobalVariable generally doesn't have a `color` in WGSL, as it isn't allowed by
+    /// the spec. The location maybe attached by transforms such as CanonicalizeEntryPointIO.
+    std::optional<uint32_t> color;
 };
 
 /// GlobalVariable is a module-scope variable
@@ -210,6 +214,8 @@
     std::optional<uint32_t> location;
     /// The `index` attribute value for the variable, if set
     std::optional<uint32_t> index;
+    /// The `color` attribute value for the variable, if set
+    std::optional<uint32_t> color;
 };
 
 /// Parameter is a function parameter