[Fix] (Texture): decode ETC2/EAC uploads into the texel shadow

This commit is contained in:
2026-10-12 08:32:53 -04:00
parent 1962e0c603
commit c9bf43b710
5 changed files with 558 additions and 11 deletions
+1
View File
@@ -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
+50 -11
View File
@@ -20,6 +20,7 @@
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Texture/PixelStoreProcessor.h>
#include <MG_Util/Texture/TextureFormatProcessor.h>
#include <MG_Util/Texture/EtcDecoder.h>
#include <MG_Util/Classifiers/TextureEnumClassifier.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
@@ -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<SizeT>(levelSize.x()) * static_cast<SizeT>(levelSize.y()) *
static_cast<SizeT>(levelDepth) * texelBytes;
if (levelBytes == 0 || texture->GetMipmapByteSize(uploadTarget, level) < levelBytes) return false;
auto* shadow = static_cast<Uint8*>(texture->MapMipmapData(uploadTarget, level));
if (shadow == nullptr) return false;
MG_Util::EtcDecoder::DecodeRegion(format, static_cast<const Uint8*>(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<Bool> 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<Uint>(level), format,
blob.data(), blobSize);
DecodeEtcBlocksIntoTexelShadow(textureMipmapObject, textureUploadTarget, static_cast<Uint>(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<Bool> 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<Uint>(level), format,
blob.data(), blobSize);
DecodeEtcBlocksIntoTexelShadow(textureMipmapObject, textureUploadTarget, static_cast<Uint>(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<Uint>(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<Uint>(level),
internalformat, compressedBytes, {0, 0, 0}, {width, height, 1});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
}
+91
View File
@@ -36,6 +36,7 @@
#include <MG_Util/Math/SmallFloat.h>
#include <MG_Util/Texture/PixelStoreProcessor.h>
#include <MG_Util/Texture/TextureFormatProcessor.h>
#include <MG_Util/Texture/EtcDecoder.h>
#include <cstring>
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<Uint8>(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<GLsizei>(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<GLsizei>(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);
}
+375
View File
@@ -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 <algorithm>
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<Uint32>((word >> low) & ((Uint64{1} << (high - low + 1)) - 1));
}
Uint8 Clamp255(Int value) { return static_cast<Uint8>(std::clamp(value, 0, 255)); }
Int Extend4(Uint32 v) { return static_cast<Int>((v << 4) | v); }
Int Extend5(Uint32 v) { return static_cast<Int>((v << 3) | (v >> 2)); }
Int Extend6(Uint32 v) { return static_cast<Int>((v << 2) | (v >> 4)); }
Int Extend7(Uint32 v) { return static_cast<Int>((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<Int>((((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<Int>(v) - 8 : static_cast<Int>(v); };
const Int r = static_cast<Int>(Bits(word, 63, 59));
const Int g = static_cast<Int>(Bits(word, 55, 51));
const Int b = static_cast<Int>(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<Uint32>(r));
base[1][0] = Extend5(static_cast<Uint32>(r2));
base[0][1] = Extend5(static_cast<Uint32>(g));
base[1][1] = Extend5(static_cast<Uint32>(g2));
base[0][2] = Extend5(static_cast<Uint32>(b));
base[1][2] = Extend5(static_cast<Uint32>(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<Int>((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<Int>(Bits(word, 63, 56));
const Int multiplier = static_cast<Int>(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<Int>(Bits(word, 63, 56));
if (isSigned) {
base = static_cast<Int>(static_cast<int8_t>(static_cast<Uint8>(base)));
if (base == -128) base = -127;
}
const Int multiplier = static_cast<Int>(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<Uint8>(static_cast<int8_t>(value < 0 ? -magnitude : magnitude));
}
return static_cast<Uint8>((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<SizeT>(dz) * static_cast<SizeT>(destHeight) + static_cast<SizeT>(dy)) *
static_cast<SizeT>(destWidth) +
static_cast<SizeT>(dx)) *
texelBytes;
std::copy(texels + (y * 4 + x) * texelBytes, texels + (y * 4 + x + 1) * texelBytes,
dest + offset);
}
}
}
}
}
return true;
}
} // namespace MobileGL::MG_Util::EtcDecoder
+41
View File
@@ -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 <Includes.h>
// 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