High chrome Memory Corruption 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in ANGLE
DescriptionMemory corruption in ANGLE
ComponentANGLE
Sub-componentD3D11
Bug ClassMemory Corruption
Tracker563355257
Fix commitb5bab166e6a7 (angle/angle) +316/-61
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Files Changed

  • src/tests/gl_tests/CopyTexImageTest.cpp
  • src/libANGLE/renderer/d3d/TextureD3D.cpp
  • src/libANGLE/renderer/d3d/TextureD3D.h
From b5bab166e6a7c94bd545babe61c3d9b82c56de3a Mon Sep 17 00:00:00 2001
From: Ran Wang <wangra@google.com>
Date: Wed, 23 Sep 2026 20:42:06 -0400
Subject: [PATCH] D3D11: Sync image from storage before CPU CopyTexSubImage

Check getBaseLevel() in canCreateRenderTargetForImage and sync clean
staging images from mTexStorage via getImageAndSyncFromStorageIfNeeded
before copyFromFramebuffer so partial copies preserve existing storage
contents.

Test:
    angle_end2end_tests --gtest_filter="*CopyTexImageTestES3.*Preserves**"
Bug: b/563355257
Change-Id: I3ad952bbd73af45e6938e43c9e1e31ce8d67437b
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8452764
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Yuxin Hu <yuxinhu@google.com>
Commit-Queue: Ran Wang <wangra@google.com>
---
diff --git a/src/libANGLE/renderer/d3d/TextureD3D.cpp b/src/libANGLE/renderer/d3d/TextureD3D.cpp
index ba6813e..4496850 100644
--- a/src/libANGLE/renderer/d3d/TextureD3D.cpp
+++ b/src/libANGLE/renderer/d3d/TextureD3D.cpp
@@ -155,7 +155,7 @@
     }
 
     ImageD3D *destImage = nullptr;
-    ANGLE_TRY(getImageAndSyncFromStorage(context, destIndex, &destImage));
+    ANGLE_TRY(getImageAndSyncFromStorageIfNeeded(context, destIndex, &destImage));
 
     const gl::Box sourceBox(0, 0, 0, clippedArea.width, clippedArea.height, 1);
     ANGLE_TRY(mRenderer->copyImage(context, destImage, sourceCopy.get(), sourceBox, destOffset,
@@ -184,13 +184,18 @@
     return angle::Result::Continue;
 }
 
-angle::Result TextureD3D::getImageAndSyncFromStorage(const gl::Context *context,
-                                                     const gl::ImageIndex &index,
-                                                     ImageD3D **outImage)
+angle::Result TextureD3D::getImageAndSyncFromStorageIfNeeded(const gl::Context *context,
+                                                             const gl::ImageIndex &index,
+                                                             ImageD3D **outImage)
 {
     ImageD3D *image = getImage(index);
-    if (mTexStorage && mTexStorage->isRenderTarget() && isValidIndex(index) &&
-        isImageComplete(index))
+    // Syncing from mTexStorage is needed only when mTexStorage exists/is complete and
+    // !image->isDirty(). In TextureD3D, !image->isDirty() means mTexStorage holds the latest
+    // authoritative data (e.g. from FBO rendering/clears or direct TextureStorage11::setData
+    // uploads) while the CPU staging buffer does not. Conversely, when image->isDirty() is true,
+    // the CPU staging image already holds newer uncommitted data, so we must not overwrite it from
+    // mTexStorage (and we defer flushing staging to mTexStorage until commitRegion/updateStorage).
+    if (mTexStorage && isValidIndex(index) && isImageComplete(index) && !image->isDirty())
     {
         ANGLE_TRY(image->copyFromTexStorage(context, index, mTexStorage));
         image->markClean();
@@ -870,10 +875,42 @@
 
     ImageD3D *image = getImage(index);
     ASSERT(image);
-    bool levelsComplete = (isImageComplete(index) && isImageComplete(getImageIndex(0, 0)));
+    bool levelsComplete =
+        (isImageComplete(index) && isImageComplete(getImageIndex(getBaseLevel(), 0)));
     return (image->isRenderableFormat() && levelsComplete);
 }
 
+angle::Result TextureD3D::copyImageFromFramebufferToStaging(const gl::Context *context,
+                                                            const gl::ImageIndex &index,
+                                                            const gl::Offset &destOffset,
+                                                            const gl::Rectangle &clippedArea,
+                                                            gl::Framebuffer *source)
+{
+    ASSERT(!canCreateRenderTargetForImage(index));
+    // canCreateRenderTargetForImage(index) checks isRenderableFormat(), isImageComplete(index),
+    // and base-level completeness. Because all formats valid for CopyTexImage/CopyTexSubImage are
+    // renderable and isImageComplete(index) implies the base level is also complete,
+    // !canCreateRenderTargetForImage(index) means !isImageComplete(index).
+    //
+    // Since mTexStorage only stores mip-complete levels (and updateStorage() skips incomplete
+    // levels), mTexStorage never holds newer data for an incomplete index, so no sync from
+    // mTexStorage to the staging image is needed.
+    //
+    // onStateChange(DirtyBitsFlagged) is not needed here: CopyTexImage already signals
+    // SubjectChanged via signalDirtyStorage(), and for CopyTexSubImage an incomplete level cannot
+    // be uploaded to mTexStorage or sampled until a subsequent level redefinition or texture
+    // parameter change (e.g. GL_TEXTURE_BASE_LEVEL) makes it complete, which itself signals
+    // SubjectChanged.
+    ASSERT(!isImageComplete(index));
+
+    ImageD3D *image = getImage(index);
+    ASSERT(image);
+    gl::Offset stagingOffset(destOffset.x, destOffset.y, 0);
+    ANGLE_TRY(image->copyFromFramebuffer(context, stagingOffset, clippedArea, source));
+    mDirtyImages = true;
+    return angle::Result::Continue;
+}
+
 angle::Result TextureD3D::commitRegion(const gl::Context *context,
                                        const gl::ImageIndex &index,
                                        const gl::Box &region)
@@ -1414,9 +1451,8 @@
 
     if (!canCreateRenderTargetForImage(index))
     {
-        ANGLE_TRY(mImageArray[index.getLevelIndex()]->copyFromFramebuffer(context, destOffset,
-                                                                          clippedArea, source));
-        mDirtyImages = true;
+        ANGLE_TRY(
+            copyImageFromFramebufferToStaging(context, index, destOffset, clippedArea, source));
     }
     else
     {
@@ -1452,16 +1488,14 @@
     const gl::Offset clippedOffset(destOffset.x + clippedArea.x - sourceArea.x,
                                    destOffset.y + clippedArea.y - sourceArea.y, 0);
 
-    // can only make our texture storage to a render target if level 0 is defined (with a width &
-    // height) and the current level we're copying to is defined (with appropriate format, width &
-    // height)
+    // can only make our texture storage to a render target if the base level is defined (with a
+    // width & height) and the current level we're copying to is defined (with appropriate format,
+    // width & height)
 
     if (!canCreateRenderTargetForImage(index))
     {
-        ANGLE_TRY(mImageArray[index.getLevelIndex()]->copyFromFramebuffer(context, clippedOffset,
-                                                                          clippedArea, source));
-        mDirtyImages = true;
-        onStateChange(angle::SubjectMessage::DirtyBitsFlagged);
+        ANGLE_TRY(
+            copyImageFromFramebufferToStaging(context, index, clippedOffset, clippedArea, source));
     }
     else
     {
@@ -1522,10 +1556,11 @@
         gl::ImageIndex sourceImageIndex = gl::ImageIndex::Make2D(sourceLevel.get());
         TextureD3D *sourceD3D           = GetImplAs<TextureD3D>(source);
         ImageD3D *sourceImage           = nullptr;
-        ANGLE_TRY(sourceD3D->getImageAndSyncFromStorage(context, sourceImageIndex, &sourceImage));
+        ANGLE_TRY(
+            sourceD3D->getImageAndSyncFromStorageIfNeeded(context, sourceImageIndex, &sourceImage));
 
         ImageD3D *destImage = nullptr;
-        ANGLE_TRY(getImageAndSyncFromStorage(context, index, &destImage));
+        ANGLE_TRY(getImageAndSyncFromStorageIfNeeded(context, index, &destImage));
 
         ANGLE_TRY(mRenderer->copyImage(context, destImage, sourceImage, sourceBox, destOffset,
                                        unpackFlipY, unpackPremultiplyAlpha, unpackUnmultiplyAlpha));
@@ -1570,10 +1605,11 @@
         gl::ImageIndex sourceImageIndex = gl::ImageIndex::Make2D(sourceLevel.get());
         TextureD3D *sourceD3D           = GetImplAs<TextureD3D>(source);
         ImageD3D *sourceImage           = nullptr;
-        ANGLE_TRY(sourceD3D->getImageAndSyncFromStorage(context, sourceImageIndex, &sourceImage));
+        ANGLE_TRY(
+            sourceD3D->getImageAndSyncFromStorageIfNeeded(context, sourceImageIndex, &sourceImage));
 
         ImageD3D *destImage = nullptr;
-        ANGLE_TRY(getImageAndSyncFromStorage(context, index, &destImage));
+        ANGLE_TRY(getImageAndSyncFromStorageIfNeeded(context, index, &destImage));
 
         ANGLE_TRY(mRenderer->copyImage(context, destImage, sourceImage, sourceBox, destOffset,
                                        unpackFlipY, unpackPremultiplyAlpha, unpackUnmultiplyAlpha));
@@ -2140,10 +2176,8 @@
 
     if (!canCreateRenderTargetForImage(index))
     {
-        ANGLE_TRY(mImageArray[faceIndex][index.getLevelIndex()]->copyFromFramebuffer(
-            context, destOffset, clippedArea, source));
-        mDirtyImages = true;
-        onStateChange(angle::SubjectMessage::DirtyBitsFlagged);
+        ANGLE_TRY(
+            copyImageFromFramebufferToStaging(context, index, destOffset, clippedArea, source));
     }
     else
     {
@@ -2182,10 +2216,8 @@
 
     if (!canCreateRenderTargetForImage(index))
     {
-        ANGLE_TRY(mImageArray[faceIndex][index.getLevelIndex()]->copyFromFramebuffer(
-            context, clippedOffset, clippedArea, source));
-        mDirtyImages = true;
-        onStateChange(angle::SubjectMessage::DirtyBitsFlagged);
+        ANGLE_TRY(
+            copyImageFromFramebufferToStaging(context, index, clippedOffset, clippedArea, source));
     }
     else
     {
@@ -2245,10 +2277,11 @@
         gl::ImageIndex sourceImageIndex = gl::ImageIndex::Make2D(sourceLevel.get());
         TextureD3D *sourceD3D           = GetImplAs<TextureD3D>(source);
         ImageD3D *sourceImage           = nullptr;
-        ANGLE_TRY(sourceD3D->getImageAndSyncFromStorage(context, sourceImageIndex, &sourceImage));
+        ANGLE_TRY(
+            sourceD3D->getImageAndSyncFromStorageIfNeeded(context, sourceImageIndex, &sourceImage));
 
         ImageD3D *destImage = nullptr;
-        ANGLE_TRY(getImageAndSyncFromStorage(context, index, &destImage));
+        ANGLE_TRY(getImageAndSyncFromStorageIfNeeded(context, index, &destImage));
 
         ANGLE_TRY(mRenderer->copyImage(context, destImage, sourceImage, sourceBox, destOffset,
                                        unpackFlipY, unpackPremultiplyAlpha, unpackUnmultiplyAlpha));
@@ -2295,10 +2328,11 @@
         gl::ImageIndex sourceImageIndex = gl::ImageIndex::Make2D(sourceLevel.get());
         TextureD3D *sourceD3D           = GetImplAs<TextureD3D>(source);
         ImageD3D *sourceImage           = nullptr;
-        ANGLE_TRY(sourceD3D->getImageAndSyncFromStorage(context, sourceImageIndex, &sourceImage));
+        ANGLE_TRY(
+            sourceD3D->getImageAndSyncFromStorageIfNeeded(context, sourceImageIndex, &sourceImage));
 
         ImageD3D *destImage = nullptr;
-        ANGLE_TRY(getImageAndSyncFromStorage(context, index, &destImage));
+        ANGLE_TRY(getImageAndSyncFromStorageIfNeeded(context, index, &destImage));
 
         ANGLE_TRY(mRenderer->copyImage(context, destImage, sourceImage, sourceBox, destOffset,
                                        unpackFlipY, unpackPremultiplyAlpha, unpackUnmultiplyAlpha));
@@ -2877,18 +2911,14 @@
     // date before the copy and then copy back to the storage afterwards if needed.
     // TODO: Investigate 3D blits in D3D11.
 
-    bool syncTexStorage = mTexStorage && isLevelComplete(index.getLevelIndex());
-    if (syncTexStorage)
-    {
-        ANGLE_TRY(
-            mImageArray[index.getLevelIndex()]->copyFromTexStorage(context, index, mTexStorage));
-    }
-    ANGLE_TRY(mImageArray[index.getLevelIndex()]->copyFromFramebuffer(context, clippedDestOffset,
-                                                                      clippedSourceArea, source));
+    gl::ImageIndex levelIndex = gl::ImageIndex::Make3D(index.getLevelIndex());
+    ImageD3D *image           = nullptr;
+    ANGLE_TRY(getImageAndSyncFromStorageIfNeeded(context, levelIndex, &image));
+    ANGLE_TRY(image->copyFromFramebuffer(context, clippedDestOffset, clippedSourceArea, source));
     mDirtyImages = true;
     onStateChange(angle::SubjectMessage::DirtyBitsFlagged);
 
-    if (syncTexStorage)
+    if (mTexStorage && isLevelComplete(index.getLevelIndex()))
     {
         ANGLE_TRY(updateStorageLevel(context, index.getLevelIndex()));
     }
@@ -2944,8 +2974,9 @@
         ImageD3D *destImage        = nullptr;
         TextureD3D *sourceD3D      = GetImplAs<TextureD3D>(source);
 
-        ANGLE_TRY(getImageAndSyncFromStorage(context, destIndex, &destImage));
-        ANGLE_TRY(sourceD3D->getImageAndSyncFromStorage(context, sourceIndex, &sourceImage));
+        ANGLE_TRY(getImageAndSyncFromStorageIfNeeded(context, destIndex, &destImage));
+        ANGLE_TRY(
+            sourceD3D->getImageAndSyncFromStorageIfNeeded(context, sourceIndex, &sourceImage));
 
         ANGLE_TRY(mRenderer->copyImage(context, destImage, sourceImage, sourceBox, destOffset,
                                        unpackFlipY, unpackPremultiplyAlpha, unpackUnmultiplyAlpha));
@@ -2991,10 +3022,11 @@
         gl::ImageIndex sourceImageIndex = gl::ImageIndex::Make3D(sourceLevel.get());
         TextureD3D *sourceD3D           = GetImplAs<TextureD3D>(source);
         ImageD3D *sourceImage           = nullptr;
-        ANGLE_TRY(sourceD3D->getImageAndSyncFromStorage(context, sourceImageIndex, &sourceImage));
+        ANGLE_TRY(
+            sourceD3D->getImageAndSyncFromStorageIfNeeded(context, sourceImageIndex, &sourceImage));
 
         ImageD3D *destImage = nullptr;
-        ANGLE_TRY(getImageAndSyncFromStorage(context, destIndex, &destImage));
+        ANGLE_TRY(getImageAndSyncFromStorageIfNeeded(context, destIndex, &destImage));
 
         ANGLE_TRY(mRenderer->copyImage(context, destImage, sourceImage, sourceBox, destOffset,
                                        unpackFlipY, unpackPremultiplyAlpha, unpackUnmultiplyAlpha));
@@ -3540,11 +3572,8 @@
 
     if (!canCreateRenderTargetForImage(index))
     {
-        gl::Offset destLayerOffset(clippedDestOffset.x, clippedDestOffset.y, 0);
-        ANGLE_TRY(mImageArray[index.getLevelIndex()][clippedDestOffset.z]->copyFromFramebuffer(
-            context, destLayerOffset, clippedSourceArea, source));
-        mDirtyImages = true;
-        onStateChange(angle::SubjectMessage::DirtyBitsFlagged);
+        ANGLE_TRY(copyImageFromFramebufferToStaging(context, index, clippedDestOffset,
+                                                    clippedSourceArea, source));
     }
     else
     {
@@ -3618,9 +3647,10 @@
             ImageD3D *destImage   = nullptr;
             TextureD3D *sourceD3D = GetImplAs<TextureD3D>(source);
 
-            ANGLE_TRY(getImageAndSyncFromStorage(context, currentDestDepthIndex, &destImage));
-            ANGLE_TRY(sourceD3D->getImageAndSyncFromStorage(context, currentSourceDepthIndex,
-                                                            &sourceImage));
+            ANGLE_TRY(
+                getImageAndSyncFromStorageIfNeeded(context, currentDestDepthIndex, &destImage));
+            ANGLE_TRY(sourceD3D->getImageAndSyncFromStorageIfNeeded(
+                context, currentSourceDepthIndex, &sourceImage));
             gl::Box imageBox(sourceBox.x, sourceBox.y, 0, sourceBox.width, sourceBox.height, 1);
             ANGLE_TRY(mRenderer->copyImage(context, destImage, sourceImage, imageBox, destOffset,
                                            unpackFlipY, unpackPremultiplyAlpha,
@@ -3680,11 +3710,11 @@
 
             TextureD3D *sourceD3D = GetImplAs<TextureD3D>(source);
             ImageD3D *sourceImage = nullptr;
-            ANGLE_TRY(
-                sourceD3D->getImageAndSyncFromStorage(context, currentSourceIndex, &sourceImage));
+            ANGLE_TRY(sourceD3D->getImageAndSyncFromStorageIfNeeded(context, currentSourceIndex,
+                                                                    &sourceImage));
 
             ImageD3D *destImage = nullptr;
-            ANGLE_TRY(getImageAndSyncFromStorage(context, currentDestIndex, &destImage));
+            ANGLE_TRY(getImageAndSyncFromStorageIfNeeded(context, currentDestIndex, &destImage));
 
             ANGLE_TRY(mRenderer->copyImage(context, destImage, sourceImage, currentLayerBox,
                                            destOffset, unpackFlipY, unpackPremultiplyAlpha,
diff --git a/src/libANGLE/renderer/d3d/TextureD3D.h b/src/libANGLE/renderer/d3d/TextureD3D.h
index 2ef651b..c95f12e 100644
--- a/src/libANGLE/renderer/d3d/TextureD3D.h
+++ b/src/libANGLE/renderer/d3d/TextureD3D.h
@@ -48,9 +48,12 @@
     virtual ImageD3D *getImage(const gl::ImageIndex &index) const = 0;
     virtual GLsizei getLayerCount(int level) const                = 0;
 
-    angle::Result getImageAndSyncFromStorage(const gl::Context *context,
-                                             const gl::ImageIndex &index,
-                                             ImageD3D **outImage);
+    // Returns the ImageD3D for |index|, syncing its CPU staging buffer from |mTexStorage| if
+    // |mTexStorage| holds newer data (i.e. when |mTexStorage| is valid/complete and the image does
+    // not already have uncommitted CPU-side modifications).
+    angle::Result getImageAndSyncFromStorageIfNeeded(const gl::Context *context,
+                                                     const gl::ImageIndex &index,
+                                                     ImageD3D **outImage);
 
     GLint getBaseLevelWidth() const;
     GLint getBaseLevelHeight() const;
@@ -183,6 +186,11 @@
     virtual bool isImageComplete(const gl::ImageIndex &index) const = 0;
 
     bool canCreateRenderTargetForImage(const gl::ImageIndex &index) const;
+    angle::Result copyImageFromFramebufferToStaging(const gl::Context *context,
+                                                    const gl::ImageIndex &index,
+                                                    const gl::Offset &destOffset,
+                                                    const gl::Rectangle &clippedArea,
+                                                    gl::Framebuffer *source);
     angle::Result ensureBindFlags(const gl::Context *context, BindFlags bindFlags);
     angle::Result ensureRenderTarget(const gl::Context *context);
 
diff --git a/src/tests/gl_tests/CopyTexImageTest.cpp b/src/tests/gl_tests/CopyTexImageTest.cpp
index 8a89b3e..bfe6b9e 100644
--- a/src/tests/gl_tests/CopyTexImageTest.cpp
+++ b/src/tests/gl_tests/CopyTexImageTest.cpp
@@ -1459,7 +1459,8 @@
                         kTexSize / 2);
     ASSERT_GL_NO_ERROR();
 
-    // Verify it.
+    // Verify both regions.
+    verifyResults(dstColor, kExpected.data(), kTexSize, 0, 0, kTexSize / 2, kTexSize / 2, 1.0);
     verifyResults(dstColor, kExpected.data(), kTexSize, kTexSize / 2, kTexSize / 2, kTexSize,
                   kTexSize, 1.0);
 }
@@ -2142,6 +2143,222 @@
     EXPECT_GL_NO_ERROR();
 }
 
+// Verify that partial glCopyTexSubImage2D into a cleared non-zero base level (with level 0
+// undefined) preserves the rest of the cleared level.
+TEST_P(CopyTexImageTestES3, CopyTexSubImagePreservesClearedNonZeroBaseLevel2D)
+{
+    constexpr GLsizei kSize = 64;
+
+    GLTexture dstTex;
+    glBindTexture(GL_TEXTURE_2D, dstTex);
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+
+    GLFramebuffer dstFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, dstFbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dstTex, 1);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    GLRenderbuffer srcRbo;
+    glBindRenderbuffer(GL_RENDERBUFFER, srcRbo);
+    glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 4, 4);
+    GLFramebuffer srcFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, srcFbo);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, srcRbo);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, srcFbo);
+    glCopyTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 0, 0, 1, 1);
+    ASSERT_GL_NO_ERROR();
+
+    glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    glCopyTexSubImage2D(GL_TEXTURE_2D, 1, 1, 0, 0, 0, 1, 1);
+    ASSERT_GL_NO_ERROR();
+
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, dstFbo);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+    EXPECT_PIXEL_COLOR_EQ(1, 0, GLColor::blue);
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/CopyTexImageTest.cpp b/src/tests/gl_tests/CopyTexImageTest.cpp
index 8a89b3e..bfe6b9e 100644
--- a/src/tests/gl_tests/CopyTexImageTest.cpp
+++ b/src/tests/gl_tests/CopyTexImageTest.cpp
@@ -1459,7 +1459,8 @@
                         kTexSize / 2);
     ASSERT_GL_NO_ERROR();
 
-    // Verify it.
+    // Verify both regions.
+    verifyResults(dstColor, kExpected.data(), kTexSize, 0, 0, kTexSize / 2, kTexSize / 2, 1.0);
     verifyResults(dstColor, kExpected.data(), kTexSize, kTexSize / 2, kTexSize / 2, kTexSize,
                   kTexSize, 1.0);
 }
@@ -2142,6 +2143,222 @@
     EXPECT_GL_NO_ERROR();
 }
 
+// Verify that partial glCopyTexSubImage2D into a cleared non-zero base level (with level 0
+// undefined) preserves the rest of the cleared level.
+TEST_P(CopyTexImageTestES3, CopyTexSubImagePreservesClearedNonZeroBaseLevel2D)
+{
+    constexpr GLsizei kSize = 64;
+
+    GLTexture dstTex;
+    glBindTexture(GL_TEXTURE_2D, dstTex);
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+
+    GLFramebuffer dstFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, dstFbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dstTex, 1);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    GLRenderbuffer srcRbo;
+    glBindRenderbuffer(GL_RENDERBUFFER, srcRbo);
+    glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 4, 4);
+    GLFramebuffer srcFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, srcFbo);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, srcRbo);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, srcFbo);
+    glCopyTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 0, 0, 1, 1);
+    ASSERT_GL_NO_ERROR();
+
+    glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    glCopyTexSubImage2D(GL_TEXTURE_2D, 1, 1, 0, 0, 0, 1, 1);
+    ASSERT_GL_NO_ERROR();
+
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, dstFbo);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+    EXPECT_PIXEL_COLOR_EQ(1, 0, GLColor::blue);
+    EXPECT_PIXEL_RECT_EQ(2, 0, kSize - 2, 1, GLColor::green);
+    EXPECT_PIXEL_RECT_EQ(0, 1, kSize, kSize - 1, GLColor::green);
+}
+
+// Verify that partial glCopyTexSubImage2D into a cleared non-zero base level of a cubemap face
+// (with level 0 undefined) preserves the rest of the cleared face.
+TEST_P(CopyTexImageTestES3, CopyTexSubImagePreservesClearedNonZeroBaseLevelCube)
+{
+    constexpr GLsizei kSize = 64;
+
+    GLTexture dstTex;
+    glBindTexture(GL_TEXTURE_CUBE_MAP, dstTex);
+    for (GLenum face = GL_TEXTURE_CUBE_MAP_POSITIVE_X; face <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
+         ++face)
+    {
+        glTexImage2D(face, 1, GL_RGBA8, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    }
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, 1);
+
+    GLFramebuffer dstFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, dstFbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X,
+                           dstTex, 1);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    GLRenderbuffer srcRbo;
+    glBindRenderbuffer(GL_RENDERBUFFER, srcRbo);
+    glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 4, 4);
+    GLFramebuffer srcFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, srcFbo);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, srcRbo);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, srcFbo);
+    glCopyTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 1, 0, 0, 0, 0, 1, 1);
+    ASSERT_GL_NO_ERROR();
+
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, dstFbo);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+    EXPECT_PIXEL_RECT_EQ(1, 0, kSize - 1, 1, GLColor::green);
+    EXPECT_PIXEL_RECT_EQ(0, 1, kSize, kSize - 1, GLColor::green);
+}
+
+// Verify that partial glCopyTexSubImage3D into a cleared non-zero base level of a 2D array texture
+// (with level 0 undefined) preserves the rest of the cleared layer.
+TEST_P(CopyTexImageTestES3, CopyTexSubImagePreservesClearedNonZeroBaseLevel2DArray)
+{
+    constexpr GLsizei kSize = 64;
+
+    GLTexture dstTex;
+    glBindTexture(GL_TEXTURE_2D_ARRAY, dstTex);
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 nullptr);
+    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 1);
+
+    GLFramebuffer dstFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, dstFbo);
+    glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, dstTex, 1, 0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    GLRenderbuffer srcRbo;
+    glBindRenderbuffer(GL_RENDERBUFFER, srcRbo);
+    glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 4, 4);
+    GLFramebuffer srcFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, srcFbo);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, srcRbo);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, srcFbo);
+    glCopyTexSubImage3D(GL_TEXTURE_2D_ARRAY, 1, 0, 0, 0, 0, 0, 1, 1);
+    ASSERT_GL_NO_ERROR();
+
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, dstFbo);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+    EXPECT_PIXEL_RECT_EQ(1, 0, kSize - 1, 1, GLColor::green);
+    EXPECT_PIXEL_RECT_EQ(0, 1, kSize, kSize - 1, GLColor::green);
+}
+
+// Verify that partial glCopyTexSubImage3D into a cleared 3D texture (which always uses the CPU
+// fallback via getImageAndSyncFromStorageIfNeeded in TextureD3D_3D::copySubImage) preserves the
+// cleared slice across multiple sub-region copies.
+TEST_P(CopyTexImageTestES3, CopyTexSubImagePreservesCleared3D)
+{
+    constexpr GLsizei kSize = 16;
+
+    GLTexture dstTex;
+    glBindTexture(GL_TEXTURE_3D, dstTex);
+    glTexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, kSize, kSize, 2);
+
+    GLFramebuffer dstFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, dstFbo);
+    glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, dstTex, 0, 0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    GLRenderbuffer srcRbo;
+    glBindRenderbuffer(GL_RENDERBUFFER, srcRbo);
+    glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 4, 4);
+    GLFramebuffer srcFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, srcFbo);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, srcRbo);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    glCopyTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 0, 0, 1, 1);
+    ASSERT_GL_NO_ERROR();
+
+    glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    glCopyTexSubImage3D(GL_TEXTURE_3D, 0, 1, 0, 0, 0, 0, 1, 1);
+    ASSERT_GL_NO_ERROR();
+
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, dstFbo);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+    EXPECT_PIXEL_COLOR_EQ(1, 0, GLColor::blue);
+    EXPECT_PIXEL_RECT_EQ(2, 0, kSize - 2, 1, GLColor::green);
+    EXPECT_PIXEL_RECT_EQ(0, 1, kSize, kSize - 1, GLColor::green);
+}
+
+// Verify that partial glCopySubTextureCHROMIUM into a GL_SRGB8_ALPHA8 texture (which forces the
+// CPU fallback in TextureD3D_2D::copySubTexture even at base level 0) preserves storage data
+// uploaded via setData when the storage is not a render target.
+TEST_P(CopyTexImageTestES3, CopySubTexturePreservesStorageDataSRGB)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_CHROMIUM_copy_texture"));
+
+    constexpr GLsizei kSize = 16;
+    std::vector<GLColor> greenData(kSize * kSize, GLColor::green);
+    std::vector<GLColor> redData(4 * 4, GLColor::red);
+    std::vector<GLColor> blueData(4 * 4, GLColor::blue);
+
+    GLTexture dstTex;
+    glBindTexture(GL_TEXTURE_2D, dstTex);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_SRGB8_ALPHA8, kSize, kSize);
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSize, kSize, GL_RGBA, GL_UNSIGNED_BYTE,
+                    greenData.data());
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+    GLTexture srcTex;
+    glBindTexture(GL_TEXTURE_2D, srcTex);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, redData.data());
+
+    glCopySubTextureCHROMIUM(srcTex, 0, GL_TEXTURE_2D, dstTex, 0, 0, 0, 0, 0, 1, 1, GL_FALSE,
+                             GL_FALSE, GL_FALSE);
+    ASSERT_GL_NO_ERROR();
+
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, blueData.data());
+    glCopySubTextureCHROMIUM(srcTex, 0, GL_TEXTURE_2D, dstTex, 0, 1, 0, 0, 0, 1, 1, GL_FALSE,
+                             GL_FALSE, GL_FALSE);
+    ASSERT_GL_NO_ERROR();
+
+    glBindTexture(GL_TEXTURE_2D, dstTex);
+    glBindFramebuffer(GL_FRAMEBUFFER, 0);
+    glViewport(0, 0, kSize, kSize);
+    draw2DTexturedQuad(0.5f, 1.0f, true);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+    EXPECT_PIXEL_COLOR_EQ(1, 0, GLColor::blue);
+    EXPECT_PIXEL_RECT_EQ(2, 0, kSize - 2, 1, GLColor::green);
+    EXPECT_PIXEL_RECT_EQ(0, 1, kSize, kSize - 1, GLColor::green);
+}
+
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(
     CopyTexImageTest,
     ES2_D3D11_PRESENT_PATH_FAST(),
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.