From b831dae8d516349ce39be8a2e84f2bd52ff79e67 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 11:08:02 -0400 Subject: [PATCH] [Feat] (MG_Backend/DirectGLES): packed-type readback encoding - repack wide RGBA reads into all GL 3.3 packed pixel types (3_3_2/2_3_3_REV, 5_6_5(_REV), 4_4_4_4(_REV), 5_5_5_1/1_5_5_5_REV, 8_8_8_8(_REV), 10_10_10_2/2_10_10_10_REV, packed-float 10F_11F_11F_REV and shared-exponent 5_9_9_9_REV) for ReadPixels/GetTexImage; conversion helpers extracted to context-free ReadbackImpl (Utils.cpp) with unit tests asserting exact packed words against the GL CTS pack_* oracle layouts --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 250 +----------- MobileGL/MG_Backend/DirectGLES/Utils.cpp | 380 ++++++++++++++++++ MobileGL/MG_Backend/DirectGLES/Utils.h | 45 +++ .../MG_Test/Framebuffer/FramebufferTest.cpp | 178 ++++++++ 4 files changed, 621 insertions(+), 232 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 61b98e32..35bcc6bb 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -3269,69 +3269,13 @@ namespace MobileGL::MG_Backend::DirectGLES { // GL_RGBA/GL_FLOAT for float buffers plus one implementation-defined pair, while desktop GL clients read // back narrower layouts (RED, RG, RGB, BGR, byte-order packed types, ...). For those we read a guaranteed // wide RGBA format into scratch memory and repack into the caller's (format, type) layout on the CPU, - // honoring the client-side PACK pixel-store parameters. + // honoring the client-side PACK pixel-store parameters. The pure repacking helpers live in + // ReadbackImpl (Utils.cpp) so unit tests can assert the exact packed words. - using MG_Util::DecodeHalfBitsToFloat; - using MG_Util::EncodeFloatToHalfBits; - - struct ReadbackChannelMapping { - Int sourceChannel[4]; // RGBA source channel feeding each destination channel - Int channelCount; // destination channel count - Bool isInteger; - }; - - static Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping) { - switch (format) { - case GL_RED: outMapping = {{0, 0, 0, 0}, 1, false}; return true; - case GL_RED_INTEGER: outMapping = {{0, 0, 0, 0}, 1, true}; return true; - // Desktop-GL single-channel client formats (GL CTS packed_pixels rgba8_format_green/blue): - // the destination holds one component sourced from the named channel of the wide RGBA read. - // GL_ALPHA is mapped here from the raw enum because the state layer folds it into Red for the - // legacy alpha-texture upload hack. - case GL_GREEN: outMapping = {{1, 0, 0, 0}, 1, false}; return true; - case GL_GREEN_INTEGER: outMapping = {{1, 0, 0, 0}, 1, true}; return true; - case GL_BLUE: outMapping = {{2, 0, 0, 0}, 1, false}; return true; - case GL_BLUE_INTEGER: outMapping = {{2, 0, 0, 0}, 1, true}; return true; - case GL_ALPHA: outMapping = {{3, 0, 0, 0}, 1, false}; return true; - case GL_ALPHA_INTEGER: outMapping = {{3, 0, 0, 0}, 1, true}; return true; - case GL_RG: outMapping = {{0, 1, 0, 0}, 2, false}; return true; - case GL_RG_INTEGER: outMapping = {{0, 1, 0, 0}, 2, true}; return true; - case GL_RGB: outMapping = {{0, 1, 2, 0}, 3, false}; return true; - case GL_RGB_INTEGER: outMapping = {{0, 1, 2, 0}, 3, true}; return true; - case GL_BGR: outMapping = {{2, 1, 0, 0}, 3, false}; return true; - case GL_BGR_INTEGER: outMapping = {{2, 1, 0, 0}, 3, true}; return true; - case GL_RGBA: outMapping = {{0, 1, 2, 3}, 4, false}; return true; - case GL_RGBA_INTEGER: outMapping = {{0, 1, 2, 3}, 4, true}; return true; - case GL_BGRA: outMapping = {{2, 1, 0, 3}, 4, false}; return true; - case GL_BGRA_INTEGER: outMapping = {{2, 1, 0, 3}, 4, true}; return true; - default: - return false; - } - } - - static Bool IsPackedReadback8888Type(GLenum type) { - return type == GL_UNSIGNED_INT_8_8_8_8 || type == GL_UNSIGNED_INT_8_8_8_8_REV; - } - - static SizeT GetReadbackComponentSize(GLenum type) { - switch (type) { - case GL_UNSIGNED_BYTE: - case GL_BYTE: - return 1; - case GL_UNSIGNED_SHORT: - case GL_SHORT: - case GL_HALF_FLOAT: - return 2; - case GL_UNSIGNED_INT: - case GL_INT: - case GL_FLOAT: - case GL_UNSIGNED_INT_8_8_8_8: - case GL_UNSIGNED_INT_8_8_8_8_REV: - return 4; - default: - return 0; - } - } + using ReadbackImpl::GetReadbackChannelMapping; + using ReadbackImpl::GetReadbackComponentSize; + using ReadbackImpl::GetReadbackDstPixelSize; + using ReadbackImpl::ReadbackChannelMapping; static Bool CanDecodeWideSourceType(GLenum type) { switch (type) { @@ -3379,17 +3323,14 @@ namespace MobileGL::MG_Backend::DirectGLES { if (!GetReadbackChannelMapping(format, mapping)) { return false; } - const Bool packed8888 = IsPackedReadback8888Type(type); - if (packed8888 && mapping.channelCount != 4) { - return false; - } - if (mapping.isInteger && (type == GL_FLOAT || type == GL_HALF_FLOAT)) { + // Covers unknown types, packed field-count/format mismatches and float types on integer formats. + const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type); + if (dstPixelBytes == 0) { return false; } + ReadbackImpl::PackedReadbackLayout packedLayout{}; + const Bool isPackedType = ReadbackImpl::GetPackedReadbackLayout(type, packedLayout); const SizeT dstComponentSize = GetReadbackComponentSize(type); - if (dstComponentSize == 0) { - return false; - } if (width <= 0 || height <= 0) { return true; @@ -3473,8 +3414,6 @@ namespace MobileGL::MG_Backend::DirectGLES { // Destination layout is computed from the client-side PACK parameters; only the actual pixel // rows are written so skip regions of the destination stay untouched. const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false); - const SizeT dstPixelBytes = - packed8888 ? sizeof(Uint32) : static_cast(mapping.channelCount) * dstComponentSize; const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment); const SizeT dstSkipOffset = static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + @@ -3497,164 +3436,11 @@ namespace MobileGL::MG_Backend::DirectGLES { for (GLsizei row = 0; row < height; ++row) { const Uint8* srcRow = wide.data() + static_cast(row) * static_cast(width) * srcPixelBytes; - for (GLsizei col = 0; col < width; ++col) { - const Uint8* srcPixel = srcRow + static_cast(col) * srcPixelBytes; - Uint8* dstPixel = convertedRow.data() + static_cast(col) * dstPixelBytes; - if (mapping.isInteger) { - Int64 src[4]; - for (Int c = 0; c < 4; ++c) { - src[c] = wideType == GL_INT - ? static_cast(reinterpret_cast(srcPixel)[c]) - : static_cast(reinterpret_cast(srcPixel)[c]); - } - if (packed8888) { - Uint32 word = 0; - for (Int ch = 0; ch < 4; ++ch) { - const auto v = - static_cast(std::clamp(src[mapping.sourceChannel[ch]], 0, 255)); - word |= type == GL_UNSIGNED_INT_8_8_8_8 ? v << (24 - ch * 8) : v << (ch * 8); - } - Memcpy(dstPixel, &word, sizeof(word)); - } else { - for (Int ch = 0; ch < mapping.channelCount; ++ch) { - const Int64 v = src[mapping.sourceChannel[ch]]; - Uint8* dstComponent = dstPixel + static_cast(ch) * dstComponentSize; - switch (type) { - case GL_UNSIGNED_BYTE: - *dstComponent = static_cast(std::clamp(v, 0, 255)); - break; - case GL_BYTE: { - const auto out = static_cast(std::clamp(v, -128, 127)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_UNSIGNED_SHORT: { - const auto out = static_cast(std::clamp(v, 0, 65535)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_SHORT: { - const auto out = static_cast(std::clamp(v, -32768, 32767)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_UNSIGNED_INT: { - const auto out = static_cast(std::clamp(v, 0, 4294967295LL)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_INT: { - const auto out = - static_cast(std::clamp(v, -2147483648LL, 2147483647LL)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - default: - break; - } - } - } - } else { - Float src[4]; - switch (wideType) { - case GL_UNSIGNED_BYTE: - for (Int c = 0; c < 4; ++c) { - src[c] = static_cast(srcPixel[c]) / 255.0f; - } - break; - case GL_BYTE: - for (Int c = 0; c < 4; ++c) { - src[c] = std::max( - static_cast(reinterpret_cast(srcPixel)[c]) / 127.0f, -1.0f); - } - break; - case GL_UNSIGNED_SHORT: - for (Int c = 0; c < 4; ++c) { - src[c] = static_cast(reinterpret_cast(srcPixel)[c]) / 65535.0f; - } - break; - case GL_SHORT: - for (Int c = 0; c < 4; ++c) { - src[c] = std::max( - static_cast(reinterpret_cast(srcPixel)[c]) / 32767.0f, -1.0f); - } - break; - case GL_HALF_FLOAT: - for (Int c = 0; c < 4; ++c) { - src[c] = DecodeHalfBitsToFloat(reinterpret_cast(srcPixel)[c]); - } - break; - default: // GL_FLOAT - for (Int c = 0; c < 4; ++c) { - src[c] = reinterpret_cast(srcPixel)[c]; - } - break; - } - if (packed8888) { - Uint32 word = 0; - for (Int ch = 0; ch < 4; ++ch) { - const auto v = static_cast( - std::llround(std::clamp(src[mapping.sourceChannel[ch]], 0.0f, 1.0f) * 255.0)); - word |= type == GL_UNSIGNED_INT_8_8_8_8 ? v << (24 - ch * 8) : v << (ch * 8); - } - Memcpy(dstPixel, &word, sizeof(word)); - } else { - for (Int ch = 0; ch < mapping.channelCount; ++ch) { - const Float v = src[mapping.sourceChannel[ch]]; - Uint8* dstComponent = dstPixel + static_cast(ch) * dstComponentSize; - switch (type) { - case GL_UNSIGNED_BYTE: - *dstComponent = - static_cast(std::llround(std::clamp(v, 0.0f, 1.0f) * 255.0)); - break; - case GL_BYTE: { - const auto out = - static_cast(std::llround(std::clamp(v, -1.0f, 1.0f) * 127.0)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_UNSIGNED_SHORT: { - const auto out = - static_cast(std::llround(std::clamp(v, 0.0f, 1.0f) * 65535.0)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_SHORT: { - const auto out = - static_cast(std::llround(std::clamp(v, -1.0f, 1.0f) * 32767.0)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_UNSIGNED_INT: { - const auto out = static_cast( - std::llround(static_cast(std::clamp(v, 0.0f, 1.0f)) * 4294967295.0)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_INT: { - const auto out = static_cast( - std::llround(static_cast(std::clamp(v, -1.0f, 1.0f)) * 2147483647.0)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_FLOAT: - Memcpy(dstComponent, &v, sizeof(v)); - break; - case GL_HALF_FLOAT: { - const Uint16 out = EncodeFloatToHalfBits(v); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - default: - break; - } - } - } - } - } + ReadbackImpl::ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast(width), wideType, + mapping, type); if (packParams.SwapBytes) { - const SizeT groupSize = packed8888 ? sizeof(Uint32) : dstComponentSize; + const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize; if (groupSize > 1) { for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) { std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize); @@ -3696,8 +3482,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // of killing the process; spec-invalid combinations are already rejected with GL errors at the state layer. const Bool useNativeReadback = IsLegacyNativeReadPixelsFormat(format) && IsLegacyNativeReadPixelsType(type); ReadbackChannelMapping conversionMapping{}; - const Bool convertible = - GetReadbackChannelMapping(format, conversionMapping) && GetReadbackComponentSize(type) != 0; + const Bool convertible = GetReadbackChannelMapping(format, conversionMapping) && + GetReadbackDstPixelSize(conversionMapping, type) != 0; if (!useNativeReadback && !convertible) { MGLOG_E("ReadPixels: format %s with type %s is not implemented yet, skipping readback", MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); @@ -3838,8 +3624,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // spec-invalid combinations are already rejected with GL errors at the state layer. const Bool useNativeReadback = IsNativeGetTexImagePair(format, type); ReadbackChannelMapping conversionMapping{}; - const Bool convertible = - GetReadbackChannelMapping(format, conversionMapping) && GetReadbackComponentSize(type) != 0; + const Bool convertible = GetReadbackChannelMapping(format, conversionMapping) && + GetReadbackDstPixelSize(conversionMapping, type) != 0; if (!useNativeReadback && !convertible) { MGLOG_E("GetTexImage: format %s with type %s is not implemented yet, skipping readback", MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index 4b28e477..f446e25a 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -18,6 +18,9 @@ #include #include #include +#include + +#include namespace MobileGL::MG_Backend::DirectGLES { namespace { @@ -448,4 +451,381 @@ namespace MobileGL::MG_Backend::DirectGLES { } } } // namespace Utils + + // ---- Client-format readback conversion helpers ------------------------------------------------- + // ReadPixels/GetTexImage read a guaranteed wide RGBA(_INTEGER) layout from the ES driver and repack + // it on the CPU into the client's (format, type) layout. Everything here is pure byte shuffling so + // unit tests can assert the exact packed words; field positions follow GL 3.3 table 3.6 and mirror + // the GL CTS packed_pixels oracle (glcPackedPixelsTests.cpp pack_UNSIGNED_* helpers). + namespace ReadbackImpl { + using MG_Util::DecodeHalfBitsToFloat; + using MG_Util::EncodeFloatToHalfBits; + + Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping) { + switch (format) { + case GL_RED: outMapping = {{0, 0, 0, 0}, 1, false}; return true; + case GL_RED_INTEGER: outMapping = {{0, 0, 0, 0}, 1, true}; return true; + // Desktop-GL single-channel client formats (GL CTS packed_pixels rgba8_format_green/blue): + // the destination holds one component sourced from the named channel of the wide RGBA read. + // GL_ALPHA is mapped here from the raw enum because the state layer folds it into Red for the + // legacy alpha-texture upload hack. + case GL_GREEN: outMapping = {{1, 0, 0, 0}, 1, false}; return true; + case GL_GREEN_INTEGER: outMapping = {{1, 0, 0, 0}, 1, true}; return true; + case GL_BLUE: outMapping = {{2, 0, 0, 0}, 1, false}; return true; + case GL_BLUE_INTEGER: outMapping = {{2, 0, 0, 0}, 1, true}; return true; + case GL_ALPHA: outMapping = {{3, 0, 0, 0}, 1, false}; return true; + case GL_ALPHA_INTEGER: outMapping = {{3, 0, 0, 0}, 1, true}; return true; + case GL_RG: outMapping = {{0, 1, 0, 0}, 2, false}; return true; + case GL_RG_INTEGER: outMapping = {{0, 1, 0, 0}, 2, true}; return true; + case GL_RGB: outMapping = {{0, 1, 2, 0}, 3, false}; return true; + case GL_RGB_INTEGER: outMapping = {{0, 1, 2, 0}, 3, true}; return true; + case GL_BGR: outMapping = {{2, 1, 0, 0}, 3, false}; return true; + case GL_BGR_INTEGER: outMapping = {{2, 1, 0, 0}, 3, true}; return true; + case GL_RGBA: outMapping = {{0, 1, 2, 3}, 4, false}; return true; + case GL_RGBA_INTEGER: outMapping = {{0, 1, 2, 3}, 4, true}; return true; + case GL_BGRA: outMapping = {{2, 1, 0, 3}, 4, false}; return true; + case GL_BGRA_INTEGER: outMapping = {{2, 1, 0, 3}, 4, true}; return true; + default: + return false; + } + } + + Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out) { + switch (type) { + // Non-REV types pack the first format component starting at the most significant bit, + // *_REV types starting at the least significant bit (GL CTS pack_UNSIGNED_SHORT_5_6_5: + // R bits 15-11; pack_UNSIGNED_SHORT_1_5_5_5_REV: R bits 4-0, A bit 15). + case GL_UNSIGNED_BYTE_3_3_2: out = {3, {3, 3, 2, 0}, {5, 2, 0, 0}, 1, false}; return true; + case GL_UNSIGNED_BYTE_2_3_3_REV: out = {3, {3, 3, 2, 0}, {0, 3, 6, 0}, 1, false}; return true; + case GL_UNSIGNED_SHORT_5_6_5: out = {3, {5, 6, 5, 0}, {11, 5, 0, 0}, 2, false}; return true; + case GL_UNSIGNED_SHORT_5_6_5_REV: out = {3, {5, 6, 5, 0}, {0, 5, 11, 0}, 2, false}; return true; + case GL_UNSIGNED_SHORT_4_4_4_4: out = {4, {4, 4, 4, 4}, {12, 8, 4, 0}, 2, false}; return true; + case GL_UNSIGNED_SHORT_4_4_4_4_REV: out = {4, {4, 4, 4, 4}, {0, 4, 8, 12}, 2, false}; return true; + case GL_UNSIGNED_SHORT_5_5_5_1: out = {4, {5, 5, 5, 1}, {11, 6, 1, 0}, 2, false}; return true; + case GL_UNSIGNED_SHORT_1_5_5_5_REV: out = {4, {5, 5, 5, 1}, {0, 5, 10, 15}, 2, false}; return true; + case GL_UNSIGNED_INT_8_8_8_8: out = {4, {8, 8, 8, 8}, {24, 16, 8, 0}, 4, false}; return true; + case GL_UNSIGNED_INT_8_8_8_8_REV: out = {4, {8, 8, 8, 8}, {0, 8, 16, 24}, 4, false}; return true; + case GL_UNSIGNED_INT_10_10_10_2: out = {4, {10, 10, 10, 2}, {22, 12, 2, 0}, 4, false}; return true; + case GL_UNSIGNED_INT_2_10_10_10_REV: out = {4, {10, 10, 10, 2}, {0, 10, 20, 30}, 4, false}; return true; + // Packed-float RGB types: fields hold unsigned small floats; 5_9_9_9_REV's shared 5-bit + // exponent (bits 31-27) is emitted by EncodeSharedExponentRGB9E5, not a component field. + case GL_UNSIGNED_INT_10F_11F_11F_REV: out = {3, {11, 11, 10, 0}, {0, 11, 22, 0}, 4, true}; return true; + case GL_UNSIGNED_INT_5_9_9_9_REV: out = {3, {9, 9, 9, 0}, {0, 9, 18, 0}, 4, true}; return true; + default: + return false; + } + } + + SizeT GetReadbackComponentSize(GLenum type) { + PackedReadbackLayout packedLayout{}; + if (GetPackedReadbackLayout(type, packedLayout)) { + return packedLayout.byteSize; + } + switch (type) { + case GL_UNSIGNED_BYTE: + case GL_BYTE: + return 1; + case GL_UNSIGNED_SHORT: + case GL_SHORT: + case GL_HALF_FLOAT: + return 2; + case GL_UNSIGNED_INT: + case GL_INT: + case GL_FLOAT: + return 4; + default: + return 0; + } + } + + SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type) { + PackedReadbackLayout packedLayout{}; + if (GetPackedReadbackLayout(type, packedLayout)) { + if (packedLayout.fieldCount != mapping.channelCount) { + return 0; // 3-field packed types pair with 3-component formats only, 4 with 4 + } + if (mapping.isInteger && packedLayout.isFloatPacked) { + return 0; // packed-float RGB types never pair with integer formats + } + return packedLayout.byteSize; + } + if (mapping.isInteger && (type == GL_FLOAT || type == GL_HALF_FLOAT)) { + return 0; + } + const SizeT componentSize = GetReadbackComponentSize(type); + return componentSize == 0 ? 0 : static_cast(mapping.channelCount) * componentSize; + } + + namespace { + // Encodes an unsigned small float with a 5-bit exponent (bias 15) and mantissaBits mantissa + // bits, per the EXT_packed_float conversion rules: negatives (including -Inf) go to zero, + // +Inf stays +Inf, NaN stays NaN, and finite values above the largest representable value + // clamp to it. The mantissa is truncated (rounding mode is implementation-defined). + Uint32 EncodeFloatToUnsignedSmallFloat(Float value, Int mantissaBits) { + const Uint32 bits = std::bit_cast(value); + const Bool negative = (bits & 0x80000000u) != 0; + const Uint32 exponent = (bits >> 23) & 0xFFu; + const Uint32 mantissa = bits & 0x7FFFFFu; + const Uint32 exponentMask = 0x1Fu << mantissaBits; + if (exponent == 0xFFu) { + if (mantissa != 0) { + return exponentMask | 1u; // NaN keeps NaN + } + return negative ? 0u : exponentMask; // -Inf -> 0, +Inf -> +Inf + } + if (negative) { + return 0u; + } + const Int32 smallExponent = static_cast(exponent) - 127 + 15; + if (smallExponent >= 31) { // above the largest finite value -> clamp to it + return ((31u - 1u) << mantissaBits) | ((1u << mantissaBits) - 1u); + } + if (smallExponent <= 0) { // subnormal range: renormalize, flushing tiny values to zero + const Uint32 fullMantissa = mantissa | 0x800000u; + const Int32 shift = (23 - mantissaBits) + 1 - smallExponent; + return shift > 23 ? 0u : fullMantissa >> shift; + } + return (static_cast(smallExponent) << mantissaBits) | + (mantissa >> (23u - static_cast(mantissaBits))); + } + + void WritePackedReadbackWord(Uint8* dst, Uint32 word, SizeT byteSize) { + switch (byteSize) { + case 1: { + const auto out = static_cast(word); + Memcpy(dst, &out, sizeof(out)); + break; + } + case 2: { + const auto out = static_cast(word); + Memcpy(dst, &out, sizeof(out)); + break; + } + default: + Memcpy(dst, &word, sizeof(word)); + break; + } + } + } // namespace + + Uint32 EncodeFloatToUnsignedF11(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 6); } + Uint32 EncodeFloatToUnsignedF10(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 5); } + + // RGB9E5 shared-exponent encode, following the EXT_texture_shared_exponent spec algorithm + // (N = 9 mantissa bits, B = 15 exponent bias, Emax = 31). + Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) { + constexpr Int kMantissaBits = 9; + constexpr Int kExponentBias = 15; + constexpr Float kSharedExpMax = 511.0f / 512.0f * 65536.0f; // (2^N-1)/2^N * 2^(Emax-B) + + Float clamped[3]; + for (Int i = 0; i < 3; ++i) { + const Float v = rgb[i]; + clamped[i] = (std::isnan(v) || v < 0.0f) ? 0.0f : std::min(v, kSharedExpMax); + } + const Float maxComponent = std::max(clamped[0], std::max(clamped[1], clamped[2])); + + Int sharedExponent = 0; // all-zero input keeps the all-zero word + if (maxComponent > 0.0f) { + sharedExponent = std::max(-kExponentBias - 1, static_cast(std::floor(std::log2(maxComponent)))) + + 1 + kExponentBias; + const Float maxScaled = std::floor( + maxComponent / std::exp2(static_cast(sharedExponent - kExponentBias - kMantissaBits)) + + 0.5f); + if (maxScaled >= 512.0f) { // rounded up to 2^N: bump the shared exponent instead + ++sharedExponent; + } + } + + const Float scale = std::exp2(static_cast(sharedExponent - kExponentBias - kMantissaBits)); + Uint32 word = static_cast(sharedExponent) << 27; + for (Int i = 0; i < 3; ++i) { + const auto field = static_cast(std::floor(clamped[i] / scale + 0.5f)); + word |= std::min(field, 511u) << (i * kMantissaBits); + } + return word; + } + + void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType, + const ReadbackChannelMapping& mapping, GLenum type) { + PackedReadbackLayout packedLayout{}; + const Bool isPacked = GetPackedReadbackLayout(type, packedLayout); + const SizeT dstComponentSize = GetReadbackComponentSize(type); + const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type); + const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType); + + for (SizeT col = 0; col < width; ++col) { + const Uint8* srcPixel = src + col * srcPixelBytes; + Uint8* dstPixel = dst + col * dstPixelBytes; + if (mapping.isInteger) { + Int64 srcValues[4]; + for (Int c = 0; c < 4; ++c) { + srcValues[c] = wideType == GL_INT + ? static_cast(reinterpret_cast(srcPixel)[c]) + : static_cast(reinterpret_cast(srcPixel)[c]); + } + if (isPacked) { + // Integer sources clamp each component to the unsigned range of its field + // (GL 3.3 section 4.3.1 final conversion). + Uint32 word = 0; + for (Int ch = 0; ch < packedLayout.fieldCount; ++ch) { + const Int64 fieldMax = (Int64{1} << packedLayout.width[ch]) - 1; + const auto v = static_cast( + std::clamp(srcValues[mapping.sourceChannel[ch]], 0, fieldMax)); + word |= v << packedLayout.shift[ch]; + } + WritePackedReadbackWord(dstPixel, word, packedLayout.byteSize); + } else { + for (Int ch = 0; ch < mapping.channelCount; ++ch) { + const Int64 v = srcValues[mapping.sourceChannel[ch]]; + Uint8* dstComponent = dstPixel + static_cast(ch) * dstComponentSize; + switch (type) { + case GL_UNSIGNED_BYTE: + *dstComponent = static_cast(std::clamp(v, 0, 255)); + break; + case GL_BYTE: { + const auto out = static_cast(std::clamp(v, -128, 127)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_UNSIGNED_SHORT: { + const auto out = static_cast(std::clamp(v, 0, 65535)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_SHORT: { + const auto out = static_cast(std::clamp(v, -32768, 32767)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_UNSIGNED_INT: { + const auto out = static_cast(std::clamp(v, 0, 4294967295LL)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_INT: { + const auto out = + static_cast(std::clamp(v, -2147483648LL, 2147483647LL)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + default: + break; + } + } + } + } else { + Float srcValues[4]; + switch (wideType) { + case GL_UNSIGNED_BYTE: + for (Int c = 0; c < 4; ++c) { + srcValues[c] = static_cast(srcPixel[c]) / 255.0f; + } + break; + case GL_BYTE: + for (Int c = 0; c < 4; ++c) { + srcValues[c] = std::max( + static_cast(reinterpret_cast(srcPixel)[c]) / 127.0f, -1.0f); + } + break; + case GL_UNSIGNED_SHORT: + for (Int c = 0; c < 4; ++c) { + srcValues[c] = + static_cast(reinterpret_cast(srcPixel)[c]) / 65535.0f; + } + break; + case GL_SHORT: + for (Int c = 0; c < 4; ++c) { + srcValues[c] = std::max( + static_cast(reinterpret_cast(srcPixel)[c]) / 32767.0f, -1.0f); + } + break; + case GL_HALF_FLOAT: + for (Int c = 0; c < 4; ++c) { + srcValues[c] = DecodeHalfBitsToFloat(reinterpret_cast(srcPixel)[c]); + } + break; + default: // GL_FLOAT + for (Int c = 0; c < 4; ++c) { + srcValues[c] = reinterpret_cast(srcPixel)[c]; + } + break; + } + if (isPacked) { + Uint32 word = 0; + if (packedLayout.isFloatPacked) { + const Float fields[3] = {srcValues[mapping.sourceChannel[0]], + srcValues[mapping.sourceChannel[1]], + srcValues[mapping.sourceChannel[2]]}; + word = type == GL_UNSIGNED_INT_5_9_9_9_REV + ? EncodeSharedExponentRGB9E5(fields) + : (EncodeFloatToUnsignedF11(fields[0]) << packedLayout.shift[0]) | + (EncodeFloatToUnsignedF11(fields[1]) << packedLayout.shift[1]) | + (EncodeFloatToUnsignedF10(fields[2]) << packedLayout.shift[2]); + } else { + // Normalized encode: round(clamp(v, 0, 1) * (2^bits - 1)) into each field. + for (Int ch = 0; ch < packedLayout.fieldCount; ++ch) { + const auto fieldMax = static_cast((1u << packedLayout.width[ch]) - 1u); + const auto v = static_cast(std::llround( + std::clamp(srcValues[mapping.sourceChannel[ch]], 0.0f, 1.0f) * fieldMax)); + word |= v << packedLayout.shift[ch]; + } + } + WritePackedReadbackWord(dstPixel, word, packedLayout.byteSize); + } else { + for (Int ch = 0; ch < mapping.channelCount; ++ch) { + const Float v = srcValues[mapping.sourceChannel[ch]]; + Uint8* dstComponent = dstPixel + static_cast(ch) * dstComponentSize; + switch (type) { + case GL_UNSIGNED_BYTE: + *dstComponent = + static_cast(std::llround(std::clamp(v, 0.0f, 1.0f) * 255.0)); + break; + case GL_BYTE: { + const auto out = + static_cast(std::llround(std::clamp(v, -1.0f, 1.0f) * 127.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_UNSIGNED_SHORT: { + const auto out = + static_cast(std::llround(std::clamp(v, 0.0f, 1.0f) * 65535.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_SHORT: { + const auto out = + static_cast(std::llround(std::clamp(v, -1.0f, 1.0f) * 32767.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_UNSIGNED_INT: { + const auto out = static_cast( + std::llround(static_cast(std::clamp(v, 0.0f, 1.0f)) * 4294967295.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_INT: { + const auto out = static_cast( + std::llround(static_cast(std::clamp(v, -1.0f, 1.0f)) * 2147483647.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_FLOAT: + Memcpy(dstComponent, &v, sizeof(v)); + break; + case GL_HALF_FLOAT: { + const Uint16 out = EncodeFloatToHalfBits(v); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + default: + break; + } + } + } + } + } + } + } // namespace ReadbackImpl } // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index 484a8d0f..1068d0cd 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -45,6 +45,51 @@ namespace MobileGL::MG_Backend::DirectGLES { namespace FramebufferImpl {} // namespace FramebufferImpl + // Pure CPU helpers of the client-format readback conversion (ReadPixels/GetTexImage repack a wide + // RGBA(_INTEGER) read into the caller's (format, type) layout). Kept context-free so unit tests can + // exercise the exact packing the GL CTS packed_pixels oracle compares against. + namespace ReadbackImpl { + struct ReadbackChannelMapping { + Int sourceChannel[4]; // RGBA source channel feeding each destination component + Int channelCount; // destination component count + Bool isInteger; + }; + Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping); + + // Byte size of one destination component of `type`; packed types report the packed word size. + // 0 = type not supported by the conversion path. + SizeT GetReadbackComponentSize(GLenum type); + + // Bit-field layout of a GL packed pixel type. width/shift are indexed in the client format's + // component order (matching ReadbackChannelMapping); shift is the LSB position of the field in + // the packed word: non-REV types pack the first component from the MSB, *_REV types from the + // LSB (GL 3.3 table 3.6; field positions mirror the GL CTS glcPackedPixelsTests pack_* oracle). + struct PackedReadbackLayout { + Int fieldCount; // format components stored in the packed word + Int width[4]; // bit width of each component's field + Int shift[4]; // LSB bit position of each component's field + SizeT byteSize; // packed word size in bytes (1, 2 or 4) + Bool isFloatPacked; // 10F_11F_11F_REV / 5_9_9_9_REV: fields hold unsigned small floats + }; + Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out); + + // Unsigned small-float encoders (EXT_packed_float / EXT_texture_shared_exponent semantics). + Uint32 EncodeFloatToUnsignedF11(Float value); + Uint32 EncodeFloatToUnsignedF10(Float value); + Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]); + + // Destination bytes per pixel for a (format mapping, type) readback pair; 0 when the pair is + // not convertible (unknown type, packed field count != format component count, floating-point + // or packed-float type with an integer format). + SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type); + + // Repacks one row of wide RGBA(_INTEGER) texels (4 components of wideType each) into the + // client's (format, type) layout. src holds width * 4 * GetReadbackComponentSize(wideType) + // bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes. + void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType, + const ReadbackChannelMapping& mapping, GLenum type); + } // namespace ReadbackImpl + namespace PrgramImpl { String ProcessOutColorLocations(const String& glslCode); String ForceSupporterOutput(const String& glslCode); diff --git a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp index 2d38e379..c3b24c5e 100644 --- a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp +++ b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp @@ -11,6 +11,7 @@ #include "Includes.h" #include "Init.h" #include +#include #include #include #include @@ -496,3 +497,180 @@ TEST_F(FramebufferTest, BlitNamedFramebufferAllowsDefaultFramebufferZero) { EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(), defaultRead); EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } + +// ---- Packed-type readback encoding ------------------------------------------------------------------ +// Oracle-independent guard for the DirectGLES client-format readback conversion: feeds known wide RGBA +// rows through ReadbackImpl::ConvertWideReadbackRow and asserts the exact packed words. Field positions +// were hand-computed from GL 3.3 table 3.6 and match the GL CTS packed_pixels comparison functions +// (glcPackedPixelsTests.cpp pack_UNSIGNED_*): non-REV types pack the first format component from the +// most significant bit, *_REV types from the least significant bit. + +namespace { + namespace ReadbackImpl = MG_Backend::DirectGLES::ReadbackImpl; + + // Converts a row of wide pixels (4 components of wideType each) into `format`/`type` words. + template + Vector ConvertWideRowToPackedWords(const Vector& wide, GLenum wideType, GLenum format, + GLenum type) { + ReadbackImpl::ReadbackChannelMapping mapping{}; + EXPECT_TRUE(ReadbackImpl::GetReadbackChannelMapping(format, mapping)); + EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(mapping, type), sizeof(WordT)); + const SizeT width = wide.size() / 4; + Vector out(width, static_cast(0)); + ReadbackImpl::ConvertWideReadbackRow(reinterpret_cast(wide.data()), + reinterpret_cast(out.data()), width, wideType, mapping, + type); + return out; + } + + // Normalized encodes read the wide row as RGBA8 (values are v / 255). + template + Vector ConvertRGBA8Row(const Vector& rgba, GLenum format, GLenum type) { + return ConvertWideRowToPackedWords(rgba, GL_UNSIGNED_BYTE, format, type); + } + + // Wide RGBA8 pattern shared by the normalized-encode tests. Expected fields below are + // round(v / 255 * (2^bits - 1)), computed by hand per pixel. + // R G B A + const Vector kRGBA8Row{255, 0, 128, 64, // P0 + 10, 250, 33, 200, // P1 + 85, 170, 255, 0}; // P2 +} // namespace + +TEST(PackedReadbackEncodeTest, EncodesUnsignedShort565) { + // P0: R=31 G=0 B=round(128*31/255)=16 -> 31<<11 | 0<<5 | 16 = 0xF810 + // P1: R=round(10*31/255)=1 G=round(250*63/255)=62 B=round(33*31/255)=4 -> 1<<11|62<<5|4 = 0x0FC4 + // P2: R=round(85*31/255)=10 G=round(170*63/255)=42 B=31 -> 10<<11|42<<5|31 = 0x555F + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGB, GL_UNSIGNED_SHORT_5_6_5); + EXPECT_EQ(words[0], 0xF810u); + EXPECT_EQ(words[1], 0x0FC4u); + EXPECT_EQ(words[2], 0x555Fu); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedShort565Rev) { + // REV packs R from the LSB: P2 -> 10 | 42<<5 | 31<<11 = 0xFD4A + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGB, GL_UNSIGNED_SHORT_5_6_5_REV); + EXPECT_EQ(words[2], 0xFD4Au); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedShort4444) { + // P0: R=15 G=0 B=round(128*15/255)=8 A=round(64*15/255)=4 -> 0xF084 + // P2: R=round(85*15/255)=5 G=round(170*15/255)=10 B=15 A=0 -> 0x5AF0 + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4); + EXPECT_EQ(words[0], 0xF084u); + EXPECT_EQ(words[2], 0x5AF0u); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedShort4444Rev) { + // P0 fields R=15 G=0 B=8 A=4 packed from the LSB -> 15 | 0<<4 | 8<<8 | 4<<12 = 0x480F + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4_REV); + EXPECT_EQ(words[0], 0x480Fu); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedShort5551) { + // P0: R=31 G=0 B=16 A=round(64/255)=0 -> 31<<11 | 16<<1 = 0xF820 + // P1: R=1 G=round(250*31/255)=30 B=4 A=round(200/255)=1 -> 1<<11|30<<6|4<<1|1 = 0x0F89 + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1); + EXPECT_EQ(words[0], 0xF820u); + EXPECT_EQ(words[1], 0x0F89u); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedShort1555Rev) { + // P1 fields R=1 G=30 B=4 A=1 packed from the LSB -> 1 | 30<<5 | 4<<10 | 1<<15 = 0x93C1 + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_1_5_5_5_REV); + EXPECT_EQ(words[1], 0x93C1u); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedInt2101010Rev) { + // P0: R=1023 G=0 B=round(128*1023/255)=514 A=round(64*3/255)=1 -> 1023|514<<20|1<<30 = 0x602003FF + // P2: R=round(85*1023/255)=341 G=round(170*1023/255)=682 B=1023 A=0 -> 0x3FFAA955 + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV); + EXPECT_EQ(words[0], 0x602003FFu); + EXPECT_EQ(words[2], 0x3FFAA955u); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedInt1010102) { + // P2 fields R=341 G=682 B=1023 A=0 packed from the MSB -> 341<<22 | 682<<12 | 1023<<2 = 0x556AAFFC + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_10_10_10_2); + EXPECT_EQ(words[2], 0x556AAFFCu); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedByte332) { + // P0: R=7 G=0 B=round(128*3/255)=2 -> 7<<5 | 2 = 0xE2 + // P1: R=round(10*7/255)=0 G=round(250*7/255)=7 B=round(33*3/255)=0 -> 7<<2 = 0x1C + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGB, GL_UNSIGNED_BYTE_3_3_2); + EXPECT_EQ(words[0], 0xE2u); + EXPECT_EQ(words[1], 0x1Cu); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedByte233Rev) { + // P0 fields R=7 G=0 B=2 packed from the LSB -> 7 | 0<<3 | 2<<6 = 0x87 + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGB, GL_UNSIGNED_BYTE_2_3_3_REV); + EXPECT_EQ(words[0], 0x87u); +} + +TEST(PackedReadbackEncodeTest, Encodes8888KeepsLegacyByteOrder) { + // Regression for the previously supported types: P0 = (255, 0, 128, 64). + const auto msbFirst = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8); + EXPECT_EQ(msbFirst[0], 0xFF008040u); + const auto lsbFirst = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV); + EXPECT_EQ(lsbFirst[0], 0x408000FFu); +} + +TEST(PackedReadbackEncodeTest, EncodesBGRAWithChannelMapping) { + // BGRA's first format component is Blue: P0 fields B=8 G=0 R=15 A=4 -> 8<<12 | 15<<4 | 4 = 0x80F4 + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_BGRA, GL_UNSIGNED_SHORT_4_4_4_4); + EXPECT_EQ(words[0], 0x80F4u); +} + +TEST(PackedReadbackEncodeTest, EncodesIntegerRGBA2101010RevWithFieldClamp) { + // Integer sources clamp to each field's unsigned range (10/10/10/2 bits). + const Vector wide{1023u, 1024u, 5u, 4u}; + const auto words = + ConvertWideRowToPackedWords(wide, GL_UNSIGNED_INT, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV); + EXPECT_EQ(words[0], 0xC05FFFFFu); // 1023 | 1023<<10 | 5<<20 | 3<<30 +} + +TEST(PackedReadbackEncodeTest, EncodesIntegerNegativeValuesClampToZero) { + const Vector wide{-5, 2, 100000, 1}; + const auto words = + ConvertWideRowToPackedWords(wide, GL_INT, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV); + EXPECT_EQ(words[0], 0x7FF00800u); // 0 | 2<<10 | 1023<<20 | 1<<30 +} + +TEST(PackedReadbackEncodeTest, EncodesIntegerRGB565) { + const Vector wide{31u, 64u, 2u, 0u}; + const auto words = + ConvertWideRowToPackedWords(wide, GL_UNSIGNED_INT, GL_RGB_INTEGER, GL_UNSIGNED_SHORT_5_6_5); + EXPECT_EQ(words[0], 0xFFE2u); // 31<<11 | 63<<5 | 2 (G clamps 64 -> 63) +} + +TEST(PackedReadbackEncodeTest, EncodesPackedFloat10F11F11FRev) { + // F11(1.0)=0x3C0 F11(0.5)=0x380 F10(0.25)=0x1A0 -> 0x3C0 | 0x380<<11 | 0x1A0<<22 = 0x681C03C0. + // Second pixel: values above 65024 clamp to the max finite F11 (0x7BF), negatives go to zero. + const Vector wide{1.0f, 0.5f, 0.25f, 1.0f, 100000.0f, -1.0f, 0.25f, 1.0f}; + const auto words = ConvertWideRowToPackedWords(wide, GL_FLOAT, GL_RGB, GL_UNSIGNED_INT_10F_11F_11F_REV); + EXPECT_EQ(words[0], 0x681C03C0u); + EXPECT_EQ(words[1], 0x680007BFu); +} + +TEST(PackedReadbackEncodeTest, EncodesSharedExponent5999Rev) { + // (1.0, 0.5, 0.25): shared exponent 16, fields 256/128/64 -> 256 | 128<<9 | 64<<18 | 16<<27 + const Vector wide{1.0f, 0.5f, 0.25f, 1.0f}; + const auto words = ConvertWideRowToPackedWords(wide, GL_FLOAT, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV); + EXPECT_EQ(words[0], 0x81010100u); +} + +TEST(PackedReadbackEncodeTest, RejectsMismatchedPackedFieldCounts) { + ReadbackImpl::ReadbackChannelMapping rgba{}; + ASSERT_TRUE(ReadbackImpl::GetReadbackChannelMapping(GL_RGBA, rgba)); + ReadbackImpl::ReadbackChannelMapping rgbInteger{}; + ASSERT_TRUE(ReadbackImpl::GetReadbackChannelMapping(GL_RGB_INTEGER, rgbInteger)); + + // 3-field packed types never pair with 4-component formats and vice versa. + EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgba, GL_UNSIGNED_SHORT_5_6_5), 0u); + EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_SHORT_4_4_4_4), 0u); + // Packed-float RGB types never pair with integer formats. + EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_5_9_9_9_REV), 0u); + EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_10F_11F_11F_REV), 0u); +}