diff --git a/MobileGL/Config.h b/MobileGL/Config.h index f21f2b87..c4bb4db6 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -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). diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 2344901f..5d4d6a9d 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -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 @@ -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(ipc.StrictErrors), static_cast(ipc.Audit), + ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, + static_cast(ipc.StrictErrors), static_cast(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 " diff --git a/MobileGL/MG_Backend/Init.cpp b/MobileGL/MG_Backend/Init.cpp index 8a8fb04f..97168fe4 100644 --- a/MobileGL/MG_Backend/Init.cpp +++ b/MobileGL/MG_Backend/Init.cpp @@ -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()); diff --git a/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp b/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp index e7ce2e90..c84f6f6d 100644 --- a/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp +++ b/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp @@ -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(MG_Pipe::kCapRunAheadApply), + 0u); + EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/true) & + static_cast(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(MG_Pipe::kCapRunAheadApply), + 0u); + EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectGLES, /*ready=*/true) & + static_cast(MG_Pipe::kCapRunAheadApply), + static_cast(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(MG_Pipe::kCapRunAheadApply)); + static_assert(static_cast(MG_Pipe::kCapRunAheadApply) == (1ull << 10), + "kCapRunAheadApply moved bit; CONTRACT-P5E table 0 names bit 10"); + } #endif // MOBILEGL_PIPE_PUSH } // namespace