mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 10:03:23 +09:00
394 lines
22 KiB
C++
394 lines
22 KiB
C++
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TriangleScenario.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
|
|
//
|
|
// Scenario - THE SMALLEST THING THAT DRAWS: one VBO, one program, one VAO, a clear, a
|
|
// VBO-backed glDrawArrays and a glReadPixels.
|
|
//
|
|
// This is target B of P5's reduced path (BRIEF-P5 4). It is an ORDINARY GL scenario and runs
|
|
// in every lane; the DirectGLES.Split. entries run the same two cases with
|
|
// MOBILEGL_TRANSPORT=inproc, where the same body is the smallest workload that crosses a real
|
|
// ring. Four things about its shape are decisions rather than defaults, and all four come from
|
|
// the measured verb census (docs/archive/notes/p5/verb-census.md):
|
|
//
|
|
// 1. THE DRAW IS VBO-BACKED AND HAS NO CLIENT-ARRAY INDICES. glDrawArrays against a buffer
|
|
// bound to GL_ARRAY_BUFFER, never glDrawElements with a client pointer. That keeps
|
|
// kDrawHasUserIndices' MGHostSpan out of the first IPC frame entirely - the split filling of
|
|
// a host span is P8 - and it is why table 0 can pin kCapNeedsHostIndexBytes and
|
|
// kCapNeedsHostUboBytes at 0 for the whole of P5.
|
|
//
|
|
// 2. THERE IS NO glFlush, AND ITS ABSENCE IS THE POINT. `Flush` is not a GLFunctionsTable slot
|
|
// at all, and MG_Impl/GLImpl/Exporting/Definitions.cpp:111-112 makes glFlush() and glFinish()
|
|
// LITERALLY EMPTY BODIES - one MGLOG_D and a return. A scenario that called glFlush to order
|
|
// its readback would be ordering nothing and would still pass, which makes the ordering it
|
|
// believes in unfalsifiable. glReadPixels IS the ordering point: it is a blocking readback on
|
|
// both backends today and a SEG_REPLY round trip under split, so the pixels it returns are
|
|
// the pixels the draw produced or the case fails.
|
|
//
|
|
// 3. THE FIRST CASE ENDS WITH EndFrame(), DELIBERATELY. `Present` has ZERO call sites in
|
|
// MG_Impl - it is reached only through EGLImpl.cpp:178 -> BackendObject.cpp:396 - so a
|
|
// scenario that never swaps never touches it, and B would then exercise a STRICT SUBSET of
|
|
// what ClearThenReadPixelsScenario already covers. BRIEF-P5 4.B lists Present(67) among the
|
|
// catalogue rows this target needs, so the frame boundary is here on purpose: B is A's slot
|
|
// set plus DrawArrays, not minus Present.
|
|
//
|
|
// 4. THE SECOND CASE REDRAWS ACROSS A FRAME BOUNDARY WITHOUT REBUILDING ANYTHING. The VBO, the
|
|
// program and the VAO outlive the swap and only the clear colour changes. Under split that
|
|
// is the difference between "the client re-declares every object every frame" (which would
|
|
// pass a single-frame test and be the whole cost of the design) and a steady state; under
|
|
// monolith it is the backend's per-frame retire/aging path, which is exactly where P3a's
|
|
// respecify bug lived.
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "../Harness/HeadlessGL.h"
|
|
#include "../Harness/PipeStatsWindow.h"
|
|
#include "../Harness/ScenarioFixture.h"
|
|
#include "../Harness/SplitLane.h"
|
|
#include "../Harness/SplitRuntimePeek.h"
|
|
#include "../Harness/WireLedgerChecks.h"
|
|
|
|
#ifdef GLAPI
|
|
#undef GLAPI
|
|
#endif
|
|
#define GL_GLEXT_PROTOTYPES
|
|
#include <GL/gl.h>
|
|
#include <GL/glcorearb.h>
|
|
#undef GL_GLEXT_PROTOTYPES
|
|
|
|
namespace MGITest {
|
|
namespace {
|
|
|
|
// #version 330 core, because that is what the retrace lane's
|
|
// MESA_GLSL_VERSION_OVERRIDE pins and what every other scenario in this module that does
|
|
// not need a later feature uses.
|
|
constexpr const char* kVertexSource = R"(#version 330 core
|
|
layout(location = 0) in vec2 aPos;
|
|
layout(location = 1) in vec3 aColor;
|
|
out vec3 vColor;
|
|
void main() {
|
|
vColor = aColor;
|
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
|
}
|
|
)";
|
|
|
|
constexpr const char* kFragmentSource = R"(#version 330 core
|
|
in vec3 vColor;
|
|
out vec4 oColor;
|
|
void main() { oColor = vec4(vColor, 1.0); }
|
|
)";
|
|
|
|
struct Vertex {
|
|
float x, y;
|
|
float r, g, b;
|
|
};
|
|
|
|
// A single triangle with its base at y = -0.8 and its apex at y = +0.8, so the
|
|
// interior box the cases assert on (the middle 10% of the width, a fifth of the way up)
|
|
// is far from every edge and the corner box they assert the CLEAR on is far outside it.
|
|
// Flat green: one colour over the whole primitive means an offender pixel is a real
|
|
// disagreement rather than an interpolation rounding difference between two drivers.
|
|
constexpr Vertex kTriangle[3] = {
|
|
{-0.8f, -0.8f, 0.0f, 1.0f, 0.0f},
|
|
{0.8f, -0.8f, 0.0f, 1.0f, 0.0f},
|
|
{0.0f, 0.8f, 0.0f, 1.0f, 0.0f},
|
|
};
|
|
|
|
class TriangleScenario : public ScenarioTest {
|
|
protected:
|
|
void SetUp() override {
|
|
ScenarioTest::SetUp();
|
|
// Ready() is false both when there is no GPU and when the base SetUp skipped a
|
|
// Split lane that has no client to assert against (ScenarioFixture.h).
|
|
if (!Ready()) return;
|
|
|
|
std::string error;
|
|
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
|
ASSERT_NE(m_program, 0u) << error;
|
|
|
|
glGenVertexArrays(1, &m_vao);
|
|
glBindVertexArray(m_vao);
|
|
glGenBuffers(1, &m_vbo);
|
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
|
glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(sizeof(kTriangle)), kTriangle, GL_STATIC_DRAW);
|
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
|
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
|
reinterpret_cast<void*>(2 * sizeof(float)));
|
|
glEnableVertexAttribArray(0);
|
|
glEnableVertexAttribArray(1);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "building the one VBO and one VAO this scenario has";
|
|
}
|
|
|
|
void TearDown() override {
|
|
if (!Ready() || IsSkipped()) return;
|
|
glBindVertexArray(0);
|
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
|
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
if (m_program != 0) glDeleteProgram(m_program);
|
|
m_vbo = m_vao = m_program = 0;
|
|
}
|
|
|
|
// Clear, draw, read back. No glFlush between the draw and the readback: see the
|
|
// file header, point 2.
|
|
Image ClearThenDrawThenRead(float clearR, float clearG, float clearB) {
|
|
HeadlessGL& gl = Gl();
|
|
BindDefaultFramebuffer();
|
|
glViewport(0, 0, gl.Width(), gl.Height());
|
|
glDisable(GL_SCISSOR_TEST);
|
|
glDisable(GL_DEPTH_TEST);
|
|
ClearTo(clearR, clearG, clearB, 1.0f);
|
|
glUseProgram(m_program);
|
|
glBindVertexArray(m_vao);
|
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
return ReadPixels(gl.Width(), gl.Height());
|
|
}
|
|
|
|
// The box inside the triangle: the middle tenth of the width, a fifth of the way up
|
|
// from the base, which is interior for the vertex set above at any surface size the
|
|
// harness uses.
|
|
void ExpectTriangleInterior(const Image& image, const char* color, const std::string& when) {
|
|
const int w = image.Width();
|
|
const int h = image.Height();
|
|
EXPECT_TRUE(RegionIsMostly(image, (w * 45) / 100, (w * 55) / 100, (h * 20) / 100,
|
|
(h * 30) / 100, color, 0.0, when));
|
|
}
|
|
|
|
// The bottom-left corner, which is below the triangle's base and left of its left
|
|
// edge, so it carries the clear and nothing else.
|
|
void ExpectClearedCorner(const Image& image, const char* color, const std::string& when) {
|
|
const int w = image.Width();
|
|
const int h = image.Height();
|
|
EXPECT_TRUE(RegionIsMostly(image, 0, (w * 5) / 100, 0, (h * 5) / 100, color, 0.0, when));
|
|
}
|
|
|
|
// EXIT GATE E3(e)'s DRIVE LOOP. Ordinary GL through this scenario's own objects -
|
|
// a clear and a VBO-backed draw per iteration, no readback (a readback is a
|
|
// SEG_REPLY round trip per iteration and would make this cost seconds rather than
|
|
// milliseconds) - repeated until the producer has written more bytes into SEG_CMD
|
|
// than the lane's ring holds. Returns the bytes this loop drove.
|
|
//
|
|
// NOTHING HERE TOUCHES THE RING DIRECTLY. The loop's only input is the producer's
|
|
// own head cursor, read through Harness/SplitRuntimePeek, and its only output is
|
|
// GL calls the scenario already makes. R-16: an assertion may not construct the
|
|
// state it observes, and "the workload makes the ring wrap" is a different claim
|
|
// from "a test can make the ring wrap".
|
|
unsigned long long DriveUntilSmallRingOverruns() {
|
|
const unsigned long long before = WireLedger::CmdBytesWritten();
|
|
unsigned long long driven = 0;
|
|
for (unsigned int i = 0; i < WireLedger::kSmallRingLaneMaxIterations; ++i) {
|
|
ClearTo(0.0f, 0.0f, (i & 1u) ? 1.0f : 0.0f, 1.0f);
|
|
glUseProgram(m_program);
|
|
glBindVertexArray(m_vao);
|
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
// Cheap: this is a member read on the encoder, not a wire round trip.
|
|
driven = WireLedger::CmdBytesWritten() - before;
|
|
if (driven > WireLedger::kSmallRingLaneCmdByteTarget) break;
|
|
}
|
|
return driven;
|
|
}
|
|
|
|
unsigned int m_program = 0;
|
|
unsigned int m_vao = 0;
|
|
unsigned int m_vbo = 0;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// The reduced path's target B, in one case: GetCaps (reached by the first glCompileShader of
|
|
// the context, not by any verb - verb-census trap 2), Clear, DrawArrays, ReadPixels, Present.
|
|
TEST_F(TriangleScenario, AVboBackedTriangleReachesReadPixels) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
|
|
const Image image = ClearThenDrawThenRead(0.0f, 0.0f, 1.0f);
|
|
EXPECT_EQ(FirstGLError(), 0u);
|
|
ExpectTriangleInterior(image, "green", "the interior of a VBO-backed glDrawArrays triangle");
|
|
ExpectClearedCorner(image, "blue", "the corner outside the triangle, which carries the clear");
|
|
|
|
// The frame boundary, deliberately (file header, point 3): this is the only thing in the
|
|
// scenario that reaches the Present slot.
|
|
Gl().EndFrame();
|
|
}
|
|
|
|
// Steady state: the same VBO, program and VAO across a swap, with only the clear colour
|
|
// changing. Nothing is re-created, so a client that re-declared its objects every frame and
|
|
// a backend that lost them at the frame boundary both show up here and in no single-frame
|
|
// case.
|
|
TEST_F(TriangleScenario, TheSameVboAndVaoRedrawAcrossAFrameBoundary) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
|
|
const Image first = ClearThenDrawThenRead(0.0f, 0.0f, 1.0f);
|
|
ExpectTriangleInterior(first, "green", "frame 0's triangle");
|
|
ExpectClearedCorner(first, "blue", "frame 0's clear");
|
|
Gl().EndFrame();
|
|
|
|
// Second frame: black clear, nothing else touched.
|
|
const Image second = ClearThenDrawThenRead(0.0f, 0.0f, 0.0f);
|
|
EXPECT_EQ(FirstGLError(), 0u);
|
|
ExpectTriangleInterior(second, "green",
|
|
"frame 1's triangle, drawn from the SAME VBO and VAO with no "
|
|
"re-specification of either");
|
|
ExpectClearedCorner(second, "black", "frame 1's clear, which is the only thing that changed");
|
|
Gl().EndFrame();
|
|
|
|
// ---- the split lanes' two readings of the wire producer's ledger --------------------
|
|
//
|
|
// They are HERE, at the end of the steady-state case, and not in a case of their own,
|
|
// for a reason that is about the gate and not about tidiness: `integration-split` is a
|
|
// NAMED census (19 ran / 2 skipped by design) and a new entry moves it, so the phase
|
|
// would have to re-baseline a number the joint gate just pinned. The measurement wants
|
|
// this workload anyway - BRIEF 8 item 3 names this case - and a reading taken after the
|
|
// case's own pixel assertions is a reading over a run that is known to have been
|
|
// correct.
|
|
//
|
|
// Both are skipped, loudly and by the same predicate every other split-only assertion
|
|
// uses, in the monolith lanes: there is no encoder there, and every field of the
|
|
// ledger reads 0.
|
|
const std::string skip = SplitLane::SkipReasonForSplitOnlyAssertions();
|
|
if (!skip.empty()) {
|
|
RecordProperty("wire_ledger_skip_reason", skip);
|
|
return;
|
|
}
|
|
|
|
// R-10's proof obligation over target B. Published in every split lane, small ring
|
|
// included - the cap moves with MOBILEGL_IPC_RING_MB, so the SmallRing lane is also the
|
|
// arm where a record closest to its cap would show up first.
|
|
WireLedger::ExpectMaxRecordBytesUnderCap(
|
|
"TriangleScenario.TheSameVboAndVaoRedrawAcrossAFrameBoundary");
|
|
|
|
// Exit gate E3(e). Only the small-ring lane drives the overrun: at the default 8 MiB
|
|
// the same loop would take eight times as long to say the same thing, and the point of
|
|
// the lane is that IT is the arm with a ring the workload can fill.
|
|
if (SplitLane::IsSmallRingLane()) {
|
|
const unsigned long long driven = DriveUntilSmallRingOverruns();
|
|
// Ordinary uploads, each fitting by itself, jointly exceed the lane's 1 MiB
|
|
// staging segment. Delay only scheduling between applied and retired: the
|
|
// production allocator, not this test, must observe capacity and wait.
|
|
GLuint pressureBuffer = 0;
|
|
glGenBuffers(1, &pressureBuffer);
|
|
glBindBuffer(GL_COPY_WRITE_BUFFER, pressureBuffer);
|
|
std::vector<unsigned char> upload(768 * 1024, 0x5a);
|
|
glBufferData(GL_COPY_WRITE_BUFFER, upload.size(), nullptr, GL_DYNAMIC_DRAW);
|
|
DelaySplitRetirementForTesting(true);
|
|
glBufferSubData(GL_COPY_WRITE_BUFFER, 0, upload.size(), upload.data());
|
|
upload[0] = 0xa5;
|
|
glBufferSubData(GL_COPY_WRITE_BUFFER, 0, upload.size(), upload.data());
|
|
DelaySplitRetirementForTesting(false);
|
|
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
|
glDeleteBuffers(1, &pressureBuffer);
|
|
Gl().EndFrame();
|
|
WireLedger::ExpectSmallRingWrappedAtLeastOnce(
|
|
"TriangleScenario.TheSameVboAndVaoRedrawAcrossAFrameBoundary", driven);
|
|
}
|
|
}
|
|
|
|
// ===================================================================================
|
|
// P5e (gl), ID-115 / ID-119: THE STRICT LANE'S POSITIVE CONTROL
|
|
// ===================================================================================
|
|
//
|
|
// WHY THE LANE NEEDS ONE AT ALL. Of the seven entries that passed `integration-split` under
|
|
// MOBILEGL_IPC_STRICT_ERRORS=1 before this phase, three were MOBILEGL_TRANSPORT=monolith -
|
|
// where the server never stamps a verb boundary, so the entire strict mechanism is
|
|
// structurally unreachable - two self-skipped, one was a death test and one was a Python
|
|
// check. Not one was a record-carrying split GL scenario. So "116/116 under strict" and
|
|
// "strict was never armed on anything that draws" were indistinguishable from outside, and
|
|
// a gate that cannot tell those apart is not a gate (ID-115).
|
|
//
|
|
// WHAT IT ASSERTS, AND WHY EACH PART IS NECESSARY.
|
|
// `vbs` > 0 - the server stamped at least one verb boundary IN THIS ENTRY. Everything
|
|
// strict checks is downstream of that stamp. This is the arming proof and
|
|
// it is deliberately not rsp: rsp goes to ZERO exactly when the phase
|
|
// SUCCEEDS, so a control built on it would start failing on the day the
|
|
// debt is paid.
|
|
// `draws` > 0 - and the entry really drew, so "armed" is not "armed on a clear".
|
|
// rsp vs draws (ID-119) - the old pin, "rsp = 0 on unbarriered records", was VACUOUS: the
|
|
// unbarriered arm of CountBarrierPull is [[noreturn]] and runs before the
|
|
// counter, so no implementation could ever have made it false. What is
|
|
// checkable is the SHAPE: while the draw path's debt is unretired every draw
|
|
// pulls, so rsp scales with draws; when it retires, rsp goes to 0. The
|
|
// measured device numbers are the same statement - rsp was ~852 (the draw
|
|
// count) under inproc and 0 under monolith. So the assertion is "rsp is 0 or
|
|
// it scales with draws", and the retirement is visible as the transition.
|
|
//
|
|
// THE WINDOW IS READ, NOT INFERRED. `found == false` means the stats channel never reached
|
|
// this process, which is a DIFFERENT failure from "the counter read zero" and is reported as
|
|
// one - otherwise a lane that lost MOBILEGL_PIPE_STATS would report the arming proof as a
|
|
// clean zero. The lane entry that runs this case owns its own MOBILEGL_LOG_FILE_PATH for
|
|
// PipeStatsWindow.h's reason: the library opens it "w", so two entries sharing a path under
|
|
// `ctest -j` race into an empty read that looks exactly like "never emitted".
|
|
TEST_F(TriangleScenario, TheServerStampedAVerbBoundaryOnThisDrawingFrame) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
|
|
const std::string skip = SplitLane::SkipReasonForSplitOnlyAssertions();
|
|
if (!skip.empty()) {
|
|
// The monolith arms' skip twin (G2/G14), and it is LOUD rather than silent because
|
|
// this case's whole subject is a counter that is zero there BY CONSTRUCTION -
|
|
// nothing stamps a verb boundary in a monolith build, and a case that "passed"
|
|
// by reading that zero would be the exact confusion it exists to prevent.
|
|
RecordProperty("strict_arming_skip_reason", skip);
|
|
GTEST_SKIP() << "the server verb stamp exists only under split: " << skip;
|
|
return;
|
|
}
|
|
// ONE LANE OWNS THIS CASE, and it declares itself by name the way MGITEST_PMAP_LANE does.
|
|
// TriangleScenario.* is discovered by several blocks (the plain split lane, the small-ring
|
|
// lane, the monolith lanes), and every one of them gives its entries a private
|
|
// MOBILEGL_LOG_FILE_PATH - so "is there a log path" cannot distinguish the lane that
|
|
// asked for counters from the lanes that merely have somewhere to log. Reading the window
|
|
// in those would assert on a summary line nobody enabled.
|
|
if (!SplitLane::MarkerIsOne("MGITEST_STRICT_ARMING_LANE")) {
|
|
GTEST_SKIP() << "not the strict-arming lane: this case reads a PipeStats window and "
|
|
"only DirectGLES.Split.StrictArming. sets MOBILEGL_PIPE_STATS=1, "
|
|
"MOBILEGL_PIPE_STATS_PERIOD=1 and a log path of its own";
|
|
return;
|
|
}
|
|
if (PipeStatsWindow::LibraryLogPath().empty()) {
|
|
GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the library's "
|
|
"summary line is the only channel this module has for PipeStats";
|
|
return;
|
|
}
|
|
|
|
// Close the setup window, run the workload, close it again: the LAST line then covers
|
|
// the draw and nothing else (PipeStatsWindow.h's "since the previous line").
|
|
Gl().EndFrame();
|
|
const Image image = ClearThenDrawThenRead(0.0f, 0.0f, 1.0f);
|
|
EXPECT_EQ(FirstGLError(), 0u);
|
|
ExpectTriangleInterior(image, "green", "the drawing frame the counters are taken over");
|
|
Gl().EndFrame();
|
|
// Run-ahead returns from Present before its stats window is published.
|
|
// Wait for this emitted Present, so a fast client cannot sample the
|
|
// preceding empty setup frame and falsely report an unarmed server.
|
|
ASSERT_TRUE(WaitForSplitAppliedForTesting(PeekSplitRuntime().emitSeq));
|
|
|
|
const PipeStatsWindow::Window window = PipeStatsWindow::LastFromLaneLog();
|
|
ASSERT_TRUE(window.found)
|
|
<< "the library emitted no `MGPipe stats:` line, so this lane's arming proof is "
|
|
"MISSING rather than zero - check MOBILEGL_PIPE_STATS=1, "
|
|
"MOBILEGL_PIPE_STATS_PERIOD=1 and this entry's private MOBILEGL_LOG_FILE_PATH";
|
|
|
|
const long long draws = PipeStatsWindow::CounterOrAbsent(window, "draws");
|
|
const long long stamps = PipeStatsWindow::CounterOrAbsent(window, "vbs");
|
|
const long long residual = PipeStatsWindow::CounterOrAbsent(window, "rsp");
|
|
RecordProperty("pipe_stats_window", window.line);
|
|
RecordProperty("server_verb_boundaries", std::to_string(stamps));
|
|
RecordProperty("residual_pulls", std::to_string(residual));
|
|
|
|
EXPECT_GT(draws, 0) << "no draw reached the instrumented entry point in the window this "
|
|
"case took, so there is nothing for the arming proof to be about: "
|
|
<< window.line;
|
|
EXPECT_GT(stamps, 0)
|
|
<< "the server stamped NO verb boundary on a drawing split entry. Every strict check "
|
|
"- the poison, rsp, the BARRIER-PULLED verdict - is downstream of that stamp, so "
|
|
"a green strict lane containing only entries like this one would mean the knob "
|
|
"was never armed rather than that the debt was paid (ID-115): "
|
|
<< window.line;
|
|
EXPECT_EQ(residual, 0)
|
|
<< "P5f exit: a drawing frame still reads client residual state: " << window.line;
|
|
}
|
|
|
|
} // namespace MGITest
|