[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).

This commit is contained in:
2026-10-07 16:06:02 -04:00
parent 0bd0abf652
commit 785df78d77
8 changed files with 97 additions and 48 deletions
+14 -19
View File
@@ -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<SizeT>(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<Uint8*>(BufferImpl::UboRingMappedPtr()) + offset,
MGB_UBO_BYTES, uboSize);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal,
static_cast<Uint64>(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<Uint64>(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<GLintptr>(ringSlot.offset),
static_cast<GLsizeiptr>(bindSize));
ringBound = true;
@@ -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<Uint8*>(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; }
@@ -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();
@@ -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()) {
@@ -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<Vector<VkBufferObject>> m_deferredBufferReleases;
std::unordered_map<Uint64, WireBufferResource> m_wireBuffers;
// P14: a slot-indexed front for m_wireBuffers (every draw resolves each vertex, index and
@@ -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<VkTimerQueryManager>();
if (m_timerQueryManager->Initialize({.device = m_device,
+30 -11
View File
@@ -2932,6 +2932,10 @@ namespace MobileGL::MG_Pipe {
Array<Uint8, kClasses> WalkCount{};
Array<Array<Uint8, kMGPipeInputFieldCount>, kClasses> Sticky{};
Array<Uint8, kClasses> 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<Array<Uint8, kMGPipeInputFieldCount>, kClasses> Copy{};
Array<Uint8, kClasses> 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<MGPipeInputField>(i);
if (!MGPipeFieldMaskHas(classMask, field)) continue;
@@ -3015,10 +3020,12 @@ namespace MobileGL::MG_Pipe {
g_fillPlan.Sticky[c][sticky++] = static_cast<Uint8>(i);
continue;
}
g_fillPlan.Walk[c][walk++] = static_cast<Uint8>(
i | (MGPipeFieldMaskHas(built, field) ? 0 : ResidualFillPlan::kCopy));
const Bool copied = !MGPipeFieldMaskHas(built, field);
g_fillPlan.Walk[c][walk++] = static_cast<Uint8>(i | (copied ? ResidualFillPlan::kCopy : 0));
if (copied) g_fillPlan.Copy[c][copy++] = static_cast<Uint8>(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<MGPipeInputField>(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<MGPipeInputField>(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<SizeT>(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()) {
+15 -16
View File
@@ -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) {