Files
MobileGL/MobileGL/MG_Backend/DirectGLES/Utils.cpp
T

832 lines
41 KiB
C++

// MobileGL - MobileGL/MG_Backend/DirectGLES/Utils.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 "DirectGLES.h"
#include "Utils.h"
#include "Managers.h"
#include "MG_Backend/BackendObjects.h"
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
#include "MG_Util/Texture/TextureFormatProcessor.h"
#include <MG_State/GLState/Core.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
#include <MG_Util/Math/HalfFloat.h>
#include <cmath>
namespace MobileGL::MG_Backend::DirectGLES {
namespace {
Flags<PixelFormatNormalizeOptionBit> GetForcedPixelFormatNormalizeOptions() {
Flags<PixelFormatNormalizeOptionBit> options;
if (g_GLESCapabilities.IsAngleRenderer) {
options |= PixelFormatNormalizeOptionBit::NoRgb16;
options |= PixelFormatNormalizeOptionBit::NoSnorm16;
options |= PixelFormatNormalizeOptionBit::NoSnorm8;
}
return options;
}
Flags<PixelFormatNormalizeOptionBit> GetDriverPixelFormatNormalizeOptions() {
Flags<PixelFormatNormalizeOptionBit> options = PixelFormatNormalizeOptionBit::NoDepthComponent32;
options |= PixelFormatNormalizeOptionBit::NoRGBA8Snorm;
options |= PixelFormatNormalizeOptionBit::NoRGB16Snorm;
if (!g_GLESCapabilities.SupportsNorm16Texture) {
options |= PixelFormatNormalizeOptionBit::NoNorm16;
}
return options;
}
Flags<PixelFormatNormalizeOptionBit> GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat) {
using namespace MG_Util::TextureFormatProcessor;
const Flags<PixelFormatNormalizeOptionBit> forcedOptions =
GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat, GetForcedPixelFormatNormalizeOptions());
if (forcedOptions) {
return forcedOptions;
}
return GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat,
GetDriverPixelFormatNormalizeOptions());
}
Bool HasCachedFormatCapability(TextureInternalFormat internalFormat,
SizeT targetIndex,
Bool caveat,
FormatCapability capability) {
if (!pActiveBackendObject || targetIndex >= kFormatCapabilityTargetCount) {
return false;
}
const SizeT formatIndex = static_cast<SizeT>(internalFormat);
if (formatIndex >= kFormatCapabilityFormatCount) {
return false;
}
const FormatCapabilityCache& cache = pActiveBackendObject->GetFormatCapabilities();
const FormatCapabilityFlags caps =
caveat ? cache.CaveatCaps[targetIndex][formatIndex] : cache.FullCaps[targetIndex][formatIndex];
return HasFormatCapability(caps, capability);
}
Bool HasAnyCachedFormatCapability(TextureInternalFormat internalFormat,
Bool caveat,
FormatCapability capability) {
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTargetCount; ++targetIndex) {
if (HasCachedFormatCapability(internalFormat, targetIndex, caveat, capability)) {
return true;
}
}
return false;
}
Bool ShouldUseCaveatFormat(TextureInternalFormat internalFormat, SizeT targetIndex) {
if (targetIndex < kFormatCapabilityTargetCount) {
const Bool fullCreatable =
HasCachedFormatCapability(internalFormat, targetIndex, false, FormatCapability::Creatable);
const Bool caveatCreatable =
HasCachedFormatCapability(internalFormat, targetIndex, true, FormatCapability::Creatable);
const Bool fullRenderable =
HasCachedFormatCapability(internalFormat, targetIndex, false, FormatCapability::FramebufferRenderable);
const Bool caveatRenderable =
HasCachedFormatCapability(internalFormat, targetIndex, true, FormatCapability::FramebufferRenderable);
return (!fullCreatable && caveatCreatable) || (!fullRenderable && caveatRenderable);
}
if (HasAnyCachedFormatCapability(internalFormat, false, FormatCapability::Creatable)) {
return false;
}
return HasAnyCachedFormatCapability(internalFormat, true, FormatCapability::Creatable);
}
void GenerateFormatInfo(TextureInternalFormat internalFormat,
SizeT targetIndex,
GLenum* outInternalFormat,
GLenum* outFormat,
GLenum* outType) {
using namespace MobileGL::MG_Util::TextureFormatProcessor;
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
Flags<PixelFormatNormalizeOptionBit> options;
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat);
}
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
}
} // namespace
namespace TextureImpl {
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType, TextureTarget target) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const SizeT targetIndex =
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
GenerateFormatInfo(internalFormat, targetIndex, outInternalFormat, outFormat, outType);
}
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
GenerateFormatInfo(internalFormat, GetRenderbufferFormatCapabilityTargetIndex(), outInternalFormat,
outFormat, outType);
}
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target) {
const SizeT targetIndex =
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
return ShouldUseCaveatFormat(internalFormat, targetIndex);
}
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat) {
return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
}
} // namespace TextureImpl
namespace PrgramImpl {
String ProcessOutColorLocations(const String& glslCode) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const static std::regex pattern(R"(\n(out highp vec4 outColor)(\d+);)");
const String replacement = "\nlayout(location=$2) $1$2;";
return std::regex_replace(glslCode, pattern, replacement);
}
String ForceSupporterOutput(const String& glslCode) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
Bool hasPrecisionFloat =
glslCode.find("precision ") != String::npos && glslCode.find("float;") != String::npos;
Bool hasPrecisionInt = glslCode.find("precision ") != String::npos && glslCode.find("int;") != String::npos;
String result = glslCode;
String precisionFloat;
String precisionInt;
if (hasPrecisionFloat && hasPrecisionInt) {
std::istringstream iss(result);
std::vector<String> lines;
String line;
while (std::getline(iss, line)) {
Bool isPrecisionLine = (line.find("precision ") != String::npos) &&
(line.find("float;") != String::npos || line.find("int;") != String::npos);
if (!isPrecisionLine) {
lines.push_back(line);
}
}
result.clear();
for (SizeT i = 0; i < lines.size(); ++i) {
if (i != 0) result += '\n';
result += lines[i];
}
precisionFloat = "precision highp float;\n";
precisionInt = "precision highp int;\n";
} else {
precisionFloat = hasPrecisionFloat ? "" : "precision highp float;\n";
precisionInt = hasPrecisionInt ? "" : "precision highp int;\n";
}
SizeT lastExtensionPos = result.rfind("#extension");
SizeT insertionPos = 0;
if (lastExtensionPos != String::npos) {
SizeT nextNewline = result.find('\n', lastExtensionPos);
if (nextNewline != String::npos) {
insertionPos = nextNewline + 1;
} else {
insertionPos = result.length();
}
} else {
SizeT firstNewline = result.find('\n');
if (firstNewline != String::npos) {
insertionPos = firstNewline + 1;
} else {
result = precisionFloat + precisionInt + result;
return result;
}
}
result.insert(insertionPos, precisionFloat + precisionInt);
return result;
}
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
Uint32 unormOutputMask) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const Uint32 outputMask = snormOutputMask | unormOutputMask;
if (shaderType != GL_FRAGMENT_SHADER || outputMask == 0) {
return glslCode;
}
const std::regex outputPattern(
R"(layout\s*\(\s*location\s*=\s*([0-9]+)\s*\)\s*out\s+(?:(?:lowp|mediump|highp)\s+)?vec4\s+([A-Za-z_][A-Za-z0-9_]*)\s*;)");
std::sregex_iterator outputIt(glslCode.begin(), glslCode.end(), outputPattern);
std::sregex_iterator outputEnd;
struct OutputClamp {
String Name;
Bool Signed;
};
Vector<OutputClamp> outputClamps;
for (; outputIt != outputEnd; ++outputIt) {
const Uint location = static_cast<Uint>(std::stoul((*outputIt)[1].str()));
if (location < 32 && (outputMask & (1u << location))) {
outputClamps.push_back({(*outputIt)[2].str(), static_cast<Bool>(snormOutputMask & (1u << location))});
}
}
if (outputClamps.empty()) {
return glslCode;
}
const std::regex mainPattern(R"(void\s+main\s*\([^)]*\)\s*\{)");
std::smatch mainMatch;
if (!std::regex_search(glslCode, mainMatch, mainPattern)) {
return glslCode;
}
SizeT bracePos = static_cast<SizeT>(mainMatch.position(0) + mainMatch.length(0) - 1);
Int depth = 0;
for (SizeT pos = bracePos; pos < glslCode.size(); ++pos) {
if (glslCode[pos] == '{') {
++depth;
} else if (glslCode[pos] == '}') {
--depth;
if (depth == 0) {
String clampLine;
for (const OutputClamp& outputClamp : outputClamps) {
const String minValue = outputClamp.Signed ? "-1.0" : "0.0";
clampLine += "\n " + outputClamp.Name + " = clamp(" + outputClamp.Name +
", vec4(" + minValue + "), vec4(1.0));";
}
clampLine += "\n";
glslCode.insert(pos, clampLine);
return glslCode;
}
}
}
return glslCode;
}
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
String result = glslCode;
const String integerType = R"((?:(?:lowp|mediump|highp)\s+)?(?:u?int|[iu]vec[234])\b)";
auto addFlatQualifier = [&result, &integerType](const String& qualifier) {
const std::regex pattern("(layout\\s*\\([^)]*\\)\\s*)(?!(?:flat|smooth|noperspective)\\s)(" +
qualifier + "\\s+" + integerType + ")");
result = std::regex_replace(result, pattern, "$1flat $2");
};
switch (shaderType) {
case GL_VERTEX_SHADER:
addFlatQualifier("out");
break;
case GL_GEOMETRY_SHADER:
addFlatQualifier("in");
addFlatQualifier("out");
break;
case GL_FRAGMENT_SHADER:
addFlatQualifier("in");
break;
default:
break;
}
return result;
}
String RemoveLayoutBinding(const String& glslCode) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Sampler and uniform-block bindings are re-established at draw time through the
// API, so their layout qualifiers are stripped (they may exceed ES limits). SSBO
// blocks and image uniforms are different: ES has no glShaderStorageBlockBinding,
// and image units cannot be set with glUniform1i, so for those declarations the
// binding qualifier is the only binding mechanism and must be preserved.
static std::regex bindingRegex(R"(layout\s*\(\s*binding\s*=\s*\d+\s*\)\s*)");
static std::regex bindingRegex2(R"(layout\s*\(\s*binding\s*=\s*\d+\s*,)");
static std::regex keepBindingRegex(R"(\b(buffer|[iu]?image[A-Za-z0-9]*)\b)");
String result;
result.reserve(glslCode.size());
SizeT lineStart = 0;
while (lineStart <= glslCode.size()) {
SizeT lineEnd = glslCode.find('\n', lineStart);
const Bool lastLine = lineEnd == String::npos;
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
if (!std::regex_search(line, keepBindingRegex)) {
line = std::regex_replace(line, bindingRegex, "");
line = std::regex_replace(line, bindingRegex2, "layout(");
}
result += line;
if (lastLine) {
break;
}
result += '\n';
lineStart = lineEnd + 1;
}
return result;
}
} // namespace PrgramImpl
namespace Utils {
void CheckGLESError() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
for (GLenum err = g_GLESFuncs.glGetError(); err != GL_NO_ERROR; err = g_GLESFuncs.glGetError()) {
MGLOG_E("-> GLES Error: %s", MG_Util::ConvertGLEnumToString(err).c_str());
}
}
GLenum GetBindingQuery(GLenum target, bool isTexture) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
switch (target) {
case GL_TEXTURE_BUFFER:
return isTexture ? GL_TEXTURE_BINDING_BUFFER : GL_TEXTURE_BUFFER_BINDING;
case GL_ARRAY_BUFFER:
return GL_ARRAY_BUFFER_BINDING;
case GL_ATOMIC_COUNTER_BUFFER:
return GL_ATOMIC_COUNTER_BUFFER_BINDING;
case GL_COPY_READ_BUFFER:
return GL_COPY_READ_BUFFER_BINDING;
case GL_COPY_WRITE_BUFFER:
return GL_COPY_WRITE_BUFFER_BINDING;
case GL_DISPATCH_INDIRECT_BUFFER:
return GL_DISPATCH_INDIRECT_BUFFER_BINDING;
case GL_DRAW_INDIRECT_BUFFER:
return GL_DRAW_INDIRECT_BUFFER_BINDING;
case GL_ELEMENT_ARRAY_BUFFER:
return GL_ELEMENT_ARRAY_BUFFER_BINDING;
case GL_PIXEL_PACK_BUFFER:
return GL_PIXEL_PACK_BUFFER_BINDING;
case GL_PIXEL_UNPACK_BUFFER:
return GL_PIXEL_UNPACK_BUFFER_BINDING;
case GL_QUERY_BUFFER:
return GL_QUERY_BUFFER_BINDING;
case GL_SHADER_STORAGE_BUFFER:
return GL_SHADER_STORAGE_BUFFER_BINDING;
case GL_TRANSFORM_FEEDBACK_BUFFER:
return GL_TRANSFORM_FEEDBACK_BUFFER_BINDING;
case GL_UNIFORM_BUFFER:
return GL_UNIFORM_BUFFER_BINDING;
case GL_FRAMEBUFFER:
case GL_DRAW_FRAMEBUFFER:
return GL_DRAW_FRAMEBUFFER_BINDING;
case GL_READ_FRAMEBUFFER:
return GL_READ_FRAMEBUFFER_BINDING;
case GL_RENDERBUFFER:
return GL_RENDERBUFFER_BINDING;
case GL_VERTEX_ARRAY:
case GL_VERTEX_ARRAY_BINDING:
return GL_VERTEX_ARRAY_BINDING;
case GL_PROGRAM_PIPELINE:
return GL_PROGRAM_PIPELINE_BINDING;
case GL_PROGRAM:
return GL_CURRENT_PROGRAM;
case GL_SAMPLER:
return GL_SAMPLER_BINDING;
case GL_TEXTURE:
return GL_TEXTURE_BINDING_2D;
case GL_TEXTURE_1D:
return GL_TEXTURE_BINDING_1D;
case GL_TEXTURE_1D_ARRAY:
return GL_TEXTURE_BINDING_1D_ARRAY;
case GL_TEXTURE_2D:
return GL_TEXTURE_BINDING_2D;
case GL_TEXTURE_2D_ARRAY:
return GL_TEXTURE_BINDING_2D_ARRAY;
case GL_TEXTURE_2D_MULTISAMPLE:
return GL_TEXTURE_BINDING_2D_MULTISAMPLE;
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
return GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY;
case GL_TEXTURE_3D:
return GL_TEXTURE_BINDING_3D;
case GL_TEXTURE_CUBE_MAP:
return GL_TEXTURE_BINDING_CUBE_MAP;
case GL_TEXTURE_CUBE_MAP_ARRAY:
return GL_TEXTURE_BINDING_CUBE_MAP_ARRAY;
case GL_TEXTURE_RECTANGLE:
return GL_TEXTURE_BINDING_RECTANGLE;
case GL_TRANSFORM_FEEDBACK:
return GL_TRANSFORM_FEEDBACK_BINDING;
case GL_SAMPLES_PASSED:
return GL_SAMPLES_PASSED;
case GL_PRIMITIVES_GENERATED:
return GL_PRIMITIVES_GENERATED;
case GL_DEBUG_OUTPUT:
return GL_DEBUG_OUTPUT;
case GL_DEBUG_OUTPUT_SYNCHRONOUS:
return GL_DEBUG_OUTPUT_SYNCHRONOUS;
default:
return 0;
}
}
} // 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<SizeT>(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<Uint32>(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<Int32>(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<Uint32>(smallExponent) << mantissaBits) |
(mantissa >> (23u - static_cast<Uint32>(mantissaBits)));
}
void WritePackedReadbackWord(Uint8* dst, Uint32 word, SizeT byteSize) {
switch (byteSize) {
case 1: {
const auto out = static_cast<Uint8>(word);
Memcpy(dst, &out, sizeof(out));
break;
}
case 2: {
const auto out = static_cast<Uint16>(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<Int>(std::floor(std::log2(maxComponent)))) +
1 + kExponentBias;
const Float maxScaled = std::floor(
maxComponent / std::exp2(static_cast<Float>(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<Float>(sharedExponent - kExponentBias - kMantissaBits));
Uint32 word = static_cast<Uint32>(sharedExponent) << 27;
for (Int i = 0; i < 3; ++i) {
const auto field = static_cast<Uint32>(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<Int64>(reinterpret_cast<const Int32*>(srcPixel)[c])
: static_cast<Int64>(reinterpret_cast<const Uint32*>(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<Uint32>(
std::clamp<Int64>(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<SizeT>(ch) * dstComponentSize;
switch (type) {
case GL_UNSIGNED_BYTE:
*dstComponent = static_cast<Uint8>(std::clamp<Int64>(v, 0, 255));
break;
case GL_BYTE: {
const auto out = static_cast<Int8>(std::clamp<Int64>(v, -128, 127));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_UNSIGNED_SHORT: {
const auto out = static_cast<Uint16>(std::clamp<Int64>(v, 0, 65535));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_SHORT: {
const auto out = static_cast<Int16>(std::clamp<Int64>(v, -32768, 32767));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_UNSIGNED_INT: {
const auto out = static_cast<Uint32>(std::clamp<Int64>(v, 0, 4294967295LL));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_INT: {
const auto out =
static_cast<Int32>(std::clamp<Int64>(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<Float>(srcPixel[c]) / 255.0f;
}
break;
case GL_BYTE:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = std::max(
static_cast<Float>(reinterpret_cast<const Int8*>(srcPixel)[c]) / 127.0f, -1.0f);
}
break;
case GL_UNSIGNED_SHORT:
for (Int c = 0; c < 4; ++c) {
srcValues[c] =
static_cast<Float>(reinterpret_cast<const Uint16*>(srcPixel)[c]) / 65535.0f;
}
break;
case GL_SHORT:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = std::max(
static_cast<Float>(reinterpret_cast<const Int16*>(srcPixel)[c]) / 32767.0f, -1.0f);
}
break;
case GL_HALF_FLOAT:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = DecodeHalfBitsToFloat(reinterpret_cast<const Uint16*>(srcPixel)[c]);
}
break;
default: // GL_FLOAT
for (Int c = 0; c < 4; ++c) {
srcValues[c] = reinterpret_cast<const Float*>(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<Float>((1u << packedLayout.width[ch]) - 1u);
const auto v = static_cast<Uint32>(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<SizeT>(ch) * dstComponentSize;
switch (type) {
case GL_UNSIGNED_BYTE:
*dstComponent =
static_cast<Uint8>(std::llround(std::clamp(v, 0.0f, 1.0f) * 255.0));
break;
case GL_BYTE: {
const auto out =
static_cast<Int8>(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<Uint16>(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<Int16>(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<Uint32>(
std::llround(static_cast<Double>(std::clamp(v, 0.0f, 1.0f)) * 4294967295.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_INT: {
const auto out = static_cast<Int32>(
std::llround(static_cast<Double>(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