diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 900b9502..a51d7e58 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -253,6 +253,22 @@ namespace MobileGL::MG_Config { // LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is // the negative control the emulation is measured against. QuirkOverride ViewportArrayEmulation = QuirkOverride::Auto; + // MOBILEGL_WIDEN_PACKED16_STORAGE: DirectGLES stores GL_RGB565/GL_RGB5(A1)/GL_RGBA4 + // images as 8-bit-per-channel ES storage (GL_RGB8/GL_RGBA8) instead of the driver's + // native 16-bit packed formats. Auto defers to a POST driver-bug probe + // (SelfTest::CopyImageMirrorsPacked16FieldOrder): some Mali drivers keep a MIRRORED + // field order for the 16-bit packed texels of a non-zero mip level of a + // GL_TEXTURE_2D_ARRAY, so glCopyImageSubData - a raw texel-block move - lands + // R/G/B/A reversed whenever exactly one endpoint is such a level + // (KHR-GL4x.copy_image.functional rgb5/rgb5_a1/rgba4 x every *2d_array* pair). + // With no 16-bit packed ES image left there is no field order to disagree about; the + // client word still round-trips exactly, because the canonical shadow is already + // UNorm8 and an n-bit field encodes to UNorm8 and back losslessly for n <= 8. + // ForceOn widens on any driver (the llvmpipe suites use it to exercise the widened + // path); ForceOff keeps the native narrow storage even where the probe fires - the + // negative control that replays the corruption. Costs 2x the memory of the affected + // formats where it engages, which is why Auto is probe-gated rather than always-on. + QuirkOverride EsprytWidenPacked16Storage = QuirkOverride::Auto; }; extern FeaturesTable Features; } // namespace MobileGL::MG_Config diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 6d2745c9..e2d0f579 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -201,6 +201,8 @@ namespace MobileGL::MG_ConfigLoader { features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE"); features.ViewportArrayEmulation = QueryEnvQuirkOverride("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION"); + features.EsprytWidenPacked16Storage = + QueryEnvQuirkOverride("MOBILEGL_WIDEN_PACKED16_STORAGE"); } inline void InitBackendType() { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index f03609e2..6324359b 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -3375,6 +3375,13 @@ namespace MobileGL::MG_Backend::DirectGLES { if (format != TextureInternalFormat::RGB5 && format != TextureInternalFormat::RGB5A1) { return data; } + // With the storage widened to 8-bit-per-channel (the packed16 field-order quirk) + // there is no driver requantization left for the repack to pre-empt - the shadow's + // UNorm8 bytes ARE the stored bytes - and the packed 16-bit client type this leg + // retargets to is not a legal upload for a GL_RGB8/GL_RGBA8 store at all. + if (TextureImpl::UsesWidenedPacked16NormStorage(format)) { + return data; + } const Bool hasAlpha = format == TextureInternalFormat::RGB5A1; const GLenum packedType = hasAlpha ? GL_UNSIGNED_SHORT_5_5_5_1 : GL_UNSIGNED_SHORT_5_6_5; // Idempotent across a region's level loop: glType is shared, so later levels arrive with diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index f23bf969..4239930f 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -11,8 +11,10 @@ #include "Managers.h" #include "MG_Backend/BackendObjects.h" #include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h" +#include "MG_Util/SelfTest/DriverBugProbes.h" #include "MG_Util/Texture/TextureFormatProcessor.h" #include "MG_Util/ShaderTranspiler/ShaderCompiler.h" +#include #include #include @@ -125,6 +127,14 @@ namespace MobileGL::MG_Backend::DirectGLES { requestedInternalFormat, TextureImpl::GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex)); } + // Outside the caveat branch on purpose: the driver CAN create the native narrow + // storage - the capability probes say so - it just cannot be trusted as a raw-copy + // endpoint. Texture and renderbuffer targets both come through here, which is what + // keeps a renderbuffer -> texture copy of these formats same-ES-format when the + // widening engages. + if (TextureImpl::UsesWidenedPacked16NormStorage(internalFormat)) { + options |= PixelFormatNormalizeOptionBit::WidenPacked16Norm; + } NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType); } } // namespace @@ -182,6 +192,36 @@ namespace MobileGL::MG_Backend::DirectGLES { return options; } + Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat) { + switch (internalFormat) { + // TextureInternalFormat::RGB5 is both GL_RGB5 and GL_RGB565 - the GL-to-MG + // converter folds the two spellings onto one logical format. + case TextureInternalFormat::RGB5: + case TextureInternalFormat::RGB5A1: + case TextureInternalFormat::RGBA4: + break; + default: + return false; + } + switch (MG_Config::Features.EsprytWidenPacked16Storage) { + case MG_Config::QuirkOverride::ForceOn: + return true; + case MG_Config::QuirkOverride::ForceOff: + return false; + case MG_Config::QuirkOverride::Auto: + break; + } + // Behind the backend gate on purpose: the memoized probe latches its first answer + // for the whole process, and before the backend is up the GL function table may + // not be resolved yet - a probe run then would latch "cannot tell" as "clean" + // forever. Once the backend exists, the first narrow-format image this process + // creates runs the probe on a live context. + if (pActiveBackendObject == nullptr) { + return false; + } + return MG_Util::SelfTest::CopyImageMirrorsPacked16FieldOrder(g_GLESFuncs); + } + void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat, GLenum* outFormat, GLenum* outType, TextureTarget target) { #ifdef TRACY_ENABLE diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index 3ad92a45..c795198c 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -46,6 +46,15 @@ namespace MobileGL::MG_Backend::DirectGLES { Flags GetRenderTargetNormalizeOptions( const MG_External::GLESCapabilities& capabilities, SizeT targetIndex); + // Whether this format's ES storage is widened to 8-bit-per-channel because the + // driver's 16-bit packed storage mirrors its field order at a non-zero array mip + // level (PixelFormatNormalizeOptionBit::WidenPacked16Norm). True only for + // GL_RGB565/GL_RGB5(_A1)/GL_RGBA4, and only where the POST probe measured the + // divergence (or MOBILEGL_WIDEN_PACKED16_STORAGE forces it). The transfer paths + // consult it too: the packed-norm re-upload leg must stand down when the ES storage + // is no longer 16-bit packed. + Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat); + void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat, GLenum* outFormat, GLenum* outType, TextureTarget target = TextureTarget::Unknown); diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 99996c6a..06e3c104 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -106,6 +106,7 @@ add_executable(MobileGLIntegrationTest Scenarios/VertexArrayEnableDisableScenario.cpp Scenarios/CopyImageLevelRangeScenario.cpp Scenarios/CopyImageLayeredScenario.cpp + Scenarios/CopyImagePacked16Scenario.cpp Scenarios/TextureViewScenario.cpp Scenarios/PackedWordReadbackScenario.cpp Scenarios/LayeredAttachmentBarrierScenario.cpp @@ -362,6 +363,8 @@ mgl_itest_join_environment(MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1" "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/unlocated-io-blocks.log" ${MGL_ITEST_COMMON_ENV}) +mgl_itest_join_environment(MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_WIDEN_PACKED16_STORAGE=1" ${MGL_ITEST_COMMON_ENV}) # TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it. set(MGL_ITEST_TIMEOUT 120) @@ -541,3 +544,21 @@ gtest_discover_tests(MobileGLIntegrationTest TIMEOUT ${MGL_ITEST_TIMEOUT} ENVIRONMENT "${MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT}" ) + +# The packed16 copy scenarios again, with the 8-bit storage widening PINNED ON. The ambient +# registrations above cover the narrow storage - on every CI driver the widening's POST +# probe finds no field-order mirror, so Auto keeps the native 16-bit path - which means the +# storage every AFFECTED device will actually run would otherwise execute nowhere at all: +# no CI driver has the Mali bug that arms it. This lane is what proves the widened storage +# is client-invisible (same packed words in and out on every leg the 18 failing CTS bodies +# used, the renderbuffer one included). DirectGLES only - the flag steers nothing on +# DirectVulkan, which has always stored these formats widened. +gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.WidenedPacked16." + TEST_FILTER "CopyImagePacked16Scenario.*" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS integration-gpu + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT}" +) diff --git a/MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp new file mode 100644 index 00000000..812ec8cb --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp @@ -0,0 +1,355 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp +// Copyright (c) 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 +// +// Scenario - glCopyImageSubData PRESERVES 16-BIT PACKED WORDS ACROSS AN ARRAY MIP LEVEL. +// +// The shape is lifted verbatim from the 18 Espryt bodies of KHR-GL4x.copy_image.functional +// that survived every earlier wave: the three internal formats MobileGL can keep as 16-bit +// packed ES storage - GL_RGB5 (stored GL_RGB565), GL_RGB5_A1, GL_RGBA4 - crossed with the +// target pairs that put a GL_TEXTURE_2D_ARRAY's MIP LEVEL 1 on one side of the copy. On the +// affected Mali the driver's physical field order for such a level is the *_REV mirror of +// every other image's, and glCopyImageSubData - a raw texel-block move - lands the fields +// reversed: src word 0x0047 arrives as 0x8C20 (its 5_5_5_1 -> 1_5_5_5_REV re-encoding), +// 0x0007 as 0x3800, byte-exact on every failing body. Uploads and readbacks of the same +// level are clean (the driver decodes its own layout consistently), which is why only the +// copy path ever crossed the two layouts and why the CTS's "source image was not modified" +// checks always passed. +// +// The array is 30x30x12 with two levels because that is the shape the failures pin: the same +// suite's level-0 copies and a 14x14 base's level 1 measured clean on the same driver, so a +// smaller array might sit on the clean side of whatever allocation threshold picks the +// driver's layout. +// +// The repair under test is the packed16 storage widening +// (PixelFormatNormalizeOptionBit::WidenPacked16Norm): where the POST probe +// (SelfTest::CopyImageMirrorsPacked16FieldOrder) measures the mirror - or +// MOBILEGL_WIDEN_PACKED16_STORAGE forces it - the three formats are stored as +// GL_RGB8/GL_RGBA8, leaving no 16-bit packed image for a copy to disagree about. The client +// word still round-trips exactly: the canonical shadow is already UNorm8, and an n-bit field +// encodes to UNorm8 and back losslessly for every n <= 8. +// +// This scenario runs in BOTH configurations, and both must hand back identical client words: +// * the ambient registrations take the narrow path on a clean driver (llvmpipe has no +// mirror, so Auto keeps the native 16-bit storage - the pre-existing behaviour stays +// covered); +// * the DirectGLES.WidenedPacked16. registration pins MOBILEGL_WIDEN_PACKED16_STORAGE=1, +// which is the storage every affected device will actually run - without it the repair +// is unfalsifiable off-device, because no CI driver has the bug that arms it. +// The Mali mirror itself CANNOT be reproduced here; only the on-device CTS run can show the +// widening killing the 18 bodies. What this scenario pins is that the widened storage is +// client-invisible: same words in, same words out, on every leg the failing bodies used. +// +// DirectVulkan is the control - Magma has always resolved these formats to RGBA8 - so a +// failure on both backends means the scenario is wrong, and a failure on DirectGLES alone +// means the widening (or the narrow path it replaces) is. + +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + constexpr int kBaseSize = 30; // array level 0; level 1 is 15x15 + constexpr int kLevel1Size = kBaseSize / 2; + constexpr int kLayers = 12; + constexpr int kFlatSize = 7; // the plain-2D / renderbuffer endpoint, level 0 + // Copies cover the whole flat endpoint and land at (8, 8) inside the 15x15 level so + // that offsets are honoured, not just texel (0, 0): 8 + 7 == 15 reaches the far edge. + constexpr int kRegion = kFlatSize; + constexpr int kArrayOffset = 8; + + struct PackedFormatCase { + GLenum internalFormat; // the spelling the CTS uses + GLenum transferFormat; + GLenum transferType; + const char* name; + }; + + // Per-texel varying words, every field inside its width, so a swapped field order (or + // a mis-addressed row) cannot cancel out the way a uniform fill would let it. + GLushort MakeWord(GLenum type, int i) { + switch (type) { + case GL_UNSIGNED_SHORT_5_6_5: { + const int r = i % 32, g = (i * 7 + 3) % 64, b = (i * 5 + 11) % 32; + return static_cast((r << 11) | (g << 5) | b); + } + case GL_UNSIGNED_SHORT_4_4_4_4: { + const int r = i % 16, g = (i * 3 + 1) % 16, b = (i * 7 + 5) % 16, a = (i * 5 + 2) % 16; + return static_cast((r << 12) | (g << 8) | (b << 4) | a); + } + case GL_UNSIGNED_SHORT_5_5_5_1: { + const int r = i % 32, g = (i * 7 + 3) % 32, b = (i * 3 + 11) % 32, a = i % 2; + return static_cast((r << 11) | (g << 6) | (b << 1) | a); + } + default: + return 0; + } + } + + std::vector MakeWords(GLenum type, int count, int seed) { + std::vector words(static_cast(count)); + for (int i = 0; i < count; ++i) { + words[static_cast(i)] = MakeWord(type, i + seed); + } + return words; + } + + class CopyImagePacked16Scenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + // 16-bit rows are 2-byte aligned; the default 4-byte row alignment would pad + // every odd-width row of the 15x15 level and shear the comparisons. + glPixelStorei(GL_UNPACK_ALIGNMENT, 2); + glPixelStorei(GL_PACK_ALIGNMENT, 2); + if (!CopyImageSubDataUsable()) { + GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName(); + } + } + + void TearDown() override { + if (!Ready()) return; + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + for (const GLuint texture : m_textures) { + glDeleteTextures(1, &texture); + } + m_textures.clear(); + if (m_renderbuffer != 0) { + glDeleteRenderbuffers(1, &m_renderbuffer); + m_renderbuffer = 0; + } + if (m_fbo != 0) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &m_fbo); + m_fbo = 0; + } + } + + bool CopyImageSubDataUsable() { + GLuint probe[2] = {0, 0}; + glGenTextures(2, probe); + for (const GLuint texture : probe) { + glBindTexture(GL_TEXTURE_2D_ARRAY, texture); + glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1); + } + glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + while (glGetError() != GL_NO_ERROR) { + } + glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0, + 0, 1, 1, 1); + const bool usable = glGetError() == GL_NO_ERROR; + glDeleteTextures(2, probe); + return usable; + } + + // The CTS's own mutable shape: glTexImage3D per level, filter NEAREST, the chain + // clamped to the two levels it has. + GLuint MakeArrayTexture(const PackedFormatCase& format, const std::vector& level0, + const std::vector& level1) { + GLuint texture = 0; + glGenTextures(1, &texture); + m_textures.push_back(texture); + glBindTexture(GL_TEXTURE_2D_ARRAY, texture); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 1); + glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, static_cast(format.internalFormat), kBaseSize, kBaseSize, + kLayers, 0, format.transferFormat, format.transferType, level0.data()); + glTexImage3D(GL_TEXTURE_2D_ARRAY, 1, static_cast(format.internalFormat), kLevel1Size, + kLevel1Size, kLayers, 0, format.transferFormat, format.transferType, level1.data()); + glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + return texture; + } + + GLuint MakeFlatTexture(const PackedFormatCase& format, const std::vector& texels) { + GLuint texture = 0; + glGenTextures(1, &texture); + m_textures.push_back(texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + glTexImage2D(GL_TEXTURE_2D, 0, static_cast(format.internalFormat), kFlatSize, kFlatSize, 0, + format.transferFormat, format.transferType, texels.data()); + glBindTexture(GL_TEXTURE_2D, 0); + return texture; + } + + std::vector ReadTexImage(GLenum target, GLuint texture, int level, + const PackedFormatCase& format, size_t texelCount) { + std::vector words(texelCount, 0); + glBindTexture(target, texture); + glGetTexImage(target, level, format.transferFormat, format.transferType, words.data()); + glBindTexture(target, 0); + return words; + } + + // Every word of `got` inside the kRegion-square at (x0, y0) of a width-wide layer-0 + // image equals the corresponding source word, and every word outside it still holds + // `fill`'s. Failures name the texel and both words, which is what turns a field-order + // regression into a one-line diagnosis. + void ExpectRegion(const std::vector& got, int width, int x0, int y0, + const std::vector& source, int sourceWidth, int sourceX0, int sourceY0, + const std::vector& fill, const char* what) { + for (int y = 0; y < width; ++y) { + for (int x = 0; x < width && static_cast(y * width + x) < got.size(); ++x) { + const bool inRegion = + x >= x0 && x < x0 + kRegion && y >= y0 && y < y0 + kRegion; + const GLushort actual = got[static_cast(y * width + x)]; + const GLushort expected = + inRegion ? source[static_cast((sourceY0 + y - y0) * sourceWidth + sourceX0 + + (x - x0))] + : fill[static_cast(y * width + x)]; + EXPECT_EQ(actual, expected) + << what << ": texel (" << x << ", " << y << ")" + << (inRegion ? " (copied)" : " (untouched)") << " holds 0x" << std::hex << actual + << ", expected 0x" << expected; + if (actual != expected) return; // one texel names the defect; 224 more would bury it + } + } + } + + std::vector m_textures; + GLuint m_renderbuffer = 0; + GLuint m_fbo = 0; + }; + + const PackedFormatCase kFormats[] = { + {GL_RGB5, GL_RGB, GL_UNSIGNED_SHORT_5_6_5, "rgb5"}, + {GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, "rgb5_a1"}, + {GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4, "rgba4"}, + }; + + // texture_2d (the ES image behind GL_TEXTURE_RECTANGLE too) -> the array's level 1: + // the array-as-destination direction of 12 of the 18 failing bodies. + TEST_F(CopyImagePacked16Scenario, FlatImageLandsInArrayMipLevelIntact) { + if (!Ready() || IsSkipped()) return; + for (const PackedFormatCase& format : kFormats) { + const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1); + const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7); + const auto flat = MakeWords(format.transferType, kFlatSize * kFlatSize, 131); + const GLuint array = MakeArrayTexture(format, level0, level1); + const GLuint source = MakeFlatTexture(format, flat); + ASSERT_EQ(glGetError(), static_cast(GL_NO_ERROR)) << format.name << ": setup failed"; + + glCopyImageSubData(source, GL_TEXTURE_2D, 0, 0, 0, 0, array, GL_TEXTURE_2D_ARRAY, 1, kArrayOffset, + kArrayOffset, 0, kRegion, kRegion, 1); + ASSERT_EQ(glGetError(), static_cast(GL_NO_ERROR)) + << format.name << ": glCopyImageSubData raised an error"; + + const auto got = ReadTexImage(GL_TEXTURE_2D_ARRAY, array, 1, format, + static_cast(kLevel1Size) * kLevel1Size * kLayers); + ExpectRegion(got, kLevel1Size, kArrayOffset, kArrayOffset, flat, kFlatSize, 0, 0, level1, + (std::string("2d->2d_array level 1, ") + format.name).c_str()); + // The source must not have moved - the CTS asserts this before it ever looks at + // the destination, and it is what pins the corruption to the copy itself. + const auto sourceAfter = + ReadTexImage(GL_TEXTURE_2D, source, 0, format, static_cast(kFlatSize) * kFlatSize); + ExpectRegion(sourceAfter, kFlatSize, 0, 0, flat, kFlatSize, 0, 0, flat, + (std::string("source after 2d->2d_array, ") + format.name).c_str()); + } + } + + // The array's level 1 -> texture_2d: the array-as-source direction of the other 6 + // bodies (2d_array -> 3d and 2d_array -> rectangle both read the level-1 array). + TEST_F(CopyImagePacked16Scenario, ArrayMipLevelLandsInFlatImageIntact) { + if (!Ready() || IsSkipped()) return; + for (const PackedFormatCase& format : kFormats) { + const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1); + const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7); + const auto fill = MakeWords(format.transferType, kFlatSize * kFlatSize, 131); + const GLuint array = MakeArrayTexture(format, level0, level1); + const GLuint destination = MakeFlatTexture(format, fill); + ASSERT_EQ(glGetError(), static_cast(GL_NO_ERROR)) << format.name << ": setup failed"; + + glCopyImageSubData(array, GL_TEXTURE_2D_ARRAY, 1, kArrayOffset, kArrayOffset, 0, destination, + GL_TEXTURE_2D, 0, 0, 0, 0, kRegion, kRegion, 1); + ASSERT_EQ(glGetError(), static_cast(GL_NO_ERROR)) + << format.name << ": glCopyImageSubData raised an error"; + + const auto got = ReadTexImage(GL_TEXTURE_2D, destination, 0, format, + static_cast(kFlatSize) * kFlatSize); + ExpectRegion(got, kFlatSize, 0, 0, level1, kLevel1Size, kArrayOffset, kArrayOffset, fill, + (std::string("2d_array level 1 -> 2d, ") + format.name).c_str()); + } + } + + // renderbuffer -> the array's level 1: the leg the remaining 3 bodies use, and the one + // that requires the renderbuffer's ES storage to move together with the textures' - + // glCopyImageSubData needs both endpoints in the same driver format, so a widening that + // reached textures alone would break exactly here. + TEST_F(CopyImagePacked16Scenario, RenderbufferLandsInArrayMipLevelIntact) { + if (!Ready() || IsSkipped()) return; + for (const PackedFormatCase& format : kFormats) { + const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1); + const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7); + const GLuint array = MakeArrayTexture(format, level0, level1); + + if (m_renderbuffer == 0) glGenRenderbuffers(1, &m_renderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer); + glRenderbufferStorage(GL_RENDERBUFFER, format.internalFormat, kFlatSize, kFlatSize); + if (m_fbo == 0) glGenFramebuffers(1, &m_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast(GL_FRAMEBUFFER_COMPLETE)) + << format.name << ": the renderbuffer is not attachable"; + // Field values picked to encode exactly in the narrow fields AND in their + // UNorm8 expansions, so the expected word is the same whichever storage the + // configuration picked - which is the point of the whole scenario. + const int maxG = format.transferType == GL_UNSIGNED_SHORT_5_6_5 ? 63 : 31; + const int max = format.transferType == GL_UNSIGNED_SHORT_4_4_4_4 ? 15 : 31; + const int maxGreen = format.transferType == GL_UNSIGNED_SHORT_4_4_4_4 ? 15 : maxG; + const GLfloat clearColor[4] = {static_cast(8 % (max + 1)) / max, + static_cast(maxGreen / 2) / maxGreen, + static_cast(max - 2) / max, 1.0f}; + glClearBufferfv(GL_COLOR, 0, clearColor); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + ASSERT_EQ(glGetError(), static_cast(GL_NO_ERROR)) << format.name << ": setup failed"; + + glCopyImageSubData(m_renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, array, GL_TEXTURE_2D_ARRAY, 1, + kArrayOffset, kArrayOffset, 0, kRegion, kRegion, 1); + ASSERT_EQ(glGetError(), static_cast(GL_NO_ERROR)) + << format.name << ": glCopyImageSubData raised an error"; + + GLushort clearedWord = 0; + switch (format.transferType) { + case GL_UNSIGNED_SHORT_5_6_5: + clearedWord = static_cast((8 << 11) | ((maxGreen / 2) << 5) | (max - 2)); + break; + case GL_UNSIGNED_SHORT_5_5_5_1: + clearedWord = static_cast((8 << 11) | ((maxGreen / 2) << 6) | ((max - 2) << 1) | 1); + break; + case GL_UNSIGNED_SHORT_4_4_4_4: + clearedWord = static_cast((8 << 12) | ((maxGreen / 2) << 8) | ((max - 2) << 4) | 15); + break; + default: + break; + } + std::vector expectedRegion(static_cast(kRegion) * kRegion, clearedWord); + const auto got = ReadTexImage(GL_TEXTURE_2D_ARRAY, array, 1, format, + static_cast(kLevel1Size) * kLevel1Size * kLayers); + ExpectRegion(got, kLevel1Size, kArrayOffset, kArrayOffset, expectedRegion, kRegion, 0, 0, level1, + (std::string("renderbuffer -> 2d_array level 1, ") + format.name).c_str()); + } + } + + } // namespace +} // namespace MGITest diff --git a/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp b/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp index ff5ac77a..c5332379 100644 --- a/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp +++ b/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp @@ -26,6 +26,7 @@ using MobileGL::MG_Util::SelfTest::ProbeGeometryStageSsboWriteAfterEmitDropped; using MobileGL::MG_Util::SelfTest::ProbeImageLocationPerNameBudget; using MobileGL::MG_Util::SelfTest::ProbeImageWriteReadCoherencyResidual; using MobileGL::MG_Util::SelfTest::ProbeBlitIgnoresDestinationArrayLayer; +using MobileGL::MG_Util::SelfTest::ProbeCopyImageMirrorsPacked16FieldOrder; using MobileGL::MG_Util::SelfTest::ProbeExplicitVertexInputLocationCeiling; using MobileGL::MG_Util::SelfTest::ProbeR32FMultisampleSwizzleCorruption; @@ -107,6 +108,21 @@ namespace { bool blitIgnoresDestinationLayer = false; bool blitIgnoresSourceLayer = false; + // Probe 7: the driver's PHYSICAL field order for a 16-bit packed texel at a non-zero + // mip level of a 2D array is the *_REV mirror of the plain-image order. Modelled at + // upload, which is where the real defect lives: the words such a level stores are the + // mirrored re-encoding, the raw copy moves them verbatim, and the plain 2D readback + // decodes them with the non-REV order - exactly the 0x0047 -> 0x8C20 arithmetic the + // affected Mali hands back. Uploads and readbacks of the SAME image stay consistent, + // which is why only the copy path can observe the knob. + bool packed16ArrayMipFieldOrderMirrored = false; + // The inconclusive path: EVERY array level stores mirrored words, level 0 included, so + // the probe's level-0 control copy is dirty too and no verdict may be reached. + bool packed16EveryArrayLevelMirrored = false; + // Another inconclusive path: the copy silently lands nothing, so the destination keeps + // its 0xFFFF fill - a value that is neither the word nor its mirror. + bool packed16CopyDoesNothing = false; + // ---- object bookkeeping --------------------------------------------- GLenum pendingError = GL_NO_ERROR; GLuint nextShaderId = 1; @@ -136,7 +152,14 @@ namespace { std::map> arrayLayerFill; // framebuffer id -> the (2D array texture, layer) glFramebufferTextureLayer attached. std::map> framebufferLayerAttachment; + // (texture, level) -> the PHYSICAL 16-bit word every texel of that 5551 image holds. + // One word per level is all the packed16 probe distinguishes: it uploads a uniform + // fill and reads one texel. + std::map, GLushort> packedTexelWords; + // framebuffer id -> the plain 2D texture glFramebufferTexture2D attached. + std::map framebuffer2DAttachment; GLuint boundArrayTexture = 0; + GLuint boundTexture2D = 0; GLuint boundDrawFramebuffer = 0; GLuint boundReadFramebuffer = 0; @@ -164,6 +187,17 @@ namespace { return haystack.find(needle) != std::string::npos; } + // The 5_5_5_1 <-> 1_5_5_5_REV field-order mirror: the same fields, packed from the other + // end of the word. 0x0047 (R,G,B,A = 0,1,3,1) becomes 0x8C20 - the exact pair every + // failing KHR-GL4x.copy_image body printed on the affected Mali. + GLushort MirrorPacked5551(GLushort word) { + const GLushort r = (word >> 11) & 0x1F; + const GLushort g = (word >> 6) & 0x1F; + const GLushort b = (word >> 1) & 0x1F; + const GLushort a = word & 0x1; + return static_cast((a << 15) | (b << 10) | (g << 5) | r); + } + // Every `image2D ` the program declares, across all its stages. std::vector DeclaredImageNames(GLuint program) { std::vector names; @@ -401,6 +435,7 @@ namespace { funcs.glBindTexture = [](GLenum target, GLuint texture) { if (target == GL_TEXTURE_2D_MULTISAMPLE) g_fake.boundMultisampleTexture = texture; if (target == GL_TEXTURE_2D_ARRAY) g_fake.boundArrayTexture = texture; + if (target == GL_TEXTURE_2D) g_fake.boundTexture2D = texture; }; funcs.glTexStorage3D = [](GLenum target, GLsizei, GLenum, GLsizei, GLsizei, GLsizei) { if (target == GL_TEXTURE_2D_ARRAY) g_fake.arrayLayerFill[g_fake.boundArrayTexture] = {0, 0}; @@ -418,6 +453,13 @@ namespace { funcs.glDeleteTextures = [](GLsizei n, const GLuint* textures) { for (GLsizei i = 0; i < n; ++i) { if (textures[i] != 0) --g_fake.aliveTextures; + for (auto it = g_fake.packedTexelWords.begin(); it != g_fake.packedTexelWords.end();) { + if (it->first.first == textures[i]) { + it = g_fake.packedTexelWords.erase(it); + } else { + ++it; + } + } } }; funcs.glTexParameteri = [](GLenum target, GLenum pname, GLint param) { @@ -426,8 +468,39 @@ namespace { static_cast(param); } }; - funcs.glTexImage2D = [](GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, - const void*) {}; + // The packed16 probe's endpoints. A plain 2D image stores its 5551 words in the + // canonical (non-REV) order on every knob setting - the defect is confined to array + // mip levels, and keeping the 2D side clean is what lets the readback below decode + // with one order and still reproduce the mirror. + funcs.glTexImage2D = [](GLenum target, GLint level, GLint, GLsizei, GLsizei, GLint, GLenum, + GLenum type, const void* pixels) { + if (target != GL_TEXTURE_2D || type != GL_UNSIGNED_SHORT_5_5_5_1 || pixels == nullptr) return; + GLushort word = 0; + std::memcpy(&word, pixels, sizeof(word)); + g_fake.packedTexelWords[{g_fake.boundTexture2D, level}] = word; + }; + // The defect itself, at the upload where the physical layout is chosen: a mirrored + // array level stores the re-encoded word. Its own readback would decode it back + // consistently - only the raw copy below ever leaks the layout. + funcs.glTexImage3D = [](GLenum target, GLint level, GLint, GLsizei, GLsizei, GLsizei, GLint, + GLenum, GLenum type, const void* pixels) { + if (target != GL_TEXTURE_2D_ARRAY || type != GL_UNSIGNED_SHORT_5_5_5_1 || pixels == nullptr) return; + GLushort word = 0; + std::memcpy(&word, pixels, sizeof(word)); + const bool mirrored = g_fake.packed16EveryArrayLevelMirrored || + (g_fake.packed16ArrayMipFieldOrderMirrored && level >= 1); + g_fake.packedTexelWords[{g_fake.boundArrayTexture, level}] = + mirrored ? MirrorPacked5551(word) : word; + }; + // A raw texel-block move: the PHYSICAL word travels, whichever layout wrote it. + funcs.glCopyImageSubData = [](GLuint srcName, GLenum, GLint srcLevel, GLint, GLint, GLint, + GLuint dstName, GLenum, GLint dstLevel, GLint, GLint, GLint, + GLsizei, GLsizei, GLsizei) { + if (g_fake.packed16CopyDoesNothing) return; + const auto source = g_fake.packedTexelWords.find({srcName, srcLevel}); + if (source == g_fake.packedTexelWords.end()) return; + g_fake.packedTexelWords[{dstName, dstLevel}] = source->second; + }; funcs.glTexSubImage2D = [](GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const void*) {}; funcs.glTexStorage2D = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei) {}; @@ -446,7 +519,11 @@ namespace { g_fake.boundReadFramebuffer = framebuffer; } }; - funcs.glFramebufferTexture2D = [](GLenum, GLenum, GLenum, GLuint, GLint) {}; + funcs.glFramebufferTexture2D = [](GLenum target, GLenum, GLenum, GLuint texture, GLint) { + const GLuint framebuffer = (target == GL_READ_FRAMEBUFFER) ? g_fake.boundReadFramebuffer + : g_fake.boundDrawFramebuffer; + g_fake.framebuffer2DAttachment[framebuffer] = texture; + }; funcs.glFramebufferTextureLayer = [](GLenum target, GLenum, GLuint texture, GLint, GLint layer) { const GLuint framebuffer = (target == GL_READ_FRAMEBUFFER) ? g_fake.boundReadFramebuffer : g_fake.boundDrawFramebuffer; @@ -482,6 +559,7 @@ namespace { for (GLsizei i = 0; i < n; ++i) { if (framebuffers[i] != 0) --g_fake.aliveFramebuffers; g_fake.framebufferLayerAttachment.erase(framebuffers[i]); + g_fake.framebuffer2DAttachment.erase(framebuffers[i]); } }; funcs.glGenVertexArrays = [](GLsizei n, GLuint* arrays) { @@ -561,9 +639,31 @@ namespace { funcs.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { const std::size_t texels = static_cast(width) * static_cast(height); - // Answered before anything else: a read framebuffer that names an array LAYER is the - // layered-blit probe asking what that layer holds, and its bytes have nothing to do - // with the pass/fail texel encoding the image probes below share. + // A read framebuffer naming a plain 2D texture that holds a 5551 word is the + // packed16 probe reading its copy destination. The driver decodes its OWN storage + // with the canonical non-REV order and expands each field by bit replication - + // which is exactly how the mirrored word 0x8C20 becomes (140, 132, 132, 0). + if (const auto attached = g_fake.framebuffer2DAttachment.find(g_fake.boundReadFramebuffer); + attached != g_fake.framebuffer2DAttachment.end() && + g_fake.packedTexelWords.count({attached->second, 0}) != 0) { + // Gated on the texture actually holding a 5551 word, so every OTHER probe that + // attaches a plain 2D texture keeps the pass/fail readback paths below. + const GLushort w = g_fake.packedTexelWords[{attached->second, 0}]; + const auto expand5 = [](GLushort v) { + return static_cast((v << 3) | (v >> 2)); + }; + GLubyte* out = static_cast(pixels); + for (std::size_t i = 0; i < texels; ++i) { + out[i * 4 + 0] = expand5((w >> 11) & 0x1F); + out[i * 4 + 1] = expand5((w >> 6) & 0x1F); + out[i * 4 + 2] = expand5((w >> 1) & 0x1F); + out[i * 4 + 3] = (w & 0x1) ? 255 : 0; + } + return; + } + // Answered before anything else below: a read framebuffer that names an array LAYER + // is the layered-blit probe asking what that layer holds, and its bytes have nothing + // to do with the pass/fail texel encoding the image probes below share. if (const auto layered = g_fake.framebufferLayerAttachment.find(g_fake.boundReadFramebuffer); layered != g_fake.framebufferLayerAttachment.end()) { const auto& fill = g_fake.arrayLayerFill[layered->second.first]; @@ -626,6 +726,8 @@ TEST(DriverBugProbes, AProbeThatCannotRunReportsNoBug) { EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected); EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl)); EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected); + EXPECT_FALSE(ProbeCopyImageMirrorsPacked16FieldOrder(gl)) + << "a probe with no entry points has measured nothing"; } // The section lists only bugs the device HAS, so a driver nothing could be probed on renders @@ -945,3 +1047,52 @@ TEST(DriverBugProbes, ImageCoherencyNeedsBothHalvesOfTheSplitPairInOneStage) { const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected); } + +TEST(DriverBugProbes, Packed16FieldOrderIsCleanOnAConformingDriver) { + ResetFakeDriver(); + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeCopyImageMirrorsPacked16FieldOrder(gl)); + ExpectProbeReleasedEverything(); +} + +TEST(DriverBugProbes, Packed16FieldOrderIsDetectedWhenTheArrayMipLevelIsMirrored) { + ResetFakeDriver(); + g_fake.packed16ArrayMipFieldOrderMirrored = true; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_TRUE(ProbeCopyImageMirrorsPacked16FieldOrder(gl)); + ExpectProbeReleasedEverything(); +} + +// THE CONTROL. A driver whose EVERY array level stores mirrored words fails the level-0 +// control copy too - a different (and larger) defect than the one this probe is entitled to +// report, so it must reach no verdict rather than pin the mip-level shape. +TEST(DriverBugProbes, Packed16FieldOrderReportsNothingWhenTheControlIsMirroredToo) { + ResetFakeDriver(); + g_fake.packed16ArrayMipFieldOrderMirrored = true; + g_fake.packed16EveryArrayLevelMirrored = true; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeCopyImageMirrorsPacked16FieldOrder(gl)); + ExpectProbeReleasedEverything(); +} + +// And the shape that is not this bug: a copy that lands nothing leaves the destination's +// 0xFFFF fill, which matches neither the word nor its mirror - "reached no verdict". +TEST(DriverBugProbes, Packed16FieldOrderReportsNothingWhenTheCopyLandsNothing) { + ResetFakeDriver(); + g_fake.packed16ArrayMipFieldOrderMirrored = true; + g_fake.packed16CopyDoesNothing = true; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeCopyImageMirrorsPacked16FieldOrder(gl)); + ExpectProbeReleasedEverything(); +} + +// The byte arithmetic the fake's mirror encodes, pinned against the QPA evidence. The fake +// models the ARRAY-AS-SOURCE direction (decode 5_5_5_1, re-encode 1_5_5_5_REV): 0x0047 must +// deliver 0x8C20, the exact pair every failing array-as-source copy_image body printed. The +// QPA's array-as-destination bodies show the INVERSE transform (enc_5551 of dec_REV: 0x0007 +// delivered as 0x3800), and enc_REV(dec_5551(x)) inverts enc_5551(dec_REV(x)), so feeding +// the delivered word back through the fake's mirror must reproduce the original. +TEST(DriverBugProbes, Packed16MirrorArithmeticMatchesTheDeviceEvidence) { + EXPECT_EQ(MirrorPacked5551(0x0047), 0x8C20); + EXPECT_EQ(MirrorPacked5551(0x3800), 0x0007); +} diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 4592d792..070885e8 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -3388,6 +3388,46 @@ TEST_F(TextureTest, NormalizePixelFormatKeepsPackedTransferTypesForPackedSizedFo } } +// The packed16 field-order quirk (PixelFormatNormalizeOptionBit::WidenPacked16Norm): where the +// driver's 16-bit packed storage mirrors its field order at a non-zero array mip level (the +// Mali defect behind the KHR-GL4x.copy_image rgb5/rgb5_a1/rgba4 x *2d_array* failures), the +// three ES narrow formats move to 8-bit-per-channel storage. The transfer pair must NOT move +// with the bit - it is already the UNorm8 component layout the canonical shadow holds - and +// no other format may move with it either. +TEST_F(TextureTest, NormalizePixelFormatWidensThePacked16FormatsUnderTheQuirkBit) { + using MG_Util::TextureFormatProcessor::NormalizePixelFormat; + struct { + GLenum requested; + GLenum expectedNarrow; + GLenum expectedWidened; + GLenum expectedFormat; + } cases[] = { + {GL_RGB565, GL_RGB565, GL_RGB8, GL_RGB}, + {GL_RGB5_A1, GL_RGB5_A1, GL_RGBA8, GL_RGBA}, + {GL_RGBA4, GL_RGBA4, GL_RGBA8, GL_RGBA}, + // Negative controls: a 32-bit packed format and an already-8-bit one stay put with + // the bit set - the quirk is about 16-bit packed normalized storage and nothing else. + {GL_RGB10_A2, GL_RGB10_A2, GL_RGB10_A2, GL_RGBA}, + {GL_RGBA8, GL_RGBA8, GL_RGBA8, GL_RGBA}, + }; + for (const auto& c : cases) { + GLenum narrowInternal = 0, narrowFormat = 0, narrowType = 0; + NormalizePixelFormat(c.requested, PixelFormatNormalizeOptionBit::None, &narrowInternal, &narrowFormat, + &narrowType); + EXPECT_EQ(narrowInternal, c.expectedNarrow) << "internalformat 0x" << std::hex << c.requested; + + GLenum widenedInternal = 0, widenedFormat = 0, widenedType = 0; + NormalizePixelFormat(c.requested, PixelFormatNormalizeOptionBit::WidenPacked16Norm, &widenedInternal, + &widenedFormat, &widenedType); + EXPECT_EQ(widenedInternal, c.expectedWidened) << "internalformat 0x" << std::hex << c.requested; + // The transfer pair is identical narrow and widened: the widening changes only the ES + // storage, never how client data is described to it. + EXPECT_EQ(widenedFormat, narrowFormat) << "internalformat 0x" << std::hex << c.requested; + EXPECT_EQ(widenedType, narrowType) << "internalformat 0x" << std::hex << c.requested; + EXPECT_EQ(widenedFormat, c.expectedFormat) << "internalformat 0x" << std::hex << c.requested; + } +} + // GL_RGB565 (ARB_ES2_compatibility / GL 4.1, used directly by the GL CTS) must round-trip // through the internal-format enums; it had no GLToMG mapping at all, so glTexImage* with // GL_RGB565 was rejected as an unknown internal format. diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp index 575b3633..03479881 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp @@ -127,6 +127,21 @@ namespace MobileGL::MG_Util::SelfTest { GLfloat clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f}; GLint packAlignment = 4; GLint packRowLength = 0; + // The rest of the pixel-transfer scope. The probes that upload or read back texels + // run under whatever scope their caller left - the lazy ones run from live paths, + // not just the POST screen - and a caller's skip/row-length/PBO would silently + // shear a probe's own data. Saved so a probe can zero them and the caller gets + // them back. + GLint packSkipPixels = 0; + GLint packSkipRows = 0; + GLint unpackAlignment = 4; + GLint unpackRowLength = 0; + GLint unpackImageHeight = 0; + GLint unpackSkipPixels = 0; + GLint unpackSkipRows = 0; + GLint unpackSkipImages = 0; + GLint pixelPackBuffer = 0; + GLint pixelUnpackBuffer = 0; GLint imageName = 0; GLint imageLevel = 0; GLint imageLayered = 0; @@ -166,6 +181,16 @@ namespace MobileGL::MG_Util::SelfTest { gl.glGetIntegerv(GL_TEXTURE_BINDING_2D_ARRAY, &state.texture2DArray); gl.glGetIntegerv(GL_PACK_ALIGNMENT, &state.packAlignment); gl.glGetIntegerv(GL_PACK_ROW_LENGTH, &state.packRowLength); + gl.glGetIntegerv(GL_PACK_SKIP_PIXELS, &state.packSkipPixels); + gl.glGetIntegerv(GL_PACK_SKIP_ROWS, &state.packSkipRows); + gl.glGetIntegerv(GL_UNPACK_ALIGNMENT, &state.unpackAlignment); + gl.glGetIntegerv(GL_UNPACK_ROW_LENGTH, &state.unpackRowLength); + gl.glGetIntegerv(GL_UNPACK_IMAGE_HEIGHT, &state.unpackImageHeight); + gl.glGetIntegerv(GL_UNPACK_SKIP_PIXELS, &state.unpackSkipPixels); + gl.glGetIntegerv(GL_UNPACK_SKIP_ROWS, &state.unpackSkipRows); + gl.glGetIntegerv(GL_UNPACK_SKIP_IMAGES, &state.unpackSkipImages); + gl.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &state.pixelPackBuffer); + gl.glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &state.pixelUnpackBuffer); if (gl.glGetFloatv != nullptr) { gl.glGetFloatv(GL_COLOR_CLEAR_VALUE, state.clearColor); } @@ -221,6 +246,18 @@ namespace MobileGL::MG_Util::SelfTest { if (gl.glPixelStorei != nullptr) { gl.glPixelStorei(GL_PACK_ALIGNMENT, state.packAlignment); gl.glPixelStorei(GL_PACK_ROW_LENGTH, state.packRowLength); + gl.glPixelStorei(GL_PACK_SKIP_PIXELS, state.packSkipPixels); + gl.glPixelStorei(GL_PACK_SKIP_ROWS, state.packSkipRows); + gl.glPixelStorei(GL_UNPACK_ALIGNMENT, state.unpackAlignment); + gl.glPixelStorei(GL_UNPACK_ROW_LENGTH, state.unpackRowLength); + gl.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, state.unpackImageHeight); + gl.glPixelStorei(GL_UNPACK_SKIP_PIXELS, state.unpackSkipPixels); + gl.glPixelStorei(GL_UNPACK_SKIP_ROWS, state.unpackSkipRows); + gl.glPixelStorei(GL_UNPACK_SKIP_IMAGES, state.unpackSkipImages); + } + if (gl.glBindBuffer != nullptr) { + gl.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast(state.pixelPackBuffer)); + gl.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast(state.pixelUnpackBuffer)); } if (gl.glClearColor != nullptr) { gl.glClearColor(state.clearColor[0], state.clearColor[1], state.clearColor[2], @@ -1886,6 +1923,199 @@ namespace MobileGL::MG_Util::SelfTest { return measurement; } + namespace { + // ===================== PACKED16 COPY-IMAGE FIELD ORDER ===================== + + constexpr const char* kPacked16CopyProbeName = "packed16 copy-image field order"; + + // The shape the KHR-GL4x.copy_image failures pin, verbatim: on the affected Mali only a + // 2D array whose base level is 30x30x12 showed the divergence at LEVEL 1 (the same + // suite's level-0 copies and a 14x14 base's level 1 round-trip clean), so the probe + // reproduces those dimensions rather than a minimal shape that might sit on the clean + // side of whatever allocation threshold picks the driver's layout. + constexpr GLsizei kPacked16BaseSize = 30; + constexpr GLsizei kPacked16Layers = 12; + constexpr GLsizei kPacked16DstSize = 7; + + // One GL_RGB5_A1 texel, as the client word the probe uploads everywhere: + // (R, G, B, A) = (0, 1, 3, 1) under GL_UNSIGNED_SHORT_5_5_5_1. Chosen because 5551 is + // the one 16-bit packed layout whose field widths are not a palindrome - its mirror + // fixes the DIRECTION of the swap - and because this word's mirror differs in every + // channel including alpha, so no expansion rounding can confuse the two predictions. + constexpr Uint16 kPacked16Word = 0x0047; + // What an FBO readback answers for the word, as UNorm8: (0, 1, 3) / 31 and alpha 1. + constexpr GLubyte kPacked16Expected[4] = {0, 8, 25, 255}; + // The same readback when the stored bits are the mirrored re-encoding: 0x0047 decoded + // as 5_5_5_1 and re-encoded as 1_5_5_5_REV is 0x8C20, which the destination's non-REV + // layout then decodes as (17, 16, 16) / 31 with alpha 0. This is byte-for-byte the + // arithmetic behind every failing CTS body (src 0x0047 -> got 0x8C20). + constexpr GLubyte kPacked16Mirrored[4] = {140, 132, 132, 0}; + // A 5-bit step is 255/31 ~ 8.2 UNorm8 codes; half a step accepts every 5-bit-to-8-bit + // expansion a driver uses (floor, round, bit replication) while still telling two + // adjacent 5-bit values apart. + constexpr Int kPacked16Tolerance = 4; + + // A 2-level GL_RGB5_A1 2D array allocated the way MobileGL's own mutable-texture path + // allocates one (glTexImage3D per level), with every texel of both levels holding + // kPacked16Word. MAX_LEVEL is clamped so the two-level chain is complete - some + // drivers refuse glCopyImageSubData on an incomplete texture. + GLuint MakePacked16ArrayTexture(const GLESFunctionsTable& gl) { + GLuint texture = 0; + gl.glGenTextures(1, &texture); + if (texture == 0) return 0; + gl.glBindTexture(GL_TEXTURE_2D_ARRAY, texture); + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 1); + for (GLint level = 0; level < 2; ++level) { + const GLsizei size = kPacked16BaseSize >> level; + const Vector words( + static_cast(size) * static_cast(size) * kPacked16Layers, kPacked16Word); + gl.glTexImage3D(GL_TEXTURE_2D_ARRAY, level, GL_RGB5_A1, size, size, kPacked16Layers, 0, + GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, words.data()); + } + gl.glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + return texture; + } + + // A one-level GL_RGB5_A1 2D destination filled with 0xFFFF - the CTS's own (1,1,1,1) + // destination fill - so a copy that silently did nothing reads as "no verdict" rather + // than as either prediction. + GLuint MakePacked16DstTexture(const GLESFunctionsTable& gl) { + GLuint texture = 0; + gl.glGenTextures(1, &texture); + if (texture == 0) return 0; + gl.glBindTexture(GL_TEXTURE_2D, texture); + gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + const Vector fill(static_cast(kPacked16DstSize) * kPacked16DstSize, Uint16{0xFFFF}); + gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB5_A1, kPacked16DstSize, kPacked16DstSize, 0, GL_RGBA, + GL_UNSIGNED_SHORT_5_5_5_1, fill.data()); + gl.glBindTexture(GL_TEXTURE_2D, 0); + return texture; + } + + // The destination's texel (0, 0), through a framebuffer of its own. False when the + // attachment is incomplete or the read errors - both are declines, not verdicts. + Bool ReadPacked16DstTexel(const GLESFunctionsTable& gl, GLuint texture, GLubyte out[4]) { + GLuint framebuffer = 0; + gl.glGenFramebuffers(1, &framebuffer); + if (framebuffer == 0) return false; + gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + gl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); + Bool read = false; + if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { + gl.glReadBuffer(GL_COLOR_ATTACHMENT0); + Drain(gl); + gl.glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out); + read = gl.glGetError() == GL_NO_ERROR; + } + gl.glBindFramebuffer(GL_FRAMEBUFFER, 0); + gl.glDeleteFramebuffers(1, &framebuffer); + Drain(gl); + return read; + } + + // Copies a kPacked16DstSize-square region out of layer 0 of the array's `sourceLevel` + // onto a freshly filled 2D destination and hands back the destination's texel (0, 0). + // False when the copy raised an error or the readback could not run. + Bool Packed16CopyLandsTexel(const GLESFunctionsTable& gl, GLuint array, GLint sourceLevel, + GLubyte out[4]) { + const GLuint destination = MakePacked16DstTexture(gl); + if (destination == 0) return false; + Drain(gl); + gl.glCopyImageSubData(array, GL_TEXTURE_2D_ARRAY, sourceLevel, 0, 0, 0, destination, + GL_TEXTURE_2D, 0, 0, 0, 0, kPacked16DstSize, kPacked16DstSize, 1); + const Bool copied = gl.glGetError() == GL_NO_ERROR; + const Bool read = copied && ReadPacked16DstTexel(gl, destination, out); + gl.glDeleteTextures(1, &destination); + Drain(gl); + return read; + } + + Bool Packed16TexelNear(const GLubyte got[4], const GLubyte want[4]) { + for (Int i = 0; i < 4; ++i) { + const Int delta = static_cast(got[i]) - static_cast(want[i]); + if (delta > kPacked16Tolerance || delta < -kPacked16Tolerance) return false; + } + return true; + } + } // namespace + + Bool ProbeCopyImageMirrorsPacked16FieldOrder(const GLESFunctionsTable& gl) { + if (!gl.glGenTextures || !gl.glBindTexture || !gl.glTexParameteri || !gl.glTexImage2D || + !gl.glTexImage3D || !gl.glDeleteTextures || !gl.glCopyImageSubData || !gl.glGenFramebuffers || + !gl.glBindFramebuffer || !gl.glFramebufferTexture2D || !gl.glCheckFramebufferStatus || + !gl.glDeleteFramebuffers || !gl.glReadBuffer || !gl.glReadPixels || !gl.glPixelStorei || + !gl.glGetError) { + return false; + } + + SavedState saved; + Save(gl, saved); + // The uploads and readbacks below run under the probe's own tight pixel-transfer + // scope - a caller's skip/row-length/PBO would shear the probe's data into a false + // verdict either way. Restore puts the caller's scope back with the rest. + gl.glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + gl.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + gl.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); + gl.glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + gl.glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); + gl.glPixelStorei(GL_UNPACK_SKIP_IMAGES, 0); + gl.glPixelStorei(GL_PACK_ALIGNMENT, 1); + gl.glPixelStorei(GL_PACK_ROW_LENGTH, 0); + gl.glPixelStorei(GL_PACK_SKIP_PIXELS, 0); + gl.glPixelStorei(GL_PACK_SKIP_ROWS, 0); + if (gl.glBindBuffer != nullptr) { + gl.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + gl.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + } + Drain(gl); + + Bool detected = false; + const GLuint array = MakePacked16ArrayTexture(gl); + GLubyte control[4] = {0, 0, 0, 0}; + GLubyte subject[4] = {0, 0, 0, 0}; + // THE CONTROL: the identical copy out of the array's LEVEL 0, which is clean on the + // affected driver too. It proves glCopyImageSubData works between a 5551 array and a + // 5551 2D image at all, that the upload and the FBO readback round-trip the word, and + // that only the mip level moves the answer - so a driver with no copy_image, or none + // for these formats, reaches no verdict instead of being reported as this. + if (array == 0 || !Packed16CopyLandsTexel(gl, array, 0, control)) { + MGLOG_I("[driver-bug] %s probe reached no verdict (the level-0 control copy could not run)", + kPacked16CopyProbeName); + } else if (!Packed16TexelNear(control, kPacked16Expected)) { + MGLOG_I("[driver-bug] %s probe reached no verdict (the level-0 control read back " + "(%d, %d, %d, %d) instead of the uploaded word's (%d, %d, %d, %d))", + kPacked16CopyProbeName, control[0], control[1], control[2], control[3], + kPacked16Expected[0], kPacked16Expected[1], kPacked16Expected[2], kPacked16Expected[3]); + } else if (!Packed16CopyLandsTexel(gl, array, 1, subject)) { + MGLOG_I("[driver-bug] %s probe reached no verdict (the level-1 subject copy could not run)", + kPacked16CopyProbeName); + } else if (Packed16TexelNear(subject, kPacked16Mirrored)) { + detected = true; + MGLOG_I("[driver-bug] %s probe: a copy out of the array's level 1 delivered " + "(%d, %d, %d, %d), the 1_5_5_5_REV re-encoding of the word - THE FIELD ORDER " + "OF A NON-ZERO ARRAY MIP LEVEL IS MIRRORED", + kPacked16CopyProbeName, subject[0], subject[1], subject[2], subject[3]); + } else if (!Packed16TexelNear(subject, kPacked16Expected)) { + MGLOG_I("[driver-bug] %s probe reached no verdict (the level-1 copy read back " + "(%d, %d, %d, %d), which is neither the word nor its mirror)", + kPacked16CopyProbeName, subject[0], subject[1], subject[2], subject[3]); + } + if (array != 0) gl.glDeleteTextures(1, &array); + Restore(gl, saved); + return detected; + } + + Bool CopyImageMirrorsPacked16FieldOrder(const GLESFunctionsTable& gl) { + // One driver per process, and the answer is structural (the driver's storage layout + // for a shape), not sampled. + static const Bool mirrored = ProbeCopyImageMirrorsPacked16FieldOrder(gl); + return mirrored; + } + namespace { Optional ProbeExplicitVertexInputLocationCeilingBug(const GLESFunctionsTable& gl) { const VertexInputLocationCeilingMeasurement& measurement = ExplicitVertexInputLocationCeiling(gl); @@ -2053,6 +2283,25 @@ namespace MobileGL::MG_Util::SelfTest { Move(detail)}; } + Optional ProbeCopyImagePacked16FieldOrderBug(const GLESFunctionsTable& gl) { + if (!CopyImageMirrorsPacked16FieldOrder(gl)) return std::nullopt; + return DriverBugFinding{ + "glCopyImageSubData mirrors 16-bit packed texels at a non-zero array mip level", + DriverBugVerdict::Fixed, + "the driver's physical field order for a 16-bit packed texel (RGB565 / RGB5_A1 / " + "RGBA4) at a non-zero mip level of a GL_TEXTURE_2D_ARRAY is the *_REV mirror of " + "the order every other image uses, so a glCopyImageSubData - a raw texel-block " + "move - between such a level and any other image lands the R/G/B/A fields " + "reversed (a 5551 word 0x0047 arrives as 0x8C20). Uploads and readbacks of the " + "same level are clean - the driver decodes its own layout consistently, which is " + "this probe's control - so only the raw-copy path ever crosses the two layouts. " + "MobileGL stores these three formats as 8-bit-per-channel ES storage on this " + "driver instead (GL_RGB8 / GL_RGBA8, the storage their canonical shadow already " + "holds and the client word round-trips through exactly), so no 16-bit packed " + "image is left for a copy to disagree about, at twice the memory for images of " + "those formats"}; + } + // The table. One row per known driver bug; see the header for how to add a sibling. using DriverBugProbeFn = Optional (*)(const GLESFunctionsTable&); constexpr DriverBugProbeFn kGlesDriverBugProbes[] = { @@ -2064,6 +2313,7 @@ namespace MobileGL::MG_Util::SelfTest { &ProbeExplicitVertexInputLocationCeilingBug, &ProbeLayeredBlitDestinationBug, &ProbeLocatedIoBlockPayloadBug, + &ProbeCopyImagePacked16FieldOrderBug, }; } // namespace diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h index c7671bd1..62e74fe4 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h @@ -299,6 +299,35 @@ namespace MobileGL::MG_Util::SelfTest { const ImageCoherencyResidualMeasurement& ImageWriteReadCoherencyResidual( const MG_External::GLESFunctionsTable& gl); + // Copies one known GL_UNSIGNED_SHORT_5_5_5_1 word out of a GL_RGB5_A1 2D array's mip + // level 1 into a plain 2D image with glCopyImageSubData and reads the landed texel back. + // Returns true only when the level-1 copy delivers the word's 5_5_5_1 <-> 1_5_5_5_REV + // field-order mirror while the identical level-0 copy delivers the word itself. + // + // The affected Mali stores 16-bit packed texels (RGB565 / RGB5_A1 / RGBA4) at a non-zero + // mip level of a 2D array in the *_REV field order every other image does NOT use. + // Uploads and readbacks decode that layout consistently, so nothing but a raw texel-block + // move can see it - which is exactly what glCopyImageSubData is defined to be, and why + // the whole KHR-GL4x.copy_image rgb5/rgb5_a1/rgba4 x *2d_array* matrix fails there while + // every other suite touching these formats passes. The texture reproduces the failing + // shape verbatim (a 30x30x12 two-level array; a 14x14 base's level 1 measured clean on + // the same driver, so a minimal shape might not manifest the layout). + // + // THE CONTROL is the identical copy out of mip level 0, which is clean on the affected + // driver too: it proves copy_image works between these images at all and that the + // upload/readback round trip is exact, so a driver that cannot host the shape reaches no + // verdict instead of being reported as this. The subject must also match the mirror + // PREDICTION, not merely differ from the expectation - a copy that delivered anything + // else is a different defect and reaches no verdict either. Restores every piece of GL + // state it touches. + Bool ProbeCopyImageMirrorsPacked16FieldOrder(const MG_External::GLESFunctionsTable& gl); + + // ProbeCopyImageMirrorsPacked16FieldOrder(), evaluated at most once per process. The + // DirectGLES format normalization consults this to decide whether the three 16-bit packed + // normalized formats must be stored as 8-bit-per-channel ES storage (see + // PixelFormatNormalizeOptionBit::WidenPacked16Norm). + Bool CopyImageMirrorsPacked16FieldOrder(const MG_External::GLESFunctionsTable& gl); + // Every known driver bug this GLES driver actually has. Bugs it does not have are absent, // so an unaffected device renders an empty section rather than a wall of "not affected". Vector CollectGlesKnownDriverBugs(const MG_External::GLESFunctionsTable& gl); diff --git a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp index df504d4e..0797bc2d 100644 --- a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp +++ b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp @@ -340,17 +340,38 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { // (VkTextureManager::ResolveTextureFormatInfo resolves all six legacy low-bit formats // to R8G8B8A8_UNORM), so the two backends now agree here. // - // Only the DESKTOP-ONLY formats move. GL_RGBA4 and GL_RGB5_A1 are ES formats an - // application can legitimately ask for - the same normalization picks the storage for - // glRenderbufferStorage - so widening them would be a memory decision, not a - // correctness one. Nothing about the REPORTED precision moves either way: - // GL_TEXTURE_*_SIZE and glGetInternalformativ answer from TextureMetrics, keyed on the - // requested format, not on the ES storage. + // Only the DESKTOP-ONLY formats move UNCONDITIONALLY. GL_RGBA4, GL_RGB5_A1 and + // GL_RGB565 are ES formats an application can legitimately ask for - the same + // normalization picks the storage for glRenderbufferStorage - so widening them + // used to be declined as "a memory decision, not a correctness one". The 18 + // KHR-GL4x.copy_image.functional bodies on Mali falsified that: the driver's own + // 16-bit packed storage keeps a MIRRORED field order at a non-zero mip level of a + // 2D array, so a raw glCopyImageSubData between such a level and any other image + // delivers the channels reversed (0x0007 -> 0x3800 for a 5551 word: the 1_5_5_5_REV + // re-encoding of the same fields). Where that is measured - + // WidenPacked16Norm, set from the POST probe or its ForceOn override - the three + // formats take the same 8-bit widening; everywhere else they stay narrow and the + // memory argument stands. Nothing about the REPORTED precision moves either way: + // GL_TEXTURE_*_SIZE and glGetInternalformativ answer from TextureMetrics, keyed on + // the requested format, not on the ES storage. case GL_R3_G3_B2: case GL_RGB4: case GL_RGB5: *outInternalFormat = GL_RGB8; break; + // GL_RGB5 above is nominally the same resolution, but a TEXTURE never arrives here + // as GL_RGB5: ConvertGLEnumToTextureInternalFormat folds GL_RGB5 and GL_RGB565 onto + // one logical format whose GL spelling is GL_RGB565, so this case is the one the + // allocation path actually reaches for both spellings. + case GL_RGB565: + *outInternalFormat = + (options & PixelFormatNormalizeOptionBit::WidenPacked16Norm) ? GL_RGB8 : internalFormat; + break; + case GL_RGB5_A1: + case GL_RGBA4: + *outInternalFormat = + (options & PixelFormatNormalizeOptionBit::WidenPacked16Norm) ? GL_RGBA8 : internalFormat; + break; case GL_RGB10: case GL_RGB12: *outInternalFormat = (options & PixelFormatNormalizeOptionBit::NoNorm16) || diff --git a/MobileGL/MG_Util/Texture/TextureFormatProcessor.h b/MobileGL/MG_Util/Texture/TextureFormatProcessor.h index 23c3d46c..bb2420fd 100644 --- a/MobileGL/MG_Util/Texture/TextureFormatProcessor.h +++ b/MobileGL/MG_Util/Texture/TextureFormatProcessor.h @@ -44,6 +44,17 @@ namespace MobileGL { // IS exact - every value in [-127, 127] divided by 127 round-trips through a half - so the // substitute matches what the always-on GL_RGBA8_SNORM fallback already picks. NoSnorm8RenderTarget = 1 << 9, + // Store the three 16-bit packed normalized formats (GL_RGB565, GL_RGB5_A1, GL_RGBA4) + // as 8-bit-per-channel ES storage (GL_RGB8 / GL_RGBA8), the way the desktop-only + // narrow formats already are. Set by DirectGLES when the driver's 16-bit packed + // storage cannot be trusted as a raw-copy endpoint: some Mali drivers keep a + // MIRRORED field order for these texels at a non-zero mip level of a 2D array, so + // glCopyImageSubData (a raw texel-block move) delivers the channels reversed. The + // (format, type) transfer pair does not move with the bit - it is already the + // UNorm8 component layout the canonical shadow holds for all three formats. + // Reported precision does not move either: GL_TEXTURE_*_SIZE and + // glGetInternalformativ answer from TextureMetrics, keyed on the requested format. + WidenPacked16Norm = 1 << 10, None = 0, }; namespace MG_Util::TextureFormatProcessor {