diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index 62e0e1a2..bee83f4c 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -1061,7 +1061,7 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage1D, GLuint texture, GLint level, G DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, type, pixels) DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data) -DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data) +DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data) DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width) DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height) @@ -1849,7 +1849,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage3DEXT, GLuint texture, DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage2DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage2DEXT, texture, target, level, internalformat, width, height, border, imageSize, bits) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage1DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage1DEXT, texture, target, level, internalformat, width, border, imageSize, bits) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3DEXT, texture, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits) -DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2DEXT, texture, target, level, xoffset, yoffset, width, height, format, imageSize, bits) +DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, bits) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1DEXT, texture, target, level, xoffset, width, format, imageSize, bits) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImageEXT, GLuint texture, GLenum target, GLint lod, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImageEXT, texture, target, lod, img) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage3DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage3DEXT, texunit, target, level, internalformat, width, height, depth, border, imageSize, bits) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index b4237cb7..19ab48dc 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -2238,6 +2238,23 @@ namespace MobileGL::MG_Impl::GLImpl { DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, 1}, internalBytes}); + // GL 4.6 core 8.5: a SPECIFIC compressed internalformat (unlike a generic + // GL_COMPRESSED_* one, where the implementation is free to choose) commits the + // level to that format - GL_TEXTURE_COMPRESSED must then answer true for it and + // GL_TEXTURE_INTERNAL_FORMAT must report it, which is how an application asks for + // the size to hand glCompressedTexSubImage2D afterwards. Only the tag and the size + // are recorded: there is no BC/ETC codec here, so the texel shadow keeps the + // uncompressed storage this format resolved to (which is also what lets the level + // sample as the application's texels), and the compressed image the tag describes + // is zero-filled - the one reproducible answer glGetCompressedTexImage can give for + // an image nothing ever compressed. AllocateStorage above clears the tag, so this + // has to follow it. + const auto compressedInfo = MG_Util::GetCompressedFormatInfo(static_cast(internalformat)); + if (compressedInfo.blockWidth != 0) { + textureMipmapObject->SetMipmapCompressedImage( + textureUploadTarget, level, static_cast(internalformat), nullptr, + MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1})); + } } if (!originalPixels) { @@ -2965,9 +2982,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_INTERNAL_FORMAT: if (params) { // A level stored compressed must report the token it was given, not the - // uncompressed format backing it (GL 4.6 core 8.11). Only glCompressedTexImage* sets - // that tag, so every level created by glTexImage*D - including one given a compressed - // internalformat - still answers with its resolved storage format. + // uncompressed format backing it (GL 4.6 core 8.11). glCompressedTexImage2D sets + // that tag, and so does a glTexImage2D given a SPECIFIC compressed internalformat; + // every other level answers with its resolved storage format. const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level); *params = (compressedFormat != GL_NONE) ? (GLint)compressedFormat @@ -3103,9 +3120,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_INTERNAL_FORMAT: if (params) { // A level stored compressed must report the token it was given, not the - // uncompressed format backing it (GL 4.6 core 8.11). Only glCompressedTexImage* sets - // that tag, so every level created by glTexImage*D - including one given a compressed - // internalformat - still answers with its resolved storage format. + // uncompressed format backing it (GL 4.6 core 8.11). glCompressedTexImage2D sets + // that tag, and so does a glTexImage2D given a SPECIFIC compressed internalformat; + // every other level answers with its resolved storage format. const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level); *params = (GLfloat)((compressedFormat != GL_NONE) ? compressedFormat @@ -3468,10 +3485,170 @@ namespace MobileGL::MG_Impl::GLImpl { RecordUnsupportedCompressedFormat(__func__); } + // Replaces a block-aligned rectangle of the compressed image glCompressedTexImage2D (or a + // compressed glTexImage2D) shadowed for this level. Same deviation as the image call it + // patches: the uncompressed texel shadow beside it is NOT touched, because there is no + // BC/ETC codec here to decode the incoming blocks with - so what changes is the image + // glGetCompressedTexImage hands back, not what the level samples as. Marking the texels + // dirty would therefore only re-upload bytes that did not change. void CompressedTexSubImage2D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) { - // TODO: implement compressed upload - see CompressedTexImage2D_State. - RecordUnsupportedCompressedFormat(__func__); + // ======================= Converting ================================ + const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); + const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); + // Zero block width doubles as "format is not a specific compressed format", the + // INVALID_ENUM case - one lookup answers both questions. + const auto compressedInfo = MG_Util::GetCompressedFormatInfo(format); + + // ===================== Error Checking ============================== + if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; + // A proxy holds no image to modify; only the glTexImage*/glCompressedTexImage* pair + // accepts one. + if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique("MG_Impl/GLImpl", __func__, + "A proxy target has no texture image to modify.")); + return; + } + if (!TextureImpl::ValidateTextureLevelNumber(level)) return; + if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return; + if (width < 0 || height < 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, "width and height must be non-negative.")); + return; + } + if (compressedInfo.blockWidth == 0) { + RecordUnsupportedCompressedFormat(__func__); + return; + } + + auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + if (!TextureImpl::ValidateTextureObject(textureObject)) return; + auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get()); + if (textureMipmapObject == nullptr) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed.")); + return; + } + // GL 4.6 core 8.7: INVALID_OPERATION unless the image being modified is stored in + // exactly this compressed format. That is also what makes the block arithmetic below + // sound - the level's grid is measured with THIS format's block size. + const GLenum levelFormat = + textureMipmapObject->GetMipmapCompressedFormat(textureUploadTarget, static_cast(level)); + if (levelFormat != format) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "format does not match the internal format of the texture image.")); + return; + } + + const IntVec3 levelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast(level)); + if (xoffset < 0 || yoffset < 0 || xoffset + width > levelSize.x() || yoffset + height > levelSize.y()) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, + "The replaced region does not lie within the texture image.")); + return; + } + // GL 4.6 core 8.7 for block-based formats: the region must start on a block boundary + // and must either be a whole number of blocks wide/high or run to the image's edge. + const Int blockWidth = static_cast(compressedInfo.blockWidth); + const Int blockHeight = static_cast(compressedInfo.blockHeight); + const Bool alignedX = (xoffset % blockWidth == 0) && + (width % blockWidth == 0 || xoffset + width == levelSize.x()); + const Bool alignedY = (yoffset % blockHeight == 0) && + (height % blockHeight == 0 || yoffset + height == levelSize.y()); + if (!alignedX || !alignedY) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "The replaced region is not aligned to the format's compressed blocks.")); + return; + } + // Exactly the size the format and dimensions imply, which is also what keeps the copy + // below in bounds. + const SizeT expectedImageSize = + MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1}); + if (imageSize < 0 || static_cast(imageSize) != expectedImageSize) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, + "imageSize does not match the compressed image size.")); + return; + } + + // ======================= Processing ================================ + const void* compressedBytes = data; + const auto& pixelUnpackBufferObject = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject(); + if (pixelUnpackBufferObject) { + if (pixelUnpackBufferObject->IsMapped()) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "Pixel unpack buffer is currently mapped.")); + return; + } + const SizeT offset = reinterpret_cast(data); + const SizeT bufferSize = pixelUnpackBufferObject->GetSize(); + if (offset > bufferSize || expectedImageSize > bufferSize - offset) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "Unpacking would read past the end of the pixel unpack buffer.")); + return; + } + compressedBytes = reinterpret_cast(pixelUnpackBufferObject->MappedData()) + offset; + } + if (expectedImageSize == 0) return; // a zero-sized region is a legal no-op + if (compressedBytes == nullptr) { + // No unpack buffer and a null client pointer: there is nothing to read. GL leaves + // this undefined rather than erroring, and dereferencing it is the one answer that + // is never acceptable. + MGLOG_D("%s: null data with no pixel unpack buffer bound, nothing to replace", __func__); + return; + } + + // The level's compressed image is stored as one blob, so the rectangle is patched into + // a copy of it and the whole thing handed back. Compressed sub-image uploads are not a + // hot path, and this keeps the storage layer's compressed API to the two calls it has. + const SizeT blobSize = + textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast(level)); + const void* existing = + textureMipmapObject->MapMipmapCompressedImage(textureUploadTarget, static_cast(level)); + if (blobSize == 0 || existing == nullptr) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "The texture level holds no compressed image to modify.")); + return; + } + Vector blob(blobSize); + Memcpy(blob.data(), existing, blobSize); + + const SizeT blockByteSize = compressedInfo.blockByteSize; + const SizeT levelBlocksX = (static_cast(levelSize.x()) + compressedInfo.blockWidth - 1) / + compressedInfo.blockWidth; + const SizeT levelRowBytes = levelBlocksX * blockByteSize; + const SizeT regionBlocksX = (static_cast(width) + compressedInfo.blockWidth - 1) / + compressedInfo.blockWidth; + const SizeT regionBlocksY = (static_cast(height) + compressedInfo.blockHeight - 1) / + compressedInfo.blockHeight; + const SizeT firstBlockX = static_cast(xoffset) / compressedInfo.blockWidth; + const SizeT firstBlockY = static_cast(yoffset) / compressedInfo.blockHeight; + const SizeT regionRowBytes = regionBlocksX * blockByteSize; + const auto* source = static_cast(compressedBytes); + for (SizeT row = 0; row < regionBlocksY; ++row) { + const SizeT destOffset = (firstBlockY + row) * levelRowBytes + firstBlockX * blockByteSize; + if (destOffset + regionRowBytes > blobSize) break; // a level whose blob predates its size + Memcpy(blob.data() + destOffset, source + row * regionRowBytes, regionRowBytes); + } + textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, static_cast(level), format, + blob.data(), blobSize); } void CompressedTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, @@ -4472,6 +4649,14 @@ namespace MobileGL::MG_Impl::GLImpl { free(processedPixels); } + void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, + GLsizei height, GLenum format, GLsizei imageSize, const void* data) { + auto textureObject = GetTextureObjectByName(texture, __func__); + WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { + CompressedTexSubImage2D_State(target, level, xoffset, yoffset, width, height, format, imageSize, data); + }); + } + void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) { auto textureObject = GetTextureObjectByName(texture, __func__); diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h index 7f800cf5..604f881b 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h @@ -37,6 +37,8 @@ namespace MobileGL::MG_Impl::GLImpl { GLenum format, GLenum type, const void* pixels); void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels); + void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, + GLsizei height, GLenum format, GLsizei imageSize, const void* data); void TextureParameterf(GLuint texture, GLenum pname, GLfloat param); void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params); void TextureParameteri(GLuint texture, GLenum pname, GLint param); diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index e215c11e..6ad2fed3 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -1572,6 +1573,238 @@ TEST_F(TextureTest, CompressedInternalFormatsResolveToTheirUncompressedStorage) } } +// Resolving to uncompressed storage is a storage decision, not a licence to answer the level +// queries as if the application had asked for an uncompressed format. GL 4.6 core 8.5 lets the +// implementation choose for the GENERIC formats (GL_COMPRESSED_RED and friends), but a SPECIFIC +// one commits the level: GL_TEXTURE_COMPRESSED is true, GL_TEXTURE_INTERNAL_FORMAT is the token +// that was passed, and GL_TEXTURE_COMPRESSED_IMAGE_SIZE answers instead of erroring - which is +// exactly the three-query sequence KHR-GL44.buffer_storage.map_persistent_texture opens with to +// size the image it then uploads through glCompressedTexSubImage2D. +TEST_F(TextureTest, ASpecificCompressedInternalFormatTagsTheLevelCompressed) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + 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); + + GLint compressed = GL_FALSE; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &compressed); + EXPECT_EQ(compressed, GL_TRUE); + + GLint internalFormat = 0; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat); + EXPECT_EQ(internalFormat, static_cast(GL_COMPRESSED_RED_RGTC1)); + + // 8x8 in 4x4 blocks of 8 bytes each. + GLint imageSize = 0; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize); + EXPECT_EQ(imageSize, 32); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // The texel shadow behind the tag still carries the uncompressed storage the format resolves + // to - which is what lets the level sample, and what every size computation downstream + // divides by. + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + ASSERT_NE(textureObject, nullptr); + EXPECT_EQ(textureObject->GetFormat(), TextureInternalFormat::R8); +} + +// The negative control for the case above, and the reason it cannot simply tag every +// GL_COMPRESSED_* token: for a generic format the implementation's choice IS the answer, and +// MobileGL chooses uncompressed - so the level is not compressed and the size query is the +// INVALID_OPERATION GL 4.6 core 8.11 prescribes for an uncompressed image. +TEST_F(TextureTest, AGenericCompressedInternalFormatLeavesTheLevelUncompressed) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + GLint compressed = GL_TRUE; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &compressed); + EXPECT_EQ(compressed, GL_FALSE); + + GLint internalFormat = 0; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat); + EXPECT_EQ(internalFormat, static_cast(GL_R8)); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + GLint imageSize = 0; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize); + ExpectSingleGlError(GL_INVALID_OPERATION); +} + +// A plain glTexImage2D over a level that was tagged compressed has to un-tag it, the same way it +// does for a level a glCompressedTexImage2D shadowed - otherwise the size query would keep +// answering for an image that no longer exists. +TEST_F(TextureTest, AnUncompressedRespecificationClearsTheCompressedTag) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + 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); + + GLint compressed = GL_TRUE; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &compressed); + EXPECT_EQ(compressed, GL_FALSE); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +namespace { + // 8x8 RGTC1: 2x2 blocks of 8 bytes, so the stored image is 32 bytes and one block row is 16. + constexpr GLsizei kRgtc1Size8x8 = 32; + + GLuint MakeCompressedRgtc1Texture8x8() { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + MG_Impl::GLImpl::CompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 0, kRgtc1Size8x8, + nullptr); + return texture; + } +} // namespace + +// 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 +// in either order. +TEST_F(TextureTest, CompressedTexSubImage2DReplacesTheStoredBlocks) { + const GLuint texture = MakeCompressedRgtc1Texture8x8(); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + 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 stored[kRgtc1Size8x8] = {}; + MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored); + EXPECT_EQ(std::memcmp(stored, whole, sizeof(whole)), 0); + + // The right-hand block column only: one block wide, two block rows high. Its two blocks land + // at byte 8 and byte 24, not at bytes 0 and 8. + const Uint8 column[16] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, + 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7}; + MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 4, 0, 4, 8, GL_COMPRESSED_RED_RGTC1, + static_cast(sizeof(column)), column); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + Uint8 expected[kRgtc1Size8x8]; + std::memcpy(expected, whole, sizeof(expected)); + std::memcpy(expected + 8, column, 8); + std::memcpy(expected + 24, column + 8, 8); + + std::memset(stored, 0, sizeof(stored)); + MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored); + EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// The same call sourcing its blocks from a buffer bound to GL_PIXEL_UNPACK_BUFFER, where `data` is +// an offset into that buffer rather than a client pointer - which is the form +// KHR-GL44.buffer_storage.map_persistent_texture uses for every one of its operations. +TEST_F(TextureTest, CompressedTexSubImage2DUnpacksFromAPixelUnpackBuffer) { + Uint8 source[256]; + for (Int i = 0; i < 256; ++i) source[i] = static_cast(i); + GLuint buffer = 0; + MG_Impl::GLImpl::GenBuffers(1, &buffer); + MG_Impl::GLImpl::BindBuffer(GL_PIXEL_UNPACK_BUFFER, buffer); + MG_Impl::GLImpl::BufferData(GL_PIXEL_UNPACK_BUFFER, sizeof(source), source, GL_STATIC_DRAW); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const GLuint texture = MakeCompressedRgtc1Texture8x8(); + MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8, + reinterpret_cast(static_cast(64))); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + Uint8 stored[kRgtc1Size8x8] = {}; + MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored); + EXPECT_EQ(std::memcmp(stored, source + 64, sizeof(stored)), 0); + + // Reading past the end of the buffer is the unpack-buffer error, not a read out of bounds. + MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8, + reinterpret_cast(static_cast(sizeof(source) - 8))); + ExpectSingleGlError(GL_INVALID_OPERATION); + + MG_Impl::GLImpl::BindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + (void)texture; +} + +// glCompressedTextureSubImage2D was an exported no-op that raised no error at all, so an +// application could not tell the write had not happened. It must reach the NAMED texture and leave +// the binding it borrowed exactly as it found it. +TEST_F(TextureTest, CompressedTextureSubImage2DModifiesTheNamedTextureOnly) { + const GLuint bound = MakeCompressedRgtc1Texture8x8(); + Uint8 boundImage[kRgtc1Size8x8]; + for (Int i = 0; i < kRgtc1Size8x8; ++i) boundImage[i] = 0x11; + MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8, + boundImage); + + const GLuint named = MakeCompressedRgtc1Texture8x8(); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, bound); // `named` is NOT the bound texture + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + Uint8 namedImage[kRgtc1Size8x8]; + for (Int i = 0; i < kRgtc1Size8x8; ++i) namedImage[i] = 0x22; + MG_Impl::GLImpl::CompressedTextureSubImage2D(named, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8, + namedImage); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // The borrowed binding is back, and it kept its own image. + Uint8 stored[kRgtc1Size8x8] = {}; + MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored); + EXPECT_EQ(std::memcmp(stored, boundImage, sizeof(stored)), 0); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, named); + std::memset(stored, 0, sizeof(stored)); + MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored); + EXPECT_EQ(std::memcmp(stored, namedImage, sizeof(stored)), 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, CompressedTexSubImage2DRejectsTheRegionsGLForbids) { + const GLuint texture = MakeCompressedRgtc1Texture8x8(); + Uint8 blocks[kRgtc1Size8x8] = {}; + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // A format that is not the one the image is stored in. + MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RG_RGTC2, 64, blocks); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // A start that is not on a block boundary. + MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 2, 0, 4, 8, GL_COMPRESSED_RED_RGTC1, 16, blocks); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // A width that is neither a whole number of blocks nor a run to the image's edge. + MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 8, GL_COMPRESSED_RED_RGTC1, 16, blocks); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // A region that runs off the image. + MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 4, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, 32, blocks); + ExpectSingleGlError(GL_INVALID_VALUE); + + // An imageSize that does not match the region. + MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, 16, blocks); + ExpectSingleGlError(GL_INVALID_VALUE); + + // A format with no defined block layout here. + MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_RGBA8, 32, blocks); + ExpectSingleGlError(GL_INVALID_ENUM); + + // An uncompressed image has nothing for it to replace. + GLuint plain = 0; + MG_Impl::GLImpl::GenTextures(1, &plain); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, plain); + 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::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8, + blocks); + ExpectSingleGlError(GL_INVALID_OPERATION); + (void)texture; +} + // RGTC compresses 4x4 blocks of a 2D image and has no 3D form, so glTexImage3D must reject it even // though the same enum is accepted on a 2D target. The generic compressed formats carry no such // restriction and stay legal in 3D.