Add validation for Framebuffer Fetch

This adds the reflection of the framebuffer inputs from Tint, and
validation that these inputs match the render pipeline's color state.

Also adds tests, docs, rename a couple identifiers and restructure the
ColorState validation to do less casting.

Tests found the following bugs during development:
 - Forgot to skip over unused framebuffer inputs during validation.
 - Forget to update SetWGSLExtensionAllowList.
 - Indexed with I instead of J in a test.

Bug: dawn:2195
Change-Id: I367626b07c5b3fc0bb7008439170e8c3aa66abd8
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/160581
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Austin Eng <enga@chromium.org>
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/dawn.json b/dawn.json
index bff7679..3d41fa9 100644
--- a/dawn.json
+++ b/dawn.json
@@ -1919,6 +1919,7 @@
             {"value": 1022, "name": "host mapped pointer", "tags": ["dawn"]},
             {"value": 1023, "name": "multi planar render targets", "tags": ["dawn"]},
             {"value": 1024, "name": "multi planar format nv12a", "tags": ["dawn"]},
+            {"value": 1025, "name": "framebuffer fetch", "tags": ["dawn"]},
 
             {"value": 1100, "name": "shared texture memory vk dedicated allocation", "tags": ["dawn", "native"]},
             {"value": 1101, "name": "shared texture memory a hardware buffer", "tags": ["dawn", "native"]},
diff --git a/docs/dawn/features/framebuffer_fetch.md b/docs/dawn/features/framebuffer_fetch.md
new file mode 100644
index 0000000..b47e701
--- /dev/null
+++ b/docs/dawn/features/framebuffer_fetch.md
@@ -0,0 +1,14 @@
+# Framebuffer Fetch (experimental)
+
+This extension enables support for the [`chromium_experimental_framebuffer_fetch`](../../tint/extensions/chromium_experimental_framebuffer_fetch.md) WGSL extension.
+
+The extension is experimental and might change for example to gain new validation rules (with extension struct) in the future.
+
+It is available on tiler Metal GPUs.
+
+## Validation
+
+In `Device::CreateRenderPipeline` or `Device::CreateRenderPipelineAsync`:
+ - For each `@color(N) in : T` fragment in:
+   - color target N must exist
+   - color target N's format must match T in both component count and base type
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index 927eac3..adf2915 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -1482,6 +1482,10 @@
         mWGSLAllowedFeatures.extensions.insert(
             tint::wgsl::Extension::kChromiumExperimentalPixelLocal);
     }
+    if (mEnabledFeatures.IsEnabled(Feature::FramebufferFetch)) {
+        mWGSLAllowedFeatures.extensions.insert(
+            tint::wgsl::Extension::kChromiumExperimentalFramebufferFetch);
+    }
 }
 
 const tint::wgsl::AllowedFeatures& DeviceBase::GetWGSLAllowedFeatures() const {
diff --git a/src/dawn/native/Features.cpp b/src/dawn/native/Features.cpp
index b7a9cf3..4c55707 100644
--- a/src/dawn/native/Features.cpp
+++ b/src/dawn/native/Features.cpp
@@ -258,6 +258,11 @@
       "https://dawn.googlesource.com/dawn/+/refs/heads/main/docs/dawn/features/"
       "host_mapped_pointer.md",
       FeatureInfo::FeatureState::Experimental}},
+    {Feature::FramebufferFetch,
+     {"Support loading the current framebuffer value in fragment shaders.",
+      "https://dawn.googlesource.com/dawn/+/refs/heads/main/docs/dawn/features/"
+      "framebuffer_fetch.md",
+      FeatureInfo::FeatureState::Experimental}},
 };
 
 }  // anonymous namespace
diff --git a/src/dawn/native/RenderPipeline.cpp b/src/dawn/native/RenderPipeline.cpp
index cd47356..1d03bf9 100644
--- a/src/dawn/native/RenderPipeline.cpp
+++ b/src/dawn/native/RenderPipeline.cpp
@@ -31,6 +31,7 @@
 #include <cmath>
 
 #include "dawn/common/BitSetIterator.h"
+#include "dawn/common/ityp_span.h"
 #include "dawn/native/ChainUtils.h"
 #include "dawn/native/CommandValidation.h"
 #include "dawn/native/Commands.h"
@@ -420,69 +421,78 @@
 
 MaybeError ValidateColorTargetState(
     DeviceBase* device,
-    const ColorTargetState* descriptor,
+    const ColorTargetState& descriptor,
+    const Format* format,
     bool fragmentWritten,
-    const EntryPointMetadata::FragmentOutputVariableInfo& fragmentOutputVariable) {
-    DAWN_INVALID_IF(descriptor->nextInChain != nullptr, "nextInChain must be nullptr.");
+    const EntryPointMetadata::FragmentRenderAttachmentInfo& fragmentOutputVariable) {
+    DAWN_INVALID_IF(descriptor.nextInChain != nullptr, "nextInChain must be nullptr.");
 
-    if (descriptor->blend) {
-        DAWN_TRY_CONTEXT(ValidateBlendState(device, descriptor->blend), "validating blend state.");
+    if (descriptor.blend) {
+        DAWN_TRY_CONTEXT(ValidateBlendState(device, descriptor.blend), "validating blend state.");
     }
 
-    DAWN_TRY(ValidateColorWriteMask(descriptor->writeMask));
-
-    const Format* format;
-    DAWN_TRY_ASSIGN(format, device->GetInternalFormat(descriptor->format));
+    DAWN_TRY(ValidateColorWriteMask(descriptor.writeMask));
     DAWN_INVALID_IF(!format->IsColor() || !format->isRenderable,
-                    "Color format (%s) is not color renderable.", descriptor->format);
+                    "Color format (%s) is not color renderable.", format->format);
 
     DAWN_INVALID_IF(
-        descriptor->blend &&
+        descriptor.blend &&
             !(format->GetAspectInfo(Aspect::Color).supportedSampleTypes & SampleTypeBit::Float),
-        "Blending is enabled but color format (%s) is not blendable.", descriptor->format);
+        "Blending is enabled but color format (%s) is not blendable.", format->format);
 
-    if (fragmentWritten) {
+    if (!fragmentWritten) {
         DAWN_INVALID_IF(
-            fragmentOutputVariable.baseType != format->GetAspectInfo(Aspect::Color).baseType,
-            "Color format (%s) base type (%s) doesn't match the fragment "
-            "module output type (%s).",
-            descriptor->format, format->GetAspectInfo(Aspect::Color).baseType,
-            fragmentOutputVariable.baseType);
-
-        DAWN_INVALID_IF(fragmentOutputVariable.componentCount < format->componentCount,
-                        "The fragment stage has fewer output components (%u) than the color format "
-                        "(%s) component count (%u).",
-                        fragmentOutputVariable.componentCount, descriptor->format,
-                        format->componentCount);
-
-        if (descriptor->blend) {
-            if (fragmentOutputVariable.componentCount < 4u) {
-                // No alpha channel output
-                // Make sure there's no alpha involved in the blending operation
-                DAWN_INVALID_IF(BlendFactorContainsSrcAlpha(descriptor->blend->color.srcFactor) ||
-                                    BlendFactorContainsSrcAlpha(descriptor->blend->color.dstFactor),
-                                "Color blending srcfactor (%s) or dstFactor (%s) is reading alpha "
-                                "but it is missing from fragment output.",
-                                descriptor->blend->color.srcFactor,
-                                descriptor->blend->color.dstFactor);
-            }
-        }
-    } else {
-        DAWN_INVALID_IF(
-            descriptor->writeMask != wgpu::ColorWriteMask::None,
+            descriptor.writeMask != wgpu::ColorWriteMask::None,
             "Color target has no corresponding fragment stage output but writeMask (%s) is "
             "not zero.",
-            descriptor->writeMask);
+            descriptor.writeMask);
+        return {};
+    }
+
+    DAWN_INVALID_IF(
+        fragmentOutputVariable.baseType != format->GetAspectInfo(Aspect::Color).baseType,
+        "Color format (%s) base type (%s) doesn't match the fragment "
+        "module output type (%s).",
+        format->format, format->GetAspectInfo(Aspect::Color).baseType,
+        fragmentOutputVariable.baseType);
+
+    DAWN_INVALID_IF(fragmentOutputVariable.componentCount < format->componentCount,
+                    "The fragment stage has fewer output components (%u) than the color format "
+                    "(%s) component count (%u).",
+                    fragmentOutputVariable.componentCount, format->format, format->componentCount);
+
+    if (descriptor.blend && fragmentOutputVariable.componentCount < 4u) {
+        // No alpha channel output, make sure there's no alpha involved in the blending operation.
+        DAWN_INVALID_IF(BlendFactorContainsSrcAlpha(descriptor.blend->color.srcFactor) ||
+                            BlendFactorContainsSrcAlpha(descriptor.blend->color.dstFactor),
+                        "Color blending srcFactor (%s) or dstFactor (%s) is reading alpha "
+                        "but it is missing from fragment output.",
+                        descriptor.blend->color.srcFactor, descriptor.blend->color.dstFactor);
     }
 
     return {};
 }
 
-MaybeError ValidateCompatibilityColorTargetState(
-    const uint8_t firstColorTargetIndex,
-    const ColorTargetState* const firstColorTargetState,
-    const uint8_t targetIndex,
-    const ColorTargetState* target) {
+MaybeError ValidateFramebufferInput(
+    DeviceBase* device,
+    const Format* format,
+    const EntryPointMetadata::FragmentRenderAttachmentInfo& inputVar) {
+    DAWN_INVALID_IF(inputVar.baseType != format->GetAspectInfo(Aspect::Color).baseType,
+                    "Color format (%s) base type (%s) doesn't match the fragment "
+                    "module input type (%s).",
+                    format->format, format->GetAspectInfo(Aspect::Color).baseType,
+                    inputVar.baseType);
+    DAWN_INVALID_IF(inputVar.componentCount != format->componentCount,
+                    "The fragment stage number of input components (%u) doesn't match the color "
+                    "format (%s) component count (%u).",
+                    inputVar.componentCount, format->format, format->componentCount);
+    return {};
+}
+
+MaybeError ValidateColorTargetStatesMatch(const uint8_t firstColorTargetIndex,
+                                          const ColorTargetState* const firstColorTargetState,
+                                          const uint8_t targetIndex,
+                                          const ColorTargetState* target) {
     DAWN_INVALID_IF(firstColorTargetState->writeMask != target->writeMask,
                     "targets[%u].writeMask (%s) does not match targets[%u].writeMask (%s).",
                     targetIndex, target->writeMask, firstColorTargetIndex,
@@ -537,7 +547,7 @@
                                  const FragmentState* descriptor,
                                  const PipelineLayoutBase* layout,
                                  const DepthStencilState* depthStencil,
-                                 bool alphaToCoverageEnabled) {
+                                 const MultisampleState& multisample) {
     DAWN_INVALID_IF(descriptor->nextInChain != nullptr, "nextInChain must be nullptr.");
 
     DAWN_TRY_CONTEXT(ValidateProgrammableStage(device, descriptor->module, descriptor->entryPoint,
@@ -546,11 +556,6 @@
                      "validating fragment stage (%s, entryPoint: %s).", descriptor->module,
                      descriptor->entryPoint);
 
-    uint32_t maxColorAttachments = device->GetLimits().v1.maxColorAttachments;
-    DAWN_INVALID_IF(descriptor->targetCount > maxColorAttachments,
-                    "Number of targets (%u) exceeds the maximum (%u).", descriptor->targetCount,
-                    maxColorAttachments);
-
     const EntryPointMetadata& fragmentMetadata =
         descriptor->module->GetEntryPoint(descriptor->entryPoint);
 
@@ -569,41 +574,53 @@
                         depthStencil->format, descriptor->module, descriptor->entryPoint);
     }
 
-    uint8_t firstColorTargetIndex = 0;
-    const ColorTargetState* firstColorTargetState = nullptr;
-    ColorAttachmentFormats colorAttachmentFormats;
+    uint32_t maxColorAttachments = device->GetLimits().v1.maxColorAttachments;
+    DAWN_INVALID_IF(descriptor->targetCount > maxColorAttachments,
+                    "Number of targets (%u) exceeds the maximum (%u).", descriptor->targetCount,
+                    maxColorAttachments);
+    ityp::span<ColorAttachmentIndex, const ColorTargetState> targets(
+        descriptor->targets, ColorAttachmentIndex(uint8_t(descriptor->targetCount)));
 
-    for (ColorAttachmentIndex attachmentIndex(uint8_t(0));
-         attachmentIndex < ColorAttachmentIndex(static_cast<uint8_t>(descriptor->targetCount));
-         ++attachmentIndex) {
-        const uint8_t i = static_cast<uint8_t>(attachmentIndex);
-        const ColorTargetState* target = &descriptor->targets[i];
-
-        if (target->format != wgpu::TextureFormat::Undefined) {
-            DAWN_TRY_CONTEXT(
-                ValidateColorTargetState(device, target,
-                                         fragmentMetadata.fragmentOutputsWritten[attachmentIndex],
-                                         fragmentMetadata.fragmentOutputVariables[attachmentIndex]),
-                "validating targets[%u].", i);
-            colorAttachmentFormats->push_back(&device->GetValidInternalFormat(target->format));
-            if (device->IsCompatibilityMode()) {
-                if (!firstColorTargetState) {
-                    firstColorTargetState = target;
-                    firstColorTargetIndex = i;
-                } else {
-                    DAWN_TRY_CONTEXT(ValidateCompatibilityColorTargetState(
-                                         firstColorTargetIndex, firstColorTargetState, i, target),
-                                     "validating targets[%u] in compatibility mode.", i);
-                }
-            }
+    ityp::bitset<ColorAttachmentIndex, kMaxColorAttachments> targetMask;
+    for (ColorAttachmentIndex i{}; i < targets.size(); ++i) {
+        if (targets[i].format == wgpu::TextureFormat::Undefined) {
+            DAWN_INVALID_IF(targets[i].blend,
+                            "Color target[%u] blend state is set when the format is undefined.",
+                            static_cast<uint8_t>(i));
         } else {
-            DAWN_INVALID_IF(target->blend,
-                            "Color target[%u] blend state is set when the format is undefined.", i);
+            targetMask.set(i);
         }
     }
+
+    ColorAttachmentFormats colorAttachmentFormats;
+    for (ColorAttachmentIndex i : IterateBitSet(targetMask)) {
+        const Format* format;
+        DAWN_TRY_ASSIGN(format, device->GetInternalFormat(targets[i].format));
+
+        DAWN_TRY_CONTEXT(ValidateColorTargetState(device, targets[i], format,
+                                                  fragmentMetadata.fragmentOutputMask[i],
+                                                  fragmentMetadata.fragmentOutputVariables[i]),
+                         "validating targets[%u] framebuffer output.", static_cast<uint8_t>(i));
+        colorAttachmentFormats->push_back(&device->GetValidInternalFormat(targets[i].format));
+
+        if (fragmentMetadata.fragmentInputMask[i]) {
+            DAWN_TRY_CONTEXT(ValidateFramebufferInput(device, format,
+                                                      fragmentMetadata.fragmentInputVariables[i]),
+                             "validating targets[%u]'s framebuffer input.",
+                             static_cast<uint8_t>(i));
+        }
+    }
+
+    auto extraFramebufferInputs = fragmentMetadata.fragmentInputMask & ~targetMask;
+    DAWN_INVALID_IF(
+        extraFramebufferInputs.any(),
+        "Framebuffer input at index %u is used without a corresponding color target state.",
+        uint8_t(ityp::Sub(GetHighestBitIndexPlusOne(extraFramebufferInputs),
+                          ColorAttachmentIndex(uint8_t(1)))));
+
     DAWN_TRY(ValidateColorAttachmentBytesPerSample(device, colorAttachmentFormats));
 
-    if (alphaToCoverageEnabled) {
+    if (multisample.alphaToCoverageEnabled) {
         DAWN_INVALID_IF(fragmentMetadata.usesSampleMaskOutput,
                         "alphaToCoverageEnabled is true when the sample_mask builtin is a "
                         "pipeline output of fragment stage of %s.",
@@ -621,12 +638,34 @@
             format->format);
     }
 
+    if (multisample.count != 1) {
+        DAWN_INVALID_IF(fragmentMetadata.fragmentInputMask.any(),
+                        "Framebuffer inputs are used when the sample count (%u) is not 1.",
+                        multisample.count);
+    }
+
     if (device->IsCompatibilityMode()) {
         DAWN_INVALID_IF(
             fragmentMetadata.usesSampleMaskOutput,
             "sample_mask is not supported in compatibility mode in the fragment stage (%s, "
             "entryPoint: %s)",
             descriptor->module, descriptor->entryPoint);
+
+        // Check that all the color target states match.
+        ColorAttachmentIndex firstColorTargetIndex{};
+        const ColorTargetState* firstColorTargetState = nullptr;
+        for (ColorAttachmentIndex i : IterateBitSet(targetMask)) {
+            if (!firstColorTargetState) {
+                firstColorTargetState = &targets[i];
+                firstColorTargetIndex = i;
+                continue;
+            }
+
+            DAWN_TRY_CONTEXT(ValidateColorTargetStatesMatch(
+                                 static_cast<uint8_t>(firstColorTargetIndex), firstColorTargetState,
+                                 static_cast<uint8_t>(i), &targets[i]),
+                             "validating targets in compatibility mode.");
+        }
     }
 
     return {};
@@ -735,8 +774,7 @@
 
     if (descriptor->fragment != nullptr) {
         DAWN_TRY_CONTEXT(ValidateFragmentState(device, descriptor->fragment, descriptor->layout,
-                                               descriptor->depthStencil,
-                                               descriptor->multisample.alphaToCoverageEnabled),
+                                               descriptor->depthStencil, descriptor->multisample),
                          "validating fragment state.");
 
         bool hasStorageAttachments =
diff --git a/src/dawn/native/ShaderModule.cpp b/src/dawn/native/ShaderModule.cpp
index 9af60c5..0a99e7b 100644
--- a/src/dawn/native/ShaderModule.cpp
+++ b/src/dawn/native/ShaderModule.cpp
@@ -625,7 +625,9 @@
     metadata->usedInterStageVariables.resize(maxInterStageShaderVariables);
     metadata->interStageVariables.resize(maxInterStageShaderVariables);
 
+    // Vertex shader specific reflection.
     if (metadata->stage == SingleShaderStage::Vertex) {
+        // Vertex input reflection.
         for (const auto& inputVar : entryPoint.input_variables) {
             uint32_t unsanitizedLocation = inputVar.attributes.location.value();
             if (DelayedInvalidIf(unsanitizedLocation >= maxVertexAttributes,
@@ -641,6 +643,7 @@
             metadata->usedVertexInputs.set(location);
         }
 
+        // Vertex ouput (inter-stage variables) reflection.
         uint32_t totalInterStageShaderComponents = 0;
         for (const auto& outputVar : entryPoint.output_variables) {
             EntryPointMetadata::InterStageVariableInfo variable;
@@ -668,6 +671,7 @@
             metadata->interStageVariables[location] = variable;
         }
 
+        // Other vertex metadata.
         metadata->totalInterStageShaderComponents = totalInterStageShaderComponents;
         DelayedInvalidIf(totalInterStageShaderComponents > maxInterStageShaderComponents,
                          "Total vertex output components count (%u) exceeds the maximum (%u).",
@@ -677,38 +681,44 @@
         metadata->usesInstanceIndex = entryPoint.instance_index_used;
     }
 
+    // Fragment shader specific reflection.
     if (metadata->stage == SingleShaderStage::Fragment) {
         uint32_t totalInterStageShaderComponents = 0;
+
+        // Fragment input (inter-stage variables) reflection.
         for (const auto& inputVar : entryPoint.input_variables) {
-            if (inputVar.attributes.location.has_value()) {
-                uint32_t location = inputVar.attributes.location.value();
-                EntryPointMetadata::InterStageVariableInfo variable;
-                variable.name = inputVar.variable_name;
-                DAWN_TRY_ASSIGN(variable.baseType, TintComponentTypeToInterStageComponentType(
-                                                       inputVar.component_type));
-                DAWN_TRY_ASSIGN(
-                    variable.componentCount,
-                    TintCompositionTypeToInterStageComponentCount(inputVar.composition_type));
-                DAWN_TRY_ASSIGN(
-                    variable.interpolationType,
-                    TintInterpolationTypeToInterpolationType(inputVar.interpolation_type));
-                DAWN_TRY_ASSIGN(variable.interpolationSampling,
-                                TintInterpolationSamplingToInterpolationSamplingType(
-                                    inputVar.interpolation_sampling));
-                totalInterStageShaderComponents += variable.componentCount;
-
-                if (DelayedInvalidIf(location >= maxInterStageShaderVariables,
-                                     "Fragment input variable \"%s\" has a location (%u) that "
-                                     "is greater than or equal to (%u).",
-                                     inputVar.name, location, maxInterStageShaderVariables)) {
-                    continue;
-                }
-
-                metadata->usedInterStageVariables[location] = true;
-                metadata->interStageVariables[location] = variable;
+            // Skip over @color framebuffer fetch, it is handled below.
+            if (!inputVar.attributes.location.has_value()) {
+                DAWN_ASSERT(inputVar.attributes.color.has_value());
+                continue;
             }
+
+            uint32_t location = inputVar.attributes.location.value();
+            EntryPointMetadata::InterStageVariableInfo variable;
+            variable.name = inputVar.variable_name;
+            DAWN_TRY_ASSIGN(variable.baseType,
+                            TintComponentTypeToInterStageComponentType(inputVar.component_type));
+            DAWN_TRY_ASSIGN(variable.componentCount, TintCompositionTypeToInterStageComponentCount(
+                                                         inputVar.composition_type));
+            DAWN_TRY_ASSIGN(variable.interpolationType,
+                            TintInterpolationTypeToInterpolationType(inputVar.interpolation_type));
+            DAWN_TRY_ASSIGN(variable.interpolationSampling,
+                            TintInterpolationSamplingToInterpolationSamplingType(
+                                inputVar.interpolation_sampling));
+            totalInterStageShaderComponents += variable.componentCount;
+
+            if (DelayedInvalidIf(location >= maxInterStageShaderVariables,
+                                 "Fragment input variable \"%s\" has a location (%u) that "
+                                 "is greater than or equal to (%u).",
+                                 inputVar.name, location, maxInterStageShaderVariables)) {
+                continue;
+            }
+
+            metadata->usedInterStageVariables[location] = true;
+            metadata->interStageVariables[location] = variable;
         }
 
+        // Other fragment metadata
         if (entryPoint.front_facing_used) {
             totalInterStageShaderComponents += 1;
         }
@@ -726,9 +736,10 @@
                          "Total fragment input components count (%u) exceeds the maximum (%u).",
                          totalInterStageShaderComponents, maxInterStageShaderComponents);
 
+        // Fragment output reflection.
         uint32_t maxColorAttachments = limits.v1.maxColorAttachments;
         for (const auto& outputVar : entryPoint.output_variables) {
-            EntryPointMetadata::FragmentOutputVariableInfo variable;
+            EntryPointMetadata::FragmentRenderAttachmentInfo variable;
             DAWN_TRY_ASSIGN(variable.baseType,
                             TintComponentTypeToTextureComponentType(outputVar.component_type));
             DAWN_TRY_ASSIGN(variable.componentCount, TintCompositionTypeToInterStageComponentCount(
@@ -745,9 +756,40 @@
 
             ColorAttachmentIndex attachment(static_cast<uint8_t>(unsanitizedAttachment));
             metadata->fragmentOutputVariables[attachment] = variable;
-            metadata->fragmentOutputsWritten.set(attachment);
+            metadata->fragmentOutputMask.set(attachment);
         }
 
+        // Fragment input reflection.
+        for (const auto& inputVar : entryPoint.input_variables) {
+            if (!inputVar.attributes.color.has_value()) {
+                continue;
+            }
+
+            // Tint should disallow using @color(N) without the respective enable, which is gated
+            // on the extension.
+            DAWN_ASSERT(device->HasFeature(Feature::FramebufferFetch));
+
+            EntryPointMetadata::FragmentRenderAttachmentInfo variable;
+            DAWN_TRY_ASSIGN(variable.baseType,
+                            TintComponentTypeToTextureComponentType(inputVar.component_type));
+            DAWN_TRY_ASSIGN(variable.componentCount, TintCompositionTypeToInterStageComponentCount(
+                                                         inputVar.composition_type));
+            DAWN_ASSERT(variable.componentCount <= 4);
+
+            uint32_t unsanitizedAttachment = inputVar.attributes.color.value();
+            if (DelayedInvalidIf(unsanitizedAttachment >= maxColorAttachments,
+                                 "Fragment input variable \"%s\" has a location (%u) that "
+                                 "exceeds the maximum (%u).",
+                                 inputVar.name, unsanitizedAttachment, maxColorAttachments)) {
+                continue;
+            }
+
+            ColorAttachmentIndex attachment(static_cast<uint8_t>(unsanitizedAttachment));
+            metadata->fragmentInputVariables[attachment] = variable;
+            metadata->fragmentInputMask.set(attachment);
+        }
+
+        // Fragment PLS reflection.
         if (!entryPoint.pixel_local_members.empty()) {
             metadata->usesPixelLocal = true;
             metadata->pixelLocalBlockSize =
@@ -762,6 +804,7 @@
         }
     }
 
+    // Generic resource binding reflection.
     for (const tint::inspector::ResourceBinding& resource :
          inspector->GetResourceBindings(entryPoint.name)) {
         ShaderBindingInfo info;
@@ -843,6 +886,7 @@
                         resource.binding, resource.bind_group);
     }
 
+    // Reflection of combined sampler and texture uses.
     auto samplerTextureUses = inspector->GetSamplerTextureUses(entryPoint.name);
     metadata->samplerTexturePairs.reserve(samplerTextureUses.Length());
     std::transform(samplerTextureUses.begin(), samplerTextureUses.end(),
diff --git a/src/dawn/native/ShaderModule.h b/src/dawn/native/ShaderModule.h
index f813085..1df1170 100644
--- a/src/dawn/native/ShaderModule.h
+++ b/src/dawn/native/ShaderModule.h
@@ -211,14 +211,19 @@
         vertexInputBaseTypes;
     ityp::bitset<VertexAttributeLocation, kMaxVertexAttributes> usedVertexInputs;
 
-    // An array to record the basic types (float, int and uint) of the fragment shader outputs.
-    struct FragmentOutputVariableInfo {
+    // An array to record the basic types (float, int and uint) of the fragment shader framebuffer
+    // input/outputs (inputs being "framebuffer fetch").
+    struct FragmentRenderAttachmentInfo {
         TextureComponentType baseType;
         uint8_t componentCount;
     };
-    ityp::array<ColorAttachmentIndex, FragmentOutputVariableInfo, kMaxColorAttachments>
+    ityp::array<ColorAttachmentIndex, FragmentRenderAttachmentInfo, kMaxColorAttachments>
         fragmentOutputVariables;
-    ityp::bitset<ColorAttachmentIndex, kMaxColorAttachments> fragmentOutputsWritten;
+    ityp::bitset<ColorAttachmentIndex, kMaxColorAttachments> fragmentOutputMask;
+
+    ityp::array<ColorAttachmentIndex, FragmentRenderAttachmentInfo, kMaxColorAttachments>
+        fragmentInputVariables;
+    ityp::bitset<ColorAttachmentIndex, kMaxColorAttachments> fragmentInputMask;
 
     struct InterStageVariableInfo {
         std::string name;
diff --git a/src/dawn/native/metal/RenderPipelineMTL.mm b/src/dawn/native/metal/RenderPipelineMTL.mm
index 124ad5d..d4a2a76 100644
--- a/src/dawn/native/metal/RenderPipelineMTL.mm
+++ b/src/dawn/native/metal/RenderPipelineMTL.mm
@@ -401,13 +401,13 @@
             mStagesRequiringStorageBufferLength |= wgpu::ShaderStage::Fragment;
         }
 
-        const auto& fragmentOutputsWritten = fragmentStage.metadata->fragmentOutputsWritten;
+        const auto& fragmentOutputMask = fragmentStage.metadata->fragmentOutputMask;
         for (ColorAttachmentIndex i : IterateBitSet(GetColorAttachmentsMask())) {
             descriptorMTL.colorAttachments[static_cast<uint8_t>(i)].pixelFormat =
                 MetalPixelFormat(GetDevice(), GetColorAttachmentFormat(i));
             const ColorTargetState* descriptor = GetColorTargetState(i);
             ComputeBlendDesc(descriptorMTL.colorAttachments[static_cast<uint8_t>(i)], descriptor,
-                             fragmentOutputsWritten[i]);
+                             fragmentOutputMask[i]);
         }
 
         if (GetAttachmentState()->HasPixelLocalStorage()) {
diff --git a/src/dawn/native/vulkan/RenderPipelineVk.cpp b/src/dawn/native/vulkan/RenderPipelineVk.cpp
index d05cfb6..c7ad09f 100644
--- a/src/dawn/native/vulkan/RenderPipelineVk.cpp
+++ b/src/dawn/native/vulkan/RenderPipelineVk.cpp
@@ -488,13 +488,13 @@
             blend.colorWriteMask = 0;
         }
 
-        const auto& fragmentOutputsWritten =
-            GetStage(SingleShaderStage::Fragment).metadata->fragmentOutputsWritten;
+        const auto& fragmentOutputMask =
+            GetStage(SingleShaderStage::Fragment).metadata->fragmentOutputMask;
         ColorAttachmentIndex highestColorAttachmentIndexPlusOne =
             GetHighestBitIndexPlusOne(GetColorAttachmentsMask());
         for (ColorAttachmentIndex i : IterateBitSet(GetColorAttachmentsMask())) {
             const ColorTargetState* target = GetColorTargetState(i);
-            colorBlendAttachments[i] = ComputeColorDesc(target, fragmentOutputsWritten[i]);
+            colorBlendAttachments[i] = ComputeColorDesc(target, fragmentOutputMask[i]);
         }
 
         colorBlend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
diff --git a/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp b/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
index de01a26..03ac33b 100644
--- a/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
@@ -2511,5 +2511,120 @@
     }
 }
 
+class FramebufferFetchFeatureTest : public RenderPipelineValidationTest {
+  protected:
+    WGPUDevice CreateTestDevice(native::Adapter dawnAdapter,
+                                wgpu::DeviceDescriptor descriptor) override {
+        wgpu::FeatureName requiredFeatures[1] = {wgpu::FeatureName::FramebufferFetch};
+        descriptor.requiredFeatures = requiredFeatures;
+        descriptor.requiredFeatureCount = 1;
+        return dawnAdapter.CreateDevice(&descriptor);
+    }
+};
+
+// Test that the framebuffer input must have a corresponding color target.
+TEST_F(FramebufferFetchFeatureTest, FramebufferInputMustHaveColorTarget) {
+    uint32_t colorIndices[] = {0, 1, 2, kMaxColorAttachments - 1, kMaxColorAttachments};
+    for (uint32_t colorIndex : colorIndices) {
+        std::ostringstream fsStream;
+        fsStream << R"(
+            enable chromium_experimental_framebuffer_fetch;
+            @fragment fn main(@color()"
+                 << colorIndex << R"() in : vec4f) -> @location(1) vec4f {
+                return in;
+            }
+        )";
+
+        utils::ComboRenderPipelineDescriptor desc;
+        desc.vertex.module = vsModule;
+        desc.vertex.entryPoint = "main";
+        desc.cFragment.module = utils::CreateShaderModule(device, fsStream.str().c_str());
+        desc.cFragment.entryPoint = "main";
+        desc.cFragment.targetCount = 2;
+        desc.cTargets[0].format = wgpu::TextureFormat::Undefined;
+        desc.cTargets[1].format = wgpu::TextureFormat::RGBA8Unorm;
+
+        // Only colorIndex 1 should work because it is the only index with a color target.
+        if (colorIndex == 1) {
+            device.CreateRenderPipeline(&desc);
+        } else {
+            ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&desc));
+        }
+    }
+}
+
+// Test that the framebuffer fetch requires multisampling to be off.
+TEST_F(FramebufferFetchFeatureTest, MultisampleDisallowed) {
+    utils::ComboRenderPipelineDescriptor desc;
+    desc.vertex.entryPoint = "main";
+    desc.vertex.module = vsModule;
+    desc.cFragment.entryPoint = "main";
+    desc.cFragment.module = utils::CreateShaderModule(device, R"(
+        enable chromium_experimental_framebuffer_fetch;
+        @fragment fn main(@color(0) in : vec4f) -> @location(0) vec4f {
+            return in;
+        }
+    )");
+
+    desc.multisample.count = 1;
+    device.CreateRenderPipeline(&desc);
+
+    desc.multisample.count = 4;
+    ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&desc));
+}
+
+// Test that the framebuffer fetch type matches the texture format exactly.
+TEST_F(FramebufferFetchFeatureTest, InputMatchesFormat) {
+    struct ValidPair {
+        const char* type;
+        wgpu::TextureFormat format;
+    };
+
+    std::array<ValidPair, 9> validPairs = {{
+        {"f32", wgpu::TextureFormat::R32Float},
+        {"vec2f", wgpu::TextureFormat::RG16Float},
+        {"vec4f", wgpu::TextureFormat::RGBA8Unorm},
+        {"u32", wgpu::TextureFormat::R32Uint},
+        {"vec2u", wgpu::TextureFormat::RG16Uint},
+        {"vec4u", wgpu::TextureFormat::RGBA8Uint},
+        {"i32", wgpu::TextureFormat::R32Sint},
+        {"vec2i", wgpu::TextureFormat::RG16Sint},
+        {"vec4i", wgpu::TextureFormat::RGBA8Sint},
+    }};
+
+    for (size_t i = 0; i < validPairs.size(); i++) {
+        wgpu::TextureFormat format = validPairs[i].format;
+        const char* outputType = validPairs[i].type;
+
+        for (size_t j = 0; j < validPairs.size(); j++) {
+            const char* inputType = validPairs[j].type;
+
+            std::ostringstream fsStream;
+            fsStream << R"(
+                enable chromium_experimental_framebuffer_fetch;
+                @fragment fn main(@color(0) in : )"
+                     << inputType << R"() -> @location(0) )" << outputType << R"( {
+                    var res : )"
+                     << outputType << R"(;
+                    return res;
+                }
+            )";
+
+            utils::ComboRenderPipelineDescriptor desc;
+            desc.vertex.module = vsModule;
+            desc.vertex.entryPoint = "main";
+            desc.cFragment.module = utils::CreateShaderModule(device, fsStream.str().c_str());
+            desc.cFragment.entryPoint = "main";
+            desc.cTargets[0].format = format;
+
+            if (i == j) {
+                device.CreateRenderPipeline(&desc);
+            } else {
+                ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&desc));
+            }
+        }
+    }
+}
+
 }  // anonymous namespace
 }  // namespace dawn
diff --git a/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp b/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp
index 67fe96d..d713d92 100644
--- a/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp
@@ -784,20 +784,22 @@
 };
 
 struct WGSLExtensionInfo {
-    const char* WGSLName;
+    const char* wgslName;
     // Is this WGSL extension experimental, i.e. guarded by AllowUnsafeAPIs toggle
     bool isExperimental;
     // The WebGPU feature that required to enable this extension, set to nullptr if no feature
     // required.
-    const char* RequiredFeatureName;
+    const char* requiredFeatureName;
 };
 
 constexpr struct WGSLExtensionInfo kExtensions[] = {
     {"f16", false, "shader-f16"},
     {"chromium_experimental_dp4a", true, "chromium-experimental-dp4a"},
     {"chromium_experimental_subgroups", true, "chromium-experimental-subgroups"},
+    {"chromium_experimental_pixel_local", true, "pixel-local-storage-coherent"},
     {"chromium_disable_uniformity_analysis", true, nullptr},
     {"chromium_internal_dual_source_blending", true, "dual-source-blending"},
+    {"chromium_experimental_framebuffer_fetch", true, "framebuffer-fetch"},
 
     // Currently the following WGSL extensions are not enabled under any situation.
     /*
@@ -825,13 +827,13 @@
 TEST_F(ShaderModuleExtensionValidationTestSafeNoFeature,
        OnlyStableExtensionsRequiringNoFeatureAllowed) {
     for (auto& extension : kExtensions) {
-        std::string wgsl = std::string("enable ") + extension.WGSLName + R"(;
+        std::string wgsl = std::string("enable ") + extension.wgslName + R"(;
 
 @compute @workgroup_size(1) fn main() {})";
 
         // On a safe device with no feature required, only stable extensions requiring no features
         // are allowed.
-        if (!extension.isExperimental && !extension.RequiredFeatureName) {
+        if (!extension.isExperimental && !extension.requiredFeatureName) {
             utils::CreateShaderModule(device, wgsl.c_str());
         } else {
             ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, wgsl.c_str()));
@@ -857,13 +859,13 @@
 TEST_F(ShaderModuleExtensionValidationTestUnsafeNoFeature,
        OnlyExtensionsRequiringNoFeatureAllowed) {
     for (auto& extension : kExtensions) {
-        std::string wgsl = std::string("enable ") + extension.WGSLName + R"(;
+        std::string wgsl = std::string("enable ") + extension.wgslName + R"(;
 
 @compute @workgroup_size(1) fn main() {})";
 
         // On an unsafe device with no feature required, only extensions requiring no features are
         // allowed.
-        if (!extension.RequiredFeatureName) {
+        if (!extension.requiredFeatureName) {
             utils::CreateShaderModule(device, wgsl.c_str());
         } else {
             ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, wgsl.c_str()));
@@ -888,7 +890,7 @@
 
 TEST_F(ShaderModuleExtensionValidationTestSafeAllFeatures, OnlyStableExtensionsAllowed) {
     for (auto& extension : kExtensions) {
-        std::string wgsl = std::string("enable ") + extension.WGSLName + R"(;
+        std::string wgsl = std::string("enable ") + extension.wgslName + R"(;
 
 @compute @workgroup_size(1) fn main() {})";
 
@@ -918,7 +920,7 @@
 
 TEST_F(ShaderModuleExtensionValidationTestUnsafeAllFeatures, AllExtensionsAllowed) {
     for (auto& extension : kExtensions) {
-        std::string wgsl = std::string("enable ") + extension.WGSLName + R"(;
+        std::string wgsl = std::string("enable ") + extension.wgslName + R"(;
 
 @compute @workgroup_size(1) fn main() {})";
 
diff --git a/src/dawn/wire/SupportedFeatures.cpp b/src/dawn/wire/SupportedFeatures.cpp
index 5a3f191..1097dca 100644
--- a/src/dawn/wire/SupportedFeatures.cpp
+++ b/src/dawn/wire/SupportedFeatures.cpp
@@ -84,6 +84,7 @@
         case WGPUFeatureName_PixelLocalStorageCoherent:
         case WGPUFeatureName_PixelLocalStorageNonCoherent:
         case WGPUFeatureName_Norm16TextureFormats:
+        case WGPUFeatureName_FramebufferFetch:
             return true;
     }