[Fix, Test] (GLImpl, GLState): refuse a compressed texture in glClearTexImage/glClearTexSubImage

This commit is contained in:
2026-08-20 10:44:52 -04:00
parent 85cd6913b3
commit a8bebe1a3c
9 changed files with 194 additions and 26 deletions
+82 -26
View File
@@ -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<Uint>(level)) != GL_NONE ||
mipmapTexture->GetMipmapRequestedCompressedFormat(uploadTargets[0], static_cast<Uint>(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<GLenum>(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<GLenum>(internalformat))) {
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
static_cast<GLenum>(internalformat));
}
}
if (!originalPixels) {
@@ -2356,6 +2406,13 @@ namespace MobileGL::MG_Impl::GLImpl {
textureUploadTarget, level, static_cast<GLenum>(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<GLenum>(internalformat))) {
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
static_cast<GLenum>(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<GLenum>(internalFormat))) {
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
static_cast<GLenum>(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<Uint>(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<Uint>(level), internalformat);
}
}
// See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
@@ -4617,32 +4693,6 @@ namespace MobileGL::MG_Impl::GLImpl {
textureObject->SetImmutableLevels(static_cast<Uint>(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<Uint>(level), internalformat);
}
}
// See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
@@ -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) {
@@ -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<Vector<MipmapDirtyRegion>> m_dirtyRects;
Vector<Vector<Uint8>> m_compressedData;
Vector<GLenum> m_compressedFormats;
Vector<GLenum> m_requestedCompressedFormats;
};
} // namespace GLState
} // namespace MG_State
@@ -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<MipmapStorage, TargetCount> m_storage;
};
@@ -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};
@@ -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;
@@ -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,
@@ -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;
+34
View File
@@ -377,6 +377,40 @@ TEST_F(TextureTest, ClearTexImageErrorContracts) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(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) {