diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 3fd0fa13..8916a878 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -3165,11 +3165,22 @@ namespace MobileGL::MG_Pipe { values.TransformFeedbackGeneration = ctx.GetTransformFeedbackGeneration(); values.BoundTransformFeedbackLifetimeId = ctx.GetBoundTransformFeedbackLifetimeId(); values.TransformFeedbackCapturedVertices = ctx.GetTransformFeedbackCapturedVertices(); + // P14: the record is rebuilt every draw and nearly always equal to the last one. When it + // is byte-equal to the record whose hash the suppressor still holds, the suppressor + // would drop it: skip the hash. (An InvalidateAll changes what the suppressor holds, + // so a reset still re-emits.) + static MGPContextValues s_lastValues{}; + static Uint64 s_lastHash = 0; + auto& suppressor = MGPipeSetHashSuppressorInstance(); + if (s_lastHash != 0 && std::memcmp(&values, &s_lastValues, sizeof(values)) == 0 && + suppressor.LastEmitted(MGPipeSuppressorSlot::SetContextValues) == s_lastHash) + return 0; // The whole record is the hash input, padding included - `values{}` zeroes it, so // the pad bytes are defined and the hash is stable. const Uint64 contentHash = XXH64(&values, sizeof(values), 0); - if (!MGPipeSetHashSuppressorInstance().ShouldEmit(MGPipeSuppressorSlot::SetContextValues, - contentHash)) { + s_lastValues = values; + s_lastHash = contentHash == 0 ? 1 : contentHash; // the suppressor's own latch value + if (!suppressor.ShouldEmit(MGPipeSuppressorSlot::SetContextValues, contentHash)) { return 0; } MGPipeRouteSetContextValues(values); diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp index 006af2fa..b570f538 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp @@ -308,6 +308,7 @@ namespace MobileGL::MG_Pipe { } void MGPipeSlotAllocator::Reset() { + ++m_resetEpoch; for (KindState& state : m_kinds) { state.Slots.clear(); state.FreeList.clear(); diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.h b/MobileGL/MG_Impl/Pipe/SlotAllocator.h index a3a368c7..70115686 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.h +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.h @@ -120,6 +120,9 @@ namespace MobileGL::MG_Pipe { // Context teardown / server reset / a unit test's fixture. void Reset(); + // Moves at every Reset(): a memo of handles minted by this allocator is good only while it + // holds (a handle lives as long as its object, and only a reset forgets that). + Uint64 ResetEpoch() const { return m_resetEpoch; } private: struct SlotState { @@ -161,6 +164,7 @@ namespace MobileGL::MG_Pipe { Uint32 BandLiveCount = 0; }; + Uint64 m_resetEpoch = 0; KindState& StateOf(MGPipeKind kind); const KindState& StateOf(MGPipeKind kind) const; // The SlotState a (kind, slot) names, in whichever of the two vectors holds it, or diff --git a/MobileGL/MG_Impl/Pipe/VertexInputEmit.h b/MobileGL/MG_Impl/Pipe/VertexInputEmit.h index 022e0665..eff5a2dc 100644 --- a/MobileGL/MG_Impl/Pipe/VertexInputEmit.h +++ b/MobileGL/MG_Impl/Pipe/VertexInputEmit.h @@ -168,7 +168,12 @@ namespace MobileGL::MG_Pipe { const Uint64 lifetimeId = vao->GetLifetimeId(); const Uint32 configVersion = vao->GetConfigVersion(); - const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::VertexElementsCso, lifetimeId); + // The VAO's handle is the allocator's for the object's whole life (only an allocator + // reset forgets it), so it is resolved once per VAO rather than per draw (VaoMemo). + VaoMemo& memo = MemoFor(*vao); + if (MGPipeHandleIsNull(memo.ElementsHandle)) + memo.ElementsHandle = MGPipeSlots().Acquire(MGPipeKind::VertexElementsCso, lifetimeId); + const MGPipeHandle handle = memo.ElementsHandle; const SizeT slot = handle.Slot; if (slot >= m_latch.size()) m_latch.resize(slot + 1); Latch& latch = m_latch[slot]; @@ -200,7 +205,20 @@ namespace MobileGL::MG_Pipe { const Array* ownedClientBuffers = nullptr) { const auto& vao = ctx.GetBoundVertexArray(); Uint32 count = 0; - if (vao) { + // The resolved set is a function of the VAO's configuration alone (buffers, strides, + // divisors and enables all move its version; its buffers' handles live as long as the + // VAO holds the buffers), so it is walked once per configuration rather than per draw - + // a Minecraft frame binds hundreds of chunk VAOs. Not for client-memory arrays, whose + // entries the draw itself supplies. + VaoMemo* memo = vao && ownedClientBuffers == nullptr ? &MemoFor(*vao) : nullptr; + if (memo != nullptr && memo->BuffersValid && memo->ConfigVersion == vao->GetConfigVersion()) { + count = memo->BufferCount; + for (SizeT i = 0; i < count; ++i) { + m_entries[i] = memo->Buffers[i]; + MGPipeResourceTrackerInstance().NoteBoundAs(m_entries[i].Res, BufferTarget::Vertex); + } + } else if (vao) { + Bool memoizable = memo != nullptr; for (SizeT i = 0; i < kAttribs; ++i) { if (vao->GetAttribute(static_cast(i)).Enabled) count = static_cast(i) + 1; } @@ -230,6 +248,13 @@ namespace MobileGL::MG_Pipe { entry.Stride = static_cast(attrib.Stride); entry.Divisor = static_cast(attrib.Divisor); entry.BindingIndex = static_cast(i); + if (attrib.Enabled && !attrib.Buffer) memoizable = false; + } + if (memo != nullptr) { + memo->BuffersValid = memoizable && count <= kVaoMemoBuffers; + memo->ConfigVersion = vao->GetConfigVersion(); + memo->BufferCount = count; + for (SizeT i = 0; i < count && i < kVaoMemoBuffers; ++i) memo->Buffers[i] = m_entries[i]; } } @@ -261,7 +286,13 @@ namespace MobileGL::MG_Pipe { m_lastIndex = MGPIndexBuffer{}; if (vao) { if (const auto& bound = vao->GetIndexBufferBindingSlot().GetBoundObject()) { - m_lastIndex.Res = MGPipeSlots().Acquire(MGPipeKind::Buffer, bound->GetLifetimeId()); + // Per VAO, keyed on the element buffer's own lifetime id (VaoMemo). + VaoMemo& memo = MemoFor(*vao); + if (memo.IndexLifetimeId != bound->GetLifetimeId() || MGPipeHandleIsNull(memo.IndexHandle)) { + memo.IndexLifetimeId = bound->GetLifetimeId(); + memo.IndexHandle = MGPipeSlots().Acquire(MGPipeKind::Buffer, memo.IndexLifetimeId); + } + m_lastIndex.Res = memo.IndexHandle; // The INDEX|ELEMENT_ARRAY bind bits. ELEMENT_ARRAY's D-B7 reader // (kCapNeedsHostIndexBytes) is RESERVED (P8-F) and was never built. // Noted at every draw for RefreshBindMask's reason: an EBO defined through @@ -353,6 +384,36 @@ namespace MobileGL::MG_Pipe { void ResetCounters() { m_creates = m_binds = m_bufferSets = m_indexSets = 0; } private: + // P14: what EmitVertexElements / EmitVertexBuffers / EmitIndexBuffer resolve from a VAO, + // direct-mapped on the VAO's lifetime id (monotonic, never reused) and dropped whole at an + // allocator reset. A colliding VAO simply takes the entry over. It changes no record: the + // same handles and entries are emitted, only found without the per-draw walk and lookups. + static constexpr SizeT kVaoMemoEntries = 1024; + static constexpr SizeT kVaoMemoBuffers = 8; + struct VaoMemo { + Uint64 LifetimeId = 0; + Uint64 SlotEpoch = 0; + MGPipeHandle ElementsHandle = kMGPipeNullHandle; + Uint64 IndexLifetimeId = 0; + MGPipeHandle IndexHandle = kMGPipeNullHandle; + Bool BuffersValid = false; + Uint32 ConfigVersion = 0; + Uint32 BufferCount = 0; + Array Buffers{}; + }; + VaoMemo& MemoFor(const VertexArrayObject& vao) { + const Uint64 lifetimeId = vao.GetLifetimeId(); + const Uint64 epoch = MGPipeSlots().ResetEpoch(); + VaoMemo& memo = m_vaoMemo[lifetimeId % kVaoMemoEntries]; + if (memo.LifetimeId != lifetimeId || memo.SlotEpoch != epoch) { + memo = VaoMemo{}; + memo.LifetimeId = lifetimeId; + memo.SlotEpoch = epoch; + } + return memo; + } + Array m_vaoMemo{}; + struct Latch { // The PER-CONTEXT half: "has this emitter told THIS server about this handle's // configuration". Cleared by Reset() at every make-current.