mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[MG_Backend, MG_Impl, MG_Remote, MG_Test, MG_IntegrationTest, CI] (Disaggregated): P5f f1 - the dual-block rehearsal: per-role PipeInputs, the MOBILEGL_IPC_ROLE_SPLIT_STATE knob, and the named-Fatal arm that makes the red a census
This commit is contained in:
@@ -1341,6 +1341,70 @@ jobs:
|
||||
fi
|
||||
echo "integration-split under strict is GREEN, two-sided, with the arming control present"
|
||||
|
||||
# ---- P5f (f1), P5F-WIRE-COMPLETENESS.md §4: THE DUAL-BLOCK CENSUS LANE ---------------
|
||||
#
|
||||
# The same integration-split entries (plus Magma's own lane), re-run under
|
||||
# MOBILEGL_IPC_ROLE_SPLIT_STATE=1: the client's residual fill writes a client-role block
|
||||
# and the backend reads a server-role one, so every BARRIER-PULLED read aborts with a
|
||||
# named Fatal{UnmigratedPipeInput, "<field>@<verb>"}. THE RED IS THE CENSUS - it is the
|
||||
# machine-checked list of what still depends on the two roles sharing one object, and it
|
||||
# is ratcheted two-sided against Harness/dualblock-expected-fatals.txt: a new pair is a
|
||||
# debt somebody now owes, a vanished pair must be removed in the commit that retired it.
|
||||
#
|
||||
# THE STEP ALSO FAILS WHEN THE LANE IS GREEN while the expected file still has rows -
|
||||
# "green" would mean the knob stopped arming, which is exactly the decay ID-122 assigns
|
||||
# to a control rather than to a lane's good mood. At P5f's exit the file is empty and the
|
||||
# green arm is the correct one; delete the red expectation in the same commit.
|
||||
- name: Integration-split under the dual-block rehearsal - expected red, census ratcheted (P5f f1)
|
||||
working-directory: build-split
|
||||
env:
|
||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||
MOBILEGL_TRANSPORT: "inproc"
|
||||
MOBILEGL_IPC_ROLE_SPLIT_STATE: "1"
|
||||
MOBILEGL_IPC_STRICT_ERRORS: "1"
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
helper="${GITHUB_WORKSPACE}/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py"
|
||||
expected="${GITHUB_WORKSPACE}/MobileGL/MG_IntegrationTest/Harness/dualblock-expected-fatals.txt"
|
||||
|
||||
ctest -L 'integration-split|integration-magma-split' --show-only=json-v1 \
|
||||
> "${RUNNER_TEMP}/dualblock-lane.json"
|
||||
|
||||
# Stale logs from an earlier step would be read as this run's evidence (ID-119's rule).
|
||||
python3 - "${RUNNER_TEMP}/dualblock-lane.json" "${helper}" <<'PY'
|
||||
import importlib.util, json, os, sys
|
||||
spec = importlib.util.spec_from_file_location("slp", sys.argv[2])
|
||||
slp = importlib.util.module_from_spec(spec); spec.loader.exec_module(slp)
|
||||
logs = slp.marker_log_paths(json.load(open(sys.argv[1])))
|
||||
for path in set(logs.values()):
|
||||
try:
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
pass
|
||||
print(f"reset {len(set(logs.values()))} private log(s) across {len(logs)} entries")
|
||||
PY
|
||||
|
||||
rc=0
|
||||
ctest -L 'integration-split|integration-magma-split' --no-tests=error -j 4 \
|
||||
--output-junit "${RUNNER_TEMP}/dualblock.xml" \
|
||||
> "${RUNNER_TEMP}/dualblock-split.txt" 2>&1 || rc=$?
|
||||
tail -5 "${RUNNER_TEMP}/dualblock-split.txt"
|
||||
echo "dual-block census run: rc=${rc}"
|
||||
|
||||
rows=$(grep -cvE '^\s*(#|$)' "${expected}")
|
||||
if [ "${rc}" -eq 0 ] && [ "${rows}" -gt 0 ]; then
|
||||
echo "::error::the dual-block census lane went GREEN with ${rows} expected pair(s) outstanding - the knob stopped arming, or the fields retired and dualblock-expected-fatals.txt was not emptied in the same commit"
|
||||
exit 1
|
||||
fi
|
||||
python3 "${helper}" expect-fatal "${RUNNER_TEMP}/dualblock-lane.json" \
|
||||
"${RUNNER_TEMP}/dualblock.xml" "${expected}"
|
||||
|
||||
# The mechanism's own negative control (P5F §6): the same named case must be green with
|
||||
# the knob on and red - on its own assertion text - with it off.
|
||||
- name: Dual-block negative control (P5f f1, P5F section 6)
|
||||
working-directory: build-split
|
||||
run: bash "${GITHUB_WORKSPACE}/scripts/ci/dualblock_negative_control.sh"
|
||||
|
||||
# ---- P5e (gl), CONTRACT-P5E §7: MAGMA'S OWN LANE, EXPECTED RED -----------------------
|
||||
#
|
||||
# The two DirectVulkan.Split.NamedBlit entries abort on GetFramebufferBindingSlot@Clear
|
||||
|
||||
@@ -535,6 +535,17 @@ namespace MobileGL::MG_Config {
|
||||
// build, the seven sticky forwards that are otherwise exempt - from "count it in
|
||||
// rsp" to Fatal (R-7.3).
|
||||
Bool StrictErrors = false;
|
||||
// MOBILEGL_IPC_ROLE_SPLIT_STATE (P5f f1, P5F-WIRE-COMPLETENESS.md §4): the dual-block
|
||||
// rehearsal. 1 = the client's residual fill writes a CLIENT-ROLE PipeInputs block and
|
||||
// the backend/applier keep reading the SERVER-ROLE one, so every path that today works
|
||||
// only because the two roles share one object turns into a named
|
||||
// Fatal{UnmigratedPipeInput, "<field>@<verb>"} instead of a silent cross-role read.
|
||||
// Meaningless under monolith transport (the two roles are one thread there, so the
|
||||
// selection folds to the single shared block) and forced off by MOBILEGL_PIPE_VERIFY
|
||||
// (the comparator owns the one fill block it compares against). 0 is not merely the
|
||||
// default, it is the negative control: the lane's distinctness case must go red
|
||||
// without it (P5F §6).
|
||||
Bool RoleSplitState = false;
|
||||
// MOBILEGL_IPC_AUDIT: after a record retires, the server fills the SEG_STAGE bytes
|
||||
// it referenced with 0xDD (R-2.5). This is the ONLY mechanical control that an
|
||||
// inproc implementation did not quietly keep using a pointer past its lifetime.
|
||||
|
||||
@@ -393,17 +393,28 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
ipc.StrictErrors = QueryEnvFlag("MOBILEGL_IPC_STRICT_ERRORS");
|
||||
ipc.Audit = QueryEnvFlag("MOBILEGL_IPC_AUDIT");
|
||||
QueryEnvVariable("MOBILEGL_IPC_SERVER_AFFINITY", ipc.ServerAffinity, "auto");
|
||||
// P5f f1: the dual-block rehearsal (P5F-WIRE-COMPLETENESS.md §4). Forced OFF by the
|
||||
// verify harness: the comparator's entry compare and compare-at-read hook are built on
|
||||
// there being ONE filled block (the hook pins itself to &gPipeInputs,
|
||||
// PipeFill.cpp's MGPipeVerifyReadHook), which is exactly what the rehearsal splits.
|
||||
ipc.RoleSplitState = QueryEnvFlag("MOBILEGL_IPC_ROLE_SPLIT_STATE");
|
||||
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;
|
||||
}
|
||||
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
|
||||
// One line, on the arm where these numbers decide behaviour, because every one of
|
||||
// them is a number a bug report has to quote.
|
||||
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB spin=%uus persistent-block=%uKiB "
|
||||
"adopt-tier=%u verb-barrier=%u run-ahead=%u present-credit=%u strict=%d "
|
||||
"audit=%d affinity='%s'",
|
||||
"audit=%d role-split-state=%d affinity='%s'",
|
||||
ipc.RingMb, ipc.StageMb, ipc.SpinUs, ipc.PersistentBlockKb, ipc.AdoptTier,
|
||||
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit,
|
||||
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
|
||||
ipc.ServerAffinity.c_str());
|
||||
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str());
|
||||
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 "
|
||||
|
||||
@@ -160,6 +160,20 @@ namespace MobileGL::MG_Pipe {
|
||||
// MGPipeBarrierPullAdmitted is generated from those three tables (ID-116); there is no
|
||||
// list here to drift.
|
||||
void CountBarrierPull(MGPipeInputField field, MGPipeVerb verb) {
|
||||
// P5f (f1), P5F-WIRE-COMPLETENESS.md §4: UNDER THE DUAL-BLOCK REHEARSAL EVERY
|
||||
// BARRIER-PULLED READ IS A NAMED FATAL, unconditionally - ahead of the barriered
|
||||
// question, ahead of the strict knob, ahead of the counter. The server block has no
|
||||
// residual fill to answer from, so the value the read would return is not "torn"
|
||||
// or "stale" but ABSENT: the field's stamp was withdrawn by the server's own verb
|
||||
// boundary and nothing on this side of the role split ever writes it. Letting the
|
||||
// read proceed would hand the backend default storage - or, for the O-class rows
|
||||
// with no null check, an empty SharedPtr to dereference - which is an UNNAMED
|
||||
// crash exactly where the rehearsal exists to produce a named one. The red is the
|
||||
// census (Harness/dualblock-expected-fatals.txt), so the marker keeps the strict
|
||||
// grammar with the knob's own name as the reason.
|
||||
if (MGPipeRoleSplitActive()) {
|
||||
StrictBarrierPullFatal(field, verb, "MOBILEGL_IPC_ROLE_SPLIT_STATE=1");
|
||||
}
|
||||
if (!MGPipeApplierCurrentRecordIsBarriered()) {
|
||||
StrictBarrierPullFatal(field, verb, "UNBARRIERED, the client did not fill it");
|
||||
}
|
||||
@@ -260,8 +274,50 @@ namespace MobileGL::MG_Pipe {
|
||||
static void SetServerStamped(PipeInputs& inputs, Bool stamped) {
|
||||
inputs.m_serverStampedVerb = stamped;
|
||||
}
|
||||
// P5f (f1): the server block's identity half of the stamp door (CONTRACT-P5E §3.2).
|
||||
// The client's SetIdentity lives in MG_Impl's MGPipeFillAccess and writes the CLIENT
|
||||
// block now; this one is how the server block learns which served context it describes
|
||||
// without MG_Impl being in the process at all.
|
||||
static void SetIdentity(PipeInputs& inputs, Bool live, const void* identity) {
|
||||
inputs.m_live = live;
|
||||
inputs.m_contextIdentity = identity;
|
||||
}
|
||||
};
|
||||
|
||||
// ---- P5f (f1): the dual-block rehearsal's selection functions (PipeInputs.h) -----------
|
||||
//
|
||||
// MGPipeRoleSplitActive is deliberately NOT latched: it is two global loads, asked once per
|
||||
// verb on the fill side and once per verb boundary on the stamp side, and a latch is a
|
||||
// second thing a test that flips the knob mid-process would have to know about.
|
||||
Bool MGPipeRoleSplitActive() {
|
||||
return MG_Config::Ipc.RoleSplitState &&
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith;
|
||||
}
|
||||
|
||||
PipeInputs& MGPipeClientInputs() {
|
||||
return MGPipeRoleSplitActive() ? gPipeInputsClientBlock : gPipeInputs;
|
||||
}
|
||||
|
||||
void MGPipeClientClearVerbBoundary() {
|
||||
MGPipeStampAccess::SetServerStamped(MGPipeClientInputs(), false);
|
||||
}
|
||||
|
||||
void MGPipeServerBlockNoteIdentity() {
|
||||
if (!MGPipeRoleSplitActive()) return;
|
||||
// The server cannot name the client's GLContext - under a real transport it is in
|
||||
// another process - so the identity the server block carries is the server's OWN
|
||||
// served-context clock: MGPipeApplierContextSerial moves exactly when the served
|
||||
// context does (MGPipeApplierReset), which is all the backends' per-context memo keys
|
||||
// ask of an identity. The odd-encoding keeps the token non-null at serial 0: a null
|
||||
// identity reads as a hit against DirectGLES' zero-initialised fb-slot memo cache and
|
||||
// hands out a null slot - an unnamed crash where the rehearsal exists to produce a
|
||||
// named one.
|
||||
const Uint64 serial = MGPipeApplierContextSerial();
|
||||
MGPipeStampAccess::SetIdentity(gPipeInputs, true,
|
||||
reinterpret_cast<const void*>(static_cast<std::uintptr_t>(
|
||||
(serial << 1) | Uint64{1})));
|
||||
}
|
||||
|
||||
void MGPipeServerStampVerbBoundary(MGPipeVerb verb) {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
// P5e (gl), ID-115: THE POSITIVE CONTROL'S ONLY HONEST SIGNAL, and it belongs HERE
|
||||
@@ -302,6 +358,11 @@ namespace MobileGL::MG_Pipe {
|
||||
filled.FilledGen[i] = serial & (Uint64{0} - bit);
|
||||
}
|
||||
MGPipeStampAccess::SetServerStamped(inputs, true);
|
||||
// P5f (f1): the server block's identity rides the stamp (CONTRACT-P5E §3.2's
|
||||
// "SetIdentity moves to ApplyOne" - the stamp is the per-verb half of ApplyOne, and
|
||||
// refreshing here rather than once at Attach keeps the token in step with
|
||||
// MGPipeApplierContextSerial across a context switch). A no-op with the rehearsal off.
|
||||
MGPipeServerBlockNoteIdentity();
|
||||
}
|
||||
|
||||
void MGPipeServerClearVerbBoundary() { MGPipeStampAccess::SetServerStamped(gPipeInputs, false); }
|
||||
|
||||
@@ -834,6 +834,61 @@ namespace MobileGL::MG_Pipe {
|
||||
// (P3a; the exit-time heap corruption this closes is p3a-results/exit-order-v1.md.)
|
||||
inline PipeInputs& gPipeInputs = *new PipeInputs();
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ============================================================================
|
||||
// P5f (f1), P5F-WIRE-COMPLETENESS.md §4: THE DUAL-BLOCK REHEARSAL
|
||||
// ============================================================================
|
||||
//
|
||||
// One gPipeInputs served both roles because the verb barrier (R-1) made "the client filled
|
||||
// it" and "the server is reading it" mutually exclusive in TIME on the same object. The
|
||||
// rehearsal removes the same-object half of that sentence: under
|
||||
// MOBILEGL_IPC_ROLE_SPLIT_STATE=1 the client's residual fill writes the CLIENT block below
|
||||
// and the backend/applier keep reading gPipeInputs, which is the SERVER block. Every path
|
||||
// that worked only because the two were one object - a BARRIER-PULLED field, a sticky
|
||||
// forward's read of the fill side's stamps - then has no value to read, and the read
|
||||
// becomes a NAMED Fatal{UnmigratedPipeInput, "<field>@<verb>"} (CountBarrierPull's
|
||||
// dual-block arm, PipeInputs.cpp) instead of a silent cross-role answer.
|
||||
//
|
||||
// THE READ SIDE'S SPELLING DOES NOT MOVE. gPipeInputs remains the server-role block, so the
|
||||
// 379 MGB_CTX sites and PipeApply.cpp's applier writes are untouched; the only new spelling
|
||||
// is on the fill side (MG_Impl/Pipe/PipeFill.cpp), which asks MGPipeClientInputs(). Under
|
||||
// monolith transport - every unit and integration-gpu lane of a split build - the selection
|
||||
// folds back to the single shared block and behaviour is byte-for-byte the old one.
|
||||
//
|
||||
// Same leak-at-exit storage as gPipeInputs above, for the same reason.
|
||||
inline PipeInputs& gPipeInputsClientBlock = *new PipeInputs();
|
||||
|
||||
// PipeInputs.cpp. Whether the rehearsal is armed: the knob AND a real transport (and not
|
||||
// the verify build, which ConfigLoader forces off). Constant for the life of the process.
|
||||
Bool MGPipeRoleSplitActive();
|
||||
// PipeInputs.cpp. THE FILL SIDE'S ONE NEW SPELLING: the client block when the rehearsal is
|
||||
// armed, gPipeInputs otherwise. Everything in MG_Impl/Pipe/PipeFill.cpp that used to spell
|
||||
// gPipeInputs spells this instead.
|
||||
PipeInputs& MGPipeClientInputs();
|
||||
// PipeInputs.cpp. The client-role half of MGPipeServerClearVerbBoundary: clears the stamp
|
||||
// flag on the FILL side's block. With the rehearsal off that IS gPipeInputs, so the two
|
||||
// client call sites keep their old semantics exactly; with it on the client block's flag is
|
||||
// never raised (nothing server-stamps it) and the clear is a no-op - which is the point:
|
||||
// the client no longer reaches into the server's block at all.
|
||||
void MGPipeClientClearVerbBoundary();
|
||||
// PipeInputs.cpp. CONTRACT-P5E §3.2's other half: the server block's identity is
|
||||
// SERVER-OWNED. Called from PipeApplier::Attach and from MGPipeServerStampVerbBoundary;
|
||||
// with the rehearsal armed it sets m_live and points m_contextIdentity at a token derived
|
||||
// from MGPipeApplierContextSerial() - the server's own served-context clock, which is what
|
||||
// moves when the served context does. Without it the server block's ContextIdentity() would
|
||||
// stay nullptr, and DirectGLES' fb-slot memo cache compares identity FIRST (a nullptr
|
||||
// against its own nullptr initialiser reads as a hit and hands out a null slot): an
|
||||
// unnamed crash where the rehearsal exists to produce a named one. A no-op with the
|
||||
// rehearsal off, so the client's per-verb SetIdentity keeps owning the shared block there.
|
||||
void MGPipeServerBlockNoteIdentity();
|
||||
#else
|
||||
// The push-without-transport build has one role and one block, so the fill side's spelling
|
||||
// folds onto gPipeInputs and PipeFill.cpp reads identically in both build flavours. An
|
||||
// inline that no caller in such a build ever has a reason to call twice - the disaggregated
|
||||
// arm above is the real one.
|
||||
inline PipeInputs& MGPipeClientInputs() { return gPipeInputs; }
|
||||
#endif
|
||||
|
||||
// Every field has storage or is forwarded, and nothing else.
|
||||
#define MGP_INPUT_COUNT_ONE(Field, Member) +1
|
||||
static_assert(0 MGP_INPUT_STORAGE_LIST(MGP_INPUT_COUNT_ONE) + kMGPipeForwardedFieldCount == kMGPipeInputFieldCount,
|
||||
|
||||
@@ -624,9 +624,9 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
|
||||
void ReportDivergence(MGPipeInputField field, const char* where) {
|
||||
const Uint64 serial = MGPipeFillAccess::Filled(gPipeInputs).CurrentVerbSerial;
|
||||
const Uint64 serial = MGPipeFillAccess::Filled(MGPipeClientInputs()).CurrentVerbSerial;
|
||||
MGLOG_F("MGPipe: Fatal{PipeVerifyDiffer, \"%s@%s\", verb=%llu, where=%s}",
|
||||
kMGPipeInputFieldNames[static_cast<SizeT>(field)], MGPipeVerbName(gPipeInputs.CurrentVerb()),
|
||||
kMGPipeInputFieldNames[static_cast<SizeT>(field)], MGPipeVerbName(MGPipeClientInputs().CurrentVerb()),
|
||||
static_cast<unsigned long long>(serial), where);
|
||||
if (g_verify.Fatal) std::abort();
|
||||
g_verify.Divergences.fetch_add(1, std::memory_order_relaxed);
|
||||
@@ -650,7 +650,7 @@ namespace MobileGL::MG_Pipe {
|
||||
void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask) {
|
||||
auto* ctx = LiveContext();
|
||||
MGPipeFillAccess::SetIdentity(snapshot, ctx);
|
||||
MGPipeFillAccess::SetVerb(snapshot, gPipeInputs.CurrentVerb());
|
||||
MGPipeFillAccess::SetVerb(snapshot, MGPipeClientInputs().CurrentVerb());
|
||||
if (ctx == nullptr) return;
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
@@ -700,7 +700,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// filled must stay Fatal{UnmigratedPipeInput} on the next read rather than be healed by
|
||||
// an unrelated frontend write.
|
||||
void MGPipeNoteFrontendMutation(MGPipeInputField field) {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
PipeInputs& inputs = MGPipeClientInputs();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5e (ra, CONTRACT-P5E §3.1): THIS REFRESHES ONE FIELD OF THE LAST FILL, so with no
|
||||
// fill behind it there is nothing to refresh. Under run-ahead the last verb may have
|
||||
@@ -1214,7 +1214,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// fill only for UNBARRIERED records); an unbarriered apply may not read it at all, and
|
||||
// §3.3's detector is what says so by name. Clearing the stamps as well would trade that
|
||||
// named abort for the poison Fatal, which names the field but not the rule it broke.
|
||||
void MGPipeReleaseResidualFillPins() { MGPipeFillAccess::ReleaseObjectPins(gPipeInputs); }
|
||||
void MGPipeReleaseResidualFillPins() { MGPipeFillAccess::ReleaseObjectPins(MGPipeClientInputs()); }
|
||||
|
||||
// P5e (ra), CONTRACT-P5E §2.5. Declared in PipeMutation.h; see there for why it is not a
|
||||
// ClientSession call at the GL entry point.
|
||||
@@ -2202,7 +2202,7 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint32 MGPipePendingBaseInstance() { return MGPipeTrackerInstance().PendingBaseInstance(); }
|
||||
|
||||
void MGPipeLeaveVerb() {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
PipeInputs& inputs = MGPipeClientInputs();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5e (ra, §3.1): a verb whose fill was skipped has no stamps of this thread's to
|
||||
// retire, and the bump below would move a serial the SERVER's stamp owns. Leave
|
||||
@@ -2225,7 +2225,7 @@ namespace MobileGL::MG_Pipe {
|
||||
++MGPipeFillAccess::Filled(inputs).CurrentVerbSerial;
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MGPipeServerClearVerbBoundary();
|
||||
MGPipeClientClearVerbBoundary();
|
||||
#endif
|
||||
MGPipeFillAccess::SetVerb(inputs, MGPipeVerb::kVerbCount);
|
||||
// The pending base instance belongs to the verb that was about to run, so leaving
|
||||
@@ -3013,7 +3013,7 @@ namespace MobileGL::MG_Pipe {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (ClientRunsAhead()) return sizeof(MGPVertexAttribDefaults) + header.Count * sizeof(MGPAttribValue);
|
||||
#endif
|
||||
const auto* mirror = MGPipeFillAccess::VertexAttribDefaultsOf(gPipeInputs);
|
||||
const auto* mirror = MGPipeFillAccess::VertexAttribDefaultsOf(MGPipeClientInputs());
|
||||
Bool reproduced = true;
|
||||
for (SizeT i = 0; i < kAttribs && reproduced; ++i) {
|
||||
if ((header.Mask & (Uint32{1} << static_cast<Uint32>(i))) == 0) continue;
|
||||
@@ -3024,7 +3024,7 @@ namespace MobileGL::MG_Pipe {
|
||||
MGLOG_W_ONCE("MGPipe: MGPipeApplySetVertexAttribDefaults did not reproduce the "
|
||||
"carried three views on this build - the client is keeping "
|
||||
"m_currentVertexAttribute authoritative");
|
||||
MGPipeFillAccess::CopyField(gPipeInputs, ctx, MGPipeInputField::GetCurrentVertexAttribute);
|
||||
MGPipeFillAccess::CopyField(MGPipeClientInputs(), ctx, MGPipeInputField::GetCurrentVertexAttribute);
|
||||
}
|
||||
return sizeof(MGPVertexAttribDefaults) + header.Count * sizeof(MGPAttribValue);
|
||||
}
|
||||
@@ -3345,7 +3345,10 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
// ---- the validate point (P2 brief D1) ----
|
||||
void MGPipeValidateForVerb(MGPipeVerb verb) {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
// P5f (f1): the fill side's one new spelling. With MOBILEGL_IPC_ROLE_SPLIT_STATE=1 this
|
||||
// is the CLIENT-role block and gPipeInputs is the server's alone; off, or under
|
||||
// monolith transport, it folds back onto gPipeInputs and nothing below changes.
|
||||
PipeInputs& inputs = MGPipeClientInputs();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ---- P5e (ra), CONTRACT-P5E §3: WHO OWNS gPipeInputs FOR THIS VERB ----------------
|
||||
//
|
||||
@@ -3434,7 +3437,14 @@ namespace MobileGL::MG_Pipe {
|
||||
// while the apply thread is inside a record that depends on it. For an unbarriered
|
||||
// verb the stamp is not touched at all, and the applier's own LeaveApplier is then
|
||||
// its only writer.
|
||||
MGPipeServerClearVerbBoundary();
|
||||
//
|
||||
// P5f (f1): this is now the CLIENT block's flag (MGPipeClientClearVerbBoundary).
|
||||
// With the rehearsal off both names denote the one shared block and the semantics
|
||||
// above are unchanged; with it on the client block's flag is never raised and the
|
||||
// clear is a no-op, and withdrawing the SERVER's stamp is the applier's job alone
|
||||
// (PipeApplier::LeaveApplier) - which is CONTRACT-P5E §3.2's per-role stamp
|
||||
// ownership, landed as the dual block rather than as moved fields.
|
||||
MGPipeClientClearVerbBoundary();
|
||||
#endif
|
||||
MGPipeFillAccess::SetVerb(inputs, verb);
|
||||
}
|
||||
|
||||
@@ -2496,3 +2496,58 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_STRICT_ARMING_ENVIRONMENT}"
|
||||
)
|
||||
endif()
|
||||
|
||||
# =====================================================================================
|
||||
# P5f (f1), P5F-WIRE-COMPLETENESS.md §4/§6: THE DUAL-BLOCK REHEARSAL'S OWN LANE
|
||||
# =====================================================================================
|
||||
#
|
||||
# The lane has TWO halves, and only one of them registers entries here:
|
||||
#
|
||||
# - the CENSUS half is an environment-variable lane, exactly like strict: the gate runs the
|
||||
# EXISTING `integration-split` entries under MOBILEGL_IPC_ROLE_SPLIT_STATE=1 +
|
||||
# MOBILEGL_IPC_STRICT_ERRORS=1, and every BARRIER-PULLED read there aborts with a named
|
||||
# Fatal{UnmigratedPipeInput, "<field>@<verb>"} - the red IS the census, collected and
|
||||
# ratcheted against Harness/dualblock-expected-fatals.txt by split_log_paths.py's
|
||||
# `expect-fatal` mode. Re-registering the whole set under a second prefix would double the
|
||||
# discovered test count for zero new information.
|
||||
#
|
||||
# - the CONTROL half is the one entry below: DualBlockScenario is the mechanism's negative
|
||||
# control (§6: turning the knob OFF must turn a named case from green to red), so it needs
|
||||
# an entry of its own, under a label of its own.
|
||||
#
|
||||
# THE LABEL IS `integration-dualblock-split`, and the spelling is load-bearing twice over
|
||||
# (ID-131): `ctest -L` is a REGEX, so a label CONTAINING `integration-split` as a substring
|
||||
# would be selected by the main lane's runs - and this one is expected-red there, which must
|
||||
# never poison the hard-green gate. Checked both directions: `-L integration-split` does not
|
||||
# match it (`dualblock` stands between the two words), and `-L integration-dualblock-split`
|
||||
# matches nothing else in the tree. It does not carry `integration-gpu` either: that label must
|
||||
# be able to go green, and this entry goes green only under the armed knob.
|
||||
#
|
||||
# THE KNOB IS NOT PINNED IN THE ENTRY'S ENVIRONMENT, deliberately, and that is the ONE deviation
|
||||
# from the N-6 pinning rule every other split entry follows: a ctest ENVIRONMENT property
|
||||
# overrides the process environment, so pinning MOBILEGL_IPC_ROLE_SPLIT_STATE=1 here would make
|
||||
# the knob=0 form - which IS the negative control - unreachable through ctest. The failure
|
||||
# direction is safe: a bare `-L integration-dualblock-split` run without the knob is RED by
|
||||
# name, never silently green.
|
||||
#
|
||||
# MOBILEGL_IPC_RUN_AHEAD=0 IS pinned: the serial-separation half asserts on the fill side's
|
||||
# behaviour per verb boundary, and under run-ahead an unbarriered verb fills nothing, which
|
||||
# would make the assertion timing-dependent instead of deterministic.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
target_sources(MobileGLIntegrationTest PRIVATE
|
||||
Scenarios/DualBlockScenario.cpp Harness/DualBlockPeek.cpp)
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_DUALBLOCK_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_TRANSPORT=inproc"
|
||||
"MOBILEGL_IPC_SERVER_PATH=${MGL_ITEST_SPLIT_SERVER_PATH}" "MGITEST_SPLIT_LANE=1"
|
||||
"MOBILEGL_IPC_RUN_AHEAD=0"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.Split.DualBlock."
|
||||
TEST_FILTER "DualBlockScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-dualblock-split"
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_DUALBLOCK_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/split-logs/DirectGLES.Split.DualBlock.TheTwoRolesHaveDistinctBlocks.log"
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/DualBlockPeek.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "DualBlockPeek.h"
|
||||
|
||||
#if defined(MGITEST_SPLIT_RUNTIME_PEEK) && !defined(__ANDROID__)
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
// The serials live behind FilledState(), which exists only where the poison build flag is on -
|
||||
// and PipeInputs.h derives that flag, so the question can only be asked AFTER the include. A
|
||||
// disaggregated build always has it (MOBILEGL_BUILD_DISAGGREGATED is one of its three arms).
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
#define MGITEST_DUAL_BLOCK_PEEK_LIVE 1
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
#if defined(MGITEST_DUAL_BLOCK_PEEK_LIVE)
|
||||
// The serials live behind the poison build's FilledState(); the peek is live exactly in
|
||||
// the configurations where MOBILEGL_PIPE_POISON is (a disaggregated build always has it,
|
||||
// which is what the #if above says), so an answer here is never a different build's guess.
|
||||
bool PeekDualBlockState(DualBlockPeekState* out) {
|
||||
if (out == nullptr) return false;
|
||||
namespace MGP = MobileGL::MG_Pipe;
|
||||
out->peekAvailable = true;
|
||||
out->roleSplitActive = MGP::MGPipeRoleSplitActive();
|
||||
MGP::PipeInputs& client = MGP::MGPipeClientInputs();
|
||||
out->blocksDistinct = &client != &MGP::gPipeInputs;
|
||||
out->clientSerial = client.FilledState().CurrentVerbSerial;
|
||||
out->serverSerial = MGP::gPipeInputs.FilledState().CurrentVerbSerial;
|
||||
out->serverIdentity = MGP::gPipeInputs.ContextIdentity();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DualBlockDriveClientVerbBoundary() {
|
||||
// ReadPixels: a verb whose class mask carries the pixel-pack state the scenario dirties
|
||||
// (so the emission step has a record to publish and the fixture's wire-moved assertion
|
||||
// is fed), while the read_pixels VERB RECORD itself is never emitted - that is the GL
|
||||
// entry point's job, not the validate point's - so nothing here reaches the server's
|
||||
// stamp path (Clear / DrawVbo / ReadPixels / Blit) and the server block's serial is
|
||||
// provably not this call's to move.
|
||||
MobileGL::MG_Pipe::MGPipeValidateForVerb(MobileGL::MG_Pipe::MGPipeVerb::ReadPixels);
|
||||
MobileGL::MG_Pipe::MGPipeLeaveVerb();
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
bool PeekDualBlockState(DualBlockPeekState*) { return false; }
|
||||
bool DualBlockDriveClientVerbBoundary() { return false; }
|
||||
#endif
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,54 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/DualBlockPeek.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// P5f (f1), P5F-WIRE-COMPLETENESS.md §4/§6: THE DUAL-BLOCK REHEARSAL, READ FROM THE RUNNING
|
||||
// PROCESS. The lane's negative control is a scenario that is green exactly when the two roles
|
||||
// hold two DISTINCT PipeInputs blocks, and "the blocks are distinct" is a fact about pointers
|
||||
// and serials inside the library - so, like SplitRuntimePeek, the reading is taken from the
|
||||
// process and never inferred from the environment the entry was launched with.
|
||||
//
|
||||
// A SEPARATE TRANSLATION UNIT for PipeApplyPeek's reason, verbatim: the scenario sources
|
||||
// include the GL headers with prototypes and PipeInputs.h is not meant to meet them in one
|
||||
// file.
|
||||
//
|
||||
// PeekDualBlockState returns false, touching nothing, where it cannot look: a build without
|
||||
// MOBILEGL_BUILD_DISAGGREGATED (the block split is compiled there only), or Android, where
|
||||
// this module links the shipping libMobileGL.so built -fvisibility=hidden. A caller that gets
|
||||
// false has learned NOTHING and must skip, not fail.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
struct DualBlockPeekState {
|
||||
// False wherever the peek cannot look (see the header above). Everything else is
|
||||
// meaningful only when this is true.
|
||||
bool peekAvailable = false;
|
||||
// MGPipeRoleSplitActive(): the knob AND a real transport. The control's red half turns
|
||||
// on this answer alone.
|
||||
bool roleSplitActive = false;
|
||||
// &MGPipeClientInputs() != &gPipeInputs - the fill side and the read side are two
|
||||
// different objects.
|
||||
bool blocksDistinct = false;
|
||||
// The two blocks' CurrentVerbSerial. The client block's advances with the fill side's
|
||||
// verb boundaries; the server block's moves only on a server verb stamp.
|
||||
unsigned long long clientSerial = 0;
|
||||
unsigned long long serverSerial = 0;
|
||||
// gPipeInputs.ContextIdentity(): non-null once the server has stamped a verb boundary
|
||||
// under the rehearsal (CONTRACT-P5E §3.2's server-owned identity).
|
||||
const void* serverIdentity = nullptr;
|
||||
};
|
||||
|
||||
bool PeekDualBlockState(DualBlockPeekState* out);
|
||||
|
||||
// Drives ONE client-side verb boundary - MGPipeValidateForVerb + MGPipeLeaveVerb - with no
|
||||
// draw behind it: exactly the fill side's write shape (serial bump, fill, verb set), and
|
||||
// nothing that stamps the server block. Returns false where the peek cannot look.
|
||||
bool DualBlockDriveClientVerbBoundary();
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,30 @@
|
||||
# The Fatal{UnmigratedPipeInput} pairs the dual-block lane ACTUALLY PRODUCES under
|
||||
# MOBILEGL_IPC_ROLE_SPLIT_STATE=1 + MOBILEGL_IPC_STRICT_ERRORS=1, one `<field>@<verb>` per line.
|
||||
#
|
||||
# P5f (f1), P5F-WIRE-COMPLETENESS.md §4. This is the strict-expected-markers.txt shape turned
|
||||
# around: the strict lane ratchets ADMITTED pulls (debts a phase has agreed to leave standing);
|
||||
# this file ratchets the FATAL census of the dual-block rehearsal, which is the full list of
|
||||
# "this read only ever worked because both roles shared one gPipeInputs". Every entry in the
|
||||
# census lane aborts on its FIRST such read, so the pairs below are first-blockers, and retiring
|
||||
# one advances the entry to its next one - the file shrinks package by package (fm/fs/fr/fv)
|
||||
# and P5f's exit is this file empty with the lane green.
|
||||
#
|
||||
# TWO-SIDED, same rule as the strict file (ID-119): split_log_paths.py's `expect-fatal` mode
|
||||
# fails when a pair appears that is not listed here AND when a pair listed here no longer
|
||||
# appears. Remove a row in the commit that retires it.
|
||||
#
|
||||
# HOW TO REFRESH (never by hand from a guess):
|
||||
# ctest --test-dir build-split -L 'integration-split|integration-magma-split' \
|
||||
# --show-only=json-v1 > lane.json
|
||||
# # reset the private logs, then:
|
||||
# MOBILEGL_IPC_ROLE_SPLIT_STATE=1 MOBILEGL_IPC_STRICT_ERRORS=1 \
|
||||
# ctest --test-dir build-split -L 'integration-split|integration-magma-split' -j 4 \
|
||||
# --output-junit dualblock.xml
|
||||
# python3 MobileGL/MG_IntegrationTest/Harness/split_log_paths.py expect-fatal \
|
||||
# lane.json dualblock.xml this-file
|
||||
# and put the census it prints here.
|
||||
#
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# INITIAL CENSUS (f1, measured on the WSL gate tree - see docs/Disaggregated/notes/p5f/
|
||||
# f1-report.md for the run):
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
@@ -113,6 +113,107 @@ def print_marker_table(title, table):
|
||||
print(f" {len(table[pair]):4d} {pair}")
|
||||
|
||||
|
||||
def expect_fatal(document, junit_path, expected_path):
|
||||
"""THE DUAL-BLOCK LANE'S CENSUS AND TWO-SIDED RATCHET (P5f f1, P5F §4/§6).
|
||||
|
||||
The markers mode above ratchets the strict lane's ADMITTED pairs; the dual-block lane's
|
||||
currency is the FATAL pair, because under MOBILEGL_IPC_ROLE_SPLIT_STATE=1 every
|
||||
BARRIER-PULLED read aborts unconditionally (CountBarrierPull's dual-block arm) - the red
|
||||
IS the list of fields that still read the other role's memory.
|
||||
|
||||
Three checks, all load-bearing:
|
||||
|
||||
1. EVERY failed entry with a private log carries at least one Fatal{UnmigratedPipeInput}
|
||||
marker in it. A red entry without one is an UNNAMED crash - a segfault or an abort
|
||||
from something else - which is precisely what the rehearsal must make impossible
|
||||
(the O-class fields have no null check; without the Fatal arm they die nameless).
|
||||
2. No ADMITTED marker anywhere: under the dual-block arm the Fatal fires before the
|
||||
admission question is asked, so an Admitted line means the arm did not fire.
|
||||
3. The Fatal pair set equals the committed expected file, BOTH ways - a new pair is a
|
||||
debt somebody now owes, a vanished pair means a later f package retired it and the
|
||||
file must lose the row in the same commit (the strict file's rule, ID-119).
|
||||
|
||||
A green entry needs no marker and asserts nothing; the lane turning fully green with an
|
||||
empty expected file IS the P5f exit state, which this mode then verifies rather than
|
||||
obstructs."""
|
||||
logs = marker_log_paths(document)
|
||||
cases = ET.parse(junit_path).getroot().findall(".//testcase")
|
||||
by_name = {}
|
||||
for case in cases:
|
||||
by_name.setdefault(case.get("name"), []).append(case)
|
||||
|
||||
problems = []
|
||||
fatal, admitted, escalated = {}, {}, {}
|
||||
scanned = 0
|
||||
skipped = 0
|
||||
green = 0
|
||||
red = 0
|
||||
for name, entries in sorted(by_name.items()):
|
||||
if any(c.find("skipped") is not None for c in entries):
|
||||
skipped += 1
|
||||
continue
|
||||
failed = any(c.find("failure") is not None or c.get("status") == "fail"
|
||||
for c in entries)
|
||||
if not failed:
|
||||
green += 1
|
||||
continue
|
||||
red += 1
|
||||
path = logs.get(name)
|
||||
if path is None:
|
||||
problems.append(f"{name} is RED and declares no MOBILEGL_LOG_FILE_PATH, so the "
|
||||
"red cannot be shown to be a named Fatal - an unnamed crash is "
|
||||
"exactly what this lane exists to forbid")
|
||||
continue
|
||||
if not Path(path).is_file():
|
||||
problems.append(f"{name} is RED and its private log {path} does not exist - the "
|
||||
"process died before it could write one, which is an unnamed crash")
|
||||
continue
|
||||
scanned += 1
|
||||
text = Path(path).read_text(errors="replace")
|
||||
entry_fatal = set()
|
||||
for tag, field, verb, why in MARKER_RE.findall(text):
|
||||
pair = f"{field}@{verb}"
|
||||
if tag == "Fatal":
|
||||
entry_fatal.add(pair)
|
||||
fatal.setdefault(pair, set()).add(name)
|
||||
elif (why or "").strip() == "ADMITTED-ESCALATED":
|
||||
escalated.setdefault(pair, set()).add(name)
|
||||
else:
|
||||
admitted.setdefault(pair, set()).add(name)
|
||||
if not entry_fatal:
|
||||
problems.append(f"{name} is RED but its private log carries no "
|
||||
"Fatal{UnmigratedPipeInput} marker - an unnamed failure, not a "
|
||||
"census entry")
|
||||
|
||||
print(f"SplitLogPaths expect-fatal: {len(by_name)} entrie(s) in the run - "
|
||||
f"{green} green, {skipped} skipped, {red} red ({scanned} with a private log scanned)")
|
||||
print_marker_table("Fatal{UnmigratedPipeInput", fatal)
|
||||
if admitted or escalated:
|
||||
problems.append("%d Admitted marker(s) under the dual-block knob - the unconditional "
|
||||
"Fatal arm in CountBarrierPull did not fire first: %s"
|
||||
% (len(admitted) + len(escalated),
|
||||
", ".join(sorted(set(admitted) | set(escalated)))))
|
||||
|
||||
expected = read_pair_set(expected_path)
|
||||
observed = set(fatal)
|
||||
appeared = sorted(observed - expected)
|
||||
vanished = sorted(expected - observed)
|
||||
if appeared:
|
||||
problems.append("%d Fatal pair(s) the lane has not seen before: %s. Each is a field "
|
||||
"that reads the other role's memory - add it to %s with the package "
|
||||
"that retires it, or retire it."
|
||||
% (len(appeared), ", ".join(appeared), Path(expected_path).name))
|
||||
if vanished:
|
||||
problems.append("%d expected pair(s) no longer appear: %s. Remove them from %s in the "
|
||||
"commit that retired them - an expected set that keeps rows nothing "
|
||||
"writes any more is how this lane rots green."
|
||||
% (len(vanished), ", ".join(vanished), Path(expected_path).name))
|
||||
if problems:
|
||||
raise ValueError("the dual-block lane's fatal ratchet: " + " | ".join(problems))
|
||||
print(f"SplitLogPaths expect-fatal: ratchet OK - {len(observed)} fatal pair(s), "
|
||||
f"zero admitted, every red entry named")
|
||||
|
||||
|
||||
def markers(document, selector, allowed_path, expected_path, require_no_fatal):
|
||||
"""THE TWO-SIDED RATCHET (ID-119).
|
||||
|
||||
@@ -190,6 +291,9 @@ def main():
|
||||
markers(json.loads(Path(sys.argv[2]).read_text()), sys.argv[3], sys.argv[4], sys.argv[5],
|
||||
len(sys.argv) > 6 and sys.argv[6] == "no-fatal")
|
||||
return
|
||||
if mode == "expect-fatal":
|
||||
expect_fatal(json.loads(Path(sys.argv[2]).read_text()), sys.argv[3], sys.argv[4])
|
||||
return
|
||||
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
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DualBlockScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// P5f (f1), P5F-WIRE-COMPLETENESS.md §6: THE DUAL-BLOCK MECHANISM'S NEGATIVE CONTROL, and it is
|
||||
// deterministic rather than observational. "Give the two roles one PipeInputs block each"
|
||||
// proves nothing if no case can tell one block from two, so this scenario asserts exactly that,
|
||||
// from inside the process:
|
||||
//
|
||||
// 1. MGPipeRoleSplitActive() - the knob is armed AND the transport is real;
|
||||
// 2. &MGPipeClientInputs() != &gPipeInputs - two distinct objects;
|
||||
// 3. a client verb boundary moves the CLIENT block's serial and leaves the SERVER block's
|
||||
// serial alone - the fill side's writes no longer reach the read side.
|
||||
//
|
||||
// With MOBILEGL_IPC_ROLE_SPLIT_STATE unset every one of the three fails by name - the block is
|
||||
// shared, so (2) is false by construction and (3)'s "server serial does not move" fails because
|
||||
// there is no second serial. The lane entry deliberately does NOT pin the knob in its own
|
||||
// ENVIRONMENT: the gate's dualblock step exports MOBILEGL_IPC_ROLE_SPLIT_STATE=1 for the green
|
||||
// form and scripts/ci/dualblock_negative_control.sh runs the same entry with it off for the red
|
||||
// form, and a ctest ENVIRONMENT property would override the process environment in BOTH runs and
|
||||
// make the red form unreachable. A bare `ctest -L integration-dualblock-split` without the knob
|
||||
// is red, which is the honest answer for a lane whose name promises the split.
|
||||
//
|
||||
// No draw, clear or readback: under the armed knob every BARRIER-PULLED read is a named Fatal
|
||||
// by design (that census is the lane's other half), and this case is the control - it must be
|
||||
// GREEN on the armed arm. The lane pins MOBILEGL_IPC_RUN_AHEAD=0 so the fill decision is not a
|
||||
// third variable: every verb boundary fills, deterministically.
|
||||
|
||||
#include "../Harness/DualBlockPeek.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
class DualBlockScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
const auto why = SplitRuntimeSkipReason();
|
||||
if (!why.empty()) GTEST_SKIP() << why;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(DualBlockScenario, TheTwoRolesHaveDistinctBlocks) {
|
||||
if (!Ready()) return;
|
||||
DualBlockPeekState before;
|
||||
ASSERT_TRUE(PeekDualBlockState(&before))
|
||||
<< "the dual-block peek is compiled out: this entry is only ever registered in a "
|
||||
"disaggregated build, so a false here means the registration and the build disagree";
|
||||
ASSERT_TRUE(before.roleSplitActive)
|
||||
<< "dual-block control: MGPipeRoleSplitActive() is false - MOBILEGL_IPC_ROLE_SPLIT_STATE "
|
||||
"is not armed in this process (or the transport is monolith), so the two roles share "
|
||||
"one PipeInputs block and this case is the knob's negative control";
|
||||
EXPECT_TRUE(before.blocksDistinct)
|
||||
<< "dual-block control: MGPipeClientInputs() and gPipeInputs are the SAME object with "
|
||||
"the rehearsal armed - the fill side and the read side were never split";
|
||||
EXPECT_NE(before.serverIdentity, nullptr)
|
||||
<< "dual-block control: the server block's identity was never set (CONTRACT-P5E §3.2's "
|
||||
"server-owned SetIdentity) - the backend's identity-keyed memo caches read nullptr "
|
||||
"as a hit on their zero-initialised slot";
|
||||
|
||||
// One client verb boundary, driven directly so no draw reaches the server: the CLIENT
|
||||
// block's serial must move and the SERVER block's must not. Under a shared block the second
|
||||
// half is impossible, which is the control's other edge. The glPixelStorei dirties the
|
||||
// pixel-pack state so the boundary's emission step has a record to publish - the fixture's
|
||||
// destructor asserts the wire moved on every armed split case.
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
ASSERT_TRUE(DualBlockDriveClientVerbBoundary());
|
||||
DualBlockPeekState after;
|
||||
ASSERT_TRUE(PeekDualBlockState(&after));
|
||||
EXPECT_GT(after.clientSerial, before.clientSerial)
|
||||
<< "dual-block control: a client verb boundary did not move the client block's serial";
|
||||
EXPECT_EQ(after.serverSerial, before.serverSerial)
|
||||
<< "dual-block control: a fill-side verb boundary moved the SERVER block's serial "
|
||||
<< before.serverSerial << " -> " << after.serverSerial
|
||||
<< " - the two roles are still writing one block";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -1145,6 +1145,12 @@ namespace MobileGL::MG_Remote::Server {
|
||||
m_verbs.SetBackend(backend);
|
||||
m_decoder = Wire::PipeWireDecoder(control, m_segments, m_replies);
|
||||
m_decoder.SetVerbSink(&m_verbs);
|
||||
// P5f (f1), CONTRACT-P5E §3.2: the server block's identity, once per session (the
|
||||
// per-verb stamp refreshes it against the served-context serial). A no-op unless the
|
||||
// dual-block rehearsal is armed; without it the server block's ContextIdentity() stays
|
||||
// nullptr and the backend's identity-keyed memo caches read that as a HIT on their
|
||||
// zero-initialised slot - an unnamed null dereference instead of a named marker.
|
||||
MG_Pipe::MGPipeServerBlockNoteIdentity();
|
||||
m_attached = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -865,6 +865,104 @@ TEST_F(FieldOwnershipTest, ResidualPullsReachThePublishedPerFrameCounter) {
|
||||
PS::SetEnabledForTesting(false);
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// P5f (f1): THE DUAL-BLOCK REHEARSAL AT THE BLOCK LEVEL (P5F-WIRE-COMPLETENESS.md §4)
|
||||
// ================================================================================
|
||||
//
|
||||
// MOBILEGL_IPC_ROLE_SPLIT_STATE=1 gives the fill side its own PipeInputs block
|
||||
// (MGPipeClientInputs()) and leaves gPipeInputs to the server alone. These cases pin the
|
||||
// mechanism's three arms without a transport running: the selection folds to the shared block
|
||||
// when the knob is off OR the transport is monolith, the two blocks are distinct objects when
|
||||
// it is armed, and a BARRIER-PULLED read under it is a NAMED Fatal with no strict knob
|
||||
// involved (the fork cases below).
|
||||
|
||||
namespace {
|
||||
// Arms the rehearsal by hand - the knob is parsed from the environment once per process,
|
||||
// so a case sets the two globals directly, and the guard puts them back on every exit
|
||||
// path, ASSERT death included.
|
||||
struct RoleSplitArm {
|
||||
RoleSplitArm() {
|
||||
m_previousTransport = MG_Config::Transport;
|
||||
m_previousKnob = MG_Config::Ipc.RoleSplitState;
|
||||
}
|
||||
~RoleSplitArm() {
|
||||
MG_Config::Transport = m_previousTransport;
|
||||
MG_Config::Ipc.RoleSplitState = m_previousKnob;
|
||||
}
|
||||
void Arm(Bool arm) {
|
||||
MG_Config::Ipc.RoleSplitState = arm;
|
||||
MG_Config::Transport = arm ? MG_Config::TransportMode::InProcess
|
||||
: MG_Config::TransportMode::Monolith;
|
||||
}
|
||||
MG_Config::TransportMode m_previousTransport;
|
||||
Bool m_previousKnob;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(FieldOwnershipTest, RoleSplitOffFoldsTheFillSideOntoTheSharedBlock) {
|
||||
EXPECT_FALSE(MGPipeRoleSplitActive());
|
||||
EXPECT_EQ(&MGPipeClientInputs(), &gPipeInputs);
|
||||
}
|
||||
|
||||
// The knob alone is not the arm: under monolith transport the two roles are one thread and
|
||||
// there is exactly one block, or every read would starve. This is the fold the whole unit
|
||||
// lane and the integration-gpu lane of a split build stand on.
|
||||
TEST_F(FieldOwnershipTest, RoleSplitUnderMonolithTransportIsStillOneBlock) {
|
||||
RoleSplitArm guard;
|
||||
MG_Config::Ipc.RoleSplitState = true;
|
||||
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||
EXPECT_FALSE(MGPipeRoleSplitActive());
|
||||
EXPECT_EQ(&MGPipeClientInputs(), &gPipeInputs);
|
||||
}
|
||||
|
||||
TEST_F(FieldOwnershipTest, RoleSplitGivesTheFillSideADistinctBlockTheStampNeverTouches) {
|
||||
RoleSplitArm guard;
|
||||
guard.Arm(true);
|
||||
ASSERT_TRUE(MGPipeRoleSplitActive());
|
||||
ASSERT_NE(&MGPipeClientInputs(), &gPipeInputs);
|
||||
|
||||
const Uint64 serverSerialBefore = gPipeInputs.FilledState().CurrentVerbSerial;
|
||||
const Uint64 clientSerialBefore = MGPipeClientInputs().FilledState().CurrentVerbSerial;
|
||||
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
||||
EXPECT_EQ(gPipeInputs.FilledState().CurrentVerbSerial, serverSerialBefore + 1);
|
||||
EXPECT_EQ(MGPipeClientInputs().FilledState().CurrentVerbSerial, clientSerialBefore)
|
||||
<< "the server's verb stamp reached into the client block";
|
||||
EXPECT_TRUE(gPipeInputs.ServerStampedVerb());
|
||||
EXPECT_FALSE(MGPipeClientInputs().ServerStampedVerb());
|
||||
|
||||
// The fill side's clear is the client block's own: it must not disarm the server's stamp,
|
||||
// which is CONTRACT-P5E §3.2's "the server's stamp is server-private" as a fact rather
|
||||
// than as a comment.
|
||||
MGPipeClientClearVerbBoundary();
|
||||
EXPECT_TRUE(gPipeInputs.ServerStampedVerb())
|
||||
<< "the client-role clear withdrew the SERVER's stamp";
|
||||
MGPipeServerClearVerbBoundary();
|
||||
EXPECT_FALSE(gPipeInputs.ServerStampedVerb());
|
||||
|
||||
// And the server block's identity is server-owned under the rehearsal (§3.2's other half):
|
||||
// the stamp set it, and it is non-null - a null identity reads as a hit against
|
||||
// DirectGLES' zero-initialised fb-slot memo cache, which is the unnamed crash this line
|
||||
// exists to preclude.
|
||||
EXPECT_NE(gPipeInputs.ContextIdentity(), nullptr);
|
||||
}
|
||||
|
||||
// The fill side's writers land in the client block alone. MGPipeLeaveVerb rather than
|
||||
// MGPipeValidateForVerb: the validate point's step 3 EMITS, which is session machinery this
|
||||
// process does not have, while LeaveVerb's serial bump and verb reset are exactly the fill
|
||||
// side's write shape with nothing else in the way.
|
||||
TEST_F(FieldOwnershipTest, TheClientVerbLeaveWritesTheClientBlockAlone) {
|
||||
RoleSplitArm guard;
|
||||
guard.Arm(true);
|
||||
ASSERT_NE(&MGPipeClientInputs(), &gPipeInputs);
|
||||
const Uint64 serverSerialBefore = gPipeInputs.FilledState().CurrentVerbSerial;
|
||||
const Uint64 clientSerialBefore = MGPipeClientInputs().FilledState().CurrentVerbSerial;
|
||||
MGPipeLeaveVerb();
|
||||
EXPECT_EQ(MGPipeClientInputs().FilledState().CurrentVerbSerial, clientSerialBefore + 1);
|
||||
EXPECT_EQ(gPipeInputs.FilledState().CurrentVerbSerial, serverSerialBefore)
|
||||
<< "a fill-side write reached the server block";
|
||||
EXPECT_EQ(MGPipeClientInputs().CurrentVerb(), MGPipeVerb::kVerbCount);
|
||||
}
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
|
||||
// R-7.3's proof that the instrumentation can go red. An instrumentation that cannot is
|
||||
@@ -1117,6 +1215,42 @@ TEST_F(FieldOwnershipTest, AnUnmigratedEmulationIsFatalUnderARealTransportAndIne
|
||||
<< split.Log;
|
||||
}
|
||||
|
||||
// P5f (f1): THE REHEARSAL'S FATAL ARM, AND THE PROOF IT NEEDS NO OTHER KNOB. Under
|
||||
// MOBILEGL_IPC_ROLE_SPLIT_STATE=1 a BARRIER-PULLED read is Fatal{UnmigratedPipeInput} with
|
||||
// StrictErrors FALSE - the server block has no residual fill to answer from, so the read has
|
||||
// no legal value whatever the strict knob says, and the marker names the knob that armed it.
|
||||
// This is also the red-once for the lane's "no unnamed crashes" gate: the read below is an
|
||||
// O-class SharedPtr field, which without this arm would hand the backend an empty pointer to
|
||||
// dereference.
|
||||
TEST_F(FieldOwnershipTest, DualBlockMakesABarrierPulledReadANamedAbortWithoutStrict) {
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||
MG_Config::Ipc.RoleSplitState = true;
|
||||
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
||||
(void)gPipeInputs.GetBoundVertexArray();
|
||||
});
|
||||
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetBoundVertexArray@DrawArrays\"}"),
|
||||
std::string::npos)
|
||||
<< r.Log;
|
||||
EXPECT_NE(r.Log.find("BARRIER-PULLED"), std::string::npos) << r.Log;
|
||||
EXPECT_NE(r.Log.find("MOBILEGL_IPC_ROLE_SPLIT_STATE=1"), std::string::npos) << r.Log;
|
||||
}
|
||||
|
||||
// ... and the same read with the knob set but the transport MONOLITH is the folded arm: one
|
||||
// block, the pull counted, no Fatal. The knob is not the mechanism; the knob AND a real
|
||||
// transport are.
|
||||
TEST_F(FieldOwnershipTest, DualBlockDoesNotArmUnderMonolithTransport) {
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Ipc.RoleSplitState = true; // the knob, with no transport behind it
|
||||
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
||||
(void)gPipeInputs.GetBoundVertexArray();
|
||||
if (MGPipeResidualPullCount() != 1) ::_exit(7);
|
||||
});
|
||||
ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
EXPECT_EQ(r.Log.find("Fatal{"), std::string::npos) << r.Log;
|
||||
}
|
||||
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# 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:
|
||||
#
|
||||
# 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
|
||||
# its ctest ENVIRONMENT - a property would override the process environment and make the
|
||||
# red form below unreachable, see its registration block). A skipped baseline is not a
|
||||
# green one (ID-62), so skips are a hard error here, not a pass.
|
||||
# 2. THE RED MUST CARRY THE SELECTED CASE'S OWN FAILURE TEXT. With the knob off the two roles
|
||||
# share one block again and DualBlockScenario.TheTwoRolesHaveDistinctBlocks must fail on
|
||||
# its own "dual-block control:" assertion - a timeout, a setup abort or an unrelated
|
||||
# failure proves nothing about the knob (ID-46 finding 8).
|
||||
#
|
||||
# Usage: dualblock_negative_control.sh
|
||||
# CTEST ctest binary (default: ctest)
|
||||
# CONTROL_TMPDIR scratch dir for the junit + output (default: ${RUNNER_TEMP:-/tmp})
|
||||
set -u
|
||||
|
||||
CTEST="${CTEST:-ctest}"
|
||||
CONTROL_TMPDIR="${CONTROL_TMPDIR:-${RUNNER_TEMP:-/tmp}}"
|
||||
mkdir -p "${CONTROL_TMPDIR}"
|
||||
|
||||
label="integration-dualblock-split"
|
||||
tally_tool="$(dirname "$0")/junit_tally.py"
|
||||
|
||||
matched=$("${CTEST}" -N -L "${label}" | grep -cE '^ *Test *#[0-9]+:')
|
||||
if [ "${matched}" -lt 1 ]; then
|
||||
echo "::error::the dual-block lane selected ${matched} tests; its label no longer matches anything"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- the baseline: knob ON, and it must be GREEN ------------------------------------------
|
||||
baseline_xml="${CONTROL_TMPDIR}/dualblock-baseline.xml"
|
||||
rm -f "${baseline_xml}"
|
||||
MOBILEGL_IPC_ROLE_SPLIT_STATE=1 "${CTEST}" -L "${label}" --no-tests=error \
|
||||
--output-on-failure --output-junit "${baseline_xml}"
|
||||
baseline_rc=$?
|
||||
tally=$(python3 "${tally_tool}" "${baseline_xml}")
|
||||
baseline_passed=$(echo "${tally}" | cut -d' ' -f1)
|
||||
baseline_failed=$(echo "${tally}" | cut -d' ' -f2)
|
||||
baseline_skipped=$(echo "${tally}" | cut -d' ' -f3)
|
||||
echo "dual-block baseline (knob on) - passed: ${baseline_passed}, failed: ${baseline_failed}, skipped: ${baseline_skipped}"
|
||||
if [ "${baseline_rc}" -ne 0 ] || [ "${baseline_failed}" -gt 0 ]; then
|
||||
echo "::error::the dual-block lane is ALREADY RED with the knob armed, so turning it off below proves nothing. Fix the lane first."
|
||||
exit 1
|
||||
fi
|
||||
if [ "${baseline_passed}" -lt 1 ] || [ "${baseline_skipped}" -gt 0 ]; then
|
||||
echo "::error::the dual-block baseline skipped or ran nothing - a control whose armed arm never executed is not a control (ID-62)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- the control: knob OFF, and the red must be the case's own assertion -------------------
|
||||
control_xml="${CONTROL_TMPDIR}/dualblock-control.xml"
|
||||
out="${CONTROL_TMPDIR}/dualblock-control-output.txt"
|
||||
rm -f "${control_xml}"
|
||||
control_rc=0
|
||||
MOBILEGL_IPC_ROLE_SPLIT_STATE=0 "${CTEST}" -L "${label}" --no-tests=error \
|
||||
--output-on-failure --output-junit "${control_xml}" > "${out}" 2>&1 || control_rc=$?
|
||||
cat "${out}"
|
||||
if [ "${control_rc}" -eq 0 ]; then
|
||||
echo "::error::MOBILEGL_IPC_ROLE_SPLIT_STATE=0 left the dual-block lane GREEN: the two roles shared one block and nothing noticed - the mechanism is not load-bearing (P5F §6)"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q 'dual-block control:' "${out}"; then
|
||||
echo "::error::the knob-off run is red but carries none of the case's own 'dual-block control:' assertions - the failure is something else wearing the control's name"
|
||||
exit 1
|
||||
fi
|
||||
echo "negative control P5f-f1 (MOBILEGL_IPC_ROLE_SPLIT_STATE=0) turned ${matched} dual-block entries red, and the red is the case's own assertion, as it must"
|
||||
Reference in New Issue
Block a user