mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[Test] (MG_IntegrationTest): add the Triangle and PersistentCoherentMap scenarios and the DirectGLES.Split. lanes - the split family skips naming c0's remaining signature stubs rather than going green against the monolith path it would otherwise run
This commit is contained in:
@@ -141,6 +141,11 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/ObjectSubsystemControlScenario.cpp
|
Scenarios/ObjectSubsystemControlScenario.cpp
|
||||||
Scenarios/P4aSeamAuditScenario.cpp
|
Scenarios/P4aSeamAuditScenario.cpp
|
||||||
Scenarios/P4aFinalFixScenario.cpp
|
Scenarios/P4aFinalFixScenario.cpp
|
||||||
|
# P5's two new scenarios, targets B and C of the reduced path (BRIEF-P5 4). Both are
|
||||||
|
# ORDINARY GL scenarios that run in every lane; the DirectGLES.Split. entries further down
|
||||||
|
# run the same cases with MOBILEGL_TRANSPORT=inproc.
|
||||||
|
Scenarios/TriangleScenario.cpp
|
||||||
|
Scenarios/PersistentCoherentMapScenario.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||||
@@ -1757,3 +1762,172 @@ if (MOBILEGL_PIPE_VERIFY)
|
|||||||
ENVIRONMENT "${MGL_ITEST_VULKAN_POISON_OMIT_ENVIRONMENT}"
|
ENVIRONMENT "${MGL_ITEST_VULKAN_POISON_OMIT_ENVIRONMENT}"
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# --- P5: the DirectGLES.Split. lanes and the persistent-map ARM lanes ------------------------
|
||||||
|
#
|
||||||
|
# THE PRIVATE SECOND LABEL IS WHAT MAKES THE LANE FALSIFIABLE, and it is the verify block above
|
||||||
|
# copied verbatim for the same argument: `ctest -L integration-split --no-tests=error` in a build
|
||||||
|
# that forgot -DMOBILEGL_BUILD_DISAGGREGATED=ON matches NOTHING and FAILS, instead of reporting a
|
||||||
|
# green run of zero entries. Keeping `integration-gpu` as well is why "a split build's
|
||||||
|
# `ctest -L integration-gpu` still describes the whole registration set" stays true.
|
||||||
|
#
|
||||||
|
# REGISTERED INSIDE `if (MOBILEGL_BUILD_DISAGGREGATED)`, and that is what keeps gate G2 (pull and
|
||||||
|
# push name-for-name identical) untouched: neither of those two builds sets the option, so the
|
||||||
|
# Split family is absent from BOTH of them rather than present in one. The two SCENARIO FILES the
|
||||||
|
# lanes point at are added to the source list unconditionally, so their cases DO appear in the
|
||||||
|
# ambient DirectGLES./DirectVulkan. registrations of every build - they are ordinary GL scenarios
|
||||||
|
# and their monolith behaviour is the baseline the split arm is compared against.
|
||||||
|
#
|
||||||
|
# WHAT HAPPENS BEFORE PACKAGES c1, s1 AND v1 LAND. MOBILEGL_TRANSPORT is parsed only under
|
||||||
|
# MOBILEGL_BUILD_DISAGGREGATED (CONTRACT-P5 5) and, until something consumes it, a parsed
|
||||||
|
# `inproc` still runs monolith. A Split entry that went green there would be the exact failure
|
||||||
|
# the lane exists to prevent, so the probes below arm the cases and they SKIP - naming the
|
||||||
|
# missing thing - until they answer yes. The registration is never deleted: a ctest name may
|
||||||
|
# never disappear (gate G14).
|
||||||
|
#
|
||||||
|
# THE EXISTENCE OF THE DIRECTORY IS NOT THE QUESTION, and finding that out cost a false green
|
||||||
|
# here. Package c0 landed MG_Remote/{Client,Server} as SIGNATURE STUBS - CapsMirror.cpp,
|
||||||
|
# ClientSession.cpp, EmitTables.cpp, PipeApplier.cpp, ServerLoop.cpp, ServerSession.cpp all
|
||||||
|
# exist, all compile, and every entry point in them is `MGLOG_F("Fatal{Unimplemented...")` +
|
||||||
|
# abort. A probe for the c1 symbols alone therefore answered YES against a tree where
|
||||||
|
# RemoteEmitTable() aborts on sight and ImplementedVerbCount() returns 0, and the Split lanes
|
||||||
|
# armed and asserted monolith behaviour.
|
||||||
|
#
|
||||||
|
# So the arming condition is a CONJUNCTION with a NEGATIVE half: the client names its own
|
||||||
|
# symbols AND no source under Client or Server still carries a c0 signature stub. The negative
|
||||||
|
# half is the sharp one and it is layout-independent in the way the house rule demands - the
|
||||||
|
# owning packages have to DELETE those aborts to implement anything, whatever files they
|
||||||
|
# choose to put the implementation in. Both halves are printed at configure time.
|
||||||
|
mgl_itest_probe_for_symbol(MGL_ITEST_REMOTE_CLIENT_SIGNATURES
|
||||||
|
"${MGL_ITEST_ROOT}/MobileGL/MG_Remote/Client"
|
||||||
|
"kRemoteEmitSlotCount|RemoteEmitTable|CapsMirror")
|
||||||
|
mgl_itest_probe_for_symbol(MGL_ITEST_REMOTE_CLIENT_STUB
|
||||||
|
"${MGL_ITEST_ROOT}/MobileGL/MG_Remote/Client" "Fatal.Unimplemented")
|
||||||
|
mgl_itest_probe_for_symbol(MGL_ITEST_REMOTE_SERVER_STUB
|
||||||
|
"${MGL_ITEST_ROOT}/MobileGL/MG_Remote/Server" "Fatal.Unimplemented")
|
||||||
|
set(MGL_ITEST_SPLIT_CAPABILITY_ENV "")
|
||||||
|
if (MGL_ITEST_REMOTE_CLIENT_SIGNATURES AND NOT MGL_ITEST_REMOTE_CLIENT_STUB AND
|
||||||
|
NOT MGL_ITEST_REMOTE_SERVER_STUB)
|
||||||
|
message(STATUS "Integration tests: MG_Remote/{Client,Server} carry no c0 signature stub and "
|
||||||
|
"the client names its own symbols (${MGL_ITEST_REMOTE_CLIENT_SIGNATURES}) - "
|
||||||
|
"the DirectGLES.Split. lanes assert")
|
||||||
|
list(APPEND MGL_ITEST_SPLIT_CAPABILITY_ENV "MGITEST_REMOTE_CLIENT_PRESENT=1")
|
||||||
|
elseif (MGL_ITEST_REMOTE_CLIENT_STUB OR MGL_ITEST_REMOTE_SERVER_STUB)
|
||||||
|
message(STATUS "Integration tests: MG_Remote still carries c0 signature stubs "
|
||||||
|
"(${MGL_ITEST_REMOTE_CLIENT_STUB} ${MGL_ITEST_REMOTE_SERVER_STUB}) - packages "
|
||||||
|
"c1/s1/v1 have not landed, so every DirectGLES.Split. entry stays registered "
|
||||||
|
"and SKIPS")
|
||||||
|
else()
|
||||||
|
message(STATUS "Integration tests: no source under MobileGL/MG_Remote/Client names "
|
||||||
|
"kRemoteEmitSlotCount/RemoteEmitTable/CapsMirror - there is no client at all, "
|
||||||
|
"so every DirectGLES.Split. entry stays registered and SKIPS")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# ARCHITECTURE.md:543 - every new ctest ENVIRONMENT and add_trace_replay_test's SPLIT branch
|
||||||
|
# carries MOBILEGL_IPC_SERVER_PATH, because the dladdr fallback cannot find the server from a
|
||||||
|
# binary that links MobileGL_s statically. P5 is inproc-only and nothing reads the value yet; it
|
||||||
|
# is carried now so that an unparsed variable and a parsed-and-ignored one stop being
|
||||||
|
# indistinguishable the day P6 consumes it. A generator expression is deliberately NOT used here:
|
||||||
|
# a $<TARGET_FILE_DIR:...> inside a gtest_discover_tests PROPERTIES value is written into the
|
||||||
|
# generated ctest include file verbatim and never expanded.
|
||||||
|
set(MGL_ITEST_SPLIT_SERVER_PATH "${CMAKE_BINARY_DIR}/libMobileGLServer.so")
|
||||||
|
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_GLES_SPLIT_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_TRANSPORT=inproc"
|
||||||
|
"MOBILEGL_IPC_SERVER_PATH=${MGL_ITEST_SPLIT_SERVER_PATH}" "MGITEST_SPLIT_LANE=1"
|
||||||
|
${MGL_ITEST_SPLIT_CAPABILITY_ENV} ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||||
|
|
||||||
|
# The two counting lanes, one per transport, and they are a PAIR: exit gate E3(c) asks that the
|
||||||
|
# split arm's `mpr` equal the monolith arm's, and one test process can only ever see its own. So
|
||||||
|
# both lanes run the same workload and assert the same constant, and the equality holds by
|
||||||
|
# construction with each half able to fail on its own.
|
||||||
|
#
|
||||||
|
# Each gets a LOG PATH OF ITS OWN and a TEST_FILTER that selects ONE case, for the reason
|
||||||
|
# PipeStatsWindow.h gives: the library opens its log fopen(path, "w"), so every process in a lane
|
||||||
|
# truncates it, and two readers in one lane race under `ctest -j`. RESOURCE_LOCK on top of that,
|
||||||
|
# for the reason the UnlocatedIoBlocks lane gives - measured on this tree, three runs of the full
|
||||||
|
# integration-gpu label at -j 8 produced 4, 0 and 2 spurious failures of the log-reading cases
|
||||||
|
# without it.
|
||||||
|
#
|
||||||
|
# The monolith lane declares NO arm: which arm AcquireMemoryRange takes for a sub-16-MiB
|
||||||
|
# PERSISTENT|WRITE|COHERENT map is a property of the driver and the build, so the case RECORDS it
|
||||||
|
# there and skips the assertion. Only the split lane - where R-6 pins the adopt tier at T2 -
|
||||||
|
# declares one.
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_GLES_PMAP_ARM_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_PMAP_LANE=1"
|
||||||
|
"MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||||
|
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/persistent-map-arm-DirectGLES.log"
|
||||||
|
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||||
|
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.PersistentMapArm."
|
||||||
|
TEST_FILTER "PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS integration-gpu
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
RESOURCE_LOCK persistent-map-arm-DirectGLES.log
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_PMAP_ARM_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||||
|
# One block per scenario, following the DirectGLES.MapPersistentRoundtrips. precedent: a
|
||||||
|
# `:`-separated multi-pattern TEST_FILTER is not used anywhere in this file, so its escaping
|
||||||
|
# through gtest_discover_tests' flat PROPERTIES forwarding is unproven, and three blocks cost
|
||||||
|
# nothing.
|
||||||
|
#
|
||||||
|
# ClearThenReadPixelsScenario is target A of the reduced path and already existed; Triangle is
|
||||||
|
# target B and PersistentCoherentMap is target C, both new in P5.
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.Split."
|
||||||
|
TEST_FILTER "ClearThenReadPixelsScenario.*"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS "integration-gpu\;integration-split"
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.Split."
|
||||||
|
TEST_FILTER "TriangleScenario.*"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS "integration-gpu\;integration-split"
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.Split."
|
||||||
|
TEST_FILTER "PersistentCoherentMapScenario.*"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS "integration-gpu\;integration-split"
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# The split half of the counting pair. MGITEST_PERSISTENT_MAP_ARM=emulated is R-6: under split
|
||||||
|
# the adopt tier is pinned at T2, the resource owner declines every acquisition and the client
|
||||||
|
# pushes the mapping's dirty blocks - so pmap must be non-zero and mpr must be the monolith
|
||||||
|
# lane's number. pmap == 0 here means MGPipeApplyMapPersistent handed back a pointer, which is
|
||||||
|
# exit gate E3(d) failing, and under inproc that failure is invisible in pixels.
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_GLES_SPLIT_PMAP_ARM_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_TRANSPORT=inproc"
|
||||||
|
"MOBILEGL_IPC_SERVER_PATH=${MGL_ITEST_SPLIT_SERVER_PATH}" "MGITEST_SPLIT_LANE=1"
|
||||||
|
"MGITEST_PMAP_LANE=1" "MGITEST_PERSISTENT_MAP_ARM=emulated"
|
||||||
|
"MGITEST_SPLIT_EXPECT_TRANSPORT=inproc"
|
||||||
|
"MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||||
|
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/persistent-map-arm-split-DirectGLES.log"
|
||||||
|
${MGL_ITEST_SPLIT_CAPABILITY_ENV} ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||||
|
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.Split.PersistentMapArm."
|
||||||
|
TEST_FILTER "PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS "integration-gpu\;integration-split"
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
RESOURCE_LOCK persistent-map-arm-split-DirectGLES.log
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_PMAP_ARM_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -91,4 +91,23 @@ namespace MGITest::PipeStatsWindow {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The same lookup for a counter that is printed as a FIXED-POINT PER-FRAME FIGURE rather
|
||||||
|
// than as an integer, which is every member of the bytes/f[...] bracket: FormatWindowLine
|
||||||
|
// divides each byte class by the window's frame count and prints two decimals whenever the
|
||||||
|
// window contains a Present. `pmap` is one of those, so CounterOrAbsent's strtoll reads
|
||||||
|
// "0.37" as 0 and an assertion that a push HAPPENED silently becomes an assertion that it
|
||||||
|
// pushed at least one whole byte per frame - the one way this counter can be wrong without
|
||||||
|
// ever failing. Returns -1.0 when the line does not carry the name; every real value of a
|
||||||
|
// byte class is >= 0, so the sentinel cannot collide with one.
|
||||||
|
inline double CounterAsDoubleOrAbsent(const Window& window, const char* shortName) {
|
||||||
|
if (!window.found) return -1.0;
|
||||||
|
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::strtod(window.line.c_str() + at + key.size(), nullptr);
|
||||||
|
}
|
||||||
|
return -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace MGITest::PipeStatsWindow
|
} // namespace MGITest::PipeStatsWindow
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include "HeadlessGL.h"
|
#include "HeadlessGL.h"
|
||||||
|
#include "SplitLane.h"
|
||||||
|
|
||||||
namespace MGITest {
|
namespace MGITest {
|
||||||
|
|
||||||
@@ -95,6 +96,24 @@ namespace MGITest {
|
|||||||
FAIL() << "MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU is set but the context landed on a software "
|
FAIL() << "MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU is set but the context landed on a software "
|
||||||
<< "rasterizer: " << gl.RendererString();
|
<< "rasterizer: " << gl.RendererString();
|
||||||
}
|
}
|
||||||
|
// P5's DirectGLES.Split. lanes, in ONE place rather than in each scenario they
|
||||||
|
// point at. A Split entry runs with MOBILEGL_TRANSPORT=inproc, and until packages
|
||||||
|
// c1/s1/v1 land there is nothing to consume that variable: ConfigLoader parses it,
|
||||||
|
// logs it, and the process then runs monolith. Every case in the lane would go GREEN
|
||||||
|
// against the monolith path under a name that says it tested the split one - the one
|
||||||
|
// outcome the whole lane exists to prevent, and the reason CONTRACT-P5 5 also
|
||||||
|
// demands a BUILD-level `nm ... | grep MG_Remote` check for the other half of this
|
||||||
|
// question. The registrations are never deleted (gate G14: a ctest name may never
|
||||||
|
// disappear); they skip, naming exactly what is missing.
|
||||||
|
//
|
||||||
|
// Here rather than in TriangleScenario/PersistentCoherentMapScenario because the
|
||||||
|
// Split family also points at ClearThenReadPixelsScenario, which is target A of the
|
||||||
|
// reduced path and predates P5 - and any later Split lane gets the same guarantee
|
||||||
|
// without anyone having to remember it.
|
||||||
|
if (const std::string splitSkip = SplitLane::SkipReasonForSplitOnlyAssertions();
|
||||||
|
SplitLane::IsSplitLane() && !splitSkip.empty()) {
|
||||||
|
GTEST_SKIP() << splitSkip;
|
||||||
|
}
|
||||||
// A scenario starts from a clean slate but shares the context (and so
|
// A scenario starts from a clean slate but shares the context (and so
|
||||||
// the renderer's memos) with every other scenario in this process -
|
// the renderer's memos) with every other scenario in this process -
|
||||||
// which is exactly the situation both shipped bugs needed.
|
// which is exactly the situation both shipped bugs needed.
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/SplitLane.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
|
||||||
|
//
|
||||||
|
// The four harness markers the `DirectGLES.Split.` ctest entries set, and the one question
|
||||||
|
// every Split case has to ask before it asserts anything.
|
||||||
|
//
|
||||||
|
// WHY A MARKER AND NOT MOBILEGL_TRANSPORT. The library's own variable says what was ASKED FOR,
|
||||||
|
// not what happened. In a build without -DMOBILEGL_BUILD_DISAGGREGATED the whole
|
||||||
|
// MOBILEGL_TRANSPORT parser does not exist (CONTRACT-P5.md 5: putting a complaint in the
|
||||||
|
// unconditional part of ConfigLoader would move a pull-build symbol and break G1), so
|
||||||
|
// MOBILEGL_TRANSPORT=inproc is accepted by the environment and SILENTLY IGNORED. A Split case
|
||||||
|
// that keyed off MOBILEGL_TRANSPORT would therefore run monolith and go green - which is
|
||||||
|
// precisely the failure the whole lane exists to make impossible. The build-level guard against
|
||||||
|
// that is `nm --defined-only libMobileGL.so | grep -i MG_Remote` in build-split, asserted by the
|
||||||
|
// gate's part 1 and by the build-linux-split CI job; the markers here are the TEST-level guard
|
||||||
|
// for the other half of the same question, "is there a client to assert about yet".
|
||||||
|
//
|
||||||
|
// MGITEST_SPLIT_LANE=1
|
||||||
|
// Set by the DirectGLES.Split.* entries and by nothing else. A scenario's split-only
|
||||||
|
// assertions are the ones that are meaningless in the ambient DirectGLES./DirectVulkan.
|
||||||
|
// lanes, and this is how a case tells the two apart in ONE binary that is registered many
|
||||||
|
// times over.
|
||||||
|
//
|
||||||
|
// MGITEST_REMOTE_CLIENT_PRESENT=1
|
||||||
|
// Set by a CMake content probe over MobileGL/MG_Remote/Client (package c1's directory), for
|
||||||
|
// a SYMBOL and never a filename - the reason is argued at length above
|
||||||
|
// mgl_itest_probe_for_symbol in MG_IntegrationTest/CMakeLists.txt: the owning package picks
|
||||||
|
// its own file layout, and a filename probe answers "no" forever the moment it moves the
|
||||||
|
// code. Until c1 lands there is no emitter, so `MOBILEGL_TRANSPORT=inproc` reaches a library
|
||||||
|
// that parses it, logs it and then runs monolith anyway. A Split entry in that state must
|
||||||
|
// SKIP NAMING THE MISSING THING; it must not be deleted (gate G14 - a ctest name may never
|
||||||
|
// disappear) and it must not go green.
|
||||||
|
//
|
||||||
|
// MGITEST_PERSISTENT_MAP_ARM=adopted|emulated
|
||||||
|
// The arm the LANE declares. AcquireMemoryRange adopts a PERSISTENT|WRITE map that is not
|
||||||
|
// FLUSH_EXPLICIT whenever the resource owner mints one (BufferObject.cpp:645-661), and
|
||||||
|
// declines to the shadow otherwise - two completely different code paths, chosen by the
|
||||||
|
// driver and the build rather than by the test, and MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION
|
||||||
|
// does NOT separate them (it guards TryAdoptLargeStorage's 16 MiB path, which a
|
||||||
|
// scenario-sized buffer never reaches at all). So the lane states which arm it expects and
|
||||||
|
// the scenario asserts it landed there. R-6 pins the split lane at T2 = emulated.
|
||||||
|
//
|
||||||
|
// MGITEST_SPLIT_EXPECT_TRANSPORT=inproc|monolith
|
||||||
|
// What the lane expects MG_Config::Transport to have resolved to, for the case that reads
|
||||||
|
// the library's own log back. Only meaningful in a lane that gave itself a private
|
||||||
|
// MOBILEGL_LOG_FILE_PATH.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace MGITest::SplitLane {
|
||||||
|
|
||||||
|
inline std::string MarkerValue(const char* name) {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
return (value != nullptr) ? std::string(value) : std::string();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool MarkerIsOne(const char* name) { return MarkerValue(name) == "1"; }
|
||||||
|
|
||||||
|
// True in the DirectGLES.Split.* entries only.
|
||||||
|
inline bool IsSplitLane() { return MarkerIsOne("MGITEST_SPLIT_LANE"); }
|
||||||
|
|
||||||
|
// True once any source under MG_Remote/Client names one of the symbols CONTRACT-P5 fixes
|
||||||
|
// for it. See the probe in CMakeLists.txt.
|
||||||
|
inline bool RemoteClientPresent() { return MarkerIsOne("MGITEST_REMOTE_CLIENT_PRESENT"); }
|
||||||
|
|
||||||
|
// Empty when this case may assert; otherwise the reason to GTEST_SKIP() with. The reason
|
||||||
|
// is spelled out rather than summarised because a skip line is the only thing anyone reads
|
||||||
|
// when they ask "did the split lane actually run".
|
||||||
|
inline std::string SkipReasonForSplitOnlyAssertions() {
|
||||||
|
if (!IsSplitLane()) {
|
||||||
|
return "not the split lane (MGITEST_SPLIT_LANE is unset): this case's split-only "
|
||||||
|
"assertions are about MOBILEGL_TRANSPORT=inproc and say nothing in a monolith "
|
||||||
|
"process";
|
||||||
|
}
|
||||||
|
if (!RemoteClientPresent()) {
|
||||||
|
return "MobileGL/MG_Remote/Client does not exist yet - no source there names "
|
||||||
|
"kRemoteEmitSlotCount / BackendObject_Remote / CapsMirror, so package c1 has "
|
||||||
|
"not landed and MOBILEGL_TRANSPORT=inproc reaches a library that parses it and "
|
||||||
|
"then runs monolith. Passing here would be a green that means 'the thing I test "
|
||||||
|
"does not exist yet'; the entry stays registered (G14) and skips instead";
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// "adopted", "emulated", or empty when the lane declared nothing.
|
||||||
|
inline std::string DeclaredPersistentMapArm() { return MarkerValue("MGITEST_PERSISTENT_MAP_ARM"); }
|
||||||
|
|
||||||
|
// "inproc", "monolith", or empty when the lane declared nothing.
|
||||||
|
inline std::string DeclaredTransport() { return MarkerValue("MGITEST_SPLIT_EXPECT_TRANSPORT"); }
|
||||||
|
|
||||||
|
} // namespace MGITest::SplitLane
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PersistentCoherentMapScenario.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 - A PERSISTENT|WRITE|COHERENT MAPPING, WRITTEN THROUGH WITH NO GL CALL ANNOUNCING IT.
|
||||||
|
//
|
||||||
|
// Target C of P5's reduced path, spelled out in full at docs/Disaggregated/ARCHITECTURE.md:500:
|
||||||
|
// map PERSISTENT|WRITE|COHERENT, write through the pointer, MAKE NO OTHER GL CALL, draw, read
|
||||||
|
// back and check.
|
||||||
|
//
|
||||||
|
// THE "NO OTHER GL CALL" IS THE ENTIRE POINT and it is exit gate E3(b). Under the split shape the
|
||||||
|
// server has no access to the client's address space, so a coherent mapping that the application
|
||||||
|
// writes into is bytes nobody told anyone about: the client tracker has to push the dirty blocks
|
||||||
|
// at EVERY VALIDATE POINT, on its own initiative, because no glBufferSubData, no
|
||||||
|
// glFlushMappedBufferRange and no unmap will ever come. A scenario that slipped in one announcing
|
||||||
|
// call - even a glGetError between the write and the draw - would let a "push on the next explicit
|
||||||
|
// buffer operation" implementation pass, which is the implementation this gate exists to reject.
|
||||||
|
// So the write/draw pairs below are exactly `std::memcpy(...)` followed by `glDrawArrays(...)`
|
||||||
|
// with nothing in between, and the sequence is map, write, draw, WRITE AGAIN, draw again, read
|
||||||
|
// back: the second write is the one that cannot have been carried by anything the first one did.
|
||||||
|
//
|
||||||
|
// WHICH ARM IS THIS RUNNING ON. A scenario-sized buffer is far below the 16 MiB
|
||||||
|
// kLargeBufferAdoptBytes, so BufferObject::TryAdoptLargeStorage never fires here - but
|
||||||
|
// AcquireMemoryRange has an adoption of its OWN that fires for any PERSISTENT|WRITE map that is
|
||||||
|
// not FLUSH_EXPLICIT (BufferObject.cpp:645-661). So the same test lands in the ADOPTED arm (the
|
||||||
|
// resource owner minted host-visible coherent storage, the CPU shadow was released, the
|
||||||
|
// application writes straight into GPU-visible memory) or in the EMULATED arm (the owner
|
||||||
|
// declined, the shadow is the truth, and the client has to push blocks) depending on driver and
|
||||||
|
// build - and those are completely different code paths. MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION
|
||||||
|
// does NOT separate them: it guards the 16 MiB path this buffer never reaches.
|
||||||
|
//
|
||||||
|
// So the lane DECLARES the arm (MGITEST_PERSISTENT_MAP_ARM, Harness/SplitLane.h) and
|
||||||
|
// TheMapLandsInTheArmItsLaneDeclares asserts it landed there, reading the library's own summary
|
||||||
|
// line. R-6 pins the split arm at T2 = emulated. The observable that separates them is
|
||||||
|
// `pmap` (persistent-map-push bytes, PipeStats ByteClass::PersistentMapPush): a push happens if
|
||||||
|
// and only if the acquisition was DECLINED, so pmap > 0 IS the black-box spelling of exit gate
|
||||||
|
// E3(d), "MGPipeApplyMapPersistent never returns a non-null pointer under split". It is the only
|
||||||
|
// spelling available from here - an adopted inproc store still renders correctly, because the
|
||||||
|
// address really is valid in this process, which is precisely why E3(d) is worth gating at all -
|
||||||
|
// and a direct count of declines would need a counter from packages b1/v1.
|
||||||
|
//
|
||||||
|
// `mpr` (map-persistent-roundtrips) is counted per ACQUISITION ATTEMPT, mint or decline
|
||||||
|
// (ARCHITECTURE.md:492), so it is the SAME NUMBER on both arms and in both transports: that is
|
||||||
|
// what makes exit gate E3(c)'s "mpr equal to the monolith arm's" checkable by one process. Both
|
||||||
|
// lanes assert the same constant against the same workload, so the equality holds by
|
||||||
|
// construction and each lane can fail on its own.
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/PipeStatsWindow.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
#include "../Harness/SplitLane.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 {
|
||||||
|
|
||||||
|
constexpr const char* kVertexSource = 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* kFragmentSource = R"(#version 330 core
|
||||||
|
in vec3 vColor;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() { oColor = vec4(vColor, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
float r, g, b;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A full-viewport quad as two triangles, flat-coloured: one region readback then speaks
|
||||||
|
// for the whole draw, and an offender pixel is a real disagreement rather than an
|
||||||
|
// interpolation difference between two drivers.
|
||||||
|
std::array<Vertex, 6> Quad(float r, float g, float b) {
|
||||||
|
return {{
|
||||||
|
{-1.f, -1.f, r, g, b},
|
||||||
|
{1.f, -1.f, r, g, b},
|
||||||
|
{1.f, 1.f, r, g, b},
|
||||||
|
{-1.f, -1.f, r, g, b},
|
||||||
|
{1.f, 1.f, r, g, b},
|
||||||
|
{-1.f, 1.f, r, g, b},
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr GLsizeiptr kQuadBytes = GLsizeiptr(sizeof(Vertex) * 6);
|
||||||
|
constexpr GLbitfield kCoherentFlags = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT;
|
||||||
|
|
||||||
|
// The ctest entry that reads the summary line sets this and nothing else does; the case
|
||||||
|
// skips everywhere else rather than racing for the lane's log (PipeStatsWindow.h).
|
||||||
|
constexpr const char* kCounterLaneMarker = "MGITEST_PMAP_LANE";
|
||||||
|
|
||||||
|
// Draws issued against the mapping inside the counted window. One mapping, many draws:
|
||||||
|
// "one per acquisition" (1) and "one per draw" (kDrawsInTheWindow) have to be different
|
||||||
|
// numbers or the assertion cannot tell them apart.
|
||||||
|
constexpr int kDrawsInTheWindow = 4;
|
||||||
|
|
||||||
|
// Acquisitions the counted window performs: exactly one glMapBufferRange of exactly one
|
||||||
|
// freshly defined immutable store.
|
||||||
|
constexpr long long kExpectedRoundtrips = 1;
|
||||||
|
|
||||||
|
bool BuildMarkerIsSet(const char* name) {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
return value != nullptr && value[0] == '1' && value[1] == '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// A store defined with the three bits and mapped once, or a reason the driver refused.
|
||||||
|
// Returns false WITHOUT touching gtest state, so it can be called from a helper: a
|
||||||
|
// GTEST_SKIP() inside a value-returning function does not compile (the macro expands to
|
||||||
|
// a bare `return`), and every earlier attempt to hide one in a factory grew a second
|
||||||
|
// "did it skip" channel that drifted from the first.
|
||||||
|
struct CoherentStore {
|
||||||
|
unsigned int vbo = 0;
|
||||||
|
void* map = nullptr;
|
||||||
|
std::string refusal;
|
||||||
|
};
|
||||||
|
|
||||||
|
CoherentStore MakeCoherentlyMappedQuadStore() {
|
||||||
|
CoherentStore store;
|
||||||
|
glGenBuffers(1, &store.vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, store.vbo);
|
||||||
|
glBufferStorage(GL_ARRAY_BUFFER, kQuadBytes, nullptr, kCoherentFlags);
|
||||||
|
if (FirstGLError() != 0u) {
|
||||||
|
glDeleteBuffers(1, &store.vbo);
|
||||||
|
store.vbo = 0;
|
||||||
|
store.refusal =
|
||||||
|
"this driver has no immutable storage with GL_MAP_PERSISTENT_BIT|GL_MAP_COHERENT_BIT";
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
// THE ONE ACQUISITION. Everything below writes through this pointer and never maps
|
||||||
|
// again: AcquireMemoryRange only attempts an adoption while the store is not yet
|
||||||
|
// resident, so a second map would emit a second map_persistent and make the counted
|
||||||
|
// window's expected mpr a function of how often the test remapped.
|
||||||
|
store.map = glMapBufferRange(GL_ARRAY_BUFFER, 0, kQuadBytes, kCoherentFlags);
|
||||||
|
if (store.map == nullptr || FirstGLError() != 0u) {
|
||||||
|
glDeleteBuffers(1, &store.vbo);
|
||||||
|
store.vbo = 0;
|
||||||
|
store.map = nullptr;
|
||||||
|
store.refusal = "glMapBufferRange(PERSISTENT|WRITE|COHERENT) was refused";
|
||||||
|
}
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DescribeAttributes() {
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
|
||||||
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||||
|
reinterpret_cast<void*>(2 * sizeof(float)));
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PersistentCoherentMapScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
// Ready() is false both when there is no GPU and when the base SetUp skipped a
|
||||||
|
// Split lane that has no client to assert against (ScenarioFixture.h).
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
const CoherentStore store = MakeCoherentlyMappedQuadStore();
|
||||||
|
if (!store.refusal.empty()) {
|
||||||
|
GTEST_SKIP() << store.refusal;
|
||||||
|
}
|
||||||
|
m_vbo = store.vbo;
|
||||||
|
m_map = store.map;
|
||||||
|
DescribeAttributes();
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "configuring the VAO over the coherently mapped store";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glBindVertexArray(0);
|
||||||
|
if (m_vbo != 0) {
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
if (m_map != nullptr) glUnmapBuffer(GL_ARRAY_BUFFER);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
glDeleteBuffers(1, &m_vbo);
|
||||||
|
}
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
m_vbo = m_vao = m_program = 0;
|
||||||
|
m_map = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything the draw needs, set once, so that the write/draw pairs below are
|
||||||
|
// memcpy + glDrawArrays and nothing else.
|
||||||
|
void ArmTheDrawState() {
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, gl.Width(), gl.Height());
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "arming the draw state";
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE WHOLE CONTRACT, IN TWO STATEMENTS. Nothing may be inserted between them - no
|
||||||
|
// glGetError, no glBindBuffer, no assertion that calls into GL. See the file header.
|
||||||
|
static void WriteThroughTheMapThenDraw(void* map, float r, float g, float b) {
|
||||||
|
const std::array<Vertex, 6> quad = Quad(r, g, b);
|
||||||
|
std::memcpy(map, quad.data(), sizeof(Vertex) * quad.size());
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The whole surface minus an 8-pixel inset, so a primitive edge cannot contribute an
|
||||||
|
// offender.
|
||||||
|
::testing::AssertionResult WholeSurfaceIs(const Image& image, const char* color,
|
||||||
|
const std::string& when) {
|
||||||
|
return RegionIsMostly(image, 8, image.Width() - 9, 8, image.Height() - 9, color, 0.0, when);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int m_program = 0;
|
||||||
|
unsigned int m_vao = 0;
|
||||||
|
unsigned int m_vbo = 0;
|
||||||
|
void* m_map = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// E3(b): map, write, draw, WRITE AGAIN, draw again, read back. The second write is announced
|
||||||
|
// by nothing at all, so an implementation that pushed on an explicit buffer operation fails
|
||||||
|
// here and only here.
|
||||||
|
TEST_F(PersistentCoherentMapScenario, TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
ArmTheDrawState();
|
||||||
|
|
||||||
|
WriteThroughTheMapThenDraw(m_map, 0.0f, 1.0f, 0.0f);
|
||||||
|
const Image afterFirst = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_TRUE(WholeSurfaceIs(afterFirst, "green",
|
||||||
|
"the first write through a PERSISTENT|WRITE|COHERENT mapping, with "
|
||||||
|
"no GL call announcing it"));
|
||||||
|
|
||||||
|
// The second write. Whatever carried the first one - an unmap, a flush, the definition
|
||||||
|
// itself - is in the past; only a push taken at this draw's validate point can carry it.
|
||||||
|
WriteThroughTheMapThenDraw(m_map, 1.0f, 0.0f, 0.0f);
|
||||||
|
const Image afterSecond = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_TRUE(WholeSurfaceIs(afterSecond, "red",
|
||||||
|
"the SECOND write through the same mapping, announced by nothing: "
|
||||||
|
"this is exit gate E3(b), and a 'push on the next explicit buffer "
|
||||||
|
"operation' implementation reads back the FIRST write's colour here"));
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same claim across a frame boundary, because the dirty-block set is per-frame state on
|
||||||
|
// the client and a tracker that cleared it at Present without pushing would pass the case
|
||||||
|
// above and fail this one.
|
||||||
|
TEST_F(PersistentCoherentMapScenario, AWriteAfterAFrameBoundaryReachesTheNextFramesDraw) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
ArmTheDrawState();
|
||||||
|
|
||||||
|
WriteThroughTheMapThenDraw(m_map, 0.0f, 1.0f, 0.0f);
|
||||||
|
const Image first = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
EXPECT_TRUE(WholeSurfaceIs(first, "green", "frame 0's write through the mapping"));
|
||||||
|
Gl().EndFrame();
|
||||||
|
|
||||||
|
ArmTheDrawState();
|
||||||
|
WriteThroughTheMapThenDraw(m_map, 1.0f, 0.0f, 0.0f);
|
||||||
|
const Image second = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_TRUE(WholeSurfaceIs(second, "red",
|
||||||
|
"frame 1's write through the SAME mapping, after a Present"));
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// E3(c) and the black-box half of E3(d). ONE reading case, in a lane of its own, for the
|
||||||
|
// reason PipeStatsWindow.h gives: the library truncates its log per process, so two readers
|
||||||
|
// in one lane race under `ctest -j`.
|
||||||
|
TEST_F(PersistentCoherentMapScenario, TheMapLandsInTheArmItsLaneDeclares) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
if (!BuildMarkerIsSet(kCounterLaneMarker)) {
|
||||||
|
GTEST_SKIP() << "not the counting lane: " << kCounterLaneMarker
|
||||||
|
<< " is set only by the *.PersistentMapArm.* entries, which give this case "
|
||||||
|
"a private MOBILEGL_LOG_FILE_PATH and a RESOURCE_LOCK on it";
|
||||||
|
}
|
||||||
|
if (!BuildMarkerIsSet("MGITEST_PIPE_PUSH_BUILD")) {
|
||||||
|
GTEST_SKIP() << "pull build: mpr= lives in the cso[ bracket, which is compiled only "
|
||||||
|
"under MOBILEGL_PIPE_PUSH, so the summary line does not carry it";
|
||||||
|
}
|
||||||
|
if (!BuildMarkerIsSet("MGITEST_PIPE_RESOURCE_EMITTER_PRESENT")) {
|
||||||
|
GTEST_SKIP() << "no MG_Impl/Pipe source emits MapPersistentRoundtrips on this tree, so "
|
||||||
|
"mpr= is structurally zero and an assertion about it would be a "
|
||||||
|
"statement about nothing";
|
||||||
|
}
|
||||||
|
|
||||||
|
Gl().EndFrame(); // close the setup window; SetUp's own store and map go in it
|
||||||
|
|
||||||
|
// One acquisition inside the counted window: one fresh immutable store, mapped once.
|
||||||
|
const CoherentStore store = MakeCoherentlyMappedQuadStore();
|
||||||
|
if (!store.refusal.empty()) {
|
||||||
|
GTEST_SKIP() << store.refusal;
|
||||||
|
}
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, store.vbo);
|
||||||
|
DescribeAttributes();
|
||||||
|
ArmTheDrawState();
|
||||||
|
|
||||||
|
// ... and then a frame's worth of traffic through it. Four writes and four draws must
|
||||||
|
// still cost ONE acquisition.
|
||||||
|
for (int draw = 0; draw < kDrawsInTheWindow; ++draw) {
|
||||||
|
WriteThroughTheMapThenDraw(store.map, 0.0f, 1.0f, 0.0f);
|
||||||
|
}
|
||||||
|
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_TRUE(WholeSurfaceIs(image, "green",
|
||||||
|
"the draws inside the counted window; if they never landed, every "
|
||||||
|
"number below is 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, kExpectedRoundtrips)
|
||||||
|
<< "one PERSISTENT|WRITE|COHERENT map of one freshly defined store is ONE acquisition "
|
||||||
|
"attempt, mint or decline (ARCHITECTURE.md:492). This window mapped once and drew "
|
||||||
|
<< kDrawsInTheWindow << " times, so " << kExpectedRoundtrips << " is the whole cost; "
|
||||||
|
<< kDrawsInTheWindow
|
||||||
|
<< " would mean the acquisition moved onto the draw path. The number is counted as "
|
||||||
|
"ATTEMPTS precisely so that it is the SAME on the adopted and the emulated arm and "
|
||||||
|
"under both transports - which is what makes exit gate E3(c)'s 'mpr equal to the "
|
||||||
|
"monolith arm's' checkable by one process. It reported: "
|
||||||
|
<< window.line;
|
||||||
|
|
||||||
|
// The arm. pmap is bytes PUSHED because a persistently mapped range had to be published,
|
||||||
|
// and a push happens if and only if the acquisition was DECLINED - so pmap > 0 is the
|
||||||
|
// emulated arm and pmap == 0 is the adopted one.
|
||||||
|
const double pushedBytes = PipeStatsWindow::CounterAsDoubleOrAbsent(window, "pmap");
|
||||||
|
ASSERT_GE(pushedBytes, 0.0) << "the summary line carries no pmap= field: " << window.line;
|
||||||
|
const char* observedArm = (pushedBytes > 0.0) ? "emulated" : "adopted";
|
||||||
|
RecordProperty("persistent_map_arm", observedArm);
|
||||||
|
RecordProperty("persistent_map_push_bytes_per_frame", std::to_string(pushedBytes).c_str());
|
||||||
|
RecordProperty("map_persistent_roundtrips", std::to_string(roundtrips).c_str());
|
||||||
|
|
||||||
|
const std::string declared = SplitLane::DeclaredPersistentMapArm();
|
||||||
|
if (declared.empty()) {
|
||||||
|
// SUCCEED, not GTEST_SKIP. The mpr assertion above is this lane's real claim and it
|
||||||
|
// has already been made; the arm is the half it cannot assert, because WHICH arm
|
||||||
|
// AcquireMemoryRange takes for a sub-16-MiB map is a property of the driver (llvmpipe
|
||||||
|
// mints, a device may decline) and a monolith lane that pinned one would be red on
|
||||||
|
// hardware for a reason that is not a defect. A skip here would have thrown away the
|
||||||
|
// mpr result with it - and mpr is the number the split lane is compared against.
|
||||||
|
SUCCEED() << "this lane declares no MGITEST_PERSISTENT_MAP_ARM, so the arm is RECORDED "
|
||||||
|
"and not asserted, and the assertion this lane does make - one coherent "
|
||||||
|
"map costs one acquisition attempt - passed. Observed arm: "
|
||||||
|
<< observedArm << " (pmap=" << pushedBytes << ", mpr=" << roundtrips << "). "
|
||||||
|
<< window.line;
|
||||||
|
glBindVertexArray(0);
|
||||||
|
unsigned int recordedVbo = store.vbo;
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, recordedVbo);
|
||||||
|
glUnmapBuffer(GL_ARRAY_BUFFER);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
glDeleteBuffers(1, &recordedVbo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(declared, std::string(observedArm))
|
||||||
|
<< "the lane declared the " << declared << " arm and the library took the " << observedArm
|
||||||
|
<< " one (pmap=" << pushedBytes
|
||||||
|
<< "). R-6 pins the split lane at T2 = emulated, and the two arms are completely "
|
||||||
|
"different code paths: adopted means MGPipeApplyMapPersistent handed back a pointer "
|
||||||
|
"and the CPU shadow was released, emulated means it declined and the client has to "
|
||||||
|
"push blocks. Under inproc an ADOPTED store still renders correctly - the address is "
|
||||||
|
"real in this process - so pixels cannot tell them apart and this counter is the "
|
||||||
|
"only thing that can. That is exit gate E3(d) in the only spelling a black-box "
|
||||||
|
"scenario has. Line: "
|
||||||
|
<< window.line;
|
||||||
|
if (declared == "emulated") {
|
||||||
|
EXPECT_GT(pushedBytes, 0.0)
|
||||||
|
<< "the emulated arm pushes the mapping's dirty blocks at every validate point, so "
|
||||||
|
"a window with four writes and four draws through a live coherent mapping cannot "
|
||||||
|
"have pushed zero bytes. Zero here with MOBILEGL_IPC_PERSISTENT_BLOCK_KB at its "
|
||||||
|
"default is the push never being wired; zero with it set to 0 is exit gate "
|
||||||
|
"E3(a)'s negative control working as intended. Line: "
|
||||||
|
<< window.line;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int vbo = store.vbo;
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||||
|
glUnmapBuffer(GL_ARRAY_BUFFER);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
glDeleteBuffers(1, &vbo);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TriangleScenario.cpp
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - THE SMALLEST THING THAT DRAWS: one VBO, one program, one VAO, a clear, a
|
||||||
|
// VBO-backed glDrawArrays and a glReadPixels.
|
||||||
|
//
|
||||||
|
// This is target B of P5's reduced path (BRIEF-P5 4). It is an ORDINARY GL scenario and runs
|
||||||
|
// in every lane; the DirectGLES.Split. entries run the same two cases with
|
||||||
|
// MOBILEGL_TRANSPORT=inproc, where the same body is the smallest workload that crosses a real
|
||||||
|
// ring. Four things about its shape are decisions rather than defaults, and all four come from
|
||||||
|
// the measured verb census (~/w7/notes/p5/verb-census.md):
|
||||||
|
//
|
||||||
|
// 1. THE DRAW IS VBO-BACKED AND HAS NO CLIENT-ARRAY INDICES. glDrawArrays against a buffer
|
||||||
|
// bound to GL_ARRAY_BUFFER, never glDrawElements with a client pointer. That keeps
|
||||||
|
// kDrawHasUserIndices' MGHostSpan out of the first IPC frame entirely - the split filling of
|
||||||
|
// a host span is P8 - and it is why table 0 can pin kCapNeedsHostIndexBytes and
|
||||||
|
// kCapNeedsHostUboBytes at 0 for the whole of P5.
|
||||||
|
//
|
||||||
|
// 2. THERE IS NO glFlush, AND ITS ABSENCE IS THE POINT. `Flush` is not a GLFunctionsTable slot
|
||||||
|
// at all, and MG_Impl/GLImpl/Exporting/Definitions.cpp:111-112 makes glFlush() and glFinish()
|
||||||
|
// LITERALLY EMPTY BODIES - one MGLOG_D and a return. A scenario that called glFlush to order
|
||||||
|
// its readback would be ordering nothing and would still pass, which makes the ordering it
|
||||||
|
// believes in unfalsifiable. glReadPixels IS the ordering point: it is a blocking readback on
|
||||||
|
// both backends today and a SEG_REPLY round trip under split, so the pixels it returns are
|
||||||
|
// the pixels the draw produced or the case fails.
|
||||||
|
//
|
||||||
|
// 3. THE FIRST CASE ENDS WITH EndFrame(), DELIBERATELY. `Present` has ZERO call sites in
|
||||||
|
// MG_Impl - it is reached only through EGLImpl.cpp:178 -> BackendObject.cpp:396 - so a
|
||||||
|
// scenario that never swaps never touches it, and B would then exercise a STRICT SUBSET of
|
||||||
|
// what ClearThenReadPixelsScenario already covers. BRIEF-P5 4.B lists Present(67) among the
|
||||||
|
// catalogue rows this target needs, so the frame boundary is here on purpose: B is A's slot
|
||||||
|
// set plus DrawArrays, not minus Present.
|
||||||
|
//
|
||||||
|
// 4. THE SECOND CASE REDRAWS ACROSS A FRAME BOUNDARY WITHOUT REBUILDING ANYTHING. The VBO, the
|
||||||
|
// program and the VAO outlive the swap and only the clear colour changes. Under split that
|
||||||
|
// is the difference between "the client re-declares every object every frame" (which would
|
||||||
|
// pass a single-frame test and be the whole cost of the design) and a steady state; under
|
||||||
|
// monolith it is the backend's per-frame retire/aging path, which is exactly where P3a's
|
||||||
|
// respecify bug lived.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/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 {
|
||||||
|
|
||||||
|
// #version 330 core, because that is what the retrace lane's
|
||||||
|
// MESA_GLSL_VERSION_OVERRIDE pins and what every other scenario in this module that does
|
||||||
|
// not need a later feature uses.
|
||||||
|
constexpr const char* kVertexSource = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
in vec3 aColor;
|
||||||
|
out vec3 vColor;
|
||||||
|
void main() {
|
||||||
|
vColor = aColor;
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFragmentSource = R"(#version 330 core
|
||||||
|
in vec3 vColor;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() { oColor = vec4(vColor, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
float r, g, b;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A single triangle with its base at y = -0.8 and its apex at y = +0.8, so the
|
||||||
|
// interior box the cases assert on (the middle 10% of the width, a fifth of the way up)
|
||||||
|
// is far from every edge and the corner box they assert the CLEAR on is far outside it.
|
||||||
|
// Flat green: one colour over the whole primitive means an offender pixel is a real
|
||||||
|
// disagreement rather than an interpolation rounding difference between two drivers.
|
||||||
|
constexpr Vertex kTriangle[3] = {
|
||||||
|
{-0.8f, -0.8f, 0.0f, 1.0f, 0.0f},
|
||||||
|
{0.8f, -0.8f, 0.0f, 1.0f, 0.0f},
|
||||||
|
{0.0f, 0.8f, 0.0f, 1.0f, 0.0f},
|
||||||
|
};
|
||||||
|
|
||||||
|
class TriangleScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
// Ready() is false both when there is no GPU and when the base SetUp skipped a
|
||||||
|
// Split lane that has no client to assert against (ScenarioFixture.h).
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glGenBuffers(1, &m_vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(sizeof(kTriangle)), kTriangle, GL_STATIC_DRAW);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
|
||||||
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||||
|
reinterpret_cast<void*>(2 * sizeof(float)));
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "building the one VBO and one VAO this scenario has";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
m_vbo = m_vao = m_program = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear, draw, read back. No glFlush between the draw and the readback: see the
|
||||||
|
// file header, point 2.
|
||||||
|
Image ClearThenDrawThenRead(float clearR, float clearG, float clearB) {
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, gl.Width(), gl.Height());
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
ClearTo(clearR, clearG, clearB, 1.0f);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
return ReadPixels(gl.Width(), gl.Height());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The box inside the triangle: the middle tenth of the width, a fifth of the way up
|
||||||
|
// from the base, which is interior for the vertex set above at any surface size the
|
||||||
|
// harness uses.
|
||||||
|
void ExpectTriangleInterior(const Image& image, const char* color, const std::string& when) {
|
||||||
|
const int w = image.Width();
|
||||||
|
const int h = image.Height();
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, (w * 45) / 100, (w * 55) / 100, (h * 20) / 100,
|
||||||
|
(h * 30) / 100, color, 0.0, when));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The bottom-left corner, which is below the triangle's base and left of its left
|
||||||
|
// edge, so it carries the clear and nothing else.
|
||||||
|
void ExpectClearedCorner(const Image& image, const char* color, const std::string& when) {
|
||||||
|
const int w = image.Width();
|
||||||
|
const int h = image.Height();
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, 0, (w * 5) / 100, 0, (h * 5) / 100, color, 0.0, when));
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int m_program = 0;
|
||||||
|
unsigned int m_vao = 0;
|
||||||
|
unsigned int m_vbo = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The reduced path's target B, in one case: GetCaps (reached by the first glCompileShader of
|
||||||
|
// the context, not by any verb - verb-census trap 2), Clear, DrawArrays, ReadPixels, Present.
|
||||||
|
TEST_F(TriangleScenario, AVboBackedTriangleReachesReadPixels) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
const Image image = ClearThenDrawThenRead(0.0f, 0.0f, 1.0f);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
ExpectTriangleInterior(image, "green", "the interior of a VBO-backed glDrawArrays triangle");
|
||||||
|
ExpectClearedCorner(image, "blue", "the corner outside the triangle, which carries the clear");
|
||||||
|
|
||||||
|
// The frame boundary, deliberately (file header, point 3): this is the only thing in the
|
||||||
|
// scenario that reaches the Present slot.
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Steady state: the same VBO, program and VAO across a swap, with only the clear colour
|
||||||
|
// changing. Nothing is re-created, so a client that re-declared its objects every frame and
|
||||||
|
// a backend that lost them at the frame boundary both show up here and in no single-frame
|
||||||
|
// case.
|
||||||
|
TEST_F(TriangleScenario, TheSameVboAndVaoRedrawAcrossAFrameBoundary) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
const Image first = ClearThenDrawThenRead(0.0f, 0.0f, 1.0f);
|
||||||
|
ExpectTriangleInterior(first, "green", "frame 0's triangle");
|
||||||
|
ExpectClearedCorner(first, "blue", "frame 0's clear");
|
||||||
|
Gl().EndFrame();
|
||||||
|
|
||||||
|
// Second frame: black clear, nothing else touched.
|
||||||
|
const Image second = ClearThenDrawThenRead(0.0f, 0.0f, 0.0f);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
ExpectTriangleInterior(second, "green",
|
||||||
|
"frame 1's triangle, drawn from the SAME VBO and VAO with no "
|
||||||
|
"re-specification of either");
|
||||||
|
ExpectClearedCorner(second, "black", "frame 1's clear, which is the only thing that changed");
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
Reference in New Issue
Block a user