mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user