From 9eae98581fa5544ea3cca2566582d41a6b2c23ac Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 7 Sep 2026 00:44:08 -0400 Subject: [PATCH] [Test] (SelfTest, POST): probe upload/draw ordering in persistently mapped vertex arenas - native GLES controls distinguish mapped destination corruption from staging and synchronization failures Add the persistent buffer ordering probe to the GLES POST's known-driver-bug inventory. Queue updates and draws into independent FBOs before reading them back, covering SubData and copies from both coherent persistent and ordinary staging buffers. Retry fresh mapped allocations for intermittent corruption. Require passing never-mapped and fully serialized controls before reporting a finding. Include finish-before, map-then-unmap and barrier diagnostics, preserve caller GL state, and treat setup, allocation and GL errors as inconclusive. This adds detection and reporting only; no rendering workaround is enabled. Validation: all 46 DriverBugProbesTest tests pass, including 11 new ordering, control, collector and cleanup cases. The Android API 26 / NDK 27 native probe detects all three upload paths on Mali-G1-Ultra r54p1 with clean controls and no GL errors. llvmpipe reports no finding after 240 mapped FBO checks per upload path. --- CMakeLists.txt | 1 + MobileGL/MG_Test/SelfTest/CMakeLists.txt | 1 + .../PersistentBufferOrderingProbeTest.cpp | 326 ++++++++++++++ MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp | 6 + .../PersistentBufferOrderingProbe.cpp | 413 ++++++++++++++++++ .../SelfTest/PersistentBufferOrderingProbe.h | 66 +++ 6 files changed, 813 insertions(+) create mode 100644 MobileGL/MG_Test/SelfTest/PersistentBufferOrderingProbeTest.cpp create mode 100644 MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp create mode 100644 MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.h diff --git a/CMakeLists.txt b/CMakeLists.txt index eb574dc6..8d5c7805 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -313,6 +313,7 @@ set(SOURCE_FILES MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp + MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp MobileGL/MG_Util/SelfTest/DriverPost.cpp MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp diff --git a/MobileGL/MG_Test/SelfTest/CMakeLists.txt b/MobileGL/MG_Test/SelfTest/CMakeLists.txt index cc596468..768e9179 100644 --- a/MobileGL/MG_Test/SelfTest/CMakeLists.txt +++ b/MobileGL/MG_Test/SelfTest/CMakeLists.txt @@ -18,6 +18,7 @@ target_link_libraries(DriverPostIterationRPWitnessTest PRIVATE add_executable( DriverBugProbesTest DriverBugProbesTest.cpp + PersistentBufferOrderingProbeTest.cpp ) target_include_directories(DriverBugProbesTest PRIVATE diff --git a/MobileGL/MG_Test/SelfTest/PersistentBufferOrderingProbeTest.cpp b/MobileGL/MG_Test/SelfTest/PersistentBufferOrderingProbeTest.cpp new file mode 100644 index 00000000..de414840 --- /dev/null +++ b/MobileGL/MG_Test/SelfTest/PersistentBufferOrderingProbeTest.cpp @@ -0,0 +1,326 @@ +// MobileGL - MobileGL/MG_Test/SelfTest/PersistentBufferOrderingProbeTest.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 + +#include +#include + +#include +#include + +using namespace MobileGL; +using namespace MobileGL::MG_Util::SelfTest; + +namespace { + // Deferred vertex fetch, not canned ReadPixels answers: a broken mapped destination + // reads its current bytes at Finish instead of the bytes at DrawArrays. ReadPixels also + // drains these jobs, so inserting an early readback into the probe hides the bug here too. + struct FakeDriver { + struct Buffer { + Bool mapped = false; + Bool arena = false; + Bool copied = false; + Int channel = 0; + Vector staging; + }; + struct Draw { GLuint fbo, buffer; Int channel; Bool late; }; + std::map buffers; + std::map vaoBuffers; + std::map colors; + Vector draws; + std::set live; + std::map state = { + {GL_CURRENT_PROGRAM, 1}, {GL_VERTEX_ARRAY_BINDING, 2}, {GL_ARRAY_BUFFER, 3}, + {GL_COPY_READ_BUFFER, 4}, {GL_COPY_WRITE_BUFFER, 5}, + {GL_DRAW_FRAMEBUFFER_BINDING, 6}, {GL_READ_FRAMEBUFFER_BINDING, 7}, + {GL_TEXTURE_BINDING_2D, 8}, {GL_PIXEL_PACK_BUFFER, 9}, + {GL_PACK_ALIGNMENT, 8}, {GL_PACK_ROW_LENGTH, 31}, + {GL_PACK_SKIP_PIXELS, 4}, {GL_PACK_SKIP_ROWS, 5}}; + std::map enabled = {{GL_BLEND, GL_TRUE}, {GL_SCISSOR_TEST, GL_TRUE}, + {GL_SAMPLE_MASK, GL_TRUE}, {GL_RASTERIZER_DISCARD, GL_TRUE}}; + std::array viewport = {3, 4, 5, 6}; + std::array clear = {.25f, .5f, .75f, 0}; + std::array mask = {GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE}; + GLuint next = 100; + GLenum error = GL_NO_ERROR; + Bool extension = true, corruptSubData = false, corruptCopy = false; + Bool corruptUnmapped = false, corruptSerialized = false; + Bool failMap = false, failAllocation = false, failFramebuffer = false; + Int failReadbackAt = 0, readbacks = 0; + Uint arenaAllocations = 0; + Uint8 pointerSentinel = 0; + + GLuint Create() { live.insert(next); return next++; } + void Generate(GLsizei count, GLuint* ids) { for (Int i = 0; i < count; ++i) ids[i] = Create(); } + void Delete(GLsizei count, const GLuint* ids) { + for (Int i = 0; i < count; ++i) { + live.erase(ids[i]); + buffers.erase(ids[i]); + } + } + GLint Get(GLenum name) const { + const auto found = state.find(name); + return found == state.end() ? 0 : found->second; + } + static Int Channel(const void* data) { + GLfloat color[3]; + std::memcpy(color, static_cast(data) + 2 * sizeof(GLfloat), sizeof(color)); + return color[0] > .5f ? 0 : color[1] > .5f ? 1 : 2; + } + void Finish() { + for (const auto& draw : draws) { + const auto& buffer = buffers.at(draw.buffer); + colors[draw.fbo] = draw.late ? buffer.channel : draw.channel; + if (buffer.mapped && corruptSerialized) colors[draw.fbo] = (draw.channel + 1) % 3; + } + draws.clear(); + } + } driver; + + MG_External::GLESFunctionsTable Table() { + MG_External::GLESFunctionsTable gl{}; + gl.glGetIntegerv = [](GLenum name, GLint* out) { + if (name == GL_MAJOR_VERSION) *out = 3; + else if (name == GL_MINOR_VERSION) *out = 2; + else if (name == GL_NUM_EXTENSIONS) *out = driver.extension ? 1 : 0; + else if (name == GL_VIEWPORT) std::copy(driver.viewport.begin(), driver.viewport.end(), out); + else if (name == GL_ARRAY_BUFFER_BINDING) *out = driver.Get(GL_ARRAY_BUFFER); + else if (name == GL_PIXEL_PACK_BUFFER_BINDING) *out = driver.Get(GL_PIXEL_PACK_BUFFER); + else *out = driver.Get(name); + }; + gl.glGetBooleanv = [](GLenum, GLboolean* out) { std::copy(driver.mask.begin(), driver.mask.end(), out); }; + gl.glGetFloatv = [](GLenum, GLfloat* out) { std::copy(driver.clear.begin(), driver.clear.end(), out); }; + gl.glGetStringi = [](GLenum, GLuint) { return reinterpret_cast("GL_EXT_buffer_storage"); }; + gl.glGetError = []() { return std::exchange(driver.error, GL_NO_ERROR); }; + gl.glIsEnabled = [](GLenum name) -> GLboolean { return driver.enabled[name]; }; + gl.glEnable = [](GLenum name) { driver.enabled[name] = GL_TRUE; }; + gl.glDisable = [](GLenum name) { driver.enabled[name] = GL_FALSE; }; + gl.glCreateShader = [](GLenum) { return driver.Create(); }; + gl.glShaderSource = [](GLuint, GLsizei, const GLchar* const*, const GLint*) {}; + gl.glCompileShader = [](GLuint) {}; + gl.glGetShaderiv = [](GLuint, GLenum, GLint* out) { *out = GL_TRUE; }; + gl.glGetShaderInfoLog = [](GLuint, GLsizei, GLsizei*, GLchar* out) { *out = 0; }; + gl.glDeleteShader = [](GLuint id) { driver.Delete(1, &id); }; + gl.glCreateProgram = []() { return driver.Create(); }; + gl.glAttachShader = [](GLuint, GLuint) {}; + gl.glLinkProgram = [](GLuint) {}; + gl.glGetProgramiv = [](GLuint, GLenum, GLint* out) { *out = GL_TRUE; }; + gl.glGetProgramInfoLog = gl.glGetShaderInfoLog; + gl.glDeleteProgram = gl.glDeleteShader; + gl.glUseProgram = [](GLuint id) { driver.state[GL_CURRENT_PROGRAM] = id; }; + gl.glGenBuffers = [](GLsizei count, GLuint* ids) { driver.Generate(count, ids); }; + gl.glBindBuffer = [](GLenum target, GLuint id) { driver.state[target] = id; }; + gl.glBufferStorageEXT = [](GLenum target, GLsizeiptr size, const void*, GLbitfield) { + auto& buffer = driver.buffers[driver.Get(target)]; + buffer.arena = size >= 16 * 1024 * 1024; + if (buffer.arena) ++driver.arenaAllocations; + if (driver.failAllocation) driver.error = GL_OUT_OF_MEMORY; + if (!buffer.arena) buffer.staging.resize(size); + }; + gl.glBufferData = [](GLenum target, GLsizeiptr size, const void*, GLenum) { + driver.buffers[driver.Get(target)].staging.resize(size); + }; + gl.glMapBufferRange = [](GLenum target, GLintptr offset, GLsizeiptr, GLbitfield) -> void* { + if (driver.failMap) return nullptr; + auto& buffer = driver.buffers[driver.Get(target)]; + buffer.mapped = true; + return buffer.arena ? &driver.pointerSentinel : buffer.staging.data() + offset; + }; + gl.glUnmapBuffer = [](GLenum) -> GLboolean { return GL_TRUE; }; // Preserve allocation history. + gl.glBufferSubData = [](GLenum target, GLintptr offset, GLsizeiptr size, const void* data) { + auto& buffer = driver.buffers[driver.Get(target)]; + if (buffer.arena) { + buffer.channel = FakeDriver::Channel(data); + buffer.copied = false; + } else std::memcpy(buffer.staging.data() + offset, data, size); + }; + gl.glCopyBufferSubData = [](GLenum read, GLenum write, GLintptr offset, GLintptr, GLsizeiptr) { + auto& source = driver.buffers[driver.Get(read)]; + auto& dest = driver.buffers[driver.Get(write)]; + dest.channel = FakeDriver::Channel(source.staging.data() + offset); + dest.copied = true; + }; + gl.glDeleteBuffers = [](GLsizei count, const GLuint* ids) { driver.Delete(count, ids); }; + gl.glGenVertexArrays = gl.glGenBuffers; + gl.glBindVertexArray = [](GLuint id) { driver.state[GL_VERTEX_ARRAY_BINDING] = id; }; + gl.glVertexAttribPointer = [](GLuint, GLint, GLenum, GLboolean, GLsizei, const void*) { + driver.vaoBuffers[driver.Get(GL_VERTEX_ARRAY_BINDING)] = driver.Get(GL_ARRAY_BUFFER); + }; + gl.glEnableVertexAttribArray = [](GLuint) {}; + gl.glDeleteVertexArrays = gl.glDeleteBuffers; + gl.glGenTextures = gl.glGenBuffers; + gl.glBindTexture = [](GLenum, GLuint id) { driver.state[GL_TEXTURE_BINDING_2D] = id; }; + gl.glTexStorage2D = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei) {}; + gl.glDeleteTextures = gl.glDeleteBuffers; + gl.glGenFramebuffers = gl.glGenBuffers; + gl.glBindFramebuffer = [](GLenum target, GLuint id) { + if (target != GL_READ_FRAMEBUFFER) driver.state[GL_DRAW_FRAMEBUFFER_BINDING] = id; + if (target != GL_DRAW_FRAMEBUFFER) driver.state[GL_READ_FRAMEBUFFER_BINDING] = id; + }; + gl.glFramebufferTexture2D = [](GLenum, GLenum, GLenum, GLuint, GLint) {}; + gl.glCheckFramebufferStatus = [](GLenum) -> GLenum { + return driver.failFramebuffer ? GL_FRAMEBUFFER_UNSUPPORTED : GL_FRAMEBUFFER_COMPLETE; + }; + gl.glDeleteFramebuffers = gl.glDeleteBuffers; + gl.glViewport = [](GLint x, GLint y, GLsizei w, GLsizei h) { driver.viewport = {x, y, w, h}; }; + gl.glColorMask = [](GLboolean r, GLboolean g, GLboolean b, GLboolean a) { driver.mask = {r, g, b, a}; }; + gl.glClearColor = [](GLfloat r, GLfloat g, GLfloat b, GLfloat a) { driver.clear = {r, g, b, a}; }; + gl.glClear = [](GLbitfield) {}; + gl.glDrawArrays = [](GLenum, GLint, GLsizei) { + const GLuint id = driver.vaoBuffers.at(driver.Get(GL_VERTEX_ARRAY_BINDING)); + const auto& buffer = driver.buffers.at(id); + const Bool late = buffer.mapped ? (buffer.copied ? driver.corruptCopy : driver.corruptSubData) + : driver.corruptUnmapped; + driver.draws.push_back({GLuint(driver.Get(GL_DRAW_FRAMEBUFFER_BINDING)), id, buffer.channel, late}); + }; + gl.glFinish = []() { driver.Finish(); }; + gl.glMemoryBarrier = [](GLbitfield) {}; + gl.glPixelStorei = [](GLenum name, GLint value) { driver.state[name] = value; }; + gl.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum, GLenum, void* data) { + driver.Finish(); // Models the implicit wait that must NOT occur between subject draws. + if (++driver.readbacks == driver.failReadbackAt) { + driver.error = GL_INVALID_OPERATION; + return; + } + EXPECT_EQ(driver.Get(GL_PIXEL_PACK_BUFFER), 0); + EXPECT_EQ(driver.Get(GL_PACK_ROW_LENGTH), 0); + const Int channel = driver.colors.at(driver.Get(GL_READ_FRAMEBUFFER_BINDING)); + auto* pixels = static_cast(data); + for (Int i = 0; i < width * height; ++i) + for (Int c = 0; c < 4; ++c) pixels[4 * i + c] = c == channel || c == 3 ? 255 : 0; + }; + return gl; + } + + class PersistentBufferOrderingProbeTest : public ::testing::Test { + protected: + void SetUp() override { driver = FakeDriver{}; } + void TearDown() override { EXPECT_TRUE(driver.live.empty()); EXPECT_TRUE(driver.draws.empty()); } + }; +} + +TEST_F(PersistentBufferOrderingProbeTest, RequiresExtensionAndCompleteDispatchBeforeAllocating) { + auto gl = Table(); + driver.extension = false; + EXPECT_FALSE(ProbePersistentBufferUpdateOrdering(gl).supported); + driver.extension = true; + gl.glCopyBufferSubData = nullptr; + EXPECT_FALSE(ProbePersistentBufferUpdateOrdering(gl).supported); + EXPECT_EQ(driver.arenaAllocations, 0u); +} + +TEST_F(PersistentBufferOrderingProbeTest, OrderedDriverPassesAllUploadsAndRestoresCallerState) { + const auto saved = driver; + const auto measurement = ProbePersistentBufferUpdateOrdering(Table()); + ASSERT_TRUE(measurement.supported); + for (const auto& row : measurement.uploads) { + EXPECT_TRUE(row.unmapped.Passed()); + EXPECT_TRUE(row.mapped.Passed()); + EXPECT_EQ(row.mapped.frames, 240u); // Three fresh attempts before a negative result. + EXPECT_EQ(row.finishBoth.status, BufferOrderingProbeStatus::NotRun); + } + EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement)); + EXPECT_EQ(driver.state, saved.state); + EXPECT_EQ(driver.viewport, saved.viewport); + EXPECT_EQ(driver.clear, saved.clear); + EXPECT_EQ(driver.mask, saved.mask); + for (const auto& [cap, value] : driver.enabled) { + const auto found = saved.enabled.find(cap); + EXPECT_EQ(value, found == saved.enabled.end() ? GL_FALSE : found->second); + } +} + +TEST_F(PersistentBufferOrderingProbeTest, DeferredMappedSubDataFetchIsDetectedWithPassingControls) { + driver.corruptSubData = true; + const auto measurement = ProbePersistentBufferUpdateOrdering(Table()); + EXPECT_TRUE(measurement.uploads[0].Detected()); + EXPECT_TRUE(measurement.uploads[0].finishBefore.Passed()); + EXPECT_GT(measurement.uploads[0].mapThenUnmap.badFrames, 0u); + EXPECT_GT(measurement.uploads[0].barrierBefore.badFrames, 0u); + EXPECT_FALSE(measurement.uploads[1].Detected()); + EXPECT_FALSE(measurement.uploads[2].Detected()); + const auto finding = DescribePersistentBufferOrderingBug(measurement); + ASSERT_TRUE(finding); + EXPECT_EQ(finding->verdict, DriverBugVerdict::Unfixable); + EXPECT_NE(finding->detail.find("SubData:"), String::npos); + EXPECT_NE(finding->detail.find("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION=1"), String::npos); +} + +TEST_F(PersistentBufferOrderingProbeTest, DeferredCopyFetchIsDetectedWithBothStagingSources) { + driver.corruptCopy = true; + const auto measurement = ProbePersistentBufferUpdateOrdering(Table()); + EXPECT_FALSE(measurement.uploads[0].Detected()); + EXPECT_TRUE(measurement.uploads[1].Detected()); + EXPECT_TRUE(measurement.uploads[2].Detected()); +} + +TEST_F(PersistentBufferOrderingProbeTest, PostCollectorIncludesTheMeasuredFinding) { + driver.corruptSubData = true; + const auto findings = CollectGlesKnownDriverBugs(Table()); + const auto found = std::find_if(findings.begin(), findings.end(), [](const auto& finding) { + return finding.name == "Persistent-mapped vertex buffers lose upload/draw ordering"; + }); + ASSERT_NE(found, findings.end()); + EXPECT_NE(found->detail.find("never-mapped 0/80"), String::npos); + EXPECT_EQ(found->verdict, DriverBugVerdict::Unfixable); +} + +TEST_F(PersistentBufferOrderingProbeTest, CorruptNeverMappedControlCannotAccusePersistentMapping) { + driver.corruptSubData = driver.corruptCopy = driver.corruptUnmapped = true; + const auto measurement = ProbePersistentBufferUpdateOrdering(Table()); + for (const auto& row : measurement.uploads) { + EXPECT_GT(row.unmapped.badFrames, 0u); + EXPECT_EQ(row.mapped.status, BufferOrderingProbeStatus::NotRun); + } + EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement)); +} + +TEST_F(PersistentBufferOrderingProbeTest, CorruptSerializedControlCannotConfirmOrderingDefect) { + driver.corruptSubData = driver.corruptCopy = driver.corruptSerialized = true; + const auto measurement = ProbePersistentBufferUpdateOrdering(Table()); + for (const auto& row : measurement.uploads) EXPECT_GT(row.finishBoth.badFrames, 0u); + EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement)); +} + +TEST_F(PersistentBufferOrderingProbeTest, FailedMappingIsInconclusiveAndReleasesResources) { + driver.failMap = true; + const auto measurement = ProbePersistentBufferUpdateOrdering(Table()); + EXPECT_EQ(measurement.uploads[0].mapped.status, BufferOrderingProbeStatus::Failed); + EXPECT_EQ(measurement.uploads[1].unmapped.status, BufferOrderingProbeStatus::Failed); + EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement)); +} + +TEST_F(PersistentBufferOrderingProbeTest, AllocationFailureIsInconclusiveAndRestoresBindings) { + driver.failAllocation = true; + const auto saved = driver.state; + const auto measurement = ProbePersistentBufferUpdateOrdering(Table()); + for (const auto& row : measurement.uploads) { + EXPECT_EQ(row.unmapped.status, BufferOrderingProbeStatus::Failed); + EXPECT_EQ(row.unmapped.error, GLenum(GL_OUT_OF_MEMORY)); + } + EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement)); + EXPECT_EQ(driver.state, saved); +} + +TEST_F(PersistentBufferOrderingProbeTest, ReadbackErrorAfterAMismatchDoesNotProduceAFinding) { + driver.corruptSubData = true; + driver.failReadbackAt = 82; // Eighty clean control readbacks, then one corrupt subject FBO. + const auto measurement = ProbePersistentBufferUpdateOrdering(Table()); + EXPECT_GT(measurement.uploads[0].mapped.badFrames, 0u); + EXPECT_EQ(measurement.uploads[0].mapped.status, BufferOrderingProbeStatus::Failed); + EXPECT_EQ(measurement.uploads[0].mapped.error, GLenum(GL_INVALID_OPERATION)); + EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement)); +} + +TEST_F(PersistentBufferOrderingProbeTest, IncompleteFramebufferIsInconclusiveAndRestoresBindings) { + driver.failFramebuffer = true; + const auto saved = driver.state; + const auto measurement = ProbePersistentBufferUpdateOrdering(Table()); + EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement)); + EXPECT_EQ(driver.arenaAllocations, 0u); + EXPECT_EQ(driver.state, saved); +} diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp index 590f408b..8b4b01c5 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "DriverBugProbes.h" +#include "PersistentBufferOrderingProbe.h" #include #include @@ -2445,6 +2446,10 @@ namespace MobileGL::MG_Util::SelfTest { DriverBugVerdict::Unfixable, detail}; } + Optional ProbePersistentBufferOrderingBug(const GLESFunctionsTable& gl) { + return DescribePersistentBufferOrderingBug(ProbePersistentBufferUpdateOrdering(gl)); + } + // 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[] = { @@ -2457,6 +2462,7 @@ namespace MobileGL::MG_Util::SelfTest { &ProbeLayeredBlitDestinationBug, &ProbeLocatedIoBlockPayloadBug, &ProbeCopyImagePacked16FieldOrderBug, + &ProbePersistentBufferOrderingBug, }; } // namespace diff --git a/MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp b/MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp new file mode 100644 index 00000000..0b62fc57 --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp @@ -0,0 +1,413 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.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 + +#include "PersistentBufferOrderingProbe.h" +#include + +#include +#include +#include +#include + +namespace MobileGL::MG_Util::SelfTest { + namespace { + using MG_External::GLESFunctionsTable; + constexpr GLbitfield kPersistent = 0x0040; + constexpr GLbitfield kCoherent = 0x0080; + constexpr GLbitfield kDynamicStorage = 0x0100; + constexpr GLbitfield kMapFlags = GL_MAP_WRITE_BIT | kPersistent | kCoherent; + constexpr GLsizeiptr kArenaSize = 128 * 1024 * 1024; + constexpr GLintptr kOffset = 96 * 1024 * 1024 + 28; + constexpr GLsizei kSide = 128; + constexpr GLsizei kSlots = 8; + constexpr Int kBatches = 10; + constexpr Int kDraws = 32; + constexpr GLsizei kQuads = 64 * 32; + constexpr Int kAttempts = 3; + constexpr std::array kUploadNames = {"SubData", "Copy/persistent staging", + "Copy/SubData staging"}; + enum class Shape { Unmapped, Mapped, FinishBefore, FinishBoth, MapThenUnmap, BarrierBefore }; + struct Vertex { GLfloat x, y, r, g, b; }; + constexpr GLsizeiptr kPayloadSize = kQuads * 6 * sizeof(Vertex); + static_assert(kOffset + kPayloadSize <= kArenaSize); + + void DrainErrors(const GLESFunctionsTable& gl) { + for (Int i = 0; i < 32 && gl.glGetError() != GL_NO_ERROR; ++i) {} + } + + Bool CanProbe(const GLESFunctionsTable& gl) { + if (!(gl.glGetIntegerv && gl.glGetBooleanv && gl.glGetFloatv && gl.glGetError && + gl.glGetStringi && gl.glIsEnabled && gl.glEnable && gl.glDisable && + gl.glCreateShader && gl.glShaderSource && gl.glCompileShader && gl.glGetShaderiv && + gl.glGetShaderInfoLog && gl.glDeleteShader && gl.glCreateProgram && gl.glAttachShader && + gl.glLinkProgram && gl.glGetProgramiv && gl.glGetProgramInfoLog && gl.glDeleteProgram && + gl.glUseProgram && gl.glGenBuffers && gl.glBindBuffer && gl.glBufferStorageEXT && + gl.glMapBufferRange && gl.glUnmapBuffer && gl.glBufferData && gl.glBufferSubData && + gl.glCopyBufferSubData && gl.glDeleteBuffers && gl.glGenVertexArrays && + gl.glBindVertexArray && gl.glVertexAttribPointer && gl.glEnableVertexAttribArray && + gl.glDeleteVertexArrays && gl.glGenTextures && gl.glBindTexture && gl.glTexStorage2D && + gl.glDeleteTextures && gl.glGenFramebuffers && gl.glBindFramebuffer && + gl.glFramebufferTexture2D && gl.glCheckFramebufferStatus && gl.glDeleteFramebuffers && + gl.glViewport && gl.glColorMask && gl.glClearColor && gl.glClear && gl.glDrawArrays && + gl.glFinish && gl.glMemoryBarrier && gl.glPixelStorei && gl.glReadPixels)) return false; + DrainErrors(gl); + GLint major = 0, minor = 0, count = 0; + gl.glGetIntegerv(GL_MAJOR_VERSION, &major); + gl.glGetIntegerv(GL_MINOR_VERSION, &minor); + gl.glGetIntegerv(GL_NUM_EXTENSIONS, &count); + if (gl.glGetError() != GL_NO_ERROR || major < 3 || (major == 3 && minor < 1)) return false; + for (GLint i = 0; i < count; ++i) { + const auto* extension = gl.glGetStringi(GL_EXTENSIONS, i); + if (extension && std::strcmp(reinterpret_cast(extension), + "GL_EXT_buffer_storage") == 0) return true; + } + return false; + } + + // This probe touches no images/SSBO bindings. Keep its scope independent from the + // other POST probes, including pack state and the caller's currently active texture unit. + struct StateScope { + const GLESFunctionsTable& gl; + GLint program = 0, vao = 0, array = 0, copyRead = 0, copyWrite = 0; + GLint drawFbo = 0, readFbo = 0, texture = 0, packBuffer = 0; + GLint viewport[4]{}; + GLfloat clear[4]{}; + GLboolean colorMask[4]{}; + static constexpr std::array enables = { + GL_BLEND, GL_DEPTH_TEST, GL_STENCIL_TEST, GL_CULL_FACE, GL_SCISSOR_TEST, + GL_RASTERIZER_DISCARD, GL_DITHER, GL_SAMPLE_ALPHA_TO_COVERAGE, + GL_SAMPLE_COVERAGE, GL_SAMPLE_MASK}; + static constexpr std::array packNames = { + GL_PACK_ALIGNMENT, GL_PACK_ROW_LENGTH, GL_PACK_SKIP_PIXELS, GL_PACK_SKIP_ROWS}; + std::array enabled{}; + std::array pack{}; + + explicit StateScope(const GLESFunctionsTable& api) : gl(api) { + gl.glGetIntegerv(GL_CURRENT_PROGRAM, &program); + gl.glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &vao); + gl.glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &array); + gl.glGetIntegerv(GL_COPY_READ_BUFFER_BINDING, ©Read); + gl.glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, ©Write); + gl.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawFbo); + gl.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFbo); + gl.glGetIntegerv(GL_TEXTURE_BINDING_2D, &texture); + gl.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &packBuffer); + gl.glGetIntegerv(GL_VIEWPORT, viewport); + gl.glGetFloatv(GL_COLOR_CLEAR_VALUE, clear); + gl.glGetBooleanv(GL_COLOR_WRITEMASK, colorMask); + for (SizeT i = 0; i < enables.size(); ++i) enabled[i] = gl.glIsEnabled(enables[i]); + for (SizeT i = 0; i < packNames.size(); ++i) gl.glGetIntegerv(packNames[i], &pack[i]); + } + void Prepare() { + for (auto cap : enables) gl.glDisable(cap); + gl.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + gl.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + for (auto name : packNames) gl.glPixelStorei(name, name == GL_PACK_ALIGNMENT ? 1 : 0); + gl.glViewport(0, 0, kSide, kSide); + gl.glClearColor(0, 0, 0, 1); + } + ~StateScope() { + gl.glUseProgram(program); + gl.glBindVertexArray(vao); + gl.glBindBuffer(GL_ARRAY_BUFFER, array); + gl.glBindBuffer(GL_COPY_READ_BUFFER, copyRead); + gl.glBindBuffer(GL_COPY_WRITE_BUFFER, copyWrite); + gl.glBindBuffer(GL_PIXEL_PACK_BUFFER, packBuffer); + gl.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, drawFbo); + gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, readFbo); + gl.glBindTexture(GL_TEXTURE_2D, texture); + gl.glViewport(viewport[0], viewport[1], viewport[2], viewport[3]); + gl.glClearColor(clear[0], clear[1], clear[2], clear[3]); + gl.glColorMask(colorMask[0], colorMask[1], colorMask[2], colorMask[3]); + for (SizeT i = 0; i < enables.size(); ++i) { + if (enabled[i]) gl.glEnable(enables[i]); else gl.glDisable(enables[i]); + } + for (SizeT i = 0; i < packNames.size(); ++i) gl.glPixelStorei(packNames[i], pack[i]); + } + }; + + struct Resources { + const GLESFunctionsTable& gl; + GLuint program = 0, vao = 0; + std::array fbos{}, textures{}; + explicit Resources(const GLESFunctionsTable& api) : gl(api) {} + ~Resources() { + gl.glDeleteFramebuffers(kSlots, fbos.data()); + gl.glDeleteTextures(kSlots, textures.data()); + gl.glDeleteVertexArrays(1, &vao); + if (program) gl.glDeleteProgram(program); + } + GLuint Compile(GLenum type, const char* source) { + GLuint shader = gl.glCreateShader(type); + if (!shader) return 0; + gl.glShaderSource(shader, 1, &source, nullptr); + gl.glCompileShader(shader); + GLint compiled = 0; + gl.glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + if (!compiled) { + char log[512]{}; + gl.glGetShaderInfoLog(shader, sizeof(log), nullptr, log); + MGLOG_I("[driver-bug] persistent buffer ordering: shader failed: %s", log); + gl.glDeleteShader(shader); + return 0; + } + return shader; + } + Bool Setup() { + const GLuint vs = Compile(GL_VERTEX_SHADER, + "#version 310 es\nlayout(location=0) in vec2 pos; layout(location=1) in vec3 color;\n" + "out highp vec3 vColor; void main(){gl_Position=vec4(pos,0,1);vColor=color;}\n"); + const GLuint fs = Compile(GL_FRAGMENT_SHADER, + "#version 310 es\nprecision highp float; in highp vec3 vColor;\n" + "layout(location=0) out vec4 outColor; void main(){outColor=vec4(vColor,1);}\n"); + if (vs && fs) { + program = gl.glCreateProgram(); + if (program) { + gl.glAttachShader(program, vs); + gl.glAttachShader(program, fs); + gl.glLinkProgram(program); + } + } + if (vs) gl.glDeleteShader(vs); + if (fs) gl.glDeleteShader(fs); + if (!program) return false; + GLint linked = 0; + gl.glGetProgramiv(program, GL_LINK_STATUS, &linked); + if (!linked) { + char log[512]{}; + gl.glGetProgramInfoLog(program, sizeof(log), nullptr, log); + MGLOG_I("[driver-bug] persistent buffer ordering: link failed: %s", log); + return false; + } + gl.glUseProgram(program); + gl.glGenVertexArrays(1, &vao); + gl.glBindVertexArray(vao); + gl.glGenFramebuffers(kSlots, fbos.data()); + gl.glGenTextures(kSlots, textures.data()); + for (Int i = 0; i < kSlots; ++i) { + if (!vao || !fbos[i] || !textures[i]) return false; + gl.glBindTexture(GL_TEXTURE_2D, textures[i]); + gl.glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSide, kSide); + gl.glBindFramebuffer(GL_FRAMEBUFFER, fbos[i]); + gl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + textures[i], 0); + if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) return false; + } + return gl.glGetError() == GL_NO_ERROR; + } + }; + + struct Buffers { + const GLESFunctionsTable& gl; + GLuint arena = 0, staging = 0; + explicit Buffers(const GLESFunctionsTable& api) : gl(api) {} + ~Buffers() { + // All normal batches finish before cleanup; also retire a partially queued + // batch on an error path before destroying a mapped staging source. + gl.glFinish(); + gl.glDeleteBuffers(1, &arena); + gl.glDeleteBuffers(1, &staging); + } + }; + + void FillVertices(Vector& vertices, Int channel) { + constexpr std::array quad = {{{-1,-1,0,0,0}, {1,-1,0,0,0}, {1,1,0,0,0}, + {-1,-1,0,0,0}, {1,1,0,0,0}, {-1,1,0,0,0}}}; + for (SizeT k = 0; k < vertices.size(); ++k) { + auto& v = vertices[k]; + v = quad[k % 6]; + const SizeT q = k / 6; + v.x = v.x / 64.f - 1.f + (2 * (q % 64) + 1) / 64.f; + v.y = v.y / 32.f - 1.f + (2 * (q / 64) + 1) / 32.f; + v.r = channel == 0 ? 1.f : 0.f; + v.g = channel == 1 ? 1.f : 0.f; + v.b = channel == 2 ? 1.f : 0.f; + } + } + + BufferOrderingSample Run(const GLESFunctionsTable& gl, const Resources& resources, + const Vector& seed, Int upload, Shape shape) { + BufferOrderingSample sample; + sample.status = BufferOrderingProbeStatus::Failed; + Buffers buffers(gl); + Vector payload(kQuads * 6); + Vector pixels(kSide * kSide * 4); + DrainErrors(gl); + do { + gl.glGenBuffers(1, &buffers.arena); + if (!buffers.arena) break; + gl.glBindBuffer(GL_ARRAY_BUFFER, buffers.arena); + gl.glBufferStorageEXT(GL_ARRAY_BUFFER, kArenaSize, seed.data(), kMapFlags | kDynamicStorage); + sample.error = gl.glGetError(); + if (sample.error != GL_NO_ERROR) break; + if (shape != Shape::Unmapped) { + // Deliberately never dereference the destination pointer. All destination + // writes below are ordered GL commands, with no client mapping accesses. + if (!gl.glMapBufferRange(GL_ARRAY_BUFFER, 0, kArenaSize, kMapFlags)) break; + if (shape == Shape::MapThenUnmap && !gl.glUnmapBuffer(GL_ARRAY_BUFFER)) break; + } + gl.glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), + reinterpret_cast(kOffset)); + gl.glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), + reinterpret_cast(kOffset + 2 * sizeof(GLfloat))); + gl.glEnableVertexAttribArray(0); + gl.glEnableVertexAttribArray(1); + void* sourceMap = nullptr; + if (upload != 0) { + gl.glGenBuffers(1, &buffers.staging); + if (!buffers.staging) break; + gl.glBindBuffer(GL_COPY_READ_BUFFER, buffers.staging); + if (upload == 1) { + gl.glBufferStorageEXT(GL_COPY_READ_BUFFER, kSlots * kPayloadSize, nullptr, kMapFlags); + sourceMap = gl.glMapBufferRange(GL_COPY_READ_BUFFER, 0, kSlots * kPayloadSize, kMapFlags); + if (!sourceMap) break; + } else { + gl.glBufferData(GL_COPY_READ_BUFFER, kSlots * kPayloadSize, nullptr, GL_STREAM_DRAW); + } + gl.glBindBuffer(GL_COPY_WRITE_BUFFER, buffers.arena); + } + sample.error = gl.glGetError(); + if (sample.error != GL_NO_ERROR) break; + for (Int batch = 0; batch < kBatches; ++batch) { + for (Int slot = 0; slot < kSlots; ++slot) { + FillVertices(payload, (batch * kSlots + slot) % 3); + if (shape == Shape::FinishBefore || shape == Shape::FinishBoth) gl.glFinish(); + if (shape == Shape::BarrierBefore) gl.glMemoryBarrier(GL_ALL_BARRIER_BITS); + if (upload == 0) { + gl.glBufferSubData(GL_ARRAY_BUFFER, kOffset, kPayloadSize, payload.data()); + } else { + // No slot is reused until the entire batch has finished on the GPU. + if (upload == 1) { + std::memcpy(static_cast(sourceMap) + slot * kPayloadSize, + payload.data(), kPayloadSize); + } else { + gl.glBufferSubData(GL_COPY_READ_BUFFER, slot * kPayloadSize, + kPayloadSize, payload.data()); + } + gl.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, + slot * kPayloadSize, kOffset, kPayloadSize); + } + if (shape == Shape::FinishBoth) gl.glFinish(); + gl.glBindFramebuffer(GL_FRAMEBUFFER, resources.fbos[slot]); + gl.glClear(GL_COLOR_BUFFER_BIT); + for (Int draw = 0; draw < kDraws; ++draw) gl.glDrawArrays(GL_TRIANGLES, 0, kQuads * 6); + } + // No readback/Finish between subject update/draw pairs. Early readback + // would hide precisely the old-reader/new-writer overlap being tested. + gl.glFinish(); + sample.error = gl.glGetError(); + if (sample.error != GL_NO_ERROR) break; + for (Int slot = 0; slot < kSlots; ++slot) { + gl.glBindFramebuffer(GL_FRAMEBUFFER, resources.fbos[slot]); + gl.glReadPixels(0, 0, kSide, kSide, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + sample.error = gl.glGetError(); + if (sample.error != GL_NO_ERROR) break; + const Int channel = (batch * kSlots + slot) % 3; + Uint bad = 0; + for (Int pixel = 0; pixel < kSide * kSide; ++pixel) { + for (Int c = 0; c < 3; ++c) { + const Int expected = c == channel ? 255 : 0; + if (std::abs(Int(pixels[pixel * 4 + c]) - expected) > 8) ++bad; + } + } + ++sample.frames; + if (bad != 0) ++sample.badFrames; + sample.badComponents += bad; + } + if (sample.error != GL_NO_ERROR) break; + } + if (sample.error == GL_NO_ERROR && sample.frames == kBatches * kSlots) + sample.status = BufferOrderingProbeStatus::Complete; + } while (false); + if (sample.error == GL_NO_ERROR) sample.error = gl.glGetError(); + return sample; + } + + String Describe(const BufferOrderingSample& sample) { + if (sample.status == BufferOrderingProbeStatus::NotRun) return "not run"; + if (sample.status == BufferOrderingProbeStatus::Failed) + return format("inconclusive (GL error 0x{:x}, {} readbacks)", sample.error, sample.frames); + return format("{}/{} bad FBOs ({} components)", sample.badFrames, sample.frames, sample.badComponents); + } + + String DescribeUpload(const BufferOrderingUploadMeasurement& row, Int upload) { + return format("{}: mapped {}, never-mapped {}, Finish-before {}, Finish-both {}, " + "map-then-unmap {}, barrier-before {}", kUploadNames[upload], Describe(row.mapped), + Describe(row.unmapped), Describe(row.finishBefore), Describe(row.finishBoth), + Describe(row.mapThenUnmap), Describe(row.barrierBefore)); + } + } // namespace + + PersistentBufferOrderingMeasurement ProbePersistentBufferUpdateOrdering(const GLESFunctionsTable& gl) try { + PersistentBufferOrderingMeasurement measurement; + if (!CanProbe(gl)) return measurement; + measurement.supported = true; + StateScope state(gl); + state.Prepare(); + Resources resources(gl); + if (gl.glGetError() != GL_NO_ERROR || !resources.Setup()) { + for (auto& row : measurement.uploads) row.unmapped.status = BufferOrderingProbeStatus::Failed; + MGLOG_I("[driver-bug] persistent buffer ordering: setup failed; inconclusive"); + return measurement; + } + Vector seed(kArenaSize, 0); + for (Int upload = 0; upload < Int(measurement.uploads.size()); ++upload) { + auto& row = measurement.uploads[upload]; + row.unmapped = Run(gl, resources, seed, upload, Shape::Unmapped); + if (row.unmapped.Passed()) { + // A single allocation can miss on Mali. Stop once a mismatch is measured, + // otherwise retry with fresh storage rather than treating one pass as proof. + for (Int attempt = 0; attempt < kAttempts; ++attempt) { + const auto sample = Run(gl, resources, seed, upload, Shape::Mapped); + row.mapped.status = sample.status; + row.mapped.error = sample.error; + row.mapped.frames += sample.frames; + row.mapped.badFrames += sample.badFrames; + row.mapped.badComponents += sample.badComponents; + if (sample.status != BufferOrderingProbeStatus::Complete || sample.badFrames) break; + } + if (row.mapped.status == BufferOrderingProbeStatus::Complete && row.mapped.badFrames) { + row.finishBoth = Run(gl, resources, seed, upload, Shape::FinishBoth); + row.finishBefore = Run(gl, resources, seed, upload, Shape::FinishBefore); + row.mapThenUnmap = Run(gl, resources, seed, upload, Shape::MapThenUnmap); + row.barrierBefore = Run(gl, resources, seed, upload, Shape::BarrierBefore); + } + } + MGLOG_I("[driver-bug] persistent buffer ordering: %s; %s", DescribeUpload(row, upload).c_str(), + row.Detected() ? "detected" : "not detected or inconclusive"); + } + return measurement; + } catch (const std::bad_alloc&) { + // The CPU initializer is arena-sized too. An allocation failure must not discard + // the rest of the POST report or turn a partially sampled case into a finding. + MGLOG_I("[driver-bug] persistent buffer ordering: host allocation failed; inconclusive"); + PersistentBufferOrderingMeasurement measurement; + measurement.supported = true; + for (auto& row : measurement.uploads) row.unmapped.status = BufferOrderingProbeStatus::Failed; + return measurement; + } + + Optional DescribePersistentBufferOrderingBug( + const PersistentBufferOrderingMeasurement& measurement) { + String detail; + for (Int upload = 0; upload < Int(measurement.uploads.size()); ++upload) { + if (!measurement.uploads[upload].Detected()) continue; + if (!detail.empty()) detail += "; "; + detail += DescribeUpload(measurement.uploads[upload], upload); + } + if (detail.empty()) return std::nullopt; + detail += ". A 128 MiB immutable vertex destination was mapped WRITE|PERSISTENT|COHERENT, " + "but never accessed through its client pointer. Queued uploads/draws corrupt vertex data; " + "identical never-mapped and Finish-before-and-after controls pass. " + "This POST does not enable a workaround. MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION=1 " + "avoids automatic arena adoption; explicit application mappings remain separate. " + "FBO counts describe this bounded stress probe, not application flicker frequency."; + return DriverBugFinding{"Persistent-mapped vertex buffers lose upload/draw ordering", + DriverBugVerdict::Unfixable, Move(detail)}; + } +} // namespace MobileGL::MG_Util::SelfTest diff --git a/MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.h b/MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.h new file mode 100644 index 00000000..b4d457e6 --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.h @@ -0,0 +1,66 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.h +// 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 + +#pragma once +#include "DriverBugProbes.h" + +#include + +namespace MobileGL::MG_Util::SelfTest { + enum class BufferOrderingProbeStatus : Uint8 { NotRun, Complete, Failed }; + + struct BufferOrderingSample { + BufferOrderingProbeStatus status = BufferOrderingProbeStatus::NotRun; + Uint frames = 0; // Independent FBO readbacks, not draw calls or application frames. + Uint badFrames = 0; + Uint badComponents = 0; + GLenum error = GL_NO_ERROR; + + Bool Passed() const { return status == BufferOrderingProbeStatus::Complete && badFrames == 0; } + }; + + struct BufferOrderingUploadMeasurement { + BufferOrderingSample unmapped; + BufferOrderingSample mapped; + BufferOrderingSample finishBefore; + BufferOrderingSample finishBoth; + BufferOrderingSample mapThenUnmap; + BufferOrderingSample barrierBefore; + + Bool Detected() const { + return unmapped.Passed() && finishBoth.Passed() && + mapped.status == BufferOrderingProbeStatus::Complete && mapped.badFrames != 0; + } + }; + + struct PersistentBufferOrderingMeasurement { + Bool supported = false; + // SubData; CopyBufferSubData from coherent persistent staging; CopyBufferSubData + // from ordinary SubData staging. Each has its OWN otherwise-identical controls. + std::array uploads; + }; + + // POST-only: native GLES calls, no MobileGL buffers, renderer-name rules or config changes. + // The Mali r54p1 finding: updating an immutable vertex arena that has been persistently + // mapped can corrupt queued draws even when the application never accesses that mapping. + // Queue eight update/draw pairs into separate FBOs BEFORE any Finish/readback, then check + // every pixel of both old and new draws. Staging slots never overlap while in flight. + // + // Each upload runs a never-mapped control with identical storage flags. Try up to three + // fresh mapped allocations to catch intermittent failures. On corruption, measure explicit + // waits, map-then-unmap and a barrier as diagnostics. Only a passing never-mapped AND + // Finish-before-and-after control permits a finding. Setup/GL failures are inconclusive. + // Explicit allocations are one 128 MiB arena, its initializer, and small staging/FBOs; + // allocations, batches and draws are bounded. Every touched GL state is restored. + PersistentBufferOrderingMeasurement ProbePersistentBufferUpdateOrdering( + const MG_External::GLESFunctionsTable& gl); + + // Used by the POST collector. A report never labels an inconclusive sample as a bug. + Optional DescribePersistentBufferOrderingBug( + const PersistentBufferOrderingMeasurement& measurement); +} // namespace MobileGL::MG_Util::SelfTest