From a8bebe1a3c2f00b369fef0ed5c73a1795d84b40c Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 10:44:52 -0400 Subject: [PATCH] [Fix, Test] (GLImpl, GLState): refuse a compressed texture in glClearTexImage/glClearTexSubImage --- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 108 +++++++++++++----- .../GLState/TextureState/MipmapStorage.cpp | 15 +++ .../GLState/TextureState/MipmapStorage.h | 13 +++ .../TextureState/MipmapUploadTargetArray.h | 10 ++ .../GLState/TextureState/TextureObject.cpp | 12 ++ .../GLState/TextureState/TextureObject.h | 12 ++ .../TextureState/TextureObject2DCube.cpp | 12 ++ .../TextureState/TextureObject2DCube.h | 4 + MobileGL/MG_Test/Texture/TextureTest.cpp | 34 ++++++ 9 files changed, 194 insertions(+), 26 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 3ffea8c3..c690d986 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -691,6 +691,34 @@ namespace MobileGL::MG_Impl::GLImpl { return textureObject; } + // Whether a raw internalformat enum names a compressed format - the question GL asks whenever an + // entry point is forbidden on a compressed image: glTexStorage3D on TEXTURE_3D (no + // block-compressed format is defined for a three-dimensional image, so it is INVALID_OPERATION + // rather than the INVALID_ENUM an unknown sized format gets - GL 4.6 core 8.19 / Khronos bug + // 11239, KHR-GLxx.texture_storage.compressed_data) and the clear-texture pair (8.19 again). + // Written against the enum ranges rather than a name list because the families are contiguous + // and MobileGL's own internal-format enum drops the ones it cannot carry, which would make this + // check silently narrower than the API surface. + static Bool IsCompressedGLInternalFormat(GLenum internalformat) { + switch (internalformat) { + case 0x8225: // GL_COMPRESSED_RED + case 0x8226: // GL_COMPRESSED_RG + case 0x84ED: // GL_COMPRESSED_RGB + case 0x84EE: // GL_COMPRESSED_RGBA + case 0x8C48: // GL_COMPRESSED_SRGB + case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA + return true; + default: + break; + } + return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT + (internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC + (internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC + (internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC + (internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR + (internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB + } + namespace { void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) { MG_State::pGLContext->RecordError( @@ -726,6 +754,21 @@ namespace MobileGL::MG_Impl::GLImpl { std::format("Texture level {} is not defined.", level)); return nullptr; } + // GL 4.6 core 8.19: a compressed internal format is INVALID_OPERATION for both clear + // entry points. Two tags to ask, because they answer different questions: the stored + // one covers a level glCompressedTexImage* or a SPECIFIC compressed internalformat + // defined, the requested one covers the six generic GL_COMPRESSED_* enums that MobileGL + // deliberately backs with uncompressed storage (see MipmapStorage) and that would + // otherwise look like an ordinary RGBA8 image by the time the clear runs. + const auto& uploadTargets = mipmapTexture->GetUploadTargets(); + if (!uploadTargets.empty() && + (mipmapTexture->GetMipmapCompressedFormat(uploadTargets[0], static_cast(level)) != GL_NONE || + mipmapTexture->GetMipmapRequestedCompressedFormat(uploadTargets[0], static_cast(level)) != + GL_NONE)) { + RecordClearTextureError(caller, ErrorCode::InvalidOperation, + "Compressed textures cannot be cleared."); + return nullptr; + } return mipmapTexture; } @@ -2210,6 +2253,13 @@ namespace MobileGL::MG_Impl::GLImpl { textureUploadTarget, level, static_cast(internalformat), nullptr, MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth})); } + // Also after AllocateStorage, which clears it. Records the generic GL_COMPRESSED_* + // enums too, which the tag above deliberately skips - glClearTexImage has to refuse + // them all (GL 4.6 core 8.19). + if (IsCompressedGLInternalFormat(static_cast(internalformat))) { + textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level, + static_cast(internalformat)); + } } if (!originalPixels) { @@ -2356,6 +2406,13 @@ namespace MobileGL::MG_Impl::GLImpl { textureUploadTarget, level, static_cast(internalformat), nullptr, MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1})); } + // Also after AllocateStorage, which clears it. Records the generic GL_COMPRESSED_* + // enums too, which the tag above deliberately skips - glClearTexImage has to refuse + // them all (GL 4.6 core 8.19). + if (IsCompressedGLInternalFormat(static_cast(internalformat))) { + textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level, + static_cast(internalformat)); + } } if (!originalPixels) { @@ -2444,6 +2501,13 @@ namespace MobileGL::MG_Impl::GLImpl { if (!isProxy) { DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes}); + // After AllocateStorage, which clears the tag. No block-compressed format has a 1D + // layout, so only the specific-format tag the 2D/3D paths record is skipped here - the + // request itself still has to be remembered for glClearTexImage (GL 4.6 core 8.19). + if (IsCompressedGLInternalFormat(static_cast(internalFormat))) { + textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level, + static_cast(internalFormat)); + } } if (!originalPixels) { @@ -4542,6 +4606,12 @@ namespace MobileGL::MG_Impl::GLImpl { const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, 1, 1); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize}); textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false); + if (IsCompressedGLInternalFormat(internalformat)) { + // After AllocateStorage, which clears the tag. See TexImage1D_State: no compressed + // format has a 1D block layout, but glClearTexImage still has to refuse the request. + textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, + static_cast(level), internalformat); + } } // Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a // longer pre-existing chain has to be dropped explicitly. @@ -4610,6 +4680,12 @@ namespace MobileGL::MG_Impl::GLImpl { MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {levelWidth, levelHeight, 1})); } + if (IsCompressedGLInternalFormat(internalformat)) { + // Also after AllocateStorage. The generic enums land here and nowhere above, + // and glClearTexImage has to refuse them too (GL 4.6 core 8.19). + textureMipmapObject->SetMipmapRequestedCompressedFormat(uploadTarget, + static_cast(level), internalformat); + } } // See TextureStorage1D. textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast(levels)); @@ -4617,32 +4693,6 @@ namespace MobileGL::MG_Impl::GLImpl { textureObject->SetImmutableLevels(static_cast(levels)); } - // No block-compressed format is defined for a three-dimensional image, so glTexStorage3D on - // TEXTURE_3D must reject one - and with INVALID_OPERATION, not the INVALID_ENUM an unknown - // sized format gets (GL 4.6 core 8.19 / Khronos bug 11239, KHR-GLxx.texture_storage - // .compressed_data). Written against the enum ranges rather than a name list because the - // families are contiguous and MobileGL's own internal-format enum drops the ones it cannot - // carry, which would make this check silently narrower than the API surface. - static Bool IsCompressedGLInternalFormat(GLenum internalformat) { - switch (internalformat) { - case 0x8225: // GL_COMPRESSED_RED - case 0x8226: // GL_COMPRESSED_RG - case 0x84ED: // GL_COMPRESSED_RGB - case 0x84EE: // GL_COMPRESSED_RGBA - case 0x8C48: // GL_COMPRESSED_SRGB - case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA - return true; - default: - break; - } - return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT - (internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC - (internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC - (internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC - (internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR - (internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB - } - void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth) { auto textureObject = GetTextureObjectByName(texture, __func__); @@ -4705,6 +4755,12 @@ namespace MobileGL::MG_Impl::GLImpl { MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {levelWidth, levelHeight, levelDepth})); } + if (IsCompressedGLInternalFormat(internalformat)) { + // Also after AllocateStorage. The generic enums land here and nowhere above, + // and glClearTexImage has to refuse them too (GL 4.6 core 8.19). + textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, + static_cast(level), internalformat); + } } // See TextureStorage1D. textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast(levels)); diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp index 0497edaa..58affe99 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp @@ -48,6 +48,7 @@ namespace MobileGL { m_dirtyRects.resize(requiredLevelCount); m_compressedData.resize(requiredLevelCount); m_compressedFormats.resize(requiredLevelCount, GL_NONE); + m_requestedCompressedFormats.resize(requiredLevelCount, GL_NONE); } m_texelSizes[level] = input.texelSize; @@ -79,6 +80,9 @@ namespace MobileGL { m_compressedFormats[level] = GL_NONE; m_compressedData[level].clear(); m_compressedData[level].shrink_to_fit(); + // Same story for the requested-format tag: a respecified level is whatever this + // call asked for, and the compressed entry points re-arm it right afterwards. + m_requestedCompressedFormats[level] = GL_NONE; } void MipmapStorage::SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size) { @@ -110,6 +114,16 @@ namespace MobileGL { return m_compressedData[level].data(); } + void MipmapStorage::SetRequestedCompressedFormat(Uint level, GLenum internalFormat) { + if (level >= m_requestedCompressedFormats.size()) return; + m_requestedCompressedFormats[level] = internalFormat; + } + + GLenum MipmapStorage::GetRequestedCompressedFormat(Uint level) const { + if (level >= m_requestedCompressedFormats.size()) return GL_NONE; + return m_requestedCompressedFormats[level]; + } + void MipmapStorage::TruncateToLevelCount(SizeT levelCount) { if (levelCount >= m_data.size()) return; @@ -120,6 +134,7 @@ namespace MobileGL { m_dirtyRects.resize(levelCount); m_compressedData.resize(levelCount); m_compressedFormats.resize(levelCount); + m_requestedCompressedFormats.resize(levelCount); } void MipmapStorage::UpdateSubData(Uint level, DataPtr input) { diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h index fd6b8302..fe632db4 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h @@ -96,6 +96,18 @@ namespace MobileGL { SizeT GetCompressedByteSize(Uint level) const; const void* MapCompressedData(Uint level) const; + // The compressed internalformat the application ASKED for, which is not the same + // question as the one above: the six generic GL_COMPRESSED_* enums let the + // implementation choose, MobileGL chooses uncompressed storage, and the level is + // deliberately left untagged so GL_TEXTURE_COMPRESSED keeps answering false and + // glGetCompressedTexImage is not handed a blob nothing ever compressed. The entry + // points that must refuse a compressed image outright (glClearTexImage / + // glClearTexSubImage, GL 4.6 core 8.19) still need to know, so the request is + // recorded separately. Set right after AllocateLevel, which clears it. + void SetRequestedCompressedFormat(Uint level, GLenum internalFormat); + // GL_NONE when the level was not requested with a compressed internalformat. + GLenum GetRequestedCompressedFormat(Uint level) const; + protected: // Insert one clamped, non-empty write box, keeping the list disjoint // and bounded (see kMaxDirtyRects). @@ -115,6 +127,7 @@ namespace MobileGL { Vector> m_dirtyRects; Vector> m_compressedData; Vector m_compressedFormats; + Vector m_requestedCompressedFormats; }; } // namespace GLState } // namespace MG_State diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h b/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h index 1f6980d4..5142422c 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h +++ b/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h @@ -111,6 +111,16 @@ namespace MobileGL { return m_storage[targetIndex].MapCompressedData(level); } + void SetRequestedCompressedFormat(Uint targetIndex, Uint level, GLenum internalFormat) { + MOBILEGL_ASSERT(targetIndex < TargetCount, "SetRequestedCompressedFormat: target invalid"); + m_storage[targetIndex].SetRequestedCompressedFormat(level, internalFormat); + } + + GLenum GetRequestedCompressedFormat(Uint targetIndex, Uint level) const { + MOBILEGL_ASSERT(targetIndex < TargetCount, "GetRequestedCompressedFormat: target invalid"); + return m_storage[targetIndex].GetRequestedCompressedFormat(level); + } + protected: Array m_storage; }; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index b5bfa3f2..86c1a4a2 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -373,6 +373,18 @@ namespace MobileGL { return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); } + void TextureObjectWithOneMipmap::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, + Uint mipmapLevel, GLenum internalFormat) { + m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, + internalFormat); + } + + GLenum TextureObjectWithOneMipmap::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const { + return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), + mipmapLevel); + } + IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const { if (m_textureStorage.GetLevelCount() == 0) { return {0, 0, 0}; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index 9e346014..742e5e78 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -220,6 +220,15 @@ namespace MobileGL::MG_State::GLState { virtual GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; virtual SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; virtual const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; + + // The compressed internalformat the level was REQUESTED with, recorded even when MobileGL + // answered it with uncompressed storage (the six generic GL_COMPRESSED_* enums) - see + // MipmapStorage. Only the entry points GL forbids on a compressed image read it. + virtual void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel, + GLenum internalFormat) = 0; + // GL_NONE when the level was not requested with a compressed internalformat. + virtual GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const = 0; }; // Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the @@ -286,6 +295,9 @@ namespace MobileGL::MG_State::GLState { GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; + void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel, + GLenum internalFormat) override; + GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; IntVec3 GetBaseSize() const override; Bool IsComplete() const override; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp index a1997e4c..765477c5 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp @@ -96,6 +96,18 @@ namespace MobileGL { return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); } + void TextureObject2DCube::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, + Uint mipmapLevel, GLenum internalFormat) { + m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, + internalFormat); + } + + GLenum TextureObject2DCube::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const { + return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), + mipmapLevel); + } + Uint TextureObject2DCube::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const { MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target && target <= TextureUploadTarget::CubeMapNegativeZ, diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h index 547ea0e9..d705db02 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h @@ -39,6 +39,10 @@ namespace MobileGL { SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; + void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel, + GLenum internalFormat) override; + GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const override; IntVec3 GetBaseSize() const override; Bool IsComplete() const override; diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index de58330d..b663b3f7 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -377,6 +377,40 @@ TEST_F(TextureTest, ClearTexImageErrorContracts) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast(GL_INVALID_ENUM)); } +// GL 4.6 core 8.19: a compressed internal format is INVALID_OPERATION for both clear entry points. +// The generic GL_COMPRESSED_* enums are the half that needs its own tag - MobileGL answers them +// with uncompressed storage on purpose, so by the time the clear runs the level looks like any +// other RGBA8 image unless the REQUEST was recorded alongside it. +TEST_F(TextureTest, ClearTexImageRejectsCompressedTextures) { + GLuint genericTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &genericTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, genericTexture); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::ClearTexImage(genericTexture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + ExpectSingleGlError(GL_INVALID_OPERATION); + MG_Impl::GLImpl::ClearTexSubImage(genericTexture, 0, 0, 0, 0, 4, 4, 1, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // A specific compressed internalformat is refused through the tag the level already carried... + GLuint specificTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &specificTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, specificTexture); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, + nullptr); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::ClearTexImage(specificTexture, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // ...and respecifying the level with an uncompressed format makes it clearable again, because + // AllocateStorage clears both tags. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_R8, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::ClearTexImage(specificTexture, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + // GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT is float state that must answer every numeric query: GetFloatv // is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default. TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) {