diff --git a/MobileGL/MG_Impl/Pipe/SamplerEmit.h b/MobileGL/MG_Impl/Pipe/SamplerEmit.h index e08694d2..8cbe3782 100644 --- a/MobileGL/MG_Impl/Pipe/SamplerEmit.h +++ b/MobileGL/MG_Impl/Pipe/SamplerEmit.h @@ -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 diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index a5cc73a1..fbba5d93 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -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 BoundSessionGroups() { + Vector groups; + const MGPipeApplierKeyResolver resolve = g_applierKeyResolver; + MGPipeApplierKey key; + if (resolve == nullptr || !resolve(&key)) return groups; + std::lock_guard 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(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 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; diff --git a/MobileGL/MG_Test/Pipe/SamplerEmitTest.cpp b/MobileGL/MG_Test/Pipe/SamplerEmitTest.cpp index 3495a8ae..6fa8c50b 100644 --- a/MobileGL/MG_Test/Pipe/SamplerEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/SamplerEmitTest.cpp @@ -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(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) {