mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
[Fix] (Pipe): land the birth half of the P4a client API behind a publication latch both halves read, gate every emitter on its family's wired constant, raise the sampler view's death notice and count the shader composite band apart from the ordinary slots
This commit is contained in:
@@ -859,6 +859,259 @@ namespace MobileGL::MG_Pipe {
|
||||
return published;
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// P4a: the BIRTH half - the gate, the four mints, the publication latch and the seam
|
||||
// ================================================================================
|
||||
//
|
||||
// Declared in MG_Pipe/PipeMutation.h, which is the one door MG_State has into the client
|
||||
// (the closure gate's mutation-header probe keeps it a declaration), and defined here for
|
||||
// the reason every other client-side emission point is: this file is package A's for the
|
||||
// whole phase, so the gate is written ONCE and the packages that own the emitters never
|
||||
// edit it.
|
||||
namespace {
|
||||
using MG_State::GLState::FramebufferObject;
|
||||
using MG_State::GLState::ITextureObject;
|
||||
using MG_State::GLState::ProgramObject;
|
||||
using MG_State::GLState::RenderbufferObject;
|
||||
using MG_State::GLState::SamplerObject;
|
||||
|
||||
// THE SAME PAIR `wants()` APPLIES TO EVERY EMISSION at the validate point, and it is
|
||||
// deliberately the same predicate rather than a second copy of it: the operator's
|
||||
// per-subsystem A/B bit in MOBILEGL_PIPE_PUSH, AND this build having WIRED the family
|
||||
// at all. The second half is the family's own kMGPipeWired*Subsystem constant, which
|
||||
// lives in the family's emit header and is 0 until the commit that gives the emitter
|
||||
// its body - so a client path that lands before its emitter does is inert by
|
||||
// construction rather than by everyone remembering to check.
|
||||
Bool FamilyIsLive(Uint64 subsystem, Uint64 wired) {
|
||||
return (MG_Config::Features.PipePush & subsystem) != 0 && (wired & subsystem) != 0;
|
||||
}
|
||||
|
||||
// ---- THE FAMILY SEAM ----
|
||||
//
|
||||
// The forwarding from a birth hook to its family's emitter has to be written HERE,
|
||||
// once, against an emitter whose entry point does not exist yet: A owns this file for
|
||||
// the whole phase and B/C own the five emit headers, and neither may edit the other's.
|
||||
// A plain call would not compile against the stub emitter and a runtime `if` would not
|
||||
// link. So the call is made from a TEMPLATE whose `if constexpr` condition is the
|
||||
// family's own wired constant, passed as a template ARGUMENT so the condition is
|
||||
// value-dependent: while the constant is 0 the statement is discarded and never
|
||||
// instantiated, so this tree compiles against the stubs; the moment a family sets its
|
||||
// constant the statement instantiates and a missing or misspelled entry point is a
|
||||
// COMPILE ERROR in that family's own commit rather than a surprise at the merge. That
|
||||
// is the same property the four `kMGPipeWired*Subsystem == 0 || == its own bit`
|
||||
// asserts below give, one level further in.
|
||||
//
|
||||
// `call` must be a GENERIC lambda - `[&](auto& emitter) { ... }` - so its body is
|
||||
// checked at instantiation and not at definition. A non-generic one would be checked
|
||||
// here and would defeat the whole seam.
|
||||
template <Uint64 kWired, class Emitter, class Fn>
|
||||
constexpr void ForwardWhenWired(Emitter& emitter, Fn&& call) {
|
||||
if constexpr (kWired != 0) {
|
||||
call(emitter);
|
||||
} else {
|
||||
(void)emitter;
|
||||
(void)call;
|
||||
}
|
||||
}
|
||||
|
||||
// THE SEAM'S POSITIVE CONTROL, and it is not decoration: every use of it in this tree
|
||||
// passes a constant that is 0, so the TAKEN arm is never instantiated here and a seam
|
||||
// that failed to compile or failed to call would be discovered by package B or C
|
||||
// rather than by the commit that wrote it. This drives both arms against a probe
|
||||
// emitter shaped like the ones the emit headers will carry, and asserts that exactly
|
||||
// one call happened - so "discarded when 0, called when set" is a checked property of
|
||||
// this build rather than a claim in the paragraph above.
|
||||
struct SeamProbeEmitter {
|
||||
Uint32 Calls = 0;
|
||||
constexpr void Probe() { ++Calls; }
|
||||
};
|
||||
|
||||
constexpr Bool SeamForwardsExactlyWhenWired() {
|
||||
SeamProbeEmitter probe{};
|
||||
ForwardWhenWired<1ull>(probe, [](auto& emitter) { emitter.Probe(); });
|
||||
ForwardWhenWired<0ull>(probe, [](auto& emitter) { emitter.Probe(); });
|
||||
return probe.Calls == 1;
|
||||
}
|
||||
|
||||
static_assert(SeamForwardsExactlyWhenWired(),
|
||||
"the family seam must forward exactly when its wired constant is non-zero");
|
||||
|
||||
// ---- THE PUBLICATION LATCH (D-I1) ----
|
||||
//
|
||||
// "Did a create for exactly this handle actually go out?" - asked by the six death
|
||||
// helpers below and answered by whatever emitted the create. It exists because the
|
||||
// create is gated at its call site and the destroy inside the helper, so the two ask
|
||||
// the same question at two different moments; and because A SLOT IS NOT EVIDENCE OF A
|
||||
// RECORD - a backend twin table mints one through MGPipeSlots().Acquire whether or not
|
||||
// the subsystem ever asked this client to emit anything, which is exactly what a
|
||||
// MOBILEGL_PIPE_PUSH lane with P4a's bits clear runs, and a delete_* on such a handle
|
||||
// is a refused call the applier asserts on in a verify build.
|
||||
//
|
||||
// KEYED BY {kind, slot, gen}, so a recycled slot cannot inherit its predecessor's
|
||||
// answer - the same reason the identity carries a generation at all.
|
||||
//
|
||||
// THE ShaderCso COMPOSITE BAND GETS A TABLE OF ITS OWN, exactly as the allocator's
|
||||
// does and for the same arithmetic: the band's base is 983040, so a single composite
|
||||
// in a slot-indexed vector would allocate ~983k entries. Anything that indexes a
|
||||
// ShaderCso slot must test MGPipeIsCompositeShaderSlot(slot) FIRST; this is the
|
||||
// client-side worked example of that rule.
|
||||
class MGPipePublicationLatch {
|
||||
public:
|
||||
void NotePublished(MGPipeKind kind, MGPipeHandle handle) {
|
||||
Entry* entry = Grow(kind, handle.Slot);
|
||||
if (entry == nullptr) return;
|
||||
entry->Gen = handle.Gen;
|
||||
entry->Published = true;
|
||||
}
|
||||
|
||||
Bool IsPublished(MGPipeKind kind, MGPipeHandle handle) const {
|
||||
const Entry* entry = Find(kind, handle.Slot);
|
||||
return entry != nullptr && entry->Published && entry->Gen == handle.Gen;
|
||||
}
|
||||
|
||||
void NoteUnpublished(MGPipeKind kind, MGPipeHandle handle) {
|
||||
Entry* entry = const_cast<Entry*>(Find(kind, handle.Slot));
|
||||
if (entry == nullptr || entry->Gen != handle.Gen) return;
|
||||
*entry = Entry{};
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
Uint32 Gen = 0;
|
||||
Bool Published = false;
|
||||
};
|
||||
|
||||
static constexpr SizeT kKindCount = static_cast<SizeT>(MGPipeKind::KindCount);
|
||||
|
||||
Bool IsBand(MGPipeKind kind, Uint32 slot) const {
|
||||
return kind == MGPipeKind::ShaderCso && MGPipeIsCompositeShaderSlot(slot);
|
||||
}
|
||||
|
||||
Entry* Grow(MGPipeKind kind, Uint32 slot) {
|
||||
const SizeT index = static_cast<SizeT>(kind);
|
||||
if (index >= kKindCount) return nullptr;
|
||||
if (IsBand(kind, slot)) {
|
||||
const SizeT banded = slot - kMGPipeShaderCsoCompositeSlotBase;
|
||||
if (banded >= m_band.size()) m_band.resize(banded + 1);
|
||||
return &m_band[banded];
|
||||
}
|
||||
Vector<Entry>& table = m_kinds[index];
|
||||
if (slot >= table.size()) table.resize(static_cast<SizeT>(slot) + 1);
|
||||
return &table[slot];
|
||||
}
|
||||
|
||||
const Entry* Find(MGPipeKind kind, Uint32 slot) const {
|
||||
const SizeT index = static_cast<SizeT>(kind);
|
||||
if (index >= kKindCount) return nullptr;
|
||||
if (IsBand(kind, slot)) {
|
||||
const SizeT banded = slot - kMGPipeShaderCsoCompositeSlotBase;
|
||||
return banded < m_band.size() ? &m_band[banded] : nullptr;
|
||||
}
|
||||
const Vector<Entry>& table = m_kinds[index];
|
||||
return slot < table.size() ? &table[slot] : nullptr;
|
||||
}
|
||||
|
||||
Array<Vector<Entry>, kKindCount> m_kinds{};
|
||||
Vector<Entry> m_band{};
|
||||
};
|
||||
|
||||
MGPipePublicationLatch& PublicationLatch() {
|
||||
// NEVER DESTROYED, for MGPipeSlots()' reason: the six death helpers reach this
|
||||
// from frontend destructors that __run_exit_handlers drives AFTER a function-local
|
||||
// static would have gone, and a destroyed latch answers out of freed vectors.
|
||||
static MGPipePublicationLatch* latch = new MGPipePublicationLatch();
|
||||
return *latch;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void MGPipeNoteHandlePublished(MGPipeKind kind, MGPipeHandle handle) {
|
||||
if (MGPipeHandleIsNull(handle)) return;
|
||||
PublicationLatch().NotePublished(kind, handle);
|
||||
}
|
||||
|
||||
Bool MGPipeHandleIsPublished(MGPipeKind kind, MGPipeHandle handle) {
|
||||
if (MGPipeHandleIsNull(handle)) return false;
|
||||
return PublicationLatch().IsPublished(kind, handle);
|
||||
}
|
||||
|
||||
void MGPipeNoteHandleUnpublished(MGPipeKind kind, MGPipeHandle handle) {
|
||||
if (MGPipeHandleIsNull(handle)) return;
|
||||
PublicationLatch().NoteUnpublished(kind, handle);
|
||||
}
|
||||
|
||||
void MGPipeMintTextureHandle(ITextureObject& texture) {
|
||||
MGPipeSlots().Acquire(MGPipeKind::Texture, texture.GetLifetimeId());
|
||||
}
|
||||
|
||||
void MGPipeMintRenderbufferHandle(RenderbufferObject& renderbuffer) {
|
||||
MGPipeSlots().Acquire(MGPipeKind::Renderbuffer, renderbuffer.GetLifetimeId());
|
||||
}
|
||||
|
||||
void MGPipeMintFramebufferHandle(FramebufferObject& framebuffer) {
|
||||
MGPipeSlots().Acquire(MGPipeKind::Framebuffer, framebuffer.GetLifetimeId());
|
||||
}
|
||||
|
||||
void MGPipeMintShaderCsoHandle(ProgramObject& program) {
|
||||
MGPipeSlots().Acquire(MGPipeKind::ShaderCso, program.GetLifetimeId());
|
||||
}
|
||||
|
||||
void MGPipeEmitTextureResourceCreate(ITextureObject& texture) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.EmitResourceCreate(texture); });
|
||||
}
|
||||
|
||||
void MGPipeEmitTextureResourceRespecify(ITextureObject& texture) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.EmitResourceRespecify(texture); });
|
||||
}
|
||||
|
||||
void MGPipeEmitTextureParams(ITextureObject& texture) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.EmitTextureParams(texture); });
|
||||
}
|
||||
|
||||
void MGPipeNoteTextureLevelDirty(ITextureObject& storageOwner, Uint32 uploadTarget, Uint32 level) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(),
|
||||
[&](auto& emitter) { emitter.NoteLevelDirty(storageOwner, uploadTarget, level); });
|
||||
}
|
||||
|
||||
void MGPipeEmitRenderbufferResourceCreate(RenderbufferObject& renderbuffer) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(),
|
||||
[&](auto& emitter) { emitter.EmitRenderbufferCreate(renderbuffer); });
|
||||
}
|
||||
|
||||
void MGPipeEmitRenderbufferResourceRespecify(RenderbufferObject& renderbuffer) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(),
|
||||
[&](auto& emitter) { emitter.EmitRenderbufferRespecify(renderbuffer); });
|
||||
}
|
||||
|
||||
void MGPipeEmitSamplerCsoCreate(SamplerObject& sampler) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemSamplers, kMGPipeWiredSamplerSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredSamplerSubsystem>(
|
||||
MGPipeSamplerEmitterInstance(), [&](auto& emitter) { emitter.EmitSamplerCso(sampler); });
|
||||
}
|
||||
|
||||
void MGPipeEmitSamplerViewCreate(ITextureObject& texture) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemSamplers, kMGPipeWiredSamplerSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredSamplerSubsystem>(
|
||||
MGPipeSamplerEmitterInstance(), [&](auto& emitter) { emitter.EmitSamplerView(texture); });
|
||||
}
|
||||
|
||||
void MGPipeEmitShaderCsoCreate(ProgramObject& program) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemPrograms, kMGPipeWiredProgramSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredProgramSubsystem>(
|
||||
MGPipeProgramEmitterInstance(), [&](auto& emitter) { emitter.EmitShaderCso(program); });
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// P4a: one client-side death helper per kind P4a mints (D-I1)
|
||||
// ================================================================================
|
||||
@@ -879,12 +1132,20 @@ namespace MobileGL::MG_Pipe {
|
||||
// record: a backend twin table mints one through MGPipeSlots().Acquire whether or not the
|
||||
// subsystem ever asked this client to emit a create - which is exactly what a
|
||||
// MOBILEGL_PIPE_PUSH lane with P4a's bits clear runs - and a delete_* on such a handle is
|
||||
// a refused call the applier counts and asserts on. So the emitter is asked
|
||||
// RecordIsPublished(handle) before any delete goes out.
|
||||
// a refused call the applier counts and asserts on. So the PUBLICATION LATCH above is
|
||||
// asked before any delete goes out, and it is the SAME latch whatever emitted the create
|
||||
// wrote - one answer per {kind, slot, gen}, not a second reading of a live predicate.
|
||||
//
|
||||
// AT THE CONTRACT COMMIT the five family emitters are stubs that publish nothing, so every
|
||||
// helper here answers false and the legacy path runs unchanged - which is what makes this
|
||||
// commit behaviourally inert while the SHAPE is already the final one.
|
||||
// THE LATCH RATHER THAN A PER-EMITTER RecordIsPublished(handle), deliberately, and it is
|
||||
// the one place P4a's shape differs from P3a's: P3a had one kind and one emitter, so the
|
||||
// emitter could hold the latch. P4a has six kinds behind FOUR emitters and one kind -
|
||||
// SamplerViewCso - with no frontend object at all, and a ShaderCso whose composite band
|
||||
// has two independent release paths. A latch this file owns is then the only thing all
|
||||
// six can read, and it keeps the answer out of the emit headers B and C are writing.
|
||||
//
|
||||
// AT THE CONTRACT COMMIT nothing latches a publication, because every family emitter is a
|
||||
// stub, so every helper here answers false and the legacy path runs unchanged - which is
|
||||
// what makes this commit behaviourally inert while the SHAPE is already the final one.
|
||||
namespace {
|
||||
// Steps 2 and 3, shared: raise the notice while the handle still resolves, then return
|
||||
// the slot. Raised UNCONDITIONALLY, exactly as the five destructors raised it before
|
||||
@@ -894,22 +1155,49 @@ namespace MobileGL::MG_Pipe {
|
||||
MG_State::GLState::NotifyStateObjectDestroyed(kind, lifetimeId);
|
||||
if (!MGPipeHandleIsNull(handle)) MGPipeSlots().Free(kind, handle);
|
||||
}
|
||||
|
||||
MGPHandleOnly HandleOnly(MGPipeKind kind, MGPipeHandle handle) {
|
||||
MGPHandleOnly only{};
|
||||
only.Handle = handle;
|
||||
only.Kind = static_cast<Uint32>(kind);
|
||||
return only;
|
||||
}
|
||||
|
||||
// Step 1, shared: the wire delete goes out FIRST and only for a PUBLISHED handle, and
|
||||
// the latch is cleared with it so a second death path - a composite's two, a backend's
|
||||
// redundant notice - cannot emit a second delete for a record that is already gone.
|
||||
Bool EmitDeleteIfPublished(MGPipeKind kind, MGPipeHandle handle, void (*apply)(const MGPHandleOnly&)) {
|
||||
if (!MGPipeHandleIsPublished(kind, handle)) return false;
|
||||
apply(HandleOnly(kind, handle));
|
||||
MGPipeNoteHandleUnpublished(kind, handle);
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool MGPipeEmitSamplerViewCsoDestroyAndFree(Uint64 lifetimeId) {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerViewCso, lifetimeId);
|
||||
const Bool published = false; // the sampler emitter publishes nothing yet
|
||||
// A sampler view has no frontend object of its own - it is minted off the texture's
|
||||
// lifetime id - so there is no NotifyStateObjectDestroyed for kind SamplerViewCso to
|
||||
// raise and step 2 is vacuous here. The slot still goes back, last.
|
||||
if (!MGPipeHandleIsNull(handle)) MGPipeSlots().Free(MGPipeKind::SamplerViewCso, handle);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::SamplerViewCso, handle, &MGPipeApplyDeleteSamplerView);
|
||||
// THE NOTICE IS RAISED FOR THIS KIND TOO, and the reason it once was not is wrong:
|
||||
// NotifyStateObjectDestroyed takes a KIND and a lifetime id, not an object
|
||||
// (StateObjectDeathNotice.h - one entry point for every kind rather than one ops table
|
||||
// per kind), MGPipeKind has SamplerViewCso, and the view IS keyed in that kind's
|
||||
// ByLifetimeId map under the texture's id - which is exactly what the FindByLifetimeId
|
||||
// above just resolved. "It has no frontend object of its own" is why it takes the
|
||||
// lifetime id; it is not a reason to drop step 2. A backend that holds a twin per
|
||||
// SamplerViewCso slot - which is the shape both backends' slot tables take - would
|
||||
// otherwise never be told to drop it, and under a backend with no other per-kind free
|
||||
// path never drop it at all: the C-1 leak, one kind later, and invisible to
|
||||
// PipeSlotPeek because the SLOT was returned correctly.
|
||||
NotifyAndFree(MGPipeKind::SamplerViewCso, lifetimeId, handle);
|
||||
return published;
|
||||
}
|
||||
|
||||
Bool MGPipeEmitTextureDestroyAndFree(Uint64 lifetimeId) {
|
||||
const MGPipeHandle handle = MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, lifetimeId);
|
||||
const Bool published = false; // the texture emitter publishes nothing yet
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::Texture, handle, &MGPipeApplyResourceDestroy);
|
||||
NotifyAndFree(MGPipeKind::Texture, lifetimeId, handle);
|
||||
// THE SAMPLER VIEW DIES WITH ITS TEXTURE, because it is minted off the same lifetime
|
||||
// id: one SamplerViewCso per ITextureObject (D-F2), re-issued on the same handle
|
||||
@@ -921,18 +1209,31 @@ namespace MobileGL::MG_Pipe {
|
||||
// table rather than an omission: the SamplerObject every ITextureObject owns is a real
|
||||
// frontend object with its OWN lifetime id and its own #if MOBILEGL_PIPE_PUSH
|
||||
// destructor, so freeing it from the texture's lifetime id would resolve the wrong slot
|
||||
// (or, worse, a live one belonging to another object). ~SamplerObject runs immediately
|
||||
// after this - a member's destructor follows its owner's body - and takes
|
||||
// MGPipeEmitSamplerCsoDestroyAndFree below, which is the same helper, the same order
|
||||
// and idempotent.
|
||||
MGPipeEmitSamplerViewCsoDestroyAndFree(lifetimeId);
|
||||
return published;
|
||||
// (or, worse, a live one belonging to another object). Its release therefore rides
|
||||
// ~SamplerObject and MGPipeEmitSamplerCsoDestroyAndFree below - the same helper, the
|
||||
// same three-step order, idempotent.
|
||||
//
|
||||
// WHEN that runs is NOT ordered against this body and nothing here may assume it is.
|
||||
// m_sampler is a SharedPtr, so a texture unit slot or a sampler-view resolution that
|
||||
// took a reference delays ~SamplerObject arbitrarily; "a member's destructor follows
|
||||
// its owner's body" would be true of a by-value member and is not true of this one.
|
||||
// The conclusion above does not depend on the timing - the two ids are different, so
|
||||
// the two releases are independent whichever order they happen in - but a package must
|
||||
// not build an ordering on it.
|
||||
//
|
||||
// AND THE VIEW'S ANSWER IS OR-ED IN, not dropped: a texture whose ResourceDestroy was
|
||||
// suppressed (nothing ever published it) but whose DeleteSamplerView did go out has
|
||||
// already spoken on the wire for this object, and reporting false would run the legacy
|
||||
// path for both halves.
|
||||
const Bool viewPublished = MGPipeEmitSamplerViewCsoDestroyAndFree(lifetimeId);
|
||||
return published || viewPublished;
|
||||
}
|
||||
|
||||
Bool MGPipeEmitRenderbufferDestroyAndFree(Uint64 lifetimeId) {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Renderbuffer, lifetimeId);
|
||||
const Bool published = false; // the texture/renderbuffer emitter publishes nothing yet
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::Renderbuffer, handle, &MGPipeApplyResourceDestroy);
|
||||
NotifyAndFree(MGPipeKind::Renderbuffer, lifetimeId, handle);
|
||||
return published;
|
||||
}
|
||||
@@ -949,6 +1250,12 @@ namespace MobileGL::MG_Pipe {
|
||||
// victim to framebuffer 0; and a RECYCLED framebuffer handle can never be suppressed
|
||||
// against its predecessor's record, because Fbo carries Gen and Gen is inside the
|
||||
// record's ContentHash.
|
||||
//
|
||||
// NOTHING EVER TAKES THE PUBLICATION LATCH FOR THIS KIND, by contract and not by
|
||||
// omission: with no create there is nothing to latch, and with no delete there is
|
||||
// nothing for a latch to gate. The answer is therefore the literal false rather than a
|
||||
// latch read, and false is the right one - it means "the legacy path still owes
|
||||
// whatever it owed", which for a framebuffer is the death notice this just raised.
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Framebuffer, lifetimeId);
|
||||
NotifyAndFree(MGPipeKind::Framebuffer, lifetimeId, handle);
|
||||
@@ -958,7 +1265,8 @@ namespace MobileGL::MG_Pipe {
|
||||
Bool MGPipeEmitSamplerCsoDestroyAndFree(Uint64 lifetimeId) {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerCso, lifetimeId);
|
||||
const Bool published = false; // the sampler emitter publishes nothing yet
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::SamplerCso, handle, &MGPipeApplyDeleteSamplerState);
|
||||
NotifyAndFree(MGPipeKind::SamplerCso, lifetimeId, handle);
|
||||
return published;
|
||||
}
|
||||
@@ -973,7 +1281,8 @@ namespace MobileGL::MG_Pipe {
|
||||
// own (the bump rides the next handout).
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId);
|
||||
const Bool published = false; // the program emitter publishes nothing yet
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::ShaderCso, handle, &MGPipeApplyDeleteShaderState);
|
||||
NotifyAndFree(MGPipeKind::ShaderCso, lifetimeId, handle);
|
||||
return published;
|
||||
}
|
||||
@@ -1861,10 +2170,22 @@ namespace MobileGL::MG_Pipe {
|
||||
// to the runtime subsystem that owns it. Naming the subsystem constants here instead
|
||||
// would be a second copy of that map in the only path that runs, and mis-gating a bit
|
||||
// in it would pass every test the map has.
|
||||
//
|
||||
// FOUR CONDITIONS, AND THE WIRED MASK IS ONE OF THEM. `kMGPipeWiredSubsystems` is the
|
||||
// OR of the per-family constants each emit header defines, and the whole ownership
|
||||
// design rests on it MEANING what the headers, this file and the result files all say
|
||||
// it means: an emitter runs only once the commit that gave it a body set its family's
|
||||
// constant. Without this condition a family whose header still says 0 would be CALLED
|
||||
// at every verb whose bit fires under the shipped default mask, so the commit that
|
||||
// lands the body would go live one commit early and every gate run in between would
|
||||
// measure an arm nobody thinks is on - and the mirror error is worse: a family that
|
||||
// lands its body and forgets the constant would emit nothing and look broken. The
|
||||
// P2/P3a bits are all in the mask, so nothing that emits today changes.
|
||||
const Uint64 pushMask = MG_Config::Features.PipePush;
|
||||
const auto wants = [&](MGPipeDirty bit) {
|
||||
const Uint64 subsystem = MGPipeSubsystemForDirty(bit);
|
||||
return subsystem != 0 && (pushMask & subsystem) != 0 &&
|
||||
(kMGPipeWiredSubsystems & subsystem) != 0 &&
|
||||
(dirty & MGPipeDirtyBit(bit)) != 0;
|
||||
};
|
||||
Uint64 payloadBytes = 0;
|
||||
@@ -1918,9 +2239,10 @@ namespace MobileGL::MG_Pipe {
|
||||
// is that the file reads in the order the design states.
|
||||
//
|
||||
// ALL SEVEN ARE STUBS AT THE CONTRACT COMMIT and all four family bits are absent from
|
||||
// kMGPipeWiredSubsystems, so `wants()` is false for every one of them and this whole
|
||||
// block is dead until the packages that own the emitters land. Placing it here, once,
|
||||
// is what keeps those packages out of this file.
|
||||
// kMGPipeWiredSubsystems, so `wants()` is false for every one of them - it tests that
|
||||
// mask as its third condition, which is what makes the sentence true rather than
|
||||
// merely intended - and this whole block is dead until the packages that own the
|
||||
// emitters land. Placing it here, once, is what keeps those packages out of this file.
|
||||
if (wants(MGPipeDirty::NewFramebuffer)) {
|
||||
payloadBytes += EmitFramebufferState(*ctx);
|
||||
}
|
||||
|
||||
Executable → Regular
+26
-9
@@ -154,6 +154,8 @@ namespace MobileGL::MG_Pipe {
|
||||
entry->Live = true;
|
||||
entry->LifetimeId = lifetimeId;
|
||||
++state.LiveCount;
|
||||
// The band's share of LiveCount, so CompositeLiveCount() can answer without a walk.
|
||||
++state.BandLiveCount;
|
||||
if (lifetimeId != 0) {
|
||||
MOBILEGL_ASSERT(state.ByLifetimeId.find(lifetimeId) == state.ByLifetimeId.end(),
|
||||
"lifetime id %llu already owns a ShaderCso slot",
|
||||
@@ -197,6 +199,7 @@ namespace MobileGL::MG_Pipe {
|
||||
entry->LifetimeId = 0;
|
||||
--state.LiveCount;
|
||||
if (kind == MGPipeKind::ShaderCso && MGPipeIsCompositeShaderSlot(handle.Slot)) {
|
||||
--state.BandLiveCount;
|
||||
state.BandFreeList.push_back(handle.Slot);
|
||||
} else {
|
||||
state.FreeList.push_back(handle.Slot);
|
||||
@@ -219,24 +222,37 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
|
||||
Uint32 MGPipeSlotAllocator::HighWater(MGPipeKind kind) const {
|
||||
const KindState& state = StateOf(kind);
|
||||
// Literally "one past the highest slot ever handed out", composites included, so a
|
||||
// leaked composite slot moves it exactly as a leaked ordinary one does - which is what
|
||||
// the per-kind leak cases assert on and what would otherwise make the composite case
|
||||
// green for ever and mean nothing.
|
||||
if (!state.BandSlots.empty()) {
|
||||
return static_cast<Uint32>(kMGPipeShaderCsoCompositeSlotBase + state.BandSlots.size());
|
||||
}
|
||||
return static_cast<Uint32>(state.Slots.size());
|
||||
// THE ORDINARY SPACE ONLY, and the band is reported by CompositeHighWater() below.
|
||||
// Folding the two would pin this at ~983k from the first composite mint onward and
|
||||
// take the ordinary space's "the high-water mark did not move" assertion away for the
|
||||
// rest of the process - the assertion that catches a dense table that never shrinks,
|
||||
// which is the leak shape this allocator exists to make visible. Two spaces, two
|
||||
// numbers, two real assertions. See SlotAllocator.h.
|
||||
return static_cast<Uint32>(StateOf(kind).Slots.size());
|
||||
}
|
||||
|
||||
Uint32 MGPipeSlotAllocator::CompositeHighWater() const {
|
||||
const KindState& state = StateOf(MGPipeKind::ShaderCso);
|
||||
// One past the highest composite slot ever handed out; exactly the base when none ever
|
||||
// was, so the number is monotone from the first mint and a LEAKED COMPOSITE MOVES IT.
|
||||
return static_cast<Uint32>(kMGPipeShaderCsoCompositeSlotBase + state.BandSlots.size());
|
||||
}
|
||||
|
||||
Uint32 MGPipeSlotAllocator::LiveCount(MGPipeKind kind) const { return StateOf(kind).LiveCount; }
|
||||
|
||||
Uint32 MGPipeSlotAllocator::CompositeLiveCount() const {
|
||||
return StateOf(MGPipeKind::ShaderCso).BandLiveCount;
|
||||
}
|
||||
|
||||
Uint32 MGPipeSlotAllocator::FreeCount(MGPipeKind kind) const {
|
||||
const KindState& state = StateOf(kind);
|
||||
return static_cast<Uint32>(state.FreeList.size() + state.BandFreeList.size());
|
||||
}
|
||||
|
||||
Uint32 MGPipeSlotAllocator::CompositeFreeCount() const {
|
||||
return static_cast<Uint32>(StateOf(MGPipeKind::ShaderCso).BandFreeList.size());
|
||||
}
|
||||
|
||||
void MGPipeSlotAllocator::Reset() {
|
||||
for (KindState& state : m_kinds) {
|
||||
state.Slots.clear();
|
||||
@@ -245,6 +261,7 @@ namespace MobileGL::MG_Pipe {
|
||||
state.BandFreeList.clear();
|
||||
state.ByLifetimeId.clear();
|
||||
state.LiveCount = 0;
|
||||
state.BandLiveCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Executable → Regular
+34
-6
@@ -83,15 +83,40 @@ namespace MobileGL::MG_Pipe {
|
||||
// otherwise, live or not.
|
||||
Uint32 GenOfSlot(MGPipeKind kind, Uint32 slot) const;
|
||||
Uint64 LifetimeIdOfSlot(MGPipeKind kind, Uint32 slot) const;
|
||||
// One past the highest slot ever handed out of this kind - which for ShaderCso means
|
||||
// the COMPOSITE band's top once a composite has been minted, because that really is
|
||||
// the highest slot handed out. It is what the leak cases read (a leaked slot of any
|
||||
// kind, composite included, moves it), and it is NOT a table size for kind ShaderCso:
|
||||
// the band is sparse against the ordinary space by design, so a consumer indexing by
|
||||
// slot keeps the band in a table of its own, exactly as this allocator does.
|
||||
// One past the highest ORDINARY slot ever handed out of this kind. For every kind but
|
||||
// ShaderCso that is the whole story; for ShaderCso the composite band is a second,
|
||||
// separately dense space and CompositeHighWater() below answers it.
|
||||
//
|
||||
// THE TWO SPACES ARE REPORTED SEPARATELY, and that is the point rather than a detail.
|
||||
// Folding the band into this number pins it at ~983k from the first composite mint
|
||||
// onward, and every later assertion of the "the high-water mark did not move over N
|
||||
// churn rounds" shape - the one that catches a dense table that never shrinks, which
|
||||
// is the ~1.3 KB-per-record leak C-1 produced - becomes vacuously true for ordinary
|
||||
// ShaderCso slots for the rest of the process. A leak case per space is two real
|
||||
// assertions; one merged number is one real assertion and one that cannot go red.
|
||||
//
|
||||
// It is also NOT a table size for kind ShaderCso even now: the band is sparse against
|
||||
// the ordinary space by design, so a consumer indexing by slot must test
|
||||
// MGPipeIsCompositeShaderSlot(slot) first and keep the band in a table of its own,
|
||||
// exactly as this allocator does.
|
||||
Uint32 HighWater(MGPipeKind kind) const;
|
||||
// One past the highest COMPOSITE slot ever handed out, i.e.
|
||||
// kMGPipeShaderCsoCompositeSlotBase + (band slots ever handed out), and exactly the
|
||||
// base when none ever was. Kind ShaderCso is the only kind with a band, so it is
|
||||
// implied - as it is for AllocateComposite. A LEAKED COMPOSITE MOVES THIS and moves
|
||||
// nothing else, which is what the composite's own leak case asserts on.
|
||||
Uint32 CompositeHighWater() const;
|
||||
// Live slots of this kind, ORDINARY AND COMPOSITE TOGETHER for ShaderCso: a live
|
||||
// composite is a live ShaderCso, the applier's two record tables are one object class,
|
||||
// and a caller asking "how many shader CSOs does this client hold" wants both. The
|
||||
// band's own count is CompositeLiveCount(); the ordinary space's is the difference.
|
||||
Uint32 LiveCount(MGPipeKind kind) const;
|
||||
Uint32 CompositeLiveCount() const;
|
||||
// Slots waiting on a free list. Also BOTH SPACES for ShaderCso, for LiveCount's
|
||||
// reason and with the same caveat: a caller that needs to know WHICH space a slot went
|
||||
// back to reads CompositeFreeCount() and subtracts.
|
||||
Uint32 FreeCount(MGPipeKind kind) const;
|
||||
Uint32 CompositeFreeCount() const;
|
||||
|
||||
// Context teardown / server reset / a unit test's fixture.
|
||||
void Reset();
|
||||
@@ -120,6 +145,9 @@ namespace MobileGL::MG_Pipe {
|
||||
Vector<Uint32> BandFreeList;
|
||||
UnorderedMap<Uint64, Uint32> ByLifetimeId;
|
||||
Uint32 LiveCount = 0;
|
||||
// The band's share of LiveCount above, so the two spaces can be reported apart
|
||||
// without walking either table. Always 0 for every kind but ShaderCso.
|
||||
Uint32 BandLiveCount = 0;
|
||||
};
|
||||
|
||||
KindState& StateOf(MGPipeKind kind);
|
||||
|
||||
@@ -94,6 +94,15 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
class BufferObject;
|
||||
// P4a's five, for the BIRTH half at the tail of this header. Declarations only, exactly as
|
||||
// BufferObject is: none of the hooks below needs a definition, and this header must not
|
||||
// gain one - reaching a frontend class header from here would put the state machine's own
|
||||
// types in front of every mutator that spells MGP_NOTE_MUTATION.
|
||||
class ITextureObject;
|
||||
class RenderbufferObject;
|
||||
class FramebufferObject;
|
||||
class SamplerObject;
|
||||
class ProgramObject;
|
||||
}
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
@@ -211,6 +220,142 @@ namespace MobileGL::MG_Pipe {
|
||||
// Returns the coherent host pointer the resource owner donated, or null for a DECLINE -
|
||||
// which is a real answer. Every call, mint or decline, is one map-persistent roundtrip.
|
||||
void* MGPipeEmitMapPersistent(MG_State::GLState::BufferObject& buffer);
|
||||
|
||||
// ================================================================================
|
||||
// P4a: THE BIRTH HALF, one hook per client path MG_State owns (D-C .. D-I)
|
||||
// ================================================================================
|
||||
//
|
||||
// The death helpers above are half a lifetime. The other half is emitted from MG_State
|
||||
// too - a texture's create from its constructor, a renderbuffer's respecify from its
|
||||
// storage mutators, a texture's params from glTexParameter*, a sampler CSO from the
|
||||
// sampler object, a shader CSO from the program - because that is where the event
|
||||
// happens, exactly as P3a's buffer family emits from BufferObject's own dispatchers
|
||||
// (ARCHITECTURE.md 5.1 names those as the ONE exception to push-at-validate). Only the
|
||||
// texture sub-data DRAIN runs at the validate point, and even it is fed from here: the
|
||||
// drain list is appended on a level's first dirty mark.
|
||||
//
|
||||
// WHY THEY ARE DECLARED HERE. This header is the one door MG_State has into the client
|
||||
// (check_include_closure.py's mutation-header probe pins it: reaching
|
||||
// MG_Impl/Pipe/*Emit.h from a frontend mutator would pull the client's emitters into the
|
||||
// state machine that calls them). So a hook a frontend mutator calls is DECLARED here and
|
||||
// DEFINED in MG_Impl/Pipe/PipeFill.cpp, which is package A's for the whole phase - the
|
||||
// same "declaration here, definition there" split MGPipeMintResourceHandle and
|
||||
// MGPipeEmitResourceCreate use, and the reason no file is touched twice.
|
||||
//
|
||||
// WHAT EACH BODY DOES, and the division is fixed:
|
||||
// * PipeFill.cpp owns the GATE - the subsystem bit in MOBILEGL_PIPE_PUSH *and* the
|
||||
// family's own kMGPipeWired*Subsystem constant, the same pair the validate point's
|
||||
// `wants()` applies to every emission - and the four MINTS, which are pure allocator
|
||||
// work and need no family knowledge;
|
||||
// * the FAMILY EMITTER (MG_Impl/Pipe/<Family>Emit.h, owned by package B or C) owns the
|
||||
// payload build, the handle rule for its own kind and the PUBLICATION LATCH below.
|
||||
// PipeFill.cpp forwards to it through an entry point that is compiled only while that
|
||||
// family's wired constant is non-zero, so this tree links against the STUB emitters
|
||||
// and against the finished ones with no edit to PipeFill.cpp - and a family that sets
|
||||
// its constant without providing the entry point is a COMPILE ERROR in its own commit
|
||||
// rather than a surprise at the merge. The entry point each hook forwards to is named
|
||||
// beside it and spelled out in PipeFill.cpp's contract block.
|
||||
//
|
||||
// NOTHING CALLS ANY OF THEM AT THE CONTRACT COMMIT. B and C add the call sites in the
|
||||
// five MG_State directories C.7 gives them, in the SAME commit that gives the emitter its
|
||||
// body - by EDITING an existing constructor/mutator body, never by adding one (G1).
|
||||
|
||||
// ---- the publication latch (D-I1), and it is the ONE answer both halves read ----
|
||||
//
|
||||
// The create is gated at its call site and the destroy inside the death helper, so the
|
||||
// two ask the same question at two different moments. An object born while its subsystem
|
||||
// bit was clear and destroyed after it was set would otherwise free its slot with the
|
||||
// applier's record still Live - on a slot the allocator is about to hand out again. A
|
||||
// slot is NOT evidence of a record either: a backend twin table mints one through
|
||||
// MGPipeSlots().Acquire whether or not the subsystem ever asked this client to emit a
|
||||
// create, and a delete_* on such a handle is a refused call the applier asserts on.
|
||||
//
|
||||
// So the emitter latches the answer when its create actually goes out, the death helper
|
||||
// reads the latch, and the latch is keyed by {kind, slot, gen} so a recycled slot cannot
|
||||
// inherit its predecessor's answer. Defined in PipeFill.cpp beside the six death helpers,
|
||||
// declared here because both the helpers and the five emit headers read it.
|
||||
void MGPipeNoteHandlePublished(MGPipeKind kind, MGPipeHandle handle);
|
||||
Bool MGPipeHandleIsPublished(MGPipeKind kind, MGPipeHandle handle);
|
||||
void MGPipeNoteHandleUnpublished(MGPipeKind kind, MGPipeHandle handle);
|
||||
|
||||
// ---- the four mints (pure allocator work, no family knowledge) ----
|
||||
//
|
||||
// UNCONDITIONAL in a push build, for MGPipeMintResourceHandle's reason: a handle is CLIENT
|
||||
// state and other subsystems name these objects by handle whether or not their own family
|
||||
// is switched on - MGPSurface::Res names a Texture or a Renderbuffer out of the framebuffer
|
||||
// subsystem, MGPBoundView::Texture and MGPImageView::Res name a Texture out of the sampler
|
||||
// one. Gating the mint on the family bit would make those emit null handles in exactly the
|
||||
// A/B arm that exists to isolate the families. Each costs one free-list pop and one map
|
||||
// insert per object and emits nothing.
|
||||
void MGPipeMintTextureHandle(MG_State::GLState::ITextureObject& texture);
|
||||
void MGPipeMintRenderbufferHandle(MG_State::GLState::RenderbufferObject& renderbuffer);
|
||||
// A framebuffer has a handle and NO wire lifetime (D-I2): set_framebuffer_state is the only
|
||||
// call that names one, and there is no create or destroy for the kind. The mint is still
|
||||
// the object's, so the identity exists before the first validate point that pushes it.
|
||||
void MGPipeMintFramebufferHandle(MG_State::GLState::FramebufferObject& framebuffer);
|
||||
// Ordinary programs only. A program-pipeline COMPOSITE is minted by the composite resolver
|
||||
// out of the reserved band through MGPipeSlotAllocator::AllocateComposite, which is the one
|
||||
// door into it, and it is not a frontend construction event.
|
||||
void MGPipeMintShaderCsoHandle(MG_State::GLState::ProgramObject& program);
|
||||
|
||||
// ---- textures and renderbuffers: MG_Impl/Pipe/TextureEmit.h, package B ----
|
||||
//
|
||||
// resource_create from ITextureObject's constructor and RenderbufferObject's;
|
||||
// resource_respecify from every storage-defining entry point, including
|
||||
// RenderbufferObject::{SetInternalFormat, AllocateStorage, SetSamples}, which publish
|
||||
// nothing at all today (D-D2); set_texture_params from the parameter mutators, which is
|
||||
// where the READ-attachment-only gap D-E3 closes.
|
||||
//
|
||||
// Entry points MGPipeTextureEmitter must provide, all taking the frontend object by
|
||||
// reference and returning void:
|
||||
// EmitResourceCreate(ITextureObject&) / EmitResourceRespecify(ITextureObject&)
|
||||
// EmitTextureParams(ITextureObject&)
|
||||
// NoteLevelDirty(ITextureObject& storageOwner, Uint32 uploadTarget, Uint32 level)
|
||||
// EmitRenderbufferCreate(RenderbufferObject&) / EmitRenderbufferRespecify(RenderbufferObject&)
|
||||
void MGPipeEmitTextureResourceCreate(MG_State::GLState::ITextureObject& texture);
|
||||
void MGPipeEmitTextureResourceRespecify(MG_State::GLState::ITextureObject& texture);
|
||||
void MGPipeEmitTextureParams(MG_State::GLState::ITextureObject& texture);
|
||||
// The DRAIN LIST's append, on a level's FIRST dirty mark, keyed on the STORAGE OWNER from
|
||||
// day one (D-D4: a view and its owner already share one dirty state, so an upload through
|
||||
// either lands on the same key). The record itself is emitted at the validate point by
|
||||
// MGPipeTextureEmitter::DrainTextureSubData; this is only what puts the level on the list,
|
||||
// and walking every live texture per verb is the cost it exists to avoid.
|
||||
void MGPipeNoteTextureLevelDirty(MG_State::GLState::ITextureObject& storageOwner, Uint32 uploadTarget,
|
||||
Uint32 level);
|
||||
void MGPipeEmitRenderbufferResourceCreate(MG_State::GLState::RenderbufferObject& renderbuffer);
|
||||
void MGPipeEmitRenderbufferResourceRespecify(MG_State::GLState::RenderbufferObject& renderbuffer);
|
||||
|
||||
// ---- sampler CSOs and sampler views: MG_Impl/Pipe/SamplerEmit.h, package C ----
|
||||
//
|
||||
// Entry points MGPipeSamplerEmitter must provide, returning void:
|
||||
// EmitSamplerCso(SamplerObject&) - D-F1's content-addressed mint-or-share at
|
||||
// capacity 256, hashed field-wise over a canonical
|
||||
// zero-initialised copy, behind the version-first
|
||||
// skip. The HANDLE RULE FOR THIS KIND IS THE
|
||||
// EMITTER'S, not this file's: two identical
|
||||
// samplers share one CSO, so there is deliberately
|
||||
// no per-object mint above, and it is the emitter
|
||||
// that decides which lifetime id (if any) owns the
|
||||
// slot the death helper will resolve.
|
||||
// EmitSamplerView(ITextureObject&) - D-F2's ONE view per texture object, minted off
|
||||
// the texture's own lifetime id and re-issued on
|
||||
// the SAME handle when the restrictions move.
|
||||
void MGPipeEmitSamplerCsoCreate(MG_State::GLState::SamplerObject& sampler);
|
||||
void MGPipeEmitSamplerViewCreate(MG_State::GLState::ITextureObject& texture);
|
||||
|
||||
// ---- programs: MG_Impl/Pipe/ProgramEmit.h, package C ----
|
||||
//
|
||||
// Entry point MGPipeProgramEmitter must provide, returning void:
|
||||
// EmitShaderCso(ProgramObject&)
|
||||
//
|
||||
// Re-issued on the SAME handle whenever the link version moves, exactly as
|
||||
// create_vertex_elements is (Gen moves only on slot reuse). D-H4 keeps the TRACKER out of
|
||||
// it - bit 6's shutter reads GetCurrentProgram() and deliberately not GetProgramForDraw(),
|
||||
// because the tracker must not force a compile to answer "did the shader move" - so the
|
||||
// ordinary emission is the validate point's, from the join the verb was going to make
|
||||
// anyway. This hook exists for the paths that are NOT a draw: a link that completes off
|
||||
// the draw path still owns its own publication.
|
||||
void MGPipeEmitShaderCsoCreate(MG_State::GLState::ProgramObject& program);
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#define MGP_NOTE_MUTATION(Field) \
|
||||
::MobileGL::MG_Pipe::MGPipeNoteFrontendMutation(::MobileGL::MG_Pipe::MGPipeInputField::Field)
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// pins now reach. Push-only, like the translation unit that defines them - in a pull build the
|
||||
// symbol does not exist and the one case that calls it is compiled out.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#endif
|
||||
|
||||
@@ -779,3 +780,64 @@ TEST(PipeCatalogue, EveryUnmigratedEmulationIsNamedOnce) {
|
||||
for (const char* name : kNames) MGPipeUnmigratedEmulation(name);
|
||||
#endif
|
||||
}
|
||||
|
||||
// THE ShaderCso COMPOSITE BAND IS A SECOND SPACE, AND THE ALLOCATOR REPORTS IT SEPARATELY.
|
||||
//
|
||||
// The band's base is 983040, so a composite handle passes every bound an ordinary one does and
|
||||
// a slot-indexed table that forgets the band allocates ~983k entries for one program pipeline.
|
||||
// That is why the allocator keeps two dense tables - and it is also why the two must be
|
||||
// COUNTED apart: a high-water mark that folded them would be pinned at ~983k from the first
|
||||
// composite mint onward, and every "the high-water mark did not move over N churn rounds"
|
||||
// assertion about ORDINARY ShaderCso slots - the shape that catches a dense table that never
|
||||
// shrinks, i.e. the ~1.3 KB-per-record leak the P3a final review found - would be vacuously
|
||||
// true for the rest of the process. One merged number is one real assertion and one that
|
||||
// cannot go red; two numbers are two real assertions, which is what the per-kind leak cases
|
||||
// need.
|
||||
//
|
||||
// This case pins both halves: a leaked COMPOSITE moves the band's marks and not the ordinary
|
||||
// one, and an ordinary leak still moves the ordinary mark with a composite outstanding.
|
||||
TEST(PipeCatalogue, TheCompositeShaderBandIsCountedApartFromTheOrdinarySpace) {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
MGPipeSlotAllocator slots;
|
||||
|
||||
const Uint32 ordinaryBefore = slots.HighWater(MGPipeKind::ShaderCso);
|
||||
EXPECT_EQ(slots.CompositeHighWater(), kMGPipeShaderCsoCompositeSlotBase)
|
||||
<< "the band's high-water mark starts at its base, so it is monotone from the first mint";
|
||||
EXPECT_EQ(slots.CompositeLiveCount(), 0u);
|
||||
EXPECT_EQ(slots.CompositeFreeCount(), 0u);
|
||||
|
||||
// A COMPOSITE MOVES THE BAND'S MARKS AND ONLY THOSE.
|
||||
const MGPipeHandle composite = slots.AllocateComposite(9001);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(composite));
|
||||
ASSERT_TRUE(MGPipeIsCompositeShaderSlot(composite.Slot));
|
||||
EXPECT_EQ(slots.HighWater(MGPipeKind::ShaderCso), ordinaryBefore)
|
||||
<< "a composite mint moved the ORDINARY high-water mark, so the ordinary space's leak "
|
||||
"assertion is vacuous from here on";
|
||||
EXPECT_EQ(slots.CompositeHighWater(), kMGPipeShaderCsoCompositeSlotBase + 1u);
|
||||
EXPECT_EQ(slots.CompositeLiveCount(), 1u);
|
||||
// A live composite IS a live ShaderCso: the merged count is deliberate and stays.
|
||||
EXPECT_EQ(slots.LiveCount(MGPipeKind::ShaderCso), 1u);
|
||||
|
||||
// AND THE ORDINARY MARK STILL MOVES WITH A COMPOSITE OUTSTANDING - the half that stopped
|
||||
// existing when one number carried both spaces.
|
||||
const MGPipeHandle ordinary = slots.Allocate(MGPipeKind::ShaderCso);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(ordinary));
|
||||
EXPECT_FALSE(MGPipeIsCompositeShaderSlot(ordinary.Slot));
|
||||
EXPECT_GT(slots.HighWater(MGPipeKind::ShaderCso), ordinaryBefore);
|
||||
EXPECT_EQ(slots.CompositeHighWater(), kMGPipeShaderCsoCompositeSlotBase + 1u)
|
||||
<< "an ordinary mint moved the BAND's high-water mark";
|
||||
|
||||
// The slot goes back to the BAND's free list, and the high-water marks do not come back
|
||||
// down - which is exactly what makes them a leak witness rather than a live count.
|
||||
const Uint32 ordinaryHighWater = slots.HighWater(MGPipeKind::ShaderCso);
|
||||
slots.Free(MGPipeKind::ShaderCso, composite);
|
||||
EXPECT_EQ(slots.CompositeLiveCount(), 0u);
|
||||
EXPECT_EQ(slots.CompositeFreeCount(), 1u);
|
||||
EXPECT_EQ(slots.FreeCount(MGPipeKind::ShaderCso), 1u);
|
||||
EXPECT_EQ(slots.CompositeHighWater(), kMGPipeShaderCsoCompositeSlotBase + 1u);
|
||||
EXPECT_EQ(slots.HighWater(MGPipeKind::ShaderCso), ordinaryHighWater);
|
||||
EXPECT_EQ(slots.LiveCount(MGPipeKind::ShaderCso), 1u);
|
||||
#else
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no client slot allocator in a pull build";
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user