From 30fef23d52921902344b1d5c3f852d503cef04a6 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 2 Oct 2026 00:04:00 -0400 Subject: [PATCH] [Fix] (P14): scope the server staged stores and the context-live flag to the calling session --- MobileGL/MG_Backend/MGPipe/PipeInputs.cpp | 11 +- MobileGL/MG_Backend/MGPipe/PipeInputs.h | 6 ++ MobileGL/MG_Remote/Server/SessionRuntime.cpp | 13 +++ MobileGL/MG_Remote/Server/StagedShadow.h | 51 ++++++++- .../MG_Remote/Server/StagedTextureStore.h | 92 ++++++++++++---- MobileGL/MG_Test/Wire/CMakeLists.txt | 2 +- .../MG_Test/Wire/ServerContextLiveTest.cpp | 68 ++++++++++++ .../MG_Test/Wire/StagedTextureStoreTest.cpp | 100 ++++++++++++++++++ 8 files changed, 317 insertions(+), 26 deletions(-) create mode 100644 MobileGL/MG_Test/Wire/ServerContextLiveTest.cpp diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp b/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp index e1011631..2aedb3b4 100644 --- a/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp @@ -339,6 +339,8 @@ namespace MobileGL::MG_Pipe { inputs.m_live = live; inputs.m_contextIdentity = identity; } + static void SetServerContextLive(PipeInputs& inputs, Bool live) { inputs.m_serverContextLive = live; } + static Bool ServerContextLive(const PipeInputs& inputs) { return inputs.m_serverContextLive; } }; // ---- P5f (f1): the dual-block rehearsal's selection functions (PipeInputs.h) ----------- @@ -397,14 +399,13 @@ namespace MobileGL::MG_Pipe { MGPipeStampAccess::SetServerStamped(MGPipeClientInputs(), false); } - namespace { Bool g_serverContextLive = false; } - + // The calling thread's block, so the calling session's (PipeInputs.h's m_serverContextLive). void MGPipeServerSetContextLive(Bool live) { - g_serverContextLive = live; + MGPipeStampAccess::SetServerContextLive(gPipeInputs, live); MGPipeServerBlockNoteIdentity(); } - Bool MGPipeServerContextIsLive() { return g_serverContextLive; } + Bool MGPipeServerContextIsLive() { return MGPipeStampAccess::ServerContextLive(gPipeInputs); } #if MOBILEGL_BUILD_DISAGGREGATED namespace { const void* g_serverOwnedWindow = nullptr; } @@ -433,7 +434,7 @@ namespace MobileGL::MG_Pipe { // hands out a null slot - an unnamed crash where the rehearsal exists to produce a // named one. const Uint64 serial = MGPipeApplierContextSerial(); - MGPipeStampAccess::SetIdentity(gPipeInputs, g_serverContextLive, + MGPipeStampAccess::SetIdentity(gPipeInputs, MGPipeServerContextIsLive(), reinterpret_cast(static_cast( (serial << 1) | Uint64{1}))); } diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.h b/MobileGL/MG_Backend/MGPipe/PipeInputs.h index e1693deb..cc914d17 100644 --- a/MobileGL/MG_Backend/MGPipe/PipeInputs.h +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.h @@ -748,6 +748,12 @@ namespace MobileGL::MG_Pipe { // ---- identity ---- const void* m_contextIdentity = nullptr; Bool m_live = false; + // MGPipeServerSetContextLive's flag. In the block, not in a process global, because the + // in-process display server runs several sessions at once and each has its own served + // context: one session's teardown or eglMakeCurrent(NO_CONTEXT) must not mark a + // neighbour's context dead mid-frame (P14; the per-thread block resolution below is + // what makes this the calling session's). + Bool m_serverContextLive = false; MGPipeVerb m_currentVerb = MGPipeVerb::kVerbCount; #if MOBILEGL_PIPE_POISON MGPipeFilledState m_filled{}; diff --git a/MobileGL/MG_Remote/Server/SessionRuntime.cpp b/MobileGL/MG_Remote/Server/SessionRuntime.cpp index 5777a9e9..97538620 100644 --- a/MobileGL/MG_Remote/Server/SessionRuntime.cpp +++ b/MobileGL/MG_Remote/Server/SessionRuntime.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -80,6 +81,17 @@ namespace MobileGL::MG_Remote::Server { outKey->SessionKey, runtime->Session().CurrentContextToken()); return true; } + + // The server's staged stores (StagedShadow.h) file their entries under the same two + // words as the twin tables, for the same reason one level up: the bytes they hold belong + // to one session's group, and a session's teardown may drop only its own. + Bool ResolveThreadStagedBucket(StagedBucket* outBucket) { + MG_Backend::DirectGLES::TwinKey key; + if (!ResolveThreadTwinKey(&key)) return false; + outBucket->SessionKey = key.SessionKey; + outBucket->ShareGroupKey = key.ShareGroupKey; + return true; + } } // namespace void Detail::InstallSessionRuntimeHooks() { @@ -91,6 +103,7 @@ namespace MobileGL::MG_Remote::Server { // P14 S6: the twin tables' key. Same probe, one level down, installed beside the two it // is the twin of (the native tuple's and the applier's). MG_Backend::DirectGLES::SetTwinKeyResolver(&ResolveThreadTwinKey); + SetStagedBucketResolver(&ResolveThreadStagedBucket); } SessionRuntime* Detail::CurrentRuntime() { return t_runtime; } diff --git a/MobileGL/MG_Remote/Server/StagedShadow.h b/MobileGL/MG_Remote/Server/StagedShadow.h index 7f54893e..b21554d6 100644 --- a/MobileGL/MG_Remote/Server/StagedShadow.h +++ b/MobileGL/MG_Remote/Server/StagedShadow.h @@ -45,12 +45,44 @@ #include #include +#include #include #include #include namespace MobileGL::MG_Remote::Server { + // P14: WHOSE STAGED BYTES ARE THESE? The two server stores below are one per process, but the + // in-process display server serves several sessions in that process at once, and a session's + // end (DestroyEGLContext's OnBackendContextDestroyed) used to DropAll() them - every live + // neighbour's declared texture levels and buffer shadows with it. The neighbour's next upload + // into a texture it had declared long ago then read as "resource_subdata arrived before the + // server declared texture level", a Fatal that took the whole display process (and so every + // session in it) down. The texture store is also keyed by the client's {slot, gen}, which two + // clients - or two non-shared groups of one client - hand out independently. + // + // So both stores file their entries under the calling thread's {session, share group}: the + // SAME two words the S6 twin tables are keyed on, answered by the same probe (installed by + // SessionRuntime.cpp beside the twin-key one). With no probe, or on a thread that is no + // session's, the answer is {0, 0} - the single process-wide bucket these stores always were. + struct StagedBucket { + Uint64 SessionKey = 0; + Uint64 ShareGroupKey = 0; + }; + using StagedBucketResolver = Bool (*)(StagedBucket* outBucket); + inline std::atomic g_stagedBucketResolver{nullptr}; + + inline void SetStagedBucketResolver(StagedBucketResolver resolver) { + g_stagedBucketResolver.store(resolver, std::memory_order_release); + } + + inline StagedBucket CurrentStagedBucket() { + StagedBucket bucket; + const StagedBucketResolver resolver = g_stagedBucketResolver.load(std::memory_order_acquire); + if (resolver != nullptr && !resolver(&bucket)) bucket = StagedBucket{}; + return bucket; + } + // Keyed by the resource twin's ADDRESS, which is stable: the twins are heap-allocated and // held by SharedPtr in the backend slot table, and every event that ends a base's life - // an orphaning respecify, a successful map_persistent, destroy, context death - already has @@ -72,6 +104,9 @@ namespace MobileGL::MG_Remote::Server { if (!m_copies) return raw - offset; const std::lock_guard lock(m_mutex); Shadow& shadow = m_shadows[key]; + // A twin's address is already unique across buckets (each bucket owns its twins), so + // the session rides along only for DropAll's sake. + shadow.SessionKey = CurrentStagedBucket().SessionKey; const SizeT needed = std::max(width, offset + size); if (shadow.Bytes.size() < needed) shadow.Bytes.resize(needed, 0); if (size != 0 && raw != nullptr) { @@ -91,10 +126,23 @@ namespace MobileGL::MG_Remote::Server { m_shadows.erase(key); } + // The calling thread's SESSION's shadows (every group of it: a session's teardown ends + // all its contexts), or every shadow on a thread that is no session's. void DropAll() { if (!m_any.load(std::memory_order_acquire)) return; + const Uint64 session = CurrentStagedBucket().SessionKey; const std::lock_guard lock(m_mutex); - m_shadows.clear(); + if (session == 0) { + m_shadows.clear(); + return; + } + for (auto it = m_shadows.begin(); it != m_shadows.end();) { + if (it->second.SessionKey == session) { + it = m_shadows.erase(it); + } else { + ++it; + } + } } // Fatal when a drain reaches bytes no record staged. It fires ONLY for a base that is @@ -187,6 +235,7 @@ namespace MobileGL::MG_Remote::Server { Vector Bytes; // Sorted, disjoint, EXACT. Vector Covered; + Uint64 SessionKey = 0; }; const Bool m_copies; diff --git a/MobileGL/MG_Remote/Server/StagedTextureStore.h b/MobileGL/MG_Remote/Server/StagedTextureStore.h index 0a7142d9..d5622c9e 100644 --- a/MobileGL/MG_Remote/Server/StagedTextureStore.h +++ b/MobileGL/MG_Remote/Server/StagedTextureStore.h @@ -333,8 +333,9 @@ namespace MobileGL::MG_Remote::Server { const Uint8* AdoptRun(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent, Uint64 imageOffset, const void* bytes, SizeT byteSize) { if (!m_copies) return nullptr; + const ShadowKey bucketed = Bucketed(key); const std::lock_guard lock(m_mutex); - LevelShadow& shadow = m_shadows[key].Levels[PackLevel(uploadTarget, level)]; + LevelShadow& shadow = m_shadows[bucketed].Levels[PackLevel(uploadTarget, level)]; RequireDeclaredLevel(shadow, key, uploadTarget, level, extent); #if MOBILEGL_BUILD_DISAGGREGATED // A run can be the first piece of a redefined level. Old coverage @@ -354,7 +355,7 @@ namespace MobileGL::MG_Remote::Server { // P8-E: a run landed whole over a followed copy (AdoptClientBoxes is the spelling // that keeps it), so the bytes no longer hold what the driver does. shadow.HoldsFollowedCopy = false; - m_driverWritten[key] = true; + m_driverWritten[bucketed] = true; } shadow.GpuDirty = false; return CopyRunInto(shadow, key, uploadTarget, level, imageOffset, bytes, byteSize); @@ -369,8 +370,9 @@ namespace MobileGL::MG_Remote::Server { const Uint8* Adopt(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent, const void* bytes, SizeT byteSize) { if (!m_copies) return nullptr; + const ShadowKey bucketed = Bucketed(key); const std::lock_guard lock(m_mutex); - LevelShadow& shadow = m_shadows[key].Levels[PackLevel(uploadTarget, level)]; + LevelShadow& shadow = m_shadows[bucketed].Levels[PackLevel(uploadTarget, level)]; RequireDeclaredLevel(shadow, key, uploadTarget, level, extent); shadow.GpuDirty = false; // P8-E: the caller states these bytes ARE the level, which a followed copy is part of. @@ -407,6 +409,7 @@ namespace MobileGL::MG_Remote::Server { Uint8 resourceTarget = static_cast(MG_Pipe::MGPipeResourceTarget::Tex2D), Uint32 internalFormat = static_cast(TextureInternalFormat::RGBA8)) { if (!m_copies) return; + const ShadowKey bucketed = Bucketed(key); const std::lock_guard lock(m_mutex); Uint64 byteBound = 0; const Bool supportsSubData = StagedTextureTargetSupportsSubData(resourceTarget); @@ -424,7 +427,7 @@ namespace MobileGL::MG_Remote::Server { static_cast(key), uploadTarget, level, extent.x(), extent.y(), extent.z(), internalFormat); } - LevelShadow& shadow = m_shadows[key].Levels[PackLevel(uploadTarget, level)]; + LevelShadow& shadow = m_shadows[bucketed].Levels[PackLevel(uploadTarget, level)]; if (!shadow.Defined || shadow.Extent != extent || shadow.InternalFormat != internalFormat || shadow.ResourceTarget != resourceTarget || shadow.DeclaredByteBound != byteBound) { shadow.Bytes.clear(); @@ -447,8 +450,9 @@ namespace MobileGL::MG_Remote::Server { // chain defined level by level afterwards where the call defines one. void ResetLevels(Uint64 key) { if (!m_any.load(std::memory_order_acquire)) return; + const ShadowKey bucketed = Bucketed(key); const std::lock_guard lock(m_mutex); - m_shadows.erase(key); + m_shadows.erase(bucketed); } // T5's dirty mark: a GPU-side generation made this level's texels newer than any @@ -457,23 +461,35 @@ namespace MobileGL::MG_Remote::Server { // upload" without asking the client (§2.2's last row). void MarkLevelGpuDirty(Uint64 key, Uint16 uploadTarget, Uint16 level, Bool dirty) { if (!m_copies) return; + const ShadowKey bucketed = Bucketed(key); const std::lock_guard lock(m_mutex); - m_shadows[key].Levels[PackLevel(uploadTarget, level)].GpuDirty = dirty; + m_shadows[bucketed].Levels[PackLevel(uploadTarget, level)].GpuDirty = dirty; m_any.store(true, std::memory_order_release); } void Drop(Uint64 key) { if (!m_any.load(std::memory_order_acquire)) return; + const ShadowKey bucketed = Bucketed(key); const std::lock_guard lock(m_mutex); - m_shadows.erase(key); - m_driverWritten.erase(key); + m_shadows.erase(bucketed); + m_driverWritten.erase(bucketed); } + // The calling thread's SESSION's levels (every group of it - the callers are a session's + // context teardown and its applier release), or every level on a thread that is no + // session's. A live neighbour session's declared levels are not this caller's to drop: + // that is the P14 Fatal StagedShadow.h's StagedBucket describes. void DropAll() { if (!m_any.load(std::memory_order_acquire)) return; + const Uint64 session = CurrentStagedBucket().SessionKey; const std::lock_guard lock(m_mutex); - m_shadows.clear(); - m_driverWritten.clear(); + if (session == 0) { + m_shadows.clear(); + m_driverWritten.clear(); + return; + } + EraseSession(m_shadows, session); + EraseSession(m_driverWritten, session); } // ---- P8-E (docs/Disaggregated/notes/p8/E.md): WHEN THESE BYTES ARE THE LEVEL'S CONTENT ---- @@ -496,13 +512,13 @@ namespace MobileGL::MG_Remote::Server { void MarkDriverWritten(Uint64 key) { if (!m_copies) return; const std::lock_guard lock(m_mutex); - m_driverWritten[key] = true; + m_driverWritten[Bucketed(key)] = true; m_any.store(true, std::memory_order_release); } Bool IsDriverWritten(Uint64 key) const { if (!m_any.load(std::memory_order_acquire)) return false; const std::lock_guard lock(m_mutex); - return m_driverWritten.find(key) != m_driverWritten.end(); + return m_driverWritten.find(Bucketed(key)) != m_driverWritten.end(); } Bool LevelHoldsFollowedCopy(Uint64 key, Uint16 uploadTarget, Uint16 level) const { if (!m_any.load(std::memory_order_acquire)) return false; @@ -521,7 +537,7 @@ namespace MobileGL::MG_Remote::Server { const IntVec3& size) { if (!m_copies) return false; const std::lock_guard lock(m_mutex); - if (m_driverWritten.find(srcKey) != m_driverWritten.end()) return false; + if (m_driverWritten.find(Bucketed(srcKey)) != m_driverWritten.end()) return false; const LevelShadow* src = FindLevel(srcKey, srcTarget, srcLevel); LevelShadow* dst = FindLevelMutable(dstKey, dstTarget, dstLevel); if (src == nullptr || dst == nullptr || !LevelComplete(*src)) return false; @@ -709,7 +725,7 @@ namespace MobileGL::MG_Remote::Server { Bool HasShadow(Uint64 key) const { if (!m_any.load(std::memory_order_acquire)) return false; const std::lock_guard lock(m_mutex); - return m_shadows.find(key) != m_shadows.end(); + return m_shadows.find(Bucketed(key)) != m_shadows.end(); } SizeT TrackedResources() const { const std::lock_guard lock(m_mutex); @@ -717,10 +733,48 @@ namespace MobileGL::MG_Remote::Server { } SizeT TrackedLevelCount(Uint64 key) const { const std::lock_guard lock(m_mutex); - const auto it = m_shadows.find(key); + const auto it = m_shadows.find(Bucketed(key)); return it == m_shadows.end() ? 0 : it->second.Levels.size(); } + // THE ENTRY KEY: the caller's key filed under the calling thread's {session, share group} + // (StagedShadow.h's StagedBucket). Two clients' {slot, gen} - or two non-shared groups' + // of one client - are two textures and must be two entries; with no probe the bucket is + // {0, 0} and this is the bare key the store always used. + struct ShadowKey { + Uint64 SessionKey = 0; + Uint64 ShareGroupKey = 0; + Uint64 Key = 0; + Bool operator==(const ShadowKey& other) const { + return SessionKey == other.SessionKey && ShareGroupKey == other.ShareGroupKey && + Key == other.Key; + } + }; + struct ShadowKeyHash { + SizeT operator()(const ShadowKey& key) const { + Uint64 mixed = key.Key ^ (key.ShareGroupKey + 0x9E3779B97F4A7C15ull + (key.Key << 6) + (key.Key >> 2)); + mixed ^= key.SessionKey + 0x9E3779B97F4A7C15ull + (mixed << 6) + (mixed >> 2); + mixed ^= mixed >> 33; + mixed *= 0xFF51AFD7ED558CCDull; + mixed ^= mixed >> 33; + return static_cast(mixed); + } + }; + static ShadowKey Bucketed(Uint64 key) { + const StagedBucket bucket = CurrentStagedBucket(); + return ShadowKey{bucket.SessionKey, bucket.ShareGroupKey, key}; + } + template + static void EraseSession(Map& map, Uint64 session) { + for (auto it = map.begin(); it != map.end();) { + if (it->first.SessionKey == session) { + it = map.erase(it); + } else { + ++it; + } + } + } + static Uint32 PackLevel(Uint16 uploadTarget, Uint16 level) { return (static_cast(uploadTarget) << 16) | static_cast(level); } @@ -833,13 +887,13 @@ namespace MobileGL::MG_Remote::Server { } const LevelShadow* FindLevel(Uint64 key, Uint16 uploadTarget, Uint16 level) const { - const auto textureIt = m_shadows.find(key); + const auto textureIt = m_shadows.find(Bucketed(key)); if (textureIt == m_shadows.end()) return nullptr; const auto levelIt = textureIt->second.Levels.find(PackLevel(uploadTarget, level)); return levelIt == textureIt->second.Levels.end() ? nullptr : &levelIt->second; } LevelShadow* FindLevelMutable(Uint64 key, Uint16 uploadTarget, Uint16 level) { - const auto textureIt = m_shadows.find(key); + const auto textureIt = m_shadows.find(Bucketed(key)); if (textureIt == m_shadows.end()) return nullptr; const auto levelIt = textureIt->second.Levels.find(PackLevel(uploadTarget, level)); return levelIt == textureIt->second.Levels.end() ? nullptr : &levelIt->second; @@ -879,10 +933,10 @@ namespace MobileGL::MG_Remote::Server { const Bool m_copies; mutable std::mutex m_mutex; StagedTextureDeviceLimits m_deviceLimits{}; - ska::flat_hash_map m_shadows; + ska::flat_hash_map m_shadows; // P8-E: MarkDriverWritten's keys (the value is always true). Apart from m_shadows on purpose, // because ResetLevels erases a key's levels and must not forget this. - ska::flat_hash_map m_driverWritten; + ska::flat_hash_map m_driverWritten; // Read on every IsLevelGpuDirty / LevelExtentOrUndefined, so the monolith cost is one // acquire load of a never-written flag rather than a mutex and two hash lookups. std::atomic m_any{false}; diff --git a/MobileGL/MG_Test/Wire/CMakeLists.txt b/MobileGL/MG_Test/Wire/CMakeLists.txt index e239ec81..f7173472 100644 --- a/MobileGL/MG_Test/Wire/CMakeLists.txt +++ b/MobileGL/MG_Test/Wire/CMakeLists.txt @@ -186,7 +186,7 @@ gtest_discover_tests(PipeWireCodecTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS un # THIS FILE IS THE PHASE'S ONE RECURRING MERGE CONFLICT, and it is the same-point-append shape # BRIEF §5's ownership table exists to prevent: three packages, three targets, one end-of-file. # The loop is what stops there being a fourth: a package adding a suite adds a NAME. -foreach (wiretest IN ITEMS RemoteClientTest ServerLoopTest SessionHandshakeTest StagedTextureStoreTest SurfaceControlFrameTest PersistentMapFaultTest) +foreach (wiretest IN ITEMS RemoteClientTest ServerLoopTest SessionHandshakeTest StagedTextureStoreTest SurfaceControlFrameTest PersistentMapFaultTest ServerContextLiveTest) add_executable(${wiretest} ${wiretest}.cpp) target_include_directories(${wiretest} PRIVATE diff --git a/MobileGL/MG_Test/Wire/ServerContextLiveTest.cpp b/MobileGL/MG_Test/Wire/ServerContextLiveTest.cpp new file mode 100644 index 00000000..589bc9ce --- /dev/null +++ b/MobileGL/MG_Test/Wire/ServerContextLiveTest.cpp @@ -0,0 +1,68 @@ +// MobileGL - MobileGL/MG_Test/Wire/ServerContextLiveTest.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 + +// P14: MGPipeServerSetContextLive is the calling SESSION's flag. The in-process display server +// runs KWin and every Plasma client in one process; a Qt client's eglMakeCurrent(NO_CONTEXT) +// (ServerLoop's ClientRelease arm) or a session's teardown used to clear one process global, and +// KWin's next draw then read its own served context as dead (Fatal{UnmigratedPipeInput, +// "GetFramebufferBindingSlot@DrawArrays"} on the device). Each session thread resolves its own +// PipeInputs block; the flag lives there. + +#include +#include + +#include + +#include + +using namespace MobileGL; + +namespace { + thread_local MG_Pipe::PipeInputs* t_sessionBlock = nullptr; + + MG_Pipe::PipeInputs* ResolveTestSessionBlock() { return t_sessionBlock; } + + // A fresh thread, bound to `block` before its first touch of gPipeInputs - the order + // ThreadSessionScope keeps on a real session thread. + template + void OnSessionThread(MG_Pipe::PipeInputs& block, Fn fn) { + std::thread thread([&] { + t_sessionBlock = █ + fn(); + }); + thread.join(); + } +} // namespace + +TEST(ServerContextLiveTest, OneSessionsReleaseDoesNotKillANeighboursContext) { + const MG_Pipe::PipeInputsThreadResolver previous = MG_Pipe::g_pipeInputsThreadResolver; + MG_Pipe::g_pipeInputsThreadResolver = &ResolveTestSessionBlock; + auto* kwinBlock = new MG_Pipe::PipeInputs(); + auto* appBlock = new MG_Pipe::PipeInputs(); + + OnSessionThread(*kwinBlock, [] { MG_Pipe::MGPipeServerSetContextLive(true); }); + OnSessionThread(*appBlock, [] { MG_Pipe::MGPipeServerSetContextLive(true); }); + OnSessionThread(*appBlock, [] { MG_Pipe::MGPipeServerSetContextLive(false); }); + + Bool kwinLive = false; + Bool appLive = true; + OnSessionThread(*kwinBlock, [&] { kwinLive = MG_Pipe::MGPipeServerContextIsLive(); }); + OnSessionThread(*appBlock, [&] { appLive = MG_Pipe::MGPipeServerContextIsLive(); }); + EXPECT_TRUE(kwinLive) << "a neighbour session's release marked this session's context dead"; + EXPECT_FALSE(appLive); + + MG_Pipe::g_pipeInputsThreadResolver = previous; + // Leaked on purpose, like every PipeInputs block (ID-8): no thread may outlive its block. +} + +// The Wire suites link gtest without gtest_main (MG_Test/Wire/CMakeLists.txt); nothing here +// needs a log file, so this is the plain runner. +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp b/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp index 6c9ae509..9e26de9c 100644 --- a/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp +++ b/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp @@ -279,6 +279,106 @@ TEST(StagedTextureStoreTest, ResetDropAndDropAllForgetExactlyWhatTheyName) { EXPECT_FALSE(store.HasShadow(b)); } +// P14: the in-process display server serves several sessions from one store. A test-installed +// bucket probe stands in for SessionRuntime's: a thread "in" a bucket answers it, a thread in +// none answers false (the process-wide bucket). +namespace { + thread_local Server::StagedBucket t_testBucket; + thread_local Bool t_testBucketSet = false; + + Bool TestStagedBucket(Server::StagedBucket* outBucket) { + if (!t_testBucketSet) return false; + *outBucket = t_testBucket; + return true; + } + + void EnterBucket(Uint64 session, Uint64 group) { + t_testBucket = Server::StagedBucket{session, group}; + t_testBucketSet = true; + } + + struct ScopedTestBucketProbe { + Server::StagedBucketResolver Previous; + ScopedTestBucketProbe() : Previous(Server::g_stagedBucketResolver.load()) { + Server::SetStagedBucketResolver(&TestStagedBucket); + } + ~ScopedTestBucketProbe() { + t_testBucketSet = false; + Server::SetStagedBucketResolver(Previous); + } + }; +} // namespace + +// The anland Plasma crash, at unit scope: KWin and another client both hold texture {38, 0}; +// the other client's session ends (DestroyEGLContext -> DropAll), and KWin's next upload into +// the level it declared long ago used to read "arrived before the server declared texture +// level" and abort the display process. Each session's {slot, gen} is its own texture, and a +// session's DropAll takes only its own levels. +TEST(StagedTextureStoreTest, TwoSessionsHoldTheSameHandleAndOneSessionsDropAllSparesTheOther) { + ScopedTestBucketProbe probe; + Server::StagedTextureStore store(/*copies=*/true); + const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(38, 0)); + Vector kwinBytes(64, 0x11); + Vector appBytes(16, 0x22); + + EnterBucket(/*session=*/1, /*group=*/10); + store.NoteLevelDefined(key, kTex2DTarget, 0, IntVec3{4, 4, 1}); + store.Adopt(key, kTex2DTarget, 0, IntVec3{4, 4, 1}, kwinBytes.data(), kwinBytes.size()); + + EnterBucket(/*session=*/2, /*group=*/20); + EXPECT_FALSE(store.IsLevelDefined(key, kTex2DTarget, 0)) + << "another session's {slot, gen} answered for this one's"; + store.NoteLevelDefined(key, kTex2DTarget, 0, IntVec3{2, 2, 1}); + store.Adopt(key, kTex2DTarget, 0, IntVec3{2, 2, 1}, appBytes.data(), appBytes.size()); + + EnterBucket(1, 10); + EXPECT_EQ(store.LevelExtentOrUndefined(key, kTex2DTarget, 0), (IntVec3{4, 4, 1})) + << "the second session's definition redefined the first's level"; + + EnterBucket(2, 20); + store.DropAll(); + EXPECT_FALSE(store.HasShadow(key)); + + EnterBucket(1, 10); + ASSERT_TRUE(store.IsLevelDefined(key, kTex2DTarget, 0)) + << "a neighbour session's teardown dropped this session's declared level"; + EXPECT_TRUE(store.IsCovered(key, kTex2DTarget, 0)); + EXPECT_NE(store.AdoptRun(key, kTex2DTarget, 0, IntVec3{4, 4, 1}, 0, kwinBytes.data(), kwinBytes.size()), + nullptr); + + EnterBucket(1, 11); + EXPECT_FALSE(store.HasShadow(key)) << "a non-shared group of the same session is its own name space"; + + t_testBucketSet = false; + store.DropAll(); + EnterBucket(1, 10); + EXPECT_FALSE(store.HasShadow(key)) << "a thread that is no session's drops the whole store"; +} + +// The buffer half: its shadows are keyed by twin address (already unique), but DropAll ran the +// same process-wide clear on a session's teardown. +TEST(StagedTextureStoreTest, OneSessionsBufferDropAllSparesTheOthersShadows) { + ScopedTestBucketProbe probe; + Server::StagedShadowStore store(/*copies=*/true); + const int kwinTwin = 0; + const int appTwin = 0; + Vector bytes(32, 0x33); + + EnterBucket(1, 10); + store.Adopt(&kwinTwin, bytes.size(), bytes.data(), 0, bytes.size()); + EnterBucket(2, 20); + store.Adopt(&appTwin, bytes.size(), bytes.data(), 0, bytes.size()); + + store.DropAll(); + EXPECT_FALSE(store.HasShadow(&appTwin)); + EXPECT_TRUE(store.HasShadow(&kwinTwin)) << "a neighbour session's teardown dropped this session's bytes"; + EXPECT_TRUE(store.IsCovered(&kwinTwin, 0, bytes.size())); + + t_testBucketSet = false; + store.DropAll(); + EXPECT_FALSE(store.HasShadow(&kwinTwin)); +} + // T5's dirty mark: a GPU-side generation dirties the SERVER's shadow, and the mark - not the // client - answers "dirty region, level has no pending upload". It is settable and clearable // per (uploadTarget, level), independent of bytes, and inert on a monolith store.