diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp index 353f8dc7..c6f4dbd4 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp @@ -1933,21 +1933,24 @@ namespace MobileGL::MG_Util::SelfTest { // 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, and the driver picks it from - // the texture state IN FORCE WHILE THE LEVELS ARE UPLOADED. Measured raw on the - // affected Mali (same shape, same data, same context): three uploads with NEAREST and - // MAX_LEVEL=2 already set land in the plain layout, while the same three uploads on a - // fresh texture still at the driver defaults - parameters set only afterwards, which - // is exactly the order a freshly minted MobileGL backend texture performs - land in - // the *_REV layout at EVERY level; one- and two-level allocations and params-first - // uploads under a mipmapped MIN_FILTER mirror too. 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 plain, which is why the failures - // looked per-mip-level from the QPA alone. The probe therefore allocates exactly the - // way MobileGL does (uploads first), measures IN SITU, and treats a mirror delivered - // from ANY level as the finding, with machinery controls below instead of a per-level - // "clean" assumption that does not exist. + // WHAT THE DEVICE MEASUREMENTS ACTUALLY SHOWED (round 2): the mirrored field order is + // a property of the WHOLE ALLOCATION, not of a mip level, and WHICH allocations get + // it is a driver heuristic that keys on texture state during the uploads AND on + // context history - and the two interact. Measured on the affected Mali, same shape, + // same data: in a raw standalone context, uploads-on-a-default-state texture mirror + // while params-first-NEAREST allocations stay plain; in MobileGL's live context the + // first deployment's params-first array MIRRORED while a later uploads-first one came + // out PLAIN - the raw ordering rule inverted. One- and two-level allocations and + // params-first uploads under a mipmapped MIN_FILTER also mirrored raw. 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 plain, which + // is why the failures looked per-mip-level from the QPA alone. No single allocation + // recipe is therefore entitled to speak for "the" layout: the probe allocates the + // SAME client-visible texture several ways - the minted-backend order (uploads + // first), the params-first order, and the CTS copy-test shape (MAX_LEVEL bounded, + // MIN_FILTER left at its mipmapped default; copy tests never set filters) - measures + // IN SITU, and a mirror delivered from ANY level of ANY variant is the finding, each + // variant guarded by its own upload round-trip control. constexpr GLsizei kPacked16BaseSize = 30; constexpr GLsizei kPacked16Layers = 12; constexpr GLsizei kPacked16DstSize = 7; @@ -1971,23 +1974,50 @@ namespace MobileGL::MG_Util::SelfTest { // adjacent 5-bit values apart. constexpr Int kPacked16Tolerance = 4; - // 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, ORDER INCLUDED: glTexImage3D per - // level FIRST, on a fresh texture still at the driver defaults, and the parameters - // only afterwards. The order is load-bearing on the affected Mali: uploads-then-params - // (what a freshly minted backend texture gets - the storage sync runs before the - // parameter re-push, see SyncTextureObjectToBackend) lands this allocation in the - // mirrored layout at every level, while the same calls with NEAREST and MAX_LEVEL set - // BEFORE the uploads land it in the plain one - a params-first probe measured "clean" - // in the very context whose params-after textures were mirroring, which is exactly the - // miss round two exists to fix. MAX_LEVEL still ends at the CTS's - // makeTextureComplete(0, 2) shape, which keeps the chain complete - some drivers - // refuse glCopyImageSubData on an incomplete texture. - GLuint MakePacked16ArrayTexture(const GLESFunctionsTable& gl) { + // The allocation recipes the probe tries. Same client-visible texture, same data - + // only the order and the filter state during the uploads move, because those are the + // knobs the driver's layout heuristic was measured keying on (differently in + // different contexts). + enum class Packed16Recipe : Uint8 { + // glTexImage3D per level on a fresh texture at driver defaults, parameters after: + // the order a freshly minted MobileGL backend texture performs (the storage sync + // runs before the parameter re-push, see SyncTextureObjectToBackend). + UploadsFirst, + // NEAREST and MAX_LEVEL set before the uploads: the shape an application that + // configures its sampler state ahead of its data gets. + ParamsFirst, + // MAX_LEVEL bounded but MIN_FILTER left at its mipmapped default: the CTS + // copy-test texture verbatim - copy tests never touch filters, and the chain is + // complete because all three levels exist under MAX_LEVEL = 2. + CtsShape, + }; + constexpr Packed16Recipe kPacked16Recipes[] = {Packed16Recipe::UploadsFirst, + Packed16Recipe::ParamsFirst, + Packed16Recipe::CtsShape}; + const char* Packed16RecipeName(Packed16Recipe recipe) { + switch (recipe) { + case Packed16Recipe::UploadsFirst: return "uploads-first"; + case Packed16Recipe::ParamsFirst: return "params-first"; + case Packed16Recipe::CtsShape: return "cts-shape"; + } + return "?"; + } + + // A three-level GL_RGB5_A1 2D array (30/15/7, twelve layers each, every texel holding + // kPacked16Word) allocated per `recipe`. Every recipe ends mipmap-complete - some + // drivers refuse glCopyImageSubData on an incomplete texture. + GLuint MakePacked16ArrayTexture(const GLESFunctionsTable& gl, Packed16Recipe recipe) { GLuint texture = 0; gl.glGenTextures(1, &texture); if (texture == 0) return 0; gl.glBindTexture(GL_TEXTURE_2D_ARRAY, texture); + if (recipe == Packed16Recipe::ParamsFirst) { + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + if (recipe != Packed16Recipe::UploadsFirst) { + 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( @@ -1995,9 +2025,11 @@ namespace MobileGL::MG_Util::SelfTest { 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.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, kPacked16Levels - 1); + if (recipe == Packed16Recipe::UploadsFirst) { + 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, kPacked16Levels - 1); + } gl.glBindTexture(GL_TEXTURE_2D_ARRAY, 0); return texture; } @@ -2081,6 +2113,56 @@ namespace MobileGL::MG_Util::SelfTest { } return true; } + + // One recipe's whole measurement: allocate, round-trip control, both subject copies. + // Only a mirror that matches the PREDICTION while the variant's own round trip is + // clean counts; everything else is that variant's no-verdict (logged as such). + Bool RunPacked16Recipe(const GLESFunctionsTable& gl, Packed16Recipe recipe) { + Bool mirrored = false; + const GLuint array = MakePacked16ArrayTexture(gl, recipe); + GLubyte direct[4] = {0, 0, 0, 0}; + GLubyte level0[4] = {0, 0, 0, 0}; + GLubyte level1[4] = {0, 0, 0, 0}; + if (array == 0 || !ReadPacked16Texel(gl, array, true, 1, direct)) { + MGLOG_I("[driver-bug] %s probe [%s]: no verdict (the array could not be built or " + "read back)", + kPacked16CopyProbeName, Packed16RecipeName(recipe)); + } else if (!Packed16TexelNear(direct, kPacked16Expected)) { + // The variant'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 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 [%s]: 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)", + kPacked16CopyProbeName, Packed16RecipeName(recipe), 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 [%s]: no verdict (a subject copy could not run)", + kPacked16CopyProbeName, Packed16RecipeName(recipe)); + } else if (Packed16TexelNear(level0, kPacked16Mirrored) || + Packed16TexelNear(level1, kPacked16Mirrored)) { + mirrored = true; + MGLOG_I("[driver-bug] %s probe [%s]: copies delivered level 0 (%d, %d, %d, %d) / " + "level 1 (%d, %d, %d, %d) - the 1_5_5_5_REV re-encoding of the word - " + "THIS ALLOCATION'S FIELD ORDER IS MIRRORED", + kPacked16CopyProbeName, Packed16RecipeName(recipe), level0[0], level0[1], + level0[2], level0[3], level1[0], level1[1], level1[2], level1[3]); + } else if (Packed16TexelNear(level0, kPacked16Expected) && + Packed16TexelNear(level1, kPacked16Expected)) { + MGLOG_I("[driver-bug] %s probe [%s]: both levels copied the word intact", + kPacked16CopyProbeName, Packed16RecipeName(recipe)); + } else { + MGLOG_I("[driver-bug] %s probe [%s]: no verdict (copies read back level 0 " + "(%d, %d, %d, %d) / level 1 (%d, %d, %d, %d), neither the word nor its " + "mirror)", + kPacked16CopyProbeName, Packed16RecipeName(recipe), level0[0], level0[1], + level0[2], level0[3], level1[0], level1[1], level1[2], level1[3]); + } + if (array != 0) gl.glDeleteTextures(1, &array); + return mirrored; + } } // namespace Bool ProbeCopyImageMirrorsPacked16FieldOrder(const GLESFunctionsTable& gl) { @@ -2114,19 +2196,14 @@ namespace MobileGL::MG_Util::SelfTest { Drain(gl); Bool detected = false; - const GLuint array = MakePacked16ArrayTexture(gl); 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)) { + // THE MACHINERY CONTROL: a copy between two 2D images of the same three-level shape + // and allocation discipline. 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 (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); @@ -2135,55 +2212,17 @@ namespace MobileGL::MG_Util::SelfTest { "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 (!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 { - // 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); + // THE SUBJECTS: every allocation recipe of the same array, each with its own + // round-trip control; a mirror from any level of any recipe is the finding. Every + // recipe logs its own verdict either way, so a device run always shows whether + // this probe executed and what each allocation delivered - a silent clean path + // would be indistinguishable from a probe that never ran. + for (const Packed16Recipe recipe : kPacked16Recipes) { + detected = RunPacked16Recipe(gl, recipe) || detected; } } if (flatSource != 0) gl.glDeleteTextures(1, &flatSource); - if (array != 0) gl.glDeleteTextures(1, &array); Restore(gl, saved); return detected; } diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h index df5ee6b3..cce35844 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h @@ -299,34 +299,37 @@ 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 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. + // 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 + // - for SEVERAL ALLOCATION RECIPES of the same array. Returns true only when a copy from + // any level of any recipe delivers the word's 5_5_5_1 <-> 1_5_5_5_REV field-order mirror + // while the controls below hold. // // 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. + // their fields packed from the other end of the word. The mirrored layout is an + // ALLOCATION property, not a mip-level one - the failing device 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 - and WHICH allocations get it is a heuristic keying on texture + // state during the uploads and on context history, measured to INVERT between a raw + // standalone context and MobileGL's live one. No single allocation recipe is therefore + // entitled to speak for the layout: the probe builds the CTS's failing shape (three-level + // chains both endpoints: 30/15/7 x12 array, 7/3/1 plain, FUNCTIONAL_TEST_N_LEVELS = 3) + // with three recipes - uploads-first (the minted-backend-texture order), params-first, + // and the CTS copy-test shape (MAX_LEVEL bounded, MIN_FILTER left mipmapped-default) - + // in situ, in the very context the application's copies run in. 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. // - // TWO CONTROLS. The machinery: an identical copy between two SAME-shape plain-2D images, + // 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. + // being reported as this. And per recipe, 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