mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Fix] (Pipe): count the sampler cache's unaccountable releases and its referenced evictions rather than asserting them, and state what a shared reference count, an over-capacity cache and the per-unit reconciliation really cost
This commit is contained in:
@@ -220,7 +220,29 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// Mints made with the cache already full and EVERY entry referenced. A recorded
|
// Mints made with the cache already full and EVERY entry referenced. A recorded
|
||||||
// number and not a gate (ID-17): growing past 256 is the safe direction, and this
|
// number and not a gate (ID-17): growing past 256 is the safe direction, and this
|
||||||
// is what makes it visible instead of silent.
|
// is what makes it visible instead of silent.
|
||||||
|
//
|
||||||
|
// READ IT AS "HOW FAR THE CACHE PERMANENTLY GREW", not "how often it grew". Once
|
||||||
|
// m_entries.size() is past the capacity every later Mint evicts one unreferenced
|
||||||
|
// entry and pushes one, so the size stays at its high-water mark for the life of
|
||||||
|
// the process - along with that many SamplerCso slots in the allocator and that
|
||||||
|
// many create_sampler_state records in the applier. Bounded by the peak
|
||||||
|
// simultaneous pin count, so this is memory retention and not a leak; the cache
|
||||||
|
// never shrinks back.
|
||||||
Uint64 OverCapacityMints = 0;
|
Uint64 OverCapacityMints = 0;
|
||||||
|
// A RELEASE THIS CACHE COULD NOT ACCOUNT FOR. UnknownReleases: the handle names no
|
||||||
|
// entry here (a stale handle from before a ResetForTest, a handle of another kind
|
||||||
|
// passed by mistake, a handle whose entry was already evicted). UnderflowedReleases:
|
||||||
|
// the entry was found with a count already 0. Both are contract violations by a
|
||||||
|
// HOLDER and are counted rather than absorbed, because the pin they steal is
|
||||||
|
// exactly what stops the LRU taking a handle a published record still names - see
|
||||||
|
// Release(). Recorded numbers, not gates; MOBILEGL_ASSERT could not carry them
|
||||||
|
// because it compiles out at INFO, i.e. in every gate build and every shipped one.
|
||||||
|
Uint64 UnknownReleases = 0;
|
||||||
|
Uint64 UnderflowedReleases = 0;
|
||||||
|
// ID-17's Evict invariant, as a counter for the same reason (C2-n1). Mint's victim
|
||||||
|
// choice structurally guarantees it stays 0; it is what would say so in a shipped
|
||||||
|
// build if a second caller of Evict ever appeared.
|
||||||
|
Uint64 ReferencedEvictions = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The handle for `params`' value, AND A REFERENCE ON IT. Mints and emits
|
// The handle for `params`' value, AND A REFERENCE ON IT. Mints and emits
|
||||||
@@ -253,6 +275,12 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// 64-bit XXH64 collision, so without this the whole branch is uncovered and the case
|
// 64-bit XXH64 collision, so without this the whole branch is uncovered and the case
|
||||||
// named for it proves something else. It hashes nothing and simply uses the hash it is
|
// named for it proves something else. It hashes nothing and simply uses the hash it is
|
||||||
// given; production code has no reason to call it.
|
// given; production code has no reason to call it.
|
||||||
|
//
|
||||||
|
// THE THREE MEMBERS A CONSUMER MUST NOT CALL are this one, ResetForTest() (which frees
|
||||||
|
// every slot the cache holds and drops their publication latches) and ResetCounters().
|
||||||
|
// Package B's seam is Acquire / Release / RefCountOf / RecordIsPublished / Size /
|
||||||
|
// GetCounters and nothing else - the class is public because it is header-only, not
|
||||||
|
// because all nine members are for callers.
|
||||||
MGPipeHandle AcquireWithForcedHashForTest(const SamplerParameters& params, Uint64 forcedHash,
|
MGPipeHandle AcquireWithForcedHashForTest(const SamplerParameters& params, Uint64 forcedHash,
|
||||||
Uint64& payloadBytes) {
|
Uint64& payloadBytes) {
|
||||||
SamplerParameters canon;
|
SamplerParameters canon;
|
||||||
@@ -262,16 +290,31 @@ namespace MobileGL::MG_Pipe {
|
|||||||
|
|
||||||
// Drops one reference. The entry stays - a released value is still worth reusing - it
|
// Drops one reference. The entry stays - a released value is still worth reusing - it
|
||||||
// merely becomes eligible for the LRU again. Releasing a handle this cache never handed
|
// merely becomes eligible for the LRU again. Releasing a handle this cache never handed
|
||||||
// out, or releasing one twice, is a no-op rather than an underflow: the death paths that
|
// out, or releasing one twice, does not underflow: the death paths that call it are
|
||||||
// call it are idempotent by contract and a second call must not corrupt the count.
|
// idempotent by contract and a second call must not corrupt the count.
|
||||||
|
//
|
||||||
|
// BUT THE COUNT IS PER HANDLE, NOT PER HOLDER, and that is the sentence a holder has to
|
||||||
|
// read. Entries are content-addressed and shared by design - package B's texture-params
|
||||||
|
// record and this file's own bind_sampler_states will routinely name the SAME handle,
|
||||||
|
// each owing its own Release - so a holder that releases twice is not harmlessly
|
||||||
|
// repeating itself: it takes ANOTHER holder's pin, the entry can reach RefCount 0 while
|
||||||
|
// a published MGPTextureParams and a standing bind_sampler_states set still name it,
|
||||||
|
// and the LRU may then evict it with nothing to refuse and nothing to re-emit. This
|
||||||
|
// cannot be repaired here, so it is COUNTED (UnknownReleases / UnderflowedReleases)
|
||||||
|
// rather than absorbed silently, and that is what a reviewer of a holder asserts on.
|
||||||
void Release(MGPipeHandle handle) {
|
void Release(MGPipeHandle handle) {
|
||||||
if (MGPipeHandleIsNull(handle)) return;
|
if (MGPipeHandleIsNull(handle)) return;
|
||||||
for (Entry& entry : m_entries) {
|
for (Entry& entry : m_entries) {
|
||||||
if (entry.Cso != handle) continue;
|
if (entry.Cso != handle) continue;
|
||||||
if (entry.RefCount > 0) --entry.RefCount;
|
if (entry.RefCount > 0) {
|
||||||
|
--entry.RefCount;
|
||||||
|
} else {
|
||||||
|
++m_counters.UnderflowedReleases;
|
||||||
|
}
|
||||||
++m_counters.Releases;
|
++m_counters.Releases;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
++m_counters.UnknownReleases;
|
||||||
}
|
}
|
||||||
|
|
||||||
// How many live references this cache is holding for `handle`. Diagnostics and unit
|
// How many live references this cache is holding for `handle`. Diagnostics and unit
|
||||||
@@ -431,9 +474,14 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// whole of ID-17's ruling: a handle a standing record names cannot be taken, because
|
// whole of ID-17's ruling: a handle a standing record names cannot be taken, because
|
||||||
// the applier does not resolve MGPTextureParams::BuiltinSampler and an eviction is not
|
// the applier does not resolve MGPTextureParams::BuiltinSampler and an eviction is not
|
||||||
// a parameter change, so nothing would refuse and nothing would re-emit.
|
// a parameter change, so nothing would refuse and nothing would re-emit.
|
||||||
|
//
|
||||||
|
// COUNTED, NOT ASSERTED (C2-n1). MOBILEGL_ASSERT is live only at DEBUG, so an assertion
|
||||||
|
// here would be absent from all three gate builds and from every shipped build - the
|
||||||
|
// idiom D-J3 forbids and the one C-m6 removed from the program emitter. The eviction
|
||||||
|
// still proceeds when the count is non-zero: the sole caller guarantees the condition,
|
||||||
|
// and refusing here would leave Mint without the room it asked for.
|
||||||
void Evict(SizeT index) {
|
void Evict(SizeT index) {
|
||||||
MOBILEGL_ASSERT(m_entries[index].RefCount == 0,
|
if (m_entries[index].RefCount != 0) ++m_counters.ReferencedEvictions;
|
||||||
"a referenced sampler CSO was evicted; the LRU must skip pinned entries");
|
|
||||||
MGPHandleOnly handle{};
|
MGPHandleOnly handle{};
|
||||||
handle.Handle = m_entries[index].Cso;
|
handle.Handle = m_entries[index].Cso;
|
||||||
handle.Kind = static_cast<Uint32>(MGPipeKind::SamplerCso);
|
handle.Kind = static_cast<Uint32>(MGPipeKind::SamplerCso);
|
||||||
@@ -751,10 +799,25 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// A BOUND SAMPLER STATE HOLDS A REFERENCE ON ITS CSO FOR AS LONG AS IT IS BOUND
|
// A BOUND SAMPLER STATE HOLDS A REFERENCE ON ITS CSO FOR AS LONG AS IT IS BOUND
|
||||||
// (ID-17). The set the applier holds is "the last set as received" and outlives the
|
// (ID-17). The set the applier holds is "the last set as received" and outlives the
|
||||||
// pass that sent it, so a handle in it must not become the LRU's victim; the reference
|
// pass that sent it, so a handle in it must not become the LRU's victim; the reference
|
||||||
// is what makes that true. The reconciliation below is one pass over the unit array:
|
// is what makes that true. The reconciliation below walks the unit array once: Acquire
|
||||||
// Acquire has already taken a reference for every unit in the new set, so a unit whose
|
// has already taken a reference for every unit in the new set, so a unit whose handle
|
||||||
// handle did not move gives that duplicate back, a unit whose handle moved gives back
|
// did not move gives that duplicate back, a unit whose handle moved gives back the one
|
||||||
// the one it used to hold, and a unit that fell outside the window gives back its own.
|
// it used to hold, and a unit that fell outside the window gives back its own.
|
||||||
|
//
|
||||||
|
// ITS COST IS NOT "ONE EXTRA ARRAY WALK" and the ID-19 budget entry says so. Each
|
||||||
|
// Release of a NON-NULL handle is itself a linear scan of the whole cache, sitting
|
||||||
|
// beside an Acquire probe that is another, so a pass in which K units hold sampler CSOs
|
||||||
|
// costs 2 * K * O(cacheSize) handle comparisons - up to ~98 000 at K == 192 with a full
|
||||||
|
// cache, and unbounded above that because the over-capacity path lets the cache grow
|
||||||
|
// past 256. In Minecraft-shaped workloads K is typically 0: no unit has a SamplerObject
|
||||||
|
// bound, so Acquire is never called and Release(null) returns immediately. The workload
|
||||||
|
// that has both a large K and a large cache is a CTS sampler sweep. c0d moved the
|
||||||
|
// firing rate underneath this as well - bit 13's shutter now mixes
|
||||||
|
// GetTextureBindGeneration(), so this runs on EVERY texture bind at any unit and not
|
||||||
|
// only on a parameter change, and the set-hash suppressor below stops the record going
|
||||||
|
// out but not these two scans, which happen first. The per-unit (sampler lifetime id,
|
||||||
|
// ctx.GetSamplingResolutionGeneration()) latch recorded under ID-19 as C-m2 removes
|
||||||
|
// BOTH scans, not one.
|
||||||
Uint64 EmitSamplerStates(GLContext& ctx) {
|
Uint64 EmitSamplerStates(GLContext& ctx) {
|
||||||
const Int maxTouched = ctx.GetMaxTouchedTextureUnit();
|
const Int maxTouched = ctx.GetMaxTouchedTextureUnit();
|
||||||
const Uint32 count =
|
const Uint32 count =
|
||||||
|
|||||||
Reference in New Issue
Block a user