diff --git a/MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp index 71062159..7e6e50ab 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp @@ -20,10 +20,12 @@ // 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 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. // // The repair under test is the packed16 storage widening // (PixelFormatNormalizeOptionBit::WidenPacked16Norm): where the POST probe @@ -48,6 +50,7 @@ // 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 @@ -160,8 +163,9 @@ namespace MGITest { return usable; } - // The CTS's own mutable shape: glTexImage3D per level, filter NEAREST, the chain - // clamped to the two levels it has. + // The CTS's own mutable shape: glTexImage3D per level, filter NEAREST, THREE levels + // (30/15/7) with the chain clamped to them. Level 2 carries its own fill so nothing + // below can pass by reading a level that was never written. GLuint MakeArrayTexture(const PackedFormatCase& format, const std::vector& level0, const std::vector& level1) { GLuint texture = 0; @@ -170,15 +174,21 @@ namespace MGITest { 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); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 2); 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()); + const int level2Size = kLevel1Size / 2; + const auto level2 = MakeWords(format.transferType, level2Size * level2Size * kLayers, 211); + glTexImage3D(GL_TEXTURE_2D_ARRAY, 2, static_cast(format.internalFormat), level2Size, + level2Size, kLayers, 0, format.transferFormat, format.transferType, level2.data()); glBindTexture(GL_TEXTURE_2D_ARRAY, 0); return texture; } + // Three levels (7/3/1) like the CTS's plain endpoints; `texels` is level 0, the one + // every assertion reads. GLuint MakeFlatTexture(const PackedFormatCase& format, const std::vector& texels) { GLuint texture = 0; glGenTextures(1, &texture); @@ -186,9 +196,15 @@ namespace MGITest { 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); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2); glTexImage2D(GL_TEXTURE_2D, 0, static_cast(format.internalFormat), kFlatSize, kFlatSize, 0, format.transferFormat, format.transferType, texels.data()); + for (int level = 1; level <= 2; ++level) { + const int size = std::max(kFlatSize >> level, 1); + const auto fill = MakeWords(format.transferType, size * size, 97 + level); + glTexImage2D(GL_TEXTURE_2D, level, static_cast(format.internalFormat), size, size, 0, + format.transferFormat, format.transferType, fill.data()); + } glBindTexture(GL_TEXTURE_2D, 0); return texture; } diff --git a/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp b/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp index c5332379..a112a298 100644 --- a/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp +++ b/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp @@ -110,11 +110,15 @@ namespace { // 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. + // 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. @@ -156,6 +160,16 @@ namespace { // One word per level is all the packed16 probe distinguishes: it uploads a uniform // fill and reads one texel. std::map, GLushort> packedTexelWords; + // 2D-array texture id -> its allocation shape, as glTexImage3D built it. What the + // packed16 mirror is gated on: level-0 dimensions plus a mask of the levels actually + // allocated, so only the measured three-level 30x30x12 chain diverges. + struct FakeArrayAllocation { + GLsizei width = 0; + GLsizei height = 0; + GLsizei layers = 0; + unsigned levelMask = 0; + }; + std::map packedArrayAllocations; // framebuffer id -> the plain 2D texture glFramebufferTexture2D attached. std::map framebuffer2DAttachment; GLuint boundArrayTexture = 0; @@ -460,6 +474,7 @@ namespace { ++it; } } + g_fake.packedArrayAllocations.erase(textures[i]); } }; funcs.glTexParameteri = [](GLenum target, GLenum pname, GLint param) { @@ -479,27 +494,41 @@ namespace { 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) { + // Records the allocation shape the mirror below is gated on, and the uploaded word. + 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)); - const bool mirrored = g_fake.packed16EveryArrayLevelMirrored || - (g_fake.packed16ArrayMipFieldOrderMirrored && level >= 1); - g_fake.packedTexelWords[{g_fake.boundArrayTexture, level}] = - mirrored ? MirrorPacked5551(word) : word; + g_fake.packedTexelWords[{g_fake.boundArrayTexture, level}] = word; + auto& allocation = g_fake.packedArrayAllocations[g_fake.boundArrayTexture]; + if (level == 0) { + allocation.width = width; + allocation.height = height; + allocation.layers = depth; + } + if (level >= 0 && level < 8) allocation.levelMask |= 1u << level; }; - // A raw texel-block move: the PHYSICAL word travels, whichever layout wrote it. + // 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 + // 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, 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; + GLushort word = source->second; + const auto allocation = g_fake.packedArrayAllocations.find(srcName); + const bool measuredShape = allocation != g_fake.packedArrayAllocations.end() && + 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))) { + word = MirrorPacked5551(word); + } + g_fake.packedTexelWords[{dstName, dstLevel}] = word; }; funcs.glTexSubImage2D = [](GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const void*) {}; diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp index 03479881..f4e3802c 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -1932,10 +1933,17 @@ namespace MobileGL::MG_Util::SelfTest { // 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. + // 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. constexpr GLsizei kPacked16BaseSize = 30; constexpr GLsizei kPacked16Layers = 12; constexpr GLsizei kPacked16DstSize = 7; + constexpr GLint kPacked16Levels = 3; // 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 @@ -1955,9 +1963,10 @@ namespace MobileGL::MG_Util::SelfTest { // 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 + // A three-level GL_RGB5_A1 2D array (30/15/7, twelve layers each) allocated the way + // MobileGL's own mutable-texture path allocates one (glTexImage3D per level), with + // every texel of every level holding kPacked16Word. MAX_LEVEL is clamped to the + // CTS's makeTextureComplete(0, 2) shape, which also keeps the chain complete - some // drivers refuse glCopyImageSubData on an incomplete texture. GLuint MakePacked16ArrayTexture(const GLESFunctionsTable& gl) { GLuint texture = 0; @@ -1966,8 +1975,8 @@ namespace MobileGL::MG_Util::SelfTest { 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) { + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, kPacked16Levels - 1); + for (GLint level = 0; level < kPacked16Levels; ++level) { const GLsizei size = kPacked16BaseSize >> level; const Vector words( static_cast(size) * static_cast(size) * kPacked16Layers, kPacked16Word); @@ -1978,9 +1987,9 @@ namespace MobileGL::MG_Util::SelfTest { 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. + // 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) { GLuint texture = 0; gl.glGenTextures(1, &texture); @@ -1988,10 +1997,13 @@ namespace MobileGL::MG_Util::SelfTest { 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.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}); + gl.glTexImage2D(GL_TEXTURE_2D, level, GL_RGB5_A1, size, size, 0, GL_RGBA, + GL_UNSIGNED_SHORT_5_5_5_1, fill.data()); + } gl.glBindTexture(GL_TEXTURE_2D, 0); return texture; } @@ -2103,6 +2115,13 @@ namespace MobileGL::MG_Util::SelfTest { 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); } if (array != 0) gl.glDeleteTextures(1, &array); Restore(gl, saved); @@ -2285,21 +2304,40 @@ namespace MobileGL::MG_Util::SelfTest { 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 mitigation is a knob (MOBILEGL_WIDEN_PACKED16_STORAGE), so the row consults + // it: under ForceOff - the documented negative control - the corruption is + // replayed verbatim, and a hardcoded "Fixed" would be exactly the kind of + // reassurance this file exists to refuse. Auto and ForceOn both widen once this + // probe has fired. Should POST ever run before env parsing, the field still holds + // its Auto default - which is also what the widening itself consults, so the row + // and the behaviour cannot disagree. + 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 " "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"}; + "this probe's 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 " + "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; override with " + "MOBILEGL_WIDEN_PACKED16_STORAGE"; + return DriverBugFinding{ + "glCopyImageSubData mirrors 16-bit packed texels at a non-zero array mip level", + 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", + DriverBugVerdict::Unfixable, detail}; } // The table. One row per known driver bug; see the header for how to add a sibling. diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h index 62e74fe4..9b10826f 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h @@ -309,9 +309,13 @@ namespace MobileGL::MG_Util::SelfTest { // 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). + // 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 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