mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Fix] (Texture): decode ETC2/EAC uploads into the texel shadow
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user