mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Fix] (P14): scope the server staged stores and the context-live flag to the calling session
This commit is contained in:
@@ -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})));
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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 = █
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user