[Feat] (MG_State, MG_Impl, MG_Util): store a compressed texture image and hand it back

glCompressedTexImage2D rejected every internalformat with GL_INVALID_ENUM, so
direct_state_access.textures_get_image threw at its first compressed call and
reported InternalError with nothing in the log at all - the uncompressed half of
the case had already passed.

The compressed bytes are now kept verbatim, in a side-channel beside the texel
shadow rather than in place of it. That placement is the load-bearing decision:
both backends pair MapMipmapData with GetMipmapByteSize while sizing their copy
regions from GetMipmapTexelSize, and DirectGLES additionally divides the byte
size by the texel count to recover bytes-per-texel, so putting 16 bytes where a
4x4 RGBA8 extent says 64 would be an out-of-bounds read on both. The texel
storage therefore stays uncompressed and correctly sized - the image samples as
zeros, which is the same deviation the RGTC/BPTC/ETC2 arms of
ConvertGLEnumToTextureInternalFormat already document - while
glGetCompressedTexImage returns the image *as stored*, which GL 4.6 core 8.11
requires and which no re-encode could satisfy byte for byte. Nothing ever hands
the compressed bytes to GLES or Vulkan, so the shadow is authoritative rather
than potentially stale, which is why the readback never asks a backend.

The accepted set is exactly the RGTC/BPTC/ETC2-EAC formats core GL requires, and
it is deliberately the same set ConvertGLEnumToTextureInternalFormat can back
with uncompressed storage, so the upload can never accept a format whose texel
shadow it cannot allocate. imageSize is checked against the block arithmetic,
which is also what keeps the copy in bounds.

Three things the shape depends on. AllocateStorage clears the compressed tag, so
a glTexImage2D or glTexStorage2D over the level un-compresses it - without that,
textures_compressed_subimage would flip branches and start asking for data
MobileGL cannot produce. GL_TEXTURE_COMPRESSED and
GL_TEXTURE_COMPRESSED_IMAGE_SIZE are answered per level rather than per texture,
because a compressed internalformat handed to glTexImage2D resolves to
uncompressed storage and must keep reading as uncompressed. And
GL_TEXTURE_INTERNAL_FORMAT now reports the compressed token for such a level, or
it would claim GL_RGBA8 while GL_TEXTURE_COMPRESSED said true.

Still rejected on purpose: glCompressedTexImage1D/3D and every
glCompressedTexSubImage*, which caps the blast radius.

Fixes textures_get_image on both backends (Espryt 370/371, Magma 369/371). A/B
over a 1210-case compressed/texture-storage/texture-view/buffer-storage subset of
KHR-GL45 is identical before and after on both backends but for
get_texture_sub_image.errors_test, which stops throwing and fails on a value
instead.
This commit is contained in:
BZLZHH
2026-08-05 09:40:29 -04:00
parent 34f09291da
commit 0e7692251d
10 changed files with 427 additions and 36 deletions
@@ -27,11 +27,52 @@ namespace MobileGL {
m_texelSizes.reserve(std::bit_ceil(requiredLevelCount));
m_texelSizes.resize(requiredLevelCount);
m_isDirty.resize(requiredLevelCount, false);
m_compressedData.resize(requiredLevelCount);
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
}
m_texelSizes[level] = input.texelSize;
auto& data = m_data[level];
data.resize(input.byteSize, 0);
// Respecifying a level drops whatever compressed image it used to hold. Without this,
// a glTexImage2D or glTexStorage2D over a level a previous glCompressedTexImage2D had
// shadowed would leave GL_TEXTURE_COMPRESSED answering true and glGetCompressedTexImage
// handing back the stale blob. Every allocation path funnels through here, so clearing
// once covers all of them; the compressed path re-arms the tag immediately afterwards
// via SetCompressedImage.
m_compressedFormats[level] = GL_NONE;
m_compressedData[level].clear();
m_compressedData[level].shrink_to_fit();
}
void MipmapStorage::SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size) {
MOBILEGL_ASSERT(level < m_compressedData.size(), "SetCompressedImage: level out of range");
m_compressedFormats[level] = internalFormat;
auto& blob = m_compressedData[level];
// Zero-filled when data is null: glCompressedTexImage* with a null pointer defines the
// level's size and format but leaves its contents undefined, and zeros are the one
// reproducible answer a later glGetCompressedTexImage can give.
blob.assign(size, 0);
if (data != nullptr && size > 0) {
Memcpy(blob.data(), data, size);
}
}
GLenum MipmapStorage::GetCompressedFormat(Uint level) const {
if (level >= m_compressedFormats.size()) return GL_NONE;
return m_compressedFormats[level];
}
SizeT MipmapStorage::GetCompressedByteSize(Uint level) const {
if (level >= m_compressedData.size()) return 0;
return m_compressedData[level].size();
}
const void* MipmapStorage::MapCompressedData(Uint level) const {
if (level >= m_compressedData.size()) return nullptr;
return m_compressedData[level].data();
}
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
@@ -40,6 +81,8 @@ namespace MobileGL {
m_data.resize(levelCount);
m_texelSizes.resize(levelCount);
m_isDirty.resize(levelCount);
m_compressedData.resize(levelCount);
m_compressedFormats.resize(levelCount);
}
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
@@ -30,10 +30,27 @@ namespace MobileGL {
void MarkDirty(Uint level, bool dirty);
bool IsDirty(Uint level) const;
// The bytes an application handed to glCompressedTexImage*, kept verbatim beside the
// (uncompressed) texel shadow rather than in place of it. GL 4.6 core 8.11 requires
// glGetCompressedTexImage to return the image *as stored*, and no backend here has a
// BC/ETC codec, so a re-encode could never be byte-exact; at the same time m_data has
// to keep the "width * height * bytes-per-texel" layout that the backend upload
// sizing, glGenerateMipmap's bytes-per-texel division and the pixel-store packer all
// divide by. Two parallel vectors, one invariant preserved. Call order is
// AllocateLevel then SetCompressedImage - AllocateLevel clears the tag, so a plain
// glTexImage2D over the level un-compresses it.
void SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size);
// GL_NONE when the level is not stored compressed.
GLenum GetCompressedFormat(Uint level) const;
SizeT GetCompressedByteSize(Uint level) const;
const void* MapCompressedData(Uint level) const;
protected:
Vector<IntVec3> m_texelSizes;
Vector<Vector<Uint8>> m_data;
Vector<bool> m_isDirty;
Vector<Vector<Uint8>> m_compressedData;
Vector<GLenum> m_compressedFormats;
};
} // namespace GLState
} // namespace MG_State
@@ -74,6 +74,27 @@ namespace MobileGL {
return m_storage[targetIndex].IsDirty(level);
}
void SetCompressedImage(Uint targetIndex, Uint level, GLenum internalFormat, const void* data,
SizeT size) {
MOBILEGL_ASSERT(targetIndex < TargetCount, "SetCompressedImage: target invalid");
m_storage[targetIndex].SetCompressedImage(level, internalFormat, data, size);
}
GLenum GetCompressedFormat(Uint targetIndex, Uint level) const {
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetCompressedFormat: target invalid");
return m_storage[targetIndex].GetCompressedFormat(level);
}
SizeT GetCompressedByteSize(Uint targetIndex, Uint level) const {
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetCompressedByteSize: target invalid");
return m_storage[targetIndex].GetCompressedByteSize(level);
}
const void* MapCompressedData(Uint targetIndex, Uint level) const {
MOBILEGL_ASSERT(targetIndex < TargetCount, "MapCompressedData: target invalid");
return m_storage[targetIndex].MapCompressedData(level);
}
protected:
Array<MipmapStorage, TargetCount> m_storage;
};
@@ -301,6 +301,27 @@ namespace MobileGL {
return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObjectWithOneMipmap::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat, const void* data, SizeT size) {
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
internalFormat, data, size);
}
GLenum TextureObjectWithOneMipmap::GetMipmapCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
SizeT TextureObjectWithOneMipmap::GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetCompressedByteSize(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
const void* TextureObjectWithOneMipmap::MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const {
if (m_textureStorage.GetLevelCount() == 0) {
return {0, 0, 0};
@@ -150,6 +150,18 @@ namespace MobileGL::MG_State::GLState {
virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0;
virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0;
virtual Bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
// The compressed image a glCompressedTexImage* call shadowed for this level, kept verbatim
// next to the texel data rather than instead of it - see MipmapStorage. The texel shadow
// stays uncompressed and correctly sized, so nothing in the backend upload path has to know
// these exist; only glGetCompressedTexImage, glGetCompressedTextureImage and the
// GL_TEXTURE_COMPRESSED* level queries read them.
virtual void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat, const void* data, SizeT size) = 0;
// GL_NONE when this level is not stored compressed.
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;
};
// Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the
@@ -206,6 +218,11 @@ namespace MobileGL::MG_State::GLState {
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override;
bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat,
const void* data, SizeT size) override;
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;
IntVec3 GetBaseSize() const override;
Bool IsComplete() const override;
@@ -55,6 +55,27 @@ namespace MobileGL {
return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObject2DCube::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat, const void* data, SizeT size) {
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
internalFormat, data, size);
}
GLenum TextureObject2DCube::GetMipmapCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
SizeT TextureObject2DCube::GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetCompressedByteSize(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
const void* TextureObject2DCube::MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
Uint TextureObject2DCube::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const {
MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target &&
target <= TextureUploadTarget::CubeMapNegativeZ,
@@ -27,6 +27,12 @@ namespace MobileGL {
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) override;
bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat, const void* data, SizeT size) override;
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;
IntVec3 GetBaseSize() const override;
Bool IsComplete() const override;