From 5d140a41ce56024f484a8ce715f940628631094e Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 27 Aug 2026 23:06:31 -0400 Subject: [PATCH] [Fix] (Espryt): re-arm the packed16 probe on the allocation-scoped mirror the device actually has and let any mirrored level trigger the widening --- MobileGL/Config.h | 9 +- MobileGL/MG_Backend/DirectGLES/Utils.h | 4 +- .../Scenarios/CopyImagePacked16Scenario.cpp | 15 +- .../MG_Test/SelfTest/DriverBugProbesTest.cpp | 104 ++++++---- MobileGL/MG_Test/Texture/TextureTest.cpp | 4 +- MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp | 185 +++++++++++------- MobileGL/MG_Util/SelfTest/DriverBugProbes.h | 49 ++--- .../Texture/TextureFormatProcessor.cpp | 11 +- .../MG_Util/Texture/TextureFormatProcessor.h | 8 +- 9 files changed, 239 insertions(+), 150 deletions(-) diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 38f61d2c..3337f385 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -264,10 +264,11 @@ namespace MobileGL::MG_Config { // 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 + // (SelfTest::CopyImageMirrorsPacked16FieldOrder): some Mali drivers store SOME + // packed16 allocations with a MIRRORED field order (allocation-scoped and + // shape/context dependent - the failing 30x30x12 GL_TEXTURE_2D_ARRAYs are mirrored + // at every level), so glCopyImageSubData - a raw texel-block move - lands R/G/B/A + // reversed whenever exactly one endpoint sits in a mirrored allocation // (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 diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index c795198c..3f9d8379 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -47,8 +47,8 @@ namespace MobileGL::MG_Backend::DirectGLES { 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 + // driver stores some packed16 allocations with a mirrored field order + // (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 diff --git a/MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp index 7e6e50ab..5dd7eb6e 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp @@ -12,20 +12,21 @@ // 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 +// affected Mali the mirrored *_REV field order is a property of WHOLE ALLOCATIONS (shape- +// and context-dependent; the failing 30x30x12 arrays carry it at every level, the small +// arrays of the suite's passing iterations do not), and glCopyImageSubData - a raw +// texel-block move - between a mirrored allocation and a plain one 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 +// image 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 THREE levels and the flat endpoint is 7x7 with three levels // (7/3/1) because that is the allocation the failures pin - the CTS builds every functional -// texture with FUNCTIONAL_TEST_N_LEVELS = 3 (makeTextureComplete(0, 2)) - and the same -// suite's level-0 copies and a 14x14 base's level 1 measured clean on the same driver, so -// any deviation from the measured shape (a smaller array, a shorter chain) might sit on the -// clean side of whatever allocation threshold picks the driver's layout. +// texture with FUNCTIONAL_TEST_N_LEVELS = 3 (makeTextureComplete(0, 2)) - and any deviation +// from the measured shape might sit on the clean side of whatever allocation heuristic picks +// the driver's layout. // // The repair under test is the packed16 storage widening // (PixelFormatNormalizeOptionBit::WidenPacked16Norm): where the POST probe diff --git a/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp b/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp index a112a298..f8b65d93 100644 --- a/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp +++ b/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp @@ -108,23 +108,26 @@ 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 - // the raw copy, which is the only path that can observe it (uploads and readbacks of - // the same image decode the driver's own layout consistently): a copy whose SOURCE is - // such a level delivers the mirrored re-encoding, which the plain 2D readback then - // decodes with the non-REV order - exactly the 0x0047 -> 0x8C20 arithmetic the - // affected Mali hands back. The mirror only engages for the allocation the CTS - // failures were measured on - a THREE-level 30x30x12 array - so a probe that stopped - // building the triggering shape (fewer levels, other dimensions) stops detecting, - // which is exactly what these tests are for: on the real driver a 14x14 base's level 1 - // measured clean, and nobody has measured a two-level chain at all. - 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. + // Probe 7: the driver stores a WHOLE 16-bit packed ALLOCATION with its fields packed + // from the other end of the word - on the measured device, every level of the probe's + // 30x30x12 three-level array, while same-shape plain-2D images stay in the canonical + // order. Modelled at the raw copy, which is the only path that can observe it (uploads + // and readbacks of the same image decode the driver's own layout consistently): a copy + // whose SOURCE is any level of the mirrored allocation delivers the mirrored + // re-encoding, which the plain-2D readback then decodes with the non-REV order - + // exactly the 0x0047 -> 0x8C20 arithmetic the affected Mali hands back. The mirror + // only engages for the allocation the failures were measured on - a THREE-level + // 30x30x12 array - so a probe that stopped building the triggering shape (fewer + // levels, other dimensions) stops detecting, which is exactly what these tests are + // for. + bool packed16ArrayAllocationMirrored = false; + // "Not this bug": the UPLOAD corrupts, so the array's own direct readback is already + // wrong. The probe's round-trip control must veto the verdict - the widening's + // raw-copy reasoning says nothing about an upload defect. + bool packed16UploadCorrupted = false; + // The inconclusive path: the copy silently lands nothing, so every destination keeps + // its 0xFFFF fill - a value that is neither the word nor its mirror - and the 2D-to-2D + // machinery control fails first. bool packed16CopyDoesNothing = false; // ---- object bookkeeping --------------------------------------------- @@ -156,6 +159,9 @@ namespace { std::map> arrayLayerFill; // framebuffer id -> the (2D array texture, layer) glFramebufferTextureLayer attached. std::map> framebufferLayerAttachment; + // framebuffer id -> the LEVEL that same call named. Kept apart so the layered-blit + // bookkeeping above keeps its shape; the packed16 probe reads array LEVELS directly. + std::map framebufferLayerLevel; // (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. @@ -495,12 +501,15 @@ namespace { g_fake.packedTexelWords[{g_fake.boundTexture2D, level}] = word; }; // Records the allocation shape the mirror below is gated on, and the uploaded word. + // Under the upload-corruption knob the STORED word is already wrong - the "not this + // bug" shape the probe's round-trip control must catch. funcs.glTexImage3D = [](GLenum target, GLint level, GLint, GLsizei width, GLsizei height, GLsizei depth, 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)); - g_fake.packedTexelWords[{g_fake.boundArrayTexture, level}] = word; + g_fake.packedTexelWords[{g_fake.boundArrayTexture, level}] = + g_fake.packed16UploadCorrupted ? MirrorPacked5551(word) : word; auto& allocation = g_fake.packedArrayAllocations[g_fake.boundArrayTexture]; if (level == 0) { allocation.width = width; @@ -510,7 +519,7 @@ namespace { if (level >= 0 && level < 8) allocation.levelMask |= 1u << level; }; // A raw texel-block move: the PHYSICAL word travels. The defect lives here - a source - // level whose storage keeps the mirrored layout delivers the re-encoded word - and it + // in the mirrored ALLOCATION delivers the re-encoded word from EVERY level - and it // only exists for the allocation it was measured on: three levels of a 30x30x12 array. funcs.glCopyImageSubData = [](GLuint srcName, GLenum, GLint srcLevel, GLint, GLint, GLint, GLuint dstName, GLenum, GLint dstLevel, GLint, GLint, GLint, @@ -524,8 +533,7 @@ namespace { allocation->second.width == 30 && allocation->second.height == 30 && allocation->second.layers == 12 && allocation->second.levelMask == 0b111u; - if (measuredShape && (g_fake.packed16EveryArrayLevelMirrored || - (g_fake.packed16ArrayMipFieldOrderMirrored && srcLevel >= 1))) { + if (measuredShape && g_fake.packed16ArrayAllocationMirrored) { word = MirrorPacked5551(word); } g_fake.packedTexelWords[{dstName, dstLevel}] = word; @@ -553,10 +561,11 @@ namespace { : g_fake.boundDrawFramebuffer; g_fake.framebuffer2DAttachment[framebuffer] = texture; }; - funcs.glFramebufferTextureLayer = [](GLenum target, GLenum, GLuint texture, GLint, GLint layer) { + funcs.glFramebufferTextureLayer = [](GLenum target, GLenum, GLuint texture, GLint level, GLint layer) { const GLuint framebuffer = (target == GL_READ_FRAMEBUFFER) ? g_fake.boundReadFramebuffer : g_fake.boundDrawFramebuffer; g_fake.framebufferLayerAttachment[framebuffer] = {texture, layer}; + g_fake.framebufferLayerLevel[framebuffer] = level; }; funcs.glReadBuffer = [](GLenum) {}; // The defect itself: the source layer is read from where the READ framebuffer says (unless @@ -588,6 +597,7 @@ namespace { for (GLsizei i = 0; i < n; ++i) { if (framebuffers[i] != 0) --g_fake.aliveFramebuffers; g_fake.framebufferLayerAttachment.erase(framebuffers[i]); + g_fake.framebufferLayerLevel.erase(framebuffers[i]); g_fake.framebuffer2DAttachment.erase(framebuffers[i]); } }; @@ -690,11 +700,32 @@ namespace { } 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. + // A read framebuffer naming an array LEVEL that holds a 5551 word is the packed16 + // probe's round-trip control: the driver decodes its OWN storage, so whatever the + // physical word is - mirrored at upload under that knob included - its own decode + // is handed back with the canonical field meaning. if (const auto layered = g_fake.framebufferLayerAttachment.find(g_fake.boundReadFramebuffer); layered != g_fake.framebufferLayerAttachment.end()) { + const auto levelIt = g_fake.framebufferLayerLevel.find(g_fake.boundReadFramebuffer); + const GLint attachedLevel = levelIt == g_fake.framebufferLayerLevel.end() ? 0 : levelIt->second; + if (const auto word = g_fake.packedTexelWords.find({layered->second.first, attachedLevel}); + word != g_fake.packedTexelWords.end()) { + const GLushort w = word->second; + 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; + } + // Otherwise it is the layered-blit probe asking what a layer holds, and its + // bytes have nothing to do with the pass/fail texel encoding the image probes + // below share. const auto& fill = g_fake.arrayLayerFill[layered->second.first]; const GLint layer = layered->second.second; const GLubyte value = @@ -1084,31 +1115,32 @@ TEST(DriverBugProbes, Packed16FieldOrderIsCleanOnAConformingDriver) { ExpectProbeReleasedEverything(); } -TEST(DriverBugProbes, Packed16FieldOrderIsDetectedWhenTheArrayMipLevelIsMirrored) { +// The measured device shape: EVERY level of the mirrored allocation delivers the +// re-encoding, and the machinery/round-trip controls stay clean, so the probe must detect. +TEST(DriverBugProbes, Packed16FieldOrderIsDetectedWhenTheArrayAllocationIsMirrored) { ResetFakeDriver(); - g_fake.packed16ArrayMipFieldOrderMirrored = true; + g_fake.packed16ArrayAllocationMirrored = 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) { +// THE ROUND-TRIP CONTROL. A driver that corrupts the UPLOAD hands the mirror back from the +// array's own direct readback too - a different defect, and one the widening's raw-copy +// reasoning says nothing about - so the probe must reach no verdict rather than claim it. +TEST(DriverBugProbes, Packed16FieldOrderReportsNothingWhenTheUploadItselfCorrupts) { ResetFakeDriver(); - g_fake.packed16ArrayMipFieldOrderMirrored = true; - g_fake.packed16EveryArrayLevelMirrored = true; + g_fake.packed16UploadCorrupted = 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". +// And the shape that is not this bug: a copy that lands nothing leaves every destination's +// 0xFFFF fill, so the 2D-to-2D machinery control fails first - "reached no verdict". TEST(DriverBugProbes, Packed16FieldOrderReportsNothingWhenTheCopyLandsNothing) { ResetFakeDriver(); - g_fake.packed16ArrayMipFieldOrderMirrored = true; + g_fake.packed16ArrayAllocationMirrored = true; g_fake.packed16CopyDoesNothing = true; const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); EXPECT_FALSE(ProbeCopyImageMirrorsPacked16FieldOrder(gl)); diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 070885e8..e1aa29f8 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -3389,8 +3389,8 @@ 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 +// driver stores some packed16 allocations with a mirrored field order (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. diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp index f4e3802c..152b9aa6 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp @@ -1929,17 +1929,22 @@ namespace MobileGL::MG_Util::SelfTest { 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. VERBATIM INCLUDES - // THE LEVEL COUNT: the CTS allocates THREE-level chains on BOTH endpoints - // (FUNCTIONAL_TEST_N_LEVELS = 3, makeTextureComplete(0, 2): 30/15/7 x12 for the array, - // 7/3/1 for the plain image), and every device data point above came from those - // allocations - a chain one level shorter has never been measured on the affected - // driver, and a probe miss here is not a red anything, it is the widening silently - // staying inert with all 18 bodies red. + // The shape the KHR-GL4x.copy_image failures pin: a 30x30x12 GL_RGB5_A1 2D array with + // the CTS's three-level chain (FUNCTIONAL_TEST_N_LEVELS = 3, makeTextureComplete(0, 2): + // 30/15/7 x12; the plain endpoints are 7/3/1), against plain-2D endpoints. + // + // WHAT THE DEVICE MEASUREMENTS ACTUALLY SHOWED (round 2): the mirrored field order is a + // property of the WHOLE ALLOCATION, not of a mip level. In MobileGL's live context this + // array shape lands in the *_REV layout at EVERY level - the first deployment's control + // copy out of LEVEL 0 delivered the mirror too, which is what vetoed its own verdict - + // while the small arrays the CTS's passing iterations used (7- and 15-texel bases; its + // src/dst dim loop is {7, 15}, so a base-30 array only ever appears at level 1) land in + // the plain layout. A raw standalone context on the same driver additionally showed one- + // and two-level 30x30x12 arrays mirrored where a fresh three-level one is not, so the + // driver's layout choice depends on allocation shape AND context history. The probe + // therefore measures IN SITU - this very context is the one the application's copies run + // in - and treats a mirror delivered from ANY level as the finding, with the machinery + // controls below instead of a per-level "clean" assumption that does not exist. constexpr GLsizei kPacked16BaseSize = 30; constexpr GLsizei kPacked16Layers = 12; constexpr GLsizei kPacked16DstSize = 7; @@ -1990,7 +1995,11 @@ namespace MobileGL::MG_Util::SelfTest { // The plain-2D destination, three levels (7/3/1) like the CTS's, every level 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) { + // A plain-2D endpoint with the CTS's three-level 7/3/1 chain, every texel of every + // level holding `fill`: 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), + // or kPacked16Word for the machinery control's source. + GLuint MakePacked16FlatTexture(const GLESFunctionsTable& gl, Uint16 fill) { GLuint texture = 0; gl.glGenTextures(1, &texture); if (texture == 0) return 0; @@ -2000,22 +2009,28 @@ namespace MobileGL::MG_Util::SelfTest { gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, kPacked16Levels - 1); for (GLint level = 0; level < kPacked16Levels; ++level) { const GLsizei size = std::max(kPacked16DstSize >> level, 1); - const Vector fill(static_cast(size) * size, Uint16{0xFFFF}); + const Vector texels(static_cast(size) * size, fill); gl.glTexImage2D(GL_TEXTURE_2D, level, GL_RGB5_A1, size, size, 0, GL_RGBA, - GL_UNSIGNED_SHORT_5_5_5_1, fill.data()); + GL_UNSIGNED_SHORT_5_5_5_1, texels.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]) { + // Texel (0, 0) of a 2D level 0, or of layer 0 of an array's `level`, through a + // framebuffer of its own. False when the attachment is incomplete or the read errors - + // both are declines, not verdicts. + Bool ReadPacked16Texel(const GLESFunctionsTable& gl, GLuint texture, Bool isArray, GLint level, + 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); + if (isArray) { + gl.glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, level, 0); + } else { + gl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, level); + } Bool read = false; if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { gl.glReadBuffer(GL_COLOR_ATTACHMENT0); @@ -2029,18 +2044,18 @@ namespace MobileGL::MG_Util::SelfTest { 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); + // Copies a kPacked16DstSize-square region out of (source, sourceTarget, sourceLevel) + // layer 0 onto a freshly 0xFFFF-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 source, GLenum sourceTarget, + GLint sourceLevel, GLubyte out[4]) { + const GLuint destination = MakePacked16FlatTexture(gl, Uint16{0xFFFF}); if (destination == 0) return false; Drain(gl); - gl.glCopyImageSubData(array, GL_TEXTURE_2D_ARRAY, sourceLevel, 0, 0, 0, destination, + gl.glCopyImageSubData(source, sourceTarget, 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); + const Bool read = copied && ReadPacked16Texel(gl, destination, false, 0, out); gl.glDeleteTextures(1, &destination); Drain(gl); return read; @@ -2058,9 +2073,9 @@ namespace MobileGL::MG_Util::SelfTest { 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) { + !gl.glBindFramebuffer || !gl.glFramebufferTexture2D || !gl.glFramebufferTextureLayer || + !gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers || !gl.glReadBuffer || + !gl.glReadPixels || !gl.glPixelStorei || !gl.glGetError) { return false; } @@ -2087,42 +2102,74 @@ namespace MobileGL::MG_Util::SelfTest { 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)", + const GLuint flatSource = MakePacked16FlatTexture(gl, kPacked16Word); + GLubyte machinery[4] = {0, 0, 0, 0}; + GLubyte direct[4] = {0, 0, 0, 0}; + GLubyte level0[4] = {0, 0, 0, 0}; + GLubyte level1[4] = {0, 0, 0, 0}; + // CONTROL ONE, the machinery: the same copy between two 2D images of the same + // three-level shape. Two identical allocations share the driver's layout whatever it + // is, so this must deliver the word on ANY driver that can run copy_image on these + // formats at all - a driver that cannot reaches no verdict instead of being reported + // as this. + if (array == 0 || flatSource == 0 || + !Packed16CopyLandsTexel(gl, flatSource, GL_TEXTURE_2D, 0, machinery)) { + MGLOG_I("[driver-bug] %s probe reached no verdict (the 2D-to-2D machinery control " + "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], + } else if (!Packed16TexelNear(machinery, kPacked16Expected)) { + MGLOG_I("[driver-bug] %s probe reached no verdict (the 2D-to-2D machinery control " + "read back (%d, %d, %d, %d) instead of the word's (%d, %d, %d, %d))", + kPacked16CopyProbeName, machinery[0], machinery[1], machinery[2], machinery[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)", + } else if (!ReadPacked16Texel(gl, array, true, 1, direct) || + !Packed16TexelNear(direct, kPacked16Expected)) { + // CONTROL TWO, the array's own round trip: reading the level DIRECTLY decodes the + // driver's own storage and must deliver the word whatever layout it picked. A wrong + // answer here means the UPLOAD is what corrupts - a different defect, and one the + // widening's raw-copy reasoning says nothing about. + MGLOG_I("[driver-bug] %s probe reached no verdict (the array's own level-1 readback " + "answered (%d, %d, %d, %d) instead of the word - the upload, not the copy, " + "is what diverges here)", + kPacked16CopyProbeName, direct[0], direct[1], direct[2], direct[3]); + } else if (!Packed16CopyLandsTexel(gl, array, GL_TEXTURE_2D_ARRAY, 0, level0) || + !Packed16CopyLandsTexel(gl, array, GL_TEXTURE_2D_ARRAY, 1, level1)) { + MGLOG_I("[driver-bug] %s probe reached no verdict (an array-source 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]); } else { - // The clean verdict is logged too: on a device run the FIRST question is whether - // this probe executed at all, and a silent clean path is indistinguishable from a - // probe that never ran. - MGLOG_I("[driver-bug] %s probe: a copy out of the array's level 1 delivered the word " - "intact - the field order is consistent", - kPacked16CopyProbeName); + // THE SUBJECTS: the same copy out of the array's levels 0 and 1. The mirror is an + // allocation property, not a level property - the affected device delivers it from + // BOTH levels of this array in MobileGL's context - so a mirror from EITHER level + // is the finding, and each must match the mirror PREDICTION, not merely differ + // from the word: anything else is a different defect and reaches no verdict. + const Bool level0Mirrored = Packed16TexelNear(level0, kPacked16Mirrored); + const Bool level1Mirrored = Packed16TexelNear(level1, kPacked16Mirrored); + const Bool level0Clean = Packed16TexelNear(level0, kPacked16Expected); + const Bool level1Clean = Packed16TexelNear(level1, kPacked16Expected); + if (level0Mirrored || level1Mirrored) { + detected = true; + MGLOG_I("[driver-bug] %s probe: copies out of the array delivered level 0 " + "(%d, %d, %d, %d) and level 1 (%d, %d, %d, %d) - the 1_5_5_5_REV " + "re-encoding of the word - THE ALLOCATION'S FIELD ORDER IS MIRRORED", + kPacked16CopyProbeName, level0[0], level0[1], level0[2], level0[3], + level1[0], level1[1], level1[2], level1[3]); + } else if (!level0Clean || !level1Clean) { + MGLOG_I("[driver-bug] %s probe reached no verdict (array copies read back level 0 " + "(%d, %d, %d, %d) and level 1 (%d, %d, %d, %d), neither the word nor its " + "mirror)", + kPacked16CopyProbeName, level0[0], level0[1], level0[2], level0[3], + level1[0], level1[1], level1[2], level1[3]); + } else { + // The clean verdict is logged too: on a device run the FIRST question is + // whether this probe executed at all, and a silent clean path is + // indistinguishable from a probe that never ran. + MGLOG_I("[driver-bug] %s probe: copies out of both array levels delivered the " + "word intact - the field order is consistent", + kPacked16CopyProbeName); + } } + if (flatSource != 0) gl.glDeleteTextures(1, &flatSource); if (array != 0) gl.glDeleteTextures(1, &array); Restore(gl, saved); return detected; @@ -2314,13 +2361,15 @@ namespace MobileGL::MG_Util::SelfTest { const Bool widened = MG_Config::Features.EsprytWidenPacked16Storage != MG_Config::QuirkOverride::ForceOff; String detail = - "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 " + "the driver stores SOME 16-bit packed images (RGB565 / RGB5_A1 / RGBA4) with the " + "R/G/B/A fields packed from the other end of the word - which allocations get the " + "*_REV layout depends on shape and context history (measured here on a 30x30x12 " + "three-level 2D array, every level of it) - so a glCopyImageSubData, a raw " + "texel-block move, between a mirrored allocation and a plain one lands the 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. "; + "same image are clean - the driver decodes its own layout consistently, which is " + "this probe's second control - so only the raw-copy path ever crosses the two " + "layouts. "; if (widened) { detail += "MobileGL stores these three formats as 8-bit-per-channel ES storage on this " @@ -2330,13 +2379,13 @@ namespace MobileGL::MG_Util::SelfTest { "memory for images of those formats; override with " "MOBILEGL_WIDEN_PACKED16_STORAGE"; return DriverBugFinding{ - "glCopyImageSubData mirrors 16-bit packed texels at a non-zero array mip level", + "glCopyImageSubData mirrors 16-bit packed texels between differently-laid-out images", DriverBugVerdict::Fixed, detail}; } detail += "MOBILEGL_WIDEN_PACKED16_STORAGE=0 keeps the native narrow storage, so such " "copies are left exactly as the driver delivers them, mirrored words included"; return DriverBugFinding{ - "glCopyImageSubData mirrors 16-bit packed texels at a non-zero array mip level", + "glCopyImageSubData mirrors 16-bit packed texels between differently-laid-out images", DriverBugVerdict::Unfixable, detail}; } diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h index 9b10826f..df5ee6b3 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h @@ -299,31 +299,34 @@ 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. + // Copies one known GL_UNSIGNED_SHORT_5_5_5_1 word out of BOTH mip levels of a GL_RGB5_A1 + // 2D array into plain 2D images with glCopyImageSubData and reads the landed texels back. + // Returns true only when a copy from EITHER level delivers the word's 5_5_5_1 <-> + // 1_5_5_5_REV field-order mirror while both controls below hold. // - // 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 textures reproduce the failing - // shape verbatim - THREE-level chains on both endpoints (30/15/7 x12 for the array, - // 7/3/1 for the plain 2D image), exactly the CTS's FUNCTIONAL_TEST_N_LEVELS = 3 - // allocation - because a 14x14 base's level 1 measured clean on the same driver, so - // every deviation from the measured shape risks landing on the clean side of whatever - // allocation threshold picks the driver's layout, and a probe miss here is not a red - // test: it is the widening silently staying inert with all 18 bodies still failing. + // The affected Mali stores SOME 16-bit packed allocations (RGB565 / RGB5_A1 / RGBA4) with + // their fields packed from the other end of the word, and the mirrored layout is an + // ALLOCATION property, not a mip-level one: on the failing device this probe's 30x30x12 + // three-level array delivers the mirror from level 0 and level 1 alike, which is what + // vetoed the first deployment's "level 0 is the clean control" design. Which allocations + // mirror depends on shape AND context history (a raw standalone context mirrors one- and + // two-level 30x30x12 arrays but not a fresh three-level one; MobileGL's live context + // mirrors the three-level one too), so the probe measures IN SITU - this context is the + // one the application's copies run in - on the CTS's own failing shape (three-level + // chains both endpoints: 30/15/7 x12 array, 7/3/1 plain, FUNCTIONAL_TEST_N_LEVELS = 3). + // Uploads and readbacks decode each image's own layout consistently, so nothing but a raw + // texel-block move can see the divergence - 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 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. + // TWO CONTROLS. The machinery: an identical copy between two SAME-shape plain-2D images, + // which share a layout whatever it is, so it must deliver the word on any driver that can + // run copy_image on these formats - a driver that cannot reaches no verdict instead of + // being reported as this. And the array's own round trip: a direct FBO readback of its + // level 1 must answer the word, or the UPLOAD is what corrupts - a different defect. The + // subjects must also match the mirror PREDICTION, not merely differ from the word - 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 diff --git a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp index 0797bc2d..22fc0d81 100644 --- a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp +++ b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp @@ -344,11 +344,12 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { // 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 - + // KHR-GL4x.copy_image.functional bodies on Mali falsified that: the driver + // stores SOME packed16 allocations with a MIRRORED field order (allocation-scoped, + // shape- and context-dependent; the failing 30x30x12 arrays are mirrored at every + // level), so a raw glCopyImageSubData between a mirrored allocation and a plain + // one 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: diff --git a/MobileGL/MG_Util/Texture/TextureFormatProcessor.h b/MobileGL/MG_Util/Texture/TextureFormatProcessor.h index bb2420fd..67430e6f 100644 --- a/MobileGL/MG_Util/Texture/TextureFormatProcessor.h +++ b/MobileGL/MG_Util/Texture/TextureFormatProcessor.h @@ -47,9 +47,11 @@ namespace MobileGL { // 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 + // storage cannot be trusted as a raw-copy endpoint: some Mali drivers store SOME + // packed16 allocations with a MIRRORED field order (which ones depends on shape and + // context history - measured on a three-level 30x30x12 2D array, every level of it), + // so glCopyImageSubData (a raw texel-block move) between a mirrored allocation and a + // plain one 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