From 2fbcc2241094d46a045ce85e08c4de2ae1b50585 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 8 Oct 2026 19:49:04 -0400 Subject: [PATCH] Pipe: fire-and-forget is async - mipmap, copies, XFB span and storage-block rows to kWaitNone, inproc creates and PBO readbacks stop waiting, and the default dual block lets a barriered verb cost one wait (FCL BSL: ~40 sync points/frame). --- MobileGL/Config.h | 5 +- MobileGL/ConfigLoader.cpp | 6 +- MobileGL/MG_Impl/Pipe/PipeFill.cpp | 38 +++++++- MobileGL/MG_Impl/Pipe/PipeFill.h | 5 ++ .../MG_Impl/Pipe/Verb/TextureReadbackEmit.inc | 3 + MobileGL/MG_Impl/Pipe/Verb/VerbPort.cpp | 3 + .../GenerateMipmapServerScenario.cpp | 3 + .../PersistentCoherentMapScenario.cpp | 4 + MobileGL/MG_Pipe/PipeCalls.def | 18 ++-- MobileGL/MG_Pipe/generated/PipeWire.inc | 18 ++-- MobileGL/MG_Pipe/generated/PipeWireLayout.inc | 18 ++-- MobileGL/MG_Remote/Client/ClientSession.cpp | 4 + MobileGL/MG_Remote/Client/ClientSession.h | 3 + MobileGL/MG_Remote/Client/WireTables.cpp | 22 +++++ MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp | 41 ++++----- .../MG_Test/Wire/RemoteClientE1Controls.inc | 17 ++-- MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 88 +++++++++++++++++++ .../design/09-build-switches-counters.md | 2 +- 18 files changed, 234 insertions(+), 64 deletions(-) diff --git a/MobileGL/Config.h b/MobileGL/Config.h index a7f3128a..aab935b7 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -740,8 +740,9 @@ namespace MobileGL::MG_Config { // 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; + // without it (P5F §6). P15: the default is ON (unset = on, 0 = off); the negative control + // sets 0 explicitly. + Bool RoleSplitState = true; // 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. diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index ff44f945..f7594377 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -624,7 +624,11 @@ namespace MobileGL::MG_ConfigLoader { // 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"); + // P15: ON BY DEFAULT. Every split lane and the on-device retrace gate already ran with it, + // and the dual-block census is empty (no apply reads a client-filled field), so the + // shipped arm now matches what is tested - and with two blocks the client's fill is its + // 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 (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); " diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index b09cb5eb..eeb4021f 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -43,6 +43,7 @@ #include #if MOBILEGL_PIPE_VERIFY #include +#include #endif #if MOBILEGL_BUILD_DISAGGREGATED @@ -400,8 +401,31 @@ namespace MobileGL::MG_Pipe { // `ctx` MAY BE NULL: the validate point asks this question BEFORE it has decided // whether there is anything to fill. A null context has no open transform-feedback // span, so clause 2 is false for it - and the verb is a no-op below either way. + // MGPipeValidateForVerbBarriered's switch: the next ClientVerbIsBarriered answers true. + Bool g_forceBarrieredFill = false; + + // P15: A PACK-BUFFER READBACK IS FIRE-AND-FORGET, decided per call. With a pack buffer + // bound the readback emitters publish read_pixels_to_buffer / get_texture_image_to_buffer + // (kWaitNone) instead of the reply form, and nothing is owed to the client at the call; + // the predicate mirrors PackBufferReadbackTarget's conditions. The one condition it + // cannot see (the byte range past 2 GiB) sends the emitter to the reply form, which then + // re-validates barriered (MGPipeValidateForVerbBarriered) before it publishes. + Bool PackReadbackGoesToBuffer(GLContext* ctx) { + if (ctx == nullptr) return false; + const auto& pbo = ctx->GetBufferBindingSlot(::MobileGL::BufferTarget::PixelPack).GetBoundObject(); + if (!pbo) return false; +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Ipc.PboReadbackSync != 0) return false; +#endif + if (pbo->IsMapped() || !MG_Record::BufferWritebackIsReachable(*pbo)) return false; + return !MGPipeHandleIsNull(MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, pbo->GetLifetimeId())); + } + Bool ClientVerbIsBarriered(MGPipeVerb verb, GLContext* ctx) { - const MGPWireOp op = MGPipeRecordOpForVerb(verb); + if (g_forceBarrieredFill) return true; + MGPWireOp op = MGPipeRecordOpForVerb(verb); + if (op == MGPWireOp::ReadPixels && PackReadbackGoesToBuffer(ctx)) op = MGPWireOp::ReadPixelsToBuffer; + if (op == MGPWireOp::GetTextureImage && PackReadbackGoesToBuffer(ctx)) op = MGPWireOp::GetTextureImageToBuffer; // No record (answered from the caps mirror): no apply can read a fill for it. if (op == kMGPipeNoRecord) return false; if (MGPipeWaitClassFor(op) != kWaitNone) return true; @@ -441,6 +465,12 @@ namespace MobileGL::MG_Pipe { // call that returns, and the lockstep client's behaviour is unchanged byte for byte // (G1) - under lockstep appliedSeq is already at LastPublishedSeq by construction. void QuiesceApplierBeforeFill(const char* surface) { + // P15: WITH TWO BLOCKS THE FILL IS THE CLIENT'S OWN. The applier reads the server + // block, so a GL-thread write to the client block races nothing and needs no wait - + // which is what makes one barriered verb cost one wait (its record's) instead of a + // quiesce before the fill, another before the residual half and one per frontend + // mutation after it (FCL BSL: 22 of them per frame). + if (MGPipeBlocksAreDistinct()) return; MG_Remote::Client::ClientSession* session = MG_Remote::Client::ClientSession::Active(); if (session == nullptr) return; // no session: this process has no applier to outrun session->WaitForApplyToCatchUp(surface); @@ -3397,6 +3427,12 @@ namespace MobileGL::MG_Pipe { field); } + void MGPipeValidateForVerbBarriered(MGPipeVerb verb) { + g_forceBarrieredFill = true; + MGPipeValidateForVerb(verb); + g_forceBarrieredFill = false; + } + // ---- the validate point (P2 brief D1) ---- void MGPipeValidateForVerb(MGPipeVerb verb) { // P5f (f1): the fill side's one new spelling. With MOBILEGL_IPC_ROLE_SPLIT_STATE=1 this diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.h b/MobileGL/MG_Impl/Pipe/PipeFill.h index 71eaface..657205dc 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.h +++ b/MobileGL/MG_Impl/Pipe/PipeFill.h @@ -33,6 +33,11 @@ namespace MobileGL::MG_Pipe { // entry points with one call site per verb instead of nine. void MGPipeValidateForVerb(MGPipeVerb verb); + // P15: the validate point again for a verb whose first validate answered UNBARRIERED for a + // per-call reason that did not hold at emission (a pack-buffer readback that falls back to + // the reply form). Runs the barriered fill: quiesce, then fill, exactly as a barriered verb. + void MGPipeValidateForVerbBarriered(MGPipeVerb verb); + // Ends the verb in flight without starting another: bumps the serial, so every field the // verb stamped goes stale, and puts the current verb back to "none", so a read made after // it aborts as Fatal{UnmigratedPipeInput, "@"} - which is what such a read diff --git a/MobileGL/MG_Impl/Pipe/Verb/TextureReadbackEmit.inc b/MobileGL/MG_Impl/Pipe/Verb/TextureReadbackEmit.inc index 988a1e99..8556fb4d 100644 --- a/MobileGL/MG_Impl/Pipe/Verb/TextureReadbackEmit.inc +++ b/MobileGL/MG_Impl/Pipe/Verb/TextureReadbackEmit.inc @@ -133,6 +133,9 @@ sizeof(record), nullptr, 0, nullptr, 0, nullptr); return; } + // P15: the validate point answered "pack buffer, fire-and-forget" and the reply + // form is taken after all, so the barriered fill that form needs runs now. + MG_Pipe::MGPipeValidateForVerbBarriered(MG_Pipe::MGPipeVerb::GetTextureImage); } Vector bytes(static_cast(tight)); const Uint64 capacity = session.MaxReplyBytes(); diff --git a/MobileGL/MG_Impl/Pipe/Verb/VerbPort.cpp b/MobileGL/MG_Impl/Pipe/Verb/VerbPort.cpp index 5d2369eb..020bd818 100644 --- a/MobileGL/MG_Impl/Pipe/Verb/VerbPort.cpp +++ b/MobileGL/MG_Impl/Pipe/Verb/VerbPort.cpp @@ -1199,6 +1199,9 @@ namespace MobileGL::MG_Record { }); return; } + // P15: the validate point answered "pack buffer, fire-and-forget" and the reply + // form is taken after all, so the barriered fill that form needs runs now. + MG_Pipe::MGPipeValidateForVerbBarriered(MG_Pipe::MGPipeVerb::ReadPixels); } // THE COMMON CASE KEEPS THE ZERO-COPY. A neutral pack state means the destination diff --git a/MobileGL/MG_IntegrationTest/Scenarios/GenerateMipmapServerScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/GenerateMipmapServerScenario.cpp index e9b29397..379b28bd 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/GenerateMipmapServerScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/GenerateMipmapServerScenario.cpp @@ -292,6 +292,9 @@ void main() { o_color = gl_FragCoord.y < u_split ? u_bottom : u_top; } glBindTexture(GL_TEXTURE_2D, m_texture); glGenerateMipmap(GL_TEXTURE_2D); ASSERT_EQ(FirstGLError(), 0u) << "glGenerateMipmap"; + // generate_mipmap is fire-and-forget (P15): the server applies it after this + // returns, so the server log this case reads is complete only after a finish. + glFinish(); } // Under the knob the SERVER must say it filtered on the CPU; without that line the entry diff --git a/MobileGL/MG_IntegrationTest/Scenarios/PersistentCoherentMapScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/PersistentCoherentMapScenario.cpp index 99ea9578..5ff4cfb0 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/PersistentCoherentMapScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/PersistentCoherentMapScenario.cpp @@ -466,6 +466,10 @@ void main() { oColor = vec4(vColor, 1.0); } } Gl().EndFrame(); // close the setup window; SetUp's own store and map go in it + // The window is emitted where the SERVER applies that present, and the client runs ahead + // of it (a buffer create no longer waits since P15): without this the counted window's + // map can be tallied before the setup window closes and land in it. + glFinish(); unsigned long long acquisitionsBefore = 0, pushedBefore = 0; const bool separateClientStats = PeekSeparateClientMapStats(&acquisitionsBefore, &pushedBefore); diff --git a/MobileGL/MG_Pipe/PipeCalls.def b/MobileGL/MG_Pipe/PipeCalls.def index a070bc92..2029f366 100644 --- a/MobileGL/MG_Pipe/PipeCalls.def +++ b/MobileGL/MG_Pipe/PipeCalls.def @@ -265,8 +265,8 @@ /* (kBlitOrCopy), same shape, a wait that is already carried in this lane. THE COST IS */ \ /* MEASURED, NOT ASSUMED: glCopyImageSubData is zero per frame in the Minecraft 26.3-rc-3 */ \ /* scene (~849 draws/frame), so the wait is not on any per-frame path. */ \ - X(ResourceCopyRegion, MGPCopyRegion, kCtxObject, kNone, kWaitApplied) \ - X(GenerateMipmap, MGPMipPlan, kCtxObject, kNone, kWaitApplied) \ + X(ResourceCopyRegion, MGPCopyRegion, kCtxObject, kNone, kWaitNone) \ + X(GenerateMipmap, MGPMipPlan, kCtxObject, kNone, kWaitNone) \ X(GetTextureImage, MGPReadbackInfo, kCtxObject, kReplySlot, kWaitReply) \ /* ---- context: transfer calls that read whole-context state, and the commands ---- */ \ X(Blit, MGPBlit, kCtxVerb, kNone, kWaitNone) \ @@ -275,10 +275,10 @@ X(DrawVbo, MGPDrawInfo, kCtxVerb, kHostSpan|kVarTail, kWaitNone) \ X(LaunchGrid, MGPGridInfo, kCtxVerb, kNone, kWaitNone) \ X(MemoryBarrier, MGPMemoryBarrier, kCtxVerb, kNone, kWaitNone) \ - X(BeginStreamOutput, MGPStreamOutputBegin, kCtxVerb, kNone, kWaitApplied) \ - X(EndStreamOutput, MGPXfbAccounting, kCtxVerb, kNone, kWaitApplied) \ - X(PauseStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone, kWaitApplied) \ - X(ResumeStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone, kWaitApplied) \ + X(BeginStreamOutput, MGPStreamOutputBegin, kCtxVerb, kNone, kWaitNone) \ + X(EndStreamOutput, MGPXfbAccounting, kCtxVerb, kNone, kWaitNone) \ + X(PauseStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone, kWaitNone) \ + X(ResumeStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone, kWaitNone) \ X(Flush, MGPFlush, kCtxVerb, kNone, kWaitNone) \ X(Present, MGPPresent, kCtxVerb, kNone, kWaitPresent) \ X(SetSwapInterval, MGPSwapInterval, kCtxVerb, kOptional, kWaitNone) \ @@ -305,13 +305,13 @@ X(PatchParameter, MGPPatchParameter, kCtxVerb, kNone, kWaitNone) \ /* glBindTransformFeedback - the driver object rebind and the capture-binding shadow */ \ /* invalidation happen at the call; the XFB namespace has no wire lifetime. */ \ - X(BindStreamOutput, MGPStreamOutputBind, kCtxVerb, kNone, kWaitApplied) \ + X(BindStreamOutput, MGPStreamOutputBind, kCtxVerb, kNone, kWaitNone) \ /* glShaderStorageBlockBinding - the block NAME is the coordinate all three sides share */ \ /* (BackendObject.h), and a name is bytes: kHasBlob, staged whole in SEG_STAGE, NUL included. */ \ - X(SetStorageBlockBinding, MGPStorageBlockBinding, kCtxVerb, kHasBlob, kWaitApplied) \ + X(SetStorageBlockBinding, MGPStorageBlockBinding, kCtxVerb, kHasBlob, kWaitNone) \ /* glCopyTexImage2D / glCopyTexSubImage2D - a copy whose SOURCE is the read framebuffer, */ \ /* which resource_copy_region (resource -> resource) cannot express. */ \ - X(CopyFramebufferToTexture, MGPCopyFromFramebuffer, kCtxVerb, kNone, kWaitApplied) \ + X(CopyFramebufferToTexture, MGPCopyFromFramebuffer, kCtxVerb, kNone, kWaitNone) \ /* ---- APPENDED BY P5c (MG_Remote/CONTRACT-P5C.md §5): the two control records. Opcodes ---- */ \ /* ---- 77..78. Neither has an MGPipeApply* entry point and neither gains one: both reach ---- */ \ /* ---- WireVerbSink, and under monolith neither has a producer - the reset is a direct ---- */ \ diff --git a/MobileGL/MG_Pipe/generated/PipeWire.inc b/MobileGL/MG_Pipe/generated/PipeWire.inc index 2836d756..acc72f36 100644 --- a/MobileGL/MG_Pipe/generated/PipeWire.inc +++ b/MobileGL/MG_Pipe/generated/PipeWire.inc @@ -443,8 +443,8 @@ inline constexpr Uint8 kMGPipeWaitClasses[static_cast(MGPWireOp::kOpCount /* 50 ResourceSubDataComplete */ static_cast(kWaitNone), /* 51 ResourceFlushRange */ static_cast(kWaitNone), /* 52 ResourceReadback */ static_cast(kWaitReply), - /* 53 ResourceCopyRegion */ static_cast(kWaitApplied), - /* 54 GenerateMipmap */ static_cast(kWaitApplied), + /* 53 ResourceCopyRegion */ static_cast(kWaitNone), + /* 54 GenerateMipmap */ static_cast(kWaitNone), /* 55 GetTextureImage */ static_cast(kWaitReply), /* 56 Blit */ static_cast(kWaitNone), /* 57 Clear */ static_cast(kWaitNone), @@ -452,10 +452,10 @@ inline constexpr Uint8 kMGPipeWaitClasses[static_cast(MGPWireOp::kOpCount /* 59 DrawVbo */ static_cast(kWaitNone), /* 60 LaunchGrid */ static_cast(kWaitNone), /* 61 MemoryBarrier */ static_cast(kWaitNone), - /* 62 BeginStreamOutput */ static_cast(kWaitApplied), - /* 63 EndStreamOutput */ static_cast(kWaitApplied), - /* 64 PauseStreamOutput */ static_cast(kWaitApplied), - /* 65 ResumeStreamOutput */ static_cast(kWaitApplied), + /* 62 BeginStreamOutput */ static_cast(kWaitNone), + /* 63 EndStreamOutput */ static_cast(kWaitNone), + /* 64 PauseStreamOutput */ static_cast(kWaitNone), + /* 65 ResumeStreamOutput */ static_cast(kWaitNone), /* 66 Flush */ static_cast(kWaitNone), /* 67 Present */ static_cast(kWaitPresent), /* 68 SetSwapInterval */ static_cast(kWaitNone), @@ -464,9 +464,9 @@ inline constexpr Uint8 kMGPipeWaitClasses[static_cast(MGPWireOp::kOpCount /* 71 FenceWaitServer */ static_cast(kWaitNone), /* 72 BindShaderImage */ static_cast(kWaitNone), /* 73 PatchParameter */ static_cast(kWaitNone), - /* 74 BindStreamOutput */ static_cast(kWaitApplied), - /* 75 SetStorageBlockBinding */ static_cast(kWaitApplied), - /* 76 CopyFramebufferToTexture*/ static_cast(kWaitApplied), + /* 74 BindStreamOutput */ static_cast(kWaitNone), + /* 75 SetStorageBlockBinding */ static_cast(kWaitNone), + /* 76 CopyFramebufferToTexture*/ static_cast(kWaitNone), /* 77 ApplierReset */ static_cast(kWaitApplied), /* 78 ObjectDeath */ static_cast(kWaitNone), /* 79 SetContextValues */ static_cast(kWaitNone), diff --git a/MobileGL/MG_Pipe/generated/PipeWireLayout.inc b/MobileGL/MG_Pipe/generated/PipeWireLayout.inc index 97e71585..e03f15fe 100644 --- a/MobileGL/MG_Pipe/generated/PipeWireLayout.inc +++ b/MobileGL/MG_Pipe/generated/PipeWireLayout.inc @@ -743,8 +743,8 @@ inline constexpr WireCallLayout kMGPipeWireCallLayouts[] = { {"ResourceSubDataComplete", "MGPSubDataComplete", static_cast(MGPWireOp::ResourceSubDataComplete), static_cast(kNone), static_cast(kWaitNone)}, {"ResourceFlushRange", "MGPFlushRange", static_cast(MGPWireOp::ResourceFlushRange), static_cast(kNone), static_cast(kWaitNone)}, {"ResourceReadback", "MGPReadback", static_cast(MGPWireOp::ResourceReadback), static_cast(kReplySlot), static_cast(kWaitReply)}, - {"ResourceCopyRegion", "MGPCopyRegion", static_cast(MGPWireOp::ResourceCopyRegion), static_cast(kNone), static_cast(kWaitApplied)}, - {"GenerateMipmap", "MGPMipPlan", static_cast(MGPWireOp::GenerateMipmap), static_cast(kNone), static_cast(kWaitApplied)}, + {"ResourceCopyRegion", "MGPCopyRegion", static_cast(MGPWireOp::ResourceCopyRegion), static_cast(kNone), static_cast(kWaitNone)}, + {"GenerateMipmap", "MGPMipPlan", static_cast(MGPWireOp::GenerateMipmap), static_cast(kNone), static_cast(kWaitNone)}, {"GetTextureImage", "MGPReadbackInfo", static_cast(MGPWireOp::GetTextureImage), static_cast(kReplySlot), static_cast(kWaitReply)}, {"Blit", "MGPBlit", static_cast(MGPWireOp::Blit), static_cast(kNone), static_cast(kWaitNone)}, {"Clear", "MGPClear", static_cast(MGPWireOp::Clear), static_cast(kNone), static_cast(kWaitNone)}, @@ -752,10 +752,10 @@ inline constexpr WireCallLayout kMGPipeWireCallLayouts[] = { {"DrawVbo", "MGPDrawInfo", static_cast(MGPWireOp::DrawVbo), static_cast(kHostSpan | kVarTail), static_cast(kWaitNone)}, {"LaunchGrid", "MGPGridInfo", static_cast(MGPWireOp::LaunchGrid), static_cast(kNone), static_cast(kWaitNone)}, {"MemoryBarrier", "MGPMemoryBarrier", static_cast(MGPWireOp::MemoryBarrier), static_cast(kNone), static_cast(kWaitNone)}, - {"BeginStreamOutput", "MGPStreamOutputBegin", static_cast(MGPWireOp::BeginStreamOutput), static_cast(kNone), static_cast(kWaitApplied)}, - {"EndStreamOutput", "MGPXfbAccounting", static_cast(MGPWireOp::EndStreamOutput), static_cast(kNone), static_cast(kWaitApplied)}, - {"PauseStreamOutput", "MGPStreamOutputControl", static_cast(MGPWireOp::PauseStreamOutput), static_cast(kNone), static_cast(kWaitApplied)}, - {"ResumeStreamOutput", "MGPStreamOutputControl", static_cast(MGPWireOp::ResumeStreamOutput), static_cast(kNone), static_cast(kWaitApplied)}, + {"BeginStreamOutput", "MGPStreamOutputBegin", static_cast(MGPWireOp::BeginStreamOutput), static_cast(kNone), static_cast(kWaitNone)}, + {"EndStreamOutput", "MGPXfbAccounting", static_cast(MGPWireOp::EndStreamOutput), static_cast(kNone), static_cast(kWaitNone)}, + {"PauseStreamOutput", "MGPStreamOutputControl", static_cast(MGPWireOp::PauseStreamOutput), static_cast(kNone), static_cast(kWaitNone)}, + {"ResumeStreamOutput", "MGPStreamOutputControl", static_cast(MGPWireOp::ResumeStreamOutput), static_cast(kNone), static_cast(kWaitNone)}, {"Flush", "MGPFlush", static_cast(MGPWireOp::Flush), static_cast(kNone), static_cast(kWaitNone)}, {"Present", "MGPPresent", static_cast(MGPWireOp::Present), static_cast(kNone), static_cast(kWaitPresent)}, {"SetSwapInterval", "MGPSwapInterval", static_cast(MGPWireOp::SetSwapInterval), static_cast(kOptional), static_cast(kWaitNone)}, @@ -764,9 +764,9 @@ inline constexpr WireCallLayout kMGPipeWireCallLayouts[] = { {"FenceWaitServer", "MGPFenceWait", static_cast(MGPWireOp::FenceWaitServer), static_cast(kNone), static_cast(kWaitNone)}, {"BindShaderImage", "MGPImageBind", static_cast(MGPWireOp::BindShaderImage), static_cast(kNone), static_cast(kWaitNone)}, {"PatchParameter", "MGPPatchParameter", static_cast(MGPWireOp::PatchParameter), static_cast(kNone), static_cast(kWaitNone)}, - {"BindStreamOutput", "MGPStreamOutputBind", static_cast(MGPWireOp::BindStreamOutput), static_cast(kNone), static_cast(kWaitApplied)}, - {"SetStorageBlockBinding", "MGPStorageBlockBinding", static_cast(MGPWireOp::SetStorageBlockBinding), static_cast(kHasBlob), static_cast(kWaitApplied)}, - {"CopyFramebufferToTexture", "MGPCopyFromFramebuffer", static_cast(MGPWireOp::CopyFramebufferToTexture), static_cast(kNone), static_cast(kWaitApplied)}, + {"BindStreamOutput", "MGPStreamOutputBind", static_cast(MGPWireOp::BindStreamOutput), static_cast(kNone), static_cast(kWaitNone)}, + {"SetStorageBlockBinding", "MGPStorageBlockBinding", static_cast(MGPWireOp::SetStorageBlockBinding), static_cast(kHasBlob), static_cast(kWaitNone)}, + {"CopyFramebufferToTexture", "MGPCopyFromFramebuffer", static_cast(MGPWireOp::CopyFramebufferToTexture), static_cast(kNone), static_cast(kWaitNone)}, {"ApplierReset", "MGPApplierReset", static_cast(MGPWireOp::ApplierReset), static_cast(kNone), static_cast(kWaitApplied)}, {"ObjectDeath", "MGPHandleOnly", static_cast(MGPWireOp::ObjectDeath), static_cast(kNone), static_cast(kWaitNone)}, {"SetContextValues", "MGPContextValues", static_cast(MGPWireOp::SetContextValues), static_cast(kNone), static_cast(kWaitNone)}, diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index 91b90d1b..b4b15de4 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -2408,6 +2408,7 @@ namespace MobileGL::MG_Remote::Client { return seq; } + ++m_recordWaits; if (MG_Config::Ipc.WaitStats && RunAheadArmed()) ++g_waitCensus.recordWaits[static_cast(op)]; const BarrierWaitScope waiting; const Uint64 waitBudgetMs = AppliedWaitBudgetMs(op, payload); @@ -2793,6 +2794,9 @@ namespace MobileGL::MG_Remote::Client { // is exactly "no record is being applied" and not merely "not decoding". if (isBarrieredFill && !ApplyThreadIsInsideApplier()) return; if (Server::ServerLoop::OnApplyThread()) return; // the applier owns the block + // P15: with two blocks the GL thread's fill writes the client block, which the + // applier never reads (the dual-block census is empty), so there is nothing to race. + if (isBarrieredFill && MG_Pipe::MGPipeBlocksAreDistinct()) return; SessionFail(MGFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"gPipeInputs\"} - the GL thread touched " "gPipeInputs (%s) on a RUN-AHEAD session %s. With the client running ahead " "the block is the server's to read for as long as ANY record is in flight " diff --git a/MobileGL/MG_Remote/Client/ClientSession.h b/MobileGL/MG_Remote/Client/ClientSession.h index 663e2625..d9d58fb5 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.h +++ b/MobileGL/MG_Remote/Client/ClientSession.h @@ -400,6 +400,8 @@ namespace MobileGL::MG_Remote::Client { // is never the frame's critical path; a number close to the frame count means the // credit is what paces this workload, which is what the device exit measures. Uint64 PresentCreditWaits() const; + // P15: how many records this session waited on (applied or reply) after publishing. + Uint64 RecordWaits() const { return m_recordWaits; } // §2.4, called by EmitPresent immediately before it encodes: pay the credit (parking // if this client already has `MOBILEGL_IPC_PRESENT_CREDIT` presents in flight), then @@ -630,6 +632,7 @@ namespace MobileGL::MG_Remote::Client { Uint32 m_pendingDamageCount = 0; Int32 m_pendingDamage[MG_Pipe::kMGPMaxDamageRects * 4] = {}; Uint64 m_presentCreditWaits = 0; + Uint64 m_recordWaits = 0; // sm: the spawn client owns ONE end and two bells. Held here rather // than in the transport because which bell is "mine" is the session's diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp index 740ad28e..63ea6b36 100644 --- a/MobileGL/MG_Remote/Client/WireTables.cpp +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -643,6 +643,28 @@ namespace MobileGL::MG_Remote::Client { // (PipeApply.h:298-304), so nobody re-sends those texels. So it takes the blocking // path instead, which is the pre-window shape exactly. const Uint32 window = session.LinkRetainsReplies() ? CreateWindowEffective() : 1u; + // P15: UNDER RUN-AHEAD A CREATE IS FIRE-AND-FORGET. The handle is the client's own + // allocation, and the server's accept is derivable: it declines only a reserved or + // out-of-table slot or a target with no table (all protocol corruption, trip-wired on + // the server) or a texture family with no consumer, which the client already knows + // and never emits for (P4aFamilyHasItsConsumer). Waiting for that answer was a + // blocking round trip per new object (FCL + BSL: 2-3 per frame). + // A link that retains answers (the stream arm) keeps its create window, which already + // defers the answer and reads it later; this is the arm without one (the shared pool). + if (window <= 1 && session.RunAheadArmed() && !CreateIsSuspect(payload->Resource)) { + Int32 status = Wire::ReplySink::kStatusError; + const Uint64 seq = session.EmitAndWaitTails( + MGPWireOp::ResourceCreate, payload, sizeof(*payload), nullptr, 0, nullptr, 0, + &status, nullptr, /*wantReply=*/false, /*willReadReply=*/false); + if (seq != Wire::kInvalidSeq) reply->Id = seq; + MG_Pipe::MGPipePostReply(*reply, + seq == Wire::kInvalidSeq ? Wire::ReplySink::kStatusDeclined + : Wire::ReplySink::kStatusOk, + seq == Wire::kInvalidSeq ? 0u : 1u); + ++g_emitted; + if (seq == Wire::kInvalidSeq) ++g_declined; + return; + } // THE WINDOW OFF RESTORES THE OLD SHAPE EXACTLY, and is the negative control for it. // THE WINDOW OFF, THE SUSPECT LIST FULL, OR AN OBJECT THAT HAS ALREADY BEEN REFUSED: // all four take the blocking branch, which is the pre-window shape - and on the device diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index dad23b9f..505bcdbc 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -769,28 +769,16 @@ TEST(PipeCatalogue, EveryRowCarriesTheWaitClassTheContractGivesIt) { << WireOpNameForDiag(op); } - // kWaitApplied: the rows whose apply STILL reads a BARRIER_PULLED row or probes the client - // allocator, so the client parks in its own wait and CONTRACT-P5C's semantics hold for them - // unchanged - their pulls count into rsp and their probes are legal inside a scope. Two of - // them are trailing items and are expected to leave this list in a later package: - // generate_mipmap the moment tx2 resolves the texture from VerbMipRes instead of the active - // unit, and set_storage_block_binding the moment pg resolves it through - // MGPStorageBlockBinding::ShaderCso. When they do, THIS list is what has to be edited. + // kWaitApplied: only applier_reset is left. P15 (user decision: everything fire-and-forget + // is async) moved generate_mipmap, resource_copy_region, copy_framebuffer_to_texture, + // set_storage_block_binding and the five stream-output rows to kWaitNone: their applies + // resolve everything from the record's own handles (VerbMipRes, VerbCopyTexDst, the copy's + // endpoints, the storage-block program CSO, the P5f server-side span), and the dual-block + // census - where any client-filled read is a named Fatal - is empty. applier_reset keeps its + // wait: it is once per make-current and its apply probes the client allocator, which is only + // legal inside a parked scope. const MGPWireOp appliedRows[] = { - MGPWireOp::ApplierReset, MGPWireOp::GenerateMipmap, - MGPWireOp::SetStorageBlockBinding, MGPWireOp::BeginStreamOutput, - MGPWireOp::EndStreamOutput, MGPWireOp::PauseStreamOutput, - MGPWireOp::ResumeStreamOutput, MGPWireOp::BindStreamOutput, - MGPWireOp::CopyFramebufferToTexture, - // P5e (gl), ID-118. resource_copy_region JOINED THIS SET, and it is the retiring phase - // of a field that put it here: its verb is CopyImageSubData, whose apply reads - // GetTextureObject, and that row retires in P7. An unbarriered record reading client - // memory is an unconditional Fatal, so leaving it kWaitNone would have meant a real - // defect standing for two phases - or an exception hand-written into a DERIVED - // allowlist, which is the same thing with a comment on it. It sits beside - // copy_framebuffer_to_texture here for the reason it took that class: same verb class, - // no reply slot, and zero calls per frame in the measured scene. - MGPWireOp::ResourceCopyRegion, + MGPWireOp::ApplierReset, }; for (const MGPWireOp op : appliedRows) { EXPECT_EQ(MGPipeWaitClassFor(op), kWaitApplied) << WireOpNameForDiag(op); @@ -812,12 +800,17 @@ TEST(PipeCatalogue, EveryRowCarriesTheWaitClassTheContractGivesIt) { MGPWireOp::CreateShaderState, MGPWireOp::SetDrawProgram, // P9: the pack-buffer readbacks - the point of the two rows is that nothing waits. MGPWireOp::ReadPixelsToBuffer, MGPWireOp::GetTextureImageToBuffer, + // P15: fire-and-forget. + MGPWireOp::GenerateMipmap, MGPWireOp::ResourceCopyRegion, MGPWireOp::CopyFramebufferToTexture, + MGPWireOp::SetStorageBlockBinding, MGPWireOp::BeginStreamOutput, MGPWireOp::EndStreamOutput, + MGPWireOp::PauseStreamOutput, MGPWireOp::ResumeStreamOutput, MGPWireOp::BindStreamOutput, }; for (const MGPWireOp op : noneRows) { EXPECT_EQ(MGPipeWaitClassFor(op), kWaitNone) << WireOpNameForDiag(op); } - // The partition, by count. 15 + 1 + 10 = 26 rows wait (shared_image made the reply rows 15); every other row of the catalogue does + // The partition, by count. 15 + 1 + 1 = 17 rows wait (shared_image made the reply rows 15; P15 + // left one kWaitApplied row); every other row of the catalogue does // not. A row that changed class moves two of these numbers at once - which is why the // kWaitApplied count went 9 -> 10 and the kWaitNone offset 24 -> 25 in the SAME commit that // moved resource_copy_region (P5e gl, ID-118). @@ -832,9 +825,9 @@ TEST(PipeCatalogue, EveryRowCarriesTheWaitClassTheContractGivesIt) { } } EXPECT_EQ(reply, 15u); - EXPECT_EQ(applied, 10u); + EXPECT_EQ(applied, 1u); EXPECT_EQ(present, 1u); - EXPECT_EQ(none, static_cast(kMGPipeCallCount) - 26u); + EXPECT_EQ(none, static_cast(kMGPipeCallCount) - 17u); EXPECT_EQ(other, 0u) << "a row carries the kWaitClassCount terminator as its class"; // And the reply half of the partition BOTH WAYS, over the whole catalogue: exactly the rows diff --git a/MobileGL/MG_Test/Wire/RemoteClientE1Controls.inc b/MobileGL/MG_Test/Wire/RemoteClientE1Controls.inc index 3b41206d..54236f74 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientE1Controls.inc +++ b/MobileGL/MG_Test/Wire/RemoteClientE1Controls.inc @@ -16,7 +16,7 @@ struct E1BoundaryPeer : Codec::WireVerbSink { while (!bell.entered.load() && !clientReturned.load()) std::this_thread::yield(); return true; } - Bool OnGenerateMipmap(const MGPMipPlan&) override { return Apply(MGPWireOp::GenerateMipmap); } + Bool OnApplierReset(const MGPApplierReset&) override { return Apply(MGPWireOp::ApplierReset); } Bool OnClear(const MGPClear&) override { return Apply(MGPWireOp::Clear); } void Install() { if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) { @@ -49,10 +49,11 @@ void CheckE1Boundary(Bool serverCap, MGPWireOp op, Bool expectWait, const char* peer.Install(); const Uint64 before = session.Control()->submittedSeq.load(); producer.*PeerMember(ProducerBellTag{}) = &bell; - const MGPMipPlan mip{}; + const MGPApplierReset reset{}; const MGPClear clear{}; - const Uint64 seq = op == MGPWireOp::GenerateMipmap - ? session.EmitAndWait(op, &mip, sizeof(mip), nullptr, 0, nullptr, 0, nullptr) + // 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); // Observe BEFORE allowing the peer to complete a non-waiting emission. const Bool parked = bell.entered.load(); @@ -63,7 +64,7 @@ void CheckE1Boundary(Bool serverCap, MGPWireOp op, Bool expectWait, const char* 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", - op == MGPWireOp::GenerateMipmap ? "GenerateMipmap" : "Clear", + op == MGPWireOp::ApplierReset ? "ApplierReset" : "Clear", static_cast(serverCap), MG_Config::Ipc.VerbBarrier, static_cast(parked), static_cast(appliedBeforeReturn)); std::fflush(stdout); @@ -80,9 +81,9 @@ void CheckE1Boundary(Bool serverCap, MGPWireOp op, Bool expectWait, const char* TEST(RemoteWaitBoundaryControl, AppliedClassWaitsWithRunAheadCap) { #if MGTEST_HAVE_FORK - ASSERT_EQ(MGPipeWaitClassFor(MGPWireOp::GenerateMipmap), kWaitApplied); - CheckE1Boundary(true, MGPWireOp::GenerateMipmap, true, - "E1 wait boundary: GenerateMipmap returned before apply"); + ASSERT_EQ(MGPipeWaitClassFor(MGPWireOp::ApplierReset), kWaitApplied); + CheckE1Boundary(true, MGPWireOp::ApplierReset, true, + "E1 wait boundary: ApplierReset returned before apply"); #else GTEST_SKIP() << "E1 requires POSIX child isolation"; #endif diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index 702b96e8..741ce644 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -2587,6 +2587,9 @@ void StartRunAheadSession() { ::alarm(15); MG_Config::Transport = MG_Config::TransportMode::InProcess; MG_Config::Ipc.RunAhead = 1; + // The run-ahead guards below are the SHARED-block rules; the dual block (the default since + // P15) makes a barriered fill the client's own and is covered by DualBlockScenario. + MG_Config::Ipc.RoleSplitState = false; Srv::ServerSessionInstance().SetCapabilityBits( static_cast(MG_Pipe::kCapRunAheadApply)); Srv::ServerSessionInstance().SetConsumedSubsystems(kMGPipeSubsystemsMigratedAtP4a); @@ -2828,6 +2831,91 @@ TEST(RemoteRunAhead, AnIndexedDrawVerbIsUnbarrieredAndTouchesPipeInputsNotAtAll) ExpectChildSuccess(child); } +// P15: FIRE-AND-FORGET IS THE DEFAULT. The texture-op copies, mip generation and the transform +// feedback span calls owe the client no answer, so under run-ahead each publishes and returns. +// The wait classes are pinned at compile time (the red: put any of them back to kWaitApplied and +// this file stops compiling); the case below proves the client really does not wait on them. +static_assert(MGPipeWaitClassFor(MGPWireOp::GenerateMipmap) == kWaitNone); +static_assert(MGPipeWaitClassFor(MGPWireOp::ResourceCopyRegion) == kWaitNone); +static_assert(MGPipeWaitClassFor(MGPWireOp::CopyFramebufferToTexture) == kWaitNone); +static_assert(MGPipeWaitClassFor(MGPWireOp::BeginStreamOutput) == kWaitNone); +static_assert(MGPipeWaitClassFor(MGPWireOp::EndStreamOutput) == kWaitNone); +static_assert(MGPipeWaitClassFor(MGPWireOp::PauseStreamOutput) == kWaitNone); +static_assert(MGPipeWaitClassFor(MGPWireOp::ResumeStreamOutput) == kWaitNone); +static_assert(MGPipeWaitClassFor(MGPWireOp::BindStreamOutput) == kWaitNone); +static_assert(MGPipeWaitClassFor(MGPWireOp::SetStorageBlockBinding) == kWaitNone); + +struct AcceptAllPeer : Codec::WireVerbSink { + Bool OnGenerateMipmap(const MGPMipPlan&) override { return true; } + Bool OnResourceCopyRegion(const MGPCopyRegion&) override { return true; } + Bool OnCopyFramebufferToTexture(const MGPCopyFromFramebuffer&) override { return true; } + Bool OnBeginStreamOutput(const MGPStreamOutputBegin&) override { return true; } + Bool OnEndStreamOutput(const MGPXfbAccounting&) override { return true; } + Bool OnPauseStreamOutput(const MGPStreamOutputControl&) override { return true; } + Bool OnResumeStreamOutput(const MGPStreamOutputControl&) override { return true; } + Bool OnBindStreamOutput(const MGPStreamOutputBind&) override { return true; } + void Install() { + if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( + [](void* self) { + auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{}); + decoder.SetVerbSink(static_cast(self)); + return MOBILEGL_OK; + }, + this) != MOBILEGL_OK) { + ::_exit(82); + } + } +}; + +TEST(RemoteRunAhead, FireAndForgetRowsDoNotWaitForTheirApply) { + const auto child = RunInChild([] { + StartRunAheadSession(); + AcceptAllPeer peer; + peer.Install(); + ClientSession& session = ClientSessionInstance(); + const Uint64 before = session.RecordWaits(); + const auto emit = [&](MGPWireOp op, const auto& payload) { + session.EmitAndWait(op, &payload, sizeof(payload), nullptr, 0, nullptr, 0, nullptr); + }; + emit(MGPWireOp::GenerateMipmap, MGPMipPlan{}); + emit(MGPWireOp::ResourceCopyRegion, MGPCopyRegion{}); + emit(MGPWireOp::CopyFramebufferToTexture, MGPCopyFromFramebuffer{}); + emit(MGPWireOp::BindStreamOutput, MGPStreamOutputBind{}); + emit(MGPWireOp::BeginStreamOutput, MGPStreamOutputBegin{}); + emit(MGPWireOp::PauseStreamOutput, MGPStreamOutputControl{}); + emit(MGPWireOp::ResumeStreamOutput, MGPStreamOutputControl{}); + emit(MGPWireOp::EndStreamOutput, MGPXfbAccounting{}); + if (session.RecordWaits() != before) ::_exit(121); + session.Stop(); + }); + ExpectChildSuccess(child); +} + +// P15: a create is fire-and-forget under run-ahead. The handle is the client's own allocation +// and the server's accept is derivable (it declines only protocol corruption or a family with no +// consumer, which the client never emits for), so the call publishes and returns with a +// provisional accept instead of waiting for the answer. +// +// THE RED (done once while writing it): drop Wire_ResourceCreate's run-ahead fast path and the +// call takes the blocking path, so RecordWaits() moves and the child exits 112. +TEST(RemoteRunAhead, AResourceCreateDoesNotWaitForItsAnswer) { + const auto child = RunInChild([] { + StartRunAheadSession(); + ClientSession& session = ClientSessionInstance(); + const Uint64 before = session.RecordWaits(); + MG_Pipe::MGPResourceDesc desc{}; + desc.Resource = MG_Pipe::MGPipeHandle{MG_Pipe::kMGPipeFirstAllocatableSlot, 1}; + desc.Target = MG_Pipe::kMGPipeResourceTargetBuffer; + desc.Width = 64; + MG_Pipe::MGPReplySlot reply = MG_Pipe::MGPipeMintReplySlot(); + MG_Pipe::gMGPipeScreen.ResourceCreate(&desc, &reply); + if (!MG_Pipe::MGPipeTakeReplyBool(reply, "resource_create")) ::_exit(111); + if (session.RecordWaits() != before) ::_exit(112); + session.Stop(); + }); + ExpectChildSuccess(child); +} + // P15: the blit family. Every blit reaches the validate point as BlitNamedFramebuffer and // publishes one `blit` record (kWaitNone, stamped as BlitFramebuffer), so it is unbarriered: no // fill and no applier quiesce. A barriered answer here was a forced wait once per frame on FCL diff --git a/docs/Disaggregated/design/09-build-switches-counters.md b/docs/Disaggregated/design/09-build-switches-counters.md index 202e9654..16a6d425 100644 --- a/docs/Disaggregated/design/09-build-switches-counters.md +++ b/docs/Disaggregated/design/09-build-switches-counters.md @@ -60,7 +60,7 @@ CMake:`MOBILEGL_BUILD_DISAGGREGATED`(OFF)、`MOBILEGL_BUILD_DISAGGREGATED_ | `MOBILEGL_IPC_PERSISTENT_BLOCK_KB` / `_PERSISTENT_HASH_SUPPRESS` | 64 / 1 | persistent-map 推送粒度 / 只推变化的块;`0` 是对照 | | `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 / 0 | 残余输入读升级为 Fatal / 退休 staging 填 `0xDD` / 双块演练(P5f) | +| `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) | | `MOBILEGL_IPC_SPIN_US` / `MOBILEGL_IPC_SERVER_AFFINITY` | 50 / `auto` | park 前自旋预算 / apply 线程亲和(Redmi 内核忽略) | | `MOBILEGL_IPC_RECOVER` | 1 | session 因 device loss 结束后,应用下一次 `eglCreateContext` / `eglCreate*Surface` / `eglInitialize` 重新拨号得到新 session(旧 context 保持丢失,surface 在新 session 重建);`0` = 丢失对整个进程永久 | | `MOBILEGL_IPC_RESPAWN` | — | 具名拒绝:没有阶段实现 server 重启后的全量重推 |