mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[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.
This commit is contained in:
@@ -313,6 +313,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||||
|
|
||||||
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
||||||
|
MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp
|
||||||
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
||||||
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
||||||
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
|
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ target_link_libraries(DriverPostIterationRPWitnessTest PRIVATE
|
|||||||
add_executable(
|
add_executable(
|
||||||
DriverBugProbesTest
|
DriverBugProbesTest
|
||||||
DriverBugProbesTest.cpp
|
DriverBugProbesTest.cpp
|
||||||
|
PersistentBufferOrderingProbeTest.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(DriverBugProbesTest PRIVATE
|
target_include_directories(DriverBugProbesTest PRIVATE
|
||||||
|
|||||||
@@ -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 <gtest/gtest.h>
|
||||||
|
#include <MG_Util/SelfTest/PersistentBufferOrderingProbe.h>
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
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<Uint8> staging;
|
||||||
|
};
|
||||||
|
struct Draw { GLuint fbo, buffer; Int channel; Bool late; };
|
||||||
|
std::map<GLuint, Buffer> buffers;
|
||||||
|
std::map<GLuint, GLuint> vaoBuffers;
|
||||||
|
std::map<GLuint, Int> colors;
|
||||||
|
Vector<Draw> draws;
|
||||||
|
std::set<GLuint> live;
|
||||||
|
std::map<GLenum, GLint> 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<GLenum, GLboolean> enabled = {{GL_BLEND, GL_TRUE}, {GL_SCISSOR_TEST, GL_TRUE},
|
||||||
|
{GL_SAMPLE_MASK, GL_TRUE}, {GL_RASTERIZER_DISCARD, GL_TRUE}};
|
||||||
|
std::array<GLint, 4> viewport = {3, 4, 5, 6};
|
||||||
|
std::array<GLfloat, 4> clear = {.25f, .5f, .75f, 0};
|
||||||
|
std::array<GLboolean, 4> 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<const Uint8*>(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<const GLubyte*>("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<Uint8*>(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);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "DriverBugProbes.h"
|
#include "DriverBugProbes.h"
|
||||||
|
#include "PersistentBufferOrderingProbe.h"
|
||||||
|
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include <MG_Util/Debug/Log.h>
|
||||||
@@ -2445,6 +2446,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
DriverBugVerdict::Unfixable, detail};
|
DriverBugVerdict::Unfixable, detail};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Optional<DriverBugFinding> 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.
|
// The table. One row per known driver bug; see the header for how to add a sibling.
|
||||||
using DriverBugProbeFn = Optional<DriverBugFinding> (*)(const GLESFunctionsTable&);
|
using DriverBugProbeFn = Optional<DriverBugFinding> (*)(const GLESFunctionsTable&);
|
||||||
constexpr DriverBugProbeFn kGlesDriverBugProbes[] = {
|
constexpr DriverBugProbeFn kGlesDriverBugProbes[] = {
|
||||||
@@ -2457,6 +2462,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
&ProbeLayeredBlitDestinationBug,
|
&ProbeLayeredBlitDestinationBug,
|
||||||
&ProbeLocatedIoBlockPayloadBug,
|
&ProbeLocatedIoBlockPayloadBug,
|
||||||
&ProbeCopyImagePacked16FieldOrderBug,
|
&ProbeCopyImagePacked16FieldOrderBug,
|
||||||
|
&ProbePersistentBufferOrderingBug,
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
@@ -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 <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <new>
|
||||||
|
|
||||||
|
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<const char*, 3> 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<const char*>(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<GLenum, 10> 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<GLenum, 4> packNames = {
|
||||||
|
GL_PACK_ALIGNMENT, GL_PACK_ROW_LENGTH, GL_PACK_SKIP_PIXELS, GL_PACK_SKIP_ROWS};
|
||||||
|
std::array<GLboolean, enables.size()> enabled{};
|
||||||
|
std::array<GLint, packNames.size()> 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<GLuint, kSlots> 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<Vertex>& vertices, Int channel) {
|
||||||
|
constexpr std::array<Vertex, 6> 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<Uint8>& seed, Int upload, Shape shape) {
|
||||||
|
BufferOrderingSample sample;
|
||||||
|
sample.status = BufferOrderingProbeStatus::Failed;
|
||||||
|
Buffers buffers(gl);
|
||||||
|
Vector<Vertex> payload(kQuads * 6);
|
||||||
|
Vector<Uint8> 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<const void*>(kOffset));
|
||||||
|
gl.glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||||
|
reinterpret_cast<const void*>(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<Uint8*>(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<Uint8> 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<DriverBugFinding> 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
|
||||||
@@ -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 <array>
|
||||||
|
|
||||||
|
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<BufferOrderingUploadMeasurement, 3> 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<DriverBugFinding> DescribePersistentBufferOrderingBug(
|
||||||
|
const PersistentBufferOrderingMeasurement& measurement);
|
||||||
|
} // namespace MobileGL::MG_Util::SelfTest
|
||||||
Reference in New Issue
Block a user