CVE-2026-87438
Overview
Background
- ANGLE
- Chrome’s translation layer that implements OpenGL ES / WebGL on top of native graphics APIs, here targeting a native desktop/mobile
GLbackend. - Mip level
- one image in a texture’s mipmap chain, whose dimensions are normally constrained by level 0 per the OpenGL ES rules (
ceilPow2(level0) >> level). - Oversized level
- a non-zero mip level whose supplied
width/heightexceed the size implied by level 0, violating the ES mipmap sizing rules. - PBO (pixel unpack buffer)
- a GPU-side buffer bound to
GL_PIXEL_UNPACK_BUFFERfrom which the driver reads pixel data, routing an upload through a driver path distinct from immediate client-memory upload.
Root Cause Analysis
The vulnerable path is TextureGL::setImageHelper in TextureGL.cpp, which forwarded a client texImage2D/texSubImage2D for a non-zero mip level straight into the driver’s immediate software upload. When level 0 of a _2D texture is already defined and the application supplies an “oversized” non-zero level — one whose size.width/size.height exceed the slot dimensions max(1, ceilPow2(level0.size.width) >> level) / max(1, ceilPow2(level0.size.height) >> level) — certain drivers (PowerVR) size their destination allocation from level 0’s implied dimensions but copy using the larger client-supplied dimensions. The violated invariant is that a mip level’s dimensions must not exceed what level 0 dictates, so the driver’s write ran past the allocated backing store, producing an out-of-bounds write.
The fix detects this oversized-level condition in setImageHelper (gated on the new uploadOversizedMipLevelsViaUnpackBuffer feature, _2D type, level > 0, no bound unpackBuffer, and no depth/stencil format) and reroutes the upload to setImageViaScratchUnpackBuffer. That helper allocates a scratch GL_PIXEL_UNPACK_BUFFER of the exact expected uploadBytes, binds it, and issues the texImage2D with a nullptr data pointer so the driver reads from the PBO through a path that handles the sizing safely.
Attack Path
- Define level 0
A malicious WebGL page creates a
TEXTURE_2Dand defines mip level 0 with modest dimensions, establishing the texture’s expected storage footprint. - Supply an oversized non-zero level
The page calls
texImage2Dforlevel > 0withwidth/heightlarger than theceilPow2(level0) >> levelslot, violating the ES mipmap rules. - Reach the immediate upload
With no unpack buffer bound and a color (non depth/stencil) format,
setImageHelperforwarded the call directly to the driver’s software upload. - Trigger the driver overrun On an affected (PowerVR) driver, the destination is sized from level 0 but written using the oversized dimensions, causing an out-of-bounds write in GPU-process memory.
Impact Assessment
GL backend on affected drivers), corrupting memory beyond a texture’s allocation. The only precondition is the ability to issue WebGL texture-upload calls plus an affected driver stack; no special privileges are required. Such GPU-process memory corruption is a strong primitive toward sandbox escape or further exploitation.Files Changed
include/platform/autogen/FeaturesGL_autogen.hinclude/platform/gl_features.jsonsrc/libANGLE/renderer/gl/TextureGL.cpp
Audit Directions
- Client-supplied texture dimensionsAudit every backend upload path where per-level
width/height/imageSizefrom the application flow into a nativetexImage/compressedTexImagecall, and confirm they are validated against the storage implied by level 0 rather than trusted. - Immediate vs. PBO upload divergenceReview other cases distinguishing immediate client-memory uploads from pixel-unpack-buffer uploads, since driver bugs are frequently isolated to only one path and workarounds may be missing for
_3D, cube-map, or compressed variants. - Driver-specific workaround gatingCheck that feature flags like
uploadOversizedMipLevelsViaUnpackBuffercover all drivers exhibiting the sizing bug and that excluded conditions (depth/stencil formats, boundunpackBuffer,level == 0) do not leave an equivalent overrun reachable.
Patch
From 107da744f62a319b3c6851694740d9ebf247d048 Mon Sep 17 00:00:00 2001 From: Ken Russell <[email protected]> Date: Thu, 27 Aug 2026 16:34:18 -0700 Subject: [PATCH] GL: upload oversized mip levels through PBO. Add a workaround for the following scenario: if uploading data to a texture's mipmap level which is "oversized" compared to level 0 - in other words, larger than the OpenGL ES specification's mipmap rules - then upload the data via a pixel unpack buffer. Apply this workaround to PowerVR drivers. Due to another driver bug, the new workaround can not completely solve the issue for compressed textures; modify the new tests to accommodate this fact. It is believed this will not affect applications in the field. Bug: chromium:548127218 Change-Id: If4e2e8fceb6a77897b50fa2c7c932f0e2c5c5ce1 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8305515 Auto-Submit: Kenneth Russell <[email protected]> Reviewed-by: Zhenyao Mo <[email protected]> Reviewed-by: Geoff Lang <[email protected]> Commit-Queue: Geoff Lang <[email protected]> --- diff --git a/include/platform/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h index 839b476..a42c2950 100644 --- a/include/platform/autogen/FeaturesGL_autogen.h +++ b/include/platform/autogen/FeaturesGL_autogen.h @@ -776,6 +776,12 @@ &members, }; + FeatureInfo uploadOversizedMipLevelsViaUnpackBuffer = { + "uploadOversizedMipLevelsViaUnpackBuffer", + FeatureCategory::OpenGLWorkarounds, + &members, + }; + FeatureInfo validateState = { "validateState", FeatureCategory::OpenGLFeatures, diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json index 9bac71d..276028d 100644 --- a/include/platform/gl_features.json +++ b/include/platform/gl_features.json @@ -1021,6 +1021,15 @@ "issue": "http://crbug.com/499602793" }, { + "name": "upload_oversized_mip_levels_via_unpack_buffer", + "category": "Workarounds", + "description": [ + "Stage oversized nonzero-level texture definitions through a scratch pixel unpack buffer ", + "to work around driver bugs." + ], + "issue": "http://crbug.com/548127218" + }, + { "name": "validate_state", "category": "Features", "description": [ diff --git a/src/libANGLE/renderer/gl/TextureGL.cpp b/src/libANGLE/renderer/gl/TextureGL.cpp index 25f2644..3b3a0f9 100644 --- a/src/libANGLE/renderer/gl/TextureGL.cpp +++ b/src/libANGLE/renderer/gl/TextureGL.cpp @@ -226,6 +226,30 @@ gl::TextureTarget target = index.getTarget(); size_t level = static_cast<size_t>(index.getLevelIndex()); + // Oversized nonzero-level definitions may cause driver issues during immediate software + // texture upload when level 0 is already defined. Stage them through a scratch unpack buffer + // so the driver handles the upload safely. + if (features.uploadOversizedMipLevelsViaUnpackBuffer.enabled && + getType() == gl::TextureType::_2D && level > 0 && unpackBuffer == nullptr && + gl::GetInternalFormatInfo(internalFormat, type).depthBits == 0 && + gl::GetInternalFormatInfo(internalFormat, type).stencilBits == 0) + { + const gl::ImageDesc &level0 = mState.getImageDesc(gl::TextureTarget::_2D, 0); + if (level0.size.width != 0 && level0.size.height != 0) + { + const int slotW = + std::max(1, static_cast<int>(gl::ceilPow2(level0.size.width)) >> level); + const int slotH = + std::max(1, static_cast<int>(gl::ceilPow2(level0.size.height)) >> level); + if (size.width > slotW || size.height > slotH) + { + return setImageViaScratchUnpackBuffer(context, target, level, internalFormat, size, + format, type, unpack, /*isCompressed=*/false, + /*imageSize=*/0, pixels); + } + } + } + if (features.unpackOverlappingRowsSeparatelyUnpackBuffer.enabled && unpackBuffer && unpack.rowLength != 0 && unpack.rowLength < size.width) { @@ -371,6 +395,102 @@ return angle::Result::Continue; } +angle::Result TextureGL::setImageViaScratchUnpackBuffer(const gl::Context *context, + gl::TextureTarget target, + size_t level, + GLenum internalFormat, + const gl::Extents &size, + GLenum format, + GLenum type, + const gl::PixelUnpackState &unpack, + bool isCompressed, + size_t imageSize, + const uint8_t *pixels) +{ + ContextGL *contextGL = GetImplAs<ContextGL>(context); + const FunctionsGL *functions = GetFunctionsGL(context); + StateManagerGL *stateManager = GetStateManagerGL(context); + const angle::FeaturesGL &features = GetFeaturesGL(context); + + if (features.reattachFboDepthStencilOnReallocation.enabled) + { + onStateChange(angle::SubjectMessage::ObjectReallocated); + } + + GLuint uploadBytes = 0; + if (isCompressed) + { + uploadBytes = static_cast<GLuint>(imageSize); + } + else + { + ANGLE_CHECK_GL_MATH(contextGL, gl::GetInternalFormatInfo(format, type) + .computePackUnpackEndByte(type, size, unpack, + /*is3D=*/false, &uploadBytes)); + } + + GLuint scratch = 0; + functions->genBuffers(1, &scratch); + stateManager->bindBuffer(gl::BufferBinding::PixelUnpack, scratch); + // Regardless of whether the user supplied data (pixels != nullptr), the pixel unpack buffer + // must be allocated with the expected amount of data. + if (uploadBytes > 0) + { + ANGLE_GL_TRY(context, functions->bufferData(GL_PIXEL_UNPACK_BUFFER, uploadBytes, pixels, + GL_STREAM_DRAW)); + } + + ANGLE_TRY(stateManager->setPixelUnpackState(context, unpack)); + + stateManager->bindTexture(getType(), mTextureID); + + if (features.resetTexImage2DBaseLevel.enabled) + { + (void)setBaseLevel(context, 0); + } + + if (isCompressed) + { + const gl::InternalFormat &originalInternalFormatInfo = + gl::GetSizedInternalFormatInfo(internalFormat); + nativegl::CompressedTexImageFormat compressedTexImageFormat = + nativegl::GetCompressedTexImageFormat(functions, features, internalFormat); + + ANGLE_GL_TRY_ALWAYS_CHECK( + context, functions->compressedTexImage2D( + nativegl::GetTextureBindingTarget(target), static_cast<GLint>(level), + compressedTexImageFormat.internalFormat, size.width, size.height, 0, + static_cast<GLsizei>(uploadBytes), nullptr)); + + LevelInfoGL levelInfo = GetLevelInfo(features, originalInternalFormatInfo, + compressedTexImageFormat.internalFormat); + ASSERT(!levelInfo.lumaWorkaround.enabled); + setLevelInfo(context, target, level, 1, levelInfo); + } + else + { + const gl::InternalFormat &originalInternalFormatInfo = + gl::GetInternalFormatInfo(internalFormat, type); + nativegl::TexImageFormat texImageFormat = + nativegl::GetTexImageFormat(functions, features, internalFormat, format, type); + + ANGLE_GL_TRY_ALWAYS_CHECK( + context, functions->texImage2D(nativegl::GetTextureBindingTarget(target), + static_cast<GLint>(level), texImageFormat.internalFormat, + size.width, size.height, 0, texImageFormat.format, + texImageFormat.type, nullptr)); + + LevelInfoGL levelInfo = + GetLevelInfo(features, originalInternalFormatInfo, texImageFormat.internalFormat); + setLevelInfo(context, target, level, 1, levelInfo); + } + + stateManager->deleteBuffer(scratch); + + contextGL->markWorkSubmitted(); + return angle::Result::Continue; +} + angle::Result TextureGL::reserveTexImageToBeFilled(const gl::Context *context, gl::TextureTarget target, size_t level,
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index cc11e10..e5c12f8 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -2916,6 +2916,12 @@
549916890 WIN INTEL OPENGL : Texture3DIncreaseDepthTestES3.TexImage3DDepthIncreaseOutsideRangeStagedVerifyByDraw/* = SKIP
549916890 WIN INTEL OPENGL : Texture3DIncreaseDepthTestES3.TexImage3DDepthIncreaseOutsideRangeStagedVerifyByReadPixels/* = SKIP
+// New tests failing on Windows Intel OpenGL
+548127218 WIN INTEL OPENGL : Texture2DTestES3_OversizedMipLevels.Uncompressed/* = SKIP
+548127218 WIN INTEL OPENGL : Texture2DTestES3_OversizedMipLevels.CompressedASTC/* = SKIP
+548127218 WIN INTEL OPENGL : Texture2DTestES3_OversizedMipLevels.CompressedDXT/* = SKIP
+548127218 WIN INTEL OPENGL : Texture2DTestES3_OversizedMipLevels.CompressedETC/* = SKIP
+
// Various crashes when using Vulkan on Win/ARM64.
545676921 WIN QUALCOMM VULKAN : GLSLTest_ES31.MixedRowAndColumnMajorMatrices/* = SKIP
545676921 WIN QUALCOMM VULKAN : MSRTTES3Test.CubeMap/* = SKIP
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index a7c77fd..51b6704 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -23810,6 +23810,256 @@
}
}
+class Texture2DTestES3_OversizedMipLevels : public Texture2DTestES3
+{
+ protected:
+ Texture2DTestES3_OversizedMipLevels() : Texture2DTestES3() {}
+
+ void testSetUp() override
+ {
+ Texture2DTestES3::testSetUp();
+ setUpProgram();
+ glUseProgram(mProgram);
+ glUniform1i(mTexture2DUniformLocation, 0);
+ glActiveTexture(GL_TEXTURE0);
+ }
+};
+
+// Test that defining an oversized nonzero mip level on an uncompressed texture succeeds and
+// renders correctly.
+TEST_P(Texture2DTestES3_OversizedMipLevels, Uncompressed)
+{
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // Upload Level 0: 64x64 solid red.
+ constexpr int kLevel0Width = 64;
+ constexpr int kLevel0Height = 64;
+ std::vector<GLColor> redData(kLevel0Width * kLevel0Height, GLColor::red);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kLevel0Width, kLevel0Height, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, redData.data());
+ ASSERT_GL_NO_ERROR();
+
+ // Upload Level 1: 128x128 solid green (oversized relative to Level 0).
+ constexpr int kLevel1Width = 128;
+ constexpr int kLevel1Height = 128;
+ std::vector<GLColor> greenData(kLevel1Width * kLevel1Height, GLColor::green);
+ glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kLevel1Width, kLevel1Height, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, greenData.data());
+ ASSERT_GL_NO_ERROR();
+
+ // Verify Level 0 sampling.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::red);
+
+ // Verify Level 1 sampling.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::green);
+}
+
+// Test that defining an oversized nonzero mip level on an ETC compressed texture succeeds and
+// renders correctly.
+TEST_P(Texture2DTestES3_OversizedMipLevels, CompressedETC)
+{
+ const bool hasEtcExt = IsGLExtensionEnabled("GL_OES_compressed_ETC2_RGB8_texture");
+ const int clientVer = getClientMajorVersion();
+ ANGLE_SKIP_TEST_IF(!hasEtcExt && clientVer < 3);
+
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // ETC2 RGB8: 8 bytes per 4x4 block.
+ // Level 0: 64x64 (256 blocks = 2048 bytes) solid red.
+ // Byte 0 = 0xFF (R1=15, R2=15), Bytes 1..7 = 0x00. Decodes to (255, 2, 2, 255).
+ constexpr int kLevel0Width = 64;
+ constexpr int kLevel0Height = 64;
+ constexpr size_t kLevel0Blocks = (kLevel0Width / 4) * (kLevel0Height / 4);
+ constexpr size_t kLevel0Bytes = kLevel0Blocks * 8;
+ std::vector<uint8_t> redEtcData(kLevel0Bytes, 0);
+ for (size_t b = 0; b < kLevel0Blocks; ++b)
+ {
+ redEtcData[b * 8 + 0] = 0xFF;
+ }
+ glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGB8_ETC2, kLevel0Width, kLevel0Height,
+ 0, static_cast<GLsizei>(kLevel0Bytes), redEtcData.data());
+ ASSERT_GL_NO_ERROR();
+
+ // Level 1: 128x128 (1024 blocks = 8192 bytes) solid green.
+ // Byte 1 = 0xFF (G1=15, G2=15), Bytes 0, 2..7 = 0x00. Decodes to (2, 255, 2, 255).
+ constexpr int kLevel1Width = 128;
+ constexpr int kLevel1Height = 128;
+ constexpr size_t kLevel1Blocks = (kLevel1Width / 4) * (kLevel1Height / 4);
+ constexpr size_t kLevel1Bytes = kLevel1Blocks * 8;
+ std::vector<uint8_t> greenEtcData(kLevel1Bytes, 0);
+ for (size_t b = 0; b < kLevel1Blocks; ++b)
+ {
+ greenEtcData[b * 8 + 1] = 0xFF;
+ }
+ glCompressedTexImage2D(GL_TEXTURE_2D, 1, GL_COMPRESSED_RGB8_ETC2, kLevel1Width, kLevel1Height,
+ 0, static_cast<GLsizei>(kLevel1Bytes), greenEtcData.data());
+ ASSERT_GL_NO_ERROR();
+
+ // Verify Level 0 sampling.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor(255, 2, 2, 255));
+
+ // Verify Level 1 sampling.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+ drawQuad(mProgram, "position", 0.5f);
+ // Bugs in the PowerVR driver prevent full redefinition of oversized compressed mip levels
+ // when level 0 is already defined, because the driver restricts compressed level 1 storage
+ // to the slot allocated by the level 0 chain. Check only the lower-left quadrant of the
+ // rendered output.
+ EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth() / 4, getWindowHeight() / 4,
+ GLColor(2, 255, 2, 255));
+}
+
+// Test that defining an oversized nonzero mip level on an ASTC compressed texture succeeds and
+// renders correctly.
+TEST_P(Texture2DTestES3_OversizedMipLevels, CompressedASTC)
+{
+ const bool hasAstcLdr = IsGLExtensionEnabled("GL_KHR_texture_compression_astc_ldr");
+ const bool hasAstcOes = IsGLExtensionEnabled("GL_OES_texture_compression_astc");
+ ANGLE_SKIP_TEST_IF(!hasAstcLdr && !hasAstcOes);
+
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // ASTC 4x4 void-extent solid color blocks: 16 bytes per block.
+ // Red void-extent block: {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
+ // 0x00, 0x00, 0x00, 0xFF, 0xFF} Green void-extent block: {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF,
+ // 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF}
+ constexpr uint8_t kAstcRedBlock[16] = {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF};
+ constexpr uint8_t kAstcGreenBlock[16] = {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF};
+
+ // Level 0: 64x64 (256 blocks = 4096 bytes) solid red.
+ constexpr int kLevel0Width = 64;
+ constexpr int kLevel0Height = 64;
+ constexpr size_t kLevel0Blocks = (kLevel0Width / 4) * (kLevel0Height / 4);
+ constexpr size_t kLevel0Bytes = kLevel0Blocks * 16;
+ std::vector<uint8_t> redAstcData(kLevel0Bytes);
+ for (size_t b = 0; b < kLevel0Blocks; ++b)
+ {
+ memcpy(&redAstcData[b * 16], kAstcRedBlock, 16);
+ }
+ glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_ASTC_4x4_KHR, kLevel0Width,
+ kLevel0Height, 0, static_cast<GLsizei>(kLevel0Bytes),
+ redAstcData.data());
+ ASSERT_GL_NO_ERROR();
+
+ // Level 1: 128x128 (1024 blocks = 16384 bytes) solid green.
+ constexpr int kLevel1Width = 128;
+ constexpr int kLevel1Height = 128;
+ constexpr size_t kLevel1Blocks = (kLevel1Width / 4) * (kLevel1Height / 4);
+ constexpr size_t kLevel1Bytes = kLevel1Blocks * 16;
+ std::vector<uint8_t> greenAstcData(kLevel1Bytes);
+ for (size_t b = 0; b < kLevel1Blocks; ++b)
+ {
+ memcpy(&greenAstcData[b * 16], kAstcGreenBlock, 16);
+ }
+ glCompressedTexImage2D(GL_TEXTURE_2D, 1, GL_COMPRESSED_RGBA_ASTC_4x4_KHR, kLevel1Width,
+ kLevel1Height, 0, static_cast<GLsizei>(kLevel1Bytes),
+ greenAstcData.data());
+ ASSERT_GL_NO_ERROR();
+
+ // Verify Level 0 sampling.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::red);
+
+ // Verify Level 1 sampling.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+ drawQuad(mProgram, "position", 0.5f);
+ // Bugs in the PowerVR driver prevent full redefinition of oversized compressed mip levels
+ // when level 0 is already defined, because the driver restricts compressed level 1 storage
+ // to the slot allocated by the level 0 chain. Check only the lower-left quadrant of the
+ // rendered output.
+ EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth() / 4, getWindowHeight() / 4, GLColor::green);
+}
+
+// Test that defining an oversized nonzero mip level on a DXT compressed texture succeeds and
+// renders correctly.
+TEST_P(Texture2DTestES3_OversizedMipLevels, CompressedDXT)
+{
+ const bool hasDxt1 = IsGLExtensionEnabled("GL_EXT_texture_compression_dxt1");
+ const bool hasS3tc = IsGLExtensionEnabled("GL_EXT_texture_compression_s3tc");
+ const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_texture_compression_dxt1");
+ ANGLE_SKIP_TEST_IF(!hasDxt1 && !hasS3tc && !hasAngle);
+
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // DXT1: 8 bytes per 4x4 block.
+ // Red block: {0x00, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
+ // Green block: {0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
+ constexpr uint8_t kDxtRedBlock[8] = {0x00, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+ constexpr uint8_t kDxtGreenBlock[8] = {0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+ // Level 0: 64x64 (256 blocks = 2048 bytes) solid red.
+ constexpr int kLevel0Width = 64;
+ constexpr int kLevel0Height = 64;
+ constexpr size_t kLevel0Blocks = (kLevel0Width / 4) * (kLevel0Height / 4);
+ constexpr size_t kLevel0Bytes = kLevel0Blocks * 8;
+ std::vector<uint8_t> redDxtData(kLevel0Bytes);
+ for (size_t b = 0; b < kLevel0Blocks; ++b)
+ {
+ memcpy(&redDxtData[b * 8], kDxtRedBlock, 8);
+ }
+ glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, kLevel0Width,
+ kLevel0Height, 0, static_cast<GLsizei>(kLevel0Bytes), redDxtData.data());
+ ASSERT_GL_NO_ERROR();
+
+ // Level 1: 128x128 (1024 blocks = 8192 bytes) solid green.
+ constexpr int kLevel1Width = 128;
+ constexpr int kLevel1Height = 128;
+ constexpr size_t kLevel1Blocks = (kLevel1Width / 4) * (kLevel1Height / 4);
+ constexpr size_t kLevel1Bytes = kLevel1Blocks * 8;
+ std::vector<uint8_t> greenDxtData(kLevel1Bytes);
+ for (size_t b = 0; b < kLevel1Blocks; ++b)
+ {
+ memcpy(&greenDxtData[b * 8], kDxtGreenBlock, 8);
+ }
+ glCompressedTexImage2D(GL_TEXTURE_2D, 1, GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, kLevel1Width,
+ kLevel1Height, 0, static_cast<GLsizei>(kLevel1Bytes),
+ greenDxtData.data());
+ ASSERT_GL_NO_ERROR();
+
+ // Verify Level 0 sampling.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::red);
+
+ // Verify Level 1 sampling.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+ drawQuad(mProgram, "position", 0.5f);
+ // Bugs in the PowerVR driver prevent full redefinition of oversized compressed mip levels
+ // when level 0 is already defined, because the driver restricts compressed level 1 storage
+ // to the slot allocated by the level 0 chain. Check only the lower-left quadrant of the
+ // rendered output.
+ EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth() / 4, getWindowHeight() / 4, GLColor::green);
+}
+
// Test that robust init via nullptr-upload to texture after invalidating an image with emulated
// alpha works.
TEST_P(Texture2DTestES3RobustInit, InvalidateEmulatedAlphaThenInitViaEmptyUpload)
@@ -24061,6 +24311,12 @@
ES3_OPENGLES().enable(Feature::ResetTexStorage2DBaseLevel),
ES3_OPENGLES().disable(Feature::ResetTexStorage2DBaseLevel));
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Texture2DTestES3_OversizedMipLevels);
+ANGLE_INSTANTIATE_TEST_ES3_AND(
+ Texture2DTestES3_OversizedMipLevels,
+ ES3_OPENGL().enable(Feature::UploadOversizedMipLevelsViaUnpackBuffer),
+ ES3_OPENGLES().enable(Feature::UploadOversizedMipLevelsViaUnpackBuffer));
+
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TextureSizeLimitTest);
ANGLE_INSTANTIATE_TEST(TextureSizeLimitTest,
ES2_D3D11().enable(Feature::LimitMaxTextureBytesTo1MB),