diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index 9d7bd0c1..a62a9e1b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -164,6 +164,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { return VkBufferManager::SelfTestWireImport(why, whyBytes); }, #endif + // T0 in one process: the client gets the store's own mapping, as monolith's + // donation does; the client seeds it from its shadow once this returns. + .DonateInProcess = [](MG_Pipe::MGPipeHandle res, Uint64 size) -> void* { + auto* m = WireManager("donate"); + return m != nullptr ? m->DonateWireBuffer(res, size, nullptr) : nullptr; + }, }; } // namespace diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index ffc796fe..57be1d6c 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -2574,8 +2574,8 @@ if (MOBILEGL_BUILD_DISAGGREGATED) list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}") math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1") # P11 B2 (CONTRACT-P11 B2): THE SHARED-SEGMENT ARMS WITH MOBILEGL_IPC_ADOPT_TIER=0, which A1 made - # die at the handshake and B2 turned into an ASK. A host has no AHardwareBuffer, so the server - # refuses T0 by name at its first native bind, publishes no kCapAdoptT0, and the client falls + # die at the handshake and B2 turned into an ASK. A host has no AHardwareBuffer (and Espryt cannot + # hand an in-process client its stores), so the server refuses T0 by name at its first native bind, publishes no kCapAdoptT0, and the client falls # back to T2 with one named line at its first map_persistent: every case green, the arena # emulated (SplitLogPaths declares it), ASessionThatCannotRunT0FallsBackToT2OnceByName reading # the two lines. `.AdoptT0Disallowed.` is the same on the spawn arm with the server's allow switch @@ -2583,6 +2583,13 @@ if (MOBILEGL_BUILD_DISAGGREGATED) foreach(mglT0Arm IN ITEMS Split Spawn) string(TOUPPER "${mglT0Arm}" mglT0ArmUpper) string(TOLOWER "${mglT0Arm}" mglT0ArmLower) + # Split is one process: Espryt cannot hand its client its stores, and an in-process session + # never takes the AHardwareBuffer arm, so the refusal names that rather than the platform. + if(mglT0Arm STREQUAL "Split") + set(mglT0Fallback "in process") + else() + set(mglT0Fallback "no AHardwareBuffer") + endif() gtest_discover_tests(MobileGLIntegrationTest TEST_PREFIX "DirectGLES.${mglT0Arm}.AdoptTier0." TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}" @@ -2591,7 +2598,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED) PROPERTIES LABELS "integration-gpu\;integration-${mglT0ArmLower}" TIMEOUT ${MGL_ITEST_TIMEOUT} - ENVIRONMENT "${MGL_ITEST_GLES_${mglT0ArmUpper}_ENVIRONMENT}\;MOBILEGL_IPC_ADOPT_TIER=0\;MGITEST_ADOPT_T0_FALLBACK=no AHardwareBuffer" + ENVIRONMENT "${MGL_ITEST_GLES_${mglT0ArmUpper}_ENVIRONMENT}\;MOBILEGL_IPC_ADOPT_TIER=0\;MGITEST_ADOPT_T0_FALLBACK=${mglT0Fallback}" ) list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}") math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1") @@ -4090,6 +4097,14 @@ if (MOBILEGL_BUILD_DISAGGREGATED) mgl_itest_magma_arm_shape(${mglItestArm}) mgl_itest_join_environment(mglItestMagmaBandEnvironment ${MGL_ITEST_MAGMA_BAND_FLAVOUR_ENV} ${mglItestArmTransportEnv}) + # Where the >= 16 MiB arena lands on this arm (P11 A2). Split is one process: T0 (the default + # tier) is granted in process and the arena is the store Magma hands its client - adopted. + # Across processes a host has no shareable memory, so the session runs T2 - emulated. + if("${mglItestArm}" STREQUAL "Split") + set(mglItestArenaArm adopted) + else() + set(mglItestArenaArm emulated) + endif() foreach(bandCase RenderbufferReadOfExactlyTwoMiBIsBandedNotRefused TwoMiBPlusOneRowIsBanded AReadLargerThanTheWholeReplySegmentIsBanded @@ -4137,9 +4152,8 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ENVIRONMENT "${mglItestMagmaBandEnvironment}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p10-fence${mglItestArmLogTag}-${fenceCase}.log") endforeach() # P11 A2 (MG_Remote/CONTRACT-P11.md §2): the >= 16 MiB arena on Magma's three arms, in the - # same flavour. Declared emulated: every split acquisition declines (the wire resource - # table's MapPersistent returns null, VkBufferManager.cpp), so the tier case asserts T2 - # on every arm; DirectVulkan.ArenaArm. is its monolith (adopted) half. + # same flavour, declared per arm (mglItestArenaArm above); DirectVulkan.ArenaArm. is its + # monolith (adopted) half. foreach(arenaCase IN LISTS MGL_ITEST_ARENA_CASES) gtest_discover_tests(MobileGLIntegrationTest TEST_PREFIX "DirectVulkan.${mglItestArm}.Arena." @@ -4147,7 +4161,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED) DISCOVERY_TIMEOUT 30 PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} ${mglItestArmProperties} - ENVIRONMENT "${mglItestMagmaBandEnvironment}\;MGITEST_PERSISTENT_MAP_ARM=emulated\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p11-arena${mglItestArmLogTag}-${arenaCase}.log") + ENVIRONMENT "${mglItestMagmaBandEnvironment}\;MGITEST_PERSISTENT_MAP_ARM=${mglItestArenaArm}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p11-arena${mglItestArmLogTag}-${arenaCase}.log") endforeach() # P11 B2 addendum: DeletedBoundBufferScenario on Magma's three arms, same flavour (the # Espryt arms come from mgl_itest_register_split_arms). Before the frontend fix Magma did @@ -4167,7 +4181,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED) endforeach() # ...and the one-process arena pair (the Espryt half is beside mgl_itest_register_split_arms). string(REPLACE "\\;" ";" mglItestPairEnv "${mglItestMagmaBandEnvironment}") - list(APPEND mglItestPairEnv "MGITEST_PERSISTENT_MAP_ARM=emulated" + list(APPEND mglItestPairEnv "MGITEST_PERSISTENT_MAP_ARM=${mglItestArenaArm}" "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p11-arena-pair${mglItestArmLogTag}.log") string(REPLACE "\\;" ";" mglItestPairLabels "${mglItestMagmaLabels}") set(mglItestPairName "DirectVulkan.${mglItestArm}.Arena.LargeArenaAdoptionScenario.GpuWriteThenTheTierCaseInOneProcess") @@ -4181,13 +4195,17 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ${mglItestArmProperties}) # P11 A1 + B2 (CONTRACT-P11 §1, B2): MOBILEGL_IPC_ADOPT_TIER=0 on every arm, one case set. # On tcp a stream refuses the tier by name at the client's handshake (A1, the stream case - # reads the line); over shared segments (split, spawn) the knob ASKS T0, a host has no - # AHardwareBuffer, and the session falls back to T2 by name (B2, the fallback case reads - # both lines). Register only the assertion applicable to each transport; the - # parity check names these two transport-specific exceptions explicitly. + # reads the line); on spawn the knob ASKS T0, a host has no AHardwareBuffer, and the session + # falls back to T2 by name (B2, the fallback case reads both lines); on split the client is + # the server's process and T0 is granted in process (the tier case reads `adopted`). + # Register only the assertion applicable to each transport; the parity check names these + # transport-specific exceptions explicitly. if(mglItestArm STREQUAL "Tcp") set(mglItestAdoptTier0Env "MGITEST_ADOPT_TIER_REFUSAL=T0") set(mglItestAdoptTier0Case AStreamSessionRefusesTheAdoptTierOnceByName) + elseif(mglItestArm STREQUAL "Split") + set(mglItestAdoptTier0Env "MOBILEGL_IPC_ALLOW_ADOPT_T0=1") + set(mglItestAdoptTier0Case) else() set(mglItestAdoptTier0Env "MGITEST_ADOPT_T0_FALLBACK=no AHardwareBuffer") set(mglItestAdoptTier0Case ASessionThatCannotRunT0FallsBackToT2OnceByName) @@ -4199,7 +4217,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED) DISCOVERY_TIMEOUT 30 PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} ${mglItestArmProperties} - ENVIRONMENT "${mglItestMagmaBandEnvironment}\;MOBILEGL_IPC_ADOPT_TIER=0\;${mglItestAdoptTier0Env}\;MGITEST_PERSISTENT_MAP_ARM=emulated\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p11-adopt-tier0${mglItestArmLogTag}-${arenaCase}.log") + ENVIRONMENT "${mglItestMagmaBandEnvironment}\;MOBILEGL_IPC_ADOPT_TIER=0\;${mglItestAdoptTier0Env}\;MGITEST_PERSISTENT_MAP_ARM=${mglItestArenaArm}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p11-adopt-tier0${mglItestArmLogTag}-${arenaCase}.log") endforeach() endforeach() endif() diff --git a/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py b/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py index 1302ba06..c668f0ac 100644 --- a/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py +++ b/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py @@ -224,7 +224,9 @@ def specialized_required(): } for backend in ("DirectGLES", "DirectVulkan"): names.add(f"{backend}.Tcp.AdoptTier0.LargeArenaAdoptionScenario.AStreamSessionRefusesTheAdoptTierOnceByName") - for arm in ("Split", "Spawn"): + # Magma's split arm is granted T0 in process (its tier case reads `adopted`), so only Espryt, + # which cannot hand an in-process client its stores, falls back there. + for arm in (("Split", "Spawn") if backend == "DirectGLES" else ("Spawn",)): names.add(f"{backend}.{arm}.AdoptTier0.LargeArenaAdoptionScenario.ASessionThatCannotRunT0FallsBackToT2OnceByName") for arm in ("Split", "Spawn"): for case in ("Rgb16fDrawnBaseWithARefusedReadbackDeclinesByName", diff --git a/MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.cpp index c041efd5..7ad1c81d 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.cpp @@ -100,7 +100,8 @@ void main() { word = 0xC0FFEEu; } // which also set MOBILEGL_IPC_ADOPT_TIER=0; a harness marker, never read by the library. constexpr const char* kAdoptTierRefusalMarker = "MGITEST_ADOPT_TIER_REFUSAL"; // P11 B2: set by the shared-segment `.AdoptTier0.` / `.AdoptT0Disallowed.` entries to the reason word the - // server gives for not granting T0 ("no AHardwareBuffer" on a host, "disallowed" with its allow switch off). + // server gives for not granting T0 ("no AHardwareBuffer" on a host, "in process" where the backend cannot hand an + // in-process client its stores, "disallowed" with its allow switch off). constexpr const char* kAdoptT0FallbackMarker = "MGITEST_ADOPT_T0_FALLBACK"; // Draws issued against the arena inside the counted window. One definition, many draws: // "one per definition" (1) and "one per draw" (kDrawsInTheWindow) have to be different diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index 30876c83..6eca4ff0 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -157,6 +157,11 @@ namespace MobileGL::MG_Pipe { // 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, + // T0 IN ONE PROCESS. Set with kCapAdoptT0 when the client is this process (inproc) and the + // backend can donate a store: map_persistent's OK answer carries the address of the + // server's own mapped store, and the client allocates no AHardwareBuffer - the store is + // the one memory both sides already share, as monolith's donation is. + kCapAdoptInProcess = 1ull << 12, }; // 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 b447c087..e9ec9059 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -2860,6 +2860,23 @@ namespace MobileGL::MG_Pipe { if (g_resourceOps == nullptr || g_resourceOps->ImportExternal == nullptr) return false; return g_resourceOps->ImportExternal(handle.Handle, ahb, size); } + + void* MGPipeApplyDonateInProcess(const MGPHandleOnly& handle) { + if (static_cast(handle.Kind) != MGPipeKind::Buffer) { + MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption") + " donate_in_process {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 nullptr; + } + MGPipeResourceRecord* record = ResolveResource("donate_in_process", handle.Handle); + if (record == nullptr || record->Desc.Width == 0) return nullptr; + // adopt_external's pin, for adopt_external's reason: the client writes the donated store + // directly, as a monolith client writes its adopted map. + PinNoLiveHostWrites(*record, handle.Handle, "donate_in_process"); + if (g_resourceOps == nullptr || g_resourceOps->DonateInProcess == nullptr) return nullptr; + return g_resourceOps->DonateInProcess(handle.Handle, record->Desc.Width); + } #endif // ================================================================================ diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 8367ea0c..371e3f5d 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -142,6 +142,10 @@ namespace MobileGL::MG_Pipe { // 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); + // T0 for a client in this process: the store's own host-visible coherent mapping (the + // pointer a monolith map_persistent donates), handed to the client as its persistent map. + // The store takes T0's semantics from here on. Null = not donated: the store runs T2. + void* (*DonateInProcess)(MGPipeHandle res, Uint64 size); }; // Install / read the table. A null argument uninstalls, which is what a backend does at @@ -1541,6 +1545,9 @@ namespace MobileGL::MG_Pipe { // 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); + // T0 in one process: buffer `handle`'s store, donated through the backend's DonateInProcess. + // Null declines (not a live buffer, or a backend without the member); the store runs T2. + void* MGPipeApplyDonateInProcess(const MGPHandleOnly& handle); // --------------------------------------------------------------------------------- // P3a: the five vertex-input entry points (D-G, D-H, D-I) diff --git a/MobileGL/MG_Pipe/PipeVerbSink.h b/MobileGL/MG_Pipe/PipeVerbSink.h index 3ce9bb56..5b2981de 100644 --- a/MobileGL/MG_Pipe/PipeVerbSink.h +++ b/MobileGL/MG_Pipe/PipeVerbSink.h @@ -75,7 +75,10 @@ namespace MobileGL::MG_Pipe { // 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 MGPHandleOnly&, Uint64 /*seq*/, Int32& /*status*/) { + // `inProcessBase`: a client in this process gets the donated store's address in the OK + // answer (kCapAdoptInProcess); 0 = the answer carries no bytes (the AHardwareBuffer arm). + virtual Bool OnMapPersistent(const MGPHandleOnly&, Uint64 /*seq*/, Int32& /*status*/, + Uint64& /*inProcessBase*/) { return false; } virtual Bool OnFenceCreate(const MGPHandleOnly&) { return false; } diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index b4b15de4..f756292a 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -3011,11 +3012,26 @@ namespace MobileGL::MG_Remote::Client { Bool ClientSession::AdoptT0() { if (!m_adoptT0Asked) return false; if (m_adoptT0Decision >= 0) return m_adoptT0Decision == 1; - if (Caps().HasCap(MG_Pipe::kCapAdoptT0)) { + const Bool peerIsThisProcess = m_transport != nullptr && + Transport::AdoptT0::PeerIsThisProcess(m_transport->Role() == Transport::TransportRole::InProcess); + if (Caps().HasCap(MG_Pipe::kCapAdoptT0) && peerIsThisProcess && + !Caps().HasCap(MG_Pipe::kCapAdoptInProcess)) { + // A server in this process offered the AHardwareBuffer arm. The stores are this + // process's memory already; an in-process session never allocates one. + m_adoptT0Decision = 0; + ++m_t0Fallbacks; + MGLOG_W("MG_Remote client: Refuse{AdoptT0Unavailable, \"in process\"} - the server is this process " + "and published kCapAdoptT0 without kCapAdoptInProcess; the session runs T2"); + } else 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"); + if (Caps().HasCap(MG_Pipe::kCapAdoptInProcess)) { + MGLOG_I("MG_Remote client: T0 in process - the server is this process, so this session's " + "persistent maps are the server's own store mappings (no AHardwareBuffer)"); + } else { + 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; ++m_t0Fallbacks; @@ -3036,6 +3052,7 @@ namespace MobileGL::MG_Remote::Client { void* ClientSession::AdoptPersistentT0(const MG_Pipe::MGPHandleOnly& handle, Uint64 size, const void* seed) { using namespace Transport::AdoptT0; + if (Caps().HasCap(MG_Pipe::kCapAdoptInProcess)) return AdoptPersistentInProcess(handle, size, seed); // 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; @@ -3141,6 +3158,50 @@ namespace MobileGL::MG_Remote::Client { return nullptr; } + // T0 IN ONE PROCESS (kCapAdoptInProcess): the OK answer carries the address of the server's own + // mapping of the store, which becomes this buffer's persistent map - the memory both sides + // already share, as monolith's donation is. The server has drained the GPU's use of the store + // before answering and applies nothing of this client's until this call returns, so the + // shadow is copied in here, by the side that owns it. + void* ClientSession::AdoptPersistentInProcess(const MG_Pipe::MGPHandleOnly& handle, Uint64 size, + const void* seed) { + Uint64 base = 0; + Uint64 returned = 0; + Int32 status = Wire::ReplySink::kStatusError; + EmitAndWait(MG_Pipe::MGPWireOp::MapPersistent, &handle, sizeof(handle), nullptr, 0, &base, sizeof(base), + &status, &returned); + if (status == Wire::ReplySink::kStatusError) { + SessionFail(MGFatalFamily::ReplyError, + "MGPipe: Fatal{ReplyError, \"map_persistent\"} - the in-process T0 row answered ERROR, " + "which is not an acceptance answer"); + } + if (status != Wire::ReplySink::kStatusOk) { + ++m_t0Declined; + return nullptr; + } + if (returned != sizeof(base) || base == 0) { + SessionFail(MGFatalFamily::UnexpectedMapAccept, + "MGPipe: Fatal{UnexpectedMapAccept, \"map_persistent\"} - the server accepted an " + "in-process T0 store without the store's address"); + } + void* ptr = reinterpret_cast(static_cast(base)); + if (seed != nullptr) std::memcpy(ptr, seed, static_cast(size)); + ++m_t0Imported; + m_t0Bytes += size; + // No AHardwareBuffer to hold: the entry only marks the handle T0 (its release is a no-op). + const Uint64 key = T0Key(handle.Handle); + if (auto it = m_t0Stores.find(key); it != m_t0Stores.end()) { + Transport::AdoptT0::ReleaseHeld(it->second); + ++m_t0Released; + } + m_t0Stores[key] = Transport::AdoptT0::HeldStore{nullptr, ptr, size}; + MGLOG_D("MG_Remote client: T0 store {slot=%u, gen=%u} donated in process - %llu bytes; 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; + } + Bool ClientSession::EmitSharedImage(const MG_Pipe::MGPSharedImageOp& op, int sendFd, MG_Pipe::MGPSharedImageReply* reply, int* receivedFd) { if (receivedFd != nullptr) *receivedFd = -1; diff --git a/MobileGL/MG_Remote/Client/ClientSession.h b/MobileGL/MG_Remote/Client/ClientSession.h index d9d58fb5..1635f2d0 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.h +++ b/MobileGL/MG_Remote/Client/ClientSession.h @@ -293,6 +293,7 @@ namespace MobileGL::MG_Remote::Client { // 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); + void* AdoptPersistentInProcess(const MG_Pipe::MGPHandleOnly& handle, Uint64 size, const void* seed); // shared_image (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): emits `op` and waits // for its answer. `sendFd` >= 0 (an Import) is queued on the aux socket for the record's // seq BEFORE the record is published; `receivedFd` non-null (an Allocate) takes the diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 90bed7ec..6534f3ad 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -116,9 +116,10 @@ namespace MobileGL::MG_Remote::Server { return it->second; } - Bool ServerVerbSink::OnMapPersistent(const MG_Pipe::MGPHandleOnly& handle, Uint64 seq, Int32& status) { + Bool ServerVerbSink::OnMapPersistent(const MG_Pipe::MGPHandleOnly& handle, Uint64 seq, Int32& status, + Uint64& inProcessBase) { ServerSession* session = ServerSession::Active(); - return session != nullptr && session->AdoptStoreT0(handle, seq, status); + return session != nullptr && session->AdoptStoreT0(handle, seq, status, inProcessBase); } Bool ServerVerbSink::OnFenceCreate(const MG_Pipe::MGPHandleOnly& desc) { diff --git a/MobileGL/MG_Remote/Server/PipeApplier.h b/MobileGL/MG_Remote/Server/PipeApplier.h index a618c8e4..a159bfe0 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.h +++ b/MobileGL/MG_Remote/Server/PipeApplier.h @@ -156,7 +156,8 @@ namespace MobileGL::MG_Remote::Server { // queries, present, the T0 map, the control records and shared images. // 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 OnMapPersistent(const MG_Pipe::MGPHandleOnly& handle, Uint64 seq, Int32& status, + Uint64& inProcessBase) 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/ServerSession.cpp b/MobileGL/MG_Remote/Server/ServerSession.cpp index 0c60be77..13276549 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.cpp +++ b/MobileGL/MG_Remote/Server/ServerSession.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -726,6 +727,7 @@ namespace MobileGL::MG_Remote::Server { m_adoptT0Quiet = m_adoptAsk == Transport::kAdoptAskT0Default; m_adoptT0Settled = false; m_adoptT0 = false; + m_adoptT0InProcess = false; m_t0Stores = m_t0Bytes = m_t0Declined = m_t0Refusals = 0; m_adoptInbox.Clear(); m_adoptInbox.Attach(&transport); @@ -966,6 +968,7 @@ namespace MobileGL::MG_Remote::Server { // 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); + if (m_adoptT0InProcess) callMask |= static_cast(MG_Pipe::kCapAdoptInProcess); const auto timerSupported = m_backend->GetBackendFunctions().GL.IsTimerQuerySupported; if ((callMask & MG_Pipe::kCapTimerQuery) && (!timerSupported || !timerSupported())) callMask &= ~static_cast(MG_Pipe::kCapTimerQuery); @@ -1113,7 +1116,7 @@ namespace MobileGL::MG_Remote::Server { m_adoptInbox.Clear(); m_adoptInbox.Attach(nullptr); m_sharedImages.Clear(); - m_adoptT0Asked = m_adoptT0Settled = m_adoptT0 = m_adoptT0Quiet = false; + m_adoptT0Asked = m_adoptT0Settled = m_adoptT0 = m_adoptT0Quiet = m_adoptT0InProcess = false; m_adoptAsk = 2; m_consumer.Detach(); *m_commands = Transport::RingConsumer(); @@ -1153,7 +1156,27 @@ namespace MobileGL::MG_Remote::Server { const char* backendName = m_backend == nullptr ? "server" : m_backend->GetBackendType() == BackendType::DirectVulkan ? "Magma" : "Espryt"; std::string why; - if (MG_Config::Ipc.AllowAdoptT0 == 0) { + // A CLIENT IN THIS PROCESS SHARES THE SERVER'S MEMORY ALREADY: its persistent maps are the + // stores' own mappings, donated as monolith donates them, and no AHardwareBuffer, import + // or POST is involved. A backend that cannot donate leaves the session on T2 - an + // in-process session never takes the AHardwareBuffer arm below. + const Bool inProcess = m_transport != nullptr && + Transport::AdoptT0::PeerIsThisProcess(m_transport->Role() == Transport::TransportRole::InProcess); + if (inProcess) { + const MG_Pipe::MGPipeResourceOps* ops = MG_Pipe::MGPipeGetResourceOps(); + if (MG_Config::Ipc.AllowAdoptT0 == 0) { + why = "this server's MOBILEGL_IPC_ALLOW_ADOPT_T0=0 disallows it"; + } else if (ops == nullptr || ops->DonateInProcess == nullptr) { + why = std::string("the ") + backendName + " backend cannot hand its stores to a client in this process"; + } else { + m_adoptT0 = true; + m_adoptT0InProcess = true; + MGLOG_I("MG_Remote server: T0 granted in process - the client is this process, so each " + "map_persistent hands it the %s store's own mapping (no AHardwareBuffer)", + backendName); + return; + } + } else 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"; @@ -1182,7 +1205,9 @@ namespace MobileGL::MG_Remote::Server { if (!why.empty()) { ++m_t0Refusals; const char* word = MG_Config::Ipc.AllowAdoptT0 == 0 ? "disallowed" - : !Transport::AdoptT0::PlatformHasAhb() ? "no AHardwareBuffer" : "POST"; + : inProcess ? "in process" + : !Transport::AdoptT0::PlatformHasAhb() ? "no AHardwareBuffer" + : "POST"; if (m_adoptT0Quiet) { // THE DEFAULT ASK (MOBILEGL_IPC_ADOPT_TIER unset) FALLS BACK QUIETLY: every host // session would otherwise carry this line. Counted (T0Refusals, the totals line). @@ -1202,9 +1227,29 @@ namespace MobileGL::MG_Remote::Server { backendName); } - Bool ServerSession::AdoptStoreT0(const MG_Pipe::MGPHandleOnly& handle, Uint64 seq, Int32& status) { + Bool ServerSession::AdoptStoreT0(const MG_Pipe::MGPHandleOnly& handle, Uint64 seq, Int32& status, + Uint64& inProcessBase) { if (!m_adoptT0) return false; status = Wire::ReplySink::kStatusDeclined; + inProcessBase = 0; + if (m_adoptT0InProcess) { + void* base = MG_Pipe::MGPipeApplyDonateInProcess(handle); + if (base == nullptr) { + ++m_t0Declined; + MGLOG_W("MG_Remote server: T0 map_persistent {slot=%u, gen=%u} (record %llu) DECLINED - the " + "backend did not donate the store (its log names why); the store runs T2", + handle.Handle.Slot, handle.Handle.Gen, static_cast(seq)); + return true; + } + inProcessBase = static_cast(reinterpret_cast(base)); + ++m_t0Stores; + status = Wire::ReplySink::kStatusOk; + MGLOG_D("MG_Remote server: T0 donated store {slot=%u, gen=%u} in process (record %llu); session " + "total %llu stores", + handle.Handle.Slot, handle.Handle.Gen, static_cast(seq), + static_cast(m_t0Stores)); + return true; + } int hop = -1; Transport::AdoptT0::Offer offer{}; std::string why; diff --git a/MobileGL/MG_Remote/Server/ServerSession.h b/MobileGL/MG_Remote/Server/ServerSession.h index 369f43af..8ddc6780 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.h +++ b/MobileGL/MG_Remote/Server/ServerSession.h @@ -320,7 +320,12 @@ namespace MobileGL::MG_Remote::Server { // 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); + // A client in this process (inproc) takes no AHardwareBuffer: the backend donates the + // store's own mapping and `inProcessBase` carries its address in the OK answer. + Bool AdoptStoreT0(const MG_Pipe::MGPHandleOnly& handle, Uint64 seq, Int32& status, + Uint64& inProcessBase); + // T0 granted for a client in this process: map_persistent donates (kCapAdoptInProcess). + Bool AdoptT0InProcess() const { return m_adoptT0InProcess; } // 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; } @@ -426,6 +431,7 @@ namespace MobileGL::MG_Remote::Server { Uint8 m_adoptAsk = 2; Bool m_adoptT0Settled = false; Bool m_adoptT0 = false; + Bool m_adoptT0InProcess = false; AdoptInbox m_adoptInbox; SharedImages::SessionHolder m_sharedImages; Uint64 m_t0Stores = 0; diff --git a/MobileGL/MG_Remote/Transport/AdoptT0.cpp b/MobileGL/MG_Remote/Transport/AdoptT0.cpp index c3b43c17..f855e7f6 100644 --- a/MobileGL/MG_Remote/Transport/AdoptT0.cpp +++ b/MobileGL/MG_Remote/Transport/AdoptT0.cpp @@ -184,6 +184,18 @@ namespace MobileGL::MG_Remote::Transport::AdoptT0 { return test != nullptr ? test->HasAhb() : RealHasAhb(); } + namespace { + std::atomic g_peerInAnotherProcessForTest{false}; + } // namespace + + bool PeerIsThisProcess(bool inProcessTransport) { + return inProcessTransport && !g_peerInAnotherProcessForTest.load(std::memory_order_acquire); + } + + void SetPeerInAnotherProcessForTest(bool asIfCrossProcess) { + g_peerInAnotherProcessForTest.store(asIfCrossProcess, std::memory_order_release); + } + 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); diff --git a/MobileGL/MG_Remote/Transport/AdoptT0.h b/MobileGL/MG_Remote/Transport/AdoptT0.h index 326bb7e8..bf223653 100644 --- a/MobileGL/MG_Remote/Transport/AdoptT0.h +++ b/MobileGL/MG_Remote/Transport/AdoptT0.h @@ -56,6 +56,14 @@ namespace MobileGL::MG_Remote::Transport::AdoptT0 { // True where AHardwareBuffer exists (Android). Everywhere else T0 is never granted. bool PlatformHasAhb(); + // True when a session's peer is this very process (an in-process transport). Such a session + // never takes this header's arm: its stores are this process's memory already, so T0 there is + // the backend handing over its own store (kCapAdoptInProcess) or nothing (T2). + bool PeerIsThisProcess(bool inProcessTransport); + // Test seam: the protocol tests run over an in-process transport and stand for a peer in + // another process while this is set. + void SetPeerInAnotherProcessForTest(bool asIfCrossProcess); + // ---- client half ---------------------------------------------------------------------- // A store the client allocated. `ahb` is an AHardwareBuffer*, `ptr` its CPU lock, held until diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp index 161866a7..eb496e98 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -1852,9 +1852,11 @@ namespace MobileGL::MG_Remote::Wire { // 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; + Uint64 inProcessBase = 0; if (m_verbs != nullptr && - m_verbs->OnMapPersistent(*static_cast(payload), seq, t0Status)) { - PostReply(op, seq, t0Status, nullptr, 0); + m_verbs->OnMapPersistent(*static_cast(payload), seq, t0Status, inProcessBase)) { + if (inProcessBase != 0) PostReply(op, seq, t0Status, &inProcessBase, sizeof(inProcessBase)); + else PostReply(op, seq, t0Status, nullptr, 0); return true; } } diff --git a/MobileGL/MG_Test/Wire/RemoteClientAdoptT0.inc b/MobileGL/MG_Test/Wire/RemoteClientAdoptT0.inc index a99a3d10..56abbc4c 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientAdoptT0.inc +++ b/MobileGL/MG_Test/Wire/RemoteClientAdoptT0.inc @@ -38,7 +38,9 @@ void FakeDrop(FakeAhb* f) { } } bool FakeHasAhb() { return true; } +Uint64 g_fakeAllocations = 0; bool FakeAllocate(std::uint64_t size, const void* seed, T0::HeldStore& out, std::string& why) { + ++g_fakeAllocations; auto* f = new FakeAhb; f->fd = static_cast(::syscall(SYS_memfd_create, "t0-fake", 1u /*MFD_CLOEXEC*/)); if (f->fd < 0 || ::ftruncate(f->fd, static_cast(size)) != 0) { @@ -144,6 +146,20 @@ MGPipeResourceOps MakeFakeOps() { } MGPipeResourceOps g_fakeOps = MakeFakeOps(); +// ---- the stand-in donor: a backend that hands an in-process client its store's own mapping --- +alignas(64) Uint8 g_donatedStore[kT0Bytes]; +Uint64 g_donations = 0; +void* FakeDonateInProcess(MGPipeHandle res, Uint64 size) { + ++g_donations; + return res.Slot == 5 && size == kT0Bytes ? g_donatedStore : nullptr; +} +MGPipeResourceOps MakeDonatingOps() { + MGPipeResourceOps ops = MakeFakeOps(); + ops.DonateInProcess = &FakeDonateInProcess; + return ops; +} +MGPipeResourceOps g_donatingOps = MakeDonatingOps(); + // What the server's first native bind does for T0, on the apply thread like the real one: the // grant (and its POST), then the caps that bind republishes - which the client's make-current // adopts, as here. @@ -184,6 +200,7 @@ size_t CountOf(const std::string& log, const std::string& needle) { // The grant with the stand-ins, the store, and the map_persistent: returns the adopted pointer. void* AdoptOneStore(Uint8 salt, std::vector& seed) { MG_Config::Ipc.AdoptTier = 0; + T0::SetPeerInAnotherProcessForTest(true); T0::SetPlatformForTest(&kFakePlatform); MG_Pipe::MGPipeSetResourceOps(&g_fakeOps); StartControlSession(); @@ -223,6 +240,7 @@ TEST(RemoteClientAdoptT0, KnobZeroOverSharedSegmentsAsksT0AndFallsBackToT2ByName TEST(RemoteClientAdoptT0, KnobTwoIsTodaysT2WithNoT0Line) { const auto child = RunInChild([] { MG_Config::Ipc.AdoptTier = 2; + T0::SetPeerInAnotherProcessForTest(true); T0::SetPlatformForTest(&kFakePlatform); MG_Pipe::MGPipeSetResourceOps(&g_fakeOps); StartControlSession(); @@ -251,6 +269,7 @@ TEST(RemoteClientAdoptT0, KnobTwoIsTodaysT2WithNoT0Line) { TEST(RemoteClientAdoptT0, AnUnsetKnobFallsBackToT2QuietlyAndCountsIt) { const auto child = RunInChild([] { MG_Config::Ipc.AdoptTier = MG_Config::kAdoptTierUnset; + T0::SetPeerInAnotherProcessForTest(true); MG_Pipe::MGPipeSetResourceOps(&g_fakeOps); // a backend WITH the members: the platform decides StartControlSession(); if (!Srv::ServerSessionInstance().AdoptT0Asked()) ::_exit(210); // the Hello asked T0 by default @@ -292,6 +311,7 @@ TEST(RemoteClientAdoptT0, KnobOneIsRefusedByNameAndAsksT2) { TEST(RemoteClientAdoptT0, NoAhardwareBufferOnThisPlatformIsT2AndTwoNamedLines) { const auto child = RunInChild([] { MG_Config::Ipc.AdoptTier = 0; + T0::SetPeerInAnotherProcessForTest(true); MG_Pipe::MGPipeSetResourceOps(&g_fakeOps); // a backend WITH the members: the platform decides StartControlSession(); GrantAtFirstBind(); @@ -383,4 +403,60 @@ TEST(RemoteClientAdoptT0, AnOfferThatNeverArrivesIsANamedDeclineNotAHang) { EXPECT_EQ(CountOf(child.Log, "Refuse{AdoptT0NoStore, \"record"), 1u) << child.Log; EXPECT_EQ(child.Log.find("Fatal{"), std::string::npos) << child.Log; } +// T0 IN ONE PROCESS: the client is this process, so a backend that can donate hands it the store's +// own mapping - no AHardwareBuffer is allocated, no Offer, no import, no POST - and the client's +// pointer is the server's store, seeded with the shadow. +// RED-ONCE (host): grant the AHardwareBuffer arm for an in-process client (drop the in-process +// branch of ServerSession::SettleAdoptT0AtBind) -> the stand-in platform allocates, 223. +TEST(RemoteClientAdoptT0, AnInProcessClientGetsTheServersStoreAndNoAhardwareBuffer) { + const auto child = RunInChild([] { + MG_Config::Ipc.AdoptTier = 0; + T0::SetPlatformForTest(&kFakePlatform); + MG_Pipe::MGPipeSetResourceOps(&g_donatingOps); + StartControlSession(); + GrantAtFirstBind(); + if (!Srv::ServerSessionInstance().AdoptT0InProcess()) ::_exit(220); + const auto seed = Pattern(kT0Bytes, 0x77); + const MGPHandleOnly handle = MakeBufferStore(5, kT0Bytes); + void* adopted = MGPipeRouteMapPersistent(handle, kT0Bytes, seed.data()); + if (adopted != g_donatedStore) ::_exit(221); + if (std::memcmp(g_donatedStore, seed.data(), kT0Bytes) != 0) ::_exit(222); + if (g_fakeAllocations != 0 || g_fakeImports.count != 0) ::_exit(223); + if (g_donations != 1) ::_exit(224); + if (Srv::ServerSessionInstance().T0StoresImported() != 1 || ClientSessionInstance().T0StoresImported() != 1) + ::_exit(225); + ClientSessionInstance().Stop(); + }); + ExpectChildSuccess(child); + EXPECT_NE(child.Log.find("T0 granted in process"), std::string::npos) << child.Log; + EXPECT_EQ(child.Log.find("T0 POST"), std::string::npos) << child.Log; + EXPECT_EQ(child.Log.find("Fatal{"), std::string::npos) << child.Log; +} +// T0 IN ONE PROCESS NEVER TAKES THE AHardwareBuffer ARM: a backend that cannot hand an in-process +// client its store leaves the session on T2 by name - no allocation, no Offer, no import, no POST - +// even where the transfer arm would work (the stand-in platform and importer are installed). +// RED-ONCE (host): let SettleAdoptT0AtBind fall through to the AHardwareBuffer arm for an in-process +// client -> the stand-in POST passes and the session is granted T0, 230. +TEST(RemoteClientAdoptT0, AnInProcessClientWhoseBackendCannotDonateRunsT2AndNeverTheTransferArm) { + const auto child = RunInChild([] { + MG_Config::Ipc.AdoptTier = 0; + T0::SetPlatformForTest(&kFakePlatform); + MG_Pipe::MGPipeSetResourceOps(&g_fakeOps); // the transfer arm's members, no DonateInProcess + StartControlSession(); + GrantAtFirstBind(); + if (Srv::ServerSessionInstance().AdoptT0()) ::_exit(230); + if (Srv::ServerSessionInstance().T0Refusals() != 1) ::_exit(231); + const auto seed = Pattern(kT0Bytes, 0x88); + const MGPHandleOnly handle = MakeBufferStore(5, kT0Bytes); + if (MGPipeRouteMapPersistent(handle, kT0Bytes, seed.data()) != nullptr) ::_exit(232); + if (g_fakeAllocations != 0 || g_fakeImports.count != 0) ::_exit(233); + if (ClientSessionInstance().AdoptT0()) ::_exit(234); + ClientSessionInstance().Stop(); + }); + ExpectChildSuccess(child); + EXPECT_EQ(CountOf(child.Log, "Refuse{AdoptT0Unavailable, \"in process\"}"), 1u) << child.Log; + EXPECT_EQ(child.Log.find("T0 POST"), std::string::npos) << child.Log; + EXPECT_EQ(child.Log.find("T0 granted"), std::string::npos) << child.Log; + EXPECT_EQ(child.Log.find("Fatal{"), std::string::npos) << child.Log; +} #endif