From c9bf43b71072d733fbcddb56b23a1a8d91e06631 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 12 Oct 2026 08:04:52 -0400 Subject: [PATCH] [Fix] (Texture): decode ETC2/EAC uploads into the texel shadow --- CMakeLists.txt | 1 + .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 61 ++- MobileGL/MG_Test/Texture/TextureTest.cpp | 91 +++++ MobileGL/MG_Util/Texture/EtcDecoder.cpp | 375 ++++++++++++++++++ MobileGL/MG_Util/Texture/EtcDecoder.h | 41 ++ 5 files changed, 558 insertions(+), 11 deletions(-) create mode 100644 MobileGL/MG_Util/Texture/EtcDecoder.cpp create mode 100644 MobileGL/MG_Util/Texture/EtcDecoder.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b2e5667b..0955eae9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -448,6 +448,7 @@ set(SOURCE_FILES MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp + MobileGL/MG_Util/Texture/EtcDecoder.cpp MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp MobileGL/MG_Impl/GLXImpl/Exporting/GlvndVendor.cpp diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 297212e4..48ceaf27 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -785,6 +786,34 @@ namespace MobileGL::MG_Impl::GLImpl { "Compressed texture formats are not supported.")); } + // The ETC2/EAC arm of the compressed uploads: the blocks are decoded into the level's + // uncompressed texel shadow (the storage the format resolves to), so the level samples and + // reads back as the application's image on every backend. `blocks` covers the region + // (offset, size) of the level. False - nothing written - for any other compressed format, + // which still has no decoder here, or for storage not shaped as the decoder writes it. + Bool DecodeEtcBlocksIntoTexelShadow(MG_State::GLState::TextureObjectMipmap* texture, + TextureUploadTarget uploadTarget, Uint level, GLenum format, + const void* blocks, IntVec3 offset, IntVec3 size) { + if (blocks == nullptr || !MG_Util::EtcDecoder::IsEtcFormat(format)) return false; + const SizeT texelBytes = MG_Util::EtcDecoder::DecodedTexelBytes(format); + if (MG_Util::GetInternalBytesPerPixel(texture->GetFormat(), TexturePixelDataType::UnsignedByte) != + texelBytes) { + return false; + } + const IntVec3 levelSize = texture->GetMipmapTexelSize(uploadTarget, level); + const Int levelDepth = std::max(levelSize.z(), 1); + const SizeT levelBytes = static_cast(levelSize.x()) * static_cast(levelSize.y()) * + static_cast(levelDepth) * texelBytes; + if (levelBytes == 0 || texture->GetMipmapByteSize(uploadTarget, level) < levelBytes) return false; + auto* shadow = static_cast(texture->MapMipmapData(uploadTarget, level)); + if (shadow == nullptr) return false; + MG_Util::EtcDecoder::DecodeRegion(format, static_cast(blocks), size.x(), size.y(), size.z(), + shadow, levelSize.x(), levelSize.y(), levelDepth, offset.x(), + offset.y(), offset.z()); + texture->MarkStorageDirtyRegion(uploadTarget, level, offset, size); + return true; + } + // GL_TEXTURE_WIDTH of a buffer texture: how many texels of the texture's internal format fit // in the buffer range it addresses, CLAMPED to GL_MAX_TEXTURE_BUFFER_SIZE. Attaching a larger // buffer is legal (GL 4.6 core 8.9) - the texture simply addresses the first @@ -4646,8 +4675,9 @@ namespace MobileGL::MG_Impl::GLImpl { // The three-dimensional twin of CompressedTexSubImage2D_State: a block-aligned box of the // compressed image the level shadows is replaced, slice by slice. Same deviation as the 2D form - // - the uncompressed texel shadow beside it is NOT touched, so what changes is the image - // glGetCompressedTexImage hands back, not what the level samples as. + // - the uncompressed texel shadow beside it is touched only for ETC2/EAC, the one family with + // a decoder here; for the others what changes is the image glGetCompressedTexImage hands back, + // not what the level samples as. void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) { @@ -4759,7 +4789,7 @@ namespace MobileGL::MG_Impl::GLImpl { } static std::atomic announcedNoCodec3D{false}; - if (!announcedNoCodec3D.exchange(true)) { + if (!MG_Util::EtcDecoder::IsEtcFormat(format) && !announcedNoCodec3D.exchange(true)) { MGLOG_W("%s: the compressed blocks are stored verbatim and returned by " "glGetCompressedTexImage, but there is no BC/ETC decoder here, so they do not " "reach the texels this level SAMPLES as. Upload through glTexSubImage3D for " @@ -4809,14 +4839,16 @@ namespace MobileGL::MG_Impl::GLImpl { } textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, static_cast(level), format, blob.data(), blobSize); + DecodeEtcBlocksIntoTexelShadow(textureMipmapObject, textureUploadTarget, static_cast(level), format, + compressedBytes, {xoffset, yoffset, zoffset}, {width, height, depth}); } // 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. + // patches: ETC2/EAC blocks are decoded into the uncompressed texel shadow beside it + // (MG_Util::EtcDecoder), but for every other family there is no codec here, so what changes + // is the image glGetCompressedTexImage hands back, not what the level samples as - and marking + // those texels dirty would 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) { // ======================= Converting ================================ @@ -4931,7 +4963,7 @@ namespace MobileGL::MG_Impl::GLImpl { // end. MGLOG_W is the right level and now survives at INFO; it sat at MGLOG_I only // while the Log.h ordering compiled warnings out of the builds that ship. static std::atomic announcedNoCodec{false}; - if (!announcedNoCodec.exchange(true)) { + if (!MG_Util::EtcDecoder::IsEtcFormat(format) && !announcedNoCodec.exchange(true)) { MGLOG_W("%s: the compressed blocks are stored verbatim and returned by " "glGetCompressedTexImage, but there is no BC/ETC decoder here, so they do not " "reach the texels this level SAMPLES as. Upload through glTexSubImage2D for " @@ -4975,6 +5007,8 @@ namespace MobileGL::MG_Impl::GLImpl { } textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, static_cast(level), format, blob.data(), blobSize); + DecodeEtcBlocksIntoTexelShadow(textureMipmapObject, textureUploadTarget, static_cast(level), format, + compressedBytes, {xoffset, yoffset, 0}, {width, height, 1}); } void CompressedTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, @@ -4985,7 +5019,7 @@ namespace MobileGL::MG_Impl::GLImpl { // The three-dimensional twin of CompressedTexImage2D_State, and the same deviation applies: the // blocks are shadowed verbatim for glGetCompressedTexImage while the texels this level SAMPLES - // as stay zero, because there is no BC/ETC decoder here. A 3D compressed image is a stack of + // as stay zero - except ETC2/EAC, which is decoded into them. A 3D compressed image is a stack of // `depth` two-dimensional block grids - every format that reaches here has a 4x4x1 block - so // the blob layout is slice-major and CalculateCompressedTextureImageSize already multiplies by // depth. @@ -5060,6 +5094,8 @@ namespace MobileGL::MG_Impl::GLImpl { const void* compressedBytes = CompressedUnpackSource(data); textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes, expectedImageSize); + DecodeEtcBlocksIntoTexelShadow(textureMipmapObject, textureUploadTarget, static_cast(level), + internalformat, compressedBytes, {0, 0, 0}, {width, height, depth}); textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true); } @@ -5113,8 +5149,9 @@ namespace MobileGL::MG_Impl::GLImpl { // ConvertGLEnumToTextureInternalFormat already document: neither backend has a BC/ETC codec // and TextureInternalFormat has no compressed enumerator, so the shadow keeps the "one // format, N bytes per texel" layout the backend upload sizing, glGenerateMipmap's - // bytes-per-texel division and the pixel-store packer all rely on. The image therefore - // samples as zeros. The application's bytes are kept beside it so glGetCompressedTexImage can + // bytes-per-texel division and the pixel-store packer all rely on. ETC2/EAC blocks are + // decoded into that storage (MG_Util::EtcDecoder); any other family samples as zeros, having + // no decoder here. The application's bytes are kept beside it so glGetCompressedTexImage can // return the image *as stored*, which GL 4.6 core 8.11 requires and which no re-encode could // satisfy byte for byte. const TextureInternalFormat textureInternalFormat = @@ -5140,6 +5177,8 @@ namespace MobileGL::MG_Impl::GLImpl { const void* compressedBytes = CompressedUnpackSource(data); textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes, expectedImageSize); + DecodeEtcBlocksIntoTexelShadow(textureMipmapObject, textureUploadTarget, static_cast(level), + internalformat, compressedBytes, {0, 0, 0}, {width, height, 1}); textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true); } diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index da3c6294..1e92ea54 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include using namespace MobileGL; @@ -7033,3 +7034,93 @@ TEST_F(TextureTest, TheExternalTargetIsABindingPointOfItsOwn) { DeleteTextures(2, names); EXPECT_EQ(GetError(), GL_NO_ERROR); } + +namespace { + // The first layer of the conformance suite's cube-map-array ETC2 case: an 8x8 + // GL_COMPRESSED_RGB8_ETC2 image (2x2 blocks) and the RGBA8 image it was compressed from. + // Texel row y has G = 32 * y and column x has B = 32 * x (the suite draws the +Z face, whose + // t runs against window y, and compares bottom-up, so its table reads G = 224 first). + constexpr Uint8 kEtc2Rgb8x8[32] = {0x0, 0x0, 0x4, 0x2, 0x1, 0x0, 0x6f, 0xc0, 0x0, 0x1, 0x4, + 0x2, 0x1, 0xf8, 0x6f, 0xe0, 0x1, 0x0, 0x4, 0x2, 0x80, 0xf8, + 0x5f, 0xc0, 0x1, 0x0, 0xfb, 0x82, 0x81, 0xf8, 0x5f, 0xe0}; + + Int ReferenceGreen(Int y) { return 32 * y; } + Int ReferenceBlue(Int x) { return 32 * x; } +} // namespace + +// The decoder against an image a reference encoder produced; the suite's own tolerance for it is +// 5% (13 of 255), which ETC2's quantisation needs. +TEST(EtcDecoder, DecodesTheConformanceSuitesEtc2RgbImage) { + Uint8 texels[8 * 8 * 3] = {}; + ASSERT_TRUE(MG_Util::EtcDecoder::DecodeRegion(GL_COMPRESSED_RGB8_ETC2, kEtc2Rgb8x8, 8, 8, 1, texels, 8, 8, 1, 0, + 0, 0)); + for (Int y = 0; y < 8; ++y) { + for (Int x = 0; x < 8; ++x) { + const Uint8* t = texels + (y * 8 + x) * 3; + EXPECT_NEAR(t[0], 0, 13) << "red at (" << x << ", " << y << ")"; + EXPECT_NEAR(t[1], ReferenceGreen(y), 13) << "green at (" << x << ", " << y << ")"; + EXPECT_NEAR(t[2], ReferenceBlue(x), 13) << "blue at (" << x << ", " << y << ")"; + } + } +} + +// The EAC half: a block whose multiplier is 0 still moves every texel by the bare modifier, and a +// base of 128 with modifier index 4 (+2 in table 0) reads 130 in the alpha channel; the colour half +// is an individual-mode block of one flat colour (all-zero indices: +2 on 0x88 = 138). +TEST(EtcDecoder, DecodesAnEacAlphaBlockAndAnIndividualModeColourBlock) { + Uint8 block[16] = {}; + block[0] = 128; // alpha base + block[1] = 0x10; // multiplier 1, table 0 + // Every 3-bit index = 4 (0b100): the pattern 100 repeated over 48 bits. + const Uint64 indices = 0x924924924924ull; + for (Int i = 0; i < 6; ++i) block[2 + i] = static_cast(indices >> (40 - 8 * i)); + // Colour: individual mode, both subblocks 0x8 per channel, codewords 0, all indices 0. + block[8] = 0x88; + block[9] = 0x88; + block[10] = 0x88; + block[11] = 0x00; + Uint8 texels[16 * 4] = {}; + ASSERT_TRUE(MG_Util::EtcDecoder::DecodeBlock(GL_COMPRESSED_RGBA8_ETC2_EAC, block, texels)); + for (Int t = 0; t < 16; ++t) { + EXPECT_EQ(texels[t * 4 + 0], 138); + EXPECT_EQ(texels[t * 4 + 1], 138); + EXPECT_EQ(texels[t * 4 + 2], 138); + EXPECT_EQ(texels[t * 4 + 3], 130); + } +} + +// glCompressedTexImage2D of an ETC2 image used to leave the level's texels zero: the blocks were +// kept for glGetCompressedTexImage only, so the level sampled - and read back - black. They are +// decoded into the texel shadow now, which is what both backends upload. +TEST_F(TextureTest, CompressedTexImage2DDecodesEtc2IntoTheTexels) { + 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_RGB8_ETC2, 8, 8, 0, + static_cast(sizeof(kEtc2Rgb8x8)), kEtc2Rgb8x8); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + Uint8 rgba[8 * 8 * 4] = {}; + MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + for (Int y = 0; y < 8; ++y) { + for (Int x = 0; x < 8; ++x) { + const Uint8* t = rgba + (y * 8 + x) * 4; + EXPECT_NEAR(t[1], ReferenceGreen(y), 13) << "green at (" << x << ", " << y << ")"; + EXPECT_NEAR(t[2], ReferenceBlue(x), 13) << "blue at (" << x << ", " << y << ")"; + } + } + + // A sub-image replace decodes too: the top-left block becomes a flat individual-mode colour. + const Uint8 flat[8] = {0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00}; + MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_COMPRESSED_RGB8_ETC2, + static_cast(sizeof(flat)), flat); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba); + EXPECT_EQ(rgba[0], 138); + EXPECT_EQ(rgba[(3 * 8 + 3) * 4 + 2], 138) << "the replaced block's last texel"; + EXPECT_NEAR(rgba[(0 * 8 + 4) * 4 + 2], ReferenceBlue(4), 13) << "the block beside it is untouched"; + + MG_Impl::GLImpl::DeleteTextures(1, &texture); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} diff --git a/MobileGL/MG_Util/Texture/EtcDecoder.cpp b/MobileGL/MG_Util/Texture/EtcDecoder.cpp new file mode 100644 index 00000000..aba8bce5 --- /dev/null +++ b/MobileGL/MG_Util/Texture/EtcDecoder.cpp @@ -0,0 +1,375 @@ +// MobileGL - MobileGL/MG_Util/Texture/EtcDecoder.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "EtcDecoder.h" + +#include + +namespace MobileGL::MG_Util::EtcDecoder { + namespace { + // GL 4.6 core C.2 reads every block as a big-endian integer; bit 63 is the MSB of byte 0. + Uint64 ReadBigEndian64(const Uint8* bytes) { + Uint64 value = 0; + for (Int i = 0; i < 8; ++i) value = (value << 8) | bytes[i]; + return value; + } + + Uint32 Bits(Uint64 word, Uint32 high, Uint32 low) { + return static_cast((word >> low) & ((Uint64{1} << (high - low + 1)) - 1)); + } + + Uint8 Clamp255(Int value) { return static_cast(std::clamp(value, 0, 255)); } + Int Extend4(Uint32 v) { return static_cast((v << 4) | v); } + Int Extend5(Uint32 v) { return static_cast((v << 3) | (v >> 2)); } + Int Extend6(Uint32 v) { return static_cast((v << 2) | (v >> 4)); } + Int Extend7(Uint32 v) { return static_cast((v << 1) | (v >> 6)); } + + // Table C.12: intensity modifiers of the individual/differential modes, indexed by the + // codeword; a texel's (msb, lsb) picks +a, +b, -a, -b. + constexpr Int kIntensityModifiers[8][2] = {{2, 8}, {5, 17}, {9, 29}, {13, 42}, + {18, 60}, {24, 80}, {33, 106}, {47, 183}}; + // Table C.14: the T and H mode distances. + constexpr Int kDistances[8] = {3, 6, 11, 16, 23, 32, 41, 64}; + // Table C.16: the EAC modifier tables (alpha, R11 and RG11 share them). + constexpr Int kEacModifiers[16][8] = { + {-3, -6, -9, -15, 2, 5, 8, 14}, {-3, -7, -10, -13, 2, 6, 9, 12}, {-2, -5, -8, -13, 1, 4, 7, 12}, + {-2, -4, -6, -13, 1, 3, 5, 12}, {-3, -6, -8, -12, 2, 5, 7, 11}, {-3, -7, -9, -11, 2, 6, 8, 10}, + {-4, -7, -8, -11, 3, 6, 7, 10}, {-3, -5, -8, -11, 2, 4, 7, 10}, {-2, -6, -8, -10, 1, 5, 7, 9}, + {-2, -5, -8, -10, 1, 4, 7, 9}, {-2, -4, -8, -10, 1, 3, 7, 9}, {-2, -5, -7, -10, 1, 4, 6, 9}, + {-3, -4, -7, -10, 2, 3, 6, 9}, {-1, -2, -3, -10, 0, 1, 2, 9}, {-4, -6, -8, -9, 3, 5, 7, 8}, + {-3, -5, -7, -9, 2, 4, 6, 8}}; + + // One ETC2 RGB block (with the punch-through alpha variant) into 16 RGBA texels, + // row-major. Without punch-through alpha every alpha is 255. + void DecodeEtc2Rgb(const Uint8* block, Bool punchthrough, Uint8 rgba[16][4]) { + const Uint64 word = ReadBigEndian64(block); + // The punch-through variant repurposes the diff bit as the opaque bit and has no + // individual mode (C.2.3). + const Bool diff = punchthrough || Bits(word, 33, 33) != 0; + const Bool opaque = !punchthrough || Bits(word, 33, 33) != 0; + const Bool flip = Bits(word, 32, 32) != 0; + const Uint32 msbPlane = Bits(word, 31, 16); + const Uint32 lsbPlane = Bits(word, 15, 0); + // Texel (x, y) is pixel index x * 4 + y in both index planes. + const auto texelIndex = [&](Int x, Int y) { + const Int p = x * 4 + y; + return static_cast((((msbPlane >> p) & 1u) << 1) | ((lsbPlane >> p) & 1u)); + }; + const auto write = [&](Int x, Int y, Int r, Int g, Int b, Uint8 a) { + Uint8* t = rgba[y * 4 + x]; + t[0] = Clamp255(r); + t[1] = Clamp255(g); + t[2] = Clamp255(b); + t[3] = a; + }; + const auto transparent = [&](Int x, Int y) { + Uint8* t = rgba[y * 4 + x]; + t[0] = t[1] = t[2] = t[3] = 0; + }; + + Int base[2][3] = {}; + if (!diff) { + // Individual mode: two 4-bit colours. + base[0][0] = Extend4(Bits(word, 63, 60)); + base[1][0] = Extend4(Bits(word, 59, 56)); + base[0][1] = Extend4(Bits(word, 55, 52)); + base[1][1] = Extend4(Bits(word, 51, 48)); + base[0][2] = Extend4(Bits(word, 47, 44)); + base[1][2] = Extend4(Bits(word, 43, 40)); + } else { + // A 5-bit base plus a 3-bit signed delta; a sum that leaves [0, 31] selects the + // T (red), H (green) or planar (blue) mode instead. + const auto signed3 = [](Uint32 v) { return v >= 4 ? static_cast(v) - 8 : static_cast(v); }; + const Int r = static_cast(Bits(word, 63, 59)); + const Int g = static_cast(Bits(word, 55, 51)); + const Int b = static_cast(Bits(word, 47, 43)); + const Int r2 = r + signed3(Bits(word, 58, 56)); + const Int g2 = g + signed3(Bits(word, 50, 48)); + const Int b2 = b + signed3(Bits(word, 42, 40)); + if (r2 < 0 || r2 > 31) { + // T mode. + const Int c1[3] = {Extend4((Bits(word, 60, 59) << 2) | Bits(word, 57, 56)), + Extend4(Bits(word, 55, 52)), Extend4(Bits(word, 51, 48))}; + const Int c2[3] = {Extend4(Bits(word, 47, 44)), Extend4(Bits(word, 43, 40)), + Extend4(Bits(word, 39, 36))}; + const Int d = kDistances[(Bits(word, 35, 34) << 1) | Bits(word, 32, 32)]; + Int paint[4][3]; + for (Int c = 0; c < 3; ++c) { + paint[0][c] = c1[c]; + paint[1][c] = c2[c] + d; + paint[2][c] = c2[c]; + paint[3][c] = c2[c] - d; + } + for (Int y = 0; y < 4; ++y) { + for (Int x = 0; x < 4; ++x) { + const Int i = texelIndex(x, y); + if (!opaque && i == 2) { + transparent(x, y); + continue; + } + write(x, y, paint[i][0], paint[i][1], paint[i][2], 255); + } + } + return; + } + if (g2 < 0 || g2 > 31) { + // H mode. + const Uint32 r1 = Bits(word, 62, 59); + const Uint32 g1 = (Bits(word, 58, 56) << 1) | Bits(word, 52, 52); + const Uint32 b1 = (Bits(word, 51, 51) << 3) | Bits(word, 49, 47); + const Uint32 r2h = Bits(word, 46, 43); + const Uint32 g2h = Bits(word, 42, 39); + const Uint32 b2h = Bits(word, 38, 35); + const Uint32 value1 = (r1 << 8) | (g1 << 4) | b1; + const Uint32 value2 = (r2h << 8) | (g2h << 4) | b2h; + const Int d = kDistances[(Bits(word, 34, 34) << 2) | (Bits(word, 32, 32) << 1) | + (value1 >= value2 ? 1u : 0u)]; + const Int c1[3] = {Extend4(r1), Extend4(g1), Extend4(b1)}; + const Int c2[3] = {Extend4(r2h), Extend4(g2h), Extend4(b2h)}; + Int paint[4][3]; + for (Int c = 0; c < 3; ++c) { + paint[0][c] = c1[c] + d; + paint[1][c] = c1[c] - d; + paint[2][c] = c2[c] + d; + paint[3][c] = c2[c] - d; + } + for (Int y = 0; y < 4; ++y) { + for (Int x = 0; x < 4; ++x) { + const Int i = texelIndex(x, y); + if (!opaque && i == 2) { + transparent(x, y); + continue; + } + write(x, y, paint[i][0], paint[i][1], paint[i][2], 255); + } + } + return; + } + if (b2 < 0 || b2 > 31) { + // Planar mode: always opaque, an origin and two gradients. + const Int o[3] = {Extend6(Bits(word, 62, 57)), + Extend7((Bits(word, 56, 56) << 6) | Bits(word, 54, 49)), + Extend6((Bits(word, 48, 48) << 5) | (Bits(word, 44, 43) << 3) | + Bits(word, 41, 39))}; + const Int h[3] = {Extend6((Bits(word, 38, 34) << 1) | Bits(word, 32, 32)), + Extend7(Bits(word, 31, 25)), Extend6(Bits(word, 24, 19))}; + const Int v[3] = {Extend6(Bits(word, 18, 13)), Extend7(Bits(word, 12, 6)), + Extend6(Bits(word, 5, 0))}; + for (Int y = 0; y < 4; ++y) { + for (Int x = 0; x < 4; ++x) { + Int c[3]; + for (Int k = 0; k < 3; ++k) { + c[k] = (x * (h[k] - o[k]) + y * (v[k] - o[k]) + 4 * o[k] + 2) >> 2; + } + write(x, y, c[0], c[1], c[2], 255); + } + } + return; + } + // Differential mode. + base[0][0] = Extend5(static_cast(r)); + base[1][0] = Extend5(static_cast(r2)); + base[0][1] = Extend5(static_cast(g)); + base[1][1] = Extend5(static_cast(g2)); + base[0][2] = Extend5(static_cast(b)); + base[1][2] = Extend5(static_cast(b2)); + } + + const Uint32 codeword[2] = {Bits(word, 39, 37), Bits(word, 36, 34)}; + for (Int y = 0; y < 4; ++y) { + for (Int x = 0; x < 4; ++x) { + const Int sub = flip ? (y >= 2 ? 1 : 0) : (x >= 2 ? 1 : 0); + const Int i = texelIndex(x, y); + if (!opaque && i == 2) { + transparent(x, y); + continue; + } + // msb set means negative, lsb picks the larger magnitude. A non-opaque + // punch-through block drops the smaller magnitude to 0 (C.2.3). + Int modifier = kIntensityModifiers[codeword[sub]][i & 1]; + if (!opaque && (i & 1) == 0) modifier = 0; + if (i & 2) modifier = -modifier; + write(x, y, base[sub][0] + modifier, base[sub][1] + modifier, base[sub][2] + modifier, 255); + } + } + } + + // The 3-bit index of texel (x, y) in an EAC block's 48 index bits. + Int EacIndex(Uint64 word, Int x, Int y) { + const Int p = x * 4 + y; + return static_cast((word >> (45 - 3 * p)) & 7u); + } + + // An EAC alpha block (the first half of RGBA8_ETC2_EAC) into 16 alpha bytes, row-major. + void DecodeEacAlpha(const Uint8* block, Uint8 alpha[16]) { + const Uint64 word = ReadBigEndian64(block); + const Int base = static_cast(Bits(word, 63, 56)); + const Int multiplier = static_cast(Bits(word, 55, 52)); + const Int* table = kEacModifiers[Bits(word, 51, 48)]; + for (Int y = 0; y < 4; ++y) { + for (Int x = 0; x < 4; ++x) { + alpha[y * 4 + x] = Clamp255(base + table[EacIndex(word, x, y)] * multiplier); + } + } + } + + // An R11 EAC block into 16 11-bit values: [0, 2047] unsigned, [-1023, 1023] signed. + void DecodeEac11(const Uint8* block, Bool isSigned, Int values[16]) { + const Uint64 word = ReadBigEndian64(block); + Int base = static_cast(Bits(word, 63, 56)); + if (isSigned) { + base = static_cast(static_cast(static_cast(base))); + if (base == -128) base = -127; + } + const Int multiplier = static_cast(Bits(word, 55, 52)); + const Int* table = kEacModifiers[Bits(word, 51, 48)]; + for (Int y = 0; y < 4; ++y) { + for (Int x = 0; x < 4; ++x) { + const Int modifier = table[EacIndex(word, x, y)]; + // A zero multiplier still moves the value by the bare modifier (C.2.5). + const Int scaled = multiplier != 0 ? modifier * multiplier * 8 : modifier; + values[y * 4 + x] = isSigned ? std::clamp(base * 8 + scaled, -1023, 1023) + : std::clamp(base * 8 + 4 + scaled, 0, 2047); + } + } + } + + // The 11-bit value in the 8-bit storage the format resolves to, rounded to nearest. + Uint8 Eac11ToByte(Int value, Bool isSigned) { + if (isSigned) { + const Int magnitude = (std::abs(value) * 127 + 511) / 1023; + return static_cast(static_cast(value < 0 ? -magnitude : magnitude)); + } + return static_cast((value * 255 + 1023) / 2047); + } + + SizeT BlockBytes(GLenum format) { + switch (format) { + case GL_COMPRESSED_RGBA8_ETC2_EAC: + case GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC: + case GL_COMPRESSED_RG11_EAC: + case GL_COMPRESSED_SIGNED_RG11_EAC: + return 16; + default: + return 8; + } + } + } // namespace + + Bool IsEtcFormat(GLenum compressedFormat) { return DecodedTexelBytes(compressedFormat) != 0; } + + SizeT DecodedTexelBytes(GLenum compressedFormat) { + switch (compressedFormat) { + case GL_COMPRESSED_RGB8_ETC2: + case GL_COMPRESSED_SRGB8_ETC2: + return 3; + case GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2: + case GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2: + case GL_COMPRESSED_RGBA8_ETC2_EAC: + case GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC: + return 4; + case GL_COMPRESSED_R11_EAC: + case GL_COMPRESSED_SIGNED_R11_EAC: + return 1; + case GL_COMPRESSED_RG11_EAC: + case GL_COMPRESSED_SIGNED_RG11_EAC: + return 2; + default: + return 0; + } + } + + Bool DecodeBlock(GLenum compressedFormat, const Uint8* block, Uint8* out) { + Uint8 rgba[16][4]; + switch (compressedFormat) { + case GL_COMPRESSED_RGB8_ETC2: + case GL_COMPRESSED_SRGB8_ETC2: + DecodeEtc2Rgb(block, false, rgba); + for (Int t = 0; t < 16; ++t) std::copy(rgba[t], rgba[t] + 3, out + t * 3); + return true; + case GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2: + case GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2: + DecodeEtc2Rgb(block, true, rgba); + for (Int t = 0; t < 16; ++t) std::copy(rgba[t], rgba[t] + 4, out + t * 4); + return true; + case GL_COMPRESSED_RGBA8_ETC2_EAC: + case GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC: { + Uint8 alpha[16]; + DecodeEacAlpha(block, alpha); + DecodeEtc2Rgb(block + 8, false, rgba); + for (Int t = 0; t < 16; ++t) { + std::copy(rgba[t], rgba[t] + 3, out + t * 4); + out[t * 4 + 3] = alpha[t]; + } + return true; + } + case GL_COMPRESSED_R11_EAC: + case GL_COMPRESSED_SIGNED_R11_EAC: { + const Bool isSigned = compressedFormat == GL_COMPRESSED_SIGNED_R11_EAC; + Int red[16]; + DecodeEac11(block, isSigned, red); + for (Int t = 0; t < 16; ++t) out[t] = Eac11ToByte(red[t], isSigned); + return true; + } + case GL_COMPRESSED_RG11_EAC: + case GL_COMPRESSED_SIGNED_RG11_EAC: { + const Bool isSigned = compressedFormat == GL_COMPRESSED_SIGNED_RG11_EAC; + Int red[16]; + Int green[16]; + DecodeEac11(block, isSigned, red); + DecodeEac11(block + 8, isSigned, green); + for (Int t = 0; t < 16; ++t) { + out[t * 2] = Eac11ToByte(red[t], isSigned); + out[t * 2 + 1] = Eac11ToByte(green[t], isSigned); + } + return true; + } + default: + return false; + } + } + + Bool DecodeRegion(GLenum compressedFormat, const Uint8* blocks, Int width, Int height, Int depth, Uint8* dest, + Int destWidth, Int destHeight, Int destDepth, Int destX, Int destY, Int destZ) { + const SizeT texelBytes = DecodedTexelBytes(compressedFormat); + if (texelBytes == 0 || blocks == nullptr || dest == nullptr) return false; + if (width <= 0 || height <= 0 || depth <= 0) return true; + const SizeT blockBytes = BlockBytes(compressedFormat); + const Int blocksX = (width + 3) / 4; + const Int blocksY = (height + 3) / 4; + Uint8 texels[16 * 4]; + const Uint8* block = blocks; + for (Int z = 0; z < depth; ++z) { + const Int dz = destZ + z; + for (Int by = 0; by < blocksY; ++by) { + for (Int bx = 0; bx < blocksX; ++bx, block += blockBytes) { + if (dz < 0 || dz >= destDepth) continue; + DecodeBlock(compressedFormat, block, texels); + for (Int y = 0; y < 4; ++y) { + const Int sy = by * 4 + y; + const Int dy = destY + sy; + if (sy >= height || dy < 0 || dy >= destHeight) continue; + for (Int x = 0; x < 4; ++x) { + const Int sx = bx * 4 + x; + const Int dx = destX + sx; + if (sx >= width || dx < 0 || dx >= destWidth) continue; + const SizeT offset = + ((static_cast(dz) * static_cast(destHeight) + static_cast(dy)) * + static_cast(destWidth) + + static_cast(dx)) * + texelBytes; + std::copy(texels + (y * 4 + x) * texelBytes, texels + (y * 4 + x + 1) * texelBytes, + dest + offset); + } + } + } + } + } + return true; + } +} // namespace MobileGL::MG_Util::EtcDecoder diff --git a/MobileGL/MG_Util/Texture/EtcDecoder.h b/MobileGL/MG_Util/Texture/EtcDecoder.h new file mode 100644 index 00000000..c67aeb6c --- /dev/null +++ b/MobileGL/MG_Util/Texture/EtcDecoder.h @@ -0,0 +1,41 @@ +// MobileGL - MobileGL/MG_Util/Texture/EtcDecoder.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once +#include + +// CPU decoder for the ten ETC2/EAC formats core GL requires from 4.3 on (GL 4.6 core +// appendix C.2). The CPU shadow keeps a compressed level in the UNCOMPRESSED storage its +// format resolves to (ConvertGLEnumToTextureInternalFormat), and this is what fills that +// storage from the application's blocks, so the level samples - and reads back - as its +// texels on every backend. The compressed bytes themselves stay beside it for +// glGetCompressedTexImage. +namespace MobileGL::MG_Util::EtcDecoder { + // True for the ten GL_COMPRESSED_*ETC2* / *EAC* enums. + Bool IsEtcFormat(GLenum compressedFormat); + + // Bytes one decoded texel takes in the uncompressed storage the format resolves to: + // RGB8 / SRGB8 -> 3, RGBA8 / SRGB8_ALPHA8 -> 4, R8 / R8_SNORM -> 1, RG8 / RG8_SNORM -> 2. + // 0 for a format IsEtcFormat rejects. + SizeT DecodedTexelBytes(GLenum compressedFormat); + + // Decodes ONE 4x4 block (8 or 16 bytes, per the format) into `out`, 16 texels in row-major + // order (texel (x, y) at index y * 4 + x), DecodedTexelBytes(format) bytes each. The signed + // EAC formats write two's-complement SNORM bytes. Returns false for a format IsEtcFormat + // rejects, leaving `out` untouched. + Bool DecodeBlock(GLenum compressedFormat, const Uint8* block, Uint8* out); + + // Decodes a run of whole blocks covering a (width x height x depth) region and writes the + // texels that fall inside it into a tightly packed destination image of + // (destWidth x destHeight x destDepth), at (destX, destY, destZ). `blocks` is slice-major, + // then row-major in blocks, exactly as glCompressedTex*Image* lays it out. Texels of the + // last block row/column beyond the region (an image edge that is not a multiple of 4) are + // dropped, as are texels beyond the destination. + Bool DecodeRegion(GLenum compressedFormat, const Uint8* blocks, Int width, Int height, Int depth, Uint8* dest, + Int destWidth, Int destHeight, Int destDepth, Int destX, Int destY, Int destZ); +} // namespace MobileGL::MG_Util::EtcDecoder