Implement LoadResolveTexture in vulkan.

Using 2 subpasses

Bug: dawn:1710
Change-Id: If1c2ddcbadc502388f01ba56ccd0fc6966a4b5b1
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/187562
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: Quyen Le <lehoangquyen@chromium.org>
Reviewed-by: Austin Eng <enga@chromium.org>
diff --git a/docs/dawn/features/dawn_load_resolve_texture.md b/docs/dawn/features/dawn_load_resolve_texture.md
index 7da8a3f..02b739b 100644
--- a/docs/dawn/features/dawn_load_resolve_texture.md
+++ b/docs/dawn/features/dawn_load_resolve_texture.md
@@ -74,3 +74,4 @@
     - If render pipeline's color target `i` has no `wgpu::ColorTargetStateExpandResolveTextureDawn` included, then the compatible render pass's attachment `i` **must not** have any resolve target.
  - Currently the `ExpandResolveTexture` LoadOp only works on color attachment, this could be changed in future.
  - The texture is not supported if it is not resolvable by WebGPU standard. This means this feature currently doesn't work with integer textures.
+ - Using `ExpandResolveTexture` load op on a multiplanar texture's view is not supported currently.
diff --git a/src/dawn/native/BUILD.gn b/src/dawn/native/BUILD.gn
index b64a7e8..f2ea39e 100644
--- a/src/dawn/native/BUILD.gn
+++ b/src/dawn/native/BUILD.gn
@@ -804,6 +804,8 @@
       "vulkan/RenderPassCache.h",
       "vulkan/RenderPipelineVk.cpp",
       "vulkan/RenderPipelineVk.h",
+      "vulkan/ResolveTextureLoadingUtilsVk.cpp",
+      "vulkan/ResolveTextureLoadingUtilsVk.h",
       "vulkan/ResourceHeapVk.cpp",
       "vulkan/ResourceHeapVk.h",
       "vulkan/ResourceMemoryAllocatorVk.cpp",
diff --git a/src/dawn/native/BindGroupLayoutInternal.cpp b/src/dawn/native/BindGroupLayoutInternal.cpp
index 2290810..711c9ec 100644
--- a/src/dawn/native/BindGroupLayoutInternal.cpp
+++ b/src/dawn/native/BindGroupLayoutInternal.cpp
@@ -143,7 +143,16 @@
         // viewDimension defaults to 2D if left undefined, needs validation otherwise.
         wgpu::TextureViewDimension viewDimension = wgpu::TextureViewDimension::e2D;
         if (texture.viewDimension != wgpu::TextureViewDimension::Undefined) {
-            DAWN_TRY(ValidateTextureViewDimension(texture.viewDimension));
+            switch (texture.viewDimension) {
+                case kInternalInputAttachmentDim:
+                    if (allowInternalBinding) {
+                        break;
+                    }
+                    // should return validation error.
+                    [[fallthrough]];
+                default:
+                    DAWN_TRY(ValidateTextureViewDimension(texture.viewDimension));
+            }
             viewDimension = texture.viewDimension;
         }
 
diff --git a/src/dawn/native/BindingInfo.cpp b/src/dawn/native/BindingInfo.cpp
index e9199eb..a2e997a 100644
--- a/src/dawn/native/BindingInfo.cpp
+++ b/src/dawn/native/BindingInfo.cpp
@@ -90,7 +90,10 @@
     } else if (entry->sampler.type != wgpu::SamplerBindingType::Undefined) {
         perStageBindingCountMember = &PerStageBindingCounts::samplerCount;
     } else if (entry->texture.sampleType != wgpu::TextureSampleType::Undefined) {
-        if (entry->texture.viewDimension != kInternalInputAttachmentDim) {
+        if (entry->texture.viewDimension == kInternalInputAttachmentDim) {
+            // Internal use only.
+            return;
+        } else {
             perStageBindingCountMember = &PerStageBindingCounts::sampledTextureCount;
         }
     } else if (entry->storageTexture.access != wgpu::StorageTextureAccess::Undefined) {
diff --git a/src/dawn/native/BlitColorToColorWithDraw.cpp b/src/dawn/native/BlitColorToColorWithDraw.cpp
index d2115b7..772167c 100644
--- a/src/dawn/native/BlitColorToColorWithDraw.cpp
+++ b/src/dawn/native/BlitColorToColorWithDraw.cpp
@@ -196,7 +196,7 @@
                                         RenderPassEncoder* renderEncoder,
                                         const RenderPassDescriptor* renderPassDescriptor) {
     DAWN_ASSERT(device->IsLockedByCurrentThreadIfNeeded());
-    DAWN_ASSERT(device->IsResolveTextureBlitWithDrawSupported());
+    DAWN_ASSERT(device->CanTextureLoadResolveTargetInTheSameRenderpass());
 
     BlitColorToColorWithDrawPipelineKey pipelineKey;
     for (uint8_t i = 0; i < renderPassDescriptor->colorAttachmentCount; ++i) {
diff --git a/src/dawn/native/BlitColorToColorWithDraw.h b/src/dawn/native/BlitColorToColorWithDraw.h
index ac5463a..6a47ab6 100644
--- a/src/dawn/native/BlitColorToColorWithDraw.h
+++ b/src/dawn/native/BlitColorToColorWithDraw.h
@@ -75,7 +75,7 @@
 //
 // The function assumes that the render pass is already started. It won't break the render pass,
 // just performing a draw call to blit.
-// This is only valid if the device's IsResolveTextureBlitWithDrawSupported() is true.
+// This is only valid if the device's CanTextureLoadResolveTargetInTheSameRenderpass() is true.
 MaybeError ExpandResolveTextureWithDraw(DeviceBase* device,
                                         RenderPassEncoder* renderEncoder,
                                         const RenderPassDescriptor* renderPassDescriptor);
diff --git a/src/dawn/native/CMakeLists.txt b/src/dawn/native/CMakeLists.txt
index 259a0a4..3178f05 100644
--- a/src/dawn/native/CMakeLists.txt
+++ b/src/dawn/native/CMakeLists.txt
@@ -679,6 +679,8 @@
         "vulkan/RenderPassCache.h"
         "vulkan/RenderPipelineVk.cpp"
         "vulkan/RenderPipelineVk.h"
+        "vulkan/ResolveTextureLoadingUtilsVk.cpp"
+        "vulkan/ResolveTextureLoadingUtilsVk.h"
         "vulkan/ResourceHeapVk.cpp"
         "vulkan/ResourceHeapVk.h"
         "vulkan/ResourceMemoryAllocatorVk.cpp"
diff --git a/src/dawn/native/CommandEncoder.cpp b/src/dawn/native/CommandEncoder.cpp
index e52910a..92556e4 100644
--- a/src/dawn/native/CommandEncoder.cpp
+++ b/src/dawn/native/CommandEncoder.cpp
@@ -509,6 +509,10 @@
                     colorAttachment.resolveTarget, wgpu::TextureUsage::TextureBinding,
                     wgpu::LoadOp::ExpandResolveTexture);
 
+    // TODO(42240662): multiplanar textures are not supported as resolve target.
+    // The RenderPassValidationState currently rejects such usage.
+    DAWN_ASSERT(!colorAttachment.resolveTarget->GetTexture()->GetFormat().IsMultiPlanar());
+
     validationState->SetWillExpandResolveTexture(true);
 
     return {};
@@ -930,9 +934,11 @@
 MaybeError ApplyExpandResolveTextureLoadOp(DeviceBase* device,
                                            RenderPassEncoder* renderPassEncoder,
                                            const RenderPassDescriptor* renderPassDescriptor) {
-    // TODO(dawn:1710): support loading resolve texture on platforms that don't support reading
-    // it in fragment shader such as vulkan.
-    DAWN_ASSERT(device->IsResolveTextureBlitWithDrawSupported());
+    // If backend doesn't support textureLoad on resolve targets, then it should handle the load op
+    // internally.
+    if (!device->CanTextureLoadResolveTargetInTheSameRenderpass()) {
+        return {};
+    }
 
     // Read implicit resolve texture in fragment shader and copy to the implicit MSAA attachment.
     return ExpandResolveTextureWithDraw(device, renderPassEncoder, renderPassDescriptor);
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index dcbd086..ad32632 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -261,14 +261,16 @@
     *outDescriptor = descriptor;
 
     if (outDescriptor->layout == nullptr) {
-        DAWN_TRY_ASSIGN(layoutRef, PipelineLayoutBase::CreateDefault(
-                                       device, {{
-                                                   SingleShaderStage::Compute,
-                                                   outDescriptor->compute.module,
-                                                   outDescriptor->compute.entryPoint,
-                                                   outDescriptor->compute.constantCount,
-                                                   outDescriptor->compute.constants,
-                                               }}));
+        DAWN_TRY_ASSIGN(layoutRef,
+                        PipelineLayoutBase::CreateDefault(device,
+                                                          {{
+                                                              SingleShaderStage::Compute,
+                                                              outDescriptor->compute.module,
+                                                              outDescriptor->compute.entryPoint,
+                                                              outDescriptor->compute.constantCount,
+                                                              outDescriptor->compute.constants,
+                                                          }},
+                                                          /*allowInternalBinding=*/false));
         outDescriptor->layout = layoutRef.Get();
     }
 
@@ -278,7 +280,8 @@
 ResultOrError<Ref<PipelineLayoutBase>> ValidateLayoutAndGetRenderPipelineDescriptorWithDefaults(
     DeviceBase* device,
     const RenderPipelineDescriptor& descriptor,
-    RenderPipelineDescriptor* outDescriptor) {
+    RenderPipelineDescriptor* outDescriptor,
+    bool allowInternalBinding) {
     Ref<PipelineLayoutBase> layoutRef;
     *outDescriptor = descriptor;
 
@@ -287,7 +290,8 @@
         // the pipeline will take another reference.
         DAWN_TRY_ASSIGN(layoutRef,
                         PipelineLayoutBase::CreateDefault(
-                            device, GetRenderStagesAndSetPlaceholderShader(device, &descriptor)));
+                            device, GetRenderStagesAndSetPlaceholderShader(device, &descriptor),
+                            allowInternalBinding));
         outDescriptor->layout = layoutRef.Get();
     }
 
@@ -1825,6 +1829,10 @@
     return mWGSLAllowedFeatures;
 }
 
+void DeviceBase::EnableAdditionalWGSLExtension(tint::wgsl::Extension extension) {
+    mWGSLAllowedFeatures.extensions.insert(extension);
+}
+
 bool DeviceBase::IsValidationEnabled() const {
     return !IsToggleEnabled(Toggle::SkipValidation);
 }
@@ -2162,12 +2170,14 @@
 }
 
 ResultOrError<Ref<RenderPipelineBase>> DeviceBase::CreateRenderPipeline(
-    const RenderPipelineDescriptor* descriptor) {
+    const RenderPipelineDescriptor* descriptor,
+    bool allowInternalBinding) {
     // If a pipeline layout is not specified, we cannot use cached pipelines.
     bool useCache = descriptor->layout != nullptr;
 
     Ref<RenderPipelineBase> uninitializedRenderPipeline;
-    DAWN_TRY_ASSIGN(uninitializedRenderPipeline, CreateUninitializedRenderPipeline(descriptor));
+    DAWN_TRY_ASSIGN(uninitializedRenderPipeline,
+                    CreateUninitializedRenderPipeline(descriptor, allowInternalBinding));
 
     if (useCache) {
         Ref<RenderPipelineBase> cachedRenderPipeline =
@@ -2190,7 +2200,8 @@
 }
 
 ResultOrError<Ref<RenderPipelineBase>> DeviceBase::CreateUninitializedRenderPipeline(
-    const RenderPipelineDescriptor* descriptor) {
+    const RenderPipelineDescriptor* descriptor,
+    bool allowInternalBinding) {
     DAWN_TRY(ValidateIsAlive());
     if (IsValidationEnabled()) {
         DAWN_TRY(ValidateRenderPipelineDescriptor(this, descriptor));
@@ -2205,7 +2216,7 @@
     Ref<PipelineLayoutBase> layoutRef;
     RenderPipelineDescriptor appliedDescriptor;
     DAWN_TRY_ASSIGN(layoutRef, ValidateLayoutAndGetRenderPipelineDescriptorWithDefaults(
-                                   this, *descriptor, &appliedDescriptor));
+                                   this, *descriptor, &appliedDescriptor, allowInternalBinding));
 
     return CreateUninitializedRenderPipelineImpl(Unpack(&appliedDescriptor));
 }
@@ -2462,7 +2473,7 @@
     return false;
 }
 
-bool DeviceBase::IsResolveTextureBlitWithDrawSupported() const {
+bool DeviceBase::CanTextureLoadResolveTargetInTheSameRenderpass() const {
     return false;
 }
 
diff --git a/src/dawn/native/Device.h b/src/dawn/native/Device.h
index 6fc5dec..2d395a4 100644
--- a/src/dawn/native/Device.h
+++ b/src/dawn/native/Device.h
@@ -220,9 +220,11 @@
     ResultOrError<Ref<RenderBundleEncoder>> CreateRenderBundleEncoder(
         const RenderBundleEncoderDescriptor* descriptor);
     ResultOrError<Ref<RenderPipelineBase>> CreateRenderPipeline(
-        const RenderPipelineDescriptor* descriptor);
+        const RenderPipelineDescriptor* descriptor,
+        bool allowInternalBinding = false);
     ResultOrError<Ref<RenderPipelineBase>> CreateUninitializedRenderPipeline(
-        const RenderPipelineDescriptor* descriptor);
+        const RenderPipelineDescriptor* descriptor,
+        bool allowInternalBinding = false);
     ResultOrError<Ref<SamplerBase>> CreateSampler(const SamplerDescriptor* descriptor = nullptr);
     ResultOrError<Ref<ShaderModuleBase>> CreateShaderModule(
         const ShaderModuleDescriptor* descriptor,
@@ -398,9 +400,9 @@
     // See https://crbug.com/dawn/161
     virtual bool ShouldApplyIndexBufferOffsetToFirstIndex() const;
 
-    // Whether the backend supports blitting the resolve texture with draw calls in the same render
-    // pass that it will be resolved into.
-    virtual bool IsResolveTextureBlitWithDrawSupported() const;
+    // Whether the backend can use textureLoad() on a resolve target in the same render pass that it
+    // will be resolved into.
+    virtual bool CanTextureLoadResolveTargetInTheSameRenderpass() const;
 
     bool HasFeature(Feature feature) const;
 
@@ -467,6 +469,8 @@
     void DestroyObjects();
     void Destroy();
 
+    void EnableAdditionalWGSLExtension(tint::wgsl::Extension extension);
+
     virtual MaybeError GetAHardwareBufferPropertiesImpl(
         void* handle,
         AHardwareBufferProperties* properties) const {
@@ -630,7 +634,8 @@
 ResultOrError<Ref<PipelineLayoutBase>> ValidateLayoutAndGetRenderPipelineDescriptorWithDefaults(
     DeviceBase* device,
     const RenderPipelineDescriptor& descriptor,
-    RenderPipelineDescriptor* outDescriptor);
+    RenderPipelineDescriptor* outDescriptor,
+    bool allowInternalBinding = false);
 
 class IgnoreLazyClearCountScope : public NonMovable, public StackAllocated {
   public:
diff --git a/src/dawn/native/PipelineLayout.cpp b/src/dawn/native/PipelineLayout.cpp
index 0f45480..64e8816 100644
--- a/src/dawn/native/PipelineLayout.cpp
+++ b/src/dawn/native/PipelineLayout.cpp
@@ -161,7 +161,8 @@
 // static
 ResultOrError<Ref<PipelineLayoutBase>> PipelineLayoutBase::CreateDefault(
     DeviceBase* device,
-    std::vector<StageAndDescriptor> stages) {
+    std::vector<StageAndDescriptor> stages,
+    bool allowInternalBinding) {
     using EntryMap = std::map<BindingNumber, BindGroupLayoutEntry>;
 
     // Merges two entries at the same location, if they are allowed to be merged.
@@ -271,8 +272,8 @@
 
     // Creates the BGL from the entries for a stage, checking it is valid.
     auto CreateBGL = [](DeviceBase* device, const EntryMap& entries,
-                        PipelineCompatibilityToken pipelineCompatibilityToken)
-        -> ResultOrError<Ref<BindGroupLayoutBase>> {
+                        PipelineCompatibilityToken pipelineCompatibilityToken,
+                        bool allowInternalBinding) -> ResultOrError<Ref<BindGroupLayoutBase>> {
         std::vector<BindGroupLayoutEntry> entryVec;
         entryVec.reserve(entries.size());
         for (auto& [_, entry] : entries) {
@@ -284,8 +285,8 @@
         desc.entryCount = entryVec.size();
 
         if (device->IsValidationEnabled()) {
-            DAWN_TRY_CONTEXT(ValidateBindGroupLayoutDescriptor(device, &desc), "validating %s",
-                             &desc);
+            DAWN_TRY_CONTEXT(ValidateBindGroupLayoutDescriptor(device, &desc, allowInternalBinding),
+                             "validating %s", &desc);
         }
         return device->GetOrCreateBindGroupLayout(&desc, pipelineCompatibilityToken);
     };
@@ -350,8 +351,9 @@
     BindGroupIndex pipelineBGLCount = BindGroupIndex(0);
     PerBindGroup<Ref<BindGroupLayoutBase>> bindGroupLayouts = {};
     for (auto group : Range(kMaxBindGroupsTyped)) {
-        DAWN_TRY_ASSIGN(bindGroupLayouts[group],
-                        CreateBGL(device, entryData[group], pipelineCompatibilityToken));
+        DAWN_TRY_ASSIGN(
+            bindGroupLayouts[group],
+            CreateBGL(device, entryData[group], pipelineCompatibilityToken, allowInternalBinding));
         if (entryData[group].size() != 0) {
             pipelineBGLCount = ityp::PlusOne(group);
         }
diff --git a/src/dawn/native/PipelineLayout.h b/src/dawn/native/PipelineLayout.h
index b8a66c1..a288304 100644
--- a/src/dawn/native/PipelineLayout.h
+++ b/src/dawn/native/PipelineLayout.h
@@ -83,7 +83,8 @@
     static Ref<PipelineLayoutBase> MakeError(DeviceBase* device, const char* label);
     static ResultOrError<Ref<PipelineLayoutBase>> CreateDefault(
         DeviceBase* device,
-        std::vector<StageAndDescriptor> stages);
+        std::vector<StageAndDescriptor> stages,
+        bool allowInternalBinding);
 
     ObjectType GetType() const override;
 
diff --git a/src/dawn/native/d3d11/DeviceD3D11.cpp b/src/dawn/native/d3d11/DeviceD3D11.cpp
index 9747f21..2430bb6 100644
--- a/src/dawn/native/d3d11/DeviceD3D11.cpp
+++ b/src/dawn/native/d3d11/DeviceD3D11.cpp
@@ -408,7 +408,7 @@
     return DeviceBase::GetBufferCopyOffsetAlignmentForDepthStencil();
 }
 
-bool Device::IsResolveTextureBlitWithDrawSupported() const {
+bool Device::CanTextureLoadResolveTargetInTheSameRenderpass() const {
     return true;
 }
 
diff --git a/src/dawn/native/d3d11/DeviceD3D11.h b/src/dawn/native/d3d11/DeviceD3D11.h
index c13f12b..332dcdb 100644
--- a/src/dawn/native/d3d11/DeviceD3D11.h
+++ b/src/dawn/native/d3d11/DeviceD3D11.h
@@ -75,7 +75,7 @@
     float GetTimestampPeriodInNS() const override;
     bool MayRequireDuplicationOfIndirectParameters() const override;
     uint64_t GetBufferCopyOffsetAlignmentForDepthStencil() const override;
-    bool IsResolveTextureBlitWithDrawSupported() const override;
+    bool CanTextureLoadResolveTargetInTheSameRenderpass() const override;
     void SetLabelImpl() override;
 
     void DisposeKeyedMutex(ComPtr<IDXGIKeyedMutex> dxgiKeyedMutex) override;
diff --git a/src/dawn/native/metal/DeviceMTL.h b/src/dawn/native/metal/DeviceMTL.h
index 846b276..44e8f8f 100644
--- a/src/dawn/native/metal/DeviceMTL.h
+++ b/src/dawn/native/metal/DeviceMTL.h
@@ -78,7 +78,7 @@
 
     float GetTimestampPeriodInNS() const override;
 
-    bool IsResolveTextureBlitWithDrawSupported() const override;
+    bool CanTextureLoadResolveTargetInTheSameRenderpass() const override;
 
     bool UseCounterSamplingAtCommandBoundary() const;
     bool UseCounterSamplingAtStageBoundary() const;
diff --git a/src/dawn/native/metal/DeviceMTL.mm b/src/dawn/native/metal/DeviceMTL.mm
index cb3de76..2a58e4e 100644
--- a/src/dawn/native/metal/DeviceMTL.mm
+++ b/src/dawn/native/metal/DeviceMTL.mm
@@ -393,7 +393,7 @@
     return mTimestampPeriod;
 }
 
-bool Device::IsResolveTextureBlitWithDrawSupported() const {
+bool Device::CanTextureLoadResolveTargetInTheSameRenderpass() const {
     return true;
 }
 
diff --git a/src/dawn/native/null/DeviceNull.cpp b/src/dawn/native/null/DeviceNull.cpp
index c0720268..83030b5 100644
--- a/src/dawn/native/null/DeviceNull.cpp
+++ b/src/dawn/native/null/DeviceNull.cpp
@@ -574,7 +574,7 @@
     return 1.0f;
 }
 
-bool Device::IsResolveTextureBlitWithDrawSupported() const {
+bool Device::CanTextureLoadResolveTargetInTheSameRenderpass() const {
     return true;
 }
 
diff --git a/src/dawn/native/null/DeviceNull.h b/src/dawn/native/null/DeviceNull.h
index c47de81..a9190ca 100644
--- a/src/dawn/native/null/DeviceNull.h
+++ b/src/dawn/native/null/DeviceNull.h
@@ -138,7 +138,7 @@
 
     float GetTimestampPeriodInNS() const override;
 
-    bool IsResolveTextureBlitWithDrawSupported() const override;
+    bool CanTextureLoadResolveTargetInTheSameRenderpass() const override;
 
   private:
     using DeviceBase::DeviceBase;
diff --git a/src/dawn/native/vulkan/CommandBufferVk.cpp b/src/dawn/native/vulkan/CommandBufferVk.cpp
index a3ad777..9f7dd8b 100644
--- a/src/dawn/native/vulkan/CommandBufferVk.cpp
+++ b/src/dawn/native/vulkan/CommandBufferVk.cpp
@@ -49,6 +49,7 @@
 #include "dawn/native/vulkan/QueueVk.h"
 #include "dawn/native/vulkan/RenderPassCache.h"
 #include "dawn/native/vulkan/RenderPipelineVk.h"
+#include "dawn/native/vulkan/ResolveTextureLoadingUtilsVk.h"
 #include "dawn/native/vulkan/TextureVk.h"
 #include "dawn/native/vulkan/UtilsVulkan.h"
 #include "dawn/native/vulkan/VulkanError.h"
@@ -389,7 +390,9 @@
 
         query.SetSampleCount(renderPass->attachmentState->GetSampleCount());
 
-        DAWN_TRY_ASSIGN(renderPassVK, device->GetRenderPassCache()->GetRenderPass(query));
+        RenderPassCache::RenderPassInfo renderPassInfo;
+        DAWN_TRY_ASSIGN(renderPassInfo, device->GetRenderPassCache()->GetRenderPass(query));
+        renderPassVK = renderPassInfo.renderPass;
     }
 
     // Create a framebuffer that will be used once for the render pass and gather the clear
@@ -501,7 +504,12 @@
     beginInfo.clearValueCount = attachmentCount;
     beginInfo.pClearValues = clearValues.data();
 
-    device->fn.CmdBeginRenderPass(commands, &beginInfo, VK_SUBPASS_CONTENTS_INLINE);
+    if (renderPass->attachmentState->GetExpandResolveInfo().attachmentsToExpandResolve.any()) {
+        DAWN_TRY(BeginRenderPassAndExpandResolveTextureWithDraw(device, recordingContext,
+                                                                renderPass, beginInfo));
+    } else {
+        device->fn.CmdBeginRenderPass(commands, &beginInfo, VK_SUBPASS_CONTENTS_INLINE);
+    }
 
     return {};
 }
diff --git a/src/dawn/native/vulkan/DeviceVk.cpp b/src/dawn/native/vulkan/DeviceVk.cpp
index e197dfa..55879ea 100644
--- a/src/dawn/native/vulkan/DeviceVk.cpp
+++ b/src/dawn/native/vulkan/DeviceVk.cpp
@@ -168,7 +168,15 @@
     Ref<Queue> queue;
     DAWN_TRY_ASSIGN(queue, Queue::Create(this, &descriptor->defaultQueue, mMainQueueFamily));
 
-    return DeviceBase::Initialize(std::move(queue));
+    DAWN_TRY(DeviceBase::Initialize(std::move(queue)));
+
+    if (HasFeature(Feature::DawnLoadResolveTexture)) {
+        // TODO(42240662): Add a way to add additional extensions when compiling specific shader
+        // modules only.
+        EnableAdditionalWGSLExtension(tint::wgsl::Extension::kChromiumInternalInputAttachments);
+    }
+
+    return {};
 }
 
 Device::~Device() {
@@ -673,12 +681,13 @@
     DAWN_INVALID_IF(!mExternalSemaphoreService->Supported(),
                     "External semaphore usage not supported");
 
-    DAWN_INVALID_IF(!mExternalMemoryService->SupportsImportMemory(
-                        descriptor->GetType(), VulkanImageFormat(this, textureDescriptor->format),
-                        VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL,
-                        VulkanImageUsage(usage, GetValidInternalFormat(textureDescriptor->format)),
-                        VK_IMAGE_CREATE_ALIAS_BIT_KHR),
-                    "External memory usage not supported");
+    DAWN_INVALID_IF(
+        !mExternalMemoryService->SupportsImportMemory(
+            descriptor->GetType(), VulkanImageFormat(this, textureDescriptor->format),
+            VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL,
+            VulkanImageUsage(this, usage, GetValidInternalFormat(textureDescriptor->format)),
+            VK_IMAGE_CREATE_ALIAS_BIT_KHR),
+        "External memory usage not supported");
 
     // Import the external image's memory
     external_memory::MemoryImportParams importParams;
diff --git a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
index c77c31b..6075a4c 100644
--- a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
+++ b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
@@ -369,6 +369,7 @@
     EnableFeature(Feature::SurfaceCapabilities);
     EnableFeature(Feature::TransientAttachments);
     EnableFeature(Feature::AdapterPropertiesVk);
+    EnableFeature(Feature::DawnLoadResolveTexture);
 
     // Enable ChromiumExperimentalSubgroups feature if:
     // 1. Vulkan API version is 1.1 or later, and
diff --git a/src/dawn/native/vulkan/RenderPassCache.cpp b/src/dawn/native/vulkan/RenderPassCache.cpp
index ced4ffa..f38e469 100644
--- a/src/dawn/native/vulkan/RenderPassCache.cpp
+++ b/src/dawn/native/vulkan/RenderPassCache.cpp
@@ -27,6 +27,7 @@
 
 #include "dawn/native/vulkan/RenderPassCache.h"
 
+#include "absl/container/inlined_vector.h"
 #include "dawn/common/BitSetIterator.h"
 #include "dawn/common/Enumerator.h"
 #include "dawn/common/HashUtils.h"
@@ -45,9 +46,7 @@
         case wgpu::LoadOp::Clear:
             return VK_ATTACHMENT_LOAD_OP_CLEAR;
         case wgpu::LoadOp::ExpandResolveTexture:
-            // TODO(dawn:1710): Implement this on vulkan.
-            DAWN_UNREACHABLE();
-            break;
+            return VK_ATTACHMENT_LOAD_OP_DONT_CARE;
         case wgpu::LoadOp::Undefined:
             DAWN_UNREACHABLE();
             break;
@@ -69,6 +68,32 @@
     }
     DAWN_UNREACHABLE();
 }
+
+void InitializeLoadResolveSubpassDependencies(
+    absl::InlinedVector<VkSubpassDependency, 2>* subpassDependenciesOut) {
+    VkSubpassDependency dependencies[2];
+    // Dependency for resolve texture's read -> resolve texture's write.
+    dependencies[0].srcSubpass = 0;
+    dependencies[0].dstSubpass = 1;
+    dependencies[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
+    dependencies[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+    dependencies[0].srcAccessMask = VK_ACCESS_INPUT_ATTACHMENT_READ_BIT;
+    dependencies[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+    dependencies[0].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;
+
+    // Dependency for color write in subpass 0 -> color write in subpass 1
+    dependencies[1].srcSubpass = 0;
+    dependencies[1].dstSubpass = 1;
+    dependencies[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+    dependencies[1].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+    dependencies[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+    dependencies[1].dstAccessMask =
+        VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+    dependencies[1].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;
+
+    subpassDependenciesOut->insert(subpassDependenciesOut->end(), std::begin(dependencies),
+                                   std::end(dependencies));
+}
 }  // anonymous namespace
 
 // RenderPassCacheQuery
@@ -83,6 +108,7 @@
     colorLoadOp[index] = loadOp;
     colorStoreOp[index] = storeOp;
     resolveTargetMask[index] = hasResolveTarget;
+    expandResolveMask.set(index, loadOp == wgpu::LoadOp::ExpandResolveTexture);
 }
 
 void RenderPassCacheQuery::SetDepthStencil(wgpu::TextureFormat format,
@@ -112,27 +138,28 @@
 
 RenderPassCache::~RenderPassCache() {
     std::lock_guard<std::mutex> lock(mMutex);
-    for (auto [_, renderPass] : mCache) {
-        mDevice->fn.DestroyRenderPass(mDevice->GetVkDevice(), renderPass, nullptr);
+    for (auto [_, renderPassInfo] : mCache) {
+        mDevice->fn.DestroyRenderPass(mDevice->GetVkDevice(), renderPassInfo.renderPass, nullptr);
     }
 
     mCache.clear();
 }
 
-ResultOrError<VkRenderPass> RenderPassCache::GetRenderPass(const RenderPassCacheQuery& query) {
+ResultOrError<RenderPassCache::RenderPassInfo> RenderPassCache::GetRenderPass(
+    const RenderPassCacheQuery& query) {
     std::lock_guard<std::mutex> lock(mMutex);
     auto it = mCache.find(query);
     if (it != mCache.end()) {
-        return VkRenderPass(it->second);
+        return RenderPassInfo(it->second);
     }
 
-    VkRenderPass renderPass;
+    RenderPassInfo renderPass;
     DAWN_TRY_ASSIGN(renderPass, CreateRenderPassForQuery(query));
     mCache.emplace(query, renderPass);
     return renderPass;
 }
 
-ResultOrError<VkRenderPass> RenderPassCache::CreateRenderPassForQuery(
+ResultOrError<RenderPassCache::RenderPassInfo> RenderPassCache::CreateRenderPassForQuery(
     const RenderPassCacheQuery& query) const {
     // The Vulkan subpasses want to know the layout of the attachments with VkAttachmentRef.
     // Precompute them as they must be pointer-chained in VkSubpassDescription.
@@ -140,14 +167,17 @@
     // filled with VK_ATTACHMENT_UNUSED.
     PerColorAttachment<VkAttachmentReference> colorAttachmentRefs;
     PerColorAttachment<VkAttachmentReference> resolveAttachmentRefs;
+    PerColorAttachment<VkAttachmentReference> inputAttachmentRefs;
     VkAttachmentReference depthStencilAttachmentRef;
 
     for (auto i : Range(kMaxColorAttachmentsTyped)) {
         colorAttachmentRefs[i].attachment = VK_ATTACHMENT_UNUSED;
         resolveAttachmentRefs[i].attachment = VK_ATTACHMENT_UNUSED;
+        inputAttachmentRefs[i].attachment = VK_ATTACHMENT_UNUSED;
         // The Khronos Vulkan validation layer will complain if not set
         colorAttachmentRefs[i].layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
         resolveAttachmentRefs[i].layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+        inputAttachmentRefs[i].layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
     }
 
     // Contains the attachment description that will be chained in the create info
@@ -208,27 +238,58 @@
     }
 
     uint32_t resolveAttachmentCount = 0;
+    ColorAttachmentIndex highestInputAttachmentIndex(static_cast<uint8_t>(0));
+
     for (auto i : IterateBitSet(query.resolveTargetMask)) {
-        auto& attachmentRef = resolveAttachmentRefs[i];
-        auto& attachmentDesc = attachmentDescs[attachmentCount];
+        auto& resolveAttachmentRef = resolveAttachmentRefs[i];
+        auto& resolveAttachmentDesc = attachmentDescs[attachmentCount];
 
-        attachmentRef.attachment = attachmentCount;
-        attachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+        resolveAttachmentRef.attachment = attachmentCount;
+        resolveAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
 
-        attachmentDesc.flags = 0;
-        attachmentDesc.format = VulkanImageFormat(mDevice, query.colorFormats[i]);
-        attachmentDesc.samples = VK_SAMPLE_COUNT_1_BIT;
-        attachmentDesc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
-        attachmentDesc.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
-        attachmentDesc.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
-        attachmentDesc.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+        resolveAttachmentDesc.flags = 0;
+        resolveAttachmentDesc.format = VulkanImageFormat(mDevice, query.colorFormats[i]);
+        resolveAttachmentDesc.samples = VK_SAMPLE_COUNT_1_BIT;
+        resolveAttachmentDesc.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
+        if (query.expandResolveMask.test(i)) {
+            resolveAttachmentDesc.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
+            resolveAttachmentDesc.initialLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+
+            inputAttachmentRefs[i].attachment = resolveAttachmentRefs[i].attachment;
+
+            highestInputAttachmentIndex = i;
+        } else {
+            resolveAttachmentDesc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
+            resolveAttachmentDesc.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+        }
+
+        resolveAttachmentDesc.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
 
         attachmentCount++;
         resolveAttachmentCount++;
     }
 
+    absl::InlinedVector<VkSubpassDescription, 2> subpassDescs;
+    absl::InlinedVector<VkSubpassDependency, 2> subpassDependencies;
+    if (query.expandResolveMask.any()) {
+        // To simulate ExpandResolveTexture, we use two subpasses. The first subpass will read the
+        // resolve texture as input attachment.
+        subpassDescs.push_back({});
+        VkSubpassDescription& subpassDesc = subpassDescs.back();
+        subpassDesc.flags = 0;
+        subpassDesc.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
+        subpassDesc.inputAttachmentCount = static_cast<uint8_t>(highestInputAttachmentIndex) + 1;
+        subpassDesc.pInputAttachments = inputAttachmentRefs.data();
+        subpassDesc.colorAttachmentCount = static_cast<uint8_t>(highestColorAttachmentIndexPlusOne);
+        subpassDesc.pColorAttachments = colorAttachmentRefs.data();
+        subpassDesc.pDepthStencilAttachment = depthStencilAttachment;
+
+        InitializeLoadResolveSubpassDependencies(&subpassDependencies);
+    }
+
     // Create the VkSubpassDescription that will be chained in the VkRenderPassCreateInfo
-    VkSubpassDescription subpassDesc;
+    subpassDescs.push_back({});
+    VkSubpassDescription& subpassDesc = subpassDescs.back();
     subpassDesc.flags = 0;
     subpassDesc.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
     subpassDesc.inputAttachmentCount = 0;
@@ -255,17 +316,18 @@
     createInfo.flags = 0;
     createInfo.attachmentCount = attachmentCount;
     createInfo.pAttachments = attachmentDescs.data();
-    createInfo.subpassCount = 1;
-    createInfo.pSubpasses = &subpassDesc;
-    createInfo.dependencyCount = 0;
-    createInfo.pDependencies = nullptr;
+    createInfo.subpassCount = subpassDescs.size();
+    createInfo.pSubpasses = subpassDescs.data();
+    createInfo.dependencyCount = subpassDependencies.size();
+    createInfo.pDependencies = subpassDependencies.data();
 
     // Create the render pass from the zillion parameters
-    VkRenderPass renderPass;
-    DAWN_TRY(CheckVkSuccess(
-        mDevice->fn.CreateRenderPass(mDevice->GetVkDevice(), &createInfo, nullptr, &*renderPass),
-        "CreateRenderPass"));
-    return renderPass;
+    RenderPassInfo renderPassInfo;
+    renderPassInfo.mainSubpass = subpassDescs.size() - 1;
+    DAWN_TRY(CheckVkSuccess(mDevice->fn.CreateRenderPass(mDevice->GetVkDevice(), &createInfo,
+                                                         nullptr, &*renderPassInfo.renderPass),
+                            "CreateRenderPass"));
+    return renderPassInfo;
 }
 
 // RenderPassCache
@@ -280,6 +342,7 @@
     for (auto i : IterateBitSet(query.colorMask)) {
         HashCombine(&hash, query.colorFormats[i], query.colorLoadOp[i], query.colorStoreOp[i]);
     }
+    HashCombine(&hash, query.expandResolveMask);
 
     HashCombine(&hash, query.hasDepthStencil);
     if (query.hasDepthStencil) {
@@ -314,6 +377,10 @@
         }
     }
 
+    if (a.expandResolveMask != b.expandResolveMask) {
+        return false;
+    }
+
     if (a.hasDepthStencil != b.hasDepthStencil) {
         return false;
     }
diff --git a/src/dawn/native/vulkan/RenderPassCache.h b/src/dawn/native/vulkan/RenderPassCache.h
index 5ffa24d..29470aa 100644
--- a/src/dawn/native/vulkan/RenderPassCache.h
+++ b/src/dawn/native/vulkan/RenderPassCache.h
@@ -71,6 +71,7 @@
     PerColorAttachment<wgpu::TextureFormat> colorFormats;
     PerColorAttachment<wgpu::LoadOp> colorLoadOp;
     PerColorAttachment<wgpu::StoreOp> colorStoreOp;
+    ColorAttachmentMask expandResolveMask;
 
     bool hasDepthStencil = false;
     wgpu::TextureFormat depthStencilFormat;
@@ -95,11 +96,16 @@
     explicit RenderPassCache(Device* device);
     ~RenderPassCache();
 
-    ResultOrError<VkRenderPass> GetRenderPass(const RenderPassCacheQuery& query);
+    struct RenderPassInfo {
+        VkRenderPass renderPass = VK_NULL_HANDLE;
+        uint32_t mainSubpass = 0;
+    };
+
+    ResultOrError<RenderPassInfo> GetRenderPass(const RenderPassCacheQuery& query);
 
   private:
     // Does the actual VkRenderPass creation on a cache miss.
-    ResultOrError<VkRenderPass> CreateRenderPassForQuery(const RenderPassCacheQuery& query) const;
+    ResultOrError<RenderPassInfo> CreateRenderPassForQuery(const RenderPassCacheQuery& query) const;
 
     // Implements the functors necessary for to use RenderPassCacheQueries as absl::flat_hash_map
     // keys.
@@ -107,7 +113,7 @@
         size_t operator()(const RenderPassCacheQuery& query) const;
         bool operator()(const RenderPassCacheQuery& a, const RenderPassCacheQuery& b) const;
     };
-    using Cache = absl::flat_hash_map<RenderPassCacheQuery, VkRenderPass, CacheFuncs, CacheFuncs>;
+    using Cache = absl::flat_hash_map<RenderPassCacheQuery, RenderPassInfo, CacheFuncs, CacheFuncs>;
 
     raw_ptr<Device> mDevice = nullptr;
 
diff --git a/src/dawn/native/vulkan/RenderPipelineVk.cpp b/src/dawn/native/vulkan/RenderPipelineVk.cpp
index b8a7441..b88fbd8 100644
--- a/src/dawn/native/vulkan/RenderPipelineVk.cpp
+++ b/src/dawn/native/vulkan/RenderPipelineVk.cpp
@@ -349,6 +349,7 @@
                                             ->GetHandleAndSpirv(stage, programmableStage, layout,
                                                                 clampFragDepth, emitPointSize,
                                                                 /* fullSubgroups */ {}));
+        mHasInputAttachment = mHasInputAttachment || moduleAndSpirv.hasInputAttachment;
         // Record cache key for each shader since it will become inaccessible later on.
         StreamIn(&mCacheKey, stream::Iterable(moduleAndSpirv.spirv, moduleAndSpirv.wordCount));
 
@@ -500,17 +501,31 @@
     dynamic.dynamicStateCount = sizeof(dynamicStates) / sizeof(dynamicStates[0]);
     dynamic.pDynamicStates = dynamicStates;
 
-    // Get a VkRenderPass that matches the attachment formats for this pipeline, load/store ops
-    // don't matter so set them all to LoadOp::Load / StoreOp::Store. Whether the render pass
-    // has resolve target and whether depth/stencil attachment is read-only also don't matter,
-    // so set them both to false.
-    VkRenderPass renderPass = VK_NULL_HANDLE;
+    // Get a VkRenderPass that matches the attachment formats for this pipeline.
+    // VkRenderPass compatibility rules let us provide placeholder data for a bunch of arguments.
+    // Load and store ops are all equivalent, though we still specify ExpandResolveTexture as that
+    // controls the use of input attachments. Single subpass VkRenderPasses are compatible
+    // irrespective of resolve attachments being used, but for ExpandResolveTexture that uses two
+    // subpasses we need to specify which attachments will be resolved.
+    RenderPassCache::RenderPassInfo renderPassInfo;
     {
         RenderPassCacheQuery query;
+        ColorAttachmentMask resolveMask =
+            GetAttachmentState()->GetExpandResolveInfo().resolveTargetsMask;
+        ColorAttachmentMask expandResolveMask =
+            GetAttachmentState()->GetExpandResolveInfo().attachmentsToExpandResolve;
 
         for (auto i : IterateBitSet(GetColorAttachmentsMask())) {
-            query.SetColor(i, GetColorAttachmentFormat(i), wgpu::LoadOp::Load, wgpu::StoreOp::Store,
-                           false);
+            wgpu::LoadOp colorLoadOp = wgpu::LoadOp::Load;
+            bool hasResolveTarget = resolveMask.test(i);
+
+            if (expandResolveMask.test(i)) {
+                // ExpandResolveTexture will use 2 subpasses in a render pass so we have to create
+                // an appropriate query.
+                colorLoadOp = wgpu::LoadOp::ExpandResolveTexture;
+            }
+            query.SetColor(i, GetColorAttachmentFormat(i), colorLoadOp, wgpu::StoreOp::Store,
+                           hasResolveTarget);
         }
 
         if (HasDepthStencilAttachment()) {
@@ -521,7 +536,7 @@
         query.SetSampleCount(GetSampleCount());
 
         StreamIn(&mCacheKey, query);
-        DAWN_TRY_ASSIGN(renderPass, device->GetRenderPassCache()->GetRenderPass(query));
+        DAWN_TRY_ASSIGN(renderPassInfo, device->GetRenderPassCache()->GetRenderPass(query));
     }
 
     // The create info chains in a bunch of things created on the stack here or inside state
@@ -543,11 +558,18 @@
         (GetStageMask() & wgpu::ShaderStage::Fragment) ? &colorBlend : nullptr;
     createInfo.pDynamicState = &dynamic;
     createInfo.layout = ToBackend(GetLayout())->GetHandle();
-    createInfo.renderPass = renderPass;
-    createInfo.subpass = 0;
+    createInfo.renderPass = renderPassInfo.renderPass;
     createInfo.basePipelineHandle = VkPipeline{};
     createInfo.basePipelineIndex = -1;
 
+    // - If the pipeline uses input attachments in shader, currently this is only used by
+    //   ExpandResolveTexture subpass, hence we need to set the subpass to 0.
+    // - Otherwise, the pipeline will operate on the main subpass.
+    // - TODO(42240662): Add explicit way to specify subpass instead of implicitly deducing based on
+    // mHasInputAttachment.
+    //   That also means mHasInputAttachment would be removed in future.
+    createInfo.subpass = mHasInputAttachment ? 0 : renderPassInfo.mainSubpass;
+
     // Record cache key information now since createInfo is not stored.
     StreamIn(&mCacheKey, createInfo, layout->GetCacheKey());
 
diff --git a/src/dawn/native/vulkan/RenderPipelineVk.h b/src/dawn/native/vulkan/RenderPipelineVk.h
index 003aec3..71fb8ff 100644
--- a/src/dawn/native/vulkan/RenderPipelineVk.h
+++ b/src/dawn/native/vulkan/RenderPipelineVk.h
@@ -64,6 +64,9 @@
     VkPipelineDepthStencilStateCreateInfo ComputeDepthStencilDesc();
 
     VkPipeline mHandle = VK_NULL_HANDLE;
+
+    // Whether the pipeline has any input attachment being used in the frag shader.
+    bool mHasInputAttachment = false;
 };
 
 }  // namespace dawn::native::vulkan
diff --git a/src/dawn/native/vulkan/ResolveTextureLoadingUtilsVk.cpp b/src/dawn/native/vulkan/ResolveTextureLoadingUtilsVk.cpp
new file mode 100644
index 0000000..3f00cad
--- /dev/null
+++ b/src/dawn/native/vulkan/ResolveTextureLoadingUtilsVk.cpp
@@ -0,0 +1,302 @@
+// Copyright 2024 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/ResolveTextureLoadingUtilsVk.h"
+
+#include <sstream>
+#include <string>
+#include <utility>
+
+#include "absl/container/inlined_vector.h"
+#include "dawn/common/Assert.h"
+#include "dawn/common/Enumerator.h"
+#include "dawn/native/BindGroup.h"
+#include "dawn/native/Commands.h"
+#include "dawn/native/Device.h"
+#include "dawn/native/InternalPipelineStore.h"
+#include "dawn/native/utils/WGPUHelpers.h"
+#include "dawn/native/vulkan/BindGroupLayoutVk.h"
+#include "dawn/native/vulkan/BindGroupVk.h"
+#include "dawn/native/vulkan/DeviceVk.h"
+#include "dawn/native/vulkan/PipelineLayoutVk.h"
+#include "dawn/native/vulkan/RenderPipelineVk.h"
+#include "dawn/native/vulkan/TextureVk.h"
+#include "dawn/native/vulkan/UtilsVulkan.h"
+#include "dawn/native/vulkan/VulkanError.h"
+#include "dawn/native/webgpu_absl_format.h"
+
+namespace dawn::native::vulkan {
+
+namespace {
+
+constexpr char kBlitToColorVS[] = R"(
+
+@vertex fn vert_fullscreen_quad(
+  @builtin(vertex_index) vertex_index : u32,
+) -> @builtin(position) vec4f {
+  const pos = array(
+      vec2f(-1.0, -1.0),
+      vec2f( 3.0, -1.0),
+      vec2f(-1.0,  3.0));
+  return vec4f(pos[vertex_index], 0.0, 1.0);
+}
+)";
+
+std::string GenerateFS(const BlitColorToColorWithDrawPipelineKey& pipelineKey) {
+    std::ostringstream outputStructStream;
+    std::ostringstream assignOutputsStream;
+    std::ostringstream finalStream;
+
+    finalStream << "enable chromium_internal_input_attachments;";
+
+    for (auto i : IterateBitSet(pipelineKey.attachmentsToExpandResolve)) {
+        finalStream << absl::StrFormat(
+            "@group(0) @binding(%u) @input_attachment_index(%u) var srcTex%u : "
+            "input_attachment<f32>;\n",
+            i, i, i);
+
+        outputStructStream << absl::StrFormat("@location(%u) output%u : vec4f,\n", i, i);
+
+        assignOutputsStream << absl::StrFormat(
+            "\toutputColor.output%u = inputAttachmentLoad(srcTex%u);\n", i, i);
+    }
+
+    finalStream << "struct OutputColor {\n" << outputStructStream.str() << "}\n\n";
+    finalStream << R"(
+@fragment fn blit_to_color() -> OutputColor {
+    var outputColor : OutputColor;
+)" << assignOutputsStream.str()
+                << R"(
+    return outputColor;
+})";
+
+    return finalStream.str();
+}
+
+ResultOrError<Ref<RenderPipelineBase>> GetOrCreateColorBlitPipeline(
+    DeviceBase* device,
+    const BlitColorToColorWithDrawPipelineKey& pipelineKey,
+    uint8_t colorAttachmentCount) {
+    InternalPipelineStore* store = device->GetInternalPipelineStore();
+    {
+        auto it = store->expandResolveTexturePipelines.find(pipelineKey);
+        if (it != store->expandResolveTexturePipelines.end()) {
+            return it->second;
+        }
+    }
+
+    // vertex shader.
+    Ref<ShaderModuleBase> vshaderModule;
+    DAWN_TRY_ASSIGN(vshaderModule, utils::CreateShaderModule(device, kBlitToColorVS));
+
+    // fragment shader's source will depend on pipeline key.
+    std::string fsCode = GenerateFS(pipelineKey);
+    Ref<ShaderModuleBase> fshaderModule;
+    DAWN_TRY_ASSIGN(fshaderModule, utils::CreateShaderModule(device, fsCode.c_str()));
+
+    FragmentState fragmentState = {};
+    fragmentState.module = fshaderModule.Get();
+
+    // Color target states.
+    PerColorAttachment<ColorTargetState> colorTargets = {};
+    PerColorAttachment<wgpu::ColorTargetStateExpandResolveTextureDawn> msaaExpandResolveStates{};
+
+    for (auto [i, target] : Enumerate(colorTargets)) {
+        target.format = pipelineKey.colorTargetFormats[i];
+        // We shouldn't change the color targets that are not involved in.
+        if (pipelineKey.resolveTargetsMask[i]) {
+            target.nextInChain = &msaaExpandResolveStates[i];
+            msaaExpandResolveStates[i].enabled = pipelineKey.attachmentsToExpandResolve[i];
+            if (msaaExpandResolveStates[i].enabled) {
+                target.writeMask = wgpu::ColorWriteMask::All;
+            } else {
+                target.writeMask = wgpu::ColorWriteMask::None;
+            }
+        } else {
+            target.writeMask = wgpu::ColorWriteMask::None;
+        }
+    }
+
+    fragmentState.targetCount = colorAttachmentCount;
+    fragmentState.targets = colorTargets.data();
+
+    RenderPipelineDescriptor renderPipelineDesc = {};
+    renderPipelineDesc.label = "blit_color_to_color";
+    renderPipelineDesc.vertex.module = vshaderModule.Get();
+    renderPipelineDesc.fragment = &fragmentState;
+
+    // Depth stencil state.
+    DepthStencilState depthStencilState = {};
+    if (pipelineKey.depthStencilFormat != wgpu::TextureFormat::Undefined) {
+        depthStencilState.format = pipelineKey.depthStencilFormat;
+
+        renderPipelineDesc.depthStencil = &depthStencilState;
+    }
+
+    // Multisample state.
+    DAWN_ASSERT(pipelineKey.sampleCount > 1);
+    renderPipelineDesc.multisample.count = pipelineKey.sampleCount;
+
+    renderPipelineDesc.layout = nullptr;
+
+    Ref<RenderPipelineBase> pipeline;
+    DAWN_TRY_ASSIGN(
+        pipeline, device->CreateRenderPipeline(&renderPipelineDesc, /*allowInternalBinding=*/true));
+
+    store->expandResolveTexturePipelines.emplace(pipelineKey, pipeline);
+    return pipeline;
+}
+
+}  // namespace
+
+MaybeError BeginRenderPassAndExpandResolveTextureWithDraw(Device* device,
+                                                          CommandRecordingContext* commandContext,
+                                                          const BeginRenderPassCmd* renderPass,
+                                                          const VkRenderPassBeginInfo& beginInfo) {
+    DAWN_ASSERT(device->IsLockedByCurrentThreadIfNeeded());
+
+    // Construct pipeline key
+    BlitColorToColorWithDrawPipelineKey pipelineKey;
+    ColorAttachmentIndex colorAttachmentCount =
+        GetHighestBitIndexPlusOne(renderPass->attachmentState->GetColorAttachmentsMask());
+    for (ColorAttachmentIndex colorIdx :
+         IterateBitSet(renderPass->attachmentState->GetColorAttachmentsMask())) {
+        const auto& colorAttachment = renderPass->colorAttachments[colorIdx];
+        const auto& view = colorAttachment.view;
+        DAWN_ASSERT(view != nullptr);
+        const Format& format = view->GetFormat();
+        TextureComponentType baseType = format.GetAspectInfo(Aspect::Color).baseType;
+        // Blitting integer textures are not currently supported.
+        DAWN_ASSERT(baseType == TextureComponentType::Float);
+
+        if (colorAttachment.loadOp == wgpu::LoadOp::ExpandResolveTexture) {
+            // TODO(42240662): Handle the cases where resolveTarget is altered by workarounds such
+            // as ResolveMultipleAttachmentInSeparatePasses/AlwaysResolveIntoZeroLevelAndLayer. We
+            // need to careful handle such cases because the render pass' compatibility could be
+            // affected as well.
+            DAWN_INVALID_IF(colorAttachment.resolveTarget == nullptr,
+                            "resolveTarget at %d has been removed by some workarounds. %s doesn't "
+                            "support this yet.",
+                            colorIdx, colorAttachment.loadOp);
+            DAWN_ASSERT(colorAttachment.resolveTarget->GetLayerCount() == 1u);
+            DAWN_ASSERT(colorAttachment.resolveTarget->GetDimension() ==
+                        wgpu::TextureViewDimension::e2D);
+            pipelineKey.attachmentsToExpandResolve.set(colorIdx);
+        }
+        pipelineKey.resolveTargetsMask.set(colorIdx, colorAttachment.resolveTarget != nullptr);
+
+        pipelineKey.colorTargetFormats[colorIdx] = format.format;
+        pipelineKey.sampleCount = view->GetTexture()->GetSampleCount();
+    }
+
+    DAWN_ASSERT(pipelineKey.attachmentsToExpandResolve.any());
+
+    pipelineKey.depthStencilFormat = wgpu::TextureFormat::Undefined;
+    if (renderPass->depthStencilAttachment.view != nullptr) {
+        pipelineKey.depthStencilFormat =
+            renderPass->depthStencilAttachment.view->GetFormat().format;
+    }
+
+    Ref<RenderPipelineBase> pipeline;
+    DAWN_TRY_ASSIGN(pipeline, GetOrCreateColorBlitPipeline(
+                                  device, pipelineKey, static_cast<uint8_t>(colorAttachmentCount)));
+
+    RenderPipeline* pipelineVk = ToBackend(pipeline.Get());
+    PipelineLayout* layoutVk = ToBackend(pipeline->GetLayout());
+    DAWN_ASSERT(layoutVk != nullptr);
+
+    // Construct bind group.
+    Ref<BindGroupLayoutBase> bgl;
+    DAWN_TRY_ASSIGN(bgl, pipelineVk->GetBindGroupLayout(0));
+
+    Ref<BindGroupBase> bindGroup;
+    absl::InlinedVector<BindGroupEntry, kMaxColorAttachments> bgEntries = {};
+
+    for (auto colorIdx : IterateBitSet(pipelineKey.attachmentsToExpandResolve)) {
+        const auto& colorAttachment = renderPass->colorAttachments[colorIdx];
+        bgEntries.push_back({});
+        auto& bgEntry = bgEntries.back();
+        bgEntry.binding = static_cast<uint8_t>(colorIdx);
+        bgEntry.textureView = colorAttachment.resolveTarget.Get();
+
+        // Transition the resolve texture
+        auto* textureVk = static_cast<Texture*>(colorAttachment.resolveTarget->GetTexture());
+        textureVk->TransitionUsageNow(commandContext, kResolveAttachmentLoadingUsage,
+                                      wgpu::ShaderStage::Fragment,
+                                      colorAttachment.resolveTarget->GetSubresourceRange());
+    }
+
+    BindGroupDescriptor bgDesc = {};
+    bgDesc.layout = bgl.Get();
+    bgDesc.entryCount = bgEntries.size();
+    bgDesc.entries = bgEntries.data();
+    DAWN_TRY_ASSIGN(bindGroup, device->CreateBindGroup(&bgDesc, UsageValidationMode::Internal));
+    BindGroup* bindGroupVk = ToBackend(bindGroup.Get());
+
+    // Start the render pass
+    VkCommandBuffer commandBuffer = commandContext->commandBuffer;
+
+    device->fn.CmdBeginRenderPass(commandBuffer, &beginInfo, VK_SUBPASS_CONTENTS_INLINE);
+
+    // Draw to perform the blit.
+    VkViewport viewport{};
+    viewport.x = 0.0f;
+    viewport.y = 0.0f;
+    viewport.width = static_cast<float>(renderPass->width);
+    viewport.height = static_cast<float>(renderPass->height);
+    viewport.minDepth = 0.0f;
+    viewport.maxDepth = 1.0f;
+    device->fn.CmdSetViewport(commandBuffer, 0, 1, &viewport);
+
+    VkRect2D scissor{};
+    scissor.offset = {0, 0};
+    scissor.extent.width = renderPass->width;
+    scissor.extent.height = renderPass->height;
+    device->fn.CmdSetScissor(commandBuffer, 0, 1, &scissor);
+
+    device->fn.CmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
+                               *pipelineVk->GetHandle());
+    device->fn.CmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
+                                     *layoutVk->GetHandle(), 0, 1, &*bindGroupVk->GetHandle(), 0,
+                                     nullptr);
+    device->fn.CmdDraw(commandBuffer, 3, 1, 0, 0);
+
+    device->fn.CmdNextSubpass(commandBuffer, VK_SUBPASS_CONTENTS_INLINE);
+
+    // Subpass dependency automatically transitions the layouts of the resolve textures
+    // to RenderAttachment. So we need to notify TextureVk and don't need to use any explicit
+    // barriers.
+    for (auto colorIdx : IterateBitSet(pipelineKey.attachmentsToExpandResolve)) {
+        const auto& colorAttachment = renderPass->colorAttachments[colorIdx];
+        auto* textureVk = static_cast<Texture*>(colorAttachment.resolveTarget->GetTexture());
+        textureVk->UpdateUsage(wgpu::TextureUsage::RenderAttachment, wgpu::ShaderStage::Fragment,
+                               colorAttachment.resolveTarget->GetSubresourceRange());
+    }
+
+    return {};
+}
+}  // namespace dawn::native::vulkan
diff --git a/src/dawn/native/vulkan/ResolveTextureLoadingUtilsVk.h b/src/dawn/native/vulkan/ResolveTextureLoadingUtilsVk.h
new file mode 100644
index 0000000..39ae9c5
--- /dev/null
+++ b/src/dawn/native/vulkan/ResolveTextureLoadingUtilsVk.h
@@ -0,0 +1,53 @@
+// Copyright 2024 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_RESOLVETEXTURELOADINGUTILSVK_H_
+#define SRC_DAWN_NATIVE_VULKAN_RESOLVETEXTURELOADINGUTILSVK_H_
+
+#include "dawn/common/vulkan_platform.h"
+#include "dawn/native/Error.h"
+
+namespace dawn::native {
+
+struct BeginRenderPassCmd;
+
+namespace vulkan {
+
+struct CommandRecordingContext;
+class Device;
+
+// This function begins render pass then performs the ExpandResolveTexture load operation for the
+// render pass by blitting the resolve target to the MSAA attachment.
+MaybeError BeginRenderPassAndExpandResolveTextureWithDraw(Device* device,
+                                                          CommandRecordingContext* commandContext,
+                                                          const BeginRenderPassCmd* renderPass,
+                                                          const VkRenderPassBeginInfo& beginInfo);
+
+}  // namespace vulkan
+}  // namespace dawn::native
+
+#endif  // SRC_DAWN_NATIVE_VULKAN_RESOLVETEXTURELOADINGUTILSVK_H_
diff --git a/src/dawn/native/vulkan/ShaderModuleVk.cpp b/src/dawn/native/vulkan/ShaderModuleVk.cpp
index 5f004ac..1e06207 100644
--- a/src/dawn/native/vulkan/ShaderModuleVk.cpp
+++ b/src/dawn/native/vulkan/ShaderModuleVk.cpp
@@ -114,7 +114,8 @@
     }
     ModuleAndSpirv AddOrGet(const TransformedShaderModuleCacheKey& key,
                             VkShaderModule module,
-                            CompiledSpirv compilation) {
+                            CompiledSpirv compilation,
+                            bool hasInputAttachment) {
         DAWN_ASSERT(module != VK_NULL_HANDLE);
         std::lock_guard<std::mutex> lock(mMutex);
 
@@ -123,7 +124,7 @@
             bool added = false;
             std::tie(iter, added) = mTransformedShaderModuleCache.emplace(
                 key, Entry{module, std::move(compilation.spirv),
-                           std::move(compilation.remappedEntryPoint)});
+                           std::move(compilation.remappedEntryPoint), hasInputAttachment});
             DAWN_ASSERT(added);
         } else {
             // No need to use FencedDeleter since this shader module was just created and does
@@ -139,13 +140,12 @@
         VkShaderModule vkModule;
         std::vector<uint32_t> spirv;
         std::string remappedEntryPoint;
+        bool hasInputAttachment;
 
         ModuleAndSpirv AsRefs() const {
             return {
-                vkModule,
-                spirv.data(),
-                spirv.size(),
-                remappedEntryPoint.c_str(),
+                vkModule,           spirv.data(), spirv.size(), remappedEntryPoint.c_str(),
+                hasInputAttachment,
             };
         }
     };
@@ -309,12 +309,15 @@
                         tint::spirv::writer::binding::ExternalTexture{metadata, plane0, plane1});
                 },
                 [&](const InputAttachmentBindingInfo& bindingInfo) {
-                    // TODO(341117913): implement input attachment binding.
-                    DAWN_UNREACHABLE();
+                    bindings.input_attachment.emplace(
+                        srcBindingPoint, tint::spirv::writer::binding::InputAttachment{
+                                             dstBindingPoint.group, dstBindingPoint.binding});
                 });
         }
     }
 
+    const bool hasInputAttachment = !bindings.input_attachment.empty();
+
     std::optional<tint::ast::transform::SubstituteOverride::Config> substituteOverrideConfig;
     if (!programmableStage.metadata->overrides.empty()) {
         substituteOverrideConfig = BuildSubstituteOverridesTransformConfig(programmableStage);
@@ -479,8 +482,8 @@
         // Set the label on `newHandle` now, and not on `moduleAndSpirv.module` later
         // since `moduleAndSpirv.module` may be in use by multiple threads.
         SetDebugName(ToBackend(GetDevice()), newHandle, "Dawn_ShaderModule", GetLabel());
-        moduleAndSpirv =
-            mTransformedShaderModuleCache->AddOrGet(cacheKey, newHandle, compilation.Acquire());
+        moduleAndSpirv = mTransformedShaderModuleCache->AddOrGet(
+            cacheKey, newHandle, compilation.Acquire(), hasInputAttachment);
     }
 
     return std::move(moduleAndSpirv);
diff --git a/src/dawn/native/vulkan/ShaderModuleVk.h b/src/dawn/native/vulkan/ShaderModuleVk.h
index 06959d1..f87c682 100644
--- a/src/dawn/native/vulkan/ShaderModuleVk.h
+++ b/src/dawn/native/vulkan/ShaderModuleVk.h
@@ -69,6 +69,7 @@
         const uint32_t* spirv;
         size_t wordCount;
         std::string remappedEntryPoint;
+        bool hasInputAttachment;
     };
 
     static ResultOrError<Ref<ShaderModule>> Create(
diff --git a/src/dawn/native/vulkan/SharedTextureMemoryVk.cpp b/src/dawn/native/vulkan/SharedTextureMemoryVk.cpp
index 6560fad..7691e89 100644
--- a/src/dawn/native/vulkan/SharedTextureMemoryVk.cpp
+++ b/src/dawn/native/vulkan/SharedTextureMemoryVk.cpp
@@ -251,7 +251,7 @@
     VkFormat vkFormat = VulkanImageFormat(device, properties.format);
 
     // Usage flags to create the image with.
-    VkImageUsageFlags vkUsageFlags = VulkanImageUsage(properties.usage, *internalFormat);
+    VkImageUsageFlags vkUsageFlags = VulkanImageUsage(device, properties.usage, *internalFormat);
 
     // Number of memory planes in the image which will be queried from the DRM modifier.
     uint32_t memoryPlaneCount;
@@ -595,7 +595,7 @@
     sharedTextureMemory->APIGetProperties(&properties);
 
     // Compute the Vulkan usage flags to create the image with.
-    VkImageUsageFlags vkUsageFlags = VulkanImageUsage(properties.usage, *internalFormat);
+    VkImageUsageFlags vkUsageFlags = VulkanImageUsage(device, properties.usage, *internalFormat);
 
     const auto& compatibleViewFormats = device->GetCompatibleViewFormats(*internalFormat);
 
diff --git a/src/dawn/native/vulkan/SwapChainVk.cpp b/src/dawn/native/vulkan/SwapChainVk.cpp
index 21505a5..32ec032 100644
--- a/src/dawn/native/vulkan/SwapChainVk.cpp
+++ b/src/dawn/native/vulkan/SwapChainVk.cpp
@@ -265,7 +265,7 @@
 
     // Choose the target usage or do a blit.
     VkImageUsageFlags targetUsages =
-        VulkanImageUsage(GetUsage(), GetDevice()->GetValidInternalFormat(GetFormat()));
+        VulkanImageUsage(GetDevice(), GetUsage(), GetDevice()->GetValidInternalFormat(GetFormat()));
     VkImageUsageFlags supportedUsages = surfaceInfo.capabilities.supportedUsageFlags;
     if (!IsSubset(targetUsages, supportedUsages)) {
         config.needsBlit = true;
diff --git a/src/dawn/native/vulkan/TextureVk.cpp b/src/dawn/native/vulkan/TextureVk.cpp
index ff47906..6c12824 100644
--- a/src/dawn/native/vulkan/TextureVk.cpp
+++ b/src/dawn/native/vulkan/TextureVk.cpp
@@ -154,6 +154,14 @@
         flags |= VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
     }
 
+    if (usage & kResolveAttachmentLoadingUsage) {
+        // - The texture will be used as input attachment in the first subpass and loaded with
+        // VK_ATTACHMENT_LOAD_OP_LOAD. This requires VK_ACCESS_COLOR_ATTACHMENT_READ_BIT access.
+        // - It will also be read as subpass input in fragment shader. This requires
+        // VK_ACCESS_INPUT_ATTACHMENT_READ_BIT.
+        flags |= VK_ACCESS_INPUT_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT;
+    }
+
     if (usage & kPresentAcquireTextureUsage) {
         // The present acquire usage is only used internally by the swapchain and is never used in
         // combination with other usages.
@@ -234,6 +242,14 @@
             flags |= VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
         }
     }
+    if (usage & kResolveAttachmentLoadingUsage) {
+        // - The texture will be used as input attachment in the first subpass and loaded with
+        // VK_ATTACHMENT_LOAD_OP_LOAD. This happens at VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
+        // stage.
+        // - It will also be read as subpass input in fragment shader.
+        flags |=
+            VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+    }
     if (usage & (wgpu::TextureUsage::RenderAttachment | kReadOnlyRenderAttachment)) {
         if (format.HasDepthOrStencil()) {
             flags |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
@@ -579,7 +595,9 @@
 
 // Converts the Dawn usage flags to Vulkan usage flags. Also needs the format to choose
 // between color and depth attachment usages.
-VkImageUsageFlags VulkanImageUsage(wgpu::TextureUsage usage, const Format& format) {
+VkImageUsageFlags VulkanImageUsage(const DeviceBase* device,
+                                   wgpu::TextureUsage usage,
+                                   const Format& format) {
     VkImageUsageFlags flags = 0;
 
     if (usage & wgpu::TextureUsage::CopySrc) {
@@ -605,6 +623,12 @@
             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.
+                flags |= VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT;
+            }
         }
     }
 
@@ -648,6 +672,7 @@
 
             // The layout returned here is the one that will be used at bindgroup creation time.
         case wgpu::TextureUsage::TextureBinding:
+        case kResolveAttachmentLoadingUsage:
             // The sampled image can be used as a readonly depth/stencil attachment at the same
             // time if it is a depth/stencil renderable format, so the image layout need to be
             // VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL.
@@ -839,7 +864,7 @@
     createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
     createInfo.format = VulkanImageFormat(device, GetFormat().format);
     createInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
-    createInfo.usage = VulkanImageUsage(GetInternalUsage(), GetFormat()) | extraUsages;
+    createInfo.usage = VulkanImageUsage(device, GetInternalUsage(), GetFormat()) | extraUsages;
     createInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
     createInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
 
@@ -959,7 +984,7 @@
                                            external_memory::Service* externalMemoryService) {
     Device* device = ToBackend(GetDevice());
     VkFormat format = VulkanImageFormat(device, GetFormat().format);
-    VkImageUsageFlags usage = VulkanImageUsage(GetInternalUsage(), GetFormat());
+    VkImageUsageFlags usage = VulkanImageUsage(device, GetInternalUsage(), GetFormat());
 
     [[maybe_unused]] bool supportsDisjoint;
     DAWN_INVALID_IF(
@@ -1456,6 +1481,20 @@
     }
 }
 
+void Texture::UpdateUsage(wgpu::TextureUsage usage,
+                          wgpu::ShaderStage shaderStages,
+                          const SubresourceRange& range) {
+    std::vector<VkImageMemoryBarrier> barriers;
+
+    VkPipelineStageFlags srcStages = 0;
+    VkPipelineStageFlags dstStages = 0;
+
+    TransitionUsageAndGetResourceBarrier(usage, shaderStages, range, &barriers, &srcStages,
+                                         &dstStages);
+
+    // barriers are ignored.
+}
+
 void Texture::TransitionUsageAndGetResourceBarrier(wgpu::TextureUsage usage,
                                                    wgpu::ShaderStage shaderStages,
                                                    const SubresourceRange& range,
@@ -1766,7 +1805,7 @@
 
     VkImageViewUsageCreateInfo usageInfo = {};
     usageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO;
-    usageInfo.usage = VulkanImageUsage(usage, GetFormat());
+    usageInfo.usage = VulkanImageUsage(device, usage, GetFormat());
     createInfo.pNext = &usageInfo;
 
     VkSamplerYcbcrConversionInfo samplerYCbCrInfo = {};
diff --git a/src/dawn/native/vulkan/TextureVk.h b/src/dawn/native/vulkan/TextureVk.h
index d3409bc..eb08296 100644
--- a/src/dawn/native/vulkan/TextureVk.h
+++ b/src/dawn/native/vulkan/TextureVk.h
@@ -51,7 +51,9 @@
 // properties of the Device.
 VkFormat ColorVulkanImageFormat(wgpu::TextureFormat format);
 ResultOrError<wgpu::TextureFormat> FormatFromVkFormat(const Device* device, VkFormat vkFormat);
-VkImageUsageFlags VulkanImageUsage(wgpu::TextureUsage usage, const Format& format);
+VkImageUsageFlags VulkanImageUsage(const DeviceBase* device,
+                                   wgpu::TextureUsage usage,
+                                   const Format& format);
 VkImageLayout VulkanImageLayout(const Format& format, wgpu::TextureUsage usage);
 VkImageLayout VulkanImageLayoutForDepthStencilAttachment(const Format& format,
                                                          bool depthReadOnly,
@@ -106,6 +108,13 @@
                                 std::vector<VkImageMemoryBarrier>* imageBarriers,
                                 VkPipelineStageFlags* srcStages,
                                 VkPipelineStageFlags* dstStages);
+    // Change the texture to be used as `usage`. Note: this function assumes the barriers are
+    // already invoked before calling it. Typical use case is an input attachment, at the beginning
+    // of render pass, its usage is transitioned to TextureBinding. Then subpass' dependency
+    // automatically transitions the texture to RenderAttachment without any explicit barrier call.
+    void UpdateUsage(wgpu::TextureUsage usage,
+                     wgpu::ShaderStage shaderStages,
+                     const SubresourceRange& range);
 
     // Eagerly transition the texture for export.
     void TransitionEagerlyForExport(CommandRecordingContext* recordingContext);
diff --git a/src/dawn/tests/end2end/MultisampledRenderingTests.cpp b/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
index e72e23a..03b6734 100644
--- a/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
+++ b/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
@@ -30,6 +30,7 @@
 #include <vector>
 
 #include "dawn/common/Assert.h"
+#include "dawn/native/DawnNative.h"
 #include "dawn/tests/DawnTest.h"
 #include "dawn/utils/ComboRenderPipelineDescriptor.h"
 #include "dawn/utils/WGPUHelpers.h"
@@ -1542,6 +1543,7 @@
 }
 
 class DawnLoadResolveTextureTest : public MultisampledRenderingTest {
+  protected:
     void SetUp() override {
         MultisampledRenderingTest::SetUp();
 
@@ -1559,6 +1561,10 @@
         }
         return requiredFeatures;
     }
+
+    bool HasResolveMultipleAttachmentInSeparatePassesToggle() {
+        return HasToggleEnabled("resolve_multiple_attachments_in_separate_passes");
+    }
 };
 
 // Test rendering into a resolve texture then start another render pass with
@@ -1662,10 +1668,13 @@
 // Test rendering into 2 attachments. The 1st attachment will use
 // LoadOp::ExpandResolveTexture.
 TEST_P(DawnLoadResolveTextureTest, TwoOutputsDrawThenLoadColor0) {
-    auto multiSampledTexture1 = CreateTextureForRenderAttachment(
-        kColorFormat, 4, 1, 1,
-        /*transientAttachment=*/device.HasFeature(wgpu::FeatureName::TransientAttachments),
-        /*supportsTextureBinding=*/false);
+    // TODO(42240662): "resolve_multiple_attachments_in_separate_passes" is currently not working
+    // with DawnLoadResolveTexture feature if there are more than one attachment.
+    DAWN_TEST_UNSUPPORTED_IF(HasResolveMultipleAttachmentInSeparatePassesToggle());
+
+    auto multiSampledTexture1 = CreateTextureForRenderAttachment(kColorFormat, 4, 1, 1,
+                                                                 /*transientAttachment=*/false,
+                                                                 /*supportsTextureBinding=*/false);
     auto multiSampledTextureView1 = multiSampledTexture1.CreateView();
 
     auto multiSampledTexture2 = CreateTextureForRenderAttachment(kColorFormat, 4, 1, 1,
@@ -1740,15 +1749,18 @@
 // Test rendering into 2 attachments. The 2nd attachment will use
 // LoadOp::ExpandResolveTexture.
 TEST_P(DawnLoadResolveTextureTest, TwoOutputsDrawThenLoadColor1) {
+    // TODO(42240662): "resolve_multiple_attachments_in_separate_passes" is currently not working
+    // with DawnLoadResolveTexture feature if there are more than one attachment.
+    DAWN_TEST_UNSUPPORTED_IF(HasResolveMultipleAttachmentInSeparatePassesToggle());
+
     auto multiSampledTexture1 = CreateTextureForRenderAttachment(kColorFormat, 4, 1, 1,
                                                                  /*transientAttachment=*/false,
                                                                  /*supportsTextureBinding=*/false);
     auto multiSampledTextureView1 = multiSampledTexture1.CreateView();
 
-    auto multiSampledTexture2 = CreateTextureForRenderAttachment(
-        kColorFormat, 4, 1, 1,
-        /*transientAttachment=*/device.HasFeature(wgpu::FeatureName::TransientAttachments),
-        /*supportsTextureBinding=*/false);
+    auto multiSampledTexture2 = CreateTextureForRenderAttachment(kColorFormat, 4, 1, 1,
+                                                                 /*transientAttachment=*/false,
+                                                                 /*supportsTextureBinding=*/false);
     auto multiSampledTextureView2 = multiSampledTexture2.CreateView();
 
     auto singleSampledTexture1 =
@@ -1817,16 +1829,18 @@
 // Test rendering into 2 attachments. The both attachments will use
 // LoadOp::ExpandResolveTexture.
 TEST_P(DawnLoadResolveTextureTest, TwoOutputsDrawThenLoadColor0AndColor1) {
-    auto multiSampledTexture1 = CreateTextureForRenderAttachment(
-        kColorFormat, 4, 1, 1,
-        /*transientAttachment=*/device.HasFeature(wgpu::FeatureName::TransientAttachments),
-        /*supportsTextureBinding=*/false);
+    // TODO(42240662): "resolve_multiple_attachments_in_separate_passes" is currently not working
+    // with DawnLoadResolveTexture feature if there are more than one attachment.
+    DAWN_TEST_UNSUPPORTED_IF(HasResolveMultipleAttachmentInSeparatePassesToggle());
+
+    auto multiSampledTexture1 = CreateTextureForRenderAttachment(kColorFormat, 4, 1, 1,
+                                                                 /*transientAttachment=*/false,
+                                                                 /*supportsTextureBinding=*/false);
     auto multiSampledTextureView1 = multiSampledTexture1.CreateView();
 
-    auto multiSampledTexture2 = CreateTextureForRenderAttachment(
-        kColorFormat, 4, 1, 1,
-        /*transientAttachment=*/device.HasFeature(wgpu::FeatureName::TransientAttachments),
-        /*supportsTextureBinding=*/false);
+    auto multiSampledTexture2 = CreateTextureForRenderAttachment(kColorFormat, 4, 1, 1,
+                                                                 /*transientAttachment=*/false,
+                                                                 /*supportsTextureBinding=*/false);
     auto multiSampledTextureView2 = multiSampledTexture2.CreateView();
 
     auto singleSampledTexture1 =
@@ -1968,16 +1982,18 @@
 // Test ExpandResolveTexture load op rendering with depth test works correctly with
 // two outputs both use ExpandResolveTexture load op.
 TEST_P(DawnLoadResolveTextureTest, TwoOutputsDrawWithDepthTestColor0AndColor1) {
-    auto multiSampledTexture1 = CreateTextureForRenderAttachment(
-        kColorFormat, 4, 1, 1,
-        /*transientAttachment=*/device.HasFeature(wgpu::FeatureName::TransientAttachments),
-        /*supportsTextureBinding=*/false);
+    // TODO(42240662): "resolve_multiple_attachments_in_separate_passes" is currently not working
+    // with DawnLoadResolveTexture feature if there are more than one attachment.
+    DAWN_TEST_UNSUPPORTED_IF(HasResolveMultipleAttachmentInSeparatePassesToggle());
+
+    auto multiSampledTexture1 = CreateTextureForRenderAttachment(kColorFormat, 4, 1, 1,
+                                                                 /*transientAttachment=*/false,
+                                                                 /*supportsTextureBinding=*/false);
     auto multiSampledTextureView1 = multiSampledTexture1.CreateView();
 
-    auto multiSampledTexture2 = CreateTextureForRenderAttachment(
-        kColorFormat, 4, 1, 1,
-        /*transientAttachment=*/device.HasFeature(wgpu::FeatureName::TransientAttachments),
-        /*supportsTextureBinding=*/false);
+    auto multiSampledTexture2 = CreateTextureForRenderAttachment(kColorFormat, 4, 1, 1,
+                                                                 /*transientAttachment=*/false,
+                                                                 /*supportsTextureBinding=*/false);
     auto multiSampledTextureView2 = multiSampledTexture2.CreateView();
 
     auto singleSampledTexture1 =
@@ -2127,6 +2143,7 @@
                       OpenGLESBackend(),
                       VulkanBackend(),
                       VulkanBackend({"always_resolve_into_zero_level_and_layer"}),
+                      VulkanBackend({"resolve_multiple_attachments_in_separate_passes"}),
                       MetalBackend({"emulate_store_and_msaa_resolve"}),
                       MetalBackend({"always_resolve_into_zero_level_and_layer"}),
                       MetalBackend({"always_resolve_into_zero_level_and_layer",
diff --git a/src/tint/lang/spirv/writer/common/option_helper.cc b/src/tint/lang/spirv/writer/common/option_helper.cc
index f209b63..e1723bf 100644
--- a/src/tint/lang/spirv/writer/common/option_helper.cc
+++ b/src/tint/lang/spirv/writer/common/option_helper.cc
@@ -106,6 +106,10 @@
         diagnostics.AddNote(Source{}) << "when processing sampler";
         return Failure{std::move(diagnostics)};
     }
+    if (!valid(options.bindings.input_attachment)) {
+        diagnostics.AddNote(Source{}) << "when processing input_attachment";
+        return Failure{std::move(diagnostics)};
+    }
 
     for (const auto& it : options.bindings.external_texture) {
         const auto& src_binding = it.first;
@@ -201,6 +205,7 @@
     create_remappings(options.bindings.texture);
     create_remappings(options.bindings.storage_texture);
     create_remappings(options.bindings.sampler);
+    create_remappings(options.bindings.input_attachment);
 
     // External textures are re-bound to their plane0 location
     for (const auto& it : options.bindings.external_texture) {
diff --git a/src/tint/lang/spirv/writer/common/options.h b/src/tint/lang/spirv/writer/common/options.h
index 5eedab8..616e28e 100644
--- a/src/tint/lang/spirv/writer/common/options.h
+++ b/src/tint/lang/spirv/writer/common/options.h
@@ -66,6 +66,7 @@
 using Texture = BindingInfo;
 using StorageTexture = BindingInfo;
 using Sampler = BindingInfo;
+using InputAttachment = BindingInfo;
 
 /// An external texture
 struct ExternalTexture {
@@ -94,6 +95,8 @@
 using SamplerBindings = std::unordered_map<BindingPoint, binding::Sampler>;
 // Maps the WGSL binding point to the plane0, plane1, and metadata information for external textures
 using ExternalTextureBindings = std::unordered_map<BindingPoint, binding::ExternalTexture>;
+// Maps the WGSL binding point to the SPIR-V group,binding for input attachments
+using InputAttachmentBindings = std::unordered_map<BindingPoint, binding::InputAttachment>;
 
 /// Binding information
 struct Bindings {
@@ -109,9 +112,18 @@
     SamplerBindings sampler{};
     /// External bindings
     ExternalTextureBindings external_texture{};
+    /// Input attachment bindings
+    InputAttachmentBindings input_attachment{};
 
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
-    TINT_REFLECT(Bindings, uniform, storage, texture, storage_texture, sampler, external_texture);
+    TINT_REFLECT(Bindings,
+                 uniform,
+                 storage,
+                 texture,
+                 storage_texture,
+                 sampler,
+                 external_texture,
+                 input_attachment);
 };
 
 /// Configuration options used for generating SPIR-V.