[Fix] (Pipe): an evicted sampler CSO is deleted from every share group of its session.

This commit is contained in:
2026-10-03 05:37:53 -04:00
parent a84104a40f
commit 2acc6c41bb
3 changed files with 80 additions and 6 deletions
+6 -6
View File
@@ -576,12 +576,12 @@ namespace MobileGL::MG_Pipe {
// here - a content-addressed CSO has no frontend object whose death is being
// announced, which is precisely why this eviction is the only death path it has.
//
// THE DELETE REACHES THE BOUND SHARE GROUP ONLY, so it is sent only when that group
// holds the record; a group that was never sent the create would count
// it as a refused object call. Other groups keep an unreferenced record at this slot
// (nothing may name it: the count is 0) until a create for the slot's next
// generation is published there and starts it over, or the group is torn down.
if (IsPublishedIn(m_entries[index], MGPipeCurrentShareGroupToken())) {
// THE DELETE ENDS THE HANDLE IN EVERY SHARE GROUP IT WAS PUBLISHED TO, not only the
// bound one: the applier drops a sampler CSO from every group of its session
// (MGPipeApplyDeleteSamplerState), because the handle this frees is one identity for
// the whole session. So it is sent whenever ANY group was sent the create - and not
// at all for an entry no group holds, which the applier would count as a refusal.
if (m_entries[index].PublishedGroupCount != 0) {
MGPipeRouteDeleteSamplerState(handle);
}
// AND THE LATCH GOES WITH THE DELETE. This is the "an emitter that drops a record
+40
View File
@@ -663,6 +663,25 @@ namespace MobileGL::MG_Pipe {
return raw;
}
// Every share group's object records in the session the calling thread's records are
// applied for, or nothing on a thread that serves no session (the process-wide applier is
// then the only one). The pointers stay valid after the lock is dropped: a group's records
// are held by UniquePtr, and only this session's own apply thread adds or drops its groups.
Vector<MGPipeObjectRecords*> BoundSessionGroups() {
Vector<MGPipeObjectRecords*> groups;
const MGPipeApplierKeyResolver resolve = g_applierKeyResolver;
MGPipeApplierKey key;
if (resolve == nullptr || !resolve(&key)) return groups;
std::lock_guard<std::mutex> guard(g_applierSessionsMutex);
const auto session = g_applierSessions.find(key.SessionKey);
if (session == g_applierSessions.end()) return groups;
for (auto& [groupKey, records] : session->second.Groups) {
(void)groupKey;
if (records) groups.push_back(records.get());
}
return groups;
}
thread_local MGPipeApplierState* t_applierCached = nullptr;
thread_local Uint64 t_applierCachedSession = 0;
thread_local Uint64 t_applierCachedContext = 0;
@@ -3286,6 +3305,27 @@ namespace MobileGL::MG_Pipe {
void MGPipeApplyDeleteSamplerState(const MGPHandleOnly& handle) {
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::SamplerCso),
"delete_sampler_state on kind %u", handle.Kind);
// A SAMPLER CSO IS ONE VALUE FOR THE WHOLE SESSION, and its delete drops it from every
// share group that holds it. The client's cache is content-addressed and process-wide:
// one handle names one canonical value, published into each group that uses it (a
// record per group, all of them copies), and the cache's eviction frees the handle -
// which ends it in every group at once, whichever context happens to be bound. Dropping
// only the bound group's copy left every other group a record at a slot the client hands
// out again. The handle space is the client's, so no other object can hold this slot at
// this generation in any of the session's groups.
if (const Vector<MGPipeObjectRecords*> groups = BoundSessionGroups(); !groups.empty()) {
Bool dropped = false;
for (MGPipeObjectRecords* group : groups) {
MGPipeSamplerCsoRecord* held = FindIn(group->SamplerCsos, handle.Handle);
if (held == nullptr) continue;
const Uint32 gen = held->Gen;
*held = MGPipeSamplerCsoRecord{};
held->Gen = gen;
dropped = true;
}
if (!dropped) (void)ResolveObject(MGPipeApplier().SamplerCsos, "delete_sampler_state", handle.Handle);
return;
}
MGPipeSamplerCsoRecord* record =
ResolveObject(MGPipeApplier().SamplerCsos, "delete_sampler_state", handle.Handle);
if (record == nullptr) return;
+34
View File
@@ -908,6 +908,40 @@ namespace {
Cache().Release(second);
}
// AN EVICTION ENDS THE VALUE IN EVERY SHARE GROUP IT WAS PUBLISHED TO. The cache frees the
// handle, which is one identity for the whole session, whichever context is bound when the LRU
// runs. Red while the delete reached the bound group only: the other group kept a record at a
// slot the cache hands out again.
TEST(SamplerEmit, AnEvictionDropsTheValueFromEveryShareGroupItWasPublishedTo) {
EmitterScope scope;
ShareGroupPair groups(/*shared=*/false);
Uint64 payload = 0;
const SamplerParameters params = DistinctParameters();
groups.MakeCurrent(0);
const MGPipeHandle victim = Cache().Acquire(params, payload);
groups.MakeCurrent(1);
ASSERT_EQ(Cache().Acquire(params, payload), victim);
ASSERT_NE(BoundGroupSamplerRecord(victim), nullptr) << "the second group was not sent the create";
Cache().Release(victim);
Cache().Release(victim);
// The first group fills the cache with values nothing keeps naming: the victim is the
// least recently used entry, and the LRU takes it while the FIRST group is bound.
groups.MakeCurrent(0);
SamplerParameters filler = DistinctParameters();
for (SizeT i = 0; i < kMGPipeSamplerCsoCacheCapacity; ++i) {
filler.minLod = static_cast<float>(i) + 0.5f;
Cache().Release(Cache().Acquire(filler, payload));
}
ASSERT_FALSE(Cache().RecordIsPublished(victim)) << "the LRU did not take the victim";
EXPECT_EQ(BoundGroupSamplerRecord(victim), nullptr) << "the bound group still holds the evicted value";
groups.MakeCurrent(1);
EXPECT_EQ(BoundGroupSamplerRecord(victim), nullptr)
<< "the other share group still holds a sampler CSO the cache has freed";
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
}
// ============================ D-F2: the sampler view ============================
TEST(SamplerEmit, AnUnchangedTextureReIssuesNothing) {