CVE-2026-5272
Overview
Background
- ANGLE GL backend (`TextureGL`)
- ANGLE’s translation of WebGL/GLES calls onto a native OpenGL driver. It keeps its own shadow of each texture level’s size and format (
mState.getImageDesc) and validates WebGL calls against that shadow, not the driver’s state. - `glGenerateMipmap` level range
- Generating mipmaps should redefine every level from
effectiveBaseLevel + 1throughgetMipmapMaxLevel()to the base level’s format with halved dimensions, and must not touch levels outside that range. - Mutable vs immutable textures
- Textures defined with
glTexImage*(mutable,!mState.getImmutableFormat()) can have each level independently redefined with arbitrary sizes and formats, including compressed formats likeGL_COMPRESSED_RED_RGTC1_EXT. Immutable (glTexStorage*) textures cannot. - ANGLE feature workaround
- A named flag in
gl_features.json/FeaturesGLthat is turned on for specific drivers viaANGLE_FEATURE_CONDITION. HererecreateMipmapLevelsBeforeGenerateis enabled whenisMesa.
Root Cause Analysis
The new test and the renderergl_utils.cpp comment describe a Mesa driver bug. With mismatched level formats, glGenerateMipmap can silently no-op. ANGLE then believes the levels were recreated in the base format, while the driver keeps the old shape and format (here RGTC1 at level 1). On a later glGenerateMipmap, issued with GL_TEXTURE_BASE_LEVEL = 1, ANGLE’s validation lets the call through because it thinks level 1 is RGBA. Mesa instead processes the call as RGTC1 and, per the test comment, “mistakenly process[es] levels 5 and 6 above (out of mipmap range)”. The test shows a crash, which the advisory classifies as a heap buffer overflow in the GPU process. The broken invariant is that ANGLE’s shadow level state must match the driver’s real level allocations whenever glGenerateMipmap is called.
The fix extracts the existing manual level-allocation loop into TextureGL::allocateMipmapLevelsForGeneration, which is no longer 2D-only and now handles _3D, _2DArray, CubeMapArray and every CubeMap face. When the Mesa workaround is enabled on a mutable texture, it calls this function before any generation. Every level in [effectiveBaseLevel+1, maxLevel] whose size or format differs from the expected value is then explicitly re-specified with texImage2D/texImage3D (with no pixel unpack buffer bound), so the driver’s level state matches ANGLE’s before mipmap generation runs.
glGenerateMipmap to redefine all in-range levels. A silent Mesa no-op left driver-side levels with stale compressed formats or sizes that ANGLE’s validation no longer saw. The fix explicitly recreates every in-range level in the base format before calling the driver on Mesa, so ANGLE and driver state cannot diverge.Attack Path
- Craft mismatched mip levels
WebGL2 content defines a mutable
TEXTURE_2Dwith out-of-range levels (5, 6), a compressed RGTC1 image at level 1, then RGBA images at levels 0 and 2, as inMipmapTestES3.MismatchingLevelFormats. - Trigger the silent no-op
The first
glGenerateMipmapis dropped by Mesa. ANGLE now records level 1 as RGBA while the driver keeps it as RGTC1 16x12. - Pass validation with stale driver state
Script re-uploads level 0, sets
GL_TEXTURE_BASE_LEVELto 1, and callsglGenerateMipmapagain. ANGLE allows it, but Mesa generates from an RGTC1 base and walks past the mipmap range into levels 5 and 6. - GPU-process memory corruption The driver’s mismatched level processing overflows a heap buffer in the GPU process, according to the advisory and the test’s “crashes” comment. Turning this into controlled corruption or code execution is not shown (inferred).
Impact Assessment
GL_EXT_texture_compression_rgtc, the extension the test needs. The advisory rates it High (“Heap buffer overflow in GPU”) and does not say it was exploited in the wild.Files Changed
include/platform/autogen/FeaturesGL_autogen.hinclude/platform/gl_features.jsonsrc/libANGLE/renderer/gl/TextureGL.cppsrc/libANGLE/renderer/gl/TextureGL.hsrc/libANGLE/renderer/gl/renderergl_utils.cppsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/gl_tests/MipmapTest.cpputil/autogen/angle_features_autogen.cpputil/autogen/angle_features_autogen.h
Audit Directions
- Shadow-state vs driver-state divergenceLook for other ANGLE GL-backend paths that update
mStateassuming a driver call succeeded (copyTexImage,generateMipmapwithuseIntermediateTextureForGenerateMipmap,texSubImagewith compressed formats,glCopyTexSubImageafter redefinition). A silent driver no-op there would leave validation trusting stale level metadata. - Mixed compressed/uncompressed levelsFuzz mutable textures whose levels mix compressed (RGTC, ETC, ASTC, S3TC) and uncompressed formats, combined with
TEXTURE_BASE_LEVEL/MAX_LEVELchanges, across all ANGLE backends (GL, Vulkan, Metal, D3D). Out-of-range levels are a recurring trigger for driver-side miscalculation. - Workaround scopingThe fix is gated on
isMesaand!getImmutableFormat(). Check whether other drivers (the test is already skipped on Windows Intel OpenGL and Mac OpenGL/Metal) show similar silent-failure behavior that remains unmitigated.
Patch
diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json
index c7bd3fb..41f6567 100644
--- a/include/platform/gl_features.json
+++ b/include/platform/gl_features.json
@@ -878,6 +878,15 @@
"issue": "http://crbug.com/475877320"
},
{
+ "name": "recreate_mipmap_levels_before_generate",
+ "category": "Workarounds",
+ "description": [
+ "Before calling glGenerateMipmap on a mutable texture, recreate levels that the driver is ",
+ "supposed to recreate to work around driver bugs."
+ ],
+ "issue": "http://crbug.com/491732188"
+ },
+ {
"name": "limit_max_buffer_size_to_1gb",
"category": "Workarounds",
"description": [
diff --git a/src/libANGLE/renderer/gl/TextureGL.cpp b/src/libANGLE/renderer/gl/TextureGL.cpp
index 9a2c293..05299c9 100644
--- a/src/libANGLE/renderer/gl/TextureGL.cpp
+++ b/src/libANGLE/renderer/gl/TextureGL.cpp
@@ -1497,6 +1497,13 @@
StateManagerGL *stateManager = GetStateManagerGL(context);
const angle::FeaturesGL &features = GetFeaturesGL(context);
+ bool recreateMipmapLevelsBeforeGenerate =
+ features.recreateMipmapLevelsBeforeGenerate.enabled && !mState.getImmutableFormat();
+ if (recreateMipmapLevelsBeforeGenerate)
+ {
+ ANGLE_TRY(allocateMipmapLevelsForGeneration(context));
+ }
+
const GLuint effectiveBaseLevel = mState.getEffectiveBaseLevel();
const GLuint maxLevel = mState.getMipmapMaxLevel();
@@ -1513,34 +1520,19 @@
nativegl::SupportsNativeRendering(functions, mState.getType(),
baseLevelInfo.nativeInternalFormat))))
{
- nativegl::TexImageFormat texImageFormat = nativegl::GetTexImageFormat(
- functions, features, baseLevelInternalFormat.internalFormat,
- baseLevelInternalFormat.format, baseLevelInternalFormat.type);
-
// Manually allocate the mip levels of this texture if they don't exist
- GLuint levelCount = maxLevel - effectiveBaseLevel + 1;
- for (GLuint levelIdx = 1; levelIdx < levelCount; levelIdx++)
+ // This might already be done above if recreateMipmapLevelsBeforeGenerate is in effect.
+ if (!recreateMipmapLevelsBeforeGenerate)
{
- gl::Extents levelSize(std::max(baseLevelDesc.size.width >> levelIdx, 1),
- std::max(baseLevelDesc.size.height >> levelIdx, 1), 1);
-
- const gl::ImageDesc &levelDesc =
- mState.getImageDesc(gl::TextureTarget::_2D, effectiveBaseLevel + levelIdx);
-
- if (levelDesc.size != levelSize || *levelDesc.format.info != baseLevelInternalFormat)
- {
- // Make sure no pixel unpack buffer is bound
- stateManager->bindBuffer(gl::BufferBinding::PixelUnpack, 0);
-
- ANGLE_GL_TRY_ALWAYS_CHECK(
- context, functions->texImage2D(
- ToGLenum(getType()), effectiveBaseLevel + levelIdx,
- texImageFormat.internalFormat, levelSize.width, levelSize.height,
- 0, texImageFormat.format, texImageFormat.type, nullptr));
- }
+ ANGLE_TRY(allocateMipmapLevelsForGeneration(context));
}
// Use the blitter to generate the mips
+ const nativegl::TexImageFormat texImageFormat = nativegl::GetTexImageFormat(
+ functions, features, baseLevelInternalFormat.internalFormat,
+ baseLevelInternalFormat.format, baseLevelInternalFormat.type);
+ const GLuint levelCount = maxLevel - effectiveBaseLevel + 1;
+
BlitGL *blitter = GetBlitGL(context);
if (baseLevelInternalFormat.colorEncoding == GL_SRGB)
{
@@ -1565,6 +1557,84 @@
return angle::Result::Continue;
}
+angle::Result TextureGL::allocateMipmapLevelsForGeneration(const gl::Context *context)
+{
+ const FunctionsGL *functions = GetFunctionsGL(context);
+ StateManagerGL *stateManager = GetStateManagerGL(context);
+ const angle::FeaturesGL &features = GetFeaturesGL(context);
+
+ const GLuint effectiveBaseLevel = mState.getEffectiveBaseLevel();
+ const GLuint maxLevel = mState.getMipmapMaxLevel();
+
+ const gl::ImageDesc &baseLevelDesc = mState.getBaseLevelDesc();
+ const gl::InternalFormat &baseLevelInternalFormat = *baseLevelDesc.format.info;
+
+ nativegl::TexImageFormat texImageFormat =
+ nativegl::GetTexImageFormat(functions, features, baseLevelInternalFormat.internalFormat,
+ baseLevelInternalFormat.format, baseLevelInternalFormat.type);
+
+ const bool is3D = getType() == gl::TextureType::_3D;
+ const gl::TextureTarget target = getType() == gl::TextureType::CubeMap
+ ? gl::TextureTarget::CubeMapPositiveX
+ : NonCubeTextureTypeToTarget(getType());
+
+ // Manually allocate the mip levels of this texture if they don't exist
+ GLuint levelCount = maxLevel - effectiveBaseLevel + 1;
+ for (GLuint levelIdx = 1; levelIdx < levelCount; levelIdx++)
+ {
+ gl::Extents levelSize(
+ std::max(baseLevelDesc.size.width >> levelIdx, 1),
+ std::max(baseLevelDesc.size.height >> levelIdx, 1),
+ is3D ? std::max(baseLevelDesc.size.depth >> levelIdx, 1) : baseLevelDesc.size.depth);
+
+ const gl::ImageDesc &levelDesc = mState.getImageDesc(target, effectiveBaseLevel + levelIdx);
+
+ if (levelDesc.size != levelSize || *levelDesc.format.info != baseLevelInternalFormat)
+ {
+ // Make sure no pixel unpack buffer is bound
+ stateManager->bindBuffer(gl::BufferBinding::PixelUnpack, 0);
+
+ switch (getType())
+ {
+ case gl::TextureType::_2D:
+ ANGLE_GL_TRY_ALWAYS_CHECK(
+ context,
+ functions->texImage2D(ToGLenum(getType()), effectiveBaseLevel + levelIdx,
+ texImageFormat.internalFormat, levelSize.width,
+ levelSize.height, 0, texImageFormat.format,
+ texImageFormat.type, nullptr));
+ break;
+ case gl::TextureType::_3D:
+ case gl::TextureType::_2DArray:
+ case gl::TextureType::CubeMapArray:
+ ANGLE_GL_TRY_ALWAYS_CHECK(
+ context,
+ functions->texImage3D(ToGLenum(getType()), effectiveBaseLevel + levelIdx,
+ texImageFormat.internalFormat, levelSize.width,
+ levelSize.height, levelSize.depth, 0,
+ texImageFormat.format, texImageFormat.type, nullptr));
+ break;
+ case gl::TextureType::CubeMap:
+ for (gl::TextureTarget face : gl::AllCubeFaceTextureTargets())
+ {
+ ANGLE_GL_TRY_ALWAYS_CHECK(
+ context,
+ functions->texImage2D(ToGLenum(face), effectiveBaseLevel + levelIdx,
+ texImageFormat.internalFormat, levelSize.width,
+ levelSize.height, 0, texImageFormat.format,
+ texImageFormat.type, nullptr));
+ }
+ break;
+ default:
+ // Cannot call glGenerateMipmap with any other texture type
+ UNREACHABLE();
+ break;
+ }
+ }
+ }
+ return angle::Result::Continue;
+}
+
angle::Result TextureGL::clearImage(const gl::Context *context,
GLint level,
GLenum format,
diff --git a/src/libANGLE/renderer/gl/TextureGL.h b/src/libANGLE/renderer/gl/TextureGL.h
index 95af317..03f4711 100644
--- a/src/libANGLE/renderer/gl/TextureGL.h
+++ b/src/libANGLE/renderer/gl/TextureGL.h
@@ -267,6 +267,7 @@
const gl::PixelUnpackState &unpack,
const gl::Buffer *unpackBuffer,
const uint8_t *pixels);
+ angle::Result allocateMipmapLevelsForGeneration(const gl::Context *context);
angle::Result syncTextureStateSwizzle(const gl::Context *context,
const FunctionsGL *functions,
diff --git a/src/libANGLE/renderer/gl/renderergl_utils.cpp b/src/libANGLE/renderer/gl/renderergl_utils.cpp
index a4cd6db..f9334e8 100644
--- a/src/libANGLE/renderer/gl/renderergl_utils.cpp
+++ b/src/libANGLE/renderer/gl/renderergl_utils.cpp
@@ -2690,6 +2690,10 @@
// exposed.
ANGLE_FEATURE_CONDITION(features, bgraTexImageFormatsBroken, !isMesa && isQualcomm);
+ // glGenerateMipmap may silently fail on mesa, leaving mips that are expected to be recreated to
+ // match the base level in their original shape, hidden from ANGLE and its validation.
+ ANGLE_FEATURE_CONDITION(features, recreateMipmapLevelsBeforeGenerate, isMesa);
+
// https://github.com/flutter/flutter/issues/47164
// https://github.com/flutter/flutter/issues/47804
// Some devices expose the QCOM tiled memory extension string but don't actually provide the
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index aa9ac1f..71c19db 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -49,7 +49,6 @@
493505759 PIXEL10 GLES : SRGBTextureTestES3.SRGBDecodeTexelFetchInHelper/* = SKIP
493505759 PIXEL10 GLES : SRGBTextureTestES3.SRGBDecodeTexelFetchWithSamplerInStruct/* = SKIP
-
// Advanced blend ops currently fail on Windows/Intel.
376899587 WIN INTEL VULKAN : AdvancedBlendTest.*/* = SKIP
@@ -171,6 +170,7 @@
42266866 WIN INTEL OPENGL : GLSLTest_ES3.LargeInterfaceBlockArray/* = SKIP
42266866 WIN INTEL OPENGL : GLSLTest_ES3.LargeInterfaceBlockNestedArray/* = SKIP
463961767 WIN INTEL OPENGL : EGLSurfacelessContextTest.Switcheroo/* = SKIP
+494350632 WIN INTEL OPENGL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
// Failures on Intel Alder Lake S UHD Graphics 770 (8086:4680-31.0.101.5333)
352085732 WIN INTEL OPENGL : GeometryShaderTest.LayeredFramebufferClear2DArrayColor/* = SKIP
@@ -473,6 +473,8 @@
464927523 METAL : GLSLTest_ES3_PackUnpackEmulation.PackSnorm2x16/* = SKIP
464927523 METAL : GLSLTest_ES3_PackUnpackEmulation.PackUnorm2x16/* = SKIP
42266214 MAC APPLE METAL : Texture2DTest.ManySupersedingTextureUpdates/* = SKIP
+494392011 MAC METAL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
+494341324 MAC OPENGL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
// The workaround is not intended to be enabled in this configuration so
// skip it as the failure is likely a driver bug.
diff --git a/src/tests/gl_tests/MipmapTest.cpp b/src/tests/gl_tests/MipmapTest.cpp
index ce2a53d..cdcc7e2 100644
--- a/src/tests/gl_tests/MipmapTest.cpp
+++ b/src/tests/gl_tests/MipmapTest.cpp
@@ -2470,12 +2470,76 @@
}
}
+// Test glGenerateMipmap in the presence of mismatching level formats. Regression test for a bug in
+// mesa.
+TEST_P(MipmapTestES3, MismatchingLevelFormats)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_texture_compression_rgtc"));
+
+ GLTexture texture;
+ glBindTexture(GL_TEXTURE_2D, texture);
+
+ const std::vector<GLColor> kAllGreen(1000, GLColor::green);
+ const std::vector<GLColor> kAllBlue(1000, GLColor::blue);
+ const std::vector<GLColor> kAllCyan(1000, GLColor::cyan);
+
+ // Create mips at levels that should not be touched by glGenerateMipmap.
+ glTexImage2D(GL_TEXTURE_2D, 5, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllCyan.data());
+ glTexImage2D(GL_TEXTURE_2D, 6, GL_RGBA, 20, 30, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllBlue.data());
+
+ // Create RGTC1 at level 1. A 16x12 image has 4x3 blocks of 8 bytes each.
+ const std::vector<uint8_t> redRGTC1((16 / 4) * (12 / 4) * 8, 0xFF);
+ glCompressedTexImage2D(GL_TEXTURE_2D, 1, GL_COMPRESSED_RED_RGTC1_EXT, 16, 12, 0,
+ static_cast<GLsizei>(redRGTC1.size()), redRGTC1.data());
+
+ // Trigger mesa bug: After uploading a 4x4 image at level 0 and 1x2 at level 2, calling
+ // glGenerateMipmap silently gets nooped. While ANGLE considers mip 1 now to be in RGBA format,
+ // mesa still thinks it's RGTC1 due to the noop.
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllGreen.data());
+ glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, 1, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllBlue.data());
+ glGenerateMipmap(GL_TEXTURE_2D);
+ ASSERT_GL_NO_ERROR();
+
+ // Now dirty mesa's tracking again by reuploading to level 0, then call glGenerateMipmap on
+ // level 1, which ANGLE lets through because that mip is supposed to be RGBA now. Mesa supports
+ // glGenerateMipmap on RGTC1 textures. Where the bug is present, this glGenerateMipmap crashes
+ // by mistakenly processing levels 5 and 6 above (out of mipmap range).
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllBlue.data());
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ glGenerateMipmap(GL_TEXTURE_2D);
+ ASSERT_GL_NO_ERROR();
+
+ // Verify correctness
+ ANGLE_GL_PROGRAM(verify, essl3_shaders::vs::Texture2DLod(), essl3_shaders::fs::Texture2DLod());
+ glUseProgram(verify);
+ const GLint lodLoc = glGetUniformLocation(verify, essl3_shaders::LodUniform());
+
+ glUniform1i(glGetUniformLocation(verify, essl3_shaders::Texture2DUniform()), 0);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+
+ glUniform1f(lodLoc, 0);
+ drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+
+ glUniform1f(lodLoc, 1);
+ drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+
+ glUniform1f(lodLoc, 2);
+ drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
// Use this to select which configurations (e.g. which renderer, which GLES major version) these
// tests should be run against.
ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(
MipmapTest,
ES2_METAL().disable(Feature::AllowGenMultipleMipsPerPass),
- ES2_OPENGLES().enable(Feature::UseIntermediateTextureForGenerateMipmap));
+ ES2_OPENGL().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+ ES2_OPENGLES().enable(Feature::UseIntermediateTextureForGenerateMipmap),
+ ES2_OPENGLES()
+ .enable(Feature::UseIntermediateTextureForGenerateMipmap)
+ .enable(Feature::UseIntermediateTextureForGenerateMipmap));
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Mipmap3DBoxFilterTest);
ANGLE_INSTANTIATE_TEST(Mipmap3DBoxFilterTest,
@@ -2483,7 +2547,13 @@
ES2_METAL().disable(Feature::AllowGenMultipleMipsPerPass));
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MipmapTestES3);
-ANGLE_INSTANTIATE_TEST_ES3_AND(MipmapTestES3, ES3_WEBGPU());
+ANGLE_INSTANTIATE_TEST_ES3_AND(MipmapTestES3,
+ ES3_OPENGL().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+ ES3_OPENGLES().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+ ES3_WEBGPU());
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MipmapTestES31);
-ANGLE_INSTANTIATE_TEST_ES31(MipmapTestES31);
+ANGLE_INSTANTIATE_TEST_ES31_AND(
+ MipmapTestES31,
+ ES31_OPENGL().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+ ES31_OPENGLES().enable(Feature::RecreateMipmapLevelsBeforeGenerate));
diff --git a/include/platform/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h
index d04f3d5..bac58e9 100644
--- a/include/platform/autogen/FeaturesGL_autogen.h
+++ b/include/platform/autogen/FeaturesGL_autogen.h
@@ -674,6 +674,12 @@
&members,
};
+ FeatureInfo recreateMipmapLevelsBeforeGenerate = {
+ "recreateMipmapLevelsBeforeGenerate",
+ FeatureCategory::OpenGLWorkarounds,
+ &members,
+ };
+
FeatureInfo limitMaxBufferSizeTo1gb = {
"limitMaxBufferSizeTo1gb",
FeatureCategory::OpenGLWorkarounds,
diff --git a/util/autogen/angle_features_autogen.cpp b/util/autogen/angle_features_autogen.cpp
index 7028061..5485ec3 100644
--- a/util/autogen/angle_features_autogen.cpp
+++ b/util/autogen/angle_features_autogen.cpp
@@ -283,6 +283,7 @@
{Feature::QueryCounterBitsGeneratesErrors, "queryCounterBitsGeneratesErrors"},
{Feature::ReadPixelsUsingImplementationColorReadFormatForNorm16, "readPixelsUsingImplementationColorReadFormatForNorm16"},
{Feature::ReapplyUBOBindingsAfterUsingBinaryProgram, "reapplyUBOBindingsAfterUsingBinaryProgram"},
+ {Feature::RecreateMipmapLevelsBeforeGenerate, "recreateMipmapLevelsBeforeGenerate"},
{Feature::RecycleVkEvent, "recycleVkEvent"},
{Feature::RegenerateStructNames, "regenerateStructNames"},
{Feature::RemoveDynamicIndexingOfSwizzledVector, "removeDynamicIndexingOfSwizzledVector"},
diff --git a/util/autogen/angle_features_autogen.h b/util/autogen/angle_features_autogen.h
index 89ac914..fdc51c8 100644
--- a/util/autogen/angle_features_autogen.h
+++ b/util/autogen/angle_features_autogen.h
@@ -283,6 +283,7 @@
QueryCounterBitsGeneratesErrors,
ReadPixelsUsingImplementationColorReadFormatForNorm16,
ReapplyUBOBindingsAfterUsingBinaryProgram,
+ RecreateMipmapLevelsBeforeGenerate,
RecycleVkEvent,
RegenerateStructNames,
RemoveDynamicIndexingOfSwizzledVector,
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index aa9ac1f..71c19db 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -49,7 +49,6 @@
493505759 PIXEL10 GLES : SRGBTextureTestES3.SRGBDecodeTexelFetchInHelper/* = SKIP
493505759 PIXEL10 GLES : SRGBTextureTestES3.SRGBDecodeTexelFetchWithSamplerInStruct/* = SKIP
-
// Advanced blend ops currently fail on Windows/Intel.
376899587 WIN INTEL VULKAN : AdvancedBlendTest.*/* = SKIP
@@ -171,6 +170,7 @@
42266866 WIN INTEL OPENGL : GLSLTest_ES3.LargeInterfaceBlockArray/* = SKIP
42266866 WIN INTEL OPENGL : GLSLTest_ES3.LargeInterfaceBlockNestedArray/* = SKIP
463961767 WIN INTEL OPENGL : EGLSurfacelessContextTest.Switcheroo/* = SKIP
+494350632 WIN INTEL OPENGL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
// Failures on Intel Alder Lake S UHD Graphics 770 (8086:4680-31.0.101.5333)
352085732 WIN INTEL OPENGL : GeometryShaderTest.LayeredFramebufferClear2DArrayColor/* = SKIP
@@ -473,6 +473,8 @@
464927523 METAL : GLSLTest_ES3_PackUnpackEmulation.PackSnorm2x16/* = SKIP
464927523 METAL : GLSLTest_ES3_PackUnpackEmulation.PackUnorm2x16/* = SKIP
42266214 MAC APPLE METAL : Texture2DTest.ManySupersedingTextureUpdates/* = SKIP
+494392011 MAC METAL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
+494341324 MAC OPENGL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
// The workaround is not intended to be enabled in this configuration so
// skip it as the failure is likely a driver bug.
diff --git a/src/tests/gl_tests/MipmapTest.cpp b/src/tests/gl_tests/MipmapTest.cpp
index ce2a53d..cdcc7e2 100644
--- a/src/tests/gl_tests/MipmapTest.cpp
+++ b/src/tests/gl_tests/MipmapTest.cpp
@@ -2470,12 +2470,76 @@
}
}
+// Test glGenerateMipmap in the presence of mismatching level formats. Regression test for a bug in
+// mesa.
+TEST_P(MipmapTestES3, MismatchingLevelFormats)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_texture_compression_rgtc"));
+
+ GLTexture texture;
+ glBindTexture(GL_TEXTURE_2D, texture);
+
+ const std::vector<GLColor> kAllGreen(1000, GLColor::green);
+ const std::vector<GLColor> kAllBlue(1000, GLColor::blue);
+ const std::vector<GLColor> kAllCyan(1000, GLColor::cyan);
+
+ // Create mips at levels that should not be touched by glGenerateMipmap.
+ glTexImage2D(GL_TEXTURE_2D, 5, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllCyan.data());
+ glTexImage2D(GL_TEXTURE_2D, 6, GL_RGBA, 20, 30, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllBlue.data());
+
+ // Create RGTC1 at level 1. A 16x12 image has 4x3 blocks of 8 bytes each.
+ const std::vector<uint8_t> redRGTC1((16 / 4) * (12 / 4) * 8, 0xFF);
+ glCompressedTexImage2D(GL_TEXTURE_2D, 1, GL_COMPRESSED_RED_RGTC1_EXT, 16, 12, 0,
+ static_cast<GLsizei>(redRGTC1.size()), redRGTC1.data());
+
+ // Trigger mesa bug: After uploading a 4x4 image at level 0 and 1x2 at level 2, calling
+ // glGenerateMipmap silently gets nooped. While ANGLE considers mip 1 now to be in RGBA format,
+ // mesa still thinks it's RGTC1 due to the noop.
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllGreen.data());
+ glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, 1, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllBlue.data());
+ glGenerateMipmap(GL_TEXTURE_2D);
+ ASSERT_GL_NO_ERROR();
+
+ // Now dirty mesa's tracking again by reuploading to level 0, then call glGenerateMipmap on
+ // level 1, which ANGLE lets through because that mip is supposed to be RGBA now. Mesa supports
+ // glGenerateMipmap on RGTC1 textures. Where the bug is present, this glGenerateMipmap crashes
+ // by mistakenly processing levels 5 and 6 above (out of mipmap range).
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllBlue.data());
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ glGenerateMipmap(GL_TEXTURE_2D);
+ ASSERT_GL_NO_ERROR();
+
+ // Verify correctness
+ ANGLE_GL_PROGRAM(verify, essl3_shaders::vs::Texture2DLod(), essl3_shaders::fs::Texture2DLod());
+ glUseProgram(verify);
+ const GLint lodLoc = glGetUniformLocation(verify, essl3_shaders::LodUniform());
+
+ glUniform1i(glGetUniformLocation(verify, essl3_shaders::Texture2DUniform()), 0);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+
+ glUniform1f(lodLoc, 0);
+ drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+
+ glUniform1f(lodLoc, 1);
+ drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+
+ glUniform1f(lodLoc, 2);
+ drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
// Use this to select which configurations (e.g. which renderer, which GLES major version) these
// tests should be run against.
ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(
MipmapTest,
ES2_METAL().disable(Feature::AllowGenMultipleMipsPerPass),
- ES2_OPENGLES().enable(Feature::UseIntermediateTextureForGenerateMipmap));
+ ES2_OPENGL().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+ ES2_OPENGLES().enable(Feature::UseIntermediateTextureForGenerateMipmap),
+ ES2_OPENGLES()
+ .enable(Feature::UseIntermediateTextureForGenerateMipmap)
+ .enable(Feature::UseIntermediateTextureForGenerateMipmap));
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Mipmap3DBoxFilterTest);
ANGLE_INSTANTIATE_TEST(Mipmap3DBoxFilterTest,
@@ -2483,7 +2547,13 @@
ES2_METAL().disable(Feature::AllowGenMultipleMipsPerPass));
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MipmapTestES3);
-ANGLE_INSTANTIATE_TEST_ES3_AND(MipmapTestES3, ES3_WEBGPU());
+ANGLE_INSTANTIATE_TEST_ES3_AND(MipmapTestES3,
+ ES3_OPENGL().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+ ES3_OPENGLES().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+ ES3_WEBGPU());
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MipmapTestES31);
-ANGLE_INSTANTIATE_TEST_ES31(MipmapTestES31);
+ANGLE_INSTANTIATE_TEST_ES31_AND(
+ MipmapTestES31,
+ ES31_OPENGL().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+ ES31_OPENGLES().enable(Feature::RecreateMipmapLevelsBeforeGenerate));