CVE-2026-95281
Overview
Background
- ASTC compressed textures
- a block-compressed GPU texture format uploaded through
glCompressedTexImage2D(full image) andglCompressedTexSubImage2D(sub-region). - `TEXTURE_BASE_LEVEL`
- the lowest mip level a texture samples from; ANGLE tracks the applied value as
mAppliedBaseLeveland can change it withsetBaseLevel. - PowerVR base-level driver bug
- on PowerVR GPUs, uploading an ASTC image while
base_level > 0makes the driver miscompute the destination offset, writing outside the intended allocation. - ANGLE GL workaround feature
- a per-vendor flag (here
resetBaseLevelForASTCImage) enabled viaANGLE_FEATURE_CONDITION(..., IsPowerVR(vendor))that gates a compensating code path.
Root Cause Analysis
ANGLE already carried a workaround, resetBaseLevelForASTCSubImage, that reset TEXTURE_BASE_LEVEL to 0 before glCompressedTexSubImage2D on PowerVR to dodge the driver’s offset miscomputation, but the full-image path glCompressedTexImage2D had no such guard. When an application uploaded an ASTC image with base_level > 0 through the full-image entry point, the PowerVR driver miscomputed the write offset and overflowed the texture allocation — a GPU-process buffer overflow reachable from WebGL.
The fix renames the feature to resetBaseLevelForASTCImage and applies the reset-to-0-then-restore dance in the glCompressedTexImage2D path as well: before the upload, if the feature is enabled and the format is ASTC, setBaseLevel(context, 0) is called, and the original level is restored afterward. Both upload paths now share the same protection, closing the overflow on the full-image route.
glCompressedTexSubImage2D, leaving the equivalent glCompressedTexImage2D path unprotected; generalizing the feature to resetBaseLevelForASTCImage and wrapping both uploads with the base-level reset removes the overflow.Attack Path
- PowerVR target
the victim runs Chrome on a PowerVR GPU, where
resetBaseLevelForASTCImage(and the underlying driver bug) apply. - Enable ASTC in WebGL attacker JavaScript obtains a WebGL context and the ASTC compressed-texture extension.
- Raise the base level
it sets
TEXTURE_BASE_LEVEL > 0on a texture, then uploads an ASTC image via the full-image path (compressedTexImage2D). - Driver miscomputes offset pre-fix, ANGLE forwarded the call with a non-zero base level, and the PowerVR driver wrote the compressed data outside the allocation.
- Heap overflow in the GPU process the out-of-bounds write corrupts adjacent GPU-process memory, a primitive toward escaping the renderer/GPU boundary.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ASTCCompressedWithBaseLevelTestsrc/tests/gl_tests/TextureTest.cpp |
modified |
Files Changed
src/tests/gl_tests/TextureTest.cpputil/autogen/angle_features_autogen.hsrc/libANGLE/renderer/gl/renderergl_utils.cppinclude/platform/autogen/FeaturesGL_autogen.hutil/autogen/angle_features_autogen.cppsrc/libANGLE/renderer/gl/TextureGL.cppinclude/platform/gl_features.json
Audit Directions
- Asymmetric image vs sub-image guardsaudit every texture workaround applied to only one of the
TexImage/TexSubImage(or 2D/3D) pairs; driver bugs usually affect both entry points. - Base-level-sensitive uploadsflag compressed-texture paths that forward uploads without normalizing
TEXTURE_BASE_LEVELon GPUs known to miscompute offsets. - Vendor-gated workaroundsreview
ANGLE_FEATURE_CONDITIONflags that guard memory-safety fixes to confirm the condition matches every affected driver family and every affected call site.
Patch
From a5804ada06ac3756d2d1917912856d4ab110a434 Mon Sep 17 00:00:00 2001
From: Zhenyao Mo <zmo@chromium.org>
Date: Thu, 27 Aug 2026 13:17:43 -0700
Subject: [PATCH] GL: Reset base level for ASTC compressed image uploads
Rename ResetBaseLevelForASTCSubImage workaround to
ResetBaseLevelForASTCImage, and apply it to glCompressedTexImage2D
in addition to glCompressedTexSubImage2D. For ASTC textures when
base_level > 0, base_level is reset to 0 before calling
glCompressedTexImage2D or glCompressedTexSubImage2D and restored to
its original value afterwards to work around a PowerVR driver bug.
Bug: chromium:551708184
Change-Id: Ia0218a2148ae007a15773fe7bb58b723f5159e9d
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8315907
Reviewed-by: Kenneth Russell <kbr@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Zhenyao Mo <zmo@chromium.org>
---
diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json
index ee4a40b..82506cf 100644
--- a/include/platform/gl_features.json
+++ b/include/platform/gl_features.json
@@ -309,13 +309,13 @@
"issue": "https://crbug.com/705865"
},
{
- "name": "reset_base_level_for_ASTC_sub_image",
+ "name": "reset_base_level_for_ASTC_image",
"category": "Workarounds",
"description": [
- "Reset texture base level before calling glCompressedTexSubImage2D for ASTC textures to ",
+ "Reset texture base level before calling glCompressedTexImage2D or glCompressedTexSubImage2D for ASTC textures to ",
"work around PowerVR driver bug."
],
- "issue": "https://crbug.com/528131939"
+ "issue": "https://crbug.com/551708184"
},
{
"name": "recreate_immutable_texture_on_base_level_increase",
diff --git a/src/libANGLE/renderer/gl/TextureGL.cpp b/src/libANGLE/renderer/gl/TextureGL.cpp
index ee23e09..91d435d 100644
--- a/src/libANGLE/renderer/gl/TextureGL.cpp
+++ b/src/libANGLE/renderer/gl/TextureGL.cpp
@@ -881,6 +881,13 @@
stateManager->bindTexture(getType(), mTextureID);
ANGLE_TRY(stateManager->setPixelUnpackState(context, unpack));
+
+ const bool isASTC = gl::IsASTC2DFormat(internalFormat) || gl::IsASTC3DFormat(internalFormat);
+ if (features.resetBaseLevelForASTCImage.enabled && isASTC)
+ {
+ ANGLE_TRY(setBaseLevel(context, 0));
+ }
+
if (nativegl::UseTexImage2D(getType()))
{
ASSERT(size.depth == 1);
@@ -935,7 +942,7 @@
ANGLE_TRY(stateManager->setPixelUnpackState(context, unpack));
const bool isASTC = gl::IsASTC2DFormat(format) || gl::IsASTC3DFormat(format);
- if (features.resetBaseLevelForASTCSubImage.enabled && isASTC)
+ if (features.resetBaseLevelForASTCImage.enabled && isASTC)
{
ANGLE_TRY(setBaseLevel(context, 0));
}
diff --git a/src/libANGLE/renderer/gl/renderergl_utils.cpp b/src/libANGLE/renderer/gl/renderergl_utils.cpp
index 13ff8c9..558a872 100644
--- a/src/libANGLE/renderer/gl/renderergl_utils.cpp
+++ b/src/libANGLE/renderer/gl/renderergl_utils.cpp
@@ -2403,7 +2403,7 @@
ANGLE_FEATURE_CONDITION(features, resetTexImage2DBaseLevel,
IsApple() && isIntel && GetMacOSVersion() >= OSVersion(10, 12, 4));
- ANGLE_FEATURE_CONDITION(features, resetBaseLevelForASTCSubImage, IsPowerVR(vendor));
+ ANGLE_FEATURE_CONDITION(features, resetBaseLevelForASTCImage, IsPowerVR(vendor));
ANGLE_FEATURE_CONDITION(features, recreateImmutableTextureOnBaseLevelIncrease,
IsPowerVR(vendor));
ANGLE_FEATURE_CONDITION(features, resetTexStorage2DBaseLevel, IsPowerVR(vendor));
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 80a00f4..5e7b3cd 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -12472,15 +12472,57 @@
}
}
-// Test that compressed sub-image updates work when TEXTURE_BASE_LEVEL > 0.
-// This is a workaround for a PowerVR driver bug where it miscomputes the offset.
-TEST_P(Texture2DTestES3, ASTCCompressedSubImageWithBaseLevel)
+class ASTCCompressedWithBaseLevelTest : public Texture2DTestES3
{
- ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_texture_compression_astc_ldr"));
+ protected:
+ void uploadASTCLevel(GLenum target,
+ GLenum format,
+ GLint level,
+ GLsizei width,
+ GLsizei height,
+ GLsizei blockWidth,
+ GLsizei blockHeight,
+ const GLColor &color,
+ bool useSubImage)
+ {
+ // Void-extent block for ASTC.
+ // Format: 0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ // R_lo, R_hi, G_lo, G_hi, B_lo, B_hi, A_lo, A_hi
+ const uint8_t block[16] = {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFF, color.R, color.R, color.G, color.G,
+ color.B, color.B, color.A, color.A};
- // Use shaders that match the proof-of-concept.
- // They don't use vertex attributes, but gl_VertexID to generate a quad.
- const char *kVS = R"(#version 300 es
+ GLsizei numBlocksX = (width + blockWidth - 1) / blockWidth;
+ GLsizei numBlocksY = (height + blockHeight - 1) / blockHeight;
+ GLsizei numBlocks = numBlocksX * numBlocksY;
+
+ std::vector<uint8_t> data;
+ data.reserve(numBlocks * 16);
+ for (GLsizei i = 0; i < numBlocks; ++i)
+ {
+ data.insert(data.end(), std::begin(block), std::end(block));
+ }
+
+ if (useSubImage)
+ {
+ glCompressedTexSubImage2D(target, level, 0, 0, width, height, format,
+ static_cast<GLsizei>(data.size()), data.data());
+ }
+ else
+ {
+ glCompressedTexImage2D(target, level, format, width, height, 0,
+ static_cast<GLsizei>(data.size()), data.data());
+ }
+ ASSERT_GL_NO_ERROR();
+ }
+
+ void runASTCCompressedWithBaseLevelTest(bool useSubImage)
+ {
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_texture_compression_astc_ldr"));
+
+ // Use shaders that match the proof-of-concept.
+ // They don't use vertex attributes, but gl_VertexID to generate a quad.
+ const char *kVS = R"(#version 300 es
out vec2 uv;
void main()
{
@@ -12489,7 +12531,7 @@
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
})";
- const char *kFS = R"(#version 300 es
+ const char *kFS = R"(#version 300 es
precision highp float;
uniform sampler2D t;
in vec2 uv;
@@ -12499,85 +12541,96 @@
c = texture(t, uv);
})";
- ANGLE_GL_PROGRAM(program, kVS, kFS);
- glUseProgram(program);
- GLint texLocation = glGetUniformLocation(program, "t");
- ASSERT_NE(-1, texLocation);
- glUniform1i(texLocation, 0);
- ASSERT_GL_NO_ERROR();
+ ANGLE_GL_PROGRAM(program, kVS, kFS);
+ glUseProgram(program);
+ GLint texLocation = glGetUniformLocation(program, "t");
+ ASSERT_NE(-1, texLocation);
+ glUniform1i(texLocation, 0);
+ ASSERT_GL_NO_ERROR();
- // 8x5 ASTC format.
- GLenum format = GL_COMPRESSED_RGBA_ASTC_8x5_KHR;
- constexpr GLsizei kWidth = 8;
- constexpr GLsizei kHeight = 160;
- constexpr GLsizei kLevels = 5;
- // Void-extent blocks for ASTC.
- // Format: 0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, R_lo, R_hi, G_lo, G_hi, B_lo, B_hi,
- // A_lo, A_hi Red: (255, 0, 0, 255) -> R=0xFFFF, G=0x0000, B=0x0000, A=0xFFFF
- constexpr uint8_t kBlockRed[16] = {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
- 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF};
- // Green: (0, 255, 0, 255) -> R=0x0000, G=0xFFFF, B=0x0000, A=0xFFFF
- constexpr uint8_t kBlockGreen[16] = {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
- 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF};
+ // 8x5 ASTC format.
+ GLenum format = GL_COMPRESSED_RGBA_ASTC_8x5_KHR;
+ // This needs to be big enough, so at smallest level (=4), width >= kBlockWidth.
+ // This is to work around a driver bug: https://crbug.com/562185979.
+ constexpr GLsizei kWidth = 128;
+ constexpr GLsizei kHeight = 160;
+ constexpr GLsizei kLevels = 5;
+ constexpr GLsizei kBlockWidth = 8;
+ constexpr GLsizei kBlockHeight = 5;
- // Level 4: 1x10 pixels. 8x5 blocks. Padded: 8x10. Blocks: 1x2 = 2.
- std::vector<uint8_t> dataRed;
- dataRed.reserve(32);
- dataRed.insert(dataRed.end(), std::begin(kBlockRed), std::end(kBlockRed));
- dataRed.insert(dataRed.end(), std::begin(kBlockRed), std::end(kBlockRed));
+ // Loop multiple times to increase chances of hitting OOB write/crash if workaround fails.
+ // Keep textures alive to groom the heap similarly to the WebGL PoC.
+ constexpr int kIterations = 16;
+ std::vector<GLTexture> textures(kIterations);
+ for (int i = 0; i < kIterations; ++i)
+ {
+ glBindTexture(GL_TEXTURE_2D, textures[i]);
- // Level 3: 1x20 pixels. 8x5 blocks. Padded: 8x20. Blocks: 1x4 = 4.
- std::vector<uint8_t> dataGreen;
- dataGreen.reserve(64);
- for (int i = 0; i < 4; ++i)
- {
- dataGreen.insert(dataGreen.end(), std::begin(kBlockGreen), std::end(kBlockGreen));
+ if (useSubImage)
+ {
+ glTexStorage2D(GL_TEXTURE_2D, kLevels, format, kWidth, kHeight);
+ ASSERT_GL_NO_ERROR();
+ }
+ else
+ {
+ // We need to initialize all levels to work around a driver bug.
+ // https://crbug.com/556435507.
+ for (GLint level = 0; level < kLevels; ++level)
+ {
+ GLsizei levelWidth = std::max(1, kWidth >> level);
+ GLsizei levelHeight = std::max(1, kHeight >> level);
+ uploadASTCLevel(GL_TEXTURE_2D, format, level, levelWidth, levelHeight,
+ kBlockWidth, kBlockHeight, GLColor::black,
+ /*useSubImage=*/false);
+ }
+ }
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 4);
+ ASSERT_GL_NO_ERROR();
+
+ // Upload Red to level 4 (8x10).
+ uploadASTCLevel(GL_TEXTURE_2D, format, 4, 8, 10, kBlockWidth, kBlockHeight,
+ GLColor::red, useSubImage);
+
+ // Upload Green to level 3 (16x20).
+ uploadASTCLevel(GL_TEXTURE_2D, format, 3, 16, 20, kBlockWidth, kBlockHeight,
+ GLColor::green, useSubImage);
+
+ // Draw. Since BASE_LEVEL is 4, it should sample from level 4 (Red).
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+ glFinish();
+ ASSERT_GL_NO_ERROR();
+
+ EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(255, 0, 0, 255), 1);
+
+ // Change BASE_LEVEL to 3.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 3);
+ ASSERT_GL_NO_ERROR();
+
+ // Draw again. Now it should sample from level 3 (Green).
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+ glFinish();
+ ASSERT_GL_NO_ERROR();
+
+ EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(0, 255, 0, 255), 1);
+ }
}
+};
- // Loop multiple times to increase chances of hitting OOB write/crash if workaround fails.
- // Keep textures alive to groom the heap similarly to the WebGL PoC.
- constexpr int kIterations = 16;
- std::vector<GLTexture> textures(kIterations);
- for (int i = 0; i < kIterations; ++i)
- {
- glBindTexture(GL_TEXTURE_2D, textures[i]);
+// Test that compressed sub-image updates work when TEXTURE_BASE_LEVEL > 0.
+// This is a workaround for a PowerVR driver bug where it miscomputes the offset.
+TEST_P(ASTCCompressedWithBaseLevelTest, SubImage)
+{
+ runASTCCompressedWithBaseLevelTest(/*useSubImage=*/true);
+}
- glTexStorage2D(GL_TEXTURE_2D, kLevels, format, kWidth, kHeight);
- ASSERT_GL_NO_ERROR();
-
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 4);
- ASSERT_GL_NO_ERROR();
-
- // Upload Red to level 4.
- glCompressedTexSubImage2D(GL_TEXTURE_2D, 4, 0, 0, 1, 10, format,
- static_cast<GLsizei>(dataRed.size()), dataRed.data());
- ASSERT_GL_NO_ERROR();
-
- // Upload Green to level 3.
- glCompressedTexSubImage2D(GL_TEXTURE_2D, 3, 0, 0, 1, 20, format,
- static_cast<GLsizei>(dataGreen.size()), dataGreen.data());
- ASSERT_GL_NO_ERROR();
-
- // Draw. Since BASE_LEVEL is 4, it should sample from level 4 (Red).
- glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
- glFinish();
- ASSERT_GL_NO_ERROR();
-
- EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(255, 0, 0, 255), 1);
-
- // Change BASE_LEVEL to 3.
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 3);
- ASSERT_GL_NO_ERROR();
-
- // Draw again. Now it should sample from level 3 (Green).
- glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
- glFinish();
- ASSERT_GL_NO_ERROR();
-
- EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(0, 255, 0, 255), 1);
- }
+// Test that compressed image updates work when TEXTURE_BASE_LEVEL > 0.
+// This is a workaround for a PowerVR driver bug where it miscomputes the offset.
+TEST_P(ASTCCompressedWithBaseLevelTest, Image)
+{
+ runASTCCompressedWithBaseLevelTest(/*useSubImage=*/false);
}
// Test that the selected decode precision is actually used for texture decoding.
@@ -25212,6 +25265,9 @@
ES3_OPENGL().enable(Feature::UseTexSubImageForClientDataNpotUploads),
ES3_OPENGLES().enable(Feature::UseTexSubImageForClientDataNpotUploads));
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(ASTCCompressedWithBaseLevelTest);
+ANGLE_INSTANTIATE_TEST_ES3(ASTCCompressedWithBaseLevelTest);
+
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TextureSizeLimitTest);
ANGLE_INSTANTIATE_TEST(TextureSizeLimitTest,
ES2_D3D11().enable(Feature::LimitMaxTextureBytesTo1MB),
diff --git a/include/platform/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h
index 6a87983..59e2223 100644
--- a/include/platform/autogen/FeaturesGL_autogen.h
+++ b/include/platform/autogen/FeaturesGL_autogen.h
@@ -254,8 +254,8 @@
&members,
};
- FeatureInfo resetBaseLevelForASTCSubImage = {
- "resetBaseLevelForASTCSubImage",
+ FeatureInfo resetBaseLevelForASTCImage = {
+ "resetBaseLevelForASTCImage",
FeatureCategory::OpenGLWorkarounds,
&members,
};
diff --git a/util/autogen/angle_features_autogen.cpp b/util/autogen/angle_features_autogen.cpp
index 15187b8..d8d24f3 100644
--- a/util/autogen/angle_features_autogen.cpp
+++ b/util/autogen/angle_features_autogen.cpp
@@ -315,7 +315,7 @@
{Feature::RemoveInvariantAndCentroidForESSL3, "removeInvariantAndCentroidForESSL3"},
{Feature::RequireGpuFamily2, "requireGpuFamily2"},
{Feature::RescopeGlobalVariables, "rescopeGlobalVariables"},
- {Feature::ResetBaseLevelForASTCSubImage, "resetBaseLevelForASTCSubImage"},
+ {Feature::ResetBaseLevelForASTCImage, "resetBaseLevelForASTCImage"},
{Feature::ResetSampleCoverageOnFBOChange, "resetSampleCoverageOnFBOChange"},
{Feature::ResetTexImage2DBaseLevel, "resetTexImage2DBaseLevel"},
{Feature::ResetTexStorage2DBaseLevel, "resetTexStorage2DBaseLevel"},
diff --git a/util/autogen/angle_features_autogen.h b/util/autogen/angle_features_autogen.h
index 7cd5166..a06aa66 100644
--- a/util/autogen/angle_features_autogen.h
+++ b/util/autogen/angle_features_autogen.h
@@ -315,7 +315,7 @@
RemoveInvariantAndCentroidForESSL3,
RequireGpuFamily2,
RescopeGlobalVariables,
- ResetBaseLevelForASTCSubImage,
+ ResetBaseLevelForASTCImage,
ResetSampleCoverageOnFBOChange,
ResetTexImage2DBaseLevel,
ResetTexStorage2DBaseLevel,
Regression Test / PoC
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 80a00f4..5e7b3cd 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -12472,15 +12472,57 @@
}
}
-// Test that compressed sub-image updates work when TEXTURE_BASE_LEVEL > 0.
-// This is a workaround for a PowerVR driver bug where it miscomputes the offset.
-TEST_P(Texture2DTestES3, ASTCCompressedSubImageWithBaseLevel)
+class ASTCCompressedWithBaseLevelTest : public Texture2DTestES3
{
- ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_texture_compression_astc_ldr"));
+ protected:
+ void uploadASTCLevel(GLenum target,
+ GLenum format,
+ GLint level,
+ GLsizei width,
+ GLsizei height,
+ GLsizei blockWidth,
+ GLsizei blockHeight,
+ const GLColor &color,
+ bool useSubImage)
+ {
+ // Void-extent block for ASTC.
+ // Format: 0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ // R_lo, R_hi, G_lo, G_hi, B_lo, B_hi, A_lo, A_hi
+ const uint8_t block[16] = {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFF, color.R, color.R, color.G, color.G,
+ color.B, color.B, color.A, color.A};
- // Use shaders that match the proof-of-concept.
- // They don't use vertex attributes, but gl_VertexID to generate a quad.
- const char *kVS = R"(#version 300 es
+ GLsizei numBlocksX = (width + blockWidth - 1) / blockWidth;
+ GLsizei numBlocksY = (height + blockHeight - 1) / blockHeight;
+ GLsizei numBlocks = numBlocksX * numBlocksY;
+
+ std::vector<uint8_t> data;
+ data.reserve(numBlocks * 16);
+ for (GLsizei i = 0; i < numBlocks; ++i)
+ {
+ data.insert(data.end(), std::begin(block), std::end(block));
+ }
+
+ if (useSubImage)
+ {
+ glCompressedTexSubImage2D(target, level, 0, 0, width, height, format,
+ static_cast<GLsizei>(data.size()), data.data());
+ }
+ else
+ {
+ glCompressedTexImage2D(target, level, format, width, height, 0,
+ static_cast<GLsizei>(data.size()), data.data());
+ }
+ ASSERT_GL_NO_ERROR();
+ }
+
+ void runASTCCompressedWithBaseLevelTest(bool useSubImage)
+ {
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_texture_compression_astc_ldr"));
+
+ // Use shaders that match the proof-of-concept.
+ // They don't use vertex attributes, but gl_VertexID to generate a quad.
+ const char *kVS = R"(#version 300 es
out vec2 uv;
void main()
{
@@ -12489,7 +12531,7 @@
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
})";
- const char *kFS = R"(#version 300 es
+ const char *kFS = R"(#version 300 es
precision highp float;
uniform sampler2D t;
in vec2 uv;
@@ -12499,85 +12541,96 @@
c = texture(t, uv);
})";
- ANGLE_GL_PROGRAM(program, kVS, kFS);
- glUseProgram(program);
- GLint texLocation = glGetUniformLocation(program, "t");
- ASSERT_NE(-1, texLocation);
- glUniform1i(texLocation, 0);
- ASSERT_GL_NO_ERROR();
+ ANGLE_GL_PROGRAM(program, kVS, kFS);
+ glUseProgram(program);
+ GLint texLocation = glGetUniformLocation(program, "t");
+ ASSERT_NE(-1, texLocation);
+ glUniform1i(texLocation, 0);
+ ASSERT_GL_NO_ERROR();
- // 8x5 ASTC format.
- GLenum format = GL_COMPRESSED_RGBA_ASTC_8x5_KHR;
- constexpr GLsizei kWidth = 8;
- constexpr GLsizei kHeight = 160;
- constexpr GLsizei kLevels = 5;
- // Void-extent blocks for ASTC.
- // Format: 0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, R_lo, R_hi, G_lo, G_hi, B_lo, B_hi,
- // A_lo, A_hi Red: (255, 0, 0, 255) -> R=0xFFFF, G=0x0000, B=0x0000, A=0xFFFF
- constexpr uint8_t kBlockRed[16] = {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
- 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF};
- // Green: (0, 255, 0, 255) -> R=0x0000, G=0xFFFF, B=0x0000, A=0xFFFF
- constexpr uint8_t kBlockGreen[16] = {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
- 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF};
+ // 8x5 ASTC format.
+ GLenum format = GL_COMPRESSED_RGBA_ASTC_8x5_KHR;
+ // This needs to be big enough, so at smallest level (=4), width >= kBlockWidth.
+ // This is to work around a driver bug: https://crbug.com/562185979.
+ constexpr GLsizei kWidth = 128;
+ constexpr GLsizei kHeight = 160;
+ constexpr GLsizei kLevels = 5;
+ constexpr GLsizei kBlockWidth = 8;
+ constexpr GLsizei kBlockHeight = 5;
- // Level 4: 1x10 pixels. 8x5 blocks. Padded: 8x10. Blocks: 1x2 = 2.
- std::vector<uint8_t> dataRed;
- dataRed.reserve(32);
- dataRed.insert(dataRed.end(), std::begin(kBlockRed), std::end(kBlockRed));
- dataRed.insert(dataRed.end(), std::begin(kBlockRed), std::end(kBlockRed));
+ // Loop multiple times to increase chances of hitting OOB write/crash if workaround fails.
+ // Keep textures alive to groom the heap similarly to the WebGL PoC.
+ constexpr int kIterations = 16;
+ std::vector<GLTexture> textures(kIterations);
+ for (int i = 0; i < kIterations; ++i)
+ {
+ glBindTexture(GL_TEXTURE_2D, textures[i]);
- // Level 3: 1x20 pixels. 8x5 blocks. Padded: 8x20. Blocks: 1x4 = 4.
- std::vector<uint8_t> dataGreen;
- dataGreen.reserve(64);
- for (int i = 0; i < 4; ++i)
- {
- dataGreen.insert(dataGreen.end(), std::begin(kBlockGreen), std::end(kBlockGreen));
+ if (useSubImage)
+ {
+ glTexStorage2D(GL_TEXTURE_2D, kLevels, format, kWidth, kHeight);
+ ASSERT_GL_NO_ERROR();
+ }
+ else
+ {
+ // We need to initialize all levels to work around a driver bug.
+ // https://crbug.com/556435507.
+ for (GLint level = 0; level < kLevels; ++level)
+ {
+ GLsizei levelWidth = std::max(1, kWidth >> level);
+ GLsizei levelHeight = std::max(1, kHeight >> level);
+ uploadASTCLevel(GL_TEXTURE_2D, format, level, levelWidth, levelHeight,
+ kBlockWidth, kBlockHeight, GLColor::black,
+ /*useSubImage=*/false);
+ }
+ }
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 4);
+ ASSERT_GL_NO_ERROR();
+
+ // Upload Red to level 4 (8x10).
+ uploadASTCLevel(GL_TEXTURE_2D, format, 4, 8, 10, kBlockWidth, kBlockHeight,
+ GLColor::red, useSubImage);
+
+ // Upload Green to level 3 (16x20).
+ uploadASTCLevel(GL_TEXTURE_2D, format, 3, 16, 20, kBlockWidth, kBlockHeight,
+ GLColor::green, useSubImage);
+
+ // Draw. Since BASE_LEVEL is 4, it should sample from level 4 (Red).
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+ glFinish();
+ ASSERT_GL_NO_ERROR();
+
+ EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(255, 0, 0, 255), 1);
+
+ // Change BASE_LEVEL to 3.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 3);
+ ASSERT_GL_NO_ERROR();
+
+ // Draw again. Now it should sample from level 3 (Green).
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+ glFinish();
+ ASSERT_GL_NO_ERROR();
+
+ EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(0, 255, 0, 255), 1);
+ }
}
+};
- // Loop multiple times to increase chances of hitting OOB write/crash if workaround fails.
- // Keep textures alive to groom the heap similarly to the WebGL PoC.
- constexpr int kIterations = 16;
- std::vector<GLTexture> textures(kIterations);
- for (int i = 0; i < kIterations; ++i)
- {
- glBindTexture(GL_TEXTURE_2D, textures[i]);
+// Test that compressed sub-image updates work when TEXTURE_BASE_LEVEL > 0.
+// This is a workaround for a PowerVR driver bug where it miscomputes the offset.
+TEST_P(ASTCCompressedWithBaseLevelTest, SubImage)
+{
+ runASTCCompressedWithBaseLevelTest(/*useSubImage=*/true);
+}
- glTexStorage2D(GL_TEXTURE_2D, kLevels, format, kWidth, kHeight);
- ASSERT_GL_NO_ERROR();
-
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 4);
- ASSERT_GL_NO_ERROR();
-
- // Upload Red to level 4.
- glCompressedTexSubImage2D(GL_TEXTURE_2D, 4, 0, 0, 1, 10, format,
- static_cast<GLsizei>(dataRed.size()), dataRed.data());
- ASSERT_GL_NO_ERROR();
-
- // Upload Green to level 3.
- glCompressedTexSubImage2D(GL_TEXTURE_2D, 3, 0, 0, 1, 20, format,
- static_cast<GLsizei>(dataGreen.size()), dataGreen.data());
- ASSERT_GL_NO_ERROR();
-
- // Draw. Since BASE_LEVEL is 4, it should sample from level 4 (Red).
- glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
- glFinish();
- ASSERT_GL_NO_ERROR();
-
- EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(255, 0, 0, 255), 1);
-
- // Change BASE_LEVEL to 3.
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 3);
- ASSERT_GL_NO_ERROR();
-
- // Draw again. Now it should sample from level 3 (Green).
- glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
- glFinish();
- ASSERT_GL_NO_ERROR();
-
- EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(0, 255, 0, 255), 1);
- }
+// Test that compressed image updates work when TEXTURE_BASE_LEVEL > 0.
+// This is a workaround for a PowerVR driver bug where it miscomputes the offset.
+TEST_P(ASTCCompressedWithBaseLevelTest, Image)
+{
+ runASTCCompressedWithBaseLevelTest(/*useSubImage=*/false);
}
// Test that the selected decode precision is actually used for texture decoding.
@@ -25212,6 +25265,9 @@
ES3_OPENGL().enable(Feature::UseTexSubImageForClientDataNpotUploads),
ES3_OPENGLES().enable(Feature::UseTexSubImageForClientDataNpotUploads));
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(ASTCCompressedWithBaseLevelTest);
+ANGLE_INSTANTIATE_TEST_ES3(ASTCCompressedWithBaseLevelTest);
+
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TextureSizeLimitTest);
ANGLE_INSTANTIATE_TEST(TextureSizeLimitTest,
ES2_D3D11().enable(Feature::LimitMaxTextureBytesTo1MB),