mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Perf] (MG_Remote/Client): the create window lands OFF - the deferral cannot be collected, 8 reply slots against 59,673 replies in one frame - measured on the device at 4,536 blocking creates either way
The window was the handoff's remaining item: let up to N resource_create answers be
outstanding, read a provisional accept, collect the answers in order, and un-latch an
object whose late answer is DECLINED. It is a sound mechanism and the gates below pin
it - and on the device it does nothing at all, which is why the default is 1.
MEASURED, same device, same frame, same 101,421,396 B staged, same binary:
MOBILEGL_IPC_CREATE_WINDOW=4 by_op 2 = 4,536 wall 28.64 s
MOBILEGL_IPC_CREATE_WINDOW=1 by_op 2 = 4,536 wall 28.64 s
(the handoff's pre-window baseline: 31.3 s)
by_op 2 counts the creates that took the BLOCKING path (LinkMetricsBeginReply is
called with ownsReplySlot = rowCarriesReplySlot && wantReply), so it not moving means
no create deferred. A temporary counter in the row said why: pushed=24, blocked=4,476,
took=20, readfail=8,951, pending pinned at 4 - the drain READ the answers back and
failed every time.
WHY, IN THE PROTOCOL'S OWN ARITHMETIC. An answer is readable only until its pool slot
is reused, and the pool is kDefaultReplySlotCount = 8. The server answers EVERY
reply-owning record, not only the rows that wait, because it cannot know which those
are (ClientSession.cpp:1897-1900: "The server posts either way"). This frame posts
59,673 of them: create 4,536 + respecify 20,633 + params 18,056 + sub-data 12,671 +
the rest. 8 slots against 59,673 replies churns 7,459 times per frame, i.e. 13.2
replies between two creates, so an answer is overwritten after 8 replies - 0.61 of a
create. The deferral is not slow, it is UNREADABLE, and no window depth fixes that.
AND IT IS NOT NEUTRAL: with the window on, the provisional accept latches an object
the applier may have refused, and the un-latch that corrects it lives in that same
unreadable drain. A refusal would become an object the client believes is published -
the silent divergence the suspect list exists to prevent. So the machinery is kept
(one experiment away from usable: a deeper pool, or a "do not answer me" bit, both
wire changes) and the default is off.
Worktree note: the window implementation itself was uncommitted in the tree at
82ed1df9 (MOBILEGL-CS-HANDOFF-2.md section 2, "can compile, gates mostly green, never
run on a device"). This commit lands it in the state the measurement supports. The
row also gains the heal the suspect list was missing - a BLOCKING create that lands
takes its object back out of it, or kCreateSuspectMax recoveries would retire the
window for the rest of the process - and the clamp that ties the environment's value
to the array, with a static_assert tying the array to half the reply pool so "someone
raises kCreateWindowMax to 8 and steps on the pool" is a compile error.
Gates (RemoteClientControls.inc), all green, 171/171 RemoteClientTest:
ACreateWindowDefersTheAnswersAndTheWindowOffDoesNot - window on = 12 records 0 waits;
the SAME CHILD turns the knob to 1 and it is 12 records 12 waits (the negative
control); pending <= effective after every create; 99 clamps to the array; the
array is <= half the pool
ARefusedCreateUnlatchesItsObjectAndItsNextCreateBlocks - a real refusal (the
server's consumed mask narrowed) clears the latch, +1 suspect, the object's next
create BLOCKS (2 records 2 waits), and restoring the consumer HEALS it (latch
back, suspect -1)
Docs: CONTRACT-P5E.md section 2.5 rewritten (it still said all three rows keep
kWaitReply), plus
docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md (the numbers, the arithmetic,
the picture evidence: both arms SSIM 0.977) and
docs/Disaggregated/notes/p12/UNPUBLISH-GUARD-SIGNALS.md (handoff section 3: neither
caps generation nor FreshlyPrimed is a signal for "the applier dropped its records",
and the residual gap is one line - the dedup at TextureEmit.h:838 does not read the
latch, so an UNCHANGED descriptor after a drop produces no record and therefore no
refusal to self-heal from).
This commit is contained in:
@@ -548,6 +548,20 @@ namespace MobileGL::MG_Config {
|
||||
// every reply-slot row), which is the only place BARRIER-PULLED fields are read. 0
|
||||
// restores the per-record barrier of R-1.
|
||||
Uint32 BatchWaits = 1;
|
||||
// MOBILEGL_IPC_CREATE_WINDOW: how many RESOURCE_CREATE answers may be outstanding before the
|
||||
// client waits for one. 1 = every create waits, which is the shape R-5 shipped with AND THE
|
||||
// DEFAULT, because the deferral was measured on the device and does not work: the reply pool
|
||||
// is 8 slots and one load frame posts 59,673 replies (create 4,536 + respecify 20,633 +
|
||||
// params 18,056 + sub-data 12,671 + the rest), so an answer is overwritten after 8 replies -
|
||||
// 0.61 of a create - and the window's drain can never read one back. All 4,536 creates took
|
||||
// the blocking path at 4 and at 1 alike (28.64 s both ways). Worse than inert: the
|
||||
// provisional accept latches an object the applier may have refused, and the un-latch that
|
||||
// corrects it lives in that same unreadable drain. WireTables.cpp's section comment has the
|
||||
// counters and the arithmetic, and the gates in RemoteClientControls.inc still pin the
|
||||
// mechanism, which is sound and simply unusable against this much other reply traffic.
|
||||
// N > 1 is reachable for the experiment that would make it usable (a deeper pool, or a way
|
||||
// to tell the server not to answer a fire-and-forget row).
|
||||
Uint32 CreateWindow = 1;
|
||||
// MOBILEGL_IPC_ADOPT_TIER: 2 = emulate (client keeps the shadow and pushes), which
|
||||
// is the only tier P5 implements and the reason persistent-map-push can be non-zero
|
||||
// at all (R-6). 0 and 1 parse and are Fatal at use with "P11"; they exist now so the
|
||||
|
||||
@@ -412,6 +412,11 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// xxHash64 changed since the last push are shipped.
|
||||
ipc.PersistentHashSuppress = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS", 1, 0, 1);
|
||||
ipc.BatchWaits = QueryEnvUint32("MOBILEGL_IPC_BATCH_WAITS", 1, 0, 1);
|
||||
// The create window. DEFAULT 1 - OFF - because the deferral was measured on the device and
|
||||
// cannot be collected (Config.h has the numbers, WireTables.cpp the arithmetic: 59,673
|
||||
// replies per frame into 8 pool slots). Capped at 4 rather than the pool's 8 for the reason
|
||||
// the mechanism is sized that way at all.
|
||||
ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 1, 1, 4);
|
||||
// The verify harness compares the pushed block against the applier per verb; a
|
||||
// batched queue lets the comparer read a supplied field mid-apply, which is a
|
||||
// torn read rather than a divergence. The batch is therefore off whenever the
|
||||
|
||||
@@ -206,20 +206,47 @@ non-wire arm keep the old answer, which is what G1 measures. Gate:
|
||||
`RemoteClientControls.TextureUploadsStopOwningAReplyOnTheWireArm` reads back the same `wait_replies`
|
||||
the P65LinkMetrics line prints (`LinkMetricsReplyWaits`), asserting a pure upload moves it by ZERO
|
||||
while a `resource_create` row still moves it; red-once with the arm test removed is exit 142.
|
||||
**`ResourceCreate`, `ResourceRespecify` and `SetTextureParams` stay `kWaitReply`, and that is a
|
||||
ruling rather than an omission**: each one's answer carries a fact about the SERVER's record table
|
||||
that the client's own tracker provably cannot know. `RespecifyOnce` (`TextureEmit.h:1332-1356`) says
|
||||
it in one line - "the applier's REFUSAL is the only signal that says 'I hold nothing for this
|
||||
handle'", which is the scope `MGPipeApplierReleaseObjectRecords` leaves behind for a served
|
||||
context whose frontend objects live on; `EmitTextureParams` (`:991-1017`) uses its own refusal as
|
||||
the same self-heal trigger; and `PublishCreate` (`:1303-1319`, D-I1/c0b) may not take the
|
||||
publication latch on a create the applier refused, or the death path emits a `resource_destroy` for
|
||||
a record that does not exist. Removing any of those waits removes the signal, so unlike the upload
|
||||
half they are not "the emission gate already answered this" rows. Measured split for the two shapes
|
||||
a stitcher produces (P12, `RemoteClientControls.ReplyWaitsByOpForAnAtlasShapedLoad`): an atlas
|
||||
stitch - 64 sub-uploads into one level - goes from 64 reply waits to **0**, while a texture per
|
||||
sprite - 64 textures, 256 `glTexParameteri` - goes from 384 to 320, with the remainder all three of
|
||||
these rows (create 76, respecify 130, params 130).
|
||||
**`ResourceCreate`, `ResourceRespecify` and `SetTextureParams` are `kWaitReply` ROWS, and what the
|
||||
caller does with that is now per-object rather than per-row - THIS PARAGRAPH IS THE ONE P12
|
||||
SUPERSEDED, and it is rewritten rather than deleted because its reasoning is what the replacement
|
||||
had to answer.** Each one's answer carries a fact about the SERVER's record table that the client's
|
||||
own tracker cannot know by itself (`RespecifyOnce`, `TextureEmit.h`, "the applier's REFUSAL is the
|
||||
only signal that says 'I hold nothing for this handle'" - the scope
|
||||
`MGPipeApplierReleaseObjectRecords` leaves behind for a served context whose frontend objects live
|
||||
on). What P12 added is the observation that the client holds a REASONABLE COPY of that fact:
|
||||
`MGPipeHandleIsPublished`, set only when a create was ACCEPTED, is exactly "the applier holds this
|
||||
handle", and it is what `MGPipeResourceRespecifyWantsItsReply` (`PipeRoute.h`) and
|
||||
`Wire_SetTextureParams` read. So:
|
||||
|
||||
- **`SetTextureParams` and the texture half of `ResourceRespecify` wait only until the object is
|
||||
CONFIRMED.** For a published object the record is fire-and-forget and the caller reads a
|
||||
provisional accept; the object's one real confirmation is its create, paid at its FIRST USE
|
||||
(`EmitTextureParams` -> `HealUnpublishedRecord`, `EmitResourceRespecify`'s unpublished arm). When
|
||||
the latch is clear both rows still wait, and the refusal-driven `RespecifyOnce` heal is unchanged
|
||||
- which is what keeps the paragraph above true rather than merely overruled.
|
||||
- **The buffer half of `ResourceRespecify` never waits**: both call sites discard its Bool
|
||||
(`PipeFill.cpp:1105/:1142`).
|
||||
- **`ResourceCreate` is the one row whose answer may not be dropped**, because `PublishCreate`
|
||||
latches on it and a false latch makes every row above fire-and-forget against a record the server
|
||||
does not hold. It was given a WINDOW instead of a rule - `MOBILEGL_IPC_CREATE_WINDOW`, answers
|
||||
deferred and collected in order, a late DECLINE un-latching the object - and **the window is OFF
|
||||
BY DEFAULT because the deferral was measured and cannot be collected**: the reply pool is
|
||||
`kDefaultReplySlotCount` = 8 slots deep and one load frame posts 59,673 replies (the server
|
||||
answers every reply-owning record whether or not the client wants the answer), so an answer is
|
||||
overwritten after 8 replies - 0.61 of a create. On the device all 4,536 creates took the blocking
|
||||
path at window 4 and at window 1 alike (28.64 s both ways, against the handoff's 31.3 s
|
||||
pre-window baseline on the same device). `WireTables.cpp`'s section comment carries the counters
|
||||
and the arithmetic. The client-visible consequence is worth stating here: with the window ON, the
|
||||
un-latch that the paragraph above relies on lives in the same unreadable drain, so a refused
|
||||
create would stay latched - a protocol change (a deeper pool, or a "do not answer me" bit) is
|
||||
what a retry would need.
|
||||
|
||||
Measured split for the two shapes a stitcher produces (P12,
|
||||
`RemoteClientControls.ReplyWaitsByOpForAnAtlasShapedLoad`): an atlas stitch - 64 sub-uploads into
|
||||
one level - goes from 64 reply waits to **0**, while a texture per sprite - 64 textures, 256
|
||||
`glTexParameteri` - goes from 384 to 320 at the contract commit; the P12 first-use change takes the
|
||||
rest of the params/respecify rows out of that frame's wait list entirely (one frame: 43,232 waits ->
|
||||
4,543, 221.5 s -> 31.3 s, `MOBILEGL-CS-HANDOFF-2.md` §1).
|
||||
per call, analyzed separately rather than swept in here.
|
||||
|
||||
### 2.6 Event-ring flow control (ARCHITECTURE §11.7 made real)
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
// P5e (pg): ID-87 asks this package for bytes-per-link measured rather than argued, and the
|
||||
// archive's byte count exists exactly once - here, where it is serialised.
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
// P12: kDefaultReplySlotCount, because the create window's ceiling is derived from the pool it
|
||||
// shares rather than from a number written twice (see kCreateWindowMax below).
|
||||
#include <MG_Remote/Transport/ReplySlot.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
@@ -316,10 +319,184 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// "always accept" is ID-39's 66 lost DirectVulkan uploads and "accept if we emitted" is
|
||||
// the same bug wearing a counter.
|
||||
|
||||
void Wire_ResourceCreate(const MG_Pipe::MGPResourceDesc* payload, MG_Pipe::MGPReplySlot* reply) {
|
||||
if (RunsAsTheServerRole()) { MG_Pipe::MGPipeMonolithScreen().ResourceCreate(payload, reply); return; }
|
||||
ClientSession& session = RequireSession("ResourceCreate");
|
||||
Int32 status = 0;
|
||||
// ---- P12: the CREATE WINDOW, AND WHY IT IS OFF ----------------------------------------
|
||||
//
|
||||
// THE IDEA. The create row is the last per-record round trip on the load path (4,536 of one
|
||||
// frame's 4,543 reply waits) and the one row whose answer is not optional: PublishCreate
|
||||
// latches "the applier holds this handle" on it, and that latch is what every converted row
|
||||
// reads. So this row would not STOP reading its answer - it would DEFER it. Up to
|
||||
// MOBILEGL_IPC_CREATE_WINDOW creates go out fire-and-forget, the caller reads a provisional
|
||||
// accept (so the latch is taken), and the answers are collected IN ORDER: the oldest blocks
|
||||
// when the window is full, and one that comes back DECLINED un-latches its object
|
||||
// (MGPipeNoteHandleUnpublished), which sends every later row for that object back down the
|
||||
// blocking path.
|
||||
//
|
||||
// IT DOES NOT WORK, AND NOT FOR A REASON THAT TUNING REACHES. MEASURED on the device (the
|
||||
// same frame, the same binary, the same 101,421,396 B staged, axe aikjs4s8cys8ba7l):
|
||||
//
|
||||
// MOBILEGL_IPC_CREATE_WINDOW=4 by_op 2 = 4,536 wall 28.64 s
|
||||
// MOBILEGL_IPC_CREATE_WINDOW=1 by_op 2 = 4,536 wall 28.64 s
|
||||
// (the handoff's own pre-window baseline was 31.3 s on this device)
|
||||
//
|
||||
// by_op 2 counts the creates that took the BLOCKING path, and it did not move, so no create
|
||||
// deferred. A temporary counter in the row said why: pushed=24, blocked=4,476, took=20,
|
||||
// readfail=8,951, pending pinned at 4. The drain READ the window's answers back and failed
|
||||
// every time.
|
||||
//
|
||||
// WHY, IN THE PROTOCOL'S OWN ARITHMETIC. An answer is readable only until its reply-pool
|
||||
// slot is reused, and the pool is kDefaultReplySlotCount = 8 slots deep. The frame posts a
|
||||
// reply for EVERY reply-owning record - not only for the rows that wait, because the server
|
||||
// cannot know which those are (ClientSession.cpp:1897-1900: "The server posts either way")
|
||||
// - and that is 59,673 replies in this one frame:
|
||||
//
|
||||
// create 4,536 + respecify 20,633 + params 18,056 + sub-data 12,671 + 3,770 + 6 + 1
|
||||
//
|
||||
// 8 slots against 59,673 replies is a pool that churns 7,459 times per frame, i.e. 13.2
|
||||
// replies between two creates: an answer posted at create N is overwritten after EIGHT
|
||||
// replies, which is 0.61 of a create. There is no window depth for which the deferred
|
||||
// answer still exists when the client comes back for it - the deferral is not slow, it is
|
||||
// UNREADABLE. (A 1-deep "window" is the blocking path, which is what the row does.)
|
||||
//
|
||||
// AND THE FAILURE IS NOT NEUTRAL: with the window on, the provisional accept latches an
|
||||
// object the applier may have refused, and the un-latch that would correct it lives in the
|
||||
// drain - the same drain that cannot read. A refusal therefore becomes an object the client
|
||||
// believes is published, which is exactly the silent divergence the suspect list exists to
|
||||
// prevent. So the default is 1: the row is the pre-window shape unless someone sets
|
||||
// MOBILEGL_IPC_CREATE_WINDOW explicitly, and the machinery below is kept as the record of
|
||||
// what was tried and what it cost (a fix would have to make a deferred answer outlive
|
||||
// 59,673 intervening replies, which means a deeper pool or a way for the client to tell
|
||||
// the server "do not answer this one" - both are wire changes, not tuning).
|
||||
//
|
||||
// The gates that pin it are in MG_Test/Wire/RemoteClientControls.inc, and they still hold:
|
||||
// the window is a coherent mechanism, it is simply unusable on a frame with this much
|
||||
// other reply traffic.
|
||||
constexpr Uint32 kCreateWindowMax = 4;
|
||||
// THE WINDOW MAY NOT OWN MORE THAN HALF THE POOL, and this is where that stops being a
|
||||
// sentence in a comment. The pool is kDefaultReplySlotCount slots addressed seq % count, so
|
||||
// an answer is readable only while its slot has not been reused; every other reply-owning
|
||||
// row - fence, query, and the blocking rows themselves - needs a slot too. The change the
|
||||
// handoff names as the one worth pinning ("someone raises kCreateWindowMax to 8 and steps on
|
||||
// the pool") goes red HERE, by name, at compile time, instead of as a wrong picture on a
|
||||
// device. Raised to 8 it would take the whole pool and leave nothing for the answer the
|
||||
// blocking rows are waiting on.
|
||||
static_assert(kCreateWindowMax <= Transport::kDefaultReplySlotCount / 2,
|
||||
"the create window may hold at most half the reply pool: the other half "
|
||||
"belongs to the rows that take their answers immediately");
|
||||
// AND THE ENVIRONMENT MAY NOT RAISE IT EITHER. The configured value is clamped to the
|
||||
// array's own size, so the fixed array below cannot be overrun by a config bound and this
|
||||
// constant having drifted apart. 1 (and anything below it) is the pre-window shape.
|
||||
Uint32 CreateWindowEffective() {
|
||||
if (MG_Config::Ipc.CreateWindow <= 1) return 1;
|
||||
return MG_Config::Ipc.CreateWindow < kCreateWindowMax ? MG_Config::Ipc.CreateWindow
|
||||
: kCreateWindowMax;
|
||||
}
|
||||
struct PendingCreate {
|
||||
Uint64 Seq = 0;
|
||||
MG_Pipe::MGPipeKind Kind = MG_Pipe::MGPipeKind::None;
|
||||
MG_Pipe::MGPipeHandle Handle{};
|
||||
};
|
||||
PendingCreate g_createWindow[kCreateWindowMax];
|
||||
Uint32 g_createWindowHead = 0;
|
||||
Uint32 g_createWindowCount = 0;
|
||||
Uint64 g_createRefusals = 0;
|
||||
|
||||
// A HANDLE WHOSE CREATE CAME BACK DECLINED IS NEVER WINDOWED AGAIN. Without this the window
|
||||
// would UNDO the repair it exists to keep: every later use of the object sends another create,
|
||||
// reads another provisional accept, and latches the object as published again - so a server
|
||||
// that keeps refusing (the consumer-gate asymmetry the audit found, or any bug) would be fed
|
||||
// fire-and-forget records for ever with the client believing all of them landed. With it, a
|
||||
// refused object leaves the window and its next create is BLOCKING, so the refusal reaches the
|
||||
// caller, the latch stays clear, and every converted row for that object is back on the loud
|
||||
// blocking path. The window is for objects that have never been refused.
|
||||
//
|
||||
// BOUNDED, AND THE BOUND IS THE SAFE DIRECTION: past kCreateSuspectMax suspects the window
|
||||
// switches itself off entirely, which is the pre-window shape.
|
||||
constexpr SizeT kCreateSuspectMax = 256;
|
||||
Vector<MG_Pipe::MGPipeHandle> g_createSuspects;
|
||||
|
||||
Bool CreateIsSuspect(MG_Pipe::MGPipeHandle handle) {
|
||||
for (const MG_Pipe::MGPipeHandle& suspect : g_createSuspects) {
|
||||
if (suspect.Slot == handle.Slot && suspect.Gen == handle.Gen) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void NoteCreateSuspect(MG_Pipe::MGPipeHandle handle) {
|
||||
if (CreateIsSuspect(handle) || g_createSuspects.size() >= kCreateSuspectMax) return;
|
||||
g_createSuspects.push_back(handle);
|
||||
}
|
||||
void ClearCreateSuspect(MG_Pipe::MGPipeHandle handle) {
|
||||
for (SizeT i = 0; i < g_createSuspects.size(); ++i) {
|
||||
if (g_createSuspects[i].Slot != handle.Slot || g_createSuspects[i].Gen != handle.Gen)
|
||||
continue;
|
||||
for (SizeT j = i + 1; j < g_createSuspects.size(); ++j)
|
||||
g_createSuspects[j - 1] = g_createSuspects[j];
|
||||
g_createSuspects.pop_back();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Which publication latch an object's create owns. The three resource kinds are spelled out
|
||||
// rather than defaulted, so a target added later cannot silently borrow the texture latch.
|
||||
MG_Pipe::MGPipeKind CreateRecordKind(const MG_Pipe::MGPResourceDesc& desc) {
|
||||
switch (static_cast<MG_Pipe::MGPipeResourceTarget>(desc.Target)) {
|
||||
case MG_Pipe::MGPipeResourceTarget::Buffer: return MG_Pipe::MGPipeKind::Buffer;
|
||||
case MG_Pipe::MGPipeResourceTarget::Renderbuffer: return MG_Pipe::MGPipeKind::Renderbuffer;
|
||||
default: return MG_Pipe::MGPipeKind::Texture;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect every answer that is already available. `blockForOldest` is the window being full,
|
||||
// which is the one moment an answer is worth waiting for - appliedSeq is monotone, so taking
|
||||
// the oldest is what frees the slot the next create needs. FIFO throughout: an unapplied
|
||||
// oldest means none of the rest are ready either.
|
||||
void DrainCreateWindow(ClientSession& session, Bool blockForOldest) {
|
||||
Bool blocking = blockForOldest;
|
||||
while (g_createWindowCount != 0) {
|
||||
const PendingCreate head = g_createWindow[g_createWindowHead];
|
||||
const Transport::SessionWait wait =
|
||||
session.WaitForApplied(head.Seq, blocking ? 120000u : 0u);
|
||||
if (wait != Transport::SessionWait::Reached) return;
|
||||
Int32 status = Wire::ReplySink::kStatusError;
|
||||
Uint64 size = 0;
|
||||
// AN UNREADABLE SLOT IS NOT AN ERROR: ReadReply refuses when the stamp does not
|
||||
// match yet (or when the answer does not fit the scratch), and treating that as
|
||||
// ERROR would invent a transport fault out of a reader that arrived early. The
|
||||
// entry stays in the window and is taken at the next drain.
|
||||
if (!session.ReadReply(head.Seq, nullptr, 0, &status, &size)) return;
|
||||
// ERROR IS NOT A DECLINE (M4 / R-5), and a deferred answer does not change that:
|
||||
// the same named Fatal the blocking take raises.
|
||||
if (status == Wire::ReplySink::kStatusError) {
|
||||
SessionFail(MGFatalFamily::ReplyError, "MGPipe: Fatal{ReplyError, \"resource_create\"} - the row answered "
|
||||
"ERROR, which is not an acceptance answer; folding it into accepted or "
|
||||
"refused would make a transport fault look like a resource decision");
|
||||
}
|
||||
if (status == Wire::ReplySink::kStatusDeclined) {
|
||||
MG_Pipe::MGPipeNoteHandleUnpublished(head.Kind, head.Handle);
|
||||
NoteCreateSuspect(head.Handle);
|
||||
++g_createRefusals;
|
||||
} else {
|
||||
// It landed: the object is published for real, so it is no longer suspect.
|
||||
ClearCreateSuspect(head.Handle);
|
||||
}
|
||||
g_createWindowHead = (g_createWindowHead + 1) % kCreateWindowMax;
|
||||
--g_createWindowCount;
|
||||
blocking = false;
|
||||
}
|
||||
}
|
||||
// THE PRE-WINDOW SHAPE, AND THE HEAL BESIDE IT. Every path that cannot defer takes this
|
||||
// one: the window off, the object already refused, the suspect list full, and the window
|
||||
// still full after a blocking drain. The record waits and its caller reads the REAL
|
||||
// answer.
|
||||
//
|
||||
// AND AN ACCEPTED CREATE TAKES ITS OBJECT BACK OUT OF THE SUSPECT LIST. "Never windowed
|
||||
// again" is the answer to a REFUSAL, and a blocking create that landed means the refusal
|
||||
// is over - the object is published, by the caller's own latch, on the answer this call
|
||||
// just read. Leaving the entry standing would retire the object from the window for the
|
||||
// rest of the process, and kCreateSuspectMax such recoveries would retire the WINDOW
|
||||
// itself, which is the one way this row's speed-up can be lost with nothing going wrong.
|
||||
void EmitCreateBlocking(ClientSession& session, const MG_Pipe::MGPResourceDesc* payload,
|
||||
MG_Pipe::MGPReplySlot* reply) {
|
||||
Int32 status = Wire::ReplySink::kStatusError;
|
||||
const Uint64 seq = session.EmitAndWait(MGPWireOp::ResourceCreate, payload,
|
||||
sizeof(*payload), nullptr, 0, nullptr, 0,
|
||||
&status);
|
||||
@@ -328,6 +505,58 @@ namespace MobileGL::MG_Remote::Client {
|
||||
MG_Pipe::MGPipePostReply(*reply, status, status == 0 ? 1u : 0u);
|
||||
++g_emitted;
|
||||
if (status == 1) ++g_declined;
|
||||
if (status == 0) ClearCreateSuspect(payload->Resource);
|
||||
}
|
||||
void Wire_ResourceCreate(const MG_Pipe::MGPResourceDesc* payload, MG_Pipe::MGPReplySlot* reply) {
|
||||
if (RunsAsTheServerRole()) { MG_Pipe::MGPipeMonolithScreen().ResourceCreate(payload, reply); return; }
|
||||
ClientSession& session = RequireSession("ResourceCreate");
|
||||
const Uint32 window = CreateWindowEffective();
|
||||
// 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
|
||||
// this is the branch the whole row takes, because the deferral above it cannot be
|
||||
// collected. See the section comment for the measurement and the arithmetic.
|
||||
if (window <= 1 || g_createSuspects.size() >= kCreateSuspectMax ||
|
||||
CreateIsSuspect(payload->Resource)) {
|
||||
EmitCreateBlocking(session, payload, reply);
|
||||
return;
|
||||
}
|
||||
// Take what is already answerable, and if the window is full take the oldest for real.
|
||||
// The poll is not politeness: it is what keeps a burst shorter than the window from
|
||||
// leaving its answers unread until the next burst.
|
||||
DrainCreateWindow(session, /*blockForOldest=*/false);
|
||||
if (g_createWindowCount >= window) {
|
||||
DrainCreateWindow(session, /*blockForOldest=*/true);
|
||||
// IF IT IS STILL FULL the oldest answer is not readable yet, and the window is a
|
||||
// fixed array: pushing would silently OVERWRITE a confirmation. This record takes
|
||||
// the blocking path instead - slower for this one call, never lossy. Reaching it
|
||||
// at all means the oldest answer did not arrive inside the drain's own budget, so
|
||||
// the session is on its way out; it is a net, not a hot path. The comparison is
|
||||
// against the effective window, which is what the array below is sized for, so
|
||||
// this line IS the overwrite guard.
|
||||
if (g_createWindowCount >= window) {
|
||||
EmitCreateBlocking(session, payload, reply);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Int32 status = Wire::ReplySink::kStatusError;
|
||||
const Uint64 seq =
|
||||
session.EmitAndWaitTails(MGPWireOp::ResourceCreate, payload, sizeof(*payload), nullptr,
|
||||
0, nullptr, 0, &status, nullptr, /*wantReply=*/false);
|
||||
if (seq == Wire::kInvalidSeq) {
|
||||
// Cancelled: there is no ordinal to read an answer from, and the caller must NOT
|
||||
// latch an object the record never described.
|
||||
MG_Pipe::MGPipePostReply(*reply, Wire::ReplySink::kStatusDeclined, 0u);
|
||||
++g_emitted;
|
||||
++g_declined;
|
||||
return;
|
||||
}
|
||||
g_createWindow[(g_createWindowHead + g_createWindowCount) % kCreateWindowMax] =
|
||||
PendingCreate{seq, CreateRecordKind(*payload), payload->Resource};
|
||||
++g_createWindowCount;
|
||||
reply->Id = seq;
|
||||
MG_Pipe::MGPipePostReply(*reply, Wire::ReplySink::kStatusOk, 1u);
|
||||
++g_emitted;
|
||||
}
|
||||
|
||||
// A PARAMETER CHANGE FOR AN OBJECT WHOSE EXISTENCE IS CONFIRMED OWES NO ANSWER (P12).
|
||||
@@ -922,6 +1151,23 @@ namespace MobileGL::MG_Remote::Client {
|
||||
Uint64 ClientWireRecordsEmitted() { return g_emitted; }
|
||||
Uint64 ClientWireRecordsDeclined() { return g_declined; }
|
||||
|
||||
// ---- P12: the CREATE WINDOW, as numbers a test can read ---------------------------------
|
||||
//
|
||||
// The window is a DEFERRAL, so the property that has to be checkable is not "is it faster"
|
||||
// but "was every deferred answer eventually taken, and did the window ever hold more than it
|
||||
// is sized for". Pending is that second question, and it is the one a silent overwrite would
|
||||
// answer wrongly: the ring wraps, so an unguarded push would not grow the count past the
|
||||
// ceiling - it would REPLACE the oldest confirmation and leave the count where it was. Read it
|
||||
// against the effective ceiling and the array size, which are the two numbers the guard itself
|
||||
// is written in terms of, and not against a literal repeated in the test.
|
||||
Uint32 ClientCreateWindowPending() { return g_createWindowCount; }
|
||||
Uint32 ClientCreateWindowEffective() { return CreateWindowEffective(); }
|
||||
Uint32 ClientCreateWindowArraySize() { return kCreateWindowMax; }
|
||||
// Objects whose create came back DECLINED: out of the window, next create blocking, and back
|
||||
// in the moment a blocking create lands.
|
||||
Uint32 ClientCreateWindowSuspects() { return static_cast<Uint32>(g_createSuspects.size()); }
|
||||
Uint64 ClientCreateWindowRefusals() { return g_createRefusals; }
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Client
|
||||
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
@@ -78,6 +78,20 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// inferred, R-8's rule one level out.
|
||||
Uint64 ClientWireRecordsDeclined();
|
||||
|
||||
// ---- P12: the CREATE WINDOW (MOBILEGL_IPC_CREATE_WINDOW) ---------------------------------
|
||||
//
|
||||
// The resource_create row defers its answer instead of dropping it: up to N creates go out
|
||||
// fire-and-forget, the caller reads a provisional accept, and the answers are taken in order.
|
||||
// The four numbers below are what a test can say about that without reaching into the row:
|
||||
// how many answers are outstanding, the ceiling the code actually applies (the config value
|
||||
// clamped to the array), the array's own size, and the two tallies - objects whose create came
|
||||
// back DECLINED and are therefore out of the window, and how many refusals there have been.
|
||||
Uint32 ClientCreateWindowPending();
|
||||
Uint32 ClientCreateWindowEffective();
|
||||
Uint32 ClientCreateWindowArraySize();
|
||||
Uint32 ClientCreateWindowSuspects();
|
||||
Uint64 ClientCreateWindowRefusals();
|
||||
|
||||
// TRUE ON THE SERVER ROLE's OWN THREAD (the apply thread), false everywhere else. It is
|
||||
// v1's ServerLoop::OnApplyThread(), exposed here because it is table 3's role split made
|
||||
// into one predicate and TWO packages read it: the wire emitters below (a routed call that
|
||||
|
||||
@@ -904,13 +904,14 @@ TEST(RemoteClientControls, TextureUploadsStopOwningAReplyOnTheWireArm) {
|
||||
// control visible instead of silently wrong.
|
||||
const Uint32 uploadOp = static_cast<Uint32>(MGPWireOp::ResourceSubData);
|
||||
const Uint32 createOp = static_cast<Uint32>(MGPWireOp::ResourceCreate);
|
||||
const Uint64 createWaitsBefore = Transport::LinkMetricsReplyWaitsFor(createOp);
|
||||
const Uint64 createRecordsBefore = Transport::LinkMetricsRecordsFor(createOp);
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(0x0DE1, texture);
|
||||
MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 4, 4, 0, 0x1908, 0x1401, pixels);
|
||||
MGP_FILL(DrawArrays);
|
||||
if (Transport::LinkMetricsReplyWaitsFor(createOp) <= createWaitsBefore) ::_exit(140);
|
||||
// THE CREATE WENT OUT (the record), and the live control is the fence below.
|
||||
if (Transport::LinkMetricsRecordsFor(createOp) <= createRecordsBefore) ::_exit(140);
|
||||
// THE ROW UNDER TEST. A second write into the same level is a pure resource_subdata: the
|
||||
// resource is created, so no create crosses, and the level is dirty again - the only
|
||||
// record it can produce is the one item B is about.
|
||||
@@ -1044,7 +1045,7 @@ TEST(RemoteClientControls, ReplyWaitsByOpForAnAtlasShapedLoad) {
|
||||
// emitter stopped sending, which is a different (and much worse) change than item B.
|
||||
if (Transport::LinkMetricsRecordsFor(static_cast<Uint32>(MGPWireOp::ResourceSubData)) < 64)
|
||||
::_exit(148);
|
||||
if (Transport::LinkMetricsReplyWaitsFor(static_cast<Uint32>(MGPWireOp::ResourceCreate)) == 0)
|
||||
if (Transport::LinkMetricsRecordsFor(static_cast<Uint32>(MGPWireOp::ResourceCreate)) == 0)
|
||||
::_exit(144);
|
||||
// AND ResourceRespecify FOLLOWS SetTextureParams OFF THAT LIST FOR THE SAME REASON (P12/R3):
|
||||
// for a confirmed object it is fire-and-forget too. What must still hold is that its RECORDS
|
||||
@@ -1098,7 +1099,19 @@ TEST(RemoteClientControls, AParameterChangeForAConfirmedObjectTakesNoReply) {
|
||||
MG_Impl::GLImpl::TexParameteri(0x0DE1, 0x2801, 0x2601); // MIN_FILTER = LINEAR
|
||||
MGP_FILL(DrawArrays);
|
||||
if (Transport::LinkMetricsRecordsFor(createOp) == 0) ::_exit(150);
|
||||
if (Transport::LinkMetricsReplyWaitsFor(createOp) == 0) ::_exit(151);
|
||||
// THE LIVE CONTROL IS map_persistent, NOT A CREATE AND NOT A FENCE. Not a create, because
|
||||
// the create row now batches its answers behind MOBILEGL_IPC_CREATE_WINDOW. Not a FENCE,
|
||||
// because a control session has no backend object: FenceCreate is refused, FindFence then
|
||||
// dies of Fatal{ProtocolCorruption, "Fence.handle"}, and the child ABORTS - a case cannot
|
||||
// read an exit code off a corpse. map_persistent needs no backend at all: under split its
|
||||
// answer is a CONSTANT DECLINE that still costs a record and still owns a reply slot
|
||||
// (PipeRoute.h, R-6), so it is exactly "a row that always takes its answer".
|
||||
{
|
||||
const Uint64 waitsBeforeControl = Transport::LinkMetricsReplyWaits();
|
||||
MG_Pipe::MGPHandleOnly controlHandle{};
|
||||
(void)MG_Pipe::MGPipeRouteMapPersistent(controlHandle, 64, nullptr);
|
||||
if (Transport::LinkMetricsReplyWaits() <= waitsBeforeControl) ::_exit(151);
|
||||
}
|
||||
// NOW THE OBJECT IS CONFIRMED. Eight real parameter changes: every one must still be SENT,
|
||||
// and none of them may take a reply.
|
||||
const Uint64 recordsBefore = Transport::LinkMetricsRecordsFor(paramsOp);
|
||||
@@ -1182,4 +1195,215 @@ TEST(RemoteClientControls, ABufferRespecifyTakesNoReplyWhileATextureOneStillDoes
|
||||
});
|
||||
ExpectChildSuccess(child);
|
||||
}
|
||||
// =====================================================================================
|
||||
// P12: the CREATE WINDOW - the confirmation is deferred, not dropped
|
||||
// =====================================================================================
|
||||
//
|
||||
// The create row is the last per-record round trip on the load path (4,536 of a frame's 4,543
|
||||
// waits) and the one row whose answer IS an object's confirmation. It therefore defers: up to
|
||||
// MOBILEGL_IPC_CREATE_WINDOW creates go out, the caller reads a provisional accept, and the
|
||||
// answers are collected in order. A late DECLINED un-latches its object, which puts every later row
|
||||
// for that object back on the blocking path - the repair that already existed, not a new one.
|
||||
//
|
||||
// THE THREE WAYS THIS CAN GO WRONG, one reading each:
|
||||
//
|
||||
// 1. THE WINDOW DEFERS NOTHING (the row pays a wait per record and the change is decoration) -
|
||||
// the ON half below;
|
||||
// 2. THE WINDOW IS NOT WHAT DECIDES (the row defers while the operator asked it not to, which is
|
||||
// the same defect as 1 with the sign flipped) - THE NEGATIVE CONTROL, run in the SAME CHILD as
|
||||
// the ON half so that both readings share a session, a server, a set of objects and a pair of
|
||||
// counters. MOBILEGL_IPC_CREATE_WINDOW=1 must restore the pre-window shape exactly; this is
|
||||
// the assertion that goes red if the window ever stops being the knob;
|
||||
// 3. THE WINDOW HOLDS MORE THAN IT IS SIZED FOR - and the ring is what makes this one quiet: an
|
||||
// unguarded push does not grow a count past its ceiling, it REPLACES the oldest confirmation
|
||||
// and leaves the count exactly where it was. So the invariant is read against the effective
|
||||
// ceiling after every create, the ceiling is read against the array, the array is read against
|
||||
// the reply pool it shares, and the environment is not allowed to raise any of them.
|
||||
TEST(RemoteClientControls, ACreateWindowDefersTheAnswersAndTheWindowOffDoesNot) {
|
||||
const auto child = RunInChild([] {
|
||||
::setenv("MOBILEGL_PIPE_STATS", "1", 1);
|
||||
CapsPeer backend;
|
||||
StartControlSession(&backend);
|
||||
MG_Config::Features.PipePush |= kMGPipeSubsystemResources |
|
||||
kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers;
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
const Uint32 createOp = static_cast<Uint32>(MGPWireOp::ResourceCreate);
|
||||
Uint8 pixels[4 * 4 * 4];
|
||||
std::memset(pixels, 0x21, sizeof(pixels));
|
||||
// ---- the clamp and the ceiling, before a single record goes out ----
|
||||
// THE WINDOW IS OPT-IN AND THE DEFAULT IS OFF (Config.h: the deferral was measured on the
|
||||
// device and the drain cannot collect it), so both arms of this case SET the knob rather
|
||||
// than inheriting it - the ON half below is the mechanism being exercised, not a claim
|
||||
// about what ships.
|
||||
// WHAT THE OPERATOR WROTE IS NOT WHAT THE CODE MAY USE. With the clamp gone a configured
|
||||
// window of 99 is compared against a count that the ring keeps below its own size, so the
|
||||
// fallback never fires and the push index runs off the array.
|
||||
MG_Config::Ipc.CreateWindow = 99;
|
||||
if (ClientCreateWindowEffective() != ClientCreateWindowArraySize()) ::_exit(170);
|
||||
MG_Config::Ipc.CreateWindow = 4;
|
||||
if (ClientCreateWindowEffective() != 4u) ::_exit(171);
|
||||
// AND THE ARRAY IS SIZED FROM THE POOL IT SHARES, not from a second literal: the pool is
|
||||
// Transport::kDefaultReplySlotCount slots addressed seq % count, and every reply-owning row
|
||||
// (fence, query, the blocking rows themselves) needs one. Raising the window to the whole
|
||||
// pool is the change the handoff names, and it is red here as well as at the static_assert
|
||||
// in WireTables.cpp - deliberately twice, because the compile-time one is the guard and
|
||||
// this one is the statement of what the guard is for.
|
||||
if (ClientCreateWindowArraySize() > Transport::kDefaultReplySlotCount / 2) ::_exit(172);
|
||||
|
||||
// TWELVE OBJECTS, ONE CREATE EACH, AND THE INVARIANT READ AFTER EVERY ONE. This is the
|
||||
// reading a silent overwrite cannot fake: the count stays at the ceiling while an entry is
|
||||
// replaced underneath it.
|
||||
Uint32 highWater = 0;
|
||||
const auto burst = [&](Int32 count) {
|
||||
for (Int32 i = 0; i < count; ++i) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(0x0DE1, texture);
|
||||
MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 4, 4, 0, 0x1908, 0x1401, pixels);
|
||||
MGP_FILL(DrawArrays);
|
||||
if (ClientCreateWindowPending() > ClientCreateWindowEffective()) ::_exit(173);
|
||||
if (ClientCreateWindowPending() > highWater) highWater = ClientCreateWindowPending();
|
||||
}
|
||||
};
|
||||
|
||||
// ---- THE WINDOW ON: twelve creates, strictly fewer waits ----
|
||||
const Uint64 recordsBefore = Transport::LinkMetricsRecordsFor(createOp);
|
||||
const Uint64 waitsBefore = Transport::LinkMetricsReplyWaitsFor(createOp);
|
||||
burst(12);
|
||||
const Uint64 records = Transport::LinkMetricsRecordsFor(createOp) - recordsBefore;
|
||||
const Uint64 waits = Transport::LinkMetricsReplyWaitsFor(createOp) - waitsBefore;
|
||||
std::printf("create window ON: window=%u records=%llu waits=%llu high_water=%u\n",
|
||||
static_cast<unsigned>(MG_Config::Ipc.CreateWindow),
|
||||
static_cast<unsigned long long>(records),
|
||||
static_cast<unsigned long long>(waits),
|
||||
static_cast<unsigned>(highWater));
|
||||
std::fflush(nullptr);
|
||||
if (records < 12) ::_exit(164);
|
||||
// THE DEFERRAL ITSELF, and the honest size of the claim: the first `window` records pay
|
||||
// nothing, so twelve records cannot pay twelve waits once anything was deferred. A ratio
|
||||
// would be a promise about how fast the server answers, which is not this suite's to make.
|
||||
if (waits >= records) ::_exit(165);
|
||||
|
||||
// ---- AND THE SAME TWELVE WITH THE WINDOW OFF, IN THIS SAME CHILD ----
|
||||
// The negative control shares everything with the ON half except the knob, which is what
|
||||
// makes it a control rather than a second measurement. ONE WAIT PER RECORD is the
|
||||
// pre-window shape: R-5 shipped with exactly that, and the whole change is the difference
|
||||
// between the two lines this case prints.
|
||||
MG_Config::Ipc.CreateWindow = 1;
|
||||
const Uint64 offRecordsBefore = Transport::LinkMetricsRecordsFor(createOp);
|
||||
const Uint64 offWaitsBefore = Transport::LinkMetricsReplyWaitsFor(createOp);
|
||||
burst(12);
|
||||
const Uint64 offRecords = Transport::LinkMetricsRecordsFor(createOp) - offRecordsBefore;
|
||||
const Uint64 offWaits = Transport::LinkMetricsReplyWaitsFor(createOp) - offWaitsBefore;
|
||||
std::printf("create window OFF: window=%u records=%llu waits=%llu\n",
|
||||
static_cast<unsigned>(MG_Config::Ipc.CreateWindow),
|
||||
static_cast<unsigned long long>(offRecords),
|
||||
static_cast<unsigned long long>(offWaits));
|
||||
std::fflush(nullptr);
|
||||
if (offRecords < 12) ::_exit(166);
|
||||
// ONE WAIT PER CREATE, EXACTLY. Every create on this path is an EmitAndWait, so the two
|
||||
// counters are equal whatever else the row does - which is the assertion that cannot be
|
||||
// satisfied by a window that is still deferring sometimes.
|
||||
if (offWaits != offRecords) ::_exit(167);
|
||||
ClientSessionInstance().Stop();
|
||||
});
|
||||
ExpectChildSuccess(child);
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// P12: A REFUSED CREATE LEAVES THE WINDOW - read ON THE LATCH AND ON THE SUSPECT LIST
|
||||
// =====================================================================================
|
||||
//
|
||||
// The window's answer is provisional, so the one thing it must never do is keep feeding a refused
|
||||
// object fire-and-forget creates: the client would hold a confirmation the server has refused, for
|
||||
// ever, and every row that reads the latch would believe it. The repair is deliberately the path
|
||||
// that already existed - DECLINED un-latches the object, the object leaves the window, its next
|
||||
// create BLOCKS and the refusal reaches the caller - and this case reads all three steps rather
|
||||
// than the symptom. It then reads the HEAL, which is the other half of the same rule: a blocking
|
||||
// create that lands puts the object back, so a transient refusal cannot retire an object from the
|
||||
// window for the rest of the process (or, at kCreateSuspectMax of them, retire the window).
|
||||
//
|
||||
// THE REFUSAL IS MADE REAL RATHER THAN SIMULATED, and it is the exact case the audit found the two
|
||||
// ends can disagree about: the applier asks for bit 10 while the client's mirror still says the
|
||||
// family is live, so every create the client emits is refused. Narrowing the server's consumed mask
|
||||
// reproduces that without touching the client's mirror.
|
||||
TEST(RemoteClientControls, ARefusedCreateUnlatchesItsObjectAndItsNextCreateBlocks) {
|
||||
const auto child = RunInChild([] {
|
||||
::setenv("MOBILEGL_PIPE_STATS", "1", 1);
|
||||
CapsPeer backend;
|
||||
StartControlSession(&backend);
|
||||
MG_Config::Features.PipePush |= kMGPipeSubsystemResources |
|
||||
kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers;
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
// THE WINDOW IS SET HERE, not inherited: this case is about what the window does with a
|
||||
// refusal, so it must have one whatever the suite was launched with.
|
||||
MG_Config::Ipc.CreateWindow = 4;
|
||||
const Uint32 createOp = static_cast<Uint32>(MGPWireOp::ResourceCreate);
|
||||
// ROOM FOR THE LARGEST LEVEL THIS CASE DEFINES. The storage definition's texels travel as
|
||||
// resource_subdata out of the drain list, so the pointer is read after this call returns.
|
||||
Uint8 pixels[64 * 64 * 4];
|
||||
std::memset(pixels, 0x21, sizeof(pixels));
|
||||
Srv::ServerSessionInstance().SetConsumedSubsystems(
|
||||
kMGPipeSubsystemsMigratedAtP4a & ~kMGPipeSubsystemTextureResources);
|
||||
// ---- one object we can name, then enough neighbours to fill and drain the window ----
|
||||
// The drain is what READS a deferred answer, so the refusal of the first create is only
|
||||
// discovered once later creates have pushed the ring past its ceiling.
|
||||
const auto defineStorage = [&](GLuint name, Int32 extent) {
|
||||
MG_Impl::GLImpl::BindTexture(0x0DE1, name);
|
||||
MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, extent, extent, 0, 0x1908, 0x1401, pixels);
|
||||
MGP_FILL(DrawArrays);
|
||||
};
|
||||
GLuint refused = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &refused);
|
||||
defineStorage(refused, 8);
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
GLuint filler = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &filler);
|
||||
defineStorage(filler, 8);
|
||||
}
|
||||
// ---- (a) THE LATCH IS CLEAR AND THE OBJECT IS OUT OF THE WINDOW ----
|
||||
const auto refusedObject = MG_State::pGLContext->GetTextureObject(refused);
|
||||
if (refusedObject == nullptr) ::_exit(174);
|
||||
const MG_Pipe::MGPipeHandle refusedHandle =
|
||||
MG_Pipe::MGPipeTextureEmitterInstance().FindTexture(*refusedObject);
|
||||
if (MG_Pipe::MGPipeHandleIsNull(refusedHandle)) ::_exit(175);
|
||||
// THE DIRECT READING, which is what the converted rows read: not "the create was refused"
|
||||
// but "this object is not published", which is the fact every fire-and-forget row acts on.
|
||||
if (MG_Pipe::MGPipeHandleIsPublished(MG_Pipe::MGPipeKind::Texture, refusedHandle)) ::_exit(176);
|
||||
if (ClientCreateWindowRefusals() == 0) ::_exit(177);
|
||||
const Uint32 suspectsBefore = ClientCreateWindowSuspects();
|
||||
if (suspectsBefore == 0) ::_exit(178);
|
||||
// ---- (b) AND ITS NEXT CREATE BLOCKS: ONE WAIT PER CREATE, WITH THE WINDOW WIDE OPEN ----
|
||||
// The window has room (the ceiling is 4 and this case has just drained it), so a deferring
|
||||
// row would pay ZERO waits here. Every create this restorage emits pays one instead, which
|
||||
// is the blocking path and nothing else.
|
||||
const Uint64 createRecordsBefore = Transport::LinkMetricsRecordsFor(createOp);
|
||||
const Uint64 createWaitsBefore = Transport::LinkMetricsReplyWaitsFor(createOp);
|
||||
defineStorage(refused, 16);
|
||||
const Uint64 suspectRecords = Transport::LinkMetricsRecordsFor(createOp) - createRecordsBefore;
|
||||
const Uint64 suspectWaits = Transport::LinkMetricsReplyWaitsFor(createOp) - createWaitsBefore;
|
||||
std::printf("refused create: suspect_records=%llu suspect_waits=%llu suspects=%u\n",
|
||||
static_cast<unsigned long long>(suspectRecords),
|
||||
static_cast<unsigned long long>(suspectWaits),
|
||||
static_cast<unsigned>(suspectsBefore));
|
||||
std::fflush(nullptr);
|
||||
if (suspectRecords == 0) ::_exit(179);
|
||||
if (suspectWaits < suspectRecords) ::_exit(180);
|
||||
// ---- (c) THE HEAL: GIVE THE FAMILY ITS CONSUMER BACK AND RE-DEFINE THE SAME OBJECT ----
|
||||
// The create still blocks (the object is still suspect), now it LANDS, and landing is what
|
||||
// takes it out of the suspect list - exactly one entry, because no other object is touched.
|
||||
Srv::ServerSessionInstance().SetConsumedSubsystems(kMGPipeSubsystemsMigratedAtP4a);
|
||||
defineStorage(refused, 32);
|
||||
std::printf("refused create healed: published=%d suspects %u -> %u\n",
|
||||
static_cast<int>(MG_Pipe::MGPipeHandleIsPublished(MG_Pipe::MGPipeKind::Texture,
|
||||
refusedHandle)),
|
||||
static_cast<unsigned>(suspectsBefore),
|
||||
static_cast<unsigned>(ClientCreateWindowSuspects()));
|
||||
std::fflush(nullptr);
|
||||
if (!MG_Pipe::MGPipeHandleIsPublished(MG_Pipe::MGPipeKind::Texture, refusedHandle)) ::_exit(181);
|
||||
if (ClientCreateWindowSuspects() != suspectsBefore - 1) ::_exit(182);
|
||||
ClientSessionInstance().Stop();
|
||||
});
|
||||
ExpectChildSuccess(child);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Backend/DirectGLES/Managers.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
// P12: MGPipeTextureEmitterInstance(), so a control case can name the handle of an object it
|
||||
// created and read the publication latch the converted rows read.
|
||||
#include <MG_Impl/Pipe/TextureEmit.h>
|
||||
#include <Init.h>
|
||||
#include <MG_Impl/EGLImpl/EGLImpl.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# create window 的实测否决,与 StreamLink 的一处 ILink 契约违反(handoff-2 §2 收尾记录)
|
||||
|
||||
> 2026-09-26。分支 `feat/disaggregated` @ `82ed1df9` 加本轮工作树改动。对应
|
||||
> [`MOBILEGL-CS-HANDOFF-2.md`](../../../../MOBILEGL-CS-HANDOFF-2.md) 的 §2(工作树里那版 create window)
|
||||
> 与 §7 的「把 create 那 4,536 批成 ~1,134」。
|
||||
|
||||
## 0. 一句话
|
||||
|
||||
- **窗口开了等于没开**:同一台设备、同一帧、同一 101,421,396 B staged,`MOBILEGL_IPC_CREATE_WINDOW=4`
|
||||
与 `=1` 的 create 行都是 **4,536 次等待**,墙钟都是 **28.64 s**(handoff 的 pre-window 基线是 31.3 s)。
|
||||
临时计数器给出的形态是:`pushed=24 blocked=4,476 took=20 readfail=8,951 pending=4`——**drain 读不回来**。
|
||||
- **不是调参问题**:回复池 8 槽,而这一帧发出 **59,673 条带回复的记录**(服务端对每条 kReplySlot 记录都回,
|
||||
不管客户端要不要),所以一条答案在 **8 条回复**之后就被覆盖,等于 **0.61 个 create**。任何深度的窗口都读不回来。
|
||||
- **顺带修掉一个真 bug**:`StreamLink::ReadReply` 只认「最新一条」(单槽),违背 `ILink::ReadReply(seq)`
|
||||
的按 seq 读取语义。已改成一个 8 槽环 + 用例。
|
||||
- **默认关掉**:`MOBILEGL_IPC_CREATE_WINDOW` 默认 1。窗口开着不只是白费,它会让**被拒绝的 create 留在「已发布」状态**
|
||||
(反闩动作在同一个读不回来的 drain 里),正是 suspect 表要防的那种静默分歧。
|
||||
|
||||
## 1. 实测(同一设备 aikjs4s8cys8ba7l,同一世界,出厂省电)
|
||||
|
||||
| run | 窗口 | `frame-sent 2=` | `frame-op by_op 2=` | 墙钟 | 日志 |
|
||||
|---|---|---|---|---|---|
|
||||
| handoff 基线(无窗口的二进制) | — | 4,536 | 4,536 | 31.26 s | `logs/mcx.client` |
|
||||
| 窗口 4 | 4 | 4,536 | **4,536** | **28.64 s** | `logs/mcx-fix4.client` |
|
||||
| 窗口 1(负控) | 1 | 4,536 | **4,536** | **28.64 s** | `logs/mcx-fix1.client` |
|
||||
|
||||
`by_op 2` 数的是**走了阻塞路径的 create**(`LinkMetricsBeginReply(ownsReplySlot, op)`,
|
||||
`ownsReplySlot = rowCarriesReplySlot && wantReply`,`ClientSession.cpp:1840/1883`)。它没动,就是「一条都没延后」。
|
||||
两臂的 `frame-sent` 全表逐 op 相同(4,536 / 20,633 / 3,770 / 18,056 / 12,671 …),也就是**记录一条没少发**。
|
||||
|
||||
窗口确实进去过——临时行(每 500 条打一次)在窗口 4 下的读数:
|
||||
|
||||
~~~
|
||||
P12diag create=501 cfg=4 eff=4 suspects=0 refusals=0 pending=4 pushed=24 blocked=476 took=20 waitfail=22 readfail=951
|
||||
P12diag create=4501 cfg=4 eff=4 suspects=0 refusals=0 pending=4 pushed=24 blocked=4476 took=20 waitfail=22 readfail=8951
|
||||
~~~
|
||||
|
||||
`pending` 从第 500 条起**一直钉在 4**,`refusals=0`(一条答案都没读到过),`readfail` 约每条 create 涨 2
|
||||
(非阻塞 poll + 阻塞 drain 各失败一次)。
|
||||
|
||||
## 2. 根因:回复池 8 槽 vs 一帧 59,673 条回复
|
||||
|
||||
答案只在**它的槽没被复用**之前可读(`ReplySlotPool`,`kDefaultReplySlotCount = 8`,
|
||||
`Transport/ReplySlot.h:111`)。而这一帧发回复的记录数是:
|
||||
|
||||
| 行 | 条数 | 为什么回 |
|
||||
|---|---|---|
|
||||
| 2 `ResourceCreate` | 4,536 | 等 |
|
||||
| 3 `ResourceRespecify` | 20,633 | **不等也给**(首用确认后 fire-and-forget) |
|
||||
| 47 `SetTextureParams` | 18,056 | **不等也给** |
|
||||
| 48 `ResourceSubData` + 4 | 16,441 | **不等也给** |
|
||||
| 5 / 9 等 | 7 | 等 |
|
||||
| **合计** | **59,673** | |
|
||||
|
||||
服务端**无法知道**客户端要不要这条答案——`ClientSession.cpp:1897-1900` 原文:「The server posts either way,
|
||||
so the wire is unchanged and the row keeps its flag」。于是池子一帧churn **7,459 次**,两条 create 之间平均夹
|
||||
**13.2 条回复**,而一条答案只活得下 **8 条**:
|
||||
|
||||
8 / 13.2 = 0.61 个 create
|
||||
|
||||
**所以窗口的失败不是「慢」,是「读不到」。** 1 深度的窗口就是阻塞路径本身。
|
||||
|
||||
## 3. 顺带发现并修掉:`StreamLink::ReadReply` 违背了自己的接口
|
||||
|
||||
`ILink::ReadReply`(`Transport/ILink.h:285`)的签名是**按 seq 读**;`ShmLink` 用 `ReplySlotPool` 实现它,
|
||||
而 `StreamLink` 到本轮为止只保留**一条**回复(`reply` / `replySeq` / `replyStatus`),并且:
|
||||
|
||||
~~~cpp
|
||||
x.replyReady.wait(lock, [&]{ return closed || x.replySeq >= seq || appliedSeq >= seq; });
|
||||
if (x.replySeq != seq) return ... MOBILEGL_ERR_PROTOCOL_MISMATCH; // 等的是 >=,判的是 ==
|
||||
~~~
|
||||
|
||||
「等到至少 seq」与「只接受恰好 seq」自相矛盾:任何早于最新一条的答案都读不回来。此前没有调用者受影响,
|
||||
是因为每个调用者读的都是自己刚等到的、也就是最新的一条;create window 是第一个读**旧**答案的调用者。
|
||||
已改成 8 槽环(`kReplyRing`,深度取它顶替的那个池:`kDefaultReplySlotCount`),
|
||||
用例 `StreamLinkTest.AnOlderReplyIsStillReadableByItsSeq` 三连发、按 1→2→3 顺序读回。
|
||||
|
||||
**注意这只治好了「单槽」,没治好「池只有 8 槽」**——`ShmLink` 侧本来就是池,也一样读不回来。这也是为什么
|
||||
设备上的读数在修掉单槽之后仍然一模一样。
|
||||
|
||||
## 4. 为什么这是个必须默认关掉的开关,而不是一个慢一点的开关
|
||||
|
||||
窗口的答案是**临时接受**:`PublishCreate` 会因此落闩(「applier holds this handle」),
|
||||
而纠正它的唯一动作——迟到 DECLINE → `MGPipeNoteHandleUnpublished` + suspect 表——**就在那个读不回来的 drain 里**。
|
||||
换句话说窗口开着时,**服务端的拒绝会被客户端当成已发布**,之后每条已转换的行都对一个不存在的记录 fire-and-forget。
|
||||
这正是 suspect 表存在的理由,也是 `TextureEmit.h` 里 `RespecifyOnce` 那句话(「applier 的 REFUSAL 是唯一说
|
||||
『我手里没有这个 handle』的信号」)要防的东西。
|
||||
|
||||
## 5. 门(`MG_Test/Wire/RemoteClientControls.inc`)
|
||||
|
||||
窗口机制本身是自洽的,只是在这条协议上不可用,所以门保留并 pin 住机制:
|
||||
|
||||
| 用例 | 钉住什么 | 读数 |
|
||||
|---|---|---|
|
||||
| `ACreateWindowDefersTheAnswersAndTheWindowOffDoesNot` | 窗口开=12 记录 0 等待;**同一子进程**把窗口关回 1 = 12 记录 **12** 等待(负控);`pending <= effective` 逐条断言;99 被夹到数组大小;数组 <= 池的一半 | ON 0 waits / OFF 12 waits |
|
||||
| `ARefusedCreateUnlatchesItsObjectAndItsNextCreateBlocks` | 真拒绝(收窄服务端 consumed mask)→ **闩清零**、suspect 计数 +1、下一条 create **阻塞**(2 记录 2 等待)、恢复消费者后 **heal**(闩回来、suspect −1) | suspects 6 → 5 |
|
||||
| `StreamLinkTest.AnOlderReplyIsStillReadableByItsSeq` | 传输层按 seq 可读 | 3/3 |
|
||||
|
||||
编译期还有一条:`WireTables.cpp` 的 `static_assert(kCreateWindowMax <= kDefaultReplySlotCount / 2, …)`,
|
||||
把「有人把窗口改成 8 就会踩池子」变成编译错误而不是设备上的错画面。
|
||||
|
||||
## 6. 画面(handoff-2 §6 要的那一格)
|
||||
|
||||
设备端(`MOBILEGL_IPC_SURFACE=server`,画面在手机屏上)在负载帧闭合后 +8 s 截屏:
|
||||
`logs/mcx-fix4.shot2.png` 与 `logs/mcx-fix1.shot2.png` 都是真实世界(地形、物品栏、聊天行),
|
||||
**两臂 SSIM = 0.977**(均值绝对差 2.26/255)。同一 run 内 加载页 vs 世界 SSIM 只有 0.12,说明这个数字不是
|
||||
「两张都黑」的假阳性。**这仍然只是间接证据**:两臂行为本来就相同(窗口是 inert 的),它能说明的是
|
||||
「窗口开着没有把画面弄坏」,不是「延后一条确认后画面仍然对」。
|
||||
|
||||
## 7. 下一步(如果要救这个想法)
|
||||
|
||||
要让延后的答案活下来,只有两条路,**都是协议面而不是调参**:
|
||||
|
||||
1. **加深回复池**:`ReplySlotCount` 在 Hello 里协商(`ClientSession.cpp:957` / `ServerSession.cpp:745`),
|
||||
两端一致即可改。需要的深度 \>= 两条 create 之间的回复数(本帧 13.2)乘以窗口深度,即 4 深度要 ~53 槽;
|
||||
而 `ReplyBytes` 固定 16 MiB 时槽会变小,`ReadPixels` 的 `ReplyCanHold` 会先撞墙,所以要一起抬 `ReplyBytes`。
|
||||
2. **让客户端能说「这条别回」**:四个 Bool 行(create/respecify/params/sub-data)目前服务端一律回;
|
||||
一个 record flag 就能把 59,673 砍到 ~4,543,那时 8 槽够用。这是 wire 改动,两端都要动。
|
||||
|
||||
## 8. 复算
|
||||
|
||||
~~~sh
|
||||
# 两臂的帧读数(handoff-2 §1 的读法)
|
||||
python3 mgl-device/bigframe.py mgl-device/logs/mcx-fix4.client mgl-device/logs/mcx-fix1.client
|
||||
grep -a "kind=frame-op frame=" mgl-device/logs/mcx-fix4.client | tail -1
|
||||
grep -a "kind=frame-sent frame=" mgl-device/logs/mcx-fix4.client | tail -1
|
||||
# 一轮设备 run(自带 force-stop、就绪判据、负载帧检测、截屏、SIGTERM 收尾)
|
||||
cd mgl-device && python3 run-once.py <tag> [MOBILEGL_IPC_CREATE_WINDOW=4]
|
||||
# 门
|
||||
build-split/MobileGL/MG_Test/Wire/RemoteClientTest --gtest_filter='*CreateWindow*:*RefusedCreate*'
|
||||
build-split/MobileGL/MG_Test/Wire/StreamLinkTest --gtest_filter='*OlderReply*'
|
||||
~~~
|
||||
@@ -0,0 +1,100 @@
|
||||
# 「①」该挂哪个信号:三个候选的判定(handoff-2 §3 调查记录)
|
||||
|
||||
> 2026-09-26。分支 `feat/disaggregated`。对应 [`MOBILEGL-CS-HANDOFF-2.md`](../../../../MOBILEGL-CS-HANDOFF-2.md)
|
||||
> 的 §3「把『①』挂到已有信号上」。**结论是 handoff §3.2 的前两个候选都不是信号**,成本仍在协议面;
|
||||
> 本文把三条理由落到符号,并把真正的缺口缩到一句话。
|
||||
|
||||
## 0. 一句话
|
||||
|
||||
- **§3.2 候选 1(caps 代次)不是信号**:caps Adopt 是「快照重发」,而 `MGPipeApplierReset`(make-current 的
|
||||
服务端半边)**故意保留**对象记录——代次动了 ≠ 记录没了。
|
||||
- **§3.2 候选 2(FreshlyPrimed)是同一个理由,而且它就是空的**:`TextureEmit.h:1257` 的
|
||||
`void Reset() {}` 函数体为空,注释写明「Only LATCHES reset here - the applier's object records survive a
|
||||
make-current」。
|
||||
- **真正的缺口只有一种**:描述符**没变**的 respecify 会在发射端被去重吞掉(`TextureEmit.h:838` 的
|
||||
`unchanged` 不读闩),连「拒绝」这个唯一的客户端可见信号都产生不了。描述符变了的那一半今天**已经能自愈**。
|
||||
- **今天不可达**:`MGPipeApplierReleaseObjectRecords` 在生产代码里没有调用者(只有 MG_Test,实测 grep 见 §5)。
|
||||
|
||||
## 1. 失效时要清的三样,逐条落到符号
|
||||
|
||||
| handoff §3.1 | 符号 | 今天的状态 |
|
||||
|---|---|---|
|
||||
| 发布闩 | `MGPipeNoteHandleUnpublished(kind, handle)`(`PipeFill.cpp:1890`),按 `{kind, slot, gen}` 键 | 对象销毁会清;跨 make-current 保留(设计如此) |
|
||||
| 各 emitter 的镜像 | `TextureEmit` 的 `entry.HasLastDesc` / `entry.HasParamsLatch` | **只清闩不够**:去重比闩更早,见 §4 |
|
||||
| create window 的 suspect 表 | `g_createSuspects`(`WireTables.cpp`) | 窗口默认已关(见 `CREATE-WINDOW-MEASURED.md`),这一条随之为空 |
|
||||
|
||||
## 2. 候选 1:`CapsMirror::Generation()` —— 动的是快照,不是记录
|
||||
|
||||
`CapsMirror::Generation()` 每次 `Adopt` 自增,而 Adopt 的触发点是**caps 快照重发**:`ServerMakeEGLCurrent`
|
||||
对「它没持有的 tuple」转发绑定时会重发一次(ID-67,`MakeCurrentRepublishCount`)。也就是说代次动
|
||||
**只说明服务端重新回答了一次能力问题**,不说明它的记录表空了。反证在 `MGPipeApplierReset` 自己身上:
|
||||
它是 make-current 的服务端半边,`PipeApply.cpp:1584-1590` 清的是 working state 与三个 binding-point 窗口,
|
||||
**texture / sampler / view / shader-CSO 记录一个都不清**——注释把理由写死了:「a GL object lives in a
|
||||
share group, and re-publishing one would move its Serial for nothing」。
|
||||
|
||||
照代次清 = 把「记录还活着」的每一次 make-current 也当成失效,每次切上下文重发全部存活纹理。方向安全
|
||||
(`PipeApply.h:822`:「Over-firing is the safe」),但这是**为一个今天不可达的路径付每次 make-current 的钱**。
|
||||
|
||||
## 3. 候选 2:`FreshlyPrimed` —— 同一个理由,且这个函数是空的
|
||||
|
||||
`PipeFill.cpp:3761` 的 FreshlyPrimed 臂确实在 reset 五个 emitter,但纹理那一个的 reset 是:
|
||||
|
||||
~~~cpp
|
||||
// A fresh context: what the server has is no longer what this emitter last sent. Only
|
||||
// LATCHES reset here - the applier's object records survive a make-current and
|
||||
// re-publishing them would move their serials for nothing.
|
||||
void Reset() {} // TextureEmit.h:1257
|
||||
~~~
|
||||
|
||||
空实现就是「这里没有可清的东西」的落地写法。同一个文件 `TextureEmit.h:1267-1271` 还把话说到另一头:
|
||||
「a texture handle and the applier record it names are SHARE-GROUP OBJECT STATE, so nothing here is
|
||||
per-context and **no re-publication path exists or may exist**」。所以候选 2 不是「一行」,是**推翻一条设计**。
|
||||
|
||||
## 4. 真正的缺口:去重在发射端,比闩更早
|
||||
|
||||
`TextureEmit.h:838`:
|
||||
|
||||
~~~cpp
|
||||
const Bool unchanged = entry.HasLastDesc && std::memcmp(&entry.LastDesc, &desc, sizeof(desc)) == 0;
|
||||
~~~
|
||||
|
||||
**它不读闩。** 于是 `MGPipeApplierReleaseObjectRecords` 之后两条路不一样:
|
||||
|
||||
| 情形 | 今天的走向 | 结果 |
|
||||
|---|---|---|
|
||||
| 描述符**变了** | 不去重 → 闩仍为真所以不发 create → `RespecifyOnce` 发 respecify → applier 拒绝 → **补一个 create 再重试** → 接受 | **已自愈**(handoff §3.3 要的门,今天就是绿的) |
|
||||
| 描述符**没变** | `if (unchanged) return;` | **一条记录都不过线**,服务端没有机会拒绝,自愈链断在这里 |
|
||||
|
||||
所以「①」要清的其实是第 2 行,而它要的**不是清闩,是清镜像**(`HasLastDesc`/`HasParamsLatch`)——
|
||||
只清闩的话那条「相同 desc」照样不发(handoff §3.1 已经点到,这里是它的符号)。
|
||||
|
||||
## 5. 可达性(本轮复核)
|
||||
|
||||
~~~
|
||||
$ grep -rn "MGPipeApplierReleaseObjectRecords" MobileGL/ --include=*.cpp --include=*.h | grep -v MG_Test
|
||||
PipeApply.cpp:1593 void MGPipeApplierReleaseObjectRecords() { <- 定义
|
||||
(其余全部是注释)
|
||||
~~~
|
||||
|
||||
与 handoff §3.4 一致:**生产调用者为零**,唯一调用者是 MG_Test(`PipeWireCodecTest`/`StagedTextureStoreTest`
|
||||
各一处、若干 `*EmitTest` 的 SetUp/TearDown)。闩按 `{kind, slot, gen}` 键、对象销毁会清闩,槽位复用安全。
|
||||
|
||||
## 6. 建议(本轮不做,理由在下面)
|
||||
|
||||
1. **不加反向回调**:那是协议面(两端重出 APK),而触发者今天不存在。handoff §7 的判词是「今天是潜伏,
|
||||
别为它付协议面的钱」。
|
||||
2. **不挂代次**:理由见 §2/§3——它会在每次 make-current 上 over-fire,为一个不可达路径付真实成本。
|
||||
3. **该做的是把缺口写成门**:用 `MGPipeApplierReleaseObjectRecords` 造「记录被丢掉」,
|
||||
断言**描述符变了那一半必须自愈**(今天绿,红的方式是把 `RespecifyOnce` 的补 create 去掉),
|
||||
并把**描述符没变那一半**作为已知缺口记账——它是这件事真正欠的债。
|
||||
4. 如果哪天真出现生产调用者,正确形状是给 entry 记一个「本代次内确认过」的戳(caps 代次或会话代次),
|
||||
代次一动就重发一次:它是 over-fire 方向,不用动 wire。
|
||||
|
||||
## 7. 复算
|
||||
|
||||
~~~sh
|
||||
grep -rn "MGPipeApplierReleaseObjectRecords" MobileGL/ --include=*.cpp --include=*.h | grep -v MG_Test
|
||||
sed -n '1249,1258p' MobileGL/MG_Impl/Pipe/TextureEmit.h # void Reset() {}
|
||||
sed -n '834,840p' MobileGL/MG_Impl/Pipe/TextureEmit.h # unchanged,不读闩
|
||||
sed -n '1575,1591p' MobileGL/MG_Pipe/PipeApply.cpp # MGPipeApplierReset 清什么、不清什么
|
||||
~~~
|
||||
Reference in New Issue
Block a user