Chrome · ANGLE › D3D11
CVE-2026-113285
Memory Corruption in ANGLE (D3D11)
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
src/tests/gl_tests/CopyTexImageTest.cppsrc/libANGLE/renderer/d3d/TextureD3D.cppsrc/libANGLE/renderer/d3d/TextureD3D.h
Patch
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 ®ion)
@@ -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.
References
On This Page