diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index 2a364ce1..1bd52dd8 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -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) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 663df754..a0cd486b 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -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& 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("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(level)); - const void* src = textureMipmapObject->MapMipmapCompressedImage(uploadTarget, static_cast(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(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& 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("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(level)); + const void* src = textureMipmapObject->MapMipmapCompressedImage(uploadTarget, static_cast(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(static_cast(TextureUploadTarget::CubeMapPositiveX) + z) + : GetPrimaryUploadTarget(textureObject); + }; + const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, imageTarget(zoffset), level); + if (compressedFormat == GL_NONE) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("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(blockInfo.blockWidth); + const Int blockHeight = static_cast(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("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((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 region(rowBytes * rows * static_cast(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( + textureMipmapObject->MapMipmapCompressedImage(target, static_cast(level))); + const SizeT imageSize = textureMipmapObject->GetMipmapCompressedByteSize(target, static_cast(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(zoffset + z); + const SizeT firstBlock = layer * levelImageBlocks + + static_cast(yoffset / blockHeight) * levelRowBlocks + + static_cast(xoffset / blockWidth); + const SizeT endByte = (firstBlock + (rows - 1) * levelRowBlocks) * blockInfo.blockByteSize + rowBytes; + if (!image || endByte > imageSize) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("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(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). diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h index e14e7480..7993e70d 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h @@ -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); diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index abc4eaba..a8a1764d 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -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(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