mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-18 00:58:30 +09:00
[Fix] (Pipe): key the composite resolver's memo on the context as well as the pipeline's GL name - the resolver is a process singleton while GL names are per context, so a make-current released the other context's live composite
This commit is contained in:
@@ -39,10 +39,39 @@
|
||||
// no-op: MGPipeSlotAllocator::Free refuses a slot that is not live at that generation and
|
||||
// bumps no generation of its own, so a double release cannot skip a generation either.
|
||||
//
|
||||
// THE KEY IS ComputeDrawProgramSignature(), the per-graphics-stage {lifetimeId, GetLinkVersion()}
|
||||
// array - and DELIBERATELY NOT GetBackendStateVersion(), which is what made the SSO
|
||||
// conformance loop rebuild the composite (glslang + SPIR-V + spirv-opt) on every draw, because
|
||||
// a glUniform1i to a sampler moves it.
|
||||
// THE MEMO's KEY IS (CONTEXT ID, PIPELINE GL NAME) AND THE CONTEXT HALF IS NOT OPTIONAL.
|
||||
// This resolver is a PROCESS singleton while a pipeline's GL name is per context: GLContext
|
||||
// owns m_programPipelines AND its own name generator m_programPipelineNames (Core.h), so name
|
||||
// N names two different ProgramPipelineObjects in two contexts, each with its own composite
|
||||
// and its own handle. Keyed on the name alone, the first emission after a make-current found
|
||||
// the OTHER context's entry, matched nothing - two composites are two ProgramObjects with two
|
||||
// lifetime ids, so the handles differ even when the stage set and the signature are identical
|
||||
// - and released it: a delete_shader_state and a cleared publication latch for a composite
|
||||
// whose frontend ProgramObject is alive, its band slot handed back and re-issued at gen + 1,
|
||||
// and the server rebuilding that program (glslang + SPIR-V + spirv-opt, the very cost the
|
||||
// signature below exists to avoid) once per context switch.
|
||||
//
|
||||
// THE CONTEXT ID IS GLContext::GetTextureContextId() AND NOTHING ELSE - the tree's existing
|
||||
// never-reused per-context id (TextureState::AllocateContextId; PipeInputs carries it as
|
||||
// m_textureContextId at seven fill points and the backends' own per-context memos key on it).
|
||||
// Deliberately NOT the GLContext ADDRESS that MGB_CTX_IDENTITY and MGPipeTracker::m_context
|
||||
// compare, because Core.h states the reason that id exists at all: a context freed and remade
|
||||
// lands on the old heap address, which would put this same defect back one context recreation
|
||||
// later.
|
||||
//
|
||||
// WHAT RELEASES A DESTROYED CONTEXT's ENTRIES: nothing in this file, and that is the correct
|
||||
// answer rather than an omission. Destroying a context drops m_programPipelines, which drops
|
||||
// each ProgramPipelineObject, which drops the composite it cached; ~ProgramObject then runs
|
||||
// MGPipeEmitShaderCsoDestroyAndFree - the composite's OWN release path, the second of the two
|
||||
// above - and the slot goes back exactly once. The entries those composites leave behind can
|
||||
// never be found again (no future Observe can carry a dead context id) and could not release
|
||||
// anything if they were (the allocator erases the lifetime-id mapping on Free), so Reset()
|
||||
// DROPS them instead of releasing them. That is also what bounds the vector; see Reset().
|
||||
//
|
||||
// THE SIGNATURE IS ComputeDrawProgramSignature(), the per-graphics-stage {lifetimeId,
|
||||
// GetLinkVersion()} array - and DELIBERATELY NOT GetBackendStateVersion(), which is what made
|
||||
// the SSO conformance loop rebuild the composite (glslang + SPIR-V + spirv-opt) on every draw,
|
||||
// because a glUniform1i to a sampler moves it.
|
||||
//
|
||||
// HEADER-ONLY, for the ownership reason Tracker.h states: a new .cpp would need the root
|
||||
// CMakeLists.txt, which is the contract package's.
|
||||
@@ -84,6 +113,11 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 Mints = 0; // signatures this resolver has seen minted
|
||||
Uint64 Reuses = 0; // a signature that had not moved
|
||||
Uint64 Releases = 0; // signature-move releases, i.e. the pipeline-cache path
|
||||
// Entries dropped by Reset() because the composite's slot was already gone - the
|
||||
// shape every entry of a DESTROYED CONTEXT ends in. A dropped entry is not a
|
||||
// release: nothing is emitted and nothing is freed, the obligation having been
|
||||
// discharged by the composite's own ~ProgramObject.
|
||||
Uint64 Sweeps = 0;
|
||||
};
|
||||
|
||||
// Told, at every emission, which composite the frontend handed out for which pipeline.
|
||||
@@ -95,63 +129,81 @@ namespace MobileGL::MG_Pipe {
|
||||
// one death helper and in its fixed order. That is the pipeline-cache release path; the
|
||||
// composite's own destructor is the other one and the second of the two is the proven
|
||||
// no-op.
|
||||
MGPipeHandle Observe(const ProgramPipelineObject& pipeline, const ProgramObject& composite,
|
||||
MGPipeHandle handle) {
|
||||
MGPipeHandle Observe(Uint64 contextId, const ProgramPipelineObject& pipeline,
|
||||
const ProgramObject& composite, MGPipeHandle handle) {
|
||||
const DrawProgramSignature signature = pipeline.ComputeDrawProgramSignature();
|
||||
const Uint key = pipeline.GetExternalIndex();
|
||||
Entry* entry = Find(key);
|
||||
const Uint pipelineName = pipeline.GetExternalIndex();
|
||||
Entry* entry = Find(contextId, pipelineName);
|
||||
if (entry != nullptr) {
|
||||
if (entry->Signature == signature && entry->Handle == handle) {
|
||||
// THE SAME COMPOSITE. Not merely "the same signature": the handle is minted
|
||||
// off the composite ProgramObject's own lifetime id, so an identical handle
|
||||
// IS an identical object, and there is nothing to release whatever the
|
||||
// memo's freshness says. Re-arming Fresh here is what a Reset() costs - one
|
||||
// re-observation - and Live is deliberately NOT touched, because it is the
|
||||
// release obligation and it is still owed for exactly this handle.
|
||||
entry->Fresh = true;
|
||||
// IS an identical object and there is nothing to release. Live is
|
||||
// deliberately NOT touched - it is the release obligation and it is still
|
||||
// owed for exactly this handle.
|
||||
++m_counters.Reuses;
|
||||
return handle;
|
||||
}
|
||||
// A MOVED SIGNATURE ON THIS CONTEXT's OWN ENTRY, which is the only thing that
|
||||
// can reach here now: another context's pipeline of the same name is not found
|
||||
// above and therefore not released, its obligation staying owed to the context
|
||||
// that took it.
|
||||
ReleaseEntry(*entry);
|
||||
} else {
|
||||
m_entries.push_back(Entry{});
|
||||
entry = &m_entries.back();
|
||||
entry->PipelineName = key;
|
||||
entry->ContextId = contextId;
|
||||
entry->PipelineName = pipelineName;
|
||||
}
|
||||
entry->Signature = signature;
|
||||
entry->Handle = handle;
|
||||
entry->CompositeLifetimeId = composite.GetLifetimeId();
|
||||
entry->Live = true;
|
||||
entry->Fresh = true;
|
||||
++m_counters.Mints;
|
||||
return handle;
|
||||
}
|
||||
|
||||
// A make-current. The entries name composites that belong to the frontend objects of
|
||||
// the context being left, and those objects outlive the switch, so the RECORDS are not
|
||||
// released here - releasing them would emit a delete for a live program.
|
||||
// A make-current, and it RELEASES NOTHING. The entries name composites that belong to
|
||||
// the frontend objects of the context being left, those objects outlive the switch, and
|
||||
// releasing them would emit a delete for a live program.
|
||||
//
|
||||
// ONLY THE MEMO's FRESHNESS IS DROPPED, AND `Fresh` IS A SEPARATE FLAG FROM `Live` FOR
|
||||
// EXACTLY THAT REASON. The two were one flag and the conflation was a real defect: the
|
||||
// reuse branch above returns before anything could restore it, so after the first
|
||||
// make-current every entry stayed at false for the life of the process and ReleaseEntry
|
||||
// early-returned for ever - no delete_shader_state, no Free, no counter, and the old
|
||||
// composite's handle simply overwritten out of the resolver. `Live` is a RELEASE
|
||||
// OBLIGATION and nothing but ReleaseEntry may clear it; `Fresh` is the memo's own "does
|
||||
// this entry describe the current context's pipeline of this name", which costs at most
|
||||
// one re-observation when it is wrong.
|
||||
// NOR IS ANY MEMO INVALIDATED, and that is what the context key bought. This used to
|
||||
// clear a per-entry `Fresh` flag beside `Live`, because with a name-only key an entry
|
||||
// could not say whether it described "my own pipeline before the switch" or "another
|
||||
// context's pipeline of the same name" - and exactly one of those two properties could
|
||||
// hold at a time. The key answers the question directly now, so the freshness flag and
|
||||
// its one reader (a HandleFor() accessor that had no caller anywhere in the tree) are
|
||||
// both gone rather than left as scaffolding: `Live`, the release obligation, is the
|
||||
// entry's only state and nothing but ReleaseEntry may clear it.
|
||||
//
|
||||
// WHAT IS LEFT TO DO HERE IS RECLAMATION, and this is the one moment the client is told
|
||||
// that a context boundary was crossed. An entry whose composite slot is no longer live
|
||||
// has had its obligation discharged elsewhere - by that composite's own ~ProgramObject,
|
||||
// which is precisely what happened to EVERY entry of a context that has just been
|
||||
// destroyed - so it is DROPPED rather than released: a release would resolve nothing
|
||||
// anyway (the allocator erases the lifetime-id mapping on Free) and no reader is left.
|
||||
// Without this the vector would grow by one per (context, pipeline name) pair the
|
||||
// process ever used, where the name-only key bounded it by the highest pipeline name;
|
||||
// with it, it is bounded by the pairs whose composite slot is actually live.
|
||||
void Reset() {
|
||||
for (Entry& entry : m_entries) entry.Fresh = false;
|
||||
SizeT kept = 0;
|
||||
for (SizeT i = 0; i < m_entries.size(); ++i) {
|
||||
if (!m_entries[i].Live || !MGPipeSlots().IsLive(MGPipeKind::ShaderCso, m_entries[i].Handle)) {
|
||||
++m_counters.Sweeps;
|
||||
continue;
|
||||
}
|
||||
if (kept != i) m_entries[kept] = m_entries[i];
|
||||
++kept;
|
||||
}
|
||||
m_entries.resize(kept);
|
||||
}
|
||||
|
||||
void ResetCounters() { m_counters = Counters{}; }
|
||||
|
||||
MGPipeHandle HandleFor(Uint pipelineName) const {
|
||||
for (const Entry& entry : m_entries) {
|
||||
if (entry.PipelineName == pipelineName && entry.Live && entry.Fresh) return entry.Handle;
|
||||
}
|
||||
return kMGPipeNullHandle;
|
||||
}
|
||||
// Diagnostics and unit cases only; nothing on the emission path asks. There is no
|
||||
// HandleFor(name) accessor and there must not be one: the emitter takes the handle from
|
||||
// the composite ProgramObject it already holds, so a lookup by name would be a second
|
||||
// authority on an identity the allocator already owns.
|
||||
SizeT Size() const { return m_entries.size(); }
|
||||
const Counters& GetCounters() const { return m_counters; }
|
||||
|
||||
@@ -161,17 +213,21 @@ namespace MobileGL::MG_Pipe {
|
||||
// choice: a static that held one would put a frontend destructor on an exit
|
||||
// handler's path into a torn-down pipe. A GL name, a signature of plain integers,
|
||||
// a handle and a lifetime id are all this needs.
|
||||
// KEYED ON THE GL NAME, because a ProgramPipelineObject has no lifetime id -
|
||||
// ComputeDrawProgramSignature reads the STAGE programs' ids and the pipeline itself
|
||||
// carries none. glGenProgramPipelines recycles names, so a deleted-and-recreated
|
||||
// pipeline can inherit its predecessor's entry; that is bounded and self-correcting
|
||||
// rather than a hazard. The first Observe on the new object finds a signature and a
|
||||
// handle that do not match and releases the old entry, and that release resolves
|
||||
// NOTHING - the allocator erases the lifetime-id mapping on Free, so a stale
|
||||
// CompositeLifetimeId emits no delete and frees no slot; all it costs is one
|
||||
// redundant, idempotent death notice, which is the same shape the composite's own
|
||||
// second release path already has. The vector is keyed by name, so it cannot grow
|
||||
// past the highest pipeline name the process ever used.
|
||||
// KEYED ON (CONTEXT ID, GL NAME), and the name half is the GL name because a
|
||||
// ProgramPipelineObject has no lifetime id - ComputeDrawProgramSignature reads the
|
||||
// STAGE programs' ids and the pipeline itself carries none. The context half is
|
||||
// GLContext::GetTextureContextId(); see the file header for why the name alone was
|
||||
// wrong and why the context ADDRESS would be too.
|
||||
//
|
||||
// WITHIN ONE CONTEXT glGenProgramPipelines recycles names, so a deleted-and-
|
||||
// recreated pipeline can still inherit its predecessor's entry; that is bounded and
|
||||
// self-correcting rather than a hazard. The first Observe on the new object finds a
|
||||
// signature and a handle that do not match and releases the old entry, and that
|
||||
// release resolves NOTHING - the allocator erases the lifetime-id mapping on Free,
|
||||
// so a stale CompositeLifetimeId emits no delete and frees no slot; all it costs is
|
||||
// one redundant, idempotent death notice, which is the same shape the composite's
|
||||
// own second release path already has.
|
||||
Uint64 ContextId = 0;
|
||||
Uint PipelineName = 0;
|
||||
DrawProgramSignature Signature{};
|
||||
MGPipeHandle Handle = kMGPipeNullHandle;
|
||||
@@ -179,13 +235,13 @@ namespace MobileGL::MG_Pipe {
|
||||
// THE RELEASE OBLIGATION. Set when this entry takes responsibility for a composite's
|
||||
// slot, cleared ONLY by ReleaseEntry when that responsibility is discharged.
|
||||
Bool Live = false;
|
||||
// THE MEMO's FRESHNESS, and deliberately not the same flag as Live - see Reset().
|
||||
Bool Fresh = false;
|
||||
};
|
||||
|
||||
Entry* Find(Uint pipelineName) {
|
||||
// BOTH HALVES OF THE KEY, always. An entry of another context is not this pipeline's
|
||||
// entry: not found, not matched, not released.
|
||||
Entry* Find(Uint64 contextId, Uint pipelineName) {
|
||||
for (Entry& entry : m_entries) {
|
||||
if (entry.PipelineName == pipelineName) return &entry;
|
||||
if (entry.ContextId == contextId && entry.PipelineName == pipelineName) return &entry;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -115,7 +115,14 @@ namespace MobileGL::MG_Pipe {
|
||||
// allocator refuses a slot that is not live at that generation.
|
||||
if (drawProgram && MGPipeProgramIsPipelineComposite(*drawProgram)) {
|
||||
if (const auto& pipeline = ctx.GetBoundProgramPipeline()) {
|
||||
MGPipeCompositeResolverInstance().Observe(*pipeline, *drawProgram, drawCso);
|
||||
// THE CONTEXT IS PART OF THE RESOLVER's KEY and this is the only place that
|
||||
// supplies it: the resolver is a process singleton and a pipeline's GL name
|
||||
// is per context, so without it a make-current between two contexts holding
|
||||
// one pipeline name released the other context's LIVE composite.
|
||||
// GetTextureContextId() is the tree's never-reused per-context id, the same
|
||||
// one PipeInputs carries and the backends' per-context memos key on.
|
||||
MGPipeCompositeResolverInstance().Observe(ctx.GetTextureContextId(), *pipeline,
|
||||
*drawProgram, drawCso);
|
||||
}
|
||||
}
|
||||
const MGPipeHandle dispatchCso =
|
||||
|
||||
Reference in New Issue
Block a user