// MobileGL - MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.cpp // Copyright (c) 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 "SplitRuntimePeek.h" // MGITEST_SPLIT_RUNTIME_PEEK is defined by MG_IntegrationTest/CMakeLists.txt under // MOBILEGL_BUILD_DISAGGREGATED and nowhere else. NO SOURCE PROBE decides it: the three symbols // below are c0's, they exist in every disaggregated build from the contract commit onward, and // their VALUES are what answer the question. That is the whole of the fix for review findings // M-1, M-2 and M-3 - there is no longer a string for anyone to rename, comment out, or land // outside a probed directory. #if defined(MGITEST_SPLIT_RUNTIME_PEEK) && !defined(__ANDROID__) #include #include #include #include #include #include #include #include #include #define MGITEST_SPLIT_RUNTIME_PEEK_LIVE 1 #endif // P13 W5: THE RECORD-ARM HALF OF THE PEEK, which needs no transport. A library built with the // record arm and without MG_Remote (the FCL shape) still answers dataArmIsRecord, so the scenarios // that branch on it assert the arm the process really runs rather than the frontend arm a missing // peek would imply. #if defined(MGITEST_RECORD_ARM_PEEK) && !defined(__ANDROID__) #include #define MGITEST_RECORD_ARM_PEEK_LIVE 1 #endif namespace MGITest { #if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE) namespace { std::atomic g_holdArmed{false}, g_holdActive{false}, g_holdRelease{true}; void HoldOneAppliedBatch() { if (!g_holdArmed.exchange(false, std::memory_order_acq_rel)) return; g_holdActive.store(true, std::memory_order_release); const auto until = std::chrono::steady_clock::now() + std::chrono::seconds(2); while (!g_holdRelease.load(std::memory_order_acquire) && std::chrono::steady_clock::now() < until) std::this_thread::sleep_for(std::chrono::milliseconds(1)); g_holdActive.store(false, std::memory_order_release); } } #endif bool ArmSplitApplyHoldForTesting() { #if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE) g_holdRelease.store(false, std::memory_order_release); g_holdArmed.store(true, std::memory_order_release); MobileGL::MG_Remote::Server::ServerLoopInstance().SetBeforeRetireHookForTesting(&HoldOneAppliedBatch); return true; #else return false; #endif } bool WaitForSplitApplyHoldForTesting(unsigned int timeoutMs) { #if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE) const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); while (std::chrono::steady_clock::now() < until) { if (g_holdActive.load(std::memory_order_acquire)) return true; std::this_thread::sleep_for(std::chrono::milliseconds(1)); } #else (void)timeoutMs; #endif return false; } bool SplitApplyHoldIsActiveForTesting() { #if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE) return g_holdActive.load(std::memory_order_acquire); #else return false; #endif } void ReleaseSplitApplyHoldForTesting() { #if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE) g_holdRelease.store(true, std::memory_order_release); g_holdArmed.store(false, std::memory_order_release); MobileGL::MG_Remote::Server::ServerLoopInstance().SetBeforeRetireHookForTesting(nullptr); while (g_holdActive.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); #endif } bool SplitProducerIsParkedForTesting() { #if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE) // This is the only reader intended for the observer thread. Do not call // PeekSplitRuntime there: its encoder and client-ledger fields are not atomic. auto* session = MobileGL::MG_Remote::Client::ClientSession::Active(); return session && session->Control() && session->Control()->producerParked.load(std::memory_order_acquire); #else return false; #endif } bool WaitForSplitAppliedForTesting(unsigned long long seq, unsigned int timeoutMs) { #if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE) auto* session = MobileGL::MG_Remote::Client::ClientSession::Active(); auto* control = session ? session->Control() : nullptr; if (!control) return false; const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); do { if (control->Progress.appliedSeq.load(std::memory_order_acquire) >= seq) return true; std::this_thread::sleep_for(std::chrono::milliseconds(1)); } while (std::chrono::steady_clock::now() < until); return control->Progress.appliedSeq.load(std::memory_order_acquire) >= seq; #else (void)seq; (void)timeoutMs; return false; #endif } void DelaySplitRetirementForTesting(bool enabled) { #if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE) MobileGL::MG_Remote::Server::ServerLoopInstance().SetBeforeRetireHookForTesting( enabled ? +[] { std::this_thread::sleep_for(std::chrono::milliseconds(30)); } : nullptr); #else (void)enabled; #endif } SplitRuntimeState PeekSplitRuntime() { SplitRuntimeState state; #if defined(MGITEST_RECORD_ARM_PEEK_LIVE) state.dataArmIsRecord = MobileGL::MG_Config::DataArmIsRecord(); #endif #if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE) using MobileGL::MG_Config::TransportMode; state.peekAvailable = true; state.totalVerbSlots = MobileGL::MG_Remote::Client::kRemoteEmitSlotCount; switch (MobileGL::MG_Config::Transport) { case TransportMode::Monolith: state.transportName = "monolith"; break; case TransportMode::InProcess: state.transportName = "inproc"; break; default: state.transportName = "non-monolith"; break; } state.transportResolved = MobileGL::MG_Config::Transport != TransportMode::Monolith; state.dataArmIsRecord = MobileGL::MG_Config::DataArmIsRecord(); // Active() is c0's one deliberately non-aborting accessor: "does a session exist" has a // legitimate no. Everything below it is only reached through a live session, so nothing // here can trip one of c0's Fatal stubs. MobileGL::MG_Remote::Client::ClientSession* session = MobileGL::MG_Remote::Client::ClientSession::Active(); state.sessionActive = session != nullptr; state.implementedVerbs = MobileGL::MG_Remote::Client::ImplementedVerbCount(); if (session != nullptr) { // Encoder() returns the member; EmitSeq() returns m_emitSeq. Neither is a stub, and // neither emits anything - this is a read. const MobileGL::MG_Remote::Wire::PipeWireEncoder& encoder = session->Encoder(); state.emitSeq = encoder.EmitSeq(); state.runAheadArmed = session->RunAheadArmed(); // P7 wave 2 package C, OQ-10: read through the SAME accessor the frontend's own // MGPipeResourceOpsHaveSubDataResident reads (MG_Impl/Pipe/PipeFill.cpp), so a // green case here and a resident emission there cannot disagree about the bit. state.residentSubDataCap = MobileGL::MG_Remote::Client::CapsMirrorInstance().HasCap( MobileGL::MG_Pipe::kCapResidentSubData); state.presentCredit = MobileGL::MG_Config::Ipc.PresentCredit; state.presentCreditWaits = session->PresentCreditWaits(); if (const auto* control = session->Control()) { state.appliedSeq = control->Progress.appliedSeq.load(std::memory_order_acquire); state.retiredSeq = control->Progress.retiredSeq.load(std::memory_order_acquire); state.presentAckSerial = control->Progress.presentAckSerial.load(std::memory_order_acquire); } // The producer's ledger. Every one of these is a plain member read on the encoder // or on the RingProducer it holds; none of them emits, publishes or waits, so a // case may read them between two GL calls without changing what the next record is. state.maxRecordBytes = encoder.MaxRecordBytesSeen(); state.maxRecordBytesCap = encoder.MaxRecordBytesCap(); state.cmdWraps = encoder.CmdWraps(); state.cmdWrapPads = encoder.CmdWrapPads(); state.cmdBytesWritten = encoder.CmdBytesWritten(); state.stageReclaimWaits = encoder.StageReclaimWaits(); state.replyPostings = session->ReplyPostings(); state.packBufferReadbackMarks = MobileGL::MG_Record::ProducerMarkCount( MobileGL::MG_Record::GpuWriteProducer::ReadPixelsPackBuffer); const auto fences = MobileGL::MG_Remote::Client::ReadFencePollCounters(); state.fenceLocalAnswers = fences.LocalAnswers; state.fenceEscalations = fences.Escalations; state.fenceRoundTrips = fences.RoundTrips; state.fenceServerReports = fences.ServerReports; } #endif return state; } std::string SplitRuntimeSkipReason() { const SplitRuntimeState state = PeekSplitRuntime(); if (!state.peekAvailable) { return "this build did not compile MG_Remote (no -DMOBILEGL_BUILD_DISAGGREGATED=ON), or " "this is the Android binary, which links the shipping libMobileGL.so built " "-fvisibility=hidden and can reach no internal symbol. MOBILEGL_TRANSPORT is " "ACCEPTED AND SILENTLY IGNORED in such a build (CONTRACT-P5 5), so a green here " "would be a monolith run under a name that says split"; } if (!state.transportResolved) { return "MG_Config::Transport resolved to '" + state.transportName + "', not to a split transport. The variable is read from the process, not from a " "log line - a DEBUG-level pull build prints the same KEY=VALUE string out of " "ConfigLoader's env dump (review M-5). Check MOBILEGL_TRANSPORT reached this " "process"; } if (!state.sessionActive) { return "MG_Remote::Client::ClientSession::Active() is null: no client session exists in " "this process. c0 shipped Start() as a Fatal stub and Active() as a deliberate " "null, so this is what 'packages s1 (construction and handshake) and c1 have not " "landed' looks like from inside a running test. The entry stays registered (gate " "G14) and skips rather than passing against the monolith path"; } if (state.implementedVerbs == 0) { return "MG_Remote::Client::ImplementedVerbCount() is 0 of " + std::to_string(state.totalVerbSlots) + ": the emit table has no real emitter, so every verb this scenario issues would " "take the Fatal{UnmigratedVerb} arm or fall through. Package c1 owns it"; } return {}; } } // namespace MGITest