Import Tint changes from Dawn

Changes:
  - b383cf0629faeda3a75c1a6c8eb5b5859040671d [tint][gn] Hook up tint_info to the build by Ben Clayton <bclayton@google.com>
  - d8f6206dc6e0f9de2fb30dd9b73f49faf1f65f93 [tint][resolver] Add validation test for @color and @loca... by Ben Clayton <bclayton@google.com>
  - 570461ecdae1e36c61aa830034506bebb57b6186 [tint][transform] Update CanonicalizeEntryPointIO tests by Ben Clayton <bclayton@google.com>
  - 0e4aee6e79ee3c5c24d2a34b41b1398cc7a277cb [tint][wgsl] Parse @color by Ben Clayton <bclayton@google.com>
  - e73b3936268202c1baf069830ab3ef867c9ee681 [tint][msl] Implement chromium_experimental_framebuffer_f... by Ben Clayton <bclayton@google.com>
  - 7836e1b2d5e834a5eac09c4bcc3b0079e127f081 [tint][resolver] Fix false-positive dual-source-blending ... by Ben Clayton <bclayton@google.com>
  - 013fd37bcd800eab06da0893a9f3a560d0bc7540 [tint] Fix conflict between two CLs that landed, breaking... by Ben Clayton <bclayton@google.com>
  - 4348a05e24ea85775b808b0068d97d1648a9f046 [spirv-writer] Support clamp_frag_depth for multiple entr... by James Price <jrprice@google.com>
  - 2af581505700219f2f7dcfc501b0027215e7c111 [tint][inspector] Reflect the @color attribute by Ben Clayton <bclayton@google.com>
  - 45f5c607bdcdef17b8c382f8a279bd48490bab0e [tint] Resolve ColorAttribute by Ben Clayton <bclayton@google.com>
  - c36093e932c33b6269204461ebbf864bd7e717de [tint][wgsl] Add ColorAttribute by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: b383cf0629faeda3a75c1a6c8eb5b5859040671d
Change-Id: If54ace3ab35b5ed717aed8e90ba81199a70a57dc
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/160340
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 549aefc..d083bef 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -262,6 +262,7 @@
   deps = []
   if (tint_build_cmds) {
     deps += [
+      "${tint_src_dir}/cmd/info",
       "${tint_src_dir}/cmd/remote_compile",
       "${tint_src_dir}/cmd/tint",
     ]
diff --git a/src/tint/cmd/common/helper.cc b/src/tint/cmd/common/helper.cc
index 41fc79a..4570f41 100644
--- a/src/tint/cmd/common/helper.cc
+++ b/src/tint/cmd/common/helper.cc
@@ -270,8 +270,12 @@
             for (const auto& var : entry_point.input_variables) {
                 std::cout << "\t";
 
-                if (var.has_location_attribute) {
-                    std::cout << "@location(" << var.location_attribute << ") ";
+                if (auto location = var.attributes.location) {
+                    std::cout << "@location(" << location.value() << ") ";
+                }
+
+                if (auto color = var.attributes.color) {
+                    std::cout << "@color(" << color.value() << ") ";
                 }
                 std::cout << var.name << std::endl;
             }
@@ -282,8 +286,8 @@
             for (const auto& var : entry_point.output_variables) {
                 std::cout << "\t";
 
-                if (var.has_location_attribute) {
-                    std::cout << "@location(" << var.location_attribute << ") ";
+                if (auto location = var.attributes.location) {
+                    std::cout << "@location(" << location.value() << ") ";
                 }
                 std::cout << var.name << std::endl;
             }
diff --git a/src/tint/cmd/info/main.cc b/src/tint/cmd/info/main.cc
index 3160358..a68a46b 100644
--- a/src/tint/cmd/info/main.cc
+++ b/src/tint/cmd/info/main.cc
@@ -108,14 +108,9 @@
         std::cout << std::endl;
         std::cout << "{" << std::endl;
         std::cout << "\"name\": \"" << var.name << "\"";
-        if (var.has_location_attribute) {
-            std::cout << "," << std::endl;
-            std::cout << "\"location\": " << var.location_attribute << "," << std::endl;
-            std::cout << "\"component_type\": \""
-                      << tint::cmd::ComponentTypeToString(var.component_type) << "\"," << std::endl;
-            std::cout << "\"composition_type\": \""
-                      << tint::cmd::CompositionTypeToString(var.composition_type) << "\","
-                      << std::endl;
+        std::cout << "," << std::endl;
+        if (auto location = var.attributes.location) {
+            std::cout << "\"location\": " << location.value() << "," << std::endl;
             std::cout << "\"interpolation\": {" << std::endl;
             std::cout << "\"type\": \""
                       << tint::cmd::InterpolationTypeToString(var.interpolation_type) << "\","
@@ -123,8 +118,15 @@
             std::cout << "\"sampling\": \""
                       << tint::cmd::InterpolationSamplingToString(var.interpolation_sampling)
                       << "\"" << std::endl;
-            std::cout << "}" << std::endl;
+            std::cout << "}," << std::endl;
         }
+        if (auto color = var.attributes.color) {
+            std::cout << "\"color\": " << color.value() << "," << std::endl;
+        }
+        std::cout << "\"component_type\": \""
+                  << tint::cmd::ComponentTypeToString(var.component_type) << "\"," << std::endl;
+        std::cout << "\"composition_type\": \""
+                  << tint::cmd::CompositionTypeToString(var.composition_type) << "\"" << std::endl;
         std::cout << std::endl;
         std::cout << "}";
     };
diff --git a/src/tint/cmd/tint/main.cc b/src/tint/cmd/tint/main.cc
index cc3aba1..19a9c85 100644
--- a/src/tint/cmd/tint/main.cc
+++ b/src/tint/cmd/tint/main.cc
@@ -734,7 +734,8 @@
         if (enable->HasExtension(tint::wgsl::Extension::kChromiumExperimentalSubgroups)) {
             msl_version = std::max(msl_version, tint::msl::validate::MslVersion::kMsl_2_1);
         }
-        if (enable->HasExtension(tint::wgsl::Extension::kChromiumExperimentalPixelLocal)) {
+        if (enable->HasExtension(tint::wgsl::Extension::kChromiumExperimentalPixelLocal) ||
+            enable->HasExtension(tint::wgsl::Extension::kChromiumExperimentalFramebufferFetch)) {
             msl_version = std::max(msl_version, tint::msl::validate::MslVersion::kMsl_2_3);
         }
     }
diff --git a/src/tint/lang/core/attribute.cc b/src/tint/lang/core/attribute.cc
index 7b18915..2568294 100644
--- a/src/tint/lang/core/attribute.cc
+++ b/src/tint/lang/core/attribute.cc
@@ -51,6 +51,9 @@
     if (str == "builtin") {
         return Attribute::kBuiltin;
     }
+    if (str == "color") {
+        return Attribute::kColor;
+    }
     if (str == "compute") {
         return Attribute::kCompute;
     }
@@ -103,6 +106,8 @@
             return "binding";
         case Attribute::kBuiltin:
             return "builtin";
+        case Attribute::kColor:
+            return "color";
         case Attribute::kCompute:
             return "compute";
         case Attribute::kDiagnostic:
diff --git a/src/tint/lang/core/attribute.h b/src/tint/lang/core/attribute.h
index 3e7b087..46d7619 100644
--- a/src/tint/lang/core/attribute.h
+++ b/src/tint/lang/core/attribute.h
@@ -52,6 +52,7 @@
     kAlign,
     kBinding,
     kBuiltin,
+    kColor,
     kCompute,
     kDiagnostic,
     kFragment,
@@ -85,9 +86,9 @@
 Attribute ParseAttribute(std::string_view str);
 
 constexpr std::string_view kAttributeStrings[] = {
-    "align",    "binding", "builtin", "compute",        "diagnostic", "fragment",
-    "group",    "id",      "index",   "interpolate",    "invariant",  "location",
-    "must_use", "size",    "vertex",  "workgroup_size",
+    "align",    "binding",  "builtin", "color",  "compute",        "diagnostic",
+    "fragment", "group",    "id",      "index",  "interpolate",    "invariant",
+    "location", "must_use", "size",    "vertex", "workgroup_size",
 };
 
 }  // namespace tint::core
diff --git a/src/tint/lang/core/attribute_bench.cc b/src/tint/lang/core/attribute_bench.cc
index 1621a94..16a660c 100644
--- a/src/tint/lang/core/attribute_bench.cc
+++ b/src/tint/lang/core/attribute_bench.cc
@@ -66,97 +66,104 @@
         "Euiltin",
         "bPTTltin",
         "builtdxx",
-        "c44mpute",
-        "coSSpuVVe",
-        "RomR22e",
+        "c44lor",
+        "coVVSSr",
+        "22RRr",
+        "color",
+        "cFor",
+        "colr",
+        "ROOHVr",
+        "copuye",
+        "llnorrp77te",
+        "comp4t00",
         "compute",
-        "cFpu9e",
-        "comute",
-        "VOORRHte",
-        "dyagnstic",
-        "d77agnnnsllrrc",
-        "dia400ostic",
+        "opooe",
+        "zzpute",
+        "ciimppu1",
+        "XXiagnostic",
+        "IIia99nonnt55c",
+        "dYagSSrrstHHac",
         "diagnostic",
-        "danstooc",
-        "dignszzic",
-        "d11ansppiic",
-        "XXragment",
-        "fIIa9955nnnt",
-        "aarHHgmenYSS",
+        "dakkoHtc",
+        "jiagnsgRR",
+        "diagbost",
+        "fragjent",
+        "fragmnt",
+        "frqent",
         "fragment",
-        "fkkaet",
-        "gjamRRn",
-        "fabmnt",
-        "gjoup",
-        "goup",
-        "goq",
+        "fragenNN",
+        "ravvent",
+        "frgmQQnt",
+        "grof",
+        "grojp",
+        "NNrw2u",
         "group",
-        "Nroup",
-        "govv",
-        "gruQQ",
-        "r",
-        "jd",
-        "NNw",
+        "grup",
+        "grroup",
+        "Group",
+        "iFF",
+        "NN",
+        "iAA",
         "id",
-        "i",
-        "rrd",
-        "iG",
-        "inFFex",
-        "iE",
-        "inrrx",
+        "d",
+        "L",
+        "yy",
+        "nek",
+        "indx",
+        "Jndx",
         "index",
-        "inx",
-        "inJJD",
-        "ie",
-        "inerpklae",
-        "intrpolate",
-        "inJerpolae",
+        "incex",
+        "iOdex",
+        "__nttKKvv",
+        "int8rpoxx5e",
+        "inteqq__lte",
+        "interpqlate",
         "interpolate",
-        "interpocate",
-        "interpolaOe",
-        "__nttevvpoKKate",
-        "xnvari5n8",
-        "inFq__ant",
-        "iqqariant",
+        "33ntOpolat66",
+        "intoott6QQlate",
+        "66terpolate",
+        "zzxvO6rint",
+        "invayyiant",
+        "HHnariZt",
         "invariant",
-        "invar6a33O",
-        "i96arQttanoo",
-        "inari66nt",
-        "lOxati6zz",
-        "locyytion",
-        "lHHtion",
+        "iWW44rianq",
+        "iOOvaiant",
+        "ivariYnt",
+        "ltion",
+        "loaFion",
+        "wocatio",
         "location",
-        "qWW4caton",
-        "locOOton",
-        "ocatiYn",
-        "m_use",
-        "mutFuse",
-        "wust_us",
+        "Kcatoff",
+        "qocKKtion",
+        "lFcmmt3on",
+        "mustuse",
+        "must_se",
+        "ubbt_ube",
         "must_use",
-        "Kst_sff",
-        "qusKK_use",
-        "mFsmm_3se",
-        "ize",
-        "sze",
-        "sbbb",
+        "mstiius",
+        "muqt_uOe",
+        "muTTt_usvv",
+        "FFize",
+        "QP00",
+        "siPe",
         "size",
-        "iie",
-        "siqe",
-        "svvTTe",
-        "vertFFx",
-        "vrQ00P",
-        "vePtex",
+        "sis77",
+        "CiRbbe",
+        "sizXX",
+        "CCrtOOOO",
+        "vrsuL",
+        "verteX",
         "vertex",
-        "vsste77",
-        "veCtRRbb",
-        "verteXX",
-        "workgqou_siCCOOO",
-        "worsgroupsuzL",
-        "wXrkgroup_size",
+        "verte",
+        "qqrx",
+        "verte22",
+        "workgou0yzzizXX",
+        "workgrop_VPize",
+        "wokgroupnnsCze",
         "workgroup_size",
-        "workgroup_sze",
-        "wqqrOgoupize",
-        "workg22oup_size",
+        "workgrouq_sizHA",
+        "workgrup_size",
+        "forroupKKsize",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/lang/core/attribute_test.cc b/src/tint/lang/core/attribute_test.cc
index 58e03ab..ac6b338 100644
--- a/src/tint/lang/core/attribute_test.cc
+++ b/src/tint/lang/core/attribute_test.cc
@@ -57,14 +57,23 @@
 }
 
 static constexpr Case kValidCases[] = {
-    {"align", Attribute::kAlign},           {"binding", Attribute::kBinding},
-    {"builtin", Attribute::kBuiltin},       {"compute", Attribute::kCompute},
-    {"diagnostic", Attribute::kDiagnostic}, {"fragment", Attribute::kFragment},
-    {"group", Attribute::kGroup},           {"id", Attribute::kId},
-    {"index", Attribute::kIndex},           {"interpolate", Attribute::kInterpolate},
-    {"invariant", Attribute::kInvariant},   {"location", Attribute::kLocation},
-    {"must_use", Attribute::kMustUse},      {"size", Attribute::kSize},
-    {"vertex", Attribute::kVertex},         {"workgroup_size", Attribute::kWorkgroupSize},
+    {"align", Attribute::kAlign},
+    {"binding", Attribute::kBinding},
+    {"builtin", Attribute::kBuiltin},
+    {"color", Attribute::kColor},
+    {"compute", Attribute::kCompute},
+    {"diagnostic", Attribute::kDiagnostic},
+    {"fragment", Attribute::kFragment},
+    {"group", Attribute::kGroup},
+    {"id", Attribute::kId},
+    {"index", Attribute::kIndex},
+    {"interpolate", Attribute::kInterpolate},
+    {"invariant", Attribute::kInvariant},
+    {"location", Attribute::kLocation},
+    {"must_use", Attribute::kMustUse},
+    {"size", Attribute::kSize},
+    {"vertex", Attribute::kVertex},
+    {"workgroup_size", Attribute::kWorkgroupSize},
 };
 
 static constexpr Case kInvalidCases[] = {
@@ -77,45 +86,48 @@
     {"ppqqiliHH", Attribute::kUndefined},
     {"bucv", Attribute::kUndefined},
     {"biltGn", Attribute::kUndefined},
-    {"compiive", Attribute::kUndefined},
-    {"8WWmpute", Attribute::kUndefined},
-    {"cxxpute", Attribute::kUndefined},
-    {"dXagnosigg", Attribute::kUndefined},
-    {"dagnXuVc", Attribute::kUndefined},
-    {"diagnosti3", Attribute::kUndefined},
-    {"fraEment", Attribute::kUndefined},
-    {"PPagTTent", Attribute::kUndefined},
-    {"xxragddnt", Attribute::kUndefined},
-    {"g44oup", Attribute::kUndefined},
-    {"grVVSSp", Attribute::kUndefined},
-    {"22RRp", Attribute::kUndefined},
-    {"d", Attribute::kUndefined},
-    {"i", Attribute::kUndefined},
-    {"OVd", Attribute::kUndefined},
-    {"ndyx", Attribute::kUndefined},
-    {"n77rrldGx", Attribute::kUndefined},
-    {"inde40", Attribute::kUndefined},
-    {"itooolate", Attribute::kUndefined},
-    {"intezplate", Attribute::kUndefined},
-    {"ppnerii1olat", Attribute::kUndefined},
-    {"invarianXX", Attribute::kUndefined},
-    {"inv55ria99nII", Attribute::kUndefined},
-    {"irrvariaSSaHH", Attribute::kUndefined},
-    {"lkkcin", Attribute::kUndefined},
-    {"gjctRRo", Attribute::kUndefined},
-    {"lcbton", Attribute::kUndefined},
-    {"mustjuse", Attribute::kUndefined},
-    {"must_se", Attribute::kUndefined},
-    {"muquse", Attribute::kUndefined},
-    {"szNN", Attribute::kUndefined},
-    {"zvv", Attribute::kUndefined},
-    {"QQze", Attribute::kUndefined},
-    {"eterf", Attribute::kUndefined},
-    {"vertjx", Attribute::kUndefined},
-    {"v82wNNx", Attribute::kUndefined},
-    {"worgroup_size", Attribute::kUndefined},
-    {"workgrourr_size", Attribute::kUndefined},
-    {"workgroGp_size", Attribute::kUndefined},
+    {"covior", Attribute::kUndefined},
+    {"co8WWr", Attribute::kUndefined},
+    {"Mxxlo", Attribute::kUndefined},
+    {"cXputgg", Attribute::kUndefined},
+    {"opuXe", Attribute::kUndefined},
+    {"comp3te", Attribute::kUndefined},
+    {"diagnostiE", Attribute::kUndefined},
+    {"TTiagnosPPi", Attribute::kUndefined},
+    {"diagdoxxtic", Attribute::kUndefined},
+    {"44ragment", Attribute::kUndefined},
+    {"fSSagmenVV", Attribute::kUndefined},
+    {"Rag2Rent", Attribute::kUndefined},
+    {"gFup", Attribute::kUndefined},
+    {"grop", Attribute::kUndefined},
+    {"ROOHVp", Attribute::kUndefined},
+    {"y", Attribute::kUndefined},
+    {"Gn77rl", Attribute::kUndefined},
+    {"04d", Attribute::kUndefined},
+    {"oox", Attribute::kUndefined},
+    {"inzz", Attribute::kUndefined},
+    {"1ippex", Attribute::kUndefined},
+    {"interpoXXate", Attribute::kUndefined},
+    {"intII99r55olate", Attribute::kUndefined},
+    {"intaarpoSSrHHYe", Attribute::kUndefined},
+    {"kkvHant", Attribute::kUndefined},
+    {"jgaianRR", Attribute::kUndefined},
+    {"inaianb", Attribute::kUndefined},
+    {"locajion", Attribute::kUndefined},
+    {"locaton", Attribute::kUndefined},
+    {"loqion", Attribute::kUndefined},
+    {"mustusNN", Attribute::kUndefined},
+    {"usvvuse", Attribute::kUndefined},
+    {"mut_QQse", Attribute::kUndefined},
+    {"srf", Attribute::kUndefined},
+    {"sije", Attribute::kUndefined},
+    {"NNz2w", Attribute::kUndefined},
+    {"vrtex", Attribute::kUndefined},
+    {"rrertex", Attribute::kUndefined},
+    {"vGrtex", Attribute::kUndefined},
+    {"workgroup_sizFF", Attribute::kUndefined},
+    {"wErkrp_size", Attribute::kUndefined},
+    {"worgrroup_size", Attribute::kUndefined},
 };
 
 using AttributeParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/lang/core/core.def b/src/tint/lang/core/core.def
index 4e8ced4..2337b79 100644
--- a/src/tint/lang/core/core.def
+++ b/src/tint/lang/core/core.def
@@ -212,6 +212,9 @@
   size
   vertex
   workgroup_size
+
+  // framebuffer-fetch input
+  color
 }
 
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/src/tint/lang/core/ir/transform/shader_io.cc b/src/tint/lang/core/ir/transform/shader_io.cc
index 368dcf0..6e5f8a1 100644
--- a/src/tint/lang/core/ir/transform/shader_io.cc
+++ b/src/tint/lang/core/ir/transform/shader_io.cc
@@ -120,10 +120,11 @@
             vertex_point_size_index =
                 backend->AddOutput(ir.symbols.New("vertex_point_size"), ty.f32(),
                                    core::type::StructMemberAttributes{
-                                       /* location */ {},
-                                       /* index */ {},
+                                       /* location */ std::nullopt,
+                                       /* index */ std::nullopt,
+                                       /* color */ std::nullopt,
                                        /* builtin */ core::BuiltinValue::kPointSize,
-                                       /* interpolation */ {},
+                                       /* interpolation */ std::nullopt,
                                        /* 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..3546b1e 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
@@ -1492,10 +1492,11 @@
                                         mod.symbols.New("global_id"),
                                         ty.vec3<u32>(),
                                         core::type::StructMemberAttributes{
-                                            /* location */ {},
-                                            /* index */ {},
+                                            /* location */ std::nullopt,
+                                            /* index */ std::nullopt,
+                                            /* color */ std::nullopt,
                                             /* builtin */ core::BuiltinValue::kGlobalInvocationId,
-                                            /* interpolation */ {},
+                                            /* interpolation */ std::nullopt,
                                             /* invariant */ false,
                                         },
                                     },
@@ -1503,10 +1504,11 @@
                                         mod.symbols.New("index"),
                                         ty.u32(),
                                         core::type::StructMemberAttributes{
-                                            /* location */ {},
-                                            /* index */ {},
+                                            /* location */ std::nullopt,
+                                            /* index */ std::nullopt,
+                                            /* color */ std::nullopt,
                                             /* builtin */ core::BuiltinValue::kLocalInvocationIndex,
-                                            /* interpolation */ {},
+                                            /* interpolation */ std::nullopt,
                                             /* 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..3b09a66 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
@@ -208,6 +208,15 @@
         manager.Add<ast::transform::ZeroInitWorkgroupMemory>();
     }
 
+    {
+        PixelLocal::Config cfg;
+        for (auto it : options.pixel_local_options.attachments) {
+            cfg.attachments.Add(it.first, it.second);
+        }
+        data.Add<PixelLocal::Config>(cfg);
+        manager.Add<PixelLocal>();
+    }
+
     // CanonicalizeEntryPointIO must come after Robustness
     manager.Add<ast::transform::CanonicalizeEntryPointIO>();
     data.Add<ast::transform::CanonicalizeEntryPointIO::Config>(std::move(entry_point_io_cfg));
@@ -225,15 +234,6 @@
     // SubgroupBallot() must come after CanonicalizeEntryPointIO.
     manager.Add<SubgroupBallot>();
 
-    {
-        PixelLocal::Config cfg;
-        for (auto it : options.pixel_local_options.attachments) {
-            cfg.attachments.Add(it.first, it.second);
-        }
-        data.Add<PixelLocal::Config>(cfg);
-        manager.Add<PixelLocal>();
-    }
-
     // ArrayLengthFromUniform must come after SimplifyPointers, as
     // it assumes that the form of the array length argument is &var.array.
     manager.Add<ast::transform::ArrayLengthFromUniform>();
@@ -275,6 +275,7 @@
                 wgsl::Extension::kChromiumExperimentalPixelLocal,
                 wgsl::Extension::kChromiumExperimentalReadWriteStorageTexture,
                 wgsl::Extension::kChromiumExperimentalSubgroups,
+                wgsl::Extension::kChromiumExperimentalFramebufferFetch,
                 wgsl::Extension::kChromiumInternalDualSourceBlending,
                 wgsl::Extension::kChromiumInternalRelaxedUniformLayout,
                 wgsl::Extension::kF16,
@@ -1969,7 +1970,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)";
@@ -2005,20 +2006,8 @@
 
             bool ok = Switch(
                 type,  //
-                [&](const core::type::Struct* str) {
-                    bool is_pixel_local = false;
-                    if (auto* sem_str = str->As<sem::Struct>()) {
-                        for (auto* member : sem_str->Members()) {
-                            if (ast::HasAttribute<PixelLocal::Attachment>(
-                                    member->Declaration()->attributes)) {
-                                is_pixel_local = true;
-                                break;
-                            }
-                        }
-                    }
-                    if (!is_pixel_local) {
-                        out << " [[stage_in]]";
-                    }
+                [&](const core::type::Struct*) {
+                    out << " [[stage_in]]";
                     return true;
                 },
                 [&](const core::type::Texture*) {
@@ -2838,6 +2827,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()) {
@@ -2852,13 +2845,6 @@
             out << " " << invariant_define_name_;
         }
 
-        if (auto* sem_mem = mem->As<sem::StructMember>()) {
-            if (auto* attachment =
-                    ast::GetAttribute<PixelLocal::Attachment>(sem_mem->Declaration()->attributes)) {
-                out << " [[color(" << attachment->index << ")]]";
-            }
-        }
-
         out << ";";
 
         if (is_host_shareable) {
diff --git a/src/tint/lang/msl/writer/ast_raise/pixel_local.cc b/src/tint/lang/msl/writer/ast_raise/pixel_local.cc
index 55d110b..1d6fb0a 100644
--- a/src/tint/lang/msl/writer/ast_raise/pixel_local.cc
+++ b/src/tint/lang/msl/writer/ast_raise/pixel_local.cc
@@ -39,7 +39,6 @@
 #include "src/tint/utils/containers/transform.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::msl::writer::PixelLocal);
-TINT_INSTANTIATE_TYPEINFO(tint::msl::writer::PixelLocal::Attachment);
 TINT_INSTANTIATE_TYPEINFO(tint::msl::writer::PixelLocal::Config);
 
 using namespace tint::core::number_suffixes;  // NOLINT
@@ -110,10 +109,10 @@
                     // Obtain struct of the pixel local.
                     auto* pixel_local_str = global->Type()->UnwrapRef()->As<sem::Struct>();
 
-                    // Add an attachment decoration to each member of the pixel_local structure.
+                    // Add an Color attribute to each member of the pixel_local structure.
                     for (auto* member : pixel_local_str->Members()) {
                         ctx.InsertBack(member->Declaration()->attributes,
-                                       Attachment(AttachmentIndex(member->Index())));
+                                       b.Color(u32(AttachmentIndex(member->Index()))));
                         ctx.InsertBack(member->Declaration()->attributes,
                                        b.Disable(ast::DisabledValidation::kEntryPointParameter));
                     }
@@ -130,6 +129,19 @@
             return SkipTransform;
         }
 
+        // At this point, the `var<pixel_local>` will have been replaced with `var<private>`, and
+        // the entry point will use `@color`, which requires the framebuffer fetch extension.
+        // Replace the `chromium_experimental_pixel_local` enable with
+        // `chromium_experimental_framebuffer_fetch`.
+        for (auto* enable : src.AST().Enables()) {
+            for (auto* ext : enable->extensions) {
+                if (ext->name == wgsl::Extension::kChromiumExperimentalPixelLocal) {
+                    ctx.Replace(ext, b.create<ast::Extension>(
+                                         wgsl::Extension::kChromiumExperimentalFramebufferFetch));
+                }
+            }
+        }
+
         ctx.Clone();
         return resolver::Resolve(b);
     }
@@ -240,12 +252,6 @@
                Vector{b.Stage(ast::PipelineStage::kFragment)});
     }
 
-    /// @returns a new Attachment attribute
-    /// @param index the index of the attachment
-    PixelLocal::Attachment* Attachment(uint32_t index) {
-        return b.ASTNodes().Create<PixelLocal::Attachment>(b.ID(), b.AllocateNodeID(), index);
-    }
-
     /// @returns the attachment index for the pixel local field with the given index
     /// @param field_index the pixel local field index
     uint32_t AttachmentIndex(uint32_t field_index) {
@@ -284,17 +290,4 @@
 
 PixelLocal::Config::~Config() = default;
 
-PixelLocal::Attachment::Attachment(GenerationID pid, ast::NodeID nid, uint32_t idx)
-    : Base(pid, nid, Empty), index(idx) {}
-
-PixelLocal::Attachment::~Attachment() = default;
-
-std::string PixelLocal::Attachment::InternalName() const {
-    return "attachment(" + std::to_string(index) + ")";
-}
-
-const PixelLocal::Attachment* PixelLocal::Attachment::Clone(ast::CloneContext& ctx) const {
-    return ctx.dst->ASTNodes().Create<Attachment>(ctx.dst->ID(), ctx.dst->AllocateNodeID(), index);
-}
-
 }  // namespace tint::msl::writer
diff --git a/src/tint/lang/msl/writer/ast_raise/pixel_local.h b/src/tint/lang/msl/writer/ast_raise/pixel_local.h
index 4808a33..4459e59 100644
--- a/src/tint/lang/msl/writer/ast_raise/pixel_local.h
+++ b/src/tint/lang/msl/writer/ast_raise/pixel_local.h
@@ -39,6 +39,9 @@
 /// PixelLocal transforms module-scope `var<pixel_local>`s and fragment entry point functions that
 /// use them:
 /// * `var<pixel_local>` will be transformed to `var<private>`.
+/// * All of the members of the pixel local struct will have an additional `@color` attribute added.
+/// * The chromium_experimental_pixel_local extension enable will be replaced with an enable for
+///   chromium_experimental_framebuffer_fetch.
 /// * The entry point function will be wrapped with another function ('outer') that calls the
 ///  'inner' function.
 /// * The outer function will have an additional parameter of the pixel local struct type, which is
@@ -63,32 +66,6 @@
         Hashmap<uint32_t, uint32_t, 8> attachments;
     };
 
-    /// Intrinsic is an InternalAttribute that's used to decorate a pixel local attachment
-    /// parameter, return value or structure member.
-    class Attachment final : public Castable<Attachment, ast::InternalAttribute> {
-      public:
-        /// Constructor
-        /// @param pid the identifier of the program that owns this node
-        /// @param nid the unique node identifier
-        /// @param idx the attachment index
-        Attachment(GenerationID pid, ast::NodeID nid, uint32_t idx);
-
-        /// Destructor
-        ~Attachment() override;
-
-        /// @return a short description of the internal attribute which will be
-        /// displayed as `@internal(<name>)`
-        std::string InternalName() const override;
-
-        /// Performs a deep clone of this object using the program::CloneContext `ctx`.
-        /// @param ctx the clone context
-        /// @return the newly cloned object
-        const Attachment* Clone(ast::CloneContext& ctx) const override;
-
-        /// The attachment index
-        const uint32_t index;
-    };
-
     /// Constructor
     PixelLocal();
 
diff --git a/src/tint/lang/msl/writer/ast_raise/pixel_local_test.cc b/src/tint/lang/msl/writer/ast_raise/pixel_local_test.cc
index 1d2c51b..fba0af3 100644
--- a/src/tint/lang/msl/writer/ast_raise/pixel_local_test.cc
+++ b/src/tint/lang/msl/writer/ast_raise/pixel_local_test.cc
@@ -70,7 +70,7 @@
 
     auto* expect =
         R"(
-enable chromium_experimental_pixel_local;
+enable chromium_experimental_framebuffer_fetch;
 
 struct PixelLocal {
   a : i32,
@@ -102,7 +102,7 @@
 
     auto* expect =
         R"(
-enable chromium_experimental_pixel_local;
+enable chromium_experimental_framebuffer_fetch;
 
 struct F_res {
   @location(1)
@@ -117,7 +117,7 @@
 }
 
 struct PixelLocal {
-  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  @color(1u) @internal(disable_validation__entry_point_parameter)
   a : u32,
 }
 
@@ -155,7 +155,7 @@
 
     auto* expect =
         R"(
-enable chromium_experimental_pixel_local;
+enable chromium_experimental_framebuffer_fetch;
 
 struct F_res {
   @location(1)
@@ -170,7 +170,7 @@
 }
 
 struct PixelLocal {
-  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  @color(1u) @internal(disable_validation__entry_point_parameter)
   a : u32,
 }
 
@@ -211,7 +211,7 @@
 
     auto* expect =
         R"(
-enable chromium_experimental_pixel_local;
+enable chromium_experimental_framebuffer_fetch;
 
 struct F_res {
   @location(1)
@@ -230,11 +230,11 @@
 }
 
 struct PixelLocal {
-  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  @color(1u) @internal(disable_validation__entry_point_parameter)
   a : u32,
-  @internal(attachment(0)) @internal(disable_validation__entry_point_parameter)
+  @color(0u) @internal(disable_validation__entry_point_parameter)
   b : i32,
-  @internal(attachment(10)) @internal(disable_validation__entry_point_parameter)
+  @color(10u) @internal(disable_validation__entry_point_parameter)
   c : f32,
 }
 
@@ -269,7 +269,7 @@
 
     auto* expect =
         R"(
-enable chromium_experimental_pixel_local;
+enable chromium_experimental_framebuffer_fetch;
 
 struct F_res {
   @location(1)
@@ -284,7 +284,7 @@
 }
 
 struct PixelLocal {
-  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  @color(1u) @internal(disable_validation__entry_point_parameter)
   a : u32,
 }
 
@@ -318,7 +318,7 @@
 
     auto* expect =
         R"(
-enable chromium_experimental_pixel_local;
+enable chromium_experimental_framebuffer_fetch;
 
 struct F_res {
   @location(1)
@@ -333,7 +333,7 @@
 }
 
 struct PixelLocal {
-  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  @color(1u) @internal(disable_validation__entry_point_parameter)
   a : u32,
 }
 
@@ -371,7 +371,7 @@
 
     auto* expect =
         R"(
-enable chromium_experimental_pixel_local;
+enable chromium_experimental_framebuffer_fetch;
 
 struct F_res {
   @location(1)
@@ -386,7 +386,7 @@
 }
 
 struct PixelLocal {
-  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  @color(1u) @internal(disable_validation__entry_point_parameter)
   a : u32,
 }
 
@@ -429,7 +429,7 @@
 
     auto* expect =
         R"(
-enable chromium_experimental_pixel_local;
+enable chromium_experimental_framebuffer_fetch;
 
 struct F_res {
   @location(1)
@@ -444,7 +444,7 @@
 }
 
 struct PixelLocal {
-  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  @color(1u) @internal(disable_validation__entry_point_parameter)
   a : u32,
 }
 
@@ -483,7 +483,7 @@
 
     auto* expect =
         R"(
-enable chromium_experimental_pixel_local;
+enable chromium_experimental_framebuffer_fetch;
 
 struct F_res {
   @location(1)
@@ -498,7 +498,7 @@
 }
 
 struct PixelLocal {
-  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  @color(1u) @internal(disable_validation__entry_point_parameter)
   a : u32,
 }
 
@@ -537,7 +537,7 @@
 
     auto* expect =
         R"(
-enable chromium_experimental_pixel_local;
+enable chromium_experimental_framebuffer_fetch;
 
 struct F_res {
   @location(1)
@@ -552,7 +552,7 @@
 }
 
 struct PixelLocal {
-  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  @color(1u) @internal(disable_validation__entry_point_parameter)
   a : u32,
 }
 
@@ -593,7 +593,7 @@
 
     auto* expect =
         R"(
-enable chromium_experimental_pixel_local;
+enable chromium_experimental_framebuffer_fetch;
 
 struct F_res {
   @location(1)
@@ -608,7 +608,7 @@
 }
 
 struct PixelLocal {
-  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  @color(1u) @internal(disable_validation__entry_point_parameter)
   a : u32,
 }
 
@@ -647,7 +647,7 @@
 
     auto* expect =
         R"(
-enable chromium_experimental_pixel_local;
+enable chromium_experimental_framebuffer_fetch;
 
 struct F_res {
   @location(1)
@@ -662,7 +662,7 @@
 }
 
 struct PixelLocal {
-  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  @color(1u) @internal(disable_validation__entry_point_parameter)
   a : u32,
 }
 
@@ -704,7 +704,7 @@
 
     auto* expect =
         R"(
-enable chromium_experimental_pixel_local;
+enable chromium_experimental_framebuffer_fetch;
 
 struct F_res {
   @location(1)
@@ -721,7 +721,7 @@
 }
 
 struct PixelLocal {
-  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  @color(1u) @internal(disable_validation__entry_point_parameter)
   a : u32,
 }
 
@@ -763,7 +763,7 @@
 
     auto* expect =
         R"(
-enable chromium_experimental_pixel_local;
+enable chromium_experimental_framebuffer_fetch;
 
 struct F_res {
   @location(1)
@@ -784,9 +784,9 @@
 }
 
 struct PixelLocal {
-  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  @color(1u) @internal(disable_validation__entry_point_parameter)
   a : u32,
-  @internal(attachment(5)) @internal(disable_validation__entry_point_parameter)
+  @color(5u) @internal(disable_validation__entry_point_parameter)
   b : u32,
 }
 
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.cc b/src/tint/lang/spirv/writer/raise/shader_io.cc
index 3de0a6b..c44fdee 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io.cc
+++ b/src/tint/lang/spirv/writer/raise/shader_io.cc
@@ -43,6 +43,12 @@
 
 namespace {
 
+/// State that persists across the whole module and can be shared between entry points.
+struct PerModuleState {
+    /// The frag_depth clamp arguments.
+    core::ir::Value* frag_depth_clamp_args = nullptr;
+};
+
 /// PIMPL state for the parts of the shader IO transform specific to SPIR-V.
 /// For SPIR-V, we declare a global variable for each input and output. The wrapper entry point then
 /// loads from and stores to these variables. We also modify the type of the SampleMask builtin to
@@ -56,12 +62,15 @@
     /// The configuration options.
     const ShaderIOConfig& config;
 
-    /// The frag_depth clamp arguments.
-    core::ir::Value* frag_depth_clamp_args = nullptr;
+    /// The per-module state object.
+    PerModuleState& module_state;
 
     /// Constructor
-    StateImpl(core::ir::Module& mod, core::ir::Function* f, const ShaderIOConfig& cfg)
-        : ShaderIOBackendState(mod, f), config(cfg) {}
+    StateImpl(core::ir::Module& mod,
+              core::ir::Function* f,
+              const ShaderIOConfig& cfg,
+              PerModuleState& mod_state)
+        : ShaderIOBackendState(mod, f), config(cfg), module_state(mod_state) {}
 
     /// Destructor
     ~StateImpl() override {}
@@ -94,7 +103,8 @@
                     io.type->is_integer_scalar_or_vector()) {
                     io.attributes.interpolation = {core::InterpolationType::kFlat};
                 }
-            } else {
+            }
+            if (io.attributes.location) {
                 name << "_loc" << io.attributes.location.value();
                 if (io.attributes.index.has_value()) {
                     name << "_idx" << io.attributes.index.value();
@@ -172,7 +182,7 @@
         }
 
         // Create the clamp args struct and variable.
-        if (!frag_depth_clamp_args) {
+        if (!module_state.frag_depth_clamp_args) {
             // Check that there are no push constants in the module already.
             for (auto* inst : *ir.root_block) {
                 if (auto* var = inst->As<core::ir::Var>()) {
@@ -194,11 +204,11 @@
             // Declare the variable.
             auto* var = b.Var("tint_frag_depth_clamp_args", ty.ptr(push_constant, str));
             ir.root_block->Append(var);
-            frag_depth_clamp_args = var->Result();
+            module_state.frag_depth_clamp_args = var->Result();
         }
 
         // Clamp the value.
-        auto* args = builder.Load(frag_depth_clamp_args);
+        auto* args = builder.Load(module_state.frag_depth_clamp_args);
         auto* frag_depth_min = builder.Access(ty.f32(), args, 0_u);
         auto* frag_depth_max = builder.Access(ty.f32(), args, 1_u);
         return builder
@@ -217,8 +227,9 @@
         return result;
     }
 
+    PerModuleState module_state;
     core::ir::transform::RunShaderIOBase(ir, [&](core::ir::Module& mod, core::ir::Function* func) {
-        return std::make_unique<StateImpl>(mod, func, config);
+        return std::make_unique<StateImpl>(mod, func, config, module_state);
     });
 
     return Success;
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..bfea71b 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,
@@ -1266,6 +1286,163 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(SpirvWriter_ShaderIOTest, ClampFragDepth_MultipleFragmentShaders) {
+    auto* str_ty = ty.Struct(mod.symbols.New("Outputs"),
+                             {
+                                 {
+                                     mod.symbols.New("color"),
+                                     ty.f32(),
+                                     core::type::StructMemberAttributes{
+                                         /* location */ 0u,
+                                         /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
+                                         /* builtin */ std::nullopt,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ false,
+                                     },
+                                 },
+                                 {
+                                     mod.symbols.New("depth"),
+                                     ty.f32(),
+                                     core::type::StructMemberAttributes{
+                                         /* location */ std::nullopt,
+                                         /* index */ std::nullopt,
+                                         /* color */ std::nullopt,
+                                         /* builtin */ core::BuiltinValue::kFragDepth,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ false,
+                                     },
+                                 },
+                             });
+
+    auto make_entry_point = [&](std::string_view name) {
+        auto* ep = b.Function(name, str_ty);
+        ep->SetStage(core::ir::Function::PipelineStage::kFragment);
+        b.Append(ep->Block(), [&] {  //
+            b.Return(ep, b.Construct(str_ty, 0.5_f, 2_f));
+        });
+    };
+    make_entry_point("ep1");
+    make_entry_point("ep2");
+    make_entry_point("ep3");
+
+    auto* src = R"(
+Outputs = struct @align(4) {
+  color:f32 @offset(0), @location(0)
+  depth:f32 @offset(4), @builtin(frag_depth)
+}
+
+%ep1 = @fragment func():Outputs -> %b1 {
+  %b1 = block {
+    %2:Outputs = construct 0.5f, 2.0f
+    ret %2
+  }
+}
+%ep2 = @fragment func():Outputs -> %b2 {
+  %b2 = block {
+    %4:Outputs = construct 0.5f, 2.0f
+    ret %4
+  }
+}
+%ep3 = @fragment func():Outputs -> %b3 {
+  %b3 = block {
+    %6:Outputs = construct 0.5f, 2.0f
+    ret %6
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+Outputs = struct @align(4) {
+  color:f32 @offset(0)
+  depth:f32 @offset(4)
+}
+
+FragDepthClampArgs = struct @align(4), @block {
+  min:f32 @offset(0)
+  max:f32 @offset(4)
+}
+
+%b1 = block {  # root
+  %ep1_loc0_Output:ptr<__out, f32, write> = var @location(0)
+  %ep1_frag_depth_Output:ptr<__out, f32, write> = var @builtin(frag_depth)
+  %tint_frag_depth_clamp_args:ptr<push_constant, FragDepthClampArgs, read_write> = var
+  %ep2_loc0_Output:ptr<__out, f32, write> = var @location(0)
+  %ep2_frag_depth_Output:ptr<__out, f32, write> = var @builtin(frag_depth)
+  %ep3_loc0_Output:ptr<__out, f32, write> = var @location(0)
+  %ep3_frag_depth_Output:ptr<__out, f32, write> = var @builtin(frag_depth)
+}
+
+%ep1_inner = func():Outputs -> %b2 {
+  %b2 = block {
+    %9:Outputs = construct 0.5f, 2.0f
+    ret %9
+  }
+}
+%ep2_inner = func():Outputs -> %b3 {
+  %b3 = block {
+    %11:Outputs = construct 0.5f, 2.0f
+    ret %11
+  }
+}
+%ep3_inner = func():Outputs -> %b4 {
+  %b4 = block {
+    %13:Outputs = construct 0.5f, 2.0f
+    ret %13
+  }
+}
+%ep1 = @fragment func():void -> %b5 {
+  %b5 = block {
+    %15:Outputs = call %ep1_inner
+    %16:f32 = access %15, 0u
+    store %ep1_loc0_Output, %16
+    %17:f32 = access %15, 1u
+    %18:FragDepthClampArgs = load %tint_frag_depth_clamp_args
+    %19:f32 = access %18, 0u
+    %20:f32 = access %18, 1u
+    %21:f32 = clamp %17, %19, %20
+    store %ep1_frag_depth_Output, %21
+    ret
+  }
+}
+%ep2 = @fragment func():void -> %b6 {
+  %b6 = block {
+    %23:Outputs = call %ep2_inner
+    %24:f32 = access %23, 0u
+    store %ep2_loc0_Output, %24
+    %25:f32 = access %23, 1u
+    %26:FragDepthClampArgs = load %tint_frag_depth_clamp_args
+    %27:f32 = access %26, 0u
+    %28:f32 = access %26, 1u
+    %29:f32 = clamp %25, %27, %28
+    store %ep2_frag_depth_Output, %29
+    ret
+  }
+}
+%ep3 = @fragment func():void -> %b7 {
+  %b7 = block {
+    %31:Outputs = call %ep3_inner
+    %32:f32 = access %31, 0u
+    store %ep3_loc0_Output, %32
+    %33:f32 = access %31, 1u
+    %34:FragDepthClampArgs = load %tint_frag_depth_clamp_args
+    %35:f32 = access %34, 0u
+    %36:f32 = access %34, 1u
+    %37:f32 = clamp %33, %35, %36
+    store %ep3_frag_depth_Output, %37
+    ret
+  }
+}
+)";
+
+    ShaderIOConfig config;
+    config.clamp_frag_depth = true;
+    Run(ShaderIO, config);
+
+    EXPECT_EQ(expect, str());
+}
+
 TEST_F(SpirvWriter_ShaderIOTest, EmitVertexPointSize) {
     auto* ep = b.Function("foo", ty.vec4<f32>());
     ep->SetStage(core::ir::Function::PipelineStage::kVertex);
diff --git a/src/tint/lang/wgsl/ast/BUILD.bazel b/src/tint/lang/wgsl/ast/BUILD.bazel
index 31601b6..f7514c9 100644
--- a/src/tint/lang/wgsl/ast/BUILD.bazel
+++ b/src/tint/lang/wgsl/ast/BUILD.bazel
@@ -57,6 +57,7 @@
     "case_selector.cc",
     "case_statement.cc",
     "clone_context.cc",
+    "color_attribute.cc",
     "compound_assignment_statement.cc",
     "const.cc",
     "const_assert.cc",
@@ -137,6 +138,7 @@
     "case_selector.h",
     "case_statement.h",
     "clone_context.h",
+    "color_attribute.h",
     "compound_assignment_statement.h",
     "const.h",
     "const_assert.h",
@@ -244,6 +246,7 @@
     "case_selector_test.cc",
     "case_statement_test.cc",
     "clone_context_test.cc",
+    "color_attribute_test.cc",
     "compound_assignment_statement_test.cc",
     "const_assert_test.cc",
     "continue_statement_test.cc",
diff --git a/src/tint/lang/wgsl/ast/BUILD.cmake b/src/tint/lang/wgsl/ast/BUILD.cmake
index ebad7e9..fe4eef9 100644
--- a/src/tint/lang/wgsl/ast/BUILD.cmake
+++ b/src/tint/lang/wgsl/ast/BUILD.cmake
@@ -77,6 +77,8 @@
   lang/wgsl/ast/case_statement.h
   lang/wgsl/ast/clone_context.cc
   lang/wgsl/ast/clone_context.h
+  lang/wgsl/ast/color_attribute.cc
+  lang/wgsl/ast/color_attribute.h
   lang/wgsl/ast/compound_assignment_statement.cc
   lang/wgsl/ast/compound_assignment_statement.h
   lang/wgsl/ast/const.cc
@@ -244,6 +246,7 @@
   lang/wgsl/ast/case_selector_test.cc
   lang/wgsl/ast/case_statement_test.cc
   lang/wgsl/ast/clone_context_test.cc
+  lang/wgsl/ast/color_attribute_test.cc
   lang/wgsl/ast/compound_assignment_statement_test.cc
   lang/wgsl/ast/const_assert_test.cc
   lang/wgsl/ast/continue_statement_test.cc
diff --git a/src/tint/lang/wgsl/ast/BUILD.gn b/src/tint/lang/wgsl/ast/BUILD.gn
index 4580e46..6cc263d 100644
--- a/src/tint/lang/wgsl/ast/BUILD.gn
+++ b/src/tint/lang/wgsl/ast/BUILD.gn
@@ -80,6 +80,8 @@
     "case_statement.h",
     "clone_context.cc",
     "clone_context.h",
+    "color_attribute.cc",
+    "color_attribute.h",
     "compound_assignment_statement.cc",
     "compound_assignment_statement.h",
     "const.cc",
@@ -244,6 +246,7 @@
       "case_selector_test.cc",
       "case_statement_test.cc",
       "clone_context_test.cc",
+      "color_attribute_test.cc",
       "compound_assignment_statement_test.cc",
       "const_assert_test.cc",
       "continue_statement_test.cc",
diff --git a/src/tint/lang/wgsl/ast/builder.h b/src/tint/lang/wgsl/ast/builder.h
index 1cbdfba..1b80ec9 100644
--- a/src/tint/lang/wgsl/ast/builder.h
+++ b/src/tint/lang/wgsl/ast/builder.h
@@ -67,6 +67,7 @@
 #include "src/tint/lang/wgsl/ast/call_expression.h"
 #include "src/tint/lang/wgsl/ast/call_statement.h"
 #include "src/tint/lang/wgsl/ast/case_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/const_assert.h"
@@ -3167,6 +3168,23 @@
         return create<ast::LocationAttribute>(source, Expr(std::forward<EXPR>(location)));
     }
 
+    /// Creates an ast::ColorAttribute
+    /// @param index the index value expression
+    /// @returns the index attribute pointer
+    template <typename EXPR>
+    const ast::ColorAttribute* Color(EXPR&& index) {
+        return create<ast::ColorAttribute>(source_, Expr(std::forward<EXPR>(index)));
+    }
+
+    /// Creates an ast::ColorAttribute
+    /// @param source the source information
+    /// @param index the index value expression
+    /// @returns the index attribute pointer
+    template <typename EXPR>
+    const ast::ColorAttribute* Color(const Source& source, EXPR&& index) {
+        return create<ast::ColorAttribute>(source, Expr(std::forward<EXPR>(index)));
+    }
+
     /// Creates an ast::LocationAttribute
     /// @param location the location value expression
     /// @returns the location attribute pointer
diff --git a/src/tint/lang/wgsl/ast/color_attribute.cc b/src/tint/lang/wgsl/ast/color_attribute.cc
new file mode 100644
index 0000000..daa1597
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/color_attribute.cc
@@ -0,0 +1,60 @@
+// 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/ast/color_attribute.h"
+
+#include <string>
+
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::ColorAttribute);
+
+namespace tint::ast {
+
+ColorAttribute::ColorAttribute(GenerationID pid,
+                               NodeID nid,
+                               const Source& src,
+                               const Expression* exp)
+    : Base(pid, nid, src), expr(exp) {
+    TINT_ASSERT_GENERATION_IDS_EQUAL(exp, generation_id);
+}
+
+ColorAttribute::~ColorAttribute() = default;
+
+std::string ColorAttribute::Name() const {
+    return "color";
+}
+
+const ColorAttribute* ColorAttribute::Clone(CloneContext& ctx) const {
+    // Clone arguments outside of create() call to have deterministic ordering
+    auto src = ctx.Clone(source);
+    auto e = ctx.Clone(expr);
+    return ctx.dst->create<ColorAttribute>(src, e);
+}
+
+}  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/color_attribute.h b/src/tint/lang/wgsl/ast/color_attribute.h
new file mode 100644
index 0000000..d0a21e4
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/color_attribute.h
@@ -0,0 +1,68 @@
+// 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.
+
+#ifndef SRC_TINT_LANG_WGSL_AST_COLOR_ATTRIBUTE_H_
+#define SRC_TINT_LANG_WGSL_AST_COLOR_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/lang/wgsl/ast/attribute.h"
+
+// Forward declarations
+namespace tint::ast {
+class Expression;
+}
+
+namespace tint::ast {
+
+/// A color attribute (enabled with the frame-buffer fetch extension)
+class ColorAttribute final : public Castable<ColorAttribute, Attribute> {
+  public:
+    /// constructor
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
+    /// @param src the source of this node
+    /// @param expr the frame-buffer index value
+    ColorAttribute(GenerationID pid, NodeID nid, const Source& src, const Expression* expr);
+    ~ColorAttribute() override;
+
+    /// @returns the WGSL name for the attribute
+    std::string Name() const override;
+
+    /// Clones this node and all transitive child nodes using the `CloneContext`
+    /// `ctx`.
+    /// @param ctx the clone context
+    /// @return the newly cloned node
+    const ColorAttribute* Clone(CloneContext& ctx) const override;
+
+    /// The index value expression
+    const Expression* const expr;
+};
+
+}  // namespace tint::ast
+
+#endif  // SRC_TINT_LANG_WGSL_AST_COLOR_ATTRIBUTE_H_
diff --git a/src/tint/lang/wgsl/ast/color_attribute_test.cc b/src/tint/lang/wgsl/ast/color_attribute_test.cc
new file mode 100644
index 0000000..9afba5f
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/color_attribute_test.cc
@@ -0,0 +1,66 @@
+// 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 "gtest/gtest-spi.h"
+
+#include "src/tint/lang/wgsl/ast/color_attribute.h"
+#include "src/tint/lang/wgsl/ast/helper_test.h"
+
+using namespace tint::core::number_suffixes;  // NOLINT
+
+namespace tint::ast {
+namespace {
+
+using ColorAttributeTest = TestHelper;
+
+TEST_F(ColorAttributeTest, Creation) {
+    auto* expr = Expr(1_u);
+    auto* c = Color(expr);
+    EXPECT_EQ(c->expr, expr);
+}
+
+TEST_F(ColorAttributeTest, Assert_Null_Builtin) {
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder b;
+            b.Color(nullptr);
+        },
+        "internal compiler error");
+}
+
+TEST_F(ColorAttributeTest, Assert_DifferentGenerationID_Color) {
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder b1;
+            ProgramBuilder b2;
+            b1.Color(b2.Expr(1_u));
+        },
+        "internal compiler error");
+}
+
+}  // namespace
+}  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
index 45c270c..1408dcc 100644
--- a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
+++ b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
@@ -41,6 +41,7 @@
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/function.h"
+#include "src/tint/utils/containers/transform.h"
 
 using namespace tint::core::number_suffixes;  // NOLINT
 using namespace tint::core::fluent_types;     // NOLINT
@@ -64,6 +65,8 @@
     std::optional<uint32_t> location;
     /// The struct member index if provided
     std::optional<uint32_t> index;
+    /// The struct member color if provided
+    std::optional<uint32_t> color;
 };
 
 /// FXC is sensitive to field order in structures, this is used by StructMemberComparator to ensure
@@ -105,7 +108,7 @@
 // Returns true if `attr` is a shader IO attribute.
 bool IsShaderIOAttribute(const Attribute* attr) {
     return attr->IsAnyOf<BuiltinAttribute, InterpolateAttribute, InvariantAttribute,
-                         LocationAttribute, IndexAttribute>();
+                         LocationAttribute, ColorAttribute, IndexAttribute>();
 }
 
 }  // namespace
@@ -284,11 +287,13 @@
     /// @param name the name of the shader input
     /// @param type the type of the shader input
     /// @param location the location if provided
+    /// @param color the color if provided
     /// @param attrs the attributes to apply to the shader input
     /// @returns an expression which evaluates to the value of the shader input
     const Expression* AddInput(std::string name,
                                const core::type::Type* type,
                                std::optional<uint32_t> location,
+                               std::optional<uint32_t> color,
                                tint::Vector<const Attribute*, 8> attrs) {
         auto ast_type = CreateASTTypeFor(ctx, type);
 
@@ -347,8 +352,8 @@
             // Otherwise, move it to the new structure member list.
             Symbol symbol = input_names.emplace(name).second ? b.Symbols().Register(name)
                                                              : b.Symbols().New(name);
-            wrapper_struct_param_members.Push(
-                {b.Member(symbol, ast_type, std::move(attrs)), location, std::nullopt});
+            wrapper_struct_param_members.Push({b.Member(symbol, ast_type, std::move(attrs)),
+                                               location, /* index */ std::nullopt, color});
             return b.MemberAccessor(InputStructSymbol(), symbol);
         }
     }
@@ -428,8 +433,8 @@
         }
 
         auto name = param->Declaration()->name->symbol.Name();
-        auto* input_expr =
-            AddInput(name, param->Type(), param->Attributes().location, std::move(attributes));
+        auto* input_expr = AddInput(name, param->Type(), param->Attributes().location,
+                                    param->Attributes().color, std::move(attributes));
         inner_call_parameters.Push(input_expr);
     }
 
@@ -463,7 +468,7 @@
             auto attributes =
                 CloneShaderIOAttributes(member->Declaration()->attributes, do_interpolate);
             auto* input_expr = AddInput(name, member->Type(), member->Attributes().location,
-                                        std::move(attributes));
+                                        member->Attributes().color, std::move(attributes));
             inner_struct_values.Push(input_expr);
         }
 
@@ -546,44 +551,57 @@
     }
 
     /// Comparison function used to reorder struct members such that all members with
-    /// location attributes appear first (ordered by location slot), followed by
-    /// those with builtin attributes.
+    /// color attributes appear first (ordered by color slot), then location attributes (ordered by
+    /// location slot), followed by those with builtin attributes (ordered by BuiltinOrder).
     /// @param x a struct member
     /// @param y another struct member
     /// @returns true if a comes before b
     bool StructMemberComparator(const MemberInfo& x, const MemberInfo& y) {
-        auto* x_loc = GetAttribute<LocationAttribute>(x.member->attributes);
-        auto* y_loc = GetAttribute<LocationAttribute>(y.member->attributes);
-        auto* x_blt = GetAttribute<BuiltinAttribute>(x.member->attributes);
-        auto* y_blt = GetAttribute<BuiltinAttribute>(y.member->attributes);
-        if (x_loc) {
-            if (!y_loc) {
-                // `a` has location attribute and `b` does not: `a` goes first.
-                return true;
-            }
+        if (x.color.has_value() && y.color.has_value()) {
+            // Both have color attributes: smallest goes first.
+            return x.color < y.color;
+        }
+        if (x.color.has_value() != y.color.has_value()) {
+            // The member with the color goes first
+            return x.color.has_value();
+        }
+
+        if (x.location.has_value() && y.location.has_value()) {
             // Both have location attributes: smallest goes first.
             return x.location < y.location;
-        } else {
-            if (y_loc) {
-                // `b` has location attribute and `a` does not: `b` goes first.
-                return false;
-            }
-            // Both are builtins: order matters for FXC.
-            auto builtin_a = BuiltinOf(x_blt);
-            auto builtin_b = BuiltinOf(y_blt);
-            return BuiltinOrder(builtin_a) < BuiltinOrder(builtin_b);
         }
+        if (x.location.has_value() != y.location.has_value()) {
+            // The member with the location goes first
+            return x.location.has_value();
+        }
+
+        {
+            auto* x_blt = GetAttribute<BuiltinAttribute>(x.member->attributes);
+            auto* y_blt = GetAttribute<BuiltinAttribute>(y.member->attributes);
+            if (x_blt && y_blt) {
+                // Both are builtins: order matters for FXC.
+                auto builtin_a = BuiltinOf(x_blt);
+                auto builtin_b = BuiltinOf(y_blt);
+                return BuiltinOrder(builtin_a) < BuiltinOrder(builtin_b);
+            }
+            if ((x_blt != nullptr) != (y_blt != nullptr)) {
+                // The member with the builtin goes first
+                return x_blt != nullptr;
+            }
+        }
+
+        TINT_UNREACHABLE();
+        return false;
     }
+
     /// Create the wrapper function's struct parameter and type objects.
     void CreateInputStruct() {
         // Sort the struct members to satisfy HLSL interfacing matching rules.
         std::sort(wrapper_struct_param_members.begin(), wrapper_struct_param_members.end(),
                   [&](auto& x, auto& y) { return StructMemberComparator(x, y); });
 
-        tint::Vector<const StructMember*, 8> members;
-        for (auto& mem : wrapper_struct_param_members) {
-            members.Push(mem.member);
-        }
+        auto members =
+            tint::Transform(wrapper_struct_param_members, [](auto& info) { return info.member; });
 
         // Create the new struct type.
         auto struct_name = b.Sym();
@@ -614,9 +632,12 @@
             }
             member_names.insert(name.Name());
 
-            wrapper_struct_output_members.Push(
-                {b.Member(name, outval.type, std::move(outval.attributes)), outval.location,
-                 std::nullopt});
+            wrapper_struct_output_members.Push({
+                /* member */ b.Member(name, outval.type, std::move(outval.attributes)),
+                /* location */ outval.location,
+                /* color */ std::nullopt,
+                /* index */ std::nullopt,
+            });
             assignments.Push(b.Assign(b.MemberAccessor(wrapper_result, name), outval.value));
         }
 
diff --git a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io_test.cc b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io_test.cc
index dd75db3..f30d07f 100644
--- a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io_test.cc
@@ -70,28 +70,35 @@
 
 TEST_F(CanonicalizeEntryPointIOTest, Parameters_Spirv) {
     auto* src = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 @fragment
 fn frag_main(@location(1) loc1 : f32,
              @location(2) @interpolate(flat) loc2 : vec4<u32>,
-             @builtin(position) coord : vec4<f32>) {
-  var col : f32 = (coord.x * loc1);
+             @builtin(position) coord : vec4<f32>,
+             @color(3) color : vec4<f32>) {
+  var col : f32 = (coord.x * loc1) + color.g;
 }
 )";
 
     auto* expect = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 @location(1) @internal(disable_validation__ignore_address_space) var<__in> loc1_1 : f32;
 
 @location(2) @interpolate(flat) @internal(disable_validation__ignore_address_space) var<__in> loc2_1 : vec4<u32>;
 
 @builtin(position) @internal(disable_validation__ignore_address_space) var<__in> coord_1 : vec4<f32>;
 
-fn frag_main_inner(loc1 : f32, loc2 : vec4<u32>, coord : vec4<f32>) {
-  var col : f32 = (coord.x * loc1);
+@color(3) @internal(disable_validation__ignore_address_space) var<__in> color_1 : vec4<f32>;
+
+fn frag_main_inner(loc1 : f32, loc2 : vec4<u32>, coord : vec4<f32>, color : vec4<f32>) {
+  var col : f32 = ((coord.x * loc1) + color.g);
 }
 
 @fragment
 fn frag_main() {
-  frag_main_inner(loc1_1, loc2_1, coord_1);
+  frag_main_inner(loc1_1, loc2_1, coord_1, color_1);
 }
 )";
 
@@ -104,29 +111,36 @@
 
 TEST_F(CanonicalizeEntryPointIOTest, Parameters_Msl) {
     auto* src = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 @fragment
 fn frag_main(@location(1) loc1 : f32,
              @location(2) @interpolate(flat) loc2 : vec4<u32>,
-             @builtin(position) coord : vec4<f32>) {
-  var col : f32 = (coord.x * loc1);
+             @builtin(position) coord : vec4<f32>,
+             @color(3) color : vec4<f32>) {
+  var col : f32 = (coord.x * loc1) + color.g;
 }
 )";
 
     auto* expect = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 struct tint_symbol_1 {
+  @color(3)
+  color : vec4<f32>,
   @location(1)
   loc1 : f32,
   @location(2) @interpolate(flat)
   loc2 : vec4<u32>,
 }
 
-fn frag_main_inner(loc1 : f32, loc2 : vec4<u32>, coord : vec4<f32>) {
-  var col : f32 = (coord.x * loc1);
+fn frag_main_inner(loc1 : f32, loc2 : vec4<u32>, coord : vec4<f32>, color : vec4<f32>) {
+  var col : f32 = ((coord.x * loc1) + color.g);
 }
 
 @fragment
 fn frag_main(@builtin(position) coord : vec4<f32>, tint_symbol : tint_symbol_1) {
-  frag_main_inner(tint_symbol.loc1, tint_symbol.loc2, coord);
+  frag_main_inner(tint_symbol.loc1, tint_symbol.loc2, coord, tint_symbol.color);
 }
 )";
 
@@ -139,16 +153,23 @@
 
 TEST_F(CanonicalizeEntryPointIOTest, Parameters_Hlsl) {
     auto* src = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 @fragment
 fn frag_main(@location(1) loc1 : f32,
              @location(2) @interpolate(flat) loc2 : vec4<u32>,
-             @builtin(position) coord : vec4<f32>) {
-  var col : f32 = (coord.x * loc1);
+             @builtin(position) coord : vec4<f32>,
+             @color(3) color : vec4<f32>) {
+  var col : f32 = (coord.x * loc1) + color.g;
 }
 )";
 
     auto* expect = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 struct tint_symbol_1 {
+  @color(3)
+  color : vec4<f32>,
   @location(1)
   loc1 : f32,
   @location(2) @interpolate(flat)
@@ -157,13 +178,13 @@
   coord : vec4<f32>,
 }
 
-fn frag_main_inner(loc1 : f32, loc2 : vec4<u32>, coord : vec4<f32>) {
-  var col : f32 = (coord.x * loc1);
+fn frag_main_inner(loc1 : f32, loc2 : vec4<u32>, coord : vec4<f32>, color : vec4<f32>) {
+  var col : f32 = ((coord.x * loc1) + color.g);
 }
 
 @fragment
 fn frag_main(tint_symbol : tint_symbol_1) {
-  frag_main_inner(tint_symbol.loc1, tint_symbol.loc2, tint_symbol.coord);
+  frag_main_inner(tint_symbol.loc1, tint_symbol.loc2, tint_symbol.coord, tint_symbol.color);
 }
 )";
 
@@ -246,6 +267,8 @@
 
 TEST_F(CanonicalizeEntryPointIOTest, StructParameters_Spirv) {
     auto* src = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 struct FragBuiltins {
   @builtin(position) coord : vec4<f32>,
 };
@@ -253,16 +276,24 @@
   @location(1) loc1 : f32,
   @location(2) @interpolate(flat) loc2 : vec4<u32>,
 };
+struct FragColors {
+  @color(3) col3 : vec4<f32>,
+  @color(1) col1 : vec4<u32>,
+  @color(2) col2 : vec4<i32>,
+};
 
 @fragment
 fn frag_main(@location(0) loc0 : f32,
              locations : FragLocations,
-             builtins : FragBuiltins) {
-  var col : f32 = ((builtins.coord.x * locations.loc1) + loc0);
+             builtins : FragBuiltins,
+             colors : FragColors) {
+  var col : f32 = (((builtins.coord.x * locations.loc1) + loc0) + colors.col3.g);
 }
 )";
 
     auto* expect = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 @location(0) @internal(disable_validation__ignore_address_space) var<__in> loc0_1 : f32;
 
 @location(1) @internal(disable_validation__ignore_address_space) var<__in> loc1_1 : f32;
@@ -271,6 +302,12 @@
 
 @builtin(position) @internal(disable_validation__ignore_address_space) var<__in> coord_1 : vec4<f32>;
 
+@color(3) @internal(disable_validation__ignore_address_space) var<__in> col3_1 : vec4<f32>;
+
+@color(1) @interpolate(flat) @internal(disable_validation__ignore_address_space) var<__in> col1_1 : vec4<u32>;
+
+@color(2) @interpolate(flat) @internal(disable_validation__ignore_address_space) var<__in> col2_1 : vec4<i32>;
+
 struct FragBuiltins {
   coord : vec4<f32>,
 }
@@ -280,13 +317,19 @@
   loc2 : vec4<u32>,
 }
 
-fn frag_main_inner(loc0 : f32, locations : FragLocations, builtins : FragBuiltins) {
-  var col : f32 = ((builtins.coord.x * locations.loc1) + loc0);
+struct FragColors {
+  col3 : vec4<f32>,
+  col1 : vec4<u32>,
+  col2 : vec4<i32>,
+}
+
+fn frag_main_inner(loc0 : f32, locations : FragLocations, builtins : FragBuiltins, colors : FragColors) {
+  var col : f32 = (((builtins.coord.x * locations.loc1) + loc0) + colors.col3.g);
 }
 
 @fragment
 fn frag_main() {
-  frag_main_inner(loc0_1, FragLocations(loc1_1, loc2_1), FragBuiltins(coord_1));
+  frag_main_inner(loc0_1, FragLocations(loc1_1, loc2_1), FragBuiltins(coord_1), FragColors(col3_1, col1_1, col2_1));
 }
 )";
 
@@ -299,11 +342,14 @@
 
 TEST_F(CanonicalizeEntryPointIOTest, StructParameters_Spirv_OutOfOrder) {
     auto* src = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 @fragment
 fn frag_main(@location(0) loc0 : f32,
              locations : FragLocations,
-             builtins : FragBuiltins) {
-  var col : f32 = ((builtins.coord.x * locations.loc1) + loc0);
+             builtins : FragBuiltins,
+             colors : FragColors) {
+  var col : f32 = (((builtins.coord.x * locations.loc1) + loc0) + colors.col3.g);
 }
 
 struct FragBuiltins {
@@ -313,9 +359,16 @@
   @location(1) loc1 : f32,
   @location(2) @interpolate(flat) loc2 : vec4<u32>,
 };
+struct FragColors {
+  @color(3) col3 : vec4<f32>,
+  @color(1) col1 : vec4<u32>,
+  @color(2) col2 : vec4<i32>,
+};
 )";
 
     auto* expect = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 @location(0) @internal(disable_validation__ignore_address_space) var<__in> loc0_1 : f32;
 
 @location(1) @internal(disable_validation__ignore_address_space) var<__in> loc1_1 : f32;
@@ -324,13 +377,19 @@
 
 @builtin(position) @internal(disable_validation__ignore_address_space) var<__in> coord_1 : vec4<f32>;
 
-fn frag_main_inner(loc0 : f32, locations : FragLocations, builtins : FragBuiltins) {
-  var col : f32 = ((builtins.coord.x * locations.loc1) + loc0);
+@color(3) @internal(disable_validation__ignore_address_space) var<__in> col3_1 : vec4<f32>;
+
+@color(1) @interpolate(flat) @internal(disable_validation__ignore_address_space) var<__in> col1_1 : vec4<u32>;
+
+@color(2) @interpolate(flat) @internal(disable_validation__ignore_address_space) var<__in> col2_1 : vec4<i32>;
+
+fn frag_main_inner(loc0 : f32, locations : FragLocations, builtins : FragBuiltins, colors : FragColors) {
+  var col : f32 = (((builtins.coord.x * locations.loc1) + loc0) + colors.col3.g);
 }
 
 @fragment
 fn frag_main() {
-  frag_main_inner(loc0_1, FragLocations(loc1_1, loc2_1), FragBuiltins(coord_1));
+  frag_main_inner(loc0_1, FragLocations(loc1_1, loc2_1), FragBuiltins(coord_1), FragColors(col3_1, col1_1, col2_1));
 }
 
 struct FragBuiltins {
@@ -341,6 +400,12 @@
   loc1 : f32,
   loc2 : vec4<u32>,
 }
+
+struct FragColors {
+  col3 : vec4<f32>,
+  col1 : vec4<u32>,
+  col2 : vec4<i32>,
+}
 )";
 
     DataMap data;
@@ -352,6 +417,8 @@
 
 TEST_F(CanonicalizeEntryPointIOTest, StructParameters_kMsl) {
     auto* src = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 struct FragBuiltins {
   @builtin(position) coord : vec4<f32>,
 };
@@ -359,16 +426,24 @@
   @location(1) loc1 : f32,
   @location(2) @interpolate(flat) loc2 : vec4<u32>,
 };
+struct FragColors {
+  @color(3) col3 : vec4<f32>,
+  @color(1) col1 : vec4<u32>,
+  @color(2) col2 : vec4<i32>,
+};
 
 @fragment
 fn frag_main(@location(0) loc0 : f32,
              locations : FragLocations,
-             builtins : FragBuiltins) {
-  var col : f32 = ((builtins.coord.x * locations.loc1) + loc0);
+             builtins : FragBuiltins,
+             colors : FragColors) {
+  var col : f32 = (((builtins.coord.x * locations.loc1) + loc0) + colors.col3.g);
 }
 )";
 
     auto* expect = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 struct FragBuiltins {
   coord : vec4<f32>,
 }
@@ -378,7 +453,19 @@
   loc2 : vec4<u32>,
 }
 
+struct FragColors {
+  col3 : vec4<f32>,
+  col1 : vec4<u32>,
+  col2 : vec4<i32>,
+}
+
 struct tint_symbol_1 {
+  @color(1)
+  col1 : vec4<u32>,
+  @color(2)
+  col2 : vec4<i32>,
+  @color(3)
+  col3 : vec4<f32>,
   @location(0)
   loc0 : f32,
   @location(1)
@@ -387,13 +474,13 @@
   loc2 : vec4<u32>,
 }
 
-fn frag_main_inner(loc0 : f32, locations : FragLocations, builtins : FragBuiltins) {
-  var col : f32 = ((builtins.coord.x * locations.loc1) + loc0);
+fn frag_main_inner(loc0 : f32, locations : FragLocations, builtins : FragBuiltins, colors : FragColors) {
+  var col : f32 = (((builtins.coord.x * locations.loc1) + loc0) + colors.col3.g);
 }
 
 @fragment
 fn frag_main(@builtin(position) coord : vec4<f32>, tint_symbol : tint_symbol_1) {
-  frag_main_inner(tint_symbol.loc0, FragLocations(tint_symbol.loc1, tint_symbol.loc2), FragBuiltins(coord));
+  frag_main_inner(tint_symbol.loc0, FragLocations(tint_symbol.loc1, tint_symbol.loc2), FragBuiltins(coord), FragColors(tint_symbol.col3, tint_symbol.col1, tint_symbol.col2));
 }
 )";
 
@@ -406,11 +493,14 @@
 
 TEST_F(CanonicalizeEntryPointIOTest, StructParameters_kMsl_OutOfOrder) {
     auto* src = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 @fragment
 fn frag_main(@location(0) loc0 : f32,
              locations : FragLocations,
-             builtins : FragBuiltins) {
-  var col : f32 = ((builtins.coord.x * locations.loc1) + loc0);
+             builtins : FragBuiltins,
+             colors : FragColors) {
+  var col : f32 = (((builtins.coord.x * locations.loc1) + loc0) + colors.col3.g);
 }
 
 struct FragBuiltins {
@@ -420,10 +510,23 @@
   @location(1) loc1 : f32,
   @location(2) @interpolate(flat) loc2 : vec4<u32>,
 };
+struct FragColors {
+  @color(3) col3 : vec4<f32>,
+  @color(1) col1 : vec4<u32>,
+  @color(2) col2 : vec4<i32>,
+};
 )";
 
     auto* expect = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 struct tint_symbol_1 {
+  @color(1)
+  col1 : vec4<u32>,
+  @color(2)
+  col2 : vec4<i32>,
+  @color(3)
+  col3 : vec4<f32>,
   @location(0)
   loc0 : f32,
   @location(1)
@@ -432,13 +535,13 @@
   loc2 : vec4<u32>,
 }
 
-fn frag_main_inner(loc0 : f32, locations : FragLocations, builtins : FragBuiltins) {
-  var col : f32 = ((builtins.coord.x * locations.loc1) + loc0);
+fn frag_main_inner(loc0 : f32, locations : FragLocations, builtins : FragBuiltins, colors : FragColors) {
+  var col : f32 = (((builtins.coord.x * locations.loc1) + loc0) + colors.col3.g);
 }
 
 @fragment
 fn frag_main(@builtin(position) coord : vec4<f32>, tint_symbol : tint_symbol_1) {
-  frag_main_inner(tint_symbol.loc0, FragLocations(tint_symbol.loc1, tint_symbol.loc2), FragBuiltins(coord));
+  frag_main_inner(tint_symbol.loc0, FragLocations(tint_symbol.loc1, tint_symbol.loc2), FragBuiltins(coord), FragColors(tint_symbol.col3, tint_symbol.col1, tint_symbol.col2));
 }
 
 struct FragBuiltins {
@@ -449,6 +552,12 @@
   loc1 : f32,
   loc2 : vec4<u32>,
 }
+
+struct FragColors {
+  col3 : vec4<f32>,
+  col1 : vec4<u32>,
+  col2 : vec4<i32>,
+}
 )";
 
     DataMap data;
@@ -460,6 +569,8 @@
 
 TEST_F(CanonicalizeEntryPointIOTest, StructParameters_Hlsl) {
     auto* src = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 struct FragBuiltins {
   @builtin(position) coord : vec4<f32>,
 };
@@ -467,16 +578,24 @@
   @location(1) loc1 : f32,
   @location(2) @interpolate(flat) loc2 : vec4<u32>,
 };
+struct FragColors {
+  @color(3) col3 : vec4<f32>,
+  @color(1) col1 : vec4<u32>,
+  @color(2) col2 : vec4<i32>,
+};
 
 @fragment
 fn frag_main(@location(0) loc0 : f32,
              locations : FragLocations,
-             builtins : FragBuiltins) {
-  var col : f32 = ((builtins.coord.x * locations.loc1) + loc0);
+             builtins : FragBuiltins,
+             colors : FragColors) {
+  var col : f32 = (((builtins.coord.x * locations.loc1) + loc0) + colors.col3.g);
 }
 )";
 
     auto* expect = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 struct FragBuiltins {
   coord : vec4<f32>,
 }
@@ -486,7 +605,19 @@
   loc2 : vec4<u32>,
 }
 
+struct FragColors {
+  col3 : vec4<f32>,
+  col1 : vec4<u32>,
+  col2 : vec4<i32>,
+}
+
 struct tint_symbol_1 {
+  @color(1)
+  col1 : vec4<u32>,
+  @color(2)
+  col2 : vec4<i32>,
+  @color(3)
+  col3 : vec4<f32>,
   @location(0)
   loc0 : f32,
   @location(1)
@@ -497,13 +628,13 @@
   coord : vec4<f32>,
 }
 
-fn frag_main_inner(loc0 : f32, locations : FragLocations, builtins : FragBuiltins) {
-  var col : f32 = ((builtins.coord.x * locations.loc1) + loc0);
+fn frag_main_inner(loc0 : f32, locations : FragLocations, builtins : FragBuiltins, colors : FragColors) {
+  var col : f32 = (((builtins.coord.x * locations.loc1) + loc0) + colors.col3.g);
 }
 
 @fragment
 fn frag_main(tint_symbol : tint_symbol_1) {
-  frag_main_inner(tint_symbol.loc0, FragLocations(tint_symbol.loc1, tint_symbol.loc2), FragBuiltins(tint_symbol.coord));
+  frag_main_inner(tint_symbol.loc0, FragLocations(tint_symbol.loc1, tint_symbol.loc2), FragBuiltins(tint_symbol.coord), FragColors(tint_symbol.col3, tint_symbol.col1, tint_symbol.col2));
 }
 )";
 
@@ -516,11 +647,14 @@
 
 TEST_F(CanonicalizeEntryPointIOTest, StructParameters_Hlsl_OutOfOrder) {
     auto* src = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 @fragment
 fn frag_main(@location(0) loc0 : f32,
              locations : FragLocations,
-             builtins : FragBuiltins) {
-  var col : f32 = ((builtins.coord.x * locations.loc1) + loc0);
+             builtins : FragBuiltins,
+             colors : FragColors) {
+  var col : f32 = (((builtins.coord.x * locations.loc1) + loc0) + colors.col3.g);
 }
 
 struct FragBuiltins {
@@ -530,10 +664,23 @@
   @location(1) loc1 : f32,
   @location(2) @interpolate(flat) loc2 : vec4<u32>,
 };
+struct FragColors {
+  @color(3) col3 : vec4<f32>,
+  @color(1) col1 : vec4<u32>,
+  @color(2) col2 : vec4<i32>,
+};
 )";
 
     auto* expect = R"(
+enable chromium_experimental_framebuffer_fetch;
+
 struct tint_symbol_1 {
+  @color(1)
+  col1 : vec4<u32>,
+  @color(2)
+  col2 : vec4<i32>,
+  @color(3)
+  col3 : vec4<f32>,
   @location(0)
   loc0 : f32,
   @location(1)
@@ -544,13 +691,13 @@
   coord : vec4<f32>,
 }
 
-fn frag_main_inner(loc0 : f32, locations : FragLocations, builtins : FragBuiltins) {
-  var col : f32 = ((builtins.coord.x * locations.loc1) + loc0);
+fn frag_main_inner(loc0 : f32, locations : FragLocations, builtins : FragBuiltins, colors : FragColors) {
+  var col : f32 = (((builtins.coord.x * locations.loc1) + loc0) + colors.col3.g);
 }
 
 @fragment
 fn frag_main(tint_symbol : tint_symbol_1) {
-  frag_main_inner(tint_symbol.loc0, FragLocations(tint_symbol.loc1, tint_symbol.loc2), FragBuiltins(tint_symbol.coord));
+  frag_main_inner(tint_symbol.loc0, FragLocations(tint_symbol.loc1, tint_symbol.loc2), FragBuiltins(tint_symbol.coord), FragColors(tint_symbol.col3, tint_symbol.col1, tint_symbol.col2));
 }
 
 struct FragBuiltins {
@@ -561,6 +708,12 @@
   loc1 : f32,
   loc2 : vec4<u32>,
 }
+
+struct FragColors {
+  col3 : vec4<f32>,
+  col1 : vec4<u32>,
+  col2 : vec4<i32>,
+}
 )";
 
     DataMap data;
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/inspector/entry_point.cc b/src/tint/lang/wgsl/inspector/entry_point.cc
index 080f418..a93a83b 100644
--- a/src/tint/lang/wgsl/inspector/entry_point.cc
+++ b/src/tint/lang/wgsl/inspector/entry_point.cc
@@ -30,16 +30,7 @@
 namespace tint::inspector {
 
 StageVariable::StageVariable() = default;
-StageVariable::StageVariable(const StageVariable& other)
-    : name(other.name),
-      variable_name(other.variable_name),
-      has_location_attribute(other.has_location_attribute),
-      location_attribute(other.location_attribute),
-      component_type(other.component_type),
-      composition_type(other.composition_type),
-      interpolation_type(other.interpolation_type),
-      interpolation_sampling(other.interpolation_sampling) {}
-
+StageVariable::StageVariable(const StageVariable& other) = default;
 StageVariable::~StageVariable() = default;
 
 EntryPoint::EntryPoint() = default;
diff --git a/src/tint/lang/wgsl/inspector/entry_point.h b/src/tint/lang/wgsl/inspector/entry_point.h
index 1dee5c1..24164d0 100644
--- a/src/tint/lang/wgsl/inspector/entry_point.h
+++ b/src/tint/lang/wgsl/inspector/entry_point.h
@@ -86,11 +86,13 @@
     std::string name;
     /// Name of the variable itself. (e.g. 'var')
     std::string variable_name;
-    /// Is location attribute present
-    bool has_location_attribute = false;
-    /// Value of the location attribute, only valid if #has_location_attribute is
-    /// true.
-    uint32_t location_attribute;
+    /// Attributes applied to the variable
+    struct {
+        /// Value of the location attribute, if set.
+        std::optional<uint32_t> location;
+        /// Value of the color attribute, if set.
+        std::optional<uint32_t> color;
+    } attributes;
     /// Scalar type that the variable is composed of.
     ComponentType component_type = ComponentType::kUnknown;
     /// How the scalars are composed for the variable.
diff --git a/src/tint/lang/wgsl/inspector/inspector.cc b/src/tint/lang/wgsl/inspector/inspector.cc
index 7f49f66..c07001a 100644
--- a/src/tint/lang/wgsl/inspector/inspector.cc
+++ b/src/tint/lang/wgsl/inspector/inspector.cc
@@ -178,7 +178,7 @@
         AddEntryPointInOutVariables(param->Declaration()->name->symbol.Name(),
                                     param->Declaration()->name->symbol.Name(), param->Type(),
                                     param->Declaration()->attributes, param->Attributes().location,
-                                    entry_point.input_variables);
+                                    param->Attributes().color, entry_point.input_variables);
 
         entry_point.input_position_used |= ContainsBuiltin(
             core::BuiltinValue::kPosition, param->Type(), param->Declaration()->attributes);
@@ -198,7 +198,8 @@
 
     if (!sem->ReturnType()->Is<core::type::Void>()) {
         AddEntryPointInOutVariables("<retval>", "", sem->ReturnType(), func->return_type_attributes,
-                                    sem->ReturnLocation(), entry_point.output_variables);
+                                    sem->ReturnLocation(), /* @color */ std::nullopt,
+                                    entry_point.output_variables);
 
         entry_point.output_sample_mask_used = ContainsBuiltin(
             core::BuiltinValue::kSampleMask, sem->ReturnType(), func->return_type_attributes);
@@ -581,6 +582,7 @@
                                             const core::type::Type* type,
                                             VectorRef<const ast::Attribute*> attributes,
                                             std::optional<uint32_t> location,
+                                            std::optional<uint32_t> color,
                                             std::vector<StageVariable>& variables) const {
     // Skip builtins.
     if (ast::HasAttribute<ast::BuiltinAttribute>(attributes)) {
@@ -594,7 +596,8 @@
         for (auto* member : struct_ty->Members()) {
             AddEntryPointInOutVariables(name + "." + member->Name().Name(), member->Name().Name(),
                                         member->Type(), member->Declaration()->attributes,
-                                        member->Attributes().location, variables);
+                                        member->Attributes().location, member->Attributes().color,
+                                        variables);
         }
         return;
     }
@@ -607,9 +610,8 @@
     std::tie(stage_variable.component_type, stage_variable.composition_type) =
         CalculateComponentAndComposition(type);
 
-    TINT_ASSERT(location.has_value());
-    stage_variable.has_location_attribute = true;
-    stage_variable.location_attribute = location.value();
+    stage_variable.attributes.location = location;
+    stage_variable.attributes.color = color;
 
     std::tie(stage_variable.interpolation_type, stage_variable.interpolation_sampling) =
         CalculateInterpolationData(type, attributes);
diff --git a/src/tint/lang/wgsl/inspector/inspector.h b/src/tint/lang/wgsl/inspector/inspector.h
index 1af117d..14c5ef6 100644
--- a/src/tint/lang/wgsl/inspector/inspector.h
+++ b/src/tint/lang/wgsl/inspector/inspector.h
@@ -174,13 +174,15 @@
     /// @param variable_name the name of the variable being added
     /// @param type the type of the variable
     /// @param attributes the variable attributes
-    /// @param location the location value if provided
+    /// @param location the location attribute value if provided
+    /// @param color the color attribute value if provided
     /// @param variables the list to add the variables to
     void AddEntryPointInOutVariables(std::string name,
                                      std::string variable_name,
                                      const core::type::Type* type,
                                      VectorRef<const ast::Attribute*> attributes,
                                      std::optional<uint32_t> location,
+                                     std::optional<uint32_t> color,
                                      std::vector<StageVariable>& variables) const;
 
     /// Recursively determine if the type contains builtin.
diff --git a/src/tint/lang/wgsl/inspector/inspector_test.cc b/src/tint/lang/wgsl/inspector/inspector_test.cc
index 0eb5c8e..c33f950 100644
--- a/src/tint/lang/wgsl/inspector/inspector_test.cc
+++ b/src/tint/lang/wgsl/inspector/inspector_test.cc
@@ -443,15 +443,13 @@
     ASSERT_EQ(1u, result[0].input_variables.size());
     EXPECT_EQ("in_var", result[0].input_variables[0].name);
     EXPECT_EQ("in_var", result[0].input_variables[0].variable_name);
-    EXPECT_TRUE(result[0].input_variables[0].has_location_attribute);
-    EXPECT_EQ(0u, result[0].input_variables[0].location_attribute);
+    EXPECT_EQ(0u, result[0].input_variables[0].attributes.location);
     EXPECT_EQ(component, result[0].input_variables[0].component_type);
 
     ASSERT_EQ(1u, result[0].output_variables.size());
     EXPECT_EQ("<retval>", result[0].output_variables[0].name);
     EXPECT_EQ("", result[0].output_variables[0].variable_name);
-    EXPECT_TRUE(result[0].output_variables[0].has_location_attribute);
-    EXPECT_EQ(0u, result[0].output_variables[0].location_attribute);
+    EXPECT_EQ(0u, result[0].output_variables[0].attributes.location);
     EXPECT_EQ(component, result[0].output_variables[0].component_type);
 }
 INSTANTIATE_TEST_SUITE_P(InspectorGetEntryPointTest,
@@ -466,6 +464,8 @@
                                                           CompositionType::kVec4)));
 
 TEST_F(InspectorGetEntryPointTest, MultipleInOutVariables) {
+    Enable(wgsl::Extension::kChromiumExperimentalFramebufferFetch);
+
     auto* in_var0 = Param("in_var0", ty.u32(),
                           Vector{
                               Location(0_u),
@@ -478,8 +478,7 @@
                           });
     auto* in_var4 = Param("in_var4", ty.u32(),
                           Vector{
-                              Location(4_u),
-                              Flat(),
+                              Color(2_u),
                           });
     Func("foo", Vector{in_var0, in_var1, in_var4}, ty.u32(),
          Vector{
@@ -501,28 +500,28 @@
     ASSERT_EQ(3u, result[0].input_variables.size());
     EXPECT_EQ("in_var0", result[0].input_variables[0].name);
     EXPECT_EQ("in_var0", result[0].input_variables[0].variable_name);
-    EXPECT_TRUE(result[0].input_variables[0].has_location_attribute);
-    EXPECT_EQ(0u, result[0].input_variables[0].location_attribute);
+    EXPECT_EQ(0u, result[0].input_variables[0].attributes.location);
+    EXPECT_EQ(std::nullopt, result[0].input_variables[0].attributes.color);
     EXPECT_EQ(InterpolationType::kFlat, result[0].input_variables[0].interpolation_type);
     EXPECT_EQ(ComponentType::kU32, result[0].input_variables[0].component_type);
     EXPECT_EQ("in_var1", result[0].input_variables[1].name);
     EXPECT_EQ("in_var1", result[0].input_variables[1].variable_name);
-    EXPECT_TRUE(result[0].input_variables[1].has_location_attribute);
-    EXPECT_EQ(1u, result[0].input_variables[1].location_attribute);
+    EXPECT_EQ(1u, result[0].input_variables[1].attributes.location);
+    EXPECT_EQ(std::nullopt, result[0].input_variables[1].attributes.color);
     EXPECT_EQ(InterpolationType::kFlat, result[0].input_variables[1].interpolation_type);
     EXPECT_EQ(ComponentType::kU32, result[0].input_variables[1].component_type);
     EXPECT_EQ("in_var4", result[0].input_variables[2].name);
     EXPECT_EQ("in_var4", result[0].input_variables[2].variable_name);
-    EXPECT_TRUE(result[0].input_variables[2].has_location_attribute);
-    EXPECT_EQ(4u, result[0].input_variables[2].location_attribute);
+    EXPECT_EQ(std::nullopt, result[0].input_variables[2].attributes.location);
+    EXPECT_EQ(2u, result[0].input_variables[2].attributes.color);
     EXPECT_EQ(InterpolationType::kFlat, result[0].input_variables[2].interpolation_type);
     EXPECT_EQ(ComponentType::kU32, result[0].input_variables[2].component_type);
 
     ASSERT_EQ(1u, result[0].output_variables.size());
     EXPECT_EQ("<retval>", result[0].output_variables[0].name);
     EXPECT_EQ("", result[0].output_variables[0].variable_name);
-    EXPECT_TRUE(result[0].output_variables[0].has_location_attribute);
-    EXPECT_EQ(0u, result[0].output_variables[0].location_attribute);
+    EXPECT_EQ(0u, result[0].output_variables[0].attributes.location);
+    EXPECT_EQ(std::nullopt, result[0].output_variables[0].attributes.color);
     EXPECT_EQ(ComponentType::kU32, result[0].output_variables[0].component_type);
 }
 
@@ -569,31 +568,27 @@
     ASSERT_EQ(1u, result[0].input_variables.size());
     EXPECT_EQ("in_var_foo", result[0].input_variables[0].name);
     EXPECT_EQ("in_var_foo", result[0].input_variables[0].variable_name);
-    EXPECT_TRUE(result[0].input_variables[0].has_location_attribute);
-    EXPECT_EQ(0u, result[0].input_variables[0].location_attribute);
+    EXPECT_EQ(0u, result[0].input_variables[0].attributes.location);
     EXPECT_EQ(InterpolationType::kFlat, result[0].input_variables[0].interpolation_type);
     EXPECT_EQ(ComponentType::kU32, result[0].input_variables[0].component_type);
 
     ASSERT_EQ(1u, result[0].output_variables.size());
     EXPECT_EQ("<retval>", result[0].output_variables[0].name);
     EXPECT_EQ("", result[0].output_variables[0].variable_name);
-    EXPECT_TRUE(result[0].output_variables[0].has_location_attribute);
-    EXPECT_EQ(0u, result[0].output_variables[0].location_attribute);
+    EXPECT_EQ(0u, result[0].output_variables[0].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[0].output_variables[0].component_type);
 
     ASSERT_EQ(1u, result[1].input_variables.size());
     EXPECT_EQ("in_var_bar", result[1].input_variables[0].name);
     EXPECT_EQ("in_var_bar", result[1].input_variables[0].variable_name);
-    EXPECT_TRUE(result[1].input_variables[0].has_location_attribute);
-    EXPECT_EQ(0u, result[1].input_variables[0].location_attribute);
+    EXPECT_EQ(0u, result[1].input_variables[0].attributes.location);
     EXPECT_EQ(InterpolationType::kFlat, result[1].input_variables[0].interpolation_type);
     EXPECT_EQ(ComponentType::kU32, result[1].input_variables[0].component_type);
 
     ASSERT_EQ(1u, result[1].output_variables.size());
     EXPECT_EQ("<retval>", result[1].output_variables[0].name);
     EXPECT_EQ("", result[1].output_variables[0].variable_name);
-    EXPECT_TRUE(result[1].output_variables[0].has_location_attribute);
-    EXPECT_EQ(1u, result[1].output_variables[0].location_attribute);
+    EXPECT_EQ(1u, result[1].output_variables[0].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[1].output_variables[0].component_type);
 }
 
@@ -626,8 +621,7 @@
     ASSERT_EQ(1u, result[0].input_variables.size());
     EXPECT_EQ("in_var1", result[0].input_variables[0].name);
     EXPECT_EQ("in_var1", result[0].input_variables[0].variable_name);
-    EXPECT_TRUE(result[0].input_variables[0].has_location_attribute);
-    EXPECT_EQ(0u, result[0].input_variables[0].location_attribute);
+    EXPECT_EQ(0u, result[0].input_variables[0].attributes.location);
     EXPECT_EQ(ComponentType::kF32, result[0].input_variables[0].component_type);
 
     ASSERT_EQ(0u, result[0].output_variables.size());
@@ -659,25 +653,21 @@
     ASSERT_EQ(2u, result[0].input_variables.size());
     EXPECT_EQ("param.a", result[0].input_variables[0].name);
     EXPECT_EQ("a", result[0].input_variables[0].variable_name);
-    EXPECT_TRUE(result[0].input_variables[0].has_location_attribute);
-    EXPECT_EQ(0u, result[0].input_variables[0].location_attribute);
+    EXPECT_EQ(0u, result[0].input_variables[0].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[0].input_variables[0].component_type);
     EXPECT_EQ("param.b", result[0].input_variables[1].name);
     EXPECT_EQ("b", result[0].input_variables[1].variable_name);
-    EXPECT_TRUE(result[0].input_variables[1].has_location_attribute);
-    EXPECT_EQ(1u, result[0].input_variables[1].location_attribute);
+    EXPECT_EQ(1u, result[0].input_variables[1].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[0].input_variables[1].component_type);
 
     ASSERT_EQ(2u, result[0].output_variables.size());
     EXPECT_EQ("<retval>.a", result[0].output_variables[0].name);
     EXPECT_EQ("a", result[0].output_variables[0].variable_name);
-    EXPECT_TRUE(result[0].output_variables[0].has_location_attribute);
-    EXPECT_EQ(0u, result[0].output_variables[0].location_attribute);
+    EXPECT_EQ(0u, result[0].output_variables[0].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[0].output_variables[0].component_type);
     EXPECT_EQ("<retval>.b", result[0].output_variables[1].name);
     EXPECT_EQ("b", result[0].output_variables[1].variable_name);
-    EXPECT_TRUE(result[0].output_variables[1].has_location_attribute);
-    EXPECT_EQ(1u, result[0].output_variables[1].location_attribute);
+    EXPECT_EQ(1u, result[0].output_variables[1].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[0].output_variables[1].component_type);
 }
 
@@ -709,25 +699,21 @@
     ASSERT_EQ(2u, result[0].output_variables.size());
     EXPECT_EQ("<retval>.a", result[0].output_variables[0].name);
     EXPECT_EQ("a", result[0].output_variables[0].variable_name);
-    EXPECT_TRUE(result[0].output_variables[0].has_location_attribute);
-    EXPECT_EQ(0u, result[0].output_variables[0].location_attribute);
+    EXPECT_EQ(0u, result[0].output_variables[0].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[0].output_variables[0].component_type);
     EXPECT_EQ("<retval>.b", result[0].output_variables[1].name);
     EXPECT_EQ("b", result[0].output_variables[1].variable_name);
-    EXPECT_TRUE(result[0].output_variables[1].has_location_attribute);
-    EXPECT_EQ(1u, result[0].output_variables[1].location_attribute);
+    EXPECT_EQ(1u, result[0].output_variables[1].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[0].output_variables[1].component_type);
 
     ASSERT_EQ(2u, result[1].input_variables.size());
     EXPECT_EQ("param.a", result[1].input_variables[0].name);
     EXPECT_EQ("a", result[1].input_variables[0].variable_name);
-    EXPECT_TRUE(result[1].input_variables[0].has_location_attribute);
-    EXPECT_EQ(0u, result[1].input_variables[0].location_attribute);
+    EXPECT_EQ(0u, result[1].input_variables[0].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[1].input_variables[0].component_type);
     EXPECT_EQ("param.b", result[1].input_variables[1].name);
     EXPECT_EQ("b", result[1].input_variables[1].variable_name);
-    EXPECT_TRUE(result[1].input_variables[1].has_location_attribute);
-    EXPECT_EQ(1u, result[1].input_variables[1].location_attribute);
+    EXPECT_EQ(1u, result[1].input_variables[1].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[1].input_variables[1].component_type);
 
     ASSERT_EQ(0u, result[1].output_variables.size());
@@ -765,40 +751,33 @@
     ASSERT_EQ(5u, result[0].input_variables.size());
     EXPECT_EQ("param_a.a", result[0].input_variables[0].name);
     EXPECT_EQ("a", result[0].input_variables[0].variable_name);
-    EXPECT_TRUE(result[0].input_variables[0].has_location_attribute);
-    EXPECT_EQ(0u, result[0].input_variables[0].location_attribute);
+    EXPECT_EQ(0u, result[0].input_variables[0].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[0].input_variables[0].component_type);
     EXPECT_EQ("param_a.b", result[0].input_variables[1].name);
     EXPECT_EQ("b", result[0].input_variables[1].variable_name);
-    EXPECT_TRUE(result[0].input_variables[1].has_location_attribute);
-    EXPECT_EQ(1u, result[0].input_variables[1].location_attribute);
+    EXPECT_EQ(1u, result[0].input_variables[1].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[0].input_variables[1].component_type);
     EXPECT_EQ("param_b.a", result[0].input_variables[2].name);
     EXPECT_EQ("a", result[0].input_variables[2].variable_name);
-    EXPECT_TRUE(result[0].input_variables[2].has_location_attribute);
-    EXPECT_EQ(2u, result[0].input_variables[2].location_attribute);
+    EXPECT_EQ(2u, result[0].input_variables[2].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[0].input_variables[2].component_type);
     EXPECT_EQ("param_c", result[0].input_variables[3].name);
     EXPECT_EQ("param_c", result[0].input_variables[3].variable_name);
-    EXPECT_TRUE(result[0].input_variables[3].has_location_attribute);
-    EXPECT_EQ(3u, result[0].input_variables[3].location_attribute);
+    EXPECT_EQ(3u, result[0].input_variables[3].attributes.location);
     EXPECT_EQ(ComponentType::kF32, result[0].input_variables[3].component_type);
     EXPECT_EQ("param_d", result[0].input_variables[4].name);
     EXPECT_EQ("param_d", result[0].input_variables[4].variable_name);
-    EXPECT_TRUE(result[0].input_variables[4].has_location_attribute);
-    EXPECT_EQ(4u, result[0].input_variables[4].location_attribute);
+    EXPECT_EQ(4u, result[0].input_variables[4].attributes.location);
     EXPECT_EQ(ComponentType::kF32, result[0].input_variables[4].component_type);
 
     ASSERT_EQ(2u, result[0].output_variables.size());
     EXPECT_EQ("<retval>.a", result[0].output_variables[0].name);
     EXPECT_EQ("a", result[0].output_variables[0].variable_name);
-    EXPECT_TRUE(result[0].output_variables[0].has_location_attribute);
-    EXPECT_EQ(0u, result[0].output_variables[0].location_attribute);
+    EXPECT_EQ(0u, result[0].output_variables[0].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[0].output_variables[0].component_type);
     EXPECT_EQ("<retval>.b", result[0].output_variables[1].name);
     EXPECT_EQ("b", result[0].output_variables[1].variable_name);
-    EXPECT_TRUE(result[0].output_variables[1].has_location_attribute);
-    EXPECT_EQ(1u, result[0].output_variables[1].location_attribute);
+    EXPECT_EQ(1u, result[0].output_variables[1].attributes.location);
     EXPECT_EQ(ComponentType::kU32, result[0].output_variables[1].component_type);
 }
 
diff --git a/src/tint/lang/wgsl/reader/parser/error_resync_test.cc b/src/tint/lang/wgsl/reader/parser/error_resync_test.cc
index 0a368a9..a38fd87 100644
--- a/src/tint/lang/wgsl/reader/parser/error_resync_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/error_resync_test.cc
@@ -67,7 +67,7 @@
      ^
 
 test.wgsl:4:2 error: expected attribute
-Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
+Possible values: 'align', 'binding', 'builtin', 'color', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
 @_ fn -> {}
  ^
 )");
@@ -135,7 +135,7 @@
          ^^^^
 
 test.wgsl:7:6 error: expected attribute
-Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
+Possible values: 'align', 'binding', 'builtin', 'color', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
     @- x : i32,
      ^
 )");
diff --git a/src/tint/lang/wgsl/reader/parser/function_attribute_list_test.cc b/src/tint/lang/wgsl/reader/parser/function_attribute_list_test.cc
index 259f1a1..4fae501 100644
--- a/src/tint/lang/wgsl/reader/parser/function_attribute_list_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/function_attribute_list_test.cc
@@ -66,7 +66,7 @@
     EXPECT_TRUE(attrs.value.IsEmpty());
     EXPECT_EQ(p->error(), R"(1:2: expected attribute
 Did you mean 'invariant'?
-Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
+Possible values: 'align', 'binding', 'builtin', 'color', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
 }
 
 }  // namespace
diff --git a/src/tint/lang/wgsl/reader/parser/parser.cc b/src/tint/lang/wgsl/reader/parser/parser.cc
index 6a10c72..8cc0bba 100644
--- a/src/tint/lang/wgsl/reader/parser/parser.cc
+++ b/src/tint/lang/wgsl/reader/parser/parser.cc
@@ -3088,6 +3088,8 @@
             return create<ast::BindingAttribute>(t.source(), args[0]);
         case core::Attribute::kBuiltin:
             return create<ast::BuiltinAttribute>(t.source(), args[0]);
+        case core::Attribute::kColor:
+            return create<ast::ColorAttribute>(t.source(), args[0]);
         case core::Attribute::kCompute:
             return create<ast::StageAttribute>(t.source(), ast::PipelineStage::kCompute);
         case core::Attribute::kFragment:
diff --git a/src/tint/lang/wgsl/reader/parser/variable_attribute_list_test.cc b/src/tint/lang/wgsl/reader/parser/variable_attribute_list_test.cc
index f10f725..417ab1f 100644
--- a/src/tint/lang/wgsl/reader/parser/variable_attribute_list_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/variable_attribute_list_test.cc
@@ -64,7 +64,7 @@
     EXPECT_TRUE(attrs.value.IsEmpty());
     EXPECT_EQ(p->error(), R"(1:2: expected attribute
 Did you mean 'invariant'?
-Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
+Possible values: 'align', 'binding', 'builtin', 'color', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
 }
 
 }  // namespace
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
index 5e99225..e2c1c43 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
@@ -459,6 +459,9 @@
                 if (param_sem->Attributes().location.has_value()) {
                     param->SetLocation(param_sem->Attributes().location.value(), interpolation);
                 }
+                if (param_sem->Attributes().color.has_value()) {
+                    TINT_UNIMPLEMENTED() << "IR does not currently support texel fetch extension";
+                }
             }
 
             scopes_.Set(p->name->symbol, param);
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..92151d0 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,15 @@
             Pass,
         },
         TestParams{
+            {AttributeKind::kColor},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kColor, AttributeKind::kLocation},
+            R"(3:4 error: multiple entry point IO attributes
+1:2 note: previously consumed @color)",
+        },
+        TestParams{
             {AttributeKind::kDiagnostic},
             R"(1:2 error: @diagnostic is not valid for function parameters)",
         },
@@ -823,6 +868,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 +973,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 +1059,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 +1171,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 +1300,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 +1379,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/dual_source_blending_extension_test.cc b/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
index 9ea0b05..223744a 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
@@ -190,6 +190,29 @@
     EXPECT_EQ(r()->error(), "12:34 error: @index can only be used with @location(0)");
 }
 
+TEST_F(DualSourceBlendingExtensionTests, NoNonZeroCollisionsBetweenInAndOut) {
+    // struct NonZeroLocation {
+    //   @location(1) a : vec4<f32>,
+    // };
+    // struct NonZeroIndex {
+    //   @location(0) @index(1) a : vec4<f32>,
+    // };
+    // fn X(in : NonZeroLocation) -> NonZeroIndex { return NonZeroIndex(); }
+    // fn Y(in : NonZeroIndex) -> NonZeroLocation { return NonZeroLocation(); }
+    Structure("NonZeroLocation", Vector{
+                                     Member("a", ty.vec4<f32>(), Vector{Location(1_a)}),
+                                 });
+    Structure("NonZeroIndex", Vector{
+                                  Member("a", ty.vec4<f32>(), Vector{Location(0_a), Index(1_a)}),
+                              });
+    Func("X", Vector{Param("in", ty("NonZeroLocation"))}, ty("NonZeroIndex"),
+         Vector{Return(Call("NonZeroIndex"))}, Vector{Stage(ast::PipelineStage::kFragment)});
+    Func("Y", Vector{Param("in", ty("NonZeroIndex"))}, ty("NonZeroLocation"),
+         Vector{Return(Call("NonZeroLocation"))}, Vector{Stage(ast::PipelineStage::kFragment)});
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
 class DualSourceBlendingExtensionTestWithParams : public ResolverTestWithParam<int> {
   public:
     DualSourceBlendingExtensionTestWithParams() {
@@ -200,6 +223,12 @@
 // Rendering to multiple render targets while using dual source blending should fail.
 TEST_P(DualSourceBlendingExtensionTestWithParams,
        MultipleRenderTargetsNotAllowed_IndexThenNonZeroLocation) {
+    // struct S {
+    //   @location(0) @index(0) a : vec4<f32>,
+    //   @location(0) @index(1) b : vec4<f32>,
+    //   @location(n)           c : vec4<f32>,
+    // };
+    // fn F() -> S { return S(); }
     Structure("S",
               Vector{
                   Member("a", ty.vec4<f32>(), Vector{Location(0_a), Index(0_a)}),
@@ -218,6 +247,12 @@
 
 TEST_P(DualSourceBlendingExtensionTestWithParams,
        MultipleRenderTargetsNotAllowed_NonZeroLocationThenIndex) {
+    // struct S {
+    //   @location(n)           a : vec4<f32>,
+    //   @location(0) @index(0) b : vec4<f32>,
+    //   @location(0) @index(1) c : vec4<f32>,
+    // };
+    // fn F() -> S { return S(); }
     Structure("S",
               Vector{
                   Member("a", ty.vec4<f32>(), Vector{Location(Source{{1, 2}}, AInt(GetParam()))}),
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..05b2a44 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;
         }
     }
@@ -1345,11 +1414,14 @@
     // should be validated independently from those used in parameters.
     builtins.Clear();
     locations_and_indices.Clear();
+    first_nonzero_location = nullptr;
+    first_nonzero_index = nullptr;
 
     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 +2226,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 +2251,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 +2271,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 +2345,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 +2382,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
diff --git a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
index ec38e3d..12688b4 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
@@ -40,6 +40,7 @@
 #include "src/tint/lang/wgsl/ast/break_statement.h"
 #include "src/tint/lang/wgsl/ast/call_expression.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"
@@ -505,6 +506,11 @@
                 EmitExpression(out, location->expr);
                 out << ")";
             },
+            [&](const ast::ColorAttribute* color) {
+                out << "color(";
+                EmitExpression(out, color->expr);
+                out << ")";
+            },
             [&](const ast::IndexAttribute* index) {
                 out << "index(";
                 EmitExpression(out, index->expr);