diff --git a/CMakeLists.txt b/CMakeLists.txt index e41fe550..9ed45a00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -496,6 +496,7 @@ set(SOURCE_FILES MobileGL/MG_Backend/Init.cpp MobileGL/MG_Backend/BackendObject.cpp + MobileGL/MG_Backend/ClientReadback.cpp MobileGL/MG_Backend/GpuHangWatch.cpp MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp diff --git a/MobileGL/MG_Backend/ClientReadback.cpp b/MobileGL/MG_Backend/ClientReadback.cpp new file mode 100644 index 00000000..084184b5 --- /dev/null +++ b/MobileGL/MG_Backend/ClientReadback.cpp @@ -0,0 +1,432 @@ +// MobileGL - MobileGL/MG_Backend/ClientReadback.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 "ClientReadback.h" +#include +#include +#include + +#include +#include + +namespace MobileGL::MG_Backend { + // ---- 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 ClientReadback { + 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 { + 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 + + // Shared encoders live in MG_Util/Math/SmallFloat.h so the upload conversion + // (PixelStoreProcessor) uses byte-identical packing; kept exported here for unit tests. + Uint32 EncodeFloatToUnsignedF11(Float value) { return MG_Util::EncodeFloatToUnsignedF11(value); } + Uint32 EncodeFloatToUnsignedF10(Float value) { return MG_Util::EncodeFloatToUnsignedF10(value); } + Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) { return MG_Util::EncodeSharedExponentRGB9E5(rgb); } + + 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; + } + } + } + } + } + } + + static SizeT AlignReadbackRow(SizeT rowBytes, Int alignment) { + const SizeT align = alignment > 0 ? static_cast(alignment) : 1; + return (rowBytes + align - 1) / align * align; + } + + // Walks the client-side destination the PACK parameters describe and hands each row to + // `fillRow(slice, row, dstRow)`, which writes width * dstPixelBytes bytes of finished client + // texels. Shared by the converting and the raw-word stores so both address the destination + // identically. The destination is always client memory: a readback into a pack buffer takes + // the verb path and never reaches these stores. + // applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage + // of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4). + // Per the GL addressing rules, slice k row j lands at + // SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes + // + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride. + template + static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight, + GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) { + // 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_Pipe::gPipeInputs->GetPixelStoreParameters(false); + const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); + const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment); + const SizeT imageRows = + applyPackImageParams && packParams.ImageHeight > 0 + ? static_cast(packParams.ImageHeight) + : static_cast(sliceHeight); + const SizeT dstImageStride = imageRows * dstRowStride; + const SizeT skipImages = + applyPackImageParams ? static_cast(std::max(packParams.SkipImages, 0)) : SizeT{0}; + const SizeT dstSkipOffset = skipImages * dstImageStride + + static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + + static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; + const SizeT dstRowBytes = static_cast(width) * dstPixelBytes; + + Vector convertedRow(dstRowBytes); + + for (GLsizei slice = 0; slice < sliceCount; ++slice) { + for (GLsizei row = 0; row < sliceHeight; ++row) { + fillRow(slice, row, convertedRow.data()); + + if (packParams.SwapBytes && swapGroupSize > 1) { + for (SizeT offset = 0; offset + swapGroupSize <= dstRowBytes; offset += swapGroupSize) { + std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + swapGroupSize); + } + } + + const SizeT dstOffset = dstSkipOffset + static_cast(slice) * dstImageStride + + static_cast(row) * dstRowStride; + Memcpy(static_cast(pixels) + dstOffset, convertedRow.data(), dstRowBytes); + } + } + return true; + } + + // Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the + // client-side PACK parameters. `wide` holds + // `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked), + // 4 components x GetReadbackComponentSize(wideType) bytes each. + Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight, + GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type, + void* pixels, Bool applyPackImageParams) { + const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type); + if (dstPixelBytes == 0) { + return false; + } + PackedReadbackLayout packedLayout{}; + const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout); + const SizeT swapGroupSize = isPackedType ? packedLayout.byteSize : GetReadbackComponentSize(type); + const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType); + + return StoreClientRows(dstPixelBytes, swapGroupSize, width, sliceHeight, sliceCount, pixels, + applyPackImageParams, + [&](GLsizei slice, GLsizei row, Uint8* dstRow) { + const SizeT flatRow = static_cast(slice) * + static_cast(sliceHeight) + + static_cast(row); + const Uint8* srcRow = + wide + flatRow * static_cast(width) * srcPixelBytes; + ConvertWideReadbackRow(srcRow, dstRow, static_cast(width), wideType, + mapping, type); + }); + } + + Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount, + GLenum type, void* pixels, Bool applyPackImageParams) { + PackedReadbackLayout packedLayout{}; + if (!GetPackedReadbackLayout(type, packedLayout) || packedLayout.byteSize != 4) { + return false; + } + const SizeT srcRowBytes = static_cast(width) * 4; + + return StoreClientRows(4, packedLayout.byteSize, width, sliceHeight, sliceCount, pixels, + applyPackImageParams, + [&](GLsizei slice, GLsizei row, Uint8* dstRow) { + const SizeT flatRow = static_cast(slice) * + static_cast(sliceHeight) + + static_cast(row); + Memcpy(dstRow, srcWords + flatRow * srcRowBytes, srcRowBytes); + }); + } + } // namespace ClientReadback +} // namespace MobileGL::MG_Backend diff --git a/MobileGL/MG_Backend/ClientReadback.h b/MobileGL/MG_Backend/ClientReadback.h new file mode 100644 index 00000000..aba9f32a --- /dev/null +++ b/MobileGL/MG_Backend/ClientReadback.h @@ -0,0 +1,75 @@ +// MobileGL - MobileGL/MG_Backend/ClientReadback.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 + +namespace MobileGL::MG_Backend { + // 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 ClientReadback { + 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); + + // Stores wide RGBA(_INTEGER) rows into client memory, honoring the client-side PACK + // pixel-store parameters (row length, alignment, skips, swap-bytes, and - when + // applyPackImageParams - image height/skip images). Shared by the DirectGLES and DirectVulkan + // readback conversion paths. + Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight, + GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type, + void* pixels, Bool applyPackImageParams); + + // Stores packed 32-bit source words verbatim, with the same destination addressing and PACK + // parameters as StoreWideRowsToClient. For the sources whose storage word already IS the + // client word (MG_Util::IsRawPackedPixelTransfer): routing those through the wide float + // intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes + // the shared exponent, so glGetTexImage would answer with different bits than were stored. + // `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words. + // False when `type` is not a 4-byte packed type. + Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount, + GLenum type, void* pixels, Bool applyPackImageParams); + } // namespace ClientReadback +} // namespace MobileGL::MG_Backend diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 7716b031..c23f6f14 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -14,6 +14,7 @@ #include "Utils.h" #include "Managers.h" #include "MultiDraw.h" +#include #include #include #include @@ -11509,12 +11510,12 @@ namespace MobileGL::MG_Backend::DirectGLES { // 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. The pure repacking helpers live in - // ReadbackImpl (Utils.cpp) so unit tests can assert the exact packed words. + // ClientReadback (MG_Backend/ClientReadback.cpp) so unit tests can assert the exact packed words. - using ReadbackImpl::GetReadbackChannelMapping; - using ReadbackImpl::GetReadbackComponentSize; - using ReadbackImpl::GetReadbackDstPixelSize; - using ReadbackImpl::ReadbackChannelMapping; + using ClientReadback::GetReadbackChannelMapping; + using ClientReadback::GetReadbackComponentSize; + using ClientReadback::GetReadbackDstPixelSize; + using ClientReadback::ReadbackChannelMapping; static Bool CanDecodeWideSourceType(GLenum type) { switch (type) { @@ -11904,8 +11905,8 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - if (!ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, height, /*sliceCount=*/1, mapping, type, pixels, - honorPackImageParams)) { + if (!ClientReadback::StoreWideRowsToClient(wide.data(), wideType, width, height, /*sliceCount=*/1, mapping, + type, pixels, honorPackImageParams)) { return false; } diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index 02f4ef86..2b5c3a7d 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -2278,419 +2278,4 @@ 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 { - 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 - - // Shared encoders live in MG_Util/Math/SmallFloat.h so the upload conversion - // (PixelStoreProcessor) uses byte-identical packing; kept exported here for unit tests. - Uint32 EncodeFloatToUnsignedF11(Float value) { return MG_Util::EncodeFloatToUnsignedF11(value); } - Uint32 EncodeFloatToUnsignedF10(Float value) { return MG_Util::EncodeFloatToUnsignedF10(value); } - Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) { return MG_Util::EncodeSharedExponentRGB9E5(rgb); } - - 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; - } - } - } - } - } - } - - static SizeT AlignReadbackRow(SizeT rowBytes, Int alignment) { - const SizeT align = alignment > 0 ? static_cast(alignment) : 1; - return (rowBytes + align - 1) / align * align; - } - - // Walks the client-side destination the PACK parameters describe and hands each row to - // `fillRow(slice, row, dstRow)`, which writes width * dstPixelBytes bytes of finished client - // texels. Shared by the converting and the raw-word stores so both address the destination - // identically. The destination is always client memory: a readback into a pack buffer takes - // the verb path and never reaches these stores. - // applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage - // of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4). - // Per the GL addressing rules, slice k row j lands at - // SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes - // + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride. - template - static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight, - GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) { - // 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_Pipe::gPipeInputs->GetPixelStoreParameters(false); - const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); - const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment); - const SizeT imageRows = - applyPackImageParams && packParams.ImageHeight > 0 - ? static_cast(packParams.ImageHeight) - : static_cast(sliceHeight); - const SizeT dstImageStride = imageRows * dstRowStride; - const SizeT skipImages = - applyPackImageParams ? static_cast(std::max(packParams.SkipImages, 0)) : SizeT{0}; - const SizeT dstSkipOffset = skipImages * dstImageStride + - static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + - static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; - const SizeT dstRowBytes = static_cast(width) * dstPixelBytes; - - Vector convertedRow(dstRowBytes); - - for (GLsizei slice = 0; slice < sliceCount; ++slice) { - for (GLsizei row = 0; row < sliceHeight; ++row) { - fillRow(slice, row, convertedRow.data()); - - if (packParams.SwapBytes && swapGroupSize > 1) { - for (SizeT offset = 0; offset + swapGroupSize <= dstRowBytes; offset += swapGroupSize) { - std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + swapGroupSize); - } - } - - const SizeT dstOffset = dstSkipOffset + static_cast(slice) * dstImageStride + - static_cast(row) * dstRowStride; - Memcpy(static_cast(pixels) + dstOffset, convertedRow.data(), dstRowBytes); - } - } - return true; - } - - // Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the - // client-side PACK parameters. `wide` holds - // `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked), - // 4 components x GetReadbackComponentSize(wideType) bytes each. - Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight, - GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type, - void* pixels, Bool applyPackImageParams) { - const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type); - if (dstPixelBytes == 0) { - return false; - } - PackedReadbackLayout packedLayout{}; - const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout); - const SizeT swapGroupSize = isPackedType ? packedLayout.byteSize : GetReadbackComponentSize(type); - const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType); - - return StoreClientRows(dstPixelBytes, swapGroupSize, width, sliceHeight, sliceCount, pixels, - applyPackImageParams, - [&](GLsizei slice, GLsizei row, Uint8* dstRow) { - const SizeT flatRow = static_cast(slice) * - static_cast(sliceHeight) + - static_cast(row); - const Uint8* srcRow = - wide + flatRow * static_cast(width) * srcPixelBytes; - ConvertWideReadbackRow(srcRow, dstRow, static_cast(width), wideType, - mapping, type); - }); - } - - Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount, - GLenum type, void* pixels, Bool applyPackImageParams) { - PackedReadbackLayout packedLayout{}; - if (!GetPackedReadbackLayout(type, packedLayout) || packedLayout.byteSize != 4) { - return false; - } - const SizeT srcRowBytes = static_cast(width) * 4; - - return StoreClientRows(4, packedLayout.byteSize, width, sliceHeight, sliceCount, pixels, - applyPackImageParams, - [&](GLsizei slice, GLsizei row, Uint8* dstRow) { - const SizeT flatRow = static_cast(slice) * - static_cast(sliceHeight) + - static_cast(row); - Memcpy(dstRow, srcWords + flatRow * srcRowBytes, srcRowBytes); - }); - } - } // namespace ReadbackImpl } // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index d013f6b0..7bfc8b85 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -193,69 +193,6 @@ 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); - - // Stores wide RGBA(_INTEGER) rows into client memory, honoring the client-side PACK - // pixel-store parameters (row length, alignment, skips, swap-bytes, and - when - // applyPackImageParams - image height/skip images). Shared by the DirectGLES and DirectVulkan - // readback conversion paths. - Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight, - GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type, - void* pixels, Bool applyPackImageParams); - - // Stores packed 32-bit source words verbatim, with the same destination addressing and PACK - // parameters as StoreWideRowsToClient. For the sources whose storage word already IS the - // client word (MG_Util::IsRawPackedPixelTransfer): routing those through the wide float - // intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes - // the shared exponent, so glGetTexImage would answer with different bits than were stored. - // `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words. - // False when `type` is not a 4-byte packed type. - Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount, - GLenum type, void* pixels, Bool applyPackImageParams); - } // namespace ReadbackImpl - namespace PrgramImpl { String ProcessOutColorLocations(const String& glslCode); String ForceSupporterOutput(const String& glslCode); diff --git a/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc b/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc index f5b1d29c..10fba55d 100644 --- a/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc +++ b/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc @@ -145,7 +145,7 @@ Vector converted(pixels * pixelBytes); const SizeT sourceRowBytes = static_cast(width) * 4 * GetReadbackComponentSize(wideType); for (GLsizei row = 0; row < height; ++row) - ReadbackImpl::ConvertWideReadbackRow(wide.data() + static_cast(row) * sourceRowBytes, + ClientReadback::ConvertWideReadbackRow(wide.data() + static_cast(row) * sourceRowBytes, converted.data() + static_cast(row) * width * pixelBytes, width, wideType, mapping, type); bytes = Move(converted); return true; @@ -406,7 +406,7 @@ const SizeT wideRowBytes = rowTexels * 4 * GetReadbackComponentSize(wideType); out.resize(texels * pixelBytes); for (SizeT row = 0; row < rows; ++row) { - ReadbackImpl::ConvertWideReadbackRow(wide.data() + row * wideRowBytes, + ClientReadback::ConvertWideReadbackRow(wide.data() + row * wideRowBytes, out.data() + row * rowTexels * pixelBytes, rowTexels, wideType, mapping, type); } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 4d5a3432..554ca607 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -13,7 +13,7 @@ #include "OffscreenSurfaceRoute.h" #include "MG_Backend/DirectVulkan/SubgroupSupportPolicy.h" -#include "MG_Backend/DirectGLES/Utils.h" +#include "MG_Backend/ClientReadback.h" #include "VertexInputStateFactory.h" #include "VertexInputStateBuilder.h" @@ -1910,9 +1910,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } - DirectGLES::ReadbackImpl::ReadbackChannelMapping mapping{}; - if (!DirectGLES::ReadbackImpl::GetReadbackChannelMapping(format, mapping) || - DirectGLES::ReadbackImpl::GetReadbackDstPixelSize(mapping, type) == 0) { + ClientReadback::ReadbackChannelMapping mapping{}; + if (!ClientReadback::GetReadbackChannelMapping(format, mapping) || + ClientReadback::GetReadbackDstPixelSize(mapping, type) == 0) { MGLOG_E_ONCE("DirectVulkan readback skipped: unsupported format=0x%x type=0x%x", format, type); return false; } @@ -1925,8 +1925,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer( GetPackedReadbackInternalFormat(srcFormat), MG_Util::ConvertGLEnumToTextureInputFormat(format), MG_Util::ConvertGLEnumToTexturePixelDataType(type))) { - return DirectGLES::ReadbackImpl::StorePackedWordsToClient(srcPixels, width, sliceHeight, sliceCount, - type, pixels, applyPackImageParams); + return ClientReadback::StorePackedWordsToClient(srcPixels, width, sliceHeight, sliceCount, type, pixels, + applyPackImageParams); } Vector wide; @@ -1960,9 +1960,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { return false; } - return DirectGLES::ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight, - sliceCount, mapping, type, pixels, - applyPackImageParams); + return ClientReadback::StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight, sliceCount, mapping, + type, pixels, applyPackImageParams); } } // namespace diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireTextureReadback.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireTextureReadback.inc index 8efcb4f4..de150aa2 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireTextureReadback.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireTextureReadback.inc @@ -11,7 +11,7 @@ static Bool PackWireColorTight(const Uint8* source, SizeT sourceBytes, VkFormat format, TextureInternalFormat logicalFormat, const MG_Pipe::MGPReadbackInfo& info, Vector& result) { - using namespace DirectGLES::ReadbackImpl; + using namespace ClientReadback; ReadbackChannelMapping mapping{}; if (!GetReadbackChannelMapping(info.Format, mapping)) return false; const SizeT sourcePixelSize = VulkanRenderer::GetReadbackTexelSize(format); @@ -225,7 +225,7 @@ if (!PackDepthStencilTight(depth ? mapped : nullptr, stencil ? mapped + stencilOffset : nullptr, resource->format, pixels, info.Format, info.Type, result)) return false; } else { - using namespace DirectGLES::ReadbackImpl; + using namespace ClientReadback; ReadbackChannelMapping mapping{}; if (!GetReadbackChannelMapping(info.Format, mapping) || GetReadbackDstPixelSize(mapping, info.Type) != destinationPixelSize) return false; diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.h b/MobileGL/MG_Backend/MGPipe/PipeInputs.h index 8117dc32..794a0ccd 100644 --- a/MobileGL/MG_Backend/MGPipe/PipeInputs.h +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.h @@ -392,7 +392,7 @@ namespace MobileGL::MG_Pipe { // (MGPipeApplySetPixelPackState) - so the field is APPLIER-DERIVED and the UNPACK half is FATAL: // every gPipeInputs->GetPixelStoreParameters site in the tree passes false // (DirectGLES.cpp's PackStateFromContext, StoreReadbackRowsToClient and ReadPixels, Utils.cpp's - // ReadbackImpl::StoreClientRows, RecordVerbSink::ReadBoundFramebufferTight and + // ClientReadback::StoreClientRows, RecordVerbSink::ReadBoundFramebufferTight and // VulkanRenderer::ReadDepthStencilImageToClient), and PipeFill.cpp's EmitPixelPackState says the same from // the other side: "nothing on the far side of the boundary reads unpack state". PixelStoreParameters GetPixelStoreParameters(Bool isUnpack) const { diff --git a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp index 2d08b6f5..acc1e9c2 100644 --- a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp +++ b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -605,26 +606,25 @@ TEST_F(FramebufferTest, BlitNamedFramebufferAllowsDefaultFramebufferZero) { // ---- 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 +// rows through ClientReadback::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; + namespace ClientReadback = MG_Backend::ClientReadback; // 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)); + ClientReadback::ReadbackChannelMapping mapping{}; + EXPECT_TRUE(ClientReadback::GetReadbackChannelMapping(format, mapping)); + EXPECT_EQ(ClientReadback::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); + ClientReadback::ConvertWideReadbackRow(reinterpret_cast(wide.data()), + reinterpret_cast(out.data()), width, wideType, mapping, type); return out; } @@ -767,17 +767,17 @@ TEST(PackedReadbackEncodeTest, EncodesSharedExponent5999Rev) { } TEST(PackedReadbackEncodeTest, RejectsMismatchedPackedFieldCounts) { - ReadbackImpl::ReadbackChannelMapping rgba{}; - ASSERT_TRUE(ReadbackImpl::GetReadbackChannelMapping(GL_RGBA, rgba)); - ReadbackImpl::ReadbackChannelMapping rgbInteger{}; - ASSERT_TRUE(ReadbackImpl::GetReadbackChannelMapping(GL_RGB_INTEGER, rgbInteger)); + ClientReadback::ReadbackChannelMapping rgba{}; + ASSERT_TRUE(ClientReadback::GetReadbackChannelMapping(GL_RGBA, rgba)); + ClientReadback::ReadbackChannelMapping rgbInteger{}; + ASSERT_TRUE(ClientReadback::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); + EXPECT_EQ(ClientReadback::GetReadbackDstPixelSize(rgba, GL_UNSIGNED_SHORT_5_6_5), 0u); + EXPECT_EQ(ClientReadback::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); + EXPECT_EQ(ClientReadback::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_5_9_9_9_REV), 0u); + EXPECT_EQ(ClientReadback::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_10F_11F_11F_REV), 0u); } // ---- GL CTS packed_pixels readback root-cause regressions -------------------------------------- diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 1e92ea54..884396c3 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -4688,7 +4689,7 @@ TEST_F(TextureTest, TexImage2DRGB9E5FromOtherPackedFloatTypeStillConverts) { TEST_F(TextureTest, StorePackedWordsToClientCopiesWordsVerbatimUnderPackParams) { // Readback direction: the raw store copies the words bit-for-bit while still honoring the // client-side PACK addressing (alignment, skip rows/pixels) and GL_PACK_SWAP_BYTES. - namespace ReadbackImpl = MG_Backend::DirectGLES::ReadbackImpl; + namespace ClientReadback = MG_Backend::ClientReadback; const Uint32 source[] = {0xF8FC0000u, 0x60000000u, 0x08000001u, // row 0 0x0FFFFFFFu, 0xFFFFFFFFu, 0x00000000u}; // row 1 @@ -4703,10 +4704,10 @@ TEST_F(TextureTest, StorePackedWordsToClientCopiesWordsVerbatimUnderPackParams) // readback verb: a test that calls it directly says so, or the block is unfilled and the // first read is Fatal{UnmigratedPipeInput} in a push build. MG_Test::ScopedPipeVerb readback(MG_Pipe::MGPipeVerb::ReadPixels); - ASSERT_TRUE(ReadbackImpl::StorePackedWordsToClient(reinterpret_cast(source), /*width=*/3, - /*sliceHeight=*/2, /*sliceCount=*/1, - GL_UNSIGNED_INT_5_9_9_9_REV, destination, - /*applyPackImageParams=*/false)); + ASSERT_TRUE(ClientReadback::StorePackedWordsToClient(reinterpret_cast(source), /*width=*/3, + /*sliceHeight=*/2, /*sliceCount=*/1, + GL_UNSIGNED_INT_5_9_9_9_REV, destination, + /*applyPackImageParams=*/false)); // Row 0 lands at SKIP_ROWS * 16 + SKIP_PIXELS * 4 = 20 bytes = word 5; row 1 one 16-byte // stride further along, at word 9. for (Int i = 0; i < 3; ++i) { @@ -4727,10 +4728,10 @@ TEST_F(TextureTest, StorePackedWordsToClientCopiesWordsVerbatimUnderPackParams) MG_Impl::GLImpl::PixelStorei(GL_PACK_SWAP_BYTES, GL_TRUE); // A second readback, after the PACK parameters moved. readback.Renew(); - ASSERT_TRUE(ReadbackImpl::StorePackedWordsToClient(reinterpret_cast(source), /*width=*/3, - /*sliceHeight=*/1, /*sliceCount=*/1, - GL_UNSIGNED_INT_5_9_9_9_REV, destination, - /*applyPackImageParams=*/false)); + ASSERT_TRUE(ClientReadback::StorePackedWordsToClient(reinterpret_cast(source), /*width=*/3, + /*sliceHeight=*/1, /*sliceCount=*/1, + GL_UNSIGNED_INT_5_9_9_9_REV, destination, + /*applyPackImageParams=*/false)); EXPECT_EQ(destination[0], 0x0000FCF8u); // byte-reversed 0xF8FC0000 EXPECT_EQ(destination[1], 0x00000060u);