[Fix] (GL): implement glGetCompressedTextureSubImage over the compressed block shadow

This commit is contained in:
2026-10-12 08:22:18 -04:00
parent 60ba52bfe4
commit 049c8a421c
4 changed files with 153 additions and 25 deletions
@@ -1118,7 +1118,7 @@ DECLARE_GL_FUNCTION_HEAD(void, GetQueryBufferObjectiv, GLuint id, GLuint buffer,
DECLARE_GL_FUNCTION_HEAD(void, GetQueryBufferObjectui64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryBufferObjectui64v, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryBufferObjectuiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryBufferObjectuiv, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_HEAD(void, GetTextureSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, bufSize, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, bufSize, pixels)
DECLARE_GL_FUNCTION_HEAD(void, GetCompressedTextureSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetCompressedTextureSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, bufSize, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnCompressedTexImage, GLenum target, GLint lod, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnCompressedTexImage, target, lod, bufSize, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnTexImage, GLenum target, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnTexImage, target, level, format, type, bufSize, pixels)
DECLARE_GL_FUNCTION_HEAD(void, GetnUniformdv, GLuint program, GLint location, GLsizei bufSize, GLdouble* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetnUniformdv, program, location, bufSize, params)
+108 -24
View File
@@ -3897,28 +3897,12 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// The half glGetCompressedTexImage and glGetCompressedTextureImage share, factored out for the
// same reason ValidateTextureImageQuery was: the by-name entry point must not drift away from
// the by-target one's rules. bufSize < 0 means "no destination-size argument" - the by-target
// form has none (GL 4.6 core 8.11 has the caller size it from GL_TEXTURE_COMPRESSED_IMAGE_SIZE),
// so only the DSA form passes a real bound.
void CopyCompressedTextureImageToClientOrPBO(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureUploadTarget uploadTarget, GLint level, GLsizei bufSize,
void* pixels, const char* caller) {
if (GetCompressedLevelFormat(textureObject, uploadTarget, level) == GL_NONE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Texture level is not stored in a compressed format."));
return;
}
const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
const SizeT imageSize =
textureMipmapObject->GetMipmapCompressedByteSize(uploadTarget, static_cast<Uint>(level));
const void* src = textureMipmapObject->MapMipmapCompressedImage(uploadTarget, static_cast<Uint>(level));
if (!src || imageSize == 0) return;
// Where every compressed readback lands its bytes: the destination-size check, then either the
// bound pixel pack buffer (pixels is an offset into it) or client memory. No pixel-store packing
// here on purpose: GL 4.6 core 8.11 says the pixel storage modes are ignored for a compressed
// image, which is also the only way the round trip stays byte-exact.
static void WriteCompressedImageToClientOrPBO(const void* src, SizeT imageSize, GLsizei bufSize, void* pixels,
const char* caller) {
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < imageSize) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -3948,11 +3932,34 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
// No pixel-store packing here on purpose: GL 4.6 core 8.11 says the pixel storage modes are
// ignored for a compressed image, which is also the only way the round trip stays byte-exact.
if (pixels) Memcpy(pixels, src, imageSize);
}
// The half glGetCompressedTexImage and glGetCompressedTextureImage share, factored out for the
// same reason ValidateTextureImageQuery was: the by-name entry point must not drift away from
// the by-target one's rules. bufSize < 0 means "no destination-size argument" - the by-target
// form has none (GL 4.6 core 8.11 has the caller size it from GL_TEXTURE_COMPRESSED_IMAGE_SIZE),
// so only the DSA form passes a real bound.
void CopyCompressedTextureImageToClientOrPBO(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureUploadTarget uploadTarget, GLint level, GLsizei bufSize,
void* pixels, const char* caller) {
if (GetCompressedLevelFormat(textureObject, uploadTarget, level) == GL_NONE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Texture level is not stored in a compressed format."));
return;
}
const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
const SizeT imageSize =
textureMipmapObject->GetMipmapCompressedByteSize(uploadTarget, static_cast<Uint>(level));
const void* src = textureMipmapObject->MapMipmapCompressedImage(uploadTarget, static_cast<Uint>(level));
if (!src || imageSize == 0) return;
WriteCompressedImageToClientOrPBO(src, imageSize, bufSize, pixels, caller);
}
void GetCompressedTexImage_State(GLenum target, GLint level, void* img) {
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
@@ -6746,6 +6753,83 @@ namespace MobileGL::MG_Impl::GLImpl {
__func__);
}
// GL 4.6 core 8.11.4. Like glGetCompressedTextureImage this answers out of the CPU shadow of the
// compressed blocks (no backend was handed them), cropped to whole blocks: the region has to
// start on a block boundary and cover whole blocks except where it reaches the level's edge.
void GetCompressedTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLsizei bufSize, void* pixels) {
auto textureObject = GetTextureObjectByName(texture, __func__);
if (!textureObject) return;
IntVec3 levelExtent;
if (!ValidateTextureSubImageRegion(textureObject, level, xoffset, yoffset, zoffset, width, height, depth,
levelExtent, __func__)) {
return;
}
// A cube map's faces are separate images; every other target keeps its layers in one.
const Bool isCubeMap = textureObject->GetTarget() == TextureTarget::TextureCubeMap;
const auto imageTarget = [&](GLint z) {
return isCubeMap
? static_cast<TextureUploadTarget>(static_cast<Int>(TextureUploadTarget::CubeMapPositiveX) + z)
: GetPrimaryUploadTarget(textureObject);
};
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, imageTarget(zoffset), level);
if (compressedFormat == GL_NONE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Texture level is not stored in a compressed format."));
return;
}
const auto blockInfo = MG_Util::GetCompressedFormatInfo(compressedFormat);
const Int blockWidth = static_cast<Int>(blockInfo.blockWidth);
const Int blockHeight = static_cast<Int>(blockInfo.blockHeight);
if (blockWidth == 0 || blockHeight == 0) return;
if (xoffset % blockWidth != 0 || yoffset % blockHeight != 0 ||
(width % blockWidth != 0 && xoffset + width != levelExtent.x()) ||
(height % blockHeight != 0 && yoffset + height != levelExtent.y())) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"The region is not aligned to the compressed block size."));
return;
}
const auto blocksFor = [](Int extent, Int block) { return static_cast<SizeT>((extent + block - 1) / block); };
const SizeT levelRowBlocks = blocksFor(levelExtent.x(), blockWidth);
const SizeT levelImageBlocks = levelRowBlocks * blocksFor(levelExtent.y(), blockHeight);
const SizeT rowBlocks = blocksFor(width, blockWidth);
const SizeT rows = blocksFor(height, blockHeight);
const SizeT rowBytes = rowBlocks * blockInfo.blockByteSize;
Vector<Uint8> region(rowBytes * rows * static_cast<SizeT>(depth));
const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
for (GLsizei z = 0; z < depth && rows > 0 && rowBytes > 0; ++z) {
const auto target = imageTarget(zoffset + z);
const auto* image = static_cast<const Uint8*>(
textureMipmapObject->MapMipmapCompressedImage(target, static_cast<Uint>(level)));
const SizeT imageSize = textureMipmapObject->GetMipmapCompressedByteSize(target, static_cast<Uint>(level));
// Layers of one image are stored one after another, each a full level's worth of blocks.
const SizeT layer = isCubeMap ? 0 : static_cast<SizeT>(zoffset + z);
const SizeT firstBlock = layer * levelImageBlocks +
static_cast<SizeT>(yoffset / blockHeight) * levelRowBlocks +
static_cast<SizeT>(xoffset / blockWidth);
const SizeT endByte = (firstBlock + (rows - 1) * levelRowBlocks) * blockInfo.blockByteSize + rowBytes;
if (!image || endByte > imageSize) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Texture image is not stored in a compressed format."));
return;
}
for (SizeT row = 0; row < rows; ++row) {
Memcpy(region.data() + (static_cast<SizeT>(z) * rows + row) * rowBytes,
image + (firstBlock + row * levelRowBlocks) * blockInfo.blockByteSize, rowBytes);
}
}
WriteCompressedImageToClientOrPBO(region.data(), region.size(), bufSize, pixels, __func__);
}
// A buffer texture carries none of the sampler or level state these queries report. Reached by
// name there is no target token to blame, so the wrong object is INVALID_OPERATION rather than
// the INVALID_ENUM the target forms report for an unaccepted target (GL 4.6 core 8.11).
@@ -74,6 +74,8 @@ namespace MobileGL::MG_Impl::GLImpl {
void TextureBufferRange(GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size);
void GetTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, GLsizei bufSize, void* pixels);
void GetCompressedTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLsizei bufSize, void* pixels);
void GetTextureParameterfv(GLuint texture, GLenum pname, GLfloat* params);
void GetTextureParameterIiv(GLuint texture, GLenum pname, GLint* params);
void GetTextureParameterIuiv(GLuint texture, GLenum pname, GLuint* params);
+42
View File
@@ -2759,6 +2759,48 @@ namespace {
}
} // namespace
// glGetCompressedTextureSubImage was a stub. It returns whole blocks of the stored image (GL 4.6 core
// 8.11.4): the right-hand block column of an 8x8 RGTC1 image is the blocks at bytes 8 and 24, and the
// region errors match glGetTextureSubImage's.
TEST_F(TextureTest, GetCompressedTextureSubImageCropsWholeBlocks) {
const GLuint texture = MakeCompressedRgtc1Texture8x8();
Uint8 whole[kRgtc1Size8x8];
for (Int i = 0; i < kRgtc1Size8x8; ++i) whole[i] = static_cast<Uint8>(i + 1);
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8,
whole);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 column[16] = {};
MG_Impl::GLImpl::GetCompressedTextureSubImage(texture, 0, 4, 0, 0, 4, 8, 1, sizeof(column), column);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(std::memcmp(column, whole + 8, 8), 0);
EXPECT_EQ(std::memcmp(column + 8, whole + 24, 8), 0);
Uint8 all[kRgtc1Size8x8] = {};
MG_Impl::GLImpl::GetCompressedTextureSubImage(texture, 0, 0, 0, 0, 8, 8, 1, sizeof(all), all);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(std::memcmp(all, whole, sizeof(all)), 0);
MG_Impl::GLImpl::GetCompressedTextureSubImage(texture, 0, 2, 0, 0, 4, 4, 1, sizeof(all), all);
ExpectSingleGlError(GL_INVALID_VALUE); // not on a block boundary
MG_Impl::GLImpl::GetCompressedTextureSubImage(texture, 0, 4, 4, 0, 8, 8, 1, sizeof(all), all);
ExpectSingleGlError(GL_INVALID_VALUE); // past the level
MG_Impl::GLImpl::GetCompressedTextureSubImage(texture, 0, 0, 0, 1, 8, 8, 2, sizeof(all), all);
ExpectSingleGlError(GL_INVALID_VALUE); // a 2D texture has one layer
MG_Impl::GLImpl::GetCompressedTextureSubImage(texture, 0, 0, 0, 0, 8, 8, 1, 1, all);
ExpectSingleGlError(GL_INVALID_OPERATION); // bufSize too small
GLuint plain = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &plain);
MG_Impl::GLImpl::TextureStorage2D(plain, 1, GL_RGBA8, 4, 4);
MG_Impl::GLImpl::GetCompressedTextureSubImage(plain, 0, 0, 0, 0, 4, 4, 1, sizeof(all), all);
ExpectSingleGlError(GL_INVALID_OPERATION); // not compressed
MG_Impl::GLImpl::DeleteTextures(1, &plain);
MG_Impl::GLImpl::DeleteTextures(1, &texture);
DrainPendingGlErrors();
}
// glCompressedTexSubImage2D was a stub that answered GL_INVALID_ENUM to every call. It replaces a
// block-aligned rectangle of the stored image, and the arithmetic that places the incoming blocks
// is what the partial write below pins: a full-width write would pass with the rows concatenated