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).

This commit is contained in:
2026-10-08 19:49:04 -04:00
parent 1771381057
commit 2fbcc22410
18 changed files with 234 additions and 64 deletions
+3 -2
View File
@@ -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.
+5 -1
View File
@@ -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); "
+37 -1
View File
@@ -43,6 +43,7 @@
#include <Config.h>
#if MOBILEGL_PIPE_VERIFY
#include <MG_Impl/Pipe/Verb/VerbPort.h>
#include <MG_Impl/Pipe/Verb/GpuWriteSet.h>
#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
+5
View File
@@ -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, "<Field>@<none>"} - which is what such a read
@@ -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<Uint8> bytes(static_cast<SizeT>(tight));
const Uint64 capacity = session.MaxReplyBytes();
+3
View File
@@ -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
@@ -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
@@ -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);
+9 -9
View File
@@ -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 ---- */ \
+9 -9
View File
@@ -443,8 +443,8 @@ inline constexpr Uint8 kMGPipeWaitClasses[static_cast<SizeT>(MGPWireOp::kOpCount
/* 50 ResourceSubDataComplete */ static_cast<Uint8>(kWaitNone),
/* 51 ResourceFlushRange */ static_cast<Uint8>(kWaitNone),
/* 52 ResourceReadback */ static_cast<Uint8>(kWaitReply),
/* 53 ResourceCopyRegion */ static_cast<Uint8>(kWaitApplied),
/* 54 GenerateMipmap */ static_cast<Uint8>(kWaitApplied),
/* 53 ResourceCopyRegion */ static_cast<Uint8>(kWaitNone),
/* 54 GenerateMipmap */ static_cast<Uint8>(kWaitNone),
/* 55 GetTextureImage */ static_cast<Uint8>(kWaitReply),
/* 56 Blit */ static_cast<Uint8>(kWaitNone),
/* 57 Clear */ static_cast<Uint8>(kWaitNone),
@@ -452,10 +452,10 @@ inline constexpr Uint8 kMGPipeWaitClasses[static_cast<SizeT>(MGPWireOp::kOpCount
/* 59 DrawVbo */ static_cast<Uint8>(kWaitNone),
/* 60 LaunchGrid */ static_cast<Uint8>(kWaitNone),
/* 61 MemoryBarrier */ static_cast<Uint8>(kWaitNone),
/* 62 BeginStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 63 EndStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 64 PauseStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 65 ResumeStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 62 BeginStreamOutput */ static_cast<Uint8>(kWaitNone),
/* 63 EndStreamOutput */ static_cast<Uint8>(kWaitNone),
/* 64 PauseStreamOutput */ static_cast<Uint8>(kWaitNone),
/* 65 ResumeStreamOutput */ static_cast<Uint8>(kWaitNone),
/* 66 Flush */ static_cast<Uint8>(kWaitNone),
/* 67 Present */ static_cast<Uint8>(kWaitPresent),
/* 68 SetSwapInterval */ static_cast<Uint8>(kWaitNone),
@@ -464,9 +464,9 @@ inline constexpr Uint8 kMGPipeWaitClasses[static_cast<SizeT>(MGPWireOp::kOpCount
/* 71 FenceWaitServer */ static_cast<Uint8>(kWaitNone),
/* 72 BindShaderImage */ static_cast<Uint8>(kWaitNone),
/* 73 PatchParameter */ static_cast<Uint8>(kWaitNone),
/* 74 BindStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 75 SetStorageBlockBinding */ static_cast<Uint8>(kWaitApplied),
/* 76 CopyFramebufferToTexture*/ static_cast<Uint8>(kWaitApplied),
/* 74 BindStreamOutput */ static_cast<Uint8>(kWaitNone),
/* 75 SetStorageBlockBinding */ static_cast<Uint8>(kWaitNone),
/* 76 CopyFramebufferToTexture*/ static_cast<Uint8>(kWaitNone),
/* 77 ApplierReset */ static_cast<Uint8>(kWaitApplied),
/* 78 ObjectDeath */ static_cast<Uint8>(kWaitNone),
/* 79 SetContextValues */ static_cast<Uint8>(kWaitNone),
@@ -743,8 +743,8 @@ inline constexpr WireCallLayout kMGPipeWireCallLayouts[] = {
{"ResourceSubDataComplete", "MGPSubDataComplete", static_cast<Uint64>(MGPWireOp::ResourceSubDataComplete), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"ResourceFlushRange", "MGPFlushRange", static_cast<Uint64>(MGPWireOp::ResourceFlushRange), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"ResourceReadback", "MGPReadback", static_cast<Uint64>(MGPWireOp::ResourceReadback), static_cast<Uint64>(kReplySlot), static_cast<Uint64>(kWaitReply)},
{"ResourceCopyRegion", "MGPCopyRegion", static_cast<Uint64>(MGPWireOp::ResourceCopyRegion), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"GenerateMipmap", "MGPMipPlan", static_cast<Uint64>(MGPWireOp::GenerateMipmap), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"ResourceCopyRegion", "MGPCopyRegion", static_cast<Uint64>(MGPWireOp::ResourceCopyRegion), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"GenerateMipmap", "MGPMipPlan", static_cast<Uint64>(MGPWireOp::GenerateMipmap), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"GetTextureImage", "MGPReadbackInfo", static_cast<Uint64>(MGPWireOp::GetTextureImage), static_cast<Uint64>(kReplySlot), static_cast<Uint64>(kWaitReply)},
{"Blit", "MGPBlit", static_cast<Uint64>(MGPWireOp::Blit), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"Clear", "MGPClear", static_cast<Uint64>(MGPWireOp::Clear), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
@@ -752,10 +752,10 @@ inline constexpr WireCallLayout kMGPipeWireCallLayouts[] = {
{"DrawVbo", "MGPDrawInfo", static_cast<Uint64>(MGPWireOp::DrawVbo), static_cast<Uint64>(kHostSpan | kVarTail), static_cast<Uint64>(kWaitNone)},
{"LaunchGrid", "MGPGridInfo", static_cast<Uint64>(MGPWireOp::LaunchGrid), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"MemoryBarrier", "MGPMemoryBarrier", static_cast<Uint64>(MGPWireOp::MemoryBarrier), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"BeginStreamOutput", "MGPStreamOutputBegin", static_cast<Uint64>(MGPWireOp::BeginStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"EndStreamOutput", "MGPXfbAccounting", static_cast<Uint64>(MGPWireOp::EndStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"PauseStreamOutput", "MGPStreamOutputControl", static_cast<Uint64>(MGPWireOp::PauseStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"ResumeStreamOutput", "MGPStreamOutputControl", static_cast<Uint64>(MGPWireOp::ResumeStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"BeginStreamOutput", "MGPStreamOutputBegin", static_cast<Uint64>(MGPWireOp::BeginStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"EndStreamOutput", "MGPXfbAccounting", static_cast<Uint64>(MGPWireOp::EndStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"PauseStreamOutput", "MGPStreamOutputControl", static_cast<Uint64>(MGPWireOp::PauseStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"ResumeStreamOutput", "MGPStreamOutputControl", static_cast<Uint64>(MGPWireOp::ResumeStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"Flush", "MGPFlush", static_cast<Uint64>(MGPWireOp::Flush), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"Present", "MGPPresent", static_cast<Uint64>(MGPWireOp::Present), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitPresent)},
{"SetSwapInterval", "MGPSwapInterval", static_cast<Uint64>(MGPWireOp::SetSwapInterval), static_cast<Uint64>(kOptional), static_cast<Uint64>(kWaitNone)},
@@ -764,9 +764,9 @@ inline constexpr WireCallLayout kMGPipeWireCallLayouts[] = {
{"FenceWaitServer", "MGPFenceWait", static_cast<Uint64>(MGPWireOp::FenceWaitServer), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"BindShaderImage", "MGPImageBind", static_cast<Uint64>(MGPWireOp::BindShaderImage), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"PatchParameter", "MGPPatchParameter", static_cast<Uint64>(MGPWireOp::PatchParameter), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"BindStreamOutput", "MGPStreamOutputBind", static_cast<Uint64>(MGPWireOp::BindStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"SetStorageBlockBinding", "MGPStorageBlockBinding", static_cast<Uint64>(MGPWireOp::SetStorageBlockBinding), static_cast<Uint64>(kHasBlob), static_cast<Uint64>(kWaitApplied)},
{"CopyFramebufferToTexture", "MGPCopyFromFramebuffer", static_cast<Uint64>(MGPWireOp::CopyFramebufferToTexture), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"BindStreamOutput", "MGPStreamOutputBind", static_cast<Uint64>(MGPWireOp::BindStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"SetStorageBlockBinding", "MGPStorageBlockBinding", static_cast<Uint64>(MGPWireOp::SetStorageBlockBinding), static_cast<Uint64>(kHasBlob), static_cast<Uint64>(kWaitNone)},
{"CopyFramebufferToTexture", "MGPCopyFromFramebuffer", static_cast<Uint64>(MGPWireOp::CopyFramebufferToTexture), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"ApplierReset", "MGPApplierReset", static_cast<Uint64>(MGPWireOp::ApplierReset), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"ObjectDeath", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::ObjectDeath), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"SetContextValues", "MGPContextValues", static_cast<Uint64>(MGPWireOp::SetContextValues), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
@@ -2408,6 +2408,7 @@ namespace MobileGL::MG_Remote::Client {
return seq;
}
++m_recordWaits;
if (MG_Config::Ipc.WaitStats && RunAheadArmed()) ++g_waitCensus.recordWaits[static_cast<SizeT>(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 "
@@ -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
+22
View File
@@ -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
+17 -24
View File
@@ -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<SizeT>(kMGPipeCallCount) - 26u);
EXPECT_EQ(none, static_cast<SizeT>(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
@@ -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<unsigned>(serverCap), MG_Config::Ipc.VerbBarrier,
static_cast<unsigned>(parked), static_cast<unsigned>(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
@@ -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<Uint64>(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<AcceptAllPeer*>(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
@@ -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 重启后的全量重推 |