diff --git a/MobileGL/Config.h b/MobileGL/Config.h index b1188987..1702f2a2 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -148,6 +148,9 @@ namespace MobileGL::MG_Config { // record is byte-identical to the one its handle last published (a glBindVertexBuffer // that only changed the buffer) re-publishes nothing. Auto is ON; ForceOff re-creates. QuirkOverride PipeVertexElementsDedup = QuirkOverride::Auto; + // P15: the client reuses a bound framebuffer's built set_framebuffer_state while nothing it + // reads has moved (MGPipeFramebufferEmitter::BuildBoundFramebufferState). + QuirkOverride PipeFramebufferStateMemo = QuirkOverride::Auto; // MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension // string. `double` in a shader always WORKS - it is narrowed to 32 bits before any // module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 255bab7f..1cf23b19 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -374,6 +374,7 @@ namespace MobileGL::MG_ConfigLoader { features.MagmaWriteRename = QueryEnvQuirkOverride("MOBILEGL_MAGMA_WRITE_RENAME"); features.EsprytVaoShadow = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_VAO_SHADOW"); features.PipeVertexElementsDedup = QueryEnvQuirkOverride("MOBILEGL_PIPE_VERTEX_ELEMENTS_DEDUP"); + features.PipeFramebufferStateMemo = QueryEnvQuirkOverride("MOBILEGL_PIPE_FRAMEBUFFER_STATE_MEMO"); features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64"); features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK"); features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV"); diff --git a/MobileGL/MG_Impl/Pipe/FramebufferEmit.h b/MobileGL/MG_Impl/Pipe/FramebufferEmit.h index d4d264eb..e9dd705a 100644 --- a/MobileGL/MG_Impl/Pipe/FramebufferEmit.h +++ b/MobileGL/MG_Impl/Pipe/FramebufferEmit.h @@ -275,13 +275,13 @@ namespace MobileGL::MG_Pipe { Bool drawOk = false; Bool readOk = false; if (shared) { - drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Both, drawState); + drawOk = BuildBoundFramebufferState(*drawFbo, MGPipeFramebufferTarget::Both, drawState); } else { if (drawFbo) { - drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Draw, drawState); + drawOk = BuildBoundFramebufferState(*drawFbo, MGPipeFramebufferTarget::Draw, drawState); } if (readFbo) { - readOk = BuildFramebufferState(*readFbo, MGPipeFramebufferTarget::Read, readState); + readOk = BuildBoundFramebufferState(*readFbo, MGPipeFramebufferTarget::Read, readState); } } if (!drawOk && !readOk) return 0; @@ -414,6 +414,8 @@ namespace MobileGL::MG_Pipe { const MGPFramebufferState& LastNamed() const { return m_lastNamed; } Uint64 EmissionCount() const { return m_emissions; } Uint64 RefusedCount() const { return m_refusals; } + // P15: bound-target builds answered from the memo (BuildBoundFramebufferState). + Uint64 MemoHitCount() const { return m_memoHits; } // A fresh context: what the server has is no longer what this emitter last sent. Only // LATCHES reset here - MGPipeApplierReset clears the applier's DrawFramebuffer and @@ -438,6 +440,7 @@ namespace MobileGL::MG_Pipe { void ResetForTest() { Reset(); ResetCounters(); + for (auto& memo : m_buildMemo) memo.Valid = false; m_lastDraw = MGPFramebufferState{}; m_lastRead = MGPFramebufferState{}; m_lastNamed = MGPFramebufferState{}; @@ -563,6 +566,40 @@ namespace MobileGL::MG_Pipe { return true; } + // BuildFramebufferState for a BOUND target, reusing the last record built for the same + // framebuffer and target while nothing the build reads has moved. The build runs per draw + // and its answer is a function of: + // - the framebuffer: lifetime id (identity) and object version (attachments, draw and + // read buffers, no-attachment defaults all move it); + // - each attached texture: lifetime id, shape version (sizes, level set, internal + // format: completeness and geometry) and its samples / fixed-locations pair, which + // move the params version rather than the shape; + // - each attached renderbuffer: lifetime id plus its size, format and samples, read + // directly (its storage has no version); + // - the handles it minted: the slot allocator's reset epoch, and the texture emitter's + // table epoch for the sticky bind bits SurfaceOf notes (a dropped table must be + // re-noted). + // Not for the default framebuffer, whose geometry follows the current surface. + // Kill switch MOBILEGL_PIPE_FRAMEBUFFER_STATE_MEMO=0. + Bool BuildBoundFramebufferState(const FramebufferObject& fbo, MGPipeFramebufferTarget target, + MGPFramebufferState& out) { + const Bool memoOn = MG_Config::Features.PipeFramebufferStateMemo != MG_Config::QuirkOverride::ForceOff && + !fbo.IsDefaultFramebuffer(); + // Direct-mapped on (framebuffer, target): the build only runs when the bound framebuffer + // changed, so a per-target single entry would miss on every A/B/A switch between targets. + const Uint64 key = fbo.GetLifetimeId() * 4u + static_cast(target); + BuildMemo& memo = m_buildMemo[static_cast((key * 0x9E3779B97F4A7C15ull) >> 60) % m_buildMemo.size()]; + if (memoOn && memo.Valid && MemoHolds(memo, fbo, target)) { + out = memo.State; + ++m_memoHits; + return true; + } + memo.Valid = false; + if (!BuildFramebufferState(fbo, target, out)) return false; + if (memoOn) FillMemo(memo, fbo, target, out); + return true; + } + static Bool IsBoundTo(const FramebufferObject& fbo, MobileGL::FramebufferTarget target) { if (MG_State::pGLContext == nullptr) return false; const auto& bound = MG_State::pGLContext->GetFramebufferBindingSlot(target).GetBoundObject(); @@ -642,6 +679,96 @@ namespace MobileGL::MG_Pipe { out.FixedSampleLocations = fbo.GetDefaultFixedSampleLocations() ? 1 : 0; } + struct MemoAttachment { + Uint32 Point = 0; + Bool IsTexture = false; + Uint64 LifetimeId = 0; + Uint64 ShapeVersion = 0; + Int Width = 0, Height = 0, Samples = 0; + Uint32 Format = 0; + Bool FixedSampleLocations = false; + }; + struct BuildMemo { + Bool Valid = false; + Uint8 Target = 0; + Uint16 ObjectVersion = 0; + Uint64 FboLifetimeId = 0; + Uint64 AllocatorEpoch = 0; + Uint64 TextureTableEpoch = 0; + Uint32 AttachmentCount = 0; + Array(FramebufferAttachmentType::FramebufferAttachmentTypeCount)> + Attachments{}; + MGPFramebufferState State{}; + }; + + static Bool AttachmentMatches(const MemoAttachment& entry, + const MG_State::GLState::FramebufferAttachmentObject& attachment) { + if (attachment.IsEmpty() || attachment.IsTexture() != entry.IsTexture) return false; + if (entry.IsTexture) { + const auto& texture = attachment.GetTexture(); + return texture->GetLifetimeId() == entry.LifetimeId && + texture->GetShapeVersion() == entry.ShapeVersion && + texture->GetSamples() == entry.Samples && + texture->HasFixedSampleLocations() == entry.FixedSampleLocations; + } + const auto& renderbuffer = attachment.GetRenderbuffer(); + return renderbuffer->GetLifetimeId() == entry.LifetimeId && renderbuffer->GetWidth() == entry.Width && + renderbuffer->GetHeight() == entry.Height && renderbuffer->GetSamples() == entry.Samples && + static_cast(renderbuffer->GetInternalFormat()) == entry.Format; + } + + Bool MemoHolds(const BuildMemo& memo, const FramebufferObject& fbo, MGPipeFramebufferTarget target) const { + if (memo.Target != static_cast(target) || memo.FboLifetimeId != fbo.GetLifetimeId() || + memo.ObjectVersion != fbo.GetObjectVersion() || memo.AllocatorEpoch != MGPipeSlots().ResetEpoch() || + memo.TextureTableEpoch != MGPipeTextureEmitterInstance().TableEpoch()) { + return false; + } + const auto& all = fbo.GetAllAttachmentObjects(); + for (Uint32 i = 0; i < memo.AttachmentCount; ++i) { + const MemoAttachment& entry = memo.Attachments[i]; + if (!AttachmentMatches(entry, all[entry.Point])) return false; + } + return true; + } + + void FillMemo(BuildMemo& memo, const FramebufferObject& fbo, MGPipeFramebufferTarget target, + const MGPFramebufferState& state) { + memo.Target = static_cast(target); + memo.FboLifetimeId = fbo.GetLifetimeId(); + memo.ObjectVersion = fbo.GetObjectVersion(); + memo.AllocatorEpoch = MGPipeSlots().ResetEpoch(); + memo.TextureTableEpoch = MGPipeTextureEmitterInstance().TableEpoch(); + memo.AttachmentCount = 0; + const auto& all = fbo.GetAllAttachmentObjects(); + for (SizeT point = 0; point < all.size(); ++point) { + const auto& attachment = all[point]; + if (attachment.IsEmpty()) continue; + MemoAttachment& entry = memo.Attachments[memo.AttachmentCount++]; + entry = MemoAttachment{}; + entry.Point = static_cast(point); + entry.IsTexture = attachment.IsTexture(); + if (entry.IsTexture) { + const auto& texture = attachment.GetTexture(); + entry.LifetimeId = texture->GetLifetimeId(); + entry.ShapeVersion = texture->GetShapeVersion(); + entry.Samples = texture->GetSamples(); + entry.FixedSampleLocations = texture->HasFixedSampleLocations(); + } else { + const auto& renderbuffer = attachment.GetRenderbuffer(); + entry.LifetimeId = renderbuffer->GetLifetimeId(); + entry.Width = renderbuffer->GetWidth(); + entry.Height = renderbuffer->GetHeight(); + entry.Samples = renderbuffer->GetSamples(); + entry.Format = static_cast(renderbuffer->GetInternalFormat()); + } + } + memo.State = state; + memo.Valid = true; + } + + // Direct-mapped on (framebuffer lifetime id, target); Named never reaches it. + Array m_buildMemo{}; + Uint64 m_memoHits = 0; Array m_lastEmitted{}; // The per-FRAMEBUFFER suppressor for Named records, slot-indexed with the generation // checked, exactly as the applier's own table is. A framebuffer has no wire lifetime diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index e001467a..6e38463e 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -2640,6 +2640,44 @@ namespace MobileGL::MG_Pipe { // consumer mask gains bit 13 in the same // commit, or R-8 withholds the whole family. kMGPipeWiredBufferBindingSubsystem; + + // P15: the dirty bits whose family this build, push mask and consumer emit - every + // condition of the per-bit gate in MGPipeValidateForVerb except the dirty bit itself. + Uint32 ComputeLiveDirtyBits(Uint64 pushMask) { + Uint32 live = 0; + for (Uint32 b = 0; b < kMGPipeDirtyCount; ++b) { + const Uint64 subsystem = MGPipeSubsystemForDirty(static_cast(b)); + if (subsystem != 0 && (pushMask & subsystem) != 0 && (kMGPipeWiredSubsystems & subsystem) != 0 && + P4aFamilyHasItsConsumer(subsystem) && P4aFamilyDependenciesAreSet(subsystem, pushMask) && + P5eFamilyIsLive(subsystem, pushMask)) { + live |= MGPipeDirtyBit(static_cast(b)); + } + } + return live; + } + + // Per verb, these conditions used to be re-asked for every bit (~15 consumer-registration + // and dependency-table walks per draw). In monolith their only inputs are the push mask and + // the registered consumer, so the answer is kept until either moves. Under a transport the + // caps mirror answers and can move with a new session, so it is recomputed every verb. + Uint32 LiveDirtyBits(Uint64 pushMask) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return ComputeLiveDirtyBits(pushMask); +#endif + static Bool valid = false; + static Uint64 cachedPushMask = 0; + static const MGPipeResourceOps* cachedConsumer = nullptr; + static Uint32 cachedLive = 0; + const MGPipeResourceOps* consumer = MGPipeGetResourceOps(); + if (!valid || cachedPushMask != pushMask || cachedConsumer != consumer) { + cachedLive = ComputeLiveDirtyBits(pushMask); + cachedPushMask = pushMask; + cachedConsumer = consumer; + valid = true; + } + return cachedLive; + } + // Each family constant is either 0 or its own subsystem bit and nothing else. Without // this a header that set the wrong constant - the sampler bit in the program header, // say - would switch the wrong family on and every gate would still pass. @@ -3621,15 +3659,8 @@ namespace MobileGL::MG_Pipe { // binding-point family and asks bit 13's OWN consumer bit rather than the resource // family's. It answers true for every subsystem but bit 13, so nothing that emitted // before P5e changes. - const auto wants = [&](MGPipeDirty bit) { - const Uint64 subsystem = MGPipeSubsystemForDirty(bit); - return subsystem != 0 && (pushMask & subsystem) != 0 && - (kMGPipeWiredSubsystems & subsystem) != 0 && - P4aFamilyHasItsConsumer(subsystem) && - P4aFamilyDependenciesAreSet(subsystem, pushMask) && - P5eFamilyIsLive(subsystem, pushMask) && - (dirty & MGPipeDirtyBit(bit)) != 0; - }; + const Uint32 liveDirty = LiveDirtyBits(pushMask) & dirty; + const auto wants = [&](MGPipeDirty bit) { return (liveDirty & MGPipeDirtyBit(bit)) != 0; }; Uint64 payloadBytes = 0; // A fresh context is a fresh server, and that is true of EVERY subsystem, so it is diff --git a/MobileGL/MG_Impl/Pipe/ProgramEmit.h b/MobileGL/MG_Impl/Pipe/ProgramEmit.h index 24547503..ef277f20 100644 --- a/MobileGL/MG_Impl/Pipe/ProgramEmit.h +++ b/MobileGL/MG_Impl/Pipe/ProgramEmit.h @@ -663,7 +663,8 @@ namespace MobileGL::MG_Pipe { // composite. MGPipeHandle AcquireShaderCsoHandle(const ProgramObject& program) { const Uint64 lifetimeId = program.GetLifetimeId(); - const MGPipeHandle existing = MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId); + const MGPipeHandle existing = + MGPipeSlots().FindByLifetimeIdHinted(MGPipeKind::ShaderCso, lifetimeId, program.PipeHandleHint()); if (!MGPipeHandleIsNull(existing)) return existing; // A composite is minted off ITS OWN lifetime id, out of the reserved band, and is // an ordinary ShaderCso handle in every other respect - the same kind, the same diff --git a/MobileGL/MG_Impl/Pipe/ResourceTracker.h b/MobileGL/MG_Impl/Pipe/ResourceTracker.h index 427204ae..c1bf990f 100644 --- a/MobileGL/MG_Impl/Pipe/ResourceTracker.h +++ b/MobileGL/MG_Impl/Pipe/ResourceTracker.h @@ -281,7 +281,8 @@ namespace MobileGL::MG_Pipe { // gating it would make the vertex-input subsystem emit null handles whenever the // resource subsystem is off. Only the CALLS are gated (D-A1). MGPipeHandle Acquire(BufferObject& buffer) { - const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Buffer, buffer.GetLifetimeId()); + const MGPipeHandle handle = + MGPipeSlots().AcquireHinted(MGPipeKind::Buffer, buffer.GetLifetimeId(), buffer.PipeHandleHint()); const SizeT slot = handle.Slot; if (slot >= m_bySlot.size()) m_bySlot.resize(slot + 1); m_bySlot[slot].Object = &buffer; @@ -292,7 +293,8 @@ namespace MobileGL::MG_Pipe { // The handle a buffer already has, or the null handle. Never mints - the emission // path calls Acquire, the query paths call this. MGPipeHandle Find(const BufferObject& buffer) const { - return MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, buffer.GetLifetimeId()); + return MGPipeSlots().FindByLifetimeIdHinted(MGPipeKind::Buffer, buffer.GetLifetimeId(), + buffer.PipeHandleHint()); } // D-D's inverse, and a RAW pointer is exact here: the entry exists only between the diff --git a/MobileGL/MG_Impl/Pipe/SamplerEmit.h b/MobileGL/MG_Impl/Pipe/SamplerEmit.h index b5e79570..e0de902c 100644 --- a/MobileGL/MG_Impl/Pipe/SamplerEmit.h +++ b/MobileGL/MG_Impl/Pipe/SamplerEmit.h @@ -859,7 +859,9 @@ namespace MobileGL::MG_Pipe { unitSampler ? unitSampler.get() : texture->GetSamplerObject().get(); if (MG_State::GLState::SamplesAsIncompleteTexture(texture.get(), effective)) continue; - entry.Texture = MGPipeSlots().Acquire(MGPipeKind::Texture, texture->GetLifetimeId()); + entry.Texture = MGPipeSlots().AcquireHinted( + MGPipeKind::Texture, texture->GetLifetimeId(), + texture->PipeHandleHint(ITextureObject::PipeHintKind::Texture)); // D-A4: a texture the sampler-view resolution names in an emitted MGPBoundView // is SAMPLER-bound from then on (sticky; the texture emitter's contract door, // since this header is included BY TextureEmit.h). One early-out per unit per @@ -976,7 +978,8 @@ namespace MobileGL::MG_Pipe { MGPipeHandle AcquireSamplerView(const ITextureObject& texture, MGPipeHandle textureHandle, Uint64& payloadBytes) { const MGPipeHandle handle = - MGPipeSlots().Acquire(MGPipeKind::SamplerViewCso, texture.GetLifetimeId()); + MGPipeSlots().AcquireHinted(MGPipeKind::SamplerViewCso, texture.GetLifetimeId(), + texture.PipeHandleHint(ITextureObject::PipeHintKind::SamplerView)); const SizeT slot = handle.Slot; if (slot >= m_viewLatch.size()) m_viewLatch.resize(slot + 1); ViewLatch& latch = m_viewLatch[slot]; diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp index b570f538..1488605d 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp @@ -223,6 +223,31 @@ namespace MobileGL::MG_Pipe { return MGPipeHandle{it->second, entry->Gen}; } + MGPipeHandle MGPipeSlotAllocator::FindByLifetimeIdHinted(MGPipeKind kind, Uint64 lifetimeId, + Uint64& hint) const { + if (hint != 0 && lifetimeId != 0) { +#if MOBILEGL_BUILD_DISAGGREGATED + MGPipeRefuseAllocatorFromApplyThread("FindByLifetimeIdHinted"); +#endif + const Uint32 slot = static_cast(hint); + const Uint32 gen = static_cast(hint >> 32); + const SlotState* entry = EntryOf(StateOf(kind), kind, slot); + if (entry != nullptr && entry->Live && entry->Gen == gen && entry->LifetimeId == lifetimeId) + return MGPipeHandle{slot, gen}; + } + const MGPipeHandle found = FindByLifetimeId(kind, lifetimeId); + hint = MGPipeHandleIsNull(found) ? 0 : MGPipePackHandleHint(found); + return found; + } + + MGPipeHandle MGPipeSlotAllocator::AcquireHinted(MGPipeKind kind, Uint64 lifetimeId, Uint64& hint) { + const MGPipeHandle existing = FindByLifetimeIdHinted(kind, lifetimeId, hint); + if (!MGPipeHandleIsNull(existing)) return existing; + const MGPipeHandle minted = Acquire(kind, lifetimeId); + hint = MGPipeHandleIsNull(minted) ? 0 : MGPipePackHandleHint(minted); + return minted; + } + MGPipeHandle MGPipeSlotAllocator::Acquire(MGPipeKind kind, Uint64 lifetimeId) { #if MOBILEGL_BUILD_DISAGGREGATED MGPipeRefuseAllocatorFromApplyThread("Acquire"); diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.h b/MobileGL/MG_Impl/Pipe/SlotAllocator.h index 70115686..41027ad1 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.h +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.h @@ -72,6 +72,12 @@ namespace MobileGL::MG_Pipe { MGPipeHandle FindByLifetimeId(MGPipeKind kind, Uint64 lifetimeId) const; // FindByLifetimeId, then AllocateFor when it misses. The ordinary client path. MGPipeHandle Acquire(MGPipeKind kind, Uint64 lifetimeId); + // P15: the same two answers, first trying `hint` - the handle this object was last given, + // kept on the frontend object itself (MGPipePackHandleHint). The hint is believed only when + // the slot it names is live, at that generation, and owned by this very lifetime id, so a + // reset, a free or a recycle can only make it miss; the hint is refreshed on every miss. + MGPipeHandle FindByLifetimeIdHinted(MGPipeKind kind, Uint64 lifetimeId, Uint64& hint) const; + MGPipeHandle AcquireHinted(MGPipeKind kind, Uint64 lifetimeId, Uint64& hint); // Returns the slot to the free list. The Gen bump happens on the NEXT handout of that // slot, not here, so a handle that is freed twice cannot skip a generation and the @@ -179,6 +185,12 @@ namespace MobileGL::MG_Pipe { // The monolith's one client allocator. Under split there is one per client context. MGPipeSlotAllocator& MGPipeSlots(); + // A handle packed into the Uint64 a frontend object keeps as its hint (0 = none; slot 0 is + // never handed out). + inline Uint64 MGPipePackHandleHint(MGPipeHandle handle) { + return static_cast(handle.Slot) | (static_cast(handle.Gen) << 32); + } + #if MOBILEGL_BUILD_DISAGGREGATED // P5c (hd, CONTRACT-P5C §3.1 / §6 layer 1): with an active transport this allocator is a // CLIENT-only surface. Its singleton accessor and original three method guards reject apply - diff --git a/MobileGL/MG_Impl/Pipe/TextureEmit.h b/MobileGL/MG_Impl/Pipe/TextureEmit.h index 29cfd6a4..cf7774ed 100644 --- a/MobileGL/MG_Impl/Pipe/TextureEmit.h +++ b/MobileGL/MG_Impl/Pipe/TextureEmit.h @@ -590,7 +590,11 @@ namespace MobileGL::MG_Pipe { // gating the mint would make the other subsystems emit null handles in exactly the // A/B arm that exists to isolate them. Only the CALLS are gated. MGPipeHandle AcquireTexture(Uint64 lifetimeId, ITextureObject* object) { - const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Texture, lifetimeId); + const MGPipeHandle handle = + object != nullptr + ? MGPipeSlots().AcquireHinted(MGPipeKind::Texture, lifetimeId, + object->PipeHandleHint(ITextureObject::PipeHintKind::Texture)) + : MGPipeSlots().Acquire(MGPipeKind::Texture, lifetimeId); Entry& entry = EntryFor(m_textures, handle); RetireIfRecycled(entry, handle); entry.Texture = object; @@ -1287,7 +1291,12 @@ namespace MobileGL::MG_Pipe { // MGPipeResourceTracker::ResetForTest for the rule this restates: a texture handle and // the applier record it names are SHARE-GROUP OBJECT STATE, so nothing here is // per-context and no re-publication path exists or may exist. + // Moves whenever the handle table below is dropped (ResetForTest), so a memo that skipped + // NoteTextureBoundAs / AcquireTexture for handles it already noted knows to note again. + Uint64 TableEpoch() const { return m_tableEpoch; } + void ResetForTest() { + ++m_tableEpoch; // EVERY REFERENCE THIS EMITTER OWES IS GIVEN BACK FIRST. A case that dropped the // table without releasing would pin cache entries for the rest of the process and // the next case's LRU would mint over capacity for reasons it cannot see. @@ -1689,6 +1698,7 @@ namespace MobileGL::MG_Pipe { MGPTextureParams m_lastParams{}; MGPSubData m_lastSubData{}; + Uint64 m_tableEpoch = 0; Uint64 m_creates = 0; Uint64 m_respecifies = 0; Uint64 m_paramSets = 0; diff --git a/MobileGL/MG_Impl/Pipe/VertexInputEmit.h b/MobileGL/MG_Impl/Pipe/VertexInputEmit.h index 8828c704..f1402b23 100644 --- a/MobileGL/MG_Impl/Pipe/VertexInputEmit.h +++ b/MobileGL/MG_Impl/Pipe/VertexInputEmit.h @@ -256,13 +256,29 @@ namespace MobileGL::MG_Pipe { for (SizeT i = 0; i < kAttribs; ++i) { if (vao->GetAttribute(static_cast(i)).Enabled) count = static_cast(i) + 1; } + // P15: interleaved attributes share one buffer (FCL MC 1.21.5: every chunk draw + // re-points ~4 attributes at the next chunk's buffer), so the previous attribute's + // answer is reused for the same object - one lookup and one sticky-bit note per + // buffer instead of per attribute. Scoped to this loop: same object, same handle. + const MG_State::GLState::BufferObject* lastBuffer = nullptr; + MGPipeHandle lastHandle = kMGPipeNullHandle; for (SizeT i = 0; i < count; ++i) { const auto& attrib = vao->GetAttribute(static_cast(i)); MGPVertexBuffer& entry = m_entries[i]; entry = MGPVertexBuffer{}; - entry.Res = attrib.Buffer ? MGPipeSlots().Acquire(MGPipeKind::Buffer, - attrib.Buffer->GetLifetimeId()) - : kMGPipeNullHandle; + const Bool sameBuffer = attrib.Buffer && attrib.Buffer.get() == lastBuffer; + if (sameBuffer) { + entry.Res = lastHandle; + } else { + entry.Res = attrib.Buffer ? MGPipeSlots().AcquireHinted(MGPipeKind::Buffer, + attrib.Buffer->GetLifetimeId(), + attrib.Buffer->PipeHandleHint()) + : kMGPipeNullHandle; + if (attrib.Buffer) { + lastBuffer = attrib.Buffer.get(); + lastHandle = entry.Res; + } + } if (ownedClientBuffers != nullptr && attrib.Enabled && !attrib.Buffer) { entry.Res = (*ownedClientBuffers)[i]; } @@ -271,7 +287,7 @@ namespace MobileGL::MG_Pipe { // glNamedBuffer* may never be bound at any resource emission, but a draw // that fetches from it resolves it right here, on the GL thread, at every // draw. Sticky, so one draw is enough for the rest of its life. - MGPipeResourceTrackerInstance().NoteBoundAs(entry.Res, BufferTarget::Vertex); + if (!sameBuffer) MGPipeResourceTrackerInstance().NoteBoundAs(entry.Res, BufferTarget::Vertex); // The attribute's own byte offset lives in MGPVertexAttribWire::Offset, // so the entry's is the BINDING's, which the frontend already folded in. entry.Offset = 0; diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.h b/MobileGL/MG_State/GLState/BufferState/BufferObject.h index 0e71e48b..4ef3a66a 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.h +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.h @@ -202,6 +202,9 @@ namespace MobileGL { // glGenBuffers) and never the heap address (recycled by the allocator): both // let a deleted-and-recreated buffer answer to a dead one's cache entry. Uint64 GetLifetimeId() const { return m_lifetimeId; } + // P15: the pipe client's last Buffer handle for this object (MGPipeSlotAllocator:: + // AcquireHinted). Only a hint: it is checked against the allocator on every use. + Uint64& PipeHandleHint() const { return m_pipeHandleHint; } // Monotonic counter bumped on every shadow mutation; backends use it to // validate cached transient slices. Uint64 GetChangeSerial() const; @@ -238,6 +241,7 @@ namespace MobileGL { const Uint m_externalIndex = 0; const Uint64 m_lifetimeId = AllocateLifetimeId(); + mutable Uint64 m_pipeHandleHint = 0; SizeT m_size = 0; BufferUsage m_usage = BufferUsage::StaticDraw; // Owns the buffer's bytes (CPU shadow or backend persistent GPU map) and diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index a61a08bc..63445e95 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -1281,6 +1281,9 @@ namespace MobileGL::MG_State::GLState { // the next glCreateProgram, this distinguishes a deleted-and-recreated program from the // original, so an identity cache can't false-hit on name recycling. Uint64 GetLifetimeId() const { return m_lifetimeId; } + // P15: the pipe client's last ShaderCso handle for this program (MGPipeSlotAllocator:: + // FindByLifetimeIdHinted). Only a hint: it is checked against the allocator on every use. + Uint64& PipeHandleHint() const { return m_pipeHandleHint; } private: // ---- The one and only join gate for link output (P1 invariant I5) ---- @@ -1372,6 +1375,7 @@ namespace MobileGL::MG_State::GLState { // writes them. const Uint m_externalIndex = 0; const Uint64 m_lifetimeId = 0; + mutable Uint64 m_pipeHandleHint = 0; // The attach lists are mutated only in Link()'s GL-thread prologue, which is why // glGetAttachedShaders / GL_ATTACHED_SHADERS / the orphan-shader sweep need no join. Vector> m_shaders; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index 0b8af18a..66c1e454 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -82,6 +82,10 @@ namespace MobileGL::MG_State::GLState { virtual Bool HasFixedSampleLocations() const = 0; virtual void SetFixedSampleLocations(Bool fixedSampleLocations) = 0; virtual Uint64 GetLifetimeId() const = 0; + // P15: the pipe client's last handles for this texture, one per kind it is looked up under + // (MGPipeSlotAllocator::AcquireHinted). Only hints: checked against the allocator on use. + enum class PipeHintKind : Uint8 { Texture = 0, SamplerView = 1, Count = 2 }; + virtual Uint64& PipeHandleHint(PipeHintKind kind) const = 0; // Which aspect of a packed depth/stencil texture a sampler reads (GL 4.6 core 8.10). // DEPTH_COMPONENT until set, and meaningless for every other format. virtual GLenum GetDepthStencilTextureMode() const = 0; @@ -164,6 +168,7 @@ namespace MobileGL::MG_State::GLState { Bool HasFixedSampleLocations() const override; void SetFixedSampleLocations(Bool fixedSampleLocations) override; Uint64 GetLifetimeId() const override; + Uint64& PipeHandleHint(PipeHintKind kind) const override { return m_pipeHandleHints[static_cast(kind)]; } // A plain texture owns its storage; TextureObjectView overrides this. const SharedPtr& GetViewStorageOwner() const override; Uint GetViewMinLevel() const override { return m_viewMinLevel; } @@ -241,6 +246,7 @@ namespace MobileGL::MG_State::GLState { const Uint m_externalIndex; const Uint64 m_lifetimeId; + mutable Uint64 m_pipeHandleHints[static_cast(PipeHintKind::Count)] = {}; const TextureTarget m_target = TextureTarget::Unknown; TextureInternalFormat m_internalFormat = TextureInternalFormat::Unknown; SharedPtr m_sampler = nullptr; diff --git a/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp b/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp index 830310f2..4a6771f9 100644 --- a/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp @@ -1264,6 +1264,111 @@ TEST(FramebufferEmit, AFramebufferRecordThatNamesNoUsableHandleIsRefusedRatherTh << "an out-of-range slot resized the table instead of being refused"; } +// ============================ P15: the bound-target build memo ============================ +// +// EmitFramebufferState reuses the record it built for the same framebuffer and target while +// nothing the build reads has moved (MGPipeFramebufferEmitter::BuildBoundFramebufferState). Each +// case first proves the memo ENGAGED (an unchanged second emission is a hit), then moves one input +// the framebuffer's own version does not see and expects the record to follow it, by field name. +namespace { + MGPipeHandle HandleOfTexture(const SharedPtr& texture) { + return MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, texture->GetLifetimeId()); + } +} // namespace + +TEST(FramebufferEmit, TheBuildMemoEngagesAndFollowsAnAttachedTexturesRespecify) { + FramebufferScope scope; + const auto color = MakeColorTexture(80, 32); + const auto fbo = MakeShared(20); + fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D); + BindDrawAndRead(fbo, fbo); + Framebuffers().EmitFramebufferState(Ctx()); + const Uint64 hits = Framebuffers().MemoHitCount(); + EXPECT_EQ(Framebuffers().EmitFramebufferState(Ctx()), 0u); + ASSERT_EQ(Framebuffers().MemoHitCount(), hits + 1) << "an unchanged framebuffer did not reuse its built record"; + + // The texture's level 0 grows; the framebuffer's object version does not move. + color->AllocateStorage(TextureUploadTarget::Texture2D, 0, MipmapInput{IntVec3{64, 64, 1}, 64u * 64u * 4u}); + EXPECT_EQ(Framebuffers().EmitFramebufferState(Ctx()), sizeof(MGPFramebufferState)); + EXPECT_EQ(Framebuffers().LastDraw().Width, 64u) << "MGPFramebufferState::Width"; + EXPECT_EQ(Framebuffers().LastDraw().Height, 64u) << "MGPFramebufferState::Height"; +} + +// The build runs when the bound framebuffer changes, so the memo has to hold more than the last +// framebuffer: an A/B/A switch must find A's record again. +TEST(FramebufferEmit, TheBuildMemoKeepsEachFramebufferAcrossABindingSwitch) { + FramebufferScope scope; + const auto colorA = MakeColorTexture(83, 32); + const auto colorB = MakeColorTexture(84, 16); + const auto a = MakeShared(24); + const auto b = MakeShared(25); + a->AttachTexture(FramebufferAttachmentType::Color0, colorA, TextureUploadTarget::Texture2D); + b->AttachTexture(FramebufferAttachmentType::Color0, colorB, TextureUploadTarget::Texture2D); + BindDrawAndRead(a, a); + Framebuffers().EmitFramebufferState(Ctx()); + BindDrawAndRead(b, b); + Framebuffers().EmitFramebufferState(Ctx()); + const Uint64 hits = Framebuffers().MemoHitCount(); + BindDrawAndRead(a, a); + Framebuffers().EmitFramebufferState(Ctx()); + EXPECT_EQ(Framebuffers().MemoHitCount(), hits + 1) << "switching back to a framebuffer rebuilt its record"; + EXPECT_EQ(Framebuffers().LastDraw().Width, 32u) << "MGPFramebufferState::Width"; +} + +TEST(FramebufferEmit, TheBuildMemoFollowsARestoragedAttachedRenderbuffer) { + FramebufferScope scope; + const auto renderbuffer = MakeShared(4); + renderbuffer->SetInternalFormat(TextureInternalFormat::RGBA8); + renderbuffer->AllocateStorage(IntVec2{32, 32}); + const auto fbo = MakeShared(21); + fbo->AttachRenderbuffer(FramebufferAttachmentType::Color0, renderbuffer); + BindDrawAndRead(fbo, fbo); + Framebuffers().EmitFramebufferState(Ctx()); + const Uint64 hits = Framebuffers().MemoHitCount(); + Framebuffers().EmitFramebufferState(Ctx()); + ASSERT_EQ(Framebuffers().MemoHitCount(), hits + 1) << "an unchanged framebuffer did not reuse its built record"; + + renderbuffer->AllocateStorage(IntVec2{48, 40}); + Framebuffers().EmitFramebufferState(Ctx()); + EXPECT_EQ(Framebuffers().LastDraw().Width, 48u) << "MGPFramebufferState::Width"; + EXPECT_EQ(Framebuffers().LastDraw().Height, 40u) << "MGPFramebufferState::Height"; +} + +TEST(FramebufferEmit, TheBuildMemoFollowsAnAttachedTexturesSampleCount) { + FramebufferScope scope; + const auto color = MakeColorTexture(81, 32); + const auto fbo = MakeShared(22); + fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D); + BindDrawAndRead(fbo, fbo); + Framebuffers().EmitFramebufferState(Ctx()); + Framebuffers().EmitFramebufferState(Ctx()); + + // Samples move the params version, not the shape. + color->SetSamples(4); + Framebuffers().EmitFramebufferState(Ctx()); + EXPECT_EQ(Framebuffers().LastDraw().Samples, 4u) << "MGPFramebufferState::Samples"; +} + +TEST(FramebufferEmit, TheBuildMemoReNotesBindBitsAfterTheTextureTableIsDropped) { + FramebufferScope scope; + const auto color = MakeColorTexture(82, 32); + const auto fbo = MakeShared(23); + fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D); + BindDrawAndRead(fbo, fbo); + Framebuffers().EmitFramebufferState(Ctx()); + const MGPipeHandle handle = HandleOfTexture(color); + ASSERT_FALSE(MGPipeHandleIsNull(handle)); + ASSERT_NE(MGPipeTextureEmitterInstance().TextureBindMask(handle) & kMGPipeBindRenderTarget, 0) + << "the attachment's RENDER_TARGET bit was never noted"; + + // A dropped table (a replaced session) forgets the sticky bits; the next build must note them again. + MGPipeTextureEmitterInstance().ResetForTest(); + MGPipeSetHashSuppressorInstance().InvalidateAll(); + Framebuffers().EmitFramebufferState(Ctx()); + EXPECT_NE(MGPipeTextureEmitterInstance().TextureBindMask(handle) & kMGPipeBindRenderTarget, 0) + << "MGPResourceDesc::BindMask lost RENDER_TARGET after the texture table was dropped"; +} + int main(int argc, char** argv) { // Before anything logs: the logger reads this variable once, on its first write, and // caches the handle. The name carries this process's pid, and the file is removed on the diff --git a/MobileGL/MG_Test/Pipe/SlotAllocatorTest.cpp b/MobileGL/MG_Test/Pipe/SlotAllocatorTest.cpp index ec2d8099..de64fff8 100644 --- a/MobileGL/MG_Test/Pipe/SlotAllocatorTest.cpp +++ b/MobileGL/MG_Test/Pipe/SlotAllocatorTest.cpp @@ -97,6 +97,38 @@ namespace { EXPECT_TRUE(allocator.IsLive(MGPipeKind::Buffer, reused)); } + // P15: a hint (the handle a frontend object was last given, kept on the object) is a shortcut + // and never an answer of its own. It must give the live handle back, and it must miss - never + // name another object's handle - after a free, a recycle of its slot, or a reset. + TEST(SlotAllocator, AHandleHintIsOnlyBelievedWhileItsSlotStillBelongsToTheObject) { + MGPipeSlotAllocator allocator; + constexpr Uint64 kFirst = 1001, kSecond = 1002; + + Uint64 hint = 0; + const MGPipeHandle minted = allocator.AcquireHinted(MGPipeKind::Buffer, kFirst, hint); + ASSERT_FALSE(MGPipeHandleIsNull(minted)); + EXPECT_EQ(hint, MGPipePackHandleHint(minted)) << "a mint must leave the hint naming the new handle"; + EXPECT_TRUE(allocator.FindByLifetimeIdHinted(MGPipeKind::Buffer, kFirst, hint) == minted); + + // The slot is freed and handed to another object. The dead object's hint must miss, even + // when it is forged to carry the successor's exact slot and generation: the owner moved. + allocator.Free(MGPipeKind::Buffer, minted); + EXPECT_TRUE(MGPipeHandleIsNull(allocator.FindByLifetimeIdHinted(MGPipeKind::Buffer, kFirst, hint))) + << "a freed object's hint still answered"; + Uint64 staleHint = MGPipePackHandleHint(minted); + const MGPipeHandle successor = allocator.AcquireHinted(MGPipeKind::Buffer, kSecond, staleHint); + EXPECT_EQ(successor.Slot, minted.Slot) << "the free list did not hand the slot back"; + Uint64 forged = MGPipePackHandleHint(successor); // the successor's exact slot and generation + EXPECT_TRUE(MGPipeHandleIsNull(allocator.FindByLifetimeIdHinted(MGPipeKind::Buffer, kFirst, forged))) + << "a hint naming another object's live slot answered for the dead object"; + + // A reset forgets every handle; a hint from before it can only miss. + Uint64 beforeReset = MGPipePackHandleHint(successor); + allocator.Reset(); + EXPECT_TRUE(MGPipeHandleIsNull(allocator.FindByLifetimeIdHinted(MGPipeKind::Buffer, kSecond, beforeReset))) + << "a hint survived the allocator's reset"; + } + TEST(SlotAllocator, FreedSlotComesBackBeforeHighWaterGrows) { MGPipeSlotAllocator allocator; diff --git a/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp b/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp index 3a4a725e..a8958d56 100644 --- a/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp @@ -277,6 +277,34 @@ namespace { } } + // P15: the rebuild reuses the previous attribute's handle when it names the SAME buffer object + // (interleaved attributes), and only then. Two buffers interleaved A, A, B, A: every entry must + // carry its own buffer's handle, including the A after the B. + TEST(VertexInputEmit, InterleavedAttributesOnTwoBuffersEachCarryTheirOwnBuffersHandle) { + EmitterScope scope; + const SharedPtr vao = MakeVao(1); + const SharedPtr a = Ctx().CreateBufferObject(1); + const SharedPtr b = Ctx().CreateBufferObject(2); + a->Respecify(256, nullptr); + b->Respecify(256, nullptr); + const SharedPtr* owners[] = {&a, &a, &b, &a}; + for (Uint i = 0; i < 4; ++i) { + vao->SetAttributeFormat(i, 4, DataType::Float32, false, 16, static_cast(4 * i), false, false, 16); + vao->MirrorPointerIntoBinding(i, *owners[i], static_cast(4 * i), 16); + vao->BindAttributeBuffer(i, *owners[i]); + vao->EnableAttribute(i); + } + Emitter().EmitVertexBuffers(Ctx(), 0); + ASSERT_EQ(Emitter().LastVertexBuffers().Count, 4u); + for (Uint i = 0; i < 4; ++i) { + const MGPipeHandle expected = + MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, (*owners[i])->GetLifetimeId()); + ASSERT_FALSE(MGPipeHandleIsNull(expected)); + EXPECT_TRUE(Emitter().LastEntries()[i].Res == expected) + << "MGPVertexBuffer::Res of entry " << i << " is not its own buffer's handle"; + } + } + // KHR-GL43.vertex_attrib_binding.basic-input-case7/8: a pointer call's stride 0 means // "tightly packed" and the frontend already resolved it to the element size, so a zero // that reaches the wire can only have come from the binding model - where it means every