Compare commits

...
24 Commits
Author SHA1 Message Date
swung0x48 c4eb620982 [Docs] (MG_Backend/DirectVulkan): fix two comments citing the deleted floor line and sink tallies 2026-10-18 10:02:09 -04:00
swung0x48 584641dc5e [Test] (docs/ci, MG_IntegrationTest/Harness): keep the ambient CtWire log to the three cases that count
- a log on the ambient RoleViolation entries would arm its guard-line check, which they never ran
2026-10-18 10:02:09 -04:00
swung0x48 21925f99d4 [Refactor, Test] (MG_Impl/Pipe, MG_Test/Pipe): rename ResetForTest to ForgetSession where the session end calls it 2026-10-18 10:02:08 -04:00
swung0x48 12a96be87c [Refactor] (MG_Backend/DirectVulkan): drop the always-printed MGWIRE-FLOOR line from the decline dump 2026-10-18 10:02:07 -04:00
swung0x48 fd2cdf9e21 [Refactor] (MG_Backend/DirectVulkan): delete the MOBILEGL_MAGMA_WIREBUF_PROBE investigation probe 2026-10-18 10:02:06 -04:00
swung0x48 0d11af0d86 [Refactor] (MG_Util/ShaderTranspiler): delete the uncalled TranslationCache::SetCapsForTesting 2026-10-18 10:02:05 -04:00
swung0x48 9ad0d4835d [Refactor] (android-plugin/app, MobileGL): delete the unread MOBILEGL_TRACE_SKIP_AUTODESTROY knob 2026-10-18 10:02:05 -04:00
swung0x48 ee30726fde [Refactor, Test] (docs/ci, docs/Disaggregated, MG_IntegrationTest, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, MG_Remote/Server, MG_Test/Util, MG_Test/Wire, MG_Util/Metrics, scripts/ci, tools/trace_replay): delete the MOBILEGL_TEST_SERVER_COUNTERS knob
- CtWireScenario reads `ar` / `od` off the server's PipeStats line on every arm
- the serial == resets assertion could not fail and is dropped
- ServerVerbSink's unread BindContexts, Presents and LastPresentSerial go too
2026-10-18 10:02:04 -04:00
swung0x48 9824beabd1 [Docs] (MG_Pipe, MobileGL): describe the bad-knob Fatal and the arm-time setting lookup 2026-10-18 10:02:03 -04:00
swung0x48 ead1a78bec [Test] (.github/workflows, docs/ci, MG_IntegrationTest, MG_IntegrationTest/Scenarios, MG_Test/Pipe): restore the fatal-arm, per-verb capability and verb-scope checks
- Armed requires fatal=1; the monolith control envs share the ambient lane's base list
- the capability field is corrupted through the knob at Clear, DrawArrays and ReadPixels
- the library's poison omission is checked to match its verb, not its class
2026-10-18 10:02:02 -04:00
swung0x48 b2daab61b7 [Refactor, Test] (docs/ci, docs/Disaggregated, MG_Backend/MGPipe, MG_Impl/Pipe, MG_IntegrationTest, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, MG_Pipe, MG_Test/Pipe, MobileGL, scripts/data, tools/trace_replay): delete the MOBILEGL_PIPE_POISON_OMIT knob
- the PoisonOmitted and VerifySplitPoisonOmitted entries set the harness marker MGITEST_POISON_OMIT; the re-executed child arms the pair through Harness/PoisonOmissionArm.h
- MGPipeSetPoisonOmission, the stamp withhold, the name lookups and the server half (now one atomic word) compile only with MOBILEGL_PIPE_VERIFY
- three PipeInputsTest cases withhold the stamp test-side; PoisonOmitKnobArmsTheOmission and BadPoisonOmitKnobIsFatalNamingTheKnob are retired
- skip census: the two retired cases leave the unit-strict and unit-verify names
2026-10-18 10:02:02 -04:00
swung0x48 9b5a60b2f7 [Refactor, Test] (.github/workflows, docs/ci, docs/Disaggregated, MG_Impl/Pipe, MG_IntegrationTest, MG_IntegrationTest/Scenarios, MG_Test/Pipe, MobileGL, scripts/data): read MOBILEGL_PIPE_VERIFY_CORRUPT only in verify builds
- ArmVerify reads it through LookupSetting; Features.PipeVerifyCorrupt is gone
- unit PipeInputsTest.EveryCorruptedFieldIsNamedByTheComparator replaces negative control A2
- integration-verify's controls A and A2 and integration-verify-split's control A with its log copy step are retired
- PipeVerifyArmingScenario.Armed no longer skips when the knob is set
- skip census: the new case is a listed skip in the unit, unit-split and unit-strict lanes
2026-10-18 10:02:01 -04:00
swung0x48 f5df1f893e [Refactor] (docs/Disaggregated, MobileGL): force the verify IPC settings only in builds with the comparator 2026-10-18 10:02:00 -04:00
swung0x48 ebd7926604 [CI] (.github/workflows, docs/ci, scripts/ci): keep the split lane's fallback run when the lane step never ran
- 'Split lane fallback (lane step skipped)' runs the ownership check and the DirectGLES.Split. entries only when split-baseline.xml is missing, as the retired controls step did
- the integration-split header and junit_tally.py's docstring stop naming the retired controls
2026-10-18 10:01:59 -04:00
swung0x48 95ecdd21e5 [Test] (docs/ci, MG_IntegrationTest, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, scripts/data): check the push control across a Present and only where it can run
- the forgotten mapping's next-frame write must also miss the server; the re-announce must ship the latest write
- the case is left out of the ambient, Verify, VerifySplit and Magma Full registrations, where it only skipped
- DirectVulkan.Split pins MOBILEGL_IPC_ADOPT_TIER=2 for it, so its emulated declaration holds
2026-10-18 10:01:58 -04:00
swung0x48 87b83384c8 [Refactor, Test] (docs/ci, docs/Disaggregated, MG_IntegrationTest/Scenarios, MG_State/GLState, MG_Test, MG_Test/Buffer, MG_Util/Metrics, MobileGL, scripts/data): fix the persistent-map push block at 64 KiB
- retires SplitBufferSet.AZeroBlockSizeTurnsThePushOffRatherThanMakingItUnlimited
2026-10-18 10:01:57 -04:00
swung0x48 9032561722 [Refactor, Test] (docs/ci, docs/Disaggregated, MG_Backend/MGPipe, MG_Remote/Client, MG_Remote/Transport, MG_Test/Wire, MobileGL, scripts/data, tools/trace_replay): delete the MOBILEGL_IPC_VERB_BARRIER knob
- retires RemoteRunAhead.ClearingTheVerbBarrierDisarmsRunAheadEvenWithTheCapBit
2026-10-18 10:01:56 -04:00
swung0x48 81994940c4 [CI] (.github/workflows, docs/ci, docs/Disaggregated, MG_IntegrationTest, MG_IntegrationTest/Harness, scripts/ci, scripts/data): retire the split barrier and map-push negative controls
- skip census: lane junit-integration-split/control retired with its step
2026-10-18 10:01:56 -04:00
swung0x48 82a8fb816e [Test] (MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, MG_Test/Wire, scripts/data): add in-test controls for the map push and the wait probe
- PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer
- RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait
2026-10-18 10:01:54 -04:00
swung0x48 1d13ea8280 [CI] (.github/workflows): retry a timed-out retrace result upload once 2026-10-18 08:44:22 -04:00
swung0x48 4422732e98 [CI] (.github/workflows): let the emulator retrace jobs use the whole runner pool
- APK is the later of the two runs (64 against 38 min), so its jobs no longer leave runners idle
2026-10-18 07:26:23 -04:00
swung0x48 da1ec09c27 [CI] (.github/workflows, android-plugin): cache a pre-booted AVD, not boot snapshots
- snapshots doubled the cache restore (25 to 52 s) and device-offline retries went from 3 to 10
- the AVD job boots the new AVD once and powers it off; retrace jobs cold-boot it as before
2026-10-18 07:06:46 -04:00
swung0x48 8f64fa7686 [CI] (.github/workflows, android-plugin): boot retrace emulators from a cached snapshot
- the AVD job boots the new AVD once per GPU mode and saves a snapshot into the cache
- retrace jobs load their mode's snapshot, log whether it loaded, and skip SDK setup on a hit
2026-10-18 05:42:48 -04:00
swung0x48 a26d9a4482 [CI] (.github/workflows): run 16 emulator retrace jobs at a time
- 231 jobs at 4 a time took 182 of the run's 198 minutes while 16 runners sat idle
2026-10-18 05:32:39 -04:00
82 changed files with 1085 additions and 1994 deletions
+71 -14
View File
@@ -379,7 +379,7 @@ jobs:
${{ env.ANDROID_SDK_ROOT }}/platform-tools
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v4-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
- name: Create AVD
if: steps.android-avd-cache.outputs.cache-hit != 'true'
@@ -391,6 +391,33 @@ jobs:
--profile pixel_6 \
--avd-name "${AVD_NAME}"
- name: Enable KVM
if: steps.android-avd-cache.outputs.cache-hit != 'true'
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
# Boot the new AVD once so the cached AVD is past its first-boot setup (measured 37 s first
# boot, 26 s for the next cold boot). A quick-boot snapshot was tried instead: it doubled the
# cache restore and tripled "device offline" retries, so retrace jobs cold-boot. If the
# pre-boot fails, the AVD is created afresh so the cache never holds a half-booted one.
- name: Pre-boot AVD
if: steps.android-avd-cache.outputs.cache-hit != 'true'
run: |
log="${RUNNER_TEMP}/avd-preboot.log"
pid="${RUNNER_TEMP}/avd-preboot.pid"
if ! sh android-plugin/run-avd-ci.sh preboot --avd-name "${AVD_NAME}" --gpu software \
--emulator-log "${log}" --pid-file "${pid}" --boot-timeout 300; then
echo "::warning::the AVD pre-boot failed; caching a never-booted AVD instead"
tail -50 "${log}" || true
sh android-plugin/run-avd-ci.sh stop --avd-name "${AVD_NAME}" --emulator-log "${log}" --pid-file "${pid}"
timeout 60 tail --pid="$(cat "${pid}")" -f /dev/null || true
sh android-plugin/run-avd-ci.sh create --api-level 35 --target google_apis --arch x86_64 \
--profile pixel_6 --avd-name "${AVD_NAME}"
fi
du -sh "${ANDROID_AVD_HOME}"
retrace:
name: retrace (${{ matrix.backend.name }} / ${{ matrix.transport || 'monolith' }}, ${{ matrix.case.name }})
runs-on: ubuntu-26.04
@@ -410,7 +437,10 @@ jobs:
ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
strategy:
fail-fast: false
max-parallel: 4
# The account runs at most 20 jobs at once, shared with Test. These ~230 emulator jobs are
# the longer of the two runs, so they may take the whole pool: what is minimised is when the
# later of APK and Test finishes, not Test alone.
max-parallel: 20
matrix: ${{ fromJSON(needs.trace-cases.outputs.android) }}
steps:
- name: Set swap space
@@ -438,8 +468,29 @@ jobs:
echo "EMULATOR_LOG=${RUNNER_TEMP}/mobilegl-emulator.log" >> "$GITHUB_ENV"
echo "EMULATOR_PID_FILE=${RUNNER_TEMP}/mobilegl-emulator.pid" >> "$GITHUB_ENV"
- name: Restore Android AVD cache
id: android-avd-cache
uses: actions/cache/restore@v5
with:
path: |
${{ env.ANDROID_AVD_HOME }}
${{ env.ANDROID_SDK_ROOT }}/emulator
${{ env.ANDROID_SDK_ROOT }}/platform-tools
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v4-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
# The cache holds everything a retrace job runs (emulator, adb, image, AVD); the SDK manager
# is only needed to create the AVD when the cache missed.
- name: Use the cached SDK tools
if: steps.android-avd-cache.outputs.cache-hit == 'true'
run: |
echo "${ANDROID_SDK_ROOT}/platform-tools" >> "$GITHUB_PATH"
echo "${ANDROID_SDK_ROOT}/emulator" >> "$GITHUB_PATH"
- name: Setup Android SDK
id: netsetup415
if: steps.android-avd-cache.outputs.cache-hit != 'true'
continue-on-error: true
timeout-minutes: 15
uses: android-actions/setup-android@v4
@@ -458,20 +509,9 @@ jobs:
cmdline-tools-version: 13114758
- name: Accept Android SDK licenses
if: steps.android-avd-cache.outputs.cache-hit != 'true'
run: yes | sdkmanager --licenses >/dev/null
- name: Restore Android AVD cache
id: android-avd-cache
uses: actions/cache/restore@v5
with:
path: |
${{ env.ANDROID_AVD_HOME }}
${{ env.ANDROID_SDK_ROOT }}/emulator
${{ env.ANDROID_SDK_ROOT }}/platform-tools
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
- name: Download retrace APK
uses: actions/download-artifact@v8
with:
@@ -659,13 +699,30 @@ jobs:
--pid-file "${EMULATOR_PID_FILE}"
- name: Upload Android retrace result
id: upload-retrace-result
if: always()
continue-on-error: true
uses: actions/upload-artifact@v7
with:
name: MobileGL-android-retrace-result-${{ matrix.lane || 'monolith-control' }}-${{ matrix.transport || 'monolith' }}-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
path: android-retrace-result/**
if-no-files-found: warn
# The artifact service sometimes times out (ETIMEDOUT on CreateArtifact, one job in 231 on
# 2026-10-18), which turned a passing case red; one delayed retry absorbs that.
- name: Wait before retrying the upload
if: always() && steps.upload-retrace-result.outcome == 'failure'
run: sleep 30
- name: Upload Android retrace result (retry)
if: always() && steps.upload-retrace-result.outcome == 'failure'
uses: actions/upload-artifact@v7
with:
name: MobileGL-android-retrace-result-${{ matrix.lane || 'monolith-control' }}-${{ matrix.transport || 'monolith' }}-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
path: android-retrace-result/**
if-no-files-found: warn
overwrite: true
retrace-summary:
name: retrace summary
runs-on: ubuntu-26.04
+35 -127
View File
@@ -799,9 +799,9 @@ jobs:
if-no-files-found: error
# The verify lane itself, plus the controls that keep it falsifiable: its own control entries,
# checked by name, and the two corrupted-snapshot steps. They are always on, not a manual
# exercise: a gate that can only be shown to work by someone remembering to break it on purpose
# is a gate that has already stopped working.
# checked by name, and the comparator's unit cases on its library. They are always on, not a
# manual exercise: a gate that can only be shown to work by someone remembering to break it on
# purpose is a gate that has already stopped working.
integration-verify:
runs-on: ubuntu-26.04
timeout-minutes: 180
@@ -879,7 +879,8 @@ jobs:
# skip in the `test` job's build and assert only here, as do poison-gated cases on branches
# where that build is a monolith. The packaging step above ships MG_Test whole, so the cost
# is the run itself. --no-tests=error, so a packaging change that dropped the unit binaries
# fails here instead of passing with nothing run.
# fails here instead of passing with nothing run. Among them is the comparator's per-field
# control, PipeInputsTest.EveryCorruptedFieldIsNamedByTheComparator.
- name: Unit tests on the verify runtime (G6, G10)
working-directory: build-verify
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)" --output-junit "${RUNNER_TEMP}/unit-verify.xml"
@@ -904,8 +905,10 @@ jobs:
# The lane's own control entries, by name: each must have run and passed, and a skip is a
# failure here although ctest reports it green. VerifyArming asserts in-test that the
# comparator printed its armed line, VerifyCorrupted that a corrupted field is reported by
# name, and PoisonOmitted that an omitted stamp kills a forked child with its own Fatal.
# comparator printed its armed line with fatal=1, under the ambient entries' own environment
# (MG_IntegrationTest/CMakeLists.txt builds both from one list), VerifyCorrupted that a
# corrupted field is reported by name, and PoisonOmitted that an omitted stamp kills a
# forked child with its own Fatal.
- name: Verify lane control entries
run: |
python3 scripts/ci/junit_tally.py "${RUNNER_TEMP}/verify.xml" \
@@ -916,59 +919,6 @@ jobs:
--require-entry-passed DirectGLES.PoisonOmitted.PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb \
--require-entry-passed DirectVulkan.PoisonOmitted.PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb
# NEGATIVE CONTROL A (gate G4). The knob perturbs one field in the snapshot arm before the
# entry compare, so a working comparator must abort the run. This step passes when ctest
# FAILS - `if ctest ...; then error` - which is the only shape that can catch a comparator
# that silently compares nothing.
#
# The knob reaches the test process through the JOB environment: no ctest ENVIRONMENT
# property on the ambient Verify. entries names it (MG_IntegrationTest/CMakeLists.txt says
# so out loud), and a property entry would otherwise override this and the control would
# prove nothing. Same precedent as MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH in `integration`.
- name: Negative control A (corrupted snapshot)
working-directory: build-verify
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_PIPE_VERIFY_CORRUPT: GetRenderStateParameters
run: |
FILTER='DirectGLES\.Verify\..*ClearThenReadPixels'
# An empty selection would ALSO make ctest exit non-zero (--no-tests=error), and this
# step reads non-zero as "the control worked" - so the selection is counted first. A
# control that passes because it ran nothing is worse than no control.
matched=$(ctest -N -L integration-verify -R "${FILTER}" | grep -cE '^ *Test *#[0-9]+:')
if [ "${matched}" -lt 1 ]; then
echo "::error::negative control A selected ${matched} tests; its filter no longer matches anything"
exit 1
fi
if ctest --output-on-failure -L integration-verify -R "${FILTER}" --no-tests=error; then
echo "::error::MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters left ${matched} verify entries GREEN. The comparator is not comparing, so every green entry above is green for no reason."
exit 1
fi
echo "the corrupted field turned ${matched} selected entries red, as it must"
# NEGATIVE CONTROL A2 (P13 W3c): THE CAPABILITY CROSS-CHECK MOVED HERE. ResidualValueBlock
# carried the 35 capability bits redundantly so the applier could trip on a capability no
# record carried; P13 deleted it, and the intent is the comparator's now. So a corrupted
# capability field must turn the same selection red, or the block's deletion lost its
# check rather than moving it.
- name: Negative control A2 (corrupted capability)
working-directory: build-verify
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_PIPE_VERIFY_CORRUPT: IsCapabilityEnabled
run: |
FILTER='DirectGLES\.Verify\..*ClearThenReadPixels'
matched=$(ctest -N -L integration-verify -R "${FILTER}" | grep -cE '^ *Test *#[0-9]+:')
if [ "${matched}" -lt 1 ]; then
echo "::error::negative control A2 selected ${matched} tests; its filter no longer matches anything"
exit 1
fi
if ctest --output-on-failure -L integration-verify -R "${FILTER}" --no-tests=error; then
echo "::error::MOBILEGL_PIPE_VERIFY_CORRUPT=IsCapabilityEnabled left ${matched} verify entries GREEN. The comparator does not compare the capability field, so deleting the residual block (P13 W3c) lost the check it carried."
exit 1
fi
echo "the corrupted capability field turned ${matched} selected entries red, as it must"
- name: Upload verify lane logs
if: ${{ always() }}
uses: actions/upload-artifact@v7
@@ -995,7 +945,7 @@ jobs:
if-no-files-found: ignore
# The comparator on the split (inproc) arm, both backends, with its control entries checked by
# name and the corrupted-snapshot control red on that arm.
# name.
#
# ITS OWN BUILD, because neither existing one can carry it: build-linux-verify configures
# without MOBILEGL_BUILD_DISAGGREGATED (so MG_Remote and the transport parser do not exist and
@@ -1115,49 +1065,6 @@ jobs:
done
python3 scripts/ci/junit_tally.py "${RUNNER_TEMP}/verify-split.xml" "${args[@]}"
# THE GREEN LANE'S LOGS, KEPT BEFORE THE CONTROLS OVERWRITE THEM. The control step below
# re-runs ambient VerifySplit. entries with a knob exported, and an ambient entry writes to
# ITS OWN per-entry path under verify-split-logs/ - which the library opens "w". So by the
# time the upload step at the end of this job runs, the selected files are the CONTROL
# runs' logs sitting under the LANE's names: an artifact showing deliberate divergences,
# with the green run they were meant to be evidence of already gone. `if: always()` on the
# upload made that worse rather than better, because a failure investigation is exactly
# when the overwritten half is the half somebody needs.
- name: Keep lane logs before controls
if: ${{ always() }}
working-directory: build-verify-split/MobileGL/MG_IntegrationTest
run: |
rm -rf verify-split-logs-lane
cp -a verify-split-logs verify-split-logs-lane
cp -p "${RUNNER_TEMP}/verify-split.xml" verify-split-logs-lane/ || true
echo "kept $(ls verify-split-logs-lane | wc -l) file(s) from the green lane run"
# NEGATIVE CONTROL A ON THE SPLIT ARM - integration-verify's step A, same knob, same shape
# (this step passes when ctest FAILS), aimed at the ambient VerifySplit. entries of BOTH
# backends. Measured at landing: 10 of 10 red, each with
# Fatal{PipeVerifyDiffer, "GetRenderStateParameters@Clear"|"@DrawArrays", ..., where=entry}.
- name: Negative control A (corrupted snapshot)
working-directory: build-verify-split
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_PIPE_VERIFY_CORRUPT: GetRenderStateParameters
run: |
FILTER='(DirectGLES|DirectVulkan)\.VerifySplit\..*ClearThenReadPixels'
matched=$(ctest -N -L '^integration-verify-split$' -R "${FILTER}" | grep -cE '^ *Test *#[0-9]+:')
if [ "${matched}" -lt 2 ]; then
echo "::error::split negative control A selected ${matched} tests; its filter no longer matches both backends"
exit 1
fi
if ctest --output-on-failure -L '^integration-verify-split$' -R "${FILTER}" --no-tests=error; then
echo "::error::MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters left ${matched} verify-split entries GREEN. The comparator is not comparing on the split arm."
exit 1
fi
echo "the corrupted field turned ${matched} selected split entries red, as it must"
# BOTH SETS, AND THEY ARE DIFFERENT EVIDENCE. verify-split-logs-lane/ is the green run the
# arm proof read; verify-split-logs/ is whatever the control step left behind, which for
# its entries is a deliberately red re-run under the same name. Uploading only the
# second - which is what this step did before the copy above existed - loses the first.
# The run log needs a signed-in reader; annotations do not. Name every failed ctest entry,
# with the lines of its output that say why, where anyone reading the run can see them.
- name: Annotate failed entries
@@ -1170,7 +1077,6 @@ jobs:
with:
name: integration-verify-split-logs
path: |
build-verify-split/MobileGL/MG_IntegrationTest/verify-split-logs-lane/**
build-verify-split/MobileGL/MG_IntegrationTest/verify-split-logs/**
build-verify-split/MobileGL/MG_IntegrationTest/pipe-*split*.log*
if-no-files-found: warn
@@ -1551,7 +1457,9 @@ jobs:
if-no-files-found: error
# The split lane itself: the three-arm shape ARCHITECTURE.md §13.2 asks for verbatim, plus the
# first real run of the five MG_Test/Wire suites, plus P5's two exit-gate negative controls.
# first real run of the five MG_Test/Wire suites. P5's two exit-gate negative controls are in-test
# now (RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait and
# PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer).
integration-split:
runs-on: ubuntu-26.04
timeout-minutes: 120
@@ -1689,11 +1597,7 @@ jobs:
ulimit -c unlimited
export MOBILEGL_FLATC_EXECUTABLE="${RUNNER_TEMP}/flatc-build/flatc"
test -x "${MOBILEGL_FLATC_EXECUTABLE}"
# The exit code is kept beside the JUnit: the controls step tallies this run as its baseline.
rc=0
ctest --output-on-failure -L integration-split --no-tests=error -j 4 --output-junit "${RUNNER_TEMP}/split-baseline.xml" || rc=$?
echo "${rc}" > "${RUNNER_TEMP}/split-baseline.rc"
[ "${rc}" -eq 0 ] || exit "${rc}"
ctest --output-on-failure -L integration-split --no-tests=error -j 4 --output-junit "${RUNNER_TEMP}/split-baseline.xml"
python3 ../scripts/ci/junit_tally.py "${RUNNER_TEMP}/split-baseline.xml" --require-split-ran
# Exit gate 9.3: the spawn arm green at the same count. It runs the same cases through a
# SECOND PROCESS, so a failure here that the line above did not see is a transport fact
@@ -1782,20 +1686,28 @@ jobs:
print(f"the two arms agree on all {len(a)} entries, name and status")
PY
# Negative controls for the verb barrier (MOBILEGL_IPC_VERB_BARRIER=0) and the persistent-map
# push (MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0): each knob must turn its selected entries red.
# The body is scripts/ci/split_negative_controls.sh, so scripts/ci/control_smoke_test.sh can
# run the same lines off a runner. It needs a green baseline with at least one passed entry,
# tallied from the lane step's JUnit and exit code (re-run only when that step never ran),
# and accepts a red only when it carries the control's own diagnostic.
- name: Negative controls (barrier, map push)
# The DirectGLES.Split. entries and their log-ownership check, once, in a job whose 'Split
# lanes' step never ran because an earlier step failed: without this, a red unit lane or
# flatc build would hide every split integration result of the run. When the lane step ran
# it left split-baseline.xml, and this step does nothing.
- name: Split lane fallback (lane step skipped)
if: ${{ !cancelled() }}
working-directory: ${{ env.SPLIT_BUILD_DIR }}
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
CONTROL_TMPDIR: ${{ runner.temp }}
SPLIT_BASELINE_JUNIT: ${{ runner.temp }}/split-baseline.xml
run: bash "${GITHUB_WORKSPACE}/scripts/ci/split_negative_controls.sh"
run: |
if [ -f "${RUNNER_TEMP}/split-baseline.xml" ]; then
echo "the lane step ran and left split-baseline.xml; nothing to fall back to"
exit 0
fi
echo "::notice::the lane step left no split-baseline.xml, so the DirectGLES.Split. entries run here"
python3 "${GITHUB_WORKSPACE}/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py" check ctest "$PWD"
# A directory of its own: the job uploads RUNNER_TEMP/*.xml for the skip census, which
# has no row for this run.
mkdir -p "${RUNNER_TEMP}/split-fallback"
ctest --output-on-failure -L integration-split -R '^DirectGLES\.Split\.' -j 4 --no-tests=error \
--output-junit "${RUNNER_TEMP}/split-fallback/isplit.xml"
python3 "${GITHUB_WORKSPACE}/scripts/ci/junit_tally.py" "${RUNNER_TEMP}/split-fallback/isplit.xml" --require-split-ran
# build-linux ran the server link-closure ratchet on the build these lanes took, with
# continue-on-error so that a red there does not skip build-linux's own consumers.
@@ -3869,15 +3781,11 @@ jobs:
python3 scripts/gen_pipe_field_ownership.py --check
python3 scripts/gen_pipe_field_ownership.py --self-test
# Negative controls must themselves be able to go red (ARCHITECTURE.md §13.2).
# control_smoke_test.sh runs the split control script against a stubbed ctest: an unrelated
# failure or an empty selection must make it report FAILED, and the scenarios' real
# diagnostics must make it report PASSED. The Python tests run the trace and TCP lane tooling
# on stubs and synthetic data; test_run_trace_case.py feeds run_trace_case.cmake canned logs.
# No branch guard; no build, GPU or device.
# Negative controls must themselves be able to go red (ARCHITECTURE.md §13.2). The Python
# tests run the trace and TCP lane tooling on stubs and synthetic data; test_run_trace_case.py
# feeds run_trace_case.cmake canned logs. No branch guard; no build, GPU or device.
- name: Control and trace-tool smoke tests (R-16)
run: |
bash scripts/ci/control_smoke_test.sh
python3 scripts/ci/test_trace_infrastructure.py
python3 scripts/ci/tcp_lane_tools_test.py
python3 tools/trace_replay/test_run_tcp_matrix.py
+5 -32
View File
@@ -244,8 +244,6 @@ namespace MobileGL::MG_Config {
// flush calls on rewritten maps become error-free no-ops. Non-persistent maps
// keep spec FLUSH_EXPLICIT behavior.
Bool CoherentAsFlush = false;
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
Bool TraceSkipAutodestroy = false;
// MOBILEGL_ESPRYT_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
// (negative control / driver-bug escape hatch).
@@ -463,24 +461,12 @@ namespace MobileGL::MG_Config {
// the semantic gate that replaces byte identity, and it catches the dangerous
// direction - a dirty bit that fires too RARELY - which no purity gate can see.
Bool PipeVerify = false;
// The three knobs of the MOBILEGL_PIPE_VERIFY build (P1 brief D2). Compiled only
// under MOBILEGL_PIPE_PUSH so the pull build's FeaturesTable does not change size.
// MOBILEGL_PIPE_VERIFY_FATAL: the first divergence aborts (default). 0 logs and
// counts instead, for triage and for the lane that must survive to read its own
// log. Tri-state parse like PipeLegacyMemos: only an explicit falsy value turns it
// off.
// log. Tri-state parse: only an explicit falsy value turns it off. The comparator's
// negative control, MOBILEGL_PIPE_VERIFY_CORRUPT, has no field here: only a verify
// build reads it, when the comparator arms (MG_Impl/Pipe/PipeFill.cpp, ArmVerify).
Bool PipeVerifyFatal = true;
// MOBILEGL_PIPE_VERIFY_CORRUPT: a field name from kMGPipeInputFieldNames[]; the
// comparator perturbs that field in the SNAPSHOT arm before the entry compare, so a
// green verify run goes red naming it (negative control A). Unknown name is
// Fatal{PipeVerifyBadKnob}.
String PipeVerifyCorrupt;
// MOBILEGL_PIPE_POISON_OMIT: <Verb>:<FieldName>; the filler skips the STAMP (not
// the value) of that field for that verb, an omission indistinguishable from a
// forgotten FillPoints.def row, so that verb's read of it is
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
// Fatal{PipeVerifyBadKnob}.
String PipePoisonOmit;
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
Bool PipeStats = false;
@@ -513,8 +499,7 @@ namespace MobileGL::MG_Config {
// in MG_Backend/Init.cpp compiles away entirely rather than becoming a branch nobody can
// take. A FeaturesTable member is a runtime field in every build, which is the opposite
// of that; it would also resize MG_Config::Features and break G1 (the pull build's
// symbol set must not move) for the same reason the MOBILEGL_PIPE_VERIFY knobs above sit
// behind their own #if.
// symbol set must not move).
//
// ONE CONSEQUENCE, STATED SO IT IS NOT REDISCOVERED: in a build without the option,
// MOBILEGL_TRANSPORT=inproc is ACCEPTED BY THE ENVIRONMENT AND SILENTLY IGNORED - the
@@ -550,10 +535,6 @@ namespace MobileGL::MG_Config {
// by design": no forced sync, so a one-frame respecify-and-draw loop grows without bound
// and MagmaWireReclaimScenario's watermark cases must go red.
Uint32 WireDeferredMb = 64;
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB: block granularity of the persistent-map push.
// 0 IS A NEGATIVE CONTROL, NOT "unlimited": it disables the push, and
// PersistentCoherentMapScenario must go RED under it (exit gate E3(a)).
Uint32 PersistentBlockKb = 64;
// MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS: 1 = the persistent-map push ships only
// blocks whose xxHash64 changed since the last push, instead of the whole mapped
// range every verb. 0 restores the whole-range push (A/B control).
@@ -696,12 +677,6 @@ namespace MobileGL::MG_Config {
// platform and the backend's POST self-test allow it; 0 = never grant it (the session
// runs T2 and says so). Read by the server role only; kept through ServerSpawn.
Uint32 AllowAdoptT0 = 1;
// MOBILEGL_IPC_VERB_BARRIER: 1 = the client blocks at every verb boundary until
// appliedSeq reaches its emitSeq (R-1). 0 is the negative control: it is EXPECTED to
// be red, because 31 of the 63 PipeInputs fields are still pulled from a live
// GLContext by the client's residual fill and a free-running queue lets the server
// read a FUTURE value of them.
Uint32 VerbBarrier = 1;
// MOBILEGL_IPC_RUN_AHEAD (P5e, docs/archive/contracts/CONTRACT-P5E.md §1): 1 = after publishing an
// UNBARRIERED record the client returns immediately instead of waiting for its apply.
// It is one half of a conjunction and never a switch on its own - the client arms
@@ -712,9 +687,7 @@ namespace MobileGL::MG_Config {
// 0 IS THE A/B CONTROL AND NOT A NEGATIVE ONE: the server code, the fill decision and
// every record are identical on both arms, and the only difference is whether the
// client waits. That is what makes "is the picture the same" a question about the wait
// rule alone. MOBILEGL_IPC_VERB_BARRIER=0 keeps its own meaning and stays the
// lockstep arm's negative control; under run-ahead it is the one that must go red, at
// the first barriered row's stale pull.
// rule alone.
//
// Forced to 0 by MOBILEGL_PIPE_VERIFY, beside BatchWaits: the comparator needs a
// client-filled gPipeInputs block for every verb, and run-ahead is precisely the
+16 -26
View File
@@ -137,8 +137,9 @@ namespace MobileGL::MG_ConfigLoader {
}
// Everything the system files say, the backend file ahead of client.conf (first emplace
// wins). Re-read on every call: callers are few (Init, and a handful of lookups before it),
// and a file that changed between two processes' starts must not be served stale.
// wins). Re-read on every call: callers are few (Init, a handful of lookups before it, and a
// verify build's comparator when it arms), and a file that changed between two processes'
// starts must not be served stale.
void ReadSystemConfig(UnorderedMap<String, String>& out, Bool log) {
if (IsServerProcess()) return;
ReadBackendFile(FilePathSetting("MOBILEGL_BACKEND_FILE", kDefaultBackendFile), out);
@@ -402,7 +403,6 @@ namespace MobileGL::MG_ConfigLoader {
features.EsprytInvalidImagePlaceholder = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_INVALID_IMAGE_PLACEHOLDER");
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
@@ -452,8 +452,6 @@ namespace MobileGL::MG_ConfigLoader {
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
features.PipeVerifyFatal =
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096);
features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000);
@@ -601,22 +599,25 @@ namespace MobileGL::MG_ConfigLoader {
// The verify harness compares the pushed block against the applier per verb; a
// batched queue lets the comparer read a supplied field mid-apply, which is a
// torn read rather than a divergence. The batch is therefore off whenever the
// shadow comparer is armed.
// shadow comparer is armed, which only a verify build compiles.
#if MOBILEGL_PIPE_VERIFY
if (MG_Config::Features.PipeVerify) ipc.BatchWaits = 0;
#endif
// UNSET is T0 with quiet fallbacks (the default since 2026-09-29); 0 asks for T0 by name, 1 is
// refused by name, 2 is T2 - settled at the handshake from the data plane (P11 A1/B2,
// MG_Remote/Transport/AdoptTier.h). An unparseable value is warned about and reads as unset.
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", MG_Config::kAdoptTierUnset, 0, 2);
// P11 B2: the server's own T0 allow switch (Config.h).
ipc.AllowAdoptT0 = QueryEnvUint32("MOBILEGL_IPC_ALLOW_ADOPT_T0", 1, 0, 1);
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
// P5e (docs/archive/contracts/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every
// other IPC knob and armed only where the server publishes kCapRunAheadApply.
ipc.RunAhead = QueryEnvUint32("MOBILEGL_IPC_RUN_AHEAD", 1, 0, 1);
// ... and forced OFF by the verify harness for BatchWaits' reason, one step further:
// run-ahead's whole point is that the client stops filling gPipeInputs for an
// unbarriered record, and the comparator has nothing left to compare when it does.
#if MOBILEGL_PIPE_VERIFY
if (MG_Config::Features.PipeVerify) ipc.RunAhead = 0;
#endif
// The present credit (ruling 4). 1 is one frame of overlap; 8 is the ceiling because a
// deeper queue buys nothing on a CPU-bound client and pays for it in latency. 0 is NOT
// admitted: a credit of zero would mean "publish no present at all".
@@ -646,12 +647,14 @@ namespace MobileGL::MG_ConfigLoader {
// own and needs no applier quiesce. 0 still turns it off (the negative control does).
ipc.RoleSplitState =
QueryEnvQuirkOverride("MOBILEGL_IPC_ROLE_SPLIT_STATE") != MG_Config::QuirkOverride::ForceOff;
#if MOBILEGL_PIPE_VERIFY
if (MG_Config::Features.PipeVerify && ipc.RoleSplitState) {
MGLOG_W("Config: MOBILEGL_IPC_ROLE_SPLIT_STATE=1 is incompatible with "
"MOBILEGL_PIPE_VERIFY (the comparator owns the single fill block); "
"the dual-block rehearsal is OFF for this run");
ipc.RoleSplitState = false;
}
#endif
// P6 `dl` (CONTRACT-P6 5.3): MOBILEGL_IPC_RESPAWN IS A NAMED REFUSAL, NOT A NO-OP.
//
@@ -715,24 +718,18 @@ namespace MobileGL::MG_ConfigLoader {
// changes the wall clock by seconds (the P12 sweep: 28.3 s at 1 down to 10.8 s at 6) a
// log that does not name it makes every run ambiguous about its own configuration.
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus event-wait=%ums "
"persistent-block=%uKiB create-window=%u pbo-readback-sync=%u poll-escalate=%u "
"adopt-tier=%s allow-t0=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
"create-window=%u pbo-readback-sync=%u poll-escalate=%u "
"adopt-tier=%s allow-t0=%u run-ahead=%u present-credit=%u control-timeout=%ums "
"cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s' surface=%s",
ipc.RingMb, ipc.StageMb, MG_Config::RecordArm.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
MG_Config::RecordArm.PersistentBlockKb, ipc.CreateWindow, ipc.PboReadbackSync, ipc.PollEscalate,
ipc.CreateWindow, ipc.PboReadbackSync, ipc.PollEscalate,
ipc.AdoptTier == 0 ? "0"
: ipc.AdoptTier == 1 ? "1"
: ipc.AdoptTier == 2 ? "2"
: "unset(T0)",
ipc.AllowAdoptT0, ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs,
ipc.ColdStartMs, static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str(),
MG_Config::ServerOwnedWindowSurfaces() ? "server" : "offscreen");
if (ipc.VerbBarrier == 0) {
MGLOG_W("Config: MOBILEGL_IPC_VERB_BARRIER=0 is the R-1 NEGATIVE CONTROL and is "
"expected to fail: the client still pulls 31 of 63 PipeInputs fields from a "
"live GLContext, so an unbarriered queue lets the server read future values");
}
ipc.AllowAdoptT0, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs,
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit), static_cast<int>(ipc.RoleSplitState),
ipc.ServerAffinity.c_str(), MG_Config::ServerOwnedWindowSurfaces() ? "server" : "offscreen");
}
#endif
@@ -742,9 +739,6 @@ namespace MobileGL::MG_ConfigLoader {
// P7 wave 4 M2 (Config.h has the semantics). 0 is admitted ON PURPOSE as the negative
// control: no forced sync, and MagmaWireReclaimScenario's watermark case must go red.
ipc.WireDeferredMb = QueryEnvUint32("MOBILEGL_IPC_WIRE_DEFERRED_MB", 64, 0, 65536);
// 0 is admitted ON PURPOSE and is the negative control of exit gate E3(a): it turns
// the persistent-map push OFF, and PersistentCoherentMapScenario must go red.
ipc.PersistentBlockKb = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_BLOCK_KB", 64, 0, 65536);
// Whole-range push is the 0 arm; with it on (default) only blocks whose
// xxHash64 changed since the last push are shipped.
ipc.PersistentHashSuppress = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS", 1, 0, 1);
@@ -758,10 +752,6 @@ namespace MobileGL::MG_ConfigLoader {
"server never forces a sync for orphaned wire buffer stores, so a long frame "
"that respecifies and draws holds every one of them until the frame ends");
}
if (ipc.PersistentBlockKb == 0) {
MGLOG_W("Config: MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 is the E3(a) NEGATIVE CONTROL: "
"the persistent-map push is OFF and a coherent-map scenario must go red");
}
}
void Init() {
+2 -1
View File
@@ -28,7 +28,8 @@ namespace MobileGL {
void Init();
// One setting resolved the way Init() resolves it - the environment, then the system
// configuration files (ConfigLoader.cpp) - for the few readers that must decide something
// before MobileGL initializes. False when neither names it.
// before MobileGL initializes, and for a verify build's comparator, which reads
// MOBILEGL_PIPE_VERIFY_CORRUPT when it arms. False when neither names it.
Bool LookupSetting(const char* key, String& outValue);
} // namespace MG_ConfigLoader
@@ -27,19 +27,6 @@
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
// P7 wave 2 package B3, INVESTIGATION PROBE (split-only, read once, not in
// ConfigLoader's accepted-env table - package B's MGITEST_MAGMA_FORCE_SHADER_MIPMAP
// shape). MOBILEGL_MAGMA_WIREBUF_PROBE=1 prints one line per streamed-buffer event so
// the `glBufferSubData -> draw` ordering of a trace can be read off a log instead of
// inferred. Costs a branch on a cached bool when unset.
Bool WireBufProbeEnabled() {
static const Bool enabled = [] {
const char* value = std::getenv("MOBILEGL_MAGMA_WIREBUF_PROBE");
return value && value[0] == '1';
}();
return enabled;
}
// P7 wave 2 package B3, FORCING KNOB (split-only, read once, not in ConfigLoader's
// accepted-env table - package B's MGITEST_MAGMA_FORCE_SHADER_MIPMAP shape).
//
@@ -424,8 +411,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->contentSerial = ++m_wireContentSerial;
if (!resource->shadow.empty()) Memcpy(resource->shadow.data() + offset, bytes, static_cast<SizeT>(size));
Bool uploaded = false;
const Uint64 probeLastUse = resource->lastUseSerial;
const Uint64 probeCompleted = GetCompletedSerial();
// ---- P7 wave 2 package B3: WHAT EACH TERM OF THIS PREDICATE IS FOR -----------------
//
@@ -451,10 +436,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// submission - BindProgramUniformBuffers -> SyncWireTextureShape's preserve path ->
// FlushWirePendingCommandsForTextureUpdate submits the stamped index without the draw -
// so the stamp can name S1 while the draw rides S2. What it does do: with the floor
// sound it should never be the term that says "busy" (the WBUF probe reports bySubmit
// separately, so a device that shows one has found a floor hole), and it is the only
// thing the MGITEST_MAGMA_FORCE_STALE_BUFFER_SERIAL knob leaves standing, which is what
// the StaleSerial. entries pin.
// sound it should never be the term that says "busy" (a write it alone orders points to a
// floor hole), and it is the only thing the MGITEST_MAGMA_FORCE_STALE_BUFFER_SERIAL knob
// leaves standing, which is what the StaleSerial. entries pin.
Bool busyBySerial = resource->lastUseSerial > GetCompletedSerial();
if (busyBySerial && ForceStaleSerialSelector() != 0) {
const Uint64 index = ++g_orderedWireWriteCounter;
@@ -494,16 +478,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-upload\"}");
std::abort();
}
if (WireBufProbeEnabled()) {
MGLOG_I("WBUF write slot=%u gen=%u off=%llu size=%llu branch=%s bySerial=%d bySubmit=%d "
"lastUse=%llu completed=%llu frameSerial=%llu floor=%llu lastUseSubmit=%llu",
res.Slot, res.Gen, static_cast<unsigned long long>(offset), static_cast<unsigned long long>(size),
(busyBySerial || busyBySubmission) ? "STAGED-or-WAIT" : "HOST-IMMEDIATE", busyBySerial ? 1 : 0,
busyBySubmission ? 1 : 0, static_cast<unsigned long long>(probeLastUse),
static_cast<unsigned long long>(probeCompleted), static_cast<unsigned long long>(m_frameSerial),
static_cast<unsigned long long>(m_completedSerialFloor),
static_cast<unsigned long long>(resource->lastUseSubmitIndex));
}
MG_Record::StagedShadowStore::CoverageAdd(resource->stagedCoverage, static_cast<SizeT>(offset),
static_cast<SizeT>(offset + size));
}
@@ -624,10 +598,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->lastUseSubmitIndex = pVulkanRenderer->GetWireNextSubmitIndex();
outSlice = resource->buffer.GetSlice(0, resource->size);
outSlice.mapped = nullptr;
if (WireBufProbeEnabled()) {
MGLOG_I("WBUF bind slot=%u gen=%u frameSerial=%llu", res.Slot, res.Gen,
static_cast<unsigned long long>(m_frameSerial));
}
return true;
}
@@ -1422,11 +1392,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"VkBufferManager::BeginFrame frame index out of range");
m_currentFrameIndex = frameIndex;
++m_frameSerial;
if (WireBufProbeEnabled()) {
MGLOG_I("WBUF BEGINFRAME idx=%u newSerial=%llu floor=%llu (arena slot rewound)", frameIndex,
static_cast<unsigned long long>(m_frameSerial),
static_cast<unsigned long long>(m_completedSerialFloor));
}
CollectDeferredReleases(frameIndex);
m_transientUploadArena.BeginFrame(frameIndex);
}
@@ -242,10 +242,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (const auto& remaining : m_inFlightSubmits) {
if (remaining.frameSerial <= advanceTo) {
WireDeclineTally::CountUnsoundSerialComplete();
// Logged per event, not once: this is the red-once's reading and the log is
// the only place it can be read from: the renderer this trace builds is
// never Shutdown() (the process exits), so a teardown dump reports the
// start-up context's zero instead of the replay's count.
// Logged per event, not once: this line is the red-once's reading and the only
// place the count is printed. A line at teardown would not do: the renderer
// this trace builds is never Shutdown() (the process exits).
MGLOG_W("MGWIRE-FLOOR unsound-serial-complete #%llu: advancing the completed "
"frame-serial floor to %llu, which submission %llu (serial %llu) still carries",
static_cast<unsigned long long>(WireDeclineTally::UnsoundSerialCompleteEvents()),
@@ -255,23 +254,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
break;
}
}
{
static const Bool probe = [] {
const char* value = std::getenv("MOBILEGL_MAGMA_WIREBUF_PROBE");
return value && value[0] == '1';
}();
if (probe) {
Uint64 minRemaining = ~Uint64{0};
for (const auto& r : m_inFlightSubmits)
minRemaining = std::min(minRemaining, r.frameSerial);
MGLOG_I("FLOOR retire upTo=%llu advanceTo=%llu remaining=%zu minRemainingSerial=%lld "
"frameSerial=%llu",
static_cast<unsigned long long>(submitIndex), static_cast<unsigned long long>(advanceTo),
m_inFlightSubmits.size(),
m_inFlightSubmits.empty() ? -1LL : static_cast<long long>(minRemaining),
static_cast<unsigned long long>(m_bufferManager.GetFrameSerial()));
}
}
m_bufferManager.NotifyFrameSerialComplete(advanceTo);
}
// Mid-frame-flushed command buffers whose submission just completed can
@@ -338,23 +320,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// loops still rewind the arena and age their caches every 8 iterations -
// bounded by 8 iterations' transient usage.
++m_drainsSinceLastPresent;
// B3 probe: a drain that reaches here found the GPU caught up and nothing recording.
// Every 8th one is treated as a FRAME BOUNDARY - mid-frame. This probe is what REFUTED
// that as the OpenRA mechanism (one successful drain, zero boundary works across the
// replay; magma-b3.md §2.4): the cause was the floor in OnSubmitsCompletedUpTo.
{
static const Bool probe = [] {
const char* value = std::getenv("MOBILEGL_MAGMA_WIREBUF_PROBE");
return value && value[0] == '1';
}();
if (probe) {
MGLOG_I("WBUF drain#%llu%s submit=%llu completed=%llu",
static_cast<unsigned long long>(m_drainsSinceLastPresent),
(m_drainsSinceLastPresent % 8) == 0 ? " ***FRAME-BOUNDARY-WORK***" : "",
static_cast<unsigned long long>(m_submitCounter),
static_cast<unsigned long long>(m_completedSubmitCounter));
}
}
if ((m_drainsSinceLastPresent % 8) != 0) {
return true;
}
@@ -19,8 +19,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// P7 wave 2 package B3: the per-site tally behind CONTRACT-P7 §0 rule I.
//
// Shape is PipeApplier.h's ServerVerbSink tallies, and for the same reason (R-16: a probe may
// not arm against a stub). "The retrace passed" and "the retrace silently dropped nine draws"
// Shape is RecordVerbSink.h's per-verb tallies, and for the same reason (R-16: a probe may not
// arm against a stub). "The retrace passed" and "the retrace silently dropped nine draws"
// produce the same exit code and the same log, so the lane has to assert the number that only
// this path can move.
//
@@ -100,10 +100,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// from the forced-condition harness; silent when nothing declined, so a green lane stays
// quiet and a lane that dropped a draw cannot.
static void Dump(const char* tag) {
// The floor counter prints unconditionally: "zero unsound advances" is the reading the
// red-once needs, and an absent line cannot be told from a lane that never ran.
MGLOG_I("MGWIRE-FLOOR[%s] unsoundSerialComplete=%llu", tag,
static_cast<unsigned long long>(UnsoundSerialCompleteEvents()));
if (TotalDeclines() == 0 && PeakPendingUploadBytes() == 0) return;
MGLOG_I("MGWIRE-DECLINES[%s] total=%llu pendingEntries=%llu pendingBytes=%llu peakPendingBytes=%llu", tag,
static_cast<unsigned long long>(TotalDeclines()),
+42 -59
View File
@@ -6,13 +6,14 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The backend-side half of the PipeInputs block: the poison Fatal with its verb name, the
// name lookups the runtime knobs need, and - in a verify build - the per-field equality,
// the entry comparator and the corruption injector. Compiled only under MOBILEGL_PIPE_PUSH
// (CMakeLists.txt appends it to SOURCE_FILES there), so the pull build never sees it. Spells
// no MG_State global: everything that reads the live context lives in MG_Impl/Pipe/PipeFill.cpp.
// The backend-side half of the PipeInputs block: the poison Fatal with its verb name and -
// in a verify build - the per-field equality, the entry comparator, the corruption injector,
// the name lookups its knob and the poison omission need, and the omission's server half.
// Spells no MG_State global: everything that reads the live context lives in
// MG_Impl/Pipe/PipeFill.cpp.
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <atomic>
#include <cstdint>
#include <cstring>
@@ -29,22 +30,6 @@ namespace MobileGL::MG_Pipe {
MGPipeInputPoisonFatal(field, MGPipeVerbName(verb));
}
Optional<MGPipeInputField> MGPipeFindInputField(const char* name) {
if (name == nullptr) return std::nullopt;
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
if (std::strcmp(kMGPipeInputFieldNames[i], name) == 0) return static_cast<MGPipeInputField>(i);
}
return std::nullopt;
}
Optional<MGPipeVerb> MGPipeFindVerb(const char* name) {
if (name == nullptr) return std::nullopt;
for (SizeT i = 0; i < kMGPipeVerbCount; ++i) {
if (std::strcmp(kMGPipeVerbNames[i], name) == 0) return static_cast<MGPipeVerb>(i);
}
return std::nullopt;
}
// The stamp is the poison's (m_filled, the generations): a build without MOBILEGL_PIPE_POISON
// compiles no read check that could ask any of this. Every transport build has the poison.
#if MOBILEGL_PIPE_POISON
@@ -55,46 +40,24 @@ namespace MobileGL::MG_Pipe {
Uint64 g_residualPulls = 0;
#if MOBILEGL_PIPE_VERIFY
// ---- MOBILEGL_PIPE_POISON_OMIT, server half ----
// ---- negative control B, server half (MGPipeServerSetPoisonOmission) ----
//
// The knob withholds the stamp of one (verb, field) pair, a negative control for the
// The omission withholds the stamp of one (verb, field) pair, a negative control for the
// poison check. Under a transport the backend reads the server's stamp, which re-stamps
// every field the verb's class can answer, so the server must withhold the pair too or
// the control cannot go red. Parsed here because this file is in the server library and
// MG_Impl is not; a malformed value is the client's Fatal{PipeVerifyBadKnob}, and this
// side simply does not arm. Verify builds only, so the shipped stamp stays branch-free.
struct ServerPoisonOmission {
Bool Parsed = false;
String Value;
Bool Armed = false;
MGPipeVerb Verb = MGPipeVerb::kVerbCount;
MGPipeInputField Field = MGPipeInputField::kFieldCount;
};
ServerPoisonOmission g_serverOmission;
// the control cannot go red. The harness arms it while the apply thread is already
// running, so the pair is ONE atomic word: the verb in the high half, the field in the
// low one, and no verb at all (0xffff) until something is armed. Verify builds only, so
// the shipped stamp stays branch-free.
constexpr Uint32 kNoServerOmission = ~Uint32{0};
std::atomic<Uint32> g_serverOmission{kNoServerOmission};
const ServerPoisonOmission& ServerOmission() {
const String& knob = MG_Config::Features.PipePoisonOmit;
if (g_serverOmission.Parsed && knob.size() == g_serverOmission.Value.size() &&
(knob.empty() || knob == g_serverOmission.Value)) {
return g_serverOmission;
}
g_serverOmission = ServerPoisonOmission{};
g_serverOmission.Parsed = true;
g_serverOmission.Value = knob;
const auto colon = knob.find(':');
if (colon == String::npos || colon == 0 || colon + 1 >= knob.size()) return g_serverOmission;
const auto verb = MGPipeFindVerb(knob.substr(0, colon).c_str());
const auto field = MGPipeFindInputField(knob.substr(colon + 1).c_str());
if (!verb || !field) return g_serverOmission;
g_serverOmission.Armed = true;
g_serverOmission.Verb = *verb;
g_serverOmission.Field = *field;
return g_serverOmission;
constexpr Uint32 PackServerOmission(MGPipeVerb verb, MGPipeInputField field) {
return (static_cast<Uint32>(verb) << 16) | static_cast<Uint32>(field);
}
Bool ServerOmitsStamp(MGPipeVerb verb, MGPipeInputField field) {
const ServerPoisonOmission& omission = ServerOmission();
return omission.Armed && omission.Verb == verb && omission.Field == field;
return g_serverOmission.load(std::memory_order_acquire) == PackServerOmission(verb, field);
}
#endif // MOBILEGL_PIPE_VERIFY
@@ -453,12 +416,10 @@ namespace MobileGL::MG_Pipe {
filled.FilledGen[i] = serial & (Uint64{0} - bit);
}
#if MOBILEGL_PIPE_VERIFY
// Negative control B's server half (ServerPoisonOmission above): the one withheld pair.
// Negative control B's server half (g_serverOmission above): the one withheld pair.
{
const ServerPoisonOmission& omission = ServerOmission();
if (omission.Armed && omission.Verb == verb) {
filled.FilledGen[static_cast<SizeT>(omission.Field)] = 0;
}
const Uint32 omission = g_serverOmission.load(std::memory_order_acquire);
if ((omission >> 16) == static_cast<Uint32>(verb)) filled.FilledGen[omission & 0xffffu] = 0;
}
#endif
MGPipeStampAccess::SetServerStamped(inputs, true);
@@ -475,6 +436,12 @@ namespace MobileGL::MG_Pipe {
MGPipeStampAccess::SetServerStamped(gPipeInputs, false);
}
#if MOBILEGL_PIPE_VERIFY
void MGPipeServerSetPoisonOmission(MGPipeVerb verb, MGPipeInputField field) {
g_serverOmission.store(PackServerOmission(verb, field), std::memory_order_release);
}
#endif
Uint64 MGPipeResidualPullCount() {
return g_residualPulls;
}
@@ -691,5 +658,21 @@ namespace MobileGL::MG_Pipe {
return true;
});
}
Optional<MGPipeInputField> MGPipeFindInputField(const char* name) {
if (name == nullptr) return std::nullopt;
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
if (std::strcmp(kMGPipeInputFieldNames[i], name) == 0) return static_cast<MGPipeInputField>(i);
}
return std::nullopt;
}
Optional<MGPipeVerb> MGPipeFindVerb(const char* name) {
if (name == nullptr) return std::nullopt;
for (SizeT i = 0; i < kMGPipeVerbCount; ++i) {
if (std::strcmp(kMGPipeVerbNames[i], name) == 0) return static_cast<MGPipeVerb>(i);
}
return std::nullopt;
}
#endif // MOBILEGL_PIPE_VERIFY
} // namespace MobileGL::MG_Pipe
+8 -7
View File
@@ -46,10 +46,6 @@ namespace MobileGL::MG_Pipe {
[[noreturn]] void MGPipeInputPoisonFatalForVerb(MGPipeInputField field, MGPipeVerb verb);
// kMGPipeVerbNames[verb], or "<none>" for kVerbCount (no verb has been filled yet).
const char* MGPipeVerbName(MGPipeVerb verb);
// Name lookups for the runtime knobs (MOBILEGL_PIPE_VERIFY_CORRUPT names a field,
// MOBILEGL_PIPE_POISON_OMIT a Verb:Field pair). Empty on an unknown name.
Optional<MGPipeInputField> MGPipeFindInputField(const char* name);
Optional<MGPipeVerb> MGPipeFindVerb(const char* name);
// The server-side read check (ARCHITECTURE.md §9.2.1). Inside a server-stamped verb
// (PipeInputs::ServerStampedVerb) a stale read is judged by the field's ownership class:
@@ -965,9 +961,7 @@ namespace MobileGL::MG_Pipe {
// PLAIN, NOT ATOMIC, AND THAT IS THE SAME RULING TABLE 3 MAKES FOR gPipeInputS ITSELF
// (CONTRACT-P5.md section 4): the verb barrier leaves at most one of {GL thread, apply
// thread} runnable, so there is one writer at any instant. This counter, m_serverStampedVerb
// and FilledGen[] all rest on that and on nothing else - so MOBILEGL_IPC_VERB_BARRIER=0,
// R-1's negative control, is a data race on all three as well as the correctness failure it
// is there to show. It is expected to be red; it is not expected to be meaningful.
// and FilledGen[] all rest on that and on nothing else.
Uint64 MGPipeResidualPullCount();
void MGPipeResetResidualPullCountForTesting();
@@ -998,6 +992,13 @@ namespace MobileGL::MG_Pipe {
// dereferenced, the snapshot is only ever compared). Returns false for a forwarded field,
// which has nothing to corrupt.
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field);
// PipeInputs.cpp. Name lookups for MOBILEGL_PIPE_VERIFY_CORRUPT (a field) and
// MGPipeSetPoisonOmission (a verb and a field). Empty on an unknown name.
Optional<MGPipeInputField> MGPipeFindInputField(const char* name);
Optional<MGPipeVerb> MGPipeFindVerb(const char* name);
// PipeInputs.cpp. Negative control B's server half, armed by MGPipeSetPoisonOmission: the
// server's verb stamp withholds the stamp of `field` at `verb` too.
void MGPipeServerSetPoisonOmission(MGPipeVerb verb, MGPipeInputField field);
// PipeFill.cpp. Writes the MOBILEGL_PIPE_VERIFY_FATAL=0 summary line ("N divergence(s)
// survived") NOW, while the role's log file is still open. MobileGL::Destroy calls it right
// before MG_Util::Debug::Close(): the line used to come from a namespace-scope static's
+3 -1
View File
@@ -400,7 +400,9 @@ namespace MobileGL::MG_Pipe {
void ResetCounters() { m_emissions = m_refusals = 0; }
void ResetForTest() {
// A session that replaced an ended one (MGPipeForgetEndedSession), and a unit fixture:
// the latches and the counters, and the build memo and last states beside them.
void ForgetSession() {
Reset();
ResetCounters();
for (auto& memo : m_buildMemo)
+48 -77
View File
@@ -8,10 +8,9 @@
// The client side of the PipeInputs block (ARCHITECTURE.md 9.2 phase A): the only place in
// the push arm that reads MG_State::pGLContext. Holds the per-verb filler, the F-class
// forwarders, IsLive, the MOBILEGL_PIPE_POISON_OMIT knob and - in a verify build - the
// second arm (SnapshotFromGLContext), the entry compare, the compare-at-read hook and the
// MOBILEGL_PIPE_VERIFY_CORRUPT / _FATAL knobs. Compiled only under MOBILEGL_PIPE_PUSH
// (CMakeLists.txt appends it to SOURCE_FILES there).
// forwarders, IsLive and - in a verify build - the second arm (SnapshotFromGLContext), the
// entry compare, the compare-at-read hook, the MOBILEGL_PIPE_VERIFY_CORRUPT knob, which only
// this build reads, and the poison omission (MGPipeSetPoisonOmission).
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/BufferState/BufferState.h>
// C-1: the vertex-elements CSO's death path raises the backend notice from here, between the
@@ -44,6 +43,8 @@
#if MOBILEGL_PIPE_VERIFY
#include <MG_Impl/Pipe/Verb/VerbPort.h>
#include <MG_Impl/Pipe/Verb/GpuWriteSet.h>
// MG_ConfigLoader::LookupSetting: ArmVerify reads MOBILEGL_PIPE_VERIFY_CORRUPT itself.
#include <Init.h>
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
@@ -456,62 +457,40 @@ namespace MobileGL::MG_Pipe {
return null;
}
#if MOBILEGL_PIPE_VERIFY
[[noreturn]] void BadKnob(const char* knob, const char* value, const char* why) {
MGLOG_F("MGPipe: Fatal{PipeVerifyBadKnob, \"%s=%s\": %s}", knob, value, why);
std::abort();
}
// ---- MOBILEGL_PIPE_POISON_OMIT (negative control B, P1 brief D6) ----
// ---- negative control B (P1 brief D6), armed by MGPipeSetPoisonOmission ----
// The filler skips the STAMP (never the value) of one (verb, field) pair: an omission
// indistinguishable from a forgotten FillPoints.def row, so that verb's read of the
// field is Fatal{UnmigratedPipeInput, "Field@Verb"} and no other verb is affected.
// field is Fatal{UnmigratedPipeInput, "Field@Verb"} and no other verb is affected. The
// GL thread arms it and the GL thread's fill reads it; the server's stamp has its own.
struct PoisonOmission {
Bool Armed = false;
MGPipeVerb Verb = MGPipeVerb::kVerbCount;
MGPipeInputField Field = MGPipeInputField::kFieldCount;
};
PoisonOmission g_omission;
Bool g_omissionKnobParsed = false;
String g_omissionKnobValue; // the value the last parse saw
// Parsed on the first fill and again only when the value changes. A lane loads
// Features once, before any fill, so that is one parse per process there; a forked
// test child that sets Features after its parent already filled gets its own parse,
// which is what puts the parser and its Fatal{PipeVerifyBadKnob} under a unit test.
// An empty value never clears an omission a test armed through MGPipeSetPoisonOmission.
void ParsePoisonOmissionKnob() {
const String& knob = MG_Config::Features.PipePoisonOmit;
// THE UNSET KNOB IS TWO LENGTH LOADS AND A BRANCH, inline, and that is the whole
// change (P5d r3, package C): this runs at every verb - 852 draws a frame on the
// profiled workload - and `knob == g_omissionKnobValue` is an out-of-line String
// compare even when both sides are empty, which is every shipping configuration.
// Sizes first, contents only when a non-empty value is involved; the re-parse
// condition is unchanged (the value differing from what the last parse latched).
if (g_omissionKnobParsed && knob.size() == g_omissionKnobValue.size() &&
(knob.empty() || knob == g_omissionKnobValue)) {
return;
// The stamp the fill's walk is about to overwrite for the omitted pair, put back after the
// branch-free stamp of the whole walk: the value is copied either way, only the stamp is
// withheld. Index kMGPipeInputFieldCount is "nothing withheld at this verb".
struct WithheldStamp {
SizeT Index = kMGPipeInputFieldCount;
Uint64 Gen = 0;
void PutBack(MGPipeFilledState& filled) const {
if (Index < kMGPipeInputFieldCount) filled.FilledGen[Index] = Gen;
}
g_omissionKnobParsed = true;
g_omissionKnobValue = knob;
if (knob.empty()) return;
const auto colon = knob.find(':');
if (colon == String::npos || colon == 0 || colon + 1 >= knob.size()) {
BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "expected <Verb>:<FieldName>");
}
const String verbName = knob.substr(0, colon);
const String fieldName = knob.substr(colon + 1);
const auto verb = MGPipeFindVerb(verbName.c_str());
if (!verb) BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "no such verb in kMGPipeVerbNames");
const auto field = MGPipeFindInputField(fieldName.c_str());
if (!field) BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "no such field in kMGPipeInputFieldNames");
MGPipeSetPoisonOmission(verbName.c_str(), fieldName.c_str());
};
WithheldStamp StampToWithhold(const MGPipeFilledState& filled, MGPipeVerb verb) {
if (!g_omission.Armed || g_omission.Verb != verb) return {};
const auto index = static_cast<SizeT>(g_omission.Field);
return {index, filled.FilledGen[index]};
}
[[maybe_unused]] Bool IsOmitted(MGPipeVerb verb, MGPipeInputField field) {
return g_omission.Armed && g_omission.Verb == verb && g_omission.Field == field;
}
#if MOBILEGL_PIPE_VERIFY
// ---- the MOBILEGL_PIPE_VERIFY comparator (P1 brief D8) ----
// Two mechanisms, both active only when Features.PipeVerify is set: the ENTRY compare
// once per verb (the pushed block against a second snapshot of the live context,
@@ -543,7 +522,6 @@ namespace MobileGL::MG_Pipe {
Bool Fatal = true;
Bool InHook = false; // a re-read that re-enters an accessor is not re-verified
Optional<MGPipeInputField> Corrupt;
String CorruptKnob; // the MOBILEGL_PIPE_VERIFY_CORRUPT value the last arm saw
std::atomic<Uint64> Divergences{0};
Uint64 Summarised = 0; // how many of them the summary line has reported so far
// THE SUMMARY IS WRITTEN FROM MobileGL::Destroy, NOT FROM HERE (V1 fix round 2). This
@@ -568,24 +546,25 @@ namespace MobileGL::MG_Pipe {
};
VerifyState g_verify;
// Armed on the first fill and re-armed when any of the three verify knobs'
// Features value (PipeVerify, PipeVerifyFatal, PipeVerifyCorrupt) differs from what
// the last arm latched (the same reason as ParsePoisonOmissionKnob: one arm per lane
// process, a fresh arm for a forked test child that turns a knob after its parent
// filled). Cost: two Bool compares and one String compare per fill, verify builds only.
// Armed on the first fill and re-armed when PipeVerify or PipeVerifyFatal differs from
// what the last arm latched: one arm per lane process, a fresh arm for a forked test
// child that turns a knob after its parent filled. Each arm that enables the comparator
// reads MOBILEGL_PIPE_VERIFY_CORRUPT the way the loader resolves every knob (the
// environment, then the system configuration), so the knob exists in this build only.
// Cost: two Bool compares per fill, verify builds only.
void ArmVerify() {
const auto& features = MG_Config::Features;
if (g_verify.Parsed && g_verify.Enabled == features.PipeVerify &&
g_verify.Fatal == features.PipeVerifyFatal && g_verify.CorruptKnob == features.PipeVerifyCorrupt) {
g_verify.Fatal == features.PipeVerifyFatal) {
return;
}
g_verify.Parsed = true;
g_verify.Enabled = features.PipeVerify;
g_verify.Fatal = features.PipeVerifyFatal;
g_verify.CorruptKnob = features.PipeVerifyCorrupt;
g_verify.Corrupt = Optional<MGPipeInputField>{};
if (!g_verify.Enabled) return;
const String& corrupt = g_verify.CorruptKnob;
String corrupt;
(void)MG_ConfigLoader::LookupSetting("MOBILEGL_PIPE_VERIFY_CORRUPT", corrupt);
if (!corrupt.empty()) {
const auto field = MGPipeFindInputField(corrupt.c_str());
if (!field) {
@@ -1905,25 +1884,17 @@ namespace MobileGL::MG_Pipe {
return published;
}
void MGPipeSetPoisonOmission(const char* verb, const char* field) {
if (verb == nullptr || field == nullptr) {
g_omission = PoisonOmission{};
return;
}
#if MOBILEGL_PIPE_VERIFY
Bool MGPipeSetPoisonOmission(const char* verb, const char* field) {
const auto v = MGPipeFindVerb(verb);
const auto f = MGPipeFindInputField(field);
if (!v || !f) BadKnob("MOBILEGL_PIPE_POISON_OMIT", verb, "unknown verb or field");
g_omission.Armed = true;
g_omission.Verb = *v;
g_omission.Field = *f;
#if MOBILEGL_PIPE_POISON
if (!v || !f) return false;
g_omission = PoisonOmission{true, *v, *f};
MGPipeServerSetPoisonOmission(*v, *f);
MGLOG_I("MGPipe: poison omission armed - %s@%s", field, verb);
#else
MGLOG_W_ONCE("MGPipe: poison omission %s@%s requested but the poison is not compiled in "
"(MOBILEGL_PIPE_POISON=0): no stamp exists to omit",
field, verb);
#endif
return true;
}
#endif
// ---- liveness ----
Bool PipeInputs::IsLive() const {
@@ -3038,7 +3009,6 @@ namespace MobileGL::MG_Pipe {
// every verb) and the guard below then fires Fatal{RoleViolation, "gPipeInputs"} on
// the first unbarriered verb, by name.
const Bool runAhead = ClientRunsAhead();
ParsePoisonOmissionKnob();
#if MOBILEGL_PIPE_VERIFY
ArmVerify();
#else
@@ -3389,18 +3359,19 @@ namespace MobileGL::MG_Pipe {
MGPipeFillAccess::CopyField(inputs, *ctx, static_cast<MGPipeInputField>(copies[k]));
}
#if MOBILEGL_PIPE_POISON
// The value is copied either way; only the stamp is withheld for the omitted pair,
// which is put back to what it held after the branch-free stamp of the whole walk.
const Uint8* const walk = g_fillPlan.Walk[planClass].data();
const SizeT walkCount = g_fillPlan.WalkCount[planClass];
const Uint64 serial = filled.CurrentVerbSerial;
const Bool omit = g_omission.Armed && g_omission.Verb == verb;
const SizeT omitted = omit ? static_cast<SizeT>(g_omission.Field) : 0;
const Uint64 omittedGen = omit ? filled.FilledGen[omitted] : 0;
#if MOBILEGL_PIPE_VERIFY
// Negative control B: the omitted pair's stamp goes back after the walk (WithheldStamp).
const WithheldStamp withheld = StampToWithhold(filled, verb);
#endif
for (SizeT k = 0; k < walkCount; ++k) {
filled.FilledGen[walk[k] & ~ResidualFillPlan::kCopy] = serial;
}
if (omit) filled.FilledGen[omitted] = omittedGen;
#if MOBILEGL_PIPE_VERIFY
withheld.PutBack(filled);
#endif
#endif
}
if (payloadBytes != 0 && MG_Util::PipeStats::Enabled()) {
@@ -3444,13 +3415,13 @@ namespace MobileGL::MG_Pipe {
MGPipeTrackerInstance().Reset();
// The texture emitter's records and its owed uploads belong to the ended server; its
// sampler references go back to the cache before the cache is emptied.
MGPipeTextureEmitterInstance().ResetForTest();
MGPipeTextureEmitterInstance().ForgetSession();
MGPipeSamplerEmitterInstance().Reset();
MGPipeSamplerCsoCacheInstance().ResetForTest();
MGPipeSamplerCsoCacheInstance().ForgetSession();
MGPipeCsoCacheInstance().Reset();
MGPipeSetHashSuppressorInstance().InvalidateAll();
MGPipeVertexInputEmitterInstance().Reset();
MGPipeFramebufferEmitterInstance().ResetForTest();
MGPipeFramebufferEmitterInstance().ForgetSession();
MGPipeImageEmitterInstance().Reset();
MGPipeProgramEmitterInstance().Reset();
MGPipeShaderBufferEmitterInstance().Reset();
+7 -8
View File
@@ -81,14 +81,6 @@ namespace MobileGL::MG_Pipe {
// (TrackerWalk.ABaseInstanceSurvivesTheFirstWalkOnAFreshContext).
Uint32 MGPipePendingBaseInstance();
// PipeFill.cpp. Negative control B (P1 brief D6): the filler withholds the STAMP - never
// the value - of `field` at `verb`, so that verb's read of it is
// Fatal{UnmigratedPipeInput, "Field@Verb"} while every other verb is unaffected. The
// MOBILEGL_PIPE_POISON_OMIT knob ("<Verb>:<FieldName>") calls this once, on the first
// fill; tests call it directly. Both null clears the omission. An unknown name is
// Fatal{PipeVerifyBadKnob}.
void MGPipeSetPoisonOmission(const char* verb, const char* field);
// PipeFill.cpp. How many times set_vertex_attrib_defaults' applier failed to reproduce
// the value the call carried, so the client wrote the mirror itself
// (EmitVertexAttribDefaults). It is the ONE observable of that repair: the window it
@@ -119,6 +111,13 @@ namespace MobileGL::MG_Pipe {
// is the branch that survives P13, which is why it is its own function rather than the
// filler's loop.
void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask);
// PipeFill.cpp. Negative control B (P1 brief D6): from now on the filler withholds the STAMP -
// never the value - of `field` at `verb`, and so does the server's verb stamp, so that verb's
// read of it is Fatal{UnmigratedPipeInput, "Field@Verb"} while every other verb is unaffected.
// Names from kMGPipeVerbNames / kMGPipeInputFieldNames; false, arming nothing, for an unknown
// one. Call it on the GL thread. The integration harness arms it (Harness/PoisonOmissionArm.h).
Bool MGPipeSetPoisonOmission(const char* verb, const char* field);
#endif
} // namespace MobileGL::MG_Pipe
#define MGP_FILL(Verb) ::MobileGL::MG_Pipe::MGPipeValidateForVerb(::MobileGL::MG_Pipe::MGPipeVerb::Verb)
+10 -9
View File
@@ -151,7 +151,7 @@ namespace MobileGL::MG_Pipe {
// never shrinks back.
Uint64 OverCapacityMints = 0;
// A RELEASE THIS CACHE COULD NOT ACCOUNT FOR. UnknownReleases: the handle names no
// entry here (a stale handle from before a ResetForTest, a handle of another kind
// entry here (a stale handle from before a ForgetSession, a handle of another kind
// passed by mistake, a handle whose entry was already evicted). UnderflowedReleases:
// the entry was found with a count already 0. Both are contract violations by a
// HOLDER and are counted rather than absorbed, because the pin they steal is
@@ -189,11 +189,12 @@ namespace MobileGL::MG_Pipe {
// named for it proves something else. It hashes nothing and simply uses the hash it is
// given; production code has no reason to call it.
//
// THE THREE MEMBERS A CONSUMER MUST NOT CALL are this one, ResetForTest() (which frees
// every slot the cache holds and drops their publication latches) and ResetCounters().
// Package B's seam is Acquire / Release / RefCountOf / RecordIsPublished / Size /
// GetCounters and nothing else - the class is public because it is header-only, not
// because all nine members are for callers.
// The members a consumer must not call are this one and ResetCounters(); ForgetSession()
// (which frees every slot the cache holds and drops their publication latches) belongs to
// the end of a session (MGPipeForgetEndedSession) and to unit fixtures. Package B's seam
// is Acquire / Release / RefCountOf / RecordIsPublished / Size / GetCounters and nothing
// else - the class is public because it is header-only, not because all nine members are
// for callers.
MGPipeHandle AcquireWithForcedHashForTest(const SamplerParameters& params, Uint64 forcedHash,
Uint64& payloadBytes) {
SamplerParameters canon;
@@ -255,8 +256,8 @@ namespace MobileGL::MG_Pipe {
return false;
}
// A unit test's fixture, and a session that replaced an ended one (MGPipeForgetEndedSession:
// the fresh server holds none of these records). NOT called from the validate point's
// A session that replaced an ended one (MGPipeForgetEndedSession: the fresh server holds
// none of these records), and a unit test's fixture. NOT called from the validate point's
// FreshlyPrimed arm: MGPipeApplierReset is a make-current and does NOT drop object
// records, so a sampler CSO the applier holds outlives a context switch. Dropping the
// cache there would leak the applier's record and re-mint a value it already has.
@@ -265,7 +266,7 @@ namespace MobileGL::MG_Pipe {
// delete_sampler_state behind them, so a latch left standing would make a future death
// helper - on the recycled slot at the same generation - emit a delete for a record
// that never existed, which is the refusal the applier counts.
void ResetForTest() {
void ForgetSession() {
for (const Entry& entry : m_entries) {
MGPipeNoteHandleUnpublished(MGPipeKind::SamplerCso, entry.Cso);
MGPipeSlots().Free(MGPipeKind::SamplerCso, entry.Cso);
+7 -7
View File
@@ -1154,16 +1154,16 @@ namespace MobileGL::MG_Pipe {
m_deadResolves = 0;
}
// A unit fixture's per-case reset, and the reset of a session that replaced an ended one
// (MGPipeForgetEndedSession: the fresh server holds none of these records). See
// MGPipeResourceTracker::ResetForTest for the rule this restates: a texture handle and
// the applier record it names are SHARE-GROUP OBJECT STATE, so nothing here is
// per-context and no re-publication path exists or may exist.
// Moves whenever the handle table below is dropped (ResetForTest), so a memo that skipped
// ForgetSession is the reset of a session that replaced an ended one
// (MGPipeForgetEndedSession: the fresh server holds none of these records), and a unit
// fixture's per-case reset. See MGPipeResourceTracker::ResetForTest for the rule this
// restates: a texture handle and the applier record it names are SHARE-GROUP OBJECT
// STATE, so nothing here is per-context and no re-publication path exists or may exist.
// Moves whenever the handle table below is dropped (ForgetSession), so a memo that skipped
// NoteTextureBoundAs / AcquireTexture for handles it already noted knows to note again.
Uint64 TableEpoch() const { return m_tableEpoch; }
void ResetForTest() {
void ForgetSession() {
++m_tableEpoch;
// EVERY REFERENCE THIS EMITTER OWES IS GIVEN BACK FIRST. A case that dropped the
// table without releasing would pin cache entries for the rest of the process and
+72 -75
View File
@@ -56,6 +56,7 @@ add_executable(MobileGLIntegrationTest
Harness/P4aSeamPeek.cpp
Harness/P4aFinalFixPeek.cpp
Harness/PersistentMapPeek.cpp
Harness/PoisonOmissionArm.cpp
Harness/SplitRuntimePeek.cpp
# ---- P3b/P4b wave 2-D package D2 (own block; D1 edits this file elsewhere) --------------
# The client texture emitter's own counters, for TextureViewAliasScenario's half of the
@@ -794,12 +795,20 @@ set(MGL_ITEST_MONOLITH_MECHANISM_FILTER
# would be a name in a list.
set(MGL_ITEST_GLES_VIEW_FILTER
"TextureViewScenario.*:TextureViewAliasScenario.*:F1WireScenario.ComputeImageStoreThroughViewPreservesOtherRootLayer:F1WireScenario.TextureViewClearTargetsItsRootMipAndLayer:F1WireScenario.GenerateMipmapThroughViewKeepsOwnerLevelsAndLayersOutsideItsWindow:InvalidImageUnitScenario.ALevelPastATextureViewsWindowReadsZero")
# Cases that run only in the split-arm entries Harness/SplitLogPaths.cmake.in declares a lane
# for, and skip by construction anywhere else (no split transport, no declared arm). They are
# left out of the ambient, verify and whole-binary Magma registrations for the reason above:
# there they would green-skip on every run.
set(MGL_ITEST_SPLIT_ARM_ONLY_FILTER
"PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer")
mgl_itest_lane("DirectGLES." "-${MGL_ITEST_MONOLITH_MECHANISM_FILTER}:${MGL_ITEST_GLES_VIEW_FILTER}"
ENV "${MGL_ITEST_GLES_ENVIRONMENT}")
# The two lists are named so that SplitLogPaths.cmake.in can reach the ambient CtWireScenario
# entries, which the integration job's inproc leg runs on the transport it exports.
mgl_itest_lane("DirectGLES." "-${MGL_ITEST_MONOLITH_MECHANISM_FILTER}:${MGL_ITEST_GLES_VIEW_FILTER}:${MGL_ITEST_SPLIT_ARM_ONLY_FILTER}"
TEST_LIST MGL_AMBIENT_TESTS_GLES ENV "${MGL_ITEST_GLES_ENVIRONMENT}")
mgl_itest_lane("DirectVulkan." "-${MGL_ITEST_MONOLITH_MECHANISM_FILTER}"
ENV "${MGL_ITEST_VULKAN_ENVIRONMENT}")
mgl_itest_lane("DirectVulkan." "-${MGL_ITEST_MONOLITH_MECHANISM_FILTER}:${MGL_ITEST_SPLIT_ARM_ONLY_FILTER}"
TEST_LIST MGL_AMBIENT_TESTS_VULKAN ENV "${MGL_ITEST_VULKAN_ENVIRONMENT}")
# ScenarioIsolationScenario's two cases in ONE process: the first leaves a deleted program current
# and an error unread, the second asserts ScenarioTest::SetUp cleared both. The entries above run
@@ -995,9 +1004,7 @@ mgl_itest_lane("DirectVulkan.PointSizeDemotion." "PointSizeDemotionScenario.*"
# --- MOBILEGL_PIPE_VERIFY lanes (ARCHITECTURE.md §13.2), under `if (MOBILEGL_PIPE_VERIFY)` below
# They exist only in a -DMOBILEGL_PIPE_VERIFY=ON build, so `ctest -L integration-verify
# --no-tests=error` fails when the option is lost. The ambient Verify. entries must not set
# MOBILEGL_PIPE_VERIFY_CORRUPT or MOBILEGL_PIPE_POISON_OMIT: CI's negative controls export them
# in the job environment, and a property of the same name would override them.
# --no-tests=error` fails when the option is lost.
# --- handle ABA: one always-on monolith HandleRecycle.Handles lane per backend -------------
# MGITEST_HANDLE_ARM selects the arm (the scenario skips in the ambient entries). The lanes run
# the default subsystem mask, so they measure the {slot, gen} key that ships.
@@ -1255,17 +1262,22 @@ if (MOBILEGL_PIPE_VERIFY)
# boundary and again at every accessor read, which the design budgets at 5-10x.
set(MGL_ITEST_VERIFY_TIMEOUT 900)
mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_ENVIRONMENT
"MOBILEGL_TRANSPORT=monolith"
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-DirectGLES.log"
# What "the monolith verify lane" means, per backend, in one place: the ambient Verify. entries
# and every control entry below are this list plus their own log path and markers. So the
# controls run the configuration the ambient entries run, and an edit that leaves that lane
# unarmed or non-fatal turns PipeVerifyArmingScenario.Armed red, which asserts both.
set(MGL_ITEST_GLES_VERIFY_BASE_ENV
"MOBILEGL_TRANSPORT=monolith" "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1"
${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_ENVIRONMENT
"MOBILEGL_TRANSPORT=monolith"
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-DirectVulkan.log"
set(MGL_ITEST_VULKAN_VERIFY_BASE_ENV
"MOBILEGL_TRANSPORT=monolith" "MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1"
${MGL_ITEST_VULKAN_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_ENVIRONMENT ${MGL_ITEST_GLES_VERIFY_BASE_ENV}
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-DirectGLES.log")
mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_ENVIRONMENT ${MGL_ITEST_VULKAN_VERIFY_BASE_ENV}
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-DirectVulkan.log")
# The arming assertion's own lane, one case per backend, with a log path nothing else writes to.
#
# PipeVerifyArmingScenario.Armed reads the library's log, and the log is a per-LANE resource: it
@@ -1275,65 +1287,52 @@ if (MOBILEGL_PIPE_VERIFY)
# the primgen reroute, the point-size demotion) is registered exactly like this for the same
# reason. MGITEST_PIPE_ARMING_LANE is a harness marker - the library never reads it - and it is
# what makes the case skip in the ambient lane instead of racing there.
mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_ARMING_ENVIRONMENT
"MOBILEGL_TRANSPORT=monolith"
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1" "MGITEST_PIPE_ARMING_LANE=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-arming-DirectGLES.log"
${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_ARMING_ENVIRONMENT
"MOBILEGL_TRANSPORT=monolith"
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1" "MGITEST_PIPE_ARMING_LANE=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-arming-DirectVulkan.log"
${MGL_ITEST_VULKAN_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_ARMING_ENVIRONMENT ${MGL_ITEST_GLES_VERIFY_BASE_ENV}
"MGITEST_PIPE_ARMING_LANE=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-arming-DirectGLES.log")
mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_ARMING_ENVIRONMENT ${MGL_ITEST_VULKAN_VERIFY_BASE_ENV}
"MGITEST_PIPE_ARMING_LANE=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-arming-DirectVulkan.log")
# Negative control A (G4). MOBILEGL_PIPE_VERIFY_FATAL=0 so the process SURVIVES its own
# divergence and the case can read the report back out of the log; the CI step that exports
# the same corruption against the ambient lane, where FATAL keeps its default of 1, asserts
# the other half - that a divergence aborts and reds the entry.
mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_CORRUPT_ENVIRONMENT
"MOBILEGL_TRANSPORT=monolith"
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1"
# divergence and the case can read the report back out of the log; the other half, that with
# FATAL at its default of 1 a divergence aborts, is PipeInputsTest's
# VerifyCorruptKnobNamesTheFieldAtEntry.
mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_CORRUPT_ENVIRONMENT ${MGL_ITEST_GLES_VERIFY_BASE_ENV}
"MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters" "MOBILEGL_PIPE_VERIFY_FATAL=0"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-corrupt-DirectGLES.log"
${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_CORRUPT_ENVIRONMENT
"MOBILEGL_TRANSPORT=monolith"
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-corrupt-DirectGLES.log")
mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_CORRUPT_ENVIRONMENT ${MGL_ITEST_VULKAN_VERIFY_BASE_ENV}
"MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters" "MOBILEGL_PIPE_VERIFY_FATAL=0"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-corrupt-DirectVulkan.log"
${MGL_ITEST_VULKAN_ENV})
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-corrupt-DirectVulkan.log")
# Negative control B (G5). The omission skips the STAMP of one field for one verb while still
# copying its value, which is indistinguishable from a fill row nobody wrote; the scenario
# forks, so the resulting std::abort() is a datum in waitpid() rather than a dead lane.
# forks, so the resulting std::abort() is a datum in waitpid() rather than a dead lane. The
# pair is a harness marker, MGITEST_POISON_OMIT: the library has no knob for it, and the
# re-executed child arms it through the verify build's setter (Harness/PoisonOmissionArm.h).
# P13 W5: the pair is the pack state at ReadPixels, which monolith's record arm still reads from
# the fill (around its neutral-pack readback). GetActiveTextureUnit at GenerateMipmap was the
# frontend arm's read; the record arm resolves the texture from the verb's own handle and never
# reads it, so that pair could no longer go red for its own reason.
mgl_itest_join_environment(MGL_ITEST_GLES_POISON_OMIT_ENVIRONMENT
"MOBILEGL_TRANSPORT=monolith"
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1"
"MOBILEGL_PIPE_POISON_OMIT=ReadPixels:GetPixelStoreParameters"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectGLES.log"
${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_POISON_OMIT_ENVIRONMENT
"MOBILEGL_TRANSPORT=monolith"
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1"
"MOBILEGL_PIPE_POISON_OMIT=ReadPixels:GetPixelStoreParameters"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectVulkan.log"
${MGL_ITEST_VULKAN_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_POISON_OMIT_ENVIRONMENT ${MGL_ITEST_GLES_VERIFY_BASE_ENV}
"MGITEST_POISON_OMIT=ReadPixels:GetPixelStoreParameters"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectGLES.log")
mgl_itest_join_environment(MGL_ITEST_VULKAN_POISON_OMIT_ENVIRONMENT ${MGL_ITEST_VULKAN_VERIFY_BASE_ENV}
"MGITEST_POISON_OMIT=ReadPixels:GetPixelStoreParameters"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectVulkan.log")
# The whole suite again, per backend, with the comparator armed. Same scenarios, same
# assertions, but every backend read of frontend state is now checked against a snapshot taken
# from the live context at the verb boundary - which is what "the 742 integration entries
# prove push equals pull" means. Labelled integration-gpu as well so a verify build's
# `ctest -L integration-gpu` still describes the whole registration set.
mgl_itest_lane("DirectGLES.Verify." "-${MGL_ITEST_GLES_VIEW_FILTER}"
mgl_itest_lane("DirectGLES.Verify." "-${MGL_ITEST_GLES_VIEW_FILTER}:${MGL_ITEST_SPLIT_ARM_ONLY_FILTER}"
LABELS "integration-gpu\;integration-verify\;integration-monolith-control" TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT} ENV "${MGL_ITEST_GLES_VERIFY_ENVIRONMENT}")
mgl_itest_lane("DirectGLES.Verify." "${MGL_ITEST_GLES_VIEW_FILTER}"
LABELS "integration-verify\;integration-monolith-control" TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT} ENV "${MGL_ITEST_GLES_VERIFY_ENVIRONMENT}\;MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1")
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.Verify."
TEST_FILTER "-${MGL_ITEST_SPLIT_ARM_ONLY_FILTER}"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-verify\;integration-monolith-control"
@@ -1342,9 +1341,9 @@ if (MOBILEGL_PIPE_VERIFY)
)
# The arming assertion, one entry per backend. This is the entry that fails a lane whose library
# never armed: it runs the same library and the same MOBILEGL_PIPE_VERIFY=1 as the ambient
# entries above, but unlike them it cannot be green against a library with no comparator
# compiled in. Its log is its own, so `-j 4` cannot make it flake.
# never armed: it runs the same library and the same base list as the ambient entries above,
# but unlike them it cannot be green against a library with no comparator compiled in, or with
# one that only logs a divergence. Its log is its own, so `-j 4` cannot make it flake.
mgl_itest_lane("DirectGLES.VerifyArming." "PipeVerifyArmingScenario.Armed"
LABELS "integration-gpu\;integration-verify\;integration-monolith-control" TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT} ENV "${MGL_ITEST_GLES_VERIFY_ARMING_ENVIRONMENT}")
mgl_itest_lane("DirectVulkan.VerifyArming." "PipeVerifyArmingScenario.Armed"
@@ -2410,7 +2409,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# running it here captured only the calls made ABOVE this line and silently dropped the F1
# and Ct blocks below it. SplitLogPaths.PrivateAndDistinct is what caught that, naming
# F1WireScenario.ClearBufferfiPixels; the generated file now sees every call.
# Python is mandatory for this split integration gate (also used by the CI controls).
# Python is mandatory for this split integration gate.
# Do not silently omit the ownership check when the interpreter is unavailable.
find_package(Python3 REQUIRED COMPONENTS Interpreter)
add_test(NAME SplitLogPaths.PrivateAndDistinct
@@ -3214,10 +3213,10 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
endif()
# P5c ct (docs/archive/contracts/CONTRACT-P5C.md §5): the two control records, split-only source for the
# same reason as f1's - the scenario reads the SERVER sink's tallies, which a monolith build
# has no symbol for. One block per case with a LOG PATH OF ITS OWN, the F1 pattern:
# SplitLogPaths.PrivateAndDistinct fails a lane whose cases share one file, and the death
# case's child truncates whatever path it inherits.
# same reason as f1's - the scenario reads the server's control-record counters (`ar`, `od` on the
# server role's PipeStats line), which only a build with a server role produces. One block per case
# with a LOG PATH OF ITS OWN, the F1 pattern: SplitLogPaths.PrivateAndDistinct fails a lane whose
# cases share one file, and the death case's child truncates whatever path it inherits.
if (MOBILEGL_BUILD_DISAGGREGATED)
target_sources(MobileGLIntegrationTest PRIVATE Scenarios/CtWireScenario.cpp)
# P7 wave 2 package C adds the fifth: the OQ-10 capability read. It belongs in THIS list
@@ -3546,8 +3545,9 @@ endif()
# Whole integration binary on Magma split (informational, no CI gate)
# =====================================================================================
# The set the monolith `DirectVulkan.` arm runs (all but MGL_ITEST_MONOLITH_MECHANISM_FILTER,
# oracles that must not inherit a transport), minus DualBlockScenario (its own lane). No
# MGITEST_SPLIT_LANE: query-only cases emit no record (ARCHITECTURE.md §16.1). Private log paths
# oracles that must not inherit a transport, and MGL_ITEST_SPLIT_ARM_ONLY_FILTER), minus
# DualBlockScenario (its own lane). No MGITEST_SPLIT_LANE: query-only cases emit no record
# (ARCHITECTURE.md §16.1). Private log paths
# come through MGL_MAGMA_FULL_TEST_LISTS, not MGL_SPLIT_ARM_TEST_LISTS, whose entries also get
# lane markers that would arm unrelated oracles here; SplitLogPaths.cmake.in walks both.
if (MOBILEGL_BUILD_DISAGGREGATED)
@@ -3558,7 +3558,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
mgl_itest_join_environment(mglItestMagmaFullEnvironment
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ITEST_REQUIRE_GPU=1"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_VULKAN_ENV} ${mglItestArmTransportEnv})
mgl_itest_lane("DirectVulkan.${mglItestArm}.Full." "-${MGL_ITEST_MONOLITH_MECHANISM_FILTER}:DualBlockScenario.*"
mgl_itest_lane("DirectVulkan.${mglItestArm}.Full." "-${MGL_ITEST_MONOLITH_MECHANISM_FILTER}:DualBlockScenario.*:${MGL_ITEST_SPLIT_ARM_ONLY_FILTER}"
LABELS "integration-magma-full-${mglItestArmLower}" TEST_LIST "MGL_MAGMA_FULL_TESTS_${mglItestArmUpper}" PROPERTIES ${mglItestArmProperties} ENV "${mglItestMagmaFullEnvironment}")
list(APPEND MGL_MAGMA_FULL_TEST_LISTS "MGL_MAGMA_FULL_TESTS_${mglItestArmUpper}")
endforeach()
@@ -3602,11 +3602,11 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND MOBILEGL_PIPE_VERIFY)
# MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 or DirectGLES withholds GL_ARB_texture_view and they
# green-skip, so they are subtracted from the first registration and re-registered with the
# knob. "Every existing verify case on the split arm" is false without the second block.
mgl_itest_lane("DirectGLES.VerifySplit." "-${MGL_ITEST_MONOLITH_MECHANISM_FILTER}:${MGL_ITEST_GLES_VIEW_FILTER}:DualBlockScenario.*:${MGL_ITEST_VERIFY_SPLIT_GLES_MAGMA_DECLINE_FILTER}"
mgl_itest_lane("DirectGLES.VerifySplit." "-${MGL_ITEST_MONOLITH_MECHANISM_FILTER}:${MGL_ITEST_GLES_VIEW_FILTER}:DualBlockScenario.*:${MGL_ITEST_VERIFY_SPLIT_GLES_MAGMA_DECLINE_FILTER}:${MGL_ITEST_SPLIT_ARM_ONLY_FILTER}"
LABELS "integration-verify-split" TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT} TEST_LIST MGL_VERIFY_SPLIT_TESTS_GLES ENV "${MGL_ITEST_GLES_VERIFY_SPLIT_ENVIRONMENT}")
mgl_itest_lane("DirectGLES.VerifySplit." "${MGL_ITEST_GLES_VIEW_FILTER}"
LABELS "integration-verify-split" TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT} TEST_LIST MGL_VERIFY_SPLIT_TESTS_GLES_VIEW ENV "${MGL_ITEST_GLES_VERIFY_SPLIT_ENVIRONMENT}\;MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1")
mgl_itest_lane("DirectVulkan.VerifySplit." "-${MGL_ITEST_MONOLITH_MECHANISM_FILTER}:DualBlockScenario.*"
mgl_itest_lane("DirectVulkan.VerifySplit." "-${MGL_ITEST_MONOLITH_MECHANISM_FILTER}:DualBlockScenario.*:${MGL_ITEST_SPLIT_ARM_ONLY_FILTER}"
LABELS "integration-verify-split" TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT} TEST_LIST MGL_VERIFY_SPLIT_TESTS_VULKAN ENV "${MGL_ITEST_VULKAN_VERIFY_SPLIT_ENVIRONMENT}")
list(APPEND MGL_VERIFY_SPLIT_TEST_LISTS MGL_VERIFY_SPLIT_TESTS_GLES
MGL_VERIFY_SPLIT_TESTS_GLES_VIEW MGL_VERIFY_SPLIT_TESTS_VULKAN)
@@ -3633,8 +3633,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND MOBILEGL_PIPE_VERIFY)
# four entries with the split environment underneath and nothing else changed: each is
# green when its control REPORTS, so each goes red the day the comparator or the poison
# stops reaching the split arm. test.yml's integration-verify-split job requires each of
# them to pass by name, and control A's CI half - the same knob exported against the
# ambient VerifySplit. entries, where ctest has to FAIL - runs there too.
# them to pass by name.
#
# A (G4): the corruption perturbs the snapshot arm at the CLIENT's entry compare, so the
# report lands in the client role's log, which is the half LibraryLog() reads.
@@ -3658,15 +3657,13 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND MOBILEGL_PIPE_VERIFY)
# PoisonOmissionScenario's ReadChildLog reads BOTH role halves of the child's log, because
# under a transport the poisoned read can fire on either side of the seam.
#
# A DIFFERENT PAIR FROM THE MONOLITH ENTRIES', and the monolith pair is not "also fine
# here": GenerateMipmap:GetActiveTextureUnit is inert on this arm - the split backend's
# generate_mipmap resolves its texture from the verb's handle and never reads the active
# unit, so the child exited 0 with that stamp withheld (measured, both backends).
# ReadPixels:GetPixelStoreParameters is read on the server under the server's stamp,
# which is the stamp the knob has to withhold on this arm (PipeInputs.cpp's
# ServerPoisonOmission). PoisonOmissionScenario.cpp's kOmittedPairs carries both.
# THE MONOLITH ENTRIES' PAIR. ReadPixels:GetPixelStoreParameters is read on the server under
# the server's stamp, which is the stamp the omission has to withhold on this arm
# (PipeInputs.cpp's MGPipeServerSetPoisonOmission). GenerateMipmap:GetActiveTextureUnit is
# inert here: generate_mipmap resolves its texture from the verb's handle and never reads
# the active unit, so the child exited 0 with that stamp withheld (measured, both backends).
mgl_itest_lane("${mglItestVerifyBackend}.VerifySplitPoisonOmitted." "PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb"
LABELS "integration-verify-split" TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT} LOCK "pipe-poison-omit-split-${mglItestVerifyBackend}.log" ENV "${mglItestVerifyArmEnv}\;MOBILEGL_PIPE_POISON_OMIT=ReadPixels:GetPixelStoreParameters\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-split-${mglItestVerifyBackend}.log")
LABELS "integration-verify-split" TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT} LOCK "pipe-poison-omit-split-${mglItestVerifyBackend}.log" ENV "${mglItestVerifyArmEnv}\;MGITEST_POISON_OMIT=ReadPixels:GetPixelStoreParameters\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-split-${mglItestVerifyBackend}.log")
# B's AMBIENT HALF. `Verify.PoisonOmissionScenario.WithoutOmissionCompletes` runs the
# scenario with nothing withheld, and the mechanism filter took it out of the ambient set
# above along with the rest of the scenario. Registered back by hand, with no knob, so
@@ -103,4 +103,28 @@ namespace MGITest {
#endif
}
bool ForgetLivePersistentMap(unsigned int bufferName) {
#if defined(MGITEST_PERSISTENT_MAP_TRACKER_LIVE)
MobileGL::MG_State::GLState::BufferObject* buffer = FrontendBuffer(bufferName);
if (buffer == nullptr) return false;
MobileGL::MG_Record::PersistentMapTracker::Instance().Forget(*buffer);
return true;
#else
(void)bufferName;
return false;
#endif
}
bool ReannounceLivePersistentMap(unsigned int bufferName) {
#if defined(MGITEST_PERSISTENT_MAP_TRACKER_LIVE)
MobileGL::MG_State::GLState::BufferObject* buffer = FrontendBuffer(bufferName);
if (buffer == nullptr) return false;
buffer->NotePersistentMapStateChanged();
return true;
#else
(void)bufferName;
return false;
#endif
}
} // namespace MGITest
@@ -58,6 +58,15 @@ namespace MGITest {
// Same contract as above: false means "could not look".
bool PeekBufferIsLivePersistentMap(unsigned int bufferName, bool* outLive);
// The two production calls that take a buffer out of the tracker's set and put it back:
// PersistentMapTracker::Forget, which an unmap or a destroy reaches, and
// BufferObject::NotePersistentMapStateChanged, which every map-state change makes. They let
// a scenario show the push is the only route a coherent write takes to the server.
//
// Same contract: false means "could not reach the tracker", and nothing was changed.
bool ForgetLivePersistentMap(unsigned int bufferName);
bool ReannounceLivePersistentMap(unsigned int bufferName);
// Separate-process clients own these counters; the remote Present window
// belongs to the server and cannot report client map acquisitions or bytes.
bool PeekSeparateClientMapStats(unsigned long long* acquisitions, unsigned long long* pushedBytes);
@@ -0,0 +1,39 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PoisonOmissionArm.cpp
// Copyright (c) 2025-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 "PoisonOmissionArm.h"
#if !defined(__ANDROID__)
#include <MG_Pipe/MGPipe.h>
#include <MG_Impl/Pipe/PipeFill.h>
#if MOBILEGL_PIPE_VERIFY
#define MGITEST_POISON_OMISSION_ARM_LIVE 1
#endif
#endif
namespace MGITest {
#if defined(MGITEST_POISON_OMISSION_ARM_LIVE)
bool PoisonOmissionArmAvailable() {
return true;
}
bool ArmPoisonOmission(const char* verb, const char* field) {
return MobileGL::MG_Pipe::MGPipeSetPoisonOmission(verb, field);
}
#else
bool PoisonOmissionArmAvailable() {
return false;
}
bool ArmPoisonOmission(const char*, const char*) {
return false;
}
#endif
} // namespace MGITest
@@ -0,0 +1,34 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PoisonOmissionArm.h
// Copyright (c) 2025-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
//
// NEGATIVE CONTROL B'S OMISSION, ARMED FROM THE HARNESS. PoisonOmissionScenario's re-executed
// child withholds the stamp of one (verb, field) pair before it runs the sequence, so that verb's
// read of the field must be Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}. The library has no knob
// for it: only a -DMOBILEGL_PIPE_VERIFY=ON build compiles the setter (MG_Impl/Pipe/PipeFill.h,
// MGPipeSetPoisonOmission), and this file is the one place that calls it.
//
// A SEPARATE TRANSLATION UNIT for PipeApplyPeek's reason, verbatim: the scenario sources include
// the GL headers with prototypes and the pipe headers are not meant to meet them in one file.
//
// Both entry points return false, touching nothing, where the setter is out of reach: a build
// without MOBILEGL_PIPE_VERIFY, or Android, where this module links the shipping libMobileGL.so
// built -fvisibility=hidden.
#pragma once
namespace MGITest {
// True when this build can arm the omission at all.
bool PoisonOmissionArmAvailable();
// Withholds the stamp of `field` at `verb` (kMGPipeVerbNames / kMGPipeInputFieldNames spellings)
// for the rest of this process, in the client's fill and in the server's verb stamp. Call it on
// the thread that issues the GL calls. False when the build cannot, or a name is unknown.
bool ArmPoisonOmission(const char* verb, const char* field);
} // namespace MGITest
@@ -2,6 +2,9 @@
# CTest appends ENVIRONMENT here; keep all previously discovered lane settings.
file(MAKE_DIRECTORY "@CMAKE_CURRENT_BINARY_DIR@/split-logs")
file(MAKE_DIRECTORY "@CMAKE_CURRENT_BINARY_DIR@/verify-split-logs")
# CtWireScenario reads the server's control-record counters (`ar`, `od`) off the server role's
# PipeStats line, so its entries turn the counters on with one line per frame.
set(mglCtWireStatsEnvironment "MOBILEGL_PIPE_STATS=1;MOBILEGL_PIPE_STATS_PERIOD=1")
# P5b t2's two lanes ride the same rule: one private log path per entry, or
# SplitLogPaths.PrivateAndDistinct is red for them (ID-53).
# P5e vi's client-array lane joins the same list for the same reason: its two entries are the
@@ -25,41 +28,46 @@ foreach(listName IN ITEMS @MGL_SPLIT_ARM_TEST_LISTS@)
elseif(entry MATCHES "PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares$")
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
"MGITEST_PMAP_LANE=1;MGITEST_PERSISTENT_MAP_ARM=emulated;MOBILEGL_PIPE_STATS=1;MOBILEGL_PIPE_STATS_PERIOD=1")
elseif(entry MATCHES "PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer$")
# The case takes the push away, which only the emulated arm has. Every split arm
# emulates by default except Magma in process, which is granted T0 and adopts, so
# that entry asks for T2 by name, as the Magma buffer lanes do.
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "MGITEST_PERSISTENT_MAP_ARM=emulated")
if(entry MATCHES "^DirectVulkan\\.Split\\.")
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "MOBILEGL_IPC_ADOPT_TIER=2")
endif()
elseif(entry MATCHES "LargeArenaAdoptionScenario.TheArenaLandsInTheTierItsLaneDeclares$")
# P11 A2: every split arm runs T2, so the arena is declared emulated.
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "MGITEST_PERSISTENT_MAP_ARM=emulated")
elseif(entry MATCHES "CtWireScenario\\.")
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "MOBILEGL_TEST_SERVER_COUNTERS=1")
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "${mglCtWireStatsEnvironment}")
endif()
endforeach()
endforeach()
# P7 package L tier 3: the whole-suite Magma census. A LIST OF ITS OWN, and the log path is the
# ONLY property it gets - the loop above also arms the strict-arming, pmap and server-counter
# ONLY property it gets - the loop above also arms the strict-arming, pmap and persistent-map-arm
# lane markers by entry name, and a census is here to be READ, not to assert three unrelated
# oracles it never opted into. The path itself is not optional: these entries are
# DirectVulkan.{Split,Spawn,Tcp}. names, so split_log_paths.py's ownership gate requires exactly
# one absolute private path each, and on the spawn and tcp arms two processes append to it.
#
# ONE EXCEPTION, AND IT IS TELEMETRY RATHER THAN AN ORACLE: CtWireScenario reads the server's
# own control-record tallies, and without MOBILEGL_TEST_SERVER_COUNTERS=1 they are not published
# at all - the case then reds on `server control-record telemetry missing`, which says nothing
# about Magma. Measured: that is exactly what the spawn arm's ApplierReset case did on this
# tier's first run. A switch that makes a counter readable is not the same kind of thing as the
# strict-arming, pmap or persistent-map-arm markers above, which make a case ASSERT something it
# never opted into, and those stay out of the census.
# One exception, telemetry rather than an oracle: CtWireScenario's counters. Without them the case
# reds on `server control-record telemetry missing`, which says nothing about Magma, and a switch
# that makes a counter readable does not make a case assert anything it never opted into, unlike
# the markers above.
foreach(listName IN ITEMS @MGL_MAGMA_FULL_TEST_LISTS@)
foreach(entry IN LISTS ${listName})
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log")
if(entry MATCHES "CtWireScenario\\.")
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "MOBILEGL_TEST_SERVER_COUNTERS=1")
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "${mglCtWireStatsEnvironment}")
endif()
endforeach()
endforeach()
# P7 wave 3 (V1): the verify comparator on the split arm. A LIST OF ITS OWN, like tier 3's and
# for tier 3's reason - the first loop also arms the strict-arming, pmap and server-counter lane
# for tier 3's reason - the first loop also arms the strict-arming, pmap and persistent-map-arm lane
# markers by entry name, and this lane is here to compare fields, not to opt three unrelated
# oracles into a whole-binary set.
# oracles into a whole-binary set. CtWireScenario's counters are the same exception as tier 3's.
#
# THE PATH IS WHAT MAKES THE LANE ARMED RATHER THAN MERELY GREEN. These entries carry no
# MGITEST_SPLIT_LANE marker (MG_IntegrationTest/CMakeLists.txt says why: it reds 13 query-only
@@ -72,8 +80,22 @@ foreach(listName IN ITEMS @MGL_VERIFY_SPLIT_TEST_LISTS@)
foreach(entry IN LISTS ${listName})
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/verify-split-logs/${entry}.log")
if(entry MATCHES "CtWireScenario\\.")
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "${mglCtWireStatsEnvironment}")
endif()
endforeach()
endforeach()
# The ambient CtWireScenario cases that count control records take the counters and a log of their
# own as well: the integration job's inproc leg runs them on the inproc transport it exports (under
# monolith they skip). Only those three: a log would also arm the RoleViolation case's check of the
# guard's line, which the ambient entries never ran. The log is outside split-logs/, which holds
# the split arms' entries.
foreach(entry IN LISTS MGL_AMBIENT_TESTS_GLES MGL_AMBIENT_TESTS_VULKAN)
if(entry MATCHES "CtWireScenario\\.(ApplierReset|TextureDeath|FramebufferDeath)")
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/ct-wire-${entry}.log;${mglCtWireStatsEnvironment}")
endif()
endforeach()
foreach(entry IN LISTS MGL_SPLIT_PA_STRICT_DRAW_TESTS MGL_SPLIT_PA_STRICT_DISPATCH_TESTS)
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
@@ -89,6 +111,8 @@ foreach(scenario @MGL_SPLIT_SMALL_RING_SCENARIOS@)
elseif(entry MATCHES "PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares$")
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
"MGITEST_PMAP_LANE=1;MGITEST_PERSISTENT_MAP_ARM=emulated;MOBILEGL_PIPE_STATS=1;MOBILEGL_PIPE_STATS_PERIOD=1")
elseif(entry MATCHES "PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer$")
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "MGITEST_PERSISTENT_MAP_ARM=emulated")
endif()
endforeach()
endforeach()
@@ -7,8 +7,7 @@
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
"""Validate discovered Split log ownership; read only freshly reset control logs."""
import os
"""Validate discovered Split log ownership and read the entries' private logs."""
import json
from pathlib import Path
import re
@@ -699,66 +698,7 @@ def main():
f"{' '.join(f'{k}={v}' for k, v in expected.items())}: "
f"{['proven', '', '', 'a default differs'][code] if code != 1 else 'FAILED'}")
sys.exit(code)
selected = paths(json.loads(Path(sys.argv[2]).read_text()))
selected = {name: path for name, path in selected.items() if re.search(sys.argv[3], name)}
# The SAME exclusion run_control hands ctest -E. Without it this helper and ctest disagree
# about the subject set, and the helper reports an entry ctest never ran as "did not run"
# (P5e, ID-122). ctest -R is POSIX ERE and cannot express the exclusion inline.
_excl = os.environ.get("SPLIT_LOG_EXCLUDE", "")
if _excl:
selected = {n: q for n, q in selected.items() if not re.search(_excl, n)}
if not selected:
raise ValueError(f"integration-split: empty selection for {sys.argv[3]}")
if mode == "reset":
for path in selected.values():
Path(path).unlink(missing_ok=True)
elif mode == "results":
cases = ET.parse(sys.argv[4]).getroot().findall(".//testcase")
label = sys.argv[5]
by_name = {}
for case in cases:
by_name.setdefault(case.get("name"), []).append(case)
skipped = sum(any(c.find("skipped") is not None for c in by_name.get(n, []))
for n in selected)
# ID-62: pre-flight Fatal is not evidence that a selected entry ran.
if skipped:
raise ValueError(f"{label} control: the knob killed the pre-flight, not the entry - "
f"{skipped} selected entries skipped")
missing = sum(len(by_name.get(n, [])) != 1 or
by_name[n][0].get("status") in ("notrun", "disabled") for n in selected)
if missing:
raise ValueError(f"{label} control: {missing} selected entries did not run")
not_failed = sum(c.find("failure") is None or c.get("status") != "fail"
for n in selected for c in by_name[n])
if not_failed:
raise ValueError(f"{label} control: {not_failed} selected entries did not fail")
elif mode == "assertion":
cases = ET.parse(sys.argv[4]).getroot().findall(".//testcase")
by_name = {case.get("name"): case for case in cases}
for name in selected:
case = by_name.get(name)
output = "" if case is None else " ".join(" ".join(case.itertext()).split())
if not re.search(sys.argv[5], output):
raise ValueError(f"E3(a) FAILED: {name} red lacks its persistent-map push diagnostic")
elif mode == "evidence":
missing = []
label = sys.argv[5] if len(sys.argv) > 5 else ""
for name, path in selected.items():
if any_role_file(path) and re.search(sys.argv[4], read_role_logs(path)):
print(f"private-log evidence: {name}: {path}")
else:
missing.append(f"{name} ({path})")
if missing:
if label:
raise ValueError(f"{label} FAILED: no selected private log carries /{sys.argv[4]}/. "
"The library's own line is the only channel for this: ctest's "
"transcript is a FALSE ZERO for library output, because the console "
"sink is compiled out of the configurations these lanes run. "
+ ", ".join(missing))
raise ValueError("E1 FAILED: selected private logs lack expected Fatal{BarrierViolation, \"<slot>\"} line: "
+ ", ".join(missing))
else:
raise ValueError(f"unknown mode: {mode}")
raise ValueError(f"unknown mode: {mode}")
if __name__ == "__main__":
@@ -10,9 +10,10 @@
//
// applier_reset (§5.1): this process's first validate primes the pipe tracker, and the
// FreshlyPrimed edge is the record's producer (MG_Impl/Pipe/PipeFill.cpp). What the scenario
// asserts is the SERVER sink's own counters - moved by nobody else - plus the pixels, so a
// reset that never crossed is red by the tally and a client that fell through to the driver
// is red by ScenarioFixture's emit-ordinal rule.
// asserts is the server's own count of the control records it applied (`ar` and `od` on the
// server role's PipeStats line, moved by nobody else) plus the pixels, so a reset that never
// crossed is red by the count and a client that fell through to the driver is red by
// ScenarioFixture's emit-ordinal rule.
//
// object_death (§5.2): a texture and a framebuffer die, the deaths cross, and the slots
// recycle. The picture after recycling is the behavioural half: a stale twin answering for
@@ -31,8 +32,6 @@
#include "../Harness/SplitRuntimePeek.h"
#include <MG_Pipe/PipeApply.h>
#include <Config.h> // `t6`: the spawn arm cannot observe the server process's counters
#include <MG_Remote/Server/ServerSession.h>
#if !defined(_WIN32)
#include <csignal>
@@ -52,36 +51,6 @@
namespace MGITest {
namespace {
MobileGL::MG_Remote::Server::ServerVerbSink& ServerVerbs() {
return MobileGL::MG_Remote::Server::ServerSessionInstance().Applier().Verbs();
}
// Inproc reads its local sink. A separate-process session snapshots that
// same sink on LogFlush, after the client fences its complete command prefix.
// TCP forwards the server log; unix+shm reads the server's local role file.
struct CounterSnapshot {
bool valid = false;
MobileGL::Uint64 resets = 0, serial = 0, deaths = 0;
};
CounterSnapshot ServerCounters() {
if (MobileGL::MG_Config::Transport != MobileGL::MG_Config::TransportMode::Spawn) {
return {true, ServerVerbs().ApplierResets(), ServerVerbs().ExpectedApplierResetSerial(),
ServerVerbs().ObjectDeaths()};
}
MGPipeSyncPeerLog();
const std::string log = PipeStatsWindow::ReadWholeFile(PipeStatsWindow::ServerLibraryLogPath());
const auto at = log.rfind("MGPipe server counters:");
if (at == std::string::npos) return {};
const PipeStatsWindow::Window window{true, log.substr(at, log.find('\n', at) - at)};
const auto resets = PipeStatsWindow::CounterOrAbsent(window, "resets");
const auto serial = PipeStatsWindow::CounterOrAbsent(window, "reset-serial");
const auto deaths = PipeStatsWindow::CounterOrAbsent(window, "deaths");
if (resets < 0 || serial < 0 || deaths < 0) return {};
return {true, static_cast<MobileGL::Uint64>(resets), static_cast<MobileGL::Uint64>(serial),
static_cast<MobileGL::Uint64>(deaths)};
}
class CtWireScenario : public ScenarioTest {
protected:
void SetUp() override {
@@ -109,8 +78,8 @@ namespace MGITest {
}
// The texture's level-0 content, read back through a throwaway FBO, as four bytes.
// The FBO is bound AND deleted here, so it never leaks a framebuffer death into the
// tally a case is watching.
// The FBO is bound AND deleted here, so its death lands in whichever window is open
// and a case that counts deaths reads it outside the window it counts.
void ReadTextureLevel(GLuint texture, GLubyte out[4]) {
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
@@ -120,6 +89,47 @@ namespace MGITest {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
}
// The control records the server applied in one window: `ar` (applier_reset records
// accepted) and `od` (object_death records dispatched) on the server role's PipeStats
// line. The entries set MOBILEGL_PIPE_STATS=1 and MOBILEGL_PIPE_STATS_PERIOD=1, so every
// swap ends a window and writes one line: under inproc the apply thread writes it to the
// server role's log, under spawn and tcp the server process writes or forwards it there.
struct ControlRecords {
bool valid = false;
long long applierResets = -1;
long long objectDeaths = -1;
};
// Ends the current window. The records and the present that ends it are kWaitNone, so
// the client moves on and the server counts them when it applies them. A readback is
// kWaitReply on every arm, and its answer is posted only after its record and every
// earlier one were applied, the present and the line it writes included. The harness's
// applied-seq wait is not that boundary off inproc: there the client's progress page
// moves with frames rather than with the peer's apply.
void EndWindow() {
Gl().EndFrame();
GLubyte fence[4]{};
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, fence);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the window's fence readback left a GL error behind";
}
// Ends the setup's window, so that the next one counts only what follows the mark.
PipeStatsWindow::LogMark StartWindow() {
EndWindow();
return PipeStatsWindow::MarkLaneLog();
}
ControlRecords EndWindowAndReadControlRecords(const PipeStatsWindow::LogMark& mark) {
EndWindow();
const PipeStatsWindow::Window window = PipeStatsWindow::LastFromServerLogSince(mark);
if (window.found) RecordProperty("stats_line_server", window.line);
ControlRecords records;
records.applierResets = PipeStatsWindow::CounterOrAbsent(window, "ar");
records.objectDeaths = PipeStatsWindow::CounterOrAbsent(window, "od");
records.valid = records.applierResets >= 0 && records.objectDeaths >= 0;
return records;
}
};
TEST_F(CtWireScenario, ApplierResetCrossesAtThePrimedEdgeInSerialOrder) {
@@ -127,25 +137,25 @@ namespace MGITest {
// The first verb of the process primes the tracker; the edge is the producer. The
// clear also gives the case its pixels, so a lane that emitted nothing is red twice
// (here and in the fixture's emit-ordinal rule).
const PipeStatsWindow::LogMark mark = PipeStatsWindow::MarkLaneLog();
glClearColor(0.2f, 0.4f, 0.6f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
const auto counters = ServerCounters();
ASSERT_TRUE(counters.valid) << "server control-record telemetry missing";
EXPECT_GE(counters.resets, 1u)
<< "the FreshlyPrimed edge emitted no applier_reset; the server's g_applier was "
"never reset for this session";
// The serial sequence is the session's own count (§1: asserted, never dispatched
// on): every record the sink accepted carried the serial it expected, or the counter
// and the accepted tally would disagree.
EXPECT_EQ(counters.serial, counters.resets)
<< "an applier_reset record was dropped or replayed on the wire";
GLubyte pixel[4]{};
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "Ct.ApplierReset.error";
const int expected[4] = {51, 102, 153, 255};
for (int i = 0; i < 4; ++i)
EXPECT_NEAR(pixel[i], expected[i], 1) << "Ct.ApplierReset.pixels";
// No swap precedes this one in the process, so its window holds the edge wherever the
// first validate ran (this clear or the fixture's state reset). The serial needs no
// check of its own: OnApplierReset counts only a record whose serial it accepted, and
// any other latches Fatal{ProtocolCorruption, "ApplierReset.ContextSerial"}.
const ControlRecords counters = EndWindowAndReadControlRecords(mark);
ASSERT_TRUE(counters.valid) << "server control-record telemetry missing";
EXPECT_GE(counters.applierResets, 1)
<< "the FreshlyPrimed edge emitted no applier_reset; the server's g_applier was "
"never reset for this session";
}
TEST_F(CtWireScenario, TextureDeathCrossesAndTheRecycledSlotAnswersTheNewObject) {
@@ -159,7 +169,7 @@ namespace MGITest {
// went backend-agnostic and that the INPROC arm failed here with `deaths` 0 vs 0
// (docs/archive/notes/p7/magma-two-process-first-run.md §6); the table is what closes the inproc
// half, and this case is the gate on it. It is also the RED-ONCE for that table: with
// the install site short-circuited, both cases red on the two EXPECT_GT below.
// the install site short-circuited, both cases red on their `od` assertions below.
const GLubyte red[4] = {255, 0, 0, 255};
const GLubyte green[4] = {0, 255, 0, 255};
@@ -176,45 +186,14 @@ namespace MGITest {
ASSERT_NEAR(before[i], redExpected[i], 1) << "Ct.TextureDeath.before";
// The object dies unbound so the destructor - and the death record - fire at the
// delete, and the tally is read AFTER the EmitAndWait the delete blocked on.
// delete. The counted window holds the delete alone: the FBO ReadTextureLevel deleted
// died in the setup's window, which StartWindow ends.
glBindTexture(GL_TEXTURE_2D, 0);
const auto beforeCounters = ServerCounters();
ASSERT_TRUE(beforeCounters.valid) << "server object-death telemetry missing";
const MobileGL::Uint64 deathsBefore = beforeCounters.deaths;
const PipeStatsWindow::LogMark mark = StartWindow();
glDeleteTextures(1, &texture);
// THE FENCE, AND WHY IT IS NO LONGER A CLEAR.
//
// object_death is a kCtxObject value-class record whose wait class is kWaitNone, so
// under run-ahead the client publishes it and moves on, and the server-side tally below
// only moves at APPLY time. The clear this case used to fence on is kWaitNone TOO -
// PipeCalls.def's Clear row - so once the wait column governed (P5e's run-ahead), it
// waited for nothing and the read below raced the apply thread. Nothing had hidden that
// for a while except the per-record barriers the converted rows still took; P12 took
// those away on the wire arm (set_texture_params, respecify and the texture sub-data
// stop owning an answer), the client now runs further ahead, and the race stopped being
// a rarity. MEASURED on this entry under llvmpipe: 2 red of 5 runs (`deaths` 1 vs 1,
// the record applied a moment later) and 3 green of 3 with MOBILEGL_IPC_RUN_AHEAD=0. A
// timing race, never a lost record: the notice is emitted and applied either way.
//
// WHAT IS A REAL BOUNDARY, MEASURED ON ALL THREE ARMS RATHER THAN ASSUMED. The
// harness's applied-seq fence (WaitForSplitAppliedForTesting) is the right shape for
// inproc - and it is NOT usable here: this case is registered on the spawn and tcp arms
// too, the client's progress page there is advanced by frames rather than by the
// peer's own apply, and the wait times out. MEASURED with that fence in place: this
// case red at 5.0 s on DirectGLES.Tcp.Ct.
//
// A READBACK IS THE ONE BOUNDARY THAT HOLDS ON EVERY ARM. ReadPixels is
// kReplySlot|kWaitReply, so the client blocks on its answer, and an answer is posted
// only after its record has been APPLIED - and the ring applies in order, so the death
// published before it is applied too. (On the spawn and tcp arms the counter below
// comes from the peer's log, which ServerCounters() syncs before it reads - and that
// sync is itself a round trip the death has already crossed.)
GLubyte fencePixel[4]{};
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, fencePixel);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the fence readback left a GL error behind";
const auto afterCounters = ServerCounters();
ASSERT_TRUE(afterCounters.valid) << "server object-death telemetry missing";
EXPECT_GT(afterCounters.deaths, deathsBefore)
const ControlRecords counters = EndWindowAndReadControlRecords(mark);
ASSERT_TRUE(counters.valid) << "server object-death telemetry missing";
EXPECT_GE(counters.objectDeaths, 1)
<< "the texture's death produced no object_death record; the server's twin was "
"never told to let go";
@@ -323,17 +302,15 @@ namespace MGITest {
// Die unbound, framebuffer first so the attachment's own death is a separate record.
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
const auto beforeCounters = ServerCounters();
ASSERT_TRUE(beforeCounters.valid) << "server object-death telemetry missing";
const MobileGL::Uint64 deathsBefore = beforeCounters.deaths;
const PipeStatsWindow::LogMark mark = StartWindow();
// THE CLIENT'S OWN ACCOUNTING, read beside the server's (P7 wave 2 package C,
// CONTRACT-P7 §5.5's PipeSlotPeek clause). The two numbers answer DIFFERENT halves of
// one death and neither implies the other, which is exactly why both are read here:
// * `deaths` below says the SERVER was told - the half the missing Magma table
// * `od` below says the SERVER was told - the half the missing Magma table
// broke, and the only delivery a framebuffer has (no wire delete opcode at all);
// * this one says the CLIENT returned the slot. That half has been backend-neutral
// since P4a (PipeFill.cpp's NotifyAndFree frees whatever the notice does), so it
// is a CONTROL rather than the subject: if it ever regressed, a green `deaths`
// is a CONTROL rather than the subject: if it ever regressed, a green `od`
// would be measuring an object that never let go of its handle.
// `false` means the allocator is out of reach (a pull build, or Android's hidden
// symbols), and the peek is then simply not asserted - "could not look" is not
@@ -341,20 +318,9 @@ namespace MGITest {
unsigned fboSlotsBefore = 0;
const bool slotsReadable = PeekPipeSlotLiveCount(PipeSlotKind::Framebuffer, &fboSlotsBefore);
glDeleteFramebuffers(1, &fbo);
// THE FENCE, the texture case's exactly: object_death is a kCtxObject value-class
// record with wait class kWaitNone, so under run-ahead it is published WITHOUT waiting
// for its own apply (production-safe: the in-order ring applies it before any record
// that recycles the slot), and the server-side counter this assertion reads only moves
// at apply time. A clear cannot be that boundary - its own class is kWaitNone too - so
// this waits on a READBACK, which is kWaitReply on every arm. The texture case carries
// the measurements, including the boundary that is right for inproc and wrong for
// spawn/tcp.
GLubyte fencePixel[4]{};
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, fencePixel);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the fence readback left a GL error behind";
const auto afterCounters = ServerCounters();
ASSERT_TRUE(afterCounters.valid) << "server object-death telemetry missing";
EXPECT_GT(afterCounters.deaths, deathsBefore)
const ControlRecords counters = EndWindowAndReadControlRecords(mark);
ASSERT_TRUE(counters.valid) << "server object-death telemetry missing";
EXPECT_GE(counters.objectDeaths, 1)
<< "the framebuffer's death produced no object_death record - and no other "
"opcode can carry it. Backend "
<< Gl().BackendName();
@@ -445,6 +445,61 @@ void main() { oColor = vec4(vColor, 1.0); }
Gl().EndFrame();
}
// E3(a): the push is the ONLY route a coherent write takes to the server. The cases above go
// green just as well if something else delivered the bytes - an unmap or flush record, a
// whole-buffer re-sync, the live-host-writes state record, an inproc server reading client
// memory - so this one takes the push away and requires the draws to miss the write, within
// one frame and across a Present (the two shapes the cases above draw). Forget is what an
// unmap or a destroy calls and leaves the mapping itself alone; the re-announce is the call
// every map-state change makes, and the next draw must see the latest write.
TEST_F(PersistentCoherentMapScenario, ThePushIsTheOnlyRouteAWriteTakesToTheServer) {
if (!Ready() || IsSkipped()) return;
if (!SplitLane::IsSplitLane() || SplitLane::DeclaredPersistentMapArm() != "emulated") {
GTEST_SKIP() << "the push exists only on the emulated arm of a split transport, and this lane "
"declares no such arm (MGITEST_PERSISTENT_MAP_ARM=emulated, set per entry by "
"Harness/SplitLogPaths.cmake.in); monolith adopts the store and has no push";
}
if (!PersistentMapTrackerAvailable()) {
GTEST_SKIP() << "Harness/PersistentMapPeek cannot reach the tracker in this build, so the push "
"cannot be taken away";
}
ArmTheDrawState();
WriteThroughTheMapThenDraw(m_map, 0.0f, 1.0f, 0.0f);
const Image pushed = ReadPixels(Gl().Width(), Gl().Height());
EXPECT_EQ(FirstGLError(), 0u);
ASSERT_TRUE(WholeSurfaceIs(pushed, "green", "the write the push carries, before it is taken away"));
ASSERT_TRUE(ForgetLivePersistentMap(m_vbo)) << "no frontend BufferObject behind GL buffer " << m_vbo;
WriteThroughTheMapThenDraw(m_map, 1.0f, 0.0f, 0.0f);
const Image unpushed = ReadPixels(Gl().Width(), Gl().Height());
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(WholeSurfaceIs(unpushed, "green",
"a write through a mapping the tracker has forgotten: red here means "
"something other than the persistent-map push delivered it, and the "
"two cases above would stay green without the push"));
Gl().EndFrame();
ArmTheDrawState();
WriteThroughTheMapThenDraw(m_map, 0.0f, 0.0f, 1.0f);
const Image nextFrame = ReadPixels(Gl().Width(), Gl().Height());
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(WholeSurfaceIs(nextFrame, "green",
"the next frame's write through the forgotten mapping, after a Present: "
"red or blue here means the frame boundary delivered a write the push "
"did not carry"));
ASSERT_TRUE(ReannounceLivePersistentMap(m_vbo)) << "no frontend BufferObject behind GL buffer " << m_vbo;
glDrawArrays(GL_TRIANGLES, 0, 6);
const Image repushed = ReadPixels(Gl().Width(), Gl().Height());
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(WholeSurfaceIs(repushed, "blue",
"the latest write after the map is announced again: the tracker must "
"push the whole mapping once, or the green above proves nothing"));
AssertOrRecordArm(m_vbo, "after the push was taken away and given back");
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`.
@@ -549,11 +604,9 @@ void main() { oColor = vec4(vColor, 1.0); }
"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: "
"zero bytes. Zero here 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. Line: "
<< window.line;
} else {
// Recorded, not asserted: on this arm the number is whatever the driver's adoption
@@ -16,10 +16,11 @@
// clean pass. That is the failure mode this scenario exists to make impossible:
//
// Armed - the environment says the comparator is on for this process, so the
// library must SAY it armed. It asserts a library observable against
// the environment, the same shape UnlocatedIoBlockScenario's arming
// case and AsyncCompileScenario::ExtensionStringMatchesTheConfiguration
// use. A lane whose library never armed FAILS here; it never passes.
// library must SAY it armed, with fatal=1. It asserts a library
// observable against the environment, the same shape
// UnlocatedIoBlockScenario's arming case and
// AsyncCompileScenario::ExtensionStringMatchesTheConfiguration use. A
// lane whose library never armed FAILS here; it never passes.
// CorruptedFieldIsReported - the negative control for the comparator itself (gate G4). With
// MOBILEGL_PIPE_VERIFY_CORRUPT naming a field, the snapshot arm is
// perturbed before the entry compare, so a comparator that works must
@@ -221,11 +222,6 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
"set, so the library has nowhere to record that it armed; the Verify. "
"ctest entries set both";
}
if (StringKnobIsSet("MOBILEGL_PIPE_VERIFY_CORRUPT")) {
GTEST_SKIP() << "MOBILEGL_PIPE_VERIFY_CORRUPT is armed in this process, so a divergence "
"is the EXPECTED outcome and asserting on its absence here would be "
"backwards; the VerifyCorrupted. lane owns that half";
}
const MGITest::PipeStatsWindow::LogMark before = LibraryLogMark();
ASSERT_NO_FATAL_FAILURE(DrawOneFrame());
@@ -235,13 +231,28 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
// of the process, which may already have happened during the harness bring-up. The file
// is truncated at this process's first log write, so it still carries nothing else.
const std::string whole = LibraryLog();
EXPECT_NE(whole.find(kArmedLine), std::string::npos)
const std::size_t armedAt = whole.find(kArmedLine);
EXPECT_NE(armedAt, std::string::npos)
<< "MOBILEGL_PIPE_VERIFY=1 is set for this process and a frame was cleared, drawn and "
"read back, and the library never reported arming the comparator. Either this "
"library was not built with -DMOBILEGL_PIPE_VERIFY=ON (in which case the whole lane "
"is verifying nothing), or the arming MGLOG_I is gone. Log:\n"
<< whole;
// Fatal as well as armed: an ambient entry fails on a divergence only through the abort,
// so a lane armed non-fatal logs every divergence and stays green. This entry is the
// ambient entries' environment plus its marker and log (CMakeLists.txt builds both from
// one list), so its arming line answers for theirs.
if (armedAt != std::string::npos) {
const std::size_t eol = whole.find('\n', armedAt);
const std::string line =
whole.substr(armedAt, eol == std::string::npos ? std::string::npos : eol - armedAt);
EXPECT_NE(line.find("fatal=1"), std::string::npos)
<< "the comparator armed, but not fatal, so a divergence in any entry of this lane "
"is logged and the entry stays green. Arming line:\n"
<< line;
}
const std::string appended = LibraryLogSince(before);
EXPECT_EQ(appended.find(kDifferPrefix), std::string::npos)
<< "the comparator reported a push/pull divergence on an ordinary frame:\n"
@@ -257,9 +268,8 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
//
// It runs in its own lane (VerifyCorrupted.) because the knob is process-wide, and with
// MOBILEGL_PIPE_VERIFY_FATAL=0 so the process survives its own divergence and this case can
// read the report back out of the log. The CI step that runs the SAME knob against the
// ambient lane - where FATAL keeps its default - asserts the other half: there, the
// divergence must abort and ctest must go red.
// read the report back out of the log. With FATAL at its default the divergence aborts
// instead, which PipeInputsTest.VerifyCorruptKnobNamesTheFieldAtEntry asserts.
TEST_F(PipeVerifyArmingScenario, CorruptedFieldIsReported) {
if (!Ready()) return;
@@ -11,22 +11,26 @@
// The per-verb poison is the half of P1 that makes a forgotten fill row loud instead of silent: the
// filler stamps a generation on every field it copies for a verb, and an accessor whose stamp is not
// this verb's aborts with Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}. A mechanism that can only be
// observed when someone forgets a row is a mechanism nobody can trust, so MOBILEGL_PIPE_POISON_OMIT
// forges the mistake on purpose: it names one (verb, field) pair whose STAMP the filler skips while
// still copying the value, which is indistinguishable from a row that was never written.
// observed when someone forgets a row is a mechanism nobody can trust, so the control forges the
// mistake on purpose: the harness marker MGITEST_POISON_OMIT names one (verb, field) pair, and the
// child arms it through the verify build's setter (Harness/PoisonOmissionArm.h), so the filler skips
// that pair's STAMP while still copying the value - indistinguishable from a row never written.
//
// The scenario asserts both halves of "on THAT verb, and only there":
//
// OmittedFieldAbortsOnThatVerb - with MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit,
// a draw must still complete (GetActiveTextureUnit is not in kDraw's
// mask, and the draw's own fields are stamped normally) and the
// following glGenerateMipmap must abort naming exactly that pair.
// WithoutOmissionCompletes - the identical sequence with the knob unset runs to completion with
// no Fatal at all. Without this half, "it aborted" would say nothing
// about WHY: a poison that fired on every verb would look just as red.
// OmittedFieldAbortsOnThatVerb - with MGITEST_POISON_OMIT=ReadPixels:GetPixelStoreParameters, the
// draw and the glGenerateMipmap must still complete (the stamp is
// withheld at ReadPixels alone, and their own fields are stamped
// normally) and the following glReadPixels must abort naming
// exactly that pair.
// WithoutOmissionCompletes - the identical sequence with the marker unset runs to completion
// with no Fatal at all. Without this half, "it aborted" would say
// nothing about WHY: a poison that fired on every verb would look
// just as red.
//
// The knob is process-wide, so the two cases cannot share a lane: the first runs in the PoisonOmitted.
// entries, the second in the ambient Verify. entries (the first skips when the knob is unset).
// The omission is process-wide, so the two cases cannot share a lane: the first runs in the
// PoisonOmitted. entries, the second in the ambient Verify. entries (the first skips when the marker
// is unset).
//
// WHY THE SEQUENCE RUNS IN A SEPARATE PROCESS, AND WHY THAT PROCESS IS fork()+execve() AND NOT fork()
// ALONE. The poison reports with MGLOG_F and then std::abort(), in the middle of a GL command - so the
@@ -56,6 +60,7 @@
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/HeadlessGL.h"
#include "../Harness/PoisonOmissionArm.h"
#include "../Harness/ScenarioFixture.h"
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
@@ -81,28 +86,23 @@ extern char** environ;
namespace MGITest {
namespace {
// What the PoisonOmitted. ctest entries name, which CI requires to pass by name. Each pair is
// spelled here so the assertion below is about the exact string the poison contracts to
// print (ARCHITECTURE.md 9.2: Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}).
// What the PoisonOmitted. and VerifySplitPoisonOmitted. ctest entries put in
// MGITEST_POISON_OMIT, and CI requires both to pass by name. The pair is spelled here so the
// assertion below is about the exact string the poison contracts to print (ARCHITECTURE.md
// 9.2: Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}).
//
// TWO PAIRS SINCE P7 WAVE 3, ONE PER ARM, because the first one is structurally inert on
// the split arm. GenerateMipmap:GetActiveTextureUnit is the monolith control: the monolith
// backend resolves the texture to mip through the active unit. Under a transport it does
// not - since P5c hd the server's generate_mipmap resolves the texture from the verb's own
// handle (MGPipeApplier().VerbMipRes) and never reads the active unit - so omitting that
// stamp is omitting a stamp nobody reads, and the child exits 0 (measured on
// integration-verify-split, both backends). ReadPixels:GetPixelStoreParameters is the
// split control: the server's read_pixels backend call DOES read the pack half through the
// accessor, under the server's own stamp, which honours the knob in a verify build
// (MG_Backend/MGPipe/PipeInputs.cpp's ServerPoisonOmission).
// ONE PAIR FOR BOTH ARMS SINCE P13 W5. The record arm resolves glGenerateMipmap's texture
// from the verb's own handle and never reads the active unit, so the old monolith pair,
// GenerateMipmap:GetActiveTextureUnit, withheld a stamp nobody reads. ReadPixels' pack
// state is read on both arms: by the monolith record arm around its neutral-pack readback,
// and by the server's read_pixels under the server's own stamp, which withholds the pair
// too (MG_Backend/MGPipe/PipeInputs.cpp, MGPipeServerSetPoisonOmission).
//
// AND EACH PAIR SAYS WHICH ROLE HAS TO FIRE (`FiresOnTheServer`), which is the whole point
// of the split pair and was not checked at all until P7 wave 3's V1 fix round. The reader
// below concatenates the child's two role halves and the assertion searched the union, so
// `VerifySplitPoisonOmitted.` would have been just as green if the stamp the knob withheld
// had been the CLIENT's - which is the case it exists to rule out, because the mechanism
// under test is MG_Backend/MGPipe/PipeInputs.cpp's server-side stamp honouring the knob.
// The monolith pair has one role and one log half, so it keeps the union.
// AND THE PAIR SAYS WHICH ROLE HAS TO FIRE (`FiresOnTheServer`, P7 wave 3's V1 fix round).
// The reader below concatenates the child's two role halves, so `VerifySplitPoisonOmitted.`
// would be just as green if the withheld stamp had been the CLIENT's - the case it exists to
// rule out. A build without MOBILEGL_BUILD_DISAGGREGATED has one role and one log half, so
// there the union is all there is.
struct OmittedPair {
const char* Verb;
const char* Field;
@@ -110,7 +110,6 @@ namespace MGITest {
bool FiresOnTheServer; // the Fatal must be in the child's SERVER half, not merely present
};
constexpr OmittedPair kOmittedPairs[] = {
{"GenerateMipmap", "GetActiveTextureUnit", {"@DrawArrays", "@DrawArrays"}, false},
{"ReadPixels", "GetPixelStoreParameters", {"@DrawArrays", "@GenerateMipmap"}, true},
};
constexpr const char* kFatalPrefix = "Fatal{UnmigratedPipeInput";
@@ -120,6 +119,11 @@ namespace MGITest {
constexpr const char* kChildMarker = "MGITEST_POISON_OMISSION_CHILD";
constexpr const char* kWorkerFilter = "--gtest_filter=PoisonOmissionScenario.TheSequenceThePoisonControlsRun";
// The pair to withhold, as <Verb>:<Field>, set by the PoisonOmitted. ctest entries and
// inherited by the child. A HARNESS marker (hence MGITEST_): the library never reads it,
// and the worker arms the omission itself (Harness/PoisonOmissionArm.h).
constexpr const char* kOmissionMarker = "MGITEST_POISON_OMIT";
constexpr const char* kVS = R"(#version 330 core
in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
@@ -265,14 +269,14 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
glFinish();
std::fprintf(stderr, "[itest] poison worker: the draw completed\n");
// The verb the omission names. Under MOBILEGL_PIPE_POISON_OMIT this must abort.
// A texture verb between the draw and the readback; the omission does not name it.
glBindTexture(GL_TEXTURE_2D, texture);
glGenerateMipmap(GL_TEXTURE_2D);
glFinish();
std::fprintf(stderr, "[itest] poison worker: glGenerateMipmap returned\n");
// The split arm's verb (kOmittedPairs[1]): a readback, which is a round trip, so
// the server has applied it - and read the pack state - before this returns.
// The verb the omission names (kOmittedPairs): a readback, which is a round trip,
// so the server has applied it - and read the pack state - before this returns.
unsigned char centre[4] = {};
glReadPixels(gl.Width() / 2, gl.Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, centre);
std::fprintf(stderr, "[itest] poison worker: glReadPixels returned\n");
@@ -382,6 +386,18 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
}
if (!Ready()) return;
// The omission the marker names, armed on this thread - the one that issues the GL calls
// - before the sequence's first verb. Without the marker nothing is armed, which is
// WithoutOmissionCompletes' half.
if (const char* marker = std::getenv(kOmissionMarker); marker != nullptr && *marker != '\0') {
const std::string pair(marker);
const std::string::size_type colon = pair.find(':');
ASSERT_NE(colon, std::string::npos) << kOmissionMarker << "=" << pair << " is not <Verb>:<Field>";
ASSERT_TRUE(ArmPoisonOmission(pair.substr(0, colon).c_str(), pair.substr(colon + 1).c_str()))
<< "could not arm the omission " << kOmissionMarker << "=" << pair
<< ": the build has no setter (-DMOBILEGL_PIPE_VERIFY=ON compiles it) or a name is unknown";
}
RunSequence();
#if MGITEST_POISON_HAVE_FORK
@@ -404,21 +420,22 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
TEST_F(PoisonOmissionScenario, OmittedFieldAbortsOnThatVerb) {
if (!Ready()) return;
if (!StringKnobIsSet("MOBILEGL_PIPE_POISON_OMIT")) {
GTEST_SKIP() << "this case is the poison's negative control and needs "
"MOBILEGL_PIPE_POISON_OMIT=<Verb>:<Field> for the whole process, which "
"is what the PoisonOmitted. ctest entries set";
if (!StringKnobIsSet(kOmissionMarker)) {
GTEST_SKIP() << "this case is the poison's negative control and needs " << kOmissionMarker
<< "=<Verb>:<Field>, which is what the PoisonOmitted. ctest entries set";
}
const std::string knob = std::getenv("MOBILEGL_PIPE_POISON_OMIT");
const std::string marker = std::getenv(kOmissionMarker);
const OmittedPair* pair = nullptr;
for (const OmittedPair& candidate : kOmittedPairs) {
if (knob == std::string(candidate.Verb) + ":" + candidate.Field) pair = &candidate;
if (marker == std::string(candidate.Verb) + ":" + candidate.Field) pair = &candidate;
}
if (pair == nullptr) {
GTEST_SKIP() << "MOBILEGL_PIPE_POISON_OMIT is " << knob << ", but this case only knows "
<< "how to provoke " << kOmittedPairs[0].Verb << ":" << kOmittedPairs[0].Field << " and "
<< kOmittedPairs[1].Verb << ":" << kOmittedPairs[1].Field;
GTEST_SKIP() << kOmissionMarker << " is " << marker << ", but this case only knows how to provoke "
<< kOmittedPairs[0].Verb << ":" << kOmittedPairs[0].Field;
}
ASSERT_TRUE(PoisonOmissionArmAvailable())
<< kOmissionMarker << " is set, but only a -DMOBILEGL_PIPE_VERIFY=ON build compiles the setter "
<< "the child arms the omission with (Harness/PoisonOmissionArm.h), so this control cannot run here";
const std::string expectedPair = std::string(pair->Field) + "@" + pair->Verb;
int status = 0;
@@ -452,10 +469,10 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
<< childLog;
#if MOBILEGL_BUILD_DISAGGREGATED
// WHICH ROLE, for the split pair (P7 wave 3, V1). The union above answers "did the
// poison fire", which is all the monolith pair can be asked; this pair exists to prove
// that the SERVER'S OWN verb stamp honours MOBILEGL_PIPE_POISON_OMIT
// (MG_Backend/MGPipe/PipeInputs.cpp's ServerPoisonOmission) - the thing that was NOT
// true before wave 3 and made the control silently green. A client-side firing would
// poison fire", which is all a one-role build can be asked; this pair exists to prove
// that the SERVER'S OWN verb stamp honours the omission
// (MG_Backend/MGPipe/PipeInputs.cpp's MGPipeServerSetPoisonOmission) - the thing that was
// NOT true before wave 3 and made the control silently green. A client-side firing would
// satisfy the union and prove the opposite of what the entry claims, so the half is
// named here.
if (pair->FiresOnTheServer) {
@@ -481,11 +498,11 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
}
// The ambient half of the pair, in the ambient Verify. lanes: the same sequence with the
// knob UNSET must run to completion and log no Fatal at all. The PoisonOmitted. entries are
// the armed half (knob set, the verb must abort), and CI requires those entries by name.
// marker UNSET must run to completion and log no Fatal at all. The PoisonOmitted. entries are
// the armed half (marker set, the verb must abort), and CI requires those entries by name.
//
// It does not skip when MOBILEGL_PIPE_POISON_OMIT is set: run by hand with the knob
// exported, it goes red, and the note in its failure says that is the poison firing.
// It does not skip when MGITEST_POISON_OMIT is set: run by hand with the marker exported,
// it goes red, and the note in its failure says that is the poison firing.
TEST_F(PoisonOmissionScenario, WithoutOmissionCompletes) {
if (!Ready()) return;
@@ -493,7 +510,7 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
GTEST_SKIP() << "the poison is only compiled into the push/verify builds; in an ordinary "
"build there is nothing for this control to be a control OF";
}
const bool omissionArmed = StringKnobIsSet("MOBILEGL_PIPE_POISON_OMIT");
const bool omissionArmed = StringKnobIsSet(kOmissionMarker);
int status = 0;
std::string reason;
@@ -501,10 +518,10 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
const std::string childLog = ReadChildLog();
const std::string note =
omissionArmed
? std::string(" NOTE: MOBILEGL_PIPE_POISON_OMIT is set in this process, so this failure is "
"expected: the poison IS armed, and this entry going red is the proof.")
: std::string();
omissionArmed ? std::string(" NOTE: ") + kOmissionMarker +
" is set in this process, so this failure is expected: the poison IS armed, and "
"this entry going red is the proof."
: std::string();
ASSERT_TRUE(WIFEXITED(status))
<< "with no omission armed, a draw followed by glGenerateMipmap must complete; the child "
<< DescribeStatus(status)
+1 -1
View File
@@ -31,7 +31,7 @@
X(UnnamedIdentity, ProtocolCorruption, "a per-context memo was consulted with a null context identity") \
X(PipeLegacyMemosDisabled, ProtocolCorruption, "a subsystem bit is clear and this build compiles no legacy arm to fall back to (unreachable since P13 W3b fixed bits 0-13 on)") \
X(PipeSubsystemsFixedOn, ProtocolCorruption, "MOBILEGL_PIPE_PUSH cleared one of subsystem bits 0-13, which P13 fixed on") \
X(PipeVerifyBadKnob, ProtocolCorruption, "a MOBILEGL_PIPE_VERIFY/POISON knob named a verb or field that does not exist") \
X(PipeVerifyBadKnob, ProtocolCorruption, "MOBILEGL_PIPE_VERIFY_CORRUPT named a field that does not exist") \
X(PipeVerifyDiffer, ProtocolCorruption, "the verify comparator found a pushed field differing from the live one") \
X(UnexpectedMapAccept, ProtocolCorruption, "a persistent-map accept arrived where the protocol permits none") \
X(UncarriedInitialBytes, ProtocolCorruption, "a producer's initial bytes were required on the wire and absent") \
+2 -2
View File
@@ -10,8 +10,8 @@
// MGP_FILL_VERB_LIST: every GLFunctionsTable function pointer in declaration order, with its
// class (gen_pipe.py refuses any other set). MGP_FILL_FIELD_LIST: the fields a class may read;
// sticky fields (MGP_COVERAGE_STICKY_LIST) are implicit. Fix Fatal{UnmigratedPipeInput} by
// adding a row, never by making it sticky. A row is removed only on a MOBILEGL_PIPE_POISON_OMIT
// run of the full CTS caselist on both devices; a corpus that never reaches a path proves nothing.
// adding a row, never by making it sticky. A row is removed only after a poison build without it
// ran the full CTS caselist on both devices; a corpus that never reaches a path proves nothing.
// gen_pipe.py's block regexes end at a blank line: keep the empty line after each macro.
// clang-format off
+1 -1
View File
@@ -15,7 +15,7 @@
// materialising a queued clear, overriding a sampler's filter) would leave the copy stale, so
// the mutator calls MGP_NOTE_MUTATION(Field) where it moves the value. The notice refreshes
// that one field when it belongs to the verb in flight. It never touches the poison stamp, so a
// stamp withheld by MOBILEGL_PIPE_POISON_OMIT stays withheld.
// stamp the fill withheld (negative control B) stays withheld.
#include <MG_Pipe/MGPipe.h>
namespace MobileGL::MG_Pipe {
// MG_Impl/Pipe/PipeFill.cpp (the client side, the only place that may spell pGLContext).
+5 -38
View File
@@ -236,14 +236,6 @@ namespace MobileGL::MG_Remote::Client {
#endif
}
Bool VerbBarrierFromConfig() {
#if MOBILEGL_BUILD_DISAGGREGATED
return MG_Config::Ipc.VerbBarrier != 0;
#else
return true;
#endif
}
// ---- c1: the barrier's two flags ------------------------------------------------
//
// thread_local for the client's own "am I waiting", a shared atomic for "is the apply
@@ -1810,14 +1802,6 @@ namespace MobileGL::MG_Remote::Client {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
m_barrierArmed = VerbBarrierFromConfig();
if (!m_barrierArmed) {
MGLOG_W("MG_Remote client: MOBILEGL_IPC_VERB_BARRIER=0 - this is R-1's NEGATIVE "
"CONTROL and is EXPECTED to be red. 31 of the 63 PipeInputs fields are still "
"pulled from a live GLContext by the client's residual fill, so a free-running "
"queue lets the server read a FUTURE value of them");
}
// ---- 6. the client's segment table. IT MUST NOT INSTALL THE PROCESS RESOLVER: there is
// exactly one gMGPipeSegmentResolver per process, the SERVER role owns it (table 3), and
// the client never resolves a span at all - it only ever writes Ptr = nullptr (R-2's
@@ -1989,9 +1973,8 @@ namespace MobileGL::MG_Remote::Client {
// permits submittedSeq to be published lazily and RingProducer::Publish encourages batching
// the publish, so the shared watermark is allowed to lag the emitter - and a drain
// that waited for `appliedSeq >= submittedSeq` would then under-wait and free an
// emitter-owned var-tail while a record still names it. With the verb barrier armed
// the two are equal; under MOBILEGL_IPC_VERB_BARRIER=0, R-1's negative control that
// the phase has to run once, they are not.
// emitter-owned var-tail while a record still names it. Run-ahead and the batched
// waits return before the apply, so the two may differ when Stop() runs.
const Uint64 submitted = m_producer.LastPublishedSeq();
m_producer.PublishAndNotify(submitted);
if (submitted != 0 &&
@@ -2338,14 +2321,6 @@ namespace MobileGL::MG_Remote::Client {
}
}
if (!m_barrierArmed && !ownsReplySlot) {
// R-1's NEGATIVE CONTROL ARM, and the only thing it turns off is the barrier. A
// reply-slot row still waits: the answer is not derivable here and R-5 forbids
// inventing one, so MOBILEGL_IPC_VERB_BARRIER=0 makes the queue free-running, not
// the client clairvoyant.
return seq;
}
++m_recordWaits;
if (MG_Config::Ipc.WaitStats && RunAheadArmed()) ++g_waitCensus.recordWaits[static_cast<SizeT>(op)];
const BarrierWaitScope waiting;
@@ -2441,10 +2416,6 @@ namespace MobileGL::MG_Remote::Client {
return seq;
}
Bool ClientSession::BarrierArmed() const {
return m_barrierArmed;
}
// ---- P5e (ra): the run-ahead latch and the waits it leaves behind ---------------------
Bool ClientSession::RunAheadArmed() const {
@@ -2465,8 +2436,8 @@ namespace MobileGL::MG_Remote::Client {
#if MOBILEGL_BUILD_DISAGGREGATED
// THE CONJUNCTION, IN THE CONTRACT'S OWN ORDER (§1). The caps read is last because it
// is the only one that costs a mirror load, and because it is the only one whose answer
// can change: the other two are process configuration.
const Bool armed = m_barrierArmed && MG_Config::Ipc.RunAhead != 0 && Caps().HasCap(MG_Pipe::kCapRunAheadApply);
// can change: the other one is process configuration.
const Bool armed = MG_Config::Ipc.RunAhead != 0 && Caps().HasCap(MG_Pipe::kCapRunAheadApply);
if (!m_runAheadLatched) {
m_runAheadLatched = true;
m_runAheadArmed = armed;
@@ -2475,7 +2446,7 @@ namespace MobileGL::MG_Remote::Client {
"kCapRunAheadApply, so an unbarriered record is published and not "
"waited for (CONTRACT-P5E §2.3), paced by a present credit of %u",
MG_Config::Ipc.PresentCredit);
} else if (MG_Config::Ipc.RunAhead != 0 && m_barrierArmed) {
} else if (MG_Config::Ipc.RunAhead != 0) {
// THE KNOB IS NOT A SWITCH ON ITS OWN, and an operator who set it has to be
// told that, or an A/B arm that measured nothing looks like an A/B arm that
// measured no difference (Config.h's own argument for parsing it everywhere).
@@ -2747,10 +2718,6 @@ namespace MobileGL::MG_Remote::Client {
if (MG_Config::Ipc.BatchWaits != 0) return;
if (Server::ServerLoop::OnApplyThread()) return; // the applier owns the block inside a verb
if (!ClientSessionInstance().Started()) return; // the bring-up window pre-dates the roles
// MOBILEGL_IPC_VERB_BARRIER=0 is R-1's NEGATIVE CONTROL: the single-writer rule is off
// by the operator's own hand there, and EmitAndWait's Fatal{BarrierViolation} owns the
// red. Firing here instead would pre-empt the control's evidence line.
if (!ClientSessionInstance().BarrierArmed()) return;
if (!ApplyThreadIsInsideApplier()) return;
// LAST, because it is the only thread_local left in the chain: an emutls call per
// access in a shared library, and it must stay thread-local (see InBarrierWait's note
+6 -12
View File
@@ -193,7 +193,7 @@ namespace MobileGL::MG_Remote::Client {
Wire::PipeWireEncoder& Encoder();
CapsMirror& Caps();
// Emit one record and, if the barrier is armed, wait for it. `replyOut`/`replyBytes`
// Emit one record and wait for it unless the wait rule lets it go. `replyOut`/`replyBytes`
// name where a kReplySlot answer lands; pass {nullptr, 0} for a call that has none.
// Returns the record's seq, which is also its reply-slot id.
//
@@ -280,18 +280,13 @@ namespace MobileGL::MG_Remote::Client {
// !willReadReply, and !willReadReply on a row with no reply slot, are Fatal by name: the
// first would wait for an answer never sent, the second discards one never posted.
// MOBILEGL_IPC_VERB_BARRIER. False is the R-1 negative control and is EXPECTED to be
// red; it must be run once and the way it goes red recorded.
Bool BarrierArmed() const;
// ---- P5e (ra): the wait rule (CONTRACT-P5E §1, §2.3, §2.4, §2.5) -------------------
// A CONJUNCTION OF THREE, LATCHED ONCE: `m_barrierArmed && Ipc.RunAhead &&
// Caps().HasCap(kCapRunAheadApply)`. The third conjunct is the server's own statement
// that it applies an unbarriered record without reading client memory, so run-ahead is
// never something the client can turn on by itself - on Magma, and on Espryt until the
// integration commit publishes bit 10, this answers false and EmitAndWaitTails runs
// today's lockstep path byte for byte.
// A CONJUNCTION OF TWO, LATCHED ONCE: `Ipc.RunAhead && Caps().HasCap(kCapRunAheadApply)`.
// The second conjunct is the server's own statement that it applies an unbarriered record
// without reading client memory, so run-ahead is never something the client can turn on
// by itself - on Magma, and on Espryt until the integration commit publishes bit 10,
// this answers false and EmitAndWaitTails runs today's lockstep path byte for byte.
//
// LATCHED AT THE FIRST CAPS ADOPTION AFTER Start, AND A LATER SNAPSHOT MAY ONLY TURN IT
// OFF. A caps re-run cannot promote a lockstep server to a run-ahead one mid-frame:
@@ -540,7 +535,6 @@ namespace MobileGL::MG_Remote::Client {
Wire::PipeWireEncoder m_encoder;
Wire::SegmentTable m_segments;
Bool m_barrierArmed = true;
// P5e (ra): the latch of RunAheadArmed() and the fact that it has been taken once.
Bool m_runAheadArmed = false;
Bool m_runAheadLatched = false;
+8 -7
View File
@@ -28,6 +28,7 @@
#include <MG_Pipe/PipeApply.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Debug/Log.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <MG_Util/Metrics/TextureMetrics.h>
#include <cstdlib>
@@ -274,10 +275,10 @@ namespace MobileGL::MG_Remote::Server {
// backend->SwapEGLBuffers -> BackendObject::SwapEGLBuffers -> eglSwapBuffers, and never
// Present() - so the two paths do NOT both end here. The frame count staying in step with
// the swap count rests entirely on c1 emitting exactly one present record per swap;
// nothing here compares Presents() to a swap count. If that drifts, the frame fence and
// nothing here compares the present count to a swap count. If that drifts, the frame fence and
// TrimBufferPool's recycle watermark stop tracking frames - which is the reason the 1:1
// was wanted, recorded here so a future swap-without-present is looked for rather than
// assumed impossible. Presents() is exposed for a lane that wants to make the comparison.
// assumed impossible.
// The frame's damage rides to the native present for the length of this call.
MG_Backend::SetCurrentPresentDamage(DamageOfRecord(present.DamageCount, present.Damage));
table->Present();
@@ -302,7 +303,7 @@ namespace MobileGL::MG_Remote::Server {
"waits on it for its credit (CONTRACT-P5E §2.4); returning a credit for "
"serial 0 would release a wait that is asking about frame N");
}
m_lastPresentSerial = present.FrameSerial != 0 ? present.FrameSerial : m_presents;
const Uint64 serial = present.FrameSerial != 0 ? present.FrameSerial : m_presents;
// §2.4's other half, and the reason ServerSession::ReturnPresentCredit has had no
// production caller since v1 wrote it: ONE CREDIT PER SWAP, returned after Present()
// has returned rather than before it, because what the client is waiting for is the
@@ -314,7 +315,7 @@ namespace MobileGL::MG_Remote::Server {
// or two ago at once, and a report posted only after this batch would race it. A
// status query, no submit: the swap has just submitted.
if (session->EventRingHasRoom()) ReportSignaledFences(/*flush=*/false);
session->ReturnPresentCredit(m_lastPresentSerial);
session->ReturnPresentCredit(serial);
}
return true;
}
@@ -350,7 +351,8 @@ namespace MobileGL::MG_Remote::Server {
// MGPipeApplierReset passes because this IS the apply thread; the GL-thread direct
// call it replaced is the Fatal arm.
MG_Pipe::MGPipeApplierReset();
++m_applierResets;
if (MG_Util::PipeStats::Enabled())
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ApplierResets, 1);
return true;
}
@@ -608,7 +610,6 @@ namespace MobileGL::MG_Remote::Server {
"release) is the only token that needs no entry",
static_cast<unsigned long long>(bind.ClientContextToken));
}
++m_bindContexts;
#if MOBILEGL_BUILD_DISAGGREGATED
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md). THE NATIVE HALF OF THE SWITCH.
//
@@ -658,7 +659,7 @@ namespace MobileGL::MG_Remote::Server {
// to nothing.
MG_Backend::DirectGLES::ReleaseTwinsForWireObjectDeath(death.Handle,
static_cast<MG_Pipe::MGPipeKind>(death.Kind));
++m_objectDeaths;
if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ObjectDeaths, 1);
return true;
}
+3 -27
View File
@@ -183,20 +183,6 @@ namespace MobileGL::MG_Remote::Server {
Bool OnSharedImage(const MG_Pipe::MGPSharedImageOp& op, Uint64 seq,
MG_Pipe::MGPSharedImageReply& reply) override;
// P5c ct's tallies, for the same reason every other row's tally exists (R-16: a probe
// may not arm against a stub). ApplierResets counts the records ACCEPTED (serial
// checked, reset run); ObjectDeaths counts every record the sink dispatched.
Uint64 ApplierResets() const { return m_applierResets; }
Uint64 ObjectDeaths() const { return m_objectDeaths; }
Uint64 BindContexts() const { return m_bindContexts; }
// The ContextSerial the NEXT applier_reset record must carry. Exposed so a case can
// assert the sequence rather than only the count.
Uint64 ExpectedApplierResetSerial() const { return m_applierResetSerial; }
// The session's per-verb tallies; the record verbs' are RecordVerbSink's.
Uint64 Presents() const { return m_presents; }
Uint64 LastPresentSerial() const { return m_lastPresentSerial; }
private:
// A backend change ends every fence and query this sink holds (they are the old
// backend's natives).
@@ -231,20 +217,10 @@ namespace MobileGL::MG_Remote::Server {
Bool QueryGpuComplete(QueryEntry& entry, Bool wait);
UnorderedMap<Uint32, QueryEntry> m_queries;
Uint64 m_presents = 0;
Uint64 m_lastPresentSerial = 0;
// P5c ct. m_applierResetSerial is BOTH the expected ContextSerial of the next record
// and the count of accepted resets: the two are one number because the serial is the
// session's own count of applier_reset records (CONTRACT-P5C.md §1: asserted, never
// dispatched on). m_objectDeaths counts records dispatched, released twin or not -
// the idempotent second path answering "nothing held it" is a legal record.
// m_applierResetSerial is both the expected ContextSerial of the next record and the
// count of accepted resets: the serial is the session's own count of applier_reset
// records (CONTRACT-P5C.md §1: asserted, never dispatched on).
Uint64 m_applierResetSerial = 0;
Uint64 m_applierResets = 0;
Uint64 m_objectDeaths = 0;
// P14 S1. BindContexts counts the records the sink dispatched and bound on the session;
// the token itself lives on the session (ServerSession::CurrentContextToken), which is
// the object the control plane's CreateContext/DestroyContext also own - one table, one
// owner, and the sink is only the ring's door into it.
Uint64 m_bindContexts = 0;
// The newest native fence (shared_image NativeFence) this session handed out, kept so a
// later fence with nothing left to export still answers with a descriptor: by then this
// one has signaled (the backend waited the work out), and a signaled sync_file stays so.
-13
View File
@@ -765,19 +765,6 @@ namespace {
const auto* envelope = Protocol::GetCtrlEnvelope(buffer.data());
if (const auto* flush = envelope->msg_as_LogFlush()) {
if (flush->ack()) break;
if (const char* inspect = std::getenv("MOBILEGL_TEST_SERVER_COUNTERS");
inspect && std::strcmp(inspect, "1") == 0) {
// SyncPeerLog has fenced the client's complete command prefix.
// Acquire the applier's publication before reading its counters;
// this serial client sends no more records until this ack.
const auto applied = session.DataLink()->Progress()->appliedSeq.load(std::memory_order_acquire);
const auto& verbs = session.Applier().Verbs();
MGLOG_I("MGPipe server counters: applied=%llu resets=%llu reset-serial=%llu deaths=%llu",
static_cast<unsigned long long>(applied),
static_cast<unsigned long long>(verbs.ApplierResets()),
static_cast<unsigned long long>(verbs.ExpectedApplierResetSerial()),
static_cast<unsigned long long>(verbs.ObjectDeaths()));
}
FlushAck ack{control.get(), logForward.get(), flush->seq()};
MobileGL::MG_Util::Debug::WithLogBarrier(&SendLogAck, &ack);
continue;
+2 -3
View File
@@ -267,9 +267,8 @@ namespace MobileGL::MG_Remote::Transport {
// encourages batching the publish, so the shared watermark may lag the
// emitter - and a drain that waits for `appliedSeq >= submittedSeq` would
// then under-wait and free an emitter's var-tail while a record still
// names it. With the verb barrier armed the two are equal; with
// MOBILEGL_IPC_VERB_BARRIER=0, R-1's negative control which the phase has
// to run once, they are not.
// names it. Run-ahead and the batched waits publish without waiting, so
// at teardown the two may differ.
std::uint64_t LastPublishedSeq() const { return m_lastPublishedSeq; }
// The verb barrier's wait, AND the reply's wait: they are the same wait
@@ -501,14 +501,7 @@ namespace MobileGL::MG_State::GLState {
// carries are real, current and the cheapest honest ones there are. A zero-length
// record would have been the alternative and it is illegal by contract rule A.
if (m_size == 0) return;
const Uint64 blockBytes = MG_Record::PersistentMapTracker::BlockBytes();
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 TURNS THE WHOLE MECHANISM OFF, STATE RECORD
// INCLUDED (E3(a)). An earlier cut skipped the blocks and still shipped a whole buffer
// here, which meant an unmap delivered the bytes the control exists to withhold - and
// a negative control that still delivers them is not a control. With the knob at 0
// nothing is published and nothing is pushed, so the probe answers clean, the frame
// draws the last uploaded bytes, and PersistentCoherentMapScenario goes red.
if (blockBytes == 0) return;
constexpr Uint64 blockBytes = MG_Record::PersistentMapTracker::BlockBytes();
const SizeT length = blockBytes >= static_cast<Uint64>(m_size) ? m_size : static_cast<SizeT>(blockBytes);
NotifySubData(0, length);
}
@@ -150,7 +150,7 @@ namespace MobileGL {
// behaviour and D-J forbids it. The pull build compiles none of it, which is also
// how G1 holds over a file this central.
// ONE MOBILEGL_IPC_PERSISTENT_BLOCK_KB BLOCK of the mapped span, emitted through
// ONE BLOCK of the mapped span (PersistentMapTracker::BlockBytes), emitted through
// the private NotifySubData - the same serial, the same defined-content flag, the
// same record - so the split arm and the monolith arm differ in HOW the span is
// cut and in nothing else. Called only by
@@ -724,10 +724,6 @@ namespace MobileGL::MG_Record {
return *instance;
}
Uint64 PersistentMapTracker::BlockBytes() {
return static_cast<Uint64>(MG_Config::RecordArm.PersistentBlockKb) * 1024ull;
}
Bool PersistentMapTracker::PushIsArmed() {
// P13 W4a: the record arm's buffer reads never ask the frontend object to sync a map, so
// the client pushes the mapped span wherever that arm runs - monolith's included.
@@ -908,33 +904,7 @@ namespace MobileGL::MG_Record {
Forget(buffer);
return;
}
const Uint64 blockBytes = BlockBytes();
// 0 IS THE NEGATIVE CONTROL, NOT "unlimited" (E3(a)). Pushing one whole-span block
// here would make the control green for the wrong reason - it has to disable the
// push, so that PersistentCoherentMapScenario draws the last uploaded bytes and goes
// red exactly the way an unpushed map does.
//
// AND IT SAYS SO, ONCE. Until now this was a silent `return`, so E3(a)'s red could
// only ever be the scenario's pixel assertion and the control had no way to tell "the
// push was disabled" apart from "the push was never armed, or never reached, or the
// knob never got here" (joint-v1.md §3: "There is no Fatal for block size zero";
// ID-65 assigns the line to x2). The control now requires BOTH: the pixel red AND
// this line in the entry's own private log. It is MGLOG_W and not a Fatal because 0
// is a legal configured value whose whole purpose is to keep running with the push
// off; aborting here would turn every E3(a) entry into a subprocess abort and take
// the pixel evidence with it.
if (blockBytes == 0) {
if (!m_blockZeroAnnounced) {
m_blockZeroAnnounced = true;
MGLOG_W("MGPipe: persistent-map push disabled - MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 "
"is exit gate E3(a)'s NEGATIVE CONTROL, not 'unlimited': a live "
"persistent WRITE mapping's dirty blocks are NOT being pushed, so the "
"server draws whatever bytes last crossed by some other route. A lane "
"that stays green with this set is not getting its pixels from the push");
}
return;
}
constexpr Uint64 blockBytes = BlockBytes();
const auto range = buffer.GetMappedRange();
const Uint64 begin = static_cast<Uint64>(range.start);
const Uint64 end = static_cast<Uint64>(range.end);
@@ -1077,15 +1047,12 @@ namespace MobileGL::MG_Record {
if (shadow != nullptr) {
state = &m_blockHashes[buffer.GetLifetimeId()];
const Uint64 blockCount = (end - begin + blockBytes - 1) / blockBytes;
// A remap, a resize or a block-size change invalidates the layout the
// hashes were taken against: reset, which forces one full push - the
// conservative answer for a new layout.
if (state->begin != begin || state->end != end || state->blockBytes != blockBytes ||
state->hashes.size() != blockCount) {
// A remap or a resize invalidates the layout the hashes were taken against:
// reset, which forces one full push - the conservative answer for a new layout.
if (state->begin != begin || state->end != end || state->hashes.size() != blockCount) {
*state = {};
state->begin = begin;
state->end = end;
state->blockBytes = blockBytes;
state->hashes.assign(static_cast<SizeT>(blockCount), 0);
}
}
@@ -28,14 +28,11 @@
// pushing, and the two arms diverge with no test able to say so. IsLivePersistentMap() is the
// single spelling; BufferObject::SyncPersistentMappedRange is held to it by a unit case.
//
// THE GRANULARITY. MOBILEGL_IPC_PERSISTENT_BLOCK_KB (default 64) blocks, keyed
// {MGPipeHandle, blockIndex} - the key is implicit, because a block ships as an ORDINARY
// resource_subdata record whose destination range is [blockIndex * blockBytes, + blockBytes).
// NO NEW RECORD KIND: the existing record is already chunked by
// MGPipeForEachSubDataRecordRange and already acceptance-gated, and a second way to say
// "these bytes go there" is a second way to get it wrong. A block size of 0 is E3(a)'s
// NEGATIVE CONTROL and means "push nothing" - not "one unlimited block" - so
// PersistentCoherentMapScenario must go red under it.
// THE GRANULARITY. 64 KiB blocks (BlockBytes()), keyed {MGPipeHandle, blockIndex} - the key
// is implicit, because a block ships as an ORDINARY resource_subdata record whose destination
// range is [blockIndex * blockBytes, + blockBytes). NO NEW RECORD KIND: the existing record is
// already chunked by MGPipeForEachSubDataRecordRange and already acceptance-gated, and a second
// way to say "these bytes go there" is a second way to get it wrong.
//
// Only changed blocks ship. At each validate point a member pushes just the blocks written
// since its last push. A map the mprotect write-fault tracker holds (MOBILEGL_PIPE_PERSISTENT_MPROTECT;
@@ -101,8 +98,8 @@ namespace MobileGL::MG_Record {
// client-exclusive). Leaks at exit for ID-8's reason, once per role-local singleton.
static PersistentMapTracker& Instance();
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB * 1024. Zero means the push is OFF (E3(a)).
static Uint64 BlockBytes();
// The push's block size (THE GRANULARITY above).
static constexpr Uint64 BlockBytes() { return 64ull * 1024ull; }
// Transport != Monolith. The whole module is inert on the monolith path: an extra
// resource_subdata record there would be new behaviour, which D-J forbids.
static Bool PushIsArmed();
@@ -154,7 +151,6 @@ namespace MobileGL::MG_Record {
void ResetCountersForTest() {
m_blocksPushed = 0;
m_bytesPushed = 0;
m_blockZeroAnnounced = false;
}
// Unit tests only: clears the membership, the hash cache and the mprotect tracked
// slots (restoring every tracked page writable first), and resets the counters.
@@ -225,7 +221,6 @@ namespace MobileGL::MG_Record {
struct BlockHashState {
Uint64 begin = 0;
Uint64 end = 0;
Uint64 blockBytes = 0;
Vector<Uint64> hashes;
};
UnorderedMap<Uint64, BlockHashState> m_blockHashes;
@@ -252,11 +247,6 @@ namespace MobileGL::MG_Record {
// ran them once (the two Members hooks) - the guard's thread-local read was a
// measurable per-buffer-per-draw cost on the emutls path.
void PushBlocksForChecked(MG_State::GLState::BufferObject& buffer);
// E3(a)'s diagnostic is emitted ONCE per process. PushBlocksFor runs at every validate
// point of every member, so an unlatched MGLOG_W would be one line per draw per mapping
// - and a control that has to grep a log cannot tell a message that fired from a
// message that flooded.
Bool m_blockZeroAnnounced = false;
};
// What the client's emit table calls immediately BEFORE emitting any verb that can read a
@@ -86,13 +86,11 @@ namespace {
protected:
void SetUp() override {
m_transport = MG_Config::Transport;
m_blockKb = MG_Config::RecordArm.PersistentBlockKb;
m_adoptTier = MG_Config::Ipc.AdoptTier;
m_pipeStats = MG_Config::Features.PipeStats;
m_context = MG_State::pGLContext.Take();
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Config::Transport = MG_Config::TransportMode::InProcess;
MG_Config::RecordArm.PersistentBlockKb = 64;
MG_Config::Ipc.AdoptTier = 2;
MG_Config::Features.PipeStats = true;
MG_Util::PipeStats::Init();
@@ -108,7 +106,6 @@ namespace {
PersistentMapTracker::Instance().ClearForTest();
MG_State::pGLContext = Move(m_context);
MG_Config::Transport = m_transport;
MG_Config::RecordArm.PersistentBlockKb = m_blockKb;
MG_Config::Ipc.AdoptTier = m_adoptTier;
MG_Config::Features.PipeStats = m_pipeStats;
MG_Util::PipeStats::Init();
@@ -121,7 +118,6 @@ namespace {
}
MG_Config::TransportMode m_transport = MG_Config::TransportMode::Monolith;
Uint32 m_blockKb = 64;
Uint32 m_adoptTier = 2;
Bool m_pipeStats = false;
SharedPtr<MG_State::GLState::GLContext> m_context;
@@ -463,35 +459,6 @@ TEST_F(SplitBufferSet, BothEdgesOfAWriteMapPublishOneStateRecord) {
"the second the record stays dirty for the buffer's life";
}
// E3(a)'s NEGATIVE CONTROL: 0 disables the push, it does not mean "one unlimited block" - and
// it disables the STATE RECORDS with it. An earlier cut skipped the blocks and still shipped a
// whole buffer at unmap, so a scenario that unmapped before reading back went green and the
// control was dead.
TEST_F(SplitBufferSet, AZeroBlockSizeTurnsThePushOffRatherThanMakingItUnlimited) {
MG_Config::RecordArm.PersistentBlockKb = 0;
constexpr SizeT kSize = 128u * 1024u;
auto buffer = MakeBuffer(23u, kSize);
const Uint64 beforeMap = buffer->GetChangeSerial();
buffer->AcquireMemoryRange(Range1D{0, kSize}, BufferMappingAccessBit::Write | BufferMappingAccessBit::Persistent);
ASSERT_TRUE(PersistentMapTracker::IsLivePersistentMap(*buffer));
EXPECT_EQ(buffer->GetChangeSerial(), beforeMap) << "the rising-edge state record went out with the push disabled";
const Uint64 before = MG_Util::PipeStats::TotalBytes(MG_Util::PipeStats::ByteClass::PersistentMapPush);
MG_Record::PushPersistentMapsBeforeVerb();
EXPECT_EQ(PersistentMapTracker::Instance().BlocksPushed(), 0u);
EXPECT_EQ(MG_Util::PipeStats::TotalBytes(MG_Util::PipeStats::ByteClass::PersistentMapPush), before)
<< "MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 must ship nothing, so that "
"PersistentCoherentMapScenario goes red under it";
const Uint64 beforeUnmap = buffer->GetChangeSerial();
buffer->ReleaseMemory(/*landStagedWrites=*/true);
EXPECT_EQ(buffer->GetChangeSerial(), beforeUnmap + 1)
<< "with the push off the unmap must emit only its own NotifyFlushMappedRange - a "
"falling-edge record here delivers a whole buffer the control exists to withhold, "
"and a negative control that still delivers the bytes is not a control";
}
// The set does not keep a pointer to a dead buffer.
TEST_F(SplitBufferSet, ADestroyedBufferLeavesTheSet) {
{
@@ -871,9 +838,6 @@ TEST(SplitBufferSet, TheLastBlockIsTheRemainderAndNotAWholeBlock) {
TEST(SplitBufferSet, BothEdgesOfAWriteMapPublishOneStateRecord) {
MGL_SPLIT_ONLY_OR_SKIP();
}
TEST(SplitBufferSet, AZeroBlockSizeTurnsThePushOffRatherThanMakingItUnlimited) {
MGL_SPLIT_ONLY_OR_SKIP();
}
TEST(SplitBufferSet, ADestroyedBufferLeavesTheSet) {
MGL_SPLIT_ONLY_OR_SKIP();
}
@@ -248,8 +248,8 @@ namespace {
kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers;
m_previousContext = MG_State::pGLContext.Take();
MG_State::pGLContext = MakeUnique<GLContext>();
MGPipeFramebufferEmitterInstance().ResetForTest();
MGPipeTextureEmitterInstance().ResetForTest();
MGPipeFramebufferEmitterInstance().ForgetSession();
MGPipeTextureEmitterInstance().ForgetSession();
MGPipeSetHashSuppressorInstance().InvalidateAll();
// The applier is a process singleton and its object records outlive a case; with both
// families' wired constants set the emissions actually land, so a case must start
@@ -259,8 +259,8 @@ namespace {
MGPipeApplierReleaseObjectRecords();
}
~FramebufferScope() {
MGPipeFramebufferEmitterInstance().ResetForTest();
MGPipeTextureEmitterInstance().ResetForTest();
MGPipeFramebufferEmitterInstance().ForgetSession();
MGPipeTextureEmitterInstance().ForgetSession();
MGPipeSetHashSuppressorInstance().InvalidateAll();
MGPipeApplierReset();
MGPipeApplierReleaseObjectRecords();
@@ -1345,7 +1345,7 @@ TEST(FramebufferEmit, TheBuildMemoReNotesBindBitsAfterTheTextureTableIsDropped)
<< "the attachment's RENDER_TARGET bit was never noted";
// A dropped table (a replaced session) forgets the sticky bits; the next build must note them again.
MGPipeTextureEmitterInstance().ResetForTest();
MGPipeTextureEmitterInstance().ForgetSession();
MGPipeSetHashSuppressorInstance().InvalidateAll();
Framebuffers().EmitFramebufferState(Ctx());
EXPECT_NE(MGPipeTextureEmitterInstance().TextureBindMask(handle) & kMGPipeBindRenderTarget, 0)
+1 -1
View File
@@ -581,7 +581,7 @@ void main() { imageStore(img, ivec2(0), vec4(1.0)); }
// Nothing produced the bit before the fix round.
TEST(ImageEmit, AnImageBoundTextureIsMarkedShaderImageBoundAtTheBind) {
EmitterScope scope;
MGPipeTextureEmitterInstance().ResetForTest();
MGPipeTextureEmitterInstance().ForgetSession();
const GLuint name = MakeImageTexture();
const auto& texture = Ctx().GetTextureObject(name);
ASSERT_TRUE(texture);
+114 -67
View File
@@ -75,10 +75,8 @@ namespace {
void SetUp() override {
m_previous = MG_State::pGLContext.Take();
MG_State::pGLContext = MakeUnique<GLContext>();
MGPipeSetPoisonOmission(nullptr, nullptr);
}
void TearDown() override {
MGPipeSetPoisonOmission(nullptr, nullptr);
MG_State::pGLContext = Move(m_previous);
}
SharedPtr<GLContext> m_previous;
@@ -88,11 +86,20 @@ namespace {
Bool Fresh(MGPipeInputField field) {
return MGPipeInputFieldIsFresh(gPipeInputs->FilledState(), field);
}
// Negative control B's omission, made here rather than by the library: fill `verb`, then put
// the stamp of `field` back to what it held before the fill. The value is copied and only the
// stamp is withheld, which is what a FillPoints.def row nobody wrote leaves behind.
void FillWithholdingTheStamp(MGPipeVerb verb, MGPipeInputField field) {
auto& filled = const_cast<MGPipeFilledState&>(gPipeInputs->FilledState());
const Uint64 before = filled.FilledGen[static_cast<SizeT>(field)];
MGPipeValidateForVerb(verb);
filled.FilledGen[static_cast<SizeT>(field)] = before;
}
#endif
[[maybe_unused]] constexpr const char* kOmittedFatal =
"Fatal{UnmigratedPipeInput, \"GetActiveTextureUnit@GenerateMipmap\"}";
[[maybe_unused]] constexpr const char* kOmissionKnob = "GenerateMipmap:GetActiveTextureUnit";
#if MGTEST_HAVE_FORK
struct ChildResult {
@@ -139,7 +146,8 @@ namespace {
// Negative control B, layer 1 (P1 brief D6 / G5): the omitted (verb, field) pair is the
// ONLY thing that goes stale - every other bit of the verb class's mask is fresh, every
// field outside it is not, and the verb after it neither heals the field (not in kDraw's
// mask) nor loses one of its own.
// mask) nor loses one of its own. The omission is the test's own (FillWithholdingTheStamp); a
// verify build also checks the scope of the library's (MGPipeSetPoisonOmission).
TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
#if !MOBILEGL_PIPE_POISON
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
@@ -148,8 +156,7 @@ TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
EXPECT_TRUE(Fresh(MGPipeInputField::GetActiveTextureUnit));
EXPECT_TRUE(Fresh(MGPipeInputField::GetTextureUnitObject));
MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit");
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
FillWithholdingTheStamp(MGPipeVerb::GenerateMipmap, MGPipeInputField::GetActiveTextureUnit);
EXPECT_TRUE(Fresh(MGPipeInputField::GetTextureUnitObject));
EXPECT_FALSE(Fresh(MGPipeInputField::GetActiveTextureUnit));
// The value was still copied: only the stamp is withheld.
@@ -173,15 +180,33 @@ TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
// A sticky field stays fresh across every verb.
EXPECT_TRUE(Fresh(MGPipeInputField::RecordError));
// And the omission is scoped to its verb: a different verb of the same class keeps it.
// And the omission is scoped to its fill: the next verb of the same class stamps it again.
MGPipeValidateForVerb(MGPipeVerb::BindImageTexture);
EXPECT_TRUE(Fresh(MGPipeInputField::GetActiveTextureUnit));
#if MOBILEGL_PIPE_VERIFY && MGTEST_HAVE_FORK
// The library's own omission, the one the PoisonOmitted entries arm, stays armed across fills
// and is scoped to its VERB: a different verb of the same class keeps the stamp. In a child,
// because nothing disarms it.
const ChildResult r = RunInChild([] {
if (!MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit")) ::_exit(3);
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
if (Fresh(MGPipeInputField::GetActiveTextureUnit)) ::_exit(4);
MGPipeValidateForVerb(MGPipeVerb::BindImageTexture);
if (!Fresh(MGPipeInputField::GetActiveTextureUnit)) ::_exit(5);
});
EXPECT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r)
<< " (3: the setter refused the names; 4: GenerateMipmap stamped the omitted "
"field; 5: BindImageTexture did not stamp it, so the omission matched the "
"class rather than the verb)\n"
<< r.Log;
#endif
#endif
}
// Negative control B, layer 2 (G5): the read itself. The child fills GenerateMipmap with the
// omission and reads gPipeInputs->GetActiveTextureUnit(); the parent expects SIGABRT and the
// exact Fatal line, and that nothing else was fatal.
// stamp of GetActiveTextureUnit withheld and reads gPipeInputs->GetActiveTextureUnit(); the
// parent expects SIGABRT and the exact Fatal line, and that nothing else was fatal.
TEST_F(PipeInputsTest, ReadingAnOmittedFieldAbortsNamingTheVerb) {
#if !MOBILEGL_PIPE_POISON
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
@@ -190,10 +215,9 @@ TEST_F(PipeInputsTest, ReadingAnOmittedFieldAbortsNamingTheVerb) {
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
const ChildResult r = RunInChild([] {
MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit");
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
(void)gPipeInputs->GetRenderStateParameters(); // a filled field of the preceding draw: must not abort
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
FillWithholdingTheStamp(MGPipeVerb::GenerateMipmap, MGPipeInputField::GetActiveTextureUnit);
(void)gPipeInputs->GetTextureUnitObject(0); // the sibling field: filled, must not abort
(void)gPipeInputs->GetActiveTextureUnit(); // the omitted field: Fatal
::_exit(3); // reached only if the poison failed
@@ -253,53 +277,6 @@ TEST_F(PipeInputsTest, ReadingBeforeAnyFillAbortsNamingNoVerb) {
#endif
}
// The MOBILEGL_PIPE_POISON_OMIT parser, through the Features field the loader fills: the
// child sets the knob after its parent filled without it, and the first fill after that
// parses it, logs the arming line and withholds the stamp exactly as the programmatic form.
TEST_F(PipeInputsTest, PoisonOmitKnobArmsTheOmission) {
#if !MOBILEGL_PIPE_POISON
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
#elif !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
MGPipeValidateForVerb(MGPipeVerb::DrawArrays); // the parent's parse saw an empty knob
const ChildResult r = RunInChild([] {
MG_Config::Features.PipePoisonOmit = kOmissionKnob;
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
(void)gPipeInputs->GetTextureUnitObject(0); // the sibling field: filled, must not abort
(void)gPipeInputs->GetActiveTextureUnit(); // the omitted field: Fatal
::_exit(3);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("MGPipe: poison omission armed - GetActiveTextureUnit@GenerateMipmap"), std::string::npos)
<< r.Log;
EXPECT_NE(r.Log.find(kOmittedFatal), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("GetTextureUnitObject@"), std::string::npos) << r.Log;
#endif
}
// An unknown verb in the knob is Fatal{PipeVerifyBadKnob} naming the knob and the value,
// before any stamp is withheld.
TEST_F(PipeInputsTest, BadPoisonOmitKnobIsFatalNamingTheKnob) {
#if !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
const ChildResult r = RunInChild([] {
MG_Config::Features.PipePoisonOmit = "NoSuchVerb:GetActiveTextureUnit";
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
::_exit(3);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{PipeVerifyBadKnob, \"MOBILEGL_PIPE_POISON_OMIT=NoSuchVerb:GetActiveTextureUnit\": "
"no such verb in kMGPipeVerbNames}"),
std::string::npos)
<< r.Log;
EXPECT_EQ(r.Log.find("poison omission armed"), std::string::npos) << r.Log;
#endif
}
// Negative control A at the block level (G4): a clean snapshot compares equal to the pushed
// block over the verb's mask; corrupting one field of the snapshot makes the compare name
// exactly that field, at the serial of the fill it belongs to.
@@ -336,6 +313,78 @@ TEST_F(PipeInputsTest, CorruptedSnapshotFieldIsNamedWithItsSerial) {
#endif
}
// The same control for EVERY field, which is where P13 W3c moved the capability cross-check the
// residual block used to carry: two snapshots of one live context compare equal over all fields,
// and corrupting any one field that has storage makes the compare name exactly that field. The
// seven forwarded fields have no storage, so there is nothing to corrupt and nothing to compare.
TEST_F(PipeInputsTest, EveryCorruptedFieldIsNamedByTheComparator) {
#if !MOBILEGL_PIPE_VERIFY
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
#else
constexpr MGPipeFieldMask kEveryField{{~Uint64{0}, ~Uint64{0}}};
const auto name = [](MGPipeInputField field) {
return field == MGPipeInputField::kFieldCount ? "<none>" : kMGPipeInputFieldNames[static_cast<SizeT>(field)];
};
// A fresh block per snapshot: the snapshot copies only what the context holds (the binding
// points past the touched count keep their storage), so a reused block would carry one
// field's corruption into the next field's compare.
const auto snapshot = [&] {
auto block = MakeUnique<PipeInputs>();
SnapshotFromGLContext(*block, kEveryField);
return block;
};
const auto reference = snapshot();
MGPipeInputField named = MGPipeInputField::kFieldCount;
ASSERT_TRUE(MGPipeVerifyInputs(*reference, *snapshot(), kEveryField, &named))
<< "two snapshots of one context differ at " << name(named);
SizeT corruptedFields = 0;
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
const auto field = static_cast<MGPipeInputField>(f);
const auto corrupted = snapshot();
if (kMGPipeInputFieldSticky[f]) {
EXPECT_FALSE(MGPipeApplyVerifyCorruption(*corrupted, field)) << name(field) << " is forwarded";
continue;
}
ASSERT_TRUE(MGPipeApplyVerifyCorruption(*corrupted, field)) << name(field) << " has no storage to corrupt";
++corruptedFields;
named = MGPipeInputField::kFieldCount;
EXPECT_FALSE(MGPipeVerifyInputs(*reference, *corrupted, kEveryField, &named))
<< "the comparator does not see a corrupted " << name(field);
EXPECT_EQ(named, field) << "a corrupted " << name(field) << " was reported as " << name(named);
}
EXPECT_EQ(corruptedFields, kMGPipeInputFieldCount - kMGPipeForwardedFieldCount);
#if MGTEST_HAVE_FORK
// And the capability field through the knob and each verb's own mask, at the verbs a clear,
// draw and readback frame crosses: every one whose class fills the field must report it at
// entry, and at least one must fill it.
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
static constexpr MGPipeVerb kFrameVerbs[] = {MGPipeVerb::Clear, MGPipeVerb::DrawArrays, MGPipeVerb::ReadPixels};
const ChildResult r = RunInChild([] {
MG_Config::Features.PipeVerify = true;
MG_Config::Features.PipeVerifyFatal = false;
::setenv("MOBILEGL_PIPE_VERIFY_CORRUPT", "IsCapabilityEnabled", 1);
for (const MGPipeVerb verb : kFrameVerbs)
MGPipeValidateForVerb(verb);
});
ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log;
SizeT filling = 0;
for (const MGPipeVerb verb : kFrameVerbs) {
const auto cls = static_cast<SizeT>(kMGPipeVerbClass[static_cast<SizeT>(verb)]);
const Bool fills = MGPipeFieldMaskHas(kMGPipeClassFieldMask[cls], MGPipeInputField::IsCapabilityEnabled);
filling += fills ? 1 : 0;
const std::string report = std::string("Fatal{PipeVerifyDiffer, \"IsCapabilityEnabled@") +
kMGPipeVerbNames[static_cast<SizeT>(verb)] + "\"";
EXPECT_EQ(r.Log.find(report) != std::string::npos, fills)
<< report << (fills ? " missing" : " unexpected") << " in\n"
<< r.Log;
}
EXPECT_GT(filling, 0u) << "no verb of a clear, draw and readback frame fills IsCapabilityEnabled";
#endif
#endif
}
// The compare-at-read arm (P1 brief D8, the arm that is real in P1): a value that changes in
// the live context between the verb boundary and the read is Fatal{PipeVerifyDiffer,
// "Field@Verb", verb=<serial>, where=read}; the same read before the mutation completes.
@@ -370,9 +419,9 @@ TEST_F(PipeInputsTest, MutatedFieldIsNamedAtRead) {
#endif
}
// The MOBILEGL_PIPE_VERIFY_CORRUPT parser through Features: the fill after the child arms it
// logs both arming lines and the entry compare names the corrupted field at that fill's
// serial, where=entry.
// The MOBILEGL_PIPE_VERIFY_CORRUPT knob through the environment, where the lanes set it: the
// fill after the child sets it logs both arming lines and the entry compare names the
// corrupted field at that fill's serial, where=entry.
TEST_F(PipeInputsTest, VerifyCorruptKnobNamesTheFieldAtEntry) {
#if !MOBILEGL_PIPE_VERIFY
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
@@ -384,7 +433,7 @@ TEST_F(PipeInputsTest, VerifyCorruptKnobNamesTheFieldAtEntry) {
const Uint64 serial = gPipeInputs->FilledState().CurrentVerbSerial + 1;
const ChildResult r = RunInChild([] {
MG_Config::Features.PipeVerify = true;
MG_Config::Features.PipeVerifyCorrupt = "GetRenderStateParameters";
::setenv("MOBILEGL_PIPE_VERIFY_CORRUPT", "GetRenderStateParameters", 1);
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
::_exit(3);
});
@@ -409,7 +458,7 @@ TEST_F(PipeInputsTest, BadVerifyCorruptKnobIsFatalNamingTheKnob) {
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
const ChildResult r = RunInChild([] {
MG_Config::Features.PipeVerify = true;
MG_Config::Features.PipeVerifyCorrupt = "NoSuchField";
::setenv("MOBILEGL_PIPE_VERIFY_CORRUPT", "NoSuchField", 1);
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
::_exit(3);
});
@@ -439,7 +488,7 @@ TEST_F(PipeInputsTest, VerifyFatalOffLogsTheDivergenceAndContinues) {
const ChildResult r = RunInChild([] {
MG_Config::Features.PipeVerify = true;
MG_Config::Features.PipeVerifyFatal = false;
MG_Config::Features.PipeVerifyCorrupt = "GetRenderStateParameters";
::setenv("MOBILEGL_PIPE_VERIFY_CORRUPT", "GetRenderStateParameters", 1);
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
MGPipeValidateForVerb(MGPipeVerb::DrawElements);
std::exit(0);
@@ -522,8 +571,7 @@ TEST_F(PipeInputsTest, TheMutationNoticeRefreshesTheValueButNotTheStamp) {
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
#else
auto& ctx = *MG_State::pGLContext;
MGPipeSetPoisonOmission("DrawArrays", "GetSamplingResolutionGeneration");
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
FillWithholdingTheStamp(MGPipeVerb::DrawArrays, MGPipeInputField::GetSamplingResolutionGeneration);
ASSERT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration));
ctx.BumpSamplingResolutionGeneration();
EXPECT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration))
@@ -531,7 +579,6 @@ TEST_F(PipeInputsTest, TheMutationNoticeRefreshesTheValueButNotTheStamp) {
// FenceSync is a kQuery verb: its mask holds no texture field at all, so the notice must
// leave the generation unfilled and a read of it Fatal{UnmigratedPipeInput}.
MGPipeSetPoisonOmission(nullptr, nullptr);
MGPipeValidateForVerb(MGPipeVerb::FenceSync);
ASSERT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration));
ctx.BumpSamplingResolutionGeneration();
+3 -3
View File
@@ -543,7 +543,7 @@ namespace {
static void Clear() {
MGPipeSamplerEmitterInstance().Reset();
MGPipeSamplerEmitterInstance().ResetCounters();
MGPipeSamplerCsoCacheInstance().ResetForTest();
MGPipeSamplerCsoCacheInstance().ForgetSession();
MGPipeSamplerCsoCacheInstance().ResetCounters();
MGPipeSetHashSuppressorInstance().InvalidateAll();
}
@@ -729,7 +729,7 @@ namespace {
// The ordinary path, for completeness: two states that differ in ONE field never share
// a handle and never reach the confirm at all.
Cache().ResetForTest();
Cache().ForgetSession();
Cache().ResetCounters();
const MGPipeHandle plainFirst = Cache().Acquire(first, payload);
const MGPipeHandle plainSecond = Cache().Acquire(second, payload);
@@ -1193,7 +1193,7 @@ namespace {
// Nothing produced the bit before the fix round.
TEST(SamplerEmit, AResolvedSamplerViewMarksItsTextureAsSamplerBound) {
EmitterScope scope;
MGPipeTextureEmitterInstance().ResetForTest();
MGPipeTextureEmitterInstance().ForgetSession();
namespace GL = MobileGL::MG_Impl::GLImpl;
const Uint program = MakeSamplerProgram();
GLint linked = 0;
+2 -2
View File
@@ -233,7 +233,7 @@ namespace {
kMGPipeSubsystemResources | kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers;
m_previousContext = MG_State::pGLContext.Take();
MG_State::pGLContext = MakeUnique<GLContext>();
MGPipeTextureEmitterInstance().ResetForTest();
MGPipeTextureEmitterInstance().ForgetSession();
// AND THE APPLIER IS A PROCESS SINGLETON, so its object records outlive a case. With
// kMGPipeWiredTextureSubsystem flipped the emitter's calls actually LAND, and a case
// that inherited the previous one's records would assert against state it did not
@@ -245,7 +245,7 @@ namespace {
MGPipeApplierReleaseObjectRecords();
}
~TextureScope() {
MGPipeTextureEmitterInstance().ResetForTest();
MGPipeTextureEmitterInstance().ForgetSession();
MGPipeApplierReset();
MGPipeApplierReleaseObjectRecords();
MG_State::pGLContext.reset();
-3
View File
@@ -4326,12 +4326,10 @@ TEST(DirectGLESBufferDrawProbe, UnderSplitTheRecordAloneAnswersTheLiveHostMapQue
// case reaches is optional.
const Uint64 previousPush = MG_Config::Features.PipePush;
const auto previousTransport = MG_Config::Transport;
const Uint32 previousBlockKb = MG_Config::RecordArm.PersistentBlockKb;
MG_Pipe::MGPipeResourceOps ops{};
MG_Config::Features.PipePush |= MG_Pipe::kMGPipeSubsystemResources;
MG_Pipe::MGPipeSetResourceOps(&ops);
MG_Config::Transport = MG_Config::TransportMode::InProcess;
MG_Config::RecordArm.PersistentBlockKb = 64;
// P5 c1 / R-8: UNDER SPLIT THE OP TABLE IS NO LONGER THE ARMING CONDITION, and this case is
// the first place that shows. `MGPipeSetResourceOps(&ops)` is the SERVER's registration; the
// client's liveness gate now reads the caps mirror's consumer mask instead, because under a
@@ -4424,7 +4422,6 @@ TEST(DirectGLESBufferDrawProbe, UnderSplitTheRecordAloneAnswersTheLiveHostMapQue
BufferImpl::g_backendBufferResources.ReleaseByHandle(res);
}
MG_Config::RecordArm.PersistentBlockKb = previousBlockKb;
MG_Config::Transport = previousTransport;
MG_Pipe::MGPipeSetResourceOps(nullptr);
MG_Config::Features.PipePush = previousPush;
+5
View File
@@ -306,6 +306,9 @@ namespace {
EXPECT_NE(line.find(" wlivepk="), String::npos) << line;
EXPECT_NE(line.find(" wdefpk="), String::npos) << line;
EXPECT_NE(line.find(" wdefsync="), String::npos) << line;
// The server's control-record counts, which CtWireScenario reads by these names.
EXPECT_NE(line.find(" ar="), String::npos) << line;
EXPECT_NE(line.find(" od="), String::npos) << line;
}
// Per-frame fields carry two decimals for the same reason acc/draw does: they are small
@@ -544,6 +547,8 @@ namespace {
// MEASUREMENTS.md entry will quote.
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageSegmentBytes), "stage-segment-bytes");
EXPECT_STREQ(PS::NameOf(PS::CallClass::WireRecords), "wire-records");
EXPECT_STREQ(PS::NameOf(PS::CallClass::ApplierResets), "applier-resets");
EXPECT_STREQ(PS::NameOf(PS::CallClass::ObjectDeaths), "object-deaths");
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytRenderState), "espryt-render-state");
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytTextureSyncList), "espryt-texture-sync-list");
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytUnitBindingsEpoch), "espryt-unit-bindings-epoch");
@@ -54,7 +54,6 @@ namespace {
::alarm(10);
MG_Config::Transport = MG_Config::TransportMode::Spawn;
MG_Config::Ipc.RunAhead = 1;
MG_Config::Ipc.VerbBarrier = 1;
int reserve = ::socket(AF_INET, SOCK_STREAM, 0);
ASSERT_GE(reserve, 0);
sockaddr_in address{};
+23 -10
View File
@@ -23,6 +23,7 @@
#include <gtest/gtest.h>
#include <MG_Util/Debug/Log.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <cstdio>
#include <cstdlib>
@@ -3290,21 +3291,27 @@ TEST(ApplierResetWireRoundTrip, TheSerialSequenceIsAssertedAndTheServerResetRuns
EXPECT_TRUE(applied);
};
// The reset RUNS - asserted on the applier's own state, not on the tally (R-16: a probe
// may not arm against a stub, and a tally a stub could also move is a stub's witness).
// BoundVertexElements is one of the fields MGPipeApplierReset clears (PipeApply.cpp).
// The reset runs - asserted on the applier's own state: BoundVertexElements is one of the
// fields MGPipeApplierReset clears (PipeApply.cpp). The accepted count is the sink's
// applier-resets PipeStats counter.
namespace PS = MG_Util::PipeStats;
PS::SetEnabledForTesting(true);
PS::ResetForTesting();
MGPipeApplier().BoundVertexElements = MakeHandle(77);
send(0); // 0 = the first make-current (CONTRACT-P5C.md §1)
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundVertexElements));
EXPECT_EQ(sink.ApplierResets(), 1u);
EXPECT_EQ(sink.ExpectedApplierResetSerial(), 1u);
EXPECT_EQ(PS::TotalCalls(PS::CallClass::ApplierResets), 1u);
// The second edge carries the next serial and is accepted in order.
// Each edge carries the next serial and is accepted in order; a serial the session does not
// expect aborts (CtWireFatals below), so accepting 1 and then 2 shows the expected one moved.
MGPipeApplier().BoundVertexElements = MakeHandle(78);
send(1);
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundVertexElements));
EXPECT_EQ(sink.ApplierResets(), 2u);
EXPECT_EQ(sink.ExpectedApplierResetSerial(), 2u);
EXPECT_EQ(PS::TotalCalls(PS::CallClass::ApplierResets), 2u);
send(2);
EXPECT_EQ(PS::TotalCalls(PS::CallClass::ApplierResets), 3u);
PS::ResetForTesting();
PS::SetEnabledForTesting(false);
}
TEST(ObjectDeathWireRoundTrip, TheTextureAndFramebufferTwinsReleaseByHandleAndNeverByAStaleGeneration) {
@@ -3323,6 +3330,12 @@ TEST(ObjectDeathWireRoundTrip, TheTextureAndFramebufferTwinsReleaseByHandleAndNe
Uint64 bits;
~RestorePush() { MG_Config::Features.PipePush = bits; }
} restore{savedPush};
// The deaths the sink dispatched are its object-deaths PipeStats counter.
namespace PS = MG_Util::PipeStats;
PS::SetEnabledForTesting(true);
struct StatsOff {
~StatsOff() { PS::SetEnabledForTesting(false); }
} statsOff;
// Texture AND Framebuffer - the two kinds R-16 names for the death-recycle path. The
// others share the one dispatch and the one table walk, so two kinds pin the shape: one
@@ -3352,14 +3365,14 @@ TEST(ObjectDeathWireRoundTrip, TheTextureAndFramebufferTwinsReleaseByHandleAndNe
ASSERT_EQ(liveGen(one.slot), 1u) << static_cast<Uint32>(one.kind);
// The death crosses and the twin retires: the slot's live generation goes back to 0.
const Uint64 deathsBefore = sink.ObjectDeaths();
const Uint64 deathsBefore = PS::TotalCalls(PS::CallClass::ObjectDeaths);
const MGPHandleOnly death{MakeHandle(one.slot, 1), static_cast<Uint32>(one.kind), 0};
EXPECT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ObjectDeath, &death, sizeof(death)), kInvalidSeq);
bool applied = false;
EXPECT_TRUE(wire.PumpOne(&applied));
EXPECT_TRUE(applied);
EXPECT_EQ(liveGen(one.slot), 0u) << static_cast<Uint32>(one.kind);
EXPECT_EQ(sink.ObjectDeaths(), deathsBefore + 1u);
EXPECT_EQ(PS::TotalCalls(PS::CallClass::ObjectDeaths), deathsBefore + 1u);
// THE ABA HALF (LiveGenAt's semantics): the slot is recycled forward to a NEW object
// at generation 2, and the dead object's handle - replayed, as a duplicated or
@@ -35,12 +35,15 @@ struct E1BoundaryPeer : Codec::WireVerbSink {
}
};
void CheckE1Boundary(Bool serverCap, MGPWireOp op, Bool expectWait, const char* diagnostic) {
// How the probe's record leaves the client. Rule is EmitAndWait, whose wait rule the cases
// test. PublishOnly encodes through the session's own encoder and publishes with no wait at all,
// so a case can show the probe reads a missing wait as one.
enum class E1Emit { Rule, PublishOnly };
void CheckE1Boundary(Bool serverCap, MGPWireOp op, Bool expectWait, const char* diagnostic,
E1Emit emit = E1Emit::Rule) {
const auto child = RunInChild([=] {
::alarm(15);
const char* barrier = std::getenv("MOBILEGL_IPC_VERB_BARRIER");
if (barrier && std::strcmp(barrier, "0") && std::strcmp(barrier, "1")) ::_exit(80);
MG_Config::Ipc.VerbBarrier = barrier ? static_cast<Uint32>(barrier[0] - '0') : 1;
MG_Config::Ipc.BatchWaits = 1;
MG_Config::Ipc.RunAhead = 1;
MG_Config::Ipc.SpinUs = 0;
@@ -59,10 +62,17 @@ void CheckE1Boundary(Bool serverCap, MGPWireOp op, Bool expectWait, const char*
producer.*PeerMember(ProducerBellTag{}) = &bell;
const MGPApplierReset reset{};
const MGPClear clear{};
const void* payload = op == MGPWireOp::ApplierReset ? static_cast<const void*>(&reset) : &clear;
const Uint64 payloadBytes = op == MGPWireOp::ApplierReset ? sizeof(reset) : sizeof(clear);
// P15: applier_reset is the one kWaitApplied row left (generate_mipmap went async).
const Uint64 seq = op == MGPWireOp::ApplierReset
? session.EmitAndWait(op, &reset, sizeof(reset), nullptr, 0, nullptr, 0, nullptr)
: session.EmitAndWait(op, &clear, sizeof(clear), nullptr, 0, nullptr, 0, nullptr);
Uint64 seq = Codec::kInvalidSeq;
if (emit == E1Emit::PublishOnly) {
seq = session.Encoder().EncodeRecord(op, payload, payloadBytes);
if (seq == Codec::kInvalidSeq) ::_exit(87);
producer.PublishAndNotify(seq);
} else {
seq = session.EmitAndWait(op, payload, payloadBytes, nullptr, 0, nullptr, 0, nullptr);
}
// Observe BEFORE allowing the peer to complete a non-waiting emission.
const Bool parked = bell.entered.load();
const Bool appliedBeforeReturn = session.Control()->Progress.appliedSeq.load() >= seq;
@@ -71,9 +81,9 @@ void CheckE1Boundary(Bool serverCap, MGPWireOp op, Bool expectWait, const char*
if (session.WaitForApplied(seq, 2000) != Transport::SessionWait::Reached) ::_exit(85);
if (seq <= before || peer.seen.load() != 1 || session.Control()->Progress.appliedSeq.load() < seq) ::_exit(86);
const Bool correct = expectWait ? parked && appliedBeforeReturn : !parked && !appliedBeforeReturn;
std::printf("E1 observation: op=%s cap=%u barrier=%u parked=%u applied_before_return=%u emitted=1 peer_seen=1\n",
std::printf("E1 observation: op=%s cap=%u emit=%s parked=%u applied_before_return=%u emitted=1 peer_seen=1\n",
op == MGPWireOp::ApplierReset ? "ApplierReset" : "Clear",
static_cast<unsigned>(serverCap), MG_Config::Ipc.VerbBarrier,
static_cast<unsigned>(serverCap), emit == E1Emit::PublishOnly ? "publish-only" : "rule",
static_cast<unsigned>(parked), static_cast<unsigned>(appliedBeforeReturn));
std::fflush(stdout);
session.Stop();
@@ -97,6 +107,19 @@ TEST(RemoteWaitBoundaryControl, AppliedClassWaitsWithRunAheadCap) {
#endif
}
// The control for the case above: the same ApplierReset, published with no wait. The probe has
// to read it as not parked, or a wait that never happened would pass the cases that must wait.
TEST(RemoteWaitBoundaryControl, AnUnwaitedPublishIsSeenAsNoWait) {
#if MGTEST_HAVE_FORK
ASSERT_EQ(MGPipeWaitClassFor(MGPWireOp::ApplierReset), kWaitApplied);
CheckE1Boundary(true, MGPWireOp::ApplierReset, false,
"E1 control: an ApplierReset published without a wait was observed as a wait",
E1Emit::PublishOnly);
#else
GTEST_SKIP() << "E1 requires POSIX child isolation";
#endif
}
TEST(RemoteWaitBoundaryControl, MissingServerCapKeepsClearLockstep) {
#if MGTEST_HAVE_FORK
ASSERT_EQ(MGPipeWaitClassFor(MGPWireOp::Clear), kWaitNone);
@@ -2730,29 +2730,6 @@ TEST(RemoteRunAhead, TheKnobAloneDoesNotArmRunAheadWithoutTheServersCapBit) {
ExpectChildSuccess(child);
}
// §1 again, from the other side: MOBILEGL_IPC_VERB_BARRIER=0 is the LOCKSTEP arm's negative
// control and it disarms run-ahead outright, because the barrier is the first term of the
// conjunction. Under run-ahead its documented red is the first barriered row's stale pull -
// that one is a scenario, not a unit case; what is pinned here is that the two controls do not
// silently compose into a third mode nobody designed.
TEST(RemoteRunAhead, ClearingTheVerbBarrierDisarmsRunAheadEvenWithTheCapBit) {
const auto child = RunInChild([] {
MG_Config::Ipc.RunAhead = 1;
MG_Config::Ipc.VerbBarrier = 0;
::alarm(15);
MG_Config::Transport = MG_Config::TransportMode::InProcess;
Srv::ServerSessionInstance().SetCapabilityBits(static_cast<Uint64>(MG_Pipe::kCapRunAheadApply));
Srv::ServerSessionInstance().SetConsumedSubsystems(kMGPipeSubsystemsMigratedAtP4a);
Srv::ServerSessionInstance().SetBackend(ControlCapsPeer());
if (ClientSessionInstance().Start(MG_Config::TransportMode::InProcess, {}) != MOBILEGL_OK) {
::_exit(81);
}
if (ClientSessionInstance().RunAheadArmed()) ::_exit(101);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// §2.4's RED-ONCE, BY COUNT. With a credit of 1 the client may have one present in flight, so
// the second and third presents each pay the credit before they encode - `credit-waits` is 2
// after three presents and the present-ack watermark has reached 2. Delete the
+2 -3
View File
@@ -1244,9 +1244,8 @@ TEST(SessionTest, ABorrowedSlotIsNotHandedBackUntilItIsReleased) {
// q-1. Teardown's drain keys on the PRODUCER's own last-published seq, not on
// RingControl::submittedSeq: Ring.h's header comment permits that watermark to be published lazily, so a
// drain that trusted it would under-wait and free an emitter-owned var-tail while a record
// still names it. With the verb barrier armed the two are equal; under
// MOBILEGL_IPC_VERB_BARRIER=0 - R-1's negative control, which the phase must run once - they
// are not.
// still names it. Run-ahead and the batched waits publish without waiting, so at teardown the
// two may differ.
TEST(SessionTest, TheProducerRemembersWhatItPublishedEvenIfTheWatermarkLags) {
SessionFixture session;
ASSERT_TRUE(session.Build(TestSizes()));
@@ -57,7 +57,6 @@ namespace {
::alarm(5);
MG_Config::Transport = MG_Config::TransportMode::Spawn;
MG_Config::Ipc.RunAhead = 1;
MG_Config::Ipc.VerbBarrier = 1;
MG_Config::Ipc.BatchWaits = 1;
MG_Config::Ipc.PresentCredit = 3;
Transport::SessionSegmentSizes sizes;
+9 -2
View File
@@ -79,8 +79,8 @@
// per sub-draw), so this class is Espryt-only by construction.
// persistent-map-push WIRED IN P5 (b1), and only a split build can ever move it. The one
// site is BufferObject::PushMappedSpanBlock, i.e. the client shipping
// one MOBILEGL_IPC_PERSISTENT_BLOCK_KB block of a persistently mapped
// span because MGPipeApplyMapPersistent declined the adoption (R-6,
// one 64 KiB block of a persistently mapped span because
// MGPipeApplyMapPersistent declined the adoption (R-6,
// tier T2). Zero in every monolith build, and that zero is CORRECT
// rather than missing: a persistent map there is a permanent address
// space donation (D4/D-B4), the application writes straight into GPU
@@ -131,6 +131,8 @@
// therefore a LOWER BOUND on the per-draw accessor count, and it is
// the bound over exactly the six gates section 2.3.1 tabulates.
// texture-* Per (target, level) emission, both backends.
// applier-resets ServerVerbSink::OnApplierReset, once per record accepted.
// object-deaths ServerVerbSink::OnObjectDeath, once per record dispatched.
//
// Gates: the six of section 2.3.1, each counted exactly once per probe.
//
@@ -252,6 +254,8 @@ namespace MobileGL::MG_Util::PipeStats {
"wire-host-wait-us",
"wire-indirect-native-dispatches",
"resource-readbacks",
"applier-resets",
"object-deaths",
};
const char* const kGateNames[kGateCount] = {
"espryt-render-state", "espryt-texture-sync-list", "espryt-unit-bindings-epoch",
@@ -658,6 +662,9 @@ namespace MobileGL::MG_Util::PipeStats {
// never ran. It is deliberately NOT rsp: rsp reaches zero when the phase SUCCEEDS, so a
// positive control built on it would fail on the day the debt is paid.
line += " vbs=" + std::to_string(calls[static_cast<Uint32>(CallClass::ServerVerbBoundaries)]);
// The control records the server applied in this window (see the CallClass enum).
line += " ar=" + std::to_string(calls[static_cast<Uint32>(CallClass::ApplierResets)]);
line += " od=" + std::to_string(calls[static_cast<Uint32>(CallClass::ObjectDeaths)]);
// P6 GATE 8's SECOND NUMBER (CONTRACT-P6.md §9 item 8): records/frame AFTER chunking, i.e.
// the count of records the wire encoder actually committed this window rather than the
// number of times an emitter was asked for one. The gap between the two is the whole
+6
View File
@@ -292,6 +292,12 @@ namespace MobileGL::MG_Util::PipeStats {
// GPU write - before even asking whether the draw had a client-memory array to snapshot.
// Not on the summary line; read through TotalCalls by the scenario that pins it.
ResourceReadbacks,
// `ar` / `od` - the two control records, counted where the server applies them
// (ServerVerbSink::OnApplierReset after its serial check, ServerVerbSink::OnObjectDeath per
// record dispatched). A framebuffer has no other wire delete, so `od` is the only sign on
// the server that its death crossed. Server-side counts: a separate client process prints 0.
ApplierResets,
ObjectDeaths,
Count
};
@@ -288,19 +288,6 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
static_cast<unsigned long long>(stats.duplicateInserts), EntryCount(), StoredBytes() / 1024u);
}
// Tests only: makes the caps small enough to exercise eviction without
// building megabytes of shaders. Clears the cache, because shrinking the
// caps under live entries would otherwise leave it over budget.
void SetCapsForTesting(SizeT maxEntries, SizeT maxBytes) {
const std::lock_guard<std::mutex> lock(m_mutex);
m_maxEntries = maxEntries;
m_maxBytes = maxBytes;
m_index.clear();
m_entries.clear();
m_storedBytes = 0;
m_stats = {};
}
private:
struct Entry {
TranslationCacheKey key;
@@ -160,7 +160,6 @@ void ApplyEnvOverrides(const std::vector<std::string>& entries) {
bool LoadMobileGL(const Request& request, std::string& error) {
setenv("MOBILEGL_BACKEND_TYPE", request.backend.c_str(), 1);
setenv("MOBILEGL_TRACE_LIBRARY", request.mobileGlLibrary.c_str(), 1);
setenv("MOBILEGL_TRACE_SKIP_AUTODESTROY", "1", 1);
// Retrace is a test lane on every platform, including the Android AVD one where
// MobileGL's __ANDROID__ default would leave validation off. No overwrite: an outer
// MOBILEGL_VALIDATE_SPIRV=0 must keep working as the escape hatch, and retracing the
+74 -31
View File
@@ -18,6 +18,13 @@ Usage:
--pid-file FILE \
--boot-timeout SECONDS
sh android-plugin/run-avd-ci.sh preboot \
--avd-name NAME \
--gpu GPU \
--emulator-log FILE \
--pid-file FILE \
--boot-timeout SECONDS
sh android-plugin/run-avd-ci.sh stop \
--avd-name NAME \
--emulator-log FILE \
@@ -118,35 +125,8 @@ create_avd() {
echo no | "${avdmanager_bin}" create avd --force --name "${avd_name}" --package "${system_image}" --device "${profile}"
}
start_avd() {
avd_name=""
gpu=""
emulator_log=""
pid_file=""
boot_timeout=""
while [ "$#" -gt 0 ]; do
case "$1" in
--avd-name) avd_name="$(next_arg "$@")"; shift 2 ;;
--gpu) gpu="$(next_arg "$@")"; shift 2 ;;
--emulator-log) emulator_log="$(next_arg "$@")"; shift 2 ;;
--pid-file) pid_file="$(next_arg "$@")"; shift 2 ;;
--boot-timeout) boot_timeout="$(next_arg "$@")"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) die "unknown start argument: $1" ;;
esac
done
require_value "${avd_name}" "--avd-name"
require_value "${gpu}" "--gpu"
require_value "${emulator_log}" "--emulator-log"
require_value "${pid_file}" "--pid-file"
require_value "${boot_timeout}" "--boot-timeout"
adb_bin="$(find_android_tool adb platform-tools/adb)"
emulator_bin="$(find_android_tool emulator emulator/emulator)"
ensure_android_avd_home
# The retrace jobs and `preboot` launch the emulator the same way, through here.
launch_emulator() {
mkdir -p "$(dirname "${emulator_log}")" "$(dirname "${pid_file}")"
"${emulator_bin}" -avd "${avd_name}" \
-no-window \
@@ -157,9 +137,10 @@ start_avd() {
-camera-back none \
-camera-front none \
> "${emulator_log}" 2>&1 &
emulator_pid="$!"
echo "${emulator_pid}" > "${pid_file}"
echo "$!" > "${pid_file}"
}
wait_for_boot() {
if ! timeout "${boot_timeout}" "${adb_bin}" wait-for-device; then
print_diagnostics "${emulator_log}"
die "emulator did not connect to adb within ${boot_timeout}s"
@@ -185,6 +166,67 @@ start_avd() {
"${adb_bin}" shell input keyevent 82 || true
}
parse_boot_args() {
avd_name=""
gpu=""
emulator_log=""
pid_file=""
boot_timeout=""
while [ "$#" -gt 0 ]; do
case "$1" in
--avd-name) avd_name="$(next_arg "$@")"; shift 2 ;;
--gpu) gpu="$(next_arg "$@")"; shift 2 ;;
--emulator-log) emulator_log="$(next_arg "$@")"; shift 2 ;;
--pid-file) pid_file="$(next_arg "$@")"; shift 2 ;;
--boot-timeout) boot_timeout="$(next_arg "$@")"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) die "unknown argument: $1" ;;
esac
done
require_value "${avd_name}" "--avd-name"
require_value "${gpu}" "--gpu"
require_value "${emulator_log}" "--emulator-log"
require_value "${pid_file}" "--pid-file"
require_value "${boot_timeout}" "--boot-timeout"
adb_bin="$(find_android_tool adb platform-tools/adb)"
emulator_bin="$(find_android_tool emulator emulator/emulator)"
ensure_android_avd_home
}
start_avd() {
parse_boot_args "$@"
started_at="$(date +%s)"
launch_emulator
wait_for_boot
echo "run-avd-ci.sh: ${gpu} cold boot took $(( $(date +%s) - started_at ))s"
}
# Boot a freshly created AVD once and power it off, so the cached AVD is past its first-boot setup
# and every retrace job's cold boot skips it. The guest powers off itself, so its data is flushed
# before the emulator exits.
preboot_avd() {
parse_boot_args "$@"
started_at="$(date +%s)"
launch_emulator
wait_for_boot
echo "run-avd-ci.sh: first boot took $(( $(date +%s) - started_at ))s"
"${adb_bin}" shell reboot -p >/dev/null 2>&1 || true
emulator_pid="$(cat "${pid_file}")"
for _ in $(seq 1 120); do
kill -0 "${emulator_pid}" 2>/dev/null || break
sleep 1
done
if kill -0 "${emulator_pid}" 2>/dev/null; then
"${adb_bin}" emu kill >/dev/null 2>&1 || true
kill "${emulator_pid}" >/dev/null 2>&1 || true
die "emulator did not exit within 120s of the guest power-off"
fi
}
stop_avd() {
avd_name=""
emulator_log=""
@@ -226,6 +268,7 @@ shift
case "${command}" in
create) create_avd "$@" ;;
start) start_avd "$@" ;;
preboot) preboot_avd "$@" ;;
stop) stop_avd "$@" ;;
-h|--help) usage; exit 0 ;;
*) die "unknown command: ${command}" ;;
+1 -1
View File
@@ -27,4 +27,4 @@ P0 已回答的不再列出(spike A 的域、spike B 的分档、`posix_spawn`
21. **配对 / 认证的形式。** **已裁定(ID-P7-3,2026-09-22):令牌足够,不做 TLS。** 威胁模型是「任何人都能连的远程代码路径」而不是窃听:令牌常量时间比较、≥16 字节、无令牌只 loopback、数据面以 `Welcome.dataNonce` 绑定到已认证的控制连接、fork 前认证。TLS 作为 P12 的开放项保留。
22. **client 平台范围。** 默认先 Linux + Android(现有 CI 能验),Windows client 第二批(`ShmSegmentWin32` 在;`pipe:` 具名拒绝作废、TCP 替代),macOS 仍不拆分。这决定 `ARCHITECTURE.md` §15.3 "Windows 机器不是正确性门"要不要改。
23. **半开连接的检测。** Wi-Fi 掉线不产生挂断事件;dl 的闩取自描述符挂断,TCP 上需要 keepalive / 心跳超时并**由传输层报告为挂断**——这与"绝不取自 apply 超时"不矛盾,写清楚免得重吵。P6.5 ct。
24. **E1 对照的重定义(ID-122)。** **已关闭(2026-09-29 债务审查)**:`10bfd29f` 把 E1 的像素竞速换成观测协议等待的负控 `scripts/ci/wait_boundary_negative_control.py`,由 `split_negative_controls.sh:174` 调用。
24. **E1 对照的重定义(ID-122)。** **已关闭(2026-09-29 债务审查)**:`10bfd29f` 把 E1 的像素竞速换成观测协议等待的负控 `scripts/ci/wait_boundary_negative_control.py`。2026-10-18 该负控退役:探针看得见缺失的等待,改由常驻单元用例 `RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait` 证明(同一个 `ApplierReset`,发布后不等待,必须读到 parked=0)。
@@ -12,7 +12,7 @@
| T1 | server 导出 opaque fd | 仅 Adreno 的 Vulkan 路径 |
| T2 | 拒绝,client 侧推送 | 永久正确回退;**今天 split 用的档**(`MOBILEGL_IPC_ADOPT_TIER=2`),stream 数据面上强制(P11 A1:两端握手时 `Transport::SettleAdoptTierAtHandshake` 对 0/1 打一行 `Refuse{AdoptTierOnStream}` 并走 T2;共享段上 client 握手期 `Fatal{UnimplementedAdoptTier}`,[`CONTRACT-P11.md`](../../archive/contracts/CONTRACT-P11.md) §1) |
T2 下 client 侧推送三件套:不做 map/unmap 命令对(payload 带 `hasLiveHostWrites`);在每个 validate 点按块(`MOBILEGL_IPC_PERSISTENT_BLOCK_KB`,默认 64)推送可达的已映射 buffer,脏页追踪 + 哈希抑制只发变化的块(P5d);门 `PersistentCoherentMapScenario`。`mpr` 数的是每一次 `MapPersistent` 发射(铸成或拒绝都算),所以在 monolith 下也可断言。
T2 下 client 侧推送三件套:不做 map/unmap 命令对(payload 带 `hasLiveHostWrites`);在每个 validate 点按 64 KiB 块推送可达的已映射 buffer,脏页追踪 + 哈希抑制只发变化的块(P5d);门 `PersistentCoherentMapScenario`。`mpr` 数的是每一次 `MapPersistent` 发射(铸成或拒绝都算),所以在 monolith 下也可断言。
### 12.1 采纳档的握手期结算
@@ -47,7 +47,7 @@ CMake:`MOBILEGL_BUILD_DISAGGREGATED`(OFF)、`MOBILEGL_BUILD_DISAGGREGATED_
| 变量 | 默认 | 说明 |
|---|---|---|
| `MOBILEGL_PIPE_PUSH` | `0x3fff` | 子系统位图(`MG_Pipe/MGPipe.h`,位永不复用);P13 起位 0–13 固定开,清任何一位都在启动时具名拒绝;只剩位 63("关 CSO 内容寻址"的行为对照)可设 |
| `MOBILEGL_PIPE_VERIFY` / `_VERIFY_FATAL` / `_VERIFY_CORRUPT` / `_POISON_OMIT` | 0 / 1 / 空 / 空 | 影子比对与两个阴性对照(verify 构建才有) |
| `MOBILEGL_PIPE_VERIFY` / `_VERIFY_FATAL` / `_VERIFY_CORRUPT` | 0 / 1 / 空 | 影子比对与它的阴性对照。比对器只编进 verify 构建,其他构建里 `MOBILEGL_PIPE_VERIFY=1` 只告警;`_VERIFY_CORRUPT` 不进 `Config.h`,只有 verify 构建在比对器武装时读它(`PipeFill.cpp` 的 `ArmVerify`,同样经环境与系统配置文件)。poison 的阴性对照没有运行时开关:集成测试的 harness 标记 `MGITEST_POISON_OMIT` 由重新 exec 的子进程经 verify 构建才有的 `MGPipeSetPoisonOmission` 武装(`Harness/PoisonOmissionArm.h`) |
| `MOBILEGL_PIPE_STATS` / `_STATS_PERIOD` / `_STATS_FILE` | 0 / 120 / 空 | 边界计数器(附 B);dump 按角色写 `<base>.client.json` / `<base>.server.json` |
| `MOBILEGL_PIPE_INDEX_MIRROR_MB` | 64 | 索引镜像预算(P8);`_LEGACY_MEMOS`、`_TEXEL_RETAIN_MB`、`_HANDLE_ABA_CONTROL` 随各自的臂在 P13 删除 |
@@ -64,14 +64,13 @@ CMake:`MOBILEGL_BUILD_DISAGGREGATED`(OFF)、`MOBILEGL_BUILD_DISAGGREGATED_
| `MOBILEGL_IPC_SURFACE` | `offscreen` | `server` = 使用 server 自有窗口,client 无头(P12) |
| `MOBILEGL_IPC_RING_MB` / `MOBILEGL_IPC_STAGE_MB` | 8 / 32 | `SEG_CMD` / `SEG_STAGE`;stage 的 1/4 是内容分块预算 |
| `MOBILEGL_IPC_RUN_AHEAD` | 1 | P5e run-ahead;只是合取式的一半(server 须发布 `kCapRunAheadApply`);`0` 是 A/B 对照 |
| `MOBILEGL_IPC_VERB_BARRIER` | 1 | `0` 只作阴性对照,且同时关掉 run-ahead |
| `MOBILEGL_FRAMES_IN_FLIGHT` | 3 | 1..64,两个后端共用(旧名 `MOBILEGL_MAGMA_FRAMESINFLIGHT` 为弃用别名):Magma 的帧上下文数;Espryt 在 present S 后等待帧 S+1-N 完成;split 的 present credit 由它导出为 max(1, N-2)(P15) |
| `MOBILEGL_IPC_PRESENT_CREDIT` | 由 `MOBILEGL_FRAMES_IN_FLIGHT` 导出(默认 1) | 1..8,§14;显式设置覆盖导出值,仅供实验 |
| `MOBILEGL_IPC_POLL_ESCALATE` | 64 | 未报告 fence 的零超时轮询本地答"未完成",同一 fence 连续第 N 次才往返(P10,`CONTRACT-P10.md` §1);`0` = 每次轮询都往返,是 A/B 对照 |
| `MOBILEGL_IPC_BATCH_WAITS` | 1 | lockstep 下值类记录发布即返回;verify 强制 0 |
| `MOBILEGL_IPC_BATCH_WAITS` | 1 | lockstep 下值类记录发布即返回;verify 构建里 `MOBILEGL_PIPE_VERIFY=1` 强制 0(`RUN_AHEAD` 同样强制 0,`ROLE_SPLIT_STATE` 强制关;其他构建不改它们) |
| `MOBILEGL_IPC_EVENT_WAIT_MS` | 2000 | 反向事件遇满环时 server 等 client 排空的整笔预算(§11.7) |
| `MOBILEGL_IPC_WIRE_DEFERRED_MB` | 64 | Magma wire 臂延迟回收的水位线;`0` 是 M2 阴性对照 |
| `MOBILEGL_IPC_PERSISTENT_BLOCK_KB` / `_PERSISTENT_HASH_SUPPRESS` | 64 / 1 | persistent-map 推送粒度 / 只推变化的块;`0` 是对照 |
| `MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS` | 1 | persistent-map 推送只推变化的块(粒度固定 64 KiB,`PersistentMapTracker::BlockBytes()`);`0` 是整段推送的 A/B 对照 |
| `MOBILEGL_IPC_ADOPT_TIER` | 未设 = T0 | 未设(用户 2026-09-29 定为缺省)= 能用就 T0,不能用(stream、无 AHB、POST 失败、server 不允许)安静回退 T2:只 MGLOG_D、照样计数;`0` = 点名要 T0,回退有具名行(A1 的 `Refuse{AdoptTierOnStream}` 等);`1` = T1 已关,具名拒绝后 T2;`2` = T2(P11 前的缺省)。`CONTRACT-P11.md` B2 |
| `MOBILEGL_IPC_ALLOW_ADOPT_T0` | 1 | server 自有:`0` = 永不批准 T0(会话走 T2),经 `ServerSpawn` 保留 |
| `MOBILEGL_IPC_STRICT_ERRORS` / `MOBILEGL_IPC_AUDIT` / `MOBILEGL_IPC_ROLE_SPLIT_STATE` | 0 / 0 / 1 | 残余输入读升级为 Fatal / 退休 staging 填 `0xDD` / 双块(P5f;P15 起默认开,`0` 关闭,阴性对照显式设 0;双块下 barriered fill 写 client 块,不再 quiesce applier) |
@@ -83,4 +82,4 @@ CMake:`MOBILEGL_BUILD_DISAGGREGATED`(OFF)、`MOBILEGL_BUILD_DISAGGREGATED_
## 附 B:边界计数器(`MobileGL/MG_Util/Metrics/PipeStats.h`)
关闭时每站点一次全局 load + 一条永不命中的分支。字节类(stage buffer / texture / UBO / client 数组、`pmap`、`resid`、`csob-blob`、`seg` = 写进 `SEG_STAGE` 的字节)、调用类(`draws`、`accessor-calls`、纹理上传 emit / box / rect / jobs、`csom` / `csob`、`mpr`、`trp`、`rsp`、`wrec`、`maxrec` / `ringwraps` / `ringpads` / `ringwaits`、门铃 `srv` / `srvpark` / `cli` / `clipark`)与六个 memo 门的 hit/miss。每 `MOBILEGL_PIPE_STATS_PERIOD` 帧一条 `MGPipe stats:` 汇总行,按角色写日志。读法陷阱:`accessor-calls` 是静态下界,判性能只看 CPU 时间;spawn 下 client 不推进帧窗口(`frames=0`),窗口化字段在 spawn client 上无效,run total 仍有效([`docs/archive/notes/p6/README.md`](../../archive/notes/p6/README.md) §12.1)。站点清单(哪些路径**没有**接线)写在 `PipeStats.cpp` 头部,那份清单是契约。
关闭时每站点一次全局 load + 一条永不命中的分支。字节类(stage buffer / texture / UBO / client 数组、`pmap`、`resid`、`csob-blob`、`seg` = 写进 `SEG_STAGE` 的字节)、调用类(`draws`、`accessor-calls`、纹理上传 emit / box / rect / jobs、`csom` / `csob`、`mpr`、`trp`、`rsp`、`wrec`、server 应用的控制记录 `ar`(applier_reset)/ `od`(object_death)、`maxrec` / `ringwraps` / `ringpads` / `ringwaits`、门铃 `srv` / `srvpark` / `cli` / `clipark`)与六个 memo 门的 hit/miss。每 `MOBILEGL_PIPE_STATS_PERIOD` 帧一条 `MGPipe stats:` 汇总行,按角色写日志。读法陷阱:`accessor-calls` 是静态下界,判性能只看 CPU 时间;spawn 下 client 不推进帧窗口(`frames=0`),窗口化字段在 spawn client 上无效,run total 仍有效([`docs/archive/notes/p6/README.md`](../../archive/notes/p6/README.md) §12.1)。站点清单(哪些路径**没有**接线)写在 `PipeStats.cpp` 头部,那份清单是契约。
+2 -2
View File
@@ -11,7 +11,7 @@
| 17.1 | verb barrier 与诚实的同地址空间传输(P5) | 同地址空间不得成为旁路(R-2):encoder 把 `MGHostSpan::Ptr` 恒写 `nullptr`;reply 以记录序号为 slot id(R-3);等待看门狗 120 s 只判死锁,不是性能门 | [`docs/archive/notes/p5/README.md`](../../archive/notes/p5/README.md) |
| 17.2 | 后端槽三类、caps、tight readback(P5) | A 本地答 / B 发射 / C 具名拒绝,永不回落 monolith(R-4);能力只读 `CallMask`(R-8);`ReadPixels` 线上恒 tight,client 按自己的 pack state 散射 | 同上 |
| 17.3 | server 角色、shadow 与退出(P5) | apply 线程终身持有 native context,EGL 罕见操作经控制邮箱进 apply 线程;staged shadow 只交 server-owned 拷贝;有界退出顺序 | 同上 |
| 17.4 | lane 隔离(P5) | 每条 split 条目私有日志;E1 / E3 控制把"选中条目被跳过"判为失败 | 同上 |
| 17.4 | lane 隔离(P5) | 每条 split 条目私有日志;E1 与 E3(a) 的对照是常驻用例(`RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait`、`PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer`),不重跑 lane | 同上 |
| 17.5 | class-C verb 迁移(P5b) | 迁移顺序由动态普查决定;一次迁移 = 一个发射器 + 一个 sink 体;记录逐字带 GL 调用参数,未测量的形式按名拒绝(规则 D) | [`docs/archive/notes/p5b/README.md`](../../archive/notes/p5b/README.md) |
| 17.6 | 共享地址空间访问归零(P5c) | server 端纹理 staged shadow;`SEG_EVENT` 是唯一反向通道;sink / twin 按记录句柄解析;`applier_reset` / `object_death` 控制记录;角色守卫 `Fatal{RoleViolation}` | [`docs/archive/notes/p5c/README.md`](../../archive/notes/p5c/README.md) |
| 17.7 | run-ahead(P5e) | 规则 F:unbarriered 记录的 apply 不读任何 client 内存(违反即具名 Fatal,与 strict 开关无关);`gPipeInputs` 是 server 角色内存;`RunAheadArmed()` 在首次 caps 采纳时锁存,只能关不能开;`RUN_AHEAD=0` 是 A/B 对照,`VERB_BARRIER=0` 会连带关掉 run-ahead | [`docs/archive/notes/p5e/README.md`](../../archive/notes/p5e/README.md) |
| 17.7 | run-ahead(P5e) | 规则 F:unbarriered 记录的 apply 不读任何 client 内存(违反即具名 Fatal,与 strict 开关无关);`gPipeInputs` 是 server 角色内存;`RunAheadArmed()` 在首次 caps 采纳时锁存,只能关不能开;`RUN_AHEAD=0` 是 A/B 对照 | [`docs/archive/notes/p5e/README.md`](../../archive/notes/p5e/README.md) |
+10
View File
@@ -54,6 +54,16 @@ them wrong.
| 2026-10-17 | `Magma split lane (strict, dual-block)` on the disaggregation branches when the dual-block step proved its defaults | Test / `integration-split-strict` | the dual-block census run covers the `integration-magma-split` label in the same environment | the dual-block census run |
| 2026-10-17 | the 75 retrace-split inproc rows other than OpenRA's, when `build-linux-split` reuses `build-linux` | Test / `retrace-split` | the retrace job's inproc-acceptance rows replay the same cases on the same library, strict and with the dual block and run-ahead on; no one-sided pair since 10-05 | the inproc-acceptance rows, which now prove per leg from the client log that they run with strict=1, role-split-state=1 and run-ahead=1 (`MGTRACE_EXPECT_IPC`) and keep a refusal census; every remaining retrace-split leg proves its defaults (strict=0, role-split-state=1, run-ahead=1) and a changed default reds every leg with the rows to restore; `trace_cases.py --self-test-split-matrix` (disaggregation branches) proves every dropped row is an acceptance row; OpenRA's inproc rows stay for the wire evidence |
| 2026-10-17 | `Transport resolution check`'s grep of one entry's log for the inproc resolution sentence | Test / `integration-split` | it checked one entry (`DirectGLES.Split.PersistentMapArm.`) after the fact | in-test: the split fixture (`Harness/ScenarioFixture.h`) now fails every armed Split/Spawn/Tcp entry whose process did not resolve the transport its registration claims, or whose server is not where the claim says (a thread here for inproc, a process of its own for spawn and tcp), from the running process (`SplitRuntimePeek`, `ClientSession::PeerServerPid()`); `SplitLaneHarness.TransportClaimRule` (unit) proves the rule red on canned inputs; the step keeps the IPC default proof as `Default-knob resolution check` |
| 2026-10-18 | `Negative controls (barrier, map push)` (`scripts/ci/split_negative_controls.sh`; skip-census lane `junit-integration-split/control`) | Test / `integration-split` | it reran the split entries with `MOBILEGL_IPC_VERB_BARRIER=0` and `MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0`, two product knobs that exist only for it; of what was left without them, the ownership check and the baseline tally repeat `SplitLogPaths.PrivateAndDistinct` and the lane step's own `junit_tally.py --require-split-ran`, and the `DirectGLES.Split.` rerun for a job whose lane step never ran stays as the step `Split lane fallback (lane step skipped)` | E1: `RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait` (unit, always on); E3(a): `PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer`, always on in the split-arm entries (DirectGLES inproc, spawn, tcp and small ring; Magma's in the informational tier), within one frame and across a Present. Narrower than the control: it takes the push away with `PersistentMapTracker::Forget`, which also drops the membership (only the push reads it today), and draws its own frames, so it no longer shows that the bodies of `TwoWrites...` and `AWriteAfter...` make no announcing call |
| 2026-10-18 | `scripts/ci/wait_boundary_negative_control.py` | Test / `integration-split` | E1's body inside the step above: the three boundary cases rerun with the barrier off | `RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait`: the same ApplierReset published with no wait must read parked=0, the observation the owed waits read as parked=1 |
| 2026-10-18 | `scripts/ci/control_smoke_test.sh`, `scripts/ci/testdata/stub_ctest.sh`, `scripts/ci/testdata/split_private_log_smoke.sh`, and the `reset` / `results` / `assertion` / `evidence` modes of `split_log_paths.py` | Test / pipe-gates (R-16) | smoke cases and helpers of the step above only | none needed |
| 2026-10-18 | unit `RemoteRunAhead.ClearingTheVerbBarrierDisarmsRunAheadEvenWithTheCapBit`, and the `MOBILEGL_IPC_VERB_BARRIER` knob with its arm in the shipped client (`ClientSession`: the free-running `EmitAndWaitTails` return, the run-ahead latch term, the `gPipeInputs` guard's early return, the `verb-barrier=` field of the `Config: IPC` line) | Test / unit (split); product code | `0` was never a product mode, only E1's negative control, retired above; the unit case pinned that the control also disarmed run-ahead | `RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait`; `MOBILEGL_IPC_RUN_AHEAD=0` stays the lockstep A/B arm |
| 2026-10-18 | unit `SplitBufferSet.AZeroBlockSizeTurnsThePushOffRatherThanMakingItUnlimited` (and its skip stub in the non-disaggregated half), and the `MOBILEGL_IPC_PERSISTENT_BLOCK_KB` knob with its `0` arm in the shipped tracker (`PersistentMapTracker::PushBlocksForChecked`'s early return and once-per-process warning, `BufferObject::NotePersistentMapStateChanged`'s, the `persistent-block=` field of the `Config: IPC` line) | Test / unit; product code | `0` was never a product mode, only E3(a)'s negative control, retired above; the unit case pinned that `0` shipped nothing; the block size is now the constant 64 KiB (`PersistentMapTracker::BlockBytes()`) | `PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer` |
| 2026-10-18 | `Negative control A (corrupted snapshot)` | Test / `integration-verify` | it re-ran the five `DirectGLES.Verify.ClearThenReadPixelsScenario` entries with `MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters` exported job-wide and accepted any red | the in-lane `DirectGLES/DirectVulkan.VerifyCorrupted.PipeVerifyArmingScenario.CorruptedFieldIsReported` entries, required by name (`Verify lane control entries`), which require `Fatal{PipeVerifyDiffer, "GetRenderStateParameters@...` in the log; the abort with `MOBILEGL_PIPE_VERIFY_FATAL` at its default is unit `PipeInputsTest.VerifyCorruptKnobNamesTheFieldAtEntry` on the verify runtime; that the ambient entries run the comparator armed and fatal is `VerifyArming.PipeVerifyArmingScenario.Armed`, whose environment is now the ambient entries' list plus its marker and log, and which requires `fatal=1` on the arming line |
| 2026-10-18 | `Negative control A2 (corrupted capability)` | Test / `integration-verify` | the same five-entry rerun with `MOBILEGL_PIPE_VERIFY_CORRUPT=IsCapabilityEnabled`, accepting any red; it proved that the comparator compares the capability field (P13 W3c moved the residual block's cross-check there) | unit `PipeInputsTest.EveryCorruptedFieldIsNamedByTheComparator` (`Unit tests on the verify runtime`): every field with storage, the capability field among them, corrupted in turn must be the field the comparator names; then, in a forked child with the knob set to `IsCapabilityEnabled`, each of Clear, DrawArrays and ReadPixels whose class fills the field must report it at entry, and one must |
| 2026-10-18 | `Negative control A (corrupted snapshot)` and `Keep lane logs before controls` | Test / `integration-verify-split` | the first re-ran the ten `VerifySplit.ClearThenReadPixelsScenario` entries of both backends with the same knob and accepted any red; the second only kept the lane's logs from being overwritten by that rerun | the in-lane `VerifySplitCorrupted` and `VerifySplitReadCorrupted` entries of both backends, required by name (`Verify-split control entries`); that the ambient entries arm is the per-entry arm proof, and that they run fatal is `VerifySplitArming.PipeVerifyArmingScenario.Armed` (the ambient environment plus its marker and log), which requires `fatal=1` on the arming line |
| 2026-10-18 | unit `PipeInputsTest.{PoisonOmitKnobArmsTheOmission, BadPoisonOmitKnobIsFatalNamingTheKnob}`, and the `MOBILEGL_PIPE_POISON_OMIT` knob: `Features.PipePoisonOmit`, its parser on every verb (`MGPipeValidateForVerb`'s `ParsePoisonOmissionKnob`, compiled into every build) and the server stamp's own parse of it | Test / unit (every poison lane); product code | the knob existed only for negative control B, and the two cases pinned its parser and its `Fatal{PipeVerifyBadKnob}` | the PoisonOmitted and VerifySplitPoisonOmitted entries set the harness marker `MGITEST_POISON_OMIT` to the same pair, and the re-executed child arms it through `MGPipeSetPoisonOmission`, compiled only with `-DMOBILEGL_PIPE_VERIFY=ON` (`Harness/PoisonOmissionArm.h`); `OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale`, `ReadingAnOmittedFieldAbortsNamingTheVerb` and `TheMutationNoticeRefreshesTheValueButNotTheStamp` withhold the stamp test-side and still run in every poison lane; the setter's scope to its verb, not its class, is checked by `OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale` in a forked child, on the verify runtime only |
| 2026-10-18 | the `serial == resets` assertion of `CtWireScenario.ApplierResetCrossesAtThePrimedEdgeInSerialOrder`, and the `MOBILEGL_TEST_SERVER_COUNTERS` knob: `ServerMain`'s `MGPipe server counters:` line on every LogFlush and `ServerVerbSink`'s test-only tallies (`ApplierResets`, `ObjectDeaths`, `ExpectedApplierResetSerial`) | Test / `integration-split`, `integration-split-strict`, `integration-verify-split`, `integration (inproc)`; product code | the knob existed only so the spawn and tcp arms could read the sink's tallies; the serial assertion could not fail, since `OnApplierReset` moves the count and the serial together and any other serial latches `Fatal{ProtocolCorruption, "ApplierReset.ContextSerial"}` | `ar` / `od` on the server role's `MGPipe stats:` line, counted in `ServerVerbSink::OnApplierReset` / `OnObjectDeath`; every CtWire entry that counts them sets `MOBILEGL_PIPE_STATS=1` with period 1 and reads them on every arm, inproc included, over a window closed by a swap and a readback; the unit cases `ApplierResetWireRoundTrip.TheSerialSequenceIsAssertedAndTheServerResetRuns` and `ObjectDeathWireRoundTrip.TheTextureAndFramebufferTwinsReleaseByHandleAndNeverByAStaleGeneration` read the same counters, and the first shows the expected serial moving by having serial 2 accepted |
## Kept after investigation
-143
View File
@@ -1,143 +0,0 @@
#!/bin/bash
# MobileGL - scripts/ci/control_smoke_test.sh
# Copyright (c) 2025-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
# A smoke test for the CI negative controls themselves.
#
# A negative control is a gate, and a workflow `run:` block only executes on a runner, so this
# file runs the real control script - the same file .github/workflows/test.yml invokes, not a copy
# of it - against a stubbed ctest, and checks that it passes exactly when it should.
#
# The case that matters is the first one. A stubbed ctest reports a non-empty selection and then
# fails with UNRELATED_CONTROL_FAILURE: a reason that has nothing to do with the knob the control
# turns. The control must report FAILED on it, and PASSED only on its scenarios' own diagnostic.
#
# usage: control_smoke_test.sh
set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
WORK="$(mktemp -d)" || exit 1
trap 'rm -rf "${WORK}"' EXIT
STUB_DIR="${WORK}/stub"
mkdir -p "${STUB_DIR}"
cp "${HERE}/testdata/stub_ctest.sh" "${STUB_DIR}/ctest"
chmod +x "${STUB_DIR}/ctest"
passes=0
failures=0
# expect <expected: PASSED|FAILED> <label> -- <command...>
expect() {
want="$1"; label="$2"; shift 3 # shift past the literal "--"
outfile="${WORK}/run.out"
"$@" > "${outfile}" 2>&1
rc=$?
if [ "${rc}" -eq 0 ]; then got="PASSED"; else got="FAILED"; fi
if [ "${got}" = "${want}" ]; then
passes=$((passes + 1))
printf 'ok %-58s %s (rc=%d)\n' "${label}" "${got}" "${rc}"
else
failures=$((failures + 1))
printf 'NOT OK %-58s expected %s, got %s (rc=%d)\n' "${label}" "${want}" "${got}" "${rc}"
sed 's/^/ | /' "${outfile}"
fi
}
# The lane step's own baseline, as the workflow leaves it: the stub's JUnit for the whole label and
# its exit code beside it, in <base>.xml and <base>.rc.
lane_baseline() { # $1 = STUB_MODE, $2 = base path
env -i PATH="${STUB_DIR}:/usr/bin:/bin" STUB_MODE="$1" CONTROL_TMPDIR="${WORK}/tmp-$1" \
ctest -L integration-split --no-tests=error --output-junit "$2.xml" > /dev/null 2>&1
echo "$?" > "$2.rc"
}
# run_split <STUB_MODE> [baseline path]: the controls, tallying the lane step's baseline. A path
# with no files behind it is the job whose lane step never ran.
run_split() {
local base="${2:-${WORK}/base-$1}"
[ -n "${2:-}" ] || lane_baseline "$1" "${base}"
env -i PATH="${STUB_DIR}:/usr/bin:/bin" STUB_MODE="$1" \
CTEST=ctest CONTROL_TMPDIR="${WORK}/tmp-$1" SPLIT_BASELINE_JUNIT="${base}.xml" \
bash "${HERE}/split_negative_controls.sh"
}
# run_split_saying <sentence> <args for run_split...>: run_split, and its output must carry the
# sentence that says which baseline it used.
run_split_saying() {
local sentence="$1"
shift
local out
out=$(run_split "$@" 2>&1)
local rc=$?
printf '%s\n' "${out}"
printf '%s' "${out}" | grep -qF "${sentence}" || { echo "missing: ${sentence}"; return 1; }
return "${rc}"
}
# The lane step's JUnit says green but its exit code says red: ctest's "Not Run" is a skip in
# JUnit, so the code is part of the baseline and must be honoured.
run_split_red_code() {
lane_baseline evidence "${WORK}/redcode"
echo 8 > "${WORK}/redcode.rc"
run_split evidence "${WORK}/redcode"
}
# The lane step never ran: the controls rerun the baseline, and the rerun's JUnit stays out of
# CONTROL_TMPDIR/*.xml, which the job uploads for the skip census.
run_split_rerun() {
rm -f "${WORK}/tmp-evidence/isplit.xml"
run_split_saying "reran the DirectGLES.Split. baseline" evidence "${WORK}/absent" || return 1
if [ -e "${WORK}/tmp-evidence/isplit.xml" ]; then
echo "the rerun's JUnit landed in CONTROL_TMPDIR/*.xml"
return 1
fi
}
# The lane step ran nothing of the DirectGLES.Split. arm (only other entries of the label).
run_split_no_arm() {
printf '%s\n' '<testsuite><testcase name="SplitLogPaths.PrivateAndDistinct" status="run"/></testsuite>' \
> "${WORK}/noarm.xml"
echo 0 > "${WORK}/noarm.rc"
run_split evidence "${WORK}/noarm"
}
echo "=== the split lane's E1 / E3(a) controls (scripts/ci/split_negative_controls.sh)"
# THE FINDING, REPRODUCED. Non-empty selection, green baseline, and a red that is not the knob's.
expect FAILED "unrelated failure with a non-empty selection" -- run_split unrelated
# ... and the same control on the same stub, failing for its own reason: it must PASS.
expect PASSED "the scenarios' own diagnostic" -- run_split evidence
# The pre-existing half of the control, which was never broken: a knob that reds nothing.
expect FAILED "the knob leaves the selection green" -- run_split green
# The arming counter's half of the finding: a baseline that is already red cannot arm anything.
expect FAILED "the baseline is already red" -- run_split red-baseline
# P5 is complete: losing the runtime implementation must no longer disarm the gate.
expect FAILED "every split entry skipped (implementation lost)" -- run_split all-skipped
expect FAILED "E1 missing boundary probes" -- run_split e1-empty
expect FAILED "E1 own baseline is already red" -- run_split e1-red-baseline
expect FAILED "E1 restored boundary remains red" -- run_split e1-restore-red
expect FAILED "E1 record never reaches the peer" -- run_split e1-no-peer
expect FAILED "E1 observed no real emission" -- run_split e1-no-emit
expect FAILED "E1 incorrectly waits every record" -- run_split e1-waitall
# The baseline is the lane step's own run; the controls rerun it only when that step never ran.
expect PASSED "the lane step's baseline is tallied, not rerun" -- run_split_saying "no rerun" evidence
expect PASSED "no lane JUnit: the controls rerun the baseline" -- run_split_rerun
expect FAILED "the lane step's exit code is red, its JUnit green" -- run_split_red_code
expect FAILED "the lane JUnit has no DirectGLES.Split. entry" -- run_split_no_arm
echo
echo "smoke test: ${passes} passed, ${failures} failed"
# Keep the private-file cases on the entry point used by CI and the local gate.
if ! bash "${HERE}/testdata/split_private_log_smoke.sh"; then
failures=$((failures + 1))
fi
if [ "${failures}" -gt 0 ]; then
echo "CONTROL_SMOKE_TEST_FAILED"
exit 1
fi
echo "CONTROL_SMOKE_TEST_OK"
+1 -2
View File
@@ -10,8 +10,7 @@
# P5f (f1)'s NEGATIVE CONTROL (P5F-WIRE-COMPLETENESS.md §6): the dual-block rehearsal must be
# the thing the dual-block lane measures.
#
# The contract is split_negative_controls.sh's, shrunk to one control because there is one
# knob:
# Its contract has two rules:
#
# 1. THE BASELINE MUST BE GREEN. DirectGLES.Split.DualBlock.* runs with
# MOBILEGL_IPC_ROLE_SPLIT_STATE=1 exported (the entry deliberately does NOT pin the knob in
+3 -2
View File
@@ -9,8 +9,9 @@
"""Tally a ctest --output-junit file as "passed failed skipped".
Split out of .github/workflows/test.yml's negative-control step so that the workflow, the local
gate and the control smoke test all count a run the same way.
Split out of .github/workflows/test.yml's split negative-control step (since retired) so that
every caller counts a run the same way: the split, spawn, TCP, verify and dual-block lane steps,
the split lane's fallback, and scripts/ci/dualblock_negative_control.sh.
WHY THIS EXISTS AT ALL (ID-46 finding 8, second half). The counter this replaces lived inline in
the workflow and counted a case as having "run" when it was merely not <skipped/>:
-216
View File
@@ -1,216 +0,0 @@
#!/bin/bash
# MobileGL - scripts/ci/split_negative_controls.sh
# Copyright (c) 2025-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
# EXIT GATE E1's NEGATIVE CONTROL and EXIT GATE E3(a)'s.
#
# The body of test.yml's "Negative controls (barrier, map push)" step, which requires the verb
# barrier and the persistent-map push to be load-bearing. It is a file, not an inline `run:` block,
# so scripts/ci/control_smoke_test.sh runs these same lines off a runner against a stubbed ctest.
#
# WHAT THE REVIEW FOUND (ID-46 finding 8, CONFIRMED by execution; ID-48 assigns it here).
# The previous version accepted ANY non-zero ctest exit as "the knob is load-bearing". A timeout, a
# setup abort, an unrelated assertion, a harness that died before it read the knob at all - every
# one of them printed "turned N selected entries red, as it must" and the step went green. The
# verifier demonstrated it: a stubbed ctest with a NON-EMPTY selection that failed with
# `UNRELATED_CONTROL_FAILURE` produced both controls' success messages and HARNESS_EXIT=0.
#
# So each control now has to say WHY the red is its own:
#
# 1. THE BASELINE MUST BE GREEN. The arming run below used to end in `|| true` and count every
# case that was not <skipped/> as "ran" - so a case that RAN AND FAILED armed the controls,
# and a control that turns an already-red entry red proves nothing at all. It now counts
# PASSED cases, and a baseline with any failure in it is a hard error rather than an arming
# signal.
# 2. THE RED MUST CARRY THE SELECTED CASE'S OWN FAILURE TEXT. Each control names a regex of the
# diagnostics its scenarios emit when that knob is off, and the red is refused if the output
# carries none of them.
#
# WHY THE EVIDENCE IS THE SCENARIO'S ASSERTION TEXT AND NOT THE KNOB'S OWN LOG LINE.
# ConfigLoader logs a named line for both knobs ("is the R-1 NEGATIVE
# CONTROL", "is the E3(a) NEGATIVE CONTROL"), and it is tempting to grep for that. It is not
# evidence: it is written at config load, by every process in the run, whatever happens next. A
# setup abort would carry it too. It proves the knob was READ, never that the knob caused the red.
# Only the failing case's own diagnostic does that. E1 now observes deterministic transport
# waits in dedicated CPU tests; E3(a) retains its scenario assertion and private library log.
# P5e ID-122: ordinary run-ahead draws need not fail with VERB_BARRIER=0, and the old
# overlap Fatal cannot fire with the default BATCH_WAITS=1. Pixel failures are not E1 evidence.
#
# Usage: split_negative_controls.sh [--self-test]
# CTEST ctest binary (default: ctest)
# CONTROL_TMPDIR scratch dir for the junit + output (default: ${RUNNER_TEMP:-/tmp})
# SPLIT_BASELINE_JUNIT the JUnit an earlier `ctest -L integration-split` wrote; its exit code is
# read from the file of the same name ending in .rc. When both exist the
# baseline is tallied from them instead of re-run.
set -u
if [ "${1:-}" = "--self-test" ]; then
bash "$(dirname "$0")/control_smoke_test.sh"
exit $?
fi
CTEST="${CTEST:-ctest}"
CONTROL_TMPDIR="${CONTROL_TMPDIR:-${RUNNER_TEMP:-/tmp}}"
mkdir -p "${CONTROL_TMPDIR}"
log_helper="$(dirname "$0")/../../MobileGL/MG_IntegrationTest/Harness/split_log_paths.py"
# Check ownership even while the runtime lane is disarmed and will skip.
python3 "${log_helper}" check "${CTEST}" "$PWD" || exit 1
# ---- the baseline ---------------------------------------------------------------------------
#
# 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 a
# runtime fact inside each test process (MG_Config::Transport, ClientSession::Active(),
# ImplementedVerbCount(), read by Harness/SplitRuntimePeek), so the only honest way to ask it from a
# shell is to look at what the entries DID.
# The metadata ownership test shares the label but cannot arm runtime controls.
#
# The lane step before this one already ran every integration-split entry on this runner and
# library, and left its JUnit and exit code. Both are read when they exist; only the
# DirectGLES.Split. entries count, as in the rerun. The rerun stays for a job whose lane step
# never ran (an earlier gate failed), where this baseline is the only run of those entries.
baseline_rc_file="${SPLIT_BASELINE_JUNIT:-}"
baseline_rc_file="${baseline_rc_file%.xml}.rc"
if [ -n "${SPLIT_BASELINE_JUNIT:-}" ] && [ -f "${SPLIT_BASELINE_JUNIT}" ] && [ -f "${baseline_rc_file}" ]; then
junit="${SPLIT_BASELINE_JUNIT}"
baseline_rc=$(tr -dc '0-9' < "${baseline_rc_file}")
baseline_rc="${baseline_rc:-1}"
echo "baseline tallied from ${junit} (the lane step's own run, ctest exit ${baseline_rc}); no rerun"
else
# A directory of its own: the job uploads CONTROL_TMPDIR/*.xml for the skip census, which has
# no row for this rerun.
mkdir -p "${CONTROL_TMPDIR}/split-controls"
junit="${CONTROL_TMPDIR}/split-controls/isplit.xml"
rm -f "${junit}"
echo "::notice::no baseline JUnit and exit code from the lane step (SPLIT_BASELINE_JUNIT='${SPLIT_BASELINE_JUNIT:-}'), so the controls reran the DirectGLES.Split. baseline"
"${CTEST}" -L integration-split -R '^DirectGLES\.Split\.' -j 4 --no-tests=error --output-junit "${junit}"
baseline_rc=$?
fi
if [ ! -f "${junit}" ]; then
echo "::error::the baseline run wrote no ${junit} (ctest exit ${baseline_rc}), so nothing below can tell an armed lane from a broken one"
exit 1
fi
tally=$(python3 "$(dirname "$0")/junit_tally.py" "${junit}" --require-split-ran)
if [ -z "${tally}" ]; then
echo "::error::could not tally ${junit}; a run whose result cannot be read is not an arming signal"
exit 1
fi
baseline_passed=$(echo "${tally}" | cut -d' ' -f1)
baseline_failed=$(echo "${tally}" | cut -d' ' -f2)
baseline_skipped=$(echo "${tally}" | cut -d' ' -f3)
echo "split entries - passed: ${baseline_passed}, failed: ${baseline_failed}, skipped: ${baseline_skipped} (ctest exit ${baseline_rc})"
# A RED BASELINE DISARMS THE CONTROLS RATHER THAN ARMING THEM (review finding 8, second half).
# `|| true` plus a "not skipped" counter used to treat a case that ran and FAILED as evidence the
# lane was live. Turning an already-red entry red is not a measurement.
if [ "${baseline_failed}" -gt 0 ] || [ "${baseline_rc}" -ne 0 ]; then
echo "::error::${baseline_failed} DirectGLES.Split. entries are ALREADY RED with both knobs at their defaults, so neither negative control below can attribute its red to the knob it turns. Fix the lane first; a control measured against a red baseline is not a control. (This used to be swallowed by an unconditional '|| true' and counted as 'the lane is armed'.)"
exit 1
fi
if [ "${baseline_passed}" -lt 1 ]; then
echo "::error::split baseline FAILED: every DirectGLES.Split. entry SKIPPED; the split implementation did not execute"
exit 1
fi
# ---- the controls ---------------------------------------------------------------------------
#
# run_control <name> <filter> <ctest evidence regex> <private-log evidence regex|""> <VAR=VALUE>...
#
# THE FOURTH ARGUMENT IS NEW AND E3(a) IS WHY. The header above argues that the knob's own
# ConfigLoader line is not evidence, and that is still true: it is written at config load, by
# every process in the run, whatever happens next. What IS evidence is a line the knob's
# BEHAVIOUR emits at the site that changed - and until now E3(a) had none, because
# PersistentMapTracker::PushBlocksFor simply `return`ed at blockBytes == 0. The joint gate
# recorded exactly that ("There is no Fatal for block size zero ... No nonexistent private-file
# Fatal is quoted", joint-v1.md 3) and ID-65 assigned the missing line here. With it, E3(a) no
# longer rests on a pixel assertion alone: the red must carry the scenario's own diagnostic AND
# the library's own statement that the push was disabled, from the entry's private file.
# The optional 5th positional is a ctest -E EXCLUSION, applied to every ctest invocation this
# function makes. It exists because ctest -R is POSIX ERE: there is no negative lookahead, so a
# selection that must say "these, except that one" cannot say it in -R alone. E1 below is the
# case that forced it (ID-122).
run_control() {
name="$1"; filter="$2"; evidence="$3"; private_evidence="$4"; exclude="${5:-}"; shift 5
if [ -n "${exclude}" ]; then EX=(-E "${exclude}"); else EX=(); fi
export SPLIT_LOG_EXCLUDE="${exclude}"
matched=$("${CTEST}" -N -L integration-split -R "${filter}" "${EX[@]}" | 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
manifest="${CONTROL_TMPDIR}/split-tests.json"
"${CTEST}" --show-only=json-v1 > "${manifest}" || exit 1
# Remove selected files first: a previous Fatal must never arm a new red.
python3 "${log_helper}" reset "${manifest}" "${filter}" || exit 1
out="${CONTROL_TMPDIR}/control-output.txt"
result="${CONTROL_TMPDIR}/control.xml"
rm -f "${result}"
env "$@" "${CTEST}" --output-on-failure -L integration-split -R "${filter}" "${EX[@]}" --no-tests=error --output-junit "${result}" > "${out}" 2>&1
control_rc=$?
cat "${out}"
# Inspect JUnit before exit status or private Fatal: a skipped pre-flight can carry both.
python3 "${log_helper}" results "${manifest}" "${filter}" "${result}" 'E3(a)' || exit 1
if [ "${control_rc}" -eq 0 ]; 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
if ! python3 "${log_helper}" assertion "${manifest}" "${filter}" "${result}" "${evidence}"; then
echo "::error::${name} FAILED: red lacks its persistent-map push diagnostic. Required: ${evidence}"
exit 1
fi
# ... and, where the knob has one, the LIBRARY's own line as well, out of the entry's private
# file. Both halves are required: the scenario assertion says the pixels were wrong, and this
# says the code path the knob turns off is the one that stopped running. A red that has only
# the first half is consistent with any other defect in the same scenario.
if [ -n "${private_evidence}" ]; then
python3 "${log_helper}" evidence "${manifest}" "${filter}" "${private_evidence}" "${name}" || exit 1
fi
echo "${name} turned ${matched} selected entries red, and the red carries the scenario's own diagnostic, as it must"
}
# E1: the default arm must actually park for a kWaitApplied record and for a
# kWaitNone record whose server has no run-ahead cap. Removing the verb barrier
# must fail those exact return-before-apply assertions. A third positive case
# proves that a cap-authorized kWaitNone record does not wait. The peer releases
# on an observed Park or on the client's return, not on a sleep or pixel race.
# The helper demands green baseline, two named reds, and green restoration;
# skipped/missing cases, timeouts and unrelated failures never count as evidence.
python3 "$(dirname "$0")/wait_boundary_negative_control.py" \
--ctest "${CTEST}" --out "${CONTROL_TMPDIR}/e1" || exit 1
# E3(a): PersistentMapTracker::PushBlocksFor stops at blockBytes == 0 - deliberately, because 0
# is the negative control and not "unlimited". Two independent halves are now required:
# * the SCENARIO's own assertion in ctest's output. PersistentCoherentMapScenario.cpp:414-417 /
# 442-443 name the missing second write; a generic source-line Failure is not accepted,
# because an unrelated assertion in the same case is not this red;
# * the LIBRARY's own line in the entry's private file, saying the push was disabled by this
# knob. It did not exist until ID-65 assigned it (joint-v1.md 3), which is why this control
# used to rest on the pixels alone.
# TheMapLandsInTheArmItsLaneDeclares skips by design outside PersistentMapArm.
# Select only the pixel cases; a pre-flight skip in either remains a hard failure.
run_control "negative control E3(a) (MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0)" \
'DirectGLES\.Split\.(SmallRing\.)?PersistentCoherentMapScenario\.(TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw|AWriteAfterAFrameBoundaryReachesTheNextFramesDraw)$' \
"the SECOND write through the same mapping, announced by nothing|frame 1's write through the SAME mapping, after a Present" \
'MGPipe: persistent-map push disabled - MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0' \
'' \
MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0
+1 -1
View File
@@ -68,7 +68,7 @@ def start(args):
if args.state.exists():
raise RuntimeError(f'stale fixture state; run stop first: {args.state}')
env = dict(os.environ, MOBILEGL_IPC_ROLE='server', MOBILEGL_IPC_DIAL='no',
MOBILEGL_IPC_TOKEN='', MOBILEGL_IPC_LOG_FORWARD='1', MOBILEGL_TEST_SERVER_COUNTERS='1',
MOBILEGL_IPC_TOKEN='', MOBILEGL_IPC_LOG_FORWARD='1',
MOBILEGL_PIPE_STATS='1', MOBILEGL_PIPE_STATS_PERIOD='1')
for key in ('MOBILEGL_TRANSPORT', 'MOBILEGL_IPC_SERVER_PATH', 'MOBILEGL_IPC_RING_MB', 'MOBILEGL_IPC_STAGE_MB',
'MOBILEGL_IPC_CONTROL', 'MOBILEGL_IPC_ENDPOINT', 'MOBILEGL_BACKEND_TYPE'):
-47
View File
@@ -1,47 +0,0 @@
#!/bin/bash
# MobileGL - scripts/ci/testdata/split_private_log_smoke.sh
# Copyright (c) 2025-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
# Exercise E1's per-test boundary evidence and E3(a)'s private-file read (R-16).
set -euo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"
WORK=$(mktemp -d "${TMPDIR:-/tmp}/split-private-log.XXXXXX")
trap 'rm -rf "${WORK}"' EXIT
cp "${HERE}/testdata/stub_ctest.sh" "${WORK}/ctest"
chmod +x "${WORK}/ctest"
passes=0
for mode in missing-fatal stdout-fatal stale-fatal evidence e3-unrelated e3-no-private skipped-selection e3-skipped-selection notrun-selection missing-selection partial-fatal wrong-fatal; do
mkdir -p "${WORK}/${mode}"
rc=0
STUB_MODE="${mode}" CTEST="${WORK}/ctest" CONTROL_TMPDIR="${WORK}/${mode}" \
bash "${HERE}/split_negative_controls.sh" > "${WORK}/${mode}.out" 2>&1 || rc=$?
if [ "${mode}" = evidence ]; then
[ "${rc}" = 0 ] && grep -q "negative control E3(a).*scenario's own diagnostic" "${WORK}/${mode}.out" || {
cat "${WORK}/${mode}.out"; echo "NOT OK own boundary/private-file evidence must PASS"; exit 1;
}
grep -qFx "private-log evidence: DirectGLES.Split.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw: ${WORK}/${mode}/pmap.log" "${WORK}/${mode}.out" || {
cat "${WORK}/${mode}.out"; echo "NOT OK private-file evidence line must name the selected entry and path"; exit 1;
}
else
message='red lacks its own observed wait-boundary failure'
[ "${mode}" != e3-unrelated ] || message='FAILED: red lacks its persistent-map push diagnostic'
case "${mode}" in
e3-no-private) message='no selected private log carries /MGPipe: persistent-map push disabled' ;;
skipped-selection|notrun-selection) message='selected testcase skipped or did not run' ;;
e3-skipped-selection) message='SplitLogPaths FAILED: E3(a) control: the knob killed the pre-flight, not the entry - 1 selected entries skipped' ;;
missing-selection) message='selected testcase names are missing, duplicated or unexpected' ;;
esac
match=(-qF)
[ "${rc}" != 0 ] && grep "${match[@]}" "${message}" "${WORK}/${mode}.out" || {
cat "${WORK}/${mode}.out"; echo "NOT OK ${mode}: control must report FAILED for its own reason"; exit 1;
}
fi
echo "ok private-log ${mode} (control rc=${rc})"
passes=$((passes + 1))
done
echo "private-log smoke test: ${passes} passed, 0 failed"
-269
View File
@@ -1,269 +0,0 @@
#!/bin/bash
# MobileGL - scripts/ci/testdata/stub_ctest.sh
# Copyright (c) 2025-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
# A stubbed `ctest` for scripts/ci/control_smoke_test.sh.
#
# Descended from the verification agent's stub (wave1-codex-verify.md 8, ~/w7/p5-verify-f8-stub/ctest),
# which is what CONFIRMED that the negative controls accepted an unrelated failure. Every mode below
# is deliberately the BEST case for the control under test: the selection is never empty except in
# the mode that exists to test the empty-selection guard, and the baseline is green except in the
# mode that exists to test the red-baseline guard. If a control passes here it is because the
# control's logic is wrong, not because the stub starved it.
#
# STUB_MODE:
# unrelated baseline green; the control's own run fails with UNRELATED_CONTROL_FAILURE
# evidence baseline green; the control's own run fails with the scenarios' own wording
# missing-fatal private file exists but lacks Fatal (stdout status still fails)
# stdout-fatal Fatal exists only on stdout, never in the private file
# stale-fatal Fatal exists before reset, never from this control run
# e3-unrelated E1 has its private Fatal; E3(a) fails for an unrelated reason
# skipped-selection knob kills pre-flight; private Fatal exists, entry skips, ctest exits 0
# notrun-selection selected entry was not run, despite a private Fatal
# missing-selection selected entry is absent from the result XML
# partial-fatal two entries fail, but only the first has its expected private Fatal
# wrong-fatal entry fails with a different Fatal in its private file
# e3-no-private E1 has its private Fatal; E3(a) has its PIXEL assertion but the library
# never says the push was disabled (the half ID-65 added)
# green baseline green; the control's own run PASSES (the knob is not load-bearing)
# red-baseline the baseline itself has a failed entry
# all-skipped the baseline is entirely skipped (lost implementation, a hard failure)
set -u
mode="${STUB_MODE:?STUB_MODE must be set}"
listing=1
junit=""
prev=""
for a in "$@"; do
[ "$a" = "--show-only=json-v1" ] && json_requested=1
[ "$a" = "-N" ] && listing_requested=1
if [ "$prev" = "--output-junit" ]; then junit="$a"; fi
if [ "$prev" = "-R" ]; then selector="$a"; fi
prev="$a"
done
listing_requested="${listing_requested:-0}"
# E1's CPU-only boundary probes use their own testcase results. Keep observations
# inside each testcase so stdout-only and stale evidence remain rejected.
if [[ "${selector:-}" == *RemoteWaitBoundaryControl* ]]; then
python3 - "${mode}" "${junit}" "${selector}" <<'PY'
import os, re, sys
import xml.etree.ElementTree as ET
mode, path, selector = sys.argv[1:]
barrier = int(os.environ['MOBILEGL_IPC_VERB_BARRIER'])
negative = barrier == 0
prefix = 'RemoteWaitBoundaryControl.'
specs = [
('AppliedClassWaitsWithRunAheadCap', 'ApplierReset', 1, True,
'E1 wait boundary: ApplierReset returned before apply'),
('MissingServerCapKeepsClearLockstep', 'Clear', 0, True,
'E1 wait boundary: Clear without server cap returned before apply'),
('ServerCapAllowsClearToRunAhead', 'Clear', 1, False, ''),
]
root = ET.Element('testsuite')
failures = 0
for index, (short, op, cap, waits, diagnostic) in enumerate(specs):
name = prefix + short
if not re.search(selector, name):
continue
if mode == 'e1-empty' or (negative and mode == 'missing-selection' and index == 1):
continue
case = ET.SubElement(root, 'testcase', name=name, status='run')
waited = int(waits and barrier == 1)
observation = (f'E1 observation: op={op} cap={cap} barrier={barrier} parked={waited} '
f'applied_before_return={waited} emitted=1 peer_seen=1')
failed = negative and mode != 'green'
if mode == 'e1-red-baseline' and 'baseline' in path:
failed = True
if mode == 'e1-restore-red' and 'restored' in path:
failed = True
if mode == 'e1-waitall' and not waits:
failed = True
observation = observation.replace('parked=0', 'parked=1').replace('applied_before_return=0', 'applied_before_return=1')
if negative and mode == 'skipped-selection':
ET.SubElement(case, 'skipped')
failed = False
if negative and mode == 'notrun-selection':
case.set('status', 'notrun')
failed = False
if failed:
ET.SubElement(case, 'failure', message='control red')
failures += 1
text = observation + '\n' + (diagnostic if negative else '')
if negative:
if mode == 'unrelated' or mode == 'wrong-fatal':
text = observation + '\nUNRELATED_CONTROL_FAILURE'
elif mode == 'missing-fatal' or (mode == 'partial-fatal' and index == 1):
text = diagnostic
elif mode == 'stdout-fatal':
print(text) # Must not qualify as this testcase's own evidence.
text = 'UNRELATED_CONTROL_FAILURE'
elif mode == 'stale-fatal':
text = text.replace('barrier=0', 'barrier=1')
elif mode == 'e1-no-peer':
text = text.replace('peer_seen=1', 'peer_seen=0')
elif mode == 'e1-no-emit':
text = text.replace('emitted=1', 'emitted=0')
ET.SubElement(case, 'system-out').text = text
ET.ElementTree(root).write(path, encoding='utf-8', xml_declaration=True)
print(f'E1 stub: {len(root)} selected, {failures} failed')
sys.exit(8 if failures else 0)
PY
exit $?
fi
emit_listing() {
echo "Test project /stub"
echo " Test #1: DirectGLES.Split.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels"
if [ "${mode}" = partial-fatal ]; then
echo " Test #2: DirectGLES.Split.TriangleScenario.SecondEntry"
echo "Total Tests: 2"
else
echo "Total Tests: 1"
fi
}
write_junit() {
if [ "${MOBILEGL_IPC_VERB_BARRIER:-1}" = 0 ] || [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
entry=DirectGLES.Split.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels
[ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" != 0 ] || entry=DirectGLES.Split.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw
body="<testcase name=\"${entry}\" status=\"fail\"><failure message=\"control red\"/></testcase>"
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
case "${mode}" in
evidence|e3-no-private)
body="<testcase name=\"${entry}\" status=\"fail\"><failure/><system-out>the SECOND write through the same mapping, announced by nothing</system-out></testcase>" ;;
esac
fi
if [ "${mode}" = partial-fatal ] && [ "${MOBILEGL_IPC_VERB_BARRIER:-1}" = 0 ]; then
body="${body}<testcase name=\"DirectGLES.Split.TriangleScenario.SecondEntry\" status=\"fail\"><failure/></testcase>"
fi
case "${mode}" in
e3-skipped-selection)
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
body="<testcase name=\"${entry}\" status=\"notrun\"><skipped/></testcase>"
fi ;;
skipped-selection) body="<testcase name=\"${entry}\" status=\"notrun\"><skipped/></testcase>" ;;
notrun-selection) body="<testcase name=\"${entry}\" status=\"notrun\"/>" ;;
missing-selection) body='' ;;
green) body="<testcase name=\"${entry}\" status=\"run\"/>" ;;
esac
printf '%s\n' "<testsuite>${body}</testsuite>" > "$1"
return
fi
case "${mode}" in
red-baseline)
body='<testcase name="DirectGLES.Split.TriangleScenario.AVboBackedTriangleReachesReadPixels" status="failed"><failure message="already red"/></testcase>'
;;
all-skipped)
body='<testcase name="DirectGLES.Split.TriangleScenario.AVboBackedTriangleReachesReadPixels" status="notrun"><skipped/></testcase>'
;;
*)
body='<testcase name="DirectGLES.Split.TriangleScenario.AVboBackedTriangleReachesReadPixels" status="run" time="0.3"/>'
;;
esac
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' "<testsuite name=\"stub\">" " ${body}" '</testsuite>' > "$1"
}
# Model the library file sink separately from ctest stdout (ID-53).
#
# P6: the real sink writes one file per ROLE, `<base>.client.log` / `<base>.server.log`, and the
# validator derives those from the ENV base. So the manifest below keeps the base names (what the
# lane sets) while the on-disk writes here land in the CLIENT file - which is the role that raises
# these markers in a real run.
log="${CONTROL_TMPDIR}/entry.client.log"
if [ "${json_requested:-0}" = 1 ]; then
python3 -c 'import json, os; p=os.environ["CONTROL_TMPDIR"]; entries=[("ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels", "entry.log"), ("PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw", "pmap.log")]; entries += [("TriangleScenario.SecondEntry", "second.log")] if os.environ["STUB_MODE"] == "partial-fatal" else []; print(json.dumps({"tests": [{"name": "DirectGLES.Split."+n, "properties": [{"name": "LABELS", "value": ["integration-split"]}, {"name": "ENVIRONMENT", "value": ["MOBILEGL_LOG_FILE_PATH="+p+"/"+f]}]} for n,f in entries]}))'
if [ "${mode}" = stale-fatal ]; then
echo 'Fatal{BarrierViolation, "DrawVbo"}' > "${log}"
fi
exit 0
fi
if [ "${listing_requested}" = "1" ]; then
emit_listing
exit 0
fi
if [ -n "${junit}" ]; then
write_junit "${junit}"
if [ "${MOBILEGL_IPC_VERB_BARRIER:-1}" != 0 ] && [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" != 0 ]; then
case "${mode}" in
red-baseline) echo "1/1 Test #1: ... ***Failed"; exit 8 ;;
*) echo "100% tests passed, 0 tests failed out of 1"; exit 0 ;;
esac
fi
fi
# The control's own run.
if [ "${MOBILEGL_IPC_VERB_BARRIER:-1}" = 0 ]; then
case "${mode}" in
evidence|e3-unrelated|e3-no-private|skipped-selection|e3-skipped-selection|notrun-selection|missing-selection|partial-fatal) echo 'Fatal{BarrierViolation, "DrawVbo"}' > "${log}" ;;
wrong-fatal) echo 'Fatal{ReplyMissing, "DrawVbo"}' > "${log}" ;;
missing-fatal) echo "library setup only; no fatal" > "${log}" ;;
stdout-fatal) echo 'Fatal{BarrierViolation, "DrawVbo"}' ;;
esac
fi
# E3(a)'s library line, in the PERSISTENT-MAP entry's own private file - a different file from
# E1's, exactly as the manifest above declares. `e3-no-private` is the mode that leaves it out:
# the pixel assertion arrives, the library says nothing, and the control must refuse the red.
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ] && [ "${mode}" = evidence ]; then
echo 'MGPipe: persistent-map push disabled - MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 is exit gate E3(a)'"'"'s NEGATIVE CONTROL' \
> "${CONTROL_TMPDIR}/pmap.client.log"
fi
case "${mode}" in
e3-skipped-selection)
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
echo 'selected E3 entry ... ***Skipped'
exit 0
fi
exit 8
;;
skipped-selection|notrun-selection|missing-selection)
echo '1/1 Test #1: selected entry ... ***Skipped'
echo '100% tests passed, 0 tests failed out of 1'
exit 0
;;
e3-no-private)
# E1's half passes (its private Fatal is written above); E3(a)'s pixel assertion arrives on
# stdout and its private line does not, so the control must stop at the second half.
echo "1/1 Test #1: DirectGLES.Split.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels ...***Failed"
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
echo "../MobileGL/MG_IntegrationTest/Scenarios/PersistentCoherentMapScenario.cpp:414: Failure"
echo "the SECOND write through the same mapping, announced by nothing: this is exit gate E3(b)"
fi
exit 8
;;
unrelated|missing-fatal|stdout-fatal|stale-fatal|e3-unrelated|partial-fatal|wrong-fatal)
echo "1/1 Test #1: DirectGLES.Split.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels ...***Failed"
echo "UNRELATED_CONTROL_FAILURE: the harness aborted in setup before the knob was read"
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
case "${mode}" in
missing-fatal|stdout-fatal|stale-fatal)
echo "the SECOND write through the same mapping, announced by nothing" ;;
esac
fi
exit 8
;;
evidence)
# E1 is file-only above; E3(a) emits its scenario assertion to ctest.
echo "1/1 Test #1: DirectGLES.Split.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels ...***Failed"
echo "../MobileGL/MG_IntegrationTest/Scenarios/ClearThenReadPixelsScenario.cpp:290: Failure"
echo "Expected: (bottom.r) > (200), actual: '\\0' vs 200"
echo "the SECOND write through the same mapping, announced by nothing: this is exit gate E3(b)"
exit 8
;;
green)
echo "100% tests passed, 0 tests failed out of 1"
exit 0
;;
*)
echo "stub_ctest: unknown STUB_MODE '${mode}'" >&2
exit 127
;;
esac
@@ -1,105 +0,0 @@
#!/usr/bin/env python3
# MobileGL - scripts/ci/wait_boundary_negative_control.py
# Copyright (c) 2025-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
"""E1: green/red/restored-green proof of real ClientSession wait boundaries.
The peer holds apply until the actual transport parks or the emit call returns.
VERB_BARRIER=0 must fail only the two owed waits; ordinary run-ahead draws need
not fail. Each arm is checked from its own CTest JUnit testcase, never a global
grep, stale library log, arbitrary nonzero exit, or timing-dependent pixel.
"""
import argparse
import os
from pathlib import Path
import re
import subprocess
import sys
import xml.etree.ElementTree as ET
PREFIX = "RemoteWaitBoundaryControl."
CASES = {
"AppliedClassWaitsWithRunAheadCap": ("ApplierReset", 1, True,
"E1 wait boundary: ApplierReset returned before apply"),
"MissingServerCapKeepsClearLockstep": ("Clear", 0, True,
"E1 wait boundary: Clear without server cap returned before apply"),
"ServerCapAllowsClearToRunAhead": ("Clear", 1, False, ""),
}
def own_negative(text, expected_observation, diagnostic):
return expected_observation in text and diagnostic in text
def inspect(path, selected, barrier, negative, rc):
root = ET.parse(path).getroot()
cases = root.findall(".//testcase")
names = [case.get("name") for case in cases]
if len(names) != len(set(names)) or set(names) != set(selected):
raise ValueError("selected testcase names are missing, duplicated or unexpected")
for case in cases:
name = case.get("name")
if case.find("skipped") is not None or case.get("status") in {"notrun", "disabled"}:
raise ValueError(f"{name}: selected testcase skipped or did not run")
failed = case.find("failure") is not None or case.find("error") is not None
op, cap, waits, diagnostic = CASES[name.removeprefix(PREFIX)]
waited = int(waits and barrier == 1)
observation = (f"E1 observation: op={op} cap={cap} barrier={barrier} parked={waited} "
f"applied_before_return={waited} emitted=1 peer_seen=1")
text = "\n".join(case.itertext())
if negative:
if not failed or rc == 0:
raise ValueError(f"{name}: disabled barrier did not fail the owed wait")
if not own_negative(text, observation, diagnostic):
raise ValueError(f"{name}: red lacks its own observed wait-boundary failure")
elif failed or rc != 0 or observation not in text:
raise ValueError(f"{name}: baseline/restored arm did not execute its green boundary proof")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--ctest", default=os.environ.get("CTEST", "ctest"))
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
args.out.mkdir(parents=True, exist_ok=True)
all_cases = [PREFIX + name for name in CASES]
negative_cases = [PREFIX + name for name, (_, _, waits, _) in CASES.items() if waits]
ok = True
for arm, barrier, negative, selected in (
("baseline", 1, False, all_cases),
("control", 0, True, negative_cases),
("restored", 1, False, all_cases),
):
# Never run a negative arm against a failed baseline. A failed negative
# still proceeds through restoration and must not consume stale XML.
if arm == "control" and not ok:
continue
result = args.out / f"e1-{arm}.xml"
result.unlink(missing_ok=True)
env = os.environ.copy()
env["MOBILEGL_IPC_VERB_BARRIER"] = str(barrier)
selector = "^(" + "|".join(re.escape(name) for name in selected) + ")$"
command = [args.ctest, "-L", "unit", "-R", selector, "--no-tests=error",
"--output-on-failure", "--output-junit", str(result)]
try:
run = subprocess.run(command, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, timeout=90)
(args.out / f"e1-{arm}.out").write_text(run.stdout, encoding="utf-8")
print(run.stdout, end="", flush=True)
inspect(result, selected, barrier, negative, run.returncode)
print(f"E1 {arm}: {'EXPECTED_RED' if negative else 'GREEN'} ({len(selected)} cases)", flush=True)
except (OSError, ValueError, ET.ParseError, subprocess.TimeoutExpired) as exc:
print(f"E1 FAILED: {arm}: {exc}", file=sys.stderr, flush=True)
ok = False
if ok:
print("negative control E1: owed waits failed by their own observed boundary assertions; baseline and restoration green")
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())
+16 -17
View File
@@ -3845,6 +3845,7 @@
"DirectGLES.Spawn.PartialUploadOverGpuContentScenario.TwoDistantSubImagesKeepTheGpuClearBetweenThem",
"DirectGLES.Spawn.PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"DirectGLES.Spawn.PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares",
"DirectGLES.Spawn.PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer",
"DirectGLES.Spawn.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"DirectGLES.Spawn.PipelineFailureScenario.SamplerArrayInAStructDrawsWithoutKillingTheProcess",
"DirectGLES.Spawn.PipelineFailureScenario.TheSameDrawRepeatsIdenticallyWithNoPoisonedPipelineCache",
@@ -4571,6 +4572,7 @@
"DirectGLES.Split.PartialUploadOverGpuContentScenario.TwoDistantSubImagesKeepTheGpuClearBetweenThem",
"DirectGLES.Split.PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"DirectGLES.Split.PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares",
"DirectGLES.Split.PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer",
"DirectGLES.Split.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"DirectGLES.Split.PersistentMapArm.PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares",
"DirectGLES.Split.PipelineFailureScenario.SamplerArrayInAStructDrawsWithoutKillingTheProcess",
@@ -4636,6 +4638,7 @@
"DirectGLES.Split.SmallRing.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"DirectGLES.Split.SmallRing.TriangleScenario.AVboBackedTriangleReachesReadPixels",
"DirectGLES.Split.SmallRing.TriangleScenario.TheSameVboAndVaoRedrawAcrossAFrameBoundary",
@@ -5256,6 +5259,7 @@
"DirectGLES.Tcp.PartialUploadOverGpuContentScenario.TwoDistantSubImagesKeepTheGpuClearBetweenThem",
"DirectGLES.Tcp.PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"DirectGLES.Tcp.PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares",
"DirectGLES.Tcp.PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer",
"DirectGLES.Tcp.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"DirectGLES.Tcp.PipelineFailureScenario.SamplerArrayInAStructDrawsWithoutKillingTheProcess",
"DirectGLES.Tcp.PipelineFailureScenario.TheSameDrawRepeatsIdenticallyWithNoPoisonedPipelineCache",
@@ -7907,6 +7911,7 @@
"DirectGLES.Split.PartialUploadOverGpuContentScenario.TwoDistantSubImagesKeepTheGpuClearBetweenThem",
"DirectGLES.Split.PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"DirectGLES.Split.PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares",
"DirectGLES.Split.PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer",
"DirectGLES.Split.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"DirectGLES.Split.PersistentMapArm.PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares",
"DirectGLES.Split.PipelineFailureScenario.SamplerArrayInAStructDrawsWithoutKillingTheProcess",
@@ -7968,6 +7973,7 @@
"DirectGLES.Split.SmallRing.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"DirectGLES.Split.SmallRing.TriangleScenario.AVboBackedTriangleReachesReadPixels",
"DirectGLES.Split.SmallRing.TriangleScenario.TheSameVboAndVaoRedrawAcrossAFrameBoundary",
@@ -9128,6 +9134,7 @@
"DirectGLES.Split.PartialUploadOverGpuContentScenario.TwoDistantSubImagesKeepTheGpuClearBetweenThem",
"DirectGLES.Split.PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"DirectGLES.Split.PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares",
"DirectGLES.Split.PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer",
"DirectGLES.Split.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"DirectGLES.Split.PersistentMapArm.PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares",
"DirectGLES.Split.PipelineFailureScenario.SamplerArrayInAStructDrawsWithoutKillingTheProcess",
@@ -9189,6 +9196,7 @@
"DirectGLES.Split.SmallRing.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"DirectGLES.Split.SmallRing.TriangleScenario.AVboBackedTriangleReachesReadPixels",
"DirectGLES.Split.SmallRing.TriangleScenario.TheSameVboAndVaoRedrawAcrossAFrameBoundary",
@@ -9365,6 +9373,7 @@
"PipeInputsTest.AFrontendMutationInsideAVerbDoesNotDivergeAtRead",
"PipeInputsTest.BadVerifyCorruptKnobIsFatalNamingTheKnob",
"PipeInputsTest.CorruptedSnapshotFieldIsNamedWithItsSerial",
"PipeInputsTest.EveryCorruptedFieldIsNamedByTheComparator",
"PipeInputsTest.MutatedFieldIsNamedAtRead",
"PipeInputsTest.VerifyCorruptKnobNamesTheFieldAtEntry",
"PipeInputsTest.VerifyFatalOffLogsTheDivergenceAndContinues",
@@ -10494,13 +10503,12 @@
"PipeCatalogue.WireOpcodesAreThePositionsInTheCatalogue",
"PipeInputsTest.AFrontendMutationInsideAVerbDoesNotDivergeAtRead",
"PipeInputsTest.AFrontendMutationInsideAVerbRefreshesThePushedField",
"PipeInputsTest.BadPoisonOmitKnobIsFatalNamingTheKnob",
"PipeInputsTest.BadVerifyCorruptKnobIsFatalNamingTheKnob",
"PipeInputsTest.CorruptedSnapshotFieldIsNamedWithItsSerial",
"PipeInputsTest.EveryCorruptedFieldIsNamedByTheComparator",
"PipeInputsTest.EveryVerbFillsItsClassAndNothingElse",
"PipeInputsTest.MutatedFieldIsNamedAtRead",
"PipeInputsTest.OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale",
"PipeInputsTest.PoisonOmitKnobArmsTheOmission",
"PipeInputsTest.ReadingAFilledFieldCompletes",
"PipeInputsTest.ReadingAnOmittedFieldAbortsNamingTheVerb",
"PipeInputsTest.ReadingBeforeAnyFillAbortsNamingNoVerb",
@@ -11189,9 +11197,9 @@
"RemoteRunAhead.AnEventBurstBehindAnUnwaitedSequenceFlowControlsInsteadOfAborting",
"RemoteRunAhead.AnIndexedDrawVerbIsUnbarrieredAndTouchesPipeInputsNotAtAll",
"RemoteRunAhead.AnUnbarrieredVerbDoesNotFillPipeInputsAndIsAllowed",
"RemoteRunAhead.ClearingTheVerbBarrierDisarmsRunAheadEvenWithTheCapBit",
"RemoteRunAhead.OnlyTheTextureHalfOfResourceSubDataWantsItsReply",
"RemoteRunAhead.TheKnobAloneDoesNotArmRunAheadWithoutTheServersCapBit",
"RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait",
"RemoteWaitBoundaryControl.AppliedClassWaitsWithRunAheadCap",
"RemoteWaitBoundaryControl.MissingServerCapKeepsClearLockstep",
"RemoteWaitBoundaryControl.ServerCapAllowsClearToRunAhead",
@@ -11606,7 +11614,6 @@
"SplitBufferSet.AFaultedMapTheDrawDidNotBindStillShipsBeforeTheDrawThatDoes",
"SplitBufferSet.AShadowThatFailsThePageGranularTestKeepsTheInwardAlignmentAndItsEdges",
"SplitBufferSet.ASubPageMapIsOneProtectedPageAndAWriteThroughItFaults",
"SplitBufferSet.AZeroBlockSizeTurnsThePushOffRatherThanMakingItUnlimited",
"SplitBufferSet.BothEdgesOfAWriteMapPublishOneStateRecord",
"SplitBufferSet.MembershipIsSyncPersistentMappedRangesOwnEarlyOutChain",
"SplitBufferSet.Row0EverySsboBindingPointIsMarkedByADraw",
@@ -12960,15 +12967,6 @@
"names_from": "junit-integration-monolith-control-monolith/integration",
"optional": "runs only when build-linux-split builds its own library, or the split lane's logs do not prove the IPC defaults"
},
"junit-integration-split/control": {
"skipped": [],
"names": [
"DirectGLES.Split.PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"DirectGLES.Split.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw"
]
},
"junit-integration-split/spawn-baseline": {
"skipped": [
"DirectGLES.Spawn.ClipDistanceScenario.ADisabledClipDistanceRemovesNothing",
@@ -13430,6 +13428,7 @@
"DirectGLES.Spawn.PartialUploadOverGpuContentScenario.TwoDistantSubImagesKeepTheGpuClearBetweenThem",
"DirectGLES.Spawn.PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"DirectGLES.Spawn.PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares",
"DirectGLES.Spawn.PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer",
"DirectGLES.Spawn.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"DirectGLES.Spawn.PipelineFailureScenario.SamplerArrayInAStructDrawsWithoutKillingTheProcess",
"DirectGLES.Spawn.PipelineFailureScenario.TheSameDrawRepeatsIdenticallyWithNoPoisonedPipelineCache",
@@ -14124,6 +14123,7 @@
"DirectGLES.Tcp.PartialUploadOverGpuContentScenario.TwoDistantSubImagesKeepTheGpuClearBetweenThem",
"DirectGLES.Tcp.PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"DirectGLES.Tcp.PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares",
"DirectGLES.Tcp.PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer",
"DirectGLES.Tcp.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"DirectGLES.Tcp.PipelineFailureScenario.SamplerArrayInAStructDrawsWithoutKillingTheProcess",
"DirectGLES.Tcp.PipelineFailureScenario.TheSameDrawRepeatsIdenticallyWithNoPoisonedPipelineCache",
@@ -14357,6 +14357,7 @@
"PipeInputsTest.AFrontendMutationInsideAVerbDoesNotDivergeAtRead",
"PipeInputsTest.BadVerifyCorruptKnobIsFatalNamingTheKnob",
"PipeInputsTest.CorruptedSnapshotFieldIsNamedWithItsSerial",
"PipeInputsTest.EveryCorruptedFieldIsNamedByTheComparator",
"PipeInputsTest.MutatedFieldIsNamedAtRead",
"PipeInputsTest.VerifyCorruptKnobNamesTheFieldAtEntry",
"PipeInputsTest.VerifyFatalOffLogsTheDivergenceAndContinues",
@@ -15929,7 +15930,6 @@
"SplitBufferSet.AFaultedMapTheDrawDidNotBindStillShipsBeforeTheDrawThatDoes",
"SplitBufferSet.AShadowThatFailsThePageGranularTestKeepsTheInwardAlignmentAndItsEdges",
"SplitBufferSet.ASubPageMapIsOneProtectedPageAndAWriteThroughItFaults",
"SplitBufferSet.AZeroBlockSizeTurnsThePushOffRatherThanMakingItUnlimited",
"SplitBufferSet.AnOlderWritebackCannotRetireANewerGpuWriteMark",
"SplitBufferSet.BothEdgesOfAWriteMapPublishOneStateRecord",
"SplitBufferSet.MembershipIsSyncPersistentMappedRangesOwnEarlyOutChain",
@@ -16799,13 +16799,12 @@
"PipeCatalogue.WireOpcodesAreThePositionsInTheCatalogue",
"PipeInputsTest.AFrontendMutationInsideAVerbDoesNotDivergeAtRead",
"PipeInputsTest.AFrontendMutationInsideAVerbRefreshesThePushedField",
"PipeInputsTest.BadPoisonOmitKnobIsFatalNamingTheKnob",
"PipeInputsTest.BadVerifyCorruptKnobIsFatalNamingTheKnob",
"PipeInputsTest.CorruptedSnapshotFieldIsNamedWithItsSerial",
"PipeInputsTest.EveryCorruptedFieldIsNamedByTheComparator",
"PipeInputsTest.EveryVerbFillsItsClassAndNothingElse",
"PipeInputsTest.MutatedFieldIsNamedAtRead",
"PipeInputsTest.OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale",
"PipeInputsTest.PoisonOmitKnobArmsTheOmission",
"PipeInputsTest.ReadingAFilledFieldCompletes",
"PipeInputsTest.ReadingAnOmittedFieldAbortsNamingTheVerb",
"PipeInputsTest.ReadingBeforeAnyFillAbortsNamingNoVerb",
@@ -17418,7 +17417,6 @@
"SplitBufferSet.AFaultedMapTheDrawDidNotBindStillShipsBeforeTheDrawThatDoes",
"SplitBufferSet.AShadowThatFailsThePageGranularTestKeepsTheInwardAlignmentAndItsEdges",
"SplitBufferSet.ASubPageMapIsOneProtectedPageAndAWriteThroughItFaults",
"SplitBufferSet.AZeroBlockSizeTurnsThePushOffRatherThanMakingItUnlimited",
"SplitBufferSet.AnOlderWritebackCannotRetireANewerGpuWriteMark",
"SplitBufferSet.BothEdgesOfAWriteMapPublishOneStateRecord",
"SplitBufferSet.MembershipIsSyncPersistentMappedRangesOwnEarlyOutChain",
@@ -19419,6 +19417,7 @@
"PipeInputsTest.AFrontendMutationInsideAVerbDoesNotDivergeAtRead",
"PipeInputsTest.BadVerifyCorruptKnobIsFatalNamingTheKnob",
"PipeInputsTest.CorruptedSnapshotFieldIsNamedWithItsSerial",
"PipeInputsTest.EveryCorruptedFieldIsNamedByTheComparator",
"PipeInputsTest.MutatedFieldIsNamedAtRead",
"PipeInputsTest.VerifyCorruptKnobNamesTheFieldAtEntry",
"PipeInputsTest.VerifyFatalOffLogsTheDivergenceAndContinues",
+5 -5
View File
@@ -460,11 +460,11 @@ stacks before treating a local result as a statement about CI.
`run_tcp_matrix.py` uses a CTest JSON catalog plus the **current** `trace_cases.json`.
It does not build, install an APK, or launch the remote server. For the formal
P6.5 run-ahead matrix, use `--require-run-ahead`. It forces `MOBILEGL_IPC_RUN_AHEAD=1`
and `MOBILEGL_IPC_VERB_BARRIER=1`, then requires `run-ahead ARMED` in each private
client log and rejects any `running lockstep` / `DISARMED` marker. The checkpoint
records `requested_arm` and `actual_arm`; strict resume cannot reuse an earlier
lockstep image pass. Start the intended server first, then run serially:
P6.5 run-ahead matrix, use `--require-run-ahead`. It forces `MOBILEGL_IPC_RUN_AHEAD=1`,
then requires `run-ahead ARMED` in each private client log and rejects any
`running lockstep` / `DISARMED` marker. The checkpoint records `requested_arm` and
`actual_arm`; strict resume cannot reuse an earlier lockstep image pass. Start the intended
server first, then run serially:
```bash
ctest --test-dir /path/to/retrace-build --show-only=json-v1 > /tmp/trace-catalog.json
+1 -1
View File
@@ -59,7 +59,7 @@ def measure(args, case, credit, work):
env = dict(os.environ, MOBILEGL_TRANSPORT='spawn', MOBILEGL_IPC_CONTROL=args.endpoint,
MOBILEGL_IPC_DATA='stream', MOBILEGL_IPC_TOKEN=args.token, MOBILEGL_IPC_REQUIRE_SAME_BUILD='1',
MOBILEGL_IPC_LOG_FORWARD='1', MOBILEGL_PIPE_STATS='1', MOBILEGL_PIPE_STATS_PERIOD='1',
MOBILEGL_IPC_PRESENT_CREDIT=str(credit), MOBILEGL_IPC_RUN_AHEAD='1', MOBILEGL_IPC_VERB_BARRIER='1',
MOBILEGL_IPC_PRESENT_CREDIT=str(credit), MOBILEGL_IPC_RUN_AHEAD='1',
MOBILEGL_IPC_BATCH_WAITS='1', MOBILEGL_BACKEND_TYPE='DirectGLES',
MOBILEGL_IPC_STRICT_ERRORS='0', MOBILEGL_IPC_ROLE_SPLIT_STATE='0', MOBILEGL_IPC_AUDIT='0')
for name in ('MOBILEGL_IPC_DIAL', 'MOBILEGL_IPC_SERVER_PATH'):
+2 -2
View File
@@ -58,13 +58,13 @@ def run(args, case, transport, repeat):
out.mkdir(parents=True)
env = dict(os.environ, MOBILEGL_TRANSPORT='spawn', MOBILEGL_BACKEND_TYPE='DirectGLES',
MOBILEGL_IPC_SERVER_PATH=str(args.server), MOBILEGL_PIPE_STATS='1', MOBILEGL_PIPE_STATS_PERIOD='1',
MOBILEGL_IPC_PRESENT_CREDIT='1', MOBILEGL_IPC_RUN_AHEAD='1', MOBILEGL_IPC_VERB_BARRIER='1',
MOBILEGL_IPC_PRESENT_CREDIT='1', MOBILEGL_IPC_RUN_AHEAD='1',
MOBILEGL_IPC_BATCH_WAITS='1', MOBILEGL_IPC_SPIN_US='50', MOBILEGL_IPC_STRICT_ERRORS='0',
MOBILEGL_IPC_ROLE_SPLIT_STATE='0', MOBILEGL_IPC_AUDIT='0',
LIBGL_ALWAYS_SOFTWARE='1', EGL_PLATFORM='surfaceless',
MESA_GL_VERSION_OVERRIDE='3.3', MESA_GLSL_VERSION_OVERRIDE='330')
for key in ('MOBILEGL_IPC_CONTROL', 'MOBILEGL_IPC_DATA', 'MOBILEGL_IPC_TOKEN', 'MOBILEGL_IPC_DIAL',
'MOBILEGL_IPC_REQUIRE_SAME_BUILD', 'MOBILEGL_TEST_SERVER_COUNTERS'):
'MOBILEGL_IPC_REQUIRE_SAME_BUILD'):
env.pop(key, None)
if transport == 'tcp':
env.update(MOBILEGL_IPC_CONTROL=args.endpoint, MOBILEGL_IPC_DATA='stream', MOBILEGL_IPC_TOKEN='',
@@ -36,7 +36,7 @@ def run(args):
MOBILEGL_IPC_REQUIRE_SAME_BUILD='1', MOBILEGL_IPC_LOG_FORWARD='1',
MOBILEGL_BACKEND_TYPE='DirectGLES', MOBILEGL_PIPE_STATS='1',
MOBILEGL_PIPE_STATS_PERIOD='1', MOBILEGL_IPC_RUN_AHEAD='1',
MOBILEGL_IPC_VERB_BARRIER='1', MOBILEGL_IPC_PRESENT_CREDIT='1',
MOBILEGL_IPC_PRESENT_CREDIT='1',
MOBILEGL_LOG_FILE_PATH=str(args.out / 'mobilegl.log'), EGL_PLATFORM='surfaceless')
for name in ('MOBILEGL_IPC_DIAL', 'MOBILEGL_IPC_SERVER_PATH'):
os.environ.pop(name, None)
+2 -2
View File
@@ -118,11 +118,11 @@ def run(args, transport, case, repeat):
env = dict(os.environ, MOBILEGL_TRANSPORT=transport, MOBILEGL_IPC_SERVER_PATH=str(args.server),
MOBILEGL_PIPE_STATS='0', MOBILEGL_IPC_PRESENT_CREDIT='1', MOBILEGL_IPC_RUN_AHEAD='1',
MOBILEGL_IPC_STRICT_ERRORS='0', MOBILEGL_IPC_ROLE_SPLIT_STATE='0', MOBILEGL_IPC_AUDIT='0',
MOBILEGL_IPC_VERB_BARRIER='1', MOBILEGL_IPC_BATCH_WAITS='1', MOBILEGL_IPC_SPIN_US='50',
MOBILEGL_IPC_BATCH_WAITS='1', MOBILEGL_IPC_SPIN_US='50',
LIBGL_ALWAYS_SOFTWARE='1', EGL_PLATFORM='surfaceless',
MESA_GL_VERSION_OVERRIDE='3.3', MESA_GLSL_VERSION_OVERRIDE='330')
for key in ('MOBILEGL_IPC_CONTROL', 'MOBILEGL_IPC_DATA', 'MOBILEGL_IPC_TOKEN', 'MOBILEGL_IPC_DIAL',
'MOBILEGL_IPC_REQUIRE_SAME_BUILD', 'MOBILEGL_TEST_SERVER_COUNTERS'):
'MOBILEGL_IPC_REQUIRE_SAME_BUILD'):
env.pop(key, None)
if Path('/usr/share/glvnd/egl_vendor.d/50_mesa.json').exists():
env['__EGL_VENDOR_LIBRARY_FILENAMES'] = '/usr/share/glvnd/egl_vendor.d/50_mesa.json'
+1 -1
View File
@@ -140,7 +140,7 @@ def start_server(args, device):
command = [sys.executable, str(script), 'start', '--serial', args.serial,
'--package', args.package, '--listen', args.listen, '--token', args.token,
'--allow-idle', '--state-file', str(args.server_idle_state), '--env',
'MOBILEGL_TEST_SERVER_COUNTERS=1;MOBILEGL_PIPE_STATS=1;MOBILEGL_PIPE_STATS_PERIOD=1;'
'MOBILEGL_PIPE_STATS=1;MOBILEGL_PIPE_STATS_PERIOD=1;'
'MOBILEGL_LOG_FILE_PATH=/data/data/' + args.package + '/files/mgl.log']
result = subprocess.run(command, check=True, capture_output=True, text=True, timeout=30)
(args.out / 'server-start.txt').write_text(result.stdout + result.stderr, encoding='utf-8')
+2 -3
View File
@@ -140,7 +140,6 @@ def case_environment(test, base, args, output=None):
MOBILEGL_IPC_PRESENT_CREDIT=str(args.credit))
if getattr(args, "require_run_ahead", False):
env["MOBILEGL_IPC_RUN_AHEAD"] = "1"
env["MOBILEGL_IPC_VERB_BARRIER"] = "1"
if output is not None:
env["MOBILEGL_LOG_FILE_PATH"] = str(output / "output/mobilegl.log")
return env
@@ -406,7 +405,7 @@ def run_case(plan, args, row):
work = Path(row["work"])
console = work.parent / "runner.log"
env = case_environment(plan["test"], args.base_env, args, work)
row["requested_arm"] = {key: env.get(key) for key in ("MOBILEGL_IPC_RUN_AHEAD", "MOBILEGL_IPC_VERB_BARRIER")}
row["requested_arm"] = {key: env.get(key) for key in ("MOBILEGL_IPC_RUN_AHEAD",)}
props = properties(plan["test"])
cwd = props.get("WORKING_DIRECTORY", str(args.source))
def wake():
@@ -456,7 +455,7 @@ def main(argv=None):
parser.add_argument("--credit", type=int, choices=(1, 2, 3), default=2)
parser.add_argument("--resume", action="store_true")
parser.add_argument("--require-run-ahead", action="store_true",
help="require private-client ARMED proof, no lockstep fallback/DISARMED; force RUN_AHEAD=1 and VERB_BARRIER=1")
help="require private-client ARMED proof, no lockstep fallback/DISARMED; force RUN_AHEAD=1")
parser.add_argument("--idle-seconds", type=float, default=300)
parser.add_argument("--max-seconds", type=float, default=7200)
parser.add_argument("--wake-adb-serial", help="explicit opt-in: KEYCODE_WAKEUP every 15 seconds while a case runs")
+4 -4
View File
@@ -237,8 +237,8 @@ endif()
# * it carries none of the three MGPipe Fatals: Fatal{PipeVerifyDiffer (a push/pull divergence,
# the thing the mode exists to find), Fatal{UnmigratedPipeInput (a backend read of a field the
# verb's fill table does not list - fixed by adding the row to MG_Pipe/FillPoints.def, never by
# marking the field sticky), and Fatal{PipeVerifyBadKnob (a misspelt MOBILEGL_PIPE_VERIFY_CORRUPT
# or MOBILEGL_PIPE_POISON_OMIT). The third is in the regex on purpose even though D2 makes it
# marking the field sticky), and Fatal{PipeVerifyBadKnob (a misspelt MOBILEGL_PIPE_VERIFY_CORRUPT).
# The third is in the regex on purpose even though D2 makes it
# abort the process: with MOBILEGL_PIPE_VERIFY_FATAL=0 the abort is exactly what does not
# happen, and a typo'd knob would otherwise leave the negative-control lane looking healthy.
# The Fatal check is not redundant with the replay's exit status: MOBILEGL_PIPE_VERIFY_FATAL=0 is
@@ -286,8 +286,8 @@ if(DEFINED ENV{MOBILEGL_PIPE_VERIFY} AND NOT "$ENV{MOBILEGL_PIPE_VERIFY}" STREQU
"divergence and is recorded, not silenced; Fatal{UnmigratedPipeInput, \"<Field>@<Verb>\"} "
"is a missing row in MG_Pipe/FillPoints.def's class table - add it, regenerate, rerun "
"(never mark the field sticky); Fatal{PipeVerifyBadKnob, ...} is a misspelt "
"MOBILEGL_PIPE_VERIFY_CORRUPT / MOBILEGL_PIPE_POISON_OMIT - fix the spelling, the "
"vocabularies are kMGPipeInputFieldNames[] and kMGPipeVerbNames[].")
"MOBILEGL_PIPE_VERIFY_CORRUPT - fix the spelling, the vocabulary is "
"kMGPipeInputFieldNames[].")
endif()
message(STATUS "MGPipe verify: ${pipe_verify_case} armed, zero divergences, zero unmigrated reads")
endif()
+5 -5
View File
@@ -37,7 +37,7 @@ if behavior!='no-result':
actual=work/'output/actual.png';actual.write_bytes(b'fixture-image')
endpoint=os.environ['MOBILEGL_IPC_CONTROL'].removeprefix('tcp://')
arm=os.environ.get('CASE_ARM','armed')
if arm=='respect-env':arm='armed' if os.environ.get('MOBILEGL_IPC_RUN_AHEAD')=='1' and os.environ.get('MOBILEGL_IPC_VERB_BARRIER')=='1' else 'lockstep'
if arm=='respect-env':arm='armed' if os.environ.get('MOBILEGL_IPC_RUN_AHEAD')=='1' else 'lockstep'
line={'armed':'run-ahead ARMED\n','lockstep':'running lockstep\n','disarmed':'run-ahead ARMED\nrun-ahead DISARMED\n','server-only':''}[arm]
(work/'output/mobilegl.client.log').write_text('control=tcp data=stream server='+endpoint+' pid=123 dial=connect\n'+line)
if arm=='server-only':(work/'output/mobilegl.server.log').write_text('run-ahead ARMED\n')
@@ -116,11 +116,11 @@ class MatrixTests(unittest.TestCase):
test['properties'][0]['value'] += list(entries)
self.catalog.write_text(json.dumps(catalog))
def test_required_arm_forces_both_environment_knobs_and_records_actual_arm(self):
self.set_arm('respect-env',['MOBILEGL_IPC_RUN_AHEAD=0','MOBILEGL_IPC_VERB_BARRIER=0'])
def test_required_arm_forces_the_run_ahead_knob_and_records_actual_arm(self):
self.set_arm('respect-env',['MOBILEGL_IPC_RUN_AHEAD=0'])
self.assertEqual(self.run_main('--require-run-ahead'),0)
row=self.checkpoint()[0]
self.assertEqual(row['requested_arm'],{'MOBILEGL_IPC_RUN_AHEAD':'1','MOBILEGL_IPC_VERB_BARRIER':'1'})
self.assertEqual(row['requested_arm'],{'MOBILEGL_IPC_RUN_AHEAD':'1'})
self.assertEqual(row['actual_arm'],{'armed':True,'lockstep':False,'disarmed':False})
self.assertEqual(self.run_main('--require-run-ahead','--resume'),0)
self.assertEqual(len(self.checkpoint()),1)
@@ -337,7 +337,7 @@ class MatrixTests(unittest.TestCase):
def test_directvulkan_run_ahead_proof_and_checkpoint_keys_are_per_backend(self):
self.write_inputs(backends=('DirectGLES','DirectVulkan'))
self.set_arm('respect-env',['MOBILEGL_IPC_RUN_AHEAD=0','MOBILEGL_IPC_VERB_BARRIER=0'])
self.set_arm('respect-env',['MOBILEGL_IPC_RUN_AHEAD=0'])
self.assertEqual(self.run_main('--backend','DirectGLES','--backend','DirectVulkan',
'--require-run-ahead'),0)
rows=self.checkpoint()
+1 -1
View File
@@ -259,7 +259,7 @@ class RunTraceCaseTests(unittest.TestCase):
'a leg whose transport block was skipped must fail the checker')
# MGTRACE_EXPECT_IPC: the client's IPC line must carry every token, whole.
IPC = 'Config: IPC ring=8MiB verb-barrier=1 run-ahead={ra} present-credit=1 strict={st} audit=0 role-split-state={rs} x'
IPC = 'Config: IPC ring=8MiB run-ahead={ra} present-credit=1 strict={st} audit=0 role-split-state={rs} x'
def ipc_logs(self, ra='1', st='0', rs='1', line=True):
logs = good_logs('spawn')