From 590d74525da7021a3fc17f5005b73ead30503159 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 29 Sep 2026 17:40:56 -0400 Subject: [PATCH] [Feat] (MG_Remote, P11 B2): T0 behind MOBILEGL_IPC_ADOPT_TIER - control revision 5 (LinkTerms.adoptTier ask), kCapAdoptT0 granted at the first native bind after the backend's POST, the AHardwareBuffer Offer on the aux socket matched to its map_persistent record by seq, named T2 fallbacks instead of A1's handshake Fatal, the server allow switch, done-no-bytes CPU reads --- CMakeLists.txt | 4 + MobileGL/Config.h | 16 +- MobileGL/ConfigLoader.cpp | 9 +- MobileGL/MG_Pipe/MGPipeTypes.h | 7 + MobileGL/MG_Pipe/PipeApply.cpp | 24 ++ MobileGL/MG_Pipe/PipeApply.h | 24 ++ MobileGL/MG_Remote/Client/ClientSession.cpp | 213 ++++++++++++++++- MobileGL/MG_Remote/Client/ClientSession.h | 49 ++++ MobileGL/MG_Remote/Client/WireTables.cpp | 30 ++- .../Protocol/generated/protocol_generated.h | 14 +- .../MG_Remote/Protocol/mg_protocol_base.h | 3 +- MobileGL/MG_Remote/Protocol/protocol.fbs | 8 + MobileGL/MG_Remote/Server/AdoptInbox.cpp | 121 ++++++++++ MobileGL/MG_Remote/Server/AdoptInbox.h | 72 ++++++ MobileGL/MG_Remote/Server/PipeApplier.cpp | 5 + MobileGL/MG_Remote/Server/PipeApplier.h | 2 + MobileGL/MG_Remote/Server/ServerLoop.cpp | 6 + MobileGL/MG_Remote/Server/ServerSession.cpp | 155 +++++++++++- MobileGL/MG_Remote/Server/ServerSession.h | 36 +++ MobileGL/MG_Remote/Server/ServerSpawn.cpp | 6 +- MobileGL/MG_Remote/Transport/AdoptT0.cpp | 225 ++++++++++++++++++ MobileGL/MG_Remote/Transport/AdoptT0.h | 110 +++++++++ MobileGL/MG_Remote/Transport/AdoptTier.cpp | 44 ++-- MobileGL/MG_Remote/Transport/AdoptTier.h | 48 ++-- MobileGL/MG_Remote/Wire/PipeWireCodec.cpp | 13 +- MobileGL/MG_Remote/Wire/PipeWireCodec.h | 7 + .../GLState/BufferState/BufferObject.cpp | 18 ++ scripts/ci/fatal_census.py | 11 + scripts/ci/protocol_revision_pins.json | 3 +- 29 files changed, 1223 insertions(+), 60 deletions(-) create mode 100644 MobileGL/MG_Remote/Server/AdoptInbox.cpp create mode 100644 MobileGL/MG_Remote/Server/AdoptInbox.h create mode 100644 MobileGL/MG_Remote/Transport/AdoptT0.cpp create mode 100644 MobileGL/MG_Remote/Transport/AdoptT0.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a07cc14..8575b6b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -618,6 +618,8 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # P11 A1: the adopt tier settled at the handshake from the knob and the data plane, by # both roles (so SERVER-partition code in the link ratchet, not the client's tracker). MobileGL/MG_Remote/Transport/AdoptTier.cpp + # P11 B2: T0 - the client's AHardwareBuffer store and its hop descriptor (both roles). + MobileGL/MG_Remote/Transport/AdoptT0.cpp # ---- P5: the three new directories ------------------------------ # Wire/ the G3 codec: MGPWireRec_* in and out of SEG_CMD, blobs and # var-tails in and out of SEG_STAGE. [w1] @@ -661,6 +663,8 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # P11 PAIR: a socket client's control and aux connections paired by the nonce each # presents first (PairBind), not by arrival order - both unix supervisors take pairs here. MobileGL/MG_Remote/Server/PairAcceptor.cpp + # P11 B2: T0's Offers on the aux socket, matched to their map_persistent records by seq. + MobileGL/MG_Remote/Server/AdoptInbox.cpp MobileGL/MG_Remote/Server/ServerSpawn.cpp # P5f fc: the value frame that replaced the control mailbox's function-pointer payload, # and its codec against protocol.fbs's SurfaceOp/SurfaceReply (the spawn arm's seam). diff --git a/MobileGL/Config.h b/MobileGL/Config.h index c4e81b09..dced182b 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -625,12 +625,18 @@ namespace MobileGL::MG_Config { // poll loop cannot outlive a late report. 0 = every poll crosses, as before P10: the A/B // arm and the red-once of the no-reply gate (FencePollScenario). Uint32 PollEscalate = 64; - // MOBILEGL_IPC_ADOPT_TIER: 2 = emulate (T2: client keeps the shadow and pushes), the - // only implemented tier and the reason persistent-map-push can be non-zero at all - // (R-6). 0 (T0) and 1 (T1) parse and are settled at the handshake (P11 A1, - // MG_Remote/Transport/AdoptTier.h): a stream data plane refuses them by name and runs - // T2; over shared segments the client dies by name before any record. + // MOBILEGL_IPC_ADOPT_TIER, THE ZERO-COPY SWITCH (ruling ID-P11-14, CONTRACT-P11 B2): + // 2 = emulate (T2: the client keeps the shadow and pushes), the default and today's + // behaviour byte for byte; 0 = T0 (the client allocates each adopted store as an + // AHardwareBuffer and the server imports it); 1 = T1, closed - refused by name, then T2. + // A T0 this session cannot use (a stream plane, the server's allow switch, no + // AHardwareBuffer, a failed server POST) is T2 plus one named line, never a Fatal + // (MG_Remote/Transport/AdoptTier.h). Uint32 AdoptTier = 2; + // MOBILEGL_IPC_ALLOW_ADOPT_T0 (server): 1 (default) = grant a client's T0 ask when this + // platform and the backend's POST self-test allow it; 0 = never grant it (the session + // runs T2 and says so). Read by the server role only; kept through ServerSpawn. + Uint32 AllowAdoptT0 = 1; // MOBILEGL_IPC_VERB_BARRIER: 1 = the client blocks at every verb boundary until // appliedSeq reaches its emitSeq (R-1). 0 is the negative control: it is EXPECTED to // be red, because 31 of the 63 PipeInputs fields are still pulled from a live diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index f2e0e4a4..0785f384 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -433,9 +433,11 @@ namespace MobileGL::MG_ConfigLoader { // torn read rather than a divergence. The batch is therefore off whenever the // shadow comparer is armed. if (MG_Config::Features.PipeVerify) ipc.BatchWaits = 0; - // 2 is the only implemented tier (R-6). 0 and 1 parse here and are settled at the - // handshake from the data plane (P11 A1, MG_Remote/Transport/AdoptTier.h). + // 2 (T2) is the default; 0 asks for T0 and 1 is refused by name - both settled at the + // handshake from the data plane (P11 A1/B2, MG_Remote/Transport/AdoptTier.h). ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", 2, 0, 2); + // P11 B2: the server's own T0 allow switch (Config.h). + ipc.AllowAdoptT0 = QueryEnvUint32("MOBILEGL_IPC_ALLOW_ADOPT_T0", 1, 0, 1); ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1); // P5e (MG_Remote/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every // other IPC knob and armed only where the server publishes kCapRunAheadApply. @@ -530,10 +532,11 @@ namespace MobileGL::MG_ConfigLoader { // log that does not name it makes every run ambiguous about its own configuration. MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus event-wait=%ums " "persistent-block=%uKiB create-window=%u pbo-readback-sync=%u poll-escalate=%u " - "adopt-tier=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums " + "adopt-tier=%u allow-t0=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums " "cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s' surface=%s", ipc.RingMb, ipc.StageMb, ipc.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs, ipc.PersistentBlockKb, ipc.CreateWindow, ipc.PboReadbackSync, ipc.PollEscalate, ipc.AdoptTier, + ipc.AllowAdoptT0, ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs, static_cast(ipc.StrictErrors), static_cast(ipc.Audit), static_cast(ipc.RoleSplitState), ipc.ServerAffinity.c_str(), diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index 73a4aafb..d1b40ad3 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -150,6 +150,13 @@ namespace MobileGL::MG_Pipe { // snapshot may only turn it OFF, because a caps re-run must not be able to make a // lockstep server run-ahead in the middle of a frame. kCapRunAheadApply = 1ull << 10, + // P11 B2 (MG_Remote/CONTRACT-P11.md B2). THIS SESSION MAY RUN T0: the client asked for + // it in its Hello (LinkTerms.adoptTier = 0) on a shared-segment plane, the server's + // allow switch is on, this platform has AHardwareBuffer, and the backend's POST + // self-test of the sustained-lock pattern passed on its live context. Never set in the + // CapsSnapshot Accept sends (no context exists yet); set in the one the first native + // bind republishes. The client adopts through T0 only while it reads this bit. + kCapAdoptT0 = 1ull << 11, }; // Readiness is supplied independently by each backend's integration gate. diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 92cf5cb5..92a9957c 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -2497,6 +2497,30 @@ namespace MobileGL::MG_Pipe { } } +#if MOBILEGL_BUILD_DISAGGREGATED + Bool MGPipeApplyAdoptExternal(const MGPHandleOnly& handle, void* ahb, Uint64 size) { + // The same kind verdict unmap_persistent gives: a store is the buffer family's. + if (static_cast(handle.Kind) != MGPipeKind::Buffer) { + MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption") + " adopt_external {slot=%u, gen=%u}: a T0 store is the buffer family's " + "and the handle names another kind (%u)", + handle.Handle.Slot, handle.Handle.Gen, handle.Kind); + return false; + } + MGPipeResourceRecord* record = ResolveResource("adopt_external", handle.Handle); + if (record == nullptr) return false; + // The store the AHB replaces is the record's whole extent; a handle whose width moved + // (a respecify the client has not caught up with) is not this store any more. + if (size == 0 || size != record->Desc.Width) return false; + // The same pin map_persistent takes: adoption is where a producer of live host writes + // would attach, and T0 attaches none - the client writes the AHB through its lock, as a + // monolith client writes its adopted map, and the applier is told nothing either way. + PinNoLiveHostWrites(*record, handle.Handle, "adopt_external"); + if (g_resourceOps == nullptr || g_resourceOps->ImportExternal == nullptr) return false; + return g_resourceOps->ImportExternal(handle.Handle, ahb, size); + } +#endif + // ================================================================================ // P3a: the five vertex-input entry points (D-G, D-H, D-I). // diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index abade410..01bd7264 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -127,6 +127,22 @@ namespace MobileGL::MG_Pipe { void (*TextureRespecify)(MGPipeHandle res, const MGPResourceDesc& desc, const MGPRespecifiedLevel* level); void (*TextureDestroy)(MGPipeHandle res); + // P11 B2 (T0, MG_Remote/CONTRACT-P11.md B2): the two members a backend that can take a + // client's AHardwareBuffer as a store registers. Both null = this backend has no T0 and + // every session it serves runs T2. Appended after the texture trio for the same reason. + // + // ImportExternal makes `ahb` (an AHardwareBuffer*, a BLOB of `size` bytes) the resident + // store of buffer `res` - the twin a monolith map_persistent would have minted - and takes + // its OWN reference, which it releases only after its own GPU fence for the store's last + // use has completed. False = not imported: the session answers DECLINED and the store runs + // T2 as before. + Bool (*ImportExternal)(MGPipeHandle res, void* ahb, Uint64 size); + // The POST self-test of the sustained-lock pattern (the AHB API does not promise it): with + // this backend's context current on the apply thread, allocate a small AHB, hold its CPU + // lock for the whole test, import it, let the GPU read what the CPU wrote and write a + // pattern of its own, and read that back through the held pointer (and through the + // backend's own host map of the import). False with the reason in `why`. + Bool (*SelfTestExternal)(char* why, Uint64 whyBytes); #endif }; @@ -1298,6 +1314,14 @@ namespace MobileGL::MG_Pipe { // unmap_persistent: the donation ends. Never emitted by P3a's own paths; the call exists // so the pair is complete and the transport has both halves. void MGPipeApplyUnmapPersistent(const MGPHandleOnly& handle); +#if MOBILEGL_BUILD_DISAGGREGATED + // P11 B2 (T0): the split server's map_persistent for a session that runs T0. The client's + // AHardwareBuffer `ahb` (`size` bytes) becomes buffer `handle`'s resident store through the + // backend's ImportExternal. A handle that is not a live buffer of that width, or a backend + // without the member, declines (false); the codec answers DECLINED and the store runs T2. + // MapPersistentRoundtrips is not moved: under split the server never counted map_persistent. + Bool MGPipeApplyAdoptExternal(const MGPHandleOnly& handle, void* ahb, Uint64 size); +#endif // --------------------------------------------------------------------------------- // P3a: the five vertex-input entry points (D-G, D-H, D-I) diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index 5259f883..b4f0da33 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -721,7 +721,11 @@ namespace MobileGL::MG_Remote::Client { { ::flatbuffers::FlatBufferBuilder builder(512); auto stamp = builder.CreateString(BuildFingerprint()); - auto terms = ::MobileGL::Wire::CreateLinkTerms(builder, dataPlane); + // P11 B2: the adopt-tier ask rides the proposal (LinkTerms.adoptTier, append-only). + auto terms = ::MobileGL::Wire::CreateLinkTerms( + builder, dataPlane, ::MobileGL::Wire::WireForm::StructImage, 0, 0, 0, 0, + static_cast(Transport::AdoptTierAskFor( + dataPlane == ::MobileGL::Wire::DataPlane::Stream))); const char* tokenText = std::getenv("MOBILEGL_IPC_TOKEN"); auto token = builder.CreateString(tokenText == nullptr ? "" : tokenText); auto hello = ::MobileGL::Wire::CreateHello( @@ -809,8 +813,17 @@ namespace MobileGL::MG_Remote::Client { // P11 A1: the adopt tier, settled by the side that holds the knob once the link terms // say which plane this is (inproc is shared segments by construction), before any // record. T0/T1 over shared segments die here by name (Transport/AdoptTier.h). - (void)Transport::SettleAdoptTierAtHandshake( + // P11 B2: 0 = this client asked T0 in its Hello; the Welcome echoes the ask it recorded. + const std::uint32_t adoptAsk = Transport::SettleAdoptTierAtHandshake( terms->dataPlane() == ::MobileGL::Wire::DataPlane::Stream, Transport::AdoptTierSide::Client); + if (terms->adoptTier() != adoptAsk) { + const auto result = RefuseHandshake(*m_transport, ::MobileGL::Wire::RefuseCode::LinkTerms, + "the server's adoptTier echo is not this client's ask", adoptAsk, terms->adoptTier()); + Stop(); + return result; + } + ResetT0(false); + m_adoptT0Asked = adoptAsk == 0; // The four SegmentRefs are what a spawn client MAPS (P6). Under inproc the mapping // already exists, so what they are good for here is the cross-check that the two // sides agree about the geometry at all - which is the assertion that would @@ -1013,7 +1026,11 @@ namespace MobileGL::MG_Remote::Client { { ::flatbuffers::FlatBufferBuilder builder(512); auto stamp = builder.CreateString(BuildFingerprint()); - auto terms = ::MobileGL::Wire::CreateLinkTerms(builder, dataPlane); + // P11 B2: the adopt-tier ask rides the proposal (LinkTerms.adoptTier, append-only). + auto terms = ::MobileGL::Wire::CreateLinkTerms( + builder, dataPlane, ::MobileGL::Wire::WireForm::StructImage, 0, 0, 0, 0, + static_cast(Transport::AdoptTierAskFor( + dataPlane == ::MobileGL::Wire::DataPlane::Stream))); const char* tokenText = std::getenv("MOBILEGL_IPC_TOKEN"); auto token = builder.CreateString(tokenText == nullptr ? "" : tokenText); auto hello = ::MobileGL::Wire::CreateHello( @@ -1101,7 +1118,16 @@ namespace MobileGL::MG_Remote::Client { // P11 A1: as in StartOverTransportPair - the knob against the plane the link terms // settled, before the data connection and before any record. A stream refuses T0/T1 // by name and runs T2; shared segments die here by name (Transport/AdoptTier.h). - (void)Transport::SettleAdoptTierAtHandshake(stream, Transport::AdoptTierSide::Client); + const std::uint32_t adoptAsk = + Transport::SettleAdoptTierAtHandshake(stream, Transport::AdoptTierSide::Client); + if (terms->adoptTier() != adoptAsk) { + const auto result = RefuseHandshake(*m_transport, ::MobileGL::Wire::RefuseCode::LinkTerms, + "the server's adoptTier echo is not this client's ask", adoptAsk, terms->adoptTier()); + Stop(); + return result; + } + ResetT0(false); + m_adoptT0Asked = adoptAsk == 0; if (stream) { // PH-7 (4). A Stream Welcome without a nonce of exactly the minted width names no // data connection this client could open; refused rather than guessed at. @@ -1803,6 +1829,9 @@ namespace MobileGL::MG_Remote::Client { // 4. and ONLY NOW may anything an emitter owns be released: a var-tail still // referenced by an unapplied record is a use-after-free the join is what prevents. LogMemory("teardown"); + // P11 B2: the T0 arm's proof for this session, and the client's references to its stores + // (the server's imports held their own and are gone with the server's backend). + ResetT0(true); LogWireLedger(); Transport::LinkMetricsEnd(); m_producer.Detach(); @@ -1999,6 +2028,14 @@ namespace MobileGL::MG_Remote::Client { // and the window does not defer on them (see ClientSession::LinkRetainsReplies). if (rowCarriesReplySlot && willReadReply && m_link != nullptr) m_link->DeclareReplyRead(seq); + // P11 B2: T0's Offer is queued HERE - the seq exists and the record is not yet visible, + // so the server's inbox finds the descriptor already queued when it decodes the record + // (Server/AdoptInbox.h). One-shot: AdoptPersistentT0 sets it for its own record only. + if (m_prePublish) { + std::function hook = std::move(m_prePublish); + m_prePublish = nullptr; + hook(seq); + } m_producer.PublishAndNotify(seq); if (op == MG_Pipe::MGPWireOp::Present) { // A credit is permission to run ahead, not permission to retain the @@ -2666,6 +2703,174 @@ namespace MobileGL::MG_Remote::Client { Transport::ITransport* ClientSession::Control_Plane() { return m_transport; } + // ---- P11 B2: T0 (MG_Remote/CONTRACT-P11.md B2) -------------------------------------------- + + namespace { + Uint64 T0Key(MG_Pipe::MGPipeHandle handle) { + return (static_cast(handle.Gen) << 32) | handle.Slot; + } + // 0 = queue each Offer in order; > 0 = queue it that many ms AFTER its record is published + // (from another thread); < 0 = never queue it. Test seam only (SetT0OfferFaultForTest). + std::atomic g_t0OfferFault{0}; + } // namespace + + void ClientSession::SetT0OfferFaultForTest(Int32 delayMs) { + g_t0OfferFault.store(delayMs, std::memory_order_release); + } + + Bool ClientSession::AdoptT0() { + if (!m_adoptT0Asked) return false; + if (m_adoptT0Decision >= 0) return m_adoptT0Decision == 1; + if (Caps().HasCap(MG_Pipe::kCapAdoptT0)) { + m_adoptT0Decision = 1; + MGLOG_I("MG_Remote client: T0 - the server published kCapAdoptT0 (its POST self-test of the " + "sustained-lock pattern passed), so this session adopts persistent stores as " + "AHardwareBuffers the server imports"); + } else { + m_adoptT0Decision = 0; + MGLOG_W("MG_Remote client: Refuse{AdoptT0Unavailable, \"no kCapAdoptT0\"} - " + "MOBILEGL_IPC_ADOPT_TIER=0 asked for T0 and the server did not publish kCapAdoptT0 (its " + "log names why: its allow switch, no AHardwareBuffer, or its POST self-test); the " + "session runs T2 (emulate: the client keeps the shadow and pushes)"); + } + return m_adoptT0Decision == 1; + } + + void* ClientSession::AdoptPersistentT0(const MG_Pipe::MGPHandleOnly& handle, Uint64 size, + const void* seed) { + using namespace Transport::AdoptT0; + // Everything that can fail runs BEFORE the record is encoded, so the record never waits on + // a step that has not happened: either the Offer carries the handle or it says it does not. + HeldStore store; + std::string why; + const Bool held = AllocateHeld(size, seed, store, why); + if (!held) { + MGLOG_W("MG_Remote client: T0 store {slot=%u, gen=%u} (%llu bytes) could not be allocated (%s); " + "the server is told (an Offer without a handle) and this store runs T2", + handle.Handle.Slot, handle.Handle.Gen, static_cast(size), why.c_str()); + } + std::string hopWhy; + const int hop = MakeHop(held ? &store : nullptr, hopWhy); + if (hop < 0) { + // No descriptor at all, not even an empty hop. NO RECORD IS EMITTED: a map_persistent + // the server would wait on without an Offer is exactly what the protocol never sends. + // The store stays on its shadow (T2) - the same answer a DECLINED would have given - + // and the server, having seen no record, has nothing to answer. + MGLOG_W("MG_Remote client: Refuse{AdoptT0NoStore, \"hop\"} - no hop socket for the T0 store " + "{slot=%u, gen=%u} (%s); no map_persistent is sent and the store runs T2", + handle.Handle.Slot, handle.Handle.Gen, hopWhy.c_str()); + ReleaseHeld(store); + ++m_t0Declined; + return nullptr; + } + MobileGLResult shared = MOBILEGL_ERR_NOT_INITIALIZED; + m_prePublish = [&](Uint64 seq) { + Offer offer{}; + offer.magic = kOfferMagic; + offer.version = kOfferVersion; + offer.seq = seq; + offer.size = size; + offer.flags = held ? kOfferHasBuffer : 0u; + const Int32 fault = g_t0OfferFault.load(std::memory_order_acquire); + if (fault < 0) { + shared = MOBILEGL_OK; // the test's lost Offer: the server's bounded wait answers + return; + } +#if !defined(_WIN32) + if (fault > 0 && m_transport != nullptr) { + // The test's LATE Offer: the record goes out now, the descriptor `fault` ms later. + const int copy = ::dup(hop); + Transport::ITransport* transport = m_transport; + std::thread([transport, copy, offer, fault] { + std::this_thread::sleep_for(std::chrono::milliseconds(fault)); + Transport::AdoptT0::Offer late = offer; + (void)transport->ShareFd(copy, MobileGLByteSpan{&late, sizeof(late)}); + ::close(copy); + }).detach(); + shared = copy >= 0 ? MOBILEGL_OK : MOBILEGL_ERR_TRANSPORT_CLOSED; + return; + } +#endif + shared = m_transport != nullptr + ? m_transport->ShareFd(hop, MobileGLByteSpan{&offer, sizeof(offer)}) + : MOBILEGL_ERR_NOT_INITIALIZED; + }; + Int32 status = Wire::ReplySink::kStatusError; + EmitAndWait(MG_Pipe::MGPWireOp::MapPersistent, &handle, sizeof(handle), nullptr, 0, nullptr, 0, &status); + m_prePublish = nullptr; // a cancelled encode never ran it +#if !defined(_WIN32) + ::close(hop); +#endif + if (shared != MOBILEGL_OK) { + MGLOG_E("MG_Remote client: the T0 store {slot=%u, gen=%u} could not be queued on the aux socket " + "(rc=%d); its record is answered by the server's bounded wait", + handle.Handle.Slot, handle.Handle.Gen, static_cast(shared)); + } + if (status == Wire::ReplySink::kStatusError) { + ReleaseHeld(store); + SessionFail(MGFatalFamily::ReplyError, + "MGPipe: Fatal{ReplyError, \"map_persistent\"} - the T0 row answered ERROR, which is " + "not an acceptance answer"); + } + if (status == Wire::ReplySink::kStatusOk) { + if (!held) { + SessionFail(MGFatalFamily::UnexpectedMapAccept, + "MGPipe: Fatal{UnexpectedMapAccept, \"map_persistent\"} - the server accepted a T0 " + "store whose Offer carried no AHardwareBuffer"); + } + ++m_t0Imported; + m_t0Bytes += size; + void* ptr = store.ptr; + // A handle is one store at a time; a stale entry (never released) would leak a lock. + const Uint64 key = T0Key(handle.Handle); + if (auto it = m_t0Stores.find(key); it != m_t0Stores.end()) { + ReleaseHeld(it->second); + ++m_t0Released; + } + m_t0Stores[key] = store; + MGLOG_I("MG_Remote client: T0 store {slot=%u, gen=%u} adopted - %llu bytes, the server imported " + "this AHardwareBuffer; session total %llu stores / %llu bytes", + handle.Handle.Slot, handle.Handle.Gen, static_cast(size), + static_cast(m_t0Imported), static_cast(m_t0Bytes)); + return ptr; + } + ++m_t0Declined; + if (held) { + MGLOG_W("MG_Remote client: T0 store {slot=%u, gen=%u} (%llu bytes) DECLINED by the server (its log " + "names why); the store runs T2", + handle.Handle.Slot, handle.Handle.Gen, static_cast(size)); + } + ReleaseHeld(store); + return nullptr; + } + + void ClientSession::ReleaseT0Store(MG_Pipe::MGPipeHandle handle) { + auto it = m_t0Stores.find(T0Key(handle)); + if (it == m_t0Stores.end()) return; + Transport::AdoptT0::ReleaseHeld(it->second); + m_t0Stores.erase(it); + ++m_t0Released; + } + + void ClientSession::ResetT0(Bool releaseStores) { + if (m_adoptT0Asked) { + MGLOG_I("MG_Remote client: T0 session totals - %s; stores imported %llu, bytes %llu, declined %llu, " + "released %llu, held at teardown %zu", + m_adoptT0Decision == 1 ? "ran T0" : m_adoptT0Decision == 0 ? "fell back to T2" : "never decided", + static_cast(m_t0Imported), static_cast(m_t0Bytes), + static_cast(m_t0Declined), static_cast(m_t0Released), + m_t0Stores.size()); + } + if (releaseStores) { + for (auto& [key, store] : m_t0Stores) Transport::AdoptT0::ReleaseHeld(store); + } + m_t0Stores.clear(); + m_adoptT0Asked = false; + m_adoptT0Decision = -1; + m_t0Imported = m_t0Bytes = m_t0Declined = m_t0Released = 0; + m_prePublish = nullptr; + } + Transport::RoleMemorySample ClientSession::SampleMemory() const { return Transport::SampleRoleMemory(Transport::MemoryRole::Client); } diff --git a/MobileGL/MG_Remote/Client/ClientSession.h b/MobileGL/MG_Remote/Client/ClientSession.h index d2617791..16f920eb 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.h +++ b/MobileGL/MG_Remote/Client/ClientSession.h @@ -52,9 +52,13 @@ #include "../Transport/RoleMemory.h" #include "../Transport/SessionRings.h" #include "../Transport/SocketTransport.h" +#include "../Transport/AdoptT0.h" #include "../Wire/PipeWireCodec.h" #include "CapsMirror.h" +#include +#include + #include namespace MobileGL::MG_Remote::Transport { @@ -223,6 +227,37 @@ namespace MobileGL::MG_Remote::Client { Uint64* replySizeOut = nullptr, Bool wantReply = true, Bool willReadReply = true); + // ---- P11 B2: T0 (MG_Remote/CONTRACT-P11.md B2) ------------------------------------ + // + // Does this session adopt persistent stores through T0? Decided ONCE, lazily, at the first + // map_persistent - the first moment it matters, and one at which the server's first native + // bind has certainly republished its caps (a buffer store cannot exist before a + // make-current, and the make-current adopts what the bind published): the Hello asked T0 + // (MOBILEGL_IPC_ADOPT_TIER=0 over shared segments) AND the server published kCapAdoptT0. + // A T0 the session asked for and cannot use is T2 plus ONE named line here + // (`Refuse{AdoptT0Unavailable, ...}`), never a Fatal. + Bool AdoptT0(); + // T0's map_persistent: allocate the AHB, hold its lock, seed it from `seed` (the shadow), + // queue its Offer for the record's seq BEFORE the record is published (the order the + // server's inbox relies on), and wait for the answer. The held pointer on OK - the caller + // adopts it as the store's mapping - or null on DECLINED (the store stays T2). + void* AdoptPersistentT0(const MG_Pipe::MGPHandleOnly& handle, Uint64 size, const void* seed); + // The client's reference to buffer `handle`'s T0 store ends because the buffer was + // respecified or destroyed: unlock and release it. The server's import holds its own + // reference until its own fence for the store's last use has completed. No-op for a + // handle with no T0 store. + void ReleaseT0Store(MG_Pipe::MGPipeHandle handle); + // Per-session T0 arm proof (B2): stores the server imported, their bytes, and the + // map_persistent answers that came back DECLINED in a T0 session. + Uint64 T0StoresImported() const { return m_t0Imported; } + Uint64 T0BytesImported() const { return m_t0Bytes; } + Uint64 T0Declined() const { return m_t0Declined; } + // Test seam: 0 = queue each Offer before its record (the product order); > 0 = queue it + // that many ms AFTER the record is published, from another thread; < 0 = never queue it. + // What the host tests use to prove a late Offer still lands and a lost one is a named + // DECLINED rather than a hang. + static void SetT0OfferFaultForTest(Int32 delayMs); + // `willReadReply` (P12) and `wantReply` ARE TWO QUESTIONS AND MUST STAY TWO. // // wantReply does THIS CALL read the answer (it blocks on the barrier and takes the @@ -554,6 +589,20 @@ namespace MobileGL::MG_Remote::Client { // Welcome::serverPid, kept because §9.5's arm proof wants it and because a value that is // only ever logged cannot be asserted on. std::uint32_t m_peerServerPid = 0; + + // P11 B2 (T0). The Hello's ask, settled at the handshake; the lazy decision (-1 = not + // yet, 0 = T2, 1 = T0); the stores this client holds by handle key (Gen << 32 | Slot); + // the counters; and the one-shot hook EmitAndWaitTails runs between EncodeRecord (the + // seq exists) and PublishAndNotify (the record is visible) - where the Offer is queued. + Bool m_adoptT0Asked = false; + Int32 m_adoptT0Decision = -1; + std::unordered_map m_t0Stores; + Uint64 m_t0Imported = 0; + Uint64 m_t0Bytes = 0; + Uint64 m_t0Declined = 0; + Uint64 m_t0Released = 0; + std::function m_prePublish; + void ResetT0(Bool releaseStores); }; // One per process in P5, because P5 serves one context, and LEAKED AT EXIT like every other diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp index 6d126dcb..bbe1a887 100644 --- a/MobileGL/MG_Remote/Client/WireTables.cpp +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -201,7 +201,18 @@ namespace MobileGL::MG_Remote::Client { } // -- screen ------------------------------------------------------------------- - MGP_WIRE_PLAIN(ResourceDestroy, MGPHandleOnly, Screen) + // resource_destroy is MGP_WIRE_PLAIN plus one line: P11 B2 - a destroyed buffer's T0 + // store is released by this client after the record is out (the server's import holds its + // own reference until its fence for the store's last use; CONTRACT-P11.md B2). + void Wire_ResourceDestroy(const MG_Pipe::MGPHandleOnly* payload) { + if (RunsAsTheServerRole()) { MG_Pipe::MGPipeMonolithScreen().ResourceDestroy(payload); return; } + ClientSession& session = RequireSession("ResourceDestroy"); + session.EmitAndWait(MGPWireOp::ResourceDestroy, payload, sizeof(*payload), nullptr, 0, nullptr, 0, + nullptr); + ++g_emitted; + if (static_cast(payload->Kind) == MG_Pipe::MGPipeKind::Buffer) + session.ReleaseT0Store(payload->Handle); + } MGP_WIRE_PLAIN(UnmapPersistent, MGPHandleOnly, Screen) // -- context, plain ----------------------------------------------------------- @@ -887,6 +898,13 @@ namespace MobileGL::MG_Remote::Client { nullptr, 0, nullptr, 0, &status); } ++g_emitted; + // P11 B2: A RESPECIFIED BUFFER'S T0 STORE IS DONE FOR THIS CLIENT. The frontend has + // already dropped its adopted pointer (BufferObject::RedefineStorage) and the store's + // replacement is minted by the next map_persistent; the server keeps its own + // reference until its fence for the store's last use has completed, so releasing ours + // here cannot pull pages from under a frame still in flight (the fire-and-forget arm + // above included). + if (desc->Target == MG_Pipe::kMGPipeResourceTargetBuffer) session.ReleaseT0Store(desc->Resource); // ERROR IS NOT A DECLINE (M4 / codex 5 / R-5). status is 0 OK / 1 DECLINED / 2 ERROR; // an escape may not fold 2 into `false`, which is what a bare `return status == 0` // did - a transport fault then read as "the server said no". Every reply-owning row @@ -929,6 +947,16 @@ namespace MobileGL::MG_Remote::Client { return MG_Pipe::MGPipeMonolithEscapes().MapPersistent(handle, size, seedBytes); } ClientSession& session = RequireSession("MapPersistent"); + // P11 B2 (CONTRACT-P11.md B2): A T0 SESSION ADOPTS FOR REAL. The store is an + // AHardwareBuffer this client allocates, holds locked and seeds from the shadow + // (`seedBytes`, `size` of them); the server imports it and answers OK, and the held + // pointer is the mapping. Everything below is the T2 arm, unchanged byte for byte. + if (session.AdoptT0()) { + void* adopted = session.AdoptPersistentT0(*handle, size, seedBytes); + ++g_emitted; + if (adopted == nullptr) ++g_declined; + return adopted; + } (void)size; (void)seedBytes; Int32 status = 0; diff --git a/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h b/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h index 2a2fc473..dc1f997c 100644 --- a/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h +++ b/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h @@ -783,7 +783,8 @@ struct LinkTerms FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { VT_MAXREPLYBYTES = 8, VT_CMDWINDOWBYTES = 10, VT_STAGEWINDOWBYTES = 12, - VT_EVENTWINDOWBYTES = 14 + VT_EVENTWINDOWBYTES = 14, + VT_ADOPTTIER = 16 }; MobileGL::Wire::DataPlane dataPlane() const { return static_cast(GetField(VT_DATAPLANE, 0)); @@ -803,6 +804,9 @@ struct LinkTerms FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { uint64_t eventWindowBytes() const { return GetField(VT_EVENTWINDOWBYTES, 0); } + uint8_t adoptTier() const { + return GetField(VT_ADOPTTIER, 2); + } template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { return VerifyTableStart(verifier) && @@ -812,6 +816,7 @@ struct LinkTerms FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { VerifyField(verifier, VT_CMDWINDOWBYTES, 8) && VerifyField(verifier, VT_STAGEWINDOWBYTES, 8) && VerifyField(verifier, VT_EVENTWINDOWBYTES, 8) && + VerifyField(verifier, VT_ADOPTTIER, 1) && verifier.EndTable(); } }; @@ -838,6 +843,9 @@ struct LinkTermsBuilder { void add_eventWindowBytes(uint64_t eventWindowBytes) { fbb_.AddElement(LinkTerms::VT_EVENTWINDOWBYTES, eventWindowBytes, 0); } + void add_adoptTier(uint8_t adoptTier) { + fbb_.AddElement(LinkTerms::VT_ADOPTTIER, adoptTier, 2); + } explicit LinkTermsBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { start_ = fbb_.StartTable(); @@ -856,12 +864,14 @@ inline ::flatbuffers::Offset CreateLinkTerms( uint64_t maxReplyBytes = 0, uint64_t cmdWindowBytes = 0, uint64_t stageWindowBytes = 0, - uint64_t eventWindowBytes = 0) { + uint64_t eventWindowBytes = 0, + uint8_t adoptTier = 2) { LinkTermsBuilder builder_(_fbb); builder_.add_eventWindowBytes(eventWindowBytes); builder_.add_stageWindowBytes(stageWindowBytes); builder_.add_cmdWindowBytes(cmdWindowBytes); builder_.add_maxReplyBytes(maxReplyBytes); + builder_.add_adoptTier(adoptTier); builder_.add_wireForm(wireForm); builder_.add_dataPlane(dataPlane); return builder_.Finish(); diff --git a/MobileGL/MG_Remote/Protocol/mg_protocol_base.h b/MobileGL/MG_Remote/Protocol/mg_protocol_base.h index cacd67e2..81409bf9 100644 --- a/MobileGL/MG_Remote/Protocol/mg_protocol_base.h +++ b/MobileGL/MG_Remote/Protocol/mg_protocol_base.h @@ -69,7 +69,8 @@ extern "C" { // SurfaceReply.width/height/refusal. // 4 P11 PAIR: the PairBind message - a socket client's control and aux connections pair by // the nonce each presents first, not by arrival order. -#define MOBILEGL_PROTOCOL_CONTROL_REVISION 4 +// 5 P11 B2: LinkTerms.adoptTier - the client's persistent-map adoption tier ask (T0 or T2). +#define MOBILEGL_PROTOCOL_CONTROL_REVISION 5 #define MOBILEGL_ABI_VERSION(major, minor) (((uint32_t)(major) << 16) | (uint32_t)(minor)) #define MOBILEGL_ABI_MAJOR_OF(version) ((uint32_t)(version) >> 16) diff --git a/MobileGL/MG_Remote/Protocol/protocol.fbs b/MobileGL/MG_Remote/Protocol/protocol.fbs index b0f05e5a..e9be0e01 100644 --- a/MobileGL/MG_Remote/Protocol/protocol.fbs +++ b/MobileGL/MG_Remote/Protocol/protocol.fbs @@ -69,6 +69,14 @@ table LinkTerms { cmdWindowBytes: ulong; stageWindowBytes: ulong; eventWindowBytes: ulong; + // P11 B2 (MG_Remote/CONTRACT-P11.md B2), appended. In the Hello: the persistent-map adoption + // tier the client ASKS for - 0 = T0 (it allocates each adopted store as an AHardwareBuffer and + // hands it over the aux socket), 2 = T2 (emulate: shadow + push). 1 (T1) is closed and never + // sent; a stream plane always asks 2. In the Welcome: the server's echo of the ask it + // recorded. It is not the tier: T0 needs the server's POST self-test, which runs at the first + // native bind (the GPU context does not exist yet), and is published as kCapAdoptT0 in the + // CapsSnapshot that bind republishes. Absent (an older peer) reads 2. + adoptTier: ubyte = 2; } enum RefuseCode : uint { diff --git a/MobileGL/MG_Remote/Server/AdoptInbox.cpp b/MobileGL/MG_Remote/Server/AdoptInbox.cpp new file mode 100644 index 00000000..cca4f93d --- /dev/null +++ b/MobileGL/MG_Remote/Server/AdoptInbox.cpp @@ -0,0 +1,121 @@ +// MobileGL - MobileGL/MG_Remote/Server/AdoptInbox.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "AdoptInbox.h" + +#include "../Transport/FdPassing.h" + +#include + +#include +#include + +#if !defined(_WIN32) +#include +#endif + +namespace MobileGL::MG_Remote::Server { + + namespace { + void CloseFd(int fd) { +#if !defined(_WIN32) + if (fd >= 0) ::close(fd); +#else + (void)fd; +#endif + } + } // namespace + + AdoptInbox::~AdoptInbox() { Clear(); } + + void AdoptInbox::Clear() { + for (auto& [seq, kept] : m_kept) CloseFd(kept.fd); + m_kept.clear(); + } + + AdoptInbox::Outcome AdoptInbox::Take(Uint64 seq, Uint32 timeoutMs, int* outFd, + Transport::AdoptT0::Offer* outOffer, std::string& why) { + using Clock = std::chrono::steady_clock; + *outFd = -1; + *outOffer = Transport::AdoptT0::Offer{}; + if (auto it = m_kept.find(seq); it != m_kept.end()) { + *outFd = it->second.fd; + *outOffer = it->second.offer; + m_kept.erase(it); + return Outcome::Taken; + } + if (m_transport == nullptr) { + why = "the session has no control transport to receive the store on"; + return Outcome::Closed; + } + const auto deadline = Clock::now() + std::chrono::milliseconds(timeoutMs); + for (;;) { + const auto now = Clock::now(); + const Uint32 left = now >= deadline ? 0u + : static_cast(std::chrono::duration_cast(deadline - now).count()); + int fd = -1; + Uint8 sideband[Transport::FdPassing::kMaxSidebandBytes] = {}; + std::uint64_t sidebandSize = 0; + const MobileGLResult received = m_transport->ReceiveFd( + &fd, MobileGLMutableByteSpan{sideband, sizeof(sideband)}, &sidebandSize, left); + if (received == MOBILEGL_ERR_TIMEOUT) { + why = "no store arrived on the aux socket within " + std::to_string(timeoutMs) + + " ms of its record"; + return Outcome::TimedOut; + } + if (received != MOBILEGL_OK) { + why = "the aux socket closed (rc=" + std::to_string(static_cast(received)) + + ") before the store arrived"; + return Outcome::Closed; + } + Transport::AdoptT0::Offer offer{}; + if (sidebandSize != sizeof(offer)) { + ++m_malformed; + MGLOG_W("MG_Remote server: T0 - an aux descriptor with a %llu-byte sideband is not a store " + "offer; closed", + static_cast(sidebandSize)); + CloseFd(fd); + continue; + } + std::memcpy(&offer, sideband, sizeof(offer)); + if (offer.magic != Transport::AdoptT0::kOfferMagic || + offer.version != Transport::AdoptT0::kOfferVersion) { + ++m_malformed; + MGLOG_W("MG_Remote server: T0 - an aux descriptor whose sideband is not an Offer " + "(magic 0x%08x version %u); closed", + offer.magic, offer.version); + CloseFd(fd); + continue; + } + if (offer.seq == seq) { + *outFd = fd; + *outOffer = offer; + return Outcome::Taken; + } + if (offer.seq < seq) { + // Its record already timed out and was declined; the client has released the + // store on that answer, and this late copy of the handle is only a reference. + ++m_stale; + MGLOG_W("MG_Remote server: T0 - the store for record %llu arrived after its record was " + "answered; closed (stale)", + static_cast(offer.seq)); + CloseFd(fd); + continue; + } + // For a later record: keep it for that record. Replacing a kept one would leak it. + if (auto it = m_kept.find(offer.seq); it != m_kept.end()) { + ++m_malformed; + CloseFd(it->second.fd); + it->second = KeptOffer{fd, offer}; + continue; + } + m_kept.emplace(offer.seq, KeptOffer{fd, offer}); + } + } + +} // namespace MobileGL::MG_Remote::Server diff --git a/MobileGL/MG_Remote/Server/AdoptInbox.h b/MobileGL/MG_Remote/Server/AdoptInbox.h new file mode 100644 index 00000000..ea5ab141 --- /dev/null +++ b/MobileGL/MG_Remote/Server/AdoptInbox.h @@ -0,0 +1,72 @@ +// MobileGL - MobileGL/MG_Remote/Server/AdoptInbox.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// P11 B2 (MG_Remote/CONTRACT-P11.md B2): the server's end of T0's descriptor channel. +// +// WHO READS THE FD, AND WHY IT CANNOT DEADLOCK THE APPLY THREAD. The APPLY THREAD reads it, +// inline, when it decodes the map_persistent record whose seq the Offer names - nothing else reads +// a session's aux socket after the handshake, so there is no second reader to race. The wait +// cannot deadlock because of the client's order: it queues the Offer (ShareFd returns once the +// kernel - or the in-process queue - holds it) BEFORE it publishes the record, so when the apply +// thread sees the record the descriptor is already there, and the wait depends on nothing the +// client has still to do. A client emits at most one T0 map_persistent at a time (the row blocks +// on its reply), so at most one Offer is ever outstanding and the aux queue (10 datagrams on an +// Android kernel) cannot fill. The wait is still bounded (AdoptT0::kOfferWaitMs): an Offer that +// never comes is a named DECLINED, never a hang. +// +// Offers are matched by seq, not by arrival order. One that arrives for a LATER record is kept +// for it; one for an EARLIER record (its record already timed out and was declined) is closed +// and counted as stale. + +#pragma once + +#include + +#include "../Transport/AdoptT0.h" +#include "../Transport/ITransport.h" + +#include +#include + +namespace MobileGL::MG_Remote::Server { + + class AdoptInbox { + public: + enum class Outcome : Uint8 { Taken, TimedOut, Closed }; + + AdoptInbox() = default; + ~AdoptInbox(); + AdoptInbox(const AdoptInbox&) = delete; + AdoptInbox& operator=(const AdoptInbox&) = delete; + + void Attach(Transport::ITransport* transport) { m_transport = transport; } + + // The Offer for record `seq` and its hop descriptor (owned by the caller on Taken). Waits + // at most `timeoutMs` for it; `why` names what went wrong otherwise. + Outcome Take(Uint64 seq, Uint32 timeoutMs, int* outFd, Transport::AdoptT0::Offer* outOffer, + std::string& why); + + // Closes every kept descriptor (session teardown). + void Clear(); + + Uint64 StaleOffers() const { return m_stale; } + Uint64 MalformedOffers() const { return m_malformed; } + SizeT Kept() const { return m_kept.size(); } + + private: + struct KeptOffer { + int fd = -1; + Transport::AdoptT0::Offer offer{}; + }; + Transport::ITransport* m_transport = nullptr; + std::map m_kept; + Uint64 m_stale = 0; + Uint64 m_malformed = 0; + }; + +} // namespace MobileGL::MG_Remote::Server diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 9513c49c..978f76d8 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -137,6 +137,11 @@ namespace MobileGL::MG_Remote::Server { return it->second; } + Bool ServerVerbSink::OnMapPersistent(const MG_Pipe::MGPHandleOnly& handle, Uint64 seq, Int32& status) { + ServerSession* session = ServerSession::Active(); + return session != nullptr && session->AdoptStoreT0(handle, seq, status); + } + Bool ServerVerbSink::OnFenceCreate(const MG_Pipe::MGPHandleOnly& desc) { if (desc.Kind != static_cast(MG_Pipe::MGPipeKind::Fence)) Wire::WireProtocolFatal("Fence.Kind", "expected Fence namespace"); diff --git a/MobileGL/MG_Remote/Server/PipeApplier.h b/MobileGL/MG_Remote/Server/PipeApplier.h index f4e15a13..e799036c 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.h +++ b/MobileGL/MG_Remote/Server/PipeApplier.h @@ -153,6 +153,8 @@ namespace MobileGL::MG_Remote::Server { void SetBackend(MG_Backend::BackendObject* backend); void SetMaxReplyBytes(Uint64 bytes) { m_maxReplyBytes = bytes; } + // P11 B2: T0's map_persistent - ServerSession::AdoptStoreT0 (it owns the aux socket). + Bool OnMapPersistent(const MG_Pipe::MGPHandleOnly& handle, Uint64 seq, Int32& status) override; Bool OnFenceCreate(const MG_Pipe::MGPHandleOnly&) override; Bool OnFenceDestroy(const MG_Pipe::MGPHandleOnly&) override; Bool OnFenceStatus(const MG_Pipe::MGPHandleOnly&, Uint32&) override; diff --git a/MobileGL/MG_Remote/Server/ServerLoop.cpp b/MobileGL/MG_Remote/Server/ServerLoop.cpp index a1f499e1..fab60fb4 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.cpp +++ b/MobileGL/MG_Remote/Server/ServerLoop.cpp @@ -1409,6 +1409,11 @@ namespace MobileGL::MG_Remote::Server { // bind: the ID-67 control's different tuple reads 1 republish where 2 are required. ServerSession* session = ServerSession::Active(); if (session != nullptr && session->Accepted()) { + // P11 B2: THE T0 GRANT, HERE AND NOWHERE EARLIER - the first moment this session has + // a live context (Magma: a device) to run the POST self-test on, and before the + // snapshot below, which is the one the client's make-current adopts. Once per + // session; a session that did not ask T0 does nothing. + session->SettleAdoptT0AtBind(); const MobileGLResult published = session->PublishCapsSnapshot(); if (published == MOBILEGL_OK) NoteMakeCurrentRepublished(); if (published != MOBILEGL_OK) { @@ -1483,6 +1488,7 @@ namespace MobileGL::MG_Remote::Server { if (frame.ok) { ServerSession* session = ServerSession::Active(); if (session != nullptr && session->Accepted()) { + session->SettleAdoptT0AtBind(); // P11 B2: idempotent; the bind has usually settled it (void)session->PublishCapsSnapshot(); } } diff --git a/MobileGL/MG_Remote/Server/ServerSession.cpp b/MobileGL/MG_Remote/Server/ServerSession.cpp index 041508c9..2f3d579b 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.cpp +++ b/MobileGL/MG_Remote/Server/ServerSession.cpp @@ -16,15 +16,18 @@ #include "../Handshake.h" #include "../Transport/LinkMetrics.h" #include "../Transport/AdoptTier.h" +#include "../Transport/AdoptT0.h" #include "../Protocol/generated/protocol_generated.h" #include "../Transport/InProcessTransport.h" #include #include #include +#include #include #include +#include #include #include #include @@ -683,6 +686,18 @@ namespace MobileGL::MG_Remote::Server { // for, before a byte of Welcome. A stream refuses T0/T1 by name and serves T2; over // shared segments the client settles it (Transport/AdoptTier.h). 0/1 never die here. (void)Transport::SettleAdoptTierAtHandshake(stream, Transport::AdoptTierSide::Server); + // ---- P11 B2: the client's ask (LinkTerms.adoptTier). 2 or, over shared segments, 0; + // anything else is not an ask this revision knows. Recorded, echoed in the Welcome, and + // granted or refused by name at the first native bind (SettleAdoptT0AtBind). + if (terms->adoptTier() != 2 && !(terms->adoptTier() == 0 && !stream)) + return RefuseHandshake(transport, ::MobileGL::Wire::RefuseCode::LinkTerms, + "adoptTier is neither 2 nor, over shared segments, 0", 2, terms->adoptTier()); + m_adoptT0Asked = terms->adoptTier() == 0; + m_adoptT0Settled = false; + m_adoptT0 = false; + m_t0Stores = m_t0Bytes = m_t0Declined = 0; + m_adoptInbox.Clear(); + m_adoptInbox.Attach(&transport); // ---- 3. the four segments, both control pages, the rings. MobileGLResult created = MOBILEGL_ERR_UNSUPPORTED; @@ -778,7 +793,8 @@ namespace MobileGL::MG_Remote::Server { static_cast(maxReply)); } auto negotiated = ::MobileGL::Wire::CreateLinkTerms(builder, terms->dataPlane(), - ::MobileGL::Wire::WireForm::StructImage, maxReply, cmdWindow, stageWindow, eventWindow); + ::MobileGL::Wire::WireForm::StructImage, maxReply, cmdWindow, stageWindow, eventWindow, + static_cast(m_adoptT0Asked ? 0 : 2)); auto welcome = ::MobileGL::Wire::CreateWelcome( builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR, // CONTRACT-P6 4.3: THE SERVER'S OWN PID, not an echo of the client's. @@ -898,6 +914,9 @@ namespace MobileGL::MG_Remote::Server { // R-8/C-4: bits 32..47 of CallMask are the CONSUMER MASK, and this is the only place // they are produced. Uint64 callMask = CallMask(); + // P11 B2: this session's T0 grant, never part of the process-wide m_capBits - it is a + // statement about THIS client's ask and this server's self-test. + if (m_adoptT0) callMask |= static_cast(MG_Pipe::kCapAdoptT0); const auto timerSupported = m_backend->GetBackendFunctions().GL.IsTimerQuerySupported; if ((callMask & MG_Pipe::kCapTimerQuery) && (!timerSupported || !timerSupported())) callMask &= ~static_cast(MG_Pipe::kCapTimerQuery); @@ -1008,6 +1027,20 @@ namespace MobileGL::MG_Remote::Server { MG_Pipe::gMGPipeEventRingCapacityBytes = nullptr; } } + // P11 B2: the T0 arm's proof for this session, then the inbox's leftovers (an Offer whose + // record never came; its client has released the store on the session's end). + if (m_accepted && m_adoptT0Asked) { + MGLOG_I("MG_Remote server: T0 session totals - %s; stores imported %llu, bytes %llu, declined %llu, " + "stale offers %llu, malformed %llu, unclaimed %zu", + m_adoptT0 ? "ran T0" : m_adoptT0Settled ? "not granted (T2)" : "no native bind (T2)", + static_cast(m_t0Stores), static_cast(m_t0Bytes), + static_cast(m_t0Declined), + static_cast(m_adoptInbox.StaleOffers()), + static_cast(m_adoptInbox.MalformedOffers()), m_adoptInbox.Kept()); + } + m_adoptInbox.Clear(); + m_adoptInbox.Attach(nullptr); + m_adoptT0Asked = m_adoptT0Settled = m_adoptT0 = false; m_consumer.Detach(); *m_commands = Transport::RingConsumer(); *m_events = Transport::EventRingProducer(); @@ -1023,6 +1056,126 @@ namespace MobileGL::MG_Remote::Server { std::memory_order_release, std::memory_order_relaxed); } + // ---- P11 B2: T0 (MG_Remote/CONTRACT-P11.md B2) ------------------------------------------ + + namespace { + // THE POST VERDICT IS THE PROCESS'S, like DriverPost's: the driver and the device do not + // change between two sessions of one server process, and the test allocates, imports and + // waits for the GPU - not a per-session cost. Apply thread only. + struct T0PostVerdict { + Bool ran = false; + Bool passed = false; + std::string detail; + }; + T0PostVerdict& T0Post() { + static T0PostVerdict verdict; + return verdict; + } + } // namespace + + void ServerSession::SettleAdoptT0AtBind() { + if (!m_accepted || !m_adoptT0Asked || m_adoptT0Settled) return; + m_adoptT0Settled = true; + const char* backendName = m_backend == nullptr ? "server" + : m_backend->GetBackendType() == BackendType::DirectVulkan ? "Magma" : "Espryt"; + std::string why; + if (MG_Config::Ipc.AllowAdoptT0 == 0) { + why = "this server's MOBILEGL_IPC_ALLOW_ADOPT_T0=0 disallows it"; + } else if (!Transport::AdoptT0::PlatformHasAhb()) { + why = "no AHardwareBuffer on this platform"; + } else { + const MG_Pipe::MGPipeResourceOps* ops = MG_Pipe::MGPipeGetResourceOps(); + if (ops == nullptr || ops->ImportExternal == nullptr || ops->SelfTestExternal == nullptr) { + why = std::string("the ") + backendName + " backend registers no T0 import"; + } else { + T0PostVerdict& post = T0Post(); + if (!post.ran) { + char detail[512] = {}; + post.passed = ops->SelfTestExternal(detail, sizeof(detail)); + post.detail = detail; + post.ran = true; + MGLOG_I("MG_Remote server: T0 POST (%s, sustained-lock pattern) %s - %s", backendName, + post.passed ? "PASSED" : "FAILED", post.detail.c_str()); + } + if (!post.passed) why = std::string("the ") + backendName + " POST self-test failed: " + post.detail; + } + } + if (!why.empty()) { + MGLOG_W("MG_Remote server: Refuse{AdoptT0Unavailable, \"%s\"} - the client asked T0 " + "(MOBILEGL_IPC_ADOPT_TIER=0) and %s; kCapAdoptT0 is not published and the session " + "runs T2 (emulate)", + MG_Config::Ipc.AllowAdoptT0 == 0 ? "disallowed" + : !Transport::AdoptT0::PlatformHasAhb() ? "no AHardwareBuffer" : "POST", + why.c_str()); + return; + } + m_adoptT0 = true; + MGLOG_I("MG_Remote server: T0 granted - the client asked T0 and the %s POST self-test passed; " + "kCapAdoptT0 is published and each map_persistent imports the client's AHardwareBuffer", + backendName); + } + + Bool ServerSession::AdoptStoreT0(const MG_Pipe::MGPHandleOnly& handle, Uint64 seq, Int32& status) { + if (!m_adoptT0) return false; + status = Wire::ReplySink::kStatusDeclined; + int hop = -1; + Transport::AdoptT0::Offer offer{}; + std::string why; + const AdoptInbox::Outcome outcome = + m_adoptInbox.Take(seq, Transport::AdoptT0::kOfferWaitMs, &hop, &offer, why); + const auto closeHop = [&] { +#if !defined(_WIN32) + if (hop >= 0) ::close(hop); +#endif + hop = -1; + }; + if (outcome != AdoptInbox::Outcome::Taken) { + ++m_t0Declined; + MGLOG_E("MG_Remote server: Refuse{AdoptT0NoStore, \"record %llu\"} - the T0 map_persistent for " + "{slot=%u, gen=%u} is DECLINED: %s; the store runs T2", + static_cast(seq), handle.Handle.Slot, handle.Handle.Gen, why.c_str()); + return true; + } + if ((offer.flags & Transport::AdoptT0::kOfferHasBuffer) == 0) { + closeHop(); + ++m_t0Declined; + MGLOG_W("MG_Remote server: T0 map_persistent {slot=%u, gen=%u} (record %llu) DECLINED - the " + "client could not allocate its AHardwareBuffer (its log names why); the store runs T2", + handle.Handle.Slot, handle.Handle.Gen, static_cast(seq)); + return true; + } + void* ahb = nullptr; + const Bool received = Transport::AdoptT0::ReceiveFromHop(hop, offer.size, &ahb, why); + closeHop(); + if (!received) { + ++m_t0Declined; + MGLOG_E("MG_Remote server: T0 map_persistent {slot=%u, gen=%u} (record %llu) DECLINED - the " + "AHardwareBuffer could not be read off its hop: %s", + handle.Handle.Slot, handle.Handle.Gen, static_cast(seq), why.c_str()); + return true; + } + // The backend takes its own reference on success; this one is the hop's, released here. + const Bool imported = MG_Pipe::MGPipeApplyAdoptExternal(handle, ahb, offer.size); + Transport::AdoptT0::ReleaseImported(ahb); + if (!imported) { + ++m_t0Declined; + MGLOG_W("MG_Remote server: T0 map_persistent {slot=%u, gen=%u} (record %llu, %llu bytes) DECLINED - " + "the backend did not import the AHardwareBuffer (its log names why); the store runs T2", + handle.Handle.Slot, handle.Handle.Gen, static_cast(seq), + static_cast(offer.size)); + return true; + } + ++m_t0Stores; + m_t0Bytes += offer.size; + status = Wire::ReplySink::kStatusOk; + MGLOG_I("MG_Remote server: T0 imported store {slot=%u, gen=%u} - %llu bytes (record %llu); session " + "total %llu stores / %llu bytes", + handle.Handle.Slot, handle.Handle.Gen, static_cast(offer.size), + static_cast(seq), static_cast(m_t0Stores), + static_cast(m_t0Bytes)); + return true; + } + Transport::RingConsumer& ServerSession::CommandRing() { return *m_commands; } Transport::RingControl& ServerSession::Control() { diff --git a/MobileGL/MG_Remote/Server/ServerSession.h b/MobileGL/MG_Remote/Server/ServerSession.h index be03bfca..d04c95a9 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.h +++ b/MobileGL/MG_Remote/Server/ServerSession.h @@ -57,6 +57,7 @@ #include "../Transport/RoleMemory.h" #include "../Transport/SessionRings.h" #include "../Wire/PipeWireCodec.h" +#include "AdoptInbox.h" #include "ApplyThreadPolicy.h" #include "PipeApplier.h" @@ -290,6 +291,32 @@ namespace MobileGL::MG_Remote::Server { Transport::RoleMemorySample SampleMemory() const; void LogMemory(const char* phase) const; + // ---- P11 B2: T0 (MG_Remote/CONTRACT-P11.md B2) -------------------------------------- + // + // The client's Hello asked T0 (LinkTerms.adoptTier = 0 over shared segments). + Bool AdoptT0Asked() const { return m_adoptT0Asked; } + // This session runs T0 (asked, and granted at the first native bind). + Bool AdoptT0() const { return m_adoptT0; } + // THE GRANT, once per session, on the apply thread right after the first native bind and + // BEFORE the caps that bind republishes: asked, the allow switch + // (MOBILEGL_IPC_ALLOW_ADOPT_T0, default 1), AHardwareBuffer on this platform, a backend that + // registers ImportExternal + SelfTestExternal, and that backend's POST self-test of the + // sustained-lock pattern (run once per process on its live context, the verdict cached). + // Granted sets kCapAdoptT0 in every later CapsSnapshot; not granted is one named line and + // T2. A session that did not ask does nothing here (knob 2 is today's session byte for byte). + void SettleAdoptT0AtBind(); + // T0's map_persistent, from the codec's arm through the verb sink, on the apply thread: + // take the Offer for record `seq` from the aux socket (Server/AdoptInbox.h - bounded, never a + // hang), read the AHB off its hop, and import it as `handle`'s store. False = this session is + // not T0 (the codec's T2 decline answers); true = handled and `status` is the answer (OK, or + // DECLINED with a named line). + Bool AdoptStoreT0(const MG_Pipe::MGPHandleOnly& handle, Uint64 seq, Int32& status); + // The arm's proof for this session (B2): stores imported and their bytes, declined. + Uint64 T0StoresImported() const { return m_t0Stores; } + Uint64 T0BytesImported() const { return m_t0Bytes; } + Uint64 T0Declined() const { return m_t0Declined; } + AdoptInbox& T0Inbox() { return m_adoptInbox; } + private: MobileGLResult BindDataConnection(Transport::ITransport& control, Transport::ILink& link, const Uint8* nonce); @@ -325,6 +352,15 @@ namespace MobileGL::MG_Remote::Server { // the apply thread's event wait. std::atomic m_applyStopRequested{false}; std::atomic m_controlStreamEnded{false}; + // P11 B2 (T0). Set by Accept from the Hello, by SettleAdoptT0AtBind on the apply thread; + // the inbox and the counters are the apply thread's. + Bool m_adoptT0Asked = false; + Bool m_adoptT0Settled = false; + Bool m_adoptT0 = false; + AdoptInbox m_adoptInbox; + Uint64 m_t0Stores = 0; + Uint64 m_t0Bytes = 0; + Uint64 m_t0Declined = 0; }; // Leak-at-exit like every other MG_Remote singleton (ID-8): no frontend destructor may diff --git a/MobileGL/MG_Remote/Server/ServerSpawn.cpp b/MobileGL/MG_Remote/Server/ServerSpawn.cpp index ac13f9ef..1212b302 100644 --- a/MobileGL/MG_Remote/Server/ServerSpawn.cpp +++ b/MobileGL/MG_Remote/Server/ServerSpawn.cpp @@ -101,10 +101,14 @@ namespace MobileGL::MG_Remote::Server { // PH-6 (ID-P7-2) adds MOBILEGL_IPC_EVENT_WAIT_MS on the same argument: it is the server's // patience with a client that has stopped draining SEG_EVENT, nothing on the client reads // it, and scrubbing it would make a spawned server wait the default whatever the lane set. + // + // P11 B2 adds MOBILEGL_IPC_ALLOW_ADOPT_T0, the server's own T0 allow switch: only the server + // reads it, and scrubbing it would make "allow switch off" unreachable on the spawn arm. + // MOBILEGL_IPC_ADOPT_TIER itself still goes - the client's ask travels in its Hello. bool ShouldScrub(const char* entry) { static constexpr const char* kServerOwned[] = { "MOBILEGL_IPC_WIRE_DEFERRED_MB=", "MOBILEGL_IPC_SPIN_US=", "MOBILEGL_IPC_SERVER_AFFINITY=", - "MOBILEGL_IPC_AUDIT=", "MOBILEGL_IPC_EVENT_WAIT_MS=", + "MOBILEGL_IPC_AUDIT=", "MOBILEGL_IPC_EVENT_WAIT_MS=", "MOBILEGL_IPC_ALLOW_ADOPT_T0=", }; for (const char* kept : kServerOwned) { if (std::strncmp(entry, kept, std::strlen(kept)) == 0) { diff --git a/MobileGL/MG_Remote/Transport/AdoptT0.cpp b/MobileGL/MG_Remote/Transport/AdoptT0.cpp new file mode 100644 index 00000000..c3b43c17 --- /dev/null +++ b/MobileGL/MG_Remote/Transport/AdoptT0.cpp @@ -0,0 +1,225 @@ +// MobileGL - MobileGL/MG_Remote/Transport/AdoptT0.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "AdoptT0.h" + +#include +#include +#include +#include + +#if defined(__ANDROID__) +#include +#endif +#if !defined(_WIN32) +#include +#include +#endif + +namespace MobileGL::MG_Remote::Transport::AdoptT0 { + + namespace { + std::atomic g_testPlatform{nullptr}; + + std::string Format(const char* fmt, long long a, long long b = 0) { + char buffer[256]; + std::snprintf(buffer, sizeof(buffer), fmt, a, b); + return buffer; + } + +#if defined(__ANDROID__) + + bool RealHasAhb() { return true; } + + bool RealAllocateHeld(std::uint64_t size, const void* seed, HeldStore& out, std::string& why) { + out = HeldStore{}; + if (size == 0 || size > 0xFFFFFFFFull) { + why = Format("a BLOB AHardwareBuffer is at most 4 GiB wide and this store is %lld bytes", + static_cast(size)); + return false; + } + AHardwareBuffer_Desc desc{}; + desc.width = static_cast(size); + desc.height = 1; + desc.layers = 1; + desc.format = AHARDWAREBUFFER_FORMAT_BLOB; + desc.usage = AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN | + AHARDWAREBUFFER_USAGE_GPU_DATA_BUFFER; + AHardwareBuffer* ahb = nullptr; + const int allocated = AHardwareBuffer_allocate(&desc, &ahb); + if (allocated != 0 || ahb == nullptr) { + why = Format("AHardwareBuffer_allocate(%lld bytes) rc=%lld", static_cast(size), allocated); + return false; + } + void* ptr = nullptr; + const int locked = AHardwareBuffer_lock( + ahb, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN, -1, nullptr, &ptr); + if (locked != 0 || ptr == nullptr) { + why = Format("AHardwareBuffer_lock rc=%lld", locked); + AHardwareBuffer_release(ahb); + return false; + } + if (seed != nullptr) { + std::memcpy(ptr, seed, static_cast(size)); + } else { + std::memset(ptr, 0, static_cast(size)); + } + out.ahb = ahb; + out.ptr = ptr; + out.size = size; + return true; + } + + int RealMakeHop(const HeldStore* store, std::string& why) { + int pair[2] = {-1, -1}; + if (::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair) != 0) { + why = Format("socketpair errno=%lld", errno); + return -1; + } + if (store != nullptr && store->ahb != nullptr) { + const int sent = AHardwareBuffer_sendHandleToUnixSocket( + static_cast(store->ahb), pair[0]); + if (sent != 0) { + why = Format("AHardwareBuffer_sendHandleToUnixSocket rc=%lld errno=%lld", sent, errno); + ::close(pair[0]); + ::close(pair[1]); + return -1; + } + } + // The written message (and the descriptors it carries) stays queued on pair[1]. + ::close(pair[0]); + return pair[1]; + } + + void RealReleaseHeld(HeldStore& store) { + if (store.ahb != nullptr) { + auto* ahb = static_cast(store.ahb); + if (store.ptr != nullptr) (void)AHardwareBuffer_unlock(ahb, nullptr); + AHardwareBuffer_release(ahb); + } + store = HeldStore{}; + } + + bool RealReceiveFromHop(int hopFd, std::uint64_t size, void** outAhb, std::string& why) { + *outAhb = nullptr; + AHardwareBuffer* ahb = nullptr; + const int received = AHardwareBuffer_recvHandleFromUnixSocket(hopFd, &ahb); + if (received != 0 || ahb == nullptr) { + why = Format("AHardwareBuffer_recvHandleFromUnixSocket rc=%lld errno=%lld", received, errno); + return false; + } + AHardwareBuffer_Desc desc{}; + AHardwareBuffer_describe(ahb, &desc); + if (desc.format != AHARDWAREBUFFER_FORMAT_BLOB || desc.width != size || desc.height != 1) { + why = Format("the handle describes as format 0x%llx width %lld, not the BLOB its record's store needs", + static_cast(desc.format), static_cast(desc.width)); + AHardwareBuffer_release(ahb); + return false; + } + *outAhb = ahb; + return true; + } + + void RealAcquireImported(void* ahb) { + if (ahb != nullptr) AHardwareBuffer_acquire(static_cast(ahb)); + } + + void RealReleaseImported(void* ahb) { + if (ahb != nullptr) AHardwareBuffer_release(static_cast(ahb)); + } + +#else + + // No AHardwareBuffer: the server never grants T0 here, so a client never allocates. The + // empty hop still works, so a flags-0 Offer is real on every POSIX host. + bool RealHasAhb() { return false; } + + bool RealAllocateHeld(std::uint64_t, const void*, HeldStore& out, std::string& why) { + out = HeldStore{}; + why = "no AHardwareBuffer on this platform"; + return false; + } + + int RealMakeHop(const HeldStore* store, std::string& why) { +#if defined(_WIN32) + (void)store; + why = "no AF_UNIX socketpair on this platform"; + return -1; +#else + if (store != nullptr && store->ahb != nullptr) { + why = "no AHardwareBuffer on this platform"; + return -1; + } + int pair[2] = {-1, -1}; + if (::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair) != 0) { + why = Format("socketpair errno=%lld", errno); + return -1; + } + ::close(pair[0]); + return pair[1]; +#endif + } + + void RealReleaseHeld(HeldStore& store) { store = HeldStore{}; } + + bool RealReceiveFromHop(int, std::uint64_t, void** outAhb, std::string& why) { + *outAhb = nullptr; + why = "no AHardwareBuffer on this platform"; + return false; + } + + void RealAcquireImported(void*) {} + void RealReleaseImported(void*) {} + +#endif + } // namespace + + bool PlatformHasAhb() { + const PlatformForTest* test = g_testPlatform.load(std::memory_order_acquire); + return test != nullptr ? test->HasAhb() : RealHasAhb(); + } + + bool AllocateHeld(std::uint64_t size, const void* seed, HeldStore& out, std::string& why) { + const PlatformForTest* test = g_testPlatform.load(std::memory_order_acquire); + return test != nullptr ? test->AllocateHeld(size, seed, out, why) : RealAllocateHeld(size, seed, out, why); + } + + int MakeHop(const HeldStore* store, std::string& why) { + const PlatformForTest* test = g_testPlatform.load(std::memory_order_acquire); + return test != nullptr ? test->MakeHop(store, why) : RealMakeHop(store, why); + } + + void ReleaseHeld(HeldStore& store) { + const PlatformForTest* test = g_testPlatform.load(std::memory_order_acquire); + if (test != nullptr) test->ReleaseHeld(store); + else RealReleaseHeld(store); + } + + bool ReceiveFromHop(int hopFd, std::uint64_t size, void** outAhb, std::string& why) { + const PlatformForTest* test = g_testPlatform.load(std::memory_order_acquire); + return test != nullptr ? test->ReceiveFromHop(hopFd, size, outAhb, why) + : RealReceiveFromHop(hopFd, size, outAhb, why); + } + + void AcquireImported(void* ahb) { + const PlatformForTest* test = g_testPlatform.load(std::memory_order_acquire); + if (test != nullptr) test->AcquireImported(ahb); + else RealAcquireImported(ahb); + } + + void ReleaseImported(void* ahb) { + const PlatformForTest* test = g_testPlatform.load(std::memory_order_acquire); + if (test != nullptr) test->ReleaseImported(ahb); + else RealReleaseImported(ahb); + } + + void SetPlatformForTest(const PlatformForTest* platform) { + g_testPlatform.store(platform, std::memory_order_release); + } + +} // namespace MobileGL::MG_Remote::Transport::AdoptT0 diff --git a/MobileGL/MG_Remote/Transport/AdoptT0.h b/MobileGL/MG_Remote/Transport/AdoptT0.h new file mode 100644 index 00000000..326bb7e8 --- /dev/null +++ b/MobileGL/MG_Remote/Transport/AdoptT0.h @@ -0,0 +1,110 @@ +// MobileGL - MobileGL/MG_Remote/Transport/AdoptT0.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// P11 B2 (MG_Remote/CONTRACT-P11.md B2): T0, the zero-copy adoption of a persistently mapped +// store. The CLIENT allocates the store as an AHardwareBuffer (AHB), keeps a CPU lock on it for +// the store's life and uses that pointer as the adopted mapping; the SERVER imports the same +// pages as the store's resident twin (Espryt glBufferStorageExternalEXT, Magma +// VK_ANDROID_external_memory_android_hardware_buffer). +// +// HOW THE HANDLE CROSSES, AND WHY IT IS ORDERED WITH THE RECORDS THAT USE THE STORE. The adoption +// rides the data-ring map_persistent record (not a control op, which the apply thread could +// overtake under run-ahead). Its AHB travels over the session's aux socket as ONE descriptor: a +// fresh socketpair end (the "hop") on which AHardwareBuffer_sendHandleToUnixSocket already wrote +// the handle, with an Offer in the sideband naming the record's seq. The hop keeps the aux channel +// uniform (one fd + sideband per message, FdPassing's shape on every route) and the AHB's own +// message format off it. Step 1 of B2 proved both this and the direct form on the Redmi across +// every route (shell, another app, same app, spawned child; 64 KiB to 128 MiB). +// +// Every T0 map_persistent is preceded by exactly one Offer for its seq - with the handle, or with +// flags 0 when this client could not allocate one - so the server always has something to wait +// for and never waits for nothing. + +#pragma once + +#include +#include + +namespace MobileGL::MG_Remote::Transport::AdoptT0 { + + // The aux sideband that travels with the hop descriptor. + struct Offer { + std::uint32_t magic = 0; + std::uint32_t version = 0; + std::uint64_t seq = 0; // the map_persistent record's seq (its reply slot id) + std::uint64_t size = 0; // the store's bytes (the AHB's width) + std::uint32_t flags = 0; + std::uint32_t reserved = 0; + }; + static_assert(sizeof(Offer) == 32, "the Offer is a fixed 32-byte sideband"); + inline constexpr std::uint32_t kOfferMagic = 0x3054474Du; // 'MGT0' on the wire + inline constexpr std::uint32_t kOfferVersion = 1; + // The hop carries an AHB. Clear = the client could not allocate one for this store: the + // server answers DECLINED at once and the store runs T2. + inline constexpr std::uint32_t kOfferHasBuffer = 1u; + + // How long the apply thread waits for an Offer whose record it has decoded. The client queues + // the Offer BEFORE it publishes the record, so the wait normally ends at once; the bound is + // what turns a lost Offer into a named DECLINED instead of a hang. + inline constexpr std::uint32_t kOfferWaitMs = 2000; + + // True where AHardwareBuffer exists (Android). Everywhere else T0 is never granted. + bool PlatformHasAhb(); + + // ---- client half ---------------------------------------------------------------------- + + // A store the client allocated. `ahb` is an AHardwareBuffer*, `ptr` its CPU lock, held until + // ReleaseHeld. + struct HeldStore { + void* ahb = nullptr; + void* ptr = nullptr; + std::uint64_t size = 0; + }; + + // Allocates a BLOB AHB of `size` bytes (CPU_READ_OFTEN | CPU_WRITE_OFTEN | GPU_DATA_BUFFER), + // locks it for reading and writing - the lock is held for the store's life - and copies + // `seed` (size bytes, may be null = zeros) into it. False with the reason in `why`. + bool AllocateHeld(std::uint64_t size, const void* seed, HeldStore& out, std::string& why); + + // A hop descriptor for `store`: a socketpair whose first end had the handle written on it + // (AHardwareBuffer_sendHandleToUnixSocket) and was closed. Returns the other end, which the + // caller shares over aux and closes. `store == nullptr` makes an empty hop (the flags-0 + // Offer's descriptor). -1 with `why` on failure. + int MakeHop(const HeldStore* store, std::string& why); + + // Unlocks and releases the client's reference. The server's import holds its own. + void ReleaseHeld(HeldStore& store); + + // ---- server half ---------------------------------------------------------------------- + + // Reads the handle a client wrote on `hopFd` (AHardwareBuffer_recvHandleFromUnixSocket) and + // checks it is a BLOB of `size` bytes. On success *outAhb holds a reference the caller + // releases with ReleaseImported. The caller closes hopFd. + bool ReceiveFromHop(int hopFd, std::uint64_t size, void** outAhb, std::string& why); + + void AcquireImported(void* ahb); + void ReleaseImported(void* ahb); + + // ---- test seam ------------------------------------------------------------------------ + + // A host has no AHardwareBuffer, so the host tests of T0's PROTOCOL (the Offer, its order + // against the record, the inbox, the import call and the answers) run on a stand-in: every + // function above dispatches to it while one is installed. Null restores the platform's own. + // The GPU half of T0 is the device's to prove (B2's device gates). + struct PlatformForTest { + bool (*HasAhb)(); + bool (*AllocateHeld)(std::uint64_t size, const void* seed, HeldStore& out, std::string& why); + int (*MakeHop)(const HeldStore* store, std::string& why); + void (*ReleaseHeld)(HeldStore& store); + bool (*ReceiveFromHop)(int hopFd, std::uint64_t size, void** outAhb, std::string& why); + void (*AcquireImported)(void* ahb); + void (*ReleaseImported)(void* ahb); + }; + void SetPlatformForTest(const PlatformForTest* platform); + +} // namespace MobileGL::MG_Remote::Transport::AdoptT0 diff --git a/MobileGL/MG_Remote/Transport/AdoptTier.cpp b/MobileGL/MG_Remote/Transport/AdoptTier.cpp index e2dfcea5..e135e037 100644 --- a/MobileGL/MG_Remote/Transport/AdoptTier.cpp +++ b/MobileGL/MG_Remote/Transport/AdoptTier.cpp @@ -22,45 +22,49 @@ namespace MobileGL::MG_Remote::Transport { const char* SideName(AdoptTierSide side) { return side == AdoptTierSide::Client ? "client" : "server"; } } // namespace + std::uint32_t AdoptTierAskFor(bool streamDataPlane) { + return (MG_Config::Ipc.AdoptTier == 0 && !streamDataPlane) ? 0u : 2u; + } + std::uint32_t SettleAdoptTierAtHandshake(bool streamDataPlane, AdoptTierSide side) { const std::uint32_t knob = MG_Config::Ipc.AdoptTier; if (knob > 2) { // Unreachable through ConfigLoader (it admits 0..2); a direct assignment is the only - // way here, and "P11 implements it" of a 7 would be a lie. + // way here, and "T" of a 7 would be a lie. SessionFail(MGFatalFamily::UnimplementedAdoptTier, "MGPipe: Fatal{UnimplementedAdoptTier, \"%u\"} - MOBILEGL_IPC_ADOPT_TIER=%u is not an " - "adoption tier; the values are 0 and 1 (not implemented) and 2 (emulate, the default)", + "adoption tier; the values are 0 (T0), 1 (closed) and 2 (emulate, the default)", static_cast(knob), static_cast(knob)); } - if (knob == 2) { - g_settled.store(true, std::memory_order_release); - return 2; - } + g_settled.store(true, std::memory_order_release); + if (knob == 2) return 2; if (streamDataPlane) { - // ONE LINE PER SESSION, on the side that holds the knob. The tcp lane's server is a - // shared fixture that never sees the client's environment, so each side says it for - // itself; neither side dies, because T2 is the only tier a stream can carry. + // A1, UNCHANGED: ONE LINE PER SESSION, on the side that holds the knob. The tcp lane's + // server is a shared fixture that never sees the client's environment, so each side + // says it for itself; neither side dies, because T2 is the only tier a stream carries. MGLOG_W("MG_Remote %s: Refuse{AdoptTierOnStream, \"T%u\"} - MOBILEGL_IPC_ADOPT_TIER=%u names a " "shared-mapping adoption tier and this session's data plane is a stream (tcp): the two " "sides share no memory to map, so the session runs T2 (emulate: the client keeps the " "shadow and pushes)", SideName(side), static_cast(knob), static_cast(knob)); - g_settled.store(true, std::memory_order_release); return 2; } if (side == AdoptTierSide::Server) { - MGLOG_W("MG_Remote server: MOBILEGL_IPC_ADOPT_TIER=%u is the client's to settle on a shared-segment " - "link (a client naming T0/T1 dies by name at its own handshake); this server serves T2", - static_cast(knob)); - g_settled.store(true, std::memory_order_release); + // B2: over shared segments the CLIENT'S ask (its Hello's LinkTerms.adoptTier) decides, + // and the server grants T0 at its first native bind or names why not + // (ServerSession::SettleAdoptT0AtBind). Its own knob says nothing about this session. return 2; } - SessionFail(MGFatalFamily::UnimplementedAdoptTier, - "MGPipe: Fatal{UnimplementedAdoptTier, \"T%u\"} - MOBILEGL_IPC_ADOPT_TIER=%u names a " - "shared-mapping adoption tier no package implements yet (T0 is P11 package B's, T1 is " - "closed); refused at the handshake over shared segments, before any record is emitted. " - "Unset it or set 2 (emulate)", - static_cast(knob), static_cast(knob)); + if (knob == 1) { + MGLOG_W("MG_Remote client: Refuse{AdoptTierClosed, \"T1\"} - MOBILEGL_IPC_ADOPT_TIER=1 names T1 " + "(the server exports an opaque fd), which P11 closed (ruling ID-P11-1: only Adreno Vulkan " + "could run it); the session runs T2 (emulate). Set 0 for T0 or 2 for T2"); + return 2; + } + // knob 0 over shared segments: asked in the Hello. Nothing is said yet - the answer is the + // server's, at its first native bind, and a T0 this session cannot use is named once, at + // the first map_persistent (ClientSession::AdoptT0). + return 0; } bool AdoptTierSettledAtHandshake() { return g_settled.load(std::memory_order_acquire); } diff --git a/MobileGL/MG_Remote/Transport/AdoptTier.h b/MobileGL/MG_Remote/Transport/AdoptTier.h index 9bf6c1bd..d497d628 100644 --- a/MobileGL/MG_Remote/Transport/AdoptTier.h +++ b/MobileGL/MG_Remote/Transport/AdoptTier.h @@ -6,29 +6,31 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// P11 A1 (MG_Remote/CONTRACT-P11.md §1): THE PERSISTENT-MAP ADOPTION TIER IS SETTLED AT THE -// HANDSHAKE, BY THE SIDE THAT KNOWS THE DATA PLANE, BEFORE THE FIRST map_persistent. +// P11 A1 + B2 (MG_Remote/CONTRACT-P11.md §1 and B2): THE PERSISTENT-MAP ADOPTION TIER IS ASKED +// AT THE HANDSHAKE, BY THE SIDE THAT KNOWS THE DATA PLANE, BEFORE THE FIRST map_persistent. // // Tier names follow docs/Disaggregated/design/07: T0 = the client allocates an AHardwareBuffer -// and the server imports it; T1 = the server exports an opaque fd; T2 = the resource owner -// declines and the client keeps the shadow and pushes. Only T2 is implemented (T0 is P11 -// package B's, T1 is closed), so MOBILEGL_IPC_ADOPT_TIER=0/1 is either refused by name or -// fatal - never a silent T2, and never discovered at the first map_persistent: +// and the server imports it; T1 = the server exports an opaque fd (CLOSED, ID-P11-1); T2 = the +// resource owner declines and the client keeps the shadow and pushes. // -// data plane side knob 0 / 1 -// Stream (tcp) either one line `Refuse{AdoptTierOnStream, "T"}`, then T2. Never Fatal: -// a stream has no memory the two sides share, so no tier but T2 -// can exist on it. -// SharedSegments client Fatal{UnimplementedAdoptTier, "T"} at the handshake, before any -// record is emitted. -// SharedSegments server one warning that the client settles the tier on this plane, then -// T2. (inproc and fork share the client's environment, so the client -// dies by name right after; a unix-socket server launched on its own -// serves the T2 its client settled.) +// MOBILEGL_IPC_ADOPT_TIER IS THE SWITCH (ruling ID-P11-14): 2, the default, is today's T2 byte +// for byte; 0 asks for T0; 1 is refused by name and runs T2. T0 that this session cannot use is +// never a Fatal - it is T2 plus one named line: +// +// data plane side knob 0 knob 1 +// Stream (tcp) either `Refuse{AdoptTierOnStream, "T0"}`, T2 `Refuse{AdoptTierOnStream, "T1"}`, T2 +// (A1, unchanged: a stream shares no memory, so no tier but T2 exists) +// SharedSegments client asks T0 in its Hello (LinkTerms. `Refuse{AdoptTierClosed, "T1"}`, asks T2 +// adoptTier = 0); whether it runs T0 is +// read at the first map_persistent from +// kCapAdoptT0 (ClientSession::AdoptT0) +// SharedSegments server its own knob is not consulted: the client's ask decides, and the +// server grants it at its first native bind (ServerSession:: +// SettleAdoptT0AtBind) or names why not // // In MG_Remote/Transport (the link-ratchet's SERVER partition) because both roles call it: // ServerSession::Accept and ClientSession's two Start halves. The at-use check the server's -// applier and codec ask (Client::AdoptTierIsEmulate) reads the settlement back. +// applier and codec ask on the T2 path (Client::AdoptTierIsEmulate) reads the settlement back. #pragma once @@ -38,9 +40,15 @@ namespace MobileGL::MG_Remote::Transport { enum class AdoptTierSide : std::uint8_t { Client, Server }; - // Settles this session's tier from MOBILEGL_IPC_ADOPT_TIER and the negotiated data plane, - // on `side`, and returns the tier the session runs (2 until package B). Logs at most one - // line per call; aborts (SessionFail) only on the client over shared segments. + // The tier the client's Hello asks for (LinkTerms.adoptTier): 0 when the knob says 0 and the + // plane it proposes is shared segments, else 2. Pure - the lines are the settle's. + std::uint32_t AdoptTierAskFor(bool streamDataPlane); + + // Settles this side's handshake answer from MOBILEGL_IPC_ADOPT_TIER and the negotiated data + // plane and returns the tier this side asks (client) or serves by its own knob (server): 0 + // only for a client asking T0 over shared segments, 2 otherwise. Logs at most one line per + // call (A1's stream refusal, the T1 refusal). Never aborts for 0/1; a knob above 2 (only a + // direct assignment can make one) is still Fatal{UnimplementedAdoptTier}. std::uint32_t SettleAdoptTierAtHandshake(bool streamDataPlane, AdoptTierSide side); // True once a handshake in this process has settled the tier. The at-use check trusts a diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp index 72e31437..650bde6c 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -1824,7 +1824,18 @@ namespace MobileGL::MG_Remote::Wire { MGPipeApplyResourceDestroy(*static_cast(payload)); return true; - case MGPWireOp::MapPersistent: + case MGPWireOp::MapPersistent: { + // P11 B2 (CONTRACT-P11.md B2): A T0 SESSION'S ADOPTION RIDES THIS RECORD. The verb + // sink takes the client's AHardwareBuffer for this seq off the aux socket and imports + // it (Server/AdoptInbox.h says why that wait cannot hang the apply thread); it answers + // for itself or says "not T0", and everything below is the T2 arm, unchanged. + Int32 t0Status = ReplySink::kStatusDeclined; + if (m_verbs != nullptr && + m_verbs->OnMapPersistent(*static_cast(payload), seq, t0Status)) { + PostReply(op, seq, t0Status, nullptr, 0); + return true; + } + } // R-6 / R-2.4: A CONSTANT DECLINE IN P5, and the applier is not called at all. // Two reasons, and the second is the one worth writing down: the answer is a HOST // POINTER, which cannot cross; and MGPWireRec_MapPersistent's payload is a bare diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.h b/MobileGL/MG_Remote/Wire/PipeWireCodec.h index dfe2b7cf..5b8fb18f 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.h +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.h @@ -505,6 +505,13 @@ namespace MobileGL::MG_Remote::Wire { class WireVerbSink { public: virtual ~WireVerbSink() = default; + // P11 B2 (CONTRACT-P11.md B2): map_persistent in a session that runs T0. True = the sink + // handled it and `status` is the answer (OK: the client's AHardwareBuffer is now the + // store; DECLINED: the store runs T2); false = not a T0 session, and the codec gives + // today's constant decline. `seq` is the record's, which is what the store's Offer names. + virtual Bool OnMapPersistent(const MG_Pipe::MGPHandleOnly&, Uint64 /*seq*/, Int32& /*status*/) { + return false; + } virtual Bool OnFenceCreate(const MG_Pipe::MGPHandleOnly&) { return false; } virtual Bool OnFenceDestroy(const MG_Pipe::MGPHandleOnly&) { return false; } virtual Bool OnFenceStatus(const MG_Pipe::MGPHandleOnly&, Uint32&) { return false; } diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp index f9446fa1..e32156a9 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp @@ -589,6 +589,18 @@ namespace MobileGL::MG_State::GLState { // illegal until P8/P9 make it mean "fully narrowed - nothing is dirty". if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { #if MOBILEGL_PIPE_PUSH + if (m_size != 0 && MG_Pipe::MGPipeResourceSubsystemEnabled() && m_resource.IsGpuResident()) { + // P11 B2: A T0 STORE IS READ WHERE IT IS. Under split a store is resident only + // when T0 adopted it - its pages ARE the client's AHardwareBuffer - so no bytes + // need to come back: the readback's round trip is "done, no bytes". The server + // lands every resident write it queued (GPU-ordered), waits for its GPU, answers, + // and only then does this read the held mapping (monolith's adopted-store readback, + // Managers.cpp's persistentMapped arm, by the same rule). The flag clears here + // because no writeback will arrive to clear it (WritebackFromBackend). + MG_Pipe::MGPipeEmitResourceReadback(*this); + m_gpuWritePending = false; + return; + } if (m_size != 0 && MG_Pipe::MGPipeResourceSubsystemEnabled()) { const SizeT sliceBytes = MG_Remote::Client::BufferWritebackSliceBytes(); if (sliceBytes != 0 && m_size > sliceBytes) { @@ -936,6 +948,12 @@ namespace MobileGL::MG_State::GLState { !(access & BufferMappingAccessBit::FlushExplicit) && MG_Pipe::MGPipeResourceSubsystemEnabled()) { if (void* pushedBase = MG_Pipe::MGPipeEmitMapPersistent(*this)) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P11 B2 made this reachable (a T0 adoption), so the tracker lets go of the + // shadow range WHILE THE SHADOW IS STILL OURS: AdoptPersistentMap frees it, and + // an untrack after that would mprotect pages the buffer no longer owns. + MG_Remote::Client::PersistentMapTracker::Instance().Forget(*this); +#endif m_resource.AdoptPersistentMap(pushedBase); #if MOBILEGL_BUILD_DISAGGREGATED // An adoption takes the buffer OUT of the push set and out of the diff --git a/scripts/ci/fatal_census.py b/scripts/ci/fatal_census.py index 91a20cc6..f6ffca04 100644 --- a/scripts/ci/fatal_census.py +++ b/scripts/ci/fatal_census.py @@ -198,6 +198,17 @@ LOCAL_REFUSAL_WORDS = { "AdoptTierOnStream": "Transport/AdoptTier.cpp (P11 A1) - either side refusing its OWN MOBILEGL_IPC_ADOPT_TIER=0/1 " "on a stream data plane and running T2; the session goes on, so no Refuse frame is sent", + "AdoptTierClosed": + "Transport/AdoptTier.cpp (P11 B2) - the CLIENT refusing its own MOBILEGL_IPC_ADOPT_TIER=1 (T1 " + "was closed, ID-P11-1) and asking T2; the session goes on, so no Refuse frame is sent", + "AdoptT0Unavailable": + "ServerSession.cpp / ClientSession.cpp (P11 B2) - a session that asked T0 and cannot run it " + "(the server's allow switch, no AHardwareBuffer, a failed POST, no kCapAdoptT0) runs T2; one " + "line on each side, and the session goes on", + "AdoptT0NoStore": + "ServerSession.cpp / ClientSession.cpp (P11 B2) - one T0 map_persistent answered DECLINED " + "because its store never reached the server (the Offer's bounded wait, or no hop socket on " + "the client); that store runs T2 and the session goes on", } diff --git a/scripts/ci/protocol_revision_pins.json b/scripts/ci/protocol_revision_pins.json index 579fe842..51c4e859 100644 --- a/scripts/ci/protocol_revision_pins.json +++ b/scripts/ci/protocol_revision_pins.json @@ -2,5 +2,6 @@ "1": "7fc859f7fa756818401f4162e7d49a25d430b91e44d89c297321e3cb36c69638", "2": "b30928e6fc93ddc015fd86f634a6820f775a6f2f6de5a848d273981e706b2597", "3": "da09f0acb48cf245703f28556ef5a38bc9e25241ebbf7b6baa9ab1765b7ddd50", - "4": "6357c3a07ae258d2825dc42bfd610712b15a38a3beb7a96b5f56ea2988705e7f" + "4": "6357c3a07ae258d2825dc42bfd610712b15a38a3beb7a96b5f56ea2988705e7f", + "5": "f83f4c5ba6d15fac9c9f00c12b246c5f706f10c80719accd4ce116cb1a090c10" }