mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 10:03:23 +09:00
[MG_Remote, MG_Pipe, MG_Impl, MG_Test, MG_IntegrationTest, Docs] (Disaggregated): inproc performance package - dirty-driven persistent-map push, batched barrier waits, spin-clock batching
VD12 Minecraft on the Redmi (FCL, DirectGLES): the split's in-world frame rate
13 -> 60-66 fps against monolith's 110-114, and persistent-map wire bytes
25 MB/frame -> ~0.23 MB/frame. The residual split-specific costs are named at
the end, not hidden: the R-1 serialization (WaitForApplied ~12%, P11's
versioned-inputs design) and a split-only mutex complex still under
investigation.
Persistent-map push, three arms picked per buffer at map time:
- mprotect dirty tracker: the mapped range's page-aligned interior is
protected PROT_READ; the app's next write faults, the SIGSEGV handler marks
the page in the slot's bitmap and unprotects, and the push ships one 64 KB
block per block containing a faulted page. Ownership is three tests -
SEGV_ACCERR only (a userspace raise and an unmapped page chain verbatim),
inside a live tracked range, and not a refault on a still-set bit (a write
we unprotected succeeds, so that is a non-write access and it crashes
honestly). The alignment is INWARD: an outward-aligned edge page holds
foreign bytes, and a signal-blocking worker thread faulting on one is a
process kill - that was the ctest-only SmallRing segfault. Edge spans
(at most two per map) are hashed per push and ship their containing block
only on change (unconditional edges measured 112 MB/frame). Re-arm is one
mprotect per contiguous run. The first push ships everything once.
- hash-suppress arm (MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS, default 1):
per-block XXH3 against the last push's hashes; untracked buffers only.
- PushDrawConsumers: only the buffers the upcoming draw/dispatch can read
(VAO attribs/element, indexed binding points, indirect/parameter slots,
TexBuffer backing - the last also retires the per-frame-cadence cloud
twitch), plus the fault-epoch skip: an unmoved epoch proves no interior
byte changed, tracked edges are hashed in place, and any hash-arm member
vetoes. The membership map caches the tracker slot pointer (a 64-slot
rescan per buffer per draw was measured and removed).
MOBILEGL_IPC_BATCH_WAITS (default 1; MOBILEGL_PIPE_VERIFY forces 0): a
value-class record (kCtxState/kCtxCso/kCtxObject with no reply slot) is
published without waiting for its own apply; the barrier is taken at the next
pull-reading verb, the only place BARRIER-PULLED fields are read, so the R-1
fence posts are untouched and only the round trips between them change. Two
consequences are pinned rather than argued: kCtxObject verbs the tests read
synchronously get explicit fences (GenerateMipmapFieldsCross,
FramebufferDeathCrosses), and a frontend object whose last SharedPtr drops on
the apply thread - impossible before batching - now enqueues its
(kind, lifetimeId) on the deferred destroy queue and the GL thread replays
the DestroyAndFree helper at the verb hooks (CopyImageLayeredScenario aborted
Fatal{RoleViolation, MGPipeSlots} without it).
Doorbell::Wait's spin reads the clock once per 64 yields: on the workload
device a steady_clock read is a real syscall (no vDSO) and the per-iteration
reads were ~20% of the whole process at ~1000 waits/frame.
Gates: gen_pipe --check/--self-test green; unit 2188/2188;
integration-split 110/111, the one red being GenerateMipmapDepthPixels whose
DirectGLES and DirectVulkan MONOLITH siblings flake identically with every
code path of this package provably inert (PushIsArmed false, no ClientSession,
deferral's monolith early-out).
This commit is contained in:
@@ -827,13 +827,17 @@ namespace MobileGL::MG_Remote::Client {
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// R-1's INVARIANT, AS A RUNTIME CHECK RATHER THAN A SENTENCE. While the barrier holds,
|
||||
// at most one of {GL thread, apply thread} is runnable - and that is the whole reason a
|
||||
// single process-wide gPipeInputs is legal (table 3). The client is about to publish a
|
||||
// record whose fields the applier will read out of gPipeInputs, so the apply thread
|
||||
// being inside the applier right now means the invariant has already been broken and
|
||||
// the next record would be read against a half-written residual fill.
|
||||
if (ApplyThreadIsInsideApplier()) {
|
||||
// R-1's INVARIANT, AS A RUNTIME CHECK RATHER THAN A SENTENCE - but only while the
|
||||
// per-record barrier is the fencing model. With MOBILEGL_IPC_BATCH_WAITS=1 the
|
||||
// intended shape is exactly "the apply thread is inside the applier while the GL
|
||||
// thread emits the next value record", and the check would fire on the first batch:
|
||||
// its protection is already carried by the disjoint-field model (record-supplied
|
||||
// fields are written only by the applier from records and are never filled by the
|
||||
// client while the wire is live; BARRIER-PULLED fields are written only by the
|
||||
// client's residual fill and are read only at the pull-verbs, whose own wait makes
|
||||
// the fill quiescent during that apply). With the batch off, the original check
|
||||
// stands as written.
|
||||
if (MG_Config::Ipc.BatchWaits == 0 && ApplyThreadIsInsideApplier()) {
|
||||
MGLOG_F("MGPipe: Fatal{BarrierViolation, \"%s\"} - the apply thread is inside the "
|
||||
"applier while the GL thread is emitting. R-1 makes at most one of them "
|
||||
"runnable, which is what keeps one process-wide gPipeInputs legal",
|
||||
@@ -879,6 +883,27 @@ namespace MobileGL::MG_Remote::Client {
|
||||
const Bool ownsReplySlot =
|
||||
(MG_Pipe::MGPipeCallFlagsFor(op) & static_cast<Uint32>(MG_Pipe::kReplySlot)) != 0;
|
||||
|
||||
// MOBILEGL_IPC_BATCH_WAITS (default 1): a value-class record is published WITHOUT
|
||||
// waiting for its own apply. The barrier's one load-bearing reader is the backend
|
||||
// sync that pulls the object-class fields (CONTRACT-P5 table 2's remaining rows), and
|
||||
// those reads happen only at the kCtxVerb applies - draw / clear / blit / dispatch /
|
||||
// readback / XFB - plus the screen and query classes, and EVERY ONE OF THOSE STILL
|
||||
// WAITS below (as does every reply-slot row, whose answer the client consumes). So
|
||||
// the pulled fields are exactly as fresh at every read point as the per-record
|
||||
// barrier made them: between two waits the residual fill may move a field, but no
|
||||
// apply is reading it in between, and the next pull-verb's fill runs after the
|
||||
// previous pull-verb's apply was waited - the R-1 fence posts are untouched, only
|
||||
// the number of round trips between them changes. Set / CSO / object records carry
|
||||
// their whole value in the payload and are consumed at the next (waited) sync, which
|
||||
// the in-order ring guarantees precedes that sync's apply.
|
||||
if (!ownsReplySlot && MG_Config::Ipc.BatchWaits != 0) {
|
||||
const MG_Pipe::MGPipeCallClass callClass = MG_Pipe::MGPipeCallClassFor(op);
|
||||
if (callClass == MG_Pipe::kCtxState || callClass == MG_Pipe::kCtxCso ||
|
||||
callClass == MG_Pipe::kCtxObject) {
|
||||
return seq;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_barrierArmed && !ownsReplySlot) {
|
||||
// R-1's NEGATIVE CONTROL ARM, and the only thing it turns off is the barrier. A
|
||||
// reply-slot row still waits: the answer is not derivable here and R-5 forbids
|
||||
@@ -978,6 +1003,13 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// by the operator's own hand there, and EmitAndWait's Fatal{BarrierViolation} owns the
|
||||
// red. Firing here instead would pre-empt the control's evidence line.
|
||||
if (!ClientSessionInstance().BarrierArmed()) return;
|
||||
// MOBILEGL_IPC_BATCH_WAITS=1 makes "the fill runs while the apply thread applies an
|
||||
// earlier value record" the INTENDED shape: the fill writes only fields no record
|
||||
// supplies (the wire-live skips are the same answer the emitters use), the applier
|
||||
// writes only record-supplied fields, and the pull-verbs' own wait keeps their
|
||||
// pulled reads fenced. This guard exists for the model where that is not true, so
|
||||
// it stands only while the batch is off.
|
||||
if (MG_Config::Ipc.BatchWaits != 0) return;
|
||||
if (!ApplyThreadIsInsideApplier()) return;
|
||||
if (InBarrierWait()) return;
|
||||
MGLOG_F("MGPipe: Fatal{RoleViolation, \"gPipeInputs\"} - the GL thread touched gPipeInputs "
|
||||
|
||||
Reference in New Issue
Block a user