diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index d07c4b3e..f1758521 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -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 + 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(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(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(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& table = m_kinds[index]; + if (slot >= table.size()) table.resize(static_cast(slot) + 1); + return &table[slot]; + } + + const Entry* Find(MGPipeKind kind, Uint32 slot) const { + const SizeT index = static_cast(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& table = m_kinds[index]; + return slot < table.size() ? &table[slot] : nullptr; + } + + Array, kKindCount> m_kinds{}; + Vector 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( + MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.EmitResourceCreate(texture); }); + } + + void MGPipeEmitTextureResourceRespecify(ITextureObject& texture) { + if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return; + ForwardWhenWired( + MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.EmitResourceRespecify(texture); }); + } + + void MGPipeEmitTextureParams(ITextureObject& texture) { + if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return; + ForwardWhenWired( + MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.EmitTextureParams(texture); }); + } + + void MGPipeNoteTextureLevelDirty(ITextureObject& storageOwner, Uint32 uploadTarget, Uint32 level) { + if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return; + ForwardWhenWired( + MGPipeTextureEmitterInstance(), + [&](auto& emitter) { emitter.NoteLevelDirty(storageOwner, uploadTarget, level); }); + } + + void MGPipeEmitRenderbufferResourceCreate(RenderbufferObject& renderbuffer) { + if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return; + ForwardWhenWired( + MGPipeTextureEmitterInstance(), + [&](auto& emitter) { emitter.EmitRenderbufferCreate(renderbuffer); }); + } + + void MGPipeEmitRenderbufferResourceRespecify(RenderbufferObject& renderbuffer) { + if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return; + ForwardWhenWired( + MGPipeTextureEmitterInstance(), + [&](auto& emitter) { emitter.EmitRenderbufferRespecify(renderbuffer); }); + } + + void MGPipeEmitSamplerCsoCreate(SamplerObject& sampler) { + if (!FamilyIsLive(kMGPipeSubsystemSamplers, kMGPipeWiredSamplerSubsystem)) return; + ForwardWhenWired( + MGPipeSamplerEmitterInstance(), [&](auto& emitter) { emitter.EmitSamplerCso(sampler); }); + } + + void MGPipeEmitSamplerViewCreate(ITextureObject& texture) { + if (!FamilyIsLive(kMGPipeSubsystemSamplers, kMGPipeWiredSamplerSubsystem)) return; + ForwardWhenWired( + MGPipeSamplerEmitterInstance(), [&](auto& emitter) { emitter.EmitSamplerView(texture); }); + } + + void MGPipeEmitShaderCsoCreate(ProgramObject& program) { + if (!FamilyIsLive(kMGPipeSubsystemPrograms, kMGPipeWiredProgramSubsystem)) return; + ForwardWhenWired( + 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(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); } diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp old mode 100755 new mode 100644 index a3c7e0fb..c6940523 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp @@ -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(kMGPipeShaderCsoCompositeSlotBase + state.BandSlots.size()); - } - return static_cast(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(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(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(state.FreeList.size() + state.BandFreeList.size()); } + Uint32 MGPipeSlotAllocator::CompositeFreeCount() const { + return static_cast(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; } } diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.h b/MobileGL/MG_Impl/Pipe/SlotAllocator.h old mode 100755 new mode 100644 index 940eaa96..b2e9f36e --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.h +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.h @@ -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 BandFreeList; UnorderedMap 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); diff --git a/MobileGL/MG_Pipe/PipeMutation.h b/MobileGL/MG_Pipe/PipeMutation.h index 9312b6bb..271a0c61 100644 --- a/MobileGL/MG_Pipe/PipeMutation.h +++ b/MobileGL/MG_Pipe/PipeMutation.h @@ -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/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) diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index a1b104d7..a51699d8 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -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 #include #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 +}