[Fix] (P14): scope the server staged stores and the context-live flag to the calling session

This commit is contained in:
2026-10-02 00:04:00 -04:00
parent cbb87b968a
commit 30fef23d52
8 changed files with 317 additions and 26 deletions
+6 -5
View File
@@ -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<const void*>(static_cast<std::uintptr_t>(
(serial << 1) | Uint64{1})));
}
+6
View File
@@ -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{};
@@ -12,6 +12,7 @@
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Remote/FatalFunnel.h>
#include <MG_Remote/Server/StagedShadow.h>
#include <MG_Remote/Wire/PipeWireCodec.h>
#include <atomic>
@@ -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; }
+50 -1
View File
@@ -45,12 +45,44 @@
#include <MG_Util/Math/VectorTypes.h>
#include <algorithm>
#include <atomic>
#include <cstdlib>
#include <cstring>
#include <mutex>
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<StagedBucketResolver> 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<std::mutex> 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<SizeT>(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<std::mutex> 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<Uint8> Bytes;
// Sorted, disjoint, EXACT.
Vector<Range1D> Covered;
Uint64 SessionKey = 0;
};
const Bool m_copies;
+73 -19
View File
@@ -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<std::mutex> 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<std::mutex> 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<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D),
Uint32 internalFormat = static_cast<Uint32>(TextureInternalFormat::RGBA8)) {
if (!m_copies) return;
const ShadowKey bucketed = Bucketed(key);
const std::lock_guard<std::mutex> lock(m_mutex);
Uint64 byteBound = 0;
const Bool supportsSubData = StagedTextureTargetSupportsSubData(resourceTarget);
@@ -424,7 +427,7 @@ namespace MobileGL::MG_Remote::Server {
static_cast<unsigned long long>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(m_mutex);
@@ -717,10 +733,48 @@ namespace MobileGL::MG_Remote::Server {
}
SizeT TrackedLevelCount(Uint64 key) const {
const std::lock_guard<std::mutex> 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<SizeT>(mixed);
}
};
static ShadowKey Bucketed(Uint64 key) {
const StagedBucket bucket = CurrentStagedBucket();
return ShadowKey{bucket.SessionKey, bucket.ShareGroupKey, key};
}
template <typename Map>
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<Uint32>(uploadTarget) << 16) | static_cast<Uint32>(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<Uint64, TextureShadow> m_shadows;
ska::flat_hash_map<ShadowKey, TextureShadow, ShadowKeyHash> 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<Uint64, Bool> m_driverWritten;
ska::flat_hash_map<ShadowKey, Bool, ShadowKeyHash> 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<Bool> m_any{false};
+1 -1
View File
@@ -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
@@ -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 <Config.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <gtest/gtest.h>
#include <thread>
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 <typename Fn>
void OnSessionThread(MG_Pipe::PipeInputs& block, Fn fn) {
std::thread thread([&] {
t_sessionBlock = &block;
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();
}
@@ -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<Uint8> kwinBytes(64, 0x11);
Vector<Uint8> 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<Uint8> 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.