mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 08:38:30 +09:00
[Merge] (MGPipe, P5): integrate package t1
This commit is contained in:
@@ -736,6 +736,412 @@ jobs:
|
|||||||
path: /tmp/core.*
|
path: /tmp/core.*
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
# P5's split build. The verify pair cloned, because the problem is the same one: a third
|
||||||
|
# configuration of the same sources whose whole value depends on the option having taken.
|
||||||
|
#
|
||||||
|
# IT CANNOT RIDE ON build-linux'S ARTIFACT. build-linux passes no -DMOBILEGL_PIPE_PUSH, so it is
|
||||||
|
# a PULL build; MOBILEGL_BUILD_DISAGGREGATED implies MOBILEGL_PIPE_PUSH (the split path decodes
|
||||||
|
# into the MGPipe applier and PIPE_PUSH is what compiles the applier), so the split arm needs a
|
||||||
|
# build of its own exactly as the verify arm does.
|
||||||
|
#
|
||||||
|
# THE `nm` STEP IS THE POINT OF THIS JOB. In a build without the option, MG_Remote is not
|
||||||
|
# compiled at all and MOBILEGL_TRANSPORT's parser does not exist - so the variable is accepted by
|
||||||
|
# the environment and silently ignored (CONTRACT-P5 5). Every downstream lane would then run
|
||||||
|
# monolith and go green under a name that says split. CMake will not complain about a typo'd -D,
|
||||||
|
# so the build-level assertion is the only guard, and its absence is precisely what would make
|
||||||
|
# the whole split arm meaningless.
|
||||||
|
build-linux-split:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 120
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
|
contents: read
|
||||||
|
env:
|
||||||
|
BUILD_DIR: build-split
|
||||||
|
CCACHE_BASEDIR: ${{ github.workspace }}
|
||||||
|
CCACHE_COMPRESS: "true"
|
||||||
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
|
CCACHE_MAXSIZE: 4G
|
||||||
|
CCACHE_NOHASHDIR: "true"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Set Swap Space
|
||||||
|
uses: pierotofy/set-swap-space@v1.0
|
||||||
|
with:
|
||||||
|
swap-size-gb: 32
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
# recursive, and load-bearing here beyond the usual: MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
# SHADOWS ITSELF BACK TO OFF when 3rdparty/flatbuffers/include is missing
|
||||||
|
# (CMakeLists.txt:471-486, a normal variable rather than a cache force, deliberately).
|
||||||
|
# A shallow checkout would therefore configure cleanly, build a monolith library, and
|
||||||
|
# be caught only by the nm step below.
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Get CMake
|
||||||
|
uses: lukka/get-cmake@v4.3.3
|
||||||
|
|
||||||
|
- name: Restore ccache
|
||||||
|
uses: actions/cache/restore@v5
|
||||||
|
with:
|
||||||
|
path: .ccache
|
||||||
|
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-test-${{ github.job }}-ccache-
|
||||||
|
|
||||||
|
- name: Prepare Vulkan SDK
|
||||||
|
uses: humbletim/setup-vulkan-sdk@v1.2.1
|
||||||
|
with:
|
||||||
|
vulkan-query-version: 1.4.304.1
|
||||||
|
vulkan-components: Vulkan-Headers, Vulkan-Loader
|
||||||
|
vulkan-use-cache: true
|
||||||
|
|
||||||
|
- name: Update glslang external sources
|
||||||
|
working-directory: 3rdparty/glslang
|
||||||
|
run: python update_glslang_sources.py
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y ccache clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers ninja-build
|
||||||
|
|
||||||
|
- name: Show installed toolchain
|
||||||
|
run: |
|
||||||
|
ccache --version
|
||||||
|
clang-20 --version
|
||||||
|
clang++-20 --version
|
||||||
|
dpkg -l 'libc++*' 'libegl*' 'libgles*' 'mesa*' 'vulkan*' || true
|
||||||
|
|
||||||
|
- name: Configure CMake
|
||||||
|
# Release/INFO like the shipped build, for the same reason build-linux-verify gives.
|
||||||
|
# INFO specifically matters here: ConfigLoader logs the resolved transport at INFO and
|
||||||
|
# that line is what run_trace_case.cmake and the integration-split lane read back as
|
||||||
|
# proof the transport resolved in THIS process.
|
||||||
|
#
|
||||||
|
# _INPROC implies _DISAGGREGATED implies _PIPE_PUSH; all three are passed anyway, because
|
||||||
|
# an implication that is asserted in two places is an implication nobody has to remember.
|
||||||
|
run: |
|
||||||
|
cmake -S . -B "${BUILD_DIR}" -G Ninja \
|
||||||
|
-DCMAKE_C_COMPILER=clang-20 \
|
||||||
|
-DCMAKE_CXX_COMPILER=clang++-20 \
|
||||||
|
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||||
|
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
|
||||||
|
-DMOBILEGL_BUILD_TEST=ON \
|
||||||
|
-DMOBILEGL_BUILD_BENCHMARK=OFF \
|
||||||
|
-DMOBILEGL_BUILD_INTEGRATION_TEST=ON \
|
||||||
|
-DMOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/lvp_icd.json \
|
||||||
|
-DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
|
||||||
|
-DMOBILEGL_PIPE_PUSH=ON \
|
||||||
|
-DMOBILEGL_BUILD_DISAGGREGATED=ON \
|
||||||
|
-DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON \
|
||||||
|
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cmake --build "${BUILD_DIR}" --parallel "$(nproc)"
|
||||||
|
|
||||||
|
# `nm` and NOT `nm -D`, for the reason build-linux-verify spells out: everything under
|
||||||
|
# MG_Remote is a plain namespace symbol in a CXX_VISIBILITY_PRESET=hidden Release build and
|
||||||
|
# none of it reaches the dynamic table. The symbol count guards the remaining hole - a
|
||||||
|
# stripped library would make the grep fail for a third, silent reason.
|
||||||
|
#
|
||||||
|
# The mirror of this assertion already exists and is the G1 control: monolith-symbol-report
|
||||||
|
# asserts that a -DMOBILEGL_BUILD_DISAGGREGATED=OFF library defines NO MG_Remote symbol. The
|
||||||
|
# two together are ARCHITECTURE.md:524's surviving byte-level equality, in both directions.
|
||||||
|
- name: The split library really carries MG_Remote
|
||||||
|
run: |
|
||||||
|
test -f "${BUILD_DIR}/libMobileGL.so"
|
||||||
|
defined=$(nm --defined-only "${BUILD_DIR}/libMobileGL.so" | wc -l)
|
||||||
|
if [ "${defined}" -lt 1000 ]; then
|
||||||
|
echo "::error::nm --defined-only sees only ${defined} symbols in ${BUILD_DIR}/libMobileGL.so - it looks stripped, so the checks below could not have failed honestly"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
remote=$(nm --defined-only "${BUILD_DIR}/libMobileGL.so" | grep -c -i "MG_Remote" || true)
|
||||||
|
if [ "${remote}" -lt 1 ]; then
|
||||||
|
echo "::error::libMobileGL.so defines no MG_Remote symbol: -DMOBILEGL_BUILD_DISAGGREGATED=ON did not take (a typo'd -D is not a CMake error, and the option shadows itself OFF when 3rdparty/flatbuffers/include is missing). Every lane that consumes this artifact would run MONOLITH while claiming to run split, because the MOBILEGL_TRANSPORT parser does not exist in such a build and the variable is accepted and ignored."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# The transport parser itself, which is the symbol the runtime evidence depends on.
|
||||||
|
if ! nm --defined-only "${BUILD_DIR}/libMobileGL.so" | grep -q "MGPipeApply"; then
|
||||||
|
echo "::error::libMobileGL.so defines no MGPipeApply* entry point, so MOBILEGL_PIPE_PUSH did not take either and there is no applier for the split path to decode into"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "libMobileGL.so defines ${remote} MG_Remote symbol(s) and the MGPipe applier (${defined} defined symbols)"
|
||||||
|
|
||||||
|
- name: Show ccache stats
|
||||||
|
if: always()
|
||||||
|
run: ccache --show-stats
|
||||||
|
|
||||||
|
- name: Release superseded ccache entry
|
||||||
|
if: github.ref_name == github.event.repository.default_branch
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
CACHE_KEY: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
|
||||||
|
run: gh cache delete "${CACHE_KEY}" || true
|
||||||
|
|
||||||
|
- name: Save ccache
|
||||||
|
if: github.ref_name == github.event.repository.default_branch
|
||||||
|
continue-on-error: true
|
||||||
|
uses: actions/cache/save@v5
|
||||||
|
with:
|
||||||
|
path: .ccache
|
||||||
|
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
|
||||||
|
|
||||||
|
- name: Package Linux split runtime
|
||||||
|
run: |
|
||||||
|
mkdir -p ci-artifacts
|
||||||
|
mapfile -t SHARED_LIBS < <(find "${BUILD_DIR}" -type f \( -name '*.so' -o -name '*.so.*' \) -print | sort)
|
||||||
|
tar \
|
||||||
|
--exclude='*/CMakeFiles' \
|
||||||
|
--exclude='*.o' \
|
||||||
|
--exclude='*.a' \
|
||||||
|
--exclude='*.ninja*' \
|
||||||
|
--exclude='build.ninja' \
|
||||||
|
--exclude='cmake_install.cmake' \
|
||||||
|
-czf ci-artifacts/mobilegl-linux-runtime-split.tgz \
|
||||||
|
"${BUILD_DIR}/CTestTestfile.cmake" \
|
||||||
|
"${BUILD_DIR}/MobileGL/MG_Test" \
|
||||||
|
"${BUILD_DIR}/MobileGL/MG_IntegrationTest" \
|
||||||
|
"${SHARED_LIBS[@]}"
|
||||||
|
|
||||||
|
- name: Upload Linux split runtime
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: mobilegl-linux-runtime-split
|
||||||
|
path: ci-artifacts/mobilegl-linux-runtime-split.tgz
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
# The split lane itself: the three-arm shape ARCHITECTURE.md:521 asks for verbatim, plus the
|
||||||
|
# first real run of the five MG_Test/Wire suites, plus P5's two exit-gate negative controls.
|
||||||
|
integration-split:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 120
|
||||||
|
needs: build-linux-split
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Get CMake
|
||||||
|
uses: lukka/get-cmake@v4.3.3
|
||||||
|
|
||||||
|
- name: Install runtime dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libvulkan1 libegl1 libegl-mesa0 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
|
||||||
|
|
||||||
|
- name: Download Linux split runtime
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: mobilegl-linux-runtime-split
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Unpack Linux split runtime
|
||||||
|
run: |
|
||||||
|
tar -xzf mobilegl-linux-runtime-split.tgz
|
||||||
|
test -f build-split/libMobileGL.so
|
||||||
|
|
||||||
|
- name: Normalize CTest command paths
|
||||||
|
run: |
|
||||||
|
python - <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
for path in Path('build-split').rglob('CTestTestfile.cmake'):
|
||||||
|
text = path.read_text()
|
||||||
|
text = re.sub(r'"[^"]*/cmake-[^"]*/bin/cmake"', '"cmake"', text)
|
||||||
|
path.write_text(text)
|
||||||
|
PY
|
||||||
|
|
||||||
|
# THE FIRST REAL RUN OF MG_Test/Wire. Those five suites - Framing, Ring, InProcessTransport,
|
||||||
|
# ProtocolSmoke and FdPassing - are registered only under MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
# (MG_Test/CMakeLists.txt), and before this job existed NO cmake invocation anywhere in this
|
||||||
|
# workflow passed that option. They had never been compiled by CI, let alone run.
|
||||||
|
- name: Unit tests on the split runtime
|
||||||
|
working-directory: build-split
|
||||||
|
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)"
|
||||||
|
|
||||||
|
# --no-tests=error is half the gate, exactly as in integration-verify: the integration-split
|
||||||
|
# entries exist only when the library was configured with -DMOBILEGL_BUILD_DISAGGREGATED=ON,
|
||||||
|
# so a build that lost the option matches nothing and reds here instead of reporting a green
|
||||||
|
# run of nothing. The other half is the transport-resolution check below.
|
||||||
|
#
|
||||||
|
# An entry whose owning package (c1 client, s1 session, v1 server) has not landed SKIPS with
|
||||||
|
# the reason and never goes green - MG_IntegrationTest/CMakeLists.txt probes MG_Remote for
|
||||||
|
# c0's signature stubs and disarms the lane while any remain.
|
||||||
|
- name: Split scenarios under MOBILEGL_TRANSPORT=inproc
|
||||||
|
working-directory: build-split
|
||||||
|
env:
|
||||||
|
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||||
|
run: |
|
||||||
|
ulimit -c unlimited
|
||||||
|
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||||
|
ctest --output-on-failure -L integration-split --no-tests=error -j 4
|
||||||
|
|
||||||
|
# ARCHITECTURE.md:521 asks for `ctest -L integration-gpu` to be name-for-name identical
|
||||||
|
# between the monolith and the split arm of the SAME build - the G2 shape extended to a
|
||||||
|
# third arm. The knob goes in the JOB environment rather than in a ctest property, for the
|
||||||
|
# reason the MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH precedent in `integration` gives: a
|
||||||
|
# property would override it and the arm would not be an arm.
|
||||||
|
#
|
||||||
|
# The entries that name MOBILEGL_TRANSPORT in their OWN property (the Split. lanes) keep
|
||||||
|
# their value in both passes, which is correct: they are the split family in both arms and
|
||||||
|
# the comparison is about the other 1100.
|
||||||
|
- name: The same integration entries under monolith and under inproc
|
||||||
|
working-directory: build-split
|
||||||
|
env:
|
||||||
|
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||||
|
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
|
||||||
|
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: "1"
|
||||||
|
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: "1"
|
||||||
|
run: |
|
||||||
|
ulimit -c unlimited
|
||||||
|
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||||
|
count=$(ctest -N -L integration-gpu | grep -cE '^ *Test *#[0-9]+:')
|
||||||
|
if [ "${count}" -lt 1 ]; then
|
||||||
|
echo "::error::the split runtime registers ${count} integration-gpu entries"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "integration-gpu entries in the split build: ${count}"
|
||||||
|
MOBILEGL_TRANSPORT=monolith ctest --output-on-failure -L integration-gpu --no-tests=error -j 4 --output-junit "${RUNNER_TEMP}/arm-monolith.xml"
|
||||||
|
MOBILEGL_TRANSPORT=inproc ctest --output-on-failure -L integration-gpu --no-tests=error -j 4 --output-junit "${RUNNER_TEMP}/arm-inproc.xml"
|
||||||
|
# NAME **AND STATUS**, and it is the comparison this step claimed to make and did not
|
||||||
|
# (review finding N-3): the first version wrote a names file and never read it, and
|
||||||
|
# `--output-on-failure` treats a SKIPPED test as not-a-failure - so the very failure
|
||||||
|
# ARCHITECTURE.md:521 is about, "an inproc arm that skipped forty entries the monolith arm
|
||||||
|
# ran", was invisible here and caught only by the local gate. `ctest -N` cannot see it
|
||||||
|
# either: this is one build directory, so the two arms have identical name lists by
|
||||||
|
# construction and the difference is entirely in what each entry DID.
|
||||||
|
python3 - "${RUNNER_TEMP}/arm-monolith.xml" "${RUNNER_TEMP}/arm-inproc.xml" <<'PY'
|
||||||
|
import sys, xml.etree.ElementTree as ET
|
||||||
|
def rows(path):
|
||||||
|
out = {}
|
||||||
|
for case in ET.parse(path).getroot().iter('testcase'):
|
||||||
|
status = 'passed'
|
||||||
|
if case.find('failure') is not None or case.find('error') is not None:
|
||||||
|
status = 'failed'
|
||||||
|
elif case.find('skipped') is not None or case.get('status') in ('notrun', 'disabled'):
|
||||||
|
status = 'skipped'
|
||||||
|
out[case.get('name')] = status
|
||||||
|
return out
|
||||||
|
a, b = rows(sys.argv[1]), rows(sys.argv[2])
|
||||||
|
diff = sorted(set(a) ^ set(b)) + sorted(n for n in set(a) & set(b) if a[n] != b[n])
|
||||||
|
if diff:
|
||||||
|
for name in diff[:40]:
|
||||||
|
print(f"::error::{name}: monolith={a.get(name, '<absent>')} inproc={b.get(name, '<absent>')}")
|
||||||
|
print(f"::error::the monolith and inproc arms of ctest -L integration-gpu differ on "
|
||||||
|
f"{len(diff)} entries. ARCHITECTURE.md:521 requires them identical name for name "
|
||||||
|
f"AND status; an entry that SKIPPED on one arm and ran on the other is the "
|
||||||
|
f"failure this compares for, and it is not a failure to --output-on-failure.")
|
||||||
|
raise SystemExit(1)
|
||||||
|
print(f"the two arms agree on all {len(a)} entries, name and status")
|
||||||
|
PY
|
||||||
|
|
||||||
|
# THE RUNTIME HALF OF "THIS IS REALLY A SPLIT BUILD". The build-level nm check in
|
||||||
|
# build-linux-split proves the library CARRIES MG_Remote; this proves the transport
|
||||||
|
# RESOLVED in a process of this lane. ConfigLoader::InitTransport logs one INFO line when it
|
||||||
|
# selects InProcess, and the DirectGLES.Split.PersistentMapArm. entry is the one Split entry
|
||||||
|
# with a MOBILEGL_LOG_FILE_PATH of its own (nothing else writes it, so the grep means what it
|
||||||
|
# says). The line is written during bring-up, before any scenario decides to skip, so this
|
||||||
|
# check is live from the day the lanes land rather than from the day they stop skipping.
|
||||||
|
- name: The split lane really resolved the transport
|
||||||
|
working-directory: build-split
|
||||||
|
run: |
|
||||||
|
log=MobileGL/MG_IntegrationTest/persistent-map-arm-split-DirectGLES.log
|
||||||
|
if [ ! -f "${log}" ]; then
|
||||||
|
echo "::error::${log} does not exist: the DirectGLES.Split.PersistentMapArm. entry never ran, so nothing in this job establishes that MOBILEGL_TRANSPORT ever resolved to inproc in a live process"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# THE DISTINCTIVE PART OF THE INFO LINE, not the bare KEY=VALUE (review finding M-5):
|
||||||
|
# ConfigLoader logs `Config: Accepted env variable: MOBILEGL_TRANSPORT=inproc` for the
|
||||||
|
# env dump too, unconditionally and in a PULL build, and at DEBUG that line is live.
|
||||||
|
if ! grep -q "MOBILEGL_TRANSPORT=inproc - the MGPipe record stream" "${log}"; then
|
||||||
|
echo "::error::${log} carries no transport-resolution line. ConfigLoader::InitTransport logs it at INFO when it selects InProcess, and that code exists only in a MOBILEGL_BUILD_DISAGGREGATED build - so this lane ran a monolith library while claiming to be the split lane. (A bare MOBILEGL_TRANSPORT=inproc substring is NOT accepted: the env dump prints one in every build.)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "the split lane resolved MOBILEGL_TRANSPORT=inproc"
|
||||||
|
|
||||||
|
# EXIT GATE E1's NEGATIVE CONTROL and EXIT GATE E3(a)'s, in one step because they have the
|
||||||
|
# same two-state shape and the same reason for it.
|
||||||
|
#
|
||||||
|
# Both controls turn a knob that MUST make a split scenario red: MOBILEGL_IPC_VERB_BARRIER=0
|
||||||
|
# removes the lockstep fence R-1 rests on, and MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 turns the
|
||||||
|
# persistent-map push off. A control of the verify lane's shape - "this step passes when
|
||||||
|
# ctest FAILS" - cannot be written yet, because while packages c1/s1/v1 are landing the
|
||||||
|
# Split entries SKIP and ctest reports green whatever the knob says, so an unconditional
|
||||||
|
# control would be red for the whole of P5 for a reason that is not a defect.
|
||||||
|
#
|
||||||
|
# So the expected state is DERIVED rather than assumed, from the same fact the lanes derive
|
||||||
|
# it from: MG_IntegrationTest/CMakeLists.txt puts MGITEST_REMOTE_CLIENT_PRESENT=1 into the
|
||||||
|
# Split entries' ENVIRONMENT exactly when MG_Remote carries no c0 signature stub, and that
|
||||||
|
# string is in the generated ctest include files this artifact ships. When it is there the
|
||||||
|
# controls MUST fire; when it is not, the step says so loudly and does not pretend.
|
||||||
|
- name: Negative controls - the verb barrier and the persistent-map push must be load-bearing
|
||||||
|
working-directory: build-split
|
||||||
|
env:
|
||||||
|
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||||
|
run: |
|
||||||
|
# THE ARMED STATE IS DERIVED FROM BEHAVIOUR, not from a marker string in the generated
|
||||||
|
# ctest files. The first version read MGITEST_REMOTE_CLIENT_PRESENT out of
|
||||||
|
# *_tests.cmake, which was a restatement of the CMake source probe review finding M-1
|
||||||
|
# falsified; the arming condition is now a runtime fact inside each test process, so the
|
||||||
|
# only honest way to ask it from a shell is to look at what the entries DID.
|
||||||
|
ctest -L integration-split -j 4 --no-tests=error --output-junit "${RUNNER_TEMP}/isplit.xml" || true
|
||||||
|
armed=$(python3 - "${RUNNER_TEMP}/isplit.xml" <<'PY'
|
||||||
|
import sys, xml.etree.ElementTree as ET
|
||||||
|
ran = 0
|
||||||
|
for case in ET.parse(sys.argv[1]).getroot().iter('testcase'):
|
||||||
|
if case.find('skipped') is None and case.get('status') not in ('notrun', 'disabled'):
|
||||||
|
ran += 1
|
||||||
|
print(ran)
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
echo "split entries that actually ran: ${armed}"
|
||||||
|
if [ "${armed}" -lt 1 ]; then
|
||||||
|
echo "::warning::every DirectGLES.Split. entry SKIPPED, so neither negative control can fire. The arming condition is a runtime fact - MG_Config::Transport, ClientSession::Active() and ImplementedVerbCount(), read by Harness/SplitRuntimePeek - and it becomes true on the commit that lands the last of c1/s1/v1. This step becomes a gate then, with no edit; it is not a green that asserted anything today."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
run_control() {
|
||||||
|
name="$1"; filter="$2"; shift 2
|
||||||
|
matched=$(ctest -N -L integration-split -R "${filter}" | grep -cE '^ *Test *#[0-9]+:')
|
||||||
|
if [ "${matched}" -lt 1 ]; then
|
||||||
|
echo "::error::${name} selected ${matched} tests; its filter no longer matches anything"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if env "$@" ctest --output-on-failure -L integration-split -R "${filter}" --no-tests=error; then
|
||||||
|
echo "::error::${name} left ${matched} split entries GREEN, so the knob it turns is not load-bearing and the gate it controls proves nothing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "${name} turned ${matched} selected entries red, as it must"
|
||||||
|
}
|
||||||
|
# E1: R-1's lockstep verb barrier. Without it the client keeps pulling fields from a live
|
||||||
|
# GLContext while the server runs ahead, so the server reads future values.
|
||||||
|
run_control "negative control E1 (MOBILEGL_IPC_VERB_BARRIER=0)" \
|
||||||
|
'DirectGLES\.Split\.(Triangle|ClearThenReadPixels)' MOBILEGL_IPC_VERB_BARRIER=0
|
||||||
|
# E3(a): the persistent-map push. 0 is admitted by ConfigLoader on purpose and is
|
||||||
|
# documented there as this control.
|
||||||
|
run_control "negative control E3(a) (MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0)" \
|
||||||
|
'DirectGLES\.Split\.PersistentCoherentMapScenario' MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0
|
||||||
|
|
||||||
|
- name: Upload split lane logs
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: integration-split-logs
|
||||||
|
path: build-split/MobileGL/MG_IntegrationTest/*.log*
|
||||||
|
if-no-files-found: warn
|
||||||
|
|
||||||
|
- name: Upload core dumps
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: integration-split-core-dumps
|
||||||
|
path: /tmp/core.*
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
# MobileGL/MG_Remote/Protocol/generated/protocol_generated.h is COMMITTED, and
|
# MobileGL/MG_Remote/Protocol/generated/protocol_generated.h is COMMITTED, and
|
||||||
# flatc is deliberately absent from the default build graph (a codegen step in
|
# flatc is deliberately absent from the default build graph (a codegen step in
|
||||||
# the graph is how the earlier branch ended up cross-compiling an arm64 flatc
|
# the graph is how the earlier branch ended up cross-compiling an arm64 flatc
|
||||||
@@ -1013,6 +1419,7 @@ jobs:
|
|||||||
matrix: ${{ steps.trace-cases.outputs.matrix }}
|
matrix: ${{ steps.trace-cases.outputs.matrix }}
|
||||||
names: ${{ steps.trace-cases.outputs.names }}
|
names: ${{ steps.trace-cases.outputs.names }}
|
||||||
verify-matrix: ${{ steps.trace-cases.outputs.verify-matrix }}
|
verify-matrix: ${{ steps.trace-cases.outputs.verify-matrix }}
|
||||||
|
split-matrix: ${{ steps.trace-cases.outputs.split-matrix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -1025,6 +1432,20 @@ jobs:
|
|||||||
# The subset the verify build retraces ("verify": true in trace_cases.json). It is a
|
# The subset the verify build retraces ("verify": true in trace_cases.json). It is a
|
||||||
# SUBSET of the matrix above, so retrace-verify needs no fixtures of its own.
|
# SUBSET of the matrix above, so retrace-verify needs no fixtures of its own.
|
||||||
echo "verify-matrix=$(python3 tools/trace_replay/trace_cases.py --ci --format github-verify-matrix)" >> "$GITHUB_OUTPUT"
|
echo "verify-matrix=$(python3 tools/trace_replay/trace_cases.py --ci --format github-verify-matrix)" >> "$GITHUB_OUTPUT"
|
||||||
|
# P5's split subset ("split": true, DirectGLES only). Also a SUBSET of the matrix above,
|
||||||
|
# so retrace-split needs no fixtures of its own either. It is one case today - OpenRA,
|
||||||
|
# which is what the phase gate names - and trace_cases.py refuses a `split` case that is
|
||||||
|
# not in CI or does not run DirectGLES, so the subset cannot silently become empty.
|
||||||
|
SPLIT_MATRIX=$(python3 tools/trace_replay/trace_cases.py --ci --format github-split-matrix)
|
||||||
|
# AND IT MUST NOT BE EMPTY. An empty `include` is not an error to GitHub - it skips the
|
||||||
|
# whole retrace-split job with no red anywhere - so the one way this subset can vanish
|
||||||
|
# silently is guarded here. trace_cases.py now also rejects an unknown manifest key, which
|
||||||
|
# was the hole: `"splitt": true` loaded clean and emptied the subset (review N-4).
|
||||||
|
if [ "$(printf '%s' "${SPLIT_MATRIX}" | python3 -c 'import json,sys; print(len(json.load(sys.stdin)["include"]))')" -lt 1 ]; then
|
||||||
|
echo "::error::the split retrace subset is EMPTY. No case in trace_cases.json carries \"split\": true, so retrace-split would be skipped with no red. Exit gate E2 names OpenRA."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "split-matrix=${SPLIT_MATRIX}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
trace-fixtures:
|
trace-fixtures:
|
||||||
name: trace fixture (${{ matrix.case }})
|
name: trace fixture (${{ matrix.case }})
|
||||||
@@ -1397,6 +1818,214 @@ jobs:
|
|||||||
build-retrace/tools/trace_replay/${{ matrix.case }}/${{ matrix.backend }}/output/**
|
build-retrace/tools/trace_replay/${{ matrix.case }}/${{ matrix.backend }}/output/**
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
|
|
||||||
|
# P5's retrace arm: retrace-verify with MOBILEGL_PIPE_VERIFY=1 swapped for
|
||||||
|
# MOBILEGL_TRANSPORT=inproc and the comparator's symbol swapped for an MG_Remote one. Exit gate
|
||||||
|
# E2 is one row - OpenRA at SSIM >= 0.99 - and trace_cases.json's `split: true` is where that
|
||||||
|
# subset lives.
|
||||||
|
#
|
||||||
|
# IT RUNS THE UNCHANGED CTEST NAMES with the transport exported in the JOB environment, rather
|
||||||
|
# than the SPLIT-suffixed variant entries. build-retrace configures without
|
||||||
|
# -DMOBILEGL_BUILD_DISAGGREGATED, so the variant entries are deliberately not registered there
|
||||||
|
# (a name that says SPLIT in a build that cannot be one is worse than no name), and the
|
||||||
|
# unchanged names are what retrace-verify already proves this shape works with. The variant
|
||||||
|
# entries exist for a build that DOES configure the option - a local build-split with trace
|
||||||
|
# replay on - where `ctest -L retrace-split` is self-describing and needs no environment ritual.
|
||||||
|
#
|
||||||
|
# WHAT MAKES IT FALSIFIABLE is not the SSIM. A monolith run of OpenRA also scores 1.000: measured
|
||||||
|
# on this branch, a pull library under MOBILEGL_TRANSPORT=inproc produced ssim=1.0 and was caught
|
||||||
|
# only by run_trace_case.cmake's transport-resolution assertion. So there are two guards, and the
|
||||||
|
# SSIM is neither of them: the nm check below (this library carries MG_Remote) and the
|
||||||
|
# transport-resolution line in the library's own log (this PROCESS resolved inproc).
|
||||||
|
retrace-split:
|
||||||
|
name: retrace split (${{ matrix.backend }}, ${{ matrix.case }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 240
|
||||||
|
needs:
|
||||||
|
- build-linux
|
||||||
|
- build-linux-split
|
||||||
|
- build-retrace
|
||||||
|
- trace-cases
|
||||||
|
- trace-fixtures
|
||||||
|
# build-linux is needed for the negative control ONLY: its pull library is what the control
|
||||||
|
# swaps in to prove the transport-resolution assertion is load-bearing.
|
||||||
|
if: ${{ always() && needs.build-linux.result == 'success' && needs.build-linux-split.result == 'success' && needs.build-retrace.result == 'success' && needs.trace-cases.result == 'success' }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 4
|
||||||
|
matrix: ${{ fromJSON(needs.trace-cases.outputs.split-matrix) }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Set Swap Space
|
||||||
|
uses: pierotofy/set-swap-space@v1.0
|
||||||
|
with:
|
||||||
|
swap-size-gb: 16
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Download trace fixture
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: trace-fixture-${{ matrix.case }}
|
||||||
|
path: trace-fixture-download
|
||||||
|
|
||||||
|
- name: Install trace fixture
|
||||||
|
run: |
|
||||||
|
mkdir -p tools/trace_replay/fixtures
|
||||||
|
find trace-fixture-download -type f -exec cp {} tools/trace_replay/fixtures/ \;
|
||||||
|
|
||||||
|
- name: Get CMake
|
||||||
|
uses: lukka/get-cmake@v4.3.3
|
||||||
|
|
||||||
|
- name: Install runtime dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libvulkan1 libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers
|
||||||
|
test -e /usr/lib/x86_64-linux-gnu/libEGL.so
|
||||||
|
test -e /usr/lib/x86_64-linux-gnu/libGLESv2.so
|
||||||
|
|
||||||
|
- name: Download Linux split runtime
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: mobilegl-linux-runtime-split
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Download trace replay
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: mobilegl-trace-replay
|
||||||
|
path: .
|
||||||
|
|
||||||
|
# The PULL runtime, for the negative control at the end of this job and for nothing else.
|
||||||
|
- name: Download Linux pull runtime (negative control)
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: mobilegl-linux-runtime
|
||||||
|
path: pull-runtime
|
||||||
|
|
||||||
|
- name: Unpack the pull runtime (negative control)
|
||||||
|
run: |
|
||||||
|
tar -xzf pull-runtime/mobilegl-linux-runtime.tgz -C pull-runtime
|
||||||
|
test -f pull-runtime/build-linux/libMobileGL.so
|
||||||
|
|
||||||
|
- name: Unpack the SPLIT runtime as the library under test
|
||||||
|
# build-retrace's CTestTestfile.cmake has the absolute path
|
||||||
|
# <workspace>/build-linux/libMobileGL.so frozen into every case, so the swap happens here
|
||||||
|
# rather than through a variable, exactly as in retrace-verify. `nm`, not `nm -D`.
|
||||||
|
run: |
|
||||||
|
tar -xzf mobilegl-linux-runtime-split.tgz
|
||||||
|
tar -xzf mobilegl-trace-replay.tgz
|
||||||
|
test -f build-split/libMobileGL.so
|
||||||
|
test -f build-retrace/tools/trace_replay/mobilegl_trace_replay
|
||||||
|
mkdir -p build-linux
|
||||||
|
cp build-split/libMobileGL.so build-linux/libMobileGL.so
|
||||||
|
if ! nm --defined-only build-linux/libMobileGL.so | grep -q -i MG_Remote; then
|
||||||
|
echo "::error::the library unpacked at build-linux/libMobileGL.so defines no MG_Remote symbol, so this retrace would replay against a monolith build, ignore MOBILEGL_TRANSPORT entirely and match its golden having split nothing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "the library at build-linux/libMobileGL.so is the split build"
|
||||||
|
|
||||||
|
- name: Retrace and validate under MOBILEGL_TRANSPORT=inproc
|
||||||
|
working-directory: build-retrace/tools/trace_replay
|
||||||
|
# run_trace_case.cmake turns MOBILEGL_TRANSPORT into assertions of its own - the library
|
||||||
|
# must have RESOLVED the transport in this process, and its log must carry no Fatal{ - so a
|
||||||
|
# case that somehow ran the wrong library reds here instead of passing on its golden. That
|
||||||
|
# log is also the only valid refusal census: the console sink is compiled out of this
|
||||||
|
# configuration, so a `ctest -V` transcript reports a false zero for Fatal{ lines.
|
||||||
|
run: |
|
||||||
|
ulimit -c unlimited
|
||||||
|
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||||
|
export MOBILEGL_TRANSPORT=inproc
|
||||||
|
ctest -V --no-tests=error --timeout 10800 \
|
||||||
|
-R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
|
||||||
|
|
||||||
|
# THE RETRACE LANE'S OWN ALWAYS-ON NEGATIVE CONTROL, and its absence was review finding M-6:
|
||||||
|
# this job was a clone of retrace-verify with the one step removed that makes the lane mean
|
||||||
|
# anything. retrace-verify carries "a corrupted snapshot field must red this retrace" so that
|
||||||
|
# "79 traces, zero divergences" is not a statement about a comparator nobody watched; the
|
||||||
|
# same sentence applies here word for word.
|
||||||
|
#
|
||||||
|
# The control swaps the PULL library into the frozen path and requires the SAME replay to
|
||||||
|
# fail. It is the sharpest one available, because OpenRA scores ssim 1.000000 either way -
|
||||||
|
# measured - so this step fails if and only if run_trace_case.cmake's transport-resolution
|
||||||
|
# assertion has stopped working, which is the single thing standing between this job and a
|
||||||
|
# green that ran monolith end to end.
|
||||||
|
#
|
||||||
|
# NOT the control BRIEF 7 E2 names ("patch the Clear emitter to drop one emission; SSIM must
|
||||||
|
# fall below threshold"). That one needs an emitter, i.e. package c1, and it is carried as an
|
||||||
|
# explicit debt in t1-v1.md rather than silently substituted - which is what the first
|
||||||
|
# version of this package did.
|
||||||
|
#
|
||||||
|
# The rerun replays into the same case directory, so the good run's images are put aside and
|
||||||
|
# restored whichever way the control goes; "Upload actual image" below runs `if: always()`
|
||||||
|
# and would otherwise ship the deliberately-wrong run's output under the good run's name.
|
||||||
|
- name: Negative control - the PULL library must red this split retrace
|
||||||
|
working-directory: build-retrace/tools/trace_replay
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
GOOD_OUTPUT="${RUNNER_TEMP}/split-verified-output"
|
||||||
|
rm -rf "${GOOD_OUTPUT}"
|
||||||
|
if [ -d "${{ matrix.case }}" ]; then cp -a "${{ matrix.case }}" "${GOOD_OUTPUT}"; fi
|
||||||
|
# The pull library, unpacked from build-linux's artifact, over the frozen path every
|
||||||
|
# case has baked in. It defines no MG_Remote symbol, so ConfigLoader has no transport
|
||||||
|
# parser and MOBILEGL_TRANSPORT=inproc is accepted and ignored - the exact shape of "the
|
||||||
|
# split lane ran monolith".
|
||||||
|
cp "${GITHUB_WORKSPACE}/pull-runtime/build-linux/libMobileGL.so" \
|
||||||
|
"${GITHUB_WORKSPACE}/build-linux/libMobileGL.so"
|
||||||
|
if nm --defined-only "${GITHUB_WORKSPACE}/build-linux/libMobileGL.so" | grep -q -i MG_Remote; then
|
||||||
|
echo "::error::the control's own library defines MG_Remote symbols, so it is not a pull build and this control would prove nothing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
export MOBILEGL_TRANSPORT=inproc
|
||||||
|
ctest -V --no-tests=error --timeout 10800 \
|
||||||
|
-R "^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$"
|
||||||
|
control_rc=$?
|
||||||
|
set -e
|
||||||
|
if [ -d "${GOOD_OUTPUT}" ]; then
|
||||||
|
rm -rf "${{ matrix.case }}"; mv "${GOOD_OUTPUT}" "${{ matrix.case }}"
|
||||||
|
echo "restored the verified run's output over the control's"
|
||||||
|
fi
|
||||||
|
if [ "${control_rc}" -eq 0 ]; then
|
||||||
|
echo "::error::a PULL library passed the split retrace. OpenRA scores ssim 1.000000 under a monolith library too (measured), so the picture is not and cannot be this lane's gate - run_trace_case.cmake's transport-resolution assertion is, and it has stopped working. Every green in this job is then a monolith run under a name that says split."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "the pull library turned the split retrace red, as it must (ctest exit ${control_rc})"
|
||||||
|
|
||||||
|
# The refusal census, recorded rather than gated. run_trace_case.cmake already REDS the case
|
||||||
|
# on any Fatal{, so reaching here means the count is zero - but the number and the distinct
|
||||||
|
# slot names are what MEASUREMENTS wants from every split run, and reading them out of the
|
||||||
|
# library's own log is the only way to get them (ctest -V's transcript is a false zero).
|
||||||
|
- name: Refusal census from the library log
|
||||||
|
if: always()
|
||||||
|
working-directory: build-retrace/tools/trace_replay
|
||||||
|
run: |
|
||||||
|
log="${{ matrix.case }}/${{ matrix.backend }}/output/mobilegl.log"
|
||||||
|
if [ ! -f "${log}" ]; then
|
||||||
|
echo "no ${log} - the replay wrote no library log"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Fatal{ lines: $(grep -c 'Fatal{' "${log}" || true)"
|
||||||
|
grep -o 'Fatal{[A-Za-z]*, "[^"]*"' "${log}" | sort | uniq -c | sort -rn | head -20 || true
|
||||||
|
grep -m1 "MOBILEGL_TRANSPORT=" "${log}" || echo "no transport line in ${log}"
|
||||||
|
|
||||||
|
- name: Upload core dumps
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: retrace-split-core-dumps-${{ matrix.backend }}-${{ matrix.case }}
|
||||||
|
path: /tmp/core.*
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
- name: Upload actual image
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: retrace-split-result-${{ matrix.backend }}-${{ matrix.case }}
|
||||||
|
path: |
|
||||||
|
build-retrace/tools/trace_replay/${{ matrix.case }}/actual-images/**
|
||||||
|
build-retrace/tools/trace_replay/${{ matrix.case }}/${{ matrix.backend }}/output/**
|
||||||
|
if-no-files-found: warn
|
||||||
|
|
||||||
# G1's own job: the pull build must be the tree before P1, symbol for symbol and byte for byte.
|
# G1's own job: the pull build must be the tree before P1, symbol for symbol and byte for byte.
|
||||||
# workflow_dispatch only - it builds the library twice from scratch, and its answer is about a
|
# workflow_dispatch only - it builds the library twice from scratch, and its answer is about a
|
||||||
# BASELINE rather than about this push, so a per-push run would be measuring the wrong pair.
|
# BASELINE rather than about this push, so a per-push run would be measuring the wrong pair.
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Harness/PipeApplyPeek.cpp
|
Harness/PipeApplyPeek.cpp
|
||||||
Harness/P4aSeamPeek.cpp
|
Harness/P4aSeamPeek.cpp
|
||||||
Harness/P4aFinalFixPeek.cpp
|
Harness/P4aFinalFixPeek.cpp
|
||||||
|
Harness/PersistentMapPeek.cpp
|
||||||
|
Harness/SplitRuntimePeek.cpp
|
||||||
Scenarios/OrientationScenario.cpp
|
Scenarios/OrientationScenario.cpp
|
||||||
Scenarios/CrossFrameBufferScenario.cpp
|
Scenarios/CrossFrameBufferScenario.cpp
|
||||||
Scenarios/ResidentIndexScenario.cpp
|
Scenarios/ResidentIndexScenario.cpp
|
||||||
@@ -141,6 +143,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 +1764,232 @@ 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 ARMS THE SPLIT LANES, AND WHY IT IS NOT A PROBE OVER SOURCE TEXT.
|
||||||
|
#
|
||||||
|
# Until a real client session exists, MOBILEGL_TRANSPORT=inproc is parsed (CONTRACT-P5 5) and then
|
||||||
|
# nothing consumes it, so a Split entry would go green having run monolith end to end - the exact
|
||||||
|
# failure the lane exists to prevent. The first version of this file answered "has the client
|
||||||
|
# landed" with a CMake conjunction over c0's stub files: the c1 symbols present AND no
|
||||||
|
# `Fatal.Unimplemented` surviving. REVIEW FINDING M-1 FALSIFIED IT BY PERFORMING IT - a
|
||||||
|
# `sed -i 's/Fatal{Unimplemented/Fatal{NotYetImplemented/'` over c0's six stubs, with every entry
|
||||||
|
# point still aborting and ImplementedVerbCount() still 0, armed all eleven lanes and EIGHT WENT
|
||||||
|
# GREEN. A single comment line carrying the marker did the opposite and would keep them dark
|
||||||
|
# forever (M-2), and two stub files outside the two probed directories allowed a partial arm (M-3).
|
||||||
|
#
|
||||||
|
# A statement about source text can always be falsified by editing source text, and the people
|
||||||
|
# most likely to edit it are the ones landing the packages the probe watches for. So the arming
|
||||||
|
# condition moved into the PROCESS: Harness/SplitRuntimePeek.cpp reads MG_Config::Transport,
|
||||||
|
# ClientSession::Active() and ImplementedVerbCount() - three values c0 shipped and documented,
|
||||||
|
# none of which a message edit can move - and every Split case skips, naming the first one that is
|
||||||
|
# not true. All this file has to decide now is whether that peek can be COMPILED, which is exactly
|
||||||
|
# "did this build compile MG_Remote", which is exactly the build option.
|
||||||
|
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||||
|
target_compile_definitions(MobileGLIntegrationTest PRIVATE -DMGITEST_SPLIT_RUNTIME_PEEK=1)
|
||||||
|
message(STATUS "Integration tests: the DirectGLES.Split. lanes arm from the RUNNING PROCESS - "
|
||||||
|
"MG_Config::Transport, ClientSession::Active() and ImplementedVerbCount() - and "
|
||||||
|
"skip naming the first of those that is not true")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Q-3, made a build fact rather than documentation. ConfigLoader logs the resolved transport at
|
||||||
|
# INFO, and that line is what run_trace_case.cmake reads back as proof a retrace really went
|
||||||
|
# split. At WARN or above it is compiled out and every split retrace reds for a reason that is not
|
||||||
|
# a defect; at DEBUG, ConfigLoader's unconditional env dump prints a confusable KEY=VALUE line in
|
||||||
|
# a PULL build too (review M-5, also fixed on the reading side). The integration lanes themselves
|
||||||
|
# no longer depend on the log level at all - they read the variable - so this is a WARNING.
|
||||||
|
if (MOBILEGL_BUILD_DISAGGREGATED AND DEFINED MOBILEGL_LOG_ACTIVE_LEVEL
|
||||||
|
AND NOT MOBILEGL_LOG_ACTIVE_LEVEL STREQUAL "MOBILEGL_LOG_LEVEL_INFO"
|
||||||
|
AND NOT MOBILEGL_LOG_ACTIVE_LEVEL STREQUAL "MOBILEGL_LOG_LEVEL_DEBUG")
|
||||||
|
message(WARNING "Integration tests: MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL} in a "
|
||||||
|
"disaggregated build. ConfigLoader's transport line is MGLOG_I, so the "
|
||||||
|
"trace-replay split arm's transport-resolution assertion cannot see it and every "
|
||||||
|
"split retrace will red. Use MOBILEGL_LOG_LEVEL_INFO.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Package b1's client-side persistent-map tracker, which is what Harness/PersistentMapPeek.cpp
|
||||||
|
# asks the membership question of (b1-v1.md 4.1 item 2). TWO halves, and both are needed:
|
||||||
|
# MOBILEGL_BUILD_DISAGGREGATED because a build that never compiled MG_Remote cannot LINK the call,
|
||||||
|
# and the content probe because b1 may not have landed it yet. __has_include in the peek would
|
||||||
|
# answer yes in a pull build - the header is in the source tree of every build - and turn a
|
||||||
|
# healthy pull build into a link error, which is why this decision is made here.
|
||||||
|
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||||
|
mgl_itest_probe_for_symbol(MGL_ITEST_PERSISTENT_MAP_TRACKER
|
||||||
|
"${MGL_ITEST_ROOT}/MobileGL/MG_Remote/Client" "IsLivePersistentMap")
|
||||||
|
if (MGL_ITEST_PERSISTENT_MAP_TRACKER)
|
||||||
|
message(STATUS "Integration tests: package b1's persistent-map tracker is present "
|
||||||
|
"(${MGL_ITEST_PERSISTENT_MAP_TRACKER}) - PersistentCoherentMapScenario's "
|
||||||
|
"membership assertion is live")
|
||||||
|
target_compile_definitions(MobileGLIntegrationTest PRIVATE -DMGITEST_PERSISTENT_MAP_TRACKER=1)
|
||||||
|
else()
|
||||||
|
message(STATUS "Integration tests: no source under MobileGL/MG_Remote/Client names "
|
||||||
|
"IsLivePersistentMap - package b1's tracker has not landed, so "
|
||||||
|
"PersistentCoherentMapScenario's membership assertion SKIPS")
|
||||||
|
endif()
|
||||||
|
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_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.
|
||||||
|
# MOBILEGL_TRANSPORT=monolith IS NAMED HERE ON PURPOSE, and it is review finding N-6. This entry
|
||||||
|
# is the MONOLITH half of a pair; without the property a job-level or gate-level
|
||||||
|
# `MOBILEGL_TRANSPORT=inproc` export reaches it (observed: its own log then carried
|
||||||
|
# `Config: MOBILEGL_TRANSPORT=inproc`) and, in the inproc arm of the three-arm A/B, BOTH halves of
|
||||||
|
# the pair were inproc. Nothing failed - each half asserts the same constant - but the pair was
|
||||||
|
# not the pair its name describes. A ctest ENVIRONMENT property overrides the job environment,
|
||||||
|
# which is the one case where that is what you want.
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_GLES_PMAP_ARM_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_PMAP_LANE=1" "MOBILEGL_TRANSPORT=monolith"
|
||||||
|
"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"
|
||||||
|
"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_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}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- EXIT GATE E3(e): the same checks with a ring small enough to force back-pressure -----
|
||||||
|
#
|
||||||
|
# BRIEF 7 E3(e) - "以上五条再跑一遍 ring 小到足以至少发生一次背压等待的配置" - was missing from
|
||||||
|
# the first cut of this package and missing from its report (review finding M-7). It is a
|
||||||
|
# LANE, not a new scenario: the same three split scenarios with SEG_CMD and SEG_STAGE at their
|
||||||
|
# floor, so a workload that fits comfortably in the 8 MiB / 32 MiB defaults has to wrap and
|
||||||
|
# wait at least once.
|
||||||
|
#
|
||||||
|
# 1 MiB is ConfigLoader's minimum for both (Config.h / ConfigLoader.cpp:363-364, floors of 1
|
||||||
|
# rather than 0 because a ring caps ONE record at half its size). That caps a record at 512
|
||||||
|
# KiB, which every record on the reduced path is far under, so the ring is legal and small
|
||||||
|
# rather than unusable.
|
||||||
|
#
|
||||||
|
# WHAT THIS LANE DOES NOT YET ASSERT, stated rather than implied: that a back-pressure wait
|
||||||
|
# ACTUALLY happened. That needs a counter only package s1 can publish (a producer-parked or
|
||||||
|
# ring-full tally on RingControl); until it exists this lane proves the five checks survive a
|
||||||
|
# small ring, not that the small ring bit. The gap is named in t1-v1.md's debts rather than
|
||||||
|
# left for someone to discover from a green.
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_GLES_SPLIT_SMALL_RING_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_TRANSPORT=inproc"
|
||||||
|
"MOBILEGL_IPC_SERVER_PATH=${MGL_ITEST_SPLIT_SERVER_PATH}" "MGITEST_SPLIT_LANE=1"
|
||||||
|
"MGITEST_SMALL_RING_LANE=1" "MOBILEGL_IPC_RING_MB=1" "MOBILEGL_IPC_STAGE_MB=1"
|
||||||
|
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||||
|
|
||||||
|
foreach(mglItestSmallRingScenario ClearThenReadPixelsScenario TriangleScenario
|
||||||
|
PersistentCoherentMapScenario)
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.Split.SmallRing."
|
||||||
|
TEST_FILTER "${mglItestSmallRingScenario}.*"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS "integration-gpu\;integration-split"
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_SMALL_RING_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
endforeach()
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PersistentMapPeek.cpp
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "PersistentMapPeek.h"
|
||||||
|
|
||||||
|
#if !defined(__ANDROID__)
|
||||||
|
#include <MG_State/GLState/BufferState/BufferObject.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#define MGITEST_PERSISTENT_MAP_PEEK_LIVE 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// MGITEST_PERSISTENT_MAP_TRACKER is defined by MG_IntegrationTest/CMakeLists.txt, and only when
|
||||||
|
// BOTH halves are true: the build compiled MG_Remote (so the symbol can link) AND some source
|
||||||
|
// under MG_Remote/Client names IsLivePersistentMap (so package b1 landed it). __has_include is
|
||||||
|
// NOT enough on its own - the header exists in the source tree of every build, including the pull
|
||||||
|
// build that never compiles MG_Remote, so keying on it would turn a healthy pull build into a
|
||||||
|
// link error.
|
||||||
|
#if defined(MGITEST_PERSISTENT_MAP_PEEK_LIVE) && defined(MGITEST_PERSISTENT_MAP_TRACKER)
|
||||||
|
#include <MG_Remote/Client/PersistentMapTracker.h>
|
||||||
|
#define MGITEST_PERSISTENT_MAP_TRACKER_LIVE 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
bool PersistentMapPeekAvailable() {
|
||||||
|
#if defined(MGITEST_PERSISTENT_MAP_PEEK_LIVE)
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PersistentMapTrackerAvailable() {
|
||||||
|
#if defined(MGITEST_PERSISTENT_MAP_TRACKER_LIVE)
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(MGITEST_PERSISTENT_MAP_PEEK_LIVE)
|
||||||
|
namespace {
|
||||||
|
// The frontend object behind a GL buffer name, or null. GetBufferObject mints on demand
|
||||||
|
// for a name that was generated and never bound, which is harmless here: a scenario only
|
||||||
|
// ever asks about a buffer it has already defined and mapped, and a null store answers
|
||||||
|
// "not adopted", which is the same answer an un-mapped buffer would give.
|
||||||
|
MobileGL::MG_State::GLState::BufferObject* FrontendBuffer(unsigned int bufferName) {
|
||||||
|
if (bufferName == 0) return nullptr;
|
||||||
|
auto& context = MobileGL::MG_State::pGLContext;
|
||||||
|
if (!context) return nullptr;
|
||||||
|
const auto& buffer = context->GetBufferObject(static_cast<MobileGL::Uint>(bufferName));
|
||||||
|
return buffer.get();
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool PeekBufferIsAdoptedPersistentMap(unsigned int bufferName, bool* outAdopted) {
|
||||||
|
#if defined(MGITEST_PERSISTENT_MAP_PEEK_LIVE)
|
||||||
|
if (outAdopted == nullptr) return false;
|
||||||
|
MobileGL::MG_State::GLState::BufferObject* buffer = FrontendBuffer(bufferName);
|
||||||
|
if (buffer == nullptr) return false;
|
||||||
|
*outAdopted = static_cast<bool>(buffer->IsBackendPersistentMapped());
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
(void)bufferName;
|
||||||
|
(void)outAdopted;
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeekBufferIsLivePersistentMap(unsigned int bufferName, bool* outLive) {
|
||||||
|
#if defined(MGITEST_PERSISTENT_MAP_TRACKER_LIVE)
|
||||||
|
if (outLive == nullptr) return false;
|
||||||
|
MobileGL::MG_State::GLState::BufferObject* buffer = FrontendBuffer(bufferName);
|
||||||
|
if (buffer == nullptr) return false;
|
||||||
|
*outLive = static_cast<bool>(
|
||||||
|
MobileGL::MG_Remote::Client::PersistentMapTracker::IsLivePersistentMap(*buffer));
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
(void)bufferName;
|
||||||
|
(void)outLive;
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PersistentMapPeek.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
|
||||||
|
//
|
||||||
|
// WHICH ARM A PERSISTENT|WRITE|COHERENT MAP LANDED IN, read from a scenario.
|
||||||
|
//
|
||||||
|
// It exists for exit gate E3's first assertion, and package b1 (b1-v1.md 4.1) names the
|
||||||
|
// spelling: `IsBackendPersistentMapped()` must be FALSE, i.e. the store was NOT adopted and the
|
||||||
|
// CPU shadow is still the source of truth, i.e. the emulated arm. The question has no answer in
|
||||||
|
// the GL API at all - both arms map, both arms take the application's writes, both arms draw the
|
||||||
|
// same pixels - so a scenario with no peek is a scenario that silently tests whichever arm the
|
||||||
|
// driver and the build happened to choose. `MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION` does not
|
||||||
|
// separate them either: it is read only inside TryAdoptLargeStorage, and a scenario-sized buffer
|
||||||
|
// never reaches the 16 MiB threshold that calls it.
|
||||||
|
//
|
||||||
|
// A separate translation unit for BackendCapsPeek.h's reason, verbatim: the scenario sources
|
||||||
|
// include the GL headers with prototypes and MobileGL's umbrella header is not meant to meet them
|
||||||
|
// in one file.
|
||||||
|
//
|
||||||
|
// Every entry point returns FALSE, touching nothing, where the state is out of reach - on Android
|
||||||
|
// this module links the shipping libMobileGL.so built -fvisibility=hidden, so no internal symbol
|
||||||
|
// resolves, and the tracker half additionally needs a build that compiled MG_Remote/Client. A
|
||||||
|
// caller that gets false must SKIP rather than pass: "could not look" is not "it was emulated".
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
// True when the peek can answer at all in this build. A scenario asks this first so that its
|
||||||
|
// skip message can name WHY it could not look.
|
||||||
|
bool PersistentMapPeekAvailable();
|
||||||
|
|
||||||
|
// BufferObject::IsBackendPersistentMapped() for the buffer with this GL name.
|
||||||
|
//
|
||||||
|
// returns false -> could not look (no peek in this build, no current context, or no such
|
||||||
|
// buffer). *outAdopted is untouched.
|
||||||
|
// returns true -> *outAdopted is true on the ADOPTED arm (the resource owner minted
|
||||||
|
// host-visible coherent storage and the shadow was released) and false on
|
||||||
|
// the EMULATED arm (the owner declined; the shadow is the truth and the
|
||||||
|
// client has to push blocks). R-6 pins the split arm at emulated.
|
||||||
|
bool PeekBufferIsAdoptedPersistentMap(unsigned int bufferName, bool* outAdopted);
|
||||||
|
|
||||||
|
// True when this build compiled package b1's client-side persistent-map tracker, so the
|
||||||
|
// membership predicate below means something. CMake answers it, by probing MG_Remote/Client
|
||||||
|
// for the symbol: a build that never compiled MG_Remote cannot link the call, so the decision
|
||||||
|
// has to be made before the compiler sees it rather than by __has_include.
|
||||||
|
bool PersistentMapTrackerAvailable();
|
||||||
|
|
||||||
|
// MG_Remote::Client::PersistentMapTracker::IsLivePersistentMap() for this buffer - b1-v1.md
|
||||||
|
// 4.1 item 2. The membership set is meant to be exactly the early-out chain of
|
||||||
|
// SyncPersistentMappedRange; asking it here is what makes a drift between the two fail in a
|
||||||
|
// named test rather than silently stop the push.
|
||||||
|
//
|
||||||
|
// Same contract as above: false means "could not look".
|
||||||
|
bool PeekBufferIsLivePersistentMap(unsigned int bufferName, bool* outLive);
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -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 {
|
||||||
|
|
||||||
@@ -66,6 +67,33 @@ namespace MGITest {
|
|||||||
|
|
||||||
class ScenarioTest : public ::testing::Test {
|
class ScenarioTest : public ::testing::Test {
|
||||||
protected:
|
protected:
|
||||||
|
// THE BEHAVIOURAL HALF OF THE SPLIT LANE'S CLAIM, and it is in the DESTRUCTOR rather than
|
||||||
|
// in TearDown() on purpose: gtest calls only the MOST DERIVED TearDown, and every scenario
|
||||||
|
// that overrides it would have to remember to chain here. The fixture destructor always
|
||||||
|
// runs, and it runs before the test result is finalized, so ADD_FAILURE() is recorded.
|
||||||
|
//
|
||||||
|
// What it asserts: a case that ran in an ARMED split lane must have moved the client
|
||||||
|
// encoder's record ordinal. Everything else in the lane - the pixels, the readbacks, the
|
||||||
|
// arm assertion - is equally true of a monolith run of the same workload; this is the one
|
||||||
|
// statement that is only true if records crossed the ring. An emit table that resolves the
|
||||||
|
// transport and then falls through to the driver passes every other assertion in the file
|
||||||
|
// and fails exactly here.
|
||||||
|
~ScenarioTest() override {
|
||||||
|
if (!m_splitAssertionsArmed) return;
|
||||||
|
if (IsSkipped() || HasFailure()) return;
|
||||||
|
const SplitRuntimeState after = PeekSplitRuntime();
|
||||||
|
if (after.emitSeq <= m_emitSeqAtSetUp) {
|
||||||
|
ADD_FAILURE() << "this case ran in an armed DirectGLES.Split. lane and the client "
|
||||||
|
"encoder's record ordinal did not move: EmitSeq was "
|
||||||
|
<< m_emitSeqAtSetUp << " at SetUp and is " << after.emitSeq
|
||||||
|
<< " now. The workload drew, cleared and read pixels, so records were "
|
||||||
|
"due - a sequence that did not advance means the emit table "
|
||||||
|
"resolved the transport and then did not put anything on the wire, "
|
||||||
|
"which every other assertion in this lane is blind to because a "
|
||||||
|
"monolith run of the same workload produces the same pixels.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SetUp() override {
|
void SetUp() override {
|
||||||
m_ready = false;
|
m_ready = false;
|
||||||
HeadlessGL& gl = HeadlessGL::Get();
|
HeadlessGL& gl = HeadlessGL::Get();
|
||||||
@@ -95,6 +123,36 @@ 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 - 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.
|
||||||
|
//
|
||||||
|
// THE ARMING QUESTION IS ASKED OF THE PROCESS, not of the source tree. Until a real
|
||||||
|
// client session exists, MOBILEGL_TRANSPORT=inproc is parsed and then nothing consumes
|
||||||
|
// it, so every case in the lane would go GREEN against the monolith path under a name
|
||||||
|
// that says it tested the split one. The first version of this guard answered the
|
||||||
|
// question with a CMake grep over c0's stub files, and review finding M-1 falsified it
|
||||||
|
// by renaming one string: eleven lanes armed and eight went green. Harness/
|
||||||
|
// SplitRuntimePeek.h now answers it from MG_Config::Transport, ClientSession::Active()
|
||||||
|
// and ImplementedVerbCount(), none of which a message edit can move. Registrations are
|
||||||
|
// never deleted (gate G14); they skip, naming exactly which fact is not true.
|
||||||
|
if (SplitLane::IsSplitLane()) {
|
||||||
|
if (const std::string splitSkip = SplitLane::SkipReasonForSplitOnlyAssertions();
|
||||||
|
!splitSkip.empty()) {
|
||||||
|
GTEST_SKIP() << splitSkip;
|
||||||
|
}
|
||||||
|
// Armed. Take the wire's baseline, so the destructor can require that this case
|
||||||
|
// actually PUT SOMETHING THROUGH IT (review finding N-5: ten of the eleven Split
|
||||||
|
// entries had no runtime evidence of anything, and their green meant only "the
|
||||||
|
// same GL workload passed").
|
||||||
|
const SplitRuntimeState state = PeekSplitRuntime();
|
||||||
|
m_splitAssertionsArmed = true;
|
||||||
|
m_emitSeqAtSetUp = state.emitSeq;
|
||||||
|
RecordProperty("split_transport", state.transportName);
|
||||||
|
RecordProperty("split_implemented_verbs", static_cast<int>(state.implementedVerbs));
|
||||||
|
RecordProperty("split_emit_seq_at_setup", static_cast<int>(state.emitSeq));
|
||||||
|
}
|
||||||
// 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.
|
||||||
@@ -122,6 +180,8 @@ namespace MGITest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool m_ready = false;
|
bool m_ready = false;
|
||||||
|
bool m_splitAssertionsArmed = false;
|
||||||
|
unsigned long long m_emitSeqAtSetUp = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// 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 harness markers the `DirectGLES.Split.` ctest entries set.
|
||||||
|
//
|
||||||
|
// WHAT IS AND IS NOT DECIDED HERE. These markers say what the LANE asked for. Whether the lane
|
||||||
|
// GOT it is a different question and it is answered by Harness/SplitRuntimePeek.h, out of the
|
||||||
|
// running process - see the long argument in that header. The split of responsibility matters:
|
||||||
|
// an environment variable is a request, and this package's first version treated a request (plus
|
||||||
|
// a grep over source text) as evidence that the request had been honoured. Review finding M-1
|
||||||
|
// falsified that by renaming one string in six files, which armed eleven lanes and turned eight
|
||||||
|
// of them green against the monolith path.
|
||||||
|
//
|
||||||
|
// MGITEST_SPLIT_LANE=1
|
||||||
|
// Set by the DirectGLES.Split.* entries and by nothing else. It is how a case in ONE binary,
|
||||||
|
// registered many times over, knows which registration it is running under. It is NOT
|
||||||
|
// evidence of anything about the transport.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// PersistentCoherentMapScenario asserts it landed there, through
|
||||||
|
// Harness/PersistentMapPeek.h's read of IsBackendPersistentMapped(). R-6 pins the split lane
|
||||||
|
// at T2 = declined = emulated.
|
||||||
|
//
|
||||||
|
// MGITEST_PMAP_LANE=1
|
||||||
|
// The one counting entry per transport that reads the library's summary line back. It has a
|
||||||
|
// MOBILEGL_LOG_FILE_PATH of its own and a RESOURCE_LOCK on it.
|
||||||
|
//
|
||||||
|
// MGITEST_SMALL_RING_LANE=1
|
||||||
|
// Exit gate E3(e)'s lane: the same split scenarios with MOBILEGL_IPC_RING_MB and
|
||||||
|
// MOBILEGL_IPC_STAGE_MB at their floor, so that the ring is small enough to make at least one
|
||||||
|
// back-pressure wait happen. A case uses it only to say so in its recorded properties; the
|
||||||
|
// ring sizes themselves reach the library through MOBILEGL_IPC_*.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "SplitRuntimePeek.h"
|
||||||
|
|
||||||
|
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 in exit gate E3(e)'s small-ring lane.
|
||||||
|
inline bool IsSmallRingLane() { return MarkerIsOne("MGITEST_SMALL_RING_LANE"); }
|
||||||
|
|
||||||
|
// 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" - and because the previous version of this
|
||||||
|
// message named the wrong missing thing (review finding N-1): it said MG_Remote/Client did
|
||||||
|
// not exist, on a tree where it existed and compiled and every entry point aborted.
|
||||||
|
inline std::string SkipReasonForSplitOnlyAssertions() {
|
||||||
|
if (!IsSplitLane()) {
|
||||||
|
return "not the split lane (MGITEST_SPLIT_LANE is unset): this case's split-only "
|
||||||
|
"assertions are about a live MG_Remote client session and say nothing in a "
|
||||||
|
"monolith process";
|
||||||
|
}
|
||||||
|
return SplitRuntimeSkipReason();
|
||||||
|
}
|
||||||
|
|
||||||
|
// "adopted", "emulated", or empty when the lane declared nothing.
|
||||||
|
inline std::string DeclaredPersistentMapArm() { return MarkerValue("MGITEST_PERSISTENT_MAP_ARM"); }
|
||||||
|
|
||||||
|
} // namespace MGITest::SplitLane
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.cpp
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "SplitRuntimePeek.h"
|
||||||
|
|
||||||
|
// MGITEST_SPLIT_RUNTIME_PEEK is defined by MG_IntegrationTest/CMakeLists.txt under
|
||||||
|
// MOBILEGL_BUILD_DISAGGREGATED and nowhere else. NO SOURCE PROBE decides it: the three symbols
|
||||||
|
// below are c0's, they exist in every disaggregated build from the contract commit onward, and
|
||||||
|
// their VALUES are what answer the question. That is the whole of the fix for review findings
|
||||||
|
// M-1, M-2 and M-3 - there is no longer a string for anyone to rename, comment out, or land
|
||||||
|
// outside a probed directory.
|
||||||
|
#if defined(MGITEST_SPLIT_RUNTIME_PEEK) && !defined(__ANDROID__)
|
||||||
|
#include <Config.h>
|
||||||
|
|
||||||
|
#include <MG_Remote/Client/ClientSession.h>
|
||||||
|
#include <MG_Remote/Client/EmitTables.h>
|
||||||
|
#define MGITEST_SPLIT_RUNTIME_PEEK_LIVE 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
SplitRuntimeState PeekSplitRuntime() {
|
||||||
|
SplitRuntimeState state;
|
||||||
|
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
|
||||||
|
using MobileGL::MG_Config::TransportMode;
|
||||||
|
state.peekAvailable = true;
|
||||||
|
state.totalVerbSlots = MobileGL::MG_Remote::Client::kRemoteEmitSlotCount;
|
||||||
|
switch (MobileGL::MG_Config::Transport) {
|
||||||
|
case TransportMode::Monolith: state.transportName = "monolith"; break;
|
||||||
|
case TransportMode::InProcess: state.transportName = "inproc"; break;
|
||||||
|
default: state.transportName = "non-monolith"; break;
|
||||||
|
}
|
||||||
|
state.transportResolved = MobileGL::MG_Config::Transport != TransportMode::Monolith;
|
||||||
|
|
||||||
|
// Active() is c0's one deliberately non-aborting accessor: "does a session exist" has a
|
||||||
|
// legitimate no. Everything below it is only reached through a live session, so nothing
|
||||||
|
// here can trip one of c0's Fatal stubs.
|
||||||
|
MobileGL::MG_Remote::Client::ClientSession* session =
|
||||||
|
MobileGL::MG_Remote::Client::ClientSession::Active();
|
||||||
|
state.sessionActive = session != nullptr;
|
||||||
|
state.implementedVerbs = MobileGL::MG_Remote::Client::ImplementedVerbCount();
|
||||||
|
if (session != nullptr) {
|
||||||
|
// Encoder() returns the member; EmitSeq() returns m_emitSeq. Neither is a stub, and
|
||||||
|
// neither emits anything - this is a read.
|
||||||
|
state.emitSeq = session->Encoder().EmitSeq();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string SplitRuntimeSkipReason() {
|
||||||
|
const SplitRuntimeState state = PeekSplitRuntime();
|
||||||
|
if (!state.peekAvailable) {
|
||||||
|
return "this build did not compile MG_Remote (no -DMOBILEGL_BUILD_DISAGGREGATED=ON), or "
|
||||||
|
"this is the Android binary, which links the shipping libMobileGL.so built "
|
||||||
|
"-fvisibility=hidden and can reach no internal symbol. MOBILEGL_TRANSPORT is "
|
||||||
|
"ACCEPTED AND SILENTLY IGNORED in such a build (CONTRACT-P5 5), so a green here "
|
||||||
|
"would be a monolith run under a name that says split";
|
||||||
|
}
|
||||||
|
if (!state.transportResolved) {
|
||||||
|
return "MG_Config::Transport resolved to '" + state.transportName +
|
||||||
|
"', not to a split transport. The variable is read from the process, not from a "
|
||||||
|
"log line - a DEBUG-level pull build prints the same KEY=VALUE string out of "
|
||||||
|
"ConfigLoader's env dump (review M-5). Check MOBILEGL_TRANSPORT reached this "
|
||||||
|
"process";
|
||||||
|
}
|
||||||
|
if (!state.sessionActive) {
|
||||||
|
return "MG_Remote::Client::ClientSession::Active() is null: no client session exists in "
|
||||||
|
"this process. c0 shipped Start() as a Fatal stub and Active() as a deliberate "
|
||||||
|
"null, so this is what 'packages s1 (construction and handshake) and c1 have not "
|
||||||
|
"landed' looks like from inside a running test. The entry stays registered (gate "
|
||||||
|
"G14) and skips rather than passing against the monolith path";
|
||||||
|
}
|
||||||
|
if (state.implementedVerbs == 0) {
|
||||||
|
return "MG_Remote::Client::ImplementedVerbCount() is 0 of " +
|
||||||
|
std::to_string(state.totalVerbSlots) +
|
||||||
|
": the emit table has no real emitter, so every verb this scenario issues would "
|
||||||
|
"take the Fatal{UnmigratedVerb} arm or fall through. Package c1 owns it";
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.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
|
||||||
|
//
|
||||||
|
// WHETHER THIS PROCESS IS REALLY RUNNING SPLIT, asked of the process rather than of the source
|
||||||
|
// tree. This is what arms every `DirectGLES.Split.` entry.
|
||||||
|
//
|
||||||
|
// WHY IT REPLACED A CONTENT PROBE, and the lesson is worth the paragraph. The first version of
|
||||||
|
// this arming condition was a CMake `file(STRINGS ... REGEX)` conjunction: "some source under
|
||||||
|
// MG_Remote/Client names the c1 symbols AND no source under Client or Server still matches
|
||||||
|
// `Fatal.Unimplemented`". Review finding M-1 did not argue with it, it PERFORMED it: a
|
||||||
|
// `sed -i 's/Fatal{Unimplemented/Fatal{NotYetImplemented/'` over c0's six stub files - every
|
||||||
|
// entry point still ending in std::abort(), RemoteEmitTable() still aborting on sight,
|
||||||
|
// ImplementedVerbCount() still returning 0 - armed all eleven lanes and EIGHT OF THEM WENT GREEN
|
||||||
|
// having run monolith end to end. A comment line containing the marker did the opposite and kept
|
||||||
|
// them dark forever (M-2), and two further stub files outside the two probed directories made a
|
||||||
|
// partial arm possible (M-3).
|
||||||
|
//
|
||||||
|
// The general form of that defect: A STATEMENT ABOUT SOURCE TEXT CAN ALWAYS BE FALSIFIED BY
|
||||||
|
// EDITING SOURCE TEXT, and the people most likely to edit it are the ones landing the packages
|
||||||
|
// the probe is watching for. A statement about what the process actually did cannot. So the three
|
||||||
|
// facts below are read out of the running process, and each is structurally impossible in a
|
||||||
|
// monolith build:
|
||||||
|
//
|
||||||
|
// 1. `MG_Config::Transport != Monolith`. In a build without MOBILEGL_BUILD_DISAGGREGATED,
|
||||||
|
// `Transport` is a `constexpr` Monolith (Config.h:514) and this whole translation unit is
|
||||||
|
// compiled out. Read from the VARIABLE, never from a log line - the log-grep spelling of
|
||||||
|
// this question is satisfied by a DEBUG-level pull build's `Config: Accepted env variable:
|
||||||
|
// MOBILEGL_TRANSPORT=inproc` (review finding M-5).
|
||||||
|
// 2. `ClientSession::Active() != nullptr`. c0 made this one deliberately return null rather
|
||||||
|
// than Fatal, because "does a session exist" has a legitimate "no" - it is the monolith
|
||||||
|
// answer (ClientSession.cpp:31). So it is exactly "a client session exists in this process",
|
||||||
|
// and no amount of editing stub MESSAGES makes a null pointer non-null.
|
||||||
|
// 3. `ImplementedVerbCount() > 0`. c0's stub returns 0; the contract gives this function the
|
||||||
|
// job of making "a table that silently lost an emitter" distinguishable from "a table that
|
||||||
|
// never had one" (EmitTables.h). Zero means there is no emitter to test.
|
||||||
|
//
|
||||||
|
// And one BEHAVIOURAL fact, which is the half that says the run went through the wire rather than
|
||||||
|
// merely that it could have: `ClientSession::Active()->Encoder().EmitSeq()`, the highest record
|
||||||
|
// ordinal this client has produced. A scenario that armed, drew, and emitted nothing has a
|
||||||
|
// sequence that did not move, and that is the shape of an emit table that resolves the transport
|
||||||
|
// and then falls through to the driver.
|
||||||
|
//
|
||||||
|
// Every entry point returns false, touching nothing, where the state is out of reach: in a build
|
||||||
|
// that never compiled MG_Remote, and on Android where this module links the shipping
|
||||||
|
// libMobileGL.so built -fvisibility=hidden. A caller that gets false must SKIP.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
// What this process can say about itself. Every field is false/0 where the peek cannot look.
|
||||||
|
struct SplitRuntimeState {
|
||||||
|
// The peek is compiled in at all (MOBILEGL_BUILD_DISAGGREGATED, not Android).
|
||||||
|
bool peekAvailable = false;
|
||||||
|
// MG_Config::Transport != Monolith - this process RESOLVED a split transport.
|
||||||
|
bool transportResolved = false;
|
||||||
|
// The resolved transport, for a message: "monolith", "inproc", "spawn", "unix", "pipe".
|
||||||
|
std::string transportName = "monolith";
|
||||||
|
// ClientSession::Active() != nullptr.
|
||||||
|
bool sessionActive = false;
|
||||||
|
// ImplementedVerbCount(), out of kRemoteEmitSlotCount (71).
|
||||||
|
unsigned int implementedVerbs = 0;
|
||||||
|
unsigned int totalVerbSlots = 0;
|
||||||
|
// The encoder's highest produced record ordinal, or 0 when there is no session.
|
||||||
|
unsigned long long emitSeq = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
SplitRuntimeState PeekSplitRuntime();
|
||||||
|
|
||||||
|
// Empty when this process is a real split run that can be asserted about; otherwise the
|
||||||
|
// reason to GTEST_SKIP() with, naming the first fact that is not true and the package that
|
||||||
|
// owns it.
|
||||||
|
std::string SplitRuntimeSkipReason();
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,494 @@
|
|||||||
|
// 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/PersistentMapPeek.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
|
||||||
|
layout(location = 0) in vec2 aPos;
|
||||||
|
layout(location = 1) in vec3 aColor;
|
||||||
|
out vec3 vColor;
|
||||||
|
void main() {
|
||||||
|
vColor = aColor;
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFragmentSource = R"(#version 330 core
|
||||||
|
in vec3 vColor;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() { oColor = vec4(vColor, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
float r, g, b;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A 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;
|
||||||
|
// b1-v1.md 4.1 item 7, asserted rather than commented. GL_MAP_FLUSH_EXPLICIT_BIT would
|
||||||
|
// take this buffer OUT of the client's live-persistent-map set on purpose - the
|
||||||
|
// application announces its own writes through resource_flush_range and the push ships
|
||||||
|
// nothing - so a flag added here would turn every assertion below into a statement about
|
||||||
|
// a different mechanism, and it would still draw the right pixels under monolith.
|
||||||
|
static_assert((kCoherentFlags & GL_MAP_FLUSH_EXPLICIT_BIT) == 0,
|
||||||
|
"this scenario is about the PUSH, which is defined only for a persistent "
|
||||||
|
"write map that is NOT FLUSH_EXPLICIT (BufferObject.cpp:645-661)");
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
// b1-v1.md 4.1 item 1, IMMEDIATELY AFTER THE MAP and before anything else can
|
||||||
|
// change the answer. This is the assertion that decides whether the rest of the
|
||||||
|
// scenario means anything.
|
||||||
|
AssertOrRecordArm(m_vbo, "immediately after the map");
|
||||||
|
if (IsSkipped() || HasFatalFailure()) return;
|
||||||
|
AssertMembership(m_vbo, "immediately after the map");
|
||||||
|
if (IsSkipped() || HasFatalFailure()) return;
|
||||||
|
DescribeAttributes();
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "configuring the VAO over the coherently mapped store";
|
||||||
|
}
|
||||||
|
|
||||||
|
// WHICH ARM. b1-v1.md 4.1 items 1 and 5: IsBackendPersistentMapped() false is the
|
||||||
|
// emulated arm - the resource owner DECLINED, the CPU shadow is still the truth, and
|
||||||
|
// the client has to push the mapping's dirty blocks. True is the adopted arm, where
|
||||||
|
// MGPipeApplyMapPersistent handed back a pointer and the shadow was released.
|
||||||
|
//
|
||||||
|
// Under inproc BOTH ARMS RENDER CORRECTLY, because an adopted store's address really
|
||||||
|
// is valid in this process - which is exactly why exit gate E3(d) has to be checked
|
||||||
|
// and cannot be inferred from pixels. Under spawn the adopted arm would be a wild
|
||||||
|
// pointer, so this one assertion is what makes E3 mean the same thing in P5 and P6.
|
||||||
|
//
|
||||||
|
// The lane DECLARES the arm it expects (MGITEST_PERSISTENT_MAP_ARM); a lane that
|
||||||
|
// declares none RECORDS it, 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 no defect.
|
||||||
|
void AssertOrRecordArm(unsigned int vbo, const char* when) {
|
||||||
|
const std::string declared = SplitLane::DeclaredPersistentMapArm();
|
||||||
|
if (!PersistentMapPeekAvailable()) {
|
||||||
|
if (!declared.empty()) {
|
||||||
|
GTEST_SKIP() << "this lane declares the " << declared
|
||||||
|
<< " arm but Harness/PersistentMapPeek cannot look in this "
|
||||||
|
"build (on Android this module links the shipping "
|
||||||
|
"libMobileGL.so, built -fvisibility=hidden, so no internal "
|
||||||
|
"symbol resolves). 'Could not look' is not 'it was "
|
||||||
|
<< declared << "'.";
|
||||||
|
}
|
||||||
|
RecordProperty("persistent_map_arm", "unknown");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool adopted = false;
|
||||||
|
ASSERT_TRUE(PeekBufferIsAdoptedPersistentMap(vbo, &adopted))
|
||||||
|
<< "no frontend BufferObject behind GL buffer " << vbo << " " << when;
|
||||||
|
const char* arm = adopted ? "adopted" : "emulated";
|
||||||
|
RecordProperty("persistent_map_arm", arm);
|
||||||
|
if (declared.empty()) return;
|
||||||
|
ASSERT_EQ(declared, std::string(arm))
|
||||||
|
<< "the lane declared the " << declared << " arm and IsBackendPersistentMapped() "
|
||||||
|
<< when << " says " << arm
|
||||||
|
<< ". R-6 pins the split arm at adopt tier T2 = declined = emulated, and the two "
|
||||||
|
"arms are completely different code paths: adopted means the CPU shadow was "
|
||||||
|
"released and the application writes straight into storage the resource owner "
|
||||||
|
"minted, emulated means the shadow is the truth and every validate point has "
|
||||||
|
"to push blocks. Under inproc both render correctly, so pixels cannot tell "
|
||||||
|
"them apart - this is exit gate E3(d) and it is the only thing that can.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// b1-v1.md 4.1 item 2: the client's live-persistent-map set is meant to be exactly
|
||||||
|
// SyncPersistentMappedRange's early-out chain. A drift between the two stops the push
|
||||||
|
// silently; asking here makes it a named failure instead.
|
||||||
|
void AssertMembership(unsigned int vbo, const char* when) {
|
||||||
|
if (!PersistentMapTrackerAvailable()) {
|
||||||
|
RecordProperty("persistent_map_membership", "unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool live = false;
|
||||||
|
ASSERT_TRUE(PeekBufferIsLivePersistentMap(vbo, &live))
|
||||||
|
<< "the tracker is compiled in but could not answer for GL buffer " << vbo << " " << when;
|
||||||
|
EXPECT_TRUE(live)
|
||||||
|
<< "a PERSISTENT|WRITE|COHERENT map that is not FLUSH_EXPLICIT is a member of "
|
||||||
|
"the client's live-persistent-map set by construction, and it is not one "
|
||||||
|
<< when
|
||||||
|
<< ". The set is supposed to BE SyncPersistentMappedRange's early-out chain; if "
|
||||||
|
"they have drifted, the push stops shipping this buffer's blocks and nothing "
|
||||||
|
"else says so.";
|
||||||
|
}
|
||||||
|
|
||||||
|
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"));
|
||||||
|
// E3(d) again, AFTER the draws: b1-v1.md 4.1 item 5 asks that the arm hold across the
|
||||||
|
// whole scenario, not only at the map. A late adoption - the owner declining once and
|
||||||
|
// minting on a later validate point - would leave the first assertion true and this one
|
||||||
|
// false, and would be invisible in every pixel this case reads.
|
||||||
|
AssertOrRecordArm(m_vbo, "after both writes and both draws");
|
||||||
|
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"));
|
||||||
|
AssertOrRecordArm(m_vbo, "after a frame boundary and two draws");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
AssertOrRecordArm(store.vbo, "immediately after the counted window's map");
|
||||||
|
if (IsSkipped() || HasFatalFailure()) return;
|
||||||
|
AssertMembership(store.vbo, "immediately after the counted window's map");
|
||||||
|
if (IsSkipped() || HasFatalFailure()) return;
|
||||||
|
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;
|
||||||
|
|
||||||
|
// E3(c)'s second half: the bytes the push actually shipped. `pmap` is bytes PUSHED because
|
||||||
|
// a persistently mapped range had to be published, so on the emulated arm a window with
|
||||||
|
// four writes and four draws through a live coherent mapping cannot be zero.
|
||||||
|
//
|
||||||
|
// THE ARM ITSELF IS NOT DERIVED FROM THIS NUMBER. It is asserted from
|
||||||
|
// IsBackendPersistentMapped() at the map and again after the draws (b1-v1.md 4.1 items 1
|
||||||
|
// and 5), which is the direct observable; pmap is the CONSEQUENCE and is checked as a
|
||||||
|
// cross-check. Deriving the arm from pmap would have made "the push was never wired" and
|
||||||
|
// "the store was adopted" the same reading, and they are different defects with different
|
||||||
|
// owners.
|
||||||
|
const double pushedBytes = PipeStatsWindow::CounterAsDoubleOrAbsent(window, "pmap");
|
||||||
|
ASSERT_GE(pushedBytes, 0.0) << "the summary line carries no pmap= field: " << window.line;
|
||||||
|
RecordProperty("persistent_map_push_bytes_per_frame", std::to_string(pushedBytes).c_str());
|
||||||
|
RecordProperty("map_persistent_roundtrips", std::to_string(roundtrips).c_str());
|
||||||
|
|
||||||
|
if (SplitLane::DeclaredPersistentMapArm() == "emulated") {
|
||||||
|
EXPECT_GT(pushedBytes, 0.0)
|
||||||
|
<< "the emulated arm pushes the mapping's dirty blocks at every validate point, so "
|
||||||
|
"a window with "
|
||||||
|
<< kDrawsInTheWindow
|
||||||
|
<< " writes and as many draws through a live coherent mapping cannot have pushed "
|
||||||
|
"zero bytes. Zero here with MOBILEGL_IPC_PERSISTENT_BLOCK_KB at its default "
|
||||||
|
"means the push is not reaching this buffer - which is a DIFFERENT defect from "
|
||||||
|
"the store having been adopted, and the arm assertion above has already ruled "
|
||||||
|
"that one out. Zero with the knob set to 0 is exit gate E3(a)'s negative control "
|
||||||
|
"working as intended. Line: "
|
||||||
|
<< window.line;
|
||||||
|
} else {
|
||||||
|
// Recorded, not asserted: on this arm the number is whatever the driver's adoption
|
||||||
|
// decision makes it, and it is the figure MEASUREMENTS wants beside the split one.
|
||||||
|
RecordProperty("persistent_map_push_note",
|
||||||
|
"recorded only - this lane declares no emulated arm");
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
layout(location = 0) in vec2 aPos;
|
||||||
|
layout(location = 1) in vec3 aColor;
|
||||||
|
out vec3 vColor;
|
||||||
|
void main() {
|
||||||
|
vColor = aColor;
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFragmentSource = R"(#version 330 core
|
||||||
|
in vec3 vColor;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() { oColor = vec4(vColor, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
float r, g, b;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A single triangle with its base at y = -0.8 and its apex at y = +0.8, so the
|
||||||
|
// interior box the cases assert on (the middle 10% of the width, a fifth of the way up)
|
||||||
|
// is far from every edge and the corner box they assert the CLEAR on is far outside it.
|
||||||
|
// Flat green: one colour over the whole primitive means an offender pixel is a real
|
||||||
|
// disagreement rather than an interpolation rounding difference between two drivers.
|
||||||
|
constexpr Vertex kTriangle[3] = {
|
||||||
|
{-0.8f, -0.8f, 0.0f, 1.0f, 0.0f},
|
||||||
|
{0.8f, -0.8f, 0.0f, 1.0f, 0.0f},
|
||||||
|
{0.0f, 0.8f, 0.0f, 1.0f, 0.0f},
|
||||||
|
};
|
||||||
|
|
||||||
|
class TriangleScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
// Ready() is false both when there is no GPU and when the base SetUp skipped a
|
||||||
|
// Split lane that has no client to assert against (ScenarioFixture.h).
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glGenBuffers(1, &m_vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(sizeof(kTriangle)), kTriangle, GL_STATIC_DRAW);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
|
||||||
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||||
|
reinterpret_cast<void*>(2 * sizeof(float)));
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "building the one VBO and one VAO this scenario has";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
m_vbo = m_vao = m_program = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear, draw, read back. No glFlush between the draw and the readback: see the
|
||||||
|
// file header, point 2.
|
||||||
|
Image ClearThenDrawThenRead(float clearR, float clearG, float clearB) {
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, gl.Width(), gl.Height());
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
ClearTo(clearR, clearG, clearB, 1.0f);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
return ReadPixels(gl.Width(), gl.Height());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The box inside the triangle: the middle tenth of the width, a fifth of the way up
|
||||||
|
// from the base, which is interior for the vertex set above at any surface size the
|
||||||
|
// harness uses.
|
||||||
|
void ExpectTriangleInterior(const Image& image, const char* color, const std::string& when) {
|
||||||
|
const int w = image.Width();
|
||||||
|
const int h = image.Height();
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, (w * 45) / 100, (w * 55) / 100, (h * 20) / 100,
|
||||||
|
(h * 30) / 100, color, 0.0, when));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The bottom-left corner, which is below the triangle's base and left of its left
|
||||||
|
// edge, so it carries the clear and nothing else.
|
||||||
|
void ExpectClearedCorner(const Image& image, const char* color, const std::string& when) {
|
||||||
|
const int w = image.Width();
|
||||||
|
const int h = image.Height();
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, 0, (w * 5) / 100, 0, (h * 5) / 100, color, 0.0, when));
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -322,7 +322,14 @@ function(add_trace_replay_test CASE_NAME BACKEND)
|
|||||||
CROP_Y
|
CROP_Y
|
||||||
CROP_WIDTH
|
CROP_WIDTH
|
||||||
CROP_HEIGHT
|
CROP_HEIGHT
|
||||||
COHERENT_AS_FLUSH)
|
COHERENT_AS_FLUSH
|
||||||
|
# P5. VARIANT is a NAME SUFFIX and TRANSPORT is what the variant runs with; passing
|
||||||
|
# TRANSPORT without VARIANT is a configure error below, because add_test with a
|
||||||
|
# duplicate NAME is a hard CMake error and the collision is with the very case this
|
||||||
|
# entry is a second arm of.
|
||||||
|
VARIANT
|
||||||
|
TRANSPORT
|
||||||
|
IPC_SERVER_PATH)
|
||||||
cmake_parse_arguments(TRACE_CASE "" "${oneValueArgs}" "" ${ARGN})
|
cmake_parse_arguments(TRACE_CASE "" "${oneValueArgs}" "" ${ARGN})
|
||||||
foreach(required TRACE_ARCHIVE GOLDEN TARGET_CALL WIDTH HEIGHT)
|
foreach(required TRACE_ARCHIVE GOLDEN TARGET_CALL WIDTH HEIGHT)
|
||||||
if(NOT TRACE_CASE_${required})
|
if(NOT TRACE_CASE_${required})
|
||||||
@@ -347,8 +354,38 @@ function(add_trace_replay_test CASE_NAME BACKEND)
|
|||||||
if(NOT TRACE_CASE_CROP_HEIGHT)
|
if(NOT TRACE_CASE_CROP_HEIGHT)
|
||||||
set(TRACE_CASE_CROP_HEIGHT 0)
|
set(TRACE_CASE_CROP_HEIGHT 0)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# --- P5: the variant suffix, and why the OUTPUT DIRECTORIES have to carry it too ----------
|
||||||
|
#
|
||||||
|
# ARCHITECTURE.md:584 asks for the SPLIT suffix because without it the entry collides with
|
||||||
|
# the same case+backend and add_test with a duplicate NAME is a hard CMake error.
|
||||||
|
#
|
||||||
|
# THE DIRECTORIES ARE THE HALF THAT IS EASY TO FORGET AND EXPENSIVE TO GET WRONG. Both arms
|
||||||
|
# of a case write output/mobilegl.log, and run_trace_case.cmake deletes TRACE_OUTPUT_DIR
|
||||||
|
# before every run - so two arms sharing one directory means the second arm's run erases the
|
||||||
|
# first arm's log while `ctest -j` may still be reading it, and, worse, the SPLIT arm's
|
||||||
|
# refusal census is then a census of whichever arm happened to finish last. That log is the
|
||||||
|
# ONLY valid census: the console sink is compiled out of these configurations, so a `ctest -V`
|
||||||
|
# transcript reports a FALSE ZERO for Fatal{ lines. P4a shipped exactly that defect once
|
||||||
|
# already, in a different place. Same argument for TRACE_ARTIFACT_DIR, which is what CI
|
||||||
|
# uploads.
|
||||||
|
set(trace_case_suffix "")
|
||||||
|
set(trace_case_output_tag "${BACKEND}")
|
||||||
|
set(trace_case_artifact_tag "actual-images")
|
||||||
|
if(TRACE_CASE_VARIANT)
|
||||||
|
set(trace_case_suffix ".${TRACE_CASE_VARIANT}")
|
||||||
|
set(trace_case_output_tag "${BACKEND}-${TRACE_CASE_VARIANT}")
|
||||||
|
set(trace_case_artifact_tag "actual-images-${TRACE_CASE_VARIANT}")
|
||||||
|
elseif(TRACE_CASE_TRANSPORT)
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"add_trace_replay_test(${CASE_NAME} ${BACKEND} TRANSPORT ${TRACE_CASE_TRANSPORT}) with no "
|
||||||
|
"VARIANT: the entry would collide with the monolith arm of the same case and backend, and "
|
||||||
|
"a duplicate add_test NAME is a hard error")
|
||||||
|
endif()
|
||||||
|
set(trace_case_name "MobileGLTraceReplay.${CASE_NAME}.${BACKEND}${trace_case_suffix}")
|
||||||
|
|
||||||
add_test(
|
add_test(
|
||||||
NAME MobileGLTraceReplay.${CASE_NAME}.${BACKEND}
|
NAME ${trace_case_name}
|
||||||
COMMAND "${CMAKE_COMMAND}"
|
COMMAND "${CMAKE_COMMAND}"
|
||||||
-DTRACE_REPLAY_EXE=$<TARGET_FILE:mobilegl_trace_replay>
|
-DTRACE_REPLAY_EXE=$<TARGET_FILE:mobilegl_trace_replay>
|
||||||
-DMOBILEGL_LIBRARY=${mobilegl_trace_replay_mobilegl_library}
|
-DMOBILEGL_LIBRARY=${mobilegl_trace_replay_mobilegl_library}
|
||||||
@@ -367,17 +404,30 @@ function(add_trace_replay_test CASE_NAME BACKEND)
|
|||||||
-DTRACE_CROP_WIDTH=${TRACE_CASE_CROP_WIDTH}
|
-DTRACE_CROP_WIDTH=${TRACE_CASE_CROP_WIDTH}
|
||||||
-DTRACE_CROP_HEIGHT=${TRACE_CASE_CROP_HEIGHT}
|
-DTRACE_CROP_HEIGHT=${TRACE_CASE_CROP_HEIGHT}
|
||||||
-DTRACE_COHERENT_AS_FLUSH=${TRACE_CASE_COHERENT_AS_FLUSH}
|
-DTRACE_COHERENT_AS_FLUSH=${TRACE_CASE_COHERENT_AS_FLUSH}
|
||||||
-DTRACE_OUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/${CASE_NAME}/${BACKEND}
|
-DTRACE_TRANSPORT=${TRACE_CASE_TRANSPORT}
|
||||||
-DTRACE_ARTIFACT_DIR=${CMAKE_CURRENT_BINARY_DIR}/${CASE_NAME}/actual-images
|
-DTRACE_IPC_SERVER_PATH=${TRACE_CASE_IPC_SERVER_PATH}
|
||||||
|
-DTRACE_OUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/${CASE_NAME}/${trace_case_output_tag}
|
||||||
|
-DTRACE_ARTIFACT_DIR=${CMAKE_CURRENT_BINARY_DIR}/${CASE_NAME}/${trace_case_artifact_tag}
|
||||||
-P ${MOBILEGL_TRACE_ROOT}/run_trace_case.cmake)
|
-P ${MOBILEGL_TRACE_ROOT}/run_trace_case.cmake)
|
||||||
|
|
||||||
|
# The label list gains `retrace-split` on a variant arm, for the same reason
|
||||||
|
# integration-split exists: a private second label is what lets `ctest -L retrace-split
|
||||||
|
# --no-tests=error` go RED in a build that never registered the arm, instead of greenly
|
||||||
|
# running nothing. `retrace` stays so the existing whole-label selections keep describing the
|
||||||
|
# whole set. A `;` inside a property value has to be escaped.
|
||||||
|
set(trace_case_labels retrace)
|
||||||
|
if(TRACE_CASE_VARIANT)
|
||||||
|
set(trace_case_labels "retrace\;retrace-${TRACE_CASE_VARIANT}")
|
||||||
|
string(TOLOWER "${trace_case_labels}" trace_case_labels)
|
||||||
|
endif()
|
||||||
if(BACKEND STREQUAL "DirectGLES")
|
if(BACKEND STREQUAL "DirectGLES")
|
||||||
set_tests_properties(MobileGLTraceReplay.${CASE_NAME}.${BACKEND} PROPERTIES
|
set_tests_properties(${trace_case_name} PROPERTIES
|
||||||
ENVIRONMENT "EGL_PLATFORM=surfaceless;LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330"
|
ENVIRONMENT "EGL_PLATFORM=surfaceless;LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330"
|
||||||
LABELS retrace)
|
LABELS "${trace_case_labels}")
|
||||||
else()
|
else()
|
||||||
set_tests_properties(MobileGLTraceReplay.${CASE_NAME}.${BACKEND} PROPERTIES
|
set_tests_properties(${trace_case_name} PROPERTIES
|
||||||
ENVIRONMENT "LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330"
|
ENVIRONMENT "LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330"
|
||||||
LABELS retrace)
|
LABELS "${trace_case_labels}")
|
||||||
endif()
|
endif()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
@@ -386,6 +436,26 @@ function(add_trace_replay_test_for_backends CASE_NAME)
|
|||||||
add_trace_replay_test(${CASE_NAME} DirectVulkan ${ARGN})
|
add_trace_replay_test(${CASE_NAME} DirectVulkan ${ARGN})
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
# --- P5: the SPLIT arm of the retrace matrix -------------------------------------------------
|
||||||
|
#
|
||||||
|
# One variant entry per case that trace_cases.json marks `split: true` - today that is OpenRA
|
||||||
|
# alone, which is the only target the phase gate names and (measurably) the only fixture hydrated
|
||||||
|
# locally. DirectGLES only: the split shape's first backend is Espryt, and a DirectVulkan arm
|
||||||
|
# would be measuring a server nobody has written yet.
|
||||||
|
#
|
||||||
|
# REGISTERED ONLY UNDER MOBILEGL_BUILD_DISAGGREGATED, because the arm is a statement about a
|
||||||
|
# library that compiled MG_Remote. A build without the option accepts MOBILEGL_TRANSPORT=inproc
|
||||||
|
# and silently ignores it (CONTRACT-P5 5), so an unconditional registration would be an entry
|
||||||
|
# that passes by running monolith - and run_trace_case.cmake's split block would then be the only
|
||||||
|
# thing standing between that and a green, which is one guard too few for a name that says SPLIT.
|
||||||
|
function(add_trace_replay_split_test CASE_NAME)
|
||||||
|
if(NOT MOBILEGL_BUILD_DISAGGREGATED)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
add_trace_replay_test(${CASE_NAME} DirectGLES VARIANT SPLIT TRANSPORT inproc
|
||||||
|
IPC_SERVER_PATH "${CMAKE_BINARY_DIR}/libMobileGLServer.so" ${ARGN})
|
||||||
|
endfunction()
|
||||||
|
|
||||||
set(MOBILEGL_TRACE_CASES_CMAKE "${CMAKE_CURRENT_BINARY_DIR}/trace_cases.cmake")
|
set(MOBILEGL_TRACE_CASES_CMAKE "${CMAKE_CURRENT_BINARY_DIR}/trace_cases.cmake")
|
||||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${MOBILEGL_TRACE_ROOT}/trace_cases.json")
|
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${MOBILEGL_TRACE_ROOT}/trace_cases.json")
|
||||||
execute_process(
|
execute_process(
|
||||||
|
|||||||
@@ -28,6 +28,30 @@ if(TRACE_COHERENT_AS_FLUSH)
|
|||||||
list(APPEND coherent_as_flush_args --coherent-as-flush)
|
list(APPEND coherent_as_flush_args --coherent-as-flush)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# --- P5: the transport, threaded as an ENVIRONMENT rather than as a replay CLI flag ----------
|
||||||
|
#
|
||||||
|
# MOBILEGL_TRANSPORT reaches the library through the environment, so a `cmake -P` script can set
|
||||||
|
# it and the child inherits it - exactly how the MOBILEGL_PIPE_VERIFY block at the bottom of this
|
||||||
|
# file already works, and with no C++ change anywhere. The desktop CLI has no --env option
|
||||||
|
# (trace_replay_cli.cpp:116-179 is the whole option list) and the Android path already has one
|
||||||
|
# (trace-replay-ci.sh --env -> trace_replay_core.cpp's setenv block), so an environment read
|
||||||
|
# covers both directions and a new flag would buy nothing.
|
||||||
|
#
|
||||||
|
# TWO WAYS IN, ONE ASSERTION. `-DTRACE_TRANSPORT=` is what the SPLIT ctest variant passes, so
|
||||||
|
# that entry is self-describing and needs no ritual around it; exporting MOBILEGL_TRANSPORT in
|
||||||
|
# the calling process is what ~/w7/retrace_gate.py and CI's retrace-split job do over the
|
||||||
|
# UNCHANGED case names, because the gate parses a FOREIGN reference CTestTestfile.cmake whose
|
||||||
|
# name regex cannot see a variant suffix. Setting the variable from the -D FIRST means the
|
||||||
|
# assertions below read one value however it arrived.
|
||||||
|
if(DEFINED TRACE_TRANSPORT AND NOT "${TRACE_TRANSPORT}" STREQUAL "")
|
||||||
|
set(ENV{MOBILEGL_TRANSPORT} "${TRACE_TRANSPORT}")
|
||||||
|
endif()
|
||||||
|
if(DEFINED TRACE_IPC_SERVER_PATH AND NOT "${TRACE_IPC_SERVER_PATH}" STREQUAL "")
|
||||||
|
# ARCHITECTURE.md:543. P6 consumes it; P5 carries it so that "unparsed" and
|
||||||
|
# "parsed and ignored" stop being the same observation.
|
||||||
|
set(ENV{MOBILEGL_IPC_SERVER_PATH} "${TRACE_IPC_SERVER_PATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
if(EXISTS "${TRACE_OUTPUT_DIR}")
|
if(EXISTS "${TRACE_OUTPUT_DIR}")
|
||||||
file(REMOVE_RECURSE "${TRACE_OUTPUT_DIR}")
|
file(REMOVE_RECURSE "${TRACE_OUTPUT_DIR}")
|
||||||
endif()
|
endif()
|
||||||
@@ -189,3 +213,99 @@ if(DEFINED ENV{MOBILEGL_PIPE_VERIFY} AND NOT "$ENV{MOBILEGL_PIPE_VERIFY}" STREQU
|
|||||||
message(STATUS "MGPipe verify: ${pipe_verify_case} armed, zero divergences, zero unmigrated reads")
|
message(STATUS "MGPipe verify: ${pipe_verify_case} armed, zero divergences, zero unmigrated reads")
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# --- P5: MOBILEGL_TRANSPORT, the split arm's own assertions ----------------------------------
|
||||||
|
#
|
||||||
|
# The same problem the verify block above solves, with the same answer and one extra reason to
|
||||||
|
# need it. A retrace that exported MOBILEGL_TRANSPORT=inproc at a library configured WITHOUT
|
||||||
|
# -DMOBILEGL_BUILD_DISAGGREGATED=ON is not merely a no-op: the variable's PARSER does not exist
|
||||||
|
# in that build at all (CONTRACT-P5 5 - putting a complaint in the unconditional part of
|
||||||
|
# ConfigLoader would move a pull-build symbol and break gate G1), so the value is accepted by the
|
||||||
|
# environment and silently ignored, every frame still matches its golden, and the case reports a
|
||||||
|
# clean pass having run monolith end to end.
|
||||||
|
#
|
||||||
|
# THE LIBRARY'S OWN LOG IS THE ONLY CHANNEL a `cmake -P` script has for the difference, and it is
|
||||||
|
# also THE ONLY VALID REFUSAL CENSUS. A `ctest -V` transcript is a FALSE ZERO for Fatal{...}
|
||||||
|
# lines: the console sink is compiled out of the configurations these lanes run, so the aborts
|
||||||
|
# reach output/mobilegl.log and nowhere else. That is why TRACE_OUTPUT_DIR and TRACE_ARTIFACT_DIR
|
||||||
|
# carry the variant - without it both arms write one output/mobilegl.log, the second run wipes
|
||||||
|
# the first, and the census silently becomes a census of one arm. P4a shipped that defect once
|
||||||
|
# already; this is the same defect in a new place, pre-empted.
|
||||||
|
#
|
||||||
|
# Three demands, all silent when MOBILEGL_TRANSPORT is unset or "monolith":
|
||||||
|
# * mobilegl.log exists - the replay wrote one, so the library was loaded and logging;
|
||||||
|
# * it carries ConfigLoader's inproc line, which is emitted ONLY by a build that compiled the
|
||||||
|
# parser AND resolved the value to InProcess. This is the falsifiable half;
|
||||||
|
# * it carries no Fatal{ at all. Under split the emit table raises
|
||||||
|
# Fatal{UnmigratedVerb, "<slot>"} for the 64 slots P5 does not implement, so a clean run of a
|
||||||
|
# reduced-path target is a run that touched none of them - and any other Fatal{ (ProtocolCorruption,
|
||||||
|
# UnmigratedPipeInput, AbiMismatch) is a real defect. The count and the distinct names are
|
||||||
|
# printed either way, because the census is the deliverable even when the run passes.
|
||||||
|
if(DEFINED ENV{MOBILEGL_TRANSPORT} AND NOT "$ENV{MOBILEGL_TRANSPORT}" STREQUAL "")
|
||||||
|
set(split_case "${TRACE_CASE_NAME} ${TRACE_BACKEND}")
|
||||||
|
if("$ENV{MOBILEGL_TRANSPORT}" STREQUAL "monolith")
|
||||||
|
message(STATUS "MGPipe split: MOBILEGL_TRANSPORT=monolith, no split assertions for ${split_case}")
|
||||||
|
elseif(NOT EXISTS "${mobilegl_log}")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"MOBILEGL_TRANSPORT=$ENV{MOBILEGL_TRANSPORT} is set for ${split_case} but the run wrote "
|
||||||
|
"no ${mobilegl_log}, so there is no evidence the transport ever resolved. A split "
|
||||||
|
"retrace with no library log cannot be counted as a split retrace.")
|
||||||
|
else()
|
||||||
|
file(READ "${mobilegl_log}" split_log)
|
||||||
|
# THE DISTINCTIVE PART OF ConfigLoader's INFO LINE, not the bare KEY=VALUE - review finding
|
||||||
|
# M-5. ConfigLoader.cpp:71 logs `Config: Accepted env variable: %s=%s` for EVERY MOBILEGL_*
|
||||||
|
# variable, unconditionally, in every build including the pull one. That line is MGLOG_D,
|
||||||
|
# so at the INFO level CI and the gate use it is compiled out - but at
|
||||||
|
# MOBILEGL_LOG_ACTIVE_LEVEL=..._DEBUG it reads `Config: Accepted env variable:
|
||||||
|
# MOBILEGL_TRANSPORT=inproc` and satisfied the old substring search. Observed GREEN on a
|
||||||
|
# crafted pull-build log. The person most likely to hit that is the one who rebuilds at
|
||||||
|
# DEBUG to debug a split failure. The sentence below exists only in
|
||||||
|
# ConfigLoader::InitTransport's InProcess arm, which exists only under
|
||||||
|
# MOBILEGL_BUILD_DISAGGREGATED.
|
||||||
|
set(split_expected_marker "MOBILEGL_TRANSPORT=inproc - the MGPipe record stream")
|
||||||
|
string(FIND "${split_log}" "${split_expected_marker}" split_armed_at)
|
||||||
|
if(split_armed_at EQUAL -1)
|
||||||
|
# Say which transport was actually asked for. ConfigLoader REFUSES spawn / unix: /
|
||||||
|
# pipe: BY NAME and stays on monolith (they are P6's), so a run that set one of those
|
||||||
|
# has a different diagnosis from one that set inproc against a monolith library, and
|
||||||
|
# the old message named `inproc` either way.
|
||||||
|
set(split_refusal "")
|
||||||
|
if(NOT "$ENV{MOBILEGL_TRANSPORT}" STREQUAL "inproc")
|
||||||
|
set(split_refusal
|
||||||
|
" NOTE: this run asked for '$ENV{MOBILEGL_TRANSPORT}', which P5 does not "
|
||||||
|
"implement - ConfigLoader recognises spawn / unix: / pipe: and REFUSES them by "
|
||||||
|
"name, staying on monolith. Only 'inproc' can resolve in P5.")
|
||||||
|
endif()
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"MOBILEGL_TRANSPORT=$ENV{MOBILEGL_TRANSPORT} is set for ${split_case} and the library "
|
||||||
|
"never reported resolving it: ${mobilegl_log} carries no "
|
||||||
|
"\"${split_expected_marker}\". ConfigLoader::InitTransport logs that line at INFO "
|
||||||
|
"when it selects InProcess, and it exists only in a build configured with "
|
||||||
|
"-DMOBILEGL_BUILD_DISAGGREGATED=ON - in a build without it the whole parser is "
|
||||||
|
"compiled out and the variable is accepted and ignored, which is exactly the 'the "
|
||||||
|
"split lane ran monolith and went green' failure. Check that the SPLIT runtime "
|
||||||
|
"artifact is the one at ${MOBILEGL_LIBRARY}, and that MOBILEGL_LOG_ACTIVE_LEVEL "
|
||||||
|
"admits INFO (at WARN or above the line is compiled out and this reds for no "
|
||||||
|
"defect).${split_refusal}")
|
||||||
|
endif()
|
||||||
|
# The refusal census. Recorded on every split run, pass or fail.
|
||||||
|
file(STRINGS "${mobilegl_log}" split_fatals REGEX "Fatal\\{")
|
||||||
|
list(LENGTH split_fatals split_fatal_count)
|
||||||
|
message(STATUS "MGPipe split: ${split_case} transport=$ENV{MOBILEGL_TRANSPORT}, "
|
||||||
|
"Fatal{ lines in ${mobilegl_log}: ${split_fatal_count}")
|
||||||
|
if(split_fatals)
|
||||||
|
foreach(line IN LISTS split_fatals)
|
||||||
|
message(STATUS "${line}")
|
||||||
|
endforeach()
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"${split_case}: ${split_fatal_count} MGPipe Fatal(s) under MOBILEGL_TRANSPORT="
|
||||||
|
"$ENV{MOBILEGL_TRANSPORT}. Fatal{UnmigratedVerb, \"<slot>\"} is one of the 64 emit-table "
|
||||||
|
"slots P5 leaves unimplemented (R-4) - if the reduced path reached it, either the verb "
|
||||||
|
"census is wrong or this case is not on the reduced path; Fatal{ProtocolCorruption, ...} "
|
||||||
|
"is a record that crossed the wire without declaring its bytes (CONTRACT-P5 rule A); "
|
||||||
|
"Fatal{UnmigratedPipeInput, ...} is a missing row in the field-ownership table. None of "
|
||||||
|
"them is silenced here: this log is the only place they appear, because the console sink "
|
||||||
|
"is compiled out of the configurations this lane runs.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
{
|
{
|
||||||
"name": "OpenRA",
|
"name": "OpenRA",
|
||||||
"verify": true,
|
"verify": true,
|
||||||
|
"split": true,
|
||||||
"trace_archive": "openra.tgz",
|
"trace_archive": "openra.tgz",
|
||||||
"trace_file": "openra.trace",
|
"trace_file": "openra.trace",
|
||||||
"golden": "openra.0000031249.png",
|
"golden": "openra.0000031249.png",
|
||||||
|
|||||||
@@ -8,11 +8,48 @@ from pathlib import Path
|
|||||||
TRACE_CASES_JSON = Path(__file__).with_name("trace_cases.json")
|
TRACE_CASES_JSON = Path(__file__).with_name("trace_cases.json")
|
||||||
CI_BACKENDS = ("DirectGLES", "DirectVulkan")
|
CI_BACKENDS = ("DirectGLES", "DirectVulkan")
|
||||||
|
|
||||||
|
# Every key a case or the defaults block may carry. An UNKNOWN key is a hard error rather than a
|
||||||
|
# silent no-op, which is review finding N-4: `"split": true` mistyped as `"splitt": true` loaded
|
||||||
|
# clean, the split subset became [], the GitHub matrix became {"include":[]}, and `retrace-split`
|
||||||
|
# was skipped with no red anywhere. Every other way of getting `split` wrong already raised
|
||||||
|
# (`"ci": false`, a backend list without DirectGLES, a non-bool value) - the typo was the one hole,
|
||||||
|
# and it is the shape that makes a whole CI job quietly stop existing.
|
||||||
|
#
|
||||||
|
# Adding a key means adding it here, deliberately, in the same commit. That is the point.
|
||||||
|
KNOWN_CASE_KEYS = frozenset({
|
||||||
|
"name",
|
||||||
|
"trace_archive",
|
||||||
|
"trace_file",
|
||||||
|
"golden",
|
||||||
|
"alternate_golden",
|
||||||
|
"target_call",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"ssim_threshold",
|
||||||
|
"crop_x",
|
||||||
|
"crop_y",
|
||||||
|
"crop_width",
|
||||||
|
"crop_height",
|
||||||
|
"coherent_as_flush",
|
||||||
|
"timeout_seconds",
|
||||||
|
"ci",
|
||||||
|
"ci_backends",
|
||||||
|
"verify",
|
||||||
|
"split",
|
||||||
|
"avoid_angle_llvmpipe_explicit_lod_bias",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def load_trace_case_manifest(path=TRACE_CASES_JSON):
|
def load_trace_case_manifest(path=TRACE_CASES_JSON):
|
||||||
with Path(path).open("r", encoding="utf-8") as file:
|
with Path(path).open("r", encoding="utf-8") as file:
|
||||||
manifest = json.load(file)
|
manifest = json.load(file)
|
||||||
defaults = manifest.get("defaults", {})
|
defaults = manifest.get("defaults", {})
|
||||||
|
unknown_defaults = sorted(set(defaults) - KNOWN_CASE_KEYS)
|
||||||
|
if unknown_defaults:
|
||||||
|
raise ValueError(
|
||||||
|
f"unknown key(s) in the defaults block: {', '.join(unknown_defaults)}. "
|
||||||
|
f"Known keys are {', '.join(sorted(KNOWN_CASE_KEYS))}"
|
||||||
|
)
|
||||||
cases = []
|
cases = []
|
||||||
seen = set()
|
seen = set()
|
||||||
for case in manifest.get("cases", []):
|
for case in manifest.get("cases", []):
|
||||||
@@ -20,6 +57,13 @@ def load_trace_case_manifest(path=TRACE_CASES_JSON):
|
|||||||
name = merged.get("name")
|
name = merged.get("name")
|
||||||
if not name:
|
if not name:
|
||||||
raise ValueError("trace case is missing name")
|
raise ValueError("trace case is missing name")
|
||||||
|
unknown = sorted(set(case) - KNOWN_CASE_KEYS)
|
||||||
|
if unknown:
|
||||||
|
raise ValueError(
|
||||||
|
f"unknown key(s) for {name}: {', '.join(unknown)}. A mistyped flag loads clean and "
|
||||||
|
f"turns its whole CI subset into an empty matrix, which GitHub skips with no red. "
|
||||||
|
f"Known keys are {', '.join(sorted(KNOWN_CASE_KEYS))}"
|
||||||
|
)
|
||||||
if name in seen:
|
if name in seen:
|
||||||
raise ValueError(f"duplicate trace case: {name}")
|
raise ValueError(f"duplicate trace case: {name}")
|
||||||
seen.add(name)
|
seen.add(name)
|
||||||
@@ -36,6 +80,23 @@ def load_trace_case_manifest(path=TRACE_CASES_JSON):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"{name} is marked verify but excluded from CI, so the verify matrix would drop it"
|
f"{name} is marked verify but excluded from CI, so the verify matrix would drop it"
|
||||||
)
|
)
|
||||||
|
# "split" opts a case into the MOBILEGL_TRANSPORT=inproc retrace subset, the same shape
|
||||||
|
# and the same reason as "verify" above: a typo has to be a loud manifest error in every
|
||||||
|
# consumer, not a subset that is quietly one case short. The split arm additionally runs
|
||||||
|
# DirectGLES ONLY - the server the arm exercises is Espryt's - so a case that excluded
|
||||||
|
# DirectGLES from CI would leave the split matrix with nothing to run.
|
||||||
|
split = merged.get("split", False)
|
||||||
|
if not isinstance(split, bool):
|
||||||
|
raise ValueError(f"split must be true or false for {name}")
|
||||||
|
if split and not merged.get("ci", True):
|
||||||
|
raise ValueError(
|
||||||
|
f"{name} is marked split but excluded from CI, so the split matrix would drop it"
|
||||||
|
)
|
||||||
|
if split and "DirectGLES" not in ci_backends(merged):
|
||||||
|
raise ValueError(
|
||||||
|
f"{name} is marked split but does not run DirectGLES in CI; the split arm is "
|
||||||
|
f"DirectGLES-only, so the entry would be registered with no backend"
|
||||||
|
)
|
||||||
cases.append(merged)
|
cases.append(merged)
|
||||||
return {"defaults": defaults, "cases": cases}
|
return {"defaults": defaults, "cases": cases}
|
||||||
|
|
||||||
@@ -92,6 +153,16 @@ def verify_trace_cases(cases):
|
|||||||
return [case for case in cases if case.get("verify", False)]
|
return [case for case in cases if case.get("verify", False)]
|
||||||
|
|
||||||
|
|
||||||
|
def split_trace_cases(cases):
|
||||||
|
"""The subset the split (MOBILEGL_TRANSPORT=inproc) CI mode retraces.
|
||||||
|
|
||||||
|
P5's phase gate names exactly one: OpenRA, at SSIM >= 0.99. It is also the only fixture that
|
||||||
|
is hydrated locally, so keeping the subset explicit in the manifest is what stops a later
|
||||||
|
phase from widening the arm into an LFS fetch by accident.
|
||||||
|
"""
|
||||||
|
return [case for case in cases if case.get("split", False)]
|
||||||
|
|
||||||
|
|
||||||
def ci_backends(case):
|
def ci_backends(case):
|
||||||
backends = case.get("ci_backends")
|
backends = case.get("ci_backends")
|
||||||
if backends is None:
|
if backends is None:
|
||||||
@@ -122,6 +193,16 @@ def github_verify_matrix(cases):
|
|||||||
return github_test_matrix(verify_trace_cases(cases))
|
return github_test_matrix(verify_trace_cases(cases))
|
||||||
|
|
||||||
|
|
||||||
|
def github_split_matrix(cases):
|
||||||
|
"""{backend, case} for the split arm. DirectGLES only - see split_trace_cases."""
|
||||||
|
return {
|
||||||
|
"include": [
|
||||||
|
{"backend": "DirectGLES", "case": case["name"]}
|
||||||
|
for case in split_trace_cases(cases)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def github_apk_matrix(cases):
|
def github_apk_matrix(cases):
|
||||||
backends = {
|
backends = {
|
||||||
"DirectGLES": {"name": "DirectGLES", "gpu": "software"},
|
"DirectGLES": {"name": "DirectGLES", "gpu": "software"},
|
||||||
@@ -158,8 +239,8 @@ def emit_cmake(cases, fixture_root):
|
|||||||
("CROP_HEIGHT", "crop_height", False),
|
("CROP_HEIGHT", "crop_height", False),
|
||||||
("COHERENT_AS_FLUSH", "coherent_as_flush", False),
|
("COHERENT_AS_FLUSH", "coherent_as_flush", False),
|
||||||
]
|
]
|
||||||
for case in cases:
|
def emit_one(function, case):
|
||||||
lines.append(f"add_trace_replay_test_for_backends({cmake_quote(case['name'])}")
|
lines.append(f"{function}({cmake_quote(case['name'])}")
|
||||||
for cmake_key, json_key, fixture_path in keys:
|
for cmake_key, json_key, fixture_path in keys:
|
||||||
value = case.get(json_key)
|
value = case.get(json_key)
|
||||||
if value is None or value == "":
|
if value is None or value == "":
|
||||||
@@ -169,6 +250,14 @@ def emit_cmake(cases, fixture_root):
|
|||||||
lines.append(f" {cmake_key} {cmake_quote(value)}")
|
lines.append(f" {cmake_key} {cmake_quote(value)}")
|
||||||
lines.append(")")
|
lines.append(")")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
|
for case in cases:
|
||||||
|
emit_one("add_trace_replay_test_for_backends", case)
|
||||||
|
# P5's split arm, emitted beside the monolith pair rather than in a block of its own so
|
||||||
|
# that a case and its variant always carry identical parameters. The CMake side is a
|
||||||
|
# no-op unless MOBILEGL_BUILD_DISAGGREGATED is ON.
|
||||||
|
if case.get("split", False):
|
||||||
|
emit_one("add_trace_replay_split_test", case)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -183,6 +272,7 @@ def parse_args():
|
|||||||
"names",
|
"names",
|
||||||
"github-test-matrix",
|
"github-test-matrix",
|
||||||
"github-verify-matrix",
|
"github-verify-matrix",
|
||||||
|
"github-split-matrix",
|
||||||
"github-apk",
|
"github-apk",
|
||||||
"github-apk-matrix",
|
"github-apk-matrix",
|
||||||
"fixture-files",
|
"fixture-files",
|
||||||
@@ -204,6 +294,8 @@ def main():
|
|||||||
print(json.dumps(github_test_matrix(cases), separators=(",", ":")))
|
print(json.dumps(github_test_matrix(cases), separators=(",", ":")))
|
||||||
elif args.format == "github-verify-matrix":
|
elif args.format == "github-verify-matrix":
|
||||||
print(json.dumps(github_verify_matrix(cases), separators=(",", ":")))
|
print(json.dumps(github_verify_matrix(cases), separators=(",", ":")))
|
||||||
|
elif args.format == "github-split-matrix":
|
||||||
|
print(json.dumps(github_split_matrix(cases), separators=(",", ":")))
|
||||||
elif args.format == "github-apk":
|
elif args.format == "github-apk":
|
||||||
print(json.dumps([github_apk_case(case) for case in cases], separators=(",", ":")))
|
print(json.dumps([github_apk_case(case) for case in cases], separators=(",", ":")))
|
||||||
elif args.format == "github-apk-matrix":
|
elif args.format == "github-apk-matrix":
|
||||||
|
|||||||
Reference in New Issue
Block a user