[Test] (Pipe): assert a regrown adopted store costs one map-persistent round trip per storage definition, not one per draw

This commit is contained in:
2026-09-08 06:02:36 -04:00
parent 0cb46fe2bd
commit 6e9943b36e
5 changed files with 851 additions and 0 deletions
+142
View File
@@ -131,6 +131,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/PoisonOmissionScenario.cpp
Scenarios/HandleRecycleScenario.cpp
Scenarios/CsoContentAddressingScenario.cpp
Scenarios/ResourceSubsystemControlScenario.cpp
)
target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -649,6 +650,20 @@ gtest_discover_tests(MobileGLIntegrationTest
# emulation never runs - the ambient registration would be exercising the un-stripped path
# twice and calling it coverage. With the variable set, the blocks really are emitted with no
# location and the assertion is about the spelling the device gets.
# RESOURCE_LOCK, on this lane and on the three below it, and it is a bug fix rather than a
# precaution. Each of these lanes gives a WHOLE SCENARIO one MOBILEGL_LOG_FILE_PATH, and one case
# in each reads that log back to prove the pinned emulation actually armed. The library opens the
# log fopen(path, "w"), so every process in the lane TRUNCATES it - and under `ctest -j` a sibling
# case of the same lane can truncate it while the arming case is reading, which reads back as "the
# log carries no arming line" and fails a healthy lane. Measured on this tree: three runs of the
# full integration-gpu label at -j 8 produced 4 failures, 0 and 2, always one of these arming
# cases, never the same set twice.
#
# The rule stated above the verify block - a case that reads the log needs a lane whose filter
# selects it alone - would fix it by re-filtering, but that would RENAME the arming entries, and
# an existing ctest name may never disappear (gate G14). A ctest RESOURCE_LOCK named after the log
# is the same guarantee without touching a name: ctest never runs two entries holding the same lock
# at once, so the only processes that can truncate a lane's log are ones nobody is reading it for.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.UnlocatedIoBlocks."
TEST_FILTER "UnlocatedIoBlockScenario.*"
@@ -656,6 +671,7 @@ gtest_discover_tests(MobileGLIntegrationTest
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
RESOURCE_LOCK unlocated-io-blocks.log
ENVIRONMENT "${MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT}"
)
@@ -772,6 +788,7 @@ gtest_discover_tests(MobileGLIntegrationTest
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
RESOURCE_LOCK primgen-query-reroute.log
ENVIRONMENT "${MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT}"
)
@@ -807,6 +824,7 @@ gtest_discover_tests(MobileGLIntegrationTest
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
RESOURCE_LOCK point-size-demotion-gles.log
ENVIRONMENT "${MGL_ITEST_GLES_POINT_SIZE_DEMOTION_ENVIRONMENT}"
)
@@ -817,6 +835,7 @@ gtest_discover_tests(MobileGLIntegrationTest
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
RESOURCE_LOCK point-size-demotion-vulkan.log
ENVIRONMENT "${MGL_ITEST_VULKAN_POINT_SIZE_DEMOTION_ENVIRONMENT}"
)
@@ -1066,6 +1085,129 @@ gtest_discover_tests(MobileGLIntegrationTest
ENVIRONMENT "${MGL_ITEST_VULKAN_CSO_OFF_ENVIRONMENT}"
)
# --- G12: the P3a subsystem A/B, and G10's map-persistent-roundtrips lanes ------------
#
# THE MASKS ARE THE TWO PHASE CONSTANTS, not hand-picked bits: 0x1ff is
# kMGPipeSubsystemsMigratedAtP3a (the push build's default) and 0x7f is
# kMGPipeSubsystemsMigratedAtP2, which is exactly that default with bits 7 (resources) and 8
# (vertex input) cleared. MGPipe.h:79 keeps each phase's constant alive as the next phase's A/B
# control for this reason, and a lane that spelled its own bit pattern would stop being the shape
# that ships the first time the default moved.
#
# DirectGLES only. P3a migrates Espryt's buffer and VAO paths; Magma's buffer path is P7 and
# registers no MGPipeResourceOps, so a DirectVulkan arm would be measuring the client emitter
# against a backend nobody asked to change.
#
# Each reading entry gets a LOG PATH OF ITS OWN, and its ctest entry selects ONE case: the library
# opens the log fopen(path, "w") - every process in a lane truncates it - and these cases READ it.
# Two readers in one lane race under `ctest -j`, and the failure looks exactly like "the counter
# was never emitted". Same rule as the CSO lanes above and the verify arming lane below.
#
# MOBILEGL_PIPE_STATS_PERIOD=1 makes one summary line per eglSwapBuffers, which is what lets a
# workload be bracketed by two swaps and read back as a window covering exactly itself.
#
# Registered in EVERY build, including the pull build where none of the counters exists, so that
# `ctest -L integration-gpu` stays name-for-name identical between pull and push (gate G2). In a
# pull build MGITEST_PIPE_PUSH_BUILD is absent and every one of these cases skips saying so; a
# MOBILEGL_PIPE_PUSH value in the environment of a pull library steers nothing (Config.h declares
# the field inside the push guard), exactly as the HandleRecycle Legacy lanes already rely on.
mgl_itest_join_environment(MGL_ITEST_GLES_RESOURCE_SUBSYSTEM_ON_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_RESOURCE_SUBSYSTEM_LANE=on"
"MOBILEGL_PIPE_PUSH=0x1ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/resource-subsystem-on-DirectGLES.log"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_RESOURCE_SUBSYSTEM_OFF_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_RESOURCE_SUBSYSTEM_LANE=off"
"MOBILEGL_PIPE_PUSH=0x7f" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/resource-subsystem-off-DirectGLES.log"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.ResourceSubsystemControl.On."
TEST_FILTER "ResourceSubsystemControlScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_RESOURCE_SUBSYSTEM_ON_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.ResourceSubsystemControl.Off."
TEST_FILTER "ResourceSubsystemControlScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_RESOURCE_SUBSYSTEM_OFF_ENVIRONMENT}"
)
# G10's two counting entries, one per scenario that has a claim about the counter:
# StorageBufferRegrow asserts N definitions cost N round trips (never one per draw), and
# LargeArenaAdoption asserts one adoption costs exactly one. Each names a single case in its
# TEST_FILTER, for the private-log reason above.
mgl_itest_join_environment(MGL_ITEST_GLES_MPR_REGROW_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_MPR_LANE=storage-buffer-regrow"
"MOBILEGL_PIPE_PUSH=0x1ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/mpr-storage-buffer-regrow-DirectGLES.log"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_MPR_ARENA_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_MPR_LANE=large-arena-adoption"
"MOBILEGL_PIPE_PUSH=0x1ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/mpr-large-arena-adoption-DirectGLES.log"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.MapPersistentRoundtrips."
TEST_FILTER "StorageBufferRegrowScenario.NStorageDefinitionsCostNMapPersistentRoundtripsNotOnePerDraw"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_MPR_REGROW_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.MapPersistentRoundtrips."
TEST_FILTER "LargeArenaAdoptionScenario.AnAdoptionCostsExactlyOneMapPersistentRoundtrip"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_MPR_ARENA_ENVIRONMENT}"
)
# LargeArenaAdoption's three behavioural cases under BOTH arms of the same A/B. This file is where
# an adopted store's whole life is exercised - the NULL-data definition that adopts it, an
# in-flight SubData, a readback and a GPU write - so if the handle path and the legacy
# BufferBackendOps path disagree about any of it, one of these two lanes goes red and names which.
# No log path: none of these three cases reads one, and giving them one would only add a file for
# four processes to truncate. The fourth case skips in both lanes for exactly that reason, saying
# so.
mgl_itest_join_environment(MGL_ITEST_GLES_ARENA_SUBSYSTEM_ON_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_PUSH=0x1ff"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_ARENA_SUBSYSTEM_OFF_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_PUSH=0x7f"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.ResourceSubsystemOn."
TEST_FILTER "LargeArenaAdoptionScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_ARENA_SUBSYSTEM_ON_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.ResourceSubsystemOff."
TEST_FILTER "LargeArenaAdoptionScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_ARENA_SUBSYSTEM_OFF_ENVIRONMENT}"
)
if (MOBILEGL_PIPE_VERIFY)
# 900s, not the ambient 120: the comparator re-reads every field of the fill mask at the verb
# boundary and again at every accessor read, which the design budgets at 5-10x.
@@ -0,0 +1,94 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PipeStatsWindow.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
//
// Reading ONE PipeStats summary window out of the library's own log, for the scenarios whose
// claim is about a counter rather than about pixels.
//
// WHY THROUGH A LOG FILE AT ALL. MG_Util::PipeStats is internal to the library and this module
// cannot link against it (ScenarioFixture.h has the long version: on Android this binary links
// the SHIPPING libMobileGL.so, built -fvisibility=hidden). The library's `MGPipe stats:` line is
// the only channel, so a lane that wants to read a counter sets MOBILEGL_PIPE_STATS=1,
// MOBILEGL_PIPE_STATS_PERIOD=1 - one line per eglSwapBuffers - and a MOBILEGL_LOG_FILE_PATH of
// its OWN.
//
// THE LOG PATH HAS TO BE PRIVATE TO ONE CTEST ENTRY, and that is not a style rule: the library
// opens it fopen(path, "w"), so every process launched in a lane TRUNCATES it. Two entries of one
// lane reading the same path race under `ctest -j`, and the shape of the failure is an empty read
// that looks exactly like "the counter was never emitted". So a case that reads a window gets a
// ctest entry whose TEST_FILTER selects that case alone, with a log path nothing else writes -
// the rule PipeVerifyArmingScenario and CsoContentAddressingScenario already follow.
//
// THE WINDOW IS "SINCE THE PREVIOUS LINE" (PipeStats::FormatWindowLine), so the caller closes the
// setup window with a swap, runs the workload, swaps again, and reads the LAST line - which then
// covers the workload and nothing else.
#pragma once
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iterator>
#include <string>
namespace MGITest::PipeStatsWindow {
// The lane's private log path, or empty when the lane configured none.
inline std::string LibraryLogPath() {
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
return (path != nullptr && *path != '\0') ? std::string(path) : std::string();
}
inline std::string ReadWholeFile(const std::string& path) {
if (path.empty()) return {};
std::ifstream file(path, std::ios::binary);
if (!file.good()) return {};
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
}
// The last summary line in the log, verbatim. `found` is false when the library never emitted
// one, which is a different failure from "the counter read zero" and has to be reported as
// one: it means the stats channel never reached the process, not that the workload did
// nothing.
struct Window {
bool found = false;
std::string line;
};
inline Window Last(const std::string& log) {
Window window;
const std::string marker = "MGPipe stats:";
const std::size_t at = log.rfind(marker);
if (at == std::string::npos) return window;
const std::size_t end = log.find('\n', at);
window.line = log.substr(at, end == std::string::npos ? std::string::npos : end - at);
window.found = true;
return window;
}
inline Window LastFromLaneLog() { return Last(ReadWholeFile(LibraryLogPath())); }
// One counter out of that line, by its short name ("mpr", "draws", "csom"), or -1 when the
// line does not carry it. The search includes the SEPARATOR before the name and the `=` after
// it, so "draws" cannot match "draws/f=" and "mpr" cannot match a longer name ending in it -
// a substring match here would read a neighbouring counter's value and report it as this
// one's, which is the one way a counter assertion can be wrong without ever failing.
inline long long CounterOrAbsent(const Window& window, const char* shortName) {
if (!window.found) return -1;
// A counter is preceded either by a space (` mpr=`, ` draws=`) or by its bracket's
// opening (`cso[csom=`, `bytes/f[stage-buffer=`); nothing in the line is preceded by
// anything else.
for (const char* prefix : {" ", "["}) {
const std::string key = std::string(prefix) + shortName + "=";
const std::size_t at = window.line.find(key);
if (at == std::string::npos) continue;
return std::strtoll(window.line.c_str() + at + key.size(), nullptr, 10);
}
return -1;
}
} // namespace MGITest::PipeStatsWindow
@@ -25,13 +25,31 @@
// * GetBufferSubData reads back the latest CPU write - the shadow IS the map;
// * a compute-shader write through an SSBO binding of the same arena is read
// back - the GPU-written path for adopted stores (glFinish + direct read).
//
// P3a (gate G10, G12) adds a fourth case and two more lanes, and neither of them
// changes what the three above assert:
//
// * AnAdoptionCostsExactlyOneMapPersistentRoundtrip counts the acquisition.
// ARCHITECTURE.md:474 prices the adopted store at one round trip per STORAGE
// DEFINITION; `map-persistent-roundtrips` counts every map_persistent
// emission, mint or decline (D-B2), so one definition plus a frame of draws
// must publish exactly one. It reads the library's summary line, so it needs
// a lane with the stats channel and a private log path, and it SKIPS - with
// the reason - anywhere else and on any tree that does not emit the counter.
// * the three original cases are registered TWICE MORE, with P3a's resource and
// vertex-input subsystem bits set and cleared, because this file is where an
// adopted store's whole life is exercised: definition, in-flight SubData,
// readback and a GPU write. If the handle path and the legacy BufferBackendOps
// path disagree about any of it, one of the two arms goes red here.
#include <array>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
@@ -73,6 +91,19 @@ layout(std430, binding = 0) buffer Arena { uint word; };
void main() { word = 0xC0FFEEu; }
)";
// Set by the MapPersistentRoundtrips. ctest entry and by nothing else; a harness marker,
// never read by the library.
constexpr const char* kLaneMarker = "MGITEST_MPR_LANE";
// Draws issued against the arena inside the counted window. One definition, many draws:
// "one per definition" (1) and "one per draw" (kDrawsInTheWindow) have to be different
// numbers or the assertion cannot tell them apart.
constexpr int kDrawsInTheWindow = 5;
bool BuildMarkerIsSet(const char* name) {
const char* value = std::getenv(name);
return value != nullptr && value[0] == '1' && value[1] == '\0';
}
struct Vertex {
float x, y;
float r, g, b;
@@ -122,10 +153,12 @@ void main() { word = 0xC0FFEEu; }
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_arena != 0) glDeleteBuffers(1, &m_arena);
if (m_secondArena != 0) glDeleteBuffers(1, &m_secondArena);
if (m_program != 0) glDeleteProgram(m_program);
if (m_compute != 0) glDeleteProgram(m_compute);
m_vao = 0;
m_arena = 0;
m_secondArena = 0;
m_program = 0;
m_compute = 0;
}
@@ -188,6 +221,40 @@ void main() { word = 0xC0FFEEu; }
glDrawArrays(GL_TRIANGLES, 0, 6);
}
// GTEST_SKIP() returns from the function it is written in, so this cannot report
// through a return value; the caller pairs it with `if (IsSkipped()) return;`.
void SkipUnlessTheRoundtripCounterIsReadableHere() {
if (std::getenv(kLaneMarker) == nullptr) {
GTEST_SKIP() << "runs only in its own lane: the MapPersistentRoundtrips. ctest entry "
"sets MGITEST_MPR_LANE together with MOBILEGL_PIPE_PUSH's P3a mask, "
"MOBILEGL_PIPE_STATS=1, MOBILEGL_PIPE_STATS_PERIOD=1 and a private "
"MOBILEGL_LOG_FILE_PATH. The ambient entries and the two subsystem "
"arms configure none of that, and their log is shared - a read there "
"would race a neighbour's bring-up.";
return;
}
if (!BuildMarkerIsSet("MGITEST_PIPE_PUSH_BUILD")) {
GTEST_SKIP() << "this library was built without MOBILEGL_PIPE_PUSH, so "
"CallClass::MapPersistentRoundtrips does not exist and the summary "
"line carries no mpr=. The entry stays registered so that "
"`ctest -L integration-gpu` names the same tests in both builds (G2).";
return;
}
if (!BuildMarkerIsSet("MGITEST_PIPE_RESOURCE_EMITTER_PRESENT")) {
GTEST_SKIP() << "subsystem not implemented on this tree: no source under "
"MobileGL/MG_Impl/Pipe/ names MapPersistentRoundtrips, so nothing "
"emits map_persistent and mpr= is structurally zero. P3a package B "
"owns that emitter; this entry arms itself when it lands.";
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 reading "
"PipeStats";
return;
}
}
std::array<unsigned char, 4> CenterPixel() {
std::array<unsigned char, 4> px = {0, 0, 0, 0};
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
@@ -199,6 +266,9 @@ void main() { word = 0xC0FFEEu; }
unsigned int m_compute = 0;
unsigned int m_vao = 0;
unsigned int m_arena = 0;
// Only the counting case uses this one; see the comment there for why it does not
// simply re-specify m_arena.
unsigned int m_secondArena = 0;
std::string m_buildLog;
};
@@ -372,4 +442,74 @@ void main() { word = 0xC0FFEEu; }
<< "the compute write into the adopted arena did not reach the CPU readback";
}
// G10, the per-adoption half: ONE storage definition of an arena costs ONE map_persistent
// emission, however many draws read it afterwards.
//
// The arena SetUp defined is deliberately re-defined inside the counted window rather than
// measured from outside it: the window a summary line reports is "since the previous line",
// so the definition has to happen between the two swaps that bracket it, and a case that
// counted SetUp's definition would be reading a window it did not control.
//
// ONE reading case per lane, for the reason PipeStatsWindow.h gives: the library truncates the
// log per process, so two readers in a lane race under `ctest -j`.
TEST_F(LargeArenaAdoptionScenario, AnAdoptionCostsExactlyOneMapPersistentRoundtrip) {
if (!Ready() || IsSkipped()) return;
SkipUnlessTheRoundtripCounterIsReadableHere();
if (IsSkipped()) return;
Gl().EndFrame(); // close the setup window, SetUp's own definition included
// One definition of a store past the 16 MiB adoption threshold, in a SECOND arena rather
// than by re-specifying SetUp's. Re-specifying an adopted store while a VAO's attributes
// still read it leaves the backend VAO bound to the retired store - `dev`'s d7655247
// ("rebind VAOs when an adopted buffer is respecified - the immediate retire path forgot
// the buffer-id generation"), which is NOT in feat/disaggregated's history. A counting
// case that carried that crash would be red for a reason that has nothing to do with the
// counter. A fresh store is the same STORAGE DEFINITION either way, which is what
// ARCHITECTURE.md:474 prices.
glGenBuffers(1, &m_secondArena);
glBindBuffer(GL_ARRAY_BUFFER, m_secondArena);
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
glBindVertexArray(m_vao);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
reinterpret_cast<void*>(kVertexOffset));
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
reinterpret_cast<void*>(kVertexOffset + 2 * sizeof(float)));
ASSERT_EQ(FirstGLError(), 0u) << "defining the second arena inside the counted window failed";
// ... and then a frame's worth of traffic against it, of the shape the arena exists for:
// a SubData per draw, every one of which lands in the adopted mapping and none of which
// may acquire it again.
const auto vertices = QuadVertices(0.f, 1.f, 0.f);
for (int draw = 0; draw < kDrawsInTheWindow; ++draw) {
glBindBuffer(GL_ARRAY_BUFFER, m_secondArena);
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
DrawQuad();
}
const auto px = CenterPixel();
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_GT(px[1], 200) << "the draws inside the counted window never landed, so the count below "
"would be a number about nothing";
Gl().EndFrame(); // the swap that emits the window covering exactly the work above
const PipeStatsWindow::Window window = PipeStatsWindow::LastFromLaneLog();
ASSERT_TRUE(window.found) << "no 'MGPipe stats:' line in " << PipeStatsWindow::LibraryLogPath()
<< ": either MOBILEGL_PIPE_STATS / MOBILEGL_PIPE_STATS_PERIOD did not "
"reach the process, or nothing reached PipeStats::OnPresent.";
RecordProperty("stats_line", window.line.c_str());
const long long roundtrips = PipeStatsWindow::CounterOrAbsent(window, "mpr");
ASSERT_GE(roundtrips, 0) << "the summary line carries no mpr= field: " << window.line;
EXPECT_EQ(roundtrips, 1)
<< "one storage definition of an adopted arena is one map_persistent emission "
"(ARCHITECTURE.md:474, D-B2: mint OR decline, both need an answer from the resource "
"owner). This window defined the arena once and drew from it "
<< kDrawsInTheWindow << " times, so 1 is the whole cost; " << kDrawsInTheWindow
<< " would mean the acquisition moved onto the draw path - the ~167 ms/arena hiccup this "
"adoption removed, re-introduced - and 0 would mean the emission stopped happening. It "
"reported: "
<< window.line;
}
} // namespace MGITest
@@ -0,0 +1,321 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ResourceSubsystemControlScenario.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 P3a SUBSYSTEM A/B IS REAL (gate G12).
//
// P3a migrates two subsystems: kMGPipeSubsystemResources (bit 7, the resource_* family) and
// kMGPipeSubsystemVertexInput (bit 8, vertex elements / buffers / index). The push build's default
// mask becomes kMGPipeSubsystemsMigratedAtP3a = 0x1ff, and P2's 0x7f survives as the control that
// clears exactly those two bits - MGPipe.h:79's rule that every phase's constant keeps meaning what
// it meant, so an operator's recorded mask is still readable a phase later.
//
// That A/B is what every "push vs pull" number in MEASUREMENTS.md is taken against, and it has one
// characteristic failure mode: the bits stop steering anything, both arms run the same code, and
// every later comparison is quietly taken against a switch that does nothing. This file is the
// entry that cannot let that happen.
//
// WHAT IT ASSERTS, per arm:
//
// on (MOBILEGL_PIPE_PUSH=0x1ff)
// The client emits map_persistent for every definition of a store past
// BufferObject::TryAdoptLargeStorage's 16 MiB threshold, so the window's
// map-persistent-roundtrips (`mpr=`) equals the number of definitions in it - one per
// storage definition, mint or decline (D-B2).
//
// off (MOBILEGL_PIPE_PUSH=0x7f, P2's default = P3a's subsystems cleared)
// The frontend dispatch falls through to the legacy BufferBackendOps arm, nothing is emitted
// through the resource family, and mpr= must read ZERO. This is the reading a dead switch
// fails: with bit 7 ignored, this arm would report the same non-zero count as the other one.
//
// both arms
// THE PIXELS MUST NOT MOVE. The arena is filled with one solid-colour quad and drawn, and
// both arms must read back that colour. "The counters moved and the picture did not" is the
// whole claim - a switch that changed what is drawn would not be an A/B, it would be a bug.
//
// WHY IT CAN SKIP. The counter is emitted by the client-side resource tracker (P3a package B), and
// this file is written against the P3a contract commit, before that package lands. Until then
// nothing emits map_persistent, mpr= is structurally zero in BOTH arms, and an assertion about the
// difference would be a statement about nothing. The build answers the question rather than a
// hand-maintained list: MG_IntegrationTest/CMakeLists.txt greps every source under MG_Impl/Pipe/
// for the counter's name and passes the answer in as MGITEST_PIPE_RESOURCE_EMITTER_PRESENT, with a
// CONFIGURE_DEPENDS on that directory and on each file it finds so the answer cannot go stale. It
// is a CONTENT probe, not a filename probe, so the owning package keeps control of its own file
// layout - P3a's new client files are headers (D-N), and a glob for `ResourceTracker.cpp` would
// have kept this control skipping forever with a reason that had become false.
//
// DirectGLES ONLY, and that is the honest scope: P3a migrates Espryt's buffer and VAO paths.
// Magma's buffer path is P7 and registers no MGPipeResourceOps, so a DirectVulkan lane here would
// be measuring the client emitter against a backend that has not been asked to change - which is
// a real question, but it is P7's, not this control's.
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/ScenarioFixture.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 {
// Set by the two ResourceSubsystemControl. ctest entries and by nothing else; a harness
// marker, never read by the library. Its absence means an ambient entry, where neither the
// stats channel nor a private log path is configured.
constexpr const char* kLaneMarker = "MGITEST_RESOURCE_SUBSYSTEM_LANE";
constexpr const char* kLaneOn = "on";
constexpr const char* kLaneOff = "off";
// Past BufferObject::TryAdoptLargeStorage's 16 MiB threshold, so the store is offered for
// adoption at all; the vertex payload sits deep inside it so a clamped or aliased adopted
// range would miss it. Same shape as LargeArenaAdoptionScenario, deliberately: this
// control's workload has to be one the buffer path really takes.
constexpr GLsizeiptr kArenaBytes = GLsizeiptr(20) * 1024 * 1024;
constexpr GLintptr kVertexOffset = GLintptr(16) * 1024 * 1024;
// Two definitions and several draws each, so "one per definition", "one per draw" and
// "none at all" are three different numbers.
//
// TWO ARENAS, EACH DEFINED ONCE, rather than one arena defined twice, and that is a
// deliberate detour around a bug that is not this branch's: re-specifying an adopted
// store while a VAO's attributes still read it leaves the backend VAO bound to the
// retired store, which `dev`'s d7655247 ("rebind VAOs when an adopted buffer is
// respecified - the immediate retire path forgot the buffer-id generation") fixes. That
// commit is NOT in feat/disaggregated's history, and this workload reproduced it as a
// hard SIGSEGV inside the vertex fetch on the first draw after the second definition.
// A control that carried a pre-existing `dev` crash would be red for a reason that has
// nothing to do with the subsystem bits it is measuring, so it defines a second arena
// instead - which is the same number of STORAGE DEFINITIONS, and therefore the same
// count, by ARCHITECTURE.md:474's own wording. The finding is recorded for the
// integrator rather than fixed here (ROADMAP.md:88: unrelated fixes do not ride the
// split work).
constexpr int kDefinitionsInTheWindow = 2;
constexpr int kDrawsPerDefinition = 3;
constexpr int kInset = 2;
constexpr const char* kVS = R"(#version 330 core
in vec2 aPos;
in vec3 aColor;
out vec3 vColor;
void main() {
vColor = aColor;
gl_Position = vec4(aPos, 0.0, 1.0);
}
)";
constexpr const char* kFS = 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;
};
std::vector<Vertex> Quad(float r, float g, float b) {
return {
{-1.0f, -1.0f, r, g, b}, {1.0f, -1.0f, r, g, b}, {1.0f, 1.0f, r, g, b},
{-1.0f, -1.0f, r, g, b}, {1.0f, 1.0f, r, g, b}, {-1.0f, 1.0f, r, g, b},
};
}
bool BuildMarkerIsSet(const char* name) {
const char* value = std::getenv(name);
return value != nullptr && value[0] == '1' && value[1] == '\0';
}
std::string LaneName() {
const char* lane = std::getenv(kLaneMarker);
return lane != nullptr ? std::string(lane) : std::string();
}
class ResourceSubsystemControlScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_lane = LaneName();
std::string error;
m_program = CompileProgram(kVS, kFS, &error);
ASSERT_NE(m_program, 0u) << error;
// The VAO only. The arenas are created and defined inside the counted window -
// the window a summary line reports is "since the previous line", so a
// definition taken in SetUp would be counted in a window this case does not
// control - and their attribute pointers are declared only once each store
// exists, because an attribute whose offset is 16 MiB into a store that has not
// been defined yet is a range no driver has to accept.
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
RecordProperty("lane", m_lane.empty() ? "ambient" : m_lane.c_str());
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
for (GLuint& arena : m_arenas) {
if (arena != 0) glDeleteBuffers(1, &arena);
arena = 0;
}
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_program != 0) glDeleteProgram(m_program);
}
// GTEST_SKIP() returns from the function it is written in, so this cannot report
// through a return value; the caller pairs it with `if (IsSkipped()) return;`.
void SkipUnlessTheLaneIsAssertableHere() {
if (m_lane.empty()) {
GTEST_SKIP() << "runs only in its own lane: the two ResourceSubsystemControl. ctest "
"entries set MGITEST_RESOURCE_SUBSYSTEM_LANE together with the "
"MOBILEGL_PIPE_PUSH bitmask that arm means, MOBILEGL_PIPE_STATS=1, "
"MOBILEGL_PIPE_STATS_PERIOD=1 and a private MOBILEGL_LOG_FILE_PATH. "
"None of that is configured in the ambient entries, and the ambient "
"log is shared, so a read here would race.";
return;
}
if (!BuildMarkerIsSet("MGITEST_PIPE_PUSH_BUILD")) {
GTEST_SKIP() << "this library was built without MOBILEGL_PIPE_PUSH: there are no "
"subsystem bits to clear, CallClass::MapPersistentRoundtrips does "
"not exist and the summary line carries no mpr=. The entry is "
"registered here anyway so that `ctest -L integration-gpu` names the "
"same tests in the pull build and the push build (gate G2).";
return;
}
if (!BuildMarkerIsSet("MGITEST_PIPE_RESOURCE_EMITTER_PRESENT")) {
GTEST_SKIP() << "subsystem not implemented on this tree: no source under "
"MobileGL/MG_Impl/Pipe/ names MapPersistentRoundtrips, so nothing "
"emits map_persistent, mpr= is structurally zero in BOTH arms and "
"the difference between them is not observable yet. P3a package B "
"owns the client-side resource tracker; this control arms itself "
"when it lands, whatever file that package puts the emitter in.";
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 reading "
"PipeStats";
return;
}
}
// ONE storage definition - the NULL-data glBufferData past the adoption threshold,
// which is Minecraft's arena-creation idiom and the adoption point - then the
// attribute pointers into it and a few draws. Entirely inside one frame, so one
// summary window covers exactly this.
void DefineAnArenaAndDrawFromIt(int index, float r, float g, float b) {
glGenBuffers(1, &m_arenas[static_cast<std::size_t>(index)]);
glBindBuffer(GL_ARRAY_BUFFER, m_arenas[static_cast<std::size_t>(index)]);
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
const std::vector<Vertex> vertices = Quad(r, g, b);
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
glBindVertexArray(m_vao);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
reinterpret_cast<void*>(kVertexOffset));
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
reinterpret_cast<void*>(kVertexOffset + 2 * sizeof(float)));
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glUseProgram(m_program);
for (int draw = 0; draw < kDrawsPerDefinition; ++draw) {
glDrawArrays(GL_TRIANGLES, 0, 6);
}
}
std::string m_lane;
GLuint m_program = 0;
GLuint m_vao = 0;
std::vector<GLuint> m_arenas =
std::vector<GLuint>(static_cast<std::size_t>(kDefinitionsInTheWindow), 0u);
};
// ONE case per lane, and it is a constraint rather than a preference: this case READS the
// library log, the log is a per-LANE resource (the library opens it fopen(path, "w"), so
// every process in a lane truncates it), and a second case here would race this one under
// `ctest -j` with a failure indistinguishable from "the counter was never emitted". The
// plumbing is asserted first, with its own message, inside this one process.
TEST_F(ResourceSubsystemControlScenario, ClearingTheP3aBitsStopsTheEmissionsAndNotThePixels) {
if (!Ready()) return;
SkipUnlessTheLaneIsAssertableHere();
if (IsSkipped()) return;
BindDefaultFramebuffer();
Gl().EndFrame(); // close the setup window: everything below is one window
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
for (int definition = 0; definition < kDefinitionsInTheWindow; ++definition) {
DefineAnArenaAndDrawFromIt(definition, 0.0f, 1.0f, 0.0f);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR))
<< "arena definition " << definition << " left a GL error behind";
}
const Image image = ReadPixels(Gl().Width(), Gl().Height());
Gl().EndFrame(); // the swap that emits the window covering exactly the work above
const PipeStatsWindow::Window window = PipeStatsWindow::LastFromLaneLog();
ASSERT_TRUE(window.found)
<< "no 'MGPipe stats:' line in " << PipeStatsWindow::LibraryLogPath()
<< ". This IS a push build (the lane checked MGITEST_PIPE_PUSH_BUILD before getting "
"here), so either MOBILEGL_PIPE_STATS / MOBILEGL_PIPE_STATS_PERIOD did not reach the "
"process, or no summary line was emitted at all because nothing reached "
"PipeStats::OnPresent.";
RecordProperty("stats_line", window.line.c_str());
const long long roundtrips = PipeStatsWindow::CounterOrAbsent(window, "mpr");
ASSERT_GE(roundtrips, 0)
<< "the summary line carries no mpr= field, so this build's PipeStats has no "
"map-persistent-roundtrips counter to read: "
<< window.line;
if (m_lane == kLaneOn) {
EXPECT_EQ(roundtrips, static_cast<long long>(kDefinitionsInTheWindow))
<< "with bits 7|8 SET the resource family is the path a store definition takes, so "
"each of the " << kDefinitionsInTheWindow
<< " definitions in this window is one map_persistent emission (mint or decline - "
"both need an answer from the resource owner, D-B2). "
<< (kDefinitionsInTheWindow * kDrawsPerDefinition)
<< " would mean an acquisition per draw, and 0 would mean the emission never "
"happened on the arm that is supposed to do it. It reported: "
<< window.line;
} else if (m_lane == kLaneOff) {
EXPECT_EQ(roundtrips, 0)
<< "with bits 7|8 CLEARED (MOBILEGL_PIPE_PUSH=0x7f, P2's default) the frontend "
"dispatch must fall through to the legacy BufferBackendOps arm and emit nothing "
"through the resource family, so mpr= must be zero. A non-zero count here is the "
"dead-switch reading: the bits are being ignored, both arms run the same code, "
"and every push-vs-pull number taken against this A/B is measuring one arm twice. "
"It reported: "
<< window.line;
} else {
FAIL() << "unknown " << kLaneMarker << " value '" << m_lane
<< "': the arms are on / off. Reading an unrecognised name as either would make "
"this lane assert the other arm's expectation while claiming to test this one.";
}
// ... and the picture is the same whichever arm ran. The arena is drawn with one solid
// colour, so both arms must read back exactly that.
EXPECT_TRUE(RegionIsMostly(image, kInset, image.Width() - kInset, kInset,
image.Height() - kInset, "green", 0.0,
"the arena draw [" + m_lane + "]"))
<< "the subsystem bits changed what is DRAWN, which is not an A/B - the handle path and "
"the legacy path must produce the same pixels from the same arena.";
}
} // namespace
} // namespace MGITest
@@ -19,11 +19,26 @@
//
// The assertion is deliberately on the WHOLE grown range, so a partial write names the byte the
// stale extent stopped at.
//
// P3a ADDS THE COST OF THAT REGROWTH (gate G10). ARCHITECTURE.md:474 prices a persistently mapped
// store at "one round trip per STORAGE DEFINITION, not one per store" - and, emphatically, not one
// per draw. `map-persistent-roundtrips` (`mpr=` in the summary line) counts every map_persistent
// EMISSION, mint or decline (D-B2), so the claim is directly countable: N definitions of an
// adopted store must publish exactly N, whatever the workload does between them. A regression that
// re-acquires per dispatch reports N x dispatches, which is the failure this case exists to name;
// a regression that stops emitting reports 0.
//
// The second case therefore respecifies a store LARGE ENOUGH TO BE ADOPTED
// (BufferObject::TryAdoptLargeStorage's 16 MiB threshold), several times, with several dispatches
// between the definitions, and reads the one window that covers exactly that workload. It skips -
// visibly, with the reason - on a tree where nothing emits the counter yet.
#include <cstdlib>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
@@ -50,6 +65,31 @@ void main() {
constexpr int kSmallElements = 6; // 24 bytes - the first iteration's size
constexpr int kLargeElements = 24; // 96 bytes - what the second iteration grows to
// The G10 case's sizes. Every one of them is past BufferObject::TryAdoptLargeStorage's
// 16 MiB threshold, because a store below it is never offered for adoption at all and the
// window would then be asserting that nothing happened. They GROW, which is the scenario's
// subject: each glBufferData is a new storage definition, so each is one acquisition.
constexpr int kAdoptedDefinitions = 3;
constexpr int kAdoptedBaseElements = 5 * 1024 * 1024; // 20 MiB of uint
constexpr int kAdoptedGrowthElements = 1024 * 1024; // + 4 MiB per definition
// Enough dispatches per definition that "one per definition" and "one per dispatch" are
// different numbers by a wide margin (3 vs 12), and few enough to stay cheap.
constexpr int kDispatchesPerDefinition = 4;
// Only the first elements are dispatched over: the point of the large store is the
// ADOPTION, not the compute cost.
constexpr int kDispatchedElements = 6;
// Set by the MapPersistentRoundtrips. ctest entry and by nothing else; a harness marker,
// never read by the library. Its absence means an ambient entry, where neither the stats
// channel nor a private log path is configured - and where the shared log makes a read
// race a neighbour's bring-up.
constexpr const char* kLaneMarker = "MGITEST_MPR_LANE";
bool BuildMarkerIsSet(const char* name) {
const char* value = std::getenv(name);
return value != nullptr && value[0] == '1' && value[1] == '\0';
}
class StorageBufferRegrowScenario : public ScenarioTest {
protected:
void SetUp() override {
@@ -114,6 +154,56 @@ void main() {
return values;
}
// The Minecraft arena idiom, and the adoption point: a NULL-data definition of a
// store past the threshold. No host-side vector, so a 28 MiB definition costs
// nothing on this side of the API.
void DefineAdoptedStore(int elements) {
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
glBufferData(GL_SHADER_STORAGE_BUFFER,
static_cast<GLsizeiptr>(static_cast<GLsizeiptr>(elements) *
static_cast<GLsizeiptr>(sizeof(unsigned int))),
nullptr, GL_DYNAMIC_DRAW);
}
// GTEST_SKIP() returns from the function it is written in, so this cannot report
// through a return value; the caller pairs it with `if (IsSkipped()) return;`.
void SkipUnlessTheRoundtripCounterIsReadableHere() {
if (std::getenv(kLaneMarker) == nullptr) {
GTEST_SKIP() << "runs only in its own lane: the MapPersistentRoundtrips. ctest entry "
"sets MGITEST_MPR_LANE together with MOBILEGL_PIPE_PUSH's P3a mask, "
"MOBILEGL_PIPE_STATS=1, MOBILEGL_PIPE_STATS_PERIOD=1 and a private "
"MOBILEGL_LOG_FILE_PATH. None of that is configured in the ambient "
"entries, and the ambient log is shared, so a read here would race a "
"neighbour's bring-up.";
return;
}
if (!BuildMarkerIsSet("MGITEST_PIPE_PUSH_BUILD")) {
GTEST_SKIP() << "this library was built without MOBILEGL_PIPE_PUSH, so "
"CallClass::MapPersistentRoundtrips does not exist (PipeStats.h "
"declares it inside the push guard, because growing the enum in a "
"pull build resizes the counter arrays and the name table - a G1 "
"break for a counter that could never leave zero) and the summary "
"line carries no mpr=. The entry is registered here anyway so that "
"`ctest -L integration-gpu` names the same tests in the pull build "
"and the push build (gate G2).";
return;
}
if (!BuildMarkerIsSet("MGITEST_PIPE_RESOURCE_EMITTER_PRESENT")) {
GTEST_SKIP() << "subsystem not implemented on this tree: no source under "
"MobileGL/MG_Impl/Pipe/ names MapPersistentRoundtrips, so nothing "
"emits map_persistent and mpr= is structurally zero. P3a package B "
"owns the client-side resource tracker; this entry arms itself when "
"it lands, whatever file that package puts the emitter in.";
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 reading "
"PipeStats";
return;
}
}
unsigned int m_program = 0;
GLuint m_buffer = 0;
std::string m_buildLog;
@@ -153,4 +243,68 @@ void main() {
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
}
// G10. N storage definitions of an adopted store cost N map-persistent round trips - not one
// per draw, and not zero.
//
// ONE case in this lane, and that is a constraint rather than a preference: it READS the
// library log, the log is a per-lane resource (the library opens it fopen(path, "w"), so every
// process in a lane truncates it), and a second reading entry in the same lane would race this
// one under `ctest -j` with a failure that looks exactly like "the counter was never emitted".
// The plumbing is therefore asserted first, with its own message, inside this one process.
TEST_F(StorageBufferRegrowScenario, NStorageDefinitionsCostNMapPersistentRoundtripsNotOnePerDraw) {
if (!Ready()) return;
SkipUnlessTheRoundtripCounterIsReadableHere();
if (IsSkipped()) return;
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_buffer);
Gl().EndFrame(); // close the setup window: everything below is one window
for (int definition = 0; definition < kAdoptedDefinitions; ++definition) {
DefineAdoptedStore(kAdoptedBaseElements + definition * kAdoptedGrowthElements);
ASSERT_EQ(FirstGLError(), 0u) << "definition " << definition << " of the adopted store failed";
for (int dispatch = 0; dispatch < kDispatchesPerDefinition; ++dispatch) {
glUseProgram(m_program);
glDispatchCompute(static_cast<GLuint>(kDispatchedElements), 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
}
}
// The store is still the one the last definition made, and it still works: a counter
// assertion over a workload that silently stopped functioning would be measuring nothing.
std::vector<unsigned int> values(static_cast<std::size_t>(kDispatchedElements), 0xDEADBEEFu);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
EXPECT_EQ(FirstGLError(), 0u);
for (int i = 0; i < kDispatchedElements; ++i) {
EXPECT_EQ(values[static_cast<std::size_t>(i)], static_cast<unsigned int>(i + 1))
<< "the adopted store's own dispatch did not write element " << i;
}
Gl().EndFrame(); // the swap that emits the window covering exactly the loop above
const PipeStatsWindow::Window window = PipeStatsWindow::LastFromLaneLog();
ASSERT_TRUE(window.found) << "no 'MGPipe stats:' line in " << PipeStatsWindow::LibraryLogPath()
<< ". This IS a push build (the lane checked MGITEST_PIPE_PUSH_BUILD "
"before getting here), so either MOBILEGL_PIPE_STATS / "
"MOBILEGL_PIPE_STATS_PERIOD did not reach the process or no summary "
"line was emitted at all because nothing reached PipeStats::OnPresent.";
RecordProperty("stats_line", window.line.c_str());
const long long roundtrips = PipeStatsWindow::CounterOrAbsent(window, "mpr");
ASSERT_GE(roundtrips, 0)
<< "the summary line carries no mpr= field, so this build's PipeStats has no "
"map-persistent-roundtrips counter to read: " << window.line;
EXPECT_EQ(roundtrips, static_cast<long long>(kAdoptedDefinitions))
<< "an adopted store costs ONE map_persistent per STORAGE DEFINITION "
"(ARCHITECTURE.md:474). This window defined the store " << kAdoptedDefinitions
<< " times and dispatched " << kDispatchesPerDefinition << " times against each of them, so "
<< kAdoptedDefinitions << " is the whole cost. "
<< (kAdoptedDefinitions * kDispatchesPerDefinition)
<< " would mean an acquisition per DRAW - the regression this counter exists to catch - and 0 "
"would mean nothing emitted map_persistent at all. It reported: "
<< window.line;
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
}
} // namespace MGITest