[Perf] (MG_Backend): give DirectGLES one epoch that says no buffer moved

Four draw-path costs, one theme: re-proving what nothing invalidated.

A manager-wide buffer-mutation epoch (atomic; bumped with release AFTER
every mutation lands: all six BufferBackendOps via tracking wrappers,
every backend-initiated writeback - XFB readback/scatter, the five
pack-PBO readbacks - registry registration changes, and backend context
destruction; the full site inventory lives in a comment at the accessor)
lets the per-VAO resolved-buffers memo stamp the epoch after one
all-clean probe pass and skip every IsBufferDrawClean probe while it
holds. The IBO keeps its bound-object identity compare - only the probe
is elided. Non-bumping paths are enumerated with why they are safe:
GPU-authoritative writes are ignored by the probe, persistent-mapped
resources are clean by construction, and draws on non-persistent maps
are frontend-rejected GL errors.

GetProgramForDraw is hoisted to one call per PrepareForDraw and handed
to the four consumers that each re-derived it. The enabled-draw-buffers
walk feeding the fragColor broadcast count is memoized on the
(FBO, slot version, object version) trio. The UBO-binding loop probes
IsBufferDrawClean before falling back to EnsureBufferResource.

The texture chain captures (context, maxTouchedUnit, samplingGeneration,
unitBindingsEpoch) once per draw - shared by SyncNeccessaryTextures and
BindCurrentTextures, halving the epoch computations - and an aggregate
gate that is the exact conjunction of the three Sync*ToBackend
early-outs skips the per-texture cross-TU calls.

The t_egl* thread_local verification pair became owner-thread-guarded
atomics reset by MakeCurrent/ReleaseCurrent, removing __tls_get_addr
from the draw loop.

Quiet-box 6-round order-alternating A/B (with the frontend VAO-bind
commit): all NINE Espryt cases improved - sampler_churn -11.0%,
state_toggle -9.3%, ubo_range -9.1%, vanilla_draw -5.4%, pass_switch
-3.5%, the rest -1% to -2.5%. Unit tests 421/421.
This commit is contained in:
BZLZHH
2026-08-06 13:16:44 -04:00
parent f8069c0624
commit b9d8ad0421
4 changed files with 383 additions and 73 deletions
+229 -66
View File
@@ -414,14 +414,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
// memoed frontend pointer via the attribute SharedPtrs), the walk reduces to
// an IsBufferDrawClean probe per distinct buffer; only dirty entries take
// EnsureBufferResource, re-fetched through their attribute index.
// Pre-pass epoch read (acquire), stamped into the memo only after a pass in
// which EVERY probe came up clean: while the stamp still equals the current
// epoch, no path that can dirty any buffer has run (see the mutation-site
// enumeration at CurrentBufferMutationEpoch's declaration in Managers.h),
// so the probes themselves are skipped. A mid-pass bump lands after this
// read, makes the stamp stale, and re-runs the probes next draw.
const Uint64 bufferEpoch = CurrentBufferMutationEpoch();
auto* memo = vaoTwin ? &vaoTwin->GetResolvedDrawBuffersMemo() : nullptr;
const Uint32 configVersion = currentVAOObject->GetConfigVersion();
if (memo && memo->valid && memo->configVersion == configVersion) {
for (Uint i = 0; i < memo->count; ++i) {
auto& entry = memo->entries[i];
if (IsBufferDrawClean(entry.frontend, entry.resource)) continue;
// Same object the entry was built from: config version unchanged.
entry.resource = EnsureBufferResource(currentVAOObject->GetAttribute(entry.attribIndex).Buffer);
if (memo->vboCleanEpoch != bufferEpoch) {
Bool allClean = true;
for (Uint i = 0; i < memo->count; ++i) {
auto& entry = memo->entries[i];
if (IsBufferDrawClean(entry.frontend, entry.resource)) continue;
allClean = false;
// Same object the entry was built from: config version unchanged.
entry.resource =
EnsureBufferResource(currentVAOObject->GetAttribute(entry.attribIndex).Buffer);
}
// Entries EnsureBufferResource repaired are not re-probed here; the
// next pass's clean probes stamp them (one extra pass, never a skip
// of work).
memo->vboCleanEpoch = allClean ? bufferEpoch : 0;
}
} else {
// Full walk, once per distinct buffer (an interleaved Minecraft-shaped VAO
@@ -461,6 +477,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
memo->count = syncedBufferCount;
memo->configVersion = configVersion;
memo->valid = true;
// Rebuilt via EnsureBufferResource, not probed clean: the next
// probe pass stamps the epoch.
memo->vboCleanEpoch = 0;
}
}
@@ -470,12 +489,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (includeIBO) {
const auto& possibleIBO = currentVAOObject->GetIndexBufferBindingSlot().GetBoundObject();
if (possibleIBO) {
if (!memo || memo->iboFrontend != possibleIBO.get() ||
!IsBufferDrawClean(memo->iboFrontend, memo->iboResource)) {
// The epoch stamp alone is NOT enough here: the index slot can
// rebind another buffer with no epoch (and no config-version) move,
// so the identity compare always runs; only the clean PROBE is
// elided while the stamp holds.
if (memo && memo->iboFrontend == possibleIBO.get() && memo->iboCleanEpoch == bufferEpoch) {
// probed fully clean at this epoch; nothing can have dirtied it
} else if (memo && memo->iboFrontend == possibleIBO.get() &&
IsBufferDrawClean(memo->iboFrontend, memo->iboResource)) {
memo->iboCleanEpoch = bufferEpoch;
} else {
auto* resource = EnsureBufferResource(possibleIBO);
if (memo) {
memo->iboFrontend = possibleIBO.get();
memo->iboResource = resource;
// Repaired, not probed clean: stamp on the next clean probe.
memo->iboCleanEpoch = 0;
}
}
}
@@ -611,6 +640,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
continue;
}
target.buffer->WritebackFromBackend({mapped, size}, target.start);
// WritebackFromBackend bumps the frontend change serial with no
// backend op, leaving the buffer draw-dirty behind the epoch's
// back; re-open the draw-clean memos.
BufferImpl::BumpBufferMutationEpoch();
g_GLESFuncs.glUnmapBuffer(BufferImpl::TempBufferTarget);
}
}
@@ -697,6 +730,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
target.buffer->WritebackFromBackend({staged.data(), rangeBytes}, target.start);
// Serial bumped with no backend op (see ReadbackCapturedRanges).
BufferImpl::BumpBufferMutationEpoch();
if (g_GLESFuncs.glBufferSubData != nullptr) {
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, target.backendId);
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget,
@@ -900,11 +935,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the shader its own current values, which MobileGL never writes -- i.e. always (0,0,0,1).
// SyncToBackend has already issued glDisableVertexAttribArray for these locations, so the ES
// current value is what the shader will actually read.
void SyncCurrentVertexAttributeValues(BackendVertexArrayObject* vaoTwin) {
void SyncCurrentVertexAttributeValues(BackendVertexArrayObject* vaoTwin,
const SharedPtr<MG_State::GLState::ProgramObject>& program) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const auto& program = MG_State::pGLContext->GetProgramForDraw();
if (!program) return;
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
@@ -1103,7 +1138,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Uint64 g_fboTextureSyncListContextId = 0;
static Uint g_fboTextureSyncListContextGeneration = 0;
void SyncNeccessaryTextures() {
// The frontend texture-state keys the per-draw texture stages
// (SyncNeccessaryTextures, then BindCurrentTextures) both consume. Captured
// ONCE per draw/dispatch preparation and passed to both: nothing between the
// two stages can move any of these - only frontend GL entry points mutate
// them, and none run inside a backend preparation (the FBO/program syncs in
// between only read frontend state). The no-arg wrappers keep capturing at
// the call for every non-draw call site (Clear, readbacks).
struct DrawTextureSyncKeys {
Uint64 contextId = 0;
Uint64 samplingGeneration = 0;
Uint64 unitBindingsEpoch = 0;
Int maxTouchedUnit = -1;
};
DrawTextureSyncKeys CaptureDrawTextureSyncKeys() {
DrawTextureSyncKeys keys;
keys.contextId = MG_State::pGLContext->GetTextureContextId();
// Units past the frontend's high-water mark have provably-empty slots.
keys.maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit();
keys.samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
keys.unitBindingsEpoch = CurrentUnitBindingsEpoch(keys.maxTouchedUnit);
return keys;
}
void SyncNeccessaryTextures(const DrawTextureSyncKeys& keys) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
@@ -1114,22 +1173,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 2. textures used in current FBO
// 3. textures bound to image units (TODO)
// Units past the frontend's high-water mark have provably-empty slots.
const Int maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit();
const Uint64 samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
const Uint64 unitBindingsEpoch = CurrentUnitBindingsEpoch(maxTouchedUnit);
const Int maxTouchedUnit = keys.maxTouchedUnit;
const Uint64 samplingGeneration = keys.samplingGeneration;
const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch;
// The epoch survives redundant re-binds; the sampling-resolution generation
// covers the one membership input the epoch cannot see - a default texture's
// image appearing or vanishing flips IsUndefinedDefaultTexture with no binding
// moving. Its cost is a spare rebuild whenever any texture shape or sampler
// parameter actually changes, which real frames do at load time, not per draw.
if (g_unitTextureSyncListValid &&
g_unitTextureSyncListContextId == MG_State::pGLContext->GetTextureContextId() &&
g_unitTextureSyncListContextId == keys.contextId &&
g_unitTextureSyncListMaxUnit == maxTouchedUnit &&
g_unitTextureSyncListContextGeneration == g_textureContextGeneration &&
g_unitTextureSyncListEpoch == unitBindingsEpoch &&
g_unitTextureSyncListSamplingGeneration == samplingGeneration) {
for (const auto& entry : g_unitTextureSyncList) {
// Aggregate gate == the conjunction of the three callees' own
// early-outs (see IsDrawSyncClean); skipping on true is
// behavior-identical, false falls through to the calls.
if (entry.backend->IsDrawSyncClean(entry.slot->get(), keys.contextId, samplingGeneration)) {
continue;
}
entry.backend->SyncTextureParamsToBackend(*entry.slot);
entry.backend->SyncBuiltinSamplerToBackend(*entry.slot);
entry.backend->SyncMipmapsToBackend(*entry.slot);
@@ -1149,7 +1213,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
}
g_unitTextureSyncListContextId = MG_State::pGLContext->GetTextureContextId();
g_unitTextureSyncListContextId = keys.contextId;
g_unitTextureSyncListMaxUnit = maxTouchedUnit;
g_unitTextureSyncListContextGeneration = g_textureContextGeneration;
g_unitTextureSyncListEpoch = unitBindingsEpoch;
@@ -1178,10 +1242,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_fboTextureSyncListFbo == currentFBO.get() &&
g_fboTextureSyncListSlotVersion == fboSlotVersion &&
g_fboTextureSyncListObjectVersion == fboObjectVersion &&
g_fboTextureSyncListContextId == MG_State::pGLContext->GetTextureContextId() &&
g_fboTextureSyncListContextId == keys.contextId &&
g_fboTextureSyncListContextGeneration == g_textureContextGeneration;
if (fboListValid) {
for (const auto& entry : g_fboTextureSyncList) {
// Same aggregate gate as the unit list above.
if (entry.backend->IsDrawSyncClean(entry.slot->get(), keys.contextId,
samplingGeneration)) {
continue;
}
entry.backend->SyncTextureParamsToBackend(*entry.slot);
entry.backend->SyncBuiltinSamplerToBackend(*entry.slot);
entry.backend->SyncMipmapsToBackend(*entry.slot);
@@ -1200,7 +1269,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_fboTextureSyncListFbo = currentFBO.get();
g_fboTextureSyncListSlotVersion = fboSlotVersion;
g_fboTextureSyncListObjectVersion = fboObjectVersion;
g_fboTextureSyncListContextId = MG_State::pGLContext->GetTextureContextId();
g_fboTextureSyncListContextId = keys.contextId;
g_fboTextureSyncListContextGeneration = g_textureContextGeneration;
}
} else {
@@ -1209,6 +1278,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
void SyncNeccessaryTextures() { SyncNeccessaryTextures(CaptureDrawTextureSyncKeys()); }
static Bool SupportsLayeredImageBinding(TextureTarget target) {
return target == TextureTarget::Texture3D || target == TextureTarget::TextureCubeMap ||
target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMapArray ||
@@ -1798,7 +1869,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
static const MG_State::GLState::ProgramObject* g_currentDrawFrontendProgram = nullptr;
static BackendProgramObjectImpl* g_currentDrawBackendProgram = nullptr;
void SyncCurrentProgram() {
// Memo of the per-draw enabled-draw-buffers walk feeding g_fragColorBroadcastCount:
// the answer is a pure function of WHICH FBO is bound and its draw-buffer edits, so
// it is keyed exactly like SyncCurrentFBO's synced trio - object pointer (identity),
// slot version (a recycled heap address cannot re-match: every real rebind bumps
// it), and object version (every attachment/draw-buffer/read-buffer edit bumps it,
// the same documented invariant the FBO sync memo already leans on). A null FBO
// keys on (nullptr, slot version, 0).
static const MG_State::GLState::FramebufferObject* g_broadcastMemoFbo = nullptr;
static Uint16 g_broadcastMemoSlotVersion = 0;
static Uint16 g_broadcastMemoObjectVersion = 0;
static Bool g_broadcastMemoValid = false;
static Uint g_broadcastMemoCount = 1;
void SyncCurrentProgram(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
@@ -1808,7 +1892,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_currentDrawFrontendProgram = nullptr;
g_currentDrawBackendProgram = nullptr;
auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
if (!currentProgram || !currentProgram->GetLinkStatus()) {
g_GLESFuncs.glUseProgram(0);
g_lastUsedBackendProgramId = 0;
@@ -1818,17 +1901,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
// only runs later in PrepareForDraw: a program compiled against a stale count
// would not be relinked until the draw after the one that needed it.
{
Uint enabledDrawBuffers = 0;
if (const auto& drawFBO =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject()) {
const auto& drawBuffers = drawFBO->GetDrawBuffers();
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
if (drawBuffers[i] != FramebufferAttachmentType::None) {
enabledDrawBuffers = i + 1;
const auto& drawSlot = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw);
const auto& drawFBO = drawSlot.GetBoundObject();
const Uint16 slotVersion = drawSlot.GetVersion();
const Uint16 objectVersion = drawFBO ? drawFBO->GetObjectVersion() : 0;
if (!g_broadcastMemoValid || g_broadcastMemoFbo != drawFBO.get() ||
g_broadcastMemoSlotVersion != slotVersion || g_broadcastMemoObjectVersion != objectVersion) {
Uint enabledDrawBuffers = 0;
if (drawFBO) {
const auto& drawBuffers = drawFBO->GetDrawBuffers();
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
if (drawBuffers[i] != FramebufferAttachmentType::None) {
enabledDrawBuffers = i + 1;
}
}
}
g_broadcastMemoFbo = drawFBO.get();
g_broadcastMemoSlotVersion = slotVersion;
g_broadcastMemoObjectVersion = objectVersion;
g_broadcastMemoCount = std::max<Uint>(enabledDrawBuffers, 1);
g_broadcastMemoValid = true;
}
g_fragColorBroadcastCount = std::max<Uint>(enabledDrawBuffers, 1);
g_fragColorBroadcastCount = g_broadcastMemoCount;
}
BackendProgramObjectImpl* twin = g_programTwinLookupMemo.Lookup(currentProgram);
@@ -1927,7 +2021,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
FramebufferImpl::g_fboSyncedObjects[(SizeT)target] = fbo.get();
}
static void BindCurrentProgramWithResources();
static void BindCurrentProgramWithResources(
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram);
static void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys,
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram);
void PrepareForDraw(DrawSyncBit syncBit) {
#ifdef TRACY_ENABLE
@@ -1940,13 +2037,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
VertexArrayImpl::BackendVertexArrayObject* vaoTwin =
currentVAO ? VertexArrayImpl::ResolveVaoTwin(currentVAO) : nullptr;
// One program resolve and one texture-key capture serve the whole draw, for
// the same reason the twin resolve does: only frontend GL entry points move
// either, and none can run inside this preparation. GetProgramForDraw is a
// cross-TU call with a guarded static inside - repeating it per stage showed
// up in draw-loop profiles.
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys();
BufferImpl::SyncNeccessaryBuffers(currentVAO, vaoTwin, syncBit & DrawSyncBit::IndexBuffer,
syncBit & DrawSyncBit::IndirectBuffer);
VertexArrayImpl::SyncCurrentVAO(currentVAO, vaoTwin);
TextureImpl::SyncNeccessaryTextures();
TextureImpl::SyncNeccessaryTextures(textureKeys);
FramebufferImpl::SyncCurrentFBO();
PrgramImpl::SyncCurrentProgram();
PrgramImpl::SyncCurrentProgram(currentProgram);
RenderStateImpl::SyncRenderState();
BindCurrentFBO(FramebufferTarget::Draw);
@@ -1962,10 +2066,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
VertexArrayImpl::SyncCurrentVertexAttributeValues(vaoTwin);
VertexArrayImpl::SyncCurrentVertexAttributeValues(vaoTwin, currentProgram);
BindCurrentTextures();
BindCurrentProgramWithResources();
BindCurrentTextures(textureKeys, currentProgram);
BindCurrentProgramWithResources(currentProgram);
// Last: opening the capture span needs the program current and the capture
// buffers bound, and ES rejects most binding changes once it is open.
@@ -2210,23 +2314,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
// sample whatever texture the last sync left behind (e.g. Flywheel's depth
// pyramid downsample reading a stale unit-0 binding instead of the depth
// attachment).
void BindCurrentTextures() {
static void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys,
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {
#ifdef TRACY_ENABLE
ZoneScopedNC("BindCurrentTextures", TRACY_ZONECOLOR_BACKEND);
#endif
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
// Units past the frontend's high-water mark have provably-empty slots.
const Int maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit();
const Int maxTouchedUnit = keys.maxTouchedUnit;
auto& memo = g_resolvedTextureBindingMemo;
const SizeT shadowBytes =
static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(TextureImpl::g_boundTexturesCache[0]);
const Uint64 unitBindingsEpoch = TextureImpl::CurrentUnitBindingsEpoch(maxTouchedUnit);
const Bool keysMatch = memo.valid && memo.glContextId == MG_State::pGLContext->GetTextureContextId() &&
const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch;
const Bool keysMatch = memo.valid && memo.glContextId == keys.contextId &&
memo.maxTouchedUnit == maxTouchedUnit &&
memo.unitBindingsEpoch == unitBindingsEpoch &&
memo.samplingResolutionGeneration ==
MG_State::pGLContext->GetSamplingResolutionGeneration() &&
memo.samplingResolutionGeneration == keys.samplingGeneration &&
memo.program == static_cast<const void*>(currentProgram.get()) &&
memo.programLifetimeId == (currentProgram ? currentProgram->GetLifetimeId() : 0) &&
memo.programBackendStateVersion ==
@@ -2239,10 +2342,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
shadowBytes) != 0) {
memo.valid = false;
if (ResolveAndBindUnitTextures(currentProgram, maxTouchedUnit)) {
memo.glContextId = MG_State::pGLContext->GetTextureContextId();
memo.glContextId = keys.contextId;
memo.maxTouchedUnit = maxTouchedUnit;
memo.unitBindingsEpoch = unitBindingsEpoch;
memo.samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
memo.samplingResolutionGeneration = keys.samplingGeneration;
memo.program = currentProgram.get();
memo.programLifetimeId = currentProgram ? currentProgram->GetLifetimeId() : 0;
memo.programBackendStateVersion = currentProgram ? currentProgram->GetBackendStateVersion() : 0;
@@ -2256,14 +2359,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
BindCurrentUnitSamplers(maxTouchedUnit);
}
void BindCurrentTextures() {
BindCurrentTextures(TextureImpl::CaptureDrawTextureSyncKeys(), MG_State::pGLContext->GetProgramForDraw());
}
// Binds the current program's backend object and re-establishes its per-program
// resources: global UBO contents, uniform-block bindings, and sampler uniform
// units (layout(binding=N) qualifiers are stripped from transpiled ESSL, so the
// association must be rebuilt through the API). Compute dispatches depend on
// this as much as draws do — e.g. Flywheel's cull shader reads the
// _FlwFrameUniforms block and the _flw_depthPyramid sampler.
static void BindCurrentProgramWithResources() {
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
static void BindCurrentProgramWithResources(
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {
if (currentProgram && currentProgram->GetLinkStatus()) {
#ifdef TRACY_ENABLE
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
@@ -2349,6 +2456,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// bindings are re-established (they follow frontend binding points).
const auto& blockBackendIndices = backendProgram.GetUniformBlockBackendIndices();
const auto uboCount = static_cast<Int>(blockBackendIndices.size());
// Pre-loop epoch read; the per-resource drawCleanEpoch stamps below
// follow the same stamp-the-pre-read-value rule as the VAO memo
// (Managers.h documents the contract and the mutation-site list).
const Uint64 bufferEpoch =
uboCount > 0 ? BufferImpl::CurrentBufferMutationEpoch() : 0;
Uint lastUBOBinding = 0; // binding 0 is reserved for the global UBO
for (Int i = 0; i < uboCount; ++i) {
++lastUBOBinding;
@@ -2363,7 +2475,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto range = point.GetRange();
if (bufferObj) {
auto* backendResource = BufferImpl::EnsureBufferResource(bufferObj);
// Clean-probe (or epoch-stamped) fast path before the full
// EnsureBufferResource: an unchanged static UBO needs no
// storage work, only its binding re-established below.
auto* backendResource = BufferImpl::GetBufferResource(bufferObj.get());
if (!backendResource || backendResource->drawCleanEpoch != bufferEpoch) {
if (backendResource &&
BufferImpl::IsBufferDrawClean(bufferObj.get(), backendResource)) {
backendResource->drawCleanEpoch = bufferEpoch;
} else {
backendResource = BufferImpl::EnsureBufferResource(bufferObj);
}
}
if (backendResource && backendResource->id != 0) {
// glBindBufferBase/Range set the generic GL_UNIFORM_BUFFER binding
// as a side effect, so no separate BindBufferId is needed here.
@@ -2607,12 +2730,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
BufferImpl::SyncComputeBuffers(includeDispatchIndirectBuffer);
TextureImpl::SyncNeccessaryTextures();
TextureImpl::SyncImageTextureBindings();
PrgramImpl::SyncCurrentProgram();
// Single per-dispatch program resolve and texture-key capture, as in
// PrepareForDraw (nothing below can move either).
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys();
BufferImpl::SyncComputeBuffers(includeDispatchIndirectBuffer);
TextureImpl::SyncNeccessaryTextures(textureKeys);
TextureImpl::SyncImageTextureBindings();
PrgramImpl::SyncCurrentProgram(currentProgram);
if (!currentProgram || !currentProgram->GetLinkStatus()) {
g_GLESFuncs.glUseProgram(0);
PrgramImpl::g_lastUsedBackendProgramId = 0;
@@ -2622,11 +2749,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Compute shaders sample textures through the same unit bindings as draws
// (e.g. Flywheel's depth-pyramid downsample reads the depth attachment on
// unit 0), so re-establish unit bindings after the content syncs above.
BindCurrentTextures();
BindCurrentTextures(textureKeys, currentProgram);
// Compute programs need the same per-program resource sync as draws:
// uniform-block bindings and sampler units only exist through the API
// because layout(binding) is stripped from the transpiled ESSL.
BindCurrentProgramWithResources();
BindCurrentProgramWithResources(currentProgram);
}
GLuint GetBackendProgramId(GLuint program) {
@@ -5163,6 +5290,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<SizeT>(width) * sizeof(Float));
}
}
if (pixelPackBufferObject) {
// WritebackFromBackend bumps change serials with no backend op; re-open
// the buffer draw-clean memos (once for the whole row loop).
BufferImpl::BumpBufferMutationEpoch();
}
return true;
}
@@ -5284,6 +5416,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
Memcpy(static_cast<Uint8*>(pixels) + rowOffset, rowBuf.data(), rowBytes);
}
}
if (pixelPackBufferObject) {
// Serial bumps with no backend op; re-open the buffer draw-clean memos.
BufferImpl::BumpBufferMutationEpoch();
}
return true;
}
@@ -5798,6 +5934,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("ReadPixels: Copying data from PBO to client memory");
SizeT size = pixelPackBufferObject->GetSize();
pixelPackBufferObject->WritebackFromBackend({pboMappedPtr, size}, 0);
// Serial bumped with no backend op; re-open the buffer draw-clean memos.
BufferImpl::BumpBufferMutationEpoch();
MGLOG_D("ReadPixels: Unmapping PBO");
g_GLESFuncs.glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
} else {
@@ -6089,6 +6227,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("ReadPixels: Copying data from PBO to client memory");
SizeT size = pixelPackBufferObject->GetSize();
pixelPackBufferObject->WritebackFromBackend({pboMappedPtr, size}, 0);
// Serial bumped with no backend op; re-open the buffer draw-clean memos.
BufferImpl::BumpBufferMutationEpoch();
MGLOG_D("ReadPixels: Unmapping PBO");
g_GLESFuncs.glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
} else {
@@ -6476,6 +6616,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
};
}
// Defined next to the stamp globals below; every owner-id writer must reset the
// EGL verification stamp BEFORE publishing the new owner.
void InvalidateEglVerifiedStamp();
Bool MakeCurrent() {
if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE ||
g_Context == EGL_NO_CONTEXT) {
@@ -6487,6 +6631,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_E("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
return false;
}
InvalidateEglVerifiedStamp();
g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release);
// The ops table may have been unregistered when a previous ES context was
// destroyed (e.g. a probe context); re-register now that GL is usable.
@@ -6506,6 +6651,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool ReleaseCurrent() {
if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY) {
InvalidateEglVerifiedStamp();
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
return true;
}
@@ -6517,19 +6663,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Clearing the global owner works from ANY thread (a release request can
// legally arrive on a thread other than the current owner); erring towards
// "not current" only defers buffer ops, which is always safe.
InvalidateEglVerifiedStamp();
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
return true;
}
namespace {
// EGL ground-truth verification stamp, per thread. glvnd's
// eglGetCurrentContext performs fork detection with a real getpid()
// syscall on every call, and this predicate sits 2-3 deep in every
// draw - measured at 16% of the render thread on a live workload.
thread_local Uint64 t_eglVerifiedFrameSerial = ~0ull;
thread_local Uint t_eglVerifiedContextGeneration = 0;
// EGL ground-truth verification stamp. glvnd's eglGetCurrentContext performs
// fork detection with a real getpid() syscall on every call, and this
// predicate sits 2-3 deep in every draw - measured at 16% of the render
// thread on a live workload. NOT thread_local although the stamp is
// per-owner-thread state: a shared-library thread_local costs a
// __tls_get_addr call per access, which itself showed at 1.5% of the draw
// loop. Plain globals are equivalent because only the thread that passes the
// owner-identity check below can ever stamp or trust them, only one thread
// can be the owner at a time, and the ONLY writers of the owner id -
// MakeCurrent/ReleaseCurrent - reset the stamps before publishing a new
// owner, so a stamp can never leak across an ownership change. Atomics
// (relaxed) because the resets may come from a non-owner thread; the
// owner-id release/acquire pairing orders them.
std::atomic<Uint64> g_eglVerifiedFrameSerial{~0ull};
std::atomic<Uint> g_eglVerifiedContextGeneration{0};
} // namespace
void InvalidateEglVerifiedStamp() {
// ~0 frame serial matches no real frame, so the next owner-thread call
// re-verifies against EGL itself.
g_eglVerifiedFrameSerial.store(~0ull, std::memory_order_relaxed);
g_eglVerifiedContextGeneration.store(0, std::memory_order_relaxed);
}
Bool IsBackendContextCurrentOnThisThread() {
if (g_Context == EGL_NO_CONTEXT) {
return false;
@@ -6540,20 +6703,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Belt and braces: EGL itself is the ground truth. A migration that bypassed
// MakeCurrent()/ReleaseCurrent() must not leave a stale ownership claim
// standing, or GL calls would silently no-op while shadow bookkeeping (bind
// cache, synced serials) still advances. Re-verify once per (thread, frame,
// context generation) rather than per call: an external migration is caught
// at the next frame boundary instead of the next call, which recovers the
// cache, synced serials) still advances. Re-verify once per (frame, context
// generation) rather than per call: an external migration is caught at the
// next frame boundary instead of the next call, which recovers the
// bookkeeping just the same, without paying a syscall on every draw.
const Uint64 frameSerial = g_currentFrameSerial.load(std::memory_order_relaxed);
if (t_eglVerifiedFrameSerial == frameSerial &&
t_eglVerifiedContextGeneration == g_syncContextGeneration) {
if (g_eglVerifiedFrameSerial.load(std::memory_order_relaxed) == frameSerial &&
g_eglVerifiedContextGeneration.load(std::memory_order_relaxed) == g_syncContextGeneration) {
return true;
}
if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != g_Context) {
return false;
}
t_eglVerifiedFrameSerial = frameSerial;
t_eglVerifiedContextGeneration = g_syncContextGeneration;
g_eglVerifiedFrameSerial.store(frameSerial, std::memory_order_relaxed);
g_eglVerifiedContextGeneration.store(g_syncContextGeneration, std::memory_order_relaxed);
return true;
}
+65 -7
View File
@@ -281,6 +281,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// an older generation hold ids from a dead context.
Uint g_bufferContextGeneration = 1;
// Buffer-mutation epoch backing store (contract, mutation-site list and
// memory-ordering rules: Managers.h at the accessor declarations).
// Starts at 1 so the memo stamps' 0 means "never stamped". Atomic:
// frontend buffer ops may run on non-draw threads while the draw thread
// reads; the release-bump-AFTER-mutation / acquire-read-BEFORE-probes
// pairing makes a stamp taken against stale state impossible to consume.
std::atomic<Uint64> g_bufferMutationEpoch{1};
// Defined next to the indexed-binding shadow below; forward-declared so
// every glDeleteBuffers site in this namespace can scrub stale shadow
// entries (GL resets a deleted buffer's bindings - indexed and pixel
@@ -693,24 +701,71 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_hasDeferredBufferReleases.store(true, std::memory_order_release);
}
// Epoch-tracking wrappers: every op bumps the buffer-mutation epoch AFTER
// its impl returns (release; see Managers.h for why the order matters),
// covering every mutation branch inside - including the early returns
// that only queued pendingRanges or flagged pendingRespecify. Bumping on
// an op that turned out to be a no-op merely re-runs the probes once.
void Ops_RespecifyTracked(BufferObject& bufferObject) {
Ops_Respecify(bufferObject);
BumpBufferMutationEpoch();
}
void Ops_SubDataTracked(BufferObject& bufferObject, SizeT offset, SizeT size) {
Ops_SubData(bufferObject, offset, size);
BumpBufferMutationEpoch();
}
void Ops_FlushMappedRangeTracked(BufferObject& bufferObject, Range1D range,
Flags<BufferMappingAccessBit> appAccess) {
Ops_FlushMappedRange(bufferObject, range, appAccess);
BumpBufferMutationEpoch();
}
void Ops_OnDestroyTracked(SharedPtr<BackendBufferResource>&& resource) {
Ops_OnDestroy(std::move(resource));
BumpBufferMutationEpoch();
}
void* Ops_AcquirePersistentMapTracked(BufferObject& bufferObject) {
void* result = Ops_AcquirePersistentMap(bufferObject);
// Bump even on decline: the frontend still enters a persistent map the
// per-draw probes must start seeing (IsMapped-driven range pushes).
BumpBufferMutationEpoch();
return result;
}
void Ops_ReadbackFromGpuTracked(BufferObject& bufferObject) {
Ops_ReadbackFromGpu(bufferObject);
BumpBufferMutationEpoch();
}
const BufferBackendOps g_glesBufferBackendOps = {
.Respecify = Ops_Respecify,
.SubData = Ops_SubData,
.FlushMappedRange = Ops_FlushMappedRange,
.OnDestroy = Ops_OnDestroy,
.AcquirePersistentMap = Ops_AcquirePersistentMap,
.ReadbackFromGpu = Ops_ReadbackFromGpu,
.Respecify = Ops_RespecifyTracked,
.SubData = Ops_SubDataTracked,
.FlushMappedRange = Ops_FlushMappedRangeTracked,
.OnDestroy = Ops_OnDestroyTracked,
.AcquirePersistentMap = Ops_AcquirePersistentMapTracked,
.ReadbackFromGpu = Ops_ReadbackFromGpuTracked,
};
} // namespace
Uint64 CurrentBufferMutationEpoch() {
return g_bufferMutationEpoch.load(std::memory_order_acquire);
}
void BumpBufferMutationEpoch() {
g_bufferMutationEpoch.fetch_add(1, std::memory_order_release);
}
void RegisterBufferBackendOps() {
MG_State::GLState::SetBufferBackendOps(&g_glesBufferBackendOps);
// Frontend writes issued while ops were unregistered advanced change
// serials with no per-op bump; re-open every draw-clean memo.
BumpBufferMutationEpoch();
}
void UnregisterBufferBackendOps() {
if (MG_State::GLState::GetBufferBackendOps() == &g_glesBufferBackendOps) {
MG_State::GLState::SetBufferBackendOps(nullptr);
}
// From here on frontend writes bypass the tracked ops entirely.
BumpBufferMutationEpoch();
InvalidateArrayBufferBindingCache();
// Pooled ids belong to the dying context too; drop them without glDeleteBuffers.
ClearBufferPool();
@@ -721,8 +776,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
void OnBackendContextDestroyed() {
UnregisterBufferBackendOps();
UnregisterBufferBackendOps(); // also bumps the buffer-mutation epoch
++g_bufferContextGeneration;
// The generation moved AFTER the unregister bump above; re-open the
// memos again so no stamp can predate the generation change.
BumpBufferMutationEpoch();
InvalidateArrayBufferBindingCache();
InvalidateIndexedBufferBindingCache();
InvalidatePixelBufferBindingCaches();
+84
View File
@@ -120,6 +120,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace BufferImpl {
const GLenum TempBufferTarget = GL_ARRAY_BUFFER;
// --- Buffer-mutation epoch -------------------------------------------------
// Manager-wide monotonic counter: it moves whenever ANY buffer resource may
// have gone from draw-clean to dirty. Draw-path memos read it once per pass
// (CurrentBufferMutationEpoch, acquire), re-run their IsBufferDrawClean
// probes only when it moved, and stamp the PRE-pass value after a pass in
// which every probe came up clean - so a concurrent bump lands strictly
// after the stamped value and forces a re-probe on the next pass no matter
// how the probe interleaved with the mutation. Conservative-correct: a bump
// never skips work, it only re-runs the probes once.
//
// Every clean->dirty transition path bumps it (BumpBufferMutationEpoch,
// release, AFTER the mutation lands so an acquire reader that still sees
// the old epoch cannot have missed the mutation):
// * the frontend BufferBackendOps table - Respecify, SubData,
// FlushMappedRange, AcquirePersistentMap, ReadbackFromGpu, OnDestroy -
// which every frontend change-serial bump and every pending-range
// queueing reaches while ops are registered (upload, orphan/respecify,
// map flush/unmap writeback, persistent-map adoption, delete/pooling);
// * backend-initiated shadow writebacks that bump the frontend change
// serial without an op: transform-feedback capture readback
// (XfbImpl::ReadbackCapturedRanges and the scatter path) and every
// pack-PBO WritebackFromBackend site (glReadPixels/glGetTexImage);
// * RegisterBufferBackendOps/UnregisterBufferBackendOps - while ops are
// unregistered, frontend writes advance serials silently, so both edges
// of that window re-open every memo;
// * OnBackendContextDestroyed - the buffer context generation moved, so
// every previously clean resource is invalid.
// NOT bumped (cleanliness provably unchanged): MarkGpuWritten (the backend
// copy is authoritative; IsBufferDrawClean does not consult it),
// NotifyContentWrite on a GPU-resident buffer (persistent-mapped resources
// are clean by construction), and EnsureBufferResource itself (it only
// repairs toward clean). A non-persistent map (draws on it are GL errors
// the frontend rejects) sets IsMapped without an op; persistent maps reach
// AcquirePersistentMap or (FLUSH_EXPLICIT) publish only via FlushMappedRange.
Uint64 CurrentBufferMutationEpoch();
void BumpBufferMutationEpoch();
// The DirectGLES storage behind one frontend buffer. Owned (refcounted) by
// the frontend BufferObject; immediate BufferBackendOps keep it current, so
// draw-time "sync" reduces to ensuring the storage exists.
@@ -145,6 +182,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool pendingRespecify = false;
VecRange1D pendingRanges;
std::mutex pendingMutex;
// Buffer-mutation epoch (see CurrentBufferMutationEpoch) at which this
// resource last probed IsBufferDrawClean == true, 0 = never (epochs start
// at 1). Written only on the draw thread; per-draw resource consumers
// (the UBO binding walk) skip the probe while their pre-pass epoch read
// matches, exactly like the per-VAO memo stamps.
Uint64 drawCleanEpoch = 0;
// Zero-copy coherent persistent map (EXT_buffer_storage): the GL store is
// immutable, persistently+coherently mapped, and persistentPtr is what the app
// (and the frontend PipeResource) write into directly. While set, draw-time
@@ -290,6 +333,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
Array<Entry, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> entries;
MG_State::GLState::BufferObject* iboFrontend = nullptr;
BufferImpl::GLESBufferResource* iboResource = nullptr;
// Buffer-mutation epoch (BufferImpl::CurrentBufferMutationEpoch) at which
// the LAST probe pass found every entry / the IBO clean; 0 = not stamped
// (epochs start at 1). While a stamp matches the pre-pass epoch read, the
// probes are skipped outright: any path that can dirty ANY buffer bumps
// the epoch (the exhaustive site list lives at the epoch declaration).
// The IBO stamp is only trusted together with the bound-object identity
// compare - the VAO's index slot can rebind with no epoch or config move.
Uint64 vboCleanEpoch = 0;
Uint64 iboCleanEpoch = 0;
};
ResolvedDrawBuffers& GetResolvedDrawBuffersMemo() { return m_resolvedDrawBuffers; }
@@ -434,6 +486,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
void Bind(GLenum target, Uint unit = TempTextureUnit);
Uint GetBackendTextureId() const;
// Aggregate first-level clean gate for the per-draw trio
// SyncTextureParamsToBackend + SyncBuiltinSamplerToBackend +
// SyncMipmapsToBackend: EXACTLY the conjunction of their own early-outs
// (params version == synced params version; builtin-sampler version ==
// synced sampler version; and SyncMipmapsToBackend's cheap gate - stamped
// trio + content version + Mipmap storage). True means each of the three
// would provably return without work, so the caller may skip the calls;
// false only falls through to the three calls, whose own gates re-decide
// individually - this gate must never be MORE permissive than they are.
// `contextId`/`samplingGeneration` are the frontend context's current
// values, hoisted by the caller so a per-draw list walk reads them once
// instead of per texture. `t` must be the live frontend texture.
Bool IsDrawSyncClean(const MG_State::GLState::ITextureObject* t, Uint64 contextId,
Uint64 samplingGeneration) const {
if (!m_isInitialized || m_syncedShapeContextId == 0 || m_syncedShapeContextId != contextId ||
m_syncedShapeGeneration != samplingGeneration) {
return false;
}
const Uint16 paramsVersion = t->GetTextureParamsVersion();
if (m_syncedShapeParamsVersion != paramsVersion || m_syncedTextureParamsVersion != paramsVersion) {
return false;
}
if (m_syncedContentVersion == 0 || m_syncedContentVersion != t->GetContentVersion()) {
return false;
}
const auto& samplerObject = t->GetSamplerObject();
if (!samplerObject || m_syncedSamplerVersion != samplerObject->GetVersion()) {
return false;
}
return t->GetStorageType() == TextureStorageType::Mipmap;
}
private:
void RecreateBackendTexture();
+5
View File
@@ -1107,6 +1107,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
}
if (pixelPackBufferObject) {
// WritebackFromBackend bumps change serials with no backend op; re-open
// the buffer draw-clean memos (once for the whole row loop).
BufferImpl::BumpBufferMutationEpoch();
}
return true;
}
} // namespace ReadbackImpl