[Config, MG_Backend, MG_Test] (Disaggregated): P5e c0e - the two knobs and the caps arm

- IpcTable::RunAhead (MOBILEGL_IPC_RUN_AHEAD, default 1, forced 0 under PipeVerify beside
  BatchWaits) and IpcTable::PresentCredit (MOBILEGL_IPC_PRESENT_CREDIT, default 1, range
  1..8). Both parsed unconditionally like every other IPC knob and both in the IPC log
  line, because a knob that silently means nothing on one arm of an A/B is how an A/B
  stops being one.
- RUN_AHEAD=0 is an A/B CONTROL and not a negative one: identical server code, identical
  records, identical fill decision, and only the client's wait differs. VERB_BARRIER=0
  keeps its own meaning as the lockstep arm's negative control.
- MG_Backend/Init.cpp: kMGPipeP5eRunAheadReady = false, and the caps arm goes through
  MGPipeRunAheadCapBitsFor so the "DirectGLES only, and only once the integration commit
  flips the constant" decision has one spelling. A constant and not a knob: an operator
  must not be able to turn a half-migrated server into a run-ahead one, because the
  failure mode is a torn read that renders wrong rather than aborting.
- MagmaPipeIdentityTest pins both halves - no bit 10 for DirectVulkan whatever the
  integration constant says, bit 10 for DirectGLES only when it is true, and that one bit
  and nothing else.
- The push default becomes kMGPipeSubsystemsMigratedAtP5e (0x3fff). Inert: bit 13 is not
  in kMGPipeWiredSubsystems, so no emission gate opens on it; 0x1fff survives as P5e's
  "everything P4a had and nothing of mine" control.
This commit is contained in:
2026-09-18 06:26:32 -04:00
parent e99796d6ec
commit 5ef4701294
4 changed files with 125 additions and 8 deletions
+32 -3
View File
@@ -322,9 +322,9 @@ namespace MobileGL::MG_Config {
// 0 - the only shipped value until the migration lands - is "pull everything",
// i.e. exactly today's behaviour, and is the default of a PULL build, where the
// knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP4a), so MOBILEGL_PIPE_PUSH=0 in the
// environment is the all-pull control and 0x1ff (kMGPipeSubsystemsMigratedAtP3a) is
// the "everything before P4a" control P4a's A/B is run against - each phase's
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP5e = 0x3fff), so MOBILEGL_PIPE_PUSH=0 in
// the environment is the all-pull control and 0x1fff (kMGPipeSubsystemsMigratedAtP4a)
// is the "everything before P5e" control P5e's A/B is run against - each phase's
// constant survives as the next phase's control, which is why none of them is ever
// edited. Accepts decimal or 0x-prefixed hex, and operators pass it as hex, so the
// bits are listed here (MG_Pipe/MGPipe.h owns them):
@@ -342,6 +342,9 @@ namespace MobileGL::MG_Config {
// 0x800 samplers (sampler CSO, sampler view, set_sampler_views /
// bind_sampler_states / set_shader_images) - requires 0x400
// 0x1000 programs (shader CSO, set_draw/dispatch_program, global constants)
// 0x2000 buffer binding points (set_shader_buffers, the three indexed binding-point
// classes and the dirty bits 15/16/17) - requires 0x80
// (every MGPBufferRange::Res names a Buffer handle; P5e)
// A dependency that is not met is REFUSED with one ERROR naming both bits and the
// family runs its legacy arm; it is never half-run.
// 1<<63 NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of
@@ -502,6 +505,32 @@ namespace MobileGL::MG_Config {
// 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, MG_Remote/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
// run-ahead only when the server also publishes kCapRunAheadApply, so on Magma, and on
// Espryt before the P5e integration commit, 1 means exactly what 0 means and logs once
// saying so.
//
// 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.
//
// 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
// arm that stops filling it.
Uint32 RunAhead = 1;
// MOBILEGL_IPC_PRESENT_CREDIT (P5e, ruling 4 / ID-92): how many presents the client may
// have in flight before it waits for a swap to come back. 1 = the client publishes
// frame N+1's records while the server applies and swaps frame N - one frame of
// overlap, at most one frame of added latency - and the CREDIT, never the ring's bytes,
// is what paces a run-ahead client. 2 is a device MEASUREMENT arm: it buys no CPU on a
// client that is already CPU-bound and costs a frame of latency, which is why the
// default is 1 and not "as deep as the ring".
Uint32 PresentCredit = 1;
// MOBILEGL_IPC_STRICT_ERRORS: promote a BARRIER-PULLED field read - and, in a split
// build, the seven sticky forwards that are otherwise exempt - from "count it in
// rsp" to Fatal (R-7.3).
+17 -4
View File
@@ -8,7 +8,7 @@
#include "Config.h"
#if MOBILEGL_PIPE_PUSH
// For kMGPipeSubsystemsMigratedAtP4a, the push build's PipePush default (the P2 and P3a
// For kMGPipeSubsystemsMigratedAtP5e, the push build's PipePush default (the P2, P3a and P4a
// constants beside it are the phase-by-phase controls, not the default). Push-only, so the
// pull build's translation unit is unchanged.
#include <MG_Pipe/MGPipe.h>
@@ -263,7 +263,7 @@ namespace MobileGL::MG_ConfigLoader {
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
// kMGPipeSubsystemsMigratedAtP3a (0x1ff) is the phase-by-phase control - P4a's four
// subsystems off, everything P3a landed still on.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP4a);
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP5e);
#else
// Meaningless in a pull build: there is nothing to push. Config.h documents 0 as
// "pull everything" and that stays literally true.
@@ -379,6 +379,17 @@ namespace MobileGL::MG_ConfigLoader {
// point of use, which is where the "P11" in the message belongs.
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", 2, 0, 2);
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
// P5e (MG_Remote/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 (MG_Config::Features.PipeVerify) ipc.RunAhead = 0;
// 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".
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", 1, 1, 8);
ipc.StrictErrors = QueryEnvFlag("MOBILEGL_IPC_STRICT_ERRORS");
ipc.Audit = QueryEnvFlag("MOBILEGL_IPC_AUDIT");
QueryEnvVariable("MOBILEGL_IPC_SERVER_AFFINITY", ipc.ServerAffinity, "auto");
@@ -387,9 +398,11 @@ namespace MobileGL::MG_ConfigLoader {
// 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 strict=%d audit=%d affinity='%s'",
"adopt-tier=%u verb-barrier=%u run-ahead=%u present-credit=%u strict=%d "
"audit=%d affinity='%s'",
ipc.RingMb, ipc.StageMb, ipc.SpinUs, ipc.PersistentBlockKb, ipc.AdoptTier,
ipc.VerbBarrier, static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit,
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
ipc.ServerAffinity.c_str());
if (ipc.VerbBarrier == 0) {
MGLOG_W("Config: MOBILEGL_IPC_VERB_BARRIER=0 is the R-1 NEGATIVE CONTROL and is "
+31
View File
@@ -81,6 +81,22 @@ namespace MobileGL::MG_Backend {
// DirectVulkan registers NO resource ops - MGPipeSetResourceOps has exactly one caller
// in the whole tree and it is Managers.cpp:2594 - so bit 7 is CLEAR for it, which is
// the same fact ObjectSubsystemControlScenario already pins from the client side.
// P5e (MG_Remote/CONTRACT-P5E.md §6.2). THE ONE CONSTANT THE WHOLE PHASE HANGS ON.
//
// kCapRunAheadApply says "this server applies an unbarriered record without reading
// client memory". That is only true once EVERY per-draw family reads records instead
// of the frontend - vi, sb, pg, tx2 and fb all land before it is - so the caps arm
// below is gated on this constant, which the P5e INTEGRATION COMMIT flips to true
// after the last of them. Until then every P5e package lands with the wait rule, the
// static WaitClass column and the barriered predicate compiled and INERT: the client
// never latches RunAheadArmed, so it runs today's lockstep path byte for byte.
//
// It is a constant and not a knob on purpose. An operator cannot turn a half-migrated
// server into a run-ahead one, because the failure mode is not a slow frame - it is
// the apply thread reading client memory that has already moved, which renders wrong
// rather than aborting.
constexpr Bool kMGPipeP5eRunAheadReady = false;
Uint64 ConsumedSubsystemsFor(BackendType type) {
switch (type) {
case BackendType::DirectGLES: return MG_Pipe::kMGPipeSubsystemsMigratedAtP4a;
@@ -166,6 +182,21 @@ namespace MobileGL::MG_Backend {
serverBackend->GetBackendFunctions().GL.EndTransformFeedback != nullptr) {
capBits |= MG_Pipe::kCapBackendOwnsXfbCapture;
}
// P5e (CONTRACT-P5E.md §1, §6): kCapRunAheadApply, THE DIRECTGLES ARM AND ONLY IT.
//
// Magma is deliberately absent and is not an omission: it keeps the lockstep for
// the whole of P5e, its four apply-thread allocator sites are real debt P7 retires,
// and MGPipeApplierCurrentRecordIsBarriered() answers true for every record on a
// server that does not publish this bit - which is exactly what keeps those probes
// and its BARRIER_PULLED reads inside P5C's semantics and its rsp accounting
// honest. Publishing the bit here for DirectVulkan would turn accounted pulls into
// torn ones, so MagmaPipeIdentityTest pins its absence.
//
// The Espryt arm is gated on kMGPipeP5eRunAheadReady, which is false until the
// integration commit: the bit is what ARMS the client, so every package before it
// lands inert.
capBits |= MG_Pipe::MGPipeRunAheadCapBitsFor(MG_Config::ActiveBackendType,
kMGPipeP5eRunAheadReady);
session.SetCapabilityBits(capBits);
session.SetBackend(loop.Backend());
@@ -56,7 +56,8 @@ namespace {
X(MagmaPipeIdentityTest, AnIdleSlotIsRetiredAndReusedWithANewGeneration) \
X(MagmaPipeIdentityTest, AReusedSlotDoesNotServeItsPredecessorsMemo) \
X(MagmaPipeIdentityTest, TheAbaControlKnobServesTheStaleMemoAcrossAReusedSlot) \
X(MagmaPipeIdentityTest, ALiveObjectKeepsItsSlotItsGenerationAndItsMemo)
X(MagmaPipeIdentityTest, ALiveObjectKeepsItsSlotItsGenerationAndItsMemo) \
X(MagmaPipeIdentityTest, AMagmaServerNeverPublishesTheRunAheadCapBit)
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
@@ -213,5 +214,48 @@ namespace {
<< "a live object's memo was cleared without its slot changing owner";
EXPECT_EQ(mint.Count(), 1u);
}
// ---- P5e (MG_Remote/CONTRACT-P5E.md §1, §6, ruling 12) -------------------------------
//
// MAGMA DOES NOT RUN AHEAD, AND THIS IS WHERE THAT IS A TEST RATHER THAN A COMMENT.
// kCapRunAheadApply is the client's whole permission to publish a record and move on: the
// moment a server sets it, the apply thread promises it reads nothing of the client's. That
// promise is FALSE for Magma for the whole of P5e - the four apply-thread allocator sites
// inside MagmaP7AllocatorDebtScope are real debt P7 retires, and
// MGPipeApplierCurrentRecordIsBarriered() answering true for every record on a server
// without the bit is exactly what keeps them inside P5C's semantics and keeps rsp honest.
//
// The arm is a pure function precisely so this case can reach it: InitSplitRoles needs a
// live session, a backend and a handshake, and none of those belong in a unit lane. RED
// ONCE by making MGPipeRunAheadCapBitsFor answer for DirectVulkan too (the exact
// perturbation the phase's red-once list names) - both EXPECTs below fail, by name.
TEST_F(MagmaPipeIdentityTest, AMagmaServerNeverPublishesTheRunAheadCapBit) {
// Whatever the integration constant says. `true` is what the P5e integration commit
// will pass, so the Magma answer is pinned on BOTH sides of that flip and the case
// does not quietly stop asserting anything the day the constant moves.
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/false) &
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply),
0u);
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/true) &
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply),
0u);
// And the Espryt half, so the case says what the arm IS and not only what it is not:
// the bit is published for DirectGLES and ONLY once the integration commit flips
// kMGPipeP5eRunAheadReady. Until then every P5e package lands with the wait rule inert.
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectGLES, /*ready=*/false) &
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply),
0u);
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectGLES, /*ready=*/true) &
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply),
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply));
// Bit 10 and nothing else: the arm contributes ONE bit, so a future editor cannot fold
// an unrelated capability into it and have the two pins above still pass.
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectGLES, /*ready=*/true),
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply));
static_assert(static_cast<Uint64>(MG_Pipe::kCapRunAheadApply) == (1ull << 10),
"kCapRunAheadApply moved bit; CONTRACT-P5E table 0 names bit 10");
}
#endif // MOBILEGL_PIPE_PUSH
} // namespace