vulkan: Implement coherent FramebufferFetch

This adds FramebufferFetch support to the Vulkan backend when the
VK_EXT_rasterization_order_attachment_access extension is available. The
general approach is that when FramebufferFetch is enabled the main
subpass will have one input attachment corresponding to each color
attachment. If a pipeline uses the @color attribute a VkDescriptorSet
with an input attachment for each color attachment is created/bound
that Tint can sample them. Tint already has the logic to handle the
color attribute and sample attachments.

The rasterization order extension ensures that any previous draws in the
same render pass are complete before pixel values are sampled.

FramebufferFetchHelper encapsulates the logic to allocate
VkDescriptorSets and VkDescriptorSetLayouts for this feature.

Bug: 42241389
Change-Id: I369886c75577966663b181800a71c4d08f9541bc
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/290895
Commit-Queue: Kyle Charbonneau <kylechar@google.com>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Auto-Submit: Kyle Charbonneau <kylechar@google.com>
Reviewed-by: Brandon Jones <bajones@chromium.org>
diff --git a/src/dawn/native/BUILD.gn b/src/dawn/native/BUILD.gn
index e9e0b22..89eb96e 100644
--- a/src/dawn/native/BUILD.gn
+++ b/src/dawn/native/BUILD.gn
@@ -887,6 +887,8 @@
       "vulkan/Forward.h",
       "vulkan/FramebufferCache.cpp",
       "vulkan/FramebufferCache.h",
+      "vulkan/FramebufferFetchHelper.cpp",
+      "vulkan/FramebufferFetchHelper.h",
       "vulkan/MemoryTypeSelector.cpp",
       "vulkan/MemoryTypeSelector.h",
       "vulkan/PhysicalDeviceVk.cpp",
diff --git a/src/dawn/native/CMakeLists.txt b/src/dawn/native/CMakeLists.txt
index 1eaea37..3e2501f 100644
--- a/src/dawn/native/CMakeLists.txt
+++ b/src/dawn/native/CMakeLists.txt
@@ -769,6 +769,7 @@
         "vulkan/FencedDeleter.h"
         "vulkan/Forward.h"
         "vulkan/FramebufferCache.h"
+        "vulkan/FramebufferFetchHelper.h"
         "vulkan/MemoryTypeSelector.h"
         "vulkan/PhysicalDeviceVk.h"
         "vulkan/PipelineCacheVk.h"
@@ -814,6 +815,7 @@
         "vulkan/DeviceVk.cpp"
         "vulkan/FencedDeleter.cpp"
         "vulkan/FramebufferCache.cpp"
+        "vulkan/FramebufferFetchHelper.cpp"
         "vulkan/MemoryTypeSelector.cpp"
         "vulkan/PhysicalDeviceVk.cpp"
         "vulkan/PipelineCacheVk.cpp"
diff --git a/src/dawn/native/PassResourceUsage.h b/src/dawn/native/PassResourceUsage.h
index 736ad61..2cb45c5 100644
--- a/src/dawn/native/PassResourceUsage.h
+++ b/src/dawn/native/PassResourceUsage.h
@@ -104,6 +104,7 @@
     // Storage to track the occlusion queries used during the pass.
     std::vector<QuerySetBase*> querySets;
     std::vector<ityp::vector<QueryIndex, bool>> queryAvailabilities;
+    bool usesFramebufferFetch = false;
 };
 
 using RenderPassUsages = ityp::vector<PassIndex, RenderPassResourceUsage>;
diff --git a/src/dawn/native/PassResourceUsageTracker.cpp b/src/dawn/native/PassResourceUsageTracker.cpp
index 7e6e06d..266a8a6 100644
--- a/src/dawn/native/PassResourceUsageTracker.cpp
+++ b/src/dawn/native/PassResourceUsageTracker.cpp
@@ -317,9 +317,13 @@
 RenderPassResourceUsageTracker& RenderPassResourceUsageTracker::operator=(
     RenderPassResourceUsageTracker&&) = default;
 
+void RenderPassResourceUsageTracker::MarkFramebufferFetchUsed() {
+    mFramebufferFetchUsed = true;
+}
+
 RenderPassResourceUsage RenderPassResourceUsageTracker::AcquireResourceUsage() {
     RenderPassResourceUsage result;
-    *static_cast<SyncScopeResourceUsage*>(&result) = AcquireSyncScopeUsage();
+    static_cast<SyncScopeResourceUsage&>(result) = AcquireSyncScopeUsage();
 
     result.querySets.reserve(mQueryAvailabilities.size());
     result.queryAvailabilities.reserve(mQueryAvailabilities.size());
@@ -331,6 +335,8 @@
 
     mQueryAvailabilities.clear();
 
+    result.usesFramebufferFetch = mFramebufferFetchUsed;
+
     return result;
 }
 
diff --git a/src/dawn/native/PassResourceUsageTracker.h b/src/dawn/native/PassResourceUsageTracker.h
index e67a9cc..33af987 100644
--- a/src/dawn/native/PassResourceUsageTracker.h
+++ b/src/dawn/native/PassResourceUsageTracker.h
@@ -114,6 +114,8 @@
     void TrackQueryAvailability(QuerySetBase* querySet, QueryIndex queryIndex);
     const QueryAvailabilityMap& GetQueryAvailabilityMap() const;
 
+    void MarkFramebufferFetchUsed();
+
     RenderPassResourceUsage AcquireResourceUsage();
 
   private:
@@ -123,6 +125,8 @@
 
     // Tracks queries used in the render pass to validate that they aren't written twice.
     QueryAvailabilityMap mQueryAvailabilities;
+
+    bool mFramebufferFetchUsed = false;
 };
 
 }  // namespace dawn::native
diff --git a/src/dawn/native/RenderEncoderBase.cpp b/src/dawn/native/RenderEncoderBase.cpp
index c892848..6722487 100644
--- a/src/dawn/native/RenderEncoderBase.cpp
+++ b/src/dawn/native/RenderEncoderBase.cpp
@@ -567,6 +567,10 @@
 
             mCommandBufferState.SetRenderPipeline(pipeline);
 
+            if (pipeline->UsesFramebufferFetch()) {
+                mUsageTracker.MarkFramebufferFetchUsed();
+            }
+
             SetRenderPipelineCmd* cmd =
                 allocator->Allocate<SetRenderPipelineCmd>(Command::SetRenderPipeline);
             cmd->pipeline = pipeline;
diff --git a/src/dawn/native/RenderPassEncoder.cpp b/src/dawn/native/RenderPassEncoder.cpp
index 86c846b..5ac5dbf 100644
--- a/src/dawn/native/RenderPassEncoder.cpp
+++ b/src/dawn/native/RenderPassEncoder.cpp
@@ -370,6 +370,10 @@
                 bundles[i] = renderBundles[i];
 
                 mUsageTracker.MergeResourceUsages(bundles[i]->GetResourceUsage());
+                if (bundles[i]->GetResourceUsage().usesFramebufferFetch) {
+                    mUsageTracker.MarkFramebufferFetchUsed();
+                }
+
                 if (IsValidationEnabled()) {
                     mIndirectDrawMetadata.AddBundle(renderBundles[i]);
                 }
diff --git a/src/dawn/native/vulkan/CommandBufferVk.cpp b/src/dawn/native/vulkan/CommandBufferVk.cpp
index 3c10950..450e445 100644
--- a/src/dawn/native/vulkan/CommandBufferVk.cpp
+++ b/src/dawn/native/vulkan/CommandBufferVk.cpp
@@ -33,6 +33,7 @@
 #include "dawn/native/vulkan/CommandBufferVk.h"
 
 #include <algorithm>
+#include <concepts>
 #include <limits>
 #include <memory>
 #include <utility>
@@ -47,6 +48,7 @@
 #include "dawn/native/Commands.h"
 #include "dawn/native/DynamicUploader.h"
 #include "dawn/native/EnumMaskIterator.h"
+#include "dawn/native/Error.h"
 #include "dawn/native/ExternalTexture.h"
 #include "dawn/native/ImmediateConstantsTracker.h"
 #include "dawn/native/RenderBundle.h"
@@ -57,6 +59,7 @@
 #include "dawn/native/vulkan/DeviceVk.h"
 #include "dawn/native/vulkan/FencedDeleter.h"
 #include "dawn/native/vulkan/FramebufferCache.h"
+#include "dawn/native/vulkan/FramebufferFetchHelper.h"
 #include "dawn/native/vulkan/PhysicalDeviceVk.h"
 #include "dawn/native/vulkan/PipelineLayoutVk.h"
 #include "dawn/native/vulkan/QuerySetVk.h"
@@ -234,6 +237,13 @@
         mVkLayout = pipeline->GetVkLayout();
         mImmediateConstantSize = pipeline->GetImmediateConstantSize();
         mUsesResourceTable = pipeline->GetLayout()->UsesResourceTable();
+        if constexpr (std::derived_from<VkPipelineType, RenderPipelineBase>) {
+            mFramebufferFetchEnabled = pipeline->UsesFramebufferFetch();
+        }
+    }
+
+    void SetFramebufferFetchDescriptorSet(VkDescriptorSet set) {
+        mFramebufferFetchDescriptorSet = set;
     }
 
     void SetResourceTable(ResourceTable* resourceTable) { mResourceTable = resourceTable; }
@@ -258,12 +268,31 @@
         // Also clear the last resource table so it gets rebound below.
         if (mLastAppliedImmediateConstantSize != mImmediateConstantSize) {
             dirtyBindGroups = mBindGroupLayoutsMask;
+            mLastFramebufferFetchEnabled = false;
             mLastResourceTable = nullptr;
         }
 
+        BindGroupIndex startOfBindGroups{0u};
+        if (mLastFramebufferFetchEnabled != mFramebufferFetchEnabled) {
+            // When the use of framebuffer fetch changes between pipelines, dirty all bind groups
+            // because they shift by 1.
+            dirtyBindGroups = mBindGroupLayoutsMask;
+            mLastResourceTable = nullptr;
+
+            if (mFramebufferFetchEnabled) {
+                DAWN_ASSERT(mFramebufferFetchDescriptorSet != VK_NULL_HANDLE);
+                vk.CmdBindDescriptorSets(commandBuffer, bindPoint, mVkLayout,
+                                         static_cast<uint32_t>(startOfBindGroups), 1,
+                                         &*mFramebufferFetchDescriptorSet, 0, nullptr);
+            }
+        }
+        if (mFramebufferFetchEnabled) {
+            ++startOfBindGroups;
+        }
+
         // When the usage of the resource table changes between pipelines, or the resource table
         // itself is changed, dirty all bind groups because they shift by 1 (the resource table
-        // occupies VkDescriptorSet 0).
+        // occupies VkDescriptorSet 0 or 1).
         if (mLastUsesResourceTable != mUsesResourceTable || mLastResourceTable != mResourceTable) {
             dirtyBindGroups = mBindGroupLayoutsMask;
 
@@ -271,11 +300,14 @@
             if (mUsesResourceTable) {
                 DAWN_ASSERT(mResourceTable != nullptr);
                 VkDescriptorSet set = mResourceTable->GetHandle();
-                vk.CmdBindDescriptorSets(commandBuffer, bindPoint, mVkLayout, 0, 1, &*set, 0,
+                vk.CmdBindDescriptorSets(commandBuffer, bindPoint, mVkLayout,
+                                         static_cast<uint32_t>(startOfBindGroups), 1, &*set, 0,
                                          nullptr);
             }
         }
-        BindGroupIndex startOfBindGroups{mUsesResourceTable ? 1u : 0u};
+        if (mUsesResourceTable) {
+            ++startOfBindGroups;
+        }
 
         for (BindGroupIndex dirtyBGIndex : dirtyBindGroups) {
             VkDescriptorSet set = GetDescriptorSet(dirtyBGIndex);
@@ -295,6 +327,7 @@
         mLastAppliedImmediateConstantSize = mImmediateConstantSize;
         mLastUsesResourceTable = mUsesResourceTable;
         mLastResourceTable = mResourceTable;
+        mLastFramebufferFetchEnabled = mFramebufferFetchEnabled;
     }
 
     RAW_PTR_EXCLUSION VkPipelineLayout mVkLayout;
@@ -302,6 +335,9 @@
     raw_ptr<ResourceTable> mResourceTable = nullptr;
     bool mLastUsesResourceTable = false;
     bool mUsesResourceTable = false;
+    bool mFramebufferFetchEnabled = false;
+    bool mLastFramebufferFetchEnabled = false;
+    VkDescriptorSet mFramebufferFetchDescriptorSet = VK_NULL_HANDLE;
     uint32_t mLastAppliedImmediateConstantSize = 0;
     uint32_t mImmediateConstantSize = 0;
 };
@@ -1282,9 +1318,8 @@
             case Command::BeginRenderPass: {
                 BeginRenderPassCmd* cmd = mCommands.NextCommand<BeginRenderPassCmd>();
 
-                DAWN_TRY(PrepareResourcesForRenderPass(
-                    device, recordingContext,
-                    GetResourceUsages().renderPasses[nextRenderPassNumber]));
+                auto& usage = GetResourceUsages().renderPasses[nextRenderPassNumber];
+                DAWN_TRY(PrepareResourcesForRenderPass(device, recordingContext, usage));
 
                 DAWN_TRY(LazyClearRenderPassAttachments(
                     device, cmd,
@@ -1300,7 +1335,7 @@
                                                       wgpu::ShaderStage::None, range);
                         return {};
                     }));
-                DAWN_TRY(RecordRenderPass(recordingContext, cmd));
+                DAWN_TRY(RecordRenderPass(recordingContext, cmd, usage));
 
                 recordingContext->hasRecordedRenderPass = true;
                 nextRenderPassNumber++;
@@ -1635,7 +1670,8 @@
 }
 
 MaybeError CommandBuffer::RecordRenderPass(CommandRecordingContext* recordingContext,
-                                           BeginRenderPassCmd* renderPassCmd) {
+                                           BeginRenderPassCmd* renderPassCmd,
+                                           const RenderPassResourceUsage& usage) {
     Device* device = ToBackend(GetDevice());
     VkCommandBuffer commands = recordingContext->commandBuffer;
 
@@ -1653,6 +1689,13 @@
 
     RenderPassState state(device->fn, recordingContext);
 
+    if (usage.usesFramebufferFetch) {
+        VkDescriptorSet set;
+        DAWN_TRY_ASSIGN(
+            set, device->GetFramebufferFetchHelper()->GetDescriptorsForRenderPass(renderPassCmd));
+        state.descriptorSets.SetFramebufferFetchDescriptorSet(set);
+    }
+
     // Set the default value for the dynamic state
     {
         device->fn.CmdSetLineWidth(commands, 1.0f);
@@ -1689,7 +1732,6 @@
         state.immediates.SetClampFragDepth(0.0, 1.0);
     }
 
-
     // Tracks the number of commands that do significant GPU work (a draw or query write) this pass.
     uint32_t workCommandCount = 0;
 
diff --git a/src/dawn/native/vulkan/CommandBufferVk.h b/src/dawn/native/vulkan/CommandBufferVk.h
index e00cb19..77396bc 100644
--- a/src/dawn/native/vulkan/CommandBufferVk.h
+++ b/src/dawn/native/vulkan/CommandBufferVk.h
@@ -66,7 +66,8 @@
                                  BeginComputePassCmd* computePass,
                                  const ComputePassResourceUsage& resourceUsages);
     MaybeError RecordRenderPass(CommandRecordingContext* recordingContext,
-                                BeginRenderPassCmd* renderPass);
+                                BeginRenderPassCmd* renderPass,
+                                const RenderPassResourceUsage& usage);
     MaybeError RecordCopyImageWithTemporaryBuffer(CommandRecordingContext* recordingContext,
                                                   const TextureCopy& srcCopy,
                                                   const TextureCopy& dstCopy,
diff --git a/src/dawn/native/vulkan/DeviceVk.cpp b/src/dawn/native/vulkan/DeviceVk.cpp
index bce88cd..4dd4a96 100644
--- a/src/dawn/native/vulkan/DeviceVk.cpp
+++ b/src/dawn/native/vulkan/DeviceVk.cpp
@@ -50,6 +50,7 @@
 #include "dawn/native/vulkan/ComputePipelineVk.h"
 #include "dawn/native/vulkan/FencedDeleter.h"
 #include "dawn/native/vulkan/FramebufferCache.h"
+#include "dawn/native/vulkan/FramebufferFetchHelper.h"
 #include "dawn/native/vulkan/PhysicalDeviceVk.h"
 #include "dawn/native/vulkan/PipelineCacheVk.h"
 #include "dawn/native/vulkan/PipelineLayoutVk.h"
@@ -194,6 +195,10 @@
         DAWN_TRY_ASSIGN(mResourceTableLayout, ResourceTable::MakeDescriptorSetLayout(this));
     }
 
+    if (HasFeature(Feature::FramebufferFetch)) {
+        mFramebufferFetchHelper = std::make_unique<FramebufferFetchHelper>(this);
+    }
+
     return DeviceBase::Initialize(descriptor, std::move(queue));
 }
 
@@ -401,6 +406,10 @@
     return mResourceTableLayout;
 }
 
+FramebufferFetchHelper* Device::GetFramebufferFetchHelper() {
+    return mFramebufferFetchHelper.get();
+}
+
 Ref<FencedDeleter>& Device::GetFencedDeleter() {
     return mDeleter;
 }
@@ -524,6 +533,15 @@
         featuresChain.Add(&usedKnobs.demoteToHelperInvocationFeatures);
     }
 
+    if (HasFeature(Feature::FramebufferFetch)) {
+        DAWN_ASSERT(usedKnobs.HasExt(DeviceExt::RasterizationOrderAttachmentAccess));
+        auto& usedKnobFeature = usedKnobs.rasterizationOrderAttachmentAccessFeatures;
+        usedKnobFeature = mDeviceInfo.rasterizationOrderAttachmentAccessFeatures;
+        usedKnobFeature.rasterizationOrderDepthAttachmentAccess = VK_FALSE;
+        usedKnobFeature.rasterizationOrderStencilAttachmentAccess = VK_FALSE;
+        featuresChain.Add(&usedKnobs.rasterizationOrderAttachmentAccessFeatures);
+    }
+
     if (mDeviceInfo.HasExt(DeviceExt::ShaderIntegerDotProduct)) {
         DAWN_ASSERT(usedKnobs.HasExt(DeviceExt::ShaderIntegerDotProduct));
 
@@ -1034,6 +1052,8 @@
         mResourceTableLayout = VK_NULL_HANDLE;
     }
 
+    mFramebufferFetchHelper.reset();
+
     mDescriptorAllocatorsPendingDeallocation.Use([&](auto pending) {
         for (Ref<DescriptorSetAllocator>& allocator : pending->IterateUpTo(kMaxExecutionSerial)) {
             allocator->FinishDeallocation(kMaxExecutionSerial);
diff --git a/src/dawn/native/vulkan/DeviceVk.h b/src/dawn/native/vulkan/DeviceVk.h
index 5c74441..4feece8 100644
--- a/src/dawn/native/vulkan/DeviceVk.h
+++ b/src/dawn/native/vulkan/DeviceVk.h
@@ -53,6 +53,7 @@
 class BufferUploader;
 class FencedDeleter;
 class FramebufferCache;
+class FramebufferFetchHelper;
 class RenderPassCache;
 class ResourceMemoryAllocator;
 
@@ -85,6 +86,7 @@
     VkDevice GetVkDevice() const;
     uint32_t GetGraphicsQueueFamily() const;
     const VkDescriptorSetLayout& GetResourceTableLayout() const;
+    FramebufferFetchHelper* GetFramebufferFetchHelper();
 
     Ref<FencedDeleter>& GetFencedDeleter();
     FramebufferCache* GetFramebufferCache() const;
@@ -222,6 +224,7 @@
     uint32_t mMainQueueFamily = 0;
 
     VkDescriptorSetLayout mResourceTableLayout = VK_NULL_HANDLE;
+    std::unique_ptr<FramebufferFetchHelper> mFramebufferFetchHelper;
 
     // Entries can be appended without holding the device mutex.
     MutexProtected<SerialQueue<ExecutionSerial, Ref<DescriptorSetAllocator>>>
diff --git a/src/dawn/native/vulkan/FramebufferFetchHelper.cpp b/src/dawn/native/vulkan/FramebufferFetchHelper.cpp
new file mode 100644
index 0000000..8b73aee
--- /dev/null
+++ b/src/dawn/native/vulkan/FramebufferFetchHelper.cpp
@@ -0,0 +1,167 @@
+// Copyright 2026 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 "dawn/native/vulkan/FramebufferFetchHelper.h"
+
+#include <utility>
+
+#include "dawn/common/Log.h"
+#include "dawn/native/Error.h"
+#include "dawn/native/vulkan/DeviceVk.h"
+#include "dawn/native/vulkan/ShaderModuleVk.h"
+#include "dawn/native/vulkan/TextureVk.h"
+#include "dawn/native/vulkan/UtilsVulkan.h"
+#include "dawn/native/vulkan/VulkanError.h"
+#include "vulkan/vulkan_core.h"
+
+namespace dawn::native::vulkan {
+namespace {
+
+ResultOrError<VkDescriptorSetLayout> MakeFramebufferFetchLayout(Device* device,
+                                                                uint32_t attachmentCount) {
+    std::array<VkDescriptorSetLayoutBinding, kMaxColorAttachments> bindings;
+    for (uint32_t i = 0; i < attachmentCount; ++i) {
+        bindings[i].binding = i;
+        bindings[i].descriptorType = VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT;
+        bindings[i].descriptorCount = 1;
+        bindings[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
+        bindings[i].pImmutableSamplers = nullptr;
+    }
+
+    VkDescriptorSetLayoutCreateInfo createInfo;
+    createInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
+    createInfo.pNext = nullptr;
+    createInfo.flags = 0;
+    createInfo.bindingCount = attachmentCount;
+    createInfo.pBindings = bindings.data();
+
+    VkDescriptorSetLayout layout = VK_NULL_HANDLE;
+    DAWN_TRY(CheckVkSuccess(
+        device->fn.CreateDescriptorSetLayout(device->GetVkDevice(), &createInfo, nullptr, &*layout),
+        "CreateDescriptorSetLayout"));
+    return layout;
+}
+
+}  // namespace
+
+FramebufferFetchHelper::FramebufferFetchHelper(Device* device) : mDevice(device) {
+    DAWN_ASSERT(mDevice);
+}
+
+FramebufferFetchHelper::~FramebufferFetchHelper() {
+    for (uint32_t i = 0; i < kMaxColorAttachments; ++i) {
+        auto& holder = mHolders[i];
+        if (holder.layout != VK_NULL_HANDLE) {
+            mDevice->fn.DestroyDescriptorSetLayout(mDevice->GetVkDevice(), holder.layout, nullptr);
+            holder.layout = VK_NULL_HANDLE;
+        }
+        holder.allocator = nullptr;
+    }
+}
+
+ResultOrError<VkDescriptorSetLayout> FramebufferFetchHelper::GetLayout(uint32_t attachmentCount) {
+    DAWN_CHECK(attachmentCount > 0 && attachmentCount <= kMaxColorAttachments);
+    auto& holder = mHolders[attachmentCount - 1];
+
+    if (holder.layout == VK_NULL_HANDLE) {
+        DAWN_TRY_ASSIGN(holder.layout, MakeFramebufferFetchLayout(mDevice, attachmentCount));
+
+        absl::flat_hash_map<VkDescriptorType, uint32_t> descriptorCountPerType;
+        descriptorCountPerType[VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT] = attachmentCount;
+        holder.allocator =
+            DescriptorSetAllocator::Create(mDevice, std::move(descriptorCountPerType));
+    }
+
+    return holder.layout;
+}
+
+ResultOrError<VkDescriptorSet> FramebufferFetchHelper::GetDescriptorsForRenderPass(
+    const BeginRenderPassCmd* cmd) {
+    uint32_t attachmentCount = AttachmentCount(cmd->attachmentState->GetColorAttachmentsMask());
+    DAWN_CHECK(attachmentCount > 0);
+    auto& holder = mHolders[attachmentCount - 1];
+
+    // The descriptor set allocator is created along with the layout the first time a pipeline that
+    // uses framebuffer fetch with N color attachments is created.
+    DAWN_CHECK(holder.allocator);
+
+    // Needed to work around some quirks introduced by vulkan_platform.h which causes some platforms
+    // to hit compiler errors if Vulkan struct members are assigned to VK_NULL_HANDLE directly.
+    static const VkSampler nullSampler = VK_NULL_HANDLE;
+
+    DescriptorSetAllocation allocation;
+    DAWN_TRY_ASSIGN(allocation, holder.allocator->Allocate(holder.layout));
+    VkDescriptorSet set = allocation.set;
+
+    std::array<VkWriteDescriptorSet, kMaxColorAttachments> writes;
+    std::array<VkDescriptorImageInfo, kMaxColorAttachments> imageInfos;
+    uint32_t writeCount = 0;
+
+    for (auto i : cmd->attachmentState->GetColorAttachmentsMask()) {
+        auto& attachmentInfo = cmd->colorAttachments[i];
+        TextureView* textureView = ToBackend(attachmentInfo.view.Get());
+
+        VkImageView imageView;
+        if (textureView->GetDimension() == wgpu::TextureViewDimension::e3D) {
+            DAWN_TRY_ASSIGN(imageView,
+                            textureView->GetOrCreate2DViewOn3D(attachmentInfo.depthSlice));
+        } else {
+            imageView = textureView->GetHandle();
+        }
+        DAWN_ASSERT(imageView != VK_NULL_HANDLE);
+
+        auto& imageInfo = imageInfos[writeCount];
+        imageInfo.sampler = nullSampler;
+        imageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL;
+        imageInfo.imageView = imageView;
+
+        auto& write = writes[writeCount];
+        write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+        write.pNext = nullptr;
+        write.dstSet = set;
+        write.dstBinding = static_cast<uint32_t>(i);
+        write.dstArrayElement = 0;
+        write.descriptorCount = 1;
+        write.descriptorType = VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT;
+        write.pImageInfo = &imageInfo;
+        write.pBufferInfo = nullptr;
+        write.pTexelBufferView = nullptr;
+
+        writeCount++;
+    }
+    DAWN_ASSERT(writeCount == attachmentCount);
+
+    mDevice->fn.UpdateDescriptorSets(mDevice->GetVkDevice(), attachmentCount, writes.data(), 0,
+                                     nullptr);
+
+    // This will delete the VkDescriptorSet after the pending command serial has completed.
+    holder.allocator->Deallocate(&allocation);
+
+    return set;
+}
+
+}  // namespace dawn::native::vulkan
diff --git a/src/dawn/native/vulkan/FramebufferFetchHelper.h b/src/dawn/native/vulkan/FramebufferFetchHelper.h
new file mode 100644
index 0000000..5a65164
--- /dev/null
+++ b/src/dawn/native/vulkan/FramebufferFetchHelper.h
@@ -0,0 +1,70 @@
+// Copyright 2026 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_DAWN_NATIVE_VULKAN_FRAMEBUFFER_FETCH_H_
+#define SRC_DAWN_NATIVE_VULKAN_FRAMEBUFFER_FETCH_H_
+
+#include "dawn/common/Ref.h"
+#include "dawn/common/vulkan_platform.h"
+#include "dawn/native/Commands.h"
+#include "dawn/native/vulkan/DescriptorSetAllocator.h"
+#include "dawn/native/vulkan/ResourceTableVk.h"
+#include "dawn/native/vulkan/VulkanInfo.h"
+
+namespace dawn::native::vulkan {
+
+// Helper to create VkDescriptorSets for binding color attachments as input attachments in order
+// to implement FramebufferFetch.
+class FramebufferFetchHelper {
+  public:
+    explicit FramebufferFetchHelper(Device* device);
+    ~FramebufferFetchHelper();
+
+    // Returns layout suitable for a render pass with `attachmentCount` color attachments.
+    ResultOrError<VkDescriptorSetLayout> GetLayout(uint32_t attachmentCount);
+
+    // Returns a descriptor set that binds all render pass color attachments as input attachments.
+    // The returned descriptor set is only valid to use for the current render pass.
+    // It will be destroyed after the render pass is submitted to the GPU.
+    //
+    // Note there is a dependency that GetLayout() was called for appropriate number of input
+    // attachments already. This happens as part of creating the VkPipelineLayout.
+    ResultOrError<VkDescriptorSet> GetDescriptorsForRenderPass(const BeginRenderPassCmd* cmd);
+
+  private:
+    struct DescriptorSetHolder {
+        VkDescriptorSetLayout layout = VK_NULL_HANDLE;
+        Ref<DescriptorSetAllocator> allocator;
+    };
+
+    raw_ptr<Device> mDevice;
+    std::array<DescriptorSetHolder, kMaxColorAttachments> mHolders;
+};
+
+}  // namespace dawn::native::vulkan
+
+#endif  // SRC_DAWN_NATIVE_VULKAN_FRAMEBUFFER_FETCH_H_
diff --git a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
index a6618c5..28fdc00 100644
--- a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
+++ b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
@@ -330,6 +330,22 @@
         EnableFeature(Feature::DualSourceBlending);
     }
 
+    if (mDeviceInfo.HasExt(DeviceExt::RasterizationOrderAttachmentAccess) &&
+        mDeviceInfo.rasterizationOrderAttachmentAccessFeatures
+                .rasterizationOrderColorAttachmentAccess == VK_TRUE) {
+        // There are four possible ways FramebufferFetch can be supported. Currently only #1 is
+        // implemented.
+        //
+        // 1. Coherent with rasterization order extension.
+        // 2. Coherent without rasterization order extension but when GPU architecture supports
+        //    coherent input attachment reads. This needs a subpass self dependency to be added.
+        // 3. Non-coherent. This needs both a subpass self dependency and barriers to be
+        //    inserted before draws that use FramebufferFetch.
+        // 4. When dynamic rendering is used FramebufferFetch requires the dynamic rendering local
+        //    storage extension and barriers to be inserted before draws that use FramebufferFetch.
+        EnableFeature(Feature::FramebufferFetch);
+    }
+
     if (mDeviceInfo.features.shaderClipDistance == VK_TRUE) {
         EnableFeature(Feature::ClipDistances);
     }
diff --git a/src/dawn/native/vulkan/PipelineLayoutVk.cpp b/src/dawn/native/vulkan/PipelineLayoutVk.cpp
index 817742a..d3a4411 100644
--- a/src/dawn/native/vulkan/PipelineLayoutVk.cpp
+++ b/src/dawn/native/vulkan/PipelineLayoutVk.cpp
@@ -35,6 +35,7 @@
 #include "dawn/native/vulkan/BindGroupLayoutVk.h"
 #include "dawn/native/vulkan/DeviceVk.h"
 #include "dawn/native/vulkan/FencedDeleter.h"
+#include "dawn/native/vulkan/FramebufferFetchHelper.h"
 #include "dawn/native/vulkan/UtilsVulkan.h"
 #include "dawn/native/vulkan/VulkanError.h"
 
@@ -52,13 +53,22 @@
 ResultOrError<Ref<RefCountedVkHandle<VkPipelineLayout>>> PipelineLayout::CreateVkPipelineLayout(
     const Specialization& specialization) {
     // Compute the array of VkDescriptorSetLayouts that will be chained in the create info.
-    ityp::array<BindGroupIndex, VkDescriptorSetLayout, size_t(kMaxBindGroupsTyped) + 1> setLayouts;
+    ityp::array<BindGroupIndex, VkDescriptorSetLayout, size_t(kMaxBindGroupsTyped) + 2> setLayouts;
 
-    // The first VkDescriptorSetLayout is the one for the resource table if needed.
+    // The first VkDescriptorSetLayouts are the for framebuffer fetch and/or the resource table if
+    // needed.
     BindGroupIndex startOfBindGroups{0};
+    if (specialization.framebufferFetchAttachmentCount > 0) {
+        DAWN_TRY_ASSIGN(setLayouts[startOfBindGroups],
+                        ToBackend(GetDevice())
+                            ->GetFramebufferFetchHelper()
+                            ->GetLayout(specialization.framebufferFetchAttachmentCount));
+        ++startOfBindGroups;
+    }
+
     if (UsesResourceTable()) {
-        startOfBindGroups = BindGroupIndex(1);
-        setLayouts[BindGroupIndex(0)] = ToBackend(GetDevice())->GetResourceTableLayout();
+        setLayouts[startOfBindGroups] = ToBackend(GetDevice())->GetResourceTableLayout();
+        ++startOfBindGroups;
     }
 
     // The all the descriptor sets for BindGroupLayouts, including the empty BGLs.
diff --git a/src/dawn/native/vulkan/PipelineLayoutVk.h b/src/dawn/native/vulkan/PipelineLayoutVk.h
index 82c4efa..a07c521 100644
--- a/src/dawn/native/vulkan/PipelineLayoutVk.h
+++ b/src/dawn/native/vulkan/PipelineLayoutVk.h
@@ -53,13 +53,14 @@
     struct Specialization {
         PerBindGroup<BindGroupLayout::Specialization> bindGroups = {};
         uint32_t pushConstantBytes;
+        uint32_t framebufferFetchAttachmentCount = 0;
 
         template <typename H>
         friend H AbslHashValue(H h, const Specialization& s) {
             for (auto& bg : s.bindGroups) {
                 h = H::combine(std::move(h), bg);
             }
-            return H::combine(std::move(h), s.pushConstantBytes);
+            return H::combine(std::move(h), s.pushConstantBytes, s.framebufferFetchAttachmentCount);
         }
         bool operator==(const Specialization& other) const = default;
     };
diff --git a/src/dawn/native/vulkan/RenderPassCache.cpp b/src/dawn/native/vulkan/RenderPassCache.cpp
index 2a2be46..bddbd89 100644
--- a/src/dawn/native/vulkan/RenderPassCache.cpp
+++ b/src/dawn/native/vulkan/RenderPassCache.cpp
@@ -31,6 +31,7 @@
 #include <vector>
 
 #include "absl/container/inlined_vector.h"
+#include "dawn/common/Assert.h"
 #include "dawn/common/Enumerator.h"
 #include "dawn/common/HashUtils.h"
 #include "dawn/common/Log.h"
@@ -62,6 +63,7 @@
             colorAttachmentRefs[i] = defaultRef;
             resolveAttachmentRefs[i] = defaultRef;
             inputAttachmentRefs[i] = defaultRef;
+            framebufferFetchAttachmentRefs[i] = defaultRef;
         }
         depthStencilAttachmentRef = defaultRef;
 
@@ -72,6 +74,7 @@
     PerColorAttachment<VkAttachmentReference> colorAttachmentRefs;
     PerColorAttachment<VkAttachmentReference> resolveAttachmentRefs;
     PerColorAttachment<VkAttachmentReference> inputAttachmentRefs;
+    PerColorAttachment<VkAttachmentReference> framebufferFetchAttachmentRefs;
     VkAttachmentReference depthStencilAttachmentRef;
 
     std::array<VkAttachmentDescription, kMaxAttachmentCount> attachmentDescs = {};
@@ -98,6 +101,7 @@
             colorAttachmentRefs[i] = defaultRef;
             resolveAttachmentRefs[i] = defaultRef;
             inputAttachmentRefs[i] = defaultRef;
+            framebufferFetchAttachmentRefs[i] = defaultRef;
         }
         depthStencilAttachmentRef = defaultRef;
 
@@ -127,6 +131,7 @@
     PerColorAttachment<VkAttachmentReference2> colorAttachmentRefs;
     PerColorAttachment<VkAttachmentReference2> resolveAttachmentRefs;
     PerColorAttachment<VkAttachmentReference2> inputAttachmentRefs;
+    PerColorAttachment<VkAttachmentReference2> framebufferFetchAttachmentRefs;
     VkAttachmentReference2 depthStencilAttachmentRef;
 
     std::array<VkAttachmentDescription2, kMaxAttachmentCount> attachmentDescs = {};
@@ -140,6 +145,8 @@
 void InitializePassInfo(Device* device, const RenderPassCacheQuery& query, InfoType& passInfo) {
     VkSampleCountFlagBits vkSampleCount = VulkanSampleCount(query.sampleCount);
 
+    const bool framebufferFetchEnabled = device->HasFeature(Feature::FramebufferFetch);
+
     // The Vulkan subpasses want to know the layout of the attachments with VkAttachmentRef.
     // Precompute them as they must be pointer-chained in VkSubpassDescription.
     uint32_t attachmentCount = 0;
@@ -149,7 +156,13 @@
         auto& attachmentDesc = passInfo.attachmentDescs[attachmentCount];
 
         attachmentRef.attachment = attachmentCount;
-        attachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+        // If we can read from a color attachment it must use VK_IMAGE_LAYOUT_GENERAL for the color
+        // attachment description. As long as the initial/final layouts are
+        // VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, the choice of the VK_IMAGE_LAYOUT_GENERAL for
+        // the subpass layout is efficient; the few GPUs that treat VK_IMAGE_LAYOUT_GENERAL
+        // differently recognize this pattern and keep the internal layout optimal.
+        attachmentRef.layout = framebufferFetchEnabled ? VK_IMAGE_LAYOUT_GENERAL
+                                                       : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
 
         attachmentDesc.flags = 0;
         attachmentDesc.format = VulkanImageFormat(device, query.colorFormats[i]);
@@ -272,8 +285,30 @@
     auto& subpassDesc = passInfo.subpassDescs[subpassCount];
     subpassDesc.flags = 0;
     subpassDesc.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
-    subpassDesc.inputAttachmentCount = 0;
-    subpassDesc.pInputAttachments = nullptr;
+
+    if (framebufferFetchEnabled) {
+        // When framebuffer fetch is enable add a corresponding input attachment for each color
+        // attachment in case we need to read from it.
+        for (auto i : query.colorMask) {
+            auto& attachmentRef = passInfo.colorAttachmentRefs[i];
+
+            auto& inputRef = passInfo.framebufferFetchAttachmentRefs[i];
+            inputRef.attachment = attachmentRef.attachment;
+            inputRef.layout = VK_IMAGE_LAYOUT_GENERAL;
+            if constexpr (std::same_as<InfoType, RenderPassCreateInfo2>) {
+                inputRef.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+            }
+        }
+
+        subpassDesc.inputAttachmentCount = static_cast<uint8_t>(highestColorAttachmentIndexPlusOne);
+        subpassDesc.pInputAttachments = passInfo.framebufferFetchAttachmentRefs.data();
+
+        subpassDesc.flags |=
+            VK_SUBPASS_DESCRIPTION_RASTERIZATION_ORDER_ATTACHMENT_COLOR_ACCESS_BIT_EXT;
+    } else {
+        subpassDesc.inputAttachmentCount = 0;
+        subpassDesc.pInputAttachments = nullptr;
+    }
     subpassDesc.colorAttachmentCount = static_cast<uint8_t>(highestColorAttachmentIndexPlusOne);
     subpassDesc.pColorAttachments = passInfo.colorAttachmentRefs.data();
 
diff --git a/src/dawn/native/vulkan/RenderPipelineVk.cpp b/src/dawn/native/vulkan/RenderPipelineVk.cpp
index ed4727de..0cceb5b 100644
--- a/src/dawn/native/vulkan/RenderPipelineVk.cpp
+++ b/src/dawn/native/vulkan/RenderPipelineVk.cpp
@@ -408,9 +408,13 @@
     bool buildCacheKey =
         !GetDevice()->GetTogglesState().IsEnabled(Toggle::VulkanMonolithicPipelineCache);
 
-    Specialization specialization = {
-        .layout = {.pushConstantBytes = ToPushConstantBytes(mImmediateMask)},
-    };
+    Specialization specialization = {.layout = {
+                                         .pushConstantBytes = ToPushConstantBytes(mImmediateMask),
+                                     }};
+    if (UsesFramebufferFetch()) {
+        specialization.layout.framebufferFetchAttachmentCount =
+            AttachmentCount(GetColorAttachmentsMask());
+    }
 
     SpecializationResult r;
     DAWN_TRY_ASSIGN(r, InitializeSpecialization(specialization, buildCacheKey));
@@ -425,6 +429,10 @@
     Specialization&& specializationIn) {
     Specialization specialization = specializationIn;
     specialization.layout.pushConstantBytes = ToPushConstantBytes(mImmediateMask);
+    if (UsesFramebufferFetch()) {
+        specialization.layout.framebufferFetchAttachmentCount =
+            AttachmentCount(GetColorAttachmentsMask());
+    }
 
     if (auto it = mSpecializations.find(specialization); it != mSpecializations.end()) {
         return PipelineHandles{.pipeline = it->second.pipeline->Get(),
@@ -493,6 +501,7 @@
         .ycbcrExternalTextures = &specialization.ycbcrExternalTextures,
         .emitPointSize = GetPrimitiveTopology() == wgpu::PrimitiveTopology::PointList,
         .polyfillPixelCenter = NeedsPixelCenterPolyfill(),
+        .pipelineUsesFramebufferFetch = UsesFramebufferFetch(),
     }));
 
     // Add the fragment stage if present.
@@ -503,6 +512,7 @@
             .immediateMask = GetImmediateMask(),
             .ycbcrExternalTextures = &specialization.ycbcrExternalTextures,
             .polyfillPixelCenter = NeedsPixelCenterPolyfill(),
+            .pipelineUsesFramebufferFetch = UsesFramebufferFetch(),
             .needsMultisampledFramebufferFetch = UseSampleRateShading() && UsesFramebufferFetch(),
         }));
     }
@@ -562,8 +572,8 @@
     multisample.pNext = nullptr;
     multisample.flags = 0;
     multisample.rasterizationSamples = VulkanSampleCount(GetSampleCount());
-    multisample.sampleShadingEnable = VK_FALSE;
-    multisample.minSampleShading = 0.0f;
+    multisample.sampleShadingEnable = UsesFramebufferFetch() ? VK_TRUE : VK_FALSE;
+    multisample.minSampleShading = 1.0f;
     // VkPipelineMultisampleStateCreateInfo.pSampleMask is an array of length
     // ceil(rasterizationSamples / 32) and since we're passing a single uint32_t
     // we have to assert that this length is indeed 1.
@@ -605,6 +615,12 @@
         colorBlend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
         colorBlend.pNext = nullptr;
         colorBlend.flags = 0;
+
+        if (GetStage(SingleShaderStage::Fragment).metadata->fragmentInputMask.any()) {
+            colorBlend.flags |=
+                VK_PIPELINE_COLOR_BLEND_STATE_CREATE_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_BIT_EXT;
+        }
+
         // LogicOp isn't supported so we disable it.
         colorBlend.logicOpEnable = VK_FALSE;
         colorBlend.logicOp = VK_LOGIC_OP_CLEAR;
diff --git a/src/dawn/native/vulkan/ShaderModuleVk.cpp b/src/dawn/native/vulkan/ShaderModuleVk.cpp
index 534f0d1..b97369c 100644
--- a/src/dawn/native/vulkan/ShaderModuleVk.cpp
+++ b/src/dawn/native/vulkan/ShaderModuleVk.cpp
@@ -133,9 +133,20 @@
 #if TINT_BUILD_SPV_WRITER
     // Creation of module and spirv is deferred to this point when using tint generator
 
-    // The first VkDescriptorSetLayout is the one for the resource table if needed and pushes the
-    // bindings for all other bindgroups by 1.
+    // The first VkDescriptorSetLayout is the one for the framebuffer fetch and/or resource table if
+    // needed and pushes the bindings for all other bindgroups.
     BindGroupIndex startOfBindGroups{0};
+
+    std::unordered_map<uint32_t, tint::BindingPoint> framebuffer_fetch_bindings;
+    if (in.pipelineUsesFramebufferFetch) {
+        if (in.stage->metadata->fragmentInputMask.any()) {
+            for (uint32_t i = 0; i < kMaxColorAttachments; ++i) {
+                framebuffer_fetch_bindings[i] = {static_cast<uint32_t>(startOfBindGroups), i};
+            }
+        }
+        startOfBindGroups = startOfBindGroups + BindGroupIndex(1);
+    }
+
     std::optional<tint::ResourceTableConfig> resourceTableConfig = std::nullopt;
     if (in.layout->UsesResourceTable()) {
         startOfBindGroups = BindGroupIndex(1);
@@ -249,6 +260,7 @@
     };
     req.tintOptions.bindings = std::move(bindings);
     req.tintOptions.resource_table = std::move(resourceTableConfig);
+    req.tintOptions.colour_index_to_binding_point = std::move(framebuffer_fetch_bindings);
 
     req.tintOptions.disable_robustness = !GetDevice()->IsRobustnessEnabled();
     req.tintOptions.disable_workgroup_init =
diff --git a/src/dawn/native/vulkan/ShaderModuleVk.h b/src/dawn/native/vulkan/ShaderModuleVk.h
index 38de472..eefd10b 100644
--- a/src/dawn/native/vulkan/ShaderModuleVk.h
+++ b/src/dawn/native/vulkan/ShaderModuleVk.h
@@ -74,6 +74,7 @@
 
         bool emitPointSize = false;
         bool polyfillPixelCenter = false;
+        bool pipelineUsesFramebufferFetch = false;
         bool needsMultisampledFramebufferFetch = false;
     };
 
diff --git a/src/dawn/native/vulkan/TextureVk.cpp b/src/dawn/native/vulkan/TextureVk.cpp
index e3a87bb..42eb66d 100644
--- a/src/dawn/native/vulkan/TextureVk.cpp
+++ b/src/dawn/native/vulkan/TextureVk.cpp
@@ -683,10 +683,13 @@
             flags |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
         } else {
             flags |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
-            if (!format.IsMultiPlanar() && (usage & wgpu::TextureUsage::TextureBinding) &&
-                device->HasFeature(Feature::DawnLoadResolveTexture)) {
-                // Automatically set "input attachment" usage so that the texture would be
-                // used in ExpandResolveTexture subpass.
+
+            // Automatically set "input attachment" usage so that the texture can be used in
+            // ExpandResolveTexture subpass or with framebuffer fetch.
+            bool mayNeedToExpandTexture = !format.IsMultiPlanar() &&
+                                          usage & wgpu::TextureUsage::TextureBinding &&
+                                          device->HasFeature(Feature::DawnLoadResolveTexture);
+            if (mayNeedToExpandTexture || device->HasFeature(Feature::FramebufferFetch)) {
                 flags |= VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT;
             }
         }
diff --git a/src/dawn/native/vulkan/UtilsVulkan.cpp b/src/dawn/native/vulkan/UtilsVulkan.cpp
index 1342fa9..4825d87 100644
--- a/src/dawn/native/vulkan/UtilsVulkan.cpp
+++ b/src/dawn/native/vulkan/UtilsVulkan.cpp
@@ -72,6 +72,10 @@
     return static_cast<uint32_t>(immediates.count()) * kImmediateConstantElementByteSize;
 }
 
+uint32_t AttachmentCount(const ColorAttachmentMask& mask) {
+    return static_cast<uint32_t>(mask.count());
+}
+
 VkCompareOp ToVulkanCompareOp(wgpu::CompareFunction op) {
     switch (op) {
         case wgpu::CompareFunction::Never:
diff --git a/src/dawn/native/vulkan/UtilsVulkan.h b/src/dawn/native/vulkan/UtilsVulkan.h
index 48782d1..ae5af90 100644
--- a/src/dawn/native/vulkan/UtilsVulkan.h
+++ b/src/dawn/native/vulkan/UtilsVulkan.h
@@ -115,6 +115,7 @@
 };
 
 uint32_t ToPushConstantBytes(const ImmediateConstantMask& immediates);
+uint32_t AttachmentCount(const ColorAttachmentMask& mask);
 
 VkCompareOp ToVulkanCompareOp(wgpu::CompareFunction op);
 
diff --git a/src/dawn/native/vulkan/VulkanExtensions.cpp b/src/dawn/native/vulkan/VulkanExtensions.cpp
index 0f20c15..40f78205 100644
--- a/src/dawn/native/vulkan/VulkanExtensions.cpp
+++ b/src/dawn/native/vulkan/VulkanExtensions.cpp
@@ -155,6 +155,7 @@
     {DeviceExt::CooperativeMatrix, "VK_KHR_cooperative_matrix"},
     {DeviceExt::MultisampledRenderToSingleSampled, "VK_EXT_multisampled_render_to_single_sampled"},
     {DeviceExt::PhysicalDeviceDrm, "VK_EXT_physical_device_drm"},
+    {DeviceExt::RasterizationOrderAttachmentAccess, "VK_EXT_rasterization_order_attachment_access"},
 
     {DeviceExt::ExternalMemoryAndroidHardwareBuffer,
      "VK_ANDROID_external_memory_android_hardware_buffer"},
@@ -221,6 +222,7 @@
             case DeviceExt::ShaderBufferInt64Atomics:
             case DeviceExt::VulkanMemoryModel:
             case DeviceExt::CooperativeMatrix:
+            case DeviceExt::RasterizationOrderAttachmentAccess:
             case DeviceExt::ShaderFloatControls:
             case DeviceExt::DescriptorIndexing:
             case DeviceExt::CreateRenderPass2:
diff --git a/src/dawn/native/vulkan/VulkanExtensions.h b/src/dawn/native/vulkan/VulkanExtensions.h
index fa82cc4..455be6c 100644
--- a/src/dawn/native/vulkan/VulkanExtensions.h
+++ b/src/dawn/native/vulkan/VulkanExtensions.h
@@ -114,6 +114,7 @@
     CooperativeMatrix,
     MultisampledRenderToSingleSampled,
     PhysicalDeviceDrm,
+    RasterizationOrderAttachmentAccess,
 
     // External* extensions
     ExternalMemoryAndroidHardwareBuffer,
diff --git a/src/dawn/native/vulkan/VulkanInfo.cpp b/src/dawn/native/vulkan/VulkanInfo.cpp
index 2d87068..0538369 100644
--- a/src/dawn/native/vulkan/VulkanInfo.cpp
+++ b/src/dawn/native/vulkan/VulkanInfo.cpp
@@ -369,6 +369,12 @@
                             VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_PROPERTIES);
     }
 
+    if (info.extensions[DeviceExt::RasterizationOrderAttachmentAccess]) {
+        featuresChain.Add(
+            &info.rasterizationOrderAttachmentAccessFeatures,
+            VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_FEATURES_EXT);
+    }
+
     if (info.extensions[DeviceExt::DynamicRendering]) {
         featuresChain.Add(&info.dynamicRenderingFeatures,
                           VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR);
diff --git a/src/dawn/native/vulkan/VulkanInfo.h b/src/dawn/native/vulkan/VulkanInfo.h
index a6a5f48..25bf340 100644
--- a/src/dawn/native/vulkan/VulkanInfo.h
+++ b/src/dawn/native/vulkan/VulkanInfo.h
@@ -76,6 +76,8 @@
     VkPhysicalDeviceCooperativeMatrixFeaturesKHR cooperativeMatrixFeatures;
     VkPhysicalDeviceDescriptorIndexingFeatures descriptorIndexingFeatures;
     VkPhysicalDevicePipelineRobustnessFeatures pipelineRobustnessFeatures;
+    VkPhysicalDeviceRasterizationOrderAttachmentAccessFeaturesEXT
+        rasterizationOrderAttachmentAccessFeatures;
     VkPhysicalDeviceDynamicRenderingFeaturesKHR dynamicRenderingFeatures;
     VkPhysicalDeviceMultisampledRenderToSingleSampledFeaturesEXT
         multisampledRenderToSingleSampledFeatures;
diff --git a/src/dawn/tests/end2end/FramebufferFetchTests.cpp b/src/dawn/tests/end2end/FramebufferFetchTests.cpp
index 29805d2..07ed9cb 100644
--- a/src/dawn/tests/end2end/FramebufferFetchTests.cpp
+++ b/src/dawn/tests/end2end/FramebufferFetchTests.cpp
@@ -395,7 +395,12 @@
                       {0, 0});
 }
 
-DAWN_INSTANTIATE_TEST(FramebufferFetchTests, MetalBackend());
+// TODO(crbug.com/42241389): Add a test that uses FramebufferFetch and ResourceTable in the same
+// pipeline.
+
+DAWN_INSTANTIATE_TEST(FramebufferFetchTests,
+                      MetalBackend(),
+                      VulkanBackend({}, {"vulkan_use_dynamic_rendering"}));
 
 }  // anonymous namespace
 }  // namespace dawn