Metal: Initial implementation of MSAA render to single sampled.

This CL implements initial support for Multisampled render to single
sampled feature on Metal.
- This first version will only support one single color attachment
(plus any depth stencil attachment if any).
- Implicit multi-sampled textures are allocated internally by Dawn and
cached. It could be reused for multiple single-sampled textures of the
same size and format.
  - The implicit multi-sampled texture is deallocated when the last
  single sampled texture using it is released.
- The single-sampled texture is internally treated as resolve target for
the implicit multi-sampled texture in the render pass.
- To support LoadOp=Load, at the beginning of the render pass, the
single sampled texture will be blitted to the multi-sampled texture
using a full screen draw step. This step is implemented on front-end
layer rather than back-end specific layer. It can be reused by D3D
implementation in future patch.

Bug: dawn:1710
Change-Id: I35990b528d13bde14afd0a42cbd95b349466c370
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/135200
Reviewed-by: Austin Eng <enga@chromium.org>
Commit-Queue: Quyen Le <lehoangquyen@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/dawn.json b/dawn.json
index 8a25394..926dea5 100644
--- a/dawn.json
+++ b/dawn.json
@@ -1462,7 +1462,8 @@
             {"value": 1006, "name": "timestamp query inside passes", "tags": ["dawn"]},
             {"value": 1007, "name": "implicit device synchronization", "tags": ["dawn", "native"]},
             {"value": 1008, "name": "surface capabilities", "tags": ["dawn"]},
-            {"value": 1009, "name": "transient attachments", "tags": ["dawn"]}
+            {"value": 1009, "name": "transient attachments", "tags": ["dawn"]},
+            {"value": 1010, "name": "MSAA render to single sampled", "tags": ["dawn"]}
         ]
     },
     "filter mode": {
@@ -1973,6 +1974,7 @@
 
     "render pass color attachment": {
         "category": "structure",
+        "extensible": "in",
         "members": [
             {"name": "view", "type": "texture view", "optional": true},
             {"name": "resolve target", "type": "texture view", "optional": true},
@@ -1981,7 +1983,15 @@
             {"name": "clear value", "type": "color"}
         ]
     },
-
+    "dawn render pass color attachment render to single sampled": {
+        "tags": ["dawn"],
+        "category": "structure",
+        "chained": "in",
+        "chain roots": ["render pass color attachment"],
+        "members": [
+            {"name": "implicit sample count", "type": "uint32_t", "default": 1}
+        ]
+    },
     "render pass depth stencil attachment": {
         "category": "structure",
         "members": [
@@ -2300,6 +2310,16 @@
         ]
     },
 
+    "dawn multisample state render to single sampled": {
+        "tags": ["dawn"],
+        "category": "structure",
+        "chained": "in",
+        "chain roots": ["multisample state"],
+        "members": [
+            {"name": "enabled", "type": "bool", "default": "false"}
+        ]
+    },
+
     "fragment state": {
         "category": "structure",
         "extensible": "in",
@@ -2622,7 +2642,9 @@
             {"value": 1008, "name": "dawn toggles descriptor", "tags": ["dawn", "native"]},
             {"value": 1009, "name": "dawn shader module SPIRV options descriptor", "tags": ["dawn"]},
             {"value": 1010, "name": "request adapter options LUID", "tags": ["dawn", "native"]},
-            {"value": 1011, "name": "request adapter options get GL proc", "tags": ["dawn", "native"]}
+            {"value": 1011, "name": "request adapter options get GL proc", "tags": ["dawn", "native"]},
+            {"value": 1012, "name": "dawn multisample state render to single sampled", "tags": ["dawn"]},
+            {"value": 1013, "name": "dawn render pass color attachment render to single sampled", "tags": ["dawn"]}
         ]
     },
     "texture": {
diff --git a/docs/dawn/features/msaa_render_to_single_samples.md b/docs/dawn/features/msaa_render_to_single_samples.md
new file mode 100644
index 0000000..a03c12d
--- /dev/null
+++ b/docs/dawn/features/msaa_render_to_single_samples.md
@@ -0,0 +1,51 @@
+# MSAA Render To Single Sampled/Multisampled Render To Single Sampled
+
+The `msaa-render-to-single-sampled` feature allows a render pass to include single-sampled attachments while rendering is done with a specified number of samples. When this feature is used, the client doesn't need to explicitly allocate any multi-sammpled color textures. We denote this kind of render passes as "implicit multi-sampled" render passes.
+
+Additional functionalities:
+ - Adds `wgpu::DawnRenderPassColorAttachmentRenderToSingleSampled` as chained struct for `wgpu::RenderPassColorAtttachment` to specify number of samples to be rendered for the respective single-sampled attachment.
+ - Adds `wgpu::DawnMultisampleStateRenderToSingleSampled` as chained struct for `wgpu::RenderPipelineDescriptor::MultisampleState` to indicate that the render pipeline is going to be used in a "implicit multi-sampled" render pass.
+
+Example Usage:
+```
+// Create texture with TextureBinding usage.
+wgpu::TextureDescriptor desc = ...;
+desc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding;
+
+auto texture = device.CreateTexture(&desc);
+
+// Create a render pipeline to be used in a "implicit multi-sampled" render pass.
+wgpu::DawnMultisampleStateRenderToSingleSampled pipelineMSAARenderToSingleSampledDesc;
+pipelineMSAARenderToSingleSampledDesc.enabled = true;
+
+wgpu::RenderPipelineDescriptor pipelineDesc = ...;
+pipelineDesc.multisample.count = 4;
+pipelineDesc.multisample.nextInChain = &pipelineMSAARenderToSingleSampledDesc;
+
+auto pipeline = device.CreateRenderPipeline(&pipelineDesc);
+
+// Create a render pass with "implicit multi-sampled" enabled.
+wgpu::DawnRenderPassColorAttachmentRenderToSingleSampled colorAttachmentRenderToSingleSampledDesc;
+colorAttachmentRenderToSingleSampledDesc.implicitSampleCount = 4;
+
+wgpu::RenderPassDescriptor renderPassDesc = ...;
+renderPassDesc.colorAttachments[0].view = texture.CreateView();
+renderPassDesc.colorAttachments[0].nextInChain
+  = &colorAttachmentRenderToSingleSampledDesc;
+
+auto renderPassEncoder = encoder.BeginRenderPass(&renderPassDesc);
+
+renderPassEncoder.SetPipeline(pipeline);
+renderPassEncoder.Draw(3);
+renderPassEncoder.End();
+
+```
+
+Notes:
+ - If a texture needs to be used as an attachment in a "implicit multi-sampled" render pass, it must have `wgpu::TextureUsage::TextureBinding` usage.
+ - If `wgpu::DawnMultisampleStateRenderToSingleSampled` chained struct is not included in a `wgpu::RenderPipelineDescriptor::MultisampleState`  or if it is included but `enabled` boolean flag is false, then the result render pipeline cannot be used in a "implicit multi-sampled" render pass.
+   - Similarly, a render pipeline created with `wgpu::DawnMultisampleStateRenderToSingleSampled`'s `enabled` flag = `true` won't be able to be used in normal render passes.
+ - If a texture is attached to a "implicit multi-sampled" render pass. It must be single-sampled. It mustn't be assigned to the `resolveTarget` field of the the render pass' color attachment.
+ - Depth stencil textures can be attached to a "implicit multi-sampled" render pass. But its sample count must match the number specified in one color attachment's `wgpu::DawnRenderPassColorAttachmentRenderToSingleSampled`'s `implicitSampleCount` field.
+ - Currently only one color attachment is supported, 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.
diff --git a/src/dawn/native/AttachmentState.cpp b/src/dawn/native/AttachmentState.cpp
index bbb8ecd..ff5416c 100644
--- a/src/dawn/native/AttachmentState.cpp
+++ b/src/dawn/native/AttachmentState.cpp
@@ -15,6 +15,7 @@
 #include "dawn/native/AttachmentState.h"
 
 #include "dawn/common/BitSetIterator.h"
+#include "dawn/native/ChainUtils_autogen.h"
 #include "dawn/native/Device.h"
 #include "dawn/native/ObjectContentHasher.h"
 #include "dawn/native/Texture.h"
@@ -33,10 +34,18 @@
         }
     }
     mDepthStencilFormat = descriptor->depthStencilFormat;
+
+    // TODO(dawn:1710): support MSAA render to single sampled in render bundle.
 }
 
 AttachmentStateBlueprint::AttachmentStateBlueprint(const RenderPipelineDescriptor* descriptor)
     : mSampleCount(descriptor->multisample.count) {
+    const DawnMultisampleStateRenderToSingleSampled* msaaRenderToSingleSampledDesc = nullptr;
+    FindInChain(descriptor->multisample.nextInChain, &msaaRenderToSingleSampledDesc);
+    if (msaaRenderToSingleSampledDesc != nullptr) {
+        mIsMSAARenderToSingleSampledEnabled = msaaRenderToSingleSampledDesc->enabled;
+    }
+
     if (descriptor->fragment != nullptr) {
         ASSERT(descriptor->fragment->targetCount <= kMaxColorAttachments);
         for (ColorAttachmentIndex i(uint8_t(0));
@@ -58,16 +67,31 @@
 AttachmentStateBlueprint::AttachmentStateBlueprint(const RenderPassDescriptor* descriptor) {
     for (ColorAttachmentIndex i(uint8_t(0));
          i < ColorAttachmentIndex(static_cast<uint8_t>(descriptor->colorAttachmentCount)); ++i) {
-        TextureViewBase* attachment = descriptor->colorAttachments[static_cast<uint8_t>(i)].view;
+        const RenderPassColorAttachment& colorAttachment =
+            descriptor->colorAttachments[static_cast<uint8_t>(i)];
+        TextureViewBase* attachment = colorAttachment.view;
         if (attachment == nullptr) {
             continue;
         }
         mColorAttachmentsSet.set(i);
         mColorFormats[i] = attachment->GetFormat().format;
-        if (mSampleCount == 0) {
-            mSampleCount = attachment->GetTexture()->GetSampleCount();
+
+        const DawnRenderPassColorAttachmentRenderToSingleSampled* msaaRenderToSingleSampledDesc =
+            nullptr;
+        FindInChain(colorAttachment.nextInChain, &msaaRenderToSingleSampledDesc);
+        uint32_t attachmentSampleCount;
+        if (msaaRenderToSingleSampledDesc != nullptr &&
+            msaaRenderToSingleSampledDesc->implicitSampleCount > 1) {
+            attachmentSampleCount = msaaRenderToSingleSampledDesc->implicitSampleCount;
+            mIsMSAARenderToSingleSampledEnabled = true;
         } else {
-            ASSERT(mSampleCount == attachment->GetTexture()->GetSampleCount());
+            attachmentSampleCount = attachment->GetTexture()->GetSampleCount();
+        }
+
+        if (mSampleCount == 0) {
+            mSampleCount = attachmentSampleCount;
+        } else {
+            ASSERT(mSampleCount == attachmentSampleCount);
         }
     }
     if (descriptor->depthStencilAttachment != nullptr) {
@@ -100,6 +124,9 @@
     // Hash sample count
     HashCombine(&hash, attachmentState->mSampleCount);
 
+    // Hash MSAA render to single sampled flag
+    HashCombine(&hash, attachmentState->mIsMSAARenderToSingleSampledEnabled);
+
     return hash;
 }
 
@@ -127,6 +154,11 @@
         return false;
     }
 
+    // Both attachment state must either enable MSSA render to single sampled or disable it.
+    if (a->mIsMSAARenderToSingleSampledEnabled != b->mIsMSAARenderToSingleSampledEnabled) {
+        return false;
+    }
+
     return true;
 }
 
@@ -165,4 +197,8 @@
     return mSampleCount;
 }
 
+bool AttachmentState::IsMSAARenderToSingleSampledEnabled() const {
+    return mIsMSAARenderToSingleSampledEnabled;
+}
+
 }  // namespace dawn::native
diff --git a/src/dawn/native/AttachmentState.h b/src/dawn/native/AttachmentState.h
index 815ce29..1356362 100644
--- a/src/dawn/native/AttachmentState.h
+++ b/src/dawn/native/AttachmentState.h
@@ -57,6 +57,8 @@
     // Default (texture format Undefined) indicates there is no depth stencil attachment.
     wgpu::TextureFormat mDepthStencilFormat = wgpu::TextureFormat::Undefined;
     uint32_t mSampleCount = 0;
+
+    bool mIsMSAARenderToSingleSampledEnabled = false;
 };
 
 class AttachmentState final : public AttachmentStateBlueprint,
@@ -70,6 +72,7 @@
     bool HasDepthStencilAttachment() const;
     wgpu::TextureFormat GetDepthStencilFormat() const;
     uint32_t GetSampleCount() const;
+    bool IsMSAARenderToSingleSampledEnabled() const;
 
     size_t ComputeContentHash() override;
 
diff --git a/src/dawn/native/BUILD.gn b/src/dawn/native/BUILD.gn
index 30e6b05..82e342b 100644
--- a/src/dawn/native/BUILD.gn
+++ b/src/dawn/native/BUILD.gn
@@ -203,6 +203,8 @@
     "BindingInfo.h",
     "BlitBufferToDepthStencil.cpp",
     "BlitBufferToDepthStencil.h",
+    "BlitColorToColorWithDraw.cpp",
+    "BlitColorToColorWithDraw.h",
     "BlitDepthToDepth.cpp",
     "BlitDepthToDepth.h",
     "BlitTextureToBuffer.cpp",
diff --git a/src/dawn/native/BindGroup.cpp b/src/dawn/native/BindGroup.cpp
index 6a99551..f49f8ab 100644
--- a/src/dawn/native/BindGroup.cpp
+++ b/src/dawn/native/BindGroup.cpp
@@ -140,14 +140,21 @@
         case BindingInfoType::Texture: {
             SampleTypeBit supportedTypes =
                 texture->GetFormat().GetAspectInfo(aspect).supportedSampleTypes;
-            SampleTypeBit requiredType = SampleTypeToSampleTypeBit(bindingInfo.texture.sampleType);
-
             DAWN_TRY(ValidateCanUseAs(texture, wgpu::TextureUsage::TextureBinding, mode));
 
             DAWN_INVALID_IF(texture->IsMultisampledTexture() != bindingInfo.texture.multisampled,
                             "Sample count (%u) of %s doesn't match expectation (multisampled: %d).",
                             texture->GetSampleCount(), texture, bindingInfo.texture.multisampled);
 
+            SampleTypeBit requiredType;
+            if (bindingInfo.texture.sampleType == kInternalResolveAttachmentSampleType) {
+                // If the binding's sample type is kInternalResolveAttachmentSampleType,
+                // then the supported types must contain float.
+                requiredType = SampleTypeBit::UnfilterableFloat;
+            } else {
+                requiredType = SampleTypeToSampleTypeBit(bindingInfo.texture.sampleType);
+            }
+
             DAWN_INVALID_IF(
                 !(supportedTypes & requiredType),
                 "None of the supported sample types (%s) of %s match the expected sample "
diff --git a/src/dawn/native/BindGroupLayout.cpp b/src/dawn/native/BindGroupLayout.cpp
index eee8193..9cc339d 100644
--- a/src/dawn/native/BindGroupLayout.cpp
+++ b/src/dawn/native/BindGroupLayout.cpp
@@ -101,7 +101,19 @@
         bindingMemberCount++;
         bindingType = BindingInfoType::Texture;
         const TextureBindingLayout& texture = entry.texture;
-        DAWN_TRY(ValidateTextureSampleType(texture.sampleType));
+        // The kInternalResolveAttachmentSampleType is used internally and not a value
+        // in wgpu::TextureSampleType.
+        switch (texture.sampleType) {
+            case kInternalResolveAttachmentSampleType:
+                if (allowInternalBinding) {
+                    break;
+                }
+                // should return validation error.
+                [[fallthrough]];
+            default:
+                DAWN_TRY(ValidateTextureSampleType(texture.sampleType));
+                break;
+        }
 
         // viewDimension defaults to 2D if left undefined, needs validation otherwise.
         wgpu::TextureViewDimension viewDimension = wgpu::TextureViewDimension::e2D;
diff --git a/src/dawn/native/BlitColorToColorWithDraw.cpp b/src/dawn/native/BlitColorToColorWithDraw.cpp
new file mode 100644
index 0000000..610498d
--- /dev/null
+++ b/src/dawn/native/BlitColorToColorWithDraw.cpp
@@ -0,0 +1,225 @@
+// Copyright 2023 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "dawn/native/BlitColorToColorWithDraw.h"
+
+#include "dawn/common/Assert.h"
+#include "dawn/common/HashUtils.h"
+#include "dawn/native/BindGroup.h"
+#include "dawn/native/CommandEncoder.h"
+#include "dawn/native/Device.h"
+#include "dawn/native/InternalPipelineStore.h"
+#include "dawn/native/RenderPassEncoder.h"
+#include "dawn/native/RenderPipeline.h"
+#include "dawn/native/utils/WGPUHelpers.h"
+
+namespace dawn::native {
+
+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);
+}
+)";
+
+constexpr char kBlitToFloatColorFS[] = R"(
+@group(0) @binding(0) var src_tex : texture_2d<f32>;
+
+@fragment fn blit_to_color(@builtin(position) position : vec4f) -> @location(0) vec4f {
+  return textureLoad(src_tex, vec2u(position.xy), 0);
+}
+
+)";
+
+ResultOrError<Ref<RenderPipelineBase>> GetOrCreateColorBlitPipeline(
+    DeviceBase* device,
+    const Format& colorInternalFormat,
+    wgpu::TextureFormat depthStencilFormat,
+    uint32_t sampleCount) {
+    InternalPipelineStore* store = device->GetInternalPipelineStore();
+    BlitColorToColorWithDrawPipelineKey pipelineKey;
+    pipelineKey.colorFormat = colorInternalFormat.format;
+    pipelineKey.depthStencilFormat = depthStencilFormat;
+    pipelineKey.sampleCount = sampleCount;
+    {
+        auto it = store->msaaRenderToSingleSampledColorBlitPipelines.find(pipelineKey);
+        if (it != store->msaaRenderToSingleSampledColorBlitPipelines.end()) {
+            return it->second;
+        }
+    }
+
+    const auto& formatAspectInfo = colorInternalFormat.GetAspectInfo(Aspect::Color);
+
+    // vertex shader's source.
+    ShaderModuleWGSLDescriptor wgslDesc = {};
+    ShaderModuleDescriptor shaderModuleDesc = {};
+    shaderModuleDesc.nextInChain = &wgslDesc;
+    wgslDesc.code = kBlitToColorVS;
+
+    Ref<ShaderModuleBase> vshaderModule;
+    DAWN_TRY_ASSIGN(vshaderModule, device->CreateShaderModule(&shaderModuleDesc));
+
+    // fragment shader's source will depend on color format type.
+    switch (formatAspectInfo.baseType) {
+        case TextureComponentType::Float:
+            wgslDesc.code = kBlitToFloatColorFS;
+            break;
+        default:
+            // TODO(dawn:1710): blitting integer textures are not currently supported.
+            UNREACHABLE();
+            break;
+    }
+    Ref<ShaderModuleBase> fshaderModule;
+    DAWN_TRY_ASSIGN(fshaderModule, device->CreateShaderModule(&shaderModuleDesc));
+
+    FragmentState fragmentState = {};
+    fragmentState.module = fshaderModule.Get();
+    fragmentState.entryPoint = "blit_to_color";
+
+    // Color target state.
+    ColorTargetState colorTarget;
+    colorTarget.format = colorInternalFormat.format;
+
+    fragmentState.targetCount = 1;
+    fragmentState.targets = &colorTarget;
+
+    RenderPipelineDescriptor renderPipelineDesc = {};
+    renderPipelineDesc.label = "blit_color_to_color";
+    renderPipelineDesc.vertex.module = vshaderModule.Get();
+    renderPipelineDesc.vertex.entryPoint = "vert_fullscreen_quad";
+    renderPipelineDesc.fragment = &fragmentState;
+
+    // Depth stencil state.
+    DepthStencilState depthStencilState = {};
+    if (depthStencilFormat != wgpu::TextureFormat::Undefined) {
+        depthStencilState.format = depthStencilFormat;
+        depthStencilState.depthWriteEnabled = false;
+        depthStencilState.depthCompare = wgpu::CompareFunction::Always;
+
+        renderPipelineDesc.depthStencil = &depthStencilState;
+    }
+
+    // Multisample state.
+    ASSERT(sampleCount > 1);
+    renderPipelineDesc.multisample.count = sampleCount;
+    DawnMultisampleStateRenderToSingleSampled msaaRenderToSingleSampledDesc = {};
+    msaaRenderToSingleSampledDesc.enabled = true;
+    renderPipelineDesc.multisample.nextInChain = &msaaRenderToSingleSampledDesc;
+
+    // Bind group layout.
+    Ref<BindGroupLayoutBase> bindGroupLayout;
+    DAWN_TRY_ASSIGN(bindGroupLayout,
+                    utils::MakeBindGroupLayout(
+                        device,
+                        {
+                            {0, wgpu::ShaderStage::Fragment, kInternalResolveAttachmentSampleType,
+                             wgpu::TextureViewDimension::e2D},
+                        },
+                        /* allowInternalBinding */ true));
+    Ref<PipelineLayoutBase> pipelineLayout;
+    DAWN_TRY_ASSIGN(pipelineLayout, utils::MakeBasicPipelineLayout(device, bindGroupLayout));
+    renderPipelineDesc.layout = pipelineLayout.Get();
+
+    Ref<RenderPipelineBase> pipeline;
+    DAWN_TRY_ASSIGN(pipeline, device->CreateRenderPipeline(&renderPipelineDesc));
+
+    store->msaaRenderToSingleSampledColorBlitPipelines[pipelineKey] = pipeline;
+    return pipeline;
+}
+
+}  // namespace
+
+MaybeError BlitMSAARenderToSingleSampledColorWithDraw(
+    DeviceBase* device,
+    RenderPassEncoder* renderEncoder,
+    const RenderPassDescriptor* renderPassDescriptor,
+    uint32_t renderPassImplicitSampleCount) {
+    ASSERT(device->IsLockedByCurrentThreadIfNeeded());
+    ASSERT(device->IsResolveTextureBlitWithDrawSupported());
+
+    // TODO(dawn:1710): support multiple attachments.
+    ASSERT(renderPassDescriptor->colorAttachmentCount == 1);
+
+    // The original color attachment of the render pass will be used as source.
+    TextureViewBase* src = renderPassDescriptor->colorAttachments[0].view;
+    TextureBase* srcTexture = src->GetTexture();
+
+    // ASSERT that the src texture is not multisampled nor having more than 1 layer.
+    ASSERT(srcTexture->GetSampleCount() == 1u);
+    ASSERT(src->GetLayerCount() == 1u);
+    ASSERT(src->GetDimension() == wgpu::TextureViewDimension::e2D);
+
+    wgpu::TextureFormat depthStencilFormat = wgpu::TextureFormat::Undefined;
+    if (renderPassDescriptor->depthStencilAttachment != nullptr) {
+        depthStencilFormat = renderPassDescriptor->depthStencilAttachment->view->GetFormat().format;
+    }
+
+    ASSERT(renderPassImplicitSampleCount > 1);
+
+    Ref<RenderPipelineBase> pipeline;
+    DAWN_TRY_ASSIGN(pipeline,
+                    GetOrCreateColorBlitPipeline(device, src->GetFormat(), depthStencilFormat,
+                                                 renderPassImplicitSampleCount));
+
+    Ref<BindGroupLayoutBase> bgl;
+    DAWN_TRY_ASSIGN(bgl, pipeline->GetBindGroupLayout(0));
+
+    Ref<BindGroupBase> bindGroup;
+    {
+        BindGroupEntry bgEntry = {};
+        bgEntry.binding = 0;
+        bgEntry.textureView = src;
+
+        BindGroupDescriptor bgDesc = {};
+        bgDesc.layout = bgl.Get();
+        bgDesc.entryCount = 1;
+        bgDesc.entries = &bgEntry;
+        DAWN_TRY_ASSIGN(bindGroup, device->CreateBindGroup(&bgDesc, UsageValidationMode::Internal));
+    }
+
+    // Draw to perform the blit.
+    renderEncoder->APISetBindGroup(0, bindGroup.Get(), 0, nullptr);
+    renderEncoder->APISetPipeline(pipeline.Get());
+    renderEncoder->APIDraw(3, 1, 0, 0);
+
+    return {};
+}
+
+size_t BlitColorToColorWithDrawPipelineKey::HashFunc::operator()(
+    const BlitColorToColorWithDrawPipelineKey& key) const {
+    size_t hash = 0;
+
+    HashCombine(&hash, key.colorFormat);
+    HashCombine(&hash, key.depthStencilFormat);
+    HashCombine(&hash, key.sampleCount);
+
+    return hash;
+}
+
+bool BlitColorToColorWithDrawPipelineKey::EqualityFunc::operator()(
+    const BlitColorToColorWithDrawPipelineKey& a,
+    const BlitColorToColorWithDrawPipelineKey& b) const {
+    return a.colorFormat == b.colorFormat && a.depthStencilFormat == b.depthStencilFormat &&
+           a.sampleCount == b.sampleCount;
+}
+
+}  // namespace dawn::native
diff --git a/src/dawn/native/BlitColorToColorWithDraw.h b/src/dawn/native/BlitColorToColorWithDraw.h
new file mode 100644
index 0000000..f821aea
--- /dev/null
+++ b/src/dawn/native/BlitColorToColorWithDraw.h
@@ -0,0 +1,67 @@
+// Copyright 2023 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_DAWN_NATIVE_BLITCOLORTOCOLORWITHDRAW_H_
+#define SRC_DAWN_NATIVE_BLITCOLORTOCOLORWITHDRAW_H_
+
+#include <unordered_map>
+
+#include "dawn/native/Error.h"
+
+namespace dawn::native {
+
+class DeviceBase;
+class RenderPassEncoder;
+struct RenderPassDescriptor;
+class TextureViewBase;
+
+struct BlitColorToColorWithDrawPipelineKey {
+    wgpu::TextureFormat colorFormat;
+    wgpu::TextureFormat depthStencilFormat;
+    uint32_t sampleCount = 1;
+
+    struct HashFunc {
+        size_t operator()(const BlitColorToColorWithDrawPipelineKey& key) const;
+    };
+
+    struct EqualityFunc {
+        bool operator()(const BlitColorToColorWithDrawPipelineKey& a,
+                        const BlitColorToColorWithDrawPipelineKey& b) const;
+    };
+};
+
+using BlitColorToColorWithDrawPipelinesCache =
+    std::unordered_map<BlitColorToColorWithDrawPipelineKey,
+                       Ref<RenderPipelineBase>,
+                       BlitColorToColorWithDrawPipelineKey::HashFunc,
+                       BlitColorToColorWithDrawPipelineKey::EqualityFunc>;
+
+// In a MSAA render to single sampled render pass, a color attachment will be used as resolve
+// target internally and an implicit MSAA texture will be used as the actual color attachment.
+//
+// This function performs the load operation for the render pass by blitting the resolve target (the
+// original color attachment) to the implicit MSAA attachment.
+//
+// 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.
+MaybeError BlitMSAARenderToSingleSampledColorWithDraw(
+    DeviceBase* device,
+    RenderPassEncoder* renderEncoder,
+    const RenderPassDescriptor* renderPassDescriptor,
+    uint32_t renderPassImplicitSampleCount);
+
+}  // namespace dawn::native
+
+#endif  // SRC_DAWN_NATIVE_BLITCOLORTOCOLORWITHDRAW_H_
diff --git a/src/dawn/native/CMakeLists.txt b/src/dawn/native/CMakeLists.txt
index 7f035e8..52ba6b6 100644
--- a/src/dawn/native/CMakeLists.txt
+++ b/src/dawn/native/CMakeLists.txt
@@ -49,6 +49,8 @@
     "BindingInfo.h"
     "BlitBufferToDepthStencil.cpp"
     "BlitBufferToDepthStencil.h"
+    "BlitColorToColorWithDraw.cpp"
+    "BlitColorToColorWithDraw.h"
     "BlitDepthToDepth.cpp"
     "BlitDepthToDepth.h"
     "BlitTextureToBuffer.cpp"
diff --git a/src/dawn/native/CommandEncoder.cpp b/src/dawn/native/CommandEncoder.cpp
index d3559a2..28811e0 100644
--- a/src/dawn/native/CommandEncoder.cpp
+++ b/src/dawn/native/CommandEncoder.cpp
@@ -23,6 +23,7 @@
 #include "dawn/native/ApplyClearColorValueWithDrawHelper.h"
 #include "dawn/native/BindGroup.h"
 #include "dawn/native/BlitBufferToDepthStencil.h"
+#include "dawn/native/BlitColorToColorWithDraw.h"
 #include "dawn/native/BlitDepthToDepth.h"
 #include "dawn/native/BlitTextureToBuffer.h"
 #include "dawn/native/Buffer.h"
@@ -153,16 +154,32 @@
 }
 
 MaybeError ValidateOrSetColorAttachmentSampleCount(const TextureViewBase* colorAttachment,
+                                                   uint32_t implicitSampleCount,
                                                    uint32_t* sampleCount) {
+    uint32_t attachmentSampleCount = 0;
+    std::string implicitPrefixStr;
+    if (implicitSampleCount > 1) {
+        DAWN_INVALID_IF(colorAttachment->GetTexture()->GetSampleCount() != 1,
+                        "Color attachment %s sample count (%u) is not 1 when it has implicit "
+                        "sample count (%u).",
+                        colorAttachment, colorAttachment->GetTexture()->GetSampleCount(),
+                        implicitSampleCount);
+
+        attachmentSampleCount = implicitSampleCount;
+        implicitPrefixStr = "implicit ";
+    } else {
+        attachmentSampleCount = colorAttachment->GetTexture()->GetSampleCount();
+    }
+
     if (*sampleCount == 0) {
-        *sampleCount = colorAttachment->GetTexture()->GetSampleCount();
+        *sampleCount = attachmentSampleCount;
         DAWN_ASSERT(*sampleCount != 0);
     } else {
         DAWN_INVALID_IF(
-            *sampleCount != colorAttachment->GetTexture()->GetSampleCount(),
-            "Color attachment %s sample count (%u) does not match the sample count of the "
+            *sampleCount != attachmentSampleCount,
+            "Color attachment %s %ssample count (%u) does not match the sample count of the "
             "other attachments (%u).",
-            colorAttachment, colorAttachment->GetTexture()->GetSampleCount(), *sampleCount);
+            colorAttachment, implicitPrefixStr, attachmentSampleCount, *sampleCount);
     }
 
     return {};
@@ -225,16 +242,70 @@
     return {};
 }
 
+MaybeError ValidateColorAttachmentRenderToSingleSampled(
+    const DeviceBase* device,
+    const RenderPassColorAttachment& colorAttachment,
+    const DawnRenderPassColorAttachmentRenderToSingleSampled* msaaRenderToSingleSampledDesc) {
+    ASSERT(msaaRenderToSingleSampledDesc != nullptr);
+
+    DAWN_INVALID_IF(
+        !device->HasFeature(Feature::MSAARenderToSingleSampled),
+        "The color attachment %s has implicit sample count while the %s feature is not enabled.",
+        colorAttachment.view, FeatureEnumToAPIFeature(Feature::MSAARenderToSingleSampled));
+
+    DAWN_INVALID_IF(!IsValidSampleCount(msaaRenderToSingleSampledDesc->implicitSampleCount) ||
+                        msaaRenderToSingleSampledDesc->implicitSampleCount <= 1,
+                    "The color attachment %s's implicit sample count (%u) is not supported.",
+                    colorAttachment.view, msaaRenderToSingleSampledDesc->implicitSampleCount);
+
+    DAWN_INVALID_IF(!colorAttachment.view->GetTexture()->IsImplicitMSAARenderTextureViewSupported(),
+                    "Color attachment %s was not created with %s usage, which is required for "
+                    "having implicit sample count (%u).",
+                    colorAttachment.view, wgpu::TextureUsage::TextureBinding,
+                    msaaRenderToSingleSampledDesc->implicitSampleCount);
+
+    DAWN_INVALID_IF(!colorAttachment.view->GetFormat().supportsResolveTarget,
+                    "The color attachment %s format (%s) does not support being used with "
+                    "implicit sample count (%u). The format does not support resolve.",
+                    colorAttachment.view, colorAttachment.view->GetFormat().format,
+                    msaaRenderToSingleSampledDesc->implicitSampleCount);
+
+    DAWN_INVALID_IF(colorAttachment.resolveTarget != nullptr,
+                    "Cannot set %s as a resolve target. No resolve target should be specified "
+                    "for the color attachment %s with implicit sample count (%u).",
+                    colorAttachment.resolveTarget, colorAttachment.view,
+                    msaaRenderToSingleSampledDesc->implicitSampleCount);
+
+    return {};
+}
+
 MaybeError ValidateRenderPassColorAttachment(DeviceBase* device,
                                              const RenderPassColorAttachment& colorAttachment,
                                              uint32_t* width,
                                              uint32_t* height,
                                              uint32_t* sampleCount,
+                                             uint32_t* implicitSampleCount,
                                              UsageValidationMode usageValidationMode) {
     TextureViewBase* attachment = colorAttachment.view;
     if (attachment == nullptr) {
         return {};
     }
+
+    DAWN_TRY(ValidateSingleSType(colorAttachment.nextInChain,
+                                 wgpu::SType::DawnRenderPassColorAttachmentRenderToSingleSampled));
+
+    const DawnRenderPassColorAttachmentRenderToSingleSampled* msaaRenderToSingleSampledDesc =
+        nullptr;
+    FindInChain(colorAttachment.nextInChain, &msaaRenderToSingleSampledDesc);
+    if (msaaRenderToSingleSampledDesc) {
+        DAWN_TRY(ValidateColorAttachmentRenderToSingleSampled(device, colorAttachment,
+                                                              msaaRenderToSingleSampledDesc));
+        *implicitSampleCount = msaaRenderToSingleSampledDesc->implicitSampleCount;
+        // Note: we don't need to check whether the implicit sample count of different attachments
+        // are the same. That already is done by indirectly comparing the sample count in
+        // ValidateOrSetColorAttachmentSampleCount.
+    }
+
     DAWN_TRY(device->ValidateObject(attachment));
     DAWN_TRY(ValidateCanUseAs(attachment->GetTexture(), wgpu::TextureUsage::RenderAttachment,
                               usageValidationMode));
@@ -266,9 +337,14 @@
                         "Color clear value (%s) contain a NaN.", &clearValue);
     }
 
-    DAWN_TRY(ValidateOrSetColorAttachmentSampleCount(attachment, sampleCount));
+    DAWN_TRY(
+        ValidateOrSetColorAttachmentSampleCount(attachment, *implicitSampleCount, sampleCount));
 
-    DAWN_TRY(ValidateResolveTarget(device, colorAttachment, usageValidationMode));
+    if (*implicitSampleCount <= 1) {
+        // This step is skipped if implicitSampleCount > 1, because in that case, there shoudn't be
+        // any explicit resolveTarget specified.
+        DAWN_TRY(ValidateResolveTarget(device, colorAttachment, usageValidationMode));
+    }
 
     DAWN_TRY(ValidateAttachmentArrayLayersAndLevelCount(attachment));
     DAWN_TRY(ValidateOrSetAttachmentSize(attachment, width, height));
@@ -419,6 +495,7 @@
                                         uint32_t* width,
                                         uint32_t* height,
                                         uint32_t* sampleCount,
+                                        uint32_t* implicitSampleCount,
                                         UsageValidationMode usageValidationMode) {
     DAWN_TRY(ValidateSingleSType(descriptor->nextInChain,
                                  wgpu::SType::RenderPassDescriptorMaxDrawCount));
@@ -432,10 +509,10 @@
     bool isAllColorAttachmentNull = true;
     ColorAttachmentFormats colorAttachmentFormats;
     for (uint32_t i = 0; i < descriptor->colorAttachmentCount; ++i) {
-        DAWN_TRY_CONTEXT(
-            ValidateRenderPassColorAttachment(device, descriptor->colorAttachments[i], width,
-                                              height, sampleCount, usageValidationMode),
-            "validating colorAttachments[%u].", i);
+        DAWN_TRY_CONTEXT(ValidateRenderPassColorAttachment(
+                             device, descriptor->colorAttachments[i], width, height, sampleCount,
+                             implicitSampleCount, usageValidationMode),
+                         "validating colorAttachments[%u].", i);
         if (descriptor->colorAttachments[i].view) {
             isAllColorAttachmentNull = false;
             colorAttachmentFormats->push_back(&descriptor->colorAttachments[i].view->GetFormat());
@@ -506,6 +583,15 @@
         descriptor->colorAttachmentCount == 0 && descriptor->depthStencilAttachment == nullptr,
         "Render pass has no attachments.");
 
+    if (*implicitSampleCount > 1) {
+        // TODO(dawn:1710): support multiple attachments.
+        DAWN_INVALID_IF(
+            descriptor->colorAttachmentCount != 1,
+            "colorAttachmentCount (%u) is not supported when the render pass has implicit sample "
+            "count (%u). (Currently) colorAttachmentCount = 1 is supported.",
+            descriptor->colorAttachmentCount, *implicitSampleCount);
+    }
+
     return {};
 }
 
@@ -642,6 +728,25 @@
     return true;
 }
 
+// Load resolve texture to MSAA attachment if needed.
+MaybeError ApplyMSAARenderToSingleSampledLoadOp(DeviceBase* device,
+                                                RenderPassEncoder* renderPassEncoder,
+                                                const RenderPassDescriptor* renderPassDescriptor,
+                                                uint32_t implicitSampleCount) {
+    // TODO(dawn:1710): support multiple attachments.
+    ASSERT(renderPassDescriptor->colorAttachmentCount == 1);
+    if (renderPassDescriptor->colorAttachments[0].loadOp != wgpu::LoadOp::Load) {
+        return {};
+    }
+
+    // TODO(dawn:1710): support loading resolve texture on platforms that don't support reading
+    // it in fragment shader such as vulkan.
+    ASSERT(device->IsResolveTextureBlitWithDrawSupported());
+
+    // Read implicit resolve texture in fragment shader and copy to the implicit MSAA attachment.
+    return BlitMSAARenderToSingleSampledColorWithDraw(device, renderPassEncoder,
+                                                      renderPassDescriptor, implicitSampleCount);
+}
 // Tracks the temporary resolve attachments used when the AlwaysResolveIntoZeroLevelAndLayer toggle
 // is active so that the results can be copied from the temporary resolve attachment into the
 // intended target after the render pass is complete.
@@ -847,6 +952,8 @@
     uint32_t width = 0;
     uint32_t height = 0;
     uint32_t sampleCount = 0;
+    // The implicit multisample count used by MSAA render to single sampled.
+    uint32_t implicitSampleCount = 0;
     bool depthReadOnly = false;
     bool stencilReadOnly = false;
     Ref<AttachmentState> attachmentState;
@@ -857,9 +964,10 @@
         this,
         [&](CommandAllocator* allocator) -> MaybeError {
             DAWN_TRY(ValidateRenderPassDescriptor(device, descriptor, &width, &height, &sampleCount,
-                                                  mUsageValidationMode));
+                                                  &implicitSampleCount, mUsageValidationMode));
 
-            ASSERT(width > 0 && height > 0 && sampleCount > 0);
+            ASSERT(width > 0 && height > 0 && sampleCount > 0 &&
+                   (implicitSampleCount == 0 || implicitSampleCount == sampleCount));
 
             mEncodingContext.WillBeginRenderPass();
             BeginRenderPassCmd* cmd =
@@ -872,19 +980,38 @@
             for (ColorAttachmentIndex index :
                  IterateBitSet(cmd->attachmentState->GetColorAttachmentsMask())) {
                 uint8_t i = static_cast<uint8_t>(index);
-                TextureViewBase* view = descriptor->colorAttachments[i].view;
-                TextureViewBase* resolveTarget = descriptor->colorAttachments[i].resolveTarget;
+                TextureViewBase* colorTarget;
+                TextureViewBase* resolveTarget;
 
-                cmd->colorAttachments[index].view = view;
+                if (implicitSampleCount <= 1) {
+                    colorTarget = descriptor->colorAttachments[i].view;
+                    resolveTarget = descriptor->colorAttachments[i].resolveTarget;
+
+                    cmd->colorAttachments[index].view = colorTarget;
+                    cmd->colorAttachments[index].loadOp = descriptor->colorAttachments[i].loadOp;
+                    cmd->colorAttachments[index].storeOp = descriptor->colorAttachments[i].storeOp;
+                } else {
+                    // We use an implicit MSAA texture and resolve to the client supplied
+                    // attachment.
+                    resolveTarget = descriptor->colorAttachments[i].view;
+                    Ref<TextureViewBase> implicitMSAATargetRef;
+                    DAWN_TRY_ASSIGN(implicitMSAATargetRef,
+                                    device->CreateImplicitMSAARenderTextureViewFor(
+                                        resolveTarget->GetTexture(), implicitSampleCount));
+                    colorTarget = implicitMSAATargetRef.Get();
+
+                    cmd->colorAttachments[index].view = std::move(implicitMSAATargetRef);
+                    cmd->colorAttachments[index].loadOp = wgpu::LoadOp::Clear;
+                    cmd->colorAttachments[index].storeOp = wgpu::StoreOp::Discard;
+                }
+
                 cmd->colorAttachments[index].resolveTarget = resolveTarget;
-                cmd->colorAttachments[index].loadOp = descriptor->colorAttachments[i].loadOp;
-                cmd->colorAttachments[index].storeOp = descriptor->colorAttachments[i].storeOp;
 
                 Color color = descriptor->colorAttachments[i].clearValue;
                 cmd->colorAttachments[index].clearColor =
-                    ClampClearColorValueToLegalRange(color, view->GetFormat());
+                    ClampClearColorValueToLegalRange(color, colorTarget->GetFormat());
 
-                usageTracker.TextureViewUsedAs(view, wgpu::TextureUsage::RenderAttachment);
+                usageTracker.TextureViewUsedAs(colorTarget, wgpu::TextureUsage::RenderAttachment);
 
                 if (resolveTarget != nullptr) {
                     usageTracker.TextureViewUsedAs(resolveTarget,
@@ -1006,13 +1133,20 @@
 
         mEncodingContext.EnterPass(passEncoder.Get());
 
-        if (ShouldApplyClearBigIntegerColorValueWithDraw(device, descriptor)) {
-            MaybeError error =
-                ApplyClearBigIntegerColorValueWithDraw(passEncoder.Get(), descriptor);
-            if (error.IsError()) {
-                return RenderPassEncoder::MakeError(device, this, &mEncodingContext,
-                                                    descriptor ? descriptor->label : nullptr);
-            }
+        MaybeError error;
+
+        if (implicitSampleCount > 1) {
+            error = ApplyMSAARenderToSingleSampledLoadOp(device, passEncoder.Get(), descriptor,
+                                                         implicitSampleCount);
+        } else if (ShouldApplyClearBigIntegerColorValueWithDraw(device, descriptor)) {
+            // This is skipped if implicitSampleCount > 1. Because implicitSampleCount > 1 is only
+            // supported for non-integer textures.
+            error = ApplyClearBigIntegerColorValueWithDraw(passEncoder.Get(), descriptor);
+        }
+
+        if (device->ConsumedError(std::move(error))) {
+            return RenderPassEncoder::MakeError(device, this, &mEncodingContext,
+                                                descriptor ? descriptor->label : nullptr);
         }
 
         return passEncoder;
diff --git a/src/dawn/native/CommandValidation.cpp b/src/dawn/native/CommandValidation.cpp
index 86abb419..1435be5 100644
--- a/src/dawn/native/CommandValidation.cpp
+++ b/src/dawn/native/CommandValidation.cpp
@@ -59,13 +59,21 @@
             [&](const SubresourceRange&, const wgpu::TextureUsage& usage) -> MaybeError {
                 bool readOnly = IsSubset(usage, kReadOnlyTextureUsages);
                 bool singleUse = wgpu::HasZeroOrOneBits(usage);
-                if (!readOnly && !singleUse) {
-                    return DAWN_VALIDATION_ERROR(
-                        "%s usage (%s) includes writable usage and another usage in the same "
-                        "synchronization scope.",
-                        scope.textures[i], usage);
+                if (readOnly || singleUse) {
+                    return {};
                 }
-                return {};
+                // kResolveTextureLoadAndStoreUsages are kResolveAttachmentLoadingUsage &
+                // RenderAttachment usage used in the same pass.
+                // This is accepted because kResolveAttachmentLoadingUsage is an internal loading
+                // operation for blitting a resolve target to an MSAA attachment. And there won't be
+                // and read-after-write hazard.
+                if (usage == kResolveTextureLoadAndStoreUsages) {
+                    return {};
+                }
+                return DAWN_VALIDATION_ERROR(
+                    "%s usage (%s) includes writable usage and another usage in the same "
+                    "synchronization scope.",
+                    scope.textures[i], usage);
             }));
     }
     return {};
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index 88b6edd..b1f1496 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -926,6 +926,31 @@
     mCaches->computePipelines.Erase(obj);
 }
 
+ResultOrError<Ref<TextureViewBase>> DeviceBase::CreateImplicitMSAARenderTextureViewFor(
+    const TextureBase* singleSampledTexture,
+    uint32_t sampleCount) {
+    ASSERT(IsLockedByCurrentThreadIfNeeded());
+
+    TextureDescriptor desc = {};
+    desc.dimension = wgpu::TextureDimension::e2D;
+    desc.format = singleSampledTexture->GetFormat().format;
+    desc.size = {singleSampledTexture->GetWidth(), singleSampledTexture->GetHeight(), 1};
+    desc.sampleCount = sampleCount;
+    desc.usage = wgpu::TextureUsage::RenderAttachment;
+    if (HasFeature(Feature::TransientAttachments)) {
+        desc.usage = desc.usage | wgpu::TextureUsage::TransientAttachment;
+    }
+
+    Ref<TextureBase> msaaTexture;
+    Ref<TextureViewBase> msaaTextureView;
+
+    DAWN_TRY_ASSIGN(msaaTexture, CreateTexture(&desc));
+
+    DAWN_TRY_ASSIGN(msaaTextureView, msaaTexture->CreateView());
+
+    return std::move(msaaTextureView);
+}
+
 ResultOrError<Ref<TextureViewBase>>
 DeviceBase::GetOrCreatePlaceholderTextureViewForExternalTexture() {
     if (!mExternalTexturePlaceholderView.Get()) {
@@ -1991,6 +2016,10 @@
     return false;
 }
 
+bool DeviceBase::IsResolveTextureBlitWithDrawSupported() const {
+    return false;
+}
+
 uint64_t DeviceBase::GetBufferCopyOffsetAlignmentForDepthStencil() const {
     // For depth-stencil texture, buffer offset must be a multiple of 4, which is required
     // by WebGPU and Vulkan SPEC.
diff --git a/src/dawn/native/Device.h b/src/dawn/native/Device.h
index a14f7bd..01fbc34 100644
--- a/src/dawn/native/Device.h
+++ b/src/dawn/native/Device.h
@@ -202,6 +202,10 @@
 
     void UncacheComputePipeline(ComputePipelineBase* obj);
 
+    ResultOrError<Ref<TextureViewBase>> CreateImplicitMSAARenderTextureViewFor(
+        const TextureBase* singleSampledTexture,
+        uint32_t sampleCount);
+
     ResultOrError<Ref<TextureViewBase>> GetOrCreatePlaceholderTextureViewForExternalTexture();
 
     ResultOrError<Ref<PipelineLayoutBase>> GetOrCreatePipelineLayout(
@@ -395,6 +399,10 @@
     virtual bool ShouldDuplicateParametersForDrawIndirect(
         const RenderPipelineBase* renderPipelineBase) 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;
+
     bool HasFeature(Feature feature) const;
 
     const CombinedLimits& GetLimits() const;
diff --git a/src/dawn/native/Features.cpp b/src/dawn/native/Features.cpp
index c00de74..a63eb53 100644
--- a/src/dawn/native/Features.cpp
+++ b/src/dawn/native/Features.cpp
@@ -117,6 +117,10 @@
       "Support transient attachments that allow render pass operations to stay in tile memory, "
       "avoiding VRAM traffic and potentially avoiding VRAM allocation for the textures.",
       "https://bugs.chromium.org/p/dawn/issues/detail?id=1695", FeatureInfo::FeatureState::Stable}},
+    {Feature::MSAARenderToSingleSampled,
+     {"msaa-render-to-single-sampled",
+      "Support multisampled rendering on single-sampled attachments efficiently.",
+      "https://bugs.chromium.org/p/dawn/issues/detail?id=1710", FeatureInfo::FeatureState::Stable}},
 }};
 
 Feature FromAPIFeature(wgpu::FeatureName feature) {
@@ -167,6 +171,8 @@
             return Feature::TransientAttachments;
         case wgpu::FeatureName::Float32Filterable:
             return Feature::Float32Filterable;
+        case wgpu::FeatureName::MSAARenderToSingleSampled:
+            return Feature::MSAARenderToSingleSampled;
     }
     return Feature::InvalidEnum;
 }
@@ -213,6 +219,8 @@
             return wgpu::FeatureName::TransientAttachments;
         case Feature::Float32Filterable:
             return wgpu::FeatureName::Float32Filterable;
+        case Feature::MSAARenderToSingleSampled:
+            return wgpu::FeatureName::MSAARenderToSingleSampled;
 
         case Feature::EnumCount:
             break;
diff --git a/src/dawn/native/Features.h b/src/dawn/native/Features.h
index f8507fa..ef27ff6 100644
--- a/src/dawn/native/Features.h
+++ b/src/dawn/native/Features.h
@@ -49,6 +49,7 @@
     ImplicitDeviceSynchronization,
     SurfaceCapabilities,
     TransientAttachments,
+    MSAARenderToSingleSampled,
 
     EnumCount,
     InvalidEnum = EnumCount,
diff --git a/src/dawn/native/InternalPipelineStore.h b/src/dawn/native/InternalPipelineStore.h
index 9f96b22..408f0aa 100644
--- a/src/dawn/native/InternalPipelineStore.h
+++ b/src/dawn/native/InternalPipelineStore.h
@@ -20,6 +20,7 @@
 
 #include "dawn/common/HashUtils.h"
 #include "dawn/native/ApplyClearColorValueWithDrawHelper.h"
+#include "dawn/native/BlitColorToColorWithDraw.h"
 #include "dawn/native/ObjectBase.h"
 #include "dawn/native/ScratchBuffer.h"
 #include "dawn/native/dawn_platform.h"
@@ -84,6 +85,8 @@
     std::unordered_map<wgpu::TextureFormat, BlitR8ToStencilPipelines> blitR8ToStencilPipelines;
 
     std::unordered_map<wgpu::TextureFormat, Ref<RenderPipelineBase>> depthBlitPipelines;
+
+    BlitColorToColorWithDrawPipelinesCache msaaRenderToSingleSampledColorBlitPipelines;
 };
 
 }  // namespace dawn::native
diff --git a/src/dawn/native/PassResourceUsageTracker.cpp b/src/dawn/native/PassResourceUsageTracker.cpp
index e1f1ae6..66bcb94 100644
--- a/src/dawn/native/PassResourceUsageTracker.cpp
+++ b/src/dawn/native/PassResourceUsageTracker.cpp
@@ -115,7 +115,14 @@
 
             case BindingInfoType::Texture: {
                 TextureViewBase* view = group->GetBindingAsTextureView(bindingIndex);
-                TextureViewUsedAs(view, wgpu::TextureUsage::TextureBinding);
+                switch (bindingInfo.texture.sampleType) {
+                    case kInternalResolveAttachmentSampleType:
+                        TextureViewUsedAs(view, kResolveAttachmentLoadingUsage);
+                        break;
+                    default:
+                        TextureViewUsedAs(view, wgpu::TextureUsage::TextureBinding);
+                        break;
+                }
                 break;
             }
 
diff --git a/src/dawn/native/RenderPipeline.cpp b/src/dawn/native/RenderPipeline.cpp
index 6b39cc6..33dd20d 100644
--- a/src/dawn/native/RenderPipeline.cpp
+++ b/src/dawn/native/RenderPipeline.cpp
@@ -248,8 +248,19 @@
     return {};
 }
 
-MaybeError ValidateMultisampleState(const MultisampleState* descriptor) {
-    DAWN_INVALID_IF(descriptor->nextInChain != nullptr, "nextInChain must be nullptr.");
+MaybeError ValidateMultisampleState(const DeviceBase* device, const MultisampleState* descriptor) {
+    const DawnMultisampleStateRenderToSingleSampled* msaaRenderToSingleSampledDesc = nullptr;
+    FindInChain(descriptor->nextInChain, &msaaRenderToSingleSampledDesc);
+    if (msaaRenderToSingleSampledDesc != nullptr) {
+        DAWN_INVALID_IF(!device->HasFeature(Feature::MSAARenderToSingleSampled),
+                        "The msaaRenderToSingleSampledDesc is not empty while the "
+                        "msaa-render-to-single-sampled feature is not enabled.");
+
+        DAWN_INVALID_IF(descriptor->count <= 1,
+                        "The msaaRenderToSingleSampledDesc is not empty while multisample count "
+                        "(%u) is not > 1.",
+                        descriptor->count);
+    }
 
     DAWN_INVALID_IF(!IsValidSampleCount(descriptor->count),
                     "Multisample count (%u) is not supported.", descriptor->count);
@@ -601,7 +612,7 @@
                          "validating depthStencil state.");
     }
 
-    DAWN_TRY_CONTEXT(ValidateMultisampleState(&descriptor->multisample),
+    DAWN_TRY_CONTEXT(ValidateMultisampleState(device, &descriptor->multisample),
                      "validating multisample state.");
 
     DAWN_INVALID_IF(
diff --git a/src/dawn/native/ShaderModule.cpp b/src/dawn/native/ShaderModule.cpp
index ecb2ab5..ece5cd9 100644
--- a/src/dawn/native/ShaderModule.cpp
+++ b/src/dawn/native/ShaderModule.cpp
@@ -419,8 +419,16 @@
                 layoutInfo.texture.multisampled, shaderInfo.texture.multisampled);
 
             // TODO(dawn:563): Provide info about the sample types.
-            DAWN_INVALID_IF(!(SampleTypeToSampleTypeBit(layoutInfo.texture.sampleType) &
-                              shaderInfo.texture.compatibleSampleTypes),
+            SampleTypeBit requiredType;
+            if (layoutInfo.texture.sampleType == kInternalResolveAttachmentSampleType) {
+                // If the layout's texture's sample type is kInternalResolveAttachmentSampleType,
+                // then the shader's compatible sample types must contain float.
+                requiredType = SampleTypeBit::UnfilterableFloat;
+            } else {
+                requiredType = SampleTypeToSampleTypeBit(layoutInfo.texture.sampleType);
+            }
+
+            DAWN_INVALID_IF(!(shaderInfo.texture.compatibleSampleTypes & requiredType),
                             "The sample type in the shader is not compatible with the "
                             "sample type of the layout.");
 
diff --git a/src/dawn/native/Texture.cpp b/src/dawn/native/Texture.cpp
index 4e94e6a..f62d240 100644
--- a/src/dawn/native/Texture.cpp
+++ b/src/dawn/native/Texture.cpp
@@ -362,7 +362,8 @@
 
     DAWN_INVALID_IF(
         internalUsageDesc != nullptr && !device->HasFeature(Feature::DawnInternalUsages),
-        "The internalUsageDesc is not empty while the dawn-internal-usages feature is not enabled");
+        "The internalUsageDesc is not empty while the dawn-internal-usages feature is not "
+        "enabled");
 
     const Format* format;
     DAWN_TRY_ASSIGN(format, device->GetInternalFormat(descriptor->format));
@@ -577,8 +578,9 @@
     }
     GetObjectTrackingList()->Track(this);
 
-    // dawn:1569: If a texture with multiple array layers or mip levels is specified as a texture
-    // attachment when this toggle is active, it needs to be given CopyDst usage internally.
+    // dawn:1569: If a texture with multiple array layers or mip levels is specified as a
+    // texture attachment when this toggle is active, it needs to be given CopyDst usage
+    // internally.
     bool applyAlwaysResolveIntoZeroLevelAndLayerToggle =
         device->IsToggleEnabled(Toggle::AlwaysResolveIntoZeroLevelAndLayer) &&
         (GetArrayLayers() > 1 || GetNumMipLevels() > 1) &&
@@ -864,6 +866,10 @@
     return result.Detach();
 }
 
+bool TextureBase::IsImplicitMSAARenderTextureViewSupported() const {
+    return (GetUsage() & wgpu::TextureUsage::TextureBinding) != 0;
+}
+
 void TextureBase::APIDestroy() {
     Destroy();
 }
diff --git a/src/dawn/native/Texture.h b/src/dawn/native/Texture.h
index 6914e70..1748b03 100644
--- a/src/dawn/native/Texture.h
+++ b/src/dawn/native/Texture.h
@@ -50,6 +50,11 @@
 static constexpr wgpu::TextureUsage kReadOnlyTextureUsages =
     wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::TextureBinding | kReadOnlyRenderAttachment;
 
+// Valid texture usages for a resolve texture that are loaded from at the beginning of a render
+// pass.
+static constexpr wgpu::TextureUsage kResolveTextureLoadAndStoreUsages =
+    kResolveAttachmentLoadingUsage | wgpu::TextureUsage::RenderAttachment;
+
 class TextureBase : public ApiObjectBase {
   public:
     enum class TextureState { OwnedInternal, OwnedExternal, Destroyed };
@@ -105,6 +110,8 @@
         const TextureViewDescriptor* descriptor = nullptr);
     ApiObjectList* GetViewTrackingList();
 
+    bool IsImplicitMSAARenderTextureViewSupported() const;
+
     // Dawn API
     TextureViewBase* APICreateView(const TextureViewDescriptor* descriptor = nullptr);
     void APIDestroy();
diff --git a/src/dawn/native/dawn_platform.h b/src/dawn/native/dawn_platform.h
index c5229ae..65b040f 100644
--- a/src/dawn/native/dawn_platform.h
+++ b/src/dawn/native/dawn_platform.h
@@ -57,8 +57,16 @@
 static constexpr wgpu::TextureUsage kAgainAsRenderAttachment =
     static_cast<wgpu::TextureUsage>(0x80000001);
 
+// Add an extra texture usage (load resolve texture to MSAA) for render pass resource tracking
+static constexpr wgpu::TextureUsage kResolveAttachmentLoadingUsage =
+    static_cast<wgpu::TextureUsage>(0x10000000);
+
 static constexpr wgpu::BufferBindingType kInternalStorageBufferBinding =
     static_cast<wgpu::BufferBindingType>(0xFFFFFFFF);
+
+// Extra TextureSampleType for sampling from a resolve attachment.
+static constexpr wgpu::TextureSampleType kInternalResolveAttachmentSampleType =
+    static_cast<wgpu::TextureSampleType>(0xFFFFFFFF);
 }  // namespace dawn::native
 
 #endif  // SRC_DAWN_NATIVE_DAWN_PLATFORM_H_
diff --git a/src/dawn/native/metal/BackendMTL.mm b/src/dawn/native/metal/BackendMTL.mm
index 46fb215..12e0d8c 100644
--- a/src/dawn/native/metal/BackendMTL.mm
+++ b/src/dawn/native/metal/BackendMTL.mm
@@ -534,6 +534,7 @@
         EnableFeature(Feature::RG11B10UfloatRenderable);
         EnableFeature(Feature::BGRA8UnormStorage);
         EnableFeature(Feature::SurfaceCapabilities);
+        EnableFeature(Feature::MSAARenderToSingleSampled);
     }
 
     void InitializeVendorArchitectureImpl() override {
diff --git a/src/dawn/native/metal/DeviceMTL.h b/src/dawn/native/metal/DeviceMTL.h
index 55385db..ea876a0 100644
--- a/src/dawn/native/metal/DeviceMTL.h
+++ b/src/dawn/native/metal/DeviceMTL.h
@@ -78,6 +78,8 @@
 
     float GetTimestampPeriodInNS() const override;
 
+    bool IsResolveTextureBlitWithDrawSupported() 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 b82c167..9ad20b6 100644
--- a/src/dawn/native/metal/DeviceMTL.mm
+++ b/src/dawn/native/metal/DeviceMTL.mm
@@ -503,6 +503,10 @@
     return mTimestampPeriod;
 }
 
+bool Device::IsResolveTextureBlitWithDrawSupported() const {
+    return true;
+}
+
 bool Device::UseCounterSamplingAtCommandBoundary() const {
     return mCounterSamplingAtCommandBoundary;
 }
diff --git a/src/dawn/native/null/DeviceNull.cpp b/src/dawn/native/null/DeviceNull.cpp
index c85dfb7..be3b1f6 100644
--- a/src/dawn/native/null/DeviceNull.cpp
+++ b/src/dawn/native/null/DeviceNull.cpp
@@ -503,6 +503,10 @@
     return 1.0f;
 }
 
+bool Device::IsResolveTextureBlitWithDrawSupported() const {
+    return true;
+}
+
 void Device::ForceEventualFlushOfCommands() {}
 
 Texture::Texture(DeviceBase* device, const TextureDescriptor* descriptor, TextureState state)
diff --git a/src/dawn/native/null/DeviceNull.h b/src/dawn/native/null/DeviceNull.h
index 1800bc5..6cacc8f 100644
--- a/src/dawn/native/null/DeviceNull.h
+++ b/src/dawn/native/null/DeviceNull.h
@@ -122,6 +122,8 @@
 
     float GetTimestampPeriodInNS() const override;
 
+    bool IsResolveTextureBlitWithDrawSupported() const override;
+
     void ForceEventualFlushOfCommands() override;
 
   private:
diff --git a/src/dawn/native/webgpu_absl_format.cpp b/src/dawn/native/webgpu_absl_format.cpp
index 2159a66..4481f42 100644
--- a/src/dawn/native/webgpu_absl_format.cpp
+++ b/src/dawn/native/webgpu_absl_format.cpp
@@ -199,7 +199,14 @@
         s->Append(absl::StrFormat("depthStencilFormat: %s, ", value->GetDepthStencilFormat()));
     }
 
-    s->Append(absl::StrFormat("sampleCount: %u }", value->GetSampleCount()));
+    s->Append(absl::StrFormat("sampleCount: %u", value->GetSampleCount()));
+
+    if (value->GetDevice()->HasFeature(Feature::MSAARenderToSingleSampled)) {
+        s->Append(absl::StrFormat(", msaaRenderToSingleSampled: %d",
+                                  value->IsMSAARenderToSingleSampledEnabled()));
+    }
+
+    s->Append(" }");
 
     return {true};
 }
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index f5904f7..ac9a3e1 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -671,6 +671,7 @@
 
   sources += [
     "white_box/BufferAllocatedSizeTests.cpp",
+    "white_box/InternalResolveAttachmentSampleTypeTests.cpp",
     "white_box/InternalResourceUsageTests.cpp",
     "white_box/InternalStorageBufferBindingTests.cpp",
     "white_box/QueryInternalShaderTests.cpp",
diff --git a/src/dawn/tests/end2end/MultisampledRenderingTests.cpp b/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
index 2bbf8a8..75cba45 100644
--- a/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
+++ b/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
@@ -48,7 +48,8 @@
         bool testDepth,
         uint32_t sampleMask = 0xFFFFFFFF,
         bool alphaToCoverageEnabled = false,
-        bool flipTriangle = false) {
+        bool flipTriangle = false,
+        bool enableMSAARenderToSingleSampled = false) {
         const char* kFsOneOutputWithDepth = R"(
             struct U {
                 color : vec4f,
@@ -81,12 +82,13 @@
         const char* fs = testDepth ? kFsOneOutputWithDepth : kFsOneOutputWithoutDepth;
 
         return CreateRenderPipelineForTest(fs, 1, testDepth, sampleMask, alphaToCoverageEnabled,
-                                           flipTriangle);
+                                           flipTriangle, enableMSAARenderToSingleSampled);
     }
 
     wgpu::RenderPipeline CreateRenderPipelineWithTwoOutputsForTest(
         uint32_t sampleMask = 0xFFFFFFFF,
-        bool alphaToCoverageEnabled = false) {
+        bool alphaToCoverageEnabled = false,
+        bool enableMSAARenderToSingleSampled = false) {
         const char* kFsTwoOutputs = R"(
             struct U {
                 color0 : vec4f,
@@ -107,14 +109,16 @@
             })";
 
         return CreateRenderPipelineForTest(kFsTwoOutputs, 2, false, sampleMask,
-                                           alphaToCoverageEnabled);
+                                           alphaToCoverageEnabled, /*flipTriangle=*/false,
+                                           enableMSAARenderToSingleSampled);
     }
 
     wgpu::Texture CreateTextureForRenderAttachment(wgpu::TextureFormat format,
                                                    uint32_t sampleCount,
                                                    uint32_t mipLevelCount = 1,
                                                    uint32_t arrayLayerCount = 1,
-                                                   bool transientAttachment = false) {
+                                                   bool transientAttachment = false,
+                                                   bool supportMSAARenderToSingleSampled = false) {
         wgpu::TextureDescriptor descriptor;
         descriptor.dimension = wgpu::TextureDimension::e2D;
         descriptor.size.width = kWidth << (mipLevelCount - 1);
@@ -129,6 +133,11 @@
         } else {
             descriptor.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
         }
+
+        if (supportMSAARenderToSingleSampled) {
+            descriptor.usage |= wgpu::TextureUsage::TextureBinding;
+        }
+
         return device.CreateTexture(&descriptor);
     }
 
@@ -229,7 +238,8 @@
                                                      bool hasDepthStencilAttachment,
                                                      uint32_t sampleMask = 0xFFFFFFFF,
                                                      bool alphaToCoverageEnabled = false,
-                                                     bool flipTriangle = false) {
+                                                     bool flipTriangle = false,
+                                                     bool enableMSAARenderToSingleSampled = false) {
         utils::ComboRenderPipelineDescriptor pipelineDescriptor;
 
         // Draw a bottom-right triangle. In standard 4xMSAA pattern, for the pixels on diagonal,
@@ -276,6 +286,12 @@
         pipelineDescriptor.multisample.mask = sampleMask;
         pipelineDescriptor.multisample.alphaToCoverageEnabled = alphaToCoverageEnabled;
 
+        wgpu::DawnMultisampleStateRenderToSingleSampled mssaRenderToSingleSampledDesc;
+        if (enableMSAARenderToSingleSampled) {
+            mssaRenderToSingleSampledDesc.enabled = true;
+            pipelineDescriptor.multisample.nextInChain = &mssaRenderToSingleSampledDesc;
+        }
+
         pipelineDescriptor.cFragment.targetCount = numColorAttachments;
         for (uint32_t i = 0; i < numColorAttachments; ++i) {
             pipelineDescriptor.cTargets[i].format = kColorFormat;
@@ -1197,6 +1213,179 @@
     VerifyResolveTarget(kGreen, mResolveTexture);
 }
 
+class MultisampledRenderToSingleSampledTest : public MultisampledRenderingTest {
+    void SetUp() override {
+        MultisampledRenderingTest::SetUp();
+
+        // Skip all tests if the MSAARenderToSingleSampled feature is not supported.
+        DAWN_TEST_UNSUPPORTED_IF(!SupportsFeatures({wgpu::FeatureName::MSAARenderToSingleSampled}));
+    }
+
+    std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
+        std::vector<wgpu::FeatureName> requiredFeatures = {};
+        if (SupportsFeatures({wgpu::FeatureName::MSAARenderToSingleSampled})) {
+            requiredFeatures.push_back(wgpu::FeatureName::MSAARenderToSingleSampled);
+        }
+        return requiredFeatures;
+    }
+};
+
+// Test rendering into a color attachment and start another render pass with LoadOp::Load
+// will have the content preserved.
+TEST_P(MultisampledRenderToSingleSampledTest, DrawThenLoad) {
+    auto singleSampledTexture =
+        CreateTextureForRenderAttachment(kColorFormat, 1, 1, 1, /*transientAttachment=*/false,
+                                         /*supportMSAARenderToSingleSampled=*/true);
+
+    auto singleSampledTextureView = singleSampledTexture.CreateView();
+
+    wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+    wgpu::RenderPipeline pipeline = CreateRenderPipelineWithOneOutputForTest(
+        /*testDepth=*/false, /*sampleMask=*/0xFFFFFFFF, /*alphaToCoverageEnabled=*/false,
+        /*flipTriangle=*/false, /*enableMSAARenderToSingleSampled=*/true);
+
+    constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
+
+    wgpu::DawnRenderPassColorAttachmentRenderToSingleSampled mssaRenderToSingleSampledDesc;
+    mssaRenderToSingleSampledDesc.implicitSampleCount = kSampleCount;
+
+    // In first render pass we draw a green triangle.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {singleSampledTextureView}, {nullptr}, wgpu::LoadOp::Clear, wgpu::LoadOp::Clear,
+            /*testDepth=*/false);
+        renderPass.cColorAttachments[0].nextInChain = &mssaRenderToSingleSampledDesc;
+
+        EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, kGreen);
+    }
+
+    // In second render pass we only use LoadOp::Load with no draw call.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {singleSampledTextureView}, {nullptr}, wgpu::LoadOp::Load, wgpu::LoadOp::Load,
+            /*testDepth=*/false);
+        renderPass.cColorAttachments[0].nextInChain = &mssaRenderToSingleSampledDesc;
+
+        wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
+        renderPassEncoder.End();
+    }
+
+    wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
+    queue.Submit(1, &commandBuffer);
+
+    VerifyResolveTarget(kGreen, singleSampledTexture);
+}
+
+// Test clear a color attachment (without implicit sample count) and start another render pass (with
+// implicit sample count) with LoadOp::Load plus additional drawing works correctly. The final
+// result should contain the combination of the loaded content from 1st pass and the 2nd pass.
+TEST_P(MultisampledRenderToSingleSampledTest, ClearThenLoadThenDraw) {
+    auto singleSampledTexture =
+        CreateTextureForRenderAttachment(kColorFormat, 1, 1, 1, /*transientAttachment=*/false,
+                                         /*supportMSAARenderToSingleSampled=*/true);
+
+    auto singleSampledTextureView = singleSampledTexture.CreateView();
+
+    wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+    wgpu::RenderPipeline pipeline = CreateRenderPipelineWithOneOutputForTest(
+        /*testDepth=*/false, /*sampleMask=*/0xFFFFFFFF, /*alphaToCoverageEnabled=*/false,
+        /*flipTriangle=*/false, /*enableMSAARenderToSingleSampled=*/true);
+
+    constexpr wgpu::Color kRed = {1.0f, 0.0f, 0.0f, 1.0f};
+    constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
+
+    wgpu::DawnRenderPassColorAttachmentRenderToSingleSampled mssaRenderToSingleSampledDesc;
+    mssaRenderToSingleSampledDesc.implicitSampleCount = kSampleCount;
+
+    // In first render pass we clear to red without using implicit sample count.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {singleSampledTextureView}, {nullptr}, wgpu::LoadOp::Clear, wgpu::LoadOp::Clear,
+            /*testDepth=*/false);
+        renderPass.cColorAttachments[0].clearValue = kRed;
+
+        wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
+        renderPassEncoder.End();
+    }
+
+    // In second render pass (with implicit sample count) we use LoadOp::Load then draw a green
+    // triangle.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {singleSampledTextureView}, {nullptr}, wgpu::LoadOp::Load, wgpu::LoadOp::Load,
+            /*testDepth=*/false);
+        renderPass.cColorAttachments[0].nextInChain = &mssaRenderToSingleSampledDesc;
+
+        EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, kGreen);
+    }
+
+    auto commandBuffer = commandEncoder.Finish();
+    queue.Submit(1, &commandBuffer);
+
+    constexpr wgpu::Color kHalfGreenHalfRed = {(kGreen.r + kRed.r) / 2.0, (kGreen.g + kRed.g) / 2.0,
+                                               (kGreen.b + kRed.b) / 2.0,
+                                               (kGreen.a + kRed.a) / 2.0};
+    utils::RGBA8 expectedColor = ExpectedMSAAColor(kHalfGreenHalfRed, 1.0f);
+
+    EXPECT_TEXTURE_EQ(&expectedColor, singleSampledTexture, {1, 1}, {1, 1},
+                      /* level */ 0, wgpu::TextureAspect::All, /* bytesPerRow */ 0,
+                      /* tolerance */ utils::RGBA8(1, 1, 1, 1));
+}
+
+// Test multisampled rendering with depth test works correctly.
+TEST_P(MultisampledRenderToSingleSampledTest, DrawWithDepthTest) {
+    auto singleSampledTexture =
+        CreateTextureForRenderAttachment(kColorFormat, 1, 1, 1, /*transientAttachment=*/false,
+                                         /*supportMSAARenderToSingleSampled=*/true);
+
+    auto singleSampledTextureView = singleSampledTexture.CreateView();
+
+    wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+    wgpu::RenderPipeline pipeline = CreateRenderPipelineWithOneOutputForTest(
+        /*testDepth=*/true, /*sampleMask=*/0xFFFFFFFF, /*alphaToCoverageEnabled=*/false,
+        /*flipTriangle=*/false, /*enableMSAARenderToSingleSampled=*/true);
+
+    constexpr wgpu::Color kGreen = {0.0f, 0.8f, 0.0f, 0.8f};
+    constexpr wgpu::Color kRed = {0.8f, 0.0f, 0.0f, 0.8f};
+    wgpu::DawnRenderPassColorAttachmentRenderToSingleSampled mssaRenderToSingleSampledDesc;
+    mssaRenderToSingleSampledDesc.implicitSampleCount = kSampleCount;
+
+    // In first render pass we draw a green triangle with depth value == 0.2f.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {singleSampledTextureView}, {nullptr}, wgpu::LoadOp::Clear, wgpu::LoadOp::Clear,
+            /*testDepth=*/true);
+        renderPass.cColorAttachments[0].nextInChain = &mssaRenderToSingleSampledDesc;
+
+        std::array<float, 8> kUniformData = {kGreen.r, kGreen.g, kGreen.b, kGreen.a,  // Color
+                                             0.2f};                                   // depth
+        constexpr uint32_t kSize = sizeof(kUniformData);
+        EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, kUniformData.data(), kSize);
+    }
+
+    // In second render pass we draw a red triangle with depth value == 0.5f.
+    // This red triangle should not be displayed because it is behind the green one that is drawn in
+    // the last render pass.
+    {
+        utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+            {singleSampledTextureView}, {nullptr}, wgpu::LoadOp::Load, wgpu::LoadOp::Load,
+            /*testDepth=*/true);
+        renderPass.cColorAttachments[0].nextInChain = &mssaRenderToSingleSampledDesc;
+
+        std::array<float, 8> kUniformData = {kRed.r, kRed.g, kRed.b, kRed.a,  // color
+                                             0.5f};                           // depth
+        constexpr uint32_t kSize = sizeof(kUniformData);
+        EncodeRenderPassForTest(commandEncoder, renderPass, pipeline, kUniformData.data(), kSize);
+    }
+
+    wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
+    queue.Submit(1, &commandBuffer);
+
+    // The color of the pixel in the middle of mResolveTexture should be green if MSAA resolve runs
+    // correctly with depth test.
+    VerifyResolveTarget(kGreen, singleSampledTexture);
+}
+
 DAWN_INSTANTIATE_TEST(MultisampledRenderingTest,
                       D3D11Backend(),
                       D3D12Backend(),
@@ -1227,5 +1416,19 @@
                       MetalBackend({"always_resolve_into_zero_level_and_layer",
                                     "emulate_store_and_msaa_resolve"}));
 
+DAWN_INSTANTIATE_TEST(MultisampledRenderToSingleSampledTest,
+                      D3D12Backend(),
+                      D3D12Backend({}, {"use_d3d12_resource_heap_tier2"}),
+                      D3D12Backend({}, {"use_d3d12_render_pass"}),
+                      MetalBackend(),
+                      OpenGLBackend(),
+                      OpenGLESBackend(),
+                      VulkanBackend(),
+                      VulkanBackend({"always_resolve_into_zero_level_and_layer"}),
+                      MetalBackend({"emulate_store_and_msaa_resolve"}),
+                      MetalBackend({"always_resolve_into_zero_level_and_layer"}),
+                      MetalBackend({"always_resolve_into_zero_level_and_layer",
+                                    "emulate_store_and_msaa_resolve"}));
+
 }  // anonymous namespace
 }  // namespace dawn
diff --git a/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp b/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp
index 3e368d9..14cc121 100644
--- a/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/RenderPassDescriptorValidationTests.cpp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include <cmath>
+#include <string>
 #include <vector>
 
 #include "dawn/common/Constants.h"
@@ -35,6 +36,12 @@
         ASSERT_DEVICE_ERROR(commandEncoder.Finish());
     }
 
+    void AssertBeginRenderPassError(const wgpu::RenderPassDescriptor* descriptor,
+                                    testing::Matcher<std::string> errorMatcher) {
+        wgpu::CommandEncoder commandEncoder = TestBeginRenderPass(descriptor);
+        ASSERT_DEVICE_ERROR(commandEncoder.Finish(), errorMatcher);
+    }
+
   private:
     wgpu::CommandEncoder TestBeginRenderPass(const wgpu::RenderPassDescriptor* descriptor) {
         wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
@@ -990,6 +997,20 @@
     }
 }
 
+// Creating a render pass with DawnRenderPassColorAttachmentRenderToSingleSampled chained struct
+// without MSAARenderToSingleSampled feature enabled should result in error.
+TEST_F(MultisampledRenderPassDescriptorValidationTest,
+       CreateMSAARenderToSingleSampledRenderPassWithoutFeatureEnabled) {
+    wgpu::TextureView colorTextureView = CreateNonMultisampledColorTextureView();
+
+    wgpu::DawnRenderPassColorAttachmentRenderToSingleSampled renderToSingleSampledDesc;
+    renderToSingleSampledDesc.implicitSampleCount = 4;
+    utils::ComboRenderPassDescriptor renderPass({colorTextureView});
+    renderPass.cColorAttachments[0].nextInChain = &renderToSingleSampledDesc;
+
+    AssertBeginRenderPassError(&renderPass, testing::HasSubstr("feature is not enabled"));
+}
+
 // Tests that NaN cannot be accepted as a valid color or depth clear value and INFINITY is valid
 // in both color and depth clear values.
 TEST_F(RenderPassDescriptorValidationTest, UseNaNOrINFINITYAsColorOrDepthClearValue) {
@@ -1529,5 +1550,152 @@
 
 // TODO(cwallez@chromium.org): Constraints on attachment aliasing?
 
+class MSAARenderToSingleSampledRenderPassDescriptorValidationTest
+    : public MultisampledRenderPassDescriptorValidationTest {
+  protected:
+    void SetUp() override {
+        MultisampledRenderPassDescriptorValidationTest::SetUp();
+
+        mRenderToSingleSampledDesc.implicitSampleCount = kSampleCount;
+    }
+
+    WGPUDevice CreateTestDevice(dawn::native::Adapter dawnAdapter,
+                                wgpu::DeviceDescriptor descriptor) override {
+        wgpu::FeatureName requiredFeatures[1] = {wgpu::FeatureName::MSAARenderToSingleSampled};
+        descriptor.requiredFeatures = requiredFeatures;
+        descriptor.requiredFeaturesCount = 1;
+        return dawnAdapter.CreateDevice(&descriptor);
+    }
+
+    utils::ComboRenderPassDescriptor CreateMultisampledRenderToSingleSampledRenderPass(
+        wgpu::TextureView colorAttachment,
+        wgpu::TextureView depthStencilAttachment = nullptr) {
+        utils::ComboRenderPassDescriptor renderPass({colorAttachment}, depthStencilAttachment);
+        renderPass.cColorAttachments[0].nextInChain = &mRenderToSingleSampledDesc;
+
+        return renderPass;
+    }
+
+    // Create a view for a texture that can be used with a MSAA render to single sampled render
+    // pass.
+    wgpu::TextureView CreateCompatibleColorTextureView() {
+        wgpu::Texture colorTexture = CreateTexture(
+            device, wgpu::TextureDimension::e2D, kColorFormat, kSize, kSize, kArrayLayers,
+            kLevelCount, /*sampleCount=*/1,
+            wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding);
+
+        return colorTexture.CreateView();
+    }
+
+  private:
+    wgpu::DawnRenderPassColorAttachmentRenderToSingleSampled mRenderToSingleSampledDesc;
+};
+
+// Test that using a valid color attachment with enabled MSAARenderToSingleSampled doesn't raise any
+// error.
+TEST_F(MSAARenderToSingleSampledRenderPassDescriptorValidationTest, ColorAttachmentValid) {
+    // Create a texture with sample count = 1.
+    auto textureView = CreateCompatibleColorTextureView();
+
+    auto renderPass = CreateMultisampledRenderToSingleSampledRenderPass(textureView);
+    AssertBeginRenderPassSuccess(&renderPass);
+}
+
+// When MSAARenderToSingleSampled is enabled for a color attachment, it must be created with
+// TextureBinding usage.
+TEST_F(MSAARenderToSingleSampledRenderPassDescriptorValidationTest, ColorAttachmentInvalidUsage) {
+    // Create a texture with sample count = 1.
+    auto texture =
+        CreateTexture(device, wgpu::TextureDimension::e2D, kColorFormat, kSize, kSize, kArrayLayers,
+                      kLevelCount, /*sampleCount=*/1, wgpu::TextureUsage::RenderAttachment);
+
+    auto renderPass = CreateMultisampledRenderToSingleSampledRenderPass(texture.CreateView());
+    AssertBeginRenderPassError(&renderPass, testing::HasSubstr("usage"));
+}
+
+// When MSAARenderToSingleSampled is enabled for a color attachment, there must be no explicit
+// resolve target specified for it.
+TEST_F(MSAARenderToSingleSampledRenderPassDescriptorValidationTest, ErrorSettingResolveTarget) {
+    // Create a texture with sample count = 1.
+    auto textureView1 = CreateCompatibleColorTextureView();
+    auto textureView2 = CreateCompatibleColorTextureView();
+
+    auto renderPass = CreateMultisampledRenderToSingleSampledRenderPass(textureView1);
+    renderPass.cColorAttachments[0].resolveTarget = textureView2;
+    AssertBeginRenderPassError(&renderPass, testing::HasSubstr("as a resolve target"));
+}
+
+// Using unsupported implicit sample count in DawnRenderPassColorAttachmentRenderToSingleSampled
+// chained struct should result in error.
+TEST_F(MSAARenderToSingleSampledRenderPassDescriptorValidationTest, UnsupportedSampleCountError) {
+    // Create a texture with sample count = 1.
+    auto textureView = CreateCompatibleColorTextureView();
+
+    // Create a render pass with implicit sample count = 3. Which is not supported.
+    wgpu::DawnRenderPassColorAttachmentRenderToSingleSampled renderToSingleSampledDesc;
+    renderToSingleSampledDesc.implicitSampleCount = 3;
+    utils::ComboRenderPassDescriptor renderPass({textureView});
+    renderPass.cColorAttachments[0].nextInChain = &renderToSingleSampledDesc;
+
+    AssertBeginRenderPassError(&renderPass,
+                               testing::HasSubstr("implicit sample count (3) is not supported"));
+
+    // Create a render pass with implicit sample count = 1. Which is also not supported.
+    renderToSingleSampledDesc.implicitSampleCount = 1;
+    renderPass.cColorAttachments[0].nextInChain = &renderToSingleSampledDesc;
+
+    AssertBeginRenderPassError(&renderPass,
+                               testing::HasSubstr("implicit sample count (1) is not supported"));
+}
+
+// When MSAARenderToSingleSampled is enabled in a color attachment, there should be an error if a
+// color attachment's format doesn't support resolve. Example, RGBA8Sint format.
+TEST_F(MSAARenderToSingleSampledRenderPassDescriptorValidationTest, UnresolvableColorFormatError) {
+    // Create a texture with sample count = 1.
+    auto texture =
+        CreateTexture(device, wgpu::TextureDimension::e2D, wgpu::TextureFormat::RGBA8Sint, kSize,
+                      kSize, kArrayLayers, kLevelCount, /*sampleCount=*/1,
+                      wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding);
+
+    auto renderPass = CreateMultisampledRenderToSingleSampledRenderPass(texture.CreateView());
+    AssertBeginRenderPassError(&renderPass, testing::HasSubstr("does not support resolve"));
+}
+
+// Depth stencil attachment's sample count must match the one specified in color attachment's
+// implicitSampleCount.
+TEST_F(MSAARenderToSingleSampledRenderPassDescriptorValidationTest, DepthStencilSampleCountValid) {
+    // Create a color texture with sample count = 1.
+    auto colorTextureView = CreateCompatibleColorTextureView();
+
+    // Create depth stencil texture with sample count = 4.
+    auto depthStencilTexture = CreateTexture(
+        device, wgpu::TextureDimension::e2D, wgpu::TextureFormat::Depth24PlusStencil8, kSize, kSize,
+        1, 1, /*sampleCount=*/kSampleCount, wgpu::TextureUsage::RenderAttachment);
+
+    auto renderPass = CreateMultisampledRenderToSingleSampledRenderPass(
+        colorTextureView, depthStencilTexture.CreateView());
+
+    AssertBeginRenderPassSuccess(&renderPass);
+}
+
+// Using depth stencil attachment with sample count not matching the implicit sample count will
+// result in error.
+TEST_F(MSAARenderToSingleSampledRenderPassDescriptorValidationTest,
+       DepthStencilSampleCountNotMatchImplicitSampleCount) {
+    // Create a color texture with sample count = 1.
+    auto colorTextureView = CreateCompatibleColorTextureView();
+
+    // Create depth stencil texture with sample count = 1. Which doesn't match implicitSampleCount=4
+    // specified in mRenderToSingleSampledDesc.
+    auto depthStencilTexture =
+        CreateTexture(device, wgpu::TextureDimension::e2D, wgpu::TextureFormat::Depth24PlusStencil8,
+                      kSize, kSize, 1, 1, /*sampleCount=*/1, wgpu::TextureUsage::RenderAttachment);
+
+    auto renderPass = CreateMultisampledRenderToSingleSampledRenderPass(
+        colorTextureView, depthStencilTexture.CreateView());
+
+    AssertBeginRenderPassError(&renderPass, testing::HasSubstr("does not match the sample count"));
+}
+
 }  // anonymous namespace
 }  // namespace dawn
diff --git a/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp b/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
index 6fd8002..1055f54 100644
--- a/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/RenderPipelineValidationTests.cpp
@@ -1593,6 +1593,22 @@
     }
 }
 
+// Creating render pipeline with DawnMultisampleStateRenderToSingleSampled without enabling
+// MSAARenderToSingleSampled feature should result in error.
+TEST_F(RenderPipelineValidationTest, MSAARenderToSingleSampledOnUnsupportedDevice) {
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.vertex.module = vsModule;
+    pipelineDescriptor.cFragment.module = fsModule;
+
+    wgpu::DawnMultisampleStateRenderToSingleSampled pipelineMSAARenderToSingleSampledDesc;
+    pipelineMSAARenderToSingleSampledDesc.enabled = true;
+    pipelineDescriptor.multisample.nextInChain = &pipelineMSAARenderToSingleSampledDesc;
+    pipelineDescriptor.multisample.count = 4;
+
+    ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&pipelineDescriptor),
+                        testing::HasSubstr("feature is not enabled"));
+}
+
 class DepthClipControlValidationTest : public RenderPipelineValidationTest {
   protected:
     WGPUDevice CreateTestDevice(native::Adapter dawnAdapter,
@@ -2000,5 +2016,219 @@
     ASSERT_DEVICE_ERROR(encoder.Finish());
 }
 
+class MSAARenderToSingleSampledPipelineDescriptorValidationTest
+    : public RenderPipelineValidationTest {
+  protected:
+    void SetUp() override {
+        RenderPipelineValidationTest::SetUp();
+
+        fsWithTextureModule = utils::CreateShaderModule(device, R"(
+            @group(0) @binding(0) var src_tex : texture_2d<f32>;
+
+            @fragment fn main() -> @location(0) vec4f {
+                return textureLoad(src_tex, vec2u(0, 0), 0);
+            })");
+    }
+
+    WGPUDevice CreateTestDevice(dawn::native::Adapter dawnAdapter,
+                                wgpu::DeviceDescriptor descriptor) override {
+        wgpu::FeatureName requiredFeatures[1] = {wgpu::FeatureName::MSAARenderToSingleSampled};
+        descriptor.requiredFeatures = requiredFeatures;
+        descriptor.requiredFeaturesCount = 1;
+        return dawnAdapter.CreateDevice(&descriptor);
+    }
+
+    wgpu::Texture CreateTexture(wgpu::TextureUsage textureUsage, uint32_t sampleCount) {
+        wgpu::TextureDescriptor textureDescriptor;
+        textureDescriptor.usage = textureUsage;
+        textureDescriptor.format = kColorFormat;
+        textureDescriptor.sampleCount = sampleCount;
+        textureDescriptor.size.width = 4;
+        textureDescriptor.size.height = 4;
+        return device.CreateTexture(&textureDescriptor);
+    }
+
+    static constexpr wgpu::TextureFormat kColorFormat = wgpu::TextureFormat::RGBA8Unorm;
+
+    wgpu::ShaderModule fsWithTextureModule;
+};
+
+// Test that creating and using a render pipeline with DawnMultisampleStateRenderToSingleSampled
+// chained struct should success.
+TEST_F(MSAARenderToSingleSampledPipelineDescriptorValidationTest, ValidUse) {
+    constexpr uint32_t kSampleCount = 4;
+
+    // Create single sampled texture.
+    auto texture =
+        CreateTexture(wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding, 1);
+
+    // Create render pass (with DawnRenderPassColorAttachmentRenderToSingleSampled).
+    utils::ComboRenderPassDescriptor renderPassDescriptor({texture.CreateView()});
+    renderPassDescriptor.cColorAttachments[0].loadOp = wgpu::LoadOp::Load;
+
+    wgpu::DawnRenderPassColorAttachmentRenderToSingleSampled renderPassRenderToSingleSampledDesc;
+    renderPassRenderToSingleSampledDesc.implicitSampleCount = kSampleCount;
+    renderPassDescriptor.cColorAttachments[0].nextInChain = &renderPassRenderToSingleSampledDesc;
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    wgpu::RenderPassEncoder renderPass = encoder.BeginRenderPass(&renderPassDescriptor);
+
+    // Create render pipeline
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.vertex.module = vsModule;
+    pipelineDescriptor.cFragment.module = fsWithTextureModule;
+
+    wgpu::DawnMultisampleStateRenderToSingleSampled pipelineMSAARenderToSingleSampledDesc;
+    pipelineMSAARenderToSingleSampledDesc.enabled = true;
+    pipelineDescriptor.multisample.nextInChain = &pipelineMSAARenderToSingleSampledDesc;
+    pipelineDescriptor.multisample.count = kSampleCount;
+
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
+
+    // Input texture.
+    auto sampledTexture = CreateTexture(wgpu::TextureUsage::TextureBinding, 1);
+    wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+                                                     {{0, sampledTexture.CreateView()}});
+
+    renderPass.SetPipeline(pipeline);
+    renderPass.SetBindGroup(0, bindGroup);
+    renderPass.Draw(3);
+    renderPass.End();
+
+    encoder.Finish();
+}
+
+// If a render pipeline's MultisampleState contains DawnMultisampleStateRenderToSingleSampled
+// chained struct. Then its sampleCount must be > 1.
+TEST_F(MSAARenderToSingleSampledPipelineDescriptorValidationTest,
+       PipelineSampleCountMustBeGreaterThanOne) {
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.vertex.module = vsModule;
+    pipelineDescriptor.cFragment.module = fsModule;
+
+    wgpu::DawnMultisampleStateRenderToSingleSampled pipelineMSAARenderToSingleSampledDesc;
+    pipelineMSAARenderToSingleSampledDesc.enabled = true;
+    pipelineDescriptor.multisample.nextInChain = &pipelineMSAARenderToSingleSampledDesc;
+    pipelineDescriptor.multisample.count = 1;
+
+    ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&pipelineDescriptor),
+                        testing::HasSubstr("multisample count (1) is not > 1"));
+}
+
+// If a render pipeline is created with MSAA render to single sampled enabled , then it cannot be
+// used in a render pass that wasn't created with that feature enabled.
+TEST_F(MSAARenderToSingleSampledPipelineDescriptorValidationTest,
+       MSAARenderToSingleSampledPipeline_UseIn_NormalRenderPass_Error) {
+    constexpr uint32_t kSampleCount = 4;
+
+    // Create MSAA texture.
+    auto texture = CreateTexture(wgpu::TextureUsage::RenderAttachment, 4);
+
+    // Create render pass (without DawnRenderPassColorAttachmentRenderToSingleSampled).
+    utils::ComboRenderPassDescriptor renderPassDescriptor({texture.CreateView()});
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    wgpu::RenderPassEncoder renderPass = encoder.BeginRenderPass(&renderPassDescriptor);
+
+    // Create render pipeline
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.vertex.module = vsModule;
+    pipelineDescriptor.cFragment.module = fsModule;
+
+    wgpu::DawnMultisampleStateRenderToSingleSampled pipelineMSAARenderToSingleSampledDesc;
+    pipelineMSAARenderToSingleSampledDesc.enabled = true;
+    pipelineDescriptor.multisample.nextInChain = &pipelineMSAARenderToSingleSampledDesc;
+    pipelineDescriptor.multisample.count = kSampleCount;
+
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
+    renderPass.SetPipeline(pipeline);
+    renderPass.End();
+
+    ASSERT_DEVICE_ERROR(encoder.Finish());
+}
+
+// Using a normal render pipeline in a MSAA render to single sampled render pass should result in
+// incompatible error.
+TEST_F(MSAARenderToSingleSampledPipelineDescriptorValidationTest,
+       NormalPipeline_Use_In_MSAARenderToSingleSampledRenderPass_Error) {
+    constexpr uint32_t kSampleCount = 4;
+
+    // Create single sampled texture.
+    auto texture =
+        CreateTexture(wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding, 1);
+
+    // Create render pass (with DawnRenderPassColorAttachmentRenderToSingleSampled).
+    utils::ComboRenderPassDescriptor renderPassDescriptor({texture.CreateView()});
+    wgpu::DawnRenderPassColorAttachmentRenderToSingleSampled renderPassRenderToSingleSampledDesc;
+    renderPassRenderToSingleSampledDesc.implicitSampleCount = kSampleCount;
+    renderPassDescriptor.cColorAttachments[0].nextInChain = &renderPassRenderToSingleSampledDesc;
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    wgpu::RenderPassEncoder renderPass = encoder.BeginRenderPass(&renderPassDescriptor);
+
+    // Create render pipeline (without DawnMultisampleStateRenderToSingleSampled)
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.vertex.module = vsModule;
+    pipelineDescriptor.cFragment.module = fsModule;
+
+    pipelineDescriptor.multisample.count = kSampleCount;
+
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
+    renderPass.SetPipeline(pipeline);
+    renderPass.End();
+
+    ASSERT_DEVICE_ERROR(encoder.Finish());
+}
+
+// Bind color attachment in the MSAA render to single sampled render pass as texture should result
+// in error.
+TEST_F(MSAARenderToSingleSampledPipelineDescriptorValidationTest,
+       BindColorAttachmentAsTextureError) {
+    constexpr uint32_t kSampleCount = 4;
+
+    // Create single sampled texture.
+    auto renderTexture =
+        CreateTexture(wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding |
+                          wgpu::TextureUsage::TextureBinding,
+                      1);
+
+    // Create render pass (with DawnRenderPassColorAttachmentRenderToSingleSampled).
+    utils::ComboRenderPassDescriptor renderPassDescriptor({renderTexture.CreateView()});
+    renderPassDescriptor.cColorAttachments[0].loadOp = wgpu::LoadOp::Load;
+
+    wgpu::DawnRenderPassColorAttachmentRenderToSingleSampled renderPassRenderToSingleSampledDesc;
+    renderPassRenderToSingleSampledDesc.implicitSampleCount = kSampleCount;
+    renderPassDescriptor.cColorAttachments[0].nextInChain = &renderPassRenderToSingleSampledDesc;
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    wgpu::RenderPassEncoder renderPass = encoder.BeginRenderPass(&renderPassDescriptor);
+
+    // Create render pipeline
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.vertex.module = vsModule;
+    pipelineDescriptor.cFragment.module = fsWithTextureModule;
+
+    wgpu::DawnMultisampleStateRenderToSingleSampled pipelineMSAARenderToSingleSampledDesc;
+    pipelineMSAARenderToSingleSampledDesc.enabled = true;
+    pipelineDescriptor.multisample.nextInChain = &pipelineMSAARenderToSingleSampledDesc;
+    pipelineDescriptor.multisample.count = kSampleCount;
+
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
+
+    // Use color attachment's texture as input texture.
+    wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+                                                     {{0, renderTexture.CreateView()}});
+
+    renderPass.SetPipeline(pipeline);
+    renderPass.SetBindGroup(0, bindGroup);
+    renderPass.Draw(3);
+    renderPass.End();
+
+    ASSERT_DEVICE_ERROR(
+        encoder.Finish(),
+        testing::HasSubstr(
+            "includes writable usage and another usage in the same synchronization scope"));
+}
+
 }  // anonymous namespace
 }  // namespace dawn
diff --git a/src/dawn/tests/white_box/InternalResolveAttachmentSampleTypeTests.cpp b/src/dawn/tests/white_box/InternalResolveAttachmentSampleTypeTests.cpp
new file mode 100644
index 0000000..c7e9bfa
--- /dev/null
+++ b/src/dawn/tests/white_box/InternalResolveAttachmentSampleTypeTests.cpp
@@ -0,0 +1,157 @@
+// Copyright 2023 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <vector>
+
+#include "dawn/native/BindGroupLayout.h"
+#include "dawn/native/Device.h"
+#include "dawn/native/dawn_platform.h"
+#include "dawn/tests/DawnTest.h"
+#include "dawn/utils/ComboRenderPipelineDescriptor.h"
+#include "dawn/utils/WGPUHelpers.h"
+
+namespace dawn {
+namespace {
+
+class InternalResolveAttachmentSampleTypeTests : public DawnTest {
+  protected:
+    void SetUp() override {
+        DawnTest::SetUp();
+        DAWN_TEST_UNSUPPORTED_IF(UsesWire());
+
+        // vertex shader module.
+        vsModule = utils::CreateShaderModule(device, R"(
+            @vertex fn main() -> @builtin(position) vec4f {
+                return vec4f(0.0, 0.0, 0.0, 1.0);
+            })");
+    }
+
+    wgpu::PipelineLayout CreatePipelineLayout(bool withSampler) {
+        // Create binding group layout with internal resolve attachment sample type.
+        std::vector<native::BindGroupLayoutEntry> bglEntries(2);
+        bglEntries[0].binding = 0;
+        bglEntries[0].texture.sampleType = native::kInternalResolveAttachmentSampleType;
+        bglEntries[0].texture.viewDimension = wgpu::TextureViewDimension::e2D;
+        bglEntries[0].visibility = wgpu::ShaderStage::Fragment;
+
+        native::BindGroupLayoutDescriptor bglDesc;
+
+        if (withSampler) {
+            bglEntries[1].binding = 1;
+            bglEntries[1].sampler.type = wgpu::SamplerBindingType::Filtering;
+            bglEntries[1].visibility = wgpu::ShaderStage::Fragment;
+
+            bglDesc.entryCount = bglEntries.size();
+        } else {
+            bglDesc.entryCount = 1;
+        }
+        bglDesc.entries = bglEntries.data();
+
+        native::DeviceBase* nativeDevice = native::FromAPI(device.Get());
+
+        Ref<native::BindGroupLayoutBase> bglRef =
+            nativeDevice->CreateBindGroupLayout(&bglDesc, true).AcquireSuccess();
+
+        auto bindGroupLayout = wgpu::BindGroupLayout::Acquire(native::ToAPI(bglRef.Detach()));
+
+        // Create pipeline layout from the bind group layout.
+        wgpu::PipelineLayoutDescriptor descriptor;
+        std::vector<wgpu::BindGroupLayout> bindgroupLayouts = {bindGroupLayout};
+        descriptor.bindGroupLayoutCount = bindgroupLayouts.size();
+        descriptor.bindGroupLayouts = bindgroupLayouts.data();
+        return device.CreatePipelineLayout(&descriptor);
+    }
+
+    wgpu::ShaderModule vsModule;
+};
+
+// Test that using a bind group layout with kInternalResolveAttachmentSampleType is compatible with
+// a shader using textureLoad(texture_2d<f32>) function.
+TEST_P(InternalResolveAttachmentSampleTypeTests, TextureLoadF32Compatible) {
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.vertex.module = vsModule;
+    pipelineDescriptor.cFragment.module = utils::CreateShaderModule(device, R"(
+        @group(0) @binding(0) var src_tex : texture_2d<f32>;
+
+        @fragment fn main() -> @location(0) vec4f {
+            return textureLoad(src_tex, vec2u(0, 0), 0);
+        })");
+
+    pipelineDescriptor.layout = CreatePipelineLayout(/*withSampler=*/false);
+
+    device.CreateRenderPipeline(&pipelineDescriptor);
+}
+
+// Test that using a bind group layout with kInternalResolveAttachmentSampleType is compatible
+// with a shader using textureSample(texture_2d<f32>) function.
+TEST_P(InternalResolveAttachmentSampleTypeTests, TextureSampleF32Compatible) {
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.vertex.module = vsModule;
+    pipelineDescriptor.cFragment.module = utils::CreateShaderModule(device, R"(
+        @group(0) @binding(0) var src_tex : texture_2d<f32>;
+        @group(0) @binding(1) var src_sampler : sampler;
+
+        @fragment fn main() -> @location(0) vec4f {
+            return textureSample(src_tex, src_sampler, vec2f(0.0, 0.0));
+        })");
+
+    pipelineDescriptor.layout = CreatePipelineLayout(/*withSampler=*/true);
+
+    device.CreateRenderPipeline(&pipelineDescriptor);
+}
+
+// Test that using a bind group layout with kInternalResolveAttachmentSampleType is incompatible
+// with a shader using textureLoad(texture_2d<i32>) function.
+TEST_P(InternalResolveAttachmentSampleTypeTests, TextureLoadI32Incompatible) {
+    DAWN_TEST_UNSUPPORTED_IF(HasToggleEnabled("skip_validation"));
+
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.vertex.module = vsModule;
+    pipelineDescriptor.cFragment.module = utils::CreateShaderModule(device, R"(
+        @group(0) @binding(0) var src_tex : texture_2d<i32>;
+
+        @fragment fn main() -> @location(0) vec4i {
+            return textureLoad(src_tex, vec2u(0, 0), 0);
+        })");
+
+    pipelineDescriptor.layout = CreatePipelineLayout(/*withSampler=*/false);
+
+    ASSERT_DEVICE_ERROR_MSG(device.CreateRenderPipeline(&pipelineDescriptor),
+                            testing::HasSubstr("not compatible"));
+}
+
+// Test that using a bind group layout with kInternalResolveAttachmentSampleType is incompatible
+// with a shader using textureLoad(texture_2d<u32>) function.
+TEST_P(InternalResolveAttachmentSampleTypeTests, TextureLoadU32Incompatible) {
+    DAWN_TEST_UNSUPPORTED_IF(HasToggleEnabled("skip_validation"));
+
+    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.vertex.module = vsModule;
+    pipelineDescriptor.cFragment.module = utils::CreateShaderModule(device, R"(
+        @group(0) @binding(0) var src_tex : texture_2d<u32>;
+
+        @fragment fn main() -> @location(0) vec4u {
+            return textureLoad(src_tex, vec2u(0, 0), 0);
+        })");
+
+    pipelineDescriptor.layout = CreatePipelineLayout(/*withSampler=*/false);
+
+    ASSERT_DEVICE_ERROR_MSG(device.CreateRenderPipeline(&pipelineDescriptor),
+                            testing::HasSubstr("not compatible"));
+}
+
+DAWN_INSTANTIATE_TEST(InternalResolveAttachmentSampleTypeTests, NullBackend());
+
+}  // anonymous namespace
+}  // namespace dawn
diff --git a/src/dawn/tests/white_box/InternalResourceUsageTests.cpp b/src/dawn/tests/white_box/InternalResourceUsageTests.cpp
index a2c17ce..a4dd8d1 100644
--- a/src/dawn/tests/white_box/InternalResourceUsageTests.cpp
+++ b/src/dawn/tests/white_box/InternalResourceUsageTests.cpp
@@ -60,6 +60,23 @@
     ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&bglDesc));
 }
 
+// Verify it is an error to create a bind group layout with a texture sample type that should only
+// be used internally.
+TEST_P(InternalBindingTypeTests, ErrorUseInternalResolveAttachmentSampleTypeExternally) {
+    DAWN_TEST_UNSUPPORTED_IF(HasToggleEnabled("skip_validation"));
+
+    wgpu::BindGroupLayoutEntry bglEntry;
+    bglEntry.binding = 0;
+    bglEntry.texture.sampleType = native::kInternalResolveAttachmentSampleType;
+    bglEntry.visibility = wgpu::ShaderStage::Fragment;
+
+    wgpu::BindGroupLayoutDescriptor bglDesc;
+    bglDesc.entryCount = 1;
+    bglDesc.entries = &bglEntry;
+    ASSERT_DEVICE_ERROR_MSG(device.CreateBindGroupLayout(&bglDesc),
+                            testing::HasSubstr("invalid for WGPUTextureSampleType"));
+}
+
 DAWN_INSTANTIATE_TEST(InternalBindingTypeTests, NullBackend());
 
 }  // anonymous namespace
diff --git a/src/dawn/wire/SupportedFeatures.cpp b/src/dawn/wire/SupportedFeatures.cpp
index a8e7721..b85e39a 100644
--- a/src/dawn/wire/SupportedFeatures.cpp
+++ b/src/dawn/wire/SupportedFeatures.cpp
@@ -44,6 +44,7 @@
         case WGPUFeatureName_BGRA8UnormStorage:
         case WGPUFeatureName_TransientAttachments:
         case WGPUFeatureName_Float32Filterable:
+        case WGPUFeatureName_MSAARenderToSingleSampled:
             return true;
     }