diff --git a/extensions/proposals/WEBGL_multisampled_render_to_texture/extension.xml b/extensions/proposals/WEBGL_multisampled_render_to_texture/extension.xml
new file mode 100644
index 0000000000..0ac5f8c550
--- /dev/null
+++ b/extensions/proposals/WEBGL_multisampled_render_to_texture/extension.xml
@@ -0,0 +1,160 @@
+<?xml version="1.0"?>
+
+<extension href="WEBGL_multisampled_render_to_texture/">
+  <name>WEBGL_multisampled_render_to_texture</name>
+  <contact>
+    <a href="https://www.khronos.org/webgl/public-mailing-list/">WebGL working group</a> (public_webgl 'at' khronos.org)
+  </contact>
+  <contributors>
+    <contributor>Rik Cabanier, Facebook</contributor>
+  </contributors>
+  <number>??</number>
+  <depends>
+    <api version="1.0"/>
+  </depends>
+  <overview>
+    <mirrors href="https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_multisampled_render_to_texture2.txt" name="EXT_multisampled_render_to_texture2">
+    </mirrors>
+    <features>
+      <feature>
+        Adds support for rendering multisampled to a color renderable texture, without requiring an explicit resolve of multisample data.
+      </feature>
+    </features>
+  </overview>
+  <idl xml:space="preserve">
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_multisampled_render_to_texture {
+      const GLenum RENDERBUFFER_SAMPLES_EXT = 0x8CAB;
+      const GLenum FRAMEBUFFER_INCOMPLETE_MULTISAMPLE_EXT = 0x8D56;
+      const GLenum MAX_SAMPLES_EXT = 0x8D57;
+      const GLenum FRAMEBUFFER_ATTACHMENT_TEXTURE_SAMPLES_EXT = 0x8D6C;
+
+      undefined renderbufferStorageMultisampleEXT(
+            GLenum target, GLsizei samples,
+            GLenum internalformat,
+            GLsizei width, GLsizei height);
+
+      undefined framebufferTexture2DMultisampleEXT(
+            GLenum target, GLenum attachment,
+            GLenum textarget, WebGLTexture? texture,
+            GLint level, GLsizei samples);
+};
+  </idl>
+  <newfun>
+    <function name="renderbufferStorageMultisampleEXT" type="undefined">
+      <param name="target" type="GLenum"/>
+      <param name="samples" type="GLsizei"/>
+      <param name="internalformat" type="GLenum"/>
+      <param name="width" type="GLsizei"/>
+      <param name="height" type="GLsizei"/>
+    </function>
+  </newfun>
+  <newfun>
+    <function name="framebufferTexture2DMultisampleEXT" type="undefined">
+      <param name="target" type="GLenum"/>
+      <param name="attachment" type="GLenum"/>
+      <param name="textarget" type="GLenum"/>
+      <param name="texture" type="GLuint"/>
+      <param name="level" type="GLint"/>
+      <param name="samples" type="GLsizei"/>
+    </function>
+  </newfun>
+  <newtok>
+    <function name="GetRenderbufferParameter" type="any">
+      <param name="target" type="GLenum"/>
+      <param name="pname" type="GLenum"/>
+      Calling with the <code>pname</code> set to <code>RENDERBUFFER_SAMPLES_EXT</code> returns the number of samples.
+      <br/>
+<!--
+        If <code>renderbufferStorageMultisampleEXT</code> is called with <code>samples</code> is zero, then <code>RENDERBUFFER_SAMPLES_EXT</code> is set to zero.
+        Otherwise <code>samples</code> represents a request for a desired minimum number
+        of samples. Since different implementations may support different
+        sample counts for multisampled rendering, the actual number of samples
+        allocated for the renderbuffer image is implementation-dependent.
+        However, the resulting value for <code>RENDERBUFFER_SAMPLES_EXT</code> is
+        guaranteed to be greater than or equal to samples and no more than the
+        next larger sample count supported by the implementation.
+      <br/>
+-->
+      The return type depends on the parameter queried:
+      <table width="30%">
+      <tr><th>pname</th><th>returned type</th></tr>
+      <tr><td>RENDERBUFFER_SAMPLES_EXT</td><td>GLint</td></tr>
+      </table>
+    </function>
+    <function name="checkFramebufferStatus" type="GLenum">
+      <param name="target" type="GLenum"/>
+    </function>
+    <function name="getParameter" type="any">
+      <param name="pname" type="GLenum"/>
+      Calling with the <code>pname</code> set to <code>MAX_SAMPLES_EXT</code> returns the maximum number of samples.
+      <br/>
+
+      The return type depends on the parameter queried:
+      <table width="30%">
+      <tr><th>pname</th><th>returned type</th></tr>
+      <tr><td>MAX_SAMPLES_EXT</td><td>GLint</td></tr>
+      </table>
+    </function>
+    <function name="getFramebufferAttachmentParameter" type="any">
+      <param name="target" type="GLenum"/>
+      <param name="attachment" type="GLenum"/>
+      <param name="pname" type="GLenum"/>
+      Calling with the <code>pname</code> set to <code>FRAMEBUFFER_ATTACHMENT_TEXTURE_SAMPLES_EXT</code> returns the number of samples in the multisampled texture.
+      <br/>
+      The return type depends on the parameter queried:
+      <table width="30%">
+      <tr><th>pname</th><th>returned type</th></tr>
+      <tr><td>MAX_SAMPLES_EXT</td><td>GLint</td></tr>
+      </table>
+    </function>
+  </newtok>
+  <errors>
+    <error>
+      The error <code>OUT_OF_MEMORY</code> is generated when <code>RenderbufferStorageMultisampleEXT</code> cannot create storage of the specified size.
+    </error>
+    <error>
+      If <code>RenderbufferStorageMultisampleEXT</code> is called with a value of <code>samples</code> that is greater than <code>MAX_SAMPLES_EXT</code>, then the error <code>INVALID_VALUE</code> is generated.
+    </error>
+    <error>
+      The error <code>INVALID_ENUM</code> is generated if<code> FramebufferTexture2DMultisampleEXT</code> is called with a target that is not <code>FRAMEBUFFER</code>, <code>DRAW_FRAMEBUFFER</code>, or <code>READ_FRAMEBUFFER</code>.
+    </error>
+    <error>
+      The error <code>INVALID_ENUM</code> is generated if <code>FramebufferTexture2DMultisampleEXT</code> is called with an <code>attachment</code> that is not <code>COLOR_ATTACHMENT0</code>.
+    </error>
+    <error>
+      The error <code>INVALID_ENUM</code> is generated if <code>FramebufferTexture2DMultisampleEXT</code> is called with a textarget that is not <code>TEXTURE_2D</code>, <code>TEXTURE_CUBE_MAP_POSITIVE_X</code>, <code>TEXTURE_CUBE_MAP_POSITIVE_Y</code>, <code>TEXTURE_CUBE_MAP_POSITIVE_Z</code>, <code>TEXTURE_CUBE_MAP_NEGATIVE_X</code>, <code>TEXTURE_CUBE_MAP_NEGATIVE_Y</code>, or <code>TEXTURE_CUBE_MAP_NEGATIVE_Z</code>.
+    </error>
+    <error>
+      The error <code>INVALID_OPERATION</code> is generated if <code>FramebufferTexture2DMultisampleEXT</code> is called with samples greater than the maximum number of samples supported for target and its internalformat.
+    </error>
+  </errors>
+  <samplecode xml:space="preserve">
+    <pre>
+    var gl = document.createElement('canvas').getContext('webgl2');
+    var ext = gl.getExtension('WEBGL_multisampled_render_to_texture');
+    var samples = gl.getParameter(ext.MAX_SAMPLES_EXT);
+    var fb = gl.createFramebuffer();
+    gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, fb);
+
+    // Create color texture and storage.
+    var colorTex = gl.createTexture();
+    gl.bindTexture(gl.TEXTURE_2D, colorTex);
+    gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, 512, 512);
+    ext.framebufferTexture2DMultisampleEXT(gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTex, 0, samples);
+
+    // Create depth texture and storage.
+    var depthStencilTex = gl.createTexture();
+    gl.bindTexture(gl.TEXTURE_2D, depthStencilTex);
+    ext.renderbufferStorageMultisampleEXT(gl.RENDER_BUFFER, samples, gl.DEPTH32F_STENCIL8, 512, 512);
+    ext.framebufferTexture2DMultisampleEXT(gl.DRAW_FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.TEXTURE_2D, depthStencilTex, 0, samples);
+
+    gl.drawElements(...);  // Draw will be done with multisampling and flushed to the non-multisample texture.
+    </pre>
+  </samplecode>
+  <history>
+    <revision date="2021/10/09">
+      <change>Initial revision.</change>
+    </revision>
+  </history>
+</extension>
diff --git a/sdk/tests/conformance2/extensions/webgl_multisampled_render_to_texture.html b/sdk/tests/conformance2/extensions/webgl_multisampled_render_to_texture.html
new file mode 100644
index 0000000000..b16319b40c
--- /dev/null
+++ b/sdk/tests/conformance2/extensions/webgl_multisampled_render_to_texture.html
@@ -0,0 +1,181 @@
+<!--
+Copyright (c) 2019 The Khronos Group Inc.
+Use of this source code is governed by an MIT-style license that can be
+found in the LICENSE.txt file.
+-->
+
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>WebGL OVR_multiview2 Conformance Tests</title>
+<link rel="stylesheet" href="../../resources/js-test-style.css"/>
+<script src="../../js/js-test-pre.js"></script>
+<script src="../../js/webgl-test-utils.js"></script>
+</head>
+<body>
+<div id="description"></div>
+<div id="console"></div>
+<script>
+"use strict";
+
+let wtu = WebGLTestUtils;
+let gl = wtu.create3DContext(null, null, 2);
+let ext = null;
+
+function runMultisampledTest() {
+    debug("");
+    debug("Testing that color is written as multisampled");
+    let program = wtu.setupColorQuad(gl, 0);
+    gl.viewport(0, 0, 2, 2);
+    let color = [0, 1.0, 0, 1.0];
+
+    let fb = gl.createFramebuffer();
+    gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
+
+    let colorTex = gl.createTexture();
+    gl.bindTexture(gl.TEXTURE_2D, colorTex);
+    gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, 1, 1);
+    ext.framebufferTexture2DMultisampleEXT(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTex, 0, 4);
+
+    gl.clearColor(1, 1, 1, 1);
+    gl.clear(gl.COLOR_BUFFER_BIT);
+    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors from clear");
+
+    gl.useProgram(program);
+    wtu.drawFloatColorQuad(gl, color);
+    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors from drawFloatColorQuad");
+
+    gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTex, 0);
+    let buf = new Uint8Array(4);
+    gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, buf);
+    if (buf[0] != 0 ||
+        buf[1] != 255 ||
+        buf[2] != 0 ||
+        buf[3] != 255) {
+        testFailed("Pixel at should have been (0, 255, 0, 255), " +
+                 "was (" + buf[0] + ", " + buf[1] + ", " + buf[2] + ", " + buf[3] + ")");
+    } else
+        testPassed("runMultisampledTest with full green rgb");
+
+    gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
+    let colorTex_partial = gl.createTexture();
+    gl.bindTexture(gl.TEXTURE_2D, colorTex_partial);
+    gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, 1, 1);
+    ext.framebufferTexture2DMultisampleEXT(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTex_partial, 0, 4);
+
+    gl.clearColor(1, 1, 1, 1);
+    gl.clear(gl.COLOR_BUFFER_BIT);
+    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors from clear");
+
+    program = wtu.setupColorQuad(gl, 0, { scale: 0.5 });
+    gl.useProgram(program);
+    wtu.drawFloatColorQuad(gl, color);
+    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors from drawFloatColorQuad");
+
+    gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTex_partial, 0);
+    gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, buf);
+    if (buf[0] != 191 ||
+        buf[1] != 255 ||
+        buf[2] != 191 ||
+        buf[3] != 255) {
+        testFailed("Pixel at should have been (191, 255, 191, 255), " +
+                 "was (" + buf[0] + ", " + buf[1] + ", " + buf[2] + ", " + buf[3] + ")");
+    } else
+        testPassed("runMultisampledTest with partial green rgb");
+
+    let colorTex_srgb = gl.createTexture();
+    gl.bindTexture(gl.TEXTURE_2D, colorTex_srgb);
+    gl.texStorage2D(gl.TEXTURE_2D, 1, gl.SRGB8_ALPHA8, 1, 1);
+    ext.framebufferTexture2DMultisampleEXT(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTex_srgb, 0, 4);
+
+    gl.clearColor(0, 0, 0, 0);
+    gl.clear(gl.COLOR_BUFFER_BIT);
+    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors from clear");
+
+    gl.useProgram(program);
+    wtu.drawFloatColorQuad(gl, color);
+    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors from drawFloatColorQuad");
+
+    gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTex_srgb, 0);
+    gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, buf);
+    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors from readpixels");
+    if (buf[0] != 0 ||
+        buf[1] != 63 ||
+        buf[2] != 0 ||
+        buf[3] != 63) {
+        testFailed("Pixel at should have been (0, 63, 0, 63), " +
+                 "was (" + buf[0] + ", " + buf[1] + ", " + buf[2] + ", " + buf[3] + ")");
+    } else
+        testPassed("runMultisampledTest with partial srgb green");
+
+    debug("");
+}
+
+function runDrawBuffersClearTest()
+{
+    debug("Testing that calling clear() clears all views in all draw buffers");
+
+    let width = 256;
+    let height = 256;
+
+    let samples = gl.getParameter(ext.MAX_SAMPLES_EXT);
+
+    let fb = gl.createFramebuffer();
+    gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
+    let colorTex = [null, null, null];
+    let drawBuffers = [0, 0, 0];
+    for (let texIndex = 0; texIndex < colorTex.length; ++texIndex) {
+        colorTex[texIndex] = gl.createTexture();
+        gl.bindTexture(gl.TEXTURE_2D, colorTex[texIndex]);
+        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, width, height);
+        ext.framebufferTexture2DMultisampleEXT(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + texIndex, gl.TEXTURE_2D, colorTex[texIndex], 0, samples);
+        drawBuffers[texIndex] = gl.COLOR_ATTACHMENT0 + texIndex;
+    }
+
+    gl.viewport(0, 0, width, height);
+    gl.drawBuffers(drawBuffers);
+
+    gl.clearColor(0, 1, 1, 1);
+    gl.clear(gl.COLOR_BUFFER_BIT);
+    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors from clear");
+
+    let readFb = gl.createFramebuffer();
+    gl.bindFramebuffer(gl.READ_FRAMEBUFFER, readFb);
+    for (let texIndex = 0; texIndex < colorTex.length; ++texIndex) {
+        gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTex[texIndex], 0);
+        let expectedColor = [0, 255, 255, 255];
+        wtu.checkCanvasRect(gl, 0, 0, width, height, expectedColor, 'color attachment ' + texIndex + ' should be cyan');
+    }
+
+    testPassed("runDrawBuffersClearTest");
+}
+
+description("This test verifies the functionality of the WEBGL_multisampled_render_to_texture extension, if it is available.");
+
+debug("");
+
+if (!gl) {
+  testFailed("WebGL context does not exist");
+} else {
+  testPassed("WebGL context exists");
+
+  if (!gl.getExtension("WEBGL_multisampled_render_to_texture")) {
+      testPassed("No WEBGL_multisampled_render_to_texture support -- this is legal");
+  } else {
+      testPassed("Successfully enabled WEBGL_multisampled_render_to_texture extension");
+      ext = gl.getExtension('WEBGL_multisampled_render_to_texture');
+
+      runMultisampledTest();
+
+      runDrawBuffersClearTest();
+  }
+}
+
+debug("");
+var successfullyParsed = true;
+</script>
+<script src="../../js/js-test-post.js"></script>
+
+</body>
+</html>