From 785df78d77a9a9c6ad0f77133c31e9ae8a9bef67 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 7 Oct 2026 16:06:02 -0400 Subject: [PATCH] [Perf] (P14): one UBO-ring lookup per draw, split residual copy/stamp loops, one context read for GPU-write marks, Magma slice stamps from the owner's counter (rd12 monolith Espryt 88.8 -> 90.3, Magma 69.8 -> 70.3 fps). --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 33 +++++++-------- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 19 +++++++++ MobileGL/MG_Backend/DirectGLES/Managers.h | 9 ++++ .../DirectVulkan/Renderer/VkBufferManager.cpp | 6 ++- .../DirectVulkan/Renderer/VkBufferManager.h | 5 +++ .../DirectVulkan/Renderer/VulkanRenderer.cpp | 1 + MobileGL/MG_Impl/Pipe/PipeFill.cpp | 41 ++++++++++++++----- MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.cpp | 31 +++++++------- 8 files changed, 97 insertions(+), 48 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index d379af9a..2a4303c3 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -5363,30 +5363,25 @@ namespace MobileGL::MG_Backend::DirectGLES { // below forced Adreno into a ghost/stall on every uniform-dirtying // draw (MC dirties uniforms every draw), which dominated frame time. Bool ringBound = false; - if (BufferImpl::UboRingAvailable()) { + { const SizeT bindSize = std::max(uboSize, static_cast(backendProgram.GetGlobalUboBackendBlockSize())); - const Uint64 frameSerial = CurrentFrameSerial(); auto& ringSlot = backendProgram.GetGlobalUboRingAllocation(); - Bool slotValid = ringSlot.ringGeneration == BufferImpl::UboRingGeneration() && - ringSlot.frameSerial == frameSerial && - ringSlot.contentVersion == uboContentVersion; - if (!slotValid) { - SizeT offset = 0; - if (BufferImpl::UboRingAllocate(bindSize, offset)) { - std::memcpy(static_cast(BufferImpl::UboRingMappedPtr()) + offset, - MGB_UBO_BYTES, uboSize); - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal, - static_cast(uboSize)); - } - ringSlot = {uboContentVersion, BufferImpl::UboRingGeneration(), frameSerial, - offset}; - slotValid = true; + void* write = nullptr; + Uint ringId = 0; + const auto state = BufferImpl::UboRingPrepareSlot(ringSlot, uboContentVersion, + CurrentFrameSerial(), bindSize, write, + ringId); + if (state == BufferImpl::UboRingSlotState::Allocated) { + std::memcpy(write, MGB_UBO_BYTES, uboSize); + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal, + static_cast(uboSize)); } } - if (slotValid) { - BufferImpl::BindBufferRangeCached(GL_UNIFORM_BUFFER, 0, BufferImpl::UboRingBufferId(), + if (state == BufferImpl::UboRingSlotState::Allocated || + state == BufferImpl::UboRingSlotState::Reused) { + BufferImpl::BindBufferRangeCached(GL_UNIFORM_BUFFER, 0, ringId, static_cast(ringSlot.offset), static_cast(bindSize)); ringBound = true; diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 1f0faf6c..ef2fe908 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -4040,6 +4040,25 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool UboRingAllocate(SizeT size, SizeT& outOffset) { return RingAllocate(UboRing(), size, outOffset); } + UboRingSlotState UboRingPrepareSlot(UboRingAllocation& slot, Uint32 contentVersion, Uint64 frameSerial, + SizeT bindSize, void*& outWrite, Uint& outBufferId) { + if (MG_Config::Features.EsprytDisableUboRing) return UboRingSlotState::Unavailable; + PersistentRing& ring = UboRing(); + if (!RingAvailable(ring)) return UboRingSlotState::Unavailable; + if (slot.ringGeneration == ring.store.generation && slot.frameSerial == frameSerial && + slot.contentVersion == contentVersion) { + outBufferId = ring.store.id; + return UboRingSlotState::Reused; + } + SizeT offset = 0; + if (!RingAllocate(ring, bindSize, offset)) return UboRingSlotState::Failed; + // Read after the allocation: a grow mints a new store and generation. + outWrite = static_cast(ring.store.mappedPtr) + offset; + slot = {contentVersion, ring.store.generation, frameSerial, offset}; + outBufferId = ring.store.id; + return UboRingSlotState::Allocated; + } + void* UboRingMappedPtr() { return UboRing().store.mappedPtr; } Uint UboRingBufferId() { return UboRing().store.id; } Uint32 UboRingGeneration() { return UboRing().store.generation; } diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 32e85be1..cac7a4ca 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -1109,6 +1109,15 @@ namespace MobileGL::MG_Backend::DirectGLES { // Grows the ring (new GL store, generation bump) when the in-flight span // would be overrun. Returns false when storage (re)creation fails. Bool UboRingAllocate(SizeT size, SizeT& outOffset); + // The per-draw slot dance in ONE ring lookup (the ring is per native context, and each + // lookup of it is a thread-local probe): Unavailable when the ring cannot serve (the + // caller takes the in-place upload), Reused when `slot` already holds this content in + // this frame and ring generation, Allocated when a fresh slot was carved (`outWrite` + // is where its bytes go), Failed when allocation failed. `outBufferId` names the ring + // store for Reused and Allocated. + enum class UboRingSlotState : Uint8 { Unavailable, Reused, Allocated, Failed }; + UboRingSlotState UboRingPrepareSlot(UboRingAllocation& slot, Uint32 contentVersion, Uint64 frameSerial, + SizeT bindSize, void*& outWrite, Uint& outBufferId); void* UboRingMappedPtr(); Uint UboRingBufferId(); Uint32 UboRingGeneration(); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index fee8fe70..02e3c849 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -426,8 +426,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // B3: the draw this slice is being acquired for is normally recorded into the NEXT // submission - normally, not always: a mid-draw flush (SyncWireTextureShape's preserve // path) can submit this index without the draw. This stamp is the defence term's input - // (see WriteWireBuffer); the floor behind lastUseSerial is the guarantee. - if (pVulkanRenderer) resource->lastUseSubmitIndex = pVulkanRenderer->GetWireNextSubmitIndex(); + // (see WriteWireBuffer); the floor behind lastUseSerial is the guarantee. Read off the + // owning renderer's counter (its GetWireNextSubmitIndex()) when it was handed one. + if (m_submitCounter != nullptr) resource->lastUseSubmitIndex = *m_submitCounter + 1; + else if (pVulkanRenderer) resource->lastUseSubmitIndex = pVulkanRenderer->GetWireNextSubmitIndex(); outSlice = resource->buffer.GetSlice(0, resource->size); outSlice.mapped = nullptr; if (WireBufProbeEnabled()) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index df452c7a..98f9efb3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -123,6 +123,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // frameSerial-minus-frameCount inference. void NotifyFrameSerialComplete(Uint64 serial); void SetCopyCommandProvider(IBufferCopyCommandProvider* provider); + // The owning renderer's submission counter: AcquireWireSlice stamps the next submission + // from it directly rather than finding the renderer through the session-local slot on + // every bind (a thread-local probe per vertex, index and uniform buffer of every draw). + void SetSubmitCounterSource(const Uint64* submitCounter) { m_submitCounter = submitCounter; } Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice); @@ -290,6 +294,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // See AcquireUnboundTexelBufferDescriptor. Same lifetime rules. VkBufferObject m_unboundTexelBuffer; IBufferCopyCommandProvider* m_copyProvider = nullptr; + const Uint64* m_submitCounter = nullptr; Vector> m_deferredBufferReleases; std::unordered_map m_wireBuffers; // P14: a slot-indexed front for m_wireBuffers (every draw resolves each vertex, index and diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index e9c51bf3..de9e7b8b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2205,6 +2205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { }); MOBILEGL_ASSERT(succeeded, "VkBufferManager initialization failed."); m_bufferManager.SetCopyCommandProvider(this); + m_bufferManager.SetSubmitCounterSource(&m_submitCounter); if (m_timerQuerySupported) { m_timerQueryManager = MakeUnique(); if (m_timerQueryManager->Initialize({.device = m_device, diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 9d8c6550..4975b25e 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -2932,6 +2932,10 @@ namespace MobileGL::MG_Pipe { Array WalkCount{}; Array, kClasses> Sticky{}; Array StickyCount{}; + // The walk's copy entries alone, in walk order (~11 of a draw's ~47): the copy loop + // visits only these, and the stamp loop runs apart over the whole walk. + Array, kClasses> Copy{}; + Array CopyCount{}; }; ResidualFillPlan g_fillPlan; @@ -3008,6 +3012,7 @@ namespace MobileGL::MG_Pipe { const MGPipeFieldMask& classMask = kMGPipeClassFieldMask[c]; Uint8 walk = 0; Uint8 sticky = 0; + Uint8 copy = 0; for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) { const auto field = static_cast(i); if (!MGPipeFieldMaskHas(classMask, field)) continue; @@ -3015,10 +3020,12 @@ namespace MobileGL::MG_Pipe { g_fillPlan.Sticky[c][sticky++] = static_cast(i); continue; } - g_fillPlan.Walk[c][walk++] = static_cast( - i | (MGPipeFieldMaskHas(built, field) ? 0 : ResidualFillPlan::kCopy)); + const Bool copied = !MGPipeFieldMaskHas(built, field); + g_fillPlan.Walk[c][walk++] = static_cast(i | (copied ? ResidualFillPlan::kCopy : 0)); + if (copied) g_fillPlan.Copy[c][copy++] = static_cast(i); } g_fillPlan.WalkCount[c] = walk; + g_fillPlan.CopyCount[c] = copy; g_fillPlan.StickyCount[c] = sticky; } return built; @@ -3864,16 +3871,28 @@ namespace MobileGL::MG_Pipe { if (filled.FilledGen[i] == 0) filled.FilledGen[i] = 1; } #endif - for (SizeT k = 0; fillOwed && k < g_fillPlan.WalkCount[planClass]; ++k) { - const Uint8 entry = g_fillPlan.Walk[planClass][k]; - const SizeT i = entry & ~ResidualFillPlan::kCopy; - const auto field = static_cast(i); - if (entry & ResidualFillPlan::kCopy) MGPipeFillAccess::CopyField(inputs, *ctx, field); + if (fillOwed) { + // The copies first, then the stamps: no copy reads a stamp, so splitting the one walk + // into two loops changes nothing but the cost (the stamp loop no longer reloads its + // bounds and the omission pair around an out-of-line CopyField per entry). + const Uint8* const copies = g_fillPlan.Copy[planClass].data(); + const SizeT copyCount = g_fillPlan.CopyCount[planClass]; + for (SizeT k = 0; k < copyCount; ++k) { + MGPipeFillAccess::CopyField(inputs, *ctx, static_cast(copies[k])); + } #if MOBILEGL_PIPE_POISON - // The value is copied either way; only the stamp is withheld for the omitted pair. - if (!IsOmitted(verb, field)) filled.FilledGen[i] = filled.CurrentVerbSerial; -#else - (void)field; + // The value is copied either way; only the stamp is withheld for the omitted pair, + // which is put back to what it held after the branch-free stamp of the whole walk. + const Uint8* const walk = g_fillPlan.Walk[planClass].data(); + const SizeT walkCount = g_fillPlan.WalkCount[planClass]; + const Uint64 serial = filled.CurrentVerbSerial; + const Bool omit = g_omission.Armed && g_omission.Verb == verb; + const SizeT omitted = omit ? static_cast(g_omission.Field) : 0; + const Uint64 omittedGen = omit ? filled.FilledGen[omitted] : 0; + for (SizeT k = 0; k < walkCount; ++k) { + filled.FilledGen[walk[k] & ~ResidualFillPlan::kCopy] = serial; + } + if (omit) filled.FilledGen[omitted] = omittedGen; #endif } if (payloadBytes != 0 && MG_Util::PipeStats::Enabled()) { diff --git a/MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.cpp b/MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.cpp index 97e55644..575fefe8 100644 --- a/MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.cpp +++ b/MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.cpp @@ -31,8 +31,7 @@ namespace MobileGL::MG_Record { // The transform-feedback rows, shared by the draw walk (row 3) and by // glEndTransformFeedback (row 5). Both mark the SAME set - the capture targets of the // capture program - and the split exists only so the two can be counted apart. - void MarkTransformFeedbackTargets(GpuWriteProducer producer) { - auto& context = MG_State::pGLContext; + void MarkTransformFeedbackTargets(MG_State::GLState::GLContext* context, GpuWriteProducer producer) { if (!context) return; if (!context->IsTransformFeedbackActive()) return; const auto& program = context->GetTransformFeedbackProgram(); @@ -49,8 +48,7 @@ namespace MobileGL::MG_Record { } } - void MarkShaderStorageBindings() { - auto& context = MG_State::pGLContext; + void MarkShaderStorageBindings(MG_State::GLState::GLContext* context) { if (!context) return; // The TOUCHED count, exactly as the backend twin uses it // (DirectGLES.cpp:559-560): the binding-point array is 84 entries wide and @@ -62,8 +60,7 @@ namespace MobileGL::MG_Record { } } - void MarkAtomicCounterBindings() { - auto& context = MG_State::pGLContext; + void MarkAtomicCounterBindings(MG_State::GLState::GLContext* context) { if (!context) return; // WIDER THAN ITS BACKEND TWIN, ON PURPOSE AND IN THE SAFE DIRECTION. // SyncAtomicCounterBuffers (DirectGLES.cpp:578-583) walks the GL bindings the @@ -79,8 +76,7 @@ namespace MobileGL::MG_Record { } } - void MarkWritableImageBufferTextures() { - auto& context = MG_State::pGLContext; + void MarkWritableImageBufferTextures(MG_State::GLState::GLContext* context) { if (!context) return; // The backend keeps a bitset of writable image-buffer units // (DirectGLES.cpp:2350-2352, maintained from its own SyncImageTextureBinding) and @@ -139,17 +135,20 @@ namespace MobileGL::MG_Record { void MarkGpuWritesForDraw() { if (!GpuWriteSetIsClientSide()) return; - MarkShaderStorageBindings(); - MarkAtomicCounterBindings(); - MarkWritableImageBufferTextures(); - MarkTransformFeedbackTargets(GpuWriteProducer::TransformFeedbackCapture); + // One read of the thread-local context for the four walks. + MG_State::GLState::GLContext* const context = MG_State::pGLContext.get(); + MarkShaderStorageBindings(context); + MarkAtomicCounterBindings(context); + MarkWritableImageBufferTextures(context); + MarkTransformFeedbackTargets(context, GpuWriteProducer::TransformFeedbackCapture); } void MarkGpuWritesForDispatch() { if (!GpuWriteSetIsClientSide()) return; - MarkShaderStorageBindings(); - MarkAtomicCounterBindings(); - MarkWritableImageBufferTextures(); + MG_State::GLState::GLContext* const context = MG_State::pGLContext.get(); + MarkShaderStorageBindings(context); + MarkAtomicCounterBindings(context); + MarkWritableImageBufferTextures(context); } void MarkReadPixelsPackBuffer() { @@ -162,7 +161,7 @@ namespace MobileGL::MG_Record { void MarkEndTransformFeedbackCaptureTargets() { if (!GpuWriteSetIsClientSide()) return; - MarkTransformFeedbackTargets(GpuWriteProducer::EndTransformFeedbackCapture); + MarkTransformFeedbackTargets(MG_State::pGLContext.get(), GpuWriteProducer::EndTransformFeedbackCapture); } Uint64 ProducerMarkCount(GpuWriteProducer producer) {