GL_EXT_multisampled_render_to_texture extension. Part 2.

For textures that use this extension, a multisampled texture is
implicitly created for the texture.
Upon write or read, the multisampled texture is either return
to be drawn to or resolved and returned as a single sampled texture.

This is the functionality change with end2end tests.

Bug: angleproject:980428
Change-Id: I5776875a132fed7a3f4f00fb02f9e8e250684630
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/1773717
Commit-Queue: Rafael Cintron <rafael.cintron@microsoft.com>
Reviewed-by: Jamie Madill <jmadill@chromium.org>
diff --git a/src/libANGLE/Context.cpp b/src/libANGLE/Context.cpp
index 8bce1dd..dde2219 100644
--- a/src/libANGLE/Context.cpp
+++ b/src/libANGLE/Context.cpp
@@ -4144,9 +4144,10 @@
     if (renderbuffer.value != 0)
     {
         Renderbuffer *renderbufferObject = getRenderbuffer(renderbuffer);
+        GLsizei rbSamples                = renderbufferObject->getSamples();
 
-        framebuffer->setAttachment(this, GL_RENDERBUFFER, attachment, gl::ImageIndex(),
-                                   renderbufferObject);
+        framebuffer->setAttachmentMultisample(this, GL_RENDERBUFFER, attachment, gl::ImageIndex(),
+                                              renderbufferObject, rbSamples);
     }
     else
     {
@@ -5632,7 +5633,26 @@
                                               GLuint texture,
                                               GLint level,
                                               GLsizei samples)
-{}
+{
+    Framebuffer *framebuffer = mState.getTargetFramebuffer(target);
+    ASSERT(framebuffer);
+
+    if (texture != 0)
+    {
+        TextureTarget textargetPacked = FromGLenum<TextureTarget>(textarget);
+        TextureID texturePacked       = FromGL<TextureID>(texture);
+        Texture *textureObj           = getTexture(texturePacked);
+        ImageIndex index              = ImageIndex::MakeFromTarget(textargetPacked, level, 1);
+        framebuffer->setAttachmentMultisample(this, GL_TEXTURE, attachment, index, textureObj,
+                                              samples);
+    }
+    else
+    {
+        framebuffer->resetAttachment(this, attachment);
+    }
+
+    mState.setObjectDirty(target);
+}
 
 void Context::getSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei *length, GLint *values)
 {
@@ -8523,7 +8543,8 @@
         {
             static_assert(GL_MAX_SAMPLES_ANGLE == GL_MAX_SAMPLES,
                           "GL_MAX_SAMPLES_ANGLE not equal to GL_MAX_SAMPLES");
-            if ((getClientMajorVersion() < 3) && !getExtensions().framebufferMultisample)
+            if ((getClientMajorVersion() < 3) && !(getExtensions().framebufferMultisample ||
+                                                   getExtensions().multisampledRenderToTexture))
             {
                 return false;
             }
diff --git a/src/libANGLE/Framebuffer.cpp b/src/libANGLE/Framebuffer.cpp
index 7fa5a3a..e6881eb 100644
--- a/src/libANGLE/Framebuffer.cpp
+++ b/src/libANGLE/Framebuffer.cpp
@@ -140,11 +140,43 @@
     return true;
 }
 
+bool CheckAttachmentSampleCounts(const Context *context,
+                                 GLsizei currAttachmentSamples,
+                                 GLsizei samples,
+                                 bool colorAttachment)
+{
+    if (currAttachmentSamples != samples)
+    {
+        if (colorAttachment)
+        {
+            // APPLE_framebuffer_multisample, which EXT_draw_buffers refers to, requires that
+            // all color attachments have the same number of samples for the FBO to be complete.
+            return false;
+        }
+        else
+        {
+            // CHROMIUM_framebuffer_mixed_samples allows a framebuffer to be considered complete
+            // when its depth or stencil samples are a multiple of the number of color samples.
+            if (!context->getExtensions().framebufferMixedSamples)
+            {
+                return false;
+            }
+
+            if ((currAttachmentSamples % std::max(samples, 1)) != 0)
+            {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
 bool CheckAttachmentSampleCompleteness(const Context *context,
                                        const FramebufferAttachment &attachment,
                                        bool colorAttachment,
                                        Optional<int> *samples,
-                                       Optional<bool> *fixedSampleLocations)
+                                       Optional<bool> *fixedSampleLocations,
+                                       Optional<int> *renderToTextureSamples)
 {
     ASSERT(attachment.isAttached());
 
@@ -152,6 +184,12 @@
     {
         const Texture *texture = attachment.getTexture();
         ASSERT(texture);
+        GLenum internalFormat         = attachment.getFormat().info->internalFormat;
+        const TextureCaps &formatCaps = context->getTextureCaps().get(internalFormat);
+        if (static_cast<GLuint>(attachment.getSamples()) > formatCaps.getMaxSamples())
+        {
+            return false;
+        }
 
         const ImageIndex &attachmentImageIndex = attachment.getTextureImageIndex();
         bool fixedSampleloc = texture->getAttachmentFixedSampleLocations(attachmentImageIndex);
@@ -165,29 +203,34 @@
         }
     }
 
-    if (samples->valid())
+    if (renderToTextureSamples->valid())
     {
-        if (attachment.getSamples() != samples->value())
+        // Only check against RenderToTextureSamples if they actually exist.
+        if (renderToTextureSamples->value() !=
+            FramebufferAttachment::kDefaultRenderToTextureSamples)
         {
-            if (colorAttachment)
+            if (!CheckAttachmentSampleCounts(context, attachment.getRenderToTextureSamples(),
+                                             renderToTextureSamples->value(), colorAttachment))
             {
-                // APPLE_framebuffer_multisample, which EXT_draw_buffers refers to, requires that
-                // all color attachments have the same number of samples for the FBO to be complete.
                 return false;
             }
-            else
-            {
-                // CHROMIUM_framebuffer_mixed_samples allows a framebuffer to be considered complete
-                // when its depth or stencil samples are a multiple of the number of color samples.
-                if (!context->getExtensions().framebufferMixedSamples)
-                {
-                    return false;
-                }
+        }
+    }
+    else
+    {
+        *renderToTextureSamples = attachment.getRenderToTextureSamples();
+    }
 
-                if ((attachment.getSamples() % std::max(samples->value(), 1)) != 0)
-                {
-                    return false;
-                }
+    if (samples->valid())
+    {
+        // RenderToTextureSamples takes precedence if they exist.
+        if (renderToTextureSamples->value() ==
+            FramebufferAttachment::kDefaultRenderToTextureSamples)
+        {
+            if (!CheckAttachmentSampleCounts(context, attachment.getSamples(), samples->value(),
+                                             colorAttachment))
+            {
+                return false;
             }
         }
     }
@@ -1049,6 +1092,7 @@
     Optional<int> samples;
     Optional<bool> fixedSampleLocations;
     bool hasRenderbuffer = false;
+    Optional<int> renderToTextureSamples;
 
     const FramebufferAttachment *firstAttachment = getFirstNonNullAttachment();
 
@@ -1071,7 +1115,7 @@
             }
 
             if (!CheckAttachmentSampleCompleteness(context, colorAttachment, true, &samples,
-                                                   &fixedSampleLocations))
+                                                   &fixedSampleLocations, &renderToTextureSamples))
             {
                 return GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE;
             }
@@ -1148,7 +1192,7 @@
         }
 
         if (!CheckAttachmentSampleCompleteness(context, depthAttachment, false, &samples,
-                                               &fixedSampleLocations))
+                                               &fixedSampleLocations, &renderToTextureSamples))
         {
             return GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE;
         }
@@ -1193,7 +1237,7 @@
         }
 
         if (!CheckAttachmentSampleCompleteness(context, stencilAttachment, false, &samples,
-                                               &fixedSampleLocations))
+                                               &fixedSampleLocations, &renderToTextureSamples))
         {
             return GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE;
         }
@@ -1596,10 +1640,15 @@
 
 int Framebuffer::getSamples(const Context *context)
 {
-    return (isComplete(context) ? getCachedSamples(context) : 0);
+    return (isComplete(context) ? getCachedSamples(context, AttachmentSampleType::Emulated) : 0);
 }
 
-int Framebuffer::getCachedSamples(const Context *context) const
+int Framebuffer::getResourceSamples(const Context *context)
+{
+    return (isComplete(context) ? getCachedSamples(context, AttachmentSampleType::Resource) : 0);
+}
+
+int Framebuffer::getCachedSamples(const Context *context, AttachmentSampleType sampleType) const
 {
     ASSERT(mCachedStatus.valid() && mCachedStatus.value() == GL_FRAMEBUFFER_COMPLETE);
 
@@ -1609,7 +1658,15 @@
     if (firstNonNullAttachment)
     {
         ASSERT(firstNonNullAttachment->isAttached());
-        return firstNonNullAttachment->getSamples();
+        if (sampleType == AttachmentSampleType::Resource)
+        {
+            return firstNonNullAttachment->getResourceSamples();
+        }
+        else
+        {
+            ASSERT(sampleType == AttachmentSampleType::Emulated);
+            return firstNonNullAttachment->getSamples();
+        }
     }
 
     // No attachments found.
@@ -1641,6 +1698,18 @@
                   FramebufferAttachment::kDefaultRenderToTextureSamples);
 }
 
+void Framebuffer::setAttachmentMultisample(const Context *context,
+                                           GLenum type,
+                                           GLenum binding,
+                                           const ImageIndex &textureIndex,
+                                           FramebufferAttachmentObject *resource,
+                                           GLsizei samples)
+{
+    setAttachment(context, type, binding, textureIndex, resource,
+                  FramebufferAttachment::kDefaultNumViews,
+                  FramebufferAttachment::kDefaultBaseViewIndex, false, samples);
+}
+
 void Framebuffer::setAttachment(const Context *context,
                                 GLenum type,
                                 GLenum binding,
diff --git a/src/libANGLE/Framebuffer.h b/src/libANGLE/Framebuffer.h
index 73d8ec8..f373d01 100644
--- a/src/libANGLE/Framebuffer.h
+++ b/src/libANGLE/Framebuffer.h
@@ -49,6 +49,15 @@
 class Texture;
 class TextureCapsMap;
 
+enum class AttachmentSampleType
+{
+    // The sample count of the actual resource
+    Resource,
+    // If render_to_texture is used, this is the sample count of the multisampled
+    // texture that is created behind the scenes.
+    Emulated
+};
+
 class FramebufferState final : angle::NonCopyable
 {
   public:
@@ -181,6 +190,12 @@
                        GLenum binding,
                        const ImageIndex &textureIndex,
                        FramebufferAttachmentObject *resource);
+    void setAttachmentMultisample(const Context *context,
+                                  GLenum type,
+                                  GLenum binding,
+                                  const ImageIndex &textureIndex,
+                                  FramebufferAttachmentObject *resource,
+                                  GLsizei samples);
     void setAttachmentMultiview(const Context *context,
                                 GLenum type,
                                 GLenum binding,
@@ -232,6 +247,7 @@
 
     // This method calls checkStatus.
     int getSamples(const Context *context);
+    int getResourceSamples(const Context *context);
 
     angle::Result getSamplePosition(const Context *context, size_t index, GLfloat *xy) const;
 
@@ -247,6 +263,7 @@
     void setDefaultLayers(GLint defaultLayers);
 
     void invalidateCompletenessCache();
+    ANGLE_INLINE bool cachedStatusValid() { return mCachedStatus.valid(); }
 
     ANGLE_INLINE GLenum checkStatus(const Context *context)
     {
@@ -262,7 +279,7 @@
     }
 
     // For when we don't want to check completeness in getSamples().
-    int getCachedSamples(const Context *context) const;
+    int getCachedSamples(const Context *context, AttachmentSampleType sampleType) const;
 
     // Helper for checkStatus == GL_FRAMEBUFFER_COMPLETE.
     ANGLE_INLINE bool isComplete(const Context *context)
diff --git a/src/libANGLE/Texture.cpp b/src/libANGLE/Texture.cpp
index 23f7d68..d30611c 100644
--- a/src/libANGLE/Texture.cpp
+++ b/src/libANGLE/Texture.cpp
@@ -1840,14 +1840,28 @@
 
 void Texture::onSubjectStateChange(angle::SubjectIndex index, angle::SubjectMessage message)
 {
-    ASSERT(message == angle::SubjectMessage::SubjectChanged);
-    mDirtyBits.set(DIRTY_BIT_IMPLEMENTATION);
-    signalDirtyState(DIRTY_BIT_IMPLEMENTATION);
-
-    // Notify siblings that we are dirty.
-    if (index == rx::kTextureImageImplObserverMessageIndex)
+    switch (message)
     {
-        notifySiblings(message);
+        case angle::SubjectMessage::ContentsChanged:
+            // ContentsChange is originates from TextureStorage11::resolveAndReleaseTexture
+            // which resolves the underlying multisampled texture if it exists and so
+            // Texture will signal dirty storage to invalidate its own cache and the
+            // attached framebuffer's cache.
+            signalDirtyStorage(InitState::Initialized);
+            return;
+        case angle::SubjectMessage::SubjectChanged:
+            mDirtyBits.set(DIRTY_BIT_IMPLEMENTATION);
+            signalDirtyState(DIRTY_BIT_IMPLEMENTATION);
+
+            // Notify siblings that we are dirty.
+            if (index == rx::kTextureImageImplObserverMessageIndex)
+            {
+                notifySiblings(message);
+            }
+            return;
+        default:
+            return;
     }
 }
+
 }  // namespace gl
diff --git a/src/libANGLE/queryutils.cpp b/src/libANGLE/queryutils.cpp
index da40ad2..7d544e1 100644
--- a/src/libANGLE/queryutils.cpp
+++ b/src/libANGLE/queryutils.cpp
@@ -1113,6 +1113,17 @@
             *params = attachmentObject->isLayered();
             break;
 
+        case GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_SAMPLES_EXT:
+            if (attachmentObject->type() == GL_TEXTURE)
+            {
+                *params = attachmentObject->getSamples();
+            }
+            else
+            {
+                *params = 0;
+            }
+            break;
+
         default:
             UNREACHABLE();
             break;
diff --git a/src/libANGLE/renderer/d3d/TextureD3D.cpp b/src/libANGLE/renderer/d3d/TextureD3D.cpp
index 371b051..d63f6ee 100644
--- a/src/libANGLE/renderer/d3d/TextureD3D.cpp
+++ b/src/libANGLE/renderer/d3d/TextureD3D.cpp
@@ -81,6 +81,7 @@
       mDirtyImages(true),
       mImmutable(false),
       mTexStorage(nullptr),
+      mTexStorageObserverBinding(this, kTextureStorageObserverMessageIndex),
       mBaseLevel(0)
 {}
 
@@ -796,6 +797,11 @@
     return 0;
 }
 
+void TextureD3D::onSubjectStateChange(angle::SubjectIndex index, angle::SubjectMessage message)
+{
+    onStateChange(message);
+}
+
 TextureD3D_2D::TextureD3D_2D(const gl::TextureState &state, RendererD3D *renderer)
     : TextureD3D(state, renderer)
 {
@@ -895,6 +901,10 @@
 
     // Attempt a fast gpu copy of the pixel data to the surface
     gl::Buffer *unpackBuffer = context->getState().getTargetBuffer(gl::BufferBinding::PixelUnpack);
+    if (mTexStorage)
+    {
+        ANGLE_TRY(mTexStorage->releaseMultisampledTexStorageForLevel(index.getLevelIndex()));
+    }
     if (isFastUnpackable(unpackBuffer, internalFormatInfo.sizedInternalFormat) &&
         isLevelComplete(index.getLevelIndex()))
     {
@@ -934,6 +944,10 @@
     ASSERT(index.getTarget() == gl::TextureTarget::_2D && area.depth == 1 && area.z == 0);
 
     GLenum mipFormat = getInternalFormat(index.getLevelIndex());
+    if (mTexStorage)
+    {
+        ANGLE_TRY(mTexStorage->releaseMultisampledTexStorageForLevel(index.getLevelIndex()));
+    }
     if (isFastUnpackable(unpackBuffer, mipFormat) && isLevelComplete(index.getLevelIndex()))
     {
         RenderTargetD3D *renderTarget = nullptr;
@@ -1481,6 +1495,7 @@
 
     ANGLE_TRY(releaseTexStorage(context));
     mTexStorage = newCompleteTexStorage;
+    mTexStorageObserverBinding.bind(mTexStorage);
 
     mDirtyImages = true;
 
@@ -2146,6 +2161,7 @@
 
     ANGLE_TRY(releaseTexStorage(context));
     mTexStorage = newCompleteTexStorage;
+    mTexStorageObserverBinding.bind(mTexStorage);
 
     mDirtyImages = true;
     return angle::Result::Continue;
@@ -2808,7 +2824,8 @@
                                                    TextureStorage *newCompleteTexStorage)
 {
     ANGLE_TRY(releaseTexStorage(context));
-    mTexStorage  = newCompleteTexStorage;
+    mTexStorage = newCompleteTexStorage;
+    mTexStorageObserverBinding.bind(mTexStorage);
     mDirtyImages = true;
 
     // We do not support managed 3D storage, as that is D3D9/ES2-only
@@ -3533,7 +3550,8 @@
                                                         TextureStorage *newCompleteTexStorage)
 {
     ANGLE_TRY(releaseTexStorage(context));
-    mTexStorage  = newCompleteTexStorage;
+    mTexStorage = newCompleteTexStorage;
+    mTexStorageObserverBinding.bind(mTexStorage);
     mDirtyImages = true;
 
     // We do not support managed 2D array storage, as managed storage is ES2/D3D9 only
@@ -4075,6 +4093,7 @@
     // These textures are immutable, so this should only be ever called once.
     ASSERT(!mTexStorage);
     mTexStorage = newCompleteTexStorage;
+    mTexStorageObserverBinding.bind(mTexStorage);
     return angle::Result::Continue;
 }
 
@@ -4192,6 +4211,7 @@
     // These textures are immutable, so this should only be ever called once.
     ASSERT(!mTexStorage);
     mTexStorage = newCompleteTexStorage;
+    mTexStorageObserverBinding.bind(mTexStorage);
     return angle::Result::Continue;
 }
 
diff --git a/src/libANGLE/renderer/d3d/TextureD3D.h b/src/libANGLE/renderer/d3d/TextureD3D.h
index 3e6613e..fd918c9 100644
--- a/src/libANGLE/renderer/d3d/TextureD3D.h
+++ b/src/libANGLE/renderer/d3d/TextureD3D.h
@@ -29,7 +29,7 @@
 class RenderTargetD3D;
 class TextureStorage;
 
-class TextureD3D : public TextureImpl
+class TextureD3D : public TextureImpl, public angle::ObserverInterface
 {
   public:
     TextureD3D(const gl::TextureState &data, RendererD3D *renderer);
@@ -113,6 +113,9 @@
 
     GLsizei getRenderToTextureSamples();
 
+    // ObserverInterface implementation.
+    void onSubjectStateChange(angle::SubjectIndex index, angle::SubjectMessage message) override;
+
   protected:
     angle::Result setImageImpl(const gl::Context *context,
                                const gl::ImageIndex &index,
@@ -184,6 +187,7 @@
 
     bool mImmutable;
     TextureStorage *mTexStorage;
+    angle::ObserverBinding mTexStorageObserverBinding;
 
   private:
     virtual angle::Result initializeStorage(const gl::Context *context, bool renderTarget) = 0;
diff --git a/src/libANGLE/renderer/d3d/TextureStorage.h b/src/libANGLE/renderer/d3d/TextureStorage.h
index cc83a66..f70f0b6 100644
--- a/src/libANGLE/renderer/d3d/TextureStorage.h
+++ b/src/libANGLE/renderer/d3d/TextureStorage.h
@@ -34,11 +34,14 @@
 class RenderTargetD3D;
 class ImageD3D;
 
-class TextureStorage : angle::NonCopyable
+// Dirty bit messages from TextureStorage
+constexpr size_t kTextureStorageObserverMessageIndex = 0;
+
+class TextureStorage : public angle::Subject
 {
   public:
     TextureStorage() {}
-    virtual ~TextureStorage() {}
+    ~TextureStorage() override {}
 
     virtual angle::Result onDestroy(const gl::Context *context);
 
@@ -74,6 +77,8 @@
     virtual void invalidateTextures() {}
 
     // RenderToTexture methods
+    virtual angle::Result releaseMultisampledTexStorageForLevel(size_t level);
+    virtual angle::Result resolveAndReleaseTexture(const gl::Context *context);
     virtual GLsizei getRenderToTextureSamples() const;
 
   protected:
@@ -91,6 +96,16 @@
     return angle::Result::Continue;
 }
 
+inline angle::Result TextureStorage::releaseMultisampledTexStorageForLevel(size_t level)
+{
+    return angle::Result::Continue;
+}
+
+inline angle::Result TextureStorage::resolveAndReleaseTexture(const gl::Context *context)
+{
+    return angle::Result::Continue;
+}
+
 inline GLsizei TextureStorage::getRenderToTextureSamples() const
 {
     return 0;
diff --git a/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp b/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
index f95c7ed..a4b5f54 100644
--- a/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
@@ -2703,7 +2703,7 @@
         desc.ArraySize          = 1;
         desc.Format             = formatInfo.texFormat;
         desc.SampleDesc.Count   = (supportedSamples == 0) ? 1 : supportedSamples;
-        desc.SampleDesc.Quality = 0;
+        desc.SampleDesc.Quality = (supportedSamples == 0) ? 0 : D3D11_STANDARD_MULTISAMPLE_PATTERN;
         desc.Usage              = D3D11_USAGE_DEFAULT;
         desc.CPUAccessFlags     = 0;
         desc.MiscFlags          = 0;
@@ -3558,7 +3558,8 @@
     bool partialDSBlit =
         (nativeFormat.depthBits > 0 && depthBlit) != (nativeFormat.stencilBits > 0 && stencilBlit);
 
-    if (readRenderTarget11->getFormatSet().formatID ==
+    if (drawRenderTarget->getSamples() == readRenderTarget->getSamples() &&
+        readRenderTarget11->getFormatSet().formatID ==
             drawRenderTarget11->getFormatSet().formatID &&
         !stretchRequired && !outOfBounds && !reversalRequired && !partialDSBlit &&
         !colorMaskingNeeded && (!(depthBlit || stencilBlit) || wholeBufferCopy))
diff --git a/src/libANGLE/renderer/d3d/d3d11/StateManager11.cpp b/src/libANGLE/renderer/d3d/d3d11/StateManager11.cpp
index 5abeff7..ac40084 100644
--- a/src/libANGLE/renderer/d3d/d3d11/StateManager11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/StateManager11.cpp
@@ -1531,7 +1531,7 @@
         mCurDisableStencil = disableStencil;
     }
 
-    bool multiSample = (fbo->getCachedSamples(context) != 0);
+    bool multiSample = (fbo->getCachedSamples(context, gl::AttachmentSampleType::Emulated) != 0);
     if (multiSample != mCurRasterState.multiSample)
     {
         mInternalDirtyBits.set(DIRTY_BIT_RASTERIZER_STATE);
diff --git a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
index 3cc4ab4..c2c5fb4 100644
--- a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
@@ -218,6 +218,7 @@
                                                  const gl::SamplerState &sampler,
                                                  const d3d11::SharedSRV **outSRV)
 {
+    ANGLE_TRY(resolveAndReleaseTexture(context));
     // Make sure to add the level offset for our tiny compressed texture workaround
     const GLuint effectiveBaseLevel = textureState.getEffectiveBaseLevel();
     const bool swizzleRequired      = textureState.swizzleRequired();
@@ -330,6 +331,7 @@
 {
     ASSERT(mipLevel >= 0 && mipLevel < getLevelCount());
 
+    ANGLE_TRY(resolveAndReleaseTexture(context));
     auto &levelSRVs      = (blitSRV) ? mLevelBlitSRVs : mLevelSRVs;
     auto &otherLevelSRVs = (blitSRV) ? mLevelSRVs : mLevelBlitSRVs;
 
@@ -361,6 +363,7 @@
                                              GLint maxLevel,
                                              const d3d11::SharedSRV **outSRV)
 {
+    ANGLE_TRY(resolveAndReleaseTexture(context));
     unsigned int mipLevels = maxLevel - baseLevel + 1;
 
     // Make sure there's 'mipLevels' mipmap levels below the base level (offset by the top level,
@@ -390,6 +393,7 @@
                                                const gl::ImageUnit &imageUnit,
                                                const d3d11::SharedSRV **outSRV)
 {
+    ANGLE_TRY(resolveAndReleaseTexture(context));
     // TODO(Xinghua.cao@intel.com): Add solution to handle swizzle required.
     ImageKey key(imageUnit.level, (imageUnit.layered == GL_TRUE), imageUnit.layer, imageUnit.access,
                  imageUnit.format);
@@ -422,6 +426,7 @@
                                                const gl::ImageUnit &imageUnit,
                                                const d3d11::SharedUAV **outUAV)
 {
+    ANGLE_TRY(resolveAndReleaseTexture(context));
     // TODO(Xinghua.cao@intel.com): Add solution to handle swizzle required.
     ImageKey key(imageUnit.level, (imageUnit.layered == GL_TRUE), imageUnit.layer, imageUnit.access,
                  imageUnit.format);
@@ -458,6 +463,7 @@
 angle::Result TextureStorage11::generateSwizzles(const gl::Context *context,
                                                  const gl::SwizzleState &swizzleTarget)
 {
+    ANGLE_TRY(resolveAndReleaseTexture(context));
     for (int level = 0; level < getLevelCount(); level++)
     {
         // Check if the swizzle for this level is out of date
@@ -519,6 +525,7 @@
 {
     ASSERT(srcTexture.valid());
 
+    ANGLE_TRY(resolveAndReleaseTexture(context));
     const GLint level = index.getLevelIndex();
 
     markLevelDirty(level);
@@ -583,6 +590,7 @@
 {
     ASSERT(dstTexture.valid());
 
+    ANGLE_TRY(resolveAndReleaseTexture(context));
     const TextureHelper11 *srcTexture = nullptr;
 
     // If the zero-LOD workaround is active and we want to update a level greater than zero, then we
@@ -642,14 +650,15 @@
 {
     ASSERT(sourceIndex.getLayerIndex() == destIndex.getLayerIndex());
 
+    ANGLE_TRY(resolveAndReleaseTexture(context));
     markLevelDirty(destIndex.getLevelIndex());
 
     RenderTargetD3D *source = nullptr;
     ANGLE_TRY(getRenderTarget(context, sourceIndex, 0, &source));
 
+    // dest will always have 0 since, we have just released the MS Texture struct
     RenderTargetD3D *dest = nullptr;
-    GLsizei destSamples   = 0;
-    ANGLE_TRY(getRenderTarget(context, destIndex, destSamples, &dest));
+    ANGLE_TRY(getRenderTarget(context, destIndex, 0, &dest));
 
     RenderTarget11 *rt11                   = GetAs<RenderTarget11>(source);
     const d3d11::SharedSRV &sourceSRV      = rt11->getBlitShaderResourceView(context);
@@ -696,6 +705,7 @@
 {
     ASSERT(destStorage);
 
+    ANGLE_TRY(resolveAndReleaseTexture(context));
     const TextureHelper11 *sourceResouce = nullptr;
     ANGLE_TRY(getResource(context, &sourceResouce));
 
@@ -726,6 +736,7 @@
 {
     ASSERT(!image->isDirty());
 
+    ANGLE_TRY(resolveAndReleaseTexture(context));
     markLevelDirty(index.getLevelIndex());
 
     const TextureHelper11 *resource = nullptr;
@@ -856,6 +867,36 @@
     return angle::Result::Continue;
 }
 
+angle::Result TextureStorage11::resolveTextureHelper(const gl::Context *context,
+                                                     const TextureHelper11 &texture)
+{
+    if (mMSTexInfo)
+    {
+        gl::ImageIndex indexSS = gl::ImageIndex::Make2D(mMSTexInfo->index.getLevelIndex());
+        UINT subresourceIndexSS;
+        ANGLE_TRY(getSubresourceIndex(context, indexSS, &subresourceIndexSS));
+        // For MS texture level must = 0, layer is the entire level -> 0
+        // and miplevels must = 1. D3D11CalcSubresource(level, layer, miplevels);
+        UINT subresourceIndexMS            = D3D11CalcSubresource(0, 0, 1);
+        ID3D11DeviceContext *deviceContext = mRenderer->getDeviceContext();
+        const TextureHelper11 *resource    = nullptr;
+        ANGLE_TRY(mMSTexInfo->msTex->getResource(context, &resource));
+        deviceContext->ResolveSubresource(texture.get(), subresourceIndexSS, resource->get(),
+                                          subresourceIndexMS, texture.getFormat());
+    }
+    return angle::Result::Continue;
+}
+
+angle::Result TextureStorage11::releaseMultisampledTexStorageForLevel(size_t level)
+{
+    if (mMSTexInfo && mMSTexInfo->index.getLevelIndex() == static_cast<int>(level))
+    {
+        mMSTexInfo->msTex.reset();
+        onStateChange(angle::SubjectMessage::ContentsChanged);
+    }
+    return angle::Result::Continue;
+}
+
 GLsizei TextureStorage11::getRenderToTextureSamples() const
 {
     if (mMSTexInfo)
@@ -865,6 +906,58 @@
     return 0;
 }
 
+angle::Result TextureStorage11::getMultisampledRenderTarget(const gl::Context *context,
+                                                            const gl::ImageIndex &index,
+                                                            GLsizei samples,
+                                                            RenderTargetD3D **outRT)
+{
+    const int level = index.getLevelIndex();
+    if (mMSTexInfo && level == mMSTexInfo->index.getLevelIndex() &&
+        samples == mMSTexInfo->samples && mMSTexInfo->msTex)
+    {
+        RenderTargetD3D *rt;
+        ANGLE_TRY(mMSTexInfo->msTex->getRenderTarget(context, index, samples, &rt));
+        *outRT = rt;
+        return angle::Result::Continue;
+    }
+    else
+    {
+        // if mMSTexInfo already exists, then we want to resolve and release it
+        // since the mMSTexInfo must be for a different sample count or level
+        ANGLE_TRY(resolveAndReleaseTexture(context));
+        ASSERT(!mMSTexInfo);
+
+        // Now we can create a new object for the correct sample and level
+        GLsizei width         = getLevelWidth(level);
+        GLsizei height        = getLevelHeight(level);
+        GLenum internalFormat = mFormatInfo.internalFormat;
+        std::unique_ptr<TextureStorage11_2DMultisample> texMS(
+            GetAs<TextureStorage11_2DMultisample>(mRenderer->createTextureStorage2DMultisample(
+                internalFormat, width, height, level, samples, true)));
+
+        // make sure multisample object has the blitted information.
+        gl::Rectangle area(0, 0, width, height);
+        gl::ImageIndex indexSS            = gl::ImageIndex::Make2D(level);
+        RenderTargetD3D *readRenderTarget = nullptr;
+        ANGLE_TRY(getRenderTarget(context, index, 0, &readRenderTarget));
+        gl::ImageIndex indexMS            = gl::ImageIndex::Make2DMultisample();
+        RenderTargetD3D *drawRenderTarget = nullptr;
+        ANGLE_TRY(texMS->getRenderTarget(context, indexMS, samples, &drawRenderTarget));
+
+        // blit SS -> MS
+        // mask: GL_COLOR_BUFFER_BIT, filter: GL_NEAREST
+        ANGLE_TRY(mRenderer->blitRenderbufferRect(context, area, area, readRenderTarget,
+                                                  drawRenderTarget, GL_NEAREST, nullptr, true,
+                                                  false, false));
+        mMSTexInfo        = std::make_unique<MultisampledRenderToTextureInfo>(samples, indexSS);
+        mMSTexInfo->msTex = std::move(texMS);
+        RenderTargetD3D *rt;
+        ANGLE_TRY(mMSTexInfo->msTex->getRenderTarget(context, index, samples, &rt));
+        *outRT = rt;
+        return angle::Result::Continue;
+    }
+}
+
 TextureStorage11_2D::TextureStorage11_2D(Renderer11 *renderer, SwapChain11 *swapchain)
     : TextureStorage11(renderer,
                        D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE,
@@ -1130,6 +1223,7 @@
 angle::Result TextureStorage11_2D::ensureTextureExists(const gl::Context *context, int mipLevels)
 {
     // If mMipLevels = 1 then always use mTexture rather than mLevelZeroTexture.
+    ANGLE_TRY(resolveAndReleaseTexture(context));
     bool useLevelZeroTexture = mRenderer->getFeatures().zeroMaxLodWorkaround.enabled
                                    ? (mipLevels == 1) && (mMipLevels > 1)
                                    : false;
@@ -1180,6 +1274,16 @@
     const int level = index.getLevelIndex();
     ASSERT(level >= 0 && level < getLevelCount());
 
+    bool needMS = samples > 0;
+    if (needMS)
+    {
+        return getMultisampledRenderTarget(context, index, samples, outRT);
+    }
+    else
+    {
+        ANGLE_TRY(resolveAndReleaseTexture(context));
+    }
+
     // In GL ES 2.0, the application can only render to level zero of the texture (Section 4.4.3 of
     // the GLES 2.0 spec, page 113 of version 2.0.25). Other parts of TextureStorage11_2D could
     // create RTVs on non-zero levels of the texture (e.g. generateMipmap).
@@ -1448,6 +1552,21 @@
     return angle::Result::Continue;
 }
 
+angle::Result TextureStorage11_2D::resolveAndReleaseTexture(const gl::Context *context)
+{
+    if (mMSTexInfo)
+    {
+        if (mMSTexInfo->msTex)
+        {
+            ANGLE_TRY(resolveTextureHelper(context, mTexture));
+            mRenderTarget[mMSTexInfo->index.getLevelIndex()].reset(nullptr);
+        }
+        mMSTexInfo.reset(nullptr);
+        onStateChange(angle::SubjectMessage::ContentsChanged);
+    }
+    return angle::Result::Continue;
+}
+
 TextureStorage11_External::TextureStorage11_External(
     Renderer11 *renderer,
     egl::Stream *stream,
@@ -2170,6 +2289,7 @@
 angle::Result TextureStorage11_Cube::ensureTextureExists(const gl::Context *context, int mipLevels)
 {
     // If mMipLevels = 1 then always use mTexture rather than mLevelZeroTexture.
+    ANGLE_TRY(resolveAndReleaseTexture(context));
     bool useLevelZeroTexture = mRenderer->getFeatures().zeroMaxLodWorkaround.enabled
                                    ? (mipLevels == 1) && (mMipLevels > 1)
                                    : false;
@@ -2241,6 +2361,16 @@
     ASSERT(level >= 0 && level < getLevelCount());
     ASSERT(faceIndex >= 0 && faceIndex < static_cast<GLint>(gl::kCubeFaceCount));
 
+    bool needMS = samples > 0;
+    if (needMS)
+    {
+        return getMultisampledRenderTarget(context, index, samples, outRT);
+    }
+    else
+    {
+        ANGLE_TRY(resolveAndReleaseTexture(context));
+    }
+
     Context11 *context11 = GetImplAs<Context11>(context);
 
     if (!mRenderTarget[faceIndex][level])
@@ -2534,6 +2664,23 @@
     return angle::Result::Continue;
 }
 
+angle::Result TextureStorage11_Cube::resolveAndReleaseTexture(const gl::Context *context)
+{
+    if (mMSTexInfo)
+    {
+        if (mMSTexInfo->msTex)
+        {
+            ANGLE_TRY(resolveTextureHelper(context, mTexture));
+            const int faceIndex = mMSTexInfo->index.cubeMapFaceIndex();
+            const int level     = mMSTexInfo->index.getLevelIndex();
+            mRenderTarget[faceIndex][level].reset(nullptr);
+        }
+        mMSTexInfo.reset(nullptr);
+        onStateChange(angle::SubjectMessage::ContentsChanged);
+    }
+    return angle::Result::Continue;
+}
+
 TextureStorage11_3D::TextureStorage11_3D(Renderer11 *renderer,
                                          GLenum internalformat,
                                          bool renderTarget,
diff --git a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.h b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.h
index f85444e..e47e132 100644
--- a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.h
+++ b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.h
@@ -184,6 +184,14 @@
     // Clear all cached non-swizzle SRVs and invalidate the swizzle cache.
     void clearSRVCache();
 
+    // Helper for resolving MS shadowed texture
+    angle::Result resolveTextureHelper(const gl::Context *context, const TextureHelper11 &texture);
+    angle::Result releaseMultisampledTexStorageForLevel(size_t level) override;
+    angle::Result getMultisampledRenderTarget(const gl::Context *context,
+                                              const gl::ImageIndex &index,
+                                              GLsizei samples,
+                                              RenderTargetD3D **outRT);
+
     Renderer11 *mRenderer;
     int mTopLevel;
     unsigned int mMipLevels;
@@ -298,6 +306,8 @@
 
     angle::Result ensureTextureExists(const gl::Context *context, int mipLevels);
 
+    angle::Result resolveAndReleaseTexture(const gl::Context *context) override;
+
   private:
     angle::Result createSRVForSampler(const gl::Context *context,
                                       int baseLevel,
@@ -539,6 +549,8 @@
 
     angle::Result ensureTextureExists(const gl::Context *context, int mipLevels);
 
+    angle::Result resolveAndReleaseTexture(const gl::Context *context) override;
+
   private:
     angle::Result createSRVForSampler(const gl::Context *context,
                                       int baseLevel,
diff --git a/src/libANGLE/renderer/d3d/d3d11/renderer11_utils.cpp b/src/libANGLE/renderer/d3d/d3d11/renderer11_utils.cpp
index 085be7f..b58a55d 100644
--- a/src/libANGLE/renderer/d3d/d3d11/renderer11_utils.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/renderer11_utils.cpp
@@ -1649,16 +1649,17 @@
     extensions->copyTexture                      = true;
     extensions->copyCompressedTexture            = true;
     extensions->textureStorageMultisample2DArray = true;
-    extensions->multiviewMultisample     = ((extensions->multiview || extensions->multiview2) &&
+    extensions->multiviewMultisample        = ((extensions->multiview || extensions->multiview2) &&
                                         extensions->textureStorageMultisample2DArray);
-    extensions->copyTexture3d            = true;
-    extensions->textureBorderClamp       = true;
-    extensions->textureMultisample       = true;
-    extensions->provokingVertex          = true;
-    extensions->blendFuncExtended        = true;
-    extensions->maxDualSourceDrawBuffers = 1;
-    extensions->texture3DOES                     = true;
-    extensions->baseVertexBaseInstance           = true;
+    extensions->copyTexture3d               = true;
+    extensions->textureBorderClamp          = true;
+    extensions->textureMultisample          = true;
+    extensions->provokingVertex             = true;
+    extensions->blendFuncExtended           = true;
+    extensions->maxDualSourceDrawBuffers    = 1;
+    extensions->texture3DOES                = true;
+    extensions->baseVertexBaseInstance      = true;
+    extensions->multisampledRenderToTexture = true;
 
     // D3D11 cannot support reading depth texture as a luminance texture.
     // It treats it as a red-channel-only texture.
diff --git a/src/libANGLE/renderer/gl/FramebufferGL.cpp b/src/libANGLE/renderer/gl/FramebufferGL.cpp
index 9fbe4ab..095055c 100644
--- a/src/libANGLE/renderer/gl/FramebufferGL.cpp
+++ b/src/libANGLE/renderer/gl/FramebufferGL.cpp
@@ -568,7 +568,11 @@
     GLsizei readAttachmentSamples = 0;
     if (colorReadAttachment != nullptr)
     {
-        readAttachmentSamples = colorReadAttachment->getSamples();
+        // Blitting requires that the textures be single sampled. getSamples will return
+        // emulated sample number, but the EXT_multisampled_render_to_texture extension will
+        // take care of resolving the texture, so even if emulated samples > 0, we should still
+        // be able to blit as long as the underlying resource samples is single sampled.
+        readAttachmentSamples = colorReadAttachment->getResourceSamples();
     }
 
     bool needManualColorBlit = false;
diff --git a/src/libANGLE/renderer/vulkan/FramebufferVk.cpp b/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
index 42e7446..f262f0e 100644
--- a/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
+++ b/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
@@ -635,7 +635,8 @@
     const bool blitDepthBuffer   = (mask & GL_DEPTH_BUFFER_BIT) != 0;
     const bool blitStencilBuffer = (mask & GL_STENCIL_BUFFER_BIT) != 0;
 
-    const bool isResolve = srcFramebuffer->getCachedSamples(context) > 1;
+    const bool isResolve =
+        srcFramebuffer->getCachedSamples(context, gl::AttachmentSampleType::Resource) > 1;
 
     FramebufferVk *srcFramebufferVk    = vk::GetImpl(srcFramebuffer);
     const bool srcFramebufferFlippedY  = contextVk->isViewportFlipEnabledForReadFBO();
diff --git a/src/libANGLE/validationES.cpp b/src/libANGLE/validationES.cpp
index 13d1eae..bbf30cc 100644
--- a/src/libANGLE/validationES.cpp
+++ b/src/libANGLE/validationES.cpp
@@ -1330,7 +1330,9 @@
         return false;
     }
 
-    if (!ValidateFramebufferNotMultisampled(context, drawFramebuffer))
+    // Not allow blitting to MS buffers, therefore if renderToTextureSamples exist,
+    // consider it MS. needResourceSamples = false
+    if (!ValidateFramebufferNotMultisampled(context, drawFramebuffer, false))
     {
         return false;
     }
@@ -2481,8 +2483,10 @@
         return false;
     }
 
+    // needResourceSamples = true. Treat renderToTexture textures as single sample since they will
+    // be resolved before copying
     if (!readFramebuffer->isDefault() &&
-        !ValidateFramebufferNotMultisampled(context, readFramebuffer))
+        !ValidateFramebufferNotMultisampled(context, readFramebuffer, true))
     {
         return false;
     }
@@ -3900,6 +3904,14 @@
             }
             break;
 
+        case GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_SAMPLES_EXT:
+            if (!context->getExtensions().multisampledRenderToTexture)
+            {
+                context->validationError(GL_INVALID_ENUM, kEnumNotSupported);
+                return false;
+            }
+            break;
+
         case GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:
             if (clientVersion < 3 && !context->getExtensions().sRGB)
             {
@@ -5446,8 +5458,10 @@
         return false;
     }
 
+    // needIntrinsic = true. Treat renderToTexture textures as single sample since they will be
+    // resolved before reading.
     if (!readFramebuffer->isDefault() &&
-        !ValidateFramebufferNotMultisampled(context, readFramebuffer))
+        !ValidateFramebufferNotMultisampled(context, readFramebuffer, true))
     {
         return false;
     }
@@ -6302,9 +6316,13 @@
     return true;
 }
 
-bool ValidateFramebufferNotMultisampled(Context *context, Framebuffer *framebuffer)
+bool ValidateFramebufferNotMultisampled(Context *context,
+                                        Framebuffer *framebuffer,
+                                        bool needResourceSamples)
 {
-    if (framebuffer->getSamples(context) != 0)
+    int samples = needResourceSamples ? framebuffer->getResourceSamples(context)
+                                      : framebuffer->getSamples(context);
+    if (samples != 0)
     {
         context->validationError(GL_INVALID_OPERATION, kInvalidMultisampledFramebufferOperation);
         return false;
diff --git a/src/libANGLE/validationES.h b/src/libANGLE/validationES.h
index 3fd6d89..04164d9 100644
--- a/src/libANGLE/validationES.h
+++ b/src/libANGLE/validationES.h
@@ -614,7 +614,9 @@
                                      GLsizei bufSize,
                                      GLsizei *numParams);
 
-bool ValidateFramebufferNotMultisampled(Context *context, Framebuffer *framebuffer);
+bool ValidateFramebufferNotMultisampled(Context *context,
+                                        Framebuffer *framebuffer,
+                                        bool needResourceSamples);
 
 bool ValidateMultitextureUnit(Context *context, GLenum texture);
 
diff --git a/src/libANGLE/validationES2.cpp b/src/libANGLE/validationES2.cpp
index d1ffe9b..821bc66 100644
--- a/src/libANGLE/validationES2.cpp
+++ b/src/libANGLE/validationES2.cpp
@@ -7350,6 +7350,78 @@
                                                 GLint level,
                                                 GLsizei samples)
 {
+    if (!context->getExtensions().multisampledRenderToTexture)
+    {
+        context->validationError(GL_INVALID_OPERATION, kExtensionNotEnabled);
+        return false;
+    }
+
+    if (samples < 0)
+    {
+        return false;
+    }
+
+    // EXT_multisampled_render_to_texture states that the value of samples
+    // must be less than or equal to MAX_SAMPLES_EXT (Context::getCaps().maxSamples)
+    // otherwise GL_INVALID_VALUE is generated.
+    if (static_cast<GLuint>(samples) > context->getCaps().maxSamples)
+    {
+        context->validationError(GL_INVALID_VALUE, kSamplesOutOfRange);
+        return false;
+    }
+
+    if (!ValidFramebufferTarget(context, target))
+    {
+        context->validationError(GL_INVALID_ENUM, kInvalidFramebufferTarget);
+        return false;
+    }
+
+    if (attachment != GL_COLOR_ATTACHMENT0)
+    {
+        context->validationError(GL_INVALID_ENUM, kInvalidAttachment);
+        return false;
+    }
+
+    TextureTarget textargetPacked = FromGLenum<TextureTarget>(textarget);
+    if (!ValidTexture2DDestinationTarget(context, textargetPacked))
+    {
+        context->validationError(GL_INVALID_ENUM, kInvalidTextureTarget);
+        return false;
+    }
+
+    if (texture != 0)
+    {
+        TextureID texturePacked = FromGL<TextureID>(texture);
+        Texture *tex            = context->getTexture(texturePacked);
+
+        if (tex == nullptr)
+        {
+            context->validationError(GL_INVALID_OPERATION, kMissingTexture);
+            return false;
+        }
+
+        if (level < 0)
+        {
+            context->validationError(GL_INVALID_VALUE, kInvalidMipLevel);
+            return false;
+        }
+
+        // EXT_multisampled_render_to_texture returns INVALID_OPERATION when a sample number higher
+        // than the maximum sample number supported by this format is passed.
+        // The TextureCaps::getMaxSamples method is only guarenteed to be valid when the context is
+        // ES3.
+        if (context->getClientMajorVersion() >= 3)
+        {
+            GLenum internalformat = tex->getFormat(textargetPacked, level).info->internalFormat;
+            const TextureCaps &formatCaps = context->getTextureCaps().get(internalformat);
+            if (static_cast<GLuint>(samples) > formatCaps.getMaxSamples())
+            {
+                context->validationError(GL_INVALID_OPERATION, kSamplesOutOfRange);
+                return false;
+            }
+        }
+    }
+
     return true;
 }
 
@@ -7360,6 +7432,40 @@
                                                GLsizei width,
                                                GLsizei height)
 {
+    if (!context->getExtensions().multisampledRenderToTexture)
+    {
+        context->validationError(GL_INVALID_OPERATION, kExtensionNotEnabled);
+        return false;
+    }
+    if (!ValidateRenderbufferStorageParametersBase(context, target, samples, internalformat, width,
+                                                   height))
+    {
+        return false;
+    }
+
+    // EXT_multisampled_render_to_texture states that the value of samples
+    // must be less than or equal to MAX_SAMPLES_EXT (Context::getCaps().maxSamples)
+    // otherwise GL_INVALID_VALUE is generated.
+    if (static_cast<GLuint>(samples) > context->getCaps().maxSamples)
+    {
+        context->validationError(GL_INVALID_VALUE, kSamplesOutOfRange);
+        return false;
+    }
+
+    // EXT_multisampled_render_to_texture returns GL_OUT_OF_MEMORY on failure to create
+    // the specified storage. This is different than ES 3.0 in which a sample number higher
+    // than the maximum sample number supported by this format generates a GL_INVALID_VALUE.
+    // The TextureCaps::getMaxSamples method is only guarenteed to be valid when the context is ES3.
+    if (context->getClientMajorVersion() >= 3)
+    {
+        const TextureCaps &formatCaps = context->getTextureCaps().get(internalformat);
+        if (static_cast<GLuint>(samples) > formatCaps.getMaxSamples())
+        {
+            context->validationError(GL_OUT_OF_MEMORY, kSamplesOutOfRange);
+            return false;
+        }
+    }
+
     return true;
 }
 
diff --git a/src/libANGLE/validationES3.cpp b/src/libANGLE/validationES3.cpp
index f399686..5a03bf5 100644
--- a/src/libANGLE/validationES3.cpp
+++ b/src/libANGLE/validationES3.cpp
@@ -984,7 +984,10 @@
         return false;
     }
 
-    if (!framebuffer->isDefault() && !ValidateFramebufferNotMultisampled(context, framebuffer))
+    // needIntrinsic = true. Treat renderToTexture textures as single sample since they will be
+    // resolved before copying
+    if (!framebuffer->isDefault() &&
+        !ValidateFramebufferNotMultisampled(context, framebuffer, true))
     {
         return false;
     }
diff --git a/src/tests/angle_end2end_tests.gni b/src/tests/angle_end2end_tests.gni
index 9b7bed6..206be02 100644
--- a/src/tests/angle_end2end_tests.gni
+++ b/src/tests/angle_end2end_tests.gni
@@ -81,6 +81,7 @@
   "gl_tests/MipmapTest.cpp",
   "gl_tests/MultiDrawTest.cpp",
   "gl_tests/MultisampleCompatibilityTest.cpp",
+  "gl_tests/MultisampledRenderToTextureTest.cpp",
   "gl_tests/MultisampleTest.cpp",
   "gl_tests/MultithreadingTest.cpp",
   "gl_tests/MultiviewDrawTest.cpp",
diff --git a/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp b/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
new file mode 100644
index 0000000..cb90c5b
--- /dev/null
+++ b/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
@@ -0,0 +1,738 @@
+//
+// Copyright 2019 The ANGLE Project Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+
+// MultisampledRenderToTextureTest: Tests of EXT_multisampled_render_to_texture extension
+
+#include "test_utils/ANGLETest.h"
+#include "test_utils/gl_raii.h"
+
+using namespace angle;
+
+namespace
+{
+constexpr char kBasicVertexShader[] =
+    R"(attribute vec3 position;
+void main()
+{
+    gl_Position = vec4(position, 1);
+})";
+
+constexpr char kGreenFragmentShader[] =
+    R"(void main()
+{
+    gl_FragColor = vec4(0, 1, 0, 1);
+})";
+
+constexpr char kRedFragmentShader[] =
+    R"(void main()
+{
+    gl_FragColor = vec4(1, 0, 0, 1);
+})";
+
+constexpr char kVS[] =
+    "precision highp float;\n"
+    "attribute vec4 position;\n"
+    "varying vec2 texcoord;\n"
+    "\n"
+    "void main()\n"
+    "{\n"
+    "    gl_Position = position;\n"
+    "    texcoord = (position.xy * 0.5) + 0.5;\n"
+    "}\n";
+
+constexpr char kFS[] =
+    "precision highp float;\n"
+    "uniform sampler2D tex;\n"
+    "varying vec2 texcoord;\n"
+    "\n"
+    "void main()\n"
+    "{\n"
+    "    gl_FragColor = texture2D(tex, texcoord);\n"
+    "}\n";
+
+class MultisampledRenderToTextureTest : public ANGLETest
+{
+  protected:
+    MultisampledRenderToTextureTest()
+    {
+        setWindowWidth(64);
+        setWindowHeight(64);
+        setConfigRedBits(8);
+        setConfigGreenBits(8);
+        setConfigBlueBits(8);
+        setConfigAlphaBits(8);
+    }
+
+    void testSetUp() override {}
+
+    void testTearDown() override {}
+
+    void setupCopyTexProgram()
+    {
+        mCopyTextureProgram.makeRaster(kVS, kFS);
+        ASSERT_GL_TRUE(mCopyTextureProgram.valid());
+
+        mCopyTextureUniformLocation = glGetUniformLocation(mCopyTextureProgram, "tex");
+
+        ASSERT_GL_NO_ERROR();
+    }
+
+    void verifyResults(GLuint texture,
+                       GLubyte data[4],
+                       GLint fboSize,
+                       GLint xs,
+                       GLint ys,
+                       GLint xe,
+                       GLint ye)
+    {
+        glViewport(0, 0, fboSize, fboSize);
+
+        glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+        // Draw a quad with the target texture
+        glUseProgram(mCopyTextureProgram);
+        glBindTexture(GL_TEXTURE_2D, texture);
+        glUniform1i(mCopyTextureUniformLocation, 0);
+
+        drawQuad(mCopyTextureProgram, "position", 0.5f);
+
+        // Expect that the rendered quad has the same color as the source texture
+        EXPECT_PIXEL_NEAR(xs, ys, data[0], data[1], data[2], data[3], 1.0);
+        EXPECT_PIXEL_NEAR(xs, ye - 1, data[0], data[1], data[2], data[3], 1.0);
+        EXPECT_PIXEL_NEAR(xe - 1, ys, data[0], data[1], data[2], data[3], 1.0);
+        EXPECT_PIXEL_NEAR(xe - 1, ye - 1, data[0], data[1], data[2], data[3], 1.0);
+        EXPECT_PIXEL_NEAR((xs + xe) / 2, (ys + ye) / 2, data[0], data[1], data[2], data[3], 1.0);
+    }
+
+    void clearAndDrawQuad(GLuint program, GLsizei viewportWidth, GLsizei viewportHeight)
+    {
+        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+        glClear(GL_COLOR_BUFFER_BIT);
+        glViewport(0, 0, viewportWidth, viewportHeight);
+        ASSERT_GL_NO_ERROR();
+
+        drawQuad(program, "position", 0.0f);
+    }
+
+    GLProgram mCopyTextureProgram;
+    GLint mCopyTextureUniformLocation = -1;
+};
+
+class MultisampledRenderToTextureES3Test : public MultisampledRenderToTextureTest
+{};
+
+// Checking against invalid parameters for RenderbufferStorageMultisampleEXT.
+TEST_P(MultisampledRenderToTextureTest, RenderbufferParameterCheck)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+
+    GLRenderbuffer renderbuffer;
+    glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
+
+    // Positive test case
+    glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, 4, GL_DEPTH_COMPONENT16, 64, 64);
+    ASSERT_GL_NO_ERROR();
+
+    GLint samples;
+    glGetIntegerv(GL_MAX_SAMPLES_EXT, &samples);
+    ASSERT_GL_NO_ERROR();
+    EXPECT_GE(samples, 1);
+
+    // Samples too large
+    glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, samples + 1, GL_DEPTH_COMPONENT16, 64, 64);
+    ASSERT_GL_ERROR(GL_INVALID_VALUE);
+
+    // Renderbuffer size too large
+    GLint maxSize;
+    glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxSize);
+    glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, 2, GL_DEPTH_COMPONENT16, maxSize + 1,
+                                        maxSize);
+    ASSERT_GL_ERROR(GL_INVALID_VALUE);
+    glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, 2, GL_DEPTH_COMPONENT16, maxSize,
+                                        maxSize + 1);
+    ASSERT_GL_ERROR(GL_INVALID_VALUE);
+
+    // Retrieving samples
+    glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, 4, GL_DEPTH_COMPONENT16, 64, 64);
+    GLint param = 0;
+    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_SAMPLES_EXT, &param);
+    // GE because samples may vary base on implementation. Spec says "the resulting value for
+    // RENDERBUFFER_SAMPLES_EXT is guaranteed to be greater than or equal to samples and no more
+    // than the next larger sample count supported by the implementation"
+    EXPECT_GE(param, 4);
+}
+
+// Checking against invalid parameters for FramebufferTexture2DMultisampleEXT.
+TEST_P(MultisampledRenderToTextureTest, Texture2DParameterCheck)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    ASSERT_GL_NO_ERROR();
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    // Positive test case
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                         texture, 0, 4);
+    ASSERT_GL_NO_ERROR();
+
+    // Attachment not COLOR_ATTACHMENT0
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D,
+                                         texture, 0, 4);
+    ASSERT_GL_ERROR(GL_INVALID_ENUM);
+
+    // Target not framebuffer
+    glFramebufferTexture2DMultisampleEXT(GL_RENDERBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                         texture, 0, 4);
+    ASSERT_GL_ERROR(GL_INVALID_ENUM);
+
+    GLint samples;
+    glGetIntegerv(GL_MAX_SAMPLES_EXT, &samples);
+    ASSERT_GL_NO_ERROR();
+    EXPECT_GE(samples, 1);
+
+    // Samples too large
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                         texture, 0, samples + 1);
+    ASSERT_GL_ERROR(GL_INVALID_VALUE);
+
+    // Retrieving samples
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                         texture, 0, 4);
+    GLint param = 0;
+    glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                          GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_SAMPLES_EXT, &param);
+    // GE because samples may vary base on implementation. Spec says "the resulting value for
+    // TEXTURE_SAMPLES_EXT is guaranteed to be greater than or equal to samples and no more than the
+    // next larger sample count supported by the implementation"
+    EXPECT_GE(param, 4);
+}
+
+// Checking against invalid parameters for FramebufferTexture2DMultisampleEXT (cubemap).
+TEST_P(MultisampledRenderToTextureTest, TextureCubeMapParameterCheck)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
+    for (GLenum face = 0; face < 6; face++)
+    {
+        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_RGBA, 64, 64, 0, GL_RGBA,
+                     GL_UNSIGNED_BYTE, nullptr);
+        ASSERT_GL_NO_ERROR();
+    }
+
+    GLint samples;
+    glGetIntegerv(GL_MAX_SAMPLES_EXT, &samples);
+    ASSERT_GL_NO_ERROR();
+    EXPECT_GE(samples, 1);
+
+    GLFramebuffer FBO;
+    glBindFramebuffer(GL_FRAMEBUFFER, FBO);
+    for (GLenum face = 0; face < 6; face++)
+    {
+        // Positive test case
+        glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                             GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, texture, 0, 4);
+        ASSERT_GL_NO_ERROR();
+
+        // Attachment not COLOR_ATTACHMENT0
+        glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1,
+                                             GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, texture, 0, 4);
+        ASSERT_GL_ERROR(GL_INVALID_ENUM);
+
+        // Target not framebuffer
+        glFramebufferTexture2DMultisampleEXT(GL_RENDERBUFFER, GL_COLOR_ATTACHMENT0,
+                                             GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, texture, 0, 4);
+        ASSERT_GL_ERROR(GL_INVALID_ENUM);
+
+        // Samples too large
+        glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                             GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, texture, 0,
+                                             samples + 1);
+        ASSERT_GL_ERROR(GL_INVALID_VALUE);
+
+        // Retrieving samples
+        glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                             GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, texture, 0, 4);
+        GLint param = 0;
+        glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                              GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_SAMPLES_EXT,
+                                              &param);
+        // GE because samples may vary base on implementation. Spec says "the resulting value for
+        // TEXTURE_SAMPLES_EXT is guaranteed to be greater than or equal to samples and no more than
+        // the next larger sample count supported by the implementation"
+        EXPECT_GE(param, 4);
+    }
+}
+
+// Checking for framebuffer completeness using extension methods.
+TEST_P(MultisampledRenderToTextureTest, FramebufferCompleteness)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+
+    // Checking that Renderbuffer and texture2d having different number of samples results
+    // in a FRAMEBUFFER_INCOMPLETE_MULTISAMPLE
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    ASSERT_GL_NO_ERROR();
+
+    GLFramebuffer FBO;
+    glBindFramebuffer(GL_FRAMEBUFFER, FBO);
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                         texture, 0, 4);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    GLRenderbuffer renderbuffer;
+    glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
+    glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, 8, GL_DEPTH_COMPONENT16, 64, 64);
+    ASSERT_GL_NO_ERROR();
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderbuffer);
+    EXPECT_GLENUM_EQ(GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE,
+                     glCheckFramebufferStatus(GL_FRAMEBUFFER));
+}
+
+// Draw test with color attachment only.
+TEST_P(MultisampledRenderToTextureTest, 2DColorAttachmentMultisampleDrawTest)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+    // Set up texture and bind to FBO
+    GLsizei size = 6;
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    ASSERT_GL_NO_ERROR();
+
+    GLFramebuffer FBO;
+    glBindFramebuffer(GL_FRAMEBUFFER, FBO);
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                         texture, 0, 4);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Set viewport and clear to black
+    glViewport(0, 0, size, size);
+    glClearColor(0.0, 0.0, 0.0, 1.0);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    // Set up Green square program
+    ANGLE_GL_PROGRAM(program, kBasicVertexShader, kGreenFragmentShader);
+    glUseProgram(program);
+    GLint positionLocation = glGetAttribLocation(program, "position");
+    ASSERT_NE(-1, positionLocation);
+
+    setupQuadVertexBuffer(0.5f, 0.5f);
+    glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 0, 0);
+    glEnableVertexAttribArray(positionLocation);
+
+    // Draw green square
+    glDrawArrays(GL_TRIANGLES, 0, 6);
+    ASSERT_GL_NO_ERROR();
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
+    EXPECT_PIXEL_COLOR_EQ(size / 2, size / 2, GLColor::green);
+
+    // Set up Red square program
+    ANGLE_GL_PROGRAM(program2, kBasicVertexShader, kRedFragmentShader);
+    glUseProgram(program2);
+    GLint positionLocation2 = glGetAttribLocation(program2, "position");
+    ASSERT_NE(-1, positionLocation2);
+
+    setupQuadVertexBuffer(0.5f, 0.75f);
+    glVertexAttribPointer(positionLocation2, 3, GL_FLOAT, GL_FALSE, 0, 0);
+
+    // Draw red square
+    glDrawArrays(GL_TRIANGLES, 0, 6);
+    ASSERT_GL_NO_ERROR();
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
+    EXPECT_PIXEL_COLOR_EQ(size / 2, size / 2, GLColor::red);
+
+    glDisableVertexAttribArray(0);
+    glBindBuffer(GL_ARRAY_BUFFER, 0);
+}
+
+// Draw test using both color and depth attachments.
+TEST_P(MultisampledRenderToTextureTest, 2DColorDepthMultisampleDrawTest)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+    GLsizei size = 6;
+    // create complete framebuffer with depth buffer
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    ASSERT_GL_NO_ERROR();
+
+    GLFramebuffer FBO;
+    glBindFramebuffer(GL_FRAMEBUFFER, FBO);
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                         texture, 0, 4);
+
+    GLRenderbuffer renderbuffer;
+    glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
+    glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, 4, GL_DEPTH_COMPONENT16, size, size);
+    ASSERT_GL_NO_ERROR();
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderbuffer);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Set viewport and clear framebuffer
+    glViewport(0, 0, size, size);
+    glClearColor(0.0, 0.0, 0.0, 1.0);
+    glClearDepthf(0.5f);
+    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+    // Draw first green square
+    ANGLE_GL_PROGRAM(program, kBasicVertexShader, kGreenFragmentShader);
+    glEnable(GL_DEPTH_TEST);
+    glDepthFunc(GL_GREATER);
+    glUseProgram(program);
+    GLint positionLocation = glGetAttribLocation(program, "position");
+    ASSERT_NE(-1, positionLocation);
+
+    setupQuadVertexBuffer(0.8f, 0.5f);
+    glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 0, 0);
+    glEnableVertexAttribArray(positionLocation);
+
+    // Tests that TRIANGLES works.
+    glDrawArrays(GL_TRIANGLES, 0, 6);
+    ASSERT_GL_NO_ERROR();
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
+    EXPECT_PIXEL_COLOR_EQ(size / 2, size / 2, GLColor::green);
+
+    // Draw red square behind green square
+    ANGLE_GL_PROGRAM(program2, kBasicVertexShader, kRedFragmentShader);
+    glUseProgram(program2);
+    GLint positionLocation2 = glGetAttribLocation(program2, "position");
+    ASSERT_NE(-1, positionLocation2);
+
+    setupQuadVertexBuffer(0.7f, 1.0f);
+    glVertexAttribPointer(positionLocation2, 3, GL_FLOAT, GL_FALSE, 0, 0);
+
+    glDrawArrays(GL_TRIANGLES, 0, 6);
+    ASSERT_GL_NO_ERROR();
+    glDisable(GL_DEPTH_TEST);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+    EXPECT_PIXEL_COLOR_EQ(size / 2, size / 2, GLColor::green);
+
+    glDisableVertexAttribArray(0);
+    glBindBuffer(GL_ARRAY_BUFFER, 0);
+}
+
+// Read pixels with pack buffer. ES3+.
+TEST_P(MultisampledRenderToTextureES3Test, MultisampleReadPixelsTest)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+
+    // PBO only available ES3 and above
+    ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3);
+    GLsizei size = 6;
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, size, size);
+    ASSERT_GL_NO_ERROR();
+
+    GLFramebuffer FBO;
+    glBindFramebuffer(GL_FRAMEBUFFER, FBO);
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                         texture, 0, 4);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Set viewport and clear to red
+    glViewport(0, 0, size, size);
+    glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    ASSERT_GL_NO_ERROR();
+
+    // Bind Pack Pixel Buffer and read to it
+    GLBuffer PBO;
+    glBindBuffer(GL_PIXEL_PACK_BUFFER, PBO);
+    glBufferData(GL_PIXEL_PACK_BUFFER, 4 * size * size, nullptr, GL_STATIC_DRAW);
+    glReadPixels(0, 0, size, size, GL_RGBA, GL_UNSIGNED_BYTE, 0);
+    ASSERT_GL_NO_ERROR();
+
+    // Retrieving pixel color
+    void *mappedPtr    = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, 32, GL_MAP_READ_BIT);
+    GLColor *dataColor = static_cast<GLColor *>(mappedPtr);
+    EXPECT_GL_NO_ERROR();
+
+    EXPECT_EQ(GLColor::red, dataColor[0]);
+
+    glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
+    EXPECT_GL_NO_ERROR();
+}
+
+// CopyTexImage from a multisampled texture functionality test.
+TEST_P(MultisampledRenderToTextureTest, MultisampleCopyTexImageTest)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+    GLsizei size = 16;
+
+    setupCopyTexProgram();
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    // Disable mipmapping
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+    GLFramebuffer FBO;
+    glBindFramebuffer(GL_FRAMEBUFFER, FBO);
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                         texture, 0, 4);
+
+    // Set color for framebuffer
+    glClearColor(0.25f, 1.0f, 0.75f, 0.5f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    ASSERT_GL_NO_ERROR();
+
+    GLTexture copyToTex;
+    glBindTexture(GL_TEXTURE_2D, copyToTex);
+
+    // Disable mipmapping
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+    glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 0, 0, size, size, 0);
+    ASSERT_GL_NO_ERROR();
+
+    GLubyte expected[4] = {64, 255, 191, 255};
+    verifyResults(copyToTex, expected, size, 0, 0, size, size);
+}
+
+// CopyTexSubImage from a multisampled texture functionality test.
+TEST_P(MultisampledRenderToTextureTest, MultisampleCopyTexSubImageTest)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+    GLsizei size = 16;
+
+    setupCopyTexProgram();
+
+    GLTexture texture;
+    // Create texture in copyFBO0 with color (.25, 1, .75, .5)
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    // Disable mipmapping
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+    GLFramebuffer copyFBO0;
+    glBindFramebuffer(GL_FRAMEBUFFER, copyFBO0);
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                         texture, 0, 4);
+
+    // Set color for
+    glClearColor(0.25f, 1.0f, 0.75f, 0.5f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    ASSERT_GL_NO_ERROR();
+
+    // Create texture in copyFBO[1] with color (1, .75, .5, .25)
+    GLTexture texture1;
+    glBindTexture(GL_TEXTURE_2D, texture1);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    // Disable mipmapping
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+    GLFramebuffer copyFBO1;
+    glBindFramebuffer(GL_FRAMEBUFFER, copyFBO1);
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                         texture1, 0, 4);
+
+    // Set color for
+    glClearColor(1.0f, 0.75f, 0.5f, 0.25f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    ASSERT_GL_NO_ERROR();
+
+    GLTexture copyToTex;
+    glBindTexture(GL_TEXTURE_2D, copyToTex);
+
+    // Disable mipmapping
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+    // copyFBO0 -> copyToTex
+    // copyToTex should hold what was originally in copyFBO0 : (.25, 1, .75, .5)
+    glBindFramebuffer(GL_FRAMEBUFFER, copyFBO0);
+    glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 0, 0, size, size, 0);
+    ASSERT_GL_NO_ERROR();
+
+    GLubyte expected0[4] = {64, 255, 191, 255};
+    verifyResults(copyToTex, expected0, size, 0, 0, size, size);
+
+    // copyFBO[1] - copySubImage -> copyToTex
+    // copyToTex should have subportion what was in copyFBO[1] : (1, .75, .5, .25)
+    // The rest should still be untouched: (.25, 1, .75, .5)
+    GLint half = size / 2;
+    glBindFramebuffer(GL_FRAMEBUFFER, copyFBO1);
+    glCopyTexSubImage2D(GL_TEXTURE_2D, 0, half, half, half, half, half, half);
+    ASSERT_GL_NO_ERROR();
+
+    GLubyte expected1[4] = {255, 191, 127, 255};
+    verifyResults(copyToTex, expected1, size, half, half, size, size);
+
+    // Verify rest is untouched
+    verifyResults(copyToTex, expected0, size, 0, 0, half, half);
+    verifyResults(copyToTex, expected0, size, 0, half, half, size);
+    verifyResults(copyToTex, expected0, size, half, 0, size, half);
+}
+
+// BlitFramebuffer functionality test. ES3+.
+TEST_P(MultisampledRenderToTextureES3Test, MultisampleBlitFramebufferTest)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+    // blitFramebuffer only available ES3 and above
+    ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3);
+
+    GLsizei size = 16;
+
+    // Create multisampled framebuffer to use as source.
+    GLRenderbuffer depthMS;
+    glBindRenderbuffer(GL_RENDERBUFFER, depthMS.get());
+    glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, 4, GL_DEPTH_COMPONENT24, size, size);
+
+    GLTexture colorMS;
+    glBindTexture(GL_TEXTURE_2D, colorMS);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    GLFramebuffer fboMS;
+    glBindFramebuffer(GL_FRAMEBUFFER, fboMS);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthMS.get());
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                         colorMS, 0, 4);
+    ASSERT_GL_NO_ERROR();
+
+    // Clear depth to 0.5 and color to green.
+    glClearDepthf(0.5f);
+    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
+    glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
+    glFlush();
+    ASSERT_GL_NO_ERROR();
+
+    // Draw red into the multisampled color buffer.
+    ANGLE_GL_PROGRAM(drawRed, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+    glEnable(GL_DEPTH_TEST);
+    glDepthFunc(GL_EQUAL);
+    drawQuad(drawRed.get(), essl1_shaders::PositionAttrib(), 0.0f);
+    ASSERT_GL_NO_ERROR();
+
+    // Create single sampled framebuffer to use as dest.
+    GLFramebuffer fboSS;
+    glBindFramebuffer(GL_FRAMEBUFFER, fboSS);
+    GLTexture colorSS;
+    glBindTexture(GL_TEXTURE_2D, colorSS);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorSS, 0);
+    ASSERT_GL_NO_ERROR();
+
+    // Bind MS to READ as SS is already bound to DRAW.
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, fboMS.get());
+    glBlitFramebuffer(0, 0, size, size, 0, 0, size, size, GL_COLOR_BUFFER_BIT, GL_NEAREST);
+    ASSERT_GL_NO_ERROR();
+
+    // Bind SS to READ so we can readPixels from it
+    glBindFramebuffer(GL_FRAMEBUFFER, fboSS.get());
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+    EXPECT_PIXEL_COLOR_EQ(size - 1, 0, GLColor::red);
+    EXPECT_PIXEL_COLOR_EQ(0, size - 1, GLColor::red);
+    EXPECT_PIXEL_COLOR_EQ(size - 1, size - 1, GLColor::red);
+    EXPECT_PIXEL_COLOR_EQ(size / 2, size / 2, GLColor::red);
+    ASSERT_GL_NO_ERROR();
+}
+
+// GenerateMipmap functionality test
+TEST_P(MultisampledRenderToTextureTest, MultisampleGenerateMipmapTest)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+    GLsizei size = 64;
+    // Vertex Shader source
+    constexpr char kVS[] = R"(attribute vec4 position;
+varying vec2 vTexCoord;
+
+void main()
+{
+    gl_Position = position;
+    vTexCoord   = (position.xy * 0.5) + 0.5;
+})";
+
+    // Fragment Shader source
+    constexpr char kFS[] = R"(precision mediump float;
+uniform sampler2D uTexture;
+varying vec2 vTexCoord;
+
+void main()
+{
+    gl_FragColor = texture2D(uTexture, vTexCoord);
+})";
+
+    GLProgram m2DProgram;
+    m2DProgram.makeRaster(kVS, kFS);
+    ASSERT_GL_TRUE(m2DProgram.valid());
+
+    ASSERT_GL_NO_ERROR();
+
+    // Initialize texture with blue
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, size, size, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+
+    GLFramebuffer FBO;
+    glBindFramebuffer(GL_FRAMEBUFFER, FBO);
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                         texture, 0, 4);
+    ASSERT_GLENUM_EQ(GL_FRAMEBUFFER_COMPLETE, glCheckFramebufferStatus(GL_FRAMEBUFFER));
+    glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    glViewport(0, 0, size, size);
+    glBindFramebuffer(GL_FRAMEBUFFER, 0);
+    ASSERT_GL_NO_ERROR();
+
+    // Generate mipmap
+    glGenerateMipmap(GL_TEXTURE_2D);
+    ASSERT_GL_NO_ERROR();
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
+
+    // Now draw the texture to various different sized areas.
+    clearAndDrawQuad(m2DProgram, size, size);
+    EXPECT_PIXEL_COLOR_EQ(size / 2, size / 2, GLColor::blue);
+
+    // Use mip level 1
+    clearAndDrawQuad(m2DProgram, size / 2, size / 2);
+    EXPECT_PIXEL_COLOR_EQ(size / 4, size / 4, GLColor::blue);
+
+    // Use mip level 2
+    clearAndDrawQuad(m2DProgram, size / 4, size / 4);
+    EXPECT_PIXEL_COLOR_EQ(size / 8, size / 8, GLColor::blue);
+
+    ASSERT_GL_NO_ERROR();
+}
+ANGLE_INSTANTIATE_TEST(MultisampledRenderToTextureTest,
+                       ES2_D3D9(),
+                       ES2_D3D11(),
+                       ES3_D3D11(),
+                       ES2_OPENGL(),
+                       ES3_OPENGL(),
+                       ES2_OPENGLES(),
+                       ES3_OPENGLES(),
+                       ES2_VULKAN(),
+                       ES3_VULKAN());
+ANGLE_INSTANTIATE_TEST(MultisampledRenderToTextureES3Test,
+                       ES3_D3D11(),
+                       ES3_OPENGL(),
+                       ES3_OPENGLES(),
+                       ES3_VULKAN());
+}  // namespace