mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eae98581f | ||
|
|
d7655247f7 |
@@ -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
|
||||||
|
|||||||
@@ -1081,6 +1081,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (resource->id != 0 && CanTouchGLNow() &&
|
if (resource->id != 0 && CanTouchGLNow() &&
|
||||||
resource->contextGeneration == g_bufferContextGeneration) {
|
resource->contextGeneration == g_bufferContextGeneration) {
|
||||||
NoteBufferIdDeleted(resource->id);
|
NoteBufferIdDeleted(resource->id);
|
||||||
|
// Frontend VAO bindings survive respecification; force their
|
||||||
|
// backend twins to bind the replacement buffer name.
|
||||||
|
++g_bufferBackendIdGeneration;
|
||||||
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
|
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
|
||||||
resource->id = 0;
|
resource->id = 0;
|
||||||
resource->immutableStorage = false;
|
resource->immutableStorage = false;
|
||||||
@@ -1350,7 +1353,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// See the declaration: re-mints of a live resource's driver id. Written only on
|
// See the declaration: re-mints of a live resource's driver id. Written only on
|
||||||
// the context thread (both re-mint sites run there), read only by the VAO sync.
|
// the context thread (all re-mint sites run there), read only by the VAO sync.
|
||||||
Uint64 g_bufferBackendIdGeneration = 0;
|
Uint64 g_bufferBackendIdGeneration = 0;
|
||||||
|
|
||||||
void RegisterBufferBackendOps() {
|
void RegisterBufferBackendOps() {
|
||||||
|
|||||||
@@ -102,6 +102,12 @@ void main() { word = 0xC0FFEEu; }
|
|||||||
// The NULL-data definition is the adoption point (and Minecraft's
|
// The NULL-data definition is the adoption point (and Minecraft's
|
||||||
// arena-creation idiom).
|
// arena-creation idiom).
|
||||||
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
|
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
ConfigureVertexArray(m_vao);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigureVertexArray(GLuint vao) {
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||||
reinterpret_cast<void*>(kVertexOffset));
|
reinterpret_cast<void*>(kVertexOffset));
|
||||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||||
@@ -173,12 +179,12 @@ void main() { word = 0xC0FFEEu; }
|
|||||||
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawQuad() {
|
void DrawQuad(GLuint vao = 0) {
|
||||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
glUseProgram(m_program);
|
glUseProgram(m_program);
|
||||||
glBindVertexArray(m_vao);
|
glBindVertexArray(vao != 0 ? vao : m_vao);
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +228,97 @@ void main() { word = 0xC0FFEEu; }
|
|||||||
EXPECT_LT(px[0], 50) << "the draw still shows the previous frame's bytes";
|
EXPECT_LT(px[0], 50) << "the draw still shows the previous frame's bytes";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Respecifying a frontend buffer preserves its VAO attachments even when the
|
||||||
|
// backend replaces the adopted store's GL name. Keep every attribute binding
|
||||||
|
// unchanged so a stale backend VAO cannot be repaired by a frontend rebind.
|
||||||
|
TEST_F(LargeArenaAdoptionScenario, RespecifiedVertexArenaKeepsVaoBindings) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
UploadQuad(1.f, 0.f, 0.f);
|
||||||
|
DrawQuad();
|
||||||
|
ASSERT_GT(CenterPixel()[0], 200);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
GLuint otherVao = 0;
|
||||||
|
glGenVertexArrays(1, &otherVao);
|
||||||
|
ConfigureVertexArray(otherVao);
|
||||||
|
DrawQuad(otherVao);
|
||||||
|
EXPECT_GT(CenterPixel()[0], 200);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
constexpr std::array<GLsizeiptr, 3> sizes = {
|
||||||
|
kArenaBytes, kArenaBytes + 4096, kArenaBytes - 4096,
|
||||||
|
};
|
||||||
|
constexpr std::array<std::array<float, 3>, 3> colors = {{
|
||||||
|
{0.f, 1.f, 0.f}, {0.f, 0.f, 1.f}, {1.f, 0.f, 0.f},
|
||||||
|
}};
|
||||||
|
for (std::size_t i = 0; i < sizes.size(); ++i) {
|
||||||
|
SCOPED_TRACE(sizes[i]);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizes[i], nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
UploadQuad(colors[i][0], colors[i][1], colors[i][2]);
|
||||||
|
// The unbound VAO can retain the deleted store; the current VAO's
|
||||||
|
// attachments can be cleared by deletion. Both must be repaired.
|
||||||
|
for (GLuint vao : {m_vao, otherVao}) {
|
||||||
|
SCOPED_TRACE(vao);
|
||||||
|
DrawQuad(vao);
|
||||||
|
const auto px = CenterPixel();
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
for (std::size_t channel = 0; channel < 3; ++channel) {
|
||||||
|
if (colors[i][channel] != 0.f) {
|
||||||
|
EXPECT_GT(px[channel], 200) << "VAO did not fetch the replacement vertex store";
|
||||||
|
} else {
|
||||||
|
EXPECT_LT(px[channel], 50) << "VAO still fetched the previous vertex store";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glDeleteVertexArrays(1, &otherVao);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LargeArenaAdoptionScenario, RespecifiedIndexArenaKeepsVaoBinding) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
auto vertices = QuadVertices(1.f, 0.f, 0.f);
|
||||||
|
const auto green = QuadVertices(0.f, 1.f, 0.f);
|
||||||
|
vertices.insert(vertices.end(), green.begin(), green.end());
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||||
|
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||||
|
|
||||||
|
GLuint indices = 0;
|
||||||
|
glGenBuffers(1, &indices);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indices);
|
||||||
|
// Redefine through COPY_WRITE_BUFFER so the element binding slot never
|
||||||
|
// changes. The small final store also exercises returning to shadow storage.
|
||||||
|
glBindBuffer(GL_COPY_WRITE_BUFFER, indices);
|
||||||
|
constexpr std::array<GLsizeiptr, 4> sizes = {
|
||||||
|
kArenaBytes, kArenaBytes, kArenaBytes + 4096, 4096,
|
||||||
|
};
|
||||||
|
for (std::size_t i = 0; i < sizes.size(); ++i) {
|
||||||
|
SCOPED_TRACE(sizes[i]);
|
||||||
|
const GLuint first = (i % 2) == 0 ? 0u : 6u;
|
||||||
|
const std::array<GLuint, 6> elements = {
|
||||||
|
first, first + 1, first + 2, first + 3, first + 4, first + 5,
|
||||||
|
};
|
||||||
|
glBufferData(GL_COPY_WRITE_BUFFER, sizes[i], nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
glBufferSubData(GL_COPY_WRITE_BUFFER, 0, sizeof(elements), elements.data());
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
|
||||||
|
const auto px = CenterPixel();
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_GT(px[first == 0 ? 0 : 1], 200) << "VAO did not fetch the replacement index store";
|
||||||
|
EXPECT_LT(px[first == 0 ? 1 : 0], 50) << "VAO still fetched the previous index store";
|
||||||
|
}
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||||
|
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
||||||
|
glDeleteBuffers(1, &indices);
|
||||||
|
}
|
||||||
|
|
||||||
// The shadow IS the mapping: a readback straight after a CPU write must hand
|
// The shadow IS the mapping: a readback straight after a CPU write must hand
|
||||||
// back exactly those bytes.
|
// back exactly those bytes.
|
||||||
TEST_F(LargeArenaAdoptionScenario, ReadbackSeesTheLatestCpuWrite) {
|
TEST_F(LargeArenaAdoptionScenario, ReadbackSeesTheLatestCpuWrite) {
|
||||||
|
|||||||
@@ -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